图解Redis中的9种数据结构(高级面试,必备)
如圖所示,Redis中提供了9種不同的數(shù)據(jù)操作類型,他們分別代表了不同的數(shù)據(jù)存儲結(jié)構(gòu)。
圖2-17 數(shù)據(jù)類型
String類型
String類型是Redis用的較多的一個基本類型,也是最簡單的一種類型,它和我們在Java中使用的字符類型什么太大區(qū)別,具體結(jié)構(gòu)如圖2-18所示。
圖2-19
String常用操作指令
常用炒作指令如圖2-20所示,更多的指令查詢:http://doc.redisfans.com/
圖2-20
String的實際存儲結(jié)構(gòu)
學(xué)過C++的同學(xué)都知道,C++中沒有String類型,而Redis又是基于C++來實現(xiàn)的,那么它是如何存儲String類型的呢?
Redis并沒有采用C語言的傳統(tǒng)字符串表示方式(char*或者char[]),在Redis內(nèi)部,String類型以int/SDS(simple dynamic string)作為結(jié)構(gòu)存儲,int用來存放整型數(shù)據(jù),sds存放字節(jié)/字符串和浮點型數(shù)據(jù)。
在C的標準字符串結(jié)構(gòu)下進行了封裝,用來提升基本操作的性能,同時充分利用以后的C的標準庫,簡化實現(xiàn)。我們可以在redis的源碼中【sds.h】中看到sds的結(jié)構(gòu)如下;
struct __attribute__ ((__packed__)) sdshdr8 {uint8_t len;//表示當(dāng)前sds的長度(單位是字節(jié))uint8_t alloc; //表示已為sds分配的內(nèi)存大小(單位是字節(jié))unsignedchar flags; //用一個字節(jié)表示當(dāng)前sdshdr的類型,因為有sdshdr有五種類型,所以至少需要3位來表示000:sdshdr5,001:sdshdr8,010:sdshdr16,011:sdshdr32,100:sdshdr64。高5位用不到所以都為0。char buf[];//sds實際存放的位置 };也就是說實際上sds類型就是char*類型,那sds和char*有什么區(qū)別呢?
主要區(qū)別就是:sds一定有一個所屬的結(jié)構(gòu)(sdshdr),這個header結(jié)構(gòu)在每次創(chuàng)建sds時被創(chuàng)建,用來存儲sds以及sds的相關(guān)信息
對sds結(jié)構(gòu)有一個簡單認識以后,我們?nèi)绻ㄟ^set創(chuàng)建一個字符串,那么也就是會創(chuàng)建一個sds來存儲這個字符串信息,那么這個過程是怎么樣的呢?
首先第一個要判斷選擇一個什么類型的sdshdr來存放信息?這就得根據(jù)要存儲的sds的長度決定了,redis在創(chuàng)建一個sds之前會調(diào)用【sds.c文件】sdsReqType(size_t string_size)來判斷用哪個sdshdr。該函數(shù)傳遞一個sds的長度作為參數(shù),返回應(yīng)該選用的sdshdr類型。
然后把數(shù)據(jù)保存到對應(yīng)的sdshdr中。
圖2-19
Redis采用類似C的做法存儲字符串,也就是以’\0’結(jié)尾,’\0’只作為字符串的定界符,不計入alloc或者lenkey命名小技巧
a) redis并沒有規(guī)定我們對key應(yīng)該怎么命名,但是最好的實踐是“對象類型:對象id:對象屬性.子屬性”
b) key不要設(shè)置得太長,太長的key不僅僅消耗內(nèi)存,而且在數(shù)據(jù)中查找這類鍵值計算成本很高
c) key不要設(shè)置得太短,比如u:1000:pwd 來代替user:1000:password, 雖然沒什么問題,但是后者的可讀性更好
d) 為了更好的管理你的key,對key進行業(yè)務(wù)上的分類;同時建議有一個wiki統(tǒng)一管理所有的key,通過查詢這個文檔知道redis中的key的作用
String類型的應(yīng)用場景
String類型使用比較多,一般來說,不太了解Redis的人,幾乎所有場景都是用String類型來存儲數(shù)據(jù)。
分布式緩存
首先最基本的就是用來做業(yè)務(wù)數(shù)據(jù)的緩存,如圖2-20,Redis中會緩存一些常用的熱點數(shù)據(jù),可以提升數(shù)據(jù)查詢的性能。
如圖2-20
分布式全局ID
使用String類型的incr命令,實現(xiàn)原子遞增
限流
使用計數(shù)器實現(xiàn)手機驗證碼頻率限流。
分布式session
基于登錄場景中,保存token信息。
List類型
列表類型(list)可以存儲一個有序且可重復(fù)的字符串列表,常用的操作是向列表兩端添加元素或者獲得列表的某一個片段,List的存儲結(jié)構(gòu)如圖2-20所示
圖2-20
常用操作命令
圖2-21表示list類型的常用操作命令,具體命令的操作,可以參考: http://doc.redisfans.com/
圖2-21
數(shù)據(jù)存儲結(jié)構(gòu)
如圖2-22所示,在redis6.0中,List采用了QuickList這樣一種結(jié)構(gòu)來存儲數(shù)據(jù),QuickList是一個雙向鏈表,鏈表的每個節(jié)點保存一個ziplist,所有的數(shù)據(jù)實際上是存儲在ziplist中,ziplist是一個壓縮列表,它可以節(jié)省內(nèi)存空間。
ziplist詳細說明:https://www.cnblogs.com/hunternet/p/11306690.html
聽到“壓縮”兩個字,直觀的反應(yīng)就是節(jié)省內(nèi)存。之所以說這種存儲結(jié)構(gòu)節(jié)省內(nèi)存,是相較于數(shù)組的存儲思路而言的。我們知道,數(shù)組要求每個元素的大小相同,如果我們要存儲不同長度的字符串,那我們就需要用最大長度的字符串大小作為元素的大小(假設(shè)是5個字節(jié))。存儲小于5個字節(jié)長度的字符串的時候,便會浪費部分存儲空間,比如下面這個圖所示。所以,ziplist就是根據(jù)每個節(jié)點的長度來決定占用內(nèi)存大小,然后每個元素保存時同步記錄當(dāng)前數(shù)據(jù)的長度,這樣每次添加元素是就可以計算下一個節(jié)點在內(nèi)存中的存儲位置,從而形成一個壓縮列表。
另外,數(shù)據(jù)的方式存儲數(shù)據(jù)有一個很好的優(yōu)勢,就是它存儲的是在一個連續(xù)的內(nèi)存空間,它可以很好的利用CPU的緩存來訪問數(shù)據(jù),從而提升訪問性能。
圖2-22
其中,QuickList中的每個節(jié)點稱為QuickListNode,具體的定義在quicklist.h文件中。
typedefstructquicklistNode {structquicklistNode *prev;//鏈表的上一個node節(jié)點structquicklistNode *next;//鏈表的下一個node節(jié)點unsignedchar *zl; //數(shù)據(jù)指針,如果當(dāng)前節(jié)點數(shù)據(jù)沒有壓縮,它指向一個ziplist,否則,指向一個quicklistLZFunsignedint sz; /* 指向的ziplist的總大小 */unsignedint count : 16; /* ziplist中的元素個數(shù) */unsignedint encoding : 2; /* 表示ziplist是否壓縮了,1表示沒壓縮,2表示壓縮 */unsignedint container : 2; /* 預(yù)留字段 */unsignedint recompress : 1; /* 當(dāng)使用類似lindex命令查看某一個本壓縮的數(shù)據(jù)時,需要先解壓,這個用來存儲標記,等有機會再把數(shù)據(jù)重新壓縮 */unsignedint attempted_compress : 1; /* node can't compress; too small */unsignedint extra : 10; /* more bits to steal for future usage */ } quicklistNode;quickList是list類型的存儲結(jié)構(gòu),其定義如下。
typedefstructquicklist {quicklistNode *head; //指向quicklistNode頭節(jié)點quicklistNode *tail; //指向quicklistNode的尾節(jié)點unsignedlong count; /* 所有ziplist數(shù)據(jù)項的個數(shù)綜合 */unsignedlong len; /* quicklist節(jié)點個數(shù)*/int fill : QL_FILL_BITS; /* ziplist大小設(shè)置 */unsignedint compress : QL_COMP_BITS; /* 節(jié)點壓縮深度設(shè)置 */unsignedint bookmark_count: QL_BM_BITS;quicklistBookmark bookmarks[]; } quicklist;如圖2-23所示,當(dāng)向list中添加元素時,會直接保存到某個QuickListNode中的ziplist中,不過不管是從頭部插入數(shù)據(jù),還是從尾部插入數(shù)據(jù),都包含兩種情況
如果頭節(jié)點(尾部節(jié)點)上的ziplist大小沒有超過限制,新數(shù)據(jù)會直接插入到ziplist中
如果頭節(jié)點上的ziplist達到閾值,則創(chuàng)建一個新的quicklistNode節(jié)點,該節(jié)點中會創(chuàng)建一個ziplist,然后把這個新創(chuàng)建的節(jié)點插入到quicklist雙向鏈表中。
圖2-23
實際使用場景
消息隊列
列表類型可以使用 rpush 實現(xiàn)先進先出的功能,同時又可以使用 lpop 輕松的彈出(查詢并刪除)第一個元素,所以列表類型可以用來實現(xiàn)消息隊列,如圖2-24所示。
圖2-24
發(fā)紅包的場景
在發(fā)紅包的場景中,假設(shè)發(fā)一個10元,10個紅包,需要保證搶紅包的人不會多搶到,也不會少搶到,這種情況下,可以根據(jù)圖2-25所示去實現(xiàn)。
圖2-25
Hash類型
Hash類型大家應(yīng)該都不陌生,他就是一個鍵值對集合,如圖2-26所示。Hash相當(dāng)于一個 string 類型的 key和 value 的映射表,key 還是key,但是value是一個鍵值對(key-value),類比于 Java里面的 Map<String,Map<String,Object>> 集合。
圖2-26
Hash常用操作命令
Hash結(jié)構(gòu)的常用操作命令如圖2-27所示,其他的指令可以參考:http://doc.redisfans.com/
圖2-27
Hash實際存儲結(jié)構(gòu)
如圖2-28所示,哈希類型的內(nèi)部編碼有兩種:ziplist壓縮列表,hashtable哈希表。只有當(dāng)存儲的數(shù)據(jù)量比較小的情況下,Redis 才使用壓縮列表來實現(xiàn)字典類型。具體需要滿足兩個條件:
當(dāng)哈希類型元素個數(shù)小于hash-max-ziplist-entries配置(默認512個)
所有值都小于hash-max-ziplist-value配置(默認64字節(jié))
ziplist使用更加緊湊的結(jié)構(gòu)實現(xiàn)多個元素的連續(xù)存儲,所以在節(jié)省內(nèi)存方面比hashtable更加優(yōu)秀。當(dāng)哈希類型無法滿足ziplist的條件時,Redis會使用hashtable作為哈希的內(nèi)部實現(xiàn),因為此時ziplist的讀寫效率會下降,而hashtable的讀寫時間復(fù)雜度為O(1)。
圖2-28
Hash實際應(yīng)用場景
Hash表使用用來存儲對象數(shù)據(jù),比如用戶信息,相對于通過將對象轉(zhuǎn)化為json存儲到String類型中,Hash結(jié)構(gòu)的靈活性更大,它可以任何添加和刪除對象中的某些字段。
購物車功能
1.以用戶ID作為key
2.以商品id作為field
3.以商品的數(shù)量作為value
對象類型數(shù)據(jù)
比如優(yōu)化之后的用戶信息存儲,減少數(shù)據(jù)庫的關(guān)聯(lián)查詢導(dǎo)致的性能慢的問題。
用戶信息
商品信息
計數(shù)器
Set類型
如圖2-29所示,集合類型 (Set) 是一個無序并唯一的鍵值集合。它的存儲順序不會按照插入的先后順序進行存儲。
集合類型和列表類型的區(qū)別如下:
列表可以存儲重復(fù)元素,集合只能存儲非重復(fù)元素;
列表是按照元素的先后順序存儲元素的,而集合則是無序方式存儲元素的。
圖2-29
set類型的常用操作
Set類型的常用操作指令如下。
命令 | 說明 | 時間復(fù)雜度 |
SADD key member [member ...] | 添加一個或者多個元素到集合(set)里 | O(N) |
SCARD key | 獲取集合里面的元素數(shù)量 | O(1) |
SDIFF key [key ...] | 獲得隊列不存在的元素 | O(N) |
SDIFFSTORE destination key [key ...]] | 獲得隊列不存在的元素,并存儲在一個關(guān)鍵的結(jié)果集 | O(N) |
SINTER key [key ...] | 獲得兩個集合的交集 | O(N*M) |
SINTERSTORE destination key [key ...] | 獲得兩個集合的交集,并存儲在一個關(guān)鍵的結(jié)果集 | O(N*M) |
SISMEMBER key member | 確定一個給定的值是一個集合的成員 | O(1) |
SMEMBERS key | 獲取集合里面的所有元素 | O(N) |
SMOVE source destination member | 移動集合里面的一個元素到另一個集合 | O(1) |
SPOP key [count] | 刪除并獲取一個集合里面的元素 | O(1) |
SRANDMEMBER key [count] | 從集合里面隨機獲取一個元素 | |
SREM key member [member ...]] | 從集合里刪除一個或多個元素 | O(N) |
SUNION key [key ...]] | 添加多個set元素 | O(N) |
SUNIONSTORE destination key [key ...] | 合并set元素,并將結(jié)果存入新的set里面 | O(N) |
Set類型實際存儲結(jié)構(gòu)
Set在的底層數(shù)據(jù)結(jié)構(gòu)以intset或者hashtable來存儲。當(dāng)set中只包含整數(shù)型的元素時,采用intset來存儲,否則,采用hashtable存儲,但是對于set來說,該hashtable的value值用于為NULL,通過key來存儲元素。
typedefstructintset {uint32_t encoding;uint32_t length;int8_t contents[]; } intset;intset將整數(shù)元素按順序存儲在數(shù)組里,并通過二分法降低查找元素的時間復(fù)雜度。數(shù)據(jù)量大時,
依賴于“查找”的命令(如SISMEMBER)就會由于O(logn)的時間復(fù)雜度而遇到一定的瓶頸,所以數(shù)據(jù)量大時會用dict來代替intset。
但是intset的優(yōu)勢就在于比dict更省內(nèi)存,而且數(shù)據(jù)量小的時候O(logn)未必會慢于O(1)的hash function,這也是intset存在的原因。
圖2-30
set類型的實際應(yīng)用場景
標簽管理功能
給用戶添加標簽。sadd user:1:basketball game coding swing sadd user:2:sing coding sleep basketball ... sadd user:k:tags tag1 tag2 tag4 ...
使用sinter命令,可以來計算用戶共同感興趣的標簽sinter user:1 user:2
這種標簽系統(tǒng)在電商系統(tǒng)、社交系統(tǒng)、視頻網(wǎng)站,圖書網(wǎng)站,旅游網(wǎng)站等都有著廣泛的應(yīng)用。例如一個用戶可能對娛樂、體育比較感興趣,另一個用戶可能對歷史、新聞比較感興趣,
這些興趣點就是標簽。有了這些數(shù)據(jù)就可以得到喜歡同一個標簽的人,以及用戶的共同喜好的標簽,這些數(shù)據(jù)對于用戶體驗以及增強用戶黏度比較重要。
例如一個社交系統(tǒng)可以根據(jù)用戶的標簽進行好友的推薦,已經(jīng)用戶感興趣的新聞的推薦等,一個電子商務(wù)的網(wǎng)站會對不同標簽的用戶做不同類型的推薦,比如對數(shù)碼產(chǎn)品比較感興趣的人,
在各個頁面或者通過郵件的形式給他們推薦最新的數(shù)碼產(chǎn)品,通常會為網(wǎng)站帶來更多的利益
相關(guān)商品信息展示
比如在電商系統(tǒng)中,當(dāng)用戶查看某個商品時,可以推薦和這個商品標簽有關(guān)的商品信息。
ZSet類型
有序集合類型,顧名思義,和前面講的集合類型的區(qū)別就是多了有序的功能。
如圖2-31所示,在集合類型的基礎(chǔ)上,有序集合類型為集合中的每個元素都關(guān)聯(lián)了一個分數(shù)(浮點型),這使得我們不僅可以完成插入、刪除和判斷元素是否存在等集合類型支持的操作,還能獲得分數(shù)最高(或最低)的前N個元素、獲得指定分數(shù)范圍內(nèi)的元素等與分數(shù)有關(guān)的操作。
圖2-31
ZSet常用操作命令
ZSet的常用命令如圖2-32所示,完整的操作命令,詳見:http://doc.redisfans.com/
圖2-32
ZSet的數(shù)據(jù)存儲結(jié)構(gòu)
ZSet的底層數(shù)據(jù)結(jié)構(gòu)采用了zipList(壓縮表)和skiplist(跳躍表)組成,當(dāng)同時滿足以下兩個條件時,有序集合采用的是ziplist存儲。
有序集合保存的元素個數(shù)要小于128個
有序集合保存的所有元素成員的長度必須小于64個字節(jié)
如果不能滿足以上任意一個條件,有序集合會采用skiplist(跳躍表)結(jié)構(gòu)進行存儲,如圖2-33所示,zSet不只是用skiplist,實際上,它使用了dict(字典表)和zskiplist(跳躍表)同時進行數(shù)據(jù)存儲。
dict,字典類型, 其中key表示zset的成員數(shù)據(jù),value表示zset的分值,用來支持O(1)復(fù)雜度的按照成員取分值的操作
zskiplist,跳躍表,按分值排序成員,用來支持平均復(fù)雜度為O(logn)的按照分值定位成員的操作,以及范圍查找操作。
其中zskiplistNode中*obj和Dic中*key指向同一個具體元素,所以不會存在多余的內(nèi)存消耗問題。另外,backward表示后退指針,方便進行回溯。
圖2-33
關(guān)于跳躍表
跳表(skip list) 對標的是平衡樹(AVL Tree),是一種 插入/刪除/搜索 都是 O(log n) 的數(shù)據(jù)結(jié)構(gòu)。它最大的優(yōu)勢是原理簡單、容易實現(xiàn)、方便擴展、效率更高。因此在一些熱門的項目里用來替代平衡樹,如 redis, leveldb 等。
跳表的基本思想
首先,跳表處理的是有序的鏈表(一般是雙向鏈表,下圖未表示雙向),如下:
這個鏈表中,如果要搜索一個數(shù),需要從頭到尾比較每個元素是否匹配,直到找到匹配的數(shù)為止,即時間復(fù)雜度是 O(n)O(n)。同理,插入一個數(shù)并保持鏈表有序,需要先找到合適的插入位置,再執(zhí)行插入,總計也是 O(n)O(n) 的時間。
那么如何提高搜索的速度呢?很簡單,做個索引:
如上圖,我們新創(chuàng)建一個鏈表,它包含的元素為前一個鏈表的偶數(shù)個元素。這樣在搜索一個元素時,我們先在上層鏈表進行搜索,當(dāng)元素未找到時再到下層鏈表中搜索。例如搜索數(shù)字 19 時的路徑如下圖:
先在上層中搜索,到達節(jié)點 17 時發(fā)現(xiàn)下一個節(jié)點為 21,已經(jīng)大于 19,于是轉(zhuǎn)到下一層搜索,找到的目標數(shù)字 19。
我們知道上層的節(jié)點數(shù)目為 n/2n/2,因此,有了這層索引,我們搜索的時間復(fù)雜度降為了:O(n/2)O(n/2)。同理,我們可以不斷地增加層數(shù),來減少搜索的時間:
在上面的 4 層鏈表中搜索 25,在最上層搜索時就可以直接跳過 21 之前的所有節(jié)點,因此十分高效。
更一般地,如果有 kk 層,我們需要的搜索次數(shù)會小于 ?n2k?+k?n2k?+k ,這樣當(dāng)層數(shù) kk 增加到 ?log2n??log2?n? 時,搜索的時間復(fù)雜度就變成了 lognlog?n。其實這背后的原理和二叉搜索樹或二分查找很類似,通過索引來跳過大量的節(jié)點,從而提高搜索效率。
動態(tài)跳表
上節(jié)的結(jié)構(gòu)是“靜態(tài)”的,即我們先擁有了一個鏈表,再在之上建了多層的索引。但是在實際使用中,我們的鏈表是通過多次插入/刪除形成的,換句話說是“動態(tài)”的。上節(jié)的結(jié)構(gòu)要求上層相鄰節(jié)點與對應(yīng)下層節(jié)點間的個數(shù)比是 1:2,隨意插入/刪除一個節(jié)點,這個要求就被被破壞了。
因此跳表(skip list)表示,我們就不強制要求 1:2 了,一個節(jié)點要不要被索引,建幾層的索引,都在節(jié)點插入時由拋硬幣決定。當(dāng)然,雖然索引的節(jié)點、索引的層數(shù)是隨機的,為了保證搜索的效率,要大致保證每層的節(jié)點數(shù)目與上節(jié)的結(jié)構(gòu)相當(dāng)。下面是一個隨機生成的跳表:
可以看到它每層的節(jié)點數(shù)還和上節(jié)的結(jié)構(gòu)差不多,但是上下層的節(jié)點的對應(yīng)關(guān)系已經(jīng)完全被打破了。
現(xiàn)在假設(shè)節(jié)點 17 是最后插入的,在插入之前,我們需要搜索得到插入的位置:
接著,拋硬幣決定要建立幾層的索引,偽代碼如下:
randomLevel()lvl := 1-- random() that returns a random value in [0...1)whilerandom() < p and lvl < MaxLevel dolvl := lvl + 1return lvl上面的偽代碼相當(dāng)于拋硬幣,如果是正面(random() < p)則層數(shù)加一,直到拋出反面為止。其中的 MaxLevel 是防止如果運氣太好,層數(shù)就會太高,而太高的層數(shù)往往并不會提供額外的性能,
一般 MaxLevel=log1/pnMaxLevel=log1/p?n。現(xiàn)在假設(shè) randomLevel 返回的結(jié)果是 2,那么就得到下面的結(jié)果。
如果要刪除節(jié)點,則把節(jié)點和對應(yīng)的所有索引節(jié)點全部刪除即可。當(dāng)然,要刪除節(jié)點時需要先搜索得到該節(jié)點,搜索過程中可以把路徑記錄下來,這樣刪除索引層節(jié)點的時候就不需要多次搜索了
ZSet的使用場景
排行榜系統(tǒng)有序集合比較典型的使用場景就是排行榜系統(tǒng)。例如學(xué)生成績的排名。某視頻(博客等)網(wǎng)站的用戶點贊、播放排名、電商系統(tǒng)中商品的銷量排名等。我們以博客點贊為例。添加用戶贊數(shù)例如小編Tom發(fā)表了一篇博文,并且獲得了10個贊。zadd user:ranking article1 10 取消用戶贊數(shù)這個時候有一個讀者又覺得Tom寫的不好,又取消了贊,此時需要將文章的贊數(shù)從榜單中減去1,可以使用zincrby。zincrby user:ranking -1 article1 查看某篇文章的贊數(shù)ZSCORE user:ranking arcticle1 展示獲取贊數(shù)最多的十篇文章此功能使用zrevrange命令實現(xiàn):zrevrange user:ranking 0 10 #0 到 10表示元素個數(shù)索引 zrevrangebyscore user:ranking 99 0 # 按照分數(shù)從高到低排名,99,0表示score
熱點話題排名比如想微博的熱搜,就可以使用ZSet來實現(xiàn)。
其他數(shù)據(jù)類型介紹
在Redis中,還有一些使用得非常少的數(shù)據(jù)類型,簡單給大家普及一下。
Geospatial
Geo是Redis3.2推出的一個類型,它提供了地理位置的計算功能,也就是可以計算出兩個地理位置的距離。
文檔: https://www.redis.net.cn/order/3687.html下面演示一下Geo的基本使用,其中需要用到經(jīng)緯度信息,可以從 http://www.jsons.cn/lngcode/查詢。
添加模擬數(shù)據(jù)geoadd china:city 116.40 39.90 beijing geoadd china:city 121.47 31.23 shanghai geoadd china:city 114.05 22.52 shengzhen geoadd china:city 113.28 23.12 guangzhou
獲取當(dāng)前位置的坐標值geopos china:city beijing geopos china:city shanghai
獲取兩個位置之間的距離:m-表示米/km-表示千米/mi-表示英里/ft表示英尺# 查看北京到上海的直線距離 geodist china:city beijing shanghai km # 查看北京到深圳的直線距離 geodist china:city beijing shenzhen km
給定一個經(jīng)緯度,找出該經(jīng)緯度某一半徑內(nèi)的元素# 以110 30這個點為中心,尋找方圓1000km的城市 georadius china:city 110 30 1000 km
找出指定位置周圍的其他元素georadiusbymember china:city shanghai 1000 km
比如現(xiàn)在比較火的直播業(yè)務(wù),我們需要檢索附近的主播,那么GEO就可以很好的實現(xiàn)這個功能。
一是主播開播的時候?qū)懭胫鞑d的經(jīng)緯度,
二是主播關(guān)播的時候刪除主播Id元素,這樣就維護了一個具有位置信息的在線主播集合提供給線上檢索。
HyperLogLog
HyperLogLog是Redis2.8.9提供的一種數(shù)據(jù)結(jié)構(gòu),他提供了一種基數(shù)統(tǒng)計方法。什么是基數(shù)統(tǒng)計呢?簡單來說就是一個集合中不重復(fù)元素的個數(shù),比如有一個集合{1,2,3,1,2},那么它的基數(shù)就是3。
HyperLogLog提供了三種指令。
pfadd ,Redis Pfadd 命令將所有元素參數(shù)添加到 HyperLogLog 數(shù)據(jù)結(jié)構(gòu)中。
pfcount,Redis Pfcount 命令返回給定 HyperLogLog 的基數(shù)估算值。
pgmerge,Redis Pgmerge 命令將多個 HyperLogLog 合并為一個 HyperLogLog ,合并后的 HyperLogLog 的基數(shù)估算值是通過對所有 給定 HyperLogLog 進行并集計算得出的。
使用方法如下。
pfadd uv a b c a c d e f # 創(chuàng)建一組元素 pfcount uv # 統(tǒng)計基數(shù)有同學(xué)會問了,這個功能,我用String類型、或者Set類型都可以實現(xiàn),為什么要用HyperLogLog呢?
最大的特性就是: HyperLogLog在數(shù)據(jù)量非常大的情況下,占用的存儲空間非常小,每個 HyperLogLog 鍵只需要花費 12 KB 內(nèi)存,就可以計算接近 2^64(2的64次方) 個不同元素的基數(shù),這個是一個非常龐大的數(shù)字,為什么能夠用這么小的空間來存儲這么大的數(shù)據(jù)呢?
不知道大家是否注意到,HyperLogLog并沒有提供數(shù)據(jù)查詢的命令,只提供了數(shù)據(jù)添加和數(shù)據(jù)統(tǒng)計。這是因為HyperLogLog并沒有存儲每個元素的值,它使用的是概率算法,通過存儲元素的hash值的第一個1的位置,來計算元素數(shù)量,這塊在這里就不做過多展開。
應(yīng)用場景:
HyperLogLog更適合做一些統(tǒng)計類的工作,比如統(tǒng)計一個網(wǎng)站的UV。
計算日活、7日活、月活數(shù)據(jù).如果我們通過解析日志,把 ip 信息(或用戶 id)放到集合中,例如:HashSet。如果數(shù)量不多則還好,但是假如每天訪問的用戶有幾百萬。無疑會占用大量的存儲空間。且計算月活時,還需要將一個整月的數(shù)據(jù)放到一個 Set 中,這隨時可能導(dǎo)致我們的程序 OOM。有了 HyperLogLog,這件事就變得很簡單了。因為存儲日活數(shù)據(jù)所需要的內(nèi)存只有 12K,例如。# 使用日來存儲每天的ip地址 pfadd ip_20190301 192.168.8.1 pfadd ip_20190302 xxx pfadd ip_20190303 xxx ... pfadd ip_20190331 xxx 計算某一天的日活,只需要執(zhí)行 PFCOUNT ip_201903XX 就可以了。每個月的第一天,執(zhí)行 PFMERGE 將上一個月的所有數(shù)據(jù)合并成一個 HyperLogLog,例如:ip_201903。再去執(zhí)行 PFCOUNT ip_201903,就得到了 3 月的月活。
Bit
Bit,其實是String類型中提供的一個功能,他可以設(shè)置key對應(yīng)存儲的值指定偏移量上的bit位的值,可能大家理解起來比較抽象,舉個例子
使用string類型保存一個keyset key m
通過getbit命令獲取 key的bit位的值getbit key 0 getbit key 1 getbit key 2 getbit key 3 getbit key 4 getbit key 5 getbit key 6 getbit key 7 getbit key 8 打印上面的所有輸出,會發(fā)現(xiàn)得到一個0 1 1 0 1 1 0 1的二進制數(shù)據(jù),這個二進制拼接得到的結(jié)果。 m的ascII碼對應(yīng)的是109, 109的二進制正好是0 1 1 0 1 1 0 1。所以從這里可以看出來,bit其實就是針對一個String類型的value值的bit位進行操作。
對key進行修改,修改第6位的值變成1, 第7位的值編程0.setbit key 6 1 setbit key 7 0 在此使用 get key命令,會發(fā)現(xiàn)得到的結(jié)果是n。因為n的二進制是1101110,(十進制是110)。把上面的指定位修改之后,自然就得到了這樣的結(jié)果。
bit操作在實際應(yīng)用中,可以怎么使用呢?
比如學(xué)習(xí)打卡功能就可以使用setbit操作,比如記錄一周的打卡記錄。
# 設(shè)置用戶id 1001的打卡記錄setsign:100101# 已打卡setsign:100110# 未打卡setsign:100121setsign:100131setsign:100141 查看某天是否已打卡 getbit sign 3 統(tǒng)計當(dāng)前用戶總的打卡天數(shù) bitcountsign:1001除了這個場景之外,還有很多類似的場景都可以使用,
統(tǒng)計活躍用戶
記錄用戶在線狀態(tài)
bit最大的好處在于,它通過bit位來存儲0/1表示特定含義,我們知道一個int類型是8個字節(jié),占32個bit位,意味著一個int類型的數(shù)字就可以存儲32個有意義的場景,大大壓縮了存儲空間。
階段性總結(jié)
數(shù)據(jù)結(jié)構(gòu)總結(jié)
應(yīng)用場景總結(jié)
實際上,所謂的應(yīng)用場景,其實就是合理的利用Redis本身的數(shù)據(jù)結(jié)構(gòu)的特性來完成相關(guān)業(yè)務(wù)功能,就像mysql,它可以用來做服務(wù)注冊,也可以用來做分布式鎖,但是mysql它本質(zhì)是一個關(guān)系型數(shù)據(jù)庫,只是用到了其他特性而已。
緩存——提升熱點數(shù)據(jù)的訪問速度
共享數(shù)據(jù)——數(shù)據(jù)的存儲和共享的問題
全局ID —— 分布式全局ID的生成方案(分庫分表)
分布式鎖——進程間共享數(shù)據(jù)的原子操作保證
在線用戶統(tǒng)計和計數(shù)
隊列、棧——跨進程的隊列/棧
消息隊列——異步解耦的消息機制
服務(wù)注冊與發(fā)現(xiàn) —— RPC通信機制的服務(wù)協(xié)調(diào)中心(Dubbo支持Redis)
購物車
新浪/Twitter 用戶消息時間線
抽獎邏輯(禮物、轉(zhuǎn)發(fā))
點贊、簽到、打卡
商品標簽
用戶(商品)關(guān)注(推薦)模型
電商產(chǎn)品篩選
資源獲取:大家 點贊、收藏、關(guān)注、評論啦 、 查看 👇🏻 👇🏻 👇🏻 微信公眾號獲取聯(lián)系方式 👇🏻 👇🏻 👇🏻
精彩專欄推薦訂閱:在 下方專欄 👇🏻 👇🏻 👇🏻 👇🏻
每天學(xué)四小時:Java+Spring+JVM+分布式高并發(fā),架構(gòu)師指日可待
總結(jié)
以上是生活随笔為你收集整理的图解Redis中的9种数据结构(高级面试,必备)的全部內(nèi)容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: Mac通过brew安装Nodejs错误:
- 下一篇: 关于bitlocker加密后的格式化