20 | 幻读是什么,幻读有什么问题?

主键 id ,索引 c,插入6 行数据。begin;  select * from t  where d=5 for update;  commit;

命中 d=5 行,主键 id=5 行加写锁,两阶段锁协议,commit 释放

d 上没索引,全表扫描,不满足的会不会加锁?ps:InnoDB默认可重复读

一、幻读是什么?

其他行的不加锁的话

图 1 假设只在 id=5 这一行加行锁  

Q3 读到 id=1 “幻读”:一个事务两次查询不一样

1.  可重复读,普通查询是快照读,不会看到别的事务。幻读“当前读”才出现

2.  session B 修改结果,A 后的 select 语句用“当前读”看到,不能称为幻读。幻读仅专指“新插入的行”。

如果只从第 8 篇文章《事务到底是隔离的还是不隔离的?》可见性规则分析,三条 SQL结果都没有问题。

for update都是当前读。B 和 C执行后就提交,跟可见性规则并不矛盾。但还有问题。

二、幻读有什么问题?

语义上:A 在 T1 声明d=5 行锁住,这个语义被破坏。加强说明:往  B 、C 里分别加 SQL 

图 2 假设只在 id=5 这一行加行锁 -- 语义被破坏  

B 第二条update t set c=5 where id=0

T1 时刻,没给 id=0 加上锁。 B 在 T2 时刻,可执行两条。session C同理

2.1数据一致性问题

不止内部数据状态,还包含数据和日志逻辑上一致。A 在 T1 加更新:update t set d=100 where d=5

图 3 假设只在 id=5 这一行加行锁 -- 数据一致性问题  

update 加锁语义和 select …for update 一致

1.   T1 变成 (5,5,100), T6 提交;

2.  T2 ,id=0 (0,5,5);    //B提交,写两条

3.  T4 ,(1,5,5);    //C提交写两

4.  其他不变。

binlog 里

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*/

用 binlog 克隆变成 (0,5,100)、(1,5,100) 和 (5,5,100)。数据不一致

数据不一致怎么引入的?

“select * from t where d=5 for update 加锁”导致的。碰到的行,都加写锁:

图 4 假设扫描到的行都被加上了行锁

A 提交后, B 才执行。id=0 最终结果(0,5,5)。binlog 执行序列:

insert into t  values(1,1,5); /*(1,1,5)*/  //T4

update t set c=5  where id=1; /*(1,5,5)*/


update t set d=100 where d=5;/* 所有 d=5 的行,d 改成100*/


update t set d=5  where id=0; /*(0,0,5)*/

update t set c=5  where id=0; /*(0,5,5)*/

id=1 数据库(1,5,5), binlog (1,5,100),幻读没解决。加锁时候,id=1 不存在,加不上锁.

所有都加锁,阻止不了新记录,“幻读”被单拿出解决原因。

三、如何解决幻读?

3.1间隙锁 (Gap Lock): 锁两值空隙

插入6 记录,产生 7 间隙。幻读原因:行锁只锁行,新插入要更新的“间隙”。

图 5 表 t 主键索引上的行锁和间隙锁

执行 select * from t where d=5 for update 加7间隙锁。确保无法再插入新记录。

行锁,分读\写锁,冲突关系:是“另外行锁”。

图 6 两种行锁间的冲突关系  

间隙锁存冲突关系:间隙中插入锁之间不冲突

图 7 间隙锁之间不互锁

B 不会被堵住。t 没有 c=7 ,A 是间隙锁 (5,10)。 B 也是。共同目标,之间不冲突

间隙锁和行锁合称 next-key lock,前开后闭区间。初始化后, select * from t for update锁整表,形成7 next-key lock,分别是 (-∞,0]、(0,5]、(5,10]、(10,15]、(15,20]、(20, 25]、(25,+supremum]。(+∞是开区间。每个索引加不存在最大值 supremum

间隙锁为开区间next-key lock 为前开后闭区间

3.2带来困扰

任意锁一行,不存在插入,存在更新

begin; select * from t  where id=N for update;

/* 如果行不存在*/  insert into t  values(N,N,N);

/* 如果行存在*/  update t set d=N  set id=N;

commit;

insert … on  duplicate key update 就能解决吗?多个唯一键时不行

一旦并发,就死锁。操作前 for update 锁,怎么还有死锁?假设 N=9:

图 8 间隙锁导致的死锁

不需要用到后面的 update 语句,已经形成死锁了。

1.  A select … for update  id=9 不存在,加上间隙锁(5,10)

2.  B select … for update 同加上间隙锁 (5,10),锁之间不冲突

3. B 插入 (9,9,9)被A 间隙锁挡住,等待;

4.  A 插入 (9,9,9),B 间隙锁挡住。

互相等,形成死锁。InnoDB 死锁检测让  A 的 insert 返回。

间隙锁引入,导致同样语句锁更大范围,影响并发度

为解决幻读,简单方法

间隙锁:可重复读隔离级别才会生效。读提交解决不一致,binlog_format=row

读提交隔离级别够用,业务不需可重复读,锁范围更小(没有间隙锁),合理。

都用读提交,逻辑备份时,mysqldump 为什么备份线程设置成可重复读呢?(这个我在前面的文章中已经解释过了,你可以再回顾下第 6 篇文章《全局锁和表锁 :给表加个字段怎么有这么多阻碍?》的内容)

备份时,备份线程用的是可重复读,业务线程是读提交同时存在会不会有问题

不同隔离级别现象有什么不一样?为什么“用读提交就够了”?

小结

所有行都加行锁无法解决幻读,引入间隙锁

间隙锁考虑少。会产生因为间隙锁导致的死锁现象

影响系统并发度,增加锁复杂度

思考题

图 9 事务进入锁等待状态  

B、 C 都等待,原因

预习,session C是有点难度的。下一篇说明。

线上 MySQL 配置的是什么隔离级别,为什么这么配?什么场景,必须可重复读?


varchar 加锁规则:判断间隙和int 一样,排好就有间隙。

A 执行完:begin; select * from t  where d=5 for update; /*Q1*/

B 和C假设不堵住,出现问题:推导A 需锁整个表所有行和间隙

评论1

insert into t values(0,0,0),(5,5,5),

(10,10,10),(15,15,15),(20,20,20),(25,25,25);

运行mysql> begin;

Query OK, 0 rows affected (0.00 sec)

mysql> select * from t where c>=15 and c<=20 order by c desc for update;

c 索引最右包含主键值,(0,0) (5,5) (10,10) (15,15) (20,20) (25,25) 上锁范围要匹配主键值

思考题答案:

上限会扫到c索引(20,20) 上一个键,为了防止c为20 主键值小于25 的行插入,需要锁定(20,20) (25,25) 两者的间隙;开启另一会话(26,25,25)可以插入,而(24,25,25)会被堵塞。

下限会扫描到(15,15)的下一个键也就是(10,10),测试语句会继续扫描一个键就是(5,5) ,此时会锁定,(5,5) 到(15,15)的间隙,由于id是主键不可重复所以下限也是闭区间;

在本例的测试数据中添加(21,25,25)后就可以正常插入(24,25,25)

@ 某、人 、@郭江伟 

你可能感兴趣的:(20 | 幻读是什么,幻读有什么问题?)