☰
基于Hadoop的好友推荐系统:二度人脉离线推荐实战
2026/10/9 10:46:03 网站建设 项目流程

简介:本资源为基于Hadoop的好友推荐系统完整项目源码,面向计算机、人工智能、通信工程等专业的在校学生与教师,可用于毕业设计、课程设计、作业提交或项目初期立项演示,也适合具备一定基础的小白进阶学习。压缩包共约2000个文件,整体79.5MB,涵盖73个Java源文件、24个JSP页面、88个Jar依赖包及大量PNG、CSS、GIF等前端与静态资源,另含XML配置、JS脚本与Properties属性文件,构成从后端算法到前端展示的完整工程结构。项目已通过导师指导与答辩评审,获得95分成绩,代码均经测试运行成功。内容涉及Hadoop集群环境下的距离计算、聚类数据映射、初始化距离矩阵等推荐算法模块,读者可据此理解好友推荐的核心逻辑与分布式实现思路,并在此基础上修改扩展功能。目前已有162人学习下载,适合需要完整可运行项目参考的开发者。

1. 基于Hadoop的好友推荐系统:从二度人脉到离线推荐的工程落地

社交产品里「你可能认识的人」这个模块,背后往往不是深度学习模型,而是一套跑在 Hadoop 上的离线好友推荐链路。它的核心逻辑很朴素:如果 A 和 B 有共同好友,那 A 和 B 大概率也该认识,这就是二度人脉推荐。数据量一旦上到千万级用户、亿级好友关系,单机内存放不下整张关系图,就必须借助 Hadoop 的分布式计算能力做批处理。这套系统适合谁?适合正在做 Hadoop 课程设计的学生、需要给社交产品补一个离线推荐模块的后端工程师,以及想找一个完整 MapReduce 实战项目练手的人。它解决的不是实时推荐,而是每天凌晨跑一批、第二天展示「可能认识的人」这种离线场景。部署文档和全部资料的价值,就在于把伪分布式搭建、代码编译、任务提交这条链路一次性跑通。

2. 好友推荐的核心逻辑与 Hadoop 选型理由

2.1 二度人脉推荐到底在算什么

好友推荐最经典的算法是「共同好友数」:对每一对用户 (u, v),统计他们共同好友的数量,数量越高越可能认识。数学上就是把好友关系看成无向图,找长度为 2 的路径。假设 A 的好友是 {B, C, D},B 的好友是 {A, C, E},那么 A 和 B 的共同好友是 C,共同好友数为 1。如果共同好友数超过阈值,就把对方推荐出去。

这个计算在单机上用邻接表也能做,但问题在于规模。一个中等社交产品有 5000 万用户,平均每人 100 个好友,好友关系就是 50 亿条边。每条边要展开成「好友的好友」候选对,中间数据会膨胀到几百亿条。单机内存和磁盘都扛不住,必须用 Hadoop 做分布式聚合。

MapReduce 天然适合这个场景:Map 阶段把每个用户的好友列表展开成候选对,Reduce 阶段按候选对聚合共同好友数。整个过程是典型的「展开—聚合」模式,不需要复杂的状态管理。

2.2 为什么用 Hadoop 而不是 Spark 或图数据库

很多人会问,现在 Spark 这么快,为什么还用 Hadoop MapReduce?原因有三个。第一,课程设计和教学场景下,MapReduce 的编程模型更直观,能看清数据流转的每一步,适合理解分布式计算原理。第二,如果数据是每天跑一次的全量批处理,MapReduce 的吞吐量足够,磁盘落盘反而让任务更稳定,不会因为内存不足频繁 OOM。第三,Hadoop 生态成熟,伪分布式和完全分布式搭建资料多,踩坑记录全,遇到问题容易搜到答案。

图数据库比如 Neo4j 做二度人脉查询确实快,但它适合在线查询,不适合每天全量重算。而且图数据库的分布式版本部署复杂,成本高。对于「每天凌晨跑一批推荐结果写入 HBase 或 MySQL」这种需求,Hadoop 是性价比最高的选择。

提示:如果数据量在千万级以下,单机用 Python 加 Redis 也能跑,不必上 Hadoop。Hadoop 的价值在亿级以上数据量时才真正体现。

2.3 整体架构:从好友关系表到推荐结果表

