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

  

 


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM