news 2026/10/21 14:35:04

Spring Boot整合MongoDB最佳实践:分页、事务与索引优化指南

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
Spring Boot整合MongoDB最佳实践:分页、事务与索引优化指南

做后端这些年,我最大的体会是:CRUD 阶段从来不决定一个系统的生死,真正让人半夜被电话叫醒的,永远是分页性能、事务边界和索引命中这些“看起来不起眼”的细节。Spring Boot 整合 MongoDB 这件事,表面上就是加一个 starter、写两个接口,但只要你开始做分页、开事务、上索引,事情就完全不一样了。这篇总结不聊 PPT 概念,纯粹把我实操过的方案、踩过的坑、验证过的取舍整理出来,围绕 CRUD、分页、事务、索引四条主线,把 MongoDB 在 Spring Boot 项目里真正可用的最佳实践讲透。如果你正在搭基于 Spring Boot 的 MongoDB 服务,或者已经被线上慢查询和事务超时折磨过,这篇文章应该能帮你省下不少排查时间。

先说一句立场:MongoDB 不是银弹,但它确实适合很多业务。关键是按它的特性来设计,而不是把 MySQL 那套思维硬搬过来。整篇文章会从项目设计思路开始,逐步展开配置、CRUD、分页、事务、索引,最后是高频问题排查速查表。每一段我都会写清楚“为什么这么做”,毕竟只给结论不给理由,换个场景你就没法用了。

1. 项目背景与整体设计思路

1.1 为什么选 MongoDB 而不选 MySQL

我接手过一个订单系统,业务方今天说要加一个“配送备注”,下周又说要加“优惠明细快照”,字段列表几乎每两周变一次。如果用 MySQL,每次都要写ALTER TABLE,还要处理存量数据的默认值、索引变更、ORM 映射更新,几个迭代下来 DDL 比业务代码都多。这个时候 MongoDB 的文档模型优势就非常直观:字段是灵活的,集合没有严格列约束,新字段直接写到文档里,旧数据没有这个字段也不影响读取。用一句通俗的话讲,MySQL 更像一张表格,列是提前定好的;MongoDB 更像一份 Word 文档,你随时可以在末尾加一段。

MongoDB 另一个核心优势是水平扩展。传统关系型数据库在单机性能逼近上限后,分库分表涉及分布式事务、全局 ID、跨库查询,复杂度成倍上升。MongoDB 的分片机制是内建的,数据按片键分布到多台机器,对应用层而言仍然是同一个连接入口。所以当你的项目处于“业务形态没定型、字段变化频繁、数据量增长快”的阶段,MongoDB 往往比 MySQL 更稳。

当然,选型要看场景。强一致性要求极高、大量事务性关联查询、后台复杂报表需要 join 五张表,这些场景 MongoDB 并不擅长。我的建议是主业务能用文档模型表达的,用 MongoDB;账务、库存这类强事务强关系的数据,老老实实留在关系型数据库里,中间靠消息队列或接口同步,不要让 MongoDB 去背不适合它背的锅。

1.2 技术选型与版本方案

版本选型直接决定你能不能用上事务、用上哪些聚合操作符。我在项目里的方案是:Spring Boot 3.x + Spring Data MongoDB + MongoDB 6.x,如果团队还有老项目在维护,Spring Boot 2.7.x + MongoDB 4.4/5.0 也比较稳妥。Spring Boot 3 有个明显变化是javax.*换成了jakarta.*,如果你从 2.x 升级,包名要一并修改,否则编译期就报错。

MongoDB 版本和事务能力强相关。4.0 版本才支持副本集多文档事务,4.2 才支持分片集群事务。如果你的服务器还在跑 3.6,那事务基本和你无缘。所以选型时先确认 MongoDB 版本,再决定你的代码能不能碰事务。

本地开发环境我强烈建议用 Docker 起一个副本集,而不是单节点。单节点 mongod 跑 CRUD 没问题,但一启动事务就会报错,错误信息类似Transaction numbers are only allowed on a replica set member or mongos。用 Docker Compose 定义三节点副本集,或者直接起一个单节点副本集(一主一从一仲裁比较标准),可以让本地环境和线上行为保持一致。

1.3 工程结构规划

工程结构上,我通常按 com.xx.order 这样的包根,下面分 controller、service、repository、document、dto、config。注意我写的是document而不是entity,这是 MongoDB 领域的习惯叫法,也提醒团队成员这是一份 JSON 文档映射,不是数据库表映射。

com.xx.order ├── controller ├── service │ └── impl ├── repository ├── document ├── dto ├── config └── common

repository 层放 Spring Data 接口,document 层用注解映射集合字段,service 层承载事务和业务编排,controller 只做参数接收和响应返回。如果项目里有报表、聚合查询、批量更新这类复杂操作,我会额外加一个dao或mongo-template包专门放 MongoTemplate 的代码,避免 Repository 接口变得臃肿。结构本身没有标准答案,但一个清晰的分层可以让团队在排错时迅速定位问题。

2. 环境准备与基础配置

2.1 依赖引入与连接参数

Spring Boot 项目引入 MongoDB 依赖非常轻量:

<dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-data-mongodb</artifactId> </dependency>

如果只是做 Web 接口,还需要引入 Web 起步依赖。常用的就是spring-boot-starter-web。这里不要画蛇添足加spring-boot-starter-data-rest,除非你真的需要暴露超链接 REST 资源,否则多出来的行为只会干扰你的接口设计。

核心配置在application.yml:

spring: data: mongodb: uri: mongodb://order_user:pass123@localhost:27017/order_db?authSource=admin&replicaSet=rs0

authSource=admin表示认证数据库是 admin,如果你的用户是在 order_db 下创建的,就要改成authSource=order_db。这个参数看着小,但错了最常见的表现就是本地连不上,生产环境偶尔能连,认证失败和不失败全看服务端账号配置。

连接串上还需要注意maxPoolSize。Spring Data MongoDB 底层用的是 MongoDB 驱动,默认连接池大小是 100。如果你的服务并发量高,并且经常有慢查询,连接池占满之后新请求就会排队,表现是 Waiting for connection 超时。可以按服务实例的连接压力适当调大,比如?maxPoolSize=200&minPoolSize=10。不过不要无脑调大,连接数越多,每个连接对 MongoD 的线程和文件句柄压力也越大,合理区间需要结合压测数据来定。

2.2 实体模型与文档映射

一个订单文档的典型映射如下:

@Document(collection = "orders") public class Order { @Id private String id; @Indexed(unique = true) private String orderNo; private String userId; @Field(targetType = FieldType.DECIMAL128) private BigDecimal amount; private List<OrderItem> items; private Integer status; private Instant createdAt; private Instant updatedAt; }

@Document声明这是 MongoDB 集合文档,@Id默认映射到_id字段。这里有一个细节:_id在 MongoDB 里可以是 ObjectId、字符串或数值。Spring Data 使用 String 类型接收时,如果没有特殊自定义,它会把 String 当成_id的值保存,而不是自动生成 ObjectId。如果需要 ObjectId,建议字段类型用ObjectId。如果你希望接口上统一返回 String,可以在 DTO 层转换,不要在领域模型里混用。

金额字段我一般用@Field(targetType = FieldType.DECIMAL128)。MongoDB 中有Decimal128类型,专门用于高精度浮点数,但 JDBC 驱动和 Spring Data 的默认映射对BigDecimal处理并不总是可靠。加上这个注解可以明确存储类型,避免出现精度丢失。需要额外提醒的一点是,Decimal128存在精度上限(最高 34 位十进制精度),如果业务金额可能超过这个范围,建议直接保存字符串或整数分单位。

如果很多文档里都有createdAt/updatedAt,可以抽一个 BaseDocument,里面放这两个字段和 getter/setter。实际项目中这个操作能省掉大量重复代码,也让审计字段的实现保持一致。

2.3 Repository 层与 MongoTemplate 的取舍

Spring Data MongoDB 提供两种主要访问方式:MongoRepository接口和MongoTemplate。我的经验是两者搭配而非二选一。

MongoRepository适合简单、固定的数据访问。比如按主键查询、按某个业务字段分页查询、派生查询,代码量少,阅读性高。MongoTemplate则适合条件动态变化、聚合 pipeline、批量更新、复杂查询的场景。聚合查询在 Repository 里也能用@Aggregation注解,但 pipeline 一长,注解里的字符串就很难维护,不如在 service 层用 Java 构造 Aggregation 更直观。

用 Repository 还是 MongoTemplate 还有一个判断标准:如果你要操作多集合关联数据,或者需要distinct、group、bucket这些聚合能力,直接 MongoTemplate。如果只是按 key 存取文档,Repository 足够。两者不是互斥的,同一个 service 里可以同时注入 Repository 和 MongoTemplate,只要职责分开就行。

3. 核心功能落地:从 CRUD 到分页查询

3.1 基础 CRUD 的实现与细节

定义一个 Repository 接口:

public interface OrderRepository extends MongoRepository<Order, String> { List<Order> findByUserId(String userId); List<Order> findByUserIdAndStatus(String userId, Integer status); Page<Order> findByStatus(Integer status, Pageable pageable); Optional<Order> findByOrderNo(String orderNo); @Query("{ 'userId': ?0, 'status': { $in: ?1 } }") List<Order> findUserOrdersInStatus(String userId, List<Integer> statuses); }

基础 CRUD 最容易被忽略的是save与insert的区别。save方法会先检查_id是否存在:存在就执行 upsert/update,不存在就插入。但如果你有唯一索引,业务上期望一个订单号只能出现一次,就应该用insert而不是save。用insert在唯一键冲突时会抛出DuplicateKeyException,提示更清晰。使用save在某些场景下会把一个本该报错的重复订单静默覆盖,这个坑我亲眼见过。

批量写入时,Spring Data 的saveAll底层是逐条判断_id后逐条写入,并不会保证原子性。如果追求写入性能,可以改用MongoTemplate的insert(Collection)或bulkOps(BulkOperations.BulkMode.ORDERED)。Ordered模式遇到错误会停止后续写入,Unordered模式会继续处理剩余文档,最后统一返回结果。批量写超过 1000 条时,建议分批处理,避免驱动内存压力过大。

3.2 分页查询:Pageable 与 Aggregation 的实战对比

分页是使用 MongoDB 时最容易出性能问题的地方之一。Spring Data 的分页很自然:

Pageable pageable = PageRequest.of(0, 10, Sort.by(Sort.Direction.DESC, "createdAt")); Page<Order> page = orderRepository.findByStatus(1, pageable);

这种写法对中小数据量完全够用。但底层执行逻辑你要清楚:Spring Data 会先执行一个 count 查询,再执行一个 limit + skip 查询。随着页码增加,skip 跳过的文档数越来越大,比如页大小 10,第 10000 页就要 skip 99990 条文档,即使有索引,数据库也要先扫描和丢弃这些文档,响应时间会持续恶化。

针对这种场景,我做过实测对比:数据量在百万级时,skip+limit深分页的耗时是游标分页的几十倍。所以我们的聚合分页方案会这么写:

public Page<Order> listOrdersWithAggregation(String userId, int page, int size) { Pageable pageable = PageRequest.of(page, size, Sort.by(Sort.Direction.DESC, "createdAt")); long skip = (long) page * size; MatchOperation match = Aggregation.match(Criteria.where("userId").is(userId)); SortOperation sort = Aggregation.sort(Sort.by(Sort.Direction.DESC, "createdAt")); SkipOperation skipOp = Aggregation.skip(skip); LimitOperation limitOp = Aggregation.limit(size); Aggregation dataAgg = Aggregation.newAggregation(match, sort, skipOp, limitOp); List<Order> orders = mongoTemplate.aggregate(dataAgg, "orders", Order.class).getMappedResults(); Aggregation countAgg = Aggregation.newAggregation( match, Aggregation.count().as("total") ); long total = mongoTemplate.aggregate(countAgg, "orders", CountResult.class) .getMappedResults() .stream() .findFirst() .map(CountResult::getTotal) .orElse(0L); return new PageImpl<>(orders, pageable, total); }

这里CountResult是一个简单的内部类:

@Data public static class CountResult { private long total; }

用 Aggregation 的好处是 count 和 data 可以分别走不同的查询优化,而且 pipeline 对更复杂的汇总需求(比如 group、project)扩展性更好。不过 Aggregation 也不是没有缺点,pipeline 中的skip同样会扫描跳过文档,深分页的硬伤依然存在。真正要从根本上解决深分页,就得用基于排序字段和_id的游标分页。

游标分页核心是记住上一页最后一条文档的排序位置,而不是记一个页码。比如按createdAt desc排序:

public List<Order> pageByCursor(String userId, Instant lastCreatedAt, String lastId, int size) { Criteria criteria = Criteria.where("userId").is(userId); if (lastCreatedAt != null && lastId != null) { criteria.andOperator( Criteria.where("createdAt").lt(lastCreatedAt), Criteria.where("_id").ne(lastId) ); } else if (lastCreatedAt != null) { criteria.and("createdAt").lt(lastCreatedAt); } Query query = new Query(criteria) .with(Sort.by(Sort.Direction.DESC, "createdAt")) .limit(size); return mongoTemplate.find(query, Order.class); }

严格来说,如果同一条createdAt下有几万条数据,只用< created加_id.ne会出现漏数据和重复数据。更好的做法是构造一个复合排序条件,把createdAt和_id一起作为边界:createdAt < lastCreatedAt OR (createdAt == lastCreatedAt AND _id < lastId)。这段逻辑看起来繁琐,但只有这样才能保证排序完全唯一。建议在工程里封装一个通用的 CursorPageRequest 对象,避免每个业务各写各的。

3.3 动态条件查询与排序的坑

实际业务里很少只有一两个固定查询条件。前端传什么参数,后端往往就得动态拼条件。MongoTemplate 的 Criteria 可以这么写:

Criteria criteria = new Criteria(); if (userId != null && !userId.isEmpty()) { criteria.and("userId").is(userId); } if (status != null) { criteria.and("status").is(status); } if (minAmount != null) { criteria.and("amount").gte(minAmount); } Query query = new Query(criteria); query.with(Sort.by(Sort.Direction.DESC, "createdAt"));

很多人会犯一个错误:多个and连接条件时没有考虑索引顺序。例如你有查询条件userId和status,如果索引是{status:1, userId:1},但你的查询条件先过滤userId后过滤status,MongoDB 依然能使用这个复合索引,不过索引扫描效率可能不如{userId:1, status:1}。复合索引的设计原则很简单:等值过滤字段放前面,排序字段放后面,范围条件放最后。如果你既等值又范围,尽量把等值字段放在前面,让索引能精准定位到一个小的候选集再处理范围条件。

排序还有一个隐蔽问题:如果你的查询条件没用到索引,但排序字段在另一个索引里,MongoDB 通常会在内层先排序后返回,或者出现内存排序(SORTstage),大数据量时性能会垮掉。最简单的校验方式就是下面要讲的explain,不要凭感觉猜。

4. 事务实践:从单文档 ACID 到多文档事务

4.1 MongoDB 事务机制简述

MongoDB 对事务的支持比 MySQL 要“年轻”,但能力已经足够应对常见业务。4.0 开始支持副本集上的多文档事务,4.2 扩展到了分片集群。这里的“多文档”指的是单个文档之外的多条写入和读取操作可以被放到同一个事务里,全部成功或全部失败。

事务依赖两个基础:副本集和 WiredTiger 存储引擎。副本集保证数据有多个副本,事务在提交时会用 majority 写关注确认数据已经同步到多数节点,再返回成功。如果只有一个 mongod 实例,就没有“多数派”概念,所以事务不可用。这就是为什么本地要跑副本集,线上也至少要一主一从加一个隐藏节点或仲裁节点。

事务边界不能像 MySQL 那样设计得很随意。MongoDB 事务中如果长时间持有写锁,其他操作会被阻塞,达到锁等待上限后事务会失败。所以事务内尽量不要调用外部 HTTP 接口,尽量不要做重聚合,不要一次更新成千上万个文档。事务的目的应该是保护一小片关键数据的一致性,而不是把所有业务逻辑都塞进去。

4.2 Spring 声明式事务的接入方式

Spring Data MongoDB 提供MongoTransactionManager。先在配置类注册事务管理器:

@Configuration @EnableTransactionManagement public class MongoConfig { @Bean public MongoTransactionManager transactionManager(MongoDatabaseFactory dbFactory) { return new MongoTransactionManager(dbFactory); } }

然后在需要事务的 Service 方法上使用@Transactional:

@Service public class OrderService { @Autowired private OrderRepository orderRepository; @Autowired private MongoTemplate mongoTemplate; @Transactional public void createOrderWithPayment(Order order, Payment payment) { orderRepository.save(order); mongoTemplate.insert(payment, "payments"); } }

当方法内同时操作OrderRepository和MongoTemplate时,Spring Data 的事务管理会从当前的MongoDatabaseFactory里取出同一个ClientSession,所以两条写入在同一个事务里。如果insert或save任意一步抛异常,整个事务都会回滚。

和 JPA 不同,MongoTransactionManager的@Transactional默认传播行为是REQUIRED,支持内嵌事务(嵌套事务在 MongoDB 中做不到保存点,所以实际是复用外层事务)。如果你的业务里调用了另一个同样标注@Transactional的方法,两者会共用同一个 session,任何一方的失败都会导致整体回滚。

除了声明式事务,也可以在业务代码里编程式控制事务:

@Autowired private MongoTransactionManager transactionManager; public void createOrderInTransaction(Order order, Payment payment) { TransactionDefinition def = new DefaultTransactionDefinition(); TransactionStatus status = transactionManager.getTransaction(def); try { orderRepository.save(order); mongoTemplate.insert(payment, "payments"); transactionManager.commit(status); } catch (Exception e) { transactionManager.rollback(status); throw e; } }

声明式事务更优雅,但编程式事务在需要动态控制提交时机时更灵活。大多数业务建议优先用声明式,代码清晰且不容易漏掉 rollback。

4.3 事务失效与隔离等级的常见陷阱

第一个坑最典型:同类内部调用导致事务失效。比如一个 Service 里有saveA()方法,内部this.saveAAndB(),而saveAAndB标注了@Transactional。由于this调用不会经过 Spring AOP 代理,事务注解根本不会生效。解决办法是拆成两个 bean,或者在调用方注入自身代理对象,再调用proxy.saveAAndB()。我建议设计时尽量把事务方法放在公开接口边界,内部方法不要标事务。

第二个坑是隔离级别。Spring 的@Transactional(isolation = Isolation.REPEATABLE_READ)等属性,在 MongoDB 事务里并不像 MySQL 那样完整实现。MongoDB 事务主要依靠 read concern 和 write concern 控制可见性,默认 read concern 是 local,提交前读取到的并不一定是大多数节点确认的数据。事务内如果需要强一致的读,建议通过 MongoTemplate 的 session 操作显式设置 read concern,或者在连接串配置readConcernLevel=majority。但要注意,read concern 为 majority 时对性能有额外开销。不要指望一个注解解决所有隔离问题。

第三个坑是事务和分片集群的组合限制。在分片集群中使用多文档事务时,事务内的操作必须都路由到同一个分片吗?准确说,MongoDB 4.2 之后支持跨分片事务,但事务涉及的 shard key 必须包含在更新/查询条件中,否则会报错或性能极差。如果团队还不熟悉分片键设计,初期尽量把事务相关的集合放在同一个分片上。

第四个坑是超时和写冲突。事务内并发修改同一文档时,后提交方会收到WriteConflict,表现为MongoTransactionException: WriteConflict。这不是代码 bug,而是并发控制的正常表现,但你需要在上层处理:捕获异常后重试整个事务,或者改为串行化操作。为了避免冲突概率,多个事务操作多个文档时,最好按固定的_id顺序更新,避免互相等待死锁。

5. 索引优化:覆盖查询与慢日志排查

5.1 索引类型选型与创建方式

MongoDB 索引和关系型数据库的索引核心逻辑类似,但类型更多。最常用的是单字段索引和复合索引,此外还有唯一索引、TTL 索引、稀疏索引、部分索引、地理空间索引、文本索引和哈希索引。

创建索引有几种方式:

  • 开发期:在@Document类上加注解,Spring Data 启动时会自动创建索引。
  • 生产期:建议通过迁移脚本或 MongoShell 手动创建,避免服务启动瞬间自动建索引造成线上抖动。

常见注解:

@Document(collection = "orders") @CompoundIndex(name = "idx_user_status_created", def = "{'userId': 1, 'status': 1, 'createdAt': -1}") public class Order { @Id private String id; @Indexed(unique = true) private String orderNo; private String userId; private Integer status; private Instant createdAt; }

@Indexed(unique = true)创建唯一索引,保证订单号不重复。@CompoundIndex声明复合索引,顺序是 userId 正序、status 正序、createdAt 倒序。

这里特别想强调:索引不是越多越好。写入时索引越多,插入和更新越慢,索引字段占用的磁盘空间也越大。一个集合上如果有二十几个索引,写性能和运维成本都会增加。我的经验是只对高频查询和强约束字段建索引,所有索引创建前先问自己:这个查询线上真的经常跑吗?

5.2 复合索引、稀疏索引、TTL 索引的使用场景

复合索引的设计有一条经验法则:等值字段在前,排序字段次之,范围字段最后。等值条件最利于精确定位;排序字段放在后面,可以让 MongoDB 免去内存排序;范围字段放最后,避免索引跳跃变多。

举个例子,业务查询模式是where userId = ? and status = ? order by createdAt desc,复合索引就应该是{userId: 1, status: 1, createdAt: -1}。查询条件中userId和status都是等值,createdAt负责排序。如果再加一个金额范围查询amount > ?,那么索引可以设计成{userId:1, status:1, amount:1, createdAt:-1},范围字段 amount 在前会影响 createdAt 排序顺序,实际效果需要 explain 验证。

TTL 索引非常实用,常用于保存短期数据的集合,比如登录 token、临时验证码、操作日志。字段类型必须是日期,MongoDB 会定期删除超过过期时间的文档。创建方式:

@Indexed(expireAfterSeconds = 86400) private Instant expireAt;

TTL 索引不是精确到秒的定时器,后台清理线程一般每 60 秒跑一次,所以数据删除会有 0 到 60 秒的延迟。如果你的业务要求到点立即不可见,除了 TTL 还要在查询条件里加expireAt > now的判断。

稀疏索引用于“只给有该字段的文档建立索引”,并且可以配合唯一约束防止重复。比如业务里只有部分订单有外部优惠券编号,希望有编号的不能重复,没有编号的不参与唯一约束,这时可以用:

@Indexed(sparse = true, unique = true) private String couponCode;

注意稀疏索引和唯一索引组合时,如果字段缺失,多个文档都不会被索引唯一约束限制,这正好满足“只有有值才唯一”的需求。

5.3 用 explain() 验证索引命中情况

不要以为加了索引就一定能生效。MongoDB 的查询优化器会根据统计信息选择执行计划,某些查询条件可能走了 COLLSCAN(全集合扫描)。验证索引最直接的方法是 explain。

在 MongoShell 里:

db.orders.find({ userId: "u1001", status: 1 }).sort({ createdAt: -1 }).explain("executionStats")

返回结果里重点看几个字段:

  • queryPlanner.winningPlan.stage:如果是IXSCAN,说明走了索引;如果是COLLSCAN,说明全表扫描。
  • executionStats.totalKeysExamined:索引键值扫描了多少条。
  • executionStats.totalDocsExamined:回表读取了多少文档。
  • executionStats.nReturned:实际返回了多少条。

如果totalDocsExamined和nReturned相差巨大,说明虽然走了索引,但筛选性不够好,或者复合索引顺序不匹配。如果totalKeysExamined很大,但是totalDocsExamined很小,说明索引包含的字段过滤掉了大量数据,回表少,性能好;如果两者都很大,说明索引没有有效缩小范围。

Spring Boot 代码里也可以执行 explain 命令,但更常用的是查询前先打开 MongoDB profiling,或者打印操作日志观察扫描文档数。我对团队的要求是:慢查询优化前必须先跑 explain,把优化前后的执行计划截图或文本保存下来,这样排查问题才有据可依。

6. 常见问题与排查技巧实录

6.1 连接串配置错误与认证问题

项目里最频繁出现的启动问题就是连接失败。常见的报错包括:

  • Unable to connect to host:网络不通或端口未开放。
  • Authentication failed:账号密码错误或authSource指向错误。
  • MongoSocketReadTimeoutException:连接建立后响应超时,可能是防火墙丢包或 Mongo 实例负载过高。

认证问题最常见的根因是authSource。MongoDB 账号通常创建在某个数据库上,但可以通过authSource指定在哪个库进行认证。比如在 admin 库创建了 root 角色,连接串就必须带authSource=admin;在业务库创建了普通用户,就写业务库名。不要想当然抄网上模板,先确认自己账号归属哪个库。

副本集连接串形如:

mongodb://user:pass@host1:27017,host2:27017,host3:27017/db?replicaSet=rs0&authSource=admin

连接串里不要写多个不同数据库的鉴权信息,MongoDB 只支持一个authSource。如果业务里有多个数据库需要不同用户访问,应该拆成多个 MongoClient 或使用多租户设计。

6.2 大字段分页性能问题

深分页问题前面已经提过。遇到这类问题,先判断数据量级。如果集合在几万条以内,skip+limit 完全没问题,不需要搞复杂的游标。但数据量达到几十万、上百万,用户又真的会翻到第几百页时,必须考虑游标分页或“加载更多”模式。

还有一个容易被忽略的问题:分页返回的 List 里如果包含巨大的内嵌数组或长文本,网络传输和 JSON 序列化都会变慢。解决方案是使用@Query或 MongoTemplate 的fields方法只返回需要的字段:

Query query = new Query(Criteria.where("status").is(1)); query.fields().include("orderNo").include("userId").include("createdAt"); List<Order> list = mongoTemplate.find(query, Order.class, "orders");

字段裁剪在列表接口里效果立竿见影,而且能显著减少 GC 压力。

6.3 事务超时与 Lock 冲突处理

我在生产上遇到过两次事务超时,一次是事务里调了一个耗时 3 秒的第三方接口,另一次是多个事务同时更新同一个热点用户的订单统计字段。日志里都能看到Transaction 1 is aborted due to a write conflict。

排查思路:

  • 检查事务内是否调用了外部服务,把外部调用移到事务前或事务后。
  • 检查是否存在热点文档。比如用户维度汇总文档,所有事务都更新这一条,并发一高必然冲突。解决思路是把热点文档拆成多个分片文档或改用异步任务合并更新。
  • 检查事务锁等待时间是否过短。通过maxTransactionLockRequestTimeoutMillis可以调整默认锁请求等待时间,但调大不等于解决问题,只是给冲突更多重试机会。参数和事务重试机制需结合业务。

事务内尽量避免执行findAndModify、updateMany这类可能扫描大量文档的操作。如果确实需要,建议把更新范围缩小到精确_id或唯一索引键。

6.4 索引失效场景速查表

场景原因解决建议
查询条件用$or且各分支索引不同优化器可能选择扫描多个索引后再合并尽量改为$in或建覆盖索引,避免$or
$regex搜索不是^开头无法高效使用索引对搜索场景用文本索引或允许前缀匹配
对字段使用函数或$where索引无法作用于表达式把计算字段冗余到文档中,并对冗余字段建索引
查询字段类型与索引字段类型不一致类型不一致导致索引不匹配严格制定文档字段类型规范,避免同字段混存字符串和数字
复合索引字段顺序和查询顺序不一致前缀匹配失效按等值、排序、范围原则重新设计索引
排序字段不在查询使用的索引中导致内存排序或SORTstage将排序字段加入复合索引末尾
范围条件和排序组合不当例如金额范围条件的字段排在排序字段前使用 explain 验证,必要时调整索引字段顺序

这张表基本覆盖了我在评审代码时经常要求同事复查的场景。遇到线上慢查询,先跑 explain,再对着这张表逐条排查,通常很快能定位问题。

最后分享一个我个人的实操习惯:每次上线 MongoDB 相关改动前,我都会把集合的索引列表、事务边界、分页方式写在一个变更说明里,由同事 review 一遍。MongoDB 的坑往往不是功能实现不了,而是业务上高速增长时才暴露出来。提前把设计边界画清楚,远比事后救火省心。如果你也正在做 Spring Boot 整合 MongoDB,建议先在一个中等数据量的压测环境里验证分页和事务,再推广到生产,这个顺序能帮你躲开大部分始料未及的问题。

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

手写BP神经网络实现Iris三分类:纯NumPy反向传播实验详解

简介&#xff1a;提供BP神经网络求解Iris鸢尾花三分类问题的完整Python实现&#xff0c;面向机器学习初学者或需完成课程设计的读者。压缩包内共4个文件&#xff1a;2个Python脚本分别承担用户交互与正向传播网络构建&#xff0c;1份CSV数据为经典Iris数据集&#xff0c;1份DOC…

作者头像 李华
网站建设 2026/10/21 14:33:34

Linux应急响应实战指南:从入侵排查到后门清除

刚做运维那几年&#xff0c;我最怕半夜接到值班电话&#xff0c;电话那头永远是同一句话&#xff1a;“服务器不对劲&#xff0c;你看一下。”后来处理过几次Linux应急响应之后&#xff0c;反而镇定了不少——不是因为习惯了出事&#xff0c;而是因为摸清了一套固定打法&#x…

作者头像 李华
网站建设 2026/10/21 14:33:09

机器人地图模块重构:从栅格建模到导航避障的工程实践

我花了两周时间把自研机器人框架里的地图模块彻底重构了一遍&#xff0c;过程远比想象中折腾&#xff0c;但也确实想明白了不少东西。这篇就围绕lingbot-map这个模块&#xff0c;把当初为什么这样设计、核心的数据模型怎么定、建图到导航整条链路怎么打通&#xff0c;以及实机调…

作者头像 李华
网站建设 2026/10/21 14:33:06

SIGCOMM 2026投稿攻略:从选题到Camera Ready的实战指南

如果你在做网络方向的研究&#xff0c;那么SIGCOMM这个缩写你应该不陌生。它在中国计算机学会&#xff08;CCF&#xff09;的推荐名单里属于A类学术会议&#xff0c;这个A类意味着什么&#xff0c;说直白点&#xff1a;在计算机网络领域&#xff0c;它是国内外高校、研究机构共…

作者头像 李华
网站建设 2026/10/21 14:33:04

一文读懂TUN/TAP虚拟网络设备:原理、区别与实战

搞网络的人&#xff0c;迟早会迎面撞上 TUN/TAP 这两个名字。哪怕你不做内核开发&#xff0c;在虚拟化、容器、软件定义网络的日常里&#xff0c;总会在某个配置文件或者某条启动脚本里看到tun0、tap0的身影。它们看起来像是网卡&#xff0c;又不像物理网卡那样插在机箱上&…

作者头像 李华
网站建设 2026/10/21 14:32:51

AI Agent落地指南:从Chatbot到闭环执行的关键技术与避坑实践

现在聊AI Agent&#xff0c;基本绕不开一个现实&#xff1a;这个词在2025年已经被用滥了&#xff0c;但很少有人真正把它讲透。我过去一年多里&#xff0c;从最早把每个Agent当成“会聊天的接口”&#xff0c;到现在自己搭了一套能跑批任务的智能体工作流&#xff0c;中间反复推…

作者头像 李华