分布式事务 - Seata - AT入门

目录

随着数据量的增长,单一的数据库性能已无法满足要求,因而诞生了分库分表的方式来提升RMDB访问性能,分库带来的问题就是无法通过单一DB连接(多库需要多个连接)来控制事务,因而就出现了需要协调多个单库事务的需求,而在微服务时代,由于服务的分布式部署,而各自服务又依赖不同的数据源,简单的本地事务已无法满足要求,综上诞生了分布式事务的概念。
分布式事务 - Seata - AT入门

Seata(Simple Extensible Autonomous Transaction Architecture)作为阿里开源的分布式事务解决方案,致力于在微服务架构下提供高性能和简单易用的分布式事务服务,历经了阿里历年双11的考验,商业化产品亦在阿里云、金融云进行售卖,且在2019.1月Seata 正式宣布对外开源。
Seata 将为用户提供了AT、TCC、SAGA 和 XA 事务模式,为用户打造一站式的分布式解决方案。

接下来结合实战对各模式依次介绍。

Seata整体架构

分布式事务 - Seata - AT入门
Seata的整体架构如上图,分TC、TM和RM三个角色,TC(Server端)为单独服务端部署TM和RM(Client端)由业务系统集成

TC (Transaction Coordinator) - 事务协调者
维护全局和分支事务的状态,驱动全局事务提交或回滚。

TM (Transaction Manager) - 事务管理器
定义全局事务的范围:开始全局事务、提交或回滚全局事务。

RM (Resource Manager) - 资源管理器
管理分支事务处理的资源,与TC交谈以注册分支事务和报告分支事务的状态,并驱动分支事务提交或回滚。

一个典型的事务过程:

  1. TMTC 申请开启一个全局事务,全局事务创建成功并生成一个全局唯一的 XID
  2. XID 在微服务调用链路的上下文中传播。
  3. RMTC 注册分支事务,将其纳入 XID 对应全局事务的管辖。
  4. TMTC 发起针对 XID全局提交或回滚决议
  5. TC 调度 XID 下管辖的全部分支事务完成提交或回滚请求。

启动Seata Server(TC)

Seata Server启动的主要步骤如下:

  1. 通过registry.conf指定注册中心(file、nacos、eureka、consul、etcd、zookeeper、sofa、redis)配置中心(file、nacos、apollo、consul、etcd、zookeeper)
  2. 若注册中心为file,则通过file.conf指定存储模式(file、db、redis)及其他配置
  3. 若注册中心为其他(例如nacos),则需修改/script/config-center/config.txt并将其导入到对应的配置中心

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启动过程如下:

(1)下载seata server启动包:https://github.com/seata/seata/releases
分布式事务 - Seata - AT入门
解压后目录结构如下:

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

(2)初始化DB mysql
新建数据库seata,然后导入script/server/db/mysql.sql初始化脚本
分布式事务 - Seata - AT入门
(3)启动nacos
可参见我的另一篇博客:nacos快速启动,即在windows本地开发环境快速启动nacos,访问方式如下:

http://localhost:8848/nacos
默认账户/密码:nacos/nacos

可以新建一个namespace用于本地开发使用,例如我本地开发习惯使用luo-dev
分布式事务 - Seata - AT入门

(4)修改配置中心 - conf/registry.conf -> config
由于采用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

分布式事务 - Seata - AT入门
注:
registry.conf中默认config.type=“file”,即通过本地文件进行配置,
而config.file.name="file.conf"即指定了同目录下conf/file.conf即为对应的配置文件位置,
在设置config.type="nacos"后即可理解将配置信息均放到nacos中,则不在读取默认的file.conf文件中的配置
分布式事务 - Seata - AT入门

(5)导入初始配置到nacos
在第(4)步中指定的nacos配置中,即定义了Seata Server需要依赖nacos中如下配置

namespace: luo-dev
group: SEATA_GROUP
dataId: seataServer.properties

可以将https://github.com/seata/seata克隆到本地,
默认配置即在/script/config-center/config.txt文件中定义,
分布式事务 - Seata - AT入门
由于存储模式采用db模式,则需修改config.txt中标红框部分
即store.mode="db"且store.db下修改为第(2)步中对应的初始DB配置,
具体配置说明可参见:SEATA - 用户文档 - 参数配置
分布式事务 - Seata - AT入门

关于导入nacos配置,官方文档推荐方式如下:
即可通过script/config-center/nacos/nacos-config.sh将修改后的config.txt导入到nacos中,
注:此种方式registry.conf -> registry.nacos.dataId配置项需要删除
分布式事务 - Seata - AT入门
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

导入结果如下图
分布式事务 - Seata - AT入门
可以发现导入后的结果变成N多配置项,每项都可单独修改。

还有另一种手动导入方式,我更喜欢这种导入方式
即可以根据之前registry.conf -> registry -> nacos配置,在nacos中手动创建对应的seataServer.properties配置,并将需改后的config.txt内容直接复制到seataServer.properties中,这样所有配置项就都在一个配置文件中进行维护。

