幻读
同一个事务中执行两次相同的查询语句,结果集条数可能不一致。即幻读。
比如查询 name=1的记录,其他事务可能会插入新记录或修改其他记录使name=1的记录数变多或变少。在读未提交和读已提交的隔离级别下会出现。
在可重复读的隔离级别下,快照读不会出现幻读,当前读时,innodb 通过间隙锁和行锁共同作用控制,使得查询语句扫描到的数据及数据中间不能插入也不允许修改,从而不会出现幻读现象。
间隙锁
比如表中存在 id = 1,5,10的记录,执行 select * from table where id = 3 for update;
如果id上没有索引,会全表扫描,在此过程中,会给扫描到的每一行及行间隙加上行锁和间隙锁,阻止任何插入和更新。
如果使用当前读,在一行行扫描的过程中,不仅将给行加上了行锁,还给行两边的空隙,也加上了间隙锁。以此来防止其他事务在扫描期间插入,更新,删除数据。间隙锁与行锁合称为next-key lock,每个next-key lock是前开后闭区间。
(1,5】,(5,10】,(10,supremum】supremum是索引上一个不存在的最大值
行锁与行锁之间会发生冲突,但间隙锁与间隙锁之间不会,这点有点类似lock in share mode ,跟间隙锁存在冲突关系的,是“往这个间隙中插入一个记录”这个操
作(插入操作会插入意向锁,意向锁与间隙锁冲突,从而阻止插入)。
innodb通过间隙锁解决了,当前读的幻读问题,但是间隙锁的引入会对事务并发产生影响。原因在于间隙锁与间隙锁之间不会发生冲突,像共享锁一样,两个事务可以持有同样的间隙锁,此时,所有事务再执行被锁的相关的数据的操作时,就会发生死锁。
举个业务中常见的例子:
任意锁住一行或多行,如果这些行不存在的话就插入,如果存在这些行就更新它的数据。操作如下:
begin;
select * from t where id in (a,b,c) for update;
/如果行不存在/
insert into t values(N,N,N);
/如果行存在/
update t set d=N set id=N;
commit;
这种情况下如果存在并发操作时,如果再第一个事务A上锁后,插入或者更新前,别的事务 B或C 是可以继续对这些记录上锁的, 事务B或C上锁成功后,所有事物都无法对这些记录执行插入或更新。程序出现死锁。
因此在选择数据库的隔离级别时,要注意权衡性能和并发性,根据实际情况考虑是否需要使用间隙锁,大多数情况下使用 read committed 隔离级别就足够了,对很多应用程序来说,幻读也不是什么大问题。