rr隔离级别下count()“不一致”源于快照读时机偏差:事务首次select时创建read view,后续count()基于此快照,但显式加锁、唯一索引等值查询或全表扫描中部分新提交记录可能突破快照边界。

RR隔离级别下COUNT(*)为什么可能“不一致”
不是COUNT(*)本身出错,而是你看到的“不一致”往往源于对快照读(snapshot read)时机的理解偏差。在可重复读(RR)下,事务启动时会创建一个一致性视图(read view),后续所有普通SELECT(包括COUNT(*))都基于这个快照——但前提是它走的是快照读路径。一旦语句触发了加锁读(比如被FOR UPDATE或LOCK IN SHARE MODE修饰),或者执行计划用了索引覆盖但实际扫描了已提交的新版本记录(如某些二级索引+主键回表场景),就可能突破快照边界。
如何确保COUNT(*)严格走快照读
核心是避免任何隐式升格为当前读。RR下默认SELECT就是快照读,但以下情况会失效:
- 语句里显式写了
FOR UPDATE或LOCK IN SHARE MODE→ 立即变成当前读,看到最新已提交数据 - 查询条件命中唯一索引且等值匹配(如
WHERE id = ?),InnoDB 可能优化为“直接定位+当前读”,绕过快照 - 使用
SELECT COUNT(*) FROM t WHERE ...且WHERE条件无法利用索引,导致全表扫描时部分记录被其他事务更新并提交,而你的事务快照又恰好在那些更新之后启动(注意:RR 的快照是事务**第一次执行 SELECT 时**创建的,不是BEGIN时)
实操建议:
- 确认事务内第一次
SELECT发生在你关心的COUNT(*)之前,否则快照创建时间点不对 - 用
EXPLAIN检查COUNT(*)是否走了全表扫描;如果表很大,优先建好覆盖统计所需的索引(如KEY idx_status (status)配合COUNT(*) WHERE status=1) - 避免在同一个事务里混用快照读和当前读;如果必须查最新数,显式用
SELECT COUNT(*) FROM t FOR UPDATE,别依赖“应该一样”的假设
SELECT COUNT(*)和information_schema.TABLES的区别
有人试图用SELECT TABLE_ROWS FROM information_schema.TABLES WHERE TABLE_NAME='t'替代,这是危险的:
-
TABLE_ROWS是估算值(MyISAM 是精确值,InnoDB 永远不准),来自采样统计,重启或ANALYZE TABLE后才可能更新 - 它完全不遵循事务隔离级别,也不反映当前快照,纯粹是元数据缓存
- 并发写入频繁时,误差可能达 20% 以上,不能用于一致性校验
真要快速估算,可用SHOW TABLE STATUS LIKE 't'里的Rows字段,但记住它只是提示,不是答案。
真正需要强一致计数时的替代方案
如果业务逻辑要求“某时刻精确的已提交行数”,且不能容忍快照读的延迟可见性(比如财务对账),RR本身无法满足——因为InnoDB的MVCC设计决定了快照读天然滞后于最新提交。这时得换思路:
- 用
SELECT COUNT(*) FROM t LOCK IN SHARE MODE:强制当前读,但会阻塞其他事务的写,吞吐下降明显 - 维护冗余计数字段:在业务写入时用
UPDATE counter_table SET cnt = cnt + 1 WHERE key = 't_total',配合行锁保证原子性(注意初始化和异常回滚) - 改用
READ COMMITTED隔离级别 + 显式START TRANSACTION WITH CONSISTENT SNAPSHOT(MySQL 8.0+),但RC下快照只对首次查询有效,后续查询会看到新提交,仍需控制执行顺序
最常被忽略的一点:RR的“一致性”是指**同一事务内多次读结果一致**,不是指“和系统当前状态一致”。想拿实时数,就得主动放弃快照,接受锁或估算代价。











