有趣生活

当前位置:首页>科技>splitter用法Splitter助力解决大集合参数

splitter用法Splitter助力解决大集合参数

发布时间:2026-07-25阅读(1)

导读1.概览1.1.背景有一次,一个伙伴问我:“MySQL主键查询那么慢吗,需要几秒才返回?”对此我也很好奇,从理论上来讲不大可能,主键查询是最快的查询,没有之....1. 概览1.1. 背景

有一次,一个伙伴问我:“MySQL 主键查询那么慢吗,需要几秒才返回?” 对此我也很好奇,从理论上来讲不大可能,主键查询是最快的查询,没有之一。

带着疑问,查看系统日志,大多数请求非常快,基本都在 1、2 ms 内,个别请求可能超过 500ms,甚至有请求超过 3s,整体响应时间非常不均衡。

问题可能出现在哪呢?

  1. 发生了类型转换,导致全表扫描?
  2. 数据库压力过大,影响全局性能?

查看代码,是一个非常简单的 "select * from t where id in (…)" 语句,其中 id 为 Long 类型,无需进行类型转换。但,稍等 in 了多少,程序中没做限制,直接将参数进行拼接,这可能就是问题所在。

完善日志后,继续观察,果然,in 后的参数可能高达几万,甚至十几万,这就太过分了。随后,对其进行调整,将超限参数进行拆分,提升调用频次,降低入参数量,核心代码如下:

privateIntmaxSize=1000;publicList<Entity>getByIds(List<Long>ids){List<List<Long>>splittedIds=Lists.partition(ids,maxSize);List<Entity>entities=Lists.newArrayListWithCapacity(ids.size());for(List<Long>ids2Use:splittedIds){List<Entity>entities1=this.dao.getByIds(ids2Use);entities.addAll(entities1);}returnentities;}

自此,伙伴们就 get 到了新技能,主动对大的参数进行拆分处理。随后公司制定了相应规范,对数据库参数进行限制,不允许过大参数的存在。

但,好景不长,一处小小的 bug 险些造成线上事故。

具体代码如下:

