Spring Boot 基于Redisson实现注解式分布式锁

news2024/12/23 9:45:34

依赖版本

  • JDK 17
  • Spring Boot 3.2.0
  • Redisson 3.25.0

源码地址:Gitee

导入依赖

<properties>
    <redisson.version>3.25.0</redisson.version>
</properties>

<dependencies>
    <dependency>
        <groupId>org.projectlombok</groupId>
        <artifactId>lombok</artifactId>
    </dependency>
    <dependency>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-web</artifactId>
    </dependency>
    <dependency>
        <groupId>org.redisson</groupId>
        <artifactId>redisson-spring-boot-starter</artifactId>
        <version>${redisson.version}</version>
    </dependency>
    <dependency>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-test</artifactId>
        <scope>test</scope>
    </dependency>
    <dependency>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-aop</artifactId>
    </dependency>
    <dependency>
        <groupId>org.apache.commons</groupId>
        <artifactId>commons-lang3</artifactId>
    </dependency>
</dependencies>

配置文件

# application.yml
server:
  port: 8080

spring:
  # ======== Redis配置 ========
  redis:
    redisson:
      file: classpath:redisson.yaml
# redisson.yaml
# 编码。默认值: org.redisson.codec.JsonJacksonCodec
codec: !<org.redisson.codec.Kryo5Codec> {}
# 线程池数量。默认值: 当前处理核数量 * 2
threads: 16
# Netty线程池数量。默认值: 当前处理核数量 * 2
nettyThreads: 32
# 传输模式。默认值: NIO
transportMode: "NIO"
# 监控锁的看门狗超时,单位:毫秒。默认值: 30000
lockWatchdogTimeout: 30000
# 是否保持订阅发布顺序。默认值: true
keepPubSubOrder: true

# Redisson 单实例配置
singleServerConfig:
  # 节点地址。格式:redis://host:port
  address: "redis://127.0.0.1:6379"
  # 密码。默认值: null
  password: null
  # 数据库编号。默认值: 0
  database: 0
  # 客户端名称(在Redis节点里显示的客户端名称)。默认值: null
  clientName: null
  # 连接超时,单位:毫秒。默认值: 10000
  connectTimeout: 10000
  # 命令等待超时,单位:毫秒。默认值: 3000
  timeout: 3000
  # 命令失败重试次数。默认值: 3
  retryAttempts: 3
  # 命令重试发送时间间隔,单位:毫秒。默认值: 1500
  retryInterval: 1500
  # 最小空闲连接数。默认值: 32
  connectionMinimumIdleSize: 24
  # 连接池大小。默认值: 64
  connectionPoolSize: 64
  # 单个连接最大订阅数量。默认值: 5
  subscriptionsPerConnection: 5
  # 发布和订阅连接的最小空闲连接数。默认值: 1
  subscriptionConnectionMinimumIdleSize: 1
  # 发布和订阅连接池大小。默认值: 50
  subscriptionConnectionPoolSize: 50
  # DNS监测时间间隔,单位:毫秒。默认值: 5000
  dnsMonitoringInterval: 5000
  # 连接空闲超时,单位:毫秒。默认值: 10000
  idleConnectionTimeout: 10000

Redisson 锁简单使用

public void lock(String key) throws InterruptedException {
    RLock lock = redissonClient.getLock(key);
    log.info("[Redisson 分布式] 获取锁 KEY :{}", key);
    boolean lockSuccess = lock.tryLock(500, TimeUnit.MILLISECONDS);
    if (!lockSuccess) {
        throw new RuntimeException("获取锁失败");
    }
    try {
        //执行锁内的代码逻辑
    } finally {
        if (lock.isLocked() && lock.isHeldByCurrentThread()) {
            lock.unlock();
            log.info("[Redisson 分布式锁] 释放锁 KEY :{}", key);
        }
    }
}

Redisson锁的使用很方便,提供了很多的便携方法。但是在每个需要使用锁的地方都去写这样的模板代码有点“麻烦”,所以对Redisson锁的使用进行一个简单的封装,让在开发中使用更顺手

Redisson 锁工具类封装

RedisLockService 锁工具类

采用函数式接口,可在使用时对业务代码精准落锁,减少被锁的时间,提升系统性能。

package com.yiyan.study.utils.redis;

import lombok.RequiredArgsConstructor;
import lombok.SneakyThrows;
import lombok.extern.slf4j.Slf4j;
import org.redisson.api.RLock;
import org.redisson.api.RedissonClient;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Component;

import java.util.concurrent.TimeUnit;
import java.util.function.Supplier;

/**
 * 基于Redis Redisson 分布式锁工具类
 * 
 * @createDate 2022-12-21
 */
@Slf4j
@Component
@RequiredArgsConstructor(onConstructor_ = @Autowired)
public class RedisLockService {

    private final RedissonClient redissonClient;

    // 编程式Redisson锁
    public <T> T executeWithLockThrows(String key, int waitTime, TimeUnit unit, SupplierThrow<T> supplier) throws Throwable {
        RLock lock = redissonClient.getLock(key);
        log.info("[Redisson 分布式] 获取锁 KEY :{}", key);
        boolean lockSuccess = lock.tryLock(waitTime, unit);
        if (!lockSuccess) {
            throw new RuntimeException("获取锁失败");
        }
        try {
            return supplier.execute();//执行锁内的代码逻辑
        } finally {
            if (lock.isLocked() && lock.isHeldByCurrentThread()) {
                lock.unlock();
                log.info("[Redisson 分布式锁] 释放锁 KEY :{}", key);
            }
        }
    }

    @SneakyThrows
    public <T> T executeWithLock(String key, int waitTime, TimeUnit unit, Supplier<T> supplier) {
        return executeWithLockThrows(key, waitTime, unit, supplier::get);
    }

    public <T> T executeWithLock(String key, Supplier<T> supplier) {
        return executeWithLock(key, -1, TimeUnit.MILLISECONDS, supplier);
    }

    /**
     * 函数式接口,用于执行锁内的代码逻辑
     */
    @FunctionalInterface
    public interface SupplierThrow<T> {
        T execute() throws Throwable;
    }
}

使用示例

public void lockLine() {
    // 模拟查询等不需要锁的操作
    ThreadUtil.sleep(2000L);
    redisLockService.executeWithLock("lockLine", 500, TimeUnit.MILLISECONDS,
                                     () -> {
                                         // 模拟上锁的数据操作
                                         ThreadUtil.sleep(200L);
                                         return null;
                                     });
}

Redisson 锁注解

在开发中,有些方法的功能就是原子性的,比如订单状态更新这样方法,此时方法内的代码都需要被锁住,所以可以采用注解的方式,来对需要加锁的业务进行上锁,避免编写重复冗余的代码。

RedissonLock注解

import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;
import java.util.concurrent.TimeUnit;

/**
 * Redisson 分布式锁注解
 * 
 * @createDate 2023-09-18 07:17
 */
@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
public @interface RedissonLock {
    /**
     * key的前缀,默认取方法全限定名,除非我们在不同方法上对同一个资源做分布式锁,就自己指定
     *
     * @return key的前缀
     */
    String prefixKey() default "";

    /**
     * springEl 表达式
     *
     * @return 表达式
     */
    String key() default "";

    /**
     * 等待锁的时间,默认-1,不等待直接失败,redisson默认也是-1
     *
     * @return 单位秒
     */
    int waitTime() default -1;

    /**
     * 等待锁的时间单位,默认毫秒
     *
     * @return 单位
     */
    TimeUnit unit() default TimeUnit.MILLISECONDS;
}

注解切面

