如何优雅实现不存在插入否则更新,和mongodb upsert

前言

业务中经常有这样的场景:当新增某需求时,业务需要使用新表,该表的记录之前的数据没有,业务上线后需要保证对每个业务实体,该表的记录只有一条

举个例子:当官网上线新手引导任务功能时,需要在新建的任务表,为每个站点记录该站点完成了哪些任务,跳过了哪些任务,对于之前创建的站点,任务表显然是没有该记录

此时一般有两种做法:

  1. 在需求上线前,给每个现场站点都刷一条初始化记录。但涉及到刷数据操作,比较麻烦
  2. 不刷数据,每次操作时判断有没有记录,有则更新,否则新增

但是第二种做法中,如果没有给任务表针对站点id加唯一索引,可能因为并发问题插入两条记录。如果加了唯一索引(建议加上),在并发操作中有些请求在第一次插入时遇到唯一键冲突,需要手动改为更新操作进行重试,比较麻烦

有没有比较优雅的写法呢?

mongodb

mongodb提供了upsert操作,实现没有就插入,否则更新的功能

我们看一个例子,表site_task用于保存每个站点完成的任务,有两个字段:

  • site_id:站点id
  • task_info:数组结构,存放已完成的任务

执行如下操作:

db.site_task.updateOne(

    {

        site_id:"a"

    },  // filter部分

    {

        $addToSet:

            {

                task_info:{

                    task_id:"123",  // 任务id

                    status:1,     // 1:已完成

                    operator:112  //  操作人

                 }

            }

    },  // update部分

    {

        upsert:true

    }

)

往任务表中插入站点a的任务记录,如果站点a不存在,就插入该记录,内容为filter和update部分,否则就更新。

需要注意的是,使用mongodb的upsert时,需要给filter用到的字段设置唯一索引

因为update操作不是原子的,分为两步:先搜索文档,再更新

  • 由于会先搜搜存不存在,因此如果filter条件没有索引,该步骤速度很慢
  • 执行upsert操作时,当两个请求同时发现该文档不存在,都进行插入,在没有唯一索引约束的情况下就会插入重复记录

检测和插入操作也不可能在一个原子操作中,因为此时该记录不存在,不可能在该记录加锁,若要保证原子性,就需要加类似表锁的锁来防止并发插入,成本较大

解决方法就是在filter用到的条件加唯一索引,当insert出现duplicate key异常时,重试该操作即可,此时重试就一定执行update符合预期。在4.1.6版本以后会自动重试(SERVER-37124),若使用4.1.6版本以下,需要看看使用的sdk是否针对这种情况自动重试,否则需要手动重试

除了文档本身,若文档有数组类型的字段,也可能出现并发问题

当记录有了后,需要往数组类型的字段中追加数据时,若采用先查出数据,在服务中追加,再写入数据库的方式,遇到并发时可能会更新丢失

mongdb给数组追加提供了两个修改器:$push$addToSet

  • $push:直接往数组后面追加

  • $addToSet:若数组中有该元素,就忽略,否则追加

    • 需要注意如果元素是对象,需要对象中每个字段都相等,才判定为已经有元素

例如:当执行如下操作后,

db.site_task.updateOne(

    {

        site_id:"a"

    },  // filter部分

    {

        $addToSet:

            {

                task_info:{

                    task_id:"123",
                    status:1,
                    operator:112
                    
                 }

            }

    },  // update部分

    {

        upsert:true

    }

)

此时db中数据如下:

{ _id: ObjectId("626769601ab426660b7da987"),

  site_id: 'a',

  task_info: [ { task_id: '123', status: 1, operator: 112 } ] }

再执行一次该语句,db中数据不变,因为该对象已经存在

如果修改要添加的元素中某个字段:

db.site_task.updateOne(

    {

        site_id:"a"

    },  // filter部分

    {

        $addToSet:

            {

                task_info:{

                    task_id:"124",
                    status:1,
                    operator:112

                 }

            }

    },  // update部分

    {

        upsert:true

    }

),{upsert:true})

该字段就会有两个元素:

{ _id: ObjectId("626769601ab426660b7da987"),

  site_id: 'a',

  task_info: 

   [ { task_id: '123', status: 1, operator: 112 },

     { task_id: '124', status: 1, operator: 112 } ] }

如果使用push,就会无条件往后增加元素

这两个操作能保证原子性,不会发生更新丢失的问题,更多原子性操作可以参考MongoDB 原子操作

原子追加操作配合upsert,就能实现没有记录就新建,否则往记录某个字段增加,且不会更新丢失的效果

mysql

若db选型为mysql,可以使用on duplicate key update,如果不存在就添加,如果存在就修改,且自动实现了遇到重复key就改为更新操作,示例如下:

insert into table(field1,field2,field3) values(1,2,3) on duplicate key update field1=11,field2=22,field3=33

总结

本文介绍了如何优雅实现没有就新增,否则更新,且在数组字段上实现原子追加的方法

你可能感兴趣的:(mongodb,数据库)