当前位置: 代码网 > it编程>编程语言>Java > SpringBoot集成MongoDB的高扩展存储实践

SpringBoot集成MongoDB的高扩展存储实践

2026年09月23日 Java 我要评论
一、关系型 vs 文档型建模:别急着建表,先想想数据怎么读做后端久了,大家都有路径依赖:一上来就画 er 图,拆表,定外键,恨不得把三范式背出来。关系型数据库这套设计在互联网场景下越来越别扭,尤其是分

一、关系型 vs 文档型建模:别急着建表,先想想数据怎么读

做后端久了,大家都有路径依赖:一上来就画 er 图,拆表,定外键,恨不得把三范式背出来。关系型数据库这套设计在互联网场景下越来越别扭,尤其是分库分表之后,跨表 join 要么被禁用,要么性能惨不忍睹。mongodb 的思路完全反过来,它鼓励你把一个业务聚合成一个文档,数据物理上放在一起,读的时候一次拿全,不需要拼接。

拿电商订单举例。关系型要拆订单表、订单明细表、商品表、客户表,查询时 join 三张表,一旦数据量上来,sql 优化起来能掉不少头发。mongodb 直接一个订单文档塞下所有信息:商品快照、数量、价格、总额,都在一个嵌套文档里:

{
  "_id": "order_1001",
  "userid": "user_888",
  "createdat": "2024-06-01t10:30:00z",
  "items": [
    { "productid": "p_1", "name": "iphone 15", "price": 6999, "qty": 1 },
    { "productid": "p_2", "name": "airpods pro", "price": 1899, "qty": 2 }
  ],
  "totalamount": 10797
}

这样建模不追求范式化,只关心业务上“读的时候需要什么”。订单被加载时,所有相关数据已经在文档里了,还天然支持分片。

关系型的强项是严格 schema、复杂事务、多表联合分析;mongodb 的强项是灵活、迭代快、高并发读写。没有谁全面碾压谁,关键看业务形态。如果实体间关系强、动不动需要复杂 sql,关系型依然靠谱;如果业务对象聚集性强、表结构总是变,mongodb 能省很多事。

二、内嵌还是引用?一句话:看你怎么读

内嵌和引用是 mongodb 建模里最常被问的问题。没有标准答案,但有个朴素的原则:如果读父文档时总是要同时读子数据,而且子数据量有限,也不独立变化,就内嵌;如果子数据会被单独查询、可能无限增长、或者被多个父文档共享,就引用。

内嵌的优势是局部性:一次 io,整个对象树都拿到。适合订单明细、用户地址、产品属性这种“一个主对象带几个子对象”的模型。但内嵌不能滥用,评论、日志、消息这种无限增长的数组,硬塞进去很快顶到 16mb 单文档上限,而且每次更新整个数组都要重写整个文档,代价极高。

引用就是存个 _id,运行时二次查询。适合文章和评论、用户和角色、商品和库存这种关系。特别是库存,它高频更新,独立事务,如果内嵌到商品文档里,会导致商品文档频繁重写,放大写入放大。

举个实际例子:社交平台帖子。帖子里内嵌作者信息 { userid: "u_1", nickname: "alice" },评论用引用 commentsref: ["c_1", "c_2"]。因为作者信息是快照,用户改昵称后,老帖子还显示旧昵称也没啥毛病,不必同步更新所有帖子。这就是有意识的冗余,比 join 省心得多。

三、spring data mongodb:repository 和 mongotemplate 怎么选

spring boot 集成 mongodb 非常简单,依赖一加,配置一写,两个核心工具就都在手边了。

依赖:

<dependency>
    <groupid>org.springframework.boot</groupid>
    <artifactid>spring-boot-starter-data-mongodb</artifactid>
</dependency>

配置(顺便把连接池参数也写上,后面会讲):

spring:
  data:
    mongodb:
      uri: mongodb://user:pass@host1:27017,host2:27018,host3:27019/dbname?replicaset=rs0
      auto-index-creation: true

实体定义,注意 @document@id

@document(collection = "orders")
public class order {
    @id
    private string orderid;
    private string userid;
    private list<orderitem> items;
    private bigdecimal totalamount;
    private instant createdat;

    // getters/setters
}

public class orderitem {
    private string productid;
    private string productname;
    private bigdecimal price;
    private integer qty;
}

repository 接口,简单场景够用:

public interface orderrepository extends mongorepository<order, string> {
    list<order> findbyuserid(string userid);
    page<order> findbycreatedatbetween(instant start, instant end, pageable pageable);
}

复杂查询、聚合管道、动态条件、批量更新,直接上 mongotemplate

@service
public class orderqueryservice {
    @autowired
    private mongotemplate mongotemplate;

