返回顶部
首页 > 资讯 > 精选 >Redis分布式锁该怎么实现续期
  • 470
分享到

Redis分布式锁该怎么实现续期

2023-06-26 03:06:56 470人浏览 薄情痞子
摘要

这篇文章将为大家详细讲解有关Redis分布式锁该怎么实现续期,文章内容质量较高,因此小编分享给大家做个参考,希望大家阅读完这篇文章后对相关知识有一定的了解。Redis分布式锁如何续期Redis分布式锁的正确姿势据肥朝了解,很多同学在用分布式

这篇文章将为大家详细讲解有关Redis分布式该怎么实现续期,文章内容质量较高,因此小编分享给大家做个参考,希望大家阅读完这篇文章后对相关知识有一定的了解。

Redis分布式锁如何续期

Redis分布式锁的正确姿势

据肥朝了解,很多同学在用分布式锁时,都是直接百度搜索找一个Redis分布式锁工具类就直接用了.关键是该工具类中还充斥着很多System.out.println();等语句.其实Redis分布式锁比较正确的姿势是采用redisson这个客户端工具.具体介绍可以搜索最大的同性交友网站GitHub.

如何回答

首先如果你之前用Redis的分布式锁的姿势正确,并且看过相应的官方文档的话,这个问题So easy.我们来看

Redis分布式锁该怎么实现续期

坦白说,如果你英文棒棒哒那么看英文文档可能更好理解

By default lock watchdog timeout is 30 seconds and can be changed through Config.lockWatchdogTimeout setting.

但是你如果看的是中文文档

看门狗检查锁的超时时间默认是30秒

这句话肥朝从语文角度分析就是一个歧义句,他有两个意思

看门狗默认30秒去检查一次锁的超时时间

看们狗会去检查锁的超时时间,锁的时间时间默认是30秒

看到这里,我希望大家不要黑我的小学体育老师,虽然他和语文老师是同个人.语文不行,我们可以源码来凑!

源码分析

我们根据官方文档给出的例子,写了一个最简单的demo,例子根据上面截图中Ctr+C和Ctr+V一波操作,如下

