跳转至

后端-数据库-分页offset漂移导致文章跨页重复

问题现象

  • 网页新闻列表第一页和第二页出现相同文章
  • 比如某篇文章同时出现在 page=1 和 page=2 中
  • 数据库本身无重复标题(817 条记录,duplicate titles: 0)

核心问题

三个因素叠加导致分页漂移:

1. 排序键不稳定(主要根因) - 原排序:ORDER BY collected_at DESC, id DESC - collected_at 在每次爬虫插入时刷新,同一个爬虫批次内所有文章共�享相同 collected_at(秒级精度) - 爬虫在用户浏览期间插入新数据时,旧文章的排序位置被�挤偏移

2. COUNT 和 SELECT 非原子(次要根因) - 两�次独立的 await session.execute() 调用,中间可能穿插爬虫写入 - 代码位置:modules/web-crawler/service.py:147-153

total_result = await session.execute(count_stmt)  # 快照 A
# …此时爬虫可能插入新数据…
result = await session.execute(stmt)              # 快照 B

3. Offset 分页天然缺陷(机制根因) - OFFSET 20 的语义是"跳过前 20 行",数据变化后指向不同行 - 例:请求 page=1 返回 ID 1-20;爬虫插入 5 条新记录后排在最前;请求 page=2 (OFFSET 20) 时 ID 16-20 重复出现

解决方法

  1. 排序键改为不可变的 id DESC
  2. id 自增、不可变、单调递增 → offset 永不漂移
  3. id 本身按插入时间自然有序,等效于原来的 collected_at DESC, id DESC

  4. 响应加入 next_cursor 支持 keyset 分页

  5. modules/web-crawler/service.py:170 — 满页且有更多数据时返回 items[-1].id
  6. src/shared/response.py:20PaginatedData 新增 next_cursor: str | None
  7. modules/web-crawler/frontend/api.ts — 类型新增 next_cursor?: string

  8. 涉及文件:

  9. modules/web-crawler/service.py — 核心修复
  10. modules/web-crawler/routes.py:163 — 转发 next_cursor
  11. src/shared/response.py:20 — Schema 扩展
  12. modules/web-crawler/frontend/api.ts — 前端类型

下次防错措施

  • 快速定位:服务器执行 ssh news "cd /www/news && venv/bin/python -c \"..." 检查重复标题
  • 代码规范:分页查询统一使用不可变键(idcreated_at)排序,禁止用可能变化的字段做主排�序键
  • 巡检建议:在 scripts/ 下增加分页一致性检测脚本,随机抽查相邻页是否有 id 重叠