1、MVCC
MVCC(Mutil-Version Concurrency Control),就是多版本并发控制,是一种并发控制的方法,一般是用在数据库管理系统中,实现对数据库的并发访问。
本篇文章以MySQL的InnoDB引擎为例,讲解下其MVCC的实现。
在InnoDB中,MVCC就是指在已提交读(RC,Read Commmit)和可重复读(RR,Repeat Read)这两种隔离级别下的事务对于select查询操作访问版本链中记录的过程。
一个支持MVCC的数据库(引擎),在更新(删,改)某些数据时,并不会直接覆盖目标数据,而是标记其为过时,同时新增一个新的数据版本,这样同一份数据就有多个版本,但只有一个是最新的。这些旧数据的版本串起来其实就是版本链。
2、版本链
在InnoDB引擎中,其聚簇索引记录中有两个必要的隐藏列:
- trx_id:这个id用来存储每次对某条聚簇索引记录进行修改的事务id
- roll_pointer:每次对聚簇索引记录有修改的时候,都会把老版本写入undo日志中,这个pointer就存了一个指针,指向这条聚簇索引记录的上一个版本的位置。 | id | name | trx_id | roll_pointer | | —- | —- | —- | —- | | 1 | 名字1 | 10000 | pointer_to_last_version_data |
举个栗子:
当前事务id为10001,要修改id为1的记录,sql如下:
update person set name = '名字2' where id = 1;
这时候,undo日志中就会存在如下版本链记录(假设上版本地址为 0x00000000):
id | name | trx_id | roll_pointer |
---|---|---|---|
1 | 名字2 | 10001 | 0x00000000 |
1 | 名字1 | 10000 |
以roll_pointer为链接,同一条记录的多个数据版本就串成了一条记录的版本链。
3、事务隔离的实现机制-ReadView
ReadView是指事务进行快照读的时候产生的读视图,在该事务执行快照读的那一刻,会生成数据库的快照,但并不是数据的快照,而是当前活跃的事务的快照,会被记录到该读视图中(事务id是递增发放的)。
假设当前的活跃事务为3,5,和11,那么该ReadView的三个属性分别为:
- trx_list:[3,5,6],即活跃的事务id集合
- up_limit_id:“低水位”,记录的是上述列表中最小的事务id,即为3
- low_limit_id:“高水位”,记录的是上述列表中最大的事务id+1,即下一个尚未分配的事务id,即为12
当进行快照读的时候,首先比较你查询数据的trx_id和低水位up_limit_id,如果data_trx_id
举个栗子:
在已提交读隔离级别下(为何要强调隔离级别),修改再次修改id为1的记录,sql如下:
update person set name = '名字3' where id = 1;
则该记录的版本链会是如下:
id | name | trx_id | roll_pointer |
---|---|---|---|
1 | 名字3 | 10005 | 0x00000001 |
1 | 名字2 | 10001 | 0x00000000 |
1 | 名字1 | 10000 |
在10005的事务提交之前,此时另一个事务对此记录发起了查询请求,则生成的ReadView的活跃事务列表只有[10005],,查询请求需要去版本链中寻找旧数据,发现最近的一条事务id是10005,发现在活跃事务列表内,则不可见,再往前找,找到了事务为10001的版本记录,小于列表中的活跃事务id,所以可以访问,就直接将name=’名字2’的记录返回。
那此时10005的事务提交了,然后又新开启了一个id为10010的事务,修改此记录,sql如下:
update person set name = '名字4' where id = 1;
那这时候版本链就是:
id | name | trx_id | roll_pointer |
---|---|---|---|
1 | 名字4 | 10010 | 0x00000002 |
1 | 名字3 | 10005 | 0x00000001 |
1 | 名字2 | 10001 | 0x00000000 |
1 | 名字1 | 10000 |
那这时候,之前的查询事务再次执行查询请求,会查到哪个版本的记录呢?
说到这,是不是已经比较熟悉了,这就是一个事务在两次读的中间,有其他事务修改了目标记录的问题。
如果隔离级别是已提交读,每次快照读的时候,就会重新生成一个ReadView,这时候的活跃事务列表就变成了[10010],那通过版本链去查询数据的话,就能查到name=’名字3’的数据(同一事务,两次查询结果不同,就是不可重复读)。
如果隔离级别是可重复读,那每次快照读的时候,不会重新生成ReadView,这时候 的ReadView还是第一次查询生成的,活跃事务列表还是[10005],去版本链查询还是只能查到name=’名字2’的数据(同一事务下,多次查询结果相同,这是可重复读)。
总结:当事务隔离级别是已提交读的时候,每次查询都会重新生成一个ReadView,如果过程中有其他事务修改了目标数据,那么同一事务内多次查询,结果可能不同;
当事务隔离级别是可重复读的时候,同一事务只有第一次查询会生成ReadView,之后同事务的查询请求都复用此ReadView,用于保证同一事务下多次查询结果相同。