前言

我们经常用redis来当缓存使用,既然是缓存,就有过期时间,否则就有可能数据更新不及时,造成脏数据的现象,本文我们来学习下redis的过期淘汰策略。

Redis的性能

Redis有两种使用场景,一种是作为DB(数据库)使用,一种是当做缓存使用。

关于Redis的性能,我们来看一下官方数据:

:110000次/s

:81000次/s

长期使用,key会不断增加,Redis作为缓存使用,物理内存也会满。 如果物理内存满了之后,内存就需要与硬盘交换(swap) ,就是需要我们常说的虚拟内存 ,频繁IO 会造成性能急剧下降。

为了防止Redis内存占用过多,导致服务器系统崩溃,通常我们需要设置一个内存参数maxmemory,这个参数在redis.conf文件中可以进行设置。

maxmemory可以设置,也可以不进行设置,这要看具体的场景。

不设置maxmemory的场景

有两种情况我们不需要设置maxmemory参数,一种是Redis中的key是固定的,另一种是Redis当做DB使用。

此种情况下我们设置的缓存淘汰策略是禁止驱逐(默认)

1. Redis中的key是固定的

如果我们实际的场景中Redis中的key是固定的,不会增加,那么就不需要设置maxmemory参数,因为不用担心内存不够的问题。

2. 当做DB使用

因为作为数据库,我们需要保证数据的完整性,不能淘汰。

为了存储更多数据,我们可以做Redis集群,横向扩展。

设置maxmemory的场景

当Redis作为缓存使用,并且key不断增加时,我们需要设置maxmemory。这种场景是比较常见的。

maxmemory 的默认值是0,也就是不限制。使用默认值会出现的问题是,当Redis达到物理内存之后性能会急剧下降,频繁IO,导致性能下降。

那么maxmemory设置多少合适呢?

看具体的业务,正常情况下在能够保证系统正常运行的情况下,剩下的内存的都可以设置Redis。

在redis.conf中进行如下配置(注意左边不能留有空格):

1
maxmemory 1024Mb

通过命令获取maxmemory的值:

1
2
3
127.0.0.1:6379> config get maxmemory
1) "maxmemory"
2) "1073741824"

当我们设置maxmemory了之后,当趋近maxmemory时,Redis会通过缓存淘汰策略,从内存中删除对象。

expire命令详解

expire命令的作用是可以设置一个键的存活时间(ttl: time to live),过了这段时间,该键就会自动被删除

命令参数:expire key ttl(单位秒)

使用举例如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
127.0.0.1:6379> expire name 2 #2秒失效
(integer) 1
127.0.0.1:6379> get name
(nil)
127.0.0.1:6379> set name zhangfei
OK
127.0.0.1:6379> ttl name #永久有效
(integer) -1
127.0.0.1:6379> expire name 30 #30秒失效
(integer) 1
127.0.0.1:6379> ttl name #还有24秒失效
(integer) 24
127.0.0.1:6379> ttl name #失效
(integer) -2

expire原理

先来看下Redis中关于数据库结构体的定义:

1
2
3
4
5
6
7
8
typedef struct redisDb {
dict *dict; -- key Value
dict *expires; -- key ttl
dict *blocking_keys;
dict *ready_keys;
dict *watched_keys;
int id;
} redisDb;

结构体中除了id之外都是指向字典的指针,我们只看dict和expires。

dict:用来维护一个 Redis 数据库中包含的所有 Key-Value 键值对

expires:用于维护一个 Redis 数据库中设置了失效时间的键(即key与失效时间的映射)。

当我们使用 expire命令设置一个key的失效时间时,Redis 首先到 dict 这个字典表中查找要设置的key 是否存在,如果存在就将这个key和失效时间添加到 expires 这个字典表。 当我们使用setex命令向系统插入数据时,Redis 首先将 Key 和 Value 添加到 dict 这个字典表中,然后 将 Key 和失效时间添加到 expires 这个字典表中。

总结来说就是,设置了失效时间的key和具体的失效时间全部都维护在 expires 这个字典表中。

删除策略

Redis的数据删除有定时删除、惰性删除和主动删除三种方式。 Redis目前采用惰性删除+主动删除的方式。

定时删除

在设置键的过期时间的同时,创建一个定时器,让定时器在键的过期时间来临时,立即执行对键的删除 操作。 需要创建定时器,而且消耗CPU,一般不推荐使用。

惰性删除

