WMS项目

项目简单介绍

项目本身是面向国际的奢侈品品牌的,他其实是共享模式下的Saas项目来,项目本身是所有客户共享,但数据库上不同的品牌方是完全独立的,在项目之中其实是依靠AbstractRoutingDataSource实现动态切换的,这一点比较简单就不再细说了。

架构转变 - 微服务

单体架构

WMS最早的时候其实是单体项目,大概是在12年左右的项目。只有单个服务来负责所有工作,而实际部署的时候,则是一个客户一个单体项目来部署的,这种方式当然在早期的项目设计之中非常常见,但只要请求流量大一些就会出问题,并且这样的部署方式相当于每一个客户都需要单独承担服务器的费用自然是不合理的。所以后续又改为了分布式架构。

分布式架构

因为单体项目的缺陷,后续其实将WMS根据业务做了服务拆分,其实主要就是包括以下几个服务,项目基础功能服务、仓储基础管理服务、入库服务、出库服务、EDI&报表服务。并且由公司购买一定的服务器数进行部署。来提供具体的服务功能的。如果抛开业务不谈,本质上其实是一个共享性的SAAS项目,而数据库则是一个客户独立一个的。
相比于单体,这样的拆分可以使得不同的服务的资源使用单独拆开,提高了服务总体的请求处理能力。并且对于客户来说费用是均摊的客户自然也是更能接收的

微服务架构

本质上是针对分布式架构做进一步拆分,因为按照分布式拆分的话,其实针对请求量大压力大的服务来说单体服务的问题还是一样会出现,因为请求压力的增大如果不做架构调整只能是提升服务器,而到了一定程度之后服务器的性能提升要花的钱会更多。WMS这边其实主要的压力还是在出库的方面上,并且也因为在电商促销时的压力下最终决定将分布式的架构转变为微服务的架构。
具体的拆分其实是依靠DDD领域驱动、程序设计原则(最常用的:单一职责原则、开闭原则)、业务逻辑做拆分。大体按照以下服务来进行拆分:

  • 基础服务:身份认证、权限管理、基础数据、自定义化功能等
  • 库存管理服务:库存调整、库存盘点、库存冻结、仓库管理、库存查询等
    (库存查询 + 库存冻结)、(库存调整、盘点、管理) 两大服务拆分
  • 入库服务:预入库、收货、入库质检、上架、入库订单管理
    (预入库[入库单、库存表信息创建])、收货、(质检+上架) 三大服务拆分
  • 出库服务:预占库存、拣货、出库质检、打包装箱、发货(发运)
    (出库单创建+库存预占)、拣货、(质检 + 打包装箱)、 发货发运 四大服务拆分
  • EDI服务
    对账服务、库存预警、常见定时任务 三大服务拆分
    (对账服务、库存预警的拆分其实是拆出来利用Quartz做定时调度,要求是相对独立尽量不影响核心功能)
  • 报表服务:自定义类型报表、Excel导出类型报表

实际情况,可以说在刚进公司的时候,主要微服务项目的服务器主要有8台 后面增加到 10台,拆分的服务大概十来个,主要几种在入库、出库、EDI的拆分上。像基础服务、库存管理、报表服务其实基本没怎么做拆分。
10台服务器其中3台充当数据服务器(Mysql、ElasticSearch、Redis),两台微服务核心服务器(Nacos集群、Sentinel控制台、Gateway、还有一部分服务)、五台项目服务器(出入库、库存管理、EDI、报表等等基本都在这几台服务器上)

项目QPS、TPS数据量问题

QPS / TPS

入库:入库QPS / TPS 100-200、
出库QPS / TPS 200-500、
库存管理QPS / TPS500-1000、
库存查询QPS在200 - 500平常,电商峰值可能会达到1000以上

WMS的项目因为涉及到货物管理,很多的操作除了单纯的查询动作,大部分的工作其实都是在改动数据状态和数据记录,这也导致了不少的功能都是需要开启事务的,使得TPS和QPS非常接近。粗略的可以将他们约等于的状态

数据量

单个品牌客户的SKU:70w左右,库存记录:300w左右,每天订单在2-3w,日志记录一般在3G左右。

项目业务上下游

WMS的上游是OMS / ERP(作为ERP内嵌的一部分),下游其实是物流系统TMS

核心业务

核心的业务其实还是仓储物流的那些,比如 货物入库、货物出库、仓库管理*(仓库、货架)、季、年度的货物盘点(检查是否存在保存不当以及出现库存不一致的问题)*、独立的计费系统、季 年度销售/货物情况报表导出。下面主要针对入库、出库、库存盘点、季 年度相关报表导出的工作来做介绍。

主要涉及表格

  • 入库/出库信息:入库/出库主表、入库/出库明细表
    • 入库/出库主表的作用其实是等同于商城中的订单,只不过额外会多几个仓储相关的字段
    • 入库/出库明细表与出库主表形成一对多的对应关系,本质上跟商城的订单的具体明细和订单本身的关系是一样的。等同于将订单中涉及到的多个商品信息拆开多条记录存储。
  • 库存信息:库存主表和库存变动明细表。
    • 库存主表:根据商品和仓库对库存进行统计,比如A商品在X仓库有多少可用库存、预占库存、在途库存、临时库存、冻结库存等等 (一种商品在库存主表之中会对应着多条数据记录 [不同仓库])
    • 库存明细表:记录库存变动的具体信息 (一个库存、一个库位上的库存变动,比如说预占多少库存,操作前后库存量等明细记录,预占包含有预占超时时间字段,如果直接取消预占则会直接新增一条记录) 这样拆分的作用其实相当于做了一个垂直分表,将明细的信息拆出来了,大部分经常查询的字段又保留在了主表之中。这样可以提高库存查询的效率。

入库

一般来说常见的入库,有以下两种类型

  1. 日常入库实际业务流程,他的主要业务流程可以介绍为:
    预到货通知 (创建在途库存,标记为在途库存记录) -> 确认收货(修改在途库存记录信息) -> 质检 -> 上架(自动分配库位)
  2. 退货货物入库流程,他的主要业务流程可以介绍为:
    退货审核 -> 退货接收 -> 质检 -> 重新上架(自动分配库位) 【不做详细介绍】

日常业务流程

业务大体流程:上游订单消息消费 (Api + 幂等处理) -> 预入库单、入库明细创建 + 库存主表、库存明细表添加库存记录(在途) -> 收货 (修改入库单表状态为已收货、修改库存表信息为临时库存状态) -> 质检 (修改入库单状态) -> 上架 (修改入库单状态为完成、库存表状态为可用库存)
库存状态流转:在途库存(创建预入库事务保持一致) -> 临时库存(与收货事务保持一致) -> 可用库存(与上架事务保持一致)
入库单状态变化:新建 -> 预收货 - 收货中 [存在一定的中间态] - 已收货 -> 待质检 - 质检完毕 -> 待上架 -> 已完成 / 已取消

业务具体流程

  1. 根据上游消息,创建入库单、入库明细记录,并将订单状态调整为预收货,修改库存主表对应仓库和库位的货物的在途货物数量、插入库存明细表记录库存变动。
  2. 货物实际到货后,进行收货处理 [逐个对入库明细记录做收货,并修改之前创建的库存明细的状态从在途 -> 临时,全部完成之后修改入库单状态 - 总的来说,就是做业务流转]
  3. 质检 [根据入库单一对一创建质检主表和质检明细表,质检完毕后修改入库单和入库明细状态 - 做业务流转]
    补充说明:[对不合格的还会额外修改库存明细表的库存状态,并在整个入库单之间完毕后将质检结果以邮件发送Excel的方式通知客户] - 这部分估计也没人关心
  4. 上架处理:根据一定的规则,将货物存放到仓库之中指定的库位 (货物架上)。 [修改库存明细记录状态,修改库存主表的可用库存和临时库存数量,并修改入库单状态]

入库核心业务关键点其实是预收货处理和在途库存和临时库存对出库的影响。这里大概先补充一下预收货库存的介绍,至于在途库存和临时库存的介绍会在下面出库之中介绍 。

预收货库存记录流程:

  1. 上游OMS / ERP系统通知货物入库,由接口触发 (幂等) [这里涉及分布式事务,跨上下游的调用,但不管是最终一致性还是TCC,WMS一侧只需要提供接口即可。创建接口和回滚接口,至于重试是上游的问题]
  2. 开启本地事务,并利用数据库的自动DML锁来保证库存主表改动的数据一致性
  3. 创建预收货入库单、入库单明细 (标记为在途库存)、更改对应仓库商品的库存记录、插入库存明细的记录 (库存明细之中的库存是在途库存的状态) 即可。

出库

一般来说常见的出库原因,主要有以下两种类型

  1. 日常订单出库实际业务流程,他的主要业务流程可以介绍为:
    上游订单消息 -> 库存预占 + 生成出库订单、出库明细 -> 拣货 -> 出库质检 -> 装箱 -> 交付物流TMS(实际库存扣减)
  2. 调度出库实际业务流程,他的主要业务流程可以介绍为:
    调度申请 -> 原有库存确认 -> 拣货 -> 打包装箱 -> 交付物流TMS(实际库存扣减) 【不做详细介绍】

日常业务流程

业务大体流程:上游订单消息消费 (Api + MQ(幂等处理)) -> 预占库存、出库单、出库明细创建 -> 拣货 -> 复核打包装箱 -> 出货
库存状态变化:预占 -> 出货try的冻结 -> 出货commit的扣减
出库单状态变化:新建 -> 分配(预占库存) -> 拣货 - 拣货中 - 已拣货 -> 待质检 - 已质检 -> 打包 -> 出货 / 已取消

业务具体流程

  1. 根据上游消息,检查当前库存,如果库存充足则创建库存明细记录预占订单所需的库存,并修改库存主表对应仓库和库位的货物数量记录、创建出库订单、出库订单明细记录 (该部分比较核心不做斜体处理了)

  2. 根据预占的库存做拣货 [创建拣货任务表和拣货明细表,并修改订单明细表做业务流转]

  3. 对已完成拣货任务的货物做复合打包装箱 [装箱主表和明细记录生成,同时做订单明细的业务流转]

  4. 出货(出库单完成、库存扣减),通知TMS第三方物流公司交付货物

    有一些奢侈品品牌还会有出货的质检,如果质检不合格还会从库存中重新占库存重新做拣货打包装箱的一系列处理。

出库核心业务关键点其实是在库存预占和库存扣减。有可能会有面试官对这两个部分比较感兴趣,后面补充一下这部分的内容。

库存预占 和 库存扣减

库存预占:其实指代的是WMS由上游接收到新订单请求消费的时候,提前预占好库存。尽可能保证出库订单都有货物可出 (不考虑出库质检不通过的情况)。以下为实际预占流程:

  1. 上游推送订单信息,接口接收后推送消息到MQ

  2. 出库服务注册的消费者,开启本地事务(这部分不需要分布式事务,因为根本不涉及跨服务调用)且通过使用客户 + 仓库 + 商品ID通过分布式锁加锁。避免出现库存超卖问题。

  3. 开启事务以及加锁之后,首先会通过库存主表中对应商品ID查出所有可用库存 - 预占库存 & 冻结库存 直接用可用库存来尝试分配。如果可用库存不能直接满足需要的话,还会将当前在走入库流程*(包括还没到仓库、入库单有了但未上架的)* 的库存也就是所谓的在途、临时库存也尝试做预占,提高客户货物的流转 【毕竟多卖点多赚钱】

    在途库存和临时库存参与出库处理带来了更加复杂的业务处理,也因此一般只能用在人工仓库之中,自动化仓库则完全无法支持该功能。比如:

    1. 在途库存、临时库存都没有经过质检,这就导致WMS库存计算可能不准确,出库流程复杂度提升的问题,实际可用库存数量是不能百分百无误的 质检带来的问题就导致出库时要重新占库存再拣货 - 该部分会在仓库人员质检不通过的时候,再处理。
      有库存预警的机制所以一般补货都是饱和式补货会额外预估多一点不太可能说补完还是不够的情况,如果真的出现了会触发库存预警,通知客户方继续补货
    2. 临时库存还会存在无法准确定位 (实际上由于是标准化流程能大概知道位置,只不过还是需要仓库人员帮忙快速找到货物才能顺利出库)

