只读事务仍会锁表、占内存,因其默认开启mvcc读视图、分配undo segment、持有行锁或间隙锁;若未及时commit,将导致锁不释放、undo无法purge、历史版本堆积,进而阻塞写入、拖慢查询并放大连接池泄漏风险。

只读事务为什么还会锁表、占内存?
MySQL 8.0 的只读事务(START TRANSACTION READ ONLY)虽不写数据,但默认仍会开启 MVCC 读视图、分配 undo segment、持有行锁(尤其在 SELECT ... FOR SHARE 或带索引扫描时),若未及时 COMMIT,它就和普通长事务一样持续占用资源:锁不释放、undo 日志无法 purge、历史版本堆积拖慢查询。
常见错误现象包括:SHOW PROCESSLIST 中状态为 Sleep 但 Command 是 Sleep、State 为空或 Waiting for table flush;performance_schema.data_locks 显示大量 LOCK_MODE = S 锁;INNODB_TRX 中 trx_state = RUNNING 且 trx_started 超过几分钟。
- 别只看
trx_operation_state是否为空——只读事务的锁可能在第一条SELECT后就已加,后续空闲不影响锁持有 -
READ ONLY事务仍会参与死锁检测,若与其他事务交叉加锁,照样被回滚(日志里会出现LATEST DETECTED DEADLOCK) - 云厂商 RDS 的“只读实例”开关常静默设置
super_read_only=ON,此时所有连接都强制只读,但事务仍可启动并持锁
怎么查出没提交的只读事务?
靠 information_schema.INNODB_TRX 不够准,它不区分 READ ONLY 和 READ WRITE;必须结合 performance_schema.events_transactions_current 查事务属性。
执行这条语句定位活跃只读事务:
SELECT t.thread_id, CONCAT(p.PROCESSLIST_USER, '@', p.PROCESSLIST_HOST) AS user_host, t.TIMER_WAIT / 1000000000 AS duration_sec, t.CURRENT_STATEMENT AS stmt, t.TRANSACTION_ISOLATION_LEVEL, t.TRANSACTION_READ_ONLY FROM performance_schema.events_transactions_current t JOIN performance_schema.threads th USING (thread_id) LEFT JOIN sys.processlist p ON p.thd_id = th.thread_id WHERE t.STATE = 'ACTIVE' AND t.TRANSACTION_READ_ONLY = 'YES' AND t.TIMER_WAIT > 5000000000000 -- 超过 5 秒 ORDER BY t.TIMER_WAIT DESC;
- 注意
TRANSACTION_READ_ONLY字段才是真实判断依据,read_only系统变量只是全局开关,对单个事务无约束 - 如果返回结果中
stmt为空,说明事务已启动但尚未执行任何语句——典型是应用层开启事务后异常退出或忘记COMMIT - 搭配
performance_schema.data_locks过滤THREAD_ID,可确认它到底锁了哪些记录或间隙
只读事务卡住,是不是一定没危害?
不是。只读事务若长时间不 COMMIT,会导致三类实际危害:
-
undo log 滞后:InnoDB purge 线程无法清理该事务开始前的旧版本,
History list length持续增长(查SHOW ENGINE INNODB STATUS\G中TRANSACTIONS部分),最终拖慢所有查询 -
锁阻塞写入:哪怕只是
SELECT ... LOCK IN SHARE MODE,也会加 S 锁,阻止其他事务对该行加 X 锁;范围查询还可能触发间隙锁,阻塞 INSERT -
连接池泄漏放大效应:应用使用连接池复用连接,若某次只读事务未关闭,下次复用时可能残留
START TRANSACTION READ ONLY状态,导致新请求也变成只读且持锁
特别注意:innodb_rollback_on_timeout=OFF(默认)下,只读事务不会因超时自动回滚,它就一直挂着——这比写事务更隐蔽、更难被监控捕获。
如何避免只读事务变成“幽灵资源占用者”?
核心是让只读事务生命周期可控,而不是依赖应用自觉 COMMIT。
- 在连接池配置里启用
auto-commit=true,并禁止应用显式调用BEGIN;报表类查询直接用单条语句,不启事务 - 若必须用事务,强制要求
START TRANSACTION READ ONLY+COMMIT成对出现,中间不能有网络中断或逻辑分支跳过COMMIT - 数据库侧设超时兜底:
SET SESSION lock_wait_timeout = 30(防锁等待)、SET SESSION wait_timeout = 60(空闲连接断开),但注意wait_timeout对已启事务无效 - 定期巡检脚本应查
performance_schema.events_transactions_current中TRANSACTION_READ_ONLY = 'YES'且运行超 30 秒的事务,自动告警或 KILL(需评估业务影响)
最易被忽略的一点:MySQL 8.0 的 READ ONLY 事务在 REPEATABLE READ 隔离级别下仍会固定读视图,即使只查一次,这个视图也会持续到 COMMIT —— 所以“只读一次就不管了”恰恰是最危险的用法。











