$ cat ~/articles/114 _

MySQL为什么要搞个MVCC出来?

作者:jaifire 2026-06-24 18:04 0 阅读

前置知识

脏读、幻读、不可重复读是什么?

脏读(Dirty Read) 事务A读取了事务B尚未提交(未Commit)的数据。如果事务B后来回滚(Rollback),事务A读到的就是“垃圾”数据。

不可重复读(Non-Repeatable Read) 在同一个事务A内,两次读取同一条记录,因为中间被其他已提交的事务修改了,导致两次读出的字段值不同。

幻读(Phantom Read) 定义:在同一个事务A内,两次执行相同的范围查询,因为中间被其他已提交的事务插入(Insert)或删除了符合条件的新数据,导致第二次查询返回的行数变多了(或变少了),像出现了幻觉。

MySQL四种级别按严格程度从低到高排列如下:

隔离级别 英文名称 脏读 (Dirty Read) 不可重复读 (Non-Repeatable Read) 幻读 (Phantom Read)
读未提交 READ UNCOMMITTED 可能发生 可能发生 可能发生
读已提交 READ COMMITTED 不会发生 可能发生 可能发生
可重复读 REPEATABLE READ 不会发生 不会发生 可能发生(InnoDB可部分避免)
串行化 SERIALIZABLE 不会发生 不会发生 不会发生

需要注意REPEATABLE READ(可重复读)是 MySQL InnoDB 的默认隔离级别

MySQL为什么要搞个MVCC出来?

MVCC(多版本并发控制)是InnoDB在READ COMMITTED(读已提交)和REPEATABLE READ(可重复读)这两个级别下,实现“读不阻塞写、写不阻塞读”并发能力,并解决脏读和不可重复读问题的核心技术。 专业术语叫:一致性非锁定读

数据库的并发操作,无非就是三种:

  • 读和读,不冲突
  • 写和写。必须加锁
  • 读和写?

最让人头疼的就是这个读写冲突了:就是你这边在不断地读数据,而同一时间其他人又在开启事务执行写操作 那这时候读操作到底要不要等写操作执行完呢?

  • 如果你说等,加锁排队,那数据库的并发性能直接跌入谷底;
  • 那如果你说不等,直接读,那你可能读到一个还没提交的半吊子数据,也就是脏读,数据就全乱套了。

既要保证性能不排队,又要保证正确不读错,怎么办呢?这时候MVCC就出场了。

  • 它的基础思想就一句话:写操作不阻塞读操作,读操作也不阻塞写操作。
  • 它是怎么做到的:你写你的当前版本,我读我的历史版本,大家互不干扰。

实现机制

隐藏字段、Undo Log和Read View

认识隐藏字段

用下面的方法来直观的看一下隐藏字段长什么样

  • 我们在MySQL里开启一个事务,对一行数据执行一次update,注意先别提交。
  • 然后我们查一下内置的information_schema.INNODB_TRX这个表,示例如下
mysql> begin;
Query OK, 0 rows affected (0.01 sec)

mysql> update `users` set `name`='小明' where id = 100;
Query OK, 1 row affected (0.01 sec)
Rows matched: 1  Changed: 1  Warnings: 0

mysql> SELECT trx_id, trx_state, trx_query FROM information_schema.innodb_trx;
+--------+-----------+------------------------------------------------------------------------+
| trx_id | trx_state | trx_query                                                              |
+--------+-----------+------------------------------------------------------------------------+
| 14272  | RUNNING   | SELECT trx_id, trx_state, trx_query FROM information_schema.innodb_trx |
+--------+-----------+------------------------------------------------------------------------+
1 row in set (0.02 sec)

mysql> 

解读

  • 这里清楚地记录着当前正在运行的 trx_id 是 14272,表示这条数据最后是由 14272 这个事务写进去的,而且这个事务正在运行。
  • 执行update那一瞬间,这个 14272 就已经被MySQL悄悄写进了这行数据的隐藏字段 DB_TRX_ID 里了。
  • 除了 trx_id,还有个隐藏字段叫做回滚指针(DB_ROLL_PTR),它连着 undo log,指向 undo log 当中的一条日志链。
  • 日志链中串联着多个相关联的历史数据版本。

