简体   繁体   中英

how to concurrently insert a new document into a collection in MongoDB 4 with .net driver 2.7.3

I am using MongoDB 4 and MongoDB .net Driver 2.7.3. I want to concurrently insert a new document in a collection so that there should be only one document in the collection. After the document is inserted into the collection (that is, set sequenceValue as 1), we only need to update the document (that is, increase sequenceValue by 1), instead of inserting any more new document.

In this collection named 'countersCollection', the structure of the new document is a Counter class like:

    public class Counter
    {
        [BsonId]
        public ObjectId _id { get; set; }
        public string name { get; set; }
        public long sequenceValue {get;set;}
        public DateTime date {get;set;}
    }

my code is like:

    Counter c = await this.countersCollection.AsQueryable().FirstOrDefaultAsync(x => x.name == counterName);                
    DateTime utcNow = DateTime.UtcNow;                                                                            
    if (c == null) // empty document
    {
        var options = new FindOneAndUpdateOptions<Counter, Counter>() {ReturnDocument = ReturnDocument.After, IsUpsert = true };            
        var filter = new FilterDefinitionBuilder<Counter>().Where(x => x.name == counterName);
        var update = new UpdateDefinitionBuilder<Counter>().Set(x => x.sequenceValue, 1).Set(x => x.date, utcNow);                                        
        seq = await this.countersCollection.FindOneAndUpdateAsync<Counter>(filter, update, options);                    
    }

The above code works well in a non-concurrent environment but does not work well concurrently. If multiple threads call the above code at the same time, it could create more than one counter document in the countersCollection.

Is there any way to make it work concurrently.

Thank you.

you can handle it by using optimistic concurrency approach

the algo can be like:

  1. you find Counter with the name == counterName
  2. build your filter like var filter = new FilterDefinitionBuilder<Counter>().Where(x => x.name == counterName && x.sequenceValue == c.sequenceValue );
  3. try to find and update and if the result is null retry

Ok. Let`s write some code.

  1. create separate exception for OptimisticConcurrency

     public class OptimisticConcurrencyException : Exception { } 
  2. create Counter class

     public class Counter { [BsonId] public ObjectId Id { get; set; } public string Name { get; set; } public long Version { get; set; } public DateTime Ddate { get; set; } } 
  3. create some simple retry logic

      public class CounterRepository { private readonly IMongoCollection<Counter> _countersCollection; public CounterRepository(IMongoCollection<Counter> countersCollection) { _countersCollection = countersCollection ?? throw new ArgumentNullException(nameof(countersCollection)); } public async Task<Counter> TryInsert(Counter counter) { var policy = Policy.Handle<OptimisticConcurrencyException>() .WaitAndRetryAsync(new[] { TimeSpan.FromSeconds(1), TimeSpan.FromSeconds(3), TimeSpan.FromSeconds(7) }); return await policy.ExecuteAsync(() => TryInsertInternal(counter)); } private async Task<Counter> TryInsertInternal(Counter counter) { var existingCounter = await _countersCollection.Find(c => c.Id == counter.Id).FirstOrDefaultAsync(); if (existingCounter == null) return await InsertInternal(counter); return await IncreaseVersion(existingCounter); } private async Task<Counter> InsertInternal(Counter counter) { try { counter.Version = 1; await _countersCollection.InsertOneAsync(counter); return counter; } // if smbd insert value after you called Find(returns null at that moment) // you try to insert entity with the same id and exception will be thrown // you try to handle it by marking with optimistic concurrency and retry policy // wait some time and execute the method and Find returns not null now so that // you will not insert new record but just increase the version catch (MongoException) { throw new OptimisticConcurrencyException(); } } private async Task<Counter> IncreaseVersion(Counter existing) { long versionSnapshot = existing.Version; long nextVersion = versionSnapshot + 1; var updatedCounter = await _countersCollection.FindOneAndUpdateAsync( c => c.Id == existing.Id && c.Version == versionSnapshot, new UpdateDefinitionBuilder<Counter>().Set(c => c.Version, nextVersion)); // it can be null if smbd increased existing version that you fetched from db // so you data is now the newest one and you throw OptimisticConcurrencyException if (updatedCounter == null) throw new OptimisticConcurrencyException(); return updatedCounter; } } 

The technical post webpages of this site follow the CC BY-SA 4.0 protocol. If you need to reprint, please indicate the site URL or the original address.Any question please contact:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM