《redis 设计与实现》读书笔记
大家好,我是烤鴨:
????《redis 設計與實現》,讀書筆記。
第一部分 數據結構與對象
第2章 簡單動態字符串
Redis 使用SDS 作為字符串表示。
O(1) 復雜度獲取字符串長度。
杜絕緩沖區溢出。
減少修改字符串長度時所需的內存重分配次數。
二進制安全。
兼容部分C字符串函數。
第3章 鏈表
每個鏈表節點由一個listNode結構標識,每個節點都有一個指向前置接點和后置節點的指針,實現是雙端鏈表。
每個鏈表用list結構標識,結構帶有表頭節點指針、表尾節點指針,鏈表長度等。
第4章 字典
Redis中的字典使用哈希表作為底層實現,每個字典表有兩個哈希表,一個平時使用,另一個僅在進行rehash時使用。
哈希表使用鏈地址法來解決鍵沖突,被分配到同一個索引上的多個鍵值對會連接成一個單向鏈表。
對哈希表進行擴展或者收縮操作時,程序需要現有哈希表包含的現有鍵值對rehash到新哈希表里面,rehash過程是漸進式的。
dict中的rehashidx( -1 表示不在進行rehash)
rehash的index = hash & ht[0].size - 1
哈希表擴展操作的條件:
- 沒有執行BGSAVE或者BGREWRITEAOF命令,哈希表的負載因子大于等于1。
- 服務器目前正在執行BGSAVE命令或者BGREWRITEAOF命令,并且哈希表的負載因子(ht[0].used/ht[0].size)大于等于5。
第5章 跳躍表
跳表效率跟平衡樹差不多,實現更簡單,平均O(logN),最壞O(N)復雜度處理節點。
每次創建新跳表節點,隨機生成1-32之間的一個數作為層高。
Redis的跳躍表實現由zskiplist和zskiplistNode兩個結構組成,其中zskiplist用于保存跳躍表信息(比如表頭節點、表尾節點、長度),而
zskiplistNode則用于表示跳躍表節點。
跳躍表中的節點按照分值大小進行排序,當分值相同時,節點按照成員對象的大小進行排序。
第6章 整數集合
整數集合(intset)是Redis用于保存整數值的集合抽象數據結構,它可以保存類型為int16_t、int32_t或者int64_t的整數值,并且保證
集合中不會出現重復元素,底層實現是數組。
新元素添加到整數集合里面,并且新元素的類型比整數集合現有所有元素的類型都要長時,整數集合需要先進行升級。
不支持降級。
第7章 壓縮列表
壓縮列表(ziplist)是列表鍵和哈希鍵的底層實現之一。
壓縮列表是Redis為了節約內存而開發的,是由一系列特殊編碼的連續內存塊組成的順序型(sequential)數據結構。一個壓縮列表可
以包含任意多個節點(entry),每個節點可以保存一個字節數組或者一個整數值。
ziplistPush等命令的平均復雜度僅為O(N),最壞復雜度為O(N2)。
第8章 對象
Redis使用對象來表示數據庫中的鍵和值,叫做鍵對象和值對象。
類型有 字符串、列表、哈希、集合、有序集合。
字符串編碼:int、raw或者embstr。
- int編碼使用整數集合作為底層實現。
- 釋放embstr編碼的字符串對象只需要調用一次內存釋放函數,而釋放raw編碼的字符串對象需要調用兩次內存釋放函數。
列表的編碼:ziplist或者linkedlist。
- ziplist編碼使用壓縮列表作為底層實現,每個壓縮列表節點(entry)保存了一個列表元素
- linkedlist編碼使用雙端鏈表作為底層實現。
哈希的編碼:ziplist或者hashtable(使用字典作為底層實現)。
集合的編碼:intset或者hashtable。
有序集合的編碼:ziplist或者skiplist。有序集合每個元素的成員都是一個字符串對象,而每個元素的分值都是一個double類型的浮點數。
相同對象類型各個編碼之間會轉換,字符串對象是Redis五種類型的對象中唯一一種會被其他四種類型對象嵌套的對象。
服務器在執行某些命令之前,會先檢查給定鍵的類型能否執行指定的命令,而檢查一個鍵的類型就是檢查鍵的值對象的類型。
Redis的對象系統帶有引用計數實現的內存回收機制,當一個對象不再被使用時,該對象所占用的內存就會被自動釋放。
Redis會共享值為0到9999的字符串對象。
對象會記錄自己的最后一次被訪問的時間,這個時間可以用于計算對象的空轉時間。(LRU的實現)
第二部分
第9章 數據庫
redis默認創建16個數據庫,客戶端默認是0號數據庫。
數據庫主要由dict和expires兩個字典構成,其中dict字典負責保存鍵值對,而expires字典則負責保存鍵的過期時間。
Redis使用惰性刪除和定期刪除兩種策略來刪除過期的鍵:惰性刪除策略只在碰到過期鍵時才進行刪除操作,定期刪除策略則每隔一段
時間主動查找并刪除過期鍵。
執行SAVE命令或者BGSAVE命令所產生的新RDB文件和執行BGREWRITEAOF命令所產生的重寫AOF文件不會包含已經過期的鍵。
當一個過期鍵被刪除之后,服務器會追加一條DEL命令到現有AOF文件的末尾,顯式地刪除過期鍵。
當主服務器刪除一個過期鍵之后,它會向所有從服務器發送一條DEL命令,顯式地刪除過期鍵。
從服務器即使發現過期鍵也不會自作主張地刪除它,而是等待主節點發來DEL命令,這種統一、中心化的過期鍵刪除策略可以保證主從服務器數據的一致性。
當Redis命令對數據庫進行修改之后,服務器會根據配置向客戶端發送數據庫通知。
第10章 RDB持久化
RDB持久化功能所生成的RDB文件是一個經過壓縮的二進制文件,通過該文件可以還原生成RDB文件時的數據庫狀態。
SAVE命令由服務器進程直接執行保存操作,所以該命令會阻塞服務器,BGSAVE令由子進程執行保存操作,所以該命令不會阻塞服務器。
服務器狀態中會保存所有用save選項設置的保存條件,當任意一個保存條件被滿足時,服務器會自動執行BGSAVE命令。
第11章 AOF持久化
AOF文件通過保存所有修改數據庫的寫命令請求來記錄服務器的數據庫狀態。
命令請求會先保存到AOF緩沖區里面,之后再定期寫入并同步到AOF文件。只要載入并重新執行保存在AOF文件中的命令,就可以還原數據庫本來的狀態。
AOF持久化的效率和安全性:
- 當appendfsync的值為always時,效率最慢最安全,宕機只會丟失一個事件循環中所產生的命令數據。
- 當appendfsync的值為everysec時,宕機只會丟失一秒中所產生的命令數據。
- 當appendfsync的值為no時,效率最高最不安全,同步由操作系統控制,宕機會丟失上次同步之后所產生的命令數據。
在執行BGREWRITEAOF命令時,Redis服務器會維護一個AOF重寫緩沖區,該緩沖區會在子進程創建新AOF文件期間,記錄服務器執行的所有寫命令。當子進程完成創建新AOF文件的工作之后,服務器會將重寫緩沖區中的所有內容追加到新AOF文件的末尾,使得新舊兩個AOF文件所保存的數據庫狀態一致。最后,服務器用新的AOF文件替換舊的AOF文件,以此來完成AOF文件重寫操作。
第12章 事件
Redis服務器是一個事件驅動程序,服務器處理的事件分為時間事件和文件事件兩類。雖然文件事件處理器以單線程方式運行,但通過使用I/O多路復用程序來監聽多個套接字。
文件事件處理器基于Reactor模式實現的網絡通信程序,由四個組成部分,它們分別是套接字、I/O多路復用程序、文件事件分派器(dispatcher),以及事件處理器。
文件事件分為AE_READABLE事件(讀事件)和AE_WRITABLE事件(寫事件)兩類。
時間事件分為定時事件和周期性事件:定時事件只在指定的時間到達一次,而周期性事件則每隔一段時間到達一次。
文件事件和時間事件之間是合作關系,服務器會輪流處理這兩種事件,并且處理事件的過程中也不會進行搶占。(對文件事件和時間事件的處理都是同步、有序、原子地執行的,服務器不會中途中斷事件處理,也不會對事件進行搶占,因此,不管是文件事件的處理器,還是時間事件的處理器,它們都會盡可地減少程序的阻塞時間,并在有需要時主動讓出執行權,從而降低造成事件饑餓的可能性。比如說,在命令回復處理器將一個命令回復寫入到客戶端套接字時,如果寫入字節數超過了一個預設常量的話,命令回復處理器就會主動用break跳出寫入循環,將余下的數據留到下次再寫;另外,時間事件也會將非常耗時的持久化操作放到子線程或者子進程執行。)
第13章 客戶端
服務器狀態結構使用clients鏈表連接起多個客戶端狀態,新添加的客戶端狀態會被放到鏈表的末尾。
輸入緩沖區記錄了客戶端發送的命令請求,這個緩沖區的大小不能超過1GB。
命令的參數和參數個數會被記錄在客戶端狀態的argv和argc屬性里面,而cmd屬性則記錄了客戶端要執行命令的實現函數。
客戶端的輸出緩沖區有固定大小緩沖區和可變大小緩沖區兩種緩沖區可用,其中固定大小緩沖區的最大大小為16KB,而可變大小緩沖區的最大大小不能超過服務器設置的硬性限制值。
當一個客戶端通過網絡連接連上服務器時,服務器會為這個客戶端創建相應的客戶端狀態。網絡連接關閉、發送了不合協議格式的命令請求、成為CLIENT KILL命令的目標、空轉時間超時、輸出緩沖區的大小超出限制(硬性限制和軟性限制),以上這些原因都會造成客戶端被關閉。
處理Lua腳本的偽客戶端在服務器初始化時創建,這個客戶端會一直存在,直到服務器關閉。
載入AOF文件時使用的偽客戶端在載入工作開始時動態創建,載入工作完畢之后關閉。
第14章 服務器
一個命令請求從發送到完成主要包括以下步驟:1)客戶端將命令請求發送給服務器;2)服務器讀取命令請求,并分析出命令參數;3)命令執行器根據參數查找命令的實現函數,然后執行實現函數并得出命令回復;4)服務器將命令回復返回給客戶端。
serverCron函數默認每隔100毫秒執行一次,它的工作主要包括更新服務器狀態信息,處理服務器接收的SIGTERM信號,管理客戶端資源和數據庫狀態,檢查并執行持久化操作等等。
服務器從啟動到能夠處理客戶端的命令請求需要執行以下步驟:1)初始化服務器狀態;2)載入服務器配置;3)初始化服務器數據結構;4)還原數據庫狀態;5)執行事件循環。
第三部分
第15章 復制
部分重同步可以解決斷線重新同步的問題,是通過復制偏移量、復制積壓緩沖區、服務器運行ID三個部分來實現。
在復制操作剛開始的時候,從服務器會成為主服務器的客戶端,并通過向主服務器發送命令請求來執行復制步驟,而在復制操作的后期,主從服務器會互相成為對方的客戶端。
主服務器通過向從服務器傳播命令來更新從服務器的狀態,保持主從服務器一致,而從服務器則通過向主服務器發送命令來進行心跳檢測,以及命令丟失檢測。
第16章 Sentinel
Sentinel哨兵模式,高可用的解決方案之一,由一個或多個Sentinel實例(instance)組成的Sentinel系統(system)可以監視任意多個主從服務器。在主節點下線情況下,自動將從節點升級。
Sentinel通過向主服務器發送INFO命令來獲得主服務器屬下所有從服務器的地址信息,并為這些從服務器創建相應的實例結構,以及連向這些從服務器的命令連接和訂閱連接。
Sentinel默認以每十秒一次的頻率向被監視的主服務器和從服務器發送INFO命令,當主服務器處于下線狀態,或者Sentinel正在對主服務器進行故障轉移操作時,Sentinel向從服務器發送INFO命令的頻率會改為每秒一次。
Sentinel只會與主服務器和從服務器創建命令連接和訂閱連接,Sentinel與Sentinel之間則只創建命令連接。
Sentinel以每秒一次的頻率向實例發送ping做心跳檢測。
當Sentinel將一個主服務器判斷為主觀下線時,它會向同樣監視這個主服務器的其他Sentinel進行詢問,看它們是否同意這個主服務器已經進入主觀下線狀態。
當Sentinel收集到足夠多的主觀下線投票之后,它會將主服務器判斷為客觀下線,并發起一次針對主服務器的故障轉移操作。
第17章 集群
Redis集群是Redis提供的分布式數據庫方案,集群通過分片(sharding)來進行數據共享,并提供復制和故障轉移功能。
節點通過握手來將其他節點添加到自己所處的集群當中。
集群中的16384個槽可以分別指派給集群中的各個節點,每個節點都會記錄哪些槽指派給了自己,而哪些槽又被指派給了其他節點。
CRC16(key)語句用于計算鍵key的CRC-16校驗和,而&16383語句則用于計算出一個介于0至16383之間的整數作為鍵key的槽號。
def slot_number(key):return CRC16(key) & 16383節點在接到一個命令請求時,會先檢查這個命令請求要處理的鍵所在的槽是否由自己負責,如果不是的話,節點將向客戶端返回一個MOVED錯誤,MOVED錯誤攜帶的信息可以指引客戶端轉向至正在負責相關槽的節點。
重新分片由redis-trib或者redis-cli負責執行。
# 5.0 之前 redis-trib.rb create --replicas # 5.0 之后 redis-cli -a redis-pw --cluster create --cluster-replicass遷移過程中鍵被查找,返回 ASK 指引到相關節點。
集群里的從節點用于復制主節點,并在主節點下線時,代替主節點繼續處理命令請求。
集群中的節點通過發送和接收消息來進行通信,常見的消息包括MEET、PING、PONG、PUBLISH、FAIL五種。
第四部分
第18章 發布與訂閱
頻道訂閱和模式訂閱:
-
服務器狀態在pubsub_channels字典保存了所有頻道的訂閱關系:SUBSCRIBE命令負責將客戶端和被訂閱的頻道關聯到這個字典里面,而UNSUBSCRIBE命令則負責解除客戶端和被退訂頻道之間的關聯。
-
服務器狀態在pubsub_patterns鏈表保存了所有模式的訂閱關系:PSUBSCRIBE命令負責將客戶端和被訂閱的模式記錄到這個鏈表中,而PUNSUBSCRIBE命令則負責移除客戶端和被退訂模式在鏈表中的記錄。
PUBLISH命令通過訪問pubsub_channels字典來向頻道的所有訂閱者發送消息,通過訪問pubsub_patterns鏈表來向所有匹配頻道的模式的訂閱者發送消息。
PUBSUB命令的三個子命令都是通過讀取pubsub_channels字典和pubsub_patterns鏈表中的信息來實現的。
ps:一般流量不大的可以使用redis的發布訂閱做IM,比如聊天室。
第19章 事務
redis的事務是提供打包執行命令,先進先出(FIFO)順序執行。
事務在執行過程中不會被中斷,當事務隊列中的所有命令都被執行完畢之后,事務才會結束。
帶有WATCH命令的事務會將客戶端和被監視的鍵在數據庫的watched_keys字典中進行關聯,當鍵被修改時,程序會將所有監視被修改鍵的客戶端的REDIS_DIRTY_CAS標志打開,此時服務器將拒絕執行客戶端提交的事務(樂觀鎖CAS的方式)。
Redis的事務總是具有ACID中的原子性、一致性和隔離性,當服務器運行在AOF持久化模式下,并且appendfsync選項的值為always時,事務也具有耐久性。(如果其中某一條命令出錯,其他命令會繼續執行,這個跟DB有點差異,redis作者認為這種場景生產環境很少出現且和redis高效的設計理念不相符,所以沒有回滾功能)
第20章 Lua腳本
Redis服務器在啟動時,會對內嵌的Lua環境,使用一個偽客戶端來執行Lua腳本中包含的Redis命令。
命令相關:
-
EVAL:為客戶端輸入的腳本在Lua環境中定義一個函數,并通過調用這個函數來執行腳本。
-
EVALSHA:通過直接調用Lua環境中已定義的函數來執行腳本。
-
SCRIPT FLUSH:會清空服務器lua_scripts字典中保存的腳本,并重置Lua環境。
-
SCRIPT EXISTS:接受一個或多個SHA1校驗和為參數,并通過檢查lua_scripts字典來確認校驗和對應的腳本是否存在。
-
SCRIPT LOAD:接受一個Lua腳本為參數,為該腳本在Lua環境中創建函數,并將腳本保存到lua_scripts字典中。
服務器在執行腳本之前,會為Lua環境設置一個超時處理鉤子,當腳本出現超時運行情況時,客戶端可以通過向服務器發送SCRIPT KILL命令來讓鉤子停止正在執行的腳本,或者發送SHUTDOWN nosave命令來讓鉤子關閉整個服務器。
主服務器在復制EVALSHA命令時,如果從服務器返回腳本未找到,主服務器會將EVALSHA命令轉換成等效的EVAL命令,并通過傳播EVAL命令來獲得相同的腳本執行效果。
第21章 排序
Redis的SORT命令可以對列表鍵、集合鍵或者有序集合鍵的值進行排序。通過將被排序鍵包含的元素載入到數組里面,然后對數組進行排序來完成對鍵進行排序的工作。
在默認情況下,排序鍵是數字值,如果使用了ALPHA選項,排序鍵是字符串。SORT命令的排序操作由快速排序算法實現。默認升序,DESC降序。
redis> RPUSH numbers 3 2 1 4 5 redis> SORT numbers DESC當SORT命令使用了BY選項時,命令使用其他鍵的值作為權重來進行排序操作。
redis> SADD fruits "apple" "banana" "cherry" (integer) 3 redis> SORT fruits ALPHA 1) "apple" 2) "banana" 3) "cherry"當SORT命令使用了LIMIT選項時,命令只保留排序結果集中LIMIT選項指定的元素。
redis> SADD alphabet a b c d e f (integer) 6 #集合中的元素是亂序存放的 redis> SMEMBERS alphabet 1) "d" 2) "c" 3) "a" 4) "b" 5) "f" 6) "e" #對集合進行排序,并返回所有排序后的limit元素 redis> SORT alphabet ALPHA LIMIT 0,4 1) "a" 2) "b" 3) "c" 4) "d"當SORT命令使用了GET選項時,命令會根據排序結果集中的元素,以及GET選項給定的模式,查找并返回其他鍵的值,而不是返回被排序的元素。
#設置peter、jack、tom的全名 redis> SET peter-name "Peter White" OK redis> SET jack-name "Jack Snow" OK redis> SET tom-name "Tom Smith" OK # SORT命令首先對students集合進行排序,得到排序結果 # 1) "jack" # 2) "peter" # 3) "tom" #然后根據這些結果,獲取并返回鍵jack-name、peter-name和tom-name的值 redis> SORT students ALPHA GET *-name 1) "Jack Snow" 2) "Peter White" 3) "Tom Smith"當SORT命令使用了STORE選項時,命令會將排序結果集保存在指定的鍵里面。
當SORT命令同時使用多個選項時,命令先執行排序操作(可用的選項為ALPHA、ASC或DESC、BY),然后執行LIMIT選項,之后執行GET選項,再之后執行STORE選項,最后才將排序結果集返回給客戶端。除了GET選項之外,調整選項的擺放位置不會影響SORT命令的排序結果。
第22章 二進制位數組
Redis提供了SETBIT、GETBIT、BITCOUNT、BITOP四個命令用于處理二進制位數組(bit array,又稱“位數組”)。
SDS使用逆序來保存位數組,這種保存順序簡化了SETBIT命令的實現,使得SETBIT命令可以在不移動現有二進制位的情況下,對位數組進行空間擴展。(順序的話,高位添加,需要移動原有的數組位置)
BITCOUNT命令使用了查表算法和variable-precision SWAR算法來優化命令的執行效率。(漢明重量)
- 未處理的二進制位大于等于128位,variable-precision SWAR算法。反之查表算法。
BITOP命令的所有操作都使用C語言內置的位操作來實現。
第23章 慢查詢日志
Redis的慢查詢日志功能用于記錄執行時間超過指定時長的命令。
- slowlog-log-slower-than:執行時間超過多少微秒(1秒等于1000 000微秒)的命令請求會被記錄到日志上。(默認 10000,10毫秒)
- slowlog-max-len:服務器最多保存多少條慢查詢日志。(默認 128)
Redis服務器將所有的慢查詢日志保存在服務器狀態的slowlog鏈表中,每個鏈表節點都包含一個slowlogEntry結構,每個slowlogEntry結構代表一條慢查詢日志。
新的慢查詢日志會被添加到slowlog鏈表的表頭,打印和刪除慢查詢日志可以通過遍歷slowlog鏈表來完成。
第24章 監視器
monitor 命令,服務器實時監控當前處理的命令。
redis> MONITOR OK 1378822099.421623 [0 127.0.0.1:56604] "PING" 1378822105.089572 [0 127.0.0.1:56604] "SET" "msg" "hello world"當一個客戶端從普通客戶端變為監視器時,該客戶端的REDIS_MONITOR標識會被打開。
服務器將所有監視器都記錄在monitors鏈表中。
每次處理命令請求時,服務器都會遍歷monitors鏈表,將相關信息發送給監視器。
總結
以上是生活随笔為你收集整理的《redis 设计与实现》读书笔记的全部內容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: ConcurrentHashMap底层原
- 下一篇: socket与socketServer通