当前位置: 首页 > article >正文

Redisson的看门狗策略——保障Redis数据安全与稳定的机制

前言

自定义redis分布式锁无法自动续期,比如,一个锁设置了1分钟超时释放,如果拿到这个锁的线程在一分钟内没有执行完毕,那么这个锁就会被其他线程拿到,可能会导致严重的线上问题,在秒杀场景下,很容易因为这个缺陷导致的超卖了。

在分布式系统中,Redis作为一种高性能、低延迟的内存数据存储系统,被广泛应用于各种场景。然而,在复杂的环境中,Redis数据可能会面临过期失效或死锁等问题,这对应用程序的稳定性和安全性构成了威胁。为了解决这些问题,Redisson库提供了看门狗(Watch Dog)策略。在分布式锁失效好会自动续期 ,在reddison在为定义 leaseTime情况下,开启的时候默认会开启看门狗机制,默认是30s

什么是看门狗策略

看门狗策略是一种自动检测并处理过期键的机制。它基于Redis的“WATCH”命令实现,通过在Redisson库中创建一个监视器(Watch Dog)来监控Redis服务器上的指定键。

当应用程序使用Redisson库监视一个键时,Watch Dog会向Redis服务器发送一个“WATCH”命令,并在服务器上对该键进行监视。如果另一个客户端尝试修改被监视的键,Redis服务器将返回一个错误。这个错误会被Watch Dog捕获并处理。Watch Dog会在Redisson库内部触发一个事件,并将事件传递给应用程序,以便应用程序可以采取相应的操作。

看门狗机制是Redission提供的一种自动延期机制,这个机制使得Redission提供的分布式锁是可以自动续期的。

private long lockWatchdogTimeout = 30 * 1000;

看门狗机制提供的默认超时时间是30*1000毫秒,也就是30秒。如果一个线程获取锁后,运行程序到释放锁所花费的时间大于锁自动释放时间(也就是看门狗机制提供的超时时间30s),那么Redission会自动给redis中的目标锁延长超时时间。在Redission中想要启动看门狗机制,那么我们就不用获取锁的时候自己定义leaseTime(锁自动释放时间)。如果自己定义了锁自动释放时间的话,无论是通过lock还是tryLock方法,都无法启用看门狗机制。但是,如果传入的leaseTime为-1,也是会开启看门狗机制的。

分布式锁是不能设置永不过期的,这是为了避免在分布式的情况下,一个节点获取锁之后宕机从而出现死锁的情况,所以需要个分布式锁设置一个过期时间。但是这样会导致一个线程拿到锁后,在锁的过期时间到达的时候程序还没运行完,导致锁超时释放了,那么其他线程就能获取锁进来,从而出现问题。所以,看门狗机制的自动续期,就很好地解决了这一个问题。

源码解读

进入tryLock方法,这里的tryLock(waitTime, -1, unit)有三个参数

  • waitTime:获取锁的最大等待时间(没有传默认为-1)
  • leaseTime:锁自动释放的时间(没有传的话默认-1)
  • unit:时间的单位(等待时间和锁自动释放的时间单位)
