SpringBoot通过自定义注解整合Redisson实现分布式锁(单机+集群模式)

news2025/4/22 3:09:47
😊 @ 作者: 一恍过去
💖 @ 主页: https://blog.csdn.net/zhuocailing3390
🎊 @ 社区: Java技术栈交流
🎉 @ 主题: SpringBoot通过自定义注解整合Redisson实现分布式锁
⏱️ @ 创作时间: 2023年5月13日

目录

  • 概述
  • RedLock 机制
  • Watch Dog机制
  • 代码实现
    • 引入POM
    • Yaml配置(redis部分)
    • RedissonConfigProperties
    • RedissonConfig(配置redission)
    • RedisLock(自定义注解)
    • RedisLockAspect(定义切面)
    • 开启自动代理
    • 使用
    • 测试

概述

在使用分布式锁时,如果单节点的Redis发生故障,则整个业务的分布式锁都将无法使用。
如果是主从模式或者集群模式下的Redis发生故障,由于Master 节点是独立的数据不同步,在主从同步的期间,Master 节点发生故障,即使Slaver 节点被选举为 Master 节点,分布式锁的Key信息可能就会丢失,锁失效的情况。

RedLock 机制

对于Redis集群模式下,为了解决Master 节点发生故障带来的问题,通过Redisson封装的RedLock 可以进行解决该问题;

机制流程如下:
1、依次向 N 个 Redis 服务发出请求,用能够保证全局唯一的 value 申请锁 key;
2、如果从 N/2+1 个 redis 服务中都获取锁成功,那么,本次分布式锁的获取被视为成功,否则视为获取锁失败。
3、如果获取锁失败,或执行达到 超时时间,则向所有 Redis 服务都发出解锁请求。

Watch Dog机制

分布式事务锁最常见的一个问题就是如果已经获取到锁的 client 在 TTL 时间内没有完成竞争资源的处理,而此时锁会被自动释放,造成竞争条件的发生。

这种情况如果让 client 端设置定时任务自动延长锁的占用时间,会造成 client 端逻辑的复杂和冗余。

redisson 在实现的过程中,自然也考虑到了这一问题,redisson 提供了一个“看门狗”的可选特性,并且增加了 lockWatchdogTimeout 配置参数,看门狗线程会自动在 lockWatchdogTimeout 超时后顺延锁的占用时间,从而避免上述问题的发生。

但是,由于看门狗作为独立线程存在,对于性能有所影响,如果并非是处理高度竞争且处理时长不固定的特殊资源,那么并不建议启用 redisson 的看门狗特性。

代码实现

引入POM

<dependencies>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-data-redis</artifactId>
        </dependency>
        <!-- springboot整合redisson -->
        <dependency>
            <groupId>org.redisson</groupId>
            <artifactId>redisson-spring-boot-starter</artifactId>
            <version>3.13.6</version>
        </dependency>
        <dependency>
            <groupId>org.apache.commons</groupId>
            <artifactId>commons-pool2</artifactId>
        </dependency>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-web</artifactId>
        </dependency>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-aop</artifactId>
        </dependency>
        <dependency>
            <groupId>org.projectlombok</groupId>
            <artifactId>lombok</artifactId>
            <version>1.18.8</version>
        </dependency>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-configuration-processor</artifactId>
        </dependency>
    </dependencies>

Yaml配置(redis部分)

spring:
  #redis配置
  redis:
    # Redis数据库索引(默认为0)
    database: 1
    # 连接地址
    host: 162.14.115.18
    #端口号
    port: 6379
    ##连接超时时间
    timeout: 3600
    #密码
    password: 123456
    #    集群模式
    #    cluster:
    #      nodes: 162.14.115.18:6379,162.14.115.18:6379
    lettuce:
      pool:
        # 连接池最大连接数(使用负值表示没有限制)
        max-active: 8
        # 连接池最大阻塞等待时间(使用负值表示没有限制)
        max-wait: -1ms
        # 连接池中的最大空闲连接
        max-idle: 8
        # 连接池中的最小空闲连接
        min-idle: 1
      #关闭超时
      shutdown-timeout: 500ms
#    哨兵模式
#    sentinel:
#      master: master
#      nodes: 162.14.115.18:6379,162.14.115.18:6379

RedissonConfigProperties


@Component
@ConfigurationProperties(prefix = "spring.redis")
@Data
public class RedissonConfigProperties {
    private String host;
    private String port;
    private String password;
    private Cluster cluster;
    private Integer timeout;
    private Sentinel sentinel;

