Spring MVC+Atomikos项目实战:彻底搞定XA分布式事务,从此告别数据不一致

在这里插入图片描述

肖哥弹架构 跟大家“弹弹” Spring MVC设计与实战应用,需要代码关注

欢迎 点赞,关注,转发。

关注公号Solomon肖哥弹架构获取更多精彩内容

历史热点文章

深入剖析分布式事务的核心机制与落地实践,重点讲解基于XA协议的强一致性解决方案。通过一个完整的电商订单案例,展示如何使用Atomikos实现跨库事务控制,内容涵盖:

  1. XA协议原理:两阶段提交(2PC)工作机制与故障恢复机制
  2. 全栈技术实现:从数据源配置、JTA集成到业务层事务控制
  3. 生产级优化:连接池配置、超时控制、多环境部署方案
  4. 实战案例:订单-库存-支付三库事务的完整代码实现
  5. 深度调试:XA指令日志分析与原子性保障方案

一、XA协议时序图(两阶段提交/2PC)

在这里插入图片描述

特点:

  • 强一致性:所有参与者要么全部提交,要么全部回滚
  • 阻塞协议:在准备阶段会锁定资源
  • 数据库要求:需数据库支持XA协议(MySQL InnoDB、Oracle等)

二、分布式XA落地实战

1. 业务场景

电商系统中的订单创建流程需要跨多个数据库完成操作:

  1. 订单数据库:创建订单记录
  2. 库存数据库:扣减商品库存
  3. 支付数据库:记录支付信息

这三个操作必须作为一个原子事务执行,要么全部成功,要么全部回滚。

2. 技术栈

  • Spring Boot 2.7.x
  • Spring MVC
  • JTA (Atomikos)
  • MySQL (多实例)
  • MyBatis

3. 项目技术架构图

在这里插入图片描述

3.1 分层架构说明

表现层 (Presentation Layer)

组件技术实现职责说明
REST ControllerSpring MVC (@RestController)接收HTTP请求,返回JSON响应
异常处理器@ControllerAdvice统一处理业务异常

业务逻辑层 (Service Layer)

组件技术实现关键特性
订单服务@Service + @Transactional1. 协调多数据库操作 2. 声明式事务管理
事务管理器JTA (Atomikos)1. 两阶段提交(2PC) 2. 全局事务ID管理

数据访问层 (Data Access Layer)

组件技术实现事务特性
OrderRepositoryMyBatis (@Mapper)订单表CRUD
InventoryRepoMyBatis + XA数据源库存冻结/扣减(跨库事务)
PaymentRepoMyBatis + XA数据源支付记录操作(跨库事务)

资源层 (Resource Layer)

数据库配置方式隔离级别
MySQL-OrderAtomikos XA DataSourceREAD_COMMITTED
MySQL-InventoryXA连接池(最大20连接)REPEATABLE_READ
MySQL-Payment故障恢复日志持久化SERIALIZABLE

4. 关键组件交互流程

在这里插入图片描述

5. XA事务核心机制

5.1. 两阶段提交(2PC)实现
阶段Atomikos行为日志记录
Prepare调用各XA Resource的prepare()写入xa_tx_log
Commit发送commit命令更新日志状态为"COMMITTED"
Rollback发送rollback命令记录异常堆栈信息
5.2. 故障恢复设计

在这里插入图片描述

5.3. 性能优化措施
  • 连接池配置

    spring.jta.atomikos.connectionfactory.max-pool-size=20
    spring.jta.atomikos.datasource.max-pool-size=15
    
  • 超时控制

    @Transactional(timeout = 30) // 30秒超时
    

6. 项目结构

