文章详情

短信预约-IT技能 免费直播动态提醒

请输入下面的图形验证码

提交验证

短信预约提醒成功

Redis过期键与内存淘汰策略深入分析讲解

2022-11-28 11:45

关注

以下内容是基于Redis 6.2.6 版本整理总结

一、Redis数据库的组织方式

Redis服务器将所有的数据库 都保存在src/server.h/redisServer结构中的db数组中。db数组的每个entry都是src/server.h/redisDb结构,每个redisDb结构代表一个数据库。Redis默认有16个数据库。

1.1 redisServer结构定义

struct redisServer {
    
    pid_t pid;                  
    pthread_t main_thread_id;         
	...
    redisDb *db;   // db数组
    ...
    int dbnum;     // redis db的数量
    ...
};

Redis过期键与内存淘汰策略深入分析讲解

1.2 redisDb 结构定义

typedef struct redisDb {
    dict *dict;                  //键空间,保存数据库中所有的键值对
    dict *expires;              
    dict *blocking_keys;        
    dict *ready_keys;           
    dict *watched_keys;         
    int id;                     
    long long avg_ttl;          
    unsigned long expires_cursor; 
    list *defrag_later;         
} redisDb;

各字段含义解释:

1.3 redisdb初始化

// src/server.c
void initServer(void) {
    int j;
    // ...
	server.db = zmalloc(sizeof(redisDb)*server.dbnum);
	// ...
	
    for (j = 0; j < server.dbnum; j++) {
        server.db[j].dict = dictCreate(&dbDictType,NULL);
        server.db[j].expires = dictCreate(&dbExpiresDictType,NULL);
        server.db[j].expires_cursor = 0;
        server.db[j].blocking_keys = dictCreate(&keylistDictType,NULL);
        server.db[j].ready_keys = dictCreate(&objectKeyPointerValueDictType,NULL);
        server.db[j].watched_keys = dictCreate(&keylistDictType,NULL);
        server.db[j].id = j;
        server.db[j].avg_ttl = 0;
        server.db[j].defrag_later = listCreate();
        listSetFreeMethod(server.db[j].defrag_later,(void (*)(void*))sdsfree);
    }
   //...
}

二、过期键

2.1 设置键的过期时间

redis客户端提供了expire或pexpire命令来设置键的过期时间(Time to live, TTL),在经过指定秒数或者毫秒数后,redis服务器会自动删除生存时间为0的键。ttl命令是以秒为单位返回键的剩余生存时间,pttl命令则是以毫秒为单位。

Redis过期键与内存淘汰策略深入分析讲解

也可以通过 setex 在设置某个键的同时为其设置过期时间:

Redis过期键与内存淘汰策略深入分析讲解

如果一个键没有设置过期时间或者设置了过期时间又通过persist命令取消了过期时间,则执行ttl查看键的过期时间返回-1

Redis过期键与内存淘汰策略深入分析讲解

2.2 过期键的判定

开头我们在学习redisDb 结构的时候说过,过redisDb 中的expires过期字典保存了数据中的所有键的过期时间。要判断一个键是否过期:

2.3 过期键的删除策略

惰性删除:放任过期键不管,但是每次从键空间获取键的时候,都会先检查键是否过期,如果过期了就删除,否则就正常返回。

优点:对CPU友好,对内存不友好,如果有访问的不到键,且已经过期了,则永远不会被删除。

定期删除:每隔一段时间,检查一次数据库,删除里面的过期键。要扫描多少个数据库,以及要删除多少过期键,由算法控制。

Redis服务器采用了上面两种策略的组合使用,很好的平衡了CPU的使用和内存的使用。

2.3.1 惰性删除的实现

惰性删除由expireIfNeeded函数实现,Redis在执行读写命令时都会先调用expireIfNeeded函数对键进行检查。如果已经过期,expireIfNeeded函数就会删除该键值对;如果没有过期,则什么都不做。

// db.c
int expireIfNeeded(redisDb *db, robj *key) {
    // 如果没过期,什么都不做,直接返回
    if (!keyIsExpired(db,key)) return 0;
    
    if (server.masterhost != NULL) return 1;
    
    if (checkClientPauseTimeoutAndReturnIfPaused()) return 1;
    
    // 删除过期键
    deleteExpiredKeyAndPropagate(db,key);
    return 1;
}

int keyIsExpired(redisDb *db, robj *key) {
    mstime_t when = getExpire(db,key);
    mstime_t now;
    // 如果该键没有设置过期时间
    if (when < 0) return 0; 
    
    // server加载过程中,不执行任何过期键删除操作
    if (server.loading) return 0;
    // 获取当前时间now
    
    if (server.lua_caller) {
        now = server.lua_time_snapshot;
    }
    
    else if (server.fixed_time_expire > 0) {
        now = server.mstime;
    }
    
    else {
        now = mstime();
    }
    
    // 如果当前时间大于过期时间,则该键过期,返回true
    return now > when;
}

