什么是MVCC

什么是MVCC

什么是MVCC?

MVCC,全称为Multi-Version Concurrency Control,即多版本并发控制。主要的目的是为了提高数据库的性能,更好地处理读写冲突,即使出现了读写冲突也可以通过不加锁的方式来解决。

多版本是指数据库中的数据存在多个版本,

当前读和快照读

在介绍MVCC前,需要先了解当前读和快照读两个概念。

当前读

在当前读中,事务只能读取已经提交的数据。这意味着如果一个事务正在对数据行做出修改,而另一个事务也要读取这一行,后者会等待前者完成修改。因此,当前读会提供更加一致的视图。

MySQL中提供了两种当前读的实现:

一致性读

在可重复读的隔离等级下,MySQL使用一致性读来实现当前读。在事务开始时,会创建一个一致性视图,这个视图是事务开始时刻数据库的快照,在事务执行期间,无论其他事务对数据做了何种修改,当前事务都只会从这个一致性视图中读取数据,保证了读取的一致性。

锁定读

当使用锁定读时,会对数据上一把共享锁或者排他锁,从而保证了数据的一致性。

锁定读适用于需要严格控制并发的场景,而且加锁带来的性能开销较大。

当前读会使用到的语法

select ... lock in share mode

select ... for update

update

delete

insert

快照读

快照读允许事务读取一个事务开始时的数据快照,而不受其他事务的影响。这意味着事务在其执行期间看到的数据保持一致,即使其他事务在此期间对数据进行了修改。这种方法通常通过数据库中的一种快照机制来实现,因此不会阻塞其他事务对数据的修改。通常的select操作都是快照读。

MVCC初步

MySQL中的每行数据,除了用户定义的外,还会有一些自带的隐藏字段。

字段

含义

DB_ROW_ID

自增id(隐藏主键),用于唯一地标识一行数据。

DB_TRX_ID

当前所属的事务id,每个事务在数据库中都有一个唯一的事务id,通过这个字段,可以追踪行数据和事务的所属关系。

DB_ROLL_PTR

保存回滚指针,指向了回滚事务的Undo日志记录。

Undo日志是能实现MVCC的关键所在,其主要有两个作用:

事务回滚,当事务回滚时,会用Undo日志中的旧值还原事务的开始状态。

MVCC实现,通过Undo日志,可以为每个事务都提供一个独立的事务视图。

版本链

在MVCC中,对于每次更新操作,旧值会被保存到一条undo日志中,看作是该记录的旧版本。随着更新次数的增加,所有的版本都会通过roll_pointer属性连接成一个链表,称之为版本链。

版本链的头节点代表当前记录的最新值。此外,每个版本还包含生成该版本的事务ID。

一致性视图 ReadView

用来判断版本链中哪个版本对当前事务可见。事务在进行快照读操作时生成的读视图,在该事务执行快照读的时刻,会生成一个数据库系统当前的快照,记录并维护系统当前活跃事务的id(每个事务开启时,都会被分配一个id,这个id是递增的)。另外,MVCC只在读提交和可重复读隔离等级下才有效。

ReadView会维护以下几个字段

字段

说明

m_ids

视图创建时,会将当前还未提交的事务id记录下来,后续即使它们修改了记录行的数据,对于当前事务也是不可见的。m_ids不包括当前自身的事务和已提交的事务。

m_creator_trx_id

当前事务自身的事务id

m_low_limit_id

当前出现过的最大的事务id+1,即下一个将被分配的事务id。大于等于这个id的数据版本均不可见。

m_up_limit_id

活跃事务列表m_ids中最小的事务id,如果m_ids为空,则m_low_limit_id为m_up_limit_id,小于这个id的版本均可见。

通过这几个字段和行数据的隐藏字段进行比对就可完成版本控制:

如果被访问的版本的DB_TRX_ID比视图中的m_creator_trx_id相同,说明正在访问自己修改的记录,因此数据的该版本可以被该事务访问。

如果DB_TRX_ID小于视图的m_up_limit_id,说明生成该版本数据的事务已经在之前提交了,因此该版本的数据也可以被访问到。

如果DB_TRX_ID大于或等于m_low_limit_id,说明生成该版本的事务在当前事务之后才提交的,因此该版本不能被当前事务访问到。

如果DB_TRX_ID位于m_up_limit_id和m_low_limit_id之间,则需要进一步检查DB_TRX_ID是否在m_ids列表中。如果在列表中,说明在创建视图时生成该版本的事务仍未提交,则不能访问;如果不在,则说明已经提交过了,那么就可以访问。

一致性视图遵循一个可见性原则:将要修改的数据的DB_TRX_ID和当前其他活跃的事务的id进行比对,如果DB_TRX_ID不可以被访问,那么就顺着DB_ROLL_PTR回滚指针去取出Undo日志中的DB_TRX_ID再进行对比,直到找到满足特定条件的DB_TRX_ID,此时DB_TRX_ID对应的记录就是当前事务可以看到的最新版本。

读提交隔离等级下的视图

读提交和可重复读下的视图有所差别。

读提交的每一次读取数据都会生成一个当前的视图;而可重复读只会在事务开启时生成一个视图,后面的读取都会在这个视图上操作。这就导致了两个隔离等级下看到的数据有所差别。

举例说明,当前有这样一条数据

DB_ROW_ID

DB_TRX_ID

DB_ROLL_PTR

id

name

1

1

1

小明

现在有三个事务并发执行。

事务A(事务id:100)

事务B(事务id:200)

事务C(事务id:300)

T1

begin

T2

begin

begin

T3

update user set name = "小王" where id = 1

T4

update user set name = "小红" where id = 1

select * from user where id = 1

T5

commit

update user set name = "小军" where id = 1

T6

update user set name = "小白" where id = 1

select * from user where id = 1

T7

commit

T8

select * from user where id = 1

T9

commit

在T4时刻,事务A和事务B都没有提交,所以是活跃的事务id,即m_ids = { 100, 200 },四个字段的值分别为

字段

m_ids

m_creator_trx_id

300

m_low_limit_id

400

m_up_limit_id

100

T4时刻的版本链是这样的:

那么,在T4时刻事务C能看到的数据应该是name = “小明”。

同理,在T6时刻,事务C看到的数据是name = “小红”,因为此时事务A已经提交了,而在提交读隔离等级下,每次读取都会创建一个新的视图,所以可以看到事务A提交的记录。

此时的视图四个字段值是

字段

m_ids

m_creator_trx_id

300

m_low_limit_id

400

m_up_limit_id

200

那么,在T8时刻,事务C看到的数据就是name = “小白”.

此时的视图四个字段值是

字段

m_ids

m_creator_trx_id

300

m_low_limit_id

400

m_up_limit_id

400

可重复读隔离等级下的视图

同样的事务执行流程,正如前面说的,可重复读隔离等级下每次都会使用事务开启时的视图,m_ids始终都是{ 100, 200 },那么事务C每次读出来的数据都是name = “小明”。

养生小贴士

蚂蚁短租怎么样 蚂蚁短租怎么操作?
💡 小知识

蚂蚁短租怎么样 蚂蚁短租怎么操作?

📅 07-02 👍 553
王者荣耀:新赛季官方上架“车队”功能,周末车队开黑可畅享皮肤
剧场版 Free!-Timeless Medley- 约束 (劇場版 Free!-Timeless Medley- 約束)