Redis學習(原理篇)

前言

讀完《Redis設計與實現》這本書之後,感覺講得很好很詳細,特此進行一些常用點的記錄總結,以供之後複習回顧。

對象

Redis的主要數據結構是簡單動態字符串SDS、雙端鏈表、字典、壓縮列表、整數集合、跳躍表(分別對應Redis數據類型String、List、Hash、Set和ZSet的底層實現),但是Redis並沒有直接使用這些數據結構來實現鍵值對數據庫,而是基於這些數據結構創建一個對象系統,這個對象系統中包含字符串對象、列表對象、哈希對象、集合對象和有序集合對象五種類型的對象,並且每種對象都用到了至少一種數據結構(Redis不同數據類型數據以不同類型對象的形式分別使用至少一種數據結構存儲於內存中構成數據庫)【即給一個數據類型相當於new了一個對象,這個對象類型根據數據類型來定,數據存儲的格式根據數據的類型和大小定,主要定的就是最終的存儲數據結構】

通過不同類型的對象,Redis可以在執行命令前先根據對象的類型(type)來判斷這個對象是否可以執行給定的命令。

對象系統的作用如下:

  • Redis在執行命令前,根據對象類型判斷對象是否可執行給定的命令(類型檢查)
  • 可針對不同的使用場景,爲對象設置多種不同的數據結構實現從而優化對象在不同場景下的使用效率(多態實現)
  • 實現了基於引用計數技術的內存回收機制,當程序不再使用某個對象時,這個對象佔用的內存會被自動釋放
  • 實現了基於引用計數技術實現對象共享機制,該機制在適當條件下可通過讓多個數據庫鍵共享同一個對象來節約內存
  • 對象帶有訪問時間記錄信息,用於計算數據庫鍵的空轉時長(當前時間-時間記錄變量)。在服務器啓用maxmemory功能情況下,空轉時長較大的鍵可能優先會被服務器刪除

Redis數據庫中的每個鍵值對中的鍵和值都是以對象形式定義存儲,其中對象主要通過type、encoding、ptr、refcount和lru屬性進行定義不同類型不同編碼方式(對應不同存儲結構不同命令實現方式),基於type在命令執行前進行類型檢查、基於encoding對於不同編碼方式的對象同一命令有不同實現方式(多態的一種體現方式,另一種是基本鍵類型同一命令對於不同類型對象鍵實現方式不同)、基於refcount引用計數技術共享對象(Redis自身會創建1-9999的字符串對象用於共享)和定期回收內存(每訪問一次,這個內存的refcount++)、基於lru可以計算對象的空轉時間並定期清除空轉時間長的鍵以釋放內存。

數據庫

Redis服務器將所有數據庫保存在服務器狀態redis.h/redisServer結構的db數組中,db數組的每一項都是一個redis.h/redisDb結構,每個redisDb結構代表一個數據庫(字典)。其中redisServer中有一個屬性dbnum作用是在初始化服務器時決定應該創建多少個數據庫,dbnum屬性值由服務器配置的database選項決定,默認爲16即Redis服務器默認會創建16個數據庫。

每個Redis客戶端都有自己的目標數據庫,每當客戶端執行數據庫寫/讀命令時,目標數據庫會成爲這些命令的操作對象。默認情況下Redis客戶端的目標數據庫爲0號數據庫,但客戶端可通過執行SELECT命令來切換目標數據庫。

在服務器內部,客戶端狀態redisClient結構的db屬性記錄了客戶端當前的目標數據庫,這個屬性是一個指向redisDb結構的指針,指向redisServer.db數組中的其中一個元素即客戶端的目標數據庫。

對Redis數據的CRUD操作即對數據庫db[n]下字典結構中對應數據的CRUD操作。Redis是一個鍵值對(key-value pair)數據庫服務器,服務器中的每個數據庫都由一個redis.h/redisDb結構表示。其中redisDb結構的dict字典保存數據庫中的所有鍵值對,也稱這個dict字典爲鍵空間(key space)。【因爲Redis數據庫的鍵空間是一個字典,故所有對數據庫的CRUD操作(如添加一個鍵值對到數據庫;從數據庫中刪除一個鍵值對;在數據庫中獲取某個鍵值對等)都是通過對鍵空間字典進行操作實現的。】

  • 添加一個新鍵值對到數據庫實際上就是將一個新鍵值對添加到鍵空間字典中,其中鍵爲字符串對象,值爲任意一種類型的Redis對象(String、List、Hash、Set、ZSet)。
  • 刪除數據庫中的一個鍵實際上是在鍵空間字典中刪除鍵所對應的鍵值對對象(刪除整個鍵+值)。

  • 對一個數據庫鍵更新實際上是對鍵空間字典中鍵所對應的值對象進行更新,根據值對象的類型不同更新的具體方法不同。(Hash數據類型中hset 同一個key 不同field 不同value表示對這個key進行更新,實際上是在key下添加一個新的field-value鍵值對)

  • 對一個數據庫鍵取值實際上是在鍵空間中取出鍵所對應的值對象,根據值對象類型不同取值方法有所不同。

  • FLUSHDB:通過刪除鍵空間中的所有鍵值對實現

  • RANDOMKEY:通過在鍵空間中隨機返回一個鍵實現(而非鍵對應的值)

  • DBSIZE:通過返回鍵空間中包含的鍵值對的數量實現

Redis服務器實際中使用的是惰性刪除和定期刪除兩種策略,通過配合使用這兩種策略,服務器可很好合理使用CPU時間和避免浪費內存空間間取得平衡。

 

發佈/訂閱

當一個客戶端執行SUBSCRIBE命令訂閱某個或某些頻道時,這個客戶端與被訂閱頻道間建立起一種訂閱關係。

Redis將所有頻道的訂閱關係都保存在服務器狀態即redisServer結構pubsub_channels字典中,這個字典的鍵是某個被訂閱的頻道,鍵的值則是一個鏈表,鏈表中記錄了所有訂閱這個頻道的客戶端

每當客戶端執行SUBSCRIBE命令訂閱某個/某些頻道時,服務器會將客戶端與被訂閱的頻道在pubsub_channels字典(頻道有了客戶端訂閱後即有了訂閱者後纔會出現在pubsub_channels中,如果只是服務器發佈了頻道但是無人訂閱這個頻道並不會存入pubsub_channels字典中)中進行關聯。根據頻道是否已經有其他訂閱者,關聯操作可分爲兩種情況進行:

  • 如果頻道已經有其他訂閱者,那麼其在pubsub_channels字典中必定有相應的訂閱者鏈表,程序需要做的就是將新的訂閱者(客戶端)鍵入訂閱者鏈表的末尾即可
  • 如果頻道中無其他訂閱者,那麼必然不存在於pubsub_channels字典中。程序首先在pubsub_channels字典中爲頻道創建一個鍵,鍵名爲頻道名,設置該鍵的值爲空鏈表,然後將客戶端添加到鏈表成爲鏈表的第一個元素

客戶端使用UNSUBSRCIBE命令退訂頻道,操作與SUBSCRIBE恰恰相反,當一個客戶端退訂某個/某些頻道時,服務器將從pubsub_channels中解除客戶端與被退訂頻道之間的關聯。

  • 程序根據被退訂頻道的名字,在pubsub_channels字典中找到頻道對應的訂閱者鏈表,然後從鏈表中刪除退訂客戶端的信息
  • 如果刪除客戶端信息後,頻道的訂閱者鏈表成爲空鏈表,則說明這個頻道此時已無訂閱者,程序將從pubsub_channels字典中刪除頻道對應的鍵

 

服務器將所有頻道的訂閱關係保存在服務器狀態的pubsub_channels屬性中(該屬性是一個字典結構),類似的服務器將所有模式的訂閱關係保存在服務器狀態的pubsub_patterns屬性中(該屬性是一個鏈表結構)。

每當客戶端執行PSUBSCRIBE命令訂閱某個/某些模式時,服務器會對每個被訂閱的模式執行兩個操作。

  • 新建一個pubsubPattern結構,將結構中的pattern屬性設置爲被訂閱的模式,client屬性設置爲訂閱模式的客戶端
  • 將pubsubPattern結構添加到pubsub_patterns鏈表的表尾

客戶端退訂模式使用PUNSUBSCRIBE命令,操作與PSUBSCRIBE命令相反。當一個客戶端退訂某個/某些模式時,服務器將在pubsub_patterns鏈表中查找並刪除那些pattern屬性爲被退訂模式,且client屬性爲執行退訂命令的客戶端的pubsubPattern結構(根據pattern和client查詢pubsub_patterns鏈表中的目標pubsubPattern結構並刪除)

當一個Redis客戶端執行PUBLISH <channel> <message>命令將消息message發送給頻道channel時,服務器需要執行兩個操作,分別是發送給頻道的所有訂閱者和發送給匹配該頻道的所有模式的所有訂閱者。

