《Redis設計與實現》讀書筆記

大CC發表於2014-08-11

《Redis設計與實現》讀書筆記

很喜歡這本書的創作過程,以開源的方式,託管到Git上進行創作;
作者通讀了Redis原始碼,並分享了詳細的帶註釋的原始碼,讓學習Redis的朋友輕鬆不少;
閱讀優秀的原始碼作品能快速的提升編碼內功,而像Redis這樣程式碼量不大(2萬多行)卻句句精緻的作品,當然不能錯過;
有興趣的朋友當好好享用;
原始碼:https://github.com/huangz1990/annotated_redis_source

以下是這本書重點環節的讀書筆記;

Redis的內部字串實現

Redis 使用自行實現的sds 型別來表示字串:
原因:可以高效地實現追加和長度計算,並且它還是二進位制安全的。
在 Redis 內部,字串的追加和長度計算並不少見,而 APPEND 和 STRLEN 更是這兩種操作在 Redis 命令中的直接對映,這兩個簡單的操作不應該成為效能的瓶頸。另外,Redis 除了處理 C 字串之外,還需要處理單純的位元組陣列,以及伺服器協議等內容,所以為了方便起見,Redis 的字串表示還應該是二進位制安全的:
程式不應對字串裡面儲存的資料做任何假設,資料可以是以 \0 結尾的 C 字串,也可以是單純的位元組陣列,或者其他格式的資料。
關於sds的詳情介紹,參見:
http://origin.redisbook.com/en/latest/internal-datastruct/sds.html#sds

內部對映資料結構和記憶體資料結構的區別

記憶體對映資料結構:整數集合、壓縮連結串列
內部資料結構:簡單字串(sds)、雙端連結串列、字典、跳躍表
內部資料結構非常強大,但是建立一系列完整的資料結構本身也是一件相當耗費記憶體的工作,當一個物件包含的元素數量並不多,或者元素本身的體積並不大時,使用代價高昂的內部資料結構並不是最好的辦法。為了解決這一問題,Redis 在條件允許的情況下,會使用記憶體對映資料結構來代替內部資料結構。記憶體對映資料結構是一系列經過特殊編碼的位元組序列,建立它們所消耗的記憶體通常比作用類似的內部資料結構要少得多,如果使用得當,記憶體對映資料結構可以為使用者節省大量的記憶體。不過,因為記憶體對映資料結構的編碼和操作方式要比內部資料結構要複雜得多,所以記憶體對映資料結構所佔用的 CPU 時間會比作用類似的內部資料結構要多。

集合求並與求交集

集合好用,redis集合支援求交集,求並集操作,讓集合的應用範圍大幅提升;
但是,需要注意到,求並集的演算法複雜度是O(N),而求交集的演算法複雜度為O(N的平方),在設計集合儲存策略的時候還是儘量少用交集運算;

事務的 ACID 性質

在傳統的關係式資料庫中,常常用 ACID 性質來檢驗事務功能的安全性。Redis 事務保證了其中的一致性(C)和隔離性(I),但並不保證原子性(A)和永續性(D)。
單個 Redis 命令的執行是原子性的,但 Redis 沒有在事務上增加任何維持原子性的機制,所以Redis 事務的執行並不是原子性的。如果一個事務佇列中的所有命令都被成功地執行,那麼稱這個事務執行成功。另一方面,如果 Redis 伺服器程式在執行事務的過程中被停止——比如接到 KILL 訊號、宿主機器停機,等等,那麼事務執行失敗。當事務失敗時,Redis 也不會進行任何的重試或者回滾動作。
因為事務不過是用佇列包裹起了一組 Redis 命令,並沒有提供任何額外的永續性功能,所以事務的永續性由 Redis 所使用的持久化模式決定。
詳見:http://origin.redisbook.com/en/latest/feature/transaction.html#acid

支援Lua指令碼

Lua 指令碼功能是 Reids 2.6 版本的最大亮點,通過內嵌對 Lua 環境的支援,Redis 解決了長久以來不能高效地處理 CAS(check-and-set)命令的缺點,並且可以通過組合使用多個命令,輕鬆實現以前很難實現或者不能高效實現的模式。

Lua指令碼與Redis間通過偽終端互動

