🏆本文收录于「滚雪球学SpringBoot」(全网一个名)专栏,希望能够助你一臂之力,帮你早日登顶实现财富自由🚀;同时,欢迎大家关注&&收藏&&订阅!持续更新中,up!up!up!!
发生背景
一早,我的女同事小L一脸愁容地找到我,寻求我的帮助:"为啥我们的同步任务失效了,明明我在代码中加了注解 @DataSource(value = DataSourceType.SLAVE)
指定从库,但每次查询的时候,读的还是主库?匪夷所思,太奇怪了!"
听到这个问题,我心里大概有了点想法。小L接着补充道:"我特意检查过,从库没有问题,动态数据源切换也没问题,Mapper 的查询方法写得很干净。唯一的不同就是,我在方法上加了个 @Transactional(rollbackFor = ServiceException.class)
,用于事务控制。可是,这跟数据源路由有什么关系呢?"
这是一个看似简单却非常常见的问题:读写分离本该是最优化查询性能的方案,但为什么只因为加了一个事务注解,就导致从库查询失效?为了帮助我的同事小L理清楚问题的来龙去脉,我和她一起分析代码和日志,最终搞清了其中的关键点。以下就是这次分析的完整过程,采坑经验分享给大家。
1. 问题现象
在小L负责的同步任务代码中,存在一个非常常见的场景:读取从库数据并写入主库。示例代码如下:
java
@Transactional(rollbackFor = ServiceException.class)
public void syncDepartments() {
// 从库查询部门数据
List<Map> departments = oldUserDataMapper.selectListByParam();
// 主库写入或更新部门数据
for (Map department : departments) {
departmentMapper.insertOrUpdate(department);
}
}
OldUserDataMapper
上加了动态数据源注解,明确指定查询应路由到从库。示例代码如下:
java
@DataSource(value = DataSourceType.SLAVE)
public interface OldUserDataMapper {
@Select("SELECT * FROM SECF_SYS_ORG_DEPART")
List<Map> selectListByParam();
}
从如上代码层上,selectListByParam()
应从从库查询数据。但在实际执行时,我们发现查询直接走了主库。这种现象不仅拖慢了任务效率,还让读写分离形同虚设。那么,究竟是什么原因导致事务环境下的查询不遵循动态数据源的配置呢?我们继续剖析...
2. 核心原因剖析
要理解这个问题,必须从两个关键点入手:
- Spring 事务管理的行为和规则。
- 动态数据源的路由机制。
2.1 Spring 事务的默认行为
我们都知道 @Transactional
是 Spring 中的一个核心注解,用于声明事务边界,确保方法内的所有数据库操作要么全部成功,要么全部回滚。它的默认行为是:
- 开启一个读写事务(
readOnly = false
)。 - 事务内的所有数据库操作共享一个数据库连接(Connection),由事务管理器接管。
- 为了保证事务中的数据一致性,事务管理器会尽量将所有操作路由到主库。
2.1.1 数据一致性的考虑
在事务中,Spring 强制使用主库的一个重要原因是避免 从库数据延迟 的问题。分布式架构下,主从库之间通过异步同步数据,可能存在一定的延迟。如果事务在从库中读取到旧数据,再在主库中写入,就可能导致数据不一致。为了避免这种风险,Spring 默认将所有事务中的操作(包括查询)都路由到主库。
2.1.2 事务传播和数据源切换的冲突
事务传播是 Spring 事务管理的重要特性。默认情况下,一个事务方法调用其他事务方法时,都会复用当前事务的上下文和连接。这意味着:
- 如果主方法中打开了主库连接,所有子方法也会默认复用主库连接。
- 动态数据源的切换逻辑在事务环境下可能失效,因为数据源路由的优先级低于事务管理的优先级。
2.2 动态数据源的路由规则
动态数据源是实现读写分离的核心机制,其路由逻辑通常基于以下条件:
- 方法上是否加了
@DataSource
注解,显式指定主库或从库。 - SQL 操作类型是否是只读(
SELECT
)或写入(INSERT
、UPDATE
)。 - 当前是否处于事务环境。
一般来说,动态数据源的实现会优先检查事务环境。如果当前事务不是只读事务(readOnly = false
),则会将所有操作强制路由到主库。这种规则是为了保证事务内操作的数据一致性。
3. 为什么查询会走主库?
综合以上原理分析,我们可以明确以下几点原因:
3.1 默认读写事务导致强制路由到主库
在代码中,@Transactional(rollbackFor = ServiceException.class)
默认开启了读写事务(readOnly = false
)。Spring 事务管理器出于一致性考虑,将查询操作也强制路由到了主库,覆盖了动态数据源的 @DataSource
配置。
3.2 数据源路由优先级低于事务管理
尽管 @DataSource(value = DataSourceType.SLAVE)
明确指定了从库,但在事务环境下,事务管理优先级更高。事务管理器会优先根据事务属性(读写事务 vs 只读事务)决定路由规则。
3.3 共享的数据库连接绑定了主库
Spring 的事务机制中,事务内的所有操作共享同一个数据库连接。由于事务在开始时绑定了主库连接,后续的查询操作即使被标记为从库,也会复用主库连接。
4. 解决方案
针对上述问题,可以通过以下几种方式解决。
方法 1:显式声明只读事务
如果查询方法只涉及读取操作,可以将事务声明为只读事务。Spring 事务管理器会识别只读事务,并允许路由到从库。
java
@Transactional(readOnly = true)
public List<Map> getDepartments() {
return oldUserDataMapper.selectListByParam(); // 查询从库
}
方法 2:分离读写逻辑
对于同时包含查询和写入的场景,可以将查询操作独立到一个只读事务方法中,保证查询走从库,写操作依然在主库。
java
@Transactional
public void syncDepartments() {
// 查询从库
List<Map> departments = getDepartmentsFromSlave();
// 写入主库
for (Map department : departments) {
departmentMapper.insertOrUpdate(department);
}
}
@Transactional(readOnly = true)
public List<Map> getDepartmentsFromSlave() {
return oldUserDataMapper.selectListByParam(); // 查询从库
}
方法 3:移除事务,完全依赖动态数据源
如果查询方法不需要事务支持,可以直接移除 @Transactional
注解,让动态数据源的路由逻辑完全接管数据源选择。
java
public List<Map> getDepartments() {
return oldUserDataMapper.selectListByParam(); // 查询从库
}
方法 4:检查动态数据源实现
如果使用的是第三方动态数据源(如 Druid 或自定义实现),需要确保它支持事务内的只读路由逻辑。例如,在路由逻辑中根据事务的只读属性切换数据源。
java
if (TransactionSynchronizationManager.isCurrentTransactionReadOnly()) {
// 只读事务,路由到从库
return DataSourceType.SLAVE;
} else {
// 读写事务,路由到主库
return DataSourceType.MASTER;
}

