一、需求背景
在高并发的场景下,如何安全的对账户执行操作。
二、问题
以下是我从项目中摘抄的一段代码:
// 查看该用户是否已经有该道具的记录
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指令往往同upsert、set指令配合使用。如果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相结合,可以完美的解决并发下账户操作原子性问题。