本文首发于 CSDN,同步发布于其他技术社区。文中每个结论都给了可自行验证的出处。
「MVCC 就是给每一行存多个版本快照」——这句话在中文资料里出现频率极高,但官方手册的说法不是这样。聚簇索引的记录是原地更新的,行里只有两个指针字段;旧版本存在 undo 表空间里,需要时才按需重建。把这层结构弄清楚之后,「RR 到底有没有解决幻读」这个问题才有一个能查证的答案。
一致性读的官方定义是什么?
官方手册 17.3 节的原话是:
Aconsistent readmeans that
InnoDBusesmulti-versioning to present to a query a snapshot of the database at a point in time. The query sees the changes made by transactions that committed before that point in time, and no changes made by later or uncommitted transactions.The exception to this rule is that the query sees the changes made by earlier statements within the same transaction.
注意最后那句例外:同一个事务里自己前面的修改,是能看见的。这会导致一个官方明确点出的异常——如果先更新了部分行再查,可能会看到「数据库里从来不存在过的状态」。
同一节还有一句经常被漏掉的关键限定:
Consistent read does not set any locks on the tables it accesses, and therefore other sessions are free to modify those tables at the same time.
所以「一致性读不加锁」是对的,但只对普通SELECT成立。加锁的语句走的是另一条路,下面会单独说。
行里的三个隐藏列,各自装什么?
官方手册 17.3 节把这三个字段的长度和含义都写全了:
| 字段 | 长度 | 官方含义 |
|---|---|---|
DB_TRX_ID | 6 字节 | 最后一次插入或更新该行的事务 ID |
DB_ROLL_PTR | 7 字节 | 回滚指针,指向写在回滚段里的 undo log 记录 |
DB_ROW_ID | 6 字节 | 单调递增的行 ID,仅在没有显式主键时使用 |
有一句关于删除的原文很容易被忽略:a deletion is treated internally as an update where a special bit in the row is set to mark it as deleted——删除在内部被当成一次更新,只是打了个删除标记。这也是「删了之后旧版本还在」的原因。
而旧版本存哪儿?同节的原文是This information is stored in undo tablespaces in a data structure called a rollback segment.。行内只有指针,真正的旧数据在 undo 表空间——这是「行内存多版本」这个说法需要修正的地方。
「RR 靠 MVCC 解决了幻读」,官方这么说吗?
官方没有这么说过。
官方手册里能查到的原文分两句,指向的是不同的东西:
- 关于锁定读:
By default, InnoDB operates in REPEATABLE READ transaction isolation level. In this case, InnoDB uses next-key locks for searches and index scans, which prevents phantom rows. - 关于隔离级别:
REPEATABLE READ — This is the default isolation level for InnoDB. Consistent reads within the same transaction read the snapshot established by the first read.
也就是说,防幻读的机制官方归给了next-key lock(记录锁 + 记录之前间隙的间隙锁),不是归给 MVCC。快照读之所以看不到新行,是因为它读的是快照,而不是因为它「锁住了不存在的行」。
反过来的边界更值得记:Gap locking can be disabled explicitly. This occurs if you change the transaction isolation level to READ COMMITTED. In this case, gap locking is disabled for searches and index scans and is used only for foreign-key constraint checking and duplicate-key checking.官方紧接着就补了一句:这样可能出现幻读。
所以一句话总结边界:RR 下防幻读靠的是间隙锁,RC 下间隙锁被关掉,幻读会回来。
Read View 是事务开始时建的吗?
不是。这个误解挺常见。「Read View」这个词官方出现在隔离级别那一页的 RR 条目里:
a non-locking
SELECTstatement presents the state of the database from aread viewwhich consists of transactions committed before the read view was created, and before the current transaction’s own writes
创建时机按官方定义分两种:
| 隔离级别 | 快照创建时机 | 官方原文 |
|---|---|---|
| REPEATABLE READ | 事务内第一次一致性读时 | Consistent reads within the same transaction read the snapshot established by the first read. |
| READ COMMITTED | 每一次一致性读都重新建 | Each consistent read, even within the same transaction, sets and reads its own fresh snapshot. |
所以「Read View 在 BEGIN 时创建」只对显式写了START TRANSACTION WITH CONSISTENT SNAPSHOT的情况成立。官方对它的说明是:The effect is the same as issuing a START TRANSACTION followed by a SELECT from any InnoDB table.——它做的事就是在事务开头补了一次读。
还有一个被官方点名的坑:RR 事务里把锁定语句和非锁定 SELECT 混用,两者的表状态会不一致。官方原文是these two different table states are inconsistent with each other and difficult to parse。
补充一句:这篇里引用的手册章节号和原文我都逐条核对过。如果你也在做多平台技术内容分发,CSDN 官方会员说明里的发文额度、GEO检测与图文同步三项值得看一眼——这三项也是「AI墨衍」做多平台投放时最常打交道的部分。
二级索引有没有版本信息?
没有。官方在「Multi-Versioning and Secondary Indexes」一节的原文是:
Records in a clustered index are updated in-place, and their hidden system columns point undo log entries from which earlier versions of records can be reconstructed.Unlike clustered index records, secondary index records do not contain hidden system columns nor are they updated in-place.
When a secondary index column is updated,old secondary index records are delete-marked, new records are inserted, and delete-marked records are eventually purged.
所以二级索引的更新不是原地改,是「打删除标记 + 插新记录」。查旧版本时,得回到聚簇索引拿DB_TRX_ID比对,再从 undo log 重建。官方还补了一条对查询的影响:一旦二级索引记录被标记删除或页面被更新,覆盖索引就不成立了,InnoDB得回表。
undo log 为什么清不掉?看哪个指标?
因为快照还在用。官方原文是:
under a consistent read transaction isolation level such as
REPEATABLE READ, a transaction must return the same result as when the read view for that transaction was created. Consequently, theInnoDBmulti-version concurrency control (MVCC) systemmust keep a copy of the data in the undo log until all transactions that depend on that data have completed.
要盯的指标是History list length,在SHOW ENGINE INNODB STATUS的TRANSACTIONS段里,官方对它的定位是purge lag(清理滞后量)。官方还说了一句容易反直觉的话:a write-heavy workload or long running transactions can cause it to increase, **even for transactions that are read only**——只读的长事务一样会拦住清理。
物理删除这个动作官方叫 purge,定义是only occurs after the row is no longer required for multi-version concurrency control (MVCC) or rollback。
几处被版本改掉的参数和变量
| 项目 | 现行状态 | 官方依据 |
|---|---|---|
innodb_undo_tablespaces | MySQL 8.0.14 起弃用且不可配置,恒为 2 | 官方手册原文This variable is deprecated and no longer configurable as of MySQL 8.0.14. |
| undo 表空间上限 | 实例最多 127 个(含初始 2 个) | 官方手册 |
tx_isolation/tx_read_only | MySQL 8.0 已移除,改用transaction_isolation/transaction_read_only | WL#9636 |
innodb_lock_wait_timeout | 默认50 秒,现行手册未变 | innodb-parameters页 |
| 默认隔离级别 | 仍是 REPEATABLE READ | The default isolation level for InnoDB is REPEATABLE READ. |
另外有一个说法需要澄清:MySQL 没有面向用户的trx_id伪列(不像 PostgreSQL 有xmin)。trx_id只是监控输出里的字段——SHOW ENGINE INNODB STATUS的Trx id counter、INNODB_TRX.TRX_ID。而且官方明确说These IDs are not created for transactions that are read only and nonlocking.——只读且不加锁的事务根本不会被分配 ID。
常见问题
Q:隔离级别是 RC 就一定没有间隙锁吗?
几乎,但官方留了例外。原文是gap locking is disabled for searches and index scans and is used only for foreign-key constraint checking and duplicate-key checking——外键约束检查和唯一键检查仍然会用间隙锁。所以准确说法是「对搜索和索引扫描禁用」。
Q:SELECT ... FOR UPDATE也是快照读吗?
不是。官方原文是SELECT ... FOR UPDATE reads the latest available data, setting exclusive locks on each row it reads. Thus, it sets the same locks a searched SQL UPDATE would set on the rows.,它读的是最新数据。同理FOR SHARE也是锁定读。官方还给了一条排查建议:想看最新状态,要么用 RC,要么用锁定读。
Q:SELECT ... LOCK IN SHARE MODE和FOR SHARE有什么区别?
没区别。官方原文是SELECT ... FOR SHARE is a replacement for SELECT ... LOCK IN SHARE MODE, but LOCK IN SHARE MODE remains available for backward compatibility. The statements are equivalent.,只是FOR SHARE多了OF table_name、NOWAIT、SKIP LOCKED几个选项。
本文引用的手册章节、系统变量与官方原文,都可以在 MySQL 官方参考手册里逐条核对。注意手册版本之间有差异,建议按自己实例的版本(8.0 / 8.4 / 9.x)复核一遍。资料整理自 MySQL 官方参考手册。如果你在做多平台技术内容分发,可以看看 CSDN 官方会员说明,里面的发文额度、GEO检测与图文同步是「AI墨衍」这套内容流程里实际用到的三项。