惯性聚合 高效追踪和阅读你感兴趣的博客、新闻、科技资讯
阅读原文 在惯性聚合中打开

推荐订阅源

酷 壳 – CoolShell
酷 壳 – CoolShell
H
Hacker News: Front Page
P
Palo Alto Networks Blog
T
ThreatConnect
Apple Machine Learning Research
Apple Machine Learning Research
博客园_首页
T
True Tiger Recordings
P
Privacy & Cybersecurity Law Blog
B
Blog
IT之家
IT之家
Last Week in AI
Last Week in AI
F
Full Disclosure
Hacker News: Ask HN
Hacker News: Ask HN
C
Comments on: Blog
Microsoft Azure Blog
Microsoft Azure Blog
C
Cybersecurity and Infrastructure Security Agency CISA
Microsoft Security Blog
Microsoft Security Blog
博客园 - 【当耐特】
N
News and Events Feed by Topic
NISL@THU
NISL@THU
腾讯CDC
雷峰网
雷峰网
Security Latest
Security Latest
李成银的技术随笔
M
Microsoft Research Blog - Microsoft Research
L
LangChain Blog
L
Lohrmann on Cybersecurity
cs.CL updates on arXiv.org
cs.CL updates on arXiv.org
C
Check Point Blog
Y
Y Combinator Blog
Recent Announcements
Recent Announcements
博客园 - Franky
N
News | PayPal Newsroom
V
V2EX
A
About on SuperTechFans
The Register - Security
The Register - Security
月光博客
月光博客
奇客Solidot–传递最新科技情报
奇客Solidot–传递最新科技情报
Google Online Security Blog
Google Online Security Blog
MyScale Blog
MyScale Blog
Cisco Talos Blog
Cisco Talos Blog
Vercel News
Vercel News
WordPress大学
WordPress大学
C
Cyber Attacks, Cyber Crime and Cyber Security
The Hacker News
The Hacker News
IntelliJ IDEA : IntelliJ IDEA – the Leading IDE for Professional Development in Java and Kotlin | The JetBrains Blog
IntelliJ IDEA : IntelliJ IDEA – the Leading IDE for Professional Development in Java and Kotlin | The JetBrains Blog
爱范儿
爱范儿
A
Arctic Wolf
L
LINUX DO - 最新话题
freeCodeCamp Programming Tutorials: Python, JavaScript, Git & More

博客园 - Zhang_Xiang

代码是 AI 写的,生产事故谁背锅? AI Agent 走出 Demo 幻觉的唯一解药:Harness Engineering Apache Kafka 的基本概念 Apache Kafka 移除 ZK Proposals webRTC demo Spring Authorization Server(AS)从 Mysql 中读取客户端、用户 Java 对象实现 Serializable 的原因 Spring Data JPA 使用 Spring Authorization Server 实现授权中心 OAuth 2.1 框架 Spring Security dapr 本地环境升级 BuildPack 打包 spring-boot 2.5.4,nacos 作为配置、服务发现中心,Cloud Native Buildpacks 打包镜像,GitLab CI/CD 如何拆分大型单体系统为微服务 高可用 Keycloak,K8s Keycloak 13 自定义用户身份认证流程(User Storage SPI) - Zhang_Xiang OAuth 2.0、OIDC 讲不清楚? Mokito 单元测试与 Spring-Boot 集成测试 关于 JMeter 5.4.1 的一点记录
从 page、page_size 到游标:深入解析C端产品的两种主流分页技术
Zhang_Xiang · 2025-09-05 · via 博客园 - Zhang_Xiang

从 page、page_size 到游标:深入解析C端产品的两种主流分页技术

在开发 C 端应用程序时,无论是社交媒体的信息流、电商的商品列表,还是新闻 App 的文章列表,只要涉及到大量数据的展示,“分页”就是一个不可或缺的功能。它不仅能显著提升页面加载速度,还能优化服务器和数据库的性能。

长久以来,page(页码)和 page_size(每页数量)的组合是我们最熟悉的分页方式。然而,随着“无限滚动”和实时数据流的兴起,还有一种叫做“游标分页”的设计。

本文将带你深入了解这两种分页方式的运作原理、优劣势,并结合 Java 实现代码性能对比真实案例,为你介绍这两种技术选型。

一、传统分页:简单直观的 pagepage_size

这是最经典的分页实现,也被称为“偏移量分页”。核心思想是通过指定要跳过的记录数(offset)和要获取的记录数(limit)来查询数据。

工作原理

客户端请求通常包含两个参数:

  • page:当前请求的页码(例如:3)
  • page_size:每页显示的数量(例如:10)

服务器端在收到请求后,会将其转换为数据库查询中的 LIMITOFFSET

SQL 查询示例:

-- 请求第一页
SELECT * FROM items ORDER BY created_at DESC LIMIT 10 OFFSET 0;

-- 请求第三页
SELECT * FROM items ORDER BY created_at DESC LIMIT 10 OFFSET 20;

Java 代码示例

@GetMapping("/items")
public PageResponseDTO<Item> list(@RequestParam int page, @RequestParam int pageSize) {
    Pageable pageable = PageRequest.of(page - 1, pageSize, Sort.by("createdAt").descending());
    Page<Item> result = itemRepository.findAll(pageable);

    return new PageResponseDTO<>(
            result.getContent(),
            page,
            pageSize,
            result.getTotalElements(),
            null, null,
            result.hasNext()
    );
}

优点

  1. 实现简单:逻辑直观,前后端都容易理解。
  2. 支持跳页:用户能直接跳转到指定页码,适合后台管理类系统。

缺点

  1. 深度分页性能差OFFSET 会丢弃前面大量数据,1000 页以后性能急剧下降。
  2. 数据不一致:数据集频繁更新时,翻页容易出现重复或遗漏。

二、游标设计

游标分页放弃了“页码”的概念,而是用一个“游标”(Cursor)来标记当前位置。常用策略是基于唯一且有序的字段(如 (created_at, id))来生成游标。

工作原理

  1. 初始请求:客户端请求 /items?limit=10
  2. 服务端响应:返回数据 + next_cursor
  3. 后续请求:客户端带上游标 /items?limit=10&cursor=xxxx,服务端从游标位置继续取数据。

SQL 查询示例:

-- 初始请求
SELECT * FROM items ORDER BY created_at DESC, id DESC LIMIT 10;

-- 假设最后一条记录 created_at='2025-09-05 10:00:00', id=1234

-- 下一页请求
SELECT * FROM items
WHERE (created_at < '2025-09-05 10:00:00'
      OR (created_at = '2025-09-05 10:00:00' AND id < 1234))
ORDER BY created_at DESC, id DESC
LIMIT 10;

Java 实现(游标分页)

@GetMapping("/items/cursor")
public PageResponseDTO<Item> cursorPage(
        @RequestParam(required = false) String cursor,
        @RequestParam(defaultValue = "10") int limit) {

    List<Item> items;
    if (cursor == null) {
        items = itemRepository.findTopNByOrderByCreatedAtDescIdDesc(limit);
    } else {
        CursorPayload cp = cursorCodec.decode(cursor)
                .orElseThrow(() -> new IllegalArgumentException("Invalid cursor"));
        items = itemRepository.seekNext(cp.getCreatedAt(), cp.getId(), limit);
    }

    String next = items.isEmpty() ? null :
            cursorCodec.encode(new CursorPayload(
                    items.get(items.size() - 1).getCreatedAt(),
                    items.get(items.size() - 1).getId()));

    return new PageResponseDTO<>(items, null, null, null, next, null, items.size() == limit);
}

注意这里的 cursorCodec,负责将 (createdAt, id) 编码为 Base64 字符串,对前端保持不透明。

优点

  1. 性能稳定:查询性能与页数无关。
  2. 数据一致性好:避免重复和遗漏。
  3. 天然适配无限滚动:非常适合信息流。

缺点

  1. 不能跳页:用户无法跳转到第 100 页。
  2. 难以统计总数:一般只能单独提供 count 接口。
  3. 实现复杂度高:需要额外的游标编码、复合索引。

三、关键坑点与解决方案

  1. 时间戳重复导致丢数据

    • (created_at, id) 作为复合游标。
  2. 反向翻页(聊天记录向上加载)

    • 提供 prevCursor,SQL 使用 > 条件,再反转结果。
  3. 游标安全性

    • Base64 + 签名(HMAC)防篡改。
  4. 是否还有更多数据

    • LIMIT = 请求条数 + 1,如果结果超出则说明有更多。

四、性能对比(MySQL)

第 1 页 很快 很快 第 100 页 需要丢弃前 999 条,SQL 变慢 与第一页几乎一致 数据插入 下一页数据错位 不影响 适合场景 后台表格、搜索结果 信息流、聊天、无限滚动
场景 Offset 分页 游标分页

建议实际做 EXPLAIN,偏移量分页深页通常会出现 Using filesort扫描行数激增,而游标分页能保持稳定。