《閑扯Redis二》String數據類型之底層解析


原文出處http://www.yund.tech/zdetail.html?type=1&id=585ee331353551a44b29a9e9a09a1570
作者: jstarseven


一、前言

Redis 提供了5種數據類型:String(字符串)、Hash(哈希)、List(列表)、Set(集合)、Zset(有序集合),理解每種數據類型的特點對於redis的開發和運維非常重要。

二、疑問與解析

結構圖上顯示,String類型有三種實現方式

  • 使用整數值實現的字符串對象
  • 使用 embstr 編碼的動態字符串實現的字符串對象
  • 動態字符串實現的字符串對象

疑問embstr 是什么意思,動態字符串又是什么意思?字符串對象到底什么結構?三種實現方式有什么區別呢?

不急,咱們一步一步的往下看:

1、Redis中定義的對象的結構體

/*
 * Redis 對象
 */
typedef struct redisObject {
    // 類型 4bits
    unsigned type:4;
    // 編碼方式 4bits
    unsigned encoding:4;
    // LRU 時間(相對於 server.lruclock) 24bits
    unsigned lru:22;
    // 引用計數 Redis里面的數據可以通過引用計數進行共享 32bits
    int refcount;
    // 指向對象的值 64-bit
    void *ptr;
} robj;// 16bytes

注釋:type表示該對象的類型,即上面 [String,List,Hash,Set,Zset] 中的一個,但為了提高存儲效率與程序執行效率,每種對象的底層數據結構實現都可能不止一種,encoding 表示對象底層所使用的編碼。

2、Redis對象底層八種數據結構

 REDIS_ENCODING_INT(long 類型的整數)
 REDIS_ENCODING_EMBSTR embstr (編碼的簡單動態字符串)
 REDIS_ENCODING_RAW (簡單動態字符串)
 REDIS_ENCODING_HT (字典)
 REDIS_ENCODING_LINKEDLIST (雙端鏈表)
 REDIS_ENCODING_ZIPLIST (壓縮列表)
 REDIS_ENCODING_INTSET (整數集合)
 REDIS_ENCODING_SKIPLIST (跳躍表和字典)

3、embstr與動態字符串

embstr :是專門用於保存短字符串的一種優化編碼方式,跟正常的字符編碼相比,字符編碼會調用兩次內存分配函數來分別創建 redisObject 和 sdshdr 結構(動態字符串結構),而 embstr 編碼則通過調用一次內存分配函數來分配一塊連續的內存空間,空間中包含 redisObject 和 sdshdr(動態字符串)兩個結構,兩者在同一個內存塊中。從 Redis 3.0 版本開始,字符串引入了 embstr 編碼方式,長度小於 OBJ_ENCODING_EMBSTR_SIZE_LIMIT(39) 的字符串將以EMBSTR方式存儲。

注意: 在Redis 3.2 之后,就不是以 39 為分界線,而是以 44 為分界線,主要與 Redis 中內存分配使用的是 jemalloc 有關。( jemalloc 分配內存的時候是按照 8、16、32、64 作為 chunk 的單位進行分配的。為了保證采用這種編碼方式的字符串能被 jemalloc 分配在同一個 chunk 中,該字符串長度不能超過64,故字符串長度限制

OBJ_ENCODING_EMBSTR_SIZE_LIMIT = 64 - sizeof('0')為1 - sizeof(robj) 為16 - sizeof(struct sdshdr)為8 = 39)

動態字符串 :Redis 自己構建的一種名為 簡單動態字符串(simple dynamic string,SDS)的抽象類型,並將 SDS 作為 Redis 的默認字符串表示。先簡單了解概念,后面看詳細解析

4、帶着疑問來細品下面一段話

字符串的編碼可以是 int,raw 或者 embstr。如果一個字符串內容可轉為 long,那么該字符串會被轉化為 long 類型,對象 ptr 指向該 long,並且對象類型也用 int 類型表示。普通的字符串有兩種 embstr 和 raw。如果字符串對象的長度小於 39 字節,就用 embstr,否則用 raw。

也就是說,Redis 會根據當前值的類型和長度決定使用內部編碼實現:恍然大悟

int:8個字節的長整型
embstr:小於等於39個字節的字符串
raw:大於39個字節的字符串

5、實踐驗證

命令:object encoding key ,獲取數據底層的數據結構

1)整數類型示例如下:

2)短字符串示例如下:

3)長字符串示例如下:

疑問:至此,我們知道了embstr、字符串對象, 但是動態字符串的結構還是沒說清楚啊,你是不是在逗我?
靚仔疑問,再一次出現,別急,繼續往下看

三、動態字符串

