在开始这一节之前,我们先思考一个常见的业务问题:如果你负责开发维护一个大型的网站,有一天老板找产品经理要网站每个网页每天的 UV 数据,然后让你来开发这个统计模块,你会如何实现?
如果统计 PV 那非常好办,给每个网页一个独立的 Redis 计数器就可以了,这个计数器的 key 后缀加上当天的日期。这样来一个请求,incrby 一次,最终就可以统计出所有的 PV 数据。
但是 UV 不一样,它要去重,同一个用户一天之内的多次访问请求只能计数一次。这就要求每一个网页请求都需要带上用户的 ID,无论是登陆用户还是未登陆用户都需要一个唯一ID 来标识。
你也许已经想到了一个简单的方案,那就是为每一个页面一个独立的 set 集合
来存储所有当天访问过此页面的用户 ID。但是,如果你的页面访问量非常大,比如一个爆款页面几千万的 UV,你需要一个很大的 set 集合来统计,这就非常浪费空间。如果这样的页面很多,那所需要的存储空间是惊人的。为这样一个去重功能就耗费这样多的存储空间,值得么?其实老板需要的数据又不需要太精确,105w 和 106w 这两个数字对于老板们来说并没有多大区别,So,有没有更好的解决方案呢?
这就是本节要引入的一个解决方案,Redis 提供了 HyperLogLog
数据结构就是用来解决这种统计问题的。HyperLogLog 提供不精确的去重计数方案
,虽然不精确但是也不是非常不精确,标准误差是 0.81%,这样的精确度已经可以满足上面的 UV 统计需求了。
/**
* @Auther: majx2
* @Date: 2019-3-22 10:08
* @Description:
*/
public class HyperLogLogTest {
private static final String CODEHOLE = "codehole";
private static final int EXPECT_VALUE = 1000;
Jedis jedis = RedisDS.create().getJedis();
@Test
public void testHyperLogLog(){
for (int i = 0; i < EXPECT_VALUE; i++) {
jedis.pfadd(CODEHOLE+1, "user" + i);
}
System.out.printf("真实值:%d ,期待值:%d\n", jedis.pfcount(CODEHOLE+1), EXPECT_VALUE);
for (int i = 0; i < EXPECT_VALUE; i++) {
jedis.pfadd(CODEHOLE+2, "user" + i);
}
jedis.pfmerge(CODEHOLE,CODEHOLE+1,CODEHOLE+2);
System.out.printf("真实值:%d ,期待值:%d\n", jedis.pfcount(CODEHOLE), EXPECT_VALUE);
jedis.del(CODEHOLE,CODEHOLE+1,CODEHOLE+2);
}
}
返回值:
真实值:1011 ,期待值:1000
真实值:1011 ,期待值:1000
这个方法使用还是要谨慎。经测试虽然已经实现了除重,但是还会出现真实值比期待值大的情况。当然如果是计算UV,结果大一点也是可以的。
本文基于《Redis深度历险:核心原理和应用实践》一文的JAVA实践。更多文章请参考:高性能缓存中间件Redis应用实战(JAVA)
网友评论