Redis分布式锁实战:三个真实事故与Lua脚本原子性改造

Redis分布式锁Lua脚本
于 2026-10-02 08:12:20 修改
·本内容遵循CC 4.0 BY-SA版权协议

开门见山,先说结论:Redis 分布式锁这东西,网上教程一抓一大把,但真正拿到项目里用,能一次写对不出问题的,说实话不多。我最近在维护一个订单调度系统,表面上看就是一个普通的后端服务,结果在分布式锁上接连栽了三个跟头,每次都是线上告警把我叫醒的那种。今天不聊理论,就着这三个真实场景,把 Redis 分布式锁和 Lua 脚本的实战细节、踩坑过程原原本本扒一遍。

先交代一下背景,这套系统是 Spring Boot 架构,注册中心用的 Nacos,服务部署了三个节点,Redis 用的 6.2 集群版。业务本身不算复杂,但涉及定时任务、库存扣减和跨服务状态同步三条链路,刚好把分布式锁的典型问题都踩了个遍:锁的原子性、锁的误删、锁超时与业务时长不匹配。三个问题听着是同一个东西,但每个的坑法都不一样,排查起来一个比一个隐蔽。

这篇文章适合谁看?如果你的项目里已经用了 RedisTemplate 做分布式锁,或者正准备引入、只是停留在 setnx 加 expire 这种最简单写法,那这篇文章应该能帮你省下不少半夜爬起来看日志的时间。我会把每个场景的代码、Redis 命令、异常现象和修复方案都贴出来,你可以直接照着改。

1. 三个真实场景的完整复盘

1.1 场景一:定时任务重复执行,value 用的当前日期

这个场景最早出现。系统里有一个每小时跑一次的结算任务,负责把上一个小时的订单数据做汇总。当时为了确保三个节点只有一个节点执行,团队成员用 RedisTemplate 写了个最原始的分布式锁,核心逻辑就是 setIfAbsent。

JAVA
Boolean locked = redisTemplate.opsForValue()
.setIfAbsent("lock:settle", dateStr, 60, TimeUnit.SECONDS);
if (Boolean.TRUE.equals(locked)) {
try {
doSettle();
} finally {
redisTemplate.delete("lock:settle");
}
}

这里有个细节,value 存的是当前日期字符串,比如 2025-01-12。为什么这么设计?因为我们的业务结算按天隔离,如果今天是 1 月 12 日,锁的 value 就是 1 月 12 日,任务执行完删除锁之前,会再校验一次 value 是不是当天的值。

这个场景正常跑了好几个月,一直没出问题。真正出问题是在一次发布窗口:运维同学把一个节点优雅停机,但停机前这个节点正在执行结算任务。锁还没释放,另外两个节点已经启动了,其中一个节点拿到锁之后开始执行新一轮结算,而老节点因为 GC 停顿,finally 块里的 delete 延迟了大概 3 秒才执行。结果就是,新节点刚拿到锁还没跑完,老节点的 delete 直接把新节点的锁删了。第三个节点瞬间也拿到锁进来执行,三个节点同时跑结算任务,数据重复汇总,账单金额翻了三倍。

这个场景暴露的第一层问题是:释放锁之前没有校验 value 是否还是自己的。网上所有教程都强调过这一点,但实际项目里因为加锁和释放锁的逻辑分散在两个方法里,写着写着就漏了。而这恰恰是标题里提到的“value 为当前日期”这个设计的初衷——拿日期做 value,本来是想在释放前做一次持有者校验的,但代码里只校验了日期等于当天,根本没有校验这个锁是不是自己这个节点加的。

1.2 场景二:库存扣减,先查库存再扣减的非原子问题

第二个场景来自一个秒杀活动。业务逻辑很简单:用户下单时先查 Redis 里的库存 key,如果大于 0 就执行扣减,然后落库。当时用的是 get 加 decrement 两步操作。

JAVA
Integer stock = (Integer) redisTemplate.opsForValue().get("stock:1001");
if (stock != null && stock > 0) {
redisTemplate.opsForValue().decrement("stock:1001");
createOrder(userId, skuId);
}

这个逻辑从单机角度看完全没问题,但并发一上来就崩了。三个节点同时收到请求,同时 get 到库存为 1,同时判断大于 0,然后同时 decrement。虽然 Redis 是单线程执行 decrement,最终库存变成 0 不会变负数,但三个请求都走入了 createOrder,生成了三笔订单,实际库存只有 1 件。这就是典型的“先查后改”竞态条件。

当时第一个想法是加分布式锁,把 get 和 decrement 包在锁里。但仔细一想,库存扣减这种操作如果每次都走分布式锁,锁的获取和释放开销太大,而且一旦锁服务抖动,整个下单链路就全堵住了。后来改成用 Lua 脚本把“查库存 + 扣库存”两步合并在 Redis 服务端原子执行,才从根本上解决了问题。

1.3 场景三:业务执行时间超过锁过期时间,锁被提前释放

第三个场景是最隐蔽的。系统里有一个订单状态同步任务,A 服务生成订单后通过 MQ 通知 B 服务处理,B 服务处理完需要回调 A 服务更新状态。为了保证同一笔订单不会被重复处理,在 B 服务收到消息后先抢一把锁,锁的 key 是订单号,过期时间设了 10 秒。

问题出在一笔异常订单上。B 服务在处理这笔订单时,需要调用一个外部仓储接口,结果那个接口超时了,B 服务内部重试了 3 次,总共耗时 25 秒。10 秒锁过期后,MQ 重试机制又投递了一条相同消息,另一个节点顺利拿到锁进来处理。此时两个节点在同时处理同一笔订单,一个在等外部接口返回,一个已经开始写订单状态。最终结果是订单状态被后完成的那个节点覆盖,数据不一致。

这个场景和前两个不一样,前两个是“锁没用好导致并发进入”,这个场景是“锁本身过期了,导致锁形同虚设”。后面我会专门讲怎么处理锁超时和业务执行时间不匹配的问题。

2. 从 setnx 到 Lua 脚本:原子性是王道

2.1 setnx 加 expire 的两步陷阱

第一个场景里用的 setIfAbsent,对应 Redis 命令就是 SET key value NX,这个命令本身是原子的,没问题。问题出在很多老代码还在用 SETNX 加 EXPIRE 两条命令组合。SETNX 执行完还没执行 EXPIRE,进程就挂了,锁变成了永不过期的死锁。Redis 官方早就推荐直接用 SET key value NX EX seconds 一条命令搞定,Spring 的 setIfAbsent 就是这个封装。

但这个场景真正的问题不在加锁,在释放锁。如果删除锁的逻辑不校验持有者,任何节点都可以删掉别人的锁。通用的校验方式是:删除前先 get 锁的 value,如果是自己的唯一标识才执行 delete。但在 Redis 里,get 和 delete 是两条命令,非原子,中间依然有被其他节点干扰的可能。要真正做到原子删除,必须用 Lua 脚本。

当时我们先把释放锁的逻辑改成了这样:

LUA
if redis.call("get", KEYS[1]) == ARGV[1] then
return redis.call("del", KEYS[1])
else
return 0
end

这段脚本的意思是:比较锁的 value 是否为当前持有者,是才删除。Spring 里可以用 DefaultRedisScript 封装执行。

2.2 原子性在并发场景里意味着什么

要深入理解 Lua 脚本的作用,得先想明白一个问题:为什么 Redis 单线程模型下还会有并发问题?

我的理解是,Redis 的原子性保证的是单条命令执行的原子性,而不是一组业务操作的原子性。你用 GET 查库存是原子的,用 DECR 扣库存也是原子的,但“查到库存大于 0”和“执行扣减”之间的间隙,Redis 并不会帮你阻塞其他客户端。这时两个客户端都完成了 GET,都看到库存是 1,然后先后执行 DECR,结果就是超卖。

Lua 脚本之所以能解决这个问题,是因为 Redis 从 2.6 版本开始就把 Lua 脚本当作一条命令来执行。脚本里的所有 Redis 操作在脚本执行期间不会被其他命令插入,Redis 服务端会保证整段脚本的原子性。这就像你把“检查库存”和“扣减库存”两件事打成一个大包,一次性提交给 Redis,Redis 要么不干,要么一口气干完,中间不接其他活。

一个生活化的类比:你去 ATM 取钱,插卡、输入密码、查询余额、取款、扣减余额,这些步骤如果是分开的命令,中间可能被插队或者中断。如果 ATM 把整个取款流程封装成一个“取款事务”,一步到位执行完,别人就没办法在你操作到一半的时候动你的账户。

2.3 Lua 脚本在分布式锁中的两种核心用法

分布式锁场景里,Lua 脚本主要干两件事。

