SpringBoot异步进程池的ThreadContext复用
记一次工作项目遇到的进程池ThreadContext复用问题。
遇到的问题
最近在进行某个SpringBoot项目改造中,遇到了需要动态切换数据源的操作。这部分交给我的徒弟来做,她的实现方式如下:继承org.springframework.jdbc.datasource.lookup.AbstractRoutingDataSource
,实现了一个自定义动态数据源DynamicDataSource
,重写determineCurrentLookupKey()
这个方法用于从ThreadContext
获取当前进程所需要使用的数据源Key。
查看源码发现,ThreadContext
是公司内自研框架对于ThreadLocal
的封装,用于快速存取进程内上下文。
由于项目原有功能使用的都是主数据库,这次项目改造需要为新增模块配置一个副数据库,因此在改造时设置了默认数据源为主库,只在副库的逻辑代码里添加了对ThreadContext
赋值的操作。实际测试中也未发现问题,项目原有功能因为不指定数据库,从ThreadContext
中获取的数据库Key为null,默认使用主数据库;新增模块内因为全部显式指定了副库的Key,都正常获取了副库内的数据。
然而业务上线后不到一周的时间内就出现了问题:项目新增模块运行都正常,但是原有功能模块会向副库发起查询。万幸的是因为两个库的表名表结构均不相同,请求库错误时会直接抛出异常,没有造成客户的资金损失。
问题排查与复现
因为项目原有代码在改造时无任何变化,从新增的多数据源切换代码部分进行排查。
一开始的思考方向是数据库事务造成的多数据源切换失败。但是经过排查,涉及事务的方法并未受影响,报错集中在特定几个使用了@Async
的异步方法上。因为新增模块也涉及到和原有功能使用同一个异步进程池,因此思考可能是异步进程数据源共用的问题。
经过查找公司内自研框架的材料,看到开发人员的如下解答:
- "使用者必须考虑线程内部的清理工作,可以通过调用
ThreadContext#clear
方法显式清理线程内部存储,否则存在极其严重的内存泄露隐患" - "…提供了一个交易后自动清理的
Servlet Filter
,但是该过滤器只对基于Servlet的同步HTTP交易开发有用,对基于TCP的交易开发无效,因此基于TCP的交易开发者必须自行处理内存清理!"
因为框架内封装了对于HTTP请求结束后的ThreadLocal清理,所以同步方法不受影响,每次不指定数据源的请求都会使用默认数据源。而异步进程使用了独立的进程池,由于没有在使用后进行ThreadLocal清理,如果某个进程上一次被指定使用了副数据源,下一次使用该进程的未指定数据源方法就会随之使用副数据源,造成串库的现象。
尝试复现,将自定义的异步线程池降低到单个线程,先后启动以下三个进程:
- 使用默认数据源的同步方法A,该同步方法调用一个同样使用默认数据源的异步方法a
- 使用指定副数据源的同步方法B,该同步方法调用一个同样使用指定副数据源的异步方法b
- 使用默认数据源的同步方法A,该同步方法调用一个同样使用默认数据源的异步方法a
发现使用默认数据源的同步方法A能够一致保持使用默认数据源,方法B和b由于显式指定了副数据源,实际使用的数据源也是正确的。在第二次执行异步方法a时,由于复用了方法b使用的线程且未显式指定数据源,ThreadLocal中标注的仍为副数据源,执行发生了错误。
问题修复
由于原始项目内使用的异步方法不多,计划给所有的异步方法加上显式指定数据库,上线后待观察。
- 本文链接: https://wsswms.dev/2023/06/20/threadcontext-reuse-of-springboot-asynchronous-process-pool/
- 版权声明: 本博客所有文章除特别声明外,均采用 BY-NC-SA许可协议,转载请注明出处。