当前位置: 首页 > news >正文

【Redis】笔记|第7节|大厂生产级Redis高并发分布式锁实战(二)

一、Redis主从架构锁失效问题解析

1. 核心问题背景

在Redis主从架构中,分布式锁失效的核心风险源于主从复制的异步特性主节点故障后的角色切换。即使客户端仅操作主节点写入,主节点宕机时未同步的锁数据可能导致新主节点允许重复加锁。

2. 主从切换延迟导致锁失效的原理

  1. 异步复制的天然缺陷
    • 主节点处理写请求(如加锁)后,先在本地内存执行命令,再异步同步到从节点。若主节点在同步完成前宕机,从节点的数据可能不一致。
  2. 典型失效场景
    • 客户端A在主节点加锁成功,但锁数据未同步到从节点。
    • 主节点宕机,从节点(未存储锁数据)升级为新主节点。
    • 客户端B向新主节点请求加锁,因无锁数据而成功获取锁。
    • 结果:客户端A和B同时持有锁,分布式锁失效。

3. 主节点故障的直接影响

  1. 数据丢失风险
    • 主节点宕机时,未同步到从节点的锁数据永久丢失。新主节点因无锁记录,导致后续加锁请求绕过原有锁逻辑。
  2. 故障转移的隐患
    • Redis Sentinel或手动故障转移过程中,若从节点未及时同步锁数据,升级后的新主节点会成为锁失效的根源。

4. 解决方案与优化策略

  1. Redlock算法(多节点方案)
    • 核心思想:基于多个独立Redis实例(至少3个),通过多数派选举机制降低单节点故障影响。
      • 客户端依次向所有节点请求加锁,超过半数成功则认为加锁成功。
      • 即使某个主节点宕机,只要多数节点未同时故障,锁仍有效。
    • 缺点:复杂度较高,性能略低于单节点锁;Redisson官方已废弃RedLock,推荐使用MultiLock替代。
  2. 强制主从同步(配置优化)
    • 通过Redis配置参数限制主节点写入,确保从节点同步:
min-slaves-to-write 1       # 至少1个从节点同步成功,主节点才接受写请求
min-slaves-max-lag 10       # 从节点同步延迟超过10秒时,主节点拒绝写请求
    • 风险:若从节点长时间无法同步,可能导致请求超时,影响系统可用性。
  1. 选择强一致性存储方案
    • 改用ZooKeeper、etcd等支持强一致性的分布式协调工具:
      • ZooKeeper通过ZAB协议保证数据同步,选举出的新主节点必为数据一致的节点。
    • 优点:彻底避免异步复制导致的锁失效问题。

二、从CAP角度剖析Redis与ZooKeeper实现分布式锁的区别

1. CAP 定理核心概念

  1. 一致性(Consistency):所有节点在同一时刻看到相同的数据(强一致性)。
  2. 可用性(Availability):系统在正常响应时间内处理请求,不会因故障拒绝服务。
  3. 分区容错性(Partition tolerance):系统在网络分区(节点间通信中断)时仍能正常运行。

CAP 定理结论:分布式系统无法同时满足三者,必须舍弃其一。

2. Redis 分布式锁的 CAP 定位

        1. 单节点 / 主从架构(默认异步复制)
  • 类型AP 系统(牺牲一致性,保证可用性和分区容错性)
  • 分析
    • 一致性(弱)
      • 主从架构采用异步复制,主节点宕机时未同步的锁数据会丢失,导致新主节点允许重复加锁(如前文主从切换延迟问题)。
      • 单节点模式无此问题,但单机故障会导致锁服务不可用。
    • 可用性(高)
      • 主节点正常时可快速响应加锁 / 释放请求,从节点支持读请求(提升读可用性)。
      • 主节点宕机后,通过 Sentinel 快速选举新主节点,恢复服务(牺牲短暂可用性,但整体可用性较高)。
    • 分区容错性(支持)
      • 主从节点通过异步复制容忍网络分区,允许分区期间主节点继续服务(但可能产生数据不一致)。
  • 典型场景

