🏆本文收录于「滚雪球学SpringBoot」(全网一个名)专栏,希望能够助你一臂之力,帮你早日登顶实现财富自由🚀;同时,欢迎大家关注&&收藏&&订阅!持续更新中,up!up!up!!
背景故事
一次代码评审会上,经理在特地抽查了项目组里小C的定时同步模块,要他展示下他最近的一个同步任务代码,负责由从库读取数据再写入主库,代码逻辑非常简单&&清晰,示例代码框架如下:
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();
}
然而,当他跑了一下同步任务后,却发现问题来了:查询老是走主库而没切到从库上!无论是日志打印的执行 SQL,还是数据库性能监控,都表明查询根本没有走从库。
"小C,你是不是配置写错了?"我的同事小美立马给出指示。小C当即否定:"我查过配置,数据源的切换逻辑没问题,而且其他地方都能正常路由到从库。唯独这里加了事务的方法,查询就是绕不过主库。"
这时候,大家开始意识到,问题的根源可能不是出在动态数据源上,而是和该Spring事务机制本身有关。为了解决这个问题,我们从事务管理和数据源路由机制两方面入手,最终破解了查询强制走主库的根本原因。
1. 问题现象:加了事务后查询总跑主库?
在代码中,我们可以得知,通过动态数据源和 @DataSource
注解实现读写分离:
- 写操作路由到主库(
DataSourceType.MASTER
)。 - 读操作路由到从库(
DataSourceType.SLAVE
)。
这在非事务环境下运行良好一切正常,但一旦方法上加了事务注解 @Transactional
,即使查询方法被显式标注为从库(@DataSource(value = DataSourceType.SLAVE)
),查询依然被强制路由到了主库,动态数据源的路由规则似乎完全失效,这究竟是怎么一回事呢?
为什么会出现这种现象?同学们,你们也可以先想想这个问题,随后我会给出正确解决思路。其实呢,此问题就需要从 Spring 的事务机制和动态数据源的路由规则谈起。
2. 核心原因剖析
要解决这个问题,必须从 Spring 事务管理的工作机制 和 动态数据源路由的优先级 两个角度切入,深度挖掘并分析。
2.1 Spring 事务管理的默认行为
@Transactional
-- 它是由 Spring 提供的核心注解之一,用于声明事务边界。它的默认行为主要有以下几点,大家遗忘了的可以回顾下:
1. 事务默认是读写事务
@Transactional
注释,正常使用,默认开启的是读写事务(readOnly = false
)。在这种事务模式下,Spring 假定事务可能同时包含读和写操作,而为了保证事务的一致性,Spring 会强制将所有操作路由到主库。
此话怎理解?且听我细细道来。这是因为在读写分离场景中,从库的数据可能存在一定的同步延迟。如果事务中的读操作被路由到从库,而从库数据尚未同步主库的写操作,就可能导致事务逻辑的错误。例如:
- 主库写入了新数据,但从库尚未同步;
- 事务中的查询从从库读取到旧数据,导致逻辑错误。
为避免这种数据不一致问题,Spring 事务管理器就会在事务开启时直接绑定主库,确保数据的一致性。
2. 事务内共享数据库连接
Spring 的事务管理机制中,事务内的所有数据库操作(包括读和写)共享同一个数据库连接(Connection)。这个连接在事务开始时绑定到特定的数据源(通常是主库)。因此,即使某个方法标注了从库数据源,也无法切换到新的连接来访问从库。
3. 事务传播与数据源冲突
事务注解支持传播机制(PROPAGATION_*
),比如嵌套事务会复用外部事务的连接。如果外部事务绑定了主库,嵌套的查询方法也只能复用主库连接,从而导致从库查询失效。
2.2 动态数据源的路由规则
动态数据源的核心功能是根据上下文条件选择主库或从库。常见的实现(如基于 AbstractRoutingDataSource
的实现)通常遵循以下规则:
- 非事务环境 :
SELECT
路由到从库。- 写操作(如
INSERT
、UPDATE
)路由到主库。
- 事务环境 :
- 如果是读写事务(默认模式),所有操作强制路由到主库。
- 如果是只读事务(
readOnly = true
),允许路由到从库。
但在事务环境下,动态数据源的路由规则会被事务管理器覆盖。事务管理器优先决定数据库连接的绑定,从而影响数据源的选择。
3. 为什么查询会强制走主库?
结合上述分析,我们可以总结出以下几点原因:
-
读写事务强制绑定主库
@Transactional
默认开启读写事务,Spring 为了保证数据一致性,会直接将所有操作路由到主库。- 即使查询方法被标注为从库数据源,动态数据源的配置也会被事务管理器覆盖。
-
事务内连接无法切换数据源
- 事务中的所有操作共享一个数据库连接。如果事务开始时绑定了主库连接,后续查询也只能使用这个连接,无法切换到从库。
-
事务的优先级高于动态数据源
- 在事务环境下,Spring 会优先根据事务的读写属性决定数据源,而动态数据源的路由规则优先级较低。

4. 解决方法
针对上述问题,可以采用以下几种方法来解决。
方法 1:显式声明只读事务
如果查询操作不涉及写操作,可以将事务声明为只读事务(readOnly = true
)。Spring 识别到只读事务后,会允许查询操作路由到从库,示例代码如下:
java
@Transactional(readOnly = true)
public List<Map> getDepartments() {
return oldUserDataMapper.selectListByParam(); // 查询从库
}
以下是对这段代码的详细解析,重点解释 @Transactional(readOnly = true)
的作用以及它如何影响方法的行为。
1. 方法签名
- 返回值类型 :
List<Map>
这表示该方法返回一个List
集合,集合中的每个元素都是一个Map
。通常,Map
用于存储键值对,例如数据库查询结果中的一行数据可以存储为一个Map
,其中键是列名,值是对应的列值。 - 方法名 :
getDepartments
这是一个符合Java命名规范的方法名,表示该方法的功能是获取部门信息。
2. @Transactional(readOnly = true)
注解
@Transactional
这是Spring框架提供的注解,用于声明事务管理。它确保方法在事务上下文中执行。如果方法执行过程中发生异常,事务会回滚,确保数据的一致性。readOnly = true
这个属性表示当前事务是只读的。在只读事务中,Spring会通知底层数据库驱动或连接池,当前操作不会修改数据。这可以带来以下好处:- 性能优化:数据库可以利用只读事务的特性,减少锁的使用,提高查询性能。
- 资源管理:连接池可以优化只读事务的连接管理,例如使用专门的只读连接池,进一步提高性能。
3. 方法体
oldUserDataMapper.selectListByParam()
这是方法的核心逻辑,调用了oldUserDataMapper
对象的selectListByParam
方法。oldUserDataMapper
这是一个Mapper对象,通常是由MyBatis框架生成的。Mapper对象用于与数据库进行交互,封装了对数据库的操作逻辑。从命名来看,oldUserDataMapper
可能是一个与旧用户数据相关的Mapper,这里用于查询部门信息。selectListByParam
方法 这是一个查询方法,具体实现未给出,但从命名可以推测它的功能:selectList
:表示查询并返回一个列表(List
)。ByParam
:表示该方法可能接受某些参数来过滤查询结果。虽然代码中没有显示传递参数,但方法名暗示了它可能支持参数化查询。
4. 注释
// 查询从库
这是一个单行注释,说明了selectListByParam
方法的作用是从从库中查询数据。在主从复制的数据库架构中,从库通常用于读操作,而主库用于写操作。这样可以分散读写压力,提高系统的性能和可用性。
方法 2:移除事务注解
如果查询方法不需要事务支持,可以直接移除 @Transactional
注解,让动态数据源完全控制数据源的选择,示例代码如下:
java
public List<Map> getDepartments() {
return oldUserDataMapper.selectListByParam(); // 查询从库
}
在本次的代码演示中,我将会深入剖析每句代码,详细阐述其背后的设计思想和实现逻辑。通过这样的讲解方式,我希望能够引导同学们逐步构建起对代码的深刻理解。我会先从代码的结构开始,逐步拆解每个模块的功能和作用,并指出关键的代码段,并解释它们是如何协同运行的。通过这样的讲解和实践相结合的方式,我相信每位同学都能够对代码有更深入的理解,并能够早日将其掌握,应用到自己的学习和工作中。
1. 方法签名
- 返回值类型:
List<Map>
这表示该方法返回一个List
集合,集合中的每个元素都是一个Map
。Map
通常用于存储键值对,例如数据库查询结果中的一行数据可以存储为一个Map
,其中键是列名,值是对应的列值。 - 方法名 :
getDepartments
这是一个遵循Java命名规范的方法名,表示该方法的功能是获取部门信息。
2. 方法体
oldUserDataMapper.selectListByParam()
这是方法的核心逻辑,调用了oldUserDataMapper
对象的selectListByParam
方法。oldUserDataMapper
这是一个Mapper对象,通常是由MyBatis框架生成的。Mapper对象用于与数据库进行交互,封装了对数据库的操作逻辑。从命名来看,oldUserDataMapper
可能是一个与旧用户数据相关的Mapper,这里用于查询部门信息。selectListByParam
方法
这是一个查询方法,具体实现未给出,但从命名可以推测它的功能:selectList
:表示查询并返回一个列表(List
)。ByParam
:表示该方法可能接受某些参数来过滤查询结果。虽然代码中没有显示传递参数,但方法名暗示了它可能支持参数化查询。
3. 注释
// 查询从库
这是一个单行注释,说明了selectListByParam
方法的作用是从从库中查询数据。在主从复制的数据库架构中,从库通常用于读操作,而主库用于写操作。这样可以分散读写压力,提高系统的性能和可用性。
方法 3:分离读写逻辑
对于既有查询又有写入的场景,可以将查询逻辑抽取到独立的只读事务方法中,确保查询走从库,写操作仍走主库。示例代码如下:
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();
}
在本次的代码演示中,我将会深入剖析每句代码,详细阐述其背后的设计思想和实现逻辑。通过这样的讲解方式,我希望能够引导同学们逐步构建起对代码的深刻理解。我会先从代码的结构开始,逐步拆解每个模块的功能和作用,并指出关键的代码段,并解释它们是如何协同运行的。通过这样的讲解和实践相结合的方式,我相信每位同学都能够对代码有更深入的理解,并能够早日将其掌握,应用到自己的学习和工作中。
如上代码是一个典型的Spring框架中的事务管理与数据库操作的示例,主要功能是从一个数据库(从库)中查询部门信息,并将其同步到另一个数据库(主库)。以下是对代码的详细解析:
1. 方法 syncDepartments
java
@Transactional
public void syncDepartments() {
// 查询从库
List<Map> departments = getDepartmentsFromSlave();
// 写入主库
for (Map department : departments) {
departmentMapper.insertOrUpdate(department);
}
}
功能
- 事务管理 :
@Transactional
注解表示该方法运行在一个事务中。如果方法执行过程中发生异常,事务会回滚,确保数据的一致性。 - 流程 :
- 调用
getDepartmentsFromSlave()
方法从从库中查询部门信息。 - 遍历查询到的部门信息列表,调用
departmentMapper.insertOrUpdate(department)
方法将每个部门信息插入或更新到主库中。
- 调用
关键点
- 事务传播行为 :默认情况下,
@Transactional
的事务传播行为是Propagation.REQUIRED
,即如果当前存在事务,则加入该事务;如果不存在事务,则创建一个新事务。 - 异常处理 :如果在
insertOrUpdate
方法中抛出异常,整个事务会回滚,确保主库中的数据不会出现部分更新的情况。 - 性能问题 :每次循环调用
insertOrUpdate
方法可能会导致多次数据库操作,影响性能。可以考虑批量操作(如批量插入或更新)来优化性能。
2. 方法 getDepartmentsFromSlave
java
@Transactional(readOnly = true)
public List<Map> getDepartmentsFromSlave() {
return oldUserDataMapper.selectListByParam();
}
功能
- 只读事务 :
@Transactional(readOnly = true)
注解表示该方法运行在一个只读事务中。这可以提示数据库优化查询操作,提高性能。 - 查询操作 :调用
oldUserDataMapper.selectListByParam()
方法从从库中查询部门信息。selectListByParam
方法的具体实现未给出,但可以推测它是一个基于某些参数的查询操作。
关键点
- 只读事务优化:在只读事务中,数据库可以进行一些优化,例如减少锁的使用,从而提高查询性能。
- 数据源 :从代码命名来看,
oldUserDataMapper
可能是一个与从库相关的数据访问对象(Mapper)。这表明该方法从从库中读取数据,而主库用于写入操作。
方法 4:优化动态数据源实现
如果使用的是自定义动态数据源,可以优化路由逻辑,确保在只读事务中正确切换到从库。示例代码如下:
java
if (TransactionSynchronizationManager.isCurrentTransactionReadOnly()) {
// 如果事务是只读的,路由到从库
return DataSourceType.SLAVE;
} else {
// 非只读事务,路由到主库
return DataSourceType.MASTER;
}
关键组件解析:
-
TransactionSynchronizationManager
- 作用:Spring 框架提供的工具类,用于管理当前线程的事务状态(如资源绑定、事务属性)。
isCurrentTransactionReadOnly()
:检查当前事务是否标记为只读(由@Transactional(readOnly = true)
触发)。 数据源类型
-
DataSourceType.MASTER
:主库(写操作、强一致性场景)。 -
DataSourceType.SLAVE
:从库(读操作、高并发查询场景)。

5. 总结
加了事务后查询强制走主库,表面上是动态数据源配置失效,实际上是 Spring 事务机制和数据源路由规则的交互导致的。事务默认以数据一致性为优先考虑,因此强制绑定主库连接,从而覆盖了动态数据源的配置。
解决方案:
- 如果查询只涉及读取,可以显式声明只读事务。
- 移除事务注解,让动态数据源完全控制路由。
- 分离读写逻辑,分别处理查询和写入操作。
- 优化动态数据源的实现,确保事务和路由逻辑互不冲突。
通过这些方法,我们最终解决了小C的问题,也进一步加深了对 Spring 事务机制和动态数据源的理解。在系统优化中,读写分离虽强大,但背后隐藏的规则更需要我们去深入挖掘和掌握。
📣 关于我
我是bug菌,CSDN | 掘金 | InfoQ | 51CTO | 华为云 | 阿里云 | 腾讯云 等社区博客专家,C站博客之星Top30,华为云多年度十佳博主&最具价值贡献奖,掘金多年度人气作者Top40,掘金等各大社区平台签约作者,51CTO年度博主Top12,掘金/InfoQ/51CTO等社区优质创作者;全网粉丝合计 30w+ ;硬核微信公众号「猿圈奇妙屋」,欢迎你的加入!免费白嫖最新BAT互联网公司面试真题、4000G PDF电子书籍、简历模板等海量资料,你想要的我都有,关键是你不来拿。
-End-