Redis設計與實現之數據庫

1,數據庫
Redis使用redis.h/redisServer結構保存數據庫狀態。其中的數組redisDb *db,保存數據庫中所有的數據庫。另一個變量 int dbnum屬性記錄當前服務器中數據庫的數量。dbnum默認爲16,也就是redis初始化時會默認創建16個數據庫。
2,切換數據庫
每一個redis客戶端都有自己的目標數據庫,客戶端執行數據庫的寫/讀命令時目標數據庫就是成爲命令操作的對象。默認情況下,redis客戶端使用0號數據庫,並可以使用select命令來切換目標數據庫。切換數據庫的過程也是redisClient數據結構中 redisDb *db指針重新賦值的過程。
3,數據庫鍵空間
Redis是一個鍵值對數據庫服務器,每一個數據庫使用redis.h/redisDb結構描述,其中有一個dict字典保存了該數據庫的所有鍵值對,成爲鍵空間;

typedef struct redisDb {
    dict *dict; /* The keyspace for this DB */
} redisDb;

鍵空間的鍵也就是數據庫的鍵,每一個鍵都是一個字符串對象
鍵空間的值也就是數據庫的值,每一個值可以是字符串對象、列表對象、哈希表對象、集合對象和有序集合對象的任意一種。
添加、刪除、更新和獲取數據庫中的鍵,就是對dict字典的操作。注意每一種值對象對應着不同的鍵獲取命令,如get,lrange,hget,smembers,zrange等。
其他一些針對數據庫本身的命令,也是通過對鍵空間進行處理來完成的。如flushdb用於刪除鍵空間中所有鍵值對,dbsize返回數據庫鍵數量等。
Redis對鍵空間的一些維護操作:
1,讀取鍵時,根據鍵是否存在跟新鍵空間命中次數和不命中次數,並用keyspace_hits和keyspace_misses屬性記錄。可用info stats命令查看。
2,更新鍵的lru(最後一次使用)時間,用於計算閒置時間。使用object idletime 命令查看閒置時間。
3,讀取鍵時,如果過期,要先刪除過期鍵在執行後續操作。
4,客戶端使用watch命令,監視一個鍵,如果該鍵被修改,就會被標記爲髒(dirty),以使用事務程序對其處理。
5,服務器每修改一個鍵都會講髒鍵計數器+1,該計數器會觸發服務器的持久化以及複製操作。
6,如果服務器開啓了數據庫通知功能,對鍵修改後,服務器會按配置發送相應的數據庫通知。
4,鍵的生存週期
使用expire,pexpire命令(單位分別是s和ms)設置鍵的生存時間TTL。expire ;經過指定的時間後,數據庫會自動產出ttl爲0的鍵。可以使用ttl或者pttl命令查看帶有ttl鍵的剩餘時間。
使用expireat,pexpireat命令將過期時間設置爲相應的時間戳。expireat ;
實際上expire,pexpire,expireat最終都是調用了pexpireat實現了設置ttl的功能。
expire(將s轉化爲ms)->pexpire(將ttl轉換爲timestamp)->->pexpireat
expireat(將s級timestamp轉化爲ms級)->pexpireat
redisDb結構使用名爲expires的字典保存數據庫中所有鍵的過期時間,也稱爲過期字典。

typedef struct redisDb {
    dict *expires; /* Timeout of keys with a timeout set */
} redisDb;

expires的鍵時指針,指向鍵空間的某個鍵對象
expires的值是longlong類型的整數,保存了指向數據庫鍵的過期時間,一個ms級unix時間戳。
使用persist可以解除給定鍵的過期時間。
使用ttl或者pttl命令獲得鍵的剩餘生存時間,實現在db.c/ttlGenericCommand(),僞碼如下:

def ttlGenericCommand(key):
    if key 不在數據庫中
          return -2
    if key 沒有設置expire
          return -1
    ttl = expire時間戳 - 當前時間戳
    if ttl 小於 0
          ttl = 0
    按照ms標示將ttl的單位設置爲轉化爲ms或者s
    返回 ttl