适用于非强一致性需求、追求高可用的业务(如缓存、非核心业务锁)。

        2. Redlock 算法(多节点方案)
  • 类型尽力而为的 AP 系统(尝试提升一致性,但未完全解决)
  • 分析
    • 通过多个独立 Redis 实例(至少 3 个)实现 “多数派加锁”,降低单节点故障导致的一致性问题。
    • 但本质仍是异步复制,若多数节点同时故障(如网络分区),仍可能出现锁失效(CAP 中偏向 AP)。

3. ZooKeeper 分布式锁的 CAP 定位

        1. 基于 ZAB 协议的 CP 系统
  • 类型CP 系统(牺牲可用性,保证一致性和分区容错性)
  • 分析
    • 一致性(强)
      • 通过 ZAB 协议(类似 Paxos)实现原子广播,确保所有节点数据一致。
      • 锁通过 “有序临时节点” 实现,选举出的 Leader 节点必为数据一致的节点,避免重复加锁。
    • 可用性(低)
      • 当发生网络分区或 Leader 宕机时,需进行 Leader 选举,期间整个集群不可用(直到选举完成)。
    • 分区容错性(支持)
      • 通过 Leader 选举和数据同步机制,在分区恢复后自动修复数据一致性。
    • 典型场景

        适用于强一致性需求、允许短暂不可用的关键业务(如分布式事务、核心资源锁)。

4. 对比总结:Redis vs. ZooKeeper

维度

Redis(主从 / Redlock)

ZooKeeper

CAP 类型

AP(牺牲一致性,保证可用性)

CP(牺牲可用性,保证一致性)

一致性

弱(异步复制可能丢失锁数据)

强(ZAB 协议保证数据一致)

可用性

高(快速故障转移,短暂不可用)

低(Leader 选举期间不可用)

实现复杂度

简单(单节点 / Redlock 需多实例协调)

复杂(需理解 ZAB 协议和节点监听机制)

典型场景

非核心业务锁、高并发读场景

金融级强一致场景、分布式协调中心

5. 实际应用建议

  1. 优先选 Redis 的场景
    • 业务允许短暂锁失效(如缓存击穿防护)。
    • 需要高吞吐量和低延迟(Redis 单节点性能优于 ZK)。
  2. 优先选 ZooKeeper 的场景
    • 强一致性要求(如分布式事务、分布式选举)。
    • 容忍短暂不可用,但必须保证锁的唯一性(如分布式锁核心场景)。
  3. 替代方案
    • 若追求 AP + 强一致性,可考虑 etcd(基于 Raft 协议的 CP 系统,比 ZK 更易用)。

三、RedLock分布式锁原理及存在问题

RedLock 是 Redis 官方提出的分布式锁方案,旨在解决单实例 Redis 锁的 单点故障问题,其核心思想是通过 多个独立的 Redis 实例(通常建议 ≥5 个)实现 “多数派共识”,确保锁的互斥性和容错性。

核心流程

  1. 获取锁
    • 客户端同时向 所有 Redis 实例 发送 SET key value NX PX TTL 命令(NX 表示仅当键不存在时创建,PX 设置过期时间)。
    • 客户端收集各实例的响应结果,若 至少半数实例(≥n/2+1)返回成功,且总耗时 ≤ TTL,则认为获取锁成功。
    • 若失败,需向 所有实例释放锁(即使部分实例获取锁失败,防止残留锁)。
  2. 释放锁
    • 客户端向 所有实例 发送包含 唯一标识符(UUID) 的释放命令,确保仅释放自己持有的锁(避免误释放其他客户端的锁)。

设计目标

  • 互斥性:同一时刻仅一个客户端持有锁。
  • 容错性:部分实例故障时(≤n/2),锁仍可用。
  • 高可用性:通过多实例异步复制提升可用性(Redis 本身为 AP 系统)。

存在的问题

RedLock 的设计基于 异步复制和松散的多数派共识,存在以下核心缺陷:

1. 时钟同步问题(主因)
  • 场景:若某个 Redis 实例的时钟发生 跳跃(如回退或突然变快),可能导致锁提前过期或延迟释放。
    • 例:客户端 A 在实例 1、2、3 成功获取锁(多数派),但实例 3 的时钟突然回退,其锁提前过期,客户端 B 可能在实例 3 上重新获取锁,导致 锁冲突
  • 影响:违反互斥性,可能引发业务逻辑错误(如重复扣款)。