我们直接执行SHOW ENGINE INNODB STATUS来查看引擎的底层状态,关注其中的TRANSACTIONS事务部分。

mysql> show engine innodb status;

# 只看 TRANSACTIONS 部分

------------
TRANSACTIONS
------------
Trx id counter 14273
Purge done for trx's n:o < 14272 undo n:o < 0 state: running but idle
History list length 0
LIST OF TRANSACTIONS FOR EACH SESSION:
---TRANSACTION 422125210957200, not started
0 lock struct(s), heap size 1136, 0 row lock(s)
---TRANSACTION 422125210956288, not started
0 lock struct(s), heap size 1136, 0 row lock(s)
---TRANSACTION 422125210955376, not started
0 lock struct(s), heap size 1136, 0 row lock(s)
---TRANSACTION 422125210954464, not started
0 lock struct(s), heap size 1136, 0 row lock(s)
---TRANSACTION 14272, ACTIVE 372 sec
2 lock struct(s), heap size 1136, 1 row lock(s), undo log entries 1
MySQL thread id 30866, OS thread handle 140649913382656, query id 53967 36.170.45.91 root starting
show engine innodb status

​ 解读:

  • 14272 之前的事务全部完成了,14272事务正在活跃,现在trx_id已经记到了 14273。
  • 看这行 2 lock struct(s), heap size 1136, 1 row lock(s), undo log entries 2 MySQL不仅改了原数据,还顺着隐藏的回滚指针在undo log里生成了一条旧数据的历史版本记录。
  • 如果你再改一次,它就再生成一次,像链表一样串起来,这就叫版本链。

Read View 的判断逻辑

当执行事务查询时,MySQL会记录一个快照,记录一下当前这一瞬间数据库里有哪些事务正在活跃(也就是还没提交)。 拿到这个快照后,我们就开始拿着版本链里的数据一行行去套规则,逻辑非常的严密:

  • 如果数据的事务ID比快照里最小的活跃ID还小,说明改这个数据的事务早就提交了,这个数据绝对靠谱,可见。
  • 如果数据的事务ID比当前系统即将分配的最大ID还大,说明这个数据是未来的事务改的,不可见。
  • 如果夹在中间,那就去快照的活跃集合里面查一下:
    • 如果在这个集合里,说明还没有提交,不可见;
    • 如果不在,说明已经提交了,可见。

如果当前版本不可见怎么办?那就顺着刚才咱们验证过的版本链往下找。直到找到可见的为止,就是指查询最后一个事务完成后的数据。 注意:事务自己修改过的记录,在后续查询中必须对自己可见 ​ MySQL有两种常用的隔离级别为:

  • RC(读已提交)
  • RR(可重复读)

它们底层都用了MVCC。区别在哪呢? 其实它们在应用层可能会有各种花里胡哨的表现,但是在MVCC这一层,它们的区别仅仅在于生成Read View的时机不同。

  • 在RC级别下,事务里的每一次SELECT都会重新拍一张快照,所以如果别的事务中途提交了,数据被修改了,你下一次查询就能看到了,但这就导致了不可重复读的问题。
  • 而在RR级别下,事务只在第一次SELECT的时候会拍一张快照,事后所有的查询都用同一张快照,不管外面怎么改,你看到的世界始终如一,这就实现了可重复读。

MVCC的局限性

  1. 它只解决了读写冲突。如果是写写冲突,比如两个事务同时要update同一行,还是得老老实实加行锁去排队。
  2. 幻读问题。MVCC通过快照读确实缓解了幻读,但在RR级别下,如果你做了一次普通查询,然后又做了一次update(当前读),依然会遇到幻读。要彻底解决,还得靠Next-Key Lock(间隙锁)来配合。
  3. 空间开销。我们刚才看到了Undo Log当中的版本链,如果你写了一个超级长的大事务,迟迟不提交,底层的Undo Log版本链就会越拉越长,最终可能会撑爆表空间。所以在日常开发中,千万别写大事务。