在分布式系统中,实现强一致性并不容易。即使提交了2PC和3PC阶段,也不能保证绝对的强一致性。我们也不能因为极小的不一致概率,导致系统整体性能低下,或者扩展性受到影响,架构变得极其复杂。因此,在2PC/3PC提交还没有大规模应用的情况下,最终一致性是一个比较好的解决方案,已经在业界广泛应用。1、重试机制如下图所示。服务消费者同时调用服务A和服务B。如果ServiceA调用成功,ServiceB调用它。为了保证最终的一致性,最简单的方法就是重试。重试时注意设置ServiceConsumer的超时时间,避免等待时间过长或卡死耗尽资源。Consumer重试时,需要注意以下几个方面:超时时间;重试次数;重试间隔时间;重试间隔时间的衰减;具体实现细节请参考《 基于Spring-tryer 优雅的重试方案》。2.本地记录日志通过本地记录日志,然后收集到分布式监控系统或其他后端系统中,启动定时巡检工具。根据实际情况,可选择人工处理。日志格式:TranID-A-B-DetailTransID为交易ID,可以生成一个随机序号;Detail是数据的详细内容;如果调用A成功,记录A成功;如果调用B失败,或者失败,没有记录等,即日志中没有B成功,则重新调用B;它可以定期检测并处理日志。收集识别日志的蓝图,如下图。3、可靠的消息模式考虑到实际业务场景中失败的概率比较低,可以考虑以下方案。服务消费者调用服务B失败,先重试。如果重试一定次数还是失败,则直接将消息发送到MessageQueue,转为异步处理。分布式能力强的MQ,如Kafka、RocketMQ等开源分布式消息系统,都可以用于异步处理。服务B可以专门集成一个错误处理组件,不断收集MQ的补偿消息。或者独立处理一个错误处理组件来独立处理MQ补偿消息,包括来自其他Service组件的异常。这种方案也存在消息丢失的风险,即服务消费者在消息发出前挂掉,属于小概率事件。还有另一种解决方案——可靠消息模式,如下图所示。ServiceConsumer向MessageQueueBroker发送消息,如RocketMQ、Kafka等,消息被ServiceA和ServiceB消费。MQ可以使用分布式MQ,可以持久化,通过MQ实现消息不丢失,以及MQ被认为是可靠的。可靠消息模式的优点:提高吞吐量;在某些情况下,减少响应时间;存在的问题:时间窗不一致(业务数据进入MQ,没有进入DB,导致部分场景无法读取业务数据);增加了架构的复杂性;消费者(ServiceA/B)需要保证幂等性;针对上述时间窗不一致的问题,可以进一步优化。业务分为:核心业务和从属业务核心业务服务——直接调用;下级业务服务——消费来自MQ的消息;直接调用订单服务(核心服务)将业务订单数据落地DB;同时向MQ发送消息。考虑到向MQ发送消息之前,ServiceConsumer(创建订单)可能会挂掉,也就是说调用订单服务和发送Message必须在一个事务中,因为处理分布式事务比较麻烦,影响性能。因此又创建了一张表:事件表,与订单表在同一个数据库中,可以增加事务保护,将分布式事务变成单库事务。整个过程如下:(1)创建订单——持久化业务订单数据,并在事件表中插入一条事件记录。请注意,这是在事务中完成的以确保一致性。如果失败,则不需要关心业务服务的回滚,成功则继续。(2)发送消息——发送订单消息到消息队列。如果消息发送失败,将进行重试。如果在重试成功之前消息失败,补偿服务会重新发送消息(小概率事件)。补偿服务会不断轮询事件表,找出异常事件并发送补偿消息,成功则忽略。如果消息发送成功,或者补偿服务发送消息成功,可以考虑删除事件表中的事件信息记录(逻辑删除)。(3)消费消息——其他下属业务服务可以消费MQ中的订单消息来处理自己的业务逻辑。上述设计方案中,有三点需要说明:(1)直接调用订单服务(核心业务)是为了让业务订单数据尽快落地,避免时间窗不一致,保证可读性写后一致性。(2)创建订单业务直接向MQ发送消息的目的是为了增加实时性。补偿服务仅在异常情况下使用。如果实时性要求不高,也可以考虑去掉直接发送Message的逻辑。(3)引入了一个额外的事件表,将分布式事务变成了单一的数据库事务,这也在一定程度上增加了数据库的压力。
