在并发场景下,用 `select ... for update` 结合 `insert` 来实现“查询订单是否存在,不存在则插入”,可以避免重复创建订单,核心是利用行级锁或表级锁防止并发冲突。以下是具体实现思路和示例:
 核心逻辑
1. 先锁行/锁表:用 `select ... for update` 查询订单(通常按唯一标识,如订单号),并加排他锁,阻止其他事务同时操作相同条件的记录。  
2. 判断是否存在:如果查询结果为空(订单不存在),则执行插入操作;如果已存在,则不插入(或返回“已存在”提示)。  
 关键:锁的范围
`select ... for update` 的锁范围取决于查询条件:  
 如果查询条件命中索引(如按订单号 `order_no` 索引查询),则锁定符合条件的行(行锁),其他事务对其他行的操作不受影响,效率高。  
 如果查询条件无索引(如按非索引字段查询),则会锁定整张表(表锁),并发性能差,需避免。  
 示例代码(伪代码)
假设订单表 `orders` 有唯一索引 `idx_order_no`(订单号唯一),字段包括 `order_no`(订单号)、`user_id`(用户ID)等。

 --开启事务
BEGIN;

 --1. 用 for update 查询订单,加排他锁(锁定符合条件的行)
 --假设订单号是唯一标识,通过 order_no 查询
SELECT * FROM orders 
WHERE order_no = 'ORDER123456' 
FOR UPDATE;  关键:加锁,阻止其他事务同时修改/插入相同订单号

-- 2. 判断查询结果:如果不存在,则插入
 --(以下逻辑在应用层判断,伪代码示意)
IF (查询结果为空) THEN
  INSERT INTO orders (order_no, user_id, ...)
  VALUES ('ORDER123456', 1001, ...);
ELSE
   --订单已存在,不插入(或返回错误)
  PRINT '订单已存在';
END IF;

-- 3. 提交事务(释放锁)
COMMIT;


 为什么能防止重复插入?
 当多个事务同时处理同一个订单号时,第一个事务执行 `select ... for update` 会锁定对应的行(如果不存在,会锁定“间隙”,即未来可能插入该订单号的位置,这是InnoDB的间隙锁机制)。  
 其他事务执行相同的 `select ... for update` 时,会被阻塞等待,直到第一个事务提交(释放锁)。  
 第一个事务提交后,其他事务获得锁,此时查询会发现订单已存在,从而避免重复插入。  
 注意事项
1. 必须加索引:确保 `where` 条件中的字段(如 `order_no`)有唯一索引,否则会锁表,降低并发性能。  
2. 事务要短:`for update` 会持有锁,事务执行时间过长会导致其他事务等待超时,需尽量缩短事务逻辑(只做“查+插”操作)。  
3. 处理锁等待:可设置合理的锁等待超时时间(如 `set innodb_lock_wait_timeout = 5`,单位秒),避免无限等待。  
4. 结合唯一约束:即使加了锁,建议在表中对 `order_no` 加唯一约束(`UNIQUE`),作为最后一道防线(防止极端情况下的并发漏锁)。  

简而言之:也就是在T1执行`select ... for update`时T2会阻塞等待,这种“先锁后插入”所以能保证订单的唯一性

在高并发场景下,用 select ... for update 配合插入操作,确实可能因为锁的竞争顺序不当而导致死锁。这本质上是 “锁的获取顺序不一致” 引发的问题,我们先通过一个例子理解死锁如何产生,再讲如何避免。
一、死锁产生的典型场景
假设两个事务(T1、T2)同时处理两个订单,涉及的操作可能交叉获取锁,导致互相等待:
订单表 orders 结构(简化):

字段说明索引
id主键主键索引
order_no订单号唯一索引
user_id用户 ID普通索引


场景:
两个用户(用户 A、用户 B)同时给对方创建订单(比如互相转账生成的订单),订单号分别为 A→B 和 B→A。
事务 T1(创建 A→B 订单):
第一步:select * from orders where order_no = 'A→B' for update(锁定 A→B 对应的行 / 间隙)。
第二步:需要检查用户 B 的某些状态(比如余额),执行 select * from users where user_id = B for update(锁定用户 B 的行)。
事务 T2(创建 B→A 订单):
第一步:select * from orders where order_no = 'B→A' for update(锁定 B→A 对应的行 / 间隙)。
第二步:需要检查用户 A 的某些状态,执行 select * from users where user_id = A for update(锁定用户 A 的行)。
死锁发生:
T1 持有 A→B 订单的锁,等待 T2 释放用户 A 的锁;
T2 持有 B→A 订单的锁,等待 T1 释放用户 B 的锁;
两者互相等待对方释放锁,形成死锁。
二、死锁的核心原因
select ... for update 会获取 行锁 或 间隙锁(当查询的行不存在时,InnoDB 会锁定可能插入该记录的 “间隙”),如果多个事务 获取锁的顺序不一致,就可能形成闭环等待,导致死锁。
三、如何避免死锁?
核心原则:让所有事务按 “固定顺序” 获取锁,打破闭环等待的条件。具体方案:
1. 统一锁的获取顺序
对所有需要锁定的资源(表、行、间隙),约定一个全局统一的获取顺序,所有事务严格遵守。
例:上述场景中,规定 “先锁用户 ID 小的用户,再锁订单”。假设 A 的 user_id=100,B 的 user_id=200:
T1 和 T2 都先锁定 user_id=100(A),再锁定 user_id=200(B),最后锁订单。
这样 T1 和 T2 不会交叉等待用户锁,死锁消失。
2. 缩小锁的范围
避免用 select ... for update 锁定无关的行或整张表(确保查询条件走索引,只锁必要的行)。
例如:查询订单时必须用 order_no(唯一索引),避免用无索引的字段导致表锁。
3. 缩短事务时长
for update 持有的锁会在事务提交 / 回滚后释放,事务越短,锁的持有时间越短,死锁概率越低。
优化逻辑:事务内只做 “查锁→判断→插入” 的核心操作,其他非关键逻辑(如日志、通知)放到事务外。
4. 用唯一约束兜底 + 重试机制
即使发生死锁,数据库会自动检测并终止其中一个事务(返回死锁错误),此时应用层可以重试该事务。
配合订单号的唯一约束(UNIQUE),即使重试时订单已存在,数据库也会报错(Duplicate entry),应用层可捕获并处理(返回 “订单已存在”)。
5. 避免在事务中处理多个独立订单
如果业务允许,尽量一个事务只处理一个订单,减少同时持有多个锁的场景。
例如:批量创建订单时,拆分多个单订单事务,而非一个事务处理所有订单。

更多推荐