ARTICLE · INTELLIGENCE

战地情报 · 详情页

来自尧图项目组的一线实战观察与深度解析

Java 项目实战: 交友项目 - MongoDB基础总结与自检清单

Java 项目实战: 交友项目 - MongoDB基础总结与自检清单

概述

我们要把项目从「MySQL 单存储」升级成了「MySQL + MongoDB 双存储」:通用设置和黑名单继续走mf-dubbo-db,今日佳人把缘分值数据首次存进了 Mongo,mf-dubbo-mongo模块从零搭起来并跑通了第一条 Dubbo 链路。这里不重复细节,只做三件事:把整章串成一张知识地图、沉淀可复用的套路清单、给出可勾选的自检项。

纲要

  • 本章复盘:四个功能块各自解决了什么问题,对应已交付的哪些篇目
  • 知识地图:从 MySQL 域到 Mongo 域的完整链路(Mermaid)
  • 可复用套路清单:新存储接入三步、MyBatis-Plus ↔ MongoTemplate 心智迁移、跨存储组装、条件下沉、insert vs save
  • 关键坑回顾表:全章最容易翻车的十来个点
  • 自检清单:每项都给出「怎么验」
  • 练习建议:四个可验证的技术任务(含作业改造)
  • 常见误解澄清 + 下一章预告

本章做了什么:四块内容的复盘

按讲义的推进顺序,本章四个功能块和已交付篇目的对应关系:

功能块存储归属解决的问题对应篇目
通用设置(通知开关 + 陌生人问题)MySQL单表 CRUD 的「存在则更新、不存在则保存」二段式写法MongoDB:引入前置
黑名单分页 + 移除MySQL子查询跨表分页(tb_user_info where id in (select ...)),MyBatis-Plus 自定义分页MongoDB:引入前置
MongoDB 概念 + 命令行Mongo 本身体系结构、BSON、insert/update/remove/find、索引与 explainMongoDB:介绍MongoDB的体系结构MongDB的入门:命令行操作
SpringData-Mongo + 今日佳人Mongo + MySQL 组装环境搭建、MongoTemplate CRUD、queryWithMaxScore、跨存储组装 VOSpringData-Mongo:环境搭建SpringData-Mongo:CRUD操作上/下今日佳人三篇、推荐好友列表:需求分析

本章结束时工程的形态(实读01-代码/mf快照):

mf ├── mf-app-server # 应用层:SettingsController / mfService(跨存储组装) ├── mf-model │ └── src/main/java/com/mf/model │ ├── domain # MySQL 实体:Settings / Question / BlackList / UserInfo │ ├── mongo │ │ └── RecommendUser.java # 本章新增:@Document(collection = "recommend_user") │ └── vo # SettingsVo / PageResult / TodayBest └── mf-dubbo ├── mf-dubbo-interface # 契约:SettingsApi / QuestionApi / BlackListApi / RecommendUserApi ├── mf-dubbo-db # MySQL 实现:三套 ApiImpl + Mapper,端口不变 └── mf-dubbo-mongo # 本章新增:RecommendUserApiImpl,端口 18082,dubbo 20882

有一个贯穿全章的结构性变化值得单独记一笔:mf-model引入spring-boot-starter-data-mongodb之后,所有依赖它的模块都具备了 Mongo 自动装配能力,mf-dubbo-db和mf-app-server明明不连 Mongo,启动时却会去找localhost:27017然后报错。解法是这两个模块的引导类排除MongoAutoConfiguration和MongoDataAutoConfiguration。这个「依赖传递引入了不想要的自动配置」是 Spring Boot 项目的经典问题,记住现象和排除写法即可,下章还会遇到类似的事。

本章知识地图

应用层组装(mf-app-server)

MongoDB 域

MySQL 域(mf-dubbo-db)

Dubbo: queryWithMaxScore

Dubbo

Dubbo

缓存层

通用设置
SettingsApi / QuestionApi

黑名单分页
BlackListApi + 子查询

概念与体系结构
document / collection / BSON

命令行
insert / update / find / 索引

SpringData-Mongo
MongoTemplate + @Document

今日佳人 /mf/todayBest
Mongo 查缘分 + MySQL 查资料

推荐好友列表分页
批查 + 兜底

mf-dubbo-mongo
RecommendUserApiImpl

Redis

读这张图的角度:左上是存量能力(MySQL 域),中间是本章新练的内功(概念 → 命令行 → SpringData-Mongo),右下是内功第一次落地(mf-dubbo-mongo模块 + 跨存储组装)。下一章的圈子功能,就是把中间这条 Mongo 链路全面铺开。

可复用套路清单

这一节是全章的沉淀,五个套路在后续章节反复出现,值得逐条过一遍。

套路一:新存储接入三步

本章接入 MongoDB 的过程可以抽象成固定三步,以后接入任何新存储(Redis、ES、OSS)都是这个节奏:

步骤本章落地对应的 MySQL 侧(完全对称)
依赖 + 连接配置mf-dubbo-mongo的 pom 加 starter,yml 配spring.data.mongodb.uri,端口 18082mf-dubbo-db的 datasource 配置
实体注解映射RecommendUser标@Document(collection = "recommend_user")、@Id实体标@TableName、@TableId
独立域服务模块mf-dubbo-mongo放@DubboService实现,包扫描com.mf.dubbo.apimf-dubbo-db同构

第三步的关键词是存储可插拔:接口契约放在mf-dubbo-interface,实现放在哪个模块,消费方完全不感知。哪天把黑名单从 MySQL 迁到 Mongo(练习建议里有这个任务),mf-app-server一行代码都不用改。

套路二:MyBatis-Plus ↔ MongoTemplate 心智迁移表

写过 MyBatis-Plus 的人上手 MongoTemplate,真正要做的只是「翻译」:

操作意图MyBatis-Plus 写法MongoTemplate 写法
实体→表映射@TableName("tb_settings")@Document(collection = "recommend_user")
主键@TableId,Long自增@Id,ObjectId(12 字节,ObjectId.get()生成)
字段改名@TableField("user_id")@Field("username")
等值条件qw.eq("user_id", userId)Criteria.where("toUserId").is(toUserId)
条件容器QueryWrapperQuery.query(criteria)
排序qw.orderByDesc("score").with(Sort.by(Sort.Order.desc("score")))
分页Page对象 + 分页插件手动count()+.skip((page-1)*size).limit(size)
查一条selectOne(qw)findOne(query, Class)
新增insert(entity)insert(entity)或save(entity)(语义有差别,见套路五)
删除mapper.delete(qw)remove(query, Class)

分页这一行差异最大:MyBatis-Plus 有分页插件兜底,Mongo 侧的 skip/limit 和 count 是自己拼的,参数换算错一个符号就翻车(坑表里有)。

套路三:跨存储组装模式

Mongo 存关系数据(缘分值、后续的点赞评论),MySQL 存展示资料(昵称头像),应用层组装。本章落地了三次,形态略有不同:

场景Mongo 侧查询MySQL 侧查询组装要点
今日佳人queryWithMaxScore:单对象,score 倒序取第一条userInfoApi.findById单查冷启动兜底:查不到就伪造userId=1, score=99
黑名单分页—(全在 MySQL)子查询分页一次查齐条件下沉到 SQL,不在 Java 层过滤
推荐好友列表按 toUserId 查一批 RecommendUser按用户 id 批量查 UserInfo批查防 N+1:先收集 id 再findByIds,不要循环单查;缺失兜底防 NPE

两次跨存储调用之间没有事务保证,这是这个模式的固有代价,所以 Mongo 里只放「价值较低、可再生成」的关系数据,核心账户资料仍然留在 MySQL。这个存储分工原则比代码本身更值得记住。

套路四:条件下沉 SQL

跨存储、跨表的过滤条件,必须下沉到查询层(SQL 的 where 子查询、Mongo 的 Criteria),不能查回来在 Java 里filter。黑名单分页是最典型的反例教材:如果先查tb_black_list全量,再在 Java 里按页切割,counts(总数)和pages(总页数)就算不对了,前端分页控件直接失真。过滤逻辑和分页统计必须发生在同一层。

套路五:insert vs save 语义

