Spring Boot 集成 Seata 解决分布式事务问题 (2)

注:file模式为单机模式,全局事务会话信息内存中读写并持久化本地文件root.data,性能较高;
db模式为高可用模式,全局事务会话信息通过db共享,相应性能差些

可以直接通过bash 脚本启动 Seata Server,也可以通过 Docker 镜像启动,但是 Docker 方式目前只支持使用 file 模式,不支持将 Seata-Server 注册到 Eureka 或 Nacos 等注册中心。

通过脚本启动

在 https://github.com/seata/seata/releases 下载相应版本的 Seata Server,解压后执行以下命令启动,这里使用 file 配置

通过 Docker 启动 docker run --name seata-server -p 8091:8091 seataio/seata-server:latest 项目介绍 项目名 地址 说明
sbm-account-service   127.0.0.1:8081   账户服务  
sbm-order-service   127.0.0.1:8082   订单服务  
sbm-storage-service   127.0.0.1:8083   仓储服务  
sbm-business-service   127.0.0.1:8084   主业务  
seata-server   172.16.2.101:8091   seata-server  
核心代码

为了不让篇幅太长,这里只给出部分代码,详细代码文末会给出源码地址

maven 引入 seata 的依赖 eata-spring-boot-starter

<dependency> <groupId>io.seata</groupId> <artifactId>seata-spring-boot-starter</artifactId> <version>1.0.0</version> </dependency> 仓储服务 application.properties spring.application.name=account-service server.port=8081 spring.datasource.url=jdbc:mysql://172.16.2.101:3306/db_seata?useSSL=false&serverTimezone=UTC spring.datasource.username=root spring.datasource.password=123456 seata.tx-service-group=my_test_tx_group mybatis.mapper-locations=classpath*:mapper/*Mapper.xml seata.service.grouplist=172.16.2.101:8091 logging.level.io.seata=info logging.level.io.seata.samples.account.persistence.AccountMapper=debug StorageService public interface StorageService { /** * 扣除存储数量 */ void deduct(String commodityCode, int count); } 订单服务 public interface OrderService { /** * 创建订单 */ Order create(String userId, String commodityCode, int orderCount); } 帐户服务 public interface AccountService { /** * 从用户账户中借出 */ void debit(String userId, int money); } 主要业务逻辑

只需要使用一个 @GlobalTransactional 注解在业务方法上。

@GlobalTransactional public void purchase(String userId, String commodityCode, int orderCount) { LOGGER.info("purchase begin ... xid: " + RootContext.getXID()); storageClient.deduct(commodityCode, orderCount); orderClient.create(userId, commodityCode, orderCount); } XID 的传递

全局事务ID的跨服务传递,需要我们自己实现,这里通过拦截器的方式。每个服务都需要添加下面两个类。

SeataFilter @Component public class SeataFilter implements Filter { @Override public void init(FilterConfig filterConfig) throws ServletException { } @Override public void doFilter(ServletRequest servletRequest, ServletResponse servletResponse, FilterChain filterChain) throws IOException, ServletException { HttpServletRequest req = (HttpServletRequest) servletRequest; String xid = req.getHeader(RootContext.KEY_XID.toLowerCase()); boolean isBind = false; if (StringUtils.isNotBlank(xid)) { RootContext.bind(xid); isBind = true; } try { filterChain.doFilter(servletRequest, servletResponse); } finally { if (isBind) { RootContext.unbind(); } } } @Override public void destroy() { } } SeataRestTemplateAutoConfiguration @Configuration public class SeataRestTemplateAutoConfiguration { @Autowired( required = false ) private Collection<RestTemplate> restTemplates; @Autowired private SeataRestTemplateInterceptor seataRestTemplateInterceptor; public SeataRestTemplateAutoConfiguration() { } @Bean public SeataRestTemplateInterceptor seataRestTemplateInterceptor() { return new SeataRestTemplateInterceptor(); } @PostConstruct public void init() { if (this.restTemplates != null) { Iterator var1 = this.restTemplates.iterator(); while (var1.hasNext()) { RestTemplate restTemplate = (RestTemplate) var1.next(); List<ClientHttpRequestInterceptor> interceptors = new ArrayList(restTemplate.getInterceptors()); interceptors.add(this.seataRestTemplateInterceptor); restTemplate.setInterceptors(interceptors); } } } } 测试 测试成功场景: curl -X POST :8084/api/business/purchase/commit

此时返回结果为:true

测试失败场景:

UserId 为1002 的用户下单,sbm-account-service会抛出异常,事务会回滚

:8084/api/business/purchase/rollback

此时返回结果为:false

查看 undo_log 的日志或者主键,可以看到在执行过程中有保存数据。
如查看主键自增的值,在执行前后的值会发生变化,在执行前是 1,执行后是 7 。

源码地址

https://github.com/gf-huanchupk/SpringBootLearning/tree/master/springboot-seata

参考

内容版权声明:除非注明,否则皆为本站原创文章。

转载注明出处:https://www.heiqu.com/zyysxw.html