基于Hadoop与Spring Boot的电力生产数据分析系统全链路实践
简介这是一份基于 Hadoop 与 Spring Boot 实现的电力生产数据分析系统毕业设计源码包面向计算机相关专业学生、教师及大数据入门者重点解决 HDFS 存储、Yarn 调度、PySpark 数据预处理到 Spring Boot 接口发布、Vue 页面展示的完整链路搭建问题。项目中后端采用 Spring Boot MyBatis 3.3 Druid结合 HDFS 与 Yarn 完成存储调度PySpark 承担清洗与分析并配有电力数据集与 SQL 脚本可用于快速复现电力生产数据的分析全流程。资源压缩包共 369 个文件、约 9.6MB包含 54 个 Java 后端源码、24 个 Vue 前端页面、13 个 Python 脚本、6 个 CSV 数据集、SQL 初始化脚本以及 110 张项目截图和 2 份文档说明整体覆盖源码、文档、截图与搭建指引目录分层清晰便于按需检索。已有 166 人学习浏览适合毕业设计、课程设计、项目立项演示或二次开发代码均经过测试运行成功下载后可参考 README 与技术选型说明完成环境配置并对照截图排查运行问题。1. 基于Hadoop与Spring Boot的电力生产数据分析系统它解决的难题不是数据量而是数据到结果的一整条管道电力生产数据分析系统最容易被低估的一点是单看数据量它并不夸张一台设备五分钟一条记录一天288条很多小厂用MySQL硬扛也能撑一两年。真正让项目变复杂的是多站点多设备堆起来之后的时序密度、脏数据比例和历史归档需求——这时候Hadoop加Spring Boot的组合才算真正派上用场Hadoop这边负责HDFS存原始数据、Hive做离线统计Spring Boot那边负责把分析结果变成接口和数据面板。这套方案尤其适合做课程设计、毕业设计选这个方向的人也适合电力信息化的初级工程师想找一条能看穿全链路的参考实现。标题里挂着“高分项目”说到底评分看的是能不能跑通、文档能不能还原过程、截图上的界面是不是真能对应上代码而不是代码量堆了多少。2. Hadoop侧的数据底座电力数据的存储分层、Hive建仓与最小集群配置2.1 电力生产数据长什么样为什么把它放进Hadoop而不是只靠MySQL电力生产数据的典型来源是SCADA、变电站测控装置、DTU/RTU这类设备采集内容是电压、电流、有功功率、无功功率、频率、设备运行状态这些测点采集周期常见的是5分钟或15分钟一班。这套数据有三个特征一是强时序性几乎所有查询都是按时间范围切片的二是多测点多维度一台设备有几十个测点一个项目可能有几百台设备三是质量参差缺测、突跳、零值、越限都不罕见。如果只用MySQL前半年确实没问题。但单表过亿之后索引维护和聚合查询的响应时间会明显抬头特别是做“按天、按设备、按测点维度”的统计时SQL越写越长慢查询越来越多。而Hadoop路线里HDFS承担的是海量历史文件的归档存储Hive负责把脏乱的原始记录清洗成规整的分析宽表Spark或MapReduce在资源层做分布式计算Spring Boot只跟结果数据打交道。说白了这就像把“仓库”和“店面”拆开Hadoop是仓库数据再多再乱都能收Spring Boot是店面只把客人要的东西摆出来。还有一个现实原因Hadoop伪分布式在一台普通笔记本上就能跑不需要几十台机器。这对课设和毕设特别友好能完整演示“数据入库→清洗→统计分析→接口展示”这条链路写进文档里比空谈架构有说服力得多。如果做完全分布式的高可用通常还要引入ZooKeeper来做NameNode和ResourceManager的选举但起步阶段大可不必。2.2 伪分布式Hadoop的关键配置core-site、hdfs-site与启动命令伪分布式意味着所有Hadoop进程都跑在同一台机器上配置的核心思路是“把集群地址指向本机、把副本数降到1”。我第一次搭这个环境时最容易踩的坑是只改core-site.xml结果NameNode是起来了Datanode却因为HDFS目录权限或版本残留一直连不上。下面这套配置是经过反复验证的最小可用集。core-site.xml里最核心的是fs.defaultFS它决定了客户端、NameNode、DataNode之间通信的默认地址configuration property namefs.defaultFS/name valuehdfs://localhost:9000/value /property property namehadoop.tmp.dir/name value/data/hadoop/tmp/value /property /configurationhadoop.tmp.dir是NameNode和DataNode存放元数据与数据块的根目录默认指向/tmp下系统一重启数据就有丢失风险。我一般会把它单独指定到非系统盘比如/data/hadoop/tmp这算是血泪经验。hdfs-site.xml里重点解决两个问题伪分布式只有一台机器副本数必须改成1同时把NameNode和DataNode的数据目录显式写清楚避免默认路径的权限混乱。configuration 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这里的逻辑是fs.defaultFS让所有组件知道“集群在哪”dfs.replication告诉HDFS“每份数据只存一份”两个dir则把元数据和真实数据放到持久化路径。注意顺序配置改完之后第一次启动前必须执行一次格式化也就是hdfs namenode -format它会生成初始的目录结构和镜像文件。启动命令如下# 先确认JAVA_HOME已导出否则start-dfs.sh会直接失败 echo $JAVA_HOME # 格式化NameNode只在首次部署时执行 hdfs namenode -format # 启动HDFS与YARN $HADOOP_HOME/sbin/start-dfs.sh $HADOOP_HOME/sbin/start-yarn.sh # 验证三个进程都在 jps启动后jps应该能看到NameNode、DataNode、SecondaryNameNode、ResourceManager、NodeManager这五个进程缺一个都说明对应组件没起来。排查顺序一般是先看日志NameNode日志在$HADOOP_HOME/logs/hadoop-hadoop-namenode-*.log这类日志里的关键词比控制台输出有用得多。2.3 用Hive搭ODS-DWD两层数仓建表、清洗与按天分区的SQL落地Hive在这套系统里的定位是“把原始文件变成可分析的宽表”。常见的做法是分两层ODS层直接映射HDFS上的原始CSV文件表结构跟文件字段保持一致不做任何加工DWD层做清洗和规范化过滤异常值、统一时间格式、打上质量标签并按dt天做分区。这样后续所有统计任务都只查DWD层既快又干净。先建ODS外部表外部表的意思是“Hive只管理表的元数据数据文件还在HDFS原位置”这样即使删表也不会误删原始数据对电力数据这种需要审计归档的场景很合适。CREATE DATABASE IF NOT EXISTS ods COMMENT 原始贴源层; CREATE EXTERNAL TABLE IF NOT EXISTS ods.device_metrics ( ts STRING COMMENT 采集时间yyyy-MM-dd HH:mm:ss, device_id STRING COMMENT 设备编号, voltage_a DOUBLE COMMENT A相电压, voltage_b DOUBLE COMMENT B相电压, voltage_c DOUBLE COMMENT C相电压, active_power DOUBLE COMMENT 有功功率 ) ROW FORMAT DELIMITED FIELDS TERMINATED BY , STORED AS TEXTFILE LOCATION /warehouse/ods/device_metrics;这段SQL说明两点字段分隔符必须跟数据文件保持一致常见的是英文逗号LOCATION指向HDFS路径建表前可以用hdfs dfs -mkdir先建目录也可以靠LOCATION自动创建。建好之后Hive把这张表当成“读CSV文件的窗口”文件一放进去查询就能看到。DWD层则改造成分区表按天分区能带来两个直接好处查询时通过分区裁剪只扫当天文件同时每天的数据天然独立重新跑某一天的任务不会影响其他日期。CREATE DATABASE IF NOT EXISTS dwd COMMENT 清洗明细层; CREATE TABLE IF NOT EXISTS dwd.device_metrics_clean ( ts TIMESTAMP, device_id STRING, voltage_avg DOUBLE, active_power DOUBLE, is_abnormal STRING ) PARTITIONED BY (dt STRING) STORED AS PARQUET; INSERT OVERWRITE TABLE dwd.device_metrics_clean PARTITION (dt) SELECT from_unixtime(unix_timestamp(ts, yyyy-MM-dd HH:mm:ss)) AS ts, device_id, ROUND((voltage_a voltage_b voltage_c) / 3, 2) AS voltage_avg, active_power, IF(active_power 0 OR active_power 500, abnormal, normal) AS is_abnormal, SUBSTR(ts, 1, 10) AS dt FROM ods.device_metrics WHERE SUBSTR(ts, 1, 10) 2024-06-01;这里的参数含义from_unixtime配合unix_timestamp是把字符串时间转成标准TIMESTAMP格式SUBSTR(ts,1,10)取出日期作为分区字段ROUND保留两位小数IF条件判断功率是否越限。INSERT OVERWRITE是幂等操作重复执行同一批次不会产生重复数据这点在重新清洗某天数据时非常重要。2.4 什么时候不需要Hadoop小数据量方案的边界这套架构不是万能药。如果数据总量只有几百万行、查询也基本都是单表简单过滤那么Hadoop带来的收益完全抵消不了部署和维护成本。我见过有人把几千条设备台账也塞进HDFS查个表还要等YARN分配容器体验极差。合理的判断标准有两个一是单表数据量是否过亿并且还在持续增长二是是否存在跨月的批量聚合统计需求。两个都不满足用MySQL加定时任务反而更顺。3. Spring Boot和Hadoop怎么对话配置、JDBC、接口与定时任务的代码落地3.1 不靠Spring Data Hadoop用原生FileSystem API读HDFS很多教程会提Spring Data Hadoop但这套组件维护已经停止版本适配也一直跟不上新项目里用它纯属自找麻烦。常见做法是直接在Spring Boot里引入hadoop-client依赖用Hadoop原生API操作HDFS反而简单直接。唯一要注意的是版本号必须跟集群主版本对齐不要在pom里随手填一个不相关的数字。dependency groupIdorg.apache.hadoop/groupId artifactIdhadoop-client/artifactId version${hadoop.version}/version /dependency版本变量${hadoop.version}我建议在properties里统一管理比如装的是Hadoop 3.x就填对应的3.x版本号。接着写一个配置类把集群地址注入Bean这样业务代码里直接注入FileSystem就能用不用每个类都重复创建Configuration。Configuration public class HadoopConfig { Value(${hadoop.fs.defaultFS}) private String fsDefaultFS; Bean public Configuration hdfsConfiguration() { Configuration conf new Configuration(); conf.set(fs.defaultFS, fsDefaultFS); conf.set(dfs.replication, 1); conf.set(fs.hdfs.impl, org.apache.hadoop.hdfs.DistributedFileSystem); return conf; } Bean public FileSystem fileSystem() throws IOException { return FileSystem.get(hdfsConfiguration()); } }这里的关键是fs.hdfs.impl这一行某些环境里不显式指定HDFS实现类FileSystem.get会返回本地文件系统导致路径解析全错、读写异常这是个很隐蔽的坑。application.yml里对应配置如下hadoop: fs: defaultFS: hdfs://localhost:9000注入了FileSystem之后读HDFS上的文件就和读本地路径非常相似Path path new Path(/warehouse/ods/device_metrics/device_metrics.csv); try (FSDataInputStream in fs.open(path); BufferedReader reader new BufferedReader(new InputStreamReader(in))) { String line; while ((line reader.readLine()) ! null) { // 按行解析第一条是表头就跳过 System.out.println(line); } }这段代码的整体逻辑是Path指向HDFS上的目标文件FSDataInputStream是HDFS输入流用BufferedReader按行读出来处理。参数上要注意的是Path的根路径对应的是HDFS根目录不是Linux根目录经常有人把本地路径习惯带进来导致FileNotFound。3.2 通过HiveServer2把查询结果接进Service层HDFS负责原始文件存储但业务接口不需要直接算原始数据更合理的做法是让Hive把统计结果算好Spring Boot通过JDBC查询结果。这就需要在Hive侧启动HiveServer2组件然后再配置一个JDBC数据源指向它。Component public class HiveQueryService { private static final String HIVE_URL jdbc:hive2://localhost:10000/dwd; public ListMapString, Object queryLoadCurve(String deviceId, String date) { String sql SELECT ts, active_power FROM dwd.device_metrics_clean WHERE device_id ? AND dt ? ORDER BY ts; try (Connection conn DriverManager.getConnection(HIVE_URL, hive, )) { PreparedStatement ps conn.prepareStatement(sql); ps.setString(1, deviceId); ps.setString(2, date); ResultSet rs ps.executeQuery(); ListMapString, Object result new ArrayList(); while (rs.next()) { MapString, Object row new HashMap(); row.put(ts, rs.getTimestamp(ts)); row.put(activePower, rs.getDouble(active_power)); result.add(row); } return result; } catch (SQLException e) { throw new RuntimeException(查询Hive失败, e); } } }两点说明Hive的JDBC驱动类是org.apache.hive.jdbc.HiveDriver需要在pom里额外引入hive-jdbc依赖并且加了依赖之后要用Class.forName注册驱动或者在连接串里显式指定否则会报找不到驱动这里的占位符方式是为了防止设备编号和日期被字符串拼接污染和传统数据库的防注入思路一致。HiveServer2的连接比较重每次查询都新建连接开销不小。如果查询频率高可以用连接池包一层常见配置如HikariCP指向同一个JDBC URL参数上把maximumPoolSize控制在5左右就够了毕竟Hive本身不是为高并发查询设计的。3.3 对外接口的长尾设计负荷曲线、电量统计、越限告警系统对外最常用的三类接口是按设备按日查负荷曲线、按站点按周查电量统计、按阈值查越限告警。这三类刚好对应电力生产分析里的“看趋势、看总量、看异常”三个视角。Controller层做得薄一点业务逻辑全部收口到Service层这样后续换数据源或加缓存都方便。RestController RequestMapping(/api/analysis) public class AnalysisController { private final HiveQueryService hiveQueryService; public AnalysisController(HiveQueryService hiveQueryService) { this.hiveQueryService hiveQueryService; } GetMapping(/load-curve) public ApiResult loadCurve(RequestParam String deviceId, RequestParam String date) { // 参数有效性校验放在service里controller只做转发 ListMapString, Object data hiveQueryService.queryLoadCurve(deviceId, date); return ApiResult.success(data); } GetMapping(/energy-summary) public ApiResult energySummary(RequestParam String siteId, RequestParam String startDate, RequestParam String endDate) { ListMapString, Object data hiveQueryService.queryEnergySummary(siteId, startDate, endDate); return ApiResult.success(data); } GetMapping(/abnormal-alert) public ApiResult abnormalAlert(RequestParam String date) { ListMapString, Object data hiveQueryService.queryAbnormalAlert(date); return ApiResult.success(data); } }Controller层每个接口只做三件事接收参数、调用Service、统一封装返回值。ApiResult里的code、message、data三段结构是这套系统对外交流的统一格式前端和后端联调时不会因为字段命名不一致反复改签。Service层里的阈值参数比如越限告警的功率上限500kW不要硬编码在代码里放到配置中心或数据库参数表里运维调整阈值时不用重新发版。3.4 接口层参数怎么调超时、fetch size、并行度Spring Boot与Hive交互时最容易出现的问题是默认参数不适合大数据量查询导致接口超时或内存溢出。以下是几个我踩过之后固定下来的参数基线参数推荐值说明socketTimeout60000 msHive JDBC长任务容易超过默认的几秒超时调到60秒以上fetchSize1000控制ResultSet每次拉取的行数避免一次性加载全部结果hive.exec.paralleltrue允许Hive内部并行执行子任务注意在Hive侧配置maximumPoolSize5连接池不宜过大Hive是分析引擎不是OLTPmapreduce.map.memory.mb1024单个Map任务内存根据数据量调整默认经常偏小socketTimeout和fetchSize是JDBC连接串级别的参数写法是在JDBC URL后面拼参数比如jdbc:hive2://localhost:10000/dwd?socketTimeout60000;fetchSize1000。mapreduce.map.memory.mb则是在Hive执行前通过set命令设置也可以在提交任务时带上。4. 电力数据平台搭建的常见问题排查5个踩坑记录与对应解法4.1 本地Spring Boot写HDFS被权限拒绝现象在Windows或Linux本地启动Spring Boot往HDFS上传文件时报Permission denied但命令行里用hdfs dfs -put却一切正常。原因FileSystem.get在未指定用户时会取当前系统用户名如果你用本机用户名去访问HDFS而HDFS里没有对应用户默认就会被拒绝。命令行正常是因为hdfs命令默认以HADOOP_USER_NAME指定的身份运行。解决要么在启动脚本里统一导出环境变量HADOOP_USER_NAMEhdfs也就是设置一个在所有环境里都生效的HDFS登录身份要么在代码里用UserGroupInformation.createRemoteUser(hdfs)显式包装登录线程后者的好处是不同环境可以通过配置切换不用改系统环境变量。4.2 Hive与Hadoop版本对不上导致MetaStore起不来现象启动Hive的MetaStore服务时日志里出现Unsupported class file major version或元数据表不存在的报错beeline连接也被拒。原因JDK、Hadoop、Hive三者版本矩阵不匹配。比如用了新版本JDK去跑老版本Hive字节码级别直接不兼容或者Hive的元数据库没有初始化就启动服务Hive会直接拒绝工作。解决先定Hadoop主版本再对照官方版本兼容表选Hive版本最后选一个该组合支持的JDK版本常见稳妥组合是JDK 8或11配合Hadoop 3.x。任何组件升级后用schematool重新校验元数据库不要只靠看版本号拍板。注意不要用太新的JDK 17硬跑老Hadoop发行版会翻车。4.3 YARN容器反复被杀任务卡在Running现象提交一个Hive聚合任务后进度一直卡在Running且反复失败日志中出现“Container is running beyond virtual memory limits”或物理内存超限字样。原因YARN的NodeManager默认开启vmem-check-enabled会严格校验容器虚拟内存上限而虚拟内存估算经常是物理内存的数倍在不调整参数的情况下容器容易被误杀。解决在yarn-site.xml中调整两个参数关闭虚拟内存校验并提高物理内存比例property nameyarn.nodemanager.vmem-check-enabled/name valuefalse/value /property property nameyarn.nodemanager.vmem-pmem-ratio/name value4/value /property同时检查yarn.nodemanager.resource.memory-mb是否和机器实际内存相符伪分布式单机时尤其容易漏配这个参数默认值往往远小于物理内存导致容器频繁被挤掉。4.4 按天统计的聚合任务越跑越慢数据倾斜现象统计各站点逐日电量时大部分日期的任务几十秒完成个别日期跑几十分钟还在转圈yarn application -list里看到大量Map任务早已完成只剩几个Reduce任务在慢慢磨。原因个别站点数据量远大于其他站点比如某个重点站有几百台设备Reduce端分配不均形成数据倾斜。解决先从简化聚合口径做起在SQL里对热点键加salting前缀做两级聚合也可以打开Hive的倾斜优化开关设置hive.optimize.skewjointrue让Hive在Join阶段自动拆分大键。如果确认热点集中在设备维度把业务查询改成先按设备聚合再按站点汇总也能明显缓解。4.5 NameNode格式化不是后悔药二次格式化把数据清空了现象因为元数据异常想“重置”集群手滑执行了hdfs namenode -format结果原先所有表和数据全部不可见DataNode上报的块信息和NameNode对不上集群就像一个空壳。原因format会重建NameNode的元数据镜像但DataNode磁盘里的实际数据块还在。NameNode的新镜像与DataNode的块报告不一致集群进入异常状态。解决第一原则是format前先确认目录数据是否做过备份需要保留数据就不要轻易格式化。若数据还重要尝试从dfs.namenode.name.dir下的备份镜像恢复如果数据本来就允许删除则同步清空dfs.datanode.data.dir目录并重新格式化。格式化不是后悔药这个动作的代价是整份HDFS数据的元数据重建。5. 从零搭建到完整自检伪分布式起步的一套可复现步骤5.1 部署顺序清单从JDK到HiveServer2从头部署这套系统顺序固定为先装JDK再装Hadoop启动HDFS与YARN接着装Hive并初始化元数据库最后启动HiveServer2供Spring Boot连接。顺序反了会导致后面所有组件都找不到依赖。# 0) 环境预检JDK与SSH免密 java -version ssh localhost # 1) 启动HDFS与YARN $HADOOP_HOME/sbin/start-dfs.sh $HADOOP_HOME/sbin/start-yarn.sh # 2) 验证HDFS健康状态 hdfs dfsadmin -report # 3) Hive初始化元数据库并启动两个服务 $HIVE_HOME/bin/schematool -dbType derby -initSchema nohup $HIVE_HOME/bin/hive --service metastore /data/hive/metastore.log 21 nohup $HIVE_HOME/bin/hive --service hiveserver2 /data/hive/hiveserver2.log 21 # 4) 用beeline验证HiveServer2可用 $HIVE_HOME/bin/beeline -u jdbc:hive2://localhost:10000 -e show databases;最后一步能正常打印出default等数据库列表就说明Hive整条链路是通的。注意启动两个Hive服务最好用nohup挂后台并写日志文件否则终端一关服务就断这是很常见的翻车点。5.2 造一份模拟SCADA数据灌进ODS层没有真实电力数据时用脚本生成一份符合SCADA格式的模拟数据是让系统先运作起来的常用做法。下面用Python生成20台设备、每台一天288条、5分钟周期的CSV文件import random import datetime import csv start datetime.datetime(2024, 6, 1) device_ids [fD{100 i} for i in range(20)] rows [] for i in range(20 * 288): ts start datetime.timedelta(minutes5 * i) device device_ids[i % 20] base random.uniform(95, 105) voltages [round(base random.uniform(-5, 5), 1) for _ in range(3)] power round(random.uniform(10, 200), 2) rows.append([ts.strftime(%Y-%m-%d %H:%M:%S), device] voltages [power]) with open(device_metrics.csv, w, newline) as f: writer csv.writer(f) writer.writerow([ts, device_id, voltage_a, voltage_b, voltage_c, active_power]) writer.writerows(rows)这段脚本的核心是生成具有合理时序规律的模拟数据时间戳按5分钟递增电压在95到105伏之间随机波动功率按10到200千瓦的区间模拟负荷变化。生成之后导入HDFS然后在Hive里执行建表语句就能查了hdfs dfs -mkdir -p /warehouse/ods/device_metrics hdfs dfs -put device_metrics.csv /warehouse/ods/device_metrics/5.3 跑通一次完整分析并自检HDFS健康、Hive任务、接口返回数据进仓后按顺序跑一遍完整链路并做自检比等到最后出问题再排查效率高得多。先把第2.3节的建表和清洗SQL存成init_ddl.hql用beeline批量执行再执行一条统计SQL验证DWD层数据# 建库建表并跑清洗任务 $HIVE_HOME/bin/beeline -u jdbc:hive2://localhost:10000 -f init_ddl.hql # 验证清洗结果 $HIVE_HOME/bin/beeline -u jdbc:hive2://localhost:10000 \ -e SELECT dt, COUNT(*), ROUND(AVG(voltage_avg),2), SUM(active_power) FROM dwd.device_metrics_clean GROUP BY dt;正常情况下返回一天的数据量、平均电压和总功率。接着启动Spring Boot用curl验证接口能取到同一份数据curl http://localhost:8080/api/analysis/load-curve?deviceIdD100date2024-06-01如果返回JSON里能看到288条负荷点说明HDFS、Hive、Spring Boot三方已经全部打通。这套自检流程重复执行几次整个项目的可信度就建立起来了。6. 把系统交出去之前验证数据一致性和可复现性的两个习惯6.1 用结果对账代替看日志交接前最有效的验证方式不是看日志里有没有报错而是做一次结果对账。Hive算一遍总量接口返回再算一遍两边对得上才算真正闭环。检查项操作方式期望结果HDFS健康hdfs dfsadmin -report各节点Storage Size大于0无丢失块Hive数据完整SELECT COUNT(*), SUM(active_power) FROM dwd表数值与源数据核对一致接口连通curl负荷曲线接口返回288条负荷点时间戳连续告警准确对比清洗结果与告警接口越限记录数与SQL查询一致文档可复现按部署文档从空机器重来一遍全流程无跳步跑通6.2 可复现性把启动命令和脚本收成一套文件项目交给别人之前把部署顺序、建表语句、模拟数据生成脚本、接口说明整理成init.sh、init_ddl.hql、README.md这几个文件保证任何人照着文件能从零跑通。这个习惯救过我很多次过了半年回来维护时靠的就是这套文件而不是记忆。另外接口层建议加一层参数合法性校验日期格式必须符合yyyy-MM-dd设备编号不允许包含非法字符避免脏参数直接打到Hive查询上拖慢整个队列。这就是我做这类数据系统的习惯启动顺序全部脚本化字段类型严格对齐对账不通过不发版。希望帮到你。本文还有配套的精品资源点击获取