Redis分布式锁深度解析:从SETNX到Redlock的演进与实战

分布式锁RedisSETNX 本内容遵循CC 4.0 BY-SA版权协议
谈国平 谈国平 墨衍会员 · AI 创作全网分发 墨衍智能分发 通过墨衍一键同步至各平台 可同步平台 CSDN微信公众号微博知乎掘金百家号博客园抖音小红书 你的文章也可以这样分发 墨衍会员 ¥399起/年,写一次发全网 我也要

你有没有遇到过这种情况:凌晨两点某个定时任务准时触发,早上来公司一看,负责同一业务的两台服务器都把这个任务跑了一遍。订单重复退款、短信重复发送、报表数据翻倍,线上事故就这么出来了。

这背后就是典型的分布式锁问题。单机时代我们用 synchronized 或者 ReentrantLock 就能解决并发竞争,可一旦服务拆成多节点部署,进程之间的锁就没法互相感知了。这时候 Redis 凭借单线程命令执行、高性能、易接入的特点,成了实现分布式锁最常用的载体。从最基础的 SET NX 到社区讨论激烈的 Redlock 算法,这条路我踩过不少坑,也沉淀了一些自己的理解,这篇就系统梳理一下。

这篇文章适合正在用 Redis 做分布式锁、准备在集群环境中防重跑任务的开发者,也适合准备分布式锁面试题的同学。我会从最原始的 SETNX 命令讲起,把为什么会出现 SET key value NX EX、为什么还需要 Lua 脚本、为什么最后会扯出 Redlock 这一整条演进逻辑线说清楚,最后给出一套能直接落地的方案。

1. 从单机锁到分布式锁:问题本质是什么

很多人一上来就写 SETNX,但没想明白分布式锁到底在解决什么问题。搞不清楚本质,后续的坑基本躲不掉。

1.1 单机锁为什么管不住多节点

Java 里的 synchronized 和 ReentrantLock,本质上依赖的是 JVM 内存中的 Monitor 对象。线程在进入临界区之前,需要先获取到这个锁对象,而这个锁对象只存在于当前进程的内存空间里。

当你的服务部署成两个节点,每个节点都是独立的 JVM 进程。节点 A 上的线程持有锁,节点 B 上的线程根本无法感知这个状态,它去获取的是自己 JVM 里的另一个锁对象。两个节点上的线程同时冲进临界区,锁的互斥效果就完全失效了。

这里关键点在于:分布式锁必须有一个所有节点都能访问到的公共状态存储。所有节点在这个公共存储上做竞争、做标记,才能实现跨进程的互斥。

1.2 为什么选 Redis 而不是数据库或 ZooKeeper

能做公共状态存储的不止 Redis 一个,市面上常见的方案有三类:

方案 优点 缺点
数据库唯一索引 实现简单,无需额外组件 性能差,每次加锁都是 DB 写操作,且依赖事务,高并发下容易成为瓶颈
ZooKeeper 临时顺序节点 可靠性高,有 Watch 机制自动通知 需要维护 ZK 集群,客户端较重,加锁解锁的 RTT 相对较高
Redis SET NX 性能极高,单线程模型天然保证原子性,接入成本低 极端场景下有锁丢失风险,需要自己处理续期和容错

实际开发中,大部分业务场景对锁的要求是「高性能 + 基本可靠」,而不是「绝对可靠」。Redis 在绝大多数场景下是性价比最高的选择。我见过很多团队用数据库唯一索引做分布式锁,并发一上来,数据库连接池先被打爆。而 Redis 单机就能扛住每秒几万甚至十几万的加锁请求,不在一个量级。

1.3 分布式锁的三个核心要求

不管是哪种实现,一个合格的分布式锁必须满足三个条件:

互斥性:任意时刻,只有一个客户端持有锁。

安全性:持有锁的客户端崩溃后,锁能被自动释放,不会造成死锁。这要求锁必须设置过期时间,或者有类似的兜底机制。

可用性:加锁和解锁的过程必须原子性,不能出现"设置锁成功了,但设置过期时间失败了"这种情况。

注意第三点。很多人写分布式锁出 bug,都是因为把加锁和设置过期时间拆成了两步。我后面会详细讲这个坑。

2. SET NX 的演变:从两条命令到一条原子指令

