分布式事务Seata

文章目录

  • 一. 分布式事务理论基础
    • 1. CAP定理
    • 2. BASE理论
  • 二. 初识seata
    • 1. Seata的架构
    • 2. 部署TC服务
    • 3. 微服务集成Seata
  • 三. Seata实现
    • 1. XA模式
    • 2. AT模式
    • 3. TCC模式
    • 4. SAGA模式
  • 四. 四种模式对比

一. 分布式事务理论基础

事务的ACID原则:

  • 原子性:事务中的所有操作,要么全部成功,要么全部失败
  • 一致性:要保证数据内部的完整约束、声明性约束
  • 隔离性:对统一资源操作的事务不能同时发生
  • 持久性:对数据库的一切修改将永久保存,不管是否出现故障

分布式服务的事务问题:在分布式系统下,一个业务跨越多个服务或数据源,每个服务都是一个分支事务,要保证所有分支事务最终状态一致,这样的事务就是分布式事务。由于各个微服务之间是独立的,即各个微服务之间的状态是不可互知的,这里举个例子,在购买商品时需要涉及三个微服务,分别是创建订单微服务、扣除用户用户账户余额微服务以及扣除商品库存微服务。现在假如用户购买了一个商品,但商品库存不足了现在扣除库存微服务失败,但创建订单微服务和扣除用户余额微服务还是成功执行了,这就导致了微服务之间事务状态不一致,这就是比较常见的分布式服务的事务问题。

1. CAP定理

1998年,加州大学的计算机科学家Eric Brewer提出,分布式系统有三个指标:

  • Consisitency:一致性
  • Availability:可用性
  • Partition tolerance:分区容错性
    Eric Brewer说,分布式系统无法同时满足这三个指标,这个结论就叫做CAP定理

CAP定理-Consistency

用户访问分布式系统中的任意节点,得到的数据必须一致

CAP定理-Availablility:用户访问集群中的任意健康节点,必须能得到响应,而不是超时或拒绝

CAP定理-Partition tolerance
Partition:因为网络故障或其它原因导致分布式系统中的部分节点与其它节点失去联系,形成独立分区
tolerance:在集群出现分区时,整个系统也要持续对外提供服务

2. BASE理论

又CAP定理我们知道,由于分区问题的存在,我们只能在CA或者CP中做出我们的选择,但如果我们不想放弃CAP中的任何一个,BASE理论就可以解决这个问题。
BASE理论是对CAP的一种解决思路,包括三个意思:

  • Basically Available(基本可用):分布式系统在出现故障时,运行损失部分可用性,即保证核心可用
  • Soft State(软状态):在一定时间内,运行出现中间状态,比如临时不一致
  • Eventually Consistent(最终一致性):虽然无法保证强一致性,但是在软状态介绍后,最终达到数据一致

利用BASE理论和CAP定理解决分布式事务问题

  • AP模式:各子事务分别执行和提交,允许出现结果不一致,然后采用弥补措施恢复数据即可,实现最终一致
  • CP模式:各个子事务执行后互相等待,同时提交,同时回滚,达到强一致。但事务等待过程中,处于弱可用状态

可以发现,解决分布式事务,各个子系统之间必须能感知到彼此的事务状态,才能保证状态一致,因此需要一个事务协调者来协调每一个事务的参与者(子系统事务),这里的子系统事务称为分支事务,有关联的各个分支事务在一起称为全局事务

二. 初识seata

1. Seata的架构

Seata是2019年1月份蚂蚁金服和阿里巴巴共同开源的分布式事务解决方案,致力于提供高性能和简单易用的分布式事务服务,为用户打造一站式的分布式解决方案。官网地址

Seata事务管理有三种重要的角色:

  • TC(Transaction Coordinator)-事务协调者:维护全局和分支事务的状态,协调全局事务提交或回滚
  • TM(Transaction Manager)-事务管理器:定义全局事务的范围、开始全局事务、提交和回滚全局事务
  • RM(Resource Manager)-资源管理器:管理分支事务处理的资源,与TC交谈以注册分支事务和报告分支事务的状态,以驱动分支事务提交或回滚

