从零开始学习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函数对输入键进行检查。代码如下:
- int expireIfNeeded(redisDb *db, robj *key) {
- // 取出键的过期时间
- mstime_t when = getExpire(db,key);
- mstime_t now;
- // 没有过期时间
- if (when < ) return ; /* No expire for this key */
- /* Don't expire anything while loading. It will be done later. */
- // 如果服务器正在进行载入,那么不进行任何过期检查
- if (server.loading) return ;
- /* If we are in the context of a Lua script, we claim that time is
- * blocked to when the Lua script started. This way a key can expire
- * only the first time it is accessed and not in the middle of the
- * script execution, making propagation to slaves / AOF consistent.
- * See issue #1525 on Github for more information. */
- now = server.lua_caller ? server.lua_time_start : mstime();
- /* If we are running in the context of a slave, return ASAP:
- * the slave key expiration is controlled by the master that will
- * send us synthesized DEL operations for expired keys.
- *
- * Still we try to return the right information to the caller,
- * that is, 0 if we think the key should be still valid, 1 if
- * we think the key is expired at this time. */
- // 当服务器运行在 replication 模式时
- // 附属节点并不主动删除 key
- // 它只返回一个逻辑上正确的返回值
- // 真正的删除操作要等待主节点发来删除命令时才执行
- // 从而保证数据的同步
- if (server.masterhost != NULL) return now > when;
- // 运行到这里,表示键带有过期时间,并且服务器为主节点
- /* Return when this key has not expired */
- // 如果未过期,返回 0
- if (now <= when) return ;
- /* Delete the key */
- server.stat_expiredkeys++;
- // 向 AOF 文件和附属节点传播过期信息
- propagateExpire(db,key);
- // 发送事件通知
- notifyKeyspaceEvent(REDIS_NOTIFY_EXPIRED,
- "expired",key,db->id);
- // 将过期键从数据库中删除
- return dbDelete(db,key);
- }
expireIfNeeded
代码逻辑:
如果已经过期,将键从数据库删除
如果键未过期,不做操作
定期删除策略的实现
过期删除策略由redis.c/activeExpireCycle函数实现,每当redis服务器周期性操作redis.c/serverCron函数执行时,activeExpireCycle就会被调用。
- void activeExpireCycle(int type) {
- /* This function has some global state in order to continue the work
- * incrementally across calls. */
- // 静态变量,用来累积函数连续执行时的数据
- static unsigned int current_db = ; /* Last DB tested. */
- static int timelimit_exit = ; /* Time limit hit in previous call? */
- static long long last_fast_cycle = ; /* When last fast cycle ran. */
- unsigned int j, iteration = ;
- // 默认每次处理的数据库数量
- unsigned int dbs_per_call = REDIS_DBCRON_DBS_PER_CALL;
- // 函数开始的时间
- long long start = ustime(), timelimit;
- // 快速模式
- if (type == ACTIVE_EXPIRE_CYCLE_FAST) {
- /* Don't start a fast cycle if the previous cycle did not exited
- * for time limt. Also don't repeat a fast cycle for the same period
- * as the fast cycle total duration itself. */
- // 如果上次函数没有触发 timelimit_exit ,那么不执行处理
- if (!timelimit_exit) return;
- // 如果距离上次执行未够一定时间,那么不执行处理
- if (start < last_fast_cycle + ACTIVE_EXPIRE_CYCLE_FAST_DURATION*) return;
- // 运行到这里,说明执行快速处理,记录当前时间
- last_fast_cycle = start;
- }
- /* We usually should test REDIS_DBCRON_DBS_PER_CALL per iteration, with
- * two exceptions:
- *
- * 一般情况下,函数只处理 REDIS_DBCRON_DBS_PER_CALL 个数据库,
- * 除非:
- *
- * 1) Don't test more DBs than we have.
- * 当前数据库的数量小于 REDIS_DBCRON_DBS_PER_CALL
- * 2) If last time we hit the time limit, we want to scan all DBs
- * in this iteration, as there is work to do in some DB and we don't want
- * expired keys to use memory for too much time.
- * 如果上次处理遇到了时间上限,那么这次需要对所有数据库进行扫描,
- * 这可以避免过多的过期键占用空间
- */
- if (dbs_per_call > server.dbnum || timelimit_exit)
- dbs_per_call = server.dbnum;
- /* We can use at max ACTIVE_EXPIRE_CYCLE_SLOW_TIME_PERC percentage of CPU time
- * per iteration. Since this function gets called with a frequency of
- * server.hz times per second, the following is the max amount of
- * microseconds we can spend in this function. */
- // 函数处理的微秒时间上限
- // ACTIVE_EXPIRE_CYCLE_SLOW_TIME_PERC 默认为 25 ,也即是 25 % 的 CPU 时间
- timelimit = *ACTIVE_EXPIRE_CYCLE_SLOW_TIME_PERC/server.hz/;
- timelimit_exit = ;
- if (timelimit <= ) timelimit = ;
- // 如果是运行在快速模式之下
- // 那么最多只能运行 FAST_DURATION 微秒
- // 默认值为 1000 (微秒)
- if (type == ACTIVE_EXPIRE_CYCLE_FAST)
- timelimit = ACTIVE_EXPIRE_CYCLE_FAST_DURATION; /* in microseconds. */
- // 遍历数据库
- for (j = ; j < dbs_per_call; j++) {
- int expired;
- // 指向要处理的数据库
- redisDb *db = server.db+(current_db % server.dbnum);
- /* Increment the DB now so we are sure if we run out of time
- * in the current DB we'll restart from the next. This allows to
- * distribute the time evenly across DBs. */
- // 为 DB 计数器加一,如果进入 do 循环之后因为超时而跳出
- // 那么下次会直接从下个 DB 开始处理
- 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;
- /* If there is nothing to expire try next DB ASAP. */
- // 获取数据库中带过期时间的键的数量
- // 如果该数量为 0 ,直接跳过这个数据库
- if ((num = dictSize(db->expires)) == ) {
- db->avg_ttl = ;
- break;
- }
- // 获取数据库中键值对的数量
- slots = dictSlots(db->expires);
- // 当前时间
- now = mstime();
- /* When there are less than 1% filled slots getting random
- * keys is expensive, so stop here waiting for better times...
- * The dictionary will be resized asap. */
- // 这个数据库的使用率低于 1% ,扫描起来太费力了(大部分都会 MISS)
- // 跳过,等待字典收缩程序运行
- if (num && slots > DICT_HT_INITIAL_SIZE &&
- (num*/slots < )) break;
- /* The main collection cycle. Sample random keys among keys
- * with an expire set, checking for expired ones.
- *
- * 样本计数器
- */
- // 已处理过期键计数器
- expired = ;
- // 键的总 TTL 计数器
- ttl_sum = ;
- // 总共处理的键计数器
- ttl_samples = ;
- // 每次最多只能检查 LOOKUPS_PER_LOOP 个键
- if (num > ACTIVE_EXPIRE_CYCLE_LOOKUPS_PER_LOOP)
- num = ACTIVE_EXPIRE_CYCLE_LOOKUPS_PER_LOOP;
- // 开始遍历数据库
- while (num--) {
- dictEntry *de;
- long long ttl;
- // 从 expires 中随机取出一个带过期时间的键
- if ((de = dictGetRandomKey(db->expires)) == NULL) break;
- // 计算 TTL
- ttl = dictGetSignedIntegerVal(de)-now;
- // 如果键已经过期,那么删除它,并将 expired 计数器增一
- if (activeExpireCycleTryExpire(db,de,now)) expired++;
- if (ttl < ) ttl = ;
- // 累积键的 TTL
- ttl_sum += ttl;
- // 累积处理键的个数
- ttl_samples++;
- }
- /* Update the average TTL stats for this database. */
- // 为这个数据库更新平均 TTL 统计数据
- if (ttl_samples) {
- // 计算当前平均值
- long long avg_ttl = ttl_sum/ttl_samples;
- // 如果这是第一次设置数据库平均 TTL ,那么进行初始化
- if (db->avg_ttl == ) db->avg_ttl = avg_ttl;
- /* Smooth the value averaging with the previous one. */
- // 取数据库的上次平均 TTL 和今次平均 TTL 的平均值
- db->avg_ttl = (db->avg_ttl+avg_ttl)/;
- }
- /* We can't block forever here even if there are many keys to
- * expire. So after a given amount of milliseconds return to the
- * caller waiting for the other active expire cycle. */
- // 我们不能用太长时间处理过期键,
- // 所以这个函数执行一定时间之后就要返回
- // 更新遍历次数
- iteration++;
- // 每遍历 16 次执行一次
- if ((iteration & 0xf) == && /* check once every 16 iterations. */
- (ustime()-start) > timelimit)
- {
- // 如果遍历次数正好是 16 的倍数
- // 并且遍历的时间超过了 timelimit
- // 那么断开 timelimit_exit
- timelimit_exit = ;
- }
- // 已经超时了,返回
- if (timelimit_exit) return;
- /* We don't repeat the cycle if there are less than 25% of keys
- * found expired in the current DB. */
- // 如果已删除的过期键占当前总数据库带过期时间的键数量的 25 %
- // 那么不再遍历
- } while (expired > ACTIVE_EXPIRE_CYCLE_LOOKUPS_PER_LOOP/);
- }
- }
activeExpireCycle
附带有注释的源码:https://github.com/ldw0215/redis-3.0-annotated
从零开始学习redis源码的更多相关文章
- 一起学习redis源码
redis的一些介绍,麻烦阅读前面的几篇文章,想对redis的详细实现有所了解,强力推荐<redis设计与实现>(不仅仅从作者那儿学习到redis的实现,还有项目的管理.思想等,作者可能比 ...
- __sync_fetch_and_add函数(Redis源码学习)
__sync_fetch_and_add函数(Redis源码学习) 在学习redis-3.0源码中的sds文件时,看到里面有如下的C代码,之前从未接触过,所以为了全面学习redis源码,追根溯源,学习 ...
- Redis源码研究--字典
计划每天花1小时学习Redis 源码.在博客上做个记录. --------6月18日----------- redis的字典dict主要涉及几个数据结构, dictEntry:具体的k-v链表结点 d ...
- Redis源码学习:字符串
Redis源码学习:字符串 1.初识SDS 1.1 SDS定义 Redis定义了一个叫做sdshdr(SDS or simple dynamic string)的数据结构.SDS不仅用于 保存字符串, ...
- Redis源码学习:Lua脚本
Redis源码学习:Lua脚本 1.Sublime Text配置 我是在Win7下,用Sublime Text + Cygwin开发的,配置方法请参考<Sublime Text 3下C/C++开 ...
- redis源码学习之工作流程初探
目录 背景 环境准备 下载redis源码 下载Visual Studio Visual Studio打开redis源码 启动过程分析 调用关系图 事件循环分析 工作模型 代码分析 动画演示 网络模块 ...
- redis源码学习之slowlog
目录 背景 环境说明 redis执行命令流程 记录slowlog源码分析 制造一条slowlog slowlog分析 1.slowlog如何开启 2.slowlog数量限制 3.slowlog中的耗时 ...
- redis源码学习之lua执行原理
聊聊redis执行lua原理 从一次面试场景说起 "看你简历上写的精通redis" "额,还可以啦" "那你说说redis执行lua脚本的原理&q ...
- 柔性数组(Redis源码学习)
柔性数组(Redis源码学习) 1. 问题背景 在阅读Redis源码中的字符串有如下结构,在sizeof(struct sdshdr)得到结果为8,在后续内存申请和计算中也用到.其实在工作中有遇到过这 ...
随机推荐
- 「CH2401」送礼物 解题报告
CH2401 送礼物 描述 作为惩罚,GY被遣送去帮助某神牛给女生送礼物(GY:貌似是个好差事)但是在GY看到礼物之后,他就不这么认为了.某神牛有N个礼物,且异常沉重,但是GY的力气也异常的大(-_- ...
- 东拼西凑完成一个“前端框架”(5) - Tabs操作
目录 东拼西凑完成一个后台 "前端框架" (1) - 布局 东拼西凑完成一个后台 "前端框架" (2) - 字体图标 东拼西凑完成一个"前端框架&qu ...
- JAVA8学习——Stream底层的实现(学习过程)
Stream底层的实现 Stream接口实现了 BaseStream 接口,我们先来看看BaseStream的定义 BaseStream BaseStream是所有流的父类接口. 对JavaDoc做一 ...
- [推荐]icheck-bootstrap(漂亮的ckeckbox/radiobox)
适用于Twitter Bootstrap框架的纯CSS样式的复选框/单选框按钮的插件. GitHub:https://github.com/bantikyan/icheck-bootstrap 如果你 ...
- 简单聊一聊JS中的循环引用及问题
本文主要从 JS 中为什么会出现循环引用,垃圾回收策略中引用计数为什么有很大的问题,以及循环引用时的对象在使用 JSON.stringify 时为什么会报错,怎样解决这个问题简单谈谈自己的一些理解. ...
- JWT (一):认识 JSON Web Token
JWT(一):认识 JSON WebToken JWT(二):使用 Java 实现 JWT 什么是 JWT? JSON Web Token(JWT)是一种开放标准(RFC 7519),它定义了一种紧凑 ...
- Spring Boot2 系列教程 (十六) | 整合 WebSocket 实现广播
前言 如题,今天介绍的是 SpringBoot 整合 WebSocket 实现广播消息. 什么是 WebSocket ? WebSocket 为浏览器和服务器提供了双工异步通信的功能,即浏览器可以向服 ...
- vPlayer 模块Demo
本文出自APICloud官方论坛 vPlayer iOS封装了AVPlayer视频播放功能(支持音频播放).iOS 平台上支持的视频文件格式有:WMV,AVI,MKV,RMVB,RM,XVID,MP4 ...
- 求1-n 中与 m 互质的素因子 (容斥原理)
ll prime[100]; ll cnt; void getprime(){ cnt = 0; ll num = m; for(ll i = 2; i*i <= m; i++){ // sqr ...
- 如何编写Robot Framework测试用例2---(测试用例语法1)
基本语法 测试用例由关键字组成,关键字的来源有三种: 1从测试库引入:2从资源文件引入:3从关键字表中引入(自定义关键字) 下面就是一个典型的测试用例组织形式. 图中有2个测试用例“Valid Log ...