Redis设计与实现之SDS和链表
Redis的數據結構與對象
- redis數據庫里的鍵值對都是由object組成的,數據庫建總是一個字符串對象。
- redis數據庫里的值可以是字符串對象,列表對象,哈希對象,集合對象,有序集合對象中的一種。
Redis的SDS(simple dynamic string ,簡單動態字符串)
比如我們可以在redis客戶端敲
redis> set msg "helloRedis"那么就會在Redis數據庫中創建一個新的鍵值對,keg是msg,value是helloRedis,兩個都是SDS。
又比如在redis客戶端敲
redis>rpush number 1 2 3這也就是一個list對象(列表對象),numbe為鍵,1,2,3為值。經過這幾個小的例子你會發現Redis的用法很簡單,SDS還可以被用來作緩沖區(buffer):AOF模塊的AOF緩沖區和客戶端狀態中的輸入緩沖區。
說了這么多的SDS,那么到底什么是SDS,SDS又是怎么實現的呢,那它又和我們傳統的字符串有什么區別呢?
我們可以用c的結構體來表示SDS的定義信息
struct sds{ int len; int free; char buf[]; }那這些都是代表什么意思呢?
len:用來記錄字符串的長度,也就是buf已使用的字節數量
free:用來記錄buf中剩余的數量
buf:是一個char數組,用來保存字符串的每個字符,字符串最后必須是一個空字符’\0’,不占用空間但是占用位置。這些添加字符串后綴都是由redis的內部函數來實現。
不止這一點redis的設計和c很像,redis還有很多和c相同的地方,因為這樣就可以直接重用一些c寫好的函數。
現在是不是對SDS有一些了解了,從結構可以看出它比c中的字符串多了len和free,這寫參數可以決定怎樣的性能呢?
redis獲取字符串長度時間復雜度低,保證了效率
- SDS有len來記錄字節數,獲取字符串長度操作時間復雜的理所當然的為O(1)
- 普通字符串它要遍歷計數,他要找到最后一個字符,因此它的時間復雜度為O(n),因此SDS的strlen函數不會對性能造成任何影響。
SDS不會造成緩沖去溢出
這個原因的實質還是因為沒有len,我們假設在計算機內存中按順序排列著S1,S2字符串,S2緊跟著S1字符串。如果是普通字符串進行拼接操作
- Reids如果使用拼接函數(sdscut),計算S1拼接后的字符串長度很簡單并調用函數為S1分配足夠的空間,然后進行字符串拼接操作就不會造成由于S1內存不夠而溢出到S2上去。
- 普通字符串要使用拼接函數(strcut)還必須要計算所占空間數,并且一不小心忘記這步操作就可能會使S1拼接后的字符串由于沒有位置而擠到S2的空間去,造成S1的緩存溢出。
SDS會很大程度減少內存重分配次數
為什么這么說呢?相信大家以前也做過字符串修改的函數,我們當時函數底層是怎樣的呢?在我們每次修改字符串時系統都會執行內存重分配算法,如果不經常修改的話這都不是問題(前提是不粗心,不然增的時候可能會造成內存溢出,刪的時候可能會造成內存泄漏),但是數據庫就不一樣,因為數據庫的修改操作很頻繁,因此redis采用的緩存策略。
- redis的預分配策略(主要優化字符個數增加問題):如果字符串小于1mb,那就給它再分配同樣大小的空間作為預分配空間。比如本來是10個字節(len的值)在reids的策略后就是20個字節+一個空字符=21。如果大于1mb的話,那么就會給它分配1mb的內存,比如本來是10mb+1byte,在分配策略后就20mb+1byte,這樣在頻繁的修改過程中不會經常去調用內存分配,因此在性能上就占優一點。
- 惰性空間釋放(主要優化字符個數減少問題):如果是縮短了字符串,那redis并不會立即把它多余的空間釋放掉,而是將它加入到free中可以再一度的優化字符串增長操作。但是如果在空間緊張或者有需要的時候redis也有相應的API來釋放未使用的空間,因此也就不用擔心內存浪費的問題了。
二進制安全
這個也是c中的字符串中比較難受的事情了,比如我們輸入一串數字再加上空格再加上一串數字,c就只會把它的前一段寫進去,因為c判斷結束的標識是空字符。但是Redis就不會出現這種問題,輸入進去什么樣子,輸出出來就是什么樣子,為什么呢?歸根結底還是因為有len,因為它可以記住個數而不是一味的根據空字符來判斷是不是結束了,因此它上邊就可以存任何類型的東西。比如傳一串二進制字符串,c可能得需要很復雜的處理,但是Redis根據SDS的定義就可以很輕松的解決這個問題。那Redis可以不給末尾加\0也可以實現為什么要加,還是因為它可以重用一些c的庫函數。
總結:
SDS相對于普通字符串的優點(歸根結都還是結構體的設計問題)
- 獲取長度復雜度小
- 杜絕緩沖的溢出
- 減少再修改字符串時內存重分配的次數
- 二進制安全
Redis中的鏈表
因為c里沒有鏈表庫,因此Redis就自己實現了鏈表,那鏈表在Redis中扮演一個什么的角色呢?它在Redis中的應用非常廣泛,比如鏈表鍵,發布與訂閱,慢查詢,監視器等功能,Redis服務器本身還使用鏈表來保存客戶端的狀態信息。
鏈表的操作就很簡單了,因為它就和糖葫蘆一樣一個一個穿成一串。沒有寫過或者了解鏈表的可以去一些數據結構或者算法導論等一些書上去參考更詳細的,我這就講個中心意思。
設計數據結構模型,給Node模型加上前驅和后記和節點的值。
typedef struct listNode{struct lsitNode *preb;struct listNode *next;void *value;}listNode;然后就通過指針操作頭插或尾插進行連接成一個雙向鏈表。
但是Redis是以高性能著稱的,按照上述設計那不就是一個普通的單鏈表么,因此個數據結構仍有改進的空間。Redis后來就自己設計了如下結構的節點結構。
typedef struct list{listNode *head;//表頭節點listNode *tail;//表尾unsigned long len;//鏈表所包含的字節數void *( * dup)(void *ptr);//節點復制函數void (*free)(void *ptr);//節點釋放函數int (*match)(void *ptr,void key);//節點值對比函數,對比鏈表節點所保存的值和另一個輸入值是否相等}list;這種相對于上一種的優勢所在是帶有長度計數器,和節點的特定函數。
可以總的來說一下Redis的鏈表實現的特性:
1.雙向鏈表獲取前驅和后繼的時間復雜度都是O(1)
2.是一個沒有頭節點的鏈表,最小是空
3.帶有表頭和表尾,獲取頭和尾時間復雜度為O(1)
4.多種功能:幾個特殊設定的函數,可用于保存各種不同類型的值
雖然感覺上述這些并不是很難懂,但是它都設計的很巧妙,對查詢和修改都優化了不少。
?
總結
以上是生活随笔為你收集整理的Redis设计与实现之SDS和链表的全部內容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: redis性能9个checklist和实
- 下一篇: Redis设计于实现之字典