MySql中的事务隔离机制,可重复读并未完全解决幻读

281 阅读4分钟

MySql从5.5.5版本后默认采用InnoDB作为存储引擎,网上很多文章说InnoDB的可重复读(RR)可以避免幻读,但实际上它只能避免读过程中发生的幻读,不能解决写过程中的幻读。

不可重复读与幻读

不可重复读的重点是在于当前数据行的修改,指的是同一个事务前后读取同一个数据得到的结果不同;

幻读是针对于数据行的新增,指的是同一个事务前后读取同一个数据得到数据量不同。

InnoDB默认采用可重复读(RR)的隔离级别,在Sql标准定义的隔离级别中,RR可以避免脏读和不可重复读,但不能避免幻读。网上有很多文章说InnoDB在这一级别也解决了幻读的问题,但实际上它只解决了一部分。

示例

首先创建了一张仅包含id与name的表。

CREATE TABLE `test` (
  `id` int(10) NOT NULL AUTO_INCREMENT,
  `name` varchar(100) DEFAULT NULL,
  PRIMARY KEY (`id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8;

同时开启两个事务。注意,begin并不能显示开启一个事务,只有在begin之后的增删查改语句才会真正地开启一个事务。

事务1: select * from test;

此时,数据表为空,显然得到的结果为null。

事务2:insert into test(name) values("zhangsan");往数据表中插入一条数据,然后执行commit。事务2执行完毕。

此时,事务1再次执行之前的select。得到的结果仍然为null,说明并没有发生幻读。

这也就是许多文章中所说的,InnoDB的RR级别解决了幻读这一问题。但此时如果事务1执行update会发生什么情况?

事务1: update test set name="liqiang";

可以看到,update语句,1 row affected,明明同一事务中前面的select为null,update却影响了一条数据,很显然,此时就发生了幻读。此时,如果我们再执行一次select,就会得到这次update的结果。

所以说,InnoDB的RR级别并没能完全解决了幻读问题,想要完全解决幻读问题还需要开启Serializable隔离级别。

原因

快照读-select

InnoDB的RR中对select采用的是不加锁的形式,采用快照读的方法来解决读的幻读问题。也就是说,同一个事务中,InnoDB会将第一次的select结果存储下来,之后再执行select就会直接采用这次快照读的数据,从而避免了部分幻读的发生。

当前读-update,insert,delete

为了确保事务的一致性,InnoDB在执行这三个操作的时候会对数据库中当前的最新数据做一次查找,确保修改的数据与数据库中的数据保持一致。并且,在执行这三个操作的时候,InnoDB会对数据表相应行的索引进行加锁,确保在自己执行完毕并提交之前,别的事务不能对该数据表进行操作,需要等当前事务执行完毕后获得了锁才能执行。

InnoDB采用的锁是Next-key锁(行级锁+gap锁),该锁会对数据操作的行的索引加锁,防止不可重复读,即别的事务无法对该行数据进行修改。另外,Next-key锁还会给数据表加gap锁,在针对索引(辅助索引)进行操作时,gap锁仅锁住索引的B+树上该索引的两侧间隙加上gap锁,部分数据行的增删查需要该事务释放gap锁,但不会影响表中所有数据的增删查,避免发生“当前读”的幻读,在针对非索引内容进行操作时,行级锁会对聚簇索引加锁,gap锁会对整张表的间隙进行加锁,相当于是给整张表加了锁。如果加的是共享锁的话,整张表仅能进行select操作,当然,此时的select只会读取到事务未提交的内容,增删查都需要等待该事务释放锁。锁等待超时,sql执行就会失败。

注:对聚簇索引或唯一索引(非聚簇)进行写操作时,仅需要对该行加锁,不需要加gap锁。

另外,快照读虽然保证了不会在select的时候发生幻读等并发问题,但是快照读可能导致读到不及时的消息,从而引发别的问题。可以通过对select加锁的方式来避免这种问题,加上共享锁或者互斥锁都会采用当前读的形式。

select * from table where ? lock in share mode;//共享锁

关于MySql中不同情况下加锁的情况可以参见:zhuanlan.zhihu.com/p/31875702