从零开始学习redis源码
逆袭之路666 人气:02020的开年是比较艰难的,爆发了肺炎疫情,希望大家多注意安全,也希望疫情早日好转!
以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 }
代码逻辑:
如果已经过期,将键从数据库删除
如果键未过期,不做操作
定期删除策略的实现
过期删除策略由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 }
附带有注释的源码:https://github.com/ldw0215/redis-3.0-annotated
加载全部内容