Redis數(shù)據(jù)類型超詳細講解分析
1 Redis
Redis
官網(wǎng)英文版:https://redis.io/
Redis
官網(wǎng)中文版:http://redis.cn/
1.1 概述
Redis
是以key-value
存儲的數(shù)據(jù)結構服務器,所有的key(鍵)
是字符串,而value
可以包含:
- 字符串類型(
string
):最基本的數(shù)據(jù)類型,二進制安全的字符串,最大512M
- 列表類型(
list
):按照添加順序保持順序的字符串列表 - 集合類型(
set
):無序的字符串集合,不存在重復的元素 - 有序集合類型(
sorted set
或Zset
):已排序的字符串集合 - 散列類型(
hash
):key-value對的一種集合 - 位操作(
bitmap
):更細化的一種操作,以bit
為單位。 - 基數(shù)統(tǒng)計(
hyperloglog
):基于概率的數(shù)據(jù)結構,2.8.9新增 - 地理位置(
Geo
):地理位置信息儲存起來, 并對這些信息進行操作 3.2新增 - 流(
Stream
) 5.0新增
1.2 查看內部編碼
Redis
查看內部編碼使用OBJECT ENCODING
命令
該命令用來返回數(shù)據(jù)結構的內部編碼
對象所使用的底層數(shù)據(jù)結構 | 編碼常量 | object encoding 命令輸出 |
---|---|---|
整數(shù) | REDIS_ENCODING_INT | “int” |
embstr編碼簡單動態(tài)字符串(SDS) | REDIS_ENCODING_EMBSTR | “embstr” |
簡單動態(tài)字符串 | REDIS_ENCODING_RAW | “raw” |
字典 | REDIS_ENCODING_HT | “hashtable” |
雙端鏈表 | REDIS_ENCODING_LINKEDLIST | “linkedlist” |
壓縮列表 | REDIS_ENCODING_ZIPLiST | “ziplist” |
整數(shù)集合 | REDIS_ENCODING_INTSET | “intset” |
跳躍表和字典 | REDIS_ENCODING_SKIPLIST | “skiplist” |
1.3 String字符串
1.3.1 簡介
String
是redis中最基本的數(shù)據(jù)類型,一個key對應一個value。
redis的key和string類型value限制均為512MB雖然Key的大小上限為512M
,但是一般建議key的大小不要超過1KB
,這樣既可以節(jié)約存儲空間,又有利于Redis
進行檢索
1.3.2 應用常景
String
類型是二進制安全的,意思是 redis
的 string
可以包含任何數(shù)據(jù)。如數(shù)字
,字符串
,jpg圖片
或者序列化
的對象。字符串類型實際上可以是字符串(簡單的字符串、復雜的字符串(xml、json)、數(shù)字(整數(shù)、浮點數(shù))、二進制(圖片、音頻、視頻))
緩存: 經(jīng)典使用場景,把常用信息,字符串,圖片或者視頻等信息放到redis中,redis作為緩存層,mysql做持久化層,降低mysql的讀寫壓力。
1.3.3 String內部編碼
String
內部編碼:
int
:8個字節(jié)的長整型(long,2^63-1)embstr
:小于等于44
個字節(jié)的字符串,embstr
格式的SDS(簡單動態(tài)字符串:Simple Dynamic String)
raw
:SDS大于 44
個字節(jié)的字符串
Redis
為什么要自己寫一個SDS
的數(shù)據(jù)類型,主要是為了解決C語言 char[]
的四個問題:
字符數(shù)組
必須先給目標變量分配足夠的空間,否則可能會溢出- 查詢字符數(shù)組長度,時間復雜度
O(n)
- 長度變化,需要重新分配內存
- 通過從字符串開始到結尾碰到的第一個
\0
來標記字符串的結束,因此不能保存圖片、音頻、視頻、壓縮文件等二進制(bytes
)保存的內容,二進制不安全
Redis SDS
的優(yōu)勢:
- 不用擔心內存溢出問題,如果需要會對
SDS
進行擴容 - 因為定義了
len
屬性,查詢數(shù)組長度時間復雜度O(1)
固定長度 - 空間預分配,惰性空間釋放
- 根據(jù)長度
len
來判斷是否結束,而不是\0
為什么要有embstr
編碼呢?比raw
的優(yōu)勢在哪里?
embstr
編碼將創(chuàng)建字符串對象所需的空間分配的次數(shù)從raw
編碼的兩次降低為一次。
因為emstr
編碼字符串的素有對象保持在一塊連續(xù)的內存里面,所以那個編碼的字符串對象比起raw
編碼的字符串對象能更好的利用緩存。并且釋放embstr
編碼的字符串對象只需要調用一次內存釋放函數(shù),而釋放raw
編碼對象的字符串對象需要調用兩次內存釋放函數(shù)
1.4 Hash散列
1.4.1 簡介
常用命令:hget,hsetnx,hset,hvals,hgetall,hmset,hmget 等Redis
中每個 hash
可以存儲 2^32 - 1
鍵值對(40多億)
1.4.2 應用常景
我們簡單舉個實例來描述下 Hash 的應用場景:
比如我們要存儲一個用戶信息對象數(shù)據(jù),包含以下信息:用戶 ID 為查找的 key,存儲的 value 用戶對象包含姓名,年齡,生日等信息,如果用普通的 key/value 結構來存儲,主要有以下2種存儲方式:
- 第一種方式將用戶 ID 作為查找 key,把其他信息封裝成一個對象以序列化的方式存儲,這種方式的缺點是,增加了序列化/反序列化的開銷,并且在需要修改其中一項信息時,需要把整個對象取回,并且修改操作需要對并發(fā)進行保護,引入CAS等復雜問題。
- 第二種方法是這個用戶信息對象有多少成員就存成多少個 key-value 對兒,用用戶 ID +對應屬性的名稱作為唯一標識來取得對應屬性的值,雖然省去了序列化開銷和并發(fā)問題,但是用戶 ID 為重復存儲,如果存在大量這樣的數(shù)據(jù),內存浪費還是非??捎^的。
那么 Redis
提供的 Hash
很好的解決了這個問題,Redis
的 Hash
實際是內部存儲的 Value
為一個 HashMap
,并提供了直接存取這個 Map
成員的接口
也就是說,Key 仍然是用戶 ID,value 是一個 Map,這個 Map 的 key 是成員的屬性名,value 是屬性值,這樣對數(shù)據(jù)的修改和存取都可以直接通過其內部 Map 的 Key(Redis 里稱內部 Map 的 key 為 field),也就是通過 key(用戶 ID) + field(屬性標簽)就可以操作對應屬性數(shù)據(jù)了,既不需要重復存儲數(shù)據(jù),也不會帶來序列化和并發(fā)修改控制的問題。
很好的解決了問題。這里同時需要注意,Redis
提供了接口(hgetall
)可以直接取到全部的屬性數(shù)據(jù),但是如果內部 Map 的成員很多,那么涉及到遍歷整個內部 Map
的操作,由于 Redis
單線程模型的緣故,這個遍歷操作可能會比較耗時,而另其它客戶端的請求完全不響應,這點需要格外注意。
1.4.3 Hash內部編碼
內部編碼:
ziplist
(壓縮列表):當哈希類型中元素個數(shù)小于hash-max-ziplist-entries
配置(默認512
個),同時所有值都小于hash-max-ziplist-value
配置(默認64 字節(jié)
)時,Redis
會使用ziplist
作為哈希的內部實現(xiàn)。hashtable
(哈希表):當上述條件不滿足時,Redis
則會采用hashtable
作為哈希的內部實現(xiàn)。
在 Redis 7.0
中,壓縮列表
數(shù)據(jù)結構已經(jīng)廢棄了,交由 listpack
數(shù)據(jù)結構來實現(xiàn)了
1.4.4 rehash和漸進式rehash操作
擴容和縮容都會通過rehash
來實現(xiàn),所謂漸進式rehash
是指我們的大字典的擴容是比較消耗時間的,需要重新申請新的數(shù)組,然后將舊字典所有鏈表的元素重新掛接到新的數(shù)組下面,是一個O(n)
的操作。但是因為我們的redis
是單線程的,無法承受這樣的耗時過程,所以采用了漸進式rehash
小步搬遷,雖然慢一點,但是可以搬遷完畢
redis
會在內部擴容時新建一個長度為原始長度2倍的空哈希表,然后原哈希表上的元素重新rehash
到新的哈希表中去,然后我們再使用新的哈希表即可。
那么,這樣還是有個問題要解決呀
要知道
redis
中存儲的數(shù)據(jù)可能是成百萬上千萬的,我們重新rehash
一次未免太耗時了吧,因為redis中操作大部分是單線程的。
這個過程可能會阻斷其他操作很長時間,這是不能忍受的,那要怎么處理呢
1.4.4.1 過程
首先redis
是采用了漸進式rehash
的操作,就是會有一個變量,指向第一個哈希桶,然后redis
每執(zhí)行一個添加key
,刪除key
的類似命令,就順便copy
一個哈希桶中的數(shù)據(jù)到新的哈希表中去,這樣細水長流的操作,是不會影響什么性能,就會所有的數(shù)據(jù)都被重新hash
到新的哈希表中。
那么在這個過程中,當然再有寫的操作,會直接把數(shù)據(jù)放到新的哈希表中,保證舊的肯定有copy完的時候,如果這段時間對數(shù)據(jù)庫的操作比較少,也沒有關系,redis
內部也有定時任務,每隔一段時間也會copy
一次
redis
通過鏈式哈希解決沖突,也就是同一個桶里面的元素使用鏈表
保存。但是當鏈表過長就會導致查找性能變差可能。所以redis
為了追求塊,使用了兩個全局哈希表
。用于rehash
操作,增加現(xiàn)有的哈希桶數(shù)量,減少哈希沖突
開始默認使用hash表1
保存鍵值對數(shù)據(jù),hash表2
此刻沒有分配空間。當數(shù)據(jù)越來越多的觸發(fā)rehash
操作,則執(zhí)行以下操作:
- 給
hash表2
分配更大的空間 - 將
hash表1
的數(shù)據(jù)重新映射拷貝到hash表2
中
將hash表1的數(shù)據(jù)重新映射到hash表2的過程并不是一次性的,這樣會造成redis阻塞,無法提供服務 - 釋放
hash表1
的空間
詳細步驟:
- 為ht[1]分配空間,讓字典同時持有ht[0]和ht[1]兩個hash表
- 在字典中維持一個索引計數(shù)器變量rehashidx,并將它的值設置為0,表示rehash工作正式開始
- 在rehash進行期間,每次對字典執(zhí)行添加,刪除,查找或者更新操作時,程序除了執(zhí)行特定的操作以外,還會順帶將ht[0]哈希表在rehashidx索引上的所有鍵值對rehash到ht[1],當rehash工作完成之后,程序將rehashidx屬性的值增1
- 隨著字典操作的不斷執(zhí)行,最終在某個時間點上,ht[0]的所有鍵值對都會被rehash至ht[1],這時程序將rehashidx屬性的值設為-1,表示rehash操作已完成
- 將ht[0]釋放,然后將ht[1]設置成ht[0],最后為ht[1]分配一個空白哈希表
1.4.4.2 rehash觸發(fā)條件
rehash
觸發(fā)條件:
- 擴容
我們的擴容一般會在Hash
表中的元素個數(shù)等于第一維數(shù)組的長度的時候,就會開始擴容。擴容的大小是原數(shù)組的兩倍。不過在redis
在做bgsave
(RDB持久化操作的過程)時,為了減少內存頁的過多分離(Copy On Write),redis
不會去擴容。
但是如果hash表的元素個數(shù)已經(jīng)到達了第一維數(shù)組長度的5倍的時候,就會強制擴容,不管你是否在持久化。 - 縮容
當我們的hash
表元素逐漸刪除的越來越少的時候。redis
就會對hash
表進行縮容來減少第一維數(shù)組長度的空間占用。縮容的條件是元素個數(shù)低于數(shù)組長度的10%
,并且縮容不考慮是否在做redis持久化
。
不用考慮bgsave
主要原因是因為我們的縮容的內存都是已經(jīng)使用過的,縮容的時候可以直接置空,而且由于申請的內存比較小,同時會釋放掉一些已經(jīng)使用的內存,不會增大系統(tǒng)的壓力。
1.4.5 跟JDK的HashMap的區(qū)別
數(shù)據(jù)結構上,采用了兩個數(shù)組保存數(shù)據(jù),發(fā)生hash沖突時,只采用了鏈地址法
解決hash
沖突,并沒有跟jdk1.8
一樣當鏈表超過8時優(yōu)化成紅黑樹,因此插入元素時跟jdk1.7
的hashmap
一樣采用的是頭插法
。
在發(fā)生擴容時,跟jdk的hashmap一次性、集中式進行擴容不一樣,采取的是漸進式的rehash,每次操作只會操作當前的元素,在當前數(shù)組中移除或者存放到新的數(shù)組中,直到老數(shù)組的元素徹底變成空表。
當負載因子小于0.1
時,會自動進行縮容
。jdk的hashmap出于性能考慮,不提供縮容的操作。redis
使用MurmurHash
來計算哈希表的鍵的hash值,而jdk
的hashmap
使用key.hashcode()的高十六位跟低十六位做與運算獲得鍵的hash值。
1.5 List列表
1.5.1 簡介
Redis
中的List
其實就是鏈表
(Redis
用雙端鏈表
實現(xiàn)List
)
使用List
結構,我們可以輕松地實現(xiàn)最新消息排隊功能(比如新浪微博的TimeLine)。List的另一個應用就是消息隊列,可以利用List的 PUSH 操作,將任務存放在List中,然后工作線程再用 POP 操作將任務取出進行執(zhí)行。
列表(List
)用來存儲多個有序的字符串,每個字符串稱為元素;一個列表可以存儲2^32-1
個元素。Redis
中的列表支持兩端插入和彈出,并可以獲得指定位置(或范圍)的元素,可以充當數(shù)組、隊列、棧等
1.5.2 命令和應用
常用命令:lpush,rpush,lpop,rpop,lrange
等。
應用場景
比如 twitter 的關注列表,粉絲列表等都可以用 Redis 的 list 結構來實現(xiàn),可以利用lrange命令,做基于Redis的分頁功能,性能極佳,用戶體驗好
消息隊列:Redis 的 list 是有序的列表結構,可以實現(xiàn)阻塞隊列,使用左進右出的方式。Lpush 用來生產 從左側插入數(shù)據(jù),Brpop 用來消費,用來從右側 阻塞的消費數(shù)據(jù)。
數(shù)據(jù)的分頁展示: lrange 命令需要兩個索引來獲取數(shù)據(jù),這個就可以用來實現(xiàn)分頁,可以在代碼中計算兩個索引值,然后來 redis 中取數(shù)據(jù)。
可以用來實現(xiàn)粉絲列表以及最新消息排行等功能
使用列表的技巧:
lpush+lpop=Stack
(棧)lpush+rpop=Queue
(隊列)lpush+ltrim=Capped Collection
(有限集合)lpush+brpop=Message Queue
(消息隊列)
1.5.3 List內部編碼
內部編碼:
ziplist
(壓縮列表):當列表中元素個數(shù)小于512
(默認)個,并且列表中每個元素的值都小于64
(默認)個字節(jié)時,Redis
會選擇用ziplist
來作為列表的內部實現(xiàn)以減少內存的使用。當然上述默認值也可以通過相關參數(shù)修改:list-max-ziplist-entried
(元素個數(shù))、list-max-ziplist-value
(元素值)。linkedlist
(鏈表):當列表類型無法滿足ziplist
條件時,Redis
會選擇用linkedlist
作為列表的內部實現(xiàn)。
因為雙向鏈表
占用的內存比壓縮列表
要多, 所以當創(chuàng)建新的列表鍵時, 列表會優(yōu)先考慮使用壓縮列表, 并且在有需要的時候, 才從壓縮列表實現(xiàn)轉換到雙向鏈表實現(xiàn)quicklist
(快速列表)就是linkedlist
和ziplist
的結合。quicklist
中的每個節(jié)點ziplist
都能夠存儲多個數(shù)據(jù)元素。Redis3.2
開始,列表采quicklist
進編碼
1.6 Set集合
1.6.1 簡介
Redis
的 Set
是 String
類型的無序集合。集合成員是唯一的,這就意味著集合中不能出現(xiàn)重復的數(shù)據(jù)。Redis
中集合是通過哈希表實現(xiàn)的,所以添加,刪除,查找的復雜度都是 O(1)
集合中最大的成員數(shù)為 2^32 - 1
(每個集合可存儲40多億個成員)
1.6.2 命令和應用
常用命令:sadd,spop,smembers,sunion,scard,sscan,sismember等。
應用場景:
Redis set
對外提供的功能與 list
類似是一個列表的功能,特殊之處在于 set
是可以自動去重的,當你需要存儲一個列表數(shù)據(jù),又不希望出現(xiàn)重復數(shù)據(jù)時,set
是一個很好的選擇,并且 set
提供了判斷某個成員是否在一個 set
集合內的重要接口,這個也是 list 所不能提供的。
標簽(tag):集合類型比較典型的使用場景,如一個用戶對娛樂、體育比較感興趣,另一個可能對新聞感興趣,這些興趣就是標簽,有了這些數(shù)據(jù)就可以得到同一標簽的人,以及用戶的共同愛好的標簽,這些數(shù)據(jù)對于用戶體驗以及曾強用戶粘度比較重要。
點贊,或點踩,收藏等,可以放到set中實現(xiàn)
1.6.3 Set內部編碼
Set
內部編碼:
intset
(整數(shù)集合):當集合中的元素都是整數(shù),并且集合中的元素個數(shù)小于set-max-intset-entries
參數(shù)時,默認512
,Redis
會選用intset
作為底層內部實現(xiàn)。hashtable
(哈希表):當上述條件不滿足時,Redis
會采用hashtable
作為底層實現(xiàn)。
1.7 ZSet有序集合
1.7.1 簡介
Redis
有序集合和集合一樣也是 string
類型元素的集合,且不允許重復的成員。不同的是每個元素都會關聯(lián)一個 double
類型的分數(shù)
。redis
正是通過分數(shù)來為集合中的成員進行從小到大的排序。
有序集合的成員是唯一的,但分數(shù)(score
)卻可以重復。集合是通過哈希表實現(xiàn)的,所以添加,刪除,查找的復雜度都是 O(1)。
集合中最大的成員數(shù)為 2^32 - 1
(每個集合可存儲40多億個成員)
1.7.2 命令和應用
常用命令:zadd,zrange,zrem,zcard,zscore,zcount,zlexcount等
應用常景:
- 排行榜:有序集合經(jīng)典使用場景
例如小說視頻等網(wǎng)站需要對用戶上傳的小說視頻做排行榜,榜單可以按照用戶關注數(shù),更新時間,字數(shù)等打分,做排行,
如新聞網(wǎng)站對熱點新聞排序,比如根據(jù)點擊量、點贊量等。 - 帶權重的消息隊列:重要的消息
score
大一些,普通消息score
小一些,可以實現(xiàn)優(yōu)先級高的任務先執(zhí)行
1.7.3 ZSet內部編碼
內部編碼:
ziplist
(壓縮列表):當有序集合的元素個數(shù)小于128
個(默認設置),同時每個元素的值都小于64
字節(jié)(默認設置),Redis
會采用ziplist
作為有序集合的內部實現(xiàn)。也可以通過以下參數(shù)設置:zset-max-ziplist-entries
和zset-max-ziplist-value
skiplist
(跳躍表):當上述條件不滿足時,Redis
會采用skiplist
作為內部編碼。
在 Redis 7.0
中,壓縮列表數(shù)據(jù)結構已經(jīng)廢棄了,交由 listpack
數(shù)據(jù)結構來實現(xiàn)了
1.7.4 跳表數(shù)據(jù)結構
鏈表
在查找元素的時候,因為需要逐一查找,所以查詢效率非常低,時間復雜度是O(N)
,于是就出現(xiàn)了跳表。跳表是在鏈表基礎上改進過來的,實現(xiàn)了一種 多層
的有序鏈表,這樣的好處是能快讀定位數(shù)據(jù)。
那跳表長什么樣呢?這里舉個例子,下圖展示了一個層級為 3 的跳表。
圖中頭節(jié)點有 L0~L2
三個頭指針,分別指向了不同層級的節(jié)點,然后每個層級的節(jié)點都通過指針連接起來:
- L0 層級共有 5 個節(jié)點,分別是節(jié)點1、2、3、4、5;
- L1 層級共有 3 個節(jié)點,分別是節(jié)點 2、3、5;
- L2 層級只有 1 個節(jié)點,也就是節(jié)點 3 。
如果我們要在鏈表中查找節(jié)點 4 這個元素,只能從頭開始遍歷鏈表,需要查找 4 次,而使用了跳表后,只需要查找 2 次就能定位到節(jié)點 4,因為可以在頭節(jié)點直接從 L2 層級跳到節(jié)點 3,然后再往前遍歷找到節(jié)點 4。
可以看到,這個查找過程就是在多個層級上跳來跳去,最后定位到元素。當數(shù)據(jù)量很大時,跳表的查找復雜度就是 O(logN)
。
跳表是怎么設置層高的?
跳表在創(chuàng)建節(jié)點時候,會生成范圍為
[0-1]
的一個隨機數(shù),如果這個隨機數(shù)小于0.25
(相當于概率 25%),那么層數(shù)就增加 1 層,然后繼續(xù)生成下一個隨機數(shù),直到隨機數(shù)的結果大于 0.25 結束,最終確定該節(jié)點的層數(shù)。
1.8 Bitmap位圖和布隆過濾器
1.8.1 Bitmap位圖
1.8.1.1 簡介
Bitmap
(也稱為位數(shù)組或者位向量等)是一種實現(xiàn)對位的操作的’數(shù)據(jù)結構’,在數(shù)據(jù)結構加引號主要因為:Bitmap
本身不是一種數(shù)據(jù)結構,底層實際上是字符串,可以借助字符串進行位操作。
Bitmap
單獨提供了一套命令,所以與使用字符串的方法不太相同??梢园?Bitmaps 想象成一個以位為單位的數(shù)組,數(shù)組的每個單元只能存儲 0
和 1
,數(shù)組的下標在 Bitmap
中叫做偏移量 offset
。bitmap
的出現(xiàn)是為了大數(shù)據(jù)量而來的,但是前提是統(tǒng)計的這個大數(shù)據(jù)量每個的狀態(tài)只能有兩種
,因為每一個bit
位只能表示兩種狀態(tài)。
優(yōu)點:
- 極高空間效率:bitmap 是真的節(jié)省數(shù)據(jù)存儲空間。粗略的算一下,一億位的 Bitmap 大概才占 12MB 的內存,相比其他數(shù)據(jù)結構,能極大地節(jié)省存儲空間;
- 快速查詢:位操作通常比其他數(shù)據(jù)結構查詢速度更快。無論是設置位值還是獲取位值,時間復雜度都為 O (1),能夠快速響應查詢請求;
- 易于操作:支持單個位操作、位統(tǒng)計、位邏輯運算等,運算效率高,不需要進行比較和移位;
缺點:
- 由于數(shù)據(jù)結構特點,導致它僅適用于表示兩種狀態(tài),即 0 和 1。對于需要表示更多狀態(tài)的情況,Bitmap 就不適用了;
- 只有當數(shù)據(jù)比較密集時才有優(yōu)勢,如果只設置(20,30,888888888)三個偏移量的位值,則需要創(chuàng)建一個 99999999 長度的
BitMap
,但是實際上只存了3個數(shù)據(jù),這時候就有很大的空間浪費,碰到這種問題的話,可以通過引入另一個Roaring BitMap
來解決;
1.8.1.2 應用常景
假如我們現(xiàn)在有幾億個數(shù)據(jù),數(shù)據(jù)狀態(tài)都是1或者0兩個狀態(tài),比如用戶簽到次數(shù)、或者登錄次數(shù)等。
- 場景一:用戶簽到
很多網(wǎng)站都提供了簽到功能(這里不考慮數(shù)據(jù)落地事宜),并且需要展示最近一個月的簽到情況Redis
為我們提供了bitmap
(位圖)這一數(shù)據(jù)結構,每個用戶每天的登錄記錄只占據(jù)一位,365
天就是365
位,僅僅需要46字節(jié)就可存儲,極大地節(jié)約了存儲空間。 - 場景二:統(tǒng)計活躍用戶
使用時間作為cacheKey,然后用戶ID為offset,如果當日活躍過就設置為1
那么我該如果計算某幾天/月/年的活躍用戶呢(暫且約定,統(tǒng)計時間內只有有一天在線就稱為活躍),有請下一個redis的命令 - 場景三:用戶在線狀態(tài)
前段時間開發(fā)一個項目,對方給我提供了一個查詢當前用戶是否在線的接口。不了解對方是怎么做的,自己考慮了一下,使用bitmap是一個節(jié)約空間效率又高的一種方法,只需要一個key,然后用戶ID為offset,如果在線就設置為1,不在線就設置為0,和上面的場景一樣,5000W用戶只需要6MB的空間
1.8.1.3 底層原理
我們知道 Bitmap
本身不是一種數(shù)據(jù)結構,底層實際上使用字符串
來存儲。只不過操作的粒度變成了位,即bit。
由于 Redis
中字符串的最大長度是 512 MB
字節(jié),所以 BitMap
的偏移量 offset
值也是有上限的,其最大值是:8 * 1024 * 1024 * 512 = 2^32
。由于 C 語言中字符串的末尾都要存儲一位分隔符,所以實際上 BitMap
的偏移量 offset 值上限是:2^32-1
Bitmap
本身是用 String
類型作為底層數(shù)據(jù)結構實現(xiàn)的一種統(tǒng)計二值狀態(tài)的數(shù)據(jù)類型。String
類型是會保存為二進制的字節(jié)數(shù)組,所以,Redis
就把字節(jié)數(shù)組的每個 bit
位利用起來,用來表示一個元素的二值狀態(tài)。可以把 Bitmap
看作是一個 bit
數(shù)組。
1.8.1.4 命令
- SETBIT
SETBIE
用來設置或清除存儲在鍵處的字符串值的偏移位,其返回值是原來位上存儲的值。key 在初始狀態(tài)下所有的位都為 0
基本格式:SETBIT key offset value
- GETBIT
GETBIT
用來獲取存儲在鍵處的字符串值中偏移位置的位值。
基本格式:GETBIT key offset
- BITCOUNT
BITCOUNT
用來統(tǒng)計指定區(qū)間內,值為1的個數(shù)。選擇特定的byte
范圍計數(shù),具體如下
基本格式:BITCOUNT key [start end]
(注意start和end指的是字節(jié),不是位)start
:設置位索引起始位置(包含該位置計數(shù)),第一個位置以 0 開始,start
參數(shù)需和end
參數(shù)同時設置才合法end
:設置位索引結束位置(包含該位置計數(shù)),end
參數(shù)需和start
參數(shù)同時設置才合法
- BITOP
對一個或多個保存二進制位的字符串key
進行位元操作,并將結果保存到destkey
上
語法格式:BITOP operation destkey key [key ...]
語法:operation
可以是AND(與) 、 OR (或)、 NOT(非) 、 XOR(異或)
除了NOT
操作之外,其他操作都可以接受一個或多個 key 作為輸入 - BITPOS
用來計算指定key
對應字符串中,第一位為1
或者0
的offset
位置。除此之外,BITPOS
也有兩個選項start
和end
,跟BITCOUNT
一樣。
語法格式:BITPOS key bit [ start [ end [ BYTE | BIT]]]
BYTE、BIT 這兩個選項從 7.0.0 版本開始才能使用。
1.8.2 布隆過濾器
1.8.2.1 簡介
上邊提到 bitmap
記錄字符元素的狀態(tài)時,需要先借助哈希運算得出偏移量。但引入哈希運算后可能會出現(xiàn)哈希碰撞的情況,導致狀態(tài)誤判。
布隆過濾器對這個問題做了進一步的優(yōu)化,做到了可控誤判率,當我們將一個郵箱地址添加到集合中,多個不同的哈希函數(shù)會將這個郵箱地址映射到 bitmap 中的不同偏移量位置上,且將這些位值置為 1。
要判斷郵箱地址是否在集合中,通過相同的哈希函數(shù)映射到 bitmap 上的多個位置,如果這些位上的值都為 1,則郵箱可能存在集合中;如果有任何一個位置的值為 0,則元素一定不在集合中。這是布隆過濾器的特點。
雖然但是布隆過濾器還是會發(fā)生誤判的情況,但好在我們可以通過調整布隆過濾器的大小和哈希函數(shù)的數(shù)量來控制誤判率。
注意
:需要Redis
服務器版本是4.0或以上,因為Redis 4.0
引入了插件機制,支持布隆過濾器等模塊的加載
配置的話是,打開Redis
的配置文件redis.conf
,這個文件通常位于Redis的安裝目錄下。在配置文件中,添加以下行來加載布隆過濾器模塊:loadmodule /path/to/redisbloom.so
1.8.2.2 操作命令
布隆過濾器的命令也不多,主要用到的如下幾個:
BF.RESERVE
:創(chuàng)建一個新的布隆過濾器,并指定容量 capacity
和誤判率 error_rate
。
BF.RESERVE <key> <error_rate> <capacity> BF.RESERVE myfilter 0.000001 999999
BF.INFO
:獲取布隆過濾器的信息,包括容量、誤判率等。BF.INFO <key>
BF.ADD
和 BF.MADD
:分別是向布隆過濾器中添加元素和批量添加
# 向布隆過濾器中添加元素 BF.ADD myfilter hello BF.MADD <key> <item> [item ...]
BF.EXISTS
和 BF.MEXISTS
:分別是檢查布隆過濾器中某個元素和批量檢查元素是否存在
# 元素是否存在于布隆過濾器中 BF.EXISTS myfilter hello # 元素是否存在于布隆過濾器中 BF.MEXISTS <key> <item> [item ...]
1.8.2.3 優(yōu)缺點
- 優(yōu)點:
- 布隆過濾器的空間占用也是極小,它本身不存儲完整的數(shù)據(jù),和
bitmap
一樣底層也是通過bit
位來表示數(shù)據(jù)是否存在。 - 性能比較穩(wěn)定,無論集合中元素的數(shù)量有多少,插入和查詢操作的時間復雜度都非常低,通常為 O (k),其中 k 是哈希函數(shù)的個數(shù)。也就是說在處理大規(guī)模數(shù)據(jù)時,布隆過濾器的性能不會隨著數(shù)據(jù)量的增加而急劇下降。
- 布隆過濾器的空間占用也是極小,它本身不存儲完整的數(shù)據(jù),和
- 缺點:
- 存在一定的誤識別率:布隆過濾器存在誤判的情況,即當一個元素實際上不在集合中時,有可能被判斷為在集合中。這是因為多個元素可能通過哈希函數(shù)映射到相同的位置,導致誤判。但是,當布隆過濾器判斷一個元素不在集合中時,則是 100% 正確的。
- 刪除元素比較困難:一般情況下,不能直接從布隆過濾器中刪除元素。這是因為一個位置可能被多個元素映射到,如果直接將該位置的值置為 0,可能會影響其他元素的判斷。
1.8.2.4 應用場景
布隆過濾器存在一定的誤判,所以使用它的場景就一定要允許不準確的情況發(fā)生:
- 解決
Redis
緩存穿透問題:秒殺商品詳情通常會被緩存到 Redis 中。如果有大量惡意請求查詢不存在的商品,通過布隆過濾器可以快速判斷這些商品不存在,從而避免了對數(shù)據(jù)庫的查詢,減輕了數(shù)據(jù)庫的壓力。 - 郵箱黑名單過濾:在郵件系統(tǒng)中,可以使用布隆過濾器來過濾垃圾郵件和惡意郵件。將已知的垃圾郵件發(fā)送者的地址或特征存儲在布隆過濾器中,新郵件來時判斷發(fā)送者是否在黑名單中。
- 對爬蟲網(wǎng)址進行過濾:在爬蟲程序中,為了避免重復抓取相同的網(wǎng)址,可以使用布隆過濾器來記錄已經(jīng)抓取過的網(wǎng)址。新網(wǎng)址出現(xiàn)時,先判斷是否已抓取過。
1.9 HyperLogLog基數(shù)統(tǒng)計
1.9.1 簡介
Redis 2.8.9
版本更新了 Hyperloglog
數(shù)據(jù)結構,Redis HyperLogLog
是用來做基數(shù)統(tǒng)計的算法,所謂基數(shù),也就是不重復的元素
- 優(yōu)點:
在輸入元素的數(shù)量或者體積非常大時,計算基數(shù)所需的空間總是固定的、并且是很小的。在Redis
里面,每個HyperLogLog
鍵只需要花費12 KB
內存,就可以計算接近2^64
個不同元素的基數(shù)。 - 缺點:
因為HyperLogLog
只會根據(jù)輸入元素來計算基數(shù),而不會儲存輸入元素本身,所以HyperLogLog
不能像集合那樣,返回輸入的各個元素。
估算的值,可能存在誤差,帶有 0.81% 標準錯誤的近似值
1.9.2 命令和場景
這個數(shù)據(jù)結構的命令有三個:
PFADD
:添加指定元素到HyperLogLog
PFCOUNT
:返回給定HyperLogLog
的基數(shù)估算值PFMERGE
:將多個HyperLogLog
合并為一個HyperLogLog
應用場景:
- 網(wǎng)頁統(tǒng)計UV (瀏覽用戶數(shù)量,同一天同一個ip多次訪問算一次訪問,目的是計數(shù),而不是保存用戶)
傳統(tǒng)的方式是使用set保存用戶的id,可以統(tǒng)計set中元素數(shù)量作為標準判斷。
但如果這種方式保存大量用戶id,會占用大量內存,我們的目的是為了計數(shù),而不是去保存id。 - 注冊 IP 數(shù)、每日訪問 IP 數(shù)、頁面實時UV)、在線用戶數(shù)等
1.9.3 內部編碼和原理
HyperLogLog
算法時一種非常巧妙的近似統(tǒng)計大量去重元素數(shù)量的算法,它內部維護了16384
個桶來記錄各自桶的元素數(shù)量,當一個元素過來,它會散列到其中一個桶。當元素到來時,通過 hash
算法將這個元素分派到其中的一個小集合存儲,同樣的元素總是會散列到同樣的小集合。這樣總的計數(shù)就是所有小集合大小的總和。使用這種方式精確計數(shù)除了可以增加元素外,還可以減少元素
一個HyperLogLog
實際占用的空間大約是 12k
字節(jié)。但是在計數(shù)比較小的時候,大多數(shù)桶的計數(shù)值都是零。如果 12k
字節(jié)里面太多的字節(jié)都是零,那么這個空間是可以適當節(jié)約一下的。Redis
在計數(shù)值比較小的情況下采用了稀疏存儲
,稀疏存儲
的空間占用遠遠小于 12k
字節(jié)。相對于稀疏存儲
的就是密集存儲
,密集存儲
會恒定占用 12k 字節(jié)。
內部編碼HyperLogLog
整體的內部結構就是 HLL
對象頭 加上 16384
個桶的計數(shù)值位圖。它在 Redis
的內部結構表現(xiàn)就是一個字符串位圖。你可以把 HyperLogLog
對象當成普通的字符串來進行處理。
1.10 GEO地理位置
1.10.1 簡介
Redis
的 Geo
在 Redis 3.2
版本就推出了,這個功能可以推算地理位置的信息: 兩地之間的距離, 方圓幾里的人,GEO使用的是國際通用坐標系WGS-84
1.10.2 命令和場景
命令:
help @geo 查看geo分組下所有的命令 help geoadd 用于查看單個具體命令
主要操作方法有:
geoadd
:添加地理位置的坐標
geoadd 語法格式:GEOADD key longitude latitude member [longitude latitude member ...]
geopos
:用于從給定的key
里返回所有指定名稱(member
)的位置(經(jīng)度和緯度),不存在的返回nil
geopos
語法格式:GEOPOS key member [member ...]
geodist
:用于返回兩個給定位置之間的距離
geodist 語法格式:GEODIST key member1 member2 [m|km|ft|mi]
member1 member2 為兩個地理位置
最后一個距離單位參數(shù)說明:m :米,默認單位;km :千米;mi :英里;ft :英尺georadius
:以給定的經(jīng)緯度為中心, 返回鍵包含的位置元素當中, 與中心的距離不超過給定最大距離的所有位置元素
語法格式:GEORADIUS key longitude latitude radius m|km|ft|mi [WITHCOORD] [WITHDIST] [WITHHASH] [COUNT count] [ASC|DESC] [STORE key] [STOREDIST key]
參數(shù)說明:m
:米,默認單位。km
:千米。mi
:英里。ft
:英尺。WITHDIST
: 在返回位置元素的同時, 將位置元素與中心之間的距離也一并返回。WITHCOORD
: 將位置元素的經(jīng)度和緯度也一并返回。WITHHASH
: 以 52 位有符號整數(shù)的形式, 返回位置元素經(jīng)過原始 geohash 編碼的有序集合分值。 這個選項主要用于底層應用或者調試, 實際中的作用并不大。COUNT
: 限定返回的記錄數(shù)ASC
: 查找結果根據(jù)距離從近到遠排序。DESC
: 查找結果根據(jù)從遠到近排序。
georadiusbymember
: 和GEORADIUS
命令一樣, 都可以找出位于指定范圍內的元素, 但是georadiusbymember
的中心點是由給定的位置元素
決定的, 而不是使用經(jīng)度和緯度
來決定中心點
語法格式:GEORADIUSBYMEMBER key member radius m|km|ft|mi [WITHCOORD] [WITHDIST] [WITHHASH] [COUNT count] [ASC|DESC] [STORE key] [STOREDIST key]
參數(shù)說明同GEORADIUS
geohash
:返回一個或多個位置對象的geohash
值。Redis GEO
使用geohash
來保存地理位置的坐標。geohash
用于獲取一個或多個位置元素的geohash
值。geohash
語法格式如下:GEOHASH key member [member ...]
應用場景:
用于存儲地理信息以及對地理信息作操作的場景
- 查看附近的人
- 微信位置共享
- 地圖上直線距離的展示
- 比如檢索附近的主播
1.10.3 內部編碼
需要說明的是,Geo
本身不是一種數(shù)據(jù)結構,它本質上還是借助于Sorted Set(ZSET)
,并且使用GeoHash
技術進行填充。Redis
中將經(jīng)緯度使用52位
的整數(shù)進行編碼,放進zset
中,score
就是GeoHash
的52位整數(shù)值。在使用Redis
進行Geo
查詢時,其內部對應的操作其實就是zset(skiplist)
的操作。
通過zset
的score
進行排序就可以得到坐標附近的其它元素,通過將score
還原成坐標值就可以得到元素的原始坐標。
總之,Redis
中處理這些地理位置坐標點的思想是:二維平面坐標點 --> 一維整數(shù)編碼值 --> zset(score為編碼值) --> zrangebyrank(獲取score相近的元素)、zrangebyscore --> 通過score
(整數(shù)編碼值)反解坐標點 --> 附近點的地理位置坐標
1.11 Stream流
1.11.1 簡介
Redis Stream
是 Redis 5.0
版本新增加的數(shù)據(jù)結構。Redis Stream
主要用于消息隊列(MQ,Message Queue),Redis
本身是有一個 Redis
發(fā)布訂閱 (pub/sub
) 來實現(xiàn)消息隊列的功能,但它有個缺點就是消息無法持久化,如果出現(xiàn)網(wǎng)絡斷開、Redis 宕機等,消息就會被丟棄。
點擊此處了解為什么Redis不適合用作MQ簡單來說發(fā)布訂閱 (pub/sub) 可以分發(fā)消息,但無法記錄歷史消息。
Redis Stream
提供了消息的持久化
和主備復制
功能,可以讓任何客戶端訪問任何時刻的數(shù)據(jù),并且能記住每一個客戶端的訪問位置,還能保證消息不丟失。
用一句話概括Stream
就是Redis
實現(xiàn)的內存版kafka
,支持多播的可持久化的消息隊列,用于實現(xiàn)發(fā)布訂閱功能,借鑒了 kafka
的設計。Redis Stream
的結構有一個消息鏈表,將所有加入的消息都串起來,每個消息都有一個唯一的ID和對應的內容。消息是持久化的,Redis
重啟后,內容還在。
1.11.2 命令
Redis Stream
的結構如下所示,它有一個消息鏈表,將所有加入的消息都串起來,每個消息都有一個唯一的 ID 和對應的內容
每個 Stream
都有唯一的名稱,它就是 Redis
的 key
,在我們首次使用 xadd
指令追加消息時自動創(chuàng)建。
上圖解析:
Consumer Group
:消費組,使用XGROUP CREATE
命令創(chuàng)建,一個消費組有多個消費者(Consumer
)。last_delivered_id
:游標,每個消費組會有個游標last_delivered_id
,任意一個消費者讀取了消息都會使游標last_delivered_id
往前移動。pending_ids
:消費者(Consumer
)的狀態(tài)變量,作用是維護消費者的未確認的 id。pending_ids
記錄了當前已經(jīng)被客戶端讀取的消息,但是還沒有ack
(Acknowledge character
:確認字符)。
消息隊列相關命令:
XADD
:添加消息到末尾
使用XADD
向隊列添加消息,如果指定的隊列不存在,則創(chuàng)建一個隊列
語法格式:XADD key ID field value [field value ...]
key
:隊列名稱,如果不存在就創(chuàng)建ID
:消息 id,我們使用*
表示由 redis 生成,可以自定義,但是要自己保證遞增性。field value
: 記錄
XTRIM
:對流進行修剪,限制長度
語法格式:XTRIM key MAXLEN [~] count
key
:隊列名稱MAXLEN
:長度count
:數(shù)量
XDEL
:刪除消息
語法格式:XDEL key ID [ID ...]
key
:隊列名稱ID
:消息 ID
XLEN
:獲取流包含的元素數(shù)量,即消息長度
語法格式:XLEN key
,key
:隊列名稱XRANGE
:獲取消息列表,會自動過濾已經(jīng)刪除的消息
語法格式:XRANGE key start end [COUNT count]
key
:隊列名start
:開始值,-
表示最小值end
:結束值,+
表示最大值count
:數(shù)量
XREVRANGE
:反向獲取消息列表,ID 從大到小
語法格式:XREVRANGE key end start [COUNT count]
key
:隊列名end
:結束值, + 表示最大值start
:開始值, - 表示最小值count
:數(shù)量
XREAD
:以阻塞或非阻塞方式獲取消息列表
語法格式:XREAD [COUNT count] [BLOCK milliseconds] STREAMS key [key ...] id [id ...]
count
:數(shù)量milliseconds
:可選,阻塞毫秒數(shù),沒有設置就是非阻塞模式key
:隊列名id
:消息 ID
消費者組相關命令:
XGROUP CREATE
:創(chuàng)建消費者組
語法格式:XGROUP [CREATE key groupname id-or-$] [SETID key groupname id-or-$] [DESTROY key groupname] [DELCONSUMER key groupname consumername]
key
:隊列名稱,如果不存在就創(chuàng)建groupname
:組名。$
: 表示從尾部開始消費,只接受新消息,當前 Stream 消息會全部忽略。- 從頭開始消費:
XGROUP CREATE mystream consumer-group-name 0-0
- 從尾部開始消費:
XGROUP CREATE mystream consumer-group-name $
XREADGROUP GROUP
:讀取消費者組中的消息
語法格式:XREADGROUP GROUP group consumer [COUNT count] [BLOCK milliseconds] [NOACK] STREAMS key [key ...] ID [ID ...]
group
:消費組名consumer
:消費者名count
: 讀取數(shù)量milliseconds
: 阻塞毫秒數(shù)key
: 隊列名ID
: 消息 ID
XACK
:將消息標記為"已處理"XGROUP SETID
:為消費者組設置新的最后遞送消息IDXGROUP DELCONSUMER
:刪除消費者XGROUP DESTROY
:刪除消費者組XPENDING
:顯示待處理消息的相關信息XCLAIM
:轉移消息的歸屬權XINFO
:查看流和消費者組的相關信息;XINFO GROUPS
:打印消費者組的信息;XINFO STREAM
:打印流信息
1.11.3 內部編碼
stream
底層的數(shù)據(jù)結構是radix tree
:Radix Tree
(基數(shù)樹) 事實上就是幾乎相同是傳統(tǒng)的二叉樹。僅僅是在尋找方式上,以一個unsigned int類型數(shù)為例,利用這個數(shù)的每個比特位作為樹節(jié)點的推斷。能夠這樣說,比方一個數(shù)10001010101010110101010,那么依照Radix
樹的插入就是在根節(jié)點,假設遇到0,就指向左節(jié)點,假設遇到1就指向右節(jié)點,在插入過程中構造樹節(jié)點,在刪除過程中刪除樹節(jié)點。
如下是一個保存了7個單詞的Radix Tree:
127.0.0.1:6379> xadd mystream * key1 128 "1576480551233-0" 127.0.0.1:6379> object encoding mystream "unknown"
mystream 總共由 3 部分構成:
- 第一部分是
robj
, 每個redis
對象實例都會有一個最基本的結構來存儲它實際的類型, 編碼和對應的結構的位置 - 第二部分是一個
rax
, 用作存儲 stream ID - 第三部分是
listpack
,rax
下的每一個 key 節(jié)點都會把對應的 keys 和 values 的值存在這個 listpack 結構中
總結
到此這篇關于Redis數(shù)據(jù)類型的文章就介紹到這了,更多相關Redis數(shù)據(jù)類型內容請搜索腳本之家以前的文章或繼續(xù)瀏覽下面的相關文章希望大家以后多多支持腳本之家!
相關文章
面試常問:如何保證Redis緩存和數(shù)據(jù)庫的數(shù)據(jù)一致性
在實際開發(fā)過程中,緩存的使用頻率是非常高的,只要使用緩存和數(shù)據(jù)庫存儲,就難免會出現(xiàn)雙寫時數(shù)據(jù)一致性的問題,那我們又該如何解決呢2021-09-09Redis字典實現(xiàn)、Hash鍵沖突及漸進式rehash詳解
這篇文章主要介紹了Redis字典實現(xiàn)、Hash鍵沖突以及漸進式rehash的相關知識,本文給大家介紹的非常詳細,對大家的學習或工作具有一定的參考借鑒價值,需要的朋友可以參考下2021-09-09詳解Redis數(shù)據(jù)類型實現(xiàn)原理
這篇文章主要介紹了Redis數(shù)據(jù)類型實現(xiàn)原理,在工作中或學習中有需要的小伙伴可以參考一下這篇文章2021-08-08