Redis字符串類型實現內幕

摘要

Redis不只僅是一個key-value存儲,它更是一個數據結構服務,支持不一樣類型的值。這意味着在傳統的key-value存儲中,咱們用string的key關聯string的value。而在Redis中,咱們能夠存儲的值不受限於string,咱們還能夠存儲複雜的數據結構。string是咱們在使用Redis過程當中能接觸到的最簡單的數據類型,也是Memcached中僅有的類型,所以對於Redis新手來講,首先選擇使用string類型是理所固然的。這篇文章主要介紹Redis的string類型的實現內幕。api

 

初識:簡單動態字符串

Redis中使用的字符串是經過包裝的,基於c語言字符數組實現的一個抽象數據結構,後文中提到的sds指的就是簡單動態字符串,它的定義和實如今sds.h和sds.c中,結構是這樣的:數組

struct sdshdr {
    int len;
    int free;
    char buf[];
};

Redis中定義了這樣一個結構體來表示字符串,字段含義以下:安全

  • len表示buf中存儲的字符串的長度。
  • free表示buf中空閒空間的長度。
  • buf用於存儲字符串內容。

舉個例子:數據結構

圖1app

假設上面圖1是當前buf中存儲的內容,那麼這個時候len爲8,free爲2,sds的內存佔用量能夠用下面公式表示:函數

sizeof(struct sdshdr) + len + free + 1

初識了sds以後,咱們下面分別從使用字符串的時候最關心的幾個點來繼續認識sds:性能

  1. 存儲內容
  2. 長度計算
  3. 字符串拼接
  4. 字符串截斷

 

存儲內容:二進制安全字符串

Redis keys是二進制安全的,對因而不是二進制安全,簡單理解就是對於字符串結構,咱們能不能用它來存儲二進制。咱們都知道傳統的C字符串是zero-terminated的,也就是C語言字符串函數庫認爲字符串是以'\0'結尾的,所以對於用來表示字符串的C語言字符數組中中間不能有'\0',否則在處理的過程當中會出錯,好比下面這段:ui

圖2spa

咱們申請了length爲9的char數組,將每一個字母都放到對應的位置,咱們指望獲得的是"Float Lu"這樣的字符串,而實際C字符串函數處理的過程當中會覺得這個字符串是"Float",而這並非咱們指望的結果。設計

而二進制安全的字符串,Redis中給的術語是binary-safe,它容許咱們把圖2中表示的數據當作字符串來使用,那這個二進制有什麼關係呢,由於二進制數據一般會有中間某個字節存儲'\0'的這種狀況,好比咱們存儲一個JPEG格式圖片,所以二進制安全的字符串結構容許咱們存儲像JPEG格式圖片的這種數據。從而在Redis中咱們不只僅可使用傳統字符串來當作key,使用二進制來做爲key也是被容許的,好比圖片、視頻、音頻……whatever,然而你不要高興太早,Redis對key的長度是有限制的,最大長度是512MB。

 

長度計算:O(1)時間複雜度

c語言中strlen的實現

strlen在c語言中用來計算c語言字符串的長度,strlen的實現很簡單,從內存中字符串開始的位置開始掃描並計數,知道碰到第一個'\0'爲止,這也是爲何c語言字符串是zero-terminated的緣由。很顯然,strlen的時間複雜度是O(N)。

sds中sdslen的實現

sds中用於對字符串長度計算的函數爲sdslen,咱們看一下它的實現:

static inline size_t sdslen(const sds s) {
    struct sdshdr *sh = (void*)(s-(sizeof(struct sdshdr)));
    return sh->len;
}

咱們想要獲取的sds的長度就是sdshdr中定義的len的值,時間複雜度是O(1)。

 

字符串拼接:動態擴容機制

一般狀況咱們對於字符串的拼接不只僅是一次,而是不少次,咱們寫JAVA的一般頗有感觸,好比咱們要根據用戶名來拼接一個字符串,又考慮到執行效率,咱們一般會藉助於StringBuilder像下面這樣寫:

public String makeWelcomeStr(String username) {
     StringBuilder sb = new StringBuilder();
     sb.append("welcome ");
     sb.append(username);
     sb.append("!");
     return sb.toString();
}

