Administrator
发布于 2022-12-30 / 35 阅读
0
0

Redis缓存设计

缓存的收益和成本

收益

1、加速读写,缓存一般都是基于全内存的
2、降低后端负载,帮助后端减少访问量和复杂计算

成本

1、数据不一致性,缓存层和存储层的数据存在着一定时间窗口的不一致性,时间窗口跟更新策略有关。
2、代码维护成本,加入缓存后,需要同时处理缓存层和存储层的逻辑,增大了开发者维护代码的成本。
3、运维成本

缓存的使用场景

1、开销大的复杂计算:以MySQL为例子,一些复杂的操作或者计算(例如大量联表操作、一些分组计算)
2、加速请求响应

缓存的更新策略

缓存中的数据通常都是有生命周期的,需要在指定时间后被删除或更新。

缓存中的数据会和数据源中的真实数据有一段时间窗口的不一致(数据源更新而缓存没更新期间),需要利用某些策略进行更新。

1、LRU/LFU/FIFO算法剔除

用于缓存使用量超过了预设的最大值时候,对现有的数据进行剔除。(即存太多数据的时候,选择一些数据删掉)

一致性:要清理哪些数据由具体算法决定,开发不能干预。一致性低
维护成本:只需要配置最大maxmemory和对应的策略即可,维护成本低

2、超时剔除

通过给缓存数据设置过期时间,让其在过期时间后自动删除,例如Redis提供的expire命令。

如果业务允许在过期时间内缓存数据和数据源的数据不一致,即业务数据不是那么敏感可以进行设置,过期时间就设置范围在0到能容忍的时间之间

一致性 :一段时间窗口内(取决于过期时间长短)存在一致性问题,即缓存数据和真实数据源的数据不一致。
维护成本 :维护成本不是很高,只需设置expire过期时间即可

3、主动更新

场景:应用方对于数据的一致性要求高,需要在真实数据更新后,立即更新缓存数据。 例如可以利用消息系统或者其他方式通知缓存更新。

一致性 :一致性最高,但如果主动更新发生了问题,那么这条数据很可能很长时间不会更新,
所以建议结合超时剔除一起使用效果会更好。

维护成本 :维护成本会比较高,开发者需要自己来完成更新,并保证更新操作的正确性。
策略 一致性 成本
LRU/LFU/FIFO算法剔除 最低 最低
超时剔除 居中 居中
主动更新 最高 最高
低一致性业务建议配置最大内存和淘汰策略的方式使用。

高一致性业务可以结合使用超时剔除和主动更新,这样即使主动
更新出了问题,也能保证数据过期时间后删除脏数据。

缓存粒度

缓存粒度指的是缓存数据的多少,在开发中你查询一条记录,记录不一定只有一两列属性。缓存属性的多少就是粒度的粗细。

粒度粗细的区别:

通用性:
粒度粗一点(缓存全部数据/较多数据),通用性较高,因为字段多,那么使用的场景就多
粒度细一点(缓存较少数据),通用性低,适合特定的场景

空间占用:
粒度粗的占用空间更多,还会造成浪费,毕竟有些字段数据还会用不上。
数据多传输占用的资源也多,更耗时。序列化和反序列化CPU开销也更大

粒度细的占用空间少

维护成本:
粒度粗的维护成本低

粒度细的在要增加字段时要修改代码,还要刷新缓存

缓存穿透

缓存穿透是指查询一个根本不存在的数据,缓存层和存储层都不会命中,通常出于容错的考虑,如果从存储层查不到数据则不写入缓存层。

在加入缓存的情况下查询流程为:
先查缓存,有直接返回,没有则查询数据库,数据库没有则返回空结果,不写入缓存

缓存穿透将导致不存在的数据每次请求都要到存储层去查询,可能会使后端存储负载加大,由于很多后端存储不具备高并发性,甚至可能造成后端存储宕掉。失去了缓存保护后端存储的意义。

如果发现大量存储层空命中,可能就是出现了缓存穿透问题。造成缓存穿透的基本原因有两个。

第一,自身业务代码或者数据出现问题,
第二,一些恶意攻击、爬虫等造成大量空命中。

