我编写了一个简单的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
}
因此,在尝试访问数据库或文件中的某些外部存储数据之前,并发性永远不会成为问题。 缺点是您不能在服务的方法调用之间存储任何数据,除非您将其存储在外部。
答案 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))'。
尽管如此,使用数据库是一个非常好的建议,并且可能会让您免于很多麻烦。