9.1:服務器中的數據庫redis
Redis服務器將全部的數據庫都保存在服務器狀態 redis.h/redisServer 結構的db數組中,db數組的每一個項都是一個 redis.h/redisDb 結構,每一個redisDb結構都是一個數據庫:算法
1 struct redisServer{ 2 3 //... 4 5 //一個數組,保存着服務器中全部的數據庫 6 redisDb *db; 7 8 //服務器數據庫數量 9 int dbnum; 10 //... 11 };
在初始化數據庫時,程序會根據服務器狀態的dbnum屬性來決定應該建立多少個數據庫,dbnum屬性的值是由服務器配置的database選項決定,默認狀況下,該選項值爲16,因此Redis服務器會默認建立16個數據庫,以下圖所示: 數據庫
9.2 切換數據庫數組
每一個Redis客戶端都有本身的數據庫目標,每當客戶端執行數據庫寫命令或者數據庫讀命令的時候,目標數據庫就會成爲這些命令的操做對象。服務器
默認狀況下,Redis客戶端的目標數據庫爲0號數據庫,但客戶端也能夠經過執行 SELECT 命令切換目標數據庫。函數
在服務器內部,客戶端狀態 redisClient 結構的db屬性記錄了客戶端當前的目標數據庫,這個屬性是一個指向redisDb結構的指針。spa
1 typedef struct redisClient{ 2 3 //... 4 5 //記錄當前客戶端正在使用的數據庫 6 redisDb *db; 7 8 //... 9 }redisClient;
redisClient.db 指針指向 redisServer.db 數組的其中一個元素,而被指向的元素就是客戶端的目標數據庫。3d
經過修改redisClient.db 指針,讓它指向服務器不一樣的數據庫,從而實現切換目標數據庫的功能,這就是SELECT命令的原理。指針
備註:Redis沒有返回客戶端目標數據庫的功能。code
9.3 數據庫鍵空間
Redis是一個鍵值對(key-value pair)數據庫服務器,服務器中的每一個數據庫都是由一個redis.h/redisDb結構表示,其中,redisDb結構的dict字典保存了數據庫中的全部鍵值對,咱們將這個字典稱爲鍵空間(key sapce)。
1 typedef struct redisDb{ 2 3 //... 4 5 //數據庫鍵空間,保存着數據庫中全部的鍵值對。 6 dict *dict; 7 8 //... 9 }redisDb;
9.3 讀寫鍵空間時的維護操做
當使用Redis命令對數據庫進行讀寫操做時,服務器不只會對鍵空間執行指定的讀寫操做,還會執行一些額外的操做,其中包括:
1. 在讀取一個鍵以後(讀操做和寫操做都要對鍵進行讀取),服務器都會根據鍵是否存在來更新服務器鍵的空間命中(hit)次數或鍵空間不命中(miss)次數,這兩個值能夠在INFO stats命令的 keyspace_hits 屬性和 keyspace_misses 屬性中查看。
2. 在讀取一個鍵以後,服務器會更新鍵的LRU(最後一次使用)時間,這個值能夠用於計算鍵的閒置時間,使用OBJECT idletime<key>命令能夠查看鍵key的閒置時間。
3. 若是服務器在讀取一個鍵時,發現這個鍵已通過期,那麼服務器會先刪除這個過時鍵,而後才執行其它操做。
4. 若是有客戶端使用WATCH 命令監視了某個鍵,那麼服務器在對被監視的鍵進行修改以後,會將這個過時鍵標記爲 髒(dirty),從而讓事務程序注意到這個鍵已經被修改。
5. 服務器每次修改一個鍵後,都會對 髒(dirty)鍵計數器的值增1,這個計數器會觸發服務器的持久化已經複製操做。
6. 若是服務器開啓了數據庫通知功能,那麼在對鍵進行修改以後,服務器將按配置發送響應的數據庫通知。
9.4 設置鍵的生存時間或過時時間
經過 EXPIRE 命令或者 PEXPIRE 命令,客戶端能夠以秒或者毫秒精度爲數據庫中的某個鍵設置生存時間(Time To Live,TTL),在通過指定的秒數或者毫秒數以後買服務器就會自動刪除生存時間爲0 的鍵。
1 redis> SET key value 2 OK 3 4 redis> EXPIRE key 5 5 (integer) 1 6 7 redis> GET key //5秒以內 8 "value" 9 10 redis> GET key //5秒以後 11 (nil)
備註:SETEX 命令能夠在設置一個字符串鍵的同時設置該鍵的過時時間,由於這個命令是一個類型限定命令(只能用於字符串鍵)。可是 SETEX 命令設置過時時間的原理和 EXPIRE 命令的原理是徹底同樣的。
與 EXPIRE 命令和 PEXPIRE 命令相似,客戶端能夠經過 EXPIREAT 命令和 PEXPIREAT 命令以秒或者毫秒精度爲數據庫中的某個鍵設置過時時間(expire time)。過時時間是一個 UNIX 時間戳,當鍵過時時間來臨時,服務器就會字典從數據庫刪除這個鍵。
設置過時時間:
Redis有四個不一樣耳釘命令能夠用於設置鍵的生存時間(鍵能夠存在多久)或者過時時間(鍵何時能夠刪除):
1. EXPIRE<key><ttl>命令用於將鍵key的生存時間設置爲 ttl 秒。
2. PEXPIRE<key><ttl>命令用於將鍵key的生存時間設置爲 ttl 毫秒。
3. EXPIREAT<key><timestamp>命令用於將鍵key的過時時間設置爲 timestamp 所指定的秒數時間戳。
4. PEXPIREAT <key><timestamp>命令用於將鍵key的過時時間設置爲 timestamp 所指定的毫秒數時間戳。
雖然有不一樣單位和不一樣形式的設置命令,但實際上 EXPIRE、PEXPIRE、EXPIREAT 三個命令都是使用PEEXPIREAT命令來實現的:不管客戶端執行的是以上四個命令的哪個,通過轉換時候,最終的執行都和執行 PEXPIREAT 命令。
9.4 保存過時時間
redisDb 結構的expires 字典保存了數據庫中全部鍵的過時時間,咱們稱這個字典爲 過時字典。
過時字典的鍵是一個指針,這個指針指向鍵空間中的某個鍵對象(也就是某個數據庫鍵)。
過時字典的值是一個 long long 類型的整數,這個整數保存了鍵所指向的數據庫鍵的過時時間————一個毫秒精度的 UNIX 時間戳。
1 typedef struct redisDb{ 2 3 //... 4 5 //數據庫鍵空間,保存着數據庫中全部的鍵值對。 6 dict *dict; 7 8 //過時字典,保存着鍵的過時時間 9 dict *expires; 10 11 //... 12 }redisDb;
備註:鍵空間保存了數據庫中的全部鍵值對,而過時字典則保存了數據庫鍵的過時時間。
示例:
備註:在實際中,鍵空間的鍵 和 過時字典中的鍵都是指向同一個鍵對象,因此不會形成內存浪費。
移除過時時間:
PERSIST 命令能夠移除一個鍵的過時時間。PERSIST 命令就是 PEXPIREAT 命令的反操做:PERSIST 命令命令在過時字典中查找給定的鍵,並解除鍵和值(過時時間)在過時字典中的關聯。
計算並返回剩餘時間:
TTL 命令以秒爲單位返回剪的剩餘生存時間,而 PTTL 命令則以毫秒爲單位返回鍵的剩餘生存時間。TTL 命令和 PTTL 命令都是經過計算鍵的過時時間和當前時間之間的差來實現的。
過時鍵的斷定:
經過過時字典,程序能夠經過如下步驟檢查一個給定鍵是否過時:
1. 檢查給定鍵是否存在與過時字典,若是存在,那麼取得鍵的過時時間;
2. 檢查當前 UNIX 時間戳是否大於鍵的過時時間,若是是的話,那麼鍵已通過期,不然的話,鍵未過時。
9.5 過時鍵刪除策略
數據鍵的過時時間都保存在過時字典中。若是一個鍵過時了,那麼它何時會被刪除呢?
三種不一樣的刪除策略:
1. 定時刪除(主動刪除策略):在設置鍵的過時時間的同時,建立一個定時器(timer),讓定時器在鍵過時時間來臨時,當即執行對鍵的刪除操做。
2. 惰性刪除(被動刪除策略):聽任鍵過時無論,可是每次從鍵空間獲取鍵時,都檢查取得的鍵是否過時,若是過時的話,則刪除該鍵;若是沒有,則返回該鍵。
3. 按期刪除(主動刪除策略):每隔一段時間,程序就對數據庫進行一次檢查,刪除裏面的過時鍵。置於要刪除多少過時鍵,以及要檢查多少數據庫,則由算法決定。
下面介紹各自刪除策略的特色:
1. 定時刪除:優勢:對內存是最友好的,經過使用定時器,定時刪除策略能夠保證過時鍵能夠儘量快的被刪除,並釋放過時鍵所佔用的內存;缺點:對CPU時間是不友好的,在過時鍵比較多的狀況下,刪除過時鍵的行爲可能佔用至關一部分CPU的時間,在內存不緊張可是CPU時間很是緊張額狀況下,將CPU時間用在刪除和當前任務無關的過時鍵上,無疑會對服務器的響應時間和吞吐量形成影響。
例如,若是正有大量的命令請求正等待服務器處理,而且服務器當前不缺乏內存,那麼服務器應該優先將CPU時間用在處理客戶端請求上面,而不是用在刪除過時鍵上面。
除此以外,建立一個定時器須要用到Redis服務器中的時間事件,而當前時間事件的實現方式——無序鏈表,查找一個事件的時間複雜度爲O(N),——並不能高效的處理大量時間事件。
所以,要讓服務器建立大量的定時器,從而實現那定時刪除策略,在現階段不顯示。
2. 惰性刪除:優勢:對CPU時間是最友好的,程序只會在取出鍵時纔對鍵過時檢查,這能夠保證刪除過時的鍵的操做是在非作不可的狀況下進行,而且刪除的目標僅限於當前的鍵,這個策略不會再刪除其它的過時鍵上花費任何CPU時間。缺點:對內存是不友好的,若是一個鍵已通過期,而這個鍵仍然保存在數據庫中,那麼只要這個過時鍵不刪除,它所佔用的內存就不會被釋放(除非用戶手動執行 ELFUSHDB 命令)這能夠視爲 「內存泄漏」 的一種。
3. 按期刪除:從上面對定時刪除和惰性刪除來看,這兩種刪除方式在單一使用時都有明顯的缺陷:a. 定時刪除佔用太多CPU時間,影響服務器的響應時間和吞吐量。b. 惰性刪除浪費太多內存,有內存泄漏的風險。
按期刪除策略是前兩種策略的一種整合和折中:
a. 按期刪除策略每隔一段時間執行一次刪除過時鍵操做,並經過限制刪除操做的時長和頻率來減小刪除操做對CPU的影響。除此以外,經過按期刪除過時鍵,按期刪除策略有效的減小了由於過時鍵而帶來的內存浪費。
b. 按期刪除策略的難點在於合理的設置刪除操做的頻率和時長。
9.6 Redis 的過時鍵刪除策略
Redis 服務器實際採用的是惰性刪除和按期刪除兩種策略:搭配使用,合理的使用CPU時間和避免浪費內存空間之間取得平衡。
惰性策略的實現:
過時鍵的惰性策略由 db.c/expireIfNeeded 函數實現,全部讀寫數據庫的redis命令在執行以前都會調用 expireIfNeeded 函數對輸入鍵進行檢查:
1. 若是輸入鍵已通過期,那麼 expireIfNeeded 函數將輸入鍵從數據庫刪除;
2. 若是輸入鍵沒有過時,那麼 expireIfNeeded 不作動做;
expireIfNeeded 函數就像一個過濾器,它能夠在命令真正執行以前,過濾掉過時輸入鍵,從而避免命令接觸到過時鍵。
另外,由於每一個被訪問的鍵都有肯由於過時而被 expireIfNeeded 函數刪除,因此每一個命令的實現函數都必須能同時處理鍵存在和鍵不存在這兩種狀況:
1.當鍵存在時,命令按照鍵存在的狀況執行。
2.當鍵不存在或者鍵由於過時而被 expireIfNeeded 函數刪除時,命令按照鍵不存在的狀況執行。
舉例:GET 命令的執行過程。
按期策略的實現:
過時鍵的惰性策略由 redis.c/activeExpireCycle 函數實現,每當Redis的服務器週期性操做 redis.c/activeExpireCycle 函數執行時, redis.c/activeExpireCycle 函數就會被調用,它在規定的時間內,分屢次遍歷服務器中的各個數據庫,從數據庫的 expires 字典中隨機檢查一部分鍵的過時時間,並刪除其中的過時鍵。
activeExpireCycle 函數的工做模式能夠總結以下:
1. 函數每次運行時,都從必定數量的數據庫中取出必定數量的隨機鍵進行檢查,並刪除其中的過時鍵。
2. 全局變量 current_db 會記錄當前 activeExpireCycle 函數檢查的進度,並在下一次 activeExpireCycle 調用時,接着上一次的進度進行處理。好比說,若是當前 activeExpireCycle 函數在遍歷10號數據庫返回了,那麼下次 activeExpireCycle 函數執行時,將從11號數據庫開始查找並刪除過時鍵。
3. 隨着 activeExpireCycle 函數的不斷執行,服務器中的全部數據庫都會被檢查一遍,這時函數將current_db 變量重置爲0,而後再次開始新一輪的檢查工做。
9.7 AOF、RDB 和複製功能對過時鍵的處理
生成RDB文件
在執行 SAVE 命令或者 BGSAVE 命令建立一個新的 RDB 文件是,程序會對數據庫中的鍵進行檢查,已過時的鍵不會被保存到新建立的 RDB 文件中。