绝大多数重复处理问题无需显式加锁,应优先使用带WHERE条件的原子UPDATE;例如扣库存、改订单状态等场景,通过WHERE校验状态(如status='pending')并检查影响行数,可彻底避免并发重复处理。

UPDATE带WHERE条件校验比加锁更优先
绝大多数重复处理问题,根本不需要显式加锁——用原子 UPDATE + WHERE 条件就能堵死并发窗口。比如扣库存、改订单状态、标记任务为“已处理”,只要判断逻辑能写进 WHERE,就该首选这条路。
常见错误现象:先 SELECT status FROM orders WHERE id = @id,再根据结果决定是否 UPDATE。两个事务几乎同时查到 status = 'pending',都执行 UPDATE,结果重复处理。
-
UPDATE orders SET status = 'processing' WHERE id = @id AND status = 'pending'执行后检查影响行数:@@ROWCOUNT(SQL Server)、ROW_COUNT()(MySQL)或pg_affected_rows()(PostgreSQL)为 0,说明已被别人抢先处理,直接退出 - 避免在浮点字段、JSON 字段或计算表达式上做精确匹配(如
WHERE ABS(amount - @target) ),精度误差会导致条件失效 - 若需记录修改次数或防重提交,可补一个
version字段做乐观锁:UPDATE ... WHERE id = @id AND version = @expected_version
sp_getapplock只用于业务单号级互斥
sp_getapplock 不是数据库行锁的替代品,而是应用层协调工具,适用场景非常窄:同一笔订单、同一个优惠券 ID、同一条定时任务,**必须且只能被一个进程处理一次**。
常见错误现象:所有存储过程都用硬编码资源名 'order_process' 加锁,结果整个订单系统串行化,QPS 归零。
- 资源名必须带唯一业务标识,例如
'order_' + CAST(@order_id AS VARCHAR(20))或'voucher_use_' + CAST(@voucher_id AS VARCHAR(20)) - 必须包裹在
BEGIN TRANSACTION内,且锁操作前后都要有明确的COMMIT或ROLLBACK,否则锁会随事务挂起,连接池复用后新请求静默阻塞 - 必须设
@LockTimeout(如5000),不能依赖默认-1(无限等待);返回值@result必须检查:-1表示超时,-2是死锁牺牲品,不能只看@@ERROR
SELECT FOR UPDATE只在必要时用,且必须走索引
SELECT ... FOR UPDATE 是真正的行级锁,但代价高、易出错。它不该用来代替原子更新,而应保留在“读取后需多次判断+分支写入”这类复杂逻辑中——比如要根据用户积分、等级、历史行为组合决策是否发放奖励。
常见错误现象:执行 SELECT * FROM users WHERE name = @name FOR UPDATE 却没在 name 上建索引,InnoDB 全表扫描并锁住所有行,等效于锁表。
- 调用前务必
EXPLAIN该语句,确认key列非NULL、rows值合理;理想执行计划是type=const或ref - 避免在
FOR UPDATE的WHERE中对字段做函数操作,如WHERE DATE(created_at) = '2024-01-01'会让索引失效 - 事务内所有路径(包括异常分支)都必须有
COMMIT或ROLLBACK,否则锁永不释放;建议开头加DECLARE EXIT HANDLER FOR SQLEXCEPTION ROLLBACK;
GET_LOCK()不是数据一致性方案
MySQL 没有 sp_getapplock 对等物,GET_LOCK() 是会话级命名锁,不绑定事务、不保证原子性,**绝不能单独用于防止重复写入**。
常见错误现象:压测时出现重复扣款,日志显示两个会话都成功获取了 GET_LOCK('pay_1001', 3),因为锁未释放或连接复用导致残留。
- 锁名必须唯一稳定,推荐格式:
'db_shop_pay_' + CAST(@order_id AS CHAR);纯数字或固定字符串极易冲突 - 必须配超时(如
GET_LOCK('pay_1001', 3)),且检查返回值是否为1;返回0(超时)或NULL(出错)必须拒绝继续 -
RELEASE_LOCK()必须出现在两处:正常流程末尾 +EXIT HANDLER异常处理器里;否则一次崩溃就可能让整条业务线卡住 - 即使用了
GET_LOCK(),底层仍需UPDATE ... WHERE或SELECT FOR UPDATE做最终一致性兜底
实际中最容易被忽略的点是:锁的粒度和生命周期完全由你写的 SQL 和事务控制决定,而不是靠加个函数就万事大吉。一个没提交的事务、一个没走索引的 FOR UPDATE、一个忘记 RELEASE_LOCK() 的异常分支,都会让锁变成系统瓶颈。

