2. 主从切换引发的锁冗余
  • 场景:Redis 主从复制是 异步的,若主节点在写入锁后未同步到从节点就宕机,新主节点(从节点)可能不存在该锁,导致:
    • 客户端 A 在旧主节点获取锁,但未同步到从节点;旧主节点宕机后,从节点升级为主节点,客户端 B 可在新主节点重新获取锁,两个客户端同时持有锁
  • 本质:Redis 的异步复制无法保证强一致性,RedLock 依赖的 “多数派” 未解决主从切换的 数据不一致窗口 问题。
3. 网络分区下的脑裂
  • 场景:当发生网络分区时,客户端可能在 不同的实例子集 上获取到锁。
    • 例:5 个实例分为 3 个和 2 个分区,客户端 A 在 3 个实例获取锁(多数派),客户端 B 在另外 2 个实例(旧主节点所在分区)也可能获取锁(若旧主节点未感知到分区,仍认为自己是主节点),导致 锁冲突
4. 释放锁的原子性风险
  • 释放锁时需向所有实例发送命令,若某实例因网络延迟未收到释放请求,可能导致锁 长时间占用,影响后续客户端获取锁。
5. 性能与复杂度权衡
  • 相比单实例锁,RedLock 需要 多次网络往返(n 次获取 + n 次释放),延迟较高,吞吐量下降。
  • 运维复杂度增加(需管理多个独立实例)。

社区争议与替代方案

  • 争议:分布式系统专家 Martin Kleppmann 曾质疑 RedLock 的安全性,指出其在时钟同步和主从切换场景下无法保证互斥性。
  • Redis 官方回应:承认 RedLock 不适合 强一致性场景,但适用于 非金融级、高可用需求 的业务(如缓存、临时任务锁)。
  • 替代方案
    • 若需 强一致性:选择 ZooKeeper(基于 Zab 协议的 CP 系统)、etcd(Raft 协议)。
    • 若需轻量级方案:单实例 Redis 锁 + 哨兵(适用于非严格互斥场景)。

四、大促高并发下提升缓存性能100倍-读写锁

1.利用 Redisson 读写锁优化热点缓存的实践

1. 热点缓存场景与读写锁适配性

热点缓存(如商品详情、活动配置)的核心特点是 读多写少

  • 读操作:大量并发请求读取同一份缓存数据;
  • 写操作:少量请求更新缓存(如数据库数据变更后刷新缓存)。

普通互斥锁(如 Redis 单实例 SET 锁)会导致读操作串行化,成为性能瓶颈。而 读写锁(ReadWriteLock)允许:

  • 读锁(共享锁):多个线程/进程可同时持有,支持并发读;
  • 写锁(排他锁):仅一个线程/进程持有,写操作时阻塞所有读写。
2. Redisson 读写锁的优势

Redisson 提供的 RReadWriteLock 是基于 Redis 的分布式读写锁实现,具备以下特性:

  • 跨进程互斥:基于 Redis 的分布式特性,保证不同 JVM 或服务实例间的锁互斥;
  • 可重入性:同一线程可多次获取同一读锁/写锁(需释放对应次数);
  • 自动续期(看门狗):默认每 30 秒自动延长锁的过期时间(需业务执行未完成);
  • 公平锁(可选):按请求顺序分配锁(默认非公平,性能更优)。
3. 基于 Redisson 读写锁的热点缓存实现
步骤 1:Redisson 客户端配置

首先配置 Redisson 连接 Redis(以单实例为例):

Config config = new Config();
config.useSingleServer().setAddress("redis://127.0.0.1:6379").setPassword("your-password").setDatabase(0);
RedissonClient redisson = Redisson.create(config);
步骤 2:定义缓存操作逻辑

核心逻辑:读缓存时用读锁(并发读),写缓存时用写锁(排他写),并处理缓存未命中时的加载逻辑(防缓存击穿)。

