redis存儲編碼
1. 4、Redis高性能的根本原理
內存的讀寫速度很快
Epoll 模型
常用的五大Redis的數據結構,及他們各自的底層實現結構
string hash list set sortset(zset)
string 的底層實現是 簡單動態字元串(SDS -simple dynamic string)
hash 的底層實現是 hash表 或則 壓縮列表(ziplist)
list 的底層實現是 雙向列表(quicklist) 或者 壓縮列表
set 的底層實現是 hash表(hashtable) 或者 整數數組
sortset(zset) 的底層實現是 壓縮列表 或者 跳錶
各個數據結構的底層實現概覽
value是 string 類型的時候分為三種情況
(1)、當設置的值是整數類型的時候,redis底層會將 string 類型轉化為 int 來存儲
(2)、設置的值小於等於44個位元組的時候,使用的編碼是 embstr
(3)、設置的值大於44個位元組的時候,使用的編碼是 raw
redis是用C語言編寫的,在C語言中 string 類型是鋒頃用字元數組 char[] 來實現的。redis實現字元串的底層並沒有直接使用C語言中的字元困基磨數組的形式,而是進行了改造,構造出了一種SDS的數據結構
list的底層使用 快速雙向鏈表quicklist 或者 壓縮鏈表ziplist 來實現的。
list的底層並沒有使用傳統的雙向鏈表的結構是因為
(1)、雙向鏈表需要有一個 前指針 和 後指針 ,每個指針佔用的空間分別都是8byte, 佔用內存 比較多
(2)、雙向鏈表所通用的一個問題是會形成很多的 內存碎片
壓縮鏈表 ziplist 結構是
快速雙向鏈表 quicklist 結構
hash的底層實現為 hashtable 或者 ziplist 。
hashtable的底層實現
當數據量比較小或者單個元素的時候,底層使用的是ziplist存儲,具體可以通過配置來制定
1、 hashtable 是無序的 ziplist 是有序的
2、在能使用 hash 的情況下優先使用 hash ,不要使用 String ,因為使用太多的 String ,則會創建出過多的 key ,當 key 大量的時候,就會容易發生 hash碰撞 ,所以就需要頻繁的 rehash ,每次 rehash 就會創建2倍的內存,造成內存浪費
hash的底層實現為 整數數組intset 或者 hashtable 。
當set都為整數的時候,set的底層實現都是使用 intset 結構實現
如果set中存在字元串的值,則使用 hashtable 來實現
intset 是汪斗有序的, hashtable 是無序的
sortset 底層使用 壓縮列表ziplist 或 跳錶skiplist 的結構實現
當數據量小的情況下,使用 ziplist 實現,當數據量大的情況下使用 ziplist 實現,具體可以通過配置設置
默認設置下的底層結構
skiplist 的底層實現
查找對應元素的時候,先從最高的索引層找,例如找c 150,則先從L1找,L1的指針指向b,查看b120小於150,則繼續往後找,b的指針指向null,則向下一層找,向下一層b的指針指向c,查看c的score為150,所以找到對應的元素c
1、 https://blog.csdn.net/u010710458/article/details/80604740
2. Redis底層數據結構之string
我們都知道, Redis 是由 C 語言編寫的。在 C 語言中,字元串標准形式是以空字元 作為結束符的,但是 Redis 裡面的字元串卻沒有直接沿用 C 語言的字元串。主要是因為 C 語言中獲取字元串長度可以調用 strlen 這個標准函數,這個函數的時間復雜度是 O(N) ,由於 Redis 是單線程的,承受不了這個時間復雜度。
在上一篇文亮虧章中,我們介紹了 Redis 的 RedisObject 的數據結構,如下所示:
對於不同的對象, Redis 會使用不同的類型來存儲。對於同一種類型 type 會有不同的存儲形式 encoding 。對於 string 類型的字元串,其底層編碼方式共有三種,分別為 int 、 embstr 和 raw 。
使用 object encoding key 可以查看 key 對應的 encoding 類型,如下所示:
對於 embstr 和 raw 這兩種 encoding 類型,其存儲方式還不太一樣。對於 embstr 類型,它將 RedisObject 對象頭和 SDS 對象在內存中地址是連在一起的,但對於派畝 raw 類型,二者在內存地址不是連續的。
在介紹 string 類型的存儲類型時,我們說到,對於 embstr 和 raw 兩種類型其存儲方式不一樣,但 ptr 指針最後都指向一個 SDS 的結構。那什麼是 SDS 呢? Redis 中的字元串稱之為 Simple Dynamic String ,簡稱為 SDS 。與普通 C 語言的原始字元串結構相比, sds 多了一個 sdshdr 的頭部信息, sdshdr 基本數據結構如下所示:
可以看出, SDS 的結構有點類似於 Java 中的 ArrayList 。 buf[] 表示真正存儲的字元串內容, alloc 表示所分配的數組的長度, len 表示字元串的實際長度,並且由於 len 這個屬性的存在, Redis 可以在 O(1) 的時間復雜度內獲取數組長度。
為了追求對於內存的極致優化,對於不同長度的字元串, Redis 底層會採用不同的結構體來表示。在 Redis 中的 sds.h 源碼中存在著五種 sdshdr ,分別如下:
上面說了, Redis 底層會根據字元串的長度來決定具體使用哪種類型的 sdshdr 。可以看出, sdshdr5 明顯區別於其他四種結構,它一般只用於存儲長度不會變化,且長度小於32個字元的字元串。但現在一般都不再使用該結構, 因為其結構沒有 len 和 alloc 這兩個屬性,不具備動態擴容操作 ,一旦預分配的內存空間使用完,就需要重新分配內存並完成數據的復制和遷移,類似於 ArrayList 的擴容操作,這種操作對性能的影響很大。
上面介紹 sdshdr 屬性的時候說過, flag 這個屬性用於標識使用哪種 sdshdr 類型, flag 的低三位標識當前 sds 的類型,分別如下所示:
同時,注意到在每個 sdshdr 的頭定義上都有一個 attribute((packed)) ,這個是為了告訴 gcc 取消優化對齊 ,這樣,每個欄位分配的內存地址就是 緊緊排列在一起的 , Redis 中字元串參數的傳遞直接使用 char* 指針,其實現原理在於,由於 sdshdr 內存分配禁止了優化對齊,所以 sds[-1] 指向的就是 flags 屬性的內存地址,而通過 flags 屬性又可以確定 sdshdr 的屬性,進而可以讀取頭部欄位確定 sds 的相關屬性。
sds的邏輯圖如下所示:
相比較於 C 語言原始的字元串,塵鍵森 sdshdr 的具備一些優勢。
由於 sdshdr 中存在 len 這個屬性,所以可以在 O(1) 的時間復雜度下獲得長度;而傳統的 C 語言得使用 strlen 這個標准函數獲取,時間復雜度為 O(N) 。
原始的 C 語言一直使用與長度匹配的內存,這樣在追加字元串導致字元串長度發生變化時,就必須進行內存的重新分配。內存重新分配涉及到復雜演算法和系統調用,耗費性能和時間。對於 Redis 來說,它是單線程的,如果使用原始的字元串結構,勢必會引發頻繁的內存重分配,這個顯然是不合理的。
因而, sds 每次進行內存分配時,都會通過內存的預分配來減少因為修改字元串而引發的內存重分配次數。這個原理可以參數 Java 中的 ArrayList ,一般在使用 ArrayList 時都會建議使用帶有容量的構造方式,這樣可以避免頻繁 resize 。
對於 SDS 來說,當其使用 append 進行字元串追加時,程序會用 alloc-len 比較下剩下的空餘內存是否足夠分配追加的內容 ,如果不夠自然觸發內存重分配,而如果剩餘未使用內存空間足夠放下,那麼將直接進行分配,無需內存重分配。其擴容策略為, 當字元串佔用大小小於1M時,每次分配為 len * 2,也就是保留100%的冗餘;大於1M後,為了避免浪費,只多分配1M的空間。
通過這種預分配策略, SDS 將連續增長 N 次字元串所需的內存重分配次數 從必定 N 次降低為最多 N 次。
緩沖區溢出是指當某個數據超過了處理程序限制的范圍時,程序出現的異常操作。 原始的 C 語言中,是由編碼者自己來分配字元串的內存,當出現內存分配不足時就會發生 緩存區溢出 。而 sds 的修改函數在修改前會判斷內存,動態的分配內存,杜絕了 緩沖區溢出 的可能性。
對於原始的 C 語言字元串來說,它會通過判斷當前字元串中是否存在空字元 來確定是否已經是字元串的結尾。因而在某些情況下,如使用空格進行分割一段字元串時,或者是圖片或者視頻等二進制文件中存在 等,就會出問題。而 sds 不是通過空字元串來判斷字元串是否已經到結尾,而是通過 len 這個欄位的值。所以說, sds 還具備 二進制安全 這個特性,即可以安全的存儲具有特殊格式的二進制數據。
https://www.cnblogs.com/reecelin/p/13358432.html
3. Redis涓璖tring鍜孒ash鍝涓緇撴瀯鏇村姞鐪佸唴瀛
string緇撴瀯錛
鍙浠ョ畝鍗曚換鍔Redis鐨凷tring緇撴瀯鏄鐢⊿DS錛堢畝鍗曞姩鎬佸瓧絎︿覆錛夋暟鎹緇撴瀯鏉ュ疄鐜扮殑銆
hash緇撴瀯浣跨敤ziplist緇撴瀯鏃訛細
ziplist鏈澶х殑鐗圭偣灝辨槸錛屼粬涓嶆槸hashtable緇撴瀯錛岃屾槸涓涓姣旇緝闀跨殑瀛楃︿覆錛屽皢key-value閮芥寜鐓ч『搴忎緷嬈℃憜鏀懼埌涓涓闀塊暱鐨勫瓧絎︿覆閲屾潵瀛樺偍銆傚傛灉瑕佹壘鏌愪釜key錛屽氨闇瑕侀亶鍘嗘暣涓闀垮瓧絎︿覆銆
鍘熷洜錛 浣跨敤string瀛樺偍鏁版嵁鏃訛紝姣忎竴涓璁板綍閮芥槸涓涓猄DS閮介渶瑕佸瓨鍦╨en銆乫ree鏉ユ爣璇嗐備絾鏄浣跨敤hash鐨剒iplist鏃訛紝鍙闇瑕佹爣澶寸殑鍑犱釜鏍囪瘑浣嶅栵紝鎺ョ潃閮芥槸緔у噾鐨勬暟鎹銆傝繖灝辨槸涓轟粈涔坔ash(ziplist)姣攕tring鏇磋妭鐪佸唴瀛樼殑鍘熷洜銆
hash鏁版嵁緇撴瀯錛屽湪緙栫爜鏂瑰紡涓婃湁涓ょ嶏紝1鏄痟ashTable錛2鏄痾ipList銆
鎹㈠彞璇濊達紝zipList姣旇搗hashTable鍗犵敤鐨勭┖闂村皯錛屼絾鏄浼氳楄垂鏇村歝pu鏉ヨ繘琛屾煡璇銆
緇忚繃瀹炴祴錛寁alue鏁伴噺鍦512鏃訛紝鎬ц兘鍜屽崟綰鐨刪ashTable鍑犱箮鏃犲樊鍒錛屽湪value鏁伴噺涓嶈秴榪1024鏃訛紝鎬ц兘浠呮湁鏋佸皬鐨勯檷浣庯紝鍙浠ュ拷鐣ャ
鑰屽唴瀛樺崰鐢錛寊ipList姣攈ashTable闄嶄綆鏋佸氥
redis涓涓轟粈涔坔ash姣攕tring鍋氱紦瀛樻洿鑺傜渷鍐呭瓨涓庢晥鐜囨洿楂橈紵 - 鍍忕伃闇鎬竴鏍風湅鏃ュ嚭 - 紼嬪簭鍛業TS500
閫夋嫨鍚堥俁edis鏁版嵁緇撴瀯錛屽噺灝80%鐨勫唴瀛樺崰鐢
redis string搴曞眰鏁版嵁緇撴瀯
4. Redis底層數據結構解密
一:摘要概述
很多 redis 的使用者都可以清晰明白的道出Redis中常用的對象如string、list、hash、set、zset,一些場景比較豐富的使用者可能會說布隆過濾器、geo、Hash等。但是對於這些對象底層實現的數據結構卻是知之甚少,將會詳細闡述redis中的底層數據結構。為了彌補大家的創傷,今天分享Redis底層數據結構內容。
二:SDS
string作為redis中常用對象之一,普遍用於用戶信息緩存等場景。當string對象中encoding編碼為embstr或raw時都是採用sds作為其底層實現
2.1 SDS結構
源碼文件位於redis安裝目錄src下的sds.h,sds聲明了五種頭部類型,分別為sdshdr5、sdshdr8、sdshdr16、sdshdr32、sdshdr64。根據字元串長度創建不同頭部的sds實例
struct __attribute__ ((__packed__)) sdshdr8 {
uint8_t len;
uint8_t alloc;
unsigned char flags;
char buf[];
};
屬性名稱作用含義
len字元串長度
alloc預分配空間大小
flags低三位用於表示sds類型,可以查看sds.h文件76-82行定義
buf[]存儲字元串用數組
2.2 SDS與C字元串區別
區別描述
長度計算 c中的字元串長度計算需要數組遍歷,但是redis中的sds自身維護了len屬性。所以O(1)時間復雜度即可
緩沖區溢出c中字元串更改如果未提前做好內存分配則會內存溢出,但是sds則會根據alloc與len計算預留內存是否足夠分配重新申請內存
動態擴展 緩沖區溢出已經闡述這個概念,sds的內存空間會在字元串內容變更時自動擴展計算。策略為當字元換小於1M時*2翻倍,大於1M時每次擴容1M
惰性釋放 與空間預分配相似操作的還有內存惰性釋放,即字元串刪除某些內容後所佔用的內存空間並不會立即釋放,後續字元串變更擴展就無需再申請內存
二:ZipList
ziplist可以說把redis對於內存的極致操作體現的淋漓盡致,鏈表除了節點值之外還需要維護前後節點兩個指針,並且還會造成內存碎片。壓縮列表緊湊的內存布局,所有節點都維護在整塊內存中處理
2.1 ZipList結構
屬性名稱作用含義
zlbytes列表健佔用內存的總位元組數,在對列表健內存重分配或者是計算zlend的時候使用
zltail 指向壓縮列表起始地址的指針
zllen 壓縮列表的節點數量
entry壓縮列表保存的節點數據
zlend壓縮列表的尾節點
2.2 Entry節點結構
屬性名稱作用含義
previous_entry_length 位元組為單位記錄上一個節點的長度,如果上一個位元組長度小於254佔用1位元組。大於254佔用5位元組,第一個位元組設置為OxFE(十進制254),後面四個位元組儲存長度
encoding 記錄content記錄的數據類型以及長度。長度一、二、五位元組,值的最高位為00、01、10表示類型為位元組數組,長度使用除去最高位的其它位記錄。11開頭表示儲存整數,除去最高位其他位置表示content數據長度
content 記錄壓縮列表記錄的數據