解决穿透问题

1、缓存空对象

查询某个数据在存储层都为空时,将空对象保留到缓存层中,之后再访问这个数据将会从缓存中获取

缓存空对象存在的问题

1、内存空间消耗
如果请求的数据是一样的还好,但如果数据不一样,那么就要为每个数据都做键值映射,数据量大的时候,
会消耗大量的内存空间。
对于这个问题可以设置较短的过期时间进行剔除而优化

2、数据不一致
上次查结果返回为空,之后此数据被添加了,但如果缓存没有得到更新就会出现数据不一致的问题。
解决是可以利用消息系统或者其他方式清除掉缓存层中的空对象

2、布隆过滤器拦截

在访问缓存层和存储层之前,将存在的key用布隆过滤器提前保存起来,做第一层拦截,如果布隆过滤器认为数据不存在,那么就不会访问存储层,在一定程度保护了存储层。

这种方法适用于数据命中不高、数据相对固定、实时性低(通常是数据集较大)的应用场景,代码维护较为复杂,但是缓存空间占用少。

对比

解决缓存穿透 适用场景 维护成本
缓存空对象 数据命中不高;数据频繁变化实时性高 维护简单;内存空间消耗多;数据不一致问题
布隆过滤器 数据命中不高,数据相对固定实时性低 维护复杂;内存空间消耗较少

缓存击穿

key对应的数据存在,但在redis中过期,此时若有大量并发请求过来,这些请求发现缓存过期一般都会从后端DB加载数据并回设到缓存,这个时候大并发的请求可能会瞬间把后端DB压垮

也就是一个key过期,一直访问数据库

key在某些时间点被超高并发地访问,是一种非常“热点”的数据

解决缓存击穿

(1)预先设置热门数据:在redis高峰访问之前,把一些热门数据提前存入到redis里面,
加大这些热门数据key的时长,类似在什么双十一之前预设置
(2)实时调整:现场监控哪些数据热门,实时调整key的过期时长
(3)使用锁:先判断值是否为空再让他进来与否

总结如下:

设置热门的key,加大时长过期
实时监控调整

热点key重建优化问题

当当前key是一个热点key(例如一个热门的娱乐新闻),并发量非常大。且重建缓存不能在短时间完成(可能是一个复杂计算,例如复杂的SQL、多次IO、多个依赖等)时。

在缓存失效的瞬间,有大量线程来重建缓存,造成后端负载加大,甚至可能会让应用崩溃。

解决

1、互斥锁
只允许一个线程重建缓存,其他线程等待重建缓存的线程执行完,重新从缓存获取数据即可

2、永不过期
不设置缓存的过期时间,那样就不会存在热点key过期后产生的问题;但为了保持一定的一致性,会设置一个逻辑过期时间,到逻辑过期时间后进行更新,更新期间也会有数据一致性问题,(虽然我感觉其实一直有一致性问题,逻辑过期也没啥大作用)。对数据一致性要求不高的可以使用。

互斥锁(mutex key):这种方案思路比较简单,但是存在一定的
隐患,如果构建缓存过程出现问题或者时间较长,可能会存在死锁和线
程池阻塞的风险,但是这种方法能够较好地降低后端存储负载,并在一
致性上做得比较好。

永远不过期:这种方案由于没有设置真正的过期时间,实际上
已经不存在热点key产生的一系列危害,但是会存在数据不一致的情
况,同时代码复杂度会增大。
解决方案 优点 缺点
简单的分布式锁 简单;保证数据一致性 代码复杂度增加,存在死锁和线程池阻塞的风险
永不过期 基本不会有热点key过期问题 不保证一致性;逻辑过期期间代码维护成本和内存成本增加

缓存雪崩

如果缓存层由于某些原因不能提供服务,于是所有的请求都会达到存储层,存储层的调用量会暴增,造成存储层也会级联宕机的情况。

预防和解决缓存雪崩问题

1、保证缓存层服务高可用性;如前面说到的哨兵和集群
2、依赖隔离组件为后端限流并降级(这个不熟,没了解过降级,百科一下)
3、提前演练,然后做预案

image-1672385910270


评论