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.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 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 >>>>>>
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-order
和seata-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;