namespace: luo-dev
group: SEATA_GROUP
dataId: seataServer.properties

分布式事务 - Seata - AT入门

(6)修改注册中心 - conf/registry.conf -> registry
由于采用nacos注册中心,则需修改conf/registry.conf中registry模块中标红框部分
即registry.type="nacos"且registry.nacos下修改为第(3)步中对应的nacos配置,
分布式事务 - Seata - AT入门

综合第(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配置
  
}

(7)启动Seata server
执行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 - AT入门

注:
关于Seata Server高可用部署,可参见:
Seata 高可用部署 - K8S Deployment 3副本 + ConfigMap

AT模式

根据2PC中 分支事务(RM) 的行为模式不同,Seata将分支事务划分为:

  • AT(Automatic (Branch) Transaction Mode)
    基于本地ACID事务(即适用于RMDB),在prepare阶段由开发者基于本地事务完成更新操作,且由Seata自动完成commit和rollback(基于seata undo_log表),即对应Seata AT模式。
  • MT(Manual (Branch) Transaction Mode)
    不依赖底层数据资源的支持,由开发者自定义prepare, commit, rollback逻辑,即对应Seata TCC模式。

AT模式的执行阶段可参见下图,具体的原理机制、隔离性处理等可参见官微:Seata官微 - 带你读透 SEATA 的 AT 模式

分布式事务 - Seata - AT入门

接下来结合Seata官网下单流程示例,具体讲解Java客户端如何集成Seata AT模式
分布式事务 - Seata - AT入门

示例模拟了一个下单流程,即由业务应用Business依次调用

  1. Storage -> 扣库存
  2. Order -> Account扣减用户余额
  3. Order -> 新建订单

综上将示例工程seata-demo工程分为4个对应的模块,如下图
分布式事务 - Seata - AT入门
具体服务说明如下表

服务模块 服务描述 服务暴露端口 对应数据库
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 - AT入门

(1)客户端seata Maven依赖
参考seata - 部署指南 - 注意事项


 <!-- 属性定义-->
<properties>
    <java.version>1.8</java.version>
    <spring-cloud.version>2020.0.4</spring-cloud.version>
    <spring-cloud-alibaba.version>2.2.6.RELEASE</spring-cloud-alibaba.version>
    <seata.version>1.4.2</seata.version>
</properties>

<!-- 依赖管理定义 -->
<dependencyManagement>
    <dependencies>
        <!-- springcloud依赖 -->
        <dependency>
            <groupId>org.springframework.cloud</groupId>
            <artifactId>spring-cloud-dependencies</artifactId>
            <version>${spring-cloud.version}</version>
            <type>pom</type>
            <scope>import</scope>
        </dependency>

        <!--Spring Cloud Alibaba-->
        <dependency>
            <groupId>com.alibaba.cloud</groupId>
            <artifactId>spring-cloud-alibaba-dependencies</artifactId>
            <version>${spring-cloud-alibaba.version}</version>
            <type>pom</type>
            <scope>import</scope>
        </dependency>
       
        <!-- Alibaba Seata SpringBoot依赖 -->
        <dependency>
			<groupId>io.seata</groupId>
			<artifactId>seata-spring-boot-starter</artifactId>
			<version>${seata.version}</version>
		</dependency>
		
		...
    </dependencies>
</dependencyManagement>


<dependencies>
	<!-- SpringCloudAlibaba Seata依赖 -->
	<dependency>
	    <groupId>io.seata</groupId>
	    <artifactId>seata-spring-boot-starter</artifactId>
	</dependency>
	<dependency>
	    <groupId>com.alibaba.cloud</groupId>
	    <artifactId>spring-cloud-starter-alibaba-seata</artifactId>
	    <exclusions>
	    	<!-- 此处排除低版本的seata 1.3.0,使用单独定义的1.4.2-->
	        <exclusion>
	            <groupId>io.seata</groupId>
	            <artifactId>seata-spring-boot-starter</artifactId>
	        </exclusion>
	    </exclusions>
	</dependency>
	...
</dependencies>

(2)Seata客户端集成Nacos配置
关于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个配置文件):

  1. storage-service-dev.yaml定义库存应用配置,
  2. application-dev.yaml定义环境共同配置,
  3. seata-client-dev.yaml定义seata客户端共同配置 分布式事务 - Seata - AT入门

关于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
相同,如下图
分布式事务 - Seata - AT入门

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入门

(3)创建undo_log表
每个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';

分布式事务 - Seata - AT入门
(4)AT模式编程模型
分布式事务 - Seata - AT入门
如图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中进行服务间调用传递。
分布式事务 - Seata - AT入门

待续…

  • TCC模式
    不依赖底层数据资源的支持,由开发者自定义try, commit, cancel逻辑
  • SAGA模式
  • XA模式
上一篇:BZOJ3065 带插入区间K小值


下一篇:20-Spring Cloud Alibaba Seata