简介基于 Hadoop 的好友推荐系统是一个完整可运行的高分项目面向计算机相关专业在校生、教师及企业开发者适用于毕业设计、课程设计、项目演示与 Hadoop 入门进阶。资源涵盖系统源码、部署文档和全套辅助资料代码经过实际运行验证并获导师认可答辩评审分达95分。压缩包共包含2000个文件约79.5MB其中 java/class 为核心业务逻辑与数据处理代码jsp、css 和 js 构成前端展示层jar 与 xml/properties 负责依赖库与运行配置png/gif 则记录了界面截图和运行效果便于直观理解系统功能。已有162人下载学习。项目内置完整的 Mapper 实现清晰展示好友推荐中距离计算与聚类等关键环节目录结构规范读者可在此基础上修改扩展以实现算法优化或功能定制也能直接用于毕设、课设或初期项目演示。1. 基于 Hadoop 的好友推荐系统这份资源到底能拿来做什么手里有用户行为数据却不知道怎么把可能认识的人算出来——很多人第一反应是写个双层循环算相似度数据量过万就卡死更别提拿去答辩演示。这份基于 Hadoop 的好友推荐系统项目把用户相似度计算、聚类迭代、结果可视化到数据库落库完整跑在 MapReduce 上不是挂了个 Hadoop 名头的单机 Java 程序。从资源里的类名能明显看出CalDistanceMapper 负责算距离、ClusterDataMapper 负责分簇、FindInitDCMapper 负责找初始中心配上 DrawPic 画分群图、DBService 落库和完整部署文档适合正在做课设、毕设或想完整跑通一条 MapReduce 推荐流水线的学生和入门工程师。2. 从类名逆向拆解项目架构十个核心类如何组成一条推荐流水线打开压缩包先别急着跑里面是一批源码加 .class 文件。我拿到项目的第一步永远是先看类名清单因为类的命名能直接暴露系统的边界和设计者的思路。这份资源里的类不多但每个都指向明确职责拼起来就是一条完整的分布式推荐链路。2.1 核心类分工从 Mapper 命名看系统边界先把资源正文里出现的类按职责分组这是拆项目时我习惯先做的一张表类名推断职责判断依据HUtilsHadoop 工具类封装 Configuration、FileSystem 获取H 开头典型的 Hadoop 工具类命名CloudAction云端操作可能对接 HDFS 或对象存储的上传下载Action 后缀一般封装存储读写动作DBService / BaseDAOImpl数据库服务与 DAO 层实现明确持久化职责把计算结果落到 MySQLUtils通用工具向量解析、相似度公式、字符串处理与业务无关的通用方法集合DrawPic结果可视化把用户按簇画成散点图绘图类用于答辩展示和效果验证FindInitDCMapper寻找初始聚类中心DC Data CenterFindInit 明确是初始化逻辑CalDistanceMapper计算用户到聚类中心的距离Cal CalculateDistance 直接点名ClusterDataMapper把用户分配到最近的簇Cluster 是分簇动作DeltaDistanceMapper判断聚类是否收敛Delta 表示变化量对比新旧中心偏移这个分组的逻辑是HUtils、Utils、BaseDAOImpl 是底座DBService 和 CloudAction 负责数据进出四个 Mapper 才是算法核心。一个值得注意的细节是Mapper 占了四个说明这套推荐不是单 job 跑完而是多个 MapReduce 任务串成的流水线——这正是答辩时最能讲出技术含量的部分。2.2 一条好友推荐的完整数据流把类名串起来整个系统的运行逻辑就浮出来了。我先给一个典型的 HDFS 输入样例后面所有计算都从这个格式开始u001 1,0,1,0,0 u002 0,1,1,0,1 u003 1,1,0,0,0每行第一列是用户 ID第二列是特征向量。这个向量的每个位置代表一个兴趣标签比如运动、游戏、阅读、音乐、摄影1 表示感兴趣0 表示不感兴趣。特征向量是多列逗号分隔的维度根据业务标签数量决定十几个到上百个都可能。数据流是这么走的数据准备阶段把用户行为表清洗成上面的向量格式上传到 HDFS第一个 MapReduce job 用 FindInitDCMapper 从样本里挑出 K 个初始聚类中心K 的值在提交任务时通过参数传入进入迭代循环每个循环跑两个 jobCalDistanceMapper 计算每个用户到 K 个中心的距离ClusterDataMapper 把用户归到最近的簇计算完新中心后DeltaDistanceMapper 对比新旧中心的偏移量偏移量小于阈值或迭代次数封顶循环退出输出用户 ID \t 簇 ID的映射表取同一个簇内距离最近的 TopN 个用户作为好友推荐候选通过 DBService 落库DrawPic 用二维坐标把用户按簇着色画出来答辩时一眼能看出分群效果。这个链路里最容易被忽略的是第 5 步好友推荐不是聚类本身而是聚类之后的一个查询动作。同一个簇意味着兴趣相近簇内互相推荐比全局算相似度省一个量级的计算量这是基于聚类的推荐比暴力相似度矩阵聪明的地方。2.3 为什么选 MapReduce 而不是单机跑完好友推荐在小数据量下单机用 DataFrame 处理确实更快这点我不否认。MapReduce 在这个项目里的价值有三个层面第一是数据规模。百万级用户算相似度矩阵是 n² 的存储和计算量单机内存直接爆掉但用户特征向量拆分到 HDFS 分片后每个节点只需要算自己那一份数据到 K 个中心的距离计算量从 n² 降到 n × K这是分布式带来的实质收益。第二是答辩和演示需要。一个完整的 Hadoop 课设如果只写个单机程序评委问你的分布式体现在哪就是送命题。四个 Mapper 的 job 链、DistributedCache 分发中心点、收敛判断循环每个环节都能单独展开讲十分钟。第三是学习价值。伪分布式只有一台机器MapReduce 的启动开销其实比单机还高所以这套代码更适合作为理解分布式计算的学习素材而不是追求极致性能的生产方案。你自己权衡清楚这个边界答辩被追问时才不会露怯。3. 相似度计算与聚类落到 MapReduce关键 Mapper 与参数设计这一章是整套代码的核心也是最值得你在答辩时展开讲的部分。四个 Mapper 里CalDistanceMapper 是每一轮迭代都要跑的它的实现质量直接决定聚类效果。我会先给出一个符合这个项目场景的典型实现思路再解释每个参数怎么调。3.1 CalDistanceMapper把距离计算拆到每个数据分片CalDistanceMapper 的输入是用户向量文件中的一行输出是簇 ID → 用户。难点在于聚类中心怎么传给每个 Mapper——中心点文件很小不可能作为主输入标准做法是放进 DistributedCache让每个 Mapper 在 setup 阶段把中心点读到本地内存。public class CalDistanceMapper extends MapperLongWritable, Text, Text, Text { private MapInteger, double[] centers new HashMap(); Override protected void setup(Context context) throws IOException, InterruptedException { // 从 DistributedCache 读取上一次迭代产出的聚类中心文件 Path[] cacheFiles context.getLocalCacheFiles(); if (cacheFiles ! null cacheFiles.length 0) { readCenters(cacheFiles[0]); } } Override protected void map(LongWritable key, Text value, Context context) throws IOException, InterruptedException { // 输入行格式userId \t feature1,feature2,feature3,... String line value.toString(); String[] parts line.split(\t); if (parts.length 2) { return; } String userId parts[0]; double[] vector parseVector(parts[1]); int nearestCenter findNearestCenter(vector); // 输出簇ID - 用户ID 原特征向量供下一轮计算新中心 context.write(new Text(String.valueOf(nearestCenter)), new Text(userId \t parts[1])); } private int findNearestCenter(double[] vector) { int bestId -1; double bestDistance Double.MAX_VALUE; for (Map.EntryInteger, double[] entry : centers.entrySet()) { double distance euclideanDistance(vector, entry.getValue()); if (distance bestDistance) { bestDistance distance; bestId entry.getKey(); } } return bestId; } }这段代码里三个关键点需要你理解透。DistributedCache 就是把一个 HDFS 小文件分发到每个 Task 节点本地setup 阶段读一次整轮 map 都能用避免了每个用户都远程拉一次中心点文件的网络开销。findNearestCenter 是暴力遍历 K 个中心时间复杂度 O(K)这里的 K 建议取 5 到 20 之间太大则每轮迭代耗时成倍增长。最后 context.write 输出的 value 里带着原始特征向量是为了下一轮在 Reduce 端重算新的中心点。3.2 迭代调度与收敛判断FindInitDC 和 DeltaDistance 的分工聚类是个迭代过程一个 MapReduce job 只能跑一轮所以必须要有一个 Driver 类控制循环。常见做法是用一个 while 循环反复提交 job直到 DeltaDistanceMapper 算出的中心偏移量小于阈值。这套流程的骨架大致是这样String centersPath /user/hadoop/centers/current; String outputPath /user/hadoop/cluster/output; while (iteration maxIterations) { Job job Job.getInstance(config); job.setMapperClass(CalDistanceMapper.class); job.setReducerClass(ClusterDataReducer.class); job.setMapOutputKeyClass(Text.class); job.setMapOutputValueClass(Text.class); // 把上一轮的中心文件打进缓存 job.addCacheFile(new URI(centersPath /part-r-00000)); job.waitForCompletion(true); double delta calculateDelta(centersPath, newCentersPath); if (delta convergenceThreshold) { break; } // 用新中心覆盖旧中心下一轮迭代使用 iteration; }这段代码是 Driver 层的典型写法有几个参数值得细说。maxIterations 建议设 30 到 50防止中心点震荡导致死循环convergenceThreshold 一般取 0.01 或 0.001数据做过归一化的情况下这个量级比较合理calculateDelta 是读取新旧两个中心文件逐簇计算欧氏距离之和这个动作可以由 DeltaDistanceMapper 单独跑一个 job 完成也可以直接在 Driver 里读文件算——小数据量时后者更省时间。FindInitDCMapper 发挥作用的地方是迭代之前它从全量数据里随机挑 K 个用户向量作为初始中心。这里有个值得注意的坑随机初始化的聚类结果不稳定同一份数据跑两次可能得到不同分群。如果要保证可复现可以在 Driver 里设置随机种子或者用最简单的 K-Means 思路——先随机选第一个中心然后按距离平方加权的概率选后面的中心这个改动对聚类质量的提升非常明显。3.3 距离度量与特征向量的取舍欧氏还是余弦这是我在拆项目时一定会思考的问题用户兴趣向量是 0/1 稀疏向量用欧氏距离还是余弦相似度两者差别很大。欧氏距离对向量的绝对长度敏感两个用户一个关注了 50 个标签、另一个关注了 5 个标签哪怕兴趣重合度再高欧氏距离也可能偏大余弦相似度只看方向不看长度适合处理这种高维稀疏的 0/1 兴趣向量。实际实现中余弦相似度转距离的公式是 distance 1 - cosineSimilarity相似度越接近 1距离越接近 0。如果你的特征向量里除了兴趣标签还混入了连续值特征比如登录时长、互动次数那最佳实践是把连续值先归一化到 0 到 1再混合使用欧氏距离。归一化这个动作看似不起眼但缺失它会造成一个典型故障一个维度数值特别大比如登录次数几千次直接主导了整个距离计算其他几十个标签维度全都失效聚类结果挤成一坨。特征向量的构造同样有讲究。资源里的向量是逗号分隔的定长格式如果原始数据里有缺失维度千万不要直接补 0 了事——0 和没有数据在距离计算里是两回事。我一般会把缺失维度单独编码或者在预处理阶段用该维度的均值填充然后在提交前检查一遍有没有整行都是一串 0 的用户这种用户算出来到哪个中心都差不多属于噪声数据可以先剔除。另一个高频参数是 Reducer 数量。ClusterDataReducer 负责把同一个簇的用户汇总然后重新计算中心向量Reducer 数设太少会内存溢出设太多会产生大量小文件。经验值是每个 Reducer 处理 1 到 2 万个用户你可以按这个估算。还有 Combiner建议加上但逻辑要保守——Combiner 只能做局部汇总绝不能改变输出的 key-value 语义否则结果会出错。4. 伪分布式搭建与部署流程从 Hadoop 安装到任务提交的完整步骤拿到项目源码之后最大的坎就是环境。很多同学在 Windows 上把代码跑通了一小半到 Hadoop 环节就翻车。这一章我按自己部署这套资源的标准流程来写每一步都是踩过坑后固定的操作顺序。4.1 环境准备与版本选择版本搭配是整套部署里最影响心情的环节。我的建议是 JDK 1.8 Hadoop 2.10.x这个组合是全网资料最丰富、踩坑记录最全的搭配。Hadoop 3.x 本身不差但 3.x 的 YARN 时间线服务、联邦模式这些新特性对新手不友好网上能搜到的排错帖子也少一个量级。操作系统选 CentOS 7 或 Ubuntu 18.04 都行内存 8GB 以上比较稳妥伪分布式加一堆中间数据很容易吃内存。我一般会在 /opt 下建一个 hadoop 目录专门放解压文件数据目录单独挂到 /data/hadoop不要放在系统盘根目录。这样后续磁盘满了这是必然事件清理比较安全。# 解压安装 Hadoop以 2.10.2 为例 tar -zxvf hadoop-2.10.2.tar.gz -C /opt/hadoop cd /opt/hadoop ln -s hadoop-2.10.2 current # 配置环境变量追加到 /etc/profile export HADOOP_HOME/opt/hadoop/current export PATH$PATH:$HADOOP_HOME/bin:$HADOOP_HOME/sbin export JAVA_HOME/usr/lib/jvm/java-1.8.0-openjdk # 按实际路径改 # 验证安装 hadoop version注意 JAVA_HOME 的路径每台机器都不一样用which java或者readlink -f $(which java)追到真实路径再填。很多人栽在 JAVA_HOME 配成了 jre 路径上然后启动 NameNode 时报JAVA_HOME is not set实际上就是路径解析不到 java 可执行文件。4.2 核心配置文件逐个改伪分布式和真正的集群配置差别不大主要是把副本数改成 1、把内存参数调小。总共要改五个文件我按顺序给。core-site.xml 是最先要动的它决定了 NameNode 的地址和临时目录configuration property namefs.defaultFS/name valuehdfs://localhost:9000/value /property property namehadoop.tmp.dir/name value/data/hadoop/tmp/value /property /configurationfs.defaultFS 写 localhost 还是写机器名取决于 /etc/hosts 的配置。我踩过的坑是 /etc/hosts 里把 hostname 配成了localhost localhost.localdomain然后又单独加了一行映射——这种混乱会导致 DataNode 注册时 NameNode 不认日志里全是连接超时。建议 /etc/hosts 里只留一行127.0.0.1 localhost加你的实际主机名映射。hdfs-site.xml 里最关键的是副本数伪分布式必须设为 1默认 3 的话数据块永远写不满副本数NameNode 会一直处于安全模式外表看起来就是 HDFS 一直Safe mode is ONconfiguration property namedfs.replication/name value1/value /property property namedfs.namenode.name.dir/name value/data/hadoop/namenode/value /property property namedfs.datanode.data.dir/name value/data/hadoop/datanode/value /property /configuration接着是 yarn-site.xml这组参数管的是计算资源也是后面Container 反复被杀的根源configuration property nameyarn.nodemanager.resource.memory-mb/name value8192/value /property property nameyarn.scheduler.minimum-allocation-mb/name value512/value /property property nameyarn.nodemanager.vmem-check-enabled/name valuefalse/value /property /configuration最后是 mapred-site.xml这个文件默认不存在要从模板复制cp $HADOOP_HOME/etc/hadoop/mapred-site.xml.template $HADOOP_HOME/etc/hadoop/mapred-site.xml然后写入configuration property namemapreduce.framework.name/name valueyarn/value /property property namemapreduce.job.reduces/name value2/value /property /configurationmapreduce.job.reduces 设 2 是保守值如果你机器内存不够一个 job 起太多 Reducer 会直接把内存挤爆。4.3 集群启动、格式化与任务提交配置改完正式的启动顺序是固定的先格式化 NameNode再启动 HDFS再启动 YARN最后用 jps 验证进程。这一步我要求自己每次都严格按顺序来因为启动顺序错了会留下一堆莫名其妙的缓存状态。# 首次格式化 NameNode只能执行一次 hdfs namenode -format # 启动 HDFS 和 YARN start-dfs.sh start-yarn.sh # 检查进程正常情况下应该看到 5 个 # NameNode、DataNode、SecondaryNameNode、ResourceManager、NodeManager jps如果 jps 里缺了 DataNode八成是格式化之前已经启动过一次集群或者格式化执行了两次导致 NameNode 和 DataNode 的 clusterID 不一致。解决办法是把 /data/hadoop 下 namenode 和 datanode 目录全删掉重新格式化再启动记住格式化只能做一次。集群起来之后把用户向量数据传上去然后提交项目 jar。注意输入目录要先建好hdfs dfs -mkdir -p /user/hadoop/input hdfs dfs -put user_vectors.txt /user/hadoop/input/ hadoop jar friend-recommend-1.0.jar com.example.driver.RecommendDriver \ -D cluster.k8 \ -D cluster.maxIterations30 \ /user/hadoop/input /user/hadoop/output-D 参数会被 Hadoop 框架放进 Configuration 里你的 Driver 代码里通过context.getConfiguration().getInt(cluster.k, 8)就能读出来。这是 MapReduce 传参的标准姿势参数放在 job 命令里而不是硬编码在代码中换数据集调参才不用重新编译。4.4 部署文档里容易被跳过的三个细节第一是 SSH 免密。伪分布式虽然只有一台机器但 Hadoop 的 start 脚本仍然会尝试通过 SSH 连接 localhost没配免密的话会卡在输入密码。执行ssh-keygen -t rsa -P -f ~/.ssh/id_rsa cat ~/.ssh/id_rsa.pub ~/.ssh/authorized_keys一次搞定。第二是防火墙。CentOS 7 默认 firewalld 是开着的虽然伪分布式内部通信走 localhost 不受影响但如果你后面要用宿主机浏览器访问 NameNode 的 50070 端口看 UI防火墙必须放行或直接关掉。第三是 swap 分区。集群机器上 swap 会导致 JVM 卡顿看起来像进程假死。如果装系统时已经开了 swap建议临时关闭swapoff -a让 Hadoop 的 Java 进程老实待在内核里。5. 部署与运行避坑六个让新手翻车的真实故障这一章写的每一条都是我在跑同类项目时实际遇到、或者帮别人排过的故障。每一条都按现象、原因、解决的顺序写你可以直接对照排查。5.1 现象jps 看不到 DataNode日志里全是连接失败原因格式化过两次 NameNode。第一次格式化生成了一个 clusterID第二次格式化又生成一个全新的 clusterIDDataNode 里保存的还是旧 ID注册时就被 NameNode 拒收了。更隐蔽的情况是你先启动过集群DataNode 目录已经初始化再执行格式化同样会导致 ID 不一致。解决停掉所有进程删除 namenode 和 datanode 的数据目录只格式化一次再启动。操作前先备份数据这个命令没有后悔药rm -rf /data/hadoop/namenode /data/hadoop/datanode。从那以后我每次格式化前都强制检查一遍 data 目录是否为空避免手滑。5.2 现象任务提交后一直停留在 RunningContainer 反复被杀NodeManager 日志里报内存超限原因yarn-site.xml 里没配内存上限或者配了但和机器实际内存不匹配。YARN 默认给每个 Container 分配的内存远大于你机器能提供的虚拟内存检查开启时NodeManager 会不断杀 Container 来释放资源任务就在 RUNNING 和 KILLED 之间死循环。解决把 yarn.nodemanager.resource.memory-mb 调到机器物理内存的 70% 左右同时把 yarn.nodemanager.vmem-check-enabled 设为 false。还要检查 mapred-site.xml 里的 mapreduce.map.memory.mb 和 mapreduce.reduce.memory.mb默认值 1024MB如果你的特征向量大Reduce 阶段内存不够会直接 OOM。5.3 现象聚类结果全部挤进一个簇其他簇为空原因特征向量没有归一化。某个维度的数值范围是 0 到 10000比如登录次数其他维度是 0/1欧氏距离被这个维度完全支配所有用户计算出来的最近中心都是同一个。另一个常见原因是 K 设得太小或者初始中心选到了密集区域的同一个点。解决预处理时把所有特征列都缩放到 0 到 1。对 0/1 特征不需要动对连续值特征做 min-max 归一化。然后把初始中心的选择从纯随机改成 K-Means 思路这一步能显著减少空簇现象。验证方法跑完看每个簇的用户数统计如果某个簇用户数是 0优先检查归一化而不是调 K。5.4 现象中文用户名或兴趣标签在结果文件里显示乱码原因数据文件在 Windows 上用记事本编辑过保存成了带 BOM 的 UTF-8或者干脆是 GBK 编码。Hadoop 的 TextInputFormat 默认按 UTF-8 解码GBK 的中文读了就是乱码BOM 头还会混进第一个字段。解决所有数据文件统一用 UTF-8 无 BOM在 Linux 上重新转一遍iconv -f GBK -t UTF-8 user_vectors.txt user_vectors_utf8.txt再执行dos2unix把行尾的\r\n去掉。这个坑很隐蔽因为 MapReduce 不会报错只是你最后看推荐结果的时候发现用户名全是???。5.5 现象跑了几轮迭代后磁盘空间突然爆掉原因每次迭代都往 HDFS 写一个完整的输出目录几十个中间目录叠加加上海量小文件占满 NameNode 内存。如果你还把日志都存成一份磁盘很快见底。解决中间结果目录固定复用一个路径每次迭代先删掉上一轮的输出再写最终结果单独放到一个带时间戳的目录里做保留。另外在 hdfs-site.xml 里把 dfs.datanode.du.reserved 设成比如 10GB这是给 DataNode 磁盘留的保底空间防止磁盘写满导致 NameNode 直接进入安全模式。5.6 现象启动时提示本地库平台不匹配native library 加载失败原因Hadoop 自带的 native 库是为特定平台编译的你用的 Linux 发行版或 glibc 版本不匹配导致 Snappy、lzo 这些压缩库用不了。解决这个告警可以安全忽略不压缩也能跑只是性能差一点。如果看不惯可以从 Hadoop 源码里重新编译 native 库放进去但对学习和课设来说没有性价比我建议直接跳过这个告警把精力留给真正影响结果的故障。6. 验证与进阶把推荐结果量化再改造成增量更新项目跑通只是第一步答辩或实际使用面前还有一个问题你怎么证明这个推荐系统效果好DrawPic 画出来的散点图能直观说明分群效果但评委可能会追问量化指标你需要一个数字。轮廓系数是最适合这个场景的验证方式。轮廓系数算起来不复杂对每个用户算出它到同簇所有其他用户的平均距离 a再算出它到最近邻簇所有用户的平均距离 b轮廓系数等于 (b - a) / max(a, b)。结果在 -1 到 1 之间越接近 1 说明簇内越紧、簇间越远聚类质量越好。0 附近说明用户正好落在两个簇的边界负值说明这个用户大概率被分错了簇。你可以在 DrawPic 之前先用一个简单的脚本算整体平均轮廓系数然后对照散点图看比自己盯着图猜高效得多。进阶的方向是把这套离线全量计算改造成增量更新。全量 job 跑一次要几分钟甚至更久但新用户注册后不可能等这么久。常见做法是跑完一次全量后把用户 ID → 簇 ID → 簇中心向量落库到 MySQL新用户进来时只用它自己的特征向量和 K 个簇中心算一次距离就近入簇然后从同簇里取 TopN 返回判断。这把这个召回从 MapReduce 全量计算变成了一个 O(K) 的纯内存计算响应时间从分钟级降到毫秒级。数据量继续膨胀、需要迁移集群时hadoop distcp 是绕不开的工具它用 MapReduce 做分布式拷贝比 HDFS 自带的 get/put 快得多hadoop distcp -skipCRC -m 8 -update \ hdfs://old-cluster:9000/user/hadoop/data \ hdfs://new-cluster:9000/user/hadoop/datadistcp 的 -skipCRC 跳过 CRC 校验适合已经验证过的数据能省不少时间-m 8 控制并行 Map 数可以按集群节点数往下调-update 只拷贝源端有而目标端没有或已变化的文件增量迁移就靠它。迁移完第一件事是检查目标端的文件数量和大小和源端做对比不要只看 distcp 退出码为 0 就以为万事大吉。我自己第一次跑这套项目时就栽在最基础的格式化那一步——先启动了 DataNode 再 format导致 clusterID 对不上DataNode 死活起不来日志翻到凌晨两点才发现是顺序问题。从那以后我每次换机器重新部署都强制走一遍自己的检查单先清数据目录、再格式化、再启动集群一步都不会乱这套习惯帮我省掉了大量排错时间。希望帮到你。本文还有配套的精品资源点击获取
阅读完成 · 觉得有帮助?