封装复杂度之批量接口

一、背景

在平时项目开发过程中,难免需要作为接口提供方封装批量接口给上游调用;或者作为上游系统调用下游业务或者中间件的批量接口,执行某些操作。
封装复杂度之批量接口

常见的批量操作有很多,比如批量查询内容详情,批量发送提醒;批量插入数据、批量更新、批量发送MQ消息等。

不知道,大家想过没有。

  • 为什么要提供批量接口?
  • 作为批量接口的提供方和批量接口的使用方我们通常需要注意哪些问题?

二、 问题思考

2.1 为什么要提供批量接口?

通常最主要的一个原因是为了性能优化。

通常 IO 操作是性能的主要瓶颈,批量接口可以减少网络 IO 次数,从而达到降低耗时的目的。

2.2 批量接口的提供方我们要注意哪些问题?

【1】健壮性

很多人,尤其是新手,容易直线思考,批量接口嘛,直接传给我一个 List 作为参数,返回结果即可。

(1) 批量限制

如果上游传入集合中元素多,会不会有问题?

上游传入的元素过多,很容易对本系统造成很多压力,而且非常容易超时。

public List<Order> queryOrders(String userId, List<String> orderIds){
  // 省略
}

因此,批量接口通常需要增加分页参数,通常需要对集合长度进行检查。

public List<Order> queryOrders(String userId, List<String> orderIds, PageRequest page){

  // orderIds size 检查
}

FBI Warning:请在函数的注释中或者接口文档中必须显式标注集合长度限制!

(2)参数校验

上游传入的参数合法性也要进行校验,比如例子中 userId 是否有权限查看这些 order ?

public List<Order> queryOrders(String userId, List<String> orderIds, PageRequest page){
  // userId 合法性校验
  // orderIds size 检查
}

再比如传入日期,日期的格式是否正确,是否符合预期? 都是需要考量的事情。

(3)并发校验
有些批量操作不允许并发,要考虑加分布式锁。

(4)失败处理
失败该如何处理,也是一个需要考虑的问题

将失败的对象当做返回值返回给上游? 将失败的部分忽略掉?中间有数据失败,需要回滚?

【2】可拓展性

通常建议将主要参数甚至返回值定义成自定义对象,而不是使用封装类型在函数签名中铺开。

请看下面的案例,如果后续需要新增一些参数,就需要提供新的接口:

public List<Order> queryOrders(String userId, List<String> orderIds, PageRequest page){
  // 省略
}

可以参考以下写法,将参数定义为批量查询对象:

public List<Order> queryOrders(OrderBatchQuery query, PageRequest page){
  // 省略
}

这样如果需要新增参数时,不需要修改函数签名。

对于一些“写”操作,还可以考虑,提供失败处理策略,如失败抛异常、部分失败返回失败列表等。

【3】封装复杂度

通常提供批量接口的同学会理直气壮的认为,设置集合 size 限制,最多再给个 page 参数就可以了。

如果有批量的需求,自己去对集合进行分批,自己对分页进行处理呗!

其实最大的问题是,几乎所有上游都需要对当前自己拿到的整个 list 的所有内容都要进行查询!!每个使用方都要自己处理分批和分页,非常麻烦,气得直跺脚!!!

其实有时候可以多走一步,既能体现出自己的专业度,也能更容易赢得上游的信任和称赞。

可以考虑提供一个自动分批和处理分页的方法(需告知上游虽然可以自动分批,但是如果 size 过大仍然会有因数据量过大导致调用超时,甚至 OOM 的风险)。

还可以提供一个自动对参数接口进行分批执行调用拼接结果的工具类等。

对于带返回值的调用,可以参考下面工具方法的定义:

public static <T, V> List<V> partitionCall2ListAsync(List<T> dataList,
                                                         int size,
                                                         ExecutorService executorService,
                                                     Function<List<T>, List<V>> function) {
  
}
                                                         
                                                     

其中 dataList 即待分批的集合, size 即每一批的数量, executorService 线程池,Function<List, List> function 即单次调用。

可参考为的另外一篇博文:https://blog.csdn.net/w605283073/article/details/101399427

2.3 批量接口的使用方需要主要哪些问题?

【1】长度限制

不管是业务接口还是中间件的批量接口,通常参数中集合都会有 size 限制,一定仔细看函数说明、接口文档,甚至有条件拉下对方源码看看实现方式。

工作这几年,已经见到过身边同时多次因为使用下游提供的批量接口,而下游没有在接口上写 size 限制,导致上游在数据量大时报错,测试阶段通常数据量较小不容易发现该问题。

如果下游没有提供自动分批的批量调用方法,可以自己在本系统的外部依赖模块通过编写一个 XXX对应的 XXXXClient 进行二次封装,避免将复杂度再向上游暴露。

【2】 部分失败如何处理?

要核实下游对部分失败的情况的处理办法,是提供了失败策略,还是一起回滚,我们直接失败或者重试?

三、启发

本文希望通过批量接口的编写和使用,让大家意识到封装复杂性的必要性。

希望大家在设计方案或编写代码时,一定要带着“封装复杂度”的思想,尽量将复杂度封装在更底层的位置。

这也是“迪米特法则”即“最小知道原则”的要求,也是高内聚、低耦合的要求。

创作不易,如果你觉得本文对你有帮助,欢迎点赞、收藏加关注,你的支持和鼓励是我创作的最大动力。
上一篇:CentOS 7.9下JDK 1.8安装和配置手册


下一篇:Java Map按值排序的正确姿势