Java 项目实战: 交友项目 - 动态查询开工:昨日回顾与今日任务

概述

圈子的「写」链路跑通了:发布动态落库、异步写好友时间线、能查自己的动态。现在补全「读」链路,好友动态、推荐动态、单条动态,三个查询入口共用一套 VO 组装逻辑。这里是这一天任务的导引:先盘昨天留下的能力,再按依赖顺序拆今天的活。

纲要

  • 昨日回顾:写链路就位,今天能查到什么
  • 发布动态:OSS 上传 + @Async 异步写时间线
  • 三表设计:movement / movement_timeline / friend
  • 首页推荐:MongoTemplate 分页 + 跨存储组装
  • MongoDB 集群:副本集与分片的取舍
  • 任务书:三个查询接口的需求、产出与验收标准
  • 任务依赖关系(Mermaid 图)
  • 本章代码产出预告(目录树)
  • 学习建议与常见误解澄清

昨日回顾:写链路已经就位

之前的工作建立在 day03 打下的 MongoDB 基础上,实体用 @Document 做映射、MongoTemplate 做 CRUD、mongo 服务与 mysql 服务之间的跨存储组装模式。这些能力昨天全部铺开用了一遍,今天查询侧继续复用。逐项对照:

昨天做了什么 产出什么 今天怎么用
首页推荐:分页查询推荐好友列表 RecommendUserApi.queryRecommendUserList,Mongo 分页 + 批量查 UserInfo 组装 VO 同一套「列表 + 详情批量组装」骨架,今天三个查询接口照抄这个模式
MongoDB 集群:副本集解决单点故障,分片解决海量数据 对分片键的认知:动态表按 userId 分片、时间线表按 friendId 分片 查询条件要顺着分片键走,好友动态按 friendId 查时间线正是为此
圈子表设计方案:三种方案对比,选定第三种 movement(动态总表)、movement_timeline(好友时间线)、friend(好友关系)三张表 好友动态的数据源就是昨天设计好的时间线表
发布动态:文字校验、图片传 OSS、@Async 异步写时间线 MovementApi.publish + TimeLineService.saveTimeLine 好友动态能「只查一张表」的前提,就是发布时已经把动态分发到每个好友的时间线里
我的动态:按 userId 分页查 movement MovementApi.findByUserId 今天查询单条动态、推荐动态的实现模板都从这里出

有一个衔接点值得单独说:昨天发布动态拆成了「主表同步写 + 时间线异步写」,当时看是为了发布接口快。今天写好友动态查询时会发现这个拆分的另一半价值,读的时候只需要查自己的一份时间线,不用遍历好友逐个去查他们的动态。

今日任务书

按讲义与源码顺序,今天一共三块活,外加对下一章互动功能的预告:

板块 做什么 产出物 验收标准
查询好友动态 需求分析 + 代码实现:先查自己的时间线表(friendId = 当前用户),提取 movementId 批量查动态详情,再组装 MovementsVo MovementApi.findFriendMovements 及其实现 用 106 号用户登录,只返回其好友发布的动态,按时间倒序分页
查询推荐动态 需求分析 + 代码实现:从 Redis 取推荐 pid 列表做内存分页;无推荐数据时用 $sample 随机取 10 条兜底 MovementApi.findMovementsByPids、MovementApi.randomMovements、Constants.MOVEMENTS_RECOMMEND 常量 分页返回推荐动态;清掉 Redis 键后仍能随机返回数据,不报空指针
查询单条动态 根据动态 id 查详情 MovementApi.findById 及其实现 按动态 id 返回单条 MovementsVo,作为详情页与评论列表的前置接口
预告:圈子互动 评论、点赞、喜欢,下一章的内容,这里不展开 无 无

三个查询接口的对比,差异全在数据源和分页方式上:

接口 数据源 分页方式
GET /movements(好友动态) movement_timeline,条件 friendId = 我 Mongo skip / limit,按 created 倒序
GET /movements/recommend(推荐动态) Redis 中的 pid 逗号串,兜底查 movement 表 应用内存里对 pid 数组做 skip / limit 流式截取
GET /movements/{id}(单条动态) movement,条件 _id 无分页
GET /movements/all(我的动态,昨天完成) movement,条件 userId = 我 Mongo skip / limit

推荐动态这条要展开一句:推荐系统算完的结果是写进 Redis 的,形如 "2562,3639,2063,...",一堆 pid 由逗号分隔。MongoDB 里存的只有动态原始数据,「第几页推荐了谁」这件事 Redis 不管、Mongo 也不管,得自己在 Service 层切。所以它的分页代码和另外两个长得不一样。