    @Data
    public static class Sentinel {
        private String master;

        private List<String> nodes;

        public List<String> getNodes() {
            return nodes;
        }

        public void setNodes(List<String> nodes) {
            this.nodes = nodes;
        }
    }

    @Data
    public static class Cluster {
        private List<String> nodes;

        public List<String> getNodes() {
            return nodes;
        }

        public void setNodes(List<String> nodes) {
            this.nodes = nodes;
        }
    }
}

RedissonConfig(配置redission)

@Configuration
public class RedissonConfig {

    @Resource
    private RedissonConfigProperties redissonConfigProperties;

    @Bean
    public RedissonClient redissonClient() {
        Config config = new Config();

        String redisPort = redissonConfigProperties.getPort();
        Integer redisTimeout = redissonConfigProperties.getTimeout();
        String redisPassword = redissonConfigProperties.getPassword();
        RedissonConfigProperties.Cluster cluster = redissonConfigProperties.getCluster();
        RedissonConfigProperties.Sentinel sentinel = redissonConfigProperties.getSentinel();

        // 集群
        if (cluster != null && !CollectionUtils.isEmpty(cluster.getNodes())) {
            List<String> nodes = cluster.getNodes();
            List<String> collect = nodes.stream().map(x -> x = x.startsWith("redis://") ? x : "redis://" + x).collect(Collectors.toList());
            ClusterServersConfig serverConfig = config.useClusterServers()
                    .setScanInterval(2000)
                    .addNodeAddress(collect.toArray(new String[collect.size()]));
            if (StringUtils.isNotBlank(redisPassword)) {
                serverConfig.setPassword(redisPassword);
            }
        }
        //哨兵
        else if (sentinel != null && !CollectionUtils.isEmpty(sentinel.getNodes())) {
            List<String> nodes = sentinel.getNodes();
            List<String> collect = nodes.stream().map(x -> x = x.startsWith("redis://") ? x : "redis://" + x).collect(Collectors.toList());
            SentinelServersConfig serverConfig = config.useSentinelServers()
                    .setMasterName(sentinel.getMaster())
                    .addSentinelAddress(collect.toArray(new String[collect.size()]))
                    .setTimeout(redisTimeout);
            if (StringUtils.isNotBlank(redisPassword)) {
                serverConfig.setPassword(redisPassword);
            }
        } else {
            // 单机配置
            String redisHost = redissonConfigProperties.getHost();
            redisHost = redisHost.startsWith("redis://") ? redisHost : "redis://" + redisHost;
            SingleServerConfig serverConfig = config.useSingleServer()
                    .setAddress(redisHost + ":" + redisPort)
                    .setTimeout(redisTimeout);

            if (StringUtils.isNotBlank(redisPassword)) {
                serverConfig.setPassword(redisPassword);
            }
        }
        return Redisson.create(config);
    }
}

RedisLock(自定义注解)

@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
@Documented
public @interface RedisLock {
    /**
     * 上锁的方法中,key所在参数的位置,索引从0开始
     *
     * @return
     */
    int keyNum();

    /**
     * 上锁时长,默认设置时间
     *
     * @return
     */
    long lockTime() default -1;

    /**
     * 尝试时间,设置时间内通过自旋一致尝试获取锁,默认0ms 通常时间要小于lockTime时间
     *
     * @return
     */
    long tryTime() default 0;
}

RedisLockAspect(定义切面)

@Aspect
@Component
@Slf4j
public class RedisLockAspect {

    @Resource
    private RedissonClient redissonClient;

    private static final ThreadLocal<RLock> LOCK_THREAD = new ThreadLocal<>();

    @Pointcut("@annotation(lhz.lx.aspect.RedisLock)")
    public void lockPoint() {

    }

