Redis的字符串數據類型底層實現原理

        在redis中定義了5種數據類型對象,分別爲字符串類型對象,列表類型對象,哈希類型對象,集合類型對象和有序集合類型對象。字符串類型對象就是其中的一種,今天就來講解一下redis中對字符串類型對象的實現。在redis的字符串類型對象中,底層都是採用的是簡單動態字符串(SDS, Simple Dynamic String)數據結構來實現。

什麼是SDS?

SDS(simple dynamic string),也叫簡單動態字符串。衆所周知,redis的底層是C語言編寫的,結構定義爲:

struct sdshdr{
	//buf數組中已使用的數量,統計字符串長度
	int len;
	//buf數組中未使用的數量
	int free;
	//buf字節數組,用於保存字符串
	char buf[];
}

個人仿造,便於java開發人員方便理解。在java中大致可以描述爲:

class SDS{//根據上面進行抽象
	private int len;
	private int free;
	private char[] buf;
	public SDS(String str){
		this.buf = str.toCharArray();
		this.len = str.length();
		this.free = 0;
	}
}
class Test{
	public static void main(String[] args){
		//定義一個簡單動態字符串
		SDS sds = new SDS("redis");
	}
}

舉個例子,如果客戶端執行命令:

redis> set msg "hello world";
OK

那麼Redis將在數據庫中創建一個新的鍵值對,其中鍵值對的鍵是一個字符串對象,對象的底層實現是一個保存着字符串“msg”的SDS。鍵值對的值也是一個字符串對象,對象的底層實現是一個保存着字符串“hello world”的SDS。

看到這裏,可能會有這樣的疑問?就是爲什麼不直接採用C語言提供的字符串(已空字符結尾的字符數組),而是才採用自己構建的簡單動態字符串(SDS),並將他作爲redis的默認字符串表示。這樣會不會很麻煩呢?

C字符串和SDS字符串比較

1、C字符串中他本身不記錄自身的長度信息,所有爲了獲取一個C字符串的長度,程序必須遍歷整個字符串,對每個字符進行計數,直到遇到空字符爲止,時間複雜度O(N)。

和C字符串相比SDS在len屬性中記錄了字符串本身的長度信息,因此獲取SDS長度的時間複雜度O(1),這極大程度上提高了性能。

sdshdr->len //c,獲取字符串長度
sds.getLen();//java,獲取字符串長度

2、C字符串還會容易產生緩衝區溢出:

char *strcat(char *dest, const char *src);

因爲C字符串不記錄自身的長度,所以strcat假定用戶在執行這個函數時,已經爲dest分配了足夠多的內存,可以容納src字符串中的所有內容,而一旦這個假定不成立時,就會產生緩衝區溢出。

舉個例子,假設程序裏有兩個在內存中緊鄰着的C字符串s1和s2,其中s1保存了字符串"Redis",而s2則保存了字符串"MongoDB",如下所示:
在這裏插入圖片描述
如果執行以下操作:

strcat(s1, " Cluster");

將s1的內容修改爲"Redis Cluster",但粗心的他卻忘了在執行strcat之前爲s1分配足夠的空間,那麼在strcat函數執行之後,s1的數據將溢出到s2所在的空間中,導致s2保存的內容被意外地修改,如下所示:
在這裏插入圖片描述
與C字符串不同的是,SDS的空間分配完全杜絕了發生緩衝區溢出的可能性,當需要對SDS進行修改時,程序首先會檢查SDS的空間是否滿足修改的需求 ( 例如,在添加時候,即free是否大於或等於添加的字符串長度 ) ,如果不符合會自動的對SDS空間進行修改,如何在執行實際的修改操作。所以使用SDS既不需要手動修改SDS的空間大小,也不會出在這裏插入代碼片現前面所說的緩衝區溢出問題。

舉個例子,SDS的API裏面也有一個用於執行拼接操作的sdscat函數,它可以將一個C字符串拼接到給定SDS所保存的字符串的後面,但是在執行拼接操作之前,sdscat會先檢查給定SDS的空間是否足夠,如果不夠的話,sdscat就會先擴展SDS的空間,然後才執行拼接操作。
例如,如果我們執行:

redis> set msg "Redis" //執行前
OK
sdscat(s, " Cluster"); 

那麼sdscat將在執行拼接操作之前檢查s的長度是否足夠,在發現s目前的空間不足以拼接"Cluster"之後,sdscat就會先擴展s的空間,然後才執行拼接"Cluster"的操作,拼接操作完成之後的SDS如下:
執行前:
在這裏插入圖片描述
執行後:
執行前
sdscat不僅對這個SDS進行了拼接操作,它還爲SDS分配了13字節的未使用空間,並且拼接之後的字符串也正好是13字節長,這種現象既不是bug也不是巧合,它和SDS的空間分配策略有關,接下來的將對這一策略進行說明。

分配策略

1.空間預分配
空間預分配用於優化SDS的字符串增長操作:當對SDS進行拼接時,程序不僅會爲SDS分配修改所必須要的空間,還會爲SDS分配額外的未使用空間。

額外分配的未使用空間數量由以下公式決定:

如果對SDS進行修改之後,SDS的長度(也即是len屬性的值)將
小於1MB,那麼程序分配和len屬性同樣大小的未使用空間,這時SDS
len屬性的值將和free屬性的值相同。舉個例子,如果進行修改之後,
SDS的len將變成13字節,那麼程序也會分配13字節的未使用空間,SDS
的buf數組的實際長度將變成13+13+1=27字節(額外的一字節用於保存
空字符)。

如果對SDS進行修改之後,SDS的長度將大於等於1MB,那麼程序會分配1MB的未使用空間。舉個例子,如果進行修改之後,SDS的len將變爲30MB,那麼程序會分配1MB的未使用空間,SDS的buf數組的實際長度將爲30MB+1MB+1byte。

通過空間預分配策略,Redis可以減少連續執行字符串增長操作所需的內存重分配次數。

2.惰性空間釋放
惰性空間釋放用於優化SDS的字符串縮短操作:當SDS的API需要
縮短SDS保存的字符串時,程序並不立即使用內存重分配來回收縮短後
多出來的字節,而是使用free屬性將這些字節的數量記錄起來,並等待
將來使用。

SDS和C字符的區別

在這裏插入圖片描述

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