1.AOP缓存的实现
1.1 自定义注解
在jt_common中创建annotation包
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,ElementType.FIELD,ElementType.TYPE}) // 注释的作用范围 在方法、属性、类中使用标识注解 @Target({
ElementType.METHOD})///使用标识注释 @Retention(RetentionPolicy.RUNTIME) ///什么时候有效? public @interface CacheFind {
//key-value方法的返回值 String key(); //要求用户指定key int seconds() default -1; //设置超时间 -1 无需超时 }
1.2 使用缓存注释
标识一个key
1.3 编辑RedisAOP
修改ItemSerevic中ItemCatService的类型
package com.jt.aop; import org.aspectj.lang.JoinPoint; import org.aspectj.lang.ProceedingJoinPoint; import org.aspectj.lang.annotation.Around; import.aspectj.lang.annotation.Aspect;
import org.aspectj.lang.annotation.Before;
import org.aspectj.lang.annotation.Pointcut;
import org.springframework.stereotype.Component;
import java.lang.reflect.Method;
@Component //将我们的对象交给Spring容器管理
@Aspect //表示AOP切面
public class RedisAop {
//通知选择: 是否控制目标方法是否执行. 环绕通知
//切入点表达式: 控制注解 @annotation(语法....)
/** * 需求: 如何动态获取注解中的属性值. * 原理: 反射机制 * 获取目标对象~~~~~获取方法对象~~~获取注解 原始API * @param joinPoint * @return * @throws Throwable * */
@Around("@annotation(com.jt.annotation.CacheFind)")//拦截一个注解
public Object around(ProceedingJoinPoint joinPoint) throws Throwable {
//1.获取目标对象类型,反射机制
Class targetClass = joinPoint.getTarget().getClass();
//2.获取方法
String name= joinPoint.getSignature().getName();//获取name/方法名
Object[] objArgs = joinPoint.getArgs();//获取参数
//对象转换class
Class[] classArgs = new Class[objArgs.length];
for (int i=0;i<objArgs.length;i++){
Object obj=objArgs[i];
classArgs[i] = obj.getClass();
}
Method method = targetClass.getMethod(name, classArgs);//得到方法名和方法对象
//拿到里面的注解
CacheFind cacheFind = method.getAnnotation(CacheFind.class);
String key= cacheFind.key();
//输出测试:
System.out.println(key);
return joinPoint.proceed();
}
}
测试:
注解此方法,接下来我们对重新进行优化
/** * 需求: 如何动态获取注解中的属性值. * 原理: 反射机制 * 获取目标对象~~~~~获取方法对象~~~获取注解 原始API * * @param joinPoint * @return * @throws Throwable * * 向上造型: 父类 = 子类 * 向下造型: 子类 = (强制类型转化)父类 * */
@Around("@annotation(com.jt.annotation.CacheFind)")//拦截一个注解
public Object around(ProceedingJoinPoint joinPoint) throws Throwable {
//向下造型:子类的引用指向父类对象 强制类型转化
//强制转换是有风险的,如果出现问题,需要自己解决、强转
// 向上造型:父类引用指向子类对象 不用强转
//子类属性方法 >= 父类 大的可以转换成小的,大不了舍弃不要的东西
MethodSignature methodSignature = (MethodSignature) joinPoint.getSignature();
Method method = methodSignature.getMethod();//获取对象
CacheFind cacheFind = method.getAnnotation(CacheFind.class);//获取注解
String key = cacheFind.key();
System.out.println("获取key:"+key);
return joinPoint.proceed();
}
测试:
根据spring提供的方法,我们继续优化
/* AOP中的语法规范1.: * 如果通知方法有参数需要添加,则joinPoint 必须位于第一位. * 报错信息: error at ::0 formal unbound in pointcut * AOP中的语法规范2: * 如果需要动态接受注解对象,则在切入点表达式中直接写注解参数名称即可 * 虽然看到的是名称,但是解析时变成了:包名.类型 */
//@Around("@annotation(com.jt.annotation.cacheFind)")//拦截一个注解
@Around("@annotation(cacheFind)")//拦截一个注解
public Object around(ProceedingJoinPoint joinPoint,CacheFind cacheFind) throws Throwable {
String key = cacheFind.key();
System.out.println("获取key:"+key);
return joinPoint.proceed();
}
package com.jt.aop;
import com.jt.annotation.CacheFind;
import com.jt.util.ObjectMapperUtil;
import org.aspectj.lang.JoinPoint;
import org.aspectj.lang.ProceedingJoinPoint;
import org.aspectj.lang.annotation.Around;
import org.aspectj.lang.annotation.Aspect;
import org.aspectj.lang.annotation.Before;
import org.aspectj.lang.annotation.Pointcut;
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.lang.reflect.Method;
import java.util.Arrays;
import java.util.List;
@Component //将我们的对象交给Spring容器管理
@Aspect //表示AOP切面
public class RedisAop {
//注入对象
@Autowired
private Jedis jedis;
//通知选择: 是否控制目标方法是否执行. 环绕通知
//切入点表达式: 控制注解 @annotation(语法....)
/** * 需求: 如何动态获取注解中的属性值. * 原理: 反射机制 * 获取目标对象~~~~~获取方法对象~~~获取注解 原始API * * @param joinPoint * @return * @throws Throwable * 向上造型: 父类 = 子类 * 向下造型: 子类 = (强制类型转化)父类 * * AOP中的语法规范1.: * 如果通知方法有参数需要添加,则joinPoint 必须位于第一位. * 报错信息: error at ::0 formal unbound in pointcut *AOP中的语法规范2: * 如果需要动态接受注解对象,则在切入点表达式中直接写注解参数名称即可 * 虽然看到的是名称,但是解析时变成了包名.类型 */
@Around("@annotation(cacheFind)")//拦截一个注解
public Object around(ProceedingJoinPoint joinPoint, CacheFind cacheFind) throws Throwable {
Object result = null;
//1.获取key="ITEM_CAT_PARENTID"
String key = cacheFind.key();
//2.动态拼接key 获取参数信息
//数组转换成字符串
String args = Arrays.toString(joinPoint.getArgs());
key += "::" + args;
//3.redis缓存实现
if (jedis.exists(key)) {
//存在就走缓存
//拿到缓存
String json = jedis.get(key);
//转换成具体的对象
MethodSignature methodSignature = (MethodSignature) joinPoint.getSignature();
//获取对象
Class returnType = methodSignature.getReturnType();
result = ObjectMapperUtil.toObject(json, returnType); //target:表示返回值的类型
System.out.println("AOP缓存查询!");
} else {
//不存在就走数据库
//查询数据库 执行目标方法
result = joinPoint.proceed();
//JSON转换
String json = ObjectMapperUtil.toJSON(result);
//存入缓存,设定超时时间
if (cacheFind.seconds() > 0)
jedis.setex(key, cacheFind.seconds(), json);
else
jedis.set(key, json);
System.out.println("AOP查询数据库");
}
return result;
}
// @Around("@annotation(com.jt.annotation.CacheFind)")//拦截一个注解
// public Object around(ProceedingJoinPoint joinPoint) throws Throwable {
// //向下造型:子类的引用指向父类对象 强制类型转化
// //强制转换是有风险的,如果出现问题,需要自己解决、强转
//
// // 向上造型:父类引用指向子类对象 不用强转
// //子类属性方法 >= 父类 大的可以转换成小的,大不了舍弃不要的东西
// MethodSignature methodSignature = (MethodSignature) joinPoint.getSignature();
// Method method = methodSignature.getMethod();//获取对象
// CacheFind cacheFind = method.getAnnotation(CacheFind.class);//获取注解
// String key = cacheFind.key();
// System.out.println("获取key:"+key);
// return joinPoint.proceed();
// }
// @Around("@annotation(com.jt.annotation.CacheFind)")//拦截一个注解
// public Object around(ProceedingJoinPoint joinPoint) throws Throwable {
// //1.获取目标对象类型,反射机制
// Class targetClass = joinPoint.getTarget().getClass();
// //2.获取方法
// String name= joinPoint.getSignature().getName();//获取name/方法名
// Object[] objArgs = joinPoint.getArgs();//获取参数
// //对象转换class
// Class[] classArgs = new Class[objArgs.length];
// for (int i=0;i<objArgs.length;i++){
// Object obj=objArgs[i];
// classArgs[i] = obj.getClass();
// }
// Method method = targetClass.getMethod(name, classArgs);//得到方法名和方法对象
// //拿到里面的注解
// CacheFind cacheFind = method.getAnnotation(CacheFind.class);
// String key= cacheFind.key();
// //输出测试:
// System.out.println(key);
// return joinPoint.proceed();
// }
/** * 1.定义切入点表达式 * bean: 被spring容器管理的对象称之为 bean * 1.1 bean(bean的ID) 按类匹配 1个 * bean(itemCatServiceImpl) * <p> * 1.2 within(包名.类名) 按类匹配 一堆 * within(com.jt.service.*) * <p> * 1.3 execution(返回值类型 包名.类名.方法名(参数列表)) * execution(* com.jt.service..*.*(..)) 万能的表达式 * service. 一级 * service.. 多级 * 解释: 返回值为任意类型 com.jt.service包所有的子孙包的类 * 类中的任意方法,任意参数 * execution(Integer com.jt.service..*.add*(int)) //只能是包装类型 * execution(int com.jt.service..*.add*(int)) 只能是基本类型 */
/* //@Pointcut("execution(* com.jt.service..*.*(..))") @Pointcut("bean(itemCatServiceImpl)") public void pointCut() { } //Aop如何与目标方法进行关联? JoinPoint(连接点) //如何理解什么是连接点? 被切入点拦截的方法(连接点=方法) //ProceedingJoinPoint is only supported for around advice //只有环绕通知可以控制目标方法 @Before("pointCut()") //乌龟的屁股 规定 public void before(JoinPoint joinPoint) { //前置通知 //获取目标方法的名称 String methodName = joinPoint.getSignature().getName(); //获取目标类的名称 String className = joinPoint.getSignature().getDeclaringTypeName(); //获取目标的参数 Object[] args = joinPoint.getArgs(); //获取目标的对象 Object target = joinPoint.getTarget(); //输出 System.out.println(methodName); System.out.println(className); System.out.println(args); System.out.println(target); System.out.println("我是一个前置通知"); } @Around("pointCut()") public Object around(ProceedingJoinPoint joinPoint) throws Throwable { System.out.println("环绕开始"); Object result = joinPoint.proceed(); //执行下一个通知,目标方法 System.out.println("环绕结束"); return result; }*/
}
2 关于Redis 持久化机制
2.1 业务需求
Redis
中的运行环境在内存中, 但是内存特点 断电即擦除
。
能否保存redis中的内存数据不丢失呢?
将内存数据定期保存到磁盘中。
2.2 RDB模式
2.2.1 关于RDB模式说明
1.Redis ,将内存数据保存到RDB文件中。
2.RDB模式 redis 默认的规则。
3.RDB模式记录的是,持久化效率更高(只保留最新数据)
4.RDB模式由于持久化,可能导致。
2.2.2 RDB命令
1: 持久化操作: save
同步操作 可能其他线程陷入阻塞
2: 后端持久化 :bgsave
异步操作 用户操作不会陷入阻塞 该操作什么时候完成不清楚。
2.2.3 RDB模式配置
save 900 1 900秒内用户更新1次 则持久化1次 save 300 10 300秒内用户更新10次 持久化 1次 save 60 10000 60秒内用户更新10000次 持久化1次
面试题:save 1 1
是什么意思?
用户1秒操作一次,就更新一次 保证数据安全性 问题:但是效率极低 容易阻塞…
如果想让持久化性能更优,则需要通过的手段灵活运用.
用户操作越频繁,则持久化周期越短。
2.3 AOF模式
2.3.1 开启AOF模式
默认条件下AOF模式 默认关闭的。
输入命令:vim redis.conf
展现行号:set nu
查找:/appendonly
开启AOF
2.3.2 AOF模式特点
当开启AOF策略之后,redis持久化以AOF为主。
- AOF文件默认关闭的,需要手动开启
- AOF文件记录的是用户的操作过程.则可以实现实时持久化操作.(几乎不丢数据)
- AOF文件做追加的操作,所有持久化文件较大
- AOF持久化时,采用异步的方式进行
- AOF文件需要定期清理
2.3.3 AOF持久化原则
appendfsync always 用户执行一次操作,持久化一次 appendfsync everysec 每秒持久化一次 appendfsync no 不主动持久化
2.3.4 关于AOF与RDB如何选择
1.如果用户追求速度,允许少量的数据丢失 首选RDB模式,因为它很快
2.如果用户追求数据的安全性. 首选AOF模式,数据基本不会丢失
1.redis持久化的策略有哪几种?
- RDB模式是redis的默认规则,它记录的是内存数据的快照,
- AOF 记录的是用户的操作过程,它几乎可以实现实时的持久化操作
2.如果你在redis中执行了flushAll命令,如何挽救?
修改AOF文件中,删除flushAll的命令.重启redis即可。
一般条件下 Redis会开启AOF与RDB 2种模式 一般会配置redis主从结构 主机开启RDB模式 从机开启AOF模式 输入命令:vim appendonly.aof
,进入之后干掉刚刚的删除命令, wq
保存,重启redis:redis-cli shutdown
,启动:redis-server
3 关于Redis 内存优化策略
3.1 关于内存优化的说明
Redis运行环境,在内存中. 但是内存资源有限的.不能一味的扩容.所以需要对内存数据优化. Redis内存大小的设定:vim redis.conf
最大内存设定:
3.2 LRU算法
LRU是Least Recently Used的缩写,即,是一种常用的页面置换算法,选择最近最久未使用的页面(数据)予以淘汰。该算法赋予每个页面一个访问字段,用来记录一个页面自上次被访问以来所经历的时间 t,当须淘汰一个页面时,选择现有页面中其 t 值最大的,即最近最少使用的页面予以淘汰。
时间T 链表
3.3 LFU算法
LFU(least frequently used (LFU) page-replacement algorithm)。即最不经常使用,要求在页置换时置换最小的页,因为经常使用的页应该有一个较大的引用次数。但是有些页在开始时使用次数很多,但以后就不再使用,这类页将会长时间留在内存中,因此可以将引用计数寄存器定时右移一位,形成指数衰减的平均使用次数。
引用次数 链表
3.4 随机算法
说明: 随机生成挑选数据删除。
3.5 TTL算法
说明: 根据设定了超时时间的数据,将马上要超时的数据提前删除。
3.6 算法优化
- volatile-lru 设定超时时间的数据中,采用lru算法删除数据.
- allkeys-lru 在所有的数据中 采用LRU算法删除数据.
- volatile-lfu 在设定了超时时间的数据中 采用LFU算法删除数据.
- allkeys-lfu 在所有数据中. 采用LFU算法删除数据.
- volatile-random 设定了超时时间数据,采用随机的方式删除数据
- allkeys-random 所有数据采用随机算法.
- volatile-ttl 设定超时时间的数据,采用TTl算法删除.
noeviction 默认不删除数据. 如果内存满了,则报错返回,默认策略。
4 关于Redis缓存常见面试题
4.1 什么是缓存穿透
说明: 在高并发的条件下,,导致大量的请求直接发往数据库.导致服务器宕机。
- 只要能够保障用户访问的数据 数据库中一定存在.
- IP限流、黑名单、 微服务中可以通过API网关进行控制
4.2 什么是缓存击穿
说明: 在高并发条件下 用户频繁访问 但是该热点数据在缓存中失效.导致用户直接访问数据库。
俗语: 称它病,要它命
4.3 什么是缓存雪崩
说明: 在高并发的条件下, 有, 导致redis缓存服务器命中率太低.从而导致用户直接访问数据库。
主要问题:
解决方案:
1.为热点数据设定超时时间时 采用随机数. 10秒+随机数 保障数据不会同时失效 2.设定多级缓存
4.4 布隆过滤器
4.4.1 介绍
布隆过滤器(Bloom Filter)是1970年由布隆提出的。它实际上是一个很长的和一系列。。它的优点是和查询时间都比一般的算法要好的多,缺点是有一定的和。
4.4.2 计算机进制换算
- 1字节 = 8比特
- 1kb = 1024字节
- 1mb = 1024kb
4.4.3 业务场景
假设数据库中有1000万数据,每条记录大约2kb 如果该数据都是热点数据,则需要多大的内存空间进行存储? 19G数据
1000000*2/1024/1024=19
问题: 能否优化内存数据的存储呢? 尽可能少占用内存空间!
想法: 如果使用1个bit(01)
代表一个数据, 问占用多大空间 1.19M
4.4.4 布隆过滤器原理
核心1: 很长的二进制向量 核心2: 多个hash函数 解决问题: 校验数据是否存在!!!
1).数据加载过程 2).数据校验 3).存在问题 好的布隆算法可以将误判率 降低到 < 0.03%
5 Redis分片机制
5.1 为什么需要分片
说明: 如果有海量的内存数据需要保存,但是都把数据保存到1个redis中,查询的效率太低.如果这台redis服务器宕机,则整个缓存将不能使用。
: 采用redis分片机制.
5.2 Redis分片搭建
5.2.1 准备配置文件
5.2.2 修改端口号
说明: 分别将6379/6380/6381的端口号进行配置
5.2.3 启动三台redis
[root@localhost shards]# redis-server 6379.conf & redis-server 6380.conf & redis-server 6381.conf &
检查是否启动成功
5.2.4 redis分片入门案例
public class TestRedis2 {
/** * 3台redes key如何存储?? 79/80/81 */
@Test
public void testShards(){
//创建集合对象
List<JedisShardInfo> shards = new ArrayList<>();
//添加端口号
shards.add(new JedisShardInfo("192.168.126.129", 6379));
shards.add(new JedisShardInfo("192.168.126.129", 6380));
shards.add(new JedisShardInfo("192.168.126.129", 6381));
//把端口传入shardedJedis
ShardedJedis shardedJedis = new ShardedJedis(shards);
//通过用户的操作就行操作
shardedJedis.set("shards", "redis分片机制");
//输出测试
System.out.println(shardedJedis.get("shards"));
}
}
5.3 一致性hash算法
5.3.1 介绍
一致性哈希算法在1997年由麻省理工学院提出,是一种特殊的哈希算法,目的是解决分布式缓存的问题。 [1] 在移除或者添加一个服务器时,能够尽可能小地改变已存在的服务请求与处理请求服务器之间的映射关系。一致性哈希解决了简单哈希算法在分布式哈希表( Distributed Hash Table,DHT) 中存在的动态伸缩等问题 [2] 。
1.一致性hash解决了数据与节点的映射关系(数据归谁管理) 2.节点增加/减少时,数据可以弹性伸缩.
5.3.2 一致性hash算法原理
顺时针:因为加比减快,例如生活中:电容,手机充电插上秒冲,拔出过后电量还有预电。
5.3.3 特性-平衡性
说明: 平衡性是指hash的结果应该,这样从算法上解决了负载均衡问题
引入虚拟节点
5.3.4 特性-单调性
②单调性是指在新增或者删减节点时,不影响系统正常运行 。 说明: 无论节点增/减,数据都能找到与之匹配的node进行数据的挂载.
5.3.5 特性-分散性
③分散性是指数据应该分散地存放在分布式集群中的各个节点(节点自己可以有备份),不必每个节点都存储所有的数据 。 谚语: 鸡蛋不要放到一个篮子里.
百度原理图1:
百度原理图2:
5.4 SpringBoot整合Redis分片
5.4.1 编辑配置文件
# 单台redis配置
#redis.host=192.168.126.129
#redis.port=6379
#Redis分片机制
redis.nodes=192.168.126.129:6379,192.168.126.129:6380,192.168.126.129:6381
5.4.2 编辑配置类
package com.jt.config;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.context.annotation.PropertySource;
import redis.clients.jedis.*;
import java.util.ArrayList;
import java.util.List;
@Configuration //标识为一个配置类, 一般整合第三方
@PropertySource("classpath:/properties/redis.properties")
public class RedisConfig {
//注入节点信息
@Value("${redis.nodes}")
private String nodes; //node,node,node
@Bean
public ShardedJedis shardedJedis(){
//new一个对象
List<JedisShardInfo> shards = new ArrayList<>();
//给数据赋值,获取url和ip
//把nodes按照 ,号,拆分成一个一个节点的数组
String[] nodeArray = nodes.split(",");
//node=host:port
for (String node : nodeArray){
//获取元素
String host = node.split(":")[0];
//拆分端口号,使用工具api把字符转换成int
int port = Integer.parseInt(node.split(":")[1]);
//new一个对象,封装数据
JedisShardInfo info = new JedisShardInfo(host, port);
//封装到shards中
shards.add(info);
}
return new ShardedJedis(shards);
//2.编辑redis配置文件,调整链接数量
/*JedisPoolConfig jedisPoolConfig = new JedisPoolConfig(); //调整连接池的大小 jedisPoolConfig.setMinIdle(10); //最小空闲数量,最小连接数 jedisPoolConfig.setMaxIdle(40); //最大的空闲数量 jedisPoolConfig.setMaxTotal(1000);//最大连接数 //调用连接池 ShardedJedisPool shardedJedisPool = new ShardedJedisPool(jedisPoolConfig, shards); return shardedJedisPool.getResource();*/
}
/* @Value("${redis.host}") private String host; @Value("${redis.port}") private Integer port; @Bean //将该方法的返回值,交给Spring容器管理 public Jedis jedis(){ return new Jedis(host,port); }*/
}
5.4.3 修改RedisAOP
//注入对象
@Autowired
//private Jedis jedis; //单台redis
private ShardedJedis jedis; //分片redis 内存扩容
运行测试:
6.Redis哨兵机制
6.1 关于Redis分片特点
Redis分片可以实现内存数据的扩容,但是如果节点宕机则直接影响程序的运行!
问题关键: 如果节点宕机,需要实现高可用
6.2 Redis数据同步配置
关闭所有分片:
6.2.1 复制目录
说明: 将shards目录复制 并且改名为sentinel
cp -r