主键 id ,索引 c,插入6 行数据。begin; select * from t where d=5 for update; commit;
命中 d=5 行,主键 id=5 行加写锁,两阶段锁协议,commit 释放。
d 上没索引,全表扫描,不满足的会不会加锁?ps:InnoDB默认可重复读
一、幻读是什么?
其他行的不加锁的话
Q3 读到 id=1 “幻读”:一个事务两次查询不一样
1. 可重复读,普通查询是快照读,不会看到别的事务。幻读“当前读”才出现。
2. session B 修改结果,A 后的 select 语句用“当前读”看到,不能称为幻读。幻读仅专指“新插入的行”。
如果只从第 8 篇文章《事务到底是隔离的还是不隔离的?》可见性规则分析,三条 SQL结果都没有问题。
加for update都是当前读。B 和 C执行后就提交,跟可见性规则并不矛盾。但还有问题。
二、幻读有什么问题?
语义上:A 在 T1 声明d=5 行锁住,这个语义被破坏。加强说明:往 B 、C 里分别加 SQL
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
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 加锁”导致的。碰到的行,都加写锁:
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 间隙。幻读原因:行锁只锁行,新插入要更新的“间隙”。
执行 select * from t where d=5 for update 加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:
不需要用到后面的 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 篇文章《全局锁和表锁 :给表加个字段怎么有这么多阻碍?》的内容)
备份时,备份线程用的是可重复读,业务线程是读提交。同时存在会不会有问题?
不同隔离级别现象有什么不一样?为什么“用读提交就够了”?
小结
所有行都加行锁,无法解决幻读,引入间隙锁。
间隙锁考虑少。会产生因为间隙锁导致的死锁现象。
影响系统并发度,增加锁复杂度
思考题
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)
@ 某、人 、@郭江伟