【问题标题】:How do I concurrently increment a page view count using linq-to-sql?如何使用 linq-to-sql 同时增加页面查看次数?
【发布时间】:2011-06-07 17:06:02
【问题描述】:

我有一个使用 Linq-to-SQL 作为数据访问层的 ASP.NET MVC 3 Web 应用程序。每次调用 Details 操作时,我都会尝试增加 Views 字段,但如果两个人碰巧同时点击操作,我会在 db.SubmitChanges() 上收到“未找到或更改行”错误。

public ActionResult Details(int id)
{
    DataClassesDataContext db = new DataClassesDataContext();

    var idea = db.Ideas.Where(i => i.IdeaPK == id).Single();

    idea.Views++;

    db.SubmitChanges();

    return View(new IdeaViewModel(idea));
}

我可以在我的 .dbml(数据模型)中将 Views 字段的 UpdateCheck 设置为“从不”,这样可以消除错误,但是可以使用相同的 Views 计数更新创意记录两次。即

First instance of Details action gets idea record with Views count of 1.
Second instance of Details action gets idea record with Views count of 1.
First instance increments Views to 2
First instance commits
Second instance increments Views to 2
Second instance commits

Result: Views field is 2 
Expected Result: Views field should be 3

我研究过使用 TransactionScope,但我从两个调用之一中得到以下死锁错误:

事务(进程 ID 54)是 在锁定资源上死锁 另一个过程并被选为 僵局的受害者。重新运行 交易。

当我将操作更新为如下所示时:

public ActionResult Details(int id)
{
    DataClassesDataContext db = new DataClassesDataContext();

    using (var transaction = new TransactionScope()){
        var idea = db.Ideas.Where(i => i.IdeaPK == id).Single();

        idea.Views++;

        db.SubmitChanges();

        return View(new IdeaViewModel(idea));
    }
}

我还尝试使用 TransactionScopeOptions 增加 TransactionScope 超时,但这似乎没有帮助(但我可能还必须在其他地方设置它)。我可能可以通过使用 db.ExecuteQuery 在单个 SQL 命令中执行增量来解决此示例,但我试图弄清楚如何使这项工作发挥作用,所以我会知道在更复杂的场景中该怎么做(我想执行单个事务中的多个命令)。

【问题讨论】:

    标签: c# asp.net-mvc linq-to-sql architecture concurrency


    【解决方案1】:

    我认为你应该创建一个存储过程,它会自动增加你想要的字段并通过 LINQ2SQL 调用它。

    其他选择是将您的操作包装到具有适当隔离级别的事务中。

    【讨论】:

    • 什么是合适的隔离级别?我认为在这种情况下它将是可序列化的,但是当我尝试它时导致死锁。
    • 如果理解正确,两个单独的语句可能会以死锁结束。因此,您应该将读取和写入放置得非常接近,以减少死锁的机会(创建另一个上下文,读取行,递增并将其写回)。此外,AFAIK,这是一个不同的故事,只有一个 UPDATE 声明(形式为 UPDATE SET field=field+1 ...)。当我的 DBA 朋友回答时,我会发布更新。
    • 我已经问过他并做了一些研究,看来您确实需要使用单个 UPDATE 创建一个 SP,即使使用 READ COMMITTED 也可以。事务中具有确保适当增量的隔离级别的两个单独操作可能会导致死锁。您可以参考 Rick 的答案以获取此类 SP 的示例。
    • @xelibrion,测试什么?存储过程可以在 LINQ2SQL 中表示为“方法”,因此您可以将其作为其他任何东西进行测试。至于用 2 个单独的语句重现问题,只需在 Management Studio 中通过分隔 SELECTUPDATEWAITFOR DELAY 来模拟它......老实说,我不明白你的问题。
    • 使用存储过程会导致代码无法测试并最终无法维护。这种方法在应用程序级别和持久性级别之间传播逻辑,最终形成没有人对任何事情负责的系统
    【解决方案2】:

    您不应该需要事务或存储过程。只需使用DataContext.ExecuteCommand

    db.ExecuteCommand("UPDATE Ideas SET Views = Views + 1 WHERE IdeaPK = {0}", id);
    

    这将作为一个 SQL 语句执行,因此是原子的。

    【讨论】:

    • 这就是我现在所做的,但我遇到了另一种更复杂的情况,我无法在单个数据库调用中完成。我希望在我的读取和更新周围使用 TransactionScope 会锁定该行,直到事务提交,这将解决问题,但我似乎无法让 TransactionScope 锁定该行。
    【解决方案3】:

    我会尝试捕获 Row not Found 异常并触发整个操作的重试。重新查询您的视图行并更新它并再次调用提交更改。确保使用计数器来确保仅重试操作五次左右,以免陷入无限循环。

    【讨论】:

      【解决方案4】:

      我强烈建议您按照@Dmitry 的建议查看使用存储过程,并将您的增量包装起来并选择一个操作。这将给您带来两个好处:1) 它将消除您的争用问题;2) 它将把整个操作放在对数据库的一次调用中。基本思路如下:

      CREATE PROCEDURE spIdeasRetrieveAndLog
          @IdeaPK int
      AS
      BEGIN
          UPDATE Ideas SET Views = Views + 1 WHERE IdeaPK = @IdeaPK
          GO
          SELECT * FROM Ideas WHERE IdeaPK = @IdeaPK
      END
      GO
      

      【讨论】:

        【解决方案5】:

        我建议您使用其中一种消息传递框架(例如,NServiceBus,但还有其他选项 - MassTransit、Rhino Service Bus)。他们将帮助您以非常简单和优雅的方式解决这个问题。

        【讨论】:

        • 那么,我们是否可以设置一个 numberofworkerthreads=1 的服务总线(如 NServiceBus),以便它按顺序处理命令——确保所有命令按连续顺序执行?
        • 没错。另一种选择是使用具有一些有限重试次数的多个工作线程。
        猜你喜欢
        • 2012-01-08
        • 1970-01-01
        • 1970-01-01
        • 1970-01-01
        • 1970-01-01
        • 2011-02-02
        • 1970-01-01
        • 1970-01-01
        • 1970-01-01
        相关资源
        最近更新 更多