xa-transaction-pay/
├── src/
│   ├── main/
│   │   ├── java/
│   │   │   └── com/
│   │   │       └── example/
│   │   │           └── xapay/
│   │   │               ├── config/
│   │   │               │   ├── DataSourceConfig.java
│   │   │               │   └── JtaConfig.java
│   │   │               ├── controller/
│   │   │               │   └── OrderController.java
│   │   │               ├── model/
│   │   │               │   ├── Order.java
│   │   │               │   ├── Inventory.java
│   │   │               │   └── Payment.java
│   │   │               ├── repository/
│   │   │               │   ├── OrderRepository.java
│   │   │               │   ├── InventoryRepository.java
│   │   │               │   └── PaymentRepository.java
│   │   │               ├── service/
│   │   │               │   └── OrderService.java
│   │   │               └── XaPayApplication.java
│   │   └── resources/
│   │       ├── application.properties
│   │       ├── static/
│   │       └── templates/
├── pom.xml

7. 数据库初始化脚本

7.1 订单数据库 (order_db)
CREATE DATABASE order_db;
USE order_db;

CREATE TABLE orders (
    id BIGINT PRIMARY KEY AUTO_INCREMENT,
    order_no VARCHAR(32) NOT NULL,
    user_id BIGINT NOT NULL,
    product_id BIGINT NOT NULL,
    quantity INT NOT NULL,
    amount DECIMAL(10,2) NOT NULL,
    status INT NOT NULL COMMENT '0-待支付,1-已支付,2-已取消',
    create_time DATETIME NOT NULL,
    UNIQUE KEY (order_no)
);
7.2 库存数据库 (inventory_db)
CREATE DATABASE inventory_db;
USE inventory_db;

CREATE TABLE inventory (
    id BIGINT PRIMARY KEY AUTO_INCREMENT,
    product_id BIGINT NOT NULL,
    stock INT NOT NULL COMMENT '可用库存',
    frozen INT NOT NULL DEFAULT 0 COMMENT '冻结库存',
    UNIQUE KEY (product_id)
);

-- 初始化测试数据
INSERT INTO inventory (product_id, stock) VALUES (1, 100);
7.3 支付数据库 (payment_db)
CREATE DATABASE payment_db;
USE payment_db;

CREATE TABLE payments (
    id BIGINT PRIMARY KEY AUTO_INCREMENT,
    payment_no VARCHAR(32) NOT NULL,
    order_id BIGINT NOT NULL,
    amount DECIMAL(10,2) NOT NULL,
    status INT NOT NULL COMMENT '0-未支付,1-已支付,2-已退款',
    create_time DATETIME NOT NULL,
    UNIQUE KEY (payment_no)
);

8. 核心代码实现

项目执行时序图:

在这里插入图片描述

8.1 依赖配置 (pom.xml)
<dependencies>
    <!-- Spring Boot Starter -->
    <dependency>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-web</artifactId>
    </dependency>
    <dependency>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-jta-atomikos</artifactId>
    </dependency>
    
    <!-- Database -->
    <dependency>
        <groupId>mysql</groupId>
        <artifactId>mysql-connector-java</artifactId>
    </dependency>
    <dependency>
        <groupId>org.mybatis.spring.boot</groupId>
        <artifactId>mybatis-spring-boot-starter</artifactId>
        <version>2.2.0</version>
    </dependency>
    
    <!-- Other -->
    <dependency>
        <groupId>org.projectlombok</groupId>
        <artifactId>lombok</artifactId>
        <optional>true</optional>
    </dependency>
</dependencies>
8.2 数据源配置 (DataSourceConfig.java)
@Configuration
public class DataSourceConfig {
    
    @Bean(name = "orderDataSource")
    @ConfigurationProperties(prefix = "spring.jta.atomikos.datasource.order")
    public DataSource orderDataSource() {
        return new AtomikosDataSourceBean();
    }
    
    @Bean(name = "inventoryDataSource")
    @ConfigurationProperties(prefix = "spring.jta.atomikos.datasource.inventory")
    public DataSource inventoryDataSource() {
        return new AtomikosDataSourceBean();
    }
    
    @Bean(name = "paymentDataSource")
    @ConfigurationProperties(prefix = "spring.jta.atomikos.datasource.payment")
    public DataSource paymentDataSource() {
        return new AtomikosDataSourceBean();
    }
}
8.3 JTA配置 (JtaConfig.java)
@Configuration
@EnableTransactionManagement
public class JtaConfig {
    
    @Bean
    public JtaTransactionManager transactionManager() {
        UserTransactionManager userTransactionManager = new UserTransactionManager();
        UserTransaction userTransaction = new UserTransactionImp();
        return new JtaTransactionManager(userTransaction, userTransactionManager);
    }
}
8.4 应用配置 (application.properties)
# Order DB
spring.jta.atomikos.datasource.order.unique-resource-name=orderDB
spring.jta.atomikos.datasource.order.xa-data-source-class-name=com.mysql.cj.jdbc.MysqlXADataSource
spring.jta.atomikos.datasource.order.xa-properties.url=jdbc:mysql://localhost:3306/order_db
spring.jta.atomikos.datasource.order.xa-properties.user=root
spring.jta.atomikos.datasource.order.xa-properties.password=123456

# Inventory DB
spring.jta.atomikos.datasource.inventory.unique-resource-name=inventoryDB
spring.jta.atomikos.datasource.inventory.xa-data-source-class-name=com.mysql.cj.jdbc.MysqlXADataSource
spring.jta.atomikos.datasource.inventory.xa-properties.url=jdbc:mysql://localhost:3307/inventory_db
spring.jta.atomikos.datasource.inventory.xa-properties.user=root
spring.jta.atomikos.datasource.inventory.xa-properties.password=123456

# Payment DB
spring.jta.atomikos.datasource.payment.unique-resource-name=paymentDB
spring.jta.atomikos.datasource.payment.xa-data-source-class-name=com.mysql.cj.jdbc.MysqlXADataSource
spring.jta.atomikos.datasource.payment.xa-properties.url=jdbc:mysql://localhost:3308/payment_db
spring.jta.atomikos.datasource.payment.xa-properties.user=root
spring.jta.atomikos.datasource.payment.xa-properties.password=123456

# JTA
spring.jta.log-dir=target/transaction-logs
spring.jta.transaction-manager-id=tm1
8.5 实体类
@Data
public class Order {
    private Long id;
    private String orderNo;
    private Long userId;
    private Long productId;
    private Integer quantity;
    private BigDecimal amount;
    private Integer status;
    private Date createTime;
}

@Data
public class Inventory {
    private Long id;
    private Long productId;
    private Integer stock;
    private Integer frozen;
}

@Data
public class Payment {
    private Long id;
    private String paymentNo;
    private Long orderId;
    private BigDecimal amount;
    private Integer status;
    private Date createTime;
}
8.6 Repository 接口
@Repository
@Mapper
public interface OrderRepository {
    @Insert("INSERT INTO orders(order_no, user_id, product_id, quantity, amount, status, create_time) " +
            "VALUES(#{orderNo}, #{userId}, #{productId}, #{quantity}, #{amount}, #{status}, NOW())")
    @Options(useGeneratedKeys = true, keyProperty = "id")
    int insert(Order order);
}

@Repository
@Mapper
public interface InventoryRepository {
    @Update("UPDATE inventory SET stock = stock - #{quantity}, frozen = frozen + #{quantity} " +
            "WHERE product_id = #{productId} AND stock >= #{quantity}")
    int freezeStock(@Param("productId") Long productId, @Param("quantity") Integer quantity);
    
    @Update("UPDATE inventory SET frozen = frozen - #{quantity} " +
            "WHERE product_id = #{productId} AND frozen >= #{quantity}")
    int releaseStock(@Param("productId") Long productId, @Param("quantity") Integer quantity);
    
    @Update("UPDATE inventory SET frozen = frozen - #{quantity} " +
            "WHERE product_id = #{productId} AND frozen >= #{quantity}")
    int deductStock(@Param("productId") Long productId, @Param("quantity") Integer quantity);
}

@Repository
@Mapper
public interface PaymentRepository {
    @Insert("INSERT INTO payments(payment_no, order_id, amount, status, create_time) " +
            "VALUES(#{paymentNo}, #{orderId}, #{amount}, #{status}, NOW())")
    @Options(useGeneratedKeys = true, keyProperty = "id")
    int insert(Payment payment);
}
8.7 业务服务 (OrderService.java)
@Service
@RequiredArgsConstructor
public class OrderService {
    
