说在前面
近几年互联网的快速发展已经改变了我们的生活,比如可以足不出户的进行办理银行业务、点外卖等等。而对应的互联网系统随着业务越来越复杂,其架构模型也与时俱进的产生了一些变化。分布式、微服务架构,大家对此一定不陌生,现在如果问拆分微服务的好处,大家都可以想到一大堆,比如开发冲突问题、解耦、独立弹性部署等。
然而事物都有两面性,带来好处的同时,肯定也会带来一些问题,今天我们要谈的就是分布式架构带来的其中一个无法回避的问题:分布式事务。
下面将按照以下的顺序来讲解:
- 什么是事务
- 单机事务
- 分布式事务
- 产生背景
- 理论基础
- 主流解决方案
- 开源框架
- 最后
什么是事务
事务提供一种机制将一个活动涉及的所有操作纳入到一个不可分割的执行单元。简单来说,事务提供一种“要么什么都不做,要么做全套(All or Nothing)”机制。比如交易时一手交钱,一手交货。
单机事务
单机事务一般指数据库本地事务,是由数据库服务(如mysql)自身提供的一种事务实现,说起数据库事务,一定绕不开ACID,即:
- A:原子性(Atomicity):不会停留在中间某个环节,要么全部做,要么全部不做
- C:一致性(Consistency):执行前后系统对外只有初态与终态
- I:隔离性(Isolation):并发场景下,拥有各自完整的数据空间,互不影响
- D:持久性(Durability):有据可查,崩溃、重启后能够恢复
以大家熟悉的Mysql InnoDB引擎来举例,它的ACID 是通过日志和锁来保证的:隔离性是通过数据库锁的机制实现的,持久性通过 Redo Log(重做日志)来实现,原子性和一致性通过 Undo Log 来实现。
分布式事务
定义:一个操作分为很多小的操作,这些小的操作位于不同的服务器之上,保证这些小操作要么全部成功,要么全部失败。本质上是保证不同数据库(存储)的数据一致性。
产生背景
总结来看有2个原因导致了分布式事务的产生:
-
数据库的水平拆分
随着业务数据规模的快速发展,数据量越来越大,单库单表逐渐成为瓶颈。所以我们对数据库进行了水平拆分,将原单库单表拆分成数据库分片。
如下图所示,分库分表之后,原来在一个数据库上就能完成的写操作,可能就会跨多个数据库,这就产生了跨数据库事务问题。
数据库水平拆分.png -
业务服务化拆分
在业务发展初期,单应用系统架构能满足基本的业务需求。但是随着业务的快速发展,系统的访问量和业务复杂程度都在快速增长,单系统架构逐渐成为业务发展瓶颈,解决业务系统的高耦合、可伸缩问题的需求越来越强烈。
如下图所示,将单业务系统拆分成多个业务系统,降低了各系统之间的耦合度,使不同的业务系统专注于自身业务,更有利于业务的发展和系统容量的伸缩。
业务系统按照服务拆分之后,一个完整的业务往往需要调用多个服务,如何保证多个服务间的数据一致性成为一个难题。
业务服务化.png
理论基础
从上面来看分布式事务是随着互联网高速发展应运而生的,这是一个必然。
我们之前说过数据库的 ACID 四大特性,已经无法满足我们分布式事务,这个时候又有一些新的大佬提出一些新的理论。
CAP与BASE理论.png
CAP理论:又被称布鲁尔定理,对于一个分布式计算系统,不可能同时满足一致性(Consistence)、可用性(Availability)、分区容错性(Partition Tolerance)三个设计约束。
- 一致性(Consistency):对某个指定的客户端来说,读操作保证能够返回最新的写操作结果
- 可用性(Availability):非故障的节点在合理的时间内返回合理的响应(不是错误和超时的响应)
- 分区容忍性(Partition Tolerance):当出现网络分区后,系统能够继续工作
分布式系统下网络无法100%可靠,所以分区是一个必然的现象,所以在发生分区时,我们只能选择保证CP还是AP。
关于CAP,这里补充2点:
- CAP关注的粒度是某类数据,而不是整个系统。比如用户管理系统为例,用户账号数据(用户ID、密码)可选择CP,而用户信息数据(昵称、兴趣爱好)可选择AP
- 正常运行情况下,不发生分区时不存在 CP 和 AP 的选择。架构设计时既要考虑分区发生时选择CP还是AP,也要考虑分区没有发生时如何保证CA,分区故障恢复后如何再次达成CA
这里举个例子:以用户管理系统的用户账号数据(选择是CP)为例,则发生分区后,节点1可以继续注册新用户,节点2无法注册新用户(这里就是不符合A的原因,节点2收到注册请求后会返回error),此时节点1可以将新注册但还未同步到节点2的用户记录到日志中,待分区恢复后,同步给节点2,当同步完成后,节点1和节点2就同时满足CA的状态。
BASE理论:基本可用(Basically Available)、软状态( Soft State)、最终一致性( Eventual Consistency)
- 基本可用:分布式系统在出现故障时,允许损失部分可用性,即保证核心可用。比如通过流量削峰、延迟响应、体验降级、过载保护的手段对系统进行服务降级
- 软状态:允许系统存在中间状态,而该中间状态不会影响系统整体可用性
- 最终一致性:系统中的所有数据副本经过一定时间后,最终能够达到一致的状态
BASE理论是CAP理论中AP的延伸,是对互联网大规模分布式系统的实践总结,强调可用性。从CAP理论得知,在分布式系统中实现强一致性必然影响可用性,几乎所有的互联网系统采用的都是最终一致性,只有在决定系统运行的敏感元数据,才考虑采用强一致性,比如与钱相关的支付系统或者金融系统的数据。
下面将介绍几种常见的分布式事务解决方案及它们适用的一致性要求场景:
主流解决方案
是否真的需要分布式事务
在说方案之前,首先你一定要明确你是否真的需要分布式事务?
上面说过出现分布式事务的两个原因,其中有个原因是因为服务化拆分,在选择分布式事务解决方案之前,先分析下能否把需要事务的微服务聚合成一个单机服务,使用数据库的本地事务。
因为不论任何一种方案都会增加系统的复杂度,千万不要因为追求某些设计,而引入不必要的成本和复杂度。
如果你确定需要引入分布式事务可以看看下面几种常见的方案。
方案一:2PC(强一致模型)
典型代表 XA Transactions:X/Open 组织提出的分布式事务处理的规范,定义了事务管理器(Transaction Manager)和局部资源管理器(Local Resource Manager)之间的接口。
XA.png
在 XA 协议中分为两阶段:
- 事务管理器要求每个涉及到事务的数据库预提交(precommit)此操作,并反映是否可以提交。
- 事务协调器要求每个数据库提交数据,或者回滚数据。
优点:
- 尽量保证了数据的强一致,实现成本较低,在各大主流数据库都有自己实现,对于 MySQL 是从 5.5 开始支持。
缺点:
- 单点问题:事务管理器在整个流程中扮演的角色很关键,如果其宕机,比如在第一阶段已经完成,在第二阶段正准备提交的时候事务管理器宕机,资源管理器就会一直阻塞,导致数据库无法使用。
- 同步阻塞:在准备就绪之后,资源管理器中的资源一直处于阻塞,直到提交完成,释放资源。
- 数据不一致:两阶段提交协议虽然为分布式数据强一致性所设计,但仍然存在数据不一致性的可能。
比如在第二阶段中,假设协调者发出了事务 Commit 的通知,但是因为网络问题该通知仅被一部分参与者所收到并执行了 Commit 操作,其余的参与者则因为没有收到通知一直处于阻塞状态,这时候就产生了数据的不一致性。
总的来说,XA 协议比较简单,成本较低,但是其单点问题,以及不能支持高并发(由于同步阻塞)依然是其最大的弱点,所以在互联网系统极少使用。
方案二:TCC(强一致模型)
关于 TCC(Try-Confirm-Cancel)的概念,最早是由 Pat Helland 于 2007 年发表的一篇名为《Life beyond Distributed Transactions:an Apostate’s Opinion》的论文提出。
TCC.png
不同于XA实现在资源层,TCC是一种服务层的二阶段协议,业务开发者需要实现这三个服务接口,第一阶段服务由业务代码编排来调用 Try 接口进行资源预留,所有参与者的 Try 接口都成功了,事务管理器会提交事务,并调用每个参与者的 Confirm 接口真正提交业务操作,否则调用每个参与者的 Cancel 接口回滚事务。
TCC不与具体的服务框架耦合,与底层 RPC 协议无关,与底层存储介质无关,可以灵活选择业务资源的锁定粒度,减少资源锁持有时间,可扩展性好,可以说是为独立部署的 SOA 服务而设计的。
使用TCC时,大部分工作都集中在如何实现 TCC 服务(即TCC需要提供的三个接口)上,使用TCC的主要事项(限于篇幅关系,这里只列出大纲,详细可参考文末参考链接部分):
1、将业务操作分2阶段完成
接入 TCC 前,业务操作只需要一步就能完成,但是在接入 TCC 之后,需要考虑如何将其分成 2 阶段完成,把资源的检查和预留放在一阶段的 Try 操作中进行,把真正的业务操作的执行放在二阶段的 Confirm 操作中进行。比如在账户余额增减时,增加冻结金额含义。
2、并发控制
接口上降低锁粒度,提高并发
3、允许空回滚
Try网络超时后执行二阶段Cancel,此时并没有收到Try,造成空回滚
4、防悬挂控制
Confirm或Cancel比Try先到,TCC 服务在执行晚到的 Try 之后,将永远不会再收到二阶段的 Confirm 或者 Cancel ,造成 TCC 服务悬挂
5、幂等处理
网络重传或异常事务补偿执行,均会造成被重复执行。
优点:强一致,性能高
缺点:强侵入业务,业务需改造,实现难度高、复用度低
适用场景:强隔离、要求严格一致性的业务,执行时间较短的业务,比如交易、支付、账务服务
方案三:基于异步可靠消息(最终一致性模型)
核心是将需要分布式处理的任务通过消息日志的方式来异步执行,适用于最终一致性,对异步服务的执行时间敏感度低的业务。因为无法保证消息有且只投递一次,这种方案需要消费方实现幂等。
- 本地消息表:
本地消息表这个方案最初是 eBay 提出的,eBay 的完整方案 https://queue.acm.org/detail.cfm?id=1394128
image.png
这种方案的核心在于消息生产端需要一个本地消息表和一个定时扫描任务,基本流程如下:
1、事务参与者A将业务操作写DB与写本地消息表(存储需要发送的消息)放入同一个本地事务来保证事务性,执行完DB事务后,再发送消息
2、若1中的消息方发送失败或发送成功但更新消息表时失败,则定时任务去扫描本地消息表状态为未发送成功的消息,更新本地消息表的发送状态为已发送
3、事务参与者B消费到消息后,先进行幂等处理,然后执行业务流程,然后通知事务参与者A将消息状态更新为已消费(需要这一步是因为消费可能失败,导致数据不一致)
4、定时任务还会扫描本地消息表状态为已发送未消费的消息(以RocketMq为例,有可能是因为消费端多次重试均失败后已进入死信队列),确认后进行补发。
优点:实现难度相对容易
缺点:数据库事务性能扩展问题,增加业务无关的消息表
这种方式因为简单有效,在互联网公司落地场景较多。 -
事务MQ:
image.png
在 RocketMQ 中实现了分布式事务,实际上是对本地消息表的一个封装,将本地消息表移动到了 MQ 内部。
基本流程如下:
1、 发送Prepared 消息至MQ,此时的消息不能被消费,拿到消息的标记
2、执行本地事务
3、通过第一阶段拿到的地址去访问消息,并修改状态为确认,此时消息消费者才能使用这个消息
如果有消息迟迟没有得到确认,MQ服务器会向消息发送者发送消息(这里要求消息发送者需要提供回查接口),来判断是否提交Prepared消息。
优点:不依赖本地数据库,无需增加业务无关表
缺点:需提供回查接口,实现复杂,支持产品较少
这种方式因为开源版本不提供,并且复杂,难以掌控,除阿里内部外,极少使用。
方案四:Saga(最终一致性模型)
金融核心上的业务(比如在渠道层、产品层、集成层的系统),这些系统的特点是最终一致即可、流程多、流程长、还可能要调用其它公司的服务(如金融网络),无法要求其它公司的服务也遵循 TCC 这种开发模式,同时流程长,事务边界太长会影响性能。所以对于更多的金融核心以上的业务系统可以采用补偿事务,补偿事务处理方面在30年前就提出了 Saga 理论,随着微服务的发展,近些年才逐步受到大家的关注。目前业界比较也公认 Saga 是作为长事务的解决方案。
核心思想:将长事务拆分为多个本地短事务,由 Saga 事务协调器协调
两种模式:向前恢复(T1,T2,T3,...,Tn),向后恢复(T1,T2,...,Tj,Cj,...,C2,C1)
saga.png
同TCC模型一样设计到多个阶段,所以补偿服务也要处理好空补偿、防悬挂及幂等补偿问题。
优点:
- 一阶段提交本地数据库事务,无锁,高性能;
- 参与者可以采用事务驱动异步执行,高吞吐;
- 补偿服务即正向服务的“反向”,易于理解,易于实现
缺点:
- 没有机制来保证隔离性,需要业务层面解决,比如设计时“宁可长款,不可短款”,即先扣客户钱款再入账,多收钱可以再退款,以免造成资损。
适用场景:
- 业务流程长、业务流程多
- 参与者包括遗留系统或者第三方系统
- 银行业金融机构使用广泛
社区和业务的方案有:
- Apache Camel Saga
- Eventuate Tram Saga
- Apache ServiceComb Saga
- Seata Saga
开源框架
Seata 意为:Simple Extensible Autonomous Transaction Architecture,是一套一站式分布式事务解决方案,提供了 AT、TCC、Saga 和 XA 事务模式。
项目地址:https://github.com/seata/seata
由蚂蚁金服的fescar演进而来,经历了双11的考验,同时对safa、dubbo、springcloud接入做了适配层。
关于seata的设计与实现,另文分析。
最后
设计系统时尽量不要依赖分布式事务,如果非得使用的话,结合自己的业务分析,看看自己的业务比较适合哪一种,是在乎强一致,还是最终一致即可。这里举几个不同的场景使用不同的分布式事务解决方案:
- TCC强一致解决方案:比如在实时交易场景,需要交易、支付、账务服务同时成功或同时失败。交易、支付、账务服务实现自己的Try、Confirm、Cancel接口,比如账户服务在Try接口增加买家冻结资金、减少可用资金,Confirm接口确定Try接口的结果,Cancel接口减少冻结资金,增加可用资金。
- 基于异步可靠消息:比如用户注册成功后需发送邮件场景,用户注册成功与发送MQ消息在同一个本地事务中同步执行,邮件服务消费消息后,进行异步的邮件发送。
-
补偿型的saga机制:比如机票代订业务,代订平台(比如携程)后端连接不同航空公司的订票系统,用户一趟行程可能需要多张机票,依赖不同航空公司的接口,可以在后面订票失败时,调用前面订票接口的补偿接口(退订服务),来完成整个过程的最终一致性。
最后我们用一张图来总结下今天的内容:
分布式事务 (1).png
这篇文章是在现有大量已有资料做的一个整理,感谢行业内的“巨人们”,还要特别感谢boxer和antony提供的参考资料,希望大家看完后有一点点收获,也欢迎共同探讨。
参考资料:
[1] TCC 理论及设计实现指南介绍:https://seata.io/zh-cn/blog/tcc-mode-design-principle.html
[2] 基于 Seata Saga 设计更有弹性的金融应用:https://seata.io/zh-cn/blog/design-more-flexable-application-by-saga.html
[3] 分布式事务详解:https://mp.weixin.qq.com/s/T-Q9eouj4unrWh8Q9bJoOA
网友评论