一.ThreadLocal介绍html
二.使用场景1——数据库事务问题java
2.1 问题背景算法
2.2 方案1-修改接口传参sql
2.3 方案2-使用ThreadLocal数据库
三.使用场景2——日志追踪问题api
四.其余使用场景cookie
咱们知道,变量从做用域范围进行分类,能够分为“全局变量”、“局部变量”两种:session
1.全局变量(global variable),好比类的静态属性(加static关键字),在类的整个生命周期都有效;多线程
2.局部变量(local variable),好比在一个方法中定义的变量,做用域只是在当前方法内,方法执行完毕后,变量就销毁(释放)了;并发
使用全局变量,当多个线程同时修改静态属性,就容易出现并发问题,致使脏数据;而局部变量通常来讲不会出现并发问题(在方法中开启多线程并发修改局部变量,仍可能引发并发问题);
再看ThreadLocal,从名称上就能知道,它能够用来保存局部变量,只不过这个“局部”是指“线程”做用域,也就是说,该变量在该线程的整个生命周期中有效。
下面介绍示例,UserService调用UserDao删除用户信息,涉及到两张表的操做,因此用到了数据库事务:
数据库封装类DbUtils
public class DbUtils { // 使用C3P0链接池 private static ComboPooledDataSource dataSource = new ComboPooledDataSource("dev"); public static Connection getConnectionFromPool() throws SQLException { return dataSource.getConnection(); } // 省略其余方法..... }
UserService代码以下:
public class UserService { private UserDao userDao; public void deleteUserInfo(Integer id, String operator) { Connection connection = null; try { // 从链接池中获取一个链接 connection = DbUtils.getConnectionFromPool(); // 由于涉及事务操做,因此须要关闭自动提交 connection.setAutoCommit(false); // 事务涉及两步操做,删除用户表,增长操做日志 userDao.deleteUserById(id); userDao.addOperateLog(id, operator); connection.commit(); } catch (SQLException e) { // 回滚操做 try { if (connection != null) { connection.rollback(); } } catch (SQLException ex) { } } finally { DbUtils.freeConnection(connection); } } }
下面是UserDao,省略了部分代码:
package cn.ganlixin.dao; import cn.ganlixin.util.DbUtils; import java.sql.Connection; /** * @author ganlixin * @create 2020-06-12 */ public class UserDao { public void deleteUserById(Integer id) { // 从链接池中获取一个数据链接 Connection connection = DbUtils.getConnectionFromPool(); // 利用获取的数据库链接,执行sql...........删除用户表的一条数据 // 归还链接给链接池 DbUtils.freeConnection(connection); } public void addOperateLog(Integer id, String operator) { // 从链接池中获取一个数据链接 Connection connection = DbUtils.getConnectionFromPool(); // 利用获取的数据库链接,执行sql...........插入一条记录到操做日志表 // 归还链接给链接池 DbUtils.freeConnection(connection); } }
上面的代码乍一看,好像没啥问题,可是仔细看,实际上是存在问题的!!问题出在哪儿呢?就出在从数据库链接池获取链接哪一个位置。
1.UserService会从数据库链接池获取一个链接,关闭该链接的自动提交;
2.UserService而后调用UserDao的两个接口进行数据库操做;
3.UserDao的两个接口,都会从数据库链接池获取一个链接,而后执行sql;
注意,第1步和第3步得到的链接不必定是同一个!!!!这才是关键。
若是UserService和UserDao获取的数据库链接不是同一个,那么UserService中关闭自动提交的数据库链接,并非UserDao接口中执行sql的数据库链接,当userService中捕获异常,即便执行rollback,userDao中的sql已经执行完了,并不会回滚,因此数据已经出现不一致!!!
上面的例子中,由于UserService和UserDao获取的链接不是同一个,因此并不能保证事务原子性;那么只要可以解决这个问题,就能够保证了
能够修改userDao中的代码,不要每次在UserDao中从数据库链接池获取链接,而是增长一个参数,该参数就是数据库链接,有UserService传入,这样就能保证UserService和UserDao使用同一个数据库链接了
public class UserDao { public void deleteUserById(Connection connection, Integer id) { // 利用传入的数据库链接,执行sql...........删除用户表的一条数据 } public void addOperateLog(Connection connection, Integer id, String operator) { // 利用传入的数据库链接,执行sql...........插入一条记录到操做日志表 } }
UserService调用接口时,传入数据库链接,修改代码后以下:
// 事务涉及两步操做,删除用户表,增长操做日志 // 新增参数传入数据库链接,保证UserService和UserDao使用同一个链接 userDao.deleteUserById(connection, id); userDao.addOperateLog(connection, id, operator);
这样作,的确是能解决数据库事务的问题,可是并不推荐这样作,耦合度过高,不利于维护,修改起来也不方便;
ThreadLocal能够保存当前线程有效的变量,正好适合解决这个问题,并且改动的点也特别小,只须要在DbUtils获取链接的时候,将获取到的链接存到ThreadLocal中便可:
public class DbUtils { // 使用C3P0链接池 private static ComboPooledDataSource dataSource = new ComboPooledDataSource("dev"); // 建立threadLocal对象,保存每一个线程的数据库链接对象 private static ThreadLocal<Connection> threadLocal = new ThreadLocal<>(); public static Connection getConnectionFromPool() throws SQLException { if (threadLocal.get() == null) { threadLocal.set(dataSource.getConnection()); } return threadLocal.get(); } // 省略其余方法..... }
而后UserService和UserDao中,恢复最初的版本,UserService和UserDao中都调用DbUtils获取数据库链接,此时他们获取到的链接则是同一个Connection对象,就能够解决数据库事务问题了。
若是理解了场景1的数据库事务问题,那么对于本小节的日志追踪,光看标题就知道是怎么回事了;
开发过程时,会在项目中打不少的日志,通常来讲,查看日志的时候,都是经过关键字去找日志,这就须要咱们在打日志的时候明确的写入某些标识,好比用户ID、订单号、流水号...
若是业务比较复杂,那么一个请求的处理流程就会比较长,若是将这么一长串的流程给串起来,也能够经过前面说的用户ID、订单号、流水号来串,但有个问题,某些接口没有用户ID或者订单号做为参数!!!!这个时候,固然能够像场景1中给接口增长用户ID或者订单号做为参数,可是这样实现起来,除非想被炒鱿鱼,不然就别这样作。
此时能够就可使用ThreadLocal,封装一个工具类,提供惟一标识(能够是用户ID、订单号、或者是分布式全局ID),示例以下:
package cn.ganlixin.util; /** * 描述: * 日志追踪工具类,设置和获取traceId, * 此处的traceId使用snowFlake雪花数算法,详情能够参考:https://www.cnblogs.com/-beyond/p/12452632.html * * @author ganlixin * @create 2020-06-12 */ public class TraceUtils { // 建立ThreadLocal静态属性,存Long类型的uuid private static final ThreadLocal<Long> threadLocal = new ThreadLocal<>(); // 全局id生成器(雪花数算法) private static final SnowFlakeIdGenerator generator = new SnowFlakeIdGenerator(1, 1); public static void setUuid(String uuid) { // 雪花数算法 threadLocal.set(generator.nextId()); } public static Long getUuid() { if (threadLocal.get() == null) { threadLocal.set(generator.nextId()); } return threadLocal.get(); } }
使用示例:
@Slf4j public class UserService { private UserDao userDao; public void deleteUserInfo(Integer id, String operator) { log.info("traceId:{}, id:{}, operator:{}", TraceUtils.getUuid(), id, operator); //..... } } @Slf4j public class UserDao { public void deleteUserById(Connection connection, Integer id) { log.info("traceId:{}, id:{}", TraceUtils.getUuid(), id); } }
其余场景,其实就是利用ThreadLocal“线程私有且线程间互不影响”特性,除了上面的两个场景,常见的还有用来记录用户的登陆状态(固然也能够用session或者cookie实现)。