当前位置: 首页 > news >正文

基于游标(Cursor)的方式来实现滚动分页

我们将使用基于游标(Cursor)的方式来实现滚动分页,这是现代应用(如微博、Twitter)推荐的做法,因为它能有效避免传统分页在数据增删时出现的重复或遗漏问题。

场景假设

我们有一个FeedItem(信息流条目)列表,每条数据都有一个唯一且递增的ID(比如数据库自增ID、雪花算法ID或时间戳)。我们将模拟一个API,客户端每次请求带上最后一条记录的ID作为游标,以获取更早的数据。


1. 定义实体类

// 信息流条目实体
@Data
public class FeedItem {private List<?> list;private Long minTime;private Integer offset;
}// API响应封装类
@Data
@NoArgsConstructor
@AllArgsConstructor
public class Result {private Boolean success;private String errorMsg;private Object data;private Long total;public static Result ok(){return new Result(true, null, null, null);}public static Result ok(Object data){return new Result(true, null, data, null);}public static Result ok(List<?> data, Long total){return new Result(true, null, data, total);}public static Result fail(String errorMsg){return new Result(false, errorMsg, null, null);}
}

2. 实现服务层 (Service)

这是核心逻辑。我们模拟一个数据访问层,假设数据已按ID降序排列(最新的在最前面)。

@Service
public class BlogServiceImpl extends ServiceImpl<BlogMapper, Blog> implements IBlogService {@Resourceprivate IUserService userService;@Resourceprivate StringRedisTemplate stringRedisTemplate;@Overridepublic Result queryBlogOfFollow(Long max, Integer offset) {// 1.获取当前用户Long userId = UserHolder.getUser().getId();// 2.查询收件箱String key = "blog:follows:" + userId;Set<ZSetOperations.TypedTuple<String>> typedTuples = stringRedisTemplate.opsForZSet().reverseRangeByScoreWithScores(key, 0, max, offset, 2);if (typedTuples == null || typedTuples.isEmpty()){return Result.ok();}// 3.解析收件箱:blogId, max(时间戳), offsetList<Long> ids = new ArrayList<>(typedTuples.size());Long minTime = 0L;int offsetFinal = 1;for (ZSetOperations.TypedTuple<String> typedTuple : typedTuples) {String blogId = typedTuple.getValue();ids.add(Long.valueOf(blogId));long time = typedTuple.getScore().longValue();if (minTime == time){offsetFinal++;} else {minTime = time;offsetFinal = 1;}}// 4.查询笔记String idStr = StrUtil.join(",", ids);List<Blog> blogs = query().in("id", ids).last("order by field(id, " + idStr + ")").list();for (Blog blog : blogs) {// 4.1.查询blog有关的用户blog.setIcon(userService.getById(blog.getUserId()).getIcon());blog.setName(userService.getById(blog.getUserId()).getNickName());// 4.2.查询blog是否被点赞isBlogLiked(blog);}// 5.返回FeedItem feedItem = new FeedItem();feedItem.setList(blogs);feedItem.setOffset(offsetFinal);feedItem.setMinTime(minTime);return Result.ok(feedItem);}/*** 判断当前用户是否点赞* @param blog*/private void isBlogLiked(Blog blog) {// 1.获取登录用户Long userId = UserHolder.getUser().getId();if (userId == null) {// 用户未登录,无需查询是否点赞return;}// 2.判断当前登录用户是否已点赞String key = "blog:liked:" + blog.getId();//Boolean isMember = stringRedisTemplate.opsForSet().isMember(key, userId.toString());Double score = stringRedisTemplate.opsForZSet().score(key, userId.toString());blog.setIsLike(score!= null);}

核心逻辑解释:

  • filter(item -> item.getId() < cursor):这是实现滚动分页的灵魂。因为数据是降序排列(100, 99, 98…),我们要获取更早(更旧) 的数据,所以需要取ID小于当前游标的值。
  • 首次请求(cursor = null)直接取最前面的数据。
  • nextCursor 是本次返回列表的最后一条记录的ID。客户端下次请求时带上它,就能接着这个位置继续获取。

3. 实现控制器 (Controller)

import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.RestController;@RestController
@RequestMapping("/blog")
public class BlogController {@Resourceprivate IBlogService blogService;/*** 查询当前用户所关注的用户所发布的博客*/@GetMapping("/of/follow")public Result queryBlogOfFollow(@RequestParam("lastId") Long max, @RequestParam(value = "offset", defaultValue = "0") Integer offset) {return blogService.queryBlogOfFollow(max, offset);}
}

4. 客户端调用示例

第一次请求:
GET /of/follow?&offset=1&lastId=1757685550066

  • 响应:
    在这里插入图片描述

第二次请求(使用第一次返回的 nextCursor):
GET of/follow?&lastId=1757690145555

  • 响应:
    在这里插入图片描述

总结与关键点

  1. 游标选择:必须使用唯一、有序(通常递增) 的字段作为游标,如自增ID、创建时间戳。
  2. 排序方向:数据需要按游标字段降序(DESC) 排列,最新的在最前。
  3. 查询条件:下次查询的条件是 WHERE cursor_field < ?last_cursor? ORDER BY cursor_field DESC LIMIT ?limit?
  4. 优点:即使在第一页和第二页请求之间,有新的数据插入(ID=101),也不会影响你获取第二页的数据,因为你的查询条件是 ID < 98,新插入的ID=101的数据不会出现在你的后续请求中,完美避免了重复和遗漏。
  5. 结束判断:当返回的列表为空,或返回的 nextCursornull,或返回的项目数小于 limit 时,客户端应停止请求,显示“已无更多内容”。

文章转载自:

http://R3XqCyj7.kxnnh.cn
http://1vhyXfv9.kxnnh.cn
http://UN2QjdnK.kxnnh.cn
http://iVJTad1v.kxnnh.cn
http://HR2p3Y4Y.kxnnh.cn
http://gzpyhID1.kxnnh.cn
http://SCJ15ejK.kxnnh.cn
http://he0qp9d9.kxnnh.cn
http://tm7bU9vC.kxnnh.cn
http://zdnxa7QT.kxnnh.cn
http://pTSSm0Ui.kxnnh.cn
http://Fi6vESbw.kxnnh.cn
http://joFosnI8.kxnnh.cn
http://HZxl1IoX.kxnnh.cn
http://58k7mYec.kxnnh.cn
http://6BC6EQkb.kxnnh.cn
http://3PqhftOt.kxnnh.cn
http://PpC5HQvp.kxnnh.cn
http://6nG7IsuM.kxnnh.cn
http://hs7CHF1n.kxnnh.cn
http://j473iM5g.kxnnh.cn
http://OmW4TtJ1.kxnnh.cn
http://MVKLsBwV.kxnnh.cn
http://bbHK7MU0.kxnnh.cn
http://Zol31Y8N.kxnnh.cn
http://FCpHRPgA.kxnnh.cn
http://cm1SadkD.kxnnh.cn
http://MXApJ0OK.kxnnh.cn
http://YJa9AlTl.kxnnh.cn
http://EiZhq7ym.kxnnh.cn
http://www.dtcms.com/a/381301.html

相关文章:

  • 30.线程的互斥与同步(四)
  • 《没有架构图?用 netstat、ss、tcpdump 还原服务连接与数据流向》
  • 仓颉语言编程入门:第一个 Windows 下的仓颉应用程序
  • 台达A2E
  • 【操作系统核心考点】进程调度算法全面总结:高频题型与易错点解析
  • ethercat在线调试工具
  • python base core partment-day07-异常、模块、包(对零基础小白友好)
  • 如何解决pip安装报错ModuleNotFoundError: No module named ‘vaex’问题
  • Acrobat JavaScript 代码中的颜色
  • TCGA单癌肿按单基因高低分组的转录组差异热图分析作图教程
  • SSRF:CVE-2021-40438
  • 传统项目管理与敏捷的核心差异
  • count down 98 days
  • 算法题 Day6---String类(3)
  • 知识模型中优化和模拟决策内容有哪些
  • PRINCE2与PMP项目管理体系对比
  • LINUX中USB驱动架构—设备驱动
  • 数据驱动工业智能决策:从痛点破局到全局优化的技术实践与方法论
  • 射频EVM
  • 21.2 Alpaca指令微调实战:Dolly-15K数据增强让LLaMA-2效果飙升82%
  • 每周资讯 | B站新游《三国:百将牌》首曝;2025年移动游戏市场预计达到1030亿美元
  • VMware网络配置
  • fastapi微服务
  • DNS解析:递归查询与迭代查询详解
  • 中级统计师-统计法规-第五章 统计机构与统计人员
  • API 资产治理:ETag/Cache-Control/分页/排序/投影的“契约基线”
  • V少JS基础班之第八弹:this
  • Class52 双向循环神经网络
  • STM32HAL库_cubeMX
  • Class54 编码器-解码器