privateintmaxSize=1000;publicList<Entity>getByIds(List<Long>ids){List<List<Long>>splittedIds=Lists.partition(ids,maxSize);List<Entity>entities=Lists.newArrayListWithCapacity(ids.size());for(List<Long>ids2Use:splittedIds){//在调用方法时,没有使用拆分后的新参数,直接使用拆分前参数//不仅没有解决大参数问题,而且对大参数进行了放大//每遇到一个大参数,内存承压巨大,甚至引起OOMList<Entity>entities1=this.dao.getByIds(ids);entities.addAll(entities1);}returnentities;}

这种case,很难通过正常测试覆盖;由于过于细节,Code Review 也容易忽略,该怎么从根源上杜绝呢?

1.2. 目标

能力声明式,在不 Coding 的情况下,通过在方法上增加声明式注解,使其具备自动拆分的能力。

目标很明确,拒绝编码,只在方法中增加注解,在方法调用时,使其具备自动拆分和合并的能力。

这就是 splitter 的由来,如果你也遇到过相似问题,可以直接使用。

2. 快速入门

以 Spring-boot 项目为例。

2.1. 添加 starter

首先在spring-boot 项目的pom中增加 splitter-starter,坐标如下:

<dependency><groupId>com.geekhalo.lego</groupId><artifactId>lego-starter-splitter</artifactId><version>0.0.1-SNAPSHOT</version></dependency>

2.2. 为方法增加 @Split 注解

splitter 提供多种使用方式,可以根据方法签名进行选择。具体如下:

2.2.1. 单集合拆分

这是最简单的方式,其中 @Split 注解:

  1. sizePrePartition。每个分区的参数数量;
  2. taskPreThread。每个线程执行的任务数;

@Split(sizePrePartition=2,taskPreThread=2)publicList<Long>splitByList(List<Long>params){returnconvert(params);}

2.2.2. 多参数集合拆分

如果存在多个入参,要根据其中一个入参进行拆分,需使用 @SplitParam 对要拆分的参数进行标注。

@Split(sizePrePartition=2,taskPreThread=2)publicList<Long>splitByList(@SplitParamList<Long>params,Longother){Preconditions.checkArgument(other!=null);returnconvert(params);}

2.2.3. 参数对象拆分

如果使用的是 Param Object 模式(使用一个对象对所有入参进行封装),直接在需要拆分的属性上增加 @SplitParam 即可。

拆分方法如下:

@Split(sizePrePartition=2,taskPreThread=2)publicList<Long>splitByParam(AnnBasedInputParamparam){Preconditions.checkArgument(param.getOther()!=null);returnconvert(param.getNumbers());}

AnnBasedInputParam 示例如下:

@Builder@AllArgsConstructor@NoArgsConstructor@DatapublicclassAnnBasedInputParam{@SplitParamprivateList<Long>numbers;privateLongother;}

2.2.4. SplittableParam 参数对象

对于复杂的 ParamObject 模式,splitter 提供了 SplittableParam 进行扩展。

拆分方法如下:

@Split(sizePrePartition=2,taskPreThread=2)publicList<Long>splitByParam(SplittableInputParamparam){Preconditions.checkArgument(param.getOther()!=null);returnconvert(param.getNumbers());}

SplittableParam 定义如下:

publicinterfaceSplittableParam<PextendsSplittableParam<P>>{List<P>split(intmaxSize);}

SplittableInputParam 示例如下:

@Value@BuilderpublicclassSplittableInputParamimplementsSplittableParam<SplittableInputParam>{privatefinalList<Long>numbers;privatefinalLongother;@OverridepublicList<SplittableInputParam>split(intmaxSize){List<List<Long>>partition=Lists.partition(this.numbers,maxSize);returnpartition.stream().map(ns->SplittableInputParam.builder().numbers(ns).other(other).build()).collect(toList());}}

2.3. 运行效果

测试代码如下:

@Test@Timeout(3)publicvoidsplitByList(){List<Long>params=Lists.newArrayList(1L,2L,3L,4L,5L,6L,7L,8L);List<Long>longs=this.splitTestService.splitByList(params);Assertions.assertEquals(8,longs.size());}

运行结果如下:

2022-07-2423:17:23.237INFO13309---[main]c.g.lego.splitter.SplitTestService:Threadmainrunwith[1,2]2022-07-2423:17:23.237INFO13309---[ecutor-Thread-1]c.g.lego.splitter.SplitTestService:ThreadDefault-Split-Executor-Thread-1runwith[5,6]2022-07-2423:17:24.245INFO13309---[main]c.g.lego.splitter.SplitTestService:Threadmainrunwith[3,4]2022-07-2423:17:24.245INFO13309---[ecutor-Thread-1]c.g.lego.splitter.SplitTestService:ThreadDefault-Split-Executor-Thread-1runwith[7,8]

从日志中可以看出,框架不仅仅对参数进行拆分,还是用多线程技术,并行执行任务,大大提升系统的响应时间。

3. 设计3.1. 核心流程

splitter 核心流程如下:

核心设计

核心流程包括三个步骤:

  1. 拆分,对入参进行拆封,将一个大入参拆分为多个小参数
  2. 执行,以拆分后的小参数作为入参,执行业务逻辑,获取执行结果;
  3. 合并,对多个执行结果进行合并,获得最终结果。
3.2. 核心组件

与操作步骤对应,核心组件包括:

  1. ParamSplitter 拆分器,完成入参的拆分;
  2. MethodExecutor 执行器,执行业务逻辑;
  3. Resultmerger 合并器,对结果进行合并;
  4. DefaultSplitService 拆分服务,基于以上三个组件,完成拆分流程;
3.2.1. ParamSplitter

ParamSplitter 接口定义如下:

publicinterfaceParamSplitter<P>{/***将param按照maxSize进行拆分*@paramparam原输入参数*@parammaxSize拆分后,每个分区的最大元素个数*@return*/List<P>split(Pparam,intmaxSize);}

SmartParamSplitter 是 ParamSplitter 的一个重要子类,根据类型完成组件装配,其定义如下:

publicinterfaceSmartParamSplitter<P>extendsParamSplitter<P>{/***是否能支持特定类型*@paramparamType参数类型*@return<br/>*1.true能支持paramType的拆分*2.false不能支持paramType的拆分*/booleansupport(Class<P>paramType);}

系统内置实现如下:

ParamSplitter类图

涉及的类包括:

TAGS标签:  splitter  用法  助力  解决  splitter用法

Copyright © 2024 有趣生活 All Rights Reserve吉ICP备19000289号-5 TXT地图HTML地图XML地图