记一次Mysql并发"死锁",引出的问题及讨论

这几天,在查看文章时,发现了一个Mysql并发的问题,在一开始仅仅凭借眼睛去查看时,并未发现问题及解决方法,因而咱们对其进行了具体实际操做和测试:

1、问题

一个事务内:insert记录后根据字段p来update这条记录,然而当出现并发操做的时候,update处会发生dead lock问题,把update改成id,就没事了。sql

同一个表,高并发事务,事务内先插入一条记录,再更新这条记录: (1)若是更新的是惟一索引,有异常; (2)若是更新的是自增主键,就没有异常; 画外音:先不要被“dead lock”描述所迷惑,是死锁问题,阻塞问题,仍是其余异常,还另说。session

2、测试问题及复现

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模拟并发事务 ui

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中开启两个窗口spa

  1. 窗口A,先启动事务,并插入记录;
  2. 窗口B,再启动事务,也插入记录;
  3. 窗口A,修改插入的记录;
  4. 窗口B,也修改插入的记录;

2.4 结果

奇怪的出现了!code

  • 当运行到事务1的update时,发生了等待!
  • 当运行到事务2的update时,发生了死锁,自动回滚了

3、查询问题

按道理,插入不冲突的记录,而后修改这条记录,行锁不该该冲突呀?
惟一索引,主键索引怎么会有差别呢?是否有关?是死锁,仍是其余缘由?
orm

3.1 根据show engine innodb status查询

百思不得其解,那就先看看innodb status里都有什么吧,复制粘贴下来后查看: cdn

可见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没走索引,而是扫了全表!

4、解决问题

既然找到问题了,就看看如何解决,为何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、引申问题

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 结论

抛开可重复读和读已提交他们在同一个事务中屡次读可读出的东西外,这次发现了他们其余的不一样:

  1. RC中不存在间隙锁、一样不存在属于间隙锁的插入意向锁
  2. RR下,事务1插入的数据事务2 能看到,RC下事务1插入的数据事务2看不到

5.2 Mysql中间隙锁与插入意向锁

在这次过程当中,咱们发现了: 行锁、间隙锁、插入意向锁。其中因不一样的行为产生了不一样的锁,而其意义、用处也是不一样的:

5.2.1 间隙锁(Gap Locks)

  1. 区间锁, 仅仅锁住一个索引区间(开区间,不包括双端端点)。
  2. 在索引记录之间的间隙中加锁,或者是在某一条索引记录以前或者以后加锁,并不包括该索引记录自己。好比在 一、二、3中,间隙锁的可能值有 (∞, 1),(1, 2),(2, ∞)。
  3. 间隙锁可用于防止幻读,保证索引间的不会被插入数据

5.2.2 插入意向锁(Insert Intention Locks)

  1. 插入意向锁是一种Gap锁,不是意向锁,在insert操做时产生。
  2. 在多事务同时写入不一样数据至同一索引间隙的时候,并不须要等待其余事务完成,不会发生锁等待。
  3. 假设有一个记录索引包含键值4和7,不一样的事务分别插入5和6,每一个事务都会产生一个加在4-7之间的插入意向锁,获取在插入行上的排它锁,可是不会被互相锁住,由于数据行并不冲突。
  4. 插入意向锁不会阻止任何锁,对于插入的记录会持有一个记录锁。

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的缘由:

  1. 首先要保证在符合条件的记录上加上排他锁,会锁定当前非惟一索引和对应的主键索引的值;

  2. 还要保证锁定的区间不能插入新的数据。

  3. 若是更新条件为惟一索引,则使用Record Lock(记录锁)。

相关文章
相关标签/搜索