概述
隔离性是指,事务内部的操作与其他事务是隔离的,并发执行的各个事务之间不能互相干扰,避免事务冲突问题。严格的隔离性,对应了事务隔离级别中的Serializable (串行化),但实际应用中出于性能方面的考虑很少会使用可串行化。
两个线程,对数据库据的并发操作组合不外乎三种:读读,读写、写写。读读操作因为不改变数据,不存在互相干扰问题;隔离性主要聚焦在读写、写写两种场景。针对两个不同的场景,MySQL提供了不同的方案来保证事务间的隔离性。写写冲突,主要通过锁机制来解决;读写冲突则是用多版本并发控制(MVCC)来解决。
锁机制实现事务隔离
隔离性要求同一时刻只能有一个事务对数据进行写操作,在MySQL中,通过对数据加锁的机制,实现并发写的事务隔离性。
事务在修改数据之前,需要先获得相应的锁;获得锁之后,事务便可以修改数据;该事务操作期间,这部分数据是锁定的,其他事务如果需要修改数据,需要等待当前事务提交或回滚后释放锁。
MySQL中的锁包括全局锁、表锁、行锁等多种锁,不同的锁适用场景和实现原理不尽相同,性能也不一样,不同的存储引擎对锁的支持也不同。比较常涉及的锁包括表锁和行锁,表锁在操作数据时会锁定整张表的数据,并发性能较差;行锁则只锁定需要操作的一行数据记录,并发性能好。两种锁InnoDB引擎都支持,并发性能要求较高,建议使用行锁,一般情况下也是使用行锁居多。
通过锁机制,保证了任一时刻,只有一个事务在修改数据,从而达到并发写事务间互相不干扰的目标。事务隔离级别中的Serializable (串行化),也是使用锁方式实现串行读取。关于锁的更多描述,可以参考 MySQL进阶之MySQL中的锁 。
多版本并发控制(MVCC)实现事务隔离
MVCC(Multi-Version Concurrency Control)是一种并发控制的方法,一般在数据库管理系统中,实现对数据库的并发访问。MVCC可以让读写操作互不阻塞,让每一个写操作都会创建一个新版本的数据,此时并发的读操作可以从有限多个版本的数据中挑选一个最合适的结果直接返回,由此解决读写冲突问题(脏读、不可重复读、幻读),提高了并发性能。InnoDB实现多版本控制协议,主要是通过隐藏字段、回滚日志和版本链、读视图(ReadView)实现的。在深入了解这些概念之前,下面先大致了解一下读写冲突以及事务隔离级别等,接着再讨论各种事务隔离级别再InnoDB中是如何实现的。
- 脏读、不可重复读、幻读
隔离性要求并发执行的各个事务之间不能互相干扰,并发情形下,读写读操作可能会出现三类读写冲突问题,造成事务间相互干扰。
第一类问题是脏读,指的是一个事务读取到其它事务还未提交的数据。
脏读
第二类是不可重复读,指的是一个事务对同一条数据记录的两次或多次读取结果不一致,即一个事务读取到其它事务提交的更新。
不可重复读
第三类是幻读,指的是一个事务对同一张表两次或多次读取的行数不一致,即一个事务读取到其它事务新增或删除了数据。
幻读
- 事务隔离级别
SQL标准中定义了4种事务隔离级别,如下图表所示,在并发情形下,可以看到不同隔离级别,其满足事务隔离性程度不相同。事务隔离级别越低,系统开销越小,但其隔离性越差。
不同隔离级别事务隔离性的比较
未提交读(RU)级别属于完全没有隔离性的级别,存在脏读、不可重复度、幻读的问题,实际应用中使用较少。串行(SERIALIZABLE)级别强制事务串行执行,适合对一致性要求极高且并发性低或没有的应用场景。一般比较常用的是可重复读(RR)级别,InnoDB默认的隔离级别就是可重复读。注意,在SQL标准中,可重复读会存在幻读问题,但InnoDB实现的可重复读避免了幻读问题。
- 当前读和快照读
在MVCC并发控制中,读操作可以分成两类,当前读 (current read)与快照读 (snapshot read)。
当前读
语句select ... lock in share mode(共享锁)、select ... for update、 insert 、update、delete(排他锁)这些操作都是一种当前读,它读取的是记录的最新版本,读取时还要保证其他并发事务不能修改当前记录,会对读取的记录进行加锁。
为什么将insert 、update、delete操作,都归为当前读? 这是由InnoDB的数据落盘或执行流程决定的。前面关于持久性的部分说到缓冲池,存取数据时,InnoDB会从磁盘读取数据到缓冲池中,然后再对这些数据进行更新,完毕后才会刷新到磁盘中,也就是说这些操作会存在一个读取行为。比如update、delete操作,加载到缓冲池,SQL层更新或标识删除后,再写回缓冲池,最后再刷新到磁盘。对于insert操作可能触发Unique Key的冲突检查,也会进行一个当前读。
快照读
不加锁的select操作就是快照读,即不加锁的非阻塞读;快照读的前提是隔离级别不是串行级别,串行级别下的快照读会退化成当前读;快照读的情况,是基于提高并发性能的考虑,快照读的实现是基于多版本并发控制,即MVCC,可以认为MVCC是行锁的一个变种,但它在很多情况下,避免了加锁操作,降低了开销;既然是基于多版本,即快照读可能读到的并不一定是数据的最新版本,而有可能是之前的历史版本。
- MVCC实现原理
MVCC就是为了实现读-写冲突不加锁,而这个读指的就是快照读, 而非当前读,当前读实际上是一种加锁的操作,是悲观锁的实现。接下来将探讨InnoDB中MVCC的实现原理:三个重要的概念隐藏字段、回滚日志和版本链、读视图(ReadView)。关于隐藏字段和回滚日志以及版本链,参考 MySQL进阶之InnoDB事务原子性实现原理 。
读视图(ReadView)
什么是读视图,说白了就是事务进行快照读操作的时候生产的读视图(ReadView),在该事务执行的快照读的那一刻,会生成数据库系统当前的一个快照,记录并维护系统当前活跃事务的ID(当每个事务开启时,都会被分配一个ID, 这个ID是递增的,所以最新的事务,ID值越大)
所以我们知道 ReadView主要是用来做可见性判断的, 即当我们某个事务执行快照读的时候,对该记录创建一个ReadView读视图,把它比作条件用来判断当前事务能够看到哪个版本的数据,既可能是当前最新的数据,也有可能是该行记录的回滚日志版本链里面的某个版本的数据。
ReadView遵循一个可见性算法,主要是将要被读取或修改的数据的最新记录(版本链中的一个版本)中的事务id取出来,与系统当前其他活跃事务的ID去对比(由ReadView维护),如果事务id跟ReadView的活跃事务id列表做了某些比较,不符合可见性,那就通过回滚指针去取出版本链中的下一个版本的事务id再比较,即遍历版本链中所有版本的事务id(从链首到链尾,即从最近的一次修改查起),直到找到满足特定条件的事务id, 那么这个事务id对应版本链中的数据记录就是当前事务能看见的最新老版本,然后返回该版本信息作为结果。
可以简单把ReadView理解为具有以下3个属性的数据结构
{ // 针对当前要读取的数据记录的活跃事务(id)列表 trx_ids : [3,4,6] // 最小活跃事务id ,min_trx_id : 3 // 最大活跃事务id ,max_trx_id : 6}
MVCC基本原理
MVCC在并发情形下,对于不加锁的快照读,InnoDB实现已提交读(RC)和可重复读(RR)两种隔离级别的基本原理如下:
首先,在进行快照读(不加锁的select)前,获取ReadView,其包含【当前时刻】数据库中的活跃事务列表等信息
接着,开始遍历版本链的第一个数据版本,将其事务id与ReadView中的最小活跃事务id(min_trx_id)比较,如果小于min_trx_id,则当前事务能看到这个事务id对应版本的数据记录,直接返回该数据记录;否则,进入下一个判断
判断事务id是否大于最大活跃事务id(max_trx_id), 如果是,说明该版本的数据记录在ReadView生成后才出现的,那对当前事务肯定不可见;否则,进入下一个判断
判断事务id是否在活跃事务列表之中,如果在,说明对应的版本数据记录在生成ReadView时还没有commit,处于事务活跃状态,还未提交的数据对当前事务不可见,跳过当前版本遍历下一个版本;如果不在,则说明当前版本数据记录在ReadView生成之前就已经commit了,当前事务是能看见的,直接返回该数据记录。
从上边的描述中我们可以看出来,所谓的MVCC指的就是在使用RC 、RR这两种隔离级别的事务在执行快照读访问数据记录的版本链的过程,这样子可以使不同事务的读-写、写-读操作并发执行,从而提升系统性能。
RC、RR这两个隔离级别的一个很大不同就是生成 ReadView 的时机不同, RC在每一次快照读操作前都会生成一个ReadView,所以会读取到其它事务提交的最新版本 ,而RR在其整个事务期间,只在第一次进行快照读操作前生成一个ReadView ,之后的查询操作都重复这个ReadView就好,这样就解决的不可重复读的问题。
InnoDB通过包含事务信息的回滚日志版本链,以及ReadView中活跃事务进行比较,确保读取的数据记录都是提交后的数据,确保RC、RP级别都解决了脏读问题;而通过控制获取ReadView的时机,确保RR级别解决了不可重复读的问题。
而RU隔离级别,只需要读取最新版本的数据记录返回。
MVCC实现RC事务隔离级别流程分析
下图是RC级别下的并发读取流程,每次select查询都获取一次最新的ReadView进行比较
InnoDB通过MVCC实现的RC级别隔离
第一次select查询时,版本链中的3和2都在ReadView活跃事务列表中,所以这两个版本记录不可见,最后剩下版本1的结果符合MVCC协议,所以第一次select查询结果为蕃豆咖啡。
第二次select查询时,3和2已经提交,已经不存在最新的ReadView中,所以最新的ReadView只有4。当比较版本链的事务id时,版本3符合条件,第二次select查询结果为李四。
无论怎么查询,通过回滚日志版本链和ReadView实现的MVCC协议,可以确保不会查询到未提交的数据,也就是不会出现脏读问题。
MVCC实现RR事务隔离级别流程分析
下图是RR级别下的并发读取流程,整个读事务中,只获取一次ReadView
InnoDB通过MVCC实现的RC级别隔离
第一次select查询时,版本链中的3和2都在ReadView活跃事务列表中,所以这两个版本记录不可见,最后剩下版本1的结果符合MVCC协议,所以第一次select查询结果为蕃豆咖啡。和RC是一样的,也不会读取到脏数据。
第二次select查询时,3和2已经提交,但是InnoDB实现RR级别时,只在第一次select查询时获取ReadView,之后都不会再重新获取,所有此时的ReadView中的活跃事务依然还是2和3,所以版本2和版本3不可以见,版本4大于最大ReadView中的最大活跃事务3,也同样是不可见,所以第二次select查询结果为还是蕃豆咖啡。这就是可重复读,解决了不可重复读的问题。
总结
事务的隔离性由多版本控制机制和锁实现。 对于未提交读RU级别,直接读取最新版本的数据记录即可,对于RC和RR级别主要通过MVCC来实现,其中RR级别配合Next-Key锁解决了幻读问题;对于串行隔离级别,则主要基于锁机制来实现。