redis 的數據類型可謂是 Redis 的精華所在,同樣的數據類型,例如字符串存儲不同的值對應的實際存儲結構也是不同,當你存儲的 int 值是實際的存儲結構也是 int,如果是短字符串(小于 44 字節)實際存儲的結構為 embstr,長字符串對應的實際存儲結構是 raw,這樣設計的目的是為了更好的節約內存。
我們本文的面試題是 Redis 有哪些數據類型?
典型回答
Redis 最常用的數據類型有 5 種:String(字符串類型)、Hash(字典類型)、List(列表類型)、Set(集合類型)、ZSet(有序集合類型)。
1.字符串類型
字符串類型(Simple Dynamic Strings 簡稱 SDS),譯為:簡單動態字符串,它是以鍵值對 key-value 的形式進行存儲的,根據 key 來存儲和獲取 value 值,它的使用相對來說比較簡單,但在實際項目中應用非常廣泛。
字符串的使用如下:
127.0.0.1:6379> set k1 v1 # 添加數據
OK
127.0.0.1:6379> get k1 # 查詢數據
"v1"
127.0.0.1:6379> strlen k1 # 查詢字符串的長度
(integer) 5
復制
我們也可以在存儲字符串時設置鍵值的過期時間,如下代碼所示:
127.0.0.1:6379> set k1 v1 ex 1000 # 設置 k1 1000s 后過期(刪除)
OK
復制
我們還可以使用 SDS 來存儲 int 類型的值,并且可以使用 incr 指令和 decr 指令來操作存儲的值 +1 或者 -1,具體實現代碼如下:
127.0.0.1:6379> get k1 # 查詢 k1=3
"3"
127.0.0.1:6379> incr k1 # 執行 +1 操作
(integer) 4
127.0.0.1:6379> get k1 # 查詢 k1=4
"4"
127.0.0.1:6379> decr k1 # 執行 -1 操作
(integer) 3
127.0.0.1:6379> get k1 # 查詢 k1=3
"3"
復制
字符串的常見使用場景:
- 存放用戶(登錄)信息;
- 存放文章詳情和列表信息;
- 存放和累計網頁的統計信息(存儲 int 值)。
……
2.字典類型
字典類型 (Hash) 又被稱為散列類型或者是哈希表類型,它是將一個鍵值 (key) 和一個特殊的“哈希表”關聯起來,這個“哈希表”表包含兩列數據:字段和值。例如我們使用字典類型來存儲一篇文章的詳情信息,存儲結構如下圖所示:
同理我們也可以使用字典類型來存儲用戶信息,并且使用字典類型來存儲此類信息就無需手動序列化和反序列化數據了,所以使用起來更加的方便和高效。
字典類型的使用如下:
127.0.0.1:6379> hset myhash key1 value1 # 添加數據
(integer) 1
127.0.0.1:6379> hget myhash key1 # 查詢數據
"value1"
復制
字典類型的數據結構,如下圖所示:
通常情況下字典類型會使用數組的方式來存儲相關的數據,但發生哈希沖突時才會使用鏈表的結構來存儲數據。
3.列表類型
列表類型 (List) 是一個使用鏈表結構存儲的有序結構,它的元素插入會按照先后順序存儲到鏈表結構中,因此它的元素操作 (插入和刪除) 時間復雜度為 O(1),所以相對來說速度還是比較快的,但它的查詢時間復雜度為 O(n),因此查詢可能會比較慢。
列表類型的使用如下:
127.0.0.1:6379> lpush list 1 2 3 # 添加數據
(integer) 3
127.0.0.1:6379> lpop list # 獲取并刪除列表的第一個元素
1
復制
列表的典型使用場景有以下兩個:
- 消息隊列:列表類型可以使用 rpush 實現先進先出的功能,同時又可以使用 lpop 輕松的彈出(查詢并刪除)第一個元素,所以列表類型可以用來實現消息隊列;
- 文章列表:對于博客站點來說,當用戶和文章都越來越多時,為了加快程序的響應速度,我們可以把用戶自己的文章存入到 List 中,因為 List 是有序的結構,所以這樣又可以完美的實現分頁功能,從而加速了程序的響應速度。
4.集合類型
集合類型 (Set) 是一個無序并唯一的鍵值集合。
集合類型的使用如下:
127.0.0.1:6379> sadd myset v1 v2 v3 # 添加數據
(integer) 3
127.0.0.1:6379> smembers myset # 查詢集合中的所有數據
1) "v1"
2) "v3"
3) "v2"
復制
集合類型的經典使用場景如下:
- 微博關注我的人和我關注的人都適合用集合存儲,可以保證人員不會重復;
- 中獎人信息也適合用集合類型存儲,這樣可以保證一個人不會重復中獎。
集合類型(Set)和列表類型(List)的區別如下:
- 列表可以存儲重復元素,集合只能存儲非重復元素;
- 列表是按照元素的先后順序存儲元素的,而集合則是無序方式存儲元素的。
5.有序集合類型
有序集合類型 (Sorted Set) 相比于集合類型多了一個排序屬性 score(分值),對于有序集合 ZSet 來說,每個存儲元素相當于有兩個值組成的,一個是有序結合的元素值,一個是排序值。有序集合的存儲元素值也是不能重復的,但分值是可以重復的。
當我們把學生的成績存儲在有序集合中時,它的存儲結構如下圖所示:
有序集合類型的使用如下:
127.0.0.1:6379> zadd zset1 3 golang 4 sql 1 redis # 添加數據
(integer) 3
127.0.0.1:6379> zrange zset 0 -1 # 查詢所有數據
1) "redis"
2) "MySQL"
3) "JAVA"
復制
有序集合的經典使用場景如下:
- 學生成績排名;
- 粉絲列表,根據關注的先后時間排序。
考點分析
關于 Redis 數據類型的這個問題,對于大多數人既熟悉又陌生,熟悉的是每天都在使用 Redis 存取數據,陌生的是對于 Redis 的數據類型知之甚少,因為對于普通的開發工作使用字符串類型就可以搞定了。但是善用 Redis 的數據類型可以到達意想不到的效果,不但可以提高程序的運行速度又可以減少業務代碼,可謂一舉兩得。
例如我們經常會把用戶的登錄信息存儲在 Redis 中,但通常的做法是先將用戶登錄實體類轉為 JSON 字符串存儲在 Redis 中,然后讀取時先查詢數據再反序列化為 User 對象,這個過程看似沒什么問題,但我們可以有更優的解決方案來處理此問題,比如我們可以使用 Hash 存儲用戶的信息,這樣就無需序列化的過程了,并且讀取之后無需反序列化,直接使用 Map 來接收就可以了,這樣既提高了程序的運行速度有省去了序列化和反序列化的業務代碼。
與此知識點相關的面試題還有以下幾個:
- 有序列表的實際存儲結構是什么?
- 除了五種基本的數據類型之外,還有什么數據類型?
知識擴展
有序列表的內部實現
有序集合是由 ziplist (壓縮列表) 或 skiplist (跳躍表) 組成的。
ziplist 介紹
當數據比較少時,有序集合使用的是 ziplist 存儲的,如下代碼所示:
127.0.0.1:6379> zadd myzset 1 db 2 redis 3 mysql
(integer) 3
127.0.0.1:6379> object encoding myzset
"ziplist"
復制
從結果可以看出,有序集合把 myset 鍵值對存儲在 ziplist 結構中了。 有序集合使用 ziplist 格式存儲必須滿足以下兩個條件:
- 有序集合保存的元素個數要小于 128 個;
- 有序集合保存的所有元素成員的長度都必須小于 64 字節。
如果不能滿足以上兩個條件中的任意一個,有序集合將會使用 skiplist 結構進行存儲。 接下來我們來測試以下,當有序集合中某個元素長度大于 64 字節時會發生什么情況? 代碼如下:
127.0.0.1:6379> zadd zmaxleng 1.0 redis
(integer) 1
127.0.0.1:6379> object encoding zmaxleng
"ziplist"
127.0.0.1:6379> zadd zmaxleng 2.0 aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa
(integer) 1
127.0.0.1:6379> object encoding zmaxleng
"skiplist"
復制
通過以上代碼可以看出,當有序集合保存的所有元素成員的長度大于 64 字節時,有序集合就會從 ziplist 轉換成為 skiplist。
小貼士:可以通過配置文件中的 zset-max-ziplist-entries(默認 128)和 zset-max-ziplist-value(默認 64)來設置有序集合使用 ziplist 存儲的臨界值。
skiplist 介紹
skiplist 數據編碼底層是使用 zset 結構實現的,而 zset 結構中包含了一個字典和一個跳躍表,源碼如下:
typedef struct zset {
dict *dict;
zskiplist *zsl;
} zset;
復制
跳躍表的結構如下圖所示:
根據以上圖片展示,當我們在跳躍表中查詢值 32 時,執行流程如下:
- 從最上層開始找,1 比 32 小,在當前層移動到下一個節點進行比較;
- 7 比 32 小,當前層移動下一個節點比較,由于下一個節點指向 Null,所以以 7 為目標,移動到下一層繼續向后比較;
- 18 小于 32,繼續向后移動查找,對比 77 大于 32,以 18 為目標,移動到下一層繼續向后比較;
- 對比 32 等于 32,值被順利找到。
從上面的流程可以看出,跳躍表會想從最上層開始找起,依次向后查找,如果本層的節點大于要找的值,或者本層的節點為 Null 時,以上一個節點為目標,往下移一層繼續向后查找并循環此流程,直到找到該節點并返回,如果對比到最后一個元素仍未找到,則返回 Null。
高級數據類型
除了有 5 大基本數據類型外,還有 GEO(地理位置類型)、HyperLogLog(統計類型)、Stream(流類型)。
GEO(地理位置類型)是 Redis 3.2 版本中新增的數據類型,用于存儲和查詢地理位置的,使用它我們可以實現查詢附近的人或查詢附近的商家等功能(這部分的內容會在后面的章節單獨講解)。
Stream(流類型)是 Redis 5.0 版本中新增的數據類型,因為使用 Stream 可以實現消息消費確認的功能,使用“xack key group-key ID”命令,所以此類型的出現給 Redis 更好的實現消息隊列提供了很大的幫助。
HyperLogLog(統計類型)是本文介紹的重點,HyperLogLog (下文簡稱為 HLL) 是 Redis 2.8.9 版本添加的數據結構,它用于高性能的基數 (去重) 統計功能,它的缺點就是存在極低的誤差率。
HLL 具有以下幾個特點:
- 能夠使用極少的內存來統計巨量的數據,它只需要 12K 空間就能統計 2^64 的數據;
- 統計存在一定的誤差,誤差率整體較低,標準誤差為 0.81%;
- 誤差可以被設置輔助計算因子進行降低。
HLL 的命令只有 3 個,但都非常的實用,下面分別來看。
1.添加元素
127.0.0.1:6379> pfadd key "redis"
(integer) 1
127.0.0.1:6379> pfadd key "java" "sql"
(integer) 1
復制
相關語法: pfadd key element [element ...] 此命令支持添加一個或多個元素至 HLL 結構中。
2.統計不重復的元素
127.0.0.1:6379> pfadd key "redis"
(integer) 1
127.0.0.1:6379> pfadd key "sql"
(integer) 1
127.0.0.1:6379> pfadd key "redis"
(integer) 0
127.0.0.1:6379> pfcount key
(integer) 2
復制
從 pfcount 的結果可以看出,在 HLL 結構中鍵值為 key 的元素, 有 2 個不重復的值:redis 和 sql,可以看出結果還是挺準的。 相關語法: pfcount key [key ...]
此命令支持統計一個或多個 HLL 結構。
3.合并一個或多個 HLL 至新結構
新增 k 和 k2 合并至新結構 k3 中,代碼如下:
127.0.0.1:6379> pfadd k "java" "sql"
(integer) 1
127.0.0.1:6379> pfadd k2 "redis" "sql"
(integer) 1
127.0.0.1:6379> pfmerge k3 k k2
OK
127.0.0.1:6379> pfcount k3
(integer) 3
復制
相關語法:pfmerge destkey sourcekey [sourcekey ...] ** pfmerge 使用場景:當我們需要合并兩個或多個同類頁面的訪問數據時,我們可以使用 pfmerge 來操作。
總結
本文我們介紹了 Redis 的 5 大基礎數據類型的概念以及簡單的使用:String(字符串類型)、Hash(字典類型)、List(列表類型)、Set(集合類型)、ZSet(有序集合類型),還深入的介紹了 ZSet 的底層數據存儲結構:ziplist (壓縮列表) 或 skiplist (跳躍表)。除此之外我們還介紹了 Redis 中的提前 3 個高級的數據類型:GEO(地理位置類型)用于實現查詢附近的人、HyperLogLog(統計類型)用于高效的實現數據的去重統計(存在一定的誤差)、Stream(流類型)主要應用于消息隊列的實現。