前往小程序,Get更优阅读体验!
立即前往
首页
学习
活动
专区
工具
TVP
发布
社区首页 >专栏 >基于 Redis HyperLogLog 实现用户 UV 统计功能

基于 Redis HyperLogLog 实现用户 UV 统计功能

作者头像
学院君
发布2021-01-22 11:00:33
1.1K0
发布2021-01-22 11:00:33
举报
文章被收录于专栏:学院君的专栏学院君的专栏

引子

通过 Redis 实现全站访问计数器中,学院君已经给大家演示了统计用户 PV 的实现思路,今天我们来看看如何实现用户 UV 的统计。

统计用户 UV 和统计用户 PV 不同,不能只对统计指标对应的键值做简单的自增操作,还要对来自同一用户的浏览做去重操作,比如张三今天浏览了学院君网站首页 10 次,那么对应的 PV 需要累加 10,而 UV 只能算 1 个。

通过 SET 结构实现 UV 统计

基于去重功能,很多同学可能会联想到可以通过 Redis 的 SET 结构实现用户 UV 统计 —— 将统计指标+时间后缀作为键名,然后每当有用户访问时,将对应的用户标识通过 SADD 指令存储到这个 SET 结构即可,由于 SET 结构会自动帮我们去重,所以通过 SCARD 指令就能获取到用户 UV 了:

-w773

这么实现功能上是 OK 的,对于小型站点也没什么问题,但是如果放到大型站点就不合适了,比如我们要统计上百万不同页面的用户 UV,用户量级在千万级甚至亿级,为了维护每个页面用户 UV 统计的 SET 结构,需要耗费大量的 Redis 存储空间,对于爆款页面,所要维护的 SET 结构更是巨大,只是为了实现一个去重功能而已,有必要花费这么大的代价吗?有没有更好的解决方案呢?

SET vs HyperLogLog

对于这种访问量特别大的 UV 统计,其实是可以接收一定误差的,比如一百万的访问量,误差在几百上下完全没问题,因此我们可以使用 Redis 提供的高级数据结构 HyperLogLog 来实现这样的 UV 统计功能。

HyperLogLog 这个数据结构会占用固定的存储空间(12KB),同时存在一定误差(不超过 0.81%),因此对于统计标的在几百几千访问量的小型应用不太适合,这种情况下,使用 SET 数据结构实现就可以了,因为也不会占用多少存储空间,而且较小的访问量对精确性要求更高,SET 肯定是不存在误差的,但是对于统计标的在百万千万级的大型应用,使用 HyperLogLog 的优势就显现出来了,与 SET 相比占用的存储空间就不值一提了,而且误差相较于最终的统计体量而言也几乎可以忽略。

HyperLogLog 的基本使用

HyperLogLog 指令

HyperLogLog 只提供了三个指令:

PFADD 用于往 HyperLogLog 中添加元素,PFCOUNT 用于统计 HyperLogLog 中的元素数量,PFMERGE 用于合并不同 HyperLogLog 的统计结果。和 SET 一样,HyperLogLog 也是会去重的:

模拟 UV 统计功能实现

对于数据量很小的统计,不存在误差,如果试图统计更多数据,比如万级,就会存在误差了,下面我们通过一个 Artisan 命令来模拟:

代码语言:javascript
复制
sail artisan make:command HyperLogLogDemo

然后编写这个命令类代码如下:

代码语言:javascript
复制
<?php

namespace App\Console\Commands;

use Illuminate\Console\Command;
use Illuminate\Support\Facades\Redis;

class HyperLogLogDemo extends Command
{
    /**
     * The name and signature of the console command.
     *
     * @var string
     */
    protected $signature = 'uv:demo';

    /**
     * The console command description.
     *
     * @var string
     */
    protected $description = 'UV Statistic Demo With Redis HyperLogLog';

    /**
     * Create a new command instance.
     *
     * @return void
     */
    public function __construct()
    {
        parent::__construct();
    }

