MySQL死锁:两次获取间隙锁的upsert查询?

如何解决MySQL死锁:两次获取间隙锁的upsert查询?

从查看代码来看,我似乎不可能尝试从 2 个不同的事务中插入相同的数据。

一个常见的错误是

BEGIN;
SELECT ... WHERE ...;   -- fetch the row to see if it there
IF not there THEN
    INSERT ...;  -- since it was not there
ENDIF
COMMIT;

解决方法是更改选择:

SELECT ... WHERE ... FOR UPDATE;

这会在 处获取一个锁SELECT,在那里它可以停止而不是死锁。(也就是说,innodb_lock_wait_time开始。几乎总是导致两个查询静默进行——第一个是成功的;第二个说“哎呀,那行存在”,并按照你的逻辑为这种情况做。

要考虑的另一件事:拥有两个UNIQUE键(一个是PRIMARY KEY)会增加许多事情的复杂性(插入、死锁等)。你能摆脱id并简单地使用这对(Name, TeamId)吗?(这可能涉及几个应用程序代码更改。)

  • 澄清一下,你是说我的代码中还有其他地方使用 SELECT 而没有“FOR UPDATE”?因为在这个特定的事务中,我没有使用 SELECT 语句的地方。如上所述,事务使用 2 个查询——第一个是插入到不同的表,第二个是这个 upsert 查询。即使有,它也不会先选择然后再插入,因为这是通过这个 upsert 查询本身完成的。

解决方法

我正在尝试调试一个让我困惑了一段时间的死锁场景。我想知道是否有人可以阐明这是如何发生的。

这是死锁输出:

------------------------
LATEST DETECTED DEADLOCK
------------------------
2019-06-28 14:04:18 0x7fe9a811d700
*** (1) TRANSACTION:
TRANSACTION 3141,ACTIVE 1 sec inserting
mysql tables in use 1,locked 1
LOCK WAIT 4 lock struct(s),heap size 1136,2 row lock(s),undo log entries 2
MySQL thread id 47,OS thread handle 140641311893248,query id 3281 172.17.0.1 mmuser update
INSERT INTO
                            PublicChannels(Id,DeleteAt,TeamId,DisplayName,Name,Header,Purpose)
                        VALUES
                            (?,?,?)
                        ON DUPLICATE KEY UPDATE
                            DeleteAt = ?,TeamId = ?,DisplayName = ?,Name = ?,Header = ?,Purpose = ?
*** (1) WAITING FOR THIS LOCK TO BE GRANTED:
RECORD LOCKS space id 76 page no 4 n bits 104 index Name of table `mydb`.`PublicChannels` trx id 3141 lock_mode X locks gap before rec insert intention waiting
Record lock,heap no 38 PHYSICAL RECORD: n_fields 3; compact format; info bits 0
 0: len 30; hex 6e656365737369746174696275732d697073612d6c6f6164746573746368; asc necessitatibus-ipsa-loadtestch; (total 38 bytes);
 1: len 26; hex 776e6b6969666d6d7574723337796f6437336d7031686b616377; asc wnkiifmmutr37yod73mp1hkacw;;
 2: len 26; hex 3875316d626365397a62677366677831627263656578356f6f6f; asc 8u1mbce9zbgsfgx1brceex5ooo;;
*** (2) TRANSACTION:
TRANSACTION 3140,locked 1
5 lock struct(s),3 row lock(s),undo log entries 2
MySQL thread id 50,OS thread handle 140641523848960,query id 3226 172.17.0.1 mmuser update
INSERT INTO
                            PublicChannels(Id,Purpose = ?
*** (2) HOLDS THE LOCK(S):
RECORD LOCKS space id 76 page no 4 n bits 104 index Name of table `mydb`.`PublicChannels` trx id 3140 lock_mode X locks gap before rec
Record lock,heap no 38 PHYSICAL RECORD: n_fields 3; compact format; info bits 0
 0: len 30; hex 6e656365737369746174696275732d697073612d6c6f6164746573746368; asc necessitatibus-ipsa-loadtestch; (total 38 bytes);
 1: len 26; hex 776e6b6969666d6d7574723337796f6437336d7031686b616377; asc wnkiifmmutr37yod73mp1hkacw;;
 2: len 26; hex 3875316d626365397a62677366677831627263656578356f6f6f; asc 8u1mbce9zbgsfgx1brceex5ooo;;

*** (2) WAITING FOR THIS LOCK TO BE GRANTED:
RECORD LOCKS space id 76 page no 4 n bits 104 index Name of table `mydb`.`PublicChannels` trx id 3140 lock_mode X locks gap before rec insert intention waiting
Record lock,heap no 38 PHYSICAL RECORD: n_fields 3; compact format; info bits 0
 0: len 30; hex 6e656365737369746174696275732d697073612d6c6f6164746573746368; asc necessitatibus-ipsa-loadtestch; (total 38 bytes);
 1: len 26; hex 776e6b6969666d6d7574723337796f6437336d7031686b616377; asc wnkiifmmutr37yod73mp1hkacw;;
 2: len 26; hex 3875316d626365397a62677366677831627263656578356f6f6f; asc 8u1mbce9zbgsfgx1brceex5ooo;;

*** WE ROLL BACK TRANSACTION (1)

这是表架构和索引:

mysql> describe PublicChannels;                                                                                                                                                                                    +-------------+--------------+------+-----+---------+-------+
| Field       | Type         | Null | Key | Default | Extra |
+-------------+--------------+------+-----+---------+-------+
| Id          | varchar(26)  | NO   | PRI | NULL    |       |
| DeleteAt    | bigint(20)   | YES  | MUL | NULL    |       |
| TeamId      | varchar(26)  | YES  | MUL | NULL    |       |
| DisplayName | varchar(64)  | YES  |     | NULL    |       |
| Name        | varchar(64)  | YES  | MUL | NULL    |       |
| Header      | text         | YES  |     | NULL    |       |
| Purpose     | varchar(250) | YES  |     | NULL    |       |
+-------------+--------------+------+-----+---------+-------+
7 rows in set (0.00 sec)

mysql> show INDEX FROM PublicChannels;                                                                                                                                                                             +----------------+------------+-------------------------------+--------------+-------------+-----------+-------------+----------+--------+------+------------+---------+---------------+
| Table          | Non_unique | Key_name                      | Seq_in_index | Column_name | Collation | Cardinality | Sub_part | Packed | Null | Index_type | Comment | Index_comment |
+----------------+------------+-------------------------------+--------------+-------------+-----------+-------------+----------+--------+------+------------+---------+---------------+
| PublicChannels |          0 | PRIMARY                       |            1 | Id          | A         |           1 |     NULL | NULL   |      | BTREE      |         |               |
| PublicChannels |          0 | Name                          |            1 | Name        | A         |           1 |     NULL | NULL   | YES  | BTREE      |         |               |
| PublicChannels |          0 | Name                          |            2 | TeamId      | A         |           1 |     NULL | NULL   | YES  | BTREE      |         |               |
| PublicChannels |          1 | idx_publicchannels_team_id    |            1 | TeamId      | A         |           1 |     NULL | NULL   | YES  | BTREE      |         |               |
| PublicChannels |          1 | idx_publicchannels_name       |            1 | Name        | A         |           1 |     NULL | NULL   | YES  | BTREE      |         |               |
| PublicChannels |          1 | idx_publicchannels_delete_at  |            1 | DeleteAt    | A         |           1 |     NULL | NULL   | YES  | BTREE      |         |               |
| PublicChannels |          1 | idx_publicchannels_search_txt |            1 | Name        | NULL      |           1 |     NULL | NULL   | YES  | FULLTEXT   |         |               |
| PublicChannels |          1 | idx_publicchannels_search_txt |            2 | DisplayName | NULL      |           1 |     NULL | NULL   | YES  | FULLTEXT   |         |               |
| PublicChannels |          1 | idx_publicchannels_search_txt |            3 | Purpose     | NULL      |           1 |     NULL | NULL   | YES  | FULLTEXT   |         |               |
+----------------+------------+-------------------------------+--------------+-------------+-----------+-------------+----------+--------+------+------------+---------+---------------+
9 rows in set (0.00 sec)

mysql> show CREATE TABLE PublicChannels;
| PublicChannels | CREATE TABLE `PublicChannels` (
  `Id` varchar(26) NOT NULL,`DeleteAt` bigint(20) DEFAULT NULL,`TeamId` varchar(26) DEFAULT NULL,`DisplayName` varchar(64) DEFAULT NULL,`Name` varchar(64) DEFAULT NULL,`Header` text,`Purpose` varchar(250) DEFAULT NULL,PRIMARY KEY (`Id`),UNIQUE KEY `Name` (`Name`,`TeamId`),KEY `idx_publicchannels_team_id` (`TeamId`),KEY `idx_publicchannels_name` (`Name`),KEY `idx_publicchannels_delete_at` (`DeleteAt`),FULLTEXT KEY `idx_publicchannels_search_txt` (`Name`,`DisplayName`,`Purpose`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 |
1 row in set (0.00 sec)

此事务是 2 表修改查询的一部分,在该查询被调用之前插入另一个表。此表与上表没有关系。

话虽如此,让我们深入研究这个僵局:

  • 我们可以看到这是由于间隙锁导致的死锁。Name这很清楚,因为索引是 Name ,它是一个使用and的多列索引TeamId
  • 但有趣的是,两个事务似乎都插入了相同的数据,这让我感到困惑。多列索引将保证它们始终是唯一的NameTeamId从查看代码来看,我似乎不可能尝试从 2 个不同的事务中插入相同的数据。

但是即使他们这样做了,为什么 tx 2 已经拥有相同的锁,它又会尝试再次获取相同的锁呢?

这是我认为事件发生的顺序

  • Tx 2 获得间隙锁定。
  • Tx 1 尝试获得相同的间隙锁。
  • Tx 2 尝试再次获得相同的间隙锁?

最后一步让我感到困惑。为什么 UPSERT 语句会获得 2 个间隙锁?根据我对https://dev.mysql.com/doc/refman/8.0/en/innodb-locks-set.html的阅读:

INSERT … ON DUPLICATE KEY UPDATE 与简单的 INSERT 不同之处在于,当发生重复键错误时,将在要更新的行上放置排他锁而不是共享锁。对重复的主键值采用排他索引记录锁。对重复的唯一键值采用独占的下一个键锁。

好的,所以有一个 X 锁和一个间隙锁,这很好。但这不是死锁输出似乎所说的。

MySQL 版本是 5.7.28。

版权声明:本文内容由互联网用户自发贡献,该文观点与技术仅代表作者本人。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如发现本站有涉嫌侵权/违法违规的内容, 请发送邮件至 dio@foxmail.com 举报,一经查实,本站将立刻删除。

相关推荐


依赖报错 idea导入项目后依赖报错,解决方案:https://blog.csdn.net/weixin_42420249/article/details/81191861 依赖版本报错:更换其他版本 无法下载依赖可参考:https://blog.csdn.net/weixin_42628809/a
错误1:代码生成器依赖和mybatis依赖冲突 启动项目时报错如下 2021-12-03 13:33:33.927 ERROR 7228 [ main] o.s.b.d.LoggingFailureAnalysisReporter : *************************** APPL
错误1:gradle项目控制台输出为乱码 # 解决方案:https://blog.csdn.net/weixin_43501566/article/details/112482302 # 在gradle-wrapper.properties 添加以下内容 org.gradle.jvmargs=-Df
错误还原:在查询的过程中,传入的workType为0时,该条件不起作用 <select id="xxx"> SELECT di.id, di.name, di.work_type, di.updated... <where> <if test=&qu
报错如下,gcc版本太低 ^ server.c:5346:31: 错误:‘struct redisServer’没有名为‘server_cpulist’的成员 redisSetCpuAffinity(server.server_cpulist); ^ server.c: 在函数‘hasActiveC
解决方案1 1、改项目中.idea/workspace.xml配置文件,增加dynamic.classpath参数 2、搜索PropertiesComponent,添加如下 <property name="dynamic.classpath" value="tru
删除根组件app.vue中的默认代码后报错:Module Error (from ./node_modules/eslint-loader/index.js): 解决方案:关闭ESlint代码检测,在项目根目录创建vue.config.js,在文件中添加 module.exports = { lin
查看spark默认的python版本 [root@master day27]# pyspark /home/software/spark-2.3.4-bin-hadoop2.7/conf/spark-env.sh: line 2: /usr/local/hadoop/bin/hadoop: No s
使用本地python环境可以成功执行 import pandas as pd import matplotlib.pyplot as plt # 设置字体 plt.rcParams['font.sans-serif'] = ['SimHei'] # 能正确显示负号 p
错误1:Request method ‘DELETE‘ not supported 错误还原:controller层有一个接口,访问该接口时报错:Request method ‘DELETE‘ not supported 错误原因:没有接收到前端传入的参数,修改为如下 参考 错误2:cannot r
错误1:启动docker镜像时报错:Error response from daemon: driver failed programming external connectivity on endpoint quirky_allen 解决方法:重启docker -> systemctl r
错误1:private field ‘xxx‘ is never assigned 按Altʾnter快捷键,选择第2项 参考:https://blog.csdn.net/shi_hong_fei_hei/article/details/88814070 错误2:启动时报错,不能找到主启动类 #
报错如下,通过源不能下载,最后警告pip需升级版本 Requirement already satisfied: pip in c:\users\ychen\appdata\local\programs\python\python310\lib\site-packages (22.0.4) Coll
错误1:maven打包报错 错误还原:使用maven打包项目时报错如下 [ERROR] Failed to execute goal org.apache.maven.plugins:maven-resources-plugin:3.2.0:resources (default-resources)
错误1:服务调用时报错 服务消费者模块assess通过openFeign调用服务提供者模块hires 如下为服务提供者模块hires的控制层接口 @RestController @RequestMapping("/hires") public class FeignControl
错误1:运行项目后报如下错误 解决方案 报错2:Failed to execute goal org.apache.maven.plugins:maven-compiler-plugin:3.8.1:compile (default-compile) on project sb 解决方案:在pom.
参考 错误原因 过滤器或拦截器在生效时,redisTemplate还没有注入 解决方案:在注入容器时就生效 @Component //项目运行时就注入Spring容器 public class RedisBean { @Resource private RedisTemplate<String
使用vite构建项目报错 C:\Users\ychen\work>npm init @vitejs/app @vitejs/create-app is deprecated, use npm init vite instead C:\Users\ychen\AppData\Local\npm-