随着数据量的增长,单一的数据库性能已无法满足要求,因而诞生了分库分表的方式来提升RMDB访问性能,分库带来的问题就是无法通过单一DB连接(多库需要多个连接)来控制事务,因而就出现了需要协调多个单库事务的需求,而在微服务时代,由于服务的分布式部署,而各自服务又依赖不同的数据源,简单的本地事务已无法满足要求,综上诞生了分布式事务的概念。
Seata(Simple Extensible Autonomous Transaction Architecture)作为阿里开源的分布式事务解决方案,致力于在微服务架构下提供高性能和简单易用的分布式事务服务,历经了阿里历年双11的考验,商业化产品亦在阿里云、金融云进行售卖,且在2019.1月Seata 正式宣布对外开源。
Seata 将为用户提供了 AT、TCC、SAGA 和 XA
事务模式,为用户打造一站式的分布式解决方案。
接下来结合实战对各模式依次介绍。
Seata的整体架构如上图,分TC、TM和RM三个角色,TC(Server端)为单独服务端部署,TM和RM(Client端)由业务系统集成。
TC (Transaction Coordinator) - 事务协调者
维护全局和分支事务的状态,驱动全局事务提交或回滚。
TM (Transaction Manager) - 事务管理器
定义全局事务的范围:开始全局事务、提交或回滚全局事务。
RM (Resource Manager) - 资源管理器
管理分支事务处理的资源,与TC交谈以注册分支事务和报告分支事务的状态,并驱动分支事务提交或回滚。
一个典型的事务过程:
TM
向 TC
申请开启一个全局事务,全局事务创建成功并生成一个全局唯一的 XID。RM
向 TC
注册分支事务,将其纳入 XID 对应全局事务的管辖。TM
向 TC
发起针对 XID 的全局提交或回滚决议。TC
调度 XID 下管辖的全部分支事务完成提交或回滚请求。Seata Server启动的主要步骤如下:
registry.conf
指定注册中心(file、nacos、eureka、consul、etcd、zookeeper、sofa、redis)
、配置中心(file、nacos、apollo、consul、etcd、zookeeper)
file.conf
指定存储模式(file、db、redis)
及其他配置Seata Server存储模式(store.mode)现有file、db、redis三种(后续将引入raft,mongodb),file模式无需改动,直接启动即可,不同模式的对比如下:
存储模式 | 初始化 | 说明 |
---|---|---|
file | 无需改动,直接启动 | 单机模式,性能较高, 全局事务会话信息内存中读写并持久化本地文件root.data |
db | 1. 初始DB:seata/script/server/db/mysql.sql 2. 修改存储模式:store.mode=“db” 3. 修改存储数据源:store.db相关属性 |
高可用模式, 全局事务会话信息通过db共享,相应性能差些 |
redis | 1. 修改存储模式:store.mode=“redis” 2. 修改存储数据源:store.redis相关属性 |
性能较高,存在事务信息丢失风险, 需提前配置合适当前场景的redis持久化配置 |
如下以存储模式为db Mysql
,配置和注册中心为nacos
为例,记录Seata Server启动过程如下:
下载seata server启动包:https://github.com/seata/seata/releases
解压后目录结构如下:
E:\下载\开发\seata\seata-server-1.4.2>tree /f
│ LICENSE
│
├─bin
│ seata-server.bat
│ seata-server.sh
│
├─conf
│ │ file.conf
│ │ file.conf.example
│ │ logback.xml
│ │ README-zh.md
│ │ README.md
│ │ registry.conf
│ │
│ ├─logback
│ │ console-appender.xml
│ │ file-appender.xml
│ │ kafka-appender.xml
│ │ logstash-appender.xml
│ │
│ └─META-INF
│ └─services
│ io.seata.core.rpc.RegisterCheckAuthHandler
│ io.seata.core.store.db.DataSourceProvider
│ io.seata.server.coordinator.AbstractCore
│ io.seata.server.lock.LockManager
│ io.seata.server.session.SessionManager
│
├─lib
│ │ apollo-xxx-1.6.0.jar
│ │ druid-1.1.23.jar
│ │ eureka-client-1.9.5.jar
│ │ grpc-xxx-1.17.1.jar
│ │ HikariCP-3.4.3.jar
│ │ registry-client-all-5.2.0.jar
│ │ seata-xxx-1.4.2.jar
│ │ ......
│ │ sofa-xxx-1.0.12.jar
│ │ zkclient-0.11.jar
│ │ zookeeper-3.4.14.jar
│ │
│ └─jdbc
│ mysql-connector-java-5.1.35.jar
│ mysql-connector-java-8.0.19.jar
│
└─logs
新建数据库seata,然后导入script/server/db/mysql.sql初始化脚本
可参见我的另一篇博客:nacos快速启动,即在windows本地开发环境快速启动nacos,访问方式如下:
http://localhost:8848/nacos
默认账户/密码:nacos/nacos
可以新建一个namespace用于本地开发使用,例如我本地开发习惯使用luo-dev
由于采用nacos配置中心,则需修改conf/registry.conf中config模块中标红框部分
,
即config.type="nacos"且config.nacos下修改为第(3)步中对应的nacos配置,
config.nacos.serverAdd=“127.0.0.1:8848” 对应nacos服务器地址
config.nacos.namespace=“luo-dev” 对应nacos命名空间luo-dev
config.nacos.group=“SEATA_GROUP” 对应nacos分组SEATA_GROUP
config.nacos.username=“nacao” 对应nacos的用户名
config.nacos.password=“nacos” 对应nacos的用户密码
config.nacos.dataId=“seataServer.properties” 对应nacos server对应配置的dataId
注:
registry.conf中默认config.type=“file”,即通过本地文件进行配置,
而config.file.name="file.conf"即指定了同目录下conf/file.conf即为对应的配置文件位置,
在设置config.type="nacos"后即可理解将配置信息均放到nacos中,则不在读取默认的file.conf文件中的配置
在第(4)步中指定的nacos配置中,即定义了Seata Server需要依赖nacos中如下配置
namespace: luo-dev
group: SEATA_GROUP
dataId: seataServer.properties
可以将https://github.com/seata/seata克隆到本地,
默认配置即在/script/config-center/config.txt文件中定义,
由于存储模式采用db模式,则需修改config.txt中标红框部分
,
即store.mode="db"且store.db下修改为第(2)步中对应的初始DB配置,
具体配置说明可参见:SEATA - 用户文档 - 参数配置
关于导入nacos配置,官方文档推荐方式如下:
即可通过script/config-center/nacos/nacos-config.sh将修改后的config.txt导入到nacos中,
注:此种方式registry.conf -> config.nacos.dataId配置项需要删除
nacos导入配置命令如下:
# 具体说明参见:https://github.com/seata/seata/tree/1.4.0/script/config-center
# -h: nacos host,默认localhost
# -p: nacos端口,默认8848
# -g: nacos分组,默认'SEATA_GROUP'.
# -t: 租户信息Tenant information,对应nacos namespace ID,默认''
# -u: nacos用户名,默认''
# -w: nacos用户密码,默认''
script/config-center/nacos/nacos-config.sh ^
-h localhost -p 8848 ^
-g SEATA_GROUP ^
-t luo-dev ^
-u nacos -w nacos
导入结果如下图
可以发现导入后的结果变成N多配置项,每项都可单独修改。
还有另一种手动导入方式,我更喜欢这种导入方式,
即可以根据之前registry.conf -> registry -> nacos配置,在nacos中手动创建对应的seataServer.properties配置,并将需改后的config.txt内容直接复制到seataServer.properties中,这样所有配置项就都在一个配置文件中进行维护。
namespace: luo-dev
group: SEATA_GROUP
dataId: seataServer.properties
由于采用nacos注册中心,则需修改conf/registry.conf中registry模块中标红框部分
,
即registry.type="nacos"且registry.nacos下修改为第(3)步中对应的nacos配置,
综合第(4)(5)步,最终conf/registry.conf内容如下:
registry {
# file 、nacos 、eureka、redis、zk、consul、etcd3、sofa
type = "nacos"
nacos {
application = "seata-server"
serverAddr = "127.0.0.1:8848"
namespace = "luo-dev"
group = "SEATA_GROUP"
cluster = "default"
username = "nacos"
password = "nacos"
}
# 省略eureka、redis、zk、consul、etcd3、sofa、file配置
}
config {
# file、nacos 、apollo、zk、consul、etcd3
type = "nacos"
nacos {
serverAddr = "127.0.0.1:8848"
namespace = "luo-dev"
group = "SEATA_GROUP"
username = "nacos"
password = "nacos"
dataId = "seataServer.properties"
}
# 省略consule、apollo、zk、etcd3、file配置
}
执行bin/seata-server.sh(windows系统可执行seata-server.bat)
# -h: 注册到注册中心的ip
# -p: Server rpc 监听端口
# -m: 全局事务会话信息存储模式,file、db、redis,优先读取启动参数 (Seata-Server 1.3及以上版本支持redis)
# -n: Server node,多个Server时,需区分各自节点,用于生成不同区间的transactionId,以免冲突
# -e: 多环境配置参考 http://seata.io/en-us/docs/ops/multi-configuration-isolation.html
seata-server.bat -h 127.0.0.1 -p 8091
# 启动日志
......
18:53:46,763 |-INFO in ch.qos.logback.classic.joran.JoranConfigurator@78c03f1f - Registering current configuration as safe fallback point
SLF4J: A number (18) of logging calls during the initialization phase have been intercepted and are
SLF4J: now being replayed. These are subject to the filtering rules of the underlying logging system.
SLF4J: See also http://www.slf4j.org/codes.html#replay
18:53:46.835 INFO --- [ main] io.seata.config.FileConfiguration : The file name of the operation is registry
18:53:46.839 INFO --- [ main] io.seata.config.FileConfiguration : The configuration file used is D:\programs\Java\seata-server-1.4.2\conf\registry.conf
18:53:48.453 INFO --- [ main] com.alibaba.druid.pool.DruidDataSource : {dataSource-1} inited
18:53:49.219 INFO --- [ main] i.s.core.rpc.netty.NettyServerBootstrap : Server started, listen port: 8091
启动成功后即可发现seata-server服务已注册到nacos上
注:
关于Seata Server高可用部署,可参见:
Seata 高可用部署 - K8S Deployment 3副本 + ConfigMap
根据2PC中 分支事务(RM) 的行为模式不同,Seata将分支事务划分为:
由Seata自动完成commit和rollback(基于seata undo_log表)
,即对应Seata AT模式。由开发者自定义prepare, commit, rollback逻辑
,即对应Seata TCC模式。AT模式的执行阶段可参见下图,具体的原理机制、隔离性处理等可参见官微:Seata官微 - 带你读透 SEATA 的 AT 模式
接下来结合Seata官网下单流程示例,具体讲解Java客户端如何集成Seata AT模式
示例模拟了一个下单流程,即由业务应用Business依次调用
综上将示例工程seata-demo工程分为4个对应的模块,如下图
注册和配置中心同之前Seata Server使用的nacos,
RPC调用使用Feign,
具体服务说明如下表
服务模块 | 服务描述 | 服务暴露端口 | 对应数据库 |
---|---|---|---|
business-service | 业务服务 调用storage-service、order-servcie 通过@GlobalTransactional发起全局事务 担任TM角色 |
8080 | 无 |
storage-service | 库存服务 担任RM角色 |
8081 | dtx-storage |
account-service | 用户服务 担任RM角色 |
8082 | dtx-account |
order-service | 订单服务 调用account-service 担任RM角色 |
8083 | dtx-order |
具体源码可参见:https://github.com/marqueeluo/spring-cloud-demo/tree/develop/seata-demo
具体DB初始语句、nacos配置可参见:https://github.com/marqueeluo/spring-cloud-demo/tree/develop/seata-demo/config
参考seata - 部署指南 - 注意事项
<properties>
<java.version>1.8java.version>
<spring-cloud.version>2020.0.4spring-cloud.version>
<spring-cloud-alibaba.version>2.2.6.RELEASEspring-cloud-alibaba.version>
<seata.version>1.4.2seata.version>
properties>
<dependencyManagement>
<dependencies>
<dependency>
<groupId>org.springframework.cloudgroupId>
<artifactId>spring-cloud-dependenciesartifactId>
<version>${spring-cloud.version}version>
<type>pomtype>
<scope>importscope>
dependency>
<dependency>
<groupId>com.alibaba.cloudgroupId>
<artifactId>spring-cloud-alibaba-dependenciesartifactId>
<version>${spring-cloud-alibaba.version}version>
<type>pomtype>
<scope>importscope>
dependency>
<dependency>
<groupId>io.seatagroupId>
<artifactId>seata-spring-boot-starterartifactId>
<version>${seata.version}version>
dependency>
...
dependencies>
dependencyManagement>
<dependencies>
<dependency>
<groupId>io.seatagroupId>
<artifactId>seata-spring-boot-starterartifactId>
dependency>
<dependency>
<groupId>com.alibaba.cloudgroupId>
<artifactId>spring-cloud-starter-alibaba-seataartifactId>
<exclusions>
<exclusion>
<groupId>io.seatagroupId>
<artifactId>seata-spring-boot-starterartifactId>
exclusion>
exclusions>
dependency>
...
dependencies>
关于Seata client、server的详细配置参见:
seata参数配置 1.3.0版本
seata - Nacos 配置中心
seata - Nacos注册中心
https://github.com/seata/seata/blob/develop/script/client/spring/application.yml
Seata客户端bootstrap.yaml定义如下
# Tomcat
server:
port: 8081
spring:
application:
# 应用名称
name: storage-service
profiles:
# 环境配置
active: dev
cloud:
# nacos配置
nacos:
discovery:
# 服务注册地址
server-addr: 127.0.0.1:8848
namespace: luo-dev
group: SEATA_GROUP
config:
# 配置中心地址
server-addr: 127.0.0.1:8848
namespace: luo-dev
group: SEATA_GROUP
# 配置文件格式
file-extension: yaml
# 共享配置
shared-configs:
# application-{profile}.yaml为环境共同配置
- data-id: application-${spring.profiles.active}.${spring.cloud.nacos.config.file-extension}
group: SEATA_GROUP
# seata-client-{profile}.yaml为Seata client共同配置
- data-id: seata-client-${spring.profiles.active}.${spring.cloud.nacos.config.file-extension}
group: SEATA_GROUP
其中定义了3个配置文件(对应nacos上的3个配置文件):
关于seata-client-dev.yaml
具体配置如下:
seata:
# 启动seata
enabled: true
# 事务组
tx-service-group: luo_dev_seata_demo_tx_group
enable-auto-data-source-proxy: true
# 事务模式 - AT
data-source-proxy-mode: AT
# 是否使用JDK代理(false则使用CGLIB)
use-jdk-proxy: false
service:
vgroup-mapping:
# 事务组映射到TC集群名
luo_dev_seata_demo_tx_group: default
# 配置中心(同Seata Server配置)
config:
type: nacos
nacos:
server-addr: 127.0.0.1:8848
namespace: luo-dev
group: SEATA_GROUP
username: nacos
password: nacos
data-id: seataServer.properties
# 注册中心(同Seata Server配置)
registry:
type: nacos
nacos:
server-addr: 127.0.0.1:8848
namespace: luo-dev
group: SEATA_GROUP
username: nacos
password: nacos
# 此处application需要何Seata Server启动时指定的registry.nacos.application配置相同,
# 即定义Seata Server对应的服务名
application: seata-server
# 此处application需要何Seata Server启动时指定的registry.nacos.cluster,
cluster: default
其中需要注意:
1、 关于Seata Client端事务分组配置(对应nacos中seata-client-dev.yaml)
seata.tx-service-group=luo_dev_seata_demo_tx_group
seata.service.vgroup-mapping.luo_dev_seata_demo_tx_group
=default
中的事务分组名需要与Seata Server端配置(对应nacos中seataServer.properties)中
service.vgroupMapping.luo_dev_seata_demo_tx_group
=default
相同,如下图
2、 关于Seata Client端配置中的config、registry配置(对应nacos中seata-client-dev.yaml)
seata.config.*
seata.registy.*
需要与Seata Server端配置(Seata Server中的conf/registry.conf)中
config.*
registry.*
相同,如下图
每个Seata AT的client对应的数据库中,还需要创建AT模式对应undo_log表,
建表语句参见:https://github.com/seata/seata/blob/develop/script/client/at/db/mysql.sql
-- for AT mode you must to init this sql for you business database. the seata server not need it.
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';
如图business担当TM
角色,即通过@GlobalTransactional注解发起全局事务
,
其他服务中的分支事务(即RM角色)
和普通事务@Transactional编程模型并无二样,
如下为business-storage服务的核心代码
/**
* 下单操作 - AT全局事务通过@GlobalTransctional注解发起
*
* @param userId
* @param commodityCode
* @param count
* @return
*/
@Override
@GlobalTransactional
public RespResult<Order> handleBusinessAt(String userId, String commodityCode, Integer count) {
log.info("开始AT全局事务,XID={}", RootContext.getXID());
/** 扣减库存 */
log.info("RPC扣减库存,参数:commodityCode={}, count={}", commodityCode, count);
RespResult storageResult = this.storageFeignClient.deduct(commodityCode, count);
log.info("RPC扣减库存,结果:{}", storageResult);
if (!RespResult.isSuccess(storageResult)) {
throw new MsgRuntimeException("RPC扣减库存 - 返回失败结果!");
}
/** 创建订单 */
log.info("RPC创建订单,参数:userId={}, commodityCode={}, count={}", userId, commodityCode, count);
RespResult<Order> orderResult = this.orderFeignClient.createOrder(userId, commodityCode, count);
log.info("RPC创建订单,结果:{}", orderResult);
if (!RespResult.isSuccess(orderResult)) {
throw new MsgRuntimeException("RPC创建订单 - 返回失败结果!");
}
return orderResult;
}
order-service核心代码如下:
@Override
@Transactional(rollbackFor = Exception.class)
public RespResult<Order> create(String userId, String commodityCode, Integer count) {
//计算订单金额(假设商品单价5元)
BigDecimal orderMoney = new BigDecimal(count).multiply(new BigDecimal(5));
/** 用户扣款 */
RespResult respResult = accountFeignClient.debit(userId, orderMoney);
log.info("RPC用户扣减余额服务,结果:{}", respResult);
if (!RespResult.isSuccess(respResult)) {
throw new MsgRuntimeException("RPC用户扣减余额服务失败!");
}
/** 创建订单 */
Order order = new Order();
order.setUserId(userId);
order.setCommodityCode(commodityCode);
order.setCount(count);
order.setMoney(orderMoney);
log.info("保存订单信息,参数:{}", order);
Boolean result = this.save(order);
log.info("保存订单信息,结果:{}", result);
if (!Boolean.TRUE.equals(result)) {
throw new MsgRuntimeException("保存新订单信息失败!");
}
if ("product-3".equals(commodityCode)) {
throw new MsgRuntimeException("异常:模拟业务异常:Order branch exception");
}
return RespResult.successData(order);
}
storage-service、account-service的核心代码较为简单,只是发起dao调用,
具体源码可参见:https://github.com/marqueeluo/spring-cloud-demo/tree/develop/seata-demo
需要注意的是Seata事务跨服务间(Feign调用)传递是通过SeataFeignClient类,
即将xid放在http请求头TX_XID中进行服务间调用传递。