考虑到业务层面有多数据源切换的需求,同时又要考虑事务,我使用了Mybatis-Plus3中的@DS作为多数据源的切换,它的原理的就是一个拦截器
@Overridepublic Object invoke(MethodInvocation invocation) throws Throwable { try { DynamicDataSourceContextHolder.push(determineDatasource(invocation)); return invocation.proceed(); } finally { DynamicDataSourceContextHolder.poll(); }}里面的pull和poll实际就是操作一个容器,在环绕里面进来做"压栈",出去做"弹栈",数据结构是这样的
public final class DynamicDataSourceContextHolder { /** * 为什么要用链表存储(准确的是栈) * * 为了支持嵌套切换,如ABC三个service都是不同的数据源 * 其中A的某个业务要调B的方法,B的方法需要调用C的方法。一级一级调用切换,形成了链。 * 传统的只设置当前线程的方式不能满足此业务需求,必须模拟栈,后进先出。 * */ @SuppressWarnings("unchecked") private static final ThreadLocal LOOKUP_KEY_HOLDER = new ThreadLocal() { @Override protected Object initialValue() { return new ArrayDeque(); } }; private DynamicDataSourceContextHolder() { } /** * 获得当前线程数据源 * * @return 数据源名称 */ public static String peek() { return LOOKUP_KEY_HOLDER.get().peek(); } /** * 设置当前线程数据源 ** 如非必要不要手动调用,调用后确保最终清除 *
* * @param ds 数据源名称 */ public static void push(String ds) { LOOKUP_KEY_HOLDER.get().push(StringUtils.isEmpty(ds) ? "" : ds); } /** * 清空当前线程数据源 ** 如果当前线程是连续切换数据源 只会移除掉当前线程的数据源名称 *
*/ public static void poll() { Deque deque = LOOKUP_KEY_HOLDER.get(); deque.poll(); if (deque.isEmpty()) { LOOKUP_KEY_HOLDER.remove(); } } /** * 强制清空本地线程 ** 防止内存泄漏,如手动调用了push可调用此方法确保清除 *
*/ public static void clear() { LOOKUP_KEY_HOLDER.remove(); }上面就是@DS大概实现,然后我就碰到坑了,外层service加了@Transactional,通过service调用另一个数据源做insert,在切面里看数据源切换了,但是还是显示事务内的数据源还是旧的,代码结构简单罗列下:
数据源:
dynamic: primary: master strict: false datasource: master: driver-class-name: com.mysql.cj.jdbc.Driver url: jdbc:mysql://***/phorcys-centre?useSSL=false username: root password: ***** interface: url: jdbc:mysql://***/phorcys-interface?useSSL=false username: root password: ***** driver-class-name: com.mysql.cj.jdbc.Driver外层controller调用的service
@AutowiredUserService userService;@AutowiredRedisClient redisClient;@GetMapping("/demo")@Transactionalpublic GeneralResponse demo(@RequestBody(required = false) GeneralRequest request){ SysUser sysUser = new SysUser(); sysUser.setCode("wonder"); sysUser.setName("王吉坤"); sysUser.insert(); redisClient.set("token",sysUser); List sysUsers = new SysUser().selectAll(); String item01 = userService.getUserInfo("ITEM01"); return GeneralResponse.success();}内层service
@Servicepublic class UserServiceImpl implements UserService { @Override @DS("interface") @Transactional// @Transactional(propagation = Propagation.REQUIRES_NEW) public String getUserInfo(String name) { SapItemRecord sr = new SapItemRecord(); sr.setBatchId(1L); sr.setItemCode("ITEM01"); sr.setDescription("物料1号"); if(sr.insert()){ LambdaQueryWrapper item01 = new QueryWrapper().lambda().eq(SapItemRecord::getItemCode, name); SapItemRecord sapItemRecord = new SapItemRecord().selectOne(item01); ExceptionUtils.seed("内层事务异常");// return sapItemRecord.getDescription(); } return "response : wonder"; }}1 最开始内层不加事务,全局只有一个事务,无效;2 内层加事务@Transactional,无效;3 改变事务的传播方式@Transactional(propagation = Propagation.REQUIRES_NEW),事务生效
看了java方法栈和源码,springframework5 里面spring-tx,知道问题出在什么地方,贴一个调用栈截图
spring的事务是基于aop的,这个不解释了,直接进入事务拦截器TransactionInterceptor,找到它调用的invokeWithinTransaction方法,只看