import com.yiyan.study.utils.SpElUtils;
import com.yiyan.study.utils.redis.RedisLockService;
import com.yiyan.study.utils.redis.annotation.RedissonLock;
import jakarta.annotation.Resource;
import jodd.util.StringUtil;
import lombok.extern.slf4j.Slf4j;
import org.aspectj.lang.ProceedingJoinPoint;
import org.aspectj.lang.annotation.Around;
import org.aspectj.lang.annotation.Aspect;
import org.aspectj.lang.annotation.Pointcut;
import org.aspectj.lang.reflect.MethodSignature;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.core.annotation.Order;
import org.springframework.stereotype.Component;

import java.lang.reflect.Method;
import java.util.concurrent.TimeUnit;

/**
 * Redisson 分布式锁切面
 *
 * @createDate 2023-09-18 07:17
 */
@Slf4j
@Aspect
@Component
// 确保比事务注解先执行,分布式锁在事务外
@Order(0)
public class RedissonLockAspect {

    @Resource
    private RedisLockService redisLockService;

    @Pointcut("@annotation(com.yiyan.study.utils.redis.annotation.RedissonLock)")
    public void lockPointcut() {
    }

    @Around("lockPointcut()")
    public Object around(ProceedingJoinPoint joinPoint) throws Throwable {
        Method method = ((MethodSignature) joinPoint.getSignature()).getMethod();
        RedissonLock redissonLock = method.getAnnotation(RedissonLock.class);
        // 默认方法限定名+注解排名(可能多个)
        String prefix = StringUtil.isBlank(redissonLock.prefixKey()) ? SpElUtils.getMethodKey(method) : redissonLock.prefixKey();
        String key = prefix + ":" + SpElUtils.parseSpEl(method, joinPoint.getArgs(), redissonLock.key());
        int waitTime = redissonLock.waitTime();
        TimeUnit timeUnit = redissonLock.unit();
        return redisLockService.executeWithLockThrows(key, waitTime, timeUnit, joinPoint::proceed);
    }
}

使用示例

@RedissonLock(prefixKey = "lockFunc", waitTime = 500)
public void lockFunc() {
    // 模拟查询等不需要锁的操作
    ThreadUtil.sleep(2000L);
    // 模拟上锁的数据操作
    ThreadUtil.sleep(200L);
}

Redisson 锁测试

编写测试接口

import com.yiyan.study.utils.redis.RedisLockService;
import com.yiyan.study.utils.redis.annotation.RedissonLock;
import jakarta.annotation.Resource;
import jodd.util.ThreadUtil;
import lombok.extern.slf4j.Slf4j;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;

import java.util.concurrent.TimeUnit;

@RestController
@RequestMapping("/")
@Slf4j
public class RedisLockController {

    @Resource
    private RedisLockService redisLockService;

    @GetMapping("/lock_func")
    @RedissonLock(prefixKey = "lockFunc", waitTime = 500)
    public void lockFunc() {
        // 模拟查询等不需要锁的操作
        ThreadUtil.sleep(2000L);
        // 模拟上锁的数据操作
        ThreadUtil.sleep(200L);
    }


    @GetMapping("/lock_line")
    public void lockLine() {
        // 模拟查询等不需要锁的操作
        ThreadUtil.sleep(2000L);
        redisLockService.executeWithLock("lockLine", 500, TimeUnit.MILLISECONDS,
                () -> {
                    // 模拟上锁的数据操作
                    ThreadUtil.sleep(200L);
                    return null;
                });
    }
}

测试

使用AB测试工具,10个请求,并发为5,模拟总业务时常2.5s:

springboot3-redisson-lock-测试

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

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

相关文章

OpenHarmony南向之Camera简述

Camera驱动框架 该驱动框架模型内部分为三层&#xff0c;依次为HDI实现层、框架层和设备适配层&#xff1a; HDI实现层&#xff1a;实现OHOS&#xff08;OpenHarmony Operation System&#xff09;相机标准南向接口。框架层&#xff1a;对接HDI实现层的控制、流的转发&#x…

