场景

做服务端的同学,应该都遇到过计数场景,比如我想知道浏览某一个web页面的总人数,总次数;查看某条热门动态的总人数总次数;购买某件商品的总人数总次数;对于总次数我们直接基于计数器累加就能很方便的解决,时间和空间复杂度都不高。而对于总人数,常规思路我们都是基于去重数据结构Set来存储,将所有访问了的用户id就存到到set中,最终获取set集合中的元素个数即为总人数。当平台量不大时可能还好,一旦访问人数过大,这将是一笔不小的存储开销,并且随着人数的不断增多,所需要的存储空间也会不断加大,这就有点头疼了。

redis的HyperLogLog

对于这种场景redis提供了一种数据结构HyperLogLog,最大只需要12KB的存储空间,就能进行去重统计,最大统计数能达到2^64。当然他也有缺点,就是这个统计结果并不绝对精确,存在误差。但是当数量级达到一定程度后,一定的精度损失很多场景其实是可以接受的,比如2021年北京GDP达到4万亿,至于是四万亿零一百万还是四万亿零两百万,对于数据整体的观测来说其实是没有影响的。

HyperLogLog的使用方法

HyperLogLog使用方法很简单,官方提供了两个命令:

## 增加统计值
## eg: pfadd browser_user_ids 140011
pfadd key value## 获取统计结果
## eg: pfcount browser_user_ids
pfcount key

当我们重复多次pfadd同一个值时对统计结果是不会有影响的,从而达到去重统计的效果。

效果测试

我们通过如下代码尝试进行一万项不重复数据统计,重复三十次:

class RedisTest(): TestBase() {init {initRedis()}@Testfun hyperLogLogTest(){val key = "test"(1..30).forEach {coreRedis.del(key)val list = (1..10000).map {UnionIdUtil.unionId()}coreRedis.pfadd(key, *list.toTypedArray())println("count="+ coreRedis.pfcount(key))}}
}

执行结果:

count=9978
count=9978
count=10068
count=10053
count=9976
count=10055
count=10002
count=9983
count=9945
count=10048
count=10025
count=9931
count=9966
count=10132
count=9948
count=9982
count=9959
count=10011
count=10015
count=10084

基本一万的统计差个几十的样子。

HyperLogLog原理分析

如果大家只是使用,到这里就结束了。但本着知其然,知其所以然的精神,下面我们来聊一下这东西的底层原理。这东西得从抛硬币说起。

伯努利实验

我们都知道抛硬币,每次出现正面或者反面的概率都是50%。假设我们一只抛硬币,知道抛出正面才停下来。我们可能一次就抛到了正面,也可能10次或者更多次才抛到正面,所以我们定义为抛了k次才抛到了正面。而从一开始到抛到一次正面我们称之为一次伯努利试验。假设我们进行了n次伯努利试验,就会出现n个k,如下图:

其中这n个k中有一个值最大的k,我们记为kmaxk_{max}kmax​。而上面的nnn和kmaxk_{max}kmax​之间存在一定的数学关系,根据概率论的极大似然估算法可以得到:n=2kmaxn = 2^{k_{max}}n=2kmax​,极大似然估算法本身是基于样本数据进行模型参数推导的过程,具体如何推导需要微积分和概率论这些大学高数知识,都还给老师了所以就不再推导了。不过我们可以通过程序来验证一下,我们尝试进行一百万次伯努利实验:

    /*** 进行伯努利实验* @param n 进行次数* @return 最大一次抛到正面的投掷次数 kmax*/fun bernoulliTest(n: Long): Long{//第几次伯努利实验var nowN = 0L//最大的kvar maxK = 0L//本轮伯努利实验抛硬币次数var flipCoinCnt = 0Lwhile (nowN < n) {val random = Random().nextInt(2)flipCoinCnt++if (random == 1) {//表示一次伯努利实验结束if (maxK < flipCoinCnt) {maxK = flipCoinCnt}flipCoinCnt = 0nowN++}}return maxK}@Testfun testBernoulli(){println(bernoulliTest(1000000L))}

得到了kmax=21k^{max} = 21kmax=21。我们可以去校验一下其实会发现2212^{21}221 = 2097152和1000000之间差的不是一点半点。我们对于上面的脚本重复执行个50次:

19,22,25,20,22,21,20,20,21,20,21,25,25,19,22,20,20,21,21,22,21,20,19,20,23,18,22,22,19,22,19,20,22,22,20,20,26,22,19,22,24,20,20,25,25,21,20,22,24,22

会发现每次得到的kmaxk^{max}kmax也都不一致。这个一方面是因为概率论下得到的结论本身需要趋于无穷大的样本下才能得到正确的结果,比如我们说跑硬币每次正反面的概率都是50%,但是你们连续抛两次正面是很正常的,因为在小样本面前概率论的公式是不正确的;另一方面是基于极大似然估算得到的公式本身也存在精度问题。所以我们一是需要增加样本数量,二是本身需要做精度优化。

公式优化一

一种常见的优化方式如下,对于单次实验精度不足的问题,采用多次实验求平均。另外增加一个常系数对等式关系进行补偿,我们根据这个思路可以得到新的公式:
n=A∗m∗2kmax‾n = A * m * 2^{\overline{k_{max}}}n=A∗m∗2kmax​​

A为增加的常数,m表示进行了m轮,kmax‾\overline{k_{max}}kmax​​表示m轮每一轮的kmaxk_{max}kmax​的平均值。带入上面的数据,我们将50组数据求平均,可以得到m = 50, kmax‾\overline{k_{max}}kmax​​ = 21.34, n = 1000000,可以求出A = 0.00753442108
我们基于这常数A可以对其他数据进行验算:

val a = 0.00753442108
(1..20).forEach {var cnt = 1000000L * (it % 5 + 1)val list = (1..50).map {bernoulliTest(cnt)}val value = a * 50 * Math.pow(2.0, list.sum().toDouble() / list.size)println("伯努利试验次数=${cnt},估计值:${value}")
}

得到以下结果:

伯努利试验次数=2000000,估计值:1433955.2479701438
伯努利试验次数=3000000,估计值:3680750.602382236
伯努利试验次数=4000000,估计值:4723970.64556762
伯努利试验次数=5000000,估计值:3630076.6211529947
伯努利试验次数=1000000,估计值:999999.9999681418
伯努利试验次数=2000000,估计值:2143546.9250042993
伯努利试验次数=3000000,估计值:3073750.362478103
伯努利试验次数=4000000,估计值:4658934.345725395
伯努利试验次数=5000000,估计值:4346939.44996575
伯努利试验次数=1000000,估计值:858565.4364104022
伯努利试验次数=2000000,估计值:1866065.9830141638
伯努利试验次数=3000000,估计值:3434261.7456416087
伯努利试验次数=4000000,估计值:4346939.44996575
伯努利试验次数=5000000,估计值:4469148.552146502
伯努利试验次数=1000000,估计值:812252.3963303582
伯努利试验次数=2000000,估计值:1999999.9999362836
伯努利试验次数=3000000,估计值:2496661.097723685
伯努利试验次数=4000000,估计值:3784230.5867818296
伯努利试验次数=5000000,估计值:5351710.218973959
伯努利试验次数=1000000,估计值:882702.9962625337

其实可以看到实际值和估计值也已经有几分像样了,不过精度上差距还是比较大。

公式优化二

第一个优化是基于平均数来的,而HyperLogLog实际采用的是调和平均,调和平均可以解决一些异常大值对整体的影响,比如我赚1000一个月,我朋友赚2000一个月,马月赚100000000一个月,直接平均的话我们人均月薪33334333,而调和平均是:311000+12000+1100000000\frac{3}{\frac{1}{1000}+ \frac{1}{2000}+ \frac{1}{100000000}}10001​+20001​+1000000001​3​ = 1999.98 。这样能有效解决马云的工资对我们平均工资的巨大影响。根据上面的距离我们可以总结出调和平均的计算公式:
x‾=m∑1x\overline{x} = \frac{m}{\sum{\frac{1}{x}}}x=∑x1​m​
总体的优化公式如下:
n=A∗m∗m∑12kmaxn = A * m * \frac{m}{\sum{\frac{1}{2^{k_{max}}}}}n=A∗m∗∑2kmax​1​m​
可以看到他是对2kmax2^{k_{max}}2kmax​整体做的调和平均,并不是对kmaxk_{max}kmax​做调和平均,也很好理解,单单一个kmaxk_{max}kmax​其实差距不大,但是指数之后来去就大了,所以对指数计算后的结果做调和平均比kmaxk_{max}kmax​有效的多。
我们同样可以根据这个公式,结合上面的50组数据计算出A = 0.014179944992065428,然后我们可以根据这个A再对其他数据进行验算.

伯努利试验次数=2000000,估计值:1644659.522986519
伯努利试验次数=3000000,估计值:2830458.0606781673
伯努利试验次数=4000000,估计值:4492387.58409064
伯努利试验次数=5000000,估计值:5102199.137100489
伯努利试验次数=1000000,估计值:881764.2698295031
伯努利试验次数=2000000,估计值:1813759.0088748583
伯努利试验次数=3000000,估计值:3532293.98663697
伯努利试验次数=4000000,估计值:3645278.68224478
伯努利试验次数=5000000,估计值:5244419.950399558
伯努利试验次数=1000000,估计值:1109608.2089552237
伯努利试验次数=2000000,估计值:1912379.4212218646
伯努利试验次数=3000000,估计值:2622029.344905972
伯努利试验次数=4000000,估计值:5313232.830820769
伯努利试验次数=5000000,估计值:6993523.494556978
伯努利试验次数=1000000,估计值:961212.121212121
伯努利试验次数=2000000,估计值:2212508.7189025804
伯努利试验次数=3000000,估计值:3309913.0434782603
伯努利试验次数=4000000,估计值:3544794.1888619848
伯努利试验次数=5000000,估计值:3903199.3437243635
伯努利试验次数=1000000,估计值:844440.5004880645

看下数据会发现和上面直接平均的效果差不太多,精度也不是很够。所以我们这里把样本数从50增加到一千试试,一千轮伯努利试验组,每组进行一百万次伯努利试验,得到的一千个kmaxk_{max}kmax​

25,24,23,24,22,22,19,25,19,20,21,22,24,21,20,23,20,23,25,20,26,20,22,21,20,19,26,23,22,20,23,19,19,23,22,20,23,20,21,21,22,20,21,20,20,23,22,20,22,23,27,20,22,19,19,23,23,22,20,20,24,20,22,21,23,19,21,25,23,20,20,21,23,21,21,20,24,21,22,20,25,20,22,22,22,21,20,23,20,19,25,22,21,21,26,22,21,20,22,20,20,24,21,21,21,22,22,25,22,20,19,23,18,19,21,20,23,22,20,23,27,21,21,24,23,19,21,24,18,21,20,20,20,23,21,23,20,20,22,21,21,23,20,23,23,19,22,22,20,27,24,22,20,20,19,21,22,23,21,20,19,22,20,20,19,23,24,19,21,19,23,20,22,19,22,20,19,20,21,21,24,19,22,21,20,23,21,20,22,21,21,23,21,22,22,20,27,20,23,21,23,22,19,22,24,23,21,21,19,20,19,27,21,21,22,20,22,20,18,22,21,21,19,21,22,19,22,19,23,24,23,21,19,21,20,20,21,21,21,21,20,19,21,20,22,21,20,24,21,21,24,19,22,26,22,18,20,19,23,23,21,28,23,22,24,21,22,21,25,22,22,21,19,22,23,19,25,19,25,21,20,21,20,23,25,22,19,22,21,22,24,20,19,20,19,21,23,20,21,20,19,22,19,25,21,21,19,20,19,20,20,20,22,21,20,20,20,20,21,24,27,20,21,19,20,18,19,21,21,22,22,21,21,19,23,21,20,21,22,23,23,20,21,20,20,19,18,21,26,19,19,20,18,22,22,20,23,23,19,20,22,21,19,23,20,20,20,19,19,24,23,20,23,21,20,25,21,23,24,21,21,22,19,20,19,21,19,26,20,20,21,22,19,23,19,21,20,19,19,20,20,20,18,22,20,22,21,22,24,27,20,20,20,20,21,21,21,21,21,22,20,20,21,21,24,20,19,23,18,21,21,20,29,19,18,19,21,24,21,20,24,19,21,23,21,22,22,21,20,19,19,22,24,24,20,21,21,21,20,22,19,21,20,20,19,21,21,23,18,19,18,25,23,19,20,20,20,23,22,19,20,19,25,24,22,24,22,20,23,21,21,22,20,21,20,20,19,23,22,20,20,21,24,23,21,21,20,20,20,21,24,21,20,22,21,21,21,20,20,21,19,23,19,26,22,22,23,21,23,20,21,21,20,20,20,23,21,21,20,19,21,21,20,22,21,19,21,23,21,19,27,20,25,21,19,21,20,21,22,24,24,20,22,21,22,21,22,21,21,20,21,19,25,19,22,21,21,22,20,21,23,20,20,21,22,22,27,22,20,21,25,20,22,27,20,22,22,25,20,21,23,23,19,23,26,21,20,20,20,20,21,20,22,22,22,19,19,22,21,21,20,19,21,19,22,20,18,23,25,23,23,21,20,22,23,21,19,21,23,25,22,20,22,20,20,21,24,21,21,29,22,19,24,24,23,21,22,20,21,21,23,21,19,19,23,20,23,20,26,20,22,22,21,19,21,19,21,23,19,20,21,28,21,20,27,21,21,21,26,21,18,19,21,20,21,22,20,22,21,22,22,25,21,20,20,19,20,21,19,21,21,21,20,24,21,23,20,21,21,22,21,22,21,19,21,19,24,20,22,20,20,21,20,22,19,20,21,20,19,21,21,20,24,20,19,20,21,22,19,21,25,22,21,19,23,21,22,19,23,19,21,21,20,23,20,19,24,19,20,20,26,20,22,22,19,22,22,21,27,21,22,21,20,20,21,22,21,23,20,21,20,21,22,19,23,21,23,20,20,21,21,22,21,20,23,18,22,22,24,21,25,25,22,19,22,21,20,19,20,27,24,23,25,23,18,22,21,20,21,21,17,20,21,21,23,19,21,21,20,21,26,22,22,21,22,20,23,22,25,22,20,21,20,21,22,21,23,21,20,23,23,21,21,21,20,21,21,19,21,20,20,23,22,21,19,19,21,24,23,22,23,22,19,23,21,21,23,22,21,20,21,19,20,19,20,23,23,21,20,24,18,19,18,21,20,25,23,21,20,20,20,27,20,21,22,25,22,23,20,20,21,20,20,23,20,21,20,22,18,23,21,22,20,19,19,19,21,22,22,24,22,20,25,21,21,21,23,20,21,22,20,21,21,21,23,20,24,21,23,22,20,21,22,20,21,20,20,23,23,23,19,18,21,19,21,24,22,21,20,20,22,20,24,25,23,18,22,20,19,23,20,20,19,19,27,22,19,20,21,19

我基于这一千个kmaxk_{max}kmax​可以重新计算得到 A = 0.014421418309211731。然后我们在把A带进去验算以下其他数据,由于分了一千轮,导致计算速度会很慢,这边改造成了线程池方式,总验算数也从20降到了10,代码如下:

    val threadPool = ThreadPoolExecutor(20,100, 1,TimeUnit.MINUTES, LinkedBlockingQueue(20),ThreadPoolExecutor.CallerRunsPolicy())@Testfun testBernoulli2(){val a = 0.014421418309211731val countDownLatch = CountDownLatch(10)(1..10).forEach {threadPool.execute {var cnt = 1000000L * (it % 5 + 1)val list = (1..1000).map {bernoulliTest(cnt)}val value = a * 50 * list.size.toDouble() / list.sumOf { 1.0 / Math.pow(2.0, it.toDouble()) }println("伯努利试验次数=${cnt},估计值:${value}")countDownLatch.countDown()}}countDownLatch.await()}

执行结果如下:

伯努利试验次数=1000000,估计值:993474.5188010976
伯努利试验次数=2000000,估计值:2126125.4581640214
伯努利试验次数=3000000,估计值:2954221.3761083484
伯努利试验次数=2000000,估计值:2014385.0451727884
伯努利试验次数=1000000,估计值:972014.7350666528
伯努利试验次数=3000000,估计值:2948001.6829536217
伯努利试验次数=2000000,估计值:1960978.6289981091
伯努利试验次数=1000000,估计值:972128.3891155305
伯努利试验次数=3000000,估计值:3071573.320744116
伯努利试验次数=2000000,估计值:1946117.8199038433

可以看到当我们把一千轮作为一个整体时,这个估计值和实际机制就像样了。如果大家有更多的时间或者有更强大的算力,可以试试轮次作为一个样本试试。

伯努利试验和redis去重统计扯上关系

上面我们主要以试验数据去验证和优化这个根据概率论得到的数学公式。而redis的HyperLogLog数据结构就是基于这个公式而来:
n=A∗m∗m∑12kmaxn = A * m * \frac{m}{\sum{\frac{1}{2^{k_{max}}}}}n=A∗m∗∑2kmax​1​m​
根据前面的分析我们知道,A是常数,m是轮次数,都是固定的。假设我们现在要去统计一个网页的总浏览人数,我们每次增加一个用户id,我们类比做了一次伯努利试验,原理是redis将每次加入的用户id会hash成一个64为的比特串:[000000…101…00…1],redis会把比特串的从右往左的14位用来分论次,14位一共就能分2142^{14}214 = 16384轮。我们上面从50轮提到1000轮之后,精度上我们发现已经有很大的提升了,而redis这里用了16384轮,那最终得到的结果肯定比我们精度高得多。64位去除右侧的14位后还有50位,因为都是比特,只有0和1就像我们上面抛硬币只有正反面,我们在50位中找到第一次出现1的位置其实就是本次伯努利试验的kkk,我们将这个k记录下来,因为最大也就只能到50,所以我们使用一个6位的比特串就能进行记录(26=642^{6}=6426=64已经大于50了,所以足够记录了),当我们不断的往这个数据结构中增加用户id,每个用户id都会被分到16384轮中的其中一轮,然后得到自己的k,如果自己的比原本保存的k大,则替换成自己的k,否则就不做操作。如此循环往复,我们16384轮次中,每个轮次都会留下自己的kmaxk_{max}kmax​。然后我们就可以根据上面那个公式根据kmaxk_{max}kmax​反推出总共进行的伯努利试验次数nnn,对应具体场景其实就是总共的用户id数量。
那如何避免的对重复id的累计呢,因为重复的id,会被hash成同一个64位比特串,那最终所在的轮次以及得到的k和原本都是一样的,所以当重复添加的时候是没有任何影响的,从而实现去重统计。

最大内存12KB的由来

上面我们解析出了整个去重统计的过程,其实可以看到具体的内存消耗,我们一共分了16384轮,每一轮需要个6比特来存储kmaxk_{max}kmax​,所以我们一共需要的内存是:16384 * 6比特 = 98304比特,那根据计算机内存单位转换关系,8比特 = 1 byte, 1024byte = 1KB。所以 98304 / 8 / 1024 = 12KB。所以就实现了12KB 对 2642^{64}264的数据进行基数统计的功能。

redisHyperLogLog原理解析相关推荐

  1. Spark Shuffle原理解析

    Spark Shuffle原理解析 一:到底什么是Shuffle? Shuffle中文翻译为"洗牌",需要Shuffle的关键性原因是某种具有共同特征的数据需要最终汇聚到一个计算节 ...

  2. 秋色园QBlog技术原理解析:性能优化篇:用户和文章计数器方案(十七)

    2019独角兽企业重金招聘Python工程师标准>>> 上节概要: 上节 秋色园QBlog技术原理解析:性能优化篇:access的并发极限及分库分散并发方案(十六)  中, 介绍了 ...

  3. Tomcat 架构原理解析到架构设计借鉴

    ‍ 点击上方"方志朋",选择"设为星标" 回复"666"获取新整理的面试文章 Tomcat 架构原理解析到架构设计借鉴 Tomcat 发展这 ...

  4. 秋色园QBlog技术原理解析:性能优化篇:数据库文章表分表及分库减压方案(十五)...

    文章回顾: 1: 秋色园QBlog技术原理解析:开篇:整体认识(一) --介绍整体文件夹和文件的作用 2: 秋色园QBlog技术原理解析:认识整站处理流程(二) --介绍秋色园业务处理流程 3: 秋色 ...

  5. CSS实现元素居中原理解析

    原文:CSS实现元素居中原理解析 在 CSS 中要设置元素水平垂直居中是一个非常常见的需求了.但就是这样一个从理论上来看似乎实现起来极其简单的,在实践中,它往往难住了很多人. 让元素水平居中相对比较简 ...

  6. 秋色园QBlog技术原理解析:Web之页面处理-内容填充(八)

    文章回顾: 1: 秋色园QBlog技术原理解析:开篇:整体认识(一) --介绍整体文件夹和文件的作用 2: 秋色园QBlog技术原理解析:认识整站处理流程(二) --介绍秋色园业务处理流程 3: 秋色 ...

  7. 秋色园QBlog技术原理解析:UrlRewrite之无后缀URL原理(三)

    文章回顾: 1: 秋色园QBlog技术原理解析:开篇:整体认识(一) --介绍整体文件夹和文件的作用 2: 秋色园QBlog技术原理解析:认识整站处理流程(二) --介绍秋色园业务处理流程 本节,将从 ...

  8. Android之Butterknife原理解析

    转载请标明出处:[顾林海的博客] 个人开发的微信小程序,目前功能是书籍推荐,后续会完善一些新功能,希望大家多多支持! ##前言 Butterknife是一个专注于Android系统的View注入框架, ...

  9. 【深度学习】谷歌大脑EfficientNet的工作原理解析

    [深度学习]谷歌大脑EfficientNet的工作原理解析 文章目录 1 知识点准备1.1 卷积后通道数目是怎么变多的1.2 EfficientNet 2 结构2.1 方式2.2 MBConv卷积块2 ...

最新文章

  1. 如让自己想学不好shell编程都困难?
  2. linux usb全自动安装,制作liveusb实现centos6.2全自动无人职守安装
  3. 微架构设计:微博计数器的设计
  4. socket编程 -- epoll模型服务端/客户端通信的实现
  5. 数据科学家令人惊叹的排序技巧
  6. windows下使用Caffe框架和matlab实现SRCNN官方代码的步骤
  7. shell 进入hadoop_shell启动hadoop集群
  8. matlab 图像分块及恢复
  9. Android中Activity出现与退出的自定义动画
  10. C语言实现键盘记录器
  11. 计算机常用的汉字机内码有哪几种,常用的汉字机内码有几种?
  12. 离散数学及其应用【华章版】习题答案第一章01
  13. 商贸宝显示连接不到服务器,登录T1商贸宝就提示 服务器链接失败 请重新登录 这个怎么解决?...
  14. 手机浏览器 打开 APP,APP 嵌套在了浏览器里,网页跳转app问题
  15. 微信小程序页面跳转方式
  16. 【产业互联网周报】销售易获腾讯1.2亿美元投资;国科恒泰完成11亿C轮融资;工信部、科技部推进大数据及人工智能...
  17. 组件容器服务器的关系,什么是docker 容器编排
  18. 黑马JAVA P163 字节缓冲流的性能分析
  19. go语言google pay支付验证订单
  20. 几组数据的相关性python_几的解釋|几的意思|漢典“几”字的基本解釋

热门文章

  1. C++及数据结构复习笔记(十二)(列表)
  2. matlab金字塔,高斯金字塔的matlab实现
  3. Google Earth Engine(GEE)—— 快速进行农田作物土地分类和面积统计
  4. 考驾照选择 AI 教练,心态稳定不会骂人
  5. 为什么会一闪而过 c语言程序,为什么程序运行后会一闪而过呢[求助]
  6. 【毕业设计】深度学习昆虫识别系统 - 图像识别 opencv python
  7. 头歌--C++ 面向对象 - STL 的应用
  8. Mac 安装非App Store软件
  9. 【greenplum】greenplum pg_largeobject 大对象处理实践
  10. CST入门——边界条件Boundary的选择与设置