第一件就是上面说的原子释放锁。为什么必须原子?因为“先 get 校验再 del”的两步操作存在时间窗口。极端情况如下:节点 A 持有锁,值是一个 UUID,执行 get 校验,确认锁是自己的;但在执行 del 之前,锁因为过期时间到了被 Redis 自动清除,节点 B 立刻 set 成功拿到锁;此时节点 A 才执行 del,把 B 的锁删了。这个窗口极小,但线上就是这么邪门,越是极端情况越容易触发。Lua 脚本把“get 比较 + del”放到服务端原子执行,就不会给其他节点插队的机会。

第二件是组合业务操作,比如库存扣减。我们把“判断库存是否充足 + 扣减库存 + 返回结果”整体写进 Lua 脚本,整个过程原子完成,单机 Redis 下彻底消除超卖。脚本很简单:

LUA
local stock = tonumber(redis.call("get", KEYS[1]))
if not stock then
return -1
end
if stock <= 0 then
return 0
end
redis.call("decr", KEYS[1])
return 1

注意几个细节:get 返回的是字符串,必须用 tonumber 转数字;第一个参数返回 -1 表示 key 不存在,0 表示库存不足,1 表示扣减成功。调用方根据返回值决定是否创建订单。

3. 三个场景逐一拆解,内含完整代码和命令

3.1 定时任务锁的最终改造:唯一标识 value 加 Lua 释放

回到第一个场景。我们当时的 value 用的是当前日期字符串,这个设计有个致命问题:三个节点同一天加的锁,value 完全相同,光靠比较 value 无法区分是不是自己的锁。改成用 UUID 作为每个加锁操作唯一标识是必须的。加锁时生成 UUID,释放时带上 UUID 走 Lua 脚本校验。

这是最终版的加锁工具类核心逻辑:

JAVA
public boolean tryLock(String key, String requestId, long expireSeconds) {
return Boolean.TRUE.equals(redisTemplate.opsForValue()
.setIfAbsent(key, requestId, expireSeconds, TimeUnit.SECONDS));
}
 
public boolean unlock(String key, String requestId) {
String script = "if redis.call('get', KEYS[1]) == ARGV[1] then " +
"return redis.call('del', KEYS[1]) " +
"else return 0 end";
DefaultRedisScript<Long> redisScript = new DefaultRedisScript<>(script, Long.class);
Long result = redisTemplate.execute(redisScript, Collections.singletonList(key), requestId);
return Long.valueOf(1).equals(result);
}

注意两个地方。第一,DefaultRedisScript 的 resultType 声明为 Long.class,因为脚本返回的是 del 的结果或者 0。第二,redisTemplate.execute 的泛型参数,老版本 Spring Data Redis 会有序列化问题,这个我在第四章会详细说。

改造完成之后,第一场景的问题就彻底解决了。即使老节点的 finally 块延迟执行,魔改后的释放逻辑比对发现锁的 value 不是自己的 UUID,直接返回 0 不删除,不会误伤新节点的锁。

3.2 库存扣减 Lua 脚本的完整实现与参数传递

库存场景的 Lua 脚本,上面给了核心代码。这里补全 Java 调用层面完整的写法,因为参数传递的坑也不少。

JAVA
@Autowired
private RedisTemplate<String, Object> redisTemplate;
 
private static final DefaultRedisScript<Long> STOCK_SCRIPT;
static {
STOCK_SCRIPT = new DefaultRedisScript<>();
STOCK_SCRIPT.setResultType(Long.class);
STOCK_SCRIPT.setScriptText(
"local stock = tonumber(redis.call('get', KEYS[1])) " +
"if not stock then return -1 end " +
"if stock <= 0 then return 0 end " +
"redis.call('decr', KEYS[1]) " +
"return 1"
);
}
 
public long deductStock(String key) {
return redisTemplate.execute(STOCK_SCRIPT, Collections.singletonList(key));
}

调用时传一个 KEYS,ARGV 不需要。如果脚本里需要参数,比如扣减数量、最大可扣数量,可以把它们放在 ARGV 列表里传进去。这里有个容易犯的错:execute 方法的最后一个参数是可变参数 Object...,如果脚本用了 ARGV,务必把 ARGV 的值按顺序传进去,别把 KEYS 和 ARGV 搞混了。

tonumber 这个转换为什么不能省?因为 Redis 的 get 命令返回的一定是字符串,即使你存进去的时候是 Integer,取出来也是字符串。Lua 里字符串和数字做比较,类型不一致会直接报错或者结果不符合预期。所以第一步必须是 tonumber。

如果 key 不存在,redis.call('get', KEYS[1]) 返回的是 false,在 Lua 里 not false 为 true,所以进入第一个分支返回 -1。这个分支很重要,否则脚本会在 tonumber(false) 上报错。

3.3 锁续期:用看门狗思路解决业务超时问题

第三个场景,业务执行时间超过锁过期时间,有两个改造方向。

方向一:把锁的过期时间调大,比如从 10 秒调成 60 秒。这能解决一部分问题,但治标不治本。外部接口超时可能是分钟级的,调再大也扛不住。而且锁过期时间越长,节点挂掉后锁被占用的时间就越长,对系统可用性影响越大。

方向二:给锁加续期机制,类似 Redisson 的看门狗(Watchdog)思路。锁快过期时,如果业务还没执行完,自动给锁续期。续期不是无限续,可以设置一个合理的上限,比如最多续到 5 分钟,超过这个时间直接放弃处理并告警。

我们不引 Redisson 的话,自己写续期也不复杂。核心是用一个定时任务周期性地检查锁的剩余过期时间,如果小于阈值,就执行 EXPIRE 命令给锁续期。但这里有个前提,必须确认锁还是自己的,所以续期操作也要用 Lua 脚本。

LUA
if redis.call("get", KEYS[1]) == ARGV[1] then
return redis.call("expire", KEYS[1], ARGV[2])
else
return 0
end

写一个简单的续期守护线程:

JAVA
public void renewLockWithDaemon(String key, String requestId, long expireSeconds, long renewThresholdSeconds) {
ScheduledExecutorService scheduler = Executors.newSingleThreadScheduledExecutor();
scheduler.scheduleAtFixedRate(() -> {
Long ttl = redisTemplate.getExpire(key, TimeUnit.SECONDS);
if (ttl != null && ttl > 0 && ttl < renewThresholdSeconds) {
String script = "if redis.call('get', KEYS[1]) == ARGV[1] then " +
"return redis.call('expire', KEYS[1], ARGV[2]) " +
"else return 0 end";
DefaultRedisScript<Long> redisScript = new DefaultRedisScript<>(script, Long.class);
redisTemplate.execute(redisScript, Collections.singletonList(key), requestId, expireSeconds);
}
}, 0, 3, TimeUnit.SECONDS);
}

这个方案有几个细节需要打磨。续期线程什么时候关闭?业务执行完释放锁之后一定要 shutdown 掉,否则线程一直跑着,还会给已经释放的锁续期——当然 Lua 脚本里的持有者校验会拦掉这种情况,但线程本身不能泄露。其次,续期线程是单机线程,如果执行续期的这个节点本身就卡死了,续期自然停止,锁会在过期后被其他节点获取,这种极端情况只能靠告警兜底。

考虑到我们当时的业务场景,最终没有引入完整的看门狗,而是选了一个折中方案:锁过期时间从 10 秒调到 30 秒,同时给外部接口调用加了一个 5 秒的熔断,让业务最长执行时间不超过 20 秒。这样即使最坏情况发生,30 秒的过期时间也足够覆盖。但这只是针对那个特定接口的优化,如果业务时间不可控,还是老老实实上续期机制。

4. 代码之外的坑:序列化、超时重试、环境差异

4.1 RedisTemplate 序列化问题导致 Lua 脚本值比对失败

这个坑藏得很深,排查了我一个下午。现象是:加锁成功,但释放锁时 Lua 脚本里的 get 比对始终不相等,锁一直删不掉,直到过期时间到了才自动释放。

原因出在 RedisTemplate 的序列化器上。项目中 RedisTemplate 默认用的是 JdkSerializationRedisSerializer,value 会以二进制形式存储。当你 setIfAbsent("lock:key", "uuid", ...) 存进去的是一个序列化后的二进制对象,但你在 Lua 脚本里执行 redis.call("get", KEYS[1]),返回的是字符串。拿"uuid"这个字符串去和二进制序列化后的内容做比较,当然不相等。

解决办法有两个。一个是改 RedisTemplate 的 value 序列化为 StringRedisSerializer:

JAVA
@Bean
public RedisTemplate<String, Object> redisTemplate(RedisConnectionFactory factory) {
RedisTemplate<String, Object> template = new RedisTemplate<>();
template.setConnectionFactory(factory);
StringRedisSerializer stringSerializer = new StringRedisSerializer();
template.setKeySerializer(stringSerializer);
template.setValueSerializer(stringSerializer);
template.setHashKeySerializer(stringSerializer);
template.setHashValueSerializer(stringSerializer);
template.afterPropertiesSet();
return template;
}