    /**
     * Execute the console command.
     *
     * @return int
     */
    public function handle()
    {
        $key = 'site.uv.pf.20201225';
        Redis::pipeline(function ($pipe) use ($key) {
            for ($i = 0; $i < 10000; $i++) {
                $pipe->pfAdd($key, ['user.' . $i]);
            }
        });
        $headers = ['Real UV', 'Statistic UV'];
        $this->table($headers, [[10000, Redis::pfCount($key)]]);
    }
}

这里为了加速 Redis 指令执行,我们使用了管道命令,当然,你可以可以通过数组一次性将所有统计数据添加到 HyperLogLog:

代码语言:javascript
复制
$users = [];
for ($i = 0; $i < 10000; $i++) {
    $users[] = 'user.' . $i;
}
Redis::pfAdd($key, $users);

不过前一种更符合实际的场景。运行 uv:demo 命令,结果如下:

这里我们使用了 Artisan 命令的表格输出格式让结果显示更直观一些。

可以看到,实际 UV 是 10000,通过 HyperLogLog 统计 UV 是 9973,误差率是 0.27%。

如果你再次运行这个命令,结果还是一样,说明 HyperLogLog 具备去重功能,完全可以胜任大访问量的用户 UV 统计工作。

实现全站 UV 统计中间件

接下来,和 Laravel 全站 PV 统计功能一样,我们基于 HyperLogLog 来实现一个全站 UV 统计中间件。

通过如下 Artisan 命令创建一个全局中间件:

代码语言:javascript
复制
sail artisan make:middleware SiteUV

然后编写生成的中间件类实现代码如下:

代码语言:javascript
复制
<?php

namespace App\Http\Middleware;

use Closure;
use Illuminate\Http\Request;
use Illuminate\Support\Facades\Auth;
use Illuminate\Support\Facades\Redis;

class SiteUV
{
    /**
     * Handle an incoming request.
     *
     * @param  \Illuminate\Http\Request  $request
     * @param  \Closure  $next
     * @return mixed
     */
    public function handle(Request $request, Closure $next)
    {
        // 按天为维度统计站点 UV
        $key = config('app.name') . '.uv.' . date('Ymd');
        // 根据用户是否登录获取用户唯一标识
        if (Auth::check()) {
            $userIdentifier = Auth::user()->getAuthIdentifier();
        } else {
            $userIdentifier = $request->getClientIp();
        }
        Redis::pfAdd($key, [$userIdentifier]);
        return $next($request);
    }
}

App\Http\Kernel$middleware 属性数组中添加这个中间件,将其作为全局中间件应用到所有的路由访问:

代码语言:javascript
复制
protected $middleware = [
    ...
    \App\Http\Middleware\SiteVisits::class,
    \App\Http\Middleware\SiteUV::class,
];

访问 http://redis.test 的任意路由,可以看到对于同一个用户/客户端,PV 和 UV 的结果是不一样的:

注:由于我们前面在广播教程中取消了 Redis 键名前缀 laravel_database_,所以这里都不需要添加这个前缀就可以访问对应的键值了。

PV 会不断累加,而 UV 始终是 1。

本系列教程首发在Laravel学院(laravelacademy.org)

本文参与 腾讯云自媒体分享计划,分享自微信公众号。
原始发表:2021-01-13,如有侵权请联系 cloudcommunity@tencent.com 删除

本文分享自 极客书房 微信公众号,前往查看

如有侵权,请联系 cloudcommunity@tencent.com 删除。

本文参与 腾讯云自媒体分享计划  ,欢迎热爱写作的你一起参与!

评论
登录后参与评论
0 条评论
热度
最新
推荐阅读
目录
  • 引子
    • 通过 SET 结构实现 UV 统计
      • SET vs HyperLogLog
      • HyperLogLog 的基本使用
        • HyperLogLog 指令
          • 模拟 UV 统计功能实现
          • 实现全站 UV 统计中间件
          相关产品与服务
          云数据库 Redis
          腾讯云数据库 Redis(TencentDB for Redis)是腾讯云打造的兼容 Redis 协议的缓存和存储服务。丰富的数据结构能帮助您完成不同类型的业务场景开发。支持主从热备,提供自动容灾切换、数据备份、故障迁移、实例监控、在线扩容、数据回档等全套的数据库服务。
          领券
          问题归档专栏文章快讯文章归档关键词归档开发者手册归档开发者手册 Section 归档