任务依赖关系

#mermaid-svg-zU2h1SpgoU6KhZt7{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;fill:#333;}@keyframes edge-animation-frame{from{stroke-dashoffset:0;}}@keyframes dash{to{stroke-dashoffset:0;}}#mermaid-svg-zU2h1SpgoU6KhZt7 .edge-animation-slow{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 50s linear infinite;stroke-linecap:round;}#mermaid-svg-zU2h1SpgoU6KhZt7 .edge-animation-fast{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 20s linear infinite;stroke-linecap:round;}#mermaid-svg-zU2h1SpgoU6KhZt7 .error-icon{fill:#552222;}#mermaid-svg-zU2h1SpgoU6KhZt7 .error-text{fill:#552222;stroke:#552222;}#mermaid-svg-zU2h1SpgoU6KhZt7 .edge-thickness-normal{stroke-width:1px;}#mermaid-svg-zU2h1SpgoU6KhZt7 .edge-thickness-thick{stroke-width:3.5px;}#mermaid-svg-zU2h1SpgoU6KhZt7 .edge-pattern-solid{stroke-dasharray:0;}#mermaid-svg-zU2h1SpgoU6KhZt7 .edge-thickness-invisible{stroke-width:0;fill:none;}#mermaid-svg-zU2h1SpgoU6KhZt7 .edge-pattern-dashed{stroke-dasharray:3;}#mermaid-svg-zU2h1SpgoU6KhZt7 .edge-pattern-dotted{stroke-dasharray:2;}#mermaid-svg-zU2h1SpgoU6KhZt7 .marker{fill:#333333;stroke:#333333;}#mermaid-svg-zU2h1SpgoU6KhZt7 .marker.cross{stroke:#333333;}#mermaid-svg-zU2h1SpgoU6KhZt7 svg{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;}#mermaid-svg-zU2h1SpgoU6KhZt7 p{margin:0;}#mermaid-svg-zU2h1SpgoU6KhZt7 .label{font-family:"trebuchet ms",verdana,arial,sans-serif;color:#333;}#mermaid-svg-zU2h1SpgoU6KhZt7 .cluster-label text{fill:#333;}#mermaid-svg-zU2h1SpgoU6KhZt7 .cluster-label span{color:#333;}#mermaid-svg-zU2h1SpgoU6KhZt7 .cluster-label span p{background-color:transparent;}#mermaid-svg-zU2h1SpgoU6KhZt7 .label text,#mermaid-svg-zU2h1SpgoU6KhZt7 span{fill:#333;color:#333;}#mermaid-svg-zU2h1SpgoU6KhZt7 .node rect,#mermaid-svg-zU2h1SpgoU6KhZt7 .node circle,#mermaid-svg-zU2h1SpgoU6KhZt7 .node ellipse,#mermaid-svg-zU2h1SpgoU6KhZt7 .node polygon,#mermaid-svg-zU2h1SpgoU6KhZt7 .node path{fill:#ECECFF;stroke:#9370DB;stroke-width:1px;}#mermaid-svg-zU2h1SpgoU6KhZt7 .rough-node .label text,#mermaid-svg-zU2h1SpgoU6KhZt7 .node .label text,#mermaid-svg-zU2h1SpgoU6KhZt7 .image-shape .label,#mermaid-svg-zU2h1SpgoU6KhZt7 .icon-shape .label{text-anchor:middle;}#mermaid-svg-zU2h1SpgoU6KhZt7 .node .katex path{fill:#000;stroke:#000;stroke-width:1px;}#mermaid-svg-zU2h1SpgoU6KhZt7 .rough-node .label,#mermaid-svg-zU2h1SpgoU6KhZt7 .node .label,#mermaid-svg-zU2h1SpgoU6KhZt7 .image-shape .label,#mermaid-svg-zU2h1SpgoU6KhZt7 .icon-shape .label{text-align:center;}#mermaid-svg-zU2h1SpgoU6KhZt7 .node.clickable{cursor:pointer;}#mermaid-svg-zU2h1SpgoU6KhZt7 .root .anchor path{fill:#333333!important;stroke-width:0;stroke:#333333;}#mermaid-svg-zU2h1SpgoU6KhZt7 .arrowheadPath{fill:#333333;}#mermaid-svg-zU2h1SpgoU6KhZt7 .edgePath .path{stroke:#333333;stroke-width:2.0px;}#mermaid-svg-zU2h1SpgoU6KhZt7 .flowchart-link{stroke:#333333;fill:none;}#mermaid-svg-zU2h1SpgoU6KhZt7 .edgeLabel{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-zU2h1SpgoU6KhZt7 .edgeLabel p{background-color:rgba(232,232,232, 0.8);}#mermaid-svg-zU2h1SpgoU6KhZt7 .edgeLabel rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-zU2h1SpgoU6KhZt7 .labelBkg{background-color:rgba(232, 232, 232, 0.5);}#mermaid-svg-zU2h1SpgoU6KhZt7 .cluster rect{fill:#ffffde;stroke:#aaaa33;stroke-width:1px;}#mermaid-svg-zU2h1SpgoU6KhZt7 .cluster text{fill:#333;}#mermaid-svg-zU2h1SpgoU6KhZt7 .cluster span{color:#333;}#mermaid-svg-zU2h1SpgoU6KhZt7 div.mermaidTooltip{position:absolute;text-align:center;max-width:200px;padding:2px;font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:12px;background:hsl(80, 100%, 96.2745098039%);border:1px solid #aaaa33;border-radius:2px;pointer-events:none;z-index:100;}#mermaid-svg-zU2h1SpgoU6KhZt7 .flowchartTitleText{text-anchor:middle;font-size:18px;fill:#333;}#mermaid-svg-zU2h1SpgoU6KhZt7 rect.text{fill:none;stroke-width:0;}#mermaid-svg-zU2h1SpgoU6KhZt7 .icon-shape,#mermaid-svg-zU2h1SpgoU6KhZt7 .image-shape{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-zU2h1SpgoU6KhZt7 .icon-shape p,#mermaid-svg-zU2h1SpgoU6KhZt7 .image-shape p{background-color:rgba(232,232,232, 0.8);padding:2px;}#mermaid-svg-zU2h1SpgoU6KhZt7 .icon-shape .label rect,#mermaid-svg-zU2h1SpgoU6KhZt7 .image-shape .label rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-zU2h1SpgoU6KhZt7 .label-icon{display:inline-block;height:1em;overflow:visible;vertical-align:-0.125em;}#mermaid-svg-zU2h1SpgoU6KhZt7 .node .label-icon path{fill:currentColor;stroke:revert;stroke-width:revert;}#mermaid-svg-zU2h1SpgoU6KhZt7 :root{--mermaid-font-family:"trebuchet ms",verdana,arial,sans-serif;} @Async 异步写入
发布时 idWorker 生成 pid
pid 逗号串
动态 pid 池
day03 MongoDB 基础

实体映射 / MongoTemplate / 跨存储组装
昨日:发布动态
movement 动态表
movement_timeline 时间线表
friend 好友表
今日:好友动态

friendId=我 查时间线
推荐系统离线计算
Redis

MOVEMENTS_RECOMMEND_用户id
今日:推荐动态

pid 内存分页,缺失时 $sample 兜底
今日:单条动态

详情页与评论列表的前置

读法:Mongo 能力是所有功能的地基;好友动态依赖昨天异步写入的时间线数据,没有发布动态,就没有东西可查;推荐动态依赖推荐系统提前写进 Redis 的 pid 列表;单条动态排在最后,因为它服务于从列表点进详情的场景,下一章的评论列表也要先拿它取到动态。

本章代码产出预告

实读 04-圈子功能实现/01-代码/mf 工程,与圈子动态相关的类分布如下(今天的新增以方法为主,集中在 MovementApi 一侧):

tree 复制代码
mf
├── mf-app-server
│   ├── src/main/java/com/mf/server/controller/MovementController.java
│   │   └── 今天新增:GET /movements、GET /movements/recommend、GET /movements/{id}
│   ├── src/main/java/com/mf/server/service/MovementService.java
│   │   └── 今天新增:findFriendMovements、findRecommendMovements、findById、公共组装方法 getPageResult
│   └── src/test/java/com/wb/test/MovementApiTest.java
├── mf-dubbo
│   ├── mf-dubbo-interface/src/main/java/com/mf/dubbo/api/MovementApi.java
│   │   └── 今天新增:findFriendMovements、findMovementsByPids、randomMovements、findById
│   └── mf-dubbo-mongo/src/main/java/com/mf/dubbo
│       ├── api/MovementApiImpl.java        # MongoTemplate 查询落地,含 $sample 随机取样
│       └── utils/TimeLineService.java      # 昨天的异步写时间线,今天读侧的数据来源
├── mf-model/src/main/java/com/mf/model
│   ├── mongo/Movement.java                 # 动态表实体,pid 字段是推荐系统的关联键
│   ├── mongo/MovementTimeLine.java         # 时间线实体,friendId 是好友动态的查询条件
│   └── vo/MovementsVo.java                 # 三个查询入口共用的返回 VO
└── mf-commons/src/main/java/com/mf/commons/utils
    └── Constants.java                      # 今天新增 MOVEMENTS_RECOMMEND 常量

