Java分布式事务:处理跨服务的事务
在微服务架构中,业务流程往往需要多个服务协同完成(如电商的下单流程涉及订单服务、库存服务、支付服务)。当这些服务操作不同的数据库时,如何保证数据一致性就成了核心挑战——这正是分布式事务要解决的问题。
与单体应用的本地事务(ACID特性)不同,分布式事务面临网络分区、服务故障、数据延迟等复杂问题,传统的事务机制难以直接适用。本文将从理论到实践,详细讲解Java分布式事务的解决方案,包含完整代码示例和场景分析,帮助开发者在实际项目中落地可靠的跨服务事务处理方案。
一、分布式事务的核心挑战与理论基础
在深入技术方案前,需先理解分布式事务的本质问题和底层理论,为方案选择提供依据。
1.1 什么是分布式事务?
分布式事务是指跨多个服务或数据源的事务,要求所有参与方要么全部成功,要么全部失败,以保证数据一致性。例如:
- 电商下单:订单服务创建订单、库存服务扣减库存、支付服务处理支付,三者必须同时成功或失败;
- 转账业务:用户A的账户扣款、用户B的账户收款,两个操作需保持一致。
分布式事务的难点在于:
- 服务间通过网络通信,存在超时、丢包等不可靠因素;
- 每个服务管理独立的数据库,无法通过单库事务保证全局一致性;
- 部分服务成功、部分失败时,需有机制进行回滚或补偿。
1.2 理论基础:CAP定理与BASE理论
分布式系统的特性由以下理论奠定,直接影响分布式事务方案的设计:
1.2.1 CAP定理
CAP定理指出,分布式系统无法同时满足以下三个特性,最多只能满足两个:
- 一致性(Consistency):所有节点在同一时间看到的数据是一致的;
- 可用性(Availability):任何请求(即使是部分节点故障)都能收到非错误响应;
- 分区容错性(Partition Tolerance):网络分区(部分节点无法通信)时,系统仍能继续运行。
在微服务架构中,网络分区是不可避免的(P必须满足),因此只能在A和C之间权衡:
- 优先保证一致性(C):适合金融交易等核心场景,但可能牺牲可用性;
- 优先保证可用性(A):适合社交、内容平台等,允许短暂的数据不一致,最终通过补偿达到一致。
1.2.2 BASE理论
BASE理论是对CAP定理的补充,为分布式事务提供了实用的指导思想:
- 基本可用(Basically Available):允许系统在故障时降级(如限流、返回缓存数据),保证核心功能可用;
- 软状态(Soft State):允许数据存在中间状态(如“处理中”),不影响系统整体可用性;
- 最终一致性(Eventually Consistent):不要求实时一致, but 保证经过一段时间后,数据最终达到一致状态。
BASE理论是多数微服务场景的选择:通过牺牲强一致性,换取高可用性,最终通过补偿机制实现数据一致。
1.3 分布式事务的一致性模型
根据一致性要求的强弱,分布式事务可分为:
- 强一致性:所有操作完成后,数据立即一致(如2PC方案);
- 最终一致性:允许短暂不一致,通过异步补偿最终达到一致(如TCC、Saga、事务消息等)。
实际项目中,最终一致性方案因性能优势被广泛采用,强一致性方案仅用于对数据一致性要求极高的场景(如金融核心交易)。
二、Java分布式事务解决方案详解
Java生态中有多种分布式事务解决方案,每种方案有其适用场景和实现复杂度。以下是最常用的5种方案,从原理、代码示例到优缺点进行全面解析。
2.1 两阶段提交(2PC, Two-Phase Commit)
2PC是最经典的强一致性方案,通过“准备阶段”和“提交阶段”协调所有参与者,保证要么全成功,要么全失败。
2.1.1 原理
2PC引入事务协调者(Coordinator) 角色,负责协调多个事务参与者(Participant):
- 准备阶段:协调者向所有参与者发送“准备”请求,参与者执行本地事务(不提交),记录undo/redo日志,返回“就绪”或“失败”;
- 提交阶段:
- 若所有参与者返回“就绪”,协调者发送“提交”请求,参与者执行提交并释放资源;
- 若任一参与者返回“失败”,协调者发送“回滚”请求,参与者根据undo日志回滚。
2.1.2 代码示例:基于Seata的AT模式(2PC的优化实现)
Seata是阿里开源的分布式事务框架,其AT模式是2PC的改进版(自动生成undo/redo日志,降低开发成本)。
步骤1:部署Seata Server(协调者)
- 下载Seata Server:Seata官方仓库
- 配置注册中心(如Nacos)和事务日志存储(如MySQL),启动Server。
步骤2:微服务集成Seata
以Spring Boot微服务为例,集成Seata实现分布式事务。
- 添加依赖(pom.xml):
<dependency>
<groupId>com.alibaba.cloud</groupId>
<artifactId>spring-cloud-starter-alibaba-seata</artifactId>
<version>2022.0.0.0-RC2</version>
</dependency>
- 配置Seata(application.yml):
spring:
cloud:
alibaba:
seata:
tx-service-group: my_test_tx_group # 事务分组,需与Seata Server配置一致
seata:
registry:
type: nacos # 注册中心类型
nacos:
server-addr: 127.0.0.1:8848 # Nacos地址
group: SEATA_GROUP
config:
type: nacos
nacos:
server-addr: 127.0.0.1:8848
group: SEATA_GROUP
步骤3:实现分布式事务
以“下单扣库存”为例,订单服务(order-service)和库存服务(stock-service)参与分布式事务。
- 订单服务(发起方):
@Service
public class OrderService {
@Autowired
private OrderMapper orderMapper;
@Autowired
private StockFeignClient stockFeignClient; // 调用库存服务的Feign客户端
// @GlobalTransactional:标记为全局事务,Seata自动协调
@GlobalTransactional(rollbackFor = Exception.class)
public void createOrder(Long productId, Integer count) {
// 1. 创建订单(本地事务)
Order order = new Order();
order.setProductId(productId);
order.setCount(count);
order.setStatus(1); // 1-未支付
orderMapper.insert(order);
// 2. 调用库存服务扣减库存(远程事务)
boolean deductSuccess = stockFeignClient.deductStock(productId, count);
if (!deductSuccess) {
throw new RuntimeException("库存不足,创建订单失败");
}
// 若此处抛出异常,Seata会协调所有参与者回滚
}
}
- 库存服务(参与者):
@Service
public class StockService {
@Autowired
private StockMapper stockMapper;
// 本地事务方法,由Seata代理
public boolean deductStock(Long productId, Integer count) {
// 扣减库存
int rows = stockMapper.deductStock(productId, count);
return rows > 0;
}
}
- Feign接口(订单服务调用库存服务):
@FeignClient(name = "stock-service")
public interface StockFeignClient {
@PostMapping("/stock/deduct")
boolean deductStock(@RequestParam("productId") Long productId, @RequestParam("count") Integer count);
}
2.1.3 优缺点与适用场景
- 优点:强一致性,实现简单(Seata AT模式几乎无侵入);
- 缺点:
- 性能差:准备阶段会锁定资源,协调者与参与者之间的网络通信可能导致长时间阻塞;
- 协调者单点故障风险:若协调者在提交阶段崩溃,参与者可能处于“未知状态”;
- 适用场景:短事务、一致性要求极高的场景(如金融支付),参与者数量少。
2.2 TCC模式(Try-Confirm-Cancel)
TCC是一种侵入式较强但性能优异的方案,通过业务逻辑拆分实现分布式事务,适合核心业务场景。
2.2.1 原理
TCC将分布式事务拆分为三个阶段,由业务代码显式实现:
- Try(尝试):检查资源并预留资源(如扣减库存前先冻结部分库存);
- Confirm(确认):确认执行业务操作(如将冻结的库存实际扣减),需保证幂等性;
- Cancel(取消):若业务失败,释放Try阶段预留的资源(如解冻冻结的库存),需保证幂等性。
TCC需要事务协调者记录各参与者的状态,在Try成功后触发Confirm,失败则触发Cancel。
2.2.2 代码示例:手动实现TCC
以“下单扣库存”为例,手动实现TCC的三个阶段。
步骤1:定义TCC接口
每个参与者需实现Try/Confirm/Cancel接口。
- 库存服务的TCC接口:
public interface StockTccService {
// Try:冻结库存
@Transactional
boolean tryFreezeStock(Long productId, Integer count, String xid);
// Confirm:确认扣减库存(释放冻结)
@Transactional
boolean confirmDeductStock(String xid);
// Cancel:取消冻结(解冻库存)
@Transactional
boolean cancelFreezeStock(String xid);
}
- 订单服务的TCC接口(创建订单):
public interface OrderTccService {
// Try:创建订单(状态为"待确认")
@Transactional
boolean tryCreateOrder(Long productId, Integer count, String xid);
// Confirm:确认订单(状态改为"已确认")
@Transactional
boolean confirmCreateOrder(String xid);
// Cancel:取消订单(删除或标记为"已取消")
@Transactional
boolean cancelCreateOrder(String xid);
}
步骤2:实现TCC接口
库存服务实现类:
@Service
public class StockTccServiceImpl implements StockTccService {
@Autowired
private StockMapper stockMapper;
@Autowired
private StockFreezeMapper freezeMapper; // 库存冻结记录表
// Try:冻结库存(记录冻结数量和事务ID)
@Override
public boolean tryFreezeStock(Long productId, Integer count, String xid) {
// 1. 检查库存是否充足
Stock stock = stockMapper.selectById(productId);
if (stock == null || stock.getCount() < count) {
return false;
}
// 2. 冻结库存(插入冻结记录)
StockFreeze freeze = new StockFreeze();
freeze.setProductId(productId);
freeze.setFreezeCount(count);
freeze.setXid(xid); // 事务ID,用于确认/取消时关联
freeze.setStatus(0); // 0-未处理
freezeMapper.insert(freeze);
return true;
}
// Confirm:确认扣减(实际扣减库存,删除冻结记录)
@Override
public boolean confirmDeductStock(String xid) {
// 1. 查询冻结记录
StockFreeze freeze = freezeMapper.selectByXid(xid);
if (freeze == null) {
return true; // 幂等处理:已处理过
}
// 2. 扣减实际库存
stockMapper.deductStock(freeze.getProductId(), freeze.getFreezeCount());
// 3. 标记冻结记录为已处理
freeze.setStatus(1);
freezeMapper.updateById(freeze);
return true;
}
// Cancel:取消冻结(解冻库存,删除冻结记录)
@Override
public boolean cancelFreezeStock(String xid) {
// 1. 查询冻结记录
StockFreeze freeze = freezeMapper.selectByXid(xid);
if (freeze == null) {
return true; // 幂等处理
}
// 2. 解冻库存(增加可用库存)
stockMapper.increaseStock(freeze.getProductId(), freeze.getFreezeCount());
// 3. 标记冻结记录为已取消
freeze.setStatus(2);
freezeMapper.updateById(freeze);
return true;
}
}
步骤3:事务协调者
协调者负责生成全局事务ID(xid),调用各参与者的Try方法,根据结果触发Confirm或Cancel:
@Service
public class TccCoordinator {
@Autowired
private OrderTccService orderTccService;
@Autowired
private StockTccService stockTccService;
@Autowired
private TransactionLogMapper logMapper; // 记录事务状态
public void createOrderWithTcc(Long productId, Integer count) {
// 1. 生成全局事务ID
String xid = UUID.randomUUID().toString();
boolean allTrySuccess = false;
try {
// 2. 调用所有参与者的Try方法
boolean orderTry = orderTccService.tryCreateOrder(productId, count, xid);
boolean stockTry = stockTccService.tryFreezeStock(productId, count, xid);
allTrySuccess = orderTry && stockTry;
if (!allTrySuccess) {
throw new RuntimeException("Try阶段失败");
}
// 3. Try全部成功,调用Confirm
orderTccService.confirmCreateOrder(xid);
stockTccService.confirmDeductStock(xid);
// 记录事务成功日志
logMapper.insert(new TransactionLog(xid, "SUCCESS"));
} catch (Exception e) {
// 4. 任一Try失败或异常,调用Cancel
if (allTrySuccess) {
// Try成功后异常,需Confirm补偿(防止部分Confirm未执行)
orderTccService.confirmCreateOrder(xid);
stockTccService.confirmDeductStock(xid);
} else {
// Try失败,执行Cancel
orderTccService.cancelCreateOrder(xid);
stockTccService.cancelFreezeStock(xid);
}
logMapper.insert(new TransactionLog(xid, "FAIL"));
throw e;
}
}
}
2.2.3 优缺点与适用场景
- 优点:
- 性能好:Try阶段仅锁定必要资源,无全局锁;
- 灵活性高:可根据业务定制资源预留和补偿逻辑;
- 缺点:
- 侵入性强:需手动实现Try/Confirm/Cancel接口,开发成本高;
- 需处理幂等性:Confirm/Cancel可能被重复调用(如网络重试);
- 适用场景:核心业务(如支付、库存扣减),对性能要求高,参与者数量少。
2.3 Saga模式
Saga模式适用于长事务和跨多个服务的场景,将分布式事务拆分为一系列本地事务,通过补偿事务保证最终一致性。
2.3.1 原理
Saga模式将分布式事务分解为T1, T2, …, Tn 一系列本地事务,每个Ti对应一个补偿事务Ci(用于撤销Ti的影响):
- 若所有Ti执行成功,事务完成;
- 若某个Ti执行失败,按相反顺序执行已成功Ti的补偿事务(Cn, …, Ci+1, Ci)。
Saga有两种实现方式:
- 编排式(Choreography):各服务通过事件通知协作,无中央协调者;
- 编排式(Orchestration):由一个中央协调者调用各服务的本地事务和补偿事务。
2.3.2 代码示例:基于事件的编排式Saga
以电商下单流程(订单→库存→支付)为例,通过事件驱动实现Saga。
步骤1:定义事件与事件发布
使用Spring Cloud Stream发送事件(基于Kafka/RabbitMQ):
// 事件定义
@Data
public class OrderCreatedEvent {
private String orderId;
private Long productId;
private Integer count;
private LocalDateTime createTime;
}
public class StockDeductedEvent { /* 库存扣减事件 */ }
public class PaymentCompletedEvent { /* 支付完成事件 */ }
public class StockDeductFailedEvent { /* 库存扣减失败事件 */ }
// 事件发布服务
@Service
public class EventPublisher {
@Autowired
private StreamBridge streamBridge;
// 发布订单创建事件
public void publishOrderCreated(OrderCreatedEvent event) {
streamBridge.send("orderCreatedChannel", event);
}
// 其他事件发布方法...
}
步骤2:各服务实现本地事务与补偿
- 订单服务:
@Service
public class OrderService {
@Autowired
private OrderMapper orderMapper;
@Autowired
private EventPublisher eventPublisher;
// 创建订单(T1)
@Transactional
public void createOrder(Long productId, Integer count) {
String orderId = UUID.randomUUID().toString();
Order order = new Order();
order.setId(orderId);
order.setProductId(productId);
order.setCount(count);
order.setStatus(0); // 0-待处理
orderMapper.insert(order);
// 发布订单创建事件,触发库存扣减
eventPublisher.publishOrderCreated(new OrderCreatedEvent(orderId, productId, count, LocalDateTime.now()));
}
// 补偿事务:取消订单(C1)
@Transactional
public void cancelOrder(String orderId) {
Order order = orderMapper.selectById(orderId);
if (order != null) {
order.setStatus(-1); // -1-已取消
orderMapper.updateById(order);
}
}
}
- 库存服务(监听订单创建事件,执行扣减):
@Service
public class StockService {
@Autowired
private StockMapper stockMapper;
@Autowired
private EventPublisher eventPublisher;
// 监听订单创建事件,执行库存扣减(T2)
@Bean
public Consumer<OrderCreatedEvent> handleOrderCreated() {
return event -> {
try {
// 扣减库存
int rows = stockMapper.deductStock(event.getProductId(), event.getCount());
if (rows > 0) {
// 扣减成功,发布库存扣减事件,触发支付
eventPublisher.publishStockDeducted(new StockDeductedEvent(event.getOrderId()));
} else {
// 扣减失败,发布失败事件,触发订单取消
eventPublisher.publishStockDeductFailed(new StockDeductFailedEvent(event.getOrderId()));
}
} catch (Exception e) {
// 异常处理,发布失败事件
eventPublisher.publishStockDeductFailed(new StockDeductFailedEvent(event.getOrderId()));
}
};
}
// 补偿事务:恢复库存(C2)
@Transactional
public void restoreStock(Long productId, Integer count) {
stockMapper.increaseStock(productId, count);
}
}
- 支付服务(监听库存扣减事件,执行支付):
@Service
public class PaymentService {
@Autowired
private PaymentMapper paymentMapper;
@Autowired
private EventPublisher eventPublisher;
@Autowired
private StockFeignClient stockFeignClient; // 调用库存服务补偿
// 监听库存扣减事件,执行支付(T3)
@Bean
public Consumer<StockDeductedEvent> handleStockDeducted() {
return event -> {
try {
// 模拟支付处理
Payment payment = new Payment();
payment.setOrderId(event.getOrderId());
payment.setAmount(calculateAmount(event.getOrderId())); // 计算金额
payment.setStatus(1); // 1-支付成功
paymentMapper.insert(payment);
// 发布支付完成事件,订单服务更新状态
eventPublisher.publishPaymentCompleted(new PaymentCompletedEvent(event.getOrderId()));
} catch (Exception e) {
// 支付失败,调用库存服务补偿(恢复库存)
stockFeignClient.restoreStock(event.getProductId(), event.getCount());
// 发布支付失败事件,订单服务取消订单
eventPublisher.publishPaymentFailed(new PaymentFailedEvent(event.getOrderId()));
}
};
}
}
步骤3:处理补偿逻辑
订单服务监听失败事件,执行补偿:
@Service
public class OrderEventHandler {
@Autowired
private OrderService orderService;
// 监听库存扣减失败事件,取消订单
@Bean
public Consumer<StockDeductFailedEvent> handleStockDeductFailed() {
return event -> orderService.cancelOrder(event.getOrderId());
}
// 监听支付失败事件,取消订单
@Bean
public Consumer<PaymentFailedEvent> handlePaymentFailed() {
return event -> orderService.cancelOrder(event.getOrderId());
}
}
2.3.3 优缺点与适用场景
- 优点:
- 适合长事务和多服务场景(如电商下单涉及5+服务);
- 无锁阻塞,性能好,可用性高;
- 缺点:
- 实现复杂:需设计事件交互和补偿逻辑;
- 中间状态可见:用户可能看到“订单创建中→库存扣减中→支付中”等中间状态;
- 适用场景:长事务、多服务协同(如电商订单、物流跟踪),可接受中间状态。
2.4 本地消息表(Local Message Table)
本地消息表是一种基于数据库的可靠消息方案,通过在本地事务中记录消息,保证消息的可靠发送,从而实现最终一致性。
2.4.1 原理
- 本地事务:在发起方服务的本地事务中,同时完成业务操作和消息记录(写入本地消息表);
- 消息发送:通过定时任务扫描本地消息表,将“待发送”状态的消息发送到消息队列;
- 消息消费:接收方消费消息并执行本地事务,完成后通知发起方更新消息状态;
- 失败重试:若消息发送或消费失败,定时任务重复重试,直到成功。
2.4.2 代码示例:基于MySQL本地消息表
以“订单创建后通知库存扣减”为例,实现本地消息表方案。
步骤1:创建本地消息表
在订单服务的数据库中创建消息表:
CREATE TABLE `local_message` (
`id` bigint NOT NULL AUTO_INCREMENT,
`message_id` varchar(64) NOT NULL COMMENT '消息唯一ID',
`content` text NOT NULL COMMENT '消息内容(JSON)',
`topic` varchar(128) NOT NULL COMMENT '消息队列主题',
`status` tinyint NOT NULL COMMENT '状态:0-待发送,1-已发送,2-已消费,3-失败',
`retry_count` int NOT NULL DEFAULT 0 COMMENT '重试次数',
`create_time` datetime NOT NULL,
`update_time` datetime NOT NULL,
PRIMARY KEY (`id`),
UNIQUE KEY `uk_message_id` (`message_id`)
) ENGINE=InnoDB COMMENT='本地消息表';
步骤2:订单服务实现(发起方)
- 业务逻辑与消息记录:
@Service
public class OrderService {
@Autowired
private OrderMapper orderMapper;
@Autowired
private LocalMessageMapper messageMapper;
@Autowired
private RabbitTemplate rabbitTemplate;
// 创建订单并记录消息(本地事务)
@Transactional
public void createOrder(Long productId, Integer count) {
// 1. 创建订单
String orderId = UUID.randomUUID().toString();
Order order = new Order();
order.setId(orderId);
order.setProductId(productId);
order.setCount(count);
orderMapper.insert(order);
// 2. 记录消息到本地消息表(与订单创建在同一事务)
String messageId = UUID.randomUUID().toString();
StockDeductMessage message = new StockDeductMessage(orderId, productId, count);
LocalMessage localMessage = new LocalMessage();
localMessage.setMessageId(messageId);
localMessage.setContent(JSON.toJSONString(message));
localMessage.setTopic("stock-deduct-topic");
localMessage.setStatus(0); // 待发送
localMessage.setCreateTime(LocalDateTime.now());
localMessage.setUpdateTime(LocalDateTime.now());
messageMapper.insert(localMessage);
}
// 定时任务:发送未成功的消息
@Scheduled(cron = "0 */1 * * * ?") // 每分钟执行一次
public void sendPendingMessages() {
// 查询状态为0(待发送)且重试次数<3的消息
List<LocalMessage> messages = messageMapper.selectPendingMessages(3);
for (LocalMessage msg : messages) {
try {
// 发送消息到RabbitMQ
rabbitTemplate.convertAndSend(msg.getTopic(), msg.getContent());
// 发送成功,更新状态为1(已发送)
msg.setStatus(1);
msg.setRetryCount(msg.getRetryCount() + 1);
msg.setUpdateTime(LocalDateTime.now());
messageMapper.updateById(msg);
} catch (Exception e) {
// 发送失败,更新重试次数
msg.setRetryCount(msg.getRetryCount() + 1);
if (msg.getRetryCount() >= 3) {
msg.setStatus(3); // 标记为失败
}
msg.setUpdateTime(LocalDateTime.now());
messageMapper.updateById(msg);
}
}
}
}
步骤3:库存服务实现(接收方)
@Service
public class StockService {
@Autowired
private StockMapper stockMapper;
@Autowired
private RabbitTemplate rabbitTemplate;
@Autowired
private ConsumedMessageMapper consumedMsgMapper; // 记录已消费消息(防重复)
// 监听消息并扣减库存
@RabbitListener(queues = "stock-deduct-queue")
public void handleStockDeduct(String messageJson) {
StockDeductMessage message = JSON.parseObject(messageJson, StockDeductMessage.class);
String messageId = message.getMessageId();
// 1. 幂等性检查:判断消息是否已消费
if (consumedMsgMapper.exists(messageId)) {
return;
}
// 2. 执行库存扣减(本地事务)
try {
int rows = stockMapper.deductStock(message.getProductId(), message.getCount());
if (rows > 0) {
// 扣减成功,记录已消费消息
consumedMsgMapper.insert(new ConsumedMessage(messageId));
// 发送消息确认(可选,通知订单服务)
rabbitTemplate.convertAndSend("message-confirm-topic", messageId);
}
} catch (Exception e) {
// 消费失败,消息会被RabbitMQ重新投递(需配置重试机制)
throw new AmqpRejectAndDontRequeueException("库存扣减失败,拒绝重新入队", e);
}
}
}
2.4.3 优缺点与适用场景
- 优点:
- 实现简单:基于数据库和消息队列,无需引入复杂框架;
- 可靠性高:本地事务保证消息必被记录,定时任务保证消息必被发送;
- 缺点:
- 侵入性:需在业务表外额外维护消息表;
- 性能:定时任务扫描表可能影响数据库性能;
- 适用场景:中小规模应用,对一致性要求不高但需可靠消息传递(如通知、日志同步)。
2.5 事务消息(Transactional Message)
事务消息是消息队列提供的高级特性(如RocketMQ、阿里云ONS),通过消息队列本身保证消息的最终一致性,简化分布式事务实现。
2.5.1 原理
事务消息引入“半消息”概念,解决消息发送与本地事务的原子性问题:
- 发送半消息:发起方发送“半消息”到消息队列(此时接收方无法消费);
- 执行本地事务:发起方执行本地业务逻辑;
- 确认消息:
- 本地事务成功:通知消息队列将半消息标记为“可消费”,接收方可消费;
- 本地事务失败:通知消息队列删除半消息,接收方不会收到;
- 回查机制:若消息队列未收到确认,会定时回查发起方的事务状态,决定消息是否可消费。
2.5.2 代码示例:基于RocketMQ事务消息
以“订单创建后扣减库存”为例,使用RocketMQ的事务消息功能。
步骤1:添加RocketMQ依赖
<dependency>
<groupId>org.apache.rocketmq</groupId>
<artifactId>rocketmq-spring-boot-starter</artifactId>
<version>2.2.3</version>
</dependency>
步骤2:配置RocketMQ
rocketmq:
name-server: 127.0.0.1:9876
producer:
group: order-producer-group # 生产者组
步骤3:订单服务(发起方)实现
@Service
public class OrderService {
@Autowired
private OrderMapper orderMapper;
@Autowired
private RocketMQTemplate rocketMQTemplate;
// 创建订单并发送事务消息
public void createOrder(Long productId, Integer count) {
// 1. 准备消息内容
String orderId = UUID.randomUUID().toString();
StockDeductMessage message = new StockDeductMessage(orderId, productId, count);
// 2. 发送事务消息:topic + tag,消息内容,额外参数(用于回查)
rocketMQTemplate.sendMessageInTransaction(
"stock-deduct-topic:deduct", // topic:tag
MessageBuilder.withPayload(message).build(),
orderId // 额外参数:订单ID,用于本地事务和回查
);
}
// 实现RocketMQ事务监听器:执行本地事务和回查
@RocketMQTransactionListener
public class OrderTransactionListener implements RocketMQLocalTransactionListener {
@Autowired
private OrderMapper orderMapper;
// 执行本地事务
@Override
public RocketMQLocalTransactionState executeLocalTransaction(Message msg, Object arg) {
String orderId = (String) arg;
StockDeductMessage message = JSON.parseObject(new String((byte[]) msg.getPayload()), StockDeductMessage.class);
try {
// 创建订单(本地事务)
Order order = new Order();
order.setId(orderId);
order.setProductId(message.getProductId());
order.setCount(message.getCount());
orderMapper.insert(order);
// 本地事务成功,提交消息
return RocketMQLocalTransactionState.COMMIT;
} catch (Exception e) {
// 本地事务失败,回滚消息
return RocketMQLocalTransactionState.ROLLBACK;
}
}
// 回查事务状态(消息队列未收到确认时调用)
@Override
public RocketMQLocalTransactionState checkLocalTransaction(Message msg) {
StockDeductMessage message = JSON.parseObject(new String((byte[]) msg.getPayload()), StockDeductMessage.class);
// 检查订单是否存在(判断本地事务是否成功)
Order order = orderMapper.selectById(message.getOrderId());
if (order != null) {
return RocketMQLocalTransactionState.COMMIT;
} else {
return RocketMQLocalTransactionState.ROLLBACK;
}
}
}
}
步骤4:库存服务(接收方)实现
@Service
public class StockService {
@Autowired
private StockMapper stockMapper;
@Autowired
private ConsumedMessageMapper consumedMsgMapper;
// 监听事务消息,扣减库存
@RocketMQMessageListener(
topic = "stock-deduct-topic",
consumerGroup = "stock-consumer-group",
selectorExpression = "deduct"
)
public class StockConsumer implements RocketMQListener<StockDeductMessage> {
@Override
public void onMessage(StockDeductMessage message) {
// 幂等性检查
if (consumedMsgMapper.exists(message.getMessageId())) {
return;
}
// 扣减库存
int rows = stockMapper.deductStock(message.getProductId(), message.getCount());
if (rows > 0) {
consumedMsgMapper.insert(new ConsumedMessage(message.getMessageId()));
} else {
// 库存不足,需人工干预或触发补偿(如取消订单)
throw new RuntimeException("库存不足");
}
}
}
}
2.5.3 优缺点与适用场景
- 优点:
- 可靠性高:消息队列保证消息不丢失,回查机制解决超时问题;
- 侵入性低:无需手动维护消息表,框架层处理事务协调;
- 缺点:
- 依赖特定消息队列:仅RocketMQ等少数队列支持事务消息;
- 回查机制增加复杂度:需实现事务状态回查接口;
- 适用场景:中大规模应用,已使用RocketMQ等支持事务消息的队列(如电商、支付系统)。
三、分布式事务方案对比与选择
不同分布式事务方案各有优劣,选择时需结合业务场景、性能要求、开发成本等因素综合判断。
方案 | 一致性 | 性能 | 开发成本 | 适用场景 | 典型框架/组件 |
---|---|---|---|---|---|
2PC(Seata AT) | 强一致性 | 低 | 低 | 短事务、一致性要求极高(金融支付) | Seata |
TCC | 最终一致性 | 高 | 高 | 核心业务、性能要求高(库存扣减) | Seata TCC、Hmily |
Saga | 最终一致性 | 高 | 中 | 长事务、多服务协同(电商下单) | Axon、Seata Saga |
本地消息表 | 最终一致性 | 中 | 中 | 中小应用、消息可靠传递(通知、日志) | 自定义实现(MySQL+MQ) |
事务消息 | 最终一致性 | 中 | 低 | 已使用RocketMQ的场景(电商、支付) | RocketMQ、阿里云ONS |
选择建议:
- 优先通过业务设计避免分布式事务(如合并服务、异步化非核心流程);
- 一致性要求极高(如金融交易):选2PC(Seata AT);
- 核心业务、性能优先(如库存扣减):选TCC;
- 多服务长事务(如电商下单):选Saga;
- 已集成RocketMQ:选事务消息;
- 中小应用、快速实现:选本地消息表。
四、分布式事务最佳实践
无论选择哪种方案,都需注意以下实践原则,避免常见问题:
4.1 保证幂等性
分布式事务中,由于网络重试、消息重投等,同一个操作可能被执行多次,必须保证幂等性(多次执行结果一致)。
实现方式:
- 唯一标识:为每个操作生成唯一ID(如订单ID、消息ID),通过数据库唯一索引确保重复操作不生效;
- 状态机设计:将业务状态设计为有限状态机(如订单:待支付→支付中→已支付),避免重复处理;
- 防重放令牌:调用方生成令牌,服务方验证令牌有效性,使用后失效。
4.2 处理超时与重试
网络超时是分布式系统的常态,需设计合理的重试机制:
- 重试策略:使用指数退避重试(如1s、2s、4s),避免重试风暴;
- 重试上限:设置最大重试次数(如3次),超过后标记为失败,人工干预;
- 异步重试:非核心流程使用异步重试(如定时任务),不阻塞主流程。
4.3 日志与监控
分布式事务问题排查复杂,需完善日志和监控:
- 全链路日志:记录全局事务ID(xid),串联各服务日志,便于追踪完整流程;
- 状态监控:监控事务成功率、重试次数、失败率,设置告警阈值(如失败率>1%告警);
- 补偿监控:监控补偿事务的执行情况,确保失败的事务被及时补偿。
4.4 避免大事务
分布式事务范围越大,失败概率越高,应尽量拆分:
- 拆分事务:将大事务拆分为多个小事务,减少参与方;
- 异步化:非核心步骤异步处理(如订单创建后异步发送通知);
- 缩短事务时间:避免在事务中执行耗时操作(如远程调用、大量计算)。
五、总结
分布式事务是微服务架构中的核心难题,没有“银弹”方案,需根据业务场景选择合适的实现方式:
- 强一致性方案(如2PC)适合对数据一致性要求极高的场景,但牺牲性能;
- 最终一致性方案(如TCC、Saga、事务消息)性能更优,是微服务的主流选择,需接受短暂的数据不一致并通过补偿机制保证最终一致。
实际项目中,优先通过业务设计减少分布式事务(如单库多表、异步化),必须使用时,需结合幂等性、重试机制、监控告警等保障措施,确保系统的可靠性和可维护性。
随着云原生技术的发展,Service Mesh(如Istio)也开始提供分布式事务解决方案,未来分布式事务的实现将更加透明化、低侵入化,但核心思想仍离不开本文介绍的理论和实践原则。