Java使用极小的内存完成对超大数据的去重计数,用于实时计算中统计UV

一直在想如何在实时计算中完成对海量数据去重计数的功能,即SELECT COUNT(DISTINCT) 的功能。比如:从每天零点开始,实时计算全站累计用户数(UV),以及某些组合维度上的用户数,这里的用户假设以Cookieid来计。

想想一般的解决办法,在内存中使用HaspMap、HashSet?或者是在Redis中以Cookieid为key?感觉都不合适,在数以亿计用户的业务场景下,内存显然也成了瓶颈。

如果说,实时计算的业务场景中,对UV的计算精度并不要求100%(比如:实时的监测某一网站的PV和UV),那么可以考虑采用基数估计算法来统计。这里有一个Java的实现版本 stream-lib:https://github.com/addthis/stream-lib

采用基数估计算法目的就是为了使用很小的内存,即可完成超大数据的去重计数。号称是只使用几KB的内存,就可以完成对数以条数据的去重计数。但基数估计算法都不是100%精确的,误差在0~2%之间,一般是1%左右。

本文使用stream-lib来尝试对两个数据集进行去重计数。相关的文档和下载见文章最后。

测试数据集1:

  • 文件名:small_cookies.txt
  • 文件内容:每个cookieid一行
  • 文件总记录数:14892708
  • 去重记录数:3896911
  • 文件总大小:350153062(约334M)
 
  1. [liuxiaowen@dev site_raw_log]$ head -5 small_cookies.txt
  2. 7EDCF13A03D387548FB2B8
  3. da5f0196-56036078075b9f-14892137
  4. 1D0A83B604ADD4558970EE
  5. 3DF76E7100025F553B1980
  6. 72C756700C3CAA56035EE0
  7. [liuxiaowen@dev site_raw_log]$ wc -l small_cookies.txt
  8. 14892708 small_cookies.txt
  9. [liuxiaowen@dev site_raw_log]$ awk '!a[$0]++{print $0}' small_cookies.txt | wc -l
  10. 3896911
  11. [liuxiaowen@dev site_raw_log]$ ll small_cookies.txt
  12. -rw-rw-r--. 1 liuxiaowen liuxiaowen 350153062 Sep 25 10:50 small_cookies.txt

