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.sql
和seata-stock.sql
分别建好undo_log
表,若已经建好忽略;
涉及事务的订单服务增加@GlobalTransactional
注解,即在类OrderService
的方法saveSuccess
和saveFailure
上增加;
此时应用可以启动两个应用进行调用,会发现没有效果,原因是通过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-order
及seata-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-order
和seata-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;