2013-07-08 58 views
1

我正在寫一個簡單的消息模塊,以便一個進程可以發佈消息,另一個可以訂閱它們。我使用EF/SqlServer作爲進程外通信機制。 「服務器」只是發佈者/訂戶對共同的名稱(可能被稱爲「頻道」)。你如何處理兩個客戶端upserts之間的競賽?

我有以下方法,添加一行到數據庫中代表一個名爲「服務器」

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

我遇到的問題是,當我在同一時間啓動兩個客戶端,只有一個是應該添加一個新的服務器條目,但是,這不是它的工作方式。實際上,我得到了兩個具有相同名稱的輸入的錯誤結果,並意識到Any()後衛對此並不足夠。

服務器實體使用一個int PK,據說我的倉庫會強制名稱字段的唯一性。我開始認爲這不會起作用。

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

這兩個方面,我認爲我能解決這個問題似乎都不太理想:

  1. 字符串主鍵
  2. 忽略異常

這是併發的問題,正確的?

如何在這種情況下處理它,我希望兩個客戶端使用相同的名稱調用存儲庫,但在數據庫中只獲得具有該名稱的一行結果?

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(); 
      } 
     } 
    } 
} 
+0

我認爲您需要使用事務。 – gunr2171

+0

你的兩個客戶是否重新使用'context'的同一個實例? – EkoostikMartin

+0

不,他們是兩個獨立的過程。 –

回答

1

你可以保持INT主鍵,但也對Name列中定義的unique index

這樣,在併發情況下,只有第一次插入會成功;任何嘗試插入相同服務器名稱的後續客戶端都將失敗,並顯示SqlException

+0

我想訴諸手動TSQL - 我只需要一個插入查詢,只有插入匹配行時纔會插入.. –

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); 
     } 
    } 
1

作爲徹底的練習,我(想我)解決了這個問題的另一種方式,它保留了EF上下文的類型安全,但增加了一點複雜性:

首先,this post,我學會了如何唯一約束添加到服務器表:

這裏的上下文代碼:

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(); 
    } 

最後,它調用方法210,我將可接受的條件添加到列表中:

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 }); 
     } 
    } 
+0

+1我建議你匹配SqlException.Number 。您的代碼會在其他語言環境和SQL Server的未來版本中中斷。 – usr

相關問題