MySQL存储过程不能直接用INSERT…ON DUPLICATE KEY UPDATE模拟锁,因其无法捕获1062异常实现可控重试,且事务边界易混淆;正确做法是应用层负责加锁(如SELECT…FOR UPDATE),存储过程仅执行业务逻辑。

存储过程里不能直接用 INSERT ... ON DUPLICATE KEY UPDATE 模拟锁
很多人第一反应是:在存储过程中写个 INSERT INTO distributed_lock,失败就重试。但问题在于——MySQL 存储过程本身不捕获唯一键冲突异常(如 1062 Duplicate entry)为可控流程,DECLARE HANDLER 虽能捕获,但无法在事务外做“重试等待”,且存储过程调用栈中难以实现阻塞语义。更关键的是,业务逻辑若也写在同一个存储过程中,锁的生命周期和事务边界容易混淆,导致锁未释放或提前释放。
必须用表结构 + 外部控制逻辑配合存储过程
真正可行的做法,是把锁操作“拆出来”:由应用层(如 Java/Python)负责获取/释放锁,存储过程只做纯业务逻辑,并假设调用者已持有有效锁。这时存储过程只需专注安全执行,例如:
- 所有涉及共享资源的 DML 操作(如更新库存、生成订单号)都放在一个存储过程中
- 该存储过程开头加
SELECT ... FOR UPDATE对锁表某行加行锁(需提前插入占位记录) - 用
lock_key作为主键或唯一索引字段,确保SELECT lock_key FROM distributed_lock WHERE lock_key = 'xxx' FOR UPDATE能命中单行并阻塞其他并发请求 - 业务执行完后,由外部程序执行
DELETE或更新expire_time,而非在存储过程中删——否则事务回滚会导致锁残留
SELECT ... FOR UPDATE 在锁表上才真正起作用
注意:只有当 distributed_lock 表有合适索引(如 PRIMARY KEY(lock_key) 或 UNIQUE KEY(lock_key)),SELECT ... WHERE lock_key = ? FOR UPDATE 才会锁定对应行,而不是锁全表。如果只建了普通索引或没索引,MySQL 可能升级为表锁,吞吐暴跌。
常见错误包括:
- 建表时只加了
UNIQUE KEY但没设PRIMARY KEY,导致 InnoDB 用隐式聚簇索引,FOR UPDATE行为不可控 - 查询条件用了函数(如
WHERE UPPER(lock_key) = 'XXX'),使索引失效,触发全表扫描+锁表 - 事务未显式
COMMIT或ROLLBACK,导致行锁长期持有,阻塞后续所有请求
过期清理不能依赖存储过程定时触发
有人想写个存储过程每天跑一次 DELETE FROM distributed_lock WHERE expire_time ,但这解决不了实时性问题:服务崩溃后,锁可能卡住几十秒甚至几分钟,而定时任务间隔通常是小时级。正确做法是:
- 应用层获取锁时,始终设置
expire_time = NOW() + INTERVAL N SECOND(N 至少为业务最大耗时的 2 倍) - 另起一个轻量后台任务(如 Spring Scheduler / cron job),每 10–30 秒扫描
expire_time索引字段,清理过期记录 - 绝不让存储过程承担“看门狗”职责——它没有心跳机制,也无法感知客户端是否还存活
最易被忽略的一点:expire_time 字段必须配 INDEX,否则清理 SQL 会全表扫描,高并发下直接拖垮数据库。


















