Redis中的过期设置与处理方式

在了解Redis过期策略及淘汰机制之前,我们不妨先来了解一下Redis的过期设置及过期处理方式。

Redis的过期设置

基本过期设置

Redis 中设置过期时间主要通过以下四种方式:

  • expire key seconds:设置 key 在 n 秒后过期;
  • pexpire key milliseconds:设置 key 在 n 毫秒后过期;
  • expireat key timestamp:设置 key 在某个时间戳(精确到秒)之后过期;
  • pexpireat key millisecondsTimestamp:设置 key 在某个时间戳(精确到毫秒)之后过期;

expire:N 秒后过期

1
2
3
4
5
6
127.0.0.1:6379> set k1 v1
OK
127.0.0.1:6379> expire k1 10
(integer) 1
127.0.0.1:6379> ttl k1
(integer) 8

其中命令 ttl 的全称是 Time To Live,表示此键值在 n 秒后过期。例如,上面的结果 8 表示 k1 在 8s 后过期。

pexpire:N 毫秒后过期

1
2
3
4
5
6
7
8
127.0.0.1:6379> set k2 v2
OK
127.0.0.1:6379> pexpire k2 10000
(integer) 1
127.0.0.1:6379> pttl k2
(integer) 7140
127.0.0.1:6379> ttl k2
(integer) 4

其中 pexpire key2 10000 表示设置 k2 在 10000 毫秒(10 秒)后过期。

expireat:过期时间戳精确到秒

1
2
3
4
5
6
7
8
9
127.0.0.1:6379> time
1) "1670977889"
2) "451533"
127.0.0.1:6379> set k3 v3
OK
127.0.0.1:6379> expireat k3 1671977889
(integer) 1
127.0.0.1:6379> ttl k3
(integer) 999925

其中 expireat key3 1671977889 表示 k3 在时间戳 1671977889 之后过期(精确到秒),使用 ttl 查询可以发现在 999925s 后 k3 会过期。

Tips: Redis可以使用time 命令查询当前时间的时间戳(精确到秒)

pexpireat:过期时间戳精确到毫秒

1
2
3
4
5
6
7
8
9
10
127.0.0.1:6379> time 
1) "1670978302"
2) "837211"
127.0.0.1:6379> set k4 v4
OK
127.0.0.1:6379> pexpireat k4 1670978502000
(integer) 1
127.0.0.1:6379> pttl k4
(integer) 164427
127.0.0.1:6379> ttl k4

其中 pexpireat k4 1670978502000 表示 k4 在时间戳 1670978502000 后过期(精确到毫秒),使用 ttl 查询可以发现在 164427ms 后 k4 会过期。

字符串中的过期操作

字符串中几个直接操作过期时间的方法,如下列表:

  • set key value ex seconds:设置键值对的同时指定过期时间(精确到秒);
  • set key value px milliseconds:设置键值对的同时指定过期时间(精确到毫秒);
  • setex key seconds valule:设置键值对的同时指定过期时间(精确到秒)。

移除过期时间

使用命令: persist key 可以移除键值的过期时间,如下代码所示。

1
2
3
4
5
6
127.0.0.1:6379> ttl k5
(integer) 36
127.0.0.1:6379> persist k5
(integer) 1
127.0.0.1:6379> ttl k5
(integer) -1

持久化中的过期键处理

RDB 中的过期键

RDB 文件分为两个阶段,RDB 文件生成阶段和加载阶段。

RDB 文件生成阶段

从内存状态持久化成 RDB(文件)的时候,会对 key 进行过期检查,过期的键不会被保存到新的 RDB 文件中,因此 Redis 中的过期键不会对生成新 RDB 文件产生任何影响。

RDB 文件加载阶段

RDB 加载分为以下两种情况:

  • 如果 Redis 是主服务器运行模式的话,在载入 RDB 文件时,程序会对文件中保存的键进行检查,过期键不会被载入到数据库中。所以过期键不会对载入 RDB 文件的主服务器造成影响;
  • 如果 Redis 是从服务器运行模式的话,在载入 RDB 文件时,不论键是否过期都会被载入到数据库中。但由于主从服务器在进行数据同步时,从服务器的数据会被清空。所以一般来说,过期键对载入 RDB 文件的从服务器也不会造成影响。

