案例:文章推荐

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

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

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

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


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

sql:

SELECT T1.ARTICLE,COUNT(*) C  
	FROM ATB2 T1 INNER JOIN (SELECT T.USER FROM ATB2 T WHERE T.ARTICLE=888) T2
	WHERE T1.USER=T2.USER
	AND T1.ARTICLE!=888
	GROUP BY T1.ARTICLE
	ORDER BY C DESC
	LIMIT 10;



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


package com.src.reader;

import java.io.BufferedReader;
import java.io.File;
import java.io.FileReader;
import java.util.ArrayList;
import java.util.Collections;
import java.util.Comparator;
import java.util.HashMap;
import java.util.HashSet;
import java.util.Iterator;
import java.util.List;
import java.util.Map;
import java.util.Map.Entry;
import java.util.Set;

import com.src.entity.ATB;

public class DataReader {
	public static void main(String[] args) throws Exception{
		long start = System.currentTimeMillis();
		select(888);
		long end = System.currentTimeMillis();
		System.out.println("Select cost time: "+(end-start)/1000.0+" seconds.");
	}
	public static void select(int article) throws Exception{
		//读文件到字符串
		File file = new File("d://b.txt");
		String str = reader1(file);
		//字符串切割为数组
		ATB[] all = converStr2Array1(str);
		System.out.println("数组长度为"+all.length);
		//查看过文章的用户列表,去重
		Set<Integer> users = getUsersByArticle(article, all);
		//遍历用户列表,查每个用户看过的文章,去掉参数文章(:先遍历全表再遍历用户列表)
		List<Integer> articles = getArticlesByUsers(all, users, article);
		//以文章分类,查每篇文章的总数
		Map<Integer,Integer> map = groupBy(articles);
		//排序
//		List<String> result = orderAll(map);
		List<String> result = limitAndOrder(map, 10);
//		System.out.println(result);
	}
	public static String reader1(File file) throws Exception{
		long start = System.currentTimeMillis();
		BufferedReader br = new BufferedReader(new FileReader(file));
		StringBuffer sb = new StringBuffer();
		while(br.ready()){
			sb.append(br.readLine());
		}
		br.close();
		long end = System.currentTimeMillis();
		System.out.println("读文件完成,用时"+(end-start)/1000.0+"秒。");
		return sb.toString();
	}
	
	public static ATB[] converStr2Array1(String str){
		long start = System.currentTimeMillis();
		String[] arr = str.split(";");
		System.out.println("字符串切割用时"+(System.currentTimeMillis()-start)/1000.0+"秒。");
		ATB[] all = new ATB[arr.length];
		for(int i=0;i<arr.length;i++){
			int article = Integer.parseInt(arr[i].split(",")[0]);
			int user = Integer.parseInt(arr[i].split(",")[1]);
			all[i] = new ATB(article,user);
		}
		long end = System.currentTimeMillis();
		System.out.println("字符串转换为数组完成,用时"+(end-start)/1000.0+"秒。");
		return all;
	}
	
	public static Set<Integer> getUsersByArticle(int article,ATB[] all){
		long start = System.currentTimeMillis();
		Set<Integer> set = new HashSet<Integer>();
		for(ATB a:all){
			if(a.getArticle()==article){
				set.add(a.getUser());
			}
		}
		long end = System.currentTimeMillis();
		System.out.println("查询user列表完成,用时"+(end-start)/1000.0+"秒。");
		return set;
	}
	
	public static List<Integer> getArticlesByUsers(ATB[] all,Set<Integer> users,int article){
		long start = System.currentTimeMillis();
		List<Integer> list = new ArrayList<Integer>();
		for(ATB a:all){
			if(article!=a.getArticle()&&users.contains(a.getUser())){
				list.add(a.getArticle());
			}
		}
		long end = System.currentTimeMillis();
		System.out.println("由user列表查询article列表完成,用时"+(end-start)/1000.0+"秒。");
		return list;
	}

