ThreadLocal的使用场景分析
目录
2.1 问题背景
2.2 方案1-修改接口传参
四.其他使用场景
一.ThreadLocal介绍
我们知道,变量从作用域范围进行分类,可以分为“全局变量”、“局部变量”两种:
1.全局变量(global variable),比如类的静态属性(加static关键字),在类的整个生命周期都有效;
2.局部变量(local variable),比如在一个方法中定义的变量,作用域只是在当前方法内,方法执行完毕后,变量就销毁(释放)了;
使用全局变量,当多个线程同时修改静态属性,就容易出现并发问题,导致脏数据;而局部变量一般来说不会出现并发问题(在方法中开启多线程并发修改局部变量,仍可能引起并发问题);
再看ThreadLocal,从名称上就能知道,它可以用来保存局部变量,只不过这个“局部”是指“线程”作用域,也就是说,该变量在该线程的整个生命周期中有效。
二.使用场景1——数据库事务问题
2.1问题背景
下面介绍示例,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已经执行完了,并不会回滚,所以数据已经出现不一致!!!
2.2方案1-修改接口传参
上面的例子中,因为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);
这样做,的确是能解决数据库事务的问题,但是并不推荐这样做,耦合度太高,不利于维护,修改起来也不方便;
2.3使用ThreadLocal解决
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对象,就可以解决数据库事务问题了。
三.使用场景2——日志追踪问题
如果理解了场景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实现)。
ThreadLocal的使用场景分析的更多相关文章
- ThreadLocal的理解与应用场景分析
对于Java ThreadLocal的理解与应用场景分析 一.对ThreadLocal理解 ThreadLocal提供一个方便的方式,可以根据不同的线程存放一些不同的特征属性,可以方便的在线程中进行存 ...
- TYPESDK手游聚合SDK服务端设计思路与架构之一:应用场景分析
TYPESDK 服务端设计思路与架构之一:应用场景分析 作为一个渠道SDK统一接入框架,TYPESDK从一开始,所面对的需求场景就是多款游戏,通过一个统一的SDK服务端,能够同时接入几十个甚至几百个各 ...
- Oracle dbms_lock.sleep()存储过程使用技巧-场景-分析-实例
<Oracle dbms_lock.sleep()存储过程使用技巧>-场景-分析-实例 摘要:今天是2014年3月10日,北京,雾霾,下午组织相关部门开会.会议的结尾一名开发工程师找到了我 ...
- 理解 python metaclass使用技巧与应用场景分析
理解python metaclass使用技巧与应用场景分析 参考: decorator与metaclass:http://jfine-python-classes.readthedocs. ...
- 数据结构之链表C语言实现以及使用场景分析
牢骚:本篇博客两个星期前已经存为草稿,鉴于发生一些糟糕的事情,今天才基本完成.本人6月份应届毕业生一枚,毕业后当天来到帝都,之后也非常顺利,面试了俩家公司都成功了.一家做C++方面电商ERP,一家做w ...
- mariadb 10 多源复制(Multi-source replication) 业务使用场景分析,及使用方法
mariadb 10 多源复制(Multi-source replication) 业务使用场景分析,及使用方法 官方mysql一个slave只能对应一个master,mariadb 10开始支持多源 ...
- Java 常用List集合使用场景分析
Java 常用List集合使用场景分析 过年前的最后一篇,本章通过介绍ArrayList,LinkedList,Vector,CopyOnWriteArrayList 底层实现原理和四个集合的区别.让 ...
- 068——VUE中vuex的使用场景分析与state购物车实例
<!DOCTYPE html> <html lang="en"> <head> <meta charset="UTF-8&quo ...
- EasyNVR智能云终端硬件使用场景分析:如何实现软硬一体的视频上云整体解决方案
背景分析 在于众多的客户交流中,经常会被客户问到,"EasyNVR到底是软件还是硬件?"."EasyNVR能否出一个硬件的版本,摆脱自建服务器的压力?".&qu ...
随机推荐
- 解惑求助-关于NetCore2.2中间件响应的问题
背景介绍:基于netcore2.2开发api接口程序,自定义了一个异常捕获中间件,用于捕获未经处理的异常以及状态码404.500等访问(设计的出发点就是,出现了非200的响应,我这边全部会进行处理成2 ...
- CF916C
题目链接:http://codeforces.com/contest/916/problem/C 题目大意: 用\(n\)个结点,\(m\)条边(每条边的权值范围为[1, 109]) 构造出一个无向带 ...
- 一文带你学会基于SpringAop实现操作日志的记录
前言 大家好,这里是经典鸡翅,今天给大家带来一篇基于SpringAop实现的操作日志记录的解决的方案.大家可能会说,切,操作日志记录这么简单的东西,老生常谈了.不! 网上的操作日志一般就是记录操作人, ...
- vue采用history路由的服务器部署问题
发现部署问题 在部署的时候发现打开的页面是空白 之前部署原理 之前的页面都是作为静态文件形式打包上传到服务器上 http://www.xiedashuaige.cn/bolg2.0/#/home 就和 ...
- python九九乘法表程序代码
按照c语言的思路来考虑python的,方法很简单,直接运用双重循环即可,本代码为了代码量少采用的是while嵌套双循环. 取两个随机变量 (1)i和j都从1开始(因为表中最小数值为1) (2)i控制第 ...
- 【译】Gartner CWPP市场指南
https://www.gartner.com/doc/reprints?id=1-1YSHGBQ8&ct=200416&st=sb?utm_source=marketo&ut ...
- parrot os vm镜像failed to fetch cdrom apt-get update的问题
vi /etc/apt/sources.list 注释掉第一行 cdrom x保存就可以了
- 手把手教你学numpy,从此数据处理不再慌【三】
本文始发于个人公众号:TechFlow,原创不易,求个关注 今天是numpy专题的第三篇,我们来聊聊numpy当中的索引. 上篇的末尾其实我们简单地提到了索引,但是没有过多深入.没有过多深入的原因也很 ...
- Spring Cloud 系列之 Apollo 配置中心(一)
背景 随着程序功能的日益复杂,程序的配置日益增多:各种功能的开关.参数的配置.服务器的地址等等. 对程序配置的期望值也越来越高:配置修改后实时生效,灰度发布,分环境.分集群管理配置,完善的权限.审核机 ...
- 跨域解决方案 - JSONP
目录 1. 定义 2. JSONP 解决跨域 3. 应用场景 4. 代码演示 1. 定义 在HTML 中, script 标签有两个个性质: script 标签可以不受同源策略的限制去访问服务器资源, ...