MySQL中的MVCC实现机制
MySQL中的MVCC实现机制
MVCC(多版本并发控制)是现代数据库系统中常用的处理读写冲突的手段,其目的在于提高数据库在高并发场景下的吞吐性能。本文将详细介绍MySQL中MVCC的实现机制,包括快照读与当前读的区别、undo log的作用、隐式字段的含义以及Read View的创建和使用。
什么是MVCC?
MVCC (Multiversion Concurrency Control) 中文全称叫多版本并发控制,是现代数据库(包括 MySQL、Oracle、PostgreSQL 等)引擎实现中常用的处理读写冲突的手段,目的在于提高数据库高并发场景下的吞吐性能。
MVCC是一种无锁的并发控制方法,MVCC在InnoDB中的实现主要是为了提高数据库并发性能,用更好的方式去处理读-写冲突,做到即使有读写冲突时,也能做到不加锁,非阻塞并发读。
快照读和当前读
要想搞清楚MVCC的机制,最重要的一个概念那就是快照读。
- 快照读
所谓快照读,就是读取的是快照数据,即快照生成的那一刻的数据,像我们常用的普通的SELECT语句在不加锁情况下就是快照读。如:
SELECT * FROM xx_table WHERE ...
- 当前读
和快照读相对应的另外一个概念叫做当前读,当前读就是读取最新数据,所以,加锁的 SELECT,或者对数据进行增删改都会进行当前读,比如:
SELECT * FROM xx_table LOCK IN SHARE MODE;
SELECT * FROM xx_table FOR UPDATE;
INSERT INTO xx_table ...
DELETE FROM xx_table ...
UPDATE xx_table ...
可以说快照读是MVCC实现的基础,而当前读是悲观锁实现的基础。
那么,快照读读到的快照是从哪里读到的的呢?换句话说,快照是存在哪里的呢?
undo log
undo log是MySQL中比较重要的事务日志之一,顾名思义,undo log是一种用于回退的日志,在事务没提交之前,MySQL会先记录更新前的数据到 undo log日志文件里面,当事务回滚时或者数据库崩溃时,可以利用 undo log来进行回退。
这里面提到的存在 undo log 中“更新前的数据”就是我们前面提到的快照。所以,这也是为什么说 undo log 是MVCC实现的重要手段的原因。
那么,一条记录在同一时刻可能有多个事务在执行,那么,undo log会有一条记录的多个快照,那么在这一时刻发生SELECT要进行快照读的时候,要读哪个快照呢?
这就需要用到另外几个信息了
隐式字段
其实,数据库中的每行记录中,除了保存了我们自己定义的一些字段以外,还有一些重要的隐式字段的:
db_row_id (6字节)
隐藏主键,如果我们没有给这个表创建主键,那么会以这个字段来创建聚簇索引。db_trx_id (6字节)
对这条记录做了最新一次修改的事务的ID。db_roll_ptr(7字节)
回滚指针,指向这条记录的上一个版本,其实他指向的就是Undo Log中的上一个版本的快照地址。
因为每一次记录变更之前都会先存储一份快照到undo log中,那么这几个隐式字段也会跟着记录一起保存在undo log中,就这样,每一个快照中都有一个db_trx_id字段记录了本次变更的事务ID,以及一个db_roll_ptr字段指向了上一个快照的地址。(db_trx_id和db_roll_ptr是重点,后面还会用到)。
这样,就形成了一个快照链表:
有了undo log,又有了几个隐式字段,我们好像还是不知道具体应该读取哪个快照,那怎么办呢? 这时候就需要Read View 登场了。
Read View
什么是Read View?
Read View就是事务进行快照读操作的时候生成的读视图(Read View),在该事务执行的快照读的那一刻,会生成数据库系统当前的一个快照,记录并维护系统当前活跃事务的ID(当每个事务开启时,都会被分配一个ID, 这个ID是递增的,所以最新的事务,ID值越大)。
Read View 主要来帮我们解决可见性的问题的, 即它会来告诉我们本次事务应该看到哪个快照,不应该看到哪个快照。
在 Read View 中有几个重要的属性
- trx_ids,系统当前未提交的事务 ID 的列表。
- low_limit_id,未提交的事务中最大的事务 ID。
- up_limit_id,未提交的事务中最小的事务 ID。
- creator_trx_id,创建这个 Read View 的事务 ID。
每开启一个事务,我们都会从数据库中获得一个事务 ID,这个事务 ID 是自增长的,通过 ID 大小,我们就可以判断事务的时间顺序。
说明:只有在对表中的记录做改动时(执行INSERT、DELETE、UPDATE这些语句时)才会为事务分配事务id,否则在一个只读事务中的事务id值都默认为0。
那么,一个事务应该看到哪些快照,不应该看到哪些快照该如何判断呢?
其实原则比较简单,那就是事务ID大的事务应该能看到事务ID小的事务的变更结果,反之则不能。
我们前面说过,每一条记录上都有一个隐式字段db_trx_id记录对这条记录做了最新一次修改的事务的ID。那么接下来,数据库会拿这条记录db_trx_id和Read View进行可见性比较。
在创建 Read View 后,我们可以将记录中的 db_trx_id 划分这三种情况:
db_trx_id < up_limit_id
这种情况说明,表示这个版本的记录是在创建 Read View 前已经提交的事务生成的,所以该版本的记录对当前事务可见。db_trx_id>=low_limit_id
这种情况说明,表示这个版本的记录是在创建 Read View 后才启动的事务生成的,所以该版本的记录对当前事务不可见(不可见怎么办呢?后面讲)up_limit_id < = db_trx_id < low_limit_id
这种情况下,会再拿db_trx_id和Read View中的trx_ids进行逐一比较。
如果db_trx_id 在trx_ids列表中,表示生成该版本记录的活跃事务依然活跃着(还没提交事务),所以该版本的记录对当前事务不可见。
如果db_trx_id不在trx_ids列表中,表示生成该版本记录的活跃事务已经被提交,所以该版本的记录对当前事务可见。
所以,当读取一条记录的时候,经过以上判断,发现记录对当前事务可见,那么就直接返回就行了。那么如果不可见怎么办?没错,那就需要用到undo log了。
当数据的事务ID不符合Read View规则时候,那就需要从undo log里面获取数据的历史快照,然后将数据快照的事务ID再来和Read View进行可见性比较,如果找到一条快照,则返回,找不到则返回空。
MVCC和隔离级别
其实,根据不同的事务隔离级别,Read View的创建和获取时机是不同的
读未提交
数据库不会创建 read view读提交
在每个事务开始时,数据库会创建一个 read view,用于记录事务开始时数据库中已提交事务的快照。这个 read view 会随着事务的执行而逐渐变化,跟踪新提交的事务以及旧事务的回滚。因此,每个事务都有自己独立的 read view。
一个事务中的每一次SELECT都会重新获取一次Read View
- 可重复读
在第一个查询开始执行时,MySQL 会为当前事务创建一个 read view,并在整个事务期间保持不变。这个 read view 记录了事务开始时数据库中已提交事务的快照。这意味着在同一个事务内的所有查询都会看到相同的数据快照,即使其他事务提交了新的数据也不会影响。因此就不存在重复读的问题了。
一个事务中只在第一次SELECT的时候会获取一次Read View。
- 串行化
数据库会在事务开始时创建一个 read view,并且在事务期间始终保持不变,类似于可重复读隔离级别。
总结
MVCC的目的就是多版本并发控制,在数据库中的实现,就是为了解决读写冲突,它的实现原理主要是依赖记录中的 几个隐式字段,undo日志 ,Read View 来实现的。
在InnoDB中,MVCC就是通过Read View + Undo Log来实现的,undo log中保存了历史快照,而Read View 用来判断具体哪一个快照是可见的。
总之,MVCC就是因为设计者不满意只让数据库采用悲观锁这样性能不佳的形式去解决读-写冲突问题,而提出的解决方案,所以在数据库中,因为有了MVCC,所以我们可以形成两个组合:
- MVCC + 悲观锁 MVCC解决读写冲突,悲观锁解决写写冲突
- MVCC + 乐观锁 MVCC解决读写冲突,乐观锁解决写写冲突
这种组合的方式就可以最大程度的提高数据库并发性能,并解决读写冲突,和写写冲突导致的问题
参考链接:
https://www.51cto.com/article/719614.html
https://xiaolincoding.com