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

Springboot仿抖音app开发之消息业务模块后端复盘及相关业务知识总结

​​​​​​​Springboot仿抖音app开发之粉丝业务模块后端复盘及相关业务知识总结

Springboot仿抖音app开发之用短视频务模块后端复盘及相关业务知识总结

Springboot仿抖音app开发之用户业务模块后端复盘及相关业务知识总结

 用户发表评论并显示评论总数

1. 控制层 (CommentController)

@PostMapping("create")
public GraceJSONResult create(@RequestBody @Valid CommentBO commentBO)throws Exception {CommentVO commentVO = commentService.createComment(commentBO);return GraceJSONResult.ok(commentVO);
}

控制层的主要职责:

  • 提供REST API接口 /comment/create(POST请求)
  • 接收前端传来的JSON数据并绑定到CommentBO对象
  • 使用@Valid注解进行参数校验
  • 调用服务层方法处理业务逻辑
  • 将处理结果包装成统一响应格式返回

2. 服务层 (CommentServiceImpl)

@Override
public CommentVO createComment(CommentBO commentBO) {String commentId = sid.nextShort();Comment comment = new Comment();comment.setId(commentId);comment.setVlogId(commentBO.getVlogId());comment.setVlogerId(commentBO.getVlogerId());comment.setCommentUserId(commentBO.getCommentUserId());comment.setFatherCommentId(commentBO.getFatherCommentId());comment.setContent(commentBO.getContent());comment.setLikeCounts(0);comment.setCreateTime(new Date());commentMapper.insert(comment);// redis操作,评论总数的累加redis.increment(REDIS_VLOG_COMMENT_COUNTS + ":" + commentBO.getVlogId(), 1);// 留言后的最新评论需要返回给前端进行展示CommentVO commentVO = new CommentVO();BeanUtils.copyProperties(comment, commentVO);// 系统消息相关代码被注释掉了return commentVO;
}

服务层的主要职责:

  • 生成唯一评论ID (sid.nextShort())
  • 将前端数据(CommentBO)转换为数据库实体(Comment)
  • 设置额外信息(点赞数初始化为0、创建时间等)
  • 调用Mapper层将评论保存到数据库
  • 在Redis中增加视频的评论计数
  • 将数据库实体(Comment)转换为视图对象(CommentVO)返回给前端
  • (注释掉的部分)创建系统消息通知视频作者有新评论

3. 数据访问层 (CommentMapper)

  • 使用commentMapper.insert(comment)将评论数据插入到数据库

4. 数据模型

业务对象 (CommentBO)

  • 包含用户提交的评论信息:vlogerId, fatherCommentId, vlogId, commentUserId, content
  • 使用注解实现参数验证,如@NotBlank, @Length等

数据库实体 (Comment)

  • 映射到数据库表的实体类
  • 包含完整的评论信息:id, vlogerId, fatherCommentId, vlogId, commentUserId, content, likeCounts, createTime

视图对象 (CommentVO)

  • 返回给前端的数据对象
  • 包含额外信息:commentUserNickname, commentUserFace, replyedUserNickname, isLike等

 

 评论总数显示

非常容易理解吧 从redis中获取之前存储的评论总数

    @GetMapping("counts")public GraceJSONResult counts(@RequestParam String vlogId) {String countsStr = redis.get(REDIS_VLOG_COMMENT_COUNTS + ":" + vlogId);if (StringUtils.isBlank(countsStr)) {countsStr = "0";}return GraceJSONResult.ok(Integer.valueOf(countsStr));}

为什么要Integer.valueOf(countsStr)

Integer.valueOf(countsStr) 的作用是将字符串类型的数字转换为 Integer 对象。这在这段代码中非常重要,原因如下:

  1. 数据类型一致性

    • Redis 中存储的评论数是字符串类型(即使存储的是数字,从 Redis 获取时也是字符串)
    • 前端或调用方期望接收的是数值类型(Integer),而不是字符串
  2. 统一响应格式

    • GraceJSONResult.ok() 方法接收各种数据类型,并将其包装成统一的响应格式
    • 返回 Integer 类型更符合 API 的语义,因为评论数就是一个整数
  3. 避免前端处理

    • 如果直接返回字符串 "0",前端在进行数值运算时可能需要额外的转换
    • 返回 Integer 类型让前端可以直接使用这个数值,无需自行转换
  4. 确保类型安全

    • Java 是强类型语言,通过明确的类型转换可以避免隐式转换带来的潜在问题
    • Integer.valueOf() 比 Integer.parseInt() 更优,因为它可以重用常用整数的缓存

