旁路缓存:Redis缓存应用

缓存的特征

  1. 在一个层次化的系统中,缓存一定是一个快速子系统,数据存在缓存中时,能避免每次从慢速子系统中存取数据;
  2. 缓存系统的容量大小总是小于后端慢速系统,我们不可能把所有数据都放在缓存系统中;

Redis 缓存处理请求的两种情况

  • 缓存命中:Redis 中有相应的数据,就直接读取 Redis,性能非常快。
  • 缓存缺失:Redis 中没有保存相应的数据,就从后端数据库中读取数据,性能就会变慢。而且,一旦发生缓存缺失,为了让后续请求能从缓存中读取到数据,我们需要把缺失的数据写入Redis,这个过程叫做缓存更新,缓存更新操作会设计到保证缓存和数据库之间的数据一致性问题。

Redis 作为旁路缓存的使用操作

旁路缓存:Redis 不主动访问数据库加载数据到自己的内存中,而是在应用程序读取Redis缓存,如果不存在,就去查后端数据库,再将查询到的数据加载到Redis。

在使用旁路缓存时,需要在应用程序中增加操作代码,增加了使用Redis缓存的额外工作量,但是,也正是因为Redis是旁路缓存,是一个独立的系统,我们可以单独对Redis缓存进行扩容或性能优化。而且,只要保持操作接口不变,我们在应用程序中增加的代码就不用再修改了。

Redis 缓存的两种类型:

  • 只读缓存:能加速读请求
  • 读写缓存:同时加速读写请求,读写缓存又有两种数据写回策略,可以让我们根据业务需求,在保证性能和保证数据可靠性之间进行选择。

缓存类型

只读缓存

读请求

先调用 Redis 的 Get 接口,查询数据是否存在,如果发生缓存缺失,应用会把这些数据从数据库读出来,并写到缓存中。

写请求

会直接发往后端的数据库,在数据库中增删改。对于删改的数据来说,应用需要把这些缓存的数据删除,Redis 中就没有这些数据了。

这里最新的数据在数据库中。

读写缓存

对于读写缓存来说,除了读请求会发送到缓存进行处理,写请求也会发送到缓存,在缓存中直接对数据进行增删改操作。由于Redis的高性能访问特性,数据的增删改操作可以在缓存中快速完成,处理结果也会快速返回给业务应用,提升业务响应速度。

这里,最新的数据是在Reids中,一旦出现掉电或宕机,内存中的数据就会丢失。

所以,根据业务应用对数据可靠性和缓存性能的不同要求,我们会有同步直写和异步写回两种策略。其中,同步直写策略优先保证数据可靠性,异步写回策略优先提供快速响应。

同步直写

写请求发给缓存的同时,也会发给后端数据库进行处理,等到缓存和数据库都处理完数据,才给客户端返回。

异步写回

写请求先在缓存处理,等这些增改的数据要被从缓存中淘汰出来时,缓存将他们写回后端数据库。

缓存容量设置

结合应用数据实际访问特征和成本开销来综合考虑。

一般来说,缓存容量设置为总数据量的15%到30%,兼顾访问性能和内存空间开销。

Redis可以通过以下命令设定缓存的大小:

1
CONFIG SET maxmemory 4gb

缓存写满是不可避免的,所以如何淘汰数据以及如何处理那些被淘汰的数据需要了解。

Redis 缓存的淘汰策略

一共有8种策略,4.0以前6种,之后增加了2种,其中,只有一种noeviction不淘汰数据的策略,七种淘汰数据的策略。

七种淘汰策略可以根据淘汰候选数据集的范围把它们分为两类:

  • 在设置了过期时间的数据中进行淘汰,包括:volatile-random、volatile-ttl、volatile-lru、volatile-lfu(4.0后增加) 这四种。
  • 在所有数据范围内进行淘汰,包括:allkeys-lru、allkeys-random、allkeys-lfu(4.0后增加)三种。

默认情况下,Reids 在使用的内存空间操作maxmemory值时,并不会淘汰数据,也就是设定的 noeviction 策略。对应到Redis缓存,也就是指,一旦缓存被写满,写请求再来时,Redis 不再提供服务,而是直接返回错误。

设置过期时间的数据,当过期时间快到期,或者达到了Maxmemory阈值,Redis会按照以下策略进行淘汰。

  • volatile-ttl:根据过期时间的先后进行删除,越早过期的越先被删除。
  • volatile-random:设置了过期时间的键值对中进行随机删除。
  • volatile-lru:以LRU算法筛选
  • volatile-lfu:以LFU算法筛选

和以上策略类似,对于所有数据也有以下3类淘汰策略:

  • allkeys-random
  • allkeys-lru
  • allkeys-lfu

LRU算法

全称 Least Recently Used,按最近最少使用的原则筛选数据。最不常用的数据会被筛选出来,而最近频繁使用的数据会留在缓存中。

LRU 会把所有的数据组织成一个链表,链表的头和尾分别表示MRU端和LRU端,分别代表最近最常使用的数据和最近最不常用的数据。删除数据时从LRU端开始。

每有一个数据被访问,该数据就会放入链表头,链表中其他数据则相应向后移一位。

LRU在实际实现时,会创建一个额外的链表来管理所有缓存数据,带来了额外的空间开销。当有大量数据被访问时,就会带来很多链表移动操作,会很耗时,进而降低性能。

在Redis中,LRU 算法被进行了优化,以减轻数据淘汰对缓存性能的影响。具体来说,Redis 默认会记录每个数据的最近一次访问的时间戳(由键值对数据结构 RedisObject 中的 lru 字段记录)。然后,Redis 在决定淘汰的数据时,第一次会随机选出 N 个数据,把他们作为一个候选集合。接下来,Redis 会比较这 N 个数据的 lru 字段,把 lru 字段值最小的数据从缓存中淘汰出去。(局部数据淘汰机制,所以 Redis 中可能存在长时间未删除的过期数据)

可以配置参数 maxmemory-samples 来设置 N 的个数。

1
CONFIG SET maxmemory-samples 100

当需要再次淘汰数据时,Redis 需要挑选数据进入第一次淘汰时创建的候选集合。这里的挑选标准是:能进入候选集合的数据的 lru 字段值必须小于候选集合中最小的 lru 值。 当有新数据进入候选数据集后,如果候选数据集中的数据个数达到了 maxmemory-samples,Redis 就把候选数据集中 lru 字段最小的数据淘汰出去。

这样 Redis 缓存不用为所有数据维护一个大链表,也不用在每次数据访问时都移动链表项,提升了缓存的性能。

缓存淘汰策略使用的建议

  • 优先使用 allkeys-lru 策略
  • 业务应用的数据访问频率相差不大,没有明显的冷热数据区分,建议使用 allkeys-random 策略
  • 如果业务中有置顶需求,比如置顶新闻、置顶视频,那么可以使用 volatile-lru 策略,同时不给这些置顶数据设置过期时间。

如何处理被淘汰的数据

一般来说,一个数据被淘汰策略选定之后,如果这个数据是干净数据,那么直接删除;如果这个数据是脏数据,那么我们需要把它写回数据库。

如何判断数据是干净还是脏?

干净数据和脏数据的区别就在于,和最初从后端数据库里读取时的值相比,有没有被修改过。修改过的值需要写回数据库,保证数据的一致性。