您的位置首页 >生活百科 >

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

导读 hello大家好,我是城乡经济网小晟来为大家解答以上问题,splitter用法,Splitter助力解决大集合参数)很多人还不知道,现在让我们一起来看

hello大家好,我是城乡经济网小晟来为大家解答以上问题,splitter用法,Splitter助力解决大集合参数)很多人还不知道,现在让我们一起来看看吧!

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

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

问题可能出现在哪呢?

查看代码,是一个非常简单的 "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 也容易忽略,该怎么从根源上杜绝呢?

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

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

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

以 Spring-boot 项目为例。

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

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

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

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

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

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

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

如果使用的是 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;}

对于复杂的 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());}}

测试代码如下:

@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]

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

splitter 核心流程如下:

核心设计

核心流程包括三个步骤:

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

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类图

涉及的类包括:

含义

AbstractParamSplitter

ParamSpltter 公共父类,用于封装一些通用行为

AbstractFixTypeParamSplitter

固定类型拆分器的父类,从泛型中获取类型信息,并实现 support 方法

AnnBasedParamSplitter

实现带有 @SplitParam 注解的 Param Object 的拆分

SplittableParamSplitter

实现 SplittableParam 子类的拆分

SetParamSplitter

实现对 Set 的拆分

ListParamSplitter

实现对 List 的拆分

InvokeParamsSplitter

实现对 InvokeParams 的拆分

MethodExecutor 接口定义如下:

publicinterfaceMethodExecutor{/***执行函数,并返回结果*@paramFunction待执行的函数*@paramps执行函数所需的参数*@param<P>入参*@param<R>返回值*@return*所有的执行结果*/<P,R>List<R>execute(Function<P,R>function,List<P>ps);}

核心实现包括:

ParamSplitter类图

涉及的类有:

含义

AbstractMethodExecutor

抽象父类,实现通用逻辑

SerialMethodExecutor

串行执行器,所有任务在主线程中串行执行

ParallelMethodExecutor

并行执行器,任务在主线程和线程池中并行执行

ResultMerger 接口定义如下:

publicinterfaceResultMerger<R>{/***对多个执行结果进行合并处理*@paramrs执行结果*@return合并之后的最终结果*/Rmerge(List<R>rs);}

与 ParamSplitter 类似,存在一个 SmartResultMerger 根据类型完成组件装配,其定义如下:

publicinterfaceSmartResultMerger<R>extendsResultMerger<R>{/***是否能支持特定结果的合并*@paramresultType结果类型*@return*/booleansupport(Class<R>resultType);}

核心实现包括:

ParamSplitter类图

涉及的类有:

含义

AbstractResultMerger

公共父类,对通用逻辑进行封装

AbstractFixTypeResultMerger

固定类型 合并器 公共父类,通过泛型获取类型信息,并实现 support 方法

IntResultMerger

对 int 进行合并,将结果进行 sum 处理

LongResultMerger

对 ling 进合并,将结果进行 sum 处理

ListResultMerger

对 List 进行合并

SetResultMerger

对 Set 进行合并

DefaultSplitService 基于以上三个组件,完成整个拆分流程,核心代码如下:

/***请求处理流程如下:<br />*1.对参数P进行拆分<br/>*2.用拆分结果分别调用function获取执行结果<br/>*3.将多个执行结果进行合并,并返回<br/>*@paramfunction执行方法,入参为P,返回值为R*@paramp调用函数入参*@parammaxSize每批次最大数量*@return*/@OverridepublicRsplit(Function<P,R>function,Pp,intmaxSize){Preconditions.checkArgument(function!=null);Preconditions.checkArgument(maxSize>0);//入参为null,直接调用函数if(p==null){returnfunction.apply(p);}//对参数进行拆分List<P>params=this.paramSplitter.split(p,maxSize);//没有拆分结果,直接调用函数if(CollectionUtils.isEmpty(params)){returnfunction.apply(p);}//拆分结果为1,使用拆分直接调用函数if(params.size()==1){returnfunction.apply(params.get(0));}//基于执行器和拆分结果执行函数List<R>results=this.methodExecutor.execute(function,params);//对执行结果进行合并处理Rresult=this.resultMerger.merge(results);returnresult;}

与Spring 集成,核心设计如下:

Spring集成

其中,包括几个核心组件:

由于涉及的组件比较多,为了方便使用,使用 spring-boot 的自动装配机制进行集成,无需关注细节,只需引入对应的 starter 依赖即可。

核心配置类详见 SplitterAutoConfiguration,该配置类将完成:

一般情况下,系统预设功能已经能够满足大多数需求,如有特殊情况,可以对功能进行扩展。

功能扩展主要分两个步骤:

Split 注解定义如下:

@Target(ElementType.METHOD)@Retention(RetentionPolicy.RUNTIME)public@interfaceSplit{intsizePrePartition()default20;inttaskPreThread()default3;StringparamSplitter()default"";Stringexecutor()default"defaultSplitExecutor";StringresultMerger()default"";}

配置含义详见:

配置

含义

sizePrePartition

拆解后,每一个分区最大的元素个数

taskPreThread

每一个线程执行的任务数

paramSplitter

参数拆分器名称(spring bean name),默认通过 smart 组件自动查找

executor

执行器名称(spring bean name),defaultSplitExecutor 并发执行器

resultMerger

结果合并器名称(spring bean name),默认通过 smart 组件自动组装

扩展流程为:

扩展流程为:

扩展流程为:

附上项目地址:https://gitee.com/litao851025/lego

本文就为大家讲解到这里,希望对大家有所帮助。

标签:

免责声明:本文由用户上传,如有侵权请联系删除!