如何设计并实现一个高可用的订单系统?

2,405 阅读12分钟

我们每天都在使用网上进行下单,购买各种各样的商品,作为一名后端服务的程序员,不知道你有没有好奇地想过,在网上下单后,后台流程应当是如何进行订单处理的,这是订单是又是如何生成的,又是如何推送到下游的各个系统的,以及在这个过程中,订单系统是如何保证系统低延迟、高性能、高可用的,尤其是不出现丢单、错单的问题。

image-20210222202553581

当然,在设计日万级和日千万级订单系统的架构时,肯定是不一样的,我们针对这两种不同量级的架构设计分开来讲解。我们讨论的思路是这样的:首先设计一个简单的订单系统,然后分析哪几个环节会丢单,以及面对日万级的订单量如何重构优化,最后分析下,面对日千万级的订单量时,系统应该如何升级改造。

如何设计一个简单的订单系统

这个订单系统很简单,只有一个前台和后台,前台有结算页提供用户去结算,当后台收到前台用户点击去结算的操作时,就会开始处理下单服务。

起初,订单被写入到后台的数据库中,然后异构数据到缓存中,以此提供用户在“我的订单”中进行订单查询,当用户支付完成后,收银台发送消息给下单的服务,进行数据库和缓存中的订单状态的修改。这样,一个简单的订单系统就完成了。

image-20210222203654409

不过,真实的订单系统还会有更多的业务,使得系统更加的复杂,前面只是一个示例。

接下来我们再看看这个系统中,哪些环节可能会出现丢单的问题。

其实,我们现在的后台系统研发技术和架构设计已经很成熟了,但如果是系统挂了,就是挂了,那就不是丢单的问题。所以,要考虑丢单的问题,我认为,关键点应该聚焦在写数据库、接收和发送订单消息的这些环节上。

不过,如果是代码问题,把订单写丢了,那估计谁也没办法了,只能好好地去检查代码。这里,我总结几个在设计订单系统时避免丢单的要点:

一个是:关键逻辑不要使用读写分离的查询方式,避免从库同步延迟造成订单查询异常,比如创建订单之后要创建支付单。但是,在反查订单的时候由于主从延迟,没查询到订单信息,这就可能会造成创建支付单失败。

image-20210222205603152

另外,关键逻辑也不要使用缓存来进行订单的查询,这样做,同时是为了避免因为缓存延迟造成订单反查的失败。

image-20210222211429515

还有一个点是,订单补偿不要粗暴地使用消息队列的方式,避免中间件引发的订单丢失。比如在进行订单状态的修改时,如果处理失败,就将这个订单信息插入到消息队列中,重新消费,以此完成订单的补偿,这种方式在发送消息和接收消息时有可能存在丢消息的情况。

image-20210222211234965

最后一个点是,接收消息处理失败时一定要让消息重试,避免丢失,尤其注意 return、continue 等关键字。比如,一次消费多条订单记录,一条条地进行处理,如果修改订单成功,就继续处理下一条;如果修改失败,可能会因为 retrun 或 cotinue 关键字将其余的消息都丢失掉了。

image-20210222212823080

如何设计一个支持日万级的订单系统呢?

考虑到前面可能丢单的问题,以及系统的稳定性和可用性,我们要如何进行系统的重构优化?注意这里的用词是“重构优化”,说明我们之前的设计其实是很容易支撑日万级的订单系统的。所以,你只要注意几个关键点就可以了:

一个是:注意写数据库时,数据库事务的粒度,不要太大,避免锁表,关注慢 SQL。比如,最不要犯的错误,就是在数据库事务里同时去更新其他数据源,或发送 MQ 消息等,这不仅不能保证数据一致性,还会把数据库的连接耗尽。

image-20210222214028493

另外需要注意的是:关注数据异构的性能和稳定性,尤其在网络抖动的情况下,可能会影响用户体验。最后,要关注订单系统的幂等性,避免出现计费等错误,影响后续操作等流程。

做好这几个点,前面的架构方案,基本就可以满足并支撑一个日万级订单量的订单系统了。

现在我们来看看如何设计一个支持日千万级的订单系统,日千万级和日万级的订单系统关键点的差别主要在于一个量,由于量的增大,造成系统负载过重,导致服务最终宕机。那我们先来具体分析一下,前面的架构中,哪些是系统的瓶颈点呢?

首先,前面的架构设计过重依赖于数据库,而且这个数据库还是订单库,持续读写请求会给数据库造成很大的压力,比如,修改订单状态时就需要反查数据库,并进行订单状态的更新,这些操作在高并发写请求下,会造成数据库资源的抢占,从而影响系统的稳定性。image-20210222215026747

其次,为了避免数据不一致,请求访问主要集中在主库,这样主库压力就会很大,因此,就需要实现分库分表的部署架构,下单服务为此也必须改造支持分库分表的架构设计,但由于热点数据的存在,可能导致数据库出现数据倾斜问题,引发提早的数据库扩容。

image-20210222220450240

还有,由于下单服务耦合业务过重,使得即使是多集群的部署架构,也很难实现快速的处理响应,更何况不同业务的订单处理流程还不一样,使得系统维护性也会越来愈差。比如,创建订单时由于业务不同,数码、3C、图书等订单包含的信息是不一样的,这就需要特殊处理,这种特殊处理逻辑与创建订单耦合在一起,系统自然就会变得越来越慢。

image-20210222220508582

最后,由于数据库存储量的增大,还会导致数据异构性能的直线下降,以及缓存存储容量的不断扩大,这都会极大的影响查询的性能,而且,还可能出现业务间的相互影响等问题。

