1 系统活跃度常用指标

我们先来看⼏个经常⽤来描述系统活跃度的名词:PV、UV、VV、IP。

🌰 举个栗⼦:假如你在家⽤ ADSL 拨号上⽹,早上 9 点访问了 JavaGuide 下的 2 个⻚⾯,下午 2 点⼜访问了 JavaGuide 下的 3 个⻚⾯。那么,对于 JavaGuide 来说,今天的 PV、UV、VV、IP 各项指标该如何计算?

  • PV 等于上午浏览的 2 个⻚⾯和下午浏览的 3 个⻚⾯之和,即 PV = 2 + 3
  • UV 指独⽴访客数,⼀天内同⼀访客的多次访问只计为 1 个 UV,即 UV = 1
  • VV 指访客的访问次数,上午和下午分别有⼀次访问⾏为,即 VV = 2
  • IP 为独⽴ IP 数,由于 ADSL 拨号上⽹每次都 IP 不同,即 IP = 2

PV(Page View) 即⻚⾯浏览量。每当⼀个⻚⾯被打开或者被刷新,都会产⽣⼀次 PV 。⼀般来说,PV 与来访者的数量成正⽐,但是 PV 并不直接决定⻚⾯的真实来访者数量,如果⼀个来访者通过不断的刷新⻚⾯或是使⽤爬⾍访问,也可以制造出⾮常⾼的 PV。

实际上,PV 的计算规则有很多种。就⽐如微信公众号的⼀篇⽂章,在⼀段时间内,即使你多次刷新也不会增加阅读量。这样做的好处就是:更能反映出点开⽂章的真实⽤户群体的数量了。

总结 :PV 能够反映出⽹站的⻚⾯被⽹站⽤户浏览/刷新的次数。

UV(Unique Visitor) 即独⽴访客。1 天内相同访客多次访问⽹站,只计算为 1 个独⽴访客。UV 是从⽤户个体的⻆度来统计的。

总结:UV 主要⽤来统计 1 天内访问某站点的⽤户数。

VV (Visit View) 即访客访问的次数。当访客完成所有的浏览并最终关掉该⽹站的所有⻚⾯时,便完成了⼀次访问。

总结:VV 主要⽤来记录⽹站⽤户在⼀天内访问你的站点的次数。

IP 即独⽴ IP 访问数。⼀天内使⽤不同 IP 地址的⽤户访问⽹站的次数,同⼀ IP 多次访问计数均为 1。

2 为什么要进行 PV&UV 统计?

⼤部分⽹站都会进⾏ PV&UV 的统计。就⽐如说咱们的 Github 的项⽬就⾃带 PV&UV 统计。

通过这张图,我可以清楚地知道我的项⽬访问量的真实情况。

简单来说,⽹站进⾏ PV&UV 统计有下⾯这些好处:

  • PV 和 UV 的结合更能反映项⽬的真实访问量,有助于我们更了解⾃⼰的⽹站,对于我们改进⽹站有指导意义。⽐如咱们⽹站的某个⽹⻚访问量最⼤,那我们就可以对那个⽹⻚进⾏优化改进。再⽐如我们的⽹站在周末访问量⽐较⼤,那我们周末就可以多部署⼀个服务来提⾼⽹站的稳定性和性能。
  • PV 和 UV 的结合可以帮助⼴告主预计投放⼴告可以带来的流量。

3 如何基于 Redis 统计 UV?

PV 的统计不涉及到数据的去重,⽽ UV 的计算需要根据 IP 地址或者当前登录的⽤户来作为去重标准。因此, PV 的统计相对于 UV 的统计来说更为简单⼀些。

因此我会重点介绍 UV 的统计。

最简单的办法就是:为每⼀个⽹⻚维护⼀个哈希表,⽹⻚ ID +⽇期为 Key, Value 为看过这篇⽂章的所有⽤户 ID 或者 IP(Set 类型的数据结构)。

当我们需要为指定的⽹⻚增加 UV ,⾸先需要判断对应的⽤户 ID 或者 IP 是否已经存在于对应的 Set 中。

当我们需要计算对应⻚⾯的 UV 的话,直接计算出⻚⾯对应的 Set 集合的⼤⼩即可!

这种⽅式在访问量不是特别⼤的⽹站,还是可以满⾜基本需求的。

但是,如果⽹站的访问量⽐较⼤,这种⽅式就不能够满⾜我们的需求了!

试想⼀下:如果⽹站的⼀个⻚⾯在⼀天之内就有接近 100w +不同⽤户访问的话,维护⼀个包含 100w+ ⽤户 ID 或者 ⽤户 IP 的 Set 在内存中,还要不断的判断指定的⽤户 ID 或者 ⽤户 IP 是否在其中,消耗还是⽐较⼤的,更何况这还是⼀个⻚⾯!

