zoukankan      html  css  js  c++  java
  • 从零开始学习redis源码

      2020的开年是比较艰难的,爆发了肺炎疫情,希望大家多注意安全,也希望疫情早日好转!

      以3.2版本的源码为例,开始讲解,有时会贴出源码,进行说明,并会注明源码出处。

      数据库

      应该都知道默认redis会有16个库,是根据配置文件来的,可以通过select命令来切换数据库。那原理又是如何实现的么?

      redis服务器将所有数据库都保存在服务器状态redis.h/redisServer结构的db数据中,db数组的每一项都是一个redis.h/redisDb结构,每个redisDb

    代表一个数据库;

      结构如下,这个结构大约有500行,不能全部贴出来了!

      struct redisServer {

        /* General */

        // 配置文件的绝对路径
        char *configfile; /* Absolute config file path, or NULL */

        // serverCron() 每秒调用的次数
        int hz; /* serverCron() calls frequency in hertz */

        // 数据库
        redisDb *db;

        ...

        //服务器的数据库数量

         int dbnum;                      /* Total number of configured DBs */

      };

      在服务器内部,客户端状态reidsClient结构的db属性记录了客户端当前的目标数据库:

      

      typedef struct redisClient {

        // 套接字描述符
        int fd;

        // 当前正在使用的数据库
        redisDb *db;

        ...

      }redisClient;

      数据库键空间

      redis是是一个键值对(kv)数据库服务器,如下:

      

      typedef struct redisDb {

        // 数据库键空间,保存着数据库中的所有键值对
        dict *dict;

        ...

      }redisDb;

      设置键的生存时间和过期时间

      通过expire和pexpire命令,客户端可以用过秒或毫秒设置生存时间(TTL,Time To Live);还有类似的expireat或pexpireat命令。

      有以上四个命令设置TTL,expire、pexpire和pexpireat三个命令都是通过pexpireat来实现的。

      过期键删除策略

      有三种不同的删除策略:

      定时删除:在设置键的过期时间同时,创建一个定时器,让键的过期时间来临时,立即执行对键的删除操作。

      惰性删除:放任键过期不管,但是每次从键空间获取键时,都检查取得的键是否过期,如果过期的话,就删除该键

      定期删除:每隔一段时间,检查一次,删除过期的键

      redis服务器使用的是惰性删除和定期删除策略,

      惰性删除策略的实现

      惰性删除策略由db.c/expireIfNeeded函数实现的,所有读写数据库的Redis命令在执行之前都会调用expireIfNeeded函数对输入键进行检查。代码如下:

      

     1 int expireIfNeeded(redisDb *db, robj *key) {
     2 
     3     // 取出键的过期时间
     4     mstime_t when = getExpire(db,key);
     5     mstime_t now;
     6 
     7     // 没有过期时间
     8     if (when < 0) return 0; /* No expire for this key */
     9 
    10     /* Don't expire anything while loading. It will be done later. */
    11     // 如果服务器正在进行载入,那么不进行任何过期检查
    12     if (server.loading) return 0;
    13 
    14     /* If we are in the context of a Lua script, we claim that time is
    15      * blocked to when the Lua script started. This way a key can expire
    16      * only the first time it is accessed and not in the middle of the
    17      * script execution, making propagation to slaves / AOF consistent.
    18      * See issue #1525 on Github for more information. */
    19     now = server.lua_caller ? server.lua_time_start : mstime();
    20 
    21     /* If we are running in the context of a slave, return ASAP:
    22      * the slave key expiration is controlled by the master that will
    23      * send us synthesized DEL operations for expired keys.
    24      *
    25      * Still we try to return the right information to the caller, 
    26      * that is, 0 if we think the key should be still valid, 1 if
    27      * we think the key is expired at this time. */
    28     // 当服务器运行在 replication 模式时
    29     // 附属节点并不主动删除 key
    30     // 它只返回一个逻辑上正确的返回值
    31     // 真正的删除操作要等待主节点发来删除命令时才执行
    32     // 从而保证数据的同步
    33     if (server.masterhost != NULL) return now > when;
    34 
    35     // 运行到这里,表示键带有过期时间,并且服务器为主节点
    36 
    37     /* Return when this key has not expired */
    38     // 如果未过期,返回 0
    39     if (now <= when) return 0;
    40 
    41     /* Delete the key */
    42     server.stat_expiredkeys++;
    43 
    44     // 向 AOF 文件和附属节点传播过期信息
    45     propagateExpire(db,key);
    46 
    47     // 发送事件通知
    48     notifyKeyspaceEvent(REDIS_NOTIFY_EXPIRED,
    49         "expired",key,db->id);
    50 
    51     // 将过期键从数据库中删除
    52     return dbDelete(db,key);
    53 }
    expireIfNeeded

      代码逻辑:

      如果已经过期,将键从数据库删除

      如果键未过期,不做操作

      定期删除策略的实现

      过期删除策略由redis.c/activeExpireCycle函数实现,每当redis服务器周期性操作redis.c/serverCron函数执行时,activeExpireCycle就会被调用。

      

      1 void activeExpireCycle(int type) {
      2     /* This function has some global state in order to continue the work
      3      * incrementally across calls. */
      4     // 静态变量,用来累积函数连续执行时的数据
      5     static unsigned int current_db = 0; /* Last DB tested. */
      6     static int timelimit_exit = 0;      /* Time limit hit in previous call? */
      7     static long long last_fast_cycle = 0; /* When last fast cycle ran. */
      8 
      9     unsigned int j, iteration = 0;
     10     // 默认每次处理的数据库数量
     11     unsigned int dbs_per_call = REDIS_DBCRON_DBS_PER_CALL;
     12     // 函数开始的时间
     13     long long start = ustime(), timelimit;
     14 
     15     // 快速模式
     16     if (type == ACTIVE_EXPIRE_CYCLE_FAST) {
     17         /* Don't start a fast cycle if the previous cycle did not exited
     18          * for time limt. Also don't repeat a fast cycle for the same period
     19          * as the fast cycle total duration itself. */
     20         // 如果上次函数没有触发 timelimit_exit ,那么不执行处理
     21         if (!timelimit_exit) return;
     22         // 如果距离上次执行未够一定时间,那么不执行处理
     23         if (start < last_fast_cycle + ACTIVE_EXPIRE_CYCLE_FAST_DURATION*2) return;
     24         // 运行到这里,说明执行快速处理,记录当前时间
     25         last_fast_cycle = start;
     26     }
     27 
     28     /* We usually should test REDIS_DBCRON_DBS_PER_CALL per iteration, with
     29      * two exceptions:
     30      *
     31      * 一般情况下,函数只处理 REDIS_DBCRON_DBS_PER_CALL 个数据库,
     32      * 除非:
     33      *
     34      * 1) Don't test more DBs than we have.
     35      *    当前数据库的数量小于 REDIS_DBCRON_DBS_PER_CALL
     36      * 2) If last time we hit the time limit, we want to scan all DBs
     37      * in this iteration, as there is work to do in some DB and we don't want
     38      * expired keys to use memory for too much time. 
     39      *     如果上次处理遇到了时间上限,那么这次需要对所有数据库进行扫描,
     40      *     这可以避免过多的过期键占用空间
     41      */
     42     if (dbs_per_call > server.dbnum || timelimit_exit)
     43         dbs_per_call = server.dbnum;
     44 
     45     /* We can use at max ACTIVE_EXPIRE_CYCLE_SLOW_TIME_PERC percentage of CPU time
     46      * per iteration. Since this function gets called with a frequency of
     47      * server.hz times per second, the following is the max amount of
     48      * microseconds we can spend in this function. */
     49     // 函数处理的微秒时间上限
     50     // ACTIVE_EXPIRE_CYCLE_SLOW_TIME_PERC 默认为 25 ,也即是 25 % 的 CPU 时间
     51     timelimit = 1000000*ACTIVE_EXPIRE_CYCLE_SLOW_TIME_PERC/server.hz/100;
     52     timelimit_exit = 0;
     53     if (timelimit <= 0) timelimit = 1;
     54 
     55     // 如果是运行在快速模式之下
     56     // 那么最多只能运行 FAST_DURATION 微秒 
     57     // 默认值为 1000 (微秒)
     58     if (type == ACTIVE_EXPIRE_CYCLE_FAST)
     59         timelimit = ACTIVE_EXPIRE_CYCLE_FAST_DURATION; /* in microseconds. */
     60 
     61     // 遍历数据库
     62     for (j = 0; j < dbs_per_call; j++) {
     63         int expired;
     64         // 指向要处理的数据库
     65         redisDb *db = server.db+(current_db % server.dbnum);
     66 
     67         /* Increment the DB now so we are sure if we run out of time
     68          * in the current DB we'll restart from the next. This allows to
     69          * distribute the time evenly across DBs. */
     70         // 为 DB 计数器加一,如果进入 do 循环之后因为超时而跳出
     71         // 那么下次会直接从下个 DB 开始处理
     72         current_db++;
     73 
     74         /* Continue to expire if at the end of the cycle more than 25%
     75          * of the keys were expired. */
     76         do {
     77             unsigned long num, slots;
     78             long long now, ttl_sum;
     79             int ttl_samples;
     80 
     81             /* If there is nothing to expire try next DB ASAP. */
     82             // 获取数据库中带过期时间的键的数量
     83             // 如果该数量为 0 ,直接跳过这个数据库
     84             if ((num = dictSize(db->expires)) == 0) {
     85                 db->avg_ttl = 0;
     86                 break;
     87             }
     88             // 获取数据库中键值对的数量
     89             slots = dictSlots(db->expires);
     90             // 当前时间
     91             now = mstime();
     92 
     93             /* When there are less than 1% filled slots getting random
     94              * keys is expensive, so stop here waiting for better times...
     95              * The dictionary will be resized asap. */
     96             // 这个数据库的使用率低于 1% ,扫描起来太费力了(大部分都会 MISS)
     97             // 跳过,等待字典收缩程序运行
     98             if (num && slots > DICT_HT_INITIAL_SIZE &&
     99                 (num*100/slots < 1)) break;
    100 
    101             /* The main collection cycle. Sample random keys among keys
    102              * with an expire set, checking for expired ones. 
    103              *
    104              * 样本计数器
    105              */
    106             // 已处理过期键计数器
    107             expired = 0;
    108             // 键的总 TTL 计数器
    109             ttl_sum = 0;
    110             // 总共处理的键计数器
    111             ttl_samples = 0;
    112 
    113             // 每次最多只能检查 LOOKUPS_PER_LOOP 个键
    114             if (num > ACTIVE_EXPIRE_CYCLE_LOOKUPS_PER_LOOP)
    115                 num = ACTIVE_EXPIRE_CYCLE_LOOKUPS_PER_LOOP;
    116 
    117             // 开始遍历数据库
    118             while (num--) {
    119                 dictEntry *de;
    120                 long long ttl;
    121 
    122                 // 从 expires 中随机取出一个带过期时间的键
    123                 if ((de = dictGetRandomKey(db->expires)) == NULL) break;
    124                 // 计算 TTL
    125                 ttl = dictGetSignedIntegerVal(de)-now;
    126                 // 如果键已经过期,那么删除它,并将 expired 计数器增一
    127                 if (activeExpireCycleTryExpire(db,de,now)) expired++;
    128                 if (ttl < 0) ttl = 0;
    129                 // 累积键的 TTL
    130                 ttl_sum += ttl;
    131                 // 累积处理键的个数
    132                 ttl_samples++;
    133             }
    134 
    135             /* Update the average TTL stats for this database. */
    136             // 为这个数据库更新平均 TTL 统计数据
    137             if (ttl_samples) {
    138                 // 计算当前平均值
    139                 long long avg_ttl = ttl_sum/ttl_samples;
    140                 
    141                 // 如果这是第一次设置数据库平均 TTL ,那么进行初始化
    142                 if (db->avg_ttl == 0) db->avg_ttl = avg_ttl;
    143                 /* Smooth the value averaging with the previous one. */
    144                 // 取数据库的上次平均 TTL 和今次平均 TTL 的平均值
    145                 db->avg_ttl = (db->avg_ttl+avg_ttl)/2;
    146             }
    147 
    148             /* We can't block forever here even if there are many keys to
    149              * expire. So after a given amount of milliseconds return to the
    150              * caller waiting for the other active expire cycle. */
    151             // 我们不能用太长时间处理过期键,
    152             // 所以这个函数执行一定时间之后就要返回
    153 
    154             // 更新遍历次数
    155             iteration++;
    156 
    157             // 每遍历 16 次执行一次
    158             if ((iteration & 0xf) == 0 && /* check once every 16 iterations. */
    159                 (ustime()-start) > timelimit)
    160             {
    161                 // 如果遍历次数正好是 16 的倍数
    162                 // 并且遍历的时间超过了 timelimit
    163                 // 那么断开 timelimit_exit
    164                 timelimit_exit = 1;
    165             }
    166 
    167             // 已经超时了,返回
    168             if (timelimit_exit) return;
    169 
    170             /* We don't repeat the cycle if there are less than 25% of keys
    171              * found expired in the current DB. */
    172             // 如果已删除的过期键占当前总数据库带过期时间的键数量的 25 %
    173             // 那么不再遍历
    174         } while (expired > ACTIVE_EXPIRE_CYCLE_LOOKUPS_PER_LOOP/4);
    175     }
    176 }
    activeExpireCycle

      

      附带有注释的源码:https://github.com/ldw0215/redis-3.0-annotated

      

  • 相关阅读:
    C#中正则表达式的分组构造
    CompressionModule压缩模块
    BS程序代码与安全与基本攻击/防御模式
    mssql 性能优化的一些认识
    sql语句基础
    利用.net2.0中的GZip或Deflate压缩文件
    一个完整的ID生成器,并极大地降低了并发重复的概率,转换成十六进制 时间戳
    IE中的条件注释表
    DevExpress的10个使用技巧
    几种检查调试CSS布局的有效方法
  • 原文地址:https://www.cnblogs.com/liudw-0215/p/12312423.html
Copyright © 2011-2022 走看看