Redis2.8後新增PUBSUB命令,客戶端可通過此命令來查看頻道/模式的相關信息(如訂閱的某頻道/模式當前有多少訂閱者)。其中PUBSUB命令有三個子命令PUBSUB CHANNELS、PUBSUB NUMSUB和PUBSUB NUMPAT,這三個子命令都是通過讀取pubsub_channels字典和pubsub_patterns鏈表中的信息來實現的。

【注:PUBSUB CHANNELS通過讀取pubsub_channels字典中的鍵實現;PUBSUB NUMSUB通過讀取pubsub_channels字典中的某個鍵對應的鏈表長度實現;PUBSUB NUMPAT通過讀取pubsub_patterns鏈表的長度實現】

持久化

Redis的RDB持久化方式就是要保存Redis的數據庫狀態,即將非空數據庫及其鍵值對保存在硬盤上以確保數據的完備性(Redis是內存數據庫,這些數據庫狀態時存儲在內存中的,所以需要持久化機制來防止服務器宕機後服務器進程退出導致服務器中的數據庫狀態消失從而造成損失)。

Redis默認將快照文件存儲在Redis當前進程的工作目錄中的dump.rdb文件中,可通過配置dir和dbfilename分別制定快照文件的存儲路徑和文件名。過程如下:

  • Redis使用fork函數(創建進程)複製一份當前進程(父進程)的副本(子進程)
  • 父進程繼續接收並處理客戶端發來的命令,子進程開始將內存中的數據寫入硬盤中的臨時文件
  • 當子進程寫入完所有數據後會用該臨時文件替換舊的RDB文件,至此一次快照操作完成(新的RDB文件中存儲的是fork時的內存數據)

即先創建子進程,子進程將存儲內存數據的臨時文件寫入硬盤,寫入完畢後臨時文件替換硬盤中原有的RDB(子進程寫入硬盤緩衝區,然後持久化到硬盤並替換硬盤中的舊RDB)

AOF持久化主要是通過保存當前數據庫的寫命令到硬盤上完成的(append、write、sync),當數據恢復的時候服務器直接從硬盤上將AOF文件載入並讀取執行其中的寫命令即可恢復數據庫狀態。因爲AOF文件是存放寫命令並且以協議格式存放,隨寫命令越來越多文件體積會越來越大,爲減輕硬盤負擔Redis支持定期AOF文件重寫,重寫主要是服務器進程開一個子進程給一個數據副本並且將副本中數據庫狀態轉爲增命令保存在重寫的AOF文件中,爲確保重寫前後數據庫狀態一致會設置一個AOF重寫緩衝區,當重寫完畢後服務器暫停對客戶端的服務去將AOF重寫緩衝區的寫命令追加到重寫的AOF文件中並覆蓋掉硬盤上原有的AOF文件完成重寫(開子進程-子進程重寫-父進程將新寫命令發送到AOF重寫緩衝區-追加到AOF文件-覆蓋原AOF文件)【重寫過程中AOF持久化會繼續保持,所以新增寫命令也會發送到AOF緩衝區,等待flushAppendOnlyFile函數啓動進行文件寫入和同步】。

複製

Redis中用戶可通過執行SLAVEOF命令或設置slaveof選項,讓一個服務器去複製(replicate)另一個服務器,前者爲從服務器,後者爲主服務器。經複製操作後主從服務器的數據庫狀態保持一致,並且寫操作也會同步(例如對主服務器執行寫操作即增/改/刪操作,從服務器也會執行由主服務器發過來的增/改/刪命令)。

在Redis2.8版本以前,複製功能分爲同步(sync)和命令傳播(command propagate)兩個操作。

爲解決舊版複製功能在處理斷線重複制情況的低效問題,Redis從2.8版本後使用PSYNC替代SYNC命令來執行復制時的同步操作。

PSYNC命令分爲完整重同步(full resynchronization)和部分重同步(partial resynchronization)兩種模式。

事務

Redis通過MULTI、EXEC、WATCH等命令實現事務(transaction)功能,事務提供一種將多個命令打包然後一次性、按順序執行的機制,並且在事務執行(處於事務狀態的客戶端執行EXEC命令後)期間,服務器不會中斷事務,會將事務中的所有命令都執行完畢纔去處理來自客戶端的命令請求(此時客戶端發送來的命令請求應該存儲在客戶端的輸入緩衝區中等待單進程的服務器進程空閒後進行讀取處理)。

