MVCC(Mutil-Version Concurrency Control)

本文解析了MVCC(多版本并发控制)的基本概念与实现原理,详细介绍了脏读、不可重复读和幻读等并发问题,并探讨了如何利用MVCC解决这些问题。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

MVCC(Mutil-Version Concurrency Control)

多版本并发控制 - 省流量的说法就是“乐观锁”

数据库并发问题

首先我们要搞清楚相关的并发问题,脏读、不可重复读、幻读
  • 脏读

    事务A修改了数据a(3-> 5),还没有提交事务;这个时候,事务B读取数据a,读到的a是5;然后事务A发生回滚,数据a还是3,这称为脏读。

  • 不可重复读

    事务B读取数据a(3)。事务A修改了数据a(3-> 5),事务A提交了事务。事务B读取数据a(5),在同一个事务里面,两次读到的数据不一致,这称为不可重复读。

    强调的是updatedelete

  • 幻读

    事务A,先发送一条 SQL 语句,要查询一批数据出来,如SELECT * FROM table WHERE id > 10。查询出来10条数据。接着事务B往表里插入几条数据,事务B提交,此时多了几行数据。接着事务A此时第二次查询,再次按照之前的一模一样的条件执行SELECT * FROM table WHERE id > 10。由于其他事务插入了几条数据,导致这次它查询出来了12条数据。这称之为幻读。

    强调的是insert

如何解决这些问题呢?

第一个想到的可能就是加锁,加锁将读写操作串行化,但是这样会带来性能问题。有没有其他办法呢? 乐观锁吧

MVCC

MVCC,全称Multi-Version Concurrency Control,即多版本并发控制

引入MVCC之后,只有写写之间相互阻塞,其他读读,读写,写读都可以并行,这样大幅度提高了InnoDB的并发度。

三个隐式字段

  1. row_id :隐藏的行ID,用来生成默认的聚集索引。如果创建数据表时没指定聚集索引,这时 InnoDB 就会用这个隐藏ID来创建聚集索引。采用聚集索引的方式可以提升数据的查找效率。

  2. trx_id: 操作这个数据事务ID,也就是最后一个对数据插入或者更新的事务ID。这个事务ID是自增的,通过事务ID大小,可以判断事务的时间顺序。

  3. roll_ptr:回滚指针,指向这个记录的UndoLog信息。

实际还有一个删除flag隐藏字段, 记录被更新或删除并不代表真的删除,而是删除flag改变了。

UndoLog

记录各个事务回滚的快照数据,通过roll_ptr来寻址,在判断不需要以后日志会被删除。通过UndoLog可以恢复到相应的版本。

ReadView

  • 快照读和当前读

    快照读是读取的一份快照中的数据。与之对应的当前读,读取的当前最新的数据。

  • 当前读是通过 next-key 锁(行记录锁+间隙锁)来是实现的。

    行锁(Record Lock):锁直接加在索引记录上面。
    间隙锁(Gap Lock):是 Innodb 为了解决幻读问题时引入的锁机制,所以只有在 Read Repeatable 、Serializable 隔离级别才有。
    Next-Key Lock:Record Lock + Gap Lock,锁定一个范围并且锁定记录本身 。

在事务执行的快照读的那一刻,会生成数据库系统当前的一个快照,记录并维护系统当前活跃事务的ID(当每个事务开启时,都会被分配一个ID, 这个ID是递增的,所以最新的事务,ID值越大)。

    ReadView 保存了当前事务开启时所有活跃的事务列表。也可以理解为:ReadView保存了不应该让这个事务看到的其他事务ID列表。ReadView主要是用来做可见性判断的, 即当我们某个事务执行快照读的时候,对该记录创建一个ReadView读视图,把它比作条件用来判断当前事务能够看到哪个版本的数据,既可能是当前最新的数据,也有可能是该行记录的UndoLog里面的某个版本的数据。
    主要是将要被修改的数据的最新记录中的trx_id(即当前事务ID)取出来,与系统当前其他活跃事务的ID去对比(由ReadView维护),如果trx_id跟ReadView的属性做了某些比较,不符合可见性,那就通过roll_ptr回滚指针去取出UndoLog中的trx_id再比较,即遍历链表的trx_id(从链首到链尾,即从最近的一次修改查起),直到找到满足特定条件的trx_id, 那么这个trx_id所在的旧记录就是当前事务能看见的最新老版本。

trx_ids 系统当前正在活跃的事务ID集合。   
low_limit_id ,活跃事务的最大的事务ID。   
up_limit_id 活跃的事务中最小的事务ID。  
creator_trx_id,创建这个ReadView的事务ID。  

通过creator_trx_id和其他三个参数的比较来判断数据的可见性

    如果 trx_id < 活跃的最小事务ID(up_limit_id),也就是说这个行记录在这些活跃的事务创建前就已经提交了,那么这个行记录对当前事务是可见的。
    如果trx_id > 活跃的最大事务ID(low_limit_id),这个说明行记录在这些活跃的事务之后才创建,说明这个行记录对当前事务是不可见的。
    如果 up_limit_id < trx_id <low_limit_id,说明该记录需要在 trx_ids 集合中,可能还处于活跃状态,因此我们需要在 trx_ids 集合中遍历 ,如果trx_id 存在于 trx_ids 集合中,证明这个事务 trx_id 还处于活跃状态,不可见,否则 ,trx_id 不存在于 trx_ids 集合中,说明事务trx_id 已经提交了,这行记录是可见的。

MySQL数据库innodb引擎隔离级别的不同是通过改变ReadView的生成时机实现的

读提交RC隔离级别下的事务在每次查询的开始都会生成一个独立的ReadView,而可重复读RR隔离级别则在第一次读的时候生成一个ReadView,之后的读都复用之前的ReadView。

对于正常的select查询,innodb实际上进行的是快照读,即通过判断读取到的行的 trx_id 与 roll_ptr字段指向的UndoLog回溯到事务开启前或当前事务最后一次更新的数据版本,从而在这样的场景下避免了可重复读与幻读的问题。
针对已存在的数据,更新操作虽然是进行当前读,但 insert 与 update 操作后,该行的最新修改事务ID 为当前事务ID,因此读到的值仍然是当前事务所修改的数据,不会产生不可重复读的问题。
但如果当前事务更新到了其他事务新插入并提交了的数据,这就会造成该行数据的 trx_id 被更新为当前事务 ID,此后即便进行快照读,依然会查出该行数据,产生幻读。
MySQL中默认的隔离级别是可重复读RR,可解决脏读和不可重复读的问题。但是不能解决幻读的问题。

1.正确的理解MySQL的MVCC及实现原理
2.事务到底是隔离的还是不隔离的?

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值