Redis缓存策略
2024-02-21 18:11:06 0 举报
AI智能生成
Redis缓存策略
作者其他创作
大纲/内容
缓存架构(仅针对缓存关系数据库)
加速读写
降低后端负载
收益
缓存层和存储层的数据存在着一定时间窗口的不一致性,时间窗口跟更新策略有关
数据不一致性
加入缓存后,需要同时处理缓存层和存储层的逻辑,增大了开发者维护代码的成本。
代码维护
以Redis Cluster为例,加入后无形中增加了运维成本
运维成本
成本
成本/收益
以MySQL为例子,一些复杂的操作或者计算(例如大量联表操作、一些分组计算),如果不加缓存,不但无法满足高并发量,同时也会给MySQL带来巨大的负担。
开销大的复杂计算
即使查询单条后端数据足够快(例如select*from tablewhere id=),那么依然可以使用缓存,以Redis为例子,每秒可以完成数万次读写,并且提供的批量操作可以优化整个IO链的响应时间。
加速请求响应
使用场景
先查询缓存,命中则返回,未命中则查询数据库,返回数据并写缓存
读数据
应用方对于数据的一致性要求高,需要在真实数据更新后,立即更新缓存数据。例如可以利用消息系统或者其他方式通知缓存更新。
原理
两个并发写操作,会产生脏数据(数据库和缓存数据不一致)
存在问题
先更新缓存,再更新数据库
两个并发写操作,会产生脏数据
先更新数据库,再更新缓存
一个是读操作,但是没有命中缓存,然后就到数据库中取数据,此时来了一个写操作,写完数据库后,让缓存失效,然后,之前的那个读操作再把老的数据放进去,所以,会造成脏数据。
产生过程
低概率不解决:因为这个条件需要发生在读缓存时缓存失效,而且并发着有一个写操作。而实际上数据库的写操作会比读操作慢得多,而且还要锁表,而读操作必需在写操作前进入数据库操作,而又要晚于写操作更新缓存,所有的这些条件都具备的概率基本并不大。
解决方案
一个读操作和一个写操作的并发,会产生脏数据
缓存设置失效时间
把更新数据库和删除缓存放在同一个事务里,先更新数据库,成功后再删除缓存,如果删除失败,则抛出异常,数据库事务也回滚
数据库和缓存操作不在同一个事务中,则更新数据库成功后,如果删除缓存失败(如网络问题,概率很小),会产生脏数据。
先更新数据库,成功后再删除缓存(facebook的选择)
方式
更新数据
Cache-Aside(Lazy Loading)
步骤同Cache-Aside
写数据库后,同时写缓存
写数据
Write-Through
缓存模式
当达到最大的缓存内存或设置的maxmemory时,缓存引擎将会移除键值。例如Redis使用maxmemory-policy这个配置作为内存最大值后对于数据的剔除策略
剔除算法有:LRU/LFU/FIFO/最小TTL/随机剔除等等。LRU(Lease Recently User)是一般情况下使用的算法。
缓存剔除
通过给缓存数据设置过期时间,让其在过期时间后自动删除,例如Redis提供的expire命令。如果业务可以容忍一段时间内,缓存数据和存储数据不一致,那么可以为其设置过期时间。在数据过期后,再从真实数据源获取数据,重新放到缓存并设置过期时间。也就是说,没过期时,即使数据库有更新也不会更新缓存,就有不一致窗口。例如一个视频的描述信息,可以容忍几分钟内数据不一致,但是涉及交易方面的业务,后果可想而知
缓存有效期
低一致性业务建议配置最大内存和淘汰策略的方式使用
高一致性业务可以结合使用超时移除和主动更新,这样即使主动更新出了问题,也能保证数据过期时间后删除脏数据
缓存策略建议
保存查询结果集。序列化结果集为ByteArray,然后以Sql语句为键,ByteArray为值保存在redis,保存Byte数组比直接保存String占用更少空间
用自定义格式(如JSON)保存。需要选择要保存的字段,然后组装JSON,再保存redis
用redis内置的数据类型保存。转成redis内置类型,然后保存,优点在于可以更加细粒度操作缓存
用实体类保存。序列化实体类再保存redis
缓存数据类型技巧
查询一个根本不存在的数据,缓存层和存储层都不会命中,通常出于容错的考虑,如果从存储层查不到数据则不写入缓存层。
现象
通常可以在程序中分别统计总调用数、缓存层命中数、存储层命中数,如果发现大量存储层空命中,可能就是出现了缓存穿透问题
发现方式
自身业务代码或者数据出现问题
恶意攻击、爬虫等造成大量空命中
原因
定期删除
大量空值,浪费空间
随机选取键值,依然会大量不命中
缓存层增加空值
布隆过滤器是一种数据结构,比较巧妙的概率型数据结构(probabilistic data structure),特点是高效地插入和查询,可以用来告诉你 “某样东西一定不存在或者可能存在”。
定义
布隆过滤器是一个bit数组,如果我们要映射一个值到布隆过滤器中,我们需要使用多个不同的哈希函数生成多个哈希值,并对每个生成的哈希值指向的 bit 位置 1。查找时,计算出查找项的多个哈希值,只要有一个的对应bit为0,则肯定不存在,否则可能存在(不能肯定是因为和其他项目具有相同的哈希值)。
由于存放的不是完整的数据,所以占用的内存很少,而且新增,查询速度很快
优点
随着数据的增加,误判率随之增加;无法做到删除数据;只能判断数据是否一定不存在,而无法判断数据是否一定存在。
缺点
新闻客户端的推送去重
解决缓存穿透,再缓存中找不到时,先在布隆过滤器中查询,如果可能存在才继续查数据库
应用范围
使用布隆过滤器
接口限流、熔断、降级
缓存穿透问题
缓存在同一时间内大量键过期,接着来的一大波请求瞬间都落在了数据库中。
加锁或者队列的方式来保证不会有大量的线程对数据库一次性进行读写,避免缓存失效时对数据库造成太大的压力,能够在一定的程度上缓解了数据库的压力,但是与此同时又降低了系统的吞吐量。
加锁或队列
加随机因子;不常更新的,缓存时间可以设置长些
设置不同的缓存时间
缓存雪崩问题
某个key是一个热点key,对这种key访问量可能非常大,同时,缓存的构建也需要一定时间(可能是一个复杂计算,例如复杂的sql、多次IO等等),于是在缓存失效的瞬间,有大量线程来构建缓存,造成后端负载加大
只让一个线程构建缓存,其他线程等待构建缓存的线程执行完,重新从缓存获取数据就可以了。
互斥锁
redis上没有设置过期时间,保证了不会出现热点key过期问题,也就是“物理”不过期。但是,把过期时间存在key对应的value里,如果发现要过期了,通过一个后台的异步线程进行缓存的构建,也就是“逻辑”过期
物理不过期
热点Key问题
为了满足业务需要可能会添加大量新的缓存节点,但是发现性能不但没有好转反而下降了。 用一句通俗的话解释就是,更多的节点不代表更高的性能,所谓“无底洞”就是说投入越多不一定产出越多。但是分布式又是不可避免的,因为访问量和数据量越来越大,一个节点根本扛不住,所以如何高效地在分布式缓存中批量操作是一个难点。
客户端一次批量操作会涉及多次网络操作,也就意味着批量操作会随着节点的增多,耗时会不断增大。
网络连接数变多,对节点的性能也有一定影响。
问题分析
由于n个key是比较均匀地分布在Redis Cluster的各个节点上,因此无法使用mget命令一次性获取,所以通常来讲要获取n个key的值,最简单的方法就是逐次执行n个get命令,这种操作时间复杂度较高,它的操作时间=n次网络时间+n次命令时间,网络次数是n。很显然这种方案不是最优的,但是实现起来比较简单
串行命令
Redis Cluster使用CRC16算法计算出散列值,再取对16383的余数就可以算出slot值,同时Smart客户端会保存slot和节点的对应关系,有了这两个数据就可以将属于同一个节点的key进行归档,得到每个节点的key子列表,之后对每个节点执行mget或者Pipeline操作,它的操作时间=node次网络时间+n次命令时间,网络次数是node的个数,整个过程如下图所示,很明显这种方案比第一种要好很多,但是如果节点数太多,还是有一定的性能问题
串行IO
此方案是将方案2中的最后一步改为多线程执行,网络次数虽然还是节点个数,但由于使用多线程网络时间变为O(1),这种方案会增加编程的复杂度
并行IO
Redis Cluster的hash_tag功能,它可以将多个key强制分配到一个节点上,它的操作时间=1次网络时间+n次命令时间。
hash_tag
无底洞优化
只应将热数据放到缓存中
对于很少改变的、静态的数据,可以设置更长的缓存过期时间,并且用wirte-through
对于经常改变的数据,应缩短过期时间,与数据库的数据改变时间对应
所有缓存信息都应设置过期时间
缓存过期时间应当分散以避免集中过期
缓存key应具备可读性
应避免不同业务出现同名缓存key
可对key进行适当的缩写以节省内存空间
选择合适的数据结构
避免使用耗时较长的操作命令,如:keys *,Redis默认配置中操作耗时超过10ms即视为慢查询
一个key对应的数据不应过大,对于string类型,一个key对应的value大小应控制在10K以内,1K左右更优,hash类型,不应超过5000行
缓存应有降级处理方案,缓存出了问题要能回源到数据库进行处理
缓存预热。对于上线后可能会有大量读请求的应用,在上线之前可预先将数据写入缓存中
设计原则
Redis缓存设计
0 条评论
回复 删除
下一页