redis分布式锁过期问题和自动续期和主从延迟问题

一、Watchdog 自动续期机制

问题:

如果设置锁过期时间,例如 30s,但业务执行 40s,锁会提前释放,导致其他线程进入临界区。

Watchdog 的思路:

  • 加锁时 不设置固定过期时间
  • 启动一个 后台定时任务
  • 每隔一段时间 刷新锁的 TTL
  • 只要持锁线程还活着就不断续期
  • 线程释放锁后停止续期

Redisson 默认:

  • 锁时间:30s
  • 续期周期:10s(1/3)

流程:

复制代码
线程A获取锁
   ↓
Redis key TTL = 30s
   ↓
后台 watchdog 每10秒执行
   ↓
PEXPIRE key 30000
   ↓
不断续期
   ↓
线程A释放锁
   ↓
停止续期

二、Watchdog 核心代码示例

简化版 Java 实现逻辑(模拟 Redisson):

java 复制代码
public class RedisDistributedLock {

    private ScheduledExecutorService scheduler = Executors.newScheduledThreadPool(1);
    private StringRedisTemplate redisTemplate;
    private String lockKey;
    private String lockValue;
    private int expireTime = 30;

    public boolean lock() {

        lockValue = UUID.randomUUID().toString();

        Boolean success = redisTemplate.opsForValue()
                .setIfAbsent(lockKey, lockValue, expireTime, TimeUnit.SECONDS);

        if (Boolean.TRUE.equals(success)) {

            startWatchDog();

            return true;
        }

        return false;
    }

    private void startWatchDog() {

        scheduler.scheduleAtFixedRate(() -> {

            String value = redisTemplate.opsForValue().get(lockKey);

            if (lockValue.equals(value)) {

                redisTemplate.expire(lockKey, expireTime, TimeUnit.SECONDS);

            }

        }, 10, 10, TimeUnit.SECONDS);
    }

    public void unlock() {

        String value = redisTemplate.opsForValue().get(lockKey);

        if (lockValue.equals(value)) {
            redisTemplate.delete(lockKey);
        }

        scheduler.shutdown();
    }
}

核心逻辑:

  1. setnx + expire 获取锁
  2. 后台线程定期执行 expire
  3. 只给自己加的锁续期
  4. 释放锁时停止 watchdog

三、Redisson 的真实 Watchdog 机制

Redisson 的实现更复杂:

核心逻辑:

复制代码
lock()
  ↓
设置锁 TTL = 30s
  ↓
启动 watchdog
  ↓
scheduleExpirationRenewal()
  ↓
每 10 秒执行
  ↓
renewExpirationAsync()
  ↓
PEXPIRE key 30000

源码关键代码(简化):

java 复制代码
private void scheduleExpirationRenewal(long threadId) {

    Timeout task = timer.newTimeout(timeout -> {

        renewExpirationAsync(threadId);

    }, internalLockLeaseTime / 3, TimeUnit.MILLISECONDS);

}

续期脚本:

Lua 复制代码
if redis.call("hexists", KEYS[1], ARGV[2]) == 1 then
   redis.call("pexpire", KEYS[1], ARGV[1])
   return 1
end
return 0

意思:

  • 如果 当前线程仍然持有锁
  • 执行 PEXPIRE
  • 继续续期

四、Redis 主从延迟导致的问题

Redis 主从复制是 异步复制

可能发生:

复制代码
客户端A
   ↓
master SET lock
   ↓
返回成功

(还没同步)

master 宕机
   ↓
slave 升级为 master

客户端B
   ↓
在新 master 上 SET lock
   ↓
成功

最终:

复制代码
客户端A 持锁
客户端B 也持锁

两个客户端同时进入临界区

这就是 Redis 分布式锁最大的问题之一。


五、RedLock 解决方案

RedLock 需要:

复制代码
5个独立 Redis 节点

加锁流程:

复制代码
客户端依次向 5 个 Redis 加锁
成功 >= 3 才算成功

例如:

复制代码
R1 成功
R2 成功
R3 成功
R4 失败
R5 失败

3/5 成功 → 加锁成功

如果:

复制代码
R1 成功
R2 成功
R3 失败
R4 失败
R5 失败

2/5 → 加锁失败

并且要满足:

复制代码
获取锁总耗时 < 锁过期时间

六、Redisson 的 RedLock 示例

java 复制代码
RLock lock1 = redisson1.getLock("lock");
RLock lock2 = redisson2.getLock("lock");
RLock lock3 = redisson3.getLock("lock");

RedissonRedLock redLock = new RedissonRedLock(lock1, lock2, lock3);

try {
    boolean res = redLock.tryLock(10, 30, TimeUnit.SECONDS);

    if (res) {
        // 业务代码
    }

} finally {
    redLock.unlock();
}