整个系统的数据流是这样的:原始好友关系存在 HDFS 上,格式是每行一对user_id, friend_id。第一轮 MapReduce 把关系表转成「用户 → 好友列表」的格式,方便后续展开。第二轮 MapReduce 做核心推荐计算:Map 阶段对每个用户的好友列表两两组合,输出(候选对, 共同好友);Reduce 阶段按候选对聚合,统计共同好友数,过滤掉已经是好友的关系,按共同好友数降序输出 TopN。

最终结果写入 HDFS,再通过 Sqoop 或自定义 OutputFormat 导出到 MySQL,供前端查询。如果要做 Hadoop HA 或者和 ZooKeeper 整合做高可用,那是生产环境的事,课程设计用伪分布式就够了。

3. 伪分布式环境搭建与项目部署实操

3.1 Hadoop 伪分布式搭建的关键步骤

伪分布式是单机模拟多节点,NameNode、DataNode、ResourceManager、NodeManager 都跑在一台机器上。搭建步骤不复杂,但环境变量配错一个就起不来。以下是核心命令,假设用 Hadoop 3.x 版本,JDK 用 1.8。

# 配置 SSH 免密登录,伪分布式也需要 ssh-keygen -t rsa -P '' -f ~/.ssh/id_rsa cat ~/.ssh/id_rsa.pub >> ~/.ssh/authorized_keys chmod 0600 ~/.ssh/authorized_keys # 解压 Hadoop 并配置环境变量 tar -zxvf hadoop-3.x.tar.gz -C /opt/ echo 'export HADOOP_HOME=/opt/hadoop-3.x' >> ~/.bashrc echo 'export PATH=$PATH:$HADOOP_HOME/bin:$HADOOP_HOME/sbin' >> ~/.bashrc source ~/.bashrc # 验证安装 hadoop version

环境变量配好后,需要改五个配置文件:core-site.xml配 fs.defaultFS 为hdfs://localhost:9000,hdfs-site.xml配副本数为 1,mapred-site.xml配 mapreduce.framework.name 为 yarn,yarn-site.xml配 yarn.nodemanager.aux-services,hadoop-env.sh配 JAVA_HOME。改完后执行格式化。

# 格式化 NameNode,只能执行一次 hdfs namenode -format # 启动 HDFS 和 YARN start-dfs.sh start-yarn.sh # 验证进程 jps # 应该看到 NameNode、DataNode、ResourceManager、NodeManager、SecondaryNameNode

jps输出里如果少了某个进程,先看日志。logs目录下对应进程的.log文件会写清楚原因,最常见的是端口占用和 JAVA_HOME 没配。

3.2 好友关系数据准备与 HDFS 上传

原始数据格式建议用最简单的 CSV:每行user_id,friend_id,表示两人是好友。注意好友关系是双向的,如果原始数据只有单向,需要在预处理阶段补全双向边。

# 本地准备测试数据 cat > friends.csv << 'EOF' 1001,1002 1001,1003 1001,1004 1002,1003 1002,1005 1003,1004 1004,1006 EOF # 创建 HDFS 目录并上传 hdfs dfs -mkdir -p /user/hadoop/friend/input hdfs dfs -put friends.csv /user/hadoop/friend/input/ # 验证上传 hdfs dfs -ls /user/hadoop/friend/input/ hdfs dfs -cat /user/hadoop/friend/input/friends.csv

数据上传后,建议先跑一个 WordCount 验证集群能正常提交任务。如果 WordCount 都跑不通,后面的推荐任务肯定也跑不通。

3.3 推荐任务代码结构与编译打包

核心代码分两个 Job。第一个 Job 把关系表转成邻接表,第二个 Job 做推荐计算。以下是第二个 Job 的 Mapper 和 Reducer 核心逻辑。

// Mapper: 输入是 "用户\t好友1,好友2,...",输出候选对和共同好友 public class RecommendMapper extends Mapper<LongWritable, Text, Text, Text> { @Override protected void map(LongWritable key, Text value, Context context) throws IOException, InterruptedException { String[] parts = value.toString().split("\t"); if (parts.length < 2) return; String user = parts[0]; String[] friends = parts[1].split(","); // 两两组合好友,输出 (好友A,好友B) -> 共同好友user for (int i = 0; i < friends.length; i++) { for (int j = i + 1; j < friends.length; j++) { // 保证输出顺序一致,避免 (A,B) 和 (B,A) 被当成两个 key String pair = friends[i].compareTo(friends[j]) < 0 ? friends[i] + "," + friends[j] : friends[j] + "," + friends[i]; context.write(new Text(pair), new Text(user)); } } } } // Reducer: 聚合共同好友,输出推荐结果 public class RecommendReducer extends Reducer<Text, Text, Text, Text> { @Override protected void reduce(Text key, Iterable<Text> values, Context context) throws IOException, InterruptedException { Set<String> commonFriends = new HashSet<>(); for (Text val : values) { commonFriends.add(val.toString()); } // 输出格式:用户A,用户B -> 共同好友数 context.write(key, new Text(String.valueOf(commonFriends.size()))); } }

Mapper 里最关键的是候选对排序。如果不排序,(1002,1003)和(1003,1002)会被当成两个不同的 key,导致共同好友数被拆散。Reducer 用 Set 去重,因为同一个共同好友可能通过多条路径被计数。

编译打包用 Maven,pom.xml 里引入 hadoop-client 依赖,打包成 jar 后提交。

# 编译打包 mvn clean package # 提交任务到 YARN hadoop jar friend-recommend.jar com.example.RecommendDriver \ /user/hadoop/friend/input /user/hadoop/friend/output # 查看结果 hdfs dfs -cat /user/hadoop/friend/output/part-r-00000

提交任务时如果报ClassNotFoundException,检查 jar 包里是否包含了所有依赖类。用 Maven 的 shade 插件打胖包能避免这个问题。

4. 参数调优与推荐结果质量把控

4.1 共同好友数阈值怎么定

阈值定太低,推荐一堆不相关的人;定太高,推荐结果太少。经验做法是先跑全量,统计共同好友数的分布,然后取 Top 10% 作为阈值。比如测试数据里共同好友数最多是 3,那阈值设 2 比较合适。

// 在 Reducer 里加阈值过滤 int threshold = 2; if (commonFriends.size() >= threshold) { context.write(key, new Text(String.valueOf(commonFriends.size()))); }

阈值可以通过 Configuration 传入,不用改代码重新编译。在 Driver 里用conf.setInt("recommend.threshold", 2),Reducer 的 setup 方法里读取。

4.2 排除已是好友的关系

推荐结果里不能出现已经是好友的人。做法是在 Reducer 输出前,加载一份好友关系集合,判断候选对是否已经是好友。小数据量可以直接在 Reducer 里读 HDFS 文件,大数据量建议用 DistributedCache。

// setup 阶段加载好友关系 private Set<String> existingFriends = new HashSet<>(); @Override protected void setup(Context context) throws IOException { // 从 DistributedCache 读取好友关系文件 URI[] cacheFiles = context.getCacheFiles(); if (cacheFiles != null) { for (URI uri : cacheFiles) { BufferedReader reader = new BufferedReader( new InputStreamReader(new FileInputStream(uri.getPath()))); String line; while ((line = reader.readLine()) != null) { existingFriends.add(line.trim()); } reader.close(); } } }

在 Driver 里用job.addCacheFile(new URI("/user/hadoop/friend/input/friends.csv"))把文件加入缓存。注意缓存文件在本地路径和 HDFS 路径的写法不同,伪分布式下用 HDFS 全路径。

4.3 数据倾斜与 Combiner 优化

如果某个用户好友特别多,比如大 V 有几千个好友,Mapper 展开的候选对会特别多,导致 Reduce 阶段某些 key 数据量远大于其他 key,这就是数据倾斜。解决办法是在 Mapper 端加 Combiner,提前聚合一部分。

// Combiner 和 Reducer 逻辑类似,但输出类型要匹配 Mapper 输出 public class RecommendCombiner extends Reducer<Text, Text, Text, Text> { @Override protected void reduce(Text key, Iterable<Text> values, Context context) throws IOException, InterruptedException { Set<String> set = new HashSet<>(); for (Text val : values) { set.add(val.toString()); } // 输出共同好友列表,而不是数量,因为 Reducer 还要去重 context.write(key, new Text(String.join(",", set))); } }

Combiner 的输出类型必须和 Mapper 输出一致,否则 Reducer 接收不到。这里输出的是共同好友列表字符串,Reducer 再拆分去重。加了 Combiner 后,网络传输量能减少 60% 以上。

