分布式锁实战:Redis与ZooKeeper对比选型与实现方案
分布式锁实战:Redis与ZooKeeper对比选型与实现方案
大家好,我是迪哥。分布式锁是分布式系统中最基础也是最重要的组件之一,从 Redis Redlock 到 ZooKeeper 临时节点,从数据库乐观锁到 etcd,我们尝试过多种方案。今天就聊聊分布式锁的选型和实现经验。
分布式锁需求分析
核心特性
| 特性 | 说明 |
|---|---|
| 互斥性 | 同一时刻只能有一个客户端持有锁 |
| 超时释放 | 防止死锁 |
| 可重入 | 同一客户端可重复获取同一把锁 |
| 高可用 | 锁服务本身要高可用 |
| 公平性 | 按请求顺序获取锁 |
Redis 分布式锁
基础实现
@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 算法
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 分布式锁
基于临时节点的实现
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,更可靠 |
| 实现复杂度 | 简单 | 复杂 |
| 锁类型 | 可实现公平/非公平 | 天然公平锁 |
| 适用场景 | 高吞吐、非强一致场景 | 强一致、低吞吐场景 |
数据库分布式锁
乐观锁
@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; } }UPDATE orders SET status = 'PAID', version = version + 1 WHERE id = ? AND version = ?;悲观锁
@Transactional public void processOrder(Long orderId) { Order order = orderMapper.selectForUpdate(orderId); // 处理业务 orderMapper.update(order); }最佳实践
选择建议
┌──────────────────┐ │ 性能要求高? │ └────────┬─────────┘ │ ┌────────┴────────┐ ▼ ▼ Yes No │ │ ┌────────▼────────┐ ┌─────▼─────┐ │ 使用 Redis │ │ZooKeeper │ │ Redlock 模式 │ │ 或 │ └─────────────────┘ │ 数据库锁 │ └───────────┘注意事项
- 锁粒度:尽量细粒度,避免锁住整个资源
- 超时设置:合理设置过期时间,考虑业务执行时间
- 异常处理:获取锁失败时的降级策略
- 监控告警:监控锁的获取成功率、等待时间
说到分布式锁,我家那只叫 Docker 的哈士奇最近学会了"锁机制"——它会把喜欢的玩具咬在嘴里,谁要都不给,这独占性比我们的 Redis 锁还强 😂
我是迪哥,我们下期再见!
