路灯
路灯
发布于 2026-03-04 / 0 阅读
0
0

高可靠Redis分布式锁实现方案(含自动续期 & 看门狗机制)

1. 背景与问题

在分布式系统中,常见的业务场景(如秒杀、订单扣减、幂等处理、定时任务调度等)需要使用分布式锁来保证同一时刻只有一个实例/线程能执行关键逻辑。

使用Redis作为分布式锁中间件是最主流的选择之一(SETNX + EXPIRE 或 Redisson),但存在一个经典问题:

业务执行时间 > 锁过期时间 时,会出现以下致命问题:

  1. 锁被提前释放(过期)

  2. 后续线程误获取到锁

  3. 当前业务还在执行 → 导致并发安全被破坏(超卖、重复扣款、重复调度等)

痛点总结

  • 固定过期时间很难预测业务最长执行时间

  • 业务抖动、网络延迟、GC暂停、慢查询等都会导致实际耗时不可控

  • 手动续期代码侵入性强、容易遗漏、维护困难

2. 目标

设计一套高可用、高性能、业务无侵入的Redis分布式锁方案,满足以下核心要求:

  • 锁自动续期(业务未完成就自动延长)

  • 锁持有者才能续期(防止误续)

  • 业务异常退出/宕机,锁能自动释放(不永久持有)

  • 支持可重入(同一个线程/同一个业务可多次加锁)

  • 性能损失可接受(<5ms 延迟增加)

3. 核心方案:Redisson风格看门狗(Watchdog)自动续期机制

3.1 整体架构

业务代码
   ↓
分布式锁接口(Lock / RLock)
   ↓
RedissonClient / 自研RedisLock
   ├─ Lua脚本加锁(原子性)
   ├─ 定时任务线程池(Watchdog)
   │    ↓
   └─ 后台自动续期(EXPIRE)
        ↓
Redis Server(Hash + Key)

3.2 关键数据结构(以Redisson为例)

锁Key在Redis中实际存储为 Hash 结构(支持可重入):

Key: lock_key:order:12345
Type: Hash
Fields:
  "client:thread-uuid:threadId" → "2"   (重入次数)
  "client:thread-uuid:threadId" → "5"   (示例)
Expire: 30s(初始) → 由看门狗动态续期

3.3 加锁流程(Lua原子脚本)

-- KEYS[1] = lock key
-- ARGV[1] = thread unique id (uuid:threadId)
-- ARGV[2] = internalLockLeaseTime (默认30s)
​
if redis.call('hexists', KEYS[1], ARGV[1]) == 0 then
    redis.call('hset', KEYS[1], ARGV[1], 1);
    redis.call('pexpire', KEYS[1], ARGV[2]);
    return 1;
end;
​
if redis.call('hexists', KEYS[1], ARGV[1]) == 1 then
    redis.call('hincrby', KEYS[1], ARGV[1], 1);
    redis.call('pexpire', KEYS[1], ARGV[2]);
    return 1;
end;
​
return 0;

3.4 自动续期核心:看门狗(Watchdog)机制

  1. 加锁成功后立即启动一个后台续期任务

    • 续期间隔 = 锁过期时间 / 3(默认10s,如果锁30s)

    • 每次续期把过期时间重新设置为 30s(可配置)

  2. 续期Lua脚本(同样原子)

-- KEYS[1] = lock key
-- ARGV[1] = internalLockLeaseTime (30s)
​
if redis.call('hexists', KEYS[1], ARGV[1]) == 1 then
    redis.call('pexpire', KEYS[1], ARGV[2]);
    return 1;
end;
​
return 0;
  1. 看门狗生命周期

    • 加锁成功 → 创建并启动续期TimerTask

    • 解锁成功 → 取消该TimerTask

    • 客户端进程退出 → TimerTask自然停止 → 锁自动过期释放

3.5 解锁流程(Lua原子)

-- KEYS[1] = lock key
-- ARGV[1] = thread unique id
​
local counter = redis.call('hget', KEYS[1], ARGV[1]);
if counter == false then
    return nil;
end
​
counter = tonumber(counter);
​
if counter <= 0 then
    redis.call('del', KEYS[1]);
    return nil;