5. 部署与运行中的避坑清单

5.1 坑一:NameNode 反复格式化导致 DataNode 起不来

现象:执行start-dfs.sh后jps看不到 DataNode,日志报Incompatible clusterID。原因:多次执行hdfs namenode -format,NameNode 的 clusterID 变了,但 DataNode 还保留旧的 clusterID。解决:删除 DataNode 的数据目录(dfs.datanode.data.dir配置的路径),重新格式化一次,再启动。血泪经验是格式化只能做一次,做之前确认配置没问题。

5.2 坑二:任务提交后卡在 ACCEPTED 状态

现象:hadoop jar提交后,YARN 界面显示任务一直是 ACCEPTED,不进入 RUNNING。原因:YARN 的资源配置不够,或者 NodeManager 没启动。解决:检查yarn-site.xml里yarn.nodemanager.resource.memory-mb是否大于任务需要的内存,默认 8192MB 一般够用。如果 NodeManager 没起来,看日志里是不是端口冲突。

5.3 坑三:中文乱码导致推荐结果异常

现象:输出结果里用户 ID 变成乱码,或者 Reduce 阶段报解析错误。原因:输入文件编码不是 UTF-8,或者 Java 默认编码和文件编码不一致。解决:提交任务时加-Dfile.encoding=UTF-8,并在代码里显式指定new String(bytes, StandardCharsets.UTF_8)。数据准备阶段用file命令确认文件编码。

5.4 坑四:Output 目录已存在导致任务失败

现象:第二次提交任务时报Output directory already exists。原因:Hadoop 不允许输出目录已存在,防止覆盖数据。解决:每次跑之前删除输出目录,或者在 Driver 里加判断自动删除。

// Driver 里自动删除输出目录 FileSystem fs = FileSystem.get(conf); Path outputPath = new Path(args[1]); if (fs.exists(outputPath)) { fs.delete(outputPath, true); }

5.5 坑五:内存不足导致 Container 被 Kill

现象:任务跑到 Reduce 阶段报Container killed by YARN for exceeding memory limits。原因:Reduce 阶段聚合的数据量太大,默认内存不够。解决:在 Driver 里调大 Reduce 内存,job.getConfiguration().set("mapreduce.reduce.memory.mb", "2048"),同时调大 JVM 堆内存mapreduce.reduce.java.opts为-Xmx1536m。如果还是不够,说明数据倾斜严重,需要加 Combiner 或拆分大 key。

6. 从离线推荐到增量更新:一个可复用的优化技巧

全量重算每天跑一次,数据量大时耗时很长。一个实用的优化是增量更新:只对当天有新好友关系的用户重新计算推荐,其他用户的推荐结果保持不变。做法是在输入数据里加一个时间戳字段,Mapper 里只处理时间戳在当天范围内的记录,Reduce 输出时合并历史推荐结果。

// Mapper 里按时间戳过滤 long todayStart = ...; // 当天零点时间戳 if (timestamp < todayStart) { return; // 跳过历史数据 } // 只处理当天新增的好友关系

历史推荐结果存在 HDFS 的另一个目录,Reduce 阶段用MultipleInputs同时读取新增计算结果和历史结果,合并后输出。这样每天的计算量从全量降到增量,耗时能减少 70% 以上。

验证增量更新是否正确,可以对比全量结果和增量结果的差异。写一个简单的 diff 脚本,统计两个结果集的交集和差集。如果差集超过 5%,说明增量逻辑有问题,需要检查时间戳过滤和合并逻辑。

# 对比全量和增量结果 hdfs dfs -cat /user/hadoop/friend/output_full/part-r-00000 | sort > full.txt hdfs dfs -cat /user/hadoop/friend/output_incr/part-r-00000 | sort > incr.txt diff full.txt incr.txt | head -20

我自己跑这套系统时,最大的教训是不要一上来就追求完全分布式。伪分布式先把逻辑跑通,数据量上来后再迁移到集群,能省掉大量调试时间。另一个习惯是每次改完代码先本地用少量数据测一遍,确认 Map 和 Reduce 的输入输出格式对得上,再提交到 Hadoop。很多翻车都是因为本地没测,提交后才发现 key 类型不匹配或者输出路径写错。希望帮到你。

本文还有配套的精品资源,点击获取

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询