写个注解帮你净化使用分布式锁的重复操作

news2024/11/19 17:25:08

Redisson+Aop实现分布式锁

前言

简介

Aop的意义

AOP 旨在从业务逻辑中分离出来通用逻辑,切面实现了跨越多种类型和对象的关注点(例如事务管理、日志记录、权限控制)的模块化。

例子

就以这段代码为例子,这段代码总是回去获取锁之后在执行完解开锁,基本上使用redisson作为分布式锁的代码都会以下几个操作

  1. 创建锁
  2. 获取锁
  3. 执行方法
  4. 解锁
   public Map<String, List<Catelog2Vo>> getCatalogJsonFromDbWithRedissonLock() {

        //1、占分布式锁。去redis占坑
        //(锁的粒度,越细越快:具体缓存的是某个数据,11号商品) product-11-lock
        //RLock catalogJsonLock = redissonClient.getLock("catalogJson-lock");
        //创建读锁
        RReadWriteLock readWriteLock = redissonClient.getReadWriteLock("catalogJson-lock");

        RLock rLock = readWriteLock.readLock();

        Map<String, List<Catelog2Vo>> dataFromDb = null;
        try {
            rLock.lock();
            //加锁成功...执行业务
            dataFromDb = getDataFromDb();
        } finally {
            rLock.unlock();
        }
        return dataFromDb;

    }

思路

​ 重复的操作会让业务代码可读性变差,我们本着aop的思路 让业务代码专注于业务,来改造一下redisson锁获取值的方式优化,优化方式如下:

  1. 自定义注解 作用于方法上
  2. 用AOP来做redisson的获取锁和解锁还有存储redis的操作

代码实现

依赖

<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-data-redis</artifactId>
</dependency>
<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency>
    <groupId>org.apache.commons</groupId>
    <artifactId>commons-pool2</artifactId>
</dependency>
<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-aop</artifactId>
</dependency>
<dependency>
    <groupId>com.alibaba</groupId>
    <artifactId>fastjson</artifactId>
    <version>1.2.79</version>
</dependency>
<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-devtools</artifactId>
    <scope>runtime</scope>
    <optional>true</optional>
</dependency>
<dependency>
    <groupId>org.redisson</groupId>
    <artifactId>redisson-spring-boot-starter</artifactId>
    <version>3.17.7</version>
</dependency>
<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-configuration-processor</artifactId>
    <optional>true</optional>
</dependency>
<dependency>
    <groupId>org.projectlombok</groupId>
    <artifactId>lombok</artifactId>
    <optional>true</optional>
</dependency>

配置

Application.properties

# 应用名称
spring.application.name=AspectRedisson
# 应用服务 WEB 访问端口
server.port=8080

#redis配置
spring.redis.host=xxxxxx
spring.redis.password=admin

RedisConfig

配置序列化

/**
 * @package: com.hyc.aspectredisson.config.config
 * @className: RedisConfig
 * @author: 冷环渊 doomwatcher
 * @description: TODO
 * @date: 2022/11/15 19:21
 * @version: 1.0
 */
@Configuration
public class RedisConfig {
    @Bean
    public RedisTemplate<Object, Object> redisTemplate(
            RedisConnectionFactory redisConnectionFactory) {
        RedisTemplate<Object, Object> template = new RedisTemplate<>();
        //使用fastjson序列化
        FastJsonRedisSerializer fastJsonRedisSerializer = new FastJsonRedisSerializer(Object.class);
        // value值的序列化采用fastJsonRedisSerializer
        template.setValueSerializer(fastJsonRedisSerializer);
        template.setHashValueSerializer(fastJsonRedisSerializer);
        // key的序列化采用StringRedisSerializer
        template.setKeySerializer(new StringRedisSerializer());
        template.setHashKeySerializer(new StringRedisSerializer());
        template.setConnectionFactory(redisConnectionFactory);
        return template;
    }
}

redissonConfig

/**
 * @projectName: AspectRedisson
 * @package: com.hyc.aspectredisson.config
 * @className: redissonConfig
 * @author: 冷环渊 doomwatcher
 * @description: TODO
 * @date: 2022/11/15 19:15
 * @version: 1.0
 */
@Configuration
public class redissonConfig {
    @Bean
    public RedissonClient redissonClient() throws IOException {
        Config config = new Config();
        config.useSingleServer()
                //可以用"rediss://"来启用SSL连接
                .setAddress("redis://xxxxxx:6379")
                .setPassword("admin");
        return Redisson.create(config);
    }
}

自定义注解

作用在方法上,有一个前缀参数用于区别缓存名称

