我15年初加入阿里的前半年,观察到当时的交易平台已经无法很好的对业务进行敏捷支撑:人手不够,加班严重;排期时间很长,经常延期;共建机制,协同困难等问题。本文是在当时背景之下,提出中台概念以及构建TMF2.0框架之前,所记录我观察到的问题和一些初步想法。PS:本文的示例代码,与实际代码没有任何关系。

电商业务五花八门,各个部门业务的定位也不一样。有的业务是面向特定“垂直”行业的,比如天猫服装业务、天猫电器业务、天猫汽车业务、飞猪度假业务、阿里通信业务等。也有些业务的定位是为所有的行业提供业务支撑的平台型业务,比如聚划算、导购宝等。在早期,各个业务的处理逻辑是一致的,但随着各自业务发展的需求,业务处理逻辑产生了差异化的需求。

随着业务越做越多,代码也逐渐开始从局部开始腐化,最终系统变得不堪重负、难以为继。

一次代码腐化的演进过程

我们以“减库存策略”为例子,在早期的平台减库存处理逻辑中,减库存策略都是“拍下商品后减库存”。但对于一些库存比较少并且价格比较昂贵的商品,比如空调、冰箱等大家电,如果采用“拍下商品后减库存”这种策略,会导致一些有竞争关系商家的恶拍,即只拍下商品,但不付款。这种情况,会导致卖家电的商家库存中无货可卖,而实际商品还积压在仓库中,造成大量的资金占用、仓库占用等。所以,天猫电器业务,他们就希望能自定义本业务的减库存策略为“付款后减库存”。这时,原先处理逻辑是一致的代码就演变成下面这种形式:

public ReduceTypeEnum getProductInventoryReducePolicy(OrderLine orderLine){
    if (orderLine.getProduct().hasTag(9527) ){ 
        //如果下单的商品是大家电 
        return ReduceTypeEnum.AFTER_PAYMENT;
    }
    return ReduceTypeEnum.BEFORE_PAYMENT;
}

业务的发展是如此的迅猛,上面的这段代码根本就无法稳定下来。当一些虚拟商品出现之后,有些虚拟商品是没有库存限制,不需要买家购买之后进行库存扣减。这时,上面这段代码又变成了这样:

public ReduceTypeEnum getProductInventoryReducePolicy(OrderLine orderLine){

     if ( orderLine.getProduct().isVirtual() ){ //如果是虚拟商品,就不减库存
         return ReduceTypeEnum.NO_REDUCATION;
     }
     if ( orderLine.getProduct().hasTag(9527) ){ //如是大家电商品,是付款减少
         return ReduceTypeEnum.AFTER_PAYMENT;
     }
     return ReduceTypeEnum.BEFORE_PAYMENT;
}

做虚拟商品业务的同学添加完这段逻辑之后,经过验证没问题后就发布上线了。然而,过了几个月后,大家电业务增加了一种“门店自提”的收货方式。对于部分家电,消费者是希望自己能力在门店里挑一台自己满意无瑕疵的商品。对于门店自提的商品,发的也是虚拟的提货码,而不是实物物流。但为了确保收到提货码能在门店中能提到货,这个提货码对应的商品库存依然也是要扣减的。但上面改动之后的代码,对于这种场景就失效了,因为虚拟商品不减库存的逻辑始终在大家电业务前面。大家电的业务开发分析完后,对上面代码又进一步调整如下:

public ReduceTypeEnum getProductInventoryReducePolicy(OrderLine orderLine){

     if ( orderLine.getProduct().isVirtual() ){ //如果是虚拟商品,就不减库存
         if ( !orderLine.getProduct().hasTag(9527) ){ //不是大家电商品,才进去
             return ReduceTypeEnum.NO_REDUCATION;
         }
     }
     if ( orderLine.getProduct().hasTag(9527) ){ //如是大家电商品,是付款减少
         return ReduceTypeEnum.AFTER_PAYMENT;
     }
     return ReduceTypeEnum.BEFORE_PAYMENT;
}

随着电器业务里的商品品类越来越丰富,商品数量越来越多,家电业务减库存策略统一采用“付款减库存”也不能满意需求。家电业务减库存策略变成了“如果商品单价大于5000,是付款减,否则是拍下减”。上面的代码又进一步腐化成下面样子:

public ReduceTypeEnum getProductInventoryReducePolicy(OrderLine orderLine){

     if ( orderLine.getProduct().isVirtual() ){ //如果是虚拟商品,就不减库存
         if ( !orderLine.getProduct().hasTag(9527) ){ //不是大家电商品,才进去
             return ReduceTypeEnum.NO_REDUCATION;
         }
     }
     if ( orderLine.getProduct().hasTag(9527) ){ //如是大家电商品,是付款减少
         if( orderLine.getProduct().getPrice().getCent() > 500000L ) {
             return ReduceTypeEnum.AFTER_PAYMENT;
         }
         return ReduceTypeEnum.BEFORE_PAYMENT;
     }
     return ReduceTypeEnum.BEFORE_PAYMENT;
}

