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. 开发者必须知道的七个陷阱
- 时钟漂移问题:在redis锁超时场景中,客户端与服务端时间不同步可能导致锁提前释放
- GC暂停危机:长时间GC可能导致锁超时失效但业务仍在执行
- 脑裂风险:Redis主从切换期间可能出现双锁
- 惊群效应:ZooKeeper大规模节点删除时的性能冲击
- 连接泄漏:未正确关闭ZooKeeper会话导致资源耗尽
- 误删问题:未验证锁持有者身份直接删除key
- 链式故障:重试机制不当引发的雪崩效应
9. 总结与最佳实践
通过30次线上事故分析得出的黄金法则:
- 为每个锁操作记录审计日志
- 生产环境必须实现锁续期和健康检查
- 任何解锁操作前必须验证持有者身份
- 重试策略采用指数退避算法
- 建立锁监控大盘,统计锁等待时间、锁持有时间等关键指标
评论