恒美微站
首页
关于我们
建站服务
主题模板
案例展示
资讯中心
联系我们
基于Hadoop的好友推荐系统设计与MapReduce实现
首页
资讯中心
/
基于Hadoop的好友推荐系统设计与MapReduce实现
基于Hadoop的好友推荐系统设计与MapReduce实现
发布时间:2026/10/9 14:38:54
简介基于Hadoop实现的好友推荐系统毕业设计资源包含可调试运行的Java源码和完整文档说明。项目围绕Hadoop平台设计好友推荐核心逻辑覆盖数据读取、距离计算、聚类分析、推荐生成等环节并包含距离计算、聚类、画图等模块适合计算机、通信、人工智能、自动化等专业学生用于毕业设计、课程大作业或期末项目也便于初学者学习进阶。压缩包共2000个文件大小约79.46MB主要文件类型包括Java源码、class字节码、JSP页面、CSS样式、JS脚本、XML配置、JAR依赖库以及PNG图片等其中Java/class构成核心算法JSP/CSS/JS搭建Web展示层XML/properties保存配置jar为第三方依赖目录结构清晰便于部署查看。已有200人学习下载。该项目经调试测试答辩评审分达98分具备较高借鉴价值基础扎实者可在现有框架上修改扩展实现不同推荐功能。1. 基于Hadoop的好友推荐系统为什么拿它做毕业设计最稳做毕业设计最怕的不是没思路而是思路大到一个人写不完。基于Hadoop实现的好友推荐系统恰好是那种既有算法深度、又有工程落地感的题核心算法是共同好友统计数据形态是用户关系对计算框架是Hadoop MapReduce最终产物是一个能运行、能验证、能写进文档的完整系统。按最常见的毕业设计形态它是一份 Java 源码加一份完整的文档说明从需求分析写到测试报告正好覆盖答辩要的全部过程性材料。适合三类人选了社交推荐方向、需要 Hadoop 课程设计或 Java 课程设计案例源码、以及想用最短时间把分布式计算真正跑通的在校生。先说结论用两轮 MapReduce 把推荐逻辑切出来再加一层打分与 TopN 过滤是这条路线上性价比最高、也最容易被老师认可的做法。2. 把好友推荐拆成可计算的 MapReduce 流程两轮作业与数据模型2.1 共同好友推荐的本质把社交关系变成可统计的组合推荐系统的经典做法有基于物品、基于用户、基于内容三类但毕业设计里绝大多数“好友推荐”题底层都是同一个直觉你和某个人有越多的共同好友你们越可能本来就应该认识。这个逻辑放进 Hadoop 里非常顺因为它本质上是“统计”而不是“预测”先找共同好友再统计共同好友个数最后排序取 TopN。这也是为什么 Hadoop 课程设计、hadoop 面试题都喜欢拿它当例子——计算过程能被拆成清晰的 Map 和 Reduce又不像电商推荐那样需要大规模矩阵运算。输入数据只需要一张最简单的社交关系表两列用户ID、好友ID。常见的数据格式是 CSV 或者用制表符分隔的文本每一行表示一条单向的“关注/好友”关系。为了减少后续处理的分支我一般会把好友关系按“双向”处理如果 A 是 B 的好友就认为 B 也是 A 的好友读数据时补一条反向边。这样做的好处是后面做两两组合时不会因为边的方向漏掉推荐目标代价是数据量会翻倍但对于课程设计和毕设这个量级完全可以忽略。这一步的数据模型是整个系统的地基。我建议不要一上来就设计用户画像、标签、行为日志之类的东西先把“用户—好友”这张表跑通再考虑加字段。很多毕设翻车是因为数据模型画了七八张表最后 MapReduce 里根本用不上其中一半。2.2 第一轮 MapReduce把边列表转成“用户 → 直接好友列表”第一轮 Job 要解决的问题是把每一行只有一条关系的边文件聚合出每个用户的直接好友列表。比如用户 1 的好友是 2 和 3这一轮就要输出一行1 2,3。Mapper 做的事很简单读一行把行按分隔符拆成两个字段以用户ID为 key好友ID为 value 发出去。public class FriendListMapper extends MapperLongWritable, Text, Text, Text { Override protected void map(LongWritable key, Text value, Context context) throws IOException, InterruptedException { String line value.toString().trim(); if (line.isEmpty()) { return; } // 输入是 用户ID,好友ID一行一条记录 String[] parts line.split(,); String user parts[0].trim(); String friend parts[1].trim(); context.write(new Text(user), new Text(friend)); // 补一条反向边保证好友关系在后续组合中对称可用 context.write(new Text(friend), new Text(user)); } }这段代码有两个关键地方。第一line.split(,)后面马上trim()是因为 CSV 文件里经常混着空格和不可见字符第二补反向边直接写在 Mapper 里不要等到 Reducer 再做因为 Reducer 里拿不到“原始边的另一个方向”。context.write每次调用都会产生一条中间记录Map 的输出会落盘并按 key 排序这是 Hadoop 的 shuffle 机制在做的事不需要我们手动排序。Reducer 的职责是把同一个 key也就是同一个用户下的所有好友ID去重合并输出成一行逗号分隔的列表。用LinkedHashSet去重既能保证唯一性又能让输出顺序稳定后面调试时对得上。public class FriendListReducer extends ReducerText, Text, Text, Text { Override protected void reduce(Text key, IterableText values, Context context) throws IOException, InterruptedException { LinkedHashSetString friends new LinkedHashSet(); for (Text value : values) { friends.add(value.toString()); } if (friends.isEmpty()) { return; } // key用户IDvalue该用户的全部直接好友逗号分隔 context.write(key, new Text(String.join(,, friends))); } }为什么用字符串拼接而不是直接把多个好友写到多个 value因为下一轮 Job 的 Mapper 希望“一个用户的好友列表”能整体作为一行输入这样它才能对这个列表做两两组合。如果这里输出多行下一轮还得再聚合一次。这个设计属于典型的“为下一轮优化数据格式”写在文档说明里多提一句能让老师看出你考虑过作业之间的衔接。2.3 第二轮 MapReduce两两组合统计共同好友数并过滤直连关系第二轮是算法的核心。输入是第一轮的输出格式是用户ID \t 好友1,好友2,好友3。Mapper 要做两件事一是把“当前用户和列表里的每个好友”标记为直连关系二是把列表里的好友两两组合认为“这两个人通过当前用户成为了共同好友”发出一条计数为 1 的中间记录。public class RecommendMapper extends MapperLongWritable, Text, Text, Text { private static final String DIRECT -1; private final Text outKey new Text(); private final Text outVal new 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].trim(); String[] friends parts[1].split(,); // 当前用户与列表中的每一个人已经是好友用-1标记 for (String friend : friends) { outKey.set(user : friend); outVal.set(DIRECT); context.write(outKey, outVal); } // 两两组合friends[i] 和 friends[j] 都认识 user // 所以 user 是两人的共同好友 for (String a : friends) { for (String b : friends) { if (a.equals(b)) { continue; } outKey.set(a : b); outVal.set(1); context.write(outKey, outVal); } } } }这里的 key 用了a:b这种字符串拼接方向固定为“给 a 推荐 b”。注意组合是笛卡尔积a:b和b:a会被当成不同的推荐方向分别输出这符合好友推荐的直觉A 认识了 BB 也该被推荐认识 A。直连标记却只需要当前用户到好友这一个方向吗不是直连也应该两个方向都写。当前这份代码里因为第一轮补了反向边用户 A 的好友列表里包含 B用户 B 的好友列表里也包含 A所以在两个用户各自的列表处理中A:B和B:A的直连标记都会出现能完整覆盖所有已有边。Reducer 拿到的是同一个 pair 下的所有 value里面可能混着 1 和 -1。一旦出现 -1说明这两个人已经是好友直接跳过否则把所有的 1 加起来就是这对人的共同好友数量。public class RecommendReducer extends ReducerText, Text, Text, Text { Override protected void reduce(Text key, IterableText values, Context context) throws IOException, InterruptedException { int mutual 0; for (Text value : values) { String s value.toString(); if (-1.equals(s)) { // 已经是好友绝不推荐 return; } mutual Integer.parseInt(s); } if (mutual 0) { return; } String[] pair key.toString().split(:); // 输出目标用户 \t 候选用户 \t 共同好友数 context.write(new Text(pair[0]), new Text(pair[1] \t mutual)); } }这段 Reducer 里最容易被忽略的是return而非break一旦遇到 -1整个 key 的输出都要放弃。这个细节我建议在文档说明里专门写一笔因为它体现了设计者对“直连关系优先级最高”的理解。到这里两轮 MR 已经能输出“A → B 有 N 个共同好友”的原始结果但离“推荐系统”还差排序和截断这部分放到第四章展开。关于第二轮的数据膨胀我补充一个经验值假设平均每个用户有 50 个好友两两组合会产生 50×49 也就是约 2450 条中间记录用户量 10 万时就是 2.45 亿条这个量在单机伪分布式上处理需要几分钟。所以第二轮从一开始就要考虑 Combiner。Combiner 不能随便加它必须满足一个条件合并操作对同一个 key 的 value 是可交换、可结合的。我们这个 Reducer 的求和逻辑满足条件但 -1 的优先级不满足所以要在 Combiner 里保留 -1 并把 1 求和等 Reducer 看到 -1 时仍然直接放弃。3. Hadoop 伪分布式搭建与项目骨架从空目录到第一个任务3.1 伪分布式、虚拟机还是 Docker 镜像毕设环境怎么选环境搭建是很多人在这一步放弃的原因教程里的截图千奇百怪hadoop 安装与配置的文章从 2.x 到 3.x 都有照着敲完不是起不来就是进程互相打架。我建议毕设默认走“单机伪分布式”也就是一台机器上同时跑 NameNode、DataNode、ResourceManager、NodeManager。这和真正的 hadoop 集群搭建只差在机器数量但流程完全一致足以支撑课程设计和毕业设计。除非你的文档里专门写了 HA、多节点压测否则别轻易上多节点集群光同步配置和调试网络就能耗掉一周。如果本机是 Windows不要直接在 Windows 里硬刚。最省事的路线是在虚拟机里装一个 Ubuntu Server内存给 4G 以上磁盘 40G网络用 NAT 即可。其次可以考虑 hadoop 的 Docker 镜像一条docker run起容器适合只想快速验证代码的情况但 Docker 的端口映射和容器重启后数据丢失容易在答辩前掉链子。我的血泪经验是虚拟机里的伪分布式最接近真实操作出问题也最好查。如果在虚拟机上安装 hadoop 遇到 SSH 免密登录的问题记得先执行ssh-keygen -t rsa -P -f ~/.ssh/id_rsa生成密钥再把~/.ssh/id_rsa.pub追加进~/.ssh/authorized_keys。网上很多 hadoop 安装与配置的教程卡在start-dfs.sh提示Permission denied基本都是 SSH 免密没配上和 Hadoop 本身没关系。伪分布式的核心配置只有三个文件。core-site.xml里设置默认文件系统为 HDFS 地址hdfs-site.xml里把副本数设为 1因为伪分布式只有一个 DataNode默认副本数为 3 会导致块一直处于 under-replicated 告警mapred-site.xml里把计算框架切成 YARN。很多教程让你改一堆别名和路径其实对毕设来说没必要。!-- core-site.xml -- configuration property namefs.defaultFS/name valuehdfs://localhost:9000/value /property /configuration !-- hdfs-site.xml -- configuration property namedfs.replication/name value1/value /property /configuration !-- mapred-site.xml -- configuration property namemapreduce.framework.name/name valueyarn/value /property /configuration3.2 Maven 项目骨架与三个核心配置文件源码部分最常见的工程结构是一个 Java Maven 工程三个包mapper、reducer、driver外加一个Driver主类调度两个 Job。依赖用hadoop-client一个就够它会带进 HDFS 和 MapReduce 的客户端类。下面是一个最少依赖的pom.xml。project modelVersion4.0.0/modelVersion groupIdcom.course/groupId artifactIdfriend-recommend/artifactId version1.0/version properties maven.compiler.source8/maven.compiler.source maven.compiler.target8/maven.compiler.target /properties dependencies dependency groupIdorg.apache.hadoop/groupId artifactIdhadoop-client/artifactId version3.3.6/version /dependency /dependencies build plugins plugin groupIdorg.apache.maven.plugins/groupId artifactIdmaven-shade-plugin/artifactId version3.2.4/version executions execution phasepackage/phase goalsgoalshade/goal/goals /execution /executions /plugin /plugins /build /project用 Hadoop 3.3.x 而不是 2.x 的原因很简单3.x 的接口更干净String.join之类在 Mapper 里写起来没有兼容问题而且老教程里一堆setJarByClass的坑在 3.x 下依然适用不会更复杂。如果你是照着网上 Java 课程设计案例源码改的注意把源码里的org.apache.hadoop.mapred改成org.apache.hadoop.mapreduce前者是旧 API两者的 Mapper 生命周期和 Context 接口不完全一样混用会直接编译失败。如果直接用了 hadoop 已编译 jar 包记得把HADOOP_HOME和HADOOP_CLASSPATH环境变量配置正确否则运行时大概率报ClassNotFoundException: org.apache.hadoop.mapreduce.Job。Driver 类里两个 Job 要串起来执行第一个 Job 输出到临时目录第二个 Job 把临时目录作为输入最后输出到结果目录。这里最容易踩的坑是“输出目录已存在会直接报错”所以 Driver 里每次跑之前最好把输出目录删掉或者用一个带时间戳的输出路径。我自己习惯在 Driver 里加几行清理代码否则反复调参时要手动去 HDFS 删目录很烦。FileSystem fs FileSystem.get(conf); Path outPath new Path(args[2]); if (fs.exists(outPath)) { fs.delete(outPath, true); }注意输出目录已存在时任务会直接失败所以 Driver 里先删除目标路径是常规操作。这条在 Hadoop 3.x 同样适用。3.3 提交任务到 YARN 的最小命令组编译和提交的完整流程如下。先启动 HDFS 和 YARN再用jps确认进程然后传数据、打包、跑任务、看结果。# 启动两个守护进程组 start-dfs.sh start-yarn.sh # 确认进程都活着至少应该有 NameNode、DataNode、ResourceManager、NodeManager jps # 把数据放上 HDFS hdfs dfs -mkdir -p /recommend/input hdfs dfs -put friends.csv /recommend/input/ # 打包并提交任务 mvn clean package -DskipTests hadoop jar target/friend-recommend-1.0.jar \ com.course.recommend.Driver \ /recommend/input/friends.csv \ /recommend/output/friend_list \ /recommend/output/recommend # 查看推荐结果 hdfs dfs -cat /recommend/output/recommend/part-r-00000hadoop jar后面依次是 jar 包、主类全限定名、三个路径参数。第一个参数是输入后两个是两轮 Job 各自的输出目录。YARN 的 8088 端口的 Web 页面上能看到两个 Job 先后执行的状态建议跑之前记住jps的输出如果提交后一直停留在Running job十有八九是 ResourceManager 没起来或者内存被系统杀掉。到这里一个最小可运行的推荐系统已经通了。你会发现整个链路没有用到任何 Spark、Flume 之类的东西输入就是朴素的文本文件输出也是文本文件这反而让毕业设计的“系统架构图”特别好画数据从 HDFS 进入、经 MapReduce 计算、写回 HDFS。接下来要解决的是“推荐质量”也就是怎么让结果不像一个只会计数的脚本。4. 给推荐结果加权重Jaccard 打分与 TopN 输出4.1 为什么纯共同好友计数会把热门用户推给所有人直接跑完第二轮的输出你会发现一个尴尬的现场那个好友最多、社交最活跃的用户会被推荐给几乎所有人。这不是 bug是计数模型的天性。共同好友数量只度量了“绝对关系数”没考虑“关系的稀缺性”。如果 A 和 B 有 2 个共同好友但 B 本身有 5000 个好友说明 B 是个社交达人这 2 个共同好友可能只是巧合如果 C 和 D 有 2 个共同好友而 D 总共只有 3 个好友那这 2 个共同好友就非常说明问题了。解决这个问题最常用的办法是 Jaccard 相似度。对两个用户 A 和 B定义相似度等于共同好友数除以两人好友集合的并集大小J(A,B) 共同好友数 / |A的好友 ∪ B的好友|。这个值会惩罚那些好友数很大的用户让“小众而精准”的推荐排到前面。在 hadoop 面试题里这个点也经常被拿来问“为什么纯统计不够”答上这一句分数就不一样了。要在 MapReduce 里算 Jaccard困难点在于第二轮 Reducer 只输出了共同好友数没输出两人的好友总数。所以要给打分阶段补信息。常见做法是让第二轮另外再输出一份用户度数表也就是第一轮已经算出来的好友列表长度然后在打分阶段把它作为配置参数或者小表文件读进来。4.2 在 Reducer 里算 Jaccard 相似度分母怎么凑出来一个实现成本很低的方案把用户度数表放进一个MapString, Integer在打分 Reducer 的setup里从配置读取。因为参与推荐的用户规模在课程设计里通常只有几万到几十万直接以字符串形式塞进 Configuration 会超出上限所以更稳妥的做法是先用一个小工具从第一轮输出里再跑一个简单 MR输出用户ID \t 好友数然后在打分阶段用 DistributedCache 把这张表分发到每个节点。public class ScoreReducer extends ReducerText, Text, Text, Text { private final MapString, Integer degree new HashMap(); private final TreeMapDouble, ListString ranking new TreeMap(Collections.reverseOrder()); private int topN 10; Override protected void setup(Context context) { topN context.getConfiguration().getInt(recommend.topn, 10); // 从分布式缓存读用户度数表格式“用户\t好友数” try { URI[] caches context.getCacheFiles(); for (URI uri : caches) { ListString lines Files.readAllLines( Paths.get(uri.getPath())); for (String line : lines) { String[] p line.split(\\t); degree.put(p[0], Integer.parseInt(p[1])); } } } catch (Exception ignored) { } } Override protected void reduce(Text key, IterableText values, Context context) throws IOException, InterruptedException { ranking.clear(); // key目标用户value“候选用户\t共同好友数” for (Text value : values) { String[] p value.toString().split(\\t); String candidate p[0]; int mutual Integer.parseInt(p[1]); int degA degree.getOrDefault(key.toString(), 1); int degB degree.getOrDefault(candidate, 1); // Jaccard 共同好友数 / 并集大小并集 degA degB - mutual double score mutual * 1.0 / (degA degB - mutual); ranking.computeIfAbsent(score, k - new ArrayList()).add(candidate); } emitTopN(context, key); } }这段代码的核心是score mutual / (degA degB - mutual)。degA degB - mutual就是并集大小因为共同好友同时属于两边的好友集合只算一次。分母里对缺失用户默认给 1是为了避免除零同时也是个容忍脏数据的兜底。注意ranking是 TreeMap 并且用了逆序所以分数高的候选会排在前面后面emitTopN只要取前若干个即可。setup里读分布式缓存是标准的“小表广播”手法。如果不想踩 DistributedCache 路径的坑也可以在 Driver 里把这个文件放到 HDFS 上然后用job.addCacheFile(new URI(/recommend/output/degree/part-r-00000))传入节点运行时会在本地拿到同名文件。这个方案比把度数表硬编码到代码里干净得多文档说明里画系统架构图时这条边会让数据流向显得更完整。4.3 用 TreeMap 在 Reduce 端做 TopN 排序最后一个步骤是对每个用户截取 TopN。这里不要在 Reducer 里收集全量数据再Collections.sort因为一个大用户的候选可能有几十万条全量排序的内存开销不划算。正确做法是维护一个容量固定的排行榜TreeMap 本身有序但它的computeIfAbsent会把所有分数都放进去内存还是会涨。所以更稳的写法是限制 TreeMap 的大小。private void addToRanking(String candidate, double score) { if (ranking.size() topN || score ranking.lastKey()) { ranking.computeIfAbsent(score, k - new ArrayList()).add(candidate); // 超过容量时删除分数最低的那批 while (ranking.size() topN) { ranking.pollLastEntry(); } } }这个函数的逻辑是典型的“击穿式 TopN”只有当新分数比当前最低分还高时才插入插入后如果超过容量就删掉分数最低的一个桶。因为 TreeMap 是有序的lastKey()就是最小的分数逆序排列下最后一个元素pollLastEntry()删除的就是最低分桶。这个写法在 hadoop 面试题里对应“如何在 Reduce 端取 TopN 而不 OOM”拿出来讲比一句“用排序”值钱得多。输出阶段要处理一个细节同一个 score 桶里有多个人。如果只按桶数截断可能出现实际推荐人数超过 topN。我的处理是按桶遍历桶内逐个输出累计超过 topN 就整体停掉。这样每个用户最多输出 topN 条不会多。到了这一步整个推荐系统已经具备可用的形态输入是关系数据输出是每个用户的最优候选列表。打分阶段之后整个流程经过“数据清洗 → 好友列表 → 共同好友计数 → Jaccard 打分 → TopN 截断”五步每一层的输出都是上一层的输入这样每一轮结果都能单独检查出了问题也好定位。5. 避坑与排查从伪分布式到集群好友推荐系统最常见的翻车现场5.1 任务一直 RUNNING 不动YARN 页面显示容器反复被 kill现象用hadoop jar提交后日志卡在Running jobYARN 的 8088 端口页面上能看到 Application 状态是 RUNNING但 Container 日志里一堆Killed by External signals。原因伪分布式在一台机器里同时跑四个守护进程默认的 YARN 内存参数是按集群机器配置算的。yarn.nodemanager.resource.memory-mb可能高达 8G而虚拟机总共才给了 4GNodeManager 申请到的内存被内核直接杀掉。还有一个隐蔽原因是容器内存超限判定里包含了虚拟内存Java 堆没超但堆外内存超了。解决先看jps确认 ResourceManager 和 NodeManager 在再调低 YARN 内存并把虚拟内存检查关掉。# etc/hadoop/yarn-site.xml 里追加 property nameyarn.nodemanager.resource.memory-mb/name value2048/value /property property nameyarn.scheduler.minimum-allocation-mb/name value256/value /property property nameyarn.nodemanager.vmem-check-enabled/name valuefalse/value /propertyvmem-check-enabledfalse是伪分布式环境下的常规操作但写到文档里时一定要说明这是为了规避单机虚拟内存限制生产环境不建议关闭。老师看到这句话会觉得你明白自己在做什么而不是抄了一个配置。5.2 结果里又把“已经是好友”的两人推荐了一遍现象推荐结果的某一行是A \t B \t 2可是原始数据里明明存在A,B这条边。原因直连关系的标记没有覆盖所有方向。常见写法是在第二轮 Mapper 里只对“当前用户:好友”写了 -1但两两组合输出的是好友i:好友j两者的 key 方向对不上。比如用户 1 的好友列表是2,3直连标记写的是1:2、1:3而组合输出的是2:3Reducer 根本看不到2:3的 -1于是把已经是好友的 2 和 3 又推荐了一遍。解决在 Mapper 里把直连关系也做成和组合相同的 pair 格式并且利用第一轮补过的反向边让直连 pair 的两个方向都出现。我的做法是维护一张SetString directEdges每处理完一个用户列表就把a:b和b:a都放进去组合时如果 pair 在直连集合里直接跳过。这一步属于算法正确性问题不是性能问题答辩时最容易暴露务必在测试用例里加一条“已知好友不应被推荐”的断言。5.3 全组合导致数据爆炸Reduce 阶段直接 OOM现象某个用户的好友数有 3000第二轮 Mapper 对这个列表做笛卡尔组合瞬间产生约 900 万条中间记录shuffle 阶段网络和磁盘暴涨Reducer 出现GC overhead limit exceeded。原因组合的复杂度是 O(n²)而且每条都会被写入 Map 的输出缓冲区。这在真实社交数据里不是假设而是必然出现的大 V 用户。解决至少做三层优化。第一层在 Mapper 端给好友列表长度设上限比如超过 500 的大列表单独走一条降级逻辑只对它排在最前面的活跃好友做组合第二层加 Combiner把同一个 Map 任务内的相同 pair 先求和减少 shuffle 数据量第三层在 Reducer 里对每个用户维护固定大小的 TopN 结构见第四章的addToRanking不要全量收集再排序。如果还不行就把大 V 用户单独拆成一个 Job用更多 Reduce 处理不要让普通用户被它拖累。5.4 中文用户名读出乱码第一列永远多一个字符现象数据文件是 Windows 记事本或 WPS 保存的 CSV第一行第一列的用户ID解析出来前面有个\uFEFF导致和后续关联不上或者整个文件是 GBK 编码Hadoop 默认按 UTF-8 读全部乱码。原因UTF-8 带 BOM 时Hadoop 的TextInputFormat不会主动去掉文件头的 BOM 标记它把\uFEFF当作第一个字符交给 MapperGBK 文件则是因为默认编码不匹配。解决数据入口统一转成 UTF-8 无 BOM。Linux 下一条命令搞定# 去掉BOM sed -i 1s/^\xEF\xBB\xBF// friends.csv # 检查编码 file friends.csv # 如果不是UTF-8先转码 iconv -f GBK -t UTF-8 friends.csv friends_utf8.csv代码里再兜底一层Mapper 拆完字段后对每个字段做String.replace(\uFEFF, )。不要只依赖外部命令因为答辩时老师可能直接用他手里的文件跑你的 jar你的代码必须对脏数据有容忍度。另外TextOutputFormat写出时会按系统默认字符集编码如果你的代码里new Text的是 UTF-8 字符串而部署机的 LANG 不是 UTF-8输出文件也会出现中文乱码。处理方式是启动任务前export LANGzh_CN.UTF-8或者统一以字节方式拼装输出。5.5 本地伪分布跑得好好的换了机器后结果对不上现象在一台机器上跑出来的推荐结果换到另一台机器或真正的 hadoop 集群搭建环境后输出顺序变了甚至有的用户消失。原因MapReduce 的 Reduce 输出顺序与分区、Task 数量有关本身就不保证全局稳定如果中途加了 Combiner部分求和仍然正确但结果里的推荐顺序会受 shuffle 影响。真正的隐患是代码里使用了依赖进程内状态的写法比如static的 HashMap 或者没清干净的 TreeMap多个 Task 复用 JVM 时会把上一个 key 的数据带到下一个 key。解决把打分和 TopN 都做成“每个 key 独立初始化”所有临时状态放到reduce方法内部或每轮clear。换机器前把输入数据放到 HDFS 上重新跑一遍用结果文件做 diff而不是直接对比命令行打印顺序。这个“换机器结果不一致”的问题在文档里写成一个“系统测试与容错”小节是很漂亮的加分项。如果你后面真要做 hadoop 和 zookeeper 整合实战那一套高可用方案那是另一个量级的工作量毕设不建议碰。6. 文档说明、验证与答辩让老师觉得你“真做过”6.1 毕业设计文档的六个章节怎么组织标题里那句“文档说明”别浪费。文档我建议严格按六章写需求分析、系统设计、系统实现、实验测试、总结展望。需求分析里把推荐场景讲成“基于共同好友的社交推荐”不要吹大词系统设计里放一张分层图HDFS 存储层、MapReduce 计算层、推荐结果展示层实现章节按两轮 MR 代码逐个展开每个方法说清楚输入输出。文档不要贴全部源码贴关键类加数据流向就够了。老师的耐心有限能在他翻完前看到你的核心思路比凑一百页管用。源码加笔记的搭配笔记就是对每个类的设计取舍做一句话说明比如“这里补反向边是为了第二轮组合时方向对称”这种句子比任何概念都打动人。6.2 用三五个人的小数据集验证推荐逻辑小规模验证是让自己信服的唯一办法。拿一份只有 4 个用户的小关系表手算推荐结果再和程序输出对照。例如原始关系为1-2、1-3、2-3、3-4四个用户中 3 的好友列表是1,2,4那么 4 会被推荐给 1 和 2因为共同好友都是 3而 1 和 2 虽然共同好友是 3但两人本来就是好友应被过滤。建议全部自己手算一遍目标用户候选用户共同好友是否直连是否推荐143否推荐243否推荐123是过滤34无是不推荐把这张表连同程序输出一起截进文档是答辩时最有说服力的一页。6.3 一个让系统加分的进阶技巧关联用户资料表如果你还有精力把推荐结果从“用户ID”变成“用户昵称 头像路径”很值得做。做法是用 DistributedCache 再广播一张用户资料表在最后一轮 Reducer 里直接查表拼格式。这一步不改变任何算法逻辑但演示时从 HDFS 直接输出一串 ID和输出可读的昵称观感差别极大。答辩前花一小时把 hadoop 面试题里的高频题过一遍InputSplit 是什么、Combiner 与 Reducer 区别、数据倾斜怎么处理。这三点在这套系统中全部真实遇到过能用自己的项目讲清楚比背答案强太多。我现在做任何 Hadoop 任务前都会先确认输入路径、输出路径和内存参数这三件事这个习惯帮我省了很多次无用功。希望帮到你。本文还有配套的精品资源点击获取