简介本资源为基于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:9000hdfs-site.xml配副本数为 1mapred-site.xml配 mapreduce.framework.name 为 yarnyarn-site.xml配 yarn.nodemanager.aux-serviceshadoop-env.sh配 JAVA_HOME。改完后执行格式化。# 格式化 NameNode只能执行一次 hdfs namenode -format # 启动 HDFS 和 YARN start-dfs.sh start-yarn.sh # 验证进程 jps # 应该看到 NameNode、DataNode、ResourceManager、NodeManager、SecondaryNameNodejps输出里如果少了某个进程先看日志。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 MapperLongWritable, 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 ReducerText, Text, Text, Text { Override protected void reduce(Text key, IterableText values, Context context) throws IOException, InterruptedException { SetString 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 去重因为同一个共同好友可能通过多条路径被计数。编译打包用 Mavenpom.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 SetString 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 ReducerText, Text, Text, Text { Override protected void reduce(Text key, IterableText values, Context context) throws IOException, InterruptedException { SetString 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 -formatNameNode 的 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.encodingUTF-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 类型不匹配或者输出路径写错。希望帮到你。本文还有配套的精品资源点击获取
阅读完成 · 觉得有帮助?