將來一段時間開發的項目或者需求會大量使用到Redis
,趁着這段時間業務並不太繁忙,抽點時間預習和複習Redis
的相關內容。恰好看到博客下面的UV
和PV
統計,想到了最近看書裏面提到的HyperLogLog
數據類型,因而花點時間分析一下它的使用方式和使用場景(暫時不探究HyperLogLog
的實現原理)。Redis
中HyperLogLog
數據類型是Redid 2.8.9
引入的,使用的時候確保Redis
版本>= 2.8.9
。java
基數計數(cardinality counting)
,一般用來統計一個集合中不重複的元素個數。一個很常見的例子就是統計某個文章的UV
(Unique Visitor
,獨立訪客,通常能夠理解爲客戶端IP
)。大數據量背景下,要實現基數計數,多數狀況下不會選擇存儲全量的基數集合的元素,由於能夠計算出存儲的內存成本,假設一個每一個被統計的元素的平均大小爲32bit
,那麼若是統計一億個數據,佔用的內存大小爲:redis
32 * 100000000 / 8 / 1024 / 1024 ≈ 381M
。若是有多個集合,而且容許計算多個集合的合併計數結果,那麼這個操做帶來的複雜度多是毀滅性的。所以,不會使用Bitmap
、Tree
或者HashSet
等數據結構直接存儲計數元素集合的方式進行計數,而是在不追求絕對準確計數結果的前提之下,使用基數計數的機率算法進行計數,目前常見的有機率算法如下三種:算法
Linear Counting(LC)
。LogLog Counting(LLC)
。HyperLogLog Counting(HLL)
。因此,HyperLogLog實際上是一種基數計數機率算法,並非Redis特有的,Redis基於C語言實現了HyperLogLog而且提供了相關命令API入口。shell
Redis
的做者Antirez
爲了記念Philippe Flajolet對組合數學和基數計算算法分析的研究,因此在設計HyperLogLog
命令的時候使用了Philippe Flajolet
姓名的英文首字母PF
做爲前綴。也就是說,Philippe Flajolet
博士是HLL
算法的重大貢獻者,可是他其實並非Redis
中HyperLogLog
數據類型的開發者。遺憾的是Philippe Flajolet
博士於2011年3月22日因病在巴黎辭世。這個是Philippe Flajolet
博士的維基百科照片:數據結構
Redis
提供的HyperLogLog
數據類型的特徵:app
HyperLogLog Counting(HLL)
實現,只作基數計算,不會保存元數據。HyperLogLog
每一個KEY
最多佔用12K
的內存空間,能夠計算接近2^64
個不一樣元素的基數,它的存儲空間採用稀疏矩陣存儲,空間佔用很小,僅僅在計數基數個數慢慢變大,稀疏矩陣佔用空間漸漸超過了閾值時纔會一次性轉變成稠密矩陣,轉變成稠密矩陣以後纔會佔用12K
的內存空間。Standard Error
)爲0.81%
的近似值,當數據量不大的時候,獲得的結果也多是一個準確值。內存佔用小(每一個KEY最高佔用12K)是HyperLogLog
的最大優點,而它存在兩個相對明顯的限制:異步
Redis
提供的HyperLogLog
數據類型一共有三個命令API
:PFADD
、PFCOUNT
和PFMERGE
。post
PFADD
命令參數以下:大數據
PFADD key element [element …]
支持此命令的Redis版本是:>= 2.8.9
時間複雜度:每添加一個元素的複雜度爲O(1)ui
element
添加到鍵爲key
的HyperLogLog
數據結構中。PFADD
命令的執行流程以下:
PFADD
命令的使用方式以下:
127.0.0.1:6379> PFADD food apple fish (integer) 1 127.0.0.1:6379> PFADD food apple (integer) 0 127.0.0.1:6379> PFADD throwable (integer) 1 127.0.0.1:6379> SET name doge OK 127.0.0.1:6379> PFADD name throwable (error) WRONGTYPE Key is not a valid HyperLogLog string value.
雖然HyperLogLog
數據結構本質是一個字符串,可是不能在String
類型的KEY
使用HyperLogLog
的相關命令。
PFCOUNT
命令參數以下:
PFCOUNT key [key …]
支持此命令的Redis版本是:>= 2.8.9
時間複雜度:返回單個HyperLogLog的基數計數值的複雜度爲O(1),平均常數時間比較低。當參數爲多個key的時候,複雜度爲O(N),N爲key的個數。
PFCOUNT
命令使用單個key
的時候,返回儲存在給定鍵的HyperLogLog
數據結構的近似基數,若是鍵不存在, 則返回0
。PFCOUNT
命令使用多個key
的時候,返回儲存在給定的全部HyperLogLog
數據結構的並集的近似基數,也就是會把全部的HyperLogLog
數據結構合併到一個臨時的HyperLogLog
數據結構,而後計算出近似基數。PFCOUNT
命令的使用方式以下:
127.0.0.1:6379> PFADD POST:1 ip-1 ip-2 (integer) 1 127.0.0.1:6379> PFADD POST:2 ip-2 ip-3 ip-4 (integer) 1 127.0.0.1:6379> PFCOUNT POST:1 (integer) 2 127.0.0.1:6379> PFCOUNT POST:1 POST:2 (integer) 4 127.0.0.1:6379> PFCOUNT NOT_EXIST_KEY (integer) 0
PFMERGE
命令參數以下:
PFMERGE destkey sourcekey [sourcekey ...]
支持此命令的Redis版本是:>= 2.8.9
時間複雜度:O(N),其中N爲被合併的HyperLogLog數據結構的數量,此命令的常數時間比較高
HyperLogLog
數據結構合併爲一個新的鍵爲destkey
的HyperLogLog
數據結構,合併後的HyperLogLog
的基數接近於全部輸入HyperLogLog
的可見集合(Observed Set
)的並集的基數。OK
。PFMERGE
命令的使用方式以下
127.0.0.1:6379> PFADD POST:1 ip-1 ip-2 (integer) 1 127.0.0.1:6379> PFADD POST:2 ip-2 ip-3 ip-4 (integer) 1 127.0.0.1:6379> PFMERGE POST:1-2 POST:1 POST:2 OK 127.0.0.1:6379> PFCOUNT POST:1-2 (integer) 4
假設如今有個簡單的場景,就是統計博客文章的UV
,要求UV
的計數不須要準確,也不須要保存客戶端的IP
數據。下面就這個場景,使用HyperLogLog
作一個簡單的方案和編碼實施。
這個流程可能步驟的前後順序可能會有所調整,可是要作的操做是基本不變的。先簡單假設,文章的內容和統計數據都是後臺服務返回的,兩個接口是分開設計。引入Redis
的高級客戶端Lettuce
依賴:
<dependency> <groupId>io.lettuce</groupId> <artifactId>lettuce-core</artifactId> <version>5.2.1.RELEASE</version> </dependency>
編碼以下:
public class UvTest { private static RedisCommands<String, String> COMMANDS; @BeforeClass public static void beforeClass() throws Exception { // 初始化Redis客戶端 RedisURI uri = RedisURI.builder().withHost("localhost").withPort(6379).build(); RedisClient redisClient = RedisClient.create(uri); StatefulRedisConnection<String, String> connect = redisClient.connect(); COMMANDS = connect.sync(); } @Data public static class PostDetail { private Long id; private String content; } private PostDetail selectPostDetail(Long id) { PostDetail detail = new PostDetail(); detail.setContent("content"); detail.setId(id); return detail; } private PostDetail getPostDetail(String clientIp, Long postId) { PostDetail detail = selectPostDetail(postId); String key = "puv:" + postId; COMMANDS.pfadd(key, clientIp); return detail; } private Long getPostUv(Long postId) { String key = "puv:" + postId; return COMMANDS.pfcount(key); } @Test public void testViewPost() throws Exception { Long postId = 1L; getPostDetail("111.111.111.111", postId); getPostDetail("111.111.111.222", postId); getPostDetail("111.111.111.333", postId); getPostDetail("111.111.111.444", postId); System.out.println(String.format("The uv count of post [%d] is %d", postId, getPostUv(postId))); } }
輸出結果:
The uv count of post [1] is 4
能夠適當使用更多數量的不一樣客戶端IP
調用getPostDetail()
,而後統計一下偏差。
若是想要準確統計UV
,則須要注意幾個點:
假設在不考慮內存成本的前提下,咱們依然可使用Redis
作準確和實時的UV
統計,簡單就可使用Set
數據類型,增長UV
只須要使用SADD
命令,統計UV
只須要使用SCARD
命令(時間複雜度爲O(1)
,能夠放心使用)。舉例:
127.0.0.1:6379> SADD puv:1 ip-1 ip-2 (integer) 2 127.0.0.1:6379> SADD puv:1 ip-3 ip-4 (integer) 2 127.0.0.1:6379> SCARD puv:1 (integer) 4
若是這些統計數據僅僅是用戶端展現,那麼能夠採用異步設計:
在體量小的時候,上面的全部應用的功能能夠在同一個服務中完成,消息隊列能夠用線程池的異步方案替代。
這篇文章只是簡單介紹了HyperLogLog
的使用和統計UV
的使用場景。總的來講就是:在(1)原始數據量巨大,(2)內存佔用要求儘量小,(3)容許計數存在必定偏差而且(4)不要求存放元數據的場景下,能夠優先考慮使用HyperLogLog
進行計數。
參考資料:
(本文完 c-3-d e-a-20191117)