1. 分布式锁的本质与核心诉求
分布式锁是分布式系统中协调多节点访问共享资源的机制。想象一下多个服务实例同时竞争修改数据库同一条记录的场景——没有锁机制就会导致数据错乱。与单机锁不同,分布式锁需要解决网络延迟、节点故障等分布式环境特有的问题。
核心诉求可归纳为三点:
- 互斥性:同一时刻只有一个客户端能持有锁
- 防死锁:持有锁的客户端崩溃后锁能自动释放
- 高可用:锁服务本身需要具备容错能力
提示:评估分布式锁方案时,CAP理论中的CP和AP特性选择会直接影响设计方向。Redis倾向于AP,ZK则更偏向CP。
2. Redis实现分布式锁的演进之路
2.1 基础版:SETNX + EXPIRE
最朴素的实现方式:
// 加锁 Boolean locked = redis.setnx("lock_key", "1"); if(locked) { redis.expire("lock_key", 30); } // 解锁 redis.del("lock_key");这个方案存在致命缺陷:
- 加锁和设置过期时间不是原子操作,可能设置过期时间前进程崩溃
- 可能误删其他客户端的锁(A的锁过期后被B获取,A执行完又删了B的锁)
2.2 改进版:SET扩展命令
Redis 2.6.12后支持扩展参数:
SET lock_key unique_value NX PX 30000- NX:等效于SETNX
- PX:设置毫秒级过期时间
- unique_value:客户端唯一标识,避免误删
Java实现示例:
String uuid = UUID.randomUUID().toString(); Boolean locked = redisTemplate.opsForValue() .setIfAbsent("lock_key", uuid, 30, TimeUnit.SECONDS); // 解锁时先比较value再删除 if(uuid.equals(redisTemplate.opsForValue().get("lock_key"))) { redisTemplate.delete("lock_key"); }2.3 终极挑战:锁续期问题
即使解决了原子性问题,仍存在:
- 业务执行时间超过锁过期时间
- 锁被其他客户端获取导致并发问题
解决方案是看门狗机制:启动后台线程定期检查并延长锁过期时间。这正是Redisson的核心设计之一。
3. Redisson分布式锁源码解析
3.1 加锁流程剖析
RedissonLock的核心加锁逻辑:
<T> RFuture<T> tryLockInnerAsync(long leaseTime, TimeUnit unit, long threadId, RedisStrictCommand<T> command) { // 使用Lua脚本保证原子性 return commandExecutor.evalWriteAsync(getName(), LongCodec.INSTANCE, command, "if (redis.call('exists', KEYS[1]) == 0) then " + "redis.call('hset', KEYS[1], ARGV[2], 1); " + "redis.call('pexpire', KEYS[1], ARGV[1]); " + "return nil; " + "end; " + "if (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]);", Collections.singletonList(getName()), unit.toMillis(leaseTime), getLockName(threadId)); }关键设计点:
- 使用Hash结构存储锁信息,支持可重入
- 通过Lua脚本保证多个Redis命令的原子性
- 返回nil表示加锁成功,否则返回剩余TTL
3.2 看门狗机制实现
锁续期的核心代码在RedissonLock#scheduleExpirationRenewal:
protected void scheduleExpirationRenewal(long threadId) { // 每10秒续期一次 Timeout task = commandExecutor.getConnectionManager() .newTimeout(new TimerTask() { @Override public void run(Timeout timeout) { // 续期Lua脚本 RFuture<Boolean> future = commandExecutor.evalWriteAsync( getName(), LongCodec.INSTANCE, RedisCommands.EVAL_BOOLEAN, "if (redis.call('hexists', KEYS[1], ARGV[2]) == 1) then " + "redis.call('pexpire', KEYS[1], ARGV[1]); " + "return 1; " + "end; " + "return 0;", Collections.singletonList(getName()), internalLockLeaseTime, getLockName(threadId)); future.onComplete((res, e) -> { if (e != null) { return; } if (res) { // 递归调用实现周期性续期 scheduleExpirationRenewal(threadId); } }); } }, internalLockLeaseTime / 3, TimeUnit.MILLISECONDS); }注意:默认锁超时时间是30秒,续期间隔是超时时间的1/3(即10秒)
3.3 解锁时的"脑裂"防护
解锁时除了检查客户端标识,还需要处理一种极端情况:
- 客户端A持有锁
- 因GC停顿导致看门狗线程暂停
- 锁过期后被客户端B获取
- 客户端A恢复后执行解锁操作
Redisson通过增加一步GET操作来检测:
if (redis.call('get', KEYS[1]) == ARGV[1]) then return redis.call('del', KEYS[1]) else return 0 end4. Zookeeper分布式锁实现对比
4.1 临时顺序节点方案
ZK实现分布式锁的标准模式:
- 在锁节点下创建临时顺序节点
- 获取所有子节点,判断自己是否是最小节点
- 如果是则获取锁,否则监听前一个节点的删除事件
- 处理完成后删除自己的节点
public void lock() { // 创建临时顺序节点 currentPath = zk.create(basePath + "/lock-", null, ZooDefs.Ids.OPEN_ACL_UNSAFE, CreateMode.EPHEMERAL_SEQUENTIAL); // 获取并排序所有子节点 List<String> children = zk.getChildren(basePath, false); Collections.sort(children); // 判断自己是否是最小节点 int index = children.indexOf(currentPath.substring(basePath.length() + 1)); if (index == 0) { return; // 获取锁成功 } else { // 监听前一个节点 String prevPath = basePath + "/" + children.get(index - 1); CountDownLatch latch = new CountDownLatch(1); zk.exists(prevPath, event -> { if (event.getType() == EventType.NodeDeleted) { latch.countDown(); } }); latch.await(); } }4.2 ZK与Redis方案对比
| 特性 | Redis方案 | ZK方案 |
|---|---|---|
| 实现复杂度 | 相对简单 | 较复杂 |
| 性能 | 高(内存操作) | 较低(需要持久化) |
| 可靠性 | 依赖Redis持久化 | 基于ZK的强一致性 |
| 锁释放机制 | 依赖超时 | 会话结束自动释放 |
| 惊群效应 | 存在(大量客户端重试) | 避免(顺序监听机制) |
| 适用场景 | 高并发、允许偶尔超时 | 强一致性要求的场景 |
5. 生产环境中的最佳实践
5.1 锁粒度控制技巧
- 细粒度锁:按业务ID分段加锁(如order_123_lock)
- 锁超时时间:根据业务平均耗时动态调整
- 重试策略:指数退避算法避免集群雪崩
// 分段锁示例 public void updateOrder(Long orderId) { String lockKey = "order_" + orderId + "_lock"; RLock lock = redisson.getLock(lockKey); try { if (lock.tryLock(3, 30, TimeUnit.SECONDS)) { // 业务逻辑 } } finally { lock.unlock(); } }5.2 常见问题排查指南
锁永远不释放
- 检查看门狗线程是否正常运行
- 确认finally块中调用了unlock()
解锁时报IllegalMonitorStateException
- 通常是不同线程加锁解锁导致
- 确保加锁解锁在同一线程执行
Redis主从切换导致锁失效
- 考虑使用RedLock算法(需至少3个独立Redis实例)
- 评估业务是否允许极端情况下的短暂不一致
ZK连接断开处理
- 实现ConnectionStateListener监听会话状态
- 准备补偿机制处理中间状态
5.3 监控与性能优化
Redis锁监控指标:
- 锁等待时间分布
- 锁持有时间分布
- 锁获取失败率
ZK锁监控重点:
- 临时节点数量增长趋势
- 节点监听队列深度
- 会话超时告警
我在实际项目中总结的经验是:对于秒杀类场景,Redis分布式锁的性能可以比ZK方案高出一个数量级,但在金融交易等强一致性场景,ZK的可靠性优势更加重要。建议根据业务特点选择技术方案,必要时可以组合使用——比如用Redis锁做第一层过滤,ZK锁做最终一致性控制。