当一个系统上线后,基本都需要统计用户活跃度,活跃度一般有两个指标,一个是PV(Page View)页面浏览量,一个是UV(Unique Visitor)唯一用户量,比如微信小程序后台中就有每小时UV的统计。
比如你在上午访问了腾讯社区2次,下午访问了腾讯社区3次,那么PV就是2 + 3 = 5次,UV为1次。
UV统计,同样日期为key,value为唯一标识用户的ID或IP的Set集合(本文使用用户IP来作为唯一标识),用户访问时如果Set中不存在当前访问用户IP,则UV+1,并将IP加入Set中;当我们读取UV时,即读取Set中元素个数。
如果不想在Redis中保存太多数据,我们可以把每天的PV、UV数据落库一次。
这里使用RedisTemplate访问redis,使用Hutool的ServletUtil获取用户ip。
@Resource
private RedisTemplate redisTemplate;
//redis的pv和uv前缀
final static String PV\_PREFIX = "pv\_";
final static String UV\_PREFIX = "uv\_";
/\*\*
\* 统计pv,uv
\* @return 返回统计后的pv,uv值
\*/
@GetMapping()
public AjaxResult statist(HttpServletRequest request) {
// 获取yyyy-MM-dd格式的日期
Date date = new Date();
SimpleDateFormat sdf = new SimpleDateFormat("yyyy-MM-dd");
String today = sdf.format(date);
String pvKey = PV\_PREFIX + today;
String uvKey = UV\_PREFIX + today;
// pv + 1, incr命令:将 key 中储存的数字值增一。如果 key 不存在,那么 key 的值会先被初始化为 0 ,然后再执行 INCR 操作。
Long pvNum = redisTemplate.opsForValue().increment(pvKey);
// hutool获取用户ip
String clientIP = ServletUtil.getClientIP(request, null);
// 将ip放到redis的set中
redisTemplate.opsForSet().add(uvKey, clientIP); //"SADD myset hello world"
Long uvNum = redisTemplate.opsForSet().size(uvKey); // "SCARD myset"
AjaxResult ans = AjaxResult.success();
ans.put("pv",pvNum);
ans.put("uv",uvNum);
return ans;
}
HyperLogLog统计UV为什么使用HyperLogLog在统计UV时我们刚刚使用的是Set保存全部的IP,它本身是去重的,最终Set元素的个数就是我们需要的值,用户量不多时还是可以接受的,但当用户人数上去时,达到百万,千万级时,保存全部ip还是非常占内存的。
那有没有什么办法能够减少内存使用?Redis有提供HyperLogLog的算法,它是根据统计学的基数估算算法,用最多12k的内存空间进行基数统计,但由于它是估算的算法,会有一定的误差,误差率约为0.81%。
关于HyperLogLog的命令我们主要使用以下三个:
Redis使用HyperLogLog统计UV:
// 将ip放到redis的HyperLogLog中
redisTemplate.opsForHyperLogLog().add(uvKey,clientIP); //PFADD mypf ip1 ip2
Long uvNum = redisTemplate.opsForHyperLogLog().size(uvKey); //PFCOUNT mypf
我们实际体验下,在Set和HyperLogLog中都放入10w条数据,比较他们的误差率。
@Resource
private RedisTemplate redisTemplate;
String uvSetKey = "uv_set_2024-07-13";
String uvPFKey = "uv_pf_2024-07-13";
long num = 100000;
@Test
public void initData(){
// 初始化添加100w条数据
for (int i = 1; i <= num; i++) {
redisTemplate.opsForSet().add(uvSetKey,i);
redisTemplate.opsForHyperLogLog().add(uvPFKey,i);
}
}
@Test
public void getData(){
// 获取误差率
Long setSize = redisTemplate.opsForSet().size(uvSetKey);
Long pfSize = redisTemplate.opsForHyperLogLog().size(uvPFKey);
DecimalFormat format = new DecimalFormat("##.00%");
String setFormat = format.format((double) setSize / (double) num);
String pfFormat = format.format((double) pfSize / (double) num);
System.out.println("set: " + setFormat);
System.out.println("pf: " + pfFormat);
}
结果输出:
set: 100.00%
pf: 99.56%
可以看到Set的是完全没有误差的,本次HyperLogLog的误差率为0.44%,对于统计UV这种数据时,我们一般都是有一定容忍度的,我们更专注服务器的资源使用情况,0.81%左右的误差我们是可以接受的。
在Navicat中我们可以看到10w条数据的set占用内存为4M,而HyperLogLog只占用了12k。
此外,我们可以通过Redis的命令debug object key查看某个key的序列化后的长度。返回的参数有以下五个:
➢ Value at :key 的内存地址
➢ refcount :引用次数
➢ encoding :编码类型
➢ serializedlength:序列化长度(单位是 Bytes)
➢ lru_seconds_idle:空闲时间
返回的serializedlength 仅仅代表 key 序列化后的长度,key 在内存中实际占用的内存会比这个值大。不过,它也侧面反应了一个 key 所占用的内存。
原创声明:本文系作者授权腾讯云开发者社区发表,未经许可,不得转载。
如有侵权,请联系 cloudcommunity@tencent.com 删除。
原创声明:本文系作者授权腾讯云开发者社区发表,未经许可,不得转载。
如有侵权,请联系 cloudcommunity@tencent.com 删除。