1 AOP实现Redis缓存
1.1 如何理解AOP
名称: 面向切面编程
作用: 降低系统中代码的耦合性,并且在不改变原有代码的条件下对原有的方法进行功能的扩展.
公式: AOP = 切入点表达式 + 通知方法
1.2 通知类型
1.前置通知 目标方法执行之前执行
2.后置通知 目标方法执行之后执行
3.异常通知 目标方法执行过程中抛出异常时执行
4.最终通知 无论什么时候都要执行的通知
特点: 上述的四大通知类型 不能干预目标方法是否执行.一般用来做程序运行状态的记录.监控
5.环绕通知 在目标方法执行前后都要执行的通知方法 该方法可以控制目标方法是否运行.joinPoint.proceed(); 功能作为强大的.
1.3 切入点表达式
理解: 切入点表达式就是一个程序是否进入通知的一个判断(IF)
作用: 当程序运行过程中 ,**满足了切入点表达式时才会去执行通知方法,**实现业务的扩展.
种类(写法):
1. bean(bean的名称 bean的ID) 只能拦截具体的某个bean对象 只能匹配一个对象
lg: bean(“itemServiceImpl”)
2. within(包名.类名) within(“com.jt.service.*”) 可以匹配多个对象
粗粒度的匹配原则 按类匹配
3. execution(返回值类型 包名.类名.方法名(参数列表)) 最为强大的用法
lg : execution(* com.jt.service..*.*(..))
返回值类型任意 com.jt.service包下的所有的类的所有的方法都会被拦截.
4.@annotation(包名.注解名称) 按照注解匹配.
1.4 AOP入门案例
package com.jt.aop;
import org.aspectj.lang.JoinPoint;
import org.aspectj.lang.ProceedingJoinPoint;
import org.aspectj.lang.annotation.*;
import org.springframework.stereotype.Component;
import org.springframework.stereotype.Service;
import java.util.Arrays;
@Aspect //我是一个AOP切面类
@Component //将类交给spring容器管理
public class CacheAOP {
//公式 = 切入点表达式 + 通知方法
//@Pointcut("bean(itemCatServiceImpl)")
//@Pointcut("within(com.jt.service..*)") //匹配多级目录
@Pointcut("execution(* com.jt.service..*.*(..))") //方法参数级别
public void pointCut(){
//定义切入点表达式 只为了占位
}
//区别: pointCut() 表示切入点表达式的引用 适用于多个通知 共用切入点的情况
// @Before("bean(itemCatServiceImpl)") 适用于单个通知.不需要复用的
// 定义前置通知,与切入点表达式进行绑定. 注意绑定的是方法
@Before("pointCut()")
//@Before("bean(itemCatServiceImpl)")
public void before(JoinPoint joinPoint){
String className = joinPoint.getSignature().getDeclaringTypeName();
String methodName = joinPoint.getSignature().getName();
Class targetClass = joinPoint.getTarget().getClass();
Object[] args = joinPoint.getArgs();
Long runTime = System.currentTimeMillis();
System.out.println("方法路径:" +className+"."+methodName);
System.out.println("目标对象类型:" + targetClass);
System.out.println("参数:" + Arrays.toString(args));
System.out.println("执行时间:" + runTime+"毫秒");
}
@Around("pointCut()")
public Object around(ProceedingJoinPoint joinPoint){
System.out.println("环绕通知开始!!!");
Object result = null;
try {
result = joinPoint.proceed(); //执行下一个通知或者目标方法
} catch (Throwable throwable) {
throwable.printStackTrace();
}
System.out.println("环绕通知结束");
return result;
}
}
2 关于AOP实现Redis缓存
2.1 自定义缓存注解
问题: 如何控制 哪些方法需要使用缓存? cacheFind()
解决方案: 采用自定义注解的形式 进行定义,如果 方法执行需要使用缓存,则标识注解即可.
关于注解的说明:
1.注解名称 : cacheFind
2.属性参数 :
2.1 key: 应该由用户自己手动添加 一般添加业务名称 之后动态拼接形成唯一的key
2.2 seconds: 用户可以指定数据的超时的时间
@Target(ElementType.METHOD) //注解对方法有效
@Retention(RetentionPolicy.RUNTIME) //运行期有效
public @interface CacheFind {
public String preKey(); //用户标识key的前缀.
public int seconds() default 0; //如果用户不写表示不需要超时. 如果写了以用户为准.
}
2.2 编辑CacheAOP
package com.jt.aop;
import com.jt.anno.CacheFind;
import com.jt.config.JedisConfig;
import com.jt.util.ObjectMapperUtil;
import org.aspectj.lang.JoinPoint;
import org.aspectj.lang.ProceedingJoinPoint;
import org.aspectj.lang.annotation.*;
import org.aspectj.lang.reflect.MethodSignature;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Component;
import org.springframework.stereotype.Service;
import redis.clients.jedis.Jedis;
import java.lang.reflect.Method;
import java.util.Arrays;
@Aspect //我是一个AOP切面类
@Component //将类交给spring容器管理
public class CacheAOP {
@Autowired
private Jedis jedis;
@Around("@annotation(cacheFind)")
public Object around(ProceedingJoinPoint joinPoint,CacheFind cacheFind){
Object result = null;
try {
//1.拼接redis存储数据的key
Object[] args = joinPoint.getArgs();
String key = cacheFind.preKey() +"::" + Arrays.toString(args);
//2. 查询redis 之后判断是否有数据
if(jedis.exists(key)){
//redis中有记录,无需执行目标方法
String json = jedis.get(key);
//动态获取方法的返回值类型 向上造型 向下造型
MethodSignature methodSignature = (MethodSignature) joinPoint.getSignature();
Class returnType = methodSignature.getReturnType();
result = ObjectMapperUtil.toObj(json,returnType);
System.out.println("AOP查询redis缓存");
}else{
//表示数据不存在,需要查询数据库
result = joinPoint.proceed(); //执行目标方法及通知
//将查询的结果保存到redis中去
String json = ObjectMapperUtil.toJSON(result);
//判断数据是否需要超时时间
if(cacheFind.seconds()>0){
jedis.setex(key,cacheFind.seconds(),json);
}else {
jedis.set(key, json);
}
System.out.println("aop执行目标方法查询数据库");
}
} catch (Throwable throwable) {
throwable.printStackTrace();
}
return result;
}
}
3 关于Redis 配置说明
3.1 关于Redis持久化的说明
redis默认条件下支持数据的持久化操作. 当redis中有数据时会定期将数据保存到磁盘中.当Redis服务器重启时 会根据配置文件读取指定的持久化文件.实现内存数据的恢复.
3.2持久化方式介绍
3.2.1 RDB模式
特点:
1.RDB模式是redis的默认的持久化策略.
2.RDB模式记录的是Redis 内存数据的快照. 最新的快照会覆盖之前的内容 所有RDB持久化文件占用空间更小 持久化的效率更高.
3.RDB模式由于是定期持久化 所以可能导致数据的丢失.
命令:
1. save 要求立即马上持久化 同步的操作 其他的redis操作会陷入阻塞的状态.
2. bgsave 开启后台运行 异步的操作 由于是异步操作,所以无法保证rdb文件一定是最新的需要等待.
配置:
1.持久化文件名称:
2.持久化文件位置
dir ./ 相对路径的写法
dir /usr/local/src/redis 绝对路径写法
3.RDB模式持久化策略
3.2.2 AOF模式
特点:
1.AOF模式默认条件下是关闭的,需要用户手动的开启
2. AOF模式是异步的操作 记录的是用户的操作的过程 可以防止用户的数据丢失
3. 由于AOF模式记录的是程序的运行状态 所以持久化文件相对较大,恢复数据的时间长.需要人为的优化持久化文件
配置:
3.2.2 关于持久化操作的总结
1.如果不允许数据丢失 使用AOF方式
2.如果追求效率 运行少量数据丢失 采用RDB模式
3.如果既要保证效率 又要保证数据 则应该配置redis的集群 主机使用RDB 从机使用AOF
3.3 关于Redis内存策略
3.3.1 关于内存策略的说明
说明:Redis数据的存储都在内存中.如果一直想内存中存储数据 必然会导致内存数据的溢出.
解决方式:
1. 尽可能为保存在redis中的数据添加超时时间.
2. 利用算法优化旧的数据.
3.3.2 LRU算法
特点: 最好用的内存优化算法.
LRU是Least Recently Used的缩写,即最近最少使用,是一种常用的页面置换算法,选择最近最久未使用的页面予以淘汰。该算法赋予每个页面一个访问字段,用来记录一个页面自上次被访问以来所经历的时间 t,当须淘汰一个页面时,选择现有页面中其 t 值最大的,即最近最少使用的页面予以淘汰。
维度: 时间 T
3.3.3 LFU算法
LFU(least frequently used (LFU) page-replacement algorithm)。即最不经常使用页置换算法,要求在页置换时置换引用计数最小的页,因为经常使用的页应该有一个较大的引用次数。但是有些页在开始时使用次数很多,但以后就不再使用,这类页将会长时间留在内存中,因此可以将引用计数寄存器定时右移一位,形成指数衰减的平均使用次数。
维度: 使用次数
3.3.4 RANDOM算法
随机删除数据
3.3.5 TTL算法
把设定了超时时间的数据将要移除的提前删除的算法.
3.3.6 Redis内存数据优化
- volatile-lru 设定了超时时间的数据采用lru算法
2.allkeys-lru 所有的数据采用LRU算法
3.volatile-lfu 设定了超时时间的数据采用lfu算法删除
4.allkeys-lfu 所有数据采用lfu算法删除
5.volatile-random 设定超时时间的数据采用随机算法
6.allkeys-random 所有数据的随机算法
7.volatile-ttl 设定超时时间的数据的TTL算法
8.noeviction 如果内存溢出了 则报错返回. 不做任何操作. 默认值
4 关于Redis 缓存面试题
问题描述: 由于海量的用户的请求 如果这时redis服务器出现问题 则可能导致整个系统崩溃.
运行速度:
1. tomcat服务器 150-250 之间 JVM调优 1000/秒
2. NGINX 3-5万/秒
3. REDIS 读 11.2万/秒 写 8.6万/秒 平均 10万/秒
4.1 缓存穿透
问题描述: 由于用户高并发环境下访问 数据库中不存在的数据时 ,容易导致缓存穿透.
如何解决: 设定IP限流的操作 nginx中 或者微软服务机制 API网关实现.
4.2 缓存击穿
问题描述: 由于用户高并发环境下, 由于某个数据之前存在于内存中,但是由于特殊原因(数据超时/数据意外删除)导致redis缓存失效. 而使大量的用户的请求直接访问数据库.
俗语: 趁他病 要他命
如何解决:
1.设定超时时间时 不要设定相同的时间.
2.设定多级缓存
4.3 缓存雪崩
说明: 由于高并发条件下 有大量的数据失效.导致redis的命中率太低.而使得用户直接访问数据库(服务器)导致奔溃,称之为缓存雪崩.
解决方案:
1.不要设定相同的超时时间 随机数
2.设定多级缓存.
3.提高redis缓存的命中率 调整redis内存优化策略 采用LRU等算法.