Mysql之MVCC理解

前置知识

脏读、幻读、不可重复读

如现在有这样一个表

CREATE TABLE tableA (
        id INT,
        name VARCHAR(100),
        age int,
        PRIMARY KEY (id)
    ) Engine=InnoDB CHARSET=utf8;

表中插入了数据

INSERT INTO tableA (id, name, age) VALUES 
(1, '张三', 20), 
(2, '李四', 26),
(3, '王五', 18);

脏读

一个事务读取到了另一个事务未提交的数据。

事务1事务2
begin;begin;
select * from tableA where id = 1 结果为 1, 张三,20
update tableA set name = ‘张三三’ where id = 1
select * from tableA where id = 1 结果为 1,张三三,20
rollback;

不可重复读

一个事务读取到了另一个事务修改提交后的数据,即一个事务中的两次读数据之间,由于第二个事务的修改,那么第一个事务两次读到的数据可能是不同的~

事务1事务2
begin;begin;
select * from tableA where id = 1 结果为 1, 张三,20
update tableA set name = ‘张三三’ where id = 1
commit
select * from tableA where id = 1 结果为 1,张三三,20

幻读

一个事务先查询出一些记录,之后另一个事务又向表中插入了一些数据,原来的事务在按照原条件查询时,把另一个事务插入的数据也查询出来了。

事务1事务2
begin;begin;
select * from tableA where id > 1 结果为 2, 李四,26 与 3,王五,18
insert into tableA values(4, ‘赵六’, 30)
commit;
select * from tableA where id = 1 结果为 2, 李四,26 ; 3,王五,18; 4,赵六,30

影响:脏读 > 不可重复读 > 幻读

Mysql的隔离级别

  • READ UNCOMMITED,读未提交。会导致脏读、幻读以及不可重复读;
  • READ COMMITED,读已提交。解决了脏读,但是会导致幻读以及不可重复读;
  • REPEATABLE READ,可重复读。解决了脏读以及不可重复读,会导致幻读;
  • SERIALIZABLE,序列化。隔离级别最高,解决了脏读、幻读以及不可重复读。

隔离水平高低排序:串行化 > 可重复读 > 读已提交 > 读未提交

MySQL默认隔离级别是可重复读~

而在Mysql中,是如何实现隔离级别的呢?Mysql中通过加锁的方式实现隔离级别,但加锁自然会带来性能上的损失,那么,如何解决性能问题呢?

MVCC多版本并发控制。

MVCC原理

概念

Multi-Version Concurrency Control 多版本并发控制,MVCC 是一种并发控制的方法,一般在数据库管理系统中,实现对数据库的并发访问;在编程语言中实现事务内存。
简单理解,如果读取的一行数据正在执行 DELETE 或者 UPDATE操作的话,这时读取操作不会因此等待行上锁的释放,相反的,InnoDB存储引擎会去读取行的一个快照数据,而所谓快照数据,是指该行的之前版本的数据,通过undo log日志实现。
因此可以看出,MVCC极大的提高了数据库的并发性。

MVCC只对READ COMMITED 与 REPEATABLE READ隔离级别实现~

版本链

首先我们要清楚,mysql中的表除了会记录我们自定义的列数据以外,Mysql还会为每个记录默认的添加一些列(隐藏列),具体的列:

列名是否必须占用空间描述
DB_ROW_ID6字节行ID,唯一标识一行记录
DB_TRX_ID6字节事务ID
DB_ROLL_PTR7字节回滚指针
  • DB_ROW_ID,唯一标识一行记录的作用,具体生成策略:

InnoDB存储引擎对主键的生成策略:优先使用用户自定义主键作为主键,如果用户没有自定义主键的话,则选取一个 唯一Unique键作为主键,如果表中连Unique键都没有的话,则InnoDB会为表默认添加一个名为 row_id的隐藏列作为主键。

  • DB_TRX_ID:事务ID,每当一个事务对某条记录进行改动时,都会把该事务的事务ID赋给 DB_TRX_ID。即记录改动后,才会生成一个事务id;
  • DB_ROLL_PTR:每次对某条记录进行改动时,都会吧旧的版本写入到 undo log日志中,然后DB_ROLL_PTR相当于一个指针,指向上一个版本的记录。

InnoDB存储引擎会为每条记录都添加 DB_TRX_ID 与 DB_ROLL_PTR这两列,而DB_ROW_ID则根据上述策略进行生成

如上表中最开始有这样一条数据

INSERT INTO tableA (id, name, age) VALUES (1, 'zs', 20);

且假设插入这条记录的事务ID (DB_TRX_ID) 为1的话,则该记录的版本链示意图
在这里插入图片描述
如果后续另一个事务2,修改了这条数据,如SQL

update tableA set name = 'zsss' where id = 1;

