Redis分布式锁是后端开发的高频面试点,但很多人只停留在SET key value NX EX的基础用法上。真正在线上环境跑过分布式锁的开发者都知道,锁自动过期导致的并发问题远比想象中更常见。我在电商系统开发中就遇到过这样的场景:订单支付回调处理耗时超过锁定时长,导致重复发货的严重事故。这正是Redisson Watchdog机制要解决的核心问题。
Watchdog本质上是一个后台守护线程,它的工作逻辑类似于操作系统的看门狗定时器。当你不显式指定锁过期时间时(即调用无参的lock()方法),Redisson会启动这个守护线程,以锁有效期1/3的间隔周期性地检查锁状态并延长有效期。举个例子,默认配置下锁有效期30秒,那么每10秒Watchdog就会执行一次续期操作。这种设计确保了只要客户端还"活着"且持有锁,锁就不会因为业务执行时间过长而意外释放。
让我们通过一个订单库存扣减的案例,看看没有自动续期会引发什么问题。假设我们使用原生Redis命令加锁:
bash复制SET inventory_lock_sku_123 client1 NX EX 30
这个命令给库存锁设置了30秒的固定过期时间。现在考虑以下时序:
结果是什么?原本应该扣减10件的库存,因为锁失效导致重复操作,最终扣减了20件——典型的超卖问题。我在2018年参与某电商平台重构时,就遇到过因为锁过期导致的百万级库存误差。事后我们用Redisson重构了锁机制,这类问题才彻底解决。
Redisson的Watchdog实现相当精巧,它主要由三个关键组件协同工作:
每个Redisson客户端实例启动时会生成唯一的UUID(如b9c4b9a3-558e-4a7e-959d-71c971a3b6b8),这个UUID会作为锁的值存入Redis。Watchdog续期时执行的Lua脚本如下:
lua复制if redis.call("HEXISTS", KEYS[1], ARGV[1]) == 1 then
redis.call("PEXPIRE", KEYS[1], ARGV[2])
return 1
end
return 0
这个脚本通过两个安全验证:
只有这两个条件同时满足,才会执行续期操作(PEXPIRE)。这种设计确保了不会错误地续期其他客户端持有的锁。
Watchdog线程的调度逻辑值得深入研究。在Redisson 3.16.0版本中,相关核心代码如下(简化版):
java复制private void scheduleExpirationRenewal() {
Thread task = new Thread(() -> {
while (!Thread.currentThread().isInterrupted()) {
try {
TimeUnit.MILLISECONDS.sleep(lockWatchdogTimeout / 3);
if (!renewExpiration()) {
break;
}
} catch (InterruptedException e) {
break;
}
}
});
task.setDaemon(true);
task.start();
}
这里有几个关键设计点:
完整的锁释放流程包括:
Redisson在unlock()操作时会执行以下Lua脚本:
lua复制if redis.call("HEXISTS", KEYS[1], ARGV[3]) == 0 then
return nil;
end;
local counter = redis.call("HINCRBY", KEYS[1], ARGV[3], -1);
if counter > 0 then
redis.call("PEXPIRE", KEYS[1], ARGV[2]);
return 0;
else
redis.call("DEL", KEYS[1]);
redis.call("PUBLISH", KEYS[2], ARGV[1]);
return 1;
end;
这个脚本处理了可重入锁的计数递减,当计数器归零时才真正删除锁键并发布释放消息,同时会触发Watchdog线程的终止。
根据我在多个分布式系统的实践经验,Watchdog的默认配置可能需要调整:
java复制Config config = new Config();
config.setLockWatchdogTimeout(15000); // 设置为15秒
RedissonClient redisson = Redisson.create(config);
调整原则:
retryInterval和retryAttemptsjava复制RLock lock = redisson.getLock("order_lock");
try {
if (lock.tryLock(5, 30, TimeUnit.SECONDS)) {
// 业务逻辑
}
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
// 处理中断
} finally {
if (lock.isHeldByCurrentThread()) {
lock.unlock();
}
}
注意事项:
现象:锁在业务执行期间过期
可能原因:
lock.lock(10, TimeUnit.SECONDS)会禁用Watchdog排查步骤:
TTL your_lock_keyHGETALL your_lock_keyRLock.getHoldCount()调试本地持有计数在订单履约系统中,我设计过这样的多级锁策略:
java复制// 订单处理伪代码
public void processOrder(String orderId) {
RLock orderLock = redisson.getLock("order:" + orderId);
RLock stockLock = redisson.getLock("stock:" + itemId);
try {
orderLock.lock(); // 60秒Watchdog
stockLock.lock(15, TimeUnit.SECONDS); // 固定15秒
// 业务逻辑
} finally {
stockLock.unlock();
orderLock.unlock();
}
}
对于Spring Boot项目,可以这样封装:
java复制@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.METHOD)
public @interface DistributedLock {
String key();
long waitTime() default 5;
long leaseTime() default -1; // -1表示启用Watchdog
}
@Aspect
@Component
public class DistributedLockAspect {
@Autowired
private RedissonClient redisson;
@Around("@annotation(distributedLock)")
public Object around(ProceedingJoinPoint joinPoint,
DistributedLock distributedLock) throws Throwable {
RLock lock = redisson.getLock(distributedLock.key());
boolean locked = false;
try {
if (distributedLock.leaseTime() > 0) {
locked = lock.tryLock(distributedLock.waitTime(),
distributedLock.leaseTime(),
TimeUnit.SECONDS);
} else {
locked = lock.tryLock(distributedLock.waitTime(),
TimeUnit.SECONDS);
}
if (locked) {
return joinPoint.proceed();
}
throw new RuntimeException("获取分布式锁失败");
} finally {
if (locked && lock.isHeldByCurrentThread()) {
lock.unlock();
}
}
}
}
使用方式:
java复制@DistributedLock(key = "'order:' + #orderId", leaseTime = -1)
public void processOrder(String orderId) {
// 业务逻辑
}
在千万级日订单的系统中,我们对Redisson锁进行了这些优化:
锁粒度优化:
Watchdog调参:
java复制// 在Config中调整
config.setLockWatchdogTimeout(10_000); // 10秒
config.setNettyThreads(32); // 提高网络IO线程数
监控集成:
RTopic订阅锁事件java复制// 锁监控示例
RTopic topic = redisson.getTopic("redisson_lock__channel:" + lockName);
topic.addListener(String.class, (channel, msg) -> {
if ("unlock".equals(msg)) {
metrics.recordLockRelease(lockName);
}
});
当Redisson的Watchdog机制不适合时,可以考虑:
| 方案 | 优点 | 缺点 | 适用场景 |
|---|---|---|---|
| 固定TTL+重试 | 实现简单 | 业务可能被多次执行 | 短时操作,允许重试 |
| 两层锁(ZooKeeper+Redis) | 更可靠 | 架构复杂 | 金融级一致性要求 |
| 数据库乐观锁 | 无需额外组件 | 高并发性能差 | 低频修改场景 |
| Lease机制(etcd) | 内置续期API | 学习成本高 | 云原生环境 |
在技术选型时,我通常会问三个问题:
根据这些问题的答案,才能决定是使用Redisson Watchdog还是其他方案。