news 2026/10/26 10:23:44

Java 项目实战: 交友项目 - 圈子功能实现:总结

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
Java 项目实战: 交友项目 - 圈子功能实现:总结

概述

圈子这一章把「发一条动态」背后的一整套存储设计摊开了:MongoDB 存详情、时间线表做收件箱、Redis 存推荐列表,三套存储各管一段。这里做全章复盘,知识地图、五个可复用的架构套路、十二个实测踩过的坑、自检清单和四个动手练习,最后指向下一章的圈子互动。

纲要

  • 知识地图:六条主线从发布到互动的完整链路
  • 可复用套路清单:五个能搬去任何 Feed 系统的方案
  • 关键坑回顾:十二个实测复现过的坑(坑表)
  • 自检清单:合上讲义能答上来才算过
  • 技术练习:四个「改哪里 / 怎么验证」明确的动手项
  • 下一章预告:圈子互动

知识地图

先把全章 six 条主线放一张图里,箭头是数据流向:

state=0 未审

异步改造

发布动态
POST /movements

movement
动态详情

RabbitMQ / @Async

movement_timeline
好友时间线

我的动态
按 userId 查详情

好友动态
收件箱推模式

Redis
MOVEMENTS_RECOMMEND_pid串

推荐动态

单条详情
GET /movements/&{id&}

互动入口
评论/点赞/喜欢

六条主线各自的核心结论:

主线存储与关键设计一句话结论
发布动态movement集合,state=0未审默认值,pid由 IdWorker 自增详情只存一份;写扩散发生在发布之后
发布异步化@Async起步,MQ 改造收尾时间线投递不能阻塞发布主链路,但要配套幂等与吞异常策略
好友动态movement_timeline收件箱,查询方按friendId查推模式:发布时写每个好友的收件箱,读时只查自己的表
推荐动态RedisMOVEMENTS_RECOMMEND_{userId}存逗号分隔 pid 串顺序即推荐排序,冷启动$sample兜底,分页只能在内存做
我的动态 / 单条详情按userId查movement;_id用于详情、pid用于推荐两套 id 的分工在详情页交汇
互动comment集合三合一 + Redis Hash 记状态评论/点赞/喜欢共用一张表靠commentType区分,细节在下一章

存储分层的意图要说透:MongoDB 管「内容真相」,时间线表管「可见性」,Redis 管「推荐序」。动态本体永远只有movement里那一份,时间线表里存的是引用(movementId),推荐列表里存的是pid。这带来一个必然结果,跨存储组装:不管哪条查询主线,最后一步都要拿 id 回movement反查详情,再拼UserInfo补头像昵称,才凑得出前端要的MovementsVo。

读写两条路径

把六条主线再压缩一次,其实只有两条路径。写路径只有一条:POST /movements→ OSS 传图 →movement落详情(含 pid 分配)→ 异步给每个好友投递时间线引用。读路径有三条分支,入口不同、第一步不同,第二步以后完全一样:

