其实自己之前对MVCC知之甚少,总觉得是一块很难啃的骨头,有点内惧,但当你真的掌握之后,就发现打开了一扇大门,豁然开朗,鸟语花香~~
MVCC(multiversion Concurrency Control)多版本并发控制。
见名知意,MVCC就是通过多个数据行的版本对实现数据库的并发控制的。所谓并发,就是在某些隔离级别下,可以看到被别人修改前或后的数据,这样在查询的时候就不用等别人更新完我才能看到或操作了。
MVCC在Mysql InnoDB中的实现主要是为了提供数据库的并发性能,使得即使有读写冲突,也能做到不加锁。其思想其实是乐观锁的一种实现方式。
而MVCC的工作就是:生成一个ReadView
,通过ReadView
找到符合条件的记录版本(历史版本由undo log构建)。
查询语句只能读到生成ReadView
之前已提交事务所做的修改,在生成ReadView
之前未提交的事务或者之后才开启的事务所做的修改是看不到的。
写操作针对的是最新版本,读记录和历史版本以及改动记录的最新版本并不冲突,也就是采用MVCC时,读写并不冲突。
这里记住10字真言:读为快照读,写为当前读。
所谓快照读就是读的是快照数据,快照数据的出现也是为了避免加锁从而提高并发。
既然是快照,那么快照读可能读到的并不一定是数据的最新版本,而有可能是之前的历史版本。快照读的前提是隔离级别不能是串行级别,串行级别下的快照读会退化成当前读。
而当前读就是读的最新的数据,读的时候要保证其他的并发事务不能修改当前的记录,会对读的数据进行加锁。
Read Uncommitted
(读未提交):可以读到未提交的数据,即当前读,直接读最新的即可。
Serializable Read
(串行读):使用加锁的方式来访问,也是最新读,即当前读。
Read Committed
(读已提交)和Repeatable Read
(可重复读)都是读到已提交的事务修改过的数据,如果一个事务修改了记录但未提交,就不能读取到这条最新记录,所以核心问题是判断版本链中哪个版本对当前事务可见,而这就是ReadView
要解决的问题。
所以,这四种隔离级别中,只有Read Committed
(读已提交)和Repeatable Read
(可重复读)是MVCC要解决的并发的读写冲突的问题。
其中,Read Committed
(读已提交)是每次查询的时候都获取一次最新的ReadView
。
Repeatable Read
(可重复读)是只在第一次查询时获取ReadView
,后面再查询都复用这个ReadView
。之所以只在第一次查询获取ReadView
,是为了避免不可重复读,而不可重复读就是两次读取的结果不一致,为了解决这个问题,那就索性两次读都读一个数据,这样就避免了两次读的数据不一致。
这里我还是想补充一下4种隔离级别存在的三种并发问题,以及解决和未解决的并发问题:
隔离级别 | 存在的并发问题 | 已解决的并发问题 |
---|---|---|
Read Uncommitted (读未提交) |
脏读、不可重复读,幻读 | 脏写 |
Read Committed (读已提交) |
不可重复读、幻读 | 脏写、脏读 |
Repeatable Read (可重复读) |
幻读 | 脏写、脏读、不可重复读 |
Serializable Read (串行读) |
- | 脏写、脏读、不可重复读、幻读 |
其实通过MVCC机制还解决了可重复读的幻读问题,这里我就不想细说了。
说了这么多,MVCC究竟是怎么实现的呢?前面也提到过:
MVCC的工作就是:生成一个ReadView
,通过ReadView
找到符合条件的记录版本(历史版本由undo log构建)。
总结下来MVCC的实现就是依靠三个关键字:隐藏字段,undolog版本链和ReadView来实现的。
隐藏字段主要包括:trx_id
和 roll_pointer
。
trx_id
: 每次一个事务对某条数据记录改动时,都会把事务ID赋值给trx_id
隐藏列。
roll_pointer
:每次对某条数据记录改动时,都会把旧的版本写入到undo日志中,然后这个隐藏列就相当于一个指针,可以通过它来找到记录修改前的信息。
下面举个:
当前有一张person表,有一条:
id=1,name=“韩立”,class=一班 的数据,这条数据的trx_id=8,下面对这条数据进行如下的修改:
发生时间顺序 | 事务10 | 事务20 |
---|---|---|
1 | BEGIN; | |
2 | BEGIN; | |
3 | UPDATE student SET name=“韩跑跑” WHERE id=1; | |
4 | UPDATE student SET name=“厉飞雨” WHERE id=1; | |
5 | COMMIT; | |
6 | UPDATE student SET name=“张铁” WHERE id=1; | |
7 | UPDATE student SET name=“曲魂” WHERE id=1; | |
8 | COMMIT; |
以上的操作流程对name 的每次修改都会记录一条日志,每条日志都有一个roll_pointer
属性,将这些undo日志连起来就成为了个链表:
id=1的这条记录在2个事务中做了4次变更,每次变更就会把前一个旧值放到undo日志中,随着更新的次数增多,所有的更新记录即版本就被roll_pointer连成一个链表,我们称之为版本链,链头就是当前记录的最新值,每个版本中还包括生成该版本对应的trx_id(事务ID)。
前面反复提到过ReadView
是个什么玩意呢?
ReadView
就是事务在使用MVCC机制进行快照读操作时产生的读视图。包括creator_trx_id,trx_ids,up_limit_id,low_limit_id
。
creator_trx_id
:创建这个ReadView的事务ID。(只有在对表中的记录做INSERT,DELETE,UPDATE时才会为事务分配事务ID,只有读事务的话事务ID默认为0。)trx_ids
:表示在生成ReadView时当前系统中活跃的读事务的事务ID列表。up_limit_id
:活跃的事务中最小的事务ID。low_limit_id
:表示生成ReadView时系统中应该分配给下一个事务的ID值。low_limit_id是系统中最大的事务ID值,并不仅限于活跃的事务ID。针对low_limit_id的说明:low_limit_id并不是trx_ids中最大值。假设现有id为1,2,3都未提交的事务,只有3提交了,提交之后,就剩下1和2是活跃的,3提交后有一个新的读事务生成了ReadView时,则trx_ids包括1和2,up_limit_id=1,low_limit_id=4。
有了ReadView
,会按照下面的规则,遍历版本链中的版本是否可见。
trx_id
属性值与ReadView中的creator_trx_id
值相同(trx_id
=creator_trx_id
),则说明当前事务正在访问的就是自己修改过的记录,该版本可以被当前事务访问。trx_id
属性值小于ReadView中的up_limit_id
值(trx_id
<up_limit_id
),则说明生成该版本的事务在当前事务生成ReadView之前已经提交,该版本可以被当前事务访问。trx_id
属性值大于或ReadView中的low_limit_id
值(trx_id
>=low_limit_id
),则说明生成该版本的事务在当前事务生成ReadView之后才开启,该版本不可以被当前事务访问。trx_id
属性值介于ReadView中的up_limit_id
和low_limit_id
之间(up_limit_id
<trx_id
<low_limit_id
),则需要判断一下trx_id值是否在trx_ids
列表中:
本文的知识点都是因为看了B站尚硅谷康师傅的Mysql课程,康师傅出品必属精品,等后面有时间我还会再刷两遍,真是受益匪浅,强烈安利众仙家~~
-----------------你知道的越多,不知道的越多--------------