ARTICLE DETAIL

资讯详情

深耕郑州网站建设与运营推广的一线实战洞察。

Spring Boot多数据源切换与动态数据源管理实战:从@DS到@DSTransaction

Spring Boot多数据源切换与动态数据源管理实战:从@DS到@DSTransaction 做过几年后端的人大概率都遇到过这种恶心事业务刚上线的时候一个库够用后面订单、用户、日志全揉在一起等到某天夜里大促一压慢查询把主库拖死所有接口跟着雪崩。这时候再谈拆分已经晚了半拍。更常见的是财务系统、报表中心这类项目天生就要跨好几个库读数据有的是老 Oracle有的是新 MySQL还有第三方提供的只读实例你连 JPA 都不一定敢乱用。我前一阵做一个报表聚合服务就遇到了典型的多数据源场景。代码里最蠢的做法是搞一个 Map 存多个 DataSource然后每次手动DataSourceUtils.getConnection(dataSource)写出来一堆 try-catch-finally还要自己想着把连接还回去。一旦漏了释放连接连接池直接被打满。后来我换成了dynamic-datasource这套方案用DS注解切换数据源用DSTransaction处理跨库写入项目瞬间清爽不少。这篇文章就把我实际使用的心得和源码层面的原理一起聊透希望能帮到正在被多数据源折磨的人。1. 为什么需要多数据源管理从一段混乱的切换代码说起1.1 业务场景与原始切换方案先说场景。我这个报表聚合服务需要同时读三个库订单库MySQL、历史归档库Oracle、还有一个商户维度的统计库MySQL。三个库分布在不同的实例上连接信息完全不同而且每天凌晨还要跑定时任务把统计结果写回统计库。一开始我图省事直接在 Service 里写了一个DataSourceRouter类似这样public class DataSourceRouter { private static final MapString, DataSource DATA_SOURCE_MAP new ConcurrentHashMap(); public static Connection getConnection(String key) throws SQLException { return DATA_SOURCE_MAP.get(key).getConnection(); } }然后每个方法里都这么取连接Connection conn null; try { conn DataSourceRouter.getConnection(order); // 执行SQL } catch (SQLException e) { log.error(查询失败, e); } finally { if (conn ! null) { conn.close(); } }两个问题很快暴露了。第一业务代码被连接管理逻辑淹没一个查询方法 80% 的代码都在处理连接的获取和关闭真正有价值的 SQL 没几行。第二一旦项目引入了 MyBatis-Plus 这类 ORM 框架数据源切换就彻底乱了因为框架底层自己管理连接你手动拿的连接和框架拿的连接根本不是同一个事务更是各管各的。这个阶段我意识到需要一个统一的路由层让框架层面的所有数据库操作都经过一个动态数据源然后由这个动态数据源根据上下文决定使用哪个真实数据源。这就是dynamic-datasource的核心思想。1.2 dynamic-datasource 的整体设计思路dynamic-datasource是 MyBatis-Plus 生态里非常流行的一个多数据源组件它做的事情说白了就是你告诉它当前线程要用哪个数据源它就把这个信息存到 ThreadLocal 里然后在 MyBatis 执行 SQL 之前通过一个抽象路由数据源把请求转发给对应的真实数据源。它的设计有几个关键点一个动态路由数据源DynamicRoutingDataSource继承自 Spring 的AbstractRoutingDataSource内部维护着所有真实数据源的 Map路由的 key 就是DS注解里写的那个名字。基于 ThreadLocal 的上下文传递当前线程切换到了哪个数据源这个信息存放在DynamicDataSourceContextHolder的 ThreadLocal 里方法执行完毕后必须清理否则线程池复用会导致数据源串线。AOP 拦截注解通过DS注解配合 AOP 切面在方法执行前后自动压栈和弹栈数据源上下文调用方基本无感知。我当时看到这个设计的第一反应是原来最简单可靠的路由方案就是把“当前用哪个库”这种上下文信息塞进线程局部变量里而不是用全局变量。以前我见过的不少项目是拿一个静态变量存当前数据源结果并发一高直接乱套A 请求切到从库B 请求也跟着用了从库。ThreadLocal 天然隔离线程这个坑直接从根上避开了。2. DS注解使用详解从入门到失效场景2.1 引入依赖与基本配置使用dynamic-datasource之前需要先引入依赖。我用的是目前主流的 3.x 版本dependency groupIdcom.baomidou/groupId artifactIddynamic-datasource-spring-boot-starter/artifactId version3.5.2/version /dependency如果你的项目用的 Spring Boot 3.x需要引入 4.x 版本因为 Spring Boot 3 基于 Jakarta EE包名都变了版本不能混用。引入依赖之后在application.yml里配置多数据源。注意主数据源必须叫master这是框架约定的默认路由名其他的可以随便起但是建议见名知意比如order、archive、statisticsspring: datasource: dynamic: primary: master strict: true datasource: master: url: jdbc:mysql://127.0.0.1:3306/order_db?useUnicodetruecharacterEncodingutf8 username: root password: root123 driver-class-name: com.mysql.cj.jdbc.Driver archive: url: jdbc:oracle:thin:127.0.0.1:1521:orcl username: arch password: arch123 driver-class-name: oracle.jdbc.OracleDriver statistics: url: jdbc:mysql://127.0.0.1:3306/stat_db?useUnicodetruecharacterEncodingutf8 username: root password: root123 driver-class-name: com.mysql.cj.jdbc.Driverprimary: master表示默认走主库如果没有加DS注解就用这个默认数据源。strict: true是一个容易被忽略的配置开启后如果指定了一个不存在的数据源 key会直接抛异常而不是静默走主库。我强烈建议生产环境开启否则配置文件里一个单词拼错所有查询都默默打到主库上你根本察觉不到等主库压力爆了才会发现问题。有了基本配置一个最简单的使用方式是直接在 Service 方法上加注解Service public class OrderReportService { DS(order) public ListOrderDO listOrders() { return orderMapper.selectList(null); } DS(archive) public ListArchiveDO listArchive() { return archiveMapper.selectList(null); } }就这么简单DS(order)加在方法上方法里的所有查库操作都会走 order 这个数据源。2.2 DS的四种常见用法DS注解的用法比我想象中灵活我实际项目里用到了四种比较典型的场景。第一种标注在方法上方法内切换这是最常用的方式。注意这里说的“方法内切换”指的是从进入方法到方法结束这个完整范围内所有数据库操作都使用同一个数据源。如果方法内部还调用了其他带DS注解的方法就会发生数据源切换这种嵌套场景后面讲原理的时候会详细说。第二种标注在类上类内所有方法默认使用DS(archive) Service public class ArchiveQueryService { public ListArchiveDO queryByDate(String date) { return archiveMapper.selectByDate(date); } }类级别注解的本质是给类里所有方法一个默认值如果某个方法上也有DS方法级别优先这是典型的就近覆盖原则。第三种使用通配符dynamic-datasource从 3.x 开始支持数据源名称的通配符匹配比如你可以配置一个叫shard_0、shard_1、shard_2的分库组然后这样使用DS(shard_*) public ListOrderDO listShardOrders() { return orderMapper.selectList(null); }框架会从所有已注册的数据源里匹配符合shard_*模式的数据源。这个功能比较适合分库分表场景可以避免编写大量重复的 if-else 判断。不过这里有个细节需要注意如果多个数据源都匹配了同一个通配符表达式框架会选择第一个匹配成功的顺序取决于内部 Map 的遍历顺序不具备确定性。所以我个人建议通配符场景最好保证同一时间只有一个数据源符合预期或者配合 SpEL 表达式精确指定。第四种使用 SpEL 表达式动态决定数据源这才是DS注解最强大的能力。数据源名称可以先不写死而是从方法参数或者请求上下文里动态解析。比如租户ID作为数据源后缀DS(#tenantId) public ListTenantOrderDO listTenantOrders(String tenantId) { return tenantOrderMapper.selectByTenant(tenantId); }再比如从请求头里动态获取数据源名称DS(#header.datasource) public ListUserDO listUsers(HttpServletRequest request) { return userMapper.selectList(null); }SpEL 表达式支持方法参数名、请求头、请求参数等常见上下文。实现原理是框架内部通过DsSpelExpressionHandler解析注解里的表达式然后跟方法参数进行绑定最后得到真正要路由的数据源 key。这种方式在多租户系统里尤其好用每个租户的数据源后缀就是租户ID代码里不用写一堆判断分支路由信息完全动态化。2.3 优先级、通配符与失效场景先说优先级规则DS注解的优先级从高到低是方法注解 类注解 primary默认数据源。这个优先级很好理解方法上没写就用类的默认值类上没写就回落到主库。但是我用的时候踩过一个大坑必须提醒各位DS注解不能跨方法传播。什么意思如果你在 ServiceA 的方法里调用了 ServiceB 的方法ServiceA 方法没有注解ServiceB 方法有注解那么 ServiceB 方法内部走的是 ServiceB 指定的数据源执行完弹栈之后ServiceA 方法剩下的代码会回到默认数据源。这个行为很正常但如果 ServiceA 方法有注解内部调了 ServiceB 另一个数据源的方法则会出现先压栈再弹栈的嵌套情况类似这样DS(order) public void queryFromOrderAndArchive() { // 当前数据源order ListOrderDO orders orderMapper.selectList(null); // 内部调用切换到 archive ListArchiveDO archives archiveService.queryArchive(); // 回到 order ListOrderDO moreOrders orderMapper.selectList(null); }如果代码里大量使用这种嵌套切换一定要确认“压栈”和“弹栈”是严格对称的。框架本身的设计是栈结构进入方法压栈退出方法弹栈正常情况下没问题。但我见过一种极端情况方法内部捕获了异常导致 AOP 切面的清理逻辑没有执行到ThreadLocal 里的数据源 key 没被弹出线程归还给线程池后下一个请求复用了这个线程数据源就串了。这种问题排查起来非常痛苦表象是偶发性的“查到了别人的库”实际上就是脏上下文残留。DS失效的场景有几个我整理一下同类内部调用Spring AOP 基于代理如果你在一个类的内部通过this调用另一个带DS的方法代理不生效注解直接失效。解决方法是注入自身代理或者拆分成不同 Bean。方法被final修饰CGLIB 代理无法覆盖 final 方法注解失效。DS标注在私有方法上Spring AOP 无法拦截私有方法注解直接失效这个问题很容易被新手忽略。3. DSTransaction事务注解跨库写操作的取舍3.1 为什么 Transactional 管不住多数据源当项目只有一个数据源的时候Spring 的Transactional是直接把事务委托给这个数据源对应的事务管理器。但在多数据源场景下问题就来了Spring 默认只有一个事务管理器它绑定的是某一个数据源。你DS切换到了另一个数据源但事务管理器还是原来那个事务根本作用不到新数据源上。举个具体例子Transactional DS(order) public void saveOrderAndLog() { orderMapper.insert(order); // 写入 order 库 logMapper.insert(log); // 写入 log 库如果 log 库不是 order }假设order和log是两个库Spring 的事务管理器绑定在哪个库上取决于你注入的事务管理器。如果事务管理器绑定的是 order 库那么 log 库的写入就没有事务保护中途任何一步出错order 库回滚了log 库却写进去了。这就是最典型的“跨库事务失效”。有人可能会说那我用 JTAJava Transaction API做分布式事务不就行了理论上是但 JTA 在实际项目中落地非常重需要应用服务器支持依赖 XA 协议性能开销大很多普通业务根本不需要这么强的保障。dynamic-datasource的DSTransaction走的是另一条路在尽量不引入额外中间件的前提下通过统一管理多个数据源连接上的事务提交与回滚解决一部分跨库事务问题。3.2 DSTransaction 的用法与全局事务开关DSTransaction DS(order) public void saveOrderAndLog() { orderMapper.insert(order); logMapper.insert(log); }需要在启动类或者配置类上开启全局事务支持SpringBootApplication EnableTransactionManagement public class Application { public static void main(String[] args) { SpringApplication.run(Application.class, args); } }实际上只要项目里引入了dynamic-datasource-spring-boot-starter它内部会自动注册相关的事务支持组件普通场景不需要额外配置。但在使用DSTransaction时有一个内置的事务管理器需要了解一下叫DataSourceTransactionManager。它会在事务开始之前把当前线程上下文中选定的数据源 key 记录下来然后针对这个数据源创建连接、开启事务事务结束的时候根据是否有异常决定提交还是回滚。这里有一个非常重要的认知我必须明确写出来DSTransaction并不是严格意义上的分布式事务。它本质上做的是“多数据源本地事务的协调”面对多个库各自的事务它尽量让所有库的提交动作靠后减少不一致窗口但如果中途崩溃或者提交阶段出现网络分区依然可能出现一部分库提交成功、另一部分失败的情况。所以如果你在做支付、库存扣减这类强一致性业务DSTransaction只能作为临时方案长期看还是得上 Seata、RocketMQ 事务消息这类真正的分布式事务方案。我用DSTransaction时的真实感受是它解决的最大痛点是“多个数据源操作都在同一个方法里以前只能保证一个库的事务现在大多数情况下多个库能同步提交或同步回滚”。比如上面的saveOrderAndLog如果没有DSTransactionlog 库写入失败时order 库不会回滚有了DSTransaction两个库都会回滚。对于很多内部管理系统、报表系统来说这个能力已经够用了。注意一个细节DSTransaction和Transactional不能同时使用因为 Spring 的事务切面和dynamic-datasource的事务切面会发生代理顺序冲突导致事务管理器行为异常。如果确实需要在多数据源方法上控制事务就统一用DSTransaction来控制。3.3 连接池与性能注意事项多数据源必然会带来连接池数量的增加。每个数据源都会有自己独立的连接池配置连接数的计算公式不再是“一个池子撑起所有流量”而是每个池子需要单独评估。我一般建议给核心数据源配置独立连接池参数避免默认值不合适spring: datasource: dynamic: datasource: master: url: jdbc:mysql://127.0.0.1:3306/order_db username: root password: root123 driver-class-name: com.mysql.cj.jdbc.Driver hikari: minimum-idle: 5 maximum-pool-size: 20 connection-timeout: 30000 idle-timeout: 600000 archive: url: jdbc:oracle:thin:127.0.0.1:1521:orcl username: arch password: arch123 driver-class-name: oracle.jdbc.OracleDriver hikari: minimum-idle: 2 maximum-pool-size: 5 connection-timeout: 30000 idle-timeout: 600000当时的经验是低频归档查询的库连接池不用开很大否则连接一直闲着白白占着数据库资源高频写入的库连接池需要留足余量尤其是定时任务批量导数据的时候连接数瞬间就被打起来了。另外一个性能注意点DSTransaction会同时持有多个数据源的连接而且持有时间较长直到所有操作完成后才统一提交或回滚。这意味着事务期间占用的连接数等于涉及的库数量。如果一个方法同时操作三个库高峰期 100 个并发三个连接池就会被各占 100 个连接。因此使用DSTransaction的方法要尽量短不要在事务里做耗时操作比如远程调用、文件IO否则连接池很快耗尽。4. 源码原理分析路由、AOP与事务衔接4.1 路由起点DynamicRoutingDataSource 与 determineCurrentLookupKey从源码层面看一切的核心是DynamicRoutingDataSource这个类。它继承自 Spring 的AbstractRoutingDataSource这个抽象类的思路很优雅不管底层有多少个真实数据源对外统一暴露一个DataSource接口。Spring 在需要获取连接的时候会调用一个关键方法protected abstract Object determineCurrentLookupKey();AbstractRoutingDataSource的getConnection()方法内部逻辑大致是调用determineCurrentLookupKey()得到当前线程的路由 key。根据 key 从targetDataSources这个 Map 中找到对应的真实数据源。调用真实数据源的getConnection()返回连接。DynamicRoutingDataSource重写了这个方法实现如下简化版Override protected DataSource determineDataSource() { String dsKey DynamicDataSourceContextHolder.getDataSourceLookupKey(); // 根据 dsKey 去匹配支持通配符 DataSource dataSource getDataSource(dsKey); return dataSource; }看到这里你可能会想这不就是个 key-value 查找吗是的核心路由逻辑就是这么朴素。真正有意思的是这个DynamicDataSourceContextHolder.getDataSourceLookupKey()它从哪里拿到 key答案是 ThreadLocal。DynamicDataSourceContextHolder内部维护了一个ThreadLocalDequeString不是简单的ThreadLocalString。为什么用队列因为数据源切换是允许嵌套的方法 A 切到 order方法 A 内部调用方法 B 切到 archive方法 B 执行完需要恢复 order 状态这种场景就是典型的栈结构。AOP 切面在方法执行前pushkey方法执行后popkey保证上下文状态正确。public final class DynamicDataSourceContextHolder { private static final ThreadLocalDequeString LOOKUP_KEY_HOLDER new ThreadLocal(); public static void push(String ds) { DequeString deque LOOKUP_KEY_HOLDER.get(); if (deque null) { deque new ArrayDeque(); LOOKUP_KEY_HOLDER.set(deque); } deque.push(ds); } public static String peek() { DequeString deque LOOKUP_KEY_HOLDER.get(); return deque null ? null : deque.peek(); } public static void poll() { DequeString deque LOOKUP_KEY_HOLDER.get(); if (deque null || deque.isEmpty()) { return; } deque.poll(); if (deque.isEmpty()) { LOOKUP_KEY_HOLDER.remove(); } } }注意看poll方法最后的LOOKUP_KEY_HOLDER.remove()。这是最关键的一步当栈空了之后必须直接移除 ThreadLocal 里的值。如果不移除线程池里的线程被复用后ThreadLocal 里的旧值可能被下一个请求读到造成数据源混乱。这也是我上面提到的脏上下文问题的根源。4.2 AOP拦截与数据源上下文压栈有了DynamicRoutingDataSource做路由还需要一个入口来告诉它当前线程该用哪个数据源。这个入口就是DS注解加上 AOP 切面。框架内部定义了一个DynamicDataSourceAnnotationAdvisor它是一个AbstractPointcutAdvisor切点匹配所有标了DS注解的方法。拦截逻辑在DynamicDataSourceAnnotationInterceptor里核心代码如下简化版public class DynamicDataSourceAnnotationInterceptor implements MethodInterceptor { Override public Object invoke(MethodInvocation invocation) throws Throwable { // 获取当前方法上的 DS 注解如果没有继续向上找类上的 DS DS ds determineDatasourceAnnotation(invocation); if (ds ! null) { String dsKey ds.value(); // 如果注解值是一个 SpEL 表达式这里会去解析 if (StringUtils.hasText(dsKey) dsKey.startsWith(#)) { dsKey resolver.evaluate(dsKey, invocation); } // 数据源 key 压栈 DynamicDataSourceContextHolder.push(dsKey); } try { return invocation.proceed(); } finally { if (ds ! null) { // 方法执行完毕弹栈 DynamicDataSourceContextHolder.poll(); } } } }这个拦截器的实现有几个关键点值得展开。第一注解查找顺序。determineDatasourceAnnotation会先看方法上有没有DS再看方法所属类上有没有这是前面讲的“方法优先”原则的源码依据。第二SpEL 表达式的解析时机。如果DS的值以#开头框架会认为是 SpEL 表达式通过DsSpelExpressionHandler去解析。这里有一个性能问题SpEL 解析是有开销的如果你在高频路径上使用 SpEL最好评估一下对吞吐量的影响。第三finally 块里的清理逻辑。poll放在 finally 里保证方法即使抛出异常也能把上下文清干净。这个设计是好的但前提是方法没有被DSTransaction之类的切面包住导致代理顺序变化。我曾经遇到过自定义 AOP 切面和框架切面执行顺序冲突导致上下文清理时机不对最后查了很长时间才发现是自己切面的 order 设得太靠前影响了栈的对称性。4.3 DSTransaction 事务管理器的注册与提交逻辑DSTransaction的实现比DS复杂因为它涉及多个数据源连接的事务协调。框架在启动时会注册一个DataSourceTransactionManager类型的 Bean这个事务管理器会被 Spring 的事务基础设施使用。当方法标注了DSTransaction框架会在事务开启前获取当前数据源 key然后在事务同步管理器里保存这个 key。多个数据源操作发生时每个数据源连接各自开启本地事务框架记录了所有参与事务的数据源连接。提交时框架按照注册顺序依次提交各个连接上的事务。回滚时按照相反顺序依次回滚。这个设计有一个朴素的思想尽可能保证“要么都提交要么都回滚”但正如前面说的它做不到强一致。假如提交第二个库的时候进程宕机了第一个库已经提交成功就会出现数据不一致。为什么框架要把提交顺序逆序我个人的理解是先提交最不重要的库后提交核心库。如果提交过程中出现问题核心库的数据还没提交至少可以手动修正次要库的数据。当然这只是一种补偿思路真正可靠的做法还是引入可靠消息或者分布式事务中间件。从使用角度讲我对DSTransaction的定位是“在不需要强一致但需要多库操作要么全成功要么全失败的场景下比裸写多个单数据源事务更靠谱”。比如写一个日志聚合表同时更新统计表两个库一笔操作用DSTransaction体验很好如果是“先扣用户余额再加商户钱包”这两笔钱相关性强我宁可使用 Seata 或者补救消息。4.4 与 MyBatis-Plus 等框架的联动dynamic-datasource和 MyBatis-Plus 的配合是最常见的组合。MyBatis-Plus 的SqlSessionFactory在创建时会注入一个DataSource这个 DataSource 直接注入DynamicRoutingDataSource。也就是说MyBatis 每次获取连接拿到的都是动态路由数据源里的连接而不是某个固定库的连接。这里顺带解释一个经常有人问的点为什么 Mapper 接口不用做任何数据源相关的配置因为 Mapper 本身不感知数据源它只负责执行 SQL数据源是通过SqlSessionFactory从外部注入的。所有 Mapper 共用同一个 SqlSessionFactory但每次执行 SQL 之前线程上下文里的数据源 key 已经变了路由数据源就会把连接切换到对应的库。这个机制保证了你不需要为每个库创建一套 Mapper。不过我遇到过一个情况某些 Mapper 方法内部有多条 SQL比如 MyBatis 的一级缓存和批处理特性会使得第一条 SQL 获取的连接被缓存后续 SQL 复用同一条连接。这种情况下即使你方法中途通过某种方式修改了线程上下文的数据源 key同一连接也不会切换因为连接已经绑定了具体的数据源。这也是为什么DS切换粒度是“方法级别”而不是“方法内语句级别”你不能指望在一个 Mapper 方法内部实现跨库操作。5. 常见问题与排查技巧实录5.1 常见问题速查表我把自己和朋友实际踩过的坑整理了一下做成表格方便大家排查时对照。现象可能原因解决方案方法没加 DS 却走了从库类上或父类方法上有注解或者前一个请求的脏上下文残留检查类级别注解检查线程池复用场景确认 poll 逻辑正常DS 指定了不存在的数据源静默走主库strict配置为 false将spring.datasource.dynamic.strict设置为 true同类内部方法调用 DS 不生效Spring AOP 代理不拦截内部调用拆分成独立 Bean或者通过AopContext.currentProxy()获取代理对象方法加了 Transactional 和 DS跨库回滚失败Spring 事务管理器绑定单数据源无法管理多库事务使用 DSTransaction 替代或者确认是否为多库场景线程池中出现了数据源串线方法异常导致 ThreadLocal 清理不完整检查自定义 AOP 是否影响了框架切面的 finally 执行使用 try-finally 保护方法切换数据源后PageHelper 分页查询失效PageHelper 的拦截器缓存了数据源类型或者方言检查 PageHelper 配置建议按数据库方言单独配置拦截器连接池耗尽异常多数据源事务持有多个连接连接数暴增缩小事务范围优化连接池参数避免在事务内做 IO 等耗时操作动态数据源 key 通过方法参数传递失败SpEL 表达式无法解析到参数名确认编译参数-parameters已开启或者使用Param明确指定参数名5.2 一些实战中的排错经验排错经验这块我想多说几句因为很多问题不是百度能查到的。第一个经验遇到数据源莫名其妙的切换先查 ThreadLocal 残留再查注解配置。我在排查生产问题的时候最常用的手段是在出现异常的入口打印当前线程的数据源 keyString currentKey DynamicDataSourceContextHolder.peek(); log.info(当前数据源 key: {}, currentKey);如果发现 key 跟预期不符说明上下文已经被污染了。这时候重点排查是否有自定义线程池包裹了业务逻辑线程池的 Worker 线程复用时ThreadLocal 不会被自动清理这是最常见的污染源。第二个经验动态数据源配合事务时一定要梳理代理顺序。dynamic-datasource的切面 order 默认是比较靠后的Spring 事务切面 order 默认也比较靠后。如果你自定义了切面比如用户权限切面、日志切面它们的 order 可能和框架切面产生冲突。一旦顺序乱了可能出现“先开事务再切数据源”的情况导致事务绑定了错误的数据源连接。解决办法是通过Order注解明确控制自定义切面的执行顺序让数据源切面先于事务切面执行。第三个经验不要在一个事务里切换多种数据库方言。我刚开始测试时在一个DSTransaction方法里先查 Oracle 再写 MySQL本地开发机还跑得动一上测试环境就报 SQL 方言错误。这是因为 MyBatis 的 SQL 方言判断通常基于数据源类型如果你在同一个事务里横跨多个数据库一些框架的 SQL 优化功能会错乱。实际业务中跨数据库类型读写尽量拆分成不同的方法用编程式事务或消息队列解耦。第四个经验监控每个数据源的连接池使用率。连接池监控是最容易忽视的一环。多数据源场景下某个低频数据源的连接池配置往往照搬主库结果定时任务一跑连接数不够直接打满应用崩溃。我给那些低频数据源加了单独的配置并且把连接池指标接到监控告警上。这些都是一次事故换来的教训。6. 个人实践中的几点心得说到最后分享几个我自己的选型和落地心得。第一能用单库解决的业务别急着上多数据源。多数据源最大的隐性成本不是配置而是事务、分页、缓存等横切能力的复杂度成倍上升。如果只是读写分离考虑在主库上做只读副本通过数据库层面解决不要简单粗暴地给每个查询配一个数据源。第二数据源命名要有一套规范。我见过有人给数据源起名ds1、ds2、db_primary代码里DS(ds1)满天飞过三个月自己都不记得 ds1 是哪个库。我后来统一用业务域命名order、user、archive、report配置中心和代码里的注解名一一对应维护成本低很多。第三多数据源不是分布式事务的替代品。这是一个需要反复强调的认知。DSTransaction在小型项目和内部系统里很好用但面对资金类的强一致场景还是要踏踏实实引入分布式事务框架做一套完整的异常补偿机制甚至人工对账流程。我在项目里设置了每日凌晨的数据一致性巡检任务专门检查多数据源事务涉及的关键表如果发现不一致就自动告警。这个机制虽然土但非常有效。第四升级版本要谨慎。dynamic-datasource3.x 和 4.x 之间的 API 有变化如果你从 3.x 盲目升级到 4.x一些配置项可能失效。升级前先看官方文档的 breaking changes最好在测试环境跑一遍多数据源回归用例。其实回过头看多数据源管理的核心并不复杂一个动态路由数据源 一个线程上下文 一个 AOP 切面就解决了“怎么切”的问题而DSTransaction解决的是“切了之后怎么保持一致性”的问题。理解了这个模型以后再遇到动态数据源的变体比如配置中心动态修改数据源、灰度流量切库你也能很快上手。最后再分享一个小技巧排查多数据源问题时打开dynamic-datasource的 debug 日志非常管用。在配置文件里加上logging: level: com.baomidou.dynamic.datasource: debug它能打印出每一次路由的数据源 key、当前线程栈深度、以及实际匹配到的数据源名称。我在生产环境定位“偶发切错库”的问题时就是靠这行配置快速锁定了线程池里脏上下文残留的位置。这种排查手段比一遍遍读代码高效得多建议你遇到类似问题先打开日志再看源码。
返回列表