分布式事务解决方案之 Seata(一):分布式事务的常见解决方案及 Seata 简介
作者:行百里er
博客:https://chendapeng.cn (opens new window)
提示
这里是 行百里er 的博客:行百里者半九十,凡事善始善终,吾将上下而求索!
本文将从分布式事务着手,先弄清楚如下2个问题:
- 什么是分布式事务,有哪些解决方案?
- Seata 是什么?
# 分布式事务
在一个系统中,通常是通过数据库来控制事务的,数据库和业务在同一个服务器中,这种基于数据库的事务就是本地事务,与本地事务对应的就是分布式事务。
在微服务架构下,通常一个服务处理一类事情,部署在一个服务器节点上,微服务之间互相进行通信。比如商城系统总的 下单业务
,需要经过 扣减库存
,生成订单
,为用户增加积分
等操作(中间其实还有支付操作),这些操作必须全部成功,才说明本次下单成功。如果中间任何一个环节出现问题,都不算本次操作成功。
这种在分布式环境下具有原子性的操作的事务就是分布式事务。
其实只要涉及到操作多个数据源,就可能会产生事务问题,当然在实际开发中我们要尽量避免这种问题的出现,如果避免不了,我们就需要解决这种问题。
在我们的微服务系统架构中,Seata 就是专门为解决分布式事务问题而生的。
# 分布式事务解决方案
分布式事务的产生就是为了能够解决分布式环境下数据的一致性问题,单一数据库可以通过数据库事务(ACID)来保证自己的事务处理,但是在分布式环境下,涉及的就是多个不同的服务不同的数据库,单一的事务处理是无法满足此要求的。
甚至,当数据量增加,数据库还会做分库分表,这样就存在同时操作不同库中的表的情况,那么如何保证数据的一致性呢?分布式事务就是解决数据一致性问题的。
ACID :事务的基本属性。
A tomic :原子性,构成事务的所有操作,要么都执行完成,要么全部不执行,不可能出现部分成功部分失 败的情况。
C onsistency :一致性,在事务执行前后,数据库的一致性约束没有被破坏。你说你给了别人100块,别人的账户却没有增加100,还反复强调:“100块钱都不给我,还要打我!”这就是数据的不一致。
I solation :隔离性,数据库中的事务一般都是并发的,隔离性是指并发的两个事务的执行互不干扰,一个事务不能看到其他事务运行过程的中间状态。通过配置事务隔离级别可以避脏读、重复读等问题。
D urability :持久性,事务完成之后,该事务对数据的更改会被持久化到数据库,且不会被回滚。
分布式事务的解决方案一般有:两阶段提交型
,TCC(Try-Confirm-Cancel)补偿型
,最终一致性型
和 最大努力通知型
。
# 两阶段提交(2-Phrase Commit,2PC)
两阶段提交,顾名思义,就是整个事务处理的过程中分为两个阶段,具体是:
- 准备阶段
- 提交阶段
我们谈论的分布式事务,因此必然存在多个节点操作不同的数据库,每个节点可以知道自己的操作是否成功,但是不知道其他节点的操作是成功还是失败。
那么为了整个事务的 ACID ,就需要一个中间人作为 协调者 来统一掌控所有节点,协调者知道所有节点的执行结果并最终确定并指示相应的节点是否执行最后的提交。
举个例子,两人 Jack 和 Rose 去领结婚证,首先接待员 Manager 需要分别收取这两个人的身份证等证明材料,只有这两个参与者的身份证都成功拿到了(阶段一每个参与者都执行成功
),才进行发放结婚证的操作(阶段二执行提交
)。
假如,再给 Manager 身份证的时候,Jack 已经给了,但是 Rose 的身份证忘带了或者不愿意给,那么 Manager 退还给 Jack 的身份证(阶段一执行失败
),并且决定不给他俩人发结婚证(阶段二回滚
)。
在该例子中,Jack 和 Rose 领结婚证这个事务,Jack 和 Rose 是参与者,Manager 是协调者,协调者知道两人提交资料的成功与否,并最终决定是否给他们发结婚证。
两阶段提交的思路可以概括为:参与者将操作的成功与否通知协调者,再由协调者根据参与者反馈的情况来决定各参与者是提交还是终止操作。
# TCC 补偿型
TCC,Try-Confirm-Cancel,也是分布式事务中一种处理方式,分别对应:Try
,Confirm
,Cancel
三种操作:
Try
:预留业务资源;Confirm
:确认执行业务操作;Cancel
:取消执行业务操作。
该方案的思路是:
1, 通过 Try
先锁住对象资源进行预留操作,只要预留资源这一步成功了,才会有后面的操作;
2, 资源预留成功后,执行 Confirm
操作,也就是对 Try
阶段锁定的资源进行业务操作;
3, 如果 Confirm
执行失败,则执行 Cancel
操作,也就是在所有操作失败时的回滚操作,释放预留资源。
比如,Jack 向 Rose 转钱,首先需要将 Jack 和 Rose 的账户锁住,即将账户资源预留出来,然后进行转账操作,对双方的账户进行加减操作,如果操作失败,则解冻账户,无事发生。
# 最终一致性
最终一致性 也是一种常见的分布式事务解决方案,一般是通过 MQ 进行异步处理。
我们以订单业务中的增加订单和扣减库存为例,这两个服务分别操作了两个数据库,有可能会出现如下集中情况:
- order-service 中创建订单成功了,调用 ware-service 扣减商品库存也成功,最终数据是一致的;
- order-service 中创建订单成功了,调用 ware-service 扣减库存操作失败,最终数据不一致;
- order-service 中创建订单失败了,但是进行了异常处理,会继续调用 ware-service 扣减库存,数据不一致。
解决类似数据不一致的问题,我们可以改造一下,使用 MQ 来解耦,通过消息机制来处理这一类业务。现在常见的 MQ 中,大部分 MQ 是不支持事务的,一般是通过编写一些代码来结合 MQ 使用。阿里的 RocketMQ 是支持分布式事务的。
# 基于 RocketMQ 的分布式事务
RocketMQ 分布式事务的实现,依赖于一下机制:
1, Half Message
,半消息(或预处理消息):当 Broker
收到此类消息后,会存储到 RMQ_SYS_TRANS_HALF_TOPIC
的消息消费队列中,它暂时不会被 Consumer
消费。
2, 检查事务状态: Broker
会开启一个定时任务,消费 RMQ_SYS_TRANS_HALF_TOPIC
队列中的消息,每次执行任务会向 Producer
确认事务执行状态(提交、回滚、未知),如果是未知,等待下一次回调。
3, 消息回查:有一种场景,如果发送预备消息成功,执行本地事务成功,但发送确认消息失败;那么问题就来了,因为 Producer
的业务都已经处理完毕了,就剩下 Consumer
消费了,但是你 commit 失败了,Consumer
消费不到,这里就出现了数据不一致。
RocketMQ 采用 消息状态回查 来解决这种问题,RocketMQ 会定时遍历 commitlog
中的预备消息。
因为预备消息最终肯定会变成 Commit消息
或 Rollback消息
,所以遍历预备消息去 回查本地业务的执行状态 ,如果发现本地业务没有执行成功就 Rollback,如果执行成功就发送 Commit 消息。
# 结合其他 MQ 的分布式事务
我们可以创建一个可靠性消息服务 message-service
作为中间服务,然后结合 MQ 实现数据的最终一致性。
以创建订单扣减库存为例,这种方案的大致思路是:
1, 在 order-service 中进行创建订单操作,如果操作成功,则调用可靠性消息服务 message-service
发送减库存操作;如果操作失败则进行事务回滚,不发消息给 message-service
。
2, 当 message-service
收到消息后,先将消息储存起来,只负责消息的接收。然后再通过消息发送系统 message-task
把储存的没有消费的消息发送到 MQ 。
3, ware-service 消费 MQ 里的消息,所有消息消费完后,如果消费成功,则通知 message-service
该消息已正常消费,否则不通知。message-task
对没有被正常消费的消息进行重试发送,超过重试次数人没有消费则认为消息死亡。
# 最大努力通知型事务
最大努力通知,发起通知方尽最大的努力将业务处理结果通知给接收方,但是消息可能接收不到,此时需要接收方主动调用发起通知方的接口查询业务,通知可靠性的关键在于接收方。
支付回调就是类似的原理,支付接口都需要一个回调地址,在支付成功后,支付接口提供方会将支付结果返回到我们的回调地址,如果没有收到支付成功的通知,支付接口提供方会重复调用我们的接口,直到通知指定次数后不再通知。
# Seata 是什么?
前面介绍了分布式事务以及分布式事务的集中解决方案,下面开始我们本系列的主角:Seata 。容我先抄一下官网对它的描述:
Seata 是一款开源的分布式事务解决方案,致力于提供高性能和简单易用的分布式事务服务。Seata 将为用户提供了 AT、TCC、SAGA 和 XA 事务模式,为用户打造一站式的分布式解决方案。
好家伙,提供了这么多的事务模式,不愧是一站式的分布式事务的解决方案。
从下一篇开始,我将分别介绍 AT、TCC、SAGA 和 XA 事务模式的使用和原理,感谢你的阅读。
首发公众号 行百里er ,欢迎各位关注阅读指正。