C# 在多实例web环境中防止竞争条件的最佳方法?
假设您在ASP.NET MVC中的多实例环境中有一个操作,看起来像这样*:C# 在多实例web环境中防止竞争条件的最佳方法?,c#,asp.net,azure,cloud,C#,Asp.net,Azure,Cloud,假设您在ASP.NET MVC中的多实例环境中有一个操作,看起来像这样*: public void AddLolCat(int userId) { var user = _Db.Users.ById(userId); user.LolCats.Add( new LolCat() ); user.LolCatCount = user.LolCats.Count(); _Db.SaveChanges(); } 当用户反复按下按钮或刷新时,将出现竞争条件,这使得
public void AddLolCat(int userId)
{
var user = _Db.Users.ById(userId);
user.LolCats.Add( new LolCat() );
user.LolCatCount = user.LolCats.Count();
_Db.SaveChanges();
}
当用户反复按下按钮或刷新时,将出现竞争条件,这使得LolCatCount可能与LolCats的数量不同
问题
解决这些问题的常用方法是什么?您可以用JavaScript在客户端修复它,但这并不总是可能的。也就是说,当页面刷新时发生了一些事情,或者因为有人在Fiddler中鬼混
- 我想你必须做一个基于网络的锁李>
- 你真的要忍受每次通话的额外延迟吗
- 您能告诉一个操作,每个用户只允许执行一次吗李>
- 是否有任何通用模式可供使用?像过滤器或属性
- 您是提前返回,还是真的锁定了流程
- 当您提前返回时,是否有我应该返回的“已建立”响应/响应代码
- 当您使用锁时,如何防止(半)长时间运行的进程导致线程不足
回答1:(一般方法)
如果数据存储支持事务,则可以执行以下操作:
using(var trans = new TransactionScope(.., ..Serializable..)) {
var user = _Db.Users.ById(userId);
user.LolCats.Add( new LolCat() );
user.LolCatCount = user.LolCats.Count();
_Db.SaveChanges();
trans.Complete();
}
这将锁定数据库中的用户记录,使其他请求等待事务提交
答案2:(仅适用于单个流程)启用会话和使用会话将导致来自同一用户(会话)的请求之间的隐式锁定 答案3:(具体示例)
从集合中推断Lolcat的数量,而不是在单独的字段中跟踪它,从而避免不一致的问题 对您具体问题的回答:
- 我想你必须制作某种基于网络的锁?
是的,数据库锁很常见 - 您真的需要承受每次呼叫的额外延迟吗?
说什么 - 您能告诉一个操作,每个用户只允许执行一次吗
您可以实现一个属性,该属性使用隐式会话锁定或它的某些自定义变体,但在进程之间不起作用 - 是否有任何通用模式可供使用?像过滤器或属性吗?
通常的做法是在数据库中使用锁来解决多实例问题。没有我知道的过滤器或属性 - 您是提前返回,还是真的锁定了流程?
取决于您的用例。通常您需要等待(“锁定进程”)。但是,如果您的数据库存储支持异步/等待模式,您可以执行以下操作
这将释放线程在等待锁定时执行其他工作var user = await _Db.Users.ByIdAsync(userId);
- 当您提前返回时,是否有“已建立”的响应/响应代码我应该返回?
我不这么认为,选择适合您的用例的东西 - 当您使用锁时,如何防止(半)长时间运行的进程导致线程不足?
我猜你应该考虑使用队列。
public void AddLolCat(int userId)
{
// I add here some text in front of the number, because I see its an integer
// so its better to make it a little more complex to avoid conflicts
var gl = new MyNamedLock("SiteName." + userId.ToString());
try
{
//Enter lock
if (gl.enterLockWithTimeout())
{
var user = _Db.Users.ById(userId);
user.LolCats.Add( new LolCat() );
user.LolCatCount = user.LolCats.Count();
_Db.SaveChanges();
}
else
{
// log the error
throw new Exception("Failed to enter lock");
}
}
finally
{
//Leave lock
gl.leaveLock();
}
}
这里的锁是基于用户的,所以不同的用户不会互相阻塞
关于会话锁
如果您在通话中使用asp.net会话,则您可能会从会话中赢得一张免费锁定“票证”。会话将锁定每个调用,直到页面返回
在本问答中阅读相关内容:一种可能的解决方案是避免可能导致数据不一致的冗余。 i、 e.如果可以在运行时确定
LolCatCount
,则在运行时确定它,而不是保留这些冗余信息。通过“多实例”,您显然指的是一个web场,或者可能是一个web花园,在这种情况下,仅使用互斥锁或监视器不足以序列化请求
所以。。。后端是否只有一个数据库为什么不直接使用数据库事务?
听起来您可能不想强制对所有用户id的代码的这一部分进行序列化访问,对吗?是否要按用户id序列化请求
在我看来,正确的想法是序列化对源数据的访问,这是数据库中的LolCats记录
我很喜欢在请求期间禁用浏览器中的按钮或链接,以防止用户在以前的请求完成处理并返回之前反复敲打按钮。这似乎是一个非常简单的步骤,有很多好处
但我怀疑这是否足以保证您想要强制执行的序列化访问
您还可以实现共享会话状态,并对基于会话的对象实现某种类型的锁定,但可能需要这样做
public void AddLolCat(int userId)
{
// I add here some text in front of the number, because I see its an integer
// so its better to make it a little more complex to avoid conflicts
var gl = new MyNamedLock("SiteName." + userId.ToString());
try
{
//Enter lock
if (gl.enterLockWithTimeout())
{
var user = _Db.Users.ById(userId);
user.LolCats.Add( new LolCat() );
user.LolCatCount = user.LolCats.Count();
_Db.SaveChanges();
}
else
{
// log the error
throw new Exception("Failed to enter lock");
}
}
finally
{
//Leave lock
gl.leaveLock();
}
}