MiniMall:今天说一下Seata分布式事务解决方案

1. 基础概念

1.1 什么是事务

事务可以看做是一次大的活动,它由不同的小活动组成,这些活动要么全成功,要么全失败。比如:一手交钱,一手交货。

1.2 本地事务

在计算机系统中,更多的是通过关系型数据库来控制事务,这是利用数据库本身的事务特性来实现的,因此叫数据
库事务,由于应用主要靠关系数据库来控制事务,而数据库通常和应用在同一个服务器,所以基于关系型数据库的
事务又被称为本地事务。

数据库事务在实现时会将一次事务涉及的所有操作全部纳入到一个不可分割的执行单元,该执行单元中的所有操作
要么都成功,要么都失败,只要其中任一操作执行失败,都将导致整个事务的回滚。

1.3 分布式事务

分布式系统会把一个应用系统拆分为可独立部署的多个服务,因此需要服务与服务之间远程协作才能完成事务操
作,这种分布式系统环境下由不同的服务之间通过网络远程协作完成事务称之为分布式事务,例如商品入库单生效后,增加商品库存就是一个分布式事务问题。

2. 分布式事务解决方案

目前业界常见的解决方案有2PC、TCC、可靠消息最终一致性、最大努力通知这四种。

  • 2PC

2PC即两阶段提交协议,是将整个事务流程分为两个阶段,准备阶段(Prepare phase)、提交阶段(commit phase),2是指两个阶段,P是指准备阶段,C是指提交阶段。

  • TCC

TCC是Try、Confirm、Cancel三个词语的缩写,TCC要求每个分支事务实现三个操作:预处理Try、确认Confirm、撤销Cancel。Try操作做业务检查及资源预留,Confirm做业务确认操作,Cancel实现一个与Try相反的操作即回滚操作。TM首先发起所有的分支事务的try操作,任何一个分支事务的try操作执行失败,TM将会发起所有分支事务的Cancel操作,若try操作全部成功,TM将会发起所有分支事务的Confirm操作,其中Confirm/Cancel操作若执行失败,TM会进行重试。

  • 可靠消息最终一致性

可靠消息最终一致性方案是指当事务发起方执行完成本地事务后并发出一条消息,事务参与方(消息消费者)一定能够接收消息并处理事务成功,此方案强调的是只要消息发给事务参与方最终事务要达到一致。

  • 最大努力通知

最大努力通知是指当A、B两个事务中,B的执行结果会以异步的方式通知A,若A此时无法接收到通知结果,则B会有一个间隔的时间段,再次通知A执行结果,依次类推,知道若干次后,要么通知成功;要么不再推送,由A主动来查询执行结果。

3. Seata实现2PC事务

3.1 Seata方案

Seata是由阿里中间件团队发起的开源项目 Fescar,后更名为Seata,它是一个是开源的分布式事务框架。它通过对本地关系数据库的分支事务的协调来驱动完成全局事务,是工作在应用层的中间件。主要优点是性能较好,且不长时间占用连接资源,它以高效并且对业务0侵入的方式解决微服务场景下面临的分布式事务问题。

Seata把一个分布式事务理解成一个包含了若干分支事务的全局事务。全局事务的职责是协调其下管辖的分支事务达成一致,要么一起成功提交,要么一起失败回滚。此外,通常分支事务本身就是一个关系数据库的本地事务,下图是全局事务与分支事务的关系图:
在这里插入图片描述

Seata定义了3个组件来协调分布式事务的处理过程:
在这里插入图片描述

  • Transaction Coordinator (TC): 事务协调器,它是独立的中间件,需要独立部署运行,它维护全局事务的运行状态,接收TM指令发起全局事务的提交与回滚,负责与RM通信协调各个分支事务的提交或回滚。
  • Transaction Manager ™: 事务管理器,TM需要嵌入应用程序中工作,它负责开启一个全局事务,并最终向TC发起全局提交或全局回滚的指令。
  • Resource Manager (RM): 控制分支事务,负责分支注册、状态汇报,并接收事务协调器TC的指令,驱动分支(本地)事务的提交和回滚。

3.2 业务背景

在商品入库单生效时,我们要调用基础微服务的库存模块,对入库单中的商品进行入库,也就是增加库存的操作。试想一下,如果增加库存失败,那商品入库单必然是不能生效成功的。这就是分布式事务的场景。

3.3 实现步骤

3.3.1 下载Seata服务器

下载地址:https://github.com/seata/seata/releases,目前最新版本:1.2.0,我们使用的是1.1.0。

3.3.2 如何运行

解压后,进入bin目录,然后双击seata-server.bat文件即可。

3.3.3 添加配置

Seata有两个比较重要的配置文件,分别在conf文件夹下的file.confregistry.conf,我们的微服务想要连接到Seata的服务就要将这两个配置文件配置到微服务工程的resources文件夹去。

  • registry.conf

