简介
给定一个包含上千万用户的社交网络,我们会实现一个MapReduce、Spark程序,在所有用户对中找出“共同好友”。
令 $$ {U_1,U_2,...,U_n} $$ 为包含一个所有用户列表的集合。我们的目标是为每个$(U_i,U_j)$对$i\ne j$找出共同好友。
我们本章提出3个解决方案:
-
MapReduce/Hadoop解决方案,使用基本数据类型
-
Spark解决方案,使用弹性数据集RDD。
1、共同好友的概念
如今大多数社交网络网站都提供了有关的服务,可以帮助我们与好友共享信息、图片和视频。
有些网站树森之还提供了视频聊天服务,帮助你与好友保持联系。根据定义,“好友”是指你认识、喜欢和信任的一个人。
比如我们QQ好友列表,这个列表上好友关系都是双向的。如果我是你的好友,那么你也是我的好友,注意这个特点,我们的程序中运用了这个特点,将这种关系进行合并求解交集,即可得到A,B的共同好友。
有很多办法可以找到共同好友:
- 使用缓存策略,将共同好友保存在一个缓存中(redis、memcached)
- 使用Mapreduce离线计算,每隔一段时间(例如一天)计算一次每个人的共同好友并存储这些结果;
1、POJO共同好友解决方案
令${A_1,A_2,...A_n}$是$user_1$的好友集合,${B_1,B_2,...,B_n}$是$user_2$的好友集合,那么,$user_1,user_2$的共同好友集合就可以定义为 $$ A \cap B $$ 即两个集合的交集。
POJO的简单实现如下所示:
package com.sunrun.movieshow.algorithm.friend;
import java.util.ArrayList;
import java.util.HashSet;
import java.util.List;
import java.util.Set;
public class POJOFriend {
public static Set<String> intersection(Set<String> A, Set<String> B){
if(A == null || B == null){
return null;
}
if(A.isEmpty() || B. isEmpty()){
return null;
}
Set<String> result = new HashSet<>();
result.addAll(A);
result.retainAll(B);
return result;
}
public static void main(String[] args) {
Set<String> A = new HashSet<>();
Set<String> B = new HashSet<>();
A.add("A");
A.add("B");
A.add("C");
B.add("B");
B.add("C");
B.add("D");
System.out.println(intersection(A,B));
/**
* [B, C]
*/
}
}
2、MapReduce解决方案
映射器接受一个$(k_1,v_1)$,其中$k_1$是一个用户,$v_1$是这个用户的好友列表。
映射器发出一组新的$(k_2,v_2)$,$k_2$是一个$Tuple2(k1,f_i)$,其中$f_i \in v_1$,即会迭代所有的好友列表和$k_1$进行两两组合。
归约器的key是一个用户对,value则是一个好友集合列表。reduce函数得到所有好友集合的交集,从而找出$(u_i,u_j)$对的共同好友。
至于在Mapper过程中的数据传输,会关联到数组类型,我们有两个方案:
1、依然使用文本形式,在Driver节点进行解析;
2、如果你的信息需要解析为非String,例如Long等,可以使用ArrayListOfLongsWritable类。
他是一个实现了Hadoop串行化协议的类,我们不必自己去实现这些接口,可以直接从第三方组件里面抽取使用
<dependency>
<groupId>edu.umd</groupId>
<artifactId>cloud9</artifactId>
<version>1.3.2</version>
</dependency>
该组件包含了丰富的实现了hadoop序列化的协议提供使用,比如PairOfStrings
以及ArrayListOfLongWritable
等。
2.1、Mapper
package com.sunrun.movieshow.algorithm.friend.mapreduce;
import edu.umd.cloud9.io.pair.PairOfStrings;
import org.apache.hadoop.io.LongWritable;
import org.apache.hadoop.io.Text;
import org.apache.hadoop.io.Writable;
import org.apache.hadoop.mapreduce.Mapper;
import java.io.IOException;
import java.util.ArrayList;
import java.util.List;
public class FriendMapper extends Mapper<LongWritable, Text, Text, Text> {
private static final Text KEY = new Text();
private static final Text VALUE = new Text();
// 获取朋友列表
static String getFriends(String[] tokens) {
// 不可能再有共同好友
if (tokens.length == 2) {
return "";
}
StringBuilder builder = new StringBuilder();
for (int i = 1; i < tokens.length; i++) {
builder.append(tokens[i]);
if (i < (tokens.length - 1)) {
builder.append(",");
}
}
return builder.toString();
}
// 使key有序,这里的有序只的是key的两个用户id有序,和整体数据无关
static String buildSortedKey(String user, String friend) {
long p = Long.parseLong(user);
long f = Long.parseLong(friend);
if (p < f) {
return user + "," + friend;
} else {
return friend + "," + user;
}
}
@Override
protected void map(LongWritable key, Text value, Context context) throws IOException, InterruptedException {
String[] tokes = value.toString().split(" ");
// user
String user = tokes[0];
// value
VALUE.set(getFriends(tokes));
// rescue keys
for (int i = 1; i < tokes.length ; i++) {
String otherU = tokes[i];
KEY.set(buildSortedKey(user,otherU));
context.write(KEY,VALUE);
}
}
}
2.2、Reducer
package com.sunrun.movieshow.algorithm.friend.mapreduce;
import org.apache.commons.lang.StringUtils;
import org.apache.hadoop.io.Text;
import org.apache.hadoop.mapreduce.Reducer;
import java.io.IOException;
import java.util.*;
public class FriendReducer extends Reducer<Text, Text,Text, Text> {
static void addFriends(Map<String, Integer> map, String friendsList) {
String[] friends = StringUtils.split(friendsList, ",");
for (String friend : friends) {
Integer count = map.get(friend);
if (count == null) {
map.put(friend, 1);
} else {
map.put(friend, ++count);
}
}
}
@Override
protected void reduce(Text key, Iterable<Text> values, Context context) throws IOException, InterruptedException {
Map<String, Integer> map = new HashMap<String, Integer>();
Iterator<Text> iterator = values.iterator();
int numOfValues = 0;
while (iterator.hasNext()) {
String friends = iterator.next().toString();
if (friends.equals("")) {
context.write(key, new Text("[]"));
return;
}
addFriends(map, friends);
numOfValues++;
}
// now iterate the map to see how many have numOfValues
List<String> commonFriends = new ArrayList<String>();
for (Map.Entry<String, Integer> entry : map.entrySet()) {
//System.out.println(entry.getKey() + "/" + entry.getValue());
if (entry.getValue() == numOfValues) {
commonFriends.add(entry.getKey());
}
}
// sen it to output
context.write(key, new Text(commonFriends.toString()));
}
}
3、Spark解决方案
package com.sunrun.movieshow.algorithm.friend.spark;
import avro.shaded.com.google.common.collect.ImmutableCollection;
import com.google.common.collect.Sets;
import com.sunrun.movieshow.algorithm.common.SparkHelper;
import org.apache.spark.api.java.JavaPairRDD;
import org.apache.spark.api.java.JavaRDD;
import org.apache.spark.api.java.JavaSparkContext;
import scala.Tuple2;
import shapeless.Tuple;
import java.util.*;
/**
* 共同好友的Spark解决方案
* 输入:
* [root@h24 ~]# hadoop fs -cat /friend/input/*
* A B C D E
* B A C D
* C A B D E
* D A B C
* E A C
* F A
* 即第一列代表当前用户,后面的数据代表其好友列表。
*
* 输出
* (A,B) => C,D
* 两个好友之间的公共好友。
*/
public class FriendSpark {
// 构建有序key,避免重复
static Tuple2<String,String> buildSortedKey(String u1, String u2){
if(u1.compareTo(u2) < 0){
return new Tuple2<>(u1,u2);
}else{
return new Tuple2<>(u2,u1);
}
}
public static void main(String[] args) {
// 1.读取配置文件
String hdfsUrl = "hdfs://10.21.1.24:9000/friend/";
JavaSparkContext sc = SparkHelper.getSparkContext("CommonFriend");
JavaRDD<String> rdd = sc.textFile(hdfsUrl + "input");
// 2.解析内容
/**
* A B C
* ((A,B),(B,C))
* ((A,C),(B,C))
*/
JavaPairRDD<Tuple2<String, String>, List<String>> pairs = rdd.flatMapToPair(line -> {
String[] tokes = line.split(" ");
// 当前处理的用户
String user = tokes[0];
// 该用户的好友列表
List<String> friends = new ArrayList<>();
for (int i = 1; i < tokes.length; i++) {
friends.add(tokes[i]);
}
List<Tuple2<Tuple2<String, String>, List<String>>> result = new ArrayList<>();
// 算法处理,注意顺序,依次抽取每一个好友和当前用户配对作为key,好友列表作为value输出,
// 但如果该用户只有一个好友的话,那么他们的共同好友应该设置为空集
if (friends.size() == 1) {
result.add(new Tuple2<>(buildSortedKey(user, friends.get(0)), new ArrayList<>()));
} else {
for (String friend : friends) {
Tuple2<String, String> K = buildSortedKey(user, friend);
result.add(new Tuple2<>(K, friends));
}
}
return result.iterator();
});
/**
* pairs.saveAsTextFile(hdfsUrl + "output1");
* ((A,B),[B, C, D, E])
* ((A,C),[B, C, D, E])
* ((A,D),[B, C, D, E])
* ((A,E),[B, C, D, E])
* ((A,B),[A, C, D])
* ((B,C),[A, C, D])
* ((B,D),[A, C, D])
* ((A,C),[A, B, D, E])
* ((B,C),[A, B, D, E])
* ((C,D),[A, B, D, E])
* ((C,E),[A, B, D, E])
* ((A,D),[A, B, C])
* ((B,D),[A, B, C])
* ((C,D),[A, B, C])
* ((A,E),[A, C])
* ((C,E),[A, C])
* ((A,F),[])
*/
// 3.直接计算共同好友,步骤是group以及reduce的合并过程。
JavaPairRDD<Tuple2<String, String>, List<String>> commonFriends = pairs.reduceByKey((a, b) -> {
List<String> intersection = new ArrayList<>();
for (String item : b) {
if (a.contains(item)) {
intersection.add(item);
}
}
return intersection;
});
commonFriends.saveAsTextFile(hdfsUrl + "commonFriend");
/**
* [root@h24 ~]# hadoop fs -cat /friend/commonFriend/p*
* ((A,E),[C])
* ((C,D),[A, B])
* ((A,D),[B, C])
* ((C,E),[A])
* ((A,F),[])
* ((A,B),[C, D])
* ((B,C),[A, D])
* ((A,C),[B, D, E])
* ((B,D),[A, C])
*/
}
}