public class HotCacheManager {private final RedissonClient redisson;private final Cache<String, Object> localCache; // 本地缓存(可选,如Caffeine)public HotCacheManager(RedissonClient redisson) {this.redisson = redisson;this.localCache = Caffeine.newBuilder().build();}/*** 读取热点缓存(读锁)*/public Object getHotData(String key) {// 1. 先查本地缓存(可选,减少Redis访问)Object value = localCache.getIfPresent(key);if (value != null) {return value;}// 2. 获取分布式读锁(共享锁)RReadWriteLock rwLock = redisson.getReadWriteLock("hot_cache_lock:" + key);RLock readLock = rwLock.readLock();try {// 加读锁(带超时,避免死锁)boolean locked = readLock.tryLock(5, 30, TimeUnit.SECONDS);if (!locked) {throw new RuntimeException("获取读锁失败");}// 3. 读Redis缓存value = redisson.getBucket(key).get();if (value != null) {localCache.put(key, value); // 更新本地缓存(可选)return value;}// 4. 缓存未命中,升级为写锁加载数据(防缓存击穿)RLock writeLock = rwLock.writeLock();try {// 二次检查(避免多线程重复加载)value = redisson.getBucket(key).get();if (value == null) {value = loadFromDatabase(key); // 从数据库加载redisson.getBucket(key).set(value, 300, TimeUnit.SECONDS); // 写入Redis缓存}localCache.put(key, value); // 更新本地缓存(可选)return value;} finally {writeLock.unlock(); // 释放写锁}} catch (InterruptedException e) {Thread.currentThread().interrupt();throw new RuntimeException("获取锁中断");} finally {readLock.unlock(); // 释放读锁}}/*** 更新热点缓存(写锁)*/public void updateHotData(String key, Object newValue) {RReadWriteLock rwLock = redisson.getReadWriteLock("hot_cache_lock:" + key);RLock writeLock = rwLock.writeLock();try {// 加写锁(带超时)boolean locked = writeLock.tryLock(5, 30, TimeUnit.SECONDS);if (!locked) {throw new RuntimeException("获取写锁失败");}// 更新Redis缓存redisson.getBucket(key).set(newValue, 300, TimeUnit.SECONDS);// 更新数据库(根据业务需求)updateDatabase(key, newValue);// 清空本地缓存(可选)localCache.invalidate(key);} catch (InterruptedException e) {Thread.currentThread().interrupt();throw new RuntimeException("获取锁中断");} finally {writeLock.unlock(); // 释放写锁}}private Object loadFromDatabase(String key) {// 模拟数据库查询return "db_data_" + key;}private void updateDatabase(String key, Object value) {// 模拟数据库更新}
}
关键优化点说明
  • 本地缓存(可选):通过 Caffeine 等本地缓存减少 Redis 访问,提升读性能(需权衡一致性,可设置短过期时间)。
  • 读锁防缓存击穿:缓存未命中时,通过写锁互斥加载数据,避免大量请求穿透到数据库。
  • 锁超时控制tryLock(5, 30, TimeUnit.SECONDS) 表示最多等待 5 秒获取锁,锁最长持有 30 秒(防止线程挂起导致锁无法释放)。

2. Redisson 读写锁核心源码分析(3.6.5 版本)