总地来说,前面架构存在着这样几个问题:下单服务处理接单慢;数据库压力大;数据异构延迟高、缓存数据质量差等,那我们要如何解决呢?

为了应对日千万级的订单量,我们将下单服务进行了服务拆分,使用单独的接单服务处理接单,使用订单引擎和订单管道处理订单业务逻辑,改用双写和数据补偿的方式处理缓存,使用缓存过期的方式控制数据量。接下来我们就具体来分析一下实现方案。

image-20210223204716812

当用户在结算页点击提交订单之后,接单服务会在同一个事务里,将订单插入接单库,将首任务插入任务库,再交由订单引擎进行任务调度。什么是任务?任务就是执行订单操作的步骤,比如写订单缓存、减库存、发送订单通知等,以及前面提到不同的特殊业务流程,这些都是一个个的任务。我们通过将整个订单处理流程分解成一个一个的任务,逐个单独处理,来应对日千万级的订单处理压力。

image-20210223205434268

其中,接单库为多台数据库,通过随机的方式写入,之所以没有采用哈希等算法,其原因在于扩展能力更具灵活性,当遇到流量洪峰来临,新增数台数据库,对写入逻辑是无感的。接单库采用一主多从的部署架构,当某一台机器故障,可以通过快速切换主从,或者摘除故障机等手段进行修复。

image-20210223210014697

而其中的任务库由订单引擎驱动执行,任务通过订单引擎的服务编排能力生成任务队列,首任务执行成功之后,会插入第二个任务,或者,会同时插入第二个和第三个任务。如果插入任务失败,订单引擎会重新执行当前任务,执行成功之后,继续执行插入操作,这里就需要每个任务的业务处理都需要保证幂等性。

image-20210223213641140

刚才说的是任务的创建方式,接下来说说任务的线程调度方式。任务使用多线程的异步方式进行调度,并根据配置选择是串行执行还是并发执行。

image-20210223214048466

有一个点不知道你注意到没有,前面说了任务由线程调度执行,那么,如果任务执行失败,订单引擎是如何重新执行失败任务的呢?这就是通过任务状态机来实现的,任务状态机就如同一个系统的守护线程,任务状态机通过识别任务的状态,来判断每个任务是执行完成,还是执行失败的,并根据状态来进行任务调度,并且,会多次执行失败的任务,重试调度的频次也会逐渐递减,当超过一定的重试次数之后,就会告警通知人工干预。

image-20210223214534335

其实,订单引擎真正执行调度远程服务的并非是由订单引擎来调度的,而是由订单引擎调度订单管道,订单管道去调度远程真实的服务来执行的,其原因在于任务引擎本身就是多线程的设计架构,对线程占用就比较高,而远程调度会注册很多的服务,服务调度也会启动多线程去执行,如果共同部署在同一系统,就会出现线程数过多,造成 CPU 飙升的情况。

image-20210223214933839

接下来,我们再来说下订单缓存的实现策略。接单服务在处理完一些业务逻辑之后,最后调用下单服务提交订单到订单中心,而在此之前,为了保证订单的及时性,在插入订单和任务之后,接单服务会先将订单通过接口写入到订单中心的缓存中,以支持用户在支付之后,在“我的订单”列表中能立即查询到我的订单。

总地来说,订单中心接到下单服务之后,会将订单落库,再同步更新缓存,在后续订单中心接收到台账的消息后,也会同时更新数据库和缓存,将订单状态更新为“订单完成”。

image-20210223220453756

接下来,我对日千万级的订单系统架构再概括讲一下,用户在结算页点击结算,结算页调用后台的接单服务;接单服务接收到下单请求之后,会负责接单,并将订单插入到接单库,同时在一个事务里将首任务插入任务库,并通知调度起订单引擎开始执行任务。

订单引擎,根据任务编号依次进行任务调度,更新任务状态,并由任务状态机进行任务校验补偿处理。订单引擎通过调度订单管道,实现真实服务的远程调度,订单管道请求服务之后,将处理结果返回给任务引擎。最后,订单中心会在接单服务创建订单时,异步写一份订单缓存在订单中心,然后再通过数据异构的方式,再次写一份数据在订单缓存中。

image-20210223222628184

了解完订单的处理流程之后,我们再来分析一下整个流程是如何保证下单的高性能和高可用的。

整个订单系统接单的核心流程,几乎全是同步执行,只有少数任务,比如发送订单通知给下游系统是采用消息异步的方式执行,以此来保证订单流程的高性能。而整个处理过程,基于订单引擎的调度,通过服务流程编排确定一个订单的执行步骤,并有效地保证每个环节的正确执行,避免订单丢单、卡单等异常问题的出现,进而保证订单流程的高可用。

总结

交易平台一直是各个公司的核心系统之一,涉及到数据流与资金流的流转,通俗说,就是钱从你这里走。那如何做到交易平台的高可用,是着实需要在细节点上下足功夫的。 我们知道,秒杀系统的场景会在某一时间点或某一时间段,产生大量的订单,那我们今天讲到的系统架构是否可以支撑呢?实际情况中,秒杀系统和正常的下单流程,在细节点上还是有很多不同的,就以减库存的设计为例,比如日万级的秒杀系统,采用数据库减库存的方式就可以,如果日千万级的秒杀系统实现自然要复杂了,那么要如何实现呢?你可以再仔细想一想。

最后给大家福利:获取更多学习资料,进群:712334882