面试官:MySQL 为什么使用 MVCC?原理是什么?
大家好,我是君哥。
MVCC 中文名称叫多版本并发控制,是 InnoDB 引擎为了提高并发效率引入的协议。今天来聊一聊 MVCC。
1.基础知识
数据库事务并发通常会遇到三个问题:
- 脏读:事务 A 读取了事务 B 未提交的修改数据。如果事务 B 回滚,事务 A 读取的数据就是无效的脏数据。
- 不可重复读:同一事务内多次读取同一行数据,这条数据因为被其他事务修改过并且已经提交事务,导致多次读取到的结果不一致。
- 幻读:同一事务内多次查询同一范围内的数据,因其他事务插入或删除符合条件的数据,导致事务在后面读取到的结果集不一样,像产生了幻觉。
其实出现幻读也会造成不可重复,所以幻读和不可重复读有时容易混淆。不可重复度主要针对的是老数据的修改,而幻读针对的是数据插入或数据删除。
针对这三个并发问题,数据库引入了隔离级别,不同隔离级别可以解决不同的问题。下面介绍的隔离级别隔离性依次变弱,并发性能依次变强。
串行化(Serializable):事务对数据读写都是串行化的。
可重复读(Repeatable Read):事务执行过程中,多次读取同一行数据,读取结果一致。MySQL 默认隔离级别就是可重复读。
读已提交数据(Read Committed):事务执行过程中,如果有其他事务修改了数据并且提交事务,当前事务可以读取到最新提交的数据。
读未提交数据(Read Uncommitted):事务执行过程中,可以读取到其他事务未提交的数据。
下表展示了这四种隔离级别对脏读、幻读、可重复读的解决情况。
隔离级别/并发问题 | 脏读 | 不可重复读 | 幻读 |
串行化 | x | x | x |
可重复度 | x | x | x |
读已提交 | x | ✓ | ✓ |
读未提交 | ✓ | ✓ | ✓ |
可重复读并没有完全解决幻读,配合 MySQL 中的 Next-Key Lock 来解决。
2.MVCC
上面讲了数据库事务并发存在的问题和 MySQL 的事务隔离级别。那什么是 MVCC 呢?
2.1 版本链
MVCC 是对同一行数据,记录多个事务的修改版本,这些版本串联起来,保存在 undolog 中。
InnoDB 引擎在每行记录中会添加了 3 个隐藏的列:
- DB_TRX_ID:修改(插入、更新或删除)这一条数据的事务 id;
- DB_ROLL_PTR:回滚指针,指向修改前的历史版本,用于回滚操作;
- DB_ROW_ID:当表中不定义主键时用作主键来自动生成聚簇索引。
MVCC 通过上面两个字段,把每个事务修改后的数据和修改前的历史版本串联起来,形成一个版本链。
举一个例子,我们有一张记录账户余额的表 t_account,字段包括 id、account(账户)、amount(金额)。初始阶段,id = 10,account = 1100 的这条记录在事务 1 提交后这个账户剩余金额是 100,事务 2 把剩余金额改成了 150,事务 3 把剩余金额改成了 200。
如下图,事务回滚的时候,可以根据 DB_ROLL_PTR 指向的版本,回滚到这个版本的数据。
图片
2.2 ReadView
上面讲了 MVCC 中的版本链,那如果现在有一个事务要读取 id = 10,account = 1100 的这条记录,这时候版本链上面有多个版本,这个事务应该读取哪个版本呢?
这时我们引入一个新的概念 ReadView(读视图),用来控制当前事务应该读取上面版本链中的那一个版本数据,它只作用于可重复读和读已提交这两个隔离级别。它主要包含 4 个属性:
MVCC 是指对同一行数据,记录多个事务的修改版本,这些版本串联起来,保存在 undolog 中。
InnoDB 引擎在每行记录中会添加了 3 个隐藏的列:
- DB_TRX_ID:修改(插入、更新或删除)这一条数据的事务 id;
- DB_ROLL_PTR:回滚指针,指向修改前的历史版本,用于回滚操作;
- DB_ROW_ID:如果表中没有定义主键,这个字段用作主键来自动生成聚簇索引。
ReadView 对可重复读和读已提交这 2 个隔离级别来说,有下面的不同:
- 已提交读:事务中每次查询操作,都会创建一个新的 ReadView。在上面的例子中,m_ids 集合是 {2,3},这时事务 4 开始,查询 t_account 中 id = 10 的记录,会新建一个 ReadView,查询到 amount = 100,如果事务 4 执行过程中,事务 2 提交,事务 4 中再次查询查询 t_account 中 id = 10 的记录,会再次创建一个 ReadView,查到 amount = 150。如下图:
图片
- 可重复读:只有事务开始的时候,创建一个新的 ReadView,后面的读操作都公用这个 ReadView。在上面的例子中,m_ids 集合是 {2,3},这时事务 4 开始,查询 t_account 中 id = 10 的记录,会创建一个 ReadView,查询到 amount = 100,如果事务 4 执行过程中,事务 2 提交,事务 4 中再次查询查询 t_account 中 id = 10 的记录,还是使用之前的 ReadView,查到 amount = 100。如下图:
图片
2.3 修改隔离级别
其实在实际使用中,我们在一个事务中很少用到重复读的情况,这种情况多数是代码写的有问题。所以好多公司会修改 MySQL 的默认隔离级别,改成读已提交。
改成读已提交还有一个好处就是可以减少死锁发生。
当然,读已提交不能解决幻读问题。比如在一个事务中,查询了两次订单量,两次查询中间又有新订单生成,订单数量会发现不一样。这类情况就要看业务上能不能接受了。
总结
MVCC 是 MySQL 中非常重要的一个并发优化,从事务隔离级别、版本链、ReadView 这几个方面着手,很容易理解 MVCC 的原理。
本文地址:https://www.yitenyun.com/326.html