項目再也不怕異常!Spring Boot 3 帶你優(yōu)雅實現(xiàn)事務(wù)回滾 + 自動重試機制!
在復雜的分布式業(yè)務(wù)中,事務(wù)異常與短暫性失敗是不可避免的: 網(wǎng)絡(luò)閃斷、鎖沖突、樂觀鎖更新失敗、數(shù)據(jù)庫連接暫時不可用……這些“小意外”往往導致整個操作失敗,甚至引發(fā)數(shù)據(jù)不一致。
但如果你掌握了 Spring Boot 3 的事務(wù)回滾 + 重試一體化實踐,這一切都能優(yōu)雅解決。 本文將帶你一步步實現(xiàn)這種高可用機制——讓系統(tǒng)在失敗后自動“自愈”,事務(wù)回滾、自動重試、失敗補償一氣呵成。
一體化核心思想:Transactional × Retryable
在 Spring 體系中:
- @Transactional 管理數(shù)據(jù)庫事務(wù),保障原子性,出現(xiàn)異常時回滾;
- @Retryable 提供自動重試機制,針對臨時性錯誤(如鎖沖突、超時、瞬時失聯(lián))進行重試;
- 當兩者組合時,每次重試都會創(chuàng)建一個全新的事務(wù)環(huán)境。
核心思想一句話概括:
@Retryable 在外,@Transactional 在內(nèi)。 每次重試都是全新的事務(wù),失敗即回滾,直到成功或觸發(fā) @Recover 補償。
Maven 依賴配置
<dependencies>
<!-- Spring Boot 數(shù)據(jù)層支持(包含事務(wù)管理) -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-jpa</artifactId>
</dependency>
<!-- Spring Retry:自動重試支持 -->
<dependency>
<groupId>org.springframework.retry</groupId>
<artifactId>spring-retry</artifactId>
<version>2.0.12</version>
</dependency>
<!-- AOP 切面支持(Retry 基于 AOP 實現(xiàn)) -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-aop</artifactId>
</dependency>
</dependencies>啟用重試與應用啟動類
路徑:/src/main/java/com/icoderoad/Application.java
package com.icoderoad;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.retry.annotation.EnableRetry;
@SpringBootApplication
@EnableRetry // 啟用 Spring Retry 功能
public class TransactionalRetryApplication {
public static void main(String[] args) {
SpringApplication.run(TransactionalRetryApplication.class, args);
}
}切面執(zhí)行順序:
@Retryable → 捕獲異常并觸發(fā)重試
@Transactional → 每次嘗試都在新事務(wù)內(nèi)也就是說:重試的不是 SQL,而是整個事務(wù)方法。
領(lǐng)域模型定義
路徑:/src/main/java/com/icoderoad/domain/Order.java
package com.icoderoad.domain;
import jakarta.persistence.*;
import lombok.Data;
import java.math.BigDecimal;
@Data
@Entity
@Table(name = "t_order", uniqueConstraints = {
@UniqueConstraint(name = "uk_order_req_id", columnNames = {"client_request_id"})
})
public class Order {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
@Column(name = "client_request_id", nullable = false, updatable = false)
private String clientRequestId; // 冪等請求ID
private Long productId;
private Integer quantity;
private BigDecimal amount;
@Version
private Long version; // 樂觀鎖版本號
private String status;
}核心服務(wù)層:事務(wù) + 自動重試 + 失敗補償
路徑:/src/main/java/com/icoderoad/service/OrderService.java
package com.icoderoad.service;
import com.icoderoad.domain.Order;
import com.icoderoad.repository.OrderRepository;
import com.icoderoad.request.OrderRequest;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.dao.*;
import org.springframework.retry.annotation.*;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
@Slf4j
@Service
@RequiredArgsConstructor
public class OrderService {
private final OrderRepository orderRepository;
private final InventoryService inventoryService;
/**
* 下單事務(wù)方法:支持自動重試與事務(wù)回滾
*/
@Retryable(
retryFor = {
ObjectOptimisticLockingFailureException.class,
DeadlockLoserDataAccessException.class,
CannotAcquireLockException.class,
TransientDataAccessResourceException.class
},
noRetryFor = { IllegalArgumentException.class },
maxAttempts = 3,
backoff = @Backoff(delay = 2000, multiplier = 2.0, maxDelay = 10000)
)
@Transactional
public Order placeOrder(OrderRequest request) {
log.info("執(zhí)行下單事務(wù),reqId={}", request.getClientRequestId());
// 冪等性防重復
return orderRepository.findByClientRequestId(request.getClientRequestId())
.orElseGet(() -> {
Order order = new Order();
order.setClientRequestId(request.getClientRequestId());
order.setProductId(request.getProductId());
order.setQuantity(request.getQuantity());
order.setStatus("CREATED");
Order saved = orderRepository.save(order);
inventoryService.reduceStock(request.getProductId(), request.getQuantity());
return saved;
});
}
/**
* 當所有重試失敗后執(zhí)行的兜底邏輯
*/
@Recover
public Order placeOrderFallback(Exception e, OrderRequest request) {
log.error("下單失敗,reqId={},錯誤={}", request.getClientRequestId(), e.getMessage(), e);
throw new RuntimeException("系統(tǒng)繁忙,請稍后再試", e);
}
}每次失敗都會觸發(fā)事務(wù)回滾,每次重試重新開啟事務(wù),最終進入 @Recover 執(zhí)行補償邏輯。
冪等性與安全性設(shè)計
要讓重試機制安全有效,必須防止數(shù)據(jù)重復寫入。常見策略包括:
策略 | 說明 |
唯一約束 | 通過數(shù)據(jù)庫唯一鍵防止重復插入,如 |
狀態(tài)機控制 | 限制狀態(tài)流轉(zhuǎn),避免重復更新 |
請求去重表 | 記錄請求 ID,攔截重復提交 |
令牌機制 | 客戶端發(fā)起請求時生成唯一 token |
實踐注意事項
- 冪等性是前提:否則重試會導致重復下單。
- 只重試瞬時異常:業(yè)務(wù)邏輯錯誤(如庫存不足)不應重試。
- 方法必須 public:否則切面不生效。
- AOP 自調(diào)用無效:必須由外部 Bean 調(diào)用觸發(fā)。
- @Recover 參數(shù)規(guī)則:第一個為異常類型,其余與原方法一致。
并發(fā)測試驗證
路徑:/src/test/java/com/icoderoad/service/OrderServiceIT.java
package com.icoderoad.service;
import com.icoderoad.domain.Order;
import com.icoderoad.request.OrderRequest;
import org.junit.jupiter.api.Test;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.context.SpringBootTest;
import java.util.concurrent.*;
import static org.junit.jupiter.api.Assertions.assertEquals;
@SpringBootTest
class OrderServiceIT {
@Autowired
OrderService orderService;
@Test
void should_retry_and_return_same_order_when_conflict() throws Exception {
String reqId = "REQ-" + System.nanoTime();
ExecutorService pool = Executors.newFixedThreadPool(2);
Callable<Order> task = () -> orderService.placeOrder(new OrderRequest(reqId, 1001L, 1));
Order o1 = pool.submit(task).get();
Order o2 = pool.submit(task).get();
assertEquals(o1.getId(), o2.getId());
}
}測試模擬樂觀鎖沖突,通過并發(fā)觸發(fā)重試邏輯,驗證事務(wù)一致性與冪等性。
完整可運行示例:事務(wù) + 重試 + 補償
(1) 主應用類
/src/main/java/com/icoderoad/TransactionalRetryDemoApplication.java
package com.icoderoad;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.retry.annotation.EnableRetry;
@SpringBootApplication
@EnableRetry
public class TransactionalRetryDemoApplication {
public static void main(String[] args) {
SpringApplication.run(TransactionalRetryDemoApplication.class, args);
}
}(2) DTO 類
/src/main/java/com/icoderoad/dto/OrderDTO.java
package com.icoderoad.dto;
public record OrderRequest(String productId, int quantity, double price) {}
public record OrderResponse(String orderId, String status, String message) {}(3) 控制層
/src/main/java/com/icoderoad/controller/OrderController.java
package com.icoderoad.controller;
import com.icoderoad.dto.*;
import com.icoderoad.service.OrderService;
import org.springframework.web.bind.annotation.*;
@RestController
@RequestMapping("/orders")
public class OrderController {
private final OrderService orderService;
public OrderController(OrderService orderService) { this.orderService = orderService; }
@PostMapping
public OrderResponse createOrder(@RequestBody OrderRequest request) {
try {
String orderId = orderService.placeOrder(request);
return new OrderResponse(orderId, "SUCCESS", "Order created successfully");
} catch (Exception e) {
return new OrderResponse(null, "FAILED", "Failed to create order: " + e.getMessage());
}
}
}(4) Service 層(事務(wù) + 重試 + 補償)
路徑:/src/main/java/com/icoderoad/service/OrderService.java
package com.icoderoad.service;
import org.springframework.dao.DataAccessException;
import org.springframework.retry.annotation.*;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
import javax.sql.DataSource;
import java.sql.*;
import java.util.Random;
import java.util.UUID;
@Service
public class OrderService {
private final DataSource dataSource;
private final Random random = new Random();
public OrderService(DataSource dataSource) {
this.dataSource = dataSource;
}
@Retryable(
value = {SQLException.class, DataAccessException.class},
maxAttempts = 3,
backoff = @Backoff(delay = 1000, multiplier = 2.0)
)
@Transactional(rollbackFor = Exception.class)
public String placeOrder(com.icoderoad.dto.OrderRequest request) throws SQLException {
System.out.println("Processing order for product: " + request.productId());
// 模擬臨時性數(shù)據(jù)庫故障
if (random.nextDouble() < 0.3) {
throw new SQLException("Database temporarily unavailable");
}
String orderId = UUID.randomUUID().toString();
try (Connection conn = dataSource.getConnection();
PreparedStatement stmt = conn.prepareStatement(
"INSERT INTO orders (id, product_id, quantity, price) VALUES (?, ?, ?, ?)")) {
stmt.setString(1, orderId);
stmt.setString(2, request.productId());
stmt.setInt(3, request.quantity());
stmt.setDouble(4, request.price());
stmt.executeUpdate();
updateInventory(request.productId(), request.quantity());
return orderId;
}
}
private void updateInventory(String productId, int quantity) throws SQLException {
try (Connection conn = dataSource.getConnection();
PreparedStatement stmt = conn.prepareStatement(
"UPDATE inventory SET stock = stock - ? WHERE product_id = ?")) {
stmt.setInt(1, quantity);
stmt.setString(2, productId);
int rows = stmt.executeUpdate();
if (rows == 0) throw new SQLException("Product not found: " + productId);
// 模擬庫存鎖沖突
if (random.nextDouble() < 0.1) {
throw new SQLException("Inventory lock timeout");
}
}
}
@Recover
public String recover(Exception e, com.icoderoad.dto.OrderRequest request) {
System.err.println("All retries failed for order: " + request);
System.err.println("Cause: " + e.getMessage());
try (Connection conn = dataSource.getConnection();
PreparedStatement stmt = conn.prepareStatement(
"INSERT INTO failed_orders (product_id, quantity, price, reason) VALUES (?, ?, ?, ?)")) {
stmt.setString(1, request.productId());
stmt.setInt(2, request.quantity());
stmt.setDouble(3, request.price());
stmt.setString(4, e.getMessage());
stmt.executeUpdate();
} catch (SQLException ex) {
ex.printStackTrace();
}
return null;
}
}(5) 配置文件
/src/main/resources/application.properties
spring.datasource.url=jdbc:h2:mem:testdb
spring.datasource.driverClassName=org.h2.Driver
spring.datasource.username=sa
spring.datasource.password=
spring.sql.init.schema-locations=classpath:schema.sql
spring.sql.init.mode=embedded
spring.jpa.show-sql=true
spring.h2.console.enabled=true(6) 數(shù)據(jù)庫初始化腳本
/src/main/resources/schema.sql
CREATE TABLE IF NOT EXISTS orders (
id VARCHAR(255) PRIMARY KEY,
product_id VARCHAR(255),
quantity INT,
price DOUBLE
);
CREATE TABLE IF NOT EXISTS inventory (
product_id VARCHAR(255) PRIMARY KEY,
stock INT
);
CREATE TABLE IF NOT EXISTS failed_orders (
id BIGINT AUTO_INCREMENT PRIMARY KEY,
product_id VARCHAR(255),
quantity INT,
price DOUBLE,
reason VARCHAR(500),
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
);
INSERT INTO inventory (product_id, stock) VALUES ('prod-001', 100);
INSERT INTO inventory (product_id, stock) VALUES ('prod-002', 50);API 測試示例
curl -X POST http://localhost:8080/orders \
-H "Content-Type: application/json" \
-d '{"productId":"prod-001","quantity":2,"price":29.99}'控制臺將打印重試與回滾日志,全過程可觀測。
結(jié)語:讓系統(tǒng)具備「自我修復」能力
在高并發(fā)與分布式系統(tǒng)中,瞬時性失敗不可避免。 通過 @Transactional + @Retryable + @Recover 的組合, 我們?yōu)閼脴?gòu)建了一套自愈型事務(wù)機制:
- 每次重試都在獨立事務(wù)中執(zhí)行;
- 失敗即回滾,無副作用;
- 重試窮盡后進入兜底邏輯;
- 冪等保障防止重復寫入。
這種一體化模式讓你的 Spring Boot 項目在面對異常時更加穩(wěn)健, 不只是“報錯重來”,而是真正實現(xiàn)——優(yōu)雅回滾、智能重試、可恢復的事務(wù)執(zhí)行機制。

































