我们每天都在使用下单。但是订单是如何生成的呢,又是如何推送到下游的各个系统的?
订单系统的低延迟,高可用的,不丢单问题。
万级和千万级架构是不一样的。
思路:
首先,设计一个简单的订单系统。
分析哪几个环节会丢单。
日万级如何优化。
日千万如何升级改造。
简单系统:
只有前台和后台,前台结算页提供用户去结算。当后台收到前台用户点击去结算的操作时,就会处理下单服务。
起初,订单会被处理到后台的数据库中,然后异构数据到缓存中。以此提供用户在我的订单中进行订单查询。
当用户支付完成后,收银台发送消息给下单的服务,进行数据库和缓存中的订单状态的修改。这样一个简单的订单系统就完成了。
真实的订单系统会有更多的业务,使得系统更加复杂。前面只是一个示例。
接下来,我们再看看这个系统中,哪些环节可能会出现丢单的问题。其实现在后台系统研发架构已经很成熟了。但是如果系统挂了,就是挂了。
所以关键点应该聚焦在写数据库,和接收和发送订单消息的环节上。
关键逻辑不要使用读写分离的查询方式,避免从库同步延迟造成订单查询异常。
比如
创建订单后,要创建支付单,但是在反查订单的时候由于主从延迟,没查到订单信息,这就可能会造成创建支付单的失败。
关键逻辑也不要使用缓存来进行订单的查询
这样做,同时是为了避免因为缓存延迟造成订单反查失败。
订单补偿不要粗暴的使用消息队列的方式,避免中间件引发的订单丢失。
比如在订单状态的修改时,如果处理失败,就将这个订单信息插入到消息队列中,重新消费,以此来完成订单的补偿。
这种方式在发送和接收消息时,有可能存在丢消息的可能。
接收消息处理失败时,一定要让消息重试,避免丢失。尤其注意return,continue等关键字。
比如:
比如一次消费多条记录,一条一条的处理,如果修改状态成功,就继续处理下一条。如果修改失败,可能会因为return或者continue关键字将其余的消息都丢失掉了。
那么我们要如何设计一个支持日万级的订单系统呢?
考虑到前面丢单的问题,以及系统的稳定性和可用性,我们要如何进行系统的重构和优化。
之前的系统是很容易支持日万级的订单系统的。
注意以下几点:
1.写数据库的时候,数据库事务的粒度不要太大,避免锁表,关注慢sql。
比如最不要犯的错误就是在数据库事务里同时更新其他数据源,或发送消息到消息中。
这不仅不能保证数据的一致性,还会把数据库的连接耗尽。
2.关注数据异构的性能和稳定性,尤其在网络抖动的情况下,可能会影响用户的体验。
3.关注订单系统的幂等性,避免出现计费错误,影响后续操作流程。
基本满足订单日万级别订单系统了。
关键在于一个量,由于量的增大,造成系统负载的过重,导致服务最终宕机。
比如修改订单状态的时候,就需要反查数据库,并进行订单状态的更新,这些操作在高并发场景的时候,会造成数据库资源的抢占,从而影响性能的稳定性。
为了避免数据不一致,请求访问主要集中在主库上,这样主库压力就会很大。因此就需要分库分表的架构:
下单服务也为此也必须改造,支持分库分表的设计,但由于热点数据的存在,可能导致数据库倾斜的问题。引发提早的数据库扩容问题。
由于下单服务耦合过重,使得即使是多集群的部署架构,也很难实现快速的处理业务响应。更何况,不同业务的订单处理的流程还不一样。使得系统的维护性也越来越差。
比如创建订单的时候由于业务不同
数码,3c图书等订单包含的信息是不一样的。这就需要特殊处理。
这种特殊处理与系统订单耦合在一起,系统自然就会变得越来越慢。
最后由于数据库存储量的增大,还会导致数据异构性能的直线下降。以及缓存存储容量的不断扩大,这都会极大的影响查询性能,而且还可能出现业务件的相互影响等问题。
前面的问题:
下单服务处理接单慢
数据库压力大
数据异构延迟高
缓存数据质量差
这些如何解决呢?
为了应对千万级的订单量,需要将订单服务进行拆分。
使用单独的接单服务处理接单,使用订单引擎订单管道处理订单业务逻辑。
双写和数据补偿的方式处理缓存,使用缓存过期的方式控制数据量。
接下来分析一下实现方案:
当用户在结算页点击提交订单后,接单服务会将同一个事务里,将订单插入接单库。将收任务插入到任务库。再由订单引擎进行任务调度。
什么是任务?
任务就是执行订单的操作步骤,比如写订单缓存。减库存,发送订单通知等,,以及前面提到不同的特殊业务流程。这些都是一个个的任务。
我们通过将整个订单处理流程,分解成一个个的任务。逐个单独处理。来应对日千万级别的订单处理压力。其中接单库为多台数据库。
通过随机的方式写入,之所以没有采用Hash等算法,其原因在于扩展能力更具灵活性。当遇到流量洪峰来临时,新增数台数据库,对写入逻辑是无感的。
接单库采用一主多从的部署架构,当一台机器故障,可以通过快速切换主从,或者摘除故障机等手段进行修复。
而其中任务库由订单引擎驱动执行,任务通过订单引擎的服务编排能力,生成任务队列,首任务执行成功之后,会插入第二个任务,或者同时插入第三个和第四个任务。
如果插入任务失败,订单引擎会重新执行当前任务。执行成功之后,会继续执行插入操作。这里就需要每个任务的业务处理都需要保证幂等性。
刚才说的是任务的创建方式,接下来说说任务的线程调度方式:
任务使用多线程的异步方式进行调度。
并且根据配置:
选择串行执行还是并行执行,有一个点不知道你有注意到:
前面说了任务线程调度执行,那么如果任务执行失败,订单引擎是如何执行失败的任务呢?
这就是通过任务状态机来实现。
任务状态机就如同一个系统的守护线程。任务状态机通过识别任务的状态,来识别每个任务是执行完成,还是执行失败。并根据状态来进行任务调度,并且对多次执行失败的任务,重试调度的频次也会逐渐减弱。当超过一定重试次数之后,
就会告警通知人工干预。
其实订单引擎真正执行调度远程服务的,并非订单引擎来完成的,而是由订单引擎调度订单管道,订单管道去调度远程真是的服务来执行。其原因在于引擎本身就是多线程的设计架构,对线程占用就比较高。而远程调度会注册很多的服务。
服务调度也会启动很多多线程去执行。如果共同部署在一个系统里。就会出现线程数过多。造成cpu飙升的情况。
接下来我们再说一下订单缓存的实现策略:
接单服务处理完一些业务逻辑后,最后调用下单服务提交订单到订单中心。而在此之前,为了保证订单的及时性,在插入订单和任务之后。接单服务会先将订单通过接口写入到订单中心的缓存中,以便支持用户在支付之后,在我的订单列表中能立即查询到我的订单。
总体来说,订单中心接到下单服务之后,会将订单落库,便同步到缓存中,在后续订单中心接收到台账的消息后,也会同时更新数据库和缓存,将订单状态更新完成。
千万级架构整体概括的讲一下:
用户在结算页点击结算,结算页调用后台的接单服务,接单服务收到下单请求之后,会负责接单,将订单插入到接单库,同时在一个事务里,将首任务插入到任务库,并通知调度器订单引擎开始执行任务。订单引擎根据任务编号,依次执行任务调度,并更新任务状态。并由任务状态机进行任务校验补偿处理。订单引擎通过调度订单管道,实现真实的服务远程调度。订单管道请求服务之后,将处理结果返回给任务引擎。
最后,订单中心会在接单服务创建订单时,异步地写一份订单缓存到订单中心,然后通过数据异构的方式,再次写一份数据在订单缓存中。
高可用和高性能
整个订单核心流程 同步执行,只有少数任务比如发送订单通知给下游,系统是采用消息异步的方式执行,以此来保证订单流程的高性能。而整个处理流程基于订单引擎的调度,通过服务流程编排,确定一个订单的执行步骤,并保证每个环节正确的执行,避免订单丢单,卡单等已成任务的出现,进而保证订单流程的高可用。
交易平台一直是各个公司的核心流程之一,涉及到数据流和资金流的流转。
秒杀场景下 细节还是有很多不同的。
以减库存为例:
日万级的秒杀系统,采用数据库减库存的方式就可以了。
日千万秒杀系统,更复杂一些。
基于极客时间视频整理。