渲染错误:Mermaid 渲染失败: Lexical error on line 2. Unrecognized text. ... TB subgraph 读分支:第一步各查各的 F[" ----------------------^

三条分支在「第一步」上的差异正是存储分工的体现:好友动态解决可见性(谁能看),推荐动态解决排序(先看谁),我的动态解决归属(谁发的)。而它们共享「回查详情 + 组装 VO」的后半段,说明内容真相始终只有一份。理解了这个结构,下一章互动数据的读法(评论列表按publishId挂到详情页)也只是再加一条读分支而已。

可复用套路清单

这五条不依赖项目的业务,做任何 Feed 流系统都用得上:

  • 推模式时间线写扩散:发布时把动态引用写进每个好友的收件箱(movement_timeline),读路径退化为单表分页查询;用读频次的降低换写时一次for循环,适合好友数可控的社交产品。
  • MQ 异步解耦发布链路:时间线投递从发布主流程里摘出去(@Async起步、RabbitMQ 收尾),发布接口的耗时只包含详情落库;配套要做消息体约定、唯一索引幂等、消费吞异常防无限重投。
  • 三合一互动存储:评论、点赞、喜欢共用comment集合,commentType区分类型,读写逻辑复用一套分页与计数;代价是所有写删条件必须带全(publishId, commentType, userId),漏一个就误伤。
  • Redis Hash 互动标记:一条动态一个 Hash(key),一个用户一个 field,把「百万级 key」压成「万级 field」;hasLiked回填时批量HMGET一次搞定。
  • 跨存储组装:id 列表先收集,再一次性回查详情与用户资料,内存拼 VO;禁止在循环里逐条远程调用,N+1 是这条套路的唯一大敌。

关键坑回顾

这一章的坑不是理论推演,全部在测试篇里实测复现过。十二行排一遍,修复成本从一行代码到一个删除策略不等:

坑现象根因修复思路
multipart 默认 1MB多图发布直接报超限异常Spring Boot 单文件默认上限 1MBmax-file-size: 5MB、max-request-size: 30MB,容得下 6 张图
publish 吞异常假成功Mongo 挂了接口照样 200publish里catch (Exception e)只printStackTrace详情落库失败必须上抛或补偿,别把失败藏进日志
@Async无@EnableAsync时间线不投递,且无任何报错注解代理没开启,方法同步执行启动类加@EnableAsync,条件三件:注解、public 方法、跨 Bean 调用
白名单未落地(avatar 绕过人脸)任意图片不经历审核进动态头像接口的 MIME 白名单 + 人脸检测没有搬到发布链路,state=0又默认未审发布前复用头像的校验套路,并接内容审核任务消费state
movement.getId()回填时机异步链路拿到 null idObjectId 在save之后才回填,早于 save 引用会空先 save 再取 id 传给异步任务
异步线程拿不到UserHolder异步方法里取登录人报错ThreadLocal 不跨线程主线程取完 userId 作为参数传入
默认线程池无上限高并发下线程膨胀@Async默认执行器不复用线程换有界线程池并显式命名
timeLine 残留投递取关后还能刷到对方动态发布时快照式投递,取关不清movement_timeline取关时deleteMany({userId:对方, friendId:我}),或查询时按现好友列表过滤
引用失效空洞删动态后分页少一条、counts 对不上时间线引用还在,in回查movement少一条删动态时同步删时间线引用,查询侧不补救
counts 写死 0PageResult.counts恒为 0findByUserId直接new PageResult(page, pagesize, 0l, ...)补一次count查询回填
page=0 负 skippage=0请求 500skip((page-1)*pagesize)算出 -10,Mongo 服务端报错入口Math.max(page, 1)钳制
Map 接参类型不安全key 拼错编译期无感,运行时 NPE;Integer 转 Long 炸@RequestBody Map绕过了类型系统换 DTO 接参,字段类型编译期锁定

这十二个坑有三个共同点,值得记下来当 review 清单的视角:都发生在存储边界(上传、落库、跨表、跨线程)、失败都偏安静(不报错或报错难懂)、教学快照故意不修(留作分析素材)。

自检清单

合上讲义逐条自问,答不上来的回去翻对应篇目:

  • movement集合里_id和pid各自被谁使用?为什么推荐系统要用自增 pid 而不是 ObjectId?
  • 发布动态时state默认值是多少?谁来把它改成 1?
  • 好友动态为什么查movement_timeline而不是直接查好友的movement?推模式省了什么、付出了什么?
  • 推荐动态的 pid 串存在 Redis 的什么结构里?key 怎么按用户隔离?冷启动用户查不到 key 时走什么兜底?
  • 推荐动态的分页为什么只能在内存做,不能下推给 Mongo?
  • 时间线表里的userId和friendId各是什么语义?查询条件用的是哪一个?
  • 删除一条动态后,时间线里的引用会发生什么?用户端看到什么现象?
  • @Async生效的三个条件是什么?默认线程池有什么问题?
  • comment集合为什么把三种互动放一张表?写删时的安全边界是哪几个字段?

技术练习

四个练习都是把前几篇测试篇里指出的缺陷亲手修掉,每项都给出改哪里、怎么验证。

练习一:补上 @EnableAsync

改哪里:mf-dubbo-mongo的启动类DubboMongoApplication,类上加org.springframework.scheduling.annotation.EnableAsync;顺手定义一个有界线程池替换默认执行器:

@Bean(name="timeLineExecutor")publicExecutortimeLineExecutor(){ThreadPoolTaskExecutorexecutor=newThreadPoolTaskExecutor();executor.setCorePoolSize(4);executor.setMaxPoolSize(8);executor.setQueueCapacity(200);executor.setThreadNamePrefix("timeline-");executor.initialize();returnexecutor;}

TimeLineService.saveTimeLine的@Async上指定@Async("timeLineExecutor")。

怎么验证:发一条动态,在saveTimeLine里打一条带Thread.currentThread().getName()的日志。修复前线程名是主线程(http-nio 执行线程),修复后变成timeline-1;再查movement_timeLine确认投递记录存在。对照验证:把注解删掉重发,日志回到主线程名,亲眼看一次「代码看着异步、跑起来同步」。

练习二:修掉 publish 的吞异常

改哪里:MovementApiImpl.publish的catch (Exception e)。最小改法是失败上抛:

}catch(Exceptione){log.error("发布动态失败, userId={}",movement.getUserId(),e);thrownewRuntimeException("发布动态失败",e);}