有没有对内存消耗⽐较⼩,⼜有类似 Set 功能的数据结构呢?

答案是有的!这个时候我们就需要⽤到 HyperLogLog 了!
其实, HyperLogLog 是⼀种基数计数概率算法 ,并不是 Redis 特有的。Redis 只是实现了这个算法并提供了⼀些开箱即⽤的 API。

Redis 提供的 HyperLogLog 占⽤空间⾮常⾮常⼩(基于稀疏矩阵存储), 12k 的空间就能存储接近 2^64 个不同元素。

不过, HyperLogLog 的计算结果并不是⼀个精确值,存在⼀定的误差,这是由于它本质上是⽤概率算法导致的。

但是,⼀般我们在统计 UV 这种数据的时候,是能够容忍⼀定范围内的误差的(标准误差是 0.81%,这对于 UV 的统计影响不⼤,可以忽略不计)。我们更关注的是这种⽅法能够为我们节省宝贵的服务器资源。

使⽤ Redis Hyperloglog 进⾏ UV 统计,我们主要会使⽤到以下三个命令:

  • PFADD key values : ⽤于数据添加,可以⼀次性添加多个。添加过程中,重复的记录会⾃动去重。
  • PFCOUNT key : 对 key 进⾏统计。
  • PFMERGE destkey sourcekey1 sourcekey2 : 合并多个统计结果,在合并的过程中,会⾃动去重多个集合中重复的元素。

3.1 具体是怎么做的呢 ?

1、将访问指定⻚⾯的每个⽤户 ID 添加到 HyperLogLog 中。

PFADD PAGE_1:UV USER1 USER2 ... USERn

2、统计指定⻚⾯的 UV。

PFCOUNT PAGE_1:UV

HyperLogLog 除了上⾯的 PFADDPFCOIUNT 命令外,还提供了 PFMERGE ,将多个 HyperLogLog 合并在⼀起形成⼀个新的 HyperLogLog 值。

PFMERGE destkey sourcekey [sourcekey ...]

我们来⽤ Java 写⼀个简单的程序来实际体验⼀下,顺便来对⽐⼀下 Set 和 HyperLogLog 这两种⽅式。

我们这⾥使⽤ Jedis 提供的相关 API。直接在项⽬中引⼊ Jedis 相关的依赖即可:

<dependency>
	<groupId>redis.clients</groupId>
	<artifactId>jedis</artifactId>
	<version>3.6.0</version>
</dependency>

代码如下,我们循环添加了 10w 个⽤户到指定 SetHyperLogLog 中。

400|600

输出结果:

100.00%
99.27%

从输出结果可以看出 Set 可以⾮常精确的存储这 10w 个⽤户,⽽ HyperLogLog 有⼀点点误差,误差率⼤概在 0.73% 附近。

我们再来对⽐⼀下两者的存储使⽤空间。

500|600

我们可以通过 debug object key 命令来查看某个 key 序列化后的⻓度。输出的项的说明:

  • Value at :key 的内存地址
  • refcount :引⽤次数
  • encoding :编码类型
  • serializedlength:序列化⻓度(单位是 Bytes)
  • lru_seconds_idle:空闲时间

不过,你需要注意的是 serializedlength 仅仅代表 key 序列化后的⻓度(持久化本地的时候会⽤到),并不是 key 在内存中实际占⽤的⻓度。不过,它也侧⾯反应了⼀个 key 所占⽤的内存,可以⽤来⽐较两个 key 消耗内存的⼤⼩。

从上⾯的结果可以看出内存占⽤上, Hyperloglog 消耗了 10523 bytes ≈ 10kb,⽽ Set 消耗了 988895 bytes ≈ 965kb (粗略估计,两者实际占⽤内存⼤⼩会更⼤)。

可以看出,仅仅是 10w 的数据,两者消耗的内存差别就这么⼤,如果数据量更⼤的话,两者消耗的内存的差距只会更⼤!

我们这⾥再拓展⼀下: 假如我们需要获取指定天数的 UV 怎么办呢

其实,思路很简单!我们在 key 上添加⽇期作为标识即可!

PFADD PAGE_1:UV:2021-12-19 USER1 USER2 ... USERn

那假如我们需要获取指定时间(精确到⼩时)的 UV 怎么办呢

思路也⼀样,我们在 key 上添加指定时间作为标识即可!

PFADD PAGE_1:UV:2021-12-19-12 USER1 USER2 ... USERn

4 后记

除了上⾯介绍到的⽅案之外,Doris 、ClickHouse 等⽤于联机分析(OLAP)的列式数据库管理系统(DBMS)现在也经常⽤在统计相关的场景。⽐如说百度的百度统计(⽹站流量分析)就是基于 Doris 做的,再⽐如说 Yandex(俄罗斯的⼀家做搜索引擎的公司)的在线流量分析产品就是⽤⾃家的 ClickHouse 做的。