分布式锁+AOP实现缓存

news2024/10/3 4:34:16

分布式锁+AOP实现缓存

  • 1、分布式锁+AOP实现思想
  • 2、不使用AOP的情况
    • 2.1 没有使用缓存时代码
    • 2.2 使用Redis实现分布式锁的代码
    • 2.3 使用Redisson实现分布式锁
    • 2.4 测试缓存命中
    • 2.5 存在问题
  • 3、分布式锁+AOP实现
    • 3.1 定义注解
    • 3.2 定义一个切面类加上注解
    • 3.3 使用注解完成缓存

1、分布式锁+AOP实现思想

  随着业务中缓存及分布式锁的加入,业务代码变的复杂起来,除了需要考虑业务逻辑本身,还要考虑缓存及分布式锁的问题,增加了程序员的工作量及开发难度。而缓存的玩法套路特别类似于事务,而声明式事务就是用了aop的思想实现的。

img

  1. 以 @Transactional 注解为植入点的切点,这样才能知道@Transactional注解标注的方法需要被代理。

  2. @Transactional注解的切面逻辑类似于@Around

我们的思想就是模拟事务的实现方式,缓存可以这样实现:

  • 自定义缓存注解@GmallCache(类似于事务@Transactional)

  • 编写切面类,使用环绕通知实现缓存的逻辑封装

2、不使用AOP的情况

2.1 没有使用缓存时代码

  在这里将使用AOP思想和不适用AOP思想做一个对比

  假设现在我的业务是根据skuId查询skuInfo对象,未使用分布式锁时的代码如下:

   //根据skuId查询skuInfo信息和图片列表
    @Override
    public SkuInfo getSkuInfo(Long skuId) {
        //查询数据库mysql获取数据
        return getSkuInfoDB(skuId);
    }
    //查询数据库获取skuInfo信息
    private SkuInfo getSkuInfoDB(Long skuId) {
        //查询skuInfo
        SkuInfo skuInfo = skuInfoMapper.selectById(skuId);
        //根据skuId查询图片列表
        LambdaQueryWrapper<SkuImage> wrapper = new LambdaQueryWrapper<>();
        wrapper.eq(SkuImage::getSkuId, skuId);
        List<SkuImage> skuImages = skuImageMapper.selectList(wrapper);
        //设置当前图片列表
        if(skuInfo!=null){
            skuInfo.setSkuImageList(skuImages);
        }
        return skuInfo;
    }

2.2 使用Redis实现分布式锁的代码

步骤如下:

  1、定义获取sku信息的key–skuKey

  2、根据skuKey从Redis中获取数据:

  有数据就直接返回结果

  没有数据执行下一步

  3、定义skuLock

  4、获取锁:如果没有获取到锁,设置睡眠时间继续自旋获取锁。

  如果获取到了锁,执行下一步。

  5、查询数据库获取sku数据,如果数据库中有数据,则存储数据到缓存,返回数据。

  如果数据库中没有数据,存储null到缓存,返回数据(这样做的目的是防止缓存穿透)

  6、释放锁

  7、写一个兜底的方式(其实就是查询数据库),目的是上面的代码发生异常的时候,也能正常返回数据。

 //根据skuId查询skuInfo信息和图片列表
    @Override
    public SkuInfo getSkuInfo(Long skuId) {
        //使用redis实现分布式锁缓存数据
        return getSkuInfoRedis(skuId);
    }
/**
 * 获取skuInfo,从缓存中获取数据
 * Redis实现分布式锁
 * 实现步骤:
 * 1、定义存储skuInfo的key
 * 2、根据skyKey获取skuInfo的缓存数据
 * 3、判断
 * 有:直接返回结束
 * 没有:定义锁的key,尝试加锁(失败:睡眠,重试自旋;成功:查询数据库,判断是否有值,有的话直接返回,缓存到数据库,没有,创建空值,返回数据)
 */
