Redis红锁

一、为什么需要红锁?

Redis红锁(RedLock)的出现是为了解决单点Redis分布式锁在故障场景下的可靠性问题

简单说:就是一个主节点,主节点设置锁,从节点还没同步复制完成,主节点就挂了,从节点成了主节点,另外一个线程来设置锁,也能设置成功。最终导致多客户端同时持有锁。

二、红锁的核心思想(RedLock 原理)

假设有 N 个独立的 Redis 节点(通常为 5 个),RedLock 的核心流程如下:

java 复制代码
1、户端生成唯一ID(UUID) 作为锁的值,用于标识谁持有锁。
2、并发地 向N个 Redis 实例请求加锁,使用 SET key value NX PX 3000 设置过期时间。
3、只要获取到了超过一半(例如 N=5 时为3个)实例的锁,就认为加锁成功。
4、加锁过程必须在一个时间窗口内完成(比如锁的过期时间的 2/3 时间),以避免因网络延迟导致的锁失效。

红锁中涉及的redis实例数(奇数个),是无主从关系的。各自独立。也不会同步,所以设置锁要保证成功数大于N/2+1

解锁流程:

java 复制代码
1、只解锁那些 value 与客户端ID相同的锁,防止误删别人的锁。
2、遍历所有节点发送 DEL 操作。

加锁成功条件

java 复制代码
1、至少在 N/2 + 1 个 Redis 节点 上成功加锁。
2、加锁总耗时 < 所设定锁的有效期(考虑网络延迟等)。

好处:

java 复制代码
1、高可用:多个Redis节点容忍部分节点宕机或网络抖动
2、安全性高:加锁需要超过一半 Redis 节点达成共识
3、可靠性强:每个节点的锁都有自动过期,防止死锁

代码实现

java 复制代码
// RedLock核心思想
public boolean redLockTryLock(String lockKey, long expireMs) {
    int successCount = 0;
    long startTime = System.currentTimeMillis();
    
    //1、向N个独立实例申请锁
    for (RedisInstance instance : allInstances) {
        if (instance.tryLock(lockKey, clientId, expireMs)) {
            successCount++;
        }
    }
    //2、计算有效时间
    long elapsed = System.currentTimeMillis() - startTime;
    long validityTime = expireMs - elapsed - clockDriftMargin;
    
    //3、多数派成功且还有效
    return successCount >= quorumSize && validityTime > 0;
}

三、红锁面临的问题

NPC争议问题:红锁算法自诞生起就伴随着**N(网络延迟)、P(进程暂停)、C(时钟漂移)**三个核心争议,这些现实世界中的不确定因素,动摇了红锁在数学意义上的绝对安全性。

场景重现

java 复制代码
假设真实的标准时间是 13:00:00.000,客户端设置TTL=10秒。
各节点的时间状态:
节点A(时钟快):本地时间是 13:00:03.000(比标准时间快3秒)
节点B(时钟准确):本地时间是 13:00:00.000
节点C(时钟慢):本地时间是 12:59:57.000(比标准时间慢3秒)

客户端发送加锁命令的时刻:真实时间:13:00:00.000

各节点接收到命令后的处理:

java 复制代码
在节点A上(时钟快的节点):
节点A的"当前时间" = 13:00:03.000
计算绝对过期时间 = 13:00:03.000 + 10秒 = 13:00:13.000(节点A的本地时间)

在节点B上(时钟准确的节点):
节点B的"当前时间" = 13:00:00.000
计算绝对过期时间 = 13:00:00.000 + 10秒 = 13:00:10.000

在节点C上(时钟慢的节点):
节点C的"当前时间" = 12:59:57.000
计算绝对过期时间 = 12:59:57.000 + 10秒 = 13:00:07.000(节点C的本地时间)

现在,我们统一换算到真实时间轴上来看这些锁的实际过期时间:

java 复制代码
真实时间轴上的过期时刻:
节点A的锁:在节点A本地时间 13:00:13.000过期。由于节点A的时钟比真实时间快3秒,对应的真实时间是 13:00:10.000
节点B的锁:在真实时间 13:00:10.000过期
节点C的锁:在节点C本地时间 13:00:07.000过期。由于节点C的时钟比真实时间慢3秒,对应的真实时间是 13:00:10.000
等等!这样看起来三个锁都在真实时间 13:00:10.000过期?

