MVCC
1. MVCC是什么?
MVCC
,全称Multi-Version Concurrency Control
,即多版本并发控制。MVCC
是一种并发控制的方法,一般在数据库管理系统中,实现对数据库的并发访问,在编程语言中实现事务内存。 MVCC
的具体实现,还需要依赖于数据库记录中的三个隐式字段、undo log
日志、readView
。
2. 当前读和快照读
- 当前读
当前读取的是记录的最新版本,读取时还要保证其他并发事务不能修改当前记录,会对读取的记录进行加锁。对于我们日常的操作,如:
select * from test lock in share mode; # (共掌锁)
select* from test for update; # (排他锁)
# update、insert、delete等DML语言也会加排它锁,造成当前读
-
快照读
-
Repeatable Read(可重复度隔离级别)
:开启事务后第一个selecti语句才是快照读的地方。 -
Read Committed(读提交隔离级别)
:每次select,都生成一个快照读。
-
简单的select
(不加锁)就是快照读,快照读,读取的是记录数据的可见版本,有可能是历史数据,不加锁,是非阻塞读。
3.MVCC实现原理
对于每一个数据库中的数据都会创建 3 个隐藏字段,包括 db_trx_id、db_roll_pointer、db_row_id。其中 db_trx_id 代表的是本次事务的id,db_roll_pointer 代表的是上一个已提交事务的地址,db_row_id 只有当该表不存在主键时才会创建。
undo log
调用链,undo log
会保存每一次对数据库的修改,用来保证输事务回滚。其中链表的尾部代表的是最新的修改,链表的首部代表的是最早的旧记录。
其中由于 select
不会对数据进行修改,所以 select
不会添加 undo log
日志。只有进行 DML 操作的时候才会有 undo log
-
Insert
:插入一条记录时,至少要把这条记录的主键值记下来,之后回滚的时候只需要把这个主键值对应的记录删掉就好了。 -
Update
:修改一条记录时,至少要把修改这条记录前的旧值都记录下来,这样之后回滚时再把这条记录更新为旧值就好了。 -
Delete
:删除一条记录时,至少要把这条记录中的内容都记下来,这样之后回滚时再把由这些内容组成的记录插入到表中就好了。 删除操作的时候,会有一个逻辑删除的字段DELETED_BIT
,将其置为true
并不是真正的删除
为了节省磁盘空间,InnoDB有专门的purge线程来清理DELETED_BIT为true的记录。为了不影响MVCC的正常工作,purge线程自己也维护了一个read view(这个read view相当于系统中最老活跃事务的read view);如果某个记录的DELETED_BIT为true,并且DB_TRX_ID相对于purge线程的read view可见,那么这条记录一定是可以被安全清除的。
4. readView
InnoDB的默认隔离级别是可重复读,所以在该事务之前且提交的数据改变是可见的,该事务之后的数据改变不可见。
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-MVLDfcJI-1669391753167)(images/image-20221125225734637.png)]
如图所示,如果该线程开启的事务 id 为 20,那么在 20 之前且提交的事务都是可见的,在 20 之后的事务都是不可见的。不过MySQL进行优化,用一个 readView 来存放当前的活跃事务。
如上图所示,事务 20,对于事务 7 以前的都是可见的,但是对于事务 7 、事务 9 和事务 11 是不可见的,因为他们是活跃的事务,还没有提交。对于事务20以后是不可见的,因为肯定会大于 20 。
5. 可重复读原理
可重复读就是利用了这样的一个思想,当进行第一次快照时,变回构建视图。
如上图,当开启事务 20 视图列表就会构建好。但是进行当前读的会重新构建视图。
思考
事务 9 对表中字段进行修改当前读能看到吗?如果能看到,那岂不是违背了可重复读?
答:在进行第一次快照读建立视图之后,以后所有快照读都会使用这个视图。而当前读是可以看到之前提交的内容。下面是我做的一个实验,可以看出很容易出现莫名其妙的BUG,比如我只想加 1 ,但是最后却加了 2 。如果此时采用 where 条件去筛选很可能会发生意料之外的错误。
事务1 | 事务2 |
---|---|
begin; | |
begin; | |
select * from product; # (price = 5000) | |
update product set price = price+1 where pid=1; | |
commit; | |
select * from product lock in share mode; # (price = 5001) | |
select * from product; # (price = 5000) | |
update product set price = price+1 where pid=1; | |
select * from product; # (price = 5002) 自己的修改是可见的 |
6. 读提交
了解了可重复读之后,读提交其实比较容易理解,就是每一次select
之后都进行一次视图重建。下面是采用读提交的过程。
事务1 | 事务2 |
---|---|
begin; | |
begin; | |
select * from product; # (price = 5000) | |
update product set price = price+1 where pid=1; | |
commit; | |
select * from product lock in share mode; # (price = 5001) | |
select * from product; # (price = 5001) | |
update product set price = price+1 where pid=1; | |
select * from product; # (price = 5002) 自己的修改是可见的 |
引用
https://pdai.tech/md/db/sql-mysql/sql-mysql-mvcc.html#%E4%BB%80%E4%B9%88%E6%98%AFmvcc
《MySQL实战45讲》
https://www.bilibili.com/video/BV1Kr4y1i7ru