这几天,在查看文章时,发现了一个Mysql并发的问题,在一开始仅仅凭借眼睛去查看时,并未发现问题及解决方法,于是我们对其进行了具体实际操作和测试:
(想自学习编程的小伙伴请搜索圈T社区,更多行业相关资讯更有行业相关免费视频教程。完全免费哦!)
一、问题
一个事务内:insert记录后根据字段p来update这条记录,然而当出现并发操作的时候,update处会发生dead lock问题,把update改为id,就没事了。
同一个表,高并发事务,事务内先插入一条记录,再更新这条记录:
(1)如果更新的是唯一索引,有异常;
(2)如果更新的是自增主键,就没有异常;
画外音:先不要被“dead lock”描述所迷惑,是死锁问题,阻塞问题,还是其他异常,还另说。
二、测试问题及复现
2.1 数据准备
create table t (id int(20) primary key AUTO_INCREMENT,cell varchar(20) unique)engine=innodb;
新建表: (1)存储引擎是innodb,MySQL版本是5.6; (2)id字段,自增主键; (3)cell字段,唯一索引;
start transaction;
insert into t(cell) values(11111111111);
insert into t(cell) values(22222222222);
insert into t(cell) values(33333333333);
commit;
插入一些测试数据。
2.2 session参数设置
设置事务隔离级别为RR(repeatable read)
--设置手动提交
--设置事务隔离级别为RR
set session autocommit=0;
set session transaction isolation level repeatable read;
2.3 模拟并发
多个终端session模拟并发事务
start TRANSACTION;
INSERT INTO t(cell) VALUES(44444444);
UPDATE t set cell = 123 WHERE cell = 44444444 ;
ROLLBACK;
start TRANSACTION;
INSERT INTO t(cell) VALUES(5555555);
UPDATE t set cell= 456 WHERE cell = 5555555 ;
ROLLBACK;
在Navicat中开启两个窗口
- 窗口A,先启动事务,并插入记录;
- 窗口B,再启动事务,也插入记录;
- 窗口A,修改插入的记录;
- 窗口B,也修改插入的记录;
2.4 结果
奇怪的出现了!
- 当运行到事务1的update时,发生了等待!
- 当运行到事务2的update时,发生了死锁,自动回滚了
三、查询问题
按道理,插入不冲突的记录,然后修改这条记录,行锁不应该冲突呀?唯一索引,主键索引怎么会有差异呢?是否有关?是死锁,还是其他原因?
3.1 根据show engine innodb status查询
百思不得其解,那就先看看innodb status里都有什么吧,复制粘贴下来后查看:
可见Transaction1与Transaction2 同时锁住了同一部分,而且是locak_mode X rec bur not gap Record lock
这就很奇怪了,又不是间隙锁引起的死锁,第一次update为什么会等待呢,第二次update为啥会死锁呢?
不懂,就换个地方看看
3.2 查看innodb_locks表
通过查看information_schema库中inndb_locks表,可看到,确实事务1和事务2,同时锁住了一片数据区域,导致了数据的等待、死锁,但是原因呢?
于是再次换方法查看:
3.3 explain/desc sql语句
咦!发现了重大问题
为什么这里rows居然是6!
我update为什么会扫了全表??
我是加了索引的啊
找到问题了:update没走索引,而是扫了全表!
四、解决问题
既然找到问题了,就看看如何解决,为什么update没有走索引呢?
那我们回头再看看两个update语句
UPDATE t set cell = 123 WHERE cell = 44444444 ;
UPDATE t set cell= 456 WHERE cell = 5555555 ;
看着是没啥问题呀?
寻寻腻腻,冷冷清清,凄凄惨惨戚戚,终于,在查看表时,发现了问题:
回头看建表语句/表结构
create table t (id int(20) primary key AUTO_INCREMENT,cell varchar(20) unique)engine=innodb;
cell字段数据类型是varchar类型的,而我们的update写的是cell = 444444;
并未对数据加引号!而导致了update没走索引,扫了全表
于是,我们再从头看看这个过程:
在事务隔离级别为RR(Repeat Read)下
事务1的insert产生了一个插入意向锁,事务2的insert也产生了一个插入意向锁(不会被互相锁住,因为数据行并不冲突)
此时事务1再进行update语句,因未走索引,导致扫全表,而在扫到事务2插入那条数据时,行锁与插入意向锁冲突了,导致事务1需要等待事务2释放插入意向锁而进行等待。
事务2在进行update时,也同样需要扫全表,但是全表都被事务1的update锁住了,事务2需要等待 等待事务2释放插入意向锁的 事务1 的行锁 释放,因此发生了死锁
那解决方法就很简单了,将语句改为:
UPDATE t set cell = 123 WHERE cell = "44444444" ;
UPDATE t set cell= 456 WHERE cell = "5555555" ;
即可解决死锁/等待问题
五、引伸问题
5.1 RC与RR的比较
5.1.1 表中包含历史数据的测试
其实在进行测试时,也曾经怀疑过是不是因为RR的问题,改成RC试试呢?
--将事务隔离级别改为RC
SET TRANSACTION ISOLATION LEVEL REPEATABLE COMMITTED;
修改后,对其进行相同的操作:
发现:事务1insert,事务2insert,事务1的update生效,事务2的update发生了等待
根据上文中我们找到的问题,对其进行分析:
- 事务1在进行update时,也是扫了全表,但是因为RC没有间隙锁,没有插入意向锁,因此事务1的update不会进行等待
- 事务2在进行update时,需要等待事务1的update提交释放锁,因此发生了等待
得到结论:
RC下不存在间隙锁
5.1.2 表中不包含历史数据的测试(表为空)
对于RC和RR的比较,我们对表中数据采取了删数据的方法继续进行测试:
truncate table
继续对表进行相同操作,结果:
- RR下仍然是事务1等待,事务2死锁
- RC下却是事务1与事务2都正常,未发生等待
究其原因,RR下,事务1插入的数据,事务2能看到,因此在RR下,即使数据清空,事务1仍然锁住了事务2插入的数据。
而在RC下,事务1插入的数据事务2看不到,事务2插入的数据事务1看不到,他们各自仅仅锁住了自己插入的数据,因此能执行成功。
5.1.3 结论
抛开可重复读和读已提交他们在同一个事务中多次读可读出的东西外,此次发现了他们其他的不同:
- RC中不存在间隙锁、同样不存在属于间隙锁的插入意向锁
- RR下,事务1插入的数据事务2 能看到,RC下事务1插入的数据事务2看不到
5.2 Mysql中间隙锁与插入意向锁
在此次过程中,我们发现了: 行锁、间隙锁、插入意向锁。其中因不同的行为产生了不同的锁,而其意义、用处也是不同的:
5.2.1 间隙锁(Gap Locks)
- 区间锁, 仅仅锁住一个索引区间(开区间,不包括双端端点)。
- 在索引记录之间的间隙中加锁,或者是在某一条索引记录之前或者之后加锁,并不包括该索引记录本身。比如在 1、2、3中,间隙锁的可能值有 (∞, 1),(1, 2),(2, ∞)。
- 间隙锁可用于防止幻读,保证索引间的不会被插入数据
5.2.2 插入意向锁(Insert Intention Locks)
- 插入意向锁是一种Gap锁,不是意向锁,在insert操作时产生。
- 在多事务同时写入不同数据至同一索引间隙的时候,并不需要等待其他事务完成,不会发生锁等待。
- 假设有一个记录索引包含键值4和7,不同的事务分别插入5和6,每个事务都会产生一个加在4-7之间的插入意向锁,获取在插入行上的排它锁,但是不会被互相锁住,因为数据行并不冲突。
- 插入意向锁不会阻止任何锁,对于插入的记录会持有一个记录锁。
5.2.3 锁的选择
在我们处理sql语句去执行时,不同的语句会选择不同的锁:
-
如果更新条件没有走索引,例如执行”update test set name=“hello” where name=“world”;” ,此时会进行全表扫描,扫表的时候,要阻止其他任何的更新操作,所以上升为表锁。
-
如果更新条件为索引字段,但是并非唯一索引(包括主键索引),例如执行“update test set name=“hello” where code=9;” 那么此时更新会使用Next-Key Lock。使用Next-Key Lock的原因:
-
首先要保证在符合条件的记录上加上排他锁,会锁定当前非唯一索引和对应的主键索引的值;
-
还要保证锁定的区间不能插入新的数据。
-
如果更新条件为唯一索引,则使用Record Lock(记录锁)。