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 < ) return ; /* No expire for this key */
  9.  
  10. /* Don't expire anything while loading. It will be done later. */
  11. // 如果服务器正在进行载入,那么不进行任何过期检查
  12. if (server.loading) return ;
  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 ;
  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 = ; /* Last DB tested. */
  6. static int timelimit_exit = ; /* Time limit hit in previous call? */
  7. static long long last_fast_cycle = ; /* When last fast cycle ran. */
  8.  
  9. unsigned int j, iteration = ;
  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*) 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 = *ACTIVE_EXPIRE_CYCLE_SLOW_TIME_PERC/server.hz/;
  52. timelimit_exit = ;
  53. if (timelimit <= ) timelimit = ;
  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 = ; 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)) == ) {
  85. db->avg_ttl = ;
  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*/slots < )) 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 = ;
  108. // 键的总 TTL 计数器
  109. ttl_sum = ;
  110. // 总共处理的键计数器
  111. ttl_samples = ;
  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 < ) ttl = ;
  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 == ) 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)/;
  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) == && /* check once every 16 iterations. */
  159. (ustime()-start) > timelimit)
  160. {
  161. // 如果遍历次数正好是 16 的倍数
  162. // 并且遍历的时间超过了 timelimit
  163. // 那么断开 timelimit_exit
  164. timelimit_exit = ;
  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/);
  175. }
  176. }

activeExpireCycle

  

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

  

从零开始学习redis源码的更多相关文章

  1. 一起学习redis源码

    redis的一些介绍,麻烦阅读前面的几篇文章,想对redis的详细实现有所了解,强力推荐<redis设计与实现>(不仅仅从作者那儿学习到redis的实现,还有项目的管理.思想等,作者可能比 ...

  2. __sync_fetch_and_add函数(Redis源码学习)

    __sync_fetch_and_add函数(Redis源码学习) 在学习redis-3.0源码中的sds文件时,看到里面有如下的C代码,之前从未接触过,所以为了全面学习redis源码,追根溯源,学习 ...

  3. Redis源码研究--字典

    计划每天花1小时学习Redis 源码.在博客上做个记录. --------6月18日----------- redis的字典dict主要涉及几个数据结构, dictEntry:具体的k-v链表结点 d ...

  4. Redis源码学习:字符串

    Redis源码学习:字符串 1.初识SDS 1.1 SDS定义 Redis定义了一个叫做sdshdr(SDS or simple dynamic string)的数据结构.SDS不仅用于 保存字符串, ...

  5. Redis源码学习:Lua脚本

    Redis源码学习:Lua脚本 1.Sublime Text配置 我是在Win7下,用Sublime Text + Cygwin开发的,配置方法请参考<Sublime Text 3下C/C++开 ...

  6. redis源码学习之工作流程初探

    目录 背景 环境准备 下载redis源码 下载Visual Studio Visual Studio打开redis源码 启动过程分析 调用关系图 事件循环分析 工作模型 代码分析 动画演示 网络模块 ...

  7. redis源码学习之slowlog

    目录 背景 环境说明 redis执行命令流程 记录slowlog源码分析 制造一条slowlog slowlog分析 1.slowlog如何开启 2.slowlog数量限制 3.slowlog中的耗时 ...

  8. redis源码学习之lua执行原理

    聊聊redis执行lua原理 从一次面试场景说起   "看你简历上写的精通redis" "额,还可以啦" "那你说说redis执行lua脚本的原理&q ...

  9. 柔性数组(Redis源码学习)

    柔性数组(Redis源码学习) 1. 问题背景 在阅读Redis源码中的字符串有如下结构,在sizeof(struct sdshdr)得到结果为8,在后续内存申请和计算中也用到.其实在工作中有遇到过这 ...

随机推荐

  1. 「CH2401」送礼物 解题报告

    CH2401 送礼物 描述 作为惩罚,GY被遣送去帮助某神牛给女生送礼物(GY:貌似是个好差事)但是在GY看到礼物之后,他就不这么认为了.某神牛有N个礼物,且异常沉重,但是GY的力气也异常的大(-_- ...

  2. 东拼西凑完成一个“前端框架”(5) - Tabs操作

    目录 东拼西凑完成一个后台 "前端框架" (1) - 布局 东拼西凑完成一个后台 "前端框架" (2) - 字体图标 东拼西凑完成一个"前端框架&qu ...

  3. JAVA8学习——Stream底层的实现(学习过程)

    Stream底层的实现 Stream接口实现了 BaseStream 接口,我们先来看看BaseStream的定义 BaseStream BaseStream是所有流的父类接口. 对JavaDoc做一 ...

  4. [推荐]icheck-bootstrap(漂亮的ckeckbox/radiobox)

    适用于Twitter Bootstrap框架的纯CSS样式的复选框/单选框按钮的插件. GitHub:https://github.com/bantikyan/icheck-bootstrap 如果你 ...

  5. 简单聊一聊JS中的循环引用及问题

    本文主要从 JS 中为什么会出现循环引用,垃圾回收策略中引用计数为什么有很大的问题,以及循环引用时的对象在使用 JSON.stringify 时为什么会报错,怎样解决这个问题简单谈谈自己的一些理解. ...

  6. JWT (一):认识 JSON Web Token

    JWT(一):认识 JSON WebToken JWT(二):使用 Java 实现 JWT 什么是 JWT? JSON Web Token(JWT)是一种开放标准(RFC 7519),它定义了一种紧凑 ...

  7. Spring Boot2 系列教程 (十六) | 整合 WebSocket 实现广播

    前言 如题,今天介绍的是 SpringBoot 整合 WebSocket 实现广播消息. 什么是 WebSocket ? WebSocket 为浏览器和服务器提供了双工异步通信的功能,即浏览器可以向服 ...

  8. vPlayer 模块Demo

    本文出自APICloud官方论坛 vPlayer iOS封装了AVPlayer视频播放功能(支持音频播放).iOS 平台上支持的视频文件格式有:WMV,AVI,MKV,RMVB,RM,XVID,MP4 ...

  9. 求1-n 中与 m 互质的素因子 (容斥原理)

    ll prime[100]; ll cnt; void getprime(){ cnt = 0; ll num = m; for(ll i = 2; i*i <= m; i++){ // sqr ...

  10. 如何编写Robot Framework测试用例2---(测试用例语法1)

    基本语法 测试用例由关键字组成,关键字的来源有三种: 1从测试库引入:2从资源文件引入:3从关键字表中引入(自定义关键字) 下面就是一个典型的测试用例组织形式. 图中有2个测试用例“Valid Log ...