mysql数据库先update后insert死锁分析

Wesley13
• 阅读 1212

先update再insert的并发死锁问题分析。

背景

“如果库里有对应记录,就更新,没有就插入”

很简单的一个逻辑,相信很多人都会遇到。

最近看一个工程里实现代码是这样的,mysql数据库走的是默认的事务级别:可重复读。包在一个事务中执行:

if update更新结果>0
  then return "成功";
else
  insert一条新记录

看起来似乎没什么问题,线上频频出现insert死锁。

这里总结下,分享下实验SQL,便于有兴趣研究的同学去复现。

CREATE TABLE `activity_log`(
  `id`            bigint(20) unsigned NOT NULL AUTO_INCREMENT,
  `user_id`       varchar(16) NOT NULL COMMENT '用户id',
  `activity_id`   varchar(32) NOT NULL COMMENT '活动id',
  `time_count`    int(11)     DEFAULT 0 COMMENT '活动参与次数',
  PRIMARY KEY (`id`),
  UNIQUE KEY `uniq_uid_activity_id` (`user_id`,`activity_id`))ENGINE=InnoDB CHARSET=utf8;

//更新语句
UPDATE activity_log SET time_count=time_count+1 WHERE `user_id`='5' AND `activity_id`='2020';
//插入语句
INSERT INTO activity_log(`user_id`,`activity_id`) VALUES('5','2020');

并发执行现场

mysql数据库先update后insert死锁分析

死锁日志分析

查看最近的死锁日志mysql命令:show engine innodb status

2020-12-04 19:35:01 7f7aeb30c700
*** (1) TRANSACTION:
TRANSACTION 25906, ACTIVE 35 sec inserting 
mysql tables in use 1, locked 1
LOCK WAIT 3 lock struct(s), heap size 360, 2 row lock(s), undo log entries 1
MySQL thread id 5509, OS thread handle 0x7f7b0d6f2700, query id 51970 localhost root update
INSERT INTO activity_log(`user_id`,`activity_id`) VALUES('4','2020')
*** (1) WAITING FOR THIS LOCK TO BE GRANTED:
RECORD LOCKS space id 44 page no 4 n bits 72 index `uniq_uid_activity_id` of table `test`.`activity_log` trx id 25906 lock_mode X insert intention waiting
Record lock, heap no 1 PHYSICAL RECORD: n_fields 1; compact format; info bits 0
 0: len 8; hex 73757072656d756d; asc supremum;;

*** (2) TRANSACTION:
TRANSACTION 25907, ACTIVE 26 sec inserting
mysql tables in use 1, locked 1
3 lock struct(s), heap size 360, 2 row lock(s), undo log entries 1
MySQL thread id 5510, OS thread handle 0x7f7aeb30c700, query id 51971 localhost root update
INSERT INTO activity_log(`user_id`,`activity_id`) VALUES('5','2020')
*** (2) HOLDS THE LOCK(S):
RECORD LOCKS space id 44 page no 4 n bits 72 index `uniq_uid_activity_id` of table `test`.`activity_log` trx id 25907 lock_mode X
Record lock, heap no 1 PHYSICAL RECORD: n_fields 1; compact format; info bits 0
 0: len 8; hex 73757072656d756d; asc supremum;;

*** (2) WAITING FOR THIS LOCK TO BE GRANTED:
RECORD LOCKS space id 44 page no 4 n bits 72 index `uniq_uid_activity_id` of table `test`.`activity_log` trx id 25907 lock_mode X insert intention waiting
Record lock, heap no 1 PHYSICAL RECORD: n_fields 1; compact format; info bits 0
 0: len 8; hex 73757072656d756d; asc supremum;;

*** WE ROLL BACK TRANSACTION (2)
------------

mysql死锁日志中,不同类型锁对应的日志信息如下

  1. 记录锁(LOCK_REC_NOT_GAP): lock_mode X locks rec but not gap
  2. 间隙锁(LOCK_GAP): lock_mode X locks gap before rec
  3. Next-key锁(LOCK_ORNIDARY): lock_mode X
  4. 插入意向锁(LOCK_INSERT_INTENTION): lock_mode X locks gap before rec insert intention

例外情况

如果在supremum record 上加锁,locks gap before rec 会省略掉,间隙锁会显示成 lock_mode X,插入意向锁会显示成 lock_mode X insert intention。譬如上面的

RECORD LOCKS space id 44 page no 4 n bits 72 index `uniq_uid_activity_id` of table `test`.`activity_log` trx id 25907 lock_mode X
Record lock, heap no 1 PHYSICAL RECORD: n_fields 1; compact format; info bits 0
 0: len 8; hex 73757072656d756d; asc supremum;;

