1. 为什么多线程环境下数据源会丢失?
这个问题困扰过不少使用MyBatis-Plus多数据源的开发者。想象一下,你正在开发一个电商系统,订单服务需要同时查询商品库和用户库。你按照文档配置好了dynamic-datasource,在单线程测试时一切正常。但当系统上线后,突然发现有些查询总是跑到默认数据源上,导致数据错乱。
根本原因在于DynamicDataSourceContextHolder内部使用了ThreadLocal来存储当前线程的数据源标识。ThreadLocal的特性是线程隔离的,每个线程都有自己独立的副本。当你在主线程中切换了数据源,然后开启子线程执行数据库操作时,子线程无法获取到主线程的ThreadLocal值,自然就回退到默认数据源了。
我遇到过这样一个案例:一个报表导出功能使用了@Async异步处理,结果导出的数据总是来自主库。排查后发现就是因为异步线程拿不到主线程的数据源标识。这种问题在以下场景特别常见:
- 使用@Async注解的异步方法
- 通过ThreadPoolExecutor手动创建线程池
- 使用CompletableFuture进行异步编排
- 调用parallelStream并行流处理数据
需要模型API调用? 免费领10W Token,多模型网关一键接入 Claude、DeepSeek 等主流模型。
2. 三种解决方案的深度对比
2.1 手动传递数据源标识
这是最直接的方式,适合简单的线程创建场景。核心思路是在创建线程前获取当前数据源,然后在子线程中手动设置。
java复制// 获取父线程数据源
String currentDS = DynamicDataSourceContextHolder.peek();
new Thread(() -> {
// 设置子线程数据源
DynamicDataSourceContextHolder.push(currentDS);
try {
// 业务逻辑
userMapper.selectById(1);
} finally {
// 清理数据源
DynamicDataSourceContextHolder.poll();
}
}).start();
优点:
- 实现简单,不需要额外依赖
- 适合临时性的简单异步任务
缺点:
- 需要手动维护数据源的设置和清理
- 容易遗漏清理导致内存泄漏
- 不适合复杂的线程池场景
2.2 自定义线程池包装器
对于使用线程池的场景,我们可以创建一个代理线程池,在任务执行前后自动处理数据源传递。
java复制public class DataSourceAwareExecutor implements Executor {
private final Executor delegate;
public DataSourceAwareExecutor(Executor delegate) {
this.delegate = delegate;
}
@Override
public void execute(Runnable command) {
String currentDS = DynamicDataSourceContextHolder.peek();
delegate.execute(() -> {
if (currentDS != null) {
DynamicDataSourceContextHolder.push(currentDS);
}
try {
command.run();
} finally {
if (currentDS != null) {
