1. 分布式锁的本质与核心挑战
分布式锁是分布式系统中协调多节点并发访问共享资源的基石机制。想象一下,当多个微服务实例同时竞争修改数据库中的同一条记录时,如果没有锁机制,就像十字路口没有红绿灯——必然导致数据混乱。但实现一个可靠的分布式锁远比单机环境复杂得多。
在单机系统中,我们习惯用synchronized或ReentrantLock这类本地锁,它们依赖JVM内存中的锁状态实现互斥。但在分布式环境下,这种方案立即失效——不同服务实例运行在独立的进程空间,无法感知彼此的锁状态。这就是为什么我们需要引入外部协调服务(如Redis、ZooKeeper)来实现跨进程的锁同步。
分布式锁必须解决的三大核心挑战:
- 互斥性:任何时候只能有一个客户端持有锁
- 防死锁:持有锁的客户端崩溃后,锁必须能被释放
- 容错性:即使部分节点故障,锁服务仍能正常工作
以电商系统中的库存扣减为例:
// 错误示范:本地锁在分布式环境下失效 public synchronized void reduceStock(Long itemId) { Item item = itemMapper.selectById(itemId); if (item.getStock() > 0) { item.setStock(item.getStock() - 1); itemMapper.updateById(item); } }这个看似安全的同步方法,在分布式部署时会引发超卖——因为每个实例都有自己的锁副本。正确的做法是引入分布式锁:
public void reduceStock(Long itemId) { String lockKey = "stock_lock:" + itemId; try { // 尝试获取分布式锁 boolean locked = redisLock.tryLock(lockKey, 10, TimeUnit.SECONDS); if (locked) { Item item = itemMapper.selectById(itemId); if (item.getStock() > 0) { item.setStock(item.getStock() - 1); itemMapper.updateById(item); } } } finally { redisLock.unlock(lockKey); } }2. 基于Redis的分布式锁实现方案
Redis因其高性能和丰富的数据结构,成为实现分布式锁的首选方案之一。但看似简单的SETNX命令背后,藏着许多魔鬼细节。
2.1 基础实现与致命缺陷
最朴素的Redis锁实现是这样的:
SETNX lock_key 1 # 尝试获取锁 DEL lock_key # 释放锁这个方案存在两个致命问题:
- 客户端崩溃导致死锁:如果获取锁后客户端宕机,锁永远无法释放
- 误删其他客户端的锁:客户端A的锁可能被客户端B删除
2.2 改进方案:带过期时间的唯一值锁
成熟的Redis锁实现需要三个关键改进:
- 为锁设置过期时间,避免死锁
- 每个客户端使用唯一标识(如UUID),避免误删
- 使用Lua脚本保证原子性
完整实现流程:
# 加锁 SET lock_key unique_value NX PX 30000 # 解锁(Lua脚本) if redis.call("get",KEYS[1]) == ARGV[1] then return redis.call("del",KEYS[1]) else return 0 endJava代码示例:
public class RedisDistributedLock { private Jedis jedis; private String lockKey; private String lockValue; private long expireTime; public boolean tryLock(long waitTime, TimeUnit unit) { lockValue = UUID.randomUUID().toString(); long end = System.currentTimeMillis() + unit.toMillis(waitTime); while (System.currentTimeMillis() < end) { String result = jedis.set(lockKey, lockValue, "NX", "PX", expireTime); if ("OK".equals(result)) { return true; } try { Thread.sleep(100); } catch (InterruptedException e) { Thread.currentThread().interrupt(); } } return false; } public void unlock() { String script = "if redis.call('get', KEYS[1]) == ARGV[1] then " + "return redis.call('del', KEYS[1]) " + "else return 0 end"; jedis.eval(script, Collections.singletonList(lockKey), Collections.singletonList(lockValue)); } }2.3 锁续期与红锁算法
简单的Redis锁还存在时钟漂移、主从切换等问题。生产环境还需要考虑:
- 锁续期:对于长时间操作,需要定期延长锁过期时间
- 红锁(RedLock):跨多个独立Redis节点实现更可靠的锁
锁续期示例:
private ScheduledExecutorService scheduler = Executors.newScheduledThreadPool(1); private ScheduledFuture<?> renewalTask; public void startRenewal() { renewalTask = scheduler.scheduleAtFixedRate(() -> { if (isLocked()) { jedis.expire(lockKey, (int)TimeUnit.MILLISECONDS.toSeconds(expireTime)); } }, expireTime / 3, expireTime / 3, TimeUnit.MILLISECONDS); } public void stopRenewal() { if (renewalTask != null) { renewalTask.cancel(true); } }3. ZooKeeper分布式锁的实现机制
与Redis的CP特性不同,ZooKeeper作为专门设计的协调服务,提供了更严谨的锁实现基础。
3.1 临时顺序节点原理
ZooKeeper锁的核心是利用:
- 临时节点(EPHEMERAL):客户端断开连接自动删除
- 顺序节点(SEQUENTIAL):节点名自动追加单调递增序号
加锁流程:
- 在锁目录下创建临时顺序节点(如/lock/lock_00000001)
- 获取目录下所有子节点,检查自己是否是最小序号
- 如果是,获取锁成功;否则监听前一个序号的节点
- 前一个节点删除时(锁释放),重新检查序号
public class ZkDistributedLock { private ZooKeeper zk; private String lockPath; private String currentPath; public void lock() throws Exception { currentPath = zk.create(lockPath + "/lock_", new byte[0], ZooDefs.Ids.OPEN_ACL_UNSAFE, CreateMode.EPHEMERAL_SEQUENTIAL); while (true) { List<String> children = zk.getChildren(lockPath, false); Collections.sort(children); if (currentPath.equals(lockPath + "/" + children.get(0))) { return; // 获取锁成功 } CountDownLatch latch = new CountDownLatch(1); Stat stat = zk.exists(lockPath + "/" + children.get( Collections.binarySearch(children, currentPath.substring(currentPath.lastIndexOf('/') + 1)) - 1), event -> { if (event.getType() == EventType.NodeDeleted) { latch.countDown(); } }); if (stat != null) { latch.await(); } } } public void unlock() throws Exception { zk.delete(currentPath, -1); } }3.2 对比Redis锁的优劣势
ZooKeeper优势:
- 自动处理锁释放(临时节点特性)
- 严格的顺序获取,避免惊群效应
- 原生支持读写锁等复杂场景
Redis优势:
- 性能更高(毫秒级响应 vs ZooKeeper的百毫秒级)
- 实现更简单,运维成本低
- 社区支持更丰富
选择建议:对一致性要求极高的场景(如金融交易)用ZooKeeper,高并发场景(如秒杀)用Redis
4. 分布式锁的典型陷阱与最佳实践
即使理解了原理,在实际应用中仍会遇到各种意外情况。以下是笔者在多个生产系统中总结的经验。
4.1 锁粒度的选择误区
错误案例:整个库存系统使用同一个锁
// 锁粒度过粗,导致性能瓶颈 public void updateStock(Long itemId) { String lockKey = "global_stock_lock"; // ... }正确做法:按数据ID分片加锁
public void updateStock(Long itemId) { String lockKey = "item_stock_lock:" + itemId; // ... }4.2 锁超时时间的艺术
设置锁超时需要权衡:
- 过短:业务未完成锁就失效,导致并发问题
- 过长:客户端崩溃后其他客户端等待时间过长
经验公式:
锁超时时间 = 平均业务执行时间 × 3 + 网络延迟缓冲对于波动大的业务,建议实现动态续期机制(如前文所示)。
4.3 锁重入问题
本地锁通常支持重入(如ReentrantLock),但分布式锁需要额外处理:
public class RedisReentrantLock { private ThreadLocal<Map<String, Integer>> lockCount = ThreadLocal.withInitial(HashMap::new); public boolean tryLock(String key) { Map<String, Integer> counts = lockCount.get(); Integer count = counts.get(key); if (count != null) { counts.put(key, count + 1); return true; } boolean success = redisLock.tryLock(key); if (success) { counts.put(key, 1); } return success; } public void unlock(String key) { Map<String, Integer> counts = lockCount.get(); Integer count = counts.get(key); if (count == null) return; if (count > 1) { counts.put(key, count - 1); } else { counts.remove(key); redisLock.unlock(key); } } }4.4 分布式锁的性能优化
锁分段:将热点资源拆分为多个槽,减少竞争
// 将商品库存锁分为16个段 String lockKey = "item_stock_lock:" + itemId % 16;乐观锁替代:对于冲突少的场景,可以用版本号机制
UPDATE items SET stock = stock - 1, version = version + 1 WHERE id = ? AND version = ?本地缓存+定期同步:非强一致性要求的场景可降低锁频率
在实际项目中,我曾遇到一个典型案例:某促销系统使用分布式锁保护库存,QPS始终上不去。通过将锁粒度从商品维度调整为库存分片维度(每100件商品一个分片),同时引入本地库存缓存(每10秒同步一次),最终将系统吞吐量提升了17倍。