分页场景下,先是热数据,再是冷数据,这种情况怎么处理(冷热数据分页合并问题)?

1 个回答

  • 加刑天
    明知不可为而为之就是我最大的诚意
    # 总体思路 1. **统一排序键**:用 `(create_time DESC, id DESC)`(或任一**全局唯一+单调**的复合键)做**稳定排序**与去重判定。 2. **各表各自可按同一排序键 seek 分页**:避免深分页的 `OFFSET`。 3. **应用层归并两条有序流**(就像归并排序),合并成一页返回;并携带**下一页游标**。 ## 方案 A:双流归并 + Seek 分页(强烈推荐,性能最稳) ### 1)接口入参/返回 * 入参:`pageSize`,可选的 `cursor`(上一页返回的游标)。 * 返回:`items[]` + `nextCursor`(包含两个子游标:热游标、冷游标)。 ### 2)SQL(MySQL示例,按时间倒序) **首次页(无游标)**:各取多一点(建议 *2×pageSize*,便于一次合并够用) ```sql -- 热 SELECT id, create_time, ... FROM t_hot WHERE create_time BETWEEN :start AND :end -- 有时间条件更好;无则去掉 ORDER BY create_time DESC, id DESC LIMIT :batchSize; -- batchSize = pageSize * 2 -- 冷 SELECT id, create_time, ... FROM t_cold WHERE create_time BETWEEN :start AND :end ORDER BY create_time DESC, id DESC LIMIT :batchSize; ``` **带游标(seek)翻页**:用上一页最后一条的 `(last_ct, last_id)` 作为**严格小于**的边界 ```sql -- 热 SELECT id, create_time, ... FROM t_hot WHERE (create_time < :hot_last_ct) OR (create_time = :hot_last_ct AND id < :hot_last_id) ORDER BY create_time DESC, id DESC LIMIT :batchSize; -- 冷 SELECT id, create_time, ... FROM t_cold WHERE (create_time < :cold_last_ct) OR (create_time = :cold_last_ct AND id < :cold_last_id) ORDER BY create_time DESC, id DESC LIMIT :batchSize; ``` > 说明:热、冷各自维护**独立游标**,互不影响;这样当某一侧先耗尽,不拖累另一侧。 ### 3)应用层归并(伪代码) ```java List<Row> hot = daoHot.query(hotCursor, batchSize); List<Row> cold= daoCold.query(coldCursor, batchSize); List<Row> merged = new ArrayList<>(pageSize); int i=0, j=0; while (merged.size() < pageSize && (i < hot.size() || j < cold.size())) { if (j >= cold.size() || (i < hot.size() && cmp(hot.get(i), cold.get(j)) >= 0)) { merged.add(hot.get(i++)); } else { merged.add(cold.get(j++)); } } Cursor next = buildNextCursor( i < hot.size() ? hot.get(i-1) : hotCursor, // 已消费到的最后一条 j < cold.size()? cold.get(j-1): coldCursor ); return (merged, next); ``` * `cmp` 基于 `(create_time, id)`; * **去重**:如果冷热表可能存在**重复记录**(例如迁移重叠窗口),归并时比较主键去重即可。 * **补量**:若某侧不够,另一侧继续补;当两侧都不足 `pageSize` 时说明到末尾了,`nextCursor=null`。 ### 4)优势 * **无 OFFSET 深分页**,每页都是小范围索引扫描; * 任意比例(先热后冷)自然满足; * 支持时间过滤(强烈建议服务端要求 `start/end`)进一步缩小扫描面。 ## 方案 B:SQL 层 `UNION ALL` + 外层排序(只适合小数据页) ```sql SELECT * FROM ( SELECT id, create_time, ... FROM t_hot WHERE create_time BETWEEN ? AND ? UNION ALL SELECT id, create_time, ... FROM t_cold WHERE create_time BETWEEN ? AND ? ) u ORDER BY u.create_time DESC, u.id DESC LIMIT ?, ?; ``` * **优点**:实现简单,应用层零改造。 * **缺点**:MySQL 对 `UNION ALL` 的**全量排序后再 LIMIT**可能在跨大量数据时非常慢;深分页更糟。 * 只建议用于**时间区间很小**、`pageSize` 很小、且历史数据很少的场景;否则改用方案 A。 ## 方案 C:一次性取“热不够再补冷”(简化版) 业务硬性要求“尽量多展示热数据”: 1. 先从热表按 seek 取 `pageSize`; 2. 若不足,再从冷表按 **同一游标边界**补齐剩余条数; 3. `nextCursor` 由**实际最后一条**决定(可能来自冷侧)。 * **优点**:热表 I/O 最小化; * **缺点**:当热剩余很少时,可能多打一轮冷表查询;整体也算高效。 # 关键细节与坑位 1. **排序稳定性** * 必须保证全局唯一的二级键(如 `id`)参与排序,避免同 `create_time` 下翻页重复或遗漏。 2. **时间交界重叠** * 迁移日可能存在**热冷重叠窗口**(例如最近 90 天边界附近),务必在归并时按主键去重。 3. **总条数/总页数** * 不要在在线接口上做 `COUNT(*)` 贯穿冷热两表(很慢)。 * 可提供**近似总数**(缓存/异步统计)或只提供 “是否还有下一页”。 4. **索引** * 冷热表保持一致的复合索引:`KEY idx_ct_id (create_time DESC, id DESC)`; * 过滤条件中常用的维度(如 `user_id`、`status`)要体现在联合索引前缀里,如: `KEY idx_user_ct_id (user_id, create_time DESC, id DESC)`。 5. **参数与批量** * `batchSize` 建议 `2×pageSize` 起步(经验值 2\~3 倍),减少二次回表/补拉; * 巨页/深翻页:强制走**游标式**(不可用 offset),或限制最大页深;历史浏览推荐**导出任务**而非在线翻页。 6. **SLA 与限流** * 冷查走单独线程池与超时控制; * 将“上月/上季度”常用窗口做**快照缓存**(例如每天凌晨生成),命中率高。 # 一个完整返回示例(API 设计) ```json { "items": [ { "id": 101, "createTime": "2025-08-13T09:01:00Z", ... }, ... ], "nextCursor": { "hot": { "lastCt":"2025-08-12T23:59:10Z", "lastId": 8800123 }, "cold": { "lastCt":"2025-05-01T10:09:03Z", "lastId": 6600456 } }, "hasMore": true } ``` * 客户端下一页把 `nextCursor.hot/cold` 原样带回即可。 ## 一句话总结 **最佳实践**:用**双流 seek 分页 + 应用层归并**。 它避免 `OFFSET` 深分页、优先返回热数据、不足再用冷数据补齐,还能稳定输出“下一页”游标;配合一致索引与时间过滤,性能稳、实现清晰、易扩展到多分片(>2 个时用小顶堆归并即可)。
    发布于 2025-08-11 17:25 •IP属地 青海
    举报
    评论
    收藏