3. 分布式锁基础:什么是分布式锁、分布式锁的三大特性、常见实现方式对比
在支付订单的防重处理中,当多个服务实例(或线程)同时尝试对同一笔订单执行支付操作时,必须确保只有一个实例能成功执行,其余请求必须被拒绝或等待。这种跨进程、跨机器的互斥控制,正是分布式锁的核心应用场景。
3.1 什么是分布式锁
分布式锁是一种用于在分布式系统中协调多个节点对共享资源进行互斥访问的机制。与单机锁(如 Java 的 synchronized、ReentrantLock)不同,分布式锁运行在独立的协调组件(如 Redis、ZooKeeper、etcd)之上,能够跨越进程边界和网络边界,确保在任意时刻,只有一个客户端(或线程)持有锁并操作临界资源。
核心类比: 单机锁是“房间内的一把钥匙”,分布式锁是“整栋楼唯一的一把总钥匙,存放在前台(协调组件)”,任何人要使用房间,必须先到前台取钥匙。
3.2 分布式锁的三大特性
一个生产级分布式锁必须同时满足以下三个核心特性,缺一不可。在支付场景中,任何一个特性的缺失都可能导致资金损失或系统瘫痪。
| 特性 | 定义 | 支付场景中的意义 |
|---|---|---|
| 互斥性 | 任意时刻,只有一个客户端能持有锁。 | 确保同一笔订单的支付请求不会被两个服务实例同时处理,避免重复扣款。 |
| 死锁预防 | 持有锁的客户端崩溃或网络异常时,锁能自动释放。 | 防止因服务宕机导致订单被“永久锁定”,影响后续重试或退款流程。 |
| 高可用 | 锁服务本身具备容错能力,不因单点故障而不可用。 | 保证在 Redis 主从切换或集群分区时,锁的获取与释放仍能正确执行,避免支付链路中断。 |
支付避坑: 仅满足互斥性而忽略死锁预防,是支付系统最常见的故障根源。例如,服务 A 获取锁后发生 Full GC 导致长时间停顿,锁超时自动释放,服务 B 获取锁并完成支付,随后服务 A 恢复并继续执行支付——造成重复扣款。必须结合锁超时与唯一标识(如请求 ID)双重校验。
3.3 常见实现方式对比
业界主流的分布式锁实现方案包括基于 Redis、ZooKeeper 和 etcd 三种。下表从多个维度进行对比,帮助你在支付场景中做出技术选型。
| 对比维度 | Redis(SET NX + Lua) | ZooKeeper(临时顺序节点) | etcd(Lease + TTL) |
|---|---|---|---|
| 一致性模型 | AP(最终一致),主从异步复制可能丢锁 | CP(强一致),ZAB 协议保证 | CP(强一致),Raft 协议保证 |
| 性能 | 极高(纯内存操作,QPS 可达 10 万+) | 中等(磁盘写入 + 会话管理,QPS 约数千) | 较高(内存 + 批量提交,QPS 约 1 万+) |
| 死锁预防 | 依赖锁超时(TTL),需合理设置 | 依赖会话超时(Session Expiry),自动释放 | 依赖 Lease 租约,TTL 到期自动释放 |
| 锁重入 | 需借助 ThreadLocal 或 Lua 脚本实现 | 原生不支持,需客户端封装 | 原生不支持,需客户端封装 |
| 运维复杂度 | 低(Redis 集群广泛部署) | 中(需维护 ZK 集群,节点数不宜过多) | 中(需维护 etcd 集群,与云原生生态集成好) |
| 支付场景推荐度 | ★★★★★(首选,配合 Redlock 或看门狗) | ★★★☆☆(适合对一致性要求极高的对账场景) | ★★★★☆(适合云原生支付平台) |
3.4 支付场景选型建议
基于上述对比,在支付订单防重场景中,我们推荐以 Redis 分布式锁 作为首选方案,原因如下:
- 性能优先: 支付链路对延迟极其敏感,Redis 的微秒级操作能最大程度降低锁竞争对用户体验的影响。
- 生态成熟: 业界已有 Redisson、Lettuce 等成熟客户端,内置看门狗(Watchdog)续期机制,有效防止锁超时误释放。
- 运维成本低: 大部分支付团队已部署 Redis 集群,无需额外引入 ZooKeeper 或 etcd 组件。
关键实践: 在支付防重中,不要直接使用
SET NX 裸命令。应使用 Redisson 的 RLock 或自定义 Lua 脚本,将“锁 key = 订单号”、“value = 请求唯一 ID”、“TTL = 业务最大执行时间(如 5 秒)”三者绑定,并在释放锁时通过 Lua 校验 value 是否为自己持有,防止误删他人锁。
// 伪代码:基于 Redis 的防重支付锁
String lockKey = "order:pay:lock:" + orderId;
String requestId = UUID.randomUUID().toString();
boolean locked = redisTemplate.opsForValue()
.setIfAbsent(lockKey, requestId, 5, TimeUnit.SECONDS);
if (locked) {
try {
// 执行支付逻辑
paymentService.process(orderId);
} finally {
// Lua 脚本:原子性校验并释放锁
String luaScript = "if redis.call('get', KEYS[1]) == ARGV[1] then " +
"return redis.call('del', KEYS[1]) else return 0 end";
redisTemplate.execute(new DefaultRedisScript<>(luaScript, Long.class),
Collections.singletonList(lockKey), requestId);
}
} else {
// 获取锁失败,说明已有请求在处理
throw new DuplicatePaymentException("订单正在支付中,请勿重复操作");
}