Redis 实现分布式锁的思路很简单:利用 SET 命令的 NX 参数,只有 key 不存在时才能写入成功。写入成功代表加锁成功,删除 key 代表释放锁。

但这中间经历了好几个版本的演进,每个版本都是在修复上一个版本的漏洞。

2.1 最初的 SETNX 加 EXPIRE 两步走

早期的 Redis 版本里,加锁的经典写法是:

TEXT
SETNX lock_key unique_value
EXPIRE lock_key 30

第一条命令尝试写入 lock_key,如果 key 不存在则写入成功返回 1,代表加锁成功;如果 key 已存在则返回 0,说明锁已被别人持有。第二条命令给锁设置 30 秒的过期时间,防止持有者崩溃后锁永远不释放。

这段逻辑看起来没问题,但有个致命缺陷:SETNX 和 EXPIRE 不是原子的。如果客户端在 SETNX 执行成功后、EXPIRE 执行前突然崩溃,锁就没有过期时间,会一直被占着。其他客户端永远拿不到锁,整个系统直接瘫痪。

有人会说,那我先 EXPIRE 再 SETNX 行不行?也不行,因为 key 可能本来就不存在,EXPIRE 直接作用于一个不存在的 key,没有任何效果。而且先设置过期时间再写入 key,中间还是有窗口期。

这个问题的根源是两条命令无法保证原子性,解决方案有两个方向:用 Lua 脚本把两步封装成原子操作,或者等 Redis 官方提供原子指令。

2.2 Redis 2.6.12 带来的 SET 原子指令

Redis 从 2.6.12 版本开始,扩展了 SET 命令的能力,支持同时设置 NX 和 EX 参数:

TEXT
SET lock_key unique_value NX EX 30

这条命令的语义是:当 lock_key 不存在时,设置 lock_key 的值为 unique_value,并同时设置 30 秒过期时间。整个过程是原子的,由 Redis 单线程模型保证执行期间不会被其他命令插入。

这才是现代分布式锁的起点。这条命令解决了两步操作的非原子性问题,也是后面所有围绕 SET NX 讨论的基础。现在互联网公司里大部分老旧代码,其实还停留在这条命令或者更早的 SETNX 写法上,能做对第一步的都不多。

2.3 value 的设计:为什么不能随便传一个字符串

很多人在写 SET lock_key value NX EX 30 的时候,value 随便传了个值,比如 "1"、当前时间戳、甚至是业务名称。这个细节看着不起眼,但直接关系到锁释放的安全性。

锁释放的正确流程是:获取锁时保存 value,释放锁时先判断当前锁的 value 是否等于自己保存的 value,相等才能删除。这个过程通常用 Lua 脚本实现原子性:

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

为什么要判断 value?因为存在一种情况:线程 A 持有锁后,处理时间超过了过期时间,锁被 Redis 自动释放了。这时线程 B 成功加锁。接着线程 A 处理完毕,直接执行 DEL lock_key,这把 B 的锁删掉了。B 的临界区还在执行,另一个线程 C 又成功加锁,三个人同时进入临界区,分布式锁形同虚设。

如果 value 是每个线程唯一的随机值(比如 UUID + 线程ID),A 释放锁时发现当前 value 不是自己的,就不会执行删除,B 的锁得以保留。

我看到热搜词里有"Value为当前日期"这种用法。说实话,用当前日期做 value 有参考意义吗?如果只是为了排查问题方便,看锁是什么时候创建的,可以理解。但从锁安全性的角度,当前日期不具备唯一性,同一秒内两个线程的值完全一样,无法区分持有者。真正做校验时,value 必须能唯一标识一个线程或一次请求。

2.4 过期时间设多少才合理

过期时间的长短,本质是在两个风险之间做权衡:设得太短,业务还没处理完锁就失效了;设得太长,持有者崩溃后锁长时间占着不放,影响系统可用性。

一个常用的估算方法是:先压测出业务临界区的最大执行时间,再乘上 2 到 3 倍的冗余系数。比如某个任务最大执行时间是 8 秒,过期时间设为 20 到 30 秒比较稳妥。

但这种静态设置依然有问题。线上环境的 GC 停顿、网络抖动、IO 阻塞都可能让任务执行时间超过预期。Java 服务一次 Full GC 停顿几十秒都不稀奇,锁早就过期了。这就是后面要说的看门狗续期机制要解决的问题。

3. 锁续期与可重入:SET NX 之后的第一层纵深

前面说的方案,假设业务执行时间一定小于过期时间。但真实世界不给你这个保证,所以成熟方案必须在 "锁过期了但业务没跑完" 这件事上有应对策略。

3.1 看门狗机制的原理

Redisson 里的分布式锁有一个默认行为:拿到锁之后,会启动一个后台定时任务,每隔 10 秒检查一次锁是否还被当前线程持有。如果持有,就把过期时间重置为 30 秒。这样只要客户端一直持有锁,锁的过期时间就会被不断延长,业务跑多久都不怕锁提前失效。

这个后台任务就是"看门狗"。它的工作逻辑是:

  1. 加锁成功后,记录当前线程标识和锁的过期时间(默认 30 秒)。
  2. 启动一个定时任务,每过 10 秒执行一次。10 秒是 30 秒的三分之一,这个间隔确保在锁过期之前至少能续期一次。
  3. 续期操作通过 Lua 脚本完成,先校验锁的 value 是否还属于当前线程,是才更新过期时间。
  4. 释放锁时,取消定时任务。

这套机制解决了"业务执行时间不可控"的问题。但注意,看门狗只在客户端正常运行时才能续期。如果客户端进程直接宕机,定时任务跟着消失,锁会在 30 秒后自动过期,其他节点可以重新获取锁,这恰好是期望的行为。

3.2 可重入锁:同线程为何需要重复获取锁

分布式锁还有一个容易被忽略的需求:可重入性。同一个线程在持有锁的情况下,再次调用加锁逻辑,应该能成功,而不是把自己锁死。

考虑一个递归函数,或者一个服务 A 调用服务 B、B 内部又要调用同一个加锁方法的方法的场景。如果没有可重入性,第二次加锁会因为锁已存在而失败,导致业务异常。

Redisson 的实现方式是在 value 中记录持有者信息,同时维护一个加锁次数计数器。同一个线程重复加锁时,判断持有者标识一致,计数器加一。释放锁时计数器减一,减到零才真正删除锁。

3.3 主从复制架构下的锁丢失问题

现在很多 Redis 部署是主从架构,主节点负责写,从节点负责读。这里有一个非常隐蔽的风险:

客户端在主节点上 SET 加锁成功,数据还没来得及同步到从节点,主节点突然宕机。哨兵检测到主节点故障,把从节点提升为新的主节点。此时新主节点上并没有锁的数据,另一个客户端成功加锁。两个客户端同时持有锁,互斥被打破。

这不是理论上的漏洞,而是真实发生过的事故。Redis 主从复制的异步特性决定了这种丢失无法完全避免。要彻底解决这个问题,需要引入多节点的确认机制,也就是 Redlock 算法的出发点。

4. Redlock 算法的完整拆解:为什么需要、怎么实现、是否有用

Redlock 是 Redis 作者 antirez 提出的一种分布式锁算法,全称是 Redlock Algorithm。它的设计目标很明确:在主从切换等极端场景下,依然能保证分布式锁的互斥性。

4.1 多节点独立加锁的核心思路

Redlock 解决锁丢失问题的思路是:不在单个 Redis 节点上加锁,而是在多个相互独立的 Redis 节点上都加锁,只有大多数节点加锁成功,才算真正成功。

这就好比你有一个重要的决定要做,不是问一个顾问,而是同时问五个互不通信的顾问,至少三个都同意,才执行。单个顾问出问题,不影响整体决策。

具体步骤是:

  1. 获取当前时间戳,记为 T1。
  2. 依次向 N 个独立的 Redis 节点发起加锁请求(使用 SET key value NX EX 指令)。加锁的超时时间设置得很短,比如 50 毫秒。目的是避免某个节点不可用时,客户端一直在等它。
  3. 计算整个加锁过程消耗的时间:T_elapsed = T2 - T1。
  4. 判断加锁是否成功:至少在 N/2 + 1 个节点上加锁成功,且 T_elapsed 小于锁的过期时间。
  5. 如果加锁成功,锁的有效时间 = 锁的原始过期时间 - 已消耗时间。
  6. 如果加锁失败(不足大多数节点成功,或者消耗时间超过了过期时间),向所有节点发送释放锁请求,清理可能已经成功加锁的部分节点。