	public static Map<Integer, Integer> groupBy(List<Integer> list){
		long start = System.currentTimeMillis();
		Map<Integer,Integer> map = new HashMap<Integer, Integer>();
		for(Integer i:list){
			if(map.containsKey(i)){
				map.put(i, map.get(i)+1);
			}else{
				map.put(i, 1);
			}
		}
		long end = System.currentTimeMillis();
		System.out.println("group 完成,用时"+(end-start)/1000.0+"秒。");
		return map;
	}
	
	public static List<String> limitAndOrder(Map<Integer,Integer> map,int limit){
		//排序办法:把value排序,取限制条数,去重,遍历map,由value取key
		long start = System.currentTimeMillis();
		List<String> result = new ArrayList<String>();
		List<Integer> values = new ArrayList<Integer>(map.values());
		Collections.sort(values,new Comparator<Integer>() {
			public int compare(Integer i,Integer j){
				return (j - i);
			}
		});
		long end = System.currentTimeMillis();
		System.out.println("value排序完成,用时"+(end-start)/1000.0+"秒。");
		values = values.subList(0, limit);
		//去重省略
		Iterator<Entry<Integer, Integer>> itr = map.entrySet().iterator();
		while(itr.hasNext()){
			Map.Entry<Integer, Integer> entry = (Entry<Integer, Integer>) itr.next();
			int article = entry.getKey();
			int count = entry.getValue();
			if(values.contains(count)){
				String str = leftFillWith0(String.valueOf(count)) + "," + String.valueOf(article);
				result.add(str);
				//由value查到的key可能有多个,一种办法是在添加前判断到达长度限制时删除result列表中count最小的行
				//或者再次排序和取限
			}
		}
		//再次排序和取限
		Collections.sort(result, new Comparator<String>() {
			public int compare(String str1,String str2){
				return - str1.compareTo(str2);
			}
		});
		result = result.subList(0, limit);
		long end2 = System.currentTimeMillis();
		System.out.println("排序和取限完成,总共用时"+(end2-start)/1000.0+"秒。");
		return result;
	}
	public static List<String> orderAll(Map<Integer,Integer> map){
		//排序办法分两种:1把value排序,由value取key;2重组字符串
		long start = System.currentTimeMillis();
		List<String> result = new ArrayList<String>();
		Iterator<Entry<Integer, Integer>> itr = map.entrySet().iterator();
		while(itr.hasNext()){
			Map.Entry<Integer, Integer> entry = (Entry<Integer, Integer>) itr.next();
			int article = entry.getKey();
			int count = entry.getValue();
			String str = leftFillWith0(String.valueOf(count)) + "," + String.valueOf(article);
			result.add(str);
		}
		Collections.sort(result, new Comparator<String>() {
			public int compare(String str1,String str2){
				return - str1.compareTo(str2);
			}
		});
		long end = System.currentTimeMillis();
		System.out.println("排序完成,用时"+(end-start)/1000.0+"秒。");
		return result;
	}
	public static String leftFillWith0(String str){
		int length = 8;
		String s = "";
		for(int i=0;i<length-str.length();i++){
			s = s + "0";
		}
		return s + str;
	}
}
//读文件完成,用时0.253秒。
//字符串转换为数组完成,用时4.054秒。
//数组长度为543243
//查询user列表完成,用时0.0080秒。
//由user列表查询article列表完成,用时0.085秒。
//group 完成,用时0.04秒。
//排序完成,用时0.069秒。
//Select cost time: 4.526 seconds.

//读文件完成,用时0.25秒。
//字符串切割用时0.72秒。
//字符串转换为数组完成,用时4.634秒。
//数组长度为543243
//查询user列表完成,用时0.0090秒。
//由user列表查询article列表完成,用时0.096秒。
//group 完成,用时0.036秒。
//value排序完成,用时0.034秒。
//排序和取限完成,总共用时0.064秒。
//Select cost time: 5.113 seconds.



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

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

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

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

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

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

[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做好的,就不要搞分布式了。尽量还是要用传统的方法。