作为一个内存数据库,redis也总是免不了有各种各样的问题,这篇文章主要是针对其中一个问题进行讲解:缓存雪崩。并给出一些解决方案。
关于缓存雪崩问题
缓存雪崩是指大量的缓存在同一时间过期,导致大量请求请求数据库这样会导致我们的数据库压力过大,甚至崩溃
解决方案
主要是加共享锁+双重检测锁来实现的,当然现在基本都是基于组件开发所以我准备使用注解开发,去除一大票子重复代码!!!
pom与yml省略一万字
定义一个注解@Miss
主要作用是获取方法上的入参名称以及值
/**
* CodeNOOB
*
* @date 2020/8/6 13:10
*/
//此注解只能用在方法上,也就是注解的使用范围
@Target(ElementType.METHOD)
// 注解会在class字节码文件中存在,在运行时可以通过反射获取到
@Retention(RetentionPolicy.RUNTIME)
public @interface Miss {
/**
* 主要为了区分是那张表的,并且默认为空串
*/
String value() default "";
}
其次在定义aop切面
这里不再解释毕竟度娘上一大堆对于aop的帖子
//切入点表达式,某个路径下的,或者被注解修饰的方法,返回值必须void
@Pointcut("@annotation(com.louis.springboot.demo.aop.Miss)")
private void miss() {
}
@Around("miss()")
public Object around(ProceedingJoinPoint pjp) throws Throwable {
log.info("=======进入切面=======");
// 限流类似于令牌筒
Semaphore semaphore = new Semaphore(50);
String value = "";
Map<String, Object> args = null;
StringBuffer sb = new StringBuffer();
//获取方法签名也就是被注解修饰的方法的名称以及参数
Signature signature = pjp.getSignature();
//获取方法对象
MethodSignature methodSignature = (MethodSignature) signature;
Method targetMethod = methodSignature.getMethod();
//查看当前方法是否被注解修饰
if (targetMethod.isAnnotationPresent(Miss.class)) {
//获取注解上的值
value = ((Miss) targetMethod.getAnnotation(Miss.class)).value();
args = getFieldsName(pjp);
}
//拼接redis需要的key
for (String parameterName : args.keySet()) {
sb.append(parameterName).append(":").append(args.get(parameterName)).append(":");
}
//如果缓存中有则直接返回
Object obj = redisTemplate.opsForValue().get(value + sb.toString());
if (obj != null) {
System.out.println("缓存生效");
return obj;
}
//获取一个令牌 实则是共享锁
semaphore.acquire();
try {
//使用双重检测锁为了速度快点
obj = redisTemplate.opsForValue().get(value + sb.toString());
if (obj != null) {
System.out.println("缓存生效");
return obj;
}
//没有则放行并且将对饮的值写入缓存中
Object val = pjp.proceed();
//设定key,值,过期五秒,修饰词
redisTemplate.opsForValue().set(value + sb.toString(), val, 5, TimeUnit.SECONDS);
return val;
} finally {
semaphore.release();
}
}
使用到的工具方法,主要封装参数
private Map getFieldsName(ProceedingJoinPoint joinPoint) throws ClassNotFoundException, NoSuchMethodException {
String classType = joinPoint.getTarget().getClass().getName();
String methodName = joinPoint.getSignature().getName();
// 参数值
Object[] args = joinPoint.getArgs();
Class<?>[] classes = new Class[args.length];
for (int k = 0; k < args.length; k++) {
//判断到底拿到的参数是基础类型还是pojo类型
if (!args[k].getClass().isPrimitive()) {
// 获取的是封装类型而不是基础类型
String result = args[k].getClass().getName();
Class s = map.get(result);
classes[k] = s == null ? args[k].getClass() : s;
}
}
//用于获取参数名称
ParameterNameDiscoverer pnd = new DefaultParameterNameDiscoverer();
// 获取指定的方法,第二个参数可以不传,但是为了防止有重载的现象,还是需要传入参数的类型
Method method = Class.forName(classType).getMethod(methodName, classes);
// 参数名
String[] parameterNames = pnd.getParameterNames(method);
// 通过map封装参数和参数值
HashMap<String, Object> paramMap = new HashMap();
for (int i = 0; i < parameterNames.length; i++) {
paramMap.put(parameterNames[i], args[i]);
}
return paramMap;
}
至此我们就可以愉快的使用此组件了
@Miss(value = "missTest")
@GetMapping("missTest")
public Object test(Integer id){
System.out.println("无缓存进入方法");
return new User("测试","123");
}
关于此处我使用java的Semaphore ,主要省事,没必要为了一个小bug在使用中间件来限流,这样有可能解决了一个bug又来了更多的bug