深分页实战
sql
第一页查询
sql
SELECT *
FROM your_table_name
ORDER BY id DESC
LIMIT 100;
第二页及以后的查询
假设第一页最后一条记录的id是 prev_max_id(例如 9950)。
sql
SELECT *
FROM your_table_name
WHERE id < prev_max_id -- 核心:利用WHERE条件跳过之前的所有记录
ORDER BY id DESC
LIMIT 100;
一,结论
查询到数据,即时处理,不加入集合中,每次循环结束,都会释放这一次循环对象内存
二,代码
代码1
public List<WarehouseSubBO> pageListForO2O(WarehouseSubQuery query, PageParam pageParam) {pageParam.setPage(1);pageParam.setSize(100);//主键ID降序,优先处理新创建的pageParam.setOrderBy("id DESC");WarehouseSubQuery pageQuery = new WarehouseSubQuery();pageQuery.setType(4);List<WarehouseSubBO> all = new ArrayList<>();while (true) {List<WarehouseSubBO> subBOPage = warehouseSubService.pageList(pageQuery, pageParam).getData();if (CollectionUtils.isEmpty(subBOPage)) {break;}all.addAll(subBOPage);pageQuery.setIdLt(subBOPage.get(subBOPage.size() - 1).getId());}return all;}
代码2
private List<WarehouseSubBO> findAllByCursorPagination(WarehouseSubQuery query, Integer pageSize) {List<WarehouseSubBO> allResults = new ArrayList<>();Long lastId = null;int pageCount = 0;final int maxPages = 1000;// 将PageParam提到循环外创建(重要优化)PageParam currentPage = new PageParam();currentPage.setPage(1); // 固定值currentPage.setSize(pageSize); // 固定值currentPage.setOrderBy("id DESC"); // 固定值while (pageCount < maxPages) {if (lastId != null) {query.setIdLt(lastId);}// 直接使用已经配置好的pageParam对象PageResult<WarehouseSubBO> pageResult = warehouseSubService.pageList(query, currentPage);List<WarehouseSubBO> currentPageData = pageResult.getData();if (CollectionUtils.isEmpty(currentPageData)) {break;}allResults.addAll(currentPageData);pageCount++;if (currentPageData.size() < pageSize) {break;}lastId = currentPageData.get(currentPageData.size() - 1).getId();}return allResults;
}
代码3
public void processStreaming() {while (true) {// 创建:currentPage列表 + 100个数据对象List<WarehouseSubBO> currentPage = getDataFromDB();if (currentPage.isEmpty()) break;// 处理数据(不创建其他引用)for (WarehouseSubBO item : currentPage) {saveToFile(item); // 处理完就不需要在内存中了}// 循环结束:currentPage和100个数据对象都可以被GC回收}// 方法结束时没有大量内存占用
}
代码4
/*** 流式处理大量数据(避免内存溢出)*/
public void processO2OWarehouses(WarehouseSubQuery query, Consumer<List<WarehouseSubBO>> processor) {PageParam pageParam = new PageParam();initPageParam(pageParam);WarehouseSubQuery pageQuery = createBaseQuery(query);Long lastId = null;int processedCount = 0;while (true) {if (lastId != null) {pageQuery.setIdLt(lastId);}List<WarehouseSubBO> currentPage = warehouseSubService.pageList(pageQuery, pageParam).getData();if (CollectionUtils.isEmpty(currentPage)) {break;}// 处理当前页数据,而不是保存到内存processor.accept(currentPage);processedCount += currentPage.size();lastId = currentPage.get(currentPage.size() - 1).getId();if (currentPage.size() < pageParam.getSize()) {break;}}log.info("流式处理完成,共处理{}条数据", processedCount);
}
三,疑问
1,内存占用问题
有几个占用内容的地方,被谁引用,什么时候释放。
比如
1>, List<WarehouseSubBO> currentPage ,从数据库查询出来?
2>,把所有数据存到list中 allResults.addAll(currentPage);
2,页码都是1,查询原理
3,if (lastId != null) 作用
if (lastId != null) {pageQuery.setIdLt(lastId);}
4,循环内对象释放内存时机
while (true) { // 1. 查询数据到内存 List<WarehouseSubBO> currentPage = getDataFromDB(); // ↑ 内存占用:currentPage对象 + 数据对象。
currentPage没有被集合引用的情况下。首先请解释是两个对象,在说明是循环每次完成释放,还是全部循环结束释放,还是方法结束释放
5,代码4中,推荐使用流式处理,怎么理解如下代码,怎么处理的
processor.accept(currentPage);
processor.accept(currentPage) 本身不释放内存,它只是传递数据。如何传递的,传给谁了,给出完整代码,上下文关系
6,不用5中流式处理,直接做保存处理,不行吗
可以
四,回答
4,循环内对象释放内存时机
结论:查询到数据,即时处理不加入集合中,每次循环结束,都会释放这一次循环对象内存
您问得非常到位!这里确实涉及到几个不同层次的对象和它们的生命周期。让我详细解释:
对象的层次结构
1. currentPage
变量本身(List对象)
是什么:一个
ArrayList
或其它List实现的对象大小:很小(约几十字节),包含一些元数据(容量、大小等)
2. currentPage
中包含的数据对象(WarehouseSubBO对象)
是什么:实际的业务数据对象
大小:较大(几百字节到几KB每个),包含所有字段数据
java
// 内存中的实际结构: currentPage (ArrayList对象, 很小) │ ├── elementData (Object[]数组, 中等) │ │ │ ├── WarehouseSubBO对象1 (较大) │ ├── WarehouseSubBO对象2 (较大) │ ├── ... │ └── WarehouseSubBO对象100 (较大) │ ├── size (int, 很小) └── modCount (int, 很小)
内存释放时机
情况1:没有其他引用(流式处理理想情况)
java
while (true) {// 1. 创建currentPage列表对象和其中的数据对象List<WarehouseSubBO> currentPage = getDataFromDB();// 2. 处理数据(没有添加到其他集合)processData(currentPage);// 3. 循环结束,currentPage变量超出作用域 } // ← 这里currentPage和其中的数据都可以被GC回收
释放时机:每次循环结束都可以释放!
情况2:被其他集合引用(一次性加载)
java
List<WarehouseSubBO> allResults = new ArrayList<>();while (true) {List<WarehouseSubBO> currentPage = getDataFromDB();// currentPage中的数据被allResults引用!allResults.addAll(currentPage);// currentPage变量超出作用域,但数据还在allResults中 } // ← 只有currentPage列表容器可以被释放,数据对象无法释放!// allResults仍然持有所有数据对象的引用
释放时机:方法结束后才可能释放(如果调用方不再引用)
详细的内存生命周期
图表
代码
具体的对象释放时机表
对象类型 | 创建时机 | 释放时机(无其他引用) | 释放时机(有其他引用) |
---|---|---|---|
currentPage 列表对象 | 每次循环开始 | 每次循环结束 | 每次循环结束 |
数据对象(WarehouseSubBO) | 每次查询数据库时 | 每次循环结束 | 方法结束后(或其他引用消失时) |
allResults 集合对象 | 方法开始时 | 方法结束时 | 方法结束时 |
代码示例对比
示例1:每次循环都可释放(推荐)
java
public void processStreaming() {while (true) {// 创建:currentPage列表 + 100个数据对象List<WarehouseSubBO> currentPage = getDataFromDB();if (currentPage.isEmpty()) break;// 处理数据(不创建其他引用)for (WarehouseSubBO item : currentPage) {saveToFile(item); // 处理完就不需要在内存中了}// 循环结束:currentPage和100个数据对象都可以被GC回收}// 方法结束时没有大量内存占用 }
示例2:内存累积(不推荐)
java
public List<WarehouseSubBO> loadAll() {List<WarehouseSubBO> allResults = new ArrayList<>(); // 创建集合对象while (true) {// 创建:currentPage列表 + 100个数据对象List<WarehouseSubBO> currentPage = getDataFromDB();if (currentPage.isEmpty()) break;// 数据对象被allResults引用!allResults.addAll(currentPage);// 循环结束:只有currentPage列表对象可以被回收// 100个数据对象仍然被allResults引用,无法释放!}return allResults; // 所有数据对象仍在内存中 }
关键总结
两个不同对象:
currentPage
列表对象(容器,很小)数据对象(内容,很大)
释放时机:
无其他引用:每次循环结束都可释放
有其他引用:只有容器可释放,数据要等到引用消失
流式处理优势:避免创建额外的引用,让GC可以及时回收内存
内存溢出风险:来自于数据对象被长期引用,而不是临时的小容器对象
这就是为什么流式处理可以处理海量数据而不会内存溢出的根本原因!