为什么集合写的好好的,突然要来写 Redis 呢 ?
一切的原因都要从今天的需求说起,今天接了一个需求,目标是做一个推广页的 UV (统计访客, 每天只记录同一个访客一次) 统计,因为前段时间刚刚看过老钱写的 《Redis深度历险》 HyperLogLog 篇依旧历历在目,一样是统计 UV 的场景,我就直接提出用 HyperLogLog 去处理这个问题,然后被领导问为什么要用 HyperLogLog 好在哪的时候卡壳了,emmm ... 本来想凭借我这广阔的技术海无形装逼,这特么不尴尬了。
HyperLogLog 与 基数统计
HyperLogLog 是最早由 Flajolet 及其同事在 2007 年提出的一种 估算基数的近似最优算法,具体定义可以参考维基百科:https://en.jinzhao.wiki/wiki/HyperLogLog
所谓基数统计可以理解为用来统计一个集合中不重复的元素,也就是我们今天要讲解 HyperLogLog 的原因
业务场景分析
领导的需求是:统计每个网页的访客数量, 每个用户只记录一次
我们来捋顺一下思路, 可能你会想出如下几种方案:
-
Plan A : 数据库里建张表,没有一个表解决不了的问题,如果有那么就建两张表 .
-
Plan B : 建立一个 Set 集合, 存储当天访问过此页面的所有用户 id .
-
Plan C : bitmap,类似用户签到的解决方案,依据年月做 key , 日期作为偏移量 .
我们来分析一下这几种方案,首先数据库解决是不可能了,甭说做每个月的 UV 统计分析了, 一天的访问量存起来空间就占了太多了; 那么为每个页面设置一个 Set 怎么样呢 ? 依旧是访问量的问题,如果访问量很高 Set 的集合会非常大,集合一大去重的时候就 ... 你懂的, 所以 Set 也不行; 那么 bitmap 呢 ? bitmap 的一个元素可以对应到 bit 数组中的一位,使用 bitmap 确实很节省内存,可是在大数据量的场景下,依旧需要很多内存,并不适用 。
三个计划全部拉闸,怎么办呢?哎,拉闸就对了,不拉闸怎么讲我们今天要说的 HyperLogLog 呢?
概率算法
HyperLogLog 采取概率算法,所谓概率算法就是通过一定的概率统计方法预估基数值,而概率算法并不直接存储数据集合本身,这种方法可以极大的节省内存,并保证误差在一定范围内,HyperLogLog 在标准误差(0.81%)的前提下能够统计 2^64 个数据!
实例
private static String COUNT = "cpp_bank_list_total_size_today"; private static String TOTAL_COUNT = "cpp_bank_list_total_size"; private static String TOTAL_ID = "0";
数据存入:
//获取访问者的ip String ipAdress = IPUtil.getIpAddress(httpServletRequest); log.debug("访问列表页的ip地址为:[{}]", ipAdress); //将ip存入redis HyperLogLogOperations<String, String> hyperlog = redisTemplate.opsForHyperLogLog(); hyperlog.add(COUNT, ipAdress);
数据读取:
public void saveUserAccessLog() { HyperLogLogOperations<String, String> hyperlog = redisTemplate.opsForHyperLogLog(); int count = hyperlog.size(COUNT).intValue(); Calendar calendar = Calendar.getInstance(); calendar.add(Calendar.DAY_OF_MONTH, -1); CppBankAccessLog cppBankAccessLog = CppBankAccessLog.builder() .id(Sequence.getInstance().getSequenceNumber()) .time(new SimpleDateFormat("yyyy-MM-dd").format(calendar.getTime())) .count(count) .build(); cppBankAccessLogMapper.insertSelective(cppBankAccessLog); //合并每天的访问量到总计中 hyperlog.union(TOTAL_COUNT, COUNT); int totalCount = hyperlog.size(TOTAL_COUNT).intValue(); cppBankAccessLogMapper.updateCountById(TOTAL_ID, totalCount); log.info("日活量信息入库,昨日数据:[{}],总数据:[{}]", count, totalCount); //删除today中的数据 hyperlog.delete(COUNT); }