private SkuInfo getSkuInfoRedis(Long skuId) {
    try {
        //定义存储skuKey sku:1314:info
        String skuKey = RedisConst.SKUKEY_PREFIX + skuId + RedisConst.SKUKEY_SUFFIX;
        //尝试获取缓存中的数据
        SkuInfo skuInfo = (SkuInfo) redisTemplate.opsForValue().get(skuKey);
        //判断是否有值
        if (skuInfo == null) {
            //说明缓存中没有数据
            //定义锁的key
            String lockKey = RedisConst.SKUKEY_PREFIX + skuId + RedisConst.SKULOCK_SUFFIX;
            //生成uuid标识
            String uuid = UUID.randomUUID().toString().replaceAll("-", "");
            //获取锁
            Boolean flag = redisTemplate.opsForValue().setIfAbsent(lockKey, uuid, RedisConst.SKULOCK_EXPIRE_PX2, TimeUnit.SECONDS);
            //判断是否获取到了锁
            if (flag) {//获取到了锁
                //查询数据库
                SkuInfo skuInfoDB = getSkuInfoDB(skuId);
                //判断数据库中是否有值
                if (skuInfoDB == null) {
                    SkuInfo skuInfo1 = new SkuInfo();
                    redisTemplate.opsForValue().set(skuKey, skuInfo1, RedisConst.SKUKEY_TEMPORARY_TIMEOUT, TimeUnit.SECONDS);
                    return skuInfo1;
                }
                //数据库查询的数据不为空
                //存储到缓存
                redisTemplate.opsForValue().set(skuKey, skuInfoDB, RedisConst.SKUKEY_TIMEOUT, TimeUnit.SECONDS);

                //释放锁-lua脚本
                //定义lua脚本
                String script = "if redis.call(\"get\",KEYS[1]) == ARGV[1]\n" +
                        "then\n" +
                        "    return redis.call(\"del\",KEYS[1])\n" +
                        "else\n" +
                        "    return 0\n" +
                        "end";
                //创建脚本对象
                DefaultRedisScript<Long> defaultRedisScript = new DefaultRedisScript<>();
                //设置脚本
                defaultRedisScript.setScriptText(script);
                //设置返回值类型
                defaultRedisScript.setResultType(Long.class);

                //执行删除
                redisTemplate.execute(defaultRedisScript, Arrays.asList(lockKey), uuid);
                //返回数据
                return skuInfoDB;
            } else {
                Thread.sleep(100);
                return getSkuInfoRedis(skuId);
            }

        } else {
            return skuInfo;
        }
    } catch (InterruptedException e) {
        e.printStackTrace();
    }
    //兜底,在上面从缓存中获取的过程中出现异常,这行代码也必须执行
    return getSkuInfoDB(skuId);
}

