分布式事务解决方案

本文详细介绍了分布式事务的几种经典模型,包括2PC(两阶段提交)、3PC(三阶段提交)以及TCC(尝试-确认-取消)模式。对比了它们的优缺点,并着重讲解了Seata的AT和MT模式,分析了Seata在处理分布式事务时如何降低锁的力度和提高系统吞吐量。此外,还讨论了可靠消息服务在确保事务最终一致性的角色,以及在实现过程中可能遇到的问题和解决方案。
摘要由CSDN通过智能技术生成

在这里插入图片描述

分布式事务模型

2PC&3PC

在这里插入图片描述

2PC

  1. tm有超时机制,rm没有超时机制
  2. 在整个分布式事务过程中,资源都是被锁定的

3PC

  1. tm和rm都有超时机制
  2. 3pc在1阶段增加了询问的过程,也就是判断网络通不通,服务是否可用,相对于2pc来说,降低了灾难发生的概率,其他的话跟2pc差不多。
  3. 在整个分布式事务过程中,资源都是被锁定的

TCC(Try-Confirm-Cannel)

需要手动定义确认和回退接口,增加业务的复杂度,实现复杂。
对于简单的分布式业务使用tcc模式无可厚非,如果业务很复杂,那么如果回滚的话,我就需要自己去实现回滚的业务逻辑,比较复杂,得不偿失。因此对于比较复杂的业务逻辑,直接使用lcn模式就行。

分布式事务解决方案

LCN(Lock Confirm Notify)

通过tm协调rm事务,实现分布式事务
使用方式:方法上加上注解@LcnTransation注解

使用步骤:

  • 配置tm,单独建立一个模块,yml文件配置结合redis一起使用
  • 配置服务A做为rm,yml文件配置tm的地址及端口地址
  • 配置服务B做为rm,yml文件配置tm的地址及端口地

本地事件表(通过消息队列+定时任务+本地表)

每个服务都要添加一个本地表,用于存储消息,增加了业务复杂性,这种方式可以增加系统的吞吐量,还有事务消息方案(rocketmq)和可靠消息方案也能提高系统吞吐量

在这里插入图片描述

Seata-AT模式

其中有Tm,Rm,Tc,Xid四个概念,Tm就是事务的发起者,Rm就是事务的参与者,Tc属于事务的协调者,XID就是一个全局事务的唯一标识

执行流程:

  1. TM 向 TC 申请开启一个全局事务,全局事务创建成功并生成一个全局唯一的 XID
  2. XID 在微服务调用链路的上下文中传播。
  3. RM 向 TC 注册分支事务,接着执行这个分支事务并提交(重点:RM在第一阶段就已经执行了本地事务的提交/回滚),最后将执行结果汇报给TC
  4. TM 根据 TC 中所有的分支事务的执行情况,发起全局提交或回滚决议。
  5. TC 调度 XID 下管辖的全部分支事务完成提交或回滚请求。

如下是分布式事务各个参与者的具体执行过程 ,同时模拟并发情况下全局锁的作用:
在这里插入图片描述
Seata-AT模式和传统2PC模式的区别:

一个分布式事务流程可能有很多参与者(RM),在传统2PC模式中,需要等待整个事务全部完成后另一个线程才能获取锁执行。
但在Seata中,锁的力度大大减小,有本地锁和全局锁的概念,每一个RM都有一个本地锁,假如RM1执行逻辑时先获取本地锁,逻辑执行完毕后,本地锁释放,继续执行整个事务中下一个RM2。与此同时,另一个线程也可是同时执行RM1。相比传统2PC模式,锁的力度大大减小。

Seata-MT模式(MT模式本质上是一种TCC方案)

try,confirm,cancel中独立事务,通过业务回滚,此时不需要undo.log表
tcc场景:不仅仅支持带事务的mysql还支持MongoDB等不支持事务的数据库,但是2pc方案就必须依赖事务数据库

在这里插入图片描述

Seata-tcc模式下常见的异常

