《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,都沒有這樣的一步操作命令;
過期鍵的清除
如果一個鍵是過期的,那它什么時候會被刪除?
這個問題有三種可能的答案:
- 定時刪除:在設置鍵的過期時間時,創建一個定時事件,當過期時間到達時,由事件處理器自動執行鍵的刪除操作。
- 惰性刪除:放任鍵過期不管,但是在每次從 dict 字典中取出鍵值時,要檢查鍵是否過期,如果過期的話,就刪除它,並返回空;如果沒過期,就返回鍵值。
- 定期刪除:每隔一段時間,對 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 文件的整個過程可以分為三個階段:
- 命令傳播:Redis 將執行完的命令、命令的參數、命令的參數個數等信息發送到 AOF 程序中。2. 緩存追加:AOF 程序根據接收到的命令數據,將命令轉換為網絡通訊協議的格式,然后將協議內容追加到服務器的 AOF 緩存中。
- 文件寫入和保存:AOF 緩存中的內容被寫入到 AOF 文件末尾,如果設定的 AOF 保存條件被滿足的話,fsync 函數或者 fdatasync 函數會被調用,將寫入的內容真正地保存到磁盤中。
AOF 保存模式對性能和安全性的影響
redis 目前支持三種 AOF 保存模式,它們分別是:
- AOF_FSYNC_NO :不保存。
- AOF_FSYNC_EVERYSEC :每一秒鍾保存一次。
- AOF_FSYNC_ALWAYS :每執行一個命令保存一次。
三種 AOF 保存模式,它們對服務器主進程的阻塞情況如下:
- 不保存(AOF_FSYNC_NO):寫入和保存都由主進程執行,兩個操作都會阻塞主進程。
- 每一秒鍾保存一次(AOF_FSYNC_EVERYSEC):寫入操作由主進程執行,阻塞主進程。保存操作由子線程執行,不直接阻塞主進程,但保存操作完成的快慢會影響寫入操作的阻塞時長。
- 每執行一個命令保存一次(AOF_FSYNC_ALWAYS):和模式 1 一樣。因為阻塞操作會讓 Redis 主進程無法持續處理請求,所以一般說來,阻塞操作執行得越少、完成得越快,Redis 的性能就越好。
AOF 文件的讀取和數據還原
模式 1 的保存操作只會在 AOF 關閉或 Redis 關閉時執行,或者由操作系統觸發,在一般情況下,這種模式只需要為寫入阻塞,因此它的寫入性能要比后面兩種模式要高,當然,這種性能的提高是以降低安全性為代價的:在這種模式下,如果運行的中途發生停機,那么丟失數據的數量由操作系統的緩存沖洗策略決定。
模式 2 在性能方面要優於模式 3 ,並且在通常情況下,這種模式最多丟失不多於 2 秒的數據,所以它的安全性要高於模式 1 ,這是一種兼顧性能和安全性的保存方案。
模式 3 的安全性是最高的,但性能也是最差的,因為服務器必須阻塞直到命令信息被寫入並保存到磁盤之后,才能繼續處理請求。
AOF 后台重寫
AOF 重寫程序可以很好地完成創建一個新 AOF 文件的任務,但是,在執行這個程序的時候,調用者線程會被阻塞。很明顯,作為一種輔佐性的維護手段,Redis 不希望 AOF 重寫造成服務器無法處理請求,所以Redis 決定將 AOF 重寫程序放到(后台)子進程里執行,這樣處理的最大好處是:
- 子進程進行 AOF 重寫期間,主進程可以繼續處理命令請求。
- 子進程帶有主進程的數據副本,使用子進程而不是線程,可以在避免鎖的情況下,保證數據的安全性。不過,使用子進程也有一個問題需要解決:因為子進程在進行 AOF 重寫期間,主進程還需要繼續處理命令,而新的命令可能對現有的數據進行修改,這會讓當前數據庫的數據和重寫后的AOF 文件中的數據不一致。為了解決這個問題,Redis 增加了一個 AOF 重寫緩存,這個緩存在 fork 出子進程之后開始啟用,Redis 主進程在接到新的寫命令之后,除了會將這個寫命令的協議內容追加到現有的 AOF文件之外,還會追加到這個緩存中
注 子進程與線程在訪問數據上的區別,難道不是都需加鎖么
ref:http://blog.csdn.net/wangkehuai/article/details/7089323
AOF 后台重寫的觸發條件
子進程完成 AOF 重寫之后,它會向父進程發送一個完成信號,父進程在接到完成信號之后,會調用一個信號處理函數,並完成以下工作:
- 將 AOF 重寫緩存中的內容全部寫入到新 AOF 文件中。
- 對新的 AOF 文件進行改名,覆蓋原有的 AOF 文件。當步驟 1 執行完畢之后,現有 AOF 文件、新 AOF 文件和數據庫三者的狀態就完全一致了。當步驟 2 執行完畢之后,程序就完成了新舊兩個 AOF 文件的交替。
在整個 AOF后台重寫過程中,只有最后的寫入緩存和改名操作會造成主進程阻塞,在其他時候,AOF 后台重寫都不會對主進程造成阻塞,這將 AOF 重寫對性能造成的影響降到了最低。
當 serverCron 函數執行時,它都會檢查以下條件是否全部滿足,如果是的話,就會觸發自動的 AOF 重寫:
- 沒有 BGSAVE 命令在進行。
- 沒有 BGREWRITEAOF 在進行。
- 當前 AOF 文件大小大於 server.aof_rewrite_min_size (默認值為 1 MB)。
- 當前 AOF 文件大小和最后一次 AOF 重寫后的大小之間的比率大於等於指定的增長百分比。默認情況下,增長百分比為 100% ,也即是說,如果前面三個條件都已經滿足,並且當前 AOF文件大小比最后一次 AOF 重寫時的大小要大一倍的話,那么觸發自動 AOF 重寫。
事件
事件是 Redis 服務器的核心,它處理兩項重要的任務:
- 處理文件事件:在多個客戶端中實現多路復用,接受它們發來的命令請求,並將命令的執行結果返回給客戶端。
- 時間事件:實現服務器常規操作(server cron job)
文件事件
Redis 服務器通過在多個客戶端之間進行多路復用,從而實現高效的命令請求處理:多個客戶端通過套接字連接到 Redis 服務器中,但只有在套接字可以無阻塞地進行讀或者寫時,服務器才會和這些客戶端進行交互。
當服務器有命令結果要返回客戶端,而客戶端又有新命令請求進入時,服務器先處理新命令請求。
事件的執行與調度
Redis 里面既有文件事件,又有時間事件,那么如何調度這兩種事件就成了一個關鍵問題。簡單地說,Redis 里面的兩種事件呈合作關系,它們之間包含以下三種屬性:
- 一種事件會等待另一種事件執行完畢之后,才開始執行,事件之間不會出現搶占。
- 事件處理器先處理文件事件(處理命令請求),再執行時間事件(調用 serverCron)
- 文件事件的等待時間(類 poll 函數的最大阻塞時間),由距離到達時間最短的時間事件決定。
說明:
• 時間事件分為單次執行事件和循環執行事件,服務器常規操作 serverCron 就是循環事件。
• 文件事件和時間事件之間是合作關系:一種事件會等待另一種事件完成之后再執行,不會出現搶占情況。
命令的請求、處理和結果返回
Redis 以多路復用的方式來處理多個客戶端,為了讓多個客戶端之間獨立分開、不互相干擾,服務器為每個已連接客戶端維持一個 redisClient 結構,從而單獨保存該客戶端的狀態信息。
當客戶端連上服務器之后,客戶端就可以向服務器發送命令請求了。從客戶端發送命令請求,到命令被服務器處理、並將結果返回客戶端,整個過程有以下步驟:
- 客戶端通過套接字向服務器傳送命令協議數據。
- 服務器通過讀事件來處理傳入數據,並將數據保存在客戶端對應 redisClient 結構的查詢緩存中。
- 根據客戶端查詢緩存中的內容,程序從命令表中查找相應命令的實現函數。
- 程序執行命令的實現函數,修改服務器的全局狀態 server 變量,並將命令的執行結果保存到客戶端 redisClient 結構的回復緩存中,然后為該客戶端的 fd 關聯寫事件。
- 當客戶端 fd 的寫事件就緒時,將回復緩存中的命令結果傳回給客戶端。至此,命令執行完畢。
Posted by: 大CC | 11JUL,2014
博客:blog.me115.com [訂閱]
微博:新浪微博