共計 3646 個字符,預計需要花費 10 分鐘才能閱讀完成。
這篇文章主要介紹“Redis 數據結構和內存管理方法是什么”,在日常操作中,相信很多人在 Redis 數據結構和內存管理方法是什么問題上存在疑惑,丸趣 TV 小編查閱了各式資料,整理出簡單好用的操作方法,希望對大家解答”Redis 數據結構和內存管理方法是什么”的疑惑有所幫助!接下來,請跟著丸趣 TV 小編一起來學習吧!
一. 網絡模型
Redis 是典型的基于 Reactor 的事件驅動模型,單進程單線程,高效的框架總是類似的。網絡模型與 spp 的異步模型幾乎一致。
Redis 流程上整體分為接受請求處理器、響應處理器和應答處理器三個同步模塊,每一個請求都是要經歷這三個部分。
Redis 集成了 libevent/epoll/kqueue/select 等多種事件管理機制,可以根據操作系統版本自由選擇合適的管理機制,其中 libevent 是最優選擇的機制。
Redis 的網絡模型有著所有事件驅動模型的優點,高效低耗。但是面對耗時較長的操作的時候,同樣無法處理請求,只能等到事件處理完畢才能響應,之前在業務中也遇到過這樣的場景,刪除 redis 中全量的 key-value,整個操作時間較長,操作期間所有的請求都無法響應。所以了解清楚網絡模型有助于在業務中揚長避短,減少長耗時的請求,盡可能多一些簡單的短耗時請求發揮異步模型的最大的威力,事實上在 Redis 的設計中也多次體現這一點。
二. 數據結構和內存管理 1. 字符串 1.1 結構
Redis 的字符串是對 C 語言原始字符串的二次封裝,結構如下:
struct sdshdr {
long len;
long free;
char buf[];};
可以看出,每當定義一個字符串時,除了保存字符的空間,Redis 還分配了額外的空間用于管理屬性字段。
1.2 內存管理方式
動態內存管理方式,動態方式最大的好處就是能夠較為充分的利用內存空間,減少內存碎片化,與此同時帶來的劣勢就是容易引起頻繁的內存抖動,通常采用“空間預分配”和“惰性空間釋放”兩種優化策略來減少內存抖動,redis 也不例外。
每次修改字符串內容時,首先檢查內存空間是否符合要求,否則就擴大 2 倍或者按 M 增長;減少字符串內容時,內存并不會立刻回收,而是按需回收。
關于內存管理的優化,最基本的出發點就是浪費一點空間還是犧牲一些時間的權衡,像 STL、tcmalloc、protobuf3 的 arena 機制等采用的核心思路都是“預分配遲回收”,Redis 也是一樣的。
1.3 二進制安全
判斷字符串結束與否的標識是 len 字段,而不是 C 語言的 \0,因此是二進制安全的。
放心的將 pb 序列化后的二進制字符串存入 redis。
簡而言之,通過 redis 的簡單封裝,redis 的字符串的操作更加方便,性能更友好,并且屏蔽了 C 語言字符串的一些需要用戶關心的問題。
2. 字典(哈希)
字典的底層一定是 hash,涉及到 hash 一定會涉及到 hash 算法、沖突的解決方法和 hash 表擴容和縮容。
2.1 hash 算法
Redis 使用的就是常用的 Murmurhash3,Murmurhash 算法能夠給出在任意輸入序列下的散列分布性,并且計算速度很快。之前做共享內存的 Local-Cache 的需求時也正是利用了 Murmurhash 的優勢,解決了原有結構的 hash 函數散列分布性差的問題。
2.2 hash 沖突解決方法
鏈地址法解決 hash 沖突,通用解決方案沒什么特殊的。多說一句,如果選用鏈地址解決沖突,那么勢必要有一個散列性非常好的 hash 函數,否則 hash 的性能將會大大折扣。Redis 選用了 Murmurhash,所以可以放心大膽的采用鏈地址方案。
2.3 hash 擴容和縮容
維持 hash 表在一個合理的負載范圍之內,簡稱為 rehash 過程。
rehash 的過程也是一個權衡的過程,在做評估之前首先明確一點,不管中間采用什么樣的 rehash 策略,rehash 在宏觀上看一定是:分配一個新的內存塊,老數據搬到新的內存塊上,釋放舊內存塊。
老數據何時搬?怎么搬?就變成了一個需要權衡的問題。
第一部分的網絡模型上明確的指出 Redis 的事件驅動模型特點,不適合玩長耗時操作。如果一個 hashtable 非常大,需要進行擴容就一次性把老數據 copy 過去,那就會非常耗時,違背事件驅動的特點。所以 Redis 依舊采用了一種惰性的方案:
新空間分配完畢后,啟動 rehashidx 標識符表明 rehash 過程的開始;之后所有增刪改查涉及的操作時都會將數據遷移到新空間,直到老空間數據大小為 0 表明數據已經全部在新空間,將 rehashidx 禁用,表明 rehash 結束。
將一次性的集中問題分而治之,在 Redis 的設計哲學中體現的淋漓盡致,主要是為了避免大耗時操作,影響 Redis 響應客戶請求。
3. 整數集合
變長整數存儲,整數分為 16/32/64 三個變長尺度,根據存入的數據所屬的類型,進行規劃。
每次插入新元素都有可能導致尺度升級(例如由 16 位漲到 32 位),因此插入整數的時間復雜度為 O(n)。這里也是一個權衡,內存空間和時間的一個折中,盡可能節省內存。
4. 跳躍表
Redis 的 skilplist 和普通的 skiplist 沒什么不同,都是冗余數據實現的從粗到細的多層次鏈表,Redis 中應用跳表的地方不多,常見的就是有序集合。
Redis 的跳表和普通 skiplist 沒有什么特殊之處。
5. 鏈表
Redis 的鏈表是雙向非循環鏈表,擁有表頭和表尾指針,對于首尾的操作時間復雜度是 O(1),查找時間復雜度 O(n),插入時間復雜度 O(1)。
Redis 的鏈表和普通鏈表沒有什么特殊之處。
三.AOF 和 RDB 持久化
AOF 持久化日志,RDB 持久化實體數據,AOF 優先級大于 RDB。
1.AOF 持久化
機制:通過定時事件將 aof 緩沖區內的數據定時寫到磁盤上。
2.AOF 重寫
為了減少 AOF 大小,Redis 提供了 AOF 重寫功能,這個重寫功能做的工作就是創建一個新 AOF 文件代替老的 AOF,并且這個新的 AOF 文件沒有一條冗余指令。(例如對 list 先插入 A /B/C,后刪除 B /C,再插入 D 共 6 條指令,最終狀態為 A /D,只需 1 條指令就可以)
實現原理就是讀現有數據庫的狀態,根據狀態反推指令,跟之前的 AOF 無關。同樣,為了避免長時間耗時,重寫工作放在子進程進行。
3.RDB 持久化
SAVE 和 BGSAVE 兩個命令都是用于生成 RDB 文件,區別在于 BGSAVE 會 fork 出一個子進程單獨進行,不影響 Redis 處理正常請求。
定時和定次數后進行持久化操作。
簡而言之,RDB 的過程其實是比較簡單的,滿足條件后直接去寫 RDB 文件就結束了。
四. 多機和集群 1. 主從服務器
避免單點是所有服務的通用問題,Redis 也不例外。解決單點就要有備機,有備機就要解決固有的數據同步問題。
1.1 sync——原始版主從同步
Redis 最初的同步做法是 sync 指令,通過 sync 每次都會全量數據,顯然每次都全量復制的設計比較消耗資源。改進思路也是常規邏輯,第一次全量,剩下的增量,這就是現在的 psync 指令的活。
1.2 psync
部分重同步實現的技術手段是“偏移序號 + 積壓緩沖區”,具體做法如下:
(1)主從分別維護一個 seq,主每次完成一個請求便 seq+1,從每同步完后更新自己 seq;
(2)從每次打算同步時都是攜帶著自己的 seq 到主,主將自身的 seq 與從做差結果與積壓緩沖區大小比較,如果小于積壓緩沖區大小,直接從積壓緩沖區取相應的操作進行部分重同步;
(3)否則說明積壓緩沖區不能夠 cover 掉主從不一致的數據,進行全量同步。
本質做法用空間換時間,顯然在這里犧牲部分空間換回高效的部分重同步,收益比很大。
2.Sentinel
本質:多主從服務器的 Redis 系統,多臺主從上加了管理監控,以保證系統高可用性。
3. 集群
Redis 的官方版集群尚未在工業界普及起來,下面主要介紹一下集群的管理體系和運轉體系。
2.1 slot- 集群單位
集群的數據區由 slot 組成,每個節點負責的 slot 是在集群啟動時分配的。
2.2 客戶請求
客戶請求時如果相應數據 hash 后不屬于請求節點所管理的 slots,會給客戶返回 MOVED 錯誤,并給出正確的 slots。
從這個層面看,redis 的集群還不夠友好,集群內部的狀態必須由客戶感知。
2.3 容災
主從服務器,從用于備份主,一旦主故障,從代替主。
通過 Redis 的研究,深刻體會到的一點就是:所有設計的過程都是權衡和割舍的過程。同樣放到日常的工作和開發中也是如此,一句代碼寫的好不好,一個模塊設計的是否科學,就從速度和內存的角度去衡量看是否需要優化,并去評估每一種優化會收益到什么,同時會損失什么,收益遠大于損失的就是好的優化,這樣往往對于開發和提升更有針對性,更能提高效率。
到此,關于“Redis 數據結構和內存管理方法是什么”的學習就結束了,希望能夠解決大家的疑惑。理論與實踐的搭配能更好的幫助大家學習,快去試試吧!若想繼續學習更多相關知識,請繼續關注丸趣 TV 網站,丸趣 TV 小編會繼續努力為大家帶來更多實用的文章!