本人原文地址:记一次Mysql并发"死锁",引出的问题及讨论
这几天,在查看文章时,发现了一个Mysql并发的问题,在一开始仅仅凭借眼睛去查看时,并未发现问题及解决方法,于是我们对其进行了具体实际操作和测试:
一个事务内:insert记录后根据字段p来update这条记录,然而当出现并发操作的时候,update处会发生dead lock问题,把update改为id,就没事了。
同一个表,高并发事务,事务内先插入一条记录,再更新这条记录:
(1)如果更新的是唯一索引,有异常;
(2)如果更新的是自增主键,就没有异常;
画外音:先不要被“dead lock”描述所迷惑,是死锁问题,阻塞问题,还是其他异常,还另说。
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;
插入一些测试数据。
设置事务隔离级别为RR(repeatable read)
--设置手动提交
--设置事务隔离级别为RR
set session autocommit=0;
set session transaction isolation level repeatable read;
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中开启两个窗口
奇怪的出现了!
按道理,插入不冲突的记录,然后修改这条记录,行锁不应该冲突呀?
唯一索引,主键索引怎么会有差异呢?是否有关?是死锁,还是其他原因?
百思不得其解,那就先看看innodb status里都有什么吧,复制粘贴下来后查看:
可见Transaction1与Transaction2 同时锁住了同一部分,而且是locak_mode X rec bur not gap Record lock
这就很奇怪了,又不是间隙锁引起的死锁,第一次update为什么会等待呢,第二次update为啥会死锁呢?
不懂,就换个地方看看
通过查看information_schema库中inndb_locks表,可看到,确实事务1和事务2,同时锁住了一片数据区域,导致了数据的等待、死锁,但是原因呢?
于是再次换方法查看:
咦!发现了重大问题
为什么这里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" ;
即可解决死锁/等待问题
其实在进行测试时,也曾经怀疑过是不是因为RR的问题,改成RC试试呢?
--将事务隔离级别改为RC
SET TRANSACTION ISOLATION LEVEL REPEATABLE COMMITTED;
修改后,对其进行相同的操作:
发现:事务1insert,事务2insert,事务1的update生效,事务2的update发生了等待
根据上文中我们找到的问题,对其进行分析:
得到结论:
RC下不存在间隙锁
对于RC和RR的比较,我们对表中数据采取了删数据的方法继续进行测试:
truncate table
继续对表进行相同操作,结果:
究其原因,RR下,事务1插入的数据,事务2能看到,因此在RR下,即使数据清空,事务1仍然锁住了事务2插入的数据。
而在RC下,事务1插入的数据事务2看不到,事务2插入的数据事务1看不到,他们各自仅仅锁住了自己插入的数据,因此能执行成功。
抛开可重复读和读已提交他们在同一个事务中多次读可读出的东西外,此次发现了他们其他的不同:
在此次过程中,我们发现了: 行锁、间隙锁、插入意向锁。其中因不同的行为产生了不同的锁,而其意义、用处也是不同的:
在我们处理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(记录锁)。