分布式事务的典型问题场景
微服务架构中,一个业务操作往往跨多个服务:下单时需要同时扣库存、扣余额、生成订单记录。如果扣库存成功但扣余额失败,库存数据就处于不一致状态。这类问题在单体应用中用本地事务一行@Transactional就能解决,拆成微服务后变成了分布式事务问题——多个服务各自维护独立数据库,本地事务无法跨服务生效。
解决分布式事务有两套主流思路:强一致性(Seata AT/TCC模式)和最终一致性(消息中间件驱动)。强一致性保证所有参与方同时提交或回滚,代价是锁定资源和性能损耗;最终一致性允许中间状态存在,通过消息重试保证最终所有参与方达成一致,代价是业务方需要实现补偿逻辑。
Seata AT模式:低侵入的自动分布式事务
Seata AT模式的核心是自动拦截SQL、记录前后镜像、在事务协调器控制下统一提交或回滚。对业务代码的侵入最小——加一个@GlobalTransactional注解即可。
AT模式的工作流程分三个阶段:
- 一阶段:拦截业务SQL,执行前查询数据生成before-image,执行SQL后查询数据生成after-image,生成回滚日志(undo_log)一并提交到本地事务
- 二阶段提交:异步清理undo_log
- 二阶段回滚:根据undo_log中的before-image反向补偿,恢复原始数据
// Spring Boot项目中使用Seata AT模式
// 1. 添加依赖(pom.xml)
// <dependency>
// <groupId>io.seata</groupId>
// <artifactId>seata-spring-boot-starter</artifactId>
// <version>2.2.0</version>
// </dependency>
// 2. 业务代码加注解
@Service
public class OrderService {
@GlobalTransactional(timeoutMills = 60000, name = "create-order")
public Order createOrder(OrderDTO dto) {
// 1. 创建订单(本地事务)
Order order = orderMapper.insert(dto);
// 2. 扣库存(远程调用库存服务)
inventoryClient.deduct(dto.getProductId(), dto.getQuantity());
// 3. 扣余额(远程调用账户服务)
accountClient.debit(dto.getUserId(), dto.getAmount());
return order;
}
}
// 3. 每个参与方的数据库创建undo_log表
// CREATE TABLE undo_log (
// id BIGINT PRIMARY KEY AUTO_INCREMENT,
// branch_id VARCHAR(64) NOT NULL,
// xid VARCHAR(64) NOT NULL,
// context VARCHAR(128) NOT NULL,
// rollback_info LONGBLOB NOT NULL,
// log_status INT NOT NULL,
// log_created DATETIME NOT NULL,
// log_modified DATETIME NOT NULL,
// UNIQUE KEY ux_undo_log (xid, branch_id)
// );
AT模式的局限:只支持关系型数据库,不支持NoSQL;全局锁可能导致死锁(两个全局事务交叉锁定对方需要的行);性能开销在10-30%之间(SQL拦截和镜像记录)。
Seata TCC模式:手动补偿的精确控制
TCC(Try-Confirm-Cancel)模式要求业务方实现三个方法:Try预留资源、Confirm确认提交、Cancel释放预留。相比AT模式的自动拦截,TCC需要手写补偿逻辑,但控制粒度更细,不依赖SQL拦截。
// TCC模式实现扣库存
@LocalTCC
public interface InventoryTccService {
@TwoPhaseBusinessAction(
name = "deductInventory",
commitMethod = "confirm",
rollbackMethod = "cancel"
)
boolean tryDeduct(
@BusinessActionContextParameter(paramName = "productId") Long productId,
@BusinessActionContextParameter(paramName = "quantity") Integer quantity
);
boolean confirm(BusinessActionContext context);
boolean cancel(BusinessActionContext context);
}
@Service
public class InventoryTccServiceImpl implements InventoryTccService {
@Override
public boolean tryDeduct(Long productId, Integer quantity) {
// Try阶段:冻结库存,不实际扣减
return inventoryMapper.freezeStock(productId, quantity) > 0;
}
@Override
public boolean confirm(BusinessActionContext context) {
// Confirm阶段:扣减冻结库存
Long productId = (Long) context.getActionContext("productId");
Integer quantity = (Integer) context.getActionContext("quantity");
return inventoryMapper.deductFrozenStock(productId, quantity) > 0;
}
@Override
public boolean cancel(BusinessActionContext context) {
// Cancel阶段:释放冻结库存
Long productId = (Long) context.getActionContext("productId");
Integer quantity = (Integer) context.getActionContext("quantity");
return inventoryMapper.releaseFrozenStock(productId, quantity) > 0;
}
}
TCC模式的冻结库存表设计:
CREATE TABLE inventory (
id BIGINT PRIMARY KEY AUTO_INCREMENT,
product_id BIGINT NOT NULL,
available_qty INT NOT NULL DEFAULT 0,
frozen_qty INT NOT NULL DEFAULT 0,
INDEX idx_product (product_id)
);
-- Try: UPDATE inventory SET frozen_qty = frozen_qty + ? WHERE product_id = ? AND available_qty - frozen_qty >= ?
-- Confirm: UPDATE inventory SET available_qty = available_qty - ?, frozen_qty = frozen_qty - ? WHERE product_id = ?
-- Cancel: UPDATE inventory SET frozen_qty = frozen_qty - ? WHERE product_id = ?
RocketMQ事务消息:最终一致性方案
当业务可以容忍短暂的不一致状态时,用消息中间件实现最终一致性比Seata轻量得多。RocketMQ原生支持事务消息,保证本地事务和消息发送的原子性。
以创建订单并发送扣库存消息为例:
// 1. 订单服务:发送事务消息
@Service
public class OrderMessageService {
@Autowired
private RocketMQTemplate rocketMQTemplate;
public void createOrderAndNotify(OrderDTO dto) {
rocketMQTemplate.sendMessageInTransaction(
"order-create-topic",
MessageBuilder.withPayload(dto).build(),
dto
);
}
// 本地事务执行器
@RocketMQTransactionListener
public class OrderTransactionListener implements RocketMQLocalTransactionListener {
@Autowired
private OrderService orderService;
@Override
public RocketMQLocalTransactionState executeLocalTransaction(Message msg, Object arg) {
try {
OrderDTO dto = (OrderDTO) arg;
orderService.createOrder(dto);
return RocketMQLocalTransactionState.COMMIT;
} catch (Exception e) {
return RocketMQLocalTransactionState.ROLLBACK;
}
}
@Override
public RocketMQLocalTransactionState checkLocalTransaction(Message msg) {
String orderId = msg.getHeaders().get("orderId", String.class);
Order order = orderService.getById(orderId);
return order != null ? RocketMQLocalTransactionState.COMMIT
: RocketMQLocalTransactionState.ROLLBACK;
}
}
}
// 2. 库存服务:消费消息扣库存
@Component
@RocketMQMessageListener(topic = "order-create-topic", consumerGroup = "inventory-consumer-group")
public class InventoryConsumer implements RocketMQListener<OrderDTO> {
@Autowired
private InventoryService inventoryService;
@Override
public void onMessage(OrderDTO dto) {
try {
inventoryService.deduct(dto.getProductId(), dto.getQuantity());
} catch (Exception e) {
throw new RuntimeException("扣库存失败,等待重试", e);
}
}
}
消息幂等性:避免重复消费的关键
RocketMQ保证消息至少投递一次(at-least-once),消费端可能收到重复消息。必须实现幂等性:同一消息消费多次和消费一次的效果相同。常用方案是消费记录表:
// 消费幂等表
CREATE TABLE consume_record (
id BIGINT PRIMARY KEY AUTO_INCREMENT,
msg_id VARCHAR(64) NOT NULL UNIQUE,
status TINYINT NOT NULL DEFAULT 0,
created_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
INDEX idx_msg_id (msg_id)
);
// 幂等消费逻辑
@Override
public void onMessage(OrderDTO dto) {
String msgId = dto.getMsgId();
ConsumeRecord record = consumeRecordMapper.selectByMsgId(msgId);
if (record != null && record.getStatus() == 1) {
log.info("消息已处理,跳过: {}", msgId);
return;
}
if (record == null) {
consumeRecordMapper.insert(msgId, 0);
}
try {
inventoryService.deduct(dto.getProductId(), dto.getQuantity());
consumeRecordMapper.updateStatus(msgId, 1);
} catch (Exception e) {
consumeRecordMapper.updateStatus(msgId, 2);
throw e;
}
}
API接口规范与服务治理集成
分布式事务方案的选型直接影响API接口规范设计。强一致性方案(Seata)适合同步调用的API,调用方等待事务最终结果;最终一致性方案(RocketMQ)适合异步API,调用方只关心本地事务是否成功,下游操作通过消息驱动。
API设计原则:
- 使用Seata的接口返回明确的成功/失败状态码,超时需要调用方主动查询
- 使用消息驱动的接口返回本地事务结果,下游操作通过回调或状态查询获取
- 关键操作提供查询接口:GET /api/orders/{id}/status
服务治理方面,Seata Server需要高可用部署(至少3节点集群),注册到Nacos实现服务发现。RocketMQ集群同样至少3个Broker,2主2从保证消息不丢失。
方案选型决策树
分布式事务选型决策流程:
是否需要强一致性?
├── 是 → 数据是否全部在关系型数据库?
│ ├── 是 → Seata AT模式(低侵入,自动补偿)
│ └── 否 → Seata TCC模式(手动补偿,支持非SQL资源)
└── 否 → 是否可接受短暂不一致?
├── 是 → RocketMQ事务消息(最终一致性)
└── 否 → 重新评估业务需求
注意事项:
- AT模式单事务建议不超过3个参与方,参与方越多锁冲突概率越大
- TCC模式需要业务方保证Try/Confirm/Cancel的幂等性
- 消息模式需要消费端实现幂等,消费失败依赖重试机制
- 混合方案:核心链路用Seata,非核心链路用消息,降低锁冲突范围
微服务架构下没有完美的分布式事务方案,只有适合当前业务阶段的方案。选择的核心依据是业务对一致性的容忍度和系统的吞吐量需求。低频高价值操作用Seata保强一致,高频低价值操作用消息保最终一致,两条路径组合使用是生产环境最务实的做法。
原创文章,作者:小编,如若转载,请注明出处:https://www.yunthe.com/fen-bu-shi-shi-wu-shi-zhan-seataattcc-yu-rocketmq-zui-zhong/