springboot微服务框架中,a服务的aa方法中调用了b服务的bb方法,然后调用c服务的cc方法,由于bb方法会更新数据库,但是需要等到cc方法执行成功,才要把事务提交,如果cc方法不成功,就回滚
·
针对这种跨服务的事务场景,Spring Boot 提供了几种解决方案,按简单程度推荐:
方案1:使用分布式事务(推荐)
1.1 集成 Seata(最完整方案)
步骤:
<!-- 各服务pom.xml中添加 -->
<dependency>
<groupId>io.seata</groupId>
<artifactId>seata-spring-boot-starter</artifactId>
<version>最新版本</version>
</dependency>
配置:
# application.yml
seata:
enabled: true
application-id: ${spring.application.name}
tx-service-group: my_tx_group
service:
vgroup-mapping:
my_tx_group: default
config:
type: nacos
registry:
type: nacos
代码实现:
// A服务中
@Service
public class AService {
@Autowired
private BServiceClient bServiceClient;
@Autowired
private CServiceClient cServiceClient;
@GlobalTransactional // 开启全局事务
public void aa() {
// 调用B服务
bServiceClient.bb();
// 调用C服务
cServiceClient.cc();
// 如果cc()失败,Seata会自动回滚bb()的事务
}
}
方案2:基于消息队列的最终一致性(异步解耦)
2.1 使用 RocketMQ 事务消息
@Service
public class AService {
@Autowired
private TransactionMQProducer producer;
public void aa() {
// 发送事务消息
TransactionSendResult sendResult = producer.sendMessageInTransaction(
new Message("topic", "bb完成,准备执行cc".getBytes()),
null
);
}
// 本地事务执行器
@RocketMQTransactionListener
class TransactionListenerImpl implements RocketMQLocalTransactionListener {
@Override
public RocketMQLocalTransactionState executeLocalTransaction(Message msg, Object arg) {
try {
// 执行B服务调用
bServiceClient.bb();
return RocketMQLocalTransactionState.UNKNOWN;
} catch (Exception e) {
return RocketMQLocalTransactionState.ROLLBACK;
}
}
@Override
public RocketMQLocalTransactionState checkLocalTransaction(Message msg) {
// 检查C服务是否执行成功
boolean ccSuccess = checkCCSuccess();
return ccSuccess ?
RocketMQLocalTransactionState.COMMIT :
RocketMQLocalTransactionState.ROLLBACK;
}
}
}
方案3:SAGA模式(手动编排)
3.1 使用状态机实现
@Service
public class AService {
public void aa() {
try {
// 1. 调用B服务
bServiceClient.bb();
// 2. 调用C服务
cServiceClient.cc();
} catch (Exception e) {
// 3. 如果C失败,调用B的回滚接口
bServiceClient.rollbackBb();
throw e;
}
}
}
// B服务提供回滚接口
@RestController
public class BController {
@PostMapping("/bb")
public void bb() {
// 执行数据库操作,但先不提交
// 可以记录操作日志,用于回滚
}
@PostMapping("/rollback/bb")
public void rollbackBb() {
// 根据日志回滚bb操作
}
}
方案4:本地消息表(较简单)
@Service
@Transactional
public class AService {
public void aa() {
// 1. 记录本地事务消息
LocalTransactionMessage message = new LocalTransactionMessage();
message.setStatus("PENDING");
localMessageRepo.save(message);
// 2. 调用B服务
bServiceClient.bb(message.getId());
// 3. 调用C服务
try {
cServiceClient.cc();
message.setStatus("SUCCESS");
} catch (Exception e) {
message.setStatus("FAILED");
// 通过定时任务或手动触发回滚B服务
bServiceClient.compensate(message.getId());
throw e;
}
}
}
方案比较
|
方案 |
复杂度 |
一致性 |
性能 |
适用场景 |
|---|---|---|---|---|
|
Seata |
中 |
强一致 |
中 |
金融、订单等强一致场景 |
|
消息队列 |
中 |
最终一致 |
高 |
异步、高并发场景 |
|
SAGA |
低 |
最终一致 |
中 |
业务逻辑简单,可补偿 |
|
本地消息表 |
中 |
最终一致 |
中 |
有本地数据库,可接受延迟 |
最简单方案推荐
如果你想要最简单的实现,推荐使用方案3的SAGA模式:
// 最简单的实现示例
@Service
public class AService {
@Transactional(rollbackFor = Exception.class)
public void aa() {
// 1. 先执行本地事务
// ... 本地数据库操作
// 2. 调用B服务
boolean bSuccess = false;
String transactionId = null;
try {
transactionId = bServiceClient.bb();
bSuccess = true;
} catch (Exception e) {
throw new RuntimeException("B服务调用失败", e);
}
// 3. 调用C服务
try {
cServiceClient.cc();
} catch (Exception e) {
// 如果C失败,回滚B
if (bSuccess && transactionId != null) {
bServiceClient.compensate(transactionId);
}
throw new RuntimeException("C服务调用失败,已回滚B", e);
}
}
}
B服务实现补偿接口:
@RestController
public class BController {
@PostMapping("/bb")
public String bb() {
String txId = generateTxId();
// 保存原始数据和事务ID
transactionLogRepo.save(txId, getDataToUpdate());
// 执行更新,但标记为待确认
dataRepo.updateWithHold(txId);
return txId;
}
@PostMapping("/compensate/{txId}")
public void compensate(@PathVariable String txId) {
// 根据txId回滚
Data original = transactionLogRepo.get(txId);
dataRepo.rollback(txId, original);
}
}
这种方案不需要额外中间件,实现简单,但需要业务层面对可补偿性有清晰设计。
更多推荐
所有评论(0)