3.Seata
3.1.介绍
Seata(Simple Extensible Autonomous Transaction Architecture,简单可扩展自治事务框架)是 2019 年 1 月份蚂蚁金服和阿里巴巴共同开源的分布式事务解决方案。Seata 开源半年左右,目前已经有接近一万 star,社区非常活跃。我们热忱欢迎大家参与到 Seata 社区建设中,一同将 Seata 打造成开源分布式事务标杆产品。
Seata:https://github.com/seata/seata
3.1.1. Seata 产品模块
如下图所示,Seata 中有三大模块,分别是 TM、RM 和 TC。 其中 TM 和 RM 是作为 Seata 的客户端与业务系统集成在一起,TC 作为 Seata 的服务端独立部署。
3.1.2.Seata支持的事务模型
Seata 会有 4 种分布式事务解决方案,分别是 AT 模式、TCC 模式、Saga 模式和 XA 模式。
3.2.AT模式实战
Seata中比较常用的是AT模式,这里我们拿AT模式来做演示,看看如何在SpringCloud微服务中集成Seata.
我们假定一个用户购买商品的业务逻辑。整个业务逻辑由3个微服务提供支持:
- 仓储服务:对给定的商品扣除仓储数量。
- 订单服务:根据采购需求创建订单。
- 帐户服务:从用户帐户中扣除余额。
流程图:
订单服务在下单时,同时调用库存服务和用户服务,此时就会发生跨服务和跨数据源的分布式事务问题。
3.2.1.准备数据
执行资料中提供的seata_demo.sql
文件,导入数据。
其中包含4张表。
Order表:
CREATE TABLE `order_tbl` (
`id` int(11) NOT NULL AUTO_INCREMENT,
`user_id` varchar(255) DEFAULT NULL COMMENT '用户id',
`commodity_code` varchar(255) DEFAULT NULL COMMENT '商品码',
`count` int(11) unsigned DEFAULT '0' COMMENT '购买数量',
`money` int(11) unsigned DEFAULT '0' COMMENT '总金额',
PRIMARY KEY (`id`) USING BTREE
) ENGINE=InnoDB DEFAULT CHARSET=utf8 ROW_FORMAT=COMPACT;
商品库存表:
CREATE TABLE `storage_tbl` (
`id` int(11) NOT NULL AUTO_INCREMENT,
`commodity_code` varchar(255) DEFAULT NULL COMMENT '商品码',
`count` int(11) unsigned DEFAULT '0' COMMENT '商品库存',
PRIMARY KEY (`id`) USING BTREE,
UNIQUE KEY `commodity_code` (`commodity_code`) USING BTREE
) ENGINE=InnoDB AUTO_INCREMENT=2 DEFAULT CHARSET=utf8 ROW_FORMAT=COMPACT;
用户账户表:
CREATE TABLE `account_tbl` (
`id` int(11) NOT NULL AUTO_INCREMENT,
`user_id` varchar(255) DEFAULT NULL COMMENT '用户id',
`money` int(11) unsigned DEFAULT '0' COMMENT '用户余额',
PRIMARY KEY (`id`) USING BTREE
) ENGINE=InnoDB AUTO_INCREMENT=2 DEFAULT CHARSET=utf8 ROW_FORMAT=COMPACT;
还有用来记录Seata中的事务日志表undo_log,其中会包含after_image
和before_image
数据,用于数据回滚:
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,
`ext` varchar(100) DEFAULT NULL,
PRIMARY KEY (`id`) USING BTREE,
UNIQUE KEY `ux_undo_log` (`xid`,`branch_id`) USING BTREE
) ENGINE=InnoDB DEFAULT CHARSET=utf8 ROW_FORMAT=COMPACT;
3.2.2.引入Demo工程
我们先准备基本的项目环境,实现下单的业务代码
https://altwongblog-1301531589.cos.ap-shanghai.myqcloud.com//2021/202105/%E8%B5%84%E6%96%99_1620558385445.zip
导入项目
使用Idea打开资料中提供的 seata-demo项目:
找到项目所在目录,选中并打开:
项目结构如下:
结构说明:
- account-service:用户服务,提供操作用户账号余额的功能,端口8083
- eureka-server:注册中心,端口8761
- order-service:订单服务,提供根据数据创建订单的功能,端口8082
- storage-service:仓储服务,提供扣减商品库存功能,端口8081
测试事务
接下来,我们来测试下分布式事务的现象。
下单的接口是:
- 请求方式:POST
- 请求路径:/order
- 请求参数:form表单,包括:
- userId:用户id
- commodityCode:商品码
- count:购买数量
- money:话费金额
- 返回值类型:long,订单的id
原始数据库数据:
余额:
库存:
其它两张表为空。
正常下单
此时启动项目,尝试下单,目前商品库存为10,用户余额为1000,因此只要数据不超过这两个值应该能正常下单。
查看数据库数据:
余额:
库存:
订单:
异常下单
这次,我们把money参数设置为1200,这样就超过了余额最大值,理论上所有数据都应该回滚:
看下用户余额:
因为扣款失败,因此这里没有扣减
来看下库存数据:
这说明扣减库存依然成功,并未回滚!
接下来,我们引入Seata,看看能不能解决这个问题。
3.2.3.准备TC服务
在之前讲解Seata原理的时候,我们就聊过,其中包含重要的3个角色:
- TC:事务协调器
- TM:事务管理器
- RM:资源管理器
其中,TC是一个独立的服务,负责协调各个分支事务,而TM和RM通过jar包的方式,集成在各个事务参与者中。
因此,首先我们需要搭建一个独立的TC服务。
1)安装
首先去官网下载TC的服务端安装包,GitHub的地址:https://github.com/seata/seata/releases
这里我们在资料中提供给大家1.1.0版本的安装包:
然后解压即可,其目录结构如下:
包括:
- bin:启动脚本
- conf:配置文件
- lib:依赖项
2)配置
Seata的核心配置主要是两部分:
- 注册中心的配置:在
${seata_home}/conf/
目录中,一般是registry.conf
文件 - 当前服务的配置,两种配置方式:
- 通过分布式服务的统一配置中心,例如Zookeeper
- 通过本地文件
我们先看registry.conf,内容是JSON风格
registry {
# 指定注册中心类型,这里使用eureka类型
type = "eureka"
# 各种注册中心的配置。。这里省略,只保留了eureka和Zookeeper
eureka {
serviceUrl = "http://localhost:8761/eureka"
application = "seata_tc_server"
weight = "1"
}
zk {
cluster = "default"
serverAddr = "127.0.0.1:2181"
session.timeout = 6000
connect.timeout = 2000
}
}
config {
# 配置文件方式,可以支持 file、nacos 、apollo、zk、consul、etcd3
type = "file"
nacos {
serverAddr = "localhost"
namespace = ""
group = "SEATA_GROUP"
}
zk {
serverAddr = "127.0.0.1:2181"
session.timeout = 6000
connect.timeout = 2000
}
file {
name = "file.conf"
}
}
这个文件主要配置两个内容:
- 注册中心的类型及地址,本例我们选择eureka做注册中心
- eureka.serviceUrl:是eureka的地址,例如http://localhost:8761/eureka
- application:是TC注册到eureka时的服务名称,例如
seata_tc_server
- 配置中心的类型及地址,本例我们选择本地文件做配置,就是当前目录的
file.conf
文件
再来看file.conf
文件:
## transaction log store, only used in seata-server
store {
## store mode: file、db
mode = "file"
## file store property
file {
## store location dir
dir = "sessionStore"
# branch session size , if exceeded first try compress lockkey, still exceeded throws exceptions
maxBranchSessionSize = 16384
# globe session size , if exceeded throws exceptions
maxGlobalSessionSize = 512
# file buffer size , if exceeded allocate new buffer
fileWriteBufferCacheSize = 16384
# when recover batch read size
sessionReloadReadSize = 100
# async, sync
flushDiskMode = async
}
## database store property
db {
## the implement of javax.sql.DataSource, such as DruidDataSource(druid)/BasicDataSource(dbcp) etc.
datasource = "dbcp"
## mysql/oracle/h2/oceanbase etc.
dbType = "mysql"
driverClassName = "com.mysql.jdbc.Driver"
url = "jdbc:mysql://127.0.0.1:3306/seata_demo"
user = "root"
password = "123"
minConn = 1
maxConn = 10
globalTable = "global_table"
branchTable = "branch_table"
lockTable = "lock_table"
queryLimit = 100
}
}
关键配置:
- store:TC的服务端数据存储配置
- mode:数据存储方式,支持两种:file和db
- file:将数据存储在本地文件中,性能比较好,但不支持水平扩展
- db:将数据保存在指定的数据库中,需要指定数据库连接信息
- mode:数据存储方式,支持两种:file和db
如果用文件作为存储介质,不需要其它配置了,直接运行即可。
但是如果使用db作为存储介质,还需要在数据库中创建3张表:
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,
`gmt_modified` DATETIME,
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(96),
`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;
3)启动
进入${seata_home}/bin/
目录中:
如果是linux环境(要有JRE),执行seata-server.sh
如果是windows环境,执行seata-server.bat
3.2.4.改造Order服务
接下来是微服务的改造,不管是哪一个微服务,只要是事务的参与者,步骤基本一致。
1)引入依赖
我们在父工程seata-demo
中已经对依赖做了管理:
<alibaba.seata.version>2.1.0.RELEASE</alibaba.seata.version>
<seata.version>1.1.0</seata.version>
因此,我们在项目order-service
的pom文件中,引入依赖坐标即可:
<dependency>
<groupId>com.alibaba.cloud</groupId>
<artifactId>spring-cloud-alibaba-seata</artifactId>
<version>${alibaba.seata.version}</version>
<exclusions>
<exclusion>
<artifactId>seata-all</artifactId>
<groupId>io.seata</groupId>
</exclusion>
</exclusions>
</dependency>
<dependency>
<artifactId>seata-all</artifactId>
<groupId>io.seata</groupId>
<version>${seata.version}</version>
</dependency>
2)添加配置文件
首先在application.yml中添加一行配置:
spring:
cloud:
alibaba:
seata:
tx-service-group: test_tx_group # 定义事务组的名称
这里是定义事务组的名称,接下来会用到。
然后是在resources
目录下放两个配置文件:file.conf
和registry.conf
其中,registry.conf
与TC服务端的一样,此处不再讲解。
我们来看下file.conf
transport {
# tcp udt unix-domain-socket
type = "TCP"
#NIO NATIVE
server = "NIO"
#enable heartbeat
heartbeat = true
# the client batch send request enable
enableClientBatchSendRequest = true
#thread factory for netty
threadFactory {
bossThreadPrefix = "NettyBoss"
workerThreadPrefix = "NettyServerNIOWorker"
serverExecutorThread-prefix = "NettyServerBizHandler"
shareBossWorker = false
clientSelectorThreadPrefix = "NettyClientSelector"
clientSelectorThreadSize = 1
clientWorkerThreadPrefix = "NettyClientWorkerThread"
# netty boss thread size,will not be used for UDT
bossThreadSize = 1
#auto default pin or 8
workerThreadSize = "default"
}
shutdown {
# when destroy server, wait seconds
wait = 3
}
serialization = "seata"
compressor = "none"
}
service {
vgroup_mapping.test_tx_group = "seata_tc_server"
#only support when registry.type=file, please don't set multiple addresses
seata_tc_server.grouplist = "127.0.0.1:8091"
#degrade, current not support
enableDegrade = false
#disable seata
disableGlobalTransaction = false
}
client {
rm {
asyncCommitBufferLimit = 10000
lock {
retryInterval = 10
retryTimes = 30
retryPolicyBranchRollbackOnConflict = true
}
reportRetryCount = 5
tableMetaCheckEnable = false
reportSuccessEnable = false
}
tm {
commitRetryCount = 5
rollbackRetryCount = 5
}
undo {
dataValidation = true
logSerialization = "jackson"
logTable = "undo_log"
}
log {
exceptionRate = 100
}
}
配置解读:
transport
:与TC交互的一些配置heartbeat
:client和server通信心跳检测开关enableClientBatchSendRequest
:客户端事务消息请求是否批量合并发送
service
:TC的地址配置,用于获取TC的地址vgroup_mapping.test_tx_group = "seata_tc_server"
:test_tx_group
:是事务组名称,要与application.yml中配置一致,seata_tc_server
:是TC服务端在注册中心的id,将来通过注册中心获取TC地址enableDegrade
:服务降级开关,默认关闭。如果开启,当业务重试多次失败后会放弃全局事务disableGlobalTransaction
:全局事务开关,默认false。false为开启,true为关闭
default.grouplist
:这个当注册中心为file的时候,才用到
client
:客户端配置rm
:资源管理器配asynCommitBufferLimit
:二阶段提交默认是异步执行,这里指定异步队列的大小lock
:全局锁配置retryInterval
:校验或占用全局锁重试间隔,默认10,单位毫秒retryTimes
:校验或占用全局锁重试次数,默认30次retryPolicyBranchRollbackOnConflict
:分支事务与其它全局回滚事务冲突时锁策略,默认true,优先释放本地锁让回滚成功
reportRetryCount
:一阶段结果上报TC失败后重试次数,默认5次
tm
:事务管理器配置commitRetryCount
:一阶段全局提交结果上报TC重试次数,默认1rollbackRetryCount
:一阶段全局回滚结果上报TC重试次数,默认1
undo
:undo_log的配置dataValidation
:是否开启二阶段回滚镜像校验,默认truelogSerialization
:undo序列化方式,默认JacksonlogTable
:自定义undo表名,默认是undo_log
log
:日志配置exceptionRate
:出现回滚异常时的日志记录频率,默认100,百分之一概率。回滚失败基本是脏数据,无需输出堆栈占用硬盘空间
3)代理DataSource
Seata的二阶段执行是通过拦截sql语句,分析语义来指定回滚策略,因此需要对DataSource做代理。我们在项目的cn.itcast.order.config
包中,添加一个配置类:
package cn.itcast.order.config;
import com.baomidou.mybatisplus.extension.spring.MybatisSqlSessionFactoryBean;
import io.seata.rm.datasource.DataSourceProxy;
import org.apache.ibatis.session.SqlSessionFactory;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import javax.sql.DataSource;
@Configuration
public class DataSourceProxyConfig {
@Bean
public SqlSessionFactory sqlSessionFactoryBean(DataSource dataSource) throws Exception {
// 订单服务中引入了mybatis-plus,所以要使用特殊的SqlSessionFactoryBean
MybatisSqlSessionFactoryBean sqlSessionFactoryBean = new MybatisSqlSessionFactoryBean();
// 代理数据源
sqlSessionFactoryBean.setDataSource(new DataSourceProxy(dataSource));
// 生成SqlSessionFactory
return sqlSessionFactoryBean.getObject();
}
}
注意,这里因为订单服务使用了mybatis-plus这个框架(这是一个mybatis集成框架,自动生成单表Sql),因此我们需要用mybatis-plus的MybatisSqlSessionFactoryBean
代替SqlSessionFactoryBean
如果用的是原生的mybatis,请使用SqlSessionFactoryBean
。
4)添加事务注解
给事务发起者order_service
的OrderServiceImpl
中的createOrder()
方法添加@GlobalTransactional
注解,开启全局事务:
重新启动即可。
3.2.5.改造Storage、Account服务
与OrderService类似,这里也要经过下面的步骤:
-
引入依赖:与order-service一致,略
-
添加配置文件:与order-service一致,略
-
代理DataSource,我们的storage-service和account-service都没有用mybatis-plus,所以配置要使用SqlSessionFactory:
package cn.itcast.order.config; import io.seata.rm.datasource.DataSourceProxy; import org.apache.ibatis.session.SqlSessionFactory; import org.mybatis.spring.SqlSessionFactoryBean; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; import javax.sql.DataSource; @Configuration public class DataSourceProxyConfig { @Bean public SqlSessionFactory sqlSessionFactoryBean(DataSource dataSource) throws Exception { // 因为使用的是mybatis,这里定义SqlSessionFactoryBean SqlSessionFactoryBean sqlSessionFactoryBean = new SqlSessionFactoryBean(); // 配置数据源代理 sqlSessionFactoryBean.setDataSource(new DataSourceProxy(dataSource)); return sqlSessionFactoryBean.getObject(); } }
另外,事务注解可以使用@Transactionnal
,而不是@GlobalTransactional
,事务发起者才需要添加@GlobalTransactional
。
3.2.6.测试
重启所有微服务后,我们再次测试。
目前数据情况:用户余额900,库存为6.
我们试试扣款1200元,那么扣款失败,理论上来说所有数据都会回滚.
看下用户余额:
因为扣款失败,因此这里没有扣减
来看下库存数据:
减库存依然是6,成功回滚,说明分布式事务生效了!