滚动分页查询实战示例

场景:朋友圈动态流

假设我们有一个朋友圈应用,用户关注了3个好友:小王、小李、小张。

1. 初始数据状态

时间线数据(按发布时间降序):

博客ID 作者 发布时间(时间戳) 内容
1005 小王 1672617600000 (2023-01-02 00:00:00) "新年快乐!"
1004 小李 1672614000000 (2023-01-01 23:00:00) "跨年夜聚餐"
1003 小张 1672610400000 (2023-01-01 22:00:00) "2022再见"
1002 小王 1672606800000 (2023-01-01 21:00:00) "看跨年晚会"
1001 小李 1672603200000 (2023-01-01 20:00:00) "在家做饭"
1000 小张 1672599600000 (2023-01-01 19:00:00) "整理房间"

Redis中的数据结构:

java 复制代码
# 用户1000(当前用户)的关注feed流
ZADD feed:1000 1672617600000 1005
ZADD feed:1000 1672614000000 1004
ZADD feed:1000 1672610400000 1003
ZADD feed:1000 1672606800000 1002
ZADD feed:1000 1672603200000 1001
ZADD feed:1000 1672599600000 1000

2. 客户端请求流程

第一次请求(初始加载)
java 复制代码
http

GET /of/follow?lastId=9999999999999&offset=0
复制代码

参数说明:

  • lastId=9999999999999:很大的时间戳,表示从最新开始查

  • offset=0:不跳过任何数据

服务端处理流程:

java 复制代码
// Redis查询
reverseRangeByScoreWithScores("feed:1000", 0, 9999999999999, 0, 3)

// 返回结果(按时间戳降序):
[
  {value: "1005", score: 1672617600000},  // 小王
  {value: "1004", score: 1672614000000},  // 小李
  {value: "1003", score: 1672610400000}   // 小张
]

// 解析过程:
List<Long> ids = new ArrayList<>();
long minTime = 0;
int os = 1;

// 遍历第1个元素:1005
time = 1672617600000, minTime=0 (初始)
time != minTime → minTime=1672617600000, os=1
ids = [1005]

// 遍历第2个元素:1004
time = 1672614000000, minTime=1672617600000
time != minTime → minTime=1672614000000, os=1
ids = [1005, 1004]

// 遍历第3个元素:1003
time = 1672610400000, minTime=1672614000000
time != minTime → minTime=1672610400000, os=1
ids = [1005, 1004, 1003]

// 查询数据库
SELECT * FROM blog WHERE id IN (1005,1004,1003) 
ORDER BY FIELD(id, 1005, 1004, 1003)

返回给客户端

第二次请求(滚动加载)

客户端使用第一次返回的 minTimeoffset

java 复制代码
http

GET /of/follow?lastId=1672610400000&offset=1

服务端处理流程:

java 复制代码
// Redis查询:查找时间戳 ≤ 1672610400000 的数据
// offset=1:跳过时间戳等于1672610400000的第1条数据(即博客1003)
reverseRangeByScoreWithScores("feed:1000", 0, 1672610400000, 1, 3)

// 返回结果:
[
  {value: "1002", score: 1672606800000},  // 时间戳小于1672610400000
  {value: "1001", score: 1672603200000},
  {value: "1000", score: 1672599600000}
]

// 解析过程:
List<Long> ids = new ArrayList<>();
long minTime = 0;
int os = 1;

// 遍历第1个元素:1002
time = 1672606800000, minTime=0
time != minTime → minTime=1672606800000, os=1
ids = [1002]

// 遍历第2个元素:1001
time = 1672603200000, minTime=1672606800000
time != minTime → minTime=1672603200000, os=1
ids = [1002, 1001]

// 遍历第3个元素:1000
time = 1672599600000, minTime=1672603200000
time != minTime → minTime=1672599600000, os=1
ids = [1002, 1001, 1000]

返回给客户端:

javascript 复制代码
Json
{
  "code": 200,
  "data": {
    "list": [
      {
        "id": 1002,
        "content": "看跨年晚会",
        "userId": 1001,
        "createTime": 1672606800000,
        "name": "小王",
        "icon": "avatar1.jpg"
      },
      {
        "id": 1001,
        "content": "在家做饭",
        "userId": 1002,
        "createTime": 1672603200000,
        "name": "小李",
        "icon": "avatar2.jpg"
      },
      {
        "id": 1000,
        "content": "整理房间",
        "userId": 1003,
        "createTime": 1672599600000,
        "name": "小张",
        "icon": "avatar3.jpg"
      }
    ],
    "minTime": 1672599600000,
    "offset": 1
  }
}
第三次请求(没有更多数据)
java 复制代码
http

GET /of/follow?lastId=1672599600000&offset=1

服务端处理

返回给客户端:

java 复制代码
json
{
  "code": 200,
  "data": {
    "list": [],
    "minTime": 0,
    "offset": 0
  }
}

3. 处理相同时间戳的情况

假设有新数据插入(多人在同一秒发朋友圈):

复制代码
# 新增数据(同一秒发布)
ZADD feed:1000 1672621200000 1006  # 小赵 - 23:00:00
ZADD feed:1000 1672621200000 1007  # 小钱 - 23:00:00
ZADD feed:1000 1672621200000 1008  # 小孙 - 23:00:00
ZADD feed:1000 1672621200001 1009  # 小赵 - 23:00:01(不同秒)

第一次查询(每页3条):

java 复制代码
// Redis返回:
[
  {value: "1009", score: 1672621200001},
  {value: "1008", score: 1672621200000},
  {value: "1007", score: 1672621200000}
  // 注意:1006还没被取到,因为同一秒有3条,这里只取了2条
]

// 解析:
// 1009: time=1672621200001, minTime=0 → minTime=1672621200001, os=1
// 1008: time=1672621200000, minTime=1672621200001 → minTime=1672621200000, os=1
// 1007: time=1672621200000, minTime=1672621200000 → os=2 (相同时间戳)

// 返回给客户端:
{
  "list": [博客1009, 博客1008, 博客1007],
  "minTime": 1672621200000,
  "offset": 2  // 重要:在1672621200000这个时间点上,已经取了2条
}

第二次查询:

java 复制代码
http

GET /of/follow?lastId=1672621200000&offset=2

含义:跳过时间戳等于1672621200000的前2条数据(1008和1007)

java 复制代码
// Redis查询结果:
[
  {value: "1006", score: 1672621200000},  // 这是同一秒的第3条
  {value: "1005", score: 1672617600000}
]

// 解析:
// 1006: time=1672621200000, minTime=0 → minTime=1672621200000, os=1
// 1005: time=1672617600000, minTime=1672621200000 → minTime=1672617600000, os=1

// 返回:
{
  "list": [博客1006, 博客1005],
  "minTime": 1672617600000,
  "offset": 1
}

4. 前端代码实现

javascript 复制代码
class FeedLoader {
  constructor() {
    this.minTime = Date.now();  // 初始化为当前时间(极大值)
    this.offset = 0;
    this.hasMore = true;
    this.loading = false;
  }

  // 加载动态
  async loadFeed() {
    if (this.loading || !this.hasMore) return;
    
    this.loading = true;
    try {
      const response = await fetch(
        `/of/follow?lastId=${this.minTime}&offset=${this.offset}`
      );
      const result = await response.json();
      
      if (result.code === 200) {
        const data = result.data;
        
        // 渲染动态列表
        this.renderBlogs(data.list);
        
        // 更新游标
        if (data.list.length > 0) {
          this.minTime = data.minTime;
          this.offset = data.offset;
        } else {
          this.hasMore = false;  // 没有更多数据
        }
      }
    } catch (error) {
      console.error('加载失败:', error);
    } finally {
      this.loading = false;
    }
  }

  // 监听滚动事件
  initScrollListener() {
    window.addEventListener('scroll', () => {
      const scrollTop = document.documentElement.scrollTop;
      const windowHeight = window.innerHeight;
      const scrollHeight = document.documentElement.scrollHeight;
      
      // 滚动到底部时加载更多
      if (scrollTop + windowHeight >= scrollHeight - 100) {
        this.loadFeed();
      }
    });
  }

