Redis核心技术19-缓冲替换策略
# Redis核心技术19-缓冲替换策略
Redis缓冲使用内存来保存数据,就避免了业务应用从后端数据库中读取数据,可以提升应用的响应速度。但是我们不可能把所有要访问的数据都放入缓冲,由于内存相比于磁盘来说更为昂贵,为了保证交过较高的性价比,缓存的空间容量必然要小于后端数据库的数据总量。内存大小空间有限,但是要缓存的数据量越来越大,有限的缓存空间就会被写满。
解决这个问题就涉及到缓存系统的一个重要机制,即缓存数据的淘汰机制。简单来说,数据淘汰机制包括两步:第一,根据一定的策略,筛选出对应用访问来说“不重要”的数据;第二,将这些数据从缓存中删除,为新来的数据腾出空间。
# 如何设置合适的缓存大小
实际应用中的数据访问都是具有局部性的,所谓“八二原理”,有 20% 的数据贡献了 80% 的访问了,而剩余的数据虽然体量很大,但只贡献了 20% 的访问量。这 80% 的数据在访问量上就形成了一条长长的尾巴,我们也称为“长尾效应”,对应下图的蓝线。
如果按照“八二原理”来设置缓存空间容量,也就是把缓存空间容量设置为总数据量的 20% 的话,就有可能拦截到 80% 的访问。但是如果对于秒杀产品而言,秒杀产品大概占所有产品的5%,但是这5%的大概超过90% 的访问请求。而对于商品信息的查询统计,即使按照“八二原理”缓存了 20% 的商品数据,也不能获得很好的访问性能,因为 80% 的数据仍然需要从后端数据库中获取。
对于红线,80% 的数据贡献的访问量,超过了传统的长尾效应中 80% 数据能贡献的访问量。原因在于,用户的个性化需求越来越多,在一个业务应用中,不同用户访问的内容可能差别很大,所以,用户请求的数据和它们贡献的访问量比例,不再具备长尾效应中的“八二原理”分布特征了。也就是说,20% 的数据可能贡献不了 80% 的访问,而剩余的 80% 数据反而贡献了更多的访问量,我们称之为重尾效应。
在实践过程中,我看到过的缓存容量占总数据量的比例,从 5% 到 40% 的都有。这个容量规划不能一概而论,是需要结合应用数据实际访问特征和成本开销来综合考虑的。建议把缓存容量设置为总数据量的 15% 到 30%,兼顾访问性能和内存空间开销。
# Redis缓存淘汰策略
Redis一共有8中淘汰策略,按照是否会进行数据淘汰把它们分成两类:
- 不进行数据淘汰的策略,只有 noeviction 这一种。
- 会进行淘汰的 7 种其他策略。
会进行淘汰的 7 种策略,我们可以再进一步根据淘汰候选数据集的范围把它们分成两类:
- 在设置了过期时间的数据中进行淘汰,包括 volatile-random、volatile-ttl、volatile-lru、volatile-lfu(Redis 4.0 后新增)四种。
- 在所有数据范围内进行淘汰,包括 allkeys-lru、allkeys-random、allkeys-lfu(Redis 4.0 后新增)三种。
对于不进行数据淘汰的noevction,是指一旦缓冲被写满了,再有写请求过来的时候,Redis 不再提供服务,而是直接返回错误,显然这种策略是不适合Redis缓冲的。
对于设置了过期时间的数据淘汰策略:它们筛选的候选数据范围,被限制在已经设置了过期时间的键值对上。也正因为此,即使缓存没有写满,这些数据如果过期了,也会被删除。
volatile-ttl 在筛选时,会针对设置了过期时间的键值对,根据过期时间的先后进行删除,越早过期的越先被删除。
volatile-random 就像它的名称一样,在设置了过期时间的键值对中,进行随机删除。
volatile-lru 会使用 LRU 算法筛选设置了过期时间的键值对。
volatile-lfu 会使用 LFU 算法选择设置了过期时间的键值对。
对于所有数据中进行淘汰的策略:备选淘汰数据范围,就扩大到了所有键值对,无论这些键值对是否设置了过期时间。它们筛选数据进行淘汰的规则是:
- allkeys-random 策略,从所有键值对中随机选择并删除数据;
- allkeys-lru 策略,使用 LRU 算法在所有数据中进行筛选。
- allkeys-lfu 策略,使用 LFU 算法在所有数据中进行筛选。
下面我们重点对LRU算法进行学习:
LRU 算法的全称是 Least Recently Used,按照最近最少使用的原则来筛选数据,最不常用的数据会被筛选出来,而最近频繁使用的数据会留在缓存中。LRU会把所有的数据组织成一个链表,链表的头和尾分别表示MRU 端和 LRU 端,分别代表最近最常使用的数据和最近最不常用的数据。
如果有一个新数据 15 要被写入缓存,但此时已经没有缓存空间了,也就是链表没有空余位置了,那么,LRU 算法做两件事:
- 数据 15 是刚被访问的,所以它会被放到 MRU 端;
- 算法把 LRU 端的数据 5 从缓存中删除,相应的链表中就没有数据 5 的记录了。
LRU算法的思想:刚被访问的数据,可能会被再次访问,所以就把它放在MRU端;长久不访问的数据,肯定以后就不会再被访问,所以就让它逐渐后移到 LRU 端,在缓存满时,就优先删除它。
但是LRU对于Redis实际使用而言存在两个问题:
- LRU 算法在实际实现时,需要用链表管理所有的缓存数据,这会带来额外的空间开销
- 当有数据被访问时,需要在链表上把该数据移动到 MRU 端,如果有大量数据被访问,就会带来很多链表移动操作,会很耗时,进而会降低 Redis 缓存性能。
Redis对LRU算法进行了简化,以减轻数据淘汰对缓存性能的影响。具体来说,Redis 默认会记录每个数据的最近一次访问的时间戳(由键值对数据结构 RedisObject 中的 lru 字段记录)。然后,Redis 在决定淘汰的数据时,第一次会随机选出 N 个数据,把它们作为一个候选集合。接下来,Redis 会比较这 N 个数据的 lru 字段,把 lru 字段值最小的数据从缓存中淘汰出去。
# 如何处理淘汰的数据
一般来说,一旦被淘汰的数据选定后,如果这个数据是干净数据,那么我们就直接删除;如果这个数据是脏数据,我们需要把它写回数据库,如下图所示:
如何判断一个数据是干净还是脏呢:
- 干净数据一直没有被修改,所以后端数据库里的数据也是最新值。在替换时,它可以被直接删除。
- 而脏数据就是曾经被修改过的,已经和后端数据库中保存的数据不一致了。此时,如果不把脏数据写回到数据库中,这个数据的最新值就丢失了,就会影响应用的正常使用。
所以,我们在使用 Redis 缓存时,如果数据被修改了,需要在数据修改时就将它写回数据库。否则,这个脏数据被淘汰时,会被 Redis 删除,而数据库里也没有最新的数据了。