springboot mongodb 批量(bulkOps)插入或更新(upsert)数据

参与的项目中有一个业务场景,有两个消费者从kafka中拉取数据消费:

1、订单消费者:从订单队列中拉取订单数据,插入到mongodb,集合名称为order(主要包含id、orderNo、orderName、status)

2、订单状态消费者:从订单状态队列中拉取数据,更新order集合中的状态字段(status)

存在问题:

1、订单状态消费者消费依赖于订单消费者,如果订单消费者消费速度慢了,订单还没有插入到mongodb中,订单状态根据订单号orderNo去更新数据,则会找不到订单数据,导致订单状态更新失败

2、订单量比较大,一天千万级别,需要批量存储到mongodb,否则频繁操作数据库效率会很低

解决方案:

1、订单数据使用mongotemplate bulkOps批量插入(insert)mongo,订单状态数据使用update,如果没有找到订单数据,则将订单状态数据重新扔到kafka中稍后再消费

2、订单数据和订单状态数据都使用bulkOps批量插入或更新(upsert)到mongo,这样先消费订单数据或者先消费订单状态数据都没有问题

方案分析:

1、方案一是可行的,当时考虑kafka默认不支持延迟消息,如果更新订单状态的时候没有找到订单马上放到kafka中,可能还是找不到订单数据,当积压比较大的时候,可能会被重复消费很多次。kafka要支持延迟消息,比如未找到订单数据30秒后再消费,需要自己重新开发,觉得有点麻烦就没有采用。(rocketmq支持延迟消息支持比较好)

2、方案二也是可行的,但是会存在一个并发问题,即mongodb在同一时间upsert 订单号orderNo相同的订单数据和订单状态数据,并发量高了的情况会在mongodb中产生2条数据,这显然不是我们想要的,于是在order集合中将orderNo设置唯一索引。

那么在高并发下设置了唯一索引同时upsert相同的订单数据和订单状态数据会不会出现问题?答案是会的,会偶尔出现订单数据upsert或者订单状态数据upsert的时候报错,报唯一索引冲突。那怎么解决?在代码里面使用失败重试操作,当出现upsert错误的时候,尝试几秒后重新upsert。我项目里面使用的是guava-retrying。

所以我的整个解决思路大概如下:

列举几个里面的代码片段:

    /**
     * 定义retryer,指定轮询条件,及结束条件
     */
    private Retryer<Boolean> retryer = RetryerBuilder.<Boolean>newBuilder()
            .retryIfResult(Predicates.<Boolean>isNull())
            .retryIfExceptionOfType(Exception.class)
            .retryIfRuntimeException()
            // 重试5次
            .withStopStrategy(StopStrategies.stopAfterAttempt(5))
            // 等待100毫秒
            .withWaitStrategy(WaitStrategies.fixedWait(100L, TimeUnit.MILLISECONDS))
            .build();
    /**
     * 批量插入更新记录Record
     *
     * @param recordList
     */
    @Override
    public void upsertReceiveCollection(List<Record> recordList) {
        List<Pair<Query, Update>> updateList = new ArrayList<>(recordList.size());
        recordList.forEach(record -> {
            Query query = new Query(new
                    Criteria("orderNo").is(record.getOrderNo()));
            Update update = new Update();
            update.set("status", record.getStatus());
            Pair<Query, Update> updatePair = Pair.of(query, update);
            updateList.add(updatePair);
        });

        // 执行记录upsert操作
        doUpsertReceive(updateList);
    }

/**
     * 执行记录upsert操作
     *
     * @param updateList
     */
    private void doUpsertReceive(List<Pair<Query, Update>> updateList) {
        BulkOperations operations = mongoTemplate.bulkOps(BulkOperations.BulkMode.UNORDERED, COLLECTION_NAME);

        //callable是定义具体的业务操作,并返回该操作结果的返回值
        Callable<Boolean> callable = new Callable<Boolean>() {
            @Override
            public Boolean call() throws Exception {
                operations.upsert(updateList);

                BulkWriteResult result = operations.execute();

                log.info("upsertReceive result, insertCount: {}, matchCount: {}, modifyCount:{}",
                        result.getInsertedCount(),
                        result.getMatchedCount(), result.getModifiedCount());
                return true;
            }
        };

        try {
            retryer.call(callable);
        } catch (RetryException e) {
            log.error("receive retry err: {}", e);
        } catch (Exception e) {
            log.error("receive err: {}", e);
        }
    }

mongotemplate bulkOps使用参考:MongoDB批量更新和批量插入的方式 - 晒太阳的兔子很忙 - 博客园

guava-retrying使用参考:异步回调实现- Guava Retryer - 纵码万水千山 - 博客园

总结:方案二也不是太严谨,非常极端情况下可能存在多次重试还是报唯一索引冲突情况,由于我的项目中的订单数据允许有一点点误差,几千万数据的情况下有几十条误差也影响不大。

改进方案:

1、可以将多次重试失败的数据入库,使用定时任务处理这些异常数据;

2、如果项目使用的是rocketmq,我会优先使用rocketmq延迟队列处理这种异常数据。

  • 5
    点赞
  • 31
    收藏
    觉得还不错? 一键收藏
  • 1
    评论
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值