    /**
     * 环绕通知,调用目标方法
     */
    @Around("lockPoint()")
    public Object doAround(ProceedingJoinPoint proceedingJoinPoint) throws Throwable {
        // 记录方法执行开始时间
        long startTime = System.currentTimeMillis();

        Object[] args = proceedingJoinPoint.getArgs();
        if (args.length <= 0) {
            throw new RuntimeException("keyName不存在!");
        }
        String[] argNames = ((CodeSignature) proceedingJoinPoint.getSignature()).getParameterNames();

        Signature signature = proceedingJoinPoint.getSignature();
        MethodSignature methodSignature = (MethodSignature) signature;
        Method method = methodSignature.getMethod();
        RedisLock lock = method.getAnnotation(RedisLock.class);

        int keyNum = lock.keyNum();
        if (!"keyName".equals(argNames[keyNum])) {
            throw new RuntimeException("keyName不存在!");
        }
        String key = args[keyNum].toString();
        long lockTime = lock.lockTime();
        long tryTime = lock.tryTime();

        log.info("分布式锁上锁,key:{},lockTime:{}", key, lockTime);
        RLock clientLock = redissonClient.getLock(key);
        // 尝试加锁,最多等待 tryTime 毫秒,上锁以后 lockTime 毫秒自动解锁
        // 如果lockTime不设置,则会启动看门狗机制(默认30S),每10S会自动续锁
        boolean locked = clientLock.tryLock(tryTime, lockTime, TimeUnit.MILLISECONDS);
        if (!locked) {
            log.error("上锁失败");
            // 如果为了不影响业务,可以不抛出异常,继续向下执行
            throw new RuntimeException("上锁失败!");
        }
        clientLock.lock(lockTime, TimeUnit.MILLISECONDS);
        log.info("分布式锁上锁成功,key:{},lockTime:{}", key, lockTime);
        LOCK_THREAD.set(clientLock);
        // 调用目标方法
        return proceedingJoinPoint.proceed();
    }

    /**
     * 处理完请求后执行
     *
     * @param joinPoint 切点
     */
    @AfterReturning(value = "lockPoint()", returning = "jsonResult")
    public void doAfterReturning(JoinPoint joinPoint, Object jsonResult) {
        handleData();
    }

    /**
     * 拦截异常操作
     *
     * @param joinPoint 切点
     * @param e         异常
     */
    @AfterThrowing(value = "lockPoint()", throwing = "e")
    public void doAfterThrowing(JoinPoint joinPoint, Exception e) {
        handleData();
    }

    private void handleData() {
        RLock clientLock = LOCK_THREAD.get();
        if (clientLock != null) {
            try {
                log.info("任务执行完成,当前锁状态:{}", clientLock.isLocked());
                // 无需判断锁是否存在,直接调用unlock
                clientLock.unlock();
            } catch (Exception exception) {
                exception.printStackTrace();
            } finally {
                LOCK_THREAD.remove();
            }
        }
    }
}

开启自动代理

因为说明AOP无法拦截类内部的方法之间的调用,需要对启动类加上@EnableAspectJAutoProxy配置,代码如下:

@SpringBootApplication
@EnableAspectJAutoProxy(exposeProxy = true)
public class RedisDemoApplication {

    public static void main(String[] args) {
        SpringApplication.run(RedisDemoApplication.class, args);
    }
}

使用

注意: 在调用使用分布式锁的方法时

TestController:

@RestController
@RequestMapping
@Slf4j
public class TestController {

    @Resource
    private RedisService redisService;

    @GetMapping(value = "/testLock")
    public String testLock() {	
        String key = "testKey";
//        String key = UUID.randomUUID().toString();
        redisService.testLock(key);
        return "success";
    }
}

RedisService :

public interface RedisService {
    /**
     * 方法直接调用使用锁
     *
     * @param keyName
     */
    void testLock(String keyName);
}

RedisServiceImpl:

@Service
@Slf4j
public class RedisServiceImpl implements RedisService{
    /**
     * 方法直接调用使用锁
     */
    @Override
    @RedisLock(keyNum = 0, lockTime = 1000, tryTime = 500)
    public void testLock(String keyName) {
        log.info("方法直接调用使用锁");
        try {
            // 如果业务执行过长
//            Thread.sleep(3200);
            System.out.println("加锁成功,执行业务逻辑");
        } catch (Exception e) {
            e.printStackTrace();
        }
    }
}

测试

测试一:
正常上锁情况
在这里插入图片描述

测试二:
设置tryTime,并且tryTime小于lockTime;当这样配置时,在第一个线程没有结束时,第二个线程,超过tryTime就会出现上锁失败;

修改代码如下:

    @RedisLock(keyNum = 0,lockTime = 3000,tryTime = 2000)
    public void testLock(String keyName) throws InterruptedException {
        log.info(keyName);
         TimeUnit.SECONDS.sleep(5);
    }

快速请求两次接口,截图如下:
在这里插入图片描述
通过截图可以看到,在第一个线程上锁后,过了2000ms出现了上锁失败的提示;

