java分布式简单实现

案例:文章推荐

论坛进入文章页面后,显示一个推荐列表:看过这篇文章的人还看过哪些文章,包含列为文章article、点击数count。

可能有很好很简单的解决办法,但是到最后再讲。

传统的方法是:建一张表,字段有article和user。每点击一次,增加一条记录。一个大论坛几天之内记录数就能达到千万条。而没有必要建索引,其他优化的办法,我还想不到,这样的查询别提多慢了。

传统数据库解决不了,那么分布式就该上场了。如果功能特别简单,完全可以不去使用MAPREDUCE和Hbase,自己动手搞一个吧。


这里最简单的实现:数据保存在txt文件,用Java IO读写,for循环扫描全表进行筛选,现成的Collections排序。

sql:

[sql] view plain copy
  1. SELECT T1.ARTICLE,COUNT(*) C    
  2.     FROM ATB2 T1 INNER JOIN (SELECT T.USER FROM ATB2 T WHERE T.ARTICLE=888) T2  
  3.     WHERE T1.USER=T2.USER  
  4.     AND T1.ARTICLE!=888  
  5.     GROUP BY T1.ARTICLE  
  6.     ORDER BY C DESC  
  7.     LIMIT 10;  

先查看过文章的用户列表,再查这些用户看过的文章列表,聚合,排序