public boolean tryLock(long waitTime, TimeUnit unit) throws InterruptedException {
    return tryLock(waitTime, -1, unit);
}
    @Override
    public boolean tryLock(long waitTime, long leaseTime, TimeUnit unit) throws InterruptedException {
        long time = unit.toMillis(waitTime);
        long current = System.currentTimeMillis();
        long threadId = Thread.currentThread().getId();
        Long ttl = tryAcquire(waitTime, leaseTime, unit, threadId);
        // lock acquired
        if (ttl == null) {
            return true;
        }
        
        time -= System.currentTimeMillis() - current;
        if (time <= 0) {
            acquireFailed(waitTime, unit, threadId);
            return false;
        }
        
        current = System.currentTimeMillis();
        RFuture<RedissonLockEntry> subscribeFuture = subscribe(threadId);
        if (!subscribeFuture.await(time, TimeUnit.MILLISECONDS)) {
            if (!subscribeFuture.cancel(false)) {
                subscribeFuture.onComplete((res, e) -> {
                    if (e == null) {
                        unsubscribe(subscribeFuture, threadId);
                    }
                });
            }
            acquireFailed(waitTime, unit, threadId);
            return false;
        }

        try {
            time -= System.currentTimeMillis() - current;
            if (time <= 0) {
                acquireFailed(waitTime, unit, threadId);
                return false;
            }
        
            while (true) {
                long currentTime = System.currentTimeMillis();
                ttl = tryAcquire(waitTime, leaseTime, unit, threadId);
                // lock acquired
                if (ttl == null) {
                    return true;
                }

                time -= System.currentTimeMillis() - currentTime;
                if (time <= 0) {
                    acquireFailed(waitTime, unit, threadId);
                    return false;
                }

                // waiting for message
                currentTime = System.currentTimeMillis();
                if (ttl >= 0 && ttl < time) {
                    subscribeFuture.getNow().getLatch().tryAcquire(ttl, TimeUnit.MILLISECONDS);
                } else {
                    subscribeFuture.getNow().getLatch().tryAcquire(time, TimeUnit.MILLISECONDS);
                }

                time -= System.currentTimeMillis() - currentTime;
                if (time <= 0) {
                    acquireFailed(waitTime, unit, threadId);
                    return false;
                }
            }
        } finally {
            unsubscribe(subscribeFuture, threadId);
        }
//        return get(tryLockAsync(waitTime, leaseTime, unit));
    }


看门狗策略的优点:

1.实时监控:
看门狗能够实时监控Redis服务器上的键,确保数据的实时性和准确性。
2.防止过期失效:
通过定期检测并处理过期键,看门狗策略可防止因键过期而导致的数据失效问题。
3.防止死锁:
在分布式高并发的条件下,如果一个线程获得了锁,但还没有来得及释放锁,就因为系统故障或其它原因使它无法执行释放锁的命令,这会导致其它线程都无法获得锁,从而造成死锁。通过使用看门狗策略,应用程序可以确保所有实例都能及时地响应数据变化,并避免这种情况的发生。
4.可扩展性:
看门狗策略支持横向扩展,能够随着系统的规模增长而保持高性能和稳定性。

watch dog缺点

redisson看门狗虽然能保证在线程没有执行完毕时,锁不会释放,对于秒杀这种强一致性的场景是适用的,但是对于防重这种场景是不适用的,在高并发情况下,会导致接口性能下降。

高并发防重时,如果加锁失败就快速失败,这时候可以使用自定义锁,或者tryLock,如下

RLock lock = redissonClient.getLock("Export:create:" + Context.get().getCorpId());
try {
    //尝试加锁,最多等待0秒,上锁以后5秒自动解锁
    if (lock.tryLock(0, 5, TimeUnit.SECONDS)) {
        //业务处理
    } else {
        Assert.isTrue(false, "排队中,请稍后重试!");
    }
} catch (InterruptedException e) {
    Assert.isTrue(false, "请勿重复操作!");
} finally {
    if (lock.isLocked()) {
        lock.unlock();
    }
}

Redisson 加解锁API

public void test() throws Exception{
        RLock lock = redissonClient.getLock("guodong");    // 拿锁失败时会不停的重试
        
        // 具有Watch Dog 自动延期机制 默认续30s 每隔30/3=10 秒续到30s
        lock.lock();
         
        // 具有Watch Dog 自动延期机制 默认续30s
        // 尝试拿锁10s后停止重试,返回false 具有Watch Dog 自动延期机制 默认续30s
        boolean res1 = lock.tryLock(10, TimeUnit.SECONDS); 
       
       // 没有Watch Dog 
       // 尝试获取锁10秒,如果获取不到则放弃
        lock.lock(10, TimeUnit.SECONDS);
       
       // 没有Watch Dog 
       // 尝试获取锁,等待100秒,持有锁10秒钟
        boolean res2 = lock.tryLock(100, 10, TimeUnit.SECONDS);
        
        
        Thread.sleep(40000L);
       
        
        lock.unlock();
    }

lock() 方法是阻塞获取锁的方式,如果当前锁被其他线程持有,则当前线程会一直阻塞等待获取锁,直到获取到锁或者发生超时或中断等情况才会结束等待。该方法获取到锁之后可以保证线程对共享资源的访问是互斥的,适用于需要确保共享资源只能被一个线程访问的场景。Redisson 的 lock() 方法支持可重入锁和公平锁等特性,可以更好地满足多线程并发访问的需求。

