微服务分布式事务的核心挑战
单体应用中,数据库本地事务通过ACID保证数据一致性。拆分为微服务后,一个业务操作可能涉及多个服务的数据库写入,本地事务无法跨服务生效。例如电商下单场景,需要同时扣减库存服务中的库存、创建订单服务中的订单、增加积分服务中的积分。任一步骤失败都需要回滚前序操作,这就是分布式事务要解决的核心问题。
分布式事务的理论基础是CAP定理和BASE理论。强一致性(CP)以可用性为代价,适合金融转账等对一致性要求极高的场景;最终一致性(AP)以短暂的不一致为代价换取高可用性,适合电商、社交等对延迟敏感的场景。实际项目中,两种方案经常混合使用——核心链路走强一致性,非核心链路走最终一致性。
Seata AT模式:低侵入的强一致性方案
Seata是阿里巴巴开源的分布式事务框架,AT模式是其最常用的模式,对业务代码的侵入性最低。AT模式的核心思想是一阶段拦截SQL执行并记录回滚日志,二阶段根据事务协调者的指令提交或回滚。
AT模式的工作流程:一阶段中,Seata拦截业务SQL,在执行前查询修改前的数据作为before-image,执行SQL后再查询修改后的数据作为after-image,两份镜像组成undo-log写入seata_undo_log表,然后提交本地事务。二阶段提交时,异步删除undo-log即可。二阶段回滚时,根据undo-log中的before-image生成反向SQL执行补偿。
Spring Boot集成Seata AT模式的配置步骤:
<!-- Maven依赖 -->
<dependency>
<groupId>io.seata</groupId>
<artifactId>seata-spring-boot-starter</artifactId>
<version>1.8.0</version>
</dependency>
# application.yml配置
seata:
enabled: true
application-id: order-service
tx-service-group: my-tx-group
service:
vgroup-mapping:
my-tx-group: default
registry:
type: nacos
nacos:
server-addr: 127.0.0.1:8848
namespace: seata
业务代码中只需在需要分布式事务的方法上添加@GlobalTransactional注解:
@Service
public class OrderService {
@GlobalTransactional(name = "create-order", rollbackFor = Exception.class)
public OrderResult createOrder(OrderDTO orderDTO) {
// 1. 创建订单(本地事务)
Order order = orderMapper.insert(orderDTO);
// 2. 扣减库存(远程调用)
inventoryClient.deduct(orderDTO.getProductId(), orderDTO.getQuantity());
// 3. 增加积分(远程调用)
pointsClient.earn(orderDTO.getUserId(), order.getPoints());
return OrderResult.success(order.getId());
}
}
AT模式的优势是业务代码几乎无需修改,回滚由框架自动完成。劣势在于:全局锁机制在高并发场景下可能成为瓶颈,因为一阶段提交后本地事务释放了数据库锁,但Seata的全局锁仍然持有,阻止其他事务修改同一行数据直到全局事务提交或回滚。此外,undo-log表会增大数据库存储和IO压力。
消息最终一致性:高可用的异步方案
消息最终一致性通过消息队列实现跨服务数据同步,核心思路是:上游服务完成本地事务后发送消息,下游服务消费消息并执行本地事务。如果下游处理失败,消息队列会重试投递直到成功。
这种方案的关键挑战是保证本地事务与消息发送的原子性。如果本地事务提交成功但消息发送失败,数据不一致;如果消息发送成功但本地事务回滚,下游收到错误消息。解决方案是使用事务消息(RocketMQ原生支持)或本地消息表。
本地消息表方案的实现:
@Service
public class OrderService {
@Transactional
public OrderResult createOrder(OrderDTO orderDTO) {
// 1. 创建订单
Order order = orderMapper.insert(orderDTO);
// 2. 写入本地消息表(与订单在同一事务中)
OutboxMessage msg = new OutboxMessage();
msg.setTopic("order-created");
msg.setKey(order.getId().toString());
msg.setBody(JSON.toJSONString(orderDTO));
msg.setStatus("PENDING");
msg.setRetryCount(0);
msg.setMaxRetry(5);
msg.setNextRetryTime(new Date());
outboxMapper.insert(msg);
return OrderResult.success(order.getId());
}
}
@Scheduled(fixedDelay = 5000)
public void sendOutboxMessages() {
List<OutboxMessage> messages = outboxMapper.selectPendingMessages(50);
for (OutboxMessage msg : messages) {
try {
SendResult result = rocketMQTemplate.syncSend(
msg.getTopic(),
MessageBuilder.withPayload(msg.getBody())
.setKey(msg.getKey())
.build()
);
if (result.getSendStatus() == SendStatus.SEND_OK) {
outboxMapper.updateStatus(msg.getId(), "SENT");
}
} catch (Exception e) {
outboxMapper.incrementRetry(msg.getId());
if (msg.getRetryCount() >= msg.getMaxRetry()) {
outboxMapper.updateStatus(msg.getId(), "FAILED");
alertService.notify("消息发送失败", msg);
}
}
}
}
下游消费者需要实现幂等性,因为消息队列可能重复投递。幂等性实现方式:为每条消息分配唯一ID,下游处理前先查询是否已处理过该ID的消息。数据库唯一索引是最可靠的幂等保障。
两种方案的适用场景与选型建议
Seata AT模式适合:事务链路短(2-3个服务)、对实时一致性要求高、并发量中等(QPS<5000)的场景。典型用例包括金融转账、库存扣减、账户余额变更。
消息最终一致性适合:事务链路较长、允许短暂不一致(秒级到分钟级)、高并发场景。典型用例包括订单状态同步、积分发放、通知推送。
实际项目中经常混合使用。核心链路(扣库存、创建订单)用Seata保证强一致性,非核心链路(积分、通知)走消息异步处理。这种架构既保证了核心数据的正确性,又避免了全局事务过长导致的性能问题。
分布式事务监控与运维要点
Seata Server需要独立部署和监控。关键指标包括:全局事务数量、提交/回滚比率、平均事务耗时。回滚率突然上升通常意味着业务逻辑异常或网络抖动,需要及时排查。
消息方案需要监控消息堆积量、消费延迟和死信队列大小。RocketMQ提供了Dashboard控制台查看这些指标。死信队列中的消息需要人工介入处理,建议配置告警在堆积量超过阈值时触发通知。
两种方案都需要做好灰度发布策略。Seata的undo-log格式变更可能需要兼容旧版本,消息的格式变更需要保证新旧消费者都能正常解析。建议在消息体中增加version字段,消费者根据版本号走不同的解析逻辑,实现平滑升级。
原创文章,作者:小编,如若转载,请注明出处:https://www.yunthe.com/springboot-wei-fu-wu-fen-bu-shi-shi-wu-shi-xian-fang-an/