关键问题在这里!

上面的计算是基于"时钟偏差是固定值"的理想情况。但实际中的时钟问题更复杂:

情况1:时钟漂移(Clock Drift)

java 复制代码
时钟不是简单地"快3秒"就永远固定快3秒。不同服务器的晶体振荡器频率有微小差异,会导致:
节点A的时钟可能越来越快
节点C的时钟可能越来越慢
这样就会导致计算出的绝对过期时间在真实时间轴上确实不同。

情况2:时钟跳跃(Clock Jump) - 这是更常见和危险的情况

java 复制代码
由于NTP时间同步、管理员手动调整、虚拟机迁移等原因,节点的时钟可能突然跳跃:
假设在加锁后发生了时钟跳跃:
节点A的时钟突然被调快了(或者本来就快,现在更快了)
当真实时间才到 13:00:05.000时,节点A的本地时间可能已经跳到了 13:00:15.000
此时节点A检查:当前本地时间(13:00:15.000) > 锁的绝对过期时间(13:00:13.000)
节点A认为锁已过期,于是删除了锁!
但实际上,真实时间才 13:00:05.000,客户端认为锁还有5秒才过期,仍在安全地操作共享资源。

四、总结

如果时钟偏差是固定不变的,那么所有锁在真实时间轴上确实会同时过期。

但实际问题在于:

java 复制代码
1、时钟漂移会导致偏差逐渐变化
2、时钟跳跃会导致节点上的锁"提前过期"

这才是Redlock面临的主要风险:不是固定的时间差,而是动态变化的时间差和突然的时钟调整

这就是为什么Redis作者Antirez在提出Redlock时特别强调要配置NTP服务防止时钟跳跃,因为时钟的不稳定性而非简单的偏差才是破坏锁安全性的根本原因。

红锁的使用建议

考虑到上述争议,你应该在以下情况下考虑使用红锁:

java 复制代码
1、"对一致性要求极高":你确实需要一把强一致的锁,并且可以接受红锁带来的性能下降(因为需要与多个节点通信)。
2、"可以控制运维环境":你能够确保 Redis 节点所在的机器不会发生剧烈的时钟漂移。
3、"理解其局限性":你清楚地知道,在极端情况下(如长时间的进程暂停),红锁仍然无法提供 100% 的安全保证。对于大多数业务场景,这种极端情况的发生概率和其带来的风险是可以接受的。

替代方案

1、"对于高可用要求不极端的场景":使用 Redis 主从+哨兵模式,并接受在主从切换的极小时间窗口内可能出现的锁失效问题。很多业务场景下,这种风险是可接受的。
2、"对于必须强一致的场景":考虑使用专门为分布式协调而设计的系统,如 "ZooKeeper" 或 "etcd。这些系统使用了共识算法(如Zab、Raft),它们本身就是为了在分布式环境下提供强一致性和容错性而设计的,实现分布式锁是它们的原生强项,通常能提供比红锁更可靠的安全保证。

相关推荐
人间打气筒(Ada)2 小时前
Centos7 搭建hadoop2.7.2、hbase伪分布式集群
数据库·分布式·hbase
心灵宝贝2 小时前
如何在 Mac 上安装 MySQL 8.0.20.dmg(从下载到使用全流程)
数据库·mysql·macos
奋斗的牛马3 小时前
OFDM理解
网络·数据库·单片机·嵌入式硬件·fpga开发·信息与通信
忧郁的橙子.3 小时前
一、Rabbit MQ 初级
服务器·网络·数据库
杰杰7984 小时前
SQL 实战:用户访问 → 下单 → 支付全流程转化率分析
数据库·sql
爬山算法4 小时前
Redis(120)Redis的常见错误如何处理?
数据库·redis·缓存
野生技术架构师4 小时前
盘一盘Redis的底层数据结构
数据结构·数据库·redis
Feng.Lee4 小时前
聊聊缓存测试用例设计方案
缓存·测试用例
EelBarb4 小时前
sqlite数据库迁移至mysql
数据库·mysql·sqlite