在这里插入图片描述
事务3修改这条记录,SQL

update tableA set age = 18 where id = 1;

在这里插入图片描述

从上可以看出来,每次对记录更新后,都会将旧值放到一条undo log日志中,即该记录的历史版本,随着更新次数增多,所有的版本都会被 DB_ROLL_PTR连接成为一个链表,称之为 版本链。版本链的头结点就是最新的数据,越往下越是老的数据~~

ReadView

通过 ReadView来判断版本链中的那个版本是当前事务可见的。
对于READ COMMITED 与 REPEATABLE READ来说,不同的隔离级别生成ReadView的时机也不同。

  • Read Commited:每次进行快照读的时候都生成一份新的 ReadView;
  • Repeatable Read:仅在快照读事务开始的时候生成一个ReadView,此后在同一个事务中的快照读都是用第一次生成的ReadView。

小贴士
快照读:读取历史版本的数据,平常最常用的select语句即为快照读;

select  ....

当前读:通过加锁的方式保证查询到的是最新的数据;

select  ... for update; #对读取的行记录加上一个X锁;
select ... lock in share mode; #对读取的行记录加上一个S锁;
insert ....; 
update ...;
delete ....;

ReadView到底是什么呢?
ReadView中包含了四个重要的内容:

  • m_ids:表示在生成ReadView时当前系统中活跃的读写事务事务id 列表。即当前系统中已开启事务但是还未提交的事务ID列表~~

  • min_trx_id:表示在生成ReadView时 m_ids中最小的事务ID

  • max_trx_id:表示在生成ReadView时系统应该分配给下一个事务的ID。即当前系统中最大事务ID的下一个值~

  • creator_trx_id:生成该ReadView的事务ID。

    小贴士:
    只有在对表中的记录做改动时(执行INSERT、UPDATE等操作时)才会分配一个事务ID,在一个只读事务中的事务id值默认为0~

比较规则

因此,在进行快照读的时候,通过 ReadView与版本链进行对比即可判断版本链中的某个版本是否可见:

  • 如果被访问版本的 DB_TRX_ID 事务id 与ReadView中的creator_trx_id相等的话,则意味着是当前事务在访问它自己修改过的记录,所以该版本是可见的;
  • 如果被访问版本的DB_TRX_ID 事务id 小于 ReadView中的 min_trx_id,表明生成该版本的事务在当前事务生成ReadView之前已经提交,所以该版本可以被当前事务访问
  • 如果被访问版本的 DB_TRX_ID 事务id 大于ReadView中的 max_trx_id值 的话,表明生成该版本的事务在当前事务生成ReadView后再开启,所以该版本不可以被当前事务访问
  • 如果被访问版本的DB_TRX_ID 属性值在 ReadView 的 min_trx_id 和 max_trx_id 之间,那就需要判断一下 DB_TRX_ID 属性值是不是在 m_ids 列表中,如果在,说明创建 ReadView 时生成该版本的事务还是活跃未提交的,该版本不可以被访问;如果不在,说明创建 ReadView 时生成该版本的事务已经被提交,该版本可以被访问。

简单图示
在这里插入图片描述

如果某个版本的数据对当前事务不可见的话,那就顺着版本链找到下一个版本的数据,继续按照上边的步骤判断可见性,依此类推,直到版本链中的最后一个版本。如果最后一个版本也不可见的话,那么就意味着该条记录对该事务完全不可见,查询结果就不包含该记录。

看到这可能会有点迷,接着往下看看示例吧~~

Read Commited隔离级别下MVCC实现

Read Commited隔离级别下 每次进行快照查询时都会生成一个新的ReadView

依然使用最上面的示例表~同样假设表中目前有这样一条记录,该记录由事务1创建

INSERT INTO tableA (id, name, age) VALUES (1, 'zs', 20)

目前该条记录的版本链
在这里插入图片描述
两个事务如下表执行:

事务2事务3
begin;begin;
① select * from tableA where id = 1;
update tableA set name = ‘zss’ where id = 1
②select * from tableA where id = 1;
commit;
③select * from tableA where id = 1;

①:事务2执行第一个select语句时,生成一个ReadView,因为该事务未对表中的记录做更改,且事务2与3均未提交事务,所以 creator_trx_id = 0,m_ids = [2, 3], min_trx_id = 2, max_trx_id = 4
通过该ReadView与上述的版本链进行对比,DB_TRX_ID = 1 小于 ReadView中的min_trx_id = 2,所以该查询语句能够读取到这条记录,因此返回的结果为 (1,‘zs’,20)

②:事务2执行第二个select语句时,新生成一个ReadView,creator_trx_id = 0,m_ids = [2,3],min_trx_id = 2, max_trx_id = 4
但是事务3修改了数据,因此记录版本链为
在这里插入图片描述