mongoTemplate的两个保存方法长得像,语义不同:

方法已存在相同_id时典型场景
insert抛DuplicateKeyException新增语义,主键由系统生成
save整文档覆盖(相当于 upsert)保存语义,不在乎是新增还是更新

MySQL 侧对应的「存在则更新、不存在则保存」二段式(saveQuestion里先findByUserId再分支),在 Mongo 侧可以用save一步替代,但要清楚代价:save 是整文档替换,字段会被清掉,和 update 语句漏写$set是同一种事故(见坑表第一行)。

关键坑回顾表

全章最容易翻车的点,按出现顺序回顾:

坑现象正确姿势
update 漏写$setdb.user.update({id:1},{age:25})把整个文档替换成只剩 age部分更新必须{$set:{...}};Java 侧同理,save会整文档覆盖
remove({})空条件删光整个集合,无确认弹窗删除必带条件;清空集合用drop()并想清楚
字段名写错(大小写)Criteria 里写touserid,静默查不到,不报错Mongo 无 schema 校验,字段名以插入时为准;排查手法:先find()看原始文档
skip/limit 换算错误第 2 页查出了第 1 页的数据skip((page-1)*size),注意 page 从 1 开始
userId/toUserId 写反查「我的今日佳人」查出别人的缘分值约定:toUserId是「看的人」,userId是「被推荐的人」
recommend_user 缺唯一索引同一对用户重复插入推荐记录(这个坑黑名单场景已经是第三次出现了)业务唯一键建唯一索引:db.recommend_user.createIndex({userId:1,toUserId:1},{unique:true})
兜底数据 NPE冷启动伪造了 RecommendUser,但userInfoApi.findById(1L)查不到 id=1 的资料兜底要兜两层:推荐数据兜了,资料也要有真实可用的用户
Double 截断score=99.6经longValue()变 99,前端显示少 0.6 缘分值明确取整策略:longValue()是截断不是四舍五入,需要精度就Math.round
Java 层过滤分页 total 与实际条数对不上过滤条件下沉到 SQL/Criteria(套路四)
分页插件未配置selectPage静默查全量,数据小的时候根本发现不了mf-dubbo-db引导类注册PaginationInnerInterceptor
Mongo 自动装配误伤mf-dubbo-db/mf-app-server启动报连接localhost:27017失败引导类exclude = {MongoAutoConfiguration.class, MongoDataAutoConfiguration.class}

自检清单

部署或复习完本章,逐项勾掉:

检查项怎么验
Mongo 容器能连docker ps看到 27017 端口映射;shell 里show dbs有响应
mf-dubbo-mongo注册成功Nacos 控制台服务列表出现mf-dubbo-mongo,端口 18082
实体映射正确shell 执行db.recommend_user.find().pretty(),字段名与RecommendUser的属性一致(驼峰,不是下划线)
CRUD 走通测试类依次跑 save / find / update / remove,每步用find()核对结果
今日佳人返回缘分值带 token 请求GET /mf/todayBest,响应里有fateValue且非 0
冷启动兜底生效清空recommend_user集合再请求,接口不报错,返回 userId=1 的默认资料和缘分值 99
通用设置二段式保存同一用户连续两次设置陌生人问题,tb_question里始终只有一条记录
黑名单分页正确拉黑 3 人后请求GET /settings/blacklist?page=1&size=2,counts=3、pages=2

练习建议

原课程的作业偏「照流程再走一遍」,这里改写成四个可验证的技术任务,难度递增:

任务一:给 recommend_user 的 toUserId 建索引

queryWithMaxScore每次首页刷新都会执行,但recommend_user目前只有_id索引,按toUserId查必然全集合扫描。练习内容:

// 建(toUserId, score)复合索引,查询条件与排序一次覆盖db.recommend_user.createIndex({toUserId:1,score:-1})

验证:建索引前后分别执行db.recommend_user.find({toUserId: 1002}).sort({score: -1}).explain(),对比winningPlan.stage从COLLSCAN变为IXSCAN。顺手灌 10 万条测试数据(for循环插入,命令行篇章有模板),感受数量级差异。

任务二:todayBest 加 Redis 缓存

