如果现在要开发一个功能:

统计APP或网页的一个页面,每天有多少用户点击进入的次数。同一个用户的反复点击进入记为 1 次,也就是统计 UV 数据。

让你来开发这个统计模块,你会如何实现?

如果统计 PV 数据,只要给网页一个独立的 Redis 计数器就可以了,这个计数器的 key 的格式为 puv:{pid}:{yyyyMMdd}。每来一个请求就 incrby 一次,就可以统计出所有的 PV 数据。

但是 UV 不一样,它要去重,同一个用户一天之内的多次访问请求只能计数一次。这就要求每一个网页请求都需要带上用户的 ID,无论是登陆用户还是未登陆用户都需要一个唯一 ID 来标识。

你可能会马上想到,用 Hash 数据类型就能满足去重。这确实是一种解决方法,但是当这个页面的日活达到百万或千万以上级别的话,Hash 的内存开销就会非常大。

我们来估算一下采用 Hash 的内存空间是多大。假设 keyint 类型,对应的是用户ID,valuebool 类型,表示已访问,当有百万级不同用户访问时,内存空间为:100万 * (32+8)bit = 40MB

那有更好的方法吗?有的,下面来介绍基于 HyperLogLog 的解决方案。首先我们先来了解一下 HyperLogLog

HyperLogLog

HyperLogLog 的作用是提供不精确的去重计数方案。虽然不精确,但也不是非常不精确,标准误差是 0.81%,这样的精确度已经可以满足上面的 UV 统计需求了。

它的优点是使用极少的内存就能统计大量的数据,Redis 实现的 HyperLogLog,只需要 12K 内存就能统计 $2^64$ 个数据。远比 Hash 的内存开销要少。

HyperLogLog(HLL) 是一种用于基数计数的概率算法,是基于 LogLog(LLC) 算法的优化和改进,在同样空间复杂度下,能够比 LLC 的基数估计误差更小。

HyperLogLog 算法的通俗说明:假设我们为一个数据集合生成一个8位的哈希串,那么我们得到00000111的概率是很低的,也就是说,我们生成大量连续的0的概率是很低的。生成连续5个0的概率是1/32,那么我们得到这个串时,可以估算,这个数据集的基数是32。

再深入的那就是数学公式,可参考本文最后的参考链接前往研究。

Redis 中 HLL 的使用

命令 说明 可用版本 时间复杂度
PFADD 添加 >= 2.8.9 O(1)
PFCOUNT 获得基数值 >= 2.8.9 O(1)
PFMERGE 合并多个key >= 2.8.9 O(N)

示例代码

using StackExchange.Redis;
using System; public class PageUVDemo
{
private static IDatabase db; static void Main(string[] args)
{
ConnectionMultiplexer connection = ConnectionMultiplexer.Connect("192.168.0.104:7001,password=123456"); db = connection.GetDatabase(); Console.WriteLine("hll:");
HLLVisit(1000, 1000);
HLLVisit(10000, 10000);
HLLVisit(100000, 100000); Console.WriteLine("hash:");
HashVisit(1000, 1000);
HashVisit(10000, 10000);
HashVisit(100000, 100000);
connection.Close();
} static void HLLVisit(int times, int pid)
{
string key = $"puv:hll:{pid}";
DateTime start = DateTime.Now;
for (int i = 0; i < times; i++)
{
db.HyperLogLogAdd(key, i);
}
long total = db.HyperLogLogLength(key); DateTime end = DateTime.Now; Console.WriteLine("插入{0}次:", times);
Console.WriteLine(" total:{0}", total);
Console.WriteLine(" duration:{0:F2}s", (end - start).TotalSeconds);
Console.WriteLine();
} static void HashVisit(int times, int pid)
{
string key = $"puv:hash:{pid}";
DateTime start = DateTime.Now;
for (int i = 0; i < times; i++)
{
db.HashSet(key, i, true);
}
long total = db.HashLength(key); DateTime end = DateTime.Now; Console.WriteLine("插入{0}次:", times);
Console.WriteLine(" total:{0}", total);
Console.WriteLine(" duration:{0:F2}s", (end - start).TotalSeconds);
Console.WriteLine();
}
}

运行结果

结果对比

数据通过 redis-rdb-tools 导出,更多请查看

数据类型 插入次数 内存开销 时间开销 误差率
hash 1000 35KB 3.45s 0%
10000 426KB 34.65s 0%
100000 3880KB 342.36s 0%
hll 1000 2KB 3.57s 0.1%
10000 14KB 33.25s 0.13%
100000 14KB 307.80s 0.44%

从上面的结果可以看出,10万次级别下,HyperLogLog 的误差率很低,0.44%,但内存开销是 Hash 的0.3%,随着数量级的提升,内存开销差距也越大。

应用场景

  • 统计注册 IP 数
  • 统计每日访问 IP 数
  • 统计页面实时 UV 数
  • 统计在线用户数
  • 统计用户每天搜索不同词条的个数

总结

不追求百分百的准确度时,使用 HyperLogLog 数据结构能减少内存开销。

参考资料

Redis实战篇(三)基于HyperLogLog实现UV统计功能的更多相关文章

  1. Redis实战篇

    Redis实战篇 1 Redis 客户端 1.1 客户端通信 原理 客户端和服务器通过 TCP 连接来进行数据交互, 服务器默认的端口号为 6379 . 客户端和服务器发送的命令或数据一律以 \r\n ...

  2. Redis 实战篇:巧用数据类型实现亿级数据统计

    在移动应用的业务场景中,我们需要保存这样的信息:一个 key 关联了一个数据集合,同时还要对集合中的数据进行统计排序. 常见的场景如下: 给一个 userId ,判断用户登陆状态: 两亿用户最近 7 ...

  3. Redis 实战篇:巧用Bitmap 实现亿级海量数据统计

    在移动应用的业务场景中,我们需要保存这样的信息:一个 key 关联了一个数据集合. 常见的场景如下: 给一个 userId ,判断用户登陆状态: 显示用户某个月的签到次数和首次签到时间: 两亿用户最近 ...

  4. Redis实战篇(一)搭建Redis实例

    今天是Redis实战系列的第一讲,先从如何搭建一个Redis实例开始. 下面介绍如何在Docker.Windows.Linux下安装. Docker下安装 1.查看可用的 Redis 版本 访问 Re ...

  5. Redis实战篇(二)基于Bitmap实现用户签到功能

    很多应用上都有用户签到的功能,尤其是配合积分系统一起使用.现在有以下需求: 签到1天得1积分,连续签到2天得2积分,3天得3积分,3天以上均得3积分等. 如果连续签到中断,则重置计数,每月重置计数. ...

  6. Redis实战篇(四)基于GEO实现查找附近的人功能

    如果现在要开发一个功能: 要为一款交友App实现查找附近的人,并按距离进行排序. 让你来开发这个功能,你会如何实现? MySQL 不合适 你可能想到,把用户用户的经纬度坐标使用MySQL等关系数据库( ...

  7. Redis 实战篇之搭建集群

    Redis 集群简介# Redis Cluster 即 Redis 集群,是 Redis 官方在 3.0 版本推出的一套分布式存储方案.完全去中心化,由多个节点组成,所有节点彼此互联.Redis 客户 ...

  8. Redis 实战篇:GEO助我邂逅附近女神

    码老湿,阅读了你的巧用数据类型实现亿级数据统计之后,我学会了如何游刃有余的使用不同的数据类型(String.Hash.List.Set.Sorted Set.HyperLogLog.Bitmap)去解 ...

  9. Redis实战(三)CentOS 7上Redis主从复制

    一主二从架构 1.一主二从架构图 2.通过命令 mkdir redisCluster创建redis集群文件夹 3.通过命令mkdir 6380   mkdir 6381   mkdir 6382在re ...

随机推荐

  1. vue v-on-clickaway

    vue v-on-clickaway Custom directive 自定义指令 https://stackoverflow.com/questions/36170425/detect-click- ...

  2. p5.js

    p5.js p5.j​​s是一个用于创意编码的JavaScript库,其重点是使艺术家,设计师,教育者,初学者以及其他任何人都可以访问并包含所有编码! https://p5js.org/ https: ...

  3. React & Calendar

    React & Calendar 日历 https://github.com/YutHelloWorld/calendar/blob/master/src/Calendar.js // 国际化 ...

  4. h5 localStorage和sessionStorage浏览器数据缓存

    sessionStorage 会话数据,localStorage 没有过期时间 两个的API基本都一样的 基本的使用 // 保存一个数据 sessionStorage.setItem('key', ' ...

  5. Captain technology INC:高性能电池助力新能源车企销量暴涨

    来自七个欧洲国家的11个合作伙伴共聚一堂,投身于HyFlow研究项目,共同开发一种可兼顾高性能和能源需求的混合氧化还原液流储能系统.为此,科学家们希望将高性能钒氧化还原液流电池与超级电容器相结合. 到 ...

  6. go-admin在线开发平台学习-3[细节解析]

    本章节主要的内容是对go-admin中的一些有趣编码进行分析,为自己以后提供一些借鉴 使用cli方式启动项目 使用cobra[眼镜蛇]完成强壮cli的工具,确保稳定. 使用cli的方式启动项目的好处显 ...

  7. Java开发的得力助手---Guava

    导语 guava是google出品的java类库,被google广泛用于内部项目,该类库经过google大牛们的千锤百炼,以优雅的设计在java世界流行.版本迭代至今,很多思想甚至被JDK标准库借鉴, ...

  8. 微信小程序:自定义组件

    为什么要学习自定义组件? 1.用上我自己的单词abc,我希望在页面中展示椭圆形的图片, 2.打开手机淘宝,假如现在要做一个企业级项目,里面有很多页面,首页存在导航模块,点击天猫,进入第二个页面,而第二 ...

  9. 五大自动化测试的Python框架

    1.Robot Framework 作为最重要的Python测试框架之一,Robot Framework主要被用在测试驱动(test-driven)类型的开发与验收中.虽然是由Python开发而来,但 ...

  10. MySQL学习笔记(五)

    倒数第二天!冲冲冲!!! 一.索引 一个表里面可以有多个索引. 1. 索引的作用:约束与加速查找 无索引:从前到后依次查找 有索引:会为索引列创造一个额外文件(以某种格式存储).在使用索引进行查找时, ...