网上MVCC资料太多太乱,总结一下自己学习到的MVCC,没有废话,文章主打一个言简意赅
了解MVCC之前先了解几个前置知识
1 事务
1.1 事务介绍
事务,由一个有限的数据库操作序列构成,这些操作要么全部执行,要么全部不执行,是一个不可分割的工作单位。
面试回答:事务可以使「一组操作」要么全部执行成功,要么全部执行失败。目的就是保证数据的最终一致性。
1.2 事务特性(ACID)
- 原子性(A): 当前事务的操作要么同时成功,要么同时失败。
原子性在是通过undo log日志保证的,undo log记录了事务提交前的数据,一旦事务发生异常,就会回滚到事务提交前的状态。
- 持久性(D): 一旦提交了事务,它对数据库的改变就应该是永久性的。
说白了就是,会将数据持久化在硬盘上。它主要通过redo log日志保证,一旦事务提交成功就会写入到redo log日志文件,顺序写,速度快。
- 一致性(C): 一致性确保事务将数据库从一个一致性状态转移到另一个一致性状态。
个人觉得一致性对于刚接触的小白来说并不是很好理解,这里讲一下个人的见解。
我觉得一致性无非就是事务前后数据从一个正常的状态更新另外一个正常的状态,比如我现在更新一个字段age 1->5,那么事务提交前age = 1就是一个正常的状态,事务提交完成后age = 5就是一个正常的状态。那还有一种情况是事务发生异常回滚,那事务回滚后age = 1就是一个正常的状态而不是 age = 5。
- 隔离性(I): 在事务「并发」执行时,他们内部的操作不能互相干扰。
隔离级别包括读未提交、读已提交、可重复读、可串行化。隔离级别越高隔离性越高,性能越低。
如果不做隔离会出现脏读、不可重复读、幻读等问题。
2 MVCC
2.1 MVCC介绍
MySQL InnoDB依靠MVCC实现事务隔离级别。MVCC其实就是一种并发控制的方法。
MVCC关键知识点:事务版本号、隐式字段、undo log、版本链、快照读和当前读、read view、
2.2 事务版本号
事务开启前都会从数据库中获取一个自增的ID来判断事务执行的先后顺序,这个id就是事务版本号。
注意的是只有更新数据的时候才会真正分配版本号
2.3 隐式字段
如果表中没有id又没有非null唯一值的时候才会出现row_id
2.4 undo log
undo log是回滚日志,记录了事务开始的时候的数据状态,这里不具体讲。
在MVCC中主要两个作用:
- 事务回滚。
- 用于MVCC快照读。
2.5 版本链
通过回滚指针(roll_pointer)连成的一个链表
2.6 当前读和快照读
- 当前读:其实就是读取最新的数据,当前读会对数据加锁避免其他事务修改数据。
- 快照读:读取的是可见版本的数据
2.7 Read View
Read View是事务开始的产生的一个可见视图,作用就是控制事务只能读取到指定版本的数据。
- trx_ids(min_ids):当前系统(事务开始时)活跃事务id的集合。比如当前系统有A,B,C还在执行中,min_ids就是这三个事务id的集合。
- up_limit_id(min_limit_id):当前系统活跃的事务的最小事务id,也就是m_ids中最小的。
- low_limit_id(max_limit_id):表示系统中下一个事务的id。比如我现在min_ids = [2,5,6],还有[7,8]是已经提交完成的事务,那max_limit_id = 9,而不是min_ids集合中最大值+1,这点需要搞清楚。
- creator_trx_id:创建当前read view的事务id。
搞清楚这些我们再来看看read view是怎么实现可见性的。 我们先定义一个变量事务id,来根据id范围讨论可见性(数据库分配事务id是自增的)
- id < up_limit_id:这些都是已经提交完成的事务|创建这个视图的事务,那当前事务肯定对这些事务都是可见的。
- id >= low_limit_id:这些事务都是至少可以确定在当前事务创建之后,当前事务创建的时候这些事务肯定都是还没提交的。那对这些事务肯定是不可见的。
- up_limit_id <= id < low_limit_id: 这里要分两点讨论
- 如果id在trx_ids中,说明当前事务创建的时候这些事务还没提交,不可见
- 如果id不在trx_ids中,说吗这些事务已经提交了,可见
这样我们就分析完所有的id了,其实简单点讲,无非是一个事务开始的时候要判断哪些事务是已经提交的,那些事务是未开始或者未提交的。事务只对该事务开始的时候一些已提交完成的事务修改可见。
MVCC小总结
其实MVCC并不复杂,无非就是事务开始时候创建一个read view视图,然后事务中进行快照读的时候根据trx_id和read view判断可见性,如果不可见就通过隐藏字段里面的roll_pointer查看上一个版本,找到可见的版本后通过undo log日志可以拿到具体数据。当前读就加锁,这样别的事务改不了就保证可重复读问题, 如果是更新操作那就更新trx_id,和roll_pointer,并且写到undo log中去。
2.3 脏读、不可重复读、幻读
基于这三个问题,探讨一下MVCC是如何解决的。
- 脏读:事务A修改了age = 10。事务B读取到age = 10,这时事务A发生回滚,那么B读到的数据就是脏数据,这就是脏读。
在MVCC控制下,事务读一般是快照读,对于未提交的事务的数据变更是不可见的,比如事务B在读取age的时候读取到的并不是10,因为age = 10这个版本对事务B是不可见的,他会从最新的版本通过undo log日志不断找上一个版本,直到找到可见的版本为止。
- 不可重复读:事务A读取age的值,事务B修改了age = 11,事务A再次读取age值,导致事务内两次读取操作值不一致。
快照读通过MVCC的read view控制数据的版本可见性,解决不可重读读问题。比如事务A查看一个数据,只能查看到事务A启动前已经提交的事务,后续事务对这个数据做修改对于事务A上不可见的,所以事务A读取到的这个值都是一致的。
当前读是通过加锁解决的不可重复读,当前读对数据加锁,其他事务不可对数据做修改。
- 幻读:事务A查询student表,有20条,事务B插入了3条学生记录,事务A再次查询student表发现是23条记录,这就叫幻读。
mvcc可以通过read view解决快照读场景下的幻读,当前读场景下是通过间隙锁来解决的。