怎么做pp网站百度上海分公司
举个真实的例子:你的团队刚上线了一个秒杀系统,用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] thenreturn redis.call("del",KEYS[1])
elsereturn 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暂停300msSystem.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 expireTimeString result = jedis.set(lockKey, lockValue,SetParams.setParams().nx().px(EXPIRE_TIME));if ("OK".equals(result)) {successCount++;}}}// 计算获取锁耗时long elapsedTime = System.currentTimeMillis() - startTime;// 验证:多数节点成功 且 耗时小于TTLreturn 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等基于共识算法的方案。