当文档不在工作集中时,具有upsert和唯一索引原子性的MongoDB Update

时间:2015-05-22 19:28:29

标签: java multithreading mongodb concurrency

总之,当文档不是工作集的一部分(不在常驻内存中)时,我们在对现有文档进行并发更新时遇到了这种奇怪的行为。

更多详情:

给定具有唯一索引的集合,并且当在给定的现有文档上以upsert为真运行并发更新(3个线程)时,1到2个线程引发以下异常:

Processing failed (Write failed with error code 11000 and error message 'insertDocument :: caused by :: 11000 E11000 duplicate key error index: db1.col1.$key_1  dup key: { : 1008 }'):

根据文档,我希望所有这三个更新都能成功,因为我尝试更新的文档已经存在。相反,看起来它试图在几个或所有更新请求上进行插入,很少由于唯一索引而失败。

在文档上重复相同的并发更新不会引发任何异常。此外,在文档上使用find()将其带到工作集,然后在该文档上运行并发更新也按预期运行。 此外,使用具有相同查询和设置的findAndModify也没有相同的问题。

这是否按预期工作或我遗失了什么?

设定:

-mongodb java driver 3.0.1

运行MongoDB版本" 2.6.3"

-3节点副本集

查询:

BasicDBObject query = new BasicDBObject();  
query.put("docId", 123L);
collection.update (query, object, true, false);

指数:

name: docId_1
unique: true
key: {"docId":1}
background: true

于5月28日更新,以包含重现问题的示例代码。 在本地运行MongoDB如下(请注意,测试将写入约4 GB的数据): ./mongodb-osx-x86_64-2.6.10/bin/mongod --dbpath / tmp / mongo 运行以下代码,重新启动数据库,注释掉" fillUpCollection(testMongoDB.col1,value,0,300);",然后再次运行代码。根据机器的不同,您可能需要调整一些数字才能看到异常。

package test;

import com.mongodb.BasicDBObject;
import com.mongodb.DBCollection;
import com.mongodb.DBObject;
import com.mongodb.Mongo;
import com.mongodb.MongoClient;

import java.io.IOException;
import java.util.ArrayList;
import java.util.List;
import java.util.Random;

public class TestMongoDB {
    public static final String DOC_ID = "docId";
    public static final String VALUE = "value";
    public static final String DB_NAME = "db1";
    public static final String UNIQUE = "unique";
    public static final String BACKGROUND = "background";
    private DBCollection col1;
    private DBCollection col2;

    private static DBCollection getCollection(Mongo mongo, String collectionName) {
        DBCollection col =  mongo.getDB(DB_NAME).getCollection(collectionName);
        BasicDBObject index = new BasicDBObject();
        index.append(DOC_ID, 1);
        DBObject indexOptions = new BasicDBObject();
        indexOptions.put(UNIQUE, true);
        indexOptions.put(BACKGROUND, true);
        col.createIndex(index, indexOptions);
        return col;
    }

    private static void storeDoc(String docId, DBObject doc, DBCollection dbCollection) throws IOException {
        BasicDBObject query = new BasicDBObject();
        query.put(DOC_ID, docId);
        dbCollection.update(query, doc, true, false);
        //dbCollection.findAndModify(query, null, null, false, doc, false, true);
    }

    public static void main(String[] args) throws Exception{
        final String value = new String(new char[1000000]).replace('\0', 'a');
        Mongo mongo = new MongoClient("localhost:27017");
        final TestMongoDB testMongoDB = new TestMongoDB();
        testMongoDB.col1 = getCollection(mongo, "col1");
        testMongoDB.col2 = getCollection(mongo, "col2");

        fillUpCollection(testMongoDB.col1, value, 0, 300);
        //restart Database, comment out previous line, and run again
        fillUpCollection(testMongoDB.col2, value, 0, 2000);
        updateExistingDocuments(testMongoDB, value);
    }

    private static void updateExistingDocuments(TestMongoDB testMongoDB, String value) {
        List<String> docIds = new ArrayList<String>();
        for(int i = 0; i < 10; i++) {
            docIds.add(new Random().nextInt(300) + "");
        }
        multiThreadUpdate(testMongoDB.col1, value, docIds);
    }


    private static void multiThreadUpdate(final DBCollection col, final String value, final List<String> docIds) {
        Runnable worker = new Runnable() {
            @Override
            public void run() {
                try {
                    System.out.println("Started Thread");
                    for(String id : docIds) {
                        storeDoc(id, getDbObject(value, id), col);
                    }
                } catch (Exception e) {
                    System.out.println(e);
                } finally {
                    System.out.println("Completed");
                }
            }
        };

        for(int i = 0; i < 8; i++) {
            new Thread(worker).start();
        }
    }

