> 文章列表 > 分布式锁+AOP实现缓存

分布式锁+AOP实现缓存

分布式锁+AOP实现缓存

分布式锁+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信息和图片列表@Overridepublic SkuInfo getSkuInfo(Long skuId) {//查询数据库mysql获取数据return getSkuInfoDB(skuId);}//查询数据库获取skuInfo信息private SkuInfo getSkuInfoDB(Long skuId) {//查询skuInfoSkuInfo 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信息和图片列表@Overridepublic 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:infoString skuKey = RedisConst.SKUKEY_PREFIX + skuId + RedisConst.SKUKEY_SUFFIX;//尝试获取缓存中的数据SkuInfo skuInfo = (SkuInfo) redisTemplate.opsForValue().get(skuKey);//判断是否有值if (skuInfo == null) {//说明缓存中没有数据//定义锁的keyString 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信息和图片列表@Overridepublic SkuInfo getSkuInfo(Long skuId) {//使用Redisson实现分布式锁return getSkuInfoRedisson(skuId);}
 /***使用Redisson改造skuInfo信息*/private SkuInfo getSkuInfoRedisson(Long skuId) {try {//定义sku数据获取的KeyString skuKey=RedisConst.SKUKEY_PREFIX+skuId+RedisConst.SKUKEY_SUFFIX;//尝试从缓存中获取数据SkuInfo skuInfo = (SkuInfo) redisTemplate.opsForValue().get(skuKey);//判断缓存中是否有数据if(skuInfo==null){//定义锁的keyString 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 {@Autowiredprivate RedisTemplate redisTemplate;@Autowiredprivate 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();//组合获取数据的keyString key=prefix+ Arrays.asList(args).toString()+suffix;//从缓存中获取数据object=cacheHit(key,signature);try {//判断if(object==null){//缓存中没有数据,需要从数据库查询//定义锁的keyString lockKey=prefix+":lock";//准备上锁 redis/redissonRLock 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:infopublic SkuInfo getSkuInfo(Long skuId) {//查询数据库mysql获取数据return getSkuInfoDB(skuId);//使用redis实现分布式锁缓存数据
//        return getSkuInfoRedis(skuId);//使用Redisson实现分布式锁
//        return getSkuInfoRedisson(skuId);}

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

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

image-20230419213623463