1.读写锁核心特性
  1. 互斥规则
    • 读读不互斥:多个线程可同时持有读锁
    • 读写互斥:写锁阻塞时,新读锁需等待写锁释放
    • 写写互斥:仅允许一个线程持有写锁
  2. 存储结构
    • 使用 Redis Hash 存储锁元数据,Key 为锁名,包含字段:
      • mode:标识锁类型(read/write
      • UUID:threadId:记录读锁线程的重入次数
      • UUID:threadId:write:记录写锁线程的重入次数
    • 额外键 {lockName}:UUID:threadId:rwlock_timeout:{index} 管理每个读锁的超时

2.加读锁源码解析

入口代码RedissonReadLock#tryLockInnerAsync

核心 Lua 脚本逻辑(简化自 3.6.5 源码):

-- 参数:KEYS[1]=lockName, KEYS[2]=timeoutKeyPrefix, ARGV[1]=leaseTime, ARGV[2]=threadId
local mode = redis.call('hget', KEYS[1], 'mode');
if mode == false then -- 锁不存在redis.call('hset', KEYS[1], 'mode', 'read');          -- 设置模式为读锁redis.call('hset', KEYS[1], ARGV[2], 1);             -- 记录线程重入次数=1redis.call('set', KEYS[2] .. ':1', 1);               -- 创建超时控制键redis.call('pexpire', KEYS[2] .. ':1', ARGV[1]);     -- 设置超时redis.call('pexpire', KEYS[1], ARGV[1]);             -- 设置主锁超时return nil; -- 加锁成功
end;
if mode == 'read' or (mode == 'write' and redis.call('hexists', KEYS[1], ARGV[2]..':write')) then local counter = redis.call('hincrby', KEYS[1], ARGV[2], 1); -- 重入次数+1redis.call('set', KEYS[2] .. ':' .. counter, 1);     -- 为新重入层级创建超时键redis.call('pexpire', KEYS[1], ARGV[1]);             -- 续期主锁return nil; -- 加锁成功
end;
return redis.call('pttl', KEYS[1]); -- 返回锁剩余时间(需重试)

关键逻辑

  • 首次加锁:初始化 Hash 结构,标识为读模式
  • 重入或同一线程降级(持有写锁时再加读锁):增加重入计数,新建超时键
  • 冲突处理:若存在写锁且非当前线程,返回锁 TTL 触发客户端重试

3. 加写锁源码解析

入口代码RedissonWriteLock#tryLockInnerAsync

核心 Lua 脚本

-- 参数:KEYS[1]=lockName, ARGV[1]=leaseTime, ARGV[2]=writeThreadId
local mode = redis.call('hget', KEYS[1], 'mode');
if mode == false then -- 锁不存在redis.call('hset', KEYS[1], 'mode', 'write');         -- 设置模式为写锁redis.call('hset', KEYS[1], ARGV[2], 1);             -- 记录写锁重入次数=1redis.call('pexpire', KEYS[1], ARGV[1]);             -- 设置超时return nil; -- 加锁成功
end;
if mode == 'write' and redis.call('hexists', KEYS[1], ARGV[2]) == 1 then redis.call('hincrby', KEYS[1], ARGV[2], 1);          -- 写锁重入redis.call('pexpire', KEYS[1], ARGV[1]);             -- 续期return nil; 
end;
return redis.call('pttl', KEYS[1]); -- 存在读锁或其他写锁,返回TTL

关键逻辑

  • 写锁互斥:若当前存在读锁或其他线程的写锁,直接返回 TTL 等待
  • 可重入:同一线程多次获取写锁时增加计数
  • 降级支持:持有写锁的线程可再加读锁(通过检查 write 标识实现)

4.释放锁源码解析

1. 释放读锁RedissonReadLock#unlockInnerAsync

local counter = redis.call('hincrby', KEYS[1], ARGV[2], -1); -- 重入次数-1
if counter == 0 then redis.call('hdel', KEYS[1], ARGV[2]);               -- 删除线程条目
end;
redis.call('del', KEYS[3] .. ':' .. (counter+1));       -- 删除对应超时键
if redis.call('hlen', KEYS[1]) > 1 then -- 存在其他读锁:计算最大剩余时间并更新主锁超时local maxRemainTime = calculateMaxTime(); redis.call('pexpire', KEYS[1], maxRemainTime); return 0; 
else redis.call('del', KEYS[1]);                         -- 无其他锁则彻底删除return 1; 
end;

2. 释放写锁RedissonWriteLock#unlockInnerAsync

local counter = redis.call('hincrby', KEYS[1], ARGV[3], -1); 
if counter == 0 then redis.call('hdel', KEYS[1], ARGV[3]);               -- 删除写锁条目if redis.call('hlen', KEYS[1]) == 1 then redis.call('del', KEYS[1]);                     -- 无其他锁则删除else redis.call('hset', KEYS[1], 'mode', 'read');    -- 存在读锁则降级为读模式end; return 1; 
else redis.call('pexpire', KEYS[1], ARGV[2]);            -- 重入次数未归零,仅续期return 0; 
end;

关键逻辑

  • 读锁释放:需同步清理超时键,主锁超时按剩余读锁的最大 TTL 更新
  • 写锁释放:重入计数归零时,若存在等待的读锁,将模式降级为 read

5.看门狗机制(续期)
  • 读锁续期:遍历所有读锁线程,为每个 rwlock_timeout 键续期

见 RedissonReadLock#renewExpirationAsync

for (String key : allKeys) {for (int i = 1; i <= reentrantCount; i++) {String timeoutKey = timeoutPrefix + ":" + key + ":rwlock_timeout:" + i;redis.command("pexpire", timeoutKey, leaseTime);}
}
  • 写锁续期:与普通锁相同,仅续期主锁 Key

6.锁升降级规则
  • 支持降级:持有写锁的线程可再获取读锁(避免死锁)
  • 禁止升级:持有读锁时尝试获取写锁会阻塞(因读写互斥,易死锁)

示例:

rwLock.readLock().lock();
rwLock.writeLock().lock(); // 此处永久阻塞!

总结

Redisson 读写锁通过 Redis Hash 结构 和 多键协同 实现互斥规则:

  1. 模式标识mode)决定锁类型,控制读写互斥;
  2. 超时键分离 解决读锁并发续期的一致性;
  3. 写锁降级 通过先释放写锁保留读锁实现,但升级会导致死锁。

