一、Redis 缓存问题概述
1.1 Redis 缓存应用问题
在高并发的业务场景下,数据库大多数情况都是用户并发访问出现瓶颈的环节。所以,就需要使用 Redis 做一个缓冲操作,让请求先访问到 Redis,而不是直接访问MySQL等数据库。这样可以大大缓解数据库的压力。在使用 Redis 作为缓存数据库的过程中,有时也会遇到一些棘手问题, 比如以下一些常见的问题:
- 缓存穿透
- 缓存击穿
- 缓存雪崩
- 缓存和数据库一致性
- 数据并发竞争
- 缓存污染(或者满了)
二、缓存穿透
2.1 缓存穿透简介
缓存穿透 是指当用户查询某个数据时,在缓存(cache)没有命中(缓存中不存在该数据),此时查询请求就会转向到持久层的 数据库 去进行查询,结果发现 数据库 中也不存在该数据,数据库 只能返回一个空对象,代表此次查询失败(数据不存在)。如果这种类型请求非常多,或者用户利用这种请求进行恶意攻击(频繁的发起该数据请求),就会给 数据库 造成很大压力,甚至于崩溃,这种现象就叫缓存穿透 —— 查询请求穿透缓存层,到数据库层进行查询操作。
2.2 缓存穿透解决方案
为了避免缓存穿透问题,下面介绍一些 缓存穿透 的解决方案:
-
缓存空对象 当 数据库 返回空对象时,缓存系统 将该对象缓存起来,同时为其设置一个较短的过期时间。当用户再次发起相同请求时,就会从缓存中拿到一个空对象,用户的请求被阻断在了缓存层,从而保护了后端数据库,但是这种做法也存在一些问题,虽然请求进不了 数据库,但是这种策略会占用 缓存 的缓存空间,所以需要合理设置过期时间。
-
布隆过滤器
Tips:布隆过滤器判定不存在的数据,那么该数据一定不存在,利用它的这一特点可以防止缓存穿透。
布隆过滤器是一种数据结构,可以用来检测一个元素是否在一个集合中。
首先将用户可能会访问的热点数据存储在布隆过滤器中(也称缓存预热),当有一个用户请求到来时会先经过布隆过滤器,如果请求的数据在布隆过滤器中不存在,那么该请求将直接被拒绝,否则将继续执行查询。相较于第一种方法,用布隆过滤器方法更为高效、实用。其流程示意图如下:
Tips: 缓存预热:是指系统启动时,提前将相关的数据加载到 缓存系统中。这样避免了用户请求的时再去加载数据。
Redis实现布隆过滤器 2.1 Redis中配置布隆过滤器 1、点击https://redis.io/modules 找到RedisBloom 2、点击进去下载RedisBloom-master.zip文件,上传到linux 3、解压缩刚才的RedisBloom文件
|
|
编译安装
|
|
make完生成redisbloom.so,拷贝到redis的安装目录
|
|
在redis.conf配置文件中加入如RedisBloom的redisbloom.so文件的地址,如果是集群则每个配置文件中都需要加入redisbloom.so文件的地址
|
|
保存以后重启redis服务
|
|
上面我们有提到需要重启Redis,在本地和测试环境还可以,但是正式环境能不重启就不需要重启,那这么做可以不重启Redis,使用module load命令执行。
|
|
看到以上数据则说明redisbloom加载成功了,模块名name为"bf",模块版本号ver为999999。
Redis中布隆过滤器指令使用:
Tips: 使用布隆过滤器完整指令请到官网查看
-
限制请求 对于异常频繁的访问行为,可以采取限流、封禁IP等手段进行限制。例如,可以对每个用户的访问频率、请求的速度等进行限制,超过限制则暂时封禁其请求。
-
接口鉴权 在接口层做好身份验证和参数校验,不允许非法请求或者格式不正确的请求访问数据库。
-
数据库建立合理索引 对于一些必须要访问数据库的场景,确保数据库有好的查询性能,可以通过建立合理的索引来提高查询效率。
-
二级缓存 使用本地缓存作为一级缓存,Redis作为二级缓存。当本地缓存不命中时再查询Redis,如果Redis也不命中,最后才去查询数据库。这样可以减少直接对Redis的查询请求,降低Redis的压力。
-
前端控制 在前端应用中加强校验,比如表单校验、输入内容的合法性检查等,避免发送无效请求到后端。
三、缓存击穿
3.1 缓存击穿简介
缓存击穿 指的是缓存中没有 但 数据库中有的 数据(一般是热点数据),同时有大量并发请求这个数据,这些请求会直接穿透(缓存中没查找到)缓存,全部落到数据库上进行查找,造成数据库短时间内的高压力。这与缓存穿透不同,缓存穿透是查询不存在的数据,而缓存击穿则是查询存在但是缓存刚好失效的数据。
缓存击穿 经常发生在缓存失效的瞬间,有大量并发请求这个实现数据点
3.2 缓存击穿的解决方法
-
使用互斥锁 对于同一个数据点,在缓存失效时,通过加锁或同步机制,保证不管有多少并发请求,只允许一个请求去数据库查询数据,并更新缓存,其它请求等待缓存被更新后直接从缓存中获取数据。常见的做法是使用分布式锁。
-
设置热点数据永不过期 对于一些访问频率非常高的热点数据,可以设置缓存永不过期,或者缓存失效后由后台维护线程负责更新,而不是由用户请求触发更新。
-
使用双缓存机制(Cache Aside pattern) 当缓存失效时,并不立即删除缓存,而是使用另一个缓存进行更新操作。在新缓存更新完成之前,所有的读请求仍然访问的是旧的缓存。更新完成后再进行切换。
-
提前更新缓存 对于即将到期的数据,可以通过定时任务来检测并更新它。当检测到缓存数据即将到期时,可以提前异步地更新缓存。
-
给缓存设置合理的过期时间 对于一些热点数据,根据业务场景设置合理的过期时间,避免大量并发请求在同一时刻击穿缓存。
-
分布式缓存+本地缓存 可以在本地实现一层缓存,以减少对分布式缓存服务的访问频率,即使分布式缓存服务的数据过期,本地缓存仍然可以提供服务。
-
读写分离和负载均衡 数据库使用读写分离架构和负载均衡策略,将读操作分散到多个从库,减少对主库的直接压力。
四、缓存雪崩
4.1 缓存雪崩简介
缓存雪崩 是指在缓存系统中,由于大量缓存数据在同一时间过期,或者缓存服务宕机,导致所有的请求都直接落到数据库上,造成数据库瞬间承受巨大的访问压力,从而变得不稳定甚至崩溃的现象。这类似于雪崩一样,一旦发生就会导致连锁反应,导致整个系统的性能急剧下降。
4.2 缓存雪崩的解决方法
-
缓存数据的过期时间随机化 设置缓存数据的过期时间时,不要让大量的缓存数据在同一时间点过期。可以对过期时间加上一个随机值,使得缓存数据的过期时间分散开来,防止在同一时刻大面积缓存失效。
-
使用持久化 如果缓存服务支持持久化,比如Redis的RDB和AOF,要确保开启并合理配置这些功能。这样,即使缓存服务重启,也能从持久化的数据中恢复,减少缓存雪崩的风险。
-
设置热点数据永不过期 对于一些热点数据,可以设置为永不过期,或者采用手动更新缓存的策略,避免这些热点数据集体过期。
-
使用多级缓存策略 可以使用本地缓存(如Ehcache)和 分布式缓存(如Redis)结合的多级缓存策略,即使分布式缓存不可用,本地缓存仍然可以提供服务,减少对数据库的直接压力。
-
提升缓存服务的高可用性 使用主从复制、哨兵机制、集群等高可用方案来确保缓存服务的稳定性。即便单个节点出现故障,也能快速切换到正常的节点,保障缓存服务不中断。
-
限流和熔断机制 在系统中实施限流和熔断机制,当流量或错误超过一定阈值时,暂时阻止部分请求,保护数据库和系统不被过载。
-
异步队列 当缓存失效后,可以将数据库的读取操作放入异步队列中,用异步处理的方式来缓解瞬时流量对数据库的冲击。
五、数据不一致
5.1 数据不一致简介
使用 Redis 做一个缓冲操作,让请求先访问到 Redis,而不是直接访问MySQL等数据库: 读取缓存步骤一般没有什么问题,但是一旦涉及到数据更新:数据库和缓存更新,就容易出现缓存(Redis)和数据库(MySQL)间的数据一致性问题。
不管是先写MySQL数据库,再删除Redis缓存;还是先删除缓存,再写库,都有可能出现数据不一致的情况。举一个例子:
1.如果删除了缓存Redis,还没有来得及写库MySQL,另一个线程就来读取,发现缓存为空,则去数据库中读取数据(旧数据) 并 写入缓存,之后当前线程才将修改数据写入DB,此时缓存中为脏数据(旧数据)。
2.如果先写了库,在删除缓存前,写库的线程宕机了,没有删除掉缓存,则也会出现数据不一致情况。
因为写和读是并发的,没法保证顺序,就会出现缓存和数据库的数据不一致的问题。
数据不一致问题可能导致用户获取到过时的数据,影响用户体验,并可能引发更严重的数据一致性问题。
5.2 数据不一致的解决方法
-
缓存更新策略 更新缓存的的Design Pattern有四种:Cache aside, Read through, Write through, Write behind caching;
-
Cache aside:
-
缓存延迟双删:更新数据库数据后,先删除缓存,然后延迟一小段时间再次删除缓存,以确保请求在这段时间内若读取了旧数据,也会再次删除缓存,从而读到最新数据。
-
Write/Read Through Cache:利用缓存提供的写通(Writethrough)或读取通(Read through)策略,让缓存管理器负责数据的读写,确保数据的一致性。
-
Write Behind Caching:更新操作首先在缓存中执行,然后异步批量更新到数据库,这种策略要考虑数据丢失的风险和数据一致性的问题。
-
-
数据库触发器
使用数据库触发器在数据发生变化时自动更新缓存,确保数据一致性。 -
事务消息 通过使用支持事务的消息队列,将缓存操作和数据库操作放到同一个事务中,确保两者要么都成功,要么都失败。
-
最终一致性 接受在某个时间窗口内缓存与数据库中的数据不一致,但是通过后台异步进程定期校对并同步数据,保证最终一致性。
-
使用分布式缓存解决方案 选择支持一致性哈希、数据同步等特性的分布式缓存解决方案,如Redis Cluster,保证数据在多个节点之间的一致性。
-
版本号/时间戳校验 给数据库记录添加版本号或时间戳,缓存数据时一同缓存这个版本信息,每次读取缓存数据时都检查版本或时间戳是否相符,若不符则重新从数据库加载。
-
强制缓存过期 设置较短的缓存过期时间,确保数据定期从数据库中刷新。
六、数据并发竞争
6.1 数据并发竞争简介
数据并发竞争访问问题 通常指的是多个客户端或线程同时对同一数据进行读写操作时,由于没有妥善的并发控制措施导致数据出现不一致或者丢失的情况。这个问题在分布式系统和多用户系统中尤为常见,尤其是在使用像Redis这样的缓存系统时也会遇到。
出现问题的场景
- 计数器更新:比如用Redis计数器统计网站点击量,如果多个请求同时更新计数器,可能会因为读写操作不是原子性导致计数器丢失更新。
- 库存扣减:在电商场景中,多个用户同时下单扣减库存,可能会导致超卖。
- 分布式锁:多个进程需要对同一资源进行操作时,需要使用锁来保证同时只有一个操作可以执行。
- Session共享:在分布式部署的Web应用中,多个服务器上的并发请求需要共享Session信息,可能导致Session数据不一致。
6.2 数据并发竞争的解决方法
-
使用事务 Redis事务可以通过MULTI和EXEC命令来确保一系列命令的原子性执行。
-
使用Lua脚本 Redis可以执行Lua脚本,Lua脚本在执行过程中会被当作一个整体执行,这保证了操作的原子性。
-
使用分布式锁 可以实现一个基于Redis的分布式锁来控制资源的并发访问。比如使用SETNX命令实现锁的获取和释放。
-
乐观锁/Optimistic Locking 使用WATCH命令监视一个或多个键,如果在执行事务前这些键没有被其他命令改变,事务才会被执行。
-
悲观锁/Pessimistic Locking 对于关键业务,可以选择先对数据加锁,在业务处理完成后再解锁,避免其他客户端的访问。
-
限流措施 通过限流算法如令牌桶、漏桶等,控制对某一资源的并发访问数,减少并发冲突。
-
消息队列 使用消息队列将并发请求串行化处理,确保对共享资源的访问是有序的。
七、缓存污染(或满了)
7.1 缓存污染简介
缓存污染问题 说的是缓存中一些只会被访问一次或者几次的的数据,被访问完后,再也不会被访问到,但这部分数据依然留存在缓存中,消耗缓存空间。缓存污染会随着数据的持续增加而逐渐显露,随着服务的不断运行,缓存中会存在大量的永远不会再次被访问的数据。缓存空间是有限的,如果缓存空间满了,再往缓存里写数据时就会有额外开销,影响 Redis性能。这部分额外开销主要是指写的时候判断淘汰策略,根据淘汰策略去选择要淘汰的数据,然后进行删除操作。
最大缓存设置多大 系统的设计选择是一个权衡的过程:大容量缓存是能带来性能加速的收益,但是成本也会更高,而小容量缓存不一定就起不到加速访问的效果。一般来说,建议把缓存容量设置为总数据量的 15% 到 30%,兼顾访问性能和内存空间开销。
对于 Redis 来说,一旦确定了缓存最大容量,比如 4GB,你就可以使用下面这个命令来设定缓存的大小了:
|
|
7.2 缓存淘汰策略
存被写满是不可避免的, 所以需要数据淘汰策略。
Redis共支持八种淘汰策略,分别是noeviction、volatile-random、volatile-ttl、volatile-lru、volatile-lfu、allkeys-lru、allkeys-random 和 allkeys-lfu 策略。
八种淘汰策略主要看三类看:
- 不淘汰
- noeviction (v4.0后默认的)
- 对设置了过期时间的数据中进行淘汰
- 随机:volatile-random
- ttl:volatile-ttl
- lru:volatile-lru
- lfu:volatile-lfu
- 全部数据进行淘汰
- 随机:allkeys-random
- lru:allkeys-lru
- lfu:allkeys-lfu
- noeviction(不驱逐) 该策略是Redis的默认策略。在这种策略下,一旦缓存被写满了,再有写请求来时,Redis 不再提供服务,而是直接返回错误。这种策略不会淘汰数据,所以无法解决缓存污染问题。一般生产环境不建议使用。
其他七种规则都会根据自己相应的规则来选择数据进行删除操作。
-
volatile-random(随机删除) 这个算法比较简单,在设置了过期时间的键值对中,进行随机删除。因为是随机删除,无法把不再访问的数据筛选出来,所以可能依然会存在缓存污染现象,无法解决缓存污染问题。
-
volatile-ttl(按过期时间的排序删除) 这种算法判断淘汰数据时参考的指标比随机删除时多进行一步过期时间的排序。Redis在筛选需删除的数据时,越早过期的数据越优先被选择。
-
volatile-lru(最近最少使用) LRU算法:LRU 算法的全称是
Least Recently Used
,按照最近最少使用的原则来筛选数据。这种模式下会使用 LRU 算法筛选设置了过期时间的键值对。
Redis优化的LRU算法实现:Redis会记录每个数据的最近一次被访问的时间戳。在Redis在决定淘汰的数据时,第一次会随机选出 N 个数据,把它们作为一个候选集合。接下来,Redis 会比较这 N 个数据的 lru 字段,把 lru 字段值最小的数据从缓存中淘汰出去。通过随机读取待删除集合,可以让Redis不用维护一个巨大的链表,也不用操作链表,进而提升性能。Redis 选出的数据个数 N,通过 配置参数 maxmemory-samples
进行配置。个数N越大,则候选集合越大,选择到的最久未被使用的就更准确,N越小,选择到最久未被使用的数据的概率也会随之减小。
- volatile-lfu LFU 算法:LFU 缓存策略是在 LRU 策略基础上,为每个数据增加了一个计数器,来统计这个数据的访问次数。当使用 LFU 策略筛选淘汰数据时,首先会根据数据的访问次数进行筛选,把访问次数最低的数据淘汰出缓存,如果两个数据的访问次数相同,LFU 策略再比较这两个数据的访问时效性,把距离上一次访问时间更久的数据淘汰出缓存。
Redis的LFU算法实现: 当 LFU 策略筛选数据时,Redis 会在候选集合中,根据数据 lru 字段的后 8bit 选择访问次数最少的数据进行淘汰。当访问次数相同时,再根据 lru 字段的前 16bit 值大小,选择访问时间最久远的数据进行淘汰。
Redis 只使用了 8bit 记录数据的访问次数,而 8bit 记录的最大值是 255,这样在访问快速的情况下,如果每次被访问就将访问次数加一,很快某条数据就达到最大值255,可能很多数据都是255,那么退化成LRU算法了。所以Redis为了解决这个问题,实现了一个更优的计数规则,并可以通过配置项,来控制计数器增加的速度。
LFU算法配置参数:
- lfu-log-factor 用计数器当前的值乘以配置项 lfu_log_factor 再加 1,再取其倒数,得到一个 p 值;然后,把这个 p 值和一个取值范围在(0,1)间的随机数 r 值比大小,只有 p 值大于 r 值时,计数器才加 1。
- lfu-decay-time 控制访问次数衰减。LFU 策略会计算当前时间和数据最近一次访问时间的差值,并把这个差值换算成以分钟为单位。然后,LFU 策略再把这个差值除以 lfu_decay_time 值,所得的结果就是数据 counter 要衰减的值。
lfu-log-factor 设置越大,递增概率越低,lfu-decay-time 设置越大,衰减速度会越慢。
在应用 LFU 策略时,一般可以将 lfu_log_factor 取值为 10。如果业务应用中有短时高频访问的数据的话,建议把 lfu_decay_time 值设置为 1。可以快速衰减访问次数。
volatile-lfu 策略是 Redis 4.0 后新增。
-
allkeys-lru 使用 LRU 算法在所有数据中进行筛选。具体LFU算法跟上述 volatile-lru 中介绍的一致,只是筛选的数据范围是全部缓存,这里就不在重复。
-
allkeys-random 从所有键值对中随机选择并删除数据。volatile-random 跟 allkeys-random算法一样,随机删除就无法解决缓存污染问题。
-
allkeys-lfu 使用 LFU 算法在所有数据中进行筛选。具体LFU算法跟上述 volatile-lfu 中介绍的一致,只是筛选的数据范围是全部缓存,这里就不在重复。
allkeys-lfu 策略是 Redis 4.0 后新增。