七、实际生产建议

业内常见实践:

1️⃣ 单 Redis + Lua 脚本 + Watchdog

适用于:

  • 大多数互联网系统
  • 高性能

2️⃣ Redis Cluster + Redisson

优点:

  • 自动续期
  • Lua 保证原子性
  • API 简单

3️⃣ 对一致性要求极高

使用:

  • ZooKeeper
  • etcd

因为:

复制代码
Redis 是 AP
ZooKeeper / etcd 是 CP

八、一个很多人不知道的面试点

Redis 分布式锁 还有一个隐藏问题

客户端 GC pause

例如:

复制代码
线程A获取锁
TTL=30s

发生 Full GC 40s

锁已过期
线程B获得锁

GC结束
线程A继续执行

又出现并发

解决方案:

复制代码
Fencing Token(围栏令牌)

很多分布式系统(如 Kafka / ZooKeeper)都会用这个机制。

====================================================

Redis 本身没有提供"自动续期锁"的官方机制

Redis 只提供基础命令,比如:

  • SET key value NX PX 30000
  • EXPIRE key
  • PEXPIRE key

自动续期(Watchdog)必须由客户端自己实现,或者使用第三方库实现。Redis 服务器不会自动帮你续期。

一、Redis原生命令能做什么

Redis只提供修改 TTL 的命令,例如:

设置过期时间:

复制代码
SET lock_key uuid NX PX 30000

续期:

复制代码
PEXPIRE lock_key 30000

或者

复制代码
EXPIRE lock_key 30

但 Redis 不会自动调用这些命令,需要客户端定时执行。


二、最简单的手动实现方式

应用程序自己开 定时任务

示例:

java 复制代码
ScheduledExecutorService scheduler = Executors.newScheduledThreadPool(1);

scheduler.scheduleAtFixedRate(() -> {

    String value = redisTemplate.opsForValue().get("lock_key");

    if ("uuid123".equals(value)) {
        redisTemplate.expire("lock_key", 30, TimeUnit.SECONDS);
    }

}, 10, 10, TimeUnit.SECONDS);

逻辑:

复制代码
加锁成功
    ↓
启动定时任务
    ↓
每10秒执行
    ↓
检查锁是不是自己的
    ↓
是 → expire 续期

三、为什么要判断是不是自己的锁

如果不判断会出现问题:

复制代码
线程A加锁
TTL=30s

线程A卡住

锁过期

线程B获得锁

线程A的定时任务继续续期

结果:

复制代码
A把B的锁续期了

所以必须校验:

复制代码
value == uuid

四、更安全的续期方式(Lua脚本)

实际生产中会用 Lua 保证原子性

java 复制代码
if redis.call("get", KEYS[1]) == ARGV[1] then
    return redis.call("pexpire", KEYS[1], ARGV[2])
else
    return 0
end

Java 调用:

java 复制代码
redisTemplate.execute(luaScript,
        Collections.singletonList("lock_key"),
        uuid, "30000");

保证:

复制代码
检查锁 + 续期

原子操作


五、为什么大家直接用 Redisson

因为自己实现会遇到很多坑:

  • watchdog线程管理
  • Lua原子操作
  • 锁重入
  • 锁自动续期
  • 线程ID识别
  • GC暂停问题
  • 锁释放安全

Redisson 已经帮你全部实现好了

示例:

java 复制代码
RLock lock = redissonClient.getLock("order_lock");

lock.lock(); 

默认行为:

复制代码
TTL = 30s
watchdog 每 10s 自动续期

释放:

复制代码
lock.unlock();

六、总结

Redis官方能力:

复制代码
SET NX PX
EXPIRE
PEXPIRE
Lua

没有自动续期机制

实现方式:

1️⃣ 自己写定时任务 + EXPIRE

2️⃣ Lua脚本保证原子续期

3️⃣ 使用 Redisson(最常见)

相关推荐
MmeD UCIZ2 小时前
GO 快速升级Go版本
开发语言·redis·golang
隔壁寝室老吴3 小时前
使用Flink2.0消费低版本的Kafka
分布式·kafka
難釋懷3 小时前
Redis服务器端优化-内存划分和内存配置
java·redis·spring
qiuyunoqy4 小时前
Redis 常见数据结构,编码方式
数据库·redis·缓存
Chasing__Dreams5 小时前
Mysql--基础知识点--105--分布式事务
数据库·分布式·mysql
风兮雨露5 小时前
Windows 部署Redis免安装版以及客户端
数据库·windows·redis
NaMM CHIN5 小时前
linux redis简单操作
linux·运维·redis
java干货5 小时前
Redis 分布式限流的四大算法与终极形态
数据库·redis·分布式
富士康质检员张全蛋5 小时前
Kafka架构 主题中的分区
分布式·kafka