ACID在集群环境下,保证集群的ACID几乎是很难达到,或者即使能达到那么效率和性能会大幅下降。这就需要引入一个新的理论原则来适应这种集群的情况,就是 CAP 原则。在分布式系统中,一般可用性的重要程序比一致性要高,所以在通常我会选择AP,即牺牲部分一致性去保证可用性。根据BASE理论:我们无法做到强一致,但每个应用都可以根据自身的业务特点,采用适当的方式来使系统达到最终一致性。
在分布式系统中,要实现分布式事务一般方案为:两阶段提交(2PC)、补偿事务(TCC)、本地消息表(异步确保)、MQ 事务消息、Sagas 事务模型。
两阶段提交
两阶段提交就是使用XA协议的原理:
- 第一阶段:协调者询问所有参与者是否可以执行提交操作,参与者执行准备工作,例如为资源上锁,预留资源,写undo/redo log。
- 第二阶段:若所有参与者回应“可提交”,则向所有参与者发送正式提交命令;若某个参与者回应“拒绝提交”,则向所有参与者发送回滚命令。
XA协议保障了事务的强一致性,然而由于其采用的阻塞协议带来的巨大性能开销,难以达到较高的系统吞吐量。
TCC
对于 TCC 的解释:
Try 阶段:尝试执行,完成所有业务检查(一致性),预留必需业务资源(准隔离性)。
Confirm 阶段:确认真正执行业务,不作任何业务检查,只使用 Try 阶段预留的业务资源,Confirm 操作满足幂等性。要求具备幂等设计,Confirm 失败后需要进行重试。
Cancel 阶段:取消执行,释放 Try 阶段预留的业务资源,Cancel 操作满足幂等性。Cancel 阶段的异常和 Confirm 阶段异常处理方案基本上一致。
本地消息表
核心是将需要分布式处理的任务通过消息日志的方式来异步执行。消息日志可以存储到本地文本、数据库或消息队列,再通过业务规则自动或人工发起重试。人工重试更多的是应用于支付场景,通过对账系统对事后问题的处理。
MQ 事务消息
以RocketMQ为例,消息系统的设计时,要解决消息的顺序、消息的重复这两个问题。
对于消息顺序,RocketMQ通过轮询所有队列的方式来确定消息被发送到哪一个队列(负载均衡策略),订单号相同的消息会被先后发送到同一个队列中。在获取到路由信息以后,会根据MessageQueueSelector
实现的算法来选择一个队列,同一个OrderId获取到的肯定是同一个队列。
对于消息重复,消费端处理消息的业务逻辑保持幂等性,保证每条消息都有唯一编号且保证消息处理成功与去重表的日志同时出现。简单说,就是重复消息,最后处理的结果都一样,并且利用一张日志表来记录已经处理成功的消息的ID,如果新到的消息ID已经在日志表中,那么就不再处理这条消息。
事务消息,将大事务拆分成多个小事务异步执行。这样基本上能够将跨机事务的执行效率优化到与单机一致。
以转账业务为例,在分布式下的事务流程大致为:锁定A账户——通过网络锁定B账户——检查A账户余额是否充足——A扣款——通过网络B增加金额——通过网络解锁B账户——解锁A。在分布式环境下这个过程耗时会很长,如果对这个事务进行拆分将会变为:锁定A——检查A账户——A账户扣款——解锁A;锁定B——B账户增加余额——解锁B;中间使用异步消息发送。但是本地事务(扣款)和发送异步消息应该保证同时成功或者同时失败,扣款成功,发送消息一定要成功,扣款失败,则不能再发送消息。
RocketMQ 事务消息,其思路大致为:
- 第一阶段Prepared消息,会拿到消息的地址。
- 第二阶段执行本地事务。
- 第三阶段通过第一阶段拿到的地址去访问消息,并修改状态。
RocketMQ会定期扫描消息集群中的事物消息,如果发现了Prepared消息
,它会向消息发送端(生产者)确认,A扣款是否成功?若成功是回滚还是继续发送确认消息?RocketMQ会根据发送端设置的策略来决定是回滚还是继续发送确认消息。这样就保证了消息发送与本地事务同时成功或同时失败。
Sagas 事务模型
Sagas 事务模型:该模型其核心思想就是拆分分布式系统中的长事务为多个短事务,或者叫多个本地事务,然后由 Sagas 工作流引擎负责协调,如果整个流程正常结束,那么就算是业务成功完成,如果在这过程中实现失败,那么Sagas工作流引擎就会以相反的顺序调用补偿操作,重新进行业务回滚。