另一个是直接用 StringRedisTemplate 来操作锁相关 key,因为它的 value 序列化器默认就是 StringRedisSerializer,不需要额外配置。我推荐后者,因为改动小,不会影响系统里其他用默认序列化器缓存的业务数据。

4.2 Lua 脚本执行超时与 Redis 阻塞风险

Lua 脚本在 Redis 里是原子执行的,这意味着脚本执行期间,Redis 不会处理任何其他命令。如果脚本写得有性能问题,比如在脚本里循环遍历一个大集合,或者执行了 KEYS 这种全量匹配命令,整个 Redis 实例会被卡住,所有读写请求全部阻塞。

我们当时在调试脚本时不小心敲过一个 redis.call("keys", "*"),本地测试没感觉,一上测试环境整个 Redis CPU 就飙到 100%,所有业务接口全部超时。吸取教训之后,所有线上脚本都遵循两个原则:第一,脚本内的操作数量控制在个位数,不写循环;第二,脚本涉及的 key 必须是确定的、通过传入 KEYS 指定的,不作范围查找。

另外,Spring 执行 Lua 脚本默认走 EVAL 命令。有些 Redis 集群版本对 Lua 脚本的限制严格一些,比如要求所有 key 在同一个 slot,跨 slot 的脚本会直接报错。这在我们用的 Redis 集群版上没遇到,但如果是自建分片集群,需要确认 key 的设计是否满足这个约束。

4.3 加锁超时和重试策略

分布式锁不是只有加锁和释放锁,业务代码里拿不到锁的情况怎么处理,同样重要。很多项目里的写法是拿不到锁直接抛异常,或者静默跳过。这两种都太粗暴。

更合理的做法是设置一个合理的等待时间。比如定时任务场景,如果拿不到锁说明其他节点正在执行,这时候直接退出本次任务就行,不需要重试。但库存扣减场景不太一样,用户下单等不起,拿到锁失败应该快速返回失败结果,或者走一个短暂的重试逻辑,比如 100 毫秒间隔重试 3 次。

JAVA
public boolean tryLockWithRetry(String key, String requestId,
long expireSeconds, long waitMillis, int retryTimes) {
int attempt = 0;
while (attempt < retryTimes) {
if (tryLock(key, requestId, expireSeconds)) {
return true;
}
try {
Thread.sleep(waitMillis);
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
return false;
}
attempt++;
}
return false;
}

这里还有一个细节,重试前可以用 getExpire 查一下锁的剩余时间,如果剩余时间还很长,说明持有者可能正在执行一个长任务,这时候重试大概率还是失败,不如多等一会儿。如果剩余时间很短,说明锁马上就过期了,可以快速重试。

4.4 Docker 部署和环境差异带来的坑

项目里 Redis 是用 Docker Compose 部署的,测试环境和生产环境的 Redis 版本不一致,也踩过一次锁不生效的坑。原因是测试环境的 Redis 版本是 5.x,生产是 6.x,有些命令在 5.x 上行为有细微差异。具体表现在 SET key value NX EX seconds 这条命令上,5.x 的行为没有问题,但当时项目里有一段老代码用的 SETNX 配合 EXPIRE,在 5.x 上偶尔会出现锁不生效的现象,升级到 6.x 后稳定很多。

这不是我负责的模块,但排查时顺手整理了生产环境 Redis 部署的几个要点:开启持久化避免重启丢数据、设置 maxmemory 防止内存爆掉、给锁相关的 key 加上统一前缀方便排查。用 Docker 部署时注意,redis.conf 里的 appendonly yes 必须配上,否则 Redis 容器重启后所有锁数据凭空消失,瞬间可能出现多个节点同时持有“同名锁”的假象。

拉取 Redis 镜像时也注意一下,官方镜像名称就是 redis,tag 最好明确到小版本,比如 redis:6.2.5。不要用默认的 latest,这个坑很多人踩过:本地是 6.2,线上是 7.0,命令行为差异导致的诡异问题排查起来非常耗时。

5. 常见问题速查表与排障思路

5.1 分布式锁高频问题一次说清

问题现象 可能原因 排查方向 解决方案
锁删除不掉,直到过期才释放 value 序列化方式不一致 检查 RedisTemplate 序列化器,对比 Lua 脚本中 get 返回的字符串 改用 StringRedisTemplate 或统一 String 序列化
多个节点同时进入临界区 释放锁时未校验持有者,或 get+del 非原子 查看锁释放代码是否有 Lua 校验 使用带持有者校验的 Lua 脚本释放锁
锁提前释放导致并发问题 业务执行时间超过锁过期时间 查看业务耗时和锁过期时间配置 调大过期时间或实现锁续期机制
Redis 实例 CPU 飙升 Lua 脚本内执行了 keys 或大循环 检查慢日志和脚本内容 重写脚本,避免范围查找和循环
锁 key 残留导致后续加锁失败 未设置过期时间或节点宕机 检查 Redis 中锁 key 的 TTL 统一使用 set ex nx 原子加锁
Redis 重启后锁全部丢失 未开启持久化 检查 appendonly 配置 开启 AOF 持久化

5.2 从告警到定位的三个排查步骤

如果线上出现疑似分布式锁问题,我的排查顺序是这样。

第一步,看 Redis 里锁 key 的当前状态。用 GET lock:xxx 看 value 是谁的,用 TTL lock:xxx 看还剩多少秒过期。如果 value 对应的节点已经不存在了,但 TTL 还很长,说明是有节点持锁后异常退出,锁没释放,等待过期即可。如果 TTL 已经过了但业务还在跑,那就是锁过期时间和业务时长不匹配。

第二步,查业务日志里加锁和释放锁的时间戳。正常的流程应该是先加锁、执行业务、释放锁,时间线是直的。如果释放锁的时间点明显早于业务结束时间点,说明锁被提前释放了,要么是误删,要么是过期。

第三步,统计并发进入的业务日志数量。如果同一个业务 key 在同一时间窗口内出现多份执行日志,基本可以确认锁失效。

5.3 三道分布式锁面试题背后的真实考点

顺带聊一下,这个项目做完之后,我对网上常见的几道分布式锁面试题有了新的理解。

第一题:“Redis 分布式锁怎么实现?”很多答案能背出 setnx、expire、UUID 这些关键词,但面试官真正想看的是你有没有处理过释放锁的原子性问题。把 Lua 脚本拿出来讲,再结合项目中误删锁的实际案例,说服力完全不同。

第二题:“Redisson 的看门狗机制了解吗?”这个问题的核心是考你对锁过期时间和业务执行时间不匹配的理解深度。没有在真实项目里踩过这个坑,大概率只能复述概念,说不出为什么要续期、续期失败怎么办。

第三题:“Redis 分布式锁和 Zookeeper 分布式锁有什么区别?”这题表面考技术对比,实际考的是你对 CAP 理论的理解。Redis 是 AP 模型,主从切换时可能丢失锁;Zookeeper 是 CP 模型,锁的可靠性更高但性能略差。如果项目对数据一致性要求极高,Redis 锁确实不是最优解。

这些问题都能在这三个实战场景中找到对应的影子。面试的时候不是背题,而是把你踩坑、排查、修复的过程讲清楚,这比任何标准答案都有说服力。

6. 我对 Redis 分布式锁最终的使用心得

写了这么多,最后聊点我自己的体会。

分布式锁这个东西,代码量很少,但每个细节都能挖出问题。从最初的 setnx 加 expire,到 set ex nx 原子加锁,再到 Lua 脚本原子释放,最后到锁续期机制,每一步都是在真实线上事故里逼出来的。如果你还在用最原始的写法,我建议至少先把释放锁的 Lua 校验补上,这一步能堵住大多数误删锁的问题。

另外,Lua 脚本在 Redis 里的原子性是一把双刃剑。它确实能解决竞态问题,但也意味着脚本不能太复杂,不能做耗时的操作。我的建议是,脚本里只放必要的 Redis 操作,业务逻辑尽量留在 Java 层,这样既保证了关键操作的原子性,又不会因为脚本阻塞 Redis 影响其他业务。

最后再分享一个小技巧:调试分布式锁相关问题,打开 Redis 的 monitor 命令行,实时观察锁 key 的读写时序,比看日志直观得多。在测试环境执行 redis-cli monitor,然后触发一次业务请求,你就能看到加锁、查锁、释放锁的完整命令流,任何异常的先后顺序都会暴露出来。这个方法我用了很多次,每次都能迅速定位问题。

Redis 分布式锁不是银弹,但它确实是目前最简单、最经济、可落地性最强的分布式互斥方案。只要理解了它的边界,知道它在极端情况下的失效模式,并且做好兜底,完全可以在生产环境放心使用。