【注:在一個客戶端執行MULTI命令後,此客戶端從非事務狀態切換爲事務狀態,之後的命令都被服務器發送到該客戶端放入事務隊列中等待執行(此時其他非事務狀態的客戶端正常發送請求服務器正常執行命令)。等處於事務狀態的客戶端執行EXEC命令後,服務器暫停與其他客戶端的交互專一執行事務隊列中的命令,執行完畢後此客戶端轉爲非事務狀態,服務器恢復與客戶端的交互(類似於排隊的暫時離開,等回來以後就優先執行暫時離開的)】

一個事務從開始到結束通常經歷三個階段,分別是

  • 事務開始:MULTI命令標誌事務的開始。MULTI命令將執行該命令的客戶端從非事務狀態切換至事務狀態(此轉換通過在客戶端狀態的flags屬性中打開REDIS_MULTI標識完成)
  • 命令入隊:當一個客戶端處於非事務狀態時,發送的請求命令會立即被服務器執行。但是當客戶端處於事務狀態時,服務器會根據這個客戶端發來不同命令執行不同操作。若客戶端發送EXEC、DISCARD、WATCH、MULTI命令之一,服務器立即執行命令;若客戶端發送其他命令,服務器並不立即執行而是將其放入一個事務隊列(FIFO)(保存於這個處於事務狀態的客戶端的multiState事務狀態結構中)中,然後向客戶端返回QUEUED回覆
  • 事務執行:當處於事務狀態的客戶端向服務器發送EXEC命令時,EXEC命令立即被服務器(服務器進程會暫停當前的普通與客戶端請求命令處理任務)執行。執行過程是:服務器遍歷這個客戶端的事務隊列(commands指針),執行隊列中保存的所有命令(multiCmd數組),最後將執行結果全部返回(創建一個回覆隊列存儲命令執行結果)給此客戶端

WATCH命令是個樂觀鎖optimistc locking,可以在EXEC命令執行前監視任意數量的數據庫鍵,在EXEC命令執行期間檢查被監視的鍵(事務隊列命令中包含的鍵)是否有至少一個已經被修改,如果有則服務器拒絕執行事務,並向客戶端返回代表事務執行失敗的空回覆(nil).

Redis數據庫保存一個watched_keys字典,字典的鍵是被WATCH命令監視的數據庫鍵,字典的值爲一個鏈表,這個鏈表記錄所有監視相應數據庫鍵的客戶端。(執行watch 鍵名1 命令意味着在數據庫的watched_keys字典中會添加一個鍵名=鍵名1,值爲一個包含執行這條命令的客戶端名的鏈表;如果整個鍵名本身存在,則直接將整個客戶端添加至鏈表表尾即可表示客戶端使用WATCH命令對鍵的監視

在Redis中所有對數據庫狀態(鍵值對)進行修改的命令(如SET、LPUSH、RPUSH、HSET、HMSET、LPOP、RPOP、ZADD、SADD、ZREM、DEL、FLUSHDB等),執行調用multi.c/touchWatchKey函數對watched_keys字典進行檢查,查看是否有客戶端正在監視剛剛被命令修改過的數據庫鍵,如果有touchWatchKey函數會將監視被修改鍵的客戶端的REDIS_DIRTY_CAS標識打開,以此表示該客戶端的事務安全性已經被破壞。當服務器接收到客戶端發送來的EXEC命令時,服務器會根據這個客戶端是否打開了REDIS_DIRTY_CAS標識來決定是否執行事務(如果打開則表示客戶端所監視的鍵中至少有一個鍵已經被修改,事務已經不再安全,服務器會拒絕執行該客戶端提交的事務;如果沒有被打開則表示客戶端所監視的鍵沒有鍵被修改(或者客戶端對事務中的鍵沒有執行WATCH監視命令),事務依舊安全,服務器將正常執行該客戶端提交的事務)

【注:每對某個鍵執行一個寫命令,命令執行完畢後(這個命令肯定是非事務狀態的客戶端執行的,因爲處於事務狀態的客戶端的命令都會寫入事務隊列中等待一次性順序處理)就會去調用touchWatchKey函數去檢查數據庫中watched_keys字典中是否有此鍵,如果有則遍歷打開對應鏈表中客戶端的REDIS_DIRTY_CAS標識表示該客戶端事務安全性已被破壞】

總結

此次對於Redis的概念、特點、適用場景、數據類型及底層存儲、數據庫、發佈/訂閱、複製、持久化、事務以及一些位運算、排序、慢日誌等高級功能進行較爲認真的學習,此外後續還需要對Redis的客戶端、服務端、集羣、Sentinel哨兵模式進行進一步的學習,如有機會根據遇到的實際問題結合理論知識進行總結。

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章