SpringBoot整合Redis分布式锁-为什么不能直接DEL
2026/8/29 20:19:09 网站建设 项目流程

Redis 分布式锁为什么不能直接 DEL:锁归属与 Lua 原子释放

工程判断:Redis 分布式锁最危险的代码通常不是加锁,而是 finally 里的 DEL。线程持有的锁过期后,另一个线程可能已经拿到同名锁,此时前一个线程直接删除就会误删别人的所有权。

一个可靠的锁至少要绑定持有者标识,并让“判断所有者”和“删除 Key”保持原子;还要明确租期、续期、可重入和业务执行超时。简单 setnx 加 del 只覆盖了演示场景。

MetaLite 的 Redis 封装把锁的所有权校验和释放语义收敛到公共能力,避免业务重复实现危险版本。本文先复现误删时序,再结合 RedisClient 的实现说明这层浅封装为何具有工程价值。

一、锁值为什么必须每次唯一

MetaLite 获取锁时生成一个唯一lockValue

StringlockValue=FastUniqueId.getNumber();Booleanresult=redisClient.vSetIfAbsent(lockKey,lockValue,expireMillis,TimeUnit.MILLISECONDS);

成功后调用方同时持有:

lockKey → 锁定哪个资源 lockValue → 这次锁的所有者凭证

同一个服务实例先后两次获取同一 Key,也应该拥有不同 Value。因为锁的所有权属于“一次获取”,不是某个进程永久拥有。

二、比较和删除为什么必须原子执行

如果 Java 代码先GET比较,再执行DEL,两条命令之间仍有竞争窗口:

GET 发现还是自己的值 ↓ 锁恰好过期,其他实例获得新锁 ↓ DEL 删除新锁

MetaLite 使用 Lua 把判断与删除放进 Redis 一次执行:

ifredis.call('get',KEYS[1])==ARGV[1]thenreturnredis.call('del',KEYS[1])elsereturn0end

只有 Redis 中的值仍等于本次lockValue,才允许删除。

这条脚本解决的是“误释放其他所有者的锁”,不代表业务操作与 Redis 锁获得了数据库事务级原子性。

三、过期时间为什么不可省略

如果持锁进程崩溃、机器断电或网络中断,正常解锁代码不会执行。

没有 TTL 的锁会永久存在,后续请求全部无法进入。

当前实现要求过期时间:

大于 1 秒 小于 24 小时 默认 5 秒

TTL 是防止死锁的最后保障,但也引入新的问题:业务执行时间超过 TTL 时,锁会提前失效。

因此过期时间不是越短越安全。它至少要覆盖正常执行时长、可接受抖动和基础设施延迟,并配合超时监控。

四、自动续期怎样确认自己仍是所有者

开启autoRenewal后,MetaLite 按过期时间的 30% 调度续期任务。

续期同样使用 Lua:

ifredis.call('get',KEYS[1])==ARGV[1]thenreturnredis.call('expire',KEYS[1],ARGV[2])elsereturn0end

它先验证锁值,再延长 TTL。若 Key 已不存在或值已变化,返回 0 并停止当前续期任务。

这避免旧所有者为新所有者的锁续命。

五、当前自动续期不是无限 watchdog

“自动续期”容易让人理解为:业务不结束,锁就一直续。

当前实现不是这样。

最大续期次数由下面的计算得到:

intmaxRenewalCount=(int)(1/RENEWAL_INTERVAL_RATIO)+1;

比例为 0.3 时,得到 4;任务在 finally 中先加次数,再以>判断取消,因此实际还会经历一次边界续期。

无论如何,它是有上限的定期续期,不是随业务执行时长无限延续的 watchdog。

如果任务可能运行远超锁时间,不能只看autoRenewal=true就认为互斥一定覆盖完整执行期。需要根据代码精确计算最长保护窗口,或把续期改成“持锁且业务未结束就继续”的生命周期模型,同时设置总时长保护。

六、毫秒 TTL 为什么在续期时变成了秒

首次加锁使用毫秒单位。

续期脚本却调用 RedisEXPIRE,并传入:

expireMillis/1000

这会截断不足一秒的部分。例如 1500 毫秒续期后会成为 1 秒,而不是 1.5 秒。

当前参数只要求大于 1 秒,并没有要求必须是 1000 的整数倍。

如果希望续期保持与首次加锁一致的精度,更合适的是使用PEXPIRE并直接传毫秒值。

这是典型的单位边界:配置字段写着毫秒,不代表整条执行链都保持毫秒精度。

七、注解怎样把锁与业务方法连接起来

MetaLite 提供@RedisLock

@RedisLock(key="'lock:api:order:' + #orderId",expireMillis=10000,autoRenewal=true)publicResp<?>submit(StringorderId){// 业务逻辑}

Key 可以是常量,也可以通过 SpEL 从方法参数计算。

切面拿到锁后,会把 Key、Value、过期时间和续期标记写进AspectInfo。方法结束后再从同一上下文解锁。

这种方式减少业务代码的 try/finally,但锁粒度仍由业务设计者负责。把所有订单共用一个 Key 会造成不必要串行;只用用户 ID 又可能无法保护具体订单资源。

八、异常时锁会不会释放

入口切面的通用逻辑在finally中执行所有 Handler 的postHandle

SpringScheduledJobExecuteHandler.postHandle会调用解锁,因此业务方法抛出异常时也会进入释放路径。

如果 Redis 解锁失败,TTL 仍是最终兜底。自动续期任务只有解锁成功才立即取消;锁已过期或所有权变化时,旧续期任务会依靠 Lua 比较失败或次数上限停止。

这说明分布式锁需要多重退出路径:

正常 finally 解锁 所有权校验失败不误删 TTL 自动过期 续期任务检测失效后停止

九、锁只能提供互斥,不能替代幂等

即使锁实现完全正确,仍可能发生:

  • 锁过期后旧任务继续执行;
  • Redis 主从切换带来时序窗口;
  • 业务已提交,但响应丢失导致调用方重试;
  • 续期线程长时间停顿;
  • 数据库操作与解锁之间进程崩溃。

关键写操作仍应设计业务幂等,例如唯一约束、状态机、请求号或版本号。

分布式锁降低并发进入概率,业务幂等负责即使重复进入也不产生错误结果。两者不是替代关系。

十、分布式锁要监控什么

至少应观察:

  • 获取成功率和等待/失败次数;
  • 实际执行时间与配置 TTL 的分布;
  • 续期失败次数;
  • 解锁失败次数;
  • 达到最大续期次数的锁;
  • 热点 Key 和持锁最长任务。

MetaLite 当前会在执行超过配置时间、续期失败和达到次数上限时记录日志,为发现配置不合理提供线索。

正确的 Redis 锁不是SET NX加一个DEL。它必须把所有权、原子释放、过期、续期和业务幂等放在同一条故障时间线上理解。


框架简介
MetaLite 是面向企业生产环境的新一代 Java 微服务技术底座。系列文章重点分享代码背后的设计思路、技术取舍与工程实践。

源码基线
JDK 21、Spring Boot 3.2.9、Spring Cloud 2023.0.1、Spring Cloud Alibaba 2023.0.1.3,具体组件版本以项目backend-bom为准。

作者简介
15 年 Spring 体系企业级开发经验,专注于 Java 微服务架构、工程治理与生产实践。

持续更新
MetaLite 系列内容将持续更新,围绕核心设计、源码链路、技术取舍与生产实践展开。欢迎关注作者,及时获取后续内容。

在线演示
演示地址: https://admin.metalite.top/
演示账号: guess
演示密码: admin@2026

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询