bigywater.tech
Spring · 98

读写分离:AbstractRoutingDataSource 与事务内路由

读写分离用 AbstractRoutingDataSource 为什么经常不生效?readOnly 事务里写会怎样?内层 REQUIRES_NEW 走哪个库?这篇用同一个 MySQL 实例里的两个库代替主从,逐个实测。

走了主库
直接用 AbstractRoutingDataSource,@Transactional(readOnly = true) 查询仍然落在主库
replica
外面包一层 LazyConnectionDataSourceProxy 之后,readOnly 事务才路由到从库
Connection is read-only
readOnly 事务里 REQUIRED 的写方法被驱动拦住

不生效的原因

实测 · 本机执行输出
DataSource 的类型:demo.Cfg$1(AbstractRoutingDataSource 的匿名子类)
@Transactional(readOnly = true) 的 readTx()  ⇒ demo;determineCurrentLookupKey 被调用了 1 次,依次返回 [primary]
@Transactional 的 writeTx()  ⇒ demo;determineCurrentLookupKey 被调用了 1 次,依次返回 [primary]
没有 @Transactional 的 noTx()  ⇒ demo;determineCurrentLookupKey 被调用了 1 次,依次返回 [primary]
外层 readOnly 调 REQUIRED 的写方法  ⇒ 抛 TransientDataAccessResourceException:Connection is read-only. Queries leading to data modification are not allowed;determineCurrentLookupKey 被调用了 1 次,依次返回 [primary]
外层 readOnly 调 REQUIRES_NEW 的写方法  ⇒ 外层 readOnly 事务里 → 先读在 demo,再调 REQUIRES_NEW 的写方法,这次写在 demo;determineCurrentLookupKey 被调用了 2 次,依次返回 [primary, primary]
    库 demo 的 log 表:(('writeTx',), ('from-readonly-outer-REQUIRES_NEW',))
    库 replica 的 log 表:()

路由在取连接时调用,而 readOnly 标记还没就绪,于是读到的是 false。

修复

实测 · 本机执行输出
DataSource 的类型:org.springframework.jdbc.datasource.LazyConnectionDataSourceProxy
@Transactional(readOnly = true) 的 readTx()  ⇒ replica;determineCurrentLookupKey 被调用了 2 次,依次返回 [primary, replica]
@Transactional 的 writeTx()  ⇒ demo;determineCurrentLookupKey 被调用了 1 次,依次返回 [primary]
没有 @Transactional 的 noTx()  ⇒ demo;determineCurrentLookupKey 被调用了 1 次,依次返回 [primary]
外层 readOnly 调 REQUIRED 的写方法  ⇒ 抛 TransientDataAccessResourceException:Connection is read-only. Queries leading to data modification are not allowed;determineCurrentLookupKey 被调用了 1 次,依次返回 [replica]
外层 readOnly 调 REQUIRES_NEW 的写方法  ⇒ 外层 readOnly 事务里 → 先读在 replica,再调 REQUIRES_NEW 的写方法,这次写在 demo;determineCurrentLookupKey 被调用了 2 次,依次返回 [replica, primary]
    库 demo 的 log 表:(('writeTx',), ('from-readonly-outer-REQUIRES_NEW',))
    库 replica 的 log 表:()

外面包 LazyConnectionDataSourceProxy,推迟取连接;之后 readOnly 事务走从库,REQUIRES_NEW 的写方法走主库。

分步演示

读写分离
本步的对象

    本文用到的工具与边界

    • 本机 macOS 27 arm64 / OpenJDK 21.0.12(Homebrew) / Spring Boot 4.0.8(Spring Framework 7.0.9、Tomcat 11.0.24) / Spring Cloud 2025.1.3;用 javac 编译、java 直接运行,依赖由 Maven 从 Maven Central 解析;数字每次运行会有小幅抖动(Spring Framework 7.0.9 的 AbstractRoutingDataSource 与 LazyConnectionDataSourceProxy;MySQL 是本机自己起的 26.7.0 实例,mysql-connector-j)。用同一个实例里的两个库 demo 与 replica 代替真正的主从,两个库之间没有复制;路由键取自 TransactionSynchronizationManager.isCurrentTransactionReadOnly();用 JdbcTemplate 而不是 JPA
    • 没有验证:真正的主从复制与复制延迟导致的读不到刚写入的数据;JPA / Hibernate 下 readOnly 事务的路由(HibernateJpaDialect 有自己设置连接 readOnly 的时机);用注解或 ThreadLocal 手动指定数据源的做法;从库故障时的降级;连接池按库分别配置的大小;分库分表中间件(如 ShardingSphere)的做法
    京ICP备2026067502号-1京公网安备11010502063487号