1. 概述
Seata主要由三个重要组件组成:
TC:Transaction Coordinator 事务协调器,管理全局和分支事务的状态,用于全局性事务的提交和回滚。
TM:Transaction Manager 事务管理器,用于开启、提交或者回滚全局事务。
RM:Resource Manager 资源管理器,用于分支事务上的资源管理,向TC注册分支事务,上报分支事务的状态,接受TC的命令来提交或者回滚分支事务。
Seata的执行流程如下:
-
A服务的TM向TC申请开启一个全局事务,TC就会创建一个全局事务并返回一个唯一的XID
-
A服务的RM向TC注册分支事务,并将其纳入XID对应全局事务的管辖
-
A服务执行分支事务,向数据库做操作
-
A服务开始远程调用B服务,此时XID会在微服务的调用链上传播
-
B服务的RM向TC注册分支事务,并将其纳入XID对应的全局事务的管辖
-
B服务执行分支事务,向数据库做操作
-
全局事务调用链处理完毕,TM根据有无异常向TC发起全局事务的提交或者回滚
-
TC协调其管辖之下的所有分支事务, 决定是否回滚
2. 环境准备
2.1 数据库操作
2.1.1 seata-server数据库表
GitHub建表sql:点击查看
CREATE DATABASE seata_server;
use seata_server;
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;
2.1.2 新建业务数据库
/* ============ seata_account ============= */
CREATE DATABASE seata_account;
use seata_account;
DROP TABLE IF EXISTS `account`;
CREATE TABLE `account` (
`id` int(11) NOT NULL AUTO_INCREMENT,
`balance` decimal(15, 2) COMMENT '余额' ,
PRIMARY KEY (`id`) USING BTREE
) ENGINE = InnoDB AUTO_INCREMENT = 2 CHARACTER SET = latin1 COLLATE = latin1_swedish_ci ROW_FORMAT = Dynamic;
INSERT INTO `account` VALUES (1, 100.00);
/* ============ seata_account ============= */
/* ============ seata_order ============= */
CREATE DATABASE seata_order;
use seata_order;
DROP TABLE IF EXISTS `order`;
CREATE TABLE `order` (
`id` int(11) NOT NULL AUTO_INCREMENT,
`product_name` varchar(255) COMMENT '商品名称',
PRIMARY KEY (`id`) USING BTREE
) ENGINE = InnoDB AUTO_INCREMENT = 2 CHARACTER SET = latin1 COLLATE = latin1_swedish_ci ROW_FORMAT = Dynamic;
INSERT INTO `order` VALUES (1, '手机');
/* ============ seata_order ============= */
/* ============ seata_storage ============= */
CREATE DATABASE seata_storage;
use seata_storage;
DROP TABLE IF EXISTS `storage`;
CREATE TABLE `storage` (
`id` int(11) NOT NULL AUTO_INCREMENT,
`stocks` int(255) NULL DEFAULT NULL COMMENT '剩余库存',
PRIMARY KEY (`id`) USING BTREE
) ENGINE = InnoDB AUTO_INCREMENT = 2 CHARACTER SET = latin1 COLLATE = latin1_swedish_ci ROW_FORMAT = Dynamic;
INSERT INTO `storage` VALUES (1, 10);
/* ============ seata_storage ============= */
2.1.3 在每个业务数据库添加回滚日志表undo_log
建表sql:http://seata.io/zh-cn/docs/dev/mode/at-mode.html最下面
CREATE TABLE `undo_log` (
`id` bigint(20) NOT NULL AUTO_INCREMENT,
`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 AUTO_INCREMENT=1 DEFAULT CHARSET=utf8;
2.1.4 数据库最终结构
2.2 seata-server集群(docker方式)
2.2.1 下载运行镜像
# 下载seata-server镜像
docker pull seataio/seata-server:1.3.0
# 运行镜像
# -p 这里宿主端口和容器端口都要改,否则会按8091去访问,集群可能会出问题
# -h 宿主IP地址,不用用127.0.0.1
# -e SERVER_NODE 在多个 TC Server 时,需区分各自节点,用于生成不同区间的 transactionId 事务编号,以免冲突
# -e SEATA_IP 跟 -h保持一致即可
# -e SEATA_PORT跟 -p保持一致即可
docker run \
--name c_seata1 \
-d \
-p 18091:18091 \
-h 192.168.23.131 \
-e SERVER_NODE=1 \
-e SEATA_IP=192.168.23.131 \
-e SEATA_PORT=18091 \
seataio/seata-server:1.3.0
# 进入容器,因为1.3.0版本没有bash,所以这里用sh代替
docker exec -it c_seata1 /bin/sh
#sh有自带的vi命令,而bash需要自己下载vim命令
#apt-get update
#apt-get install -y vim
2.2.2 修改resources/file.conf
vi resources/file.conf
#vim resources/file.conf
2.2.3 修改resources/registry.conf
vi resources/registry.conf
#vim resources/registry.conf
# 退出镜像
exit
# 重启镜像
docker restart c_seata1
2.2.4 设置第二个seata-server
# 运行镜像
docker run \
--name c_seata2 \
-d \
-p 28091:28091 \
-h 192.168.23.131 \
-e SERVER_NODE=2 \
-e SEATA_IP=192.168.23.131 \
-e SEATA_PORT=28091 \
seataio/seata-server:1.3.0
# 进入容器,因为1.3.0版本没有bash,所以这里用sh代替
docker exec -it c_seata2 /bin/sh
剩余操作跟上面的第一个seata-server一样
2.2.5 查看Nacos控制台服务列表
2.3 seata-server集群(Linux方式)
file.conf和registry.conf文件的修改跟2.2一样的
下载地址:https://github.com/seata/seata/releases/tag/v1.3.0
# 创建一个soft文件夹存放tar包
# 把下载的tar包上传到Linux的soft文件夹下
[root@localhost /]# mkdir /soft
[root@localhost /]# cd soft
# 解压
[root@localhost soft]# tar -zxvf seata-server-1.3.0.tar.gz
# 复制seata到/usr/local/seata1
[root@localhost soft]# cp -rf seata /usr/local/seata1
# 进入seata1的conf目录
[root@localhost seata1]# cd /usr/local/seata1/conf
# 修改file.conf配置文件
[root@localhost conf]# vim file.conf
# 修改registry.conf配置文件
[root@localhost conf]# vim registry.conf
# 把seata1复制一份
[root@localhost conf]# cp -rf /usr/local/seata1 /usr/local/seata2
# 启动seata1
# -h 这里需要设置虚拟机的IP地址,不能设置为127.0.0.1
# -p 避免端口冲突
# -n Server node,在多个 TC Server 时,需区分各自节点,用于生成不同区间的 transactionId 事务编号,以免冲突
/usr/local/seata1/bin/seata-server.sh -h 192.168.23.131 -p 18091 -n 1
# 启动seata2
/usr/local/seata2/bin/seata-server.sh -h 192.168.23.131 -p 28091 -n 2
2.4 Seata与SpringCloudAlibaba版本对应关系
父pom
<dependencyManagement>
<dependencies>
<dependency>
<groupId>com.alibaba.cloud</groupId>
<artifactId>spring-cloud-alibaba-dependencies</artifactId>
<version>2.1.4.RELEASE</version>
<type>pom</type>
<scope>import</scope>
</dependency>
</dependencies>
</dependencyManagement>
子pom
<dependency>
<groupId>com.alibaba.cloud</groupId>
<artifactId>spring-cloud-starter-alibaba-seata</artifactId>
<exclusions>
<exclusion>
<groupId>io.seata</groupId>
<artifactId>seata-spring-boot-starter</artifactId>
</exclusion>
</exclusions>
</dependency>
<!-- 指定1.3.0版本 -->
<dependency>
<groupId>io.seata</groupId>
<artifactId>seata-spring-boot-starter</artifactId>
<version>1.3.0</version>
</dependency>
3. Seata客户端应用
3.1 cloudalibaba-seata-storage9021
3.1.1 pom文件
<dependencies>
<dependency>
<groupId>com.alibaba.cloud</groupId>
<artifactId>spring-cloud-starter-alibaba-seata</artifactId>
<exclusions>
<exclusion>
<groupId>io.seata</groupId>
<artifactId>seata-spring-boot-starter</artifactId>
</exclusion>
</exclusions>
</dependency>
<dependency>
<groupId>io.seata</groupId>
<artifactId>seata-spring-boot-starter</artifactId>
<version>1.3.0</version>
</dependency>
<dependency>
<groupId>com.alibaba.cloud</groupId>
<artifactId>spring-cloud-starter-alibaba-nacos-discovery</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-jdbc</artifactId>
</dependency>
<dependency>
<groupId>mysql</groupId>
<artifactId>mysql-connector-java</artifactId>
</dependency>
<dependency>
<groupId>org.mybatis.spring.boot</groupId>
<artifactId>mybatis-spring-boot-starter</artifactId>
<version>2.1.0</version>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-devtools</artifactId>
<scope>runtime</scope>
<optional>true</optional>
</dependency>
</dependencies>
3.1.2 yml文件
server:
port: 9021
spring:
application:
name: seata-storage-service
datasource:
url: jdbc:mysql://192.168.23.131:3306/seata_storage?useUnicode=true&characterEncoding=UTF-8&serverTimezone=UTC&useSSL=false
username: root
password: root
driverClassName: com.mysql.jdbc.Driver
cloud:
nacos:
discovery:
server-addr: 192.168.23.131:8848 #Nacos服务注册中心地址
seata:
enabled: true
application-id: ${spring.application.name}
tx-service-group: seata_group
service:
vgroup-mapping:
seata_group: default
# seata单机版
# grouplist:
# default: 192.168.23.131:18091
# seata集群版
registry:
type: nacos
nacos:
application: seata-server # 通过微服务集群方式访问seata
server-addr: 192.168.23.131:8848
cluster: default
group: SEATA_GROUP
3.1.3 启动类
package com.springcloud;
import org.mybatis.spring.annotation.MapperScan;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.cloud.client.discovery.EnableDiscoveryClient;
@SpringBootApplication
@EnableDiscoveryClient // 启用Nacos客户端
@MapperScan("com.springcloud.dao") // Mybatis的Dao接口扫描
public class SeataStorageMain9021 {
public static void main(String[] args) {
SpringApplication.run(SeataStorageMain9021.class,args);
}
}
3.1.4 Dao
package com.springcloud.dao;
import org.apache.ibatis.annotations.Param;
import org.apache.ibatis.annotations.Update;
public interface StorageDao {
// 减库存
@Update("update storage set stocks=stocks-#{count} where id=#{id}")
int decrease(@Param("id") int id,@Param("count") int count);
}
3.1.5 Service
package com.springcloud.service;
public interface StorageService {
int decrease( int id,int count);
}
package com.springcloud.service.impl;
import com.springcloud.dao.StorageDao;
import com.springcloud.service.StorageService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
@Service
public class StorageServiceImpl implements StorageService {
@Autowired
private StorageDao storageDao;
@Override
public int decrease(int id, int count) {
return storageDao.decrease(id,count);
}
}
3.1.6 Controller
package com.springcloud.controller;
import com.springcloud.service.StorageService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.*;
@RestController
@RequestMapping("storage")
public class StorageController {
@Autowired
private StorageService storageService;
@GetMapping("/decrease")
public String decrease(@RequestParam("id") int id,@RequestParam("count") int count){
int result=storageService.decrease(id,count);
// int i=10/0;
return result==0?"库存操作失败":"减库存成功";
}
}
3.2 cloudalibaba-seata-account9022
几乎跟storage9021一样,此处省略
3.3 cloudalibaba-seata-order9023
3.3.1 pom文件
<dependencies>
<dependency>
<groupId>com.alibaba.cloud</groupId>
<artifactId>spring-cloud-starter-alibaba-seata</artifactId>
<exclusions>
<exclusion>
<groupId>io.seata</groupId>
<artifactId>seata-spring-boot-starter</artifactId>
</exclusion>
</exclusions>
</dependency>
<dependency>
<groupId>io.seata</groupId>
<artifactId>seata-spring-boot-starter</artifactId>
<version>1.3.0</version>
</dependency>
<dependency>
<groupId>com.alibaba.cloud</groupId>
<artifactId>spring-cloud-starter-alibaba-nacos-discovery</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-openfeign</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-jdbc</artifactId>
</dependency>
<dependency>
<groupId>mysql</groupId>
<artifactId>mysql-connector-java</artifactId>
</dependency>
<dependency>
<groupId>org.mybatis.spring.boot</groupId>
<artifactId>mybatis-spring-boot-starter</artifactId>
<version>2.1.0</version>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-devtools</artifactId>
<scope>runtime</scope>
<optional>true</optional>
</dependency>
</dependencies>
3.3.2 yml文件
server:
port: 9023
spring:
application:
name: seata-order-service
datasource:
url: jdbc:mysql://192.168.23.131:3306/seata_order?useUnicode=true&characterEncoding=UTF-8&serverTimezone=UTC&useSSL=false
username: root
password: root
driverClassName: com.mysql.jdbc.Driver
cloud:
nacos:
discovery:
server-addr: 192.168.23.131:8848 #Nacos服务注册中心地址
seata:
enabled: true
application-id: ${spring.application.name}
tx-service-group: seata_group
service:
vgroup-mapping:
seata_group: default
# grouplist:
# default: 192.168.23.131:18091
registry:
type: nacos
nacos:
application: seata-server # 通过微服务集群方式访问seata
server-addr: 192.168.23.131:8848
userName: ""
password: ""
cluster: default
3.3.3 启动类
package com.springcloud;
import org.mybatis.spring.annotation.MapperScan;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.cloud.client.discovery.EnableDiscoveryClient;
import org.springframework.cloud.openfeign.EnableFeignClients;
@SpringBootApplication
@EnableDiscoveryClient // Nacos
@EnableFeignClients // OpenFeign
@MapperScan("com.springcloud.dao") // Mybatis
public class SeataOrderMain9023 {
public static void main(String[] args) {
SpringApplication.run(SeataOrderMain9023.class,args);
}
}
3.3.4 Dao
package com.springcloud.dao;
import org.apache.ibatis.annotations.Insert;
import org.apache.ibatis.annotations.Param;
import org.apache.ibatis.annotations.Update;
public interface OrderDao {
// 下订单
@Insert("insert into `order` values (null,#{product_name})")
int create(String product_name);
}
3.3.5 Feign
package com.springcloud.feign;
import org.springframework.cloud.openfeign.FeignClient;
import org.springframework.stereotype.Component;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestParam;
@Component
@FeignClient(value = "seata-storage-service")
public interface StorageFeign {
@GetMapping("/storage/decrease")
// OpenFeign访问多个参数的请求时需要加上@RequestParam
public String decrease(@RequestParam("id") int id,@RequestParam("count") int count);
}
package com.springcloud.feign;
import org.springframework.cloud.openfeign.FeignClient;
import org.springframework.stereotype.Component;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestParam;
@Component
@FeignClient(value = "seata-account-service")
public interface AccountFeign {
@GetMapping("/account/decrease")
public String decrease(@RequestParam("id") int id,@RequestParam("amount") int amount);
}
3.3.6 Service
package com.springcloud.service;
public interface OrderService {
int create(String product_name);
}
package com.springcloud.service.impl;
import com.springcloud.dao.OrderDao;
import com.springcloud.feign.AccountFeign;
import com.springcloud.feign.StorageFeign;
import com.springcloud.service.OrderService;
import io.seata.spring.annotation.GlobalTransactional;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
@Service
public class OrderServiceImpl implements OrderService {
@Autowired
private OrderDao orderDao;
@Autowired
private AccountFeign accountFeign;
@Autowired
private StorageFeign storageFeign;
@Override
@GlobalTransactional // 分布式事务
public int create(String product_name){
// 为了方便测试,数据随便写
orderDao.create(product_name);
storageFeign.decrease(1,1);
accountFeign.decrease(1,10);
return 1;
}
}
3.3.6 Controller
package com.springcloud.controller;
import com.springcloud.service.OrderService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
@RestController
@RequestMapping("order")
public class OrderController {
@Autowired
private OrderService orderService;
@GetMapping("/create")
public String create(String product_name){
int result= orderService.create(product_name);
return result==0?"订单操作失败":"下单成功";
}
}