
本文目录今日学习内容MongoDB vs MySQL关系型与文档型数据库对比MongoDB 核心概念与数据模型MongoDB CRUD 操作实战MongoDB 聚合管道Aggregation PipelineSpring Data MongoDB 集成实战Docker 镜像构建最佳实践Docker 私有仓库部署踩坑记录收获总结明日计划今日学习内容今天是 Day 17继续在第三阶段「AI 应用 Web 开发」中前行。昨天我们学了 Seata 分布式事务、Spring Cloud Alibaba AI 集成和 Docker Compose 微服务编排今天按照计划重点攻克两个核心主题MongoDB 文档数据库数据模型设计、BSON 文档结构、CRUD 操作、聚合管道$match / $group / $lookup彻底搞懂 NoSQL 的思维方式Docker 镜像构建与私有仓库多阶段构建优化镜像体积、Dockerfile 最佳实践、Harbor 私有仓库搭建与镜像推送拉取前置知识回顾Day 11 中我们搭建了 Spring Boot 后端项目并使用 MyBatis-Plus 操作 MySQLDay 16 中我们用 Docker Compose 编排了整套微服务环境。今天将 MongoDB 作为 MySQL 的补充存储方案引入同时将昨天编写的 Spring Boot 服务正式容器化。MongoDB vs MySQL关系型与文档型数据库对比在微服务架构中不是所有数据都适合用关系型数据库存储。MongoDB 作为最流行的文档数据库在很多场景下比 MySQL 更灵活、更高性能。核心区别一览我把 MongoDB 和 MySQL 的核心差异整理成了一张对比表维度MySQL关系型MongoDB文档型数据结构表Table、行Row、列Column集合Collection、文档Document、字段FieldSchema固定 Schema提前建表定义列类型Schema-less无模式字段灵活增减数据格式关系型行列表结构BSONBinary JSON支持嵌套/数组关联关系JOIN 多表关联强一致性内嵌文档 / $lookup 关联读优先扩展方式垂直扩展加硬件分库分表复杂水平扩展Sharding 分片天然支持事务支持完整 ACID 事务4.0 支持多文档事务性能有代价适用场景订单/交易/财务等强一致性场景内容管理/日志/IoT/实时分析选型建议在 AI 全栈项目中MySQL 存储核心业务数据用户/订单/权限MongoDB 存储非结构化/半结构化数据文章内容/对话记录/IoT 传感器数据/日志。两者互补不是替代关系。MongoDB 核心概念与数据模型MongoDB 的设计哲学和 MySQL 完全不同理解它的核心概念是写好代码的前提。数据库 集合 文档MongoDB 的层级结构是Database → Collection → Document类比 MySQL 就是Database → Table → Row。但关键区别在于文档是 BSON 格式Binary JSON天然支持嵌套和数组。这意味着你可以在一个文档里直接存储复杂的数据结构而不需要像 MySQL 那样拆分成多张表再 JOIN。文档结构示例比如一篇博客文章在 MySQL 里需要 article article_tag article_category 三张表在 MongoDB 里一个文档搞定示例一篇博客文章的 MongoDB 文档BSON / JSON{ _id: ObjectId(64a1b2c3d4e5f6g7h8i9j0k1), title: MongoDB 入门指南, content: MongoDB 是一款开源的文档数据库..., author: { id: 1001, name: 张三, avatar: https://cdn.example.com/avatar/1001.jpg }, tags: [MongoDB, NoSQL, 数据库], status: published, viewCount: 1523, comments: [ { userId: 2001, content: 写得很棒, createdAt: ISODate(2026-07-25T10:30:00Z) } ], createdAt: ISODate(2026-07-25T08:00:00Z), updatedAt: ISODate(2026-07-25T12:00:00Z) }看到没author 信息直接嵌套在文档里内嵌文档tags 是数组comments 也是嵌套数组。这种读优先的设计哲学避免了大量的 JOIN 操作。_id 字段与 ObjectIdMongoDB 每个文档都必须有一个_id字段作为主键。如果不手动指定MongoDB 会自动生成一个ObjectId。ObjectId 是一个 12 字节的 BSON 值前 4 字节时间戳文档创建时间后 3 字节机器标识再 2 字节进程 ID最后 3 字节计数器同一秒内的递增值ObjectId 的天然优势自带时间戳信息可以按时间排序分布式环境下几乎不会重复比 UUID 更紧凑12 字节 vs 36 字节。在 Spring Data MongoDB 中也可以使用 String 类型的 UUID 作为自定义 _id。数据模型设计原则MongoDB 的数据模型设计有一套反范式的思维和 MySQL 的三范式截然不同内嵌 vs 引用数据一起读就内嵌如文章 作者信息数据独立更新就引用存 author_id 关联查询避免无限制增长评论数成千上万时不要全部内嵌在文章文档里应该单独建 comments 集合频繁访问的字段放顶层MongoDB 支持索引顶层字段嵌套太深查询效率会降低多对多关系用数组引用文章的 tags 数组存储 tag_id 列表MongoDB CRUD 操作实战接下来用 MongoDB Shellmongosh演示核心的 CRUD 操作。假设我们有一个博客系统的articles集合。Docker 安装 MongoDB启动 MongoDB 容器Shell# 拉取 MongoDB 7.0 镜像并启动 docker run -d \ --name mongodb \ -p 27017:27017 \ -e MONGO_INITDB_ROOT_USERNAMEadmin \ -e MONGO_INITDB_ROOT_PASSWORD123456 \ -v mongodb_data:/data/db \ mongo:7.0 # 进入 mongosh 命令行 docker exec -it mongodb mongosh -u admin -p 123456 # 创建业务数据库 use blog_system插入文档Create插入单篇和多篇文章MongoDB Shell# 插入单篇文档 db.articles.insertOne({ title: MongoDB 入门指南, content: MongoDB 是一款开源的文档数据库..., authorId: 1001, tags: [MongoDB, NoSQL], status: draft, viewCount: 0, createdAt: new Date(), updatedAt: new Date() }) # 批量插入 db.articles.insertMany([ { title: Redis 缓存实战, content: Redis 是一款高性能内存数据库..., authorId: 1001, tags: [Redis, 缓存], status: published, viewCount: 234, createdAt: new Date() }, { title: Spring Boot 微服务架构, content: 微服务架构是一种将应用拆分为..., authorId: 1002, tags: [Spring Boot, 微服务], status: published, viewCount: 567, createdAt: new Date() } ])查询文档Read基础查询与条件过滤MongoDB Shell# 查询所有已发布文章 db.articles.find({ status: published }) # 多条件查询已发布 阅读量 300 db.articles.find({ status: published, viewCount: { $gt: 300 } }) # 标签中包含 Redis 的文章数组查询 db.articles.find({ tags: Redis }) # 模糊搜索标题正则匹配 db.articles.find({ title: /Spring/i }) # 排序 限制返回条数 db.articles.find({ status: published }) .sort({ viewCount: -1 }) # 按阅读量降序 .limit(10) # 只返回前 10 条 .skip(0) # 跳过偏移量分页用 # 投影只返回 title 和 viewCount 字段 db.articles.find( { status: published }, { title: 1, viewCount: 1, _id: 0 } ) # 统计已发布文章总数 db.articles.countDocuments({ status: published })更新文档Update更新操作MongoDB Shell# 更新单篇发布文章$set 修改指定字段 db.articles.updateOne( { _id: ObjectId(...) }, { $set: { status: published, updatedAt: new Date() } } ) # 阅读量自增$inc 原子递增 db.articles.updateOne( { _id: ObjectId(...) }, { $inc: { viewCount: 1 } } ) # 添加标签$push 往数组追加元素 db.articles.updateOne( { _id: ObjectId(...) }, { $push: { tags: 实战 } } ) # 批量更新将所有 draft 状态改为 archived db.articles.updateMany( { status: draft }, { $set: { status: archived } } ) # upsert存在则更新不存在则插入 db.articles.updateOne( { title: 新文章标题 }, { $set: { content: 内容..., status: draft } }, { upsert: true } )删除文档Delete删除操作MongoDB Shell# 删除单篇 db.articles.deleteOne({ _id: ObjectId(...) }) # 批量删除删除所有 archived 状态的文章 db.articles.deleteMany({ status: archived })更新操作符速查$set设置字段值、$inc数值递增、$push/$pull数组增删、$addToSet数组去重添加、$unset删除字段、$rename重命名字段。这些操作符是 MongoDB 操作的武器库一定要熟练掌握。MongoDB 聚合管道Aggregation Pipeline聚合管道是 MongoDB 最强大的功能之一相当于 SQL 中的 GROUP BY JOIN HAVING 的组合但灵活度远超 SQL。管道阶段概念聚合管道是一个多阶段的数据处理流水线文档依次经过每个阶段每个阶段对文档进行某种转换聚合管道数据流示意图概念原始文档 → $match → $group → $sort → $limit → 结果 类比 SQL $match ≈ WHERE 过滤 $group ≈ GROUP BY 分组聚合 $sort ≈ ORDER BY 排序 $limit ≈ LIMIT 截取 $lookup ≈ LEFT JOIN 关联查询 $project ≈ SELECT 字段 投影 $unwind ≈ 展开数组 行展开实战文章统计分析聚合管道实战MongoDB Shell# 统计每位作者的已发布文章数量和总阅读量 db.articles.aggregate([ # 阶段1过滤出已发布文章 { $match: { status: published } }, # 阶段2按 authorId 分组统计 count 和 totalViews { $group: { _id: $authorId, articleCount: { $sum: 1 }, totalViews: { $sum: $viewCount }, avgViews: { $avg: $viewCount } } }, # 阶段3按文章数降序排列 { $sort: { articleCount: -1 } }, # 阶段4只取 Top 5 { $limit: 5 } ]) # 标签统计统计每个标签的文章数 db.articles.aggregate([ # $unwind 将 tags 数组展开为多条文档 { $unwind: $tags }, # 按标签分组统计 { $group: { _id: $tags, count: { $sum: 1 } } }, { $sort: { count: -1 } } ])$lookup 关联查询$lookup 多集合关联MongoDB Shell# 文章关联用户表获取作者详细信息 db.articles.aggregate([ { $match: { status: published } }, { $lookup: { from: users, # 关联的集合 localField: authorId, # 当前集合的字段 foreignField: _id, # 关联集合的字段 as: authorInfo # 输出字段名数组 } }, { $unwind: $authorInfo }, # 展开为数组只有一条的情况 { $project: { title: 1, viewCount: 1, authorName: $authorInfo.name, authorAvatar: $authorInfo.avatar } } ])Spring Data MongoDB 集成实战学会了 MongoDB Shell 操作接下来把 MongoDB 集成到 Spring Boot 项目中。Spring Data MongoDB 提供了和 MyBatis-Plus 一样便捷的 Repository 模式。依赖引入与配置pom.xml 依赖XML!-- Spring Data MongoDB -- dependency groupIdorg.springframework.boot/groupId artifactIdspring-boot-starter-data-mongodb/artifactId /dependencyapplication.yml 配置YAMLspring: data: mongodb: uri: mongodb://admin:123456localhost:27017/blog_system?authSourceadmin # 或者分开写 # host: localhost # port: 27017 # database: blog_system # username: admin # password: 123456实体类定义Article.java — MongoDB 文档实体Javapackage com.example.blog.entity; import org.springframework.data.annotation.Id; import org.springframework.data.mongodb.core.mapping.Document; import org.springframework.data.mongodb.core.mapping.Field; import java.time.LocalDateTime; import java.util.List; Document(collection articles) public class Article { Id private String id; // MongoDB 的 _id用 String 接收 private String title; private String content; private Long authorId; private ListString tags; private String status; // draft / published / archived private Long viewCount 0L; Field(name created_at) private LocalDateTime createdAt; Field(name updated_at) private LocalDateTime updatedAt; // getter / setter 省略实际开发推荐用 Lombok Data }Repository 层ArticleRepository.javaJavapackage com.example.blog.repository; import com.example.blog.entity.Article; import org.springframework.data.domain.Page; import org.springframework.data.domain.Pageable; import org.springframework.data.mongodb.repository.MongoRepository; import java.util.List; public interface ArticleRepository extends MongoRepositoryArticle, String { // 方法名查询Spring Data 自动实现 ListArticle findByStatus(String status); ListArticle findByTagsContaining(String tag); PageArticle findByStatus(String status, Pageable pageable); // 支持正则匹配的模糊搜索 ListArticle findByTitleRegex(String regex); }Service 层与聚合查询ArticleService.java — 核心业务逻辑Javapackage com.example.blog.service; import com.example.blog.entity.Article; import com.example.blog.repository.ArticleRepository; import org.springframework.data.domain.Page; import org.springframework.data.domain.PageRequest; import org.springframework.data.domain.Sort; import org.springframework.data.mongodb.core.MongoTemplate; import org.springframework.data.mongodb.core.aggregation.*; import org.springframework.stereotype.Service; import java.time.LocalDateTime; import java.util.List; Service public class ArticleService { private final ArticleRepository articleRepository; private final MongoTemplate mongoTemplate; public ArticleService(ArticleRepository articleRepository, MongoTemplate mongoTemplate) { this.articleRepository articleRepository; this.mongoTemplate mongoTemplate; } // 发布文章 public Article publishArticle(String id) { Article article articleRepository.findById(id) .orElseThrow(() - new RuntimeException(文章不存在)); article.setStatus(published); article.setUpdatedAt(LocalDateTime.now()); return articleRepository.save(article); } // 阅读量递增 public void incrementViewCount(String id) { mongoTemplate.updateFirst( Query.query(Criteria.where(_id).is(id)), new Update().inc(viewCount, 1), Article.class ); } // 分页查询已发布文章 public PageArticle listPublished(int page, int size) { Pageable pageable PageRequest.of( page, size, Sort.by(Sort.Direction.DESC, createdAt) ); return articleRepository.findByStatus(published, pageable); } // 聚合统计按标签统计文章数 public ListTagStat getTagStats() { Aggregation agg Aggregation.newAggregation( Aggregation.unwind(tags), Aggregation.group(tags).count().as(count), Aggregation.sort(Sort.Direction.DESC, count) ); AggregationResultsTagStat results mongoTemplate.aggregate(agg, articles, TagStat.class); return results.getMappedResults(); } }Spring Data MongoDB vs MyBatis-Plus两者的 Repository 模式非常相似但 MongoDB 不需要写 SQL通过方法名自动推导查询复杂聚合操作用 MongoTemplate 实现。对于开发者来说切换成本很低核心学习点在于理解 MongoDB 的文档模型和聚合管道。Docker 镜像构建最佳实践昨天我们用 Docker Compose 编排了微服务环境今天把 Spring Boot 项目正式打包成 Docker 镜像。关键在于多阶段构建Multi-stage Build大幅减小最终镜像体积。多阶段构建 DockerfileDockerfile — 多阶段构建 Spring Boot 应用Dockerfile# 阶段1Maven 构建 FROM maven:3.9-eclipse-temurin-17 AS builder WORKDIR /app # 先拷贝 pom.xml利用 Docker 缓存层加速依赖下载 COPY pom.xml . RUN mvn dependency:go-offline -B # 再拷贝源码并构建 COPY src ./src RUN mvn package -DskipTests -B # 阶段2运行时镜像 FROM eclipse-temurin:17-jre-alpine LABEL maintainerblog-system LABEL version1.0.0 # 创建非 root 用户安全最佳实践 RUN addgroup -S appgroup adduser -S appuser -G appgroup WORKDIR /app # 从构建阶段拷贝 jar 包 COPY --frombuilder /app/target/blog-service-1.0.0.jar app.jar # 修改文件所有者 RUN chown -R appuser:appgroup /app # 切换到非 root 用户 USER appuser # 暴露端口 EXPOSE 8080 # JVM 参数优化 ENV JAVA_OPTS-Xms256m -Xmx512m -XX:UseG1GC # 健康检查 HEALTHCHECK --interval30s --timeout10s --retries3 \ CMD wget -qO- http://localhost:8080/actuator/health || exit 1 # 启动命令 ENTRYPOINT [sh, -c, java $JAVA_OPTS -jar app.jar]镜像体积对比构建方式基础镜像包含内容镜像大小单阶段直接构建maven:3.9-jdk-17JDK Maven 源码 jar~850 MB多阶段推荐jre-17-alpineJRE jar最小化~180 MB多阶段构建将镜像体积从 850MB 压缩到 180MB减少了78%这在生产环境中意味着更快的拉取速度和更低的存储成本。构建与运行构建镜像并运行Shell# 构建镜像 docker build -t blog-service:1.0.0 . # 查看镜像大小 docker images blog-service # 运行容器 docker run -d \ --name blog-service \ -p 8080:8080 \ -e SPRING_PROFILES_ACTIVEprod \ -e SPRING_DATA_MONGODB_URImongodb://admin:123456mongodb:27017/blog_system \ blog-service:1.0.0 # 查看日志 docker logs -f blog-serviceDocker 私有仓库部署在团队开发或生产环境中我们需要一个私有的 Docker 镜像仓库来存储内部镜像。Harbor是企业级最常用的私有仓库方案。Harbor 快速部署docker-compose.yml — Harbor 私有仓库YAMLversion: 2.3 services: harbor: image: goharbor/harbor:2.10.0 container_name: harbor restart: always ports: - 80:80 # Web UI - 443:443 # HTTPS - 8088:8080 # Notary镜像签名验证 volumes: - harbor_data:/data - harbor_logs:/var/log/harbor volumes: harbor_data: harbor_logs:注意Harbor 的官方部署方式是使用harbor.yml配置文件 install.sh脚本而不是直接用 Docker Compose。上面的配置仅为简化示意实际部署请参考 Harbor 官方文档。在小团队测试场景下也可以使用 Docker 官方的registry镜像作为轻量级替代。推送与拉取镜像镜像推送到私有仓库Shell# 给镜像打标签仓库地址/项目名/镜像名:版本 docker tag blog-service:1.0.0 harbor.example.com/blog/blog-service:1.0.0 # 推送到私有仓库 docker push harbor.example.com/blog/blog-service:1.0.0 # 在其他服务器上拉取 docker pull harbor.example.com/blog/blog-service:1.0.0踩坑记录坑 1MongoDB 连接认证失败 — auth failed现象Spring Boot 启动时报com.mongodb.MongoSecurityException: Exception authenticating但用户名密码确认无误。原因MongoDB 连接 URI 中缺少authSource参数。默认情况下 MongoDB 使用 connected database 作为认证源但 admin 用户存储在 admin 数据库中。解决在 URI 中追加?authSourceadminmongodb://admin:123456localhost:27017/blog_system?authSourceadmin坑 2Spring Data MongoDB 实体字段名映射错误现象实体类中createdAt字段在 MongoDB 里存成了created_at查询时查不到数据。原因MongoDB 默认使用 Java 驼峰命名转 snake_case 的策略MongoMappingContext 默认配置。如果 MongoDB 中已有 camelCase 字段的数据需要用Field注解显式指定。解决在实体类字段上添加Field(createdAt)或者统一使用 snake_case。坑 3Docker 构建时 Maven 依赖下载缓慢现象每次docker build都要重新下载全部 Maven 依赖构建时间超过 5 分钟。原因Dockerfile 没有利用缓存层优化。如果把 COPY src 和 COPY pom.xml 放在一起源码一变所有依赖都要重新下载。解决先单独 COPY pom.xml 并运行mvn dependency:go-offline再 COPY src。这样只要 pom.xml 不变Docker 会直接使用缓存层。坑 4MongoDB 聚合管道 $lookup 性能问题现象$lookup 关联查询在数据量超过 10 万条时明显变慢。原因$lookup默认使用嵌套循环匹配没有索引时相当于全表扫描。解决确保关联字段foreignField上已建立索引db.users.createIndex({ _id: 1 })同时考虑在应用层做二次查询替代 $lookup避免单个聚合管道过长。收获总结今日核心收获今天学习了 MongoDB 文档数据库和 Docker 镜像构建两大主题内容跨度不小但都非常实用MongoDB 思维转换从关系型数据库的表 行 JOIN思维切换到集合 文档 内嵌思维。核心原则是读优先——把经常一起读的数据放在一起。聚合管道的威力$match / $group / $unwind / $lookup 四大阶段灵活组合可以实现 SQL 难以表达的复杂统计。Java 中通过 MongoTemplate 调用聚合管道也很方便。Spring Data MongoDB 无缝切换Repository 模式和 MyBatis-Plus 高度一致开发者只需学习文档模型设计即可。Docker 多阶段构建将 850MB 镜像压缩到 180MB是生产环境的必备技能。pom.xml 缓存层优化、非 root 用户、健康检查都是企业级实践。私有仓库的意义团队协作中统一的镜像仓库是 CI/CD 流水线的基础。Harbor 提供了权限管理、漏洞扫描等企业级功能。明日计划 · Day 18Maven 私服搭建Nexus管理团队依赖与插件阿里云 OSS 对象存储文件上传/下载/签名 URLSpring Boot 整合 OSS实现头像上传和图片管理功能前端 OSS 直传方案减少服务器中转压力