You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

Spring Data MongoDB副本集高并发下偶发E11000重复键错误

问题根因

这个偶发E11000重复键错误和副本集读一致性配置无关,是高并发场景下典型的Check-Then-Act(先检查后执行)竞态漏洞:

  • 高负载下,两个携带相同userId的请求几乎同时到达服务端
  • 两个请求先后执行查询逻辑,此时集合中还没有对应userId的记录,因此都拿到“记录不存在”的结果,各自初始化了新的UserDetail对象,这类新对象的@Id字段为null
  • Spring Data MongoDB的save()方法默认逻辑是:实体@Id字段为null时执行插入,@Id有值时执行更新。两个请求先后执行save时,第一个请求插入成功,第二个请求执行插入就会触发userId字段的唯一索引冲突,抛出11000错误
  • 你配置的readPreference=Primary、readConcern=primary仅能保证读取的是主节点已提交的最新数据,完全无法解决两个请求同时在主节点读到“无记录”后,在查询和写入的时间窗口内发生的并发写入冲突。

额外注意:报错提示冲突的索引名为userId_1,和你在实体类@Indexed注解中定义的索引名idx_us_de_on_user_id不一致,说明集合上残留了历史版本自动创建的旧唯一索引,属于配置隐患,但不是触发本次偶发错误的核心原因。

修复方案

不要依赖“先查询判断存在性再写入”的非原子逻辑规避重复,直接用服务端原子操作或异常兼容处理解决:

方案1:使用MongoDB原生upsert原子操作(推荐)

upsert是MongoDB服务端原子执行的逻辑:匹配到查询条件时执行更新,匹配不到时自动插入,整个过程没有并发竞态窗口,是这类场景的最优解。
Spring Data MongoDB中可以用findAndModify配合upsert选项实现,示例代码:

import static org.springframework.data.mongodb.core.query.Criteria.where;
import static org.springframework.data.mongodb.core.query.Query.query;
import org.springframework.data.mongodb.core.FindAndModifyOptions;
import org.springframework.data.mongodb.core.query.Update;

// 构建原子更新逻辑
Update update = new Update()
        .setOnInsert("userId", userId)
        .setOnInsert("params", new ArrayList<>());
// 业务逻辑如果需要追加参数/修改字段,在这里追加update规则,比如往params加元素用update.addToSet("params", 目标值)

FindAndModifyOptions options = FindAndModifyOptions.options()
        .upsert(true) // 开启upsert原子模式
        .returnNew(true); // 可选,返回操作完成后的最新文档

// 执行原子操作,不会出现并发重复键问题
UserDetail userDetail = mongoTemplate.findAndModify(
        query(where("userId").is(userId)),
        update,
        options,
        UserDetail.class
);

方案2:捕获重复键异常做降级兼容

如果不想改动现有查询+业务处理的代码结构,可以直接在save操作外层捕获11000重复键对应的异常,触发冲突时重新查询已有记录,再基于已有记录执行业务逻辑后保存:

import org.springframework.dao.DuplicateKeyException;

try {
    userDetailRepository.save(userDetail);
} catch (DuplicateKeyException e) {
    // 并发插入触发唯一键冲突,查询已存在的持久化记录
    UserDetail existRecord = userDetailRepository.findByUserId(userId)
            .orElseThrow(() -> new RuntimeException("数据查询异常"));
    // 将之前的业务字段修改逻辑迁移到existRecord上执行,之后重新保存
    userDetailRepository.save(existRecord);
}

索引配置修复

在业务低峰期执行如下命令删除残留的旧索引,避免后续索引规则不一致引发的问题:

// mongo shell中执行
db.user_details.dropIndex("userId_1")

执行删索引操作前,确认业务没有依赖该旧索引的查询逻辑,避免操作引发查询性能波动。

内容的提问来源于stack exchange,提问作者Anurag

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.08.28 17:57:35