Java 后端实现 App 列表滚动加载
在移动端开发中,列表滚动加载(Infinite Scroll 或 Pagination)是一种常见的优化用户体验的方式。用户滑动列表时,后端需要高效返回分片数据。面试中提到分页查询和 LIMIT
,却被面试官质疑,可能因为这种方案在某些场景下存在性能瓶颈或不够灵活。本文将探讨如何用 Java 后端实现高效的滚动加载,并优化传统分页。
为什么 LIMIT
不够好?
传统分页使用 SQL 的 LIMIT
和 OFFSET
,如:
sql
SELECT * FROM items ORDER BY id DESC LIMIT 10 OFFSET 20;
- 问题 1:性能下降
OFFSET
越大,数据库需要扫描并丢弃的行数越多,即使最终只返回少量数据,效率低下。 - 问题 2:数据一致性
如果列表数据实时更新(插入或删除),OFFSET
可能导致重复或遗漏数据。 - 面试官期待
可能希望听到更现代的方案,比如基于游标(Cursor-based Pagination)或键集分页(Keyset Pagination),这些方法更适合滚动加载。
优化方案:基于游标的分页
游标分页不依赖 OFFSET
,而是基于某个字段(如 id
或 timestamp
)的最后值来定位下一页数据。优点是性能稳定,且能应对数据动态变化。
实现步骤
-
前端请求参数
cursor
: 上次请求的最后一个记录的标识(如id
或timestamp
)。size
: 每页数据量。
-
后端逻辑
- 根据
cursor
查询大于(或小于)该值的记录。 - 返回数据和新的
cursor
。
- 根据
-
SQL 示例
假设按
id
降序加载:sqlSELECT * FROM items WHERE id < :cursor ORDER BY id DESC LIMIT :size;
Java 代码实现
以下是一个 Spring Boot + MyBatis 的示例:
1. Controller 层
java
@RestController
@RequestMapping("/api")
public class ItemController {
@Autowired
private ItemService itemService;
@GetMapping("/items")
public ResponseEntity<ItemResponse> getItems(
@RequestParam(required = false) Long cursor,
@RequestParam(defaultValue = "10") int size) {
ItemResponse response = itemService.getItems(cursor, size);
return ResponseEntity.ok(response);
}
}
2. Service 层
java
@Service
public class ItemService {
@Autowired
private ItemMapper itemMapper;
public ItemResponse getItems(Long cursor, int size) {
List<Item> items = itemMapper.findItemsByCursor(cursor, size);
Long nextCursor = items.isEmpty() ? null : items.get(items.size() - 1).getId();
return new ItemResponse(items, nextCursor);
}
}
3. Mapper 接口
java
@Mapper
public interface ItemMapper {
@Select("SELECT * FROM items WHERE ${cursor == null ? '1=1' : 'id < #{cursor}'} " +
"ORDER BY id DESC LIMIT #{size}")
List<Item> findItemsByCursor(@Param("cursor") Long cursor, @Param("size") int size);
}
4. Response DTO
java
public class ItemResponse {
private List<Item> items;
private Long nextCursor;
public ItemResponse(List<Item> items, Long nextCursor) {
this.items = items;
this.nextCursor = nextCursor;
}
// Getters and setters
}
数据表结构
sql
CREATE TABLE items (
id BIGINT AUTO_INCREMENT PRIMARY KEY,
title VARCHAR(255),
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
);
工作原理
- 首次请求
前端不传cursor
,后端返回最新 10 条数据(如id
从 100 到 91),nextCursor = 91
。 - 后续请求
前端传cursor=91
,后端返回id < 91
的 10 条数据(如 90 到 81),nextCursor = 81
。 - 结束条件
当返回数据少于size
或为空时,前端停止加载。
优势
- 性能稳定
无需扫描大量偏移行,查询复杂度不随数据量增加而恶化。 - 动态适应
数据插入或删除不影响已加载内容的正确性。 - 简单易扩展
可基于其他字段(如created_at
)实现时间排序。
进一步优化
- 索引优化
为id
或created_at
添加索引,提升查询效率。 - 缓存
对热点数据使用 Redis 缓存,减少数据库压力。 - 异步加载
使用线程池异步查询,提升响应速度。
总结
相比传统的 LIMIT
和 OFFSET
,游标分页更适合 App 的滚动加载场景。它不仅性能更优,还能保证数据一致性。如果面试官对分页查询不满意,不妨展示这种方案,体现对性能和用户体验的深入思考。