穩健!基于 Spring Boot 的事務外包模式構建可靠微服務
隨著軟件架構的不斷演變,微服務架構 成為解決系統復雜性和增強可擴展性的主要方式。然而,微服務架構也帶來了新的挑戰,尤其是在分布式環境下保證數據一致性和可靠性。隨著業務流程的復雜化,服務之間需要頻繁地交互、共享數據以及發送消息,這就帶來了“分布式事務”問題。如果某個服務在更新數據庫后需要立即通知其他服務,而在通知過程中出現問題,例如消息發送失敗或網絡故障,那么系統可能會陷入不一致狀態。
在這種情況下,簡單的事務控制(如本地事務)無法有效地解決跨服務的數據一致性問題。為了解決這個挑戰,事務外包(Transactional Outbox)模式 被提出,以確保服務在處理數據庫操作時,同時能夠可靠地發送消息,從而解決了數據庫與消息隊列之間的不一致問題。
什么是事務外包模式?
事務外包模式 是一種保證數據庫操作與消息傳遞之間一致性的設計模式。它的核心思想是將所有需要發送的消息存儲在數據庫中,將其與數據庫操作綁定在同一事務內。這樣,當數據庫操作成功提交時,消息也會被持久化到數據庫,后續通過定時任務或事件輪詢機制將這些消息發送到消息系統,如 Kafka、RabbitMQ 或其他外部系統。
傳統的分布式事務通過兩階段提交(2PC)來保證一致性,但兩階段提交會帶來較大的性能開銷,且難以處理網絡或系統故障。相比之下,事務外包模式提供了一種高效、靈活的替代方案:
- 事務一致性:通過將消息和數據庫操作放在同一事務內,保證它們要么同時成功,要么同時失敗。
- 異步處理:消息可以通過異步方式發送到消息隊列,避免對數據庫操作產生延遲。
- 高可用性和容錯性:即使在消息系統不可用的情況下,消息依然能夠可靠地保存在數據庫中,等待消息系統恢復后發送。
通過這種方式,我們可以在保持服務間松耦合的同時,確保分布式系統的數據一致性和高可用性。
事務外包模式的工作原理
- 業務數據與消息一起持久化:當一個服務執行數據庫操作時,消息并不會立即發送,而是與業務數據一起存儲在數據庫的 Outbox 表中。這樣,業務數據和消息的持久化在同一個事務中被處理,確保兩者的一致性。
- 定時輪詢消息表:系統會通過定時任務輪詢 Outbox 表,查找未發送的消息,并將其發送到目標消息系統(如 Kafka 或 RabbitMQ)。
- 消息傳遞確認:當消息成功發送后,Outbox 表中的相應記錄會被刪除或標記為已處理。
這種模式的核心思想是將消息的可靠傳遞變成一個可控的、異步的過程,并通過持久化機制保證即使消息系統暫時不可用,也不會丟失消息。
運行效果:
圖片
若想獲取項目完整代碼以及其他文章的項目源碼,且在代碼編寫時遇到問題需要咨詢交流,歡迎加入下方的知識星球。
Spring Boot 實現事務外包模式
項目基礎配置
為了實現事務外包模式,我們將使用 Spring Boot、JPA、Lombok 和 Thymeleaf,并通過定時任務來輪詢數據庫中的 Outbox 表。下面的 pom.xml 配置了項目所需的依賴:
<?xml versinotallow="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 https://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<parent>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-parent</artifactId>
<version>3.3.4</version>
<relativePath/> <!-- lookup parent from repository -->
</parent>
<groupId>com.icoderoad</groupId>
<artifactId>outbox</artifactId>
<version>0.0.1-SNAPSHOT</version>
<name>outbox</name>
<description>Demo project for Spring Boot</description>
<properties>
<java.version>17</java.version>
</properties>
<dependencies>
<!-- Spring Boot Starter Dependencies -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-jpa</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<!-- Thymeleaf -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-thymeleaf</artifactId>
</dependency>
<!-- Lombok -->
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
<scope>provided</scope>
</dependency>
<!-- 數據庫驅動依賴 -->
<dependency>
<groupId>com.mysql</groupId>
<artifactId>mysql-connector-j</artifactId>
<scope>runtime</scope>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
<scope>test</scope>
</dependency>
</dependencies>
<build>
<plugins>
<plugin>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-maven-plugin</artifactId>
</plugin>
</plugins>
</build>
</project>
application.yaml 配置
我們使用 Mysql 數據庫進行持久化,yaml 文件配置了數據庫連接和 Outbox 的輪詢間隔。
spring:
datasource:
driver-class-name: com.mysql.cj.jdbc.Driver
url: jdbc:mysql://localhost:3306/mydb?useSSL=false&allowPublicKeyRetrieval=true&serverTimeznotallow=UTC
username: root
password: root
jpa:
hibernate:
ddl-auto: update
show-sql: true
outbox:
polling-interval: 1000 # 設置輪詢間隔為 1 秒
使用 @ConfigurationProperties 讀取配置
為了方便管理和修改輪詢間隔等配置項,我們使用 @ConfigurationProperties 注解將配置文件中的屬性注入到 Java 類中。
package com.icoderoad.outbox.config;
import org.springframework.boot.context.properties.ConfigurationProperties;
import org.springframework.stereotype.Component;
import lombok.Data;
@Data
@Component
@ConfigurationProperties(prefix = "outbox")
public class OutboxProperties {
private long pollingInterval;
}
實現事務外包模式
在 Spring Boot 中,事務外包模式可以通過一個簡單的數據庫表(如 OutboxEvent)來持久化所有未處理的消息。每次有業務操作時,生成相應的事件并持久化到數據庫表中,然后通過定時任務處理這些事件。
數據庫實體類
package com.icoderoad.outbox.entity;
import jakarta.persistence.Entity;
import jakarta.persistence.GeneratedValue;
import jakarta.persistence.GenerationType;
import jakarta.persistence.Id;
import lombok.Data;
@Data
@Entity
public class OutboxEvent {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
private String aggregateType;
private String aggregateId;
private String eventType;
private String payload; // 存儲事件內容
}
Order 類實現
package com.icoderoad.outbox.entity;
import jakarta.persistence.Entity;
import jakarta.persistence.GeneratedValue;
import jakarta.persistence.GenerationType;
import jakarta.persistence.Id;
import jakarta.persistence.Table;
import lombok.Data;
@Data
@Entity
@Table(name = "orders")
public class Order {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
private String orderName; // 訂單名稱
}
OrderRepository 類實現
package com.icoderoad.outbox.repository;
import org.springframework.data.jpa.repository.JpaRepository;
import org.springframework.stereotype.Repository;
import com.icoderoad.outbox.entity.Order;
@Repository
public interface OrderRepository extends JpaRepository<Order, Long> {
}
OutboxEventRepository 類實現
package com.icoderoad.outbox.repository;
import org.springframework.data.jpa.repository.JpaRepository;
import org.springframework.stereotype.Repository;
import com.icoderoad.outbox.entity.OutboxEvent;
@Repository
public interface OutboxEventRepository extends JpaRepository<OutboxEvent, Long> {
// 這里可以定義自定義查詢方法,例如查詢未處理的事件等
// List<OutboxEvent> findByProcessedFalse();
}
業務服務類
業務邏輯中,當執行訂單操作時,事件不會直接發送,而是先持久化到 Outbox 表中。
package com.icoderoad.outbox.service;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
import com.icoderoad.outbox.entity.Order;
import com.icoderoad.outbox.entity.OutboxEvent;
import com.icoderoad.outbox.repository.OrderRepository;
import com.icoderoad.outbox.repository.OutboxEventRepository;
@Service
public class OrderService {
private final OrderRepository orderRepository;
private final OutboxEventRepository outboxEventRepository;
public OrderService(OrderRepository orderRepository, OutboxEventRepository outboxEventRepository) {
this.orderRepository = orderRepository;
this.outboxEventRepository = outboxEventRepository;
}
@Transactional
public void placeOrder(Order order) {
// 先保存訂單信息,確保生成 ID
Order savedOrder = orderRepository.save(order);
// 保存訂單之后,才能獲取訂單的 ID
OutboxEvent event = new OutboxEvent();
event.setAggregateType("Order");
event.setAggregateId(savedOrder.getId().toString()); // 使用保存后的訂單 ID
event.setEventType("OrderCreated");
event.setPayload(savedOrder.toString()); // 可以根據需要將訂單信息序列化成 JSON
// 保存事件信息
outboxEventRepository.save(event);
}
}
定時輪詢任務
定時任務用于從 Outbox 表中讀取未處理的事件并將其發送至消息隊列。
package com.icoderoad.outbox.poller;
import java.util.List;
import org.springframework.scheduling.annotation.Scheduled;
import org.springframework.stereotype.Component;
import com.icoderoad.outbox.entity.OutboxEvent;
import com.icoderoad.outbox.repository.OutboxEventRepository;
@Component
public class OutboxPoller {
private final OutboxEventRepository outboxEventRepository;
public OutboxPoller(OutboxEventRepository outboxEventRepository) {
this.outboxEventRepository = outboxEventRepository;
}
@Scheduled(fixedDelayString = "${outbox.polling-interval}")
public void pollOutbox() {
List<OutboxEvent> events = outboxEventRepository.findAll();
for (OutboxEvent event : events) {
// 發送消息至消息隊列
// messageQueue.send(event);
// 刪除或標記為已處理
outboxEventRepository.delete(event);
}
}
}
后端控制器
package com.icoderoad.outbox.controller;
import java.util.HashMap;
import java.util.Map;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
import com.icoderoad.outbox.entity.Order;
import com.icoderoad.outbox.service.OrderService;
@RestController
@RequestMapping("/api/orders")
public class OrderController {
private final OrderService orderService;
public OrderController(OrderService orderService) {
this.orderService = orderService;
}
@PostMapping
public Map<String, String> placeOrder(@RequestBody Order order) {
orderService.placeOrder(order);
Map<String, String> response = new HashMap<>();
response.put("status", "success");
response.put("message", "訂單提交成功!");
return response;
}
}
前端實現
使用 Thymeleaf 渲染頁面,并使用 JQuery 通過 AJAX 請求后端 API,將結果以 Bootstrap 風格的提示框顯示。
在 src/main/resources/templates 目錄下創建 index.html 文件:
<!DOCTYPE html>
<html xmlns:th="http://www.thymeleaf.org">
<head>
<title>訂單頁面</title>
<link rel="stylesheet" >
<script src="http://code.jquery.com/jquery-3.6.0.min.js"></script>
</head>
<body>
<div class="container">
<h2>訂單表單</h2>
<div class="alert alert-success" id="success-alert" style="display: none;"></div>
<div class="alert alert-danger" id="error-alert" style="display: none;"></div>
<form id="orderForm">
<div class="mb-3">
<label for="orderName" class="form-label">訂單名稱</label>
<input type="text" class="form-control" id="orderName" name="orderName">
</div>
<button type="submit" class="btn btn-primary">提交訂單</button>
</form>
</div>
<script>
$(document).ready(function() {
$('#orderForm').on('submit', function(event) {
event.preventDefault();
var orderData = {
orderName: $('#orderName').val()
};
$.ajax({
url: '/api/orders',
type: 'POST',
contentType: 'application/json',
data: JSON.stringify(orderData),
success: function(response) {
$('#success-alert').text(response.message).show();
$('#error-alert').hide();
},
error: function() {
$('#error-alert').text('訂單提交失敗!').show();
$('#success-alert').hide();
}
});
});
});
</script>
</body>
</html>
總結
事務外包模式 提供了一種簡潔高效的解決方案,確保在微服務架構下的消息傳遞和數據一致性問題。通過將業務數據和事件存儲在同一個數據庫事務中,并結合定時輪詢機制將事件發送至消息隊列,開發者能夠輕松處理分布式環境中的一致性挑戰。與傳統的兩階段提交相比,事務外包模式提供了更好的可擴展性、性能和可靠性。
同時,本文通過前后端結合的方式展示了如何使用 Thymeleaf、JQuery 和 Bootstrap 實現一個訂單系統。這種架構可以進一步擴展,如支持更復雜的消息系統或集成更多服務,以滿足不斷增長的業務需求。