zoukankan      html  css  js  c++  java
  • redis源码学习之服务器数据库

    参考《Redis 设计与实现》 (基于redis3.0.0) 作者:黄健宏
    学习redis3.2.13

    toc

    数据库创建

    redis服务器的数据库保存在服务器状态结构server.h/redisServer的db数组中,数据库的个数由redisServer的dbnum决定

    struct redisServer {
        /* General */
        //...
        redisDb *db;
        //...
        /* Configuration */
        int dbnum;                      /* Total number of configured DBs */
        //...

    在函数initServer中,redisServer.db数组被创建

    void initServer(void) {
        //略
        server.db = zmalloc(sizeof(redisDb)*server.dbnum);
        //略
    }

    server.dbnum在函数initServerConfig中被赋值

    server.dbnum = CONFIG_DEFAULT_DBNUM;    //#define CONFIG_DEFAULT_DBNUM     16

    数据库切换

    每个redis客户端都有属于自己操作的数据库,默认情况下,0号数据库是默认目标数据库,可以通过select命令来切换目标数据库,使用方式为select index,index为数据库下标

    数据库键空间与过期字典

    数据库中键空间与超时结构定义如下:

    typedef struct redisDb {
        dict *dict;                 /* The keyspace for this DB */
        dict *expires;              /* Timeout of keys with a timeout set */
        //...
    } redisDb;

    键空间

    redisDb中的成员dict被称为数据库的键空间,它是一个字典,保存了该数据库的键值对,键是字符串对象,值可以是字符串对象、列表对象、哈希对象、集合对象、有序集合对象中的一种。

    增删查改

    涉及到对数据库键值对的 增删改查时,都是在字典API以及存储的值对象API上操作

    读写时的维护操作

    1. 查找键后,根据是否找到对应的值来增加命中与错过的次数,在info stats命令中可以查询到两个次数
    robj *lookupKeyReadWithFlags(redisDb *db, robj *key, int flags) {
        robj *val;
        //检查key并在其过期时将其"删除"(服务器是主节点删除key并返回已删除,否则仅返回已删除)
        if (expireIfNeeded(db,key) == 1) {
            //key过期了,并且服务器是master,返回null表示key不存在
            if (server.masterhost == NULL) return NULL;
    
            //key过期了,服务器是slave,同时,调用者不是master,命令时只读的,返回null表示key不存在(返回null来保持与master的一致性视图)
            if (server.current_client &&
                server.current_client != server.master &&
                server.current_client->cmd &&
                server.current_client->cmd->flags & CMD_READONLY)
            {
                return NULL;
            }
        }
        //寻找key对应的val
        val = lookupKey(db,key,flags);
        if (val == NULL)
            server.stat_keyspace_misses++;
        else
            server.stat_keyspace_hits++;
        return val;
    }
    1. 在lookupKey某个key的val后,lookupKey函数内会根据实际情况更新val的lru时间(最后一次使用时间)
    robj *lookupKey(redisDb *db, robj *key, int flags) {
        dictEntry *de = dictFind(db->dict,key->ptr);
        if (de) {
            robj *val = dictGetVal(de);
    
            /* Update the access time for the ageing algorithm.
             * Don't do it if we have a saving child, as this will trigger
             * a copy on write madness. */
            //不存在持久化子进程并且lookup标志不是“不修改lru时间”则更新val的lru
            if (server.rdb_child_pid == -1 &&
                server.aof_child_pid == -1 &&
                !(flags & LOOKUP_NOTOUCH))
            {
                val->lru = LRU_CLOCK();
            }
            return val;
        } else {
            return NULL;
        }
    }

    过期字典

    过期字典是redisDb中的成员expires,它存储的是键空间中键与它的过期时间,过期字典中的键是一个指针,指向键空间中的某个键对象,过期字典中的值是long long整数,保存了键对应的过期时间(毫秒精度的unix时间戳)

    键的生存时间与过期时间

    相关命令

    名词 名词解释
    生存时间 键存活时间:经过制定的生存时间后,服务器会删除生存时间为0的键
    过期时间 unix时间戳,制定键被删除的时间点
    命令 命令解释
    expire key ttl 将键key的生存时间设置为ttl秒,成功返回1,key不存在或设置失败返回0
    pexpire key ttl 将键key的生存时间设置为ttl毫秒,成功返回1,key不存在或设置失败返回0
    expireat key timesramp 将key的过期时间设置为timrestamp(秒),成功返回1,key不存在或设置失败返回0
    pexpireat key timesramp 将key的过期时间设置为timrestamp(毫秒),成功返回1,key不存在或设置失败返回0
    ttl key 返回key在当前时间的剩余生存时间(秒),key不存在返回-2,没有过期时间返回-1
    pttl key 返回key在当前时间的剩余生存时间(毫秒),key不存在返回-2,没有过期时间返回-1
    persist key 去掉key的过期时间,成功返回1,key不存在或没有设置生存时间返回0

    过期键的判定

    1. 检查给定键是否存在于过期字典:如果存在,那么取得键的过期时间。
    2. 检查当前UNIX时间戳是否大于键的过期时间:如果是的话,那么键已经过期;否则的话,键未过期。

    过期键删除策略介绍

    三种删除策略

    1. 定时删除:在设置键的过期时间的同时,创建一个定时任务,当键达到过期时间时,立即执行对键的删除操作
    2. 惰性删除:放任键过期不管,但在每次从键空间获取键时,都检查取得的键是否过期,如果过期的话,就删除该键,如果没有过期,就返回该键
    3. 定期删除:每隔一点时间,程序就对数据库进行一次检查,删除里面的过期键,至于要删除多少过期键,以及要检查多少个数据库,则由算法决定

    三种删除策略的优缺点

    1. 定时删除
      优点:内存最友好:保证了尽快删除过期键,释放其占用的内存
      缺点:CPU最不友好:过期键较多时,删除操作会占用大量cpu时间,cpu时间紧张时触发删除任务则会影响服务器响应时间与吞吐量
      redis未使用此删除策略
    2. 惰性删除
      优点:CPU最友好:在从键空间访问查找指定key时,仅对当前key进行判断与删除
      缺点:内存最不友好:不访问到的的过期key永远得不到删除,占用的内存得不到释放
    3. 定期删除
      优点:是前两种策略整合和折中:
    4. 定期删除策略每隔一段时间执行一次删除过期键操作,并通过限制删除操作执行的时长和频率来减少删除操作对CPU时间的影响。
    5. 定时删除策略有效地减少了因为过期键带来的内存浪费。
      缺点:难以确定删除操作执行的时长与频率:
    6. 删除操作执行得太频繁,或者执行时间太长,定期删除策略就会退化成为定时删除策略
    7. 相反,则和惰性删除策略一样
      因此使用定期删除策略,需要根据服务器的情况合理地设置删除操作的执行时长和执行频率。

    redis过期键删除策略

    redis服务器选用了三种策略中的惰性删除和定期删除策略,两种策略配合使用,取得了CPU使用时间与避免内存浪费之间的平衡

    惰性删除策略的实现

    惰性删除策略由函数expireIfNeeded实现,所有读写数据库的redis命令都会在执行前,调用该函数对输入键进行过期检查操作,避免命令接触到过期键
    所有读写数据库的命令,都会在键空间中查找指定键,查找时,根据命令的操作类型,会调用到以下两种lookupkey之一,两种lookupkey中直接或间接调用了expireIfNeeded函数进行过期键检查

    robj *lookupKeyRead(redisDb *db, robj *key) {
        return lookupKeyReadWithFlags(db,key,LOOKUP_NONE);  //内部调用了expireIfNeeded,见上文 
    }
    
    robj *lookupKeyWrite(redisDb *db, robj *key) {
        expireIfNeeded(db,key);
        return lookupKey(db,key,LOOKUP_NONE);
    }

    expireIfNeeded函数实现如下:

    int expireIfNeeded(redisDb *db, robj *key) {
        mstime_t when = getExpire(db,key);  //取出key的过期时间
        mstime_t now;
    
        if (when < 0) return 0; //key没有被设置过期时间
    
        //载入时不进行过期检查
        if (server.loading) return 0;
    
        //得到当前时间,跑lua脚本时以脚本启动时间,否则使用系统时间
        now = server.lua_caller ? server.lua_time_start : mstime();
    
        //服务器是slave,不进行key的实际删除,返回逻辑上的过期与否状态(当master发过来删除命令才真正对键进行删      //除,以保持数据的一致), 返回 0 未过期  1已过期
        if (server.masterhost != NULL) return now > when;
    
        //服务器是master,但key没过期,不操作
        if (now <= when) return 0;
    
        //对过期key进行删除
        server.stat_expiredkeys++;
        propagateExpire(db,key);    //向slave及aof文件传播key过期消息(删除命令)
        notifyKeyspaceEvent(NOTIFY_EXPIRED,
            "expired",key,db->id);
        return dbDelete(db,key);
    }

    定期删除策略的实现

    过期键的定期删除策略由activeExpireCycle函数实现,此函数被进入事件循环前调用的函数beforeSleep,以及周期性执行的函数databasesCron调用,调用条件均为服务器是master,同样是因为slave的对键删除的取决于master的删除命令

    void databasesCron(void) {
        if (server.active_expire_enabled && server.masterhost == NULL)
            activeExpireCycle(ACTIVE_EXPIRE_CYCLE_SLOW);//
        //...
    }
    
    void beforeSleep(struct aeEventLoop *eventLoop) {
        // ...
        if (server.active_expire_enabled && server.masterhost == NULL)
            activeExpireCycle(ACTIVE_EXPIRE_CYCLE_FAST);//
           //...
    }

    activeExpireCycle函数增量式的对全部数据库进行检查,每次检查都有一个超时时间,在超时时间内会尽量检查多个数据库,对每个库按过期条件进行多次检查,每次抽取最多20个键进行检查,每1次进行超时判断,超时则退出,函数下次被调用时,从紧邻的下一个数据库开始新的检查,检查完最后一个后重新由第1个开始,如此循环

    void activeExpireCycle(int type) {
        //用于记录前次执行情况的静态局部变量,本次执行接上次后继续
        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;   //#define CRON_DBS_PER_CALL 16 
        long long start = ustime(), timelimit;
    
        //客户端暂停时不执行
        if (clientsArePaused()) return;
        //符合快速模式
        if (type == ACTIVE_EXPIRE_CYCLE_FAST) {
            if (!timelimit_exit) return;    //前次执行没有超时,本次不执行
            if (start < last_fast_cycle + ACTIVE_EXPIRE_CYCLE_FAST_DURATION*2) return;  //前次执行时间距本次执行间隔不够,本次不执行
            last_fast_cycle = start;    //符合快速模式执行条件,记录开始时间
        }
    
        //数据库个数小于CRON_DBS_PER_CALL或前次执行超时(前次超时,过期键过多),检查所有数据库,尽量减少过期键占用的空间
        if (dbs_per_call > server.dbnum || timelimit_exit)
            dbs_per_call = server.dbnum;
    
        //计算执行的时间上限(微妙)  ACTIVE_EXPIRE_CYCLE_SLOW_TIME_PERC为25   本函数的cpu执行时间占比  
        //server.hz是serverCron()函数每一秒的调用频率(本函数在serverCron的子函数中被调用,故可看做本函数的调用频率)
        timelimit = 1000000*ACTIVE_EXPIRE_CYCLE_SLOW_TIME_PERC/server.hz/100;
        timelimit_exit = 0;
        if (timelimit <= 0) timelimit = 1;
        //快速模式 的执行时间上限(微妙)
        if (type == ACTIVE_EXPIRE_CYCLE_FAST)
            timelimit = ACTIVE_EXPIRE_CYCLE_FAST_DURATION; //#define ACTIVE_EXPIRE_CYCLE_FAST_DURATION 1000
        //对多个数据库进行检查
        for (j = 0; j < dbs_per_call; j++) {
            int expired;
            redisDb *db = server.db+(current_db % server.dbnum);    
    
            //先增加下标来保证,即使本次activeExpireCycle()执行超时返回了,下次执行能从下个数据库开始,以在每个数据库上均匀分配时间
            current_db++;
    
            /* Continue to expire if at the end of the cycle more than 25%
             * of the keys were expired. */
            do {
                unsigned long num, slots;
                long long now, ttl_sum;
                int ttl_samples;
    
                //当前数据库,过期字典大小为0,跳过当前数据库,检查下一数据库
                if ((num = dictSize(db->expires)) == 0) {
                    db->avg_ttl = 0;
                    break;
                }
                slots = dictSlots(db->expires);
                now = mstime();
    
                //过期字典是空,同时过期字典发生过扩容,但过期字典的使用率负载因子小于0.1,跳过当前数据库,检查下一数据库
                //负载因子小于0.1时,dictGetRandomKey函数内部可能会循环太多次,代价太昂贵,跳过当前数据库,检查下一数据库
                if (num && slots > DICT_HT_INITIAL_SIZE &&
                    (num*100/slots < 1)) break;
    
                /* The main collection cycle. Sample random keys among keys
                 * with an expire set, checking for expired ones. */
                expired = 0;
                ttl_sum = 0;
                ttl_samples = 0;
                //每次最多检查20个过期键
                if (num > ACTIVE_EXPIRE_CYCLE_LOOKUPS_PER_LOOP) //#define ACTIVE_EXPIRE_CYCLE_LOOKUPS_PER_LOOP 20
                    num = ACTIVE_EXPIRE_CYCLE_LOOKUPS_PER_LOOP;
                //检查num个键的过期情况,并记录在expired
                while (num--) {
                    dictEntry *de;
                    long long ttl;
    
                    if ((de = dictGetRandomKey(db->expires)) == NULL) break;    //随机抽取一个设置了过期时间的键
                    ttl = dictGetSignedIntegerVal(de)-now;                      //计算生存时间
                    //函数内判断过期情况,并执行过期键的删除、传播过期消息(slaves、aof)、发出通知
                    if (activeExpireCycleTryExpire(db,de,now)) expired++;   //键如果过期累加过期键个数
                    if (ttl > 0) {
                        /* We want the average TTL of keys yet not expired. */
                        ttl_sum += ttl;
                        ttl_samples++;
                    }
                }
    
                /* Update the average TTL stats for this database. */
                if (ttl_samples) {
                    long long avg_ttl = ttl_sum/ttl_samples;
    
                    /* Do a simple running average with a few samples.
                     * We just use the current estimate with a weight of 2%
                     * and the previous estimate with a weight of 98%. */
                    if (db->avg_ttl == 0) db->avg_ttl = avg_ttl;
                    db->avg_ttl = (db->avg_ttl/50)*49 + (avg_ttl/50);
                }
    
                iteration++;    //遍历次数
                if ((iteration & 0xf) == 0) { //每遍历16次,进行一次超时检查
                    long long elapsed = ustime()-start;
    
                    latencyAddSampleIfNeeded("expire-cycle",elapsed/1000);
                    if (elapsed > timelimit) timelimit_exit = 1;
                }
                if (timelimit_exit) return; //超时则退出,等待activeExpireCycle函数下一次执行
    
                //过期键大于5个(最大随机抽取的25%),继续检查
            } while (expired > ACTIVE_EXPIRE_CYCLE_LOOKUPS_PER_LOOP/4);
        }
    }

    aof、rdb及复制功能对过期键的处理

    rdb文件对过期键的处理

    生成rdb文件
    创建新rdb文件时,数据库中的键会被检查,过期的键不会被存入新创建的rdb文件中
    载入rdb文件
    启动redis服务器时,在开启rdb功能的情况下,进行rdb文件载入:

    • 服务器是master,只载入未过期的键
    • 服务器是slave,载入rdb文件中的全部键,进行主从数据同步时,slave的数据库会被清空,所以载入了过期键也不会造成影响

    aof文件对过期键的处理

    aof文件写入
    过期键被删除后,会向aof文件追加del命令,显示记录该键被删除
    aof重写
    执行aof重写时,会对键进行检查,已过期的键不会被保存到重写后的aof文件中

    复制功能对过期键的处理

    当服务器运行在复制模式下时,slave的过期键删除动作由master控制(代码参见expireIfNeeded):

    • master因惰性或定期删除操作,删除一个过期键后,会显示地向所有slave发送del命令,告知slave删除该过期键
    • slave在执行客户端的读命令时,碰到过期键也不会删除,只会返回逻辑上的没有,客户端不会读到过期键(这是与3.0版本不同之处),只有当收到master发送的del命令,才删除过期键(见上文的函数lookupKeyRead、lookupKeyWrite调用链),

    由master控制slave统一删除,目的是保证主从数据的一致性





    原创不易,转载请注明出处,谢谢
  • 相关阅读:
    9.19题解
    9.18题解
    改码风
    找到了几个好的网站
    题目链接
    二分上机训练题解
    二分例题简单说明
    贪心上机训练题解
    贪心算法例题简单说明
    Johnson法则证明
  • 原文地址:https://www.cnblogs.com/Keeping-Fit/p/14439394.html
Copyright © 2011-2022 走看看