资讯动态

事务已经开始,为什么还能读到后来提交的数据?用两个连接看懂InnoDB快照

发布时间:2026/10/24 1:44:03 来源:尧图企业网站定制
事务已经开始为什么还能读到后来提交的数据整理“事务和锁”的笔记时很容易记住一个词可重复读。假设账户余额原本是1000。连接A先执行START TRANSACTION连接B随后把余额改成1100并提交。然后A才第一次查询余额。你觉得A会读到1000还是1100如果把“事务开始”理解成“从这一刻起眼前的数据就固定了”那应该是1000。但在下面的InnoDB可重复读实验里结果是1100。我们先把操作顺序走完再看为什么。只准备一行开两个连接用自己专门的测试实例不要在业务库里做。下面新建一个数据库同名库已存在时换名字不要为了照抄而删除旧库。这里不用临时表因为两个连接需要访问同一张表。本次实测为MySQL 8.0.46、InnoDB。A、B是同一实例上的两个独立连接所有步骤按先后手动切换不是一口气把A的整篇SQL运行完再运行B。在B初始化SELECT VERSION(); CREATE DATABASE blog_snapshot_lab; USE blog_snapshot_lab; SET autocommit1; CREATE TABLE account ( id INT PRIMARY KEY, balance INT NOT NULL ) ENGINEInnoDB; INSERT INTO account VALUES (1,1000);B保持自动提交后面每条成功的UPDATE就会提交。这里是新建的测试连接不要在有未提交业务的连接里随意切到autocommit1。A先开始事务但暂时不查表在A执行下面几句执行到这里就停USE blog_snapshot_lab; SET SESSION TRANSACTION ISOLATION LEVEL REPEATABLE READ; SELECT session.transaction_isolation; START TRANSACTION;隔离级别输出应为REPEATABLE-READ。这里查看配置变量不是读取account表。切到B把余额改为1100等UPDATE返回UPDATE account SET balance1100 WHERE id1;现在才回到A第一次查账户SELECT balance FROM account WHERE id1;结果是1100。先别急着说可重复读失效了。到现在为止A只查过一次根本还没有“两次查询”的结果可以比较。让B再改一次UPDATE account SET balance1200 WHERE id1;再让A执行相同的普通SELECTSELECT balance FROM account WHERE id1;这次仍然是1100不是1200。把刚才的变化顺着往下看初始余额1000 | ASTART TRANSACTION | B改成1100并提交 | A第一次普通查表 - 1100 | B改成1200并提交 | A第二次普通查表 - 1100原来关键点不是A执行START的那一行而是A第一次普通读InnoDB表的那一行。快照不是复制了一张表在这个可重复读事务中第一次一致性读建立快照后续一致性读沿用它。所以B在第一次读之前提交的1100可见之后提交的1200没有被后续普通读看见。MySQL一致性读说明这里的普通SELECT就是一致性读。后面会单独看加FOR UPDATE的查询不能把所有读都混在一起。“快照”这个词也容易让人想到A第一次查询时数据库是不是复制了一整张account表并不是。InnoDB利用记录的版本信息和undo等机制判断哪些版本对这个读视图可见需要时构造可见的旧版本。先用本例理解它的作用数据库里已经有1200但A仍能读到符合自己视图的1100。它不是把B的修改撤销了也不是阻止B提交。这里的“第一次读”特指该事务第一次一致性读InnoDB数据不一定是第一次读account。如果事务之前已经普通读取过其他InnoDB表就不能再把本例的account查询当成建立快照的位置。如果就是希望在START时确定快照呢先结束A的事务COMMIT;在B把本实验数据重置回1000UPDATE account SET balance1000 WHERE id1;A这次换一种开始方式仍然明确使用可重复读SET SESSION TRANSACTION ISOLATION LEVEL REPEATABLE READ; START TRANSACTION WITH CONSISTENT SNAPSHOT;然后B改为1100UPDATE account SET balance1100 WHERE id1;A现在才第一次查accountSELECT balance FROM account WHERE id1; COMMIT;这次读到1000。只改了事务开始方式快照建立的位置就提前了初始余额1000 | ASTART ... WITH CONSISTENT SNAPSHOT 此处已确定快照 | B改成1100并提交 | A第一次查account - 1000它没有把隔离级别自动改成可重复读。这是RR下的用法不要把这句当成所有隔离级别都适用的固定快照开关。START TRANSACTION语法说明换成读已提交两次普通读会怎样仍在同一对连接中。A上一步已提交在B重置UPDATE account SET balance1000 WHERE id1;A这次改用READ COMMITTEDSET SESSION TRANSACTION ISOLATION LEVEL READ COMMITTED; SELECT session.transaction_isolation; START TRANSACTION; SELECT balance FROM account WHERE id1;隔离级别输出为READ-COMMITTED第一次读取1000。然后B修改UPDATE account SET balance1100 WHERE id1;A再读SELECT balance FROM account WHERE id1; COMMIT;这次是1100。RC下每次一致性读都会取得新的快照因此A的第二次普通SELECT能看见B这次提交RR下则沿用该事务此前建立的快照。看到“两次结果不一样”时先核对实际隔离级别和是否仍是同一个事务不要只凭“我写了BEGIN”判断。一个容易混淆的尾巴加FOR UPDATE算第二次普通读吗不算。我们再用一小段操作看清楚仍然只在这张测试表里做。B重置UPDATE account SET balance1000 WHERE id1;A切回RR开始事务并读一次SET SESSION TRANSACTION ISOLATION LEVEL REPEATABLE READ; START TRANSACTION; SELECT balance FROM account WHERE id1;得到1000。B改为1100并提交UPDATE account SET balance1100 WHERE id1;然后A在自己的同一个事务中执行SELECT balance FROM account WHERE id1; SELECT balance FROM account WHERE id1 FOR UPDATE; SELECT balance FROM account WHERE id1; COMMIT;三次依次是普通SELECT - 1000 SELECT FOR UPDATE - 1100 普通SELECT - 1000中间读到1100不表示快照被刷新了。它是锁定读需要获取相应的锁并按这类读取的规则处理这里B已经提交也没有其他冲突锁所以读到1100。之后再执行普通一致性读仍沿用之前的视图返回1000。真实并发下锁定读可能等待不能简单背成“永远立即返回最新值”。本例A也没有修改这行如果A自己更新数据后续普通读还能看见本事务自己的修改不能把“始终1000”推广到那种情况。MySQL锁定读说明需要观察谁被谁阻塞可以接着看此前的三会话锁等待实验。这篇只把两种读取分清不把锁定读当作刷新快照的按钮。这个实验怎样避免测错对象人工照做时最容易出错的不是SQL拼错而是顺序A还没读B是否已经提交是不是换了一个连接导致前面的事务根本没保留验证程序因此保留A、B两条连接每做完一步等该连接返回完成标记再发下一步。不会为每条SQL重新启动一个连接也不靠“睡一秒应该执行完了”来猜。判断依据直接写成数值对照普通RR应读到1100、1100显式快照应读到1000RC应读到1000、1100混合普通读和锁定读应读到1000、1100、1000。比如提前让A读一次就会把第一组结果改变中途换连接也可能丢失原来的读视图。因此还要记录连接ID和步骤顺序不能只看SELECT有没有报错。所有实验都在独立临时数据目录启动的MySQL中完成服务不开放TCP只通过本地socket连接结束后关闭两个客户端和服务。本文只使用同一行的简单SELECT与UPDATE不包含DDL并发、事务自己的写入、多表读视图内部结构或所有隔离异常。回头再看最初那个问题A虽然先开始事务但普通RR的快照还没建立等到第一次一致性读时B的1100已经提交所以它能被看见。真正需要在时间线上圈出来的是建立快照的那一步不只是START那一步。

读完文章,也想定制专属网站?

尧图设计师 24 小时内与您沟通定制方案

免费获取报价 →
↑