Redis学习(9)--HyperLogLog

四两拔千斤-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

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值