Redis 给缓存数据设置过期时间有什么用?
一般情况下,我们设置保存的缓存数据的时候都会设置一个过期时间。为什么呢?
内存是有限且珍贵的,如果不对缓存数据设置过期时间,那内存占用就会一直增长,最终可能会导致 OOM 问题。通过设置合理的过期时间,Redis 会自动删除暂时不需要的数据,为新的缓存数据腾出空间。
Redis 自带了给缓存数据设置过期时间的功能,比如:
127.0.0.1:6379> expire key 60 # 数据在 60s 后过期
(integer) 1
127.0.0.1:6379> setex key 60 value # 数据在 60s 后过期 (setex:[set] + [ex]pire)
OK
127.0.0.1:6379> ttl key # 查看数据还有多久过期
(integer) 56
注意 ⚠️:Redis 中除了字符串类型有自己独有设置过期时间的命令 setex
外,其他方法都需要依靠 expire
命令来设置过期时间 。另外, persist
命令可以移除一个键的过期时间。
过期时间除了有助于缓解内存的消耗,还有什么其他用么?
很多时候,我们的业务场景就是需要某个数据只在某一时间段内存在,比如我们的短信验证码可能只在 1 分钟内有效,用户登录的 Token 可能只在 1 天内有效。
如果使用传统的数据库来处理的话,一般都是自己判断过期,这样更麻烦并且性能要差很多。
Redis 是如何判断数据是否过期的呢?
Redis 通过一个叫做过期字典(可以看作是 hash 表)来保存数据过期的时间。过期字典的键指向 Redis 数据库中的某个 key(键),过期字典的值是一个 long long 类型的整数,这个整数保存了 key 所指向的数据库键的过期时间(毫秒精度的 UNIX 时间戳)。
过期字典是存储在 redisDb 这个结构里的:
typedef struct redisDb {...dict *dict; //数据库键空间,保存着数据库中所有键值对dict *expires // 过期字典,保存着键的过期时间...
} redisDb;
在查询一个 key 的时候,Redis 首先检查该 key 是否存在于过期字典中(时间复杂度为 O(1)),如果不在就直接返回,在的话需要判断一下这个 key 是否过期,过期直接删除 key 然后返回 null。
Redis 过期 key 删除策略了解么?
如果假设你设置了一批 key 只能存活 1 分钟,那么 1 分钟后,Redis 是怎么对这批 key 进行删除的呢?
常用的过期数据的删除策略就下面这几种:
- 惰性删除:只会在取出/查询 key 的时候才对数据进行过期检查。这种方式对 CPU 最友好,但是可能会造成太多过期 key 没有被删除。
- 定期删除:周期性地随机从设置了过期时间的 key 中抽查一批,然后逐个检查这些 key 是否过期,过期就删除 key。相比于惰性删除,定期删除对内存更友好,对 CPU 不太友好。
- 延迟队列:把设置过期时间的 key 放到一个延迟队列里,到期之后就删除 key。这种方式可以保证每个过期 key 都能被删除,但维护延迟队列太麻烦,队列本身也要占用资源。
- 定时删除:每个设置了过期时间的 key 都会在设置的时间到达时立即被删除。这种方法可以确保内存中不会有过期的键,但是它对 CPU 的压力最大,因为它需要为每个键都设置一个定时器。
Redis 采用的那种删除策略呢?
Redis 采用的是 定期删除+惰性/懒汉式删除 结合的策略,这也是大部分缓存框架的选择。定期删除对内存更加友好,惰性删除对 CPU 更加友好。两者各有千秋,结合起来使用既能兼顾 CPU 友好,又能兼顾内存友好。
下面是我们详细介绍一下 Redis 中的定期删除具体是如何做的。
Redis 的定期删除过程是随机的(
#define ACTIVE_EXPIRE_CYCLE_FAST_DURATION 1000 /* Microseconds. */
#define ACTIVE_EXPIRE_CYCLE_SLOW_TIME_PERC 25 /* Max % of CPU to use. */
#define ACTIVE_EXPIRE_CYCLE_ACCEPTABLE_STALE 10 /* % of stale keys after whichwe do extra efforts. */
周期性地随机从设置了过期时间的 key 中抽查一批),所以并不保证所有过期键都会被立即删除。这也就解释了为什么有的 key 过期了,并没有被删除。并且,Redis 底层会通过限制删除操作执行的时长和频率来减少删除操作对 CPU 时间的影响。
另外,定期删除还会受到执行时间和过期 key 的比例的影响:
- 执行时间已经超过了阈值,那么就中断这一次定期删除循环,以避免使用过多的 CPU 时间。
- 如果这一批过期的 key 比例超过一个比例,就会重复执行此删除流程,以更积极地清理过期 key。相应地,如果过期的 key 比例低于这个比例,就会中断这一次定期删除循环,避免做过多的工作而获得很少的内存回收。
Redis 7.2 版本的执行时间阈值是 25ms,过期 key 比例设定值是 10%。
#define ACTIVE_EXPIRE_CYCLE_FAST_DURATION 1000 /* Microseconds. */
#define ACTIVE_EXPIRE_CYCLE_SLOW_TIME_PERC 25 /* Max % of CPU to use. */
#define ACTIVE_EXPIRE_CYCLE_ACCEPTABLE_STALE 10 /* % of stale keys after whichwe do extra efforts. */
每次随机抽查数量是多少?
expire.c
中定义了每次随机抽查的数量,Redis 7.2 版本为 20 ,也就是说每次会随机选择 20 个设置了过期时间的 key 判断是否过期。
#define ACTIVE_EXPIRE_CYCLE_KEYS_PER_LOOP 20 /* Keys for each DB loop. */
如何控制定期删除的执行频率?
在 Redis 中,定期删除的频率是由 hz 参数控制的。hz 默认为 10,代表每秒执行 10 次,也就是每秒钟进行 10 次尝试来查找并删除过期的 key。
hz 的取值范围为 1~500。增大 hz 参数的值会提升定期删除的频率。如果你想要更频繁地执行定期删除任务,可以适当增加 hz 的值,但这会加 CPU 的使用率。根据 Redis 官方建议,hz 的值不建议超过 100,对于大部分用户使用默认的 10 就足够了。
下面是 hz 参数的官方注释,我翻译了其中的重要信息(Redis 7.2 版本)。
类似的参数还有一个 dynamic-hz,这个参数开启之后 Redis 就会在 hz 的基础上动态计算一个值。Redis 提供并默认启用了使用自适应 hz 值的能力,
这两个参数都在 Redis 配置文件 redis.conf
中:
# 默认为 10
hz 10
# 默认开启
dynamic-hz yes
多提一嘴,除了定期删除过期 key 这个定期任务之外,还有一些其他定期任务例如关闭超时的客户端连接、更新统计信息,这些定期任务的执行频率也是通过 hz 参数决定。
为什么定期删除不是把所有过期 key 都删除呢?
这样会对性能造成太大的影响。如果我们 key 数量非常庞大的话,挨个遍历检查是非常耗时的,会严重影响性能。Redis 设计这种策略的目的是为了平衡内存和性能。
为什么 key 过期之后不立马把它删掉呢?这样不是会浪费很多内存空间吗?
因为不太好办到,或者说这种删除方式的成本太高了。假如我们使用延迟队列作为删除策略,这样存在下面这些问题:
- 队列本身的开销可能很大:key 多的情况下,一个延迟队列可能无法容纳。
- 维护延迟队列太麻烦:修改 key 的过期时间就需要调整期在延迟队列中的位置,并且,还需要引入并发控制。
大量 key 集中过期怎么办?
当 Redis 中存在大量 key 在同一时间点集中过期时,可能会导致以下问题:
- 请求延迟增加: Redis 在处理过期 key 时需要消耗 CPU 资源,如果过期 key 数量庞大,会导致 Redis 实例的 CPU 占用率升高,进而影响其他请求的处理速度,造成延迟增加。
- 内存占用过高: 过期的 key 虽然已经失效,但在 Redis 真正删除它们之前,仍然会占用内存空间。如果过期 key 没有及时清理,可能会导致内存占用过高,甚至引发内存溢出。
为了避免这些问题,可以采取以下方案:
- 尽量避免 key 集中过期: 在设置键的过期时间时尽量随机一点。
- 开启 lazy free 机制: 修改
redis.conf
配置文件,将lazyfree-lazy-expire
参数设置为yes
,即可开启 lazy free 机制。开启 lazy free 机制后,Redis 会在后台异步删除过期的 key,不会阻塞主线程的运行,从而降低对 Redis 性能的影响。
Redis 内存淘汰策略了解么?
相关问题:MySQL 里有 2000w 数据,Redis 中只存 20w 的数据,如何保证 Redis 中的数据都是热点数据?
Redis 的内存淘汰策略只有在运行内存达到了配置的最大内存阈值时才会触发,这个阈值是通过redis.conf
的maxmemory
参数来定义的。64 位操作系统下,maxmemory
默认为 0 ,表示不限制内存大小。32 位操作系统下,默认的最大内存值是 3GB。
你可以使用命令 config get maxmemory
来查看 maxmemory
的值。
config get maxmemory
maxmemory
0
Redis 提供了 6 种内存淘汰策略:
- volatile-lru(least recently used):从已设置过期时间的数据集(
server.db[i].expires
)中挑选最近最少使用的数据淘汰。 - volatile-ttl:从已设置过期时间的数据集(
server.db[i].expires
)中挑选将要过期的数据淘汰。 - volatile-random:从已设置过期时间的数据集(
server.db[i].expires
)中任意选择数据淘汰。 - allkeys-lru(least recently used):从数据集(
server.db[i].dict
)中移除最近最少使用的数据淘汰。 - allkeys-random:从数据集(
server.db[i].dict
)中任意选择数据淘汰。 - no-eviction(默认内存淘汰策略):禁止驱逐数据,当内存不足以容纳新写入数据时,新写入操作会报错。
4.0 版本后增加以下两种:
- volatile-lfu(least frequently used):从已设置过期时间的数据集(
server.db[i].expires
)中挑选最不经常使用的数据淘汰。 - allkeys-lfu(least frequently used):从数据集(
server.db[i].dict
)中移除最不经常使用的数据淘汰。
allkeys-xxx
表示从所有的键值中淘汰数据,而 volatile-xxx
表示从设置了过期时间的键值中淘汰数据。
config.c
中定义了内存淘汰策略的枚举数组:
configEnum maxmemory_policy_enum[] = {{"volatile-lru", MAXMEMORY_VOLATILE_LRU},{"volatile-lfu", MAXMEMORY_VOLATILE_LFU},{"volatile-random",MAXMEMORY_VOLATILE_RANDOM},{"volatile-ttl",MAXMEMORY_VOLATILE_TTL},{"allkeys-lru",MAXMEMORY_ALLKEYS_LRU},{"allkeys-lfu",MAXMEMORY_ALLKEYS_LFU},{"allkeys-random",MAXMEMORY_ALLKEYS_RANDOM},{"noeviction",MAXMEMORY_NO_EVICTION},{NULL, 0}
};
你可以使用 config get maxmemory-policy
命令来查看当前 Redis 的内存淘汰策略。
> config get maxmemory-policy
maxmemory-policy
noeviction
可以通过config set maxmemory-policy 内存淘汰策略
命令修改内存淘汰策略,立即生效,但这种方式重启 Redis 之后就失效了。修改 redis.conf
中的 maxmemory-policy
参数不会因为重启而失效,不过,需要重启之后修改才能生效。
maxmemory-policy noeviction