4

04.分布式事务初探

 3 years ago
source link: http://sunliangliang.com/?p=55
Go to the source link to view the article. You can view the picture content, updated content and better typesetting reading experience. If the link is broken, please click the button below to view the snapshot at that time.
neoserver,ios ssh client

04.分布式事务初探

发表于 2021-04-15 | 分类于 分布式事务 | 0 | 阅读次数 10
  • 前面已经更新完了本地事务的一些内容,本篇开始进入分布式事务的相关信息。
  • (本系列持续更新,感兴趣请关注我的公众号,不错过下一波干货)。

目前已经更新的内容如下:

本节内容主要分析下分布式事务的一些解决方案,通过图解的方式,让大家对分布式事务的执行流程有一个大致的概念,后续会通过项目来演示部分方案的使用,以及进行源码的剖析

  • XA事务
    • 两段式提交(2PC)
    • 三段式提交(3PC)
  • TCC事务
  • 本地消息表
  • 可靠性消息服务
  • 最大努力通知方案

1.XA规范

这个主要是针对单服务跨库的实现,目前很少使用,但是他的思想还是值得借鉴的。XA只是一个规范,具体的协议有数据库厂商提供实现。

这里主要涉及到几个角色,如下:

  • AP: 应用程序,如流量充值系统
  • CRM: 通信资源管理器,一般是消息队列中间件来实现
  • TM:事务管理器,一个第三方组件
  • RM:资源管理器,即mysql客户端,jdbc

1.1.2pc事务

两段式事务提交,流程如下

  • 预提交(准备阶段):每个sql客户端,先准备执行,每个sql都确保执行没问题,只是没有提交事务而已
  • 提交阶段: 事务管理器TM收到所有客户端的消息,若有任何一个客户端失败,则整体事务回滚,若都成功了,则每个客户端都提交事务。数据持久化

2pc事务存在的一些问题

  • 同步阻塞:阶段一执行prepare会占用资源,一直都整个分布式事务结束才完成,在此过程中若其他服务访问该资源则会阻塞
  • 单点故障:事务管理器(TM)是单点,若出现故障则整个事务都会出现问题
  • 事务状态丢失:即使TM做成主备系统,那么在选举过程中如果某个程序挂了,会存在不知道某个事务的状态
  • 脑裂问题:若在阶段2,出现异常导致某些数据库没有收到commit消息,那就会出现部分库数据提交,部分没有提交

1.2.3PC分布式事务原理

针对两段式提交协议,主要是解决了2PC协议的一些问题

  • CanCommit阶段:TM给各个数据发送消息,不会实际执行sql,各个库检查自己的网络环境等是否OK
  • PreCommit阶段:若每个库的CanCommit都返回成功,那么就进入该阶段,执行各个SQL,只是不提交事务。若CanCommit阶段受到失败消息,则TM通知各个数据库直接失败,结束分布式事务
  • DoCommit阶段:
    • PreCommit阶段都成功,那么TM发送DoCommit消息给各个库,通知提交事务。
    • 若某个库PreCommit失败或超时未返回,则TM通知其他库,事务回滚
    • 若某个库返回成功,但是长时间未稍等TM通知,则直接提交事务成功
  • 适度解决了同步阻塞问题:加入了canCommit,阻塞时间会缩短
  • 解决TM事务状态丢失问题:若本机数据执行成功且长时间没有TM的反馈,则自行提交事务
  • 引入超时机制:若PreCommit阶段成功了,等待时间超时还未收到TM发送的DoCommit或者Abort消息,则自己只需DoCommit

若TM在DoCommit阶段发送了Abort消息给各个库,但是某个库没有收到消息,会因为超时问题,只需DoCommit操作。

XA分布式事务可以通过Atomikos类库来实现,这部分的话,有兴趣了解的同学可以网上查一下

2.TCC

由于目前的大型系统基本都是通过服务化的方式来处理,因此一般用不到上面的XA单服务多库的方案。而TCC是在服务化中经常用到的一种分布式事务,它采用的是一种补偿的思想。 TCC是由如下3个步骤组成

  • Try:该阶段是对要执行服务的资源进行检查并锁定
  • Confirm:该阶段是对访问进行实际的操作,会将上一阶段锁定的资源变更为使用
  • Cancel:该节点是进行事务的回滚,即任意业务逻辑执行失败,都会将try阶段锁定的资源或者数据恢复到初始状态。

还拿事务最初的时候转账的例子来说明下,假设有一个account表,结构如下:

create table account
(
    id              bigint(10) unsigned auto_increment comment 'ID'
        primary key,
    user_account_id bigint(10)              default 0                         not null comment '用户账户id',
    amount          decimal(10, 2) unsigned default 0.00                      not null comment '余额',
    locked_amount   decimal(10, 2) unsigned default 0.00                      not null comment '冻结金额',
    created_time    timestamp(3)            default '1971-01-01 00:00:00.000' not null comment '创建时间',
    updated_time    timestamp(3)            default '1971-01-01 00:00:00.000' not null comment '更新时间'
)comment '账户金额' collate = utf8mb4_unicode_ci;

这里有两个账户如下,我们需要从账户user_account_id=1001转500元到user_account_id=1002账户中

user_account_id amount locked_amount 1001 10000 0 1002 0 0

2.1.try阶段

user_account_id amount locked_amount 1001 9500 500 1002 0 500
  • user_account_id=1001金额减去需要转账的金额,并将锁定金额修改为要转账的金额
  • user_account_id=1002锁定金额修改为要转账的金额

此时两个账户中都有500元的锁定金额(不可用)。账户1中

2.2.confirm阶段

user_account_id amount locked_amount 1001 9500 500-->0 1002 0-->500 500-->0

若转账的业务逻辑成功,则会将账户1和账户2的金额变更上图

2.3.cancel阶段

user_account_id amount locked_amount 1001 9500-->10000 500-->0 1002 0 500-->0

若出现问题,会执行try阶段的反向操作。

这种业务是对数据一致性要求较高的场景才会使用,必须是系统中的核心,一般都是核心自己场景才会使用。并且最好每个阶段耗时较短。

这种方式手写回滚,补偿逻辑太复杂,业务代码维护成本极高。

3.本地消息表

这个是ebay搞出来的一套思想,扩展及并发能力有限。因此很少使用。

这种方案依赖于每个服务的一个本地消息表,可以通过日志,数据表实现,然后再通过一定的规则去不断的重试。

有兴趣的同学可以自行研究,参考文档

本地消息表

https://houbb.github.io/2018/09/02/sql-distribute-transaction-mq

4.可靠消息最终一致性方案

这种方案是不用本地的消息表,而是直接基于MQ来处理。有一些MQ本身就只穿事务消息,如RocketMQ

它的执行流程如下图:这里涉及到4个组件

  • A系统:事务的发起者,一般是外部请求入口
  • B系统:A系统依赖调用的一个服务
  • 消息服务系统:专门用来做事务服务的一个系统,有些具备事务消息的MQ可以合并(RocketMQ直接将消息服务MQ合并)
  • 事务补偿系统:这个可以放到A系统中,通过定时任务来处理

结合上图,整体流程的执行步骤如下。

  • 1.A系统发送一个Prepare消息到消息服务中
  • 2.消息服务存储该条Prepare消息到消息服务的库中
  • 3.消息服务返回接收消息成功给A系统
  • 4.若步骤3成功,则A系统继续执行业务操作,否则结束
  • 5.A执行业务逻辑成功后,通知消息系统
  • 6.消息系统接收到消息之后,发送消息到MQ,更新消息状态PreparedCONFIRMED
  • 7.MQ发送消息到B系统
  • 8.B系统执行业务逻辑,此处B系统需要保障幂等,防止重复消息
  • 9.B系统处理完毕之后,通知消息系统,已完成,此时消息系统修改状态为FINISHED
  • 10.补偿机制:
    • 定时任务去扫描消息系统的消息数据,查看非终态(PreparedCONFIRMED)的消息
    • 若存在,则调用A系统的查询接口,查询数据是否处理成功
    • 若处理成功,则再次确认并发送消息,即进入第6步,若失败,则删除消息。

5.最大努力通知方案

这个从名称上我们就能猜测到,他大概是需要依赖一个mq,每个服务读取到mq的消息,之后执行自己的本地事务,若失败了就不停的重试,如果重试N次不行,那就放弃。

  • 1.A系统执行本地事务
  • 2.本地事务执行成功,写一条消息到MQ中,然后最大努力通知服务会消费到该消息
  • 3.通知B系统进行业务逻辑调用,全部处理成功后才会进行消息的ACK,若又未成功的则会重试。

6.应用场景

在真实的大型分布式系统中,对于强一致性要求较高的系统一般采用TCC方案,而其他的场景,一般是要求数据的最终一致性,采用RocketMQ等MQ中间件来实现分布式事务。关于TCC方案以及可靠性消息等后续会通过一个手机充值的简单例子来演示,并且会分析下一个TCC框架的组件。

更多内容可关注公众号

坚持有质量的创作,您的支持将支持我继续创作!

About Joyk


Aggregate valuable and interesting links.
Joyk means Joy of geeK