分布式事务Seata_第1张图片
Seata提供了四种不同的分布式事务解决方案:

  • XA模式:强一致性分阶段事务模式,牺牲了一定的可用性,无业务侵入

在 Java 微服务架构中,业务侵入是指微服务之间的业务逻辑耦合。这种耦合通常是由于微服务之间共享了某些业务逻辑而导致的。举个例子,假设有两个微服务 A 和 B,它们都需要对订单进行处理。如果 A 微服务需要调用 B 微服务来查询订单信息,那么它们之间就存在业务侵入。这是因为 A 微服务需要知道 B 微服务的具体实现细节,才能完成它自己的业务逻辑。业务侵入会导致微服务之间的依赖性增加,降低了系统的可扩展性和可维护性。为了避免业务侵入,通常采用以下两种方法:采用服务注册与发现机制,让微服务之间通过接口进行通信,而不是直接调用。将公共的业务逻辑封装到一个独立的微服务中,让其他微服务通过接口来调用,而不是直接依赖具体的实现。

  • TCC模式:最终一致的分阶段事务模式,有业务侵入们也是Seata的默认模式
  • AT模式:最终一致的分阶段事务模式,无业务侵入,也就是Seata的默认模式
  • SAGA模式:长事务模式,有业务侵入

2. 部署TC服务

  1. 下载seata源码包

分布式事务Seata_第2张图片

  1. 修改conf下的application.yaml文件

该配置文件主要分为两个部分
registry:注册中心配置(TC都要注册到注册中心,这样RM才方便找到TC)
config:TC服务的配置内容

修改后的配置文件内容如下:

server:
  port: 7091
spring:
  application:
    name: seata-server
logging:
  config: classpath:logback-spring.xml
  file:
    path: ${user.home}/logs/seata
  extend:
    logstash-appender:
      destination: 127.0.0.1:4560
    kafka-appender:
      bootstrap-servers: 127.0.0.1:9092
      topic: logback_to_logstash

console:
  user:
    username: seata
    password: seata

seata:
  config:
    # support: nacos, consul, apollo, zk, etcd3
    type: nacos
    nacos:
      server-addr: 127.0.0.1:8848
      namespace: 
      group: SEATA_GROUP
      username: nacos
      password: nacos
      data-id: seataServer.properties
  registry:
    # support: nacos, eureka, redis, zk, consul, etcd3, sofa
    type: nacos
    nacos:
      application: seata-server
      server-addr: 127.0.0.1:8848
      group: SEATA_GROUP
      namespace: 
      cluster: default
      username: nacos
      password: nacos
  security:
    secretKey: SeataSecretKey0c382ef121d778043159209298fd40bf3850a017
    tokenValidityInMilliseconds: 1800000
    ignore:
      urls: /,/**/*.css,/**/*.js,/**/*.html,/**/*.map,/**/*.svg,/**/*.png,/**/*.ico,/console-fe/public/**,/api/v1/auth/login
  1. 启动nacos添加配置文件

分布式事务Seata_第3张图片
配置文件的内容如下

# 数据存储方式,db代表数据库
store.mode=db
store.db.datasource=druid
store.db.dbType=mysql
store.db.driverClassName=com.mysql.cj.jdbc.Driver
store.db.url=jdbc:mysql://localhost:3306/seata?useSSL=false
store.db.user=root
store.db.password=123456
store.db.minConn=5
store.db.maxConn=30
store.db.globalTable=global_table
store.db.branchTable=branch_table
store.db.queryLimit=100
store.db.lockTable=lock_table
store.db.maxWait=5000
# 事务、日志等配置
server.recovery.committingRetryPeriod=1000
server.recovery.asynCommittingRetryPeriod=1000
server.recovery.rollbackingRetryPeriod=1000
server.recovery.timeoutRetryPeriod=1000
server.maxCommitRetryTimeout=-1
server.maxRollbackRetryTimeout=-1
server.rollbackRetryTimeoutUnlockEnable=false
server.undo.logSaveDays=7
server.undo.logDeletePeriod=86400000

# 客户端与服务端传输方式
transport.serialization=seata
transport.compressor=none
# 关闭metrics功能,提高性能
metrics.enabled=false
metrics.registryType=compact
metrics.exporterList=prometheus
metrics.exporterPrometheusPort=9898
  1. 创建数据库表
SET NAMES utf8mb4;
SET FOREIGN_KEY_CHECKS = 0;

-- ----------------------------
-- 分支事务表
-- ----------------------------
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(6) NULL DEFAULT NULL,
  `gmt_modified` datetime(6) 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 = Compact;

-- ----------------------------
-- 全局事务表
-- ----------------------------
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(32) 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 NULL DEFAULT NULL,
  `gmt_modified` datetime 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 = Compact;

SET FOREIGN_KEY_CHECKS = 1;
  1. 启动TC服务

错误记录:Unrecognized VM option 'CMSParallelRemarkEnabled' Error: Could not create the Java Virtual Machine. Error: A fatal exception has occurred. Program will exit.

解决方案:jdk版本过高,降低版本即可

错误记录:The stack size specified is too small, Specify at least 640k

解决方案:在启动的sh中配置Jvm的启动参数-Xss即可

可以看到Seata主页面(端口是你yaml文件中配置的端口,账号和密码都是yaml
中配置的)
分布式事务Seata_第4张图片
Nacos中可以看到TC服务部署成功
分布式事务Seata_第5张图片

3. 微服务集成Seata

导入项目seata-demo,项目百度网盘 提取码: ubc7

  • 运行项目所有服务被注册到nacos

分布式事务Seata_第6张图片

  • 引入seata的相关依赖
   <dependency>
            <groupId>com.alibaba.cloudgroupId>
            <artifactId>spring-cloud-starter-alibaba-seataartifactId>
            <exclusions>
                <exclusion>
                    <artifactId>seata-spring-boot-starterartifactId>
                    <groupId>io.seatagroupId>
                exclusion>
            exclusions>
        dependency>
        <dependency>
            <groupId>io.seatagroupId>
            <artifactId>seata-spring-boot-starterartifactId>
            <version>1.5.2version>
            
        dependency>
  • 然后配置applicatioin.yml,让微服务通过注册中心找到seata-server
seata:
  registry:
  #TC服务注册中心的配置,微服务根据这些信息区注册中心获取tc服务地址
  #参考tc服务自己的registry.conf中的配置
  #包括:地址、namespace、group、application-name、cluster
    type: nacos
    nacos: #tc
      server-addr: 127.0.0.1:8848
      namespace: ""
      group: SEATA_GROUP
      application: seata-server #tc服务载nacos中的服务名称
      username: nacos
      password: nacos
  tx-service-group: seata-demo#事务组,根据这个获取tc服务的cluster名称
  service:
    vgroup-mapping: #事务组与tc服务cluster的映射关系
      seata-demo: default

三. Seata实现

1. XA模式

XA规范是X/Open组织定义的分布式事务处理(DTP)标准,XA规范描述了全局的TM与局部的RM之间的接口,几乎所有主流数据库都对XA规范提供了支持。在分布式系统中,XA是一种经典的事务管理协议,它可以协调多个独立的事务管理器,实现分布式事务的ACID属性。XA协议定义了分布式事务的两阶段提交(Two-Phase Commit,2PC)算法,用于协调分布式事务的提交过程。
在XA模式下,分布式事务包括两个阶段:

  • 第一阶段:准备阶段(prepare phase)。在这个阶段,协调者(Coordinator)向所有参与者(Participants)发出“准备提交”的请求,参与者将会执行预提交操作并将其结果保存在一个日志中。如果所有参与者都成功地完成了预提交操作,那么协调者将进入到第二个阶段。
  • 第二阶段:提交阶段(commit phase)。在这个阶段,协调者向所有参与者发出“提交”或“回滚”指令。如果协调者收到了所
    有参与者都已经成功地预提交的回复,则它会向所有参与者发出“提交”指令,否则会向所有参与者发出“回滚”指令。

XA协议的2PC算法可以保证在任何情况下都能够保持ACID属性,但是它也有一些缺点。例如,在第一阶段,如果某个参与者失败了,则协调者必须等待该参与者恢复后才能继续进行事务提交,这可能会导致事务的执行时间过长。此外,XA模式在某些情况下也可能存在性能瓶颈,因为它需要在多个参与者之间进行频繁的网络通信和状态同步。

Seata中的XA模式:

分布式事务Seata_第7张图片
RM一阶段工作:

  • 注册分支事务到TC
  • 执行分支业务sql但不提交
  • 报告执行状态到TC

TC二阶段工作:

  • TC检测分支事务执行状态
    • 如果都成功,则通知所有RM提交事务
    • 如果都失败,则通知所有RM回滚事务

RM二阶段工作:

  • 接受TC指令,提交或回滚事务

总结
XA模式的优点:

  • 事务的强一致性,满足ACID原则
  • 常用的数据库都支持,实现简单,并没有代码侵入

XA模式的缺点:

  • 因为一阶段需要锁定数据库资源,等待第二阶段结束才能释放,性能较差
  • 依赖关系型数据库实现事务

实现XA模式

  • 修改application.yml文件,开启XA模式
data-source-proxy-mode: XA
  • 给发起全局事务的入口方法添加@GlobalTransacctionnal注解,本例中是OrderServiceImpl中的Create方法
 @Override
    @GlobalTransactional
    public Long create(Order order) {
        // 创建订单
        orderMapper.insert(order);
        try {
            // 扣用户余额
            accountClient.deduct(order.getUserId(), order.getMoney());
            // 扣库存
            storageClient.deduct(order.getCommodityCode(), order.getCount());

        } catch (FeignException e) {
            log.error("下单失败,原因:{}", e.contentUTF8(), e);
            throw new RuntimeException(e.contentUTF8(), e);
        }
        return order.getId();
    }
  • 重启服务并测试

测试之前先看数据库的情况

分布式事务Seata_第8张图片

分布式事务Seata_第9张图片

分布式事务Seata_第10张图片

然后使用Postman进行测试

  • 正常情况测试(用户购买的商品没有超过库存,此种情况下能够正常生成订单,用户能正常扣除余额,商品能正常扣除库存)

分布式事务Seata_第11张图片

  • 异常情况测试(用户购买的商品数量超过商品库存,此时AT模式下由于库存不足导致库存扣减微服务失败,所以微服务都会滚,所以事务一致性得到了保证)

分布式事务Seata_第12张图片

事务发生了会滚

在这里插入图片描述

2. AT模式

AT模式同样是分阶段提交事务模型,不过弥补了XA模型中资源锁定周期过长的缺陷。

分布式事务Seata_第13张图片
阶段一RM工作:

  • 记录分支事务
  • 记录Undo-log(数据快照)
  • 执行业务并提交Sql(这里就是和XA不同的地方,这里是直接提交,如果有业务失败,则可以使用数据快照恢复数据库情况)
  • 报告事务状态

阶段二提交时RM工作:

  • 删除undo-log即可

阶段二回滚时RM的工作:

  • 根据undo-log恢复数据更新前

总结

AT模式和XA模式的最大区别是什么?

  • XA模式一阶段不提交事务,锁定资源;AT模式一阶段直接提交,不锁定资源
  • XA模式依赖数据库机制实现回滚;AT模式利用数据快照实现数据会滚
  • XA模式强一致,AT模式最终一致

AT模式脏写问题:假如我们现在有一个事务A修改了数据库的某个值(修改值时要获得DB锁,修改完后释放DB锁),而另一个事务B在前面事务A修改事务后也对这个值进行了修改,现在第一个事务A要进行回滚操作,由于AT模式下是用快照来回滚的导致第二个事务设置的值直接覆盖掉了,也就是所谓的丢失更新的问题,导致这个问题的原因是,事务没有满足隔离性。为了解决这个问题,Seata提供了全局锁机制,由TC记录当前正在操作某行数据的事务,该事务在提交事务前持有全局锁,具备执行权。TC记录全局锁的方式如下:

xid table pk
事务1 accout 1

AT模式死锁问题:但加入全局锁机制后,又引入了新的问题,例如上面事务A获取DB锁后,并获取全局锁提交事务释放DB锁,而此时事务B获得DB锁也对数据库进行修改,并申请获得全局锁提交自己的事务,但此时全局锁在事务A手里,所以事务B需要等待,假定现在事务A现在要进行数据库会滚操作,但操作数据库之前需要获得DB锁,而DB锁现在在事务B手里,两者都在等待对方释放锁,所以陷入了死锁状态,而Seata解决该问题的方法是,设定事务申请获取全局锁不能超过30次,且每次间隔10ms,若没有申请到自己的资源,则释放自己所持有的资源(主动释放)

非Seata管理事务写隔离:前面介绍的事务A以及事务B都是在Seata管理的背景下,如果现在来了个非Seata管理的事务C 对数据库进行写操作,且它不需要获得全局锁,现在还是有可能出现上面的更新丢失的问题。Seata的解决方法是,它准备了两份快照,即数据更新前的快照和数据更新后的快照,如果两个快照不一致就说明在二阶段中有人做了手脚,会触发异常警报,这时候就需要人工处理。

实现AT模式

  • 创建两张表第一张undo_log(放在微服务访问的数据库中,我这里是seata_demo(用来记录快照)
CREATE TABLE `undo_log`  (
  `branch_id` bigint(20) NOT NULL COMMENT 'branch transaction id',
  `xid` varchar(100) CHARACTER SET utf8 COLLATE utf8_general_ci NOT NULL COMMENT 'global transaction id',
  `context` varchar(128) CHARACTER SET utf8 COLLATE utf8_general_ci 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 INDEX `ux_undo_log`(`xid`, `branch_id`) USING BTREE
) ENGINE = InnoDB CHARACTER SET = utf8 COLLATE = utf8_general_ci COMMENT = 'AT transaction mode undo table' ROW_FORMAT = Compact;

创建第二张表,放在seata服务访问的数据库lock_table中,我这里是seata(用来记录全局锁)

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 NULL DEFAULT NULL,
  `gmt_modified` datetime 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 = Compact;
  • 修改事务模式,改为AT
data-source-proxy-mode: AT
  • 测试

测试前先看一下数据库状态

分布式事务Seata_第14张图片
分布式事务Seata_第15张图片
分布式事务Seata_第16张图片

开始测试,解雇和XA模式一样,会发生回滚,这里就不详细展示了

3. TCC模式

前面介绍的AT锁和XA锁实际上都加入了锁的机制,如XA的DB锁和AT的全局锁,由于锁的加入多少会对性能造成损耗。而TCC模式和AT模式非常相似,每阶段都是独立事务,不同的是TCC通过人工编码的方式来实现数据恢复,需要实现三个方法:

  • Try:资源的检测和预留
  • Confirm:完成资源操作业务,要求Try成功Confirm一定要成功
  • Cancel:预留资源释放,可以理解为try反向操作

分布式事务Seata_第17张图片
案例:一个扣除用户余额的业务,假定业务A原来的余额是100,需要余额扣减30
阶段一(Try):检查余额是否充足,如果充足则冻结金额增加30元(是从余额100中冻结30元),可用余额扣除30
阶段二(Confirm):假如要提交(Confirm),则冻结金额扣减30
阶段二(Confirm):如果要回滚(Cancel),则冻结金额扣减30,可用余额增加30

分布式事务Seata_第18张图片

总结
TCC的优点是什么:

  • 一阶段完成直接提交事务,释放数据库资源,性能好
  • 相比AT模型,无需生成快照,无需使用全局锁,性能最强
  • 不依赖数据库事务,而是依赖补偿操作,可以用于非关系型数据库

TCC的缺点是什么:

  • 有代码侵入,需要人为的编写try、Confirm、Cancel接口,太麻烦
  • 需要考虑Confirm和Cancel的失败情况,做好幂等处理
  • 软状态,事务最终一致

所谓的幂等性,是分布式环境下的一个常见问题,一般是指我们在进行多次操作时,所得到的结果是一样的,即多次运算结果是一致的。
也就是说,用户对于同一操作,无论是发起一次请求还是多次请求,最终的执行结果是一致的,不会因为多次点击而产生副作用。

案例 改造account-service服务,利用TTC实现分布式事务:
需求如下:

  1. 修改accout-service,编写try、confirm、cancel逻辑
  2. try业务:添加冻结金额,扣减可用金额
  3. confirm业务:删除冻结金额
  4. cancel业务:删除冻结金额,恢复可用金额
  5. 保证confirm和cancel接口的幂等性
  6. 拒绝空回滚

空回滚:当某分支事务的try阶段阻塞时,可能导致全局事务超时而触发二阶段的cancel操作,在未执行try操作时执行力cancel操作,这时cancel不能做回滚,也就是空回滚

  1. 拒绝业务悬挂

业务悬挂;对于已经空回滚的业务,如果以后继续执行try(阻塞的业务可以执行了),就永远不可能confirm或cancel了,这就是业务悬挂,应当阻止执行空回滚后的try操作,避免悬挂

  • 添加数据表(该数据表是为了实现空回滚,防止业务悬挂,以及幂等性等要求),我们必须在数据库记录冻结金额的同时,记录当前事务id和执行状态。
SET NAMES utf8mb4;
SET FOREIGN_KEY_CHECKS = 0;
DROP TABLE IF EXISTS `account_freeze_tbl`;
CREATE TABLE `account_freeze_tbl`  (
  `xid` varchar(128) CHARACTER SET utf8 COLLATE utf8_general_ci NOT NULL,
  `user_id` varchar(255) CHARACTER SET utf8 COLLATE utf8_general_ci NULL DEFAULT NULL,
  `freeze_money` int(11) UNSIGNED NULL DEFAULT 0,
  `state` int(1) NULL DEFAULT NULL COMMENT '事务状态,0:try,1:confirm,2:cancel',
  PRIMARY KEY (`xid`) USING BTREE
) ENGINE = InnoDB CHARACTER SET = utf8 COLLATE = utf8_general_ci ROW_FORMAT = COMPACT;
SET FOREIGN_KEY_CHECKS = 1;
  • 实现try业务
  1. 记录冻结金额和事务状态到account_freeze表
  2. 扣除account表可用金额
  • 实现confirm业务
  1. 根据xid删除account_freeze表的冻结记录
  • 实现concel业务
  1. 修改account_freeze表,冻结金额为0,state为2
  2. 修改account表,恢复可用金额
  • 如何判断空回滚
  1. cancel业务中,根据xid查询account_freeze表,如果为null说明try还没做,需要空会滚
  • 如何避免业务悬挂
  1. try业务中,需要xid查询account_freeze,如果已经存在则证明cancel已经执行,拒绝执行try业务
  • 编写TCC业务接口
@LocalTCC   //表示是TCC业务
public interface AccoutTCCService {
    /**
     * 这个方法上面加了一个TwoPhaseBusinessAction注解表示这是Try方法,commitMethod表示confirm方法,rollbackMethod表示cancel方法
     * @BusinessActionContextParameter:该注解标记出来的参数都会放到上下文对象中
     * @param UserId
     * @param money
     */
    @TwoPhaseBusinessAction(name="deduct",commitMethod = "confirm",rollbackMethod = "cancel")
   void deduct(@BusinessActionContextParameter(paramName = "userId") String UserId, @BusinessActionContextParameter(paramName = "money") int money);

    /**
     * confirm方法
     * @param ctx 这是一个事务上下文对象,该对象可以获得事务相关信息,以及事务的一些参数信息
     * @return
     */
    boolean confirm(BusinessActionContext ctx);
    boolean cancel(BusinessActionContext ctx);

}
  • 实现TCC业务接口
@Service
@Slf4j
public class AccoutTCCServiceImpl implements AccoutTCCService {
    @Autowired
    private AccountMapper accountMapper;
    @Autowired
    private AccountFreezeMapper accountFreezeMapper;
    @Override
    @Transactional
    public void deduct(String UserId, int money) {
        //获取事务id
        String xid = RootContext.getXID();
        //判断是否空业务悬挂
        AccountFreeze accountFreeze=accountFreezeMapper.selectById(xid);
        if (accountFreeze !=null) { //判断是否有冻结记录,如果有,一定是Cancel执行了,我要拒绝业务
           return;
        }
        //1. 尝试扣减可用余额
        accountMapper.deduct(UserId,money);
        //2. 记录冻结金额和事务状态
        AccountFreeze myaccountFreeze = new AccountFreeze();
        myaccountFreeze.setUserId(UserId);
        myaccountFreeze.setFreezeMoney(money);
        myaccountFreeze.setState(AccountFreeze.State.TRY);
        myaccountFreeze.setXid(xid);
        accountFreezeMapper.insert(myaccountFreeze);
       
    }

    @Override
    public boolean confirm(BusinessActionContext ctx) {
        //获取事务ID
        String xid = ctx.getXid();
        //根据ID删除记录
        int cout = accountFreezeMapper.deleteById(xid);
        return cout==1;
    }

