SDS是Redis中實現的一種數據結構,用來存儲字符串,代碼實現以下:算法
// 文件路徑:src/sds.h struct sdshdr { // 記錄buf數組中已使用字節的數量 // 等於SDS所保存字符串的長度 int len; // 記錄buf數組中未使用字節的數量 int free; // 字節數組,用於保存字符串 char buf[]; };
常規C字符串並不記錄自身的長度信息,因此爲了獲取一個C字符串的長度,程序必須遍歷整個字符串,對遇到的每一個字符進行計數,知道遇到表明字符串結尾的空字符爲止,這個操做的複雜度爲O(N)。數據庫
而SDS使用結構體實現,結構體中的len屬性直接記錄了該SDS結構體中buf數組中已使用的長度,所以獲取字符串長度時,只須要獲取len屬性的值,這個操做的複雜度爲O(1)。數組
SDS結構體的實現確保了獲取字符串長度的工做不會成爲Redis的性能瓶頸。安全
由於C字符串不記錄自身的長度,因此當進行字符串複製的時候,若是分配內存不夠,便有可能產生緩衝區溢出。數據結構
而在Redis中,當SDS API須要對SDS進行修改時,API會先檢查SDS的空間是否知足修改所需的要求,若是不知足的話,API會自動將SDS的空間擴展至執行修改所需的大小,而後才執行實際的修改操做,因此使用SDS既不須要手動修改SDS的空間大小,也不會出現前面所說的緩衝區溢出問題。curl
Redis中 strcat
的實現代碼:函數
sds sdscat(sds s, const char *t) { return sdscatlen(s, t, strlen(t)); }
Redis中 sdscatlen
的實現代碼:性能
sds sdscatlen(sds s, const void *t, size_t len) { struct sdshdr *sh; // 原有字符串長度 直接獲取len屬性 size_t curlen = sdslen(s); // 擴展 sds 空間 // T = O(N) s = sdsMakeRoomFor(s,len); // 內存不足?直接返回 if (s == NULL) return NULL; // 複製 t 中的內容到字符串後部 // T = O(N) sh = (void*) (s-(sizeof(struct sdshdr))); memcpy(s+curlen, t, len); // 更新屬性 sh->len = curlen+len; sh->free = sh->free-len; // 添加新結尾符號 s[curlen+len] = '\0'; // 返回新 sds return s; }
常規C字符串,再執行拼接操做或者截斷操做時,一般會對數組進行內存重分配,而內存重分配操做涉及複雜的算法,而且可能執行系統調用,因此它一般是一個比較耗時的操做。優化
Redis做爲數據庫,會對數據進行頻繁的修改,而且對速度要求極爲嚴苛,因此每次修改字符串長度都須要進行內存重分配會對性能形成極大的影響。url
所以,SDS實現了空間預分配和惰性空間釋放兩種優化策略。
當SDS的API對一個SDS進行修改時,程序不只會爲SDS分配必需要的空間,還會爲SDS分配額外的未使用空間。
額外分配未使用空間數量的規則:
當SDS的len屬性值小於1MB,程序分配和len屬性一樣大小的未使用空間。
當SDS的len屬性值大於1MB,程序將多分配1M的未使用空間。
經過這種預分配策略,SDS將連續增加N次字符串所需的內存重分配次數從一定N次下降爲最多N次。
當對SDS進行字符串縮短操做時,SDS的API不會當即使用內存重分配回收多出來的字節,而是使用free屬性將這些字節的數量記錄起來,等待未來使用。
固然,SDS也提供了相應的API,能夠用來真正釋放SDS的未使用空間,因此不用擔憂惰性空間釋放策略會形成內存浪費。
C字符串和SDS之間的區別
C字符串 | SDS |
---|---|
獲取字符串長度的複雜度爲O(N) | 獲取字符串長度的複雜度爲O(1) |
API是不安全的,可能形成緩衝區溢出 | API是安全的,不會形成緩衝區溢出 |
修改字符串長度N次必然須要執行N次內存重分配 | 修改字符串長度N次最多需啊喲執行N次內存重分配 |
可使用全部<string.h>庫中的函數 | 可使用一部分<string.h>庫中的函數 |
感謝huangz的《Redis設計與實現》,本文純屬讀書筆記,資源均來源於書中。
原文做者:我纔是二亮
原文連接:http://www.2liang.me/archives/264轉載必須在正文中標註並保留原文連接、做者等信息。