InnoDB如何解决幻读?
一致性读
一致性非锁定读(Consistent Nonlocking Reads)
一致性不锁定读(Consistent Nonlocking Reads)是InnoDB使用多版本控制(MVCC)来读取某个时间点创建的快照。这个请求只能看到这个时间点之前提交的事物的修改,
看不到之后的或者未提交的事务的修改。如下图:
快照读:读取的是快照版本,也就是历史版本。普通的SELECT就是快照读
当前读:读取的是最新版本。UPDATE、DELETE、INSERT、SELECT … LOCK IN SHARE MODE、SELECT … FOR UPDATE是当前读。
什么是幻读?
The so-called phantom problem occurs within a transaction when the same query produces different sets of rows at different times. For example, if aSELECTis executed twice, but returns a row the second time that was not returned the first time, the row is a “phantom” row.
事务A 按照一定条件进行数据读取, 期间事务B 插入了相同搜索条件的新数据,事务A再次按照原先条件进行读取时,发现了事务B 新插入的数据 称为幻读
the exception to this rule is that the query sees the changes made by earlier statements within the same transaction. This exception causes the following anomaly: If you update some rows in a table, aSELECTsees the latest version of the updated rows, but it might also see older versions of any rows. If other sessions simultaneously update the same table, the anomaly means that you might see the table in a state that never existed in the database.
按照规则,在同一个事务中修改的内容,在之后的查询中应该可以看到。但是如果其他事务对同样的内容做了修改,那之后的查询就会看到本不应该看到的数据。
创建表,并插入数据:
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);
sessionAsessionB
T1begin;
T2select * from t where d=5; ####result: (5,5,5)
T3 insert into t value(1,1,5)
T4update t set d=100 where d=5;
T5select * from t where d=5; ### result: (1,1,100) (5,5,100)
因为sessionA中的每一条查询语句都加了for update 排他锁,所以都是当前读。读到的都是数据库中最新的数据。
T2时刻查询d=5,查出来一条记录(5,5,5)
T3时刻sessionC 插入了一条记录(1,1,5)
T4时刻,sessionA更新了d=5的行
T5时刻,再次查询d=5,得到两个结果 (1,100,5) (5,100,5)
sessionA在一次事务中得到了两个不同的结果,出现了幻读
间隙锁
间隙锁顾名思义,锁的是两个索引记录的间隙,或者是第一个索引记录之前或者最后一个索引之后的间隙。
SELECT c1 FROM t WHERE c1 BETWEEN 10 and 20 FOR UPDATE;
无论c1=15这个值是否在表中存在,其他的事务都不可以对t.c1=15这个值进行插入操作。因为10到20这个间隙(gap)被加锁了。
间隙锁之间并不冲突,一个事务在一个间隙加了间隙排他锁,其他事务同样可以在该间隙加排他锁或者共享锁
如果是唯一索引的等值查询,间隙锁会退化成行锁
Next-Key Locks
next-key lock是行锁和间隙锁的结合。
InnoDBperforms row-level locking in such a way that when it searches or scans a table index, it sets shared or exclusive locks on the index records it encounters. Thus, the row-level locks are actually index-record locks. A next-key lock on an index record also affects the “gap” before that index record. That is, a next-key lock is an index-record lock plus a gap lock on the gap preceding the index record. If one session has a shared or exclusive lock on recordRin an index, another session cannot insert a new index record in the gap immediately beforeRin the index order.
加锁规则:
next-key lock 是前开后闭区间。
查找过程中访问到的对象才会加锁。
索引上的等值查询,给唯一索引加锁的时候,next-key lock 退化为行锁。
索引上的等值查询,向右遍历时且最后一个值不满足等值条件的时候,next-key lock 退化为间隙锁。
唯一索引上的范围查询会访问到不满足条件的第一个值为止。
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);
场景一:等值查询的间隙锁
由于表 t 中没有 id=7 的记录
加锁单位是 next-key lock,session A 加锁范围就是 (5,10];
这是一个等值查询 (id=7),而 id=10 不满足查询条件,next-key lock 退化成间隙锁,因此最终加锁的范围是 (5,10)。所以,session B 要往这个间隙里面插入 id=8 的记录会被锁住,但是 session C 修改 id=10 这行是可以的。
场景二:非唯一索引等值锁
这里 session A 要给索引 c 上 c=5 的这一行加上读锁。
加锁单位是 next-key lock,因此会给 (0,5]加上 next-key lock。
c 是普通索引,因此仅访问 c=5 这一条记录是不能马上停下来的,需要向右遍历,查到 c=10 才放弃。访问到的都要加锁,因此要给 (5,10]加 next-key lock。
同时这个符合: 等值判断,向右遍历,最后一个值不满足 c=5 这个等值条件,因此退化成间隙锁 (5,10)。
只有访问到的对象才会加锁,这个查询使用覆盖索引,并不需要访问主键索引,所以主键索引上没有加任何锁,这就是为什么 session B 的 update 语句可以执行完成.。
lock in share mode 只锁覆盖索引,但是如果是 for update 就不一样了。 执行 for update 时,系统会认为你接下来要更新数据,因此会顺便给主键索引上满足条件的行加上行锁。
场景三:主键索引范围锁
开始执行的时候,要找到第一个 id=10 的行,因此本该是 next-key lock(5,10]。 主键 id 上的等值条件,退化成行锁,只加了 id=10 这一行的行锁。
范围查找就往后继续找,找到 id=15 这一行停下来,因此需要加 next-key lock(10,15]。
场景四:非唯一索引范围锁
在第一次用 c=10 定位记录的时候,索引 c 上加了 (5,10]这个 next-key lock 。
由于索引 c 是非唯一索引,也就是说不会蜕变为行锁,因此最终 sesion A 加的锁是,索引 c 上的 (5,10] 和 (10,15] 这两个 next-key lock。
场景五:唯一索引范围锁 bug
session A 是一个范围查询,按照原则 1 的话,应该是索引 id 上只加 (10,15]这个 next-key lock,并且因为 id 是唯一键,所以循环判断到 id=15 这一行就应该停止了。
但是实现上,InnoDB 会往前扫描到第一个不满足条件的行为止,也就是 id=20。而且由于这是个范围扫描,因此索引 id 上的 (15,20]这个 next-key lock 也会被锁上。所以你看到了,session B 要更新 id=20 这一行,是会被锁住的。同样地,session C 要插入 id=16 的一行,也会被锁住。照理说,这里锁住 id=20 这一行的行为,其实是没有必要的。因为扫描到 id=15,就可以确定不用往后再找了。但实现上还是这么做了。
场景六:非唯一索引上存在"等值"的例子
给表插入一条新的记录
mysql> insert into t values(30,10,30);
表里现在有两条c=10的记录。但是因为这两行的主键id不同,因此,这两个c=10的记录之间,也是有间隙的
session A 在遍历的时候,先访问第一个 c=10 的记录。同样地,这里加的是 (c=5,id=5) 到 (c=10,id=10) 这个 next-key lock。
session A 向右查找,直到碰到 (c=15,id=15) 这一行,循环才结束。这是一个等值查询,向右查找到了不满足条件的行,所以会退化成 (c=10,id=10) 到 (c=15,id=15) 的间隙锁。
场景七:limit 语句加锁
session A 的 delete 语句加了 limit 2。你知道表 t 里 c=10 的记录其实只有两条,因此加不加 limit 2,删除的效果都是一样的,但是加锁的效果却不同。可以看到,session B 的 insert 语句执行通过了,跟场景六的结果不同。
这是因为,这里里的 delete 语句明确加了 limit 2 的限制,因此在遍历到 (c=10, id=30) 这一行之后,满足条件的语句已经有两条,循环就结束了。
索引 c 上的加锁范围就变成了从(c=5,id=5) 到(c=10,id=30) 这个前开后闭区间
在删除数据的时候尽量加 limit。这样不仅可以控制删除数据的条数,让操作更安全,还可以减小加锁的范围。
场景八:一个死锁的例子
session A 启动事务后执行查询语句加 lock in share mode,在索引 c 上加了 next-key lock(5,10] 和间隙锁 (10,15);
session B 的 update 语句也要在索引 c 上加 next-key lock(5,10] ,进入锁等待;
然后 session A 要再插入 (8,8,8) 这一行,被 session B 的间隙锁锁住。由于出现了死锁,InnoDB 让 session B 回滚。
session B 的“加 next-key lock(5,10] ”操作,实际上分成了两步,先是加 (5,10) 的间隙锁,加锁成功;然后加 c=10 的行锁,这时候才被锁住的。
在RR的隔离级别下,Innodb使用MVCC和next-key locks解决幻读,MVCC解决的是普通读(快照读)的幻读,next-key locks解决的是当前读情况下的幻读。