简介面向计算机相关专业大学生这套基于Spring Boot的共享单车用户大数据分析项目是一份完整的课程设计参考资料。项目整合Hadoop、Hive、ECharts与百度地图API从前端可视化到后端接口再到底层数据存储完整覆盖了高校大数据课程设计从环境搭建到成果展示的主要环节。压缩包共128个文件大小5.64MB其中37个Java源文件承载核心业务逻辑10个JavaScript脚本和3个HTML、CSS页面构成前端可视化界面JSON、XML、SQL、JAR等文件负责配置、数据导入与依赖管理另有45张PNG图片可用于报告配图或答辩演示。包内还包含Hive日志gz包和mylog日志文件方便对照数据接入与运行过程。目前已有1372人学习下载对正在准备大数据或Spring Boot方向课程设计的学生而言这份源码、配置与可视化页面可直接参考能帮助理解技术栈整合、项目分层与数据可视化实现细节。1. 这个共享单车大数据项目到底在分析什么拿到一个叫基于 Spring Boot 的共享单车用户大数据数据分析项目.zip的压缩包时先别急着解压跑起来。它名义上是一个毕设或简历项目实际上是一条完整的用户行为数据链路底层是千万级骑行订单包含用户ID、车辆ID、起终点经纬度、开锁时间、骑行时长中间是 Spark 负责的清洗、聚合、用户聚类上层是 Spring Boot 应用把计算结果包成接口最终落到数据大屏上。它解决的问题很具体共享单车平台想知道哪些用户在创造价值、哪些用户快流失了、早晚高峰的潮汐车辆应该往哪里调度。适合正在做大数据项目的人也适合想把分析能力嵌进现有系统的后端工程师——不需要搭一套重型数仓一个 Spring Boot 进程加一个 Spark 任务就能形成闭环。2. 解压 zip 之后怎么下手项目骨架与数据分层这类 zip 项目的通病是解压即迷茫目录结构不统一数据文件缺胳膊少腿pom 依赖版本乱到让人想摔键盘。我拿到手之后不会直接执行 mvn spring-boot:run而是先确认三件事依赖版本是否兼容、数据文件是否完整、配置里数据分析的核心路径指向哪里。确认完这三件事再开始动手改。2.1 共享单车用户分析项目的三层拓扑与模块划分常见做法是把这个项目拆成三个接入点分别对应数据接入、离线计算、接口服务三层而不是一个 Spring Boot 应用里什么都干。接入层原始骑行数据上报接口。真实生产环境中车锁的 IoT 网关会往这个接口推 JSON 报文毕设项目里这个过程通常被简化成一个批量导入入口把 CSV 一行行读进 MySQL。计算层把 MySQL 里的明细数据导出交给 Spark 做用户级计算。为什么不让 Spring Boot 直接跑 SQL因为订单量到千万级时select user_id, count(*) from order group by user_id在单机 InnoDB 里跑一次要几分钟而 Spark 分布式跑同样的逻辑只要几十秒。更关键的是用户聚类这种算法单机 SQL 写不出来。服务层Spring Boot 的 Controller 把计算层的聚合结果读出来按前端大屏需要的格式返回。这个拓扑其实和大数据集群部署策略里讲的离线链路是一个模子采集、存储、计算、服务。你不一定需要七台机器一台 16G 内存的笔记本就能跑通因为 Spark 可以以local[*]模式跑在 JVM 里。如果你习惯 springboot modules 那种多模块组织可以把 job 模块和 web 模块拆成两个 Maven module编译期就隔离掉 Spark 与 Web 容器的依赖污染。解压之后一个结构清晰的项目一般长这样share-bike-analysis/ ├── data/ │ ├── ods/ │ │ └── ride_data_20240101.csv │ ├── dwd/ │ │ └── user_ride_day_20240101.csv │ └── ads/ ├── sql/ │ ├── schema.sql │ └── ads_table.sql ├── src/main/java/com/example/bike/ │ ├── config/SparkConfig.java │ ├── job/UserClusterJob.java │ ├── controller/DashBoardController.java │ └── service/UserClusterService.java ├── src/main/resources/ │ └── application.yml └── pom.xml这里 data 目录决定你后面能不能复现。很多这类 zip 里数据文件不是完整放进去的而是只放了几万行脱敏样例有的干脆只给一个 README 让你自行准备数据。所以第一件事是确认 ods 下有原始 CSV如果没有就得自己写一段模拟数据生成脚本否则 Spark 算起来全是空表。2.2 pom.xml 里的关键依赖怎么选Spring Boot 版本与 Spark 版本搭配我在 pom.xml 里见过太多翻车示例问题几乎都出在版本搭配上。Spring Boot 自带的 spring-boot-starter-web 用的是内嵌 Tomcat而 Spark 某些历史版本会在传递依赖里带旧版的 Netty 和 Jackson两个 jar 放在同一个 classpath 里直接 NoSuchMethodError。常见做法是死守一套经过验证的组合Java 8 Spring Boot 2.7.x Spark 3.x。先看一个典型的最小依赖清单properties java.version1.8/java.version spring.boot.version2.7.18/spring.boot.version spark.version3.4.1/spark.version hadoop.version3.3.4/hadoop.version /properties dependencies dependency groupIdorg.springframework.boot/groupId artifactIdspring-boot-starter-web/artifactId /dependency dependency groupIdorg.mybatis.spring.boot/groupId artifactIdmybatis-spring-boot-starter/artifactId version2.3.2/version /dependency dependency groupIdorg.apache.spark/groupId artifactIdspark-core_2.12/artifactId version${spark.version}/version /dependency dependency groupIdorg.apache.spark/groupId artifactIdspark-sql_2.12/artifactId version${spark.version}/version /dependency dependency groupIdorg.apache.hadoop/groupId artifactIdhadoop-client/artifactId version${hadoop.version}/version /dependency /dependencies代码里的几个关键点spark-core 和 spark-sql 必须带 _2.12 后缀因为 Spark 3.x 官方只发布基于 Scala 2.12 和 2.13 的工程产物。spark-sql 是实际做 DataFrame 分析的核心库spark-core 通常会被传递依赖带进来但显示声明是约定俗成后面排除冲突依赖时也更容易定位。hadoop-client 提供 HDFS 的 FileSystem 类即使你只读本地文件也要带上否则 Spark 底层初始化会因为找不到org.apache.hadoop.conf.Configuration直接报 ClassNotFoundException。mybatis 不是必需但这类项目十有八九要把结果写回 MySQL或者从 MySQL 读业务配置所以放进来。最需要警惕的是 spring.boot.version 别追最新。Spring Boot 3.x 强制 Java 17Spark 3.4 对 Java 17 的支持并不完善很多用过的人都反馈springboot版本太高会让 Spark 在 GC 和反射上出现奇怪的内存问题。稳妥组合就是 Java 8 Spring Boot 2.7.x Spark 3.x这个搭配在社区沉淀最久搜任何报错都有现成答案。2.3 数据分层与配置ODS、DWD、ADS 如何落地就算只是做一个数据分析项目我也建议把 data 目录按数仓分层来组织因为面试官一定会问你的数据是怎么流转的。按层分目录的成本极低收益却很大。ODS 层存放原始 CSV字段和车锁上报格式完全一致。比如这样几行user_id,order_id,bike_id,lat,lng,start_time,end_time,ride_minutes U10001,O2024010100001,B3001,31.2304,121.4737,2024-01-01 08:02:00,2024-01-01 08:24:00,22 U10002,O2024010100002,B3002,31.2104,121.4537,2024-01-01 08:03:00,2024-01-01 08:41:00,38DWD 层做清洗明细把经纬度非法、时长小于 10 秒、骑行时长超过 6 小时的脏数据过滤掉新增城市、日期、小时等维度列。ADS 层是聚合结果按用户、日期、城市聚合出来的指标最终落入 MySQL 表给前端读。在 application.yml 里把路径全部暴露成配置项不要写死在代码里。这里是一份参考配置share: bike: data: ods-path: /opt/data/ods dwd-path: /opt/data/dwd ads-path: /opt/data/ads spark: master: local[2] app-name: share-bike-user-analysis driver-memory: 2g # 参与计算的并行度本地调小集群调大 shuffle-partitions: 6这段参数的用意很直白。master 配成local[2]表示用本机两个线程模拟分布式适合开发和毕设如果要去集群上跑改成yarn-client同时把 DWD 输出路径改成hdfs://开头。driver-memory 给 1g 到 2g 足够单机模式下 driver 和 executor 在同一个 JVM 里给太大反而挤压执行内存。shuffle-partitions 默认是 200在本地小数据量下会产生 200 个小文件又丑又慢我一般直接调成和 CPU 核数相关的小值比如 6到集群上再调回几百。这里插一句选型建议有人一看到大数据项目就上 springboot整合flink但如果你只是做用户聚类和离线指标Spark 的 MLlib 和 DataFrame API 比 Flink 顺手得多。Flink 更适合做实时骑行轨迹监控那是另一个方向。这个项目的标题和绝大多数同类毕设走离线 Spark 就够了。3. 把用户分析算进 Spring BootSpark Job 与指标设计这一章是整个项目能不能打动人的核心。共享单车的数据分析不是把订单 count 一下而是要对用户行为做刻画。3.1 为什么在 Spring Boot 里跑 Spark而不是一味用 SQL单独说下选型。我见过一些团队喜欢什么计算都用 SQL 解决MySQL 里跑 join、跑聚合、跑窗口函数数据量到百万还行千万级就明显吃力尤其碰上group by user_id这种大分组。共享单车一个一线城市的年度明细轻松过千万按天分表才扛得住但分表之后要分析一个用户一年的骑行偏好SQL 得把几十张表 union 起来执行计划又臭又长。用 Spark 的优势在于它把读 MySQL 表当成读一个分布式数据集。你可以在 Spark 里用 JDBC 读全部明细也可以用read.parquet读 ODS 目录然后在 DataFrame API 里统一做清洗和聚合。用户聚类、轨迹相似度、时段分桶这类需求Spark MLlib 直接提供了 KMeans、决策树等算法不用自己从零写矩阵运算。这一点在 spark数据分析案例 里几乎算通用共识。另一个隐性好处是Spring Boot 应用通常长驻而 SparkSession 很重。如果每次请求都 new 一个 SparkSession光启动就花二十秒。常见做法是用Configuration把 SparkSession 做成单例 Bean启动时初始化一次Job 运行时只提交任务。Configuration public class SparkConfig { Bean public SparkSession sparkSession(Value(${share.bike.spark.master}) String master, Value(${share.bike.spark.app-name}) String appName) { return SparkSession.builder() .master(master) .appName(appName) .config(spark.sql.session.timeZone, Asia/Shanghai) .config(spark.sql.shuffle.partitions, 6) .getOrCreate(); } }这个配置有两个容易被忽略的坑。第一spark.sql.session.timeZone必须显式设置否则后面按小时聚合时Spark 默认用 UTC骑行凌晨零点会被归到前一天或后一天。第二shuffle.partitions 默认 200本地数据量小的时候会产生大量空任务浪费资源调成小值集群上输出到 HDFS 时再调回几百。你还可以把 spark.sql.adaptive.enabled 设为 true让 Spark 自动优化 join 和聚合的执行计划。3.2 用户画像的两个核心指标活跃时段与骑行偏好分析之前先定义指标。我见过不少项目一上来就写分析用户最后结果是一堆 count 和 avg既没有业务含义也不可解释。共享单车用户分析最重要的两个维度时间维度的活跃规律空间维度的骑行偏好。活跃时段指用户在一周内不同小时的骑行次数分布。早高峰 7 点到 9 点和晚高峰 17 点到 20 点出现两个明显的峰是通勤型用户骑行集中在午后或工作日晚上的可能是休闲型。这个分布不只是一张图它能得出运营动作通勤型用户推月卡休闲型用户推周末次卡。骑行偏好指用户常用起终点区域。这里不要直接对经纬度聚类因为经纬度是两个数值列KMeans 在欧氏距离下会把距离很近但分属不同街道的点硬切一刀。常见做法是先把经纬度用 GeoHash 编码成 7 位字符串然后映射成整数特征再喂给 KMeans。这样聚出来的每一类都能回查到这波人常年从地铁口骑到写字楼。3.3 最小可复现的 KMeans 用户聚类 Job下面这个 Job 是我常用在同类项目里的最小版本。输入是 DWD 层用户日聚合表输出是用户 ID 聚类标签 特征向量三类信息结果写进 MySQL ADS 表。Component public class UserClusterJob { private final SparkSession spark; public UserClusterJob(SparkSession spark) { this.spark spark; } public void run(String date) { DatasetRow df spark.read().option(header, true) .csv(/opt/data/dwd/user_ride_day_ date .csv); DatasetRow features df .select( df.col(user_id), df.col(weekday_rent_cnt).cast(double).as(weekday_cnt), df.col(weekend_rent_cnt).cast(double).as(weekend_cnt), df.col(avg_ride_minutes).cast(double).as(avg_minutes), df.col(night_ratio).cast(double).as(night_ratio)) .filter(avg_minutes 0 and night_ratio 0 and night_ratio 1); DatasetRow indexed new VectorAssembler() .setInputCols(new String[]{weekday_cnt, weekend_cnt, avg_minutes, night_ratio}) .setOutputCol(features) .transform(features); KMeans kmeans new KMeans() .setK(4) .setSeed(42L) .setMaxIter(20) .setFeaturesCol(features) .setPredictionCol(cluster); KMeansModel model kmeans.fit(indexed); DatasetRow labeled model.transform(indexed) .select(user_id, cluster, features); labeled.write() .mode(overwrite) .jdbc(JDBC_URL, ads_user_cluster, jdbcProps()); } }逻辑分四步读 DWD CSV选出参与聚类的四列并强转 double避免 CSV 读进来全是 string用 VectorAssembler 把四列拼成向量列 features喂给 KMeansfit 训练模型transform 给每条用户记录打上 cluster 标签最后写 MySQL ADS 表。参数是主要调试点。setK(4) 不是拍脑袋定的一般先用肘部法则跑一遍观察 K 从 2 到 10 的误差平方和曲线在拐点选 K。setSeed(42L) 保证可复现否则同一份数据每次跑出来的簇号可能对不上。setMaxIter(20) 是 KMeans 迭代上限如果损失还没收敛就加大到 50对照训练日志里的目标函数值判断。avg_ride_minutes 这类时长特征分布通常很偏建议聚类前先做一次 log1p 变换否则它会压制 weekday_cnt 和 night_ratio 这些 0 到 1 区间的特征让它们几乎不起作用。4. 用接口把分析结果喂给数据大屏数据大屏是最能直观体现项目价值的交付物。但大屏也最容易暴露一个工程问题服务端把所有明细丢给前端前端在浏览器里算聚合于是 ECharts 渲染几万行数据直接卡死。这类qt 表格大数据卡顿优化 tablewiget 到 qtableview 自定义 model的背后其实是同一个原则大数据展示的正确姿势是服务端先把聚合算好前端只做绘图。4.1 Controller 层只返回聚合结果别返回明细Controller 的设计原则我总结成一句话接口返回的每一行都是大屏上一个图表的直接数据源。折线图要的是日期 骑行量派单图要的是城市 订单量 时间片雷达图要的是用户类别 属性均值。原始骑行记录不下发。RestController RequestMapping(/api/dashboard) public class DashBoardController { Autowired private UserClusterService clusterService; GetMapping(/hourly-ride-amount) public RMapString, Long hourlyRideAmount(String city, String date) { MapString, Long data clusterService.hourlyRideAmount(city, date); return R.ok(data); } GetMapping(/user-cluster-overview) public RListClusterOverviewVO userClusterOverview() { return R.ok(clusterService.clusterOverview()); } }R 是统一的返回包装类状态码、消息、数据三件套。city 和 date 都通过 RequestParam 传date 格式统一成yyyy-MM-dd不要用 Date 对象在前后端交接时间格式的坑后面专门说。hourlyRideAmount 返回 Map键是 07:00 这种字符串值是对应骑行量前端直接拿 key 当 x 轴不需要再解析时间戳。ClusterOverviewVO 落成独立 VO 类字段只放前端需要的。public class ClusterOverviewVO { private Integer clusterId; private Long userCount; private Double avgRideMinutes; private String activeHour; private Double avgDistanceKm; }只有一个 clusterId、userCount、avgRideMinutes、activeHour、avgDistanceKm。切忌把 Spark 计算时的内部字段直接序列化出去一方面暴露实现细节另一方面 features 向量序列化成 JSON 是一长串数组大屏根本用不上。对应的 Mapper 查询也不需要复杂SELECT cluster_id, COUNT(*) AS user_count, AVG(avg_ride_minutes) AS avg_ride_minutes, AVG(avg_distance_km) AS avg_distance_km FROM ads_user_cluster GROUP BY cluster_id;这张表在 Spark Job 落地时就是按用户粒度存的所以 Mapper 这边一次 group by 就能拿出大屏要的数据。执行计划里要注意对 cluster_id 建索引否则前端一刷新数据库就要扫全表。4.2 Redis 缓存与定时刷新策略大屏的核心特征是多客户端同看、要稳定。每个用户刷新一次大屏后端就去 Spark 算一次三千万条数据那接口就废了。常见做法是定时计算 Redis 缓存 短过期兜底。Spark Job 每天凌晨跑一次落到 MySQL ADS 表Spring Boot 服务层定时把这一天的大屏数据从 MySQL 预载到 Redis接口读 Redis命中不到才回源查 MySQL。这是一个典型的冷热分离。Component public class DashBoardCacheTask { Autowired private StringRedisTemplate redisTemplate; Scheduled(cron 0 30 2 * * ?) public void warmUp() { ListClusterOverviewVO list clusterService.clusterOverviewFromDb(); String json JSON.toJSONString(list); redisTemplate.opsForValue().set(dashboard:cluster-overview, json, 12, TimeUnit.HOURS); } }几个细节值得注意。cron 用 0 30 2 * * ? 每天凌晨两点半避开凌晨 0 点的整点任务高峰也避开了大多数数据库备份时间窗。key 用冒号分隔命名空间 dashboard:cluster-overview按前缀把不同模块隔离后续清缓存只需 scan dashboard:*。过期时间 12 小时是给数据一个最长存活期防止服务异常时前端拿到三天前的旧数据。使用 Scheduled 的类必须配合启动类上的 EnableScheduling 才生效这是很常见的翻车点。另一个坑是单机多实例部署时两台服务器同时执行 warmUpRedis 会被重复写。由于两个实例写的是同一个 value结果一样危害不大所以这个项目里可用可不用。真要用用 Redis 的 setnx 做一次性锁就行别引一套 Redisson太重。5. 避坑共享单车数据分析项目里最常见的 5 个坑这一章是纯经验。每个坑我都按现象、原因、解决展开很多是这类项目里反复出现的。5.1 环境和依赖Spring Boot 与 Spark 的 jar 冲突先说第一个坑也是新手最先踩到的Spring Boot 内嵌 Tomcat 和 Spark 的 jar 冲突。现象是spring-boot:run启动没几秒就崩日志里能看到 javax.servlet 或 org.apache.catalina 相关报错。原因出在 Spark 的传递依赖里带了旧版 Servlet API和 Tomcat 撞了。解决方向有三个按优先级来。第一优先用 Spark 3.x老问题少很多第二把 spark-core 的 scope 设为 provided让它不参与打包第三用 Maven 的 dependencyManagement 统一 Servlet API 版本。还有人用 artifactSet 把和 servlet 相关的传递依赖排除掉也可以。最省事的还是 Java 8 Spring Boot 2.7.x Spark 3.x 老三样社区验证充分。第二个坑和环境相关本地能跑打成 jar 放到服务器就报找不到 SparkSession 的类。原因是 spark-core 被打进 fat jar 后打包插件没把 Scala 的清单文件合并正确Spark 找不到自己的版本号。现象是NoSuchMethodError: org.apache.spark.SparkContext$.org$apache$spark$internal$Logging。解决方法是换用 spring-boot-maven-plugin 的 excludes 配置排除掉 spark 相关依赖让 Spark jar 以独立 zip 形式放在 lib 目录启动时通过-Djava.ext.dirs加载或者直接用 maven-shade-plugin 做一次重定位。这类项目里我一般选择把计算 Job 和 Web 服务拆成两个独立进程互不干扰。5.2 数据和计算倾斜、距离、时区第三个坑是数据倾斜也是 Spark 离线任务里最容易让人挠头的。骑行订单天然聚在少数热门起终点按起终点分组统计时热门区域那个 task 要处理 80% 的数据其他任务早就跑完了整个 job 一直卡在最后几个 task 上。现象就是 Spark UI 里几个 task 的 duration 特别长一堆 task 在等它。常见缓解手段是加盐把 key 拼上随机后缀打散到多个分区算完再按真实 key 聚合。治本的方向是在 DWD 里拆热力点和普通点两套聚合逻辑热力点单独跑普通点全局跑最后合并。项目规模不大时先加盐一行改动就能感受到整个 job 的提速。第四个坑是经纬度距离计算用欧氏距离。有人把起终点距离直接取纬度差平方 经度差平方开根号结果在纬度 60 度上误差被放大到接近两倍平均骑行距离动辄几十公里用户画像完全失真。正解是 Haversine 公式把经纬度先转成弧度再按球面距离算。跨城市分析里这个误差能到几十公里必须认真处理。我一般在 DWD 清洗时把 Haversine 距离写成一个 UDF 注册到 SparkSession后面所有 Job 复用。spark.udf().register(haversine_distance, (UDF2Double, Double, Double) (lat1, lng1, lat2, lng2) - { double R 6371.0; double dLat Math.toRadians(lat2 - lat1); double dLng Math.toRadians(lng2 - lng1); double a Math.sin(dLat / 2) * Math.sin(dLat / 2) Math.cos(Math.toRadians(lat1)) * Math.cos(Math.toRadians(lat2)) * Math.sin(dLng / 2) * Math.sin(dLng / 2); return 2 * R * Math.asin(Math.sqrt(a)); });这个 UDF 的参数是两组经纬度单位是公里。骑行时长除以距离就能得到用户均速均速超过 35 公里每小时的记录基本是 GPS 漂移需要在 DWD 阶段剔除。第五个坑是时区。前面说过spark.sql.session.timeZone没设的话UTC 会把凌晨的数据算到前一天。更隐蔽的是 CSV 里的时间字段如果是不带时区的字符串Spark InferSchema 直接按 TimestampType 解析但不主动做时区转换。此时你要保证读进来是什么就按什么处理统一在 Spark 配置里声明 Asia/Shanghai前端展示也用东八区格式化不要在 Spring Boot 层再做一次 ZoneId 转换。同一个时间被转两次就会凭空多出八小时或少八小时排查起来非常绝望。提示写 MySQL 时在 JDBC 连接串里加上 rewriteBatchedStatementstrue否则 Spark 的 JDBC writer 默认一条一条 insert大批量结果写库慢到离谱。先写临时表再 swap 进正式表相当于给凌晨任务加一道后悔药跑坏了直接 drop 临时表业务数据不受影响。6. 验证与进阶让这个项目从能跑变成敢上线跑通这条链路之后整个项目已经是一个闭环Spring Boot 接住数据、Spark 算用户、MySQL 存结果、Redis 做大屏缓存。但能跑不代表敢上线我自己习惯在交付前做三件事验证。第一件事是抽样核对指标。取某一天的真实订单明细直接用 SQL 在 MySQL 里算一遍当日总骑行量、平均时长、活跃用户数和 Spark 任务产出对比允许误差在 0.5% 以内。超过就得查是不是脏数据被过滤了两次或者时区导致日期口径不一致。第二件事是设置数据质量监控。重点看 DWD 表里骑行时长小于 0、起终点经纬度为 0 的记录占比这类记录超过阈值就报警不要把坏数据喂给聚类模型。聚类模型如果吃进大量离群点KMeans 的簇中心会被拉偏用户画像就会失真。第三件事是压一次大屏接口。模拟 50 个并发客户端刷新 dashboard接口 P95 响应应该稳定在 200ms 以内。如果超过先检查 Redis 缓存是否命中和 SQL 是否走了索引不要在代码层面盲目加线程池。这样一个项目才从复现了一个 zip变成有交付标准的数据分析项目。进阶方向我会往实时化和自动化走把凌晨批处理升级成一个带依赖关系的夜间调度对接任务编排工具把骑行轨迹的实时上报接入 springboot整合flink 的管道做实时热力大屏。这些是后话先把离线链路跑稳——很多共享单车平台的运营决策到今天依然建立在每天凌晨那一次离线分析上。我现在的习惯是做这种数据项目第一件事先把时区、隔离级别、批处理开关全部写进配置再谈业务算法。希望这些经验能帮你把这个项目做得比大多数同类更扎实希望帮到你。本文还有配套的精品资源点击获取