  renderBlogs(blogs) {
    const container = document.getElementById('feed-container');
    blogs.forEach(blog => {
      const blogElement = this.createBlogElement(blog);
      container.appendChild(blogElement);
    });
  }

  createBlogElement(blog) {
    // 创建博客DOM元素
    return `
      <div class="blog-item">
        <img src="${blog.icon}" class="avatar">
        <div class="content">
          <h4>${blog.name}</h4>
          <p>${blog.content}</p>
          <span class="time">${this.formatTime(blog.createTime)}</span>
        </div>
      </div>
    `;
  }
}

// 使用示例
const loader = new FeedLoader();
loader.initScrollListener();
loader.loadFeed();  // 首次加载

5. 新数据发布场景

假设在用户浏览过程中,有新动态发布:

原始数据状态:

java 复制代码
时间线:1005(23:00) > 1004(22:00) > 1003(21:00) > ...
用户已看到:1005, 1004, 1003

新动态发布:

java 复制代码
# 小李在22:30发布新动态(时间戳介于1004和1005之间)
ZADD feed:1000 1672615800000 1010

新的时间线顺序:

java 复制代码
1005(23:00) > 1010(22:30) > 1004(22:00) > 1003(21:00) > ...

用户的浏览体验:

  1. 用户已看完第一页:1005, 1004, 1003

  2. 用户继续滚动,加载第二页:1002, 1001, 1000

  3. 新发布的1010不会影响已浏览的内容,因为它插入在1004之前

  4. 用户下次刷新时,会从最新开始重新加载,看到新内容

关键理解点:

  1. 为什么用时间戳作为score?

    • 时间戳天然有序,支持范围查询

    • 可以精确到毫秒,基本不会重复

  2. offset的作用是什么?

    • 处理同一时间戳的多条数据

    • 记录在同一时间点已经取了多少条

  3. 与传统分页的区别:

    java 复制代码
    // 传统分页(有问题)
    SELECT * FROM blog ORDER BY create_time DESC LIMIT 10 OFFSET 10
    // 如果第1-10条之间插入了新数据,第2页会重复第1页的最后一条
    
    // 滚动分页(解决)
    SELECT * FROM blog WHERE create_time < ? ORDER BY create_time DESC LIMIT 10
    // 基于最后一条的时间戳查询,不受新数据插入影响
  4. 适用场景:

    • 朋友圈、微博等动态流

    • 聊天记录翻看

    • 任何需要"不断加载更多"且数据实时更新的场景

通过这个完整示例,你应该能清楚理解滚动分页的工作原理和实际应用了!

相关推荐
咩咩啃树皮10 小时前
第40篇:Vue3组件化开发精讲——组件拆分、复用、父子通信、工程化架构
java·前端·架构
鱟鲥鳚10 小时前
Spring Boot 集成 LangChain4j:从模型调用到 Tool Calling(Demo版)
java·spring boot
大模型码小白12 小时前
【Python零基础教程】继承、多态与魔法函数:面向对象编程三大核心特性详解
java·大数据·开发语言·人工智能·python·ai编程
腾渊信息科技公司12 小时前
Spring Boot对接MES实战:视觉检测数据自动同步方案
java·人工智能·spring boot·后端·计算机视觉·ai·软件需求
爱笑的源码基地13 小时前
高并发 Redis 缓存门诊HIS系统源码,含财务统计药房进销存
java·程序·门诊系统·诊所系统·云诊所源码
wuqingshun31415913 小时前
TCP超时重传机制是为了解决什么问题?
java
二宝哥14 小时前
CentOS 7.9 系统下 Redis 5.0.4 单机安装与源码编译安装指南
redis·centos
莫逸风16 小时前
【AgentScope 2.0】 0. 学习指南
java·llm·agent·agentscope
z1234567898616 小时前
2026最新两款AI编程工具深度对比实测
java·数据库·ai编程
yaoxin52112317 小时前
470. Java 反射 - Member 接口与 AccessFlag
java·开发语言·python