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 | 127.0.0.1:6379> set k1 v1 |
其中命令 ttl 的全称是 Time To Live,表示此键值在 n 秒后过期。例如,上面的结果 8 表示 k1 在 8s 后过期。
pexpire:N 毫秒后过期
1 | 127.0.0.1:6379> set k2 v2 |
其中 pexpire key2 10000
表示设置 k2 在 10000 毫秒(10 秒)后过期。
expireat:过期时间戳精确到秒
1 | 127.0.0.1:6379> time |
其中 expireat key3 1671977889
表示 k3 在时间戳 1671977889 之后过期(精确到秒),使用 ttl 查询可以发现在 999925s 后 k3 会过期。
Tips: Redis可以使用time 命令查询当前时间的时间戳(精确到秒)
pexpireat:过期时间戳精确到毫秒
1 | 127.0.0.1:6379> time |
其中 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 | 127.0.0.1:6379> ttl k5 |
持久化中的过期键处理
RDB 中的过期键
RDB 文件分为两个阶段,RDB 文件生成阶段和加载阶段。
RDB 文件生成阶段
从内存状态持久化成 RDB(文件)的时候,会对 key 进行过期检查,过期的键不会被保存到新的 RDB 文件中,因此 Redis 中的过期键不会对生成新 RDB 文件产生任何影响。
RDB 文件加载阶段
RDB 加载分为以下两种情况:
- 如果 Redis 是主服务器运行模式的话,在载入 RDB 文件时,程序会对文件中保存的键进行检查,过期键不会被载入到数据库中。所以过期键不会对载入 RDB 文件的主服务器造成影响;
- 如果 Redis 是从服务器运行模式的话,在载入 RDB 文件时,不论键是否过期都会被载入到数据库中。但由于主从服务器在进行数据同步时,从服务器的数据会被清空。所以一般来说,过期键对载入 RDB 文件的从服务器也不会造成影响。
RDB 文件加载的源码可以在 rdb.c 文件的 rdbLoad() 函数中找到,源码在如下所示:
1 | /* Check if the key already expired. This function is used when loading |
AOF 中的过期键
AOF 文件写入阶段
当 Redis 以 AOF 模式持久化时,如果数据库某个过期键还没被删除,那么 AOF 文件会保留此过期键,当此过期键被删除后,Redis 会向 AOF 文件追加一条 DEL 命令来显式地删除该键值。
AOF 重写阶段
执行 AOF 重写时,会对 Redis 中的键值对进行检查已过期的键不会被保存到重写后的 AOF 文件中,因此不会对 AOF 重写造成任何影响。
主从库的过期键处理
当 Redis 运行在主从模式下时,从库不会进行过期扫描,从库对过期的处理是被动的。也就是即使从库中的 key 过期了,如果有客户端访问从库时,依然可以得到 key 对应的值,像未过期的键值对一样返回。
从库的过期键处理依靠主服务器控制,主库在 key 到期时,会在 AOF 文件里增加一条 del 指令,同步到所有的从库,从库通过执行这条 del 指令来删除过期的 key。
Redis过期策略
过期键执行流程
Redis 之所以能知道那些键值过期,是因为在 Redis 中维护了一个字典,存储了所有设置了过期时间的键值,我们称之为过期字典。
过期判断流程如下:
过期策略
为了防止Redis中已过期的键值占用过多的空间,Redis必然会按照一定的策略去清理这些过期数据,但是Redis是单线程的,直接定时轮询删除很可能会影响到主业务的执行,所以Redis制定了多个不同的删除策略来规避这个问题。
Redis的过期策略主要分为以下三种:
定时删除
在设置键值过期时间时,创建一个定时事件,当过期时间到达时,由事件处理器自动执行键的删除操作。
- 优点:保证内存可以被尽快地释放。
- 缺点:在 Redis 高负载的情况下或有大量过期键需要同时处理时,会造成 Redis 服务器卡顿,影响主业务执行。
定期删除
每隔一段时间检查一次数据库,随机删除一些过期键。(Redis 默认每秒进行 10 次过期扫描,此配置可通过 Redis 的配置文件 redis.conf 进行配置,配置键为 hz 它的默认值是
hz 10
)
但是Redis定期删除时并不是扫描遍历过期字典中所有的数据,而是采用随机抽取判断并删除过期键的形式执行的。定期删除的操作步骤如下:
- 从过期字典中随机取出 20 个键;
- 删除这 20 个键中过期的键;
- 如果过期 key 的比例超过 25%,重复步骤 1。
Tips:为了保证过期扫描不会出现循环过度而导致线程卡死现象,上面的算法还增加了扫描时间的上限,默认不会超过 25ms。
- 优点:通过限制删除操作的时长和频率,来减少删除操作对 Redis 主业务的影响,同时也能删除一部分过期的数据减少了过期键对空间的无效占用。
- 缺点:内存清理方面没有定时删除效果好,同时没有惰性删除使用的系统资源少。
惰性删除
不主动删除过期键,每次从数据库获取键值时判断是否过期,如果过期则删除键值,并返回 null。
- 优点:因为每次访问时,才会判断过期键,所以此策略只会使用很少的系统资源。
- 缺点:系统占用空间删除不及时,导致空间利用率降低,造成了一定的空间浪费。
Redis 使用的过期策略
Redis 使用的是惰性删除加定期删除的过期策略。
内存淘汰机制与算法
概述
在了解Redis内存淘汰机制之前,必须要搞清楚一个概念:Redis的过期策略和内存淘汰机制是完全不同的两个概念,千万不要混为一谈。
具体的原因如下:
Redis的过期策略主要是为了删除那些已经过期的键值对,而内存淘汰机制是为了在Redis在Redis运行内存到达设置的最大内存时,用什么策略来淘汰删除符合淘汰条件的数据,以此来保障Redis高效运行的。
内存淘汰执行流程如下所示:
我们可以使用命令 config get maxmemory
来查看设置的最大运行内存,命令如下:
1 | 127.0.0.1:6379> config get maxmemory |
Tips:这里的0是 64 位操作系统默认的值,表示内存没有限制大小,而不是真正的0(32 位操作系统,默认的最大内存值是 3GB)。
淘汰策略查看与设置
查看 Redis 内存淘汰策略
我们可以使用 config get maxmemory-policy
命令,来查看当前 Redis 的内存淘汰策略,命令如下:
1 | 127.0.0.1:6379> config get maxmemory-policy |
从上面可以看出当前 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 | typedef struct redisObject { |
在 Redis 中 LFU 存储分为两部分,16 bit 的 ldt(last decrement time)和 8 bit 的 logc(logistic counter)。
- logc 是用来存储访问频次,8 bit 能表示的最大整数值为 255,它的值越小表示使用频率越低,越容易淘汰;
- ldt 是用来存储上一次 logc 的更新时间。