RR 隔离级别下删除不存在数据导致 【死锁】

间隙锁与死锁:防止幻读的原理与实例分析
文章探讨了间隙锁在数据库中的作用,主要是为了防止幻读,阻止间隙内数据的插入和已有数据更新到间隙内。通过一个死锁场景的复盘,解释了当两个事务分别持有无限范围的间隙锁时,尝试插入数据可能导致死锁的情况。内容涉及数据库事务处理和InnoDB存储引擎的锁机制。

        如果删除索引的数据不存在,会出现一个间隙锁,区间(左边最近第一个值, 右边最近第一个值), 间隙锁之间是不会冲突的,只有往间隙中插入数据的操作,才会被阻塞。

重温间隙锁目的

间隙锁的目的是为了防止幻读,其主要通过两个方面实现:

(1)防止间隙内有新数据被插入

(2)防止已存在的数据,更新成间隙内的数据(例如防止numer=3的记录通过update变成number=5)

死锁场景流程复盘:开启事务,任务数据先执行批量删除操作,然后执行插入操作, 出现死锁。

数据表


CREATE TABLE `task` (
  `uid` int(11) NOT NULL,
  `task_id` int(11) NOT NULL,
  `state` int(11) DEFAULT NULL,
  `sub_time` timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
  PRIMARY KEY (`uid`,`task_id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8 COMMENT='任务数据';
  1.  空表的情况下, 删除操作都可以,从提示看没有改动数据

   2.  开始分别插入一些数据, 左边阻塞了,然后我们看锁表的信息

  3.  查看INNODB_LOCKS 表

suprenum 表示超过索引中的最大值,也就是说 两个事务的间隙锁 范围都是  (-∞, +∞),左边 执行insert 的事务正在等待右边释放锁,这时候如果右边事务也执行insert 就死锁了……

<think> 我们正在讨论在可重复读(Repeatable Read, RR隔离级别下如何彻底避免唯一键冲突的问题。在RR级别下,MySQL(尤其是InnoDB引擎)通过MVCC和间隙锁(Gap Lock)来防止幻读,但唯一键冲突仍然可能发生,尤其是在高并发插入相同唯一键值的情况下。下面我将详细说明原因和解决方案。 ### 问题分析 唯一键冲突的根本原因是多个事务尝试插入具有相同唯一键(主键或唯一索引)的记录。在RR隔离级别下,虽然通过MVCC保证了可重复读,并且通过间隙锁防止了幻读(即其他事务插入新行导致的结果集变化),但以下情况仍可能导致唯一键冲突: 1. **事务内重复插入**:同一个事务内多次插入同一个键值,第二次插入会直接报错。 2. **并发插入相同键值**:两个事务同时插入相同的唯一键值,即使它们都先检查了该键值存在,但由于检查时使用了快照读(非锁定读),而插入时使用当前读(锁定读),此时可能同时通过检查,然后插入时其中一个会失败。 ### 彻底避免唯一键冲突的方法 要彻底避免唯一键冲突,我们需要在业务逻辑和数据库操作上采取一些额外措施。以下方法在RR隔离级别下有效: #### 1. **使用`SELECT ... FOR UPDATE`显式加锁** 在插入之前,先使用`SELECT ... FOR UPDATE`查询该键值是否存在。这会施加一个排他锁(X锁),如果记录存在,则会对间隙加间隙锁(Gap Lock),阻止其他事务在这个间隙中插入。这样,在插入操作执行前,当前事务已经锁定了可能插入该键值的间隙,其他事务无法插入相同的键值。 **操作步骤:** ```sql START TRANSACTION; -- 检查id=100是否存在,并对间隙加锁 SELECT id FROM table WHERE id = 100 FOR UPDATE; -- 如果上述查询返回空,则插入 INSERT INTO table (id, ...) VALUES (100, ...); COMMIT; ``` **注意:** - 如果`id=100`已经存在,那么`SELECT ... FOR UPDATE`会锁定该记录(行锁),其他事务无法修改或删除它,但会阻止插入(因为记录已存在,插入本身会因为唯一键冲突而失败)。 - 如果`id=100`存在,那么`SELECT ... FOR UPDATE`会对该间隙加锁(例如,如果id是主键,则会在前后两个存在的键值之间加间隙锁),阻止其他事务插入相同的id。 **缺点:** 在高并发场景下,这种加锁方式可能导致性能问题(锁竞争)。 #### 2. **使用`INSERT ... ON DUPLICATE KEY UPDATE`** 这个语句在插入时如果遇到唯一键冲突,则执行更新操作。它会报错,而是转为更新。虽然能完全避免冲突(冲突仍然发生,但被转化为更新操作),但可以避免插入失败。 ```sql INSERT INTO table (id, col) VALUES (100, 'value') ON DUPLICATE KEY UPDATE col = VALUES(col); ``` **注意:** 这是避免冲突,而是处理冲突。在业务逻辑允许的情况下,这是一种优雅的解决方案。 #### 3. **使用数据库的唯一键约束** 唯一键约束是数据库保证唯一性的最可靠方法。任何违反唯一性的插入都会失败。因此,我们必须正确处理插入失败的情况(例如重试或转更新)。 **如何结合RR隔离级别使用:** - 依赖唯一键约束来捕获冲突(即捕获插入失败的错误)。 - 在业务逻辑中,当捕获到唯一键冲突错误时,进行重试或执行更新操作。 #### 4. **使用序列或自增主键** 如果唯一键是主键,可以考虑使用自增主键。这样,每个事务插入时会自动分配一个唯一的id,从而避免冲突。但这种方法只适用于主键,并且适用于业务上的唯一键(比如用户名、邮箱等)。 #### 5. **分布式场景下使用全局唯一ID生成器** 在分布式系统中,为了避免多个节点生成相同的ID,可以使用全局唯一ID生成方案(如雪花算法、UUID等),确保生成的ID在分布式环境中唯一。 ### 总结 在可重复读隔离级别下,要彻底避免唯一键冲突,最可靠的方法是**在插入前使用`SELECT ... FOR UPDATE`显式加锁**,确保在插入之前锁定间隙,阻止其他事务插入相同的键值。同时,必须依赖数据库的唯一键约束作为最后防线,并在业务逻辑中处理插入失败的情况(如重试)。 > **关键点总结**: > 1. RR隔离级别通过间隙锁可以防止其他事务插入导致幻读,但无法避免**同一事务内的两次插入冲突**或**两个事务同时插入相同键值**(因为插入前的检查可能都认为键值存在)。 > 2. 使用`SELECT ... FOR UPDATE`是RR级别下避免并发插入冲突的有效手段。 > 3. 唯一键约束是数据库层保证唯一性的终极机制,任何绕过它的尝试都是危险的。 §§相关问题§§ 1. 在RR隔离级别下,`SELECT ... FOR UPDATE`加锁的详细过程是怎样的? 2. 如何在高并发场景下优化`SELECT ... FOR UPDATE`的性能? 3. `INSERT ... ON DUPLICATE KEY UPDATE`在RR隔离级别下会产生死锁吗?如何避免? 4. 除了数据库层面的解决方案,业务层如何设计幂等性操作来应对唯一键冲突? </think> 在可重复读(Repeatable Read, RR隔离级别下彻底避免唯一键冲突,需要结合数据库机制和业务设计策略。以下是经过验证的完整解决方案: ### 核心方法:显式锁定 + 插入优化 #### 1. **`SELECT ... FOR UPDATE` 显式锁定(关键手段)** ```sql BEGIN; -- 锁定可能冲突的间隙(即使记录存在) SELECT * FROM users WHERE id = 100 FOR UPDATE; -- 对100所在间隙加锁 -- 此时其他事务无法插入id=100的记录 INSERT INTO users (id, name) VALUES (100, 'Alice'); COMMIT; ``` **实现原理**: - 在RR级别下,`FOR UPDATE` 会对查询范围加**间隙锁(Gap Lock)**,锁定 $[99,101)$ 的区间[^5] - 其他事务尝试插入相同ID时会被阻塞,直到当前事务提交 - **优势**:完全避免并发插入导致的唯一键冲突 #### 2. **`INSERT ... ON DUPLICATE KEY UPDATE`** ```sql INSERT INTO users (id, name) VALUES (100, 'Alice') ON DUPLICATE KEY UPDATE name = VALUES(name); ``` **特点**: - 原子性处理冲突(冲突时转为更新) - RR级别下自动加**排他锁(X锁)** 保护操作[^3] - **适用场景**:需要覆盖旧值的业务 #### 3. **`INSERT IGNORE` 静默处理** ```sql INSERT IGNORE INTO users (id, name) VALUES (100, 'Alice'); ``` **特点**: - 冲突时自动跳过,报错 - 需配合 `ROW_COUNT()` 检查是否插入成功 - **风险**:可能掩盖业务逻辑错误 ### 辅助策略:业务层设计 #### 1. **分布式ID生成** - 使用雪花算法(Snowflake)生成全局唯一ID: $$ \text{ID} = \text{时间戳} \times 2^{22} + \text{机器ID} \times 2^{12} + \text{序列号} $$ - **优点**:彻底消除冲突可能 #### 2. **队列串行化** ```mermaid sequenceDiagram 客户端->>消息队列: 发送插入请求 消息队列->>数据库: 顺序处理请求 数据库-->>客户端: 返回结果 ``` - 通过Kafka/RabbitMQ等实现请求序列化 - **适用场景**:高并发注册/订单创建 #### 3. **乐观锁重试机制** ```python max_retry = 3 for _ in range(max_retry): try: execute_sql("INSERT INTO users ...") break except UniqueViolation: continue ``` ### 同方法的有效性对比 | 方法 | 冲突避免 | 性能影响 | 实现复杂度 | 适用场景 | |------|----------|----------|------------|----------| | `SELECT FOR UPDATE` | ⭐⭐⭐⭐⭐ | (中) | 🟢(低) | 关键业务数据 | | `ON DUPLICATE UPDATE` | ⭐⭐⭐⭐ | 🟢(低) | (低) | 需要覆盖值的场景 | | 分布式ID | ⭐⭐⭐⭐⭐ | 🟢(低) | 🔴(高) | 分布式系统 | | 消息队列 | ⭐⭐⭐⭐⭐ | 🔴(高) | 🔴(高) | 超高并发场景 | ### 重要注意事项 1. **间隙锁的副作用**:当 `WHERE id=100` 的记录存在时,`FOR UPDATE` 会锁定临近区间(如99-101),可能引发死锁[^5] 2. **索引设计**:确保唯一键有独立索引,锁范围更精确 3. **锁超时设置**:合理配置 `innodb_lock_wait_timeout` 避免长时间阻塞 > **终极建议**:对核心业务数据采用 **`SELECT FOR UPDATE` + 唯一键约束** 组合方案,既保证数据一致性,又利用数据库原生机制实现最高可靠性[^3][^5]。
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值