基于XML的存储的WCF服务。并发问题?

时间:2009-08-06 08:31:06

标签: c# .net wcf concurrency persistence

我编写了一个简单的WCF服务,用于存储用户发送的消息,并在被要求时将这些消息发送给目标用户。目前,通过使用以下结构创建 username .xml文件来实现持久性:

<messages recipient="username">
  <message sender="otheruser">
     ...
  </message
</messages>

多个用户可以同时向同一个收件人发送邮件,这可能导致xml文件同时更新。 WCF服务目前使用basicHttp绑定实现,没有任何并发​​访问规定。

存在哪些并发风险?我应该如何处理它们?正在访问的xml文件上是否有ReadWrite锁定? 目前该服务最多运行5个用户,这可能会增长到50个,但不会更多。

编辑: 如上所述,客户端将在每次调用时实例化一个新的服务类。 (InstanceContext是PerCall,ConcurrencyMode无关紧要)这是使用basicHttpBinding和服务上的默认设置所固有的。

以下代码:

   public class SomeWCFService:ISomeServiceContract
        {
          ClassThatTriesToHoldSomeInfo useless;

          public SomeWCFService() 
          {
             useless=new ClassThatTriesToHoldSomeInfo();
          } 

          #region Implementation of ISomeServiceContract
          public void IncrementUseless()
          {
            useless.Counter++;
          }
          #endregion
        }

表现如果写的是:

  public class SomeWCFService:ISomeServiceContract
        {
          ClassThatTriesToHoldSomeInfo useless;

          public SomeWCFService() 
          {} 

          #region Implementation of ISomeServiceContract
          public void IncrementUseless()
          {
            useless=new ClassThatTriesToHoldSomeInfo();
            useless.Counter++;
          }
          #endregion
        }

因此,在尝试访问数据库或文件中的某些外部存储数据之前,并发性永远不会成为问题。 缺点是您不能在服务的方法调用之间存储任何数据,除非您将其存储在外部。

4 个答案:

答案 0 :(得分:2)

您需要在添加文件并写入磁盘之前读取该文件,因此您尝试两次重叠操作的风险(相当小) - 第二次操作在第一次操作写入磁盘之前从磁盘读取,并且第二条消息提交时,第一条消息将被覆盖。

一个简单的答案可能是对邮件进行排队,以确保它们是按顺序处理的。当您的服务收到消息时,只需将内容转储到MSMQ队列中。有另一个单线程进程从队列中读取并将适当的更改写入xml文件。这样,您可以确保一次只写一个文件并解决任何并发问题。

答案 1 :(得分:2)

如果您的WCF服务是单件服务并且保证是这样,那么您不需要做任何事情。由于WCF一次只允许处理一个请求,因此对用户名文件的并发访问不是问题,除非服务该请求的操作产生多个访问同一文件的线程。但是,正如您可以想象的那样,单例服务的可扩展性不是很高,而且我认为不是您想要的。

如果您的WCF服务不是单例,那么对同一用户文件的并发访问是一个非常现实的场景,您必须明确地解决它。多个服务实例可能会同时尝试访问同一个文件来更新它,并且您将获得“无法访问文件,因为它正被另一个进程”例外使用或类似的事件。因此,这意味着您需要同步对用户文件的访问。您可以使用监视器(锁定),ReaderWriterLockSlim等。但是,您希望此锁定基于每个文件进行操作。当不同文件的更新正在进行时,您不希望将更新锁定在其他文件上。因此,您需要为每个文件维护一个锁定对象并锁定该对象,例如

//when a new userfile is added, create a new sync object
fileLockDictionary.Add("user1file.xml",new object());

//when updating a file
lock(fileLockDictionary["user1file.xml"])
{
   //update file.
}

请注意,该字典也是需要同步访问的共享资源。

