1.利用aop实现redis的缓存处理
1.1 业务说明
由于将代码直接写道service业务层中,会导致代码的耦合性高不便于扩展,为了实现代码松耦合可以利用奥胖机制实现缓存处理
2. AOP的知识回顾
2.1 什么是AOP
AOP = 通知方法 + 切入点表达式
说明:当程序的执行曼珠了切尔u点表达式,代码执行的过程会发生变化,会执行通知方法
AOP用途:将代码中不得不做的事情,放在切面中完成
AOP作用:对原有的方法进行扩展
业务中常用的aop的场景
1.日志的记录
2.监控执行时间
3.实现事务控制
4.异常监控
5.缓存的处理
特点:公用的业务
2.2 通知方法
前置通知:在目标方法执行之前执行
后置通知:在目标方法执行之后执行
异常通知:在目标方法执行之后报错时通知
最终通知:无论用户发生了什么,都要执行的操作,最后执行
特点:前四大通知不能影响目标方法是否执行,只能做记录(监控程序的状态),不能改变程序的运行轨迹
环绕通知:目标方法执行前后都要执行的通知方法;可以影响程序的执行轨迹,功能最为强大
2.3 切入点的表达式
1.粗粒度的:按位置匹配
1.1:bean(beanId) 按照某个bean对象进行匹配 一个
1.2:within(包名.类名) 可以执行某些类进行匹配 多个
2.细粒度的
2.1:execution(返回值类型 包名.类名.方法名(参数列表))
ep:
execution( int com.tedu.service.goodsImpl.add(int))
execution( * com.tedu.service..*.*(..))
//返回值类型任意:
@annotation(包名.注解名) 被注解的类,方法,属性都可以被切入点表达式匹配
2.4 AOP的复习案例:
2.4.1 获取类名方法名和属性
package com.jt.aop;
import org.aspectj.lang.JoinPoint;
import org.aspectj.lang.annotation.Aspect;
import org.aspectj.lang.annotation.Before;
import org.aspectj.lang.annotation.Pointcut;
import org.springframework.context.annotation.Bean;
import org.springframework.stereotype.Component;
@Component //1.我是一个javaBean
@Aspect //2.我是一个切面
public class CacheTestAOP {
//定义切入点表达式:
@Pointcut("bean(itemServiceImpl)")
public void pointCut(){}
/**
* 定义通知方法
* 1. 想获取目标方法的名称
* 2. 获取目标方法的对象
* 3. 获取目标方法的参数
*/
@Before("pointCut()")
public void before(JoinPoint joinPoint){
String name = joinPoint.getSignature().getDeclaringTypeName(); //获取类名
String methodName = joinPoint.getSignture().getName(); //获取方法名
Object target = joinPoint.getTarget(); //获取对象
Object[] args = joinPoint.getArgs();//获取参数
System.out.println("我是前置通知方法");
System.out.println("这是我的名字" + name);
String.out.println("这是我的方法名" + methodName)
System.out.println("这是我的对象" + target);
System.out.println("这是我的参数" + args);
}
}
2.4.2 自定义注解
package com.jt.annotation;
import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;
@Target(ElementType.METHOD) //注解在方法中使用
@Retention(RetentionPolicy.RUNTIME) //运行期有效
public @interface CacheFind {
//设定key
String key(); // 设定key,用户自己设定
int seconds() default 0; //可以指定超时时间,也可以不指定
}
2.4.3 使用自定义注解
@Override
@CacheFind(key = "ITEM_CAT_LIST") //使用注解 自定义前缀
public List<EasyUITree> findItemCatListByParentId(Long parentId){
QueryWrapper<ItemCat> queryWrapper = new QueryWrapper<>();
queryWrapper.eq("parent_id",parentId);
List<ItemCat> itemCatList = itemCatMapper.selectList(queryWrapper);
List<EasyUITree> treeList = new ArrayList<>(); //先准备一个空集合.
//需要将数据一个一个的格式转化.
for(ItemCat itemcat : itemCatList){
Long id = itemcat.getId(); //获取ID
String text = itemcat.getName(); //获取文本
//如果是父级,则默认应该处于关闭状态 closed, 如果不是父级 则应该处于打开状态. open
String state = itemcat.getIsParent()?"closed":"open";
//利用构造方法 为VO对象赋值 至此已经实现了数据的转化
EasyUITree tree = new EasyUITree(id,text,state);
treeList.add(tree);
}
//用户需要返回List<EasyUITree>
return treeList;
}
2.4.4 使用AOP实现缓存
package com.jt.aop;
import com.jt.annotation.CacheFind;
import com.jt.utils.ObjectMapperUtil;
import org.aspectj.lang.ProceedingJoinPoint;
import org.aspectj.lang.annotation.Around;
import org.aspectj.lang.annotation.Aspect;
import org.aspectj.lang.reflect.MethodSignature;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Component;
import redis.clients.jedis.Jedis;
import java.util.Arrays;
@Component
@Aspect
public class CacheAOP {
//引入redis配置
@Autowired(required = false)
private Jedis jedis;
/**
* AOP缓存实现的业务策略
* 1:切入点表达式应该拦截 @CacheFind注解
* 2:通知方法: 环绕通知
* 注意:
* 如果使用环绕通知,这必须在第一个参数的位置添加ProceedingJoinPoint
* 完成redis配置 key
*
* 动态获取注解参数的步骤
* 1.@annotation(cacheFind) 切入点表达式要求拦截一个类型为cacheFind注解。
* 2.并且利用连接点为参数中的cacheFind赋值
*/
@Around("@annotation(cacheFind)") //输入的是切入点的表达式
public Object around(ProceedingJoinPoint joinPoint,CacheFind cacheFind){
Object result = null;
try {
//如何获取动态的获取注解中的数据?
String prokey = cacheFind.key();
//动态的获取方法中的参数
String args = Arrays.toString(joinPoint.getArgs());
String key = prokey + "::" + args;
//3.校验redis中是否有数据,
if (jedis.exists(key)){
//有缓存 从redis缓存中获取json,之后还原对象返回
String json = jedis.get(key);
//target代表这目标方法的返回值类型
//动态的获取目标方法的返回值类型
MethodSignature methodSignature = (MethodSignature) joinPoint.getSignature();
Class returnClass = methodSignature.getReturnType();
//将json转化为对象
result = ObjectMapperUtil.toObject(json, returnClass);
System.out.println("调用redis的AOP转换类型");
}else{
//没缓存,第一次查询数据库
result = joinPoint.proceed(); //执行目标方法
System.out.println("开始执行AOP保存到redis数据库");
//将数据保存到redis
String json = ObjectMapperUtil.toJSON(result);
if (cacheFind.seconds() > 0)//如果存在超时时间
jedis.setex(key,cacheFind.seconds(),json);
else//不存在超时时间
jedis.set(key,json);
}
return result;
} catch (Throwable throwable) {
throwable.printStackTrace();
throw new RuntimeException(throwable);
}
}
}
3 常见的redis的面试题:
3.1 缓存穿透
特点:用户高并发的环境下,查询数据库中不存在的数据;
影响:由于用户高并发的访问数据库,容易造成宕机的风险
解决方法:
api网关的限制
限定ip的访问的次数
3.2 缓存击穿
由于用户高并发的访问,访问的数据刚开始有缓存,但是由于特殊原因导致(少量)数据的缓存失效,用户的请求访问直接访问数据库,导致数据库有风险。
特点:只影响一段时间(时间较短)
解决方法:让业务查询多个redis,并且保证数据不同一时间删除即可。
3.3 缓存雪崩
由于高并发的环境下,有大量的用户访问服务器,redis有大量的数据在同一时间超时(删除)
解决方法:让业务查询多个redis,并且包装数据不在统一时间删除即可
3.4 redis持久化问题
问题说明: redis中的数据都保存到内存中,如果我们服务关闭或者宕机则内存资源的直接丢失,导致缓存失效
3.4.1 持久化原理:
redis中有自己的持久化策略,redis启动时根据配置文件中指定的持久化方式进行持久化操作,redis中默认的持久化的方式为RDB的方式
3.4.2 RDB模式
RDB模式采用定期持久化方式,风险可能丢失数据
RDB模式记录的是当前redis的内存记录的快照(只记录挡墙的状态)持久化的效率是最高的
RDB模式是redis默认的持久化方式。
持久化的命令:
save //同步的执行持久化的命令操作 要求马上持久化,可能对现有造成阻塞
bgsave //异步的执行持久化命令操作 开启到哪都的线程实现持久化的任务
持久化的周期:
save 900 1 在900秒内如果执行一次更新操作,则持久化一次
save 300 10 在300秒内,如果执行10次更新操作,则持久化1次
save 60 10000 在60秒内,如果执行10000次更新操作,则持久化1次
用户操作越频繁,则持久化的周期越短
3.4.3 AOF模式
特点:
AOF默认是关闭状态如果需要,则需要手动开启
AOF能够记录程序的执行过程,可以实现数据的实时持久化,AOF文件占有的空间较大,回复数据的速度较慢
AOF模式开启后,RDB是不生效的
AOF的配置:
将appendoly改为yes
appendfsync always 实时持久化
appendfsync everysec 每秒持久化一IC,略低于rdb模式
appendfsync no 自己不主动持久化
3.4.4如何选择持久化方式:
思路:如果允许数据少量的丢失,则首选rdb,如果不允许数据丢失值使用ROF模式
3.4.5 情景思考:
小张在双11前夜误操作将redis服务器执行了flushAll命令,问项目经理如何解决
A:痛批一顿,让其提交离职申请。
B:批评教育,让其深刻反省,并请主管捏脚。
C:项目经理快速解决,并且通知全部门注意。
解决方法:
修改aof文件中的命令,删除flushall之后重启redis即可
3.5 redis内存的优化策略:
3.5.1 修改redis的内存
修改内存的大小的方式
vim redis.conf
:/set nu
//找到565行将#去掉并修改数据即可
3.5.2 LRU算法
概念:LRU是Least Recently Used的缩写,即最近最少使用,是一种常用的页面置换算法,选择最近最久未使用的页面予以淘汰。该算法赋予每个页面一个访问字段,用来记录一个页面自上次被访问以来所经历的时间 t,当须淘汰一个页面时,选择现有页面中其 t 值最大的,即最近最少使用的页面予以淘汰。
3.5.3 LUF算法
LFU(least frequently used (LFU) page-replacement algorithm)。即最不经常使用页置换算法,要求在页置换时置换引用计数最小的页,因为经常使用的页应该有一个较大的引用次数。但是有些页在开始时使用次数很多,但以后就不再使用,这类页将会长时间留在内存中,因此可以将引用计数寄存器定时右移一位,形成指数衰减的平均使用次数。
least frequently used (LFU) page-replacement algorithm
即最不经常使用页置换算法,要求在页置换时置换引用计数最小的页,因为经常使用的页应该有一个较大的引用次数。但是有些页在开始时使用次数很多,但以后就不再使用,这类页将会长时间留在内存中,因此可以将引用计数寄存器定时右移一位,形成指数衰减的平均使用次数。
3.5.4 RANDOM算法
random即随机数发生器,使用之前需要使用Randomize语句进行随机数种子的初始化。RANDOM产生的是伪随机数或者说是用一种复杂的方法计算得到的序列值,因此每次运算时需要一个不同的种子值。种子值不同,得到的序列值也不同。因此也就是真正的随机数了。这也正是RANDOMIZE随机初始化的作用。 VB里用 NEW RANDOM()来表示初始化。
3.5.5 内存优化策略
- volatile-lru 在设定了超时时间的数据,采用LRU算法进行删除
- allkeys-lru 所有数据采用LRU算法0
- volatile-lfu 在设定了超时时间的数据,采用LFU算法进行删除
- allkeys-lfu 所有数据采用LFU算法
- volatile-random 设定超时时间数据采用随机算法
- allkeys-random 所有数据采用随机算法
- volatile-ttl 设定了超时时间的数据根据ttl规则进行删除
- noeviction 内存满了不做任何操作,直接报错返回