注册到Seata服务的配置文件,包含了注册方式、配置文件读取方式等等,具体的大家看源码中的配置。

registry {
  # file 、nacos 、eureka、redis、zk、consul、etcd3、sofa
  type = "eureka"

  # 这里使用eureka方式注册
  eureka {
    serviceUrl = "http://Anbang713:pwd713@localhost:9010/eureka"
    application = "default"
    weight = "1"
  }
}

config {
  # file、nacos 、apollo、zk、consul、etcd3
  type = "file"

  # 使用文件方式进行配置
  file {
    name = "file.conf"
  }
}
  • file.conf

该配置文件内包含了微服务事务方的一些配置,以及Seata服务的事务日志存储方式。

service {
  #transaction service group mapping
  vgroup_mapping.mall-product-provider-fescar-service-group = "default"
  #only support when registry.type=file, please don't set multiple addresses
  default.grouplist = "127.0.0.1:8091"
  #degrade, current not support
  enableDegrade = false
  #disable seata
  disableGlobalTransaction = false
}

## transaction log store, only used in seata-server
store {
  ## store mode: file、db
  mode = "db"

  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:3380/seata"
    user = "root"
    password = "Anbang713"
    minConn = 1
    maxConn = 10
    globalTable = "global_table"
    branchTable = "branch_table"
    lockTable = "lock_table"
    queryLimit = 100
  }
}

需要注意的是,这两个文件要放在事务参与方所在的微服务中。比如商品入库单生效增加商品库存这一个场景,商品入库单在商品微服务,商品库存在基础微服务。

3.3.4 创建表

在数据库中创建seata数据库,然后初始化以下表:

SET NAMES utf8mb4;
SET FOREIGN_KEY_CHECKS = 0;

-- ----------------------------
-- Table structure for branch_table
-- ----------------------------
DROP TABLE IF EXISTS `branch_table`;
CREATE TABLE `branch_table`  (
  `branch_id` BIGINT(20) NOT NULL,
  `xid` VARCHAR(128) CHARACTER SET utf8 COLLATE utf8_general_ci NOT NULL,
  `transaction_id` BIGINT(20) NULL DEFAULT NULL,
  `resource_group_id` VARCHAR(32) CHARACTER SET utf8 COLLATE utf8_general_ci NULL DEFAULT NULL,
  `resource_id` VARCHAR(256) CHARACTER SET utf8 COLLATE utf8_general_ci NULL DEFAULT NULL,
  `branch_type` VARCHAR(8) CHARACTER SET utf8 COLLATE utf8_general_ci NULL DEFAULT NULL,
  `status` TINYINT(4) NULL DEFAULT NULL,
  `client_id` VARCHAR(64) CHARACTER SET utf8 COLLATE utf8_general_ci NULL DEFAULT NULL,
  `application_data` VARCHAR(2000) CHARACTER SET utf8 COLLATE utf8_general_ci NULL DEFAULT NULL,
  `gmt_create` DATETIME(0) NULL DEFAULT NULL,
  `gmt_modified` DATETIME(0) NULL DEFAULT NULL,
  PRIMARY KEY (`branch_id`) USING BTREE,
  INDEX `idx_xid`(`xid`) USING BTREE
) ENGINE = INNODB CHARACTER SET = utf8 COLLATE = utf8_general_ci ROW_FORMAT = DYNAMIC;

-- ----------------------------
-- Table structure for global_table
-- ----------------------------
DROP TABLE IF EXISTS `global_table`;
CREATE TABLE `global_table`  (
  `xid` VARCHAR(128) CHARACTER SET utf8 COLLATE utf8_general_ci NOT NULL,
  `transaction_id` BIGINT(20) NULL DEFAULT NULL,
  `status` TINYINT(4) NOT NULL,
  `application_id` VARCHAR(32) CHARACTER SET utf8 COLLATE utf8_general_ci NULL DEFAULT NULL,
  `transaction_service_group` VARCHAR(128) CHARACTER SET utf8 COLLATE utf8_general_ci NULL DEFAULT NULL,
  `transaction_name` VARCHAR(128) CHARACTER SET utf8 COLLATE utf8_general_ci NULL DEFAULT NULL,
  `timeout` INT(11) NULL DEFAULT NULL,
  `begin_time` BIGINT(20) NULL DEFAULT NULL,
  `application_data` VARCHAR(2000) CHARACTER SET utf8 COLLATE utf8_general_ci NULL DEFAULT NULL,
  `gmt_create` DATETIME(0) NULL DEFAULT NULL,
  `gmt_modified` DATETIME(0) NULL DEFAULT NULL,
  PRIMARY KEY (`xid`) USING BTREE,
  INDEX `idx_gmt_modified_status`(`gmt_modified`, `status`) USING BTREE,
  INDEX `idx_transaction_id`(`transaction_id`) USING BTREE
) ENGINE = INNODB CHARACTER SET = utf8 COLLATE = utf8_general_ci ROW_FORMAT = DYNAMIC;

-- ----------------------------
-- Table structure for lock_table
-- ----------------------------
DROP TABLE IF EXISTS `lock_table`;
CREATE TABLE `lock_table`  (
  `row_key` VARCHAR(128) CHARACTER SET utf8 COLLATE utf8_general_ci NOT NULL,
  `xid` VARCHAR(96) CHARACTER SET utf8 COLLATE utf8_general_ci NULL DEFAULT NULL,
  `transaction_id` BIGINT(20) NULL DEFAULT NULL,
  `branch_id` BIGINT(20) NOT NULL,
  `resource_id` VARCHAR(256) CHARACTER SET utf8 COLLATE utf8_general_ci NULL DEFAULT NULL,
  `table_name` VARCHAR(32) CHARACTER SET utf8 COLLATE utf8_general_ci NULL DEFAULT NULL,
  `pk` VARCHAR(36) CHARACTER SET utf8 COLLATE utf8_general_ci NULL DEFAULT NULL,
  `gmt_create` DATETIME(0) NULL DEFAULT NULL,
  `gmt_modified` DATETIME(0) NULL DEFAULT NULL,
  PRIMARY KEY (`row_key`) USING BTREE,
  INDEX `idx_branch_id`(`branch_id`) USING BTREE
) ENGINE = INNODB CHARACTER SET = utf8 COLLATE = utf8_general_ci ROW_FORMAT = DYNAMIC;

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) CHARACTER SET utf8 COLLATE utf8_general_ci NOT NULL,
  `context` VARCHAR(128) CHARACTER SET utf8 COLLATE utf8_general_ci NOT NULL,
  `rollback_info` LONGBLOB NOT NULL,
  `log_status` INT(11) NOT NULL,
  `log_created` DATETIME(0) NOT NULL,
  `log_modified` DATETIME(0) NOT NULL,
  `ext` VARCHAR(100) CHARACTER SET utf8 COLLATE utf8_general_ci NULL DEFAULT NULL,
  PRIMARY KEY (`id`) USING BTREE,
  UNIQUE INDEX `ux_undo_log`(`xid`, `branch_id`) USING BTREE
) ENGINE = INNODB AUTO_INCREMENT = 1 CHARACTER SET = utf8 COLLATE = utf8_general_ci ROW_FORMAT = DYNAMIC;

SET FOREIGN_KEY_CHECKS = 1;

在商品微服务和基础微服务对应的数据库分别创建undo_log表:

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) CHARACTER SET utf8 COLLATE utf8_general_ci NOT NULL,
  `context` VARCHAR(128) CHARACTER SET utf8 COLLATE utf8_general_ci NOT NULL,
  `rollback_info` LONGBLOB NOT NULL,
  `log_status` INT(11) NOT NULL,
  `log_created` DATETIME(0) NOT NULL,
  `log_modified` DATETIME(0) NOT NULL,
  `ext` VARCHAR(100) CHARACTER SET utf8 COLLATE utf8_general_ci NULL DEFAULT NULL,
  PRIMARY KEY (`id`) USING BTREE,
  UNIQUE INDEX `ux_undo_log`(`xid`, `branch_id`) USING BTREE
) ENGINE = INNODB AUTO_INCREMENT = 1 CHARACTER SET = utf8 COLLATE = utf8_general_ci ROW_FORMAT = DYNAMIC;

3.3.5 代码开发

1)添加pom依赖
<!--引入seata-->
<dependency>
    <groupId>com.alibaba.cloud</groupId>
    <artifactId>spring-cloud-alibaba-seata</artifactId>
    <exclusions>
    	<exclusion>
    		<groupId>io.seata</groupId>
    		<artifactId>seata-all</artifactId>
    	</exclusion>
    </exclusions>
</dependency>
<dependency>
    <groupId>io.seata</groupId>
    <artifactId>seata-all</artifactId>
    <version>1.0.0</version>
</dependency>
2)添加@GlobalTransactional注解

在事务发起方添加@GlobalTransactional注解,比如对于商品入库单生效增加库存,事务的发起方就是商品入库单生效的方法。

@Override
@Transactional
@GlobalTransactional(rollbackFor = Exception.class)
public void doEffect(String uuid) {
    // 商品入库单生效
    // 调用基础微服务的库存服务增加库存
}

至此,使用Seata分布式事务解决方案介绍结束。实际上,在写这篇博客之前想了很久,因为这个分布式事务这个话题太多,业界也有很多的解决方案,很难在一篇几千字的博文里说清楚,这里也只能介绍一下我们项目中是怎么解决分布式事务的,而且这种东西还是要大家自己动手实践才能感受这整个过程到底是怎么回事。

——End——
更多精彩分享,可扫码关注微信公众号哦。

在这里插入图片描述

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值