Redis Lua脚本原子性原理与实战:从并发扣款到分布式锁
本文深入解析Redis Lua脚本实现原子性的底层机制,依托Redis单线程模型,将多命令封装为不可中断的执行单元;对比MULTI/EXEC事务的局限性,阐明Lua脚本在分布式锁、并发扣款、库存控制等场景中的核心优势;同时强调脚本轻量化、键参数合规、集群适配及避坑要点,为高一致性Redis应用提供关键技术支撑。
weixin_34318326
356
Redis+Lua实战:用脚本实现分布式锁的完整避坑指南
本文详解如何利用Redis与Lua脚本构建高可靠的分布式锁,涵盖原子性锁获取/释放、自动续期、可重入机制等核心能力,并分析Redlock适用性、监控指标、TTL调优及典型坑点(如锁误删、死锁、时钟偏移)。重点强调Lua脚本在保证多命令原子执行上的不可替代作用,适用于电商库存扣减等高并发场景。
郑多强
432
Redis——由分布式锁造成的重大事故
本文分析了一次因飞天茅台抢购导致的超卖事故,深入探讨了Redis分布式锁失效的原因,包括线程阻塞、锁覆盖及非原子性库存校验等问题。并提出了基于LUA脚本的相对安全的分布式锁实现和原子性库存校验的解决方案,最终通过优化实现了在分布式锁失效情况下仍能避免超卖。
兵临奇点
1078
Redis Lua脚本原子性深度解析:原理、场景与避坑指南
本文深入解析Redis Lua脚本原子性的底层原理,基于单线程事件循环模型,阐明其执行序列、状态观察和副作用生效三层原子性保障;明确边界限制如不跨键、无外部交互、超时风险等;对比Redis事务机制,指出Lua在分布式锁、库存扣减、限流器等场景的不可替代性;并给出集群哈希槽约束、脚本预加载、幂等设计等生产避坑指南。
weixin_34246551
414
Redis脚本实战:从Lua原子性到分布式锁,避开性能坑
本文系统讲解Redis内嵌Lua脚本的核心原理与高频应用场景,涵盖原子性保障机制、分布式锁实现、接口限流、库存扣减及缓存一致性治理。深入剖析SCRIPT LOAD/EVALSHA缓存管理、集群模式下的KEYS约束与Hash Tag用法、Redis Functions新特性,并重点揭示NOSCRIPT错误、脚本超时、BUSY阻塞、误用KEYS命令等典型性能与一致性风险,强调脚本轻量、确定性、避免复杂计算等最佳实践。
小狐狸与小道士
307
Redis篇2——Redis深度剖析:从SetNX到Redisson,分布式锁的进化之路
本文深入探讨Redis分布式锁的发展历程,从基础的SetNX命令到结合Lua脚本保障原子性,再到Redisson实现可重入与看门狗机制。进一步分析主从切换带来的安全隐患,并介绍RedLock和MultiLock如何通过多节点共识提升可靠性,适用于高并发与金融级场景。
不只是个程序员
1854
【黑马点评】- Redis 分布式锁进化史:从 SETNX 到 Lua 脚本
本文围绕Redis实现分布式锁的技术演进展开,依次分析SETNX基础方案、引入UUID身份标识防范误删锁、以及采用Lua脚本保障解锁操作的原子性。重点涵盖可见性、互斥性、高可用、安全性和性能等核心要素,并指出TTL续期问题为后续Redisson看门狗机制埋下伏笔。
Drifter_yh
728
Redis Lua脚本与分布式锁实现深度解析
本文深入解析Redis中基于Lua脚本实现分布式锁的核心机制,涵盖EVAL命令的原子性本质、典型实现误区(如竞态条件、超时泄漏、集群slot限制)、健壮锁方案设计(TTL黄金法则、hash tag强制路由)、性能优化(SCRIPT LOAD缓存、连接池配置)、生产陷阱(NTP不同步导致死锁、热点key雪崩)及RedLock适用性评估。强调Lua脚本内原子性与外部操作边界的区分,并提出分层锁架构以降低Redis压力。
浩浩耗
140
实战高并发秒杀:从单机锁失效到 Redis 分布式锁的深度进化
本文详解高并发秒杀场景下,从synchronized本地锁失效到基于Redis的分布式锁实现全过程。重点剖析SETNX的误删漏洞及原子性问题,并通过Lua脚本保障解锁操作的原子性,最终构建可靠的分布式锁方案,适用于多JVM集群环境下的超卖防控。
普普通通的小青年
1025
秒杀商品超卖事故:Redis分布式锁请慎用!
一次飞天茅台抢购活动导致的超卖事故,揭示了Redis分布式锁的隐患。文章分析了事故原因,包括分布式锁失效、非原子性库存校验等问题,并提出了基于Lua脚本的安全解锁和库存校验方案。
程序员黑哥
463
Redis分布式锁与Lua脚本实战:高并发秒杀防超卖方案解析
本文围绕高并发电商秒杀场景,深入解析Redis分布式锁与Lua脚本协同防超卖的核心方案。重点涵盖分布式锁的互斥性、防误删、自动续期等关键要求;利用Lua脚本实现库存检查与扣减的原子操作,避免临界区过长;剖析锁过期、主从切换、重试风暴等生产级风险及应对策略;并提出分段库存、短临界区优化等进阶思路。所有方案均基于Spring Boot集成Redis的真实代码实践。
Tina 小姐姐
104
生产事故复盘:你写的Redis分布式锁真的安全吗?万字图解Redisson锁底层原理
本文通过一次生产事故复盘,深入分析Redis分布式锁的安全隐患,系统梳理从SETNX到Redisson看门狗机制的演进过程,结合Lua脚本和源码解析,揭示Redisson如何保障锁的原子性、可重入性和高可用性,并探讨主从切换、锁续期等关键问题的解决方案。
码到三十五_
1208
Redis——由分布式锁,造成的一起重大事故
一次飞天茅台抢购活动导致的超卖事故,揭示了Redis分布式锁在高并发场景下的隐患。文章剖析了事故原因,包括分布式锁失效、非原子性库存校验等问题,并提出基于Lua脚本的优化方案,最终通过应用缓存实现了更安全高效的库存管理。
Java领域佼佼者
308
由 Redis 分布式锁造成的重大事故
本文分析了一次因Redis分布式锁失效导致的飞天茅台抢购超卖事故,详细探讨了事故原因,包括分布式锁的安全性问题、库存校验的原子性以及系统间的容错处理。提出了基于Lua脚本的相对安全锁实现和库存校验优化方案,最终通过应用缓存实现了性能和安全的双重提升。
ITMuch.com
275
【开发经验】之记一次Redis分布式锁造成的事故
文章通过一个线上事故场景,分析了分布式锁在高并发下的问题,即因依赖的服务超时导致锁失效,进而引发超卖。解决方案包括使用安全的分布式锁,通过LUA脚本实现原子性的锁释放和库存校验。优化后的方案提出,在某些情况下,数据库可作为最终防护,但要考虑性能损耗。文章讨论了不同分布式锁的选择以及数据库在高并发下的角色。
王廷云的博客
588
Redis分布式锁实战:从基础实现到Lua脚本的原子性保障
JjjjjNP
Redis高并发秒杀和分布式锁技术应用及实战解析
Redis高并发秒杀与分布式锁技术是现代互联网系统架构中极为关键的底层能力,其核心价值在于解决大规模用户瞬时请求下的数据一致性、资源争用与业务逻辑原子性问题。秒杀场景(如电商大促、限量商品抢购、演唱会门票抢票)本质是典型的“读多写少+超高并发+强一致性”典型负载:数百万用户在毫秒级时间窗口内发起同一请求,而库存仅几十甚至个位数,若无科学的并发控制机制,极易引发超卖、库存负数、资金错账等严重生产事故。分布式锁则是保障跨进程、跨节点、跨服务操作互斥性的基础设施,尤其在微服务架构下,传统JVM级synchronized或ReentrantLock完全失效,必须依赖外部共享存储实现全局锁协调——Redis凭借其单线程模型、高性能、支持过期时间、原子命令及Lua脚本执行能力,成为分布式锁事实上的首选载体。高并发秒杀的技术难点远不止于“快”,更在于“准”与“稳”。课程中强调的库存扣减并非简单`DECR`指令,而是需融合多层防护:前端限流(如Nginx漏桶/令牌桶)、网关层熔断降级(Sentinel/Hystrix)、应用层缓存预热(将热点商品库存加载至Redis并设置合理TTL)、数据库连接池隔离、最终一致性补偿机制(如异步消息队列核对库存与订单)。其中Redis作为核心中枢,承担着库存预占(`SETNX` + `EXPIRE`组合或`SET key value EX seconds NX`原子化)、请求排队(使用List或Sorted Set实现公平队列)、结果缓存(防止重复提交查询)三重职责。特别值得注意的是,课程深入剖析了Lua脚本在秒杀中的不可替代性——通过将“判断库存是否充足→扣减库存→生成订单号→写入待处理队列”整套逻辑封装为一个原子脚本(`EVAL`命令执行),彻底规避网络延迟导致的竞态条件,确保即使在10万QPS压力下,每笔请求的库存校验与变更都严格串行化执行,从根本上杜绝超卖。分布式锁的实现绝非简单调用`SETNX`即可落地。课程系统性拆解了工业级Redis分布式锁的六大核心要素:唯一性(客户端需生成UUID或Snowflake ID作为锁标识,避免误删他人锁)、可重入性(需记录持有者与重入次数,通过Hash结构维护)、自动续期(WatchDog机制防锁过期导致业务中断)、阻塞等待(基于`GETSET`轮询或Pub/Sub通知实现优雅等待)、锁释放安全性(必须校验锁拥有者再删除,防止A释放B的锁)、容错性(Redlock算法虽被Antirez质疑,但课程结合实际案例说明了在主从切换、网络分区等异常场景下如何通过多数派共识与租约时间设计保障锁语义)。更进一步,课程对比了Redisson、Lettuce等主流客户端封装的锁API,指出其底层均基于`SET key value NX PX milliseconds`指令,并强调务必配合`UNLOCK_SCRIPT` Lua脚本完成安全释放。在微信群红包场景中,Redis技术栈展现出极强的灵活性:利用`INCRBY`实现金额原子累加、`ZADD`按时间戳排序抢红包顺序、`ZRANGEBYSCORE`分页获取中奖用户、`HSET`存储红包详情与领取状态。课程通过ThinkPHP5框架集成实例,详细演示了如何通过Composer引入predis/redis扩展、配置连接池参数(max_connections、timeout)、封装RedisService类统一管理连接、在控制器中调用`$redis->eval($luaScript, $keys, $args)`执行复杂业务逻辑,并重点警示了常见陷阱:未设置连接超时导致线程阻塞、未捕获`RedisException`造成服务雪崩、Lua脚本中使用`KEYS[1]`硬编码引发集群模式下slot不匹配、未对`SETNX`返回值做严格布尔判断导致锁获取失败却继续执行、在事务中混用`WATCH`与`MULTI`引发乐观锁失效等。所有这些知识点均源于真实线上故障复盘,具有极高的工程指导价值。此外,课程还延伸探讨了Redis持久化策略(RDB/AOF)对秒杀数据可靠性的影响、内存淘汰策略(volatile-lru)对热点库存缓存命中率的调控、以及如何通过`INFO`命令实时监控`connected_clients`、`rejected_connections`、`expired_keys`等关键指标进行容量规划与性能调优。
Big黄勇
Go-用Redis实现分布式锁与实现任务队列
在现代高并发、分布式系统架构中,“用Redis实现分布式锁与实现任务队列”是一个极具工程实践价值的核心技术主题,它融合了分布式系统设计、并发控制理论、内存数据库特性、Go语言高性能编程以及原子性保障机制等多维度知识。首先,分布式锁是解决跨进程、跨节点资源竞争问题的关键基础设施——当多个服务实例(如Kubernetes中水平扩展的Pod)同时尝试修改同一份共享状态(例如库存扣减、订单幂等校验、定时任务防重复执行)时,必须通过强一致性的互斥机制确保“同一时刻仅有一个客户端持有锁”,否则将引发数据错乱、超卖、重复消费等严重生产事故。Redis之所以被广泛选为分布式锁的底层载体,根本原因在于其单线程模型天然保证命令执行的串行性,配合SET命令的NX(Not eXists)、EX(Expire)或PX(毫秒级过期)参数可实现“设置键值+设置过期时间+仅当键不存在时成功”的原子写入,从而规避SET+EX两条命令间可能发生的竞态。但基础SETNX存在缺陷:若客户端获取锁后崩溃未释放,锁将永久残留,因此必须引入自动过期机制;而单纯依赖EX又面临锁过期但业务未执行完导致的“误释放”风险——此时需采用可重入、带唯一标识(如UUID)、支持续租(renewal)和安全释放(校验锁拥有者身份)的增强型锁协议。Go语言在此场景中展现出显著优势:其原生goroutine轻量级并发模型与channel通信机制天然契合异步任务调度需求;标准库net/http、encoding/json、time等模块便于快速构建任务生产者/消费者服务;而第三方Redis客户端如github.com/go-redis/redis/v9不仅提供连接池管理、管道(pipeline)、事务(MULTI/EXEC)等高级能力,更支持Lua脚本嵌入——这是实现分布式锁原子释放的黄金方案:客户端在释放锁时,不再简单执行DEL key,而是通过EVAL执行一段Lua脚本,先比对key对应value是否等于当前客户端持有的token(防止A释放B的锁),再执行删除操作,整个过程由Redis单线程原子执行,彻底杜绝中间态干扰。rdslock库正是基于此原理构建的Go专用分布式锁SDK,它封装了锁生成、获取、续期、释放、阻塞等待、公平性控制等全生命周期逻辑,并内置Redlock算法实现——该算法由Redis作者Antirez提出,通过向N(通常≥3)个相互独立的Redis节点分别申请锁,仅当获得超过半数(N/2+1)节点的锁才视为加锁成功,极大提升了在主从切换、网络分区等异常场景下的容错性与安全性,避免单点故障导致的锁失效。任务队列则与分布式锁形成协同闭环:当某项耗时操作(如图片压缩、报表导出、邮件推送)需异步执行且要求严格顺序或负载均衡时,可将任务序列化为JSON结构体,通过LPUSH/RPUSH推入Redis List结构,再由多个Go Worker协程通过BRPOP阻塞式弹出任务并处理;更健壮的做法是结合Sorted Set(ZSET)实现延迟队列:以任务执行时间戳为score,通过ZRANGEBYSCORE轮询获取到期任务,或利用Redis 5.0+的Stream数据类型——其天然支持消费者组(Consumer Group)、消息确认(ACK)、失败重试、历史回溯等功能,完美替代传统消息中间件的部分能力。值得注意的是,任务队列与分布式锁常组合使用:例如在消费端,Worker获取任务前先尝试加锁(防止同一任务被多个Worker重复领取),处理完成后释放锁并标记任务完成状态;或在生产端,使用锁控制任务入队速率,避免突发流量压垮下游。此外,rdslock-master源码中必然包含严谨的错误处理(如Redis连接断开重连、命令超时熔断)、上下文取消(context.Context)集成、结构化日志(zap/slog)、性能监控(prometheus指标埋点)等工业级实践,这些细节共同构成了一个高可用、可观测、可伸缩的分布式协调系统基石。深入掌握该主题,不仅意味着能写出正确的代码,更代表着对CAP理论权衡、时钟漂移影响、脑裂场景应对、分布式共识边界等系统本质问题的深刻理解。
weixin_39840650
DistributedLock:redis分布式锁
分布式锁是构建高可用、高并发分布式系统时不可或缺的核心组件之一,其本质是在多个独立运行的服务节点之间协调对共享资源的互斥访问,确保同一时刻仅有一个节点能够执行关键业务逻辑(如库存扣减、订单生成、幂等性校验等),从而避免数据不一致、重复处理、超卖等严重生产事故。在众多实现方案中,基于 Redis 的分布式锁因其高性能、低延迟、天然支持原子操作与过期机制,成为业界最主流、最成熟、应用最广泛的实践方式。“DistributedLock: redis分布式锁”这一标题所指,正是围绕 Redis 构建健壮、安全、可重入、具备容错能力的分布式锁中间件或工具库,其背后融合了计算机科学中分布式系统理论、并发控制原理、网络可靠性模型及工程落地经验的深度结晶。Redis 分布式锁的底层基石在于其单线程架构带来的强顺序性与命令原子性。核心指令 SETNX(SET if Not eXists)是实现锁获取的关键原语:当客户端执行 SETNX lock_key random_value EX 30 时,Redis 会以原子方式判断 key 是否存在,若不存在则设置 key 并返回 1(加锁成功),否则返回 0(加锁失败)。该操作规避了“检查-设置”(check-then-set)的经典竞态条件问题。但仅靠 SETNX 远不足以构成生产级锁——它缺乏自动续期能力,无法应对业务执行时间超过锁超时导致的“锁误释放”;未绑定唯一标识(如 UUID 或线程 ID),易引发 A 客户端持有锁却由 B 客户端误删锁的致命漏洞;且在 Redis 主从异步复制场景下,主节点写入成功后宕机、从节点晋升为新主,可能导致同一把锁被两个客户端同时持有(脑裂问题)。因此,一个工业级的 DistributedLock 实现必须系统性解决五大核心挑战:(1)加锁的原子性与唯一性验证;(2)锁的自动续期(Lease Renewal)与看门狗机制;(3)解锁的安全性——严格校验锁拥有者身份,杜绝误删;(4)锁的可重入性支持(同一客户端多次加锁应成功,且需维护计数器);(5)异常场景下的容错保障,包括网络分区、Redis 故障转移、客户端崩溃等。为此,进阶方案普遍采用 Lua 脚本封装加锁与解锁逻辑。例如解锁脚本需先校验 key 存在性、value 匹配性,再执行 DEL 操作,整个过程在 Redis 单次原子执行,彻底杜绝竞态。同时,引入租约(lease)概念,将锁生命周期与业务线程绑定,通过后台守护线程定期调用 EVAL 脚本延长锁有效期(如每 10 秒续期一次,续期时间为 30 秒),既防止死锁又避免过早失效。超时控制(Timeout Control)不仅是简单的 EX 参数设置,更需结合业务最大执行时间、网络抖动容忍度、Redis RTT 波动进行动态估算,并预留安全冗余。此外,标签中提及的“高并发”直指性能瓶颈——高频锁竞争会导致大量 SETNX 失败与轮询,因此优秀实现常集成自旋退避(exponential backoff)、异步非阻塞等待、锁分片(sharding lock)甚至读写锁分离等优化策略。而“分布式系统”维度则要求锁服务本身具备可观测性(埋点监控锁获取耗时、失败率、续期次数)、可配置性(支持 Redis Cluster、Sentinel、单节点多种部署模式)、以及与 Spring Boot、Dubbo 等主流框架的无缝集成能力。DistributedLock-master 项目作为典型开源实现,其代码结构必然涵盖 Lock 接口抽象、RedisLockTemplate 核心模板、Lua 脚本资源管理、WatchDog 续期调度器、序列化与反序列化适配层、以及完整的单元测试与压测用例,是深入理解分布式锁设计哲学与工程细节的绝佳学习样本。掌握其原理,不仅关乎一行代码的正确性,更是构建大规模互联网系统稳定性的底层认知基石。
Friedrich ZHAO
springboot+redis分布式锁实现模拟抢单
资源摘要信息: 在高并发电商系统中,“抢单”场景是典型的分布式竞争资源问题,其核心挑战在于如何确保同一时间仅有一个用户能成功获取稀缺商品(如限量秒杀、抢购资格等),避免超卖、重复下单、库存扣减错乱等严重业务事故。Spring Boot + Redis 实现分布式锁正是解决此类问题的工业级主流方案。本知识点体系围绕“基于 Redis 的分布式锁在 Spring Boot 环境下的完整落地实践”,深度覆盖从理论原理、技术选型、编码实现、边界处理到生产级加固的全链路内容。首先,分布式锁的本质是跨 JVM 进程的互斥控制机制,传统 JVM 内部锁(如 synchronized、ReentrantLock)完全失效,必须依赖外部共享存储——Redis 凭借其单线程原子性、高性能读写、支持过期时间与条件写入(SETNX/SET EX NX PX)等特性,成为最广泛采用的分布式锁中间件。其中,Jedis 作为 Java 生态最成熟、轻量级的 Redis 客户端,天然支持 SET 命令的扩展参数组合,是构建锁逻辑的底层基石。关键指令 `SET key value NX PX milliseconds` 是整个方案的灵魂:NX(Not eXists)确保仅当 key 不存在时才设置成功,实现“抢占式加锁”;PX 指定毫秒级自动过期时间,从根本上杜绝死锁风险——即使持有锁的服务异常崩溃、未执行解锁逻辑,锁也会在设定时限后自动释放,保障系统最终一致性。该设计严格遵循分布式锁三大黄金原则:互斥性(同一时刻至多一个客户端持有锁)、安全性(不会出现误释放他人锁)、可用性(锁服务故障时仍具备降级或重试能力)。在抢单模拟中,10 万并发请求涌入时,每个请求需先尝试获取商品 ID 对应的锁(如 lock:goods:1001),加锁成功者进入库存校验→扣减→生成订单的临界区,失败者则快速失败或排队重试,从而将海量并发压力收敛为串行化处理,彻底规避数据库层面的乐观锁重试风暴与悲观锁长事务阻塞。但真实生产环境远比基础 SETNX 复杂:需防范锁误删(A 加锁、B 超时自动释放、A 仍执行 del 导致删除 B 新锁)、锁续期(长事务需看门狗机制延长 TTL)、可重入性(同一客户端多次加锁需支持)、阻塞等待策略(自旋 or Lua 脚本原子判断)、集群模式下 Redlock 算法适配、以及与 Spring AOP/Transaction 的协同(避免锁被事务回滚影响)。此外,必须结合唯一请求标识(如 UUID 或雪花 ID)作为锁 value,配合 Lua 脚本实现“判断 value 相等再 del”的原子解锁,杜绝跨线程误删;同时引入 JedisPool 连接池管理连接生命周期,配置合理 maxTotal/maxIdle/minEvictableIdleTimeMillis 防止连接耗尽;监控层面需埋点记录加锁耗时、失败率、平均持有时长等指标,接入 Prometheus + Grafana 实时告警。最后,对比 Redisson(封装了 WatchDog 自动续期、公平锁、读写锁、信号量等高级语义)、ShardedJedis(分片兼容性)、Lettuce(响应式支持),Jedis 方案虽需手动编码严谨性更高,却极大提升了开发者对分布式协调底层逻辑的理解深度——这正是大厂面试反复深挖“Redis 分布式锁实现细节”“为什么不能只用 SETNX 不加 PX”“如何防止 unlock 删除别人锁”等题目的底层原因。掌握此知识点,不仅打通了 Spring Boot 微服务高并发架构的核心堵点,更构建起对 CAP 理论、分布式共识、原子操作、系统容错等计算机系统本质问题的立体认知框架。
weixin_38725531
redis分布式锁
Redis分布式锁是现代高并发、分布式系统中保障数据一致性与资源互斥访问的核心技术手段之一。其本质是在多个独立JVM进程(即跨服务、跨机器、跨容器)之间,通过一个共享的、高可用的第三方协调组件——Redis,来实现对临界资源的排他性控制。与单机环境下的synchronized、ReentrantLock、StampedLock等JUC锁机制不同,分布式锁必须突破JVM内存边界,解决网络延迟、节点宕机、时钟漂移、锁失效、死锁、脑裂、重入性、可重试性、公平性等复杂问题,因而其设计与实现远比本地锁严谨和脆弱。在单JVM多线程场景中,锁的状态完全由JVM堆内存维护,线程调度、内存可见性、CAS操作均由JVM和操作系统底层保障,具备天然的原子性、可见性与有序性。但一旦进入分布式环境,各服务实例运行在独立JVM中,彼此无共享内存,也无法直接感知对方线程状态;此时若多个服务同时修改同一份数据库记录(如库存扣减)、更新缓存、执行定时任务去重、处理支付回调幂等性等,就极易引发超卖、重复扣款、脏写、数据覆盖等严重一致性事故。因此,必须引入一个全局可见、强一致(或最终一致但可控)、低延迟、高可用的协调中心——而Redis凭借其单线程模型带来的命令级原子性、毫秒级响应、主从复制+哨兵/集群高可用架构、丰富的原子操作原语(如SETNX、GETSET、EVAL)、以及成熟的客户端生态(Lettuce、Jedis、Redisson),成为实现分布式锁事实上的首选中间件。Redis实现分布式锁的核心原理建立在三个关键支柱之上:一是**原子性获取锁**,典型方式为使用`SET key value NX PX timeout_ms`命令(即SETNX + 过期时间合二为一),该命令在Redis 2.6.12后原生支持,确保“判断不存在+设置值+设置过期”三步不可分割,彻底规避了先用EXISTS再用SET导致的竞态条件;二是**唯一标识绑定**,每个锁请求必须携带唯一client_id(如UUID+threadId),避免误删他人锁(防止A加锁后因业务耗时超时自动释放,B趁机加锁并执行,A结束时却误删B的锁);三是**安全释放锁**,必须通过Lua脚本以原子方式校验key存在且value匹配后再DEL,杜绝非持有者释放锁的风险。此外,为应对长任务导致锁过期而业务未完成的问题,需引入**锁续期机制(WatchDog)**:Redisson等高级客户端会在获取锁成功后自动启动后台看门狗线程,每隔lockWatchdogTimeout/3(默认10秒)向Redis发送PEXPIRE命令延长锁有效期,直到unlock显式调用或线程终止。更进一步,在Redis主从异步复制架构下,若master加锁成功后宕机,slave晋升为新master但未同步该锁key,会导致同一资源被两个客户端同时持有锁(即“双写”问题)。为此,Antirez提出Redlock算法:要求客户端向N(通常为5)个相互独立的Redis节点(最好跨物理机/可用区)依次尝试加锁,仅当在≥(N/2+1)个节点上成功获取锁,且总耗时小于锁有效时间,才视为加锁成功;释放锁时需向所有节点广播DEL。该算法牺牲一定性能换取更强的一致性保障,但亦存在争议(如Martin Kleppmann指出其依赖严格的时间同步,实际网络分区下仍可能失效)。当前业界主流实践更倾向采用Redisson的MultiLock(组合多个RedissonClient实例)、或结合ZooKeeper的顺序临时节点方案、或基于etcd的Lease+CompareAndSwap机制作为替代。此外,生产环境中还需关注诸多工程细节:锁粒度应尽量细化(如按业务ID分片而非全局锁),避免“一把大锁锁住整个系统”;必须设置合理超时时间(既防死锁又防过早释放);异常处理需完备(网络中断、Redis连接池耗尽、序列化失败均要兜底);建议启用Redis集群模式并配置读写分离与故障转移;监控层面需埋点记录加锁成功率、平均耗时、续期频次、异常释放率等指标;日志中务必打印client_id、resourceKey、stackTrace便于问题追溯。综上,Redis分布式锁绝非简单调用一个SETNX命令即可高枕无忧,而是融合了分布式系统理论、并发编程范式、中间件原理与SRE工程实践的综合性技术体系,是每一位中高级Java工程师、分布式架构师必须深入掌握并谨慎落地的关键能力。
钢枪的代码
Redis分布式锁的正确实现方法总结
资源摘要信息:"Redis分布式锁的正确实现方法总结——全面解析高并发场景下安全、可靠、可重入、防误删的分布式互斥控制机制。本文围绕Redis作为分布式协调中间件构建分布式锁的核心原理展开,系统性阐述其在微服务架构、秒杀系统、库存扣减、订单幂等处理等关键业务场景中不可替代的作用。首先明确分布式锁的本质目标:在跨JVM、跨进程、跨机器的分布式环境中,模拟单机环境下的synchronized或ReentrantLock语义,确保同一时刻仅有一个客户端(服务实例)能执行临界区代码,从而避免数据不一致、超卖、重复提交等严重生产事故。实现上必须严格满足四大黄金准则:第一是强互斥性,即通过原子性指令(如SET key value NX PX timeout_ms)确保加锁操作不可分割,杜绝竞态条件;第二是自动续期与防死锁能力,要求锁必须设置合理且可控的过期时间(expireTime),防止客户端宕机后锁长期滞留,同时需结合唯一请求标识(requestId)实现‘解铃还须系铃人’原则,避免A客户端误删B客户端持有的锁;第三是高可用容错性,强调在Redis主从异步复制、哨兵切换、集群分片等复杂部署模式下,仍能保障锁的一致性语义,为此需引入Redlock算法(虽存在学术争议但仍是工业界重要参考)、或采用Redisson等成熟封装库内置的看门狗(WatchDog)心跳续期机制;第四是可重入性支持与锁释放原子性,即同一客户端多次加锁应成功并计数,解锁时需校验requestId+Lua脚本双重保障,确保DEL操作仅在当前客户端持有锁的前提下执行,杜绝因网络延迟导致的误删风险。技术细节层面,必须使用Jedis或Lettuce客户端,并通过try-with-resources或连接池管理连接生命周期;加锁时需生成全局唯一且可追溯的requestId(如UUID+线程ID+时间戳),避免不同请求混淆;超期时间设定需综合评估业务逻辑最长执行耗时、网络RTT、GC停顿及Redis响应延迟,通常建议为预期耗时的2~3倍并预留缓冲;解锁必须采用Lua脚本(如if redis.call('get', KEYS[1]) == ARGV[1] then return redis.call('del', KEYS[1]) else return 0 end),保证GET+DEL的原子执行,彻底规避先查后删引发的竞态漏洞。此外,还需补充异常兜底策略:如加锁失败后的指数退避重试、锁等待超时熔断、异步监控告警、锁泄漏自动巡检等运维保障手段。进阶实践中,应考虑Redis集群模式下的槽位迁移对锁Key的影响,推荐使用Hash Tag(如{order_id})强制锁Key落入同一slot;对于超高并发场景,可结合本地缓存+Caffeine做锁预判减少Redis压力;在Spring生态中,可基于AOP+自定义注解(@DistributedLock)实现声明式锁编程模型。最后强调:切忌直接使用SETNX+EXPIRE两步命令(存在竞态窗口),禁用无超时的永久锁,拒绝未校验requestId的暴力DEL,警惕主从复制导致的锁失效(从节点未同步锁即发生主从切换)。唯有将原子性、唯一性、时效性、可验证性、可观测性五大维度融会贯通,方能在真实生产环境中构建真正鲁棒的Redis分布式锁体系。"
weixin_38688371
ReLock:基于redis的简单分布式锁
ReLock 是一个基于 Redis 实现的轻量级、面向 JavaScript(Node.js)环境的分布式锁工具库,其核心目标是在分布式系统中安全、可靠、高效地协调多个服务实例对共享资源的并发访问。在现代微服务架构中,单体应用被拆分为多个独立部署、水平扩展的服务节点,这些节点可能同时尝试修改数据库记录、写入缓存、触发定时任务或处理支付回调等关键操作。若缺乏统一的协调机制,极易引发数据不一致、重复扣款、超卖、竞态条件(race condition)等严重生产事故。ReLock 正是为解决此类问题而生——它将 Redis 强大的原子性命令(如 SETNX、EVAL、GETSET、DEL 等)与 Lua 脚本执行能力、TTL(Time-To-Live)自动过期机制、唯一锁标识(lock key + 随机 token)等关键技术深度融合,构建出一套语义清晰、错误容忍度高、可嵌入业务逻辑的分布式锁 API。其设计严格遵循分布式锁的经典实践原则:互斥性(Mutual Exclusion)、无死锁(Deadlock-Free)、容错性(Fault Tolerance)、可重入性(虽 ReLock 默认不支持原生可重入,但可通过业务层封装实现)、以及最重要的——锁的持有者必须是解锁者(即“谁加锁、谁解锁”)。ReLock 的 lock 方法底层调用的是 Redis 的 SET 命令配合 NX(Not eXists)、EX(EXpire seconds)和随机 UUID 作为 value,确保仅当锁不存在时才成功设置,并自动绑定过期时间,从根本上规避因客户端崩溃导致锁长期滞留的问题;而 unlock 方法则通过 Lua 脚本原子性地比对当前锁的 value 是否与调用方传入的 lock token 一致,仅当匹配时才执行 DEL 操作,彻底杜绝了误删他人锁的风险——这是区别于简单使用 DEL 直接删除的最关键安全设计。整个流程完全避开 Redis 单点故障带来的单点失效风险(实际生产中应配合 Redis Sentinel 或 Redis Cluster 部署),并天然支持跨进程、跨机器、跨容器甚至跨云区域的锁同步。从接口抽象来看,ReLock 提供了高度简洁的 Promise 风格异步 API:lock(resource, ttl, client) 返回一个解析为唯一锁标识符(即写入 Redis 的随机字符串 token)的 Promise,该 token 不仅是解锁凭证,更是防止锁被篡改的密钥;unlock(resource, lock, client) 则需严格传入原始 resource 名称、对应 lock token 及同一 redis 客户端实例,以保障上下文一致性。值得注意的是,resource 参数并非 Redis Key 的完整路径,而是业务语义层面的资源名(如 "order:123456"、"inventory:item001"),ReLock 内部会将其规范化为形如 "relock:resource" 的命名空间前缀键,避免与其他业务键冲突。TTL 的设定尤为关键——它既是安全兜底(防止单点崩溃锁永久残留),也是性能权衡点:过短易导致锁提前释放引发并发,过长则降低系统响应弹性;实践中常结合业务最长处理耗时 + 网络抖动余量(如 3~10 秒)动态设定,并可配合看门狗(watchdog)机制实现自动续期(虽 ReLock 基础版未内置,但其 token 设计已为扩展预留接口)。此外,ReLock 对 Redis 客户端无强耦合,兼容 ioredis、node_redis 等主流驱动,且源码结构清晰(通常包含 index.js 主入口、lock.js/unlock.js 核心逻辑、utils.js 工具函数及 test/ 单元测试),便于开发者深度定制——例如增加重试策略、失败回调钩子、锁等待队列、Metrics 上报(Prometheus)、或与 Express/Koa 中间件集成实现接口级锁控制。综上,ReLock 不仅是一个代码片段,更是理解分布式锁本质的一扇窗口:它用最简代码诠释了 CAP 理论下如何在 AP 系统中通过牺牲部分 C(强一致性)换取高可用与分区容忍,同时借助 Redis 的单线程原子性与内存高速特性,在工程落地中逼近理论最优解。
笨猫猪
redis6 资料和代码 秒杀案例 分布式锁案例
Redis 6 是 Redis 发布历史中具有里程碑意义的重大版本,它在性能、安全性、功能丰富性与分布式能力等方面实现了全面升级,尤其在高并发场景下的工程实践价值极为突出。本资料聚焦于 Redis 6 的核心实战能力,围绕“秒杀系统”与“分布式锁”两大典型高并发业务场景展开深度剖析,涵盖底层原理、代码实现、问题规避、性能调优及生产级最佳实践,是掌握现代分布式缓存与协调服务不可或缺的知识体系。首先,Redis 6 最具革命性的特性之一是原生支持**多线程 I/O 模型**(非完全多线程,而是将网络读写、协议解析等耗时操作交由多个 IO 线程并行处理,而命令执行仍由主线程串行完成),显著提升了单实例吞吐量,尤其在万级并发连接、高频小包请求(如秒杀预减库存)场景下,QPS 提升可达 2–3 倍。相比 Redis 5 及之前纯单线程事件循环模型,Redis 6 在保持命令原子性与内存模型简洁性的同时,有效缓解了网络瓶颈,为构建低延迟、高响应的秒杀网关提供了坚实底座。其次,“秒杀案例”绝非简单使用 `DECR` 或 `INCR` 实现库存扣减,而是涉及**全链路一致性保障体系**:包括缓存预热(利用 `Pipeline` 批量加载商品信息与初始库存至 Redis)、热点 Key 防击穿(通过 `SETNX + 过期时间` 或布隆过滤器前置拦截无效请求)、超卖防控(必须结合原子化操作——推荐 Lua 脚本封装“判断库存+扣减+记录日志”三步为不可分割单元,避免 `GET-IF-THEN-SET` 的竞态条件)、限流降级(集成令牌桶或滑动窗口算法,如基于 `ZSET` 统计用户 1 分钟内请求频次)、异步削峰(将成功下单请求写入 `List` 或 `Stream`,由后台消费者落库,实现读写分离与流量整形)。其中,Lua 脚本的使用尤为关键——它在 Redis 服务端原子执行,规避了网络往返导致的 CAS 失败风险,且可借助 `redis.call()` 完成多 key 协同操作,是秒杀中保障“库存精确扣减”与“订单幂等生成”的黄金方案。第三,“分布式锁案例”直指分布式系统中最脆弱的一环。Redis 6 下实现可靠分布式锁需同时满足**互斥性、防死锁、容错性、可重入性与锁续期能力**五大要素。传统 `SETNX` + `EXPIRE` 存在命令分片导致的原子性断裂风险,因此必须采用 `SET key value EX seconds NX` 原子指令;但仅此仍不足——若业务执行超时锁自动释放,而线程仍在运行,将引发并发冲突。故生产环境强烈推荐 **Redisson 客户端**:其 `RLock` 接口封装了看门狗(WatchDog)机制,自动为未释放锁续期(默认 30s 心跳续期),并支持公平锁、读写锁、联锁(MultiLock)、红锁(RedLock)等高级语义;更关键的是,Redisson 底层采用 Netty 异步通信与状态机管理,完美适配 Redis 6 的多线程 I/O,大幅降低锁获取延迟。此外,所有锁操作必须配合唯一请求 ID(如 UUID)校验,解锁时通过 Lua 脚本比对 key 对应 value 是否匹配,杜绝误删他人锁的灾难性事故。再者,本资料强调**缓存一致性**这一贯穿始终的底层命题。秒杀中“缓存库存”与“数据库真实库存”的最终一致性,依赖于可靠的消息队列(如 RocketMQ 事务消息)或本地事务表+定时补偿机制,而非强一致同步——因强一致会严重拖垮性能。Redis 6 新增的 `ACL`(访问控制列表)与 `TLS 加密通信` 特性,也为跨机房部署的分布式锁集群提供了安全边界,防止未授权客户端恶意篡改锁状态或清空库存缓存。最后,所有代码均基于 Redis 6.2+ 版本编写,充分运用 `Stream` 数据结构实现订单消息持久化与消费组分发、`GeoHash` 支持地域化秒杀灰度、`Modules` 扩展(如 RedisJSON、RedisSearch)增强商品检索能力,并配套完整的压测脚本(JMeter + redis-benchmark)、监控指标(`INFO` 命令关键字段解析、`SLOWLOG` 分析、`LATENCY` 检测)、故障模拟(主从切换、Cluster 槽迁移、网络分区)及应急预案。整套体系不仅覆盖技术细节,更渗透着容量规划(Key 命名规范、内存淘汰策略选择 LFU/LRU)、成本意识(大 Value 拆分、压缩序列化)、可观测性建设(对接 Prometheus + Grafana)等资深架构师思维。掌握此资料,即掌握以 Redis 6 为中枢构建高可用、高性能、高可靠的互联网级并发系统的完整方法论。
进击的程序猿~
用注解实现redis分布式锁,防止短时间内重复请求
在分布式系统架构中,尤其是在高并发、微服务场景下,“防止短时间内重复请求”是一个极为关键且普遍存在的技术挑战。本标题所指的“用注解实现Redis分布式锁”,本质上是将分布式锁这一底层并发控制机制,通过Java注解(Annotation)的方式进行声明式封装,并结合Spring AOP(面向切面编程)实现横切逻辑的自动织入,从而在不侵入业务代码的前提下,为指定方法提供原子性保障与幂等性防护。其核心目标在于:当多个客户端(如前端重试、定时任务触发、消息重复投递、网关重放等)在极短时间内并发调用同一业务接口(例如下单、支付回调、库存扣减、用户积分更新等)时,借助Redis作为共享存储中心,确保同一时刻仅有一个线程能真正执行该方法体,其余请求则被阻塞或快速失败,从而避免数据不一致、超卖、重复扣款、状态错乱等严重生产事故。具体而言,该方案的技术实现依赖于多个关键技术点的协同配合。首先是Redis分布式锁的基本原理:利用Redis单线程特性及SET命令的NX(Not eXists)和EX(EXpire)原子选项(如`SET key value NX EX 30`),实现“加锁+设置过期时间”的一步完成,从根本上规避了先SET再EXPIRE可能引发的竞态条件。同时,必须严格保证锁的持有者唯一性——即加锁时写入的value需为全局唯一标识(如UUID+线程ID),释放锁时须通过Lua脚本比对value一致性后才DEL,防止误删他人锁,这是实现安全释放的关键。其次,为应对网络分区、服务宕机导致锁未及时释放的问题,必须设置合理的锁超时时间(Lock Timeout),既不能过短(导致业务未执行完锁已失效,引发并发冲突),也不能过长(导致故障恢复慢、资源长期被占)。实践中常采用“看门狗”机制(Watchdog),即在锁持有期间由后台线程定期续期(如每10秒自动延长20秒),前提是当前线程仍存活且业务未结束。而“注解实现”则是该方案的工程化亮点。开发者只需在目标方法上添加自定义注解(如`@RedisLock(key = "#userId", expire = 10)`),即可完成分布式锁接入。该注解本身不包含逻辑,而是作为AOP切点的标识。Spring AOP通过`@Around`通知拦截所有被该注解标记的方法,在JoinPoint执行前尝试获取Redis锁;若成功,则放行执行原方法;若失败(如等待超时、锁已被占用),则根据策略抛出异常(如LockAcquireException)或返回默认值,实现优雅降级。注解参数支持SpEL表达式(如`#userId`),可动态从方法参数中提取业务维度键(如用户ID、订单号、商品SKU),确保锁粒度精准,避免粗粒度锁引发性能瓶颈。此外,还需配套实现锁管理器(RedisLockManager)、序列化工具(如Jackson2JsonRedisSerializer处理value序列化)、连接池配置(Lettuce或Jedis)、异常兜底策略(如失败重试、异步补偿、日志告警)等完整基础设施。进一步延伸,该方案还深度关联“幂等性设计”这一分布式系统黄金准则。分布式锁虽能控制并发执行,但无法解决网络不可靠带来的重复请求问题(如HTTP重试、MQ消息重复消费)。因此,理想实践应是“锁+幂等Token+业务唯一约束”三重防护:前端在发起请求时携带一次性token,服务端首次处理后将其落库或存入Redis并设置过期;后续相同token请求直接返回历史结果;同时数据库层面增加唯一索引(如订单号唯一、交易流水号唯一),形成最终防线。此外,还需关注Redis集群模式下的锁一致性问题——标准Redis单节点锁在主从切换时存在脑裂风险,此时应优先选用Redlock算法(虽有争议)或更成熟的解决方案如Redisson的MultiLock、ZooKeeper、Etcd等强一致性协调服务。综上,该注解化Redis分布式锁方案不仅是一项实用工具,更是对并发控制、系统可靠性、代码可维护性、架构演进能力的综合体现,是每一位Java后端工程师构建高可用分布式服务不可或缺的核心技能。
yuhui66666688gfbfdy