今日佳人是首页首个请求,每次都跨两次 Dubbo 调用不值当。设计要点:key 用today_best::{userId},value 存TodayBest序列化结果,过期时间 5~10 分钟。要思考的问题是:推荐数据更新时缓存怎么办,先做「短过期时间自然淘汰」这个最简方案,把一致性方案的取舍留成注释。

任务三:黑名单改 Mongo 实现

新建black_list集合,BlackListApi契约不动,在mf-dubbo-mongo里写一套新实现。做完对比:MySQL 版靠一条子查询 SQL 搞定分页,Mongo 版不支持 join,得先分页查black_list、再批查user_info组装,多了一次跨存储往返,换来了关系数据迁出主库。这个取舍没有标准答案,写清楚你选哪个、为什么。

任务四:兜底数据配置化降级

mfService.todayBest()里硬编码的userId=1L, score=99d有两个问题:id=1 的用户可能不存在(NPE 风险),换兜底用户要改代码重新发版。改造方向:抽成配置项(@ConfigurationProperties或放 Nacos),再考虑兜底用户池(随机取一个真实存在的「官方账号」)。验收标准:把配置里的 userId 改成一个不存在的值,服务不崩、接口返回明确降级标记。

原作业里的「修改手机号」功能本质上是一次登录链路复用练习:发短信 → 校验验证码 → 保存手机号,三步分别对应登录功能已写过的组件,适合作为动手热身,此处不再展开。

常见误解澄清

误解事实
「引入 MongoDB 是为了替代 MySQL」两者分工:MySQL 存核心账户资料,Mongo 存海量、低价值密度的关系数据(推荐、后续的动态点赞评论),Redis 做缓存。选型依据是数据特点,不是新旧
「Mongo 没有 schema,所以不用设计字段」恰恰相反:没有 schema 校验意味着写错字段名不报错、静默查不到。字段命名约定(统一驼峰)要靠代码纪律保证
「Mongo 不支持索引和执行计划」createIndex/getIndexes/explain全套都有,IXSCAN与COLLSCAN的区别和 MySQL 的索引扫描/全表扫描一一对应
「save就是更新,insert就是新增」save是「存在则整文档覆盖,不存在则插入」,两个方法都能新增;差别在_id冲突时的行为
「实体类加@Document只影响 Mongo 模块」依赖在mf-model,所有引用它的模块都会触发 Mongo 自动装配,不连 Mongo 的模块必须显式 exclude
「分页插件配一次,所有模块都生效」插件是mf-dubbo-db引导类里的@Bean,只在配置了的模块生效;新模块要分页得重新注册

下一章预告

第四章进入圈子功能:发布动态、浏览好友动态、点赞、评论。这一章的价值密度很高,动态、评论、点赞全面使用 MongoDB存储,本章练的@Document映射、MongoTemplate 查询、mf-dubbo-mongo模块扩展会一次性铺开。工程实读也能佐证:本章mf-dubbo-mongo的快照里已经出现了UserLike相关的 import,说明圈子部分的代码与推荐模块同源生长。套路三的跨存储组装在圈子场景会升级成「动态(Mongo)+ 用户资料(MySQL)+ 点赞评论状态(Mongo)」的三方组装,批查防 N+1 会从建议变成刚需。

总结

  • 本章的工程成果是双存储格局落地:mf-dubbo-db(通用设置/黑名单)与mf-dubbo-mongo(今日佳人/推荐)并列,接口契约统一放mf-dubbo-interface,消费方对存储无感知
  • 最值得带走的不是 MongoTemplate 的 API,而是五个套路:新存储接入三步、MyBatis-Plus 心智迁移、跨存储组装(批查 + 兜底)、条件下沉、insert vs save 语义
  • 坑的共性也值得留意:Mongo 的「静默失败」(字段名错、分页参数错、无索引)比报错更危险,自检清单里每一项的「怎么验」就是对抗静默失败的手段
  • 四个练习任务分别对应索引优化、缓存、存储迁移取舍、配置化降级,都是后续章节会真实用到的能力
RELATED READING

延伸阅读

更多一线实战笔记与深度复盘,助您持续精进