电商平台,交易始终是众多业务逻辑中的关键部分。对于平台而言,交易流程本质上就是处理数据,资金和商品在买卖双方以及平台三方之间的流转,对应到系统服务模块,就是订单,支付和物流三大部分。其中,支付和物流目前有众多第三方平台提供支撑,前者比如微信支付宝银联,后者比如快递100和聚合数据等等。而交易流程的各个环节尤其是订单从生成到支付完成之间这部分,则由于不同的项目的商业逻辑和特定数据的差别,需要每个项目单独进行设计。当然,在大体的框架上,绝大多数EShop网站之间是差不多的。本文则重点介绍我们项目在实际操作过程中,交易系统是如何设计和演化的。
第一个阶段:功能原型阶段
时间:项目启动后1个月内。
在这个阶段,我并没有太多参考现有分布式电商系统的结构,而是把主要精力放在和项目业务息息相关的数据模型设计上。这是因为公司要做的方向是一个完全垂直的领域,虽然有同类型的竞争对手,但是几乎没有可供参考的实际数据实例。所以我们需要在最短时间里,完成业务逻辑的模型化,并尽可能发现遗漏的(包括开发人员未理解的和投资人本身没有描述清楚的潜在需求的)模型进行补足。因此这个阶段我刻意避免过度设计(因为很大可能性上有部分数据模型是要翻工设计的,事实证明这是一定的,因为大家都没有做过这个领域,认识和理解上的偏差和错误在所难免),而是带领后台开发组全力几乎是以最简单最暴力的手段实现已经明确的功能点,尽快能让移动端原型能运转起来验证我们的产品思路。
所以这个阶段的交易流程设计非常粗暴简单:
在这个阶段,后台数据库设计上有两个重点:
1)购物车和普通订单之间的关系:
在我们的设计中,添加到购物车的动作本身已经触发创建一个订单,只是这是一个特殊的订单,它的“Status=Draft”,只有在用户从购物车中确认购买时才将此订单转换成一个普通订单,也就是修改“status=waiting for payment”。同一个卖家的不同商品,在购物车中被添加进同一个订单中。
2)合并付款
合并付款是和购物车相辅相成的,没有购物车就不存在合并付款的概念。所谓合并付款,本质上是需要对多个子订单的支付单进行合并,这些子订单对象本身仍然是独立的。注意,这里的子订单一般以商家为区分。
另外需要提及的是,这里我埋了一个大坑,以至于后期第三个阶段我们不得不做出相应的改动:我从市场和投资人的需求中分析的结论是“我们根本不可能存在可以随意修改价格的订单”。所以为了速度和简单,这个阶段里所有Order都是外键直接关联到商品信息的。事实证明,这样的想法真是太天真。即便结论是正确的,作为一个上规模的电商平台,所有订单数据必须和商品数据之间做冗余。再重复一遍,这里的数据表一定要做冗余设计!后面我会仔细分析原因。
第二个阶段:完善阶段
时间点:项目启动后2-5个月
通过第一个阶段的原型验证后,商品的交易流程基本走通,商品和交易的关键属性字段都已经基本到位。于是我开始关注数据库的结构合理性。首先我们的数据库有一个非常不合理的问题:单库!
很显然我并不能责怪后台开发人员,因为在原型论证阶段,速度是我们追求的第一目标,把所有的数据库表都扔在一个库里是非常简单并且相对还是很合理的选择。但是对于产品而言,真正发布时所有的鸡蛋都在一个篮子里显然不是明智的选择,单库会面临诸多问题:
1)性能很容易到达瓶颈。这个自然不用多说。
2)升级维护困难。如果所有业务逻辑访问的是同一个库,那么在升级数据库或者维护数据时,将会造成全平台业务的暂停,这个在项目初始上线时还不是大问题,但是只要业务规模稍稍大些,产品需求变化快些,频繁的技术更新将会给平台带来严重的商务阻断。
3)数据保障性很差。一旦数据库损坏,所有业务都将无法使用。
所以,在后台业务膨胀到一定程度之前,必须提前做一些调整。这里又要再次点出我的思路宗旨——杀鸡勿用牛刀,在合适的时间段点到为止。数据库的优化是一个非常复杂和精细的工作,涵盖的技术手段是比较广的。但是我们可以做一些简单的处理却仍然能够很大幅度优化架构:
1)数据库切分:
数据库的切分有水平切分和垂直切分(包括库和表)。我比较倾向于在库级别做垂直切分,在表级别做水平切分。所谓垂直切分,一般以业务为区别,不同的业务使用不同的库表;水平切分一般是对大表而言的,当一张表因为记录过大而导致搜索性能下降时,可以对表进行水平切分,让一张单表变成多张分表,索引时,按照一定的索引算法找到对应的分表再进行查找。
我根据项目的实际情况,绝大多数数据表纪录数都在1K~10K级别,因此我觉得水平切分暂时是没有意义的。但是在这个时候按照业务内容进行垂直切分数据库就很有必要了。因为这是一个B2C的平台,平台数据在本质上就有两大类:内容类和交易类。其中内容类的数据更新频繁,而且读取远大于写入;而交易类数据在业务处理上几乎完全和内容数据累独立。所以完全可以将这两部分的数据放在不同的库里。这样的话,两部分业务逻辑的更新都不会影响到对方,同时也容易针对两部分数据分别进行优化处理,比如后文会提到的,内容部分数据可以做读写分离,而交易类数据可以做主从备份。
目前项目的主数据库分为3大部分,defaultpro,orderpro以及aispro,分别涵盖了所有内容数据表,交易订单类数据表和系统管理数据表。
2)主从备份:
相比于内容数据,交易类数据是非常敏感重要的,出一点差错都不行。因此,对于交易数据的保护来说,备份是必须的。现在既然已经把交易类数据单独出一个独立的库,而且初期项目交易数据量可以预期不会很大,那么备份就比较简单了,直接对库做1对1备份。利用阿里云自身RDS的自动主从备份功能,主从数据库warm up之后几乎可以做到毫秒级的同步备份。不过注意,截至到本文,阿里云的主从同步备份要求MySql版本不低于5.6
在这个项目里,我把交易类数据做主从备份还有一个目的。上一篇文章有提到,一个完整的B2C系统还有一套独立的财务和客服系统。当我们把交易类数据单独备份到从库后,财务系统和客服系统则在主从库上进行读写分离,所有只读操作都在从库中进行,当财务需要对交易数据进行更新时(比如更新分账,退款审核,汇款审核操等等操作),才将直接操作主数据库。在这里需要提一下,我们有对财务和客服类操作制定了不同的策略:实时更新和延迟更新。实时更新时将通过RPC直接更新主数据库,而延迟更新时则将操作缓存在Redis中,定时定点统一将缓存的操作更新到主数据库(比如我们规定退款订单更新状态可以选择在退款审核成功后第二日凌晨3:00进行)。
3)读写分离:
上面第二点其实已经提到了读写分离的概念。但是对于B2C系统而言,更加强大或者说更加重要的读写分离是针对内容数据的。因为毕竟读写分离的优势在读写比越大效果越好。而平台内容数据在读写次数比上,随着C端用户的增加将会越来越大。所以很有必要对内容数据进行读写分离。这点上,阿里云RDS的基础设施做得还是比较到位的,你可以很方便的指定库进行主从备份后进行读写分离。到目前为止,我们的系统暂时还没有对内容数据库进行读写分离,但是当业务规模增长到一定程度的时候,这显然是第一步需要做的事情。
第三个阶段:功能强化阶段
时间点:项目启动6个月
这时,我们的项目1.0版本马上就要发布了。但是在这个时候,市场部门出现了一些比较特殊的情况,1)要求我们能够主动的修改商品价格,以便他们进行业务上的推广; 2)我们的支付方式要求除了在线支付和现金汇款之外,还要有现金充值后余额支付,礼券,积分等其他计价手段。
要求1)这意味着,一个商品的价格必须要和订单价格分离,这其实已经和淘宝没有太大区别了。这算是我在整个项目中唯数不多的判断失误的地方。实时上在最一开始的时候我有考虑到这个问题,但是我错误的(其实是幼稚的)估计了投资人的思路和公司的业务模式,很主观的认为这种情况不是我们的业务模式,所以直接排除了。这也算是个小白的教训:永远不要轻信投资人对业务方向的规划,也永远不要武断地对商业模式进行提前估计,宁愿信其有不可信其无,在技术上必须干苦力活多做一些B方案储备。当然,在这个问题上,关于对商业模式的思考也是一个创业者必修功课,虽然我是技术负责人,但是我在商业模式和市场上有自己的思考,也在不断尝试着做更多产品经理的工作,这方面我会在产品分类文章里做些记录。
要求2)意味着交易流程更加复杂化,单个交易内涉及的内部业务更多。原来粗暴简单的从商品订单直接转化为支付链接的流程将不再适用。
这个时候,必须要重新设计整个交易流程的细节了。在进一步讨论新的交易流程之前,我们先来看看一个在传统制造业里普遍存在的概念:订单和工单。
订单,一般而言是对外的概念,它是指企业的外部需求,这种需求是双向的,可以是下游客户对企业的商品需求也可以是企业对上游客户的原材料需求;工单,一般是对内的概念,它是指企业完成下游客户需求的产品时,内部的运作需要。
举个栗子:
比如一个汽车制造厂,4S店是下游客户,看作是C端(Customer),他们向汽车厂提出了汽车购买需求,这就是产品订单;当汽车厂收到一个汽车订单后,它会分解成一系列的生产步骤。比如,它可能将一辆汽车的生产需求分解为车身,动力总成,轮胎外设,喷漆4个部分,分别交给4个不同的车间进行处理。这些分项需求就是工单。车身车间接到工单,可能再次分解成新的分项,比如变成锻压,精磨,焊接等等多个不同的工单交给不同的生产线部门,同时,车间(或者制造厂)还需要向上游的合作伙伴钢铁公司(B端,Business partner)要求购买钢板原材料,这时又产生了新的钢材订单。当一辆汽车订单完成时,实时上制造厂可以查询到该批次汽车的所有原材料进货商的资料和进货单。
也就是说,订单可以分解成多个工单(1:N),而工单本身又可以产生新的订单(M:N),从而下游订单和上游订单之间也存在关联(M:N)。
这是我们现在修改后的交易流程设计(对应于原始版本中红框区域):
核心分为4大表,交易订单 -> 工单流水 -> 支付订单 -> 支付流水
围绕着4个核心表,又有一些辅助表:订单流水,券币流水,余额流水等。
这些表背后的处理逻辑整体划分为两大部分:业务逻辑和支付逻辑。这两部分是相对独立的模块,业务逻辑不关心最终支付途径,支付逻辑不关心具体业务。两者之间通过交易订单ID和支付订单ID进行弱关联。
稍微具体一些表设计见下图:
这样,通过工单和支付订单这个环节的设计,我们着重解决了需求(2),而在交易订单中增加必要的冗余信息,并将订单ID和支付订单ID进行弱关联,我们解决了需求(1)。
2016年2月16日,完稿于卡帕莱。