MySQL 事务隔离级别实现原理和锁的关系
隔离级别
并发带来的问题
脏读(dirty read)
如果一个事务读到了另一个未提交事务修改过的数据,如果另一个事务发生了回滚,那么该数据就是脏数据。
不可重复读(non-repeatable read)
如果一个事务只能读到另一个已经提交的事务修改过的数据,并且其他事务每对该数据进行一次修改并提交后,该事务都能查询得到最新值,即一个事务里两次查询一个数据的结果不一样。。
幻读(phantom read)
如果一个事务先根据某些条件查询出一些记录,之后另一个事务又向表中插入了符合这些条件的记录,原先的事务再次按照该条件查询时,能把另一个事务插入的记录也读出来。
注意
脏读侧重是未提交事务的数据。 而不可重复读和幻读都是读到了已提交的数据,但不可重复读重点在于update和delete,而幻读的重点在于insert。
四种隔离级别
实现原理
MVCC
首先,在介绍实现原理之前先简单的介绍一下MySQL的MVCC机制。
悲观锁和乐观锁
悲观锁
正如其名,它指的是对数据被外界(包括本系统当前的其他事务,以及来自外部系统的事务处理)修改持保守态度,因此,在整个数据处理过程中,将数据处于锁定状态。悲观锁的实现,往往依靠数据库提供的锁机制(也只有数据库层提供的锁机制才能真正保证数据访问的排他性,否则,即使在本系统中实现了加锁机制,也无法保证外部系统不会修改数据)。
在悲观锁的情况下,为了保证事务的隔离性,就需要一致性锁定读。读取数据时给加锁,其它事务无法修改这些数据。修改删除数据时也要加锁,其它事务无法读取这些数据。
乐观锁
相对悲观锁而言,乐观锁机制采取了更加宽松的加锁机制。悲观锁大多数情况下依靠数据库的锁机制实现,以保证操作最大程度的独占性。但随之而来的就是数据库性能的大量开销,特别是对长事务而言,这样的开销往往无法承受。而乐观锁机制在一定程度上解决了这个问题。
乐观锁,大多是基于数据版本( Version )记录机制实现。何谓数据版本?即为数据增加一个版本标识,在基于数据库表的版本解决方案中,一般是通过为数据库表增加一个 “version” 字段来实现。读取出数据时,将此版本号一同读出,之后更新时,对此版本号加一。此时,将提交数据的版本数据与数据库表对应记录的当前版本信息进行比对,如果提交的数据版本号大于数据库表当前版本号,则予以更新,否则认为是过期数据。
要说明的是,MVCC的实现没有固定的规范,每个数据库都会有不同的实现方式,这里讨论的是InnoDB的MVCC。
MVCC在MySQL中的实现
所谓的MVCC(Multi-Version Concurrency Control ,多版本并发控制)指的就是在使用读已提交(READ COMMITTD)、可重复读(REPEATABLE READ)这两种隔离级别的事务在执行普通的SELECT操作时访问记录的版本链的过程,这样子可以使不同事务的读-写、写-读操作并发执行,从而提升系统性能。
SELECT时,读取创建版本号<=当前事务版本号,删除版本号为空或>当前事务版本号。 INSERT时,保存当前事务版本号为行的创建版本号 DELETE时,保存当前事务版本号为行的删除版本号 UPDATE时,插入一条新纪录,保存当前事务版本号为行创建版本号,同时保存当前事务版本号到原来删除的行
快照读与当前读
快照读:就是select
select * from table ….;
当前读:特殊的读操作,插入/更新/删除操作,属于当前读,处理的都是当前的数据,需要加锁。
select * from table where ? lock in share mode;
select * from table where ? for update;
insert;
update ;
delete;
ReadView
InnoDB在实现MVCC时用到的一致性读视图,即consistent read view,用于支持RC(Read Committed,读提交)和RR(Repeatable Read,可重复读)隔离级别的实现。具体实现可见
读未提交Read Uncommitted
不做任何加锁和MVCC操作。
读提交Read Committed
对于读操作,不加锁,为快照读,每次读取都使用最新的事务版本号生成最新的ReadView。 对于写操作,每次加行锁(提交事务时才解锁,并且更新数据库的事务版本)。
这样的话就解决了脏读问题,只要事务没提交,数据库的事务版本号就不会更新,那么ReadView中的数据永远都是这个新事务之前的数据。
但是没有解决不可重复读的问题,因为一个事务内每次查询的ReadView版本不一致。
可重复读Repeatable Read
对于读操作,不加锁,只有第一次读取的时候才会生成一个ReadView。 对于写操作,加临键锁Next-key锁(行锁+GAP间隙锁),就是除了给当行记录加锁,还会给当行记录周围区间加间隙锁。
ReadView不同的生成策略解决了不可重复读的问题,由于一个事务内用的都是第一次查询的ReadView,所以查出来的数据都是一致的。
而Next-key锁机制又在一定程度上解决了幻读的问题,由于GAP锁会把一些相邻的区间也锁上,那么插入时就会被阻塞,从而在一定程度上解决了幻读的问题,但是又没有完全解决,因为之后相距比较远的数据还是可以插入。
串行读Serializable
悲观锁机制实现 对于读操作,加读锁。 对于写操作,加写锁。 读读不互斥,读写互斥,写写互斥。 由于读写互斥,完全解决了三个问题,但是并发度比较低。
产生Gap间隙锁的条件
在可重复读事务隔离级别下(该事务隔离级别间隙锁才会生效)
普通索引
一定会产生间隙锁
唯一索引
锁定单行记录
对于指定查询某一条记录的加锁语句,如果该记录不存在,会产生记录锁和间隙锁,如果记录存在,则只会产生记录锁
如:WHERE id = 5 FOR UPDATE;
锁定多行记录
对于查找某一范围内的查询语句,会产生间隙锁
如:WHERE id BETWEEN 5 AND 7 FOR UPDATE;