Mysql中的MVCC是什么?
阅读大纲
对话解析
话题:MVCC是什么?
大佬: 那么Mysql中的MVCC?你知道是啥不?
凯歌: 那个啊,是这几个单词的简写,multi-version-concurrency-control,翻译成中文就是多版本并发控制。是一种并发控制机制。当有多个事务同时对数据库进行读写
的时候,有了这个机制就可以让他们一块执行,不用等待
。
话题:MVCC咋实现的,简单讲解
大佬: 哎呦?这么厉害?它咋实现的啊?加锁?
凯歌: 它呀,当多个事务同时读写数据库的时候,它给每个数据创建了一个数据快照,然后mysql那不会立即覆盖原有的数据,而是生成新的版本记录。每个记录都保留了对应的版本号和时间戳
。
凯歌: 多个版本之间串联
起来,就形成一条版本链,这样不同时刻启动的事务,可以无锁
的获得,不同版本的数据(普通读)。此时读(普通读)写操作不会阻塞。
凯歌: 写操作可以继续写,无非就是会创建新的数据版本,但是只有在事务提交后,新版本才会对其他事务可见,未提交的事务修改,不会影响其他事务的读取。记录中的历史版本可供已启动的事务读取。
凯歌: 老样子,画个简图,给你个看看。
图内容简介:
同一时刻,有三个事务,要对id=1的数据进行操作
第一个事务:新增id为1的数据,新增姓名为凯歌
第二个事务:修改id为1的数据,修改姓名为yes
第三个事务:修改id为1的数据,修改姓名为鱼皮
话题:版本链是什么?
大佬: 你这说的也太简单了,能不能深入说说啊,比如这个多版本,这个版本链到底是咋实现的?
凯歌: 没问题,其实吧,这个MVCC里的多版本,并不是说真的存储了多个版本的的数据,只是借助Undo Log记录了每次写操作的反向操作,所以索引上对应的记录只会有一个版本,即最新版本。只不过可以根据Undo Log中的记录反向操作得到数据的历史版本,所以看起来是多个版本。
**凯歌:**上面这段说的有点多,我给你画个图,协助理解下。
凯歌: 这一块,比较难理解**,**下面我拿上面的insert语句,再进行一个举例。
凯歌: 当我们执行insert语句成功后,在数据页中,会新增一条id=1,name=凯歌的记录。
凯歌: 在id=1,name=凯歌的时候,还会记录一些别的信息,我们称为隐藏字段,这里我们在insert成功的同时,就还存储了trx_id
和roll_pointer
这两个隐藏字段。如下图:
trx_id:当前事务ID
roll_pointer:指向Undo Log的指针
凯歌: 如上图,此时插入的事务ID=1,此时插入,会生成一条Undo Log,并且记录上的roll_pointer会指向这条Undo Log,而这条Undo Log是一个类型为TRX_UNDO_INSERT_REC的log,代表是insert生成的。
凯歌: 里面存储了主键的长度和值(其他值,暂时不提),所以InnoDB,可以根据undo log里面的主键值,找到这条记录,然后把他删除,来实现回滚(复原)的效果。
凯歌: 因此可以简单的理解undo log里面存储的就是当前操作的反向操作,可以认为里面存储了一个delete 1;
凯歌: 此时,事务ID=1的事务提交,然后,另一个事务ID=5的事务在执行update语句,更新id=1,name=yes,此时的记录和undo log 就如下图:
凯歌: 正如上图,我们之前insert对应的undo log消失了。insert的事务提交了之后,对应的undo log就回收了。因此,再有事务来访问的时候,就不可能访问到比这个还要早的版本。
凯歌: 但是!update产生的undo log是不一样的,它的类型为,TRX_UNDO_UPD_EXIST_REC。
凯歌: 具体哪里不一样,我们继续往下看,当我们的事务ID=5,也就是刚才的update事务,提交后,来了另一个事务ID=11的事务来了,它要执行update语句,将id=1,name=yes的数据,改为id=1,name=鱼皮。那么他执行后现象,就如下图所示:
凯歌: 正如上图,这条update语句执行之后,我们之前产生的 undo log没有删除,依旧保留,因为可能会有别的事务,需要访问之前的版本,所以不能删除。
凯歌: 这样就形成了一个版本链,可以看到记录本身id=1,name=鱼皮,外加两条undo log ,这条id=1的记录,一共有三个版本。
话题:ReadView是什么?
大佬: 哦,这版本链,我明白了,但是这么多版本,一个事务过来了,它咋知道用哪个版本啊?你这也没说啊?
凯歌: 说,这就说。要想知道,当前事务,需要的版本是哪个,我们就需要引入一个新的东西,叫ReadView。
凯歌: 这个ReadView吧,就是用来判断哪个版本对当前事务,是可见的。这里有四个概念,见下:
creator_trx_id:当前事务ID
m_ids:生成readView时,还不活跃的事务ID集合,也就是已经启动,但还未提交的事务列表。
min_trx_id:当前活跃ID之中的最小值。
max_trx_id:生成readView时,InnoDB将分配给,下一个事务的,ID的值。(事务ID是递增分配的,越后面申请的事务ID就越大)。
凯歌: 对于当前事务,可见版本的判断,是从最新版本开始,沿着版本链逐渐寻找老的版本,如果遇到符合条件的版本就返回。
凯歌: 判断条件如下:
TRX_ID(当前数据事务ID) | 是否可见 | 原因 |
---|---|---|
trx_id == creator_trx_id | 可见 | 修改这条数据的事务,就是当前事务,所以可见 |
trx_id <min_trx_id | 可见 | 修改这条数据的事务,在当前事务生成readView的时候,已经提交,所以可见 |
min_trx_id =< trx_id < max_trx_id 且 trx_id 在 m_ids中 | 不可见 | 说民修改这条数据的事务,还没提交 |
min_trx_id =< trx_id < max_trx_id 且 trx_id 不在 m_ids中 | 可见 | 说明修改这条数据的事务,已经提交 |
trx_id >= max_trx_id | 不可见 | 说明修改这条数据的事务,在当前事务生成readView的时候,还没启动,所以不可见。(事务ID是递增的) |
凯歌: 光看文字,没有效果,我们举个例子,练练手
举例:读已提交隔离级别下的MVCC
凯歌: 现在隔离级别是:读已提交
背景描述:
假设,此时上文的:
事务ID=1的事务,已提交
事务ID=5的事务,已执行
此时:
事务ID=6的事务来了,状态是:未提交
(trx_id=6的事务要执行update table set name = '哈哈' where id = 2;)
现在,最大事务ID是6
这时候:
有一个查询语句,开启了事务,语句为:select name from table where id = 1;
那么这个查询语句对应的上述四个概念的值,见下表
指标 | 值 | 原因 |
---|---|---|
creator_id | 0 | 因为一个事务,只有当有修改操作的时候,才会被分配事务ID |
m_ids | [5,6] | 这两个事务都未提交,是活跃的 |
min_trx_id | 5 | |
max_trx_id | 7 | 因为当最大的事务ID是6,新来的时候,会递增,也就是7 |
凯歌: 由于,trx_id=7的事务,要查找的是id=1的数据,所以需要先找到id=1的这条记录,此时的版本,如下图:
凯歌: 此时,id=1的数据的,最新版本记录上的trx_id=5,等于min_trx_id,且trx_id=5,在m_ids中,表明还是活跃的,未提交事务的,所以不可访问。
凯歌: 因为trx_id=5对应的数据,不可访问,所以,根据roll_pointer,我们找到了上图中的undo log
凯歌: 在undo log中,trx_id=1,比min_trx_id还要小,说明在生成readView之前,就已经提交了。所以可以访问。
凯歌: 因此,trx_id=7的事务,会获得name=凯歌的结果。
事务ID=7的事务,拿到结果后,事务ID=5的事务,提交了
此时,再次执行之前的SQL:select name from table where id = 1;
又会生成新的readView
同样,我们还是看那四个指标
指标 | 值 | 原因 |
---|---|---|
creator_id | 0 | 没有修改操作,所以还是0 |
m_ids | [6] | 此时5已经提交了,所以只剩6了 |
min_trx_id | 6 | |
max_trx_id | 7 | 因为没有新的事物进来,所以还是7 |
凯歌: 此时,根据SQL,我们还是查找的id=1的数据,所以当前版本还是跟刚才一样的图,如下:
凯歌: 但是!这次数据中记录的的trx_id=5 小于min_trx_id,所以数据所在的最新版本,是可见的,所以,这次得到接结果是 name=yes!
凯歌: 这个,就是读已提交的MVCC,可以看到一个事务中的两次查询得到了不同的结果,所以也叫不可重复读。
凯歌: 这种两次读取得到的结果不一致的现象,我们称之为幻读。
举例:可重复读隔离级别下的MVCC
凯歌:现在隔离级别是:可重复读
凯歌: 可重复读和读已提交的MVCC版本判断的过程是一模一样的,我们就不再画了,这里说下两者之间的差别。
凯歌: 差别:生成readView
隔离级别 | READVIEW生成规则 |
---|---|
读未提交 | 每次查询,都会重新生成新的readView,每次查询的readView都用新的 |
可重复读 | 第一次查询生成readView后,后面每次查询都用的第一次生成的 |
凯歌: 根据上述表格,我们可以知道,可重复读对比读已提交来说,差别就在第二次
凯歌: 读已提交,两次查询,四个指标
读已提交,第一次查询
指标 | 值 | 原因 |
---|---|---|
creator_id | 0 | 因为一个事务,只有当有修改操作的时候,才会被分配事务ID |
m_ids | [5,6] | 这两个事务都未提交,是活跃的 |
min_trx_id | 5 | |
max_trx_id | 7 | 因为当最大的事务ID是6,新来的时候,会递增,也就是7 |
读已提价,第二次查询
指标 | 值 | 原因 |
---|---|---|
creator_id | 0 | 没有修改操作,所以还是0 |
m_ids | [6] | 此时5已经提交了,所以只剩6了 |
min_trx_id | 6 | |
max_trx_id | 7 | 因为没有新的事物进来,所以还是7 |
凯歌: 可重复读,两次次查询,四个指标
可重复读,两次查询使用的都是第一次生成的,也就是下表
指标 | 值 | 原因 |
---|---|---|
creator_id | 0 | 因为一个事务,只有当有修改操作的时候,才会被分配事务ID |
m_ids | [5,6] | 这两个事务都未提交,是活跃的 |
min_trx_id | 5 | |
max_trx_id | 7 | 因为当最大的事务ID是6,新来的时候,会递增,也就是7 |
凯歌: 根据上述的分析过程,我们可以得到,可重复读,两次得到的查询结果都是一样的,均是:name=凯歌
话题:可重复读可以完全避免幻读的产生吗?
大佬: 哦哦哦,我明白了,读已提交,会产生幻读,而可重复读,不会产生幻读,所以我们应该用可重复读。
凯歌: 额,停,你说的有点问题,可重复读,其实并不能完全避免幻读。
大佬: 啊?我每次读都是第一个readView咋还会幻读?
凯歌: 唉,这个就得给你再展开说下了。
凯歌: 可重复读,其实分为两种实现,第一种是我们上面说的那种,也成快照读,每次都是读第一次的readview,另外一种,是当前读,就是我们读取的时候,要保证数据是最新的。
话题:当前读是什么?
凯歌: 快照读,我们上面说的很清楚了,下面我讲下当前读。
**凯歌:**在可重复读的隔离环境下,有时候,我们也需要保证我们的数据是最新的,需要保证实时更新,比如,当我们要更新一条数据的时候,需要先去查询一下数据存在不存在,对不对?如果数据都不存在了,我们总不能也还是事务执行成功吧。
凯歌: 所以,当我们在执行一些需要确保数据一致性的操作的时候,就不能使用快照读,需要使用当前读。
一些使用当前读的场景
select ..... for update
select .... lock in share mode
插入、更新、删除操作
其他需要确保数据一致性的操作,例如:create table ....like
大佬: 那当前读,是怎么保证数据一致的啊?加锁?
凯歌: 对的,当前读,会通过判断操作性质,给读取的数据加不同的锁,来保证数据的一致性,锁的一些例子,如下图:
锁类型 | 适用操作 | 解释 |
---|---|---|
共享锁(S锁) | select .... lock in share mode | 允许多个事务读取同一行,但阻止其他事物,对其进行修改 |
排他锁(X锁) | select ... for update | 阻止其他事务读或者修改本行 |
间隙锁(Gap Lock) | select ... where column > value | 锁定锁记录之间的间隙,而不是具体的行,防止其他事务,在这些间隙中插入新的记录 |
Next-key | select ... where column > value | 行锁和间隙锁的结合体,不仅锁行记录,还锁间隙,可以防止其他事务,在该行之前或之后的间隙中插入新记录 |
等等 |
凯歌: 说了这么多,我们再举个例子,演示下,可重复读隔离级别下出现幻读的现象
凯歌: 由上图我们可以看到,事务A,有两条SQL
隔离级别:可重复读
事务A:
第一条SQL:InnoDB判断操作类型,执行快照读
第一条SQL执行完毕,得到 1条数据,id=3
事务B:
执行insert语句,并提交事务
事务A:
第二条SQL:InnoDB判断操作类型,执行当前读
第二条SQL执行完毕,得到 2条数据,id=3和id=4
凯歌: 根据上述描述,我们可以看到,在可重复读的隔离级别下,依旧产生了幻读。
大佬: 那这我咋避免啊?
凯歌: 我们可以在事务A,开始执行的时候,直接执行 select ...for update。这样在事务A,开始的时候,就加了锁,其他事务也就新增不了,避免了幻读的产生。
大佬: 哦哦哦,明白了,但是感觉还有些细节,没太懂,你再给说下吧。
凯歌: 行,不过今天不行了,今天没时间了,在后续的文章中,我们再补充吧。
大佬: 你又来这招。
凯歌:以上,就是我们本次关于MySQL中MVCC是什么?的全过程的讲解了,若有错误,请帮忙指出,一定修改。