【java】@Transactional导致@DS注解切换数据源失效

  最近业务中出现了多商户多租户的逻辑,所以需要分库,项目框架使用了mybatisplus所以我们自然而然的选择了同是baomidou开发的dynamic.datasource来实现多数据源的切换。在使用初期程序运行都很好,但之后发现在调用com.baomidou.mybatisplus.extension.service.IService.saveBatch时@DS切换数据源会失效。

问题原因

  进入saveBatch方法我们可以看到方法上添加了Transactional,我们知道Transactional用来管理事务,在事务开启后进行数据库的切换时并不会生效,源代码如下,当线程持有数据库连接时会复用当前线程绑定的数据库连接,否则绑定默认的主库连接,既然最终连接到主库,说明@DS并没有生效。

尝试解决问题step1

  前往Github的dynamic-datasource代码仓库查看Issues,发现了大量的关于@DS多数据源切换无效的Issues,but官方看起来很傲慢,要么直接回复未复现,要么直接关闭。

  只有一条信息比较有用,在调用被Transactional注解的方法的方法或类上添加@DS注解,我试了有效果。

  但是我认为在Service和方法上加@DS注解并不合适,Spring框架就是因为清晰明了的分层结构深受大家喜爱,控制层专注Web,Service层专注业务逻辑,持久层专注数据库交互,所以@DS数据库切换放在Mapper我觉得是合理的,而不应该为了解决问题硬生生的放在方法和类上来破坏这种分层结构。况且mybatisplus中那么多添加了Transactional的方法在调用的地方我都需要重写并添加@DS这太2了。

尝试解决问题step2

  离开Github我马上找google度娘,毕竟我遇到的这点问题前辈们可能早就遇到了并给出了解决方案。

这里不得不吐槽一下中文技术博客的现状,很多偷文贼将别人的文章偷走,也不标转载自哪里,导致大量博客内容雷同且存在很多词不达意的内容。因为喜欢所以才会分享表达,不喜欢不热爱你说你偷别人文章干啥。

  根据搜索引擎的结果,主要分为3种解决方案。

  1. 在Service类或者方法上添加@DS注解
  2. 在调用带有Transactional注解的方法前切换数据库
  3. 自己实现TransactionManager在使用Transactional时手动指定来替换Spring默认的DataSourceTransactionManager

  方案1在step1我自己并不认可

  方案2相对方案1更加灵活,毕竟因为在方法中切换,可以根据不同的Service来获取需要切换的数据源,但这种方案个人觉得侵入性太强,需要对使用了mybatisplus批量方法的Service全部进行处理

  方案3我认为风险太高,自己实现TransactionManager事务、异步、同步等都需要考虑到还要保证单元测试尽可能的覆盖,我不认为短时间内能做的比迭代了多年的框架更好,所以也放弃

尝试解决问题step3

  我们知道Spring因为AOP特性可以轻松的实现在不对原有代码侵入的情况下对特定内容进行增强,所以我决定使用切面编程mybatisplus中带有Transactional注解的方法进行拦截,然后手动切换数据库,注册切面部分很快完成,剩下的就是调试数据库切换。

  数据库切换我使用了dynamic.datasource包内的DynamicDataSourceContextHolder.push方法,但遗憾的是数据库切换一直不成功并卡了很久,期间使用DynamicRoutingDataSource.setPrimary方法将需要使用的数据库指定为主库运行成功,但这种骚操作肯定不合适,将别的库指定为主库风险肯定很大。

  之后就是漫长的Debug,不断的F7、F8,一直没有头绪,我在方法上添加了@DS注解,并关闭了我的切面类再进行调试,突然发现了点不一样的东西,不知道有没有敏感的同学发现端倪。

  请关注chain变量,里面包含3个拦截器,更重要的是动态数据库切换的拦截器在事务拦截器前面,而我们的目的不就是在事务开启前切换数据库吗,那我现在的问题就是我的切面类在事务后执行的,所以调整我的切面类执行优先级就好了,立马把Order注解抬上来,执行程序完美运行。