建议结合 org.redisson.RedissonReadWriteLock 类调试以深入理解流程。

3. Redisson 读写锁总结

优势
  • 高性能读并发:读锁共享,支持多客户端同时读取热点缓存。
  • 强一致性:基于 Redis 的分布式特性,保证跨进程的锁互斥。
  • 自动续期:内置看门狗机制(默认 30 秒续期),防止业务执行期间锁过期。
注意事项
  • 锁粒度控制:避免锁粒度过大(如全局锁)导致竞争激烈,建议按业务维度(如商品 ID)拆分锁。
  • 写锁优先级:Redisson 读写锁默认非公平(可能导致写锁饥饿),需业务评估是否需要公平锁(RReadWriteLock#fairLock())。
  • 超时设置:需根据业务执行时间设置合理的锁超时时间(tryLock 的 leaseTime),避免锁无法及时释放。
适用场景
  • 读多写少的热点缓存(如商品详情、活动配置);
  • 数据一致性要求高(写锁保证原子性更新);
  • 分布式系统跨进程协调(如多实例缓存更新互斥)。

通过 Redisson 读写锁,可在热点缓存场景下显著提升读性能,同时保证写操作的原子性,是分布式系统中解决读多写少问题的经典方案。

http://www.xdnf.cn/news/784045.html

相关文章:

  • 二进制安全-OpenWrt-uBus
  • Ethernet/IP转DeviceNet网关:驱动大型矿山自动化升级的核心纽带
  • Freemarker快速入门
  • Linux 测试本机与192.168.1.130 主机161/udp端口连通性
  • 【办公类-48-04】202506每月电子屏台账汇总成docx-5(问卷星下载5月范围内容,自动获取excel文件名,并转移处理)
  • 【最新版】西陆洗车系统源码全开源+uniapp前端+搭建教程
  • 悟饭游戏厅苹果版(悟饭掌悦)|iOS游戏社区手柄工具
  • HCIP(BGP综合实验)
  • window 显示驱动开发-DirectX 视频加速 2.0
  • 15个基于场景的 DevOps 面试问题及答案
  • P2656 采蘑菇
  • Linux总结
  • Redis看门狗机制
  • Halcon光度立体法
  • 相机Camera日志分析之二十四:高通相机Camx 基于预览1帧的process_capture_request三级日志分析详解
  • 矩阵的偏导数
  • 点击启动「高效模式」:大腾智能 CAD 重构研发设计生产力
  • 『React』组件副作用,useEffect讲解
  • KEYSIGHT是德科技 E5063A 18G ENA系列网络分析仪
  • 【python与生活】用 Python 从视频中提取音轨:一个实用脚本的开发与应用
  • 6.RV1126-OPENCV 形态学基础膨胀及腐蚀
  • git stash介绍(临时保存当前工作目录中尚未提交的修改)
  • CentOS Stream 8 Unit network.service not found
  • 【Python进阶】元类编程
  • 本人精通各种语言输出hello world
  • Windows应用-音视频捕获
  • 关于Tabs组件下TabPane使用v-if导致顺序错误以及页面渲染异常的解决方法
  • Linux Maven Install
  • LeetCode第245题_最短单词距离III
  • PDF.js无法显示数字签名