RDB 文件加载的源码可以在 rdb.c 文件的 rdbLoad() 函数中找到,源码在如下所示:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
/* Check if the key already expired. This function is used when loading
* an RDB file from disk, either at startup, or when an RDB was
* received from the master. In the latter case, the master is
* responsible for key expiry. If we would expire keys here, the
* snapshot taken by the master may not be reflected on the slave.
*
* 如果服务器为主节点的话,
* 那么在键已经过期的时候,不再将它们关联到数据库中去
*/
if (server.masterhost == NULL && expiretime != -1 && expiretime < now) {
decrRefCount(key);
decrRefCount(val);
// 跳过
continue;
}

AOF 中的过期键

AOF 文件写入阶段

当 Redis 以 AOF 模式持久化时,如果数据库某个过期键还没被删除,那么 AOF 文件会保留此过期键,当此过期键被删除后,Redis 会向 AOF 文件追加一条 DEL 命令来显式地删除该键值。

AOF 重写阶段

执行 AOF 重写时,会对 Redis 中的键值对进行检查已过期的键不会被保存到重写后的 AOF 文件中,因此不会对 AOF 重写造成任何影响。

主从库的过期键处理

当 Redis 运行在主从模式下时,从库不会进行过期扫描,从库对过期的处理是被动的。也就是即使从库中的 key 过期了,如果有客户端访问从库时,依然可以得到 key 对应的值,像未过期的键值对一样返回。

从库的过期键处理依靠主服务器控制,主库在 key 到期时,会在 AOF 文件里增加一条 del 指令,同步到所有的从库,从库通过执行这条 del 指令来删除过期的 key。

Redis过期策略

过期键执行流程

Redis 之所以能知道那些键值过期,是因为在 Redis 中维护了一个字典,存储了所有设置了过期时间的键值,我们称之为过期字典。

过期判断流程如下:

image-20221219135358261

过期策略

为了防止Redis中已过期的键值占用过多的空间,Redis必然会按照一定的策略去清理这些过期数据,但是Redis是单线程的,直接定时轮询删除很可能会影响到主业务的执行,所以Redis制定了多个不同的删除策略来规避这个问题。

Redis的过期策略主要分为以下三种:

定时删除

在设置键值过期时间时,创建一个定时事件,当过期时间到达时,由事件处理器自动执行键的删除操作。

  • 优点:保证内存可以被尽快地释放。
  • 缺点:在 Redis 高负载的情况下或有大量过期键需要同时处理时,会造成 Redis 服务器卡顿,影响主业务执行。

定期删除

每隔一段时间检查一次数据库,随机删除一些过期键。(Redis 默认每秒进行 10 次过期扫描,此配置可通过 Redis 的配置文件 redis.conf 进行配置,配置键为 hz 它的默认值是 hz 10)

但是Redis定期删除时并不是扫描遍历过期字典中所有的数据,而是采用随机抽取判断并删除过期键的形式执行的。定期删除的操作步骤如下:

  1. 从过期字典中随机取出 20 个键;
  2. 删除这 20 个键中过期的键;
  3. 如果过期 key 的比例超过 25%,重复步骤 1。

Tips:为了保证过期扫描不会出现循环过度而导致线程卡死现象,上面的算法还增加了扫描时间的上限,默认不会超过 25ms。

  • 优点:通过限制删除操作的时长和频率,来减少删除操作对 Redis 主业务的影响,同时也能删除一部分过期的数据减少了过期键对空间的无效占用。
  • 缺点:内存清理方面没有定时删除效果好,同时没有惰性删除使用的系统资源少。

惰性删除

不主动删除过期键,每次从数据库获取键值时判断是否过期,如果过期则删除键值,并返回 null。

  • 优点:因为每次访问时,才会判断过期键,所以此策略只会使用很少的系统资源。
  • 缺点:系统占用空间删除不及时,导致空间利用率降低,造成了一定的空间浪费。

Redis 使用的过期策略

Redis 使用的是惰性删除加定期删除的过期策略。

内存淘汰机制与算法

概述

在了解Redis内存淘汰机制之前,必须要搞清楚一个概念:Redis的过期策略和内存淘汰机制是完全不同的两个概念,千万不要混为一谈。

具体的原因如下:

Redis的过期策略主要是为了删除那些已经过期的键值对,而内存淘汰机制是为了在Redis在Redis运行内存到达设置的最大内存时,用什么策略来淘汰删除符合淘汰条件的数据,以此来保障Redis高效运行的。

内存淘汰执行流程如下所示:

image-20221219142730435

我们可以使用命令 config get maxmemory 来查看设置的最大运行内存,命令如下:

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

Tips:这里的0是 64 位操作系统默认的值,表示内存没有限制大小,而不是真正的0(32 位操作系统,默认的最大内存值是 3GB)。

