1. Redis缓存的实现
1.1 自定义注解
package com.jt.anno;
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 {
//value是方法的返回值
String key(); //要求用户必须指定key
int seconds() default -1; //设定超时时间 -1 无需超时
}
1.2 使用缓存的注解
/**
* 根据parentId查询商品分类列表信息 一级商品分类信息
* 将商品分类列表转化为list<vo>对象
* 返回vo的list集合
* @param parentId
* @return
*/
@Override
@CacheFind(key = "ITEM_CAT_PARENTID")
public List<EasyUITree> findItemCatList(long parentId) {
QueryWrapper<ItemCat> queryWrapper = new QueryWrapper<>();
queryWrapper.eq("parent_id", parentId);
List<ItemCat> itemCatList = itemCatMapper.selectList(queryWrapper);
//将itemcat的对象转化为vo对象
List<EasyUITree> voList = new ArrayList<>(itemCatList.size());
for (ItemCat itemCat : itemCatList) {
long id = itemCat.getId();
String text = itemCat.getName();
String state = itemCat.getIsParent() ? "closed" : "open";
EasyUITree tree = new EasyUITree(id,text,state);
voList.add(tree);
}
return voList;
}
1.3 编辑RedisAOP
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中的语法规范3:
* 如果需要动态接受注解对象,则在切入点表达式中直接写注解参数名称即可
* 虽然看到的是名称,但是解析时变成了包名.类型
*/
@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);
//target标识返回值类型????
MethodSignature methodSignature = (MethodSignature) joinPoint.getSignature();
Class returnType = methodSignature.getReturnType();
result = ObjectMapperUtil.toObject(json, returnType);
System.out.println("AOP缓存查询!!!");
}else{
//查询数据库 执行目标方法
result = joinPoint.proceed();
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();
Object[] objArgs = joinPoint.getArgs();
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)
* 1.2 within(包名.类名) 按类匹配 一堆
* within(com.jt.service.*)
* 1.3 execution(返回值类型 包名.类名.方法名(参数列表))
* execution(* com.jt.service..*.*(..))
* 解释: 返回值为任意类型 com.jt.service包所有的子孙包的类
* 类中的任意方法,任意参数
* execution(Integer com.jt.service..*.add*(int))
* execution(int com.jt.service..*.add*(int))
*/
/* @Pointcut("execution(* com.jt.service..*.*(..))")
public void pointCut(){
}
//如何理解什么是连接点? 被切入点拦截的方法
//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 保证数据安全性 问题:效率极低 阻塞…
如果想让持久化性能更优,则需要通过监控的手段灵活运用.
用户操作越频繁,则持久化周期越短.
2.3 AOF模式
2.3.1 开启AOF模式
默认条件下AOF模式 默认关闭的.
开启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模式.
面试题:
如果你在redis中执行了flushAll命令,如何挽救??
答案: 修改AOF文件中,删除flushAll的命令.重启redis即可.
注意事项: 一般条件下 Redis会开启AOF与RDB 2种模式
常规用法: 一般会配置redis主从结构 主机开启RDB模式 从机开启AOF模式
3 关于Redis 内存优化策略
3.1 关于内存优化的说明
Redis运行环境,在内存中. 但是内存资源有限的.不能一味的扩容.所以需要对内存数据优化.
Redis内存大小的设定:
最大内存设定:
3.2 LRU算法
3.2.1 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 什么是缓存穿透
发生场景
说明: 缓存穿透的概念很简单,用户想要查询一个数据,发现redis内存数据库没有,也就是缓存没有命中,于是向持久层数据库查询。发现也没有,于是本次查询失败。当用户很多的时候,缓存都没有命中,于是都去请求了持久层数据库。这会给持久层数据库造成很大的压力,这时候就相当于出现了缓存穿透。
解决方案
- 对请求参数做校验,例如可以用正则;
- 缓存空对象, 当存储层不命中后,即使返回的空对象也将其缓存起来,同时会设置一个过期时间,之后再访问这个数据将会从缓存中获取,保护了后端数据源;
但是这种方法会存在两个问题:
- 如果空值能够被缓存起来,这就意味着缓存需要更多的空间存储更多的键,因为这当中可能会有很多的空值的键;
- 即使对空值设置了过期时间,还是会存在缓存层和存储层的数据会有一段时间窗口的不一致,这对于需要保持一致性的业务会有影响。
4.2 什么是缓存击穿
发生场景
说明: 缓存击穿,是指一个key非常热点,在不停的扛着大并发,大并发集中对这一个点进行访问,当这个key在失效的瞬间,持续的大并发就穿破缓存,直接请求数据库,就像在一个屏障上凿开了一个洞。
解决方案
- 可以将热点数据的过期时间设置为永久有效(有人可能会问,万一这个热点商品下架了,这个缓存不就成了了脏数据吗?其实会有这种场景存在,主要还是具体情况具体分析,看业务场景吧);
- 维护一个定时任务,将快要过期的key重新设置;
- 可以使用分布式锁,当在缓存中拿不到数据时,使用分布式锁去数据库中拿到数据后,重新设置到缓存;
4.3 什么是缓存雪崩
发生场景
说明: 缓存雪崩是指缓存中数据大批量到过期时间,而查询数据量巨大,引起数据库压力过大甚至down机。和缓存击穿不同的是,缓存击穿指并发查同一条数据,缓存雪崩是不同数据都过期了,很多数据都查不到从而查数据库。
解决方案
- 缓存数据的过期时间设置随机,防止同一时间大量数据过期现象发生。
- 如果缓存数据库是分布式部署,将热点数据均匀分布在不同的缓存数据库中。
- 设置热点数据永远不过期。
4.4 布隆过滤器
4.4.1 介绍
布隆过滤器(Bloom Filter)是1970年由布隆提出的。它实际上是一个很长的二进制向量和一系列随机映射函数。布隆过滤器可以用于检索一个元素是否在一个集合中。它的优点是空间效率和查询时间都比一般的算法要好的多,缺点是有一定的误识别率和删除困难。
4.4.2 计算机进制换算
1字节 = 8比特
1kb = 1024字节
1mb = 1024kb
4.4.3 业务场景
假设数据库中有1000万数据,每条记录大约2kb 如果该数据都是热点数据,则需要多大的内存空间进行存储? 19G数据!!!
问题: 能否优化内存数据的存储呢??? 尽可能少占用内存空间.
想法: 如果使用1个**bit(01)**代表一个数据, 问占用多大空间 1.19M
4.4.4 布隆过滤器原理
核心1: 很长的二进制向量
核心2: 多个hash函数
解决问题: 校验数据是否存在!!!
- 数据加载过程
- 数据校验
- 存在问题
好的布隆算法可以将误判率 降低到 < 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
redis-server 6379.conf & redis-serve
校验redis
5.2.4 redis分片入门案例
/**
* 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 = 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] 。
核心知识:
- 一致性hash解决了数据与节点的映射关系(数据归谁管理)
- 节点增加/减少时,数据可以弹性伸缩.
5.3.2 一致性hash算法原理
5.3.3 特性-平衡性
说明: 平衡性是指hash的结果应该平均分配到各个节点,这样从算法上解决了负载均衡问题
解决策略: 引入虚拟节点
5.3.4 特性-单调性
②单调性是指在新增或者删减节点时,不影响系统正常运行 [4] 。
说明: 无论节点增/减,数据都能找到与之匹配的node进行数据的挂载.
5.3.5 特性-分散性
③分散性是指数据应该分散地存放在分布式集群中的各个节点(节点自己可以有备份),不必每个节点都存储所有的数据 [4] 。
谚语: 鸡蛋不要放到一个篮子里.
百度原理图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(){
List<JedisShardInfo> shards = new ArrayList<>();
String[] nodeArray = nodes.split(",");
for (String node : nodeArray){ //node=host:port
String host = node.split(":")[0];
int port = Integer.parseInt(node.split(":")[1]);
JedisShardInfo info = new JedisShardInfo(host, port);
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
6.Redis哨兵机制
6.1 关于Redis分片特点
Redis分片可以实现内存数据的扩容,但是如果节点宕机则直接影响程序的运行.
问题关键: 如果节点宕机,需要实现高可用.
6.2 Redis数据同步配置
6.2.1 复制目录
说明: 将shards目录复制 并且改名为sentinel
6.2.2 启动3台redis
说明:
- 删除原有的持久化文件
- 启动3台redis
6.2.3 主从结构搭建
- 6379 主机 6380/6381 从机
- 主从挂载命令
slaveof 192.168.126.129 6379
6.3 Redis哨兵实现
6.3.1 哨兵原理
说明:
1.当哨兵启动时,会链接redis主节点,获取所有节点的相关信息.
2.当哨兵通过心跳检测机制 PING -PONG 命令校验服务器是否正常.
3.如果哨兵连续3次发现服务器没有响应,则断定当前主机宕机.
4.之后由哨兵采用随机算法挑选其中的一个从机当选主机.并且其他的节点当做新主机的从.
6.3.2 配置哨兵服务
- 复制文件
cp sentinel.conf sentinel/
- 关闭保护模式
- 开启后台运行
- 哨兵监控
- 修改宕机时间
6.3.3 哨兵命令
1.启动 redis-sentinel sentinel.conf
2.关闭哨兵 ps -ef |grep redis kill -9 PID号
6.3.4 哨兵高可用测试
- 关闭6379主机,检查从机是否当选主机
- 检查从机的配置文件 是否以后关联了主机
如果搭建错误,.则需要删除最后一条主从关系.,之后重启服务器.重新搭建.
6.4 SpringBoot整合哨兵
6.4.1 入门案例
/**
* 测试哨兵API
*/
@Test
public void testSentinel(){
Set<String> sets = new HashSet<>();
sets.add("192.168.126.129:26379");
JedisPoolConfig poolConfig = new JedisPoolConfig();
poolConfig.setMinIdle(10);
poolConfig.setMaxIdle(40);
poolConfig.setMaxTotal(1000);
JedisSentinelPool pool = new JedisSentinelPool("mymaster",sets,poolConfig);
Jedis jedis = pool.getResource();
jedis.set("AAA", "您好Redis");
System.out.println(jedis.get("AAA"));
jedis.close();
}
6.5 关于Redis分片/哨兵总结
- 分片作用: 扩大内存实现海量数据数据的存储. 缺点: 没有实现高可用
- 哨兵作用: 实现了节点的高可用. 缺点: 1.哨兵不能实现扩容 2.哨兵本身没有高可用
想法: 能否有一种机制 既可以实现内存扩容,又可以实现高可用(不需要第三方)
7.Redis集群规则
7.1关于集群搭建错误说明
- 关闭redis集群
- 删除多余的配置文件
- 重启redis
- 搭建redis集群