注意抛出后timeLineService.saveTimeLine的异步调用不会执行,这正是期望行为:详情都没存成,不该给好友投递引用。

怎么验证:停掉 MongoDB(或临时把集合名改成不存在的值触发异常),Postman 发POST /movements。修复前返回 200 但库里无数据(假成功);修复后返回 500,日志里有完整堆栈和 userId。恢复 Mongo 后重新发布,一切正常。

练习三:给发布链路补白名单校验

改哪里:MovementService.publishMovement,在 OSS 上传循环之前,对每个MultipartFile做三道校验,判空、大小、MIME 白名单(只认image/jpeg、image/png),校验套路直接抄 02 章头像上传的实现:

privatestaticfinalList<String>ALLOWED_TYPES=Arrays.asList("image/jpeg","image/png");privatevoidcheckImage(MultipartFilefile){if(file==null||file.isEmpty()){thrownewBusinessException(ErrorResult.error("图片为空"));}if(!ALLOWED_TYPES.contains(file.getContentType())){thrownewBusinessException(ErrorResult.error("仅支持 jpg、png 格式"));}}

这练习解决的是「avatar 绕过人脸」那行坑:头像接口有白名单 + 人脸检测,发布链路却裸奔,任何文件都能借imageContent进 OSS 并挂到动态上。完整方案还要接内容审核消费state,本练习只做上传入口这一层。

怎么验证:Postman 把一个.html文件改名为.jpg上传,修复前 200 且 OSS 里能找到该文件;修复后返回 500(业务异常),OSS 无新对象。再用正常 jpg 回归一遍确认误伤为零。

练习四:清理取关后的残留投递

改哪里:取关(删除好友)的服务方法里,环信删联系人、删friend双记录之后,追加清理时间线:

//清理取关对象投递到自己收件箱的动态引用Queryquery=Query.query(Criteria.where("userId").is(friendId)//发布人是被取关的人.and("friendId").is(currentUserId));//收件箱是我的mongoTemplate.remove(query,MovementTimeLine.class);

方向别写反:要删的是「对方发布、投给我」的记录;我此前转发到他收件箱的记录删不删取决于产品语义,练习里保持单向清理即可。

怎么验证:A、B 互加好友,A 发两条动态;B 查GET /movements能看到两条。B 取关 A 后再查,A 的动态应全部消失;mongosh 里db.movement_timeline.find({userId: A的id, friendId: B的id})应为空。反向回归:A 未取关 B,A 的列表里 B 的动态应仍在。

