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

MongoDB批量写入触发DuplicateKeyException问题求助

MongoDB批量写入重复键错误排查

我是MongoDB新手,执行集合保存查询时收到错误提示Bulk write operation error on server。

实体类代码

@Document("role")
@Data
@NoArgsConstructor
@RequiredArgsConstructor
@EqualsAndHashCode(exclude = "id") // 仅通过唯一字段`name`进行比较
public class Role {
    @Id
    private String id;
    @NotBlank
    @NonNull
    @Indexed(unique = true)
    private ERole name;
}

枚举ERole代码

public enum ERole {ROLE_USER, ROLE_MODERATOR, ROLE_ADMIN}

业务逻辑代码

Collection<Role> rolesFromDb = repository.findAll();  // (1)
Collection<Role> rolesFromEnumSet = Arrays.stream(ERole.values()).map(Role::new).collect(Collectors.toSet());
if (!(rolesFromDb.containsAll(rolesFromEnumSet) &&
     rolesFromEnumSet.containsAll(rolesFromDb))
) {
    rolesFromEnumSet.removeAll(rolesFromDb); // (2)
    repository.saveAll(rolesFromEnumSet); // (3)
}

数据库现有记录

Role(id=63dc16b253565a43cee65848, name=ROLE_USER)

执行到第(2)行后剩余集合

0 = {Role@9387} "Role(id=null, name=ROLE_ADMIN)"
1 = {Role@9388} "Role(id=null, name=ROLE_MODERATOR)"

数据库中并不存在这两条记录,为何在第(3)行会触发重复键错误?


问题原因分析

1. 数据库中已存在同名记录,但查询未正确加载

  • 枚举反序列化异常:如果MongoDB中存储的name字段格式与ERole的序列化格式不匹配(比如存的是枚举ordinal数字而非name字符串),查询时无法正确转换为ERole枚举值,导致rolesFromDb中的Role对象name字段为null。此时containsAll会误判数据库中不存在对应角色,进而尝试插入重复记录触发唯一键错误。
  • 数据一致性问题:如果查询findAll()时读取的是旧数据快照(比如使用副本集时读偏好设置为从节点,且从节点数据未同步主节点最新变更),会导致rolesFromDb不包含已存在的角色记录。

2. 并发执行冲突

多个线程或进程同时执行这段初始化逻辑时,第一个线程插入新角色后,第二个线程的findAll()在插入前已执行,仍会尝试插入相同角色,触发唯一键约束错误。

3. 索引或数据异常

  • 唯一索引未正确生效:虽然实体类标注了@Indexed(unique=true),但如果Spring Data MongoDB的spring.data.mongodb.auto-index-creation配置为false,索引可能未自动创建;不过这种情况通常不会触发重复键错误,除非手动创建了索引但存在脏数据。
  • 隐藏重复数据:之前的测试或异常流程可能导致数据库中存在name字段重复的记录,但当前查询未正确加载这些数据。

排查与修复步骤

  1. 直接验证数据库数据
    使用MongoDB客户端执行查询,确认role集合中是否存在目标角色:

    db.role.find({name: {$in: ["ROLE_ADMIN", "ROLE_MODERATOR"]}})
    
  2. 检查枚举序列化配置
    查看数据库中现有ROLE_USER记录的name字段值,确认是枚举name(如"ROLE_USER")而非ordinal数字(如0)。若格式不匹配,需调整枚举序列化规则或迁移数据库数据。

  3. 解决并发冲突
    将初始化逻辑改为幂等操作,改用MongoTemplate的upsert方法,根据name字段自动更新或插入,避免先查询再判断的竞态条件:

    Arrays.stream(ERole.values()).forEach(roleName -> {
        Query query = Query.query(Criteria.where("name").is(roleName));
        Update update = new Update();
        mongoTemplate.upsert(query, update, Role.class);
    });
    
  4. 验证索引状态
    执行命令查看集合索引,确认name字段的唯一索引存在:

    db.role.getIndexes()
    

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

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.08.02 11:46:07