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 }
代碼邏輯:
如果已經過期,將鍵從數據庫刪除
如果鍵未過期,不做操作
定期刪除策略的實現
過期刪除策略由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