本篇咱們開始講字典 key 的內部結構,也就是 sds 字符串。首先它不是普通字符串,而是 sds 字符串,這個 sds 的意思是「Simple Dynamic String」,它的結構很簡單,它是動態的,意味着能夠支持修改。不過即便是這樣簡單的字符串結構,在結構設計上做者但是煞費苦心。面試
咱們知道 C語言裏面的字符串是以0x\0結尾,一般就說是以 NULL 結尾。它不包含長度信息,當咱們須要獲取字符串長度時,須要調用 strlen(s) 來獲取長度,它的時間複雜度是 O(n),若是一個字符串太長,這個函數就太浪費 CPU了。redis
因此 Redis 不能這麼幹,它須要將長度信息使用單獨的字段進行存儲,這就須要一個額外的字段,這個字段也要佔用存儲空間。在平常使用中,小字符串纔是大頭,它的長度信息每每只須要 1byte 存儲就能夠了,能夠表示最大長度爲 255 的字符串。若是字符串再大一些,就須要 2byte,甚至是 3byte、4byte。Redis 會爲不一樣長度的字符串選擇不一樣長度的字段來表示長度信息。同時 Redis 爲了能夠直接使用標準C語言字符串庫函數,sds 的字符串內容仍是以 NULL 結尾,這會額外多佔用一個字節的空間。緩存
sds 是動態字符串,它須要支持追加操做,須要能擴充容量。若是字符串放置的比較緊湊,追加時,就須要從新分配新的更大的存儲空間,而後進行內容的拷貝(不嚴格,想一想爲何)。若是追加的太頻繁,內存的分配和拷貝就會消耗大量 CPU。架構
圖片函數
因此 Redis 爲動態字符串設計了冗餘空間,追加時只要內容不是太大,是能夠沒必要從新分配內存的,若是字符串的長度是1024,Redis 會分配2048字節的存儲空間,也就是 100% 的冗餘空間。這個設計很是相似於 Java 語言的 ArrayList 。不過 Redis 考慮的更加周到,當字符串的長度超過 1M 時,它的冗餘空間只有 1M,避免出現太大的浪費。Redis 還限制了字符串最大長度不得超過 512M。性能
下面是 sds 字符串的結構定義源碼學習
咱們平常使用的字符串都是隻讀的,通常只有拿字符串當位圖使用時纔會對字符串進行追加和修改操做。爲了不浪費,Redis 在第一次建立 sds 字符串時,不給它分配冗餘空間。在第一次追加操做以後纔會分配 100% 的冗餘空間。優化
圖片編碼
值得注意的是,咱們平時使用的字符串指針都是指向字符串內存空間的頭部,可是在 Redis 裏面咱們使用的 sds 字符串指針指向的是字符串內存空間的脖子部位,由於 sds 字符串有本身的頭部信息。debug
若是 sds 字符串只是做爲字典的 key 而存在,那麼字典裏面元素的 key 會直接指向 sds。若是 字符串是做爲 Redis的對象而存在,它還會包上一個通用的對象頭,也就是 RedisObject。對象頭的 ptr 字段會指向 sds。
講到這裏,須要提一下現代計算機的結構上在 CPU 和 內存之間存在一個緩存的結構,用來協調 CPU 的高效和訪存的相對緩慢的矛盾。咱們平時聽到的 L1 Cache、L2 Cache就是這個緩存。當 CPU 要訪問內存時先在緩存裏找一找有沒有,若是沒有就去內存裏拿了以後放到緩存裏,這個緩存的最小單位通常是 64 字節,也就是一次性緩存連續的 64 字節內容,這個最小單位稱爲「緩存行」。這樣下次獲取內存地址附近的數據時能夠直接從緩存中拿到。
對於 Redis 的字符串對象來講,咱們須要先訪問 redisObject 對象頭,拿到 ptr 指針,而後再訪問指向的 sds 字符串。若是對象頭和 sds 字符串相距較遠,就會存在緩存穿透現象,性能就會打折。因此 Redis 爲了優化硬件的緩存命中,它爲字符串設計了一種特殊的編碼結構,這種結構就是 embstr 。它將 redisObject 對象頭和 sds 字符串擠在一塊兒連續存儲,能夠一次性放到緩存行裏,這樣就能夠明顯提高緩存命中率。
object 指令觀察一下對象的編碼類型來驗證一下這個計算是否正確。
注意到上面的輸出中出現了 encoding:int 類型的編碼,這是怎麼回事呢?原來 Redis 又對整型字符串作了優化,當字符串是能夠用 long 類型表達的整數時,Redis 內部將會使用整型編碼。注意整數在 Redis 內部的類型 type 是字符串。
咱們再觀察一遍 redisObject 對象頭。
當字符串內容能夠用 long 整數表達時,對象頭的 ptr 指針將退化爲一個 long 型的整數。也就是
若是這個整數太大,超出了 long 的表達範圍,就會使用 sds 字符串表示,根據長短不一樣會分別選擇 embstr 和 raw 編碼類型。
咱們再看一個很詭異的現象
注意 debug object 指令輸出的 Value at: xxxxxxx 這個表示 redisObject 對象頭的地址。爲何值爲 9999 時,兩個對象的地址是同樣的。而變成了 10000 地址就不同了呢?
這是由於「小整數對象緩存」。Redis 在初始化的時候會構造 [0, 10000) 這1w個小整數對象持久放在內存裏,之後凡是在這個範圍內的整型字符串都會直接使用共享的小整數對象。小整數對象的引用計數字段的值恆定爲 INT_MAX。在不少面向對象的語言中,都有小整數對象緩存的概念。
接下來咱們仔細分析一下建立 embstr 的函數 createEmbeddedStringObject 的代碼
咱們能夠看到對象頭和字符串內容是經過一次zmalloc調用分配的,也就是說對象頭和字符串內容是連續的分配在一塊兒。還將 sds 字符串的 flags 設置爲 SDS_TYPE_8 說明它是一個短字符串,長度能夠直接用一個字節就能夠表示。同時在字符串內容 buf 的尾部有 '\0' 標識,這是 C 字符串的結束標誌。
歡迎工做一到五年的Java工程師朋友們加入Java架構開發:744677563
本羣提供免費的學習指導 架構資料 以及免費的解答
不懂得問題均可以在本羣提出來 以後還會有職業生涯規劃以及面試指導