勤能補拙是良訓,一分耕耘一分才。——華羅庚
引導語
Redis伺服器的資料庫實現進行詳細介紹,說明伺服器保存資料庫的方法,客戶端切換資料庫的方法,資料庫保存鍵值對的方法,以及針對資料庫的添加、刪除、查看、更新操作的實現方法等。除此之外,本章還會說明伺服器保存鍵的過期時間的方法,以及伺服器自動刪除過期鍵的方法。
1 伺服器中的資料庫
Redis伺服器將所有資料庫都保存在伺服器狀態redis.h/redisServer結構的db數組中,db數組的每個項都是一個redis.h/redisDb結構,每個redisDb結構代表一個資料庫:
struct redisServer {
//一個數組,保存著伺服器中的所有資料庫
redisDb *db;
};
在初始化伺服器時,程序會根據伺服器狀態的dbnum屬性來決定應該創建多少個資料庫:
struct redisServer {
//伺服器的資料庫數量
int dbnum;
};
dbnum屬性的值由伺服器配置的database選項決定,默認情況下,該選項的值為16,所以Redis伺服器默認會創建16個資料庫。
2 切換資料庫
每個Redis客戶端都有自己的目標資料庫,每當客戶端執行資料庫寫命令或者資料庫讀命令的時候,目標資料庫就會成為這些命令的操作對象。
默認情況下,Redis客戶端的目標資料庫為0號資料庫,但客戶端可以通過執行SELECT命令來切換目標資料庫。
在伺服器內部,客戶端狀態redisClient結構的db屬性記錄了客戶端當前的目標資料庫,這個屬性是一個指向redisDb結構的指針:
typedef struct redisClient {
//記錄客戶端當前正在使用的資料庫
redisDb *db;
} redisClient;
redisClient.db指針指向redisServer.db數組的其中一個元素,而被指向的元素就是客戶端的目標資料庫。
3 資料庫鍵空間
Redis是一個鍵值對資料庫伺服器,伺服器中的每個資料庫都由一個redis.h/redisDb結構表示,其中,redisDb結構的dict字典保存了資料庫中的所有鍵值對,我們將這個字典稱為鍵空間:
typedef struct redisDb {
//資料庫鍵空間,保存著資料庫中的所有鍵值對
dict *dict;
} redisDb;
鍵空間和用戶所見的資料庫是直接對應的:
a. 鍵空間的鍵也就是資料庫的鍵,每個鍵都是一個字符串對象。
b. 鍵空間的值也就是資料庫的值,每個值可以是字符串對象、列表對象、哈希表對象、集合對象和有序集合對象中的任意一種Redis對象。
因為資料庫的鍵空間是一個字典,所以所有針對資料庫的操作,比如添加一個鍵值對到資料庫,或者從資料庫中刪除一個鍵值對,又或者在資料庫中獲取某個鍵值對等,實際上都是通過對鍵空間字典進行操作來實現的。
讀寫鍵空間時的維護操作
當使用Redis命令對資料庫進行讀寫時,伺服器不僅會對鍵空間執行指定的讀寫操作,還會執行一些額外的維護操作,其中包括:
a. 在讀取一個鍵之後(讀操作和寫操作都要對鍵進行讀取),伺服器會根據鍵是否存在來更新伺服器的鍵空間命中(hit)次數或鍵空間不命中(miss)次數,這兩個值可以在INFO stats命令的keyspace_hits屬性和keyspace_misses屬性中查看。
b. 在讀取一個鍵之後,伺服器會更新鍵的LRU(最後一次使用)時間,這個值可以用於計算鍵的閒置時間,使用OBJECT idletime命令可以查看鍵key的閒置時間。
c. 如果伺服器在讀取一個鍵時發現該鍵已經過期,那麼伺服器會先刪除這個過期鍵,然後才執行餘下的其他操作,本章稍後對過期鍵的討論會詳細說明這一點。
d. 如果有客戶端使用WATCH命令監視了某個鍵,那麼伺服器在對被監視的鍵進行修改之後,會將這個鍵標記為髒(dirty),從而讓事務程序注意到這個鍵已經被修改過。
e. 伺服器每次修改一個鍵之後,都會對髒(dirty)鍵計數器的值增1,這個計數器會觸發伺服器的持久化以及複製操作。
f. 如果伺服器開啟了資料庫通知功能,那麼在對鍵進行修改之後,伺服器將按配置發送相應的資料庫通知。
4 設置鍵的生存時間或過期時間
通過EXPIRE命令或者PEXPIRE命令,客戶端可以以秒或者毫秒精度為資料庫中的某個鍵設置生存時間(Time To Live,TTL),在經過指定的秒數或者毫秒數之後,伺服器就會自動刪除生存時間為0的鍵。
SETEX命令可以在設置一個字符串鍵的同時為鍵設置過期時間,因為這個命令是一個類型限定的命令(只能用於字符串鍵)。
過期時間是一個UNIX時間戳,當鍵的過期時間來臨時,伺服器就會自動從資料庫中刪除這個鍵。
TTL命令和PTTL命令接受一個帶有生存時間或者過期時間的鍵,返回這個鍵的剩餘生存時間,也就是,返回距離這個鍵被伺服器自動刪除還有多長時間;都是通過計算鍵的過期時間和當前時間之間的差來實現的。
設置過期時間
Redis有四個不同的命令可以用於設置鍵的生存時間(鍵可以存在多久)或過期時間(鍵什麼時候會被刪除):
· EXPIRE<key><ttl>命令用於將鍵key的生存時間設置為ttl秒。
· PEXPIRE<key><ttl>命令用於將鍵key的生存時間設置為ttl毫秒。
· EXPIREAT<key><timestamp>命令用於將鍵key的過期時間設置為timestamp所指定的秒數時間戳。
· PEXPIREAT<key><timestamp>命令用於將鍵key的過期時間設置為timestamp所指定的毫秒數時間戳。
實際上EXPIRE、PEXPIRE、EXPIREAT三個命令都是使用PEXPIREAT命令來實現的。
保存過期時間
redisDb結構的expires字典保存了資料庫中所有鍵的過期時間,我們稱這個字典為過期字典:
a. 過期字典的鍵是一個指針,這個指針指向鍵空間中的某個鍵對象(也即是某個資料庫鍵)。
b. 過期字典的值是一個long long類型的整數,這個整數保存了鍵所指向的資料庫鍵的過期時間——一個毫秒精度的UNIX時間戳。
typedef struct redisDb {
//過期字典,保存著鍵的過期時間
dict *expires;
} redisDb;
鍵空間的鍵和過期字典的鍵都指向同一個鍵對象,所以不會出現任何重複對象,也不會浪費任何空間。
移除過期時間
PERSIST命令可以移除一個鍵的過期時間。
PERSIST命令就是PEXPIREAT命令的反操作:PERSIST命令在過期字典中查找給定的鍵,並解除鍵和值(過期時間)在過期字典中的關聯。
過期鍵的判定
通過過期字典,程序可以用以下步驟檢查一個給定鍵是否過期:
a. 檢查給定鍵是否存在於過期字典:如果存在,那麼取得鍵的過期時間。
b. 檢查當前UNIX時間戳是否大於鍵的過期時間:如果是的話,那麼鍵已經過期;否則的話,鍵未過期。
(*)實現過期鍵判定的另一種方法是使用TTL命令或者PTTL命令,比如說,如果對某個鍵執行TTL命令,並且命令返回的值大於等於0,那麼說明該鍵未過期。在實際中,Redis檢查鍵是否過期的方法和is_expired函數所描述的方法一致,因為直接訪問字典比執行一個命令稍微快一些。
5 Redis的過期鍵刪除策略
如果一個鍵過期了,那麼它什麼時候會被刪除呢?
Redis伺服器實際使用的是惰性刪除和定期刪除兩種策略:通過配合使用這兩種刪除策略,伺服器可以很好地在合理使用CPU時間和避免浪費內存空間之間取得平衡。
惰性刪除策略的實現
過期鍵的惰性刪除策略由db.c/expireIfNeeded函數實現,所有讀寫資料庫的Redis命令在執行之前都會調用expireIfNeeded函數對輸入鍵進行檢查:
· 如果輸入鍵已經過期,那麼expireIfNeeded函數將輸入鍵從資料庫中刪除。
· 如果輸入鍵未過期,那麼expireIfNeeded函數不做動作。
定期刪除策略的實現
過期鍵的定期刪除策略由redis.c/activeExpireCycle函數實現,每當Redis的伺服器周期性操作redis.c/serverCron函數執行時,activeExpireCycle函數就會被調用,它在規定的時間內,分多次遍歷伺服器中的各個資料庫,從資料庫的expires字典中隨機檢查一部分鍵的過期時間,並刪除其中的過期鍵。
說明:
· 定時刪除佔用太多CPU時間,影響伺服器的響應時間和吞吐量。
· 惰性刪除浪費太多內存,有內存洩漏的危險。
定期刪除策略是前兩種策略的一種整合和折中:
· 定期刪除策略每隔一段時間執行一次刪除過期鍵操作,並通過限制刪除操作執行的時長和頻率來減少刪除操作對CPU時間的影響。
AOF和RDB持久化時對過期鍵的處理,請參考
《面試常問道:Redis持久化之RDB》
《面試常問道:Redis持久化之AOF》
redis 2.9版本