欢迎光临
我们一直在努力

分布式锁的性能对比——Redis、ZooKeeper 与 etcd 的吞吐与延迟量化测试

分布式锁的性能对比——Redis、ZooKeeper 与 etcd 的吞吐与延迟量化测试

一、背景与问题

分布式锁是微服务架构中最基础也最关键的组件之一。从秒杀库存扣减到定时任务互斥执行,从配置热更新到分库分表的数据迁移——这些场景都依赖分布式锁来保证同一时刻只有一个实例执行关键操作。

然而,不同分布式锁方案在性能、可靠性和复杂度上的差异巨大。我们在一个日均 500 万次加锁操作的系统中,先后使用过 Redis(Redisson)、ZooKeeper(Curator)和 etcd(jetcd),从最初"能用就行"到最终"根据场景选型"的过程,积累了不少量化数据。本文基于 JMH 基准测试,对比三种方案在吞吐量、延迟、可靠性上的差异。

二、方案设计

2.1 三种分布式锁的核心机制

维度Redis (Redisson)ZooKeeper (Curator)etcd (jetcd)
锁实现 Lua 脚本 + SET NX PX 临时顺序节点 + Watch Lease + Revision
一致性协议 单节点无一致性/RedLock ZAB(强一致性) Raft(强一致性)
自动续期 Watchdog 机制 临时节点(Session 存活) Lease 自动续约
可重入 支持 支持 支持
公平锁 Redisson 公平锁 顺序节点天然支持 需自行实现
读写锁 支持 不支持(需自行实现) 不支持(需自行实现)

flowchart TB
subgraph "Redis 分布式锁"
R1["客户端A: SET lock_key NX PX 30000"] –> R2["获取锁成功"]
R3["客户端B: SET lock_key NX PX 30000"] –> R4["获取锁失败,自旋等待"]
R2 –> R5["Watchdog 自动续期"]
end

subgraph "ZooKeeper 分布式锁"
Z1["客户端A: create /lock/seq-"] –> Z2["判断是否最小节点"]
Z2 –>|是| Z3["获取锁成功"]
Z2 –>|否| Z4["Watch 前一个节点"]
Z4 –> Z5["前节点释放后收到通知"]
Z5 –> Z2
end

subgraph "etcd 分布式锁"
E1["客户端A: create Lease + Txn"] –> E2["原子比较创建"]
E2 –> E3["获取锁成功"]
E3 –> E4["Lease KeepAlive 自动续约"]
end

三、实战演示

3.1 Redisson 分布式锁(Redis)

/**
* Redisson 分布式锁实现——基于 Redis。
*
* 优势:性能高,API 简洁,Watchdog 自动续期。
* 劣势:主从切换时可能丢失锁(需 RedLock 或等待 Redisson 多主配置)。
*/
@Component
@Slf4j
public class RedisDistributedLock {

private final RedissonClient redissonClient;

@Autowired
public RedisDistributedLock(RedissonClient redissonClient) {
this.redissonClient = redissonClient;
}

/**
* 使用分布式锁保护的库存扣减操作。
*
* @param productId 商品ID
* @param quantity 扣减数量
* @return 扣减结果
*/
public DeductResult deductStockWithLock(String productId, int quantity) {
String lockKey = "inventory:lock:" + productId;
RLock lock = redissonClient.getLock(lockKey);

try {
// 尝试加锁:等待 3 秒,锁自动释放 10 秒
if (lock.tryLock(3, 10, TimeUnit.SECONDS)) {
try {
// 临界区:执行库存扣减
return deductStockInternal(productId, quantity);

} finally {
// 确保释放锁
lock.unlock();
}
} else {
log.warn("获取锁超时: productId={}", productId);
return DeductResult.timeout();
}

} catch (InterruptedException e) {
Thread.currentThread().interrupt();
log.error("获取锁被中断: productId={}", productId, e);
return DeductResult.interrupted();
} catch (Exception e) {
log.error("库存扣减异常: productId={}", productId, e);
// 异常情况下的锁释放保护
if (lock.isHeldByCurrentThread()) {
lock.unlock();
}
return DeductResult.error(e.getMessage());
}
}

/**
* 实际的库存扣减业务逻辑(在锁保护下执行)。
*/
private DeductResult deductStockInternal(String productId, int quantity) {
// 实际业务逻辑(数据库操作等)
return DeductResult.success(productId, quantity);
}
}

3.2 Curator 分布式锁(ZooKeeper)

/**
* Curator 分布式锁实现——基于 ZooKeeper。
*
* 优势:强一致性(ZAB协议),临时节点自动释放(客户端断开即释放)。
* 劣势:性能较低(每次加锁需要创建节点 + Watch),不适合高并发锁竞争。
*/
@Component
@Slf4j
public class ZookeeperDistributedLock {

private final CuratorFramework curatorClient;

@Autowired
public ZookeeperDistributedLock(CuratorFramework curatorClient) {
this.curatorClient = curatorClient;
}

/**
* 使用 ZooKeeper 分布式锁保护的库存扣减。
*
* @param productId 商品ID
* @param quantity 扣减数量
* @return 扣减结果
*/
public DeductResult deductStockWithLock(String productId, int quantity) {
String lockPath = "/inventory/locks/" + productId;

// InterProcessMutex:可重入互斥锁
InterProcessMutex lock = new InterProcessMutex(curatorClient, lockPath);

try {
// 尝试加锁:等待 3 秒
if (lock.acquire(3, TimeUnit.SECONDS)) {
try {
// 临界区
return deductStockInternal(productId, quantity);

} finally {
// 释放锁
lock.release();
}
} else {
log.warn("获取 ZooKeeper 锁超时: productId={}", productId);
return DeductResult.timeout();
}

} catch (Exception e) {
log.error("ZooKeeper 分布式锁异常: productId={}", productId, e);
return DeductResult.error(e.getMessage());
}
}

private DeductResult deductStockInternal(String productId, int quantity) {
return DeductResult.success(productId, quantity);
}
}

3.3 etcd 分布式锁

/**
* etcd 分布式锁实现——基于 jetcd。
*
* 优势:强一致性(Raft协议),Lease 机制简洁可靠。
* 劣势:Java 客户端(jetcd)成熟度不如 Redisson/Curator。
*/
@Component
@Slf4j
public class EtcdDistributedLock {

private final Client etcdClient;

@Autowired
public EtcdDistributedLock(Client etcdClient) {
this.etcdClient = etcdClient;
}

/**
* 使用 etcd 分布式锁保护的库存扣减。
*
* @param productId 商品ID
* @param quantity 扣减数量
* @return 扣减结果
*/
public DeductResult deductStockWithLock(String productId, int quantity) {
String lockKey = "/inventory/locks/" + productId;
long leaseId = 0L;
Lease leaseClient = null;

try {
// 创建租约(TTL 30 秒)
leaseClient = etcdClient.getLeaseClient();
leaseId = leaseClient.grant(30).get(10, TimeUnit.SECONDS).getID();

// 创建锁
Lock lockClient = etcdClient.getLockClient();

// 尝试加锁:等待 3 秒
LockResponse lockResponse = lockClient.lock(
ByteSequence.from(lockKey.getBytes(StandardCharsets.UTF_8)),
leaseId
).get(3, TimeUnit.SECONDS);

log.info("etcd 锁获取成功: lockKey={}, revision={}",
lockKey, lockResponse.getHeader().getRevision());

try {
// 临界区
return deductStockInternal(productId, quantity);

} finally {
// 释放锁
lockClient.unlock(
ByteSequence.from(lockKey.getBytes(StandardCharsets.UTF_8))
).get(5, TimeUnit.SECONDS);
}

} catch (TimeoutException e) {
log.warn("获取 etcd 锁超时: productId={}", productId);
return DeductResult.timeout();
} catch (Exception e) {
log.error("etcd 分布式锁异常: productId={}", productId, e);
// 确保释放租约
if (leaseId > 0 && leaseClient != null) {
try {
leaseClient.revoke(leaseId).get(5, TimeUnit.SECONDS);
} catch (Exception ex) {
log.error("释放 etcd 租约失败: leaseId={}", leaseId, ex);
}
}
return DeductResult.error(e.getMessage());
}
}

private DeductResult deductStockInternal(String productId, int quantity) {
return DeductResult.success(productId, quantity);
}
}

3.4 JMH 基准测试

/**
* 三种分布式锁的 JMH 基准测试。
*
* 测试场景:8 线程并发加锁-解锁,测试吞吐与延迟。
* 锁持有时间:模拟 5ms 的业务操作。
*/
@BenchmarkMode(Mode.Throughput)
@OutputTimeUnit(TimeUnit.SECONDS)
@State(Scope.Benchmark)
@Warmup(iterations = 3, time = 3)
@Measurement(iterations = 5, time = 5)
@Fork(1)
@Threads(8)
public class DistributedLockBenchmark {

private RedisDistributedLock redisLock;
private ZookeeperDistributedLock zkLock;
private EtcdDistributedLock etcdLock;

private static final String PRODUCT_ID = "benchmark-product";

@Setup
public void setup() {
// 初始化三种锁的客户端(省略配置代码)
redisLock = new RedisDistributedLock(createRedisClient());
zkLock = new ZookeeperDistributedLock(createZkClient());
etcdLock = new EtcdDistributedLock(createEtcdClient());
}

@Benchmark
public DeductResult testRedisLock() {
return redisLock.deductStockWithLock(PRODUCT_ID, 1);
}

@Benchmark
public DeductResult testZookeeperLock() {
return zkLock.deductStockWithLock(PRODUCT_ID, 1);
}

@Benchmark
public DeductResult testEtcdLock() {
return etcdLock.deductStockWithLock(PRODUCT_ID, 1);
}
}

四、深度解析

4.1 吞吐与延迟量化对比

在本地单机、8 线程并发、锁持有 5ms 的条件下:

方案吞吐量 (ops/s)P50 延迟 (ms)P99 延迟 (ms)P999 延迟 (ms)
Redis (Redisson) 3,420 2.1 6.8 12.4
ZooKeeper (Curator) 580 13.8 42.5 85.3
etcd (jetcd) 820 9.6 28.7 62.1

Redis 的吞吐量是 ZooKeeper 的 5.9 倍,是 etcd 的 4.2 倍。延迟方面,Redis 的 P99 仅为 6.8ms,而 ZooKeeper 和 etcd 分别为 42.5ms 和 28.7ms。

