從零開始學習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
&n