有赞订单导出的配置化实践

一、引子

1.1 背景

有赞订单导出业务隶属于有赞交易订单管理组,主要职能是将有赞商家的订单数据通过报表的形式导出并提供下载给商家使用。目前承接了有赞所有的订单导出业务,报表的字段覆盖交易、支付、会员、优惠、发货、退款、特定业务等,合计超过 100 个。

1.2 挑战

随着有赞的迅速发展,有赞的行业、业务与产品覆盖面越来越广。从行业角度来看,覆盖了微商城、新零售、餐饮、美业、教育等,从模块角度来看,覆盖了交易、资产、客户、营销、店铺等,从产品角度来看,覆盖了分销、精选等。每个行业、模块、产品都会在订单导出报表中有所诉求。如下图所示,展示了有赞订单导出的域模型:

订单导出域模型

订单导出需要跨越来自不同行业、不同产品、不同模块,对各个业务域的存储和设计有整体理解;同时,需要通过技术手段(数据域、存储域、报表域、文件域)聚合来自各个域的数据集合,生成可读的报表下载给商家。

由此可见,其主要的挑战是:如何快速支持各个域灵活多变的导出字段需求。如何应对这一挑战呢?

二、 架构重构

订单导出的最初实现是从交易的多个 DB 及多个业务 API,分别获取交易、支付、会员、发货、退款、核销、分销等多个数据,组装到一起生成报表。采用 PHP 任务脚本来实现。这种做法有两个痛点:

  • 在小量订单导出的情形尚能应付,一旦同时有多个数万订单导出任务时,资源占用非常大,CPU 基本被打满,PHP 导出进程被阻塞,从而阻塞了所有的订单导出,导出就无法提供服务了。
  • 直接访问业务数据库存在一种潜在风险:如果访问业务数据库的数据量很大,SQL 编写不当导致慢查,往往会给业务数据库带来访问压力,严重影响正常核心业务流程。

基于这两个痛点,有赞订单管理组进行了架构升级,详见有赞技术博文《有赞订单管理的三生三世与“十面埋伏》。 得益于此,订单导出也迁移到基于 ES + Hbase 的技术栈。其中订单搜索采用 ES 服务实现,订单详情则存储在 Hbase 中,通过 API 来获取。整体流程如下所示:

订单导出的整体流程

重构之后,订单导出的性能和稳定性有了很大的提升:

  • 支持百万级订单的导出,且导出的速度比之前大幅提升。以前导出几万订单慢且易阻塞,现在平均能导出 1w/1min,大多数导出可在十几秒内完成。
  • ES 和 Hbase 具有天然的弹性和容量扩展性,即使总订单量有数量级的增长,导出的速度和稳定性也不受影响。
  • 摆脱了容易被阻塞的困境,不再直接访问业务数据库,关闭了导出对核心业务流程的潜在威胁。

接下来,开始了配置化之旅。

三、配置之旅

3.1 初尝配置:设下伏笔

订单导出常常要面临添加新的报表字段的需求。最初实现不太灵活,是来一个字段,在代码流程里添加一个字段。每次增加新的字段,都需要修改多处。因此,第一个优化是采用函数接口编程,将字段定义做成枚举可配置化的,然后遍历指定的报表字段列表,拿到对应的字段定义,计算字段的值,写入报表文件。

根据报表字段列表生成报表行的伪代码如下:

public List<String> generateReportLineData(List<String> fields) {  
        return StreamUtil.map(fields, field -> {
            try {
                FieldDefinition fieldDef = getFieldDefinition(field);
                FieldMethod method = getMethod(fieldDef);
                String value = method.invoke(this.reportItem);
                return postproc(value);
            }catch (Exception e){
                logger.warn("failed to get value for field: {} orderNo: {}", field, reportItem.getOrderNo());
                return "";
            }
        });
    }

这个小小的优化,为进一步的配置化设下伏笔。当需要新增报表字段时,只要增加新的字段定义,而不需要在流程里增加代码。增强软件可扩展性的一个重要方法是,将流程变得通用,只要增删流程里的环节及定义即可。

凡基础必要总是正确的方向。

3.2 报表配置:破局之时

有赞新零售、餐饮的迅速兴起和发展,需要低成本快速地搭建起零售和餐饮的订单导出。这要求订单导出具有更大的灵活性,能够根据不同行业的要求配置不同的字段列表及导出格式,同时又能互不影响。此外,不同商家有个性化的导出需求。然而,原来的订单导出,是专门为微商城开发的商品级别的报表。要加一个字段,往往会影响所有的有赞商家,使用体验不佳,订单报表本身也变得臃肿不堪。

如何突破原来的局限,支持更灵活的订单导出呢?这是订单导出面临的一个破局点。通过订单导出模板解决了这个问题。针对行业、产品配置的导出模板存储在 DB 表 export_biz_conf 里;针对有赞商家的导出模板存储在 DB 表 export_customized_conf 里。每个导出模板包括了如下信息:报表字段列表、导出维度(订单及商品)、报表文件格式、可选项等,做到足够灵活。

若要导出不同报表字段,只要新增相应字段,指定报表字段列表即可;若要生成不同维度的报表,可使用策略模式。比如,

  • 导出大订单量,采用批量并发策略更高效;导出小订单量,采用串行策略更易理解;
  • 可以把字段定义写在本地代码里直接引用,或者配置在 Groovy 脚本里更加灵活;
  • 可以根据指定的订单级别或商品级别进行维度聚合,然后计算报表字段的值;
  • 可以根据指定的 csv 或 excel 生成相应的文件。

如图所示: 针对导出流程的各环节,可采用策略模式来选择不同实现,然后将策略组合起来。 订单导出的策略组合

通过实现报表配置功能,突破了之前的局限,可以支持不同行业、产品的标准化和定制化导出需求,并且做到相互隔离不干扰。

3.3 配置深化:更快更稳

随着有赞进入更多的行业,面临着更加多变和个性化的导出需求。比如,有赞教育需要导出知识订单的学员信息和课程信息,有赞零售需要导出导购员和发货仓库门店名称。 显然,如果要完成某个导出需求,还需要修改代码、发布系统,这种操作会非常频繁,导致开发和维护成本提升,影响系统稳定性。

如果能够在应用运行中动态地新增报表字段并加载和使用,无需修改导出工程代码,无需重新发布系统,就能更加快速地支持导出需求,将会大幅降低导出需求支持的开发和维护成本,保持系统稳定性。

为了解决这个问题,引入了动态脚本语言 Groovy. Groovy 是能够与 Java 无缝对接的好伙伴,可以直接使用 Java 类的功能。编写 Groovy 脚本实现报表字段逻辑,存储在字段配置表 export_field_conf 里, 在报表配置表 export_biz_conf 或 export_customized_conf 里引用,然后在应用启动时缓存到内存里并使用。比如粉丝姓名的 Groovy 脚本如下:

import com.youzan.trade.orderexport.util.PublicUtil 

def fansInfo = reportItem.orderInfo.extra["FANS"]
PublicUtil.fetch(fansInfo, "nickname")    

PublicUtil 是导出工程里封装的一个工具类,可以让编写字段配置脚本更加简单。值得提及的是,为了避免使用 Groovy 脚本可能导致的内存泄露,需要对编译后的 Groovy 脚本进行缓存和执行。

为了实现无需改动代码和发布系统,还需要在整体流程上打通。整体流程如下:

Step1: 当用户下单后,源数据落到业务数据库的扩展信息里;
Step2: 通过数据同步,自动同步到 Hbase 表;
Step3: 通过 Apollo 配置和可扩展的数据聚合机制,将数据自动输送到用来计算报表字段值的报表对象里;
Step4: 新增报表字段的配置;
Step5: 在报表配置中引用该字段的标识。

下图展示了通过配置自定义字段快速支持导出需求的整体流程。

订单导出配置化的整体流程

整体流程打通后,当需要新增个性化字段时,通常只要做两步:

  1. 增加个性化字段的配置,包括 Groovy 脚本;
  2. 测试通过后,刷新应用的配置即可。

个性化字段配置能力已经在线上稳定运行,比如拼团订单成团时间、零售导购员、有赞教育的课程字段等。

3.4 通用导出:锦上添花

紧接着,订单导出又面临分销采购单的导出需求。分销采购单导出流程跟订单导出有所不同,需要分别导出分销买家订单和供货订单的详情信息再导出。这个流程跟通用的订单导出流程是有所区别的。如果通过修改订单导出的通用流程来支持,显然会影响所有的订单导出,使订单导出流程不清晰。

最终采用的解决方案是:对分销采购单的导出需求和所需技术进行抽象,实现一个更加通用的导出能力模型,支持交易领域的各种潜在导出需求,而不仅仅局限于分销采购单导出。通过分析订单导出流程可以发现,绝大多数导出都遵循如下核心流程: 插件化导出流程

可以将核心流程做成插件式的。首先,定义一个插件接口,包含其配置和功能等;其次,实现常用的插件列表,支持从 ES, HBase, API 查询或获取数据,以及常用的过滤、排序、格式化、生成报表等功能;最后,将这些插件列表串联成一个具体的导出实例。整体流程则采用模板方法模式复用了订单导出流程。

比如微商城分销采购单导出通过依次执行ES查询插件、订单详情插件、数据排序插件、报表字段格式化插件、报表生成插件来实现,其中订单详情插件针对分销买家单和供货订单分别调用了一次。

四、质量保障

前面提到,订单导出的报表字段非常多,导出数据量大,如何保证代码改动或重构后订单导出的服务质量和数据准确性?主要手段如下:

  • 质量流程保障是第一位的。最主要的三项是:单测严格全部通过; CodeReview 由应用责任人及经验丰富的高级工程师同时通过;预发线上导出对比工具通过。
  • 整体架构设计保证了订单导出的性能、稳定性和可扩展性。
  • 持续小幅重构使得系统能够持续优化,避免一次性大改造伤筋动骨且容易导致线上故障。
  • 设计先行,对代码质量非常重视。
  • 运行预发线上订单导出自动化对比工具,很大程度上增强了成功发布的信心,是发布前保障质量的一道重要防线。

此外,采用函数编程及设计模式,使代码实现层面更具复用性和柔软性。18K 行代码,代码重复率约为 1.8%。

五、小结与致谢

本文简要讲述了有赞订单导出的配置化实践。通过配置化之后,订单导出的能力和稳定性有了大幅提升。当然,还有一些需要提升的地方。比如,可以增加扩展点机制,允许业务方定制化导出;局部细节可以打磨得更细腻。欢迎对海量订单业务感兴趣有经验的小伙伴与我们一起共建订单管理大局!简历可直邮 shuqin@youzan.com.

在这个过程中,有许多小伙伴给予了有力的支持,比如产品同学对订单报表的细致的规划设计、客满运营同学提出的及时反馈、有赞技术团队的支持以及自己的付出。

欢迎关注我们的公众号