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

MySQL 一致性快照怎样影响长事务读

来源:17golang原创

时间:2026-10-17 22:18:29 214浏览 收藏

MySQL 一致性快照怎样影响长事务读

在 InnoDB 的 REPEATABLE READ(RR)隔离级别下,长事务中的普通一致性读通常沿用首次一致性读建立的快照,因此其他事务后来提交的新值,不会自动出现在后续普通 SELECT 中。普通 START TRANSACTION 不等于当场创建快照;若使用 WITH CONSISTENT SNAPSHOT,则可在 RR 下主动建立。READ COMMITTED(RC)则每次一致性读采用新的快照,不保证整段长报表使用同一视图。

下面用一个报表持续显示旧数量的假设案例还原原因。所有 SQL 均为原创教学示例,本文制作时未运行数据库,也没有实际事故时间、监控数据或性能结论。适用范围为 MySQL 8.4、InnoDB 和普通非锁定 SELECT。

官方文档:https://dev.mysql.com/doc/refman/8.4/en/innodb-consistent-read.html;事务启动语法:https://dev.mysql.com/doc/refman/8.4/en/commit.html。

长报表显示旧数量,影响的是新鲜度而非必然读错

设想报表事务 A 连续读取同一条数量记录,业务事务 B 已经提交修改,但报表仍显示原来的数。若 A 使用 RR 且已有一致性读快照,这可能正是隔离级别提供的稳定视图,不应立即认定为缓存失效或提交丢失。

排查前要记录同一连接上的事务开始点、首次普通读、B 的提交点,以及 A 是否已经结束。只记录请求进入应用的时间不够:应用可能较早打开事务,也可能由框架先发出了一次查询。反过来,事务先开始但还没进行一致性读时,稍后第一次查询可能看见事务开始之后、查询之前已提交的数据。

用两个会话还原首次读取与提交的先后

请只在独立测试库执行下面的准备语句;表名必须尚不存在,若已存在就换一个新的测试表名并同步修改全部示例,不覆盖现有数据。需要两个连接到同一实例、同一测试库的会话 A 与 B,且没有其他写入者。不要使用临时表,因为另一个会话不能共享它。

-- 在测试库准备普通 InnoDB 表,仅执行一次。
CREATE TABLE snapshot_read_demo (
    id INT PRIMARY KEY,
    qty INT NOT NULL
) ENGINE=InnoDB;
INSERT INTO snapshot_read_demo VALUES (1,100), (2,200), (3,300);
COMMIT;

第一轮使用 id=1。按下面注释逐段切换会话,不要整块复制到一个窗口运行。A 在首次查询前不执行任何其他 InnoDB 普通查询。

-- 会话 A:只设置下一个事务为 RR,然后开始;此时尚未普通读。
SET TRANSACTION ISOLATION LEVEL REPEATABLE READ;
START TRANSACTION;

-- 切到会话 B:提交第一次变化。
START TRANSACTION;
UPDATE snapshot_read_demo SET qty=110 WHERE id=1;
COMMIT;

-- 切回 A:首次一致性读,预期 qty 为 110。
SELECT qty FROM snapshot_read_demo WHERE id=1;

-- 切到 B:提交第二次变化。
START TRANSACTION;
UPDATE snapshot_read_demo SET qty=120 WHERE id=1;
COMMIT;

-- 切回 A:仍使用原快照,预期依旧为 110;随后结束事务。
SELECT qty FROM snapshot_read_demo WHERE id=1;
COMMIT;

这一轮有两个观察点:A 的第一次读取看见 110,说明普通事务开始时间不等于快照创建时间;第二次仍是 110,说明快照建立以后,B 再提交不会自动刷新 A 的普通一致性读。所有数值都是按给定顺序推导的预期,不是运行输出。

显式快照生效,还需要隔离级别这个前提

第二轮使用尚未修改的 id=2,将 A 的启动语句改为显式快照。RR 下,它的作用相当于开始事务后立即建立一致性读视图,所以不能再把建立时间推迟到业务第一条 SELECT。

-- 会话 A:明确 RR,主动建立快照。
SET TRANSACTION ISOLATION LEVEL REPEATABLE READ;
START TRANSACTION WITH CONSISTENT SNAPSHOT;

