1. 分布式锁的本质与核心挑战
在分布式系统中,多个服务实例对共享资源进行互斥访问时,传统的单机锁机制会完全失效。这就是分布式锁要解决的核心问题——在跨进程、跨主机的环境下实现排他性控制。我经历过一个典型的电商库存扣减场景:当100台订单服务器同时检测到某商品剩余库存为1时,如果没有分布式锁的防护,必然会出现超卖现象。
分布式锁必须满足三个刚性条件:
- 互斥性:任意时刻只有一个客户端能持有锁
- 防死锁:即使锁持有者崩溃,锁也能自动释放
- 容错性:只要大部分锁服务节点存活,客户端就能正常获取和释放锁
在实际工程中,我们还需要额外考虑:
- 锁的自动续期(避免业务未处理完锁已过期)
- 可重入性(同一线程可重复获取锁)
- 锁等待超时(避免线程长时间阻塞)
- 锁释放校验(防止误删其他客户端的锁)
2. Redis分布式锁深度实现
2.1 基础实现方案
最基础的Redis锁实现只需要一个SETNX命令:
SETNX lock_key unique_value当返回1表示获取锁成功,0则表示失败。但这种方式存在致命缺陷——如果客户端崩溃,锁将永远无法释放。因此必须给锁设置过期时间:
SET lock_key unique_value NX PX 30000这个原子命令在Redis 2.6.12后支持,解决了锁的自动释放问题。
关键细节:unique_value必须是全局唯一标识(如UUID),这是实现锁释放安全校验的基础。
2.2 锁续期与可重入实现
对于可能长时间持有锁的场景,需要实现锁的自动续期。我推荐使用Redisson的看门狗机制:
Config config = new Config(); config.useSingleServer().setAddress("redis://127.0.0.1:6379"); RedissonClient redisson = Redisson.create(config); RLock lock = redisson.getLock("orderLock"); try { // 看门狗默认30秒过期,每10秒续期一次 lock.lock(); // 业务处理 } finally { lock.unlock(); }可重入锁的实现需要记录持有线程和重入次数。Redisson通过Lua脚本维护了Hash结构:
local counter = redis.call('hincrby', KEYS[1], ARGV[1], 1); redis.call('pexpire', KEYS[1], ARGV[2]); return counter;2.3 集群环境下的特殊考量
在Redis Cluster模式下,我们需要特别注意:
- 节点故障时可能出现锁状态不一致
- 网络分区可能导致多个客户端同时持有锁
RedLock算法试图解决这些问题,其核心流程:
- 获取当前毫秒级时间戳T1
- 依次向N个独立节点请求锁(使用相同key和value)
- 当在(N/2 + 1)个节点上获取成功,且总耗时小于锁有效期时,认为获取成功
- 锁的实际有效时间 = 初始有效时间 - 获取锁总耗时
但RedLock也存在争议,Martin Kleppmann曾指出其可能违反互斥性。我的实践经验是:
- 对一致性要求极高的场景慎用
- 适当增加重试次数和超时时间
- 配合业务层的幂等设计
3. ZooKeeper分布式锁专业实现
3.1 基于临时顺序节点的锁
ZooKeeper通过临时顺序节点实现分布式锁更加严谨:
- 每个客户端在锁目录下创建临时顺序节点
- 获取目录下所有子节点,判断自己是否是最小编号
- 如果是则获取锁;否则监听前一个节点的删除事件
- 处理完业务后主动删除节点
public class ZkDistributedLock { private final String lockPath; private final ZooKeeper zk; private String currentPath; public boolean lock() throws Exception { currentPath = zk.create(lockPath + "/lock-", null, 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; } else { String prevPath = lockPath + "/" + children.get( Collections.binarySearch(children, currentPath.substring(currentPath.lastIndexOf('/') + 1)) - 1); final CountDownLatch latch = new CountDownLatch(1); Stat stat = zk.exists(prevPath, event -> { if (event.getType() == EventType.NodeDeleted) { latch.countDown(); } }); if (stat != null) { latch.await(); } return true; } } }3.2 锁的特性对比
| 特性 | Redis锁 | ZooKeeper锁 |
|---|---|---|
| 实现复杂度 | 简单 | 较复杂 |
| 性能 | 高(内存操作) | 中(需要创建节点) |
| 可靠性 | 依赖Redis持久化 | 高(ZAB协议保证) |
| 锁释放机制 | 依赖超时 | 会话结束自动释放 |
| 惊群效应 | 存在 | 无(顺序监听) |
| 适用场景 | 高频短时锁 | 低频长时锁 |
3.3 生产环境调优建议
对于ZooKeeper锁的优化:
- 设置合理的sessionTimeout(建议10-30秒)
- 使用Curator框架的InterProcessMutex,它已经处理了连接丢失、重试等边界情况
- 监控znode数量,避免历史节点堆积
- 对于读写锁场景,使用InterProcessReadWriteLock
CuratorFramework client = CuratorFrameworkFactory.newClient( "zk.example.com:2181", new ExponentialBackoffRetry(1000, 3)); client.start(); InterProcessMutex lock = new InterProcessMutex(client, "/order-lock"); try { if (lock.acquire(10, TimeUnit.SECONDS)) { // 业务处理 } } finally { lock.release(); }4. 关键问题与实战经验
4.1 时钟漂移问题
在Redis锁实现中,严重依赖系统时钟。我曾遇到过一个案例:某台服务器时钟快了10分钟,导致锁提前过期。解决方案:
- 使用NTP服务同步时钟
- 在Redis配置中设置
timeout 0禁用命令超时 - 适当增加锁的有效期冗余
4.2 锁等待优化
当大量线程争抢锁时,会导致Redis或ZK压力过大。我采用的优化策略:
- 实现分级锁(全局锁+局部锁)
- 加入随机等待时间(jitter)
- 对于非关键路径使用乐观锁
// 分级锁示例 public void processOrder(String orderId) { // 全局锁控制库存扣减 RLock globalLock = redisson.getLock("global:order:" + orderId); // 局部锁控制订单状态修改 RLock localLock = redisson.getLock("local:order:" + orderId); try { globalLock.lock(); // 扣减库存 localLock.lock(); // 修改订单状态 } finally { localLock.unlock(); globalLock.unlock(); } }4.3 监控与告警
完善的监控体系应包括:
- 锁等待时间(超过100ms需要告警)
- 锁持有时间(异常长时间持有可能是死锁)
- 锁获取失败率(突增可能预示系统问题)
- ZK的znode数量监控
我常用的Prometheus监控指标:
# Redis锁监控 redis_lock_wait_seconds{name="order_lock"} 0.3 redis_lock_held_seconds{name="order_lock"} 1.5 redis_lock_fail_total{name="order_lock"} 42 # ZK锁监控 zk_lock_wait_count{name="payment_lock"} 128 zk_lock_acquire_errors{name="payment_lock"} 75. 技术选型决策树
根据我的经验,选择分布式锁方案时可参考以下决策流程:
是否需要强一致性?
- 是 → 选择ZooKeeper
- 否 → 进入下一步
性能要求是否极高(>5000 TPS)?
- 是 → 选择Redis
- 否 → 进入下一步
是否已有Redis/ZK基础设施?
- 有Redis → 优先考虑Redis锁
- 有ZK → 优先考虑ZK锁
锁持有时间通常多长?
- <1秒 → Redis
1秒 → ZooKeeper
对于金融支付等关键系统,我建议采用ZK锁+业务幂等的双重保障。而在电商秒杀等场景,Redis锁的高性能优势更加明显。