大家好,我是小耶,写功课只是为了我踩过的坑,你们别再踩了!
之前写过事务隔离级别------脏读、不可重复读、幻读。但那是表象。
隔离级别是怎么实现的?为什么InnoDB能做到"读不阻塞写、写不阻塞读"?为什么RR级别下幻读没有被完全解决?
答案都在MVCC(多版本并发控制)里。
今天把MVCC的底层机制彻底拆开讲清楚。
一、什么是MVCC
MVCC(Multi-Version Concurrency Control),多版本并发控制。它的核心思想是:同一行数据在数据库中保留多个版本,不同事务读取时看到的是符合自己隔离级别的那个版本。
InnoDB为每一行数据维护了三个隐藏字段:
-
DB_TRX_ID(6字节):最后一次修改该行的事务ID
-
DB_ROLL_PTR(7字节):指向Undo Log中该行的上一个版本
-
DB_ROW_ID(6字节):如果没有显式主键,InnoDB用它生成聚簇索引
DB_ROLL_PTR是版本链的关键------通过它,当前行可以一路回溯到所有历史版本。
二、版本链是怎么构建的
每次对数据进行修改(UPDATE或DELETE),InnoDB都会在Undo Log中记录修改前的旧值。修改后,当前行的DB_ROLL_PTR指向Undo Log中的旧版本。
举个例子:一个事务把id=1的name从"张三"改成"李四",再把age从25改成26。版本链会变成:
bash
当前行: name=李四, age=26, trx_id=100, roll_ptr -> Undo Log
↑
Undo Log: name=李四, age=25, trx_id=100, roll_ptr -> Undo Log
↑
Undo Log: name=张三, age=25, trx_id=90, roll_ptr -> NULL
版本链的每一个版本都记录了修改它的事务ID。 这个ID是Read View判断"哪个版本可见"的核心依据。
三、Read View:决定哪个版本可见
Read View是事务在快照读时生成的一个"可见性判断规则"。它包含四个核心字段:
| 字段 | 含义 |
|---|---|
| m_ids | 当前活跃事务ID列表(已启动但未提交的事务) |
| min_trx_id | 活跃事务中最小的事务ID |
| max_trx_id | 下一个将要分配的事务ID |
| creator_trx_id | 创建这个Read View的事务ID |
当一个事务要读取某行数据时,沿着版本链从头开始找:
-
如果版本的
trx_id小于min_trx_id,说明这个版本在Read View创建之前就已提交------可见 -
如果版本的
trx_id大于等于max_trx_id,说明这个版本在Read View创建之后才启动------不可见 -
如果
trx_id在min_trx_id和max_trx_id之间:如果trx_id在m_ids列表中,说明事务还活跃------不可见 ;如果不在,说明已提交------可见
沿着版本链一直找到第一个可见的版本,就是当前事务能读到的数据。
四、RC和RR的Read View差异
这是MVCC最核心的差异点。
| 隔离级别 | Read View生成时机 | 效果 |
|---|---|---|
| READ COMMITTED | 每次SELECT都生成新的Read View | 能读到其他事务最新提交的数据 |
| REPEATABLE READ | 只在事务第一次SELECT时生成,之后复用 | 整个事务期间看到的是同一份快照 |
RC下,事务A第一次SELECT时看到的是当时的数据快照。事务B提交后,事务A再次SELECT,会生成新的Read View,看到事务B提交的数据。这就是"读已提交"。
RR下,事务A第一次SELECT生成Read View后,后续所有SELECT都复用这个Read View。即使事务B提交了,事务A也看不到------因为它仍然用旧Read View判断可见性。这就是"可重复读"。
这也解释了为什么RR级别下幻读没有被完全解决。 快照读(普通SELECT)下,RR确实避免了幻读------因为Read View不变,新插入的行对当前事务不可见。但当前读 (SELECT ... FOR UPDATE、SELECT ... LOCK IN SHARE MODE)会读取最新数据,如果另一个事务插入了新行,当前读会看到它------这就是幻读。
五、快照读与当前读
InnoDB的读操作分两种:
快照读(Snapshot Read) :普通的SELECT语句。读取的是Read View决定的历史版本,不加锁,不阻塞写。
当前读(Current Read) :SELECT ... FOR UPDATE、SELECT ... LOCK IN SHARE MODE、INSERT、UPDATE、DELETE。读取的是最新版本,需要加锁。
| 读类型 | 语句 | 是否加锁 | 读取版本 |
|---|---|---|---|
| 快照读 | 普通SELECT | 不加锁 | Read View决定的版本 |
| 当前读 | FOR UPDATE / 写操作 | 加锁 | 最新版本 |
MVCC的核心价值就在于快照读------读操作不需要加锁,写操作不会被读阻塞。这是InnoDB高并发性能的基石。
六、长事务与Undo Log膨胀
MVCC有一个代价:历史版本必须保留到不再被任何Read View需要为止。
如果一个事务长时间不提交,它持有的Read View会一直阻止InnoDB清理旧版本。Undo Log不断膨胀,占用大量表空间。
监控长事务:
bash
SELECT trx_id, trx_state, trx_started,
TIMESTAMPDIFF(SECOND, trx_started, NOW()) AS duration_sec
FROM information_schema.innodb_trx
WHERE TIMESTAMPDIFF(SECOND, trx_started, NOW()) > 60;
如果发现长事务,需要评估是否可以提交或回滚。监控指标Innodb_history_list_length持续增长,说明Undo Log清理不及时,版本链过长。
七、小结
MVCC是InnoDB并发控制的内核。版本链记录历史,Read View决定可见性,快照读利用MVCC实现无锁读,当前读读取最新版本并加锁。RC和RR的核心差异在于Read View的生成时机------每次SELECT vs 首次SELECT。理解MVCC,才能理解InnoDB为什么能做到高并发下的读写不互相阻塞。
小耶在手,SQL 不愁
还有什么想了解的,欢迎留言!小耶一定知无不言言无不尽......我们下次见~