切面类最终代码

  如果你也遇到了调用mybatisplus中批量方法无法切换多数据源的话,可直接拷贝安全食用,不会对现有的人和代码侵入和更改。如果你只是处理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<>();

  @Pointcut("execution(* com.baomidou.mybatisplus.extension.service.IService+.*(..))")
  public void myBatisPlusMethodPointcut() {}

  @Pointcut("@annotation(org.springframework.transaction.annotation.Transactional)")
  public void transactionalPointcut() {}

  @Before("myBatisPlusMethodPointcut() && transactionalPointcut()")
  public void beforeHandler(JoinPoint joinPoint) {
    String argsJson = JSON.toJSONString(joinPoint.getArgs());
    ServiceImpl<?, ?> target = (ServiceImpl<?, ?>)joinPoint.getTarget();
    String methodName = target.getClass().getTypeName() + "." + joinPoint.getSignature().getName();

    log.info("MyBatisPlusServiceAspect拦截到{}开始执行, 参数列表->{}", methodName, argsJson);

    Class<? extends BaseMapper<?>> mapperClass = getMapperClass(target);
    DS dsAnnotation = getDSAnnotation(mapperClass);

    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<?, ?> 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
   *
   * @param target ServiceImpl实例
   */
  @SneakyThrows
  private Class<? extends BaseMapper<?>> getMapperClass(ServiceImpl<?, ?> target) {
    Field mapperClassField = target.getClass().getSuperclass().getDeclaredField("mapperClass");

    mapperClassField.setAccessible(true);

    return (Class<? extends BaseMapper<?>>) mapperClassField.get(target);
  }

  /**
   * 根据BaseMapper接口获取标记的DS注解
   *
   * @param clazz 继承自BaseMapper的mapper接口
   */
  public static DS getDSAnnotation(Class<? extends BaseMapper<?>> clazz) {
    if (clazz == null) return null;

    DS target = clazz.getAnnotation(DS.class);
    // 找不到DS注解时从继承的接口上继续查找
    if (target == null) {
      for (Class<?> parentInterface: clazz.getInterfaces()) {
        target = getDSAnnotation((Class<? extends BaseMapper<?>>)parentInterface);

        if (target != null) return target;
      }
    }
    return target;
  }
}

如果真的需要解决问题还是需要自己耐心的跟进,拒绝为了解决问题而解决问题。

参考资料

[1] mybatisplus官网: https://baomidou.com/

[2] dynamic-datasource代码仓库: https://github.com/baomidou/dynamic-datasource

[3] Spring之AOP的详细讲解: https://blog.csdn.net/m0_74097410/article/details/137476783

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:/a/979932.html

如若内容造成侵权/违法违规/事实不符,请联系我们进行投诉反馈qq邮箱809451989@qq.com,一经查实,立即删除!

相关文章

Solana 核心概念全解析:账户、交易、合约与租约,高流量区块链技术揭秘!

目录 1.Solana 核心概念简述 1.1. 账户&#xff08;Account&#xff09; 1.2. 交易&#xff08;Transaction&#xff09; 1.3. 交易指令&#xff08;Instruction&#xff09; 1.4. SPL 代币 1.5. 合约&#xff08;Program&#xff09; 1.6. 租约&#xff08;Rent&#x…

StarRocks 在爱奇艺大数据场景的实践

作者&#xff1a;林豪&#xff0c;爱奇艺大数据 OLAP 服务负责人 小编导读&#xff1a; 本文整理自爱奇艺工程师在 StarRocks 年度峰会的分享&#xff0c;介绍了爱奇艺 OLAP 引擎演化及引入 StarRocks 后的效果。 在广告业务中&#xff0c;StarRocks 替换 ImpalaKudu 后&#x…

【Linux】Linux的进程控制

目录 1. 学习思维导图 2.进程创建&#xff08;fork&#xff09; 2.1 fork创建进程失败 3.进程终止 3.1 进程退出情况 3.1.1main函数 3.1.2 退出码 3.2 exit/_exit函数 1. exit() 函数 2. _exit() 函数 4.进程等待 4.1 实现进程等待的方法 wait/waitpid方法 区别&a…

ubuntu防火墙iptables

文章目录 步骤开启自启防火墙iptables规则链Chains的区别 在 Ubuntu 上使用 iptables 配置防火墙并保证服务可用 步骤 #防火墙状态 systemctl status iptables systemctl start iptables #开启防火墙并且开启22端口 systemctl start iptables && iptables -A INPUT -p…

计算机毕业设计SpringBoot+Vue.js公司日常考勤系统(源码+文档+PPT+讲解)

温馨提示&#xff1a;文末有 CSDN 平台官方提供的学长联系方式的名片&#xff01; 温馨提示&#xff1a;文末有 CSDN 平台官方提供的学长联系方式的名片&#xff01; 温馨提示&#xff1a;文末有 CSDN 平台官方提供的学长联系方式的名片&#xff01; 作者简介&#xff1a;Java领…

企业微信里可以使用的企业内刊制作工具,FLBOOK

如何让员工及时了解公司动态、行业资讯、学习专业知识&#xff0c;并有效沉淀企业文化&#xff1f;一份高质量的企业内刊是不可或缺的。现在让我来教你该怎么制作企业内刊吧 1.登录与上传 访问FLBOOK官网&#xff0c;注册账号后上传排版好的文档 2.选择模板 FLBOOK提供了丰富的…

Hive-01之数仓、架构、数据类型、DDL、内外部表

一、主题 hive的核心概念hive与数据库的区别hive的架构原理hive的安装部署hive的交互式方式hive的数据类型hive的DDL语法操作 二、要点 1.数据仓库的基本概念 1.数据仓库的基本概念 英文名称为Data Warehouse&#xff0c;可简写为DW或DWH。数据仓库的目的是构建面向分析的…

【Markdown 语法简洁讲解】

Markdown 语法简洁语法讲解 什么是 Markdown1. 标题2. 列表3.文本样式4. 链接与图片5. 代码6. 表格7. 分割线8. 流程图9. 数学公式10. 快捷键11. 字体、字号与颜色 什么是 Markdown Markdown 是一种轻量级标记语言&#xff0c;通过简单的符号实现排版格式化&#xff0c;专注于…

