很久以前写过事务原子性、一致性、持久性的实现原理,这篇文章聊一下Innodb事务的隔离性的实现原理,默认隔离级别是可重复读。主要还是讲整体脉络,脉络清晰后,细节大家可以自行补充。
1.知识点
要明白事务隔离性的实现原理,有几个知识点必须掌握。
1.1快照读
快照读:也称为一致非锁定读或一致性读,即不加锁的非阻塞读
前提:快照读的前提是隔离级别不是串行级别,串行级别下的快照读会退化成当前读
优点:基于提高并发性能的考虑,避免了加锁操作,降低了开销
实现:快照读的实现是基于多版本并发控制,即MVCC
效果:既然是基于多版本,即快照读可能读到的并不一定是数据的最新版本,而有可能是之前的历史版本
语法:一般的select就是快照读
1.2当前读
效果:读取的是记录的最新版本
实现:读取时还要保证其他并发事务不能修改当前记录,会对读取的记录进行加锁
语法:select lock in share mode(共享锁), select for update ; update, insert ,delete(排他锁)。全是需要加锁的。
1.3事务ID
InnoDB 里面每个事务有一个唯一的事务 ID,叫作 transaction id。它是在事务开始的时候向 InnoDB 的事务系统申请的,是按申请顺序严格递增的。
1.4Undo Log
在InnoDB redo、undo、binlog,是如何合作的中曾提到过Undo Log。对每行数据进行操作之前都会记录Undo Log,目的是能将数据进行回滚。
同一个事务对数据进行多次修改或者多个事务对同一个数据进行修改,这些修改会按照时间顺序连成链,所以通过undo log可以发现数据修改的历史。
Undo Log不是一直存在的。Undo Log主要分为两种:
insert undo log:代表事务在insert新记录时产生的undo log,只在事务回滚时需要,并且在事务提交后可以被立即丢弃
update undo log:事务在进行update或delete时产生的undo log; 不仅在事务回滚时需要,在快照读时也需要;所以不能随便删除,只有在快照读或事务回滚不涉及该日志时,对应的日志才会被purge线程统一清除
1.5行结构
表中每一行的数据,除了我们自定义的字段外,还有数据库隐式定义的DB_TRX_ID,DB_ROLL_PTR,DB_ROW_ID等字段。
DB_TRX_ID:6byte,最近修改(修改/插入)事务ID。记录创建这条记录/最后一次修改该记录的事务ID
DB_ROLL_PTR:7byte,回滚指针,指向这条记录的上一个版本(存储于rollback segment里),本质上利用undo log的能力
DB_ROW_ID:6byte,隐含的自增ID(隐藏主键),如果数据表没有主键,InnoDB会自动以DB_ROW_ID产生一个聚簇索引
1.6一致性视图(read-view)
1.6.1定义
事务执行的快照读的那一刻,会生成数据库系统当前的一个快照,注意,这个快照是基于整库的。记录并维护系统当前活跃事务的ID,里面事务 ID 的最小值记为低水位,当前系统里面已经创建过的事务 ID 的最大值加 1记为高水位。他们组成了当前事务的一致性视图(read-view)。
1.6.2生成时机
begin/start transaction 命令并不是一个事务的起点,在执行到它们之后的第一个操作InnoDB 表的语句,事务才真正启动。如果你想要马上启动一个事务,可以使用 start transaction with consistent snapshot 这个命令。
第一种启动方式,一致性视图是在第执行第一个快照读语句时创建的,如select;
第二种启动方式,一致性视图是在执行 start transaction with consistent snapshot 时创建的。
1.7MVCC
MVCC,全称Multi-Version Concurrency Control,即多版本并发控制。MVCC是一种并发控制的方法,一般在数据库管理系统中,实现对数据库的并发访问,在编程语言中实现事务内存。
2.数据可见性规则
根据上面提到的Undo Log、一致性视图,来看一下数据的可见性规则。
-
如果落在绿色部分,表示这个版本是已提交的事务,这个数据是可见的;
-
如果事务id是自己的值,表示数据是可见的;
-
如果落在红色部分,表示这个版本是由将来启动的事务生成的,是肯定不可见的;
-
如果落在黄色部分,那就包括两种情况
a. 若 row trx_id 在数组中,表示这个版本是由还没提交的事务生成的,不可见;
b. 若 row trx_id 不在数组中,表示这个版本是已经提交了的事务生成的,可见。
如果当前的版本不可见,则根据链表寻找上一个版本,再次判断可见性规则,直到找到可见的数据。
按照这个流程,大家便能够清楚的知道事务读取的值会是哪一个,同时也明白事务隔离性是怎样实现的。 也能看出来所谓的快照读并不是将所有数据拷贝一份,只是通过可见性规则来进行限制的。
在可见性规则上,我们看这个例子。表中有值(id:1,k:1),有三个事务进行操作,问事务B和事务A通过select获取的值分别是多少?
3.行锁
事务B查到的是3,事务A查到的是1。
事务A查到1比较容易理解,根据可见性规则,事务B和事务C’对事务A而言都属于未开始事务,所以等事务A查找的时候,会不断回溯,找到(1,1)。
事务B比较有意思,查到的不应该也是1吗,为什么是3?这里面有两个细节。
-
在讲当前读的时候,select lock in share mode(共享锁), select for update ; update, insert ,delete(排他锁)都是当前读,对修改的那行数据都加了锁。所以必须事务C’提交完后,事务B的update才能继续执行,否则事务B阻塞。
-
update先当前读,后写,行数据被真正的进行了修改,行数据中的DB_TRX_ID已经是事务B的事务ID,根据可见性规则,事务B查询到的值变为了3。
所以能够推论出两个例子:
-
如果事务B在操作update之前,执行一次select k from t where id=1,查到的值为1。这种情况经常会让大家认为快照读失效了,其实这是符合数据可见性规则的。
-
如果事务A的select改为select k from t where id=1 lock in share mode,查到的值是3。当然,只有事务B提交后, lock in share mode才能执行,否则会被阻塞。
4.总结
可重复读的核心就是一致性读(consistent read);而事务更新数据的时候,只能用当前读。如果当前的记录的行锁被其他事务占用的话,就需要进入锁等待。
一致性读、当前读和行锁串联起事务的隔离性。
资料
-
MVCC多版本并发控制
-
Mysql-InnoDB 事务-一致性读(快照读)
-
一篇文章带你掌握mysql的一致性视图(MVCC)
-
MySQL45讲
最后
大家如果喜欢我的文章,可以关注我的公众号(程序员麻辣烫)
我的个人博客为:https://shidawuhen.github.io/
往期文章回顾: