一套成熟稳定的新零售交易系统的构建

2019-08-04 22:59:10   最后更新: 2019-08-04 22:59:10   访问数量:177




主页君自2017年2月至2018年6月主导了一套新零售订单履约系统的构建与维护,起初三个人从零开始历时三个月搭建一套完善可用的订单履约系统,迎来线上外卖店开业,到一年后团队扩充到十六人,如今离职近一年,回想起来仍有很多方面值得总结学习

 

 

本文,主页君就带大家详细了解一下整套电商订单、履约服务应该如何构建,以及有哪些可能遇到的问题及应该关注的细节

  • 注:本文只为博主记录,拒绝任何形式转载,谢谢!

 

整个电子商务系统中,无外乎三大最为重要的流程:

  1. 信息流
  2. 现金流
  3. 物流

 

我们的整个订单履约交易系统正是建立在这三大流程相互推动形成的三大闭环之上的

如何整合三大流程,让三大流程相互作用的过程中保持相互独立并且各自成环成为了整套系统架构设计的核心要点

 

 

图中,红色的箭头表示现金流的流转过程,黄色箭头表示物流的流转过程,而蓝色箭头则表示贯穿于整个系统中,驱动上述两大流程流转形成闭环的信息流流转过程

在整个交易过程中,肩负信息流、物流、现金流整合与流转的就是图中的三大系统:

  1. 订单系统
  2. 售后系统
  3. 履约系统

 

订单系统的职责

现存电商系统中,订单系统与履约系统责任划分主要有两种模式:

  1. 订单负责现金流,履约负责物流,二者信息流分别驱动自身现金流与物流的流转,因此,订单支付成功为订单终极状态,此后由履约系统负责接下来的状态流转
  2. 订单系统作为与用户签订单据的维护,统领整个交易过程中各环节,直到交易终结,钱货两清,在这样的设计中,履约系统作为整个电商系统的一个内部系统,通过物流与履约内信息流的作用驱动履约状态流转,而履约状态流转驱动订单状态的流转,最终,订单的终结状态为已送达

 

出于用户单据的统一维护,我们采用第二种设计方案,用户只面对订单单据,用户取消、退款等操作均直接与订单系统交互,订单系统根据订单具体的状态决定是否调用履约系统或售后系统

但一旦订单送达,钱货两清,则订单系统的任务完成,不再与用户交互,如果用户有售后需要,则直接与售后系统交互

所有单据一旦生成,除单据状态的流转外,其他静态数据均不能发生任何变化,因此订单系统不标记商品部分或全部缺货或退货信息,缺货信息由履约系统生成缺货单后与 CMS、售后等系统交互,完成缺货处理,退货信息由售后系统生成退款单后进行处理

 

售后系统的责任划分

售后系统的主要责任是生成退款单据并与支付系统交互,完成现金流的反向流转

售后系统拥有自己的退款单表、退款商品表、退款促销表的存储结构

 

订单系统是与用户交互的核心系统,在为线下、外卖、客户端、微信小程序等多渠道来源的用户提供服务的同时,肩负着用户信息、商品信息、促销信息等多条信息流的整合与快照存储,以及驱动用户现金流流入的重任

根据电商发展至今所形成的固定交易流程,整个订单系统由三大子系统组成:

  1. 购物车
  2. 订单生成
  3. 支付系统

 

 

 

购物车

当前电商交易的主要发起流程为:选择商品 -> 加入购物车/立即购买 -> 结算,购物车便在其中起到了承上启下的作用

 

如何交互

用户与购物车系统的交互主要是通过商品详情页或购物车页面操作增删购物车中商品,提单前可进行不限次数的若干次操作

那么,这样的场景下后台与客户端如何交互呢?

最容易想到的是,客户端传递用户增/删操作信息、商品及数量信息到后端,后端返回操作是否成功,但是这其中存在两个问题:

  1. 如果某次交互出现网络异常,服务端数据库更新成功后未将操作成功的结果返回到客户端,则客户端展示与后台存储将出现数据不一致的情况
  2. 客户端无法获取到促销相关信息,由于购物车肩负着订单转化率与GMV等核心指标提升的重要作用,促销就成为了不可或缺的核心信息,而促销有着复杂的商品堆逻辑

 

由于上述两个问题,购物车在设计中采取了客户端传递用户增/删操作信息、商品及数量信息到后端,后端返回整个购物车商品、促销列表的交互方案

一旦交互过程中出现网络问题,客户端友好提示用户后,下一次操作如查询或增删商品成功后,可保证客户端与服务端数据的一致性

 

如何存储

由于购物车数据的临时性,以及购物车操作频率远高于订单提交、查询等操作的特性,加上公司统一运维 Redis 的可靠性,购物车采用 Redis 进行购物车存储

根据上述交互模式,购物车整车列表返回,并没有车中单个商品或促销的查询需求,因此,购物车采用整车信息序列化后作为 value 存储到 Redis 的 hash 结构中

field 为购物车类型id(线上购物车,线下收银台购物车或线下自助购物购物车),key 为用户 id

未登录情况下,则使用由客户端生成的保证唯一的 uuid 作为 key,主要为了保证每台设备每个购物场景下购物车的唯一性

 

何时合并未登录购物车与登录态购物车

上述设计中,我们保证了每个 userid/uuid 在每个购物场景下购物车的唯一性,但每个未登录用户随时可能进行登录操作,此时未登录购物车需要与登录购物车中的商品进行合并

理想的设计是登录成功后,用户系统将 uuid 与 userid 对应关系发送至消息队列,监听消息队列的购物车系统执行登录前购物车与上次登录购物车信息的合并操作,这样通过消息队列,实现了用户系统与具体业务系统之间的解耦,但这么做存在一个明显的问题,由于消息队列异步通信的特性,无法保证在用户登陆后的首次操作前完成两购物车的合并

如果改为让用户系统登录后执行回调,同步调用购物车系统接口,则用户系统耦合具体业务方逻辑,这是我们不愿意看到的

另一种方案是对于登录后购物车,每次客户端请求服务端都携带 uuid、userid 参数,服务端先查询 uuid 购物车,如存在,则执行 uuid 购物车与 userid 购物车合并并删除 uuid 购物车操作,此后再进行正常的业务逻辑,这样对于所有已登录购物车都额外进行一次查询,为 Redis 带来不必要的压力

于是最终采用客户端登陆时先调用用户系统,如果登陆成功,则发起 uuid 与 userid 购物车合并请求,调用成功后提示用户登陆成功

但是这么做仍然存在问题,那就是如果客户端调用购物车系统正常,服务端完成 uuid 购物车与 userid 购物车的合并操作并删除 uuid 购物车,但返回超时,客户端提示用户登陆失败,用户返回购物车列表发现购物车被清空,这显然是不可接受的,解决方案是在 Redis 中维护一个待删除购物车 id 为 key 的 hash 结构,value 为添加时间,每日非交易时间由定时脚本统一对比购物车最后一次操作时间与待删除 hash 中的添加时间,删除 hash 中所有待删除且添加后未再次使用的购物车记录(购物车 id 由购物车类型与 uuid/userid 复合而成),从而实现延迟删除

 

与促销的职责划分

促销系统负责促销规则的制定与维护,电商所有促销,无外乎以下四大类:

  1. 折扣
  2. 减价
  3. 赠送
  4. 套装

 

但这四大类之上,每个大类又有着多种不同的细分,如折扣有普通的单件折扣,也有着多商品参与的满折、折上折等,减价有着单件的立减、多件多品类的满减,赠送有着单件买赠、多件商品满额赠、单件或品类中满件赠等等,加上各种促销活动在同一个订单中的叠加出现,以及部分参与促销、预售促销、出清促销等多种促销模式的出现,情况显得越来越复杂

