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 {@Overridepublic 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 exceptionlog.error("sockTimeout exception:{}", e);}else if (cause instanceof  InterruptedException) {// ignore exceptionlog.error("Interrupted exception:{}", e);}else {log.error("lock acquire exception:{}", e);}throw new LockException(e.getMessage(), e);}return false;}@Overridepublic 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;@Autowiredprivate JedisTemplate jedisTemplate;private ScheduledThreadPoolExecutor scheduledExecutorService;@Overridepublic void afterPropertiesSet() throws Exception {this.UNLOCK_LUA = jedisTemplate.scriptLoad(UNLOCK_LUA);this.WATCH_DOG_LUA = jedisTemplate.scriptLoad(WATCH_DOG_LUA);scheduledExecutorService = new ScheduledThreadPoolExecutor(1);}@Overridepublic boolean doAcquire(String lockKey, String requestId, int expire) {return jedisTemplate.setnxex(lockKey, requestId, expire);}@Overridepublic 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;}@Overridepublic 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;}@Overridepublic 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";@Autowiredprivate 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;}@Overridepublic 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

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

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

完美解决labelimg xml转可视化中文乱码问题,不用matplotlib

背景简述 我们有一批标注项目要转可视化&#xff0c;因为之前没有做过&#xff0c;然后网上随意找了一段代码测试完美&#xff08;并没有&#xff09;搞定&#xff0c;开始疯狂标注&#xff0c;当真正要转的时候傻眼了&#xff0c;因为测试的时候用的是英文标签&#xff0c;实…

基于linux系统的Tomcat+Mysql+Jdk环境搭建(三)centos7 安装Tomcat

Tomcat下载官网&#xff1a; Apache Tomcat - Which Version Do I Want? JDK下载官网&#xff1a; Java Downloads | Oracle 中国 如果不知道Tomcat的哪个版本应该对应哪个版本的JDK可以打开官网&#xff0c;点击Whitch Version 下滑&#xff0c;有低版本的&#xff0c;如…

Flutter实现Android拖动到垃圾桶删除效果-Draggable和DragTarget的详细讲解

文章目录 Draggable介绍构造函数参数说明使用示例 DragTarget 介绍构造函数参数说明使用示例 DragTarget 如何接收Draggable传递过来的数据&#xff1f; Draggable介绍 Draggable是Flutter框架中的一个小部件&#xff0c;用于支持用户通过手势拖动一个子部件。它是基于手势的一…

知识付费小程序开发:技术实践示例

随着知识付费小程序的兴起&#xff0c;让我们一起来看一个简单的示例&#xff0c;使用Node.js和Express框架搭建一个基础的知识付费小程序后端。 首先&#xff0c;确保你已经安装了Node.js和npm。接下来&#xff0c;创建一个新的项目文件夹&#xff0c;然后通过以下步骤创建你…

适用于 Windows 和 Mac 的 10 款最佳照片恢复软件(免费和付费)

丢失照片很容易。这里点击错误&#xff0c;那里贴错标签的 SD 卡&#xff0c;然后噗的一声&#xff0c;一切都消失了。值得庆幸的是&#xff0c;在技术领域&#xff0c;你可以纠正一些错误。其中包括删除数据或格式化错误的存储设备。 那么&#xff0c;让我们看看可用于从 SD …

[c++]—vector类___提升版(带你了解vector底层的运用)

我写我 不论主谓宾 可以反复错 &#x1f308;vector的介绍 1.vector是表示可变大小数组的序列容器2.就像数组一样&#xff0c;vector也采用的连续存储空间来存储元素&#xff0c;也就是意味着可以采用下标对vector的元素进行访问&#xff0c;和数组一样高效。但是又不像数组&…

工业性能CCD图像处理+

目录 硬件部分 ​编辑 软件部分 CCD新相机的调试处理&#xff08;更换相机处理&#xff0c;都要点执行检测来查看图像变化&#xff09; 问题:新相机拍摄出现黑屏&#xff0c;图像拍摄不清晰&#xff0c;&#xff08;可以点击图像&#xff0c;向下转动鼠标的滚轮&#xff08…

基于linux系统的Tomcat+Mysql+Jdk环境搭建(一)vmare centos7 设置静态ip和连接MobaXterm

特别注意&#xff0c;Windows10以上版本操作系统需要下载安装VMware Workstation Pro16及以上版本&#xff0c;安装方式此处略。 (可忽略 my*** 记录设置的vamare centos7 账号root/aaa 密码&#xff1a;Aa123456 ) 1、命令行和图形界面切换 如果使用的是VMware虚拟机&…

金智融门户(统一身份认证)同步数据至钉钉通讯录

前言:因全面使用金智融门户和数据资产平台,二十几个信息系统已实现统一身份认证和数据同步,目前单位使用的钉钉尚未同步组织机构和用户信息,职工入职、离职、调岗时都需要手工在钉钉后台操作,一是操作繁琐,二是钉钉通讯录更新不及时或经常遗漏,带来管理问题。通过金智融…

CAD 审图意见的导出

看图的时候喜欢在图上直接标注意见&#xff0c;但是如果还要再把意见一行一行的导出到word里面就很麻烦&#xff0c;在网上看了一个审图软件&#xff0c;报价要980&#xff0c;而且那个审图意见做的太复杂了。 我的需求就是把图上标的单行文字和多行文字直接导出来就行&#x…

debug点f8step over会进入class文件

File->settings->Bulid.Executiong.Deployment->Debugger->Stepping 取消如图对钩即可

二十七、读写文件

二十七、读写文件 27.1 文件类QFile #include <QCoreApplication>#include<QFile> #include<QDebug>int main(int argc, char *argv[]) {QCoreApplication a(argc, argv);QFile file("D:/main.txt");if(!file.open(QIODevice::WriteOnly | QIODe…

three.js模拟太阳系

地球的旋转轨迹目前设置为了圆形&#xff0c;效果&#xff1a; <template><div><el-container><el-main><div class"box-card-left"><div id"threejs" style"border: 1px solid red"></div><div c…

idea第一次提交到git(码云)

1.先创建一个仓库 2.将idea和仓库地址绑定 2.将idea和仓库地址绑定

CentOS 7系统加固详细方案SSH FTP MYSQL加固

一、删除后门账户 修改强口令 1、修改改密码长度需要编译login.defs文件 vi /etc/login.defs PASS_MIN_LEN 82、注释掉不需要的用户和用户组 或者 检查是否存在除root之外UID为0的用户 使用如下代码&#xff0c;对passwd文件进行检索&#xff1a; awk -F : ($30){print $1) …

『K8S 入门』二:深入 Pod

『K8S 入门』二&#xff1a;深入 Pod 一、基础命令 获取所有 Pod kubectl get pods2. 获取 deploy kubectl get deploy3. 删除 deploy&#xff0c;这时候相应的 pod 就没了 kubectl delete deploy nginx4. 虽然删掉了 Pod&#xff0c;但是这是时候还有 service&#xff0c…

轻松搭建FPGA开发环境:第三课——Vivado 库编译与设置说明

工欲善其事必先利其器&#xff0c;很多人想从事FPGA的开发&#xff0c;但是不知道如何下手。既要装这个软件&#xff0c;又要装那个软件&#xff0c;还要编译仿真库&#xff0c;网上的教程一大堆&#xff0c;不知道到底应该听谁的。所以很多人还没开始就被繁琐的开发环境搭建吓…

电子学会C/C++编程等级考试2021年06月(六级)真题解析

C/C++等级考试(1~8级)全部真题・点这里 第1题:逆波兰表达式 逆波兰表达式是一种把运算符前置的算术表达式,例如普通的表达式2 + 3的逆波兰表示法为+ 2 3。逆波兰表达式的优点是运算符之间不必有优先级关系,也不必用括号改变运算次序,例如(2 + 3) * 4的逆波兰表示法为* +…

智能优化算法应用:基于动物迁徙算法3D无线传感器网络(WSN)覆盖优化 - 附代码

智能优化算法应用&#xff1a;基于动物迁徙算法3D无线传感器网络(WSN)覆盖优化 - 附代码 文章目录 智能优化算法应用&#xff1a;基于动物迁徙算法3D无线传感器网络(WSN)覆盖优化 - 附代码1.无线传感网络节点模型2.覆盖数学模型及分析3.动物迁徙算法4.实验参数设定5.算法结果6.…

不同的葡萄酒瓶盖会影响葡萄酒饮用的体验

首先&#xff0c;不同的葡萄酒瓶盖会影响我们找到想要喝的葡萄酒的难易程度。螺旋盖、Zork瓶塞和起泡酒“蘑菇形瓶塞”赢得了直接的满足感&#xff0c;它们只需要拔瓶塞不需要开瓶器。来自云仓酒庄品牌雷盛红酒分享对于所有其他的酒瓶封口&#xff0c;我们都需要一个工具来打开…