测试数据集2:

  • 文件名:big_cookies.txt
  • 文件内容:每个cookieid一行
  • 文件总记录数:547631464
  • 去重记录数:190264959
  • 文件总大小:12610638153(约11.8GB)
 
  1. --总记录数
  2. spark-sql> select count(1) from big_cookies;
  3. 547631464
  4. Time taken: 7.311 seconds, Fetched 1 row(s)
  5. --去重记录数
  6. spark-sql> select count(1) from (select cookieid from big_cookies group by cookieid) x;
  7. 190264959
  8. Time taken: 80.516 seconds, Fetched 1 row(s)
  9.  
  10. hadoop fs -getmerge /hivedata/warehouse/liuxiaowen.db/big_cookies/* big_cookies.txt
  11.  
  12. [liuxiaowen@dev site_raw_log]$ wc -l big_cookies.txt
  13. 547631464 cookies.txt
  14. //总大小
  15. [liuxiaowen@dev site_raw_log]$ ll big_cookies.txt
  16. -rw-r--r--. 1 liuxiaowen liuxiaowen 12610638153 Sep 25 13:25 big_cookies.txt
  17.  

普通方法测试

所谓普通方法,就是遍历文件,将所有cookieid放到内存的HashSet中,而HashSet的size就是去重记录数。

代码如下:

 
  1. package com.lxw1234.streamlib;
  2.  
  3. import java.io.BufferedReader;
  4. import java.io.File;
  5. import java.io.FileReader;
  6. import java.io.IOException;
  7. import java.util.HashSet;
  8. import java.util.Set;
  9.  
  10. public class Test {
  11. public static void main(String[] args) {
  12. Runtime rt = Runtime.getRuntime();
  13. Set set = new HashSet();
  14. File file = new File(args[0]);
  15. BufferedReader reader = null;
  16. long count = 0l;
  17. try {
  18. reader = new BufferedReader(new FileReader(file));
  19. String tempString = null;
  20. while ((tempString = reader.readLine()) != null) {
  21. count++;
  22. set.add(tempString);
  23. if(set.size() % 5000 == 0) {
  24. System.out.println("Total count:[" + count + "] Unique count:[" + set.size() + "] FreeMemory:[" + rt.freeMemory() + "] ..");
  25. }
  26. }
  27. reader.close();
  28. } catch (Exception e) {
  29. e.printStackTrace();
  30. } finally {
  31. if (reader != null) {
  32. try {
  33. reader.close();
  34. } catch (IOException e1) {}
  35. }
  36. }
  37. System.out.println("Total count:[" + count + "] Unique count:[" + set.size() + "] FreeMemory:[" + rt.freeMemory() + "] ..");
  38. }
  39. }
  40.  

指定使用10M的内存运行,命令为:

 
  1. java -cp /tmp/teststreamlib.jar -Xms10M -Xmx10M -XX:PermSize=10M -XX:MaxPermSize=10M \
  2. com.lxw1234.streamlib.Test /home/liuxiaowen/site_raw_log/small_cookies.txt

运行结果如下:

streamlib

10M的内存,仅仅够存65000左右的cookieid,之后就报错,内存不够了。大数据集更不用说。

基数估计方法测试

采用streamlib中的基数估计算法实现ICardinality,对两个结果集的总记录数和去重记录数进行统计,代码如下:

 
  1. package com.lxw1234.streamlib;
  2.  
  3. import java.io.BufferedReader;
  4. import java.io.File;
  5. import java.io.FileReader;
  6. import java.io.IOException;
  7.  
  8. import com.clearspring.analytics.stream.cardinality.AdaptiveCounting;
  9. import com.clearspring.analytics.stream.cardinality.ICardinality;
  10.  
  11. public class TestCardinality {
  12.  
  13. public static void main(String[] args) {
  14. Runtime rt = Runtime.getRuntime();
  15. long start = System.currentTimeMillis();
  16. long updateRate = 1000000l;
  17. long count = 0l;
  18. ICardinality card = AdaptiveCounting.Builder.obyCount(Integer.MAX_VALUE).build();
  19. File file = new File(args[0]);
  20. BufferedReader reader = null;
  21. try {
  22. reader = new BufferedReader(new FileReader(file));
  23. String tempString = null;
  24. while ((tempString = reader.readLine()) != null) {
  25. card.offer(tempString);
  26. count++;
  27. if (updateRate > 0 && count % updateRate == 0) {
  28. System.out.println("Total count:[" + count + "] Unique count:[" + card.cardinality() + "] FreeMemory:[" + rt.freeMemory() + "] ..");
  29. }
  30. }
  31. reader.close();
  32. } catch (Exception e) {
  33. e.printStackTrace();
  34. } finally {
  35. if (reader != null) {
  36. try {
  37. reader.close();
  38. } catch (IOException e1) {}
  39. }
  40. }
  41. long end = System.currentTimeMillis();
  42. System.out.println("Total count:[" + count + "] Unique count:[" + card.cardinality() + "] FreeMemory:[" + rt.freeMemory() + "] ..");
  43. System.out.println("Total cost:[" + (end - start) + "] ms ..");
  44. }
  45. }
  46.  
  • 测试数据集1

指定使用10M的内存运行,测试数据集1命令为:

 
  1. java -cp /tmp/stream-2.9.1-SNAPSHOT.jar:/tmp/teststreamlib.jar -Xms10M -Xmx10M -XX:PermSize=10M -XX:MaxPermSize=10M \
  2. com.lxw1234.streamlib.TestCardinality /home/liuxiaowen/site_raw_log/small_cookies.txt

运行结果如下:

streamlib

  • 测试数据集2

同样指定使用10M的内存运行,测试数据集2命令为:

 
  1. java -cp /tmp/stream-2.9.1-SNAPSHOT.jar:/tmp/teststreamlib.jar -Xms10M -Xmx10M -XX:PermSize=10M -XX:MaxPermSize=10M \
  2. com.lxw1234.streamlib.TestCardinality /home/liuxiaowen/site_raw_log/big_cookies.txt

运行结果为:

streamlib

测试结果

streamlib

 

测试结果来看,基数估计算法统计的结果中误差的确是0~2%,如果可以接受这个误差,那么这个方案完全可以用于实时计算中的不同维度UV统计中。

从运行结果图上可以看到,虽然指定了10M内存,但空闲内存(FreeMemory)一直在差不多7M以上,也就是说,5.4亿的数据去重计数,也仅仅使用了3M左右的内存。

相关下载

以上程序需要依赖stream-2.9.1-SNAPSHOT.jar,我编译好了一份,

点击下载stream-2.9.1-SNAPSHOT.jar

你也可以从官网中下载源码,编译。

相关文章:

http://blog.youkuaiyun.com/hguisu/article/details/8433731

http://m.oschina.net/blog/315457

程序 = 数据结构 + 算法  程序是为了解决实际问题而存在的。然而为了解决问题,必定会使用到某些数据结构以及设计一个解决这种数据结构的算法。如果说各种编程语言是程序员的招式,那么数据结构和算法就相当于程序员的内功。编程实战算法,不是念PPT,我们讲的就是实战与代码实现与企业应用。程序 = 数据结构 + 算法                ——图灵奖得主,计算机科学家N.Wirth(沃斯)作为程序员,我们做机器学习也好,做python开发也好,java开发也好。有一种对所有程序员无一例外的刚需 —— 算法与数据结构日常增删改查 + 粘贴复制 + 搜索引擎可以实现很多东西。同样,这样也是没有任何竞争力的。我们只可以粘贴复制相似度极高的功能,稍复杂的逻辑没有任何办法。语言有很多,开发框架更是日新月异3个月不学就落后我们可以学习很多语言,很多框架,但招聘不会考你用5种语言10种框架实现同一个功能。真正让程序员有区分度,企业招聘年不变的点 —— 算法与数据结构。算法代表程序员水平的珠穆朗玛。 本视频由微软全球最有价值专家尹成录制,拒绝念PPT,代码实战数据结构与算法导论。除了传统数据结构算法,加入高并发线程安全数据结构,分布式负载均衡算法,分布式哈希表,分布式排序等等现代算法。  算法,晦涩难懂,却又是IT领域受视的素养之一。可以说,算法能力往往决定了一个程序员能够走多远。因此,BAT/FLAG等国内外各大名企非常喜欢在面试环节考核求职者的算法编程,这也成为了无数准程序员们过不的一道“坎”。如何入门并成为一名出色的算法工程师?但无论半路出家还是科班出身,除学生时代搞算法竞赛的同学外真正用心学习过算法与数据结构太少太少。对于后期想要学习算法与数据结构却不得不面对以下问题:没有自己的知识框架,无法关联知识点,学习效率低有疑问而无人解答,有问题无法理解全靠猜测,一个问题卡好几天市面上资料题解质量参差不齐,正确性未可知Google算法-工程师尹成大哥学习算法。
评论 1
成就一亿技术人!
拼手气红包6.0元
还能输入1000个字符
 
红包 添加红包
表情包 插入表情
 条评论被折叠 查看
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值