测试三:
设置tryTime,并且tryTime大于lockTime;当这样配置时,不会出现上锁失败,并且第二个线程会一直等到第一个线程结束;
修改代码如下:

    @RedisLock(keyNum = 0,lockTime = 3000,tryTime = 4000)
    public void testLock(String keyName) throws InterruptedException {
        log.info(keyName);
         TimeUnit.SECONDS.sleep(5);
    }

快速请求两次接口,截图如下:
在这里插入图片描述
通过截图可以看到,在第一个线程上锁后,超过了3000ms后,第二个线程开始上锁成功

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

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

相关文章

低代码信创开发核心技术(一):基于Vue.js的描述依赖渲染DDR实现模型驱动的组件

前言 随着数字化转型的不断发展&#xff0c;低代码开发平台已成为企业快速建立自己的应用程序的首选方案。然而&#xff0c;实现这样一个平台需要具备高效、灵活和可定制化的能力。这正是基于描述依赖渲染&#xff08;Description dependency rendering&#xff09;所实现的。…

Jenkins 发布到 windows 主机

由于一些原因, 最近程序部署在windows 服务器上比较多, 本文以将前端程序发布到windows server 2016 为例进行介绍 流程 需考虑网络联通性, 此处我的目标服务器没有公网固定IP, 故采用vpn 工具组网 步骤: 一、使用执行SSH脚本的方式 1. 在jenkins 服务器及目标机器上安装 VP…

Windows终端中文乱码,查看系统默认编码及修改系统默认编码

最近windows升级之后出现一个问题&#xff0c;终端出现了乱码&#xff0c;导致启动程序启动不起来。 window系统如何查看系统默认编码 方式一&#xff1a; 在Windows平台下&#xff0c;winr 打开运行—》输入cmd进入cmd窗口&#xff0c;输入&#xff1a;chcp 可以得到操作系统…

会声会影2023是什么软件,会声会影和pr到底哪个好

图1是pr&#xff08;premiere&#xff09;平时大家可能会经常听到有人说会声会影2023&#xff0c;但是很多人都不知道这是什么软件。其实听它的名字就知道这是一款和声音、影像有关系的软件。下面&#xff0c;小编就来给大家具体介绍一下这款软件吧。 会声会影2023是什么软件&…

微信客服对接-唯一客服系统文档中心

微信客服官方网址为&#xff1a;https://kf.weixin.qq.com &#xff0c;可以在微信内、外各个场景中接入微信客服&#xff0c;提供一致的咨询体验&#xff0c;企业可通过API接口回复消息&#xff0c;做好客户服务。 微信客服或者也可以叫企业微信客服&#xff0c;可通过API接口…

CSS平面转换和渐变

01-平面转换 简介 作用&#xff1a;为元素添加动态效果&#xff0c;一般与过渡配合使用 概念&#xff1a;改变盒子在平面内的形态&#xff08;位移、旋转、缩放、倾斜&#xff09; 平面转换也叫 2D 转换&#xff0c;属性是 transform 平移 transform: translate(X轴移动距…

IM即时通讯系统[SpringBoot+Netty]——梳理(三)

文章目录 七、打通业务服务器与IM服务器多端同步1、负载均衡策略—随机模式2、负载均衡策略—轮询模式3、负载均衡策略—一致性Hash4、配置负载均衡策略5、使用Apache—HttpClient封装http请求工具6、用户资料变更、群组模块回调7、数据多端同步8、封装查询用户Session工具类9、…

两分钟速览谷歌2023IO大会:AI军备竞争,全线出击

大家好&#xff0c;我是可夫小子&#xff0c;关注AIGC、读书和自媒体。解锁更多ChatGPT、AI绘画玩法。加&#xff1a;keeepdance&#xff0c;备注&#xff1a;chatgpt&#xff0c;拉你进群。 5月10日周三&#xff0c;谷歌举办了年度开发者大会Google I/O 2023&#xff0c;在会上…

【ARMv8 编程】A64 内存访问其他指令

A64 内存访问其他指令包括浮点和 NEON 标量加载存储指令、访问多个内存位置指令、非特权访问指令、预取内存指令、非临时加载存储对指令、内存屏障和栅栏指令、同步原语等。 一、浮点和 NEON 标量加载存储指令 加载和存储指令也可以访问浮点/NEON 寄存器。这里&#xff0c;大…

k8基础知识

总述 在构成扁平化网络的基础上实现Pod编排&#xff08;控制、管理&#xff09;、调度&#xff0c;再构成服务&#xff1b;对服务的管理有所欠缺&#xff1b;可以说k8s重点解决资源的问题 服务管理、应用管理&#xff1b;istio重点解决服务的问题 功能 开源、动态伸缩、负载…