    private final OrderRepository orderRepository;
    private final InventoryRepository inventoryRepository;
    private final PaymentRepository paymentRepository;

    // 触发Atomikos事务管理
    @Transactional(rollbackFor = Exception.class)
    public String createOrder(Order order) {
        // 1. 生成订单号
        String orderNo = "O" + System.currentTimeMillis();
        order.setOrderNo(orderNo);
        order.setStatus(0); // 0-待支付
        
        // 2. 冻结库存
        int affected = inventoryRepository.freezeStock(order.getProductId(), order.getQuantity());
        if (affected == 0) {
            throw new RuntimeException("库存不足");
        }
        // 调用会经过XA事务增强的Repository
        // 3. 创建订单
        orderRepository.insert(order);
        
        // 4. 创建支付记录
        Payment payment = new Payment();
        payment.setPaymentNo("P" + System.currentTimeMillis());
        payment.setOrderId(order.getId());
        payment.setAmount(order.getAmount());
        payment.setStatus(0); // 0-未支付
        paymentRepository.insert(payment);
        
        return orderNo;
    }
}
8.8 控制器 (OrderController.java)
@RestController
@RequestMapping("/orders")
@RequiredArgsConstructor
public class OrderController {
    
    private final OrderService orderService;
    
    @PostMapping
    public ResponseEntity<?> createOrder(@RequestBody Order order) {
        try {
            String orderNo = orderService.createOrder(order);
            return ResponseEntity.ok(Map.of("orderNo", orderNo));
        } catch (Exception e) {
            return ResponseEntity.badRequest().body(Map.of(
                "error", e.getMessage(),
                "timestamp", System.currentTimeMillis()
            ));
        }
    }
}

9. Atomikos调用原理

调用链全景图

在这里插入图片描述

1. 关键组件交互细节

在这里插入图片描述

1.1 Atomikos 数据源配置
// DataSourceConfig.java
@Bean(name = "orderDataSource")
@ConfigurationProperties(prefix = "spring.jta.atomikos.datasource.order")
public DataSource orderDataSource() {
    // 关键点:创建Atomikos管理的XA数据源
    return new AtomikosDataSourceBean(); 
}
1.2 Service层调用(事务入口)
@Service
public class OrderService {
    @Autowired
    private OrderRepository orderRepo; // MyBatis Mapper接口

    @Transactional // 触发Atomikos事务管理
    public void createOrder(Order order) {
        // 调用会经过XA事务增强的Repository
        orderRepo.insert(order); 
    }
}
1.3 MyBatis → Atomikos 的衔接
flowchart TB
    A[orderRepo.insert()] --> B[MyBatis代理]
    B --> C[SqlSessionTemplate]
    C --> D[DataSourceUtils.getConnection]
    D --> E[AtomikosDataSource.getXAConnection]
    E --> F[XAResource.xa_start]
    F --> G[MySQL注册XA事务]
1.4 Atomikos 核心工作流程
// Atomikos内部处理
public class AtomikosDataSourceBean {
    public Connection getConnection() {
        // 1. 从连接池获取XA连接
        XAConnection xaConn = pool.borrowObject();
        
        // 2. 获取XAResource
        XAResource xaRes = xaConn.getXAResource();
        
        // 3. 关联当前事务
        TransactionManager tm = getTransactionManager();
        tm.enlistResource(xaRes); // 将XAResource注册到全局事务
        
        // 4. 返回经过代理的Connection
        return new AtomikosConnectionProxy(xaConn);
    }
}
2. XA 指令实际执行日志

启用DEBUG日志可看到底层XA指令:

logging.level.com.atomikos=DEBUG
logging.level.org.springframework.transaction=TRACE

典型日志输出

