MYSQL数据库中,事务主要有4种隔离级别,分别是未提交读,提交读,可重复读,可串行化。
多版本并发控制(Muti-Version Concurrency Control,MVCC)是MYSQL的InnoDB引擎实现隔离级别的一种具体方式。主要用来解决提交读和可重复读。
但是单纯的MVCC还不能解决幻读的问题,于是在MVCC基础上加上Next-key Locks,达到可串行化的效果。
这里只谈简单的理解。
1. MVCC
顾名思义,通过多版本来实现并发控制。同一条记录在系统中可以存在多个版本。
谈到并发控制,无非就是加锁实现。但加锁过程也会影响性能,这就是一致性与高可用的矛盾。MVCC免去读操作的加锁(非阻塞的读),使开销更低。写操作必须加锁。
MVCC就是想办法少加点锁,通过其他方式保证数据一致性。MVCC是通过保存数据在某一个时间点快照来实现的。也就是说不管实现时间多长,每个事务看到的数据都是一致的。
于是不得不提到版本号、快照读与当前读。
版本号,包括系统版本号与事务版本号。系统版本号是递增的数字,每开始一个新的事务,系统版本号自动增加。事务版本号就是事务开始时的系统版本号。
MVCC机制虽然能够让数据可重复读,但读取的数据可能是历史数据。读取历史数据的方式,称为快照读,而读取数据库最新版本的数据,称为当前读。
快照读实现了可重复读。自然的也解决了幻读问题(历史数据未变更)。
对数据修改的操作(update、insert、delete)都是采用当前读的模式。在执行这几个操作时会读取最新的记录,即使是别的事务提交的数据也可以查询到。读取的是最新的数据,需要加锁。当前读不能解决幻读问题,需要加锁,使用Next-key Lock。
1.1 隐藏列
MVCC工作时,在每行记录后边保存两个隐藏列。分别是数据行的创建时间,数据行的删除时间。这里的时间只是一种说法,不代表具体的时间,而是当时的系统版本号。
注:这里表述的删除时间(版本号)不易理解,恰当的说法应该是回滚版本号(回滚指针)。通过该版本号在回滚日志中查询历史数据。
当开始一个新事务时,该事务的版本号肯定会大于当前所有数据行快照的创建版本号。
隐藏列对客户端来说是不可见的。
1.2 回滚日志(undo log)
当事务对数据行进行一次更新操作时,会把旧数据行记录在一个叫做undo log的记录中。
在undo log中除了记录数据行,还会记录下该行数据的对应的创建版本号。并将原来数据行中的回滚指针指向undo log 记录的这行数据。然后再在原来数据表中进行一次更新操作,如果这次更新操作回滚了,那么就可以根据回滚指针去undo log中查找之前的数据进行复原。
如果后续还有更新操作的话,就会在undo log中和之前的数据行形成一条链表,链表头就是最新的数据,这条链表就叫做版本链。
事务的可见性都是基于undo log来实现的。
1.3 事务快照(ReadView/可读视图)
当进行查询操作时,事务会生成一个ReadView,ReadView是一个事务快照,准确来说是当前时间点系统内活跃的事务列表,也就是说系统内所有未提交的事务,都会记录在这个Readview内,事务就根据它来判断哪些数据是可见的,哪些是不可见的。
查询一条数据时,事务会拿到这个ReadView,去到undo log中进行判断。若查询到某一条数据:
- 先去查看undo log中的最新数据行,如果数据行的版本号小于ReadView记录的事务版本号最小值,就说明这条数据对当前数据库是可见的,可以直接作为结果集返回
- 若数据行版本号大于ReadView记录最大值,说明这条数据是由一个新的事务修改的,对当前事务不可见,那么就顺着版本链继续往下寻找第一条满足条件的
- 若数据行版本号在ReadView最小值和最大值之间,那么就需要进行遍历了整个ReadView了,如果数据行版本号等于ReadView的某个值,则说明该行数据仍然处于活跃状态,那么对当前事务不可见
1.4 提交读与可重复读的实现
关键在于生成ReadView的时机不同。
对提交读来说,事务中的每次读操作都会生成一个新的ReadView,也就是说,如果这期间某个事务提交了,那么它就会从ReadView中移除。这样确保事务每次读操作都能读到相对比较新的数据。
而对可重复读来说,事务只有在第一次进行读操作时才会生成一个ReadView,后续的读操作都会重复使用这个ReadView。也就是说,如果在此期间有其他事务提交了,那么对于可重复读来说也是不可见的,因为对它来说,事务活跃状态在第一次进行读操作时就已经确定下来,后面不会修改了。
1.5 理解增删改查
SELECT
在SELECT过程中,InnoDB只查找版本早于当前事务版本的数据行,这样可以确保事务读取的行要么是在开始事务之前已经存在要么是事务自身插入或者修改过的,在事务开始之后才插入的行,事务不会看到。
行的删除版本号要么未定义,要么大于当前事务版本号,这样可以确保事务读取到的行在事务开始之前未被删除,在事务开始之前就已经过期的数据行,该事务也不会看到。
INSERT
将当前系统版本号作为数据行快照的创建版本号。
DELETE
将当前系统版本号作为数据行快照的删除版本号。
UPDATE
将当前系统版本号作为更新前的数据行快照的删除版本号,并将当前系统版本号作为更新后的数据行快照的创建版本号。可以理解为先执行 DELETE 后执行 INSERT。
2. Next-key Locks
Next-key Locks由record locks(记录锁) 和 gap locks(间隙锁)组合而成,每次锁住的不光是需要使用的数据,还会锁住这些数据附近的数据。
InnoDB的三种行锁算法:
- Record Lock:单个行记录上的锁。
- Gap Lock:间隙锁,锁定一个范围,但不包括记录本身。GAP锁的目的,是为了防止同一事务的两次当前读,出现幻读的情况。
- Next-Key Lock:1+2,锁定一个范围,并且锁定记录本身。对于行的查询,都是采用该方法,主要目的是解决幻读的问题。
2.1 Record Locks
锁定一个记录上的索引,而不是记录本身。
2.2 Gap Locks
锁定索引之间的间隙,但是不包含索引本身。
2.3 Next-Key Locks
Next-Key Locks 是 MySQL 的 InnoDB 存储引擎的一种锁实现。
当查询的索引含有唯一属性的时候,Next-Key Lock 会进行优化,将其降级为Record Lock,即仅锁住索引本身,不是范围。
它是 Record Locks 和 Gap Locks 的结合,不仅锁定一个记录上的索引,也锁定索引之间的间隙。