分布式事务

事务的ACID原则

A(Atomicity):原子性:事务的所有操作,要么全部成功,要么全部失败

C(Consistency):一致性:要保证数据库内部完整性约束,声明性约束

I(Isolation):隔离性:对同一资源操作的事务不能同时发生

D(Durability):持久性:对数据库做的 一切修改将永久保存,不管是否出现故障

 微服务案例

微服务下单业务,在下单时会调用订单服务,创建订单并且写入数据库。

然后订单服务调用账户服务和库存服务:
账户服务负责扣减用户余额
库存服务负责扣减商品库存 ​​​​

但是如果订单微服务呵账户微服务都操作数据库没问题,但是到了库存微服务这里库存不足,那么库存微服务肯定是会操作微服务是失败的 。这时候怎么办呢?普通的事务是监听不到远程调用的账户服务和库存服务的,@Transactional是使用了AOP来实现的,增强了我们的方法,使用了后置增强如果出错了就回滚事务,但是远程的账户服务和库存服务我们是操作不了的,因为每个微服务都是独立的,只能访问微服务中开放出来的接口而不能操作它

CAP理论

C(Consistency):一致性 ——  微服务中访问每个集群得到的数据必须一致

A(Availability):可用性——    微服务中每个健康的集群必须对外提供服务 

P(Partition tolerance):分区容错性——   微服务中,因为网络故障导致出现网络故障的节点和集群出现分区

分布式系统中无法满足这三个指标,因为P是肯定会发生的,所以需要在P的基础上考虑要使用C还是A。


ACP无法同时满足例子例子:



当我们的网络出现故障那么就会出现分区容错性,比如node03出现了网络波动或者网络故障,但是node03本身是没有问题的只是出现了网络问题不能数据同步了,这时候就会出现分区,而我们改变node02的时候,node02会将数据同步给node01,这时候我们在访问node03的时候会出现两种情况:
1:让node03仍然对外提供服务,那么因为被分区了导致node03和其他两个集群的数据不一样了,所以我们得到node03的数据肯定和node01,node02的数据是不一样的,这样就违背了C的一致性
2:让node03对位拒绝访问,这样就保证了一致性,但是因为node03是健康的节点只不过因为网络波动或者网络故障导致不能数据同步了,这样直接拒绝的话就会违背A的可用性
总结:所以综上所述,我们如果网络出现了故障出现分区,只能在A和C上选一个也就是AP或者CP

举个ES的集群例子:ES集群当一个节点出现了故障,那么就会把节点中的分片数据分给其他节点,这样出故障的这个节点就会直接拒绝访问。这就是使用了CP模式

 Base理论

 base理论是对CAP理论的一种解决思路,它有三个思想如下:
Basically Available(基本可用):分布式中出现故障的的那个节点可以直接拒绝访问保证核心的节点可用
Soft State(软状态):在一定时间内,允许出现短暂的数据不一致
EVentually Consistent(最终一致性):虽然不能保证强一致性,但是在软状态结束以后必须达到一致。比如在5s内可以运行出现摸一个节点不一致让那个出故障的节点去解决它的问题,但是过了这5s那么就必须一致

在分布式事务中最大的问题就是事务不能一起回滚或者提交,导致开头时候说的操作多个微服务数据库一个成功了一个失败了但是成功的依然把钱扣了,但是库存失败了库存量没减
所以在CAP和BASE理论上设置了AP模式和CP模式来解决这个办法

AP模式:

他是实现最终一致性。各个子事务分别提交,允许出现不一致,并且将他们的数据交给一个中间人seata,seata发现他们都成功了那么就会成功,如果都失败了那么就会通知他们每个事务都还原一下

CP模式:

它实现的强一致性。各个子事务在执行完都相互等待不提交,当都执行完毕以后交给seata,seata发现都成功了那么就提交,如果有失败的那么就回滚

总结:最终一致性的意思就是说,不管有没有失败,每个事务执行完毕都会提交事务,但是最后发现有失败的了他们都会采取补救措施还原回去。强一致性就是每个事务执行完毕以后想要提交事务就必须等待其他都执行完毕,如果都成功了才会一起提交事务,如果失败就会回滚

全局事务

全局事务就是整个分布式事务

分支事务

分布式事务中包含的每个子系统的事务,也就是说我们的程序远程调用的事务

saeta

seata是基于 CAP和BASE理论进行开发出来的,seata就是用来解决我们分布式事务的。也就是在我们使用BASE的AP和CP模式进行开发时,用来接收每个分布式的事务状态然后协调全局事务体骄傲还是回滚

Seata事务管理的三个角色

TC(Transaction Coordinator)事务协调者:

它的角色也就是我们的seata,维护全局的事务,用来控制全局事务是否回滚或则提交

RM(Transaction Manager)事务管理器:

用来操作自己分支事务的资源,与TC进行交谈和报告事务

TM(Resource Manager)资源管理器:

用来开启全局事务,定义全局事务的范围,提交或回滚全局事务

Seata的内部运行流程

 解读上图:当我们导入一个依赖就会在每个微服务中出现TM和RM,当我们访问上边那个微服务时候,上边的微服务的TM就会去找TC注册全局事务,然后全局事务给他注册全局事务之后会返回一个id,上边的那个微服务再去拿id调用下边的微服务,这样他们都去执行,通过各自的RM拿注册事务时的id去找TC,将他们的执行结果给TC,TC根据id知道了他们属于那个分支,然后判断他们是否需要提交或者回滚,再将结果告诉RM,RM接收到TC提交或者回滚操作就会去执行。

 seata的部署和集成

seata安装

 首先我们要下载seata-server包,地址在http://seata.io/zh-cn/blog/download.html

在非中文目录解压缩这个zip包,其目录结构如下:

 修改conf目录下的registry.conf文件:

 

 因为seata时保证让我们的微服务都可以请求到的,因为只有请求到才能将自己的事务成功与否高数seata,并且seata本身也是一个集群,那么就需要将seata注册到nacos注册中心。这里使用的时nacos,所以在registry.conf进行配置nacos
 

registry {
  # tc服务的注册中心类,这里选择nacos,也可以是eureka、zookeeper等
  type = "nacos"

  nacos {
    # seata tc 服务注册到 nacos的服务名称,可以自定义 spring.application.name
    application = "seata-tc-server"
    serverAddr = "127.0.0.1:8848"
    group = "DEFAULT_GROUP"
    namespace = ""
    cluster = "SH"
    username = "nacos"
    password = "nacos"
  }
}

config {
  # 读取tc服务端的配置文件的方式,这里是从nacos配置中心读取,这样如果tc是集群,可以共享配置
  type = "nacos"
  # 配置nacos地址等信息
  nacos {
    serverAddr = "127.0.0.1:8848"
    namespace = ""
    group = "SEATA_GROUP"
    username = "nacos"
    password = "nacos"
    dataId = "seataServer.properties"
  }
}

在nacos添加配置

因为seata只是为了让其他微服务将自己的事务状态给他,所以配置的时候,nacos在每个微服务中配置文件都是一样的,这时候就可以使用在nacos中使用配置中心配置
 

# 数据存储方式,db代表数据库
store.mode=db
store.db.datasource=druid
store.db.dbType=mysql
store.db.driverClassName=com.mysql.jdbc.Driver
store.db.url=jdbc:mysql://127.0.0.1:3306/seata?useUnicode=true&rewriteBatchedStatements=true
store.db.user=root
store.db.password=root
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

创建数据库:

特别注意:tc服务在管理分布式事务时,需要记录事务相关数据到数据库中,你需要提前创建好这些表。

新建一个名为seata的数据库,运行sql:


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;

启动TC服务
进入bin目录,运行其中的seata-server.bat即可:

项目整合seata 

 第一步引入依赖

这里不能在父工程直接引入依赖,需要每个服务每个服务的引入,因为只有引入了依赖才会生成RM和TM两个角色,所以想要让每个微服务分别都有各自的RM和TM就需要买个都导入依赖
 

<!--seata-->
<dependency>
    <groupId>com.alibaba.cloud</groupId>
    <artifactId>spring-cloud-starter-alibaba-seata</artifactId>
    <exclusions>
        <!--版本较低,1.3.0,因此排除--> 
        <exclusion>
            <artifactId>seata-spring-boot-starter</artifactId>
            <groupId>io.seata</groupId>
        </exclusion>
    </exclusions>
</dependency>
<dependency>
    <groupId>io.seata</groupId>
    <artifactId>seata-spring-boot-starter</artifactId>
    <!--seata starter 采用1.4.2版本-->
    <version>${seata.version}</version>
</dependency>

第二步配置TC地址

