Redis系列之简单实现watchDog自动续期机制

在分布锁的实际使用中,可能会遇到一种情况,一个业务执行时间很长,已经超过redis加锁的时间,也就是锁已经释放了,但是业务还没执行完成,这时候其它线程还是可以获取锁,那就没保证线程安全

项目环境:

  • JDK 1.8

  • SpringBoot 2.2.1

  • Maven 3.2+

  • Mysql 8.0.26

  • spring-boot-starter-data-redis 2.2.1

  • jedis3.1.0

  • 开发工具

    • IntelliJ IDEA

    • smartGit

先搭建一个springboot集成jedis的例子工程,参考我之前的博客,

抽象类,实现一些共用的逻辑

package com.example.jedis.common;

import lombok.extern.slf4j.Slf4j;

import java.net.SocketTimeoutException;
import java.util.concurrent.TimeUnit;

import static com.example.jedis.common.RedisConstant.DEFAULT_EXPIRE;
import static com.example.jedis.common.RedisConstant.DEFAULT_TIMEOUT;

@Slf4j
public abstract class AbstractDistributedLock implements DistributedLock {

    @Override
    public boolean acquire(String lockKey, String requestId, int expireTime, int timeout) {
        expireTime = expireTime <= 0 ? DEFAULT_EXPIRE : expireTime;
        timeout = timeout < 0 ? DEFAULT_TIMEOUT : timeout * 1000;

        long start = System.currentTimeMillis();
        try {
            do {
                if (doAcquire(lockKey, requestId, expireTime)) {
                    watchDog(lockKey, requestId, expireTime);
                    return true;
                }
                TimeUnit.MILLISECONDS.sleep(100);
            } while (System.currentTimeMillis() - start < timeout);

        } catch (Exception e) {
            Throwable cause = e.getCause();
            if (cause instanceof SocketTimeoutException) {
                // ignore exception
                log.error("sockTimeout exception:{}", e);
            }
            else if (cause instanceof  InterruptedException) {
                // ignore exception
                log.error("Interrupted exception:{}", e);
            }
            else {
                log.error("lock acquire exception:{}", e);
            }
            throw new LockException(e.getMessage(), e);
        }
        return false;
    }

    @Override
    public boolean release(String lockKey, String requestId) {
        try {
            return doRelease(lockKey, requestId);
        } catch (Exception e) {
            log.error("lock release exception:{}", e);
            throw new LockException(e.getMessage(), e);
        }
    }

    protected abstract boolean doAcquire(String lockKey, String requestId, int expireTime);

    protected abstract boolean doRelease(String lockKey, String requestId);

    protected abstract void watchDog(String lockKey, String requestId, int expireTime);


}

具体的实现,主要是基于一个定时任务,时间间隔一定要比加锁时间少一点,这里暂时少1s,加上一个lua脚本进行检测,检测不到数据,就关了定时任务

package com.example.jedis.common;

import cn.hutool.core.collection.CollUtil;
import cn.hutool.core.convert.Convert;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.InitializingBean;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Component;

import java.util.List;
import java.util.concurrent.ScheduledThreadPoolExecutor;
import java.util.concurrent.TimeUnit;

@Component
@Slf4j
public class JedisLockTemplate extends AbstractRedisLock implements InitializingBean {

    private String UNLOCK_LUA = "if redis.call('get', KEYS[1]) == ARGV[1] then return redis.call('del', KEYS[1]) else return 0 end";

    private String WATCH_DOG_LUA = "local lock_key=KEYS[1]\n" +
            "local lock_value=ARGV[1]\n" +
            "local lock_ttl=ARGV[2]\n" +
            "local current_value=redis.call('get',lock_key)\n" +
            "local result=0\n" +
            "if lock_value==current_value then\n" +
            "    redis.call('expire',lock_key,lock_ttl)\n" +
            "    result=1\n" +
            "end\n" +
            "return result";

    private static final Long UNLOCK_SUCCESS = 1L;

    private static final Long RENEWAL_SUCCESS = 1L;

    @Autowired
    private JedisTemplate jedisTemplate;

    private ScheduledThreadPoolExecutor scheduledExecutorService;


    @Override
    public void afterPropertiesSet() throws Exception {
        this.UNLOCK_LUA = jedisTemplate.scriptLoad(UNLOCK_LUA);
        this.WATCH_DOG_LUA = jedisTemplate.scriptLoad(WATCH_DOG_LUA);
        scheduledExecutorService = new ScheduledThreadPoolExecutor(1);
    }