上面这个代码腐化的过程不是一天形成的,他也经过了多年的日积月累。这还是非常简单的一个业务在减库存策略上的定制。而整个阿里巴巴电商业务成百上千,我曾经见过最极端的一个方法竟然长达几千行。里面有各种分支逻辑的判断,代码缩进嵌套层次也非常深。每一个程序员在面对一个新的扣减库存场景时,都小心翼翼的找到一个看似合适的位置,加上自己的if语句,并祈祷千万别影响到其他不相关业务。

引入设计模式,但依然没有银弹

随着定制化的业务逻辑越来越多,有追求的程序员开始考虑如何引入设计模式来解决代码腐化问题。还是以上面业务定制减库存策略为例子。有经验的程序员会针对这个定制点,定义一个SPI接口,如下:

public interface GetCustomInventoryReducePolicySpi {

    boolean filter( ReducePolicySettingReq request );

    ReduceTypeEnum execute( ReducePolicySettingReq request );
}

由于实现了该SPI接口的实现类会有多个,为了准确的执行正确的接口实现类,SPI接口会定义两个方法:

  • filter:用于根据当前上下文请求参数,来判断当前SPI的实现类是否生效
  • execute:当前SPI实现类生效时,平台会调用当前SPI实例的execute方法获取自定义的减库存策略

通过这种方式,我们可以将腐坏代码中每个if语句定义成SPI的实现类,平台负责对这些SPI实现类进行遍历并找到第一个返回值不为空的。如下:

public class VirtualProductReduceInventoryPolicyImpl 
     implements GetCustomInventoryReducePolicySpi {

    public boolean filter( ReducePolicySettingReq request ){
        return request.getOrderLine().getProduct().isVirtual();
    }

    public ReduceTypeEnum execute( ReducePolicySettingReq request ){
        return ReduceTypeEnum.NO_REDUCATION;
    }
}

public class TmallAppliancesReduceInventoryPolicyImpl 
     implements GetCustomInventoryReducePolicySpi {

    public boolean filter( ReducePolicySettingReq request ){
        return request.getOrderLine().getProduct().hasTag(9527);
    }

    public ReduceTypeEnum execute( ReducePolicySettingReq request ){
        OrderLine orderLine = request.getOrderLine();
        if( orderLine.getProduct().getPrice().getCent() > 500000L ) {
             return ReduceTypeEnum.AFTER_PAYMENT;
         }
         return ReduceTypeEnum.BEFORE_PAYMENT;
    }
}

public class InventoryReduceProcessor extends SpiProcessor{

    //简易方式注册一个减库存策略的SPI列表
    private List<GetCustomInventoryReducePolicySpi> reducePolicySpis =
        Lists.newArrayList(
            new TmallAppliancesReduceInventoryPolicyImpl(),
            new VirtualProductReduceInventoryPolicyImpl()
            ......
        );

    public ReduceTypeEnum getProductInventoryReducePolicy(OrderLine orderLine){
        //对减库存SPI的实现类遍历,返回第一个条件成立的SPI实例的值
        return reducePolicySpis.stream().filter( p -> p.filter(request))
             .map( p-> p.execute(p))
             .findFirst()
             .orElse(ReduceTypeEnum.AFTER_PAYMENT);
    }
}

经过这种方式处理之后,平台的核心处理代码消除了大量的if..else… ,大家欢欣鼓舞。事实也证明,当一个SPI接口的实现类不是很多的情况下,这种方式还是非常奏效,也的确能大幅度的提升系统的可扩展性。但电商平台所支撑的行业非常多,最终会导致SPI实现类注册的列表会不断增长膨胀。后续,每当来一个需求涉及到减库存策略逻辑定制时,都需要仔细的分析与评估如何对这个SPI注册树进行修改。如:

  • 是需要对现有的SPI实现做修改,还是需要新注册一个SPI实例?
  • 对于需要新增SPI实例,其注册的顺序放在第几个?
  • 对于新增加的SPI或者对已有的SPI做变动后,对于其他SPI的影响如何评估?

随着SPI实现类的增多,如何在新增SPI实例而又不对已有实现产生影响成为非常巨大的挑战。技术人员需要仔细的翻阅代码,看每个已经注册的SPI实例的filter方法是如何编写的,以确保新增的SPI实例的生效条件不至于太大而影响到其他SPI实例,同时生效条件也不能太小,导致自己的SPI实例始终无法生效。

我们的技术人员非常聪明,他们会利用更加高级的技术,比如规则引擎、远程RPC调用等来写filter方法。这个时候,想通过阅读代码来评估这些SPI实例何时会生效几乎成为不可能。

一个复杂的业务系统中,类似这样的SPI的接口定义会多达近千个,每个SPI接口上都会注册几十个实现类。以这种方式注册的SPI实现类,无论是显性注册还是以配置文件方式动态注册,在运行期平台都是以遍历方式找到并执行匹配到的SPI实现类。这种机制导致了不同业务与业务之间产生了巨大的耦合。任何一次新增或者修改SPI,对于其他业务是否有影响都有着巨大的不确定性。随着业务规模的不断变大,这种SPI注册与管理机制开始失效,并成为阻碍业务快速迭代与发展的关键障碍。

在15年,交易平台已经大量使用SPI机制来应对业务复杂度,本文只讲了SPI带来的问题。具体如何去解,敬请期待后文!