通过ReadView与版本链进行对比,DB_TRX_ID = 3 在 ReadView的min_trx_id = 2 与 max_trx_id = 4之间,并且DB_TRX_ID = 3在m_ids数组中,该版本的事务还未提交,因此读取不到该版本的数据~

则从版本链中判断下一个版本的数据,DB_TRX_ID = 1 小于 ReadView中的 min_trx_id = 2,该版本的事务已经提交,因此可以读取到版本的数据;
所以该查询语句最终返回的结果为 (1,‘zs’,20).

③事务2执行第三个select语句时,因为事务3已经提交了,因此新生成的ReadView:creator_trx_id = 0,m_ids = [2],min_trx_id = 2,max_trx_id = 4
该条记录没有变动,因此记录版本链和上面相同
在这里插入图片描述

对比版本链与 ReadView,首先 DB_TRX_ID = 3 在 ReadView中 min_trx_id = 2 与max_trx_id = 4中间,但是 DB_TRX_ID =3不在 m_ids数组当中,代表该版本的事务已经提交了,因此能够访问到该版本的数据。所以该查询语句的返回结果为 (1,‘zss’,20)

这也就是 读已提交Read Commited隔离级别下产生 不可重复读问题的原因~

Repeatable Read隔离级别下MVCC的实现

Repeatable Read隔离级别下只会在第一次快照读的时候生成一份ReadView~

仍然是上面的表与同样的两个事务执行;

①事务2执行第一个select语句时,生成的ReadView,因为该事务未对表中的记录做更改,所以 creator_trx_id = 0,m_ids = [2, 3], min_trx_id = 2, max_trx_id = 4
目前的版本链
在这里插入图片描述
对比ReadView与版本链,DB_TRX_ID小于 ReadView中的 min_trx_id = 2,因此返回的数据为 (1,‘zs’,20)

②事务2执行第二个select语句时,不会再新生成ReadView,可重复读隔离级别下只会在第一次快照读的时候生成ReadView,因此ReadView为creator_trx_id = 0,m_ids = [2, 3], min_trx_id = 2, max_trx_id = 4
此时的版本链为:
在这里插入图片描述
对比ReadView与版本链,DB_TRX_ID=3在ReadView的 min_trx_id = 2 与max_trx_id=4之间,且DB_TRX_ID=3在m_ids数组当中,说明该版本的事务还未提交,因此读取不到该版本的数据,转而对比版本链中的下一个版本;
下一个版本中的DB_TRX_ID=1,小于ReadView中的 min_trx_id=2,因此该版本的数据可以读取到,因此最终返回的数据为 (1,‘zs’,20)

③事务2执行第三个select语句时,因为ReadView和版本链均未变化,因此和第二个select语句执行流程相同,返回的结果也是相同的。

从上流程也可以看出,可重复读Repeatable Read 解决了不可重复读的问题~~~而至于幻读呢,可重复读隔离级别下其实是通过 next-key lock解决的,此处就不过多介绍啦,需要的小伙伴可以下来自行了解哈~

以上就是我对MVVC的浅浅理解啦~

参考:《MySQL是怎样运行的:从根儿上理解MySQL》

  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
MVCC(Multi-Version Concurrency Control)是 MySQL 数据库中的一种并发控制机制。它用于管理并发事务对数据的访问,以保证数据的一致性和隔离性。MVCC 的核心思想是在事务执行期间,数据库会为每个事务创建一个可见的数据版本,使得事务之间的读写操作可以并行进行,而不会相互干扰。 具体来说,MVCC 通过在每行数据中添加额外的版本信息来实现。当一个事务开始时,它会获取一个全局的事务 ID,每个数据行也会记录自己的创建版本号和删除版本号。在事务执行期间,每个事务只能看到比自己开始时间早创建的、未被删除的数据行。 对于读操作,MVCC 会根据事务的开始时间和数据行的版本号来决定是否可见。如果数据行的创建版本号早于事务开始时间,且删除版本号为空或晚于事务开始时间,则该数据行可见。 对于写操作,MVCC 会创建一个新的数据版本,并将新版本的创建版本号设置为当前事务的开始时间。同时,将原数据行的删除版本号设置为当前事务的开始时间。 MVCC 的优点是可以提高数据库的并发性能,减少锁争用和冲突。它允许读操作之间的并发执行,不会阻塞其他事务的读操作。同时,写操作也可以与读操作并发执行,只需要在提交时进行冲突检测和处理。 然而,MVCC 也存在一些限制。由于需要为每个数据行记录版本信息,会增加存储空间的消耗。同时,长事务的存在也可能导致版本链的过长,影响性能。因此,在使用 MVCC 时需要注意合理设置事务的隔离级别和管理事务的生命周期。
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值