案例:文章推荐
论坛进入文章页面后,显示一个推荐列表:看过这篇文章的人还看过哪些文章,包含列为文章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 users = getUsersByArticle(article, all);
//遍历用户列表,查每个用户看过的文章,去掉参数文章(:先遍历全表再遍历用户列表)
List articles = getArticlesByUsers(all, users, article);
//以文章分类,查每篇文章的总数
Map map = groupBy(articles);
//排序
// List result = orderAll(map);
List 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 getUsersByArticle(int article,ATB[] all){
long start = System.currentTimeMillis();
Set set = new HashSet();
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 getArticlesByUsers(ATB[] all,Set users,int article){
long start = System.currentTimeMillis();
List list = new ArrayList();
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 groupBy(List list){
long start = System.currentTimeMillis();
Map map = new HashMap();
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 limitAndOrder(Map map,int limit){
//排序办法:把value排序,取限制条数,去重,遍历map,由value取key
long start = System.currentTimeMillis();
List result = new ArrayList();
List values = new ArrayList(map.values());
Collections.sort(values,new Comparator() {
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> itr = map.entrySet().iterator();
while(itr.hasNext()){
Map.Entry entry = (Entry) 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() {
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 orderAll(Map map){
//排序办法分两种:1把value排序,由value取key;2重组字符串
long start = System.currentTimeMillis();
List result = new ArrayList();
Iterator> itr = map.entrySet().iterator();
while(itr.hasNext()){
Map.Entry entry = (Entry) 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() {
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
现在用十台主机作为分布式节点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做好的,就不要搞分布式了。尽量还是要用传统的方法。