redis五种数据类型的应用场景_Redis五种不同的数据类型
一、redis集群測試環境搭建
參考文章:https://www.jianshu.com/p/0a2f8f80983a
redis-cli -c -h 10.96.87.129 -p 7001注意:-c是以集群模式啟動redis客戶端二、redis五種不同的數據類型(value)
2.1 簡單動態字符串(SDS,Simple Dynamic String)
Redis沒有直接使用C語言傳統的字符串表示,而是構建了一種名為簡單動態字符串的抽象類型即SDS。C字符串只會作為字符串字面量用在一些無需對字符串進行修改的地方比如日志打印。另外,除了用來保存數據庫中的字符串值之外,SDS還被用作緩沖區。1、SDS的定義
struct sdshdr{ int len; //記錄buf數組中已使用的字節的數量,也就是字符串的長度 int free; //記錄buf數組中未使用的字節的數量 char buf[]; //字節數組,用于保存字符串 }SDS是遵循C字符串以空字符結尾的管理,但是這一個字節是不算在SDS的len屬性中的,而是額外分配的。這樣做的好處是空字符對于SDS的使用者來說是透明的,且咱們又可以直接重用C字符串函數庫里的函數。2、SDS與C字符串的區別
- 由于len屬性的存在,SDS獲取字符串長度復雜度為o(1),而c字符串為o(n)
- SDS能杜絕緩沖區溢出,比如SDS的API sdscat字符串拼接操作在拼接之前會事先檢查SDS空間是否足夠
- 減少修改字符串時內存重新分配次數:對于C字符串在進行增長字符串的操作時,程序需要通過內存重新分配來擴展底層數組的空間大小,如果忘了這一步會產生緩沖區溢出;縮短字符串的操作,在執行操作之后,程序需要通過內存重新分配來釋放字符串不再使用的那部分空間,如果忘了會造成內存泄露。而SDS通過free未使用空間實現了空間預分配和惰性空間釋放兩種優化策略。
- 二進制安全:SDS的API都是二進制安全的,所有SDS API都會以處理二進制的方式來處理SDS存放在buf數組中的數據,程序不會對其中的數據做任何限制、過濾或者假設,數據寫入時是什么樣的,讀取時就是什么樣。Redis不是用這個數組保存字符,而是用它來保存一系列二進制數據。
- 兼容部分C字符串函數:通過遵循C字符串以空字符結尾的慣例,SDS可以在需要時重用<String.h>函數庫。
總結:
2.1 SDS與C字符串的區別2.2 鏈表
1、鏈表節點以及鏈表的定義
被廣泛用于實現Redis的各種功能,比如列表鍵、發布與訂閱、慢查詢、監視器等
typedef struct listNode{struct listNode * prev; //前置節點struct listNode * next; //后置節點void * value; //節點的值 };typedef struct list{listNode * head; //表頭節點listNode * tail; //表尾節點unsigned long len; //鏈表所包含的節點數量void *(*dup)(void *ptr); //復制鏈表節點所保存的值void (*free)(void *ptr); //釋放鏈表節點所保存的值int (*match)(void *ptr,void *key); //對比鏈表節點所保存的值和另一個輸入值是否相等 };2、Redis鏈表實現的特性
- 雙向:帶有pre和next指針,獲取某個節點的前置或者后置節點復雜度為O(1)
- 無環:表頭節點的prev指針和表尾節點的指針next都指向null
- 帶表頭指針和表尾指針:head指針和tail指針,獲取表頭和表尾的復雜度為O(1)
- 帶鏈表長度計數器:len屬性保存節點的個數,獲取節點數量的復雜度為O(1)
- 多態**:使用void*指針保存節點值,并且可以通過list結構的dup、free、match三個屬性為節點值設置類型特定函數,所以鏈表可以保存各種不同類型的值
2.3 字典
字典又稱為符號表(Symbol table)、關聯數組(Associative array)或者映射(Map),是一種用于存儲鍵值對的抽象數據結構。Redis的字典使用哈希表作為底層實現,一個哈希表可以有多個哈希表節點,每個哈希表節點就保存了字典中一個鍵值對。1、哈希表定義
typedef struct dictht{dictEntry **table;//哈希表數組,數組中的每個元素都指向dict.h/dictEntry結構的指針并保存一個鍵值對unsigned long size;//哈希表大小unsigned long sizemask;//哈希表大小的掩碼,用于計算索引值,總是等于size-1,和哈希值一起決定了鍵的位置unsigned long used; //該哈希表已有節點的數量 }2、哈希表節點定義
typedef struct dictEntry{void *key;//鍵//值,可以是一個指針或者是一個uint64_t整數或者是int64_t整數union{void *val;uint64_tu64;int64_ts64;}v;struct dictEntry *next;//指向下個哈希表節點,形成鏈表。 }3、字典的定義
typedef struct dict{dictType *type; //類型特定函數,指向dictType結構的指針,每個dictType結構保存了一簇用于操作特定類型鍵值對的函數。Redis為用途不同的字典設置不同的類型特定函數void*privdata; //私有數據,保存了需要傳遞給那些類型特定函數的可選參數dictht ht[2]; //哈希表,包含兩個哈希表,一般情況只使用ht[0]哈希表,ht[1]哈希表只會在ht[0]進行rehash時使用int rehashidx; //記錄了rehash目前的進度,當rehash不在進行時,值為-1 }4、類型特定函數的定義
typedef struct dictType{unsigned int(*hashFunction)(const void *key); //計算哈希值的函數void *(*keyDup)(void *privdate,const void *key); //復制鍵的函數void *(*valDup)(void *privdate,const void *obj); //復制值的函數int (*keyCompare)(void *privdate,const void *key1,const void *key2); //對比鍵的函數void (*keyDestructor)(void *privdate, void *key); //銷毀鍵的函數void (*valDestructor)(const void *obj); //銷毀值的函數 }5、哈希算法
添加新的鍵值對時,現根據key計算出key的哈希值,再根據哈希表的sizemask以及剛計算的哈希值計算出索引值。
index = hash & dict->ht[x].sizemask 即:第一步:`hash = dict->type->hashfunction(key0)`第二步:`index = hash & dict.ht[0].sizemask`補充下當字典被用作數據庫的底層實現或者哈希鍵的底層實現時,redis使用MurmurHash2算法計算鍵的哈希值,這種算法的好處是即使輸入的鍵是規律的,仍然能給出一個很好的隨機分布性。6、解決鍵沖突
當有兩個或兩個以上的鍵被分配到了哈希表數組的同一個索引上時,就稱為鍵發生了沖突。Redis的哈希表使用鏈地址法來解決鍵沖突,也就是每個哈希表節點都有一個next指針,多個哈希表節點可以用next指針構成一個單向鏈表,被分配到同一個索引上的多個節點可以用這個單向鏈表連接起來。另外,因為dictEntry節點組成的鏈表沒有指向鏈表表尾的指針,所以為了速度考慮,每次將新節點添加到鏈表的表頭位置,復雜度是O(1)7、Rehash
當哈希表中的鍵值對數量太多或者太少的時候,為了讓哈希表的負載因子維持在一個合理的范圍內,程序需要對哈希表的大小進行相應的擴容或者縮容- 第一步: 為字典的ht[1]哈希表分配空間,大小取決于是擴容還是縮容,如果是擴容那么ht[1]的大小為第一個大于等于ht[0].used2的2的n次方冪,假如ht[0].used*2等于7,那么size就為2的三次方為8。如果是縮容ht[1]的大小為第一個大于等于ht[0].used的2的n次方冪
- 第二步: 將ht[0]所有鍵值對重新計算hash值以及所有值并放到ht[1]哈希表對應的位置
- 第三步:當ht[0]包含的所有鍵值對都遷移到了ht[1]之后,釋放ht[0],將ht[1]設置為ht[1],并在ht[1]新創建一個空白哈希表
當以下任意條件滿足時,程序會自動對哈希表進行擴容操作:
- 服務器沒有在執行BGSAVE命令或者BGREWRITEAOF命令,且哈希表的負載因子loadFactor>=1
- 服務器正在執行BGSAVE命令或者BGREWRITEAOF命令,且哈希表的負載因子loadFactor>=5當負載因子loadFactor<0.1時,會自動縮容>
- 負載因子load_factor=ht[0].used/h[0].size
8、漸進式Rehash
當在進行擴容或縮容的時候需要將ht[0]的鍵值對rehash到ht[1]哈希表中,但是這種rehash的動作不是一次性、集中式的,而是分多次、漸進式的。不然在鍵值對的數量量級比較大的時候,一次性地將這些鍵值對全部rehash的話可能會造成服務器在一段時間內停止服務。而漸進式rehash采用分而治之的方式,將rehash鍵值對所需的計算工作均攤到對字典的每次添加、刪除、更新和查找操作上,從而避免了集中式rehash帶來的龐大計算量。漸進式的rehash的步驟:
- 為ht[1]分配空間,讓字典同時持有ht[0]和ht[1]兩個哈希表
- 在字典中維持一個索引計數器變量rehashidx,并將它的值設置為0表示rehash工作開始
- 在rehash進行期間,每次對字典執行正常的添加、刪除、更新、查找操作時,程序除了執行指定的操作以外,還會順帶將ht[0]哈希表在rehashidx索引上的所有鍵值對reash到ht[1],當rehash工作完成之后,程序將rehashidx屬性的值增加1
- 隨著字典操作的不斷執行,ht[0]的所有鍵值對都rehash到ht[1],這時rehashidx被設置為-1,表示rehash完成
- 在漸進式的rehash過程中,字典會同時使用ht[0]和ht[1]兩個哈希表,所在在此期間字典的刪除、更新、查找等操作都會在兩個哈希表上進行。而添加操作一律會被保存到ht[1]中,ht[0]不再進行任何添加操作。
2.4 跳躍表
一種有序數據結構,查找節點的復雜度平均O(logN)、最壞O(N)。Redis使用跳躍表作為有序集合鍵的底層實現之一,只在兩個地方使用到了跳躍表,一個是實現有序集合鍵,另一個是在集群節點中用作內部數據結構2.2 跳躍表結構1、跳躍表節點的定義
typedef struct zskiplistNode{//層struct zskiplistLevel{struct zskiplistNode *forward; //前進指針unsigned int span; //跨度}level[];struct zskiplistNode *backward; //后退指針double score; //分值robj *obj; //成員對象 }zskiplistNode;- 層:跳躍表節點的level數組包含多個元素,每個元素都包含一個指向其他節點的指針,程序可以通過這些層來加快訪問其他節點的速度,一般來說層數越多速度越快。每次創建一個新的跳躍表節點的時候,根據冪次定律(越大的數出現的概率越小)隨機生成一個介于1~32之間的值作為level數組的大小就是層的高度
- 前進指針:每個層都一個指向表尾方向的前進指針,用于從表頭向表尾方向訪問節點
- 跨度:層的跨度用于記錄兩個節點之間的距離,跨度是用來計算排位的:在查找某個節點的過程中,將沿途訪問過的所有層的跨度累計起來就是目標節點在跳躍表中的排位
- 后退指針:用于從表尾向表頭方向訪問節點,后退指針只能后退至前一個節點
- 分值和成員:跳躍表中的所有節點都是按照分值從小到大來排序。節點的成員對象是一個指針,它指向一個字符串對象。在同一個跳躍表中,各個節點保存的對象必須是唯一的,但是多個節點保存的分值可以是相同的,分值相同的節點將按照成員對象在字典序中大小從小到大進行排序
2、跳躍表的定義
typedef struct zskiplist{struct skiplistNode *header,*tail; //表頭節點和表尾節點unsigned long length; //表中節點的數量int level; //表中層數最大的節點的層數 }zskiplist;- header和tail指針分別指向跳躍表的表頭和表尾節點* length屬性記錄了節點的數量,表頭節點不包含leng
- length屬性記錄了節點的數量,表頭節點不包含length中* level屬性記錄了跳躍表中層高最大的那個節點的層數量,表
2.5 整數集合
intset 是集合鍵的底層實現之一,當一個集合只包含整數,且元素數量不多時,Redis會使用此作為集合鍵的底層實現??梢员4骖愋蚷nt16_t、int32_t、int64_t的整數值,并且保證集合中不會出現重復元素且是有序的1、整數集合intset的定義
typedef struct intset{uint32_t encoding; //編碼方式uint32_t length; //集合中包含的元素的數量int8_t contents[]; //保存元素的數組,各個項在數組中從小到大有序排序,且不會重復 }zskiplist;- contents數組雖然聲明為int8_t類型的數組,但實際上真正的類型取決于encoding屬性的值
- 當向一個底層為int16_t數組的整數集合添加一個int64_t類型的整數時,根據整數集合的升級規則整數集合已有的所有元素都會被轉換成int64_t類型
- 整數集合的升級的算法復雜度是O(n)
2、整數集合的升級步驟
- 根據新元素的類型,擴展整數集合底層數組的空間大小,并為新的元素分配空間
- 將底層數組現有的所有元素都轉換成與新元素相同類型,并將類型轉換后的元素放置到準確位置上,放置過程維持有序
- 將新元素添加到底層數組里面
3、升級的好處
- 提升靈活性:我們可以隨意地將int16_t、int32_t、int64_t類型的整數添加到集合中,不必擔心出現類型錯誤
- 節約內存:要讓一個數組同時保存int16_t、int32_t、int64_t三種類型,最簡單的方式是直接使用int64_t類型的數組,但是這樣會浪費內存。升級可以讓集合既能同時保存三種不同類型的值又可以確保升級操作只會在需要的時候進行。
- 整數集合不支持降級操作,一旦對數組進行了升級,就有一直保存升級后的狀態
2.6 壓縮列表ziplist
壓縮列表是列表鍵和哈希鍵的底層實現之一,當一個列表鍵或者哈希鍵包含少量列表項并且每個列表項要么是小整數值要么是長度比較短的字符串,Redis就會使用壓縮列表來做列表鍵的是底層實現。是一種為節約內存而開發的順序型結構1、壓縮列表的組成
2.3 壓縮列表的組成- zlbytes:整個壓縮列表占用的內存字節數,對壓縮列表進行內存重分配或計算zlend時使用
- zltail:記錄列表表尾節點距離起始位置有多少距離
- zllen:壓縮列表包含的節點數量
- entryX:列表節點
- zlen:特殊值,用于標記壓縮列表的末端
2、壓縮列表節點的組成
每個節點可以保存一個字節數組或者整數值
2.4 壓縮列表節點的組成- previous_entry_length:壓縮列表前一個節點的長度,長度可以是一個字節或者五個字節。如果前一個字節的長度小于254字節,那么previous_entry_length屬性的長度為1字節,前一個節點的長度就保存在這一個字節里面。如果前一個節點的長度大于等于254,那么previous_entry_length屬性的長度為5個字節,第一個字節會被設置為0xFE(十進制254),之后的四個字節用于保存前一個節點的長度。previous_entry_length屬性記錄了前一個節點的長度,所以程序可以通過指針,算出前一個節點的起始位置。
- encoding:記錄了保存的數據類型以及長度
- content:保存節點的值
3、連鎖更新
在一些特殊情況下,連續多次空間擴展操作稱之為連鎖更新。連鎖更新在最壞情況下需要對壓縮列表進行N次空間分配操作,而每次空間分配的最壞的復雜度是O(N)。所以連鎖更新的最壞復雜度是O(n^2)造成連鎖更新的概率是很低的,因為壓縮列表恰好有多個連續的、長度介于250~253之間的節點,連續更新才有可能被觸發,實際情況下出現的概率很低其次,出現連鎖更新時,只要被更新的節點數量不多,也不會造成性能問題
2.7 對象
- 對象包括字符串對象、列表對象、哈希對象、集合對象和有序集合對象五種類型。
- 使用對象的好處是Redis執行命令之前可以根據對象的類型來判斷一個對象是否可以執行給定的命令。
- 另外更靈活,可以針對不同的場景對對象設置多種不同的數據結構來實現。
- Redis的對象系統還實現了基于引用計數技術的內存回收機制。當程序不再使用某個對象的時候,這個對象占用的內存會被自動釋放。并且通過這種技術實現了對象共享機制,可以節約內存
- Redis的對象帶有訪問時間記錄信息,該信息可以用于計算數據庫鍵的空轉時長,在服務啟用了maxmemory功能情況下,空轉時長較大的那些鍵會優先被服務器刪除
1、對象的類型與編碼
typedef struct redisObject{unsigned type:4;//類型unsigned encoding:4; //編碼void *ptr; //指向底層實現數據結構的指針 }robj;- 對象的類型
- 編碼:就是對象使用了什么數據結構作為對象的底層實現
- 不同類型對象使用的不同的編碼
- 通過encoding屬性來設定對象所使用的編碼,而不是為特定類型的對象關聯一種固定的編碼,極大地提升了Redis的靈活性和效率,因為Reid可以根據不同的使用場景來為一個對象設置不同的編碼,從而優化對象在某一場景下的效率
2、字符串對象
字符串對象的編碼可以是int、raw或者embstr- 如果字符串對象保存的是整數,且這個整數值可以用long類型來表示,則字符串對象編碼為int
- 如果保存的是一個字符串值,并且這個字符串值的長度大于44字節,那么就會使用SDS來保存這個字符串值,編碼設置為raw。如果字符串長度小于等于44字節,則會使用embstr。raw會調用兩次內存分配函數來分別創建redisObject結構和sdshdr結構,而embstr編碼則通過調用一次內存分配函數來分配一塊連續的空間,空間中一次包含redisObject結構和sdshdr結構
- double類型的浮點數也是作為字符串值來保存的
- 編碼的轉換:對于int編碼的字符串對象,向對象執行了命令使得這個對象保存不再是整數值,而是一個字符串值,那么字符串對象的編碼將從int變為raw。embstr編碼的字符串對象在執行任何修改命令之后,就會變成raw編碼的字符串對象。
3、列表對象
列表對象的編碼可以是ziplist或者linkedlist編碼轉換:滿足以下兩個條件時,列表對象使用ziplist編碼
- 列表對象保存的所有字符串元素的長度都小于64字節
- 列表對象保存的元素數量小于512個。不能滿足這兩個條件的列表對象需要使用linkedlist編碼,但是在reids3.2版本以后列表統一底層實現為quicklist
4、哈希對象
哈希對象的編碼可以是ziplist或者hashtable- 如果是ziplist,先將鍵推入壓縮列表的表尾,再將值推入壓縮列表的表尾,同一鍵值對的兩個節點總是挨著的,且鍵在前值在后。另外,先加入的鍵值對放在壓縮列表的表頭方向,后加入的添加到表尾方向。
- hashtable編碼的哈希對象中每個點只對都是使用一個字典鍵值對來保存
編碼轉換:滿足以下兩個條件會使用ziplist
- 哈希對象保存的所有鍵值對的鍵和值的字符串長度都小于64
- 哈希對象保存的鍵值對數量小于512個
5、集合對象
集合對象的編碼可以是intset或者hashtable。當使用hashtable作為底層實現,字典的每個鍵都是一個包含一個集合元素的字符串對象,而每個值都為null編碼轉換:滿足以下兩個條件會使用intset
- 集合對象保存的所有元素都是整數值
- 集合對象保存的所有元素的數量不超過512個
6、有序集合對象
有序集合對象的編碼可以是ziplist或者skiplist。使用ziplist編碼的壓縮列表作為底層實現,每個集合元素使用兩個緊挨在一起的壓縮列表節點保存,第一個節點保存元素的成員(member),第二個節點保存元素的分值(score)。當使用skiplist編碼的有序集合對象使用zset結構作為底層實現,一個zset結構同時包含一個字典和一個跳躍表。跳躍表每個節點的object屬性保存了元素的成員,score屬性保存了元素的分值。字典則是保存了成員和分值的映射,字典的鍵保存了元素的成員,值保存了相應的分值。這樣可以用O(1)的復雜度查找指定元素的分值。另外,這兩種數據結構都會通過指針共享相同元素的成員和分值,所以不會浪費額外的內存。編碼轉換:滿足以下兩個條件會使用ziplist
- 有序集合保存的元素數量小于128個
- 有序集合保存的所有元素成員的長度都小于64字節
7、類型檢查與命令多態
Redis操作鍵的命令基本上分為兩種,一種是可以對任何類型的鍵執行,比如del、expire、rename、type、object等命令。另一種是只能對特定類型的鍵執行,比如- SET、GET、APPEND、STRLEN等只能用于字符串鍵
- HDEL、HSET、HGET、HLEN等命令只能對哈希鍵執行
- RPUSH、LPOP、LINSERT、LLEN等命令只能對列表鍵執行
- SADD、SPOP、SINTER、SCARD等命令只能對集合鍵執行
- ZADD、ZCARD、ZRANK、ZSCORE等命令只能對有序集合執行
7.1 類型檢查的實現
類型檢查是為了確保只有指定類型的鍵可以執行某些特定的命令,是通過redisObject結構的type屬性來實現的。7.2 多態命令的實現
根據值對象的編碼方式,選擇正確的命令實現代碼來執行命令。像del、expire、type等命令式基于類型的多態,一個命令可以同時處理多種不同類型的鍵。SET、LLEN等命令是基于編碼類型的多態,一個命令可以同時處理不同的編碼2.8 多態命令的實現7.3 內存回收
redis通過構建了一個引用計數技術實現的內存回收機制。每個對象的引用計數信息保存在redisObject結構的refcount屬性。當創建一個新對象時,引用計數的值會初始化為1;當對象被新的程序使用時,它的引用過計數值會被增1;當對象不再被一個程序使用時,它的引用計數值會被減一;當對象的引用計數變為0時,對象所占用的內存會被釋放。7.4 對象共享
實現原理也是使用對的引用計數屬性。讓多個鍵共享同一個值對象只要把鍵的值指針指向一個現有的值對象,并且把被共享的值對象的引用計數增一。Redis只對包含整數值的字符串對象進行共享。7.5 對象的空轉時長
redisObject最后一個屬性lru記錄了對象最后一次被命令程序訪問的時間。當服務器占用的內存數超過了maxmemory選項所設置的上限值時,空轉時長較高的那部分鍵會優先被服務器釋放掉,從而回收內存。總結:
- Redis數據庫中的每個鍵值對的鍵和值都是一個對象
- Redis共有字符串、列表、哈希、集合、有序集合五種類型的對象,每種類型的對象至少都有兩種或以上的編碼方式,不同的編碼可以在不同的使用場景下優化對象的使用效率
- Redis的對象系統帶有引用計數實現的內存回收機制,當一個對象不再被使用時,該對象所占用的內存就會被自動釋放
- Redis會共享值為0到9999的字符串對象
- 對象會記錄自己的最后一次被訪問的時間,這個時間可以用于計算對象的空轉時間
總結
以上是生活随笔為你收集整理的redis五种数据类型的应用场景_Redis五种不同的数据类型的全部內容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: 服务器高并发时请求报错_基于redis的
- 下一篇: python语言句块的标记_Python