Redis基础教程——详解Redis的超时(过期)机制
redisDb结构介绍
首先看下redisDb的结构,在server.h文件中:
typedef struct redisDb {
dict *dict; /* The keyspace for this DB */
dict *expires; /* Timeout of keys with a timeout set */
dict *blocking_keys; /* Keys with clients waiting for data (BLPOP)*/
dict *ready_keys; /* Blocked keys that received a PUSH */
dict *watched_keys; /* WATCHED keys for MULTI/EXEC CAS */
int id; /* Database ID */
long long avg_ttl; /* Average TTL, just for stats */
unsigned long expires_cursor; /* Cursor of the active expire cycle. */
list *defrag_later; /* List of key names to attempt to defrag one by one, gradually. */
} redisDb;
- dict:保存db中所有的键值对
- expires:保存所有有过期时间的键值对
- expires_cursor:周期性删除过期键的游标,应该是6.0版本以后才有的,因为书里和网上的一些介绍都没有这个字段
redis与过期相关的命令
redis中可以使用以下命令设置key的过期时间
- EXPIRE <key> <ttl>: 将key的生存时间设置为ttl秒
- PEXPIRE <key> <ttl>: 将key的生存时间设置为ttl毫秒
- EXPIREAT <key> <timestamp>: 设置key于时间戳timestamp时过期,秒数时间戳
- PEXPIREAT <key> <timestamp>: 设置key于时间戳timestamp时过期,毫秒数时间戳
命令很好理解,P开头的都是以毫秒为单位,相关代码实现也很简单。以上四条命令的实现在expire.c
文件中:
/* EXPIRE key seconds */
void expireCommand(client *c) {
expireGenericCommand(c,mstime(),UNIT_SECONDS);
}
/* EXPIREAT key time */
void expireatCommand(client *c) {
expireGenericCommand(c,0,UNIT_SECONDS);
}
/* PEXPIRE key milliseconds */
void pexpireCommand(client *c) {
expireGenericCommand(c,mstime(),UNIT_MILLISECONDS);
}
/* PEXPIREAT key ms_time */
void pexpireatCommand(client *c) {
expireGenericCommand(c,0,UNIT_MILLISECONDS);
}
可以看出以上命令都是调用同一个函数实现的,也就是说这四个设置命令之间是可以互相转换的。mstime()
是获取当前秒级时间戳的函数,也就是说,EXPIRE、PEXPIRE,EXPIREAT命令,最终其实都是执行PEXPIREAT命令。
- TTL <key>:返回键的剩余生存时间,单位秒
- PTTL <key>:返回键的剩余生存时间,单位毫秒
这两个命令的实现也在expire.c
中,ttlCommand和pttlCommand,实现很简单,这里伪代码描述:
if(key不存在):
return -2
if(key没设置过期时间):
return -1
ttl = 计算过期时间()
return 过期时间
过期键删除策略
键过期了如何删除?一般有以下三种方法:
- 定时删除:插入过期键的同时,开一个定时任务,在键的过期来临时执行删除任务
- 惰性删除:用户查询的时候判断是否过期,过期则删除,用户不差则永远不删除
- 定期删除:每隔一段时间进行一次删除任务,遍历多少个db,删多少个键由算法决定
第一种方式优点是删除快,对内存友好,但是频繁的执行对cpu不友好;
第二中方式对cpu友好,对内存不友好
第三种方式是一、二的折中,当然如果执行的太频繁,或者每次删除的过多,就跟方式一没什么区别。
redis的过期键删除策略
redis使用了惰性删除和定期删除两种方式。
惰性实现在db.c/expireIfNeeded()中。逻辑很简单:
- key没过期,return 0
- key过期&&当前的库是从库,return 1
- key过期&&当前的库是主库,删除过期键并return1
这里需要注意的就是redis只会从主库删除过期键,主库再同步从库。
redis的定期删除策略
redis会定期执行过期键删除,定期删除函数在expire.c/activeExpireCycle(int),这个函数有两个地方调用,一个是server.c/databasesCron()函数,一个是server.c/beforeSleep()函数,databasesCron函数负责处理redis的一些后台操作,例如过期键的处理,调整大小,重散列等操作。
activeExpireCycle()有两种模式,ACTIVE_EXPIRE_CYCLE_FAST和ACTIVE_EXPIRE_CYCLE_SLOW,代表不同的执行周期,前者为“快周期”,后者“慢周期”
为了继续探究redis的删除策略,需要看activeExpireCycle(int)这个函数的实现。
首先先看下代码中对activeExpireCycle函数的注释, 翻译出来大概是以下几个点:
- 这个算法是一个自适应的算法,如果只有少量的key过期,那么只会使用少量的cpu资源去清理这些过期key,但如果有过多的过期key,那么就会采用一些策略,不会占用过多的cpu资源去清理这些过期键;
- 在每个清理的周期中,redis不会一口气清理所有的db的过期键,而是分片执行,第二个执行周期会接着第一个执行周期结束时所在的database继续执行,每个执行周期所遍历的数据库数量不会超过常量CRON_DBS_PER_CALL,这一版该常量是16;
- 该函数每次执行的工作量取决于入参是什么type,目前两种模式:快速过期模式和慢速过期模式。我们通常使用慢周期去清理过期键,频率通常是10赫兹,这个频率的变量定义是server.hz,由redis.conf中的hz变量控制,设置区间为1~500,hz与cpu的消耗成正比,通常是10,官方也不建议我们设置过高
- “慢周期”方式执行是很耗时间的,经常会因为超时而退出,所以在beforesleep()函数中会调用“快周期”模式去执行,“快周期”不会经常被调用;
- 如果是ACTIVE_EXPIRE_CYCLE_FAST类型,redis会以比较快的过期周期进行清理。如果是ACTIVE_EXPIRE_CYCLE_SLOW类型,redis会以正常的过期周期进行清理,时间周期是REDIS_HZ的一个百分比,由ACTIVE_EXPIRE_CYCLE_SLOW_TIME_PERC定义。
- 无论是哪种模式,都会有一个清理的baseline,例如每次遍历多少db,每个db遍历多少个键?过期的键多少时开始执行过期键删除工作?
翻译完我们清楚了这个函数大致的执行方式,接下来就看一些细节。
- 具体的删除策略是怎样的?过期键多少时才会触发清理策略?
首先看下代码中先定义的一些默认常量:
#define ACTIVE_EXPIRE_CYCLE_KEYS_PER_LOOP 20 /* 默认每个数据库检查的键数量. */
#define ACTIVE_EXPIRE_CYCLE_FAST_DURATION 1000 /* “快周期”的周期时间,单位微秒. */
#define ACTIVE_EXPIRE_CYCLE_SLOW_TIME_PERC 25 /* "慢周期“做多可占用的cpu资源,% */
#define ACTIVE_EXPIRE_CYCLE_ACCEPTABLE_STALE 10 /*可容忍的过期键所占内存百分比 */
从这些默认值可以看出,默认过期键数量不能超过内存的10%,并避免消耗超过25%的cpu资源。
这些只是一些系统默认的常量,redis给了我们一个额外的参数effort,让我们去修改上面这些默认变量。effort变量由redis.conf中的active-expire-effort控制,1到10,设置越大cpu的消耗也会相应增加。
可以通过下面这段代码更清楚的看出effort这个变量的作用
effort = server.active_expire_effort-1, /* Rescale from 0 to 9. */
config_keys_per_loop = ACTIVE_EXPIRE_CYCLE_KEYS_PER_LOOP +
ACTIVE_EXPIRE_CYCLE_KEYS_PER_LOOP/4*effort,
config_cycle_fast_duration = ACTIVE_EXPIRE_CYCLE_FAST_DURATION +
ACTIVE_EXPIRE_CYCLE_FAST_DURATION/4*effort,
config_cycle_slow_time_perc = ACTIVE_EXPIRE_CYCLE_SLOW_TIME_PERC +
2*effort,
config_cycle_acceptable_stale = ACTIVE_EXPIRE_CYCLE_ACCEPTABLE_STALE-
effort;
effort改变了过期键可占内存的最大百分比,改变了最大占用cpu的百分比等,effort越大,cpu负担越重,所以根据自己的需要设置effort的值。
- 如何控制一个周期内清除多少过期键?下一个周期如何接着上一个周期继续清理?
我们可以看到代码中设置了几个静态变量,用于记录每个周期结束时的一些东西。
static unsigned int current_db = 0; /* 上个周期结束时执行到的db */
static int timelimit_exit = 0; /* 上一个周期是否因为超时而退出? */
static long long last_fast_cycle = 0; /* 上一个”快周期“的运行时间 */
这里为了看得清楚,用伪代码表示:
//如果上次不是因为超时而结束,并且当前过期键数量小于可容忍的过期键数量,不处理
if(type == ACTIVE_EXPIRE_CYCLE_FAST):
if(timelimit_exit && server.stat_expired_stale_perc < config_cycle_acceptable_stale) :
return;
//如果距离上次fast模式的运行时间小于两倍的fast模式的周期,不处理
if(start < last_fast_cycle + (long long)config_cycle_fast_duration*2) :
return;
//以上检查全过,那么就可以开始fast模式,记录一下本次的运行时间
last_fast_cycle = start
//如果服务器的db数量比默认的db检查数量小,以服务器的数量为准
//如果上次是超时退出,也会在检查一遍过期键数量
if(dbs_per_call > server.dbnum || timelimit_exit) :
dbs_per_call = server.dbnum;
//开始处理每个db中的过期键
for i in range(dbs_per_call):
//获取当前的db,并将currentdb的索引加一
redisDb = server.db[current_db]
current_db += 1
//如果当先过期键的数量大于可容忍的过期键数量,那么开始执行清理
while((expired*100/sampled) > config_cycle_acceptable_stale):
//该db没有过期键,不处理
if((num = dictSize(db->expires)) == 0):
break
max_buckets = num*20; //最大的hash桶数量
checked_buckets = 0; //当前检查的hash桶
//采样数量小于每个db的默认检查键数量,当前检查的桶小于最大桶
while(sampled < num && checked_buckets < max_buckets):
//从当前db的过期游标开始检查过期键
idx = db->expires_cursor
idx &= db->expires->ht[table].sizemask
dictEntry *de = db->expires->ht[table].table[idx]
while(de):
dictEntry *e = de
de = de->next
ttl = dictGetSignedIntegerVal(e)-now;
//如果key过期,删除并计数+1
if (activeExpireCycleTryExpire(db,e,now)) expired+=1;
//记录采样数据
if (ttl > 0):
ttl_sum += ttl;//为了统计平均ttl用的
ttl_samples+=1;
samples += 1
db->expires_cursor++;
total_expired += expired
total_sampled += sampled
//超时则退出,并标记
if(ustime()-start > timelimit):
timelimit_exit = 1;
server.stat_expired_time_cap_reached_count++;
break
我们可以总结以下redis定期删除的工作模式:
- 分为快、慢两周周期模式,通常使用慢周期模式进行键的删除;
- 每次清理周期需要控制db的数量
- 控制每个db中检查键的数量,并删除其中的过期键
- 会有一些全局变量记录当前周期的检查进度,方便下个周期继续执行;
推荐阅读
-
详解Redis与Redisson的lock和tryLock功能实现机制
-
详解Redis与Redisson的lock和tryLock功能实现机制
-
RedLock: 如何实现Redis分布式锁定功能的底层机制详解
-
【2022新手指南】Java编程进阶之路 - 六、技术架构篇 ### MySQL索引底层解析与优化实战 - 你会讲解MySQL索引的数据结构吗?性能调优技巧知多少? - Redis深度揭秘:你知道多少?从基础到哨兵、主从复制全梳理 - Redis持久化及哨兵模式详解,还有集群搭建和Leader选举黑箱打开 - Zookeeper是个啥?特性和应用场景大公开 - ZooKeeper集群搭建攻略及 Leader选举、读写一致性、共享锁实现细节 - 探究ZooKeeper中的Leader选举机制及其在分布式环境中的作用 - Zab协议深入剖析:原理、功能与在Zookeeper中的核心地位 - RabbitMQ全方位解读:工作模式、消费限流、可靠投递与配置策略 - 设计者视角:RabbitMQ过期时间、死信队列与延时队列实践指南 - RocketMQ特性和应用场景揭示:理解其精髓与差异化优势 - Kafka详细介绍:特性及广泛应用于实时数据处理的场景解析 - ElasticSearch实力揭秘:特性概述与作为搜索引擎的广泛应用 - MongoDB认知升级:非关系型数据库的优势阐述,安装与使用实战教学 - BIO/NIO/AIO网络模型对比:掌握它们的区别与在网络编程中的实际应用 - Netty带你飞:理解其超快速度背后的秘密,包括线程模型分析 - 网络通信黑科技:Netty编解码原理与常用编解码器的应用,Protostuff实战演示 - 解密Netty粘包与拆包现象,怎样有效应对这一常见问题 - 自定义Netty心跳检测机制,轻松调整检测间隔时间的艺术 - Dubbo轻骑兵介绍:核心特性概览,服务降级实战与其实现益处 - Dubbo三大神器解读:本地存根与本地伪装的实战运用与优势呈现 ----------------------- 七、结语与回顾
-
Redis基础教程——详解Redis的超时(过期)机制
-
标题:【Redis】Redis魔法:揭秘Key的自动消失术——过期删除机制解析
-
标题:一文搞定Redis面试,附Redis面试大纲+常见Redis面试题-一、基础篇 快速上手 ①. 什么是redis ②. 为什么使用redis ③. 安装 ④. 基本使用(常见数据结构的命令) Java操作redis ①. Jedis ②. SpringBoot 启动redis的方式 ①. 配置文件 ②. 生产环境启动方案 二、进阶篇 redis实现session共享 redis缓存的使用 ①. 注解式 ②. Spring Cache 数据库和缓存双写一致性问题——穿透 redis实现附近的人 redis实现计数器 redis事务 redis分布式锁的使用 redis集群 redis实现延时队列 redis实现限流 redis实现布隆过滤器 发布订阅 redis优化 三、原理篇 redis单线程为什么性能好 数据类型的底层实现 持久化机制 过期策略 内存淘汰 redis优化 哨兵模