MongoDB Bulk write operation error on server duplicate key error问题解决

背景

在做数据推送功能遇到的一个问题。具体来说,通过SQL查询语句,将Impala中100多万条数据写入到MongoDB时报错。大致的报错信息如下:

java.lang.Exception: org.springframework.dao.DuplicateKeyException: Bulk write operation error on server 
101.202.303.404:7056. Write errors: [BulkWriteError{index=0, code=11000, message='E11000 duplicate key error collection:
pddai_cbd_report_api.autojobtab_fa42f748674d59091a3f71adf25de2d5 index: _id_ dup key: { _id:ObjectId('628cdb23d04f507321288fa5') }', details={}}]

理论

问题出在_id这个key重复。故而需要先提一点关于ObjectId的理论知识。

_id的特征:

  1. _id是集合中文档的主键,用于区分文档(记录)
  2. _id自动编入索引。指定{ _id: }的查找将使用_id索引。

默认情况下,_id字段的类型为 ObjectID,支持用户自定义。ObjectID 长度为 12 字节,由4部分组成:

  1. 4 字节的值,表示自 Unix 纪元以来的秒数
  2. 3 字节的机器标识符
  3. 2 字节的进程 ID
  4. 3 字节的计数器,以随机值开始

一般情况下,不建议使用自定义并覆盖已有的_id生成方式,除非是对全局唯一主键生成算法(如:snowflake等)比较熟悉,或者业务数据量非常大导致MongoDB自带的生成方式不满足要求等。

排查

在我的业务场景中,使用MongoDB字段的默认生成方式。

因此导致_id重复的原因只可能是:

  1. 插入两条一模一样的数据,即这两条数据的_id相同;
  2. 插入MongoDB中已存在的数据,后面插入的数据的_id是之前计算好的。

第1种情况:程序代码有Bug,确实是在插入两条一模一样的数据。

第2种情况,问题根源实际上还是第一种情况的衍生情况,比如说这样一个场景:比如2022年6月3日 13:29:43,根据这个时间戳生成一批_id数据。这批数据可能放在代码的内存中,在2022年6月3日 13:30:58,再次插入。

调试见真章:
在这里插入图片描述
此时才意识到自己犯下一个很愚蠢的错误:业务数据量还算较大,SQL查询百万甚至千万级别,从JDBC中获取结果时,不是一次性组装到List<Document>,而是分批组装塞到list里面去,batchNum=500000。分批insert到MongoDB中,满足batchNum后,List<Document>没有重新new ArrayList<Document>初始化。

问题解决

是不是很小白的问题,是不是感觉很傻?

一个数组list或者集合collection,进行for循环,取list或者collection里面的数据赋值到另外一个list<实体类>里面,在for循环里面需要对实体类进行初始化。又或者遇到批量取数的情况,也是需要对list<实体类>进行初始化。

借口:之所以出错,是因为他人写的代码,if,else,for,while循环层层嵌套(不下8层),导致自己在维护他人的代码时犯迷糊。

经过简化的错误的代码片段:

List<Document> documentList = new LinkedList<>();
int q = 0;
boolean isLast = false;
long allCount = 0L;
while (!isLast) {
    String execSql = this.buildExecSql(driver, sql);
    ResultSet rs = ps.executeQuery(execSql);
    ResultSetMetaData metaData = rs.getMetaData();
    int columnCount = metaData.getColumnCount();
    while (rs.next()) {
        allCount++;
        Document document = new Document();
        for (int j = 0; j < columnCount; j++) {
        	document.append(metaData.getColumnLabel(j + 1), rs.getObject(j + 1));
        }
        documentList.add(document);
    }
    if (q == 0 && documentList.size() <= 0) {
        Document document = new Document();
        for (int j = 0; j < columnCount; j++) {
        	// 存空值
            document.append(metaData.getColumnLabel(j + 1), null);
        }
        documentList.add(document);
    }
    totalCount += documentList.size();
    if (documentList.size() < batchNum) {
        isLast = true;
    }
    // 如果循环的是第一次先删除再创建
    if (q == 0) {
        // 10天有效期, createCollectionWithExpire会先检查集合是否存在, 如果存在, 则先删除, 再新建集合
        MongodbUtil.createCollectionWithExpire(mongoTemplate, key, 10 * 60 * 60 * 24);
    }
    // 是否需要缓存到MongoDB数据库
    if (cacheApi) {
        MongodbUtil.insertCollection(mongoTemplate, documentList, key);
    }
    q++;
}

结论

其实对于MongoDB稍微有所了解,整明白其中的原理之后,答案和解决思路就很明显。

  • 1
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 2
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 2
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

johnny233

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值