数据库 -- MVCC 多版本并发控制

InnoDB的锁机制可以解决并发控制,但开销大,常常与MVCC结合使用,在大多数情况下代替行级锁,降低开销。只在 READ COMMITTED 和 REPEATABLE READ 两个隔离级别下工作。

基本原理:
通过保存数据在某个时间点的快照来实现,当对某条记录做了变更时,老版本的数据被放在undo log里,并以指针的形式关联起来,形成一个链表。在查找老的版本时,按链表顺序查找,直到找到当前事务ID之前 已经提交的事务对应的最新那条记录即可。基于 版本链undo logRead View 实现。

版本链

InnoDB 聚簇索引记录中都包含两个必要的隐藏列(row_id并不是必要的,我们创建的表中有主键或者非NULL唯一键时都不会包含row_id列):

  • trx_id:每次对某条聚簇索引记录进行改动时,会把对应的事务id赋值给trx_id隐藏列。
  • roll_pointer:每次改动时,都会把旧的版本写入到undo日志中,然后这个隐藏列就相当于一个指针,可以通过它来找到该记录修改前的信息。

假设表中只含有一条插入记录:
在这里插入图片描述
之后两个id分别为100、200的事务对这条记录进行UPDATE操作,操作流程如下:
在这里插入图片描述
注: 两个事务中不能交叉更新同一条记录。

每次对记录进行改动,都会记录一条undo日志,每条undo日志也都有一个roll_pointer属性(INSERT操作对应的undo日志没有该属性,因为该记录并没有更早的版本),可以将这些undo日志都连起来,串成一个链表:
在这里插入图片描述

对该记录每次更新后,都会将旧值放到一条undo日志中,就算是该记录的一个旧版本,随着更新次数的增多,所有的版本都会被roll_pointer属性连接成一个链表,我们把这个链表称之为版本链,版本链的头节点就是当前记录最新的值。

回滚段中的undo logs分为: insert undo log 和 update undo log

  • insert undo log : 事务对insert新记录时产生的undo log, 只在事务回滚时需要, 并且在事务提交后就可以立即丢弃。
  • update undo log : 事务对记录进行delete和update操作时产生的undo log, 不仅在事务回滚时需要, 一致性读也需要,所以不能随便删除,只有当数据库所使用的快照中不涉及该日志记录,对应的回滚日志才会被purge线程删除。

ReadView

用于判断版本链中的哪个版本是当前事务可见的,主要包含4个比较重要的内容:

  • m_ids:表示在生成ReadView时当前系统中活跃的读写事务的事务id列表。
  • min_trx_id:表示在生成ReadView时当前系统中活跃的读写事务中最小的事务id,也就是m_ids中的最小值。
  • max_trx_id:表示生成ReadView时系统中应该分配给下一个事务的id值。

注意:max_trx_id并不是m_ids中的最大值,事务id是递增分配的。比方说现在有id为1,2,3这三个事务,之后id为3的事务提交了。那么一个新的读事务在生成ReadView时,m_ids就包括1和2,min_trx_id的值就是1,max_trx_id的值就是4。

  • creator_trx_id:表示生成该ReadView的事务的事务id。

注意:只有在对表中的记录做改动时(执行INSERT、DELETE、UPDATE)才会为事务分配事务id,在一个只读事务中的事务id值都默认为0。

只需要按照下边的步骤判断记录的某个版本是否可见:

  • 如果被访问版本的trx_id属性值与creator_trx_id值相同,意味着当前事务在访问它自己修改过的记录,所以该版本可以被当前事务访问。
  • 如果被访问版本的trx_id属性值小于min_trx_id,表明生成该版本的事务在生成ReadView前已经提交,所以该版本可以被当前事务访问。
  • 如果被访问版本的trx_id属性值大于或等于max_trx_id,表明生成该版本的事务在当前事务生成ReadView后才开启,所以该版本不可以被当前事务访问。
  • 如果被访问版本的trx_id属性值在min_trx_id 和 max_trx_id之间,那就需要判断一下trx_id属性值是不是在m_ids列表中,如果在,说明创建ReadView时生成该版本的事务还是活跃的,该版本不可以被访问;如果不在,说明创建ReadView时生成该版本的事务已经被提交,该版本可以被访问。

