、传统分页为什么在 Feed 流会出问题
传统分页:page页码 + size页大小,依靠数组下标偏移取数据。
Feed 流会不断插入新数据(别人发新笔记),列表整体顺序、下标会发生变动。
举例: t1:第一页读到 10、9、8、7、6(5 条) t2:中间插入一条新数据 11,所有旧数据下标全部后移一位 t3:再查第 2 页,按传统 page=2、size=5,会读到 6、5、4、3、2 ✘ 问题:6 这条数据重复读取,出现重复条目。 新消息插入会打乱下标,传统偏移分页不再可靠。
二、滚动分页(游标分页 /lastId 方案)
核心思路:不使用页码,记录上一页最后一条的标识(这里是时间戳 score,叫 max/lastId),下一页从这个位置往后查。
- 第 1 页:
lastId = ∞(从最新时间开始),取 size 条,记录这一页最小的时间戳作为游标 - 中间有新数据插入,新数据只会排在最上面,不会干扰旧游标
- 第 2 页:直接传入上一页拿到的
lastId,只查询小于该时间戳的数据,不会读到已经加载过的旧内容,不会重复
图中例子: 第一页拿到 10‑6,记下 lastId=6; 之后新增 11,排在最顶部; 加载下一页从 lastId=6 向后取,拿到 5‑1,数据不重复。
三、Redis 选型:SortedSet(ZSet)
业务:博主发笔记,推送到每一个粉丝的收件箱
- Key:
feed:用户id,每个粉丝一个独立 zset 收件箱 - member:blog 笔记 id
- score:发布时间戳,天然按时间排序,新消息 score 更大排在前面
✅ ZSet 优势:
- score 时间戳自动完成时间排序;
- 支持
ZREVRANGEBYSCORE逆序按 score 范围查询,完美适配滚动游标分页; - 可以拿到每条数据对应的 score(时间戳),作为下一页的游标参数。
❌ List 为什么不行: List 按下标读取,新数据 LPUSH 插入头部,旧元素下标全部变化,和传统分页一样出现重复 / 漏数据。
四、完整业务流程(探店笔记 Feed 推送)
1)新增笔记(写流程,推模式)
- 把 blog 笔记存入 MySQL 数据库
- 查询该博主的所有粉丝
- 遍历每一位粉丝,向粉丝个人的 ZSet 收件箱,添加这条 blogId,score = 当前时间戳
⚠️推模式缺点:博主粉丝极多的时候,循环推送压力大;优化可以改成拉模式(用户刷的时候自己去查关注列表)
2)查询 Feed 收件箱(读流程,滚动分页)
前端传两个参数:
max:上一页最后一条笔记的时间戳(游标,第一页传无穷大)offset:偏移补偿(相同时间戳多条数据,用来跳过已经读过的条目)
后端逻辑:
- 根据当前登录用户拿到他的 feed 收件箱 key
- ZSet 逆序查询:score ≤ max,limit 偏移量、size 条
- 返回笔记 id 列表,同时返回本页最小的时间戳作为新 max、本页相同时间戳的数量作为 offset,传给前端作为下一次请求参数
- 根据 blogId 批量去 MySQL 查笔记完整内容返回给前端