一个复杂的系统往往都是从一个小而简的系统发展衍化而来,为了满足日益增长的业务需求,不断的增加系统的复杂度,从单体架构逐步发展为分布式架构,而分布式系统架构的设计主要关注:高性能,高可用,高拓展。
分布式事务
高可用是指系统无中断的执行功能的能力,代表了系统的可用程度,是进行系统设计时必须要遵守的准则之一。
而高可用的实现方案,无外乎就是冗余,就存储的高可用而言,问题不在于如何进行数据备份,而在于如何规避数据不一致对业务造成的影响。
对于分布式系统而言,要保证分布式系统中的数据一致性就需要一种方案,可以保证数据在子系统中始终保持一致,避免业务出现问题。
这种实现方案就叫做分布式事务,要么一起成功,要么一起失败,必须是一个整体性的事务。
理论基础
在讲解具体方案之前,有必要了解一下分布式中数据设计需要遵循的理论基础,CAP 理论和 BASE 理论,为后面的实践铺平道路。
CAP 理论
CAP,Consistency Availability Partition tolerance 的简写:
-
Consistency:一致性,对某个客户端来说,读操作能够返回最新的写操作结果。
-
Availability:可用性,非故障节点在合理的时间内返回合理的响应。
-
Partition tolerance:分区容错性,当出现网络分区后,系统能够继续提供服务,你知道什么是网络分区吗?
-
CP 架构
-
AP 架构
①CP 架构
-
当没有出网络分区时,系统 A 与系统 B 的数据一致,X=1。
-
将系统 A 的 X 修改为 2,X=2。
-
当出现网络分区后,系统 A 与系统 B 之间的数据同步数据失败,系统 B 的 X=1。
-
当客户端请求系统 B 时,为了保证一致性,此时系统 B 应拒绝服务请求,返回错误码或错误信息。
②AP 架构
-
当没有出网络分区时,系统 A 与系统 B 的数据一致,X=1。
-
将系统 A 的 X 修改为 2,X=2。
-
当出现网络分区后,系统 A 与系统 B 之间的数据同步数据失败,系统 B 的 X=1。
-
当客户端请求系统 B 时,为了保证可用性,此时系统 B 应返回旧值,X=1。
BASE 理论
-
BA:Basically Available 基本可用,分布式系统在出现故障的时候,允许损失部分可用性,即保证核心可用。
-
S:Soft State 软状态,允许系统存在中间状态,而该中间状态不会影响系统整体可用性。
-
E:Consistency 最终一致性,系统中的所有数据副本经过一定时间后,最终能够达到一致的状态。
分布式事务协议
X/Open XA 协议
XA 接口是双向的系统接口,在事务管理器(Transaction Manager)以及一个或多个资源管理器(Resource Manager)之间形成通信桥梁。
2PC:二阶段提交协议
-
该分布式系统中,存在一个节点作为协调者(Coordinator),其他节点作为参与者(Cohorts)。且节点之间可以进行网络通信。
-
所有节点都采用预写式日志,且日志被写入后即被保持在可靠的存储设备上,即使节点损坏不会导致日志数据的消失。
-
所有节点不会永久性损坏,即使损坏后仍然可以恢复。
-
投票阶段
-
提交阶段
-
协调者向所有参与者询问是否可以执行提交操作,并开始等待各参与者的响应。
-
参与者执行事务操作,如果执行成功就返回 Yes 响应,如果执行失败就返回 No 响应。
-
如果协调者接受参与者响应超时,也会认为执行事务操作失败。
-
如果第一阶段汇总所有参与者都返回 Yes 响应,协调者向所有参与者发出提交请求,所有参与者提交事务。
-
如果第一阶段中有一个或者多个参与者返回 No 响应,协调者向所有参与者发出回滚请求,所有参与者进行回滚操作。
-
单点故障,由于协调者的重要性,一旦协调者发生故障,参与者会一直阻塞,尤其是在第二阶段,协调者发生故障,那么所有的参与者都处于锁定事务资源的状态中,而无法继续完成事务操作。
-
同步阻塞,由于所有节点在执行操作时都是同步阻塞的,当参与者占有公共资源时,其他第三方节点访问公共资源不得不处于阻塞状态。
-
数据不一致,在第二阶段中,当协调者向参与者发送提交事务请求之后,发生了局部网络异常或者在发送提交事务请求过程中协调者发生了故障,这会导致只有一部分参与者接收到了提交事务请求。
而在这部分参与者接到提交事务请求之后就会执行提交事务操作。但是其他部分未接收到提交事务请求的参与者则无法提交事务。从而导致分布式系统中的数据不一致。
3PC:三阶段提交协议
-
CanCommit
-
PreCommit
-
DoCommit
-
如果所有参与者都返回 Yes,则执行事务。
-
如果参与者有一个或多个参与者返回 No 或者超时,则中断事务。
-
如果所有参与者都返回正确的 ACK 响应,则提交事务。
-
如果参与者有一个或多个参与者收到错误的 ACK 响应或者超时,则中断事务。
-
如果参与者无法及时接收到来自协调者的提交或者中断事务请求时,会在等待超时之后,会继续进行事务提交。
解决方案
建立时:业务量不大,用户少,系统只是一个单体架构,订单表与库存表都在一个数据库中,这时可以使用 MySQL 的本地事务保证数据一致性。
发展期:业务发展迅速,用户量变多,单数据已经出现了性能瓶颈,按照业务纬度进行分库,分为订单库和库存库,由于跨库跨机器,MySQL 的本地事务不能再保证订单库和库存库的数据一致性。
但是无论系统发展成什么样,我们都要保证业务不出问题,保证订单和库存的数据一致,这时候要思考下在服务之间我们应如何保证数据一致。
强一致性分布式事务
基于 2PC/XA 协议实现的 JTA:我们已经知道了 2PC 和 XA 协议的原理,而 JTA 是 Java 规范,是 XA 在 Java 上的实现。
-
Transaction Manager:常用方法,可以开启,回滚,获取事务。begin(),rollback()...
-
XAResouce:资源管理,通过 Session 来进行事务管理,commit(xid)...
-
XID : 每一个事务都分配一个特定的 XID。
-
基于两阶段提交,有可能会出现数据不一致的情况
-
事务时间过长,阻塞
-
性能低,吞吐量低
最终一致性分布式事务方案
①本地消息表
以本文中例子,在订单系统新增一条消息表,将新增订单和新增消息放到一个事务里完成,然后通过轮询的方式去查询消息表,将消息推送到 MQ,库存系统去消费 MQ。
-
订单系统,添加一条订单和一条消息,在一个事务里提交。
-
订单系统,使用定时任务轮询查询状态为未同步的消息表,发送到 MQ,如果发送失败,就重试发送。
-
库存系统,接收 MQ 消息,修改库存表,需要保证幂等操作。
-
如果修改成功,调用 RPC 接口修改订单系统消息表的状态为已完成或者直接删除这条消息。
-
如果修改失败,可以不做处理,等待重试。
②MQ 消息事务
消息事务一定要保证业务操作与消息发送的一致性,如果业务操作成功,这条消息也一定投递成功。
-
发送 Prepare 消息到消息中间件。
-
发送成功后,执行本地事务。
-
如果事务执行成功,则 Commit,消息中间件将消息下发至消费端。
-
如果事务执行失败,则回滚,消息中间件将这条 Prepare 消息删除。
-
消费端接收到消息进行消费,如果消费失败,则不断重试。
③最大努力通知
如果超过这个次数后还是通知失败,就不再通知,业务系统自行调用支付平台提供一个查询接口,供业务系统进行查询支付操作是否成功。
-
业务系统调用支付平台支付接口, 并在本地进行记录,支付状态为支付中。
-
支付平台进行支付操作之后,无论成功还是失败,都需要给业务系统一个结果通知。
-
如果通知一直失败则根据重试规则进行重试,达到最大通知次数后,不再通知。
-
支付平台提供查询订单支付操作结果接口。
-
业务系统根据一定业务规则去支付平台查询支付结果。
④补偿事务 TCC
-
Try 阶段:对业务系统做检测及资源预留。
-
Confirm 阶段:对业务系统做确认提交,Try 阶段执行成功并开始执行 Confirm 阶段时,默认 Confirm 阶段是不会出错的。即:只要 Try 成功,Confirm 一定成功。
-
Cancel 阶段:在业务执行错误,需要回滚的状态下执行的业务取消,预留资源释放。
比如下一个订单减一个库存:
-
Try 阶段:订单系统将当前订单状态设置为支付中,库存系统校验当前剩余库存数量是否大于 1,然后将可用库存数量设置为库存剩余数量 -1。
-
如果 Try 阶段执行成功,执行 Confirm 阶段,将订单状态修改为支付成功,库存剩余数量修改为可用库存数量。
-
如果 Try 阶段执行失败,执行 Cancel 阶段,将订单状态修改为支付失败,可用库存数量修改为库存剩余数量。
-
ByteTCC,github.com/liuyangming
-
tcc-transaction:github.com/changmingxi
作者:陈明羽,就职于京东零售