news 2026/5/7 11:53:38

【黑马点评日记】社交平台用户关注功能全解析Feed流相关操作

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
【黑马点评日记】社交平台用户关注功能全解析Feed流相关操作

🔥个人主页:北极的代码(欢迎来访)
🎬作者简介:java后端学习者
❄️个人专栏:苍穹外卖日记,SSM框架深入,JavaWeb
命运的结局尽可永在,不屈的挑战却不可须臾或缺!

前言:

我们继续对黑马点评的项目进行学习,这一章节主要学习的是关注用户的相关功能。

摘要:

本文介绍了社交平台用户关注功能的完整实现方案。

核心内容包括:1. 数据库设计使用tb_follow表存储关注关系;2. 实现关注/取消关注功能,通过Redis Set存储关注列表;3. 共同关注功能利用Set求交集实现;4. 采用推模式的Feed流方案,使用ZSet实现滚动分页推送。

文章详细分析了各功能的技术实现要点,包括Controller层参数处理、Service层业务逻辑、Redis数据结构选择等,并对比了不同方案的优缺点。该实现兼顾了功能完整性和性能考量,适合作为中小型社交平台的关注系统基础架构。

  1. 关注/取消关注- 用户可以关注感兴趣的博主

  2. 共同关注- 查看当前用户与博主的共同关注好友

  3. 关注推送(Feed流)- 接收关注用户发布的笔记动态

一、数据库设计

tb_follow 表结构

sql CREATE TABLE `tb_follow` ( `id` bigint(20) NOT NULL AUTO_INCREMENT COMMENT '主键', `user_id` bigint(20) NOT NULL COMMENT '用户id', `follow_user_id` bigint(20) NOT NULL COMMENT '关联的用户id(被关注者)', `create_time` datetime DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间', PRIMARY KEY (`id`) );

对应的实体类:

java @Data @EqualsAndHashCode(callSuper = false) @Accessors(chain = true) @TableName("tb_follow") public class Follow implements Serializable { private static final long serialVersionUID = 1L; @TableId(value = "id", type = IdType.AUTO) private Long id; private Long userId; private Long followUserId; private LocalDateTime createTime; }

注意主键设置为自增长,简化开发。

二、关注/取消关注功能实现

3.1 Controller层

java @RestController @RequestMapping("/follow") public class FollowController { @Resource private IFollowService followService; // 判断是否关注 @GetMapping("/or/not/{id}") public Result isFollow(@PathVariable("id") Long followUserId) { return followService.isFollow(followUserId); } // 关注/取消关注 @PutMapping("/{id}/{isFollow}") public Result follow(@PathVariable("id") Long followUserId, @PathVariable("isFollow") Boolean isFollow) { return followService.follow(followUserId, isFollow); } }

@PathVariable("id") Long followUserId这行代码的作用,就是把URL中的{id}参数(字符串)提取出来,自动转换成Long类型,然后赋值给followUserId变量。

整个过程的两个关键步骤是:

  1. 映射(绑定)@PathVariable("id")告诉Spring,去URL的路径里找到名叫id的变量(也就是{id}这个位置的值)。

  2. 类型转换Long告诉Spring,把拿到的字符串(比如 "10086")转换成一个Long类型的数字(比如 10086L),再交给 Java 代码使用。

图解示例

假设前端发来的请求URL是:/follow/123/true

java

@PutMapping("/{id}/{isFollow}") public Result follow( @PathVariable("id") Long followUserId, // 步骤1: 找到 "123" → 步骤2: 转换为 123L → 赋值给 followUserId @PathVariable("isFollow") Boolean isFollow // 步骤1: 找到 "true" → 步骤2: 转换为 true → 赋值给 isFollow )

执行结果就是:

  • followUserId变量的值是123L

  • isFollow变量的值是true

几个关键点
  • 变量名不重要:你可以用@PathVariable("id") Long abc,效果一样,abc变量最终的值也是123L。变量名是给开发者自己看的。

  • 转换有规则:Spring 支持大部分常见类型的自动转换,如intlongbooleanDate等。如果URL里的值无法转换(比如给Long传了个"abc"),就会报错。

  • 类型不匹配时:如果把URL里的{id}值 "123" 赋给@PathVariable("id") String followUserId,类型转换这一步就不做,followUserId的值就直接是字符串"123"

所以,严格来说是类型转换,但它和“参数绑定” (@PathVariable) 经常一起出现,共同完成了从“URL字符串”到“Java对象”的转换工作。

这个用法在Spring MVC里非常常见,不仅仅是处理路径变量,处理普通请求参数时,@RequestParam也是同样的原理。

3.2 Service层实现

java @Service public class FollowServiceImpl extends ServiceImpl<FollowMapper, Follow> implements IFollowService { @Resource private StringRedisTemplate stringRedisTemplate; @Override public Result isFollow(Long followUserId) { // 获取当前登录用户 Long userId = UserHolder.getUser().getId(); // 查询是否关注 Integer count = query() .eq("user_id", userId) .eq("follow_user_id", followUserId) .count(); return Result.ok(count > 0); } @Override public Result follow(Long followUserId, Boolean isFollow) { // 获取当前登录用户 Long userId = UserHolder.getUser().getId(); String key = "follows:" + userId; if (isFollow) { // 关注:新增数据 Follow follow = new Follow(); follow.setUserId(userId); follow.setFollowUserId(followUserId); boolean isSuccess = save(follow); if (isSuccess) { // 将关注用户ID存入Redis Set集合 stringRedisTemplate.opsForSet().add(key, followUserId.toString()); } } else { // 取关:删除数据 boolean isSuccess = remove(new QueryWrapper<Follow>() .eq("user_id", userId) .eq("follow_user_id", followUserId)); if (isSuccess) { // 从Redis Set集合中移除 stringRedisTemplate.opsForSet().remove(key, followUserId.toString()); } } return Result.ok(); } }

QueryWrapper是 MyBatis-Plus 提供的条件构造器,用于动态构建SQL查询条件,避免手写SQL字符串。

常用方法对比

方法作用SQL对应
.eq("列名", 值)等于 =WHERE 列名 = 值
.ne("列名", 值)不等于 !=WHERE 列名 != 值
.gt("列名", 值)大于 >WHERE 列名 > 值
.lt("列名", 值)小于 <WHERE 列名 < 值
.like("列名", 值)模糊匹配WHERE 列名 LIKE '%值%'
.in("列名", 集合)在集合中WHERE 列名 IN (值1, 值2)

3.3 核心要点

为什么使用Redis Set

  • 为后续"共同关注"功能做准备

  • Set集合支持高效的求交集操作

  • Key格式:follows:userId,Value为用户关注的所有博主ID

三、共同关注功能实现

4.1 Controller层

java

@GetMapping("/common/{id}") public Result followCommons(@PathVariable Long id) { return followService.followCommons(id); }

4.2 Service层实现

java

@Override public Result followCommons(Long id) { // 1. 获取当前用户 Long userId = UserHolder.getUser().getId(); String key1 = "follows:" + userId; String key2 = "follows:" + id; // 2. 求交集 Set<String> intersect = stringRedisTemplate.opsForSet() .intersect(key1, key2); if (intersect == null || intersect.isEmpty()) { return Result.ok(Collections.emptyList()); } // 3. 解析ID集合(String -> Long) List<Long> ids = intersect.stream() .map(Long::valueOf) .collect(Collectors.toList()); // 4. 查询用户信息 List<UserDTO> users = userService.listByIds(ids) .stream() .map(user -> BeanUtil.copyProperties(user, UserDTO.class)) .collect(Collectors.toList()); return Result.ok(users); }

4.3 核心要点

实现原理

  • 利用Redis Set的sinter命令求交集

  • follows:当前用户IDfollows:博主ID的交集即为共同关注

关键优化

  • 需要在关注/取关时同步维护Redis数据

  • 避免直接查询数据库,提高性能

五、关注推送(Feed流)

5.1 Feed流方案对比

Feed流有三种实现模式:

方案写比例读比例延迟实现难度使用场景
拉模式(读扩散)复杂很少使用
推模式(写扩散)简单用户量少、无大V
推拉结合很复杂过千万用户量

黑马点评选用推模式:因为项目规模较小,推模式实现简单、时效性好。

5.2 数据结构选型:ZSet

为什么使用SortedSet?

  • ✅ 元素具有唯一性(不重复推送)

  • ✅ 可按时间戳排序(score存储时间戳)

  • ✅ 支持滚动分页(避免传统分页的数据重复/遗漏问题)

对比List的问题
如果使用List按角标分页,当有新数据插入时,角标会变化,导致查询重复或遗漏。而ZSet按score范围查询可以避免此问题。

5.3 发布笔记时推送

java

@Override public Result saveBlog(Blog blog) { // 1. 获取登录用户 UserDTO user = UserHolder.getUser(); blog.setUserId(user.getId()); // 2. 保存博客到数据库 boolean isSuccess = save(blog); if (!isSuccess) { return Result.fail("发布笔记失败!"); } // 3. 查询所有粉丝 List<Follow> fans = followService.lambdaQuery() .eq(Follow::getFollowUserId, user.getId()) .list(); // 4. 推送笔记ID给所有粉丝(推模式) for (Follow fan : fans) { String key = "feed:" + fan.getUserId(); // 粉丝收件箱 stringRedisTemplate.opsForZSet().add( key, blog.getId().toString(), System.currentTimeMillis() // 时间戳作为score ); } return Result.ok(); }

5.4 滚动分页查询

传统分页的问题

text

时间线:[新] D, C, B, A [旧] 第1页:D, C 第2页:B, A(本来应该是C, B) 问题:新数据插入导致角标偏移,数据重复或遗漏

滚动分页实现

java

@Override public Result queryBlogOfFollow(Long max, Integer offset) { // 1. 获取当前用户 Long userId = UserHolder.getUser().getId(); String key = "feed:" + userId; // 2. 查询收件箱(滚动分页) Set<ZSetOperations.TypedTuple<String>> typedTuples = stringRedisTemplate.opsForZSet() .reverseRangeByScoreWithScores(key, 0, max, offset, 2); if (typedTuples == null || typedTuples.isEmpty()) { return Result.ok(); } // 3. 解析数据 List<Long> ids = new ArrayList<>(); long minTime = 0; int os = 1; for (ZSetOperations.TypedTuple<String> tuple : typedTuples) { ids.add(Long.valueOf(tuple.getValue())); long time = tuple.getScore().longValue(); if (time == minTime) { os++; } else { minTime = time; os = 1; } } // 4. 查询博客详情(保持顺序) String idStr = StrUtil.join(",", ids); List<Blog> blogs = query() .in("id", ids) .last("ORDER BY FIELD(id, " + idStr + ")") .list(); // 5. 封装返回结果 ScrollResult result = new ScrollResult(); result.setList(blogs); result.setMinTime(minTime); result.setOffset(os); return Result.ok(result); }

滚动分页参数说明

参数第1次查询第2次查询
max当前时间戳上次的最小时间戳
min00
offset0上次最小值相同分数个数
count33

5.5 常见问题与优化

问题1:MySQL查询顺序错乱

现象:Redis取出ID顺序是[5,1,3],但listByIds返回的是[1,3,5]

原因SELECT ... WHERE id IN (5,1,3)默认按主键升序返回

解决方案

java

// 使用ORDER BY FIELD强制保持顺序 .last("ORDER BY FIELD(id, " + idStr + ")")
问题2:大V粉丝量大的性能问题

推模式下,拥有百万粉丝的大V发一条笔记需写入100万次Redis,内存与性能压力巨大。

优化方案

  • 普通用户:继续使用推模式

  • 大V用户:改用拉模式或推拉结合模式(先将笔记存入发件箱,粉丝查询时再拉取)

六、技术要点总结

6.1 Redis数据结构选择

功能数据结构原因
共同关注Set支持sinter求交集
Feed流ZSet支持按时间戳排序 + 滚动分页
关注列表Set存储关注ID集合

6.2 关键技术点

  1. 读写分离:关注关系存MySQL,社交关系存Redis

  2. 缓存同步:关注/取关时同步更新Redis Set

  3. 推模式:发布时主动推送给粉丝,降低查询延迟

  4. 滚动分页:避免传统分页的数据重复问题

  5. 顺序保持:MySQL使用ORDER BY FIELD保持Redis排序

6.3 优缺点分析

优点

  • 推模式实现简单,粉丝查询延迟低

  • Redis ZSet支持高效的排序和分页

  • Set交集实现共同关注简单高效

缺点

  • 推模式对存储压力大(每个粉丝存一份)

  • 大V场景下写放大问题严重

  • 未使用推拉结合优化(适合百万级用户的方案)


这个用户关注功能的完整实现涵盖了社交产品的核心需求,适合作为学习Redis在社交场景应用的入门案例。

结语:如果对你有帮助,请点赞,关注,收藏,你的支持就是我最大的鼓励!

版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/5/7 11:51:56

中小企业IT治理困局破局之道(AISMM轻量化实施框架首次公开)

更多请点击&#xff1a; https://intelliparadigm.com 第一章&#xff1a;中小企业IT治理困局的本质解构 中小企业IT治理常被简化为“买几台服务器、装个OA、找人修电脑”&#xff0c;但其深层矛盾实为战略意图、组织能力与技术现实之间的三重断裂。当业务部门抱怨系统响应慢&…

作者头像 李华
网站建设 2026/5/7 11:51:53

ubuntu中添加用户并赋予root权限

1. 添加用户 useradd [-d homepath] [-s shell] -m username useradd -d /home/test -s /bin/bash -m test -d&#xff1a;指定用户的家目录 -s&#xff1a;用户的登录shell -m&#xff1a;创建用户家目录2. 给用户添加root权限 usermod -aG sudo username #测试用户是否有ro…

作者头像 李华
网站建设 2026/5/7 11:48:42

react-datasheet-grid 性能优化秘籍:如何轻松应对海量数据处理

react-datasheet-grid 性能优化秘籍&#xff1a;如何轻松应对海量数据处理 【免费下载链接】react-datasheet-grid An Airtable-like / Excel-like component to create beautiful spreadsheets. 项目地址: https://gitcode.com/gh_mirrors/re/react-datasheet-grid 在现…

作者头像 李华
网站建设 2026/5/7 11:47:35

三步搞定B站视频下载:BilibiliDown让你的收藏永不消失

三步搞定B站视频下载&#xff1a;BilibiliDown让你的收藏永不消失 【免费下载链接】BilibiliDown (GUI-多平台支持) B站 哔哩哔哩 视频下载器。支持稍后再看、收藏夹、UP主视频批量下载|Bilibili Video Downloader &#x1f633; 项目地址: https://gitcode.com/gh_mirrors/b…

作者头像 李华