使用spring.cloud.stream來傳送kafka訊息,並根據某欄位將訊息傳送到固定partition上

烟花火的人生發表於2024-05-10

1、問題:

在進行功能開發的時候遇到一個需求,具體需求如下:

  在某個服務A中接收到訊息,對訊息體進行校驗,判斷訊息體中的資料是否需要產生告警,若產生告警,則將告警資訊傳送到kafka中,由另一個服務B接收到訊息並記錄到mongo中;

  當A服務在此接收到訊息,發現以前的某個告警已經恢復,則再次傳送訊息到kafka中,B服務接收到訊息後對原來的告警進行狀態恢復;

  而假如在進行告警狀態更新時,告警的訊息發到了某個partition中,而恢復的訊息傳送到了另一個partition中,中間間隔時間非常短,恢復的訊息先被消費,就會導致出現問題,告警應該是恢復的,但是確沒有恢復,因為原紀錄都還沒插入資料庫;

  所以必須要將相同的告警或恢復物件傳送到同一個partition中,下游在消費的時候,預設情況下同一個customer會消費固定的partition中的訊息,所以這樣就能保證訊息的順序消費。

2、解決過程:

1)、在A和B服務中都是使用的spring.cloud.stream來進行kafka訊息的傳送和接收,版本是3.1.2,而根據官方的方式,我使用瞭如下配置進行指定partition:

# 配置獲取 Message 物件的哪個欄位作為分割槽 key,如根據例項裡邊的 id 作為 key 則寫成 payload.entityId
spring.cloud.stream.bindings.<channelName>.producer.partitionKeyExpression=payload.entityId

程式碼中傳送的程式碼寫成如下:

Message<?> payload = MessageBuilder.withPayload(entity).build();
        this.streamBridge.send(
                "ASSET_ALARM_RECORD_BINDING_NAME",
                payload
        );

2)、但是在進行測試時,又一直提示我報錯 SpelEvaluationException: EL1008E,後來查詢原因發現是在併發量很大的情況下,不能使用payload來進行分割槽,推薦使用headers,具體可以檢視:https://github.com/spring-cloud/spring-cloud-stream/issues/2213 , 所以修改為如下:

# 配置獲取 Message 物件的哪個欄位作為分割槽 key,如根據例項裡邊的 id 作為 key 則寫成headers['entityId']
spring.cloud.stream.bindings.<channelName>.producer.partitionKeyExpression=headers['entityId']
spring.cloud.stream.bindings.<channelName>.producer.configuration.key.serializer: org.apache.kafka.common.serialization.StringSerializer

程式碼中傳送的程式碼寫成如下:

Message<?> payload = MessageBuilder.withPayload(entity).setHeader("partition", entity.getEntityId()).build();
this.streamBridge.send( "ASSET_ALARM_RECORD_BINDING_NAME", payload );

3)、但是還是會報錯,我在A服務中要傳送兩種訊息到kafka的不同topic上,其中一個進行了上述配置,另外一個不需要,但是發現另外一個報了空指標異常,提示我在傳送訊息的時候從headers中找不到partition,這就很奇怪了,然後從github上找到了答案:https://github.com/spring-cloud/spring-cloud-stream/issues/2249 ,從其回答中可以看出,作者在3.2.2以後修復了這個問題,所以我將spring.cloud.stream 的版本改為3.2.10,再進行測試,發現已經可以正常的傳送訊息,並且訊息也能根據entityId傳送到指定的partition上。

相關文章