简介:面向高校大数据与Java开发课程设计场景,这份项目以Spring Boot为后端框架,集成Hadoop、Hive完成共享单车用户数据的存储与离线分析,并通过ECharts和百度地图API实现可视化展示,适合用作大二、大三学生的课设参考或入门实战。压缩包共128个文件,约5.64MB,其中包含37个Java源码、45个PNG截图、10个JS脚本及HTML/CSS/Vue页面,另有SQL初始化脚本、YML配置与日志文件,整体目录结构清晰,便于对照学习数据采集、清洗、分析和展示的完整链路。目前已有1372人浏览学习,说明该案例具有一定的参考价值。通过该项目可以了解Spring Boot整合大数据组件的基本方式,掌握Hive查询结果如何接入Web层,以及如何利用ECharts与地图API呈现统计指标;同时源码中的配置文件、日志记录和页面素材也能帮助快速复现实验环境,节省从零搭建的时间。
1. 这个SpringBoot共享单车用户大数据分析项目,到底在分析什么
做共享单车运营分析项目,最怕被问“你的指标能不能指导调度”。这是一个基于SpringBoot的共享单车用户大数据分析项目,核心不是炫技,而是把骑行订单、用户注册、车辆轨迹这些原始数据,清洗、建模、统计后输出成活跃用户、高峰时段、骑行时长、用户分层这些能落地决策的指标。适合正在入门SpringBoot和大数据分析的开发者参考,也适合业务侧数据分析同学拿来做口径对照。项目从一套可运行的SpringBoot服务出发,重点解决两个问题:一是面对几百万条订单怎么不把接口拖垮,二是分析结果怎么对业务端解释得清。
2. 共享单车用户大数据分析的链路设计:从订单流水到分析指标
拿到这个标题,第一反应不应该直接写Controller。共享单车用户分析的本质,是把订单事实表和用户维度表压缩成可读的数字。所以数据链路要定位成:采集 → 清洗 → 聚合 → 展示。如果你只想要一个能跑的demo,可以直接跳到第3章;但如果你要把它改造成自己的业务,前面的数据链路设计决定了后面代码的写法,也决定了后期指标能不能让人信服。
我见过的常见做法,是只建一张订单表,然后所有指标都在一张表上做count和group by。订单量到几十万的时候还能忍,到几百万时接口就开始卡,最后只能靠加服务器硬扛。真正合理的做法,是先把数据分层理清,再决定存储和接口。这一章会把链路拆开讲清楚。
2.1 单体架构的边界:为什么SpringBoot单体也能叫“大数据分析”
很多做Java的同学一听“大数据”,第一反应是上Hadoop集群、Spark集群。但共享单车单城市的日订单量,通常就在几十万到几百万之间,MySQL加合理的聚合表完全扛得住。这个项目的目标使用者是运营和数据分析师,不是数据平台研发,所以我不建议一上来就引入大数据集群。单体SpringBoot加上MySQL、Redis、ECharts,足以支撑大半共享单车分析场景。
模块拆分习惯上分成四个:数据接入、数据清洗与调度、指标计算、接口展示。下面这张表概括了每个模块的职责和产出,照着这个结构去建工程,能避免后面所有逻辑堆在一个Service类里。
| 模块 | 职责 | 关键产出 |
|---|---|---|
| 数据接入 | 从订单系统导入原始骑行记录 | fact_ride_order 原始订单表 |
| 清洗与调度 | 补全字段、去重、剔除脏数据,跑定时任务 | 清洗后明细表、reject_log 异常记录 |
| 指标计算 | 按日/小时/城市/用户类型做聚合 | 日活、高峰时段、平均时长、用户分层 |
| 接口展示 | 给数据大屏和管理后台提供JSON接口 | 时序指标、热力区域、TopN排行 |
这样拆还有个好处:每个环节出问题,都有对应的日志和排查入口。比如“某一天统计数据为0”,先查清洗任务是否执行成功,再查指标计算任务,不用去翻Controller。
2.2 事实表和维度表怎么建:这些字段直接决定分析口径
建表是整个项目里最不能省的一步。我见过太多人把订单表设计成全字段冗余的“大宽表”,把所有业务字段都塞进去,最后维护成本非常高。常见做法是星型模型:一张骑行订单事实表,加上用户、车辆、城市三张维度表。这样分析指标可以按任意维度切片,比如“周末男用户平均骑行时长”,只需要在事实表上join用户维度表就行。
下面给出建表SQL。事实表保留订单级明细,因为骑行距离分布、用户第几次骑行这些指标,都需要明细数据支撑。
CREATE TABLE fact_ride_order ( order_id BIGINT PRIMARY KEY AUTO_INCREMENT COMMENT '订单ID', user_id BIGINT NOT NULL COMMENT '用户ID,关联dim_user', bike_id BIGINT NOT NULL COMMENT '车辆ID', city_id INT NOT NULL COMMENT '城市ID', start_time DATETIME NOT NULL COMMENT '开始骑行时间', end_time DATETIME NOT NULL COMMENT '结束骑行时间', duration_sec INT NOT NULL COMMENT '骑行时长(秒)', distance_m INT DEFAULT 0 COMMENT '骑行距离(米)', start_lng DECIMAL(10, 6) NOT NULL COMMENT '起点经度', start_lat DECIMAL(10, 6) NOT NULL COMMENT '起点纬度', end_lng DECIMAL(10, 6) DEFAULT NULL COMMENT '终点经度', end_lat DECIMAL(10, 6) DEFAULT NULL COMMENT '终点纬度', create_time DATETIME DEFAULT CURRENT_TIMESTAMP, KEY idx_user_start (user_id, start_time), KEY idx_start_time (start_time), KEY idx_city_time (city_id, start_time) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='共享单车骑行订单事实表';几个容易踩坑的细节:duration_sec 用 INT 而不是 TIMESTAMP,因为后面要按秒做聚合,整数类型不会带时区解析负担;经纬度必须用 DECIMAL(10,6),如果用 FLOAT,地图聚合时会出现坐标漂移,热力区域会翻车;联合索引 idx_city_time 要和后面高频查询的 where 条件保持一致。索引不是越多越好,后面第5章会专门讲索引失效的问题。
用户维度表负责扩展用户属性,它从用户服务同步,不承担高频写入:
CREATE TABLE dim_user ( user_id BIGINT PRIMARY KEY COMMENT '用户ID', gender TINYINT COMMENT '0未知 1男 2女', age_group VARCHAR(16) COMMENT '年龄段:18-25, 26-35等', user_type TINYINT COMMENT '1普通用户 2月卡用户 3年卡用户', register_time DATETIME COMMENT '注册时间', city_id INT COMMENT '常驻城市ID', KEY idx_register_time (register_time) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='用户维度表';事实表加维度表设计好之后,聚合指标才有干净的来源。第4章的分析SQL都会基于这两张表来写。
2.3 ETL清洗:把不可信订单变成可分析的数据
原始订单数据大概率有脏值:骑行时长超过12小时(有人忘记还车)、距离为0、经纬度落在城市范围外。直接分析这些数据,会让活跃用户数和平均时长严重失真。清洗必须放在离线阶段,而不是页面查询时实时判断,否则接口平均响应时间直接被拉爆。
下面是一个 Java 清洗逻辑的示例,我一般放在 SpringBoot 的 @Scheduled 定时任务里,每天凌晨处理昨天的增量数据。
public RideOrder cleanRawOrder(RawOrder raw) { if (raw == null || raw.getUserId() == null || raw.getStartTime() == null) { return null; // 缺关键字段,直接丢弃 } RideOrder order = new RideOrder(); order.setUserId(raw.getUserId()); order.setBikeId(raw.getBikeId()); // 1. 时长在 1 秒到 4 小时之间,超长骑行视为设备未归位 long seconds = ChronoUnit.SECONDS.between(raw.getStartTime(), raw.getEndTime()); if (seconds <= 0 || seconds > 4 * 3600) { return null; } // 2. 起点经纬度必须在中国大陆的大致范围内,防止脏数据进热力图 if (raw.getStartLat() < 18 || raw.getStartLat() > 54 || raw.getStartLng() < 73 || raw.getStartLng() > 135) { return null; } order.setStartTime(raw.getStartTime()); order.setEndTime(raw.getEndTime()); order.setDurationSec((int) seconds); // 3. 距离为负或空值时,用 haversine 公式回填估算 order.setDistanceM(raw.getDistanceM() != null && raw.getDistanceM() > 0 ? raw.getDistanceM() : estimateDistance(raw)); return order; }这里三步处理对应三类脏数据。时长上限设4小时不是拍脑袋,共享单车客单价低,超过2小时多数是忘记锁车,超过4小时基本可以判定为故障订单,剔除掉或者单独转到异常订单表。经纬度范围检查是热力图的前置条件。清洗出来的丢弃记录建议写进reject_log表,这样被删掉的数据还有后悔药,业务方质疑时也能追溯。
3. SpringBoot核心实现:项目骨架、数据源配置与统计接口
数据链路定好之后,才开始写SpringBoot后端。这一章会从分层、配置、接口三层讲,目标是让你能照着搭出一个可运行的分析项目骨架。中间会穿插选型理由,因为很多新手照着网上的模板写完后,不知道为什么要这么分。
3.1 项目骨架:为什么我选择原生MyBatis而不是MyBatis-Plus
共享单车分析项目里大量查询都是聚合SQL,甚至会写临时表关联。MyBatis对SQL的可控性最好,尤其是复杂多表join时,XML里能显式看到执行计划。MyBatis-Plus确实开发效率高,但它的QueryWrapper不适合组织大数据的统计SQL,容易写出性能隐患。我一般选择原生MyBatis,配合PageHelper处理分页就足够了。
骨架分层是标准的 controller / service / mapper:
@RestController @RequestMapping("/api/analysis") public class UserAnalysisController { private final UserAnalysisService userAnalysisService; public UserAnalysisController(UserAnalysisService userAnalysisService) { this.userAnalysisService = userAnalysisService; } @GetMapping("/active") public ApiResult<List<UserActiveStat>> activeUser(@RequestParam String startTime, @RequestParam String endTime, @RequestParam(required = false) Integer cityId, @RequestParam(required = false) Integer userType) { // 参数校验略,注意startTime/endTime格式统一 return ApiResult.ok(userAnalysisService.getUserActive(startTime, endTime, cityId, userType)); } }Controller很薄,只做参数接收和结果包装。业务逻辑在Service里,SQL在Mapper XML里。这样做的理由很现实:统计口径一旦变动,可以直接改XML,不用动Java代码;等第5章要调慢查询时,你也会感谢这个分层。
3.2 application.yml里的关键参数:连接池、缓存和时区
数据源配置直接决定大数据量查询的稳定性。很多项目跑着跑着突然“连接池爆了”,不是代码逻辑问题,是配置参数从头就错了。下面是一份经过实际项目检验的Druid配置。
spring: datasource: url: jdbc:mysql://localhost:3306/bike_analysis?useUnicode=true&characterEncoding=utf8&serverTimezone=Asia/Shanghai username: root password: yourpassword driver-class-name: com.mysql.cj.jdbc.Driver type: com.alibaba.druid.pool.DruidDataSource druid: initial-size: 5 min-idle: 5 max-active: 50 max-wait: 60000 time-between-eviction-runs-millis: 60000 validation-query: SELECT 1 test-while-idle: true test-on-borrow: false这里有几个参数值得说清楚:max-active 设 50 是单机MySQL的合理中间值,不建议无脑设成几百,连接池太大反而会让数据库线程调度压力变大;max-wait 60000 表示拿连接等待超过60秒就报错,给慢SQL一个快速失败的机会。serverTimezone=Asia/Shanghai 这一行必须写,否则日期会差8个小时,后面避坑章还会专门说。
分析接口的查询结果集一般不大且可复用,我习惯加一层Redis缓存,避免每次请求都打MySQL:
spring: redis: host: localhost port: 6379 timeout: 3000ms lettuce: pool: max-idle: 8 min-idle: 0 max-active: 20Redis主要缓存每日活跃用户数、Top10热力区域这类热点数据,缓存过期时间按业务设定为5分钟到1小时。缓存不是越久越好,大屏场景希望近实时,1小时以上的缓存会让运营看到的是“昨天的数据”。
3.3 一个统计活跃用户的完整链路:Mapper XML + Service + Controller
下面从底层到接口,完整实现第2章链路里的第一个核心指标:每日活跃用户数。Mapper接口定义如下:
public interface RideOrderMapper { List<UserActiveStat> selectUserActive( @Param("startTime") String startTime, @Param("endTime") String endTime, @Param("cityId") Integer cityId, @Param("userType") Integer userType ); }XML里的SQL负责把日期、城市、用户类型三个维度压到一条聚合里:
<select id="selectUserActive" resultType="UserActiveStat"> SELECT DATE(start_time) AS stat_date, COUNT(DISTINCT user_id) AS active_user_cnt FROM fact_ride_order WHERE start_time BETWEEN #{startTime} AND #{endTime} <if test="cityId != null"> AND city_id = #{cityId} </if> <if test="userType != null"> AND user_id IN (SELECT user_id FROM dim_user WHERE user_type = #{userType}) </if> GROUP BY DATE(start_time) ORDER BY stat_date </select>这个SQL的逻辑说明:BETWEEN 的两端必须是日期或日期时间字符串,如果直接传“2024-01-01”,MySQL会把它转成“2024-01-01 00:00:00”,所以查询条件实际包含结束日期的零点,不会带上结束日当天数据。如果希望包含结束日全天,应该把结束时间传成“2024-01-02”。COUNT(DISTINCT user_id)计算去重后的活跃用户数,这是日活指标的标准算法。子查询过滤用户类型,虽然会增加一次查询,但让维度表的职责保持单一。
Service层加上缓存,避免同一个时间段被大屏轮询打死:
public List<UserActiveStat> getUserActive(String startTime, String endTime, Integer cityId, Integer userType) { String cacheKey = "user:active:" + cityId + ":" + userType + ":" + startTime + ":" + endTime; List<UserActiveStat> cached = redisTemplate.opsForValue().get(cacheKey); if (cached != null) { return cached; } List<UserActiveStat> result = rideOrderMapper.selectUserActive( startTime, endTime, cityId, userType); redisTemplate.opsForValue().set(cacheKey, result, Duration.ofMinutes(5)); return result; }缓存key把筛选维度全拼进去,比用一个固定key安全,不会出现“换了城市返回上个城市数据”的尴尬。缓存时间5分钟,对运营看板来说足够新鲜,对数据库来说也减了压。这个接口虽然简单,却是整个项目其他统计接口的模板。
4. 分析逻辑落地:用户活跃度、高峰时段、骑行时长分布与用户分层
第3章把SpringBoot的骨架搭起来了,这一章才算真正进入“数据分析”。很多新手项目会止步于把订单表的记录数统计出来,但没有回答业务问题。共享单车运营最关心的实际上是三件事:谁在用、什么时候用、用完去了哪。这一章围绕这三个问题写清楚指标计算逻辑。
4.1 数据分析指标的SQL实现:活跃趋势、高峰时段、时长分布
活跃趋势已经有现成的日活接口,这里补充一个按小时维度统计骑行量的SQL,用来分析城市通勤潮汐:
SELECT HOUR(start_time) AS hour_no, COUNT(*) AS ride_cnt FROM fact_ride_order WHERE start_time >= #{startDate} AND start_time < #{endDate} GROUP BY HOUR(start_time) ORDER BY hour_no;这段SQL的逻辑说明:按小时分组,能直接看到早上7点到9点、晚上18点到20点两个高峰。执行计划会用到 idx_start_time 索引。注意这里条件用了>=和<而不是BETWEEN,目的是精确控制统计区间,避免把结束日期的时间段也统计进去。
骑行时长分布是产品运营做定价的重要依据:
SELECT CASE WHEN duration_sec < 300 THEN '0-5分钟' WHEN duration_sec < 900 THEN '5-15分钟' WHEN duration_sec < 1800 THEN '15-30分钟' ELSE '30分钟以上' END AS duration_band, COUNT(*) AS cnt FROM fact_ride_order GROUP BY duration_band;这个分桶的阈值很关键:共享单车订单的骑行时长高度集中在前15分钟,超出30分钟的大多是骑行游玩或故障订单。如果你希望统计平均时长,建议在此基础上排除掉30分钟以上的长尾,否则平均值会被拉高到明显失真。分析SQL里,分桶比直接输出原始秒数更直观,大屏上的饼图也更好展示。
4.2 用户分层:把RFM模型改造成共享单车版本
电商常用的RFM模型是最近一次消费时间、消费频次、消费金额。共享单车没有金额概念,所以我把M换成累计骑行时长或骑行距离,改造后的RFM含义是:
| 维度 | 共享单车版指标 | 业务含义 |
|---|---|---|
| Recency | 距今多少天没骑过车 | 用户是否还在使用 |
| Frequency | 近30天骑行次数 | 用户使用频次 |
| Monetary | 累计骑行分钟数 | 用户对平台的依赖程度 |
计算逻辑放在Java里时,注意不要在循环里逐用户查MySQL。正确做法是在SQL层先聚合出每个用户的RFM值,再在Service里做分段。
public UserSegment calculateUserSegment(UserRfm rfm) { // 基于用户整体数据的三分位阈值计算,这里只做示意 final int recencyDay = rfm.getRecencyDays(); final int freq = rfm.getRideTimesLast30Days(); final int totalMin = rfm.getTotalRideMinutes(); if (recencyDay <= 3 && freq >= 10) { return UserSegment.HIGH_VALUE; } else if (recencyDay <= 7 && freq >= 5) { return UserSegment.MEDIUM_VALUE; } else if (recencyDay <= 3 && freq < 5) { return UserSegment.NEW_USER_TO_KEEP; } else { return UserSegment.LOW_VALUE; } }这个分段里的阈值不能照抄。不同城市、不同季节的用户行为差异很大,我一般先把近30天用户的骑次分布跑出来,取 P25/P50/P75 作为阈值来源。Java代码只负责区间判断,阈值从配置文件或数据库配置表里读取,这样运营调整规则时不用重新发版。
4.3 数据大屏接口的合并设计:别让前端一次拉太多
共享单车的数据大屏通常要同时展示实时订单量、活跃用户、高峰时段、热力区域Top10。前端最常见的问题是并发请求十几个接口,后端每个接口都查DB,高峰期直接把MySQL拖垮。我习惯做一个聚合接口,把大屏首屏需要的指标合并成一个JSON返回。
@GetMapping("/dashboard") public DashboardVO getDashboardStat() { String cacheKey = "dashboard:all"; DashboardVO vo = (DashboardVO) redisTemplate.opsForValue().get(cacheKey); if (vo != null) { return vo; } DashboardVO vo = new DashboardVO(); vo.setActiveCount(activeService.getTodayActive()); vo.setPeakHours(hourService.getLast7DaysPeak()); vo.setTopAreas(mapService.getTop10HotAreas()); redisTemplate.opsForValue().set(cacheKey, vo, Duration.ofMinutes(1)); return vo; }这个聚合接口的注意事项:每个子查询内部都有自己的缓存方法,所以即使其中一个服务慢,另外两个还能从Redis返回。聚合接口的缓存过期时间设成1分钟,比单指标接口更短,因为大屏要尽量实时。这里还有一个经验,热力区域的Top10查询需要按经纬度分桶,把经纬度精确到小数点后两位再做聚合,不然接口会把一整天的点位明细全部返回,前端根本画不动。前端表格也是一样,别一次性渲染几千行,用分页或虚拟滚动能避免页面直接卡死。
5. 避坑指南:SpringBoot大数据分析项目最常见的五个坑
数据分析项目跑起来不难,难在数据量上来以后还能稳定。这一章的坑多半是我自己踩过的,每一条都以“现象 → 原因 → 解决”的顺序写,方便你直接对号入座。
5.1 慢查询拖垮数据库:联合索引永远没有生效
现象:订单表数据量到三百万后,按城市和时间筛选的接口响应从200ms变成3s,大屏接口频繁超时。
原因:where条件里写了DATE(start_time) >= '2024-01-01',用函数包裹了索引字段,导致 idx_city_time 无法生效,MySQL只能全表扫描。还有一部分原因是传参类型不匹配,比如Java端把startTime传成Timestamp,SQL里隐式转换后索引也失效。
解决:查询条件直接写成start_time >= '2024-01-01' AND start_time < '2024-01-02',不要包DATE函数。并且把where条件里的字段顺序按索引顺序写。改完后用EXPLAIN SELECT ...看 key 字段,确认走的是 idx_city_time 而不是全表扫描。
5.2 连接池被占满:maxActive不是唯一原因
现象:接口偶尔报Data source rejected establishment of connection, message from server: Too many connections,重启后又恢复。
原因:maxActive设了100,但底层有30分钟级别的慢SQL一直占着连接不释放,新请求只能等待,最终把连接池榨干。单纯调大maxActive只会增加数据库压力,不会解决问题。
解决:把maxActive降到50,开启Druid监控页面,通过StatFilter查看最慢SQL和最长连接持有时间。对超过3秒的查询做缓存或拆分,比如把热力区域查询改成预聚合表,而不是直接查明细。配置一个合理的max-wait=60000,让请求在60秒内得不到连接就失败,至少能快速暴露问题。
5.3 日期时区问题:数据差8小时很常见
现象:数据大屏统计的23点峰值为零,但业务库里明明有夜间骑行记录;日活数量和订单系统对不上。
原因:JDBC连接URL没有设置serverTimezone,MySQL的DATETIME被JDBC按服务器时区解析,和JVM时区不一致,导致查出来的时间整体偏移8小时。
解决:在数据库连接URL里显式加serverTimezone=Asia/Shanghai。同时统一约定:数据库里存DATETIME,应用日志里存UTC时间戳,前端展示时再转本地时区。不要在多个地方各自做时间转换,那会制造更多不一致。另外,建表时日期字段统一用DATETIME,避免混用TIMESTAMP引起隐式转换问题。
5.4 大屏接口翻车:count(*)和list一起查
现象:仪表盘请求一进来,接口先count总订单数,再查当前页明细,两个大查询同时执行,MySQL瞬间CPU飙高,页面一直转圈。
原因:前端表格组件默认要“总数 + 数据列表”,后端就写了两个独立查询。明细表数据量越大,count(*)的扫描成本越高,甚至比list查询还慢。
解决:把count改为从聚合表取数,或者在大屏场景直接不展示精确总数,用“近7日日均订单量”代替。list必须分页,单页100条以内。如果一定要精确数,可以把总数放到离线任务里算好,接口只读聚合结果。记住,大屏场景的“总数”绝大多数时候没有实时精确的必要。
5.5 内存溢出:用List<Map>一把梭风险大
现象:导出“全量用户骑行明细”时,任务执行到一半抛出OutOfMemoryError,连带其他接口卡死。
原因:MyBatis的默认查询会把所有结果一次性加载进内存。几万行没问题,几十万行带字段变成List<Map>,很容易把堆撑爆。这里的血泪经验是,别把SpringBoot项目当数仓工具用,明细导出必须走异步任务。
解决:查询时只查需要的字段,不要SELECT *。导出改成分批查询,每批10000条写完文件后释放引用;或者使用MyBatis的Streaming查询游标,一边读一边写。异步任务配合线程池限流,避免一个导出任务耗尽内存。大屏和报表只允许读聚合结果,不给明细查询接口,这是底线。
6. 进阶:从离线分析走向SpringBoot整合Flink的实时升级
离线日更的大盘分析在大多数场景下已经够用。但共享单车有潮汐调度场景,运营需要看“当前五分钟哪个地铁站骑行量在涨”,这时离线任务帮不上忙。只有当业务明确提出现场调度需求时,才值得升级到实时链路。常见做法是让订单数据直接进Kafka,由Flink窗口聚合后写回Redis,SpringBoot只负责从Redis读最新聚合值,不再直接查订单表。
一个SpringBoot整合Flink的最小示意如下:
StreamExecutionEnvironment env = StreamExecutionEnvironment.getExecutionEnvironment(); DataStream<String> stream = env.addSource( new FlinkKafkaConsumer<>("ride-order", new SimpleStringSchema(), kafkaProps)); stream .map(parseOrder) // 解析订单JSON并进行基础清洗 .keyBy(RideOrder::getCityId) .window(TumblingEventTimeWindows.of(Time.minutes(5))) .aggregate(new CountAggregate()) .process(new RedisSinkFunction()); env.execute("bike-analysis-flink");这段代码的作用是:把Kafka里的订单流按城市分桶,每5分钟统计一次骑行量,结果落到Redis。TumblingEventTimeWindows的窗口时间需要配合事件时间字段和watermark使用,否则无法处理乱序数据。我的建议是,即使只是demo,也把Flink逻辑放进独立模块,不要在SpringBoot的启动流程里直接跑流任务,否则一次任务失败,整个SpringBoot服务都被拖住。
验证分析系统是否可信,我有两个固定习惯。第一,每天跑完离线任务后,把聚合结果和前一天做diff,波动超过5%就查原因,很多口径错误都是这样发现的。第二,把慢SQL日志接入一个简单统计表,查询耗时趋势一旦抬头,立刻去看索引。数据分析项目最重要的不是技术栈有多新,而是数字能不能经得起追问。我自己栽过最大的坑,是贪图新鲜直接上Spark集群,结果运维成本翻倍,业务还在等一张离线日报。共享单车这类业务,先把SpringBoot加离线分析做到极致,再决定要不要实时。希望帮到你。
本文还有配套的精品资源,点击获取