1. [Atomikos] Starting XA transaction with ID: 123
2. [MySQL-XA] XA START '123'
3. [MyBatis] Executing: INSERT INTO orders...
4. [MySQL-XA] XA END '123'
5. [Atomikos] Preparing XA resource 'orderDB' with ID 123
6. [MySQL-XA] XA PREPARE '123'
7. [Atomikos] Committing XA resource 'orderDB' with ID 123
8. [MySQL-XA] XA COMMIT '123'

10. 项目配置

10.1 application.properties 配置文件
# ========================
# 应用基础配置
# ========================

# 应用端口
server.port=8080

# 应用上下文路径
server.servlet.context-path=/xa-pay

# 激活Spring Boot的调试日志(可选)
debug=true

# ========================
# JTA Atomikos 全局配置
# ========================

# 事务日志存储目录(必须持久化存储)
spring.jta.log-dir=./transaction-logs

# 事务管理器唯一ID(集群环境需区分)
spring.jta.transaction-manager-id=tm-${random.uuid}

# 事务超时时间(秒)
spring.jta.atomikos.properties.max-timeout=300

# 是否允许嵌套事务
spring.jta.atomikos.properties.allow-sub-transactions=true

# 事务日志记录级别
spring.jta.atomikos.properties.log-base-name=transaction

# ========================
# 订单数据库(主库)配置
# ========================

# 唯一资源名称(必须全局唯一)
spring.jta.atomikos.datasource.order.unique-resource-name=orderDB

# MySQL XA驱动类
spring.jta.atomikos.datasource.order.xa-data-source-class-name=com.mysql.cj.jdbc.MysqlXADataSource

# JDBC连接URL(注意时区设置)
spring.jta.atomikos.datasource.order.xa-properties.url=jdbc:mysql://localhost:3306/order_db?useSSL=false&serverTimezone=Asia/Shanghai&characterEncoding=utf8

# 数据库用户名
spring.jta.atomikos.datasource.order.xa-properties.user=root

# 数据库密码
spring.jta.atomikos.datasource.order.xa-properties.password=123456

# 连接池最小大小
spring.jta.atomikos.datasource.order.min-pool-size=5

# 连接池最大大小
spring.jta.atomikos.datasource.order.max-pool-size=20

# 连接 borrow 超时时间(毫秒)
spring.jta.atomikos.datasource.order.borrow-connection-timeout=10000

# ========================
# 库存数据库配置
# ========================

spring.jta.atomikos.datasource.inventory.unique-resource-name=inventoryDB
spring.jta.atomikos.datasource.inventory.xa-data-source-class-name=com.mysql.cj.jdbc.MysqlXADataSource
spring.jta.atomikos.datasource.inventory.xa-properties.url=jdbc:mysql://localhost:3307/inventory_db?useSSL=false&serverTimezone=Asia/Shanghai
spring.jta.atomikos.datasource.inventory.xa-properties.user=root
spring.jta.atomikos.datasource.inventory.xa-properties.password=123456
spring.jta.atomikos.datasource.inventory.min-pool-size=5
spring.jta.atomikos.datasource.inventory.max-pool-size=15
spring.jta.atomikos.datasource.inventory.test-query=SELECT 1

# ========================
# 支付数据库配置
# ========================

spring.jta.atomikos.datasource.payment.unique-resource-name=paymentDB
spring.jta.atomikos.datasource.payment.xa-data-source-class-name=com.mysql.cj.jdbc.MysqlXADataSource
spring.jta.atomikos.datasource.payment.xa-properties.url=jdbc:mysql://localhost:3308/payment_db?useSSL=false&serverTimezone=UTC
spring.jta.atomikos.datasource.payment.xa-properties.user=root
spring.jta.atomikos.datasource.payment.xa-properties.password=123456
spring.jta.atomikos.datasource.payment.min-pool-size=3
spring.jta.atomikos.datasource.payment.max-pool-size=10
spring.jta.atomikos.datasource.payment.reap-timeout=30000

# ========================
# MyBatis 配置
# ========================

# 下划线转驼峰
mybatis.configuration.map-underscore-to-camel-case=true