看起来像是 Next-key 锁,但是看下面的 heap no 1 表示这个记录是 supremum record(另外.infimum record 的 heap no 为 0),所以这个锁应该看作是一个间隙锁。

在InnoDB存储引擎中,每个数据页中有两个虚拟的行记录,用来界定记录的边界。Infimum 是比该页中任何主键值都要小的值。Supremum 指的是比任何可能打的值还要大的值。这两个值在页创建时被建立,并且任何情况下不会删除。

最终分析

两个事务update不存在的记录,先后获得了间隙锁(gap锁),gap锁之间是兼容的所以在update环节不会阻塞。

二者都持有gap锁,然后去竞争插入意向锁。当存在其他会话持有gap锁的时候,当前会话申请不了插入意向锁,导致死锁。

推荐阅读:

《记一次mysql的insert死锁》 《记一次mysql的update死锁》

mysql数据库先update后insert死锁分析

点赞
收藏
评论区
推荐文章
blmius blmius
3年前
MySQL:[Err] 1292 - Incorrect datetime value: ‘0000-00-00 00:00:00‘ for column ‘CREATE_TIME‘ at row 1
文章目录问题用navicat导入数据时,报错:原因这是因为当前的MySQL不支持datetime为0的情况。解决修改sql\mode:sql\mode:SQLMode定义了MySQL应支持的SQL语法、数据校验等,这样可以更容易地在不同的环境中使用MySQL。全局s
皕杰报表之UUID
​在我们用皕杰报表工具设计填报报表时,如何在新增行里自动增加id呢?能新增整数排序id吗?目前可以在新增行里自动增加id,但只能用uuid函数增加UUID编码,不能新增整数排序id。uuid函数说明:获取一个UUID,可以在填报表中用来创建数据ID语法:uuid()或uuid(sep)参数说明:sep布尔值,生成的uuid中是否包含分隔符'',缺省为
Jacquelyn38 Jacquelyn38
3年前
2020年前端实用代码段,为你的工作保驾护航
有空的时候,自己总结了几个代码段,在开发中也经常使用,谢谢。1、使用解构获取json数据let jsonData  id: 1,status: "OK",data: 'a', 'b';let  id, status, data: number   jsonData;console.log(id, status, number )
Stella981 Stella981
3年前
Python3:sqlalchemy对mysql数据库操作,非sql语句
Python3:sqlalchemy对mysql数据库操作,非sql语句python3authorlizmdatetime2018020110:00:00coding:utf8'''
Stella981 Stella981
3年前
KVM调整cpu和内存
一.修改kvm虚拟机的配置1、virsheditcentos7找到“memory”和“vcpu”标签,将<namecentos7</name<uuid2220a6d1a36a4fbb8523e078b3dfe795</uuid
Easter79 Easter79
3年前
Twitter的分布式自增ID算法snowflake (Java版)
概述分布式系统中,有一些需要使用全局唯一ID的场景,这种时候为了防止ID冲突可以使用36位的UUID,但是UUID有一些缺点,首先他相对比较长,另外UUID一般是无序的。有些时候我们希望能使用一种简单一些的ID,并且希望ID能够按照时间有序生成。而twitter的snowflake解决了这种需求,最初Twitter把存储系统从MySQL迁移
Wesley13 Wesley13
3年前
mysql设置时区
mysql设置时区mysql\_query("SETtime\_zone'8:00'")ordie('时区设置失败,请联系管理员!');中国在东8区所以加8方法二:selectcount(user\_id)asdevice,CONVERT\_TZ(FROM\_UNIXTIME(reg\_time),'08:00','0
Wesley13 Wesley13
3年前
00:Java简单了解
浅谈Java之概述Java是SUN(StanfordUniversityNetwork),斯坦福大学网络公司)1995年推出的一门高级编程语言。Java是一种面向Internet的编程语言。随着Java技术在web方面的不断成熟,已经成为Web应用程序的首选开发语言。Java是简单易学,完全面向对象,安全可靠,与平台无关的编程语言。
Wesley13 Wesley13
3年前
MySQL部分从库上面因为大量的临时表tmp_table造成慢查询
背景描述Time:20190124T00:08:14.70572408:00User@Host:@Id:Schema:sentrymetaLast_errno:0Killed:0Query_time:0.315758Lock_
Python进阶者 Python进阶者
11个月前
Excel中这日期老是出来00:00:00,怎么用Pandas把这个去除
大家好,我是皮皮。一、前言前几天在Python白银交流群【上海新年人】问了一个Pandas数据筛选的问题。问题如下:这日期老是出来00:00:00,怎么把这个去除。二、实现过程后来【论草莓如何成为冻干莓】给了一个思路和代码如下:pd.toexcel之前把这