    @Override
    public boolean doAcquire(String lockKey, String requestId, int expire) {
        return jedisTemplate.setnxex(lockKey, requestId, expire);
    }

    @Override
    public boolean doRelease(String lockKey, String requestId) {
        Object eval = jedisTemplate.evalsha(UNLOCK_LUA, CollUtil.newArrayList(lockKey), CollUtil.newArrayList(requestId));
        if (UNLOCK_SUCCESS.equals(eval)) {
            scheduledExecutorService.shutdown();
            return true;
        }
        return false;
    }

    @Override
    public void watchDog(String lockKey, String requestId, int expire) {
        int period = getPeriod(expire);
        if (scheduledExecutorService.isShutdown()) {
            scheduledExecutorService = new ScheduledThreadPoolExecutor(1);
        }
        scheduledExecutorService.scheduleAtFixedRate(
                new WatchDogTask(scheduledExecutorService, CollUtil.newArrayList(lockKey), CollUtil.newArrayList(requestId, Convert.toStr(expire))),
                1,
                period,
                TimeUnit.SECONDS
                );
    }

    class WatchDogTask implements Runnable {

        private ScheduledThreadPoolExecutor scheduledThreadPoolExecutor;
        private List<String> keys;
        private List<String> args;

        public WatchDogTask(ScheduledThreadPoolExecutor scheduledThreadPoolExecutor, List<String> keys, List<String> args) {
            this.scheduledThreadPoolExecutor = scheduledThreadPoolExecutor;
            this.keys = keys;
            this.args = args;
        }

        @Override
        public void run() {
            log.info("watch dog for renewal...");
            Object evalsha = jedisTemplate.evalsha(WATCH_DOG_LUA, keys, args);
            if (!evalsha.equals(RENEWAL_SUCCESS)) {
                scheduledThreadPoolExecutor.shutdown();
            }
            log.info("renewal result:{}, keys:{}, args:{}", evalsha, keys, args);
        }
    }

    private int getPeriod(int expire) {
        if (expire < 1)
            throw new LockException("expire不允许小于1");
        return expire - 1;
    }



}

写一个测试Controller类,开始用SpringBoot测试类的,但是发现有时候还是经常出现一些连接超时情况,这个可能是框架兼容的bug

package com.example.jedis.controller;

import com.example.jedis.common.JedisLockTemplate;
import com.example.jedis.common.Lock;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.RestController;

import java.util.Random;
import java.util.concurrent.CountDownLatch;
import java.util.stream.IntStream;

@RestController
@Slf4j
public class TestController {


    private static final String REDIS_KEY = "test:lock";

    @Autowired
    private JedisLockTemplate jedisLockTemplate;

    @GetMapping("test")
    public void test(@RequestParam("threadNum")Integer threadNum) throws InterruptedException {

        CountDownLatch countDownLatch = new CountDownLatch(threadNum);
        IntStream.range(0, threadNum).forEach(e->{
            new Thread(new RunnableTask(countDownLatch)).start();
        });
        countDownLatch.await();


    }

    @GetMapping("testLock")
    @Lock(lockKey = "test:api", requestId = "123", expire = 5, timeout = 3)
    public void testLock() throws InterruptedException {
        doSomeThing();
    }

    class RunnableTask implements Runnable {

        CountDownLatch countDownLatch;

        public RunnableTask(CountDownLatch countDownLatch) {
            this.countDownLatch = countDownLatch;
        }

        @Override
        public void run() {
            redisLock();
            countDownLatch.countDown();
        }


    }


    private void redisLock() {
        String requestId = getRequestId();
        Boolean lock = jedisLockTemplate.acquire(REDIS_KEY, requestId, 5, 3);
        if (lock) {
            try {
                doSomeThing();
            } catch (Exception e) {
                jedisLockTemplate.release(REDIS_KEY, requestId);
            } finally {
                jedisLockTemplate.release(REDIS_KEY, requestId);
            }
        } else {
            log.warn("获取锁失败!");
        }
    }

    private void doSomeThing() throws InterruptedException {
        log.info("do some thing");
        Thread.sleep(15 * 1000);
    }

    private String getRequestId() {
        String str="abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789";
        Random random=new Random();
        StringBuffer sb=new StringBuffer();
        for(int i=0;i<32;i++){
            int number=random.nextInt(62);
            sb.append(str.charAt(number));
        }
        return sb.toString();

    }



}

