1. 为什么每个开发者都该懂的分布式锁

在电商大促秒杀系统中,当1000台服务器同时争夺最后一件商品库存时;在金融转账场景中,当用户余额可能被多个终端并发操作时——这就是分布式锁的战场。作为协调分布式系统有序访问资源的仲裁者,分布式锁需要具备两个核心素质:互斥性(同一时刻只能有一个客户端持有锁)和容错性(即使部分节点故障也能正常运行)。

2. Redis分布式锁的实现之道

2.1 基于SETNX的基础实现

(Java+Redis技术栈)

// Redis原生客户端Jedis实现
public class RedisLock {
    private Jedis jedis;
    private String lockKey;
    private String identifier;
    
    public RedisLock(Jedis jedis, String lockKey) {
        this.jedis = jedis;
        this.lockKey = lockKey;
        this.identifier = UUID.randomUUID().toString();
    }
    
    public boolean tryLock(long expireMillis) {
        String result = jedis.set(lockKey, identifier, 
            SetParams.setParams().nx().px(expireMillis));
        return "OK".equals(result);
    }
    
    public boolean unlock() {
        // 使用Lua脚本保证原子性
        String script = 
            "if redis.call('get', KEYS[1]) == ARGV[1] then " +
            "   return redis.call('del', KEYS[1]) " +
            "else " +
            "   return 0 " +
            "end";
        Object result = jedis.eval(script, 
            Collections.singletonList(lockKey), 
            Collections.singletonList(identifier));
        return result.equals(1L);
    }
}

关键实现要素:

  • 唯一标识符:防止误删其他客户端的锁
  • 原子操作:使用SET NX PX命令一步到位
  • 安全释放:Lua脚本保证检查与删除的原子性

2.2 红锁(RedLock)的高可用方案

当采用Redis集群时,RedLock算法要求客户端依次向N个独立Redis节点申请锁,当超过半数的节点获取成功时才算真正获得锁。这种设计能有效应对单点故障风险,但需要权衡性能与可靠性之间的关系。

3. ZooKeeper分布式锁的优雅实现

(Java+ZooKeeper技术栈)

public class ZkDistributedLock implements Watcher {
    private ZooKeeper zk;
    private String lockPath;
    private String currentPath;
    private CountDownLatch latch = new CountDownLatch(1);
    
    public ZkDistributedLock(String connectString, String lockPath) {
        this.lockPath = lockPath;
        this.zk = new ZooKeeper(connectString, 3000, this);
    }
    
    public boolean tryLock() throws Exception {
        currentPath = zk.create(lockPath + "/lock_", 
            new byte[0], ZooDefs.Ids.OPEN_ACL_UNSAFE,
            CreateMode.EPHEMERAL_SEQUENTIAL);
        
        List<String> children = zk.getChildren(lockPath, false);
        Collections.sort(children);
        
        // 判断当前节点是否是最小序号节点
        if (currentPath.endsWith(children.get(0))) {
            return true;
        }
        
        // 监控前一个节点
        int previousIndex = Collections.binarySearch(children, 
            currentPath.substring(lockPath.length() + 1)) - 1;
        String previousNode = lockPath + "/" + children.get(previousIndex);
        zk.exists(previousNode, this);
        latch.await();
        return true;
    }
    
    public void unlock() throws Exception {
        zk.delete(currentPath, -1);
        zk.close();
    }
    
    @Override
    public void process(WatchedEvent event) {
        if (event.getType() == Event.EventType.NodeDeleted) {
            latch.countDown();
        }
    }
}

实现精髓:

  • 临时有序节点:自动清理残留锁
  • 序号监控机制:避免无效轮询
  • 等待队列:实现公平锁特性

4. 生死攸关的锁超时机制

4.1 Redis锁的续约策略

// 基于定时任务的自动续期
private ScheduledExecutorService executor = Executors.newScheduledThreadPool(1);

public void renewLease() {
    executor.scheduleAtFixedRate(() -> {
        if (isLockHeld()) {
            jedis.expire(lockKey, 30);
        }
    }, 10, 10, TimeUnit.SECONDS);  // 每10秒续期一次
}

注意要点:

  • 守护线程的生命周期管理
  • 续期间隔应小于超时时间的1/3
  • 突发流量下的自动熔断

4.2 ZK的会话超时控制

通过设置合理的sessionTimeout(建议在20-60秒之间),当客户端失联时,ZooKeeper服务端会自动清理临时节点。但是过短的超时时间会导致误判,过长的则会降低系统响应速度。

5. 重入机制的实现艺术

5.1 Redis的可重入锁改造

// 使用ThreadLocal存储重入次数
private ThreadLocal<Integer> lockCount = new ThreadLocal<>();

public boolean tryReentrantLock() {
    Integer count = lockCount.get();
    if (count != null && count > 0) {
        lockCount.set(count + 1);
        return true;
    }
    
    if (tryLock(30000)) {
        lockCount.set(1);
        return true;
    }
    return false;
}

public void unlock() {
    Integer count = lockCount.get();
    if (count == null) return;
    
    if (--count == 0) {
        doRealUnlock();
        lockCount.remove();
    } else {
        lockCount.set(count);
    }
}

5.2 ZooKeeper的重入实现

通过在节点数据中存储客户端标识和重入次数:

// 节点数据格式:clientId:count
String data = "client_001:3";
byte[] payload = data.getBytes(StandardCharsets.UTF_8);

// 创建节点时携带数据
zk.create(lockPath + "/lock_", 
    payload, 
    ZooDefs.Ids.OPEN_ACL_UNSAFE,
    CreateMode.EPHEMERAL_SEQUENTIAL);

6. 典型应用场景与选型指南

6.1 Redis锁适用场景

  • 高频读写的热点数据保护(如库存扣减)
  • 对性能要求极高的短时操作(<1秒)
  • 允许出现极端情况下少量锁失效的场景

6.2 ZooKeeper锁理想场景

  • 金融交易等强一致性要求的业务
  • 长事务处理(秒级以上的业务操作)
  • 需要公平锁特性的排队系统

7. 技术对比与核心考量

维度 Redis实现 ZooKeeper实现
性能 万级TPS 千级TPS
可靠性 依赖持久化策略 原生强一致性
锁类型 通常实现非公平锁 天然支持公平锁
异常处理 需处理网络分区问题 自动处理会话失效
实现复杂度 中(需处理超时续约) 高(需处理节点监听)

8. 开发者必须知道的七个陷阱

  1. 时钟漂移问题:在redis锁超时场景中,客户端与服务端时间不同步可能导致锁提前释放
  2. GC暂停危机:长时间GC可能导致锁超时失效但业务仍在执行
  3. 脑裂风险:Redis主从切换期间可能出现双锁
  4. 惊群效应:ZooKeeper大规模节点删除时的性能冲击
  5. 连接泄漏:未正确关闭ZooKeeper会话导致资源耗尽
  6. 误删问题:未验证锁持有者身份直接删除key
  7. 链式故障:重试机制不当引发的雪崩效应

9. 总结与最佳实践

通过30次线上事故分析得出的黄金法则:

  • 为每个锁操作记录审计日志
  • 生产环境必须实现锁续期和健康检查
  • 任何解锁操作前必须验证持有者身份
  • 重试策略采用指数退避算法
  • 建立锁监控大盘,统计锁等待时间、锁持有时间等关键指标