架构设计的典型案例——订单系统
如图5.10所示,订单系统作为电商系统的“中轴线”,贯穿了整个电商系统的全部流程,电商系统中的核心系统都是围绕订单构建的,订单系统也是随着电商行业的发展逐渐演变的。
图5.10
订单系统用于管理订单类型和订单状态,收集商品、优惠、用户、收货信息、支付信息等一系列订单实时数据,并进行库存更新、订单下发等一系列动作。订单系统业务的基本模型涉及用户、商品(库存)、订单、付款,订单基本流程是下订单→减库存,下订单和减库存必须同时完成,否则,若下了订单但是没减库存,就会导致超卖,使商家库存不足,消费者下了订单却买不到东西,体验不佳;若减了库存但是没有生成订单,就会导致少卖,使商家库存积压或者反复修改商品信息,体验也不佳。
订单的基本概念
在设计订单系统时需要考虑订单字段、订单信息、用户信息、商品信息、金额信息、时间信息等内容,这些内容决定了订单系统的稳定性和可持续性。因为订单的来源和操作的多样性等,订单有多样性的特点,如图5.11所示。
图5.11
1.订单字段
订单字段包含需要在订单中记录的信息,主要用于其他系统如商品系统、物流系统、支付系统等的沟通,为下游系统提供信息依据,如图5.12所示。
图5.12
2.订单记录信息
订单号作为订单识别的标识,往往由一串数字组成,根据订单的增加进行自增,也可以在设计订单号时考虑订单的加密设置(否则其他人通过订单编号就能计算销售量)。订单号后续用作订单的唯一标识,用于WMS(仓存管理系统)和TMS(运输管理系统)时的订单识别。
3.用户信息
用户信息指购买人的相关信息,主要包括姓名、地址和手机号。
在 WMS(仓储管理系统)和TMS(运输管理系统)中,地址信息会被用于区分区域和配送安排。
4.商品信息
商品信息一般包括商品的基本信息和库存,主要影响库存更新和WMS生产。“金额”在逻辑上其实也属于商品信息的范畴,但由于其比较特殊,所以本节将其单独拿出来讲解。
其中,金额信息用于记录订单在每个状态节点的触发时间。在订单产生的商品信息里除了要记录最终金额,还要记录过程金额,比如商品分摊的优惠金额、支付金额、应付金额等。在后续的订单结算、退换货、财务等环节中都要用到。时间信息用于记录订单每个状态节点的触发时间如创建时间、支付时间和退款时间等。
订单流程
订单流程指整个订单从产生到完成的整个流程,包括正向流程和逆向流程。
1.正向流程
正向流程如图5.13所示。
图5.13
订单流程涉及的环节非常多,有如下细节需要注意。
◎ 释放库存的场景:超时未支付、订单自动取消。
◎ 如果选择 COD(货到付款),则支付环节相应地被转移到订单配送之后,在该过程中所有与款项相关的逻辑都变为只操作金额数字,不对结算和账户进行打款和退款操作。
◎ 金额需要分摊到子商品订单,方便后期订单的退款退货。
◎ 订单系统审核的一个作用是对恶意用户或者刷单情况进行处理。系统可根据白名单、黑名单、消费频次、促销品购买量等设计风控规则。如果后续会进入人工审核环节,则可以在规则上适当放宽。
当触发规则需要进行订单退订操作时,在设计上要考虑对用户体验的影响,在前台文案上说明当前节点为审核状态或者等待接单状态。
◎ 在O2O领域有催单的概念,传统电商则通过关联第三方物流信息进行跟踪。催单触发一般会设定一定的时间间隔,在特定的时间内只触发一次催单请求。
◎ 需要将预售等货和移仓做成SOA服务,以便在交易页面计算预计时间和预计到货时间。移仓处理依赖仓库的情况,也会涉及后续拆分和合并包裹的逻辑。
◎ 在生产订单时要先判断报缺情况,如果出现报缺问题,则要考虑整单报缺、部分报缺、换货或者换转退的情况(库存、仓促调拨和退款)。报缺情况分为系统报缺和实物报缺,这是相互承接但相对独立的两个环节。
◎ 电商系统要考虑 7 天无理由退货的场景,即在订单状态完成后申请退货。此时主要涉及的是金额上的计算及一些财务程序(如发票等)问题的处理。
2.逆向流程
逆向流程指因订单取消、退货及退款等情况引发的整个流程。逆、向流程的触发主要有如下几种情况,如图5.14所示。
图5.14
(1)用户自主取消订单(整单)。
(2)风控系统触发取消订单(整单)。
(3)在客服接到客户投诉仲裁后触发取消订单(整单)。
(4)超时未支付取消订单(整单)。
(5)换货报缺转为退单(整单、部分报缺)。
逆向流程触发的关注点如下。
(1)订单状态(某一节点如订单产生后不允许取消订单)。
(2)当退单被商家拒绝后需要转入客服仲裁环节。
(3)部分退款依然享有优惠活动,金额按照分摊进行退款。
3.订单状态
下面从订单状态的设计目的和存在价值去分析和理解它的设计机制,比如维度及维度的颗粒度大小。
正向流程和逆向流程的维度(各选取主要状态)如下。
◎ 正向订单:已锁定、已确认、已付款、已发货、已结算、已完成、已取消等。
◎ 正向预售订单:预付款已付未确认、已确认未付尾款(变更)。
◎ 正向问题单:未确认、未锁定、未发货、部分付款、未付款等。
◎ 逆向退单:待结算、未收到货、未入库、质检不通过、部分收货、已取消、客户已收货等。
◎ 逆向换单:完成、已结算、客服已收货等。
服务对象的维度如下。
◎ 顾客/用户:待付款、待发货、待收货、待评价、买家已付款、交易成功或失败、卖家已发货、退款成功、交易关闭等。
◎ ERP 等其他交互系统:已锁定、已确认、已分仓、已分配、已出库、已收货、已完成等。
◎ 客服:等待买家付款、待付款和待发货的订单、退款中的订单、定金已付、买家已付款、卖家已发货、交易成功、交易失败、异常订单。
4.订单推送
当状态机发生变化时,需要将对应的变化告知相关人员以便了解
当前订单的情况,这就是订单推送的作用。
订单推送的触发依赖于状态机的变化,涉及的信息有:
◎ 推送对象(用户、物流人员、商家);
◎ 推送方式(App端的消息、微信端的消息、短信);
◎ 推送节点(状态改变)。
订单系统设计中的挑战和实践
下面从订单系统需求的演变和订单系统架构的演变这两方面讲讲订单系统设计中的挑战和实践。
1.订单系统需求的演变
订单系统需求的演变一般会经历3个阶段,如下所述。
第1阶段,实现购买流程。
◎ 实现订单的创建、发货、确认等信息闭环。
◎ 支持订单审核(在初期支持人工审核即可)。
◎ 支持在用户端显示订单相关信息。
◎ 支持促销金额的计算。
第2阶段,提供服务。
◎ 提供订单分布式服务。◎ 支持跨平台交易单的生成(即在同一个大交易单内既有商家商品又有自营商品或者多个商家的商品)。
◎ 支持拆单、合单逻辑(配送单、支付单等)。
◎ 提供多样的订单推送服务来完善订单的状态。
第3阶段,支持不同营销手段下的订单类型。当平台发展到足够大的规模时,提效、稳定就变成了一个重要的话题,平台应该可以提供不同营销场景下的订单如团购、预购、秒杀等。
2.订单系统架构的演变
随着业务需求、用户量及各种系统等因素的改变,订单系统的架构也要随之改变。下面讲解订单系统的三代演变及最佳实践。
第1代订单系统的架构:简单粗暴如图5.15所示,第1代订单系统的架构简单粗暴。
图5.15
在第1代订单系统的架构中,订单状态是在特定的服务器上进行处理的,服务一旦出现问题,就会造成订单的丢失,导致订单流无法进行下去。
第1代订单系统的缺点如下。◎ 服务单点。
◎ 数据库单点。
第2代订单系统的架构:无状态异步驱动
如图5.16所示,第2代订单系统的架构为无状态异步驱动。
图5.16
在第2代订单系统的架构中,应用服务器不再保留订单状态,但给数据库服务器造成了高频查询压力,数据库相对比较脆弱。
第2代订单系统二代的缺点为状态扫描带来的服务负载。
第3代订单系统的架构:队列模式
如图5.17所示,第3代订单系统的架构为队列模式。
图5.17
在第3代订单系统的架构中,订单的状态的改变不再依靠高频查询数据库来获得,而是通过队列模式来获得,这很好地减轻了数据库的压力。但是在该系统中,System2 成了核心,该模块的维护会变得很复杂,这也是架构设计的关键:如果没有完美的架构,则只能得到一个平衡的架构。
3.系统优化的重要实践
实践1,重试和补偿。
◎ 多个机器重试不能同步,需要随机跳跃(Jitter)和指数回退(Exponential Back-off)。
◎ 正在重试的服务也可能宕机,需要保存状态(State)。
实践2,幂等性。
◎ 没收到响应不见得失败了。
◎ 自己响应了不见得别人以为自己成功了。
◎ 重试时必须带上唯一的有意义的ID。
◎ 每一个服务的调用都必须是幂等的。
◎ 非只读的服务必须保存状态。
实践3,一致性实践。
◎ 订单系统有强一致性需求。
◎ 无单点故障的分布式系统的一致性是难以解决的问题。
◎ 已有Paxos算法,现有开源系统(例如ZooKeeper)。◎ 可以使用常用的、成熟的关系数据库来避免单点故障的发生。
◎ 云端分布式无单点故障的系统。
实践4,工作流(Workflow)。
◎ 可扩展性:无状态的服务、分布式部署、分布式存储、工作流状态。
◎ 可靠性:定时器、重试、幂等性、强一致性的状态。
◎ 可维护性:工作流的描述与执行Activity的描述相分离,支持异步触发。
◎ 支持版本和升级。
数据库优化
下面从数据库读写分离和数据库分库分表这两方面讲讲如何进行数据库优化。
1.数据库读写分离
数据库读写分离的基本原理是让主数据库处理事务性查询,让从数据库处理Select查询,当然主服务器也可以提供查询服务。读写分离的最大作用是降低服务器的压力。
数据库读写分离的好处如下。
◎ 增加了冗余。
◎ 增加了机器的处理能力。◎ 对于以读操作为主的应用,使用读写分离是最好的场景,因为可以确保写的服务器压力更小,而读又可以接受时间上的延迟。
提高读写分离性能的主要原因如下。
◎ 物理服务器增加、负荷增加。
◎ 主数据库和从数据库只负责各自的写和读,可很大程度地缓解X锁和S锁争用。
◎ 从数据库可配置MyISAM引擎,提升查询性能及节约系统开销。
◎ 从数据库同步主数据库的数据与主数据库直接写还是有区别的,从数据库通过主数据库发送来的 binlog 恢复数据,但最重要的区别在于主数据库向从数据库发送binlog是异步的,从数据库恢复数据也是异步的。
◎ 读写分离适用于读多写少的场景,如果只有一台服务器,则当Select很多时,Update和Delete会被这些 Select访问中的数据堵塞,等待 Select结束,并发性能不高。对于写和读比例相近的应用,应该部署双主相互复制。
◎ 可以在从数据库启动时增加一些参数来提高其读的性能,例如 --skip-innodb 、 --skip-bdb 、 --low-priority-updates 及 --delay-key-write=ALL。当然,这些设置也是需要根据具体的业务需求来做的,不一定能用上。
◎ 分摊读取。假如我们有1个主数据库和3个从数据库,假设现在1分钟内有10条写入和150条读取,那么1个主数据库和3个从数据库相当于共计40条写入,而读取总数没变,因此平均下来每台服务器都承担了 10 条写入和 50 条读取(主数据库不承担读取操作)。因此,虽然写入请求数没变,但是读取请求数大大分摊了,提高了系统的性能。另外,在读取请求被分摊后,又间接提高了写入的性能。所以,总体性能提高了,说白了就是拿机器和带宽换性能。
◎ MySQL 复制的另一大功能是增加冗余和提高可用性,在一台数据库服务器宕机后,能通过调整另一台从数据库服务器来以最快的速度恢复服务,因此不能光看性能,也就是说有1个主数据库和1个从数据库也是可以保证数据库稳定的。
实现方案如图5.18所示。
图5.18
2.数据库分库分表
不管采用哪种分库分表框架或者平台,其核心思路都是将原本保存在单表中的过大数据进行拆分,将这些数据分散保存到多个数据库的多个表中,避免因为单表数据量太大给数据的访问带来读写性能的问题。所以,在分库分表场景下最重要的一个原则就是将数据尽可能平均拆分到后端的数据库中,如果拆分得不均匀,则还会产生数据访问热点,同样存在热点数据因为增长过快使数据库单表数据量过大的问题。
而对于数据以什么样的纬度进行拆分,在很多场景下都是对业务数据的ID(在大部分场景下,此ID都是自增长的方式)做Hash取模(哈希取模)的方式进行平均拆分的,这确实在很多场景下都是非常适用的,但并不是在所有场景下都是最优的选择。也就是说,如何拆分数据并没有所谓的金科玉律,更多地需要结合业务数据的结构和业务场景来决定。以电商订单数据拆分为例,订单是任何一个电商平台都有的业务数据,每个平台的用户在提交订单时都会在平台后端生成订单相关的数据,一般记录一条订单数据的数据库表结构如图5.19所示。
图5.19
订单数据主要由三张数据库表组成,主订单表对应的就是用户的一个订单,每提交一次订单,都会生成主订单表的一条数据。在某些情况下,用户可能在一个订单中选择不同卖家的商品,每个卖家又会按照自己在该订单中提供的商品计算相关的商品优惠(如满100元减10元),以及按照不同的收货地址设置不同的物流配送,所以会出现子订单的相关概念,即一个主订单会由多个子订单组成,则真正对应到每个商品的具体订单信息被保存在订单详情表中。
如果一个电商平台的业务健康发展,则订单数据容易因为单个数据库表中的数据量过大而造成性能瓶颈,所以需要对其进行数据库拆分。此时从理论上对订单进行拆分是可以从两方面进行的,一方面是通过订单ID(一般为自增长ID)进行取模,即以订单ID为分库分表键,另一方面是通过买家的用户ID进行哈希取模,即以买家用户ID为分库分表键。
3.两种方案的对比
如果通过订单ID进行取模,比如按1024进行取模,则可以保证主订单及相关子订单的订单详情数据平均落入后端 1024 个数据库表中,这在原则上很好地满足了数据尽可能平均拆分的原则。
如果通过买家的用户ID进行哈希后取模,比如也按照1024进行取模,则在技术上也能保证将订单数据拆分到后端的1024个数据库表中,通过用户ID进行哈希后取模会出现问题,也就是说如果某些卖家的交易量非常大,那么这些卖家的订单数据量(特别是订单详情表的数据量)会比其他卖家多出不少,也就是说会出现数据不平均的现象,最终导致这些卖家的订单数据所在的数据库相对其他数据库提前进入数据归档。
为了避免在线交易数据库的数据量增大导致数据库的性能出现问题,我们一般将3个月内的订单数据保存在在线交易数据库中,将超过3个月的订单归档到后端的专门归档数据库中。
所以从对数据尽可能平均拆分这条原则来看,通过订单 ID 进行取模看起来更能保证订单数据的平均拆分,但我们暂时不要这么快下结论,要根据不同的业务场景和最佳实践多思考不同维度的优缺点。
小结
电商平台的需求一直在变化,订单系统的架构也会随之变化,架构设计就是一个持续改进的过程。如果想将订单系统做得更好,就需要更加深入系统的每一个环节,比如对容灾、灾备、分流、流控等都需要慢慢雕琢。在架构中没有完美的架构,只有平衡的架构,不要追求单点的完美,要追求多点的平衡。