# 模拟100个并发请求
curl http://127.0.0.1:8080/springboot-jedis/test?threadNum=100

长事务还没执行完成,会自动进行续期

在这里插入图片描述

模拟100个线程的场景,只有一个线程会获取到锁

在这里插入图片描述

参考资料:

  • https://github.com/finefuture/RedisLock-with-WatchDog/blob/master/RedisLock.java
  • https://www.cnblogs.com/crazymakercircle/p/14731826.html
  • https://blog.csdn.net/Cocoxzq000/article/details/121575272

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:/a/240779.html

如若内容造成侵权/违法违规/事实不符,请联系我们进行投诉反馈qq邮箱809451989@qq.com,一经查实,立即删除!

相关文章

【Unity学习笔记】光照简介

本节主要是简单介绍一些常见的光照组件和渲染设置。 文章目录 灯光类型平行光Directional Light点光源Point Light聚光灯Spot Light面积光 Area Light 阴影设置全局光照明光照模式直接光照与间接光照Mixed Lighting 光照探针Light Probe Group光照探针组 反射探针 灯光类型 在…

使用 Python 实现简单的爬虫框架

爬虫是一种自动获取网页内容的程序&#xff0c;它可以帮助我们从网络上快速收集大量信息。在本文中&#xff0c;我们将学习如何使用 Python 编写一个简单的爬虫框架。 一、请求网页 首先&#xff0c;我们需要请求网页内容。我们可以使用 Python 的 requests 库来发送 HTTP 请…

CLEAR MOT评估指标

错误正样本&#xff08;False Positive&#xff0c;FP&#xff09;&#xff1a;整个视频中被预测为正的负样本数。 错误负样本&#xff08;False Negatives&#xff0c;FN&#xff09;&#xff1a;整个视频中被预测为负的正样本数。 IDs&#xff1a;跟踪过程中目标ID切换总数。…

springcloud微服务篇--2.微服务之间的调用

一、微服务案例需求1&#xff1a; 根据订单id查询订单的同时&#xff0c;把订单所属的用户信息一起返回 1、新建订单项目&#xff0c;用户服务。 2.RestTemplate实现微服务之间的访问。 在order-service的OrderApplication中注册RestTemplate 注入调用&#xff1a; Autowire…

【ModBus进阶日记】①ModBus协议栈解析

关注星标公众号&#xff0c;不错过精彩内容 文章目录 前言一、ModBus简介二、ModBus协议概述2.1 ModBus RTU主机框架2.2 ModBus RTU从机框架 三、ModBus帧描述四、ModBus RTU模式的报文断帧五、ModBus字节序六、ModBus事务处理流程七、ModBus异常响应八、ModBus功能码定义九、…

如何实现nacos的配置的热更新

我们在使用nacos进行修改配置后&#xff0c;需要微服务无需重启即可让配置生效&#xff0c;也就是使配置进行热更新我们可以采用下面的两种方式进行配置的热更新操作 方式一&#xff1a;在Value所注入的变量的类上添加注解RefreshScope RestController RequestMapping("/o…

Local Color Distributions Prior for ImageEnhancement

图1&#xff1a;给定同时具有过曝光&#xff08;背景窗口&#xff09;和欠曝光&#xff08;前景人物&#xff09;的输入图像&#xff08;a&#xff09;&#xff0c;现有方法不能很好地处理这两个问题。虽然&#xff08;b&#xff09;在背景上表现更好&#xff0c;但前景仅略微变…

anolisos8.8安装显卡+CUDA工具+容器运行时支持(containerd/docker)+k8s部署GPU插件

anolisos8.8安装显卡及cuda工具 一、目录 1、测试环境 2、安装显卡驱动 3、安装cuda工具 4、配置容器运行时 5、K8S集群安装nvidia插件 二、测试环境 操作系统&#xff1a;Anolis OS 8.8 内核版本&#xff1a;5.10.134-13.an8.x86_64 显卡安装版本&#xff1a;525.147.05 c…

学习笔记——GDB调试器

感谢B站up主 xiaobing1016 的学习视频&#xff1a;基于VSCode和CMake实现C/C开发 | Linux篇_哔哩哔哩_bilibili

电脑加密软件哪个最好用(电脑加密软件排行榜2023)

