使用Spring Boot和Kafka Streams實現基於SAGA模式的分散式事務原始碼教程 - Piotr
本案例原始碼是如何使用Spring Boot 和Kafka Streams實現基於SAGA 模式的分散式事務。
有三個微服務:
- 訂單服務--它向Kafka主題傳送訂單事件,並協調分散式事務的過程
- 支付服務--它根據訂單價格在客戶賬戶上執行本地事務
- 庫存服務--它根據訂單中的產品數量在商店上執行本地事務
這是我們的架構圖:
為了完全理解本示例中發生的情況,您還應該熟悉 Kafka Streams 執行緒模型。值得閱讀以下文章,它以簡潔的方式解釋了它。首先,每個流分割槽是一個完全有序的資料記錄序列,並對映到一個 Kafka 主題分割槽。這意味著,即使我們同時有多個訂單與相同的產品相關,它們也會被順序處理,因為它們具有相同的訊息鍵(productId在這種情況下)。
此外,預設情況下,只有一個流執行緒處理所有分割槽。您可以在下面的日誌中看到這一點。但是,有一些流任務充當最低階別的並行單元。因此,流任務可以獨立並行處理,無需人工干預。
這是完全基於Kafka流的。我們不會使用任何SQL資料庫:
當訂單服務傳送一個新的訂單時,它的id是訊息鍵。通過Kafka流,我們可以改變流中的一個訊息鍵。它的結果是建立新的主題和重新分割槽。有了新的訊息鍵,我們可以只對特定的customerId或productId進行計算。這種計算的結果可以儲存在永續性儲存中。
例如,當你在呼叫count()或aggregate()等有狀態的操作時,Kafka會自動建立和管理這樣的狀態儲存。我們將聚合與特定客戶或產品相關的訂單。
現在,讓我們詳細考慮一下支付服務的場景。在傳入的訂單流中,支付服務呼叫selectKey()操作。它將鍵從訂單的id改為訂單的customerId。然後它通過新的鍵對所有的訂單進行分組,並呼叫aggregate()操作。在aggregate()方法中,它根據訂單的價格和狀態(無論是新訂單還是確認訂單)計算出可用金額和保留金額。如果客戶賬戶上有足夠的資金,它將傳送ACCEPT訂單到payment-order主題。否則,它將傳送REJECT訂單。然後,訂單服務流程通過訂單的ID加入來自payment-order和inventory-order的流進行響應。作為結果,它傳送一個確認或回滾訂單。
用Kafka流進行聚合
讓我們從支付服務開始。KStream的實現在這裡並不複雜。
@Bean public KStream<Long, Order> stream(StreamsBuilder builder) { JsonSerde<Order> orderSerde = new JsonSerde<>(Order.class); JsonSerde<Reservation> rsvSerde = new JsonSerde<>(Reservation.class); KStream<Long, Order> stream = builder .stream("orders", Consumed.with(Serdes.Long(), orderSerde)) .peek((k, order) -> LOG.info("New: {}", order)); KeyValueBytesStoreSupplier customerOrderStoreSupplier = Stores.persistentKeyValueStore("customer-orders"); stream.selectKey((k, v) -> v.getCustomerId()) // (1) .groupByKey(Grouped.with(Serdes.Long(), orderSerde)) // (2) .aggregate( () -> new Reservation(random.nextInt(1000)), aggregatorService, Materialized.<Long, Reservation>as(customerOrderStoreSupplier) .withKeySerde(Serdes.Long()) .withValueSerde(rsvSerde)) // (3) .toStream() .peek((k, trx) -> LOG.info("Commit: {}", trx)); return stream; } |
第一步(1),我們呼叫selectKey()方法,獲得訂單物件的customerId值作為一個新的鍵。然後我們呼叫groupByKey()方法(2)來接收KGroupedStream作為結果。當我們擁有KGroupedStream時,我們可以呼叫其中的一個計算方法。在這種情況下,我們需要使用aggregate(),因為我們有一個比簡單計數更高階的計算方法(3)。
最後兩步只是為了列印計算後的值。
然而,上面可見的程式碼片段中最重要的一步是在aggregate()方法中呼叫的類。aggregate()方法需要三個輸入引數。其中第一個參數列示我們的計算物件的起始值。該物件代表客戶賬戶的當前狀態。它有兩個欄位: amountAvailable 和 amountReserved。為了說明問題,我們使用該物件而不是在客戶賬戶上儲存可用和保留金額的實體。每個客戶由Kafka KTable中的customerId(鍵)和Reservation物件(值)表示。只是為了測試的目的,我們要生成一個介於0和1000之間的隨機數,作為 amountAvailable 的起始值。
public class Reservation { private int amountAvailable; private int amountReserved; public Reservation() { } public Reservation(int amountAvailable) { this.amountAvailable = amountAvailable; } // GETTERS AND SETTERS ... } |
好吧,讓我們來看看我們的聚合方法。它需要實現Kafka Aggregate介面及其方法apply()。它可以處理三種型別的訂單。其中一種是訂單的確認(1)。它確認的是分散式交易,所以我們只需要通過從amountReserved欄位中減去訂單的價格來取消一個預訂。另一方面,在回滾的情況下,我們需要用訂單的價格來增加可用金額的值,並相應減少保留金額的值(2)。最後,如果我們收到一個新的訂單,如果客戶賬戶上有足夠的資金,我們需要執行保留,否則,拒絕一個訂單。
Aggregator<Long, Order, Reservation> aggregatorService = (id, order, rsv) -> { switch (order.getStatus()) { case "CONFIRMED" -> // (1) rsv.setAmountReserved(rsv.getAmountReserved() - order.getPrice()); case "ROLLBACK" -> { // (2) if (!order.getSource().equals("PAYMENT")) { rsv.setAmountAvailable(rsv.getAmountAvailable() + order.getPrice()); rsv.setAmountReserved(rsv.getAmountReserved() - order.getPrice()); } } case "NEW" -> { // (3) if (order.getPrice() <= rsv.getAmountAvailable()) { rsv.setAmountAvailable(rsv.getAmountAvailable() - order.getPrice()); rsv.setAmountReserved(rsv.getAmountReserved() + order.getPrice()); order.setStatus("ACCEPT"); } else { order.setStatus("REJECT"); } template.send("payment-orders", order.getId(), order); } } LOG.info("{}", rsv); return rsv; }; |
使用Kafka流表的狀態儲存
庫存服務的實現與支付服務非常相似。不同的是,我們計算的是庫存產品的數量而不是客戶賬戶上的可用資金。
Aggregator<Long, Order, Reservation> aggrSrv = (id, order, rsv) -> { switch (order.getStatus()) { case "CONFIRMED" -> rsv.setItemsReserved(rsv.getItemsReserved() - order.getProductCount()); case "ROLLBACK" -> { if (!order.getSource().equals("STOCK")) { rsv.setItemsAvailable(rsv.getItemsAvailable() + order.getProductCount()); rsv.setItemsReserved(rsv.getItemsReserved() - order.getProductCount()); } } case "NEW" -> { if (order.getProductCount() <= rsv.getItemsAvailable()) { rsv.setItemsAvailable(rsv.getItemsAvailable() - order.getProductCount()); rsv.setItemsReserved(rsv.getItemsReserved() + order.getProductCount()); order.setStatus("ACCEPT"); } else { order.setStatus("REJECT"); } // (1) template.send("stock-orders", order.getId(), order) .addCallback(r -> LOG.info("Sent: {}", result != null ? result.getProducerRecord().value() : null), ex -> {}); } } LOG.info("{}", rsv); // (2) return rsv; }; |
聚合方法的實現也與支付服務非常相似。然而,這一次,讓我們專注於另一件事。一旦我們處理了一個新的訂單,我們就需要向stock-orders主題傳送一個響應。我們使用KafkaTemplate來做這個。在支付服務的情況下,我們也會傳送一個響應,但要傳送到支付-訂單主題。KafkaTemplate的傳送方法並沒有阻塞執行緒。它返回ListenableFuture物件。我們可以給傳送方法新增一個回撥,使用它和傳送訊息後的結果(1)。最後,讓我們記錄一下預訂物件的當前狀態(2)。
之後,我們也在記錄保留物件的值(1)如下。為了做到這一點,我們需要將KTable轉換為KStream,然後呼叫peek方法。這個日誌是在Kafka Streams提交源主題中的偏移量後才列印的。
@Bean public KStream<Long, Order> stream(StreamsBuilder builder) { JsonSerde<Order> orderSerde = new JsonSerde<>(Order.class); JsonSerde<Reservation> rsvSerde = new JsonSerde<>(Reservation.class); KStream<Long, Order> stream = builder .stream("orders", Consumed.with(Serdes.Long(), orderSerde)) .peek((k, order) -> LOG.info("New: {}", order)); KeyValueBytesStoreSupplier stockOrderStoreSupplier = Stores.persistentKeyValueStore("stock-orders"); stream.selectKey((k, v) -> v.getProductId()) .groupByKey(Grouped.with(Serdes.Long(), orderSerde)) .aggregate(() -> new Reservation(random.nextInt(100)), aggrSrv, Materialized.<Long, Reservation>as(stockOrderStoreSupplier) .withKeySerde(Serdes.Long()) .withValueSerde(rsvSerde)) .toStream() .peek((k, trx) -> LOG.info("Commit: {}", trx)); // (1) return stream; } |
執行程式碼
如果你傳送測試訂單會發生什麼?讓我們看看日誌。你可以看到處理訊息和偏移提交之間的時間差。在你的應用程式正在執行或被優雅地停止之前,你不會有任何問題。但如果你,比如說,用kill -9命令殺死這個程式?重新啟動後,我們的應用程式將再次收到相同的訊息。由於我們使用KafkaTemplate來向stock-orders主題傳送響應,我們需要儘快提交偏移。
我們可以做什麼來避免這樣的問題呢?我們可以覆蓋commit.interval.ms Kafka Streams屬性的預設值(30000)。如果你把它設定為0,在處理完成後立即提交。
spring.kafka: streams: properties: commit.interval.ms: 0 |
另一方面,我們也可以將processing.guarantee屬性設定為exact_once。它還將commit.interval.ms的預設值改為100ms,並啟用生產者的idempotence。你可以在Kafka文件中閱讀更多關於它的內容。
spring.kafka: streams: properties: processing.guarantee: exactly_once |
相關文章
- 使用Kafka Streams和Spring Boot微服務中的分散式事務 - PiotrKafkaSpring Boot微服務分散式
- 使用Spring Boot + Kafka實現Saga分散式事務模式的原始碼 - vinsguruSpring BootKafka分散式模式原始碼
- 使用Spring Boot和Kafka Streams實現CQRSSpring BootKafka
- 使用Spring Boot實現分散式事務Spring Boot分散式
- 分散式事務 | 使用DTM 的Saga 模式分散式模式
- MassTransit | 基於StateMachine實現Saga編排式分散式事務Mac分散式
- 分散式事務Saga模式分散式模式
- Dapr實現一個簡單的基於.net分散式事務之Saga模式分散式模式
- Spring Cloud Seata系列:基於AT模式實現分散式事務SpringCloud模式分散式
- debezium官方分散式事務Saga案例原始碼分散式原始碼
- MassTransit 知多少 | 基於MassTransit Courier實現Saga 編排式分散式事務分散式
- php基於dtm分散式事務管理器實現tcc模式分散式事務demoPHP分散式模式
- 基於RocketMQ實現分散式事務MQ分散式
- 在Kubernetes上使用Spring Boot實現Hazelcast分散式快取 – PiotrSpring BootAST分散式快取
- Spring Boot的微服務分散聚集模式教程與原始碼 - vinsguruSpring Boot微服務模式原始碼
- 通過Dapr實現一個簡單的基於.net的微服務電商系統(十九)——分散式事務之Saga模式微服務分散式模式
- MySQL 中基於 XA 實現的分散式事務MySql分散式
- 微服務分散式事務Saga框架微服務分散式框架
- 深度剖析Saga分散式事務分散式
- 基於Seata探尋分散式事務的實現方案分散式
- 使用Spring Boot實現事務管理Spring Boot
- Laravel基於reset機制實現分散式事務Laravel分散式
- Seata分散式事務TA模式原始碼解讀分散式模式原始碼
- Seata 分散式事務框架 TCC 模式原始碼分析分散式框架模式原始碼
- 微服務痛點-基於Dubbo + Seata的分散式事務(AT)模式微服務分散式模式
- 實戰與原理:如何基於RocketMQ實現分散式事務?MQ分散式
- 基於微服務框架Micronaut和Eventuate Tram實現分散式事務的開源案例微服務框架分散式
- 基於Redis實現分散式鎖,Redisson使用及原始碼分析Redis分散式原始碼
- Spring boot +mybatis 實現宣告式事務管理Spring BootMyBatis
- XA式、非XA式Spring分散式事務的實現Spring分散式
- 使用JOTM實現分散式事務的例子分散式
- 分散式事務(3)---RocketMQ實現分散式事務原理分散式MQ
- 微服務痛點-基於Dubbo + Seata的分散式事務(TCC模式)微服務分散式模式
- 基於Spring Boot和Spring Cloud實現微服務架構Spring BootCloud微服務架構
- 基於Redisson實現分散式鎖原始碼解讀Redis分散式原始碼
- 分散式事務(八)Spring Cloud微服務系統基於Rocketmq可靠訊息最終一致性實現分散式事務分散式SpringCloud微服務MQ
- 使用Spring Boot實現Redis事務 | VinsguruSpring BootRedis
- 分散式事務(4)---RocketMQ實現分散式事務專案分散式MQ