如果某个版本的数据对当前事务不可见的话,那就顺着版本链找到下一个版本的数据,继续按照上边的步骤判断可见性,依此类推,直到版本链中的最后一个版本。如果最后一个版本也不可见的话,那么就意味着该条记录对该事务完全不可见,查询结果就不包含该记录。

在MySQL中,READ COMMITTEDREPEATABLE READ 隔离级别的的一个非常大的区别就是它们生成 ReadView 的时机不同。

READ COMMITTED — 每次读取数据前都生成一个ReadView

比如现在系统里有两个id分别为100、200的事务在执行:

#Transaction 100
BEGIN;
UPDATE t SET c = '关羽' WHERE id = 1;
UPDATE t SET c = '张飞' WHERE id = 1;

#Transaction 200
BEGIN;
#更新了一些别的表的记录
...

在这里插入图片描述

假设现在有一个使用READ COMMITTED隔离级别的事务开始执行:

#使用READ COMMITTED隔离级别的事务
BEGIN;
#SELECT1:Transaction 100、200未提交
SELECT * FROM t WHERE id = 1; # 得到的列c的值为'刘备'

这个SELECT1的执行过程如下:

  • 在执行SELECT会先生成一个ReadView,ReadView的m_ids列表的内容就是[100, 200]。min_trx_id为100,max_trx_id为201,creator_trx_id为0。
  • 从版本链中挑选可见的记录,从图中看出,最新版本的内容是’张飞’,该版本的trx_id 值为100,在m_ids列表内,不符合可见性要求,根据roll_pointer跳到下一个版本。
  • 下一个版本的内容是’关羽’,该版本的trx_id值也为100,也在m_ids列表内,不符合。
  • 下一个版本的列c的内容是’刘备’,该版本的trx_id值为80,小于m_ids列表中最小的事务id100,所以这个版本是符合要求的,最后返回给用户的版本就是这条列c为’刘备’的记录。

若把事务id为100的事务提交一下,然后再到事务id为200的事务中更新一下表t中id为1的记录:
在这里插入图片描述

此时m_ids列表的内容就是[200],在使用READ COMMITTED隔离级别的事务中查询表t中id值为1的记录时,得到的结果就是’诸葛亮’了。

REPEATABLE READ — 在第一次读取数据时生成一个ReadView

对于使用REPEATABLE READ隔离级别的事务来说,只会在第一次执行查询语句时生成一个ReadView,之后的查询就不会重复生成了。ReadView的m_ids列表的内容就是[100, 200]。min_trx_id为100,max_trx_id为201,creator_trx_id为0。

#SELECT1:Transaction 100、200均未提交
SELECT * FROM t WHERE id = 1; # 得到的列c的值为'刘备'

#SELECT2:Transaction 100提交,Transaction 200未提交
SELECT * FROM t WHERE id = 1; # 得到的列c的值仍为'刘备'

幻读问题

假设按照上面的例子,ReadView的m_ids列表的内容就是[100, 200]。min_trx_id为100,max_trx_id为201,creator_trx_id为0。此时事务201插入一条记录,因为没有锁,可以自由插入,max_trx_id为202,m_ids列表内容不变,导致幻读。

在MVCC并发控制中,读操作可以分成两类:快照读 (snapshot read)与当前读 (current read)。快照读,读取的是记录的可见版本 (有可能是历史版本),不用加锁。当前读,读取的是记录的最新版本,并且当前读返回的记录,都会加上锁,保证其他事务不会再并发修改这条记录。

快照读(snapshot read)
简单的select操作(不包括 select … lock in share mode, select … for update)
在快照读的情况下,RR 隔离级别只会在事务开启后的第一次查询生成 Read View ,并使用至事务提交。所以在生成 Read View 之后其它事务所做的更新、插入记录版本对当前事务并不可见,实现了可重复读和防止快照读下的 “幻读”。

当前读(current read)
select … lock in share mode
select … for update
insert
update
delete
其中,除了第一条语句,对读取记录加S锁 (共享锁)外,其他的操作,都加的是X锁 (排它锁)。

在RR级别下,快照读是通过MVCC(多版本控制)和undo log来实现的,当前读是通过加record lock(记录锁)和gap lock(间隙锁)来实现的。innodb在快照读的情况下并没有真正的避免幻读, 但是在当前读的情况下避免了幻读!

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 1
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值