Spring Boot 3 事务回滚 + 重试一体化最佳实践(Transactional × Retry)
一体化思想:@Retryable 在外、@Transactional 在内。每一次重试都会开启 全新的事务,失败即回滚;直到成功提交或用尽次数走 @Recover 兜底。
核心思路
- 事务管理 (@Transactional):保障数据库操作的原子性,遇到异常时回滚当前事务范围内所有数据库操作。
- 重试机制 (@Retryable):在遇到临时性异常(如乐观锁冲突、死锁或短暂网络错误)时,自动重新执行整个事务方法。
- 一体化:将 @Retryable 注解在 @Transactional 方法之上。这样,每次重试都是一个全新的独立事务。
1. 依赖与版本
Maven
<dependencies>
<!-- Spring Boot Starter Data JPA (包含事务管理) -->
<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>
<!-- Spring AOP (Retry 功能基于 AOP 实现) -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-aop</artifactId>
</dependency>
</dependencies>
2. 启用与整体结构
@SpringBootApplication
@EnableRetry // 启用重试功能
public class YourApplication {
public static void main(String[] args) {
SpringApplication.run(YourApplication.class, args);
}
}
切面嵌套顺序:
- Retry 在外层 → 捕获异常并决定是否重试;
- Transaction 在内层 → 每次尝试都在新事务内执行。
一句话:重试的是整段事务方法。
交互时序
3. 示例领域模型
@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; // 幂等键
private Long productId;
private Integer quantity;
private BigDecimal amount;
@Version // 乐观锁
private Long version;
private String status;
}
4. 一体化服务方法
@Slf4j
@Service
@RequiredArgsConstructor
public class OrderService {
private final OrderRepository orderRepository;
private final InventoryService inventoryService;
@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("执行下单事务,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;
});
}
@Recover
public Order placeOrderFallback(Exception e, OrderRequest request) {
log.error("下单失败,reqId={},错误={}", request.getClientRequestId(), e.getMessage(), e);
throw new RuntimeException("系统繁忙,请稍后再试", e);
}
}
5. 幂等性模式
- 唯一约束:防止重复插入;
- 状态机:避免非法状态更新;
- 去重表:记录请求 ID;
- 令牌机制:外部调用防重复。
6. 注意事项
- 幂等性必须保证,避免重试导致数据重复。
- 异常分类:只重试瞬时性异常,不重试业务逻辑错误。
- 自调用问题:方法必须从外部 Bean 调用才会触发 AOP。
- 方法必须 public,否则切面无法拦截。
- @Recover 方法签名:第一个参数为异常类型,其余参数需与原方法一致。
7. 测试验证
通过并发请求触发乐观锁冲突,验证重试与幂等:
@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());
}
}
8. 总结
- @Retryable + @Transactional 一体化 → 每次重试都是新事务,失败即回滚;
- 幂等性设计是关键,防止重复操作;
- 合理的重试策略(次数、退避、异常分类)保证鲁棒性;
- 监控与日志确保可观测性。
这种模式非常适合高并发、分布式系统下处理临时性故障,确保最终一致性。
一个完整、可运行、带重试和事务回滚、失败补偿的Spring Boot 3示例,包含数据库初始化、失败订单表、库存更新、重试与恢复逻辑。
Spring Boot 3 事务回滚与重试机制完整示例
1. 主应用类
package com.example.demo;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.retry.annotation.EnableRetry;
@SpringBootApplication
@EnableRetry // 启用 Spring Retry
public class TransactionalRetryDemoApplication {
public static void main(String[] args) {
SpringApplication.run(TransactionalRetryDemoApplication.class, args);
}
}
2. DTO 类
package com.example.demo;
record OrderRequest(String productId, int quantity, double price) {}
record OrderResponse(String orderId, String status, String message) {}
3. Controller 类
package com.example.demo;
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 类(事务 + 重试 + 补偿)
package com.example.demo;
import org.springframework.dao.DataAccessException;
import org.springframework.retry.annotation.Backoff;
import org.springframework.retry.annotation.Recover;
import org.springframework.retry.annotation.Retryable;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
import javax.sql.DataSource;
import java.sql.Connection;
import java.sql.PreparedStatement;
import java.sql.SQLException;
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(OrderRequest request) throws SQLException {
System.out.println("Attempting to process order for product: " + request.productId());
// 模拟30%数据库故障
if (random.nextDouble() < 0.3) {
System.out.println("Simulating temporary database issue...");
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());
System.out.println("Order processed successfully: " + orderId);
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 in inventory: " + productId);
}
// 模拟10%库存锁冲突
if (random.nextDouble() < 0.1) {
throw new SQLException("Inventory update failed due to lock timeout");
}
}
}
@Recover
public String recover(Exception e, OrderRequest request) {
System.err.println("All retry attempts failed for order: " + request);
System.err.println("Root 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. application.properties
# H2 内存数据库配置
spring.datasource.url=jdbc:h2:mem:testdb
spring.datasource.driverClassName=org.h2.Driver
spring.datasource.username=sa
spring.datasource.password=
# 初始化数据库 schema
spring.sql.init.schema-locations=classpath:schema.sql
spring.sql.init.mode=embedded
# 显示SQL语句
spring.jpa.show-sql=true
spring.h2.console.enabled=true
6. 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);
7. 测试接口
curl -X POST http://localhost:8080/orders \
-H "Content-Type: application/json" \
-d '{"productId":"prod-001", "quantity":2, "price":29.99}'
特点总结:
- @Transactional(rollbackFor = Exception.class):事务自动回滚
- @Retryable + Backoff:自动重试并退避
- @Recover:失败订单记录,补偿处理
- 数据库初始化包含 orders、inventory、failed_orders
- 模拟临时数据库故障和库存锁冲突
- 控制台打印日志可观察重试过程