针对这种跨服务的事务场景,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);
    }
}

这种方案不需要额外中间件,实现简单,但需要业务层面对可补偿性有清晰设计。

更多推荐