当前位置: 首页 > 科技观察

如果有人问你什么是MVCC,就把这篇文章发给他吧!

时间:2023-03-16 00:57:02 科技观察

一想到并发控制,很多人的第一反应就是加锁。确实,锁定确实是并发问题最常见的解决方案。然而,其实除了锁之外,在数据库领域,还有另一种无锁方案来实现并发控制,那就是大名鼎鼎的MVCC。MVCC是多版本并发控制的缩写。翻译过来就是多版本并发控制,也是一种并发控制方案。我们知道,在数据库中,对数据的操作主要有两种,即读和写,而在并发场景下,可能会出现以下三种情况:read-readconcurrentreadwriteconcurrentwrite-writeconcurrent我们都知道有不写就发送读写是没有问题的,写并发更多的是通过加锁来实现。那么读写并发可以通过MVCC机制来解决。本文将介绍MVCC在MySQL中的实现机制。Snapshotread和currentread如果想了解MVCC的机制,最重要的概念就是snapshotread。所谓snapshotread就是读取快照数据,也就是快照生成那一刻的数据,像我们经常使用的普通SELECT语句,就是不带锁的snapshotread。例如:SELECT*FROMxx_tableWHERE...快照读取对应的另一个概念叫做当前读取。当前读数是读取最新的数据。因此,锁定SELECT,或者增删改查数据都会执行当前读取,如:SELECT*FROMxx_tableLOCKINSHAREMODE;SELECT*FROMxx_tableFORUPDATE;INSERTINTOxx_table...DELETEFROMxx_table...UPDATExx_table...可以说快照读取是MVCC实现的基础,而当前读取是悲观锁实现的基础。那么,快照读取的快照是从哪里读取的呢?换句话说,快照存在于何处?UndoLogundo日志是Mysql中比较重要的事务日志之一。undolog顾名思义就是用来回滚的日志。在事务提交之前,MySQL会先将更新前的数据记录到undolog日志文件中。当事务回滚或者数据库崩溃时,可以使用undolog来回滚。这里所说的undolog中的“更新前的数据”就是我们前面提到的快照。所以,这也是为什么很多人说UndoLog是MVCC实现的重要手段。那么,一个记录可能有多个事务同时执行。那么,undolog就会有一条记录的多个快照。那么此时需要读取一个SELECT时,应该读取哪个snapshot呢?这需要使用其他几条信息。隐式字段其实在数据库的每一行中,除了保存一些我们自己定义的字段外,还有一些重要的隐式字段:db_row_id:隐藏主键,如果我们不为这张表创建主键,那么它使用此字段创建聚簇索引。db_trx_id:对该记录进行最新修改的事务的ID。db_roll_ptr:回滚指针,指向这条记录的前一个版本。其实就是指向UndoLog中上一版本快照的地址。因为在每条记录发生变化之前,undolog中都会存储一个快照,那么这些隐式字段也会随着记录一起保存在undolog中。这样,每个快照都有一个db_trx_id字段记录本次变化的事务ID,还有一个db_roll_ptr字段指向上一个快照的地址。(db_trx_id和db_roll_ptr是重点,后面会用到)这样就形成了一个snapshotlist:有了undolog和几个隐含字段,我们好像还是不知道应该读哪个snapshot,那怎么办该怎么办?这时候ReadView就需要ReadView登场了。ReadView主要是帮助我们解决可见性问题,即它会告诉我们在这个事务中应该看到哪个快照,不应该看到哪个快照。ReadView中有几个重要的属性:trx_ids,系统当前未提交的事务ID列表。low_limit_id,未提交事务中最大的事务ID。up_limit_id,未提交事务中最小的事务ID。creator_trx_id,创建这个ReadView的交易ID。每次我们开始一个事务,我们都会从数据库中获取一个事务ID。这个交易ID是自增的。通过ID的大小,我们可以判断交易发生的时间顺序。那么,如何判断一个事务应该看到哪些快照,哪些快照不应该看到呢?其实原理比较简单,就是交易ID大的交易应该能看到交易ID小的交易的变化结果,反之亦然!例如:如果当前有事务3要对某条记录进行快照读取,他会先创建一个ReadView,记录当前所有未提交事务的信息。比如up_limit_id=2,low_limit_id=5,trx_ids=[2,4,5],creator_trx_id=6我们之前说过,每条记录都有一个隐含的字段db_trx_id来记录最近修改这条记录ID的交易,比如db_trx_id=3;那么接下来,数据库会将这条记录db_trx_id的可见性与ReadView进行比较。如果db_trx_idlow_limit_id,则表示db_trx_id=3的事务在ReadView中所有未提交的事务创建后才提交,即当前事务打开后,其他事务修改数据并make提交。因此,这条记录对当前交易应该是不可见的。(看不到怎么办?后面再说)那么,还有一种情况,就是up_limit_id>db_trx_id>low_limit_id。在这种情况下,db_trx_id将与ReadView中的trx_ids进行一一比较。如果db_trx_id在trx_ids列表中,说明当前事务启动时,修改数据后提交了一个还没有提交的事务,那么这条记录对当前事务应该是不可见的。如果db_trx_id不在trx_ids列表中,说明在当前事务开始之前,其他事务已经修改并提交了数据,那么这条记录应该对当前事务可见。所以,在读取一条记录时,经过上面的判断,发现这条记录对当前事务可见,那么直接返回即可。那么,如果它不可见怎么办?没错,那么就需要用到undolog了。当数据的transactionID不符合ReadView规则时,需要从undolog中获取数据的历史快照,然后将数据快照的transactionID与ReadView进行比对,看是否可见。返回空。所以,总结一下,在InnoDB中,MVCC是通过ReadView+UndoLog来实现的。undolog保存的是历史快照,ReadView用于判断哪个快照是可见的。MVCC与隔离级别实际上,根据不同的事务隔离级别,获取ReadView的时机是不同的。在RC下,一个事务中的每一个SELECT都会重新获取ReadView,而在RR下,只有在一个事务中的第一个SELECT时才会获取一个ReadView。所以在可重复读的事务隔离级别下,因为MVCC机制,可以解决不可重复读的问题,因为它在第一次SELECT时只会得到一次ReadView,自然不会有重复读的问题.参考资料:https://dev.mysql.com/doc/refman/5.7/en/innodb-undo-logs.htmlhttps://time.geekbang.org/column/article/120351https://blog.csdn.net/SnailMann/article/details/94724197https://www.51cto.com/article/641019.htmlhttps://zhuanlan.zhihu.com/p/52977862