seata:
  registry: # TC服务注册中心的配置,微服务根据这些信息去注册中心获取tc服务地址
    type: nacos # 注册中心类型 nacos
    nacos:
      server-addr: 127.0.0.1:8848 # nacos地址
      namespace: "" # namespace,默认为空
      group: DEFAULT_GROUP # 分组,默认是DEFAULT_GROUP
      application: seata-tc-server # seata服务名称
      username: nacos
      password: nacos
  tx-service-group: seata-demo # 事务组名称
  service:
    vgroup-mapping: # 事务组与cluster的映射关系
      seata-demo: SH

微服务如何根据这些配置寻找TC的地址呢?

我们知道注册到Nacos中的微服务,确定一个具体实例需要四个信息:

- namespace:命名空间
- group:分组
- application:服务名
- cluster:集群名

 XA模式

原理 

 

XA模式属于CP强一致性,在第一阶段上边的微服务已经执行完毕,到那时下边的微服务出错了,这时候下边的微服务就不需要回滚,只需要回滚上边微服务就行,但是如果下边的微服务中有好几个事务并且加了@Transactional在下边微服务中内部是会回滚的。 

 XA具体流程图:

解读:当调用微服务时,上边那个微服务的TM会找TC开启全局事务得到唯一标识id比如是aaa,然后上边那个微服务的RM 会通过id找TC注册分支事务,TC看到id就知道是那个全局事务下的分支了,然后开始执行操作资源,等执行完毕不会提交,直接告诉TC结果,因为会调用下边那个微服务,所以等这两个微服务都执行完并且将结果给TC,然后TM会去访问TC全局事务是应该回滚还是提交,TC判断他们是不是都成功或者有失败的来判断时提交还是回滚给RM,然后RM再进行操作资源回滚或者提交

 

 XA模式优缺点

XA模式的优点是什么?

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

XA模式的缺点是什么?

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

XA模式Java代码实现

 在每个微服务中都添加我们使用的是seata的什么模式,因为只有每个微服务都配置了,这样各个微服务才会知道要执行什么模式对此进行操作
修改application.yml文件(==每个参与事务的微服务==),开启XA模式:

seata:
  data-source-proxy-mode: XA

给发起全局事务的入口方法添加@GlobalTransactional注解:

 

 代码的TM   TC   RM解释

@GlobalTransactional注解必须添加到事务开始的地方,当我们访问creat的时候,这个order的TM就会去找TC开始全局事务,然后

orderMapper.insert(order);
accountClient.deduct(order.getUserId(), order.getMoney());
storageClient.deduct(order.getCommodityCode(), order.getCount());

这三个都会分别注册到order的TM分支中,也就是order的TM有三个分支,然后执行这三个分支,并且三个分支会将自己的结果返回给TC,然后order中的TMTC通过访问TC,TC从得到他们三个分支的结果 判断是返回还是回滚然后结果给每个微服务的RM,然后再去操作资源的回滚或者提交

 AT模式(seata默认的模式 AP)

 执行原理

如上图:入口的微服务TM去TC开启全局事务,然后携带唯一标识符去调用分支,各个分支中的RM通过唯一标识符去TC注册到入口微服务成为分支事务,然后各个分支开始将执行的数据先保存一份到undolog日志中,接着就是运行sql然后直接提交并且把结果通过RM告诉TC,入口分支的TM去找TC询问是否可以提交或者回滚全局事务,然后TC告诉RM是提交还是回滚,这里其实已经提交过了,当TC说要回滚其实就是让我们把undolog日志中的备份数据恢复到更新前就可以了,如果TC说要提交其实就是将备份在undolog的日志删除

 分支执行流程:

AT模式中存在脏写问题

 

解读上图:AT模式中在事务1执行之前会将数据先备份一份到undolog数据表中,防止出现错误,可以直接还原数据。完毕以后 就会直接提交这是将money修改为了90,释放DB锁,但是因为是分布式,当TC还没有给事务1的RM说要不要回滚的时候,这是事务2进来了并且可以拿到DB锁,此时他将money修改为了80,但是TC后来给事务1的RM说他们全局事务中有错的需要还原数据,这时候又将事务1备份的money为100的数据还原了,这时就会导致事务2的钱原本扣除了但是后来因为事务1的还原把事务2的数据给覆盖了。事务2现在的处境也就相当于我们淘宝买东西,原本有90,下单一个10元的商品成功以后我们余额money还有80,但是因为之前其他订单出错让我们的余额又变成了100了,这样不是白嫖了一个订单吗。