5. 总结
综上所述,通过以上剖析,我们可以明确:加了 @Transactional
后查询走主库,主要是因为事务默认以数据一致性为优先考虑,而动态数据源的路由规则被事务管理器覆盖。解决方法包括但不限于:
- 使用
@Transactional(readOnly = true)
标记只读事务。 - 将读写逻辑分离到不同的方法中,分别处理从库查询和主库写入。
- 移除事务注解,完全交给动态数据源来管理数据源路由。
- 优化动态数据源实现,使其更好地支持事务内的从库查询。
最终,我们通过上述方法3成功解决了女同事小L的问题,也让整个团队更加清晰地认识到事务机制和动态数据源交互的复杂性。此次经历为后续优化读写分离的实践提供了宝贵的经验。
小L同学,她也在午饭的时候单独约我,给我加了只鸡腿以示感谢,感激我为她解决此bug给的奖励,爆赞!我最后想说的是,同学们,一个人或许走的很快,但一群人才能走的更远。
📣 关于我
我是bug菌,CSDN | 掘金 | InfoQ | 51CTO | 华为云 | 阿里云 | 腾讯云 等社区博客专家,C站博客之星Top30,华为云2023年度十佳博主,掘金多年度人气作者Top40,掘金等各大社区平台签约作者,51CTO年度博主Top12,掘金/InfoQ/51CTO等社区优质创作者;全网粉丝合计 30w+;硬核微信公众号「猿圈奇妙屋」,欢迎你的加入!免费白嫖最新BAT互联网公司面试真题、4000G PDF电子书籍、简历模板等海量资料,你想要的我都有,关键是你不来拿哇。
-End-