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
相关产品推荐
相关产品推荐

