你可能见过很多大数据方向的毕业设计题目,名字一个比一个长,但真正能讲清楚“数据从哪来、传到哪、怎么算、怎么用”的却不多。以Hadoop+Spark为核心的民宿推荐系统,其实是一条非常标准的大数据业务闭环:Hadoop负责把大量行为数据存下来,Spark负责把数据算成推荐结果,推荐系统负责把结果给到用户,可视化大屏负责把指标摆到明面上。这套方案能解决的痛点很直接——你的题目需要有大数据的存储与计算、推荐算法、可视化展示这三块硬内容,同时还能串联成一个看得见的Web系统。想认真做完一个能拿得出手、能讲清楚的大数据毕业设计的同学,这篇文章适合你。
我见过不少同学把推荐系统做成了单机Python调库,最后导师问“大数据在哪里”直接卡壳。把Hadoop、Spark、推荐系统、可视化放在一起,不是为了堆技术,而是让整个数据链路自然长成毕业设计需要的样子。下面我按自己做这个题目的过程,从选型、环境、算法、大屏、排错,一步步讲清楚。
1. 项目整体设计与技术选型思路
1.1 这套毕设到底做出来了什么
从功能上看,这是一个“能访问、能展示、能推荐、能看数据”的完整Web项目。用户访问民宿平台,可以浏览民宿、查看详情、收藏、下单;后台会记录用户对民宿的浏览、收藏、评分行为。这些行为数据落到业务库的同时,会以日志格式同步到HDFS里。Spark定期从HDFS读取原始行为日志,完成清洗、统计、训练推荐模型,最终把每个用户最可能喜欢的若干民宿写入Redis或MySQL结果表。用户再打开首页时,推荐模块直接从缓存中把结果拿出来展示。
可视化部分也不是简单画几张报表,而是做成一个民宿数据可视化大屏。大屏上能够看到全国民宿的地理分布、各城市热门民宿排行、价格区间分布、订单量趋势、用户评分分布等。这些指标不是前端临时算的,而是Spark离线聚合好之后,再通过接口供ECharts渲染。也就是说,所有展示的数据都有明确的大数据处理阶段。
从交付物来看,这个项目结构相当完整:源码、设计文档、PPT、演示视频都能对应到具体模块。你向导师汇报的时候,可以说自己实现了数据采集、分布式存储、离线计算、推荐模型、可视化展示、业务系统六个环节。这样的架构,拿到大数据方向的毕设题目里,既不会显得单薄,也踩在常见技术点上。
1.2 为什么选Hadoop+Spark而不是单机算法
很多人会问:做个推荐系统,用Python在本地读CSV不也能算出结果吗?能,但这不是毕设的核心逻辑。毕业设计不是证明你会调用某个库,而是证明你理解大数据场景下“数据量大到单机处理不了”时,应该怎么办。民宿平台如果每天产生几十万条浏览记录,用户和民宿的交互矩阵会非常稀疏,单机内存很难撑住,计算效率也很低。Hadoop的HDFS解决了分布式存储问题,Spark则解决了迭代式计算的性能问题。
Spark在推荐场景里还有一个天然优势:推荐模型训练经常是迭代式的,比如交替最小二乘(ALS)算法,每一轮迭代都要反复读写模型参数。Spark基于内存计算,相比MapReduce反复落盘的方式要快得多。所以这个题目用Spark MLlib里的ALS算法,既切合大数据处理,也非常自然。
至于Zookeeper、Redis、MySQL、ECharts这些组件,各自承担的职责不一样。Zookeeper在真正的分布式集群里负责HDFS NameNode高可用和Kafka协调;在伪分布式环境里它不是必须的,但如果你想体现集群协调能力,完全可以把它配置进去。Redis负责缓存推荐结果和可视化大屏的聚合数据,让前端接口查询速度更快。MySQL放的是用户、民宿、订单等核心业务数据,HDFS放的是用户可以不断追加的原始行为日志,两类数据各司其职。
2. 大数据环境搭建与集群准备
2.1 Hadoop伪分布式搭建:能省力的地方别硬扛
毕业设计不一定要搭真实的物理集群,绝大多数同学用一台配置还行的笔记本就能完成全部开发。对毕设来说,Hadoop伪分布式模式足够用,它的意义在于让你跑通HDFS读写、YARN资源调度和Spark任务提交,而不是让你去折腾机房服务器。我建议直接用虚拟机里的Linux或者云服务器,内存至少8G,硬盘最好留出40G以上,因为HDFS里要放原始日志,Spark运行也会产生临时文件。
别急着看各种“大数据集群部署”视频教程,先把基础组件事项理清。JDK版本建议用1.8,Hadoop版本选3.3.x,Spark选3.3.x。Hadoop伪分布式模式下,核心配置就是四个文件:core-site.xml、hdfs-site.xml、yarn-site.xml、mapred-site.xml。我在实际搭建时,最常用的一组配置长这样:
<!-- core-site.xml --> <configuration> <property> <name>fs.defaultFS</name> <value>hdfs://localhost:9000</value> </property> <property> <name>hadoop.tmp.dir</name> <value>/usr/local/hadoop/tmp</value> </property> </configuration><!-- hdfs-site.xml --> <configuration> <property> <name>dfs.replication</name> <value>1</value> </property> <property> <name>dfs.namenode.name.dir</name> <value>file:///usr/local/hadoop/namenode</value> </property> <property> <name>dfs.datanode.data.dir</name> <value>file:///usr/local/hadoop/datanode</value> </property> </configuration>配置完成后,先执行hdfs namenode -format,然后启动start-dfs.sh和start-yarn.sh,再用jps检查NameNode、DataNode、SecondaryNameNode、ResourceManager是否都在。这里有个我踩过很多次的坑:不要反复执行namenode -format,如果发现Datanode起不来,大概率是格式化之后NameNode的clusterID和DataNode不一致。解决方案是把namenode和datanode目录下的数据清空,再统一重新格式化一次。
伪分布式模式还有一个容易忽略的问题:如果你不是在root用户下运行,HDFS目录权限会导致上传文件失败。建议把Hadoop安装目录授权给你当前使用的用户,或者在代码里统一使用hdfs://localhost:9000作为默认路径,少用本地路径,避免后期权限问题。
2.2 Spark、Zookeeper、Redis在项目里的角色
Spark在这套系统里不是替代Hadoop,而是站在Hadoop的肩膀上干活。Spark从HDFS上读取原始日志,完成ETL后,用Spark SQL做指标统计,用MLlib做ALS推荐模型训练。Spark on YARN模式下,任务提交命令大致是:
spark-submit \ --master yarn \ --deploy-mode client \ --executor-memory 2g \ --driver-memory 1g \ --class com.example.recommend.ALSRecommend \ recommend-1.0.jar这里要特别注意--master yarn时,Spark必须能找到YARN的配置,否则会报Yarn application finished with status FAILED。我在开发环境里往往先在local模式下测试逻辑,再切到yarn模式跑全量数据,这样排查问题更快。
Zookeeper如果在你的架构里出现,最常见的作用是给HDFS提供高可用,也可以给Kafka、HBase做协调。但在单机伪分布式环境里,Zookeeper更多是“环境集成”上的加分项。我会在部署文档里单独写一章Hadoop和Zookeeper整合的步骤,比如下载解压Zookeeper、配置zoo.cfg、启动zkServer.sh,然后把HDFS的HA模式配置起来。如果你不想连HA都做,也可以在Spark任务调度里集成Zookeeper,用来记录推荐任务的运行状态和元数据,解释成“利用Zookeeper实现任务协调”,也说得通。
Redis承担的角色非常务实:推荐结果直接写在Redis,用户请求过来时先查Redis,如果缓存命中就不再查Spark结果表。可视化大屏的聚合指标也会从Redis里读取。我在本机装Redis后,会搭配一款Redis可视化客户端工具,比如AnotherRedisDesktopManager,方便查看key是否存在、过期时间、value结构。别小看这个工具,调试推荐接口时,你打开客户端能看到rec:user:1001里存了哪些民宿ID,比自己敲redis-cli快得多。
下面是这套项目常用的环境版本清单,我按实际使用整理出来:
| 组件 | 版本建议 | 用途 |
|---|---|---|
| JDK | 1.8 | Hadoop和Spark运行基础 |
| Hadoop | 3.3.4 | HDFS存储、YARN调度 |
| Spark | 3.3.2 | 离线计算与推荐模型训练 |
| ZooKeeper | 3.7.1 | 集群协调、任务状态记录 |
| MySQL | 8.0 | 业务数据存储 |
| Redis | 6.2 | 推荐结果缓存、大屏数据缓存 |
| ECharts | 5.x | 可视化大屏图表渲染 |
| Spring Boot | 2.7.x | 后端业务接口 |
3. 数据预处理与推荐算法实现
3.1 数据从哪来、怎么清洗
民宿推荐系统最核心的数据有三类:用户数据、民宿数据、行为数据。用户数据包括用户ID、城市、注册时间;民宿数据包括民宿ID、标题、城市、价格、评分、经纬度;行为数据包括用户ID、民宿ID、行为类型(浏览、收藏、下单、评分)、行为时间。
很多同学问数据能不能爬。我建议优先使用公开可用的民宿相关数据集,或者自己写脚本模拟生成结构化数据。模拟数据并不丢人,关键是你必须清楚每条数据代表什么含义。我会用Python脚本生成大约50万条用户行为记录,然后写进MySQL,再通过定时任务导出到HDFS的/data/behavior目录。这样你在答辩时可以说:行为数据先存储在业务库,随后以日志形式同步到HDFS,形成离线数据仓库的原始层。
Spark读取HDFS数据之后,清洗环节主要做四件事:空值处理、去重、评分范围规整、用户和民宿ID映射。这步用Spark SQL很容易:
from pyspark.sql import functions as F behavior = spark.read.csv( "hdfs://localhost:9000/data/behavior.csv", header=True, inferSchema=True ) behavior = behavior.filter( F.col("rating").between(1, 5) ).dropDuplicates( ["user_id", "house_id", "behavior_type"] ).select( "user_id", "house_id", "behavior_type", F.col("rating").cast("double").alias("rating"), "timestamp" )这里有个容易踩的坑:如果原始数据里有些行只有浏览行为没有评分,你的推荐目标是预测用户对民宿的评分还是点击概率?传统ALS需要显式评分,所以我会在数据准备阶段,把收藏行为映射成4分、下单映射成5分、评分行为直接用原始分。这样就能构造出比较稠密的隐式评分矩阵。如果你想做得更真实,也可以把行为时间衰减做成权重,比如越近的行为权重越高。
3.2 基于ALS的用户偏好挖掘与冷启动兜底
ALS的全称是交替最小二乘,它做的事情很直观:把“用户-民宿评分矩阵”拆成“用户隐因子矩阵”和“民宿隐因子矩阵”的乘积。你可以把隐因子理解为“价格敏感度”“位置偏好”“装修风格偏好”之类不容易直接测量的潜在维度。ALS算法先随机初始化用户矩阵,固定用户矩阵去优化民宿矩阵;再固定民宿矩阵去优化用户矩阵,交替进行,直到收敛。
在Spark里实现ALS推荐,代码非常短:
from pyspark.ml.evaluation import RegressionEvaluator from pyspark.ml.recommendation import ALS from pyspark.ml.tuning import ParamGridBuilder, CrossValidator train, test = behavior.randomSplit([0.8, 0.2], seed=42) als = ALS( userCol="user_id", itemCol="house_id", ratingCol="rating", rank=20, maxIter=10, regParam=0.1, coldStartStrategy="drop" ) model = als.fit(train) predictions = model.transform(test) evaluator = RegressionEvaluator( metricName="rmse", labelCol="rating", predictionCol="prediction" ) rmse = evaluator.evaluate(predictions) print(f"RMSE = {rmse:.4f}")如果你只跑一遍这个流程,RMSE看着还行,但答辩时被问“为什么选rank=20、maxIter=10”就会露馅。rank表示隐因子数量,取值太小模型学不到足够信息,取值太大容易在稀疏数据上过拟合;maxIter控制迭代次数,太小不收敛,太大训练时间翻倍;regParam是正则化系数,防止模型参数过大。我常用的调参策略是用ParamGridBuilder做交叉验证:
param_grid = ParamGridBuilder() \ .addGrid(als.rank, [10, 20, 30]) \ .addGrid(als.maxIter, [5, 10]) \ .addGrid(als.regParam, [0.01, 0.1]) \ .build() cv = CrossValidator( estimator=als, estimatorParamMaps=param_grid, evaluator=evaluator, numFolds=3 )这里提醒一下,CrossValidator在数据量大时会非常耗时,所以我一般先在100万以内数据上做交叉验证,选到最优参数后再用全量数据训练。最终模型给每个用户生成的Top N推荐列表,会写成如user_id, house_id, rank_score的格式,存到HDFS或者直接落MySQL。
ALS对老用户效果明显,但对新用户完全算不出来,这就是冷启动问题。我在系统里做了一个简单的兜底:新用户没有行为记录时,推荐系统会展示当前城市的热门民宿、高分民宿和平台精选民宿。这部分结果由Spark按“近7天订单量0.5 + 平均评分0.3 + 收藏量*0.2”排出来,写入Redis中的hot:houses。这样就形成一个“离线推荐为主、热门推荐兜底”的完整方案,在答辩里也比较好解释。
3.3 推荐结果的落地与评测
推荐结果不能只存在于Spark训练日志里,它必须能服务业务系统。我这边设计了两种落地方式。第一种是直接写Redis,key设置为rec:user:{userId},value用JSON数组存民宿ID列表,设置过期时间比如24小时。第二种是写回MySQL的recommend_result表,字段包括user_id、house_id、score、reason_type。前者给在线接口快速读取,后者是历史存档和效果复盘。
对于推荐效果,除了RMSE,我还建议补充TopN命中率或召回率。做法很简单:把用户最近一次产生的行为放到测试集,看模型推荐的Top20民宿里是否包含这个民宿。如果包含就算一次命中,最终命中数除以测试用户总数就是召回率。你不需要把这个指标做到多高,但能说明你理解推荐效果不能只看误差,还要看用户是否真的点击或下单。
我实际跑下来的经验是:在50万条行为数据上,ALS的RMSE能做到0.8以下,Top20召回率大概在12%到18%之间。这个数据看起来不高,但对基于评分的协同过滤来说是正常水平,因为民宿消费行为本身非常稀疏。答辩时如果导师问准确率低的原因,你可以从“隐式反馈弱、行为稀疏、没有引入上下文特征”三个角度解释,比强行说效果很好要有说服力。
4. 民宿可视化大屏的实现
4.1 大屏看什么:数据指标与页面布局
民宿可视化大屏不是把所有图表丢在一起,而是按照“整体概览—地理分布—核心排行—实时趋势”的逻辑来布局。我参考常见可视化大屏项目后,把页面分成四块:
- 顶部中间放核心KPI:民宿总数、用户总数、累计订单量、平均评分;
- 中间偏左放全国民宿分布地图,用散点或热力图展示各地民宿数量;
- 中间偏右放热门城市Top10和热门民宿Top10;
- 底部放价格区间分布、订单时间趋势、用户评分分布三个图表。
指标计算完全由Spark离线完成。比如“城市订单量排行”写的Spark SQL大致是:
city_df = order_df.join(house_df, order_df.house_id == house_df.house_id) \ .groupBy("city") \ .agg(F.count("*").alias("order_cnt")) \ .orderBy(F.desc("order_cnt"))计算结果会写入Redis的一个ZSet,key为screen:city_order,member是城市名,score是订单量。ECharts前端只需要调用后端接口,返回这个ZSet的数据即可。
这里要特别注意数据口径,比如“订单量”到底指支付成功订单还是全部订单,“平均评分”是指民宿评分的平均还是订单中用户给的分值平均。我在设计文档里把这些指标口径写清楚,导师问的时候你就能答得滴水不漏。
4.2 用ECharts做民宿数据可视化
ECharts的优势是免费、文档全、社区案例多,做毕设大屏足够。渲染地图时,先引入中国地图GeoJSON,然后注册地图,再放一个scatter系列。核心代码类似这样:
import * as echarts from 'echarts'; import china from './china.json'; echarts.registerMap('china', china); const mapChart = echarts.init(document.getElementById('mapChart')); mapChart.setOption({ tooltip: { trigger: 'item', formatter: (params) => `${params.name}<br/>民宿数量:${params.value || 0}` }, visualMap: { min: 0, max: 1000, text: ['高', '低'], inRange: { color: ['#e0f3f8', '#abd9e9', '#74add1', '#4575b4'] } }, series: [{ type: 'map', map: 'china', roam: true, data: cityData }] });饼图、柱状图的写法更简单。我这里给一个价格区间分布的例子:
const barChart = echarts.init(document.getElementById('priceBar')); barChart.setOption({ xAxis: { type: 'category', data: ['200元以下', '200-400元', '400-600元', '600-1000元', '1000元以上'] }, yAxis: { type: 'value' }, series: [{ type: 'bar', data: [3200, 5400, 3800, 2200, 900], barWidth: 20, itemStyle: { barBorderRadius: [4, 4, 0, 0] } }] });做可视化大屏时,我建议不要在一张页面上放超过10个图表,否则切图、布局、数据刷新都会变复杂。把最核心的5到7个图表做精致,比堆满一屏更高级。还有一个小技巧:给每个图表外层容器设置固定高度,比如height: 280px,防止ECharts初始化时拿不到高度而出现空白。
4.3 大屏数据从Spark到Redis再到前端
整个大屏的数据链路是:MySQL业务数据或HDFS日志 -> Spark离线聚合 -> Redis缓存 -> 后端接口 -> ECharts图表。我为什么要把Spark计算结果放Redis而不是让前端直接读到数据库?因为大屏通常会每隔几秒或几分钟刷新一次,如果每次都触发一次Spark任务,底层集群会长时间占用资源,而且图表加载会很慢。Redis的读取速度极快,可以扛住页面高频刷新。
具体实现中,我会用一个定时任务,比如Spring的@Scheduled或者Linux crontab调度Python脚本,每10分钟执行一次Spark任务。任务执行完成后,把结果写到Redis下这些key:
| Redis Key | Value类型 | 说明 |
|---|---|---|
screen:overview | Hash | 民宿总数、用户总数、订单量、平均评分 |
screen:city_order | ZSet | 各城市订单量排行 |
screen:hot_house | ZSet | 热门民宿排行 |
screen:price_dist | Hash | 价格区间分布 |
screen:score_dist | Hash | 评分分布 |
screen:order_trend | List | 近7天订单趋势 |
后端接口就拿screen:overview举例:
@GetMapping("/api/screen/overview") public Map<String, Object> overview() { return redisTemplate.opsForHash().entries("screen:overview"); }前端拿到接口数据后,调用echarts.setOption更新图表。大屏页面用setInterval定时刷新接口数据,比如每30秒请求一次。为了让展示效果更好,我在页面初始化时做一个图表入场动画,ECharts默认带有动画,不需要额外写太多代码。
5. 业务系统与后台管理模块怎么写
5.1 用户、民宿、收藏、订单的实体关系
可视化大屏和推荐系统说到底是在服务业务,所以业务模块不能做得太薄。我设计了五张核心表:用户表、民宿表、收藏表、订单表、评分表。用户表存用户基本信息;民宿表存城市、价格、评分、经纬度;收藏表和评分表是行为数据的一种;订单表用来支撑可视化中的交易类指标。
简单建表SQL如下:
CREATE TABLE `user` ( `id` int NOT NULL AUTO_INCREMENT, `username` varchar(64) NOT NULL, `city` varchar(64) DEFAULT NULL, `register_time` datetime DEFAULT NULL, PRIMARY KEY (`id`) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4; CREATE TABLE `house` ( `id` int NOT NULL AUTO_INCREMENT, `title` varchar(128) NOT NULL, `city` varchar(64) NOT NULL, `price` decimal(10,2) NOT NULL, `rating` decimal(2,1) DEFAULT NULL, `longitude` decimal(10,7) DEFAULT NULL, `latitude` decimal(10,7) DEFAULT NULL, PRIMARY KEY (`id`) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;这里要注意,MySQL只放业务系统的核心数据,几十万条行为日志不要全部塞进去,否则会越来越慢。行为日志以文件形式落到HDFS,由Spark定期消费。这样你在设计文档里可以清楚画出两条数据流:业务库负责联机事务,HDFS负责离线分析,互不干扰。
5.2 推荐接口与业务系统的融合
后端用Spring Boot提供REST接口,页面用Vue或Thymeleaf渲染都可以。推荐接口的设计很关键。用户登录后,点击“猜你喜欢”时,后端不会现场跑算法,而是从Redis查已经算好的推荐结果。如果Redis没有这个用户的推荐列表,就查MySQL的recommend_result表,如果也没有,就用热门民宿做替代。这个三级降级策略在演示时非常稳定。
一个简化版接口:
@GetMapping("/api/recommend") public Result recommend(@RequestParam Integer userId) { List<HouseVO> houses = redisService.getRecommend(userId); if (houses.isEmpty()) { houses = recommendService.getFromMysql(userId); if (houses.isEmpty()) { houses = recommendService.getHotHouses(userId); } } return Result.success(houses); }前端页面拿到推荐列表后,展示民宿卡片,同时会把用户后续的点击行为上报到日志系统。这些日志再进入HDFS,形成新的训练数据,第二天Spark定时任务重新训练模型。也就是说,推荐系统是持续更新的,不是训练一次就永远不变。能把这个“日志回流”讲清楚,整个项目的完整度立刻不一样。
6. 实战问题排查与调优实录
6.1 Hadoop/Spark启动和运行的坑
先说最典型的启动问题。很多人配置好Hadoop后,start-dfs.sh执行完,jps里只有NameNode没有DataNode。原因前面提过,大多是因为格式化了两次NameNode,导致DataNode里的clusterID和NameNode不一致。解决方法是停止所有服务,清空namenode和datanode目录后重新格式化,再启动。
还有一种是Spark任务提交后,状态一直是ACCEPTED但跑不动。这通常是因为内存不够或YARN资源没配置好。伪分布式下建议在yarn-site.xml设置:
<property> <name>yarn.nodemanager.resource.memory-mb</name> <value>4096</value> </property> <property> <name>yarn.scheduler.maximum-allocation-mb</name> <value>4096</value> </property>如果只给虚拟机分配了4G内存,Hadoop和Spark再同时跑,很容易频繁GC。我一般是在测试阶段先启动HDFS和YARN,然后Spark以local模式跑小数据量,等逻辑验证完,再切换到YARN模式跑全量。这样能避免调试时每跑一次就卡十分钟。
6.2 推荐效果差、数据倾斜与内存问题
推荐效果差不代表算法不行,很可能是因为你的行为数据里用户太少或者交互矩阵太稀疏。我有个习惯,先看训练集和测试集覆盖了多少用户和民宿。如果测试集里的用户大部分都没出现在训练集里,RMSE再低也没有实际意义。这种情况可以把数据按用户分组划分,保证同一个用户的记录只出现在训练集或测试集一边。
数据倾斜是Spark任务里最常见的性能杀手。比如热门城市的民宿数据特别多,某个key对应的数据量是其他key的上百倍,导致个别reduce任务跑得很慢。一个常用办法是给倾斜key加随机前缀,比如:
from pyspark.sql.functions import concat, lit, rand df = df.withColumn("city_key", concat(df["city"], lit("_"), lit(rand() * 10).cast("int")))把一个大key拆成多个小key后再聚合,最后去掉前缀汇总。这个方法在面试和答辩里都算亮点。
Spark内存溢出也很常见。日志里如果出现java.lang.OutOfMemoryError,优先调整--executor-memory和spark.sql.shuffle.partitions。默认shuffle分区数是200,数据量小的时候可以降到50,减少创建太多空task造成的损耗。
6.3 毕业答辩前的demo稳定技巧
虽然这不是一个技术算法问题,但我觉得比算法调参更值得写。毕设答辩当天,最怕的就是现场启动集群失败,或者页面刷不出数据。所以我会提前做一次完整的冷启动演练:重启电脑后,按照“启动Zookeeper—启动Hadoop—启动Spark HistoryServer—启动Redis—启动业务系统”的顺序把所有服务拉起来,然后录制一遍演示视频。视频一方面是交作业材料,另一方面是万一现场故障的备用方案。
启动顺序也有讲究。如果同时用到了HBase、Kafka这类依赖Zookeeper的组件,必须先启动Zookeeper。Spark应用如果挂在YARN上,先确保HDFS能正常读写。Redis连接客户端时,如果提示拒绝连接,要检查redis.conf里的bind和protected-mode设置,本机调试时可以直接让protected-mode no,但仅仅限于本地环境。演示前我还会把Spark日志级别调到WARN,省得控制台刷一大片INFO影响判断。
我个人在完成这个项目时最大的体会是:别一上来就追求全链路跑通,而是先用手工造的小数据从“MySQL -> HDFS -> Spark -> Redis -> 页面”走通一版,再去补算法调优和可视化细节。小数据链路通了,你不仅对每个组件的作用了如指掌,后面加数据量也只是换一台机器、改一个参数的事情。推荐系统、可视化、大数据处理这套组合,真正难的不是某个单点技术,而是把整个闭环串在一起的能力。你在做毕业设计时把这个闭环想明白了,写文档、画架构图、答辩都会顺很多。