首页 / 资讯中心 / 文章详情

基于Hadoop与Spark的学生体质健康大数据系统设计与实现

基于Hadoop与Spark的学生体质健康大数据系统设计与实现 ★ FEATURED ARTICLE
1. 项目整体定位与技术选型为什么是Hadoop、Spark加Spring Boot1.1 这个系统到底在解决什么问题去年接了一个学生体质健康信息系统的课程设计项目要求必须走大数据技术栈不能像平时交作业那样用单机MySQL糊弄过去。当时第一反应是体测数据能有多大一个学校几千人一年也就几千条记录至于上Hadoop吗真正动手之后我才理解这个课题要的不是“数据量”而是“数据处理的工程链路”——从原始体测文件采集、分布式存储、离线清洗统计到Spring Boot对外提供查询接口最后落到可视化大屏展示。把这套链路跑通比单纯写一个CRUD管理系统有价值得多。系统面向的使用场景很明确学校体育教研室每学年组织学生体测拿到身高、体重、肺活量、立定跳远、坐位体前屈、50米跑、耐力跑、引体向上或者仰卧起坐这些原始成绩然后要回答几个问题全校整体体质合格率是多少各年级、各班级的BMI分布如何某个学生连续几年的肺活量趋势是上升还是下降哪些男生的耐力跑成绩明显低于标准如果只看单个问题Excel就够了。但要把这些数据统一存起来、定期清洗、按多种维度聚合再供大屏和管理端使用就需要一个分层明确的数据处理流程。这个项目最终拆成三块独立模块Hadoop负责原始文件的分布式存储Spark负责把杂乱的体测文件清洗成结构化指标并完成统计聚合Spring Boot只负责读Spark算好的结果通过REST接口输出给可视化大屏。三块各司其职改动任何一块都不会把其他模块拖垮。1.2 三件套分工和核心数据流先说说三件套各自扮演的角色。Hadoop在这里不是用来堆数据的而是提供HDFS分布式文件系统和YARN资源调度。体测原始文件按学年、批次上传后统一落到HDFS目录比如/physique/raw/2024/。这样做的直接好处是文件不需要预先整理成严格统一的格式后续清洗交给Spark任务去处理。Spark负责的是脏活累活。原始体测数据可能是不同老师录入的Excel转CSV、有的带表头、有的身高单位不统一甚至有空值行。Spark任务把这些数据读进来做去重、类型转换、单位统一然后算出BMI、肺活量体重指数、得分等级等指标再按班级、年级、性别做聚合统计。最终统计结果写回MySQL供业务层查询。Spring Boot处在最上层它不直接接触HDFS原始数据而是读MySQL里已经算好的统计表。有人问为什么不直接从HDFS读我试过接口响应十几秒起步而且Spark底层如果没跑起来接口直接报错。改成“Spark离线算好、Spring Boot只查结果”之后大屏接口响应基本稳定在100毫秒以内。这个架构里最关键的认知是Spring Boot不是数据处理引擎它只是结果展示的入口。把计算逻辑放到Spark里把结果缓存到数据库业务层才能做到轻量、稳定。整个数据流向可以概括为上传体测文件 → HDFS存储 → Spark清洗统计 → MySQL结果表 → Spring Boot接口 → 可视化大屏。1.3 为什么不是单机数据库硬扛这个问题当时纠结了很久。几千条体测数据用Spring Boot加MySQL完全能扛为什么还要引入Hadoop和Spark后来我给自己一个合理的解释这个项目的主体不是“体测数据管理系统”而是“基于大数据的学生体质健康信息系统”。技术考察点在于你是否能独立搭建集群环境、能否用分布式计算框架完成ETL和统计分析、能否把大数据技术栈和业务展示打通。换句话说Hadoop和Spark是课程设计的核心考核对象MySQL只是结果存储附件。另外一个现实理由是扩展性。如果后续接入多个校区、多个学年的数据加入了可穿戴设备采集的实时心率、步数等字段单表单库的查询会迅速劣化。HDFS天然支持文件横向扩展Spark的分布式算子也能在集群扩容后自动并行。所以即便现在数据量不大采用这套方案也为后续扩展留了余地。2. 底层环境伪分布式搭建与集群演进的实践选择2.1 起步阶段为什么选伪分布式课程设计或者毕业设计的环境资源通常很紧张学校实验室一般给你一台8G内存的虚拟机已经是仁至义尽。这种情况下硬上三节点集群不现实所以多数人会选择Hadoop伪分布式模式在单台机器上分别启动NameNode、DataNode、ResourceManager、NodeManager进程模拟一个最小可用的Hadoop环境。伪分布式不是阉割版除了没有多节点的高可用和真正的分布式存储HDFS的命名空间、数据块复制机制、YARN的任务调度流程都是完整的。在这个模式下跑Spark任务能完整体验提交任务、查看Application日志、观察Executor日志的过程这对理解大数据架构至关重要。我搭建时用的是Hadoop 3.3.x版本关键配置就三处core-site.xml里把fs.defaultFS设为hdfs://localhost:9000hdfs-site.xml里把dfs.replication设为1单副本yarn-site.xml里把yarn.nodemanager.aux-services设为mapreduce_shuffle。很多刚接触Hadoop的同学不知道用Maven打包MapReduce任务时如果报找不到系统类需要在mapred-site.xml里配置mapreduce.application.classpath否则提交任务会一直卡在Running状态。启动顺序也有讲究。先执行hdfs namenode -format格式化NameNode再运行start-dfs.sh启动HDFS最后start-yarn.sh启动资源调度。格式化这一步只允许做一次第二次格式化会把原集群的clusterID搞乱导致DataNode起不来。我后面会专门讲这个坑。2.2 从伪分布式到HA集群Hadoop和Zookeeper整合的触发点很多人看到“hadoop和zookeeper整合实战”这个热搜词会疑惑伪分布式不是已经能用了吗为什么还要加Zookeeper答案很简单伪分布式只有一台机器NameNode挂了整个集群就歇菜。进入HA高可用模式后集群至少有两台NameNode一台Active一台Standby需要Zookeeper来协调谁当主、谁从Standby切到Active。真正把Hadoop和Zookeeper整合起来通常发生在两种场景一是课程设计要演示高可用能力二是生产环境不允许单点故障。Zookeeper在这里承担两个职责维护NameNode的主备状态以及存储HDFS的命名空间编辑日志通过JournalNode。配置主线是先启动Zookeeper集群再配置hdfs-site.xml里的nameservices、dfs.ha.namenodes.xxx、dfs.namenode.rpc-address等参数然后配置dfs.ha.automatic-failover.enabledtrue最后执行hdfs zkfc -formatZK格式化Zookeeper状态存储。如果你做的是单机伪分布式Zookeeper不是必需项。但面试或者答辩时一定会被问到“如果NameNode挂了怎么办”所以哪怕不在项目里实际部署也要把整合思路讲清楚。我用三个节点实际验证过手动把Active NameNode进程杀掉大约10秒内Standby节点通过Zookeeper完成状态切换客户端连接自动恢复到新主节点。2.3 环境变量和本地调试的坑Hadoop环境里最容易忽视的是环境变量和本地库。在Windows本机调试Spark任务去读HDFS时经常报找不到Hadoop native库的错误。解决办法是下载对应Hadoop版本的winutils.exe和hadoop.dll放到Hadoop解压目录的bin文件夹并在HADOOP_HOME环境变量指向该目录。否则每次跑Spark作业都会出现平台相关的权限异常看起来像代码问题其实是本地库缺失。还有一个容易踩的坑是用别人“已编译jar包”。网上有些教程会提供已经编译好的Hadoop二进制包但你必须确认这个包对应的Hadoop版本和你系统JDK版本是否一致。比如用JDK 11编译的Hadoop 3.3包放到JDK 8环境里启动ResourceManager会直接抛UnsupportedClassVersionError。我在项目里坚持用Apache官方源码包避免这种黑盒问题。2.4 Docker镜像作为快速验证方案如果你实在不想花一下午搭建伪分布式还有一条捷径直接用现成的Hadoop Docker镜像。国内外社区都有维护好的单节点Hadoop镜像一个docker-compose.yml就能把NameNode和DataNode拉起来端口映射出来直接访问NameNode UI。这种方式非常适合前期把Spark代码跑通、验证数据链路毕竟环境问题不应该卡住你一周。不过我不建议直接把Docker容器当作最终交付环境。答辩时老师大概率会问底层配置细节如果你只说“我用镜像拉起来就跑”会显得对底层原理不熟悉。更好的做法是先用Docker快速验证Spark计算逻辑再手动搭一遍伪分布式集群作为正式演示环境。两条腿走路既节省时间又经得起追问。3. 数据处理主链路从体测原始数据到Spark统计指标3.1 原始数据字段设计与样本格式数据格式设计决定了Spark清洗的工作量。体测原始数据字段一般包括学号、姓名、性别、年级、班级、身高、体重、肺活量、立定跳远、坐位体前屈、50米跑、800米或1000米跑、引体向上或仰卧起坐。注意耐力跑男生是1000米女生是800米引体向上主要针对男生仰卧起坐针对女生。如果字段设计不合理后面按性别统计会很痛苦。我采用JSON Lines格式每条记录占一行方便Spark按行读取。样例数据是这样{student_id:20230101,name:张三,gender:男,grade:2023,class_name:计科2301,height_cm:175.2,weight_kg:68.5,vital_capacity_ml:4120,long_jump_cm:238.0,forward_bend_cm:12.5,run_50m_s:7.3,endurance_run_s:252.0,pull_ups:8}这样设计的好处是字段类型一目了然数字型字段直接以数字类型存储避免后续再转换。唯一的不足是中文姓名在JSON里需要处理编码所以我在Spark读取时显式指定encoding(UTF-8)否则某些环境配置下中文会乱码。3.2 Spark读取JSON与Schema处理Spark读取JSON最直接的方式是spark.read.json(hdfs://localhost:9000/physique/raw/2024/*.json)。如果不指定schemaSpark会默认推断所有字段类型这在大文件场景下会额外扫描一遍数据效率低还可能推断错误。比如student_id这种包含前导0的字段容易被推断成整型丢掉0学号本来就是字符串如果用数值类型存储后面接口返回就出问题。更稳妥的做法是显式定义StructType把每个字段的类型写清楚。我给这个项目定义的精简schema如下from pyspark.sql.types import StructType, StructField, StringType, FloatType, IntegerType from pyspark.sql import SparkSession schema StructType([ StructField(student_id, StringType(), True), StructField(name, StringType(), True), StructField(gender, StringType(), True), StructField(grade, StringType(), True), StructField(class_name, StringType(), True), StructField(height_cm, FloatType(), True), StructField(weight_kg, FloatType(), True), StructField(vital_capacity_ml, FloatType(), True), StructField(long_jump_cm, FloatType(), True), StructField(forward_bend_cm, FloatType(), True), StructField(run_50m_s, FloatType(), True), StructField(endurance_run_s, FloatType(), True), StructField(pull_ups, IntegerType(), True) ]) df spark.read.option(encoding, UTF-8).option(mode, PERMISSIVE).schema(schema).json(hdfs://localhost:9000/physique/raw/2024/*.json)mode(PERMISSIVE)的意思是遇到格式错误或者类型转换失败的行Spark不会直接崩溃而是把整行置为null并记录坏数据后续可以过滤掉。这比FAILFAST模式在生产环境里更实用因为体测原始文件脏数据太常见比如身高写成“168cm”、体重缺空、耐力跑时间写成“4分20秒”等。3.3 体质指标计算逻辑与得分分档清洗完之后核心是计算体质健康指标。这里有两个必算项BMI体重指数和肺活量体重指数。BMI公式是体重公斤数除以身高米数的平方即weight_kg / (height_cm / 100) ** 2。在Spark里用withColumn实现from pyspark.sql import functions as F df df.withColumn(bmi, F.round(F.col(weight_kg) / (F.col(height_cm) / 100) ** 2, 2)) df df.withColumn(vital_index, F.round(F.col(vital_capacity_ml) / F.col(weight_kg), 2))计算出来只是数值实际统计时要按国家标准分档。BMI的分档一般是低于18.5为低体重18.5到23.9为正常24到27.9为超重28以上为肥胖。用when和otherwise表达式就能完成df df.withColumn( bmi_level, F.when(F.col(bmi) 18.5, 低体重) .when(F.col(bmi) 24, 正常) .when(F.col(bmi) 28, 超重) .otherwise(肥胖) )同时还需要根据各项成绩打一个综合等级。我在项目里简化处理先根据耐力和引体向上/仰卧起坐单项及格线判断是否合格再把体检指标加权汇总最后按分数分成优秀、良好、及格、不及格四档。具体算法不复杂关键是这个步骤完全在Spark DataFrame里完成几百行数据处理不到几秒钟就出了结果。3.4 统计结果落库写MySQL还是回写HDFS计算完每个学生的指标之后下一步是聚合统计并落库。我最终选择把统计结果写入MySQL而不是留在HDFS上。原因是Spring Boot项目里MySQL操作最顺MyBatis-Plus直接查表就能用省去额外引入查询引擎。Spark写MySQL的标准方式是使用df.write.jdbc(url, table, properties)props { user: root, password: your_password, driver: com.mysql.cj.jdbc.Driver } df.groupBy(grade, class_name, gender) \ .agg( F.count(student_id).alias(student_count), F.round(F.avg(bmi), 2).alias(avg_bmi), F.sum(F.when(F.col(bmi_level) 肥胖, 1).otherwise(0)).alias(obesity_count) ) \ .write.mode(overwrite).jdbc(jdbc:mysql://localhost:3306/physique_db, class_health_summary, props)mode(overwrite)适合每次Spark任务重新计算后整体刷新统计表保证大屏数据是最新的。要注意的是Spark写JDBC的并发和分区策略不一定最优如果统计表数据量大可以在性能方面调整分区数。对于课程设计这个量级直接写没任何压力。如果把结果回写HDFS也有一个好处保留了中间层数据方便随时用Spark重新跑历史统计。但Spring Boot读HDFS数据比较麻烦通常还得引入Hive或者对象存储客户端链路会变长。我对这个项目的建议是中间结果只保留最细粒度的指标数据在HDFS聚合统计结果写MySQL两者职责不同不要混用。4. Spring Boot服务端数据访问与接口聚合设计4.1 模块化项目结构怎么拆Spring Boot单模块跑通所有业务是最简单的但代码写多了会非常混乱。这个项目我拆成了四个模块用Maven管理physique-parent父工程统一管理依赖版本。physique-common公共实体类、统一返回体ResultT、异常处理。physique-server核心业务模块包含Controller、Service、Mapper。physique-api对外接口模块打包成独立服务。典型目录结构如下physique-server ├── controller │ ├── DashboardController.java │ ├── StudentController.java │ └── ReportController.java ├── service │ ├── HealthStatService.java │ └── impl │ └── HealthStatServiceImpl.java ├── mapper │ ├── ClassHealthSummaryMapper.java │ └── StudentPhysicalMapper.java └── config ├── CorsConfig.java └── MybatisPlusConfig.java模块分离的好处是改接口层不碰公共服务改实体类不碰Controller对于多人协作的课设项目尤其重要。而且Spring Boot的自动配置在多模块下反而更清晰SpringBootApplication放在physique-server里其他模块依赖它打包出的Jar即可。4.2 Spring Boot版本选择与依赖管理版本选择这个坑真的值得单说。很多同学上来就用最新版Spring Boot 3.x然后发现MyBatis-Plus插件不能用、javax.*包找不到、Druid连接池配置失效。Spring Boot 3.x要求JDK 17起步如果你本地是JDK 8直接启动报错。我推荐用Spring Boot 2.7.x作为课程设计和技术学习的稳定方案因为它对JDK 8和Java EE生态的支持非常成熟。项目里关键依赖是MyBatis-Plus和Druid。MyBatis-Plus 3.5.x对Spring Boot 2.7.x兼容性很好基本是引入依赖、扫描Mapper就能用。Druid连接池配置也很顺手spring: datasource: url: jdbc:mysql://localhost:3306/physique_db?useUnicodetruecharacterEncodingUTF-8serverTimezoneAsia/Shanghai username: root password: your_password driver-class-name: com.mysql.cj.jdbc.Driver type: com.alibaba.druid.pool.DruidDataSource这里有个细节MySQL驱动类名新版本是com.mysql.cj.jdbc.Driver老版本是com.mysql.jdbc.Driver。如果你用的连接池版本比较老新驱动类会识别不了。锁版本的时候把MySQL驱动、Druid、Spring Boot统一起来能避免连锁问题。4.3 数据访问层和查询优化因为Spark已经把聚合结果算好了服务端的Mapper层实际上非常简单。比如查询班级体质汇总就是MyBatis-Plus的selectList加条件构造器。我贴一段典型的查询Service public class HealthStatServiceImpl implements HealthStatService { Resource private ClassHealthSummaryMapper classHealthSummaryMapper; Override public ListClassHealthSummary listByGrade(String grade) { LambdaQueryWrapperClassHealthSummary wrapper new LambdaQueryWrapper(); wrapper.eq(ClassHealthSummary::getGrade, grade) .orderByDesc(ClassHealthSummary::getStudentCount); return classHealthSummaryMapper.selectList(wrapper); } }关键优化点有两个。第一给表加索引尤其是grade、class_name、gender这些分组和筛选字段。否则随着数据量增长MySQL回表查询会越来越慢。第二避免循环查询比如大屏需要全校总人数和各年级人数不要写一个for循环逐年级查询而是用一条带GROUP BY的SQL整合返回。MyBatis-Plus支持QueryWrapper里的select(grade, count(*) as total)直接查一个Map列表。4.4 大屏专用接口的聚合返回设计可视化大屏通常会在同一屏展示多个图表如果每个图表都单独请求一个后端接口不仅请求数量多而且前端逻辑分散。我给大屏设计了三个聚合接口每个接口返回一个图表组所需的数据结构/api/dashboard/overview返回总量卡片数据总人数、优秀率、良好率、平均BMI、男女比例。/api/dashboard/trend返回各学年BMI和合格率趋势。/api/dashboard/distribution返回年级/班级维度的分布数据供柱状图和饼图使用。统一返回体是ResultT结构如下{ code: 200, message: success, data: { totalStudents: 4820, excellentRate: 18.6, goodRate: 42.3, passRate: 82.9, avgBmi: 22.3, genderRatio: { male: 48.2, female: 51.8 } } }一个接口能支撑大屏上四个卡片前端只调一次就拿到所有概览数据非常清爽。设计接口时一定要考虑前端渲染成本能聚合就不要拆散。后端每次多查几张表对MySQL来说完全不是瓶颈但前端每次多发起一次HTTP请求在大屏这种场景上会造成明显的加载等待。5. 可视化大屏从数据到图表的落地过程5.1 大屏布局和视觉设计大屏不是普通的后台页面它的环境通常是1920×1080的电视机或者投影幕布。我采用的布局是典型的“上中下结构”顶部是标题栏中间区域放核心指标卡片下方左侧放班级分布柱状图下方中间放BMI分布饼图下方右侧放学年趋势折线图。整体配色以深色底加亮色数据为主深蓝背景配合浅蓝和橙色这样在LED大屏上不会刺眼又足够醒目。布局实现用CSS Grid就够不需要引入重型框架。为了保证不同分辨率下不变形我用了transform: scale方案固定设计稿尺寸1920×1080页面加载时根据屏幕实际宽高比计算缩放比例整体缩放大屏容器。这个做法比rem方案稳定得多也是市面上可视化大屏的常见处理方式。做这个项目时我参考过百度可视化大屏的技术方案也看过DataV和Suger这类低代码平台。它们的优点是拖拽即用但缺点是灵活性有限而且答辩时不好讲原理。所以我最后还是选择用原生Vue3加ECharts手写虽然花的时间会多一些但每个图表如何渲染、数据怎么绑定都一清二楚答辩环节更占优势。5.2 ECharts图表绑定与动态刷新ECharts是大屏图表核心核心用法是初始化实例、传入option对象。我建议把每个图表封装成一个组件接收props传入统计数据内部维护自己独立的echarts实例。折线图的series数据是用后端接口的trend字段直接映射的const trend [ { year: 2021, avgBmi: 22.5, passRate: 80.2 }, { year: 2022, avgBmi: 22.1, passRate: 82.5 }, { year: 2023, avgBmi: 22.4, passRate: 84.1 }, { year: 2024, avgBmi: 22.3, passRate: 82.9 } ]; option { xAxis: { type: category, data: trend.map(item item.year) }, yAxis: { type: value }, series: [ { name: 平均BMI, type: line, data: trend.map(item item.avgBmi) }, { name: 合格率, type: line, data: trend.map(item item.passRate) } ] };动态刷新我用的是setInterval定时轮询每60秒重新请求一次/api/dashboard/overview。这个频率对学校体测数据的实时性足够也不会给后端造成压力。要注意的是组件销毁前必须clearInterval否则页面切走之后定时器还在后台请求数据既浪费资源也会导致报错。如果你考虑实时性更强的场景比如体质监测设备实时上报那建议用WebSocket而非轮询。但课程设计的体测数据一天最多更新一次轮询方案简单可靠完全够用。5.3 大屏渲染性能优化技巧大屏常见问题是图表太多导致页面卡顿。我的经验是控制图表实例数量借助ECharts的setOption做增量更新而不是每次数据变化都销毁重建实例。chart.setOption(option, { notMerge: true, lazyUpdate: true });这里notMerge: true表示整体替换数据项lazyUpdate表示延迟到下一帧再更新能有效减少频繁请求时的抖动。另一个优化是关闭ECharts中不必要动画尤其是数据量大的柱状图动画反而会造成视觉卡顿。用animation: false可以减少渲染开销。如果前端渲染上千条数据还要考虑对原始数据进行聚合再渲染。比如趋势图看的是年度变化前端只需要拿到12个点如果后端一次性返回几千条明细前端反而要做大量DOM操作。这也是我在后端接口里做聚合而不是把明细抛给前端的原因。大屏的数据查询粒度永远要比明细层高一个维度。6. 部署调试全流程复盘踩过的坑与排查思路6.1 Hadoop节点起不来症状像“数据目录损坏”做伪分布式时最典型的故障是第一次跑通之后第二天重启机器发现jps看不到NameNode进程日志里提示Incompatible clusterIDs或者Storage directory already exists。根因几乎都是重复格式化NameNode导致同一个DataNode节点上记录的集群ID和NameNode的集群ID不一致。排查思路要按链路走先执行jps确认哪些进程存在再去logs/hadoop-xxx-namenode-xxx.log里看具体异常然后比对namespaceID和clusterID。如果确认是格式化导致的不一致最简单的解决办法就是停掉所有Hadoop进程删除/tmp/hadoop-*下的临时数据目录然后重新格式化NameNode。前提是HDFS里没有必须保留的数据否则别删。这个坑对于课程设计项目来说是必考的踩坑点因为初学者几乎都会格式化两次以上。我在实际调试中还遇到一个变体Windows虚拟机突然断电DataNode重新启动后一直尝试连接原NameNode失败后来通过清理dfs/nn和dfs/dn目录下的VERSION文件并重建集群才恢复。6.2 Spark读取JSON时的字段类型问题跑Spark任务时常见的现象是某个字段统计出来全是null或者某一行记录直接消失。第一次遇到时以为是数据本身问题后来排查发现是Schema定义和实际数据不一致。最典型的是字符串类型的字段被显式定义成数值类型导致脏数据整行被丢弃。我当时用df.printSchema()排查发现vital_capacity_ml偶发出现null。进一步用df.filter(df.vital_capacity_ml.isNull()).show(20)查看问题数据结果是某几行记录里肺活量字段写成了4120ml这种带单位的值Spark在PERMISSIVE模式下无法转换直接把字段置空。解决办法是清洗时用正则表达式提取纯数字from pyspark.sql import functions as F df df.withColumn( vital_capacity_ml, F.regexp_extract(F.col(vital_capacity_ml_raw), r(\d), 1).cast(float) )这个经验说明一个问题Spark读文件时80%的报错不是集群问题而是数据格式问题。拿到一批新数据后先不要急着算指标先跑一版show()和printSchema()观察数据长什么样再写清洗逻辑效率会高很多。6.3 Spring Boot版本太高引发的依赖冲突Spring Boot版本选择不当会引发连锁反应。我的一个同学从网上找了一个项目模板把Spring Boot升级到3.2.x结果项目里所有原本在javax.servlet包下的代码全部编译报错。因为自Spring Boot 3.0起官方把Java EE API从javax迁移到了jakarta如果你的代码或者第三方依赖还在用老包名必须全面替换。排查依赖冲突的最快方法是执行mvn dependency:tree查看引入的依赖路径。比如我在调试Druid连接池时发现项目里有不同版本的javax.servlet-api和jakarta.servlet-api同时存在导致运行时注入HttpServletResponse报错。锁版本之后问题消失。对于课程设计我给一个实际建议不要追求最新版本选Spring Boot 2.7.x加JDK 8/11即可。网上大多数教学资料、开源模板都基于这个组合遇到问题时更容易查资料。等把大数据链路跑通再回头尝试升级到Spring Boot 3.x也不迟。6.4 大屏接口跨域与超时问题大屏前端一般跑在独立的开发服务器上比如Vite默认代理到http://localhost:8080的Spring Boot服务。前端配置代理后正常情况下不存在跨域问题但如果你直接部署时前端静态资源由Nginx代理、后端独立端口就会出现跨域。这时需要在Spring Boot配置CorsFilter。Spring Boot 2.4之后allowedOrigins对跨域来源有限制推荐用allowedOriginPatterns。我项目里配置如下Configuration public class CorsConfig implements WebMvcConfigurer { Override public void addCorsMappings(CorsRegistry registry) { registry.addMapping(/api/**) .allowedOriginPatterns(*) .allowedMethods(GET, POST, PUT, DELETE, OPTIONS) .allowedHeaders(*) .allowCredentials(true) .maxAge(3600); } }另一个常见问题是超时。大屏第一次打开时如果Spark任务还在跑或者MySQL连接池还没有预热接口可能要等好几秒。解决思路有两个一是给启动流程加一个定时预热任务项目启动后主动调用几次查询接口二是把Spark批处理任务定时凌晨执行保证大屏访问时只查MySQL结果表。后一种方案才是根本解法。6.5 一条完整排查链路大屏数据为什么不对劲以“大屏显示的总人数和实际体测人数不一致”为例完整排查链路是这样的先在浏览器按F12查看Network面板确认接口返回的数据是多少如果总数少了200人再查后端接口SQL看是不是WHERE条件多加了grade参数如果SQL正常再回头查MySQL统计表里到底有多少行如果MySQL数也对问题就出在Spark任务本身大概率是清洗时把某些脏数据的整行丢弃了。那一次就是我把“性别”字段中少数“男 ”带空格的记录清洗成了null然后Spark的count在过滤null时直接忽略掉了。最终解决办法是在groupBy之前统一trim所有字符串字段df df.withColumn(gender, F.trim(F.col(gender)))这条排查链路其实可以提炼成通用方法从最终展示端往前一层一层回查。先看前端拿到的数据再看接口返回再看SQL结果再看数据源。每一步用最容易观察的环节切分不要一头扎进日志里盲目翻找。这套方法在Spark和Spring Boot联调时尤其好用。如果现在重新做一遍我会调整哪些地方项目跑到后期我才意识到真正影响开发效率的往往不是技术难度而是没有在开始前梳理清楚“哪些功能用Spark离线算哪些功能用Spring Boot实时查”。如果让我重做一遍我会在一开始就画好数据分层的边界原始文件只进HDFS中间指标数据只保留在Spark计算层聚合结果只落MySQL前端只读聚合接口。每层职责单一调试时定位问题会快很多。另外我会把Spark任务的运行方式改成通过spark-submit脚本提交而不是在IDE里直接跑。IDE运行Spark任务容易和本地Hadoop配置纠缠在一起作者本人开发机可以跑换一台机器就各种环境问题改用spark-submit后提交命令是标准的环境差异只反映在配置文件里可复现性更好。最后一个经验是文档同步问题。这个项目有源码、有文档还要调试如果文档和代码不同步过了两周自己都看不懂。我在项目里维护了一个简单的MARKDOWN文档记录每次修改的数据字段、Spark任务入口、接口返回结构。虽然麻烦但在答辩前整理材料时会发现那些随手记录的细节才是最终报告里最有价值的内容。做大数据项目数据要分层代码要分层文档同样要分层。
阅读完成 · 觉得有帮助?
咨询建站