MVCC是多版本并发控制,允许多个事务同时读取和写入数据库,而无需互相等待,从而提高数据库的并发性能。
在 MVCC 中,数据库为每个事务创建一个数据快照。每当数据被修改时,MySQL不会立即覆盖原有数据,而是生成新版本的记录。每个记录都保留了对应的版本号或时间戳。
依赖实现:隐藏字段(rowId+trxId)+undolog+readview
MVCC本质是采用乐观锁思想, 非阻塞并发读 ,而这个读指的就是快照读
, 而非当前读
-
当前读
就是加锁操作,是悲观锁的实现。当前读取的是记录的最新版本,读取时还要保证其他并发事务不能修改当前记录
SELECT ... FOR UPDATE #为查询到的每一条记录添加排他锁 SELECT ... LOCK IN SHARE MODE #为查询到的每一条记录添加共享锁
-
快照读
又叫一致性读,读的是快照数据,可能不是最新数据,而是历史数据,不加锁
的 简单select都属于快照读
ReadView
隐藏字段和 undo 版本链决定是时返回的数据,具体返回哪个数据版本,由这个ReadView控制,即 进行快照读操作时产生的读视图
设计思路
这个ReadView其实就是维护了一个集合,主要包含4个部分,分别如下:
-
creator_trx_id
,创建这个 Read View 的事务 ID -
trx_ids
: 表示在生成 ReadView 时当前系统中活跃的读写事务的事务id列表 -
up_limit_id
:活跃事务中最小的事务 ID -
low_limit_id
:生成 ReadView 时,应该分配给下一个事务的 ID 值。(最大事务 ID +1)
规则
有了ReadView,那么在访问某条记录时,只需要按照下边的步骤查看记录的对应快照版本
整体操作流程
比如查询一条记录的时候,系统如何通过MVCC找到它:
-
首先获取事务自己的版本号trxId,也就是事务 ID;
-
获取 当前的系统的 ReadView ,然后与 ReadView 中的事务版本号进行比较;
-
如果不符合 ReadView 规则,就从 Undo Log版本链中依次往下获取该记录的历史快照事务ID进行按照上面规则比较;
-
最后返回符合规则的数据。若最后一个版本都不可见,说明该条记录对于目前该事务完全不可见(没提交),也就查不到该条记录
读已提交和可重复读
在隔离级别为
读已提交
时,一个事务中的每一次SELECT查询都会重新获取一次Read View。
这时如果 Read View 不同,就可能产生不可重复读或者幻读的情况
当隔离级别为
可重复读
的时候,解决了不可重复读,并通过 间隙锁+MVCC 解决了大部分的幻读问题
大部分幻读解决:
-
因为
可重复读
复用,第一次readView,所以事务的查询快照结果是一样,不会平白无故多出数据来,通过readView解决了快照读的幻读 -
间隙锁的话解决了当前读的幻读问题,防止其他事务在这个间隙间插入新的记录。
例外情况:
-
比如,如果两个事务,事务1先进行快照读,然后事务2插入了一条记录并提交,在事务1中进行了当前读之后,再进行快照读也会发生幻读。
因为此时一个事务只在第一次 SELECT 的时候会获取一次 Read View,而后面所有的查询都会复用这个 Read View
举例说明
读已提交
每次读取数据前都生成一个ReadView
-- 现在有两个 事务id 分别为 10 、 20 的事务在执行 # Transaction 10 BEGIN; UPDATE student SET name="李四" WHERE id=1; UPDATE student SET name="王五" WHERE id=1; # Transaction 20 BEGIN; # 更新了一些别的表的记录 ...
此刻,表student 中 id
为 1
的记录得到的版本链表如下所示:
假设现在有一个使用 READ COMMITTED
隔离级别的事务30开始执行:
# 使用READ COMMITTED隔离级别的事务 BEGIN; # SELECT1:Transaction 10、20未提交 SELECT * FROM student WHERE id = 1; # 得到的列name的值为'张三',因为王五李四事务是活跃的,所以查undolog 读取历史快照张三
之后,我们把 事务id
为 10
的事务提交一下:
# Transaction 10 COMMIT;
然后再到 事务id
为 20
的事务中更新一下表 student
中 id
为 1
的记录:
# Transaction 20 ... UPDATE student SET name="钱七" WHERE id=1; UPDATE student SET name="宋八" WHERE id=1;
此刻,表student中 id
为 1
的记录的版本链就长这样:
然后再到事务30中继续查找这个 id 为 1 的记录
# SELECT2:Transaction 10提交,Transaction 20未提交 SELECT * FROM student WHERE id = 1; # 得到的列name的值为'王五'
所以看到,两次读取的记录并不一样,所以不可重复嘛
可重复读
只会在第一次执行查询语句
时生成一个 ReadView
,之后的查询就不会重复生成
了
得到的列name的值仍为'张三',因为复用第一次的readview,所以上图还是认为王五没有提交