什么是不可重复读,它在 MySQL 中具体表现为什么
不可重复读是指:同一个事务中,多次执行相同的
SELECT语句,却读到了其他事务已提交的修改结果,导致前后两次查询结果不一致。这不是幻读(幻读是读到了新插入的行),而是对**已有行的更新被感知到了**。
典型场景:
SELECT→ 其他事务
UPDATE ... COMMIT→ 再次
SELECT,发现同一行数据变了。这在
READ COMMITTED隔离级别下合法且常见。
MySQL 默认隔离级别能否避免不可重复读
MySQL 默认隔离级别是
REPEATABLE READ,它通过 MVCC + Next-Key Lock(在可重复读下对范围查询加锁)来防止不可重复读——但仅限于**快照读(普通
SELECT)**。
注意以下关键点:
REPEATABLE READ下,普通
SELECT基于事务开启时的 Read View,不会看到其他事务后续的提交,因此能保证不可重复读不发生 但
SELECT ... FOR UPDATE或
SELECT ... LOCK IN SHARE MODE是当前读,会绕过 MVCC 快照,直接读最新已提交版本,并加锁,此时可能“感知”到其他事务的更新(取决于加锁时机和范围) 如果业务混用快照读与当前读,或依赖
SELECT结果做后续判断再
UPDATE(即“读-改-写”逻辑),仍可能因条件竞态导致逻辑错误,这不是隔离级别失效,而是应用层未正确使用锁或未用
SELECT ... FOR UPDATE
真正解决并发下的不可重复读问题,该怎么做
靠隔离级别本身只能缓解,不能根治所有业务场景。必须结合具体操作方式:
对需要“读取后立即修改”的逻辑,务必用SELECT ... FOR UPDATE(在主键/索引列上)发起当前读并加行锁,确保从读到写之间该行不被其他事务修改 避免在
REPEATABLE READ下先普通
SELECT拿值,再拼
UPDATE条件——这存在窗口期,应改用原子语句如
UPDATE ... WHERE id = ? AND status = 'pending'若涉及多行一致性(如转账需查两账户余额),单靠行锁不够,需用
SELECT ... FOR UPDATE锁住所有相关行,且按固定顺序(如 ID 升序)加锁,防死锁 高并发下过度依赖
SELECT ... FOR UPDATE可能导致锁等待甚至超时,此时应评估是否可用乐观锁(如
version字段 +
UPDATE ... SET version = version + 1 WHERE id = ? AND version = ?)替代
不同隔离级别对不可重复读的实际影响对比
MySQL 支持的四种隔离级别对不可重复读的控制能力如下:
READ UNCOMMITTED:能看到未提交变更,必然出现不可重复读(以及脏读)
READ COMMITTED:每次
SELECT都新建 Read View,能看到其他事务已提交的更新 → 会出现不可重复读
REPEATABLE READ(MySQL 默认):事务启动时创建 Read View,后续快照读都复用 → 普通
SELECT不会出现不可重复读
SERIALIZABLE:所有
SELECT隐式转为
SELECT ... LOCK IN SHARE MODE,强制串行化 → 理论上杜绝,但性能代价极大,极少使用
真正容易被忽略的是:即使在
REPEATABLE READ下,只要用了
UPDATE、
DELETE或当前读,就不再受快照保护;而业务代码里往往没意识到自己触发了当前读。