随着促销规则的复杂化,分支情况的显著增长,初期采用的促销系统维护规则,订单系统查询促销规则并根据促销规则将订单中若干商品归入促销堆中这一方案显得越来越难以维护

为实现促销系统的足够内聚,最终采用:订单系统将整个订单中全部商品传递给促销,促销系统内部进行商品促销堆划分工作并返回,以促销堆为维度划分商品,而促销堆中每件商品单独关联并记录若干个促销记录,为便于退款金额计算,每个促销若有多个商品参与,则减免金额分别分摊到多个商品上单独记录,赠品按照赠品参与促销,正好减免赠品原价记录,这样,在记录所有信息的同时,为售后系统提供了最大的便利程度

 

订单提交流程

订单提交流程主要的职责是存储订单快照,生成订单凭证

 

如何抽象化订单下单流程

在这一过程中,由于订单系统接受来自不同渠道、不同场景下的用户下单请求,不同渠道、不同场景下需要调用不同的若干外部服务以及对订单本身进行不同的处理

那么,如何抽象这一过程,让这一分支过多的过程能够整合到一个服务中,实现最大的可维护性呢?

基于上述各方面复杂度的考虑,订单系统通过责任链模式实现了全流程各模块的动态插拔

订单系统将所有外部依赖、处理子流程均封装为一个个独立的类,包含具体的操作方法与指向下一模块的指针

订单系统接到用户请求后,根据预定义责任链配置及具体下单请求的渠道、场景、执行环境及参数,将所需操作模块动态拼装为一条完整的责任链后,依次执行责任链上每个模块的操作方法即可

一旦中途遇到问题,可根据配置及各模块属性,动态决定沿责任链反向调用各模块回滚方法还是忽略责任链上某模块继续执行等操作,实现了最大程度的配置化和自动化

 

如何保证分布式事务的执行

此前我们曾经介绍过分布式事务的通用解决方案:

分布式事务通用解决方案

 

订单系统在设计中,综合使用了两阶段提交、补偿事务、本地消息表等多个分布式事务策略

上面提到,在下单流程责任链中,一旦某个必要环节出现问题,则整个订单系统会按照责任链回溯,按照模块属性选择是否调用对应的补偿方法,或基于本地消息表发送状态变更消息

而另一个显著的分布式事务场景就是订单所有数据的入库操作

对于订单系统来说,数据库设计分为:订单表、订单属性表、订单商品表、订单促销表等多个表,其中订单表作为主表记录整个订单的基本信息,每个订单对应订单表中一条记录,同时对应属性表中若干条属性记录及订单商品表中若干条商品记录,而每个订单中订单商品表中的商品记录与订单促销表中的促销记录又产生了多条对应多条的关系,每个商品可参加多个促销,每个促销活动可能包含多个商品

对于每个订单的生成与入库,都伴随着这若干个数据库表中记录的插入,那么如何保证这些记录的事务性,即在订单下单成功时,所有记录全部完成插入,而不允许部分记录被插入并查询到情况发生呢?

最简单的方式是采取数据库事务,但这将对未来可扩展性造成了不利影响,例如随着业务规模的扩大,不同的表可能位于不同的数据库中,将不得不将数据库事务改造为跨库事务,虽然 mysql 5.7 版本以上原生支持了跨库事务,但跨库事务本身性能非常低,应该尽量避免去使用

基于上述考虑,订单系统结合两阶段提交的分布式事务解决方案,采取了以下策略:

  1. 订单表增加 valid 字段,标识订单是否有效,默认为 0(无效)
  2. 依次插入订单表中订单记录关联的订单属性记录、订单商品记录、订单促销记录等多个表中的记录
  3. 更新订单表中该项 valid 字段为 1(有效)

 

如果在上述第二步或第三步操作中,有任何一步操作失败,都会让整个流程中断,订单记录 valid 字段为 0(无效),所有查询服务均不会返回该订单项,从而实现整体事务一致性的保证

 

支付流程

 

如上图所示,支付系统与具体的业务解耦,不关心具体的订单,只维护交易流水号与交易信息,客户端通过每笔交易唯一的 token 进行校验,并通过交易流水号调起支付页面进行支付

整个过程就是一个第三方授权认证的过程

交易系统设计中的基本原则是力求做到各环节均保留单据用于对账,支付也是一样,每次请求支付前都会生成支付单,成功获取 paytoken 后,再将支付返回的信息,如支付独有的减免金额,支持的支付方式等信息更新到支付表中,只有入库成功,才返回成功

 

如何避免重复支付

上述操作是否存在用户重复支付多笔的可能呢?

答案是存在的,因为存在调用支付失败或入库失败或客户端请求超时的情况存在

当订单生成成功,但调用支付系统失败或入库失败、客户端请求超时等情况发生时,允许用户在订单列表中点击支付按钮重新发起支付流程

如果此时用户并发发起多个请求同时到达服务端,多个请求同时判断数据库中不存在该订单的支付单,于是试图请求支付生成不同的 paytoken 返回给用户,就存在重复支付的可能

解决这个问题最简单的方法是在支付表中设置唯一索引,从而保证只有一个 insert 语句执行成功,其他试图插入的请求捕获 duplicate key 异常后,查询数据库获取 paytoken 返回或返回用户正在支付中

那么新的问题产生了,如果未来一个订单可分笔支付多次呢?那么,我们可以在现有唯一索引中添加支付单索引号字段,该字段在每个订单中从1开始递增,标记一个订单的多个支付单,那么由于同一个订单的多个支付单是通过一条语句统一入库的,同一个订单的多个并发的入库请求就可以保证只有一个可以插入成功从而避免重复支付的发生

 

用户下单支付成功后,接下来最重要的流程就是如何指导店内员工进行快速高效的分拣、打包工作并实时对接配送团队,第一时间将货品送达到用户手中,这整个过程就是履约系统的职责

与订单系统不同,履约系统并不直接与用户交互,他最重要的职责是指导店内员工的工作,分拣、打包、交接、配送相互串联同时又相互独立,店内情况错综复杂成为了履约系统最大的挑战

在缺货、货品异常等异常情况发生后,由子系统上报到履约调度系统,再由履约调度系统将具体情况反馈到售后系统,由售后系统联系用户,执行具体的部分退货或取消订单等操作后通知到履约、订单系统,实现整个系统的信息流闭环

 

履约子系统划分

履约并不关心现金流的流转,他担负着信息流与物流的流动,因此,如何串联各子流程,如何让信息流、物流有效的相互驱动同时保持相对独立是首要解决的问题,于是诞生了最初的基础架构

 

 

上图中展示了履约系统构建初期的基本架构,其原则是,各子系统各自保持逻辑与业务的内聚,而履约调度系统成为所有子系统之上的统一调度系统,由他调用各子系统接口来驱动各子系统的具体工作

各子系统在履约调度系统的统一调度下各自生成单据驱动子系统内部流程,向履约调度系统实时反馈状态,履约调度系统不关心业务细节,仅承担整个履约单据的调度以及与外系统的必要交互

这样的架构之下,实现了履约各独立业务的内聚,同时也在最大程度上为未来履约流程的进一步复杂化提供了支持,例如,此后诞生的店内备餐、餐饮等流程,只需要增添新的履约子系统并未该子系统设计其内部流转所需的新的单据即可将新的业务流程迅速接入现有流程中

下图即后期履约主体流程时序图:

 

 

其中浅红色为缺货等异常情况处理流程

 

分拣、打包是否应该被归入仓储系统

仓储系统是电商平台中与交易系统相互独立的另一大主要系统,他承担着采购、入库、标记货架/货位与货品间的对应关系的重要责任

在京东等很多电商平台中,分拣、打包是仓储系统仓储工作中的一环,是否应将我们的分拣、打包工作放入仓储系统在我们的新零售业务系统规划初期产生了重大争议,最终,我们仍然独立诞生了履约系统,将分拣、打包工作归为履约系统的独立工作,这是为什么呢?

京东等电商平台将分拣、打包工作作为仓储系统的一个环节,原因在于其目标是纯自动化分拣、打包,这是因为其面对的是纯线上业务,分拣、打包所面对的货品完全依赖于仓储系统的货品调配,分拣前可完全指定待分拣商品的货位与分拣动线,从而让分拣系统所做的工作十分有限

而在新零售的业务场景中,卖场承担了很大一部分的仓储工作,而在线下用户购物的影响下,卖场中的仓储情况是线上无法完全控制的,在这样的业务场景中,承担线下实际工作的分拣员拥有了更大的自主性,缺货、货品所在货位错误等异常情况频繁发生,如果将这部分业务放入仓储系统,则仓储系统显得过于复杂,而与此同时,异常情况发生后的处理则与交易系统更为贴近,于是这部分业务独立成为一大与交易、仓储、售后相互平行的业务系统是对其未来发展与业务进一步复杂化的有效应对

 

线下人员抢单还是派单

对于线下人员工作的分配,通过系统自动派单,还是让线下人员主动抢单是两种不同的业务模式

显而易见,抢单模式下,系统的设计复杂度最低,只需记录是谁抢到了哪一单即可,但实际工作中,使用抢单模式如果不配合绩效打分,就会出现工作分配的严重不均,无法避免线下某些人员在实际工作中偷懒拒绝抢单的情况发生

而一旦引入绩效考核激励制度,不可避免的会产生人员竞争心态,会加速线下人员变动,在业务诞生之初,人员不足的实际情况下,人员变动的加速对于业务的发展是十分不利的

因此,最终,我们使用了派单的模式,由系统将各个单据下发到正在工作中的分拣、打包员的手持设备上,指导具体员工完成实际的工作

 

系统设计原则

最后,来说一下整套系统的设计原则

  1. 整体来说,整套系统坚持阿卡姆剃刀原则 -- 如无必要,勿增实体,满足现有需要为前提,尽量考虑可扩展性,但只在必要的时候演化架构,不提前过度设计
  2. 重构有成本,尽量不进行大规模重构,虽然设计不是在一开始完成的,而是在整个开发过程中逐渐浮现出来的,但要在整个系统构筑过程中不断改进设计,而不是堆砌代码,直到无法前进时重构,为了实现这一目标,定期组内宣讲,代码相互 review 是十分必要的
  3. 所有项目都有明确的责任负责人,有人维护,责任到人,不建立所谓的大家共同维护的公共服务与公共代码

 

代码组织原则

 

 

如上图所示,整个项目结构通过 facade 模式,将对外的接口层与内部可复用的具体业务逻辑相隔离,他们之间通过切面实现监控、日志的自动上报与打印,并通过定义外部无需感知异常来实现业务的自动降级,加上 Hystrix 自动熔断降级,保证在异常情况下系统业务的相对稳定

而 gateway 一层对外部依赖的 http、rpc、数据库、缓存等的统一而简单的封装,让业务层无需关注底层交互,只需进行通用的方法调用即可完成,而底层依赖的耗时、成功率等情况也在切面中进行了统一的监控

 

异常降级

上面已经提到,在代码整体设计中,通过切面与 Hystrix 相结合,实现了异常情况发生时的自动降级功能

对于部分重要依赖,在设计初期,需要考虑配置化手动一键降级,保证在特殊情况下的系统稳定

 

并发优化

在实际业务中,需要不断思考优化,观察系统各项监控,留心 GC 频率等指标,可以通过将可并发执行部分逻辑并发化等方式实现系统性能的提升,但内部并发调用的增多需要考虑系统的并发承受能力,此时,一定的压测是十分必要的

 

 

欢迎关注微信公众号,以技术为主,涉及历史、人文等多领域的学习与感悟,每周三到七篇推文,全部原创,只有干货没有鸡汤

 

 






work      电子商务      新零售      mtdp      订单系统      履约系统      仓储系统     


京ICP备15018585号