/**
 * @projectName: AspectRedisson
 * @package: com.hyc.aspectredisson.config.annotation
 * @className: AddLock
 * @author: 冷环渊 doomwatcher
 * @description: TODO
 * @date: 2022/11/15 20:29
 * @version: 1.0
 */
@Target({ElementType.METHOD})
@Retention(RetentionPolicy.RUNTIME)
public @interface addSectionLock {
    /**
     * 缓存key的前缀
     * @return
     */
    String prefix() default "cache";
}

切面类

​ 为方法上添加了注解的方法做环绕切面织入,实现有缓存就直接返回缓存数据,没有缓存就使用创建锁放入缓存中返回数据

/**
 * @author 冷环渊 Doomwatcher
 * @context: 处理切面环绕
 * @date: 2022/11/16 13:52
 */
@Component
@Aspect
public class RedissonLockAspect {

    @Autowired
    private RedisTemplate redisTemplate;

    @Autowired
    private RedissonClient redissonClient;

    //  切addSectionLock注解
    @SneakyThrows
    @Around("@annotation(com.hyc.aspectredisson.annotation.addSectionLock)")
    public Object cacheAroundAdvice(ProceedingJoinPoint joinPoint){
        //  声明一个对象
        Object object = new Object();
        //  在环绕通知中处理业务逻辑 {实现分布式锁}
        //  获取到注解,注解使用在方法上!
        MethodSignature signature = (MethodSignature) joinPoint.getSignature();
        addSectionLock AddLock = signature.getMethod().getAnnotation(addSectionLock.class);

        //  获取到注解上的前缀
        String prefix = AddLock.prefix(); // test

        //  方法传入的参数
        Object[] args = joinPoint.getArgs();

        //  方法名
        String MethodName = ((MethodSignature) joinPoint.getSignature()).getMethod().getName();

        //  组成缓存的key 需要前缀+方法传入的参数
        String key = prefix+ MethodName+Arrays.asList(args).toString();

        //  防止redis ,redisson 出现问题!
        try {
            //  从缓存中获取数据
            //  类似于 redisTemplate.opsForValue().get(KEY);
            object = cacheHit(key,signature);
            //  判断缓存中的数据是否为空!
            if (object==null){
                //  从数据库中获取数据,并放入缓存,防止缓存击穿必须上锁
                //  key+":lock"
                String lockKey = prefix + "lock";
                //  准备上锁
                RLock lock = redissonClient.getLock(lockKey);
                boolean result = lock.tryLock(RedisConst.LOCK_EXPIRE_PX1, RedisConst.LOCK_EXPIRE_PX2, TimeUnit.SECONDS);
                //  上锁成功
                if (result){
                    System.out.println("上锁成功"+result);
                    try {
                        //  表示执行方法体
                        object = joinPoint.proceed(joinPoint.getArgs());
                        //  判断object 是否为空
                        if (object==null){
                            //  防止缓存穿透
                            Object object1 = new Object();
                            redisTemplate.opsForValue().set(key, JSON.toJSONString(object1), RedisConst.KEY_TEMPORARY_TIMEOUT,TimeUnit.SECONDS);
                            //  返回数据
                            return object1;
                        }
                        //  放入缓存
                        redisTemplate.opsForValue().set(key, JSON.toJSONString(object),RedisConst.KEY_TIMEOUT,TimeUnit.SECONDS);
                        //  返回数据
                        return object;
                    } finally {
                        lock.unlock();
                    }
                }else{
                    //  上锁失败,睡眠自旋
                    Thread.sleep(1000);
                    return cacheAroundAdvice(joinPoint);
                    //  理想状态
                    //                  return object;
            }
                }
            return object;
            } catch (Throwable throwable) {
            throwable.printStackTrace();
        }
        //  如果出现问题数据库兜底
        return joinPoint.proceed(joinPoint.getArgs());
    }
    /**
     *  表示从缓存中获取数据
     * @param key 缓存的key
     * @param signature 获取方法的返回值类型
     * @return
     */
    private Object cacheHit(String key, MethodSignature signature) {
        //  通过key 来获取缓存的数据
        String strJson = (String) redisTemplate.opsForValue().get(key);
        //  表示从缓存中获取到了数据
        if (!StringUtils.isEmpty(strJson)){
            //  字符串存储的数据是什么?   就是方法的返回值类型
            Class returnType = signature.getReturnType();
            //  将字符串变为当前的返回值类型
            return JSON.parseObject(strJson,returnType);
        }
        return null;
    }
}

其余代码

常量类

public class RedisConst {
    public static final String KEY_PREFIX = "Test:";
    //单位:秒
    public static final long KEY_TIMEOUT = 60;
    public static final long KEY_TEMPORARY_TIMEOUT = 30;
    public static final long SKULOCK_EXPIRE_PX1 = 60;
    public static final long SKULOCK_EXPIRE_PX2 = 60;
}

