行业资讯 2025年08月6日
0 收藏 0 点赞 632 浏览 5996 个字
摘要 :

文章目录 一、问题背景 二、问题原因分析 三、尝试解决问题的过程 (一)查看Github上的Issues (二)搜索引擎查找解决方案 (三)使用切面编程解决问题 在Java开发中……




  • 一、问题背景
  • 二、问题原因分析
  • 三、尝试解决问题的过程
    • (一)查看Github上的Issues
    • (二)搜索引擎查找解决方案
    • (三)使用切面编程解决问题

    在Java开发中,多商户多租户业务场景常需分库处理。本文将分享使用mybatisplusdynamic.datasource实现多数据源切换时,@Transactional导致@DS注解切换数据源失效的问题及解决办法。

    一、问题背景

    最近在业务开发里,碰到了多商户多租户的业务逻辑,这种情况就需要进行分库操作。项目使用的是mybatisplus框架,于是选用了同是baomidou开发的dynamic.datasource来实现多数据源切换。刚开始使用的时候,程序运行一切正常。可后来发现,在调用com.baomidou.mybatisplus.extension.service.IService.saveBatch方法时,@DS注解切换数据源的功能竟然失效了。

    二、问题原因分析

    深入到saveBatch方法内部查看,会发现这个方法上加了Transactional注解。Transactional主要是用来管理事务的,在事务开启之后,如果再进行数据库的切换,这个切换操作并不会生效。从源代码来看,当线程持有数据库连接时,它会复用当前线程绑定的数据库连接;要是线程没有绑定连接,那就会绑定默认的主库连接。所以最终连接到主库,也就意味着@DS注解没有起到应有的作用。如何解决@Transactional导致@DS注解切换数据源失效问题

    三、尝试解决问题的过程

    (一)查看Github上的Issues

    发现问题后,首先到Github的dynamic-datasource代码仓库去查看Issues,想看看有没有其他人也遇到过类似问题。结果发现有大量关于@DS多数据源切换无效的Issues。但官方的回应不太给力,要么直接回复说没有复现问题,要么就直接把问题关闭了。不过在众多问题中,还是找到了一条比较有用的信息,就是在调用被Transactional注解的方法所在的方法或类上添加@DS注解,试了之后发现确实有效果。但从代码分层结构的角度来看,这样做并不合适。因为Spring框架的优势就在于清晰的分层结构,控制层负责处理Web相关的事情,Service层专注于业务逻辑,持久层负责数据库交互。把@DS注解放在Mapper层来进行数据库切换才是比较合理的,而不应该为了解决这个问题,就随意把注解加在方法和类上,破坏了原有的分层结构。而且mybatisplus里有很多添加了Transactional注解的方法,要是都需要在调用的地方重写并添加@DS注解,那工作量可太大了,也不合理。

    (二)搜索引擎查找解决方案

    在Github上没有找到满意的解决方案,就想着去问问“度娘”和“谷歌大神”,毕竟这种问题前辈们大概率早就遇到过,说不定已经有成熟的解决办法了。但中文技术博客的现状不太乐观,很多文章都是抄袭的,也不注明转载来源,导致大量博客内容相似,还存在很多表述不清楚的地方。通过搜索引擎,找到了3种常见的解决方案:

    • 在Service类或方法上添加@DS注解:这个方案在前面查看Issues的时候就试过,从代码分层的角度考虑,个人不太认可这种做法。
    • 在调用带有Transactional注解的方法前切换数据库:这种方案比第一种更灵活一些,因为可以在方法里面根据不同的Service来获取需要切换的数据源。但缺点也很明显,侵入性太强,项目里所有使用了mybatisplus批量方法的Service都得进行处理,改动量非常大。
    • 自己实现TransactionManager:通过自己实现TransactionManager,在使用Transactional时手动指定,以此来替换Spring默认的DataSourceTransactionManager。不过这个方案风险太高了,自己实现TransactionManager需要考虑事务、异步、同步等很多方面的问题,还要保证单元测试尽可能全面,短时间内很难做得比经过多年迭代的框架更好,所以也放弃了这种方案。

    (三)使用切面编程解决问题

    大家都知道Spring框架有个很强大的AOP特性,利用这个特性,能够在不修改原有代码的基础上,对特定的内容进行增强。于是决定使用切面编程来解决这个问题,具体就是拦截mybatisplus中带有Transactional注解的方法,然后手动切换数据库。注册切面部分的代码很快就写好了,接下来就是调试数据库切换的功能。

    在调试过程中,使用了dynamic.datasource包里面的DynamicDataSourceContextHolder.push方法来切换数据库,但是一直没有成功,卡了很长时间。期间还尝试用DynamicRoutingDataSource.setPrimary方法把需要使用的数据库指定为主库,这样虽然能运行成功,但这种做法风险太大,肯定不能用。

    在不断调试的过程中,发现了一个关键信息。在调试时关注chain变量,会发现里面包含3个拦截器,其中动态数据库切换的拦截器在事务拦截器前面。这就找到了问题的关键,原来是写的切面类在事务之后才执行,所以只要调整切面类的执行优先级就可以了。把Order注解的优先级提高之后,程序就完美运行了。如何解决@Transactional导致@DS注解切换数据源失效问题

    下面是最终的切面类代码,要是你也遇到了调用mybatisplus中批量方法无法切换多数据源的问题,可以直接使用这段代码,它不会对现有代码造成任何侵入和更改。如果只是处理Transactional@DS的冲突,稍微修改一下切面类的作用范围就能解决问题。

    // 以下代码用于解决@Transactional导致@DS注解切换数据源失效的问题
    // 通过切面编程,在事务开启前切换数据源,保证数据源切换生效
    package com.spman.common.aspect;
    
    import com.alibaba.fastjson2.JSON;
    import com.baomidou.dynamic.datasource.annotation.DS;
    import com.baomidou.dynamic.datasource.toolkit.DynamicDataSourceContextHolder;
    import com.baomidou.mybatisplus.core.mapper.BaseMapper;
    import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
    import java.lang.reflect.Field;
    import lombok.SneakyThrows;
    import lombok.extern.slf4j.Slf4j;
    import org.aspectj.lang.JoinPoint;
    import org.aspectj.lang.annotation.After;
    import org.aspectj.lang.annotation.Aspect;
    import org.aspectj.lang.annotation.Before;
    import org.aspectj.lang.annotation.Pointcut;
    import org.springframework.core.annotation.Order;
    import org.springframework.stereotype.Component;
    
    @Slf4j
    @Aspect
    @Order(0)
    @Component
    public class MyBatisPlusServiceTransactionalAspect {
        // 用于存储当前切面主动切换的数据库,在方法执行完成后主动出栈
        private static final ThreadLocal<String> DS_KEY = new ThreadLocal<>();
    
        // 定义切点,匹配mybatisplus扩展服务接口的所有方法
        @Pointcut(\"execution(* com.baomidou.mybatisplus.extension.service.IService+.*(..))\")
        public void myBatisPlusMethodPointcut() {}
    
        // 定义切点,匹配被@Transactional注解的方法
        @Pointcut(\"@annotation(org.springframework.transaction.annotation.Transactional)\")
        public void transactionalPointcut() {}
    
        // 在方法执行前进行处理,主要用于切换数据源
        @Before(\"myBatisPlusMethodPointcut() && transactionalPointcut()\")
        public void beforeHandler(JoinPoint joinPoint) {
            // 将方法参数转换为JSON字符串,方便记录日志
            String argsJson = JSON.toJSONString(joinPoint.getArgs());
            // 获取目标对象,即ServiceImpl实例
            ServiceImpl<?,?> target = (ServiceImpl<?,?>)joinPoint.getTarget();
            // 构建方法名,格式为类名.方法名
            String methodName = target.getClass().getTypeName() + \".\" + joinPoint.getSignature().getName();
    
            // 记录日志,表明拦截到方法开始执行,并打印参数列表
            log.info(\"MyBatisPlusServiceAspect拦截到{}开始执行, 参数列表->{}\", methodName, argsJson);
    
            // 获取ServiceImpl绑定的Mapper类
            Class<? extends BaseMapper<?>> mapperClass = getMapperClass(target);
            // 获取Mapper类上的@DS注解
            DS dsAnnotation = getDSAnnotation(mapperClass);
    
            // 如果Mapper类没有绑定@DS注解,记录日志并跳过数据源切换
            if (dsAnnotation == null) {
                log.info(\"{}未绑定DS注解, 跳过数据源切换\", mapperClass.getName());
            } else {
                // 将注解中的数据源名称存入线程变量
                DS_KEY.set(dsAnnotation.value());
                // 切换数据源
                DynamicDataSourceContextHolder.push(dsAnnotation.value());
    
                // 记录日志,表明已切换数据源
                log.info(\"{}已绑定DS注解, 已主动切换数据源为{}\", mapperClass.getName(), dsAnnotation.value());
            }
        }
    
        // 在方法执行后进行处理,主要用于恢复数据源
        @After(\"myBatisPlusMethodPointcut() && transactionalPointcut()\")
        public void afterHandler(JoinPoint joinPoint) {
            // 从线程变量中获取之前切换的数据源名称
            String dsKey = DS_KEY.get();
            // 获取目标对象,即ServiceImpl实例
            ServiceImpl<?,?> target = (ServiceImpl<?,?>)joinPoint.getTarget();
            // 构建方法名,格式为类名.方法名
            String methodName = target.getClass().getTypeName() + \".\" + joinPoint.getSignature().getName();
    
            // 如果线程变量中有数据源名称,执行数据源变量出栈操作
            if (dsKey != null &&!dsKey.isEmpty()) {
                DynamicDataSourceContextHolder.poll();
                log.info(\"DS_KEY线程变量为{}, 已执行数据源变量出栈操作\", dsKey);
            } else {
                // 如果线程变量中没有数据源名称,记录日志并跳过出栈操作
                log.info(\"DS_KEY线程变量不存在, 跳过数据源变量出栈操作\");
            }
            // 记录日志,表明拦截到方法结束执行
            log.info(\"MyBatisPlusServiceAspect拦截到{}结束执行\", methodName);
        }
    
        // 从ServiceImpl中获取service绑定的mapper
        @SneakyThrows
        private Class<? extends BaseMapper<?>> getMapperClass(ServiceImpl<?,?> target) {
            // 获取ServiceImpl父类中的mapperClass字段
            Field mapperClassField = target.getClass().getSuperclass().getDeclaredField(\"mapperClass\");
            // 设置字段可访问
            mapperClassField.setAccessible(true);
            // 获取字段的值,即Mapper类
            return (Class<? extends BaseMapper<?>>) mapperClassField.get(target);
        }
    
        // 根据BaseMapper接口获取标记的DS注解
        public static DS getDSAnnotation(Class<? extends BaseMapper<?>> clazz) {
            // 如果传入的类为空,直接返回null
            if (clazz == null) return null;
    
            // 获取类上的@DS注解
            DS target = clazz.getAnnotation(DS.class);
            // 如果类上没有@DS注解,则从继承的接口上继续查找
            if (target == null) {
                for (Class<?> parentInterface : clazz.getInterfaces()) {
                    target = getDSAnnotation((Class<? extends BaseMapper<?>>) parentInterface);
                    // 如果找到@DS注解,则返回该注解
                    if (target != null) return target;
                }
            }
            return target;
        }
    }
    

    解决技术问题时,需要耐心地深入研究,不能只是为了解决问题而简单应付,只有这样才能真正找到合适的解决方案。

微信扫一扫

支付宝扫一扫

版权: 转载请注明出处:https://www.zuozi.net/10331.html

管理员

相关推荐
2025-08-06

文章目录 一、Reader 接口概述 1.1 什么是 Reader 接口? 1.2 Reader 与 InputStream 的区别 1.3 …

988
2025-08-06

文章目录 一、事件溯源 (一)核心概念 (二)Kafka与Golang的优势 (三)完整代码实现 二、命令…

465
2025-08-06

文章目录 一、证明GC期间执行native函数的线程仍在运行 二、native线程操作Java对象的影响及处理方…

348
2025-08-06

文章目录 一、事务基础概念 二、MyBatis事务管理机制 (一)JDBC原生事务管理(JdbcTransaction)…

456
2025-08-06

文章目录 一、SnowFlake算法核心原理 二、SnowFlake算法工作流程详解 三、SnowFlake算法的Java代码…

517
2025-08-06

文章目录 一、本地Jar包的加载操作 二、本地Class的加载方法 三、远程Jar包的加载方式 你知道Groo…

832
发表评论
暂无评论

还没有评论呢,快来抢沙发~

助力内容变现

将您的收入提升到一个新的水平

点击联系客服

在线时间:08:00-23:00

客服QQ

122325244

客服电话

400-888-8888

客服邮箱

122325244@qq.com

扫描二维码

关注微信客服号