前往小程序,Get更优阅读体验!
立即前往
首页
学习
活动
专区
工具
TVP
发布
社区首页 >专栏 >分布式锁+AOP实现缓存

分布式锁+AOP实现缓存

作者头像
别团等shy哥发育
发布2023-04-23 11:57:50
2830
发布2023-04-23 11:57:50
举报
文章被收录于专栏:全栈开发那些事

分布式锁+AOP实现缓存

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

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

  1. 以 @Transactional 注解为植入点的切点,这样才能知道@Transactional注解标注的方法需要被代理。
  2. @Transactional注解的切面逻辑类似于@Around

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

  • 自定义缓存注解@GmallCache(类似于事务@Transactional)
  • 编写切面类,使用环绕通知实现缓存的逻辑封装

2、不使用AOP的情况

2.1 没有使用缓存时代码

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

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

代码语言:javascript
复制
   //根据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、写一个兜底的方式(其实就是查询数据库),目的是上面的代码发生异常的时候,也能正常返回数据。

代码语言:javascript
复制
 //根据skuId查询skuInfo信息和图片列表
    @Override
    public SkuInfo getSkuInfo(Long skuId) {
        //使用redis实现分布式锁缓存数据
        return getSkuInfoRedis(skuId);
    }
代码语言:javascript
复制
/**
 * 获取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是一样的

代码语言:javascript
复制
 //根据skuId查询skuInfo信息和图片列表
    @Override
    
    public SkuInfo getSkuInfo(Long skuId) {
        //使用Redisson实现分布式锁
        return getSkuInfoRedisson(skuId);
    }
代码语言:javascript
复制
 /**
     *使用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中测试,该接口格式如下:

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

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

  观察Redis中的数据:

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

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

2.5 存在问题

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

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

3、分布式锁+AOP实现

3.1 定义注解

代码语言:javascript
复制
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中的一样,不过我们需要借助反射获取一些参数和方法返回值等。

代码语言:javascript
复制
@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 使用注解完成缓存

  此时实现类如下:

代码语言:javascript
复制
 //根据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的前缀,可以自定义。

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

本文参与 腾讯云自媒体同步曝光计划,分享自作者个人站点/博客。
原始发表:2023-04-19,如有侵权请联系 cloudcommunity@tencent.com 删除

本文分享自 作者个人站点/博客 前往查看

如有侵权,请联系 cloudcommunity@tencent.com 删除。

本文参与 腾讯云自媒体同步曝光计划  ,欢迎热爱写作的你一起参与!

评论
登录后参与评论
0 条评论
热度
最新
推荐阅读
目录
  • 分布式锁+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 使用注解完成缓存
                  相关产品与服务
                  云数据库 Redis®
                  腾讯云数据库 Redis®(TencentDB for Redis®)是腾讯云打造的兼容 Redis 协议的缓存和存储服务。丰富的数据结构能帮助您完成不同类型的业务场景开发。支持主从热备,提供自动容灾切换、数据备份、故障迁移、实例监控、在线扩容、数据回档等全套的数据库服务。
                  领券
                  问题归档专栏文章快讯文章归档关键词归档开发者手册归档开发者手册 Section 归档