优先使用 UPDATE ... WHERE 带条件校验,因其是原子操作,可避免超卖;仅当业务逻辑无法单条 SQL 实现时才加锁,且需规范使用事务、锁名和超时设置。
优先用 UPDATE ... WHERE 带条件校验,而不是先查再更新——这是最轻量、最可靠、也最容易被忽略的解法。
UPDATE ... WHERE 比 SELECT + UPDATE 更安全“先查库存再扣减”看似合理,但两个线程几乎同时执行时,会都看到 stock = 10,都判断“够扣”,然后双双执行 UPDATE,结果 stock 变成 8,超卖 2 单。这个时间窗口无法靠 Sleep 或重试堵住。
UPDATE products SET stock = stock - 1 WHERE id = 1001 AND stock >= 1 是原子操作:条件不满足就一整条语句不生效,根本不会进入“扣减逻辑”。执行后只需检查影响行数:
ROW_COUNT()
pg_affected_rows()
@@ROWCOUNT
返回 0 就说明失败(已被别人抢走或已售罄),无需锁、不依赖隔离级别、也不需要额外字段。
当业务逻辑无法塞进单条 UPDATE 的 WHERE 条件里(比如要跨多张表校验、调外部接口后再决定是否扣减),才考虑锁。但锁极易滥用:
sp_getapplock 必须包裹在 BEGIN TRANSACTION 内,且事务结束前不能退出存储过程,否则锁残留'order_pay_' + CAST(@order_id AS VARCHAR(20)),硬编码 'mylock' 会让所有请求串行@LockTimeout 必须设具体毫秒值(如 5000),别用默认 -1(无限等待)IF @result 表示失败(-1=超时,-2=死锁牺牲品),不能只看 <code>@@ERROR
MySQL 没有 sp_getapplock 这类事务安全的应用锁,GET_LOCK() 是唯一跨会话方案,但它本身不是事务安全的:
'db_shop_voucher_use_' + CAST(@voucher_id AS CHAR),纯数字或固定字符串极易冲突RELEASE_LOCK() 必须出现在两个地方:正常流程末尾 + EXIT HANDLER 异常处理器里,否则一次崩溃就可能让锁永远挂住IS_USED_LOCK() 只能用于诊断,不能用来轮询等待——它自己也会加锁,高并发下反而成瓶颈这些不是“只读就安全”的东西:
JOIN 或 WHERE 字段,容易触发大量 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% 才轮到锁和事务设计。但多数人一上来就加锁,反而把系统拖慢。