redis-HyperLogLog

作者: 要不再等等 | 来源:发表于2019-10-11 20:30 被阅读0次

    如果你负责开发维护一个大型的网站,有一天老板找产品经理要网站每个网页每天的 UV 数据,然后让你来开发这个统计模块,你会如何实现?

    如果统计 PV 那非常好办,给每个网页一个独立的 Redis 计数器就可以了,这个计数器的 key 后缀加上当天的日期。这样来一个请求,incrby 一次,最终就可以统计出所有的 PV 数据。

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

    你也许已经想到了一个简单的方案,那就是为每一个页面一个独立的 set 集合来存储所有当天访问过此页面的用户 ID。当一个请求过来时,我们使用 sadd 将用户 ID 塞进去就可以了。通过 scard 可以取出这个集合的大小,这个数字就是这个页面的 UV 数据。没错,这是一个非常简单的方案。

    但是,如果你的页面访问量非常大,比如一个爆款页面几千万的 UV,你需要一个很大的 set 集合来统计,这就非常浪费空间。如果这样的页面很多,那所需要的存储空间是惊人的。为这样一个去重功能就耗费这样多的存储空间,值得么?其实老板需要的数据又不需要太精确,105w 和 106w 这两个数字对于老板们来说并没有多大区别,So,有没有更好的解决方案呢?

    Redis 提供了 HyperLogLog 数据结构就是用来解决这种统计问题的。HyperLogLog 提供不精确的去重计数方案,虽然不精确但是也不是非常不精确,标准误差是 0.81%,这样的精确度已经可以满足上面的 UV 统计需求了。

    HyperLogLog 数据结构是 Redis 的高级数据结构,它非常有用,但是令人感到意外的是,使用过它的人非常少。

    HyperLogLog 提供了两个指令 pfadd 和 pfcount,根据字面意义很好理解,一个是增加计数,一个是获取计数。pfadd 用法和 set 集合的 sadd 是一样的,来一个用户 ID,就将用户 ID 塞进去就是。pfcount 和 scard 用法是一样的,直接获取计数值。

    127.0.0.1:6379> pfadd codehole user1
    (integer) 1
    127.0.0.1:6379> pfcount codehole
    (integer) 1
    127.0.0.1:6379> pfadd codehole user2
    (integer) 1
    127.0.0.1:6379> pfcount codehole
    (integer) 2
    127.0.0.1:6379> pfadd codehole user3
    (integer) 1
    127.0.0.1:6379> pfcount codehole
    (integer) 3
    127.0.0.1:6379> pfadd codehole user4
    (integer) 1
    127.0.0.1:6379> pfcount codehole
    (integer) 4
    127.0.0.1:6379> pfadd codehole user5
    (integer) 1
    127.0.0.1:6379> pfcount codehole
    (integer) 5
    127.0.0.1:6379> pfadd codehole user6
    (integer) 1
    127.0.0.1:6379> pfcount codehole
    (integer) 6
    127.0.0.1:6379> pfadd codehole user7 user8 user9 user10
    (integer) 1
    127.0.0.1:6379> pfcount codehole
    (integer) 10
    

    简单试了一下,发现还蛮精确的,一个没多也一个没少。接下来我们使用脚本,往里面灌更多的数据,看看它是否还可以继续精确下去,如果不能精确,差距有多大。

    public class PfTest {
      public static void main(String[] args) {
        Jedis jedis = new Jedis();
        for (int i = 0; i < 1000; i++) {
          jedis.pfadd("codehole", "user" + i);
          long total = jedis.pfcount("codehole");
          if (total != i + 1) {
            System.out.printf("%d %d\n", total, i + 1);
            break;
          }
        }
        jedis.close();
      }
    }
    

    当我们加入第 100 个元素时,结果开始出现了不一致。接下来我们将数据增加到 10w 个,看看总量差距有多大。

    public class JedisTest {
      public static void main(String[] args) {
        Jedis jedis = new Jedis();
        for (int i = 0; i < 100000; i++) {
          jedis.pfadd("codehole", "user" + i);
        }
        long total = jedis.pfcount("codehole");
        System.out.printf("%d %d\n", 100000, total);
        jedis.close();
      }
    }
    

    跑了约半分钟,我们看输出:
    100000 99723

    差了 277 个,按百分比是 0.277%,对于上面的 UV 统计需求来说,误差率也不算高。然后我们把上面的脚本再跑一边,也就相当于将数据重复加入一边,查看输出,可以发现,pfcount 的结果没有任何改变,还是 99723,说明它确实具备去重功能。

    HyperLogLog 除了上面的 pfadd 和 pfcount 之外,还提供了第三个指令 pfmerge,用于将多个 pf 计数值累加在一起形成一个新的 pf 值。

    比如在网站中我们有两个内容差不多的页面,运营说需要这两个页面的数据进行合并。其中页面的 UV 访问量也需要合并,那这个时候 pfmerge 就可以派上用场了。

    相关文章

      网友评论

        本文标题:redis-HyperLogLog

        本文链接:https://www.haomeiwen.com/subject/nrixmctx.html