ECMAScript 6 - 通过Promise输出题理解Promise

1 题目(1) 题目背景&#xff1a;分享洛千陨 珍藏题 const p1 () > (new Promise((resolve, reject) > {console.log(1);let p2 new Promise((resolve, reject) > {console.log(2);const timeOut1 setTimeout(() > {console.log(3);resolve(4);}, 0)resolve(5)…

js for和forEach 跳出循环 替代方案

1 for循环跳出 for(let i0;i<10;i){if(i5){break;}console.log(i) }在函数中也可以return跳出循环 function fn(){for(let i0;i<10;i){if(i5){return;}console.log(i)} } fn()for ... of效果同上 2 forEach循环跳出 break会报错 [1,2,3,4,5,6,7,8,9,10].forEach(i>…

Evidential Deep Learning to Quantify Classification Uncertainty

本片文章发表于NeurIPS 2018。 文章链接&#xff1a;https://arxiv.org/abs/1806.01768 一、概述 近年来&#xff0c;神经网络在不同领域取得了革命性的进步&#xff0c;尤其是在dropout、normalization以及skip connection等方法被提出之后&#xff0c;撼动了整个机器学习领…

新火种AI|AI正在让汽车成为“消费电子产品”

作者&#xff1a;一号 编辑&#xff1a;小迪 AI正在让汽车产品消费电子化 12月28日&#xff0c;铺垫许久的小米汽车首款产品——小米SU7正式在北京亮相。命里注定要造“电车”的雷军&#xff0c;在台上重磅发布了小米的五大自研核心技术。在车型设计、新能源技术以及智能科技…

算法专题四:前缀和

前缀和 一.一维前缀和(模板)&#xff1a;1.思路一&#xff1a;暴力解法2.思路二&#xff1a;前缀和思路 二. 二维前缀和(模板)&#xff1a;1.思路一&#xff1a;构造前缀和数组 三.寻找数组的中心下标&#xff1a;1.思路一&#xff1a;前缀和 四.除自身以外数组的乘积&#xff…

002文章解读与程序——中国电机工程学报EI\CSCD\北大核心《计及源荷不确定性的综合能源生产单元运行调度与容量配置两阶段随机优化》已提供下载资源

&#x1f446;&#x1f446;&#x1f446;&#x1f446;&#x1f446;&#x1f446;&#x1f446;&#x1f446;&#x1f446;&#x1f446;&#x1f446;&#x1f446;&#x1f446;&#x1f446;&#x1f446;&#x1f446;&#x1f446;&#x1f446;下载资源链接&#x1f4…

nacos入门篇001-安装与启动

1、下载zip包 我这里下载的是版本2.2.0 Nacos 快速开始 2、修改配置文件 2.1集群模式修改成单例模式 vi startup.sh 2.2 修改数据库配置信息 3、初始化数据库 3.1 创建db名称&#xff1a;db_nacos 3.2 执行mysql-schema.sql 3.3 执行完截图&#xff1a; 4、运行脚本启动 …

leetcode贪心算法题总结(二)

本节目录 1.最长回文串2.增减字符串匹配3.分发饼干4.最优除法5.跳跃游戏II6.跳跃游戏7.加油站8.单调递增的数字9.坏了的计算器 1.最长回文串 最长回文串 class Solution { public:int longestPalindrome(string s) {//计数一&#xff1a;用数组模拟哈希表int hash[127] {0}…

SimpleCG小游戏开发系列(2)--贪吃蛇

一、前言 在之前的C语言小游戏开发系列我们已经介绍了扫雷游戏的开发&#xff0c;本篇我们继续此系列第二篇&#xff0c;同样是比较简单但好玩的一个游戏--贪吃蛇。因为有了之前的游戏框架&#xff0c;我们只需要直接搬来原来的框架即可&#xff0c;可以省去不少活。 先看看游…

命令行创建Vue项目

Vue项目创建 1. 打开UI界面 在命令行中&#xff0c;执行如下指令&#xff1a; vue ui 2. 打开项目管理器 3. 创建项目 创建项目的过程&#xff0c;需要联网进行&#xff0c;这可能会耗时比较长的时间&#xff0c;请耐心等待。 windows的命令行&#xff0c;容易卡顿&#xff0c…

使用Node Exporter采集主机数据

安装 Node Exporter 在 Prometheus 的架构设计中&#xff0c;Prometheus Server 并不直接服务监控特定的目标&#xff0c;其主要任务负责数据的收集&#xff0c;存储并且对外提供数据查询支持。因此为了能够能够监控到某些东西&#xff0c;如主机的 CPU 使用率&#xff0c;我们…

3D 渲染如何帮助电商促进销售?

在线工具推荐&#xff1a; 3D数字孪生场景编辑器 - GLTF/GLB材质纹理编辑器 - 3D模型在线转换 - Three.js AI自动纹理开发包 - YOLO 虚幻合成数据生成器 - 三维模型预览图生成器 - 3D模型语义搜索引擎 3D 渲染图像因其高转化率而成为亚马逊卖家的最新趋势。它是电子商务平…

Linux 线程安全 (1)

文章目录 线程互斥概念互斥实际使用互斥锁的原理死锁问题说明 线程互斥概念 执行流 执行流是指操作系统对进程或线程的调度和执行顺序。它决定了程序中的指令按照何种顺序被执行。 现阶段可以粗浅的理解为&#xff0c;执行流决定执行哪个线程或进程的代码(或者说执行流决定了…

MyBatis标签及其应用示例

MyBatis标签及其应用示例 1. select 1.1 标签属性 id唯一的标识符parameterType传给此语句的参数的全路径名或别名如&#xff1a;com.xxx.xxx.demo.entity.User或userresultType语句返回值类型或别名。如果是集合List&#xff0c;此处填写集合的泛型T&#xff0c;而不是集合…

人机交互中信息数量与质量

在人机交互中&#xff0c;信息的数量和质量都是非常重要的因素。 信息的数量指的是交互过程中传递的信息的多少。信息的数量直接影响到交互的效率和效果&#xff0c;如果交互中传递的信息量太少&#xff0c;可能导致交互过程中的信息不足&#xff0c;用户无法得到想要的结果或者…

js实时监听input输入框值的变化

实习日记之通过调用common chemistry的api接口实现输入keyword查找cas号和mw。做了一个简单的html网页&#xff0c;用到了ajax技术。比较简单&#xff0c;适合刚入门的宝学习参考。代码如下&#xff1a; <!DOCTYPE html> <html lang"en"> <head>&l…

面试算法78:合并排序链表

题目 输入k个排序的链表&#xff0c;请将它们合并成一个排序的链表。 分析&#xff1a;利用最小堆选取值最小的节点 用k个指针分别指向这k个链表的头节点&#xff0c;每次从这k个节点中选取值最小的节点。然后将指向值最小的节点的指针向后移动一步&#xff0c;再比较k个指…

cleanmymac这个软件怎么样?值不值得下载

cleanmymac是我必装的mac端清理软件&#xff0c;界面简洁好看&#xff0c;完美适配mac系统&#xff0c;文件清理的速度、精度都比较优秀&#xff0c;还是比较不错的呢。cleanmymac作为一款第三方清洁应用程序&#xff0c;具有专业完整的清理功能&#xff0c;包括释放内存、一键…

Halcon阈值处理的几种分割方法threshold/auto_threshold/binary_threshold/dyn_threshold

Halcon阈值处理的几种分割方法 文章目录 Halcon阈值处理的几种分割方法1. 全局阈值2. 基于直方图的自动阈值分割方法3. 自动全局阈值分割方法4. 局部阈值分割方法5. var_threshold算子6 . char_threshold 算子7. dual_threshold算子 在场景中选择物体或特征是图像测量或识别的重…