你如何处理两个客户端upsert之间的竞争?

时间:2013-07-08 20:07:09

标签: c# entity-framework concurrency

我正在编写一个简单的消息传递模块,因此一个进程可以发布消息,另一个进程可以订阅它们。我正在使用EF / SqlServer作为进程外通信机制。 “服务器”只是发布者/订阅者对的共同名称(可称为“频道”)。

我有以下方法向表示名为“Server”的数据库添加一行

    public void AddServer(string name)
    {
        if (!context.Servers.Any(c => c.Name == name))
        {
            context.Servers.Add(new Server { Name = name });
        }
    }

我遇到的问题是,当我同时启动两个客户端时,只有一个应该添加一个新的服务器条目,但是,这不是它的工作方式。我实际上得到了两个具有相同名称的条目的非常错误的结果,并且意识到Any()保护对此不够。

服务器实体使用int PK,据说我的存储库会强制使用Name字段的唯一性。我开始认为这不会起作用。

public class Server
{
    public int Id { get; set; }
    public string Name { get; set; }
}

我认为我能解决的两种方式似乎都不太理想:

  1. 字符串主键
  2. 忽略例外
  3. 这是并发问题,对吧?

    如果我希望两个客户端使用相同的名称调用存储库但在数据库中只获得一个具有该名称的行的结果,我该如何处理呢?

    enter image description here

    更新:这是存储库代码

    namespace MyBus.Data
    {
        public class Repository : IDisposable
        {
            private readonly Context context;
            private readonly bool autoSave;
    
            public delegate Chain Chain(Action<Repository> action);
            public static Chain Command(Action<Repository> action)
            {
                using (var repo = new Data.Repository(true))
                {
                    action(repo);
                }
                return new Chain(next => Command(next));
            }
    
            public Repository(bool autoSave)
            {
                this.autoSave = autoSave;
                context = new Context();
            }
    
            public void Dispose()
            {
                if (autoSave)
                    context.SaveChanges();
                context.Dispose();
            }
    
            public void AddServer(string name)
            {
                if (!context.Servers.Any(c => c.Name == name))
                {
                    context.Servers.Add(new Server { Name = name });
                }
            }
    
            public void AddClient(string name, bool isPublisher)
            {
                if (!context.Clients.Any(c => c.Name == name))
                {
                    context.Clients.Add(new Client
                    {
                        Name = name,
                        ClientType = isPublisher ? ClientType.Publisher : ClientType.Subscriber
                    });
                }
            }
    
            public void AddMessageType<T>()
            {
                var typeName = typeof(T).FullName;
                if (!context.MessageTypes.Any(c => c.Name == typeName))
                {
                    context.MessageTypes.Add(new MessageType { Name = typeName });
                }
            }
    
            public void AddRegistration<T>(string serverName, string clientName)
            {
                var server = context.Servers.Single(c => c.Name == serverName);
                var client = context.Clients.Single(c => c.Name == clientName);
                var messageType = context.MessageTypes.Single(c => c.Name == typeof(T).FullName);
                if (!context.Registrations.Any(c =>
                        c.ServerId == server.Id &&
                        c.ClientId == client.Id &&
                        c.MessageTypeId == messageType.Id))
                {
                    context.Registrations.Add(new Registration
                    {
                        Client = client,
                        Server = server,
                        MessageType = messageType
                    });
                }
            }
    
            public void AddMessage<T>(T item, out int messageId)
            {
                var messageType = context.MessageTypes.Single(c => c.Name == typeof(T).FullName);
                var serializer = new XmlSerializer(typeof(T));
                var sb = new StringBuilder();
                using (var sw = new StringWriter(sb))
                {
                    serializer.Serialize(sw, item);
                }
                var message = new Message
                {
                    MessageType = messageType,
                    Created = DateTime.UtcNow,
                    Data = sb.ToString()
                };
                context.Messages.Add(message);
                context.SaveChanges();
                messageId = message.Id;
            }
    
            public void CreateDeliveries<T>(int messageId, string serverName, string sendingClientName, T item)
            {
                var messageType = typeof(T).FullName;
    
                var query = from reg in context.Registrations
                            where reg.Server.Name == serverName &&
                                  reg.Client.ClientType == ClientType.Subscriber &&
                                  reg.MessageType.Name == messageType
                            select new
                            {
                                reg.ClientId
                            };
    
                var senderClientId = context.Clients.Single(c => c.Name == sendingClientName).Id;
    
                foreach (var reg in query)
                {
                    context.Deliveries.Add(new Delivery
                    {
                        SenderClientId = senderClientId,
                        ReceiverClientId = reg.ClientId,
                        MessageId = messageId,
                        Updated = DateTime.UtcNow,
                        DeliveryStatus = DeliveryStatus.Sent
                    });
                }
            }
    
            public List<T> GetDeliveries<T>(string serverName, string clientName, out List<int> messageIds)
            {
                messageIds = new List<int>();
                var messages = new List<T>();
                var clientId = context.Clients.Single(c => c.Name == clientName).Id;
                var query = from del in context.Deliveries
                            where del.ReceiverClientId == clientId &&
                                  del.DeliveryStatus == DeliveryStatus.Sent
                            select new
                            {
                                del.Id,
                                del.Message.Data
                            };
                foreach (var item in query)
                {
                    var serializer = new XmlSerializer(typeof(T));
                    using (var sr = new StringReader(item.Data))
                    {
                        var t = (T)serializer.Deserialize(sr);
                        messages.Add(t);
                        messageIds.Add(item.Id);
                    }
                }
                return messages;
            }
    
            public void ConfirmDelivery(int deliveryId)
            {
                using (var context = new Context())
                {
                    context.Deliveries.First(c => c.Id == deliveryId).DeliveryStatus = DeliveryStatus.Received;
                    context.SaveChanges();
                }
            }
        }
    }
    

3 个答案:

答案 0 :(得分:1)

您可以保留int主键,但也可以在Name列上定义unique index

这样,在并发情况下,只有第一个插入才会成功;尝试插入相同服务器名称的任何后续客户端都将失败并显示SqlException

答案 1 :(得分:1)

我目前正在使用此解决方案:

    public void AddServer(string name)
    {
        if (!context.Servers.Any(c => c.Name == name))
        {
            context.Database.ExecuteSqlCommand(@"MERGE Servers WITH (HOLDLOCK) AS T
                                                 USING (SELECT {0} AS Name) AS S
                                                 ON T.Name = S.Name
                                                 WHEN NOT MATCHED THEN 
                                                 INSERT (Name) VALUES ({0});", name);
        }
    }

答案 2 :(得分:1)

作为一个彻底的练习,我(我想)以另一种方式解决了这个问题,它保留了EF上下文的类型安全性,但增加了一些复杂性:

首先,this post,我学习了如何向Server表添加唯一约束:

这是上下文代码:

    public class Context : DbContext
    {
        public DbSet<MessageType> MessageTypes { get; set; }
        public DbSet<Message> Messages { get; set; }
        public DbSet<Delivery> Deliveries { get; set; }
        public DbSet<Client> Clients { get; set; }
        public DbSet<Server> Servers { get; set; }
        public DbSet<Registration> Registrations { get; set; }

        public class Initializer : IDatabaseInitializer<Context>
        {
            public void InitializeDatabase(Context context)
            {
                if (context.Database.Exists() && !context.Database.CompatibleWithModel(false))
                    context.Database.Delete();

                if (!context.Database.Exists())
                {
                    context.Database.Create();
                    context.Database.ExecuteSqlCommand(
                       @"alter table Servers 
                         add constraint UniqueServerName unique (Name)");
                }
            }
        }
    }

现在我需要一种在保存时有选择地忽略异常的方法。我通过将以下成员添加到我的存储库来完成此操作:

readonly List<Func<Exception, bool>> ExceptionsIgnoredOnSave = 
    new List<Func<Exception, bool>>();

static readonly Func<Exception, bool> UniqueConstraintViolation =
    e => e.AnyMessageContains("Violation of UNIQUE KEY constraint");

与循环的新扩展方法一起取决于内部异常链中文本的位置:

public static class Ext
{
    public static bool AnyMessageContains(this Exception ex, string text)
    {
        while (ex != null)
        {
            if(ex.Message.Contains(text))
                return true;
            ex = ex.InnerException;
        }
        return false;
    }
}

我修改了我的存储库的Dispose方法,以检查是否应该忽略或重新抛出异常:

    public void Dispose()
    {
        if (autoSave)
        {
            try
            {
                context.SaveChanges();
            }
            catch (Exception ex)
            {      
                if(!ExceptionsIgnoredOnSave.Any(pass => pass(ex)))
                    throw;
                Console.WriteLine("ignoring exception..."); // temp
            }
        }
        context.Dispose();
    }

最后,在调用Add的方法中,我将可接受的条件添加到列表中:

    public void AddServer(string name)
    {
        ExceptionsIgnoredOnSave.Add(UniqueConstraintViolation);

        if (!context.Servers.Any(c => c.Name == name))
        {
            var server = context.Servers.Add(new Server { Name = name });
        }
    }