redis源碼之數據庫

未完待續。。

數據庫

1.服務器中的數據庫

Redis 服務器將所有數據庫都保存在服務器狀態
redis.h/redisServer 結構的db數組中,
db 數組的每個項都是一個redis.h/redisDb 結構,
每個redisDb 結構代表一個數據庫。

/*
*服務器狀態
*/
struct redisServer {
...
//一個數組,保存着服務器中的所有數據庫,指向redisDb 結構,每個redisDb 結構代表一個數據庫
redisDb *db;
//服務器的默認數據庫數量,由服務器配置的database 選項決定,默認情況下值爲16,
//所以Redis 服務器默認會創建 16 個數據庫,
int dbnum;
...
}

2.切換數據庫

每個Redis 客戶端都有自己的目標數據庫作爲操作對象。默認爲0 號數據庫,但客戶端可以通過執行SELECT 命令來切換目標數據庫。例如:SELECT 2切換到2號數據庫。

/*
*客戶端狀態
*/
typedef struct redisClient {
...
//記錄客戶端當前正在使用的數據庫,這個屬性是一個指向 redisDb 結構的指針
redisDb *db;
...
}redisClient;

redisClient.db 指針指向redisServer.db 數組的其中一個元素,而被指向的元素就是客戶端的目標數據庫。
這裏寫圖片描述

3.數據庫鍵空間

服務器中的每個數據庫都由一個redis.h/redisDb 結構表示,其中, redisDb 結構的dict 字典保存了數據庫中的所有鍵值對,我們將這個字典稱爲鍵空間( key space ) :

typedef struct redisDb{
...
//數據庫鍵空間,保存着數據庫中的所有鍵值對
dict *dict;
...
}redisDb;

例子如下:
數據庫的鍵空間將會是圖9-4 所展示的樣子:

redis> SET message "hello world"
OK
redis> RPUSH alphabet "a" "b" "c"
(integer)3
redis> HSET book name "Redis in Action"
(integer) 1
redis> HSET book author "Josiah L. Carlson"
(integer) 1
redis> HSET book publisher "Manning"
(integer) 1

操作——通過對鍵空間進行處理來完成的:
1.添加鍵(鍵-值都添加),刪除鍵(鍵-值都刪除),更新鍵(值的更新)
2.對鍵取值
3.用於清空整個數據庫的FLUSHDB 命令,隨機返回數據庫中某個鍵RANDOMKEY 命令,於返回數據庫鍵數量的DBSIZE 命令,類似的命令還有EXISTS、RENAME、KEYS 等,這些命令都是通過對鍵空間進行操作來實現的。

讀寫鍵空間時的維護操作
當使用Redis 命令對數據庫進行讀寫時,服務器不僅會對鍵空間執行指定的讀寫操作,還會執行一些額外的維護操作,其中包括:

  • 在讀取一個鍵之後(讀操作和寫操作都要對鍵進行讀取),服務器會根據鍵是否存在來更新服務器的鍵空間命中( hit )次數或鍵空間不命中( miss )次數,這兩個值可以在INFO stats 命令的keyspace_hits 屬性和keyspace_rnisses 屬性中查看。
  • 在讀取一個鍵之後,服務器會更新鍵的LRU (最後一次使用)時間,這個值可以用於計算鍵的閒置時間,使用OBJECT idletime <key> 命令可以查看鍵key 的閒置時間。
  • 如果服務器在讀取一個鍵時發現該鍵已經過期,那麼服務器會先刪除這個過期鍵,然後才執行餘下的其他操作,本章稍後對過期鍵的討論會詳細說明這一點。
  • 如果有客戶端使用WATCH 命令監視了某個鍵,那麼服務器在對被監視的鍵進行修改之後,會將這個鍵標記爲髒( dirty ),從而讓事務程序注意到這個鍵已經被修改過。
  • 服務器每次修改一個鍵之後,都會對髒( dirty )鍵計數器的值增1 ,這個計數器會觸發服務器的持久化以及複製操作
  • 如果服務器開啓了數據庫通知功能,那麼在對鍵進行修改之後,服務器將按配置發送相應的數據庫通知

4.設置鍵的生存時間或過期時間