而 tryLock() 方法是一种非阻塞获取锁的方式,在尝试获取锁时不会阻塞当前线程,而是立即返回获取锁的结果,如果获取成功则返回 true,否则返回 false。Redisson 的 tryLock() 方法支持加锁时间限制、等待时间限制以及可重入等特性,可以更好地控制获取锁的过程和等待时间,避免程序出现长时间无法响应等问题。

默认情况下,看门狗的续期时间是30s,也可以通过修改Config.lockWatchdogTimeout来另行指定。另外Redisson 还提供了可以指定leaseTime参数的加锁方法来指定加锁的时间。超过这个时间后锁便自动解开了,不会延长锁的有效期。

总结

在使用Redis实现分布式锁的时候,会存在很多问题。

比如说业务逻辑处理时间>自己设置的锁自动释放时间的话,Redis就会按超时情况把锁释放掉,而其他线程就会趁虚而入抢夺锁从而出现问题,因此需要有一个续期的操作。并且,如果释放锁的操作在finally完成,需要判断一下当前锁是否是属于自己的锁,防止释放掉其他线程的锁,这样释放锁的操作就不是原子性了,而这个问题很好解决,使用lua脚本即可。

Redisson的出现,其中的看门狗机制很好解决续期的问题,它的主要步骤如下

  1. 在获取锁的时候,不能指定leaseTime或者只能将leaseTime设置为-1,这样才能开启看门狗机制。
  2. 在tryLockInnerAsync方法里尝试获取锁,如果获取锁成功调用scheduleExpirationRenewal执行看门狗机制
  3. 在scheduleExpirationRenewal中比较重要的方法就是renewExpiration,当线程第一次获取到锁(也就是不是重入的情况),那么就会调用renewExpiration方法开启看门狗机制。
  4. 在renewExpiration会为当前锁添加一个延迟任务task,这个延迟任务会在10s后执行,执行的任务就是将锁的有效期刷新为30s(这是看门狗机制的默认锁释放时间)
  5. 并且在任务最后还会继续递归调用renewExpiration。

也就是总的流程就是,首先获取到锁(这个锁30s后自动释放),然后对锁设置一个延迟任务(10s后执行),延迟任务给锁的释放时间刷新为30s,并且还为锁再设置一个相同的延迟任务(10s后执行),这样就达到了如果一直不释放锁(程序没有执行完)的话,看门狗机制会每10s将锁的自动释放时间刷新为30s。

而当程序出现异常,那么看门狗机制就不会继续递归调用renewExpiration,这样锁会在30s后自动释放。或者,在程序主动释放锁后,流程如下:

  1. 将锁对应的线程ID移除
  2. 接着从锁中获取出延迟任务,将延迟任务取消
  3. 在将这把锁从EXPIRATION_RENEWAL_MAP中移除。

http://www.kler.cn/a/108801.html

相关文章:

  • DAY6 线程
  • 常见error集合
  • 即插即用篇 | YOLOv8 引入 代理注意力 AgentAttention
  • 从0开始深度学习(28)——序列模型
  • pgsql和mysql的自增主键差异
  • Linux入门:环境变量与进程地址空间
  • Spring-声明式事务
  • 解决visual studio Just-In-Time Debugger调试
  • 论文写作框架示例:论软件系统建模方法及其应用
  • YouTube博主数据信息资源
  • JS中Map对象与object的区别
  • pythonWeb主流框架分析
  • css-表格样式
  • 【Java 进阶篇】解决Java Web应用中请求参数中文乱码问题
  • Python Wordcloud报错:Only supported for TrueType fonts,多种解决方案
  • 97. 交错字符串
  • Go学习第十二章——Go反射与TCP编程
  • 如何使用drawio画流程图以及导入导出
  • 微服务parent工程和子工程pom文件配置注意
  • 基于Qt 文本读写(QFile/QTextStream/QDataStream)实现
  • C++编程题目------平面上的最接近点对(分治算法)
  • C++设计模式_13_Flyweight享元模式
  • 漏洞复现-showdoc文件上传_v2.8.3_(CNVD-2020-26585)
  • Python环境下LaTeX数学公式转图像方案调研与探讨
  • 【大数据Hive】hive 表数据优化使用详解
  • 西工大CSAPP第二章课后题2.55答案及解析