public class DemoMain {    public static void main(String[] args) throws Exception {        Config config = new Config();        config.useSingleServer().setAddress("redis://127.0.0.1:6379");        RedissonClient redisson = Redisson.create(config);        RLock lock = redisson.getLock("anyLock");        lock.lock();        //lock.unlock();    }}

create

Redis分布式锁该怎么实现续期

从这里我们知道,internalLockLeaseTime 和 lockWatchdogTimeout这两个参数是相等的.

lockWatchdogTimeout默认值如下

public class Config {private long lockWatchdogTimeout = 30 * 1000;public long getLockWatchdogTimeout() {return lockWatchdogTimeout;}//省略无关代码}

从internalLockLeaseTime这个单词也可以看出,这个加的分布式锁的超时时间默认是30秒.但是还有一个问题,那就是这个看门狗,多久来延长一次有效期呢?我们往下看

lock

Redis分布式锁该怎么实现续期

从我图中框起来的地方我们就知道了,获取锁成功就会开启一个定时任务,也就是watchdog,定时任务会定期检查去续期renewExpirationAsync(threadId).
这里定时用的是Netty-common包中的HashedWheelTimer,肥朝公众号已经和各大搜索引擎建立了密切的合作关系,你只需要把这个类在任何搜索引擎一搜,都能知道相关api参数的意义.
从图中我们明白,该定时调度每次调用的时间差是internalLockLeaseTime / 3.也就10秒.

真相大白

通过源码分析我们知道,默认情况下,加锁的时间是30秒.如果加锁的业务没有执行完,那么到 30-10 = 20秒的时候,就会进行一次续期,把锁重置成30秒.那这个时候可能又有同学问了,那业务的机器万一宕机了呢?宕机了定时任务跑不了,就续不了期,那自然30秒之后锁就解开了呗.

Redis分布式锁的5个坑

一、锁未被释放

这种情况是一种低级错误,就是我上边犯的错,由于当前线程 获取到redis 锁,处理完业务后未及时释放锁,导致其它线程会一直尝试获取锁阻塞,例如:用Jedis客户端会报如下的错误信息

redis.clients.jedis.exceptions.JedisConnectionException: Could not get a resource from the pool

redis线程池已经没有空闲线程来处理客户端命令。

解决的方法也很简单,只要我们细心一点,拿到锁的线程处理完业务及时释放锁,如果是重入锁未拿到锁后,线程可以释放当前连接并且sleep一段时间。

public void lock() {    while (true) {        boolean flag = this.getLock(key);        if (flag) {              TODO .........        } else {              // 释放当前redis连接              redis.close();              // 休眠1000毫秒             sleep(1000);       }     } }

二、B的锁被A给释放了

我们知道Redis实现锁的原理在于 SETNX命令。当 key不存在时将 key的值设为 value ,返回值为 1;若给定的 key已经存在,则 SETNX不做任何动作,返回值为 0 。

SETNX key value

我们来设想一下这个场景:A、B两个线程来尝试给key myLock加锁,A线程先拿到锁(假如锁3秒后过期),B线程就在等待尝试获取锁,到这一点毛病没有。

那如果此时业务逻辑比较耗时,执行时间已经超过redis锁过期时间,这时A线程的锁自动释放(删除key),B线程检测到myLock这个key不存在,执行 SETNX命令也拿到了锁。

但是,此时A线程执行完业务逻辑之后,还是会去释放锁(删除key),这就导致B线程的锁被A线程给释放了。

为避免上边的情况,一般我们在每个线程加锁时要带上自己独有的value值来标识,只释放指定value的key,否则就会出现释放锁混乱的场景。

三、数据库事务超时

emm~ 聊redis锁咋还扯到数据库事务上来了?别着急往下看,看下边这段代码:

 @Transaction public void lock() {      while (true) {          boolean flag = this.getLock(key);          if (flag) {              insert();          }      } }

给这个方法添加一个@Transaction注解开启事务,如代码中抛出异常进行回滚,要知道数据库事务可是有超时时间限制的,并不会无条件的一直等一个耗时的数据库操作。

比如:我们解析一个大文件,再将数据存入到数据库,如果执行时间太长,就会导致事务超时自动回滚。

一旦你的key长时间获取不到锁,获取锁等待的时间远超过数据库事务超时时间,程序就会报异常。

一般为解决这种问题,我们就需要将数据库事务改为手动提交、回滚事务。

  @Autowired  DataSourceTransactionManager dataSourceTransactionManager;  @Transaction  public void lock() {      //手动开启事务      TransactionStatus transactionStatus = dataSourceTransactionManager.getTransaction(transactionDefinition);      try {          while (true) {             boolean flag = this.getLock(key);             if (flag) {                 insert();                 //手动提交事务                 dataSourceTransactionManager.commit(transactionStatus);             }         }     } catch (Exception e) {         //手动回滚事务         dataSourceTransactionManager.rollback(transactionStatus);     } }

四、锁过期了,业务还没执行完

这种情况和我们上边提到的第二种比较类似,但解决思路上略有不同。

同样是redis分布式锁过期,而业务逻辑没执行完的场景,不过,这里换一种思路想问题,把redis锁的过期时间再弄长点不就解决了吗?

那还是有问题,我们可以在加锁的时候,手动调长redis锁的过期时间,可这个时间多长合适?业务逻辑的执行时间是不可控的,调的过长又会影响操作性能。

要是redis锁的过期时间能够自动续期就好了。

为了解决这个问题我们使用redis客户端redisson,redisson很好的解决了redis在分布式环境下的一些棘手问题,它的宗旨就是让使用者减少对Redis的关注,将更多精力用在处理业务逻辑上。

redisson对分布式锁做了很好封装,只需调用API即可。

RLock lock = redissonClient.getLock("stockLock");

redisson在加锁成功后,会注册一个定时任务监听这个锁,每隔10秒就去查看这个锁,如果还持有锁,就对过期时间进行续期。默认过期时间30秒。这个机制也被叫做:“看门狗”,这名字。。。

举例子:假如加锁的时间是30秒,过10秒检查一次,一旦加锁的业务没有执行完,就会进行一次续期,把锁的过期时间再次重置成30秒。

通过分析下边redisson的源码实现可以发现,不管是加锁、解锁、续约都是客户端把一些复杂的业务逻辑,通过封装在lua脚本中发送给redis,保证这段复杂业务逻辑执行的原子性。

@Slf4j@Servicepublic class RedisDistributionLockPlus {      private static final long DEFAULT_LOCK_TIMEOUT = 30;  private static final long TIME_SECONDS_FIVE = 5 ;    private Map<String, LockContent> lockContentMap = new ConcurrentHashMap<>(512);    private static final Long EXEC_SUCCESS = 1L;    private static final String LOCK_SCRIPT = "if redis.call('exists', KEYS[2]) == 1 then ARGV[2] = math.floor(redis.call('get', KEYS[2]) + 10) end " +          "if redis.call('exists', KEYS[1]) == 0 then " +             "local t = redis.call('set', KEYS[1], ARGV[1], 'EX', ARGV[2]) " +             "for k, v in pairs(t) do " +               "if v == 'OK' then return tonumber(ARGV[2]) end " +             "end " +          "return 0 end";    private static final String UNLOCK_SCRIPT = "if redis.call('get', KEYS[1]) == ARGV[1] then " +          "local ctime = tonumber(ARGV[2]) " +          "local biz_timeout = tonumber(ARGV[3]) " +          "if ctime > 0 then  " +             "if redis.call('exists', KEYS[2]) == 1 then " +                 "local avg_time = redis.call('get', KEYS[2]) " +                 "avg_time = (tonumber(avg_time) * 8 + ctime * 2)/10 " +                 "if avg_time >= biz_timeout - 5 then redis.call('set', KEYS[2], avg_time, 'EX', 24*60*60) " +                 "else redis.call('del', KEYS[2]) end " +             "elseif ctime > biz_timeout -5 then redis.call('set', KEYS[2], ARGV[2], 'EX', 24*60*60) end " +          "end " +          "return redis.call('del', KEYS[1]) " +          "else return 0 end";    private static final String RENEW_SCRIPT = "if redis.call('get', KEYS[1]) == ARGV[1] then return redis.call('expire', KEYS[1], ARGV[2]) else return 0 end";  private final StringRedisTemplate redisTemplate;  public RedisDistributionLockPlus(StringRedisTemplate redisTemplate) {      this.redisTemplate = redisTemplate;      ScheduleTask task = new ScheduleTask(this, lockContentMap);      // 启动定时任务      ScheduleExecutor.schedule(task, 1, 1, TimeUnit.SECONDS);  }    public boolean lock(String lockKey, String requestId, long expire) {      log.info("开始执行加锁, lockKey ={}, requestId={}", lockKey, requestId);      for (; ; ) {          // 判断是否已经有线程持有锁,减少redis的压力          LockContent lockContentOld = lockContentMap.get(lockKey);          boolean unLocked = null == lockContentOld;          // 如果没有被锁,就获取锁          if (unLocked) {              long startTime = System.currentTimeMillis();              // 计算超时时间              long bizExpire = expire == 0L ? DEFAULT_LOCK_TIMEOUT : expire;              String lockKeyRenew = lockKey + "_renew";              RedisScript<Long> script = RedisScript.of(LOCK_SCRIPT, Long.class);              List<String> keys = new ArrayList<>();              keys.add(lockKey);              keys.add(lockKeyRenew);              Long lockExpire = redisTemplate.execute(script, keys, requestId, Long.toString(bizExpire));              if (null != lockExpire && lockExpire > 0) {                  // 将锁放入map                  LockContent lockContent = new LockContent();                  lockContent.setStartTime(startTime);                  lockContent.setLockExpire(lockExpire);                  lockContent.setExpireTime(startTime + lockExpire * 1000);                  lockContent.setRequestId(requestId);                  lockContent.setThread(Thread.currentThread());                  lockContent.setBizExpire(bizExpire);                 lockContent.setLockCount(1);                 lockContentMap.put(lockKey, lockContent);                 log.info("加锁成功, lockKey ={}, requestId={}", lockKey, requestId);                 return true;             }         }         // 重复获取锁,在线程池中由于线程复用,线程相等并不能确定是该线程的锁         if (Thread.currentThread() == lockContentOld.getThread()                   && requestId.equals(lockContentOld.getRequestId())){             // 计数 +1             lockContentOld.setLockCount(lockContentOld.getLockCount()+1);             return true;         }         // 如果被锁或获取锁失败,则等待100毫秒         try {             TimeUnit.MILLISECONDS.sleep(100);         } catch (InterruptedException e) {             // 这里用lombok 有问题             log.error("获取redis 锁失败, lockKey ={}, requestId={}", lockKey, requestId, e);             return false;         }     } }  public boolean unlock(String lockKey, String lockValue) {     String lockKeyRenew = lockKey + "_renew";     LockContent lockContent = lockContentMap.get(lockKey);     long consumeTime;     if (null == lockContent) {         consumeTime = 0L;     } else if (lockValue.equals(lockContent.getRequestId())) {         int lockCount = lockContent.getLockCount();         // 每次释放锁, 计数 -1,减到0时删除redis上的key         if (--lockCount > 0) {             lockContent.setLockCount(lockCount);             return false;         }         consumeTime = (System.currentTimeMillis() - lockContent.getStartTime()) / 1000;     } else {         log.info("释放锁失败,不是自己的锁。");         return false;     }     // 删除已完成key,先删除本地缓存,减少redis压力, 分布式锁,只有一个,所以这里不加锁     lockContentMap.remove(lockKey);     RedisScript<Long> script = RedisScript.of(UNLOCK_SCRIPT, Long.class);     List<String> keys = new ArrayList<>();     keys.add(lockKey);     keys.add(lockKeyRenew);     Long result = redisTemplate.execute(script, keys, lockValue, Long.toString(consumeTime),             Long.toString(lockContent.getBizExpire()));     return EXEC_SUCCESS.equals(result); }  public boolean renew(String lockKey, LockContent lockContent) {     // 检测执行业务线程的状态     Thread.State state = lockContent.getThread().getState();     if (Thread.State.TERMINATED == state) {         log.info("执行业务的线程已终止,不再续约 lockKey ={}, lockContent={}", lockKey, lockContent);         return false;     }     String requestId = lockContent.getRequestId();     long timeOut = (lockContent.getExpireTime() - lockContent.getStartTime()) / 1000;     RedisScript<Long> script = RedisScript.of(RENEW_SCRIPT, Long.class);     List<String> keys = new ArrayList<>();     keys.add(lockKey);     Long result = redisTemplate.execute(script, keys, requestId, Long.toString(timeOut));     log.info("续约结果,True成功,False失败 lockKey ={}, result={}", lockKey, EXEC_SUCCESS.equals(result));     return EXEC_SUCCESS.equals(result); } static class ScheduleExecutor {     public static void schedule(ScheduleTask task, long initialDelay, long period, TimeUnit unit) {         long delay = unit.toMillis(initialDelay);         long period_ = unit.toMillis(period);         // 定时执行         new Timer("Lock-Renew-Task").schedule(task, delay, period_);     } } static class ScheduleTask extends TimerTask {     private final RedisDistributionLockPlus redisDistributionLock;     private final Map<String, LockContent> lockContentMap;     public ScheduleTask(RedisDistributionLockPlus redisDistributionLock, Map<String, LockContent> lockContentMap) {         this.redisDistributionLock = redisDistributionLock;         this.lockContentMap = lockContentMap;     }     @Override     public void run() {         if (lockContentMap.isEmpty()) {             return;         }         Set<Map.Entry<String, LockContent>> entries = lockContentMap.entrySet();         for (Map.Entry<String, LockContent> entry : entries) {             String lockKey = entry.geTKEy();             LockContent lockContent = entry.getValue();             long expireTime = lockContent.getExpireTime();             // 减少线程池中任务数量             if ((expireTime - System.currentTimeMillis())/ 1000 < TIME_SECONDS_FIVE) {                 //线程池异步续约                 ThreadPool.submit(() -> {                     boolean renew = redisDistributionLock.renew(lockKey, lockContent);                     if (renew) {                         long expireTimeNew = lockContent.getStartTime() + (expireTime - lockContent.getStartTime()) * 2 - TIME_SECONDS_FIVE * 1000;                         lockContent.setExpireTime(expireTimeNew);                     } else {                         // 续约失败,说明已经执行完 OR redis 出现问题                         lockContentMap.remove(lockKey);                    }                 });             }         }     } }}

五、redis主从复制的坑

redis高可用最常见的方案就是主从复制(master-slave),这种模式也给redis分布式锁挖了一坑。

redis cluster集群环境下,假如现在A客户端想要加锁,它会根据路由规则选择一台master节点写入key mylock,在加锁成功后,master节点会把key异步复制给对应的slave节点。

如果此时redis master节点宕机,为保证集群可用性,会进行主备切换,slave变为了redis master。B客户端在新的master节点上加锁成功,而A客户端也以为自己还是成功加了锁的。

此时就会导致同一时间内多个客户端对一个分布式锁完成了加锁,导致各种脏数据的产生。

至于解决办法嘛,目前看还没有什么根治的方法,只能尽量保证机器的稳定性,减少发生此事件的概率。

小结一下:上面就是我在使用Redis 分布式锁时遇到的一些坑,有点小感慨,经常用一个方法填上这个坑,没多久就发现另一个坑又出来了,其实根本没有什么十全十美的解决方案,哪有什么银弹,只不过是在权衡利弊后,选一个在接受范围内的折中方案而已。

关于Redis分布式锁该怎么实现续期就分享到这里了,希望以上内容可以对大家有一定的帮助,可以学到更多知识。如果觉得文章不错,可以把它分享出去让更多的人看到。

--结束END--

本文标题: Redis分布式锁该怎么实现续期

本文链接: https://lsjlt.com/news/306659.html(转载时请注明来源链接)

有问题或投稿请发送至: 邮箱/279061341@qq.com    QQ/279061341

猜你喜欢
  • Redis分布式锁该怎么实现续期
    这篇文章将为大家详细讲解有关Redis分布式锁该怎么实现续期,文章内容质量较高,因此小编分享给大家做个参考,希望大家阅读完这篇文章后对相关知识有一定的了解。Redis分布式锁如何续期Redis分布式锁的正确姿势据肥朝了解,很多同学在用分布式...
    99+
    2023-06-26
  • Redis分布式锁如何实现续期
    目录Redis分布式锁如何续期Redis分布式锁的正确姿势如何回答源码分析真相大白Redis分布式锁的5个坑一、锁未被释放二、B的锁被A给释放了三、数据库事务超时四、锁过期了,业务还...
    99+
    2024-04-02
  • Redis分布式锁如何自动续期的实现
    目录Redis 实现分布式锁问题自动续期看门狗Redissson tryLock看门狗如何自动续期续期原理Redis 实现分布式锁 指定一个 key 作为锁标记,存入 Re...
    99+
    2024-04-02
  • Redis分布式锁怎么实现
    这篇文章给大家分享的是有关Redis分布式锁怎么实现的内容。小编觉得挺实用的,因此分享给大家做个参考,一起跟随小编过来看看吧。分布式锁一般有三种实现方式:1、数据库乐观锁;2、基于Redis的分布式锁;3、...
    99+
    2024-04-02
  • Redis怎么实现分布式锁
    这篇文章主要介绍“Redis怎么实现分布式锁”,在日常操作中,相信很多人在Redis怎么实现分布式锁问题上存在疑惑,小编查阅了各式资料,整理出简单好用的操作方法,希望对大家解答”Redis怎么实现分布式锁”的疑惑有所帮助!接下来,请跟着小编...
    99+
    2023-06-02
  • 如何在Redis中实现分布式锁的自动续期机制
    实现分布式锁的自动续期机制可以通过以下步骤在Redis中实现: 获取锁时设置一个过期时间,确保锁在一定时间内会自动释放。 使用一个...
    99+
    2024-03-14
    Redis
  • 分布式锁的原理及Redis怎么实现分布式锁
    这篇文章主要介绍“分布式锁的原理及Redis怎么实现分布式锁”,在日常操作中,相信很多人在分布式锁的原理及Redis怎么实现分布式锁问题上存在疑惑,小编查阅了各式资料,整理出简单好用的操作方法,希望对大家解...
    99+
    2023-02-02
    redis
  • Redis中怎么实现分布式锁
    本篇内容介绍了“Redis中怎么实现分布式锁”的有关知识,在实际案例的操作过程中,不少人都会遇到这样的困境,接下来就让小编带领大家学习一下如何处理这些情况吧!希望大家仔细阅读,能够学有所成!为什么需要分布式...
    99+
    2024-04-02
  • 怎么用Redis实现分布式锁
    本文小编为大家详细介绍“怎么用Redis实现分布式锁”,内容详细,步骤清晰,细节处理妥当,希望这篇“怎么用Redis实现分布式锁”文章能帮助大家解决疑惑,下面跟着小编的思路慢慢深入,一起来学习新知识吧。单机...
    99+
    2024-04-02
  • Redis怎么样实现分布式锁
    这篇文章主要介绍了Redis怎么样实现分布式锁,具有一定借鉴价值,感兴趣的朋友可以参考下,希望大家阅读完这篇文章之后大有收获,下面让小编带着大家一起了解一下。在一个分布式系统中,会遇到一些需要对多个节点共享的资源加锁的情况,这个时候需要用到...
    99+
    2023-06-21
  • spring redis分布式锁怎么实现
    在Spring中实现Redis分布式锁可以使用RedisTemplate来操作Redis进行加锁和解锁。 首先,我们需要定义一个分布...
    99+
    2023-10-27
    spring redis
  • Redis——》实现分布式锁
    推荐链接:     总结——》【Java】     总结——》【Mysql】     总结——》【Redis】     总结——》【Kafka】     总结——》【Spring】     总结—...
    99+
    2023-09-03
    redis 分布式 过期 lua
  • Redis实现分布式锁
    单体锁存在的问题 在单体应用中,如果我们对共享数据不进行加锁操作,多线程操作共享数据时会出现数据一致性问题。 (下述实例是一个简单的下单问题:从redis中获取库存,检查库存是否够,>0才允许下单) 我们的解决办法通常是加锁。如下加单体锁...
    99+
    2023-08-16
    分布式 java jvm
  • Redis的分布式锁应该怎么打开
    这篇文章主要讲解了“Redis的分布式锁应该怎么打开”,文中的讲解内容简单清晰,易于学习与理解,下面请大家跟着小编的思路慢慢深入,一起来研究和学习“Redis的分布式锁应该怎么打开”吧!要求基于Redis实...
    99+
    2024-04-02
  • 怎么通过redis实现分布式锁
    本篇文章展示了通过redis实现分布式锁的具体操作,代码简明扼要容易理解,绝对能让你眼前一亮,通过这篇文章的详细介绍希望你能有所收获。分布式锁是控制分布式系统之间同步访问共享资源的一种方式。在分布式系统中,...
    99+
    2024-04-02
  • 怎么用Go+Redis实现分布式锁
    这篇文章主要介绍怎么用Go+Redis实现分布式锁,文中介绍的非常详细,具有一定的参考价值,感兴趣的小伙伴们一定要看完!为什么需要分布式锁用户下单锁住 uid,防止重复下单。库存扣减锁住库存,防止超卖。余额扣减锁住账户,防止并发操作。分布式...
    99+
    2023-06-22
  • Redisson中怎么实现Redis分布式锁
    Redisson中怎么实现Redis分布式锁,针对这个问题,这篇文章详细介绍了相对应的分析和解答,希望可以帮助更多想解决这个问题的小伙伴找到更简单易行的方法。Redis几种架构Redis发展到现在,几种常见的部署架构有:单机模式;主从模式;...
    99+
    2023-06-20
  • redis分布式锁的实现
    一、使用分布式锁要满足的几个条件:1、系统是一个分布式系统(关键是分布式,单机的可以使用ReentrantLock或者synchronized代码块来实现)2、共享资源(各个系统访问同一个资源,资源的载体可...
    99+
    2024-04-02
  • Redis Template实现分布式锁
    今天就跟大家聊聊有关Redis Template实现分布式锁,可能很多人都不太了解,为了让大家更加了解,小编给大家总结了以下内容,希望大家根据这篇文章可以有所收获。可靠性首先,为了确保分布式锁可用,我们至少...
    99+
    2024-04-02
  • PHP+Redis实现分布式锁
    目录 一、分布式锁概述 二、redis实现锁的命令 1、redis实现锁的命令 3、释放锁的步骤 三、PHP+redis分布式锁示例 四、redis集群分布式锁 一、分布式锁概述         在分布式环境下,各个线程通过对公共资...
    99+
    2023-09-15
    分布式锁
软考高级职称资格查询
编程网,编程工程师的家园,是目前国内优秀的开源技术社区之一,形成了由开源软件库、代码分享、资讯、协作翻译、讨论区和博客等几大频道内容,为IT开发者提供了一个发现、使用、并交流开源技术的平台。
  • 官方手机版

  • 微信公众号

  • 商务合作