为什么面试的时候,面试官总爱问你底层原理。因为复杂系统的设计总是可以相互借鉴的。MVCC 正是这样的一个知识点。
如果没有 MVCC 机制,MySQL 将无法保证在高并发下数据一致和访问性能之间的平衡。
要聊 MVCC,就无法不提及事务。为了方便你理解,我会先带你复习回顾一下MySQL事务,然后再看看 MVCC 在不同的事务隔离级别下,分别是如何工作的。
MySQL的事务:
在事务中的操作,要么全部执行,要么全部回滚。
事务有四大特性 ACID:原子性,一致性,隔离性,持久性。
事务的实现核心是基于两个文件,也就是重做日志redo log和回滚日志undo log。
redo log
redo log 重做日志,是记录物理数据变化的日志,使用数据库DML对数据的修改操作,都会产生redo log,它可以保证事务的持久性。
redo log 记录了一系列的DML操作,因此也可以用来进行数据恢复。
redo log 分为两部分:
一部分在内存中(redo log buffer)
一部分在磁盘文件中(redo logfile)
和现有的主流日志框架一样,日志先写入内存,再异步持久化到磁盘。
Write Ahead Log,日志先行:
1,先将旧的数据从磁盘中读入内存
2,生成—条redo log并写入redo log buffer
3,当事务commit时,将redo log buffer 中的内容持久化到磁盘文件
4,将内存中修改的数据写到磁盘,定期批量执行
为什么必须要先写日志呢?
事务采用日志先行来保证数据是持久的,不管三七二十一,先写日志再说。
当一个事务提交时,其产生所有的日志必须先写到磁盘中,这样一来,若在日志写入磁盘后,内存中的数据持久化前数据库发生了宕机,那么数据库重启时,可以通过日志来保证数据的完整性。
undo log
undo log是回滚日志,有两个作用:提供回滚操作和多个行版本控制(MVCC)。
在数据修改的时候,不仅记录了redo,还记录了相对应的undo,如果因为某些原因导致事务失败或回滚了,可以借助该undo进行回滚。
undo log和redo log记录物理日志不一样。undo log主要记录的是数据的逻辑变化,它是逻辑日志。
可以认为当delete一条记录时,undo log中会记录一条对应的insert记录,反之亦然,当update一条记录时,它记录一条对应相反的update记录。
当执行rollback时,就可以从undo log中的逻辑记录读取到相应的内容并进行回滚。
多行版本控制(MVCC)的时候,也是通过undo log来实现的:
当读取的某一行被其他事务锁定时,它可以从undo log中分析出该行记录以前的数据是什么,从而提供该行版本信息,让用户实现非锁定一致性读取。
MySQL 事务隔离级别
除了redo log 和undo log 以外,另一个你在学习MVCC 之前不得不了解的知识,就是隔离级别。隔离级别是相当基础的知识,所以这里我们快速复习下就好。
注:MySQL 的默认隔离级别为repeatable-read级别
并且在MySQL中 repeatable-read级别还可以处理幻读,这是 MySQL独有的 next-keylock 实现的。
不同的数据库厂商对 SQL标准 中规定的四种隔离级别支持不一样:
Oracle 就只支持 read-committed 和 serializable 隔离级别。
MVCC 只在read-committed和repeatable-read 两个隔离级别下工作,其他两个隔离级别:
read-uncommitted,总是读取最新的数据行,而不会读当前事务版本的数据行。
serializable,则会对所有读取的行都加锁, 和 MVCC不兼容。
MVCC原理
版本链
MySQL的每行记录逻辑上其实是一个链表。
MySQL行记录中除了记录业务数据外,还有隐藏的 trx_id 和 roll_ptr
trx_id
:表示最近修改的事务的id ,每次一个事务对某条聚簇索引记录进行改动时,都会把该事务的事务id
赋值给trx_id
隐藏列。新增一个事务时,trx_id会递增,因此 trx_id 能够表示事务开始的先后顺序。roll_pointer
:指向该行上一个版本的地址,每次对某条聚簇索引记录进行改动时,都会把旧的版本写入到undo日志
中,然后这个隐藏列就相当于一个指针,可以通过它来找到该记录修改前的信息。
update user set name = '源宝 2 ' where id = 1
当然,这个链表存在于 undo log 中,和最新版本的数据不在一起。
每次更新后,都会将旧值放到一条 undo log 中,就算是该记录的一个旧版本,随着更新次数的增多,所有的版本都会被roll_ptr 属性连接成一个链表,我们把这个链表称之为版本链,版本链的头节点就是当前记录最新的值。
另外,每个版本中还包含生成该版本时对应的 事务id(trx_id),这个信息很重要,我们稍后就会用到。
ReadView
说完了undo log,再来说说 ReadView。前面我们说过:MVCC 只在 read-committed 和 repeatable-read
两个隔离级别下工作,而 read-committed 和 repeatable-read 的区别就在于它们生成 ReadView的策略不同。
对于使用 read-committed 和 repeatable-read 隔离级别的事务来说,都必须保证读到已经提交了的事务修改过的记录,也就是说假如另一个事务已经修改了记录但是尚未提交,是不能直接读取最新版本的记录的。
核心问题就是:需要判断一下,版本链中的哪个版本是当前事务可见的。
为此,InnoDB提出了一个ReadView的概念,这个ReadView 中有个 id 列表 trx_ids
来存储系统中当前活跃着的读写事务,也就是 begin 了还未 commit 或 rollback 的事务。
继续用之前的例子来理解一下 ReadView 和 trx_ids。
提交trx_id是2的记录后,接着有一个trx_id为3的事务,修改 name为源宝3,但是事务还没提交。
代码语言:javascript复制update user set name = '源宝 3 ' where id = 1
则此时的版本链是︰
显然,此时的 trx_ids 为 [ 3 ]
如果另一个事务查询id为1的记录,因为trx_ids当前只有事务 trx_id 为 3 的事务,而 trx_ids 的意义是记录未完成的事务。在这里,事务未完成,所以该条记录不可见,继续查询下—条,结果返回源宝2。
这时我把 trx_id 为 3 的事务提交了,并且新建了一个 trx_id 为 4 也修改 id 为 1 的记录name=源宝4,并且不提交事务。
代码语言:javascript复制update user set name = '源宝 4 ' where id = 1
这时候版本链就是︰
read-committed —— 每次查询数据前都生成一个 ReadView
trx_ids 将更新为[ 4 ],版本链通过 trx_id 对比查找到的结果就是源宝3。
repeatable-read —— 在第一次查询数据时生成一个 ReadView,之后的读都复用之前的。
不会有重建的 ReadView , trx_ids 还是 [ 3 ],MySQL 认为事务3未完成,所以 select 的结果是源宝2。第2次 select 结果和第1次一样,所以叫可重复读。
MVCC 小结
从上边的描述中我们可以看出来,所谓的MVCC(Multi-Version Concurrency Control ,多版本并发控制)指的就是在使用read-committed、repeatable-read这两种隔离级别的事务,在执行普通的 SELECT 操作时,访问记录的版本链的过程。
这样子可以使不同事务的读-写、写-读操作并发执行,从而提升系统性能。
read-committed、repeatable-read 这两个隔离级别的一个很大不同就是:生成ReadView 的时机不同
read-committed 在每一次进行普通 select 操作前都会生成一个 ReadView,而repeatable-read 只在第一次进行普通 select 操作前生成一个 ReadView,之后的查询操作都重复使用这个 ReadView 就好了。
优秀啊,骚年!后期更多优选推文,各种资料、分享猛料放出,关注公众号,获取实时动态:
大家还有什么需求,也可以后台留言给我,公众号上还有其他学习资源哦....