长事务不直接导致幻读但会放大其风险,关键在规范事务开启时机、执行边界与提交节奏;应延迟BEGIN、避免事务内阻塞操作、及时COMMIT、合理使用锁和唯一约束。

长事务本身不会“造成”幻读,但会显著放大幻读风险和后果;真正需要规范的是事务开启时机、执行边界与提交节奏——否则即使隔离级别设为 REPEATABLE READ,也会因锁持有时间过长、快照过旧或间隙锁范围失控,让幻读在应用逻辑中暴露出来。
事务开启太早,快照就“ stale ”了
很多代码在业务入口就 BEGIN,然后做参数校验、远程调用、缓存查询等非数据库操作,等真正要查数据时,事务已运行几十秒。此时 READ VIEW 是事务启动那一刻的,后续所有快照读都基于这个旧快照——如果其他事务在这期间插入并提交了新行,而你的逻辑又依赖“查不到就插入”,就极易触发幻读(比如 SELECT ... FOR UPDATE 没覆盖到新插入行,或根本没加锁)。
- 把
BEGIN推迟到真正需要一致性读/写之前,比如校验通过、缓存未命中、外部服务返回后 - 避免在事务内做
HTTP请求、sleep()、文件读写等阻塞操作 - 对只读报表类逻辑,直接用自动提交模式(
autocommit=1),不显式开事务
COMMIT 不及时,间隙锁就一直锁着“空气”
REPEATABLE READ 下的间隙锁(Gap Lock)不是按语句释放,而是按事务释放。一个长事务哪怕只执行了一次 SELECT * FROM orders WHERE status = 'PENDING' FOR UPDATE,它锁住的索引间隙会持续存在,阻止所有符合该条件的插入——哪怕你后续再也没查过这表。其他事务插入新订单就会被卡住,进而拖慢主从同步、抬高 innodb_row_lock_time_avg。
- 事务内只保留必要的一组读+写,写完立刻
COMMIT,不要“攒着一起交” - 批量更新必须分页:用
WHERE id BETWEEN ? AND ?+LIMIT 1000,每批单独事务 - 监控
INFORMATION_SCHEMA.INNODB_TRX,重点查trx_started超过 30 秒的事务
用错隔离级别,MVCC 快照反而成陷阱
默认 REPEATABLE READ 确实靠 MVCC 避免普通 SELECT 的幻读,但它不解决“读-判-插”这类应用级逻辑漏洞。例如:先 SELECT COUNT(*) 判定无记录,再 INSERT,中间别人插入并提交,你的 INSERT 就可能失败(唯一键冲突)或重复(无约束场景)。这时降级到 READ COMMITTED 并不能缓解——它让快照更“新鲜”,反而更容易看到别人刚插入的行,幻读概率更高。
- 这种场景必须用
SELECT ... FOR UPDATE加锁,且 WHERE 条件必须走索引,否则退化为全表间隙锁 - 更稳妥的做法是:用唯一索引约束 +
INSERT ... ON DUPLICATE KEY UPDATE,把判断和插入合并为原子操作 - 不要依赖隔离级别“自动防幻读”,要结合业务逻辑主动控制读写边界
最容易被忽略的点是:事务是否真的结束了。连接池回收连接时不会帮你 ROLLBACK,异常分支没写 finally 块或没用上下文管理器,autocommit=0 下的连接就带着未提交事务沉睡下去——它不报错,但锁、undo log、binlog 延迟全在后台默默累积。











