RedLock算法原理与实践
RedLock算法概述
RedLock是Redis作者Antirez提出的一种多节点分布式锁算法,旨在解决单节点Redis分布式锁存在的单点故障问题。
官方文档: https://redis.io/docs/manual/patterns/distributed-locks/
单节点Redis的问题
在使用单节点Redis实现分布式锁时,存在以下风险:
问题一: 单点故障
如果Redis实例宕机,所有依赖该实例的客户端都无法获取锁,导致系统不可用。
问题二: 主从同步延迟
sequenceDiagram
participant C1 as 客户端1
participant M as Master节点
participant S as Slave节点
participant C2 as 客户端2
C1->>M: 加锁成功
M->>M: 锁数据写入
Note over M,S: Master宕机,数据未同步
M-xM: 节点宕机
S->>S: 被选举为新Master
C2->>S: 尝试加锁
S->>S: 检查锁(不存在)
S-->>C2: 加锁成功
Note over C1,C2: 两个客户端同时持有锁!时序说明:
- 客户端1在Master节点加锁成功
- Master节点宕机,锁数据未同步到Slave
- Slave被选举为新Master
- 客户端2在新Master上加锁成功
- 出现两个客户端同时持有锁的情况
RedLock解决方案
RedLock通过引入多个独立的Redis节点来提供容错能力:
graph TD
A[客户端] --> B[Redis节点1]
A --> C[Redis节点2]
A --> D[Redis节点3]
A --> E[Redis节点4]
A --> F[Redis节点5]
B --> G{N/2+1个节点成功}
C --> G
D --> G
E --> G
F --> G
G -->|是| H[获取锁成功]
G -->|否| I[获取锁失败]
style A fill:#e1f5ff,stroke:#01579b,stroke-width:2px,rx:10,ry:10
style H fill:#e8f5e9,stroke:#1b5e20,stroke-width:2px,rx:10,ry:10
style I fill:#ffebee,stroke:#b71c1c,stroke-width:2px,rx:10,ry:10RedLock算法流程
加锁流程
graph TD
A[开始加锁] --> B[生成唯一标识ID]
B --> C[记录开始时间]
C --> D[向N个节点请求加锁]
D --> E{成功节点数>=N/2+1}
E -->|是| F{总耗时<锁有效期}
E -->|否| G[向所有节点发送解锁]
F -->|是| H[加锁成功]
F -->|否| G
G --> I[加锁失败]
H --> J[执行业务逻辑]
J --> K[向所有节点解锁]
style A fill:#e1f5ff,stroke:#01579b,stroke-width:2px,rx:10,ry:10
style H fill:#e8f5e9,stroke:#1b5e20,stroke-width:2px,rx:10,ry:10
style I fill:#ffebee,stroke:#b71c1c,stroke-width:2px,rx:10,ry:10详细步骤说明
步骤1: 生成唯一标识
客户端生成全局唯一ID(建议使用时间戳+随机数):
String lockId = System.currentTimeMillis() + "-" + UUID.randomUUID();步骤2: 向所有节点请求加锁
使用相同的key和唯一ID向所有Redis节点发送SETNX命令:
SET lockKey lockId NX PX 30000步骤3: 统计成功节点数
记录在所有节点上加锁成功的数量,同时记录总耗时。
步骤4: 判断加锁是否成功
满足以下两个条件才算加锁成功:
- 成功获取锁的节点数 >= N/2 + 1
- 总耗时 < 锁的有效期
步骤5: 处理失败情况
如果加锁失败,向所有节点(包括加锁成功的)发送解锁命令。
三节点示例
sequenceDiagram
participant C as 客户端
participant R1 as Redis-1
participant R2 as Redis-2
participant R3 as Redis-3
C->>C: 生成lockId
C->>C: 记录startTime
par 并行请求
C->>R1: SET key lockId NX PX 30000
C->>R2: SET key lockId NX PX 30000
C->>R3: SET key lockId NX PX 30000
end
R1-->>C: OK (成功)
R2-->>C: OK (成功)
R3-->>C: nil (失败,已被占用)
C->>C: 成功数=2, N/2+1=2, 满足条件
C->>C: 检查耗时 < 有效期
C->>C: 加锁成功!
Note over C: 执行业务逻辑
par 并行解锁
C->>R1: DEL key (if value=lockId)
C->>R2: DEL key (if value=lockId)
C->>R3: DEL key (if value=lockId)
endRedisson中的RedLock实现
代码示例
@Configuration
public class RedLockConfiguration {
@Bean
public RedissonClient redissonClient1() {
Config config = new Config();
config.useSingleServer()
.setAddress("redis://192.168.1.101:6379");
return Redisson.create(config);
}
@Bean
public RedissonClient redissonClient2() {
Config config = new Config();
config.useSingleServer()
.setAddress("redis://192.168.1.102:6379");
return Redisson.create(config);
}
@Bean
public RedissonClient redissonClient3() {
Config config = new Config();
config.useSingleServer()
.setAddress("redis://192.168.1.103:6379");
return Redisson.create(config);
}
}使用RedLock
@Service
public class OrderService {
@Autowired
private RedissonClient redissonClient1;
@Autowired
private RedissonClient redissonClient2;
@Autowired
private RedissonClient redissonClient3;
public void processOrder(String orderId) throws InterruptedException {
RLock lock1 = redissonClient1.getLock("order:" + orderId);
RLock lock2 = redissonClient2.getLock("order:" + orderId);
RLock lock3 = redissonClient3.getLock("order:" + orderId);
RedissonRedLock redLock = new RedissonRedLock(lock1, lock2, lock3);
// 等待时间3秒,锁持有时间30秒
boolean isLocked = redLock.tryLock(3, 30, TimeUnit.SECONDS);
if (isLocked) {
try {
// 处理订单业务
updateOrderStatus(orderId);
reduceInventory(orderId);
} finally {
redLock.unlock();
}
} else {
throw new BusinessException("订单处理失败,请稍后重试");
}
}
}其他RedLock实现库
除了Redisson,还有其他工具支持RedLock:
Java: Redlock-java库
<dependency>
<groupId>com.github.alturkovic</groupId>
<artifactId>distributed-lock-redis</artifactId>
<version>latest</version>
</dependency>Go: Redsync库
import "github.com/go-redsync/redsync/v4"Python: Pottery库
from pottery import RedlockRedLock的争议与问题
Martin Kleppmann的批评
Martin Kleppmann是分布式系统专家,《数据密集型应用系统设计》作者,他在博客文章中指出RedLock存在的问题:
文章链接: https://martin.kleppmann.com/2016/02/08/how-to-do-distributed-locking.html
主要问题分析
问题一: 时钟漂移
sequenceDiagram
participant C1 as 客户端1
participant R1 as Redis节点1(快)
participant R2 as Redis节点2(慢)
participant C2 as 客户端2
Note over R1,R2: 节点时钟不同步
C1->>R1: 设置锁,30秒过期
C1->>R2: 设置锁,30秒过期
Note over R1: 时钟快10秒
Note over R1: 20秒后锁过期
R1->>R1: 锁已过期,自动删除
C2->>R1: 加锁成功
C2->>R2: 加锁失败
Note over C1,C2: 虽然只在1个节点成功<br/>但可能存在并发问题不同机器的时钟可能存在微小漂移,导致锁的失效时间不一致,可能产生安全问题。
问题二: 网络分区
graph TD
A[客户端1] --> B[网络分区发生]
B --> C[子网1: R1, R2]
B --> D[子网2: R3, R4, R5]
E[客户端2] --> D
C --> F[客户端1在2个节点加锁]
D --> G[客户端2在3个节点加锁]
F --> H[客户端1认为加锁失败]
G --> I[客户端2认为加锁成功]
style H fill:#ffebee,stroke:#b71c1c,stroke-width:2px,rx:10,ry:10
style I fill:#e8f5e9,stroke:#1b5e20,stroke-width:2px,rx:10,ry:10在网络分区情况下,不同的客户端可能在不同子网中获取到"有效"的锁。
问题三: GC停顿
sequenceDiagram
participant C as 客户端
participant R as Redis集群
participant GC as GC线程
C->>R: 获取RedLock成功
Note over C: 开始执行业务
GC->>C: 触发Full GC
Note over C: 停顿40秒
Note over R: 锁过期(30秒)
R->>R: 自动释放锁
Note over C: GC结束,继续执行
C->>C: 以为还持有锁
C->>C: 继续执行业务逻辑
Note over C: 实际上锁已被释放<br/>可能产生并发问题客户端发生长时间GC停顿,导致锁过期被释放,但客户端恢复后仍认为持有锁。
Antirez的回应
Redis作者Antirez对批评进行了回应:
文章链接: http://antirez.com/news/101
主要观点:
- 时钟漂移可控: 使用NTP等工具同步时钟,将漂移控制在可接受范围
- 网络分区概率低: 分区恢复后RedLock会自动解锁,问题窗口期很短
- 合理的权衡: RedLock在可用性和一致性之间做了合理权衡,适合大多数场景
Redisson废弃RedLock
废弃公告
Redisson在后续版本中将RedLock标记为Deprecated:
/**
* @deprecated Use {@link RLock#lock()} or {@link RLock#tryLock()} instead
*/
@Deprecated
public class RedissonRedLock extends RedissonMultiLock {
// ...
}废弃原因分析
原因一: 缺乏官方认证
Redis作者虽然提出了RedLock算法,但后来表示该算法未经彻底验证,不推荐在需要严格一致性的场景中使用。
原因二: 安全性担忧
在网络分区、节点故障等情况下,RedLock可能无法保证完全的互斥性。
原因三: 维护复杂性
需要管理多个独立Redis实例,增加了部署复杂度和出错概率。
原因四: 学术界质疑
分布式系统专家Martin Kleppmann的批评文章影响广泛,指出了算法的多个关键问题。
替代方案
方案一: 单实例Redis锁
@Service
public class SimpleRedisLockService {
@Autowired
private RedissonClient redissonClient;
public void processOrder(String orderId) {
RLock lock = redissonClient.getLock("order:" + orderId);
lock.lock();
try {
// 业务处理
handleOrder(orderId);
} finally {
lock.unlock();
}
}
}适用场景:
- 可以容忍短暂服务中断
- 对一致性要求不是特别严格
- 系统规模不大
优点:
- 实现简单
- 性能高
- 易于维护
缺点:
- 存在单点故障风险
方案二: 业务层幂等+对账
@Service
public class IdempotentOrderService {
@Autowired
private RedissonClient redissonClient;
@Autowired
private OrderRepository orderRepository;
@Transactional
public void createOrder(OrderDTO orderDTO) {
RLock lock = redissonClient.getLock("order:" + orderDTO.getOrderId());
lock.lock();
try {
// 幂等性检查
Order existing = orderRepository.findByOrderId(orderDTO.getOrderId());
if (existing != null) {
log.warn("订单已存在,orderId: {}", orderDTO.getOrderId());
return;
}
// 创建订单
Order order = new Order();
order.setOrderId(orderDTO.getOrderId());
order.setStatus(OrderStatus.CREATED);
// 数据库唯一约束保证
orderRepository.save(order);
} catch (DuplicateKeyException e) {
log.warn("订单重复创建被拦截", e);
} finally {
lock.unlock();
}
}
// 定时对账任务
@Scheduled(cron = "0 0 2 * * ?")
public void reconciliation() {
// 检查订单状态一致性
// 修复不一致数据
}
}核心思路:
- 使用普通Redis锁
- 在业务层实现幂等性控制
- 使用数据库唯一约束兜底
- 定期执行对账修复
优点:
- 即使锁出现问题,业务也不会错
- 多层防护,安全性高
方案三: 强一致性组件
使用ZooKeeper、etcd或Consul等强一致性组件:
@Configuration
public class ZookeeperLockConfiguration {
@Bean
public CuratorFramework curatorFramework() {
CuratorFramework client = CuratorFrameworkFactory.builder()
.connectString("192.168.1.101:2181,192.168.1.102:2181,192.168.1.103:2181")
.sessionTimeoutMs(5000)
.retryPolicy(new ExponentialBackoffRetry(1000, 3))
.build();
client.start();
return client;
}
}
@Service
public class ZookeeperLockService {
@Autowired
private CuratorFramework curatorFramework;
public void processWithZkLock(String resourceId) throws Exception {
InterProcessMutex lock = new InterProcessMutex(
curatorFramework,
"/locks/" + resourceId
);
if (lock.acquire(5, TimeUnit.SECONDS)) {
try {
// 业务处理
handleBusiness(resourceId);
} finally {
lock.release();
}
}
}
}特点:
- 基于Raft/Paxos算法保证强一致性
- 天然支持分布式协调
- 可靠性高
适用场景:
- 对一致性要求极高
- 可接受稍高延迟
- 已有ZK等组件
对比分析:
| 特性 | Redis单实例 | RedLock | ZooKeeper |
|---|---|---|---|
| 一致性 | 弱 | 中 | 强 |
| 可用性 | 中 | 高 | 高 |
| 性能 | 高 | 中 | 中 |
| 复杂度 | 低 | 高 | 中 |
| 适用场景 | 普通业务 | 已废弃 | 强一致需求 |
实践建议
建议一: 优先选择简单方案
graph TD
A[选择分布式锁方案] --> B{业务特性}
B -->|一般业务| C[Redis单实例]
B -->|高可用需求| D[Redis+幂等]
B -->|强一致需求| E[ZooKeeper/etcd]
C --> F[配置Redis高可用]
D --> G[实现幂等控制]
E --> H[部署ZK集群]
style C fill:#e8f5e9,stroke:#1b5e20,stroke-width:2px,rx:10,ry:10
style D fill:#fff3e0,stroke:#e65100,stroke-width:2px,rx:10,ry:10
style E fill:#e1f5ff,stroke:#01579b,stroke-width:2px,rx:10,ry:10建议二: 多层防护
不要完全依赖分布式锁,要在业务层实现多层防护:
public class MultiLayerProtection {
// 第一层: 分布式锁
public boolean processWithLock(String bizId) {
RLock lock = redissonClient.getLock("biz:" + bizId);
if (!lock.tryLock()) {
return false;
}
try {
// 第二层: 状态检查
if (alreadyProcessed(bizId)) {
return false;
}
// 第三层: 数据库唯一约束
return saveWithUniqueConstraint(bizId);
} finally {
lock.unlock();
}
}
}建议三: 避免使用RedLock
鉴于RedLock已被废弃且存在争议,建议:
- 新项目不要使用RedLock
- 老项目逐步迁移到其他方案
- 优先考虑业务层幂等性
核心要点总结
- RedLock设计: 通过多节点投票解决单点故障,需要N/2+1个节点加锁成功
- 存在问题: 时钟漂移、网络分区、GC停顿等问题可能导致安全性问题
- 已被废弃: Redisson已将RedLock标记为Deprecated,不建议使用
- 替代方案:
- 单实例Redis + 高可用部署
- 业务幂等 + 数据库约束 + 定期对账
- ZooKeeper/etcd等强一致性组件
- 选型原则: 根据业务特性选择合适方案,不追求过度复杂的技术方案
- 多层防护: 不完全依赖分布式锁,在业务层实现幂等和对账机制
更新: 2025-12-04 17:38:44
原文: https://www.yuque.com/u22210564/zoxfmt/doc-02-redis-16-redlock