// 从过期字典中获取key的过期时间
long long getExpire(redisDb *db, robj *key) {
    dictEntry *de;
    
    // dictSize = db对应的ht[0].used+ht[1].used
    // 在过期字典中找不到该key,则直接返回-1
    if (dictSize(db->expires) == 0 ||
       (de = dictFind(db->expires,key->ptr)) == NULL) return -1;
    
    serverAssertWithInfo(NULL,key,dictFind(db->dict,key->ptr) != NULL);
    // 如果找到了,返回键的unix时间戳
    return dictGetSignedIntegerVal(de);
}

2.3.2 定时删除的实现

惰性删除由src/db.c/activeExpireCycle函数实现.

#define ACTIVE_EXPIRE_CYCLE_KEYS_PER_LOOP 20  // 每个数据库默认检查20个key
#define ACTIVE_EXPIRE_CYCLE_FAST_DURATION 1000         // 每个数据库默认检查20个key
#define ACTIVE_EXPIRE_CYCLE_SLOW_TIME_PERC 25   // CPU最大使用率25%
#define ACTIVE_EXPIRE_CYCLE_ACCEPTABLE_STALE 10 
void activeExpireCycle(int type) {
    
    unsigned long
    effort = server.active_expire_effort-1, 
    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;
    
    static unsigned int current_db = 0; 
    static int timelimit_exit = 0;      
    static long long last_fast_cycle = 0; 
    int j, iteration = 0;
    int dbs_per_call = CRON_DBS_PER_CALL;  // 每次默认检查16个数据库
    long long start = ustime(), timelimit, elapsed;
    
    if (checkClientPauseTimeoutAndReturnIfPaused()) return;
    if (type == ACTIVE_EXPIRE_CYCLE_FAST) {
        
        if (!timelimit_exit &&
            server.stat_expired_stale_perc < config_cycle_acceptable_stale)
            return;
        if (start < last_fast_cycle + (long long)config_cycle_fast_duration*2)
            return;
        last_fast_cycle = start;
    }
    
    if (dbs_per_call > server.dbnum || timelimit_exit)
        dbs_per_call = server.dbnum;
    
    timelimit = config_cycle_slow_time_perc*1000000/server.hz/100;
    timelimit_exit = 0;
    if (timelimit <= 0) timelimit = 1;
    if (type == ACTIVE_EXPIRE_CYCLE_FAST)
        timelimit = config_cycle_fast_duration; 
    
    long total_sampled = 0;
    long total_expired = 0;
    // 遍历各个数据库
    for (j = 0; j < dbs_per_call && timelimit_exit == 0; j++) {
        
        unsigned long expired, sampled;
        // 获取当前要处理的数据库
        redisDb *db = server.db+(current_db % server.dbnum);
        
        current_db++;
        
        do {
            unsigned long num, slots;
            long long now, ttl_sum;
            int ttl_samples;
            iteration++;
            
            // 如果当前数据库过期字典为空,跳过这个数据库
            if ((num = dictSize(db->expires)) == 0) {
                db->avg_ttl = 0;
                break;
            }
            slots = dictSlots(db->expires);
            now = mstime();
            
            if (slots > DICT_HT_INITIAL_SIZE &&
                (num*100/slots < 1)) break;
            
            expired = 0;
            sampled = 0;
            ttl_sum = 0;
            ttl_samples = 0;
            if (num > config_keys_per_loop)
                num = config_keys_per_loop;
            
            long max_buckets = num*20;
            long checked_buckets = 0;
            while (sampled < num && checked_buckets < max_buckets) {
                for (int table = 0; table < 2; table++) {
                    if (table == 1 && !dictIsRehashing(db->expires)) break;
                    unsigned long idx = db->expires_cursor;
                    idx &= db->expires->ht[table].sizemask;
                    dictEntry *de = db->expires->ht[table].table[idx];
                    long long ttl;
                    
                    checked_buckets++;
                    while(de) {
                        
                        dictEntry *e = de;
                        de = de->next;
                        ttl = dictGetSignedIntegerVal(e)-now;
                        if (activeExpireCycleTryExpire(db,e,now)) expired++;
                        if (ttl > 0) {
                            
                            ttl_sum += ttl;
                            ttl_samples++;
                        }
                        sampled++;
                    }
                }
                db->expires_cursor++;
            }
            total_expired += expired;
            total_sampled += sampled;
            
            if (ttl_samples) {
                long long avg_ttl = ttl_sum/ttl_samples;
                
                if (db->avg_ttl == 0) db->avg_ttl = avg_ttl;
                db->avg_ttl = (db->avg_ttl/50)*49 + (avg_ttl/50);
            }
            
            if ((iteration & 0xf) == 0) { 
                elapsed = ustime()-start;
                if (elapsed > timelimit) {
                    timelimit_exit = 1;
                    server.stat_expired_time_cap_reached_count++;
                    break;
                }
            }
            
        } while (sampled == 0 ||
                 (expired*100/sampled) > config_cycle_acceptable_stale);
    }
    elapsed = ustime()-start;
    server.stat_expire_cycle_time_used += elapsed;
    latencyAddSampleIfNeeded("expire-cycle",elapsed/1000);
    
    double current_perc;
    if (total_sampled) {
        current_perc = (double)total_expired/total_sampled;
    } else
        current_perc = 0;
    server.stat_expired_stale_perc = (current_perc*0.05)+
                                     (server.stat_expired_stale_perc*0.95);
}