通過EXPIRE 命令或者PEXPIRE 命令,客戶端可以以秒或者毫秒精度爲數據庫中的某個鍵設置生存時間( Time To Live, TTL ),在經過指定的秒數或者毫秒數之後,服務器就會自動刪除生存時間爲0 的鍵.
同樣的,EXPIREAT命令或PEXPIREAT命令,以秒或者毫秒精度給數據庫中的某個鍵設置過期時間( expire time )。當鍵的過期時間來臨時,服務器就會自動從數據庫中刪除這個鍵。
TTL 命令和PTTL命令接受一個帶有生存時間或者過期時間的鍵,返回這個鍵的剩餘生存時間。

具體怎麼刪除呢?是不是通過定時器,然後遍歷所有的時間戳?
數據庫如何保存鍵的生存時間和過期時間,以及服務器如何自動刪除那些帶有生存時間和過期時間的鍵?

1.實際上EXPIRE、PEXPIRE 、EXPIREAT三個命令都是使用PEXPIREAT命令來實現的(轉化爲毫秒級的過期時間):
2.redisDb 結構的expires 字典保存了數據庫中所有鍵的過期時間,我們稱這個字典爲過期字典
過期字典的鍵是一個指針,指向某個數據庫鍵;值是一個 long long 類型的整數,這個整數保存了鍵所指向的數據庫鍵的過期時間一一1個毫秒精度的UNIX 時間戳。

typedef struct redisDb {
    //過期字典,保存着鍵的過期時間
    diet *expires;
}

這裏寫圖片描述
僞代碼:

def PEXPIREAT(key, expire_time_in_ms):
//如果給定的鍵不存在於鍵空間,那麼不能設置過期時間
if key not in redisDb.dict:
returnO
//在過期字典中關聯鍵和過期時間
redisDb.expires[key] = expire_time_in_ms
//過期時間設置成功
return 1

3.移除過期時間
PERSIST message
PERSIST命令就是PEXPIREAl’命令的反操作: PERSIST命令在過期字典中查找給定的鍵,並解除鍵和值(過期時間)在過期字典中的關聯。
僞代碼:

def PERSIST(key):
//如果鍵不存在,或者鍵沒有設置過期時間,那麼直接返回
if key not in redisDb.expires:
return O
//移除過期字典中給定鍵的鍵值對關聯
redisDb.expires.remove(key)
//鍵的過期時間移除成功
return 1

4.計算並返回剩餘生存時間
TTL和PTTL 兩個命令都是通過計算鍵的過期時間和當前時間之間的差來實現的(TTL然後將差值從毫秒轉換爲秒之後得出的)。

5.過期鍵的判定
1 )檢查給定鍵是否存在於過期字典z 如果存在,那麼取得鍵的過期時間。
2 )檢查當前UNIX 時間戳是否大於鍵的過期時l曰:如果是的話,那麼鍵已經過期;否則的話,鍵未過期。
僞代碼:

def is_expired(key):
#取得鍵的過期時間
expire_time_in_ms = redisDb.expires.get(key)
#鍵沒有設置過期時間
if expire_time_in_ms is None:
return False
#取得當前時間的UNIX 時間戳
now_ms = get_current_unix_timestamp_in_ms ()
#檢查當前時間是否大於鍵的過期時間
if now_ms > expire_time_in_ms:
#是,鍵已經過期
return True
else:
#否,鍵未過期
return False

6.過期鍵刪除策略
三種不同的刪除策略:

  • 定時刪除:在設置鍵的過期時間的同時,創建一個定時器( timer ),讓定時器在鍵的過期時間來臨時,立即執行對鍵的刪除操作。
  • 惰性刪除:放任鍵過期不管,但是每次從鍵空間中獲取鍵時,都檢查取得的鍵是否過期,如果過期的話,就刪除該鍵;如果沒有過期,就返回該鍵
  • 定期刪除:每隔一段時間,程序就對數據庫進行一次檢查,刪除裏面的過期鍵。至於要刪除多少過期鍵,以及要檢查多少個數據庫,則由算法決定。

在這三種策略中,第一種和第三種爲主動刪除策略,而第二種則爲被動刪除策略。

1)定時刪除
定時刪除策略對內存是最友好的:通過使用定時器,定時刪除策略可以保證過期鍵會儘可能快地被刪除,並釋放過期鍵所佔用的內存。
另一方面,定時刪除策略的缺點是,它對CPU 時間是最不友好的:刪除過期鍵這一行爲可能會佔用相當一部分CPU 時間,在內存不緊張但是CPU 時間非常緊張的情況下,將CPU 時間用在刪除和當前任務無關的過期鍵上,無疑會對服務器的響應時間和吞吐量造成影響。
除此之外,創建一個定時器需要用到 Redis 服務器中的時間事件,而當前時間事件的實現方式一一無序鏈表,查找一個事件的時間複雜度爲O(N)一一並不能高效地處理大量時間事件。因此,要讓服務器創建大量的定時器,從而實現定時刪除策略,在現階段來說並不現實。(爲什麼不使用效率更高的定時器)
2)惰性刪除
惰性刪除策略對CPU 時間來說是最友好的,但是它對內存是最不友好的。
如果數據庫中有非常多的過期鍵,而這些過期鍵又恰好沒有被訪問到的話,那麼它們也許永遠也不會被刪除(除非用戶手動執行FLUSHDB ),我們甚至可以將這種情況看作是一種內存泄漏。
舉個例子,對於一些和時間有關的數據,比如日誌( log ),在某個時間點之後,對它們的訪問就會大大減少,甚至不再訪問,如果這類過期數據大量地積壓在數據庫中,用戶以爲服務器已經自動將它們刪除了,但實際上這些鍵仍然存在,而且鍵所佔用的內存也沒有釋放,那麼造成的後果肯定是非常嚴重的。
3)定期刪除
從上面對定時刪除和惰性刪除的討論來看,這兩種刪除方式在單一使用時都有明顯的缺陷:

  • 定時刪除佔用太多CPU 時間,影響服務器的響應時間和吞吐量。
  • 惰性刪除浪費太多內存,有內存泄漏的危險。

定期刪除策略是前兩種策略的一種整合和折中:

  • 定期刪除策略每隔一段時間執行一次刪除過期鍵操作,並通過限制刪除操作執行的時長和頻率來減少刪除操作對CPU 時間的影響
  • 除此之外,通過定期刪除過期鍵,定期刪除策略有效地減少了因爲過期鍵而帶來的內存浪費。

定期刪除策略的難點是確定刪除操作執行的時長和頻率

  • 如果刪除操作執行得太頻繁,或者執行的時間太長,定期刪除策略就會退化成定時刪除策略,以至於將CPU 時間過多地消耗在刪除過期鍵上面。
  • 如果刪除操作執行得太少,或者執行的時間太短,定期刪除策略又會和惰性刪除策
    略一樣,出現浪費內存的情況。

5.Redis 的過期鍵刪除策略

Redis 服務器實際使用的是惰性刪除和定期刪除兩種策略:通過配合使用這兩種刪除策略,服務器可以很好地在合理使用CPU 時間和避免浪費內存空間之間取得平衡。
1.惰性刪除策略的實現:
db.c/expireifNeeded
所有讀寫數據庫的Redis 命令在執行之前都會調用expireifNeeded 函數對輸入鍵進行檢查:

  • 如果輸入鍵已經過期,那麼expireIfNeeded 函數將輸入鍵從數據庫中刪除。
  • 如果輸入鍵未過期,那麼expireifNeeded 函數不做動作。
  • 當鍵不存在或者鍵因爲過期而被expireifNeeded 函數刪除時,命令按照鍵不存在的情況執行。
    這裏寫圖片描述

2.定期刪除策略的實現:
redis.c/activeExpireCycle
每當Redis 的服務器週期性操作redis.c/serverCron 函數執行時, activeExpireCycle 函數就會被調用,它在規定的時間內,分多次遍歷服務器中的各個數據庫,從數據庫的e xpires 字典中隨機檢查一部分鍵的過期時間,並刪除其中的過期鍵。

#默認每次檢查的數據庫數量
DEFAULT DB NUMBERS= 16
#默認每個數據庫檢查的鍵數量
DEFAULT KEY NUMBERS= 20
#全局變量,記錄檢查進度
current db= 0
def activeExpireCycle () :
    #初始化要檢查的數據庫數量
    #如果服務器的數據庫數量比DEFAULT DB NUMBERS 要小那麼以服務器的數據庫數量爲準
    if server.dbnum < DEFAULT DB NUMBERS:
        db numbers= server.dbnum
    else:
        db numbers= DEFAULT DB NUMBERS
    #遍歷各個數據庫
    for i in range(db_numbers):
        #如果current db 的值等於服務器的數據庫數量,這表示檢查程序已經遍歷了服務器的所有數據庫一次,
        #將current db 重置爲0 ,開始新的一輪遍歷
        if current db= server.dbnum:
            current db= 0

        #獲取當前要處理的數據庫
        redisDb = server.db[current db]
        #將數據庫索引增1 ,指向下一個要處理的數據庫
        current db+= 1
        #檢查數據庫鍵
        for j in range(DEFAULT KEY NUMBERS):
            #如果數據庫中沒有一個鍵帶有過期時間,那麼跳過這個數據庫
            if redisDb.expires.size() == 0: break
            #隨機獲取一個帶有過期時間的鍵
            key with ttl = redisDb.expires.get random key()
            #檢查鍵是否過期,如果過期就刪除它
            if is_expired (key_with_ttl):
                delete_key(key_with_ttll
            #已達到時間上限,停止處理
            if reach time limit(): 
                return
  • 函數每次運行時,都從一定數量的數據庫中取出一定數量的隨機鍵進行檢查,並刪
    除其中的過期鍵。
  • 全局變量current db 會記錄當前activeExpireCycle 畫數檢查的進度,並在下一次activeExpireCycle 函數調用時,接着上一次的進度進行處理。比如說,如果當前activeExpireCycle 函數在遍歷10 號數據庫時返回了,那麼下次activeExpireCycle 畫數執行時,將從11 號數據庫開始查找並刪除過期鍵。
  • 隨着activeExpireCycle 函數的不斷執行,服務器中的所有數據庫都會被檢查一遍,這時畫數將current db 變量重置爲0 ,然後再次開始新一輪的檢查工作。

6. AOF, RDB 和複製功能對過期鍵的處理

探討過期鍵對Redis 服務器中其他模塊的影響,看看RDB 持久化功能、AOF 持久化功能以及複製功能是如何處理數據庫中的過期鍵的。

1.生成RDB 文件
在執行SAVE 命令或者BGSAVE 命令創建一個新的RDB 文件時,程序會對數據庫中的鍵進行檢查,已過期的鍵不會被保存到新創建的RDB 文件中。因此,數據庫中包含過期鍵不會對生成新的RDB 文件造成影響

2.載入RDB 文件
在啓動Redis 服務器時,如果服務器開啓了RDB 功能,那麼服務器將對RDB 文件進行載入:

  • 如果服務器以主服務器模式運行,,程序會對文件中保存的鍵進行檢查,未過期的鍵會被載入到數據庫過期鍵則會被忽略,所以過期鍵對載入RDB 文件的主服務器不會造成影響。
  • 如果服務器以從服務器模式運行,文件中保存的所有鍵不論是否過期,都會被載入到數據庫中因爲主從服務器在進行數據同步的時候,從服務器的數據庫就會被清空,所以一般來講,過期鍵對載人RDB 文件的從服務器也不會造成影響。

3.AOF 文件寫入
當服務器以AOF 持久化模式運行時,

  • 如果數據庫中的某個鍵已經過期,但它還沒有被惰性刪除或者定期刪除,那麼AOF 文件不會因爲這個過期鍵而產生任何影響。
  • 當過期鍵被惰性刪除或者定期刪除之後,程序會向AOF 文件追加( append )一條DEL命令,來顯式地記錄該鍵已被刪除。
    舉個例子,如果客戶端使用GET message 命令,試圖訪問過期的message 鍵,那麼服務器將執行以下三個動作:
    1 )從數據庫中刪除message 鍵。
    2 )追加一條DEL message 命令到AOF 文件。
    3 )向執行GET命令的客戶端返回空回覆。

