MongoTemplate下MongoDB4.2单upsert实现计数器增量快照同步方案问询
问题原因
你之前的写法使用的是MongoDB普通更新操作语法,这类语法中$set的字段值会被当做字面量处理,不会解析$counters这类字段引用,因此才会直接把字符串"$counters"存入目标字段。
可行方案:使用聚合管道更新(满足你的版本要求)
MongoDB 4.2及以上版本支持将聚合管道作为更新参数,管道内支持引用文档自身字段,且整个更新操作是单文档原子操作,完全满足你单次upsert、增量和快照一致的需求。
Shell 参考写法
db.getCollection('counterSets').update( { "keys.country" : "US", "keys.channel" : "c999" }, // 传入聚合管道数组作为更新逻辑 [ // 第一阶段:处理计数器增量,不存在时默认初始值为0再累加 { $set: { "counters.sale": { $add: [ { $ifNull: [ "$counters.sale", 0 ] }, 10 ] } } }, // 第二阶段:写入查询条件和增量快照,保留increments下已有的其他UUID记录 { $set: { "keys": { "country": "US", "channel": "c999" }, "increments": { $mergeObjects: [ { $ifNull: [ "$increments", {} ] }, { "3000c058-b8a7-4cff-915b-4979ef9a6ed9": { "counters": "$counters" } } ] } } } ], { upsert: true } )
MongoTemplate 对应实现
import org.springframework.data.mongodb.core.query.*; import org.springframework.data.mongodb.core.aggregation.*; import org.springframework.data.mongodb.core.query.Criteria; // 构造查询条件 Query query = new Query(); query.addCriteria(Criteria.where("keys.country").is("US").and("keys.channel").is("c999")); // 构造聚合更新管道 AggregationUpdate update = Aggregation.newUpdate() // 第一阶段:增量counters.sale,无初始值时默认0 .set("counters.sale").toValue( ArithmeticOperators.Add.valueOf( ConditionalOperators.ifNull("counters.sale").then(0) ).add(10) ) // 第二阶段:写入keys和增量快照 .set("keys").toValue(Document.parse("{\"country\": \"US\", \"channel\": \"c999\"}")) .set("increments").toValue( MergeObjects.mergeObjectsOf( ConditionalOperators.ifNull("increments").then(new Document()), new Document("3000c058-b8a7-4cff-915b-4979ef9a6ed9", new Document("counters", "$counters")) ) ); // 执行upsert mongoTemplate.upsert(query, update, "counterSets");
低版本备选方案(MongoDB <4.2 时使用)
如果你的环境后续降级到4.2以下版本,无法使用聚合管道更新,可以用两次原子操作实现:
- 用
findAndModify执行$inc增量操作,设置返回更新后的新文档 - 用返回的最新
counters值作为参数,执行第二次更新写入increments对应快照
该方案虽然是两次请求,但单文档操作本身都是原子性,不会出现脏数据,仅在极端异常场景下可能出现快照写入失败的情况,对一致性要求极高的场景可以搭配MongoDB事务实现。
内容的提问来源于stack exchange,提问作者gunboxer
相关产品推荐
相关产品推荐

