如何忽略Kafka Streams应用程序中的某些消息,该应用程序读取和写入同一主题的不同事件类型

时间:2019-04-17 15:31:49

标签: apache-kafka spring-cloud avro apache-kafka-streams spring-cloud-stream

让我们假设一个Spring Cloud Stream应用程序从KStream创建一个order topic。它对OrderCreated {"id":x, "productId": y, "customerId": z}个事件感兴趣。一旦到达,它将对其进行处理并向同一OrderShipped {"id":x, "productId": y, "customerName": <, "customerAddress": z}生成一个输出事件order topic

我面临的问题是,由于它在同一个主题之间进行读写,因此Kafka Stream应用程序正在尝试处理自己的写操作,这没有任何意义。

如何防止该应用程序处理其生成的事件?

更新:正如Artem Bilan和sobychako指出的那样,我曾考虑过使用KStream.filter(),但是有些细节使我怀疑如何处理:

现在,KStream应用程序如下所示:

interface ShippingKStreamProcessor {
    ...
    @Input("order")
    fun order(): KStream<String, OrderCreated>

    @Output("output")
    fun output(): KStream<String, OrderShipped>

KStream配置

    @StreamListener
    @SendTo("output")
    fun process(..., @Input("order") order: KStream<Int, OrderCreated>): KStream<Int, OrderShipped> {

订单和输出绑定都指向订单主题作为目的地。

OrderCreated类:

data class OrderCreated(var id: Int?, var productId: Int?, var customerId: Int?) {
    constructor() : this(null, null, null)
}

OrderShipped类

data class OrderShipped(var id: Int?, var productId: Int?, var customerName: String?, var customerAddress: String?) {
    constructor() : this(null, null, null, null)
}

我正在使用 JSON 作为消息格式,因此消息看起来像这样:

  • 输入-订单已创建:{"id":1, "productId": 7,"customerId": 20}
  • 输出-OrderShipped:{"id":1, "productId": 7, "customerName": "X", "customerAddress": "Y"}

我正在考虑以下最佳方法来过滤掉不需要的邮件

如果我现在仅使用KStream.filter(),则当我得到{"id":1, "productId": 7, "customerName": "X", "customerAddress": "Y"}时,我的KStream<Int, OrderCreated>将把OrderShipped事件解组为具有一些空字段的OrderCreated对象:OrderCreated(id:1, productId: 7, customerId: null)。检查空字段听起来并不可靠。

可能的解决方案可以是向使用该主题的每种消息/类添加另一个字段eventType = OrderCreated|OrderShipped。即使在这种情况下,我最终还是拥有一个具有属性eventType = OrderShipped的OrderCreated类(记住KStream )。 这看起来很丑陋。有任何改进的想法吗?

还有另一种更自动的方式来处理此问题吗?例如,如果消息不符合预期的架构(OrderCreated),另一种序列化( AVRO ?)会阻止消息被处理吗? 根据本文的介绍,在同一主题中支持多种模式(事件类型)的这种方式似乎是一种好习惯:https://www.confluent.io/blog/put-several-event-types-kafka-topic/ 但是,尚不清楚如何对不同类型的数据进行编组/反序列化。

2 个答案:

答案 0 :(得分:1)

我已经接受了布鲁诺的回答作为解决此问题的有效方法。但是,我认为我已经提出了一种更加直观/逻辑的方法,使用了一系列带有JsonTypeInfo注释的事件。

首先,您需要一个用于Order事件的基类并指定所有子类。请注意,将在JSON文档中添加一个type属性,这将帮助Jackson封送/解组DTO:

@JsonTypeInfo(use = JsonTypeInfo.Id.NAME, include = JsonTypeInfo.As.PROPERTY, property = "type")
@JsonSubTypes(value = [
    JsonSubTypes.Type(value = OrderCreatedEvent::class, name = "orderCreated"),
    JsonSubTypes.Type(value = OrderShippedEvent::class, name = "orderShipped")
])
abstract class OrderEvent

data class OrderCreatedEvent(var id: Int?, var productId: Int?, var customerId: Int?) : OrderEvent() {
    constructor() : this(null, null, null)
}

data class OrderShippedEvent(var id: Int?, var productId: Int?, var customerName: String?, var customerAddress: String?) : OrderEvent () {
    constructor() : this(null, null, null, null)
}

安装此命令后,OrderCreatedEvent对象的生产者将生成如下消息:

key: 1 value: {"type":"orderCreated","id":1,"productId":24,"customerId":1}

现在轮到KStream了。我已将签名更改为KStream<Int, OrderEvent>,因为它可以接收OrderCreatedEvent或OrderShippedEvent。在接下来的两行中...

orderEvent.filter { _, value -> value is OrderCreatedEvent }
                .map { key, value -> KeyValue(key, value as OrderCreatedEvent) }

...我过滤仅保留OrderCreatedEvent类的消息,并映射它们以将KStream<Int, OrderEvent>转换为KStream<Int, OrderCreatedEvent>

完整的KStream逻辑:

@StreamListener
@SendTo("output")
fun process(@Input("input") input: KStream<Int, Customer>, @Input("order") orderEvent: KStream<Int, OrderEvent>): KStream<Int, OrderShippedEvent> {

        val intSerde = Serdes.IntegerSerde()
        val customerSerde = JsonSerde<Customer>(Customer::class.java)
        val orderCreatedSerde = JsonSerde<OrderCreatedEvent>(OrderCreatedEvent::class.java)

        val stateStore: Materialized<Int, Customer, KeyValueStore<Bytes, ByteArray>> =
                Materialized.`as`<Int, Customer, KeyValueStore<Bytes, ByteArray>>("customer-store")
                        .withKeySerde(intSerde)
                        .withValueSerde(customerSerde)

        val customerTable: KTable<Int, Customer> = input.groupByKey(Serialized.with(intSerde, customerSerde))
                .reduce({ _, y -> y }, stateStore)


        return (orderEvent.filter { _, value -> value is OrderCreatedEvent }
                .map { key, value -> KeyValue(key, value as OrderCreatedEvent) }
                .selectKey { _, value -> value.customerId } as KStream<Int, OrderCreatedEvent>)
                .join(customerTable, { orderIt, customer ->
                    OrderShippedEvent(orderIt.id, orderIt.productId, customer.name, customer.address)
                }, Joined.with(intSerde, orderCreatedSerde, customerSerde))
                .selectKey { _, value -> value.id }
                //.to("order", Produced.with(intSerde, orderShippedSerde))
    }

此过程之后,我将在订单主题中生成一条新消息key: 1 value: {"type":"orderShipped","id":1,"productId":24,"customerName":"Anna","customerAddress":"Cipress Street"},但这将被流过滤掉。

答案 1 :(得分:0)

您可以使用Kafka的记录标题来存储记录的类型。参见KIP-82。您可以在ProducerRecord中设置标题。

处理如下:

  1. 从主题中读取值为stream的{​​{1}}类型的KStream<Integer, Bytes>
  2. 使用KStream#transformValues()过滤和创建对象。更具体地说,在Serdes.BytesSerde中,您可以访问ProcessorContext,从而可以访问包含有关记录类型的信息的记录头。然后:

    • 如果类型为transformValues(),则返回OrderShipped
    • 否则,请从null对象创建一个OrderCreated对象并返回它。

对于使用AVRO的解决方案,您可能需要查看以下文档