基于Spring Cloud的微服务跨服务事务回滚实现方案咨询
解决跨服务调用后的分布式事务回滚问题
首先得明确:你现在遇到的核心问题是本地事务无法跨服务生效——Application 2的@Transactional只管控自己数据库的操作,一旦它成功保存账户并提交事务,哪怕Application 1后续抛出NullPointerException,也没法让Application 2回滚已经写入的数据。要解决这个问题,你需要引入分布式事务机制,这里推荐用Spring生态友好的Seata来实现强一致性的分布式事务。
具体实现步骤
1. 引入Seata依赖
在两个服务的pom.xml(Maven项目)中添加Seata的Spring Cloud Starter适配依赖:
<dependency> <groupId>io.seata</groupId> <artifactId>seata-spring-boot-starter</artifactId> <version>最新稳定版</version> </dependency> <dependency> <groupId>com.alibaba.cloud</groupId> <artifactId>spring-cloud-starter-alibaba-seata</artifactId> <version>与你的Spring Cloud版本匹配的适配版</version> </dependency>
2. 配置Seata事务协调者(TC)
你需要启动一个Seata Server作为全局事务的协调中心(生产环境推荐独立部署,测试阶段也可以用内嵌模式)。在两个服务的application.yml中添加Seata基础配置:
seata: enabled: true application-id: accounts-service-1 # Application1用这个ID,Application2改成accounts-service-2 tx-service-group: my_custom_tx_group config: type: file # 或nacos等配置中心,根据你的技术栈选择 file: name: file.conf registry: type: nacos # 或eureka,对应你的服务发现组件 nacos: application: seata-server server-addr: 127.0.0.1:8848
3. 改造代码中的事务注解
- Application 1(全局事务发起方):把
WebAccountsService中的@Transactional替换为Seata的@GlobalTransactional,这个注解会开启全局事务,自动协调所有参与的服务:
// 替换原有的@Transactional注解 @GlobalTransactional(rollbackFor = Exception.class) public void createAccount(Account account) { try { account.setOwner(account.getOwner()+System.currentTimeMillis()%1000); HttpEntity<Account> request = new HttpEntity<>(account); System.out.println("==================="+account.getOwner()); String url=serviceUrl + "/create-accounts"; ResponseEntity<String> response = restTemplate.postForEntity( url, request , String.class ); String s = null; s.equalsIgnoreCase("asa"); // 这里触发NPE } catch (NullPointerException e) { throw e; } }
- Application 2(事务参与方):保持原来的
@Transactional即可,Seata会自动将这个本地事务纳入全局事务的管控范围:
@PostMapping(value = "/create-accounts") @Transactional(rollbackFor = Exception.class) public void createAccount(@RequestBody Account account) throws Exception { try { logger.info("========================================================= " + account.getOwner()); accountRepository.save(account); logger.info("accounts-service byNumber() found: " + account); } catch (Exception e) { throw e; } }
4. 配置Seata代理数据源
Seata需要代理你的数据源来实现分支事务的拦截与回滚,所以在两个服务中添加数据源代理配置:
@Configuration public class DataSourceConfig { @Bean @ConfigurationProperties(prefix = "spring.datasource") public DataSource rawDataSource() { return new DruidDataSource(); // 替换为你项目中使用的数据源类型 } @Bean public DataSourceProxy dataSourceProxy(DataSource rawDataSource) { return new DataSourceProxy(rawDataSource); } @Bean public SqlSessionFactory sqlSessionFactory(DataSourceProxy dataSourceProxy) throws Exception { SqlSessionFactoryBean factoryBean = new SqlSessionFactoryBean(); factoryBean.setDataSource(dataSourceProxy); // 保留你原有的MyBatis配置,比如mapperLocations等 return factoryBean.getObject(); } }
方案原理说明
当Application 1的createAccount方法开启全局事务后,调用Application 2接口时,Seata会自动生成分支事务并绑定到全局事务上下文。如果Application 1后续抛出NullPointerException,全局事务会触发回滚指令,Seata会协调Application 2的分支事务回滚已经写入的账户数据,实现跨服务的事务一致性。
备选方案:最终一致性(基于消息队列)
如果你的业务场景允许最终一致性(不需要实时回滚),可以用事务消息实现:
- Application 1先发送一条半事务消息到MQ(如RocketMQ)
- Application 2消费消息并创建账户,成功后通知MQ确认消息
- 如果Application 1后续抛出异常,取消半事务消息;若Application 2已执行,则发送补偿消息让其删除账户
这种方案适合对一致性要求不是强实时的场景,但实现复杂度更高。
内容的提问来源于stack exchange,提问作者Mr Special
相关产品推荐
相关产品推荐

