在项目中如果有使用到数据库的多数据源时,使用dynamic-datasource-spring-boot-starter 可以很方便的做数据源切换。通过注解@DS("xxx")即可快速切换。
Gitee: 点我跳转
但是使用多线程时,发现了数据源切换不成功,探究其原因。
问题背景
比如说我配置了master和slave两个数据源,在Service上使用@DS做数据源切换的时候,在方法中使用多线程来处理数据时,方法加的注解时@DS("slave"),表示使用slave数据源,但是数据被写到了master库中,这表明数据源切换没有成功。
问题分析
通过分析代码看到,使用@DS注解后,动态数据源切换,其实就是在通过一个拦截器,在方法执行前后做了数据源的设置和清理。
public Object invoke(MethodInvocation invocation) throws Throwable {
Object var2;
try {
DynamicDataSourceContextHolder.push(this.determineDatasource(invocation));
var2 = invocation.proceed();
} finally {
DynamicDataSourceContextHolder.poll();
}
return var2;
}
然后在执行crud的时候,通过DynamicDataSourceContextHolder.peek()获取在队列头的数据源。
而DynamicDataSourceContextHolder中的数据源信息保存是通过ThreadLocal保存的:
public final class DynamicDataSourceContextHolder {
private static final ThreadLocal<Deque<String>> LOOKUP_KEY_HOLDER = new ThreadLocal() {
protected Object initialValue() {
return new ArrayDeque();
}
};
private DynamicDataSourceContextHolder() {
}
public static String peek() {
return (String)((Deque)LOOKUP_KEY_HOLDER.get()).peek();
}
public static void push(String ds) {
((Deque)LOOKUP_KEY_HOLDER.get()).push(StringUtils.isEmpty(ds) ? "" : ds);
}
public static void poll() {
Deque<String> deque = (Deque)LOOKUP_KEY_HOLDER.get();
deque.poll();
if (deque.isEmpty()) {
LOOKUP_KEY_HOLDER.remove();
}
}
public static void clear() {
LOOKUP_KEY_HOLDER.remove();
}
}
我们知道ThreadLocal是可以实现多线程资源隔离的,所以在我们开启一个新的线程做数据处理时,之前通过拦截器push进去的数据源就会丢失,此时就会使用默认的数据源做数据处理。
那么我们就可以在新的线程方法内通过手动设置的方式实现数据源的切换。
为什么在方法上设置注解没有用呢,因为方法被内部调用是 this 目标对象直接调用,并不是代理对象进行调用,是不会生成代理对象的。
改造如下
public void funcA(){
executor.execute(() -> funcB());
}
public void funcB() {
// 设置当前线程数据源
DynamicDataSourceContextHolder.push("aaa");
try {
//do something
} finally {
// 强制清空本地线程,防止内存泄漏,手动调用push可调用此方法确保清除
DynamicDataSourceContextHolder.push("aaa");
}
}
当然还有一种更简单的方式,就是在Mapper上加@DS注解,因为这样就可以通过aop的方式实现对象代理了。
其它多数据源@DS注解不生效场景
案例1
@Override
@DS("aaa")
public void addUserAndRole(SysUser sysUser, List<SysUserRole> sysUserRoleList, Boolean addUsers) {
// 新增或更新
this.saveOrUpdate(sysUser); // 将用户信息新增到 aaa 数据库
// 新增用户到 bbb 数据库
if (addUsers){
usersService.saveUsers(sysUser);
}
......
}
失败原因:
@DS注解受事务的影响,上述代码中我们aaa数据库的数据中操作bbb数据库,这是不行的,解决方式就是在操作bbb相应方法中新开一个事务
@DS("bbb") //切换数据源
@Transactional(rollbackFor = Exception.class, propagation = Propagation.REQUIRES_NEW) // 开启一个新事务
@Override
public void saveUsers(SysUser sysUser) {
......
}
案例2
实现层a方法:
@Service
@DS(DbEnum.A)
public class AServiceImpl {
@Autowired
private BService bService;
@Override
@Transactional(rollbackFor = Exception.class)
public void dbTest() {
B b = new B();
......
bService.save(b);
}
}
实现层b方法:
@Service
@DS(DbEnum.B)
public class BServiceImpl {
@Autowired
private BMapper bMapper;
@Override
public boolean save(B b) {
return bMapper.save(b);
}
}
失效问题:
使用动态数据源(@DS)时,@Transactional使用可能会造成@DS失效。
dbTest()方法配置了@Transactional(rollbackFor = Exception.class),save()方法没配置事务,或者配置@Transactional(rollbackFor = Exception.class),会报错save()方法里面的表找不到,其原因感觉应该是aop代理创建事务,没有切换,导致还是使用的默认数据源。
解决方法:
1、去掉事务(不建议)
2、给save()添加事务传播属性@Transactional(propagation = Propagation.REQUIRES_NEW, rollbackFor = Exception.class)
3、save方法不用添加事务,主方法用@DSTransactional
注解
待补充
评论区