测试

测试接口

@RestController
public class simpleController {
    @GetMapping(value = "/getInfo")
    @addSectionLock(prefix = RedisConst.KEY_PREFIX)
    public String getinfo(String name)
    {
        userEntity userEntity = new userEntity();
        userEntity.setName(name);
        userEntity.setUserWare(100);
        return JSON.toJSONString(userEntity);
    }

}

测试

​ 我们之前切面类中有设置获取锁成功的输出日志,按照实现逻辑,只需要执行一次存入缓存的时候需要获取锁,所以在这个日志指挥打印一次,简单测试那就开两个浏览器页面反复获取数据

image-20221116205847384

​ 在多次点击之后,还是只输出一次,这个模式就算是实现成功了,之后也有用apifox去测试,结果是没问题的,只要有缓存就不会去刷新方法,业务代码只需要加上注解设置前缀就可以完成加入缓存与存在直接调用。就算多次刷新也不会对数据库造成压力。

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

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

相关文章

【HTML】关于height值设置和显示不同的原因以及font-size对象为中文和英文的区别

关于height值设置和显示不同的原因 height: 100px; CSS中把高度设置为100px&#xff0c;但是实际测量发现高度为125px 这个是因为&#xff1a;笔记本默认显示大小为125%&#xff0c;所以100的125%就是125. 还有一点&#xff0c;在你写网页时&#xff0c;要注意网页的缩放一定…

050_阵列天线方向图乘积原理

天线增益概念。原创不易&#xff0c;恐有错误&#xff0c;恳请读者指正。碎片三分钟逛电巢App&#xff0c;收获一丢丢。 天线定向性(directivity) 假设理想的无定向性天线&#xff0c;在远场区的3D球面空间各方向的辐射功率都相等&#xff0c;则定义球面等辐射功率的方向图的定…

Linux内存泄露案例分析和内存管理分享

作者&#xff1a;李遵举 一、问题 近期我们运维同事接到线上LB&#xff08;负载均衡&#xff09;服务内存报警&#xff0c;运维同事反馈说LB集群有部分机器的内存使用率超过80%&#xff0c;有的甚至超过90%&#xff0c;而且内存使用率还再不停的增长。接到内存报警的消息&…

代码随想录day56|583. 两个字符串的删除操作|72. 编辑距离|编辑距离总结篇|Golang

代码随想录day56 考试周了解一下 目录 代码随想录day56 583. 两个字符串的删除操作 72. 编辑距离 动态规划之编辑距离总结篇 583. 两个字符串的删除操作 思路 动态规划一 本题和动态规划&#xff1a;115.不同的子序列相比&#xff0c;其实就是两个字符串都可以删除了&am…

2023职教高考报名开启,报考人数继续增加

进入11月起&#xff0c;各地区2023年职教高考报名已经陆续开始&#xff0c;报考考生主要以中职学校学生为主。根据南京日报的报道&#xff0c;11月1日就启动的江苏省职教高考报名&#xff0c;吸引了不少家长和学生的关注&#xff0c;有些专业的报名人数已经比2022年翻了不止一倍…

Flutter Hooks — 快速了解

Flutter Hooks — 快速了解 前言 Hooks,直译过来就是"钩子"&#xff0c;是前端 React 框架加入的特性&#xff0c;用来分离状态逻辑和视图逻辑。现在这个特性并不只局限在于 React 框架中&#xff0c;其它前端框架也在借鉴。 我们都知道在 FLutter 开发中的一大痛点就…

图解 | 监控系统 Prometheus 的原理

本篇将会以图解的方式剖析 Prometheus 的原理。本文主要内容如下&#xff1a; 一、Prometheus 是什么&#xff1f; ELK Stack 日志收集和检索平台想必大家应该比较熟悉&#xff0c;Elasticsearch Filebeat Logstash Kibana。 ELK 架构 而 Prometheus 就相当于一整个 ELK&a…

功率放大器的主要指标有哪些呢

功率放大器是电子测量行业比较常见的能够放大信号源电压信号的仪器&#xff0c;虽然功率放大器的应用十分广泛&#xff0c;但是很多人对于功率放大器的主要指标参数还不了解&#xff0c;就让安泰电子来为大家介绍功率放大器的主要指标。 功率放大器的核心参数指标包括带宽、电压…

python多线程返回值问题重写Thread类的run方法

python多线程使用 文章目录python多线程使用一、案例二、说明1、针对第一种是有返回值的 &#xff0c;可以通过future.result() 去拿到每个线程返回值2、无返回值问题3、我们可以重写这个Thread类重写了__init__、run方法和join方法&#xff0c;主要是start()涉及的方法太多了而…