解决思路就是引入了全局锁的概念。

在释放DB锁之前,先拿到全局锁。避免同一时刻有另外一个事务来操作当前数据。

 

注意:这个全局锁只是seata的,他其实就是一个数据库,在我们执行的时候将我们的这条数据插入到seata的一个lock_table表中,当我们在想要操作这个数据表的这一行数据时会先去 lock_table表中去查看,如果发现这个lock_table中记录了我们想要操作的那个表中的某行数据正在被事务1使用,那么即使分布式事务2得到了DB锁,但是它只能执行sql语句,当去提交或者回滚时候,必须得到全局锁,不然不行,如果发现全局锁被使用,那么就会间隔10s重新尝试获得全局锁,默认是尝试30次,如果30次都不成功就会任务超时直接回滚

分布式事务正在操作,非分布式操作过来也操作的情况

 

非分布式的事务是没有全局锁的,如果分布式事务 1还没有释放全局锁,而非分布式事务2进来也能拿到这行的DB锁,然后对money进行操作,把原本的90减为80,这时候事务1需要进行还原备份,但是等他还原的时候发现money已经被别人更改了,这时候就会记录异常让人工记录。

AT模式的流程

 

 

 

上边三个图代表的就是一个流程,当我们开启了事务并且入口进去之后就会先把我们的sql中的数据提取出来进行并且将原本要修改的数据保存到undolog中,当我们修改完以后再去进行保存修改后的,并且在我们全局表中记录当前事务也就相当于一个锁,如果其他分布式事务再来是不行的,如果需要回滚那么就根据XID在TC中找到分支的id(其实就是在表中记录的根据XID将branchId查出来),在进行解析出来原本的信息进行匹配,拿修改后的数据进行匹配,如果匹配上了那么就会回滚。

 XA和AT模式比较:

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


尤其这个资源的问题,也就是说:
XA得到数据库中一个DB锁他必须要等整个全局事务一起提交一起回滚才会释放,这样的话如果有成千上百用户调用整个接口中不同行并且都在等待他们所属的全局事务执行完一起提交或者回滚然后释放资源,那么他们将会有成千上百的DB锁被占用,这是对于我们程序来说将会是非常消耗资源

AT模式是得到操作完数据库就会把这个锁的资源释放掉,即使来了成千上百个用户访问不同行,虽然因为seata的全局锁的原因可能也是需要等待的,但是我们这时候等待的并不是真正的锁了,全局锁只是一个表lock_table,这是并没有太多的因为DB锁被占用而造成的资源浪费。

AT模式的优点:

- 一阶段完成直接提交事务,释放数据库资源,性能比较好
- 利用全局锁实现读写隔离
- 没有代码侵入,框架自动完成回滚和提交

AT模式的缺点:

- 两阶段之间属于软状态,属于最终一致
- 框架的快照功能会影响性能,但比XA模式要好很多

 实现AT模式

AT模式中的快照生成、回滚等动作都是由框架自动完成,没有任何代码侵入,因此实现非常简单。

只不过,AT模式需要一个表来记录全局锁、另一张表来记录数据快照undo_log。
 

1)导入数据库表,记录全局锁

导入Sql文件:seata-at.sql,其中lock_table导入到TC服务关联的数据库,undo_log表导入到微服务关联的数据库:


2)修改application.yml文件,将事务模式修改为AT模式即可:

 seata:
  data-source-proxy-mode: AT # 默认就是AT

 TTC模式

TCC模式与AT模式非常相似,每阶段都是独立事务,不同的是TCC通过人工编码来实现数据恢复。需要实现三个方法:

  • Try:资源的检测和预留;

  • Confirm:完成资源操作业务;要求 Try 成功 Confirm 一定要能成功。

  • Cancel:预留资源释放,可以理解为try的反向操作。

比如我们在执行order的订单添加,那么就要扣除我们的余额并且将库存减少。假设这是三个服务,那么就可以给这个三个服务的减少余额和删除库存,添加订单的方法,三别都弄一个相反的方法和一个提交的方法。一般他的应用场景就是操作余额的
例子:举例,一个扣减用户余额的业务。假设账户A原来余额是100,需要余额扣减30元。

阶段一( Try ):检查余额是否充足,如果充足则冻结金额增加30元,可用余额扣除30
 初始余额为100

 余额充足,可以冻结:

 

 时,总金额 = 冻结金额 + 可用金额,数量依然是100不变。事务直接提交无需等待其它事务。

-阶段二(Confirm):假如要提交(Confirm),则冻结金额扣减30

确认可以提交,不过之前可用金额已经扣减过了,这里只要清除冻结金额就好了:

此时,总金额 = 冻结金额 + 可用金额 = 0 + 70  = 70元 

- 段二(Canncel):如果要回滚(Cancel),则冻结金额扣减30,可用余额增加30

需要回滚,那么就要释放冻结金额,恢复可用金额:

 Seata的TCC模型图

 

比如上边的服务是订单服务,下边的是扣钱服务:
那么TM就会找TC开启全局事务,然后每个服务的RM都去注册自己的分支事务然后去执行代码并且直接提交然后将结果状态报告给TC。RM再去找TC询问是回滚还是提交。
如果TC说是回滚:
那么就会调用cancel去冻结的那个表中将冻结的金额去加上即可。
如果TC说是提交 :
那么就会调用Confirm去删除那个冻结表中的数据就算是完成了提交

优缺点

TCC模式的每个阶段是做什么的?

- Try:资源检查和预留
- Confirm:业务执行和提交
- Cancel:预留资源的释放

TCC的优点是什么?

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

TCC的缺点是什么?

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

事务悬挂和空回滚

1空回滚

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

也就是说比如上边那个图片还是比如上边的服务是订单服务,下边的是扣钱服务:那么如果订单服务成功了,扣钱服务失败了,那么扣钱服务扣钱失败也就是没有在数据库把这个钱给扣下去,这样就会TC再给RM说需要回滚时,那么扣钱服务是不需要回滚的,
空回滚的解决思路:执行cancel操作时,应当判断try是否已经执行,如果尚未执行,则应该空回滚。如果执行了在冻结金额表中将数据取出来然后得到扣除的钱,这样再将钱给加上就ok了

2)业务悬挂

对于已经空回滚的业务,之前被阻塞的try操作恢复,继续执行try,就永远不可能confirm或cancel ,事务一直处于中间状态,这就是业务悬挂。

解读:也就是说,还是订单服务和扣钱服务。如果扣钱服务已经失败了并且也已经回滚了,不管是空回滚还是正常的回滚,总归是这个服务是操作失败了,但是如果我们回滚了以后,这个请求故障又恢复了,那么这时候他又开始执行try里面的代码,但是这时候TC是操控不了让他们回滚还是提交的,因为TC在第一次失败时已经发送过请求了,也就是说一次全局事务中TC只能有一次机会给RM发送提交还是失败。
如果已经回滚了但是故障自己好了又来请求try了,这时候就会出现数据库中数据的错误更改。
解决思路:执行try操作时,应当判断cancel是否已经执行过了,如果已经执行,应当阻止空回滚后的try操作,避免悬挂


避免悬挂和空回滚思路解析

避免悬挂
这个解决思路就是在我们的数据库中使用一个表,例如在我们的冻结表中,添加一个字段,这个字段就是用来标记上一次的执行是谁执行的,比如上一次是cancel执行的,那么就会给这个冻结表中添加进去。等到那个发生故障的请求恢复了并且去操作try了,就可以在try中设置一个先查询冻结表,看看是否回滚过也就是是否让cancel操作过,如果操作过那么必然这个请求是发生过故障的,就把他拦截下来

空回滚

当我们的程序出错了,我们的try中可以写代码,如果执行操作数据库成功也就是扣钱那么就会给冻结表中写入扣了多少钱什么的,这时候可以将try操作过的信息记录,这时候如果try出错,而且钱已经被改过了,去cancel回滚是,先判断冻结表中有没有让try操作过,如果冻结表中显示try操作过那么钱肯定是被扣过了这时候cancel正常操作,扣了人家多钱就还给人家多少钱并且将他操作过表的信息插入到冻结表中,如果try出错了检测到try还没来得及修改数据,那么这时候就不能正常回滚了,就需要空回滚了,但是cancel检测冻结表中的这个信息肯定是空的,因为已有try添加金额成功以后才能给冻结表中加数据,这时候的空回滚就直接给冻结表中添加数据标志cancel执行过了,这样也能防止业务悬挂

实例代码:
这里我们定义一张表:也即是冻结表

CREATE TABLE `account_freeze_tbl` (
  `xid` varchar(128) NOT NULL,
  `user_id` varchar(255) DEFAULT NULL COMMENT '用户id',
  `freeze_money` int(11) unsigned DEFAULT '0' COMMENT '冻结金额',
  `state` int(1) DEFAULT NULL COMMENT '事务状态,0:try,1:confirm,2:cancel',
  PRIMARY KEY (`xid`) USING BTREE
) ENGINE=InnoDB DEFAULT CHARSET=utf8 ROW_FORMAT=COMPACT;
 

TCC的Try、Confirm、Cancel方法都需要在接口中基于注解来声明,

我们在account-service项目中的cn.itcast.account.service包中新建一个接口,声明TCC三个接口:
 

@LocalTCC // 设置TCC模式
public interface TCCAccountService {
    /**
     * 从用户账户中扣款
     */
    // 此注解使用在try方法上,方法名称可以自定义
    // 预留冻结
    @TwoPhaseBusinessAction(name = "deduct",commitMethod = "confirm",rollbackMethod = "cancel")
    void deduct(
            @BusinessActionContextParameter(paramName = "userId") String userId,
            @BusinessActionContextParameter(paramName = "money") int money);
    // confirm: 提交
    // BusinessActionContext: 业务上下文对象
    void confirm(BusinessActionContext context);
    // cancel: 事务补偿
    void cancel(BusinessActionContext context);
}

编写实现类

@Service
public class TCCAccountServiceImpl implements TCCAccountService {

    @Autowired
    private AccountMapper accountMapper;
    @Autowired
    private AccountFreezeMapper freezeMapper;

    // try阶段
    @Override
    @Transactional
    public void deduct(String userId, int money) {
        // 0.获取全局事务ID
        String xid = RootContext.getXID();
        // 1.查询account_freeze_tbl表,判断cancel是否执行
        AccountFreeze accountFreeze = freezeMapper.selectById(xid);
        if (accountFreeze!=null){
            // cancel执行过了.不再执行try
            if (accountFreeze.getState()==2){
                freezeMapper.deleteById(xid);
            }
            return;
        }
        // 1.冻结金额 -- 向冻结表中添加一条数据
        AccountFreeze freeze = new AccountFreeze();
        freeze.setUserId(userId);
        freeze.setFreezeMoney(money);
        freeze.setState(AccountFreeze.State.TRY);
        freeze.setXid(xid);
        freezeMapper.insert(freeze);
        // 2.从account表中减去冻结金额
        accountMapper.deduct(userId, money);
    }

    @Override
    @Transactional
    public void confirm(BusinessActionContext context) {
        // 删除冻结数据
        freezeMapper.deleteById(context.getXid());
    }

    @Override
    @Transactional
    public void cancel(BusinessActionContext context) {
        // 查看冻结数据状态,并执行补偿
        // 1.通过业务上下文对象获取
        String userId = context.getActionContext("userId").toString();
        Integer money = (Integer) context.getActionContext("money");
        String xid = context.getXid();
        // 2.判断try是否执行,如果未执行,则进行空回滚
        AccountFreeze accountFreeze = freezeMapper.selectById(xid);
        if (accountFreeze==null){
            // 添加一条记录,标准cancel执行过
            AccountFreeze freeze = new AccountFreeze();
            freeze.setUserId(userId);
            freeze.setFreezeMoney(money);
            freeze.setState(AccountFreeze.State.CANCEL);
            freeze.setXid(xid);
            freezeMapper.insert(freeze);
            return;
        }
        // 3.恢复可用金额
        accountMapper.refund(userId, money);
        // 4.移除冻结金额,修改事务状态
        freezeMapper.deleteById(context.getXid());
    }
}

然后调用那个service就可以了;如果我们的扣除订单的服务也要用TCC那么也需要这样编写扣除订单方法,编写回滚方法,编写提交代码

SAGA模式 

Saga 模式是 Seata 即将开源的长事务解决方案,将由蚂蚁金服主要贡献。

其理论基础是Hector & Kenneth  在1987年发表的论文Sagas。

Seata官网对于Saga的指南:https://seata.io/zh-cn/docs/user/saga.html

在 Saga 模式下,分布式事务内有多个参与者,每一个参与者都是一个冲正补偿服务,需要用户根据业务场景实现其正向操作和逆向回滚操作。

