使用MongoTemplate save方法偶发DuplicateKeyException问题求助
问题:MongoTemplate.save偶发DuplicateKeyException排查与解决
环境版本
- MongoDB: 3.4.12
- mongodb-driver-sync: 4.6.0
- spring-boot-starter-data-mongodb: 2.7.0
- Spring Boot: 2.7.0
问题现象
使用MongoTemplate的save方法存储记录时,偶发抛出DuplicateKeyException。按预期save内部应执行upsert逻辑,正常情况下不应出现该问题。此问题仅在生产环境偶现,测试环境无法复现。
错误日志如下:
org.springframework.dao.DuplicateKeyException: Write operation error on server mongodb1:27017. Write error: WriteError{code=11000, message='E11000 duplicate key error collection: push-db.devices index: _id_ dup key: { : { platformId: 15, uuid: "eW9Zale3ST2EPPtFLQf3R5:APA91bGcXIc4ZIQZ3qVNTp1nDo981oPmj4CR5EXJspU-Ge_oQq1b12v0HLP7E-CPjF5qrK44K7Zr5Fszl0c6tGEmboKg2QrZoQpwFTAb6a_pICvX8V8ZJwbVIW1aMrC..." } }', details={}}.; nested exception is com.mongodb.MongoWriteException: Write operation error on server mongodb1:27017. Write error: WriteError{code=11000, message='E11000 duplicate key error collection: push-db.devices index: _id_ dup key: { : { platformId: 15, uuid: "eW9Zale3ST2EPPtFLQf3R5:APA91bGcXIc4ZIQZ3qVNTp1nDo981oPmj4CR5EXJspU-Ge_oQq1b12v0HLP7E-CPjF5qrK44K7Zr5Fszl0c6tGEmboKg2QrZoQpwFTAb6a_pICvX8V8ZJwbVIW1aMrC..." } }', details={}}. at org.springframework.data.mongodb.core.MongoExceptionTranslator.translateExceptionIfPossible(MongoExceptionTranslator.java:106) at org.springframework.data.mongodb.core.MongoTemplate.potentiallyConvertRuntimeException(MongoTemplate.java:3044) at org.springframework.data.mongodb.core.MongoTemplate.execute(MongoTemplate.java:600) at org.springframework.data.mongodb.core.MongoTemplate.saveDocument(MongoTemplate.java:1595) at org.springframework.data.mongodb.core.MongoTemplate.doSave(MongoTemplate.java:1530) at org.springframework.data.mongodb.core.MongoTemplate.save(MongoTemplate.java:1473) at org.springframework.data.mongodb.core.MongoTemplate.save(MongoTemplate.java:1458)
已确认信息
1. _id索引存在
{ "v" : 1, "key" : { "_id" : 1 }, "name" : "_id_", "ns" : "push-db.devices" }
2. 集合数据结构
集合的_id为嵌套对象,由platformId和uuid组成:
{ "_id" : { "platformId" : 15, "uuid" : "eW9Zale3ST2EPPtFLQf3R5:APA91bGcXIc4ZIQZ3qVNTp1nDo981oPmj4CR5EXJspU-Ge_oQq1b12v0HLP7E-CPjF5qrK44K7Zr5Fszl0c6tGEmboKg2QrZoQpwFTAb6a_pICvX8V8ZJwbVIW1aMrCasq2323232" }, "appId" : 32342, "appVersion" : "33.556.66", "tags" : [ { "_id" : 39391, "lastLogin" : NumberLong(1666903871334), "loginCount" : 1 }, { "_id" : 34269, "lastLogin" : NumberLong(1666903871526), "loginCount" : 1 } ], "_class" : "com.myCompany.device.PushDevice" }
3. 存储逻辑代码
private final MongoTemplate mongoTemplate; @Override public void storeDevice(PushDevice device) { mongoTemplate.save(device); }
排查思路与解决方案
1. 明确save方法的执行逻辑
Spring Data MongoDB的save方法逻辑分两种情况:
- 若实体
_id不为null,执行upsert(存在则更新,不存在则插入) - 若实体
_id为null,执行insert
偶发的重复键错误,大概率是高并发下多个线程同时处理同一设备记录,且都判断记录不存在,执行insert操作导致冲突。
- 检查
PushDevice的_id生成逻辑:是否存在多线程同时为同一platformId+uuid生成_id,但未先查询记录是否存在就执行save? - 模拟高并发场景验证:用压测工具模拟多线程同时存储同一设备,看是否能复现问题。
2. 显式使用upsert替代save
save的逻辑依赖_id的存在性判断,不如显式调用upsert更可靠,无论记录是否存在都会执行插入或更新:
@Override public void storeDevice(PushDevice device) { // 基于_id的嵌套字段构建查询条件 Query query = Query.query(Criteria.where("_id.platformId").is(device.getId().getPlatformId()) .and("_id.uuid").is(device.getId().getUuid())); // 构建更新字段 Update update = Update.update("appId", device.getAppId()) .set("appVersion", device.getAppVersion()) .set("tags", device.getTags()); // 执行upsert mongoTemplate.upsert(query, update, PushDevice.class); }
3. 调整副本集写入一致性
MongoDB副本集默认writeConcern为w=1(仅确认主节点写入成功),若主节点写入后未同步到从节点就发生切换,可能引发一致性问题。可调整为w=majority,确保多数节点写入成功:
// 配置MongoTemplate的write concern mongoTemplate.setWriteConcern(WriteConcern.MAJORITY);
4. 检查UUID的完整性与合法性
错误日志中UUID被截断(末尾带...),需确认:
- 实际传入的UUID是否存在长度差异、前后空格或不可见字符,导致生成的
_id看似相同实际不同,但插入时触发重复键? - 验证
PushDevice中UUID字段的存储逻辑,是否存在截断或转义问题。
5. 版本兼容性排查
MongoDB 3.4.12已停止官方支持,与4.6.0版本的驱动可能存在兼容性隐患:
- 尝试升级MongoDB到3.6及以上版本(推荐4.x系列),或降级驱动到3.x系列(匹配MongoDB 3.4的版本),观察问题是否消失。
6. 引入分布式锁控制并发
高并发场景下,可针对同一platformId+uuid引入分布式锁,确保存储操作串行执行:
- 基于Redis或MongoDB实现分布式锁,在
storeDevice方法执行前加锁,处理完成后释放锁,避免多线程同时插入同一记录。
内容的提问来源于stack exchange,提问作者George Anil John
相关产品推荐
相关产品推荐