因為 Redis 命令必須通過客戶端來執行,所以需要在伺服器狀態中建立一個無網路連線的偽客戶端(fake client),專門用於執行 Lua 指令碼中包含的 Redis 命令:當 Lua 指令碼需要執行 Redis 命令時,它通過偽客戶端來向伺服器傳送命令請求,伺服器在執行完命令之後,將結果返回給偽客戶端,而偽客戶端又轉而將命令結果返回給 Lua 指令碼。
注 這個偽客戶端是無網路連線的,那是如何和redis通訊的麼?是在一個程式中?

消除指令碼的執行的隨機性

和隨機性質類似,如果一個指令碼的執行對任何副作用產生了依賴,那麼這個指令碼每次執行所產生的結果都可能會不一樣。為了解決這個問題,Redis 對 Lua 環境所能執行的指令碼做了一個嚴格的限制——所有指令碼都必須是無副作用的純函式(pure function)。為此,Redis 對 Lua 環境做了一些列相應的措施:
• 不提供訪問系統狀態狀態的庫(比如系統時間庫)。
• 禁止使用 loadfile 函式。
• 如果指令碼在執行帶有隨機性質的命令(比如 RANDOMKEY ),或者帶有副作用的命令(比如 TIME )之後,試圖執行一個寫入命令(比如 SET ),那麼 Redis 將阻止這個指令碼繼續執行,並返回一個錯誤。
• 如果指令碼執行了帶有隨機性質的讀命令(比如 SMEMBERS ),那麼在指令碼的輸出返回給Redis 之前,會先被執行一個自動的字典序排序,從而確保輸出結果是有序的。
• 用 Redis 自己定義的隨機生成函式,替換 Lua 環境中 math 表原有的 math.random 函式和 math.randomseed 函式,新的函式具有這樣的性質:每次執行 Lua 指令碼時,除非顯式地呼叫 math.randomseed ,否則 math.random 生成的偽隨機數序列總是相同的。

鍵的過期時間

通過 EXPIRE 、PEXPIRE 、EXPIREAT 和 PEXPIREAT 四個命令,客戶端可以給某個存在的鍵設定過期時間,當鍵的過期時間到達時,鍵就不再可用;
當儲存的鍵用於快取時,通常我們需要設定一個過期時間,到期後由redis刪除;
一般為兩步:
SET key value
EXPIRE key seconds
有了SETEX,只需要一步就可實現設定值和過期時間:
SETEX key seconds value
進一步想,如果所有的往資料庫中增加值的命令都有相應的設定過期時間的函式,豈不是很美好?當然,想歸想,實際並非如此,除了SET有SETEX,其它的如集合操作SADD,都沒有這樣的一步操作命令;

過期鍵的清除

如果一個鍵是過期的,那它什麼時候會被刪除?
這個問題有三種可能的答案:

  1. 定時刪除:在設定鍵的過期時間時,建立一個定時事件,當過期時間到達時,由事件處理器自動執行鍵的刪除操作。
  2. 惰性刪除:放任鍵過期不管,但是在每次從 dict 字典中取出鍵值時,要檢查鍵是否過期,如果過期的話,就刪除它,並返回空;如果沒過期,就返回鍵值。
  3. 定期刪除:每隔一段時間,對 expires 字典進行檢查,刪除裡面的過期鍵;定期刪除是這兩種策略的一種折中:
    • 它每隔一段時間執行一次刪除操作,並通過限制刪除操作執行的時長和頻率,籍此來減少刪除操作對 CPU 時間的影響。
    • 另一方面,通過定期刪除過期鍵,它有效地減少了因惰性刪除而帶來的記憶體浪費。

定時刪除和惰性刪除這兩種刪除方式在單一使用時都有明顯的缺陷:定時刪除佔用太多 CPU 時間,惰性刪除浪費太多記憶體;
Redis 使用的過期鍵刪除策略是惰性刪除加上定期刪除, 這兩個策略相互配合,可以很好地在合理利用 CPU 時間和節約記憶體空間之間取得平衡。
參考:http://origin.redisbook.com/en/latest/internal/db.html#id20

RDB持久化

rdbSave 函式負責將記憶體中的資料庫資料以 RDB 格式儲存到磁碟中,如果 RDB 檔案已存在,那麼新的 RDB 檔案將替換已有的 RDB 檔案。在儲存 RDB 檔案期間,主程式會被阻塞,直到儲存完成為止。SAVE 和 BGSAVE 兩個命令都會呼叫 rdbSave 函式,但它們呼叫的方式各有不同:• SAVE 直接呼叫 rdbSave ,阻塞 Redis 主程式,直到儲存完成為止。在主程式阻塞期間,伺服器不能處理客戶端的任何請求。• BGSAVE 則 fork 出一個子程式,子程式負責呼叫 rdbSave ,並在儲存完成之後向主程式傳送訊號,通知儲存已完成。因為 rdbSave 在子程式被呼叫,所以 Redis 伺服器在BGSAVE 執行期間仍然可以繼續處理客戶端的請求。

SAVE 、 BGSAVE 、 AOF 寫入和 BGREWRITEAOF

當 SAVE 執行時,Redis 伺服器是阻塞的,所以當 SAVE 正在執行時,新的SAVE 、BGSAVE 或 BGREWRITEAOF 呼叫都不會產生任何作用。只有在上一個 SAVE 執行完畢、Redis 重新開始接受請求之後,新的 SAVE 、BGSAVE 或BGREWRITEAOF 命令才會被處理。另外,因為AOF寫入由後臺執行緒完成,而BGREWRITEAOF 則由子程式完成,所以在SAVE執行的過程中,AOF 寫入和 BGREWRITEAOF 可以同時進行。

執行 SAVE 命令之前,伺服器會檢查 BGSAVE 是否正在執行當中,如果是的話,伺服器就不呼叫 rdbSave ,而是向客戶端返回一個出錯資訊,告知在 BGSAVE 執行期間,不能執行SAVE 。這樣做可以避免 SAVE 和 BGSAVE 呼叫的兩個 rdbSave 交叉執行,造成競爭條件。另一方面,當 BGSAVE 正在執行時,呼叫新 BGSAVE 命令的客戶端會收到一個出錯資訊,告知 BGSAVE 已經在執行當中。
BGREWRITEAOF 和 BGSAVE 不能同時執行:
• 如果 BGSAVE 正在執行,那麼 BGREWRITEAOF 的重寫請求會被延遲到 BGSAVE 執行完畢之後進行,執行 BGREWRITEAOF 命令的客戶端會收到請求被延遲的回覆。
• 如果 BGREWRITEAOF 正在執行,那麼呼叫 BGSAVE 的客戶端將收到出錯資訊,表示這兩個命令不能同時執行。BGREWRITEAOF 和 BGSAVE 兩個命令在操作方面並沒有什麼衝突的地方,不能同時執行它們只是一個效能方面的考慮:併發出兩個子程式,並且兩個子程式都同時進行大量的磁碟寫入操作,這怎麼想都不會是一個好主意。

總的來說:
rdbSave 會將資料庫資料儲存到 RDB 檔案,並在儲存完成之前阻塞呼叫者。
• SAVE 命令直接呼叫 rdbSave ,阻塞 Redis 主程式;BGSAVE 用子程式呼叫 rdbSave ,主程式仍可繼續處理命令請求。
• SAVE 執行期間,AOF 寫入可以在後臺執行緒進行,BGREWRITEAOF 可以在子程式進行,所以這三種操作可以同時進行。
• 為了避免產生競爭條件,BGSAVE 執行時,SAVE 命令不能執行。
• 為了避免效能問題,BGSAVE 和 BGREWRITEAOF 不能同時執行

處理載入資料期間到達的請求

載入期間,伺服器每載入 1000 個鍵就處理一次所有已到達的請求,不過只有 PUBLISH 、SUBSCRIBE 、PSUBSCRIBE 、UNSUBSCRIBE 、PUNSUBSCRIBE 五個命令的請求會被正確地處理,其他命令一律返回錯誤。等到載入完成之後,伺服器才會開始正常處理所有命令。

AOF優於RDB

因為 AOF 檔案的儲存頻率通常要高於 RDB 檔案儲存的頻率,所以一般來說,AOF 檔案中的資料會比 RDB 檔案中的資料要新。因此,如果伺服器在啟動時,開啟了 AOF 功能,那麼程式優先使用 AOF 檔案來還原資料。只有在 AOF 功能未開啟的情況下,Redis 才會使用 RDB 檔案來還原資料。