现在,处理并发性并确保以适当的粒度同步访问共享资源非常困难,不仅在于提出正确的解决方案,而且在调试和维护该解决方案方面。调试多线程应用程序并不好玩,也很难重现问题。有时候你没有选择,但有时你会这样做。那么,您是否有任何特殊原因没有使用或考虑基于数据库的解决方案?数据库将为您处理并发性。你不需要做任何事情。如果您担心购买数据库的成本,那么有很多经过验证的开源数据库,例如MySQL和PostgreSQL,它们不会花费任何成本。

基于xml文件的方法的另一个问题是更新它们将是昂贵的。您将从内存中的用户文件加载xml,创建一个消息元素,并将其保存回文件。随着xml的增长,该过程将花费更长的时间,需要更多的内存等。这也会损害您的可扩展性,因为更新过程会更长时间地保持该锁定。另外,I / O很贵。基于数据库的解决方案还带来了诸多好处:事务,备份,能够轻松查询数据,复制,镜像等。

我不知道您的要求和限制,但我认为基于文件的解决方案将会出现问题。

答案 2 :(得分:1)

基本问题是当您访问全局资源(如静态变量或文件系统上的文件)时,您需要确保锁定该资源或以某种方式序列化对它的访问。

我的建议(如果你想在不使用数据库或其他任何东西的情况下快速完成它,那会更好)将你的消息从你的服务代码插入到内存中的队列结构中。

public MyService : IMyService
{
     public static Queue queue = new Queue();
     public void SendMessage(string from, string to, string message)
     {
          Queue syncQueue = Queue.Synchronized(queue);
          syncQueue.Enqueue(new Message(from, to, message));
     }
}

然后在你的应用程序的其他地方,你可以创建一个后台线程,从该队列中读取并一次向文件系统写入一个更新。

void Main()
{


    Timer timer = new Timer();
    timer.Tick += (o, e)
       {
           Queue syncQueue = Queue.Synchronized(MyService.queue);
           while(syncQueue.Count > 0)
           {
                Message message = syncQueue.Dequeue() as Message;
                WriteMessageToXMLFile(message);
           }
           timer.Start();
       };
    timer.Start();

    //Or whatever you do here
    StartupService();
}

它不漂亮(我不是100%肯定它编译)但它应该工作。它依次是“用我拥有的工具完成它,而不是我想要的工具”,我认为你正在寻找的方法。

客户端也会尽快离线,而不是在文件系统断开连接之前等待文件写入文件系统。这也可能很糟糕......如果你的应用程序断开连接并且后台线程尚未写入消息后,客户端可能不知道他们的消息没有被传递。

这里的其他方法同样有效...我想发布序列化方法,而不是其他人建议的锁定方法。

HTH, 安德森

答案 3 :(得分:0)

嗯,碰巧我做了几乎完全相同的事情,除了它实际上并不是消息......

以下是我如何处理它。

您的服务本身与中心对象(或多个对象)进行通信,中心对象可以根据发件人调度消息请求。

与每个发件人相关的对象在更新任何内容时都会保持内部锁定。当它获得新的修改请求时,它可以从磁盘读取(如果需要),更新数据,并写入磁盘(如果需要)。

由于不同的线程会发生不同的更新,因此内部锁定将被序列化。如果你调用任何“外部”对象以避免死锁情况,请确保释放锁。

如果I / O成为瓶颈,您可以查看不同的策略,包括将消息放在一个文件中,单独的文件,而不是立即将它们写入磁盘等。事实上,我会考虑为每个用户存储消息出于这个原因,在一个单独的文件夹中。

最重要的一点是,每个服务实例本质上都是中央类的适配器,只有一个类的一个实例才会负责用于读取/写入给定收件人的消息。其他类可能请求读/写,但它们实际上并不执行它(甚至不知道它是如何执行的)。这也意味着他们的代码看起来像'AddMessage(message)',而不是'SaveMessages(GetMessages.Add(message))'。

尽管如此,使用数据库是一个非常好的建议,并且可能会让您免于很多麻烦。