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

统计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. MobX All In One

    MobX All In One Simple, scalable state management. https://mobx.js.org/README.html https://github.co ...

  2. React Testing All in One

    React Testing All in One React 测试 https://reactjs.org/docs/testing.html jest 26.4 https://jestjs.io/ ...

  3. Web Components All In One

    Web Components All In One Web Components https://www.webcomponents.org/ HTML Template Custom Element ...

  4. Top 10 JavaScript errors

    Top 10 JavaScript errors javascript errors https://rollbar.com/blog/tags/top-errors https://rollbar. ...

  5. js to svg flowchart

    js to svg flowchart flowchart https://flowchart.js.org/ https://github.com/adrai/flowchart.js https: ...

  6. node mailer & email bot

    node mailer & email bot email https://nodemailer.com/about/ https://github.com/nodemailer/nodema ...

  7. Flutter 创建透明的路由页面

    原文 import 'package:flutter/material.dart'; void main() => runApp(MyApp()); class MyApp extends St ...

  8. 瞧一瞧React Fiber

    啥是React Fiber? React Fiber,简单来说就是一个从React v16开始引入的新协调引擎,用来实现Virtual DOM的增量渲染. 说人话:就是一种能让React视图更新过程变 ...

  9. 生产者和消费者问题(synchronized、Lock)

    1.synchronized的生产者和消费者 synchronized是锁住对象 this.wait()释放了锁 并等待 this.notify()随机通知并唤醒同一个对象中的一个线程 this.no ...

  10. idea添加汉化包之后出现的一些问题 解决方案

    先把原链接放在这:https://jingyan.baidu.com/article/fb48e8bef2bcb66e622e14d2.html 关掉idea之后,一定要记住是在压缩软件中打开,而不是 ...