MVCC概念
MVCC
全称叫做Multiversion Concurrency Control
,多版本并发控制。MVCC
的出现主要是为了提升数据库并发性能,用较好的方式处理事务并发的读写冲突
,避免了加锁操作,降低性能开销,在有读写冲突
时,能够做到非阻塞并发读。
- MVCC可以通过乐观锁的方式,在可重复读隔离级别下来解决不可重复读和幻读的问题。
MVCC原理
在MVCC机制中,多个事务对同一条记录做修改,会产生多个历史快照,这些历史快照保存在undo log
里,当一个事务对该记录进行查询时,MVCC会通过ReadView解决行的可见性问题。
ReadView是事务在使用MVCC机制进行快照读时产生的读视图。
ReadView
ReadView
字段说明:
字段 | 字段说明 |
---|---|
creator_trx_id | 创建这个 Read View 的事务 ID |
trx_ids | 生成ReadView时当前系统中活跃的 事务id列表 |
up_limit_id | 活跃的事务中最小的事务 ID |
low_limit_id | 系统最大的事务ID值(注意,不是活跃事务中的最大值) |
undolog
回滚链
- mysql数据库中,最新的记录永远是行记录。行记录中的回滚指针指向的是上一个修改该数据的事务对应的
undo log
,当判断一个事务要读取哪个版本时,需要根据最新的行记录按回滚链结合undolog进行回滚,来得到历史快照,而不是直接存放了多个行记录。 如果每一个事务启动时都创建一个整张表的快照,那显然不现实,假如一个表50G,创建个50G的快照肯定就超级慢了。
假如这里有一张book
表,表里有三个字段主键id
、book_name
、user
,book_name
为书名,user
为当前借阅该书人的姓名。表里初始数据如下:
id | book_name | user |
---|---|---|
1 | 《活着》 | 小赵 |
有两条事务分别对这条数据进行如下操作: |
事务5 | 事务10 |
---|---|
BEGIN; | |
BEGIN; | |
update book set user = '小钱' where id = 1 | |
update book set user = '小孙' where id = 1 | |
COMMIT; | |
update book set user = '小李' where id = 1 | |
update book set user = '小周' where id = 1 | |
COMMIT; | |
则上述两个事务操作会形成如下表所示的版本链: |
版本标识 | id | book_name | user | trx_id | roll_pointer |
---|---|---|---|---|---|
V5 | 1 | 《活着》 | 小周 | 10 | V4 |
V4 | 1 | 《活着》 | 小李 | 10 | V3 |
V3 | 1 | 《活着》 | 小孙 | 5 | V2 |
V2 | 1 | 《活着》 | 小钱 | 5 | V1 |
V1 | 1 | 《活着》 | 小赵 | 1 | 无 |
ReadView 规则
当有事务X
查询id = 1
这条数据时,会按照什么规则进行判断读到的值呢?这里假设要查询的事务为X
,数据版本对应的事务为D
, 当进行查询时,活跃的事务有事务5和事务10,**即X
事务此时对应的ReadView
的trx_ids
值为[5,10]
,则在判断可见性时会按照如下规则进行判断:
- 数据版本的的
trx_id
和ReadView
中的creator_trx_id
和一致,说明ReadView
由当前事务X
创建,所以该数据版本可以被事务X
查看。 - 数据版本的
trx_id
小于ReadView
的up_limit_id
,则说明该数据版本对应的事务已经提交,事务X
能够看到该数据版本。 - 数据版本的
trx_id
大于等于ReadView
的low_limit_id
,说明该数据版本对应事务的提交在事务X
之前,则该数据版本对事务X
不可见。 - 数据版本的
trx_id
在ReadView
的up_limit_id
和low_limit_id
之间,则有如下两种情况:- 数据版本对应的
trx_id
在ReadView
的trx_ids
中,说明该数据版本对应的事务在事务X
创建ReadView时未提交,则该数据版本对于事务X
不可见。 - 数据版本对应的
trx_id
不在ReadView
的trx_ids
中,说明该数据版本对应的事务已经提交,所以,该数据版本对事务X
可见。
- 数据版本对应的
上述规则在阅读起来稍微有些难以理解,下面以一个表格来辅助记忆:
< up_limit_id | trx_ids:[5,10] | >=low_limit_id |
---|---|---|
区间1 | 区间2 | 区间3 |
已提交事务集合 | 未提交事务集合 | 未开始事务集合 |
一个数据版本对应的trx_id 有以下几种可能,这里将数据版本对应的事务称作事务V : |
- 落在区间1,表示已提交,则该数据对事务X可见
- 落在区间3,表示事务V在事务X创建时还未开始,则该数据对事务X不可见。
- 落在区间2
- 如果
trx_id
不在trx_ids
列表里,表示事务V已提交,则该数据对事务X可见。 - 如果
trx_id
在trx_ids
集合里,表示事务V在事务X创建时还未完成提交,则该数据对事务X不可见。
- 如果
快照读与当前读
- 快照读,又称为一致性读,读的是快照数据。简单的
select * from t where ……
读的就是快照。 - 当前读,指读取的是最新版本,并且在读取时会对读到的记录进行加锁。加锁的 SELECT,或者对数据进行增删改(更新数据都是先读后写的)都会进行当前读。
MVCC与隔离级别
在READ UNCOMMITED
隔离级别下,允许一个事务可以读取到其他事务未提交的数据,因此,此种隔离级别下无需做任何特殊处理。
在SERIALIZABLE
隔离级别下,由于事务只能串型读取,只能采取加锁的方式实现。
在READ COMMITTED
和REPEATABLE READ
隔离级别下,要保证读到的是已提交的数据。那么,这个时候就需要用到MVCC了。总结就是:MVCC会用于READ COMMITTED
和REPEATABLE READ
隔离级别的实现。
要保证REPEATABLE READ
隔离级别的实现,只需要在第一次Select
的时候获取一次ReadView
,后面所有的Select
都使用同一个ReadView
即可。
要保证READ COMMITTED
隔离级别的实现,就需要在每次Select
的时候都重新获取一次ReadView
。