手把手带你了解分布式事务

相关系列

数据库事务

对于小型的系统而言,我们只需要单体应用的形式去部署,所有的操作都在一个数据库上,我们只需要使用数据库的事务方式就能满足数据一致性,数据库事务主要有以下特点:

  • 特性(ACID):原子性、一致性、隔离性、持久性;
  • 问题:脏读、不可重复读、幻读;
  • 隔离级别:读未提交、读已提交、可重复读、串行化;

数据库事务的特性

数据库的事务有这几个特性:原子性(Atomicity)、一致性(Consistency)、隔离性(Isolation)、持久性(Durabilily),简称ACID;

  • 原子性:指事务是一个不可分割的工作单位,事务包含的所有操作要么全部成功,要么全部失败;
  • 一致性:指事务前后,数据库从一个一致性状态变换到另一个一致性状态,比如A和B一共有2000,无论A和B怎么相互转账,他们的总金额都是2000;
  • 隔离性:当多个用户并发操作某一数据时,数据库为每个用户开启一个事务,多个并发事务执行相互隔离,不能被其他事务所影响;
  • 持久性:一个事务一旦提交了,那么数据库中对应数据会产生永久性的变换,即使数据库系统出现故障的情况下,数据也不会丢失;

数据库事务存在的问题

在并发情况下,多个事务同时操作一条数据容易出现数据异常现象,具体现象如下:

脏读

一个事务读到另外一个事务还没有提交的数据,称之为脏读。

如果事务A在操作的过程中更新了某条数据V的值,然后事务B读取到了V被事务A修改后的值,这时事务A因为出现异常导致回滚了,数据V又还原为原先的值了,那么事务B刚刚读取到的值其实是不存在的。

不可重复读

一个事务先后读取同一条记录,但两次读取的数据不同,称之为不可重复读。

不可重复读强调的是其他事务对数据进行了修改或者删除,如:事务A先读取了一条数据V,然后事务B将这条数据V进行了更新或者删除操作,并且成功提交了事务,这时事务A再次读取了这条数据V,发现前后2次读取的数据不一致

幻读

指一个事务前后2次查询,读取到的记录数不一致。

相比于不可重复读,它强调的是记录条数的不一致。事务A通过查询读取到了1条记录,接着事务B插入了一条新的数据,并提交了事务,然后事务A再次进行了查询,结果返回了2条记录;

数据库事务的隔离级别

根据上述的三种不一致现象,SQL定义了四个隔离级别,隔离级别从低到高分别为:读未提交、读已提交、可重复的、串行化。随着隔离级别的提高,数据库的性能逐渐降低。

Read Uncommitted: 读未提交

未提交的写事务不允许其他事务进行修改,但允许其他事务读取该数据,因此会出现脏读、不可重复读的情况

Read Committed: 读已提交
  • 未提交的写事务不允许其他事务读取,所以不会出现脏读;
  • 读事务允许其他事务访问并修改该行数据,所以可能会出现不可重复读的情况;
Repeatable Read: 可重复度
  • 读事务允许其他的读事务对数据进行访问,但是禁止其他的写事务对数据进行操作,所以在事务期间数据不会改变,除非该事务自己修改了数据;
  • 允许其他事务进行新增操作,所以会出现幻读的情况;
Serializable:串行化
  • 事务隔离的最高级别,要求所有的事务只能一个接着一个的顺序执行,性能最低;
  • 可以避免脏读、不可重复读、幻读的出现;

分布式事务

随着系统的数据量变大,单表已经不足以支撑其系统的运行,这时就需要进行分库和分表;当系统的业务越来越庞大,单体系统的维护部署越来越低效,这时就需要拆分业务,即业务的服务化,每个业务都独立为一个服务,单独部署。当一个业务需要多个不同的服务、多个不同的数据库一起参与进来的时候,就需要采用分布式事务去保证所有的操作要么全部成功,要么全部失败。

2PC(Two-phase Commit) 两阶段提交

在分布式系统中,每个节点只能知道本身是否操作成功,但是无法知道其他节点是否操作成功。当一个事务有多个节点参与进来的时候,为了保证所有的节点的数据一致性,要么所有节点全部执行,要么所有节点全部不执行,需要引入一个协调者来统一调度。由协调者来决定这些节点最终是否提交事务。

