Redis 中对 HyperLogLog 的应用
首先,在 Redis 中,HyperLogLog 是它的一种高级数据结构。提供有包含但不限于下面两条命令:
pfadd key value,将 key 对应的一个 value 存入
pfcount key,统计 key 的 value 有多少个
回想一下,原始APP页面统计用户的问题。如果 key 对应页面名称,value 对应用户id。那么问题就刚刚好对应上了。
Redis 中的 HyperLogLog 原理
前面我们已经认识到,它的实现中,设有 16384 个桶,即:2^14 = 16384,每个桶有 6 位,每个桶可以表达的最大数字是:25+24+…+1 = 63 ,二进制为: 111 111 。
对于命令:pfadd key value
在存入时,value 会被 hash 成 64 位,即 64 bit 的比特字符串,前 14 位用来选择这个 value 的比特串中从右往左第一个 1 出现的下标位置数值要存到那个桶中去,即前 14 位用来分桶。设第一个1出现位置的数值为 index 。当 index=5 时,就是: …10000 [01 0000 0000 0000]
之所以选 14位 来表达桶编号是因为,分了 16384 个桶,而 2^14 = 16384,刚好地,最大的时候可以把桶利用完,不造成浪费。假设一个字符串的前 14 位是:00 0000 0000 0010 (从右往左看) ,其十进制值为 2。那么 index 将会被转化后放到编号为 2 的桶。
index 的转化规则:
首先因为完整的 value 比特字符串是 64 位形式,减去 14 后,剩下 50 位,那么极端情况,出现 1 的位置,是在第 50 位,即位置是 50。此时 index = 50。此时先将 index 转为 2 进制,它是:110010 。
因为16384 个桶中,每个桶是 6 bit 组成的。刚好 110010 就被设置到了第 2 号桶中去了。请注意,50 已经是最坏的情况,且它都被容纳进去了。那么其他的不用想也肯定能被容纳进去。
因为 fpadd 的 key 可以设置多个 value。例如下面的例子:
pfadd lgh golang
pfadd lgh python
pfadd lgh java
根据上面的做法,不同的 value,会被设置到不同桶中去,如果出现了在同一个桶的,即前 14 位值是一样的,但是后面出现 1 的位置不一样。那么比较原来的 index 是否比新 index 大。是,则替换。否,则不变。
最终地,一个 key 所对应的 16384 个桶都设置了很多的 value 了,每个桶有一个k_max。此时调用 pfcount 时,按照前面介绍的估算方式,便可以计算出 key 的设置了多少次 value,也就是统计值。
value 被转为 64 位的比特串,最终被按照上面的做法记录到每个桶中去。64 位转为十进制就是:2^64,HyperLogLog 仅用了:16384 * 6 /8 / 1024 K 存储空间就能统计多达 2^64 个数。
偏差修正
在估算的计算公式中,constant 变量不是一个定值,它会根据实际情况而被分支设置,例如下面的样子。
假设:m为分桶数,p是m的以2为底的对数。
我的具体使用方法:
解决问题,用户搜索相同内容只记录1次。
1、redis实现pfadd 和pfcount 的方法
public Long pfAdd(final int redisDB, String key, String value) {
return execute(key, new ExecuteCallBack() {
@Override
public Long execute(Jedis jedis) {
if (redisDB > 0) {
jedis.select(redisDB);
}
return jedis.pfadd(key, value);
}
});
}
public Long pfCount(final int redisDB, String key) {
return execute(key, new ExecuteCallBack() {
@Override
public Long execute(Jedis jedis) {
if (redisDB > 0) {
jedis.select(redisDB);
}
return jedis.pfcount(key);
}
});
}
2、使用方法
//将value存入redis,相同内容存入只会记录1次
Long pfAdd = jedisClient.pfAdd(ICacheRedisKey.REDIS_DB, key,value);
if (pfAdd < 1) {
存入redis成功
}
//取出存入内容次数,相同内容只会标记一次
Long incrValue = jedisClient.pfCount(ICacheRedisKey.REDIS_DB, key);
if (incrValue == null) {
incrValue = 0L;
}