【redis】redis实现红锁
举个真实的例子:你的团队刚上线了一个秒杀系统,用Redis锁来防止超卖。测试环境明明跑得好好的,但大促当晚却出现了100件库存卖出了120单的灵异事件。查看日志才发现:就在用户疯狂点击的瞬间,Redis主节点突然挂了,新的主节点还没拿到锁的信息,结果两个用户同时抢到了"同一把锁"。
这就是很多开发者踩过的坑——你以为用了Redis分布式锁就万事大吉,其实这些情况随时可能让锁失效:
- 主节点刚给你加完锁就崩溃了,从节点接班时一脸懵:“什么锁?我没听说过啊”
- 你的程序正处理到一半突然卡住了(比如GC停顿),等回过神来锁早就过期了
- 网络抽风导致锁信息没传到位,多个客户端都觉得自己拿到了锁
为了解决这些头疼问题,Redis作者提出了**红锁(RedLock)**方案。简单来说就是"不要把鸡蛋放在一个篮子里":让多个独立的Redis节点投票决定锁的归属,只有半数以上同意才算真正拿到锁。
但这套方案也引发过激烈争论,有人甚至说它"数学上就不安全"。本文将用最直白的语言:
- 先带你看看传统Redis锁在集群环境为什么容易翻车
- 拆解红锁这个"少数服从多数"的解决方案
- 手把手教你用Java代码实现红锁
- 揭秘Redisson框架如何简化红锁的使用
读完本文你会明白:没有完美的分布式锁,只有适合场景的选择。下次设计系统时,至少能清楚知道手里的锁到底有几成把握。
集群锁的缺陷与挑战
在Redis Cluster环境中,传统的SETNX
分布式锁存在以下致命缺陷:主从切换导致锁失效。
问题步骤复现:
-
客户端A通过
SET key random_val NX PX 30000
在主节点成功获取锁 -
主节点宕机,Redis Cluster触发故障转移,从节点升级为新主节点
-
由于Redis主从复制是异步的,锁可能未同步到新主节点
-
客户端B向新主节点申请相同资源的锁,成功获取导致数据竞争
# 主节点写入锁
SET resource_1 8a3e72 NX PX 10000
OK
# 主节点宕机,从节点晋升但未同步锁数据
# 新主节点处理客户端B的请求
SET resource_1 5b9fd2 NX PX 10000
OK # 锁被重复获取!
红锁(RedLock)的设计与实现
在N个独立Redis节点(非Cluster模式)中,当客户端在半数以上节点成功获取锁,且总耗时小于锁有效期时,才认为锁获取成功。
实现步骤详解
假设部署5个Redis节点(N=5):
-
获取当前时间:记录开始时间
T1
(毫秒精度) -
依次向所有节点申请锁:
SET lock_key valueNX PX $ttl
value
:全局唯一值(如UUID)ttl
:锁自动释放时间(如10秒)
- 计算锁有效性:
-
客户端计算获取锁总耗时
T_elapsed = T2 - T1
(T2为最后响应时间) -
仅当以下两个条件满足时,锁才有效:
- 成功获取锁的节点数 ≥ 3(N/2 + 1)
T_elapsed < ttl
(确保锁未过期)
-
加锁成功,去操作共享资源
-
释放锁:向所有节点发送Lua脚本删除锁(需验证值)
if redis.call("get",KEYS[1]) == ARGV[1] then
return redis.call("del",KEYS[1])
else
return 0
end
NPC争议问题
红锁算法自诞生起就伴随着**N(网络延迟)、P(进程暂停)、C(时钟漂移)**三个核心争议,这些现实世界中的不确定因素,动摇了红锁在数学意义上的绝对安全性。
网络延迟(Network Delay)的致命时间差
问题场景:
-
客户端在节点A、B、C成功获取锁,总耗时48ms(小于TTL 50ms)
-
但由于跨机房网络波动,实际锁在节点上的有效时间存在差异:
- 节点A记录的锁过期时间:客户端本地时间+50ms = T+50
- 节点B因网络延迟,实际锁过期时间为T+52
- 节点C因网络拥塞,实际锁过期时间仅T+48
-
在时间窗口
T+48
到T+50
之间,客户端认为锁仍有效,但节点C的锁已提前失效
后果:
其他客户端可能在此期间获取节点C的锁,导致锁状态分裂,多个客户端同时进入临界区。
进程暂停(Process Pause)的「薛定谔锁」
经典案例:
// 伪代码:获取锁后执行业务逻辑
if (redLock.tryLock()) {
// 触发Full GC暂停300ms
System.gc();
// 此时锁已过期,但客户端仍在写数据
updateInventory();
}
关键时间线:
- T0: 获取锁(TTL=200ms)
- T0+100ms: 进入GC暂停,持续300ms
- T0+400ms: GC结束,继续执行业务逻辑
- 锁实际在T0+200ms已失效,但客户端在T0+400ms仍以为自己持有锁
数据灾难:
其他客户端在T0+200ms到T0+400ms期间可能修改数据,导致最终结果错乱。
时钟漂移(Clock Drift)的时空扭曲
物理机时钟偏移实验数据:
节点 | 时钟误差范围 | 常见诱因 |
---|---|---|
节点A | ±200ms/分钟 | 虚拟机时钟不同步 |
节点B | ±500ms/天 | NTP服务异常 |
节点C | ±10秒/小时 | 宿主机硬件时钟故障 |
连锁反应:
- 客户端计算锁有效期基于本地时钟(假设为T+100ms)
- 但节点B的时钟比实际快30秒,导致其记录的锁过期时间为T-29000ms
- 锁在客户端认为的有效期内提前被节点B自动释放
行业领袖的正面交锋
Martin Kleppmann(《数据密集型应用设计》作者):
“红锁依赖的假设——『客户端能准确感知锁存活时间』,在异步分布式系统中根本无法保证。即使没有节点故障,NPC问题也会导致锁状态的不确定性。”
Antirez(Redis作者)的反驳:
"工程实践中可以通过以下手段控制风险:
- 使用带温度补偿的原子钟硬件
- 禁用NTP服务的时钟跳变调整
- 监控进程暂停(如GC日志分析)
- 为锁TTL设置冗余缓冲时间(如额外20%)"
红锁的Java实现示例
使用Jedis客户端实现红锁:
package com.morris.redis.demo.redlock;
import redis.clients.jedis.Jedis;
import redis.clients.jedis.JedisPool;
import redis.clients.jedis.params.SetParams;
import java.util.Collections;
import java.util.List;
import java.util.UUID;
import java.util.concurrent.TimeUnit;
/**
* 使用jedis手写RedLock
*/
public class JedisRedLock {
public static final int EXPIRE_TIME = 30_000;
private final List<JedisPool> jedisPoolList;
private final String lockKey;
private final String lockValue;
public JedisRedLock(List<JedisPool> jedisPoolList, String lockKey) {
this.jedisPoolList = jedisPoolList;
this.lockKey = lockKey;
this.lockValue = UUID.randomUUID().toString();
}
public void lock() {
while (!tryLock()) {
try {
TimeUnit.MILLISECONDS.sleep(100); // 失败后短暂等待
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
}
}
public boolean tryLock() {
long startTime = System.currentTimeMillis();
int successCount = 0;
try {
for (JedisPool jedisPool : jedisPoolList) {
try (Jedis jedis = jedisPool.getResource();) {
// 原子化加锁:SET lockKey UUID NX PX expireTime
String result = jedis.set(lockKey, lockValue,
SetParams.setParams().nx().px(EXPIRE_TIME));
if ("OK".equals(result)) {
successCount++;
}
}
}
// 计算获取锁耗时
long elapsedTime = System.currentTimeMillis() - startTime;
// 验证:多数节点成功 且 耗时小于TTL
return successCount >= (jedisPoolList.size() / 2 + 1) && elapsedTime < EXPIRE_TIME;
} finally {
// 若加锁失败,立即释放已获得的锁
if (successCount < (jedisPoolList.size() / 2 + 1)) {
unlock();
}
}
}
public void unlock() {
String script = "if redis.call('get', KEYS[1]) == ARGV[1] then return redis.call('del', KEYS[1]) else return 0 end";
for (JedisPool jedisPool : jedisPoolList) {
try (Jedis jedis = jedisPool.getResource()) {
jedis.eval(script, Collections.singletonList(lockKey), Collections.singletonList(lockValue));
}
}
}
}
手写RedLock的使用:
package com.morris.redis.demo.redlock;
import redis.clients.jedis.JedisPool;
import redis.clients.jedis.JedisPoolConfig;
import java.util.ArrayList;
import java.util.List;
import java.util.concurrent.CountDownLatch;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.TimeUnit;
/**
* 手写RedLock的使用
*/
public class JedisRedLockDemo {
private volatile static int count;
public static void main(String[] args) throws InterruptedException {
List<JedisPool> jedisPoolList = new ArrayList<>();
jedisPoolList.add(new JedisPool(new JedisPoolConfig(), "127.0.0.1", 6379));
jedisPoolList.add(new JedisPool(new JedisPoolConfig(), "127.0.0.1", 6380));
jedisPoolList.add(new JedisPool(new JedisPoolConfig(), "127.0.0.1", 6381));
jedisPoolList.add(new JedisPool(new JedisPoolConfig(), "127.0.0.1", 6382));
jedisPoolList.add(new JedisPool(new JedisPoolConfig(), "127.0.0.1", 6383));
int threadCount = 3;
CountDownLatch countDownLatch = new CountDownLatch(threadCount);
ExecutorService executorService = Executors.newFixedThreadPool(threadCount);
for (int i = 0; i < threadCount; i++) {
executorService.submit(() -> {
JedisRedLock jedisRedLock = new JedisRedLock(jedisPoolList, "lock-key");
jedisRedLock.lock();
try {
System.out.println(Thread.currentThread().getName() + "获得锁,开始执行业务逻辑。。。");
try {
TimeUnit.SECONDS.sleep(3);
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
System.out.println(Thread.currentThread().getName() + "获得锁,结束执行业务逻辑。。。");
count++;
} finally {
jedisRedLock.unlock();
}
countDownLatch.countDown();
});
}
countDownLatch.await();
executorService.shutdown();
System.out.println(count);
}
}
Redisson中红锁的使用
Redisson已封装红锁实现,自动处理节点通信与锁续期:
package com.morris.redis.demo.redlock;
import org.redisson.Redisson;
import org.redisson.RedissonRedLock;
import org.redisson.api.RLock;
import org.redisson.api.RedissonClient;
import org.redisson.config.Config;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;
import java.util.concurrent.CountDownLatch;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.TimeUnit;
/**
* Redisson中红锁的使用
*/
public class RedissonRedLockDemo {
private volatile static int count;
public static void main(String[] args) throws InterruptedException {
List<String> serverList = Arrays.asList("redis://127.0.0.1:6379", "redis://127.0.0.1:6380", "redis://127.0.0.1:6381",
"redis://127.0.0.1:6382", "redis://127.0.0.1:6383");
List<RedissonClient> redissonClientList = new ArrayList<>(serverList.size());
for (String server : serverList) {
Config config = new Config();
config.useSingleServer()
.setAddress(server);
redissonClientList.add(Redisson.create(config));
}
List<RLock> lockList = new ArrayList<>(redissonClientList.size());
for (RedissonClient redissonClient : redissonClientList) {
lockList.add(redissonClient.getLock("java-lock"));
}
int threadCount = 3;
CountDownLatch countDownLatch = new CountDownLatch(threadCount);
ExecutorService executorService = Executors.newFixedThreadPool(threadCount);
for (int i = 0; i < threadCount; i++) {
executorService.submit(() -> {
RedissonRedLock redissonRedLock = new RedissonRedLock(lockList.toArray(new RLock[0]));
redissonRedLock.lock();
try {
System.out.println(Thread.currentThread().getName() + "获得锁,开始执行业务逻辑。。。");
try {
TimeUnit.SECONDS.sleep(3);
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
System.out.println(Thread.currentThread().getName() + "获得锁,结束执行业务逻辑。。。");
count++;
} finally {
redissonRedLock.unlock();
}
countDownLatch.countDown();
});
}
countDownLatch.await();
executorService.shutdown();
System.out.println(count);
for (RedissonClient redissonClient : redissonClientList) {
redissonClient.shutdown();
}
}
}
Redisson优势:
• 自动续期:通过WatchDog机制延长锁有效期
• 简化API:封装底层细节,支持异步/响应式编程
• 故障容错:自动跳过宕机节点,保证半数以上成功即可
总结
红锁通过多节点投票机制,显著提升了分布式锁的可靠性,但需权衡其实现复杂度与运维成本。建议在以下场景选择红锁:
• 需要跨机房/地域部署
• 业务对数据一致性要求极高
• 已具备独立Redis节点运维能力
对于大多数场景,可优先使用Redisson等成熟框架,避免重复造轮子。若对一致性有极致要求,可考虑ZooKeeper/etcd等基于共识算法的方案。