公众号:Hoeller,有精品面试题(不是很多,150道,但很经典)
前几天发了一篇关于ThreadLocal和ScopedValue的文章,发现很多同学对ThreadLocal在实战中一般会用在那些场景不太清楚,所以这篇文章我结合我看过的源码来给大家总结一下。
第一种场景:Spring事务
先看第一种场景,直接上代码:
这是再简单不过的Spring事务的代码,那么执行上面两个SQL的数据连接是谁创建的呢?
答案是Spring,而不是JdbcTemplate或Mybatis。为什么?假如数据库连接是JdbcTemplate创建的,它是不会去修改数据库连接的autocommit
属性的,这个属性的值将是true,那么执行完一个SQL就会自动提交,等到抛NullPointerException
时,由于SQL已经提交了也就不会回滚了,也就没有达到Spring事务想要的效果。
所以,Spring需要自己创建数据库连接,并将它的autocommit
属性改为false,并且JdbcTemplate或者Mybatis需要拿到Spring所创建的数据库连接,这样执行SQL时才不会自动提交,等到执行完整个方法后,Spring才判断是需要回滚还是提交,反正Connection连接对象是Spring自己创建的,所以Spring自己调用Connection的commit()
或rollback()
方法即可。
Spring为了能够让JdbcTemplate或者Mybatis拿到数据库连接,所以会将数据库连接存到ThreadLocal中,JdbcTemplate或者Mybatis从ThreadLocal获取即可。
第二种场景:分布式事务
再来看第二种场景,一般分布式事务的设计中,都会包含两种ID:全局ID和分支ID。
每个微服务需要创建自己的分支id(branchId),但是微服务A作为全局事务的发起者,它还要负责创建一个表示该全局事务的id,也就是全局id(globalId),并且微服务A需要将全局id传递给下游,从而使得微服务B和微服务C知道自己的分支事务是属于哪个全局事务的。
如何传递呢?比如调用微服务的是http请求,则把全局id添加到请求头即可,如果是其他rpc协议,比如dubbo协议,也可以利用它的扩展机制将全局id添加到请求数据中传递给下游服务。
重点在于下游微服务B如何接收全局id呢?像Seata中用的就是mvc拦截器,拦截请求取出请求头中的全局id,并保存到ThreadLocal中,后续逻辑中,比如注册分支事务到全局事务管理器时,直接从ThreadLocal中取出全局id即可。
第三种场景:AopContext和RpcContext
在Spring Aop中,如果想要拿到代理对象,可以通过以下代码拿到:
其中核心就是AopContext.currentProxy()
,它能够拿到当前在调用test()
方法的代理对象,而它的内部用的也是ThreadLocal,比如该方法的源码为:
其中currentProxy是一个常量为:
其中NamedThreadLocal是Spring自己定义的:
和AopContext类似的还有Dubbo中的RpcContext,Dubbo服务被调用时可以通过RpcContext拿到服务消费者的URL等信息,它们的共同点是:方便在业务代码中获取框架内部生成的对象。
第四种场景:SpringBoot 3.0
SpringBoot3.0中在SpringBoot启动过程中新增了一个机制,叫做:SpringApplicationHook,通过这个机制可以用来在项目启动时设置启动监听器,比如:
withHook()
方法有两个参数,第一个参数是一个SpringApplicationHook对象,第二个参数是一个Runnable对象,都是函数式接口,所以上面代码都传入的Lambda表达式,上面代码运行时会先启动SpringBoot,在启动的最后会执行started()
方法中的打印语句。
而withHook()
方法会把SpringApplicationHook对象设置到ThreadLocal中,在SpringBoot启动过程的最后会从ThreadLocal中取出SpringApplicationHook对象并执行。
自然,肯定还有很多场景和源码中会用到ThreadLocal,大家可以把自己遇到的分享在评论区哦。
我是爱读源码的大都督周瑜,欢迎关注我的公众号:Hoeller。公众号里有跟多高质量干货系列文章和精品面试宝典。