end
​
counter = counter - 1;
redis.call('hset', KEYS[1], ARGV[1], counter);
​
if counter > 0 then
    redis.call('pexpire', KEYS[1], ARGV[2]);
    return 0;
else
    redis.call('del', KEYS[1]);
    return 1;
end

4. 推荐技术选型对比(2025-2026视角)

方案

自动续期

可重入

Lua原子

客户端维护成本

推荐场景

备注

原生 SETNX + EXPIRE

×

×

部分

极简单场景

极易出问题,不推荐

Redisson

绝大多数业务

目前最成熟、最推荐

Redlock

×

高可用强一致需求

实现复杂,性能开销大

自研Watchdog

极致定制/裁剪需求

需自己维护,bug风险高

Zookeeper Curator

对CP要求极高场景

依赖ZK,部署重

结论优先使用 Redisson,除非有极特殊需求(如完全去Redisson依赖、极致性能裁剪)才考虑自研。

5. 最佳实践与注意事项

  1. 锁Key设计规范:lock:biz:resource:{id},避免热点

  2. 业务最坏情况超时设置:建议锁默认30s,业务尽量控制在10s内

  3. 看门狗线程池隔离:建议单独线程池,避免业务线程池阻塞影响续期

  4. 监控指标必备:

    • 锁获取成功率 / 失败率

    • 锁等待时长 P99

    • 续期失败次数(理论上应≈0)

    • 锁被意外释放次数(报警)

  5. 降级策略:分布式锁失败 → 是否降级为单机锁 / 异步补偿 / 人工干预

  6. 环境差异:开发环境可关闭看门狗或调大过期时间

6. 总结

通过看门狗自动续期机制,我们可以在不侵入业务代码的前提下,有效解决“业务未完成锁已过期”的致命问题,成为目前分布式锁事实标准(Redisson实现最为完善)。

推荐在所有新项目中默认使用Redisson RLock,并做好监控与Key规范,即可获得较高可靠性的分布式锁能力。

如需极致定制或完全自研,可参考上述Lua脚本 + TimerTask续期模式自行实现,但需承担更高的维护成本与风险。

7.业务场景

以下是针对真实业务场景(秒杀、支付回调、定时任务)定制的 Redis分布式锁方案(基于Redisson看门狗机制)

场景一:秒杀(高并发抢购 / 限购 / 库存扣减)

业务特点

  • QPS 极高(单商品峰值可达数万~数十万)

  • 执行时间抖动大(Redis、数据库、第三方风控等)

  • 超卖/少卖代价极高

  • 锁粒度通常到 SKU 或 用户+SKU

推荐方案

// 推荐写法(启用看门狗)
RLock lock = redissonClient.getLock("lock:seckill:sku:" + skuId);
​
try {
    // 建议等待时间 500~1500ms,根据实际压测调整
    // -1 表示不限等待(但生产慎用,易堆积)
    boolean acquired = lock.tryLock(800, TimeUnit.MILLISECONDS);
    if (!acquired) {
        // 抢锁失败 → 返回“商品已抢光”或进入排队/降级逻辑
        return SeckillResult.fail(SeckillCode.OUT_OF_STOCK_OR_BUSY);
    }
​
    // 核心业务逻辑
    // 1. 读库存(lua 或 pipeline)
    // 2. 判断是否还有库存 & 用户限购
    // 3. 扣减库存(Lua原子)
    // 4. 记录抢购成功订单(异步MQ)
    Long stock = decrementStockLua(skuId, buyQuantity);
    if (stock < 0) {
        return SeckillResult.fail(SeckillCode.OUT_OF_STOCK);
    }
​
    // 异步发MQ创建订单
    orderEventProducer.sendCreateOrderEvent(userId, skuId, ...);
​
    return SeckillResult.success();
​
} catch (Exception e) {
    log.error("秒杀异常", e);
    // 可选:补偿性回滚(视幂等设计)
    return SeckillResult.fail(SeckillCode.SYSTEM_ERROR);
} finally {
    if (lock.isHeldByCurrentThread()) {
        lock.unlock();
    }
}

关键配置建议

  • lockWatchdogTimeout = 30000 ~ 45000 ms

  • tryLock等待时间:500~2000ms(太长会导致请求堆积)

  • 库存扣减使用 Lua 脚本 保证原子性(不依赖分布式锁内数据库事务)

  • 热点商品可考虑 本地锁 + Redis锁 二级过滤

场景二:支付回调(异步通知 / 幂等处理)

业务特点

  • 回调时间不可控(渠道方延迟可达几秒~几分钟)

  • 必须严格幂等(重复回调不能重复入账)

  • 锁粒度通常为 订单号 / 支付流水号

  • 并发不高,但一致性要求极高

推荐方案

// 支付回调接口(Controller / MQ Listener)
public void onPayCallback(PayNotifyDTO dto) {
    String lockKey = "lock:pay:order:" + dto.getOrderNo();
​
    RLock lock = redissonClient.getLock(lockKey);
​
    try {
        // 支付回调等待时间可以稍长(渠道重试间隔通常较长)
        if (!lock.tryLock(3, 45, TimeUnit.SECONDS)) {
            log.warn("支付回调获取锁失败,稍后重试 orderNo:{}", dto.getOrderNo());
            // 可选择:NACK / 重新入队 / 记录延迟处理
            return;
        }
​
        // 检查是否已处理(防幂等核心)
        if (isOrderAlreadyPaid(dto.getOrderNo())) {
            log.info("订单已支付,幂等返回 orderNo:{}", dto.getOrderNo());
            return;
        }
​
        // 核心处理
        boolean success = processPayment(dto);
        if (success) {
            updateOrderStatusPaid(dto.getOrderNo(), dto.getTradeNo());
            // 发MQ通知下游(发货、积分、券等)
        }
​
    } catch (Exception e) {
        log.error("支付回调处理异常", e);
        // 可记录到死信 / 人工审核
    } finally {
        if (lock.isHeldByCurrentThread()) {
            lock.unlock();
        }
    }
}

关键配置建议

  • lockWatchdogTimeout = 45000 ~ 90000 ms(回调处理可能涉及对账、清结算等长流程)

  • tryLock 等待时间:3~10秒(渠道回调重试间隔一般 >10s)

  • 幂等判断优先于锁(锁仅防止并发处理同一订单)

场景三:定时任务(防止重复调度 / 集群幂等执行)

业务特点

  • 多实例部署(k8s / 集群)

  • 要求同一时刻只有一个实例执行

  • 执行时间不确定(可能卡住、FullGC、慢SQL等)

推荐方案

// 使用 Redisson 的 RLock(而非 RSemaphore / RCountDownLatch)
@Scheduled(cron = "0 0/5 * * * ?")   // 每5分钟
public void executeClearExpiredCouponTask() {
    String lockKey = "lock:task:clear_expired_coupon";
​
    RLock lock = redissonClient.getLock(lockKey);
​
    if (!lock.tryLock()) {
        log.info("定时任务-清理过期优惠券 获取锁失败,本次跳过");
        return;
    }
​
    try {
        log.info("开始清理过期优惠券...");
        couponService.clearExpiredCoupons(LocalDateTime.now().minusDays(30));
        log.info("清理过期优惠券完成");
    } catch (Exception e) {
        log.error("清理过期优惠券任务异常", e);
        // 可选:告警
    } finally {
        if (lock.isHeldByCurrentThread()) {
            lock.unlock();
        }
    }
}

关键配置建议

  • lockWatchdogTimeout = 60000 ~ 180000 ms(根据任务最坏情况设置)

  • tryLock() 无等待(抢不到直接放弃本次执行)

  • 任务Key建议加上环境标识(dev/test/prod)或业务线

  • 推荐使用 Redisson Lock + Spring Task 组合,而非 ShedLock(更灵活)

对比表

场景

是否启用看门狗

推荐过期时间

tryLock等待

锁粒度建议

降级策略

秒杀

是(强烈推荐)

30~45s

0.5~2s

sku / user+sku

本地布隆 + 降级为排队页

支付回调

45~90s

3~10s

orderNo / outTradeNo

记录延迟队列 / 人工对账

定时任务

60~300s

0ms

task:类名:方法名

下次调度补偿 / 告警

希望以上方案能直接用于生产落地。如需针对具体中间件版本、Lua脚本完整示例、压测数据、Grafana看板模板等进一步补充,请随时告知。

(完)


评论