如何优化基于Mono/Flux返回分页结果的Controller接口性能
可行优化方案
当前/snapshots接口响应慢、内存占用高的核心原因有两点:
Mono.zip要求count查询、分页数据查询两个流全部完成后才会构造返回值,首字节响应必须等待两个查询全部执行完毕,和流式输出的全量接口差距极大collectList()会将当前分页的所有PersonVm对象全部攒在内存中才开始序列化输出,分页量越大内存占用越高,完全没有利用响应式框架的流式响应能力
存在完全满足约束的优化方案,核心是自定义支持响应式字段的分页返回类型,保持两个查询并行执行的同时,实现和全量接口一致的流式输出,对调用方完全透明。
具体实现
1. 自定义流式分页返回类型
通过注解控制JSON序列化字段顺序,将items数组放在最前面,分页元数据放在后面,字段类型直接使用响应流类型,不需要提前攒成实体集合:
@Introspected @JsonPropertyOrder("items", "totalNumberOfItems", "pageable") data class StreamingPage( val items: Flux<PersonVm>, val totalNumberOfItems: Mono<Long>, val pageable: Pageable )
2. 重写接口逻辑
方法入口就触发两个查询并行执行,直接返回流式分页对象,不需要等待任何一个流完成:
@Get("/snapshots") fun snapshots( @Format("yyyy-MM-dd") cutoffDate: LocalDate, pageable: Pageable ): StreamingPage { // 入口立刻发起两个查询,通过cache()保证后续序列化订阅时不会重复发起查询,保持并行执行 val totalCountMono = snapshotDao.getSnapshotCount(cutoffDate).cache() val pageItemsFlux = snapshotDao.getSnapshots( cutoffDate, pageable.size, pageable.offset ).cache() return StreamingPage( items = pageItemsFlux, totalNumberOfItems = totalCountMono, pageable = pageable ) }
方案优势
- 完全保留并行查询逻辑:两个查询在方法入口就同时发往BigQuery,和原有
Mono.zip方案的执行逻辑一致,不会出现串行查询导致的1秒额外耗时 - 首字节时间和全量接口持平:只要分页数据查询吐出第一条
PersonVm记录,响应就会立刻开始输出,不需要等待count查询完成,也不需要等待所有分页数据查完 - 内存占用和全量接口一致:去掉了
collectList()攒集合的逻辑,每查到一条记录就立刻序列化写入响应,内存中最多仅保留当前处理的单条记录 - 对调用方完全无感知:最终输出的JSON结构和原有
CustomPage返回结构完全一致,分页元数据全部在响应体中仅出现一次,不需要修改任何对外接口文档
注意事项
- 确保项目引入了响应式Jackson序列化模块(Spring WebFlux、Micronaut Reactor默认依赖都会自带),框架会自动识别Mono/Flux类型的字段,按流式逻辑序列化:Flux字段会序列化为普通JSON数组,元素逐个输出;Mono字段会等待返回值后序列化为对应类型的普通值
- 必须在方法入口就初始化两个响应流并调用
cache(),避免序列化阶段按字段顺序订阅时,先执行完items查询才触发count查询,退化为串行执行
内容的提问来源于stack exchange,提问作者Roar S.
相关产品推荐
相关产品推荐