常见误解澄清

误解实际情况
时间线表里存的是动态内容只存movementId引用,内容真相只有movement一份
@Async加了就异步必须@EnableAsync+ public 方法 + 跨 Bean 调用,三条件缺一即同步执行且无报错
推荐动态的分页由 MongoDB 完成pid 串在 Redis,分页用 Stream 在内存做,Mongo 只做pid in回查
发布动态会先经过审核state默认 0(未审),工程快照里没有审核任务,实际等于免审上线
取关后动态自然不可见快照式投递不会自动回收,残留记录要靠取关时清理或查询时过滤

官方文档

  • Spring Data MongoDB - Query & Skip/Limit ,skip/limit分页与负数 skip 的边界行为
  • RabbitMQ Tutorials , 发布链路 MQ 改造的异步模型
  • MongoDB $sample , 推荐动态冷启动随机采样

总结

圈子这一章的真实主线不是 CRUD,而是存储分工:movement存真相、movement_timeline管可见性、Redis 管推荐序,三条查询主线最后都汇到「跨存储组装 VO」这一个动作上。十二个坑里最值得带走的是三个习惯:异步必须验证真的异步了、失败不能吞进日志、投递出去的引用要有人负责回收。下一章圈子互动接着把comment集合的三合一设计、点赞的 Redis Hash 标记和取消互动的误删坑展开,互动数据一动,本章的 counts 写死 0 也就到了该还账的时候。

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

初识C语言——C语言常见概念

提示&#xff1a;文章写完后&#xff0c;目录可以自动生成&#xff0c;如何生成可参考右边的帮助文档 文章目录一、在VS2022上写代码1.创建项目2.运行程序二、main函数三、printf和库函数四、关键字五、字符和ASCII编码1.需要掌握的&#xff1a;2.打印一个字符的方法&#xff1…

作者头像 李华
网站建设 2026/10/26 10:22:50

SKILL全详解:从SKILL.md到scripts与references的Agent能力拆解

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

作者头像 李华
网站建设 2026/10/26 10:22:33

给园区系统接问数平台,我们复盘了 6 个真实坑

园区日常跑着一套智慧园区系统&#xff0c;招商、合同、账单、工单都在上面发生&#xff0c;数据自然沉淀下来。后来我们接了数智安全问数平台&#xff0c;让它用大白话问这些已经存在的经营数据&#xff0c;返回结论、图表、明细和来源。这两套东西分工清楚。智慧园区系统管业…

作者头像 李华
网站建设 2026/10/26 10:21:39

Jev 决策模型

摘要 Jev 是 TypeSafe AI 于 2026 年 9 月发布的首个「System One 模型」。它刻意不做大语言模型&#xff08;LLM&#xff09;做的事——不生成自然语言文本&#xff0c;而是接收一份程序状态和若干预定义的问题&#xff0c;返回带校准概率的类型化决策&#xff0c;让软件可以…

作者头像 李华
网站建设 2026/10/26 10:20:16

2026年广东安全员A证考试题库+答案+解析

2026年安全员ABC证考试精准题库⬆️⬆️⬆️ 点击上方卡片立即获取⬆️⬆️⬆️ 考点练习题型练习易错题模拟考错题集收藏夹2026年广东建筑安全员A证考试真题题库 题库来源&#xff1a;安教安全员通关宝1.&#xff08;单选&#xff09;根据《建筑起重机械安全监督管理规定》&am…

作者头像 李华
网站建设 2026/10/26 10:20:04

高质量数据集第一期:SQL代码问答对数据集

DatasetSeek 是面向 AI 的全栈数据基础服务平台&#xff0c;以下是平台上线的第一期数据集。 SQL 代码问答对数据集是一组「自然语言问题 — SQL 查询语句」配对数据&#xff0c;面向 Text-to-SQL 模型训练、微调与评测场景。 每条数据包含两部分&#xff1a;左侧是用户用自然…

作者头像 李华