注意第 5 步,这是 Redlock 中经常被忽略但很关键的细节。如果加锁过程消耗了 5 秒,而锁的过期时间是 10 秒,那么锁的实际有效时间只有 5 秒。这段时间内必须完成业务操作,否则锁可能提前失效。

4.2 Redlock 的时间估算和重试策略

Redlock 的部署节点数,官方推荐是 5 个。2 个节点不满足大多数原则,3 个节点中只要挂 1 个就无法加锁,5 个节点能容忍 2 个节点故障。

这里有一个数学上的考量。在 5 个节点中,需要至少 3 个节点加锁成功。这意味着系统能容忍最多 2 个节点宕机或不可用。同时,加锁的超时时间需要非常短,比如总锁的有效期是 10 秒,单个节点加锁超时 50 毫秒就放弃,避免在故障节点上浪费时间。

重试策略上,Redlock 建议在加锁失败后随机退避一段时间再重试,避免多个客户端同时重试造成活锁。随机退避的公式可以是 Math.random() * baseDelay + minDelay,具体数值需要根据业务对延迟的容忍度来调整。

4.3 围绕 Redlock 的争论与我的实践判断

Redlock 从提出那天起就有争议。最有名的是分布式系统专家 Martin Kleppmann 的质疑和 antirez 的反驳。

Martin 的核心论点有两个:一是分布式锁的健壮性依赖于时钟的准确性,但 Redis 节点之间没有共享同一套时钟,进程暂停(GC)期间锁可能已经过期;二是锁的意义在于保护共享资源,如果持有锁的客户端发生 GC 停顿,超过锁过期时间,其他客户端也能拿到锁,那锁就失效了。

antirez 的回应是:这些理论问题在大多数实际场景下并不会触发。对于非绝对安全的业务场景(比如防重复执行定时任务、库存扣减),Redlock 已经足够。他还强调,Redlock 明确要求所有节点使用物理时钟,且每个节点的时钟偏差要控制在很小范围内。

我的实践判断是这样:如果你的业务是金融级别的强一致场景,直接用 Redlock 不如引入一个共识组件(比如 ZooKeeper 或 etcd)来得稳。如果你的业务是互联网常见的"允许少量重复、但绝不允许大规模并发执行"场景,在主从架构的 Redis 上用 SET NX + 看门狗方案完全够用。不要为了追求理论上的绝对安全,给系统引入不必要的复杂度。这是我做过很多分布式锁优化后的真实体会。

5. 从理论到实战:一套可落地的分布式锁方案

前面讲了不少原理和演进,现在进入实操环节。我给出两种基于实际业务场景的方案,一种是纯 RedisTemplate 手写,一种是基于 Redisson 的成熟方案,最后聊一下定时任务防重复执行这个高频需求。

5.1 基于 RedisTemplate 的最小可用实现

大部分 Spring Boot 项目里都有 RedisTemplate,用它手写一个分布式锁并不复杂。下面这个实现包含了唯一 value 校验和 Lua 脚本释放锁,我看过很多团队最终稳定运行的版本,核心逻辑基本就是这个。

JAVA
public class RedisDistributedLock {
 
private final StringRedisTemplate redisTemplate;
private final String lockKey;
private final String lockValue;
private final long expireTime;
 
public RedisDistributedLock(StringRedisTemplate redisTemplate, String lockKey, long expireTime) {
this.redisTemplate = redisTemplate;
this.lockKey = lockKey;
this.lockValue = UUID.randomUUID().toString() + ":" + Thread.currentThread().getId();
this.expireTime = expireTime;
}
 
// 加锁
public boolean tryLock() {
Boolean result = redisTemplate.execute(
(RedisCallback<Boolean>) connection -> connection.set(
lockKey.getBytes(),
lockValue.getBytes(),
Expiration.seconds(expireTime),
RedisStringCommands.SetOption.SET_IF_ABSENT
)
);
return Boolean.TRUE.equals(result);
}
 
// 释放锁
public boolean unlock() {
String script = "if redis.call('get', KEYS[1]) == ARGV[1] then " +
"return redis.call('del', KEYS[1]) " +
"else return 0 end";
Long result = redisTemplate.execute(
new DefaultRedisScript<>(script, Long.class),
Collections.singletonList(lockKey),
lockValue
);
return Long.valueOf(1).equals(result);
}
}

这里有几个细节值得说明。

lockValue 使用 UUID + 线程 ID,确保每个线程每次加锁的值都是唯一的。检测锁是否属于自己的时候,只有 value 完全匹配才执行删除,防止误删其他线程的锁。

释放锁必须用 Lua 脚本把"检查 value"和"删除 key"两步合在一起,保证原子性。如果用 Java 代码先 GET 再 DEL,中间有窗口期,其他线程可能在这期间替换了锁,导致误删。

加锁也用了 Redis 原子指令 SET key value NX EX,而不是 SETNX 加 EXPIRE 两步操作。这个细节我之前讲过了,是所有正确实现的基础。

5.2 Redisson 方案:看门狗续期和可重入不用自己写

手写方案能做到正确,但缺少两个重要能力:锁自动续期和可重入。Redisson 这两点都帮你实现了。

JAVA
@Autowired
private RedissonClient redissonClient;
 
public void businessMethod() {
RLock lock = redissonClient.getLock("biz:order:pay:lock");
boolean isLocked = false;
try {
// 尝试加锁,最多等待5秒,加锁后30秒自动过期
isLocked = lock.tryLock(5, 30, TimeUnit.SECONDS);
if (!isLocked) {
throw new RuntimeException("系统繁忙,请稍后重试");
}
// 业务逻辑
doBusiness();
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
throw new RuntimeException("加锁过程被中断", e);
} finally {
if (isLocked && lock.isHeldByCurrentThread()) {
lock.unlock();
}
}
}

注意上面的写法中,tryLock(5, 30, TimeUnit.SECONDS) 的语义是等待最多 5 秒、leaseTime 为 30 秒。这里有个很多人踩过的坑:一旦手动指定了 leaseTime,Redisson 就不会启动看门狗续期。只有不传 leaseTime 的 tryLock() 或 lock(),才会走默认的 30 秒过期时间加上看门狗自动续期机制。

完整调用链加锁、执行业务、释放锁,全程不需要我们手动续期。Redisson 的 watch dog 会保证锁不会因为业务执行时间过长而提前失效。这在手写方案里需要额外维护一个定时任务线程,复杂度高很多。

5.3 定时任务防重复执行的完整方案

现在回到开头那个事故场景:两台服务器上的定时任务重复执行。

要彻底解决,需要在定时任务内部加分布式锁。这里的"定时任务"泛指所有通过 XXL-Job、ElasticJob、Spring Scheduler 等框架调度的周期性任务。这些框架本身有分片功能,但如果你用的是简单的 @Scheduled,或者框架配置里没开分片,双重执行的概率就很高。

我给出一个标准的执行模板:

JAVA
@Component
public class SettlementTask {
 
private static final String LOCK_KEY = "task:settlement:daily";
private static final long LOCK_EXPIRE = 60L;
 
@Autowired
private StringRedisTemplate redisTemplate;
 
@Scheduled(cron = "0 0 2 * * ?")
public void execute() {
String value = UUID.randomUUID().toString();
boolean locked = tryLock(LOCK_KEY, value, LOCK_EXPIRE);
if (!locked) {
log.info("任务已在其他节点执行,本节点跳过");
return;
}
try {
// 实际的结算逻辑
doSettlement();
} finally {
releaseLock(LOCK_KEY, value);
}
}
}

这个模板里有两个容易被忽视的细节。

第一个:任务执行完后必须在 finally 中释放锁,不能因为业务异常导致锁泄漏。很多线上事故的起因就是忘了 finally,业务抛异常,锁一直占着,后续任务全部跳过。

第二个:使用 UUID 作为 value,释放锁时校验。这确保如果本节点因为某些原因持锁时间超过过期时间,锁被自动释放后,本节点的 finally 不会误删其他节点后来获取的锁。

5.4 见过最多的误用场景和典型错误

我在Code Review里见过很多分布式锁的错误用法,列几个高频的。

错误一:用 RedisTemplate 的 setIfAbsent 方法,但没用 Lua 脚本释放锁。

JAVA
// 错误示范:获取锁
Boolean success = redisTemplate.opsForValue().setIfAbsent(lockKey, value, 30, TimeUnit.SECONDS);
// ...
// 错误示范:释放锁 - 检查后再删除,中间有窗口期
String currentValue = redisTemplate.opsForValue().get(lockKey);
if (value.equals(currentValue)) {
redisTemplate.delete(lockKey);
}

从 GET 到 DELETE 之间,锁可能已经被另一个线程抢占,然后被本线程误删。正确做法是用 Lua 脚本保证获取和删除的原子性。

错误二:加锁和 try-catch-finally 位置不对。

JAVA
// 错误示范:lock在try外面,finally里unlock
boolean locked = lock.tryLock();
try {
doSomething();
} finally {
lock.unlock(); // 如果tryLock本身抛异常,这里会NPE
}

正确做法是把加锁结果用一个变量保存,在 finally 里先判断再加锁是否成功,才决定是否解锁。

错误三:锁粒度太大或太小。

把整个接口包在分布式锁里是常见错误。锁粒度太大,所有请求串行化,系统的吞吐量直线下降。锁粒度太小,比如只锁住了某一行数据的修改,但冲突集中在同一个 key 上,等于没保护。锁的粒度应该精确作用于共享资源的写操作窗口,而不是整个业务流程。

5.5 锁冲突检测与监控:你需要知道锁是否正常工作

分布式锁不是写完就能一直正常运转的,需要在生产环境里加监控。我的做法是监控三个核心指标:

第一,加锁失败率。正常情况下,业务错峰运行时加锁失败率很低。如果某个时间点失败率突然升高,说明多个节点同时竞争某个锁,可能存在流量倾斜或任务编排问题。

第二,锁等待耗时。用 Redisson 的 tryLock(waitTime, ...) 时,如果大部分请求都在等待锁,说明锁的持有时间过长,临界区代码需要优化,或者锁粒度设置不当。

第三,持有锁的时间分布。正常情况下,业务持有锁的时间应该是稳定在某个范围内。如果出现尾部耗时特别长的请求,可能是网络抖动或者 GC 停顿导致持锁时间异常,也可能是看门狗续期失效。

这几个指标可以通过定时统计 Redis 的命令执行耗时和客户端日志来观察,也可以在加锁逻辑里埋点打日志。不要等到线上出事故才回头看,那时候已经晚了。

6. 极端场景下的兜底方案:锁失效后怎么办

分布式锁再怎么设计,依然存在失效的可能。真正健壮的系统,不会把所有的宝押在锁上。锁是重要的一环,但不是全部。

6.1 业务幂等是最后的防线

我见过有些人把分布式锁当成银弹,锁一失效,系统就崩。但实际上,即使锁完全正常工作,也无法覆盖所有故障场景。比如消息队列重复推送、网络超时导致的重试,都可能让同一个请求被处理两次。

所以,在核心链路上设计幂等逻辑才是最终的兜底。分布式锁解决的是并发冲突,幂等解决的是重复请求。两者互补,组合使用效果才最好。

订单支付回调场景下,即使没有分布式锁,也要在数据库里做幂等校验:一个订单只能成功支付一次,重复回调直接返回成功。这就是经典的最强兜底方案。

6.2 锁与本地事务的边界

另外一个容易踩的坑:分布式锁和数据库事务的边界。很多人习惯在一个事务方法里加锁,或者在一个加锁方法里开启事务,结果事务还没提交,锁已经释放了。

正确做法是:先加锁,再开启事务,执行操作,提交事务,最后释放锁。如果顺序反过来,锁释放后,其他线程进入临界区,读到的是还没提交的旧数据,锁就等于白加了。

JAVA
// 正确顺序
boolean locked = lock.tryLock();
if (locked) {
try {
TransactionTemplate tx = transactionTemplate.execute(status -> {
// 查询、修改、删除
return doDbOperation();
});
// 事务提交后再释放锁
} finally {
lock.unlock();
}
}

这个细节在分布式锁的实战中非常关键,但经常被忽略。锁和事务的作用对象不同,锁保护的是临界区的互斥访问,事务保护的是数据库操作的一致性,两者必须协调好生命周期。

6.3 锁的降级与熔断

最后一个思考:如果 Redis 本身就挂了,分布式锁怎么办?很多系统的做法是一旦 Redis 不可用,直接放弃加锁,让请求全部通过。这在某些场景下是合理的降级策略——比起系统完全不可用,接受少量的重复执行更划算。

