MVCC 是什么
多版本并发控制,是一种mysql在多事务访问下的解决线程安全问题的一种无锁解决方案。主要原理是利用undo log 形成版本链,利用undo log 中记录了不同事务产生的记录和事务id 去判断记录的可见性。
事务在并发情况下可能出现的问题
- 读读
- 读写 脏读、幻读、不可重复读问题。
- 写写 可能出现覆盖更新问题。
事务隔离级别
- 读未提交
- 读以提交
- 可重复读
- 串行化
不同隔离级别下解决的问题
- 读未提交(read uncommit) 允许读取到事务未提交的数据 可能导致脏读、不可重复读、幻读
- 读已提交(READ COMMITTED) 允许事务读取到已提交的数据可能导致不可重复读、幻读
- 可重复读(REPEATABLE READ)
对同一字段多次读取的结果都是一致的、除非是被本身事务所修改。 - 串行化 解决幻读
undo log
前面提到了mvcc原理是利用undo log 形成版本链,利用undo log 中记录了不同事务产生的记录和事务id 去判断记录的可见性来实现的,具体undo log 形成的版本链类似于这样的形式。(事务id 的生成是递增的)
trx_id:每次一个事务对某条聚簇索引记录进行改动时,都会把该事务的id赋值给trx_id隐藏列.
roll_pointer:每次对某条聚簇索引记录进行改动时,都会把旧的版本写入到undo日志中,然后这个隐藏列就相当于一个指针,可以通过它来找到该记录修改之前的信息。
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-YewTbY6b-1691564281374)(en-resource://database/554:0)]
图片来源
MVCC详解,深入浅出简单易懂_lans_g的博客-CSDN博客
每次记录更新后,都会将旧值写入到undolog中,随着修改次数的增多,我们通过roll pointer 形成了一个链表,也就是版本链。版本链的头结点就是当前记录最新的数据。每个版本的数据中还携带着生成该版本数据时对应的事务id。
Read View
ReadView 是针对于使用 READ COMMITTED 和 REPEATABLE READ 这两种隔离级别来描述的,都必须读取到其他事务已经提交的了数据。这里的核心逻辑就是如何判断版本链中的数据对哪个事务是可见的。 于是出现了Read View 这个概念。
ReadView 中包含了四个比较重要的概念:
- m_ids 表示生成Read View 时当前系统中活跃的读写事务的事务id 列表
- min_trx_id : 表示生成Read View时,当前系统中活跃的读写事务中最小的事务id。也就是m_ids 中的最小值
- max_trx_id: 表示生成ReadView时系统中应该分配给下一个事务的id值。(这里的max_trx_id 并不是指m_ids中的最大id,
事务id是递增分配的。比方说现在有id为1,2,3这三 个事务,之后id为3的事务提交了。那么一个新的读事务在生成ReadView时,m_ids就包括1和2,mi n_trx_id的值就是1,max_trx_id的值就是4。) - createor_trx_id: 表示生成该事务的事务id。
在有了ReadView 的情况下,在访问某条记录时,我们只需按照如下流程判断记录的可见性即可。
- 如果被访问版本的trx_id 与ReadView中的creator_trx_id 一致。表示他在访问自己修改的数据,所以当前版本数据被该事务可见。
- 如果被访问版本的trx_id 小于ReadView 中的min_trx_id 表示该版本数据在ReadView生成前已提交,所以当前版本数据被该事务可见。
- 如果被访问版本的trx_id 大于ReadView 中的max_trx_id 表示该版本数据在ReadView生成后生成 所以当前版本数据被该事务不可见。
- 如果被访问版本的trx_id 处于ReadView 中的min_trx_id 与 max-trx_id之间。则需要判断当前trx_id 是否存在于m_ids中。
如果存在,则证明当前事务修改的事务还未提交,所以当前版本数据被该事务不可见。
如果不在,说明创建ReadView 时生成该版本的事务已经被提交,当前版本的数据对该事务可见。
Read View 在Read Committed、Repeated read 下的区别
在MySQL 中, READ COMMITTED 和 REPEATABLE READ 隔离级别的的一个非常大的区别就是它们生成ReadView的 时机不同。
READ COMMITTED
在每次读取数据前都生成一个ReadView
# Transaction 100
BEGIN;
UPDATE hero SET name = '关羽' WHERE number = 1;
UPDATE hero SET name = '张飞' WHERE number = 1;
# Transaction 200
BEGIN;
# 更新了一些别的表的记录
此刻,表hero 中 number 为 1 的记录得到的版本链表如下所示:
假设现在有一个使用READ COMMITTED 隔离级别的事务开始执行:
# 使用READ COMMITTED隔离级别的事务
BEGIN;
# SELECT1:Transaction 100、200未提交
SELECT * FROM hero WHERE number = 1; # 得到的列name的值为'刘备'
这个SELECT1 的执行过程如下:
- 在执行SELECT 语句时会先生成一个ReadView , ReadView 的 m_ids 列表的内容就是 [100, 200] ,min_trx_id 为 100 , max_trx_id 为 201 , creator_trx_id 为 0 。
- 然后从版本链中挑选可见的记录,从图中可以看出,最新版本的列name的内容是’张飞’,该版本的trx_id 值为 100 ,在 m_ids 列表内,所以不符合可见性要求,根据 roll_pointer 跳到下一个版本。
- 下一个版本的列name 的内容是’关羽’,该版本的trx_id 值也为 100 ,也在 m_ids 列表内,所以也不符合要求,继续跳到下一个版本。
- 下一个版本的列name 的内容是’刘备’,该版本的trx_id 值为 80 ,小于 ReadView 中的 min_trx_id 值100 ,所以这个版本是符合要求的,最后返回给用户的版本就是这条列name为’刘备’的记录。
之后,我们把事务id为100的事务提交一下,就像这样:
# Transaction 100
BEGIN;
UPDATE hero SET name = '关羽' WHERE number = 1;
UPDATE hero SET name = '张飞' WHERE number = 1;
COMMIT;
然后再到事务id为200 的事务中更新一下表hero 中number 为 1 的记录:
# Transaction 200
BEGIN;
# 更新了一些别的表的记录
...
UPDATE hero SET name = '赵云' WHERE number = 1;
UPDATE hero SET name = '诸葛亮' WHERE number = 1;
此刻,表hero 中 number 为 1 的记录的版本链就长这样:
然后再到刚才使用READ COMMITTED 隔离级别的事务中继续查找这个 number 为 1 的记录,如下:
# 使用READ COMMITTED隔离级别的事务
BEGIN;
# SELECT1:Transaction 100、200均未提交
SELECT * FROM hero WHERE number = 1; # 得到的列name的值为'刘备'
# SELECT2:Transaction 100提交,Transaction 200未提交
SELECT * FROM hero WHERE number = 1; # 得到的列name的值为'张飞'
这个SELECT2 的执行过程如下:
- 在执行SELECT 语句时会又会单独生成一个 ReadView ,该 ReadView 的 m_ids 列表的内容就是 [200] ( 事务id 为 100 的那个事务已经提交了,所以再次生成快照时就没有它了),min_trx_id 为 200 ,max_trx_id 为 201 , creator_trx_id 为 0 。
- 然后从版本链中挑选可见的记录,从图中可以看出,最新版本的列name的内容是’诸葛亮’,该版本的trx_id 值为 200 ,在 m_ids 列表内,所以不符合可见性要求,根据roll_pointer 跳到下一个版本。
- 下一个版本的列name 的内容是’赵云’,该版本的trx_id 值为 200 ,也在 m_ids 列表内,所以也不符合要求,继续跳到下一个版本。
- 下一个版本的列name 的内容是’张飞’,该版本的trx_id 值为 100 ,小于 ReadView 中的 min_trx_id 值
200 ,所以这个版本是符合要求的,最后返回给用户的版本就是这条列name为’张飞’的记录。
以此类推,如果之后事务id为200的记录也提交了,再此在使用READ COMMITTED 隔离级别的事务中查询表
hero 中 number 值为 1 的记录时,得到的结果就是 ‘诸葛亮’ 了,具体流程我们就不分析了。总结一下就是:
使用READ COMMITTED隔离级别的事务在每次查询开始时都会生成一个独立的ReadView。
REPEATABLE READ
只会在第一次执行查询语句时生成一个ReadView,之后的查询就不会重复生成了。
对于使用REPEATABLE READ 隔离级别的事务来说,只会在第一次执行查询语句时生成一个 ReadView ,之后的查
询就不会重复生成了。我们还是用例子看一下是什么效果。
比方说现在系统里有两个事务id分别为100、200的事务在执行:
# Transaction 100
BEGIN;
UPDATE hero SET name = '关羽' WHERE number = 1;
UPDATE hero SET name = '张飞' WHERE number = 1;
# Transaction 200
BEGIN;
# 更新了一些别的表的记录
...
此刻,表hero 中 number 为 1 的记录得到的版本链表如下所示:
假设现在有一个使用REPEATABLE READ 隔离级别的事务开始执行:
# 使用REPEATABLE READ隔离级别的事务
BEGIN;
# SELECT1:Transaction 100、200未提交
SELECT * FROM hero WHERE number = 1; # 得到的列name的值为'刘备'
这个SELECT1 的执行过程如下:
- 在执行SELECT 语句时会先生成一个 ReadView , ReadView 的 m_ids 列表的内容就是 [100, 200] ,min_trx_id 为 100 , max_trx_id 为 201 , creator_trx_id 为 0 。
- 然后从版本链中挑选可见的记录,从图中可以看出,最新版本的列name的内容是’张飞’,该版本的trx_id 值为 100 ,在 m_ids 列表内,所以不符合可见性要求,根据 roll_pointer 跳到下一个版本。
- 下一个版本的列name 的内容是’关羽’,该版本的trx_id 值也为 100 ,也在 m_ids 列表内,所以也不符合要求,继续跳到下一个版本。
- 下一个版本的列name 的内容是’刘备’,该版本的trx_id 值为 80 ,小于 ReadView 中的 min_trx_id 值100 ,所以这个版本是符合要求的,最后返回给用户的版本就是这条列name为’刘备’的记录。
之后,我们把事务id为100的事务提交一下,就像这样:
# Transaction 100
BEGIN;
UPDATE hero SET name = '关羽' WHERE number = 1;
UPDATE hero SET name = '张飞' WHERE number = 1;
COMMIT;
然后再到事务id为200 的事务中更新一下表hero 中number 为 1 的记录:
# Transaction 200
BEGIN;
# 更新了一些别的表的记录
...
UPDATE hero SET name = '赵云' WHERE number = 1;
UPDATE hero SET name = '诸葛亮' WHERE number = 1;
此刻,表hero 中 number 为 1 的记录的版本链就长这样:
然后再到刚才使用REPEATABLE READ 隔离级别的事务中继续查找这个 number 为 1 的记录,如下:
# 使用REPEATABLE READ隔离级别的事务
BEGIN;
# SELECT1:Transaction 100、200均未提交
SELECT * FROM hero WHERE number = 1; # 得到的列name的值为'刘备'
# SELECT2:Transaction 100提交,Transaction 200未提交
SELECT * FROM hero WHERE number = 1; # 得到的列name的值仍为'刘备'
这个SELECT2 的执行过程如下:
- 因为当前事务的隔离级别为REPEATABLE READ ,而之前在执行 SELECT1 时已经生成过 ReadView 了,所以此时直接复用之前的ReadView ,之前的 ReadView 的 m_ids 列表的内容就是 [100, 200] , min_trx_id 为
100 , max_trx_id 为 201 , creator_trx_id 为 0 。 - 然后从版本链中挑选可见的记录,从图中可以看出,最新版本的列name的内容是’诸葛亮’,该版本的
trx_id 值为 200 ,在 m_ids 列表内,所以不符合可见性要求,根据 roll_pointer 跳到下一个版本。 - 下一个版本的列name 的内容是’赵云’,该版本的trx_id 值为 200 ,也在 m_ids 列表内,所以也不符合要求,继续跳到下一个版本。
- 下一个版本的列name 的内容是’张飞’,该版本的trx_id 值为 100 ,而 m_ids 列表中是包含值为 100 的事务id 的,所以该版本也不符合要求,同理下一个列name的内容是’关羽’的版本也不符合要求。继续跳
到下一个版本。 - 下一个版本的列name 的内容是’刘备’,该版本的trx_id 值为 80 ,小于 ReadView 中的 min_trx_id 值100 ,所以这个版本是符合要求的,最后返回给用户的版本就是这条列c为’刘备’的记录。
也就是说两次SELECT 查询得到的结果是重复的,记录的列c值都是’刘备’,这就是可重复读的含义。如果我们之后再把事务id为200 的记录提交了,然后再到刚才使用REPEATABLE READ 隔离级别的事务中继续查找这个number 为 1 的记录,得到的结果还是’刘备’ 。
参考:Mysql 是怎么运行的:从根上理解mysql