SpringBoot整合Redis分布式锁-为什么不能直接DEL

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

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

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

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

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

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

java 复制代码
String lockValue = FastUniqueId.getNumber();
Boolean result = redisClient.vSetIfAbsent(
        lockKey,
        lockValue,
        expireMillis,
        TimeUnit.MILLISECONDS);

成功后调用方同时持有:

text 复制代码
lockKey   → 锁定哪个资源
lockValue → 这次锁的所有者凭证

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

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

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

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

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

lua 复制代码
if redis.call('get', KEYS[1]) == ARGV[1] then
    return redis.call('del', KEYS[1])
else
    return 0
end

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

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

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

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

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

当前实现要求过期时间:

text 复制代码
大于 1 秒
小于 24 小时
默认 5 秒

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

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

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

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

续期同样使用 Lua:

lua 复制代码
if redis.call('get', KEYS[1]) == ARGV[1] then
    return redis.call('expire', KEYS[1], ARGV[2])
else
    return 0
end

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

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

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

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

当前实现不是这样。

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

java 复制代码
int maxRenewalCount =
        (int) (1 / RENEWAL_INTERVAL_RATIO) + 1;

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

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

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

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

首次加锁使用毫秒单位。

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

java 复制代码
expireMillis / 1000

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

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

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

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

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

MetaLite 提供 @RedisLock

java 复制代码
@RedisLock(
    key = "'lock:api:order:' + #orderId",
    expireMillis = 10000,
    autoRenewal = true
)
public Resp<?> submit(String orderId) {
    // 业务逻辑
}

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

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

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

八、异常时锁会不会释放

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

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

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

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

text 复制代码
正常 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 系列内容将持续更新,围绕核心设计、源码链路、技术取舍与生产实践展开。欢迎关注作者,及时获取后续内容。

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

相关推荐
Zane199422 分钟前
你以为的性能瓶颈,未必是真的瓶颈:用 cProfile 找到真凶
后端·python
请你吃div24 分钟前
Node 后端项目 Docker 自动部署教程(GitHub + 宝塔 + Self-hosted Runner)
后端·docker·node.js
Lovefoolself1 小时前
Electron框架使用vue开发跨平台桌面工具应用-后台日志发送到前台和执行导入ZIP
后端
styshoo1 小时前
[NVSentinel] syslog-health-monitor模块分析
后端
Zane19942 小时前
自定义异常该继承 Exception 还是 RuntimeException,就看这一个问题
java·后端
风曳丷2 小时前
10|攻击如何跨越 Context、阶段与时间
后端
tyung2 小时前
znet 数据编解码:字节流怎么变成消息
后端·网络协议·go
码视野2 小时前
基于 Spring Boot + Vue3 的【高校化学实验室安全准入考试与危化品配伍排查系统】设计与实现(含PRD/三端高保真源码/大屏)
前端·人工智能·spring boot·后端·安全·vue3
LXMXHJ2 小时前
springboot项目测试
java·spring boot·后端·测试