基于Kafka+Flink+Redis的电商大屏实时计算案例

本文介绍如何使用Flink进行实时数据处理,构建电商场景下的实时大屏,展示站点订单、销量与GMV统计及商品销量排名。通过Kafka数据源,结合Flink的流式计算能力,实现数据的实时聚合与输出。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

 

参考链接:https://mp.weixin.qq.com/s/pfMiDIlQNheopSKogxEXhw

阿里的双11销量大屏可以说是一道特殊的风景线。实时大屏(real-time dashboard)正在被越来越多的企业采用,用来及时呈现关键的数据指标。并且在实际操作中,肯定也不会仅仅计算一两个维度。由于Flink的“真·流式计算”这一特点,它比Spark Streaming要更适合大屏应用。本文从笔者的实际工作经验抽象出简单的模型,并简要叙述计算流程(当然大部分都是源码)。

 

 

数据格式与接入

简化的子订单消息体如下。

 

{
"userId": 234567,
"orderId": 2902306918400,
"subOrderId": 2902306918401,
"siteId": 10219,
"siteName": "site_blabla",
"cityId": 101,
"cityName": "北京市",
"warehouseId": 636,
"merchandiseId": 187699,
"price": 299,
"quantity": 2,
"orderStatus": 1,
"isNewOrder": 0,
"timestamp": 1572963672217
}

由于订单可能会包含多种商品,故会被拆分成子订单来表示,每条JSON消息表示一个子订单。现在要按照自然日来统计以下指标,并以1秒的刷新频率呈现在大屏上:

  • 每个站点(站点ID即siteId)的总订单数、子订单数、销量与GMV;

  • 当前销量排名前N的商品(商品ID即merchandiseId)与它们的销量。

由于大屏的最大诉求是实时性,等待迟到数据显然不太现实,因此我们采用处理时间作为时间特征,并以1分钟的频率做checkpointing。

 

 

StreamExecutionEnvironment env = StreamExecutionEnvironment.getExecutionEnvironment();
env.setStreamTimeCharacteristic(TimeCharacteristic.ProcessingTime);
env.enableCheckpointing(60 * 1000, CheckpointingMode.EXACTLY_ONCE);
env.getCheckpointConfig().setCheckpointTimeout(30 * 1000);

然后订阅Kafka的订单消息作为数据源。

 

 

Properties consumerProps = ParameterUtil.getFromResourceFile("kafka.properties");
DataStream<String> sourceStream = env
.addSource(new FlinkKafkaConsumer011<>(
ORDER_EXT_TOPIC_NAME, // topic
new SimpleStringSchema(), // deserializer
consumerProps // consumer properties
))
.setParallelism(PARTITION_COUNT)
.name("source_kafka_" + ORDER_EXT_TOPIC_NAME)
.uid("source_kafka_" + ORDER_EXT_TOPIC_NAME);

给带状态的算子设定算子ID(通过调用uid()方法)是个好习惯,能够保证Flink应用从保存点重启时能够正确恢复状态现场。为了尽量稳妥,Flink官方也建议为每个算子都显式地设定ID,参考:https://ci.apache.org/projects/flink/flink-docs-stable/ops/state/savepoints.html#should-i-assign-ids-to-all-operators-in-my-job

 

接下来将JSON数据转化为POJO,JSON框架采用FastJSON。

 

 

DataStream<SubOrderDetail> orderStream = sourceStream
.map(message -> JSON.parseObject(message, SubOrderDetail.class))
.name("map_sub_order_detail").uid("map_sub_order_detail");

JSON已经是预先处理好的标准化格式,所以POJO类SubOrderDetail的写法可以通过Lombok极大地简化。如果JSON的字段有不规范的,那么就需要手写Getter和Setter,并用@JSONField注解来指明。

 

 

@Getter
@Setter
@NoArgsConstructor
@AllArgsConstructor
@ToString
public class SubOrderDetail implements Serializable {
private static final long serialVersionUID = 1L;

private long userId;
private long orderId;
private long subOrderId;
private long siteId;
private String siteName;
private long cityId;
private String cityName;
private long warehouseId;
private long merchandiseId;
private long price;
private long quantity;
private int orderStatus;
private int isNewOrder;
private long timestamp;
}

统计站点指标

将子订单流按站点ID分组,开1天的滚动窗口,并同时设定ContinuousProcessingTimeTrigger触发器,以1秒周期触发计算。注意处理时间的时区问题,这是老生常谈了。

 

WindowedStream<SubOrderDetail, Tuple, TimeWindow> siteDayWindowStream = orderStream
.keyBy("siteId")
.window(TumblingProcessingTimeWindows.of(Time.days(1), Time.hours(-8)))
.trigger(ContinuousProcessingTimeTrigger.of(Time.seconds(1)));

接下来写个聚合函数。

 

 

DataStream<OrderAccumulator> siteAggStream = siteDayWindowStream
.aggregate(new OrderAndGmvAggregateFunc())
.name("aggregate_site_order_gmv").uid("aggregate_site_order_gmv");

 

public static final class OrderAndGmvAggregateFunc
implements AggregateFunction<SubOrderDetail, OrderAccumulator, OrderAccumulator> {
private static final long serialVersionUID = 1L;

@Override
public OrderAccumulator createAccumulator() {
return new OrderAccumulator();
}

@Override
public OrderAccumulator add(SubOrderDetail record, OrderAccumulator acc) {
if (acc.getSiteId() == 0) {
acc.setSiteId(record.getSiteId());
acc.setSiteName(record.getSiteName());
}
acc.addOrderId(record.getOrderId());
acc.addSubOrderSum(1);
acc.addQuantitySum(record.getQuantity());
acc.addGmv(record.getPrice() * record.getQuantity());
return acc;
}

@Override
public OrderAccumulator getResult(OrderAccumulator acc) {
return acc;
}

@Override
public OrderAccumulator merge(OrderAccumulator acc1, OrderAccumulator acc2) {
if (acc1.getSiteId() == 0) {
acc1.setSiteId(acc2.getSiteId());
acc1.setSiteName(acc2.getSiteName());
}
acc1.addOrderIds(acc2.getOrderIds());
acc1.addSubOrderSum(acc2.getSubOrderSum());
acc1.addQuantitySum(acc2.getQuantitySum());
acc1.addGmv(acc2.getGmv());
return acc1;
}
}

累加器类OrderAccumulator的实现很简单,看源码就大概知道它的结构了,因此不再多废话。唯一需要注意的是订单ID可能重复,所以需要用名为orderIds的HashSet来保存它。HashSet应付我们目前的数据规模还是没太大问题的,如果是海量数据,就考虑换用HyperLogLog吧。

接下来就该输出到Redis供呈现端查询了。这里有个问题:一秒内有数据变化的站点并不多,而ContinuousProcessingTimeTrigger每次触发都会输出窗口里全部的聚合数据,这样做了很多无用功,并且还会增大Redis的压力。所以,我们在聚合结果后再接一个ProcessFunction,代码如下。

 

 

DataStream<Tuple2<Long, String>> siteResultStream = siteAggStream
.keyBy(0)
.process(new OutputOrderGmvProcessFunc(), TypeInformation.of(new TypeHint<Tuple2<Long, String>>() {}))
.name("process_site_gmv_changed").uid("process_site_gmv_changed");

 

public static final class OutputOrderGmvProcessFunc
extends KeyedProcessFunction<Tuple, OrderAccumulator, Tuple2<Long, String>> {
private static final long serialVersionUID = 1L;

private MapState<Long, OrderAccumulator> state;

@Override
public void open(Configuration parameters) throws Exception {
super.open(parameters);
state = this.getRuntimeContext().getMapState(new MapStateDescriptor<>(
"state_site_order_gmv",
Long.class,
OrderAccumulator.class)
);
}

@Override
public void processElement(OrderAccumulator value, Context ctx, Collector<Tuple2<Long, String>> out) throws Exception {
long key = value.getSiteId();
OrderAccumulator cachedValue = state.get(key);

if (cachedValue == null || value.getSubOrderSum() != cachedValue.getSubOrderSum()) {
JSONObject result = new JSONObject();
result.put("site_id", value.getSiteId());
result.put("site_name", value.getSiteName());
result.put("quantity", value.getQuantitySum());
result.put("orderCount", value.getOrderIds().size());
result.put("subOrderCount", value.getSubOrderSum());
result.put("gmv", value.getGmv());
out.collect(new Tuple2<>(key, result.toJSONString());
state.put(key, value);
}
}

@Override
public void close() throws Exception {
state.clear();
super.close();
}
}

说来也简单,就是用一个MapState状态缓存当前所有站点的聚合数据。由于数据源是以子订单为单位的,因此如果站点ID在MapState中没有缓存,或者缓存的子订单数与当前子订单数不一致,表示结果有更新,这样的数据才允许输出。

最后就可以安心地接上Redis Sink了,结果会被存进一个Hash结构里。

 

 

// 看官请自己构造合适的FlinkJedisPoolConfig
FlinkJedisPoolConfig jedisPoolConfig = ParameterUtil.getFlinkJedisPoolConfig(false, true);
siteResultStream
.addSink(new RedisSink<>(jedisPoolConfig, new GmvRedisMapper()))
.name("sink_redis_site_gmv").uid("sink_redis_site_gmv")
.setParallelism(1);

 

public static final class GmvRedisMapper implements RedisMapper<Tuple2<Long, String>> {
private static final long serialVersionUID = 1L;
private static final String HASH_NAME_PREFIX = "RT:DASHBOARD:GMV:";

@Override
public RedisCommandDescription getCommandDescription() {
return new RedisCommandDescription(RedisCommand.HSET, HASH_NAME_PREFIX);
}

@Override
public String getKeyFromData(Tuple2<Long, String> data) {
return String.valueOf(data.f0);
}

@Override
public String getValueFromData(Tuple2<Long, String> data) {
return data.f1;
}

@Override
public Optional<String> getAdditionalKey(Tuple2<Long, String> data) {
return Optional.of(
HASH_NAME_PREFIX +
new LocalDateTime(System.currentTimeMillis()).toString(Consts.TIME_DAY_FORMAT) +
"SITES"
);
}
}

商品Top N

我们可以直接复用前面产生的orderStream,玩法与上面的GMV统计大同小异。这里用1秒滚动窗口就可以了。

 

WindowedStream<SubOrderDetail, Tuple, TimeWindow> merchandiseWindowStream = orderStream
.keyBy("merchandiseId")
.window(TumblingProcessingTimeWindows.of(Time.seconds(1)));

DataStream<Tuple2<Long, Long>> merchandiseRankStream = merchandiseWindowStream
.aggregate(new MerchandiseSalesAggregateFunc(), new MerchandiseSalesWindowFunc())
.name("aggregate_merch_sales").uid("aggregate_merch_sales")
.returns(TypeInformation.of(new TypeHint<Tuple2<Long, Long>>() { }));

聚合函数与窗口函数的实现更加简单了,最终返回的是商品ID与商品销量的二元组。

 

public static final class MerchandiseSalesAggregateFunc
implements AggregateFunction<SubOrderDetail, Long, Long> {
private static final long serialVersionUID = 1L;

@Override
public Long createAccumulator() {
return 0L;
}

@Override
public Long add(SubOrderDetail value, Long acc) {
return acc + value.getQuantity();
}

@Override
public Long getResult(Long acc) {
return acc;
}

@Override
public Long merge(Long acc1, Long acc2) {
return acc1 + acc2;
}
}


public static final class MerchandiseSalesWindowFunc
implements WindowFunction<Long, Tuple2<Long, Long>, Tuple, TimeWindow> {
private static final long serialVersionUID = 1L;

@Override
public void apply(
Tuple key,
TimeWindow window,
Iterable<Long> accs,
Collector<Tuple2<Long, Long>> out) throws Exception {
long merchId = ((Tuple1<Long>) key).f0;
long acc = accs.iterator().next();
out.collect(new Tuple2<>(merchId, acc));
}
}

既然数据最终都要落到Redis,那么我们完全没必要在Flink端做Top N的统计,直接利用Redis的有序集合(zset)就行了,商品ID作为field,销量作为分数值,简单方便。不过flink-redis-connector项目中默认没有提供ZINCRBY命令的实现(必须再吐槽一次),我们可以自己加,步骤参照之前写过的那篇加SETEX的命令的文章,不再赘述。RedisMapper的写法如下。

 

 

public static final class RankingRedisMapper implements RedisMapper<Tuple2<Long, Long>> {
private static final long serialVersionUID = 1L;
private static final String ZSET_NAME_PREFIX = "RT:DASHBOARD:RANKING:";

@Override
public RedisCommandDescription getCommandDescription() {
return new RedisCommandDescription(RedisCommand.ZINCRBY, ZSET_NAME_PREFIX);
}

@Override
public String getKeyFromData(Tuple2<Long, Long> data) {
return String.valueOf(data.f0);
}

@Override
public String getValueFromData(Tuple2<Long, Long> data) {
return String.valueOf(data.f1);
}

@Override
public Optional<String> getAdditionalKey(Tuple2<Long, Long> data) {
return Optional.of(
ZSET_NAME_PREFIX +
new LocalDateTime(System.currentTimeMillis()).toString(Consts.TIME_DAY_FORMAT) + ":" +
"MERCHANDISE"
);
}
}

后端取数时,用ZREVRANGE命令即可取出指定排名的数据了。只要数据规模不是大到难以接受,并且有现成的Redis,这个方案完全可以作为各类Top N需求的通用实现。

The End

大屏的实际呈现需要保密,截图自然是没有的。以下是提交执行时Flink Web UI给出的执行计划(实际有更多的统计任务,不止3个Sink)。通过复用源数据,可以在同一个Flink job内实现更多统计需求。

 

<think>好的,用户之前询问了济南重汽的智能转型趋势,现在接着问关于Spark和Flink实时分析车辆数据的具体实现。需要先理解用户的需求。用户可能对数据处理技术在实际应用中的具体步骤感兴趣,或者他们可能正在考虑如何在自己的项目中应用这些技术,或者他们可能是在准备面试或项目时需要了解具体实现细节。 首先,要确定用户的技术背景。他们可能有一定的编程或数据基础,但需要具体的实现流程。需要解释Spark和Flink实时分析中的角色,以及它们各自的优势和适用场景。用户可能想知道从数据采集到处理再到应用的全流程,包括架构设计、关键技术组件、代码示例等。 接下来,需要分步骤介绍实现过程。例如,从数据源开始,车辆产生的数据如何收集,然后传输到消息队列,再通过Spark或Flink进行处理,最后存储和可视化。同时,需要比较Spark和Flink的不同,比如Spark的微批处理和Flink的真正的流处理,以及它们在实时性、容错、状态管理等方面的差异。 还要考虑实际应用中的挑战,比如数据延迟、高吞吐量处理、状态管理、故障恢复等,以及如何通过窗口操作、检查点机制、并行处理等技术来解决这些问题。此外,可能需要给出具体的代码示例,比如使用Flink的DataStream API处理车辆数据,或者使用Spark Structured Streaming进行实时分析。 用户可能还关心实际案例,比如济南重汽是如何应用这些技术的,或者有哪些最佳实践。需要结合之前提到的济南重汽的智能转型,说明他们如何利用这些技术优化车辆监控、预测维护等场景。 另外,要注意用户的潜在需求,比如他们可能想知道学习这些技术需要哪些技能,或者如何在实际项目中部署和优化Spark/Flink作业。因此,在回答中需要涵盖架构设计、关键技术点、代码示例以及实际应用中的挑战和解决方案,以满足用户不同层次的需求。 最后,确保语言通俗易懂,避免过于专业的术语,或者在必要时进行解释,帮助用户更好地理解复杂的流程和技术细节。同时,结构要清晰,分点说明,便于用户跟随逻辑逐步理解整个实现过程。###</think> Spark/Flink 实时分析车辆数据的具体实现 在车辆数据实时分析场景中(如济南重汽的车联网平台),**Spark Streaming** 和 **Flink** 是两主流技术框架。以下是具体实现流程与关键技术点: --- #### **一、场景需求与架构设计** **典型场景**: - 实时监控车辆状态(车速、油耗、故障码) - 驾驶行为分析(急刹车、急加速) - 预测性维护(零部件寿命预警) - 车队调度优化(路径规划、负载均衡) **架构设计**: ``` 车辆终端(传感器/OBD) → Kafka(数据缓冲) → Spark/Flink实时计算) → 存储(HBase/ClickHouse) → 可视化(Grafana/) ``` --- #### **二、数据采集与传输** 1. **数据来源**: - **车载终端**:通过CAN总线采集发动机转速、GPS位置、电池电压等数据,每秒上报1~10条(JSON/二进制格式)。 - **边缘设备**:车载边缘计算网关(如华为MDC)预处理数据(过滤噪声、压缩)。 2. **数据传输**: - **协议**:MQTT(轻量级物联网协议)或 Apache Kafka(高吞吐量消息队列)。 - **数据格式**示例: ```json { "vin": "L123456789", "timestamp": 1719830400000, "speed": 85, "engine_temp": 92, "location": {"lng": 117.12, "lat": 36.65} } ``` --- #### **三、实时计算核心实现** ##### **方案1:Apache Flink(低延迟场景)** **优势**:真正流式处理(逐事件处理),延迟可降至毫秒级,适合高实时性场景(如碰撞预警)。 **代码示例(Flink DataStream API)**: ```java // 1. 从Kafka读取数据 DataStream<String> stream = env.addSource( new FlinkKafkaConsumer<>("vehicle_topic", new SimpleStringSchema(), kafkaProps) ); // 2. 数据解析与过滤 DataStream<VehicleData> parsedStream = stream .map(json -> JSON.parseObject(json, VehicleData.class)) .filter(data -> data.getSpeed() >= 0); // 过滤无效数据 // 3. 实时计算:统计每辆车过去5分钟的平均速度 DataStream<VehicleAvgSpeed> result = parsedStream .keyBy(VehicleData::getVin) .window(TumblingEventTimeWindows.of(Time.minutes(5))) .aggregate(new AvgSpeedAggregateFunction()); // 4. 输出到数据库 result.addSink(new JdbcSink<>( "INSERT INTO vehicle_avg_speed (vin, avg_speed) VALUES (?, ?)", (stmt, record) -> { stmt.setString(1, record.getVin()); stmt.setDouble(2, record.getAvgSpeed()); }, JdbcExecutionOptions.builder().build(), new JdbcConnectionOptions.JdbcConnectionOptionsBuilder() .withUrl("jdbc:mysql://localhost:3306/vehicle_db") .withUsername("user") .withPassword("pass") .build() )); ``` **关键技术点**: - **状态管理**:使用 `ValueState` 或 `ListState` 存储车辆历史状态(如连续急刹车次数)。 - **CEP(复杂事件处理)**:检测异常驾驶模式(例如10分钟内连续3次急刹车): ```java Pattern<VehicleData, ?> pattern = Pattern.<VehicleData>begin("start") .where(data -> data.getBrakePressure() > 90) .times(3) .within(Time.minutes(10)); ``` ##### **方案2:Spark Structured Streaming(高吞吐场景)** **优势**:微批处理(秒级延迟),适合高吞吐离线+实时混合场景(如历史数据回补)。 **代码示例(Scala)**: ```scala // 1. 从Kafka读取数据 val df = spark.readStream .format("kafka") .option("kafka.bootstrap.servers", "localhost:9092") .option("subscribe", "vehicle_topic") .load() // 2. 解析JSON数据 val vehicleDF = df.selectExpr("CAST(value AS STRING)") .select(from_json($"value", schema).as("data")) .select("data.*") // 3. 窗口聚合:统计每10秒各区域车辆数 val result = vehicleDF .withWatermark("timestamp", "2 minutes") .groupBy(window($"timestamp", "10 seconds"), $"location_grid") .count() // 4. 输出到控制台 result.writeStream .outputMode("update") .format("console") .start() .awaitTermination() ``` **优化技巧**: - **Delta Lake**:结合Spark实现ACID事务,防止数据重复消费。 - **Catalyst优化器**:通过 `.repartition(100)` 控制分区数,避免小文件问题。 --- #### **四、典型业务逻辑实现** 1. **驾驶行为评分**: - **规则引擎**:使用Drools或Flink CEP定义规则(如超速时长占比>30%则扣分)。 - **机器学习**:加载预训练模型(如XGBoost)实时预测驾驶员疲劳指数。 2. **预测性维护**: - **时序分析**:通过Flink ML或Spark MLlib检测发动机振动数据异常(如STL分解)。 - **报警触发**:当温度连续3个窗口超过阈值时,推送维修工单至服务系统。 --- #### **五、生产环境挑战与解决** 1. **数据乱序**: - **Watermark机制**(Flink/Spark):允许延迟数据(如设置2分钟水位线)。 - **事件时间处理**:基于数据中的 `timestamp` 字段而非系统时间。 2. **状态容错**: - **Checkpoint**(Flink):每5分钟保存一次状态到HDFS/S3。 - **Write-Ahead Log**(Spark):确保Kafka偏移量精确一次性消费。 3. **资源优化**: - **动态扩缩容**:K8s部署Flink集群,根据CPU利用率自动调整TaskManager数量。 - **序列化优化**:使用Protobuf替代JSON,减少网络传输开销。 --- #### **六、济南重汽的实际应用** 参考其公开技术方案,典型实现可能包括: - **Flink CEP**:实时识别渣土车违规倾倒行为(车辆静止+货箱举升动作)。 - **Spark + GIS**:基于历史轨迹预测车辆到达时间(ETA),误差<5分钟。 - **边缘协同**:在车载网关运行轻量级Flink作业(如数据脱敏),云端执行复杂分析。 --- ### **总结** - **技术选型**: - 超低延迟(<100ms)→ Flink - 高吞吐离线分析 → Spark Structured Streaming - **核心能力**: - 需掌握**事件时间处理**、**状态管理**、**与外部系统集成**(如KafkaRedis)。 - 可参考其“车-路-云”协同架构,将实时分析结果反馈至车辆控制系统(如节油驾驶建议)。
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值