这段代码的完整逻辑是:

  1. 从 Redis 获取指定视频的评论数(字符串格式)
  2. 如果 Redis 中没有该数据,默认为 "0"
  3. 将字符串转换为 Integer 对象
  4. 将转换后的 Integer 对象包装在统一响应格式中返回

这种处理方式体现了 RESTful API 设计的最佳实践,确保返回的数据类型与其语义一致,并减轻了客户端的处理负担。

 

 实现查询评论列表与联调

1. 控制层实现 (CommentController)

@GetMapping("list")
public GraceJSONResult list(@RequestParam String vlogId,@RequestParam(defaultValue = "") String userId,@RequestParam Integer page,@RequestParam Integer pageSize) {return GraceJSONResult.ok(commentService.queryVlogComments(vlogId,userId,page,pageSize));
}

控制层职责

  • 提供REST API接口 /comment/list(GET请求)
  • 接收前端传来的参数:视频ID、用户ID、页码、每页数量
  • 为userId设置默认值为空字符串,处理可选参数
  • 调用服务层方法查询评论列表
  • 将查询结果包装成统一响应格式返回

2. 服务层实现 (CommentServiceImpl)

@Override
public PagedGridResult queryVlogComments(String vlogId,String userId,Integer page,Integer pageSize) {Map<String, Object> map = new HashMap<>();map.put("vlogId", vlogId);PageHelper.startPage(page, pageSize);List<CommentVO> list = commentMapperCustom.getCommentList(map);return setterPagedGrid(list, page);
}

服务层职责

  • 创建参数Map用于传递查询条件
  • 设置vlogId作为查询条件
  • 使用PageHelper进行分页设置
  • 调用Mapper查询评论列表
  • 将结果封装为分页对象返回

3. 数据访问层实现 (CommentMapperCustom)

接口定义

public List<CommentVO> getCommentList(@Param("paramMap") Map<String, Object> map);

SQL查询实现

<select id="getCommentList" parameterType="map" resultType="com.imooc.vo.CommentVO">SELECTc.id as commentId,c.vlog_id as vlogId,u.id as vlogerId,u.nickname as commentUserNickname,u.face as commentUserFace,c.father_comment_id as fatherCommentId,c.comment_user_id as commentUserId,c.content as content,c.like_counts as likeCounts,fu.nickname as replyedUserNickname,c.create_time as createTime
FROM`comment` as c
LEFT JOINusers as u
ONc.comment_user_id = u.id
LEFT JOIN`comment` as fc
ONc.father_comment_id = fc.id
LEFT JOINusers as fu
ONfc.comment_user_id = fu.id
WHEREc.vlog_id = #{paramMap.vlogId}
ORDER BYc.like_counts DESC,c.create_time DESC</select>

 

 评论列表查询SQL的设计详解

SQL整体设计思路

这段SQL是为了实现短视频平台的评论列表功能,需要展示评论内容及相关的用户信息,并支持评论回复的层级关系。整体设计采用了多表关联查询的方式,将评论数据与用户数据整合在一起。

表结构及关系设计

该查询涉及到两个主要数据表:

  1. comment表:存储评论的基本信息
  2. users表:存储用户的基本信息

在SQL中,这些表以不同的别名出现,形成了四个逻辑实体:

  • c:主评论表,包含当前查询的评论数据
  • u:评论作者表,包含评论作者的信息
  • fc:父评论表,用于处理回复关系
  • fu:父评论作者表,包含被回复者的信息

关联关系详解

1. 评论与评论作者关联

LEFT JOIN users as u ON c.comment_user_id = u.id

这个关联将评论与发表评论的用户连接起来:

  • 关联条件:评论表的comment_user_id与用户表的id
  • 使用LEFT JOIN:确保即使用户信息缺失,评论数据也能返回
  • 查询目的:获取评论者的昵称和头像

2. 评论与父评论关联

LEFT JOIN `comment` as fc ON c.father_comment_id = fc.id

这个关联实现了评论的层级结构:

  • 关联条件:当前评论的father_comment_id与父评论的id
  • 表的自关联:comment表自己与自己关联,通过不同别名区分
  • 设计意图:支持评论回复功能,建立评论之间的父子关系
  • 使用LEFT JOIN:允许一级评论存在(father_comment_id可能为null或"0")

3. 父评论与父评论作者关联

LEFT JOIN users as fu ON fc.comment_user_id = fu.id

这个关联获取被回复者的信息:

  • 关联条件:父评论的comment_user_id与用户表的id
  • 设计目的:获取被回复者的昵称,以便显示"回复@某人"的效果
  • 使用LEFT JOIN:即使父评论作者信息缺失,也不影响结果返回

 

 

