摘要:线上最近出现了批量的死锁,百思不得姐。死锁记录如下第一反应是批量,的顺序不一样导致的死锁。什么是隐式锁隐式锁的意思就是没有锁在插入记录时,是不加锁的。
线上最近出现了批量insert的死锁,百思不得姐。死锁记录如下
出现问题的批量插入SQL中顺序是一模一样的,在顺序一样的情况下,只会进行插入等待(implicitlock转explicitX锁)下面有实验
如果是因为批量插入顺序不一致带来的死锁日志,打印的结果不是等待插入意向锁(insertintentionwaiting),下面有实验
现在采用一个简化的表,做实验
CREATETABLE`t1`(`id`int(11)NOTNULLAUTO_INCREMENT,`a`varchar(5)NOTNULLDEFAULT"",`b`varchar(5)NOTNULLDEFAULT"",PRIMARYKEY(`id`),UNIQUEKEY`uk_name`(`a`,`b`))ENGINE=InnoDB;实验01在记录不存在的情况下,两个同样顺序的批量insert同时执行,第二个会进行锁等待状态
首先truncatet1;
可以看到目前锁的状态
mysql>select*frominformation_schema.innodb_locks;+-------------+-------------+-----------+-----------+------------+------------+------------+-----------+----------+-----------+|lock_id|lock_trx_id|lock_mode|lock_type|lock_table|lock_index|lock_space|lock_page|lock_rec|lock_data|+-------------+-------------+-----------+-----------+------------+------------+------------+-----------+----------+-----------+|31AE:54:4:2|31AE|S|RECORD|`d1`.`t1`|`uk_name`|54|4|2|"1","1"||31AD:54:4:2|31AD|X|RECORD|`d1`.`t1`|`uk_name`|54|4|2|"1","1"|+-------------+-------------+-----------+-----------+------------+------------+------------+-----------+----------+-----------+在我们执行事务t1的insert时,没有在任何锁的断点处出现,这跟MySQL插入的原理有关系
insert加的是隐式锁。什么是隐式锁?隐式锁的意思就是没有锁
在t1插入记录时,是不加锁的。这个时候事务t1还未提交的情况下,事务t2尝试插入的时候,发现有这条记录,t2尝试获取S锁,会判定记录上的事务id是否活跃,如果活跃的话,说明事务未结束,会帮t1把它的隐式锁提升为显式锁(X锁)
源码如下
t2获取S锁的结果:DB_LOCK_WAIT
实验02批量插入顺序不一致的导致的死锁日志不是等待插入意向锁
写了一个代码去模拟,有很大概率会复现
funtest(){dao.delete()//对应deletefrom//sleepfor10msdao.insert()//对应insertignore}对应的SQL如下,注意是两个事务
begin;deletefromt1wherea="25"commit;begin;INSERTignoreINTO`t1`(`a`,`b`)VALUES("25","1")commit;这个代码在两个线程同时调用的时候,非常容易死锁。
如果标记为删除,说明事务已经提交,还没来得及purge,这时后面的事务加S锁等待;
在源码中打印一些日志。1.在storage/innobase/row/row0ins.c的row_ins_set_shared_rec_lock增加日志,可以看到对唯一索引增加S锁的过程
if(dict_index_is_clust(index)){err=lock_clust_rec_read_check_and_lock(0,block,rec,index,offsets,LOCK_S,type,thr);}else{err=lock_sec_rec_read_check_and_lock(0,block,rec,index,offsets,LOCK_S,type,thr);//增加如下日志fprintf(stderr,"row_ins_set_shared_rec_lock%s%lu%d",index->name,type,err);}2.在lock_rec_enqueue_waiting增加日志,可以看到锁等待的情况
staticenumdb_errlock_rec_enqueue_waiting({ fprintf(stderr,"lock_rec_enqueue_waiting:::::%s%lu",index->name,type_mode);}日志大概如下
row_ins_set_shared_rec_lockuk_name09(t1获取S锁成功)row_ins_set_shared_rec_lockuk_name09(t2获取S锁成功)lock_rec_enqueue_waiting:::::uk_name2563(t1X锁进如锁等待)lock_rec_enqueue_waiting:::::uk_name2563(t2X锁进如锁等待)其中2563=2048+512+3=LOCK_INSERT_INTENTION+LOCK_GAP+LOCK_X
这个过程跟非常经典的三个事务同时insert,一个回滚,剩下的两个事务一个成功,一个死锁,其实是一模一样的原理。
三个insertignore,一个回滚造成的死锁
insert语句都是insertignoreintot1(a,b)values("1","1");以下省略
死锁日志,跟我们案例中的一模一样
一个已提交但是未purge掉的记录会造成后续插入获取S共享锁,两个事务同时获取S锁,然后尝试获取插入意向锁,造成死锁
网上大神梳理的insert流程
首先对插入的间隙加插入意向锁(InsertIntensionLocks)
如果该间隙已被加上了GAP锁或Next-Key锁,则加锁失败进入等待;
如果没有,则加锁成功,表示可以插入;
然后判断插入记录是否有唯一键,如果有,则进行唯一性约束检查
如果不存在相同键值,则完成插入
如果存在相同键值,则判断该键值是否加锁如果没有锁,判断该记录是否被标记为删除
如果标记为删除,说明事务已经提交,还没来得及purge,这时加S锁等待;
如果没有标记删除,则报1062duplicatekey错误;
如果有锁,说明该记录正在处理(新增、删除或更新),且事务还未提交,加S锁等待;
插入记录并对记录加X记录锁;
上面是我debug源码得到的一些结论,如果我的理解有误的话,记得留言告诉我