Redis篇(5)——持久化

持久化 RDB 1、save会阻塞所有命令。而bgsave则不能与其他持久化命令同时执行 2、自动rdb的发起&#xff1a;servercorn默认每100ms执行一次&#xff0c;根据redisserver里面记录的dirty&#xff08;上次save后修改的次数&#xff09;和lastsave&#xff08;上次save的时间&a…

sklearn基础篇(七)-- 随机森林(Random forest)

上一节我们提到决策树的一个主要缺点在于经常对训练数据过拟合。随机森林是解决这个问题的一种方法。随机森林是属于集成学习&#xff0c;其核心思想就是集成多个弱分类器以达到一个强分类器的效果。 1 bagging的原理 随机森林采用Bagging的思想&#xff0c;所谓的Bagging可以…

HDFS的高级功能

3.6 HDFS的高级功能 3.6.1 安全模式 安全模式&#xff08;Safemode&#xff09;是HDFS所处的一种特殊状态。处于这种状态时&#xff0c;HDFS只接受读数据请求&#xff0c;不能对文件进行写、删除等操作。安全模式是保证一个系统保密性、完整性及可使用性的一种机制&#xff0…

精彩回顾:CACTER邮件数据防泄露EDLP亮相2022世界互联网大会

2022年世界互联网大会乌镇峰会于11月11日胜利闭幕。 本届峰会是世界互联网大会国际组织成立后的首届年会&#xff0c;以“共建网络世界 共创数字未来—携手构建网络空间命运共同体”为主题&#xff0c;共设置1场全体会议和20场分论坛&#xff0c;围绕全球网络空间热点问题展开讨…

【猿创征文】Vue3 企业级优雅实战 - 组件库框架 - 6 搭建example环境

本系列已更新文章&#xff1a; 分享一个实用的 vite vue3 组件库脚手架工具&#xff0c;提升开发效率 开箱即用 yyg-cli 脚手架&#xff1a;快速创建 vue3 组件库和vue3 全家桶项目 Vue3 企业级优雅实战 - 组件库框架 - 1 搭建 pnpm monorepo Vue3 企业级优雅实战 - 组件库框架…

线程池相关总结

多线程之线程池总结 1. 概述&#xff1a; 线程池&#xff08;Thread Pool&#xff09;&#xff1a;把一个或多个线程通过统一的方式进行调度和重复使用的技术&#xff0c;统一管理&#xff0c;避免了因线程过多而带来使用上的开销和不可控。 作用&#xff1a; 降低资源消耗…

SpringBoot进阶学习(二)---配置高级

第三方bean属性绑定 在要绑定的类上添加ConfigurationProperties(prefix “”)&#xff0c;prefix为对应的配置类中的内容&#xff0c;在添加注解ConfigurationProperties时候会产生错误&#xff0c;如&#xff1a; 这时候添加依赖&#xff1a; <dependency><groupI…

计算机网络 5 - 链路层

第6章 链路层和局域网(Link Layer and LANs)6.2 差错检测 和 纠正奇偶校验校验和CRC 循环冗余校验6.3 多路访问协议信道划分 MAC协议随机存取MAC协议6.4 LAN 局域网MAC地址 和 ARP无效的MAC帧格式Ethernet 以太网交换机第6章 链路层和局域网(Link Layer and LANs) 6.2 差错检测…

H5基本开发1——(H5简单概述)

html概述 HTML是用来描述网页的一种语言 HTML指的是超文本标记语言Hyper Text Markup Language&#xff0c;是一种用于创建网页的标准标记语言 标记语言是一套标记标签markup tag HTML使用标记标签来描述网页 HTML文档的后缀名&#xff1a;.html或者.htm&#xff0c;两种后缀名…

Deep Leakage from Gradients

Summary 对于分布式学习&#xff0c;特别是相关之前共享梯度的学习&#xff0c;提出了一种攻击方式&#xff08;DLG&#xff09;。通过窃取client之间传递的梯度反推出&#xff08;也是使用机器学习迭代的方式&#xff09;原始的输入。并在图像分类、Masked Language Model方面…

代谢ADMET在线网页预测工具SwissADME 、SOMP 、BioTransformer

药物代谢(Drug Metabolism)指药物在体内多种药物代谢酶&#xff08;尤其肝药酶&#xff09;的作用下&#xff0c;化学结构发生改变的过程&#xff1b;包括分解代谢和合成代谢 1、概念 药物的代谢反应大致可以分为氧化(oxidation)、还原(reduction)、水解(hydrolysis)和结合(co…