三、Redis内存淘汰策略

Redis为什么要有内存淘汰策略?因为Redis是内存数据库,不能无限大,达到阈值时需要淘汰部分内存的数据,来存储新的数据。

redis内存配置参数:maxmemory,一般设置为系统内存的一半(经验值),比如你的系统运行内存有哦96G,就设置为48G。

3.1 Redis针对过期key的淘汰策略

看你的业务是否使用了 expire 过期时间,如果使用了,则:

3.2 Redis最对所有key的淘汰策略

3.3 禁止淘汰策略

redis还有一种淘汰策略,就是禁止淘汰,这种策略,当redis使用的内存达到设定的最大值时,后续的写进redis的操作会失败。

四、增删改查图解

4.1 新增键值对

举例:我们在一个空的redis数据库中执行分别执行以下命令:

127.0.0.1:6379[1]> keys *
(empty array)  // 表示此时数据库中没有任何数据
127.0.0.1:6379[1]> set msg "hello world"
OK
127.0.0.1:6379[1]>

Redis过期键与内存淘汰策略深入分析讲解

 127.0.0.1:6379[1]> hmset student name panda age 20 addr beijing
OK
127.0.0.1:6379[1]>

Redis过期键与内存淘汰策略深入分析讲解

127.0.0.1:6379[1]> rpush teacher Darren Mark King
(integer) 3
127.0.0.1:6379[1]> 

Redis过期键与内存淘汰策略深入分析讲解

4.2 更新键值对

127.0.0.1:6379[1]> set msg "redis"
OK
127.0.0.1:6379[1]> get msg
"redis"
127.0.0.1:6379[1]> hset student sex male
(integer) 1
127.0.0.1:6379[1]>

Redis过期键与内存淘汰策略深入分析讲解

4.3 获取键的值

127.0.0.1:6379[1]> get msg
"redis"
127.0.0.1:6379[1]> hmget student name age addr sex
1) "panda"
2) "20"
3) "beijing"
4) "male"
127.0.0.1:6379[1]>

4.4 删除键值对

127.0.0.1:6379[1]> keys *
1) "msg"
2) "student"
3) "teacher"
127.0.0.1:6379[1]> del student
(integer) 1
127.0.0.1:6379[1]> keys *
1) "msg"
2) "teacher"
127.0.0.1:6379[1]>

Redis过期键与内存淘汰策略深入分析讲解

到此这篇关于Redis过期键与内存淘汰策略深入分析讲解的文章就介绍到这了,更多相关Redis过期键与内存淘汰策略内容请搜索我们以前的文章或继续浏览下面的相关文章希望大家以后多多支持我们!

阅读原文内容投诉

免责声明:

① 本站未注明“稿件来源”的信息均来自网络整理。其文字、图片和音视频稿件的所属权归原作者所有。本站收集整理出于非商业性的教育和科研之目的,并不意味着本站赞同其观点或证实其内容的真实性。仅作为临时的测试数据,供内部测试之用。本站并未授权任何人以任何方式主动获取本站任何信息。

② 本站未注明“稿件来源”的临时测试数据将在测试完成后最终做删除处理。有问题或投稿请发送至: 邮箱/279061341@qq.com QQ/279061341

软考中级精品资料免费领

  • 历年真题答案解析
  • 备考技巧名师总结
  • 高频考点精准押题
  • 2024年上半年信息系统项目管理师第二批次真题及答案解析(完整版)

    难度     813人已做
    查看
  • 【考后总结】2024年5月26日信息系统项目管理师第2批次考情分析

    难度     354人已做
    查看
  • 【考后总结】2024年5月25日信息系统项目管理师第1批次考情分析

    难度     318人已做
    查看
  • 2024年上半年软考高项第一、二批次真题考点汇总(完整版)

    难度     435人已做
    查看
  • 2024年上半年系统架构设计师考试综合知识真题

    难度     224人已做
    查看

相关文章

发现更多好内容

猜你喜欢

AI推送时光机
位置:首页-资讯-数据库
咦!没有更多了?去看看其它编程学习网 内容吧
首页课程
资料下载
问答资讯