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 原生序列化(
ObjectOutputStream
→byte[]
)。消费者(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.orders
、demo_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 关键点解释
-
offset 管理
-
Structured Streaming 自动管理 Kafka offset,不需要手动提交。
-
只要指定
checkpointLocation
,任务重启后就能恢复进度。
-
-
事件时间 vs 处理时间
-
我上面示例用了
current_timestamp()
(处理时间),因为你的 KafkaOrder
里没有时间戳字段。 -
如果你想用
Order
的业务时间(比如ts
字段),要在 producer 里加上ts
并传输,消费时用col("ts")
做窗口。
-
-
写 Hive 表
-
toTable("db.table")
是 Structured Streaming 2.4+ 的 API,可以直接写 Hive 表。 -
checkpointLocation
是必须的。
-
-
聚合窗口
-
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