    private static DBObject getDbObject(String value, String docId) {
        final DBObject object2 = new BasicDBObject();
        object2.put(DOC_ID, docId);
        object2.put(VALUE, value);
        return object2;
    }

    private static void fillUpCollection(DBCollection col, String value, int from, int to) throws IOException {
        for(int i = from ; i <= to; i++) {
            storeDoc(i + "", getDbObject(value, i + ""), col);
        }
    }
}

第二次运行时的示例输出:

Started Thread
Started Thread
Started Thread
Started Thread
Started Thread
Started Thread
Started Thread
Started Thread
com.mongodb.DuplicateKeyException: Write failed with error code 11000 and error message 'insertDocument :: caused by :: 11000 E11000 duplicate key error index: db1.col1.$docId_1  dup key: { : "290" }'
Completed
com.mongodb.DuplicateKeyException: Write failed with error code 11000 and error message 'insertDocument :: caused by :: 11000 E11000 duplicate key error index: db1.col1.$docId_1  dup key: { : "170" }'
Completed
com.mongodb.DuplicateKeyException: Write failed with error code 11000 and error message 'insertDocument :: caused by :: 11000 E11000 duplicate key error index: db1.col1.$docId_1  dup key: { : "241" }'
Completed
com.mongodb.DuplicateKeyException: Write failed with error code 11000 and error message 'insertDocument :: caused by :: 11000 E11000 duplicate key error index: db1.col1.$docId_1  dup key: { : "127" }'
Completed
com.mongodb.DuplicateKeyException: Write failed with error code 11000 and error message 'insertDocument :: caused by :: 11000 E11000 duplicate key error index: db1.col1.$docId_1  dup key: { : "120" }'
Completed
com.mongodb.DuplicateKeyException: Write failed with error code 11000 and error message 'insertDocument :: caused by :: 11000 E11000 duplicate key error index: db1.col1.$docId_1  dup key: { : "91" }'
Completed
com.mongodb.DuplicateKeyException: Write failed with error code 11000 and error message 'insertDocument :: caused by :: 11000 E11000 duplicate key error index: db1.col1.$docId_1  dup key: { : "136" }'
Completed
Completed

2 个答案:

答案 0 :(得分:1)

您的查询过于具体,即使创建了文档也无法找到该文档,例如:不仅要搜索独特的领域。然后upsert尝试再次创建它(另一个线程)但是因为它实际存在而失败,但是没有找到。有关详细信息,请参阅http://docs.mongodb.org/manual/reference/method/db.collection.update/#upsert-behavior

从doc:开始,为了避免多次插入同一文档,只使用upsert:如果查询字段是唯一索引的,则为true。 使用$ set等修改运算符将查询文档包含到upsert doc

如果您认为情况并非如此。请向我们提供有关您的索引的查询和一些信息。

更新

如果您尝试从cli运行代码,您将看到以下内容:

@course = Course.find_by_access_code(params[:access_code])
@instructor = @course.users.where("registrations.role = ?", 'instructor').limit(1)[0]

您有以下问题:

  • 您想要更新文档但找不到它。并且您的更新不包含修改运算符,因此您的 docid 字段将不会包含在新创建的文档中(或者更好地将其设置为null,并且null也只能在唯一索引中设置一次) )。
  • 下次尝试更新文档时,由于最后一步,您仍然无法找到它。因此,MongoDB尝试按照与之前相同的过程插入它,并再次失败。不允许第二个空值。

只需将您的更新查询更改为此,修改文档/ on upcase案例包括您的查询: db.upsert.update({“docid”:123},{$ set:{one:1两个:2}},TRUE,FALSE)

> db.upsert.ensureIndex({docid:1},{unique:true})
{
    "createdCollectionAutomatically" : true,
    "numIndexesBefore" : 1,
    "numIndexesAfter" : 2,
    "ok" : 1
}
> db.upsert.update({"docid":123},{one:1,two:2},true,false)
WriteResult({
    "nMatched" : 0,
    "nUpserted" : 1,
    "nModified" : 0,
    "_id" : ObjectId("55637413ad907a45eec3a53a")
})
> db.upsert.find()
{ "_id" : ObjectId("55637413ad907a45eec3a53a"), "one" : 1, "two" : 2 }
> db.upsert.update({"docid":123},{one:1,two:2},true,false)
WriteResult({
    "nMatched" : 0,
    "nUpserted" : 0,
    "nModified" : 0,
    "writeError" : {
        "code" : 11000,
        "errmsg" : "insertDocument :: caused by :: 11000 E11000 duplicate key error index: test.upsert.$docid_1  dup key: { : null }"
    }
})

答案 1 :(得分:1)

这看起来像MongoDB的已知问题,至少达到2.6版本。他们建议的解决方法是让您的代码在出错时重试upsert。 https://jira.mongodb.org/browse/SERVER-14322