    @Override
    public boolean cancel(BusinessActionContext ctx) {

        //获取事务id
        String xid = ctx.getXid();
        //判断是否做空回滚
        AccountFreeze accountFreeze=accountFreezeMapper.selectById(xid);
        if (accountFreeze==null) { //为null表示try现在还没有做,需要空回滚
            //空回滚也需要插入记录
            AccountFreeze myaccountFreeze = new AccountFreeze();
            myaccountFreeze.setUserId(ctx.getActionContext("userId").toString());
            myaccountFreeze.setFreezeMoney(0);
            myaccountFreeze.setState(AccountFreeze.State.CANCEL);
            myaccountFreeze.setXid(xid);
            accountFreezeMapper.insert(myaccountFreeze);
            return true;
        }
        //判断幂等性
        if(accountFreeze.getState()==AccountFreeze.State.CANCEL)
        {
            //表示依据经过Cancel处理了
            return true;
        }
        //查询冻结记录
        //恢复可用余额
        accountMapper.refund(accountFreeze.getUserId(),accountFreeze.getFreezeMoney());
        //将冻结金额清0
        accountFreeze.setFreezeMoney(0);
        accountFreeze.setState(AccountFreeze.State.CANCEL);
        int count=accountFreezeMapper.updateById(accountFreeze);
        //状态改为cancel
        return count==1;
    }
}
  • 修改Controller调用TCC业务
@RestController
@RequestMapping("account")
public class AccountController {

    @Autowired
    private AccoutTCCService accoutTCCService;

    @PutMapping("/{userId}/{money}")
    public ResponseEntity<Void> deduct(@PathVariable("userId") String userId, @PathVariable("money") Integer money){
        accoutTCCService.deduct(userId, money);
        return ResponseEntity.noContent().build();
    }
}
  • 测试

分布式事务Seata_第19张图片

分布式事务Seata_第20张图片

4. SAGA模式

Saga模式(没有隔离性)是seata提供的长事务解决方案。也分为两个阶段:

  • 一阶段:直接提交本地事务
  • 二阶段:成功则什么都不做,失败则通过编写补偿业务来回滚

所谓长事务,就是需要长时间执行的事务,这类事务往往需要访问大量的数据对象,其执行周期甚至能达到几周或几月。但传统的事务执行时需要锁定占用资源,如果在这样的一个场景下,资源被长期锁定,带来的性能消耗可想而知。因此我们引入了SAGA模式来解决长事务。

分布式事务Seata_第21张图片

Sega模式优点:

  • 事务参与者可以基于事件驱动实现异步调用,吞吐量大
  • 一阶段直接提交事务,无锁,性能好
  • 不用编写TCC中的三个阶段实现简单

Sega模式缺点:

  • 软状态持续时间不确定,时效性差
  • 没有锁,没有事务隔离,会有脏写

四. 四种模式对比

XA AT TCC SAGA
一致性 强一致 弱一致 弱一致 最终一致
隔离性 完全隔离 基于全局锁隔离 基于资源预留隔离 无隔离
代码侵入 有,需要编写三个接口 有,要编写状态机和补偿业务
性能 非常好 非常好
场景 对一执性、隔离性有高要求的业务 基于关系型数据库的大多数分布式事务场景都可以 对性能要求较高的事务、有非关系型数据库要参与的事务 业务流程长、业务流程多以及参与者包含其它公式或遗留系统服务,无法提供TCC模式要求的三个接口

你可能感兴趣的:(SpringCloude,分布式,java,微服务)