使用Kafka Streams和Spring Boot微服務中的分散式事務 - Piotr
在本文中,您將學習如何在 Spring Boot 中使用 Kafka Streams。我們將依賴 Spring Kafka 專案。為了很好地解釋它是如何工作的,我們將實現一個 saga 模式。saga 模式是一種跨微服務管理分散式事務的方法。該過程的關鍵階段是釋出觸發本地事務的事件。微服務通過訊息代理交換此類事件。事實證明,Kafka Streams 可以幫助我們。讓我們看看如何!
原始碼:
如果您想自己嘗試一下,可以隨時檢視我的原始碼。為此,您需要克隆我的 GitHub 儲存庫。之後,您應該按照我的指示進行操作。
除了 Spring Kafka,您還可以使用 Spring Cloud Stream for Kafka。您可以在本文中閱讀更多相關資訊。Spring Cloud Stream 提供了一些有用的功能,例如 DLQ 支援、預設的 JSON 序列化或互動式查詢。
架構
我們將建立一個簡單的系統,由三個微服務組成。訂單服務將訂單傳送到名為訂單的Kafka主題。其他兩個微服務stock-service和payment-service都會監聽傳入的事件。在接收到這些事件後,它們會驗證是否有可能執行該訂單。例如,如果客戶賬戶上沒有足夠的資金,訂單將被拒絕。否則,支付服務接受訂單,並向支付-訂單主題傳送一個響應。庫存服務也是如此,除了驗證庫存產品的數量並向庫存-訂單主題傳送一個響應。
然後,訂單服務通過訂單的ID將來自庫存-訂單和付款-訂單主題的兩個流連線起來。如果兩個訂單都被接受,它就確認了一個分散式交易。另一方面,如果一個訂單被接受,而第二個訂單被拒絕,它就會執行回滾。在這種情況下,它只是生成一個新的訂單事件並將其傳送到訂單主題。我們可以把訂單主題看作是訂單狀態變化的一個流,或者就像一個有最後狀態的表。下面的圖片直觀地展示了我們的場景。
Kafka Streams with Spring Boot
讓我們從訂單服務開始實施。令人驚訝的是,沒有針對Kafka的Spring Boot啟動器(除非我們使用Spring Cloud Stream)。因此,我們需要包含spring-kafka依賴項。為了處理流,我們還需要直接包含kafka-streams模組。由於訂單服務暴露了一些REST端點,所以需要新增Spring Boot Web啟動器。
<dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-web</artifactId> </dependency> <dependency> <groupId>org.springframework.kafka</groupId> <artifactId>spring-kafka</artifactId> </dependency> <dependency> <groupId>org.apache.kafka</groupId> <artifactId>kafka-streams</artifactId> </dependency> |
訂單服務是我們方案中最重要的微服務。它充當訂單閘道器和傳奇模式的協調者。它需要我們架構中使用的所有三個主題。為了在應用程式啟動時自動建立主題,我們需要定義以下bean類。
@Bean public NewTopic orders() { return TopicBuilder.name("orders") .partitions(3) .compact() .build(); } @Bean public NewTopic paymentTopic() { return TopicBuilder.name("payment-orders") .partitions(3) .compact() .build(); } @Bean public NewTopic stockTopic() { return TopicBuilder.name("stock-orders") .partitions(3) .compact() .build(); } |
然後,讓我們來定義我們的第一個Kafka流。
要做到這一點,我們需要使用StreamsBuilder Bean。
訂單服務接收來自支付服務(在payment-events主題)和股票服務(在stock-events主題)的事件。每一個事件都包含先前由訂單服務設定的ID。如果我們通過訂單的ID將這兩個流連線成一個流,我們將能夠確定我們的交易狀態。這個演算法非常簡單。如果支付服務和股票服務都接受了訂單,交易的最終狀態就是確認。如果兩個服務都拒絕了訂單,最終的狀態是REJECTED。最後一個選項是ROLLBACK--當一個服務接受了訂單,而一個服務拒絕了它。
下面是OrderManageService Bean中的描述方法:
@Service public class OrderManageService { public Order confirm(Order orderPayment, Order orderStock) { Order o = new Order(orderPayment.getId(), orderPayment.getCustomerId(), orderPayment.getProductId(), orderPayment.getProductCount(), orderPayment.getPrice()); if (orderPayment.getStatus().equals("ACCEPT") && orderStock.getStatus().equals("ACCEPT")) { o.setStatus("CONFIRMED"); } else if (orderPayment.getStatus().equals("REJECT") && orderStock.getStatus().equals("REJECT")) { o.setStatus("REJECTED"); } else if (orderPayment.getStatus().equals("REJECT") || orderStock.getStatus().equals("REJECT")) { String source = orderPayment.getStatus().equals("REJECT") ? "PAYMENT" : "STOCK"; o.setStatus("ROLLBACK"); o.setSource(source); } return o; } } |
最後,我們的流的實現。我們需要定義KStream bean。我們正在使用KStream的join方法連線兩個流。連線的視窗是10秒。結果是,我們正在設定訂單的狀態,並向訂單主題傳送一個新的訂單。我們使用與傳送新訂單相同的主題。
@Autowired OrderManageService orderManageService; @Bean public KStream<Long, Order> stream(StreamsBuilder builder) { JsonSerde<Order> orderSerde = new JsonSerde<>(Order.class); KStream<Long, Order> stream = builder .stream("payment-orders", Consumed.with(Serdes.Long(), orderSerde)); stream.join( builder.stream("stock-orders"), orderManageService::confirm, JoinWindows.of(Duration.ofSeconds(10)), StreamJoined.with(Serdes.Long(), orderSerde, orderSerde)) .peek((k, o) -> LOG.info("Output: {}", o)) .to("orders"); return stream; } |
SpringBoot配置
在Spring Boot中,應用程式的名稱預設為Kafka Streams的消費者組的名稱。因此,我們應該在application.yml中設定。當然,我們還需要設定Kafka bootstrap伺服器的地址。最後,我們要配置事件序列化的預設鍵和值。它同時適用於標準和流處理。
spring.application.name: orders spring.kafka: bootstrap-servers: 127.0.0.1:56820 producer: key-serializer: org.apache.kafka.common.serialization.LongSerializer value-serializer: org.springframework.kafka.support.serializer.JsonSerializer streams: properties: default.key.serde: org.apache.kafka.common.serialization.Serdes$LongSerde default.value.serde: org.springframework.kafka.support.serializer.JsonSerde spring.json.trusted.packages: "*" |
從Kafka主題傳送和接收事件
在上一節中,我們討論瞭如何建立一個新的Kafka流,作為連線其他兩個流的結果。
現在,讓我們看看如何處理傳入的訊息。我們可以以支付服務為例來考慮這個問題。
它監聽傳入的訂單。如果它收到一個新的訂單,它將在客戶的賬戶上執行預訂,並向payment-orders主題傳送一個帶有預訂狀態的響應。如果它收到來自訂單服務的交易確認,它將提交交易或回滾。為了啟用Kafka監聽器,我們應該用@EnableKafka來註解主類。此外,監聽方法必須用@KafkaListener來註解。下面的方法監聽訂單主題上的事件,並在支付消費者組中執行。
@SpringBootApplication @EnableKafka public class PaymentApp { private static final Logger LOG = LoggerFactory.getLogger(PaymentApp.class); public static void main(String[] args) { SpringApplication.run(PaymentApp.class, args); } @Autowired OrderManageService orderManageService; @KafkaListener(id = "orders", topics = "orders", groupId = "payment") public void onEvent(Order o) { LOG.info("Received: {}" , o); if (o.getStatus().equals("NEW")) orderManageService.reserve(o); else orderManageService.confirm(o); } } |
這裡是前面程式碼片段中使用的OrderManageService的實現。如果它收到新狀態的訂單,它將執行預訂。在預訂過程中,它將訂單的價格從 amountAvailable 欄位中減去,並在 amountReserved 欄位中新增相同的值。然後,它設定訂單的狀態,並使用KafkaTemplate向payment-orders主題傳送響應。在確認階段,它不傳送任何響應事件。它可以執行回滾,這意味著--將訂單的價格從reserved欄位中減去,並將其新增到amountAvailable欄位中。否則,它只是 "提交 "交易事務,從保留金額欄位中減去價格。
@Service public class OrderManageService { private static final String SOURCE = "payment"; private static final Logger LOG = LoggerFactory.getLogger(OrderManageService.class); private CustomerRepository repository; private KafkaTemplate<Long, Order> template; public OrderManageService(CustomerRepository repository, KafkaTemplate<Long, Order> template) { this.repository = repository; this.template = template; } public void reserve(Order order) { Customer customer = repository.findById(order.getCustomerId()).orElseThrow(); LOG.info("Found: {}", customer); if (order.getPrice() < customer.getAmountAvailable()) { order.setStatus("ACCEPT"); customer.setAmountReserved(customer.getAmountReserved() + order.getPrice()); customer.setAmountAvailable(customer.getAmountAvailable() - order.getPrice()); } else { order.setStatus("REJECT"); } order.setSource(SOURCE); repository.save(customer); template.send("payment-orders", order.getId(), order); LOG.info("Sent: {}", order); } public void confirm(Order order) { Customer customer = repository.findById(order.getCustomerId()).orElseThrow(); LOG.info("Found: {}", customer); if (order.getStatus().equals("CONFIRMED")) { customer.setAmountReserved(customer.getAmountReserved() - order.getPrice()); repository.save(customer); } else if (order.getStatus().equals("ROLLBACK") && !order.getSource().equals(SOURCE)) { customer.setAmountReserved(customer.getAmountReserved() - order.getPrice()); customer.setAmountAvailable(customer.getAmountAvailable() + order.getPrice()); repository.save(customer); } } } |
庫存服務stock-service
一個類似的邏輯在庫存服務方面被實現。然而,它使用欄位productCount而不是訂單的價格,並對所需數量的訂單產品進行預訂。下面是庫存服務中OrderManageService類的實現。
@Service public class OrderManageService { private static final String SOURCE = "stock"; private static final Logger LOG = LoggerFactory.getLogger(OrderManageService.class); private ProductRepository repository; private KafkaTemplate<Long, Order> template; public OrderManageService(ProductRepository repository, KafkaTemplate<Long, Order> template) { this.repository = repository; this.template = template; } public void reserve(Order order) { Product product = repository.findById(order.getProductId()).orElseThrow(); LOG.info("Found: {}", product); if (order.getStatus().equals("NEW")) { if (order.getProductCount() < product.getAvailableItems()) { product.setReservedItems(product.getReservedItems() + order.getProductCount()); product.setAvailableItems(product.getAvailableItems() - order.getProductCount()); order.setStatus("ACCEPT"); repository.save(product); } else { order.setStatus("REJECT"); } template.send("stock-orders", order.getId(), order); LOG.info("Sent: {}", order); } } public void confirm(Order order) { Product product = repository.findById(order.getProductId()).orElseThrow(); LOG.info("Found: {}", product); if (order.getStatus().equals("CONFIRMED")) { product.setReservedItems(product.getReservedItems() - order.getProductCount()); repository.save(product); } else if (order.getStatus().equals("ROLLBACK") && !order.getSource().equals(SOURCE)) { product.setReservedItems(product.getReservedItems() - order.getProductCount()); product.setAvailableItems(product.getAvailableItems() + order.getProductCount()); repository.save(product); } } } |
用Spring Boot查詢Kafka流
現在,讓我們考慮以下場景:
首先,訂單服務收到一個新的訂單(通過REST API)並將其傳送到Kafka主題。然後,這個訂單被其他兩個微服務接收。一旦他們發回一個積極的響應(或消極的),訂單服務就會把它們作為流來處理,並改變訂單的狀態。具有新狀態的訂單將被排放到與之前相同的主題。那麼,我們在哪裡儲存訂單的當前狀態的資料呢?在Kafka主題中。我們將再次在我們的Spring Boot應用程式中使用Kafka Streams。但是這一次,我們利用了KTable的優勢。讓我們來看看我們的場景的視覺化情況。
好了,讓我們在訂單服務中定義另一個Kafka Streams bean。我們將獲得相同的訂單主題作為一個流。我們將把它轉換為Kafka表,並在一個持久的儲存中實現它。由於這一點,我們將能夠輕鬆地從我們的REST控制器中查詢儲存。
@Bean public KTable<Long, Order> table(StreamsBuilder builder) { KeyValueBytesStoreSupplier store = Stores.persistentKeyValueStore("orders"); JsonSerde<Order> orderSerde = new JsonSerde<>(Order.class); KStream<Long, Order> stream = builder .stream("orders", Consumed.with(Serdes.Long(), orderSerde)); return stream.toTable(Materialized.<Long, Order>as(store) .withKeySerde(Serdes.Long()) .withValueSerde(orderSerde)); } |
如果我們在同一臺機器上執行多個訂單服務例項,覆蓋狀態儲存的預設位置也很重要。要做到這一點,我們應該為每一個例項定義以下獨特的屬性。
spring.kafka.streams.state-dir: /tmp/kafka-streams/1
不幸的是,Spring Boot中沒有內建支援Kafka流的互動式查詢。然而,我們可以使用自動配置的StreamsBuilderFactoryBean將KafkaStreams例項注入控制器。然後我們可以在 "物化 materialized"名稱下查詢狀態儲存。這當然是非常微不足道的例子。我們只是從KTable獲取所有的訂單。
@GetMapping public List<Order> all() { List<Order> orders = new ArrayList<>(); ReadOnlyKeyValueStore<Long, Order> store = kafkaStreamsFactory .getKafkaStreams() .store(StoreQueryParameters.fromNameAndType( "orders", QueryableStoreTypes.keyValueStore())); KeyValueIterator<Long, Order> it = store.all(); it.forEachRemaining(kv -> orders.add(kv.value)); return orders; } |
在同一個OrderController中,也有一個方法用於傳送一個新訂單到Kafka主題。
@PostMapping public Order create(@RequestBody Order order) { order.setId(id.incrementAndGet()); template.send("orders", order.getId(), order); LOG.info("Sent: {}", order); return order; } |
測試場景
在我們執行我們的示例微服務之前,我們需要啟動 Kafka 的本地例項。通常,我為此使用 Redpanda。它是一個相容 Kafka API 的流媒體平臺。與 Kafka 相比,在本地執行它相對容易。您需要做的就是在rpk本地安裝CLI(這裡是macOS的說明)。之後,您可以使用以下命令建立單節點例項:
$ rpk container start
執行後,它將列印您的節點的地址。對我來說,是 127.0.0.1:56820。您應該將該地址作為spring.kafka.bootstrap-servers屬性的值。您還可以使用以下命令顯示已建立主題的列表:
$ rpk topic list --brokers 127.0.0.1:56820
然後,讓我們執行我們的微服務。從 開始,order-service因為它正在建立所有必需的主題並構建 Kafka Streams 例項。您可以使用 REST 端點傳送單個訂單:
$ curl -X 'POST' \ 'http://localhost:8080/orders' \ -H 'Content-Type: application/json' \ -d '{ "customerId": 10, "productId": 10, "productCount": 5, "price": 100, "status": "NEW" }' |
以下 bean 負責生成 10000 個隨機訂單:
@Service public class OrderGeneratorService { private static Random RAND = new Random(); private AtomicLong id = new AtomicLong(); private Executor executor; private KafkaTemplate<Long, Order> template; public OrderGeneratorService(Executor executor, KafkaTemplate<Long, Order> template) { this.executor = executor; this.template = template; } @Async public void generate() { for (int i = 0; i < 10000; i++) { int x = RAND.nextInt(5) + 1; Order o = new Order(id.incrementAndGet(), RAND.nextLong(100) + 1, RAND.nextLong(100) + 1, "NEW"); o.setPrice(100 * x); o.setProductCount(x); template.send("orders", o.getId(), o); } } } |
可以通過呼叫端點來啟動該過程POST /orders/generate。
無論是決定傳送單個訂單還是生成多個隨機訂單,您都可以使用以下端點輕鬆查詢訂單狀態:
$ curl http://localhost:8080/orders
這是由應用程式和 Kafka Streams 生成的主題結構,用於執行連線操作並將訂單儲存KTable為狀態儲存。
相關文章
- 使用Spring Boot和Kafka Streams實現基於SAGA模式的分散式事務原始碼教程 - PiotrSpring BootKafka模式分散式原始碼
- 使用Spring Boot和GraalVM在Knative上構建微服務 - piotrSpring BootLVM微服務
- 使用Spring Boot實現分散式事務Spring Boot分散式
- 使用Spring Boot + Kafka實現Saga分散式事務模式的原始碼 - vinsguruSpring BootKafka分散式模式原始碼
- 使用Spring Boot和Kafka Streams實現CQRSSpring BootKafka
- 比較微服務中的分散式事務模式微服務分散式模式
- 微服務架構中的分散式事務全面詳解 -DZone微服務微服務架構分散式
- PHP 微服務之 [分散式事務]PHP微服務分散式
- PHP 微服務之【分散式事務】PHP微服務分散式
- 分散式鎖和spring事務管理分散式Spring
- 備忘錄五:Spring Boot + RabbitMQ 分散式事務Spring BootMQ分散式
- DTM:Golang中微服務架構的分散式事務框架Golang微服務架構分散式框架
- 微服務分散式事務元件 Seata(一)微服務分散式元件
- 分散式事務之Spring事務與JMS事務(二)分散式Spring
- 微服務的分散式事務模式比較 | RedHat微服務分散式模式Redhat
- Spring Boot 整合 Seata 解決分散式事務問題Spring Boot分散式
- 事務使用中如何避免誤用分散式事務分散式
- 微服務架構 | 11. 分散式事務微服務架構分散式
- 在Kubernetes上使用Spring Boot實現Hazelcast分散式快取 – PiotrSpring BootAST分散式快取
- 分散式事務(一)—分散式事務的概念分散式
- 分散式事務和分散式hash分散式
- spring cloud微服務分散式雲架構--hystrix的使用SpringCloud微服務分散式架構
- 本地事務和分散式事務的區別分散式
- 分散式事務處理方案,微服事務處理方案分散式
- PHP 微服務之【分散式事務】閱讀提示PHP微服務分散式
- PHP 微服務之 [分散式事務] 閱讀提示PHP微服務分散式
- 微服務架構分散式事務管理問題微服務架構分散式
- Spring Cloud Spring Boot mybatis分散式微服務雲架構CloudSpring BootMyBatis分散式微服務架構
- GRIT:eBay基於微服務的分散式事務協議微服務分散式協議
- Spring Cloud Alibaba 使用Seata解決分散式事務SpringCloud分散式
- 微服務架構及分散式事務解決方案微服務架構分散式
- DBPack 賦能 python 微服務協調分散式事務Python微服務分散式
- 使用Spring Boot實現事務管理Spring Boot
- 使用Seata徹底解決Spring Cloud中的分散式事務問題!SpringCloud分散式
- 微服務痛點-基於Dubbo + Seata的分散式事務(AT)模式微服務分散式模式
- 通過Spring Boot,Spring Cloud Gateway構建基於Consul叢集的微服務案例演示 – Piotr的TechBlogSpring BootCloudGateway微服務
- 透過Spring Boot,Spring Cloud Gateway構建基於Consul叢集的微服務案例演示 – Piotr的TechBlogSpring BootCloudGateway微服務
- Spring Boot的微服務分散聚集模式教程與原始碼 - vinsguruSpring Boot微服務模式原始碼