redis 5 - 一些特性
一、过期删除
Redis如何实现过期删除?
Redis支持对key设置过期时间。因此需要有机制对过期键执行删除操作。
那这是如何实现的呢?
首先,Redis会维护一个「过期字典」。当我们对一个key设置了过期时间,Redis就会将key和过期时间存入过期字典。
当我们查询一个key时,如果该key存在于过期字典中,那么就会将它的过期时间与当前时间做比较。如果它已经过期,则不返回值。
Redis有两种过期删除策略,分别是「惰性删除」和「定期删除」
「惰性删除」
不主动删除过期键,每次从数据库访问 key 时,都检测 key 是否过期,如果过期则删除该 key。
「定期删除」
隔一段时间「随机」从数据库中取出一定数量的 key 进行检查,并删除其中的过期key。
定期删除流程:
- 从过期字典中随机抽取 20 个 key;
- 检查这 20 个 key 是否过期,并删除已过期的 key;
- 如果本轮检查的已过期 key 的数量,超过 5 个(20/4),也就是「已过期 key 的数量」占比「随机抽取 key 的数量」大于 25%,则继续重复步骤 1;如果已过期的 key 比例小于 25%,则停止继续删除过期 key,然后等待下一轮再检查。
可以看到,定期删除是一个循环的流程。那 Redis 为了保证定期删除不会出现循环过度,导致线程卡死现象,为此增加了定期删除循环流程的时间上限,默认不会超过 25ms。
策略 | CPU | 内存 |
---|---|---|
惰性删除 | 访问时才会判断,对CPU友好 | 过期key不被访问到就一直不会被删除,对内存不友好 |
定期删除 | 取决于删除频率,频率高对CPU不友好 | 取决于删除频率,频率低对内存不友好 |
Redis采用两种策略相结合的方式。
Redis持久化时,对过期键如何处理?
对于AOF,分为写入阶段和重写阶段
写入阶段:过期键删除的时候,Redis会向AOF文件追加DEL命令删除该键。
重写阶段:扫描Redis键值对,已过期的键不会被保存。
对于RDB,分为文件生成阶段和加载阶段
文件生成阶段:持久化成RDB文件时,过期的键不会被保存。
加载阶段:
- 主服务器运行模式:过期键不会被载入到数据库里
- 从服务器运行模式:键都会被载入到数据库里。但由于主从服务器在进行数据同步时,从服务器的数据会被清空。所以一般来说,过期键对载入 RDB 文件的从服务器也不会造成影响。
Redis 主从模式中,对过期键会如何处理?
当 Redis 运行在主从模式下时,从库不会进行过期扫描,从库对过期的处理是被动的。
就是说从库中的 key 过期了,如果有客户端访问从库时,依然可以得到 key 对应的值,像未过期的键值对一样返回。
从库的过期键处理依靠主服务器控制,主库在 key 到期时,会在 AOF 文件里增加一条 del 指令,同步到所有的从库,从库通过执行这条 del 指令来删除过期的 key。
二、内存淘汰
在 Redis 的运行内存达到了某个阀值,就会触发内存淘汰机制。
这个阀值就是我们设置的最大运行内存,在配置文件中通过maxmemory
控制。
内存淘汰策略
- noeviction:当运行内存超过最大设置内存时,不淘汰数据,而是不再提供服务,直接返回错误。
- volatile-random:随机淘汰设置了过期时间的任意键值;
- volatile-ttl:优先淘汰更早过期的键值。
- volatile-lru:淘汰所有设置了过期时间的键值中,最久未使用的键值;
- volatile-lfu:淘汰所有设置了过期时间的键值中,最少使用的键值;
- allkeys-random:随机淘汰任意键值;
- allkeys-lru:淘汰整个键值中最久未使用的键值;
- allkeys-lfu:淘汰整个键值中最少使用的键值。
「LRU」:最久未使用
传统的LRU算法是基于链表的,最新操作的键会被移动到表头,链表尾部的元素就是最久未操作的键。
因此删除时选择链表尾部的元素删除即可。
这种算法有两个缺点:
- 需要使用链表来管理缓存数据,带来额外的空间开销
- 频繁移动链表,很耗时
Redis实现了一种近似的LRU算法,具体实现是
- 在Redis的对象结构体中添加一个额外的字段,用于记录此数据的最后一次访问时间。
- 进行内存淘汰时,随机取5(可以配置)个值,淘汰其中最久没有使用的那个
LRU有一个问题:缓存污染问题。
即一次读取了大量数据,但是这些数据都只会被读取一次。使用LRU算法会造成这些数据在缓存中存在很长时间,造成缓存污染。
因此引入了LFU算法。
「LFU」:最不常使用
LFU 算法是根据数据访问次数来淘汰数据的,它的核心思想是“如果数据过去被访问多次,那么将来被访问的频率也更高”。
所以, LFU 算法会记录每个数据的访问次数。当一个数据被再次访问时,就会增加该数据的访问次数。这样就解决了缓存污染问题。
该算法的具体实现是
- 在Redis的对象结构体中添加两个额外的字段,分别用于记录此数据的最后一次访问时间和访问频次(logc)
- 每次访问key时,先根据访问时间差距计算衰减次数(当前时间 - 最后一次访问时间)/ 衰减配置值;然后概率p执行
logc+=1
- p = $\frac{1}{baseval * (lfu-log-factor)}$
baseval = logc - INIT
lfu-log-factor
是配置值- 即logc越大,概率p越小
- 在进行内存淘汰时,先衰减一次,然后根据logc来排序,淘汰logc最小的那个
三、使用时常见问题
缓存更新策略
在缓存的使用中,有一个很重要的问题,就是需要保证缓存和数据库的一致性。
在更新的时候,先更新缓存还是数据库呢?
如果先更新数据库的话,就可能会出现这种数据不一致的情况。
先更新缓存也有相同的情况。
所以,采用一种方案,在更新数据时,不更新缓存,而是删除缓存中的数据。读请求没有访问到缓存中的数据时去数据库中读取,并写入缓存中。
这种方案叫做「Cache Aside」.
细分为两部分策略:
- 写策略:
- 先更新数据库
- 再删除缓存
- 读策略:
- 缓存命中时直接返回数据;
- 缓存未命中时先读取数据库
- 然后回写缓存
这里要注意的地方是「写策略」中,更新数据库和删除缓存操作不能倒过来。原因是读写并发的时候可能会造成数据不一致问题
要是先更新数据库再删除缓存呢?
可以看到,这种场景下也会出现数据库缓存不一致的情况。但是这种场景实际中出现的很少。
因为「缓存的写入」是非常快的,比较难出现数据库更新完、删除缓存后,回写缓存的操作才执行完成的情况。
Cache aside比较适合「读多写少」的场景。当写入比较多的时候,缓存会被频繁删除,影响缓存命中率。
缓存雪崩
一般缓存任务都是通过 1. 定时任务刷新、 2. 查不到之后刷新。
通过定时任务刷新的话,就会存在一个问题:大量数据在同一时刻过期,就会有大量请求访问到数据库,严重时数据库直接挂了,服务无法提供正常服务。
这就是「缓存雪崩」。
有两种解决方案:
- 将缓存失效时间随机打散:给缓存失效时间增加一个随机值,让它别在同一时刻过期;
- 设置缓存不过期:由后台服务来更新缓存数据。
缓存击穿
业务系统中,通常会有几个数据被非常频繁地访问。这类数据被称为「热点数据」。
如果某个热点数据过期了,那么同样会有大量请求直接访问到数据库。
这种场景被称为「缓存击穿」
解决方案有两种:
- 互斥锁方案。保证同一时间只有一个线程访问数据库并构建缓存。其他请求直接返回空值。
- 不给热点数据设置过期时间。后台线程异步更新缓存。
缓存穿透
前两种问题都是数据库中的数据在缓存中不存在了导致大量请求访问数据库。
但是还存在一种情况,就是这个数据在数据库中也不存在。导致每次对这种数据进行访问,都会请求到数据库。
这种场景被称为「缓存穿透」。
一般有两种可能出现这种场景:
- 误操作导致数据出现不一致。
- 恶意请求,故意访问不存在的数据。
应对方案有三种:
- 请求校验,对明显的非法请求直接拒绝。
- 数据库查询不到的数据,也在缓存中设置一个空值或者默认值。这种数据失效时间设置的短一些,避免正常有数据了缓存中也是空。
- 使用「布隆过滤器」快速判断数据是否存在。
布隆过滤器可以用于检索一个元素是否在一个集合中。
它的优点是空间效率和查询时间都比一般的算法要好的多,缺点是有一定的误识别率和删除困难。
Big key
有时候,会出现一个key对应的value占用的内存比较大的情况。这种key我们称为big key。
(经验来说:String 1MB,复合类型包含5000key就认为是big key
big key在处理时会比较占用资源,特别在redis单线程的处理方式下,会阻塞任务和网络传输。
如何发现:
- 通过工具分析Redis的RDB文件
- Redis提供了scan命令,用于发现big key
如何处理:
- 分割bigkey
- 不需要的key 手动unlink来异步删除
Hot key
一个key的访问很多且明显比其他key多,我们称为hot key
hot key会导致集群机器负载不均衡,CPU、内存资源容易耗尽、影响其他键的操作。
如何发现:
- 命令 –hotkeys
- 业务代码中加监控
如何处理
- 热键分拆
- 读写分离(加机器
- 增加缓存层,如本地缓存
使用批量操作优化性能
使用批量操作可以减少网络传输次数(降低socketIO成本)和等待时间。
怎么做:
- Redis原生支持一些批量操作,如
MGET,MSET
- pipeline,Redis支持客户端把命令封装成一组,一次性提交,减少网络传输。
需要注意的一点:
批量操作并不是说就一定是一次网络传输,因为Redis会做数据分片,我们需要操作的数据不一定在同一台机器上。