C#锁定关键字,我认为我使用不当

4

我最近在一个ASP.NET MVC应用程序中遇到了多个表单提交的问题。情况基本上是这样的,如果有人故意猛击提交按钮,他们可以强制多次发布数据,尽管旨在禁止此类操作的验证逻辑(服务器和客户端)已经存在。这是因为在初始请求上运行Transaction.Commit()方法之前,他们的帖子将会通过(这一切都是在nHibernate中完成的)。

MVC ActionMethod看起来有点像这样...

public ActionResult Create(ViewModelObject model)
{
 if(ModelState.IsValid)
 {
  // ...

  var member = membershipRepository.GetMember(User.Identity.Name);
  // do stuff with member
  // update member
 }
}

有很多解决方案被提出,但我找到了C#的lock语句,并尝试了一下,于是我修改了我的代码看起来像这样...

public ActionResult Create(ViewModelObject model)
{
 if(ModelState.IsValid)
 {
  // ...
  var member = membershipRepository.GetMember(User.Identity.Name);
  lock(member) {     
     // do stuff with member
     // update member
  }
 }
}

它有效了!我的测试人员中没有人能再复现这个错误了!我们已经花费一整天的时间攻破它了,没有人能找到任何漏洞。但我对这个关键字并不是很了解。我又查了一遍以获得澄清......
锁定关键字(lock keyword)将一个语句块标记为给定对象的临界区,通过获取互斥锁执行一个语句,然后释放锁。
好的,这很有道理。这是我的问题。
这太容易了。这个解决方案看起来简单、直接、清晰、高效而干净。 这太简单了。我知道比起这么简单的解决方法,事情肯定更加复杂。所以我想向更有经验的程序员提问......
我应该注意到什么不好的事吗?

那不是 lock(someObject) 吗? - Cosmin
是的,抱歉。我在打字时分心了。我已经更新了它。 - Ciel
3个回答

7

不,这并不容易。只有使用相同的实例时锁定才有效。

以下操作将不起作用:

public IActionResult Submit(MyModel model)
{
    lock (model)
    {
       //will not block since each post generates it's own instance
    }
}

您的示例可能有效。这完全取决于nhibernate中是否启用了二级缓存(从而返回相同的用户实例)。请注意,这不会阻止任何内容被提交到数据库,只是每个帖子将按顺序保存。

更新

另一种解决方案是在提交按钮被按下时添加return false;。它将防止按钮多次提交表单。

以下是一个jQuery脚本,可以为您解决这个问题(它将遍历所有提交按钮,并确保它们仅提交一次)。

$(document).ready(function(){
    $(':submit').click(function() {
        var $this = $(this);
        if ($this.hasClass('clicked')) {
            alert('You have already clicked on submit, please be patient..');
            return false;
        }
        $this.addClass('clicked');
    });
});

将其添加到您的布局或Javascript文件中。

更新2

请注意,jQuery代码在大多数情况下可以工作,但请记住,任何具有少量编程知识的用户都可以使用例如HttpWebRequest来垃圾POST到您的Web服务器。虽然不太可能发生,但这种情况确实存在。我要提出的观点是,您不应依赖客户端代码来处理问题,因为它们可以被规避。


+1 - 还有一个危险,如果您没有在整个应用程序中锁定成员的访问权限,在请求之间另一页将更新第二级缓存中的成员。让NHibernate处理这可能是值得的-请参阅NHibernate文档中的事务和并发性 - Jeff Sternal
问题实际上是发生在服务器端的。有逻辑来防止双重输入,但是在数据库更新以反映第一个条目已经通过之前,该逻辑无法运行,而当这种情况发生时,nHibernate已经返回了一个存在于数据库更新之前的“Member”实例,因此数据仍然被推送。 - Ciel
我知道问题发生在服务器端。客户端代码可以防止这种情况,因为只有在第一次按下按钮时才会触发服务器。我的第二次更新在大多数情况下不是问题。如果用户真的想要给你添麻烦,还有更糟糕的事情可以做。 - jgauffin

0

这很简单,但要小心你锁定的对象。它应该是所有线程的相同对象-例如,可以是静态对象。
lock是语法糖Monitor,因此在幕后有很多工作正在进行。

另外,您应该注意死锁-当您锁定两个或多个对象时可能会发生死锁。


我猜我只是担心这个“解决方案”可能会在未来造成严重后果。它似乎太简单,太显而易见了。如果那么容易,为什么之前没有人建议过呢?为什么我在更多的代码中没有看到它?看到我的偏执症从哪里来了吗?作为一个程序员,我学会了当某件事看起来太简单时,它可能没有涵盖你想象的那么多基础知识。 - Ciel
老实说,我并没有完全理解你的情况,但如果你确定需要确保某段代码一次只能被一个线程访问,那么使用“锁”是可行的方法。 - Cosmin

0

是的,这很容易,但可能会影响性能。请记住,监视器锁定将限制该代码仅由一个线程运行。每个HTTP请求都有一个新线程,这意味着在任何给定时间只有其中一个请求可以访问该代码。如果它是长时间运行的过程,或者有很多人同时尝试访问该网站的某个部分-您可能会开始看到响应变慢。


那么,我应该开始寻找解决这个问题的不同方案了吗? - Ciel
这可能是最好的行动方案 - 除非您真的只想让一个Web站点用户一次使用此代码部分(从您的描述中我认为您不会这样做),否则这不是最佳解决方案。您可能需要考虑使用一些JavaScript来隐藏按钮,以便在按下按钮后它们不会一遍又一遍地提交表单。 - Streklin

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接