spring-data-jpa spring-data-jdbc spring-data-redis
说明: 在 SpringBoot2.x 之后,原来使用的jedis 被替换为了 lettuce
jedis : 采用的直连,多个线程操作的话,是不安全的,如果想要避免不安全的,使用 jedis pool 连接池
lettuce : 采用netty,实例可以再多个线程中进行共享,不存在线程不安全的情况!可以减少线程数据了
1.加入Redis相关依赖
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-redis</artifactId>
</dependency>
2.application.properties中加入redis相关配置
# Redis数据库索引(默认为0)
spring.redis.database=0
# Redis服务器地址
spring.redis.host=192.168.0.24
# Redis服务器连接端口
spring.redis.port=6379
# Redis服务器连接密码(默认为空)
spring.redis.password=
# 连接池最大连接数(使用负值表示没有限制)
spring.redis.pool.max-active=200
# 连接池最大阻塞等待时间(使用负值表示没有限制)
spring.redis.pool.max-wait=-1
# 连接池中的最大空闲连接
spring.redis.pool.max-idle=10
# 连接池中的最小空闲连接
spring.redis.pool.min-idle=0
# 连接超时时间(毫秒)
spring.redis.timeout=1000
3.具体代码了解
spring data redis中封装了两个模板类,帮助我们实现redis的crud
RedisTemplate key value泛型都是object
StringRedisTemplate key value泛型都是string
1.两者数据各自存,各自取,数据不互通。
RedisTemplate不能取StringRedisTemplate存入的数据
StringRedisTemplate不能取RedisTemplate存入的数据
2.序列化策略不同:
RedisTemplate采用JDK的序列化策略(JdkSerializationRedisSerializer)保存的key和value都是采用此策略序列化保存的.
存储时,先将数据序列化为字节数组,再存入Redis数据库。查看Redis会发现,是字节数组的形式类似乱码读取时,会将数据当做字节数组转化为我们需要的数据,以用来存储对象,但是要实现
Serializable接口
StringRedisTemplate采用String的序列化策略(StringRedisSerializer)保存的key和value都是采用此策略序列化保存的当存入对象时,会报错:can not cast into String 存储和读取,都为可读的数据
3.两者的关系是StringRedisTemplate继承RedisTemplate
4.使用场景:
当你的redis数据库里面本来存的是字符串数据或者你要存取的数据就是字符串类型数据的时候,那么你就使用StringRedisTemplate即可。
但是如果你的数据是复杂的对象类型,而取出的时候又不想做任何的数据转换,直接从Redis里面取出一个对象,那么使用RedisTemplate是更好的选择。
五大数据类型
redisTemplate.opsForValue();//操作字符串
redisTemplate.opsForList();//操作List
redisTemplate.opsForSet();//操作Set
redisTemplate.opsForZSet();//操作ZSet
redisTemplate.opsForHash();//操作Hash
@Test
void contextLoads() {
//1.获取所有key
Set<String> keys = stringRedisTemplate.keys("*");
for (String key : keys){
System.out.println(key);
}
//各种类型支持
stringRedisTemplate.opsForList();
stringRedisTemplate.opsForHash();
stringRedisTemplate.opsForSet();
stringRedisTemplate.opsForValue();
stringRedisTemplate.opsForZSet();
//3.举例字符串
stringRedisTemplate.opsForValue().set("name","王伟涛");
String name = stringRedisTemplate.opsForValue().get("name");
System.out.println(name);
//4.操作list列表
stringRedisTemplate.opsForList().leftPush("mylist","wwt");
stringRedisTemplate.opsForList().rightPushAll("mylist","ppy","yyd");
stringRedisTemplate.opsForList().rightPop("mylist");
List<String> mylist = stringRedisTemplate.opsForList().range("mylist", 0, 1);
System.out.println(mylist);
}
//测试RedisTemplate 法意: 1.测试RedisTemplate与stringRedisTemplate存的数据相互独立
// 2.redisTemplate默认使用key序列化方式和value的序列化方式都使用的是jdk serializer序列化 所以存对象会乱码
@Test
void redisTemplates(){
ValueOperations valueOperations = redisTemplate.opsForValue();
valueOperations.set("name","wwt");
String name = (String)redisTemplate.opsForValue().get("name");
System.out.println(name);
//各种类型支持
redisTemplate.opsForList();
redisTemplate.opsForHash();
redisTemplate.opsForSet();
redisTemplate.opsForValue();
redisTemplate.opsForZSet();
Studnet s = new Studnet(1,"wls","xj");
redisTemplate.opsForValue().set("stu",s);
Studnet s1 = (Studnet) redisTemplate.opsForValue().get(s);
System.out.println(s1);
redisTemplate.opsForList().leftPushAll("mylist","睡觉","游戏");
List<String> list = redisTemplate.opsForList().range("mylist",0,-1);
for (int i = 0; i < list.size(); i++) {
String ss = list.get(i);
System.out.println(s);
}
System.out.println("打印默认序列策略"+redisTemplate.getDefaultSerializer());
}
序列化策略
改变序列化策略
默认序列化方式存储到redis的数据人工不可读
不同策略序列化的过程有性能高低的
spring-data-redis提供如下几种序列化策略
GenericToStringSerializer: 可以将任何对象泛化为字符串并序列化
Jackson2JsonRedisSerializer: 跟JacksonJsonRedisSerializer实际上是一样的
JacksonJsonRedisSerializer: 序列化object对象为json字符串
JdkSerializationRedisSerializer: 序列化java对象
StringRedisSerializer: 简单的字符串序列化
6.redis的配置
7.redis持久化
Redis 是内存数据库,如果不将内存中的数据库状态保存到磁盘,那么一旦服务器进程退出,服务器中 的数据库状态也会消失。所以 Redis 提供了持久化功能!
持久化过程保存什么
1.将当前数据状态进行保存,快照形式,存储数据结果,存储格式简单,关注点在数据 (RDB)
2.将数据的操作过程进行保存,日志形式,存储操作过程,关注点在数据的操作过程(AOF)
7.1 RDB方式
概念:
在指定的时间间隔内将内存中的数据集快照写入磁盘, 也就是行话讲的Snapshot快照,它恢复时是将 快照文件直接读到内存里
RDB手动
save指令 命令 :save 作用 :手动执行一次保存操作
save指令相关配置 dbfilename dump.rdb 说明:设置本地数据库文件名,默认值为 dump.rdb 经验:通常设置为 dump-端口号.rdb
dir 说明:设置存储.rdb文件的路径 经验:通常设置成存储空间较大的目录中,目录名称data
rdbcompression yes 说明:设置存储至本地数据库时是否压缩数据,默认为 yes,采用 LZF算法 压缩 经验:通常默认为开启状态,如果设置为no,可以节省 CPU 运行时间,但会使存储的文件变大(巨 大)
rdbchecksum yes 说明:设置是否进行CRC64算法RDB文件格式校验, 该校验过程在写文件和读文件过程均进行 经验:通常默认为开启状态,如果设置为no,可以节约读写性过程约10%时间消耗,但是存储一定的数 据损坏风险
save指令工作原理(单线程任务执行序列) 客户端1 127.0.0.1:6379>set key1 value1 客户端2 127.0.0.1:6379>set key2 value2 客户端3 127.0.0.1:6379>save
客户端4 127.0.0.1:6379>get key 对redis数据库执行4此指令顺序===> set set save get 注意:save指令的执行会阻塞当前Redis服务器,直到当前RDB过程完成为止,有可能会造成长时间阻 塞,线上环境不建议使用
bgsave指令 命令 :bgsave 作用 :手动启动后台保存操作,但不是立即执行
RDB自动
配置 :save second changes 作用 : 满足限定时间范围内key的变化数量达到指定数量即进行持久化
参数 :
second:监控时间范围
changes:监控key的变化量
位置 : 在conf文件中进行配置
注意: save配置要根据实际业务情况进行设置,频度过高或过低都会出现性能问题,结果可能是灾难性的 save配置中对于second与changes设置通常具有互补对应关系,尽量不要设置成包含性关系 save配置启动后执行的是bgsave操作
RDB优点
RDB是一个紧凑压缩的二进制文件,存储效率较高 RDB内部存储的是redis在某个时间点的数据快照,非常适合用于数据备份,全量复制等场景 RDB恢复数据的速度要比AOF快很多 RDB节省磁盘空间
RDB缺点
Fork的时候,内存中的数据被克隆了一份,大致2倍的膨胀性需要考虑 虽然Redis在fork时使用了写时拷贝技术,但是如果数据庞大时还是比较消耗性能 RDB方式无论是执行指令还是利用配置,无法做到实时持久化,具有较大的可能性丢失数据 Redis的众多版本中未进行RDB文件格式的版本统一,有可能出现各版本服务之间数据格式无法兼容现象
7.2 AOF方式
概念: AOF(append only file)持久化:以独立日志的方式记录每次写命令,重启时再重新执行AOF文件中命令 达到恢复数据的目的;与RDB相比可以简单描述为改记录数据为记录数据产生的过程AOF的主要作用是 解决了数据持久化的实时性,目前已经是Redis持久化的主流方式
AOF执行过程
客户端的请求写命令会被append追加到AOF缓冲区内; AOF缓冲区根据AOF持久化策略[always,everysec,no]将操作sync同步到磁盘的AOF文件中; AOF文件大小超过重写策略或手动重写时,会对AOF文件rewrite重写,压缩AOF文件容量; Redis服务重启时,会重新load加载AOF文件中的写操作达到数据恢复的目的;
AOF写数据三种策略(appendfsync)
always(每次) 每次写入操作均同步到AOF文件中,数据零误差,性能较低
everysec(每秒) 每秒将缓冲区中的指令同步到AOF文件中,数据准确性较高,性能较高 在系统突然宕机的情况下丢失1秒内的数据
no(系统控制) 由操作系统控制每次同步到AOF文件的周期,整体过程不可控
AOF相关配置 配置 :appendonly yes|no 作用 :是否开启AOF持久化功能,默认为不开启状态 配置 :appendfsync always|everysec|no 作用 :AOF写数据策略 配置:appendfilename filename 作用:AOF持久化文件名,默认文件名未appendonly.aof,建议配置为appendonly-端口号.aof 配置:dir 作用 :AOF持久化文件保存路径,与RDB持久化文件保持一致即可
AOF写数据遇到的问题
AOF重写 随着命令不断写入AOF,文件会越来越大,为了解决这个问题,Redis引入了AOF重写机制压缩文件体 积。AOF文件重写是将Redis进程内的数据转化为写命令同步到新AOF文件的过程。简单说就是将对同 一个数据的若干个条命令执行结果转化成最终结果数据对应的指令进行记录
AOF重写作用 降低磁盘占用量,提高磁盘利用率 提高持久化效率,降低持久化写时间,提高IO性能 降低数据恢复用时,提高数据恢复效率
AOF重写规则 进程内已超时的数据不再写入文件 忽略无效指令,重写时使用进程内数据直接生成,这样新的AOF文件只保留最终数据的写入命令 如del key1、 hdel key2、srem key3、set key4 111、set key4 222等 对同一数据的多条写命令合并为一条命令 如lpush list1 a、lpush list1 b、 lpush list1 c 可以转化为:lpush list1 a b c。 为防止数据量过大造成客户端缓冲区溢出,对list、set、hash、zset等类型,每条指令最多写入64个元素 AOF和RDB同时开启,系统默认取AOF的数据(数据不会存在丢失)
AOF重写方式
手动重写 bgrewriteaof 自动重写 触发机制,何时重写
Redis会记录上次重写时的AOF大小,默认配置是当AOF文件大小是上次rewrite后大小的一倍且文 件大于64M时触发;重写虽然可以节约大量磁盘空间,减少恢复时间。但是每次重写还是有一定的负担 的,因此设定Redis要满足一定条件才会进行重写=
auto-aof-rewrite-min-size 设置重写的基准值,最小文件64MB。达到这个值开始重写。
auto-aof-rewrite-percentage 设置重写的基准值,文件达到100%时开始重写(文件是原来重写后文件的2倍时触发)
总结:
官方推荐两个都启用,如果对数据不敏感,可以选单独用RDB,不建议单独用 AOF,因为可能会出现Bug
如果只是做纯内存缓存,可以都不用
8.Redis 删除策略
1.过期数据
Redis是一种内存级数据库,所有数据均存放在内存中,内存中的数据可以通过TTL指令获取其状态
-
XX :具有时效性的数
-
-1 :永久有效的数据
-
-2 :已经过期的数据或被删除的数据或未定义的数据
问:过期的数据真的删除了吗? 答:不是的
2.数据删除策略
数据删除策略的目标
在内存占用与CPU占用之间寻找一种平衡,顾此失彼都会造成整体redis性能的下降,甚至引发服务器宕 机或内存泄露
-
定时删除
-
惰性删除
-
定期删除
2.1定时删除
-
创建一个定时器,当key设置有过期时间,且过期时间到达时,由定时器任务立即执行对键的删除操作
-
优点:节约内存,到时就删除,快速释放掉不必要的内存占用
-
缺点:CPU压力很大,无论CPU此时负载量多高,均占用CPU,会影响redis服务器响应时间和指令吞吐量
-
总结:用处理器性能换取存储空间(拿时间换空间)
2.2 惰性删除
-
数据到达过期时间,不做处理。等下次访问该数据时
-
如果未过期,返回数据
-
发现已过期,删除,返回不存在
-
-
优点:节约CPU性能,发现必须删除的时候才删除
-
缺点:内存压力很大,出现长期占用内存的数据
-
总结:用存储空间换取处理器性能(拿空间换时间)
2.3 定期删除
两种方案都走极端,有没有折中方案?
-
Redis启动服务器初始化时,读取配置server.hz的值,默认为10
-
每秒钟执行server.hz次serverCron() 中的方法---databasesCron() ---activeExpireCycle()
-
**activeExpireCycle()**对每个expires[*]逐一进行检测,每次执行250ms/server.hz*
-
对某个expires[*]检测时,随机挑选W个key检测
-
如果key超时,删除key
-
如果一轮中删除的key的数量>W 25%,循环该过程
-
如果一轮中删除的key的数量≤W 25%,检查下一个expires[*],0-15循环
-
W取值=ACTIVE_EXPIRE_CYCLE_LOOKUPS_PER_LOOP属性值 *
-
-
参数current_db用于记录activeExpireCycle() 进入哪个expires[] 执行
-
如果**activeExpireCycle()**执行时间到期,下次从current_db继续向下执行
定期删除:周期性轮询redis库中的时效性数据,采用随机抽取的策略,利用过期数据占比的方式控制删 除频度
-
优点1:CPU性能占用设置有峰值,检测频度可自定义设置
-
优点2:内存压力不是很大,长期占用内存的冷数据会被持续清理
-
总结:周期性抽查存储空间 (随机抽查,重点抽查)
2.4 删除策略比对
-
定时删除 节约内存,无占用 不分时段占用CPU资源,频度高 拿时间换空间
-
惰性删除 内存占用严重 延时执行,CPU利用率高 拿空间换时间
-
定期删除 内存定期随机清理 每秒花费固定的CPU资源维护内存 随机抽查,重点抽查
3.逐出算法
当新数据进入redis时,如果内存不足怎么办?
-
Redis使用内存存储数据,在执行每一个命令前,会调用**freeMemoryIfNeeded()**检测内存是否充 足。如果内存不满足新加入数据的最低存储要求,redis要临时删除一些数据为当前指令清理存储 空间。清理数据的策略称为逐出算法。
-
注意:逐出数据的过程不是100%能够清理出足够的可使用的内存空间,如果不成功则反复执行。 当对所有数据尝试完毕后,如果不能达到内存清理的要求,将出现错误信息。 抛出异常:(error) OOM command not allowed when used memory >'maxmemory'
影响数据逐出的相关配置
-
maxmemory最大可使用内存 占用物理内存的比例,默认值为0,表示不限制,生产环境中根据需求设定,通常设置在50%以上。
-
maxmemory-samples每次选取待删除数据的个数 选取数据时并不会全库扫描,导致严重的性能消耗,降低读写性能。因此采用随机获取数据的方式 作为待检测删除数据
-
maxmemory-policy删除策略
检测易失数据(可能会过期的数据集server.db[i].expires )
① volatile-lru:挑选最近最少使用的数据淘汰 ② volatile-lfu:挑选最近使用次数最少的数据淘汰 ③ volatile-ttl:挑选将要过期的数据淘汰 ④ volatile-random:任意选择数据淘汰
检测全库数据(所有数据集server.db[i].dict )
⑤ allkeys-lru:挑选最近最少使用的数据淘汰 ⑥ allkeys-lfu:挑选最近使用次数最少的数据淘汰 ⑦ allkeys-random:任意选择数据淘汰
放弃数据驱逐
⑧ no-enviction(驱逐):禁止驱逐数据(redis4.0中默认策略),会引发错误OOM(Out Of Memory)达到最大内存后的,对被挑选出来的数据进行删除的策略
9.企业级解决方案
缓存预热
"宕机"服务器启动后迅速宕机
问题排查
-
请求数量较高
-
主从之间数据吞吐量较大,数据同步操作频度较高,因为刚刚启动时,缓存中没有任何数据
解决方案 准备工作:
-
日常例行统计数据访问记录,统计访问频度较高的热点数据
-
将统计结果中的数据分类,根据级别,redis优先加载级别较高的热点数据
实施:
-
使用脚本程序固定触发数据预热过程
-
如果条件允许,使用了CDN(内容分发网络),效果会更好
CDN的全称是Content Delivery Network,即内容分发网络。其基本思路是尽可能避开互联网上有可能影响数据传输速度和稳定性的瓶颈和环节,使内容传输得更快、更稳定。通过在网络各处放置节点服务器所构成的在现有的互联网基础之上的一层智能虚拟网络,CDN系统能够实时地根据网络流量和各节点的连接、负载状况以及到用户的距离和响应时间等综合信息将用户的请求重新导向离用户最近的服务节点上。其目的是使用户可就近取得所需内容,解决 Internet网络拥挤的状况,提高用户访问网站的响应速度
总结 缓存预热就是系统启动前,提前将相关的缓存数据直接加载到缓存系统。避免在用户请求的时候,先查 询数据库,然后再将数据缓存的问题!用户直接查询事先被预热的缓存数据
缓存雪崩
缓存雪崩是指在同一时段大量的缓存key同时失效或者Redis服务宕机,导致大量请求到达数据库,带来巨大压力。 解决方案:
-
给不同的Key的TTL添加随机值
-
利用Redis集群提高服务的可用性
-
给缓存业务添加降级限流策略
-
给业务添加多级缓存
缓存击穿
缓存击穿问题也叫热点Key问题,就是一个被高并发访问并且缓存重建业务较复杂的key突然失效了,无 数的请求访问会在瞬间给数据库带来巨大的冲击。
常见的解决方案有两种:
-
互斥锁
-
逻辑过期
逻辑分析:假设线程1在查询缓存之后,本来应该去查询数据库,然后把这个数据重新加载到缓存的, 此时只要线程1走完这个逻辑,其他线程就都能从缓存中加载这些数据了,但是假设在线程1没有走完的 时候,后续的线程2,线程3,线程4同时过来访问当前这个方法, 那么这些线程都不能从缓存中查询到 数据,那么他们就会同一时刻来访问查询缓存,都没查到,接着同一时间去访问数据库,同时的去执行 数据库代码,对数据库访问压力过大
解决方案一、使用锁来解决:
因为锁能实现互斥性。假设线程过来,只能一个人一个人的来访问数据库,从而避免对于数据库访问压力过大,但这也会影响查询的性能,因为此时会让查询的性能从并行变成了串行,我们可以采用tryLock方法 + double check来解决这样的问题。
假设现在线程1过来访问,他查询缓存没有命中,但是此时他获得到了锁的资源,那么线程1就会一个人去执行逻辑,假设现在线程2过来,线程2在执行过程中,并没有获得到锁,那么线程2就可以进行到休眠,直到线程1把锁释放后,线程2获得到锁,然后再来执行逻辑,此时就能够从缓存中拿到数据了。
解决方案二、逻辑过期方案
方案分析:我们之所以会出现这个缓存击穿问题,主要原因是在于我们对key设置了过期时间,假设我们不设置过期时间,其实就不会有缓存击穿的问题,但是不设置过期时间,这样数据不就一直占用我们内存了吗,我们可以采用逻辑过期方案。
我们把过期时间设置在 redis的value中,注意:这个过期时间并不会直接作用于redis,而是我们后续通过逻辑去处理。假设线程1去查询缓存,然后从value中判断出来当前的数据已经过期了,此时线程1去获得互斥锁,那么其他线程会进行阻塞,获得了锁的线程他会开启一个 线程去进行以前的重构数据的逻辑,直到新开的线程完成这个逻辑后,才释放锁, 而线程1直接进行返回,假设现在线程3过来访问,由于线程线程2持有着锁,所以线程3无法获得锁,线程3也直接返回数据,只有等到新开的线程2把重建数据构建完后,其他线程才能走返回正确的数据。
这种方案巧妙在于,异步的构建缓存,缺点在于在构建完缓存之前,返回的都是脏数据。
互斥锁方案 :由于保证了互斥性,所以数据一致,且实现简单,因为仅仅只需要加一把锁而已,也没其 他的事情需要操心,所以没有额外的内存消耗,缺点在于有锁就有死锁问题的发生,且只能串行执行性 能肯定受到影响 逻辑过期方案: 线程读取过程中不需要等待,性能好,有一个额外的线程持有锁去进行重构数据,但是 在重构数据完成前,其他的线程只能返回之前的数据,且实现起来麻烦
缓存穿透
缓存穿透 :缓存穿透是指客户端请求的数据在缓存中和数据库中都不存在,这样缓存永远不会生效,这 些请求都会打到数据库。
常见的解决方案有两种: 缓存空对象 优点:实现简单,维护方便 缺点:额外的内存消耗 可能造成短期的不一致
布隆过滤
优点:内存占用较少,没有多余key 缺点:实现复杂 存在误判可能
缓存空对象思路分析:当我们客户端访问不存在的数据时,先请求redis,但是此时redis中没有数据, 此时会访问到数据库,但是数据库中也没有数据,这个数据穿透了缓存,直击数据库,我们都知道数据 库能够承载的并发不如redis这么高,如果大量的请求同时过来访问这种不存在的数据,这些请求就都会 访问到数据库,简单的解决方案就是哪怕这个数据在数据库中也不存在,我们也把这个数据存入到redis 中去,这样,下次用户过来访问这个不存在的数据,那么在redis中也能找到这个数据就不会进入到缓存 了
布隆过滤:布隆过滤器其实采用的是哈希思想来解决这个问题,通过一个庞大的二进制数组,走哈希思 想去判断当前这个要查询的这个数据是否存在,如果布隆过滤器判断存在,则放行,这个请求会去访问 redis,哪怕此时redis中的数据过期了,但是数据库中一定存在这个数据,在数据库中查询出来这个数 据后,再将其放入到redis中,假设布隆过滤器判断这个数据不存在,则直接返回,这种方式优点在于节约内存空间,存在误判,误判原因在于:布隆过滤器走的是哈希思想,只要哈希思想,就可能存在哈希冲突