基于 Redis 实现社交 Feed 流:收件箱模式 + 时间线滚动查询
本文基于Redis ZSet实现社交平台关注 Feed 流功能采用收件箱模式推模式 完成博客发布时的粉丝消息推送结合时间戳 偏移量实现无感知滚动分页查询解决传统分页卡顿、数据重复 / 丢失问题。附带完整业务代码、核心逻辑解析、Redis 命令详解。一、项目背景与 Feed 流方案选型1.1 什么是 Feed 流1.2 Feed 流主流方案对比拉模式读扩散用户查询时去拉取关注者的内容 → 优点存储小缺点查询慢、高并发卡顿推模式写扩散 / 收件箱模式发布内容时推送给所有粉丝的收件箱 → 优点查询极快缺点写入压力大推拉结合大 V 用拉模式普通用户用推模式本项目选型推模式收件箱模式适合中小型社交项目实现简单、查询性能优异。二、Redis 收件箱模式核心原理2.1 核心思想每个用户拥有一个独立 Redis 收件箱Key用户发布博客 → 遍历所有粉丝 → 将博客 ID 时间戳存入粉丝的 Redis 收件箱用户查看关注流 → 直接从自己的 Redis 收件箱查询数据无需查询数据库2.2 Redis 数据结构选型选用ZSet有序集合Keyfeed:用户ID每个用户独立收件箱Value博客 IDScore发布时间戳实现按时间倒序排序2.3 滚动分页核心逻辑解决传统limit offset分页的痛点新数据插入导致分页重复 / 丢失大数据量 offset 过大性能差滚动分页规则max上一页最后一条数据的时间戳offset与上一页最后一条时间戳相同的元素个数去重每次查询固定条数实现无感知下拉加载三、核心技术栈与数据结构3.1 技术栈SpringBootMyBatis-PlusRedisZSetStringRedisTemplate线程本地变量UserHolder存储登录用户3.2 Redis Key 设计// Feed流收件箱Key前缀 private static final String FEED_KEY feed:; // 完整Keyfeed:10011001为用户ID四、功能实现一发布博客 → 推送到粉丝收件箱4.1 业务流程获取当前登录用户保存博客到数据库查询当前用户的所有粉丝遍历粉丝将博客 ID 时间戳存入粉丝的 Redis 收件箱4.2 完整代码Override public Result saveBlog(Blog blog) { //获取当前登录用户 UserDTO user UserHolder.getUser(); blog.setUserId(user.getId()); //保存探店博文 boolean isSuccess save(blog); if (!isSuccess) { return Result.fail(新增笔记失败); } //查询所有粉丝 ListFollow follows followService.query().eq(follow_user_id, user.getId()).list(); //遍历粉丝列表发送通知 for (Follow follow : follows) { //发送通知 Long userId follow.getUserId(); String key FEED_KEY userId; //TODO ZSet排序规则按时间戳倒序 stringRedisTemplate.opsForZSet().add(key, blog.getId().toString(), System.currentTimeMillis()); } System.out.println(新增笔记成功); return Result.ok(blog.getId()); }4.3 代码核心解析粉丝查询follow_user_id是被关注者 ID查询所有关注当前用户的粉丝Redis 推送ZSet.add以时间戳为 Score天然支持时间倒序推模式核心写时多一步操作读时极致性能五、功能实现二关注 Feed 流滚动分页查询5.1 业务流程5.2 完整代码Override public Result queryBlogOfFollow(Long max, Integer offset) { // 1. 获取当前用户 Long userId UserHolder.getUser().getId(); // 2. 查询收件箱 ZREV RANGE BY SCORE key max:当前时间戳|上一次最小时间戳 min0 limit offset count数量 String key FEED_KEY userId; //倒序查询同时把score一起返回 2 每页取两条 SetZSetOperations.TypedTupleString typedTuples stringRedisTemplate.opsForZSet() .reverseRangeByScoreWithScores(key, 0, max, offset, 2); if(typedTuples null || typedTuples.isEmpty()){ return Result.ok(Collections.emptyList()); } // 3. 解析数据: blogId、minTime时间戳、offset //offset与上一个查询最后元素相同的元素的个数 ListLong ids new ArrayList(typedTuples.size()); long minTime 0; int os 1; for (ZSetOperations.TypedTupleString tuple : typedTuples) { //添加id到列表 ids.add(Long.valueOf(Objects.requireNonNull(tuple.getValue()))); //获取时间戳 long time Objects.requireNonNull(tuple.getScore()).longValue(); if (time minTime){ os; }else{ minTime time; os 1; } } // 4. 根据id查询blog String idStr StrUtil.join(,,ids); ListBlog blogs query().in(id, ids).last(order by field(id, idStr )).list(); for (Blog blog : blogs) { //查询blog相关用户 queryBlogUser(blog); //查询blog是否被点赞 queryBlogLikes(blog.getId()); } // 5. 封装并返回 ScrollResult sr new ScrollResult(); sr.setList(blogs); sr.setOffset(os); sr.setMinTime(minTime); return Result.ok(sr); }5.3 核心代码逐行解析Redis 查询命令reverseRangeByScoreWithScores按分数倒序查询同时返回 value 和 score参数0 (最小时间) → max (上一页最后时间戳)分页offset (偏移量) → 2 (每页条数)滚动分页参数计算minTime当前页最后一条数据的时间戳下一页的 maxos与 minTime 相同的元素个数下一页的 offset解决同分数据重复数据库查询保持顺序order by field(id, ...)保证查询结果和 Redis 中 ID 顺序一致数据封装补充用户信息、点赞状态返回前端需要的完整数据5.4 前端交互逻辑首次加载max 当前时间戳offset0下拉加载将上一页返回的minTime作为 maxoffset作为 offset无数据时停止加载代码详解stringRedisTemplate中的value只能是字符串ZSet的member也只能是字符串stringRedisTemplate.opsForZSet().add(key, blog.getId().toString(), System.currentTimeMillis());查询key对应的value中分数在0到max之间的元素同时把分数一起返回SetZSetOperations.TypedTupleString typedTuples stringRedisTemplate.opsForZSet() .reverseRangeByScoreWithScores(key, 0, max, offset, 2);从 Redis ZSet 中解析出博客 ID 列表并计算滚动分页必须的两个关键参数minTime 和 offset解决同分数据重复加载问题。如果当前时间是最小时间说明当前数据与上一次查询最后一个数据重复记录重复次数为什么要记录重复次数offset如果在同一时间有大量博客发布时间戳一致下一次查询会重复加载数据返回前端时间戳和offset下一次查询时前端会跳过offset个时间戳一样的数据。for (ZSetOperations.TypedTupleString tuple : typedTuples) { //添加id到列表 ids.add(Long.valueOf(Objects.requireNonNull(tuple.getValue()))); //获取时间戳 long time Objects.requireNonNull(tuple.getScore()).longValue(); if (time minTime){ os; }else{ minTime time; os 1; } }
本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/2565222.html
如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!