醋醋百科网

Good Luck To You!

Spring Boot 3 事务回滚 + 重试一体化最佳实践

Spring Boot 3 事务回滚 + 重试一体化最佳实践(Transactional × Retry)

一体化思想:@Retryable 在外、@Transactional 在内。每一次重试都会开启 全新的事务,失败即回滚;直到成功提交或用尽次数走 @Recover 兜底。



核心思路

  1. 事务管理 (@Transactional):保障数据库操作的原子性,遇到异常时回滚当前事务范围内所有数据库操作。
  2. 重试机制 (@Retryable):在遇到临时性异常(如乐观锁冲突、死锁或短暂网络错误)时,自动重新执行整个事务方法。
  3. 一体化:将 @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. 注意事项

  1. 幂等性必须保证,避免重试导致数据重复。
  2. 异常分类:只重试瞬时性异常,不重试业务逻辑错误。
  3. 自调用问题:方法必须从外部 Bean 调用才会触发 AOP。
  4. 方法必须 public,否则切面无法拦截。
  5. @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}'

特点总结

  1. @Transactional(rollbackFor = Exception.class):事务自动回滚
  2. @Retryable + Backoff:自动重试并退避
  3. @Recover:失败订单记录,补偿处理
  4. 数据库初始化包含 orders、inventory、failed_orders
  5. 模拟临时数据库故障和库存锁冲突
  6. 控制台打印日志可观察重试过程

控制面板
您好,欢迎到访网站!
  查看权限
网站分类
最新留言