如何将基于CompletableFuture的递归分页查询转为迭代实现以规避栈溢出
如何将基于CompletableFuture的递归分页查询转为迭代实现以规避栈溢出
我完全懂你的顾虑——递归分页的写法虽然清爽,但当数据量极大、分页次数太多时,确实有可能触发栈溢出问题。毕竟每一次递归调用都会在调用栈里留下记录,深度够深就会撑爆栈空间。咱们直接来把这段递归代码改成迭代式的异步实现,既保留CompletableFuture的异步优势,又能彻底避免栈溢出的风险。
首先先理清楚原递归代码的核心逻辑:
- 从第1页开始查询,每次获取一页数据
- 如果返回的结果数量小于每页大小,说明已经到最后一页,直接返回当前结果
- 如果结果数量等于每页大小,就继续查询下一页,然后把当前页和下一页的结果合并,直到触发终止条件
现在我们要把这个递归的调用链,改成迭代式的异步链式调用,核心思路是用累积结果的CompletableFuture加上异步循环控制来代替递归方法调用。
下面是具体的迭代实现代码:
import java.util.ArrayList; import java.util.List; import java.util.concurrent.CompletableFuture; import java.util.concurrent.CompletionStage; import java.util.concurrent.atomic.AtomicLong; import java.util.concurrent.atomic.AtomicReference; public class YourDaoClass { private static final long PAGE_SIZE = 100; // 假设你的每页大小是100 // 原有的get方法,这里保留你的实现 private CompletionStage<List<Entity>> get(List<String> codes, long pageSize, long pageNumber) { // 你的数据库分页查询实现,这里省略 return null; } public CompletionStage<List<Entity>> getAll(List<String> codes) { // 用AtomicReference来保存当前累积结果的CompletableFuture,异步回调中可以修改引用 AtomicReference<CompletableFuture<List<Entity>>> accumulatedResultRef = new AtomicReference<>( CompletableFuture.completedFuture(new ArrayList<>()) ); // 用AtomicLong来保存当前页号,支持异步环境下的修改 AtomicLong currentPage = new AtomicLong(1); // 定义一个处理下一页的逻辑,用Runnable来封装迭代步骤 Runnable fetchNextPage = new Runnable() { @Override public void run() { // 获取当前页的数据 get(codes, PAGE_SIZE, currentPage.get()) .thenCompose(pageResults -> { // 将当前页结果合并到累积结果中 CompletableFuture<List<Entity>> updatedAccumulated = accumulatedResultRef.get() .thenApply(accumulated -> { List<Entity> combined = new ArrayList<>(accumulated); combined.addAll(pageResults); return combined; }); // 更新累积结果的引用 accumulatedResultRef.set(updatedAccumulated); // 页号自增 currentPage.incrementAndGet(); // 判断是否需要继续查询下一页 if (pageResults.size() == PAGE_SIZE) { // 还没到最后一页,继续执行下一次迭代 this.run(); } return updatedAccumulated; }); } }; // 启动第一次分页查询 fetchNextPage.run(); // 返回最终的累积结果Future return accumulatedResultRef.get(); } }
咱们来拆解一下这个迭代实现的关键点:
- 用原子类管理状态:因为异步回调是在不同线程中执行的,所以用
AtomicReference来保存累积结果的CompletableFuture,用AtomicLong来保存当前页号,确保多线程环境下的状态修改是安全的。 - 迭代逻辑封装:把“获取下一页、合并结果、判断是否继续”的逻辑封装在一个
Runnable中,每次处理完一页后,如果需要继续,就调用自身的run方法——这相当于异步环境下的“循环”,但不会在调用栈中累积,彻底规避栈溢出。 - 异步结果累积:每次获取到新页的数据后,通过
thenApply把新页结果合并到累积结果中,然后更新累积结果的引用,确保后续的迭代能基于最新的累积结果继续处理。
另外还有一种更简洁的实现方式,不需要用原子类,而是利用CompletableFuture的链式调用特性来控制流程:
public CompletionStage<List<Entity>> getAll(List<String> codes) { return fetchNextPage(codes, 1L, new ArrayList<>()); } private CompletionStage<List<Entity>> fetchNextPage(List<String> codes, long currentPage, List<Entity> accumulated) { return get(codes, PAGE_SIZE, currentPage) .thenCompose(pageResults -> { accumulated.addAll(pageResults); if (pageResults.size() < PAGE_SIZE) { // 最后一页,返回累积结果 return CompletableFuture.completedFuture(accumulated); } else { // 继续下一页,这里的调用在异步回调中执行,属于异步调用栈,不会累积同步调用栈深度 return fetchNextPage(codes, currentPage + 1, accumulated); } }); }
这里补充一句:其实原递归代码的栈溢出风险可能并没有你想象的那么大,因为thenCompose的回调是在异步线程中执行的,每次递归调用都是在新的线程栈中,不会累积同步调用栈的深度。但如果你还是想彻底避免任何形式的递归调用,第一种基于Runnable的迭代实现是最稳妥的选择。
内容来源于stack exchange
相关产品推荐
相关产品推荐