学习建议

  • 好友动态是异步改造的读侧回报。昨天发布动态时把时间线写入拆成异步,图的是发布接口快;今天查好友动态只要一张时间线表加一次批量 in 查询,不用遍历好友。写侧拆分换来了读侧简单,这条「写时多写一份、读时少扫多表」的取舍在时间线类业务里反复出现,值得对照代码把两条链路都走一遍。
  • 推荐动态是「读多写多」场景的典型取巧 :推荐结果一次性从 Redis 取出,翻页在应用内存里 skip / limit,不再反复打 Redis;没有推荐数据时用 $sample 随机取样兜底,保证页面永远有内容。代价是 pid 列表长了以后内存分页有浪费,这也是后面章节引出更完整方案的原因。
  • 三个查询共用一套组装逻辑 。getPageResult 抽出来复用,MovementsVo.init 负责动态数据 + 用户详情的拼装。看到重复的组装代码就往公共方法收,别复制三份。

常见误解澄清

常见误解 实际情况
查好友动态要遍历每个好友,逐个查他们发布的 movement 只查自己的 movement_timeline 表(friendId = 我),发动态时已经替你分发好了
推荐动态的结果在 MongoDB 里按分数排好队 推荐结果在 Redis 里,是一串 pid 文本;MongoDB 只有动态原始数据,分页逻辑要自己写
分页一律丢给数据库 skip / limit Redis pid 列表的分页在应用内存里用流式 skip / limit 完成,两者不能混
查询单条动态是个独立功能点 它是详情页和评论列表的前置接口,下一章评论功能第一步就是拿动态 id 查详情
时间线表是冗余,删掉也能跑 功能能跑,但查询退化成全表扫好友动态;分片场景下按 friendId 分片的设计也一并失效

API 速览

接口 方法 参数 说明
/movements GET page、pagesize 好友动态分页
/movements/recommend GET page、pagesize 推荐动态分页
/movements/{id} GET 路径参数动态 id 单条动态详情
/movements/all GET userId、page、pagesize 我的动态(昨天已完成)

官方文档

总结

  • 昨天的产出(三表设计、异步写时间线、Mongo 分页组装模板)构成了今天全部查询的地基,尤其时间线表是好友动态的直接数据源。
  • 今天的顺序是好友动态 → 推荐动态 → 单条动态:前两者数据源不同、分页方式不同,但共用一套 VO 组装;单条动态为下一章互动功能铺路。
  • 做的时候抓住一条主线:同样是「查动态列表」,数据放哪(Mongo / Redis)、怎么分页(数据库 / 内存)决定了每份实现长什么样。
相关推荐
2601_967979121 小时前
0 到 1 学爬虫:用 Python 爬取网页数据指南一(附实战案例)
爬虫·python·实战案例·html解析·网页数据
云和数据.ChenGuang1 小时前
光哥说Python 异常深度解析:从异常模型到工程化实践
java·前端·人工智能·python·langchain
SelectDB1 小时前
云数仓月度账单总是超预算:Apache Doris 与 Snowflake 五类成本项拆解与对账脚本
python
迪菲赫尔曼2 小时前
【基础篇 12】把环境与源码接起来:检查 GPU,再安装服务器上的项目
运维·服务器·开发语言·人工智能·python·yolo·目标检测
SL_staff2 小时前
APS动态重排实践:在多品种小批量场景下实现5分钟插单响应
java·开源·产品
2601_962885722 小时前
2026 Python 股票数据接口选型指南(横评 + 决策树)
开发语言·python·区块链
龙腾AI白云2 小时前
大模型检索增强生成(RAG)的技术原理与架构设计
数据库·python·机器学习·知识图谱
captain3762 小时前
Spring MVC(4)-响应
java·spring boot·后端
fanruitian2 小时前
python 可视化 panda seaborn matplotlib.pyplot
开发语言·python·matplotlib