C# 在多实例web环境中防止竞争条件的最佳方法?

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(); } 当用户反复按下按钮或刷新时,将出现竞争条件,这使得

假设您在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();
}
当用户反复按下按钮或刷新时,将出现竞争条件,这使得LolCatCount可能与LolCats的数量不同

问题

解决这些问题的常用方法是什么?您可以用JavaScript在客户端修复它,但这并不总是可能的。也就是说,当页面刷新时发生了一些事情,或者因为有人在Fiddler中鬼混

  • 我想你必须做一个基于网络的锁
  • 你真的要忍受每次通话的额外延迟吗
  • 您能告诉一个操作,每个用户只允许执行一次吗
  • 是否有任何通用模式可供使用?像过滤器或属性
  • 您是提前返回,还是真的锁定了流程
  • 当您提前返回时,是否有我应该返回的“已建立”响应/响应代码
  • 当您使用锁时,如何防止(半)长时间运行的进程导致线程不足
*这只是一个简单的愚蠢例子。现实世界的例子要复杂得多。

MVC意味着你必须自己手动处理。从纯粹主义的角度来看,我建议通过使用来防止多次按下,尽管我的首选是禁用按钮并应用适当的CSSClass来演示其禁用状态。我想我的理由是我们不能完全确定行动的消费者,所以当你提供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();
    }
}