Mongo解决账户操作原子性问题

一、需求背景

在高并发的场景下,如何安全的对账户执行操作。

二、问题

以下是我从项目中摘抄的一段代码:

// 查看该用户是否已经有该道具的记录
BackPackData backPackData = backPackDataDao4Mongo.getUserBackPackPropData(uid, propId);
if (Objects.isNull(backPackData)) {
    // 如果不存在插入新的记录
    backPackData = new BackPackData()
            .setId(redisIdService.generate(BackPackData.class))
            .setUid(uid)
            .setDataId(propId)
            .setAmount(sourceEvent.getAmount())
            .setCreateTime(System.currentTimeMillis());
    backPackDataDao4Mongo.save(backPackData);
} else {
    // 如果存在,那么执行incr操作
    backPackDataDao4Mongo.incrPropAmount(uid, propId, sourceEvent.getAmount());
}

很显然,如果在高并发的场景下,多个线程同时查询不存在,那么会出现同时插入多条相同的记录,造成计数错误;

那么如果加上分布式锁,或者使用消息队列,使其串行执行,是否能保证正确性呢?

答案是否定的,原因就是mongo的主从复制存在延迟性,当第一条写操作在主节点执行完成后,第二次的从节点读操作一旦发生在主从同步之前,那么程序会认为不存在,再次执行写操作。

三、解决方法

原子操作实现:如果存在,更新,如果不存在,添加。

upsert

是一种特殊的更新,如果没有找到符合条件的更新条件的文档,就会以这个条件和更新文档为基础创建一个新的文档;如果找到了匹配的文档,就正常更新

执行命令:
db.getCollection('test').update({"_id": 3}, {"$inc": {"amount": 1}}, true);

查询结果(插入):
{
    "_id" : 3.0,
    "amount" : 1.0
}

执行命令:
db.getCollection('test').update({"_id": 3}, {"$inc": {"amount": 1}}, true);

查询结果(更新):
{
    "_id" : 3.0,
    "amount" : 2.0
}

不足:

  • 所有需要的属性都需要存在query或update条件中;
  • 当无法使用id作为query条件时,上述命令会插入一条id为uuid的数据。
执行命令:
db.getCollection('test').update({"uid": 3}, {"$inc": {"amount": 1}}, true)

查询结果(插入):
{
    "_id" : ObjectId("63512ae115160a87df5d0818"),
    "uid" : 3.0,
    "amount" : 1.0
}

$setOnInsert

s e t O n I n s e r t 指令往往同 u p s e r t 、 setOnInsert指令往往同upsert、 setOnInsert指令往往同upsertset指令配合使用。如果upsert设为true。当满足查询条件的记录存在,则不执行 s e t O n I n s e r t 中的操作,当满足条件的记录不存在则执行 setOnInsert中的操作,当满足条件的记录不存在则执行 setOnInsert中的操作,当满足条件的记录不存在则执行setOnInsert操作。与 s e t 指令配合使用,可以作为 set指令配合使用,可以作为 set指令配合使用,可以作为set指令的补充。当满足查询条件的记录存在,则执行 s e t 操作,当满足查询条件的记录不存在,则新增一条记录,其中包含 set操作,当满足查询条件的记录不存在,则新增一条记录,其中包含 set操作,当满足查询条件的记录不存在,则新增一条记录,其中包含set指令设置的属性以及$setOnInsert 指令设置的属性。

执行命令:

db.getCollection('test').update(
{"uid": 1}, {"$setOnInsert": { "_id" : 1 },
"$inc": {"amount": 1}},
true)

查询结果(插入):
{
    "_id" : 1.0,
    "uid" : 1.0,
    "amount" : 1.0
}

db.getCollection('test').update(
{"uid": 1}, {"$setOnInsert": { "_id" : 1 },
"$inc": {"amount": 1}},
true)

查询结果(更新):
{
    "_id" : 1.0,
    "uid" : 1.0,
    "amount" : 2.0
}

当update条件中只存在$setOnInsert,可实现不存在则插入,存在则跳过语义。

四、总结

通过upsert和$setOnInsert相结合,可以完美的解决并发下账户操作原子性问题。

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

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值