Redis 已是你們耳熟能詳的東西了,平常工做也都在使用,面試中也是高頻的會涉及到,那麼咱們對它究竟瞭解有多深入呢?面試
我讀了幾本 Redis 相關的書籍,嘗試去了解它的具體實現,將一些底層的數據結構及實現原理記錄下來。redis
本文將介紹 Redis 中最基礎的 字符串 的實現方法。 它是Redis的字符串鍵的主要實現方法.數據庫
Redis 是使用 C 語言實現的,可是 Redis 中使用的字符串卻不是直接用的 C 語言中字符串的定義,而是本身實現了一個數據結構,叫作 SDS(simple dynamic String), 即簡單動態字符串。後端
Redis 中 SDS 數據結構的定義爲:數組
struct sdshdr{ int len; int free; char buf[]; }
一個保存了字符串Redis
的 SDS 示例圖以下:安全
帶有 free 空間的 SDS 示例:服務器
能夠看到 len 屬性和 buf 屬性的已使用部分都和第一個示例相同,可是 free 屬性爲 5, 同時 buf 屬性的除了保存了真實的字符串內容以外,還有 5 個空的未使用空間 ('0'結束字符不在長度中計算).微信
Redis 爲何要這麼作呢,或者說使用 SDS 來做爲字符串的具體實現結構,有什麼好處呢?數據結構
那麼就不得不提 C 語言原本的字符串了。
C 語言的字符串定義,是使用和字符串相等長度的字符數組來存儲字符串,而且在後面額外加一個字符來存儲空字符'0'. 也就是下圖:
這種實現方式的優勢就是,簡單且直觀。可是衆所周知,Redis 是一個性能極強的內存數據庫,這種實現方式並不能知足 Redis 的性能要求,固然,同時也有一部分的功能性要求沒法知足。
後面講述的每一條優勢,都是相對於 C 語言字符串而言的,具體的特性再具體分析。
從 C 語言字符串的結構圖中,咱們能夠看到,若是咱們想獲取一個字符串的長度,那麼惟一的辦法就是遍歷整個字符串。遍歷操做須要 O(N) 的時間複雜度。
而 SDS 記錄了字符串的長度,也就是 len屬性,咱們只須要直接訪問該屬性,就能夠拿到當前 SDS 的長度。訪問屬性操做的時間複雜度是 O(1).
Redis 字符串數據結構的 求長度的命令 STRLEN
. 內部即應用了這一特性。不管你的 string 中存儲了多長的字符串,當你想求出它的長度時,能夠隨意的執行 STRLEN
, 而不用擔憂對 Redis 服務器的性能形成壓力。
C 語言的的字符串拼接函數,strcat(*desc, const char *src)
, 會將第二個參數的值直接鏈接在第一個字符串後面,然而若是第一個字符串的空間本就不足,那麼此時就會產生緩衝區溢出。
SDS 記錄了字符串的長度,同時在 API 實現上杜絕了這一個問題,當須要對 SDS 進行拼接時,SDS 會首先檢查剩餘的未使用空間是否足夠,若是不足,會首先擴展未使用空間,而後進行字符串拼接。
所以,SDS 經過記錄使用長度及未使用空間長度,以及封裝 API, 完美的杜絕了在拼接字符串時容易形成緩衝區溢出的問題。
上面提到,C 語言的字符串實現,是一個長度永遠等於 字符串內容長度+1 的字節數組。那麼也就意味着,當字符串發生修改,它所佔用的內存空間必需要發生更改。
內存分配是比較底層的實現,其中實現比較複雜,且可能執行系統調用,一般狀況下比較耗時,Redis 怎麼進行對應的優化呢?
SDS 在進行修改以後,會對接下來可能須要的空間進行預分配。這也就是 free 屬性存在的意義,記錄當前預分配了多少空間。
分配策略:
在 SDS 修改時,會先查看 free屬性的值,來肯定是否須要進行空間擴展,若是不須要就直接進行拼接了。
經過預分配策略,SDS 連續增加 N 次,所須要的內存分配次數從絕對 N 次,變成了最多 N 次。
當 SDS 進行了縮短操做,那麼多餘的空間不着急進行釋放,暫時留着以備下次進行增加時使用。
聽起來預分配和惰性釋放是否是很簡單的道理?本質上也是使用空間換取時間的操做。並且可能發現了其中的一個問題,那就是在內存緊張的機器上,這樣浪費真的好嗎?
這個問題,Redis 固然考慮到了,SDS 也提供了對應的 API, 在須要的時候,會本身釋放掉多餘的未使用空間。
Redis 的字符串是二進制安全的這個特性,咱們應該在不少的文章中都看到了。可是它爲何能夠作到二進制安全呢?
C 語言的字符串不是二進制安全的,由於它使用空間符'0'來判斷一個字符串的結尾。也就是說,假如你的字符串是 abc\0aa\0 哈哈哈、0
, 那麼你就不能使用 C 語言的字符串,由於它識別到第一個空字符'0'的時候就結束識別了,它認爲此次的字符串值是'abc0'.
而二進制中的數據,咱們誰也說很差,若是咱們存儲一段音頻序列化後的數據,中間確定會有無數個空字符,這時候怎麼 C 語言的字符串就無能爲力了。
而 SDS 能夠,雖然 SDS 中也會在字符串的末尾儲存一個空字符,可是它並不以這個空字符爲判斷條件,SDS 判斷字符串的長度時使用 len屬性的,截取 字節數組 buf 中的前 len 個字符便可。
所以,在 SDS 中,能夠存儲任意格式的二進制數據,也就是咱們常說的,Redis 的字符串是二進制安全的。
上面提到,SDS 使用 len 屬性的長度來判斷字符串的結尾,可是,卻依然遵循了 C 語言的慣例,在字符串結尾的地方填充了一個空字符'0'.
這樣作能夠在處理一些純文本的字符串時,能夠方便的沿用一些 C 語言的庫函數,而不是本身從新爲 SDS 進行開發庫函數。
Redis 中使用字符串的大多數場景(鍵的字符串,字符串數據結構的實際值存儲等等)下,都不使用 C 語言的字符串,而是使用 SDS. 簡單動態字符串。
它的實現方式是:一個字節數組 buf, 一個當前字符串長度的記錄屬性 len, 一個當前未使用空間長度屬性 free. 字節數組的長度不要求絕對等於字符串值的真實長度,會有必定的緩衝。
相對於 C 語言的字符串,SDS 的優點以下:
C 字符串 | SDS |
---|---|
獲取字符串長度須要 O(N) | 獲取字符串長度須要 O(1) |
容易形成緩衝區溢出 | 經過封裝 API, 自動變化長度,避免緩衝區溢出 |
每次修改字符串長度,都須要內存從新分配 | 最壞狀況下,同 C 語言字符串,其餘不少狀況不須要內存重分配,直接使用預留緩衝便可。 |
只能保存純文本 | 二進制安全,能夠保存任意格式的二進制數據 |
無縫使用全部 C 庫函數 | 能夠兼容一部分的 C 庫函數 |
從官網上咱們能夠得知, Redis的key以及字符串數據結構的值, 最大的大小爲 512M.這是官網信息,基本上毋庸置疑.
讓咱們試一下:
public static void main(String[] args) { Jedis jedis = new Jedis("localhost"); jedis.set("test", "test"); byte[] bytes = new byte[1024 * 1024]; String str = new String(bytes); // 每次加1MB for (int i = 0; i < 512; i++) { jedis.append("test", str); } }
Redis會報錯, 報錯信息爲:
Exception in thread "main" redis.clients.jedis.exceptions.JedisDataException: ERR string exceeds maximum allowed size (512MB) at redis.clients.jedis.Protocol.processError(Protocol.java:132) at redis.clients.jedis.Protocol.process(Protocol.java:166) at redis.clients.jedis.Protocol.read(Protocol.java:220) at redis.clients.jedis.Connection.readProtocolWithCheckingBroken(Connection.java:309) at redis.clients.jedis.Connection.getIntegerReply(Connection.java:260) at redis.clients.jedis.Jedis.append(Jedis.java:689) at daily.JedisTest.main(JedisTest.java:50)
好的, 坐實了~.
《Redis 的設計與實現(第二版)》
完。
最後,歡迎關注個人我的公衆號【 呼延十 】,會不按期更新不少後端工程師的學習筆記。
也歡迎直接公衆號私信或者郵箱聯繫我,必定知無不言,言無不盡。
以上皆爲我的所思所得,若有錯誤歡迎評論區指正。
歡迎轉載,煩請署名並保留原文連接。
聯繫郵箱:huyanshi2580@gmail.com
更多學習筆記見我的博客或關注微信公衆號 < 呼延十 >------>呼延十