MongoDB作为具有WriteConcern多数和ReadConcern Linearizable

时间:2019-01-07 07:14:18

标签: c# multithreading mongodb mongodb-.net-driver

我在应用程序中获得了一个我想使用Mongo(3.6)作为多线程锁(在不同服务器上)的位置。本质上类似“如果一个线程开始工作,其他线程应该通过mongo看到它,并且不要并行开始相同的工作”。

从我学到的documentation

  

结合“多数”写关注,“可线性化”读关注使多个线程可以在单个文档上执行读写操作,就像单个线程实时执行这些操作一样。

这对我来说听起来不错,如果一个线程开始工作,我会插入某个文档,而其他线程会检查该文档是否已经存在,如果不存在,则不要启动,但是对于我的情况不起作用。

我准备了两个测试-一个非并行成功阻塞了第二个线程-但并行测试失败,我得到了两个RebuildLog文档。

using System;
using System.Threading.Tasks;
using FluentAssertions;
using Xunit;
using MongoDB.Bson.Serialization.Attributes;
using MongoDB.Driver;

namespace FindOneAndUpdateTests
{
    public class FindOneAndUpdateTests
    {
        private static IMongoDatabase GetDatabase()
        {
            var dbName = "test";
            var client = new MongoClient("mongodb://localhost:45022");

            return client.GetDatabase(dbName);
        }

        private IMongoCollection<RebuildLog> GetCollection()
        {
            return GetDatabase().GetCollection<RebuildLog>("RebuildLog");
        }

        [Fact]
        public async Task FindOneAndUpdate_NotParallel_Test()
        {
            var dlpId = Guid.NewGuid();

            var first = await FindOneAndUpdateMethod(dlpId);
            var second = await FindOneAndUpdateMethod(dlpId);

            first.Should().BeFalse();
            second.Should().BeTrue();
        }

        [Fact]
        public async Task FindOneAndUpdate_Parallel_Test()
        {
            var dlpId = Guid.NewGuid();

            var taskFirst = FindOneAndUpdateMethod(dlpId);
            var taskSecond = FindOneAndUpdateMethod(dlpId);

            var first = await taskFirst;
            var second = await taskSecond;

            first.Should().BeFalse();
            second.Should().BeTrue();
        }

        private async Task<bool> FindOneAndUpdateMethod(Guid dlpId)
        {
            var mongoCollection = GetCollection();

            var filterBuilder = Builders<RebuildLog>.Filter;
            var filter = filterBuilder.Where(w => w.DlpId == dlpId);

            var creator = Builders<RebuildLog>.Update
                .SetOnInsert(w => w.DlpId, dlpId)
                .SetOnInsert(w => w.ChangeDate, DateTime.UtcNow)
                .SetOnInsert(w => w.BuildDate, DateTime.UtcNow)
                .SetOnInsert(w => w.Id, Guid.NewGuid());

            var options = new FindOneAndUpdateOptions<RebuildLog>
            {
                IsUpsert = true,
                ReturnDocument = ReturnDocument.Before
            };

            var result = await mongoCollection
                .WithWriteConcern(WriteConcern.WMajority)
                .WithReadConcern(ReadConcern.Linearizable)
                .FindOneAndUpdateAsync(filter, creator, options);

            return result != null;
        }
    }

    [BsonIgnoreExtraElements]
    public class RebuildLog
    {
        public RebuildLog()
        {
            Id = Guid.NewGuid();
        }

        public Guid Id { get; set; }
        public DateTime ChangeDate { get; set; }
        public string ChangeUser { get; set; }
        public Guid DlpId { get; set; }
        public string Portal { get; set; }
        public DateTime? BuildDate { get; set; }
    }
}

我的怀疑是,我对原子手工制作的GetOrInsert的想法(请参阅带有IsUpsert的FindOneAndUpdate)打破了文档中“在单个文档上”的约束。有解决这个问题的主意吗?还是不可能?

1 个答案:

答案 0 :(得分:0)

这很有趣。可能您在DlpId上没有唯一索引吗?这就是为什么mongo决定不需要顺序执行这些操作的原因,因为在您的情况下,这不是写后读模式(如“客户会话和因果一致性保证”中所述)。它是同时进行两次更新或创建的。 那这个呢? :

public class SyncDocument 
{
    // ...
    [BsonElement("locked"), BsonDefaultValue(false)]
    public bool Locked { get; set; }
}

在客户端代码中:

var filter = Builders<SyncDocument>.Filter.Eq(d => d.Locked, false);
var update = Builders<SyncDocument>.Update.Set(d => d.Locked, true);
var result = collection.UpdateOne(filter, update);
if (result.ModifiedCount == 1) {
    Console.WriteLine("Lock acquired");
}

“具有锁定的文档”字段应在应用程序启动之前创建(如果适用于您的任务)。