🤯加了事务,数据源反倒不听话了?揭秘查询强制走主库的前因后果!

🏆本文收录于「滚雪球学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 路由到从库。
    • 写操作(如 INSERTUPDATE)路由到主库。
  • 事务环境
    • 如果是读写事务(默认模式),所有操作强制路由到主库。
    • 如果是只读事务(readOnly = true),允许路由到从库。

但在事务环境下,动态数据源的路由规则会被事务管理器覆盖。事务管理器优先决定数据库连接的绑定,从而影响数据源的选择。

3. 为什么查询会强制走主库?

结合上述分析,我们可以总结出以下几点原因:

  1. 读写事务强制绑定主库

    • @Transactional 默认开启读写事务,Spring 为了保证数据一致性,会直接将所有操作路由到主库。
    • 即使查询方法被标注为从库数据源,动态数据源的配置也会被事务管理器覆盖。
  2. 事务内连接无法切换数据源

    • 事务中的所有操作共享一个数据库连接。如果事务开始时绑定了主库连接,后续查询也只能使用这个连接,无法切换到从库。
  3. 事务的优先级高于动态数据源

    • 在事务环境下,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会通知底层数据库驱动或连接池,当前操作不会修改数据。这可以带来以下好处:
    1. 性能优化:数据库可以利用只读事务的特性,减少锁的使用,提高查询性能。
    2. 资源管理:连接池可以优化只读事务的连接管理,例如使用专门的只读连接池,进一步提高性能。

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集合,集合中的每个元素都是一个MapMap通常用于存储键值对,例如数据库查询结果中的一行数据可以存储为一个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 注解表示该方法运行在一个事务中。如果方法执行过程中发生异常,事务会回滚,确保数据的一致性。
  • 流程
    1. 调用 getDepartmentsFromSlave() 方法从从库中查询部门信息。
    2. 遍历查询到的部门信息列表,调用 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;
}

关键组件解析‌:

  1. TransactionSynchronizationManager‌

    • 作用‌:Spring 框架提供的工具类,用于管理当前线程的事务状态(如资源绑定、事务属性)。
    • isCurrentTransactionReadOnly()‌:检查当前事务是否标记为只读(由 @Transactional(readOnly = true) 触发)。 ‌数据源类型‌
  2. DataSourceType.MASTER:主库(写操作、强一致性场景)。

  3. DataSourceType.SLAVE:从库(读操作、高并发查询场景)。

5. 总结

加了事务后查询强制走主库,表面上是动态数据源配置失效,实际上是 Spring 事务机制和数据源路由规则的交互导致的。事务默认以数据一致性为优先考虑,因此强制绑定主库连接,从而覆盖了动态数据源的配置。

解决方案

  1. 如果查询只涉及读取,可以显式声明只读事务。
  2. 移除事务注解,让动态数据源完全控制路由。
  3. 分离读写逻辑,分别处理查询和写入操作。
  4. 优化动态数据源的实现,确保事务和路由逻辑互不冲突。

通过这些方法,我们最终解决了小C的问题,也进一步加深了对 Spring 事务机制和动态数据源的理解。在系统优化中,读写分离虽强大,但背后隐藏的规则更需要我们去深入挖掘和掌握。

📣 关于我

我是bug菌,CSDN | 掘金 | InfoQ | 51CTO | 华为云 | 阿里云 | 腾讯云 等社区博客专家,C站博客之星Top30,华为云多年度十佳博主&最具价值贡献奖,掘金多年度人气作者Top40,掘金等各大社区平台签约作者,51CTO年度博主Top12,掘金/InfoQ/51CTO等社区优质创作者;全网粉丝合计 30w+ ;硬核微信公众号「猿圈奇妙屋」,欢迎你的加入!免费白嫖最新BAT互联网公司面试真题、4000G PDF电子书籍、简历模板等海量资料,你想要的我都有,关键是你不来拿。

-End-

相关推荐
老A技术联盟10 分钟前
聊一聊消息中间件的后起之秀-pulsar及其实践
后端
隐-梵19 分钟前
Android studio前沿开发--利用socket服务器连接AI实现前后端交互(全站首发思路)
android·服务器·人工智能·后端·websocket·android studio·交互
uhakadotcom22 分钟前
Langflow:零基础快速上手AI流程可视化开发工具详解与实战案例
后端·面试·github
bobz96522 分钟前
strongswan ipsec 端口使用
后端
陈哥聊测试25 分钟前
这款自研底层框架,你说不定已经用上了
前端·后端·开源
一只叫煤球的猫39 分钟前
分布式-跨服务事务一致性的常见解决方案
java·分布式·后端
扣丁梦想家43 分钟前
Spring Boot 实现 Excel 导出功能(支持前端下载 + 文件流)
spring boot·后端·excel
赤橙红的黄44 分钟前
Spring编程式事务(本地事务)
java·数据库·spring
调试人生的显微镜1 小时前
flutter ios 自定义ios插件
后端
Java程序之猿1 小时前
Spring Boot 集成spring-boot-starter-data-elasticsearch
spring boot·elasticsearch·jenkins