4. AOF 重寫
和生成RDB 文件時類似,在執行AOF 重寫的過程中,程序會對數據庫中的鍵進行檢查,已過期的鍵不會被保存到重寫後的AOF 文件中。因此,數據庫中包含過期鍵不會對AOF 重寫造成影響。

5.複製
當服務器運行在複製模式下時,從服務器的過期鍵刪除動作由主服務器控制:

  • 主服務器在刪除一個過期鍵之後,會顯式地向所有從服務器發送一個DEL 命令,告知從服務器刪除這個過期鍵。
  • 從服務器只有在接到主服務器發來的DEL 命令之後,纔會刪除過期鍵。
  • 從服務器在執行客戶端發送的讀命令時,即使碰到過期鍵也不會將過期鍵刪除,而是繼續像處理未過期的鍵一樣來處理過期鍵。
    因此可以保證主從服務器數據的一致性,也正是因爲這個原因,當一個過期鍵仍然存在於主服務器的數據庫時,這個過期鍵在從服務器裏的複製品也會繼續存在。

例如:
如果這時有客戶端向從服務器發送命令GET message ,那麼從服務器將發message鍵已經過期,但從服務器並不會刪除message 鍵,而是繼續將message 鍵的值返回給客戶端,就好像message 鍵並沒有過期一樣;
假設在此之後,有客戶端向主服務器發送命令GET message ,那麼主服務器將發現鍵message 已經過期:主服務器會刪除message 鍵,向客戶端返回空回覆,並向從服務器發送DEL message 命令;從服務器在接收到主服務器發來的DEL message 命令之後,也會從數據庫中刪除message 鍵,在這之後,主從服務器都不再保存過期鍵message 了。

6. 數據庫通知

這個功能可以讓客戶端通過訂閱給定的頻道或者模式,來獲知數據庫中鍵的變化,以及數據庫中命令的執行情況。
舉個例子,以下代碼展示了客戶端如何獲取 0 號數據庫中針對message 鍵執行的所有命令:
SUBSCRIBE_ _keyspace@0_ _ :message#關注鍵空間:message 鍵
根據發回的通知顯示,先後共有SET、EXPIRE、DEL 三個命令對鍵message 進行了操作。
這一類關注“某個鍵執行了什麼命令”的通知稱爲鍵空間通知( key-space notification),除此之外,還有另一類稱爲鍵事件通知( key-event notification )的通知,它們關注的是“某個命令被什麼鍵執行了”。
以下是一個鍵事件通知的例子,代碼展示了客戶端如何獲取0 號數據庫中所有執行DEL 命令的鍵:
SUBSCRIBE _ _ keyevent@0 _ _ :del#關注命令:del
根據發回的通知顯示, key 、number 、message 三個鍵先後執行了DEL 命令。

服務器配置的notify-keyspace-events 選項決定了服務器所發送通知的類型:

  • 想讓服務器發送所有類型的鍵空間通知和鍵事件通知,可以將選項的值設置爲AKE。
  • 想讓服務器發送所有類型的鍵空間通知,可以將選項的值設置爲AK。
  • 想讓服務器發送所有類型的鍵事件通知,可以將選項的值設置爲AE。
  • 想讓服務器只發送和字符串鍵有關的鍵空間通知,可以將選項的值設置爲K$。
  • 想讓服務器只發送和列表鍵有關的鍵事件通知,可以將選項的值設置爲El。
    (一)發送通知
    發送數據庫通知的功能是由notify.c/notifyKeyspaceEvent 函數實現的:
void notifyKeyspaceEvent (int type, char *event, robj *key, int dbid);

函數的 type 參數是當前想要發送的通知的類型,程序會根據這個值來判斷通知是否就是服務器配置notify-keyspace-events 選項所選定的通知類型,從而決定是否發送通知。
event 、keys 和dbid 分別是事件的名稱、產生事件的鍵,以及產生事件的數據庫號
碼,函數會根據type 參數以及這三個參數來構建事件通知的內容,以及接收通知的頻道名。

