简介:面向计算机毕业设计的大数据推荐系统完整项目,基于SSM与Spark框架实现电影推荐功能,涵盖项目文档与全部源码,适合需要完成相似课题或入门大数据开发的读者。资源共1419个文件、约90.98MB,主要包含Java与Scala源码、前端HTML/CSS/JS页面、图片素材、配置文件与SQL脚本等,目录结构完整,并带有部署说明和数据库脚本,便于快速运行与二次开发。项目覆盖数据采集、清洗、统计、建模与实时计算等环节,涉及Spark批处理与流处理、SSM分层架构、基于内容与协同过滤的推荐算法等核心要点,从环境配置到算法实现均有相应文件支撑,可帮助读者理解个性化推荐的完整实现链路。已有25人学习下载,作为毕业设计参考或大数据实战演练具有较高的借鉴价值,能够为同类选题提供可落地的完整方案。
1. 基于SSM与Spark的电影推荐系统:一套能从零跑通的大数据毕设方案
如果你正在做大数据的课程设计或毕业设计选题,看到“基于ssm+spark的电影推荐系统”这类标题,第一反应通常是两个极端:要么觉得难度太大,Hadoop、Spark、Java后端一整套不知道怎么串起来;要么觉得就是个“网页 + 一个推荐列表”的演示项目,没什么技术含量。这两个判断都有偏差。真正落到代码和文档上,它是一条完整的离线推荐链路:SSM负责后台和接口展示,Hadoop负责存放批次数据,Spark负责把用户的评分记录通过协同过滤算成“可能喜欢”的TopN电影列表,最后回写数据库供前端查询。它能帮你在一个项目里同时讲清楚Java开发、大数据存储和推荐算法三件事,也是面试时最能直接聊的技术点。适合的人群很明确:准备毕设答辩的在校生、想往大数据方向转的Java开发,以及需要一个可复现样例来学Spark MLlib的入门者。
2. 架构选型与数据流:为什么是SSM、Hadoop和Spark绑在一起
2.1 整体链路:一次推荐结果是怎么算出来的
先看整体。这套系统对外是一个电影网站,用户能注册、打分,也能看到“猜你喜欢”的列表。但这些推荐结果不是Web应用里实时算的,而是来自离线计算任务。常见做法是:MySQL存用户、电影和评分记录,HDFS存历史日志或从MySQL导入的批数据,Spark定期跑一次训练任务,把每个用户的TopN推荐写回MySQL的推荐表,SSM后端再按userId查这张表,返回给页面。整个过程看起来像一套流水线,每一层只有一个明确职责。
Hadoop在这个项目里真正承担的是HDFS和YARN。本地练习时,HDFS用来放从数据库导出的评分快照,YARN负责给Spark任务分配资源。即便你的笔记本只有8G内存,也建议把Hadoop和Spark装成伪分布式,而不是直接跑Spark本地模式——因为答辩时老师一定会问“Hadoop集群里每个角色的作用”,没跑过伪分布式,这个问题很容易答空。
2.2 选型理由:SSM管“给人看”,Spark管“算出来”
为什么是SSM而不是Spring Boot?这不是技术先进性的问题,而是覆盖面问题。在很多高校的课程体系里,SSM仍然是Java Web的标准配置,用SSM能让答辩老师觉得你基本功扎实,而且项目文档里能写“Spring管理业务对象,SpringMVC对外提供接口,MyBatis操作数据库”,每一层都有话可说。
Spark选型则是因为它比原生MapReduce好写太多。协同过滤的ALS算法在Spark MLlib里直接封装好了,训练、预测、评估都是几行代码的事。如果换成MapReduce去实现矩阵分解,你得手写迭代计算和矩阵更新,代码量和出错概率完全不是一个量级。再加上Spark天然支持Scala和Java,和项目里的其他Java代码能共用依赖,所以用Spark做推荐计算是这个场景下最顺的方案。
2.3 数据模型与算法:ALS协同过滤为什么是默认主力
电影推荐场景下,最常用的算法是交替最小二乘(ALS)。它属于协同过滤里的隐语义模型,核心思路是把“用户-电影评分矩阵”分解成两个低维矩阵,一个代表用户的隐特征,一个代表电影的隐特征,再用这两个矩阵相乘去预测缺失的评分。
ALS在Spark里的落地非常标准:输入三列,分别是userId、movieId、rating,输出是模型文件或直接的推荐结果。你要关心的参数只有几个:矩阵分解的rank维度,一般取10到50;迭代次数maxIter,10次左右就够收敛;正则化参数regParam,默认0.1左右防止过拟合。这个算法选型能保证你在文档里写“基于用户行为做个性化推荐”,同时实现成本又足够低,不会在毕设阶段把自己卡死。
3. 动手落地:从环境准备到Spark作业跑通的完整流程
3.1 环境与版本映射:先定版本再动手
做这个项目最怕的不是代码写错,而是组件版本互相不兼容。建议第一次搭建时直接抄下面这张版本表,不要追求最新版,因为网上搜到的教程和报错基本都是按某个成熟版本组合来的。
| 组件 | 版本推荐 | 关键说明 |
|---|---|---|
| JDK | 1.8 | 不要用17,很多老教程和插件还在依赖Java 8语法 |
| Hadoop | 2.7.x 或 3.1.x | 配Spark请认准编译时对应的hadoop版本 |
| Spark | 2.4.x 或 3.1.x | 下载pre-built版本,避免自己编译 |
| MySQL | 5.7 | 8.0也兼容,注意JDBC驱动换8.x版本 |
| Maven | 3.6+ | 用来管理SSM后端依赖 |
我的建议是:Spark选2.4.x配Hadoop 2.7.x,这套组合最成熟、网上的坑都有现成答案。环境变量里配好JAVA_HOME、HADOOP_HOME、SPARK_HOME,然后把Hadoop的bin目录和Spark的bin目录都加进PATH。验证方式很简单,分别执行hadoop version和spark-shell --version,能正常输出版本说明环境变量没问题。
3.2 准备数据:建表与导入评分记录
建模阶段不要一上来就追求百万级数据,先用本地MySQL建好三张核心表:用户表、电影表、评分表。推荐表的字段也一并建好,方便Spark算完往里写。
CREATE DATABASE IF NOT EXISTS movie_recommend DEFAULT CHARACTER SET utf8mb4; CREATE TABLE movie_recommend.movies ( movie_id INT PRIMARY KEY, title VARCHAR(200), genres VARCHAR(100) ); CREATE TABLE movie_recommend.ratings ( user_id INT, movie_id INT, rating FLOAT, ts BIGINT, PRIMARY KEY (user_id, movie_id) ); CREATE TABLE movie_recommend.rec_result ( user_id INT, movie_id INT, score FLOAT, rank INT, PRIMARY KEY (user_id, movie_id) );建表逻辑说明:rating表的主键设计成联合主键,是为了防止同一用户对同一电影重复评分;rec_result表里专门留了rank字段,前端查询时可以直接按这个字段排序展示“猜你喜欢”。数据集方面,如果导数据不方便,直接写几行Java代码或SQL脚本,模拟200个用户、50部电影、几千条评分也能跑通整个流程。真实感更强的做法是去下载MovieLens数据集,把里面的movies.csv和ratings.csv改造后导入MySQL。
3.3 Spark作业:读数据、训练模型、生成推荐结果
环境准备好了,数据表也建好了,接下来是核心环节:写Spark作业。这里我习惯用Scala写训练逻辑,因为MLlib在Scala下的API最顺。把下面的代码打包成jar,或者直接在spark-shell里逐行跑。
import org.apache.spark.sql.SparkSession import org.apache.spark.ml.recommendation.ALS val spark = SparkSession.builder() .appName("MovieRecommendALS") .master("yarn") .config("spark.sql.shuffle.partitions", "4") .getOrCreate() // 读取MySQL评分表,注意连接串里的编码参数 val ratings = spark.read .format("jdbc") .option("url", "jdbc:mysql://localhost:3306/movie_recommend?useUnicode=true&characterEncoding=utf8") .option("dbtable", "ratings") .option("user", "root") .option("password", "123456") .load() .selectExpr("cast(user_id as int) userId", "cast(movie_id as int) movieId", "cast(rating as float) rating") ratings.cache() val als = new ALS() .setRank(12) .setMaxIter(10) .setRegParam(0.1) .setUserCol("userId") .setItemCol("movieId") .setRatingCol("rating") val model = als.fit(ratings) // 为每个用户生成Top20推荐 val recommendations = model.recommendForAllUsers(20) recommendations.show(false)这段代码的逻辑很直白:用JDBC加载MySQL里的评分数据,转成Spark需要的三列格式,然后交给ALS训练。setRank(12)表示把用户和电影各自映射成12维隐特征,这个值不是越大越好,数据量小的时候取8到15更合适;setRegParam(0.1)是正则化系数,它能让模型不过度依赖某些极端评分;recommendForAllUsers(20)会给每个用户返回20部电影,包含预测评分和排名。
训练完成后,需要用一段额外的代码把结果写回MySQL。最省事的写法是转成DataFrame后调用df.write.jdbc,指定目标表名rec_result和连接信息即可。注意写回时把预测评分字段改名为score,rank字段用row_number().over(Window.partitionBy("userId").orderBy(desc("rating")))生成,这样SSM查出来直接就是有序的推荐列表。
3.4 SSM后端接入:写推荐接口并返回给页面
Spark算完的数据已经落在MySQL的rec_result表里,后端要做的事就很单纯了:按当前登录用户的userId查这张表,把结果拼成JSON返回给前端页面。
@RestController @RequestMapping("/recommend") public class RecommendController { @Autowired private RecommendService recommendService; @GetMapping("/top/{userId}") public Result getTopRecommend(@PathVariable Integer userId) { List<RecommendMovieVO> list = recommendService.getTopNByUser(userId); return Result.success(list); } }对应的MyBatis Mapper里就是一条SQL:SELECT m.title, m.genres, r.score FROM rec_result r LEFT JOIN movies m ON r.movie_id = m.movie_id WHERE r.user_id = #{userId} ORDER BY r.rank ASC。这样Controller只做接口转发,Mapper只做查询,推荐结果从哪来、怎么算的,全被隔离在后端看不见的地方。答辩时,你可以先展示页面效果,再带着老师去看Spark的日志和模型输出,项目完整性一下就立住了。
4. 避坑指南:Hadoop和Spark联调最容易踩的四个问题
4.1 NoSuchMethodError:Hadoop与Spark版本不匹配
现象:Spark作业提交后,很快报错java.lang.NoSuchMethodError: org.apache.hadoop...,整个堆栈指向HDFS或YARN的API。
原因:Spark编译时依赖的Hadoop版本和你本机安装的Hadoop版本不一致。最常见的是Spark 3.2以上用了Hadoop 3.x的API,但你本机装的是Hadoop 2.7,或者反过来。
解决:把Spark换成本机Hadoop对应编译版本。最省事的办法是下载Spark安装包时直接选带hadoop2.7或hadoop3.1字样的预编译版本,然后重新启动spark-shell,再跑一遍数据读取。这个和代码本身无关,纯粹是版本对齐问题,不要浪费时间改代码。
4.2 ExecutorLostFailure:Spark作业一提交就失败
现象:作业跑几秒后,YARN界面里看到Executor反复挂掉,日志提示Container killed by YARN for exceeding memory limits。
原因:Spark默认每个Executor内存只有1G,但你的评分数据量偏大,或者用户数较多导致shuffle时数据膨胀,超出了容器限制。
解决:提交任务时显式指定资源参数,例如spark-submit --executor-memory 2g --driver-memory 1g --conf spark.shuffle.memoryFraction=0.4。本地练习时重点调executor-memory和spark.sql.shuffle.partitions,把shuffle分区数调小一点,比如4到8个,可以减少大量小文件带来的内存开销。
4.3 推荐表里中文乱码:MySQL连接串缺了编码参数
现象:Spark回写MySQL后,SSM在页面上查出来的电影标题全是???或乱码。
原因:JDBC连接串没有指定字符集。Spark的JDBC读写默认使用平台字符集,在英文环境或默认环境下中文就会丢。
解决:所有涉及MySQL的jdbc URL都加上useUnicode=true&characterEncoding=utf8,SSM里的数据库连接串同样加上。这个坑不起眼,但等页面展示时才发现,你还要重新训练一遍Spark,很耽误时间。
4.4 数据倾斜:某部热门电影让作业卡死在某个Stage
现象:作业日志显示某个Stage运行极慢,其他Executor都已经Finished,只有一个Executor卡在处理几百MB数据的task。
原因:MovieLens这类数据里,少数电影被大量用户评分,这会导致ALS在shuffle阶段某个key的数据量远超其他key,单个task负载失衡。
解决:最简单的方法是训练前先做一次过滤,把评分总数超过阈值(比如平均评分次数的3倍)的影评降采样或直接去掉;或者在ALS训练时调高regParam,减小异常权重。如果是作业规模大,可以考虑用repartition重新打散数据后再训练。
5. 进阶技巧:给推荐结果加一个Redis缓存层
当离线推荐链路跑通以后,你会发现每次用户打开首页,SSM都要去MySQL查一次rec_result表。虽然Rec_result表不大,但在演示答辩时,如果多个人同时操作,MySQL的并发查询压力会直接反映成页面卡顿。这时的进阶做法是引入Redis缓存。
具体设计不复杂。Spark作业完成推荐结果回写MySQL后,再由一段Java代码把rec_result里的记录同步进Redis,key设计成user:recommend:{userId},value用JSON数组存Top10电影ID和标题,TTL设成1小时。SSM查询推荐接口时先查Redis,查得到就直接返回,查不到再回调MySQL,并把结果回填Redis。这套逻辑在Spring里只需要加一个@Cacheable注解或几十行代码,但答辩时讲出来,技术层次会明显比单纯“SSM查MySQL”高出一截。
@Service public class RecommendService { @Autowired private StringRedisTemplate redisTemplate; @Autowired private RecommendMapper recommendMapper; // 先查Redis,没有则查MySQL并回填 public List<RecommendMovieVO> getTopNByUser(Integer userId) { String key = "user:recommend:" + userId; String cacheValue = redisTemplate.opsForValue().get(key); if (cacheValue != null) { return JSON.parseArray(cacheValue, RecommendMovieVO.class); } List<RecommendMovieVO> list = recommendMapper.selectTopNByUser(userId); redisTemplate.opsForValue().set(key, JSON.toJSONString(list), 1, TimeUnit.HOURS); return list; } }这段代码的逻辑就是典型的缓存旁路模式。需要注意的坑是序列化:用StringRedisTemplate时,value必须是JSON字符串,否则会写入一堆二进制乱码;TTL不要设置太长,因为Spark的推荐结果通常每天刷新一次,缓存过久反而会让用户看到老旧推荐。另外,Redis缓存命中率可以简单看日志观察,如果每次请求都穿过Redis打到MySQL,就检查一下key拼接是不是和回填时一致。
完成了这层改造,你的项目就不只是“跑通”了,而是一套有缓存意识、有离线调度意识的小型推荐系统。答辩时从数据导入、Spark训练、结果回写、Redis缓存一条链路讲下来,任何一个环节被追问你都能拿出实际配置和日志应对。这是我做类似项目时最深刻的体会——把边角问题提前解决,比追求模型精度重要得多。希望帮到你。
本文还有配套的精品资源,点击获取