分布式事务seta入门案例

1、seata是什么

SEATA(Simple Extensible Autonomous Transaction Architecture)

2、seata-demo编译运行
  • 编译seata
    github下载seata源码,源码地址https://github.com/seata/seata.git
切换到tag v1.4.2最新版本
mvn clean package -Dmaven.test.skip -Prelease-seata
  • 编译seata-samples
    github下载seata-samples源码,源码地址https://github.com/seata/seata-samples.git
mvn clean package -Dmaven.test.skip
若需要打可执行jar包则可进到目录后运行
seata\seata-samples\springcloud-eureka-seata>mvn clean package -Dmaven.test.skip -Dlicense.skip spring-boot:repackage
  • 运行demo
    演示使用springcloud-eureka-seata这个案例,依次启动以下应用

    按照脚本seata-samples\springcloud-eureka-seata\all.sql建好数据数据库表,并建好相应的用户,依次启动以下应用即可演示

1)、eureka
2)、seata-server 目录(打包编译后在目录\seata\distribution\seata-server-1.4.2\bin中seata-server.bat)
3)、account
4)、storage
5)、order
6)、budiness

访问curl http://127.0.0.1:8084/purchase/commit验证结果

3、针对现有spring-boot系统使用seata改造

由于现有系统均由spring-boot开发,并且未对分布式事务进行有效的处理或补偿,基于目前对业务量、并发量的考虑引入seata解决分布式事务的问题,各个服务之间的调用统一使用了Resttemplate实现;

  • 场景案例
    如下单场景中,订单服务(seata-order)调用了库存服务(seata-stock),若扣减库成功了,并将成功结果返回到订单服务,然后订单服务因为某些原因保存订单失败,此时就应该回滚扣减的库存;

  • 改造方案
    涉及到的分布式事务的场景较多,目标是以最小的代价完成升级改造,主要涉及几个点:
    ①引入seata依赖、②增加seata配置、③涉及全局事务的地方使用@GlobalTransactional、④增加resttemplate拦截器(目的是传递全局事务ID)、⑤创建undo_log表;

  • 升级改造前示例(使用spring-boot-2.6.4完成模拟)
    源代码https://gitee.com/viturefree/spring-boot-seata-upgrade对应的v1分支,使用时可分别使用curl -X POST http://localhost:8082/order/true模拟成功或curl -X POST http://localhost:8082/order/false模拟失败,可以看到在有库存的情况下无论成功或失败均扣库存了,我们期望的是若失败应该回滚返回库存;

    改造前的依赖如下


        
            org.springframework.boot
            spring-boot-starter-data-jpa
        
        
            org.springframework.boot
            spring-boot-starter-web
        
        
            com.alibaba
            druid-spring-boot-starter
            1.2.8
        
        
            org.mariadb.jdbc
            mariadb-java-client
            runtime
        
        
            org.projectlombok
            lombok
            true
        
        
            org.springframework.boot
            spring-boot-starter-test
            test
        
    

部分关键代码片断

    @Transactional
    public void saveSuccess(Order order) throws Exception {
        //此处模拟先调用库存服务扣减库存
        useStock();
        orderRepository.save(order);
    }

    @Transactional
    public void saveFailure(Order order) throws Exception {
        //此处模拟先调用库存服务扣减库存
        useStock();
        orderRepository.save(order);
        throw new RuntimeException("模拟保存订单失败");
    }

    private void useStock() {
        restTemplate.postForObject("http://localhost:8083/stock", "", String.class);
    }
    
------
    @Transactional
    public void quota() {
        int result = stockRepository.update(1);
        if(result==0)
            throw new RuntimeException("扣减库存失败");
        log.debug("{}", result);
    }
------
    //扣减库存操作返回操作行数,返回0的时候表示扣减失败了
    @Modifying
    @Query("update Stock s set s.stock=s.stock-10 where s.id=?1 and s.stock>=10")
    int update(int id);
  • 升级使用seata解决分布式事务问题

    引入seata依赖包

        
            io.seata
            seata-spring-boot-starter
            1.4.2
        

application.yaml中增加seata配置(其中有大量配置使用默认配置即可),注意www.mnxyz.zeo.com:8091为seata服务地址;

seata:
  enabled: true
  application-id: ${spring.application.name}
  tx-service-group: minxyz-seata-group
  service:
    vgroup-mapping:
      minxyz-seata-group: minxyz
    grouplist:
      minxyz: www.mnxyz.zeo.com:8091

seata-order.sqlseata-stock.sql分别建好undo_log表,若已经建好忽略;

涉及事务的订单服务增加@GlobalTransactional注解,即在类OrderService的方法saveSuccesssaveFailure上增加;

此时应用可以启动两个应用进行调用,会发现没有效果,原因是通过RestTemplate调用的时候并没有完成全局事务ID的跨服务传递,由于没有使用spring-cloud,如eureka的实现,不然就不需要自行实现拦截器处理事务ID了,更新方便快捷迁移;

可以通过在seata-stock打印请求头信息发现没有传递tx_xid参数

        HttpServletRequest request = ((ServletRequestAttributes) RequestContextHolder.getRequestAttributes()).getRequest();
        Enumeration headers = request.getHeaderNames();
        while (headers.hasMoreElements()) {
            String header = headers.nextElement();
            log.info("{}={}", header, request.getHeader(header));
        }

接下来实现拦截全局事务ID,在案例中是使用RestTemplate调用时需要拦截增加tx_xid

编写seata-resttemplate-spring-boot-starter,假定命名为seata-resttemplate此名字,并在seata-orderseata-stock中引入此依赖,关键代码如下

seata-resttemplate-spring-boot-starter 模块如下
pom.xml >>>>>>


    4.0.0
    com.minxyz
    seata-resttemplate-spring-boot-starter
    0.0.1
    
        1.8
        1.8
        1.8
        UTF-8
    
    
    
        
            io.seata
            seata-spring-boot-starter
            1.4.2
        
    

    
        
            
                org.springframework.boot
                spring-boot-maven-plugin
                
                    
                        
                            org.projectlombok
                            lombok
                        
                    
                
            
        
    



spring.factories >>>>>>
org.springframework.boot.autoconfigure.EnableAutoConfiguration=\
com.minxyz.seata.SeataRestTemplateAutoConfiguration

SeataRestTemplateAutoConfiguration >>>>>>
package com.minxyz.seata;

import io.seata.common.util.StringUtils;
import io.seata.core.context.RootContext;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Configuration;
import org.springframework.http.client.support.HttpRequestWrapper;
import org.springframework.web.client.RestTemplate;

import javax.annotation.PostConstruct;
import java.util.Collection;
import java.util.Iterator;

@Configuration
public class SeataRestTemplateAutoConfiguration {

    @Autowired(required = false)
    private Collection restTemplates;

    public SeataRestTemplateAutoConfiguration() {
    }

    @PostConstruct
    public void init() {
        if (this.restTemplates != null) {
            Iterator it = this.restTemplates.iterator();
            while (it.hasNext()) {
                it.next().getInterceptors().add((request, body, execution) -> {
                    HttpRequestWrapper requestWrapper = new HttpRequestWrapper(request);
                    String xid = RootContext.getXID();
                    if (StringUtils.isNotEmpty(xid)) {
                        requestWrapper.getHeaders().add(RootContext.KEY_XID, xid);
                    }
                    return execution.execute(requestWrapper, body);
                });
            }
        }

    }

}

主要目的就是拦截传递tx_xid这个header参数;

接着在seata-orderseata-stock中依赖些starter

    
        
            com.minxyz
            seata-resttemplate-spring-boot-starter
            0.0.1
        
    

源代码https://gitee.com/viturefree/spring-boot-seata-upgrade对应的v2分支

4、观察undo_log数据

由于undo_log执行时会被清除,可增加一个触发器观察执行的数据;

创建undo_log备份表
CREATE TABLE `undo_log_backup` (
  `id` bigint(20) NOT NULL,
  `branch_id` bigint(20) NOT NULL,
  `xid` varchar(100) NOT NULL,
  `context` varchar(128) NOT NULL,
  `rollback_info` longblob NOT NULL,
  `log_status` int(11) NOT NULL,
  `log_created` datetime NOT NULL,
  `log_modified` datetime NOT NULL,
  PRIMARY KEY (`id`),
  UNIQUE KEY `ux_undo_log` (`xid`,`branch_id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8;

创建触发器将undo_log数据备份
CREATE TRIGGER `undo_log_trigger` AFTER INSERT ON `undo_log` FOR EACH ROW begin 
 insert into undo_log_backup(id,branch_id,xid,context,rollback_info,log_status,log_created,log_modified) 
 values(new.id,new.branch_id,new.xid,new.context,new.rollback_info,new.log_status,new.log_created,new.log_modified); 
end;

你可能感兴趣的:(分布式事务seta入门案例)