Redis五種數據類型詳解
什么是 Redis
Redis 是基于內存的 K-V 數據庫,常用于緩存、消息隊列,分布式鎖等場景,并且提供了常見的數據結構:字符串、哈希、列表、集合、帶排序的集合
Redis 數據類型詳解
前置知識
Redis中的任意數據類型的鍵和值都會被封裝為一個 RedisObject
typedef struct redisObject { unsigned type:4; unsigned encoding:4; unsigned lru:LRU_BITS; /* LRU time (relative to global lru_clock) or * LFU data (least significant 8 bits frequency * and most significant 16 bits access time). */ int refcount; void *ptr; } robj;
type
:指具體的對象類型
encoding
:底層編碼方式
lru
:記錄最后一次被訪問的時間
refcount
:對象引用計數器,計數器為 0 時表示沒有被引用,可以回收
ptr
:實際存儲的值
五種數據結構
Redis中會根據存儲的數據類型不同,選擇不同的編碼方式。每種數據類型的使用的編碼方式如下:
數據類型 | 編碼方式 |
---|---|
OBJ_STRING | int、embstr、raw |
OBJ_LIST | LinkedList和ZipList(3.2以前)、QuickList(3.2以后) |
OBJ_SET | intset、HT |
OBJ_ZSET | ZipList、HT、SkipList |
OBJ_HASH | ZipList、HT |
Redis 的編碼方式
Redis 中會根據存儲的數據類型不同,選擇不同的編碼方式,共包含 11 種不同類型:
編號 | 編碼方式 | 說明 |
---|---|---|
0 | OBJ_ENCODING_RAW | raw編碼動態(tài)字符串 |
1 | OBJ_ENCODING_INT | long類型的整數的字符串 |
2 | OBJ_ENCODING_HT | hash表(字典dict) |
3 | OBJ_ENCODING_ZIPMAP | 已廢棄 |
4 | OBJ_ENCODING_LINKEDLIST | 雙端鏈表 |
5 | OBJ_ENCODING_ZIPLIST | 壓縮列表 |
6 | OBJ_ENCODING_INTSET | 整數集合 |
7 | OBJ_ENCODING_SKIPLIST | 跳表 |
8 | OBJ_ENCODING_EMBSTR | embstr的動態(tài)字符串 |
9 | OBJ_ENCODING_QUICKLIST | 快速列表 |
10 | OBJ_ENCODING_STREAM | Stream流 |
String
介紹
字符串 (string) 其實是最簡單的數據結構,不僅可以存儲字符串,還可以存儲整數、浮點數、二進制數據等;
常用命令
set key value
:添加一個 string 類型的鍵值對
get key
:獲取 key 對應的 value
mset
:批量設置對個鍵值對
mget
:批量獲取多個鍵值對
incr
:讓一個整形自增 1
setnx
:添加一個 string 類型的字符串,前提是這個 key 不存在
使用場景
緩存對象
直接緩存整個 json 字符串對象 SET user:1 '{"name":"ezreal", "age":18}'
緩存更新策略
- 使用 redis 的內存淘汰機制;
- 超時剔除,給數據添加 TTL 過期時間;
- 客戶端主動更新緩存;
分布式鎖
使用 setnx 指令可以實現分布式鎖,若設置值成功則表示獲取鎖成功,設置失敗則獲取鎖失敗,注意要設置該 key 的過期時間,防止一直搶占該鎖
常規(guī)計數
可以通過 incr
指令來統(tǒng)計數量,前提設置的 value 需要是整形
共享 session
我們可以把多臺服務器上的 session 統(tǒng)一放到 redis 中,無論向集群哪臺服務器發(fā)送消息,都保證去 redis 中獲取 session 的信息
底層實現
redis 的底層實現是 SDS 字符串
SDS 結構體
struct __attribute__ ((__packed__)) sdshdr8 { uint8_t len; /* buf已保存的字符串字節(jié)數,不包含結束標示 */ uint8_t alloc; /* buf申請的總的字節(jié)數,不包含結束標示 */ unsigned char flags; /* 不同的SDS頭類型 */ char buf[]; };
len
:該字符串的 長度
alloc
:buf 申請的 總字節(jié)數
buf
:實際存儲數據的位置
flag
:SDS 頭類型,用來控制SDS頭大小 :SDS_TYPE_5
,SDS_TYPE_8
,SDS_TYPE_16
,SDS_TYPE_32
,SDS_TYPE_64
整形的存儲
如果存儲的是一個整形,則則直接存儲在 RedisObject 中即可;
SDS 動態(tài)擴容
假如現在在 name 字符串后加入 ezreal,形成新的字符串 'nameezreal'
- 若新字符串的長度小于 1M,則新的內存空間為 新字符串 的長度的兩倍 + 1;
- 若新字符串的長度大于 1M,則新的內存空間為 新字符串 的長度 + 1M + 1,稱為內存預分配;
List
介紹
列表是一個有序的字符串集合,可以通過下標索引來訪問指定位置的字符串,還可以在列表頭部或者尾部來添加或刪除元素
常用命令
LPUSH KEY
:向列表左側插入元素
RPUSH KEY
:向列表右側插入元素
LPOP KEY
:向列表左側彈出元素
RPOP KEY
:向列表右側彈出元素
LRANGE KEY star end
:獲取指定范圍內的 key
BLPOP / BRPOP
:與 LPOP / RPOP 類似,只不過在沒有元素時會等待
使用場景
消息隊列
消息隊列實現的三大要素:消息保序,處理重復的消息,保證消息的可靠性
- 消息保序
我們可以基于 LPUSH / RPOP 的方法來實現一個隊列,消息先進先出,保證消息的順序;
當有一個缺點,消費者只能不斷的輪詢該隊列是否有消息,因為消費者新增一條消息后是不能通知消費者的,所以消費者要不斷輪詢 while(1)
,不斷的占用 CPU。我們可以使用 BRPOP
來優(yōu)化,消費者沒有獲取到消息時,會自動阻塞,直到生產者放入新的消息。
- 處理重復的消息
我們可以為每個消息生成一個 唯一 的消息 ID,每次消費消息時,從數據庫中判斷該消息是否已經被處理了,防止消息的重復消息。
- 保證消息可靠性
當消息被消費者獲取后,沒有處理完消息,機器就宕機了,這樣不僅消息沒有完全被消費,List 中的消息也丟失了,從而導致這個任務永遠丟失了;
我們可以使用 Redis 中的BRPOPLPUSH
命令,作用是把該消息從 List 中拿出來,同時還會把該消息放到另一個 List 中作留存。
底層實現
在 Redis 3.2 之前,List 的底層實現是壓縮列表 ( ZipList
) 或 雙向鏈表
在 Redis 3.2 之后,List 的底層實現是 QuickList
ZipList
ZipList 又叫壓縮列表,類似 "雙向鏈表",但其不是鏈表。它由一段連續(xù)的內存塊組成,可以在列表頭部或者尾部添加或者刪除元素。
zlbytes
:整個 ZipList 的 內存字節(jié)數,占用 4 字節(jié)
zltail
:從 ZipList 的起始地址到最后一個 entry 的內存字節(jié)數,占用 4 字節(jié)
zllen
:ZipList 中 entry 的個數,占用 2 字節(jié)
zlend
:結束標識 0xff
,占用 1 字節(jié)
ZipEntry結構
previous_entry_length
:上一個 entry 的長度,占用 1個 或 5個字節(jié)
- 若上一個 entry 的長度小于 256 字節(jié),則 encoding 使用 1 個字節(jié)來存儲
- 若上一個 entry 的長度大于 256 字節(jié),則 encoding 使用 5 個字節(jié)來存儲
encoding
:記錄 content 的類型 [ 整形 or 字符串 ] 和長度,占用 1個,2個 或者 5個字節(jié)
content
:實際的內容
Encoding 編碼
Entry 中的 encoding 編碼分為字符串和整數兩種:
字符串:如果 encoding 是以“00”“01”或者“10”開頭,則證明 content 是字符串
整數:如果encoding是以“11”開始,則證明content是整數,且encoding固定只占用1個字節(jié)
編碼 | 編碼長度 | 整數類型 |
---|---|---|
11000000 | 1 | int16_t(2 bytes) |
11010000 | 1 | int32_t(4 bytes) |
11100000 | 1 | int64_t(8 bytes) |
11110000 | 1 | 24位有符整數(3 bytes) |
11111110 | 1 | 8位有符整數(1 bytes) |
1111xxxx | 1 | 直接在xxxx位置保存數值,范圍從0001~1101,減1后結果為實際值 |
可見,ZipList 通過記錄上一個entry 的長度 和 自身entry 的長度(length + encoding + content),從而可以快速得到上一個 entry 和下一個 entry ,從而實現從頭到尾 或 從尾到頭的遍歷。
QuickList
QuickList 是一個雙向鏈表,它的每一個節(jié)點都是 ZipList
為了避免 QuickList 中的每個 ZipList 中 entry 過多,Redis 提供了一個配置項:list-max-ziplist-size
來限制。
如果值為正,則代表 ZipList 的允許的 entry 個數的最大值;
如果值為負,則代表ZipList的最大內存大小,分5種情況:
- -1:每個 ZipList 的內存占用不能超過 4kb
- -2:每個 ZipList 的內存占用不能超過 8kb
- -3:每個 ZipList 的內存占用不能超過 16kb
- -4:每個 ZipList 的內存占用不能超過 32kb
- -5:每個 ZipList 的內存占用不能超過 64kb
其默認值為 -2:
以下是 QuickList 的和 QuickListNode 的結構源碼:
QuickList
typedef struct quicklist { quicklistNode *head; // 頭結點指針 quicklistNode *tail; // 尾結點指針 unsigned long count; /* total count of all entries in all ziplists */ unsigned long len; /* number of quicklistNodes */ int fill : QL_FILL_BITS; /* fill factor for individual nodes */ unsigned int compress : QL_COMP_BITS; /* depth of end nodes not to compress;0=off */ unsigned int bookmark_count: QL_BM_BITS; quicklistBookmark bookmarks[]; } quicklist;
QuickListNode
typedef struct quicklistNode { struct quicklistNode *prev; // 前一個指針 struct quicklistNode *next; // 后一個指針 unsigned char *zl; // 當前結點的ziplist指針 unsigned int sz; /* ziplist size in bytes */ unsigned int count : 16; /* count of items in ziplist */ unsigned int encoding : 2; /* RAW==1 or LZF==2 */ unsigned int container : 2; /* NONE==1 or ZIPLIST==2 */ unsigned int recompress : 1; /* was this node previous compressed? */ unsigned int attempted_compress : 1; /* node can't compress; too small */ unsigned int extra : 10; /* more bits to steal for future usage */ } quicklistNode;
整體結構
Hash
介紹
Hash 是一個鍵值對的集合,形如:key:{field1: value1,field1: value1}
,非常適合存儲一個對象
常用命令
# 存儲一個哈希表key的鍵值 HSET key field value # 獲取哈希表key對應的field鍵值 HGET key field # 在一個哈希表key中存儲多個鍵值對 HMSET key field value [field value...] # 批量獲取哈希表key中多個field鍵值 HMGET key field [field ...] # 刪除哈希表key中的field鍵值 HDEL key field [field ...] # 返回哈希表key中field的數量 HLEN key # 返回哈希表key中所有的鍵值 HGETALL key # 為哈希表key中field鍵的值加上增量n HINCRBY key field n
使用場景
緩存對象
例如下圖,對于使用 string + json
還是用 hash
來存儲,建議優(yōu)先選擇 string + json
,對于某些字段經常改變的話,可以考慮使用 hash
購物車
以用戶 id 為 key,商品 id 為 field,商品數量為 value,恰好構成了購物車的 3 個要素,如下圖所示。
涉及的命令如下:
- 添加商品:HSET cart:{用戶id} {商品id} 1
- 添加數量:HINCRBY cart:{用戶id} {商品id} 1
- 商品總數:HLEN cart:{用戶id}
- 刪除商品:HDEL cart:{用戶id} {商品id}
- 獲取購物車所有商品:HGETALL cart:{用戶id}
當前僅僅是將商品 ID 存儲到了 Redis 中,在回顯商品具體信息的時候,還需要拿著商品 id 查詢一次數據庫,獲取完整的商品的信息。
底層實現
Hash 的底層實現可以是 ZipList 或者 Dict
- Hash結構默認采用
ZipList
編碼,用以節(jié)省內存。ZipList
中相鄰的兩個entry
分別保存field
和value
- 當 ZipList 的元素個數小于 512(默認)且每個 Entry 的大小小于 64 字節(jié)(默認),則會使用 ZipList 來作為 Hash 的底層實現,否則則使用 Dict
Dict
Dict 中維護一個 哈希表 DitHashTable
,里面有一個 DictEntry 數組,用來存儲實際的 key-value
,每個槽位下的 DictEntry 都會形成一個鏈表,處理哈希沖突
向 Dict 插入一個元素時,先會根據 key 計算出一個哈希值 h,然后計算 h & sizemask 得到數組下標,最后加入該 DictEntry 即可
數據結構體
字典(Dict)
typedef struct dict { dictType *type; // dict 類型,不同的哈希函數 void *privdata; // 私有數據,做特殊hash的時候用 dictht ht[2]; // 包含兩個hash表,一個是當前數據,另個為空,rehash的時候用 long rehashidx; // rehash的進度 int16_t pauserehash; // rehash是否暫停 } dict;
哈希表(DictHashTable)
typedef struct dictht { dictEntry **table; // 指向entry 的數組 unsigned long size; // 哈希表大小 unsigned long sizemask; // 掩碼:size - 1 unsigned long used; // entry 的個數 } dictht;
哈希節(jié)點(DictEntry)
typedef struct dictEntry { void *key; // 鍵 union { void *val; uint64_t u64; int64_t s64; double d; } v;// 值 struct dictEntry *next;// 下一個結點 } dictEntry;
Dict 的 rehash
不管是擴容還是收縮,必定會創(chuàng)建新的哈希表,導致哈希表的 size 和 sizemask 變化,而 key 的查詢與 sizemask 有關。因此必須對哈希表中的每一個 key 重新計算索引,插入新的哈希表,這個過程稱為 rehash。過程是這樣的:
- 計算新hash表的realeSize,值取決于當前要做的是擴容還是收縮:
- 如果是擴容,則新size為第一個大于等于
dict.ht[0].used + 1
的2^n - 如果是收縮,則新size為第一個大于等于
dict.ht[0].used
的2^n (不得小于4)
- 如果是擴容,則新size為第一個大于等于
- 按照新的 realeSize 申請內存空間,創(chuàng)建 dictht,并賦值給 dict.ht[1]
- 設置 dict.rehashidx = 0,標示開始 rehash
- 將 dict.ht[0] 中的每一個 dictEntry 都 rehash 到 dict.ht[1]
- 將 dict.ht[1] 賦值給 dict.ht[0],給 dict.ht[1] 初始化為空哈希表,釋放原來的dict.ht[0]的內存
- 將 rehashidx 賦值為 -1,代表 rehash 結束
- 在rehash過程中,新增操作,則直接寫入ht[1] ,查詢、修改和刪除則會在dict.ht[0]和dict.ht[1]依次查找并執(zhí)行。這樣可以確保 ht[0] 的數據只減不增,隨著 rehash 最終為空
Set
介紹
Set 是 Redis 中的無序集合,可以保證元素的唯一性,但不一定保證元素的有序性,可以對 set 取交集,并集等操作
常用命令
Set 常用操作
# 往集合key中存入元素,元素存在則忽略,若key不存在則新建 SADD key member [member ...] # 從集合key中刪除元素 SREM key member [member ...] # 獲取集合key中所有元素 SMEMBERS key # 獲取集合key中的元素個數 SCARD key # 判斷member元素是否存在于集合key中 SISMEMBER key member # 從集合key中隨機選出count個元素,元素不從key中刪除 SRANDMEMBER key [count] # 從集合key中隨機選出count個元素,元素從key中刪除 SPOP key [count]
Set 集合操作
# 交集運算 SINTER key [key ...] # 將交集結果存入新集合destination中 SINTERSTORE destination key [key ...] # 并集運算 SUNION key [key ...] # 將并集結果存入新集合destination中 SUNIONSTORE destination key [key ...] # 差集運算 SDIFF key [key ...] # 將差集結果存入新集合destination中 SDIFFSTORE destination key [key ...]
使用場景
點贊
使用 set 集合來存儲,key 為文章 id,value為 用戶 id 即可,獲取該文章的點贊數量取該 set 中 id 的總數即可
關注
使用 set 集合來存儲,key 為用戶 id,value 為該用戶關注的用戶的 id,可以對兩個 set 取并集,就可以找出它們的共同關注
抽獎
使用 set 集合來存儲,key 為活動 id,value 為參見該活動的用戶
- 若不可重復抽獎,則使用
SPOP key [count]
來隨機獲取count
個用戶,并從 set 中移除 - 若可以重復抽獎,則使用
SRANDMEMBER key [count]
來隨機獲取 count 個用戶,但不會從 set 中移除
底層實現
Set 底層是通過 IntSet(整數集合)或者 Dict 來實現的
- 用 Dict 來存儲實際上和 Hash 的結構一樣,只不過把 value 值統(tǒng)一存儲為 null 即可
- 當數據都是整數時,且數據的數量不超過
set-max-intset-entries
時,會使用intset
來存儲數據 - 否則則使用
Dict
來存儲
Intset
Intset 是 Redis 中的一種實現模式,基于整數數組來實現,具備長度可變,有序的特征
結構體
typedef struct intset { uint32_t encoding; // 編碼方式 uint32_t length; // 長度 int8_t contents[]; // 存儲位置 } intset;
encoding
:表示存儲整數的大小
/* Note that these encodings are ordered, so: * INTSET_ENC_INT16 < INTSET_ENC_INT32 < INTSET_ENC_INT64. */ #define INTSET_ENC_INT16 (sizeof(int16_t)) #define INTSET_ENC_INT32 (sizeof(int32_t)) #define INTSET_ENC_INT64 (sizeof(int64_t))
length
:數組的長度
contents
:存儲實際的內容
encoding
:4字節(jié)
length
:4字節(jié)
contents
:2字節(jié) * 3 = 6 字節(jié)
擴容過程
我們向該其中添加一個數字:50000,這個數字超出了 int16_t 的范圍,intset 會自動升級編碼方式到合適的大小。
以當前案例來說流程如下:
- 升級編碼為
INTSET_ENC_INT32
, 每個整數占 4 字節(jié),并按照新的編碼方式及元素個數擴容數組 - 倒序依次將數組中的元素拷貝到擴容后的正確位置
- 將待添加的元素放入數組末尾
- 最后,將 inset 的 encoding 屬性改為
INTSET_ENC_INT32
,將length
屬性改為 4
小結:
- intset 可以保證 contents 的整數有序,唯一
- 因為 contents 數組有序,所以可以使用 二分查找 來快速找到目標值
- 具備類型升級機制,可以節(jié)省內存空間
ZSet
介紹
ZSet 是一個有序集合類型,比 Set 多出了一個 score 字段,該字段用來對 value 值進行排序,保證 ZSet 中的值是有序的
所以每一個元素都有兩個值,一個 score,用來排序,一個 value 存儲實際的值
常用命令
ZSet 常用操作
# 往有序集合key中加入帶分值元素 ZADD key score member [[score member]...] # 往有序集合key中刪除元素 ZREM key member [member...] # 返回有序集合key中元素member的分值 ZSCORE key member # 返回有序集合key中元素個數 ZCARD key # 為有序集合key中元素member的分值加上increment ZINCRBY key increment member # 正序獲取有序集合key從start下標到stop下標的元素 ZRANGE key start stop [WITHSCORES] # 倒序獲取有序集合key從start下標到stop下標的元素 ZREVRANGE key start stop [WITHSCORES] # 返回有序集合中指定分數區(qū)間內的成員,分數由低到高排序。 ZRANGEBYSCORE key min max [WITHSCORES] [LIMIT offset count] # 返回指定成員區(qū)間內的成員,按字典正序排列, 分數必須相同。 ZRANGEBYLEX key min max [LIMIT offset count] # 返回指定成員區(qū)間內的成員,按字典倒序排列, 分數必須相同 ZREVRANGEBYLEX key max min [LIMIT offset count]
ZSet 運算操作
# 并集計算(相同元素分值相加),numberkeys一共多少個key,WEIGHTS每個key對應的分值乘積 ZUNIONSTORE destkey numberkeys key [key...] # 交集計算(相同元素分值相加),numberkeys一共多少個key,WEIGHTS每個key對應的分值乘積 ZINTERSTORE destkey numberkeys key [key...]
使用場景
排行榜
以文章排行榜為例子,使用 article:rank 作為 key,點贊數量作為 score,文章的 id 作為 value 即可,根據 score 從大到小進行排序,則可以得到一個 文章熱度的排行榜。
底層實現
ZSet 使用 壓縮列表 或者 跳表 + 字典來實現
- 可以根據 key 找到 value;
- 保證 value 是唯一的;
- 可以根據 value 查找分數;
Ziplist 本身是沒有排序功能的,而且沒有鍵值對的概念,需要通過編碼來實現:
- ziplist 是連續(xù)內存,score 和 value 是相互依靠的兩個 entry;
- ziplist 本身不能排序,所以只能通過 score 來對 entry 從小到大排序;
當同時滿足一下條件時,會選擇使用 Ziplist
- 元素的數量小于
zset_max_ziplist_entries
,默認是 256 - 每個元素的內存大小小于
zset_max_ziplist_value
字節(jié),默認是 64 KB
否則使用 跳表 +Dict
使用 Dict 存儲 value 和 score 的映射關系;
使用 跳表 來有序的存儲 score 和 value 值,便于范圍查找
SkipList
跳表是一個并聯(lián)多個有序鏈表的數據結構,它可以在 O(logn) 的時間復雜度內完成插入、刪除、增加的操作;
特點
- 跳表是一個雙向鏈表,每個節(jié)點包含 score 和 element 值
- 節(jié)點按 score 進行排序
- 每個節(jié)點都可以包含多層指針,層數是 1 到 32 之間的隨機數
- 不同層指針到下一個節(jié)點的跨度不同,層級越高,跨度越大
- 增刪改查效率與紅黑樹基本一致,實現卻更簡單
跳表代碼
public class SkipList { private final int MAX_LEVEL = 32; private final double factor = 0.25; private int level = 1; /** * 頭結點 */ private final Node head = new Node(null, MAX_LEVEL); static class Node { Integer value; Node[] next; int size; public Node(Integer value, int size) { this.value = value; this.size = size; this.next = new Node[size]; } } public boolean search(int value) { Node p = head; for (int i = level - 1; i >= 0; i--) { while (p.next[i] != null && p.next[i].value <= value) { if (p.next[i].value == value) { return true; } p = p.next[i]; } } return false; } public void insert(int value) { int randomLevel = randomLevel(); Node newNode = new Node(value, randomLevel); Node p = head; for (int i = level - 1; i >= 0; i--) { while (p.next[i] != null && p.next[i].value <= value) { p = p.next[i]; } if (p.next[i] == null) { p.next[i] = newNode; } else { newNode.next[i] = p.next[i]; p.next[i] = newNode; } } if (randomLevel > level) { for (int i = level ; i < randomLevel ; i++) { head.next[i] = newNode; } level = randomLevel; } } public boolean delete(int value) { boolean contain = search(value); Node p = head; Node h = null; if (contain) { for (int i = level - 1; i >= 0; i--) { while (p.next[i] != null && p.next[i].value <= value) { h = p; p = p.next[i]; } h.next[i] = p.next[i]; } } return contain; } private int randomLevel() { int level = 1; Random random = new Random(); while (random.nextDouble() <= factor && level <= MAX_LEVEL) { level++; } return level; } }
以上就是Redis五種數據類型詳解的詳細內容,更多關于Redis數據類型的資料請關注腳本之家其它相關文章!