3.14. 顺序处理

绝大多数的场景下,我们的业务操作不需要保证严格的顺序处理,但在数据存储上却是最常规的要求。比如MySQL在集群模式下多节点间的数据写入顺序必然是需要一致的。在业务操作上比较典型的是数据库日志(MySQL的Biglog或Mongo的OptLog等)的同步,我们一般会订阅到Kafka,然后从Kafka异步消费,这之中就要保证消费时记录的顺序与数据库一致。

顺序处理的场景有多种,但就其基础是要做到时钟一致,本质的技术无非如下几种:

  • 单节点处理 用一个节点处理所有消息,这种最简单,但有违微服务避免单点的原则,不过具体情况要具体分析,在可用性和可维护性上需要平衡,对一些边缘业务采用此做法也未必不合适
  • 单节时序生成 用一个节点生成Timestamp,这样就有了一个全局可排序的数据记录,当然也同样有违避免单点的原则,但这却是很多分布式数据库的选择,比如TiDB,因为它足够简单
  • TureTime方案 由多个部署有GPS同步能力的时钟及原子钟节点提供Timestamp服务,这一方案避免了单点问题,问题在于太过昂贵,一般只有大型集群才会考虑使用。以Google的Spanner为例,使用这一方案可以保证不同服务节点的时间误差小于10ns
  • Lamport Timestamps 上面说的都是物理时钟,而Lamport(此人后面还会提及)提出的是逻辑时钟概念,通过为每一操作带上本地或接收到消息的时间戳来解决访问链路的顺序问题,详细算法网络中有不少介绍,这里不再赘述。Lamport Timestamps的局限只能处理有相关性记录的顺序,像上文说到数据库日志记录就无能为力了

这几种方案都是为解决时钟一致性,但我们实际需求中只解决了时钟一致还是远远不够的,我们常用MQ做服务解耦,尤其是微服务下更为倡导事件驱动,那么就会经常遇到顺序问题,比如我们会将用户的关键操作流程打日志发送到Kafka中,日志服务订阅Kafka完成日志写入,显然流程日志是要保证顺序。目前Kafka及主流的MQ都无法保证严格顺序,因为成本太高,要先保证生产都同步生产消息到MQ,MQ的存储要尽量避免多主(多个写入节点),消费者只能有一个,逐条消费等,在性能、可用性上都大打折扣。这时我们只能根据用户Id Hash到相同的写入节点(对应于Kafka的Partition),这样就能做到同一用户的日志消费顺序等同于日志的发送(同步发送方式)顺序。

顺序处理的成本不低,在开发中我们应该尽量避免,比如车贷通的订单有 已申请待审核、审核通过待签约、已签约待支付 等,如果都是串行的话那不会有问题,但为了缩短流程用时,只要用户点击同意,我们会默认签约成功,进入支付首付款(或压金)环节,此时服务器会异步上传合同信息到电子签章服务,如果用户支付成功先于合同签约完成那就可能会影响订单的状态,这种情况下可以考虑做消息的排序以保证顺序,但更简单直接的是使用状态机在业务中自行判断。

下一节:上节介绍了顺序处理,我们实际场景中还会遇到诸如这些情况,比如对页面操作记录时要求操作事件是顺序的:Beforeload必须先于Unload,事件由同一个终端设备发送,通过设备ID Hash到同一个节点服务处理,这之中不存在时钟一致性问题,但由于事件发送是异步的,所以接收可能乱序,再比如在大数据系统中分析OAuth关系,OAuth表记录的是A应用的X用户与B应用的Y用户的关联(如果B应用没有对应的用户则Y用户为新增记录),但用户表、应用表和OAuth表都是分开采集的,即不能保证分析OAuth表时用户表对应的用户就一定已经存在。对于这类需求比较通用的解决方案是使用延迟队列。