MySQL insert on duplicate 加锁分析(1)

插入记录导致主键冲突,on duplicate key update 更新唯一索引字段值的加锁情况分析。

作者:操盛春,爱可生技术专家,公众号『一树一溪』作者,专注于研究 MySQL 和 OceanBase 源码。 爱可生开源社区出品,原创内容未经授权不得随意使用,转载请联系小编并注明来源。 本文基于 MySQL 8.0.32 源码,存储引擎为 InnoDB。

1. 准备工作

创建测试表:

CREATE TABLE `t4` (
  `id` int unsigned NOT NULL AUTO_INCREMENT,
  `i1` int DEFAULT '0',
  `i2` int DEFAULT '0',
  PRIMARY KEY (`id`) USING BTREE,
  UNIQUE KEY `uniq_i1` (`i1`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb3;

插入测试数据:

INSERT INTO `t4` (`id`, `i1`, `i2`) VALUES
(1, 11, 21), (2, 12, 22), (3, 13, 23),
(4, 14, 24), (5, 15, 25), (6, 16, 26);

2. 可重复读

把事务隔离级别设置为 REPEATABLE-READ(如已设置,忽略此步骤):

SET transaction_isolation = 'REPEATABLE-READ';

-- 确认设置成功
SHOW VARIABLES like 'transaction_isolation';
+-----------------------+-----------------+
| Variable_name         | Value           |
+-----------------------+-----------------+
| transaction_isolation | REPEATABLE-READ |
+-----------------------+-----------------+

执行以下 insert 语句(主键索引冲突,更新其它字段值):

begin;
insert into t4 (id, i1, i2) values (2, 120, 220)
on duplicate key update i1 = values(i1), i2 = values(i2);

查看加锁情况:

select
  engine_transaction_id, object_name, index_name,
  lock_type, lock_mode, lock_status, lock_data
from performance_schema.data_locks
where object_name = 't4'
and lock_type = 'RECORD'\G

***************************[ 1. row ]***************************
engine_transaction_id | 250919
object_name           | t4
index_name            | PRIMARY
lock_type             | RECORD
lock_mode             | X,REC_NOT_GAP
lock_status           | GRANTED
lock_data             | 2

lock_data = 2、lock_mode = X,REC_NOT_GAP 表示对主键索引中 <id = 2> 的记录加了排他普通记录锁。

insert 语句想要插入一条 <id = 2> 的记录,首先要找到插入记录的目标位置。

对于主键索引,经过一番不太费力的寻找,找到的目标位置是主键索引中已经存在的 <id = 2> 的记录后面。

insert 语句发现这条记录和即将插入的新记录的主键字段(id)值都是 2,要是继续插入新记录就会违反主键索引的唯一约束。

这可怎么办,就这样轻易放弃吗?

轻易放弃是不可能的,insert 语句还会进一步确认新记录是否真的和主键索引中 <id = 2> 的记录冲突。

因为对于主键索引,如果这条记录已经被标记删除了,就不会和新记录冲突,新记录可以继续插入。

insert 语句的确认操作会读取这条记录的删除标志,读取删除标志之前,需要对这条记录加锁。

按理来说,读操作对记录加锁,本来应该加共享锁,insert 语句却加了排他锁。

这是因为 insert 语句带了个小尾巴(on duplicate key update),这个小尾巴的作用是发现冲突记录时执行更新操作,更新操作需要加排他锁,所以读取删除标志之前,就直接加了排他锁。

根据主键字段值读取主键索引中的一条记录,只需要加普通记录锁就可以了。

基于以上逻辑,insert 语句对主键索引中 <id = 2> 的记录加了排他普通记录锁。

以上只是 insert 语句对主键索引中 <id = 2> 的记录第 1 次加锁。

第 1 次加锁之后,insert 语句发现这条记录是正常记录,没有被标记删除,接下来就会执行更新操作,也就是用 on duplicate key update 子句中各字段值更新这条记录。

更新之前,需要先读取 <id = 2> 的完整记录,这个过程也需要对主键索引加排他普通记录锁。

这是 insert 语句第 2 次对主键索引中 <id = 2> 的记录加锁。

第 2 次加锁时,发现之前已经加过同样的锁了,可以直接复用之前加的锁。

3. 读已提交

把事务隔离级别设置为 READ-COMMITTED(如已设置,忽略此步骤):

SET transaction_isolation = 'READ-COMMITTED';

-- 确认设置成功
SHOW VARIABLES like 'transaction_isolation';
+-----------------------+----------------+
| Variable_name         | Value          |
+-----------------------+----------------+
| transaction_isolation | READ-COMMITTED |
+-----------------------+----------------+

执行以下 insert 语句(主键索引冲突,更新其它字段值):

begin;
insert into t4 (id, i1, i2) values (2, 120, 220)
on duplicate key update i1 = values(i1), i2 = values(i2);

查看加锁情况:

select
  engine_transaction_id, object_name, index_name,
  lock_type, lock_mode, lock_status, lock_data
from performance_schema.data_locks
where object_name = 't4'
and lock_type = 'RECORD'\G

***************************[ 1. row ]***************************
engine_transaction_id | 250921
object_name           | t4
index_name            | PRIMARY
lock_type             | RECORD
lock_mode             | X,REC_NOT_GAP
lock_status           | GRANTED
lock_data             | 2

lock_data = 2、lock_mode = X,REC_NOT_GAP 表示对主键索引中 <id = 2> 的记录加了排他普通记录锁。

加锁流程和可重复读隔离级别一样,这里不再赘述。

4. 总结

insert on duplicate key update 语句,新插入记录和主键索引中已有记录冲突,可重复读和读已提交两个隔离级别下,加锁流程和加锁结果相同。

留一个小问题:如果主键索引中和新插入记录冲突的那条记录已经被标记删除了,但是执行标记删除操作的事务还没有提交,加锁情况会有什么变化?欢迎评论区留言交流。

更多技术文章,请访问:https://opensource.actionsky.com/

关于 SQLE

SQLE 是一款全方位的 SQL 质量管理平台,覆盖开发至生产环境的 SQL 审核和管理。支持主流的开源、商业、国产数据库,为开发和运维提供流程自动化能力,提升上线效率,提高数据质量。

✨ Github:https://github.com/actiontech/sqle

📚 文档:https://actiontech.github.io/sqle-docs/

💻 官网:https://opensource.actionsky.com/sqle/

👥 微信群:请添加小助手加入 ActionOpenSource

🔗 商业支持:https://www.actionsky.com/sqle

### INSERT ON DUPLICATE KEY UPDATE 中插入意向锁和 Gap 锁导致死锁的原因 在 MySQL 的 InnoDB 存储引擎中,`INSERT ... ON DUPLICATE KEY UPDATE` 是一种常见的操作模式。然而,在高并发环境下,这种语句可能会引发死锁问题。以下是关于其死锁原因以及解决方案的具体分析。 #### 死锁的根本原因 InnoDB 使用两阶段锁定协议来管理事务中的锁请求。当 `INSERT ... ON DUPLICATE KEY UPDATE` 被执行时,如果目标记录不存在,则会尝试向唯一索引的间隙(Gap)加锁以防止其他事务在同一位置插入相同的数据[^1]。具体来说: - **Gap Lock**:用于阻止其他事务在指定范围内的间隙插入新记录。 - **Insert Intention Lock**:这是一种特殊的 Gap Lock,表示某个事务打算在一个特定的位置插入一条记录。它与其他 Insert Intention Lock 或者更强的 Gap Lock 不兼容[^3]。 当两个或更多事务试图通过 `INSERT ... ON DUPLICATE KEY UPDATE` 向同一间隙插入数据时,它们可能分别持有不同的锁并相互等待对方释放资源,从而形成死锁情况[^2]。 #### 解决方案 针对上述提到的由插入意向锁和 Gap 锁引起的死锁现象,可以采取以下几种措施来进行优化和规避: 1. **调整业务逻辑减少竞争** 尽量设计应用程序使得不同事务不会频繁地争抢相同的行或者区间。可以通过预分配 ID 值等方式降低热点区域的压力。 2. **更改隔离级别** 默认情况下,InnoDB 运行于可重复读 (Repeatable Read, RR) 隔离级别之下。虽然这提供了较高的一致性保障但也增加了发生死锁的可能性。考虑将某些不敏感的操作降级至读已提交(Read Committed, RC),这样能有效减少不必要的锁开销。 3. **显式控制事务顺序** 如果能够预测哪些事务之间可能存在潜在冲突,则可以在编码层面强制规定这些事务按照固定次序访问共享资源,进而避免循环依赖关系的发生。 4. **重试机制** 对于不可避免会出现少量随机性死锁的应用场景而言,实现自动化的失败处理流程不失为一个简单有效的办法——即捕获异常后适当延迟再重新发起受影响的操作直至成功为止。 5. **升级数据库版本** 新版 MySQL 和 MariaDB 已经对此类问题做了不少改进工作;因此建议尽可能保持软件处于最新稳定状态以便利用官方提供的修复补丁和技术支持。 ```python try: cursor.execute(""" INSERT INTO table_name (column_list) VALUES (%s,%s,...) ON DUPLICATE KEY UPDATE column=value; """, value_tuple) except OperationalError as e: if 'Deadlock' in str(e): time.sleep(random.uniform(0.1, 0.5)) # Randomized backoff strategy retry_logic() # Recursive call or other recovery method else: raise # Re-throw unexpected errors ```
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值