[java] view plain copy
  1. package com.src.reader;  
  2.   
  3. import java.io.BufferedReader;  
  4. import java.io.File;  
  5. import java.io.FileReader;  
  6. import java.util.ArrayList;  
  7. import java.util.Collections;  
  8. import java.util.Comparator;  
  9. import java.util.HashMap;  
  10. import java.util.HashSet;  
  11. import java.util.Iterator;  
  12. import java.util.List;  
  13. import java.util.Map;  
  14. import java.util.Map.Entry;  
  15. import java.util.Set;  
  16.   
  17. import com.src.entity.ATB;  
  18.   
  19. public class DataReader {  
  20.     public static void main(String[] args) throws Exception{  
  21.         long start = System.currentTimeMillis();  
  22.         select(888);  
  23.         long end = System.currentTimeMillis();  
  24.         System.out.println("Select cost time: "+(end-start)/1000.0+" seconds.");  
  25.     }  
  26.     public static void select(int article) throws Exception{  
  27.         //读文件到字符串  
  28.         File file = new File("d://b.txt");  
  29.         String str = reader1(file);  
  30.         //字符串切割为数组  
  31.         ATB[] all = converStr2Array1(str);  
  32.         System.out.println("数组长度为"+all.length);  
  33.         //查看过文章的用户列表,去重  
  34.         Set<Integer> users = getUsersByArticle(article, all);  
  35.         //遍历用户列表,查每个用户看过的文章,去掉参数文章(:先遍历全表再遍历用户列表)  
  36.         List<Integer> articles = getArticlesByUsers(all, users, article);  
  37.         //以文章分类,查每篇文章的总数  
  38.         Map<Integer,Integer> map = groupBy(articles);  
  39.         //排序  
  40. //      List<String> result = orderAll(map);  
  41.         List<String> result = limitAndOrder(map, 10);  
  42. //      System.out.println(result);  
  43.     }  
  44.     public static String reader1(File file) throws Exception{  
  45.         long start = System.currentTimeMillis();  
  46.         BufferedReader br = new BufferedReader(new FileReader(file));  
  47.         StringBuffer sb = new StringBuffer();  
  48.         while(br.ready()){  
  49.             sb.append(br.readLine());  
  50.         }  
  51.         br.close();  
  52.         long end = System.currentTimeMillis();  
  53.         System.out.println("读文件完成,用时"+(end-start)/1000.0+"秒。");  
  54.         return sb.toString();  
  55.     }  
  56.       
  57.     public static ATB[] converStr2Array1(String str){  
  58.         long start = System.currentTimeMillis();  
  59.         String[] arr = str.split(";");  
  60.         System.out.println("字符串切割用时"+(System.currentTimeMillis()-start)/1000.0+"秒。");  
  61.         ATB[] all = new ATB[arr.length];  
  62.         for(int i=0;i<arr.length;i++){  
  63.             int article = Integer.parseInt(arr[i].split(",")[0]);  
  64.             int user = Integer.parseInt(arr[i].split(",")[1]);  
  65.             all[i] = new ATB(article,user);  
  66.         }  
  67.         long end = System.currentTimeMillis();  
  68.         System.out.println("字符串转换为数组完成,用时"+(end-start)/1000.0+"秒。");  
  69.         return all;  
  70.     }  
  71.       
  72.     public static Set<Integer> getUsersByArticle(int article,ATB[] all){  
  73.         long start = System.currentTimeMillis();  
  74.         Set<Integer> set = new HashSet<Integer>();  
  75.         for(ATB a:all){  
  76.             if(a.getArticle()==article){  
  77.                 set.add(a.getUser());  
  78.             }  
  79.         }  
  80.         long end = System.currentTimeMillis();  
  81.         System.out.println("查询user列表完成,用时"+(end-start)/1000.0+"秒。");  
  82.         return set;  
  83.     }  
  84.       
  85.     public static List<Integer> getArticlesByUsers(ATB[] all,Set<Integer> users,int article){  
  86.         long start = System.currentTimeMillis();  
  87.         List<Integer> list = new ArrayList<Integer>();  
  88.         for(ATB a:all){  
  89.             if(article!=a.getArticle()&&users.contains(a.getUser())){  
  90.                 list.add(a.getArticle());  
  91.             }  
  92.         }  
  93.         long end = System.currentTimeMillis();  
  94.         System.out.println("由user列表查询article列表完成,用时"+(end-start)/1000.0+"秒。");  
  95.         return list;  
  96.     }  
  97.   
  98.     public static Map<Integer, Integer> groupBy(List<Integer> list){  
  99.         long start = System.currentTimeMillis();  
  100.         Map<Integer,Integer> map = new HashMap<Integer, Integer>();  
  101.         for(Integer i:list){  
  102.             if(map.containsKey(i)){  
  103.                 map.put(i, map.get(i)+1);  
  104.             }else{  
  105.                 map.put(i, 1);  
  106.             }  
  107.         }  
  108.         long end = System.currentTimeMillis();  
  109.         System.out.println("group 完成,用时"+(end-start)/1000.0+"秒。");  
  110.         return map;  
  111.     }  
  112.       
  113.     public static List<String> limitAndOrder(Map<Integer,Integer> map,int limit){  
  114.         //排序办法:把value排序,取限制条数,去重,遍历map,由value取key  
  115.         long start = System.currentTimeMillis();  
  116.         List<String> result = new ArrayList<String>();  
  117.         List<Integer> values = new ArrayList<Integer>(map.values());  
  118.         Collections.sort(values,new Comparator<Integer>() {  
  119.             public int compare(Integer i,Integer j){  
  120.                 return (j - i);  
  121.             }  
  122.         });  
  123.         long end = System.currentTimeMillis();  
  124.         System.out.println("value排序完成,用时"+(end-start)/1000.0+"秒。");  
  125.         values = values.subList(0, limit);  
  126.         //去重省略  
  127.         Iterator<Entry<Integer, Integer>> itr = map.entrySet().iterator();  
  128.         while(itr.hasNext()){  
  129.             Map.Entry<Integer, Integer> entry = (Entry<Integer, Integer>) itr.next();  
  130.             int article = entry.getKey();  
  131.             int count = entry.getValue();  
  132.             if(values.contains(count)){  
  133.                 String str = leftFillWith0(String.valueOf(count)) + "," + String.valueOf(article);  
  134.                 result.add(str);  
  135.                 //由value查到的key可能有多个,一种办法是在添加前判断到达长度限制时删除result列表中count最小的行  
  136.                 //或者再次排序和取限  
  137.             }  
  138.         }  
  139.         //再次排序和取限  
  140.         Collections.sort(result, new Comparator<String>() {  
  141.             public int compare(String str1,String str2){  
  142.                 return - str1.compareTo(str2);  
  143.             }  
  144.         });  
  145.         result = result.subList(0, limit);  
  146.         long end2 = System.currentTimeMillis();  
  147.         System.out.println("排序和取限完成,总共用时"+(end2-start)/1000.0+"秒。");  
  148.         return result;  
  149.     }  
  150.     public static List<String> orderAll(Map<Integer,Integer> map){  
  151.         //排序办法分两种:1把value排序,由value取key;2重组字符串  
  152.         long start = System.currentTimeMillis();  
  153.         List<String> result = new ArrayList<String>();  
  154.         Iterator<Entry<Integer, Integer>> itr = map.entrySet().iterator();  
  155.         while(itr.hasNext()){  
  156.             Map.Entry<Integer, Integer> entry = (Entry<Integer, Integer>) itr.next();  
  157.             int article = entry.getKey();  
  158.             int count = entry.getValue();  
  159.             String str = leftFillWith0(String.valueOf(count)) + "," + String.valueOf(article);  
  160.             result.add(str);  
  161.         }  
  162.         Collections.sort(result, new Comparator<String>() {  
  163.             public int compare(String str1,String str2){  
  164.                 return - str1.compareTo(str2);  
  165.             }  
  166.         });  
  167.         long end = System.currentTimeMillis();  
  168.         System.out.println("排序完成,用时"+(end-start)/1000.0+"秒。");  
  169.         return result;  
  170.     }  
  171.     public static String leftFillWith0(String str){  
  172.         int length = 8;  
  173.         String s = "";  
  174.         for(int i=0;i<length-str.length();i++){  
  175.             s = s + "0";  
  176.         }  
  177.         return s + str;  
  178.     }  
  179. }  
  180. //读文件完成,用时0.253秒。  
  181. //字符串转换为数组完成,用时4.054秒。  
  182. //数组长度为543243  
  183. //查询user列表完成,用时0.0080秒。  
  184. //由user列表查询article列表完成,用时0.085秒。  
  185. //group 完成,用时0.04秒。  
  186. //排序完成,用时0.069秒。  
  187. //Select cost time: 4.526 seconds.  
  188.   
  189. //读文件完成,用时0.25秒。  
  190. //字符串切割用时0.72秒。  
  191. //字符串转换为数组完成,用时4.634秒。  
  192. //数组长度为543243  
  193. //查询user列表完成,用时0.0090秒。  
  194. //由user列表查询article列表完成,用时0.096秒。  
  195. //group 完成,用时0.036秒。  
  196. //value排序完成,用时0.034秒。  
  197. //排序和取限完成,总共用时0.064秒。  
  198. //Select cost time: 5.113 seconds.  

这段代码很多地方可以优化。

现在用十台主机作为分布式节点NODE,每台开启一个hessian服务器,提供一个处理数据的接口。一台主项目MASTER中调用这十台NODE。可以开10个线程去调用。

假如5000万条记录,新增记录时平均分布到每个节点,这样每台主机有500万数据。然后保存在100个文本文件,每个文件就是5万条记录。

然后再同时开100个甚至更多线程,同时处理这100个文件,把CPU撑到爆。

对于现在这个案例来说,分布式的处理过程是这样的:

MASTER通过hessian发起请求,只有一个参数article,每个节点接下来做的事情一样,最终要得到一个列表,如

[plain] view plain copy
  1. [00000020,9980, 00000020,9731, 00000020,8783, 00000020,8374, 00000018,9908, 00000018,9391, 00000018,8728, 00000018,8725, 00000017,9789, 00000017,9511]  
补0是为方便排序,左边是count右边是article。

这些节点得到的列表可能存在重复,如9980在节点1里面查出来点击了20次,在节点2里面查到点击了19次,这样所以要在MASTER做一个汇总,话说回来,前面一步是MAP,这一步就是REDUCE。

汇总的过程先是merge,得到以article为key,count为value的一个HashMap,然后是排序order by,然后分页。

merge和排序的开销可能又会很大,那还是老办法,再想办法分发到各个节点去做。其中排序我想到的方法,同时做分页的话比较容易,比如取点击量最大的100条,那在每个节点先做排序,取前100条返回到MASTER,然后MASTER给这1000条排序。如果查100-200条,在节点里面全表排序取前面200条,MASTER要排序的有2000条。依次下去假如每个节点总共查出10000条记录,分页在4900-5000的话,每个节点返回给MASTER有5000行,(查9000-10000行可以倒序排列只返回100行),所以这样下去还不是个完美好办法。

无所谓,再开线程,加节点就是了。

一来,在节点之中查最大100条,可以分给多个线程或者节点去做,意思是把10000条记录分成几段,查出每一段的前100条,然后汇总。

二来,在10个节点查出各自的100条之后,不会由MASTER全部处理,而是分成5份每份200条发送到五个节点分别去前100个,然后剩下500条数据,如果数据量大就再加节点。

---------------------------------------------------------------------------------------------------------------------------------------------

(三,查最中间100条的时候,能运用分布式的办法,是先查出之前的所有数据,比如用一个线程查第一个100条,第二个线程查第二个100条,全部查出,最后减去这些数据,剩下就不多了。这个方法确实是分布式,但是笨到家了。

四,全表排序如果要运用分布式,还是可以用上面的方法,100条100条的查出来,拼一下。)

---------------------------------------------------------------------------------------------------------------------------------------------

这样行不通,后来才发现其实分布式排序很简单。

比如MASTER有1000个数字,根据数字大小,分到10个节点,第一个节点保存0-100,依次101-200。然后每个节点查出来可以直接合并,这才是达到分布式的效果。

而首先还要做一个数据分布采样,以保证每个节点分到的数据量平均。采样的过程,也很容易分布化。


扩展:

如果节点数据保存在MySQL而不是文本文件上面,貌似更加方便的很。

节点可以使用内存保存管理数据。

数据异常与备份。异常的处理。


最后,这个案例还有一个办法,数据表设置两个字段,article为唯一主键,第二个列记录所有user的点击数。如果嫌这个字符串太大,那就放到文件里用Java IO读吧。

这样article和user都是唯一的。可以建索引。

如果用java做,那就保存在一个HashMap,article作为key,value也是一个HashMap,记录user和count。

这种实现,估计是最理想的。

所以,能用数据库和java做好的,就不要搞分布式了。尽量还是要用传统的方法。

展开阅读全文

没有更多推荐了,返回首页