判斷鍵是否過期:
1,檢查指定鍵是否存在於過期字典;如果存在,取得過期時間,否則返回false
2,檢查當前時間戳是否大於過期時間戳;如果大於,鍵已過期返回true,否則返回false
5,過期鍵的刪除策略
過期鍵的刪除策略有三種:
1,定時刪除
設置過期時間的同時創建一個定時器,讓定時器在鍵的過期時間來臨時立即執行對鍵的刪除。
該策略對內存比較友好,保證過期鍵會被儘快刪除,但是它對cpu時間最不友好,大量的過期鍵需要進行刪除操作時會佔用大量的cpu時間。此外定時器使用時間事件處理,redis的時間事件存放在一個無序鏈表中,查找時間複雜度爲O(N),並不高效。
2,惰性刪除
放任過期鍵不管,但是每當從鍵空間中獲取到一個鍵時,都要檢查是否過期,如果過期執行刪除操作,否則返回該鍵。
該策略對cpu時間最友好,不會在刪除其他無關的過期鍵上花費cpu時間,但是對內存最不友好。大量的過期鍵得不到刪除會佔用大量的內存,甚至是內存泄露。
3,定期刪除
每隔一段時間對數據庫進行一次檢查,刪除遇到的過期鍵。每次檢查多少個鍵,多少個數據庫,由算法決定。
該策略是前兩種策略的折中;
1,每隔一段時間執行一次過期鍵的刪除,並通過限制刪除操作執行的時長和頻率來減少對cpu時間的影響
2,定期刪除,也有效的減少了大量過期鍵帶來的內存浪費。
該策略的難度在於刪除操作執行的時長和頻率:
1,太頻繁,退化爲定時刪除,佔用大量cpu時間
2,太少,退化爲惰性刪除,內存浪費
6,redis的過期鍵刪除策略
redis同時使用惰性刪除和定期刪除兩種策略。在CPU時間和內存之間取得平衡。
惰性刪除。所有讀寫數據庫的redis命令在執行之前都會調用db.c/expireIfNeeded檢查是否過期,實現如下:
1,如果鍵過期了,expireIfNeeded函數將輸入鍵從數據庫中刪除
2,如果鍵未過期,expireIfNeeded不做操作
注:
1,當鍵存在且未過期時,命令按照鍵存在的情況執行
2,當鍵不存在或者過期時,命令按照鍵不存在的情況執行
定期刪除。redis.c/activeExpireCycle函數實現了定期刪除,當服務器週期性操作redis.c/serverCron函數執行時activeExpireCycle被執行:
1,函數每次運行都從一定數量的數據庫中隨機選取一定數量的鍵進行檢查並刪除其中的過期鍵。數量分別由REDIS_DBCRON_DBS_PER_CALL和ACTIVE_EXPIRE_CYCLE_LOOKUPS_PER_LOOP指定。
2,全局變量current_db記錄當前檢查進度。每檢查一個數據庫,current_db+1,下一次執行同樣從current_db開始
3,當所有數據庫都被檢查一遍後,current_db置0
7,AOF、RDB和複製功能對過期鍵的處理
生成RDB文件。使用save或者bgsave命令創建一個新的RGB文件時,程序會對數據庫中的鍵進行檢查,過期的鍵並不會寫入到新創建的RDB文件中。
載入RDB文件。啓動redis數據庫時,如果啓用了RDB功能,那麼服務器將對RDB文件進行載入:
1,redis服務器以主服務器模式運行,載入RDB文件時,程序會對文件中保存的鍵進行檢查,過期鍵不會被載入到數據庫中
2,redis服務器以從服務器模式運行,載入RDB文件時,文件中的所有鍵無論是否過期都會被載入到數據庫。但是主從同步時,從數據庫的數據會被清空,所以過期鍵對載入RDB文件也不會有影響。
AOF文件寫入。在AOF持久化模式下,如果鍵已過期但是還沒有被刪除,那麼對AOF文件不會有影響。當過期鍵被刪除時,程序會向AOF文件append一條del命令,顯式記錄該鍵已被刪除。
AOF文件重寫時的情況和生成RDB文件的情況是一樣的。
主從複製模式下服務器的過期鍵刪除由主服務器控制,從而保證了主從數據一致性:
1,主服務器刪除過期鍵時,向所有從服務器發送del命令,告知刪除這個過期鍵
2,從服務器執行客戶端命令時,及時遇到過期鍵也不會將過期鍵刪除,而是將過期鍵當做非過期鍵處理
3,從服務器只有收到主服務器的del通知之後,纔會刪除相應的過期鍵
5,數據庫通知
使用subscribe命令訂閱針對鍵或者命令的操作信息,來獲知鍵的變化。如:

subscribe __keyspace@0__:<key|command>

此外,服務器配置notify_keyspace_events選項決定了服務器發送通知的類型:
AKE———-服務器發送所有鍵空間通知和鍵事件通知
AK————服務器發送所有鍵空間通知
AE————服務器發送所有鍵事件通知
K$————服務器發送所有和字符串相關的鍵空間通知
El————-服務器發送所有和列表鍵相關的鍵事件通知
發送通知。當命令被執行的時候,如果執行成功後會調用notify.c/notifyKeyspaceEvent函數發送通知。僞代碼如下:

def notifyKeyspaceEvent(type,event,key dbid):
    if 通知類型type不是server允許發送通知類型:
          return
    #發送鍵空間通知
    if server.notify_keyspace_events & NOTIFY_KEYSPACE
          #將通知發送給頻道__keyspace@<dbid>__:<key>,內容爲發生的事件通知<event>
          chan = __keyspace@<dbid>__:<key>
          pubsubPublishMessage(chanobj, eventobj);
    #發送鍵事件通知
    if server.notify_keyspace_events & NOTIFY_KEYEVENT
          #將通知發送給頻道__keyspace@<dbid>__:<event>,內容爲發生的事件通知<key>
          chan = __keyspace@<dbid>__:<event>
          pubsubPublishMessage(chanobj, keyobj);
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章