事务隔离级别和实现原理

事务具有原子性(Atomicity)一致性(Consistency)、隔离性(Isolation)持久性(Durability)四个特性,简称 ACID,缺一不可。原子性由undo log保证,持久性由redo log保证,今天要说的就是隔离性

标准SQL通过行共享锁、行排他锁、表共享锁、表排他锁实现四种事务隔离级别,InnoDB事务在RU, S 两种隔离级别实现原理和标准SQL差不多,在RC级别它通过MVCC提前标准SQL一个级别解决了不可重复读,在RR级别通过间隙锁提前标准SQL一个隔离级别解决了幻读。(MVCC: 为了不加锁解决读写冲突的问题)

1. 概念说明

以下几个概念是事务隔离级别要实际解决的问题,所以需要搞清楚都是什么意思。

脏读

脏读指的是读到了其他事务未提交的数据,未提交意味着这些数据可能会回滚,也就是可能最终不会存到数据库中,也就是不存在的数据。读到了并一定最终存在的数据,这就是脏读。

可重复读

可重复读指的是在一个事务内,最开始读到的数据和事务结束前的任意时刻读到的同一批数据都是一致的。通常针对数据**更新(UPDATE)**操作。

不可重复读

对比可重复读,不可重复读指的是在同一事务内,不同的时刻读到的同一批数据可能是不一样的,可能会受到其他事务的影响,比如其他事务改了这批数据并提交了。通常针对数据**更新(UPDATE)**操作。

幻读

幻读是针对数据**插入(INSERT)**操作来说的。假设事务A对某些行的内容作了更改,但是还未提交,此时事务B插入了与事务A更改前的记录相同的记录行,并且在事务A提交之前先提交了,而这时,在事务A中查询,会发现好像刚刚的更改对于某些数据未起作用,但其实是事务B刚插入进来的,让用户感觉很魔幻,感觉出现了幻觉,这就叫幻读。

2. 事务隔离级别

SQL 标准定义了四种隔离级别,MySQL 全都支持。这四种隔离级别分别是:

  1. 读未提交(READ UNCOMMITTED)
  2. 读提交 (READ COMMITTED)
  3. 可重复读 (REPEATABLE READ)
  4. 串行化 (SERIALIZABLE)

从上往下,隔离强度逐渐增强,性能逐渐变差。采用哪种隔离级别要根据系统需求权衡决定,其中,可重复读是 MySQL 的默认级别。

事务隔离其实就是为了解决上面提到的脏读、不可重复读、幻读这几个问题,下面展示了 4 种隔离级别对这三个问题的解决程度。

img

只有串行化的隔离级别解决了全部这 3 个问题,其他的 3 个隔离级别都有缺陷。

3. 标准SQL事务隔离级别实现原理

我们上面遇到的问题其实就是并发事务下的控制问题,解决并发事务的最常见方式就是悲观并发控制了(也就是数据库中的锁)。标准SQL事务隔离级别的实现是依赖锁的,我们来看下具体是怎么实现的:

事务隔离级别 实现方式
未提交读(RU) 事务对当前被读取的数据不加锁;事务在更新某数据的瞬间(就是发生更新的瞬间),必须先对其加行级共享锁,直到事务结束才释放。
提交读(RC) 事务对当前被读取的数据加行级共享锁(当读到时才加锁),一旦读完该行,立即释放该行级共享锁;事务在更新某数据的瞬间(就是发生更新的瞬间),必须先对其加行级排他锁,直到事务结束才释放。
可重复读(RR) 事务在读取某数据的瞬间(就是开始读取的瞬间),必须先对其加行级共享锁,直到事务结束才释放;事务在更新某数据的瞬间(就是发生更新的瞬间),必须先对其加行级排他锁,直到事务结束才释放。
序列化读(S) 事务在读取数据时,必须先对其加表级共享锁 ,直到事务结束才释放;事务在更新数据时,必须先对其加表级排他锁 ,直到事务结束才释放。

可以看到,在只使用锁来实现隔离级别的控制的时候,需要频繁的加锁解锁,而且很容易发生读写的冲突(例如在RC级别下,事务A更新了数据行1,事务B则在事务A提交前读取数据行1都要等待事务A提交并释放锁)。