当然了也因为库存预占如果将在途库存和临时库存一并统计出库,在出库热销商品的时候往往有可能会出现超卖的情况,并且预占的货物也有可能会因为后续质检不合格要重新分配(重新占用库存),这往往会较大程度增加仓库人员的工作量。因此实际上在平常正常提供服务的时候,是不会将这部分库存考虑进去的。
平常模式:直接通过Redisson分布式锁对品牌Id + Sku Id的作为key进行加锁避免集群并发问题,再做常规的库存预占即可
电商促销(通过系统参数配置决定是平常还是促销):直接加分布式锁的方式,性能会比较差。
目前的解决方案的具体流程如下:

  1. 依靠Redis针对热门商品的库存缓存做检查,判断是否还有库存 (缓存中库存会有 在途、临时、可用三种)
  2. 通过Redisson 针对品牌 + SKU进行加锁,避免并发出库造成超卖问题
  3. 开启事务,检查库存表做库存预占和出库表、出库明细创建
  4. 利用Redisson的执行lua脚本 (利用Pipleline扣减商品缓存库存 incry key、利用hash结构 hset做幂等处理)

库存扣减:其实指代的是WMS出库流程最终真正完成出库的部分,因为只有在这个时候库存才真正意义上离开了仓储物流所以实际的库存扣减也是在这个部分发生的。当然因为涉及到TMS第三方的调用这个部分其实会涉及到分布式事务的处理。在实际实现的时候不同的用户有不同的处理方式,一般分为TCC和基于MQ的最终一致性处理。

使用TCC开启分布式事务

Try方法:

  • 将库存主表原先预占的库存数改为冻结数 [需要用数据库加互斥锁 / 分布式锁避免并发导致不一致]
  • 修改库存明细表库存状态为冻结态
  • 调用第三方TMS的try接口 (他们那边其实类似于WMS的预到货订单状态)

Confrim方法:如果都成功,解除库存冻结并且实际扣减库存,并通知TMS从预发货订单改为发货 (他们来拿)

Cancel方法:如果任一失败则回滚

MQ半事务消息最终一致性

通过MQ半事务消息来实现的方式就更简单了,就是通过分布式下的本地消息表记录调用的第三方TMS的工作,并通过第三方的响应结果来修改本地事务表中事务的状态标记位来记录事务是否成功执行,还是失败。

跨第三方调用分布式事务维护问题

  • 较强的一致性事务要求:针对出库的业务来说,基本是通过本地事务 + TCC事务尽量来保证一致性的实现,其实就是基于Seata的TCC实现,只要OMS通知失败 / 本地事务 (修改订单流转状态、库存主表的锁定库存修改、库存明细表预占改为扣减) 唯一失败都会全部回滚,当然虽然seata解决了常见的TCC事务问题 (幂等、悬挂、空回滚问题)。但实际上TCC还是会出现超时 (cancel都超时调用失败)、不同事务交叉执行对资源造成死锁,也还是有可能会出现数据不一致的。
  • 最终一致性事务:还有一部分是通过MQ半消息事务的方式来实现的,并通过消息补偿机制来实现最终一致性,当然通过MQ和本地消息表的方式来实现的分布式事务往往会因为网络阻塞和超时的原因,会导致事务的一致性被破坏掉
解决

对于TCC类型事务来说:如果事务执行失败其实是直接在页面显示错误的信息给操作人员看的,所以一般来说直接重试就好了。当然如果分布式事务被破坏了的话,处理其实是跟MQ的最终一致性是类似的,也是通过定时任务检查是否出现事务不一致的情况,再通过邮件等方式通知。 (可以通过定时器每隔一段时间去检查Seata数据库的gloabl_table的信息,如果出现有事务重试次数超过我们设定的次数发送邮件警告)

对于MQ的半事务消息来说:因为是依靠在网络异步的方式来实现最终一致性,势必会有可能会出现数据不一致的问题。针对这一类的数据不一致问题,首先项目之中其实是由定时任务来负责对库存记录进行对账的操作检查是否出现数据不一致的问题,每天都检查昨天的改动记录的方式,如果出现不一致,基本都是创建人工校对任务,然后以邮件的方式通知,最后由人工介入进行处理的 (货物昂贵,为了稳健目前采取人工介入的方式保证一致性)


库存对账

其实本质上,WMS毕竟在整个供应链系统之中的定位其实就是一个中间服务,他毕竟不是一套完整的ERP系统,而只是其中一个部分这就导致他的调用和服务的提供其实必然是涉及到分布式事务无法避免的数据不一致的问题,而最终的兜底方案其实就是以对账系统。

而在WMS之中,一般对账系统可以分为两种类型的对账,第一种是对内的对账,第二种则是对外的对账。
对内对账:一般来说就是通过检查过去一天新增的库存记录和入库和出库单的记录是否一致,对应的创建
对账记录存储到数据库中,并生成一份对账报告方便做核对。
对外对账

  1. 主动推送对账:以上游ERP的对账举例,一般是由上游ERP做主动推送将对账信息推送到WMS中指定的MQ中(调接口推送),而由WMS特定的消费者程序负责对账,其实就是将ERP对账内容中的数据与WMS对账记录中的数据做一致性检查,其实本质上就是将ERP的采购收货单和WMS的入库单做一致性检查。
  2. 双向对账:也有一些是双向对账的,也大差不差无非就是将WNS对内对账的结果推送给ERP,相互在通过消费者做对账罢了。

库存预警

实际上在WMS之中,为了避免某个商品库存情况对上游ERP或者WMS资源造成影响的而做的提前预警。
大致上可以分为:

  1. 库存过低 (无法出库)
  2. 库存挤压 (仓库资源被长期占用)
  3. 周转率过低 (仓库资源被长期占用)

配置

在项目之中,具体什么商品需要配置预警其实是由用户通过自定义化配置的方式实现的。WMS中通过设置库存规则表,通过配置不同的参数类型来标记是什么作用的参数。设置规则的类型、规则的作用范围 (范围可能会很大)、预警效果(邮件、通知中心、MQ)等等。

触发

实际上库存预警有两种触发方式。

  1. 通过定时器在晚上分散项目时间的方式来执行定时任务检查指定商品的库存情况。
  2. 是在商品发生出库或者入库等实际发生库存变动的时候,也会触发库存预警。

定时器触发

针对库存过低和库存挤压两种预警的处理其实是类似的都需要分为两种情况:

1.库存预警规则已经设置较长时间的 2. 新增的库存预警 / 商品(属于预警范围)

  • 针对库存预警设置较长时间的商品,实际上是利用 EDI针对上次跑到目前的位置库存明细表的新增库存变动相关记录做检查,本质上就是做时间范围检查。如果低于设定的规则,则针对做通知处理。并将该触发记录保存到表之中,方便明天再针对该商品做检查。
  • 针对刚设库存的预警规则和新增的商品,实际上是通过EDI对库存预警规则表和商品表商品的创建时间进行检查得知,然后再针对具体的商品来检查库存主表的库存数量,如果触发预警则做通知并做记录保存

而针对类似于周转率过低的这种库存预警实际上相当复杂,涉及到的数据量会比较大,一般正常来说是要用ES来进行统计的,但这个服务很贵,没多少客户愿意买,所以我也没咋接触过

出入库触发

对于库存不足和库存过度还有稍微比较特殊的处理,从业务就能看出来,实际上库存不足对于我们来说往往响应是要求比较紧急的,所以在实际做库存预占的时候,库存不足的商品还会做库存预警,通过邮件 / WMS通知中心 / 客户MQ消息做通知处理。


库存盘点

库存盘点流程:盘点计划 -> 盘点任务分配 -> 盘点执行 -> 差异检查处理 -> 盘点结果确认

库存盘点这部分我负责的比较少,涉及的不多,可能理解比较浅薄。
盘点本质上其实是针对系统库存记录和实际仓库存储商品做清点核对嘛,一般来说无非就是检查是否有库存记录不准确、库存可能发生(发霉、损坏)等情况。
(不是WMS物流行业的其实也不太在意这个方面的实现问题,也不打算后续继续补充了)

盘点一般分为两种,第一种是增量式的库存盘点(周期盘点),第两种则是全量式的库存盘点。

增量式库存盘点频率比较高,会根据商品重要程度和最近出现过库存异常的商品或仓库做盘点;
全量式库存盘点频率则相对来说较少,往往是只有在年末的时候才会让库存进行一次完整的库存盘点处理

具体盘点过程:会先根据范围创建盘点计划到盘点主表和盘点任务明细表(根据货架也就是库位细分),然后交由仓库主管等人员来负责具体任务分配,而具体的盘点处理其实是通过一些类似于超市中的手持枪或者手机的方式来实现扫描的,扫描RFID或者条形码的方式来实现扫描盘点。在盘点结束之后,再对不一致产生具体的报表结果,生成盘点结果报告。


个性化需求实现

针对自定义需求的实现方面,就我个人的理解来看主要两种解决方案:

  1. 前端按钮的动态化 + 后台依靠项目配置的参数来做自定义化的实现
    这种方式实现比较简单,而且不对现有项目会造成影响,但代价就是管理起来不太方便,而且代码上看起来也会比较复杂。
    在前端渲染之前,需要先调用后端接口依靠用户的ID等,查询具体的页面权限动态的按钮配置信息,返回后前端再根据这些动态的配置进行加载,并最终显示。
    对于后端来说,其实是通过在业务方法调用时,在进行处理的时候,检查某些系统参数的配置来判断是否应该进行某些处理最终实现自定义的功能流程配置。
    当然这种通过前端按钮的动态配置的方式,会对前端页面打开造成性能影响,一般来说还需要通过使用Redis缓存的方式将具体的用户页面按钮进行缓存,尽可能提高页面打开的性能。
    (共享性Saas数据库之间基本相互隔离,也更加适合这种实现,当然如果复杂程度过高还是推荐使用规则引擎的实现方式)
  2. 复杂系统,可以通过使用规则引擎也就是所谓的状态机来做。引入规则引擎的方式更适合更为复杂的系统做处理。(暂时没有接触过,有点可惜)

非核心业务

新客户接入导致的功能迁移

该部分主要负责的其实主要重构和调整的就是to B端大量的消息同步时候的批量文件消费处理部分,当然还有to C端日常数据的同步处理。说到底其实就是较大的需求新增和改动的时候,连带着旧的功能一起携带迁移到新项目之中的变动逻辑,原理其实很简单。

项目代码重构 + 优化

一般来说常见的重构方式会通过一些常见的设计模式的方式来对代码结构进行调整。我常用的其实主要包括以下方式:通过模板方法、Map + 函数式接口 / 桥接模式 / 责任链模式优化 if-else。