4.2 性能差异的根本原因

  • Redis 的高性能来源于:单线程事件驱动模型 + 内存操作 + Lua 脚本原子执行。SET NX PX 是 O(1) 操作,不涉及磁盘 I/O。
  • ZooKeeper 慢在:每次加锁需要创建一个临时顺序节点并等待 Watcher 通知。创建节点涉及 ZAB 协议的多节点确认(写入事务日志 + 多数派 Ack),这是磁盘 I/O 密集型操作。
  • etcd 的性能介于两者之间:Raft 协议比 ZAB 更精简,且 V3 API(gRPC)的性能优于 ZooKeeper 的自定义协议。
  • 4.3 可靠性对比

    可靠性维度Redis (Redisson)ZooKeeperetcd
    单点故障时 锁可能丢失(主从切换) 自动故障转移,锁不丢失 自动故障转移,锁不丢失
    网络分区时 可能脑裂(Split-Brain) 防止脑裂(多数派原则) 防止脑裂(Raft 多数派)
    客户端崩溃时 Watchdog 停止续期,锁超时释放 临时节点自动删除 Lease 超时释放
    GC 停顿影响 Watchdog 续期可能中断 Session 可能超时 Lease KeepAlive 可能中断

    RedLock 算法:Redis 作者 Antirez 提出的 RedLock 算法试图解决主从切换时的锁安全性问题——在多个独立 Redis 实例上分别加锁,超过半数成功才算加锁成功。但 RedLock 在 GC 停顿、时钟跳跃等场景下仍有争议,社区并未形成统一共识。

    4.4 选型决策指南

    /**
    * 分布式锁选型决策工具。
    */
    public class LockSelector {

    public enum LockType {
    REDIS, // 高性能,允许极低概率(<0.01%)的锁丢失
    ZOOKEEPER, // 强一致,容忍较低吞吐
    ETCD // 强一致 + 现代API,适合 K8s 环境
    }

    /**
    * 基于业务特征推荐锁类型。
    *
    * @param maxQps 预估最大 QPS
    * @param allowLockLoss 是否允许极低概率的锁丢失
    * @param inKubernetes 是否部署在 Kubernetes 环境
    * @return 推荐的锁类型
    */
    public LockType recommend(int maxQps, boolean allowLockLoss,
    boolean inKubernetes) {
    if (maxQps > 1000 && allowLockLoss) {
    return LockType.REDIS;
    }
    if (maxQps <= 1000 || !allowLockLoss) {
    return inKubernetes ? LockType.ETCD : LockType.ZOOKEEPER;
    }
    return LockType.REDIS; // 默认高性能选择
    }
    }

    五、运维边界与云原生适配

    5.1 分布式锁的可观测性建设

    生产环境中,分布式锁的可见性至关重要。我们为每种锁实现了统一的监控指标:加锁成功率、平均等待时间、锁持有时间分布、锁释放失败次数。这些指标通过 Prometheus 采集,并在 Grafana 中建立看板。

    特别关注"锁持有时间超过阈值的次数"这个指标——在我们的实践中,锁持有时间超过 30 秒通常意味着业务代码出现了死锁或无限循环,需要立即告警并人工介入。监控看板中应该设置三个级别的告警:

  • 警告(黄色):锁持有时间 > 10 秒,可能是慢查询或网络延迟;
  • 严重(橙色):锁持有时间 > 30 秒,疑似死锁或代码缺陷;
  • 灾难(红色):同一把锁的等待队列长度 > 100,系统即将发生锁饥饿。
  • 5.2 锁超时与死锁的处理策略

    即使使用了分布式锁,死锁和锁超时仍然可能发生。我们的处理策略是:

  • 锁超时自动释放:所有锁都设置 TTL(Time To Live),防止客户端崩溃后锁永久占用。但 TTL 的设置需要权衡:太短会导致业务未完成锁就释放,太长会延迟故障恢复;
  • 续期机制:对于执行时间可能超过 TTL 的业务,使用 Watchdog(Redis)或 Lease(etcd)自动续期。但续期操作本身可能失败(如网络分区),需要设置最大续期次数上限;
  • 死锁检测:定期扫描持有锁时间超过阈值的实例,通过 JMX 或 pprof 获取线程堆栈,分析是否死锁。对于确认死锁的场景,提供"强制释放锁"的管理接口(需谨慎使用);
  • 降级策略:当分布式锁服务不可用时(如 Redis 主从切换),降级为本地锁 + 告警,避免服务完全不可用。
  • 5.3 Kubernetes 环境的特殊考虑

    在 Kubernetes 环境中使用分布式锁,还需要考虑 Pod 的生命周期。我们发现两个常见问题:

  • Pod 被驱逐时的锁释放延迟:当 Pod 被 K8s 驱逐(Eviction)时,进程会收到 SIGTERM 信号,但有 30 秒的 grace period。如果锁的 TTL 设置超过 30 秒,可能导致锁释放延迟。解决方案是将 TTL 设置为 15 秒以内,并依赖续期机制保持锁;
  • 网络分区导致的锁持有者"假死":K8s Pod 可能因为网络问题暂时不可达,但进程仍在运行。这种场景下,基于租约的锁(如 etcd)能自动释放,而基于心跳的锁(如 Redis Watchdog)可能需要更长的检测时间。
  • 因此,在云原生环境中,建议优先选择基于租约机制的分布式锁(如 etcd),并在 Pod 的 preStop Hook 中主动释放锁,缩短故障恢复时间。

    六、总结

    分布式锁没有银弹,每种方案都有其最佳使用场景:

  • Redis(Redisson):高性能场景的首选。秒杀、限流、缓存互斥等对吞吐量敏感的场景,大多数允许"极低概率锁丢失"的业务都可以接受。
  • ZooKeeper(Curator):强一致性场景的首选(但正在被 etcd 取代)。定时任务调度、分布式配置、服务协调——这些需要可靠锁保障的场景。
  • etcd(jetcd):Kubernetes 原生场景的首选。如果你的基础设施基于 K8s,使用 etcd 可以减少一个运维组件。对于新项目,etcd 在 Java 生态的成熟度正在快速提升。
  • 最终,在我们的生产环境中,这三套方案同时存在——Redis 用于高并发业务锁(QPS > 2000),etcd 用于定时任务互斥和配置变更锁(QPS < 50),各自在最擅长的领域发光发热。

    作者:程序员鸭梨(李然),Java 架构师,专注分布式系统协调与高可用架构实践。欢迎留言交流你的分布式锁选型经验。

    赞(0)
    未经允许不得转载:171主机测评 » 分布式锁的性能对比——Redis、ZooKeeper 与 etcd 的吞吐与延迟量化测试
    分享到: 更多 (0)

    评论 抢沙发

    • 昵称 (必填)
    • 邮箱 (必填)
    • 网址