2.3 使用Redisson实现分布式锁

  这个步骤和2.2是一样的

 //根据skuId查询skuInfo信息和图片列表
    @Override
    
    public SkuInfo getSkuInfo(Long skuId) {
        //使用Redisson实现分布式锁
        return getSkuInfoRedisson(skuId);
    }
 /**
     *使用Redisson改造skuInfo信息
     */
    private SkuInfo getSkuInfoRedisson(Long skuId) {
        try {
            //定义sku数据获取的Key
            String skuKey=RedisConst.SKUKEY_PREFIX+skuId+RedisConst.SKUKEY_SUFFIX;
            //尝试从缓存中获取数据
            SkuInfo skuInfo = (SkuInfo) redisTemplate.opsForValue().get(skuKey);
            //判断缓存中是否有数据
            if(skuInfo==null){
                //定义锁的key
                String skuLock=RedisConst.SKUKEY_PREFIX+skuId+RedisConst.SKULOCK_SUFFIX;
                //获取锁
                RLock lock = redissonClient.getLock(skuLock);
                //加锁
                boolean res = lock.tryLock(RedisConst.SKULOCK_EXPIRE_PX1, RedisConst.SKULOCK_EXPIRE_PX2, TimeUnit.SECONDS);
                //判断
                if(res){
                    try {
                        //获取到了锁,查询数据库
                        skuInfo= getSkuInfoDB(skuId);
                        //判断
                        if(skuInfo==null){
                            //存储null,避免缓存穿透
                            skuInfo=new SkuInfo();
                            redisTemplate.opsForValue().set(skuKey,skuInfo,RedisConst.SKUKEY_TEMPORARY_TIMEOUT,TimeUnit.SECONDS);
                            return skuInfo;
                        }else{
                            //存储
                            redisTemplate.opsForValue().set(skuKey,skuInfo,RedisConst.SKUKEY_TIMEOUT,TimeUnit.SECONDS);
//                            redisTemplate.opsForValue().set(skuKey,skuInfo);
                            //返回
                            return skuInfo;
                        }
                    } finally {
                        //释放锁
                        lock.unlock();
                    }

                }else{
                    //没有获取到锁
                    Thread.sleep(100);
                    return getSkuInfoRedisson(skuId);
                }
            }else{
                //缓存中有数据
                return skuInfo;
            }
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        //兜底方法,前面代码异常,这里会执行
        return getSkuInfoDB(skuId);
    }

2.4 测试缓存命中

  这里直接在Swagger中测试,该接口格式如下:

image-20230419211718178

第一次点击发送,从响应可以看出请求时成功的

image-20230419211834707

  观察该服务的控制台,发现第一次是查询了控制台的。

image-20230419211901963

  观察Redis中的数据:

image-20230419211944633

  然后清空该服务的控制台之后,再次发送同样的请求再观察控制台的输出

image-20230419212111425

  可以看到,此时已经不用查数据库了,而是直接取的Redis中的数据

2.5 存在问题

  每次实现分布式锁的时候都需要写一大段重复代码,增加了工作量,代码也不优雅。

  解决方案:借助AOP思想,用自定义注解封装下这段重复的代码,这样后面需要分布式锁的时候我们直接加个注解再修改个参数就行。

3、分布式锁+AOP实现

3.1 定义注解

import java.lang.annotation.*;

/**
 * 元注解:简单理解就是修饰注解的注解
 * @Target:用于描述注解的使用范围,简单理解就是当前注解可以用在什么地方
 * @Retention:表示注解的生命周期
 *      SOURCE:只存在类文件中,在class字节码不存在
 *      CLASS:存在到字节码文件中
 *      RUNTIME:运行时
 * @Inherited:表示被GmallCache修饰的类的子类会不会继承GmallCache
 * @Documented:表明这个注解应该被javadoc工具记录,因此可悲javadoc类的工具文档化
 */
@Target({ElementType.METHOD})
@Retention(RetentionPolicy.RUNTIME)
@Inherited
@Documented
public @interface GmallCache {

    //缓存的前缀
    String prefix() default "cache:";

    //缓存的后缀
    String suffix() default ":info";

}

3.2 定义一个切面类加上注解

  参考文档:https://docs.spring.io/spring-framework/docs/5.3.27/reference/html/core.html#aop-ataspectj-example

  实现步骤和2.2中的一样,不过我们需要借助反射获取一些参数和方法返回值等。

@Component
@Aspect
public class GmallCacheAspect {

    @Autowired
    private RedisTemplate redisTemplate;

    @Autowired
    private RedissonClient redissonClient;

    /**
     * 使用AOP实现分布式锁和缓存
     *  Around:环绕通知
     *      value:切入的位置
     * 1、定义获取数据的key
     *  例如获取skuInfo  key === sku:skuId
     *      (1)获取添加了@GmallCache注解的方法
     *          可以获取注解、注解的属性、方法的参数
     *      (2)可以尝试获取数据
     */
    @Around("@annotation(com.atguigu.gmall.common.cache.GmallCache)")
    public Object cacheGmallAspect(ProceedingJoinPoint joinPoint) throws Throwable {
        //创建返回对象
        Object object=new Object();
        //获取添加了注解的方法
        MethodSignature signature = (MethodSignature) joinPoint.getSignature();
        //获取注解
        GmallCache gmallCache = signature.getMethod().getAnnotation(GmallCache.class);
        //获取属性前缀
        String prefix = gmallCache.prefix();
        //获取属性后缀
        String suffix = gmallCache.suffix();
        //获取方法传入的参数
        Object[] args = joinPoint.getArgs();
        //组合获取数据的key
        String key=prefix+ Arrays.asList(args).toString()+suffix;
        //从缓存中获取数据
        object=cacheHit(key,signature);
        try {
            //判断
            if(object==null){
                //缓存中没有数据,需要从数据库查询
                //定义锁的key
                String lockKey=prefix+":lock";
                //准备上锁 redis/redisson
                RLock lock = redissonClient.getLock(lockKey);
                //上锁
                boolean flag = lock.tryLock(RedisConst.SKULOCK_EXPIRE_PX1, RedisConst.SKULOCK_EXPIRE_PX2, TimeUnit.SECONDS);
                //判断是否成功
                if(flag){
                    try {
                        //获取到了锁
                        //查询数据库,执行切入的方法体实际上就是查询数据库
                        object= joinPoint.proceed(args);

                        //判断是否从mysql查询到了数据
                        if(object==null){
                            //反射
                            Class aClass = signature.getReturnType();
                            //创建对象
                            object= aClass.newInstance();
                            //存储
                            redisTemplate.opsForValue().set(key,JSON.toJSONString(object),RedisConst.SKUKEY_TEMPORARY_TIMEOUT,TimeUnit.SECONDS);
                            return object;
                        }else{
                            //存储
                            redisTemplate.opsForValue().set(key,JSON.toJSONString(object),RedisConst.SKUKEY_TEMPORARY_TIMEOUT,TimeUnit.SECONDS);
                            return object;
                        }
                    } finally {
                        //释放锁
                        lock.unlock();
                    }
                }else{
                    //睡眠
                    Thread.sleep(100);
                    //自旋
                    return cacheGmallAspect(joinPoint);
                }
            }else{
                //从缓存中获取了数据
                return object;
            }
        } catch (Throwable throwable) {
            throwable.printStackTrace();
        }
        //兜底的方法--查询数据库,实际上执行方法体就是查询数据库
        return joinPoint.proceed(args);
    }

    //从缓存中获取数据
    private Object cacheHit(String key, MethodSignature signature) {
        //获取数据--存储的时候,转换成JSON字符串,所以从Redis取出来的时候是个字符串
        String strJson = (String) redisTemplate.opsForValue().get(key);
        //判断
        if(!StringUtils.isEmpty(strJson)){
            //获取当前方法的返回值类型
            Class returnType = signature.getReturnType();
            //将字符串转换成指定的类型
            return JSON.parseObject(strJson,returnType);
        }
        return null;
    }
}

3.3 使用注解完成缓存

  此时实现类如下:

 //根据skuId查询skuInfo信息和图片列表
    @Override
    @GmallCache(prefix ="sku:")  //key:  sku:1314:info
    public SkuInfo getSkuInfo(Long skuId) {
        //查询数据库mysql获取数据
        return getSkuInfoDB(skuId);
        //使用redis实现分布式锁缓存数据
//        return getSkuInfoRedis(skuId);
        //使用Redisson实现分布式锁
//        return getSkuInfoRedisson(skuId);
    }

  现在这个方法中写的是调用数据库查询的代码,不过我们在这里加了一个@GmallCache自定义注解,其中参数prefix是缓存中key的前缀,可以自定义。

  这样每次在进入到这个方法的时候会执行我们定义的那个切面类,把分布式锁的步骤走一遍,可以看到,这样代码侵入性就比较低了,如果在其他地方也想使用分布式锁,那就直接加上这个注解,再给个前缀参数即可。

image-20230419213623463

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

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

相关文章

函数的缺省参数,函数重载与底层函数名修饰解释,引用的初步介绍

TIPS 使用C输入输出更方便&#xff0c;不需要像printf/scanf输入输出时那样&#xff0c;需要手动控制格式。C的输入输出可以自动识别变量类型。在日常练习中&#xff0c;建议直接using namespace std即可&#xff0c;这样就很方便。using namespace std展开&#xff0c;标准库…

ReetrantLock源码剖析_03公平锁、非公平锁

一直努力就会有offer&#xff0c;一直努力就会有offer&#xff0c;一直努力就会有offer&#xff01; 文章目录 ReetrantLock公平锁代码解析ReetrantLock公平锁执行流程ReetrantLock非公平锁代码解析ReetrantLock非公平锁执行流程公平锁与非公平锁的比较 ReetrantLock公平锁代码…

前端部署发布项目后,如何通知用户刷新页面、清除缓存

以下只是一些思路&#xff0c;有更好的实现方式可以留言一起交流学习 方式一&#xff1a;纯前端 在每次发布前端时&#xff0c;使用webpack构建命令生成一个json文件&#xff0c;json中写个随机生成的一个字符串&#xff08;比如时间戳&#xff09;&#xff0c;每次打包程序都…

【Python入门第五十天】Python丨NumPy 数组搜索

搜索数组 可以在数组中搜索&#xff08;检索&#xff09;某个值&#xff0c;然后返回获得匹配的索引。 要搜索数组&#xff0c;请使用 where() 方法。 实例 查找值为 4 的索引&#xff1a; import numpy as nparr np.array([1, 2, 3, 4, 5, 4, 4])x np.where(arr 4)pri…

node可以用nvm快速切换版本,golang如何快速切换版本?用gvm就行。

使用 gvm 可以带来以下好处&#xff1a; 快速切换 Golang 版本&#xff0c;方便进行版本测试和开发&#xff1b;可以在多个项目中同时使用不同版本的 Golang 包和工具&#xff0c;避免冲突&#xff1b;可以通过 gvm 管理不同版本的 Golang&#xff0c;方便安装、卸载和更新&am…

STL--vector

一、vector介绍 vector是表示大小可以更改的数组的序列容器 就像数组一样&#xff0c;vector也采用的连续存储空间来存储元素。也就是意味着可以采用下标对vector的元素进行访问&#xff0c;和数组一样高效。但是又不像数组&#xff0c;它的大小是可以动态改变的&#xff0c;而…

移动端屏幕适配

文章目录 移动端屏幕适配移动端屏幕适配和响应式布局区别基本知识简单屏幕适配 移动端屏幕适配 移动端屏幕适配和响应式布局区别 移动端适配响应式布局终端移动端PC端和移动端常用单位宽高&#xff1a;rem 或 %字体&#xff1a;px宽&#xff1a;%高、字体&#xff1a;px宽高宽…

Docker网络模式与cgroups资源控制

目录 1.docker网络模式原理 2.端口映射 3.Docker网络模式&#xff08;41种&#xff09; 1.查看docker网络列表 2.网络模式详解 4.Docker cgroups资源控制 1.CPU资源控制 2.对内存使用的限制 3.对磁盘IO的配置控制&#xff08;blkio&#xff09;的限制 4.清除docker占用…

Vue3教程

文章目录 参考资料1 setup语法糖1.1 vue2中的写法1.2 setup语法糖在vue3中使用 2 ref reactive 事件2.1 ref2.2 reactive2.3 事件&#xff1a;在setup script中&#xff0c;直接定义事件&#xff0c;不需要像vue2那样在method中定义 3 computed & watch & watchEffect3…

详解DHCP和DNS实验汇总

文章目录 1.实验说明2.实验步骤2.1&#xff08;linux的CentOS 7-2&#xff09;命令配置2.2 &#xff08;linux的CentOS 7-3&#xff09;命令配置2.3 客户端(WIN10)命令配置2.4 客户端(CentOS 7-1)命令配置 1.实验说明 实验要求&#xff1a;要求在一台主机中同时配置DNS服务器和…

【SpringCloud】1、Nacos注册中心、配置中心搭建

1、Nacos 简介 Nacos /nɑ:kəʊs/ 是 Dynamic Naming and Configuration Service的首字母简称&#xff0c;一个更易于构建云原生应用的动态服务发现、配置管理和服务管理平台。 Nacos 致力于帮助您发现、配置和管理微服务。Nacos 提供了一组简单易用的特性集&#xff0c;帮助…

PartiQL 对 SQL 的扩展,可以查询非结构化的数据

目录 开始 先决条件 下载 PartiQL CLI 运行 PartiQL CLI 窗户 macOS &#xff08;Mac&#xff09; 和 Unix 命令行教程 介绍 PartiQL 查询与 SQL 兼容 PartiQL 数据模型&#xff1a;许多底层数据存储格式的抽象 了解更多信息 查询嵌套数据 嵌套集合 取消嵌套嵌套…

Hbase数据库完全分布式搭建以及java中操作Hbase

文章目录 1.基础的环境准备2.完全分布式 Fully-distributed2.1 配置文件hase-env.sh2.2 hbase-site.xml2.3 配置regionservers2.4 配置备用的master2.5 HDFS客户端配置2.6 启动2.7 通过页面查看节点信息 3. java中客户端操作Hbase3.1 引入依赖3.2 初始化创建连接3.3 操作Hbase数…

Qt/QML编程学习之心得:D-BUS进程间通信(四)

Qt/QML应用编程最适合于一些触摸的嵌入式界面设计&#xff0c;那么GUI界面怎么与底层的设备通信&#xff0c;怎么与一个系统内其他模块通信的呢&#xff1f;这就不得不说一个很重要的设计模式&#xff1a;d-bus。 D-BUS是一个系统中消息总线&#xff0c;用于IPC/RPC。消息系统…

vi编辑器的使用介绍

vi编辑器的使用 vi的特点与运用场景vi的使用简易执行一个案例按键说明第一部分&#xff1a;命令模式的按键说明(光标移动、复制粘贴、查找替换)移动光标的方法查找与替换删除、复制与粘贴 第二部分&#xff1a;命令模式切换到输入模式的可以按键进入插入或替换的编辑模式 第三部…

A100 Jeston TX1/TX2使用教程-介绍

大家好&#xff0c;我是虎哥&#xff0c;经过一段时间的整理&#xff0c;终于完成了我自己算力盒子&#xff0c;A100系统的设计和研发&#xff0c;今天就来和大家聊聊这款针对TX1和TX2的入门级计算盒子的一些特性和功能。 一、EdgeBox_Umate_A100 算力盒子 A100 算力盒子是“玩…

系统集成项目管理工程师 笔记(第五章:项目立项管理)

文章目录 5.1 项目建议 2225.2 项目可行性分析 224项目可行性研究内容&#xff1a;5.2.2 项目可行性研究阶段 227 5.4 项目招投标 229《中华人民共和国招标投标法实施条例》5.4.1 项目招标 2295.4.2 项目投标 2305.4.3 开标与评标 2345.4.4 选定项目承建方 235 5.5 项目合同谈判…

实模式下内存访问

虽然有了寄存器&#xff0c;但是数据和指令还是需要存储到内存中。通常情况下需要把数据从内存中放到寄存器中才能使用&#xff0c;同样的指令需要放到寄存器中才能被CPU执行。 所有的内存访问都需要段寄存器左移四位加上其他寄存器的值才能得到真正地址值。这是由于以前运行实…

Unity使用ShaderGragh制作透明指针

Unity使用ShaderGragh制作透明指针 1 概述2 使用环境3 制作流程3.1 创建一个ShaderGragh3.2 打开ShaderGraph编辑器3.3 编辑器界面介绍3.4 Shader节点和部分信息如下3.5 常用节点介绍3.6 使用Shader3.7 贴图规范 4 控制Shader旋转4.1 API介绍4.2 示例代码&#xff1a;3.9 Shade…

Redis 6.0+ 的 ACL 机制

目录 前言一、安装 Redis 服务二、创建 ACL 用户三、用户密码管理3.1 删除密码3.2 重置用户和密码 四、权限管理4.1 key 管理4.2 权限管理 五、ACL 用户存储5.1 配置文件实现5.2 外部 ACL 文件实现 前言 Redis 6.0 引入了 ACL 机制&#xff0c;类似 MySQL 一样全部权限管理&am…