为了不加锁解决读写冲突的问题,MySQL引入了MVCC(Multiversion concurrency control)机制,详细可见我以前的分析文章:一文读懂数据库中的乐观锁和悲观锁和MVCC

共享锁、排他锁都是悲观锁。

4. InnoDB事务隔离级别实现原理

在往下分析之前,我们有几个概念需要先了解下:

4.1 锁定读和一致性非锁定读

锁定读:在一个事务中,主动给读加锁,如SELECT … LOCK IN SHARE MODE 和 SELECT … FOR UPDATE。分别加上了行共享锁和行排他锁。15.7.2.4 Locking Reads

一致性非锁定读:InnoDB使用MVCC向事务的查询提供某个时间点的数据库快照。查询会看到在该时间点之前提交的事务所做的更改,而不会看到稍后或未提交的事务所做的更改(本事务除外)。也就是说在开始了事务之后,事务看到的数据就都是事务开启那一刻的数据了,其他事务的后续修改不会在本次事务中可见。

Consistent read是InnoDB在RC和RR隔离级别处理SELECT语句的默认模式。一致性非锁定读不会对其访问的表设置任何锁,因此,在对表执行一致性非锁定读的同时,其它事务可以同时并发的读取或者修改它们。15.7.2.3 Consistent Nonlocking Reads

4.2 当前读和快照读

当前读

读取的是最新版本,像UPDATE、DELETE、INSERT、SELECT … LOCK IN SHARE MODE、SELECT … FOR UPDATE这些操作都是一种当前读,为什么叫当前读?就是它读取的是记录的最新版本,读取时还要保证其他并发事务不能修改当前记录,会对读取的记录进行加锁。

快照读

读取的是快照版本,也就是历史版本,像不加锁的SELECT操作就是快照读,即不加锁的非阻塞读;快照读的前提是隔离级别不是未提交读和序列化读级别,因为未提交读总是读取最新的数据行,而不是符合当前事务版本的数据行,而序列化读则会对表加锁

4.3 隐式锁定和显式锁

隐式锁定

InnoDB在事务执行过程中,使用两阶段锁协议(不主动进行显示锁定的情况):

  • 随时都可以执行锁定,InnoDB会根据隔离级别在需要的时候自动加锁;
  • 锁只有在执行commit或者rollback的时候才会释放,并且所有的锁都是在同一时刻被释放。

显式锁定

  • InnoDB也支持通过特定的语句进行显示锁定(存储引擎层)
select ... lock in share mode //共享锁
select ... for update //排他锁
  • MySQL Server层的显示锁定:
lock table
unlock table

了解完上面的概念后,我们来看下InnoDB的事务具体是怎么实现的(下面的读都指的是非主动加锁的select)

事务隔离级别 实现方式
未提交读(RU) 事务对当前被读取的数据不加锁,都是当前读;事务在更新某数据的瞬间(就是发生更新的瞬间),必须先对其加行级共享锁,直到事务结束才释放。
提交读(RC) 事务对当前被读取的数据不加锁,且是快照读;事务在更新某数据的瞬间(就是发生更新的瞬间),必须先对其加行级排他锁(Record),直到事务结束才释放。通过快照(MVCC),在这个级别MySQL就解决了不可重复读的问题
可重复读(RR) 事务对当前被读取的数据不加锁,且是快照读;事务在更新某数据的瞬间(就是发生更新的瞬间),必须先对其加行级排他锁(Record,GAP,Next-Key),直到事务结束才释放。通过间隙锁,在这个级别MySQL就解决了幻读的问题
序列化读(S) 事务在读取数据时,必须先对其加表级共享锁 ,直到事务结束才释放,都是当前读;事务在更新数据时,必须先对其加表级排他锁 ,直到事务结束才释放。

可以看到,InnoDB通过MVCC很好的解决了读写冲突的问题,而且提前一个级别就解决了标准级别下会出现的幻读和不可重复读问题,大大提升了数据库的并发能力。

参考

深入理解MySQL中事务隔离级别的实现原理

MySQL事务隔离级别和实现原理(看这一篇文章就够了!)

Innodb中的事务隔离级别和锁的关系

版权

评论