
简介这是一份基于Hadoop的好友推荐系统完整项目资源面向计算机相关专业学生及企业开发者适合毕业设计、课程设计、初期项目演示及进阶学习。系统涵盖MapReduce分布式计算、协同过滤推荐等核心模块并附带部署文档与全部源码资料项目已获导师指导认可答辩评审分达到95分。压缩包共2000个文件大小79.5MB主要包含73个Java源文件、55个class字节码文件、24个JSP页面、22个XML与11个properties配置文件以及大量CSS、PNG前端展示资源并配有jar依赖包覆盖环境部署、数据计算、界面呈现的完整链路。实现内容包括数据库服务、聚类距离计算、推荐结果绘图等关键功能读者可获得可直接运行的工程代码、部署文档与清晰目录结构方便二次开发和排错学习。该资源已有162人学习下载是高含金量的实战项目参考。1. 基于Hadoop的好友推荐系统核心不在“推荐”而在“计数”好友推荐听起来像搜索问题真正动手才会发现它是一个计数问题任意两个用户之间数共同好友再按共同好友数排序。边数到百万级、顶点到十万级之后单机 Python 和 NetworkX 开始吃力MapReduce 的价值在于把“两两组合 去重计数”拆成可以并行推进的 Shuffle 任务。这个课题频繁出现在课程设计和毕业设计里因为它把图算法、数据倾斜、参数调优、伪分布式部署串成了一条完整链路。下面按“算法选型 → 设计与实现 → 部署落地 → 结果调优”的顺序展开答辩时能讲清楚两两组合和去重的改写逻辑通常就是加分点所在。2. 好友推荐算法选型Hadoop 上如何改写成两两组合计数2.1 “可能认识的人”在图论里就是共同邻居社交产品里“你可能认识的人”经典模型是 Common Neighbors把好友关系看成无向图顶点是用户边是好友关系两个还不是好友的用户推荐分等于它们邻居集合的交集大小。这个指标的好处是局部性计算任意一个候选对只需要两个用户各自的一跳邻居不需要全图状态所以天生适合分而治之。用公式表达就是 score(u, v) |N(u) ∩ N(v)|且 (u, v) 不属于边集。真实产品还会给共同好友加权比如按互动频次、最近联系时间给每个共同好友一个权重最终得分变成加权和。加权只会改变 Reduce 阶段的聚合方式骨架仍然是计数。反过来看PageRank、标签传播这类算法每轮迭代都依赖全图状态放进 Hadoop 批处理要反复扫描全量数据不属于这个课题该碰的范围。2.2 两种落地形态共同好友计数与物品协同过滤“好友推荐”通常有两个数据入口。第一种输入是好友关系边输出“可能认识的人”第二种输入是用户行为记录比如收藏、关注话题、点赞内容先算用户和物品的共现次数再把共享行为的数量当作两人相似度——这就是 item-based 协同过滤换了个推荐对象。两种形态在 MapReduce 上的改写完全一致对每个用户的邻居列表或行为物品列表做两两组合组合结果当 key当前用户当 valueReduce 阶段去重计数差异只在输入解析和分值含义。输入数据两两组合的语义推荐输出适用场景好友关系边 (u, v)u 同时认识 a、b给 (a, b) 投一票可能认识的人社交通讯用户行为 (u, item)u 同时消费 a、b给 (a, b) 投一票兴趣相似用户内容社区下面所有实现都按好友关系边来写换成行为表只改输入解析和字段名。2.3 数据没到千万级为什么还要跑在 Hadoop 上MapReduce 的优势是并行吞吐而非单条延迟。好友图只有几万顶点、几十万边时单机 Python 排序几分钟出结果Hadoop 作业从提交到调度就花掉几十秒反而是负优化。那课程设计为什么还要求基于 Hadoop一是题目考察的就是分布式编程能力二是为将来千万级边数的真实场景做技术储备。常见做法是先跑一个统计作业看用户度分布平均度没到两位数时共同好友几乎全是 0模型没有区分度数据是自己造的就按幂律分布生成图少数大 V 拥有大量好友推荐结果才不至于全空这也是后面数据倾斜问题的来源。3. 设计与实现从好友边表到可运行的 MapReduce 作业3.1 输入格式先用一步归组把边表变成邻接表实现共同好友计数最容易踩的坑是输入格式。如果直接读“u001 u002”这样的边表Mapper 处理一行时只能看到一条边无法生成“某个用户所有好友的两两组合”。所以要先做一步归组把边表变成邻接表每行一个用户加它的全部好友。这一步可以用一个只有 Mapper 和分组逻辑的作业完成也可以直接用 Hive 一条 SQL 完成。后续作业统一按下面格式读入列内容要求第 1 列用户 ID全局唯一第 2 列好友 ID 列表逗号分隔已去重样本数据如下代码块里用四个空格代替制表符实际文件必须是 tab 分隔# 格式用户IDTAB好友ID列表 u001 u002,u003,u004 u002 u001,u003,u005 u003 u001,u002,u004,u005从这个样本能手工验算u001 和 u003 的共同好友是 u002、u004所以 (u001:u003) 的推荐分是 2。设计时还要给单用户好友数设上限组合复杂度是 O(k^2)好友列表 500 人时单用户要产生大约 12 万条中间记录大 V 用户会拖垮作业超过上限就随机采样或者只保留互动最频繁的前 K 个好友。3.2 共同好友计数作业Mapper 输出两两组合Reducer 去重计数核心作业只需要一个 Mapper 和一个 Reducer逻辑全部围绕 Text 的 key-value 展开package com.example.friendrec; import java.io.IOException; import java.util.HashSet; import java.util.Set; import org.apache.hadoop.conf.Configuration; import org.apache.hadoop.fs.Path; import org.apache.hadoop.io.Text; import org.apache.hadoop.mapreduce.*; import org.apache.hadoop.mapreduce.lib.input.FileInputFormat; import org.apache.hadoop.mapreduce.lib.output.FileOutputFormat; public class CommonFriends { public static class PairMapper extends MapperObject, Text, Text, Text { private Text pair new Text(); public void map(Object key, Text value, Context ctx) throws IOException, InterruptedException { String[] parts value.toString().split(\t); if (parts.length 2) return; String user parts[0].trim(); String[] friends parts[1].split(,); // EXIST 表示这两个人已经是好友Reduce 阶段据此过滤掉该候选对 for (String f : friends) { f f.trim(); if (f.isEmpty() || f.equals(user)) continue; ctx.write(pair(user, f), new Text(EXIST)); } // 两两组合对 user 的任意两个好友 (fi, fj)user 是它们的共同好友 for (int i 0; i friends.length; i) { String fi friends[i].trim(); if (fi.isEmpty() || fi.equals(user)) continue; for (int j i 1; j friends.length; j) { String fj friends[j].trim(); if (fj.isEmpty() || fj.equals(user)) continue; ctx.write(pair(fi, fj), new Text(user)); } } } // 字典序拼接保证 (a,b) 和 (b,a) 进入同一个 key private Text pair(String a, String b) { pair.set(a.compareTo(b) 0 ? a : b : b : a); return pair; } } public static class CountReducer extends ReducerText, Text, Text, Text { public void reduce(Text key, IterableText values, Context ctx) throws IOException, InterruptedException { boolean direct false; SetString common new HashSetString(); for (Text v : values) { String s v.toString(); if (EXIST.equals(s)) direct true; else common.add(s); } // 已经是好友的两人直接丢弃共同好友数为 0 的也不输出 if (!direct !common.isEmpty()) { ctx.write(key, new Text(String.valueOf(common.size()))); } } } public static void main(String[] args) throws Exception { Configuration conf new Configuration(); Job job Job.getInstance(conf, common-friends); job.setJarByClass(CommonFriends.class); job.setMapperClass(PairMapper.class); job.setReducerClass(CountReducer.class); job.setOutputKeyClass(Text.class); job.setOutputValueClass(Text.class); FileInputFormat.addInputPath(job, new Path(args[0])); FileOutputFormat.setOutputPath(job, new Path(args[1])); System.exit(job.waitForCompletion(true) ? 0 : 1); } }逻辑说明Mapper 对每个用户做两件事。第一件事是为 user 的每条直接好友边输出一个 EXIST 标记第二件事是对好友列表内任意两个不同好友输出组合键和当前用户 ID表示“当前用户是这两个人的共同好友”。Reducer 拿到以“a:b”为 key 的完整 value 流后先看有没有 EXIST有就说明 a、b 已经是好友直接丢弃没有就统计出现过的不同用户数这就是共同好友数。参数说明这个作业没有显式设置 Reducer 数量默认只起 1 个 Reduce 任务伪分布式下够用数据量大时要按输出规模调大 reduce 数。Map 侧的内存压力和度分布直接相关中间记录总量约等于 所有用户度平方之和的一半度分布越不均匀越容易出现单个 map 任务长时间不结束的现象。HashSet 去重是防御性写法如果归组阶段没把好友列表去重同一个用户会对同一候选对重复投票。3.3 排序与 TopN用 Hive SQL 收尾少写一个 MapReduce计数作业输出的是全量候选对格式是“u001:u003 2”还不能直接当推荐结果用。推荐接口要的是“每个用户按分值倒序取前 10 个候选”。最省事的做法是把第一个作业的输出导成 Hive 外部表用窗口函数一条 SQL 完成排序和截断SELECT user_id, friend_id, score FROM ( SELECT split(pair, :)[0] AS user_id, split(pair, :)[1] AS friend_id, CAST(cnt AS INT) AS score, ROW_NUMBER() OVER (PARTITION BY split(pair, :)[0] ORDER BY CAST(cnt AS INT) DESC) AS rn FROM friend_score ) t WHERE rn 10;PARTITION BY 保证每个用户的排序互不干扰rn 10 等价于 TopN。如果环境里没有 Hive再写一个 Mapper 把 pair 拆成 (user, friend:score)Reducer 里用 TreeMap 维护前 10 个即可逻辑不复杂。这里不要做全局排序数据量大时全局排序要引 TotalOrderPartitioner课程设计阶段按用户分区已经足够。4. 部署落地Hadoop 伪分布式搭建与作业提交4.1 环境检查JDK、SSH 免密和 Hadoop 安装位置部署文档的第一步永远先是环境检查。常见做法是在 Ubuntu 上安装 Hadoop 3.x 的 release 版本配 OpenJDK 8 或 11Hadoop 3 已经不支持 Java 7这一点经常被忽略。先确认 java -version 和 ssh localhost 能直接连上本机。SSH 免密是 start-dfs.sh 的硬前提这个脚本要用本地免密登录去拉起 DataNode 进程ssh-keygen -t rsa -P -f ~/.ssh/id_rsa cat ~/.ssh/id_rsa.pub ~/.ssh/authorized_keys chmod 600 ~/.ssh/authorized_keys ssh localhost能免密登录后再解压 Hadoop设置 HADOOP_HOME 和 PATH并在 etc/hadoop/hadoop-env.sh 里写死 JAVA_HOME。开发环境搭建和伪分布式搭建在这个阶段是同一套动作之后的区别只体现在配置文件上。4.2 四个核心配置文件core-site、hdfs-site、mapred-site、yarn-site我们只需要改四个文件每个文件只动最关键的属性文件负责的模块必改属性core-site.xml全局文件系统fs.defaultFS 指向 hdfs://localhost:9000hdfs-site.xmlHDFS 存储dfs.replication 设为 1mapred-site.xmlMapReduce 运行时mapreduce.framework.name 设为 yarnyarn-site.xml资源调度yarn.nodemanager.aux-services 为 mapreduce_shufflecore-site.xml 里还要配 hadoop.tmp.dir默认指向 /tmp系统重启会清空 NameNode 元数据这是最容易“第二天起不来”的坑configuration property namefs.defaultFS/name valuehdfs://localhost:9000/value /property property namehadoop.tmp.dir/name value/home/hadoop/hadoop_tmp/value /property /configurationhdfs-site.xml 里把副本数改成 1并显式指定 NameNode 和 DataNode 的数据目录避免默认路径的权限问题configuration property namedfs.replication/name value1/value /property property namedfs.namenode.name.dir/name valuefile:///home/hadoop/hdfs/name/value /property property namedfs.datanode.data.dir/name valuefile:///home/hadoop/hdfs/data/value /property /configurationmapred-site.xml 只需要一行把计算框架切到 YARN否则作业会跑在 local 模式日志里根本看不到 ApplicationMasterconfiguration property namemapreduce.framework.name/name valueyarn/value /property /configurationyarn-site.xml 里配置 shuffle 辅助服务并给 NodeManager 分配可用内存。虚拟机内存只有 4G 时yarn.nodemanager.resource.memory-mb 建议给 3072留出系统余量configuration property nameyarn.nodemanager.aux-services/name valuemapreduce_shuffle/value /property property nameyarn.nodemanager.resource.memory-mb/name value3072/value /property /configuration四份配置改完Hadoop 安装与配置这一步就算完成接下来才是格式化与启动。4.3 格式化、启动服务、提交作业的完整命令启动顺序是固定的先格式化 NameNode再启动 HDFS 和 YARN最后用 jps 确认关键进程都在hdfs namenode -format start-dfs.sh start-yarn.sh jps注意hdfs namenode -format 只能执行一次。第二次格式化会生成新的 cluster IDDataNode 拿旧 ID 注册不上表现就是 jps 里有 DataNode但日志一直报连接失败。误格式化后要删掉 NameNode 和 DataNode 的元数据目录重新来这正是配置文件里显式指定目录的价值。确认 jps 能看到 NameNode、DataNode、SecondaryNameNode、ResourceManager、NodeManager 五个进程后把样本数据放上 HDFS 并提交作业hdfs dfs -mkdir -p /user/hadoop/friend/input hdfs dfs -put friendship_data.txt /user/hadoop/friend/input/ hadoop jar target/friend-recommend-1.0.jar com.example.friendrec.CommonFriends \ /user/hadoop/friend/input /user/hadoop/friend/output hdfs dfs -cat /user/hadoop/friend/output/part-r-* | sort -k2 -nr | head -20hadoop jar 后面依次是 jar 包路径、主类全限定名、输入目录、输出目录。输出目录不能预先存在框架会在作业启动时自动创建否则直接报 FileAlreadyExistsException。最后一条命令把结果按第二列分值倒序取前 20 行快速确认产出有效。要扩展到集群搭建差别只在 workers 文件填多台机器、各机器配好相同 JDK 和 Hadoop、把免密从本机扩大到所有机器只有做 HA 双 NameNode 才需要引入 Zookeeper 做故障切换单机伪分布式不用动它格式化与启动顺序完全不变。5. 好友推荐作业的结果验证、内存参数与日志定位5.1 用带答案的小样本验证推荐正确性作业跑通不等于结果正确。改模型之前先用一个能手工算答案的小图验证。拿 3.1 节的样本u001 和 u003 的共同好友是 u002、u004输出里必须有“u001:u003 2”u001 和 u002 已经是好友输出里不能出现这一对。再取一个 5 顶点的小图把期望结果写成文本逐个对比 part-r-* 的实际输出比抽查大结果可靠得多。5.2 三个必调参数规模验证之后调节奏主要看三个参数参数作用伪分布式建议yarn.nodemanager.resource.memory-mb单机可用内存上限机器内存的 3/4 左右mapreduce.map.memory.mb单个 Map 容器内存512 到 1024和堆大小匹配mapreduce.job.reducesReducer 数量默认 1数据量大再往上调这里不要直接给作业加 Combiner。Combiner 会先按 key 做局部合并但这个 value 流里混着 EXIST 标记和用户名局部合并要么破坏去重要么丢失 EXIST 语义实现复杂收益却不大真正的去重发生在 Shuffle 之后交给 Reducer 一次处理更干净。5.3 作业失败时的日志定位顺序作业失败先分三层看ApplicationMaster 日志管作业生命周期容器日志管具体异常jobhistory 管已完成作业汇总。先拿到 applicationId然后一条命令抓异常yarn logs -applicationId application_1710000000000_0001 2/dev/null \ | grep -E Exception|Caused by | head -40grep 出堆栈后再区分两类失败Container 被 kill 基本是内存参数问题改 yarn.nodemanager.resource.memory-mb 和 mapreduce.map.memory.mb异常来自业务代码则是输入数据格式问题重点看 split 出来的字段是否越界。这套定位顺序在伪分布式和真实集群上一致差别只是集群里要先把日志聚合开关打开否则 yarn logs 取不到容器日志。本文还有配套的精品资源点击获取