并发delete+insert duplicate-key冲突导致死锁

本文通过一个具体的MySQL死锁案例,详细分析了死锁产生的原因及解决方法。案例发生在REPEATABLE-READ隔离级别下,涉及并发事务中的DELETE和INSERT操作。文章还探讨了不同事务隔离级别对死锁的影响。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

死锁案例

简化后的死锁场景如下

环境: mysql5.7,事务隔离级别REPEATABLE-READ

表结构

 CREATE TABLE `t4` (
  `a` int(11) NOT NULL AUTO_INCREMENT,
  PRIMARY KEY (`a`)
) ENGINE=InnoDB
数据:
mysql> select * from t4;
+---+
| a |
+---+
| 1 |
+---+

并发事务

T1T2
begin;begin
delete from t4 where a = 1;//ok, 0 rows affected
delete from t4 where a = 1; //wating,被阻塞
insert into t4 values(1);//Query OK, 1 row affected (0.01 sec)
ERROR 1213 (40001): Deadlock found when trying to get lock;

从上面可以看出,并发事务执行delete,T2等待,T1再执行insert相同value的数据时出现死锁。

死锁分析

死锁原因

insert时如果数据出现duplicate key,会加LOCK_S锁,类型为NEXT-KEY LOCK,此时事务2已经在申请record lock X锁,在申请队列中了,事务1再加NEXT-KEY LOCK S锁则需要等待事务2提交,这就造成了相互等待。

加锁分析

  1. T2 delete语句需要等待记录上的X锁;
  2. 普通insert时,其加锁过程为先在插入间隙上获取插入意向锁,插入数据后再获取插入行上的排它锁。由于这里T1 insert的数据检测到duplicate key,会加S锁(如果扫描到的记录被标记删除时,也会加S 锁),且针对主键索引加LOCK_ORDINARY类型的记录锁(NEXT-KEY LOCK),而NEXT-KEY LOCK与record互斥 。

另外,如果delete的where子句没有满足条件的记录,而对于不存在的记录 并且在RR级别下,delete加锁类型为gap lock。可以参考我的这篇博客https://my.oschina.net/hebaodan/blog/1835966

RC级别测试上述场景无死锁发生。

死锁解决

  • 避免出现按某个key delete后又插入相同key的场景,规避insert数据duplicate key的问题
  • 如果非特殊需求修改为非唯一索引
  • 通过其他手段如分布式锁实现串行化处理

附:死锁日志

*** (1) TRANSACTION:
TRANSACTION 11074, ACTIVE 11 sec starting index read
mysql tables in use 1, locked 1
LOCK WAIT 2 lock struct(s), heap size 1136, 1 row lock(s)
MySQL thread id 10, OS thread handle 123145442168832, query id 160 localhost 127.0.0.1 root updating
delete from t4 where a = 1
*** (1) WAITING FOR THIS LOCK TO BE GRANTED:
RECORD LOCKS space id 68 page no 3 n bits 72 index PRIMARY of table `aliyun`.`t4` trx id 11074 lock_mode X locks rec but not gap waiting
Record lock, heap no 2 PHYSICAL RECORD: n_fields 3; compact format; info bits 32
 0: len 4; hex 80000001; asc     ;;
 1: len 6; hex 000000002b41; asc     +A;;
 2: len 7; hex 2e000001dc13c4; asc .      ;;

*** (2) TRANSACTION:
TRANSACTION 11073, ACTIVE 40 sec inserting
mysql tables in use 1, locked 1
3 lock struct(s), heap size 1136, 2 row lock(s), undo log entries 1
MySQL thread id 9, OS thread handle 123145442725888, query id 161 localhost 127.0.0.1 root update
insert into t4 values(1)
*** (2) HOLDS THE LOCK(S):
RECORD LOCKS space id 68 page no 3 n bits 72 index PRIMARY of table `aliyun`.`t4` trx id 11073 lock_mode X locks rec but not gap
Record lock, heap no 2 PHYSICAL RECORD: n_fields 3; compact format; info bits 32
 0: len 4; hex 80000001; asc     ;;
 1: len 6; hex 000000002b41; asc     +A;;
 2: len 7; hex 2e000001dc13c4; asc .      ;;

