簡單來說,底層資料結構一共有 6 種,分別是簡單動態字串、雙向連結串列、壓縮列表、雜湊表、跳錶和整數陣列。它們和資料型別的對應關係如下圖所示:
為了實現從鍵到值的快速訪問,Redis 使用了一個雜湊表來儲存所有鍵值對。一個雜湊表,其實就是一個陣列,陣列的每個元素稱為一個雜湊桶。
雜湊桶中的元素儲存的並不是值本身,而是指向具體值的指標。這也就是說,不管值是 String,還是集合型別,雜湊桶中的元素都是指向它們的指標。雜湊桶中的 entry 元素中儲存了key和value指標,分別指向了實際的鍵和值,這樣一來,即使值是一個集合,也可以通過value指標被查詢到。
Redis 解決雜湊衝突的方式,就是鏈式雜湊。鏈式雜湊也很容易理解,就是指同一個雜湊桶中的多個元素用一個連結串列來儲存,它們之間依次用指標連線。
集合型別的底層資料結構主要有 5 種:整數陣列、雙向連結串列、雜湊表、壓縮列表和跳錶。
Redis 是單執行緒,主要是指 Redis 的網路 IO 和鍵值對讀寫是由一個執行緒來完成的,這也是 Redis 對外提供鍵值儲存服務的主要流程。但 Redis 的其他功能,比如持久化、非同步刪除、叢集資料同步等,其實是由額外的執行緒執行的。
單執行緒 Redis 為什麼那麼快?
一方面,Redis 的大部分操作在記憶體上完成,再加上它採用了高效的資料結構,例如雜湊表和跳錶,這是它實現高效能的一個重要原因。另一方面,就是 Redis 採用了多路複用機制,使其在網路 IO 操作中能併發處理大量的客戶端請求,實現高吞吐率。
Linux 中的 IO 多路複用機制是指一個執行緒處理多個 IO 流,就是我們經常聽到的 select/epoll 機制。簡單來說,在 Redis 只執行單執行緒的情況下,該機制允許核心中,同時存在多個監聽套接字和已連線套接字。核心會一直監聽這些套接字上的連線請求或資料請求。一旦有請求到達,就會交給 Redis 執行緒處理,這就實現了一個 Redis 執行緒處理多個 IO 流的效果。
下圖就是基於多路複用的 Redis IO 模型。圖中的多個 FD 就是剛才所說的多個套接字。Redis 網路框架呼叫 epoll 機制,讓核心監聽這些套接字。此時,Redis 執行緒不會阻塞在某一個特定的監聽或已連線套接字上,也就是說,不會阻塞在某一個特定的客戶端請求處理上。正因為此,Redis 可以同時和多個客戶端連線並處理請求,從而提升併發性。
AOF 日誌是如何實現的?
說到日誌,我們比較熟悉的是資料庫的寫前日誌(Write Ahead Log, WAL),也就是說,在實際寫資料前,先把修改的資料記到日誌檔案中,以便故障時進行恢復。不過,AOF 日誌正好相反,它是寫後日志,“寫後”的意思是 Redis 是先執行命令,把資料寫入記憶體,然後才記錄日誌,如下圖所示:
傳統資料庫的日誌,例如 redo log(重做日誌),記錄的是修改後的資料,而 AOF 裡記錄的是 Redis 收到的每一條命令,這些命令是以文字形式儲存的。
寫後日志這種方式,就是先讓系統執行命令,只有命令能執行成功,才會被記錄到日誌中,否則,系統就會直接向客戶端報錯。所以,Redis 使用寫後日志這一方式的一大好處是,可以避免出現記錄錯誤命令的情況。它是在命令執行後才記錄日誌,所以不會阻塞當前的寫操作。
AOF 也有兩個潛在的風險:
如果剛執行完一個命令,還沒有來得及記日誌就當機了,那麼這個命令和相應的資料就有丟失的風險。
AOF 雖然避免了對當前命令的阻塞,但可能會給下一個操作帶來阻塞風險。這是因為,AOF 日誌也是在主執行緒中執行的,如果在把日誌檔案寫入磁碟時,磁碟寫壓力大,就會導致寫盤很慢,進而導致後續的操作也無法執行了。
AOF提供三種寫回策略
AOF 重寫機制
AOF 重寫機制就是在重寫時,Redis 根據資料庫的現狀建立一個新的 AOF 檔案,也就是說,讀取資料庫中的所有鍵值對,然後對每一個鍵值對用一條命令記錄它的寫入。比如說,當讀取了鍵值對“testkey”: “testvalue”之後,重寫機制會記錄 set testkey testvalue 這條命令。這樣,當需要恢復時,可以重新執行該命令,實現“testkey”: “testvalue”的寫入。
重寫機制具有“多變一”功能。所謂的“多變一”,也就是說,舊日誌檔案中的多條命令,在重寫後的新日誌中變成了一條命令。
AOF 檔案是以追加的方式,逐一記錄接收到的寫命令的。當一個鍵值對被多條寫命令反覆修改時,AOF 檔案會記錄相應的多條命令。但是,在重寫的時候,是根據這個鍵值對當前的最新狀態,為它生成對應的寫入命令。這樣一來,一個鍵值對在重寫日誌中只用一條命令就行了,而且,在日誌恢復時,只用執行這條命令,就可以直接完成這個鍵值對的寫入了。
AOF 重寫會阻塞嗎?
和 AOF 日誌由主執行緒寫回不同,重寫過程是由後臺子程式 bgrewriteaof 來完成的,這也是為了避免阻塞主執行緒,導致資料庫效能下降。
每次執行重寫時,主執行緒 fork 出後臺的 bgrewriteaof 子程式。此時,fork 會把主執行緒的記憶體拷貝一份給 bgrewriteaof 子程式,這裡面就包含了資料庫的最新資料。然後,bgrewriteaof 子程式就可以在不影響主執行緒的情況下,逐一把拷貝的資料寫成操作,記入重寫日誌。
因為主執行緒未阻塞,仍然可以處理新來的操作。此時,如果有寫操作,第一處日誌就是指正在使用的 AOF 日誌,Redis 會把這個操作寫到它的緩衝區。這樣一來,即使當機了,這個 AOF 日誌的操作仍然是齊全的,可以用於恢復。
而第二處日誌,就是指新的 AOF 重寫日誌。這個操作也會被寫到重寫日誌的緩衝區。這樣,重寫日誌也不會丟失最新的操作。等到拷貝資料的所有操作記錄重寫完成後,重寫日誌記錄的這些最新操作也會寫入新的 AOF 檔案,以保證資料庫最新狀態的記錄。此時,我們就可以用新的 AOF 檔案替代舊檔案了。
每次 AOF 重寫時,Redis 會先執行一個記憶體拷貝,用於重寫;然後,使用兩個日誌保證在重寫過程中,新寫入的資料不會丟失。而且,因為 Redis 採用額外的執行緒進行資料重寫,所以,這個過程並不會阻塞主執行緒。
三種寫回策略體現了系統設計中的一個重要原則 ,即 trade-off,或者稱為“取捨”,指的就是在效能和可靠性保證之間做取捨。這是做系統設計和開發的一個關鍵哲學,希望能充分地理解這個原則,並在日常開發中加以應用。
RDB記憶體快照
對 Redis 來說,它實現類似照片記錄效果的方式,就是把某一時刻的狀態以檔案的形式寫到磁碟上,也就是快照。這樣一來,即使當機,快照檔案也不會丟失,資料的可靠性也就得到了保證。這個快照檔案就稱為 RDB 檔案,其中,RDB 就是 Redis DataBase 的縮寫。
Redis 提供了兩個命令來生成 RDB 檔案,分別是 save 和 bgsave。
save:在主執行緒中執行,會導致阻塞;
bgsave:建立一個子程式,專門用於寫入 RDB 檔案,避免了主執行緒的阻塞,這也是 Redis RDB 檔案生成的預設配置。
在快照執行期間,Redis 就會藉助作業系統提供的寫時複製技術(Copy-On-Write, COW),在執行快照的同時,正常處理寫操作。
bgsave 子程式是由主執行緒 fork 生成的,可以共享主執行緒的所有記憶體資料。bgsave 子程式執行後,開始讀取主執行緒的記憶體資料,並把它們寫入 RDB 檔案。
此時,如果主執行緒對這些資料也都是讀操作(例如圖中的鍵值對 A),那麼,主執行緒和 bgsave 子程式相互不影響。但是,如果主執行緒要修改一塊資料(例如圖中的鍵值對 C),那麼,這塊資料就會被複制一份,生成該資料的副本。然後,bgsave 子程式會把這個副本資料寫入 RDB 檔案,而在這個過程中,主執行緒仍然可以直接修改原來的資料。
這既保證了快照的完整性,也允許主執行緒同時對資料進行修改,避免了對正常業務的影響。
如果頻繁地執行全量快照,也會帶來兩方面的開銷。
一方面,頻繁將全量資料寫入磁碟,會給磁碟帶來很大壓力,多個快照競爭有限的磁碟頻寬,前一個快照還沒有做完,後一個又開始做了,容易造成惡性迴圈。
另一方面,bgsave 子程式需要通過 fork 操作從主執行緒建立出來。雖然,子程式在建立後不會再阻塞主執行緒,但是,fork 這個建立過程本身會阻塞主執行緒,而且主執行緒的記憶體越大,阻塞時間越長。如果頻繁 fork 出 bgsave 子程式,這就會頻繁阻塞主執行緒了。
增量快照,就是指,做了一次全量快照後,後續的快照只對修改的資料進行快照記錄,這樣可以避免每次全量快照的開銷。
Redis 4.0 中提出了一個混合使用 AOF 日誌和記憶體快照的方法。簡單來說,記憶體快照以一定的頻率執行,在兩次快照之間,使用 AOF 日誌記錄這期間的所有命令操作。
這樣一來,快照不用很頻繁地執行,這就避免了頻繁 fork 對主執行緒的影響。而且,AOF 日誌也只用記錄兩次快照間的操作,也就是說,不需要記錄所有操作了,因此,就不會出現檔案過大的情況了,也可以避免重寫開銷。
這個方法既能享受到 RDB 檔案快速恢復的好處,又能享受到 AOF 只記錄操作命令的簡單優勢
資料同步
Redis 提供了主從庫模式,以保證資料副本的一致,主從庫之間採用的是讀寫分離的方式。
讀操作:主庫、從庫都可以接收;
寫操作:首先到主庫執行,然後,主庫將寫操作同步給從庫。
當我們啟動多個 Redis 例項的時候,它們相互之間就可以通過 replicaof(Redis 5.0 之前使用 slaveof)命令形成主庫和從庫的關係,之後會按照三個階段完成資料的第一次同步。
通過“主 - 從 - 從”模式將主庫生成 RDB 和傳輸 RDB 的壓力,以級聯的方式分散到從庫上。
哨兵機制
哨兵其實就是一個執行在特殊模式下的 Redis 程式,主從庫例項執行的同時,它也在執行。哨兵主要負責的就是三個任務:監控、選主(選擇主庫)和通知。
監控是指哨兵程式在執行時,週期性地給所有的主從庫傳送 PING 命令,檢測它們是否仍然線上執行。如果從庫沒有在規定時間內響應哨兵的 PING 命令,哨兵就會把它標記為“下線狀態”;同樣,如果主庫也沒有在規定時間內響應哨兵的 PING 命令,哨兵就會判定主庫下線,然後開始自動切換主庫的流程。
主庫掛了以後,哨兵就需要從很多個從庫裡,按照一定的規則選擇一個從庫例項,把它作為新的主庫。這一步完成後,現在的叢集裡就有了新主庫。
在執行通知任務時,哨兵會把新主庫的連線資訊發給其他從庫,讓它們執行 replicaof 命令,和新主庫建立連線,並進行資料複製。同時,哨兵會把新主庫的連線資訊通知給客戶端,讓它們把請求操作發到新主庫上。
哨兵機制通常會採用多例項組成的叢集模式進行部署,這也被稱為哨兵叢集。引入多個哨兵例項一起來判斷,就可以避免單個哨兵因為自身網路狀況不好,而誤判主庫下線的情況。同時,多個哨兵的網路同時不穩定的概率較小,由它們一起做決策,誤判率也能降低。
基於 pub/sub 機制的哨兵叢集組成
哨兵只要和主庫建立起了連線,就可以在主庫上釋出訊息了,比如說釋出它自己的連線資訊(IP 和埠)。同時,它也可以從主庫上訂閱訊息,獲得其他哨兵釋出的連線資訊。當多個哨兵例項都在主庫上做了釋出和訂閱操作後,它們之間就能知道彼此的 IP 地址和埠。
哨兵除了彼此之間建立起連線形成叢集外,還需要和從庫建立連線。這是因為,在哨兵的監控任務中,它需要對主從庫都進行心跳判斷,而且在主從庫切換完成後,它還需要通知從庫,讓它們和新主庫進行同步。
哨兵向主庫傳送 INFO 命令來完成的。就像下圖所示,哨兵 2 給主庫傳送 INFO 命令,主庫接受到這個命令後,就會把從庫列表返回給哨兵。接著,哨兵就可以根據從庫列表中的連線資訊,和每個從庫建立連線,並在這個連線上持續地對從庫進行監控。哨兵 1 和 3 可以通過相同的方法和從庫建立連線。
切片叢集
切片叢集,也叫分片叢集,就是指啟動多個 Redis 例項組成一個叢集,然後按照一定的規則,把收到的資料劃分成多份,每一份用一個例項來儲存。
如何儲存更多資料?
為了儲存大量資料,我們使用了大記憶體雲主機和切片叢集兩種方法。實際上,這兩種方法分別對應著 Redis 應對資料量增多的兩種方案:縱向擴充套件(scale up)和橫向擴充套件(scale out)。
縱向擴充套件:升級單個 Redis 例項的資源配置,包括增加記憶體容量、增加磁碟容量、使用更高配置的 CPU。就像下圖中,原來的例項記憶體是 8GB,硬碟是 50GB,縱向擴充套件後,記憶體增加到 24GB,磁碟增加到 150GB。
橫向擴充套件:橫向增加當前 Redis 例項的個數,就像下圖中,原來使用 1 個 8GB 記憶體、50GB 磁碟的例項,現在使用三個相同配置的例項。
本作品採用《CC 協議》,轉載必須註明作者和本文連結