下载帮

您现在的位置是:首页 > 数据库 > MongoDB

MongoDB

MongoDB高并发下的upsert问题

2022-06-12 18:20MongoDB

MongoDB高并发下的upsert问题

一、问题现象

运行中心同事报告生产环境日志每天0点附近都会出现几笔错误日志,日志内容截图如下:

二、问题分析

系统采用MongoDB数据库,该表建有groupName+limitKey组合唯一索引,所以分析下来第一感觉就是重复执行insert报duplicate key错误了。

找到对应的业务代码如下:

        return mongoTemplate.findAndModify(
                new Query()
                        .addCriteria(Criteria.where("limitKey").is(limitKey))
                        .addCriteria(Criteria.where("groupName").is(groupName)),
                new Update()
                        .inc("cnt", delta)
                        .set("updateTime", LocalDateTime.now())
                        .setOnInsert("createTime", LocalDateTime.now())
                        .setOnInsert("expireTime", keepAliveDays == 0 ? null : LocalDateTime.now().plusDays(keepAliveDays)),
                new FindAndModifyOptions()
                        // 先查询,如果没有符合条件的,会执行插入,插入的值是查询值 + 更新值
                        .upsert(true)
                        // 返回当前最新值
                        .returnNew(true),
                LimitKeyStatEntity.class);

执行upsert操作,即首先查库里是否已存在记录,存在则执行update更新操作,否则执行insert插入操作。

代码逻辑并未发现有啥问题,但是为啥就报insert冲突了呢?

经上网查询发现,MongoDB本身是有文档级的写入锁的。也就是说,当一个进程开始修改一个文档时,该文档被锁定,其他文档不可以再对其进行写入甚至读取。这个写入锁的存在本身就是为了防止不同程序更新文档时产生的写入冲突。然而,update其实分为两步。首先是搜索文档位置,然后再执行文档更新。

因此MongoDB在高并发的情况下,可能会出现以下问题:

  • 两个线程t1,t2同时update一条记录时
  • t1读,记录不存在;t2读,记录不存在。
  • t1写,执行insert,插入成功
  • t2写,执行insert,由于唯一索引原因,重复插入失败。

再对应查到这几笔累计请求,确实系生产上某个大商户每天0点开始频繁发退货,而且退货间隔时间小到1ms导致的。

三、解决方案

mongoTemplate的findAndModify()方法目前暂无好的办法来解决高并发下上述缺陷,只能从业务方案上来解决。

由于该应用场景为消费订单MQ消息并执行额度累计,因此在写MongoDB异常导致业务上更新累计值失败时,认为消费MQ消息失败并抛出异常,利用RocketMQ消息失败重试机制,重新消费并执行额度累计。

文章评论