删除评论功能的实现分析 

1. 控制层实现 (CommentController)

@DeleteMapping("delete")
public GraceJSONResult delete(@RequestParam String commentUserId,@RequestParam String commentId,@RequestParam String vlogId) {commentService.deleteComment(commentUserId,commentId,vlogId);return GraceJSONResult.ok();
}

控制层职责

  • 提供REST API接口 /comment/delete(DELETE请求)
  • 接收三个关键参数:评论者ID、评论ID和视频ID
  • 调用服务层方法执行删除逻辑
  • 返回统一的成功响应格式

设计特点

  • 使用HTTP DELETE方法符合RESTful设计规范
  • 参数通过@RequestParam接收,表明这些是必填项
  • 返回标准化的响应格式

2. 服务层实现 (CommentServiceImpl)

@Override
public void deleteComment(String commentUserId,String commentId,String vlogId) {Comment pendingDelete = new Comment();pendingDelete.setId(commentId);pendingDelete.setCommentUserId(commentUserId);commentMapper.delete(pendingDelete);// 评论总数的累减redis.decrement(REDIS_VLOG_COMMENT_COUNTS + ":" + vlogId, 1);
}

服务层职责

  • 构建待删除的评论对象
  • 设置评论ID和评论者ID作为删除条件
  • 调用Mapper执行数据库删除操作
  • 在Redis中减少视频的评论计数

设计特点

  • 同时验证评论ID和评论者ID,确保用户只能删除自己的评论
  • 数据库操作与缓存操作结合,保持数据一致性
  • 使用Redis计数器维护评论数,避免频繁查询数据库

3. 数据访问层实现

虽然代码片段中没有显示commentMapper.delete()的具体实现,但根据通用Mapper的使用方式,这是一个条件删除操作:

// MyBatis通用Mapper的典型实现
public interface CommentMapper extends Mapper<Comment> {// delete方法会根据非空字段作为条件// 等价于 DELETE FROM comment WHERE id = #{id} AND comment_user_id = #{commentUserId}
}

数据访问层职责

  • 执行SQL删除操作
  • 根据提供的条件(评论ID和评论者ID)定位要删除的记录

 评论点赞与取消点赞功能分析

1. 点赞功能实现

@PostMapping("like")
public GraceJSONResult like(@RequestParam String commentId,@RequestParam String userId) {// 故意犯错,bigkeyredis.incrementHash(REDIS_VLOG_COMMENT_LIKED_COUNTS, commentId, 1);//        // 改为每个评论单独一个 Key//        String key = REDIS_VLOG_COMMENT_LIKED_COUNTS + ":" + commentId;//        redis.increment(key, 1);redis.setHashValue(REDIS_USER_LIKE_COMMENT, userId + ":" + commentId, "1");//        // 改为每个用户+评论组合一个 Key//        String key = REDIS_USER_LIKE_COMMENT + ":" + userId + ":" + commentId;//        redis.set(key, "1");//        redis.hset(REDIS_USER_LIKE_COMMENT, userId, "1");return GraceJSONResult.ok();
}

功能逻辑:

  1. 接收评论ID和用户ID作为参数
  2. 在Redis中增加评论的点赞数
  3. 记录用户对该评论的点赞状态
  4. 返回成功响应

2. 取消点赞功能实现

@PostMapping("unlike")
public GraceJSONResult unlike(@RequestParam String commentId,@RequestParam String userId) {redis.decrementHash(REDIS_VLOG_COMMENT_LIKED_COUNTS, commentId, 1);redis.hdel(REDIS_USER_LIKE_COMMENT, userId + ":" + commentId);return GraceJSONResult.ok();
}

功能逻辑:

  1. 接收评论ID和用户ID作为参数
  2. 在Redis中减少评论的点赞数
  3. 删除用户对该评论的点赞状态记录
  4. 返回成功响应

3. 设计问题分析

3.1 Redis Bigkey问题

代码中注释部分明确指出"故意犯错,bigkey",这表明当前实现存在Redis性能问题:

// 故意犯错,bigkey
redis.incrementHash(REDIS_VLOG_COMMENT_LIKED_COUNTS, commentId, 1);

问题描述

  • 使用单个hash结构(REDIS_VLOG_COMMENT_LIKED_COUNTS)存储所有评论的点赞数
  • 随着评论数增长,这个hash会变得非常大,成为"bigkey"
  • 大型hash会导致Redis性能下降,甚至可能导致服务不稳定

更好的方案(代码中已注释):

String key = REDIS_VLOG_COMMENT_LIKED_COUNTS + ":" + commentId;
redis.increment(key, 1);

这种方案为每个评论创建独立的key,避免了单个hash过大的问题。

3.2 用户点赞记录存储问题

同样的bigkey问题也存在于用户点赞记录的存储中:

redis.setHashValue(REDIS_USER_LIKE_COMMENT, userId + ":" + commentId, "1");

问题描述

  • 使用单个hash(REDIS_USER_LIKE_COMMENT)存储所有用户的所有点赞记录
  • 该hash将随用户数和评论数的增长而迅速膨胀
  • 对大型hash的操作会变得越来越慢

更好的方案(代码中已注释):

String key = REDIS_USER_LIKE_COMMENT + ":" + userId + ":" + commentId;
redis.set(key, "1");

这种方案为每个用户对每个评论的点赞创建独立的key,避免了hash结构过大。

Redis BigKey 问题解析 

什么是 BigKey 问题

BigKey 指的是 Redis 中存储的某个 Key 对应的 Value 过大,通常表现为:

  • 一个 String 类型的 Value 过大(超过 10KB)
  • 一个 Hash/List/Set/ZSet 类型的元素过多(比如 Hash 有数千个字段)

当前代码中的 BigKey 问题

1. REDIS_VLOG_COMMENT_LIKED_COUNTS Hash 结构问题

redis.incrementHash(REDIS_VLOG_COMMENT_LIKED_COUNTS, commentId, 1);

这种方式将所有视频评论的点赞数都存储在一个 Hash 中:

  • 随着评论数量增加,这个 Hash 会变得非常庞大
  • 每次操作都需要加载整个 Hash 到内存
  • 可能导致内存占用过高、操作延迟增加

2. REDIS_USER_LIKE_COMMENT Hash 结构问题

redis.setHashValue(REDIS_USER_LIKE_COMMENT, userId + ":" + commentId, "1");

这种方式将所有用户对评论的点赞关系存储在一个 Hash 中:

  • 用户和评论数量增加时,这个 Hash 会变得极其庞大
  • 每个用户点赞一个评论就增加一个字段
  • 可能导致内存爆炸性增长

改进方案(注释中的正确做法)

1. 为每个评论单独创建 Key

String key = REDIS_VLOG_COMMENT_LIKED_COUNTS + ":" + commentId;
redis.increment(key, 1);

优点:

  • 每个评论的点赞数独立存储
  • 避免单个 Key 过大
  • 操作更高效

2. 为每个用户+评论组合创建 Key

String key = REDIS_USER_LIKE_COMMENT + ":" + userId + ":" + commentId;
redis.set(key, "1");

优点:

  • 每个点赞关系独立存储
  • 避免 Hash 结构无限增长
  • 更容易管理和清理

BigKey 的危害

  1. 内存不均:导致 Redis 内存使用不平衡,可能引发内存不足
  2. 阻塞风险:操作大 Key 可能导致 Redis 阻塞
  3. 网络拥塞:传输大 Key 消耗更多带宽
  4. 持久化问题:AOF 重写和 RDB 保存时效率降低
  5. 迁移困难:集群环境下数据迁移更困难

最佳实践建议

  1. 避免使用单个 Key 存储大量数据
  2. 合理拆分数据结构,如示例中的改进方案
  3. 对于计数器类需求,考虑使用多个 Key 而不是单个 Hash
  4. 定期监控 Redis 的 Key 大小分布
  5. 对大 Key 进行拆分或使用其他存储方案

 

Redis Hash 数据结构详解 

Redis Hash 的三层结构

