四两拔千斤-HyperLogLog >=2.8.9
背景: 统计某一个爆款页面的UV(访问人次),可能会有几千万个,如果使用set则需要一个很大的Set集合进行统计,会非常浪费空间。Redis提供了HyperLogLog数据结构来解决这种统计问题.HyperLogLog提供不精确的去重计数方案,虽然不精确,但也不是非常离谱,标准误差是0.81%.
使用方法
> pfadd codehole user1 # 与set集合的sadd用法一样,来一个用户ID,就将用户ID塞进去
(integer) 1
> pfcount codehole # 与set集合的scard的用法一样,直接获取计数值
(integer) 1
redis> PFADD hll1 foo bar zap a
(integer) 1
redis> PFADD hll2 a b c foo
(integer) 1
redis> PFMERGE hll3 hll1 hll2 #将多个 HyperLogLog 合并为一个 HyperLogLog
"OK"
redis> PFCOUNT hll3
(integer) 6
redis>
注意事项
HyperLogLog需要占据12KB的存储空间,所以不适合统计单个用户相关的数据。如果你的用户有上亿个,可以算算,这个空间成本是非常惊人的。但是相比set存储方案,HyperLogLog所使用的的空间也就是九牛之一毛了。
不过你也不必担心,因为redis对HyperLogLog的存储进行了优化,在计数比较小的时候,它的存储空间采用稀疏矩阵存储,空间占用很小,仅仅在计数慢慢变大,稀疏矩阵占用空间渐渐超过了阈(yù)值时,才会一次性转变为稠密矩阵,才会占用12KB的空间。
简单理解HyperLogLog实现原理
package main
import (
"fmt"
"math"
"math/rand"
)
type bitKeeper struct {
maxbits int //低位连续零位的最大长度K
}
type experiment struct {
n int
k int
keepers []*bitKeeper
}
/**
* @Description: 返回试验对象
*/
func newExperiment(n int) *experiment {
keepers := make([]*bitKeeper, 1024)
for i := 0; i < len(keepers); i++ {
keepers[i] = new(bitKeeper)
}
return &experiment{
keepers: keepers,
n: n,
k: 1024,
}
}
func (e *experiment) work() {
for i := 0; i < e.n; i++ {
m := rand.Intn(1 << 32)
keeper := e.keepers[((m&0xfff0000)>>16)%len(e.keepers)]
keeper.random(m)
}
}
/**
* @Description: 调和平均(倒数的平均)。普通的平均算法可能因为个别离群值对平均结果产生较大的影响,调和平均可以有效平滑离群值的影响
*/
func (e *experiment) estimate() float64 {
sumbitsInverse := 0.0
for _, keeper := range e.keepers {
sumbitsInverse += 1.0 / float64(keeper.maxbits)
}
avgBits := float64(len(e.keepers)) / sumbitsInverse
return math.Pow(2.0, avgBits) * float64(e.k)
}
func (k *bitKeeper) random(value int) {
bits := lowZeros(value)
if bits > k.maxbits {
k.maxbits = bits
}
}
/**
* @Description: 获取低位0的个数
*/
func lowZeros(value int) int {
i := 1
for ; i < 32; i++ {
if value>>uint32(i)<<uint32(i) != value {
break
}
}
return i - 1
}
func main() {
for i := 100_000; i <= 1_000_000; i += 100_000 {
exp := newExperiment(i)
exp.work()
est := exp.estimate()
fmt.Printf("%d %.2f %.2f\n", i, est, math.Abs(est-float64(i))/float64(i))
}
}
真实的hyperloglog要比上面的示例代码更加复杂一些,也更加精确一些。上面的这个算法在随机次数很少的情况下会出现除零错误,因为maxbits=0是不可以求倒数的。
pf的内存占用为什么是12KB
我们在上面的算法中使用了1024个通进行独立计数,不过在Redis的HyperLogLog实现中用的是16384个桶,也就是214,每个桶的maxbits需要6个bit来存储,最大可以表示maxbits=63,于是总共占用内存就是(214)*6/8=12KB
参考链接:神奇的HyperLogLog