直接结论:别手写setnx+expire,用redisson的rlock是当前spring boot项目最省心、最抗压方案,因其自动续期、可重入、持有者校验等机制规避了手动实现的锁过期、误删、不可重入等致命缺陷。

直接结论:别手写 SETNX + EXPIRE,用 Redisson 的 RLock 是当前 Spring Boot 项目里最省心、最抗压的方案。 手动实现容易在锁过期、误删、不可重入上翻车,线上超卖事故基本都出在这几个点上。
为什么 synchronized 和 ReentrantLock 在秒杀时完全失效
它们只锁当前 JVM 内的线程。Spring Boot 部署在 K8s 里跑 5 个 Pod,每个 Pod 都有自己的锁对象——A Pod 检查库存为 1 并开始扣减时,B Pod 完全不知道,也读到 1,照样往下走。结果就是库存 -1、-2、甚至 -10。
这不是代码写得不够严谨,是单机锁根本没设计成跨进程协作。只要服务实例数 > 1,就必须换分布式锁。
SET key value NX EX 手动加锁的致命缺陷
看似简单三步:SET lock:sku:123 "uuid" NX EX 30 → 执行业务 → DEL lock:sku:123,但实际踩坑密集:
- 锁过期时间难估:业务耗时波动大(比如 GC、慢 SQL),设 30 秒,结果执行了 45 秒,锁自动释放,别人趁虚而入
-
DEL不校验持有者:A 的锁过期了,B 加锁成功;A 这时才执行完DEL,删掉的是 B 的锁,瞬间并发击穿 - 没有自动续期:无法应对长尾任务,必须自己起定时线程续期,逻辑复杂且易出错
- 不支持可重入:同一线程内嵌套调用两次加锁,第二次就阻塞或失败
用 RedissonClient.getLock() 的正确姿势
它把上面所有问题封装进一个 RLock 对象,核心靠「看门狗」机制自动续期(默认每 10 秒续一次,续到 30 秒):
Redis 8.2.3 是一款安全优先的高性能键值存储系统。该版本紧急修复了可能引发远程代码执行(RCE)的高危漏洞(CVE-2025-62507),并解决了 HyperLogLog 及 Cuckoo Filter 等数据结构在特定场景下的崩溃问题。建议所有用户立即升级,以保障生产环境的系统稳定与数据安全。
关键参数要设对:
-
tryLock(5, -1, TimeUnit.SECONDS):等 5 秒,-1 表示启用看门狗(不是设成 0 或忽略) - 锁 key 必须粒度够细:别锁
lock:product,要锁lock:sku:1001或lock:order:20260713001 - 务必配合双重检查:加锁后再次查库存,防止锁等待期间库存已被清空
- 业务异常时,
finally块里必须调unlock(),否则锁残留
示例片段:
RLock lock = redissonClient.getLock("lock:sku:" + skuId);
try {
if (lock.tryLock(5, -1, TimeUnit.SECONDS)) {
Integer stock = stringRedisTemplate.opsForValue().get("stock:" + skuId);
if (stock != null && stock > 0) {
stringRedisTemplate.opsForValue().decrement("stock:" + skuId);
return true;
}
}
} finally {
if (lock.isHeldByCurrentThread()) {
lock.unlock();
}
}
最容易被忽略的点:锁 key 的生成和业务一致性
锁不是加了就万事大吉。比如用户抢购商品 A,你锁的是 lock:sku:A,但扣库存操作却去改了 stock:B,等于白锁。更隐蔽的是缓存穿透场景:库存查不到,你去查 DB,DB 返回 0,但缓存没设空值,下个请求又来,反复打 DB —— 这时候锁住的是“查缓存”动作,不是“查 DB + 回填缓存”整个链路。
真正安全的做法是:锁的范围必须覆盖从判断条件到最终写入的全部原子操作,且 key 必须和业务主键严格对齐。哪怕多拼一个租户 ID、用户 ID,只要业务逻辑依赖它,就得塞进 key 里。