眾所周知,Redis 是用 C 語言寫的,但是對於 Redis 的字符串,卻不是 C 語言中的字符串(即以空字符 ’\0’ 結尾的字符數組),它是自己構建了一種名為 簡單動態字符串(simple dynamic string,SDS)的抽象類型,並將 SDS 作為 Redis 的默認字符串表示。

1、動態字符串結構分析

SDS 定義

struct sdshdr{
     //記錄buf數組中已使用字節的數量
     //等於 SDS 保存字符串的長度 4byte
     int len;
     //記錄 buf 數組中未使用字節的數量 4byte
     int free;
     //字節數組,用於保存字符串 字節\0結尾的字符串占用了1byte
     char buf[];
}

用 SDS 保存字符串 “Redis” 具體結構如下圖


對於 SDS 數據類型的定義:

  • len 保存了SDS保存字符串的長度
  • buf[] 數組用來保存字符串的每個元素
  • free 記錄了 buf 數組中未使用的字節數量

上面的定義相對於 C 語言對於字符串的定義,多出了 len 屬性以及 free 屬性。為什么不直接使用 C 語言字符串實現,而是要使用 SDS 呢?有什么特別的優勢呢?

2、SDS結構與C語言字符串結構比較分析
7

1)獲取字符串長度復雜度

 sdshdr 中由於 len 屬性的存在,獲取 SDS 字符串的長度只需要讀取 len 屬性,時間復雜度為 O(1),而對於 C 語言來說,獲取字符串的長度通常是遍歷字符串計數來實現的,時間復雜度為 O(n)。

2)API安全性與緩沖區溢出

  緩沖區溢出(buffer overflow):是這樣的一種異常,當程序將數據寫入緩沖區時,會超過緩沖區的邊界,並覆蓋相鄰的內存位置。在 C 語言中使用 strcat 函數來進行兩個字符串的拼接,一旦沒有分配足夠長度的內存空間,就會造成緩沖區溢出,如

8

s1 = 'Redis',s2 = 'MongoDB',當執行strcat(s1, " Cluster")時,未給 s1 分配足夠內存空間,s1 的數據將溢出到 s2 所在的內存空間,導致 s2 保存的內容被意外地修改。

由於 SDS 記錄了自身長度,同時在修改時,API 會按照如下步驟進行:

   (1)先檢查SDS的空間是否滿足修改所需的要求;

   (2)如果不滿足要求的話,API 會自動將 SDS 的空間擴展至執行修改所需的大小(realloc);

   (3)然后才執行實際的修改操作;

    所以SDS不會造成緩沖區溢出情況

3)字符串的內存重分配次數

 C 語言由於不記錄字符串的長度,所以如果要修改字符串,必須要重新分配內存。
 SDS 實現了空間預分配和惰性釋放兩種策略:
(1)空間預分配:當 SDS 的 API 對一個 SDS 進行修改,並且需要對 SDS 進行空間擴展的時候,程序不僅會為 SDS 分配修改所必須的空間,還會為 SDS 分配額外的未使用空間,這樣可以減少連續執行字符串增長操作所需的內存重分配次數。
(2)惰性釋放:當 SDS 的 API 需要對 SDS 保存的字符串進行縮短時,程序並不立即使用內存重分配來回收縮短后多出來的字節,而是使用 free 屬性將這些字節的數量記錄起來,並等待將來使用,如

sdstrim(s, "XY"); // 移除 SDS 字符串中的所有 'X' 和 'Y' 

結果

4)二進制數據安全

 二進制安全(binary-safe):指能處理任意的二進制數據,包括非 ASCII 和 null 字節。
 C 字符串以空字符 '\0',作為字符串結束的標識,而對於一些二進制文件(如圖片等),內容可能包括空字符串'\0',導致程序讀入的空字符會被誤認為是字符串的結尾,因此C字符串無法正確存取二進制數據;
 SDS 的 API 都是以處理二進制的方式來處理 buf 里面的元素,並且 SDS 不是以空字符串'\0'來判斷是否結束,而是以 len 屬性表示的長度來判斷字符串是否結束,

因此 Redis 不僅可以保存文本數據,還可以保存任意格式的二進制數據。

5)C字符串函數兼容

  SDS 的buf數組會以'\0'結尾,這樣可以重用 C 語言庫<string.h> 中的一部分函數,避免了不必要的代碼重復。

四、要點總結

String 類型對象三種實現方式,int,embstr,raw
字符串內容可轉為 long,采用 int 類型,否則長度<39(3.2版本前39,3.2版本后分界線44) 用 embstr,其他用 raw
SDS 是Redis自己構建的一種簡單動態字符串的抽象類型,並將 SDS 作為 Redis 的默認字符串表示
SDS 與 C 語言字符串結構相比,具有五大優勢


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM