java代码实现spark消费kafka,然后写入hive表

一、背景

最近有一个需求是:要求有一个类对象为Order,它有string类型的字段orderNo和Long类型的字段cost,生产者写到kafka的value是Order对象序列化后的字节数组、key值是orderNo字段,要求spark以手动提交的方式消费kafka,并将数据依次写入到hive表中,并且spark中有一个5分钟滑动窗口,滑动步长为1分钟,统计5分钟内的cost总值并输出。

 

二、实战演练

前期准备

下面列一个简化版项目代码,包含:

  • pom.xml(依赖)

  • Order Java 类(可序列化)

  • Kafka 生产者示例(把 Order 对象序列化为字节数组发送;key 为 orderNo

  • Spark Streaming 程序(使用 Spark Streaming DStream + Kafka 0.10 直连,手动提交 offset;把订单写入 Hive 表;并实现 5 分钟滑动窗口,步长 1 分钟,统计 5 分钟内 cost 总和并输出到 Hive)

  • Hive 建表 SQL

  • 运行与注意事项说明

说明与设计决策(重要)

  • 生产者使用 Java 原生序列化(ObjectOutputStreambyte[])。消费者(Spark)使用 ObjectInputStream 反序列化为 Order 对象。若你偏好 JSON/Avro/ProtoBuf 可替换序列化方式(更安全/可扩展)。

  • 为了实现“手动提交 offset”,示例使用 Spark Streaming 的 Kafka 0.10 直连模式 (createDirectStream) 并在每个 RDD 处理完成后调用 Kafka 提交接口提交 offsets(使用 CanCommitOffsets.commitAsync)。这是常见做法:在确保数据成功写入外部存储(Hive)后再提交 offset。

  • Window 聚合用 DStream 的 reduceByKeyAndWindow(windowDuration=5min, slideDuration=1min),并在每个窗口的 RDD 中写入 Hive(或输出到控制台)。

  • 代码是完整 Java 示例,便于直接在集群或本地模式跑 spark-submit

实战代码

1. pom.xml准备

 1 <project xmlns="http://maven.apache.org/POM/4.0.0" ...>
 2   <modelVersion>4.0.0</modelVersion>
 3   <groupId>com.example</groupId>
 4   <artifactId>spark-kafka-hive-demo</artifactId>
 5   <version>1.0-SNAPSHOT</version>
 6   <properties>
 7     <java.version>1.8</java.version>
 8     <scala.version>2.12</scala.version>
 9     <spark.version>3.3.2</spark.version>
10     <kafka.clients.version>2.8.1</kafka.clients.version>
11   </properties>
12   <dependencies>
13     <!-- Spark Core / Streaming / SQL / Hive support -->
14     <dependency>
15       <groupId>org.apache.spark</groupId>
16       <artifactId>spark-core_2.12</artifactId>
17       <version>${spark.version}</version>
18       <scope>provided</scope>
19     </dependency>
20     <dependency>
21       <groupId>org.apache.spark</groupId>
22       <artifactId>spark-streaming_2.12</artifactId>
23       <version>${spark.version}</version>
24       <scope>provided</scope>
25     </dependency>
26     <dependency>
27       <groupId>org.apache.spark</groupId>
28       <artifactId>spark-sql_2.12</artifactId>
29       <version>${spark.version}</version>
30       <scope>provided</scope>
31     </dependency>
32     <dependency>
33       <groupId>org.apache.spark</groupId>
34       <artifactId>spark-hive_2.12</artifactId>
35       <version>${spark.version}</version>
36       <scope>provided</scope>
37     </dependency>
38 
39     <!-- Spark Kafka integration -->
40     <dependency>
41       <groupId>org.apache.spark</groupId>
42       <artifactId>spark-streaming-kafka-0-10_2.12</artifactId>
43       <version>${spark.version}</version>
44     </dependency>
45 
46     <!-- Kafka clients for producer -->
47     <dependency>
48       <groupId>org.apache.kafka</groupId>
49       <artifactId>kafka-clients</artifactId>
50       <version>${kafka.clients.version}</version>
51     </dependency>
52 
53     <!-- Optional: Slf4j -->
54     <dependency>
55       <groupId>org.slf4j</groupId>
56       <artifactId>slf4j-api</artifactId>
57       <version>1.7.36</version>
58     </dependency>
59     <dependency>
60       <groupId>org.slf4j</groupId>
61       <artifactId>slf4j-log4j12</artifactId>
62       <version>1.7.36</version>
63     </dependency>
64   </dependencies>
65   <build>
66     <plugins>
67       <plugin>
68         <groupId>org.apache.maven.plugins</groupId>
69         <artifactId>maven-compiler-plugin</artifactId>
70         <version>3.8.1</version>
71         <configuration>
72           <source>1.8</source>
73           <target>1.8</target>
74         </configuration>
75       </plugin>
76     </plugins>
77   </build>
78 </project>

2.Hive 建表 SQL

运行在你的 Hive CLI 或 Hue:

-- 用于存放原始订单(append)
CREATE DATABASE IF NOT EXISTS demo_db;

CREATE TABLE IF NOT EXISTS demo_db.orders (
  orderNo STRING,
  cost BIGINT
)
STORED AS PARQUET;

-- 用于存放窗口聚合结果:每个窗口的开始/结束时间和总 cost
CREATE TABLE IF NOT EXISTS demo_db.order_cost_window_agg (
  window_start TIMESTAMP,
  window_end TIMESTAMP,
  total_cost BIGINT
)
STORED AS PARQUET;

3,Java 类:Order.java

 1 package com.example;
 2 
 3 import java.io.Serializable;
 4 
 5 public class Order implements Serializable {
 6     private static final long serialVersionUID = 1L;
 7 
 8     private String orderNo;
 9     private Long cost;
10 
11     public Order() {}
12 
13     public Order(String orderNo, Long cost) {
14         this.orderNo = orderNo;
15         this.cost = cost;
16     }
17 
18     public String getOrderNo() {
19         return orderNo;
20     }
21 
22     public void setOrderNo(String orderNo) {
23         this.orderNo = orderNo;
24     }
25 
26     public Long getCost() {
27         return cost;
28     }
29 
30     public void setCost(Long cost) {
31         this.cost = cost;
32     }
33 
34     @Override
35     public String toString() {
36         return "Order{orderNo='" + orderNo + "', cost=" + cost + "}";
37     }
38 }

4.Kafka 生产者示例:OrderProducer.java

使用 Java 原生序列化把 Order 转成 byte[],key 使用 orderNo

 1 package com.example;
 2 
 3 import org.apache.kafka.clients.producer.*;
 4 import org.apache.kafka.common.serialization.ByteArraySerializer;
 5 import org.apache.kafka.common.serialization.StringSerializer;
 6 
 7 import java.io.ByteArrayOutputStream;
 8 import java.io.ObjectOutputStream;
 9 import java.util.Properties;
10 import java.util.UUID;
11 
12 public class OrderProducer {
13     public static byte[] serialize(Order order) throws Exception {
14         ByteArrayOutputStream bos = new ByteArrayOutputStream();
15         ObjectOutputStream oos = new ObjectOutputStream(bos);
16         oos.writeObject(order);
17         oos.flush();
18         return bos.toByteArray();
19     }
20 
21     public static void main(String[] args) throws Exception {
22         String brokers = "localhost:9092"; // 修改为你的 Kafka broker 列表
23         String topic = "orders-topic";
24 
25         Properties props = new Properties();
26         props.put(ProducerConfig.BOOTSTRAP_SERVERS_CONFIG, brokers);
27         props.put(ProducerConfig.KEY_SERIALIZER_CLASS_CONFIG, StringSerializer.class.getName());
28         props.put(ProducerConfig.VALUE_SERIALIZER_CLASS_CONFIG, ByteArraySerializer.class.getName());
29 
30         KafkaProducer<String, byte[]> producer = new KafkaProducer<>(props);
31 
32         // 发送一些示例订单
33         for (int i = 0; i < 10; i++) {
34             String orderNo = "ORD-" + UUID.randomUUID().toString().substring(0, 8);
35             long cost = (long) (Math.random() * 1000);
36             Order order = new Order(orderNo, cost);
37             byte[] payload = serialize(order);
38 
39             ProducerRecord<String, byte[]> record = new ProducerRecord<>(topic, orderNo, payload);
40             producer.send(record, new Callback() {
41                 @Override
42                 public void onCompletion(RecordMetadata metadata, Exception exception) {
43                     if (exception != null) {
44                         exception.printStackTrace();
45                     } else {
46                         System.out.printf("Sent order %s to partition %d offset %d%n",
47                                 orderNo, metadata.partition(), metadata.offset());
48                     }
49                 }
50             });
51 
52             Thread.sleep(200); // 小间隔
53         }
54 
55         producer.flush();
56         producer.close();
57     }
58 }

5.Spark Streaming 程序(手动提交 offsets,并写入 Hive + 窗口聚合)

文件:SparkKafkaToHive.java

功能:

  • 使用 Kafka 0.10 直连消费(key=String, value=byte[])

  • 反序列化为 Order

  • 每个微批的 RDD 将订单写入 Hive 表 demo_db.orders

  • 使用 window(5 分钟,滑动 1 分钟)统计窗口内 cost 总和并写入 demo_db.order_cost_window_agg

  • 在每个 RDD 处理完后,手动提交该 RDD 的 offsets 到 Kafka(commitAsync

  1 package com.example;
  2 
  3 import org.apache.kafka.clients.consumer.ConsumerConfig;
  4 import org.apache.kafka.common.serialization.ByteArrayDeserializer;
  5 import org.apache.kafka.common.serialization.StringDeserializer;
  6 import org.apache.spark.SparkConf;
  7 import org.apache.spark.api.java.JavaRDD;
  8 import org.apache.spark.api.java.JavaSparkContext;
  9 import org.apache.spark.api.java.function.Function;
 10 import org.apache.spark.sql.*;
 11 import org.apache.spark.streaming.*;
 12 import org.apache.spark.streaming.api.java.*;
 13 import org.apache.spark.streaming.kafka010.*;
 14 
 15 import java.io.ByteArrayInputStream;
 16 import java.io.ObjectInputStream;
 17 import java.sql.Timestamp;
 18 import java.util.*;
 19 import java.util.stream.Collectors;
 20 
 21 import scala.Tuple2;
 22 
 23 public class SparkKafkaToHive {
 24     public static Order deserialize(byte[] bytes) throws Exception {
 25         if (bytes == null) return null;
 26         ByteArrayInputStream bis = new ByteArrayInputStream(bytes);
 27         ObjectInputStream ois = new ObjectInputStream(bis);
 28         Object o = ois.readObject();
 29         return (Order) o;
 30     }
 31 
 32     // SparkSession 单例帮助类(在 foreachRDD 中复用 SparkSession)
 33     public static class JavaSparkSessionSingleton {
 34         private static transient SparkSession instance = null;
 35 
 36         public static SparkSession getInstance(SparkConf conf) {
 37             if (instance == null) {
 38                 synchronized (JavaSparkSessionSingleton.class) {
 39                     if (instance == null) {
 40                         instance = SparkSession.builder()
 41                                 .config(conf)
 42                                 .enableHiveSupport()
 43                                 .getOrCreate();
 44                     }
 45                 }
 46             }
 47             return instance;
 48         }
 49     }
 50 
 51     public static void main(String[] args) throws Exception {
 52         String brokers = "localhost:9092";
 53         String topic = "orders-topic";
 54         String groupId = "spark-orders-consumer-group";
 55 
 56         // SparkConf & StreamingContext(微批间隔:1 分钟,符合滑动步长)
 57         SparkConf sparkConf = new SparkConf()
 58                 .setAppName("SparkKafkaToHiveDemo")
 59                 .setIfMissing("spark.master", "local[2]") // 本地测试时使用
 60                 .set("spark.serializer", "org.apache.spark.serializer.KryoSerializer");
 61 
 62         // 这里设置 1 分钟 batch interval(因为滑动步长是 1 分钟)
 63         Duration batchInterval = Durations.minutes(1);
 64         JavaStreamingContext jssc = new JavaStreamingContext(sparkConf, batchInterval);
 65 
 66         // Kafka params
 67         Map<String, Object> kafkaParams = new HashMap<>();
 68         kafkaParams.put(ConsumerConfig.BOOTSTRAP_SERVERS_CONFIG, brokers);
 69         kafkaParams.put(ConsumerConfig.KEY_DESERIALIZER_CLASS_CONFIG, StringDeserializer.class);
 70         kafkaParams.put(ConsumerConfig.VALUE_DESERIALIZER_CLASS_CONFIG, ByteArrayDeserializer.class);
 71         kafkaParams.put(ConsumerConfig.GROUP_ID_CONFIG, groupId);
 72         kafkaParams.put(ConsumerConfig.AUTO_OFFSET_RESET_CONFIG, "latest");
 73         // 禁用自动提交,由我们手动提交
 74         kafkaParams.put(ConsumerConfig.ENABLE_AUTO_COMMIT_CONFIG, false);
 75 
 76         Collection<String> topics = Collections.singletonList(topic);
 77 
 78         // Create Direct Stream
 79         JavaInputDStream<org.apache.kafka.clients.consumer.ConsumerRecord<String, byte[]>> stream =
 80                 KafkaUtils.createDirectStream(
 81                         jssc,
 82                         LocationStrategies.PreferConsistent(),
 83                         ConsumerStrategies.<String, byte[]>Subscribe(topics, kafkaParams)
 84                 );
 85 
 86         // --- 1) 将每个 micro-batch 的所有订单写入 Hive ---
 87         stream.foreachRDD((rdd, time) -> {
 88             if (rdd.isEmpty()) {
 89                 // 没有数据也应该尝试提交 offsets?这里跳过提交以示例简单(可视需求决定)
 90                 return;
 91             }
 92 
 93             // 获取 offset ranges
 94             OffsetRange[] offsetRanges = ((HasOffsetRanges) rdd.rdd()).offsetRanges();
 95 
 96             // 反序列化为 Order 对象 JavaRDD<Order>
 97             JavaRDD<Order> ordersRDD = rdd.map(record -> {
 98                 try {
 99                     return deserialize(record.value());
100                 } catch (Exception e) {
101                     // 处理反序列化异常(记录日志并丢弃)
102                     e.printStackTrace();
103                     return null;
104                 }
105             }).filter(Objects::nonNull);
106 
107             // 写入 Hive
108             if (!ordersRDD.isEmpty()) {
109                 // 获取(或创建)SparkSession
110                 SparkSession spark = JavaSparkSessionSingleton.getInstance(sparkConf);
111                 Dataset<Row> df = spark.createDataFrame(ordersRDD, Order.class);
112                 // 写入 Hive 表(append)
113                 df.write().mode(SaveMode.Append).insertInto("demo_db.orders");
114             }
115 
116             // 处理完业务后,提交 offsets 到 Kafka(手动提交)
117             // 需要对 stream 进行类型转换以调用 commitAsync
118             try {
119                 ((CanCommitOffsets) stream.inputDStream()).commitAsync(offsetRanges, (offsets, exception) -> {
120                     if (exception != null) {
121                         System.err.println("CommitAsync failed: " + exception.getMessage());
122                         exception.printStackTrace();
123                     } else {
124                         System.out.println("Committed offsets: " + Arrays.toString(offsetRanges));
125                     }
126                 });
127             } catch (Exception commitEx) {
128                 commitEx.printStackTrace();
129             }
130         });
131 
132         // --- 2) 窗口聚合:5 分钟窗口,1 分钟滑动,统计 cost 总和,并写入 Hive ---
133         // 先将 stream 转成 PairDStream<"all", cost>
134         JavaPairDStream<String, Long> costPairs = stream.mapToPair(record -> {
135             try {
136                 Order o = deserialize(record.value());
137                 if (o != null && o.getCost() != null) {
138                     return new Tuple2<>("all", o.getCost());
139                 } else {
140                     return new Tuple2<>("all", 0L);
141                 }
142             } catch (Exception e) {
143                 e.printStackTrace();
144                 return new Tuple2<>("all", 0L);
145             }
146         });
147 
148         // reduceByKeyAndWindow (windowDuration=5min, slideDuration=1min)
149         JavaPairDStream<String, Long> windowed = costPairs.reduceByKeyAndWindow(
150                 (a, b) -> a + b,
151                 Durations.minutes(5),
152                 Durations.minutes(1)
153         );
154 
155         // 在每个窗口的 RDD 中写入 Hive(可以插入到 order_cost_window_agg)
156         windowed.foreachRDD((rdd, time) -> {
157             if (rdd.isEmpty()) return;
158 
159             // time 是窗口的结束时间(即当前批次时间)
160             long windowEndMs = time.milliseconds();
161             long windowStartMs = windowEndMs - 5 * 60 * 1000 + 1; // 包含窗口起点
162 
163             List<Row> rows = rdd.map(tuple -> {
164                 long total = tuple._2();
165                 return RowFactory.create(new Timestamp(windowStartMs), new Timestamp(windowEndMs), total);
166             }).collect();
167 
168             if (!rows.isEmpty()) {
169                 SparkSession spark = JavaSparkSessionSingleton.getInstance(sparkConf);
170                 StructType schema = new StructType(new StructField[]{
171                         new StructField("window_start", DataTypes.TimestampType, false, Metadata.empty()),
172                         new StructField("window_end", DataTypes.TimestampType, false, Metadata.empty()),
173                         new StructField("total_cost", DataTypes.LongType, false, Metadata.empty())
174                 });
175                 Dataset<Row> df = spark.createDataFrame(rows, schema);
176                 // 写入 Hive 聚合表
177                 df.write().mode(SaveMode.Append).insertInto("demo_db.order_cost_window_agg");
178 
179                 // 同时打印到控制台
180                 df.show(false);
181             }
182         });
183 
184         // 启动流
185         jssc.start();
186         jssc.awaitTermination();
187     }
188 }

6.运行说明(本地测试步骤)

1)启动 Kafka(和 Zookeeper),创建 topic:

kafka-topics.sh --create --bootstrap-server localhost:9092 --replication-factor 1 --partitions 3 --topic orders-topic

2)确保 Hive Metastore 可用,且 Hive 表已经用上面的 SQL 建好(demo_db.ordersdemo_db.order_cost_window_agg

3)使用 Maven 打包:

mvn clean package -DskipTests

打包得到 jar(注意 Spark 的 provided 依赖在运行时需由 spark-submit 提供)。

4)运行 producer(发送示例订单):

java -cp target/spark-kafka-hive-demo-1.0-SNAPSHOT.jar com.example.OrderProducer

5)

使用 spark-submit 提交 Spark 应用(确保在 classpath 中包含 kafka connector jar,如果 spark 集群中没提供,则把相关依赖打包或在 --packages 中添加):

本地测试示例

$SPARK_HOME/bin/spark-submit \
  --class com.example.SparkKafkaToHive \
  --master local[2] \
  --packages org.apache.spark:spark-streaming-kafka-0-10_2.12:3.3.2,org.apache.spark:spark-sql_2.12:3.3.2 \
  target/spark-kafka-hive-demo-1.0-SNAPSHOT.jar

在集群上请把 --master--deploy-mode--jars 配置为集群环境所需的参数,并保证 Hive metastore 与 spark 能通信(hive-site.xml 放到 classpath)。

7.注意事项 & 常见问题

  • 序列化选择:示例使用 Java 原生序列化,生产环境建议改用 JSON/Avro/Protobuf(更兼容且易排查)。

  • 手动提交 offset:示例在处理完单个 RDD 后调用 commitAsync 提交 offsets。若你希望确保“窗口聚合的结果”也在提交前完成,需确保窗口操作不会依赖未提交的 offsets(窗口是跨批次的,offset 提交策略需结合业务容错设计)。通常做法是:在所有需要的输出都成功写入后再提交 offsets;上面示例在“写入 orders 表”之后提交偏移,而窗口聚合以滑动窗口在后续批次产生输出,不阻塞 offsets 提交(这在容错策略上是一个权衡)。

  • 幂等写入:当你提交 offset 前,若写入 Hive 发生失败,要保证可重试且避免重复写入(例如使用去重逻辑或幂等写入策略)。

  • 性能:生产环境请调整并行度、partition 与 Kafka topic partition 配置,合理设定批次间隔(本示例使用 1 分钟)。

  • Checkpoint:如果想做 Spark Streaming 的容错重启(恢复驱动状态),请配置 jssc.checkpoint(checkpointDir)。但使用 direct Kafka + manual commit,checkpoint 逻辑和 offset 管理会更复杂,需谨慎设计。

  • 安全与配置:如果 Kafka/Hive 使用 Kerberos/ACLs,请在运行前配置好相应的安全参数(JAAS、krb5)。

 

三、拓展

1.改用structed steaming

1.1 提前说明

  • Structured Streaming 不能手动提交 offset,它是通过 checkpoint 目录来管理 offset(exactly-once 语义依赖 checkpoint)。

    • 也就是说你只要设置 .option("checkpointLocation", "..."),Spark 就会自动管理 offset。

    • 如果你一定要“手动提交 offset”,Structured Streaming 不适合,还是得用 DStream API。

  • 优势:Structured Streaming 和 DataFrame API 集成得更好,写 Hive / 聚合都更方便。

1.2 实战代码

 1 package com.example;
 2 
 3 import org.apache.spark.sql.*;
 4 import org.apache.spark.sql.streaming.StreamingQuery;
 5 import org.apache.spark.sql.streaming.Trigger;
 6 import org.apache.spark.sql.types.*;
 7 
 8 import java.io.ByteArrayInputStream;
 9 import java.io.ObjectInputStream;
10 import java.sql.Timestamp;
11 import java.util.Arrays;
12 
13 public class StructuredStreamingKafkaToHive {
14     // 反序列化工具
15     public static Order deserialize(byte[] bytes) throws Exception {
16         if (bytes == null) return null;
17         try (ByteArrayInputStream bis = new ByteArrayInputStream(bytes);
18              ObjectInputStream ois = new ObjectInputStream(bis)) {
19             return (Order) ois.readObject();
20         }
21     }
22 
23     public static void main(String[] args) throws Exception {
24         SparkSession spark = SparkSession.builder()
25                 .appName("StructuredStreamingKafkaToHive")
26                 .enableHiveSupport()   // 启用 Hive
27                 .getOrCreate();
28 
29         // 从 Kafka 读取数据
30         Dataset<Row> kafkaDF = spark.readStream()
31                 .format("kafka")
32                 .option("kafka.bootstrap.servers", "localhost:9092")
33                 .option("subscribe", "orders-topic")
34                 .option("startingOffsets", "latest")
35                 .load();
36 
37         // key 是 string,value 是 byte[]
38         Dataset<Row> keyValueDF = kafkaDF.selectExpr(
39                 "CAST(key AS STRING) as orderNo",
40                 "value"
41         );
42 
43         // 自定义 UDF:反序列化 byte[] 为 cost
44         spark.udf().register("deserializeOrder", (byte[] bytes) -> {
45             try {
46                 Order o = deserialize(bytes);
47                 return o == null ? null : o.getCost();
48             } catch (Exception e) {
49                 return null;
50             }
51         }, DataTypes.LongType);
52 
53         // 添加 cost 字段
54         Dataset<Row> orderDF = keyValueDF.withColumn("cost", functions.callUDF("deserializeOrder", keyValueDF.col("value")))
55                 .select("orderNo", "cost")
56                 .filter("cost IS NOT NULL");
57 
58         // ============ 写入 Hive 原始订单表 ============
59         StreamingQuery orderQuery = orderDF.writeStream()
60                 .format("hive")
61                 .outputMode("append")
62                 .option("checkpointLocation", "/user/hive/warehouse/checkpoints/orders_raw")
63                 .toTable("demo_db.orders");
64 
65         // ============ 5分钟窗口聚合 ============
66         Dataset<Row> windowAggDF = orderDF
67                 .withColumn("event_time", functions.current_timestamp()) // 这里假设没有事件时间,用处理时间
68                 .groupBy(
69                         functions.window(orderDF.col("event_time"), "5 minutes", "1 minute")
70                 )
71                 .agg(functions.sum("cost").alias("total_cost"))
72                 .selectExpr("window.start as window_start", "window.end as window_end", "total_cost");
73 
74         StreamingQuery aggQuery = windowAggDF.writeStream()
75                 .format("hive")
76                 .outputMode("append")
77                 .option("checkpointLocation", "/user/hive/warehouse/checkpoints/orders_agg")
78                 .toTable("demo_db.order_cost_window_agg");
79 
80         // 等待两个流
81         orderQuery.awaitTermination();
82         aggQuery.awaitTermination();
83     }
84 }

1.3 关键点解释

  1. offset 管理

    • Structured Streaming 自动管理 Kafka offset,不需要手动提交。

    • 只要指定 checkpointLocation,任务重启后就能恢复进度。

  2. 事件时间 vs 处理时间

    • 我上面示例用了 current_timestamp()(处理时间),因为你的 Kafka Order 里没有时间戳字段。

    • 如果你想用 Order 的业务时间(比如 ts 字段),要在 producer 里加上 ts 并传输,消费时用 col("ts") 做窗口。

  3. 写 Hive 表

    • toTable("db.table") 是 Structured Streaming 2.4+ 的 API,可以直接写 Hive 表。

    • checkpointLocation 是必须的。

  4. 聚合窗口

    • window("5 minutes", "1 minute") 实现 5 分钟窗口 + 1 分钟滑动。

    • 输出模式必须是 append,因为窗口会生成新结果。

2.Order类中增加事件事件

2.1 实战代码

order.java

 1 package com.example;
 2 
 3 import java.io.Serializable;
 4 
 5 public class Order implements Serializable {
 6     private static final long serialVersionUID = 1L;
 7 
 8     private String orderNo;
 9     private Long cost;
10     private Long ts; // 事件时间(毫秒)
11 
12     public Order() {}
13 
14     public Order(String orderNo, Long cost, Long ts) {
15         this.orderNo = orderNo;
16         this.cost = cost;
17         this.ts = ts;
18     }
19 
20     public String getOrderNo() {
21         return orderNo;
22     }
23 
24     public void setOrderNo(String orderNo) {
25         this.orderNo = orderNo;
26     }
27 
28     public Long getCost() {
29         return cost;
30     }
31 
32     public void setCost(Long cost) {
33         this.cost = cost;
34     }
35 
36     public Long getTs() {
37         return ts;
38     }
39 
40     public void setTs(Long ts) {
41         this.ts = ts;
42     }
43 
44     @Override
45     public String toString() {
46         return "Order{orderNo='" + orderNo + "', cost=" + cost + ", ts=" + ts + "}";
47     }
48 }

 

Kafka Producer 改造(发送带事件时间的 Order)

 1 // 在 OrderProducer.java 中改造发送逻辑:
 2 for (int i = 0; i < 10; i++) {
 3     String orderNo = "ORD-" + UUID.randomUUID().toString().substring(0, 8);
 4     long cost = (long) (Math.random() * 1000);
 5     long ts = System.currentTimeMillis();  // 当前时间作为事件时间
 6     Order order = new Order(orderNo, cost, ts);
 7     byte[] payload = serialize(order);
 8 
 9     ProducerRecord<String, byte[]> record = new ProducerRecord<>(topic, orderNo, payload);
10     producer.send(record, (metadata, exception) -> {
11         if (exception != null) {
12             exception.printStackTrace();
13         } else {
14             System.out.printf("Sent order %s (cost=%d, ts=%d) to partition %d offset %d%n",
15                     orderNo, cost, ts, metadata.partition(), metadata.offset());
16         }
17     });
18 
19     Thread.sleep(200);
20 }

 

Structured Streaming 消费端改造

在 Structured Streaming 中,我们要用 ts 作为事件时间:

 1 // 在 StructuredStreamingKafkaToHive.java 中修改
 2 
 3 // 注册 UDF:反序列化为 Order 的 cost 和 ts
 4 spark.udf().register("deserializeCost", (byte[] bytes) -> {
 5     try {
 6         Order o = deserialize(bytes);
 7         return o == null ? null : o.getCost();
 8     } catch (Exception e) {
 9         return null;
10     }
11 }, DataTypes.LongType);
12 
13 spark.udf().register("deserializeTs", (byte[] bytes) -> {
14     try {
15         Order o = deserialize(bytes);
16         return o == null ? null : o.getTs();
17     } catch (Exception e) {
18         return null;
19     }
20 }, DataTypes.LongType);
21 
22 // 增加 ts 字段(事件时间)
23 Dataset<Row> orderDF = keyValueDF
24         .withColumn("cost", functions.callUDF("deserializeCost", keyValueDF.col("value")))
25         .withColumn("ts", functions.callUDF("deserializeTs", keyValueDF.col("value")))
26         .select("orderNo", "cost", "ts")
27         .filter("cost IS NOT NULL AND ts IS NOT NULL");
28 
29 // 写入 Hive 原始订单表(包含 ts)
30 StreamingQuery orderQuery = orderDF.writeStream()
31         .format("hive")
32         .outputMode("append")
33         .option("checkpointLocation", "/user/hive/warehouse/checkpoints/orders_raw")
34         .toTable("demo_db.orders");
35 
36 // 5分钟窗口,1分钟滑动,基于事件时间 ts
37 Dataset<Row> windowAggDF = orderDF
38         .withColumn("event_time", functions.col("ts").cast("timestamp")) // 把 long 转成 timestamp
39         .withWatermark("event_time", "10 minutes") // 允许最大 10 分钟延迟
40         .groupBy(
41                 functions.window(functions.col("event_time"), "5 minutes", "1 minute")
42         )
43         .agg(functions.sum("cost").alias("total_cost"))
44         .selectExpr("window.start as window_start", "window.end as window_end", "total_cost");
45 
46 StreamingQuery aggQuery = windowAggDF.writeStream()
47         .format("hive")
48         .outputMode("append")
49         .option("checkpointLocation", "/user/hive/warehouse/checkpoints/orders_agg")
50         .toTable("demo_db.order_cost_window_agg");
51 
52 orderQuery.awaitTermination();
53 aggQuery.awaitTermination();

 

Hive 表更新(增加 ts 字段)

DROP TABLE IF EXISTS demo_db.orders;
CREATE TABLE demo_db.orders (
  orderNo STRING,
  cost BIGINT,
  ts BIGINT  -- 毫秒时间戳
)
STORED AS PARQUET;

-- 聚合表保持不变
CREATE TABLE IF NOT EXISTS demo_db.order_cost_window_agg (
  window_start TIMESTAMP,
  window_end TIMESTAMP,
  total_cost BIGINT
)
STORED AS PARQUET;

✅ 现在:

  • Kafka 生产的 Order 带有 ts 字段(事件时间)。

  • Spark Structured Streaming 用 ts 做窗口聚合(5 分钟窗口,1 分钟滑动,允许 10 分钟延迟)。

  • 原始订单和窗口聚合结果分别落入 Hive 表。

 

转载请注明出处:https://www.cnblogs.com/fnlingnzb-learner/p/19072631

posted @ 2025-09-04 00:07  Boblim  阅读(24)  评论(0)    收藏  举报