Redis 的 Hash 数据结构实际上是三层结构

  1. 外层 Key - Hash 结构的名称(如 REDIS_VLOG_COMMENT_LIKED_COUNTS
  2. 内层 Field - Hash 内部的字段名(如 commentId
  3. Value - 字段对应的值(点赞数)

所以完整结构是:Key → (Field → Value)

对比普通 Key-Value

结构类型示例说明
普通 Key-ValueSET comment:1001:likes 5直接键值对
Hash 结构HSET comment_likes comment:1001 5两层映射关系

您的代码具体解析

redis.incrementHash(REDIS_VLOG_COMMENT_LIKED_COUNTS, commentId, 1);

对应关系:

  • 外层 KeyREDIS_VLOG_COMMENT_LIKED_COUNTS(所有评论点赞数的容器)
  • FieldcommentId(具体是哪个评论)
  • Value:点赞数(通过参数1实现原子递增)

为什么这样设计?

  1. 组织相关数据:可以把相关联的多个字段放在一个Hash中

    • 例如用户信息:user:1001 → {name:"张三", age:25, email:"xx@xx.com"}
  2. 减少Key数量:避免为每个小数据都创建独立Key

  3. 原子操作:可以单独操作Hash中的某个字段

用户是否点赞评论与评论总数展示 

主要是在查询接口中添加业务逻辑

@Overridepublic PagedGridResult queryVlogComments(String vlogId,String userId,Integer page,Integer pageSize) {Map<String, Object> map = new HashMap<>();map.put("vlogId", vlogId);PageHelper.startPage(page, pageSize);List<CommentVO> list = commentMapperCustom.getCommentList(map);for (CommentVO cv:list) {String commentId = cv.getCommentId();// 当前短视频的某个评论的点赞总数String countsStr = redis.getHashValue(REDIS_VLOG_COMMENT_LIKED_COUNTS, commentId);Integer counts = 0;if (StringUtils.isNotBlank(countsStr)) {counts = Integer.valueOf(countsStr);}cv.setLikeCounts(counts);// 判断当前用户是否点赞过该评论String doILike = redis.hget(REDIS_USER_LIKE_COMMENT, userId + ":" + commentId);if (StringUtils.isNotBlank(doILike) && doILike.equalsIgnoreCase("1")) {cv.setIsLike(YesOrNo.YES.type);}}return setterPagedGrid(list, page);}

 

1. 基础评论数据查询

Map<String, Object> map = new HashMap<>();
map.put("vlogId", vlogId);
PageHelper.startPage(page, pageSize);
List<CommentVO> list = commentMapperCustom.getCommentList(map);
  • 创建参数Map,设置视频ID作为查询条件
  • 使用PageHelper设置分页参数
  • 调用自定义Mapper查询评论列表
  • 得到包含基本评论信息的VO对象列表

2. 为每条评论添加点赞数

for (CommentVO cv:list) {String commentId = cv.getCommentId();// 当前短视频的某个评论的点赞总数String countsStr = redis.getHashValue(REDIS_VLOG_COMMENT_LIKED_COUNTS, commentId);Integer counts = 0;if (StringUtils.isNotBlank(countsStr)) {counts = Integer.valueOf(countsStr);}cv.setLikeCounts(counts);// ...
}
  • 遍历评论列表
  • 从Redis的hash结构中获取每条评论的点赞数
  • 若点赞数不存在,则默认为0
  • 将点赞数设置到评论VO对象中

3. 判断当前用户是否点赞过评论

// 判断当前用户是否点赞过该评论
String doILike = redis.hget(REDIS_USER_LIKE_COMMENT, userId + ":" + commentId);
if (StringUtils.isNotBlank(doILike) && doILike.equalsIgnoreCase("1")) {cv.setIsLike(YesOrNo.YES.type);
}
  • 构造用户点赞记录的key格式为userId:commentId
  • 从Redis的hash结构中查询该记录是否存在
  • 如果记录存在且值为"1",则设置评论VO对象的isLike属性为YES

4. 封装分页结果返回

return setterPagedGrid(list, page);
  • 将处理后的评论列表封装成统一的分页响应对象返回

相关文章:

  • php反序列化漏洞学习
  • [安卓按键精灵辅助工具]一些安卓端可以用的雷电模拟器adb命令
  • 关于安卓dialogFragment中,EditText无法删除文字的问题
  • Android NTP自动同步时间机制
  • 展开说说Android之Glide详解_使用篇
  • DRG支付场景模拟器扩展分析:技术实现与应用价值
  • 算法导论第三章:数据结构艺术与高效实现
  • 为什么TCP有粘包问题,而UDP没有
  • 前端导出PDF(适配ios Safari浏览器)
  • 力扣HOT100之技巧:136. 只出现一次的数字
  • opencl的简单介绍以及c++实例
  • 爱普生FC-135R晶振在广域网LoRa设备中的应用
  • openEuler 虚拟机中 Shell 脚本实现自动化备份与清理实践
  • Tomcat线程模型
  • 单链表经典算法
  • nt!CcGetDirtyPages函数分析
  • 软件测试相关问题
  • 蓝牙无线串口入门使用教程(以大夏龙雀 WF24 和 BT36 为例)
  • PCI总线概述
  • 【开源工具】:基于PyQt5的智能网络驱动器映射工具开发全流程(附源码)
  • 郑州网站关键字优化/新网站应该怎么做seo
  • 网站源码 后台/在线生成html网页
  • 企业网站制作一/长沙有实力seo优化
  • 域名买好了怎么做网站/中国新闻最新消息
  • 南京做征信服务的公司网站/seo排名优化排行
  • 景观设计公司排名前十强/昆明优化网站公司