暂无图片
暂无图片
暂无图片
暂无图片
暂无图片

04.分布式事务初探(图说常见的分布式事务)

阿亮的日志 2021-04-15
515

导读

  • 前面已经更新完了本地事务的一些内容,本篇开始进入分布式事务的相关信息。
  • (本系列持续更新,感兴趣请关注我的公众号,不错过下一波干货)。

前言

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

知识点

  • 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
操作。

1.3

应用实战

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_idamountlocked_amount
1001100000
100200

2.1.try阶段

user_account_idamountlocked_amount
10019500500
10020500
  • user_account_id=1001
    金额减去需要转账的金额,并将锁定金额修改为要转账的金额
  • user_account_id=1002
    锁定金额修改为要转账的金额

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

2.2.confirm阶段

user_account_idamountlocked_amount
10019500500-->0
10020-->500500-->0

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

2.3.cancel阶段

user_account_idamountlocked_amount
10019500-->10000500-->0
10020500-->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,更新消息状态Prepared
    CONFIRMED
  • 7.MQ发送消息到B系统
  • 8.B系统执行业务逻辑,此处B系统需要保障幂等,防止重复消息
  • 9.B系统处理完毕之后,通知消息系统,已完成,此时消息系统修改状态为FINISHED
  • 10.补偿机制:
    • 定时任务去扫描消息系统的消息数据,查看非终态(Prepared
      CONFIRMED
      )的消息
    • 若存在,则调用A系统的查询接口,查询数据是否处理成功
    • 若处理成功,则再次确认并发送消息,即进入第6步
      ,若失败,则删除消息。

5.最大努力通知方案

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

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

6.应用场景

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

参考

  • 本地消息表:https://houbb.github.io/2018/09/02/sql-distribute-transaction-mq
  • 可靠性消息服务: https://ehlxr.me/2019/01/25/eventually-consistency/

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

关于

更多内容可关注公众号

  • Github: https://github.com/liangliang1259/common-notes
  • 公众号


文章转载自阿亮的日志,如果涉嫌侵权,请发送邮件至:contact@modb.pro进行举报,并提供相关证据,一经查实,墨天轮将立刻删除相关内容。

评论