# Mapper文件位置
mybatis.mapper-locations=classpath:mapper/*.xml

# 打印SQL日志(开发环境开启)
logging.level.com.example.xapay.repository=DEBUG

# ========================
# 事务高级配置
# ========================

# 默认事务超时(秒)
spring.transaction.default-timeout=30

# 是否允许自定义隔离级别
spring.jta.allow-custom-isolation-levels=true

# 恢复日志保留天数
spring.jta.atomikos.properties.recovery.duration=7

# ========================
# 监控配置(可选)
# ========================

# 启用Actuator端点
management.endpoints.web.exposure.include=health,info,transactions

# 事务监控端点
management.endpoint.transactions.enabled=true

关键配置说明:

  1. XA数据源配置

    • 每个数据库需要配置唯一的unique-resource-name
    • xa-data-source-class-name必须使用支持XA的驱动类
    • URL中建议显式设置时区参数
  2. 连接池优化

    • min-pool-size:防止突发流量导致连接创建延迟
    • max-pool-size:根据数据库性能调整
    • test-query:连接验证SQL(MySQL推荐使用SELECT 1
  3. 事务日志

    • log-dir必须指向持久化存储目录
    • 生产环境建议设置为绝对路径(如/var/transaction-logs
  4. 故障恢复

    • recovery.duration设置事务日志保留时间
    • 系统崩溃后Atomikos会自动扫描日志进行恢复
  5. MySQL特别配置

    -- 必须确保XA支持已开启
    SHOW VARIABLES LIKE 'innodb_support_xa'; -- 应为ON
    SET GLOBAL innodb_support_xa=1;
    
10.2 多环境配置建议
  1. 创建application-dev.properties(开发环境):
# 启用H2控制台(开发用)
spring.h2.console.enabled=true
spring.h2.console.path=/h2-console

# 更详细的日志
logging.level.org.springframework.transaction=TRACE
  1. 创建application-prod.properties(生产环境):
# 生产环境关闭调试日志
debug=false

# 更大的连接池
spring.jta.atomikos.datasource.order.max-pool-size=50
spring.jta.atomikos.datasource.inventory.max-pool-size=30

# 事务日志存到专用目录
spring.jta.log-dir=/data/transaction-logs

11. 测试用例

@SpringBootTest
class OrderServiceTest {
    
    @Autowired
    private OrderService orderService;
    
    @Autowired
    private OrderRepository orderRepository;
    
    @Autowired
    private InventoryRepository inventoryRepository;
    
    @Autowired
    private PaymentRepository paymentRepository;
    
    @Test
    void testCreateOrderSuccess() {
        Order order = new Order();
        order.setUserId(1L);
        order.setProductId(1L);
        order.setQuantity(2);
        order.setAmount(new BigDecimal("200.00"));
        
        String orderNo = orderService.createOrder(order);
        assertNotNull(orderNo);
        
        // 验证订单
        Order createdOrder = orderRepository.findByOrderNo(orderNo);
        assertNotNull(createdOrder);
        
        // 验证库存
        Inventory inventory = inventoryRepository.findByProductId(1L);
        assertEquals(98, inventory.getStock()); // 100 - 2
        assertEquals(0, inventory.getFrozen());
        
        // 验证支付记录
        Payment payment = paymentRepository.findByOrderId(createdOrder.getId());
        assertNotNull(payment);
    }
    
    @Test
    void testCreateOrderFail() {
        Order order = new Order();
        order.setUserId(1L);
        order.setProductId(1L);
        order.setQuantity(200); // 超过库存
        order.setAmount(new BigDecimal("20000.00"));
        
        assertThrows(RuntimeException.class, () -> {
            orderService.createOrder(order);
        });
        
        // 验证库存未扣减
        Inventory inventory = inventoryRepository.findByProductId(1L);
        assertEquals(100, inventory.getStock());
        assertEquals(0, inventory.getFrozen());
        
        // 验证没有订单记录
        assertEquals(0, orderRepository.count());
        
        // 验证没有支付记录
        assertEquals(0, paymentRepository.count());
    }
}
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

Solomon_肖哥弹架构

你的欣赏就是我最大的动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值