淘汰策略查看与设置

查看 Redis 内存淘汰策略

我们可以使用 config get maxmemory-policy 命令,来查看当前 Redis 的内存淘汰策略,命令如下:

1
2
3
127.0.0.1:6379> config get maxmemory-policy
1) "maxmemory-policy"
2) "noeviction"

从上面可以看出当前 Redis 使用的是 noeviction 类型的内存淘汰机制,它表示当运行内存超过最大设置内存时,不淘汰任何数据,但新增操作会报错。

修改 Redis 内存淘汰策略

设置内存淘汰策略有两种方法,这两种方法各有利弊,需要使用者自己去权衡。

  • 方式一:通过“config set maxmemory-policy 策略”命令设置。它的优点是设置之后立即生效,不需要重启 Redis 服务,缺点是重启 Redis 之后,设置就会失效。
  • 方式二:通过修改 Redis 配置文件修改,设置“maxmemory-policy 策略”,它的优点是重启 Redis 服务后配置不会丢失,缺点是必须重启 Redis 服务,设置才能生效。

Redis淘汰策略介绍

早期Redis有六种淘汰策略

策略 描述
volatile-lru 从已设置过期时间的 KV 集中优先对最近最少使用(less recently used)的数据淘汰
volitile-ttl 从已设置过期时间的 KV 集中优先对剩余时间短(time to live)的数据淘汰
volitile-random 从已设置过期时间的 KV 集中随机选择数据淘汰
allkeys-lru 从所有 KV 集中优先对最近最少使用(less recently used)的数据淘汰
allKeys-random 从所有 KV 集中随机选择数据淘汰
noeviction 不淘汰策略,当运行内存超过最大设置内存时,不淘汰数据,但新增返回错误信息

4.0 版本后增加以下两种

  • volatile-lfu:从已设置过期时间的数据集(server.db[i].expires)中挑选最不经常使用的数据淘汰
  • allkeys-lfu:当内存不足以容纳新写入数据时,在键空间中,移除最不经常使用的 key

LRU 与 LFU算法

上面的淘汰策略中,除了比较简单的随机删除与不删除外,主要还有两种算法:LRU 和 LFU, 下面我们来了解一下这两种算法的实现与区别。

LRU 算法

LRU 全称是 Least Recently Used 译为最近最少使用,是一种常用的页面置换算法,选择最近最久未使用的页面予以淘汰。

LRU 算法实现

LRU 算法需要基于链表结构,链表中的元素按照操作顺序从前往后排列,最新操作的键会被移动到表头,当需要内存淘汰时,只需要删除链表尾部的元素即可。

近 LRU 算法

Redis 使用的是一种近似 LRU 算法,目的是为了更好的节约内存,它的实现方式是给现有的数据结构添加一个额外的字段,用于记录此键值的最后一次访问时间,Redis 内存淘汰时,会使用随机采样的方式来淘汰数据,它是随机取 5 个值(此值可配置),然后淘汰最久没有使用的那个。

LRU 算法缺点

LRU 算法有一个缺点,比如说很久没有使用的一个键值,如果最近被访问了一次,那么它就不会被淘汰,即使它是使用次数最少的缓存,那它也不会被淘汰。

LFU 算法

介于上文中LRU算法的缺点, Redis 4.0 在之后引入了 LFU 算法。

LFU 全称是 Least Frequently Used 翻译为最不常用的,最不常用的算法是根据总访问次数来淘汰数据的,它的核心思想是“如果数据过去被访问多次,那么将来被访问的频率也更高”。

LFU 解决了偶尔被访问一次之后,数据就不会被淘汰的问题,相比于 LRU 算法也更合理一些。

在 Redis 中每个对象头中记录着 LFU 的信息,源码如下:

1
2
3
4
5
6
7
8
9
typedef struct redisObject {
unsigned type:4;
unsigned encoding:4;
unsigned lru:LRU_BITS; /* LRU time (relative to global lru_clock) or
* LFU data (least significant 8 bits frequency
* and most significant 16 bits access time). */
int refcount;
void *ptr;
} robj;

在 Redis 中 LFU 存储分为两部分,16 bit 的 ldt(last decrement time)和 8 bit 的 logc(logistic counter)。

  1. logc 是用来存储访问频次,8 bit 能表示的最大整数值为 255,它的值越小表示使用频率越低,越容易淘汰;
  2. ldt 是用来存储上一次 logc 的更新时间。