對於C語言來講咱們並不能這麼瀟灑,咱們須要先苦逼的申請一塊內存區域將"welcome "放入,當咱們須要拼接username的時候,咱們須要苦逼的再申請一塊內存,長度爲原有內容長度加上username的長度,而後再將原有內容拷貝到新的內存區域,而後再放心的將username放入新的內存區域的後面……還有"!"沒有拼接呢,我天!

苦逼!

sds中咱們不須要考慮拼接的時候要不要擴容,擴多少容等,這些sds都爲咱們作了,咱們只須要簡單的調用sdscat便可(sds中用來拼接字符串的函數是sdscat),sdscat的核心實如今sdscatlen和sdsMakeRoomFor中,假設咱們正在拼接字符串:

圖3

個人名字是"Float Lu",我將它拼接在"welcome"後面,我不須要考慮buf的free長度是多少,能不能放下"Float Lu",咱們將要放的字符串長度爲8,看看sds是怎麼作的:

在拼接新的字符串以前會檢查當前free是否夠用,若是當前的free空間大於等於8,則不須要申請內存,直接將字符串放入,修改len和free。

若是空間不夠用,sds有一套擴容規則,接着上面的例子,老的內容長度爲len=9,新的內容長度newlen=len+8,爲16:

  1. 若是newlen小於1024(byte) * 1024(byte)=1(MB)則新的長度爲二倍的newlen。
  2. 若是newlen的長度大於等於1MB,則新的newlen的長度爲newlen的長度加上1MB。

(這讓我想起了Netty的內存擴容規則),接着上面的例子,擴容完以後的len爲16,free爲16,加上1字節的'\0'。

這個時候咱們再繼續拼接"!"的時候能夠直接將"!"放入剛纔申請多餘的內存區域內同時將len加1,將free減1便可。

sds經過預分配一些內存區域來減小內存申請,拷貝的次數,雖然預分配規則很簡單,可是是頗有效的。

 

字符串截斷:內存空間懶釋放

考慮到咱們要清理字符串中的一些內容,傳統的作法是新申請一塊內存區域,將須要保留的內容放入新的區域而後釋放原始區域,這其中必然會涉及內存的申請,拷貝。加入這個時候又有往剛纔保留的字符串後面拼接一個字符串又要涉及一些重操做,好比內存申請,拷貝。。。

咱們來看看sds是怎麼作的,在sds中提供了sdstrim這樣的一個方法,它的定義:

sds sdstrim(sds s, const char *cset)

即清除s中全部在cset中出現過的字符,看一個例子:

s = sdsnew("AA...AA.aHelloWorld::");
s = sdstrim(s,"A. :");
printf("%s\n", s);

結果是"Hello World"。

對於上面的狀況,原來的len爲21,假如free爲0,清理完成以後不涉及內存的申請操做,len爲10,free爲11,加入這個時候有字符串拼接需求,直接將內容放到free的11個字節內便可,固然是若是放的下的話。

sds並不會當即釋放掉不須要的已經申請的內存,實際中,這些內存後續極可能還能會被用到,若是你擔憂內存浪費的話,能夠手動調用sds提供的接口釋放這些空間,好比sdsfree函數。

 

sds VS c語言字符串

上面咱們分別字符串操做最常涉及到的一些問題認識了sds,最後咱們經過將sds和c語言字符串進行比較一下來總結sds的優缺點:

C語言     sds
佔用內存一般爲內容長度 佔用內存包括結構體和free的長度
非二進制安全 二進制安全
長度計算時間複雜度爲O(N) 長度計算時間複雜度爲O(1)
須要掌握字符串的長度 sds幫助咱們把握長度和內存申請
字符串拼接每次要進行內存申請和拷貝 不必定內次都要申請內存和拷貝

 

總結

sds在Redis中做爲字符串基礎服務,爲Redis的keys和其餘涉及string操做的地方提供服務,sds的設計不只考慮到api使用的安全性,更多的是爲了提升性能,爲高性能Redis奠基基礎。字符串操做方面提升性能的核心點在於儘可能減小內存的申請和內存拷貝,在設計的時候容許利用必定的內存空間換取時間效率。

 

參考文獻

《Redis Documentation》

《Redis2.8.13源碼》

《Redis設計與實現》

 

注:本文由博主原創,歡迎轉載,若有問題還請多多包涵。

相關文章
相關標籤/搜索