官网
下载地址
在这里下载
我使用的是1.4.1版本
懒得开虚拟机了,所以我下载的是.zip文件,其实使用起来没啥区别,在Windows上启动使用seata-server的批处理文件,在Linux上是使用.sh文件,就这点区别
配置
下载好后,我们看一下seata/conf目录下的文件
这里有两个README,其中这个-zh的是中文的,我们打开看看里边写的内容。
# 脚本说明
## [client](https://github.com/seata/seata/tree/develop/script/client)
> 存放用于客户端的配置和SQL
- at: AT模式下的 `undo_log` 建表语句
- conf: 客户端的配置文件
- saga: SAGA 模式下所需表的建表语句
- spring: SpringBoot 应用支持的配置文件
## [server](https://github.com/seata/seata/tree/develop/script/server)
> 存放server侧所需SQL和部署脚本
- db: server 侧的保存模式为 `db` 时所需表的建表语句
- docker-compose: server 侧通过 docker-compose 部署的脚本
- helm: server 侧通过 Helm 部署的脚本
- kubernetes: server 侧通过 Kubernetes 部署的脚本
## [config-center](https://github.com/seata/seata/tree/develop/script/config-center)
> 用于存放各种配置中心的初始化脚本,执行时都会读取 `config.txt`配置文件,并写入配置中心
- nacos: 用于向 Nacos 中添加配置
- zk: 用于向 Zookeeper 中添加配置,脚本依赖 Zookeeper 的相关脚本,需要手动下载;ZooKeeper相关的配置可以写在 `zk-params.txt` 中,也可以在执行的时候输入
- apollo: 向 Apollo 中添加配置,Apollo 的地址端口等可以写在 `apollo-params.txt`,也可以在执行的时候输入
- etcd3: 用于向 Etcd3 中添加配置
- consul: 用于向 consul 中添加配置
我这里使用的是客户端AT模式,服务端db模式,MySQL。
所以我们得到服务端的sql脚本如下:
-- -------------------------------- The script used when storeMode is 'db' --------------------------------
-- the table to store GlobalSession data
CREATE TABLE IF NOT EXISTS `global_table`
(
`xid` VARCHAR(128) NOT NULL,
`transaction_id` BIGINT,
`status` TINYINT NOT NULL,
`application_id` VARCHAR(32),
`transaction_service_group` VARCHAR(32),
`transaction_name` VARCHAR(128),
`timeout` INT,
`begin_time` BIGINT,
`application_data` VARCHAR(2000),
`gmt_create` DATETIME,
`gmt_modified` DATETIME,
PRIMARY KEY (`xid`),
KEY `idx_gmt_modified_status` (`gmt_modified`, `status`),
KEY `idx_transaction_id` (`transaction_id`)
) ENGINE = InnoDB
DEFAULT CHARSET = utf8;
-- the table to store BranchSession data
CREATE TABLE IF NOT EXISTS `branch_table`
(
`branch_id` BIGINT NOT NULL,
`xid` VARCHAR(128) NOT NULL,
`transaction_id` BIGINT,
`resource_group_id` VARCHAR(32),
`resource_id` VARCHAR(256),
`branch_type` VARCHAR(8),
`status` TINYINT,
`client_id` VARCHAR(64),
`application_data` VARCHAR(2000),
`gmt_create` DATETIME(6),
`gmt_modified` DATETIME(6),
PRIMARY KEY (`branch_id`),
KEY `idx_xid` (`xid`)
) ENGINE = InnoDB
DEFAULT CHARSET = utf8;
-- the table to store lock data
CREATE TABLE IF NOT EXISTS `lock_table`
(
`row_key` VARCHAR(128) NOT NULL,
`xid` VARCHAR(128),
`transaction_id` BIGINT,
`branch_id` BIGINT NOT NULL,
`resource_id` VARCHAR(256),
`table_name` VARCHAR(32),
`pk` VARCHAR(36),
`gmt_create` DATETIME,
`gmt_modified` DATETIME,
PRIMARY KEY (`row_key`),
KEY `idx_branch_id` (`branch_id`)
) ENGINE = InnoDB
DEFAULT CHARSET = utf8;
然后创建一个服务端的数据库seata,执行上述脚本,得到数据库
得到的客户端sql脚本:
CREATE TABLE IF NOT EXISTS `undo_log`
(
`branch_id` BIGINT NOT NULL COMMENT 'branch transaction id',
`xid` VARCHAR(128) NOT NULL COMMENT 'global transaction id',
`context` VARCHAR(128) NOT NULL COMMENT 'undo_log context,such as serialization',
`rollback_info` LONGBLOB NOT NULL COMMENT 'rollback info',
`log_status` INT(11) NOT NULL COMMENT '0:normal status,1:defense status',
`log_created` DATETIME(6) NOT NULL COMMENT 'create datetime',
`log_modified` DATETIME(6) NOT NULL COMMENT 'modify datetime',
UNIQUE KEY `ux_undo_log` (`xid`, `branch_id`)
) ENGINE = InnoDB
AUTO_INCREMENT = 1
DEFAULT CHARSET = utf8 COMMENT ='AT transaction mode undo table';
我们这里创建三个客户端的数据库seata_account、seata_order和seata_storage,然后分别在这三个数据库中执行上述脚本
以上就是使用seata时必要的数据库配置
接下来我们看readme文档中的第三项,config.txt
这一项是用于向配置中心添加配置时使用的,我这里使用的nacos,并且以python的方式执行脚本,所以我们在得到config.txt之后选择的是nacos/nacos-config.py
下载后,修改config.txt中的store.db.url项,使之指向刚才创建的seata数据库
然后修改nacos-config.py,将画框位置的值指向自己config.txt所在位置
然后在命令窗口中使用python命令执行nacos-config.py脚本:
python nacos-config.py
执行结束后,我们打开nacos的控制台,会生成一大堆这样的配置
我们再回到seata/conf目录中,修改file.conf和registry.conf配置文件(是的,配置非常繁琐)
先看file.conf
我们这里使用的是db模式,所以mode的值改为db
然后在db的配置中,将url、user和password的值,改为我们刚才创建的seata数据库的
接着我们看registry.conf文件
我们这里是使用nacos,所以将registry项中的mode值设置为nacos,而对应的nacos项中的地址,改为自己nacos服务的地址
到这里还没完,往下还有一个config项
这一项也是,mode改为nacos,然后对应的nacos中的地址改为自己nacos服务的地址
到这里,服务端的配置算完事了,接着启动seata服务,启动之后没有报错就OK了
创建客户端
这里我们创建三个服务,分别是order,account和storage
我们要做的测试流程是,order服务接受一个订单,在order数据库中插入一条订单信息,然后order服务调用account服务,在账户中修改已使用的金额和剩余金额,返回后再调用storage服务,修改库存使用量和剩余量
创建order服务
先在我们之前创建的seata_order数据库中创建订单表
CREATE TABLE `t_order` (
`id` bigint(11) NOT NULL AUTO_INCREMENT,
`user_id` bigint(11) DEFAULT NULL COMMENT '用户id',
`product_id` bigint(11) DEFAULT NULL COMMENT '产品ID',
`count` int(11) DEFAULT NULL COMMENT '产品数量',
`money` decimal(11,0) DEFAULT NULL COMMENT '金额',
`status` int(1) DEFAULT NULL COMMENT '订单状态 0创建中 1已完结',
PRIMARY KEY (`id`)
) ENGINE=InnoDB AUTO_INCREMENT=6 DEFAULT CHARSET=utf8;
然后创建订单的微服务,结构如下:
依赖:
<?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>
<parent>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-parent</artifactId>
<version>2.2.4.RELEASE</version>
<relativePath/> <!-- lookup parent from repository -->
</parent>
<groupId>com.czz</groupId>
<artifactId>seata-order</artifactId>
<version>0.0.1-SNAPSHOT</version>
<name>seata-order</name>
<description>Demo project for Spring Boot</description>
<properties>
<java.version>1.8</java.version>
</properties>
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency>
<groupId>org.mybatis.spring.boot</groupId>
<artifactId>mybatis-spring-boot-starter</artifactId>
<version>2.1.4</version>
</dependency>
<dependency>
<groupId>com.alibaba.cloud</groupId>
<artifactId>spring-cloud-starter-alibaba-nacos-discovery</artifactId>
<version>2.2.5.RELEASE</version>
</dependency>
<dependency>
<groupId>com.alibaba.cloud</groupId>
<artifactId>spring-cloud-starter-alibaba-nacos-config</artifactId>
<version>2.2.5.RELEASE</version>
</dependency>
<dependency>
<groupId>com.alibaba.cloud</groupId>
<artifactId>spring-cloud-starter-alibaba-seata</artifactId>
<exclusions>
<exclusion>
<groupId>io.seata</groupId>
<artifactId>seata-all</artifactId>
</exclusion>
<exclusion>
<groupId>io.seata</groupId>
<artifactId>seata-spring-boot-starter</artifactId>
</exclusion>
</exclusions>
</dependency>
<dependency>
<groupId>io.seata</groupId>
<artifactId>seata-all</artifactId>
<version>1.4.1</version>
</dependency>
<dependency>
<groupId>io.seata</groupId>
<artifactId>seata-spring-boot-starter</artifactId>
<version>1.4.1</version>
</dependency>
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-openfeign</artifactId>
<version>3.0.3</version>
</dependency>
<dependency>
<groupId>mysql</groupId>
<artifactId>mysql-connector-java</artifactId>
<scope>runtime</scope>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
<scope>test</scope>
<exclusions>
<exclusion>
<groupId>org.junit.vintage</groupId>
<artifactId>junit-vintage-engine</artifactId>
</exclusion>
</exclusions>
</dependency>
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-openfeign-core</artifactId>
<version>2.2.1.RELEASE</version>
<scope>compile</scope>
</dependency>
</dependencies>
<dependencyManagement>
<dependencies>
<dependency>
<groupId>com.alibaba.cloud</groupId>
<artifactId>spring-cloud-alibaba-dependencies</artifactId>
<version>2.2.1.RELEASE</version>
<type>pom</type>
<scope>import</scope>
</dependency>
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-dependencies</artifactId>
<version>Hoxton.SR1</version>
<type>pom</type>
<scope>import</scope>
</dependency>
</dependencies>
</dependencyManagement>
<build>
<plugins>
<plugin>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-maven-plugin</artifactId>
</plugin>
</plugins>
</build>
</project>
bootstrap.yml:nacos的配置中心必须放在bootstrap.yml中配置,索性我就把nacos的配置都放进来了
spring:
cloud:
nacos:
discovery:
server-addr: localhost:8848
config:
server-addr: localhost:8848
file-extension: yaml
profiles:
active: dev
application.yml:这其中的数据库链接配置和mybatis配置不是咱们这篇文章的重点,主要是cloud下边的这些,tx-service-group: my_test_tx_group这一项的值之所以是my_test_tx_group,这个不是我们随便写的,还记得上边我们讲过的config.txt么,这其中有一项service.vgroupMapping.my_test_tx_group=default,看到这其中的关系了把,也就是说vgroupMapping这后边是什么值,我们在yml里就得配置什么值;
server:
port: 8041
spring:
application:
name: seata-order
cloud:
alibaba:
seata:
tx-service-group: my_test_tx_group
# service:
# vgroup-mapping:
# my_test_tx_group: default
datasource:
url: jdbc:mysql://192.168.0.100:3306/seata_order?useUnicode=true&characterEncoding=utf8&serverTimezone=UTC
username: root
password: 123456
driver-class-name: com.mysql.cj.jdbc.Driver
mybatis:
mapper-locations: classpath:mapper/*Mapper.xml
type-aliases-package: com.czz.seataorder.entity
启动类:
@MapperScan("**.dao.**")
@EnableFeignClients
@EnableDiscoveryClient
@SpringBootApplication
public class SeataOrderApplication {
public static void main(String[] args) {
SpringApplication.run(SeataOrderApplication.class, args);
}
}
dao层就不说了,反正是往t_order表里插入一条数据
AccountFeignClient和StorageFeignClient
@FeignClient(name = "seata-account")
public interface AccountFeignClient {
@GetMapping("/reduce")
Boolean reduce(@RequestParam("userId") Long userId, @RequestParam("money")BigDecimal money);
}
@FeignClient(name = "seata-storage")
public interface StorageFeignClient {
@GetMapping("/storage/deduct")
Boolean deduct(@RequestParam("productId")Long productId, @RequestParam("count")int count);
}
要注意feign的接口要和对应服务接口的请求地址,参数列表一致,这个不是本文的重点,但还是需要提一句。
service层:@GlobalTransactional意味着全局事务的开始,在一次事务的调用链中只能有一个这个注解出现,并且在事务链中的所有子事务都必须带有 @Transactional 注解
@Service
public class OrderService {
@Autowired
private IOrderDao iOrderDao;
@Autowired
private AccountFeignClient accountFeignClient;
@Autowired
private StorageFeignClient storageFeignClient;
@GlobalTransactional
@Transactional
public void insertOrder(Long userId, Long productId, int count){
double singlePrice = 0.5;
BigDecimal bigDecimal = new BigDecimal(singlePrice * count);
Order order = new Order();
order.setUserId(userId);
order.setProductId(productId);
order.setCount(count);
order.setPrice(bigDecimal);
order.setStatus(0);
accountFeignClient.reduce(userId,bigDecimal);
storageFeignClient.deduct(productId, count);
iOrderDao.insert(order);
//int i = 1/0;
}
}
controller层:
@RestController
public class OrderController {
@Autowired
private OrderService orderService;
@GetMapping("/orderSmt")
public String orderSmt(){
orderService.insertOrder(1L,1L,1);
return "done";
}
}
创建account服务
account服务的依赖和yml配置基本和order服务的一直,不同的只有服务名称和mybatis的配置,在这里我就不赘述了。
在seata-account库中创建t_account表:
CREATE TABLE `t_account` (
`id` bigint(11) NOT NULL AUTO_INCREMENT,
`user_id` bigint(11) DEFAULT NULL,
`total` decimal(10,0) DEFAULT NULL,
`used` decimal(10,0) DEFAULT NULL,
`residue` decimal(10,0) DEFAULT '0',
PRIMARY KEY (`id`)
) ENGINE=InnoDB AUTO_INCREMENT=2 DEFAULT CHARSET=utf8;
插入一条数据:
INSERT INTO `t_account` VALUES (1, 1, 1000, 0, 1000);
启动类:
@MapperScan("**.dao.**")
@EnableDiscoveryClient
@EnableFeignClients
@SpringBootApplication
public class SeataAccountApplication {
public static void main(String[] args) {
SpringApplication.run(SeataAccountApplication.class, args);
}
}
service类:这里我们之前说过了,需要在每个子事务中使用 @Transactional注解
@Service
public class AccountService {
@Autowired
private IAccountDao iAccountDao;
@Transactional
public void reduce(Long userId){
iAccountDao.update(userId);
}
}
controller类:
@RestController
public class AccountController {
@Autowired
private AccountService accountService;
@GetMapping("/reduce")
public Boolean reduce(@RequestParam("userId")Long userId, @RequestParam("money")BigDecimal money){
accountService.reduce(userId);
return true;
}
}
创建storage服务
配置不多说了,和上边一样
在seata_storage库中创建t_storage表
CREATE TABLE `t_storage` (
`id` bigint(11) NOT NULL AUTO_INCREMENT,
`product_id` bigint(11) DEFAULT NULL,
`total` int(11) DEFAULT NULL,
`used` int(11) DEFAULT NULL,
`residue` int(11) DEFAULT NULL COMMENT '剩余库存',
PRIMARY KEY (`id`)
) ENGINE=InnoDB AUTO_INCREMENT=2 DEFAULT CHARSET=utf8;
插入一条数据:
INSERT INTO `t_storage` VALUES (1, 1, 100, 0, 100);
启动类:
@MapperScan("**.dao.**")
@EnableFeignClients
@EnableDiscoveryClient
@SpringBootApplication
public class SeataStorageApplication {
public static void main(String[] args) {
SpringApplication.run(SeataStorageApplication.class, args);
}
}
service类:
@Service
public class StorageService {
@Autowired
private IStorageDao iStorageDao;
@Transactional
public void deduct(Long productId, int count){
iStorageDao.update(productId);
}
}
controller类:
@RestController
public class StorageController {
@Autowired
private StorageService storageService;
@GetMapping("/storage/deduct")
public Boolean deduct(@RequestParam Long productId, @RequestParam int count){
storageService.deduct(productId, count);
return true;
}
}
测试
先测试正常情况,我们在浏览器中输入订单请求地址:http://localhost:8041/orderSmt
没有报错,然后我们看下数据库保存的数据情况(因为我点了好几次,所以数据库里产生了好几条记录):
t_order表
t_account表:
t_storage表:
以上可以看到在正常情况下数据落地没有问题,接下来我们测试一下,在全局事务结束前程序报错了,看看事务是否生效
我们修改下order服务的service类,在执行完所有子事务之后,我们抛出个异常,再看看数据是否有变化
@GlobalTransactional
@Transactional
public void insertOrder(Long userId, Long productId, int count){
double singlePrice = 0.5;
BigDecimal bigDecimal = new BigDecimal(singlePrice * count);
Order order = new Order();
order.setUserId(userId);
order.setProductId(productId);
order.setCount(count);
order.setPrice(bigDecimal);
order.setStatus(0);
accountFeignClient.reduce(userId,bigDecimal);
storageFeignClient.deduct(productId, count);
iOrderDao.insert(order);
int i = 1/0; //我们在这里让程序出个异常
}
重启order服务,然后再调用订单接口
如我们所愿,报错了,接下来看看数据的情况
t_order表:
还是6条
t_account表:
没有变化
t_storage表:
还是没有变化,也就是说,我们的事务时生效了的
以上就是seata的部署及使用的方法
总结
先贴一个官网的架构图
解释一下其中的术语:
TC (Transaction Coordinator) - 事务协调者
维护全局和分支事务的状态,驱动全局事务提交或回滚。
TM (Transaction Manager) - 事务管理器
定义全局事务的范围:开始全局事务、提交或回滚全局事务。
RM (Resource Manager) - 资源管理器
管理分支事务处理的资源,与TC交谈以注册分支事务和报告分支事务的状态,并驱动分支事务提交或回滚。
seata的工作过程大概是这样的,首先是TM开始全局事务,并向TC申请一个事务ID,在向下调用的时候会传播这个事务ID,RM拿到这个事务ID后与TC建立沟通,当所有分支事务全部安全完成时,由TC通知个分支事务的RM统一提交事务;当任一分支事务产生异常时,会向TC报告,然后TC通知所有RM回滚事务