原文地址:www.xilidou.com/2018/03/20/…
之前的文章講解了 Redis 的資料結構,這回就可以看看作為記憶體資料庫,Redis 是怎麼儲存資料的。以及鍵是怎麼過期的。
閱讀這篇文章你將會了解到:
- Redis 的資料庫實現
- Redis 鍵過期的策略
資料庫的實現
我們先看程式碼 server.h/redisServer
struct redisServer{
...
//儲存 db 的陣列
redisDb *db;
//db 的數量
int dbnum;
...
}
複製程式碼
再看redisDb的程式碼:
typedef struct redisDb {
dict *dict; /* The keyspace for this DB */
dict *expires; /* Timeout of keys with a timeout set */
dict *blocking_keys; /* Keys with clients waiting for data (BLPOP)*/
dict *ready_keys; /* Blocked keys that received a PUSH */
dict *watched_keys; /* WATCHED keys for MULTI/EXEC CAS */
int id; /* Database ID */
long long avg_ttl; /* Average TTL, just for stats */
} redisDb;
複製程式碼
總體來說redis的 server 包含若干個(預設16個) redisDb 資料庫。
Redis 是一個 k-v 儲存的鍵值對資料庫。其中字典 dict 儲存了資料庫中的所有鍵值對,這個地方叫做 keyspace
直譯過來就是“鍵空間”。
所以我們就可以這麼認為,在 redisDb 中我們使用 dict(字典)來維護鍵空間。
-
keyspace 的 kay 是資料庫的 key,每一個key 是一個字串物件。注意不是字串,而是字串物件。
-
keyspace 的 value 是資料庫的 value,這個 value 可以是 redis 的,字串物件,列表物件,雜湊表物件,集合物件或者有序物件中的一種。
資料庫讀寫操作
所以對於資料的增刪改查,就是對 keyspace 這個大 map 的增刪改查。
當我們執行:
>redis SET mobile "13800000000"
複製程式碼
實際上就是為 keyspace 增加了一個 key 是包含字串“mobile”的字串物件,value 為包含字元“13800000000”的字串物件。
看圖:
對於刪改查,沒啥好說的。類似java 的 map 操作,大多數程式設計師應該都能理解。
需要特別注意的是,再執行對鍵的讀寫操作的時候,Redis 還要做一些額外的維護動作:
- 維護 hit 和 miss 兩個計數器。用於統計 Redis 的快取命中率。
- 更新鍵的 LRU 時間,記錄鍵的最後活躍時間。
- 如果在讀取的時候發現鍵已經過期,Redis 先刪除這個過期的鍵然後再執行餘下操作。
- 如果有客戶對這個鍵執行了 WATCH 操作,會把這個鍵標記為 dirty,讓事務注意到這個鍵已經被改過。
- 沒修改一次 dirty 會增加1。
- 如果伺服器開啟了資料庫通知功能,鍵被修改之後,會按照配置傳送通知。
鍵的過期實現
Redis 作為快取使用最主要的一個特性就是可以為鍵值對設定過期時間。就看看 Redis 是如果實現這一個最重要的特性的?
在 Redis 中與過期時間有關的命令
- EXPIRE 設定 key 的存活時間單位秒
- EXPIREAT 設定 key 的過期時間點單位秒
- PEXPIRE 設定 key 的存活時間單位毫秒
- PEXPIREAT 設定 key 的過期時間點單位毫秒
其實這些命令,底層的命令都是由 REXPIREAT 實現的。
在 redisDb 中使用了 dict *expires,來儲存過期時間的。其中 key 指向了 keyspace 中的 key(c 語言中的指標), value 是一個 long long 型別的時間戳,標定這個 key 過期的時間點,單位是毫秒。
如果我們為上文的 mobile 增加一個過期時間。
>redis PEXPIREAT mobile 1521469812000
複製程式碼
這個時候就會在過期的 字典中增加一個鍵值對。如下圖:
對於過期的判斷邏輯就很簡單:
- 在 字典 expires 中 key 是否存在。
- 如果 key 存在,value 的時間戳是否小於當前系統時間戳。
接下來就需要討論一下過期的鍵的刪除策略。
key的刪除有三種策略:
- 定時刪除,Redis定時的刪除記憶體裡面所有過期的鍵值對,這樣能夠保證記憶體友好,過期的key都會被刪除,但是如果key的數量很多,一次刪除需要CPU運算,CPU不友好。
- 惰性刪除,只有 key 在被呼叫的時候才去檢查鍵值對是否過期,但是會造成記憶體中儲存大量的過期鍵值對,記憶體不友好,但是極大的減輕CPU 的負擔。
- 定時部分刪除,Redis定時掃描過期鍵,但是隻刪除部分,至於刪除多少鍵,根據當前 Redis 的狀態決定。
這三種策略就是對時間和空間有不同的傾向。Redis為了平衡時間和空間,採用了後兩種策略 惰性刪除和定時部分刪除。
惰性刪除比較簡單,不做過多介紹。主要討論一下定時部分刪除。
過期鍵的定時刪除的策略由 expire.c/activeExpireCycle() 函式實現,server.c/serverCron() 定時的呼叫 activieExpireCycle()
。
activeExpireCycle 的大的操作原則是,如果過期的key比較少,則刪除key的數量也比較保守,如果,過期的鍵多,刪除key的策略就會很激進。
static unsigned int current_db = 0; /* Last DB tested. */
static int timelimit_exit = 0; /* Time limit hit in previous call? */
static long long last_fast_cycle = 0; /* When last fast cycle ran. */
複製程式碼
-
首先三個
static
全域性引數分別記錄目前遍歷的 db下標,上一次刪除是否是超時退出的,上一次快速操作是什麼時候進行的。 -
計算
timelimit = 1000000*ACTIVE_EXPIRE_CYCLE_SLOW_TIME_PERC/server.hz/100;
可以理解為 25% 的 cpu 時間。 -
如果 db 中 expire 的大小為0 不操作
-
expire 佔總 key 小於 1% 不操作
-
num = dictSize(db->expires);num 是 expire 使用的key的數量。
-
slots = dictSlots(db->expires); slots 是 expire 字典的尺寸大小。
-
已使用的key(num) 大於 ACTIVE_EXPIRE_CYCLE_LOOKUPS_PER_LOOP 則設定為 ACTIVE_EXPIRE_CYCLE_LOOKUPS_PER_LOOP。也就是說每次只檢查 ACTIVE_EXPIRE_CYCLE_LOOKUPS_PER_LOOP 個鍵。
-
隨機獲取帶過期的 key。計算是否過期,如果過期就刪除。
-
然後各種統計,包括刪除鍵的次數,平均過期時間。
-
每遍歷十六次,計算操作時間,如果超過 timelimit 結束返回。
-
如果刪除的過期鍵大於 ACTIVE_EXPIRE_CYCLE_LOOKUPS_PER_LOOP 的 14 就跳出迴圈,結束。
步驟比較複雜,總結一下:(這裡都是以預設配置描述)
- redis 會用最多 25% 的 cpu 時間處理鍵的過期。
- 遍歷所有的 redisDb
- 在每個 redisDb 中如果資料中沒有過期鍵或者過期鍵比例過低就直接進入下一個 redisDb。
- 否則,遍歷 redisDb 中的過期鍵,如果刪除的鍵達到有過期時間的的key 的25% ,或者操作時間大於 cpu 時間的 25% 就結束當前迴圈,進入下一個redisDb。
後記
這篇文章主要解釋了 Redis 的資料庫是怎麼實現的,同時介紹了 Redis 處理過期鍵的邏輯。看 Redis 的程式碼越多越發現,實際上 Redis 一直在做的一件事情就是平衡,一直在平衡程式的空間和時間。其實平時的業務設計,就是在巨集觀上平衡,平衡系統的時間和空間。所以我想說的是,看原始碼是讓我們從微觀學習系統架構,是每一位架構師的必經之路。大家加油。
我之前的三篇關於 Redis 的基礎資料結構連結地址,歡迎大家閱讀。
Redis 的基礎資料結構(二) 整數集合、跳躍表、壓縮列表
歡迎關注我的微信公眾號: