分布式系统事务一致性
单数据库一致性:
1. 利用事务
分布式系统事务一致性:
1. 本地事务消息队列:两段提交,利用本地事务保证消息的可靠性
生产者:
1). 在数据库(mysql)增加一个消息表,将本地数据修改和消息记录放到同一个事务中,保证同时成功或失败。
2). 本地数据修改成功后,事务提交完毕。producer向MQ发送一个消息,发送成功,更新消息表消息为已读
3). 为避免producer消息发送失败的情况。有一个定时任务,定时查询消息表,将未消费的消息放到MQ的消息队列中
消费者:
1). consumer从MQ队列中,取出消息去消费。
2). 先查询这条消息对应的数据是否已被修改,如果已被修改则将消息置为无效。如果消息未被消费,则将消息消费掉,数据更新成功以后,将消息置为已消费。
3). 对于MQ消息量大、有顺序要求的数据,可以通过 特性算法(hash)将制定下消息放到同一个队列中,对应一个 消费者。
总结:
这方式是一种非常经典的实现,基本避免了分布式事务,实现了“最终一致性”。但是,关系型数据库的吞吐量和性能方面存在瓶颈,频繁的读写消息会给数据库造成压力。
所以,在真正的高并发场景下,该方案也会有瓶颈和限制的。
2. MQ(非事务消息)
生产者(数据库修改和发送消息操作在一个事务中,发送消息操作可以通过异常可以进行数据回滚):
1). 操作数据库成功,向MQ中投递消息也成功,皆大欢喜
2). 操作数据库失败,不会向MQ中投递消息了
3). 操作数据库成功,但是向MQ中投递消息时失败,向外抛出了异常,刚刚执行的更新数据库的操作将被回滚
消费者端面临的问题:
1). 消息出列后,消费者对应的业务操作要执行成功。如果业务执行失败,消息不能失效或者丢失。需要保证消息与业务操作一致
2). 尽量避免消息重复消费。如果重复消费,也不能因此影响业务结果
风险点:
如果生产端(假设为A),A对数据库修改完成,发送MQ消息,但是事务还没有提交。消费者可能已经开始消费MQ里面的消息,如果有用到A端的数据,可能查不到。如果要保证流程的严谨性,
应该保证消费端的执行,对应A端数据库提交的内容没有依赖。
总结:
这种方式比较常见,性能和吞吐量是优于使用关系型数据库消息表的方案。如果MQ自身和业务都具有高可用性,理论上是可以满足大部分的业务场景的。不过在没有充分测试的情况下,不建议在交易业务中直接使用。
3. MQ(事务消息)
下面以阿里巴巴的RocketMQ中间件为例,分析下其设计和实现思路。
RocketMQ第一阶段发送Prepared消息时,会拿到消息的地址,第二阶段执行本地事物,第三阶段通过第一阶段拿到的地址去访问消息,并修改状态。细心的读者可能又发现问题了,
如果确认消息发送失败了怎么办?RocketMQ会定期扫描消息集群中的事物消息,这时候发现了Prepared消息,它会向消息发送者确认,Bob的钱到底是减了还是没减呢?如果减了是
回滚还是继续发送确认消息呢?RocketMQ会根据发送端设置的策略来决定是回滚还是继续发送确认消息。这样就保证了消息发送与本地事务同时成功或同时失败。
发送端设置的策略 可以通过实现 指定的接口,由业务方自己来实现。
总结:
据笔者的了解,各大知名的电商平台和互联网公司,几乎都是采用类似的设计思路来实现“最终一致性”的。这种方式适合的业务场景广泛,而且比较可靠。不过这种方式技术实现的难度比较大。目前
主流的开源MQ(ActiveMQ、RabbitMQ、Kafka)均未实现对事务消息的支持,所以需二次开发或者新造轮子。比较遗憾的是,RocketMQ事务消息部分的代码也并未开源,需要自己去实现。
4. 补偿机制(重试补偿机制:调用超时,服务短暂宕机等):
做过支付宝交易接口的同学都知道,我们一般会在支付宝的回调页面和接口里,解密参数,然后调用系统中更新交易状态相关的服务,将订单更新为付款成功。同时,只有当我们回调页面中输出了success字样
或者标识业务处理成功相应状态码时,支付宝才会停止回调请求。否则,支付宝会每间隔一段时间后,再向客户方发起回调请求,直到输出成功标识为止。
其实这就是一个很典型的补偿例子,跟一些MQ重试补偿机制很类似。
5. 人工补偿机制:
为了交易系统更可靠,我们一般会在类似交易这种高级别的服务代码中,加入详细日志记录的,一旦系统内部引发类似致命异常,会有邮件通知。同时,后台会有定时任务扫描和分析此类日志,检查出
这种特殊的情况,会尝试通过程序来补偿并邮件通知相关人员。在某些特殊的情况下,还会有“人工补偿”的,这也是最后一道屏障。
如何避免消息被重复消费造成的问题?
1). 保证消费者调用业务的服务接口的幂等性
2). 通过消费日志或者类似状态表来记录消费状态,便于判断(建议在业务上自行实现,而不依赖MQ产品提供该特性)
参见:http://www.infoq.com/cn/articles/solution-of-distributed-system-transaction-consistency
https://www.zhihu.com/question/27707687 (消息队列顺序)
互联网系统大多将 强一致性 需求转换成 最终一致性 的需求,并通过系统执行 幂等性 的保证,保证数据的最终一致性。
2PC(Two Phase Commit,两阶段提交):
第一阶段:
1). 协调者会问所有的参与者结点,是否可以执行提交操作。
2). 各个参与者开始事务执行的准备工作:如:为资源上锁,预留资源,写undo/redo log……
3). 参与者响应协调者,如果事务的准备工作成功,则回应“可以提交”,否则回应“拒绝提交”。
第二阶段:
1). 如果所有的参与者都回应“可以提交”,那么,协调者向所有的参与者发送“正式提交”的命令。参与者完成正式提交,并释放所有资源,然后回应“完成”,协调者收集各结点的“完成”回应后结束这个Global Transaction。
2). 如果有一个参与者回应“拒绝提交”,那么,协调者向所有的参与者发送“回滚操作”,并释放所有资源,然后回应“回滚完成”,协调者收集各结点的“回滚”回应后,取消这个Global Transaction。
3PC(Three Phase Commit):
1). 三段提交的核心理念是:在询问的时候并不锁定资源,除非所有人都同意了,才开始锁资源。
2). 理论上来说,如果第一阶段所有的结点返回成功,那么有理由相信成功提交的概率很大。这样一来,可以降低参与者Cohorts的状态未知的概率。也就是说,一旦参与者收到了PreCommit,意味他知道大家其实都同意修改了。
Two Generals Problem(两将军问题):
这个问题是无解的。
从工程上来说,一个解决两个将军问题的实际方法是使用一个能够承受通信信道不可靠性的方案,并不试图去消除这个不可靠性,但要将不可靠性削减到一个可以接受的程度。比如,第一位将军排出了100位信使并
预计他们都被捕的可能性很小。
Paxos算法:
简单说来,Paxos的目的是让整个集群的结点对某个值的变更达成一致。Paxos算法基本上来说是个民主选举的算法——大多数的决定会成个整个集群的统一决定。任何一个点都可以提出要修改某个数据的提案,
是否通过这个提案取决于这个集群中是否有超过半数的结点同意(所以Paxos算法需要集群中的结点是单数)。
第一阶段:Prepare阶段;第二阶段:Accept阶段
参见:https://coolshell.cn/articles/10910.html
1. eBay 模式:
此方案的核心是将需要分布式处理的任务通过消息日志的方式来异步执行。消息日志可以存储到本地文本、数据库或消息队列,再通过业务规则自动或人工发起重试。人工重试
更多的是应用于支付场景,通过对账系统对事后问题的处理。消息日志方案的核心是保证服务接口的幂等性。
2. 去哪儿:
1). 异步消息:一种方式是业务逻辑保证幂等;另外一种方式如果业务逻辑无法保证幂等,则要增加一个去重表或者类似的实现。发消息和业务操作在同一个本地事务里
2). 有的业务不适合异步消息的方式,事务的各个参与方都需要同步的得到结果。每个参与方的本地业务库的同实例上面放一个事务记录库。由一个中心服务对比三方的事务记录表,
做一个最终决定。假设现在三方的事务记录是 A 成功,B 失败,C 成功。那么最终决定有两种方式,根据具体场景:
重试 B,直到 B 成功,事务记录表里记录了各项调用参数等信息;
执行 A 和 B 的补偿操作(一种可行的补偿方式是回滚)。
总结起来,其实两种方式的根本原理是类似的,也就是将分布式事务转换为多个本地事务,然后依靠重试等方式达到最终一致性。
3). 那么可能有人觉得在业务库的同实例里放消息库或事务记录库,会对业务侵入,业务还要关心这个库,是否一个合理的设计?
实际上可以依靠运维的手段来简化开发的侵入,我们的方法是让 DBA 在公司所有 MySQL 实例上预初始化这个库,通过框架层(消息的客户端或事务 RPC 框架)透明的在背后操作这个库,
业务开发人员只需要关心自己的业务逻辑,不需要直接访问这个库。
3. 蘑菇街:
1). 消息通知往往不能保证 100% 成功;且消息通知后,接收方业务是否能执行成功还是未知数。前者问题可以通过重试解决;后者可以选用事务消息来保证。
2). 我们在交易创建流程中,首先创建一个不可见订单,然后在同步调用锁券和扣减库存时,针对调用异常(失败或者超时),发出废单消息到MQ。如果消息发送失败,本地会做时间阶梯式的异步重试;
优惠券系统和库存系统收到消息后,会进行判断是否需要做业务回滚,这样就准实时地保证了多个本地事务的最终一致性。
参见:https://weibo.com/ttarticle/p/show?id=2309403965965003062676