细品Reids的HyperLogLog数据结构
时间:2022-07-23
本文章向大家介绍细品Reids的HyperLogLog数据结构,主要内容包括其使用实例、应用技巧、基本知识点总结和需要注意事项,具有一定的参考价值,需要的朋友可以参考一下。
背景
实现一个统计页面的UV数据,每个网页的用户访问量(同一个用户多次请求只算一次)。那这个功能我们怎么去实现呢?
也许有同学就会说了,我们都用的是growingIO,不用自己实现成本太高,直接用别人的。这样也挺好。
今天我们来看一下我们自己如何实现这个需求并且可以抗击较高的TPS服务呢?
设计方案
- 既然是页面用户量的统计是不重复的那我们选择一个数据结构那就是SET集合进行存储。将用户的ID进行存储,如果是没有登录的用户随机生成一个(使用时间戳等),存入set。为了快那就基于内存来搞?但是不可能用自身服务的内存吧,那就借助与第三方服务的内存,那就使用redis进行且自身带有set集合数据结构。
- 选定好了方向和中间件,那就得考虑一下量的问题,统计的可都是热点页面,一天有个几千万的UV,那你就得有很大的浪费很大的空间进行存储,那我们这样做值得吗?还有就是只是为了大概过一下请求量不需要太精确的数据,那我们还有没有更佳的数据结构呢?
- Redis 提供了 HyperLogLog 数据结构就是用来解决 这种统计问题的
HyPerLogLog
简介
HyperLogLog 提供不精确的去重计数方案,虽然不精确但是也不是非常不 精确,标准误差是 0.81%,这样的精确度已经可以满足上面的 UV 统计需求了。 HyperLogLog 数据结构是 Redis 的高级数据结构,它非常有用,今天我们就来“深入”的探究一下(九浅一深的那种)。
使用
- HyperLogLog提供了三个指令pfadd和pfcount ,pfmerge 根据字面意义很好理解,一个是增加计数一个是获取计数,pfadd的用法和set集合的sadd是一样的,来一个用户ID,就将用户ID 赛进去就是。pfcount和scard的用法是一样的。直接获取计数值。还有pfmerge字面意思是合并,那就是当两个key的统计合为一个key的统计(将两个页面合在一起的统计数量)
- 但是有人会问为什么又个pf呢?咋不安常规套路出牌,HL不是更好吗?这个PF是这个数据结构发明人的拼写。
- 可以对应一下上面的业务需求,将页面作为key 将用户iD放入HyPerLogLog进行统计。
127.0.0.1:6379> pfadd codehole user1
(integer) 1
127.0.0.1:6379> pfcount codehole
(integer) 1
127.0.0.1:6379> pfadd codehole user2
(integer) 1
127.0.0.1:6379> pfcount codehole
(integer) 2
127.0.0.1:6379> pfadd codehole user3
127.0.0.1:6379> pfcount codehole
(integer) 3
127.0.0.1:6379> pfadd codehole user4
(integer) 1
127.0.0.1:6379> pfcount codehole
(integer) 4
127.0.0.1:6379> pfadd codehole user5
(integer) 1
127.0.0.1:6379> pfcount codehole
(integer) 5
127.0.0.1:6379> pfadd codehole user6
(integer) 1
127.0.0.1:6379> pfcount codehole
(integer) 6
127.0.0.1:6379> pfadd codehole user7 user8 user9 user10
(integer) 1
127.0.0.1:6379> pfcount codehole
(integer) 10
进行测试一下,感觉还是挺准确的。凭感觉这回事,往往都是错误的。 我们通过脚本进行大量的测试一下:
public class JedisTest {
public static void main(String[] args) {
Jedis jedis = new Jedis();
for (int i = 0; i < 100000; i++) {
jedis.pfadd("codehole", "user" + i);
}
long total = jedis.pfcount("codehole");
System.out.printf("%d %dn", 100000, total);
jedis.close();
} }
跑完后查看: 100000 99723 再跑一遍:100000 99723 没有变说明确实是去重了。 差了 277 个,按百分比是 0.277%
实现原理
几个重要的概念:
- HyperLogLog实际上不会存储每个元素的值,它使用的是概率算法,通过存储元素的hash值的第一个1的位置,来计算元素数量。
- 伯努利实验: 大概意思就是 进行N撩妹,无非就是撩到妹子和没撩到妹子(成功和不成功都为50%,没有其他因素)。撩到妹子就算一回合(比如说你撩妹100次都没成功底101次成功了那这101次才算一回合)。于是进行了N回合,把最长的那次给哥们说了,让你哥们猜测一共进行了多少回合?(也就是猜这个N是多少)。有一个大佬也遇到了此问题通过大量实验得到了一个公式,就可以计算出来,具体了解可以点击(https://zhuanlan.zhihu.com/p/58519480)
- 下面图的意思是,给定一系列的随机整数,我们记录下低位连续零位的最大长度 k,通 过这个 k 值可以估算出随机数的数量。 (这个随机数类比为我们的用户ID,最大长度K也就是这个随机数通过hash计算出来放在桶上的重复次数(个人理解)) 可以和上面的伯努利实验对比一下。
4.总而言之 我们可以在不存储值的情况下,通过概率计算公式可以得到相差不是很大的统计结果。
代码简单的实现
- 实现的一个简单的计算公式(帮助理解一下):
import java.util.concurrent.ThreadLocalRandom;
public class PfTest {
static class BitKeeper {
private int maxbits;
public void random(long value) {
int bits = lowZeros(value);
if (bits > this.maxbits) {
this.maxbits = bits;
}
}
private int lowZeros(long value) {
int i = 1;
for (; i < 32; i++) {
if (value >> i << i != value) {
break;
}
}
return i - 1;
}
}
static class Experiment {
private int n;
private int k;
private BitKeeper[] keepers;
public Experiment(int n) {
this(n, 1024);
}
public Experiment(int n, int k) {
this.n = n;
this.k = k;
this.keepers = new BitKeeper[k];
for (int i = 0; i < k; i++) {
this.keepers[i] = new BitKeeper();
}
}
public void work() {
for (int i = 0; i < this.n; i++) {
long m = ThreadLocalRandom.current().nextLong(1L << 32);
BitKeeper keeper = keepers[(int) (((m & 0xfff0000) >> 16) % keepers.length)];
keeper.random(m);
}
}
public double estimate() {
double sumbitsInverse = 0.0;
for (BitKeeper keeper : keepers) {
sumbitsInverse += 1.0 / (float) keeper.maxbits;
}
double avgBits = (float) keepers.length / sumbitsInverse;
return Math.pow(2, avgBits) * this.k;
}
}
public static void main(String[] args) {
for (int i = 100000; i < 1000000; i += 100000) {
Experiment exp = new Experiment(i);
exp.work();
double est = exp.estimate();
System.out.printf("%d %.2f %.2fn", i, est, Math.abs(est - i) / i);
}
}
}
总结
- HyperLogLog可以高性能实现UV统计,但是会有百分比的误差
- HyperLogLog的三个命令 pfadd pfcount pfmerge
- HyperLogLog的大概原理通过概率统计(伯努利实验)得出结果,不用记录具体的值
- 使用Java简单实现概率统计,计算随机数的量。
更加牛皮的文章
https://zhuanlan.zhihu.com/p/58519480 https://en.wikipedia.org/wiki/HyperLogLog
《redis深度历险》–书
- JavaScript 教程
- JavaScript 编辑工具
- JavaScript 与HTML
- JavaScript 与Java
- JavaScript 数据结构
- JavaScript 基本数据类型
- JavaScript 特殊数据类型
- JavaScript 运算符
- JavaScript typeof 运算符
- JavaScript 表达式
- JavaScript 类型转换
- JavaScript 基本语法
- JavaScript 注释
- Javascript 基本处理流程
- Javascript 选择结构
- Javascript if 语句
- Javascript if 语句的嵌套
- Javascript switch 语句
- Javascript 循环结构
- Javascript 循环结构实例
- Javascript 跳转语句
- Javascript 控制语句总结
- Javascript 函数介绍
- Javascript 函数的定义
- Javascript 函数调用
- Javascript 几种特殊的函数
- JavaScript 内置函数简介
- Javascript eval() 函数
- Javascript isFinite() 函数
- Javascript isNaN() 函数
- parseInt() 与 parseFloat()
- escape() 与 unescape()
- Javascript 字符串介绍
- Javascript length属性
- javascript 字符串函数
- Javascript 日期对象简介
- Javascript 日期对象用途
- Date 对象属性和方法
- Javascript 数组是什么
- Javascript 创建数组
- Javascript 数组赋值与取值
- Javascript 数组属性和方法
- VBA快速提取引用工程的代码
- NuGet 如何设置图标
- VBA解析VBAProject 00
- Echarts图表宽度变成100px,让图表宽度随着父元素自动适应,Vue实时监听宽度的变化,这可能是史上最好的解决方案!
- Vim 基础和常用命令整理
- TinyMCE 优化百度地图 bdmap 插件
- 更新!万字长文带你拿下九大排序的原理、Java 实现以及算法分析
- mysql 同一张表查询 left join
- uni-app运行到浏览器跨域H5页面的跨域问题解决方案
- 手牵手,使用uni-app从零开发一款视频小程序 (系列上 准备工作篇)
- 树状数据库表查询2次以上(自连接、内连接、别名)方法
- 网页背景H5视频自动播放---PC端、移动端兼容问题完美解决方案(IOS、安卓、微信端)
- 【STM32F407】第11章 RL-TCPnet V7.X之TCP服务器
- 如何解决nodejs中cpu密集型的任务
- 博客园主题1【备份】