有一次,一个伙伴问我:“MySQL 主键查询那么慢吗,需要几秒才返回?” 对此我也很好奇,从理论上来讲不大可能,主键查询是最快的查询,没有之一。
带着疑问,查看系统日志,大多数请求非常快,基本都在 1、2 ms 内,个别请求可能超过 500ms,甚至有请求超过 3s,整体响应时间非常不均衡。
问题可能出现在哪呢?
查看代码,是一个非常简单的 "select * from t where id in (…)" 语句,其中 id 为 Long 类型,无需进行类型转换。但,稍等 in 了多少,程序中没做限制,直接将参数进行拼接,这可能就是问题所在。
完善日志后,继续观察,果然,in 后的参数可能高达几万,甚至十几万,这就太过分了。随后,对其进行调整,将超限参数进行拆分,提升调用频次,降低入参数量,核心代码如下:
private int maxSize = 1000;public List getByIds(List ids){ List> splittedIds = Lists.partition(ids, maxSize); List entities = Lists.newArrayListWithCapacity(ids.size()); for (List ids2Use : splittedIds){ List entities1 = this.dao.getByIds(ids2Use); entities.addAll(entities1); } return entities;}
自此,伙伴们就 get 到了新技能,主动对大的参数进行拆分处理。随后公司制定了相应规范,对数据库参数进行限制,不允许过大参数的存在。
但,好景不长,一处小小的 bug 险些造成线上事故。
具体代码如下:
private int maxSize = 1000;public List getByIds(List ids){ List> splittedIds = Lists.partition(ids, maxSize); List entities = Lists.newArrayListWithCapacity(ids.size()); for (List ids2Use : splittedIds){ // 在调用方法时,没有使用拆分后的新参数,直接使用拆分前参数 // 不仅没有解决大参数问题,而且对大参数进行了放大 // 每遇到一个大参数,内存承压巨大,甚至引起 OOM List entities1 = this.dao.getByIds(ids); entities.addAll(entities1); } return entities;}
这种case,很难通过正常测试覆盖;由于过于细节,Code Review 也容易忽略,该怎么从根源上杜绝呢?
能力声明式,在不 Coding 的情况下,通过在方法上增加声明式注解,使其具备自动拆分的能力。
目标很明确,拒绝编码,只在方法中增加注解,在方法调用时,使其具备自动拆分和合并的能力。
这就是 splitter 的由来,如果你也遇到过相似问题,可以直接使用。
以 spring-boot 项目为例。
首先在spring-boot 项目的pom中增加 splitter-starter,坐标如下:
com.geekhalo.lego lego-starter-splitter 0.0.1-SNAPSHOT
splitter 提供多种使用方式,可以根据方法签名进行选择。具体如下:
这是最简单的方式,其中 @Split 注解:
@Split(sizePrePartition = 2, taskPreThread = 2)public List splitByList(List params){ return convert(params);}
如果存在多个入参,要根据其中一个入参进行拆分,需使用 @SplitParam 对要拆分的参数进行标注。
@Split(sizePrePartition = 2, taskPreThread = 2)public List splitByList(@SplitParam List params, Long other){ Preconditions.checkArgument(other != null); return convert(params);}
如果使用的是 Param Object 模式(使用一个对象对所有入参进行封装),直接在需要拆分的属性上增加 @SplitParam 即可。
拆分方法如下:
@Split(sizePrePartition = 2, taskPreThread = 2)public List splitByParam(AnnBasedInputParam param){ Preconditions.checkArgument(param.getOther() != null); return convert(param.getNumbers());}
AnnBasedInputParam 示例如下:
@Builder@AllArgsConstructor@NoArgsConstructor@Datapublic class AnnBasedInputParam { @SplitParam private List numbers; private Long other;}
对于复杂的 ParamObject 模式,splitter 提供了 SplittableParam 进行扩展。
拆分方法如下:
@Split(sizePrePartition = 2, taskPreThread = 2)public List splitByParam(SplittableInputParam param){ Preconditions.checkArgument(param.getOther() != null); return convert(param.getNumbers());}
SplittableParam 定义如下:
public interface SplittableParam> { List
split(int maxSize);}
SplittableInputParam 示例如下:
@Value@Builderpublic class SplittableInputParam implements SplittableParam { private final List numbers; private final Long other; @Override public List split(int maxSize) { List> partition = Lists.partition(this.numbers, maxSize); return partition.stream() .map(ns -> SplittableInputParam.builder() .numbers(ns) .other(other) .build() ).collect(toList()); }}
测试代码如下:
@Test@Timeout(3)public void splitByList() { List params = Lists.newArrayList(1L, 2L, 3L, 4L, 5L, 6L, 7L, 8L); List longs = this.splitTestService.splitByList(params); Assertions.assertEquals(8, longs.size());}
运行结果如下:
2022-07-24 23:17:23.237 INFO 13309 --- [ main] c.g.lego.splitter.SplitTestService : Thread main run with [1, 2]2022-07-24 23:17:23.237 INFO 13309 --- [ecutor-Thread-1] c.g.lego.splitter.SplitTestService : Thread Default-Split-Executor-Thread-1 run with [5, 6]2022-07-24 23:17:24.245 INFO 13309 --- [ main] c.g.lego.splitter.SplitTestService : Thread main run with [3, 4]2022-07-24 23:17:24.245 INFO 13309 --- [ecutor-Thread-1] c.g.lego.splitter.SplitTestService : Thread Default-Split-Executor-Thread-1 run with [7, 8]
从日志中可以看出,框架不仅仅对参数进行拆分,还是用多线程技术,并行执行任务,大大提升系统的响应时间。
splitter 核心流程如下:
核心设计
核心流程包括三个步骤:
与操作步骤对应,核心组件包括:
ParamSplitter 接口定义如下:
public interface ParamSplitter { /** * 将 param 按照 maxSize 进行拆分 * @param param 原输入参数 * @param maxSize 拆分后,每个分区的最大元素个数 * @return */ List
split(P param, int maxSize);}
SmartParamSplitter 是 ParamSplitter 的一个重要子类,根据类型完成组件装配,其定义如下:
public interface SmartParamSplitter extends ParamSplitter
{ /** * 是否能支持特定类型 * @param paramType 参数类型 * @return < br/> * 1. true 能支持 paramType 的拆分 * 2. false 不能支持 paramType 的拆分 */ boolean support(Class
paramType);}
系统内置实现如下:
ParamSplitter类图
涉及的类包括:
类 | 含义 |
AbstractParamSplitter | ParamSpltter 公共父类,用于封装一些通用行为 |
AbstractFixTypeParamSplitter | 固定类型拆分器的父类,从泛型中获取类型信息,并实现 support 方法 |
AnnBasedParamSplitter | 实现带有 @SplitParam 注解的 Param Object 的拆分 |
SplittableParamSplitter | 实现 SplittableParam 子类的拆分 |
SetParamSplitter | 实现对 Set 的拆分 |
ListParamSplitter | 实现对 List 的拆分 |
InvokeParamsSplitter | 实现对 InvokeParams 的拆分 |
MethodExecutor 接口定义如下:
public interface MethodExecutor { /** * 执行函数,并返回结果 * @param function 待执行的函数 * @param ps 执行函数所需的参数 * @param 入参 * @param 返回值 * @return * 所有的执行结果 */ List execute(Function function, List
ps);}
核心实现包括:
ParamSplitter类图
涉及的类有:
类 | 含义 |
AbstractMethodExecutor | 抽象父类,实现通用逻辑 |
SerialMethodExecutor | 串行执行器,所有任务在主线程中串行执行 |
ParallelMethodExecutor | 并行执行器,任务在主线程和线程池中并行执行 |
ResultMerger 接口定义如下:
public interface ResultMerger { /** * 对多个执行结果进行合并处理 * @param rs 执行结果 * @return 合并之后的最终结果 */ R merge(List rs);}
与 ParamSplitter 类似,存在一个 SmartResultMerger 根据类型完成组件装配,其定义如下:
public interface SmartResultMerger extends ResultMerger{ /** * 是否能支持特定结果的合并 * @param resultType 结果类型 * @return */ boolean support(Class resultType);}
核心实现包括:
ParamSplitter类图
涉及的类有:
类 | 含义 |
AbstractResultMerger | 公共父类,对通用逻辑进行封装 |
AbstractFixTypeResultMerger | 固定类型 合并器 公共父类,通过泛型获取类型信息,并实现 support 方法 |
IntResultMerger | 对 int 进行合并,将结果进行 sum 处理 |
LongResultMerger | 对 ling 进合并,将结果进行 sum 处理 |
ListResultMerger | 对 List 进行合并 |
SetResultMerger | 对 Set 进行合并 |
DefaultSplitService 基于以上三个组件,完成整个拆分流程,核心代码如下:
/** * 请求处理流程如下:
* 1. 对参数 P 进行拆分
* 2. 用拆分结果分别调用 function 获取执行结果
* 3. 将多个执行结果进行合并,并返回
* @param function 执行方法,入参为 P,返回值为 R * @param p 调用函数入参 * @param maxSize 每批次最大数量 * @return */@Overridepublic R split(Function function, P p, int maxSize) { Preconditions.checkArgument(function != null); Preconditions.checkArgument(maxSize > 0); // 入参为 null,直接调用函数 if (p == null){ return function.apply(p); } // 对参数进行拆分 List
params = this.paramSplitter.split(p, maxSize); //没有拆分结果,直接调用函数 if (CollectionUtils.isEmpty(params)){ return function.apply(p); } // 拆分结果为 1,使用拆分直接调用函数 if (params.size() == 1){ return function.apply(params.get(0)); } // 基于执行器 和 拆分结果 执行函数 List results = this.methodExecutor.execute(function, params); // 对执行结果进行合并处理 R result = this.resultMerger.merge(results); return result;}
与Spring 集成,核心设计如下:
Spring集成
其中,包括几个核心组件:
由于涉及的组件比较多,为了方便使用,使用 spring-boot 的自动装配机制进行集成,无需关注细节,只需引入对应的 starter 依赖即可。
核心配置类详见 SplitterAutoConfiguration,该配置类将完成:
一般情况下,系统预设功能已经能够满足大多数需求,如有特殊情况,可以对功能进行扩展。
功能扩展主要分两个步骤:
Split 注解定义如下:
@Target(ElementType.METHOD)@Retention(RetentionPolicy.RUNTIME)public @interface Split { int sizePrePartition() default 20; int taskPreThread() default 3; String paramSplitter() default ""; String executor() default "defaultSplitExecutor"; String resultMerger() default "";}
配置含义详见:
配置 | 含义 |
sizePrePartition | 拆解后,每一个分区最大的元素个数 |
taskPreThread | 每一个线程执行的任务数 |
paramSplitter | 参数拆分器名称(spring bean name),默认通过 smart 组件自动查找 |
executor | 执行器名称(spring bean name),defaultSplitExecutor 并发执行器 |
resultMerger | 结果合并器名称(spring bean name),默认通过 smart 组件自动组装 |
扩展流程为:
扩展流程为:
扩展流程为:
附上项目地址:https://gitee.com/litao851025/lego
留言与评论(共有 0 条评论) “” |