Rpc 优化 - 自动分页/分段工具类
目前的问题
目前在 Rpc 的时候会存在以下两个问题:
-
定时任务刷数据的时候,需要全量拉取其他服务的数据,目前的做法是直接通过列表接口拉取
- 目前拉取数据是从读库拉,不会给主库造成压力,但是在拉取数据的时候,如果数据量比较大,目标服务节点需要一次性序列化大量数据,能用,但不优雅
- 随着日后数据量的增长,如果类似的操作不断增多,将有引发 OOM 的风险,不仅定时任务执行失败,还会导致系统不稳定
-
目前的内网网关对 URL 的限制较短,偶尔会出现 Get 请求传参过多,排查后发现是传了一个比较长的 List,整体 URL 较长,导致 Rpc 请求失败
- 由于该情况只是偶现且业务需求繁多,遇到这种问题时直接粗暴地把 Get 请求换成 Post 请求,通过把参数放在 Body 里面解决,不够优雅,等需求量稍微减少后,需要针对这种情况提供一个解决方案
解决方案的思考
-
对于第一个问题,很自然想到的方案就是分页查询,只需要列表数据有序(比如根据 id 排序),就可以很轻松地分页查询,解决方案呼之欲出:
- 弄一个分页工具类,把分页查询的代码进行封装
- 仅仅是普通的分页还不够,还需要提供并发查询的能力,不然查起来会很慢,因为一次请求变成了多次,除了网络开销以外,需要执行的 SQL 数量也将变成原来的 N 倍
-
对于第二个问题
-
临时的解决方案
通过观察 url 发现,传递
List时,目前的传参格式为url?param=1¶m=2,参数名会重复很多遍,但这是 Feign 默认的传参格式,且没有提供全局修改的能力,只能在接口上面通过@CollectionFormat(feign.CollectionFormat.CSV)注解把传参格式修改为url?paran=1,2,3,以此减少 url 长度 -
比较整体的解决方案
在进行 Rpc 查询时,先把
List进行分段,分段以后再根据每个段发送查询请求
-
总结来说,最后的解决方案是编写一个工具类,提供分页/分段的功能,编写业务需求的时候直接使用工具类来进行查询即可
工具类介绍
适用场景
该工具类主要适用于以下三种场景:
-
参数为一个分页查询 DTO,自动分页查询
RpcUtils.listWithAutoPage(new UserQueryDTO(), feignClient::listUser) -
参数为一个 List,自动对 List 进行分段
RpcUtils.listByIdsWithAutoPartition(userIdList, feignClient::listUserById) -
参数为一个 Query 对象,对象里有一个很长的 List,自动对 List 分段
-
如果参数只有这个 List 的话,可以这样:
RpcUtils.listByIdsWithAutoPartition( userIdList, UserQueryDTO::createWithIdList, feignClient::listUser); -
如果参数比较多,可以这样:
RpcUtils.listByIdsWithAutoPartition( userIdList, // 注意, 这里必须的 DTO 不能在外面 new 出来 // 必须保证每次调用这个 Function 都得到一个新对象, 否则会有线程安全问题 (idList) -> UserQueryDTO.builder() .type(1L) .idList(idList) .build(), feignClient::listUser);
-
分页/分段大小与多线程选项
上面只展示了最简单的用法,除了以上的必传参数以外,还支持设置两个可选项:
-
分页大小 pageSize / 分段大小 partitionSize,这两个大小都已经定义了一些常量,比如 PAGE_SIZE_128、PARTITION_SIZE_256 等等,建议直接使用常量
-
是否多线程 isConcurrent
下面的代码中,默认值是走单线程,业务场景对响应时长有要求时可以走多线程
这个是出于对数据库性能的考虑,这个工具类至少要保证 CV 到不同公司、不同项目后可以安全使用,如果数据库性能比较孱弱,默认走多线程可能会数据库打爆
两个特殊场景
-
场景 1
特殊的,你可能会遇到这样的场景,参数为 PageQuery,但是要传一个特别长的参数,暂时还没有 API 能直接解决这个问题,但可以采用曲线救国的方式:
直接把分页的大小设成
Integer.MAX_VALUE,时间复杂度为O(n),但是失去了 PageQuery 本身的意义,变成了一个类似第三种场景的状态 -
场景 2
如果有两个及以上很长的 List,本工具暂时无法解决,因为参数之间的关联关系一般为“与”,
a in [1,2] and b in [3,4]没有办法简单地转换为(a in [1] and b in [3]) + (a in [2] and b in [4])
当然它可以做笛卡尔积来解决这个问题, 但是这个场景目前比较稀缺, 后续有需要再做
上代码
下面的导包隐去了 jdk 自带的一些包,以及 PageQuery、CommonResponse 等类的导入,CV 的到时候自行替换即可
import cn.hutool.core.collection.CollectionUtil;
import cn.hutool.core.collection.ListUtil;
import com.alibaba.fastjson.JSON;
import com.google.common.collect.Lists;
import org.springframework.core.ResolvableType;
public class RpcUtils {
/**
* 分页大小 - 64
*/
public static final int PAGE_SIZE_64 = 64;
/**
* 分页大小 - 128
*/
public static final int PAGE_SIZE_128 = 128;
/**
* 分页大小 - 256
*/
public static final int PAGE_SIZE_256 = 256;
/**
* 分页大小 - 512
*/
public static final int PAGE_SIZE_512 = 512;
/**
* 分页大小 - 1024
*/
public static final int PAGE_SIZE_1024 = 1024;
/**
* 分页大小 - 2048
*/
public static final int PAGE_SIZE_2048 = 2048;
/**
* 分页大小 - 4096
*/
public static final int PAGE_SIZE_4096 = 4096;
/**
* 分页大小 - 8192
*/
public static final int PAGE_SIZE_8192 = 8192;
/**
* 分页大小 - 16384
*/
public static final int PAGE_SIZE_16384 = 16384;
/**
* 默认分页大小
*/
public static final int DEFAULT_PAGE_SIZE = PAGE_SIZE_128;
/**
* 分段大小 - 64
*/
public static final int PARTITION_SIZE_64 = 64;
/**
* 分段大小 - 128
*/
public static final int PARTITION_SIZE_128 = 128;
/**
* 分段大小 - 256
*/
public static final int PARTITION_SIZE_256 = 256;
/**
* 分段大小 - 512
*/
public static final int PARTITION_SIZE_512 = 512;
/**
* 分段大小 - 1024
*/
public static final int PARTITION_SIZE_1024 = 1024;
/**
* 分段大小 - 2048
*/
public static final int PARTITION_SIZE_2048 = 2048;
/**
* 分段大小 - 4096
*/
public static final int PARTITION_SIZE_4096 = 4096;
/**
* 分段大小 - 8192
*/
public static final int PARTITION_SIZE_8192 = 8192;
/**
* 分段大小 - 16384
*/
public static final int PARTITION_SIZE_16204 = 16384;
/**
* 默认分段大小 (内部网关的请求参数限制太小了, 256 都可能会报错, 128 目前还没有报错过)
*/
public static final int DEFAULT_PARTITION_SIZE = PARTITION_SIZE_128;
/**
* 带自动分页的 RPC
*
* @param pageQuery rpc查询条件
* @param fetcher rpc 方法
* @param <T> rpc查询条件的类型
* @param <R> rpc 方法返回值的类型
* @return
*/
public static <T extends PageQuery, R> List<R> listWithAutoPage(T pageQuery, Function<T, CommonResponse<CommonPage<R>>> fetcher) {
return listWithAutoPage(pageQuery, DEFAULT_PAGE_SIZE, false, fetcher);
}
/**
* 带自动分页的 RPC
*
* @param pageQuery rpc查询条件
* @param pageSize 分页大小
* @param fetcher rpc 方法
* @param <T> rpc查询条件的类型
* @param <R> rpc 方法返回值的类型
* @return
*/
public static <T extends PageQuery, R> List<R> listWithAutoPage(T pageQuery, int pageSize, Function<T, CommonResponse<CommonPage<R>>> fetcher) {
return listWithAutoPage(pageQuery, pageSize, false, fetcher);
}
/**
* 带自动分页的 RPC
*
* @param pageQuery rpc查询条件
* @param isConcurrent 是否多线程
* @param fetcher rpc 方法
* @param <T> rpc查询条件的类型
* @param <R> rpc 方法返回值的类型
* @return
*/
public static <T extends PageQuery, R> List<R> listWithAutoPage(T pageQuery, boolean isConcurrent, Function<T, CommonResponse<CommonPage<R>>> fetcher) {
return listWithAutoPage(pageQuery, DEFAULT_PAGE_SIZE, isConcurrent, fetcher);
}
/**
* 带自动分页的 RPC
*
* @param pageQuery rpc查询条件
* @param pageSize 分页大小
* @param isConcurrent 是否多线程
* @param fetcher rpc 方法
* @param <T> rpc查询条件的类型
* @param <R> rpc 方法返回值的类型
* @return
*/
public static <T extends PageQuery, R> List<R> listWithAutoPage(T pageQuery, int pageSize, boolean isConcurrent, Function<T, CommonResponse<CommonPage<R>>> fetcher) {
if (isConcurrent) {
return listWithAutoPageConcurrently(pageQuery, pageSize, fetcher);
}
return listWithAutoPageNotConcurrently(pageQuery, pageSize, fetcher);
}
/**
* 带自动分页的 RPC (单线程)
*
* @param pageQuery rpc查询条件
* @param pageSize rpc 分页大小
* @param fetcher rpc 方法
* @param <T> rpc查询条件的类型
* @param <R> rpc 方法返回值的类型
* @return
*/
public static <T extends PageQuery, R> List<R> listWithAutoPageNotConcurrently(T pageQuery, int pageSize, Function<T, CommonResponse<CommonPage<R>>> fetcher) {
var pageIndex = 1;
pageQuery.setPageSize(pageSize);
var result = Lists.<R>newArrayList();
while (true) {
pageQuery.setPageIndex(pageIndex++);
var list = collectToList(fetcher.apply(pageQuery));
if (CollectionUtil.isEmpty(list)) {
break;
}
result.addAll(list);
}
return result;
}
/**
* 带自动分页的 RPC (多线程)
* 有 BUG, 暂时停用
*
* @param pageQuery rpc查询条件
* @param pageSize rpc 分页大小
* @param fetcher rpc 方法
* @param <T> rpc查询条件的类型
* @param <R> rpc 方法返回值的类型
* @return
*/
public static <T extends PageQuery, R> List<R> listWithAutoPageConcurrently(T pageQuery, int pageSize, Function<T, CommonResponse<CommonPage<R>>> fetcher) {
// 计算页数
int totalCount = queryTotalCount(pageQuery, fetcher);
int totalPageCount = computePageCount(totalCount, pageSize);
// 开始查询
pageQuery.setPageSize(pageSize);
var pageQueryClass = (Class<T>) ResolvableType.forInstance(pageQuery).resolve();
var futureList = Lists.<CompletableFuture<List<R>>>newArrayListWithCapacity(totalCount);
for (int i = 1; i <= totalPageCount; i++) {
// 如果用同一个 pageQuery 对象, 会出现 pageIndex 的线程竞争问题, 导致某些页被多次请求、某些页被漏查, 可以加锁解决, 但这样就跟单线程没差了, 还多了上下文切换的开销
// 其实 BeanUtils.copy() 浅克隆也可以, 只要发送 feign 时使用的 pageQuery 对象不同就可以避免 pageIndex 的线程竞争
final T clonedPageQuery = JSON.parseObject(JSON.toJSONString(pageQuery), pageQueryClass);
clonedPageQuery.setPageIndex(i);
CompletableFuture<List<R>> result = CompletableFuture.supplyAsync(() -> collectToList(fetcher.apply(clonedPageQuery)));
futureList.add(result);
}
return flatCompletableFutureList(futureList, totalCount);
}
/**
* 获取查询条件对应的结果总数
*
* @param pageQuery 查询条件, 里面的下表跟页数会被修改
* @param fetcher
* @param <T>
* @param <R>
* @return
*/
public static <T extends PageQuery, R> int queryTotalCount(T pageQuery, Function<T, CommonResponse<CommonPage<R>>> fetcher) {
pageQuery.setPageIndex(0);
pageQuery.setPageSize(0);
var resp = fetcher.apply(pageQuery).data();
return Long.valueOf(resp.getTotalCount()).intValue();
}
/**
* 计算页数, 结果并不准确, 可能会多一页
*
* @param totalCount
* @param pageSize
* @return
*/
public static int computePageCount(int totalCount, int pageSize) {
return totalCount / pageSize + 1;
}
/**
* 自动把 idList 分段的 RPC
* 适用于 rpc 的参数直接是 idList 的场景
*
* @param idList 用于查询的 id
* @param fetcher rpc 方法
* @param <T> id 的类型
* @param <R> rpc 方法返回值的类型
* @return
*/
public static <T,R> List<R> listByIdsWithAutoPartition(List<T> idList, Function<List<T>, CommonResponse<List<R>>> fetcher) {
return listByIdsWithAutoPartition(idList, DEFAULT_PARTITION_SIZE, false, fetcher);
}
/**
* 自动把 idList 分段的 RPC
* 适用于 rpc 的参数直接是 idList 的场景
*
* @param idList 用于查询的 id
* @param isConcurrent 是否多线程
* @param fetcher rpc 方法
* @param <T> id 的类型
* @param <R> rpc 方法返回值的类型
* @return
*/
public static <T,R> List<R> listByIdsWithAutoPartition(List<T> idList, boolean isConcurrent, Function<List<T>, CommonResponse<List<R>>> fetcher) {
return listByIdsWithAutoPartition(idList, DEFAULT_PARTITION_SIZE, isConcurrent, fetcher);
}
/**
* 自动把 idList 分段的 RPC
* 适用于 rpc 的参数直接是 idList 的场景
*
* @param idList 用于查询的 id
* @param partitionSize 分段的大小
* @param fetcher rpc 方法
* @param <T> id 的类型
* @param <R> rpc 方法返回值的类型
* @return
*/
public static <T,R> List<R> listByIdsWithAutoPartition(List<T> idList, int partitionSize, Function<List<T>, CommonResponse<List<R>>> fetcher) {
return listByIdsWithAutoPartition(idList, partitionSize, false, fetcher);
}
/**
* 自动把 idList 分段的 RPC
* 适用于 rpc 的参数直接是 idList 的场景
*
* @param idList 用于查询的 id
* @param partitionSize 分段的大小
* @param isConcurrent 是否多线程
* @param fetcher rpc 方法
* @param <T> id 的类型
* @param <R> rpc 方法返回值的类型
* @return
*/
public static <T,R> List<R> listByIdsWithAutoPartition(List<T> idList, int partitionSize, boolean isConcurrent, Function<List<T>, CommonResponse<List<R>>> fetcher) {
return listByIdsWithAutoPartition(idList, partitionSize, Function.identity(), isConcurrent, fetcher);
}
/**
* 自动把 idList 分段的 RPC
* 适用于 rpc 的参数并不直接是 idList 的场景, 需要通过 rpcParamWrapper 包装一下
*
* @param idList 用于查询的 id
* @param rpcParamWrapper 包装器, 把 idList 包装为 rpc 方法的参数
* @param fetcher rpc 方法
* @param <T> id 的类型
* @param <W> rpc查询条件的类型
* @param <R> rpc 方法返回值的类型
* @return
*/
public static <T, W, R> List<R> listByIdsWithAutoPartition(List<T> idList, Function<List<T>, W> rpcParamWrapper, Function<W, CommonResponse<List<R>>> fetcher) {
return listByIdsWithAutoPartition(idList, DEFAULT_PARTITION_SIZE, rpcParamWrapper, false, fetcher);
}
/**
* 自动把 idList 分段的 RPC
* 适用于 rpc 的参数并不直接是 idList 的场景, 需要通过 rpcParamWrapper 包装一下
*
* @param idList 用于查询的 id
* @param rpcParamWrapper 包装器, 把 idList 包装为 rpc 方法的参数
* @param isConcurrent 是否多线程
* @param fetcher rpc 方法
* @param <T> id 的类型
* @param <W> rpc查询条件的类型
* @param <R> rpc 方法返回值的类型
* @return
*/
public static <T, W, R> List<R> listByIdsWithAutoPartition(List<T> idList, Function<List<T>, W> rpcParamWrapper,
boolean isConcurrent, Function<W, CommonResponse<List<R>>> fetcher) {
return listByIdsWithAutoPartition(idList, DEFAULT_PARTITION_SIZE, rpcParamWrapper, isConcurrent, fetcher);
}
/**
* 自动把 idList 分段的 RPC
* 适用于 rpc 的参数并不直接是 idList 的场景, 需要通过 rpcParamWrapper 包装一下
*
* @param idList 用于查询的 id
* @param partitionSize 分段大小
* @param rpcParamWrapper 包装器, 把 idList 包装为 rpc 方法的参数
* @param fetcher rpc 方法
* @param <T> id 的类型
* @param <W> rpc查询条件的类型
* @param <R> rpc 方法返回值的类型
* @return
*/
public static <T, W, R> List<R> listByIdsWithAutoPartition(List<T> idList, int partitionSize,
Function<List<T>, W> rpcParamWrapper,
Function<W, CommonResponse<List<R>>> fetcher) {
return listByIdsWithAutoPartition(idList, partitionSize, rpcParamWrapper, false, fetcher);
}
/**
* 自动把 idList 分段的 RPC
* 适用于 rpc 的参数并不直接是 idList 的场景, 需要通过 rpcParamWrapper 包装一下
*
* @param idList 用于查询的 id
* @param partitionSize 分段大小
* @param rpcParamWrapper 包装器, 把 idList 包装为 rpc 方法的参数
* @param isConcurrent 是否多线程
* @param fetcher rpc 方法
* @param <T> id 的类型
* @param <W> rpc查询条件的类型
* @param <R> rpc 方法返回值的类型
* @return
*/
public static <T, W, R> List<R> listByIdsWithAutoPartition(List<T> idList, int partitionSize,
Function<List<T>, W> rpcParamWrapper, boolean isConcurrent,
Function<W, CommonResponse<List<R>>> fetcher) {
var isMoreThanOnePartition = CollectionUtil.size(idList) > partitionSize;
if (isConcurrent && isMoreThanOnePartition) {
return listByIdsWithAutoPartitionConcurrently(idList, partitionSize, rpcParamWrapper, fetcher);
}
return listByIdsWithAutoPartitionNotConcurrently(idList, partitionSize, rpcParamWrapper, fetcher);
}
/**
* 自动把 idList 分段的 RPC (单线程)
*
* @param idList 用于查询的 id
* @param partitionSize 分段的大小
* @param rpcParamWrapper 包装器, 把 idList 包装为 rpc 方法的参数
* @param fetcher rpc 方法
* @param <T> id 的类型
* @param <W> rpc查询条件的类型
* @param <R> rpc 方法返回值的类型
* @return
*/
public static <T, W, R> List<R> listByIdsWithAutoPartitionNotConcurrently(List<T> idList, int partitionSize,
Function<List<T>, W> rpcParamWrapper,
Function<W, CommonResponse<List<R>>> fetcher) {
var partitionIdList = ListUtil.partition(idList, partitionSize);
return partitionIdList.stream()
.flatMap(ids -> {
var result = collectList(fetcher, rpcParamWrapper.apply(ids));
return CollectionUtil.isEmpty(result) ? Stream.empty() : result.stream();
})
.collect(Collectors.toList());
}
/**
* 自动把 idList 分段的 RPC (多线程)
*
* @param idList 用于查询的 id
* @param partitionSize 分段的大小
* @param rpcParamWrapper 包装器, 把 idList 包装为 rpc 方法的参数
* @param fetcher rpc 方法
* @param <T> id 的类型
* @param <W> rpc查询条件的类型
* @param <R> rpc 方法返回值的类型
* @return
*/
public static <T, W, R> List<R> listByIdsWithAutoPartitionConcurrently(List<T> idList, int partitionSize, Function<List<T>, W> rpcParamWrapper, Function<W, CommonResponse<List<R>>> fetcher) {
var estimateResultSize = CollectionUtil.size(idList);
var partitionedIdList = ListUtil.partition(idList, partitionSize);
var futureList = partitionedIdList.stream()
.map(ids -> CompletableFuture.supplyAsync(() -> collectList(fetcher, rpcParamWrapper.apply(ids))))
.collect(Collectors.toList());
return flatCompletableFutureList(futureList, estimateResultSize);
}
/**
* 把 CompletableFuture 的结果聚合起来
* 由于直接使用 steam 的 api 会报 unhandled exceptions , 需要用 for, 所以有了这个方法
*
* @param futureList 异步任务列表
* @param estimateResultSize 最后返回的 List 的预计大小
* @param <R> 返回值的类型
* @return 异步任务列表里面的所有返回值
*/
public static <R> List<R> flatCompletableFutureList(List<CompletableFuture<List<R>>> futureList, Integer estimateResultSize) {
var resultList = Objects.nonNull(estimateResultSize) && estimateResultSize >= 0 ? Lists.<R>newArrayListWithCapacity(estimateResultSize) : Lists.<R>newArrayList();
try {
CompletableFuture.allOf(futureList.toArray(new CompletableFuture[0])).get();
for (var future : futureList) {
resultList.addAll(future.get());
}
} catch (InterruptedException | ExecutionException e) {
e.printStackTrace();
throw new BizException("CompletableFuture 获取结果异常: " + e.getMessage());
}
return resultList;
}
public static <R> List<R> collectToList(CommonResponse<CommonPage<R>> response) {
return Lists.newArrayList(response.data().getList());
}
public static <T, R> List<R> collectList(Function<T, CommonResponse<List<R>>> func, T arg) {
var data = func.apply(arg).data();
return CollectionUtil.isEmpty(data) ? Collections.emptyList() : data;
}
}