分布式事务执行过程中,依次执行各参与者的正向操作,如果所有正向操作均执行成功,那么分布式事务提交。如果任何一个正向操作执行失败,那么分布式事务会去退回去执行前面各参与者的逆向回滚操作,回滚已提交的参与者,使分布式事务回到初始状态。
 

 

也就是说他就是一个链子,一天一天给下递进,第一个执行完直接提交去第二个,第二个执行完直接提交去第三个,第三个服务错了就会给第二个说发生了错误,第二个进行弥补措施,第二个找 第一个让第一个进行弥补措施

优点:

  • 事务参与者可以基于事件驱动实现异步调用,吞吐高

  • 一阶段直接提交事务,无锁,性能好

  • 不用编写TCC中的三个阶段,实现简单

缺点:

  • 软状态持续时间不确定,时效性差

  • 没有锁,没有事务隔离,会有脏写

四种模式对比 

我们从以下几个方面来对比四种实现:

- 一致性:能否保证事务的一致性?强一致还是最终一致?
- 隔离性:事务之间的隔离性如何?
- 代码侵入:是否需要对业务代码改造?
- 性能:有无性能损耗?
- 场景:常见的业务场景

 seata的高可用和异地容灾

Seata的TC服务作为分布式事务核心,一定要保证集群的高可用性。

搭建TC服务集群非常简单,启动多个TC服务,注册到nacos即可。

但集群并不能确保100%安全,万一集群所在机房故障怎么办?所以如果要求较高,一般都会做异地多机房容灾。

 第一步:

新建一个配置 client.properties:

配置组名称: SEATA_GROUP

# 事务组映射关系
service.vgroupMapping.seata-demo=SH

service.enableDegrade=false
service.disableGlobalTransaction=false
# 与TC服务的通信配置
transport.type=TCP
transport.server=NIO
transport.heartbeat=true
transport.enableClientBatchSendRequest=false
transport.threadFactory.bossThreadPrefix=NettyBoss
transport.threadFactory.workerThreadPrefix=NettyServerNIOWorker
transport.threadFactory.serverExecutorThreadPrefix=NettyServerBizHandler
transport.threadFactory.shareBossWorker=false
transport.threadFactory.clientSelectorThreadPrefix=NettyClientSelector
transport.threadFactory.clientSelectorThreadSize=1
transport.threadFactory.clientWorkerThreadPrefix=NettyClientWorkerThread
transport.threadFactory.bossThreadSize=1
transport.threadFactory.workerThreadSize=default
transport.shutdown.wait=3
# RM配置
client.rm.asyncCommitBufferLimit=10000
client.rm.lock.retryInterval=10
client.rm.lock.retryTimes=30
client.rm.lock.retryPolicyBranchRollbackOnConflict=true
client.rm.reportRetryCount=5
client.rm.tableMetaCheckEnable=false
client.rm.tableMetaCheckerInterval=60000
client.rm.sqlParserType=druid
client.rm.reportSuccessEnable=false
client.rm.sagaBranchRegisterEnable=false
# TM配置
client.tm.commitRetryCount=5
client.tm.rollbackRetryCount=5
client.tm.defaultGlobalTransactionTimeout=60000
client.tm.degradeCheck=false
client.tm.degradeCheckAllowTimes=10
client.tm.degradeCheckPeriod=2000

# undo日志配置
client.undo.dataValidation=true
client.undo.logSerialization=jackson
client.undo.onlyCareUpdateColumns=true
client.undo.logTable=undo_log
client.undo.compress.enable=true
client.undo.compress.type=zip
client.undo.compress.threshold=64k
client.log.exceptionRate=100

接下来,需要修改每一个微服务的application.yml文件,让微服务读取nacos中的client.properties文件:

seata:
  config:
    type: nacos
    nacos:
      server-addr: 127.0.0.1:8848
      username: nacos
      password: nacos
      group: SEATA_GROUP
      data-id: client.properties

这个意思就是,我们把seata分为多个微服务都注册到了nacos,然后我们的服务原本设置死的读取SH的那个seata,这时候我们把读取什么集群的代码配置到nacos的配置中心中,这时候我们就可以手动热更新了,比如想要读取HZ的,直接在nacos设置就可以了。这样如果集群中有HZ这个集群就会去匹配HZ 这个集群的seata,这样就做到了如果一个SH的seata集群不可用,还有HZ的可以热更新

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

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值