2PC将事务的提交分为两个阶段:请求阶段(Commit-request)提交阶段(Commit)。简单来讲就是所有参与者将各自的执行结果告知协调者,协调者根据收到的结果决定所有参与者是提交还是回滚操作

  1. 协调者询问所有参与者是否可以进行提交,并等待所有参与者响应;
  2. 所有参与者开始执行业务(但是不提交事务),并告知协调者自己的执行结果成功(本地事务执行成功)还是失败(本地事务执行失败),然后等待协调者通知最终是提交事务还是回退事务;
  3. 如果协调者收到所有节点都执行成功了,那么通知所有节点全部进行提交事务操作,否则只要存在1个参与者执行失败,或者协调者超时了还没有收到全部参与者的执行结果,那么通知所有参与者回退事务;
  4. 所有参与者根据协调者的通知,统一进行提交或者回退事务,并反馈信息;
举个例子

现在有个会议要召开,领导通知大家到会议室集合,收到通知的员工到会议室占好座位,如果人到齐了,那么开始会议;结果不凑巧,小明生病今天请假了,导致人不全,领导通知取消这次会议,大家各自忙各自的。

2PC存在的问题:
  • 资源同步阻塞:在执行的过程中,所有参与节点访问的资源都是独占的,其他第三方节点访问这些资源的时候将被阻塞;
  • 单点故障:如果协调者出现故障,那么所有的参与者占有的资源会一直阻塞下去;
  • 数据不一致:在第二阶段中,如果协调者发送了Commit通知,这时出现了网络问题,导致只有部分参与者收到了通知,进行了事务提交,而其他未收到通知的参与者仍然处于阻塞状态,这时就造成了数据的不一致;

3PC(Three-phase commit) 三阶段提交

三阶段提交是二阶段提交的改进版,将2PC中的准备阶段一分为二,用于保证在最后提交阶段之前,所有的节点状态都是一致的。并且在协调者和参与者中都引入了超时机制,一旦参与者长时间没有收到协调者的通知,那么参与者将执行提交事务操作

  1. CanCommit阶段
    • 协调者询问所有参与者是否可以进行提交,并等待所有参与者响应;
    • 所有参与者预估判断是否可以提交(这里不执行事务),将结果(YES/NO)反馈给协调者;
    • 如果上一阶段存在参与者返回NO,或者协调者等待超时,那么中断事务,不继续后面的操作;
    • 如果所有参与者都返回YES,则进入PreCommit阶段;
  2. PreCommit阶段:
    • 协调者通知参与者进入准备阶段,并等待参与者响应;
    • 参与者执行事务(但不提交),并将执行结果反馈给协调者,然后等待协调者通知最终是提交事务还是回退事务;
  3. DoCommit阶段
    • 如果所有参与者都反馈了YES,那么协调者向参与者发送提交事务的通知;
    • 参与者返回NO,或者协调者等待超时,那么协调者向参与者发送回退事务的通知;
举个例子

领导想要在明天下午开个全员大会,所以提前询问大家明天下午要开会是否有空(询问是否可以执行),如果有员工没空,那么直接通知大家取消这次会议会议。如果大家反馈说都有空(参与者反馈),那么就决定明天下午开会,领导再次通知大家:确定了明天下午开会,大家一定要到场(锁定资源)。到了第二天下午,领导通知大家要开会啦,可以到会议室集合了。大家反馈:我们已经到会议室了(执行commit操作,并反馈

3PC存在的问题:

在协调者向所有参与者发送回退事务指令的情况下,如果因为网络原因导致参与者没有收到通知,当参与者等待超时后会自动执行提交事务,这样就造成了数据不一致的现象。

TCC(Try - Confirm - Cancle) 补偿事务

TCC主要在应用层面上,需要我们自己编写业务逻辑,TCC将业务分TryConfirmCancle三部分逻辑。Try为尝试执行业务,如果Try阶段执行成功进入Confirm阶段,确认执行业务,否则进入Cancle阶段,取消执行业务。下面以订单系统来具体讲解,用户下单、扣库存、添加积分。

Try阶段:尝试执行业务,完成业务的检查,预留业务需要的资源

将订单状态设为UPDATING,该状态表示正在处理该订单;
库存表需要新增一个预留库存字段,库存减1,预留库存设为1;
积分表需要新增一个待新增字段,将本次需要添加的积分存入该字段;

Confirm阶段:直接使用Try阶段的预留资源执行业务,这里不需要进行业务校验,因为在Try阶段已经校验过了

将订单状态修改为PAYED,表示该订单已支付;
库存表的预留库存字段清0;
积分表中将待新增积分加入积分中,并将待新增积分清0;

Cancle阶段:取消执行业务

将订单状态修改为CANCELED,表示该订单已取消;
库存表库存+1,预留库存字段清0;
积分表中待新增积分清0;

通常我们需要引入TCC分布式事务框架来感知各个步骤的执行状态,以此判断下一步操作是什么。常用的 TCC 开源组件有 Tcc-transaction、ByteTCC、Spring-cloud-rest-tcc。在执行的过程中TCC分布式事务框架会记录事务的活动日志,保留了事务各个阶段的运行状态。当遇到某个服务的Confirm或者Cancle执行失败,可以通过活动日志,重新调用Confirm或者Cancle的业务逻辑,知道成功。

TCC的缺点

对业务侵入性大,每个相关的业务都需要配置Try,Confirm,Cancle三块的的业务逻辑。

基于消息队列的最终一致性

对于简单的业务,我们服务之间通常都是同步调用的,一个服务调用另一个服务的接口,使用TCC分布式事务就能实现事务的一致性。但是对于复杂的业务,同步调用的方式,耗时久,整体性能就比较差了,用户体验也差,通常我们可以基于消息队列的最终一致性来异步实现分布式事务。这样的好处是异步操作,响应速度快,大家各自处理自己的业务,不需要关心其他服务的执行结果,通过MQ来进行服务之间的交互。

比如:用户下了个订单,订单系统处理好订单业务后向MQ发送一条消息,然后直接返回用户下单成功,不需要关心库存系统、积分系统、物流系统是否执行成功。

为了确保最终的一致性,我们要确保服务能够收到消息

可靠消息最终一致性方案

需要一个可靠消息服务,用来记录需要发送MQ的内容,以及当前消息状态;消息服务内设置一个定时任务,定时查找超时且状态为未完成的数据,对这些数据进行确认操作,确保数据最终一致性。上游服务和下游服务都需要提供一个查询本次业务是否执行成功的接口,供消息服务调用。

  1. 上游服务先向消息服务发送一条消息,消息内容为发送给下游MQ中的消息内容;
  2. 消息服务收到消息后,将消息落地,并将状态设为待确认
  3. 上游服务执行业务,并将执行结果发送给消息服务
  4. 消息服务收到上游业务处理结果,如果上游失败,那么消息服务删除对应的消息;如果上游执行成功,那么消息服务将对应的消息状态修改为已发送,并且向MQ发送一条消息(更新状态和发送MQ需要在一个事务中执行);
  5. 下游服务消费MQ消息,执行本地业务。
  6. 如果下游执行成功了,那么通知消息服务已执行成功。
  7. 消息服务收到下游服务执行成功的信息后,更新消息状态为已完成

可靠性保障:
如果上游服务执行完成后通知消息服务时,出现问题,导致消息服务没有收到上游的通知:

此时消息服务中的状态为待确认,消息服务会定时去查询那些处理超时且状态为待确认的信息,回调上游业务是否执行成功的接口,如果上游反馈已经执行成功了,那么消息服务将状态改为已发送,并且向MQ发送一条消息(同样需要在一个事务中执行);如果上游反馈执行失败了,那么消息服务删除对应的消息;

下游服务没有消费到消息或者下游服务业务处理失败,导致消息服务没有收到下游的反馈:

此时消息服务中的状态为已发送,消息服务会定时去查询那些处理超时且状态为已发送的信息,回调下游业务是否执行成功的接口,如果下游反馈已经执行成功了,那么消息服务将状态改为已完成,如果反馈说还没有执行,那么消息服务重新向下游服务发送MQ消息;

日常求赞

创作不易,如果各位觉得有帮助,求点赞 支持


求关注

微信公众号: 俞大仙

俞大仙


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

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值