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= # 连接池最大连接数(使用负值表示没有限制) 3.具体代码了解 序列化策略 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
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
序列化策略
改变序列化策略
默认序列化方式存储到redis的数据人工不可读
不同策略序列化的过程有性能高低的
spring-data-redis提供如下几种序列化策略
GenericToStringSerializer: 可以将任何对象泛化为字符串并序列化 Jackson2JsonRedisSerializer: 跟JacksonJsonRedisSerializer实际上是一样的 JacksonJsonRedisSerializer: 序列化object对象为json字符串 JdkSerializationRedisSerializer: 序列化java对象 StringRedisSerializer: 简单的字符串序列化
Redis持久化
Redies是内存数据库,如果不将内存中的数据库状态保存到磁盘, 那么一旦服务器进程退出,服务器中的数据库状态也会小时。所以Redis提供了持久化功能!
持久化过程保存什么
- 将当前数据状态进行保存,快照形式,存储数据结果,存储格式简单,关注点在数据(PDB)
- 将数据的操作过程进行保存,日志形式,存储操作过程,关注点在数据的操作过程(AOF)
RDB方式
概念:在指定的时间间隔内将内存的数据集快照写入磁盘,也就是行话讲的Snapshot快照,他恢复时是将快照文件读到内存里
RDB手动
save指令
命令:save
作用:手动执行一次保存操作
save指令相关配置
dbfilename dump.rdb
说明:设置本地数据库文件名,默认值为dump.rdb
经验:通常设置成存储空间较大的目录中,目录名称data
dir
说明:设置存储.rdb文件的路径
经验:通常设置成存储空间较大的目录中,目录名称data
rdbcompression yes
说明:设置存储至本地数据库时是否压缩数据,默认为yes,采用LZF算法压缩
经验:通常默认为开启状态,如果设置为no,可以节省 CPU 运行时间,但会使存储的文件变大(巨 大)
rdbchecksum yes
说明:设置是否进行CRC64算法RDB文件格式校验, 该校验过程在写文件和读文件过程均进行
经验:通常默认为开启状态,如果设置为no,可以节约读写性过程约10%时间消耗,但是存储一定的数 据损坏风险
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文件格式的版本统一,有可能出现各版本服务之间数据格式无法兼容现象
AOF方式
AOF持久化:以独立日志的方式记录每次写命令,重启时再重新执行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,文件会越来越大,为了解决这个问题,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区别
总结
官方推荐两个都启用,如果对数据不敏感,可以选单独用RDB,不建议单独用 AOF,因为可能会出现Bug
如果只是做纯内存缓存,可以都不用
Redis 删除策略
1.过期数据
Redis是一种内存级数据库,所有数据均存放在内存中,内存中的数据可以通过TTL指令获取其状态
- XX :具有时效性的数据
-
-1 :永久有效的数据
-2 :已经过期的数据或被删除的数据或未定义的数据
2.数据删除策略
数据删除策略的目标
在内存占用与CPU占用之间寻找一种平衡,顾此失彼都会造成整体redis性能的下降,甚至引发服务器宕机或内存泄露
1. 定时删除 2. 惰性删除 3. 定期删除
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:内存压力不是很大,长期占用内存的冷数据会被持续清理
总结:周期性抽查存储空间 (随机抽查,重点抽查)
删除策略比对
1. 定时删除 节约内存,无占用 不分时段占用CPU资源,频度高 拿时间换空间
2. 惰性删除 内存占用严重 延时执行,CPU利用率高 拿空间换时间
3. 定期删除 内存定期随机清理 每秒花费固定的CPU资源维护内存 随机抽查,重点抽查
逐出算法
当新数据进入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)达到最大内存后的,对被挑选出来的数据进行删除的策略
企业级解决方案
3.缓存预热
缓存预热是一种优化技术,用于提高Web应用的性能和响应速度。其基本原理是在应用启动或更新后 ,预先加载一部分常用的数据到缓存中,这样当用户首次访问时,就可以直接从缓存中获取数据,而不需要先查询数据库,再把结果存储到缓存中,从而减少了用户的等待时间和服务器的压力。
问题排查:
-
请求数量较高:当系统刚启动时,由于缓存为空,所有请求都回落到后端数据库上,这可能导致数据库负载激增,影响响应时间。
-
主从之间数据吞吐量较大:在分布式系统中,如果主节点更新了数据,那么需要将这部分数据同步到从节点。如果没有缓存预热机制,在系统启动初期,大量的数据更新操作可能会导致主从之间的数据同步变得非常频繁,增加网络负载。
准备工作:
-
日常例行统计数据访问记录:通过历史访问数据的统计,可以找出那些数据是高频访问的,从而确定哪些数据需要在系统启动时预热。
-
将统计结果中的数据分类:根据数据的重要性和访问频率对其进行分类,对于重要性高且访问频率的数据,应该优先进行预热。
解决方案
-
使用脚本程序固定触发数据预热过程:可以通过编写脚本来实现数据预热,该脚本可以部署阶段或定期运行,将关键数据加载进缓存。
-
使用CDN(内容分发网络):对于静态资源或者分布广泛的用户群,使用CDN可以进一步减少延迟,提高用户体验。虽然CDN主要用于静态文件的分发,但它也可以作为缓存的一部分减轻服务器压力。
-
定期刷新缓存:设置一个定时任务来定期刷新缓存中的数据,确保缓存中的数据是最新的,同时也要注意不要过于频繁刷新,以避免增加不必要的开销。
总结
缓存预热就是系统启动前,提前将相关的缓存数据直接加载到缓存系统。避免在用户请求的时候,先查询数据库,然后再将数据缓存的问题!用户直接查询事先被预热的缓存数据。
4.缓存雪崩
缓存雪崩是指在同一时段大量的缓存key同时失效或者Redis服务宕机,导致大量请求到达数据库,带来巨大压力。
解决方案
-
给不同的key的TTL添加随机值:为了避免所有的缓存键同时失效,可以在设置键的有效期(TTL) 时加入一定的随机值,使得它们不会在同一时间过期。
-
利用Redis集群提高服务的可用性:通过搭建Redis集群,可以提高缓存服务的可用性,即使单个节点发生故障,集群中的其他节点仍能提供服务。
-
给缓存业务添加降级限流策略:在缓存服务不可用时,可以采用降级处理,返回一些默认的、不依赖实时性的数据,或者限制请求的速率,防止数据库被压垮。
-
给业务添加多级缓存:例如,可以先使用本地缓存(如GuavaCache),再使用远程缓存(如Redis),这样即使远程缓存出现问题,还有本地缓存作为缓冲。
5.缓存击穿
缓存击穿指的是某个高并发访问的热点键突然失效,导致原本由缓存处理的请求都打到了数据库上,给数据库造成瞬时的高负载。
解决方案:
- 互斥锁:当一个键失效后,多个并发请求同时达到时,可以使用互斥锁来确保只有一个请求去加载数据并写入缓存。其他请求则等待所释放后在尝试从缓存中获取数据。
- 逻辑过期:为了避免缓存键正好在某一时刻过期,可以设置一个“逻辑过期”时间,即在缓存键中存储一个过期时间戳,每次访问时检查这个时间戳来决定是否需要更新缓存。
逻辑分析:假设线程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把重建数据构建完后,其他线程才能走返回正确的数据。
这种方案巧妙在于,异步的构建缓存,缺点在于在构建完缓存之前,返回的都是脏数据。
互斥锁方案与逻辑过期方案:
互斥锁方案:由于保证了互斥性,所以数据一致,且实现简单,因为仅仅只需要加一把锁而已,也没其他的事情需要操心,所以没有额外的内存消耗,缺点在于有锁就有死锁问题的发生,且只能串行执行性能肯定受到影响。
逻辑过期方案: 线程读取过程中不需要等待,性能好,有一个额外的线程持有锁去进行重构数据,但是在重构数据完成前,其他的线程只能返回之前的数据,且实现起来麻烦
6.缓存穿透
缓存穿透是指客户端请求的数据既不在缓存中也不在数据库中存在,导致每次请求都需要查询数据库,从而给数据库带来不必要的压力。这种情况通常发生在恶意攻击或者系统设计缺陷的情况下,比如客户端发送不存在的ID来请求数据。
解决方案
- 缓存空对象 优点:
·实现相对简单,维护成本较低。
· 可以有效减少数据库的无效查询次数。
缺点:
·会增加额外的内存消耗,因为需要缓存空对象。
·如果不正确地管理这些空对象的有效期,可能会导致短暂的数据不一致性问题。 实现思路:
·当用户请求的数据在数据库中不存在时,可以将一个特殊标记(例如null、空对象或者特定的错误信息)存储到缓存中,并设置一个合理的过期时间。
·下次相同的请求到达时,可以直接从缓存中返回这个特殊标记,从而避免了再次查询数据库。- 使用布隆过滤器(Bloom Filter)
优点:
内存占用较少,因为只需要存储位数组和哈希函数信息。
·不需要在缓存中存储大量的空对象。
缺点:
实现相对复杂,需要理解并正确配置哈希函数。
存在一定的误判率,即有一定概率将不存在的数据误判为存在。实现思路:
- 在请求到达缓存层之前,先通过布隆过滤器来判断该请求所对应的键是否存在。
- 如果布隆过滤器表明该键不存在,则直接返回,不再查询缓存或数据库。
- ·如果布隆过滤器认为该键可能存在,则允许请求继续到缓存层。如果缓存中找不到,则查询数据库,并将结果存入缓存。
具体应用
缓存空对象示例
如果一个请求查询的ID在数据库中不存在,可以将该ID对应的空结果(例如{"id": "xxx",
"data": null})存入缓存,并设置一个合理的过期时间。这样,后续对该ID的请求可以直接从缓存中获取空结果,而不必再去查询数据库。布隆过滤器示例
在请求到达缓存层之前,先通过布隆过滤器来判断该请求的键是否存在。如果布隆过滤器判断该键不存在,则直接返回一个错误消息,告知客户端该数据不存在。如果布隆过滤器认为该键可能存在,则允许请求继续到缓存层。