你公司的电脑数据在裸奔&#xff1f; 你公司的核心文件完全开放&#xff1f; 你公司的机密图纸谁都能获取&#xff1f; …… 作为老板的你&#xff0c;该不会还不知道电脑加密软件能够解决上述这些问题吧&#xff01; 我看过一个案例&#xff0c;新闻报道的略显夸张&#x…

企业资产管理技巧,这一点厉害了!

​随着市场竞争的不断加剧和金融环境的日益复杂&#xff0c;有效地管理和优化资产已经成为保持竞争优势和实现财务目标的不可或缺的一环。资产不仅仅是组织或个人的财富&#xff0c;更是潜在的增长动力和风险源。 传统的资产管理方法已经不再适应现代快速变化的经济环境。为了适…

【C语言程序设计】选择结构程序设计

目录 前言 一、程序阅读 二、程序改错 三、程序设计 总结 &#x1f308;嗨&#xff01;我是Filotimo__&#x1f308;。很高兴与大家相识&#xff0c;希望我的博客能对你有所帮助。 &#x1f4a1;本文由Filotimo__✍️原创&#xff0c;首发于CSDN&#x1f4da;。 &#x1f4e3;如…

时间序列预测 — CNN-LSTM实现多变量多步光伏预测(Tensorflow)

目录 1 数据处理 1.1 导入库文件 1.2 导入数据集 1.3 缺失值分析 2 构造训练数据 ​3 模型训练 3.1 CNN-LSTM网络 3.2 模型训练 4 模型预测 专栏链接&#xff1a;https://blog.csdn.net/qq_41921826/category_12495091.html 1 数据处理 1.1 导入库文件 import scip…

一些paper工具帮你搞定日常科研工作

如果你是在校生&#xff0c;科研er 你的日常避免各种各样的pepers&#xff1b;找papers&#xff0c;读papers&#xff0c;写papers。这三部曲贯穿这你整个科研工作&#xff0c;如何在有限的时间里&#xff0c;能够高效的完成科研&#xff0c;且保质保量&#xff0c;我们需要一些…

C# WPF上位机开发(内嵌虚拟机的软件开发)

【 声明&#xff1a;版权所有&#xff0c;欢迎转载&#xff0c;请勿用于商业用途。 联系信箱&#xff1a;feixiaoxing 163.com】 学习过halcon的同学都知道&#xff0c;它不仅有很多的图像算子可以使用&#xff0c;而且调试很方便。每一步骤的调试结果&#xff0c;都可以看到对…

MES系统设备日常点检:提升设备管理效率与维护质量

一、MES系统设备日常点检概述 MES系统设备日常点检是指通过MES系统对设备进行定期的外观、功能、性能等方面的检查&#xff0c;以评估设备的运行状态和潜在问题。这种检查方式有助于及时发现设备故障和隐患&#xff0c;确保设备的正常运行&#xff0c;提高生产效率。 二、MES系…

公益众筹源码系统:水滴筹模式+完整的安装包+部署教程

今天小编i来非大家来介绍一款公益众筹源码系统&#xff0c;带有完整的搭建教程。 一、系统开发 1.需求分析&#xff1a;在开发公益众筹源码系统之前&#xff0c;我们首先进行了深入的需求分析&#xff0c;明确了系统的目标、功能、性能等方面的要求。 2.技术选型&#xff1a;根…

什么是泊松图像混合

泊松图像混合&#xff08;Poisson Image Editing&#xff09;的原理基于泊松方程。该方法旨在保持图像中的梯度一致性&#xff0c;从而在图像编辑中实现平滑和无缝的混合。以下是泊松图像混合的基本原理和公式&#xff1a; 泊松方程 泊松方程是一个偏微分方程&#xff0c;通常…

【头歌系统Python实验】经典函数实例

目录 第1关&#xff1a;递归函数 - 汉诺塔的魅力 第2关&#xff1a;lambda 函数 - 匿名函数的使用 第3关&#xff1a;Map-Reduce - 映射与归约的思想 如果对你有帮助的话&#xff0c;不妨点赞收藏评论一下吧&#xff0c;爱你么么哒&#x1f618;❤️❤️❤️ 第1关&#xff…

Leetcode刷题笔记题解(C++):BM11 链表相加(二)

思路&#xff1a;先对两个链表进行反转&#xff0c;反转求和注意进位运算&#xff0c;求和完成之后再进行反转得到结果 /*** struct ListNode {* int val;* struct ListNode *next;* ListNode(int x) : val(x), next(nullptr) {}* };*/ #include <cstddef> class Soluti…