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模拟失败,可以看到在有库存的情况下无论成功或失败均扣库存了,我们期望的是若失败应该回滚返回库存;

    改造前的依赖如下

<dependencies>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-data-jpa</artifactId>
        </dependency>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-web</artifactId>
        </dependency>
        <dependency>
            <groupId>com.alibaba</groupId>
            <artifactId>druid-spring-boot-starter</artifactId>
            <version>1.2.8</version>
        </dependency>
        <dependency>
            <groupId>org.mariadb.jdbc</groupId>
            <artifactId>mariadb-java-client</artifactId>
            <scope>runtime</scope>
        </dependency>
        <dependency>
            <groupId>org.projectlombok</groupId>
            <artifactId>lombok</artifactId>
            <optional>true</optional>
        </dependency>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-test</artifactId>
            <scope>test</scope>
        </dependency>
    </dependencies>

部分关键代码片断

    @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依赖包

        <dependency>
            <groupId>io.seata</groupId>
            <artifactId>seata-spring-boot-starter</artifactId>
            <version>1.4.2</version>
        </dependency>

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<String> 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 >>>>>>
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
         xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 https://maven.apache.org/xsd/maven-4.0.0.xsd">
    <modelVersion>4.0.0</modelVersion>
    <groupId>com.minxyz</groupId>
    <artifactId>seata-resttemplate-spring-boot-starter</artifactId>
    <version>0.0.1</version>
    <properties>
        <java.version>1.8</java.version>
        <maven.compiler.source>1.8</maven.compiler.source>
        <maven.compiler.target>1.8</maven.compiler.target>
        <encoding>UTF-8</encoding>
    </properties>
	
    <dependencies>
        <dependency>
            <groupId>io.seata</groupId>
            <artifactId>seata-spring-boot-starter</artifactId>
            <version>1.4.2</version>
        </dependency>
    </dependencies>

    <build>
        <plugins>
            <plugin>
                <groupId>org.springframework.boot</groupId>
                <artifactId>spring-boot-maven-plugin</artifactId>
                <configuration>
                    <excludes>
                        <exclude>
                            <groupId>org.projectlombok</groupId>
                            <artifactId>lombok</artifactId>
                        </exclude>
                    </excludes>
                </configuration>
            </plugin>
        </plugins>
    </build>

</project>

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<RestTemplate> restTemplates;

    public SeataRestTemplateAutoConfiguration() {
    }

    @PostConstruct
    public void init() {
        if (this.restTemplates != null) {
            Iterator<RestTemplate> 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

    <dependencies>
        <dependency>
            <groupId>com.minxyz</groupId>
            <artifactId>seata-resttemplate-spring-boot-starter</artifactId>
            <version>0.0.1</version>
        </dependency>
    </dependencies>

源代码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;


赞赏(Donation)
微信(Wechat Pay)

donation-wechatpay