但如果你的业务是绝对不能并发的(比如资金清算),那 Redis 挂了就只能拒绝服务,也不能降级为无锁模式。这里需要根据业务对一致性的要求来决策,没有统一答案。

我个人的经验是:给锁加上熔断开关。通过配置中心动态控制"强制加锁""降级放行""熔断拒绝"三种模式,线上出问题时能快速调整策略,而不是改代码重新发布。这个开关意味着你提前为最深的坑留了一个逃生通道。

写到最后的一点体会

从最初的一条 SETNX,到今天的 Redlock 和看门狗机制,Redis 分布式锁的演进本质上是不断对抗不确定性:命令的非原子性、时钟的不可靠性、网络的不可预知性、进程的不可控停顿。每一步演进都在消除一个已知的确定性漏洞,但新的不确定性又会冒出来。

在实际项目中,我越来越倾向于一个原则:根据业务的风险承受能力,选择合适复杂度的锁方案。大多数业务用 SET key value NX EX 加上 Lua 脚本释放锁就够了,最多引入 Redisson 的看门狗续期。只有极少数对一致性要求很高的场景,才值得引入 Redlock 或者 ZooKeeper。不要因为面试题里讲了 Redlock,就认为生产环境必须上 Redlock,那是把理论当成了教条。

最后分享一个实战小技巧:上线分布式锁之前,最好写一个并发压测脚本,模拟多节点并发执行同一条任务,验证锁在竞争激烈、线程数远超锁槽数量时也能正常工作。我遇到过一次隐藏很深的死锁问题,就是只有压测才触发,测试环境的低流量根本跑不出来。分布式锁这种东西,看着简单,出问题的时候往往要查很久,前置的压测和监控投入非常值得。

相关推荐

基于Python+Open CV的手势识别算法设计

伴随着人工智能时代的到来,人机交互的领域也逐渐成为研究的一大方向;其中,手势识别是人机交互领域的一项关键技术,自此手势识别也迎来了一波高潮,近几年无论是在消费领域、电子领域、数码领域、家电领域甚至汽车领域上,都能或多或少的见到手势识别的身影。 手势识别在设计智能高效的人机界面方面具有至关重要的作用, 目前手势识别已应用到手语识别、智能监控、到虚拟现实等各个领域,手势识别的原理都是利用各种传感器(例如红外、摄像头等)对手部的形态进行捕捉并进行建模,形成一个模型信息的序列帧,而后将这些信息序列转换为机器能够识别的相对应指令(例如打开、切换菜单、移动等)来完成控制。 采用Python的集成开发环境Pycharm进行本次课程设计,在Pycharm中进行需要库(模块)的下载,调取电脑摄像头,按帧读取摄像头采集到的头像,形态学处理,图像旋转(由于摄像头采集到的图像是镜像,需要用cv2.flip将摄像头采集到的图像进行处理),选取图片中固定位置作为手势输入,用红线画出手势识别框,基于hsv的肤色检测,进行高斯滤波,找出轮廓,求出图像中手势的凹凸点,手指间角度求取。

努力学习前端的小陈

ROS-CV-ImageConverter:将ROS图像转换为OpenCV(cv :: Mat)类型的图像

ROS-CV图像转换器 依存关系 cv_bridge(ROS软件包)

Willis Wang

计算机视觉图像处理-CV2与图像插值算法

用最近邻插值与双线性插值原理完成图像缩放 最近邻插值,是指将目标图像中的点,对应到源图像中后,找到最相邻的整数点,作为插值后的输。缺点:使用最近邻插值算法处理图像放大时,在图象中可能出现明显的块状效应 双线性插值,在两个方向上做三次线性插值。使用双线性插值算法处理图像放大时,在图象中可能出现马赛克 代码 import cv2 img = cv2.imread(’./xiabang.jpg’, cv2.IMREAD_UNCHANGED) #缩放比例 scale_percent = 0.3 width = int(img.shape[1] * scale_percent) height = int

weixin_38697274

基于水平集CV模型的图像分割(Matlab代码)

基于水平集CV模型的图像分割(Matlab代码),全套代码

小宇QAQ

导向滤波(guide_filter)的python实现

导向滤波(guide_filter)的python实现,可以结合暗通道;理论实现比较好的图像去噪效果。

zachzhan_g
返回顶部
谈国平
博客等级 码龄8年 37粉丝 8,276原创