登录
推荐 文章 Go 技术 课程 下载 专题 AI
首页 >  数据库 >  MySQL

MySQL 事务隔离解释一致性读与当前读差异的实现方法

来源:17golang原创

时间:2026-09-19 23:00:48 306浏览 收藏

我排查过一类很容易误判的库存问题:事务里的普通 SELECT 明明看不到刚提交的数据,紧接着 UPDATE 却能命中它。根因通常不是 MySQL 随机返回结果,而是两条语句使用了不同的读方式。InnoDB 的普通查询是一致性读,依赖 read view;UPDATEDELETE 和带 FOR SHAREFOR UPDATE 的查询属于当前读或锁定读,要面对最新可用版本。

只想稳定地读一份事务快照,用普通 SELECT;读完马上要改,或必须判断最新库存,用 FOR UPDATE;只保护关联行不被修改,用 FOR SHARE。不要在 REPEATABLE READ 事务里把快照读和当前读的结果当成同一张表。
要点速览
  • InnoDB 默认隔离级别是 REPEATABLE READ,同一事务的普通 SELECT 通常复用首次一致性读的快照。
  • READ COMMITTED 会让每次一致性读获得新快照,但不会把普通 SELECT 自动变成加锁读取。
  • FOR UPDATE 面向最新可用行并加排他锁,锁在提交或回滚后释放,适合读取后修改。

普通 SELECT 为什么会停留在旧快照

一致性读的目标是让查询看到一个稳定的数据库时间点。REPEATABLE READ 下,事务第一次执行普通 SELECT 时建立 read view,之后同一事务的普通查询继续按这个视图判断哪些版本可见。因此,另一个事务后来提交的插入、更新或删除,不会自动改变这份快照。

这个规则只约束一致性读,不代表事务中的所有语句都使用同一视图。尤其是写语句需要定位可修改的真实行,不能拿过期版本直接加锁。排查时先记下每条语句的类型、首次普通 SELECT 的位置和两个事务的 COMMIT 顺序。

MySQL InnoDB REPEATABLE READ 中事务、read view 与可见行版本的关系说明图
图1:InnoDB 一致性读的 read view 关系说明图,不是运行截图。

隔离级别怎样改变一致性读的时间点

下面的示例只用于构造两个会话的观察场景。注释保留在 SQL 内,方便把事务边界和检查动作对应起来:

-- 会话 A:先建立普通 SELECT 的观察时间点
SET SESSION TRANSACTION ISOLATION LEVEL REPEATABLE READ;
START TRANSACTION;
SELECT id, stock FROM inventory WHERE id = 7; -- 首次一致性读建立 read view

-- 会话 B:另一个事务提交库存变化
START TRANSACTION;
UPDATE inventory SET stock = stock - 1 WHERE id = 7; -- 写入新版本并持有行锁
COMMIT; -- 提交后,新版本对后续合适的读可见

-- 会话 A:普通 SELECT 仍按原快照读取
SELECT id, stock FROM inventory WHERE id = 7; -- REPEATABLE READ 下可能仍是旧值
COMMIT; -- 结束事务,下一次事务才会建立新的观察点

-- 需要每次普通查询都刷新视图时,在新事务开始前选择 READ COMMITTED
SET SESSION TRANSACTION ISOLATION LEVEL READ COMMITTED;

REPEATABLE READ 适合报表式的事务内稳定读取;READ COMMITTED 则让每次一致性读使用新快照,适合更关注最新已提交状态的场景。改隔离级别前先确认应用的 binlog、锁等待和可重复读取要求,不能只为“看到新值”就全局调整。

需要最新值时怎样切换到当前读

如果业务动作是“读取库存后扣减”“检查父记录后插入子记录”或“读取计数器后递增”,普通 SELECT 与后续写入之间存在竞态。此时应该在同一事务内使用锁定读:

-- 读取库存并锁住当前行,随后在同一事务中完成扣减
START TRANSACTION;
SELECT stock FROM inventory WHERE id = 7 FOR UPDATE; -- 当前读:等待并读取最新可用版本
UPDATE inventory SET stock = stock - 1 WHERE id = 7; -- 依据已锁定的行执行修改
COMMIT; -- 提交修改并释放 FOR UPDATE 持有的锁

-- 只需保护关联记录时使用共享锁,允许其他事务继续读取
START TRANSACTION;
SELECT id FROM parent WHERE id = 9 FOR SHARE; -- 防止父记录在本事务完成前被修改或删除
INSERT INTO child(parent_id, value) VALUES (9, 'demo'); -- 继续写入关联数据
COMMIT; -- 提交后释放共享锁

FOR UPDATE 是排他锁语义,FOR SHARE 是共享锁语义;它们可能等待其他事务结束。锁定读必须放在显式事务中,且锁的范围仍受索引条件、范围扫描和隔离级别影响,不能把一条 SQL 简化成“永远只锁一行”。

MySQL 普通 SELECT、FOR SHARE、FOR UPDATE 与 UPDATE 的读版本和锁关系说明图
图2:一致性读、当前读与锁定读的选择关系说明图,不是运行截图。

三类现象的排查清单

现象先检查处理方向
SELECT 看不到,UPDATE 却命中是否在 REPEATABLE READ 的旧 read view 中区分展示查询与写前校验,必要时使用锁定读
同一事务的普通查询不刷新首次 SELECT、隔离级别和 COMMIT 边界提交后重开事务,或评估 READ COMMITTED
FOR UPDATE 长时间等待阻塞事务、索引条件和未提交事务缩短事务,补合适索引,按业务决定 NOWAIT/SKIP LOCKED

我的经验是先把语句分成“看快照”“看最新值”“读取并准备修改”三组,再看事务边界;直接把所有查询改成 FOR UPDATE,往往只会把读请求变成锁竞争。

相关问题

普通 SELECT 会不会加行锁?

在 InnoDB 的一致性读模式下,普通 SELECT 不为访问的表设置锁,其他事务可以同时修改。它的保护重点是可见性,不是阻止并发写入。

FOR SHARE 和 FOR UPDATE 怎么选?

后续只需要保证关联行在事务完成前不被修改,可用 FOR SHARE;读取后要更新行或递增计数器,应使用 FOR UPDATE。

READ COMMITTED 能解决所有旧数据问题吗?

不能。它只改变一致性读建立快照的频率;需要读取后安全修改时,仍应使用合适的锁定读并缩短事务范围。

声明:本文转载于:17golang原创 如有侵犯,请联系study_golang@163.com删除
相关阅读
更多>
最新阅读
更多>
课程推荐
更多>