【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种解决方案。
- 在Service类或者方法上添加@DS注解
- 在调用带有Transactional注解的方法前切换数据库
- 自己实现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实例*/@SneakyThrowsprivate 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
相关文章:
【java】@Transactional导致@DS注解切换数据源失效
最近业务中出现了多商户多租户的逻辑,所以需要分库,项目框架使用了mybatisplus所以我们自然而然的选择了同是baomidou开发的dynamic.datasource来实现多数据源的切换。在使用初期程序运行都很好,但之后发现在调用com.baomidou.mybatisplus.ex…...
003 SpringBoot集成Kafka操作
4.SpringBoot集成Kafka 文章目录 4.SpringBoot集成Kafka1.入门示例2.yml完整配置3.关键配置注释说明1. 生产者优化参数2. 消费者可靠性配置3. 监听器高级特性4. 安全认证配置 4.配置验证方法5.不同场景配置模板场景1:高吞吐日志收集场景2:金融级事务消息…...
Android SystemUI开发(一)
frameworks/base/packages/SystemUI/src/com/android/systemui/SystemUI.java frameworks/base/packages/SystemUI/src/com/android/systemui/SystemUIService.java 关键文件 SystemUI 关键服务 简介 Dependency.class:处理系统依赖关系,提供资源或服…...
C#贪心算法
贪心算法:生活与代码中的 “最优选择大师” 在生活里,我们常常面临各种选择,都希望能做出最有利的决策。比如在超市大促销时,面对琳琅满目的商品,你总想用有限的预算买到价值最高的东西。贪心算法,就像是一…...
Vue程序下载
Vue是一个基于JavaScript(JS)实现的框架,想要使用它,就得先拿到Vue的js文件 Vue官网 Vue2:Vue.js Vue3:Vue.js - 渐进式 JavaScript 框架 | Vue.js 下载并安装vue.js 第一步:打开Vue2官网&a…...
【UCB CS 61B SP24】Lecture 17 - Data Structures 3: B-Trees学习笔记
本文以 2-3-4 树详细讲解了 B 树的概念,逐步分析其操作,并用 Java 实现了标准的 B 树。 1. 2-3 & 2-3-4 Trees 上一节课中讲到的二叉搜索树当数据是随机顺序插入的时候能够使得树变得比较茂密,如下图右侧所示,时间复杂度也就…...
机器学习决策树
一、香农公式 熵: 信息增益: 信息增益信息熵-条件熵 前者是初始信息熵大小,后者是因为条件加入后带来的确定性增加 信息增益表示得知特征X的信息而使得类Y的信息的不确定性减少的程度 信息增益越大说明影响越大 二、代码 ""&…...
Spring Boot + MyBatis 实现 RESTful API 的完整流程
后端开发:Spring Boot 快速开发实战 引言 在现代后端开发中,Spring Boot 因其轻量级、快速开发的特性而备受开发者青睐。本文将带你从零开始,使用 Spring Boot MyBatis 实现一个完整的 RESTful API,并深入探讨如何优雅地处理异…...
通过 ANSYS Discovery 进行 CFD 分析,增强工程设计
概括 工程师使用计算流体动力学 (CFD) 分析来研究和优化各种应用中的流体流动和传热分析。ANSYS Discovery 是一个用户友好的软件平台,使工程师能够轻松设置和解决 CFD 模型,并能够通知设计修改 在这篇博文中,我们将重点介绍在 Ansys Disc…...
家用可燃气体探测器——家庭燃气安全的坚实防线
随着社会的发展和变迁,天然气为我们的生活带来了诸多便利,无论是烹饪美食,还是温暖取暖,都离不开它的支持。然而,燃气安全隐患如影随形,一旦发生泄漏,可能引发爆炸、火灾等严重事故,…...
ListControl双击实现可编辑
为Edit Control控件添加丢失输入焦点事件,可见设为false 为List Control控件添加双击事件 控件和成员变量之间交换数据 CListCtrl ListPrint1; //列表输出 CEdit...
ave-form.vue 组件中 如何将产品名称发送给后端 ?
如何将产品名称发送给后端。 在这段代码中,产品名称(productName)的处理和发送主要发生在 save() 方法中。让我逐步分析: 产品ID的选择: <w-form-selectv-model"form.productId"label"涉及产品&q…...
DeepSeek行业应用实践报告-智灵动力【112页PPT全】
DeepSeek(深度搜索)近期引发广泛关注并成为众多企业/开发者争相接入的现象,主要源于其在技术突破、市场需求适配性及生态建设等方面的综合优势。以下是关键原因分析: 一、技术核心优势 开源与低成本 DeepSeek基于开源架构…...
【Markdown 语法简洁讲解】
Markdown 语法简洁语法讲解 什么是 Markdown1. 标题2. 列表3.文本样式4. 链接与图片5. 代码6. 表格7. 分割线8. 流程图9. 数学公式10. 快捷键11. 字体、字号与颜色 什么是 Markdown Markdown 是一种轻量级标记语言,通过简单的符号实现排版格式化,专注于…...
250301-OpenWebUI配置DeepSeek-火山方舟+硅基流动+联网搜索+推理显示
A. 最终效果 B. 火山方舟配置(一定要点击添加) C. 硅基流动配置(最好要点击添加,否则会自动弹出所有模型) D. 联网搜索配置 E. 推理过程显示 默认是没有下面的推理过程的显示的 设置步骤: 在Functions函…...
【3天快速入门WPF】12-MVVM
目录 1. 什么是MVVM2. 实现简单MVVM2.1. Part 12.2. Part 21. 什么是MVVM MVVM 是 Model-View-ViewModel 的缩写,是一种用于构建用户界面的设计模式,是一种简化用户界面的事件驱动编程方式。 MVVM 的目标是实现用户界面和业务逻辑之间的彻底分离,以便更好地管理和维护应用…...
查找Excel包含关键字的行(の几种简单快速方法)
需求:数据在后缀为xlsx的Excel的sheet1中且量比较大,比如几十万行几百列;想查找一个关键字所在的行,比如"全网首发"; 情况①知道关键字在哪一列 情况②不确定在哪一列,很多列相似又不同,本文演…...
性能测试分析和调优
步骤 性能调优的步骤 性能调优的步骤: 1.确定问题:根据性能测试的结果来分析确定bug。–测试人员职责 2.分析原因:分析问题产生的原因。----开发人员职责 3.给出解决方案:可以是修改软件配置、增加硬件资源配置、修改代码等----…...
(视频教程)Compass代谢分析详细流程及python版-R语言版下游分析和可视化
不想做太多的前情解说了,有点累了,做了很久的内容,包括整个分析,从软件安装和报错解决到后期下游python版-R语言版下游分析和可视化!单细胞代谢分析我们写过很多了,唯独少了最“高级”的compass,…...
【SQL】MySQL中的字符串处理函数:concat 函数拼接字符串,COALESCE函数处理NULL字符串
MySQL中的字符串处理函数:concat 函数 一、concat ()函数 1.1、基本语法1.2、示例1.3、特殊用途 二、COALESCE()函数 2.1、基本语法2.2、示例2.3、用途 三、进阶练习 3.1 条件和 SQL 语句3.2、解释 一、concat &…...
vscode里如何用git
打开vs终端执行如下: 1 初始化 Git 仓库(如果尚未初始化) git init 2 添加文件到 Git 仓库 git add . 3 使用 git commit 命令来提交你的更改。确保在提交时加上一个有用的消息。 git commit -m "备注信息" 4 …...
基于大模型的 UI 自动化系统
基于大模型的 UI 自动化系统 下面是一个完整的 Python 系统,利用大模型实现智能 UI 自动化,结合计算机视觉和自然语言处理技术,实现"看屏操作"的能力。 系统架构设计 #mermaid-svg-2gn2GRvh5WCP2ktF {font-family:"trebuchet ms",verdana,arial,sans-…...
【WiFi帧结构】
文章目录 帧结构MAC头部管理帧 帧结构 Wi-Fi的帧分为三部分组成:MAC头部frame bodyFCS,其中MAC是固定格式的,frame body是可变长度。 MAC头部有frame control,duration,address1,address2,addre…...
【网络安全产品大调研系列】2. 体验漏洞扫描
前言 2023 年漏洞扫描服务市场规模预计为 3.06(十亿美元)。漏洞扫描服务市场行业预计将从 2024 年的 3.48(十亿美元)增长到 2032 年的 9.54(十亿美元)。预测期内漏洞扫描服务市场 CAGR(增长率&…...
postgresql|数据库|只读用户的创建和删除(备忘)
CREATE USER read_only WITH PASSWORD 密码 -- 连接到xxx数据库 \c xxx -- 授予对xxx数据库的只读权限 GRANT CONNECT ON DATABASE xxx TO read_only; GRANT USAGE ON SCHEMA public TO read_only; GRANT SELECT ON ALL TABLES IN SCHEMA public TO read_only; GRANT EXECUTE O…...
React---day11
14.4 react-redux第三方库 提供connect、thunk之类的函数 以获取一个banner数据为例子 store: 我们在使用异步的时候理应是要使用中间件的,但是configureStore 已经自动集成了 redux-thunk,注意action里面要返回函数 import { configureS…...
JAVA后端开发——多租户
数据隔离是多租户系统中的核心概念,确保一个租户(在这个系统中可能是一个公司或一个独立的客户)的数据对其他租户是不可见的。在 RuoYi 框架(您当前项目所使用的基础框架)中,这通常是通过在数据表中增加一个…...
GraphQL 实战篇:Apollo Client 配置与缓存
GraphQL 实战篇:Apollo Client 配置与缓存 上一篇:GraphQL 入门篇:基础查询语法 依旧和上一篇的笔记一样,主实操,没啥过多的细节讲解,代码具体在: https://github.com/GoldenaArcher/graphql…...
PH热榜 | 2025-06-08
1. Thiings 标语:一套超过1900个免费AI生成的3D图标集合 介绍:Thiings是一个不断扩展的免费AI生成3D图标库,目前已有超过1900个图标。你可以按照主题浏览,生成自己的图标,或者下载整个图标集。所有图标都可以在个人或…...
负载均衡器》》LVS、Nginx、HAproxy 区别
虚拟主机 先4,后7...
