首先我们创建一个表,并插入测试数据:
CREATE TABLE `t` ( `id` int(11) NOT NULL, `c` int(11) DEFAULT NULL, `d` int(11) DEFAULT NULL, PRIMARY KEY (`id`), KEY `c` (`c`) ) ENGINE=InnoDB; insert into t values(0,0,0),(5,5,5), (10,10,10),(15,15,15),(20,20,20),(25,25,25);
什么是幻读?
- 在可重复读隔离级别下,普通的快照读诗不会看到别的事务插入的数据的,因此,幻读只在“当前读”下才会出现
- 幻读专指新插入的行。修改的数据被其他事务的“当前读”看到,不能称为幻读。
幻读有什么问题?
语义:
如图:T1时刻, sessionA的语言是将所有d=5的数据,加行锁。T2时刻,SessionB将id=0这条数据改成了d=5,此时有两条 d=5的数据,但是由于第二条数据并没有加锁,可以对其进行修改。这就破坏了sessionA里的Q1语句要锁住所有d=5的行的加锁声明。
数据一致性:
数据一致性不仅指数据库内部数据状态的一致性,还包含了数据和日志在逻辑上的一致性
T6时刻后,数据库中的数据为:
(5,5,100)
(0,5,5)
(1,5,5)
而bin log里的内容如下:
update t set d=5 where id=0; /*(0,0,5)*/ update t set c=5 where id=0; /*(0,5,5)*/ insert into t values(1,1,5); /*(1,1,5)*/ update t set c=5 where id=1; /*(1,5,5)*/ update t set d=100 where d=5;/* 所有 d=5 的行,d 改成 100*/
可以看到id=0和id=1这两行,发生了数据不一致。
可以看到id=0和id=1这两行,发生了数据不一致。
InnoDB如何解决幻读问题?
为了解决幻读的问题,InnoDB引入了新的锁——间隙锁(Gap Lock)
即将两个值之间的空隙进行加锁,比如表t,初始化插入了6个记录,这就会产生7个空隙
此时,我们执行
select * from t where d = 5 for update;
的时候,就不仅给这已有的6个记录加上了行锁,同时加了7个间隙锁,这样就保证了无法再插入新的记录。
注意:
对于非索引字段进行update或select..for update操作的时候,代价极高(上述SQL)。所有记录上锁,已经所有间隙加锁
对于索引字段执行上述操作,只有字段本身一级附近的间隙会被加锁。
间隙锁存在冲突,是“往这个间隙中插入一个记录”这个操作。
思考一下这是为什么呢?
InnoDB索引是一颗B+树,同一级上的节点是顺序排列的,所以只需要锁住字段本身和附近间隙即可,因为后插入的数据位置是固定的。非索引字段则不具备这样的结构,所以只能采用全表扫描的方式加锁。
例如:
这里由于表中没有c=7这条记录,并且 c是索引字段,所以SessionA会在(5,10)这个间隙上加锁。而SessionB也是在这个间隙进行加锁,这两个锁的目的都是为了保护这个间隙不被插入数据,所以他们之间是不冲突的。
间隙锁和行锁合城 next-key lock。每个next-key lock都是前开后闭区间。
间隙锁带来的问题:
例如:
这是一个间隙锁导致死锁的问题。
- SessionA执行更新语句后,由于id=9这一行数据不存在,所以会在(5,10)上加上间隙锁。
- SessionB同理,两个间隙锁不会冲突。
- SessionB试图插入一行数据(0,9,9),被SessionA的间隙锁挡住,进入等待。
- SessionA试图插入一行数据(0,9,9),被SessionB的间隙锁挡住了。
- 循环等待,形成死锁。
间隙锁的引入,可能会导致同样的语句锁住更大的范围,这其实是影响了并发度的。
那么有没有简单的方式,去解决幻读呢?
可以把数据库的隔离级别设置为读提交,再将bin log的格式设置为row,这样可以解决数据和日志不一致的问题。如果我们的业务需求,用读提交就够了,不用可重复度。可以采用这种方法进行配置。