-- 切到会话 B:A 的快照建立后才修改并提交。
START TRANSACTION;
UPDATE snapshot_read_demo SET qty=210 WHERE id=2;
COMMIT;

-- 切回 A:普通 SELECT 预期仍是 200。
SELECT qty FROM snapshot_read_demo WHERE id=2;
COMMIT;

WITH CONSISTENT SNAPSHOT 不会替你修改隔离级别。在其他隔离级别下,该修饰子会被忽略并产生警告,不能指望在 RC 下靠它固定整段事务的快照。若对行为有疑问,可在启动语句后立即查看 SHOW WARNINGS;不要等别的语句覆盖了诊断信息再回看。

RR和RC与各自快照策略的静态对应结构图
图一:隔离级别与快照策略的对应关系。结构图不表现操作先后,实际顺序以正文会话示例为准。

根因判断只适用于一致性读,不是所有 SQL

这里的“同一快照”指普通非锁定 SELECT 的可见性。FOR UPDATE、FOR SHARE 和 UPDATE 等语句不能简单套用同一结论,也不应为了让报表看到新值就随手加锁。它们有不同的读取与锁定语义,可能改变并发行为。

当前事务自身之前写入的变化也是重要例外:同一事务会看见自己的修改。因此不要一边让 A 更新记录,一边用“A 的全部结果永远等于旧快照”来解释实验。本例特意让 A 只读、B 写入,保持比较条件清楚。显式快照也不是为每张表复制一份实体数据文件,不需要用存储复制来理解可见性。

普通一致性读与其他读取行为的边界结构图
图二:普通读的快照依据与需要另行解释的语句分组。连线表示语义关联,不是执行流程或真实数据库截图。

修复动作要先选择稳定视图还是逐次更新

如果报表要求多条查询在同一历史视图下相互一致,RR 的旧读可能是期望行为,此时应保留快照并向用户标明报表基准,而不是在中间强制刷新。若业务希望每条查询看见执行时已提交的新变化,可以评估 RC,但要接受前后结果可能不同。

第三轮用 id=3 对照 RC,仍只影响 A 的下一个事务,不修改全局配置。

-- 会话 A:第一次读,预期为 300。
SET TRANSACTION ISOLATION LEVEL READ COMMITTED;
START TRANSACTION;
SELECT qty FROM snapshot_read_demo WHERE id=3;

-- 切到 B:在 A 两次读取之间提交。
START TRANSACTION;
UPDATE snapshot_read_demo SET qty=310 WHERE id=3;
COMMIT;

-- 切回 A:新的一致性读快照,预期为 310。
SELECT qty FROM snapshot_read_demo WHERE id=3;
COMMIT;

另一种做法是在明确的业务边界结束旧事务,再开始新事务。对已有写入的事务,提交意味着正式保存,回滚意味着放弃,不能为了刷新视图盲目选择其中一个。重新发 START TRANSACTION 也不是安全的“清快照”操作:它可能隐式结束当前事务,必须先处理事务原本的业务责任。

防止事务边界被等待与连接复用拖长

应用应让事务只包住确有一致性要求的数据库操作,不把用户等待、网络调用和长时间计算顺手放进去。记录连接标识、事务边界与首条业务查询,异常路径显式结束事务,连接归还池前遵循驱动或框架的事务清理约定。不要只凭一次查询耗时短就断定整个事务很短。

模式普通一致性读的快照口径适用取舍
RR 普通事务首次一致性读建立并复用同事务稳定读取
RR 显式快照启动修饰子主动建立需要明确快照起点
RC每次一致性读新建接受前后值变化

事务开始很早,第一次查询为什么反而读到新值?

普通 RR 事务可能直到首次一致性读才建立快照;要确认之前是否真的没有其他一致性读。

提交另一个连接,能刷新当前 RR 事务吗?

不能据此刷新已建立的快照。是否需要结束当前事务并重新读取,应由业务一致性边界决定。

READ ONLY 会自动等于固定快照吗?

不会。读写访问模式与隔离级别、快照建立时机是不同维度;只读声明不能替代 RR 或显式快照语义。

这个假设案例的结论是:先对齐首次一致性读与并发提交的时机,再判断旧值是否异常。长事务不是“所有数据冻结在 BEGIN 时刻”的同义词,隔离级别、读类型和显式快照缺一项都可能改变解释。

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