AOF寫檔案的三階段

命令到 AOF 檔案的整個過程可以分為三個階段:

  1. 命令傳播:Redis 將執行完的命令、命令的引數、命令的引數個數等資訊傳送到 AOF 程式中。2. 快取追加:AOF 程式根據接收到的命令資料,將命令轉換為網路通訊協議的格式,然後將協議內容追加到伺服器的 AOF 快取中。
  2. 檔案寫入和儲存:AOF 快取中的內容被寫入到 AOF 檔案末尾,如果設定的 AOF 儲存條件被滿足的話,fsync 函式或者 fdatasync 函式會被呼叫,將寫入的內容真正地儲存到磁碟中。

AOF 儲存模式對效能和安全性的影響

redis 目前支援三種 AOF 儲存模式,它們分別是:

  1. AOF_FSYNC_NO :不儲存。
  2. AOF_FSYNC_EVERYSEC :每一秒鐘儲存一次。
  3. AOF_FSYNC_ALWAYS :每執行一個命令儲存一次。

三種 AOF 儲存模式,它們對伺服器主程式的阻塞情況如下:

  1. 不儲存(AOF_FSYNC_NO):寫入和儲存都由主程式執行,兩個操作都會阻塞主程式。
  2. 每一秒鐘儲存一次(AOF_FSYNC_EVERYSEC):寫入操作由主程式執行,阻塞主程式。儲存操作由子執行緒執行,不直接阻塞主程式,但儲存操作完成的快慢會影響寫入操作的阻塞時長。
  3. 每執行一個命令儲存一次(AOF_FSYNC_ALWAYS):和模式 1 一樣。因為阻塞操作會讓 Redis 主程式無法持續處理請求,所以一般說來,阻塞操作執行得越少、完成得越快,Redis 的效能就越好。

AOF 檔案的讀取和資料還原

模式 1 的儲存操作只會在 AOF 關閉或 Redis 關閉時執行,或者由作業系統觸發,在一般情況下,這種模式只需要為寫入阻塞,因此它的寫入效能要比後面兩種模式要高,當然,這種效能的提高是以降低安全性為代價的:在這種模式下,如果執行的中途發生停機,那麼丟失資料的數量由作業系統的快取沖洗策略決定。
模式 2 在效能方面要優於模式 3 ,並且在通常情況下,這種模式最多丟失不多於 2 秒的資料,所以它的安全性要高於模式 1 ,這是一種兼顧效能和安全性的儲存方案。
模式 3 的安全性是最高的,但效能也是最差的,因為伺服器必須阻塞直到命令資訊被寫入並儲存到磁碟之後,才能繼續處理請求。

AOF 後臺重寫

AOF 重寫程式可以很好地完成建立一個新 AOF 檔案的任務,但是,在執行這個程式的時候,呼叫者執行緒會被阻塞。很明顯,作為一種輔佐性的維護手段,Redis 不希望 AOF 重寫造成伺服器無法處理請求,所以Redis 決定將 AOF 重寫程式放到(後臺)子程式裡執行,這樣處理的最大好處是:

  1. 子程式進行 AOF 重寫期間,主程式可以繼續處理命令請求。
  2. 子程式帶有主程式的資料副本,使用子程式而不是執行緒,可以在避免鎖的情況下,保證資料的安全性。不過,使用子程式也有一個問題需要解決:因為子程式在進行 AOF 重寫期間,主程式還需要繼續處理命令,而新的命令可能對現有的資料進行修改,這會讓當前資料庫的資料和重寫後的AOF 檔案中的資料不一致。為了解決這個問題,Redis 增加了一個 AOF 重寫快取,這個快取在 fork 出子程式之後開始啟用,Redis 主程式在接到新的寫命令之後,除了會將這個寫命令的協議內容追加到現有的 AOF檔案之外,還會追加到這個快取中
    注 子程式與執行緒在訪問資料上的區別,難道不是都需加鎖麼
    ref:http://blog.csdn.net/wangkehuai/article/details/7089323

AOF 後臺重寫的觸發條件

