引入场景
如果你负责开发维护一个大型的网站,有一天老板找产品经理要网站每个网页每天的UV数据,然后让你来开发这个统计模块,你会如何实现?
- 如果是统计PV那是非常好办,给每个网页一个独立的Redis计数器就可以了,这个计数器的key后缀加上当前的日期,这样来一个请求,incrby一次,最终就可以统计出所有的PV数据
- 但是UV不一样,它要去重,同一个用户一天之内的多次访问请求之内计数一次。这就就要求每一个网页请求都需要带上用户的ID,无论是登录用户还是未登录用户都需要一个唯一的ID来表示
也许你已经想到了一个简单的方案,那就是为每一个页面一个独立的set集合来存储所有当天访问过此页面的用户ID。当一个请求过来时,我们使用sadd将用户ID塞进去就可以了。通过scard
可以取出这个集合的大小,这个数字就是这个页面的UV数据。没错,这是一个非常简单的方案。
但是,如果你得页面访问量非常大,比如一个爆款页面几千万的UV,你需要一个很大的set集合来统计,这就非常浪费空间。如果这样的页面很大,那需要的存储空间是非常惊人的。为这样一个去重功能就耗费这样多的存储空间,值得么?其实老板需要的数据又不需要太精确,105w 和 106w 这两个数字对于老板们来说并没有多大区别,So,有没有更好的解决方案呢?
------->:Redis 提供了 HyperLogLog 数据结构就是用来解决这种统计问题的。
应用场景:如果不需要获取数据集的内容,只是想得到不同值得个数。“唯一计数”
UV(独立游客) 、 PV(页面浏览量)
-
UV(Unique visitor,独立游客) :
- 是指通过互联网访问、浏览这个网页的自然人。
- 访问网站的一台电脑客户端为一个访问
- 00:00-24:00内相同的客户端只被计算一次。
-
IP(Internet Protocol):
- 独立IP是指访问过某站点的IP总数,以用户的IP地址作为统计依据
- 00:00-24:00内相同IP地址之被计算一次。
-
UV与IP区别:
- 你和你的家人用各自的账号在同一台电脑上登录新浪微博,则IP数+1,UV数+2。
- 由于使用的是同一台电脑,所以IP不变,但使用的不同账号,所以UV+2
-
PV(Page View):
- 即页面浏览量或点击量,用户每1次对网站中的每个网页访问均被记录1个PV
- 用户对同一页面的多次访问,访问量累计,用以衡量网站用户访问的网页数量
VV(Visit View)
- 用以统计所有访客1天内访问网站的次数
- 当访客完成所有浏览并最终关掉该网站的所有页面时便完成了一次访问,同一访客1天内可能有多次访问行为,访问次数累计。
- PV与VV区别:
- 你今天10点钟打开了百度,访问了它的三个页面;
- 11点钟又打开了百度,访问了它的两个页面,则PV数+5,VV数+2.
- PV是指页面的浏览次数,VV是指你访问网站的次数。
HyperLogLog
hyperloglog
并不是一种新的数据结构(实际类型为字符串类型),而是一种基数算法,通过hyperloglog可以利用极小的内存空间完成独立总数的统计,数据集可以是IP、Email、ID等
- HyperLogLog 是用来做基数统计的算法。
- HyperLogLog 的优点是,在输入元素的数量或者体积非常非常大时,计算基数所需的空间总是固定的、并且是很小的
- 在redis里面,每个HyperLogLog 键只需要花费12KB内存,就可以计算接近2^64个不同元素的基数。这和计算基数时,元素越多耗费内存就越多的集合形成鲜明对比
什么是基数
比如数据集 {1, 3, 5, 7, 5, 7, 8}, 那么这个数据集的基数集为 {1, 3, 5 ,7, 8}, 基数(不重复元素)为5。 基数估计就是在误差可接受的范围内,快速计算基数。
命令
pfadd:添加
作用
添加指定元素到 HyperLogLog 中。
语法
redis 127.0.0.1:6379> PFADD key element [element ...]
返回值
整型,如果至少有个元素被添加返回 1, 否则返回 0。
实例
redis 127.0.0.1:6379> PFADD mykey a b c d e f g h i j
(integer) 1
redis 127.0.0.1:6379> PFCOUNT mykey
(integer) 10
pfcount:统计
作用
- 返回给定 HyperLogLog 的基数估算值。
语法
redis 127.0.0.1:6379> PFCOUNT key [key ...]
返回值
- 整数
- 返回给定 HyperLogLog 的基数值
- 如果多个 HyperLogLog 则返回基数估值之和。
实例
redis 127.0.0.1:6379> PFADD hll foo bar zap
(integer) 1
redis 127.0.0.1:6379> PFADD hll zap zap zap
(integer) 0
redis 127.0.0.1:6379> PFADD hll foo bar
(integer) 0
redis 127.0.0.1:6379> PFCOUNT hll
(integer) 3
redis 127.0.0.1:6379> PFADD some-other-hll 1 2 3
(integer) 1
redis 127.0.0.1:6379> PFCOUNT hll some-other-hll
(integer) 6
redis>
pfmerge:合并
作用
pfmerge可以求出多个HyperLogLog的并集并赋值给destkey
语法
PFMERGE destkey sourcekey [sourcekey ...]
返回值
返回 OK。
实例
redis> PFADD hll1 foo bar zap a
(integer) 1
redis> PFADD hll2 a b c foo
(integer) 1
redis> PFMERGE hll3 hll1 hll2
"OK"
redis> PFCOUNT hll3
(integer) 6
redis>
内部编码
HLL实际上是当作字符串存储的,因此,作为一个键值对,他可以很容易的被持久化至外部或者从外部持久化中恢复。
在redis内部中,使用了两种方式来存储HLL对象
- 稀疏Sparse:对于那些长度小于配置中hll-sparse-max-bytes选项设置的值(默认为3000)的HLL对象,采用此编码,存储效率更高,但是会消耗更多CPU
- 稠密Dense:当Sparse不适用时采用
总结
HyperLogLog内存占用量非常小,但是存在一定的误差,开发者在进行数据结构选项时只需要确认如下两条即可:
- 只是为了统计独立计数,不需要获取单条数据
- 可以容忍一定误差,毕竟HyperLogLog在内存的占用上有很大优势