每當一個Redis 命令需要發送數據庫通知的時候,該命令的實現畫數就會調用notifyKeyspaceEvent函數,並向函數傳遞傳遞該命令所引發的事件的相關信息。
例如SADD 命令的實現函數和DEL 命令的實現函數,

  • SADD 命令:當SADD 命令至少成功地向集合添加了一個集合元素之後,命令就會發送通知
  • DEL 命令:函數遍歷所有輸入鍵,並在刪除鍵成功時,發送通知
    (二)notifyKeyspaceEvent函數的實現
    僞代碼:
def notifyKeyspaceEvent(type, event, key, dbid):
    #如果給定的通知不是服務暴允許發送的通知,那麼直接返回
    if not(server.notify_keyspace_events & type):
        return
    #發送鍵空間通知
    if server.notify_keyspace_events & REDIS_NOTIFY_KEYSPACE:
        #將通知發送給頻道_keyspace@<dbid>_:<key>
        #內容爲鍵所發生的事件<event>
        #構建頻道名字
        chan =”__keyspace@{dbid}_: {key)” .format(dbid=dbid, key=key)
        #發送通知
        pubsubPublishMessage(chan, event)
    #發送鍵事件通知
    if server.notify_keyspace_events & REDIS_NOTIFY_KEYEVENT:
        #將通知發送給頻道_keyevent@<dbid>__:<event>
        #內容爲發生事件的鍵<key>
        #構建頻道名字
        chan =__keyevent@{dbid}_: {event}".format(dbid=dbid,event=event)
        #發送通知
        pubsubPublishMessage (chan, key)

1)server.notify keys pace_ events 屬性就是服務器配置 notify-keyspaceevents 選項所設置的值,如果給定的通知類型 type 不是服務器允許發送的通知類型,那麼函數會直接返回,不做任何動作。
2 )如果給定的通知是服務器允許發送的通知,那麼下一步函數會檢測服務器是否允許發送鍵空間通知,如果允許的話,程序就會構建併發送事件通知。
3 )最後,函數檢測服務器是否允許發送鍵事件通知,如果允許的話,程序就會構建併發送事件通知。

  • Redis 服務器的所有數據庫都保存在redisServer.db 數組中,而數據庫的數量則
    由redisServer.dbnum 屬性保存。
  • 客戶端通過修改目標數據庫指針,讓它指向redisServer.db 數組中的不同元素
    來切換不同的數據庫e
  • 數據庫主要由diet 和expires 兩個字典構成,其中diet 字典負責保存鍵值對,
    而expires 字典則負責保存鍵的過期時間。
  • 因爲數據庫由字典構成,所以對數據庫的操作都是建立在字典操作之上的。
  • 數據庫的鍵總是一個字符串對象,而值則可以是任意一種Redis 對象類型,包括字
    符串對象、晗希表對象、集合對象、列表對象和有序集合對象,分別對應字符鍵、哈希表鍵、集合鍵、列表鍵和有序集合鍵。
  • expires 字典的鍵指向數據庫中的某個鍵,而值則記錄了數據庫鍵的過期時間,過
    期時間是一個以毫秒爲單位的UNIX 時間戳。
  • Redis 使用惰性刪除和定期刪除兩種策略來刪除過期的鍵:惰性刪除策略只在碰到過期鍵時才進行刪除操作,定期刪除策略則每隔一段時間主動查找並刪除過期鍵。
  • 執行SAVE 命令或者BGSAVE 命令所產生的新RDB 文件不會包含已經過期的鍵。
  • 執行BGREWRITEAOF 命令所產生的重寫AOF 文件不會包含已經過期的鍵。
  • 當一個過期鍵被刪除之後,服務器會追加一條DEL 命令到現有AOF 文件的末尾,
    顯式地刪除過期鍵。
  • 當主服務器刪除一個過期鍵之後,它會向所有從服務器發送一條DEL 命令,顯式地
    刪除過期鍵。
  • 從服務器即使發現過期鍵也不會自作主張地刪除它,而是等待主節點發來DEL 命令,這種統一、中心化的過期鍵刪除策略可以保證主從服務器數據的一致性。
  • 當Redis 命令對數據庫進行修改之後,服務器會根據配置向客戶端發送數據庫通知。
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章