分布式锁实战:Redis与ZooKeeper对比选型与实现方案
大家好,我是迪哥。分布式锁是分布式系统中最基础也是最重要的组件之一,从 Redis Redlock 到 ZooKeeper 临时节点,从数据库乐观锁到 etcd,我们尝试过多种方案。今天就聊聊分布式锁的选型和实现经验。
分布式锁需求分析
核心特性
| 特性 | 说明 |
|---|---|
| 互斥性 | 同一时刻只能有一个客户端持有锁 |
| 超时释放 | 防止死锁 |
| 可重入 | 同一客户端可重复获取同一把锁 |
| 高可用 | 锁服务本身要高可用 |
| 公平性 | 按请求顺序获取锁 |
Redis 分布式锁
基础实现
java
@Service
public class RedisLockService {
@Autowired
private StringRedisTemplate redisTemplate;
private static final String LOCK_PREFIX = "lock:";
private static final long DEFAULT_EXPIRE = 30000; // 30秒
public boolean tryLock(String lockKey, String requestId, long expire) {
Boolean result = redisTemplate.opsForValue()
.setIfAbsent(LOCK_PREFIX + lockKey, requestId, expire, TimeUnit.MILLISECONDS);
return Boolean.TRUE.equals(result);
}
public void unlock(String lockKey, String requestId) {
String script = """
if redis.call('get', KEYS[1]) == ARGV[1] then
return redis.call('del', KEYS[1])
else
return 0
end
""";
redisTemplate.execute(new DefaultRedisScript<>(script, Long.class),
Collections.singletonList(LOCK_PREFIX + lockKey), requestId);
}
}
Redlock 算法
java
public class RedlockService {
private final List<StringRedisTemplate> redisTemplates;
public boolean acquireLock(String lockKey, String requestId, long expire) {
int quorum = redisTemplates.size() / 2 + 1;
int successCount = 0;
for (StringRedisTemplate redis : redisTemplates) {
try {
Boolean result = redis.opsForValue()
.setIfAbsent(lockKey, requestId, expire, TimeUnit.MILLISECONDS);
if (Boolean.TRUE.equals(result)) {
successCount++;
}
} catch (Exception e) {
// 节点不可用
}
}
return successCount >= quorum;
}
}
Redis 锁的问题
- 单点故障:主从切换时可能丢失锁
- 超时问题:业务执行时间超过锁过期时间
- 续期问题:需要看门狗机制
ZooKeeper 分布式锁
基于临时节点的实现
java
public class ZkLockService {
private final CuratorFramework client;
public boolean tryLock(String lockKey) throws Exception {
String lockPath = "/locks/" + lockKey;
// 创建临时有序节点
String nodePath = client.create()
.creatingParentContainersIfNeeded()
.withMode(CreateMode.EPHEMERAL_SEQUENTIAL)
.forPath(lockPath + "/lock-");
// 获取所有子节点
List<String> children = client.getChildren().forPath(lockPath);
Collections.sort(children);
// 判断是否是最小节点
if (nodePath.endsWith(children.get(0))) {
return true;
}
// 监听前一个节点
String prevNode = children.get(findPrevIndex(children, nodePath));
CountDownLatch latch = new CountDownLatch(1);
Watcher watcher = event -> {
if (event.getType() == Watcher.Event.EventType.NodeDeleted) {
latch.countDown();
}
};
client.getData().usingWatcher(watcher).forPath(lockPath + "/" + prevNode);
latch.await();
// 重新检查
return checkMinNode(nodePath, lockPath);
}
}
ZooKeeper 锁的优势
- 天生分布式:基于 ZAB 协议,天然高可用
- 自动释放:会话断开自动删除临时节点
- 公平锁:按节点顺序获取锁
对比选型
| 维度 | Redis | ZooKeeper |
|---|---|---|
| 性能 | 高(百万级 QPS) | 中等(万级 QPS) |
| 可靠性 | 依赖主从复制 | 基于 ZAB,更可靠 |
| 实现复杂度 | 简单 | 复杂 |
| 锁类型 | 可实现公平/非公平 | 天然公平锁 |
| 适用场景 | 高吞吐、非强一致场景 | 强一致、低吞吐场景 |
数据库分布式锁
乐观锁
java
@Service
public class OptimisticLockService {
@Autowired
private OrderMapper orderMapper;
@Transactional
public boolean updateOrder(Long orderId, Integer expectedVersion) {
int rows = orderMapper.updateWithVersion(orderId, expectedVersion);
return rows > 0;
}
}
sql
UPDATE orders
SET status = 'PAID', version = version + 1
WHERE id = ? AND version = ?;
悲观锁
java
@Transactional
public void processOrder(Long orderId) {
Order order = orderMapper.selectForUpdate(orderId);
// 处理业务
orderMapper.update(order);
}
最佳实践
选择建议
┌──────────────────┐
│ 性能要求高? │
└────────┬─────────┘
│
┌────────┴────────┐
▼ ▼
Yes No
│ │
┌────────▼────────┐ ┌─────▼─────┐
│ 使用 Redis │ │ZooKeeper │
│ Redlock 模式 │ │ 或 │
└─────────────────┘ │ 数据库锁 │
└───────────┘
注意事项
- 锁粒度:尽量细粒度,避免锁住整个资源
- 超时设置:合理设置过期时间,考虑业务执行时间
- 异常处理:获取锁失败时的降级策略
- 监控告警:监控锁的获取成功率、等待时间
说到分布式锁,我家那只叫 Docker 的哈士奇最近学会了"锁机制"------它会把喜欢的玩具咬在嘴里,谁要都不给,这独占性比我们的 Redis 锁还强 😂
我是迪哥,我们下期再见!