*** (2) WAITING FOR THIS LOCK TO BE GRANTED:
RECORD LOCKS space id 68 page no 3 n bits 72 index PRIMARY of table `aliyun`.`t4` trx id 11073 lock mode S waiting
Record lock, heap no 2 PHYSICAL RECORD: n_fields 3; compact format; info bits 32
 0: len 4; hex 80000001; asc     ;;
 1: len 6; hex 000000002b41; asc     +A;;
 2: len 7; hex 2e000001dc13c4; asc .      ;;

*** WE ROLL BACK TRANSACTION (1)

参考

InnoDB 事务锁系统简介

https://dev.mysql.com/doc/refman/8.0/en/innodb-locks-set.html

http://hedengcheng.com/?p=844

转载于:https://my.oschina.net/hebaodan/blog/3033276

<think>好的,我现在需要回答用户关于“不建使用ON DUPLICATE KEY的原因”的问题。首先,我需要回忆一下ON DUPLICATE KEY UPDATE(ODKU)的用法和潜在的问题。用户可能想知道为什么有时候不建议使用这个语句,或者在某些情况下应该避免使用它。接下来,我要分步骤分析可能的原因,并确保内容结构清晰,符合用户之前的格式要求。 首先,我应该从功能上解释ODKU的作用,即当插入数据时,如果主键或唯一索引冲突,就执行更新操作。然后,列举不使用它的原因。可能的原因包括性能问题、死锁风险、主键设计不合理、可读性差、触发器和外键的影响、数据一致性问题,以及版本兼容性等。 接下来,我需要详细说明每个原因。例如,高并发下使用ODKU可能导致竞争,特别是在InnoDB的行机制下,可能引发死锁。主键设计方面,如果主键是自增的,但业务中又依赖其他唯一键,可能导致主键不连续或者冲突处理不符合预期。另外,ODKU的语法可能不够直观,影响代码可读性,尤其是在团队协作中。 还需要考虑触发器和外键约束的问题。使用ODKU时,触发器的执行顺序和次数可能与预期不同,外键约束可能导致更新失败。此外,数据一致性方面,如果更新操作涉及多个唯一索引,处理逻辑可能复杂,容易出错。 然后,要给出替代方案,比如先查询后插入或更新,使用INSERT IGNORE,或者用事务包裹多个操作。同时,提醒用户在使用ODKU时需要注意的事项,比如明确主键和唯一索引,监控情况,避免在触发器复杂的表上使用等。 需要确保回答结构清晰,分点列出,每个部分有示例或解释,帮助用户理解。同时,要使用中文,避免技术术语过于晦涩,必要时举例说明。最后,总结使用ODKU的适用场景和注意事项,帮助用户做出合理选择。 在整理过程中,要检查是否有遗漏的原因,比如是否有其他潜在的问题,比如对自增ID的影响,或者在大数据量下的性能问题。同时,要参考之前关于联合索引失效的回答结构,保持一致性,使用标题、子标题、示例代码块和总结表格,增强可读性。 最后,确保内容准确,没有错误的技术细节,例如ODKU在MySQL中的具体行为,的机制,触发器的触发条件等。如果有不确定的地方,需要查阅可靠的资料或官方文档进行确认,避免误导用户。</think>### **一、ON DUPLICATE KEY UPDATE 的作用** `ON DUPLICATE KEY UPDATE`(简称 ODKU)是 MySQL 中用于处理**主键或唯一索引冲突**的语法。当插入数据时,若触发唯一性冲突(主键或唯一索引重复),则执行更新操作而非报错。 **示例**: ```sql INSERT INTO users (id, name, age) VALUES (1, '张三', 25) ON DUPLICATE KEY UPDATE age = 26; ``` - 若 `id=1` 已存在,则更新 `age=26`; - 若不存在,则插入新记录。 --- ### **二、不建议使用 ON DUPLICATE KEY 的原因** #### **1. 性能问题** **问题描述**: 在高并发场景下,ODKU 可能导致竞争或死锁- **行冲突**:InnoDB 会对冲突的行加排他(X),其他事务需等待,降低并发性能。 - **死锁风险**:若多个事务同时尝试插入相同主键,可能因顺序问题触发死锁。 **示例场景**: 两个事务同时插入 `id=100` 的记录,触发冲突后尝试更新,可能互相等待释放。 --- #### **2. 主键设计不合理时引发问题** **问题描述**: - **自增主键的陷阱**:若表使用自增主键,但业务中依赖其他唯一键触发 ODKU,可能导致主键不连续或冲突- **隐式覆盖数据**:若更新条件不明确,可能意外覆盖其他字段的值。 **示例**: ```sql -- 表结构:id(自增主键), email(唯一索引), name INSERT INTO users (email, name) VALUES ('a@test.com', '李四') ON DUPLICATE KEY UPDATE name = '王五'; ``` - 若 `email='a@test.com'` 已存在,会更新 `name`,但自增主键 `id` 会递增(产生空洞)。 --- #### **3. 可读性与维护性问题** **问题描述**: - **代码意图不清晰**:ODKU 将插入和更新逻辑合并,可能让后续维护者难以快速理解业务逻辑。 - **复杂条件难处理**:若需根据冲突字段的值动态更新其他字段,ODKU 的语法会变得复杂且易错。 --- #### **4. 触发器和外键的副作用** **问题描述**: - **触发器多次触发**:ODKU 实际执行的是 `INSERT` 后紧跟 `UPDATE`,可能触发 `BEFORE/AFTER INSERT` 和 `BEFORE/AFTER UPDATE` 的触发器。 - **外键约束失败**:若更新的值违反外键约束,操作会直接失败。 --- #### **5. 数据一致性风险** **问题描述**: - **更新条件不明确**:若表有多个唯一索引,ODKU 可能因不确定哪个唯一索引触发了冲突导致更新逻辑错误。 - **部分更新问题**:若更新字段未明确指定,可能保留旧值(需显式列出所有需更新的字段)。 --- #### **6. 版本兼容性限制** **问题描述**: - ODKU 是 MySQL 特有语法,其他数据库(如 PostgreSQL、SQL Server)不支持,降低代码可移植性。 --- ### **三、替代方案** #### **1. 先查询后操作** 通过 `SELECT` 判断记录是否存在,再决定执行 `INSERT` 或 `UPDATE`。 ```sql START TRANSACTION; SELECT * FROM users WHERE id = 1 FOR UPDATE; -- 根据查询结果执行 INSERT 或 UPDATE COMMIT; ``` **优点**:逻辑清晰,避免隐式更新问题。 **缺点**:需手动处理事务,增加代码量。 --- #### **2. 使用 INSERT IGNORE** 若冲突时无需更新,仅需忽略插入,可使用 `INSERT IGNORE`。 ```sql INSERT IGNORE INTO users (id, name) VALUES (1, '张三'); ``` **适用场景**:重复数据可安全丢弃的情况。 --- #### **3. 拆分操作(适合批量处理)** 对批量数据先筛选出冲突记录,再分别执行 `INSERT` 和 `UPDATE`。 ```sql -- 找出已存在的记录 SELECT id FROM users WHERE id IN (1, 2, 3); -- 插入新记录 INSERT INTO users (id, name) VALUES (4, '李四'); -- 更新旧记录 UPDATE users SET name = '王五' WHERE id IN (1, 2); ``` --- #### **4. 使用 REPLACE INTO(谨慎使用)** `REPLACE INTO` 会先删除冲突记录,再插入新记录(注意自增主键变化和触发器触发)。 ```sql REPLACE INTO users (id, name) VALUES (1, '张三'); ``` **缺点**:实际执行 `DELETE` + `INSERT`,可能影响性能和外键约束。 --- ### **四、使用 ON DUPLICATE KEY 的注意事项** 若必须使用 ODKU,需注意以下问题: 1. **明确主键和唯一索引**:确保冲突判断依据清晰。 2. **显式指定更新字段**:避免遗漏字段导致数据残留。 3. **监控竞争**:在高并发场景中观察等待时间。 4. **避免在触发器复杂的表中使用**:防止触发器逻辑混乱。 --- ### **五、总结** | **场景** | **问题** | **替代方案** | |-----------------------------|----------------------------------|--------------------------| | 高并发写入 | 竞争、死锁 | 先查询后操作,拆分事务 | | 自增主键与其他唯一键共存 | 主键空洞、数据覆盖 | 显式指定更新字段 | | 多唯一索引 | 冲突条件不明确 | 使用先查询后操作 | | 需跨数据库兼容 | 语法不兼容 | 使用标准 SQL 实现 | **核心建议**: - 在**低并发、简单业务逻辑**的场景中,ODKU 可简化代码。 - 在**高并发、复杂数据关系**的场景中,优先选择更可控的替代方案。 - 始终通过 `EXPLAIN` 和监控工具验证操作的实际影响。
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值