数据如何安全“过桥”?分类分级与风险评估,守护数据流通安全

信息化高速发展&#xff0c;数据已成为企业的核心资产&#xff0c;驱动着业务决策、创新与市场竞争力。随着数据开发利用不断深入&#xff0c;常态化的数据流通不仅促进了信息的快速传递与共享&#xff0c;还能帮助企业快速响应市场变化&#xff0c;把握商业机遇&#xff0c;实…

Linux网络 TCP全连接队列与tcpdump抓包

TCP全连接队列 在 Linux 网络中&#xff0c;TCP 全连接队列&#xff08;也称为 Accept 队列&#xff09;是一个重要的概念&#xff0c;用于管理已经完成三次握手&#xff0c;即已经处于 established 状态但尚未被应用程序通过 accept( ) 函数处理的 TCP 连接&#xff0c;避免因…

网络流算法: Edmonds-Karp算法

图论相关帖子 基本概念图的表示: 邻接矩阵和邻接表图的遍历: 深度优先与广度优先拓扑排序图的最短路径:Dijkstra算法和Bellman-Ford算法最小生成树二分图多源最短路径强连通分量欧拉回路和汉密尔顿回路网络流算法: Edmonds-Karp算法网络流算法: Dinic算法 环境要求 本文所用…

Python的那些事第三十六篇:基于 Vega 和 Vega-Lite 的数据可视化解决方案,Altair 声明式可视化库

Altair 声明式可视化库:基于 Vega 和 Vega-Lite 的数据可视化解决方案 摘要 在数据科学和分析领域,有效的数据可视化是理解数据、发现模式和传达见解的关键。Python 作为数据科学的主要编程语言之一,提供了多种数据可视化库。其中,Altair 是一个基于 Vega 和 Vega-Lite 的…

文件描述符与重定向

1. open系统调用 在 Linux 中, open() 系统调用用于打开一个文件或设备&#xff0c;并返回一个文件描述符&#xff0c;通过该描述符可以进行文件读写操作。open() 可以用于创建新文件或打开已存在的文件&#xff0c;具体行为取决于传递给它的参数。 需要包含的头文件&#xf…

【WPF】绑定报错:双向绑定需要 Path 或 XPath

背景 最开始使用的是 TextBlock: <ItemsControl ItemsSource"{Binding CameraList}"><ItemsControl.ItemsPanel><ItemsPanelTemplate><StackPanel Orientation"Horizontal"/></ItemsPanelTemplate></ItemsControl.Item…

【Linux高级IO】五种IO模型 多路转接(select)

目录 1. 五种IO模型 1.1 阻塞式IO 1.2 非阻塞IO 1.3 信号驱动IO 1.4 多路转接 1.5 异步IO 2. 同步通信与异步通信 3. 多路转接 3.1 select 总结 1. 五种IO模型 1.1 阻塞式IO 阻塞式IO最为常见&#xff0c;在内核将数据准备好之前, 系统调用会一直等待&#xff0c;所有的…

Linux服务升级:Almalinux 升级 DeepSeek-R1

目录 一、实验 1.环境 2.Almalinux 部署 Ollama 3.Almalinux 升级 DeepSeek-R1 4.Almalinux 部署 docker 5. docker 部署 DeepSeek-R1 6.Almalinux 部署 Cpolar (内网穿透) 7.使用cpolar内网穿透 二、问题 1.构建容器失败 一、实验 1.环境 &#xff08;1&#xff09…

深度剖析数据分析职业成长阶梯

一、数据分析岗位剖析 目前&#xff0c;数据分析领域主要有以下几类岗位&#xff1a;业务数据分析师、商业数据分析师、数据运营、数据产品经理、数据工程师、数据科学家等&#xff0c;按照工作侧重点不同&#xff0c;本文将上述岗位分为偏业务和偏技术两大类&#xff0c;并对…

CosyVoice2整合包 特殊声音标记,声音克隆更逼真,新增批量生成

新增批量生成,可用于制作直播话术音频 特殊声音标记 符号示例1_语气加强<strong> </strong>每天都<strong>付出</strong>和<strong>精进</strong>&#xff0c;才能达到巅峰。2_呼吸声[breath][breath] 吸气,[breath] 呼气! [breath] 吸,[b…

vector习题

完数和盈数 题目 完数VS盈数_牛客题霸_牛客网 一个数如果恰好等于它的各因子(该数本身除外)之和&#xff0c;如&#xff1a;6321。则称其为“完数”&#xff1b;若因子之和大于该数&#xff0c;则称其为“盈数”。 求出2到60之间所有“完数”和“盈数”。 输入描述&#xff…

如何保证 Redis 缓存和数据库的一致性?

如何保证 Redis 缓存和数据库的一致性&#xff1f; 1. 问题出现场景 先修改数据库&#xff0c;再删除缓存 删除数据库数据成功了&#xff0c;但是删除缓存却失败了&#xff0c;缓存中仍保留的是旧数据 先删除缓存&#xff0c;再删除数据库 如果 Redis 缓存删除成功后&#xf…