分布式事务详解
分布式事务概述
什么是分布式事务
分布式事务是指事务的参与者、支持事务的服务器、资源服务器以及事务管理器分别位于不同的分布式系统的不同节点之上。在微服务架构中,一个业务操作往往需要跨多个服务、多个数据库才能完成,传统的本地事务(ACID)无法直接保证跨服务的数据一致性。
典型场景
- 电商下单:订单服务创建订单 + 库存服务扣减库存 + 账户服务扣减余额
- 转账:A账户扣款 + B账户入账(可能在不同数据库)
- 分布式任务:多个微服务协同完成一个业务流程
分布式事务的核心挑战
| 挑战 | 说明 |
|---|---|
| 网络不可靠 | 节点间通信可能超时、丢包、乱序 |
| 节点故障 | 参与者可能在任意时刻宕机 |
| 数据一致性 | 多节点数据需要保持逻辑一致 |
| 性能开销 | 协调协议引入额外的网络往返 |
ACID vs BASE
ACID(强一致性)
ACID是传统关系型数据库事务的四大特性:
- Atomicity(原子性):事务中的操作要么全部成功,要么全部回滚
- Consistency(一致性):事务执行前后,数据从一个一致状态到另一个一致状态
- Isolation(隔离性):并发事务之间互不干扰
- Durability(持久性):事务提交后,数据永久保存
java
// 本地ACID事务示例
@Service
public class OrderService {
@Autowired
private OrderRepository orderRepository;
@Autowired
private AccountRepository accountRepository;
@Transactional(rollbackFor = Exception.class)
public void createOrder(OrderDTO dto) {
// 同一数据库内,ACID由数据库保证
orderRepository.save(new Order(dto));
accountRepository.deduct(dto.getUserId(), dto.getAmount());
}
}
BASE(最终一致性)
BASE是对CAP中一致性和可用性权衡的结果:
- Basically Available(基本可用):系统出现故障时,允许损失部分可用性
- Soft State(软状态):允许系统中的数据存在中间状态
- Eventually Consistent(最终一致性):经过一段时间后,数据最终达到一致
ACID vs BASE 对比
| 维度 | ACID | BASE |
|---|---|---|
| 一致性 | 强一致性 | 最终一致性 |
| 可用性 | 较低 | 较高 |
| 性能 | 较低(锁开销) | 较高 |
| 适用场景 | 金融核心交易 | 互联网高并发业务 |
| 实现复杂度 | 低(数据库原生) | 高(需补偿机制) |
2PC(两阶段提交)
基本原理
两阶段提交(Two-Phase Commit)是最经典的分布式事务协议,由一个协调者(Coordinator)和多个参与者(Participant)组成。
执行流程
阶段一:准备阶段(Prepare/Vote)
- 协调者向所有参与者发送 Prepare 请求
- 参与者执行本地事务但不提交,将 undo/redo 信息写入日志
- 参与者回复 Yes(准备成功)或 No(准备失败)
阶段二:提交阶段(Commit/Abort)
- 若所有参与者都回复 Yes:协调者发送 Commit 请求,参与者提交本地事务
- 若任一参与者回复 No 或超时:协调者发送 Abort 请求,参与者回滚本地事务
Java模拟实现
java
// 参与者接口
public interface TwoPhaseParticipant {
boolean prepare(String transactionId);
void commit(String transactionId);
void rollback(String transactionId);
}
// 协调者实现
public class TwoPhaseCoordinator {
private final List<TwoPhaseParticipant> participants;
public TwoPhaseCoordinator(List<TwoPhaseParticipant> participants) {
this.participants = participants;
}
public boolean execute(String transactionId) {
// 阶段一:准备
boolean allPrepared = true;
for (TwoPhaseParticipant participant : participants) {
try {
if (!participant.prepare(transactionId)) {
allPrepared = false;
break;
}
} catch (Exception e) {
allPrepared = false;
break;
}
}
// 阶段二:提交或回滚
if (allPrepared) {
for (TwoPhaseParticipant participant : participants) {
participant.commit(transactionId);
}
return true;
} else {
for (TwoPhaseParticipant participant : participants) {
participant.rollback(transactionId);
}
return false;
}
}
}
2PC的缺陷
| 问题 | 说明 |
|---|---|
| 同步阻塞 | 参与者在准备后阻塞等待协调者指令,资源被锁定 |
| 单点故障 | 协调者宕机后,参与者将无限期阻塞 |
| 数据不一致 | 提交阶段部分参与者收到Commit、部分未收到 |
| 网络分区 | 协调者与部分参与者失联时无法做出全局决策 |
3PC(三阶段提交)
基本原理
三阶段提交(Three-Phase Commit)在2PC基础上增加了 CanCommit 阶段,并引入超时机制,降低了阻塞范围。
执行流程
阶段一:CanCommit
- 协调者向参与者发送 CanCommit 请求
- 参与者检查自身资源是否可用,回复 Yes/No(不锁定资源)
阶段二:PreCommit
- 若全部回复 Yes,协调者发送 PreCommit 请求
- 参与者执行事务但不提交,记录 undo/redo 日志
- 参与者回复 ACK
阶段三:DoCommit
- 协调者收到所有 ACK 后,发送 DoCommit 请求
- 参与者正式提交事务
3PC vs 2PC
| 维度 | 2PC | 3PC |
|---|---|---|
| 阶段数 | 2 | 3 |
| 阻塞范围 | 准备后一直阻塞 | 仅PreCommit后短暂阻塞 |
| 超时机制 | 无 | 有(参与者超时自动提交) |
| 网络分区容忍 | 差 | 稍好(仍有问题) |
| 实际应用 | 数据库XA协议 | 很少实际使用 |
3PC的局限
3PC虽然降低了阻塞,但在网络分区场景下仍可能导致数据不一致。参与者超时后自动提交的策略,在协调者实际发送了Abort的情况下会产生不一致。因此3PC在实际生产中很少使用。
TCC(Try/Confirm/Cancel)
基本原理
TCC是一种业务层面的两阶段补偿机制,将每个业务操作拆分为三个步骤:
- Try(尝试):预留业务资源(如冻结库存、冻结金额)
- Confirm(确认):确认执行业务(如扣减冻结的库存)
- Cancel(取消):释放预留资源(如解冻库存)
执行流程
- 全局事务管理器调用所有参与者的 Try 方法
- 若所有 Try 成功,调用所有参与者的 Confirm 方法
- 若任一 Try 失败,调用所有已Try成功参与者的 Cancel 方法
Java实现示例
java
// TCC接口定义
public interface InventoryTccService {
@TwoPhaseBusinessAction(name = "deductStock",
commitMethod = "confirm",
rollbackMethod = "cancel")
boolean tryDeduct(BusinessActionContext context,
@BusinessActionContextParameter(paramName = "skuId") String skuId,
@BusinessActionContextParameter(paramName = "quantity") int quantity);
boolean confirm(BusinessActionContext context);
boolean cancel(BusinessActionContext context);
}
// TCC实现
@Service
public class InventoryTccServiceImpl implements InventoryTccService {
@Autowired
private InventoryMapper inventoryMapper;
@Override
@Transactional
public boolean tryDeduct(BusinessActionContext context, String skuId, int quantity) {
// Try:冻结库存(available_stock减少,frozen_stock增加)
int rows = inventoryMapper.freezeStock(skuId, quantity);
if (rows == 0) {
throw new BusinessException("库存不足");
}
return true;
}
@Override
@Transactional
public boolean confirm(BusinessActionContext context) {
// Confirm:扣减冻结库存
String skuId = (String) context.getActionContext("skuId");
int quantity = (int) context.getActionContext("quantity");
inventoryMapper.deductFrozenStock(skuId, quantity);
return true;
}
@Override
@Transactional
public boolean cancel(BusinessActionContext context) {
// Cancel:解冻库存
String skuId = (String) context.getActionContext("skuId");
int quantity = (int) context.getActionContext("quantity");
inventoryMapper.unfreezeStock(skuId, quantity);
return true;
}
}
TCC注意事项
| 问题 | 解决方案 |
|---|---|
| 空回滚 | Cancel中检查是否有Try记录,无则直接返回成功 |
| 悬挂 | Try中检查是否已执行过Cancel,是则拒绝执行 |
| 幂等性 | Confirm/Cancel需保证幂等,通过事务状态表判断 |
java
// 幂等性控制示例
@Override
@Transactional
public boolean cancel(BusinessActionContext context) {
String xid = context.getXid();
// 检查事务状态表,防止空回滚和重复回滚
TxRecord record = txRecordMapper.selectByXid(xid);
if (record == null) {
// 空回滚:Try未执行,插入标记防止悬挂
txRecordMapper.insert(new TxRecord(xid, "CANCELLED"));
return true;
}
if ("CANCELLED".equals(record.getStatus())) {
return true; // 幂等:已回滚
}
// 执行实际回滚逻辑
inventoryMapper.unfreezeStock(record.getSkuId(), record.getQuantity());
txRecordMapper.updateStatus(xid, "CANCELLED");
return true;
}
Saga模式(编排式/协调式)
基本原理
Saga将一个长事务拆分为多个本地事务,每个本地事务都有对应的补偿操作。当某个本地事务失败时,依次执行之前已完成事务的补偿操作。
编排式Saga(Choreography)
各服务通过事件驱动的方式自行协调,没有中心协调者。
订单服务 --创建订单事件--> 库存服务 --扣减库存事件--> 支付服务
^ | |
| v v
+--------订单失败事件-----+--------支付失败事件------+
java
// 编排式Saga - 订单服务监听事件
@Component
public class OrderEventListener {
@Autowired
private OrderService orderService;
@KafkaListener(topics = "payment-failed")
public void onPaymentFailed(PaymentFailedEvent event) {
// 补偿:取消订单
orderService.cancelOrder(event.getOrderId());
}
@KafkaListener(topics = "stock-deduct-failed")
public void onStockDeductFailed(StockDeductFailedEvent event) {
// 补偿:取消订单
orderService.cancelOrder(event.getOrderId());
}
}
协调式Saga(Orchestration)
由一个中心协调者(Saga Orchestrator)统一调度各步骤。
java
// 协调式Saga - 编排器
@Component
public class OrderSagaOrchestrator {
@Autowired
private OrderService orderService;
@Autowired
private InventoryService inventoryService;
@Autowired
private PaymentService paymentService;
public void executeOrderSaga(OrderRequest request) {
SagaDefinition<OrderRequest> saga = Saga.<OrderRequest>builder()
.step(ctx -> orderService.createOrder(ctx.getRequest()))
.compensateWith(ctx -> orderService.cancelOrder(ctx.getOrderId()))
.step(ctx -> inventoryService.deductStock(ctx.getSkuId(), ctx.getQuantity()))
.compensateWith(ctx -> inventoryService.restoreStock(ctx.getSkuId(), ctx.getQuantity()))
.step(ctx -> paymentService.charge(ctx.getUserId(), ctx.getAmount()))
.compensateWith(ctx -> paymentService.refund(ctx.getUserId(), ctx.getAmount()))
.build();
saga.execute(request);
}
}
编排式 vs 协调式
| 维度 | 编排式 | 协调式 |
|---|---|---|
| 耦合度 | 低(事件驱动) | 较高(中心协调) |
| 复杂度 | 流程分散,难追踪 | 集中管理,易理解 |
| 适用场景 | 步骤少(2-3步) | 步骤多、流程复杂 |
| 监控 | 困难 | 容易 |
| 单点风险 | 无 | 协调者可能成为瓶颈 |
本地消息表
基本原理
本地消息表是一种基于本地事务 + 消息轮询的最终一致性方案。核心思想是将消息发送与业务操作放在同一个本地事务中。
执行流程
- 业务操作与消息写入在同一个本地事务中完成
- 后台定时任务轮询消息表,将未发送的消息投递到MQ
- 下游服务消费消息,处理完成后回调确认
- 确认后更新消息状态为已完成
数据库表设计
sql
-- 本地消息表
CREATE TABLE local_message (
id BIGINT PRIMARY KEY AUTO_INCREMENT,
biz_id VARCHAR(64) NOT NULL COMMENT '业务ID',
message_body TEXT NOT NULL COMMENT '消息内容(JSON)',
topic VARCHAR(128) NOT NULL COMMENT '目标Topic',
status TINYINT NOT NULL DEFAULT 0 COMMENT '0-待发送 1-已发送 2-已确认 3-失败',
retry_count INT NOT NULL DEFAULT 0 COMMENT '重试次数',
max_retry INT NOT NULL DEFAULT 5 COMMENT '最大重试次数',
create_time DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
update_time DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
INDEX idx_status_create (status, create_time)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
Java实现
java
@Service
public class OrderServiceWithLocalMessage {
@Autowired
private OrderMapper orderMapper;
@Autowired
private LocalMessageMapper messageMapper;
@Autowired
private RocketMQTemplate rocketMQTemplate;
@Transactional(rollbackFor = Exception.class)
public void createOrder(OrderDTO dto) {
// 1. 业务操作
Order order = new Order(dto);
orderMapper.insert(order);
// 2. 写入本地消息表(同一事务)
LocalMessage message = new LocalMessage();
message.setBizId(order.getId());
message.setTopic("stock-deduct-topic");
message.setMessageBody(JSON.toJSONString(
new StockDeductMsg(order.getSkuId(), order.getQuantity())));
message.setStatus(0);
messageMapper.insert(message);
}
}
// 定时任务:轮询发送消息
@Component
public class MessagePollingTask {
@Autowired
private LocalMessageMapper messageMapper;
@Autowired
private RocketMQTemplate rocketMQTemplate;
@Scheduled(fixedDelay = 5000)
public void pollAndSend() {
List<LocalMessage> messages = messageMapper.selectPending(100);
for (LocalMessage msg : messages) {
try {
rocketMQTemplate.syncSend(msg.getTopic(), msg.getMessageBody());
messageMapper.updateStatus(msg.getId(), 1);
} catch (Exception e) {
if (msg.getRetryCount() >= msg.getMaxRetry()) {
messageMapper.updateStatus(msg.getId(), 3);
// 告警通知
} else {
messageMapper.incrementRetry(msg.getId());
}
}
}
}
}
最大努力通知
基本原理
最大努力通知是最简单的一致性方案,适用于对一致性要求不高的跨系统场景(如支付结果通知)。发起方尽最大努力通知接收方,但不保证一定成功。
特点
- 发起方在完成本地事务后,通过MQ或HTTP回调通知接收方
- 通知失败时按策略重试(如1min、5min、30min、2h)
- 超过最大重试次数后放弃,接收方可主动查询
- 不保证强一致性,只保证最终一致性
Java实现
java
@Service
public class PaymentNotifyService {
@Autowired
private RestTemplate restTemplate;
@Autowired
private NotifyRecordMapper notifyRecordMapper;
// 重试间隔策略(秒)
private static final int[] RETRY_INTERVALS = {60, 300, 1800, 7200, 14400};
@Async
public void notifyMerchant(String merchantUrl, PaymentResult result) {
NotifyRecord record = new NotifyRecord(merchantUrl, JSON.toJSONString(result));
notifyRecordMapper.insert(record);
for (int i = 0; i < RETRY_INTERVALS.length; i++) {
try {
ResponseEntity<String> response = restTemplate.postForEntity(
merchantUrl, result, String.class);
if ("SUCCESS".equals(response.getBody())) {
notifyRecordMapper.markSuccess(record.getId());
return;
}
} catch (Exception e) {
// 记录失败日志
}
try {
Thread.sleep(RETRY_INTERVALS[i] * 1000L);
} catch (InterruptedException ex) {
Thread.currentThread().interrupt();
break;
}
}
// 超过最大重试次数,标记为失败,等待商户主动查询
notifyRecordMapper.markFailed(record.getId());
}
}
Seata框架(AT/TCC/Saga/XA模式)
Seata概述
Seata(Simple Extensible Autonomous Transaction Architecture)是阿里巴巴开源的分布式事务解决方案,提供高性能、易接入的分布式事务服务。
核心架构
| 角色 | 说明 |
|---|---|
| TC(Transaction Coordinator) | 事务协调器,维护全局事务状态,驱动提交或回滚 |
| TM(Transaction Manager) | 事务管理器,定义全局事务范围,开启/提交/回滚全局事务 |
| RM(Resource Manager) | 资源管理器,管理分支事务资源,向TC汇报状态 |
四种模式对比
| 模式 | 一致性 | 性能 | 侵入性 | 适用场景 |
|---|---|---|---|---|
| AT | 最终一致 | 高 | 无侵入 | 大多数OLTP场景 |
| TCC | 最终一致 | 高 | 强侵入 | 高性能、自定义补偿 |
| Saga | 最终一致 | 高 | 中等 | 长事务、流程编排 |
| XA | 强一致 | 低 | 无侵入 | 对一致性要求极高 |
Seata AT模式原理(全局锁/undo_log)
AT模式工作流程
一阶段(业务执行):
- 解析业务SQL,获取 before image(修改前数据快照)
- 执行业务SQL
- 获取 after image(修改后数据快照)
- 将 before/after image 插入 undo_log 表
- 向TC注册分支事务,获取全局锁
- 本地事务提交(业务数据 + undo_log 在同一本地事务)
二阶段-提交:
- TC通知RM提交
- 异步删除 undo_log(因为一阶段已提交,无需额外操作)
二阶段-回滚:
- TC通知RM回滚
- 通过 undo_log 中的 before image 生成反向SQL
- 校验 after image 是否被其他事务修改(脏写检查)
- 执行反向SQL恢复数据
- 删除 undo_log
undo_log表结构
sql
CREATE TABLE undo_log (
branch_id BIGINT NOT NULL COMMENT '分支事务ID',
xid VARCHAR(128) NOT NULL COMMENT '全局事务ID',
context VARCHAR(128) NOT NULL COMMENT '上下文(序列化方式等)',
rollback_info LONGBLOB NOT NULL COMMENT '回滚信息(before/after image)',
log_status INT NOT NULL COMMENT '0-正常 1-全局已完成',
log_created DATETIME(6) NOT NULL COMMENT '创建时间',
log_modified DATETIME(6) NOT NULL COMMENT '修改时间',
UNIQUE KEY ux_undo_log (xid, branch_id)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
全局锁机制
AT模式通过全局锁实现写隔离:
- 一阶段本地事务提交前,必须先获取全局锁
- 全局锁由TC维护(存储在
lock_table中) - 若获取全局锁失败,本地事务回滚,释放本地锁
- 二阶段回滚时,通过全局锁保证不会回滚已被其他全局事务修改的数据
sql
-- Seata Server端锁表
CREATE TABLE lock_table (
row_key VARCHAR(128) NOT NULL COMMENT '表名:主键值',
xid VARCHAR(128) COMMENT '全局事务ID',
transaction_id BIGINT COMMENT '事务ID',
branch_id BIGINT NOT NULL COMMENT '分支事务ID',
resource_id VARCHAR(256) COMMENT '数据源标识',
table_name VARCHAR(32) COMMENT '表名',
pk VARCHAR(36) COMMENT '主键值',
status TINYINT NOT NULL DEFAULT 0 COMMENT '0-锁定 1-已释放',
gmt_create DATETIME,
gmt_modified DATETIME,
PRIMARY KEY (row_key),
KEY idx_branch_id (branch_id),
KEY idx_xid (xid)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
AT模式的局限
- 仅支持关系型数据库(需要SQL解析)
- 不支持NoSQL(如Redis、MongoDB)
- 全局锁在高并发下可能成为瓶颈
- 脏写检查依赖 after image 对比,极端情况可能失败
可靠消息最终一致性(RocketMQ事务消息)
基本原理
利用RocketMQ的事务消息机制,保证本地事务与消息发送的原子性,从而实现跨服务的最终一致性。
执行流程
- 生产者发送 Half Message(半消息)到Broker
- Broker存储半消息,对消费者不可见
- 生产者执行本地事务
- 根据本地事务结果,向Broker发送 Commit/Rollback
- 若Broker长时间未收到确认,主动回查生产者事务状态
- 消息Commit后,消费者消费消息并执行本地事务
Java实现(RocketMQ事务消息)
java
// 事务消息生产者
@Service
public class OrderTransactionService {
@Autowired
private RocketMQTemplate rocketMQTemplate;
public void createOrderWithTransaction(OrderDTO dto) {
// 构建消息
String msgBody = JSON.toJSONString(new StockDeductMsg(dto.getSkuId(), dto.getQuantity()));
Message<String> message = MessageBuilder.withPayload(msgBody)
.setHeader("orderId", dto.getOrderId())
.build();
// 发送事务消息
rocketMQTemplate.sendMessageInTransaction(
"stock-deduct-topic",
message,
dto // 传递给本地事务执行器的参数
);
}
}
// 本地事务监听器
@RocketMQTransactionListener
public class OrderTransactionListener implements RocketMQLocalTransactionListener {
@Autowired
private OrderMapper orderMapper;
@Override
public RocketMQLocalTransactionState executeLocalTransaction(Message msg, Object arg) {
OrderDTO dto = (OrderDTO) arg;
try {
// 执行本地事务:创建订单
Order order = new Order(dto);
orderMapper.insert(order);
// 本地事务成功,提交消息
return RocketMQLocalTransactionState.COMMIT;
} catch (Exception e) {
// 本地事务失败,回滚消息
return RocketMQLocalTransactionState.ROLLBACK;
}
}
@Override
public RocketMQLocalTransactionState checkLocalTransaction(Message msg) {
// 事务回查:检查订单是否已创建
String orderId = (String) msg.getHeaders().get("orderId");
Order order = orderMapper.selectById(orderId);
if (order != null) {
return RocketMQLocalTransactionState.COMMIT;
}
return RocketMQLocalTransactionState.ROLLBACK;
}
}
// 消费者:库存服务
@Component
@RocketMQMessageListener(topic = "stock-deduct-topic", consumerGroup = "stock-consumer-group")
public class StockDeductConsumer implements RocketMQListener<String> {
@Autowired
private InventoryMapper inventoryMapper;
@Override
public void onMessage(String message) {
StockDeductMsg msg = JSON.parseObject(message, StockDeductMsg.class);
// 幂等检查
if (inventoryMapper.isDeducted(msg.getOrderId())) {
return;
}
// 扣减库存
int rows = inventoryMapper.deductStock(msg.getSkuId(), msg.getQuantity());
if (rows == 0) {
throw new RuntimeException("库存不足,触发重试");
}
// 记录已处理(幂等标记)
inventoryMapper.markDeducted(msg.getOrderId());
}
}
分布式事务选型
选型决策矩阵
| 场景 | 推荐方案 | 理由 |
|---|---|---|
| 强一致性 + 低并发 | XA / 2PC | 数据库原生支持,无侵入 |
| 高并发 + 最终一致 | Seata AT | 无侵入,性能好 |
| 超高并发 + 自定义补偿 | TCC | 性能最优,但开发成本高 |
| 长流程 + 多步骤 | Saga | 适合编排复杂业务流程 |
| 异步解耦 + 最终一致 | 可靠消息(RocketMQ) | 天然异步,削峰填谷 |
| 跨系统通知 | 最大努力通知 | 最简单,适合非核心链路 |
| 遗留系统集成 | 本地消息表 | 不依赖特定MQ事务特性 |
选型流程图
是否需要强一致性?
├── 是 → 并发量如何?
│ ├── 低 → XA/2PC
│ └── 高 → 重新评估业务是否真的需要强一致
└── 否(最终一致即可)
├── 是否需要同步调用?
│ ├── 是 → Seata AT/TCC
│ └── 否 → 可靠消息/Saga
├── 流程步骤是否超过3步?
│ ├── 是 → Saga(协调式)
│ └── 否 → TCC 或 Seata AT
└── 是否跨公司/跨系统?
├── 是 → 最大努力通知
└── 否 → 本地消息表 / RocketMQ事务消息
Java实战(Seata集成Spring Boot)
环境准备
Seata Server部署(Docker):
bash
docker run -d --name seata-server \
-p 8091:8091 \
-p 7091:7091 \
-e SEATA_PORT=8091 \
-e STORE_MODE=db \
seataio/seata-server:1.7.0
Maven依赖
xml
<dependencies>
<!-- Spring Boot -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<!-- Seata -->
<dependency>
<groupId>io.seata</groupId>
<artifactId>seata-spring-boot-starter</artifactId>
<version>1.7.0</version>
</dependency>
<!-- MyBatis Plus -->
<dependency>
<groupId>com.baomidou</groupId>
<artifactId>mybatis-plus-boot-starter</artifactId>
<version>3.5.3</version>
</dependency>
<!-- MySQL -->
<dependency>
<groupId>com.mysql</groupId>
<artifactId>mysql-connector-j</artifactId>
<scope>runtime</scope>
</dependency>
<!-- Nacos(注册中心) -->
<dependency>
<groupId>com.alibaba.cloud</groupId>
<artifactId>spring-cloud-starter-alibaba-nacos-discovery</artifactId>
<version>2022.0.0.0</version>
</dependency>
</dependencies>
配置文件
yaml
# application.yml
server:
port: 8081
spring:
application:
name: order-service
datasource:
url: jdbc:mysql://localhost:3306/order_db?useUnicode=true&characterEncoding=utf8
username: root
password: root
driver-class-name: com.mysql.cj.jdbc.Driver
cloud:
nacos:
discovery:
server-addr: localhost:8848
# Seata配置
seata:
enabled: true
application-id: ${spring.application.name}
tx-service-group: my_tx_group
service:
vgroup-mapping:
my_tx_group: default
grouplist:
default: 127.0.0.1:8091
config:
type: nacos
nacos:
server-addr: localhost:8848
group: SEATA_GROUP
namespace: ""
registry:
type: nacos
nacos:
server-addr: localhost:8848
group: SEATA_GROUP
namespace: ""
application: seata-server
业务代码(AT模式)
java
// 订单服务 - 全局事务发起者
@RestController
@RequestMapping("/order")
public class OrderController {
@Autowired
private OrderBusinessService orderBusinessService;
@PostMapping("/create")
public Result<String> createOrder(@RequestBody OrderDTO dto) {
String orderId = orderBusinessService.createOrder(dto);
return Result.success(orderId);
}
}
@Service
public class OrderBusinessService {
@Autowired
private OrderMapper orderMapper;
@Autowired
private InventoryFeignClient inventoryClient;
@Autowired
private AccountFeignClient accountClient;
@GlobalTransactional(name = "create-order", rollbackFor = Exception.class)
public String createOrder(OrderDTO dto) {
// 1. 创建订单(本地事务,Seata自动代理)
Order order = new Order();
order.setUserId(dto.getUserId());
order.setSkuId(dto.getSkuId());
order.setQuantity(dto.getQuantity());
order.setAmount(dto.getAmount());
order.setStatus(0); // 待支付
orderMapper.insert(order);
// 2. 扣减库存(远程调用,Feign自动传递XID)
inventoryClient.deduct(dto.getSkuId(), dto.getQuantity());
// 3. 扣减账户余额(远程调用)
accountClient.debit(dto.getUserId(), dto.getAmount());
// 4. 更新订单状态
order.setStatus(1); // 已完成
orderMapper.updateById(order);
return order.getId();
}
}
// Feign客户端 - 库存服务
@FeignClient(name = "inventory-service")
public interface InventoryFeignClient {
@PostMapping("/inventory/deduct")
Result<Void> deduct(@RequestParam("skuId") String skuId,
@RequestParam("quantity") int quantity);
}
// 库存服务 - 分支事务参与者
@Service
public class InventoryService {
@Autowired
private InventoryMapper inventoryMapper;
@Transactional(rollbackFor = Exception.class)
public void deductStock(String skuId, int quantity) {
// Seata AT模式自动拦截SQL,生成undo_log
int rows = inventoryMapper.deduct(skuId, quantity);
if (rows == 0) {
throw new BusinessException("库存不足: " + skuId);
}
}
}
Seata数据源代理配置
java
@Configuration
public class DataSourceProxyConfig {
@Bean
@ConfigurationProperties(prefix = "spring.datasource")
public DataSource druidDataSource() {
return new DruidDataSource();
}
@Bean
public DataSourceProxy dataSourceProxy(DataSource druidDataSource) {
// Seata数据源代理,用于拦截SQL生成undo_log
return new DataSourceProxy(druidDataSource);
}
@Bean
public SqlSessionFactory sqlSessionFactory(DataSourceProxy dataSourceProxy) throws Exception {
MybatisSqlSessionFactoryBean factoryBean = new MybatisSqlSessionFactoryBean();
factoryBean.setDataSource(dataSourceProxy);
factoryBean.setMapperLocations(new PathMatchingResourcePatternResolver()
.getResources("classpath:mapper/*.xml"));
return factoryBean.getObject();
}
}
各服务所需SQL(AT模式)
sql
-- 每个参与服务的数据库中都需要创建 undo_log 表
-- order_db、inventory_db、account_db 均需执行
CREATE TABLE IF NOT EXISTS undo_log (
branch_id BIGINT NOT NULL COMMENT 'branch transaction id',
xid VARCHAR(128) NOT NULL COMMENT 'global transaction id',
context VARCHAR(128) NOT NULL COMMENT 'undo_log context, such as serialization',
rollback_info LONGBLOB NOT NULL COMMENT 'rollback info',
log_status INT NOT NULL COMMENT '0: normal status, 1: defense status',
log_created DATETIME(6) NOT NULL COMMENT 'create datetime',
log_modified DATETIME(6) NOT NULL COMMENT 'modify datetime',
UNIQUE KEY ux_undo_log (xid, branch_id)
) ENGINE = InnoDB DEFAULT CHARSET = utf8mb4 COMMENT = 'AT transaction mode undo table';
ALTER TABLE undo_log ADD INDEX ix_log_created (log_created);
最佳实践
设计原则
- 能用本地事务就不用分布式事务:通过合理的数据库设计(如共享数据库)避免分布式事务
- 优先选择最终一致性:大多数业务场景不需要强一致性
- 缩短事务持续时间:减少锁持有时间,降低冲突概率
- 保证幂等性:所有参与者的操作必须支持幂等重试
幂等性设计
java
// 通用幂等注解
@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
public @interface Idempotent {
String key() default "";
long expireSeconds() default 300;
}
// 幂等切面
@Aspect
@Component
public class IdempotentAspect {
@Autowired
private StringRedisTemplate redisTemplate;
@Around("@annotation(idempotent)")
public Object around(ProceedingJoinPoint point, Idempotent idempotent) throws Throwable {
String key = buildKey(point, idempotent);
Boolean success = redisTemplate.opsForValue()
.setIfAbsent(key, "1", idempotent.expireSeconds(), TimeUnit.SECONDS);
if (Boolean.FALSE.equals(success)) {
throw new BusinessException("重复请求,请勿重复提交");
}
try {
return point.proceed();
} catch (Exception e) {
redisTemplate.delete(key); // 失败时释放,允许重试
throw e;
}
}
private String buildKey(ProceedingJoinPoint point, Idempotent idempotent) {
if (!idempotent.key().isEmpty()) {
return "idempotent:" + idempotent.key();
}
MethodSignature signature = (MethodSignature) point.getSignature();
return "idempotent:" + signature.getMethod().getName()
+ ":" + Arrays.hashCode(point.getArgs());
}
}
异常处理与补偿
java
// 全局事务异常处理建议
@GlobalTransactional(rollbackFor = Exception.class, timeoutMills = 60000)
public void businessMethod() {
try {
step1();
step2();
step3();
} catch (BusinessException e) {
// 业务异常:直接抛出,触发全局回滚
throw e;
} catch (TimeoutException e) {
// 超时异常:记录日志,触发回滚,人工介入
log.error("分布式事务超时,需人工处理", e);
throw new SystemException("系统繁忙,请稍后重试");
}
}
监控与告警
java
// Seata事务监控指标(集成Micrometer)
@Component
public class SeataMetricsCollector {
private final MeterRegistry meterRegistry;
private final Counter txCommitCounter;
private final Counter txRollbackCounter;
private final Timer txDurationTimer;
public SeataMetricsCollector(MeterRegistry meterRegistry) {
this.meterRegistry = meterRegistry;
this.txCommitCounter = Counter.builder("seata.tx.commit")
.description("全局事务提交次数")
.register(meterRegistry);
this.txRollbackCounter = Counter.builder("seata.tx.rollback")
.description("全局事务回滚次数")
.register(meterRegistry);
this.txDurationTimer = Timer.builder("seata.tx.duration")
.description("全局事务耗时")
.register(meterRegistry);
}
public void recordCommit(long durationMs) {
txCommitCounter.increment();
txDurationTimer.record(durationMs, TimeUnit.MILLISECONDS);
}
public void recordRollback(long durationMs) {
txRollbackCounter.increment();
txDurationTimer.record(durationMs, TimeUnit.MILLISECONDS);
}
}
生产环境注意事项
| 事项 | 建议 |
|---|---|
| TC高可用 | 部署多节点Seata Server,使用DB存储模式 |
| 超时设置 | 全局事务超时时间 > 各分支事务超时之和 |
| 连接池 | 适当增大数据库连接池(Seata会占用额外连接) |
| 日志 | 开启Seata客户端日志,便于排查问题 |
| 降级 | 非核心链路可降级为最终一致性方案 |
| 压测 | 上线前进行全链路压测,关注全局锁竞争 |
| 版本兼容 | Seata版本与Spring Cloud版本需严格对应 |
常见踩坑点
- Feign调用未传递XID :确保引入
seata-spring-boot-starter,Seata会自动拦截Feign请求传递全局事务XID - 异步调用丢失上下文 :
@Async或线程池中无法自动传递XID,需手动通过RootContext.bind(xid)绑定 - 多数据源未全部代理 :每个数据源都需要用
DataSourceProxy包装 - undo_log序列化问题:字段类型变更可能导致回滚失败,生产环境谨慎DDL
- 全局锁死锁:避免不同全局事务以不同顺序操作相同记录
java
// 异步场景手动传递XID示例
@Service
public class AsyncBusinessService {
@Autowired
private ThreadPoolTaskExecutor executor;
public void asyncProcess() {
// 主线程获取XID
String xid = RootContext.getXID();
executor.execute(() -> {
try {
// 子线程绑定XID
RootContext.bind(xid);
// 执行业务逻辑(参与全局事务)
doBusiness();
} finally {
// 解绑,防止线程池复用时污染
RootContext.unbind();
}
});
}
}
