订单系统设计的思考(分层篇)

TL;DR

出于系统分层的目的,售卖系统中订单可以设计成业务层订单和支付层订单,前者关注业务行为,后者更关注资金变更,二者通过唯一ID关联。

不分层的订单系统

前提

以下讨论的订单系统基于LNMP实现。

对于描述错误的地方,烦请告知。

订单的基本组成

说起订单,个人认为至少应该包含如下元素:

  • 商品信息
  • 购买者信息
  • 支付信息
  • 支付状态
  • 订单状态

商品信息指的是诸如商品的编号,名称这类商品的基础信息。

购买者信息是购买者的物流信息,用户身份信息等数据。

支付信息是支付金额,支付方式等数据。

支付状态是订单支付相关行为的操作依据。

订单状态是订单在业务系统中,对其他使用者给出的带有业务特征的操作依据。

不做分层的实现

最简单的一种方式,就是一条表记录,记录上述所有信息。

接触过的某网店框架的订单部分,订单的所有信息,甚至物流信息都记录在一张表中,好处当然是有的,就是操作起来相当方便,只要一次查询,就能拿出订单相关的所有信息。对于数据报表之类的需求来说,这样的订单表设计大大降低了数据获取部分的开发难度。

在业务系统规模较小时,这并不成为任何问题,因为一切运行正常,不需要过度设计。

一旦业务规模增大,需要使用这一系统的业务方增多,对接的外部服务越来越复杂,这个系统还能正常运转吗?

想象一下当需要修改新增订单的一个状态,然而其他需求的开发人员提到这个状态添加之后会给他们的功能带来流程上的影响;或者是需要通过多个字段才能确定出一个订单的实际状态,相信会想问当初设计时为什么杂糅了这么多的数据。

可能会引发的现象:

  • 表字段过多,难以添加有效索引
  • 新增业务行为需要新增字段,增加表字段数量
  • 对接上游支付的行为需要业务开发人员关注(耦合了实际支付操作)
  • 逻辑单元越来越臃肿,开发上难以细致的拆分工作
  • 错误的操作引起不正确的支付行为

分层方案

分层目标

之前提到说不分层在业务规模发展到一定程度后会给后续功能开发带来不便,分层的目标就是解决这些问题。

归结起来,核心的问题就是如何能提高业务的可扩展性开发效率安全性

设计方案

可以在订单系统内部实现两层。

一层在本文中称为业务层,关注的是业务行为,比如订单的商品个数,收件人,地址,电话等数据。

另一层在本文中会称作支付层,关注的是订单的金额,与上游支付系统的关联关系,支付的状态。

二者之间通过唯一ID进行关联,支付层通过业务层订单ID进行关联,而支付层则与上游的支付系统通过支付层订单ID进行关联。

所有操作通过 HTTP API 进行。

解决问题

乍一看这样的划分不过是相当于增加了表,通信还增加了成本,感觉像是引入的更多的问题。

首先从可扩展性说起。

可扩展性问题

单表超多字段的问题,通过拆表,其实就能解决。在表记录不多时,不需要分层,直接通过数据库事务对单次操作多张关联的数据表,可以完成业务功能。

这样强依赖事务的业务,数据库会对系统的吞吐产生较大的影响。

如果订单数量相当巨大(比如历史悠久的小额充值记录,订单记录多);或者并发增大,单机数据库逐渐无法支撑业务的发展。

订单如果存在多个影响因素(物流状态/退款/优惠券/返现)等信息时,如果采用多个数据库提高处理能力,就无法通过数据库事务完成二者的状态一致性。

所以,拆表不是目的,拆表的初衷是实现资源的横向可扩展性

分层之后,业务层专注做好业务层的新需求,业务数据与核心数据分离,任何改动对订单的核心数据支付信息支付状态可以降低到最小。需要关注的方向越少,就更容易的控制复杂度,增加功能。

支付层分层完成之后,支付层需要关心的数据只有操作者与操作金额数,以及操作的最终状态。简单来说,就是“谁付了多少钱给谁”。无论再新增任何类型的订单,最终都转化为同样的支付行为。

最后,从资源上来说,由于把数据库事务分拆成了多个系统之间的任务,通过 HTTP API 进行通信,在正常情况下,资源无论如何分布,由于约束已经确定,业务逻辑不会受到影响。

综上,更容易增加功能,资源可以拆分,无论代码执行者还是资源约束上,整个系统扩展起来会变得更加的容易。

开发效率

订单作为用户购买行为的记录,必定会收到购买物品的影响,购买商品的一些特定属性会影响到记录订单的方式。

但是,归根结底,订单终究是表示操作者和操作金额数的。这些产品业务形态上的复杂度,应该在有限的领域内进行处理。

此外,订单的关键步骤——支付还涉及到与支付上游的交互,一旦需要新增支付上游,比如近期苹果要求打赏等服务接入IAP这类事件时,和业务系统耦合程度越高,越会带来开发上的问题。

开发效率不仅和功能的复杂度,结构的清晰程度有关系,也和人力的合理安排有关系。当需要加速开发速度时,划分出合理的结构,可以让不同的人员能真正并行的开发,负责的功能点越小,能更容易高质量的实现,就像 Unix 的设计哲学提到的:

Write programs that do one thing and do it well

安全性

想象一下,直接操作数据库,和通过 API 操作数据库,哪一个更可能带来副作用?

API 约束了操作方式,检查了参数,实现得正确的话,可以保证至少操作不会出错。不出错,是订单系统的底线。

问题

唯一ID选用

订单分层中,各层数据之间进行关联需要通过唯一ID,订单号在一个系统中必定唯一的,所以可以考虑使用订单ID关联业务层支付层订单。

但是这两类订单都有自身的ID,到底选用谁关联谁成为了一个问题。或者说,能不能只用业务层的订单ID关联所有的操作?

考虑如下一种情况,支付可能会失败,由于各层之间应当实现的是幂等的接口,订单ID常作为请求标识,如果只使用业务层的订单ID,支付失败之后,再次使用这一ID是无法创建新的支付请求的,原因是这一个ID已经失败了。

所以,出于幂等操作的考虑,支付层通过业务层订单ID进行关联,而支付层则与上游的支付系统通过支付层订单ID进行关联。

如果非要使用同一个业务层ID进行关联,就需要引入其他的幂等操作标识。

状态一致

上面的篇幅描述了分层的意义和一个基本的分层方案,但是这个方案似乎没有提到拆分之后带来的一个巨大的问题——多层数据状态一致的解决方案。

这个问题就交给下一篇文章《订单系统设计的思考(状态一致篇)》来解释了。