1.问题分析
1.考虑很多线程去查数据库,对数据库冲击较大,效率也比较低。
2.所以需要引入缓存。
3.考虑缓存击穿,缓存失败。
4.考虑redis集群宕机。
5.考虑很多方法都要使用缓存,都要解决这些问题。
6.锁需要支持集群
7.综合以上几点,技术选型:
redisson+aop+redisTemplate
2.思路梳理
1.首先定义一个注解,标识这个注解的方法会被我们自定义的切面获取并解析,实现查询缓存。
2.可以在注解里面定义一个前缀属性,每个方法查询的数据肯定不一样,所以为了防止key重复,
每个方法的前缀key都在标识注解的时候指定。
3.定义切面类,定义环绕方法,@Around("@annotation(com.atguigu.gmall.common.cache.MySelfCache)")
4.首先利用ProceedingJoinPoint 简称point 来获取方法的参数 和方法上的这个注解。
将注解里指定的参数和方法参数拼接成key
5.考虑一个大情况 ,redis集群宕机,此时拿着key去缓存查就会抛出异常,不会继续去查数据库。
解决办法:用try-catch包裹接下来所有代码,只在方法最后return 一个null,兜底。
6.先去拿着key查缓存,缓存有的话没直接返回。
7.缓存没有的话,就得去数据库查,但是考虑到需要防止缓存穿透,所以就需要用redisson上锁。
8.上锁之后,还需要判断,假如多线程,只有一个线程获取到了锁,所以,其他没获取到锁的线程,只需要自旋即可。
9.获取到锁的线程,直接去数据库查。此时还需要考虑2个情况。
10.数据库没有这个数据,考虑到防止缓存击穿,利用反射,获取方法的返回值类型,造一个空对象放到redis,并直接返回,
注意:因为数据库暂时并没有这个数据,所以我们给这个数据需要设置一个时间不太长的过期时间。
11.数据库有这个数据,直接将数据加入redis并返回。
3.代码实现
/**
* @author yinhuidong
* @createTime 2020-08-02-7:41
*/
@Retention(RetentionPolicy.RUNTIME)
@Documented
@Target(ElementType.METHOD)
public @interface MySelfCache {
String prefix() default "cache";
}
/**
* @author yinhuidong
* @createTime 2020-08-02-7:42
*/
@Component
@Aspect
public class MySelfCacheAspect {
@Autowired
private RedisTemplate redisTemplate;
@Autowired
private RedissonClient redissonClient;
@Around("@annotation(com.atguigu.gmall.common.cache.MySelfCache)")
public Object aroundMethod(ProceedingJoinPoint point) {
//声明返回结果
Object result = null;
//获取中间变量
MethodSignature signature = (MethodSignature) point.getSignature();
//获取方法上的注解
MySelfCache mySelfCache = signature.getMethod().getAnnotation(MySelfCache.class);
//获取方法参数
Object[] args = point.getArgs();
//组装key
String key = mySelfCache.prefix() + Arrays.asList(args);
//假如redis集群宕机,程序并不知道,因此还回去缓存查数据,此时就会抛出异常,所以需要try-catch,如果发生异常,直接return
try {
//去缓存查
result = searchCache(key);
//如果缓存没数据
if (StringUtils.isEmpty(result)) {
//防止缓存穿透,加锁
RLock lock = redissonClient.getLock(key + ":cache");
//配置看门狗 最大等待时间 等待时间 时间单位
boolean flag = lock.tryLock(10, 1, TimeUnit.SECONDS);
//如果加锁成功
if (flag) {
try {
//从数据库查
result = point.proceed(args);
//如果数据库没有这个ID对应的数据,防止缓存击穿
if (StringUtils.isEmpty(result)) {
//反射获取方法的返回值类型,创建空对象,加入redis缓存
Object o = signature.getReturnType().newInstance();
redisTemplate.opsForValue().set(key, o, 10, TimeUnit.MINUTES);
return o;
}
//如果有数据,加入缓存,直接return
redisTemplate.opsForValue().set(key, result, 24 * 7, TimeUnit.HOURS);
} catch (Throwable throwable) {
System.out.println("server-product" + "**************" + signature.getMethod() + "方法业务逻辑出现异常");
} finally {
//解锁
lock.unlock();
}
} else {//加锁失败,考虑自旋
TimeUnit.SECONDS.sleep(1);
result = searchCache(key);
return result;
}
} else {//缓存有数据
return result;
}
} catch (InterruptedException e) {
e.printStackTrace();
}
return result;
}
public Object searchCache(String key) {
return redisTemplate.opsForValue().get(key);
}
}