一聚教程网:一个值得你收藏的教程网站

最新下载

热门教程

怎样解决SQL存储过程在多线程并发下的数据争抢问题?

时间:2026-07-11 09:45:03 编辑:袖梨 来源:一聚教程网

优先使用 UPDATE ... WHERE 带条件校验,因其是原子操作,可避免超卖;仅当业务逻辑无法单条 SQL 实现时才加锁,且需规范使用事务、锁名和超时设置。

优先用 UPDATE ... WHERE 带条件校验,而不是先查再更新——这是最轻量、最可靠、也最容易被忽略的解法。

为什么 UPDATE ... WHERESELECT + UPDATE 更安全

“先查库存再扣减”看似合理,但两个线程几乎同时执行时,会都看到 stock = 10,都判断“够扣”,然后双双执行 UPDATE,结果 stock 变成 8,超卖 2 单。这个时间窗口无法靠 Sleep 或重试堵住。

UPDATE products SET stock = stock - 1 WHERE id = 1001 AND stock >= 1 是原子操作:条件不满足就一整条语句不生效,根本不会进入“扣减逻辑”。执行后只需检查影响行数:

  • MySQL 用 ROW_COUNT()
  • PostgreSQL 用 pg_affected_rows()
  • SQL Server 用 @@ROWCOUNT

返回 0 就说明失败(已被别人抢走或已售罄),无需锁、不依赖隔离级别、也不需要额外字段。

什么时候必须加锁?怎么加才不卡死

当业务逻辑无法塞进单条 UPDATE 的 WHERE 条件里(比如要跨多张表校验、调外部接口后再决定是否扣减),才考虑锁。但锁极易滥用:

  • SQL Server 的 sp_getapplock 必须包裹在 BEGIN TRANSACTION 内,且事务结束前不能退出存储过程,否则锁残留
  • 锁名必须带业务上下文,例如 'order_pay_' + CAST(@order_id AS VARCHAR(20)),硬编码 'mylock' 会让所有请求串行
  • @LockTimeout 必须设具体毫秒值(如 5000),别用默认 -1(无限等待)
  • 必须检查返回值:IF @result 表示失败(-1=超时,-2=死锁牺牲品),不能只看 <code>@@ERROR

MySQL 怎么模拟应用级锁

MySQL 没有 sp_getapplock 这类事务安全的应用锁,GET_LOCK() 是唯一跨会话方案,但它本身不是事务安全的:

  • 锁名必须唯一稳定,推荐格式:'db_shop_voucher_use_' + CAST(@voucher_id AS CHAR),纯数字或固定字符串极易冲突
  • RELEASE_LOCK() 必须出现在两个地方:正常流程末尾 + EXIT HANDLER 异常处理器里,否则一次崩溃就可能让锁永远挂住
  • IS_USED_LOCK() 只能用于诊断,不能用来轮询等待——它自己也会加锁,高并发下反而成瓶颈

视图、临时表、自增 ID 在并发下特别容易翻车

这些不是“只读就安全”的东西:

  • 视图查询会真实访问基表,若含无索引 JOINWHERE 字段,容易触发大量 S 锁,和正在 UPDATE 的事务互相等待,直接死锁
  • 多线程用 SELECT MAX(oid) 算新 ID 插入,必然错乱——A 和 B 线程同时读到 oid=100,都插 101,主键冲突或数据错位
  • 正确做法是建一张 max_oid 表,用 UPDATE max_oid SET current_val = current_val + N 原子获取一批连续 ID,再插入

真正难的不是写锁,而是判断哪一步真需要锁——90% 的并发问题,其实一条带条件的 UPDATE 就能解决,剩下 10% 才轮到锁和事务设计。但多数人一上来就加锁,反而把系统拖慢。

热门栏目