肖哥弹架构 跟大家“弹弹” Spring MVC设计与实战应用,需要代码关注
欢迎 点赞,关注,转发。
关注公号Solomon肖哥弹架构获取更多精彩内容
历史热点文章
- MyCat应用实战:分布式数据库中间件的实践与优化(篇幅一)
- 图解深度剖析:MyCat 架构设计与组件协同 (篇幅二)
- 一个项目代码讲清楚DO/PO/BO/AO/E/DTO/DAO/ POJO/VO
- 写代码总被Dis:5个项目案例带你掌握SOLID技巧,代码有架构风格
- 里氏替换原则在金融交易系统中的实践,再不懂你咬我
深入剖析分布式事务的核心机制与落地实践,重点讲解基于XA协议的强一致性解决方案。通过一个完整的电商订单案例,展示如何使用Atomikos实现跨库事务控制,内容涵盖:
- XA协议原理:两阶段提交(2PC)工作机制与故障恢复机制
- 全栈技术实现:从数据源配置、JTA集成到业务层事务控制
- 生产级优化:连接池配置、超时控制、多环境部署方案
- 实战案例:订单-库存-支付三库事务的完整代码实现
- 深度调试:XA指令日志分析与原子性保障方案
一、XA协议时序图(两阶段提交/2PC)
特点:
- 强一致性:所有参与者要么全部提交,要么全部回滚
- 阻塞协议:在准备阶段会锁定资源
- 数据库要求:需数据库支持XA协议(MySQL InnoDB、Oracle等)
二、分布式XA落地实战
1. 业务场景
电商系统中的订单创建流程需要跨多个数据库完成操作:
- 订单数据库:创建订单记录
- 库存数据库:扣减商品库存
- 支付数据库:记录支付信息
这三个操作必须作为一个原子事务执行,要么全部成功,要么全部回滚。
2. 技术栈
- Spring Boot 2.7.x
- Spring MVC
- JTA (Atomikos)
- MySQL (多实例)
- MyBatis
3. 项目技术架构图
3.1 分层架构说明
表现层 (Presentation Layer)
组件 | 技术实现 | 职责说明 |
---|---|---|
REST Controller | Spring MVC (@RestController ) | 接收HTTP请求,返回JSON响应 |
异常处理器 | @ControllerAdvice | 统一处理业务异常 |
业务逻辑层 (Service Layer)
组件 | 技术实现 | 关键特性 |
---|---|---|
订单服务 | @Service + @Transactional | 1. 协调多数据库操作 2. 声明式事务管理 |
事务管理器 | JTA (Atomikos) | 1. 两阶段提交(2PC) 2. 全局事务ID管理 |
数据访问层 (Data Access Layer)
组件 | 技术实现 | 事务特性 |
---|---|---|
OrderRepository | MyBatis (@Mapper ) | 订单表CRUD |
InventoryRepo | MyBatis + XA数据源 | 库存冻结/扣减(跨库事务) |
PaymentRepo | MyBatis + XA数据源 | 支付记录操作(跨库事务) |
资源层 (Resource Layer)
数据库 | 配置方式 | 隔离级别 |
---|---|---|
MySQL-Order | Atomikos XA DataSource | READ_COMMITTED |
MySQL-Inventory | XA连接池(最大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
关键配置说明:
-
XA数据源配置:
- 每个数据库需要配置唯一的
unique-resource-name
xa-data-source-class-name
必须使用支持XA的驱动类- URL中建议显式设置时区参数
- 每个数据库需要配置唯一的
-
连接池优化:
min-pool-size
:防止突发流量导致连接创建延迟max-pool-size
:根据数据库性能调整test-query
:连接验证SQL(MySQL推荐使用SELECT 1
)
-
事务日志:
log-dir
必须指向持久化存储目录- 生产环境建议设置为绝对路径(如
/var/transaction-logs
)
-
故障恢复:
recovery.duration
设置事务日志保留时间- 系统崩溃后Atomikos会自动扫描日志进行恢复
-
MySQL特别配置:
-- 必须确保XA支持已开启 SHOW VARIABLES LIKE 'innodb_support_xa'; -- 应为ON SET GLOBAL innodb_support_xa=1;
10.2 多环境配置建议
- 创建
application-dev.properties
(开发环境):
# 启用H2控制台(开发用)
spring.h2.console.enabled=true
spring.h2.console.path=/h2-console
# 更详细的日志
logging.level.org.springframework.transaction=TRACE
- 创建
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());
}
}