不少人看到“SpringBoot Hadoop 推荐系统”这三个词凑在一起第一反应是这不就是典型的毕设标题配置吗但真把这个题目落到实处要趟的坑远比想象中多。我前阵子刚好完整做完了一个基于SpringBoot和Hadoop的健康饮食推荐系统从选题拆解、环境搭建到算法落地、接口联调都走了一遍。这篇文章把我从零到一的全过程、关键代码、踩坑记录都整理出来给准备做类似系统的同学一份可以直接参考的路线图。先说清楚这个系统是干什么的。它不是一个简单罗列菜谱的网站而是收集用户的身高、体重、年龄、运动习惯、健康目标等信息结合食材和菜品的营养数据每天自动给用户生成一份符合热量需求和营养配比的三餐推荐。后端服务用SpringBoot搭建提供登录、健康档案维护、推荐查询、饮食记录等接口Hadoop承担的是“数据仓库”角色存用户行为日志MapReduce负责离线计算用户相似度、菜品热度这些推荐系统需要的基础数据。整体是一个比较经典的“在线服务 离线计算”架构。如果你正好在做相关毕设或者想拿这个题目练手这篇文章会把系统拆分、技术选型、核心实现、常见问题都讲透。1. 项目概述与整体设计思路1.1 这个系统到底解决了什么问题先说需求侧。现在普通人知道“要健康饮食”但打开外卖App或者菜谱网站看到的是几万道菜根本不知道怎么选。网上虽然有各种“减脂食谱”“增肌食谱”但都是给一个固定人群用的不会结合你自己的BMI、基础代谢率和运动量做调整。同样的食谱让一个久坐程序员和一个每天跑步一小时的人去执行效果完全是两回事。所以这个系统的核心价值在于基于用户个人身体数据和饮食目标做个性化的每日推荐。从技术角度看这个题目也很有意思。它既有传统的Web CRUD用户、菜品管理、记录管理又有算法推荐引擎还有大数据处理Hadoop存储与离线统计一个项目把Web开发、数据库设计、算法设计和分布式处理全覆盖了。这也是为什么这类题目在毕设里经久不衰——它能让评委看到你具备全栈开发思路而不是只会写个增删改查。1.2 为什么是SpringBoot Hadoop这对组合选SpringBoot没什么好纠结的。它是现阶段Java后端最主流的框架内置Tomcat简化配置MyBatis-Plus、Redis、JWT这些常用组件都有成熟的starter开发接口特别快。前后端分离也好配合做一个Vue页面就能直接对接网上的教程也最多遇到问题基本都能搜到答案。Hadoop在这个项目里扮演的角色需要想清楚这也是很多新手容易犯糊涂的地方。推荐系统实时给用户出结果Hadoop本身并不擅长毫秒级响应——它是离线批处理框架。所以实际设计里推荐链路分成两条一条是离线链路每天凌晨用MapReduce处理全天的用户行为日志跑出用户相似度矩阵、菜品热度排名、营养统计数据写入Redis另一条是在线链路用户请求推荐接口时SpringBoot优先查Redis里离线计算好的结果查不到就触发基于营养学规则的实时兜底推荐。这种“离线计算 在线查询”的组合既发挥了Hadoop处理海量日志的优势又保证了用户请求的响应速度。不是所有环节都要让Hadoop参与这也是真实工业界最常见的做法。这么做还有个额外好处Hadoop离线处理的结果数据集是固定的算法产物线上服务只管读两边解耦开发和调试都轻松很多。1.3 系统模块与数据流全景我把系统拆成六个模块模块核心职责主要技术点用户模块注册登录、个人中心SpringBoot JWT健康档案模块身高体重年龄、运动等级、健康目标管理MyBatis-Plus 操作 MySQL菜品管理模块菜品、食材、营养素数据的维护爬虫采集 人工清洗行为数据模块用户对菜品的评分、收藏、点击记录MySQL表 日志上报接口推荐引擎模块离线协同过滤 营养规则推荐MapReduce Redis统计报表模块营养摄入分析、饮食趋势ECharts MySQL聚合数据流向是一条闭环用户在页面上产生行为浏览、评分、收藏→ 行为明细写入MySQL → 定时任务每晚会话日志抽取到HDFS → MapReduce跑出推荐结果表 → 结果写入Redis → 次日用户打开App推荐接口直接从Redis取数据。这套架构最核心的设计点是Hadoop并不参与实时请求它只是“夜晚工厂”白天负责把计算结果输送出去。想清楚这一点整个系统的主线就清晰了。2. 开发环境准备与Hadoop集群搭建2.1 Linux虚拟机里的Hadoop伪分布式环境搭建Hadoop最舒服的跑法还是Linux。我用VMware装了Ubuntu 18.04分配2核4G内存就够了。不用搭真正的分布式集群那是资源浪费——毕设和练手项目跑通伪分布式模式完整体验NameNode、DataNode、YARN流程已经满足需求。先把JDK装好。Hadoop对JDK版本有要求我用的是JDK 1.8Hadoop版本2.10.2。直接解压配置环境变量即可export JAVA_HOME/usr/lib/jvm/java-8-openjdk-amd64 export HADOOP_HOME/opt/hadoop-2.10.2 export PATH$PATH:$HADOOP_HOME/bin:$HADOOP_HOME/sbin接下来是SSH免密登录伪分布式模式下NameNode要通过SSH启停DataNode每次输密码会疯掉ssh-keygen -t rsa -P -f ~/.ssh/id_rsa cat ~/.ssh/id_rsa.pub ~/.ssh/authorized_keys然后改四个配置文件。core-site.xml里指定NameNode地址和临时目录hdfs-site.xml里设置副本数为1并指定NameNode和DataNode目录yarn-site.xml开启yarn集群管理mapred-site.xml指定用yarn作为运行框架。核心配置如下!-- core-site.xml -- configuration property namefs.defaultFS/name valuehdfs://localhost:9000/value /property property namehadoop.tmp.dir/name value/opt/hadoop-2.10.2/tmp/value /property /configuration首次启动前先格式化NameNode然后依次启动hdfs namenode -format start-dfs.sh start-yarn.sh用jps检查看到NameNode、DataNode、SecondaryNameNode、ResourceManager、NodeManager这五个进程全都活着再用浏览器访问http://localhost:50070看到HDFS管理界面环境就算通了。这里有个经典教训hdfs namenode -format这个命令最多执行一次跑完整个环境生命周期都不要再去碰它。重复格式化会引发NameNode和DataNode的clusterID不一致导致DataNode进程反复启动失败这个坑我后文会详细讲。2.2 Windows下使用IDEA连接Hadoop的常见坑开发机是WindowsIDEA里写的MapReduce和HDFS操作代码要连虚拟机的Hadoop这里面的坑比想象中多得多。最常见的是启动任何HDFS相关程序直接报null空指针或者提示Failed to locate the winutils binary in the hadoop home directory。问题根源是Hadoop的Windows本地依赖它需要winutils.exe和hadoop.dll这两个文件才能正常运行。解决办法是下载对应版本的工具包放到一个本地目录比如D:\hadoop-common-2.10.2然后设置环境变量HADOOP_HOMED:\hadoop-common-2.10.2 PATH%HADOOP_HOME%\bin需要提醒的是Hadoop官方根本不提供Windows编译版网上能找到的都是第三方编译的所以千万不要去官网找winutils找不到的。下载时务必确认版本和你用的Hadoop一致2.10.2就用对应的winutils包混版本经常出兼容问题。Windows下还常遇到一个诡异的权限异常调用fileSystem.mkdirs()时报Permission denied。这是跨平台引起的用户身份映射错位HDFS把当前Windows用户当成了HDFS操作者而这个用户没有写权限。临时解法是在虚拟机里执行hdfs dfs -chmod -R 777 /更体面的解法是在hdfs-site.xml里关闭权限检查property namedfs.permissions.enabled/name valuefalse/value /property如果你就图省事最省心的方案是开发和调试Hadoop相关代码时直接在Linux虚拟机里用mvn package打jar包然后hadoop jar提交流程跑Windows只用IDEA写代码。这样省掉一半的兼容性折腾我就是这么干的。2.3 SpringBoot工程结构与核心依赖配置工程名我起的diet-recommend-system目录结构是标准Maven分包diet-recommend-system/ ├── src/main/java/com/example/diet/ │ ├── controller/ # 接口层 │ ├── service/ # 业务逻辑 │ ├── mapper/ # MyBatis-Plus持久层 │ ├── entity/ # 数据库实体 │ ├── config/ # 配置类 │ ├── hadoop/ # Hadoop客户端操作 │ └── recommend/ # 推荐算法 ├── src/main/resources/ │ ├── mapper/ # XML映射文件 │ └── application.yml └── pom.xmlpom.xml里的依赖比较常规最主要的几个parent groupIdorg.springframework.boot/groupId artifactIdspring-boot-starter-parent/artifactId version2.7.18/version /parent dependencies dependency groupIdorg.springframework.boot/groupId artifactIdspring-boot-starter-web/artifactId /dependency dependency groupIdcom.baomidou/groupId artifactIdmybatis-plus-boot-starter/artifactId version3.5.3/version /dependency dependency groupIdorg.apache.hadoop/groupId artifactIdhadoop-client/artifactId version2.10.2/version /dependency dependency groupIdorg.springframework.boot/groupId artifactIdspring-boot-starter-data-redis/artifactId /dependency dependency groupIdcom.auth0/groupId artifactIdjava-jwt/artifactId version4.4.0/version /dependency /dependenciesapplication.yml里配置MySQL数据源、Redis连接和HDFS参数spring: datasource: url: jdbc:mysql://localhost:3306/diet_db?useUnicodetruecharacterEncodingutf8serverTimezoneAsia/Shanghai username: root password: 123456 redis: host: localhost port: 6379 hadoop: hdfs-uri: hdfs://192.168.1.100:9000HDFS的操作类我会封装一个HdfsService负责文件上传、下载和读取。实际上这个项目里HDFS的核心用途是存储每天导出的行为日志文件MapReduce直接读HDFS上的文件做计算写完之后结果表再回写到MySQL和RedisHDFS在这里是“日志仓库”而不仅仅是摆设。3. 核心业务模块设计与数据库建模3.1 用户健康档案模块用户注册流程结束后系统引导用户完善健康档案。这是整个推荐系统最前面的“输入”档案的质量直接决定推荐效果。用户健康档案表主要字段字段类型说明user_idbigint用户IDgendertinyint性别1男/2女heightdecimal(5,1)身高cmweightdecimal(5,1)体重kgageint年龄exercise_leveltinyint运动等级1-5targettinyint健康目标1减脂/2增肌/3保持dietary_prefvarchar(255)口味偏好清淡/重口/素食等allergenvarchar(255)过敏原虾/花生等这些字段没一个是多余的。身高体重用来算BMI和基础代谢率运动等级用来算每日消耗总热量健康目标决定热量盈余或缺口过敏原用来过滤不安全菜品。设计表的时候我就想好了推荐引擎不能只吃营养数据满足用户的安全偏好至少跟营养达标同等重要。3.2 菜品与营养素数据设计菜品表是系统的“货架”字段设计要同时服务于搜索过滤和推荐排序。最核心的是营养素字段和标签字段CREATE TABLE food ( food_id BIGINT PRIMARY KEY AUTO_INCREMENT, name VARCHAR(100), category VARCHAR(50), -- 早餐/午餐/晚餐/加餐 cuisine VARCHAR(50), -- 家常菜/川菜/粤菜等 calories DECIMAL(8,1), -- 千卡 protein DECIMAL(8,1), -- 克 fat DECIMAL(8,1), -- 克 carbs DECIMAL(8,1), -- 克 fiber DECIMAL(8,1), -- 克 tags VARCHAR(255), -- 低脂|高蛋白|无糖 allergens VARCHAR(255), -- 含麸质|含花生 image_url VARCHAR(255), create_time DATETIME );数据来源主要是两个一个是从公开营养数据库抓取食物营养数据比如中国食物成分表另一个是从菜谱网站爬常见菜品的做法和热量估算。爬下来的数据质量参差不齐很多是半成品需要清洗。我写了一个Python脚本把同一种食材不同写法的名称统一比如“土豆”“马铃薯”“洋芋”统一成“土豆”热量值做区间校验超过合理范围的打回重新查。菜品标签需要格外上心。比如“鸡胸肉炒西兰花”标签我打的是“高蛋白|低脂|减脂友好”“红烧肉”打的是“高热量|高脂肪|偶尔食用”。标签是规则推荐的匹配基础如果标签不准整个规则引擎的输出都会失真。3.3 评分与行为数据采集推荐系统最缺的是真实反馈数据。新系统没有用户也就没有评分协同过滤没法跑。所以我在设计的时候把“行为即评分”作为基础思路用户浏览菜品是弱反馈收藏是中反馈主动评分是强反馈。数据库表记录所有这些行为CREATE TABLE user_food_behavior ( id BIGINT PRIMARY KEY AUTO_INCREMENT, user_id BIGINT NOT NULL, food_id BIGINT NOT NULL, behavior_type TINYINT COMMENT 1-浏览 2-收藏 3-评分, score DECIMAL(3,1) COMMENT 评分1-5behavior_type3时有值, create_time DATETIME );前端在菜品详情页埋点用户打开详情页、点收藏、提交评分各发送一次异步请求到/api/behavior/report。夜间定时任务把这些行为数据抽取出来清洗重复记录和异常值比如一个人在1秒内刷了50条浏览记录明显是脚本行为然后合并成用户-菜品评分矩阵导出到HDFS。这里有个小技巧浏览、收藏、评分三种行为的权重不同合并到一个评分矩阵时不能简单取平均值。我用的映射是浏览计2分收藏计3分评分按原始值乘以2。这样一个用户的最终评分范围是0-10区分度比直接拿1-5评分做协同过滤好得多。4. 推荐引擎的实现从协同过滤到营养规则4.1 基于用户的协同过滤相似的人吃相似的东西推荐引擎的离线主算法我选的是基于用户的协同过滤UserCF核心假设是跟你口味、健康目标相似的用户他们喜欢吃的菜品也大概率适合你。计算分为三步第一步从行为数据构建用户-菜品评分向量。每个用户对所有评过分的菜品生成一个特征向量向量的每一维是一道菜值就是该用户对这道菜的最终得分。第二步计算用户之间的相似度。我用的皮尔逊相关系数Pearson Correlation Coefficient。公式是sim(u, v) Σ[(r_ui - avg_u) * (r_vi - avg_v)] / sqrt(Σ[(r_ui - avg_u)^2] * Σ[(r_vi - avg_v)^2])其中r_ui是用户u对菜品i的评分avg_u是用户u所有评分的平均值。用平均值做中心化处理可以消除不同用户打分宽严尺度不同的问题——有人习惯打4分以上有人喜欢吃也打3分不用平均值中心化的话会出现“虚高用户霸榜”的问题。第三步为目标用户选出TopN相似用户基于相似用户的评分加权生成推荐候选集。最终推荐得分计算score_rec(u, i) Σ[sim(u, v) * r_vi] / Σ|sim(u, v)|简单说相似度越高的用户他对菜品的评分在推荐结果中权重越大。最后取前20名未吃过的菜品作为候选把评分最高的10个作为离线推荐结果。这一步我用MapReduce实现。第一个Job负责按用户聚合行为数据输出userId - (foodId, score)格式第二个Job计算用户对之间的相似度这里需要自连接操作也就是把每个用户的评分向量作为一行两两组合计算皮尔逊相关系数。数据量大时这种pair-wise计算很占资源但毕设数据集级别几百个用户几千道菜完全没问题。计算代码的核心在相似度计算部分我当时写的简化版本public double pearsonSimilarity(MapLong, Double userRatings, MapLong, Double otherRatings) { SetLong commonItems new HashSet(userRatings.keySet()); commonItems.retainAll(otherRatings.keySet()); if (commonItems.size() 2) { return 0.0; // 共同评分数太少相似度不可靠 } double avgU commonItems.stream() .mapToDouble(userRatings::get).average().orElse(0.0); double avgV commonItems.stream() .mapToDouble(otherRatings::get).average().orElse(0.0); double numerator 0.0, denomU 0.0, denomV 0.0; for (Long itemId : commonItems) { double uDiff userRatings.get(itemId) - avgU; double vDiff otherRatings.get(itemId) - avgV; numerator uDiff * vDiff; denomU uDiff * uDiff; denomV vDiff * vDiff; } if (denomU 0.0 || denomV 0.0) { return 0.0; } return numerator / Math.sqrt(denomU * denomV); }跑完MapReduce后结果写入user_similarity和user_recommend两张MySQL表。注意这里的设计MapReduce不负责给用户实时推荐它只是把推荐“候选池”算好真正的筛选和过滤留在线上的SpringBoot服务里做。4.2 基于营养学的规则推荐不说算法的兜底方案协同过滤有个天生的毛病冷启动。新用户没有行为数据没有任何相似用户算出来的推荐是空的。这种情况不能给用户甩一个“暂无数据”必须有一套不需要历史行为的推荐方案。这就是规则推荐。规则推荐的逻辑很简单根据用户健康档案算出一日热量总目标和宏量营养素比例再从菜品库中按规则筛选组合出符合条件的三餐。每日总热量消耗TDEETotal Daily Energy Expenditure的计算公式是基础代谢率BMR 男性BMR 10 * 体重kg 6.25 * 身高cm - 5 * 年龄 5 女性BMR 10 * 体重kg 6.25 * 身高cm - 5 * 年龄 - 161 Mifflin-St Jeor公式 TDEE BMR * 运动系数 运动系数久坐1.2 / 轻度运动1.375 / 中度运动1.55 / 高强度1.725 / 极高强度1.9然后根据健康目标调整热量目标日摄入热量三大营养配比碳水:蛋白质:脂肪减脂TDEE * 0.84:3:3增肌TDEE * 1.14:3:3保持TDEE * 1.05:2:3以我为例子男性体重70kg身高175cm年龄24岁每周运动3次算中度运动系数1.55目标是减脂。BMR 10×70 6.25×175 - 5×24 5 1668.75TDEE 1668.75 × 1.55 ≈ 2586减脂期摄入目标 2586 × 0.8 ≈ 2069千卡。再按4:3:3拆分碳水207克、蛋白质155克、脂肪69克。匹配菜品时用菜品的tags字段做硬过滤然后写一个评分函数对菜品进行排序。评分函数主要看三块score 热量匹配度 * 0.4 蛋白质达标度 * 0.4 口味偏好匹配 * 0.2热量匹配度计算的是菜品卡路里在目标份额内的分区得分太超太多都扣分。蛋白质达标度看的是这一餐的蛋白质目标完成率。口味偏好匹配看菜品标签和用户dietary_pref的重合度。最后按得分降序在一日三餐里各取前几名。4.3 上下结合的双链路推荐架构实际线上跑的推荐服务是离线协同过滤结果和在线规则推荐结果两层融合。我用一张图来描述这个流程文字版用户请求推荐接口 → 先查Redis缓存key为recommend:daily:{date}:{userId}→ 命中就直接返回 → 未命中查MySQL里的user_recommend表该表是昨晚MapReduce产出的结果 → 如果结果非空重新包装写入Redis并返回 → 如果查不到冷启动用户或数据还没生成走规则推荐引擎现场计算 → 返回规则推荐结果并标记sourcerule。这个架构好在哪一是把计算量大的协同过滤工作全部放在夜间批量处理线上请求几乎不涉及重计算二是冷启动用户有规则引擎兜底不会体验很差三是Redis缓存命中率高数据库查询压力小并发扛得住。对毕设来说答辩时把这一套“离线在线”的设计理念讲清楚比单纯说“我用了协同过滤”高一个层次。5. 关键功能开发与接口实现5.1 健康画像计算服务我把健康指标计算封装成一个独立服务类避免Controller里塞一堆公式代码。核心逻辑如下Service public class HealthProfileService { public HealthProfile calcProfile(UserHealthInfo info) { double bmi info.getWeight() / Math.pow(info.getHeight() / 100.0, 2); double bmr 10.0 * info.getWeight() 6.25 * info.getHeight() - 5.0 * info.getAge() (info.getGender() 1 ? 5.0 : -161.0); double[] exerciseFactors {1.2, 1.375, 1.55, 1.725, 1.9}; double tdee bmr * exerciseFactors[info.getExerciseLevel() - 1]; double targetCalories tdee * info.getTargetFactor(); return new HealthProfile(bmi, bmr, tdee, targetCalories); } }BMI的语义解释也要跟上。低于18.5是偏瘦18.5到24正常24到28超重超过28肥胖。低于正常值的目标就不是减脂而是增肌/保持规则推荐逻辑要随之切换。这个细节需要处理不处理的话会出现一个80斤的女生被推荐一顿1300千卡的“减脂餐”的荒谬结果。5.2 推荐接口设计与Redis缓存策略推荐接口的核心路径就是GET /api/recommend/daily参数只需要一个日期后端根据当前登录用户自动查档案。响应结构分成早餐、午餐、晚餐三组外加当日营养素汇总{ code: 200, data: { date: 2025-01-15, totalCalories: 2069, totalProtein: 155, totalFat: 69, totalCarbs: 207, meals: [ { type: breakfast, foods: [ {foodId: 12, name: 全麦面包燕麦粥, calories: 380, protein: 21.5, tags: [低脂, 慢碳]} ], calories: 520, protein: 32 }, { type: lunch, foods: [ {foodId: 57, name: 香煎鸡胸肉沙拉, calories: 620, protein: 45.2, tags: [高蛋白, 减脂友好]} ], calories: 760, protein: 64 }, { type: dinner, foods: [ {foodId: 83, name: 清蒸鲈鱼配杂粮饭, calories: 450, protein: 38.5, tags: [低脂, 优质蛋白]} ], calories: 640, protein: 55 } ], source: collaborative } }Redis缓存key我统一用recommend:daily:{date}:{userId}过期时间设置在每天的凌晨5点这样用户第二天凌晨访问时数据已经刷新。需要注意一个问题Redis的Key如果要带用户ID和日期缓存粒度是“每人每天一份推荐”如果用户量到百万级这个Key空间会很大。但毕设场景完全够用没必要过度设计。真要优化可以改成分时段缓存比如早餐、午餐、晚餐分别缓存某个用户如果只看了早餐就不必整天的推荐全部失效。还有降级逻辑Redis挂了不能影响核心功能我用try-catch包裹Redis读写异常时打印日志后直接走MySQL和规则引擎不让缓存层的故障传导到用户请求上。5.3 自动生成一周饮食计划的实现光有当天推荐不太过瘾我给系统加了一个“一键生成一周食谱”的功能。逻辑简单说取用户7天内不同菜品的推荐候选加约束条件比如连续两天不能重复同一道菜、同一餐的热量波动控制在20%以内然后逐天生成。这个功能不能每次请求都实时算一遍否则接口会明显变慢。我的做法是每天凌晨的定时任务里对活跃用户批量预计算未来7天的推荐计划写入weekly_plan表。用户点击查看时直接读表。定时任务用Spring自带的Scheduled注解实现配合EnableScheduling开启Component public class RecommendScheduledTask { Scheduled(cron 0 30 2 * * ?) public void generateDailyRecommendations() { // 1. 从MySQL导出前一日行为数据到HDFS // 2. 调用MapReduce作业计算用户相似度和推荐候选 // 3. 把计算结果写入MySQL和Redis } }定时任务如果跑得太久注意避免和用户高峰期重合。凌晨2点半跑比较合适只要不是秒杀场景这个时间段基本没人访问。6. 常见问题与排查技巧实录6.1 Hadoop环境类问题先聊几个我实际踩过、几乎每个Hadoop新手都会遇到的坑。第一是格式化两次。新手通常第一次format后启动发现某个进程卡住就stop-all.sh然后重新format再启动结果DataNode一直起不来。日志里报java.io.IOException: Incompatible clusterIDs in ...。原因就是第二次格式化生成了新的clusterIDNameNode认新的DataNode还留着旧的两边对不上。解决方法是把临时目录下NameNode和DataNode的数据目录清空把core-site.xml里hadoop.tmp.dir指向的目录整个删掉再重新格式化一次让它干干净净地初始化。第二是NameNode进入safemode。启动后页面显示safe mode is ON数据只能读不能写。这通常发生在异常关机或者数据目录有损坏时。安全模式本身是个保护机制不用慌数据检查没问题后手动退出hdfs dfsadmin -safemode leave第三是Windows下开发环境连接HDFS超时。IDEA里连虚拟机HDFS连接很慢甚至直接Connection refused。先ping一下虚拟机IP确认网络通再检查core-site.xml里fs.defaultFS的地址是否绑定的localhost。绑定localhost的话外部机器连不上要改成hdfs://192.168.x.x:9000让Hadoop监听外部可达的IP。第四是中文文件名乱码。HDFS存日志文件文件名或内容带中文在Linux上读写没问题从Windows上传时可能出现编码错乱。解决的根子在环境统一虚拟机用UTF-8Windows代码里读写HDFS文件时用FileSystem的API明确指定UTF-8编码不要依赖平台默认编码。6.2 SpringBoot整合Hadoop的坑SpringBoot项目里引入hadoop-client依赖比较简单但依赖传递会带来一堆jar包冲突最常见的表现是org.apache.hadoop.fs.FileSystem相关的类加载报错。第一个坑是版本不一致。SpringBoot内置的某些依赖比如commons-lang、guava和Hadoop客户端依赖版本冲突。我是这么处理的在pom.xml中排除冲突的传递依赖显式指定版本保证全局用的是Hadoop配套的那套。代码里操作HDFS最简洁的方式是写一个配置类通过ConfigurationProperties读取application.yml里的hdfs路径然后注入org.apache.hadoop.conf.ConfigurationConfiguration public class HadoopConfig { Value(${hadoop.hdfs-uri}) private String hdfsUri; Bean public Configuration hadoopConfiguration() { Configuration conf new Configuration(); conf.set(fs.defaultFS, hdfsUri); conf.setBoolean(dfs.support.append, true); return conf; } Bean public FileSystem fileSystem() throws IOException { return FileSystem.get(URI.create(hdfsUri), hadoopConfiguration()); } }第二个坑是Windows下调试时运行时找不到winutils。如果必须在本机调试装好winutils并配置环境变量如果嫌麻烦直接用远程连虚拟机Linux环境跑用hadoop jar命令把MapReduce作业提交上去SpringBoot只管调用Windows只开发Web端两边不交叉。事实证明这样最省心。第三个坑是日志刷屏。Hadoop客户端在控制台打印大量INFO日志开发时很干扰排查。在src/main/resources下放一个logback.xml把Hadoop相关包的日志级别调到WARNlogger nameorg.apache.hadoop levelWARN / logger nameorg.apache.zookeeper levelWARN /6.3 推荐效果与数据质量调优系统上线后最容易出现的不是报错而是推荐结果很差差到让人觉得“这算法有问题”。我在调优过程中总结了几个方向冷启动问题。新用户没有任何行为数据协同过滤候选集为空直接走规则推荐。规则推荐需要保证菜品库覆盖足够多的早餐/午餐/晚餐类型而且每个分类下都有充足的候选。我菜品库一共准备了2000多道菜早餐类至少200道否则可组合的空间太小推荐的多样性会很差。数据稀疏问题。用户只对少量菜品有行为算出来的相似用户共同评分很少皮尔逊相关系数会失真。我在代码里加了一个保护逻辑共同评分菜品数低于2个直接返回相似度为0宁可放弃这条相似关系也不要让噪声数据污染推荐结果。可以再加一个惩罚因子共同评分数量越少相似度折扣越大让这个保护逻辑对“看起来很像但实际只有一次交集”的伪相似用户不那么友好。推荐结果太单一。协同过滤的天然问题是会推荐大量相似的热门菜品用户看久了会腻。我在融合层的后处理里加了多样性限制同一餐推荐的菜品最多2道属于同一菜系标签重复度控制在50%以内。比如用户档案里有“川菜”偏好推荐就不会全是水煮肉片、辣子鸡这种川菜三连而是川菜搭配清淡粤菜错开。行为数据质量。另一个容易被忽视的问题是脏数据。爬虫导入的菜品热量值经常出现离谱偏差比如一份“米饭”写成500千卡、一份“烤鱼”写成50千卡这种。我在清洗脚本里写了一个规则校验器对热量范围、三大营养素加和占比做合理性判断超出正常范围的数据自动标记并人工复核。一套数据质量校验规则比想象的更能提升推荐效果因为算法再准也扛不住源头数据是错的。7. 写在最后的一点个人体会项目做到后期我最大的体会是一个“大数据推荐系统”的难点往往不在算法本身而在数据链路的完整性和各层组件的协作。Hadoop和Spark这类组件真正适合干的是离线繁重计算而用户饭点打开页面等结果的时间窗口只有几百毫秒靠的不是Hadoop跑得快而是离线计算把结果提前准备好、在线服务快速取回。想明白这一点你会发现很多技术上看似“高大上”的组合落地时其实是各司其职、彼此配合的关系。如果现在让我重做一遍我会在数据量级和真实用户反馈上多花时间比如接入更多真实的菜谱数据、模拟更多用户行为轨迹因为推荐系统是“数据喂出来的”数据质量和覆盖度决定了算法的上限。我也建议做类似项目的同学先把用户行为埋点做好再考虑算法的高大上——行为和评分数据是整个推荐系统的弹药库没有弹药算法就是摆设。这套“SpringBoot做服务、Hadoop做离线计算”的组合拳在项目里已经验证是能打的希望对正在做同类系统的你有一点参考价值。