在key被访问时如果发现它已经失效,那么就删除它。 调用expireIfNeeded函数,该函数的意义是:读取数据之前先检查一下它有没有失效,如果失效了就删 除它。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
int expireIfNeeded(redisDb *db, robj *key) {
//获取主键的失效时间 get当前时间-创建时间>ttl
long when = getExpire(db,key);
//假如失效时间为负数,说明该主键未设置失效时间(失效时间默认为-1),直接返回0
if (when < 0) return 0;
//假如Redis服务器正在从RDB文件中加载数据,暂时不进行失效主键的删除,直接返回0
if (server.loading) return 0;
...
//如果以上条件都不满足,就将主键的失效时间与当前时间进行对比,如果发现指定的主键
//还未失效就直接返回0
if (mstime() <= when) return 0;
//如果发现主键确实已经失效了,那么首先更新关于失效主键的统计个数,然后将该主键失
//效的信息进行广播,最后将该主键从数据库中删除
server.stat_expiredkeys++;
propagateExpire(db,key);
return dbDelete(db,key);

主动删除

在redis.conf文件中可以配置主动删除策略,默认是no-enviction(不删除)

1
maxmemory-policy allkeys-lru

LRU(最近最少使用)算法

LRU (Least recently used) 最近最少使用,算法根据数据的历史访问记录来进行淘汰数据,其核心思想 是“如果数据最近被访问过,那么将来被访问的几率也更高”。

最常见的实现是使用一个链表保存缓存数据,详细算法实现如下 :

  1. 新数据插入到链表头部;
  2. 每当缓存命中(即缓存数据被访问),则将数据移到链表头部;
  3. 当链表满的时候,将链表尾部的数据丢弃。
  4. 在Java中可以使用LinkHashMap(哈希链表)去实现LRU

我们以用户信息的需求为例,来演示一下LRU算法的基本思路:

,假设我们使用哈希链表来缓存用户信息,目前缓存了4个用户,这4个用户是按照时间顺序依次从链表 右端插入的。

,此时,业务方访问用户5,由于哈希链表中没有用户5的数据,我们从数据库中读取出来,插入到缓存 当中。这时候,链表中最右端是最新访问到的用户5,最左端是最近最少访问的用户1。

,业务方访问用户2,哈希链表中存在用户2的数据,我们怎么做呢?我们把用户2从它的前驱 节点和后继节点之间移除,重新插入到链表最右端。这时候,链表中最右端变成了最新访问到的用户 2,最左端仍然是最近最少访问的用户1。

,业务方请求修改用户4的信息。同样道理,我们把用户4从原来的位置移动到链表最右侧,并 把用户信息的值更新。这时候,链表中最右端是最新访问到的用户4,最左端仍然是最近最少访问的用 户1。

⑤,业务访问用户6,用户6在缓存里没有,需要插入到哈希链表。假设这时候缓存容量已经达到上限,必 须先删除最近最少访问的数据,那么位于哈希链表最左端的用户1就会被删除掉,然后再把用户6插入到 最右端 。

Redis的LRU数据淘汰机制

从 struct redisObject 中可以发现,每一个 redis 对象都会设置相应的 lru。可以想象的是,每一 次访问数据的时候,会更新 redisObject.lru。 LRU 数据淘汰机制是这样的:在数据集中随机挑选几个键值对,取出其中 lru 最大的键值对淘汰。

用当前时间-最近访问时间,差值越大,说明访问间隔时间越长。

不可能遍历key,因为特别耗时间。有下面两种方式:

volatile-lru

从已设置过期时间的数据集(server.db[i].expires)中挑选最近最少使用的数据淘汰。

allkeys-lru

从数据集(server.db[i].dict)中挑选最近最少使用的数据淘汰。

LFU(最不经常使用)算法

LFU (Least frequently used) 最不经常使用,如果一个数据在最近一段时间内使用次数很少,那么在将 来一段时间内被使用的可能性也很小。

LFU算法也分volatile-lfuallkeys-lfu两种。

Random(随机)淘汰算法

volatile-random 从已设置过期时间的数据集(server.db[i].expires)中任意选择数据淘汰 allkeys-random 从数据集(server.db[i].dict)中任意选择数据淘汰

TTL(存活时间)淘汰算法

volatile-ttl 从已设置过期时间的数据集(server.db[i].expires)中挑选将要过期的数据淘汰 redis 数据集数据结构中保存了键值对过期时间的表,即 redisDb.expires。 TTL 数据淘汰机制:从过期时间的表中随机挑选几个键值对,取出其中 ttl 最小的键值对淘汰。

noenviction(禁止驱逐)算法

禁止驱逐数据,不删除数据。这是redis默认的策略。

淘汰策略的选择

allkeys-lru : 在不确定时一般采用策略。

volatile-lru : 比allkeys-lru性能差。因为要存过期时间。

allkeys-random : 希望请求符合平均分布(每个元素以相同的概率被访问)

总结

本文主要学习了Redis常见的几种缓存淘汰策略,包括lru、lfu、ttl、noenviction等。重点对LRU算法进行了介绍。在实际中我们需要根据实际情况选择适合自己的算法。