在简历之中我其实提到不少优化和重构的工作,这个其实是因为一方面以前的接口出于时间的原因又或者是当时条件、业务来回改动等历史原因,代码会出现一些不合理的地方,像是一长串的if-else,又或者是对同一类业务的实现的时候,随着业务扩展出现多个重复的业务代码,最可怕的是一旦发生总体的业务改动每一个实际业务实现类都需要做改动,万一改漏了又会出问题。当然大部分这种代码都是已经上线了的,自然是不好改动的。但因为公司刚好要做架构调整,改为微服务实现那自然就会有不少对代码重构优化的机会。我接下来介绍一下最常用的三种重构方式。

对于长if-else的重构

其实一般来说if-else可以分为两种,第一种是简单的if-else长链,但是内部的实现逻辑比较简单。第二种则是内部逻辑很复杂的场景,针对这两种场景我的具体实现其实是有所不同的。

Map<Type,Function>

第一种方式是通过Map(记录类型 - 函数式变成 Function<String, Result>)通过key来区分调用具体的方法是谁,然后再传入参数调用拿到实际的调用结果就好。这样的好处是查看业务逻辑的时候直接看Map就好了,简单明了的同时新增业务,加Entry键对值就行。

策略模式

第二种方式就是常见的策略模式,策略模式从整个结构来说无非就是三个部分,上下文引用切换类、抽象策略类、具体策略实现类。需要新增策略的时候,只需要新增一个策略实现类就好了,增加策略类型也可以通过添加抽象策略类和在上下文策略切换中添加变量的方式来实现。好处就是符合闭合、符合单一功能独立、解耦特性。

责任链模式

在业务处理的时候,有时候是业务往往像流式一样,也像生产线的那种,要前后做一系列的处理,一部分的产品需要做某些步骤,另外一些需要做某些步骤,而这些步骤往往有交集,且都需要做某些流程处理。
在旧代码之中,这些业务往往集中在一个方法或者类之中使用做具体的处理,一旦某个业务具体的操作要发生调整,往往需要从一堆业务处理中找出具体要修改哪一个地方要改动,甚至麻烦起来的时候可能会需要debug来找,面对这种情况的代码的时候,可以通过使用责任链模式来做处理。
这样优化的话,如果新增业务,只需要修改责任链节点前后连接,修改也能快速定位,符合开闭原则、低耦合的特点。

模板模式重构 & EDI重构 + 性能调优

