RedisNotes

第 11 章:分布式锁

zjc 于 2026-01-11 发布

这是《Redis 零基础实战指南》的独立章节版。本章从概念、实操和生产排查三个视角展开,代码块保留了原书可直接运行的版本。 分布式锁是 Redis 最常用也最容易出错的能力。本章从基础 SET NX 到 Redisson,再到 Redlock、故障切换与业务幂等,把正确性边界讲清楚。

11.1 为什么需要分布式锁

多个服务实例同时修改同一资源:

实例A: 读库存=1
实例B: 读库存=1
实例A: 扣减成功
实例B: 扣减成功 -> 超卖

单机锁只能保护一个 JVM,分布式锁让多个进程互斥:

实例A -> 尝试 SET lock NX 成功 -> 进入临界区
实例B -> SET 失败 -> 等待/返回
实例A -> 释放 -> 实例B 可进入

11.2 最基础的正确写法

SET lock:order:1001 owner-uuid NX PX 30000

必须满足:

  1. NX:不存在才设置,保证互斥;
  2. PX:设置过期,防止持有者崩溃导致死锁;
  3. value 唯一:标识持有者,释放时只能释放自己的锁。

错误写法:

SETNX lock:order value
EXPIRE lock:order 30

两步之间进程崩溃,锁可能永远存在。

11.3 释放锁:不能直接 DEL

直接 DEL 可能删掉别人的锁:

A 获取锁 -> GC/网络延迟超过 TTL
锁过期 -> B 获取锁
A 恢复 -> DEL lock -> 删除了 B 的锁
C 又能获取 -> B/C 同时进入

正确释放必须校验 value:

if redis.call('GET', KEYS[1]) == ARGV[1] then
    return redis.call('DEL', KEYS[1])
else
    return 0
end

Java 实现:

String token = UUID.randomUUID().toString();
Boolean ok = redis.opsForValue()
        .setIfAbsent(key, token, Duration.ofSeconds(30));

if (Boolean.TRUE.equals(ok)) {
    try {
        doBusiness();
    } finally {
        release(key, token);
    }
}

11.4 锁超时与续期

TTL 是保险丝,不是业务时长。设置过短会“锁过期但业务还在执行”;过长会导致持有者崩溃后长时间不可用。

方案:

  1. 预估业务最大耗时,TTL = 最大耗时 + 缓冲;
  2. 使用 Redisson 看门狗自动续期;
  3. 记录锁获取、释放、业务耗时指标;
  4. 关键流程设置总超时。

看门狗只能解决进程活着但执行慢的情况,不能解决代码死循环,因此要配合最大执行时间。

11.5 Redisson 锁实现要点

RLock lock = redisson.getLock("lock:order:" + orderId);

if (!lock.tryLock(100, TimeUnit.MILLISECONDS)) {
    return Result.busy();
}

try {
    payOrder(orderId);
} finally {
    if (lock.isHeldByCurrentThread()) {
        lock.unlock();
    }
}

Redisson 底层:

11.6 锁粒度设计

场景 建议 key
订单支付 lock:order:{orderId}
用户资料更新 lock:user:{userId}
SKU 库存扣减 常直接 Lua,不一定加锁
全局配置刷新 lock:config:global
定时任务防重 lock:job:{jobName}:{yyyyMMddHHmm}

原则:

11.7 主从切换下的锁风险

常见异步复制流程:

1. 客户端A 在 Master 获取锁
2. Master 崩溃,锁还没同步到 Replica
3. Replica 提升为新 Master
4. 客户端B 在新 Master 获取同一把锁
5. A/B 同时持有

这不是代码 bug,而是异步系统的一致性边界。可能的应对:

  1. 业务幂等:即使锁失效,重复执行也不造成资金/库存错误;
  2. 数据库乐观锁/唯一约束:把最终正确性放在事实源;
  3. 等待复制:降低丢失窗口,但牺牲可用性;
  4. 强一致协调服务:etcd/ZooKeeper/Consul,代价是吞吐与运维复杂度;
  5. Redlock:多实例多数派,争议较多,需谨慎评估。

生产系统最重要的是 1 和 2:锁用于减少冲突,不承担最终正确性。

11.8 Redlock 简述

Redlock 流程:

1. 获取当前时间
2. 依次向 N 个独立 Redis 实例加锁
3. 多数实例成功且总耗时小于锁有效期,则视为成功
4. 有效期 = 初始TTL - 获取耗时
5. 失败则向所有实例释放

它试图降低单实例故障导致锁失效的概率,但存在争议:

建议:

11.9 锁与数据库乐观锁配合

可靠订单支付示例:

UPDATE orders
SET status = 'PAID', version = version + 1
WHERE order_id = ? AND status = 'PENDING' AND version = ?;

流程:

1. 尝试 Redis 锁,减少并发请求
2. 锁内读取订单
3. 数据库条件更新,只有 PENDING 才支付成功
4. 释放锁

即使 Redis 锁异常失效,数据库条件更新也会保证只有一个请求成功。这是生产系统的推荐形态。

11.10 分布式锁常见错误

错误 后果
忘记 TTL 宕机后死锁
SETNX 后再 EXPIRE 中间崩溃造成永久锁
value 固定字符串 释放别人的锁
DEL 释放 同上
业务耗时大于 TTL 多实例同时执行
finally 中盲目 unlock IllegalMonitorStateException
锁粒度过大 并发度下降
只靠锁保证正确性 主从切换后可能出错

11.11 可观测性

指标:

日志字段:

lock_key, owner_id, wait_ms, hold_ms, result, trace_id

出现 lock_timeout_while_running 必须告警,它意味着业务执行时间超过预期或锁被异常过期。

本章小结

思考题

  1. 为什么 value 必须是每个持有者唯一的随机值?
  2. 业务执行 5 秒但锁 TTL 3 秒,会发生什么?给出两种修复方案。
  3. 如果锁只能提高效率而不能保证绝对互斥,系统最终靠什么保证正确?