子程式完成 AOF 重寫之後,它會向父程式傳送一個完成訊號,父程式在接到完成訊號之後,會呼叫一個訊號處理函式,並完成以下工作:

  1. 將 AOF 重寫快取中的內容全部寫入到新 AOF 檔案中。
  2. 對新的 AOF 檔案進行改名,覆蓋原有的 AOF 檔案。當步驟 1 執行完畢之後,現有 AOF 檔案、新 AOF 檔案和資料庫三者的狀態就完全一致了。當步驟 2 執行完畢之後,程式就完成了新舊兩個 AOF 檔案的交替。
    在整個 AOF後臺重寫過程中,只有最後的寫入快取和改名操作會造成主程式阻塞,在其他時候,AOF 後臺重寫都不會對主程式造成阻塞,這將 AOF 重寫對效能造成的影響降到了最低。

當 serverCron 函式執行時,它都會檢查以下條件是否全部滿足,如果是的話,就會觸發自動的 AOF 重寫:

  1. 沒有 BGSAVE 命令在進行。
  2. 沒有 BGREWRITEAOF 在進行。
  3. 當前 AOF 檔案大小大於 server.aof_rewrite_min_size (預設值為 1 MB)。
  4. 當前 AOF 檔案大小和最後一次 AOF 重寫後的大小之間的比率大於等於指定的增長百分比。預設情況下,增長百分比為 100% ,也即是說,如果前面三個條件都已經滿足,並且當前 AOF檔案大小比最後一次 AOF 重寫時的大小要大一倍的話,那麼觸發自動 AOF 重寫。

事件

事件是 Redis 伺服器的核心,它處理兩項重要的任務:

  1. 處理檔案事件:在多個客戶端中實現多路複用,接受它們發來的命令請求,並將命令的執行結果返回給客戶端。
  2. 時間事件:實現伺服器常規操作(server cron job)

檔案事件

Redis 伺服器通過在多個客戶端之間進行多路複用,從而實現高效的命令請求處理:多個客戶端通過套接字連線到 Redis 伺服器中,但只有在套接字可以無阻塞地進行讀或者寫時,伺服器才會和這些客戶端進行互動。

當伺服器有命令結果要返回客戶端,而客戶端又有新命令請求進入時,伺服器先處理新命令請求。

事件的執行與排程

Redis 裡面既有檔案事件,又有時間事件,那麼如何排程這兩種事件就成了一個關鍵問題。簡單地說,Redis 裡面的兩種事件呈合作關係,它們之間包含以下三種屬性:

  1. 一種事件會等待另一種事件執行完畢之後,才開始執行,事件之間不會出現搶佔。
  2. 事件處理器先處理檔案事件(處理命令請求),再執行時間事件(呼叫 serverCron)
  3. 檔案事件的等待時間(類 poll 函式的最大阻塞時間),由距離到達時間最短的時間事件決定。

說明:
• 時間事件分為單次執行事件和迴圈執行事件,伺服器常規操作 serverCron 就是迴圈事件。
• 檔案事件和時間事件之間是合作關係:一種事件會等待另一種事件完成之後再執行,不會出現搶佔情況。

命令的請求、處理和結果返回

Redis 以多路複用的方式來處理多個客戶端,為了讓多個客戶端之間獨立分開、不互相干擾,伺服器為每個已連線客戶端維持一個 redisClient 結構,從而單獨儲存該客戶端的狀態資訊。
當客戶端連上伺服器之後,客戶端就可以向伺服器傳送命令請求了。從客戶端傳送命令請求,到命令被伺服器處理、並將結果返回客戶端,整個過程有以下步驟:

  1. 客戶端通過套接字向伺服器傳送命令協議資料。
  2. 伺服器通過讀事件來處理傳入資料,並將資料儲存在客戶端對應 redisClient 結構的查詢快取中。
  3. 根據客戶端查詢快取中的內容,程式從命令表中查詢相應命令的實現函式。
  4. 程式執行命令的實現函式,修改伺服器的全域性狀態 server 變數,並將命令的執行結果儲存到客戶端 redisClient 結構的回覆快取中,然後為該客戶端的 fd 關聯寫事件。
  5. 當客戶端 fd 的寫事件就緒時,將回復快取中的命令結果傳回給客戶端。至此,命令執行完畢。

Posted by: 大CC | 11JUL,2014
部落格:blog.me115.com [訂閱]
微博:新浪微博

相關文章