爱奇艺2020校招Java方向笔试题(第一场)

1.计算下列程序的时间复杂度&#xff08;&#xff09; for (i1;i<n;i)for(j1;j<m;j){a1,a2,a3,a4};A.O(n) B.O(mn) C.O(m) D.O(1) 答案:B 2.求递归方程T(n)4T(n/2)n 的解 ( ) A.O(n) B.O(n^2) C.O(n^3) D.O(logn) 答案:B 用Mater公式计算 3.下列关于动态规划算法说法错…

RCWA包:光学模拟的强大工具——用于计算光子 晶体R/T 光谱 的严格耦合波分析

目录 RCWA包&#xff1a;光学模拟的强大工具 RCWA包的功能 如何开始使用RCWA包 RCWA包的特性 RCWA包的应用示例 示例代码 基本的光学概念介绍。 1. 反射率、透射率和散射率 2. 衍射和衍射光栅 3. 光子晶体 4. 布拉格镜 5. 垂直腔面发射激光器&#xff08;VCSEL&…

芒果改进YOLOv8系列:改进特征融合网络 BiFPN 结构,融合更多有效特征

芒果改进YOLOv8系列:改进特征融合网络 BiFPN 结构,融合更多有效特征 在这篇文章中,将 BiFPN 思想加入到 YOLOv8 结构中 该版本为高效简洁版,涨点多、还速度快(实际效果反馈) 本篇博客 不占用 高阶专栏的总篇数计划中 文章目录 一、BiFPN 论文理论二、效果反馈(涨点)…

华为OD机试真题 Java 实现【任务总执行时长】【2023Q1 100分】

一、题目描述 任务编排服务负责对任务进行组合调度。 参与编排的任务有两种类型&#xff0c;其中一种执行时长为taskA&#xff0c;另一种执行时长为taskB。 任务一旦开始执行不能被打断&#xff0c;且任务可连续执行。服务每次可以编排num个任务。请编写一个方法&#xff0c…

Git常用操作:基础命令、生成公钥、webhook同步

这里写目录标题 下载安装GIT基础命令克隆初始化与仓库建立连接下拉代码提交代码清空本地缓存 本地生成GIT公钥webhook&#xff08;本地-码云-服务器代码同步&#xff09; 下载安装GIT 此处只介绍windows系统下的安装&#xff0c;linux一般都是自带git&#xff08;自行百度&…

linux跑python控制台输出日志无内容或者断掉

问题描述 训练时候的输出日志要么没有&#xff0c;要么就是输出了一点点就没有了&#xff08;突然不输出内容了&#xff09;&#xff0c;记得之前也出现过训练中途突然日志不打印了&#xff0c;当时以为服务器原因可能被挤掉了。但这次是还没开始并且试过很多次了。 所用语句…

85. `if __name__ == “__main__“`的作用和原理(适合小白)

85. if __name__ "__main__"的作用和原理&#xff08;适合小白&#xff09; 文章目录 85. if __name__ "__main__"的作用和原理&#xff08;适合小白&#xff09;1. 代码体验2. __name__的作用2.1 模块知识回顾2.2 __name__的作用 3. if __name__ "…

水果手机SJ流程

注册教程 第一步:注册新邮箱。 首先&#xff0c;你需要一个邮箱账号&#xff0c;国内126.QQ.163等都可以&#xff0c;只要你没有用它注册过AppleID。 第二步:注册。 复制链接到浏览器&#xff0c;打开进入appleID注册界面:https://appleid.apple.com/account#&#xff01;&…

采用UWB(超宽频)技术开发的java版智慧工厂定位系统源码

室内定位系统源码&#xff0c;采用UWB定位技术开发的智慧工厂定位系统源码 技术架构&#xff1a;单体服务 硬件&#xff08;UWB定位基站、卡牌&#xff09; 开发语言&#xff1a;java 开发工具&#xff1a;idea 、VS Code 前端框架&#xff1a;vue 后端框架&#xff1a;s…

Linux内核(十七)Input 子系统详解 IV —— 配对的input设备与input事件处理器 input_register_handle

文章目录 input_handle结构体详解配对的input设备与input事件处理器实例input核心层对驱动层和事件层之间的框架建立流程图 本文章中与input子系统相关的结构体可参考input子系统结构体解析 input函数路径&#xff1a;drivers/input/input.c input_handle结构体详解 input_ha…