优化(SQL [参考上文] + 程序优化 [业务校验、订单校验等 CompletableFuture.thenAccept / thenApply(不处理异常) / handle / allOf])
重构:
if-else-if 没有嵌套时 -> Map <Type, Function> 然后通过传入Type返回Function调用的方式简化 if-else。
if-else再多重嵌套 -> 使用策略模式 (上下文策略切换、策略抽象类、策略实现类) 实现起来比较方便
长条形业务 责任链模式,一般是用在处理旧系统中的那种很长的代码,而且再去其他的功能中有有不少类似处理的时候。责任链模式简单的来说,其实就是通过声明抽象上层和具体实现,并串联在一起,这样方便后续增加或者去掉业务。并且也可以重用其逻辑处理。
完全相同流程的业务 模板模式,在我简历上那个WMS的开发上,我提到我做过EDI和Kafka的消费实现和EDI优化,并且提升EDI 150性能,这点就是在重构和优化的下实现的。实际上我个人认为模板模式在处理一类类似功能的重构时,非常好用。
具体背景:旧系统向新系统迁移时,做过都知道实际上不可能说一下子迁移过去的,都是先搭建好框架然后服务只有在大改动的时候,才会迁移过去。平时最多一点点的迁移。而EDI的迁移和一部分Kafka消费的服务迁移其实就是发生在新增大需求的情况下的。当时是搞定了一个新的客户,让他们接入使用我们服务。大部分的功能其实都差不多,但是他们提出接入的数据导入不通过页面导入,而是通过FTP服务器上传一种叫做EDI X12的格式,我那边拉取文件消费的方式实现。
并且会涉及到商品、订单等文件导入,算是一个比较大的需求改动,开会的结论就是EDI的B端 和 Api接口C端部分的功能都迁移过去。
C端的迁移很常规,跟原先的旧系统的逻辑差不多,没啥改动。最多也就是看到可以通过修改@KafkaListener的currency或者是使用CompletableFuture多线程优化业务前校验。 (C端客户还是用的JSON)
B端的迁移重构,我就使用到了模板模式来重构、实现。虽然旧系统没有完全相同的处理逻辑,但我还是看了一下以前怎么处理的,都是比较单纯的通过长代码方法来处理。分析了一下不同类型的流程后我发现都是固定的:1. FTP拉文件、2.格式校验 、3.业务校验、4. 批量插入 / 更新 5. 返回消息通知客户。完美符合模板模式的重构特点。具体就是通过定义抽象父类以及流程方法和钩子方法,在具体业务实现类之中相继实现。
B端的迁移优化,其实主要是对业务校验和批量数据处理部分:所谓的业务校验其实就是校验文件中的商品、订单是否已经存在,如果存在会通过一个EDI X12格式的校验文件返回错误的订单和商品信息回去(指定在原先文件中哪一行哪一个元素)。以前的B段EDI也有类似的业务校验,是通过SQL一批批跟业务表检查是否存在实现的 not exist。但是这种方式在应对大量的数据的时候,性能并不太好。而需求分析师BA那边说,客户提供的信息是一个文件大概1w,一批会有七八个文件左右。如果都按照旧系统的方式来实现性能肯定不行,最后我是通过构建临时表的方式来解决的,Mysql的临时表是基于SqlSession级别的嘛,只要断开连接就会被回收。我通过 create temporary table 的方式声明临时表,对关键字段建了组合索引,再将解析出来的订单 / 商品信息与业务表做left join,判断业务表id是否没有匹配来处理的,1w条数据基本在十秒不到的内检查出来。
(当然使用临时表还得注意临时表大小受到两个Mysql配置的限制,一个是max_heap_table_size另一个是tmp_table_size,超过这两者任一一个都会使临时表变成磁盘临时表,在默认值的情况下tmp_table_size是16M,max_heap_table_size是32MB,构造的临时表就几个字段加起来大概一行400个字节。假设整个tmp_table_size都被我这个需求用掉了大概能存储4w2的数据,可能不够。因此实际上,我跟技术经理沟通过后,为了尽可能提高性能,将max_table_size调大到了32M,max_heap_table_size则调整到了64M。基本能满足需要了)
除了业务校验还有最后批量插入 / 更新的部分也有所不同。我这里为了简单直接说批量插入了,更新其实也差不多实际上等同于先删然后在插入罢了。

其实是比较经典的插入问题了,常见的插入有 一条条插入、自己写SQL拼接插入、使用Mybatis-plus的saveBatch或者JDBC的executeBatch来实现。实际上原本的代码就已经在使用saveBatch了,但是我发现实际上跟我们自己拼接SQL插入的速度差距不少,我尝试写了一个简单的测试,1000条数据下saveBatch差不多半分钟,自己拼接SQL只花了2 3秒,后面查资料的时候知道Mybatis也好、JDBC也好为了避免批量插入的时候出现某条数据插入失败而导致全部都插入失败的情况,实际上的插入操作是一条条进行的,只有当所有都插入成功或者出现插入失败的时候才会调用flush将写入实际写入。但对于我目前的业务来说,按照要求只需要以文件作为事务基准进行消费就可以,失败那就后面再重试,并不需要该处理,按照官方介绍我添加了 rewritebatchstatements = true 在JDBC连接的url之中,测试发现性能跟我自己拼接的差不多。当然为了避免对其他的功能造成服务,得独立构造连接池来专门服务类似的功能。
最后就是多线程的优化了,这个比较简单,单纯的将要消费的数据分为多个List,开启多个线程交给项目ThreadPoolExecutors创建线程池执行就好。