# 分布式事务

# 事务

  • 指要做的或所做的事情
  • 事务由事务开始(begin transaction)和事务结束(end transaction) 之间执行的全体操作组成;
  • 事务中的多个操作,要么全部成功,要么全部失败;

# 分布式事务

  • 微服务环境下,一个事务由多个分布式应用构成
  • 事务操作位于不同的节点上,原本通过数据库连接控制的单体应用事务不再适用

# 常见分布式事务处理模型

# 两阶段提交(Two-phase Commit,2PC)

引入协调者(Coordinator),来协调参与者的行为,协调者最终决定这些参与者是否要真正执行事务

  • 第一阶段:准备阶段
    • 协调者询问事务参与者,是否执行成功
    • 事务参与者发回事务执行结果
  • 第二阶段:提交阶段
    • 如果所有事务参与者都有成功反馈,则通知所有事务参与者提交事务;
    • 否则,协调者发送通知,让所有的事务参与者回滚事务
  • 存在的问题
    • 事务参与者在等待其它参与者响应时,处于同步阻塞状态
    • 二阶段,如果网络异常,会导致部分Commit事务,使得数据不一致,不可靠;
    • 任意一个节点失败,就会导致事务崩溃,容错性差;

# 补偿事务(Try-Confirm-Cancel,TCC)

针对每个操作,都注册一个与其对应的确认和补偿操作.分为三个阶段.

  • 第一阶段:Try
    • 对业务系统做检测,及资源预留(上锁).
  • 第二阶段:Confirm
    • 对业务系统做确认提交
  • 第三阶段:Cancel
    • 在业务执行错误时,需要回滚的状态下,执行的业务取消,释放预留资源
  • 存在的问题
    • 属于应用层的一种补偿方式,需要程序员在实现时,多写很多补偿代码;
    • 在一些场景中,一些业务流程用TCC不太好定义及处理;
    • 第二阶段,跟第三阶段都可能失败,不稳定;

# 本地消息表(异步确保)

新增本地消息表,它与业务数据表,处于同一个数据库中,利用本地事务,来保证对这两个表的操作满足事务特性。

# MQ事务消息

有一些第三方MQ支持事务消息,如RocketMQ.支持事务消息的方式,类似于二阶段提交.

# seata

一款由阿里巴巴开源的分布式事务解决方案,致力于在微服务架构下,提供高性能和简单易用的分布式事务服务。

# 发展历程

  • 原名为fescar(Fast&Easy Commit And Rollback),由2014年的**TXC(Taobao Transaction Constructor)及2016年的GTS(Global Transaction Service)**的技术积累而来。

  • 2019.01,发布0.1版本

  • 2019.08,发布0.8版本

  • 2020.02,发布1.1版本

  • 2021.04,发布1.4.2版本

# 事务模型

  • TM: 控制全局事务的边界,全局事务的开启、提交、回滚
  • RM: 控制分支事务,分支事务的注册、状态汇报、接受事务协调器指令,分支事务的提交和回滚
  • TC:事务协调器,维护全局事务的状态,负责协调并驱动全局事务的提交或回滚

# 事务模式

# AT模式

两阶段提交协议的演变。

  • 一阶段: 业务数据和回滚日志记录在同一个本地事务中提交,释放本地锁和连接资源
  • 二阶段:
    • 成功,则提交异步化
    • 失败,则回滚。 通过一阶段的回滚日志,进行反向补偿

# TCC模式

  • 支持把自定义的事务,纳入到全局事务的管理中。

# Saga模式

  • 长事务解决方案

# Seata-AT模式实践

# 启动nacos作为注册中心

注:nacos1与nacos2有一些差别不兼容,建议选择nacos1.x

# 获取seata-server并修改配置(1.4.2)

  • 其它版本不保证兼容,尤其是配置项等(目前是最新版,20220325)

  • registry.conf

registry {
  # file 、nacos 、eureka、redis、zk、consul、etcd3、sofa
  type = "nacos"
  nacos {
    application = "seata-server"
    serverAddr = "127.0.0.1:8848"
    group = "SEATA_GROUP"
    namespace = ""
    cluster = "default"
    username = ""
    password = ""
  }
}

config {
  # file、nacos 、apollo、zk、consul、etcd3
  type = "file"
  
  file {
    name = "file.conf"
  }
}
  • file.conf
store {
  ## store mode: file、db、redis
  mode = "db"
  
  ## database store property
  db {
    ## the implement of javax.sql.DataSource, such as DruidDataSource(druid)/BasicDataSource(dbcp)/HikariDataSource(hikari) etc.
    datasource = "druid"
    ## mysql/oracle/postgresql/h2/oceanbase etc.
    dbType = "mysql"
    driverClassName = "com.mysql.jdbc.Driver"
    ## if using mysql to store the data, recommend add rewriteBatchedStatements=true in jdbc connection param
    url = "jdbc:mysql://127.0.0.1:3306/seata_server?useUnicode=true&characterEncoding=utf-8&useSSL=false&serverTimezone=UTC&serverTimezone=Asia/Shanghai&rewriteBatchedStatements=true"
    user = "root"
    password = "chenkaihai"
    minConn = 5
    maxConn = 100
    globalTable = "global_table"
    branchTable = "branch_table"
    lockTable = "lock_table"
    queryLimit = 100
    maxWait = 5000
  }
}

# seata-server数据表说明

  • 全局事务表global_table
    • todo
  • 分支事务表branch_table
    • todo
  • 全局锁lock_table
    • todo

# 启动seata-server(TC)

  • 创建库表结构
-- 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(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;
  • 实际执行过程中,字段大小可能会进行调整
  • 在seata-server的lib目录中,将jdbc文件夹下的mysql驱动,拷贝至lib目录;
  • 修改mysql连接参数
  • 执行seata-server.bat

# 启动客户端(TM,RM)

  • 修改数据库连接参数,建表及初始化
SET
FOREIGN_KEY_CHECKS=0;

-- ----------------------------
-- Table structure for t_account
-- ----------------------------
DROP TABLE IF EXISTS `t_account`;
CREATE TABLE `t_account`
(
    `id`      int(11) NOT NULL AUTO_INCREMENT,
    `user_id` varchar(255) DEFAULT NULL,
    `amount`  double(14, 2
) DEFAULT '0.00',
  PRIMARY KEY (`id`)
) ENGINE=InnoDB AUTO_INCREMENT=2 DEFAULT CHARSET=utf8;

DROP TABLE IF EXISTS `t_order`;
CREATE TABLE `t_order`
(
    `id`             int(11) NOT NULL AUTO_INCREMENT,
    `order_no`       varchar(255) DEFAULT NULL,
    `user_id`        varchar(255) DEFAULT NULL,
    `commodity_code` varchar(255) DEFAULT NULL,
    `count`          int(11) DEFAULT '0',
    `amount`         double(14, 2
) DEFAULT '0.00',
  PRIMARY KEY (`id`)
) ENGINE=InnoDB AUTO_INCREMENT=64 DEFAULT CHARSET=utf8;

DROP TABLE IF EXISTS `t_stock`;
CREATE TABLE `t_stock`
(
    `id`             int(11) NOT NULL AUTO_INCREMENT,
    `commodity_code` varchar(255) DEFAULT NULL,
    `name`           varchar(255) DEFAULT NULL,
    `count`          int(11) DEFAULT '0',
    PRIMARY KEY (`id`),
    UNIQUE KEY `commodity_code` (`commodity_code`)
) ENGINE=InnoDB AUTO_INCREMENT=2 DEFAULT CHARSET=utf8;

DROP TABLE IF EXISTS `undo_log`;
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`),
    UNIQUE KEY `ux_undo_log` (`xid`,`branch_id`)
) ENGINE=InnoDB AUTO_INCREMENT=1 DEFAULT CHARSET=utf8;

SET
FOREIGN_KEY_CHECKS=1;
INSERT INTO `t_account`
VALUES ('1', '1', '4000.00');

INSERT INTO `t_stock`
VALUES ('1', 'C201901140001', '水杯', '1000');
  • 在nacos配置中心,配置事务分组,几个应用的事务分组,值为default
seata:
  tx-service-group: automannn-business-order # 事务群组(可以每个应用独立取名,也可以使用相同的名字)
seata:
  tx-service-group: automannn-business # 事务群组(可以每个应用独立取名,也可以使用相同的名字)
seata:
  tx-service-group: automannn-business-stock # 事务群组(可以每个应用独立取名,也可以使用相同的名字)
seata:
  tx-service-group: automannn-business-account # 事务群组(可以每个应用独立取名,也可以使用相同的名字)
group=SEATA_GROUP
dataId=service.vgroupMapping.automannn-business,\
service.vgroupMapping.automannn-business-account,\
service.vgroupMapping.automannn-business-order,\
service.vgroupMapping.automannn-business-stock
content=default
  • 分别启动客户端应用,建议顺序
    • account
    • stocka
    • order
    • business

# 调用接口检验

# 执行成功,全局事务提交

http://localhost:8104/business/buy
{
    "userId": "1",
    "commodityCode": "C201901140001",
    "name": "成是党",
    "count": 59,
    "amount": 54
}

# 执行失败,分布式事务回滚

http://localhost:8104/business/buyFail
{
    "userId": "1",
    "commodityCode": "C201901140001",
    "name": "成是党",
    "count": 59,
    "amount": 54
}

# 官方demo存在的问题

# dubbo版本的问题,导致消费者无法正常发现提供者

  • 通过给服务设置分组,指定同一分组,进行处理;

# 客户端无法发现Seata-Server

  • 在nacos注册中,配置事务分组集群

# 集成指导

# springBoot依赖项

<properties>
	<seata.version>1.4.2</seata.version>
</properties>		

<dependency>
     <groupId>io.seata</groupId>
     <artifactId>seata-spring-boot-starter</artifactId>
     <version>${seata.version}</version>
</dependency>