唯一索引不能自动实现高性能分布式锁,必须配合过期检查、持有者校验和重试机制,否则易导致空转、误删或死锁。

不能靠唯一索引“自动”实现高性能分布式锁——它本身不阻塞、不等待,必须配合重试、过期和校验逻辑才能用,否则极易出现空转、误删、死锁。
为什么INSERT + 唯一索引不是开箱即用的锁
很多人以为建好表、写个 INSERT INTO distributed_lock (lock_key, client_id, expire_time) VALUES ('order_1001', 'abc', NOW() + INTERVAL 30 SECOND) 就算加锁成功了。其实这只是原子性判别起点,离“可用锁”还差三步:是否已过期、是否被别人持有、失败后要不要等。MySQL 不会主动阻塞后续请求,INSERT 失败就直接报错(Duplicate entry 'order_1001' for key 'uk_lock_key'),业务层必须自己处理这个错误并决定重试策略。
- 不带
expire_time字段 → 服务宕机后锁永远不释放,变成死锁 - 没存
client_id→ 释放时只能DELETE FROM distributed_lock WHERE lock_key = 'xxx',结果删掉别人刚续期的锁 - 插入后不校验影响行数 → 把主键冲突、字段超长等非锁冲突误判为“抢锁失败”
建表必须包含这四个字段且满足约束
唯一索引只是基础,真正起作用的是字段组合与语义约定。下面这张表结构是生产可用的最小闭环:
CREATE TABLE `distributed_lock` ( `id` BIGINT UNSIGNED NOT NULL AUTO_INCREMENT, `lock_key` VARCHAR(64) NOT NULL COMMENT '资源标识,如 order:1001', `client_id` VARCHAR(64) NOT NULL COMMENT '本机标识,建议 IP+端口+UUID', `expire_time` DATETIME NOT NULL COMMENT '绝对过期时间,非 TTL', `create_time` DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP, PRIMARY KEY (`id`), UNIQUE KEY `uk_lock_key` (`lock_key`) ) ENGINE=InnoDB;
-
lock_key必须是业务维度的最小粒度标识(不能用模糊前缀如order:),否则无法精确释放 -
client_id不能用线程 ID 或随机字符串——它要能被服务实例稳定生成、跨重启一致 -
expire_time必须是DATETIME类型的绝对时间(如NOW() + INTERVAL 30 SECOND),不用TIMESTAMP避免时区转换歧义 - 不要加
ON UPDATE CURRENT_TIMESTAMP到expire_time,否则续期操作会干扰判断
获取锁时必须检查三件事,缺一不可
单纯执行 INSERT 只完成一半工作。一次安全的加锁流程应包含:
- 先尝试
INSERT,若影响行数为 1 → 加锁成功,立刻返回 - 若报错
Duplicate entry→ 立即查表:SELECT client_id, expire_time FROM distributed_lock WHERE lock_key = 'order_1001' - 查到记录后,检查
expire_time :成立则说明锁已过期,用 <code>UPDATE ... WHERE lock_key = ? AND expire_time 尝试覆盖(注意:该 <code>UPDATE必须同时更新client_id和expire_time,否则可能覆盖别人的有效锁) - 如果
UPDATE影响行为 1 → 覆盖成功,相当于“抢过期锁”,可继续;否则说明锁仍有效,当前客户端必须放弃或等待
释放锁必须用条件删除,且不能放在事务里
最常见误操作是:在业务事务中执行 DELETE FROM distributed_lock WHERE lock_key = 'xxx'。一旦事务回滚,锁被删了但业务状态没提交,等于白加;更糟的是,如果事务卡住未提交,锁一直挂着,其他节点全被堵死。
- 释放操作必须独立于业务事务,走单独连接执行
- 删除语句必须同时校验
lock_key和client_id:DELETE FROM distributed_lock WHERE lock_key = 'order_1001' AND client_id = '10.0.1.5:8080:abc123' - 不要依赖
expire_time做删除条件——它只用于加锁阶段的过期判定,释放时只认身份 - 执行后检查影响行数:为 0 表示锁已被别人释放或根本不存在,此时不应报错,而是静默忽略
真正难的从来不是“怎么加锁”,而是“怎么确认锁还归我管”——所有绕过 client_id 校验或忽略 expire_time 检查的设计,上线后都会在凌晨三点准时爆发。











