1. 项目背景与核心目标
作为一名长期从事大数据分析的工程师,我经常遇到需要处理海量交通数据的场景。这次分享的毕业设计项目,源于一个真实的地铁客流分析需求——通过大数据技术手段,从数百万条地铁刷卡记录中挖掘出有价值的运营洞察。
这个系统的核心价值在于:将原始的、杂乱的地铁刷卡数据转化为直观的、可操作的业务知识。比如哪些站点在早高峰压力最大?不同线路的客流构成有什么特点?票价收入与客流量是否成正比?这些问题的答案直接影响着地铁运营方的排班调度、设备维护和商业决策。
2. 技术选型与架构设计
2.1 技术栈组成
整个系统采用Lambda架构设计,兼顾批处理和实时计算:
- 数据采集层:原始CSV格式的地铁刷卡数据(781,472条记录)
- 批处理层:
- Flink 1.13.2(数据清洗与聚合)
- Elasticsearch 7.10(数据存储)
- Kibana 7.10(可视化)
- 实时计算层:
- Flink(实时窗口计算)
- HBase 2.3(实时结果存储)
- 辅助工具:
- Hadoop 3.2.2(分布式存储)
- Zookeeper 3.6.3(集群协调)
选择Flink而非Spark Streaming的原因:Flink的Exactly-Once语义和低延迟特性更适合实时客流统计场景,其事件时间处理机制能有效应对数据乱序问题。
2.2 数据流程设计

- 数据接入:原始CSV文件通过Flink FileSource接入
- 数据清洗:
- 过滤无效记录(单边交易、同站进出等)
- 补全完整乘车记录(匹配进站与出站)
- 批处理分析:
- 按线路/站点/时段的聚合计算
- 结果写入Elasticsearch
- 实时计算:
- 5分钟滚动窗口统计
- 结果写入HBase
- 可视化:通过Kibana展示分析结果
3. 数据预处理关键细节
3.1 原始数据特征
数据集包含2018年9月1日5:00-11:35期间的数据:
- 总记录数:781,472条(进站415,741条,出站365,731条)
- 覆盖范围:8条线路、170个站点
- 数据字段示例:
csv复制各字段含义:时间戳、卡号、应收金额、实收金额、交易类型、线路、站点ID、站点名称、设备编号、设备ID1535752434000,HHJJAFGAH,0.0,0.0,地铁入站,地铁二号线,0,大剧院,AGM-109,260036109
3.2 数据清洗规则
通过Flink DataStream API实现以下清洗逻辑:
java复制DataStream<Transaction> cleaned = rawData
.filter(t -> !t.getStationIn().equals(t.getStationOut())) // 规则1:过滤同站进出
.filter(t -> t.getTimestamp() >= 1535767200000L) // 规则2:过滤6:00前数据
.keyBy("cardId")
.process(new CardMatchingProcessFunction()); // 规则3:匹配进出站记录
其中CardMatchingProcessFunction的核心逻辑是:
- 为每张卡维护一个状态队列
- 遇到进站记录时存入队列
- 遇到出站记录时尝试匹配最近的进站记录
- 超时未匹配的记录定期清理
3.3 数据质量报告
清洗前后对比:
| 指标 | 原始数据 | 有效数据 | 过滤率 |
|---|---|---|---|
| 总记录数 | 781,472 | 572,156 | 26.8% |
| 完整乘车记录 | - | 286,078 | - |
| 平均乘车时间 | - | 28分钟 | - |
主要无效数据类型:
- 单边交易(只有进站或出站):占过滤量的62%
- 同站进出:占21%
- 非运营时段数据:占17%
4. 关键分析指标实现
4.1 客流热力分析
4.1.1 线路级分析
通过Flink SQL实现聚合计算:
sql复制INSERT INTO es_line_traffic
SELECT
line_name,
COUNT(*) as total_count,
SUM(CASE WHEN type='in' THEN 1 ELSE 0 END) as in_count,
SUM(CASE WHEN type='out' THEN 1 ELSE 0 END) as out_count,
HOP_START(event_time, INTERVAL '5' MINUTE, INTERVAL '1' HOUR) as window_start
FROM transactions
GROUP BY
HOP(event_time, INTERVAL '5' MINUTE, INTERVAL '1' HOUR),
line_name
Top3线路客流对比(单位:人次):
| 线路 | 进站量 | 出站量 | 总量 |
|---|---|---|---|
| 5号线 | 58,742 | 54,896 | 113,638 |
| 3号线 | 48,635 | 45,123 | 93,758 |
| 1号线 | 42,157 | 49,872 | 92,029 |
4.1.2 站点级分析
发现几个典型模式:
- 居住区站点:早高峰进站量大(如五和站进站占比9.53%)
- 商务区站点:早高峰出站量大(如罗湖站出站量是进站的2.1倍)
- 换乘站点:双向流量均衡(如深圳北站进出站比1:1.2)
4.2 时间维度分析
4.2.1 全天客流趋势

关键发现:
- 进站高峰:08:30(峰值12,345人次/5分钟)
- 出站高峰:08:45(峰值14,567人次/5分钟)
- 进站流量更平缓,出站呈现"脉冲式"特征
4.2.2 线路时段特征
各线路早高峰表现差异:
- 5号线:进站高峰08:15-08:45,持续30分钟
- 1号线:出站高峰08:30-09:00,呈现双峰特征
- 9号线:全天客流平稳,无明显高峰
4.3 收入分析
4.3.1 票价分布
通过Elasticsearch的Terms Aggregation统计:
json复制{
"aggs": {
"fare_distribution": {
"terms": {
"field": "actual_fare",
"size": 10
}
}
}
}
常见票价占比:
| 票价(元) | 占比 | 典型区间 |
|---|---|---|
| 2.85 | 28.7% | 3-5站 |
| 1.90 | 22.3% | 1-2站 |
| 4.75 | 15.6% | 6-8站 |
| 0.00 | 2.1% | 员工/特殊群体 |
4.3.2 线路收入排行
虽然1号线客流量仅排第三,但收入排名第一:
- 1号线:¥423,156(平均票价¥4.60)
- 5号线:¥387,245(平均票价¥3.41)
- 3号线:¥285,674(平均票价¥3.05)
收入差异主要来自:
- 1号线连接机场/高铁站,长途乘客多
- 5号线多为通勤短途客流
5. 实时计算实现
5.1 HBase表设计
bash复制# 创建站点客流表
create 'StationTraffic',
{NAME => 'meta', VERSIONS => 1},
{NAME => 'traffic', VERSIONS => 3}
RowKey设计:日期 时间 排名(如2018-09-01 08:30 001)
- 支持按时间范围快速扫描
- 天然保持排序
5.2 Flink实时作业
核心窗口计算逻辑:
java复制dataStream
.keyBy("stationId")
.timeWindow(Time.minutes(5), Time.minutes(1)) // 5分钟窗口,1分钟滑动
.aggregate(new TrafficAggregator())
.addSink(new HBaseSink());
TrafficAggregator实现:
- 统计每个站点的进出人次
- 计算环比变化率
- 标记异常波动(>30%变化)
5.3 实时查询示例
获取08:30-08:45各站点TopN客流:
scala复制val scan = new Scan()
.setStartRow(Bytes.toBytes("2018-09-01 08:30"))
.setStopRow(Bytes.toBytes("2018-09-01 08:46"))
.setFilter(new PageFilter(10))
val results = table.getScanner(scan)
results.forEach(r => {
val station = Bytes.toString(r.getValue("traffic".getBytes, "name".getBytes))
val count = Bytes.toInt(r.getValue("traffic".getBytes, "count".getBytes))
println(s"$station: $count人次")
})
6. 项目经验与优化建议
6.1 踩坑实录
-
数据乱序问题
初期发现约5%的记录存在时间戳乱序,导致窗口计算不准确。解决方案:- 使用Flink的EventTime特性
- 设置允许延迟(allowedLateness=30s)
- 启用水印生成
-
HBase热点问题
最初直接使用站点ID作为RowKey前缀,导致写入集中在少数Region。优化方案:- 改用时间前缀(
小时-分钟) - 预分区(预创建24个Region对应小时)
- 改用时间前缀(
-
状态管理
乘客乘车记录匹配时,状态数据增长过快。解决方法:- 设置TTL(StateTtlConfig)
- 定期清理超时未匹配的记录
6.2 性能优化
通过以下优化将处理吞吐从5k EPS提升到50k EPS:
| 优化措施 | 效果 | 实现方式 |
|---|---|---|
| 反序列化优化 | 提升30% | 改用Protobuf格式 |
| 状态后端调优 | 提升40% | 使用RocksDBStateBackend |
| 网络缓冲调优 | 提升25% | taskmanager.network.memory.fraction=0.3 |
| 并行度调整 | 提升50% | 根据数据分区特征设置keyBy |
6.3 扩展建议
-
预测功能
可接入历史天气、节假日数据,构建客流预测模型 -
异常检测
实时监测客流突变,触发应急预警 -
多源数据融合
整合公交GPS、出租车轨迹等数据,构建综合交通分析
7. 完整代码结构
项目采用Maven多模块设计:
code复制traffic-analysis/
├── core/ # 公共模块
│ ├── model/ # 数据模型
│ └── utils/ # 工具类
├── batch/ # 批处理
│ ├── cleaning/ # 数据清洗
│ └── aggregation/ # 聚合计算
├── streaming/ # 实时计算
│ ├── process/ # 业务逻辑
│ └── sink/ # 输出连接器
└── web/ # 可视化
├── controller/ # API接口
└── service/ # 业务服务
关键类说明:
TransactionParser:原始数据解析CardMatcher:进出站记录匹配TrafficAggregator:客流统计HBaseWriter:实时结果写入KibanaConfig:可视化仪表盘配置
这个项目完整展示了从原始数据到业务洞察的全流程,其中的技术方案和经验教训都来自真实实践。对于想学习大数据处理的学生开发者,建议重点关注Flink的状态管理和时间处理机制,这是构建实时系统的核心能力。