最常见的主要是这三种异常,空回滚、幂等、悬挂。

1 空回滚:
空回滚就是对于一个分布式事务,在没有调用 TCC 资源 Try 方法的情况下,调用了二阶段的 Cancel 方法,

Cancel 方法需要识别出这是一个空回滚,然后直接返回成功。


< try 未执行,Cancel 执行 >

什么样的情形会造成空回滚呢?注册分支事务是在调用 RPC 时,Seata 框架的切面会拦截到该次调用请求,

先向 TC 注册一个分支事务,然后才去执行 RPC 调用逻辑。如果 RPC 调用逻辑有问题,

比如调用方机器宕机、网络异常,都会造成 RPC 调用失败,即未执行 Try 方法。但是分布式事务已经开启了,

需要推进到终态,因此,TC 会回调参与者二阶段 Cancel 接口,从而形成空回滚。

解决:

需要一张额外的事务控制表,其中有分布式事务 ID 和分支事务 ID,第一阶段 Try 方法里会插入一条记录,

表示一阶段执行了。Cancel 接口里读取该记录,如果该记录存在,则正常回滚;如果该记录不存在,则是空回滚。
2 幂等:
幂等就是对于同一个分布式事务的同一个分支事务,重复去调用该分支事务的第二阶段接口,

因此,要求 TCC 的二阶段 Confirm 和 Cancel 接口保证幂等,不会重复使用或者释放资源。如果幂等控制没有做好,

很有可能导致资损等严重问题。


< 多次 执行 Confirm 或 Cancel>

什么样的情形会造成重复提交或回滚?从图中可以看到,提交或回滚是一次 TC 到参与者的网络调用。因此,

网络故障、参与者宕机等都有可能造成参与者 TCC 资源实际执行了二阶段防范,但是 TC 没有收到返回结果的情况,

这时,TC 就会重复调用,直至调用成功,整个分布式事务结束。

 
解决:

事务控制表的每条记录关联一个分支事务,那我们完全可以在这张事务控制表上加一个状态字段,

用来记录每个分支事务的执行状态。

状态字段有三个值,分别是初始化、已提交、已回滚。Try 方法插入时,是初始化状态。

二阶段 Confirm 和 Cancel 方法执行后修改为已提交或已回滚状态。当重复调用二阶段接口时,

先获取该事务控制表对应记录,检查状态,如果已执行,则直接返回成功;否则正常执行。
3.悬挂:
悬挂就是对于一个分布式事务,其二阶段 Cancel 接口比 Try 接口先执行。

因为允许空回滚的原因,Cancel 接口认为 Try 接口没执行,空回滚直接返回成功,对于 Seata 框架来说,

认为分布式事务的二阶段接口已经执行成功,整个分布式事务就结束了。但是这之后 Try 方法才真正开始执行。


< Confirm 在 try 之前>

什么样的情况会造成悬挂呢?按照前面所讲,在 RPC 调用时,先注册分支事务,再执行 RPC 调用,

如果此时 RPC 调用的网络发生拥堵,通常 RPC 调用是有超时时间的,RPC 超时以后,发起方就会通知 TC 回滚该分布式事务,

可能回滚完成后,RPC 请求才到达参与者,真正执行,从而造成悬挂。

解决:

可以在二阶段执行时插入一条事务控制记录,状态为已回滚,这样当一阶段执行时,先读取该记录,

如果记录存在,就认为二阶段已经执行,进行 空 try ,否则二阶段没执行,正常执行 try。
TCC模式 需要的数据库表:
SET FOREIGN_KEY_CHECKS=0;


