震驚!Redis 的字符串居然是這樣實現的…

雲棲號資訊:【點擊查看更多行業資訊
在這裏您可以找到不同行業的第一手的上雲資訊,還在等什麼,快來!

之前本人在找工作面試時在Redis相關問題上可栽了跟頭。
在面試前按常規套路準備了一下,比如 Redis 的常用5種數據結構,Redis持久化策略,Redis實現分佈式鎖,簡單發佈訂閱等等都準備了,當時不知天高地厚以爲十拿九穩了,可是萬萬沒想到我終究還是在Redis的被問的第一個問題上翻船了~~

面試官 : 看你簡歷上寫了熟悉常用數據結構,都有哪些說說
本人 : 常用有5種,string,list,set,zset,hash(內心很得意)
面試官 : 那你說說都用過哪些數據結構_
本人 : 用的最多的是string,通常會把json字符串存進去_
面試官 : 那你知道Redis內部是怎麼實現它的string的麼?_
本人 : 呃~,我瞭解Redis是用C語言寫的,至於具體實現就不清楚了~
到此一面卒~~~
有相同經歷的朋友麼?

回去後惡補了一下Redis有關原理性的知識點,恰好最近在最總結面試經歷於是有了今天這篇文章。
本篇會講以下內容:
Redis字符串的實現
Redis字符串的性能優勢

Redis字符串的實現

Redis雖然是用C語言寫的,但卻沒有直接用C語言的字符串,而是自己實現了一套字符串。目的就是爲了提升速度,提升性能,可以看出Redis爲了高性能也是煞費苦心。

Redis構建了一個叫做簡單動態字符串(Simple Dynamic String),簡稱SDS

1.SDS 代碼結構

struct sdshdr{  
    //  記錄已使用長度  
    int len;  
    // 記錄空閒未使用的長度  
    int free;  
    // 字符數組  
    char[] buf;  
};  

SDS ?什麼鬼?可能對此陌生的朋友對這個名稱有疑惑。只是個名詞而已不必在意,我們要重點欣賞借鑑Redis的設計思路。下面畫個圖來說明,一目瞭然。

DDC5553D_87B9_4742_A27A_A4939E96877C

Redis的字符串也會遵守C語言的字符串的實現規則,即最後一個字符爲空字符。然而這個空字符不會被計算在len裏頭。

2.SDS 動態擴展特點

SDS的最厲害最奇妙之處在於它的Dynamic。動態變化長度。舉個例子

17BD48C2_D554_4706_9E36_4E1A985305DA

如上圖所示剛開始s1 只有5個空閒位子,後面需要追加' world' 6個字符,很明顯是不夠的。那咋辦?Redis會做以下三個操作:

1.計算出大小是否足夠
2.開闢空間至滿足所需大小
3.開闢與已使用大小len相同長度的空閒free空間(如果len < 1M)開闢1M長度的空閒free空間(如果len >= 1M)

看到這兒爲止有沒有朋友覺得這個實現跟Java的列表List實現有點類似呢?看完後面的會覺得更像了。

Redis字符串的性能優勢

  • 快速獲取字符串長度
  • 避免緩衝區溢出
  • 降低空間分配次數提升內存使用效率

1.快速獲取字符串長度
再看下上面的SDS結構體:

struct sdshdr{  
    //  記錄已使用長度  
    int len;  
    // 記錄空閒未使用的長度  
    int free;  
    // 字符數組  
    char[] buf;  
};  

由於在SDS裏存了已使用字符長度len,所以當想獲取字符串長度時直接返回len即可,時間複雜度爲O(1)。如果使用C語言的字符串的話它的字符串長度獲取函數時間複雜度爲O(n),n爲字符個數,因爲他是從頭到尾(到空字符'0')遍歷相加。

2.避免緩衝區溢出

對一個C語言字符串進行strcat追加字符串的時候需要提前開闢需要的空間,如果不開闢空間的話可能會造成緩衝區溢出,而影響程序其他代碼。如下圖,有一個字符串s1="hello" 和 字符串s2="baby",現在要執行strcat(s1,"world"),並且執行前未給s1開闢空間,所以造成了緩衝區溢出。

FDEC23FD_88C7_4b80_B8FE_7A97DD3449B6

而對於Redis而言由於每次追加字符串時都會檢查空間是否夠用,所以不會存在緩衝區溢出問題。每次追加操作前都會做如下操作:

  • 計算出大小是否足夠
  • 開闢空間至滿足所需大小

3.降低空間分配次數提升內存使用效率

字符串的追加操作會涉及到內存分配問題,然而內存分配問題會牽扯內存劃分算法以及系統調用所以如果頻繁發生的話影響性能,所以對於性能至上的Redis來說這是萬萬不能忍受的。

所以採取了以下兩種優化措施
空間與分配
惰性空間回收

  1. 空間預分配
    對於追加操作來說,Redis不僅會開闢空間至夠用而且還會預分配未使用的空間(free)來用於下一次操作。至於未使用的空間(free)的大小則由修改後的字符串長度決定。

當修改後的字符串長度len < 1M,則會分配與len相同長度的未使用的空間(free)
當修改後的字符串長度len >= 1M,則會分配1M長度的未使用的空間(free)
有了這個預分配策略之後會減少內存分配次數,因爲分配之前會檢查已有的free空間是否夠,如果夠則不開闢了~

  1. 惰性空間回收
    與上面情況相反,惰性空間回收適用於字符串縮減操作。比如有個字符串s1="hello world",對s1進行sdstrim(s1," world")操作,執行完該操作之後Redis不會立即回收減少的部分,而是會分配給下一個需要內存的程序。當然,Redis也提供了回收內存的api,可以自己手動調用來回收縮減部分的內存。

到此爲止結束了~

下次在遇到這個問題可以侃侃而談了,哈哈哈~

【雲棲號在線課堂】每天都有產品技術專家分享!
課程地址:https://yqh.aliyun.com/zhibo

立即加入社羣,與專家面對面,及時瞭解課程最新動態!
【雲棲號在線課堂 社羣】https://c.tb.cn/F3.Z8gvnK

原文發佈時間:2020-08-06
本文作者:小小木的博客
本文來自:“互聯網架構師”,瞭解相關信息可以關注“互聯網架構師

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章