最近在讀一篇關於Redis的專欄,叫做《Redis核心技術與實戰》,作者在Redis方面研究頗深,讀后非常受益,特在此做記錄。
一、Redis基礎
1)知識圖和問題畫像圖
Redis知識全景圖都包括“兩大維度,三大主線”。“兩大維度”就是指系統維度和應用維度,“三大主線”也就是指高性能、高可靠和高可擴展。
高性能主線,包括線程模型、數據結構、持久化、網絡框架;高可靠主線,包括主從復制、哨兵機制;高可擴展主線,包括數據分片、負載均衡。
Redis 各大典型問題,同時結合相關的技術點,手繪了一張 Redis 的問題畫像圖。按照“問題 --> 主線 --> 技術點”的方式梳理出來。
2)數據結構
底層數據結構一共有 6 種,分別是簡單動態字符串、雙向鏈表、壓縮列表、哈希表、跳表和整數數組。
壓縮列表實際上類似於一個數組,數組中的每一個元素都對應保存一個數據。和數組不同的是,壓縮列表在表頭有三個字段 zlbytes、zltail 和 zllen,分別表示列表長度、列表尾的偏移量和列表中的 entry 個數;壓縮列表在表尾還有一個 zlend,表示列表結束。在壓縮列表中,如果我們要查找定位第一個元素和最后一個元素,可以通過表頭三個字段的長度直接定位,復雜度是 O(1)。而查找其他元素時,就沒有這么高效了,只能逐個查找,此時的復雜度就是 O(N) 了。
跳表在鏈表的基礎上,增加了多級索引,通過索引位置的幾個跳轉,實現數據的快速定位。
集合常見操作的復雜度:
- 單元素操作是基礎;
- 范圍操作非常耗時;
- 統計操作通常高效;
- 例外情況只有幾個,例如壓縮列表和雙向鏈表都會記錄表頭和表尾的偏移量。
3)單線程
Redis 是單線程,主要是指 Redis 的網絡 IO 和鍵值對讀寫是由一個線程來完成的,這也是 Redis 對外提供鍵值存儲服務的主要流程。但 Redis 的其他功能,比如持久化、異步刪除、集群數據同步等,其實是由額外的線程執行的。
多線程的開銷,系統中通常會存在被多線程同時訪問的共享資源,比如一個共享的數據結構。當有多個線程要修改這個共享資源時,為了保證共享資源的正確性,就需要有額外的機制進行保證,而這個額外的機制,就會帶來額外的開銷。
通常來說,單線程的處理能力要比多線程差很多,但是 Redis 卻能使用單線程模型達到每秒數十萬級別的處理能力。一方面,Redis 的大部分操作在內存上完成,再加上它采用了高效的數據結構,例如哈希表和跳表,這是它實現高性能的一個重要原因。另一方面,就是 Redis 采用了多路復用機制,使其在網絡 IO 操作中能並發處理大量的客戶端請求,實現高吞吐率。
在 Redis 只運行單線程的情況下,該機制允許內核中,同時存在多個監聽套接字和已連接套接字。內核會一直監聽這些套接字上的連接請求或數據請求。一旦有請求到達,就會交給 Redis 線程處理,這就實現了一個 Redis 線程處理多個 IO 流的效果。
4)AOF和RDB
Redis 的持久化主要有兩大機制,即 AOF(Append Only File)日志和 RDB 快照。
AOF 日志正好相反,它是寫后日志,“寫后”的意思是 Redis 是先執行命令,把數據寫入內存,然后才記錄日志。
AOF 里記錄的是 Redis 收到的每一條命令,這些命令是以文本形式保存的。
- Redis 使用寫后日志這一方式的一大好處是,可以避免出現記錄錯誤命令的情況。
- 還有一個好處:它是在命令執行后才記錄日志,所以不會阻塞當前的寫操作。
AOF 也有兩個潛在的風險。
- 首先,如果剛執行完一個命令,還沒有來得及記日志就宕機了,那么這個命令和相應的數據就有丟失的風險。
- 其次,AOF 雖然避免了對當前命令的阻塞,但可能會給下一個操作帶來阻塞風險。
二、實踐
1)string
當你保存 64 位有符號整數時,String 類型會把它保存為一個 8 字節的 Long 類型整數,這種保存方式通常也叫作 int 編碼方式。
但是,當你保存的數據中包含字符時,String 類型就會用簡單動態字符串(Simple Dynamic String,SDS)結構體來保存,
- buf:字節數組,保存實際數據。為了表示字節數組的結束,Redis 會自動在數組最后加一個“\0”,這就會額外占用 1 個字節的開銷。
- len:占 4 個字節,表示 buf 的已用長度。
- alloc:也占個 4 字節,表示 buf 的實際分配長度,一般大於 len。
另外,對於 String 類型來說,除了 SDS 的額外開銷,還有一個來自於 RedisObject 結構體的開銷。一個 RedisObject 包含了 8 字節的元數據和一個 8 字節指針。
當字符串大於 44 字節時,SDS 的數據量就開始變多了,Redis 就不再把 SDS 和 RedisObject 布局在一起了,而是會給 SDS 分配獨立的空間,並用指針指向 SDS 結構。
2)統計模式
聚合統計,就是指統計多個集合元素的聚合結果,包括:統計多個集合的共有元素(交集統計);把兩個集合相比,統計其中一個集合獨有的元素(差集統計);統計多個集合的所有元素(並集統計)。
Set 的差集、並集和交集的計算復雜度較高,在數據量較大的情況下,如果直接執行這些計算,會導致 Redis 實例阻塞。小建議:你可以從主從集群中選擇一個從庫,讓它專門負責聚合計算,或者是把數據讀取到客戶端,在客戶端來完成聚合統計。
在面對需要展示最新列表、排行榜等場景時,如果數據更新頻繁或者需要分頁顯示,建議你優先考慮使用 Sorted Set。
二值狀態就是指集合元素的取值就只有 0 和 1 兩種。Bitmap 本身是用 String 類型作為底層數據結構實現的一種統計二值狀態的數據類型。
基數統計就是指統計一個集合中不重復的元素個數。
3)GEO
GEO 類型的底層數據結構就是用 Sorted Set 來實現的。
Redis 采用了業界廣泛使用的 GeoHash 編碼方法,這個方法的基本原理就是“二分區間,區間編碼”。
對於一個地理位置信息來說,它的經度范圍是[-180,180]。GeoHash 編碼會把一個經度值編碼成一個 N 位的二進制值,我們來對經度范圍[-180,180]做 N 次的二分區操作,其中 N 可以自定義。
4)異步機制
和客戶端交互時的阻塞點。復雜度高的增刪改查操作肯定會阻塞 Redis。
- 第一個阻塞點:集合全量查詢和聚合操作。
- 第二個阻塞點:bigkey 刪除操作。
- 第三個阻塞點:清空數據庫。
和磁盤交互時的阻塞點。Redis 開發者早已認識到磁盤 IO 會帶來阻塞,所以就把 Redis 進一步設計為采用子進程的方式生成 RDB 快照文件,以及執行 AOF 日志重寫操作。
- 第四個阻塞點了:AOF 日志同步寫。
主從節點交互時的阻塞點。在主從集群中,主庫需要生成 RDB 文件,並傳輸給從庫。主庫在復制的過程中,創建和傳輸 RDB 文件都是由子進程來完成的,不會阻塞主線程。
- 第五個阻塞點:加載 RDB 文件。
Redis 主線程啟動后,會使用操作系統提供的 pthread_create 函數創建 3 個子線程,分別由它們負責 AOF 日志寫操作、鍵值對刪除以及文件關閉的異步執行。
5)內存碎片
Redis 釋放的內存空間可能並不是連續的,那么,這些不連續的內存空間很有可能處於一種閑置的狀態。
這就會導致一個問題:雖然有空閑空間,Redis 卻無法用來保存數據,不僅會減少 Redis 能夠實際保存的數據量,還會降低 Redis 運行機器的成本回報率。
內存碎片的形成有內因和外因兩個層面的原因。簡單來說,內因是操作系統的內存分配機制,外因是 Redis 的負載特征。
Redis 是內存數據庫,內存利用率的高低直接關系到 Redis 運行效率的高低。為了讓用戶能監控到實時的內存使用情況,Redis 自身提供了 INFO 命令。
這里有一個 mem_fragmentation_ratio 的指標,它表示的就是 Redis 當前的內存碎片率。mem_fragmentation_ratio 大於 1 但小於 1.5。這種情況是合理的。
6)替換策略
“八二原理”,有 20% 的數據貢獻了 80% 的訪問了,而剩余的數據雖然體量很大,但只貢獻了 20% 的訪問量。
- volatile-ttl 在篩選時,會針對設置了過期時間的鍵值對,根據過期時間的先后進行刪除,越早過期的越先被刪除。
- volatile-random 就像它的名稱一樣,在設置了過期時間的鍵值對中,進行隨機刪除。
- volatile-lru 會使用 LRU 算法篩選設置了過期時間的鍵值對。
- volatile-lfu 會使用 LFU 算法選擇設置了過期時間的鍵值對。
- allkeys-random 策略,從所有鍵值對中隨機選擇並刪除數據;
- allkeys-lru 策略,使用 LRU 算法在所有數據中進行篩選。
- allkeys-lfu 策略,使用 LFU 算法在所有數據中進行篩選。
7)原子操作
原子操作是指執行過程保持原子性的操作,而且原子操作執行時並不需要再加鎖,實現了無鎖操作。
Redis 的原子操作采用了兩種方法:
- 把多個操作在 Redis 中實現成一個操作,也就是單命令操作;
- 把多個操作寫到一個 Lua 腳本中,以原子性方式執行單個 Lua 腳本。
Redis 是使用單線程來串行處理客戶端的請求操作命令的,所以,當 Redis 執行某個命令操作時,其他命令是無法執行的,這相當於命令操作是互斥執行的。
當然,Redis 的快照生成、AOF 重寫這些操作,可以使用后台線程或者是子進程執行,也就是和主線程的操作並行執行。不過,這些操作只是讀取數據,不會修改數據,所以,我們並不需要對它們做並發控制。
8)腦裂
腦裂就是指在主從集群中,同時有兩個主節點,它們都能接收寫請求。而腦裂最直接的影響,就是客戶端不知道應該往哪個主節點寫入數據,結果就是不同的客戶端會往不同的主節點上寫入數據。而且,嚴重的話,腦裂會進一步導致數據丟失。
主從切換后,從庫一旦升級為新主庫,哨兵就會讓原主庫執行 slave of 命令,和新主庫重新進行全量同步。而在全量同步執行的最后階段,原主庫需要清空本地的數據,加載新主庫發送的 RDB 文件,這樣一來,原主庫在主從切換期間保存的新寫數據就丟失了。