DROP TABLE IF EXISTS `branch_table`;
CREATE TABLE `branch_table` (
`branch_id` bigint(20) NOT NULL,
`xid` varchar(128) NOT NULL,
`transaction_id` bigint(20) DEFAULT NULL,
`resource_group_id` varchar(32) DEFAULT NULL,
`resource_id` varchar(256) DEFAULT NULL,
`branch_type` varchar(8) DEFAULT NULL,
`status` tinyint(4) DEFAULT NULL,
`client_id` varchar(64) DEFAULT NULL,
`application_data` varchar(2000) DEFAULT NULL,
`gmt_create` datetime(6) DEFAULT NULL,
`gmt_modified` datetime(6) DEFAULT NULL,
PRIMARY KEY (`branch_id`),
KEY `idx_xid` (`xid`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8;


DROP TABLE IF EXISTS `global_table`;
CREATE TABLE `global_table` (
`xid` varchar(128) NOT NULL,
`transaction_id` bigint(20) DEFAULT NULL,
`status` tinyint(4) NOT NULL,
`application_id` varchar(32) DEFAULT NULL,
`transaction_service_group` varchar(32) DEFAULT NULL,
`transaction_name` varchar(128) DEFAULT NULL,
`timeout` int(11) DEFAULT NULL,
`begin_time` bigint(20) DEFAULT NULL,
`application_data` varchar(2000) DEFAULT NULL,
`gmt_create` datetime DEFAULT NULL,
`gmt_modified` datetime DEFAULT NULL,
PRIMARY KEY (`xid`),
KEY `idx_gmt_modified_status` (`gmt_modified`,`status`),
KEY `idx_transaction_id` (`transaction_id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8;

 

DROP TABLE IF EXISTS `lock_table`;
CREATE TABLE `lock_table` (
`row_key` varchar(128) NOT NULL,
`xid` varchar(96) DEFAULT NULL,
`transaction_id` bigint(20) DEFAULT NULL,
`branch_id` bigint(20) NOT NULL,
`resource_id` varchar(256) DEFAULT NULL,
`table_name` varchar(32) DEFAULT NULL,
`pk` varchar(36) DEFAULT NULL,
`gmt_create` datetime DEFAULT NULL,
`gmt_modified` datetime DEFAULT NULL,
PRIMARY KEY (`row_key`),
KEY `idx_branch_id` (`branch_id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8;

可靠消息,最终一致性

此方案的好处是承受的并发量高,把整条调用链路变成了调用单体的单个功能,我仅仅需要将我的信息发给自己开发的可靠消息服务就行了,就可以直接返回数据了,后面链路怎么走,我不需要管

在这里插入图片描述
遇到的问题:
2-2阶段发送消息失败,那么对于可靠消息服务模块这个服务来说,就会发生回滚操作,消息就会变成待确认状态,怎么解决呢?
可以通过定时任务查询待确认消息然后回调接口4-1阶段回查消息状态,然后去更新消息为已确认

通过Rocketmq实现事务消息

rocketmq本身支持事务消息

在这里插入图片描述

遇到的问题

  • 4.1阶段发送确认消息失败怎么办?
    rocketmq本身自带回查功能,当mq一段时间没有收到对half msg(半消息)的确认时,就会进行5阶段的回查(调用checkLocalTransaction接口),回查成功后,mq就会将消息发给支付服务,如果回查不成功会一直回查下去,你也可以通过数据库加字段限制回查次数
  • rocketmq向支付服务发送消息失败怎么办?
    发送失败进入死信队列,设置消费多少次失败后进入死信队列,默认16次消费不成功进入死信队列,然后监听死信队列进行发邮件之类的进行人工处理
  • 支付服务消息消息失败怎么办?
    支付服务消费消息成功会向broker发送ack(ConsumeConcurrentlyStatus.CONSUME_SUCCESS),如果失败,会发送ConsumeConcurrentlyStatus.RECONSUME_LATER,mq会有重试机制,默认重试16次,如果还失败的话进入死信队列
    在这里插入图片描述
  • 如果支付服务正常消费了消息,但是由于网络原因,mq没有收到ack确认,那么mq会再次投递消息,那不就重复消费了吗?
    可以添加数据库表的形式通过主键的方式保证幂等性,每次消费消息时向数据库插入数据,插入失败说明消息已经被消费过了,就不再消费了

总结分布式事务

在这里插入图片描述

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值