事务内读操作必须走主库,因读写分离中间件不感知事务上下文,select...for update等需加锁语句及强一致性读依赖gtid+row格式复制,应用层须控制连接路由与事务边界。

事务内读操作必须走主库,read_only 不能绕过
MySQL 读写分离下,事务中的 SELECT 默认可能被路由到从库,导致读到旧数据或事务间不一致。这不是中间件或代理的 bug,而是多数读写分离方案(如 ProxySQL、ShardingSphere、甚至应用层 DataSource 路由)的默认行为——它们只看 SQL 类型,不感知事务上下文。
真正起作用的是 MySQL 自身的 innodb_read_only 和会话级 read_only 配置,但注意:read_only=ON 在从库上是强制的,而在主库上设为 ON 会直接拒绝写入,所以它不能用来“标记事务该走哪”。关键在客户端控制:
- 开启事务前,显式执行
SET SESSION innodb_read_only = OFF(仅对当前连接有效,且仅当主库未设全局read_only=ON时才允许) - 更可靠的做法:所有事务一律使用主库连接池,不在事务中切换数据源
- 如果用 MyBatis + Druid,确保
@Transactional方法绑定的DataSource是主库,而非逻辑路由后的抽象源
SELECT ... FOR UPDATE 和 SELECT ... LOCK IN SHARE MODE 必须命中主库
这类语句本质是写操作前置,需要加行锁、更新事务视图(read view),若落到从库会报错或静默失败。常见错误现象:Lock wait timeout exceeded; try restarting transaction 或直接返回空结果,但无明确提示。
原因在于从库的 relay_log 回放有延迟,且不支持加写锁。即使你配置了 slave_parallel_type=LOGICAL_CLOCK,也不能改变这个限制。
- 检查中间件是否识别
FOR UPDATE并自动切主——不是所有代理都支持,ProxySQL 需要自定义mysql_query_rules规则匹配正则SELECT.*FOR UPDATE - Spring Boot 中,避免在
@Transactional(readOnly = true)方法里混用FOR UPDATE,这会导致事务实际以只读开启,后续加锁失败 - 若用
xa_transaction或跨库事务,FOR UPDATE必须全部落在同一节点,否则两阶段提交会卡在 prepare 阶段
强一致性读配置:transaction_isolation = REPEATABLE-READ 不够,得靠 binlog_format = ROW + GTID
很多人以为调高隔离级别就能解决一致性,其实不然。REPEATABLE-READ 只保证单库事务内可重复读,无法约束主从之间数据新鲜度。真正影响“强一致性读”的是复制机制本身。
如果你看到从库 SELECT 结果比主库慢几百毫秒,问题大概率出在复制格式和位点同步上:
-
binlog_format = STATEMENT在函数、NOW()、UUID()等场景下会导致主从不一致,且无法用于精确回放 - 必须启用
gtid_mode = ON,并让应用在读从库前通过SELECT MASTER_POS_WAIT(...)或SOURCE_UUID等待指定 GTID 执行完成(需从库开启enforce_gtid_consistency = ON) - 部分 SDK(如 MySQL Connector/J 8.0+)支持
sessionVariables=transactionReadOnly=false+allowMasterDownConnections=true组合,但不会自动等待 GTID,仍需业务层调用connection.getMetaData().getURL()判断节点角色
应用层最容易忽略的坑:连接复用与事务传播污染
一个 HTTP 请求内多次调用 DAO 方法,如果共用同一个数据库连接(比如 Druid 连接池未配置 removeAbandonedOnBorrow=true),而中间某个方法开了事务又没正确关闭,后续非事务查询可能意外继承了事务上下文,导致本该走从库的读被拖去主库,或反之。
更隐蔽的是 Spring 的 Propagation.REQUIRED 默认行为:外层事务未结束,内层 @Transactional(propagation = Propagation.SUPPORTS) 也会共享连接,进而共享路由目标。
- 检查 Druid 日志中是否频繁出现
recycle connection或abandoned,这是连接泄漏的信号 - 在事务方法入口加日志打印
connection.getMetaData().getURL(),确认实际连的是主还是从 - 禁止在事务方法内手动
dataSource.getConnection(),这会脱离 Spring 事务管理器的控制
SELECT 就可能读错地方。











