UPDATE在RR下读到新提交数据不是bug而是设计使然,因其属于当前读而非快照读,会加锁并读取最新已提交版本进行条件判断和更新。

UPDATE 在 REPEATABLE READ 下读到新提交的数据,不是 bug,是设计如此 —— 它根本就不是快照读,而是当前读。
UPDATE 不走 MVCC 快照,而是加锁后读最新版本
很多人误以为 RR 隔离级别下所有语句都基于事务启动时的快照,但 UPDATE、DELETE、SELECT FOR UPDATE 这些写或加锁操作,全部属于「当前读」。它们会:
- 先根据 WHERE 条件定位行(依赖索引,否则全表扫描)
- 对匹配的行加 X 锁(记录锁或间隙锁)
- 然后直接读取该行**最新已提交版本**做条件判断(比如
balance >= 100) - 最后执行更新
这意味着:即使你在事务 A 中第一次 SELECT balance 看到的是 500,之后事务 B 提交了 +100,当你在事务 A 中执行 UPDATE ... WHERE user_id = ? AND balance >= 100 时,它读到的 balance 是 600,而不是快照里的 500 —— 所以条件成立,扣减成功。这就是为什么并发下可能扣成负数:两个 UPDATE 同时读到“余额足够”,但实际只有一笔该成功。
没索引时 UPDATE 会锁全表(等效)
如果 WHERE 条件没走索引(比如缺少 user_id 上的索引),InnoDB 就无法精准定位目标行:
- 只能全表扫描
- 对**每一行**都尝试加记录锁(X lock)
- 效果接近锁住整个聚簇索引,其他事务任何涉及该表的 UPDATE/DELETE 都会被阻塞
这不是语法错误,但性能和并发性灾难。验证方式:执行 EXPLAIN UPDATE ...,看 type 是否为 ALL 或 index;再查 SHOW ENGINE INNODB STATUS 的 TRANSACTIONS 部分,观察锁数量是否异常多。
安装、修复、更新或打包一个基于 Python 和 PyMySQL 实现的 Windows `mysql` 命令。适用于用户希望在 PowerShell/CMD 中输入 `mysql` 时重定向到 Python 实现、需要交互式输入 MySQL 连接信息、需要同步到当前用户本地 bin 目录,或需要将该能力封装成可公开复用的 Codex skill。
autocommit=1 时,每条 UPDATE 都是独立事务
默认 autocommit = 1 下,单条 UPDATE 自动开启并立即提交一个事务。它不和前后语句构成逻辑事务单元:
- 你无法在同个事务里先
SELECT再UPDATE做原子判断 - 想保证“读-判-改”不被干扰,必须显式
BEGIN/START TRANSACTION - 且推荐搭配
SELECT ... FOR UPDATE,把读和锁合并一步,避免条件竞态
例如积分扣减,正确写法不是裸 UPDATE ... WHERE balance >= 100,而是:
START TRANSACTION; SELECT balance FROM user_points WHERE user_id = 123 FOR UPDATE; -- 应用层判断 balance >= 100 UPDATE user_points SET balance = balance - 100 WHERE user_id = 123; COMMIT;
真正容易被忽略的点在于:RR 隔离级别只保障「一致性读」,不保障「一致性写」;写操作永远基于最新状态做决策,而这个最新状态本身,就是并发冲突的源头。