    public list<order> findrecentorders(int limit) {
        query query = new query();
        query.with(sort.by(sort.direction.desc, "createdat"));
        query.limit(limit);
        return mongotemplate.find(query, order.class);
    }
}

我的习惯是:简单语义化查询走 repository,复杂聚合和动态条件走 mongotemplate,两者可以混用,没必要二选一。

四、聚合管道:复杂统计就用它,别把数据拉回应用层

聚合框架是 mongodb 特别能打的能力,一套管道式的数据处理模型,过滤、分组、投影、展开数组、排序、计算都能做。spring data mongodb 的 aggregation 对象让你用 java dsl 写管道,不用手拼 json。

常用的几个阶段:

  • $match:过滤
  • $group:分组聚合
  • $project:投影计算
  • $unwind:展开数组
  • $lookup:跨集合左连接

举个例子,订单文档里有 items 数组,每个 item 有 categoryamount,要按分类统计销售额:

public map<string, bigdecimal> sumsalesbycategory() {
    aggregation agg = aggregation.newaggregation(
        aggregation.unwind("items"),
        aggregation.group("items.category")
            .sum("items.amount").as("totalamount"),
        aggregation.sort(sort.by(sort.direction.desc, "totalamount"))
    );
    aggregationresults<categorysales> results = 
        mongotemplate.aggregate(agg, "orders", categorysales.class);
    return results.getmappedresults().stream()
        .collect(collectors.tomap(categorysales::getcategory, 
                                  categorysales::gettotalamount));
}

再比如按月份和地区统计 uv。这里有个小技巧:聚合框架不支持嵌套 group,可以先把年份、月份投影出来,再组合分组:

aggregation agg = aggregation.newaggregation(
    aggregation.project("region", "userid")
        .andexpression("year(createdat)").as("year")
        .andexpression("month(createdat)").as("month"),
    aggregation.group("region", "year", "month")
        .addtoset("userid").as("uvset"),
    aggregation.project("region", "year", "month")
        .and("uvset").size().as("uv")
);

addtoset 天然去重,size 得到 uv 数。

想搞个 top n 排行,就是 match + unwind + group + sort + limit:

aggregation agg = aggregation.newaggregation(
    aggregation.match(criteria.where("status").is("paid")),
    aggregation.unwind("items"),
    aggregation.group("items.productid")
        .sum("items.qty").as("totalqty")
        .avg("items.price").as("avgprice"),
    aggregation.sort(sort.by(sort.direction.desc, "totalqty")),
    aggregation.limit(5)
);

聚合引擎会尽量把管道下推,配合索引,百万级数据量毫秒级返回没啥问题。

五、多文档事务:能用,但别当万能药

mongodb 4.0 才支持副本集内多文档 acid 事务,4.2 才扩展到分片集群。spring data mongodb 2.x 开始支持 @transactional,用起来跟 jpa 差不多:

@transactional
public void placeorder(order order, string userid, product product, int qty) {
    orderrepository.save(order);
    productrepository.decreasestock(product.getproductid(), qty);
    paymentrepository.insert(new payment(...));
}

但有前提:mongodb 必须是副本集或分片集群,单机 standalone 不行,存储引擎得是 wiredtiger(默认)。另外,事务操作在分片集群里如果涉及跨分片,约束会更多。

事务是有开销的。事务开始要协调时间戳,要处理锁冲突和快照,每个文档还要记录意向锁信息。所以别把事务当万能药,能不用就不用。比如扣库存 + 生成订单 + 扣款,这种强一致场景必须事务;但订单创建后异步发短信、更新统计报表,完全不需要事务,用 mq 异步慢慢处理就行,最终一致就够了。

还要学会规避事务:

  1. 重新设计文档边界。把库存、订单、支付想办法塞进一个文档,用 $inc 原子更新。mongodb 单文档操作天然原子,不需要跨文档事务。
  2. 状态机模式。订单状态从“待付款”到“已付款”,用条件更新 findandmodify + $set 保证只有特定状态才能流转。
  3. 补偿机制。如果后续步骤失败,用补偿任务把前面已成功的操作回退,保证最终一致。

六、索引:没有索引的 mongodb 就是全表扫描,迟早出事

查询性能全靠索引,这一点 mongodb 和关系型没区别。spring data 可以通过注解自动创建,但生产环境还是建议手动管理。

复合索引很关键。设计原则:先等值字段,再排序字段,再范围字段。比如订单查询经常按 userid 等值 + createdat 倒序:

@document(collection = "orders")
@compoundindex(def = "{'userid': 1, 'createdat': -1}")
public class order {
    // ...
}

也可以动态建:

mongotemplate.indexops("orders")
    .ensureindex(new index().on("userid", sort.direction.asc)
                            .on("createdat", sort.direction.desc)
                            .named("idx_user_created"));

注意字段顺序:把区分度高的、等值匹配的字段放前面,能更高效裁剪数据。这样的复合索引也能覆盖 userid 单字段查询。

聚合管道里,$match$sort 尽量命中索引。比如 $match 过滤 status,就建 status 索引;按 createdat 排序统计,就建 createdat 索引。$group 阶段索引帮不上大忙,主要靠 $match 前置减少输入量。

ttl 索引做数据自动过期很好用,日志、会话、验证码这类场景:

@document(collection = "sessions")
public class usersession {
    @id
    private string id;
    private string userid;
    @indexed(expireafterseconds = 3600)
    private instant lastaccess;
}

mongodb 后台定期扫描 ttl 字段,过期就删。注意字段必须是日期类型,索引单字段。

索引也别建太多,写入时要同步维护索引,开销不小。一个集合建议不超过 5 个索引。基很低的字段(比如布尔值)建了基本没用。线上有疑问就 explain() 看查询计划,别靠猜。

七、连接池与游标:高并发下别把连接和内存打爆

mongodb java 驱动默认就有连接池,spring boot 里直接配 uri 参数:

spring:
  data:
    mongodb:
      uri: mongodb://localhost:27017/db?maxpoolsize=100&minpoolsize=10&maxidletimems=120000&waitqueuemultiple=5&waitqueuetimeoutms=10000

或者编程式配置:

@bean
public mongoclientsettings mongoclientsettings() {
    return mongoclientsettings.builder()
        .applytoconnectionpoolsettings(builder -> 
            builder.maxsize(100)
                   .minsize(10)
                   .maxconnectionidletime(120, timeunit.seconds)
                   .maxconnectionlifetime(1, timeunit.hours))
        .build();
}

maxpoolsize 不建议太大,默认 100 够用,太大的话文件描述符和内存都扛不住。maxidletimems 负责回收空闲连接,防止连接失效。waitqueuemultiple 控制等待队列,超过就快速失败,避免线程堆积。

查询结果集特别大,几十万上百万条,直接全捞到内存必然 oom。用游标流式处理,别攒着:

try (closeableiterator<order> iterator = mongotemplate.stream(query, order.class)) {
    while (iterator.hasnext()) {
        process(iterator.next());
    }
}

repository 也能返回 stream,同样要记得关:

try (stream<order> stream = orderrepository.findbycreatedatafter(start)) {
    stream.foreach(this::process);
}

游标使用期间会占用连接,所以处理动作要快,别在循环里做耗时的远程调用,长时间占用连接会把连接池耗尽。

用户可见的列表老老实实分页:

page<order> page = orderrepository.findbyuserid(userid, pagerequest.of(0, 20, sort.by("createdat").descending()));

千万别 skip(1000000).limit(20) 这么翻,翻得越深越慢。海量列表要用基于游标的分页,也就是按照上次查询的最后一条 _id 或排序值继续往后取:

query query = new query();
query.addcriteria(criteria.where("_id").gt(lastid));
query.limit(20);
query.with(sort.by(sort.direction.asc, "_id"));

这种方案的性能与翻页深度无关,适合大数据量场景。

总结

mongodb 不是来替代关系型的,它是用来解决关系型在特定场景下的痛点的。spring boot 生态里,spring data mongodb 从实体映射到聚合管道、事务控制、索引管理都提供了完善的支持。关键还是想清楚自己的业务:

  • 建模时,围绕数据怎么读来决定内嵌还是引用,尽量减少 join 和关联查询;
  • 查询统计用聚合管道,别把数据拉到应用层去算;
  • 事务能不用就不用,优先单文档原子操作和最终一致;
  • 性能优化从索引和连接池着手,海量数据用游标和基于游标的分页。

在业务快速迭代、数据量越来越大的今天,mongodb + spring boot 这套组合还是相当能打的。希望这些实践能帮你在实际项目中少踩些坑。

以上就是springboot集成mongodb的高扩展存储实践的详细内容,更多关于springboot集成mongodb存储实践的资料请关注代码网其它相关文章!

(0)

相关文章:

版权声明:本文内容由互联网用户贡献,该文观点仅代表作者本人。本站仅提供信息存储服务,不拥有所有权,不承担相关法律责任。 如发现本站有涉嫌抄袭侵权/违法违规的内容, 请发送邮件至 2386932994@qq.com 举报,一经查实将立刻删除。

发表评论

验证码:
Copyright © 2017-2026  代码网 保留所有权利. 粤ICP备2024248653号
站长QQ:2386932994 | 联系邮箱:2386932994@qq.com