在移動(dòng)應(yīng)用的業(yè)務(wù)場(chǎng)景中,我們需要保存這樣的信息:一個(gè) key 關(guān)聯(lián)了一個(gè)數(shù)據(jù)集合,同時(shí)還要對(duì)集合中的數(shù)據(jù)進(jìn)行統(tǒng)計(jì)排序。
常見(jiàn)的場(chǎng)景如下:
- 給一個(gè) userId ,判斷用戶(hù)登陸狀態(tài);
- 兩億用戶(hù)最近 7 天的簽到情況,統(tǒng)計(jì) 7 天內(nèi)連續(xù)簽到的用戶(hù)總數(shù);
- 統(tǒng)計(jì)每天的新增與第二天的留存用戶(hù)數(shù);
- 統(tǒng)計(jì)網(wǎng)站的對(duì)訪(fǎng)客(Unique Visitor,UV)量
- 最新評(píng)論列表
- 根據(jù)播放量音樂(lè)榜單
通常情況下,我們面臨的用戶(hù)數(shù)量以及訪(fǎng)問(wèn)量都是巨大的,比如百萬(wàn)、千萬(wàn)級(jí)別的用戶(hù)數(shù)量,或者千萬(wàn)級(jí)別、甚至億級(jí)別的訪(fǎng)問(wèn)信息。
所以,我們必須要選擇能夠非常高效地統(tǒng)計(jì)大量數(shù)據(jù)(例如億級(jí))的集合類(lèi)型。
如何選擇合適的數(shù)據(jù)集合,我們首先要了解常用的統(tǒng)計(jì)模式,并運(yùn)用合理的數(shù)據(jù)來(lái)解決實(shí)際問(wèn)題。
四種統(tǒng)計(jì)類(lèi)型:
- 二值狀態(tài)統(tǒng)計(jì);
- 聚合統(tǒng)計(jì);
- 排序統(tǒng)計(jì);
- 基數(shù)統(tǒng)計(jì)。
本文將用到 String、Set、Zset、List、hash 以外的拓展數(shù)據(jù)類(lèi)型 Bitmap
、HyperLogLog
來(lái)實(shí)現(xiàn)。
今天我們來(lái)看下剩下的三種統(tǒng)計(jì)類(lèi)型。
文章涉及到的指令可以通過(guò)在線(xiàn) Redis 客戶(hù)端運(yùn)行調(diào)試,地址:https://try.redis.io/,超方便的說(shuō)。
基數(shù)統(tǒng)計(jì)
“?
基數(shù)統(tǒng)計(jì):統(tǒng)計(jì)一個(gè)集合中不重復(fù)元素的個(gè)數(shù),常見(jiàn)于計(jì)算獨(dú)立用戶(hù)數(shù)(UV)。
實(shí)現(xiàn)基數(shù)統(tǒng)計(jì)最直接的方法,就是采用集合(Set)這種數(shù)據(jù)結(jié)構(gòu),當(dāng)一個(gè)元素從未出現(xiàn)過(guò)時(shí),便在集合中增加一個(gè)元素;如果出現(xiàn)過(guò),那么集合仍保持不變。
當(dāng)頁(yè)面訪(fǎng)問(wèn)量巨大,就需要一個(gè)超大的 Set 集合來(lái)統(tǒng)計(jì),將會(huì)浪費(fèi)大量空間。
另外,這樣的數(shù)據(jù)也不需要很精確,到底有沒(méi)有更好的方案呢?
這個(gè)問(wèn)題問(wèn)得好,Redis 提供了 HyperLogLog
數(shù)據(jù)結(jié)構(gòu)就是用來(lái)解決種種場(chǎng)景的統(tǒng)計(jì)問(wèn)題。
HyperLogLog
是一種不精確的去重基數(shù)方案,它的統(tǒng)計(jì)規(guī)則是基于概率實(shí)現(xiàn)的,標(biāo)準(zhǔn)誤差 0.81%,這樣的精度足以滿(mǎn)足 UV 統(tǒng)計(jì)需求了。
關(guān)于 HyperLogLog 的原理過(guò)于復(fù)雜,如果想要了解的請(qǐng)移步:
- https://www.zhihu.com/question/53416615
- https://en.wikipedia.org/wiki/HyperLogLog
網(wǎng)站的 UV
通過(guò) Set 實(shí)現(xiàn)
一個(gè)用戶(hù)一天內(nèi)多次訪(fǎng)問(wèn)一個(gè)網(wǎng)站只能算作一次,所以很容易就想到通過(guò) Redis 的 Set 集合來(lái)實(shí)現(xiàn)。
用戶(hù)編號(hào) 89757 訪(fǎng)問(wèn) 「Redis 為什么這么快 」時(shí),我們將這個(gè)信息放到 Set 中。
SADDRedis為什么這么快:uv89757
當(dāng)用戶(hù)編號(hào) 89757 多次訪(fǎng)問(wèn)「Redis 為什么這么快」頁(yè)面,Set 的去重功能能保證不會(huì)重復(fù)記錄同一個(gè)用戶(hù) ID。
通過(guò) SCARD
命令,統(tǒng)計(jì)「Redis 為什么這么快」頁(yè)面 UV。指令返回一個(gè)集合的元素個(gè)數(shù)(也就是用戶(hù) ID)。
SCARDRedis為什么這么快:uv
通過(guò) Hash 實(shí)現(xiàn)
“?
還可以利用 Hash 類(lèi)型實(shí)現(xiàn),將用戶(hù) ID 作為 Hash 集合的 key,訪(fǎng)問(wèn)頁(yè)面則執(zhí)行 HSET 命令將 value 設(shè)置成 1。
即使用戶(hù)重復(fù)訪(fǎng)問(wèn),重復(fù)執(zhí)行命令,也只會(huì)把這個(gè) userId 的值設(shè)置成 “1"。
最后,利用 HLEN
命令統(tǒng)計(jì) Hash 集合中的元素個(gè)數(shù)就是 UV。
如下:
HSETredis集群:uvuserId:897571
//統(tǒng)計(jì)UV
HLENredis集群
HyperLogLog 王者方案
“?
Set 雖好,如果文章非常火爆達(dá)到千萬(wàn)級(jí)別,一個(gè) Set 就保存了千萬(wàn)個(gè)用戶(hù)的 ID,頁(yè)面多了消耗的內(nèi)存也太大了。同理,Hash數(shù)據(jù)類(lèi)型也是如此。咋辦呢?
利用 Redis 提供的 HyperLogLog
高級(jí)數(shù)據(jù)結(jié)構(gòu)(不要只知道 Redis 的五種基礎(chǔ)數(shù)據(jù)類(lèi)型了)。這是一種用于基數(shù)統(tǒng)計(jì)的數(shù)據(jù)集合類(lèi)型,即使數(shù)據(jù)量很大,計(jì)算基數(shù)需要的空間也是固定的。
每個(gè) HyperLogLog
最多只需要花費(fèi) 12KB 內(nèi)存就可以計(jì)算 2 的 64 次方個(gè)元素的基數(shù)。
Redis 對(duì) HyperLogLog
的存儲(chǔ)進(jìn)行了優(yōu)化,在計(jì)數(shù)比較小的時(shí)候,存儲(chǔ)空間采用系數(shù)矩陣,占用空間很小。
只有在計(jì)數(shù)很大,稀疏矩陣占用的空間超過(guò)了閾值才會(huì)轉(zhuǎn)變成稠密矩陣,占用 12KB 空間。
PFADD
將訪(fǎng)問(wèn)頁(yè)面的每個(gè)用戶(hù) ID 添加到 HyperLogLog
中。
PFADDRedis主從同步原理:uvuserID1userID2useID3
PFCOUNT
利用 PFCOUNT
獲取 「Redis主從同步原理」頁(yè)面的 UV值。
PFCOUNTRedis主從同步原理:uv
PFMERGE 使用場(chǎng)景
HyperLogLog
除了上面的 PFADD
和 PFCOIUNT
外,還提供了 PFMERGE
,將多個(gè) HyperLogLog
合并在一起形成一個(gè)新的 HyperLogLog
值。
語(yǔ)法
PFMERGEdestkeysourcekey[sourcekey...]
使用場(chǎng)景
比如在網(wǎng)站中我們有兩個(gè)內(nèi)容差不多的頁(yè)面,運(yùn)營(yíng)說(shuō)需要這兩個(gè)頁(yè)面的數(shù)據(jù)進(jìn)行合并。
其中頁(yè)面的 UV 訪(fǎng)問(wèn)量也需要合并,那這個(gè)時(shí)候 PFMERGE
就可以派上用場(chǎng)了,也就是同樣的用戶(hù)訪(fǎng)問(wèn)這兩個(gè)頁(yè)面則只算做一次 。
如下所示:Redis、MySQL 兩個(gè) Bitmap 集合分別保存了兩個(gè)頁(yè)面用戶(hù)訪(fǎng)問(wèn)數(shù)據(jù)。
PFADDRedis數(shù)據(jù)user1user2user3
PFADDMySQL數(shù)據(jù)user1user2user4
PFMERGE數(shù)據(jù)庫(kù)Redis數(shù)據(jù)MySQL數(shù)據(jù)
PFCOUNT數(shù)據(jù)庫(kù)//返回值=4
將多個(gè) HyperLogLog 合并(merge)為一個(gè) HyperLogLog , 合并后的 HyperLogLog 的基數(shù)接近于所有輸入 HyperLogLog 的可見(jiàn)集合(observed set)的并集 。
user1、user2 都訪(fǎng)問(wèn)了 Redis 和 MySQL,只算訪(fǎng)問(wèn)了一次。
基于 Spring Boot + MyBatis Plus + Vue & Element 實(shí)現(xiàn)的后臺(tái)管理系統(tǒng) + 用戶(hù)小程序,支持 RBAC 動(dòng)態(tài)權(quán)限、多租戶(hù)、數(shù)據(jù)權(quán)限、工作流、三方登錄、支付、短信、商城等功能
排序統(tǒng)計(jì)
Redis 的 4 個(gè)集合類(lèi)型中(List、Set、Hash、Sorted Set),List 和 Sorted Set 就是有序的。
- List:按照元素插入 List 的順序排序,使用場(chǎng)景通??梢宰鳛?消息隊(duì)列、最新列表、排行榜;
- Sorted Set:根據(jù)元素的 score 權(quán)重排序,我們可以自己決定每個(gè)元素的權(quán)重值。使用場(chǎng)景(排行榜,比如按照播放量、點(diǎn)贊數(shù))。
最新評(píng)論列表
“?
我可以利用 List 插入的順序排序?qū)崿F(xiàn)評(píng)論列表
比如公 眾號(hào)的后臺(tái)回復(fù)列表(不要杠,舉例子),每一公 眾號(hào)對(duì)應(yīng)一個(gè) List,這個(gè) List 保存該公 眾號(hào)的所有的用戶(hù)評(píng)論。
每當(dāng)一個(gè)用戶(hù)評(píng)論,則利用 LPUSH key value [value ...]
插入到 List 隊(duì)頭。
LPUSH碼哥字節(jié)123456
接著再用 LRANGE key star stop
獲取列表指定區(qū)間內(nèi)的元素。
>LRANGE碼哥字節(jié)04
1)"6"
2)"5"
3)"4"
4)"3"
5)"2"
注意,并不是所有最新列表都能用 List 實(shí)現(xiàn),對(duì)于因?yàn)閷?duì)于頻繁更新的列表,list類(lèi)型的分頁(yè)可能導(dǎo)致列表元素重復(fù)或漏掉。
比如當(dāng)前評(píng)論列表 List ={A, B, C, D}
,左邊表示最新的評(píng)論,D 是最早的評(píng)論。
LPUSH碼哥字節(jié)DCBA
展示第一頁(yè)最新 2 個(gè)評(píng)論,獲取到 A、B:
LRANGE碼哥字節(jié)01
1)"A"
2)"B"
按照我們想要的邏輯來(lái)說(shuō),第二頁(yè)可通過(guò) LRANGE 碼哥字節(jié) 2 3
獲取 C,D。
如果在展示第二頁(yè)之前,產(chǎn)生新評(píng)論 E,評(píng)論 E 通過(guò) LPUSH 碼哥字節(jié) E
插入到 List 隊(duì)頭,List = {E, A, B, C, D }。
現(xiàn)在執(zhí)行 LRANGE 碼哥字節(jié) 2 3
獲取第二頁(yè)評(píng)論發(fā)現(xiàn), B 又出現(xiàn)了。
LRANGE碼哥字節(jié)23
1)"B"
2)"C"
出現(xiàn)這種情況的原因在于 List 是利用元素所在的位置排序,一旦有新元素插入,List = {E,A,B,C,D}
。
原先的數(shù)據(jù)在 List 的位置都往后移動(dòng)一位,導(dǎo)致讀取都舊元素。
小結(jié)
只有不需要分頁(yè)(比如每次都只取列表的前 5 個(gè)元素)或者更新頻率低(比如每天凌晨統(tǒng)計(jì)更新一次)的列表才適合用 List 類(lèi)型實(shí)現(xiàn)。
對(duì)于需要分頁(yè)并且會(huì)頻繁更新的列表,需用使用有序集合 Sorted Set 類(lèi)型實(shí)現(xiàn)。
另外,需要通過(guò)時(shí)間范圍查找的最新列表,List 類(lèi)型也實(shí)現(xiàn)不了,需要通過(guò)有序集合 Sorted Set 類(lèi)型實(shí)現(xiàn),如以成交時(shí)間范圍作為條件來(lái)查詢(xún)的訂單列表。
排行榜
“?
對(duì)于最新列表的場(chǎng)景,List 和 Sorted Set 都能實(shí)現(xiàn),為啥還用 List 呢?直接使用 Sorted Set 不是更好,它還能設(shè)置 score 權(quán)重排序更加靈活。
原因是 Sorted Set 類(lèi)型占用的內(nèi)存容量是 List 類(lèi)型的數(shù)倍之多,對(duì)于列表數(shù)量不多的情況,可以用 Sorted Set 類(lèi)型來(lái)實(shí)現(xiàn)。
比如要一周音樂(lè)榜單,我們需要實(shí)時(shí)更新播放量,并且需要分頁(yè)展示。
除此以外,排序是根據(jù)播放量來(lái)決定的,這個(gè)時(shí)候 List 就無(wú)法滿(mǎn)足了。
我們可以將音樂(lè) ID 保存到 Sorted Set 集合中,score
設(shè)置成每首歌的播放量,該音樂(lè)每播放一次則設(shè)置 score = score +1。
ZADD
比如我們將《青花瓷》和《花田錯(cuò)》播放量添加到 musicTop 集合中:
ZADDmusicTop100000000青花瓷8999999花田錯(cuò)
ZINCRBY
《青花瓷》每播放一次就通過(guò) ZINCRBY
指令將 score + 1。
>ZINCRBYmusicTop1青花瓷
100000001
ZRANGEBYSCORE
最后我們需要獲取 musicTop 前十 播放量音樂(lè)榜單,目前最大播放量是 N ,可通過(guò)如下指令獲?。?/p>
ZRANGEBYSCOREmusicTopN-9NWITHSCORES
“?
65哥:可是這個(gè) N 我們?cè)趺传@取呀?
ZREVRANGE
可通過(guò) ZREVRANGE key start stop [WITHSCORES]
指令。
其中元素的排序按 score
值遞減(從大到小)來(lái)排列。
具有相同 score
值的成員按字典序的逆序(reverse lexicographical order)排列。
>ZREVRANGEmusicTop00WITHSCORES
1)"青花瓷"
2)100000000
小結(jié)
即使集合中的元素頻繁更新,Sorted Set 也能通過(guò) ZRANGEBYSCORE
命令準(zhǔn)確地獲取到按序排列的數(shù)據(jù)。
在面對(duì)需要展示最新列表、排行榜等場(chǎng)景時(shí),如果數(shù)據(jù)更新頻繁或者需要分頁(yè)顯示,建議優(yōu)先考慮使用 Sorted Set。
“推薦下自己做的 Spring Cloud 的實(shí)戰(zhàn)項(xiàng)目:
https://github.com/YunaiV/onemall
聚合統(tǒng)計(jì)
指的就是統(tǒng)計(jì)多個(gè)集合元素的聚合結(jié)果,比如說(shuō):
- 統(tǒng)計(jì)多個(gè)元素的共有數(shù)據(jù)(交集);
- 統(tǒng)計(jì)兩個(gè)集合其中的一個(gè)獨(dú)有元素(差集統(tǒng)計(jì));
- 統(tǒng)計(jì)多個(gè)集合的所有元素(并集統(tǒng)計(jì))。
“?
什么樣的場(chǎng)景會(huì)用到交集、差集、并集呢?
Redis 的 Set 類(lèi)型支持集合內(nèi)的增刪改查,底層使用了 Hash 數(shù)據(jù)結(jié)構(gòu),無(wú)論是 add、remove 都是 O(1) 時(shí)間復(fù)雜度。
并且支持多個(gè)集合間的交集、并集、差集操作,利用這些集合操作,解決上邊提到的統(tǒng)計(jì)問(wèn)題。
交集-共同好友
比如 QQ 中的共同好友正是聚合統(tǒng)計(jì)中的交集。我們將賬號(hào)作為 Key,該賬號(hào)的好友作為 Set 集合的 value。
模擬兩個(gè)用戶(hù)的好友集合:
SADDuser:碼哥字節(jié)R大Linux大神PHP之父
SADDuser:大佬Linux大神Python大神C++菜雞
統(tǒng)計(jì)兩個(gè)用戶(hù)的共同好友只需要兩個(gè) Set 集合的交集,如下命令:
SINTERSTOREuser:共同好友user:碼哥字節(jié)user:大佬
命令的執(zhí)行后,「user:碼哥字節(jié)」、「user:大佬」兩個(gè)集合的交集數(shù)據(jù)存儲(chǔ)到 user:共同好友這個(gè)集合中。
差集-每日新增好友數(shù)
比如,統(tǒng)計(jì)某個(gè) App 每日新增注冊(cè)用戶(hù)量,只需要對(duì)近兩天的總注冊(cè)用戶(hù)量集合取差集即可。
比如,2021-06-01 的總注冊(cè)用戶(hù)量存放在 key = user:20210601
set 集合中,2021-06-02 的總用戶(hù)量存放在 key = user:20210602
的集合中。
如下指令,執(zhí)行差集計(jì)算并將結(jié)果存放到 user:new
集合中。
SDIFFSTOREuser:newuser:20210602user:20210601
執(zhí)行完畢,此時(shí)的 user:new 集合將是 2021/06/02 日新增用戶(hù)量。
除此之外,QQ 上有個(gè)可能認(rèn)識(shí)的人功能,也可以使用差集實(shí)現(xiàn),就是把你朋友的好友集合減去你們共同的好友即是可能認(rèn)識(shí)的人。
并集-總共新增好友
還是差集的例子,統(tǒng)計(jì) 2021/06/01 和 2021/06/02 兩天總共新增的用戶(hù)量,只需要對(duì)兩個(gè)集合執(zhí)行并集。
SUNIONSTOREuserid:newuser:20210602user:20210601
此時(shí)新的集合 userid:new 則是兩日新增的好友。
小結(jié)
Set 的差集、并集和交集的計(jì)算復(fù)雜度較高,在數(shù)據(jù)量較大的情況下,如果直接執(zhí)行這些計(jì)算,會(huì)導(dǎo)致 Redis 實(shí)例阻塞。
所以,可以專(zhuān)門(mén)部署一個(gè)集群用于統(tǒng)計(jì),讓它專(zhuān)門(mén)負(fù)責(zé)聚合計(jì)算,或者是把數(shù)據(jù)讀取到客戶(hù)端,在客戶(hù)端來(lái)完成聚合統(tǒng)計(jì),這樣就可以規(guī)避由于阻塞導(dǎo)致其他服務(wù)無(wú)法響應(yīng)。
審核編輯 :李倩
-
數(shù)據(jù)結(jié)構(gòu)
+關(guān)注
關(guān)注
3文章
573瀏覽量
40732 -
數(shù)據(jù)集
+關(guān)注
關(guān)注
4文章
1224瀏覽量
25434 -
Redis
+關(guān)注
關(guān)注
0文章
386瀏覽量
11431
原文標(biāo)題:實(shí)戰(zhàn)!Redis 巧用數(shù)據(jù)類(lèi)型實(shí)現(xiàn)億級(jí)數(shù)據(jù)統(tǒng)計(jì)!
文章出處:【微信號(hào):芋道源碼,微信公眾號(hào):芋道源碼】歡迎添加關(guān)注!文章轉(zhuǎn)載請(qǐng)注明出處。
發(fā)布評(píng)論請(qǐng)先 登錄
【幸狐Omni3576邊緣計(jì)算套件試用體驗(yàn)】Redis最新8.0.2版本源碼安裝及性能測(cè)試
IEC101協(xié)議可以傳輸什么類(lèi)型的數(shù)據(jù)
labview數(shù)據(jù)類(lèi)型與PLC 數(shù)據(jù)類(lèi)型之間的轉(zhuǎn)換(來(lái)自于寫(xiě)入浮點(diǎn)數(shù)到匯川 PLC中的數(shù)據(jù)轉(zhuǎn)換關(guān)鍵的修改)
Redis Cluster之故障轉(zhuǎn)移

請(qǐng)問(wèn)ADS1299使用Test Signals ,獲取到的數(shù)據(jù)類(lèi)型是什么?
Redis緩存與Memcached的比較
西門(mén)子博途新數(shù)據(jù)類(lèi)型之:SINT(8位整數(shù))

AIC23采集到的數(shù)據(jù)是應(yīng)該用什么數(shù)據(jù)類(lèi)型來(lái)接收?int還是unsigned int?
labview數(shù)據(jù)類(lèi)型的取值范圍是多少
常見(jiàn)的遙感數(shù)據(jù)類(lèi)型有哪些
人體紅外傳感器的數(shù)據(jù)類(lèi)型及工作原理
恒訊科技分析:云數(shù)據(jù)庫(kù)rds和redis區(qū)別是什么如何選擇?
技術(shù)干貨驛站 ▏深入理解C語(yǔ)言:基本數(shù)據(jù)類(lèi)型和變量

評(píng)論