首页 > 解决方案 > Restful API 线程冲突

问题描述

我目前有一个购物车 API,可以在商品不存在时将商品添加到表中,然后每次添加商品时增加 qty 列:

var exist = _context.Carts.Any(a => a.CartID == dto.CartSesID && a.SweetID == dto.SweetID);


        if (!exist)
        {
            // Create a new cart item if no cart item exists
            var cartItem = new Cart
            {
                SweetID = dto.SweetID,
                CartID = dto.CartSesID, 
                Qty = qty,
                DateCreated = DateTime.Now
            };
            _context.Carts.Add(cartItem);
        }
        else
        {
           var cartItem = _context.Carts.FirstOrDefault(a => a.CartID == dto.CartSesID && a.SweetID == dto.SweetID);
            // If the item does exist in the cart, 
            // then add one to the quantity

            if(type == "plus")
            cartItem.Qty = cartItem.Qty + qty;

            if (type == "minus")
            cartItem.Qty = cartItem.Qty - qty;

            if(cartItem.Qty == 0)
            _context.Carts.Remove(cartItem);


        }
        // Save changes
        _context.SaveChanges();

问题是 if (!exist) 检查似乎认为当按钮被单击多次太快时该项目不存在(可能线程在其他启动时未完成?)导致在几行上添加相同的项目:

在此处输入图像描述

但它应该添加如下: 在此处输入图像描述

有人知道理想的解决方法吗?

标签: c#asp.net-mvcrestasp.net-web-api

解决方案


你在这里有一个竞争条件。当两个请求彼此快速跟进时,第一个请求可能在第二个请求查询相关项目的存在之前尚未将其更改提交到数据库。

您需要应用一些并发控制来解决此问题。基本上有两种方法可以走:

  1. 序列化对数据库所做的更改。同样,两个主要选项:
    1. 大多数 RDBMS 支持事务的可序列化隔离级别或
    2. 你可以使用一些锁定机制。这可以发生在数据库级别(例如表锁定)或应用程序级别(.NET 锁定结构),具体取决于您的应用程序架构。
  2. 您可以应用试错法(或者更准确地说是试错法)方法,即乐观并发控制

显然,序列化对性能有负面影响(尤其是选项 1.1),因此通常首选乐观并发控制,其他用于特殊情况。

幸运的是,EF 内置了对乐观并发处理的支持。此 MSDN 文章中讨论了所有详细信息。

在这种特殊情况下,您有一个更简单的方法。您需要在 (CartID, SweetID) 字段上定义复合唯一约束。这样做可以保证没有重复项可以插入到表中。当检测到这种尝试时,您会得到一个异常,通过捕获它,您可以根据您的要求处理这种情况。例如,您可以启动更新(但请记住,即使在这种情况下,您也需要乐观并发检查以使过程绝对安全!)

脚注

实际上,禁用按钮只是掩盖问题。在服务器端,你不能相信 JS 在客户端所做的事情,因为它完全不受你的控制。用户可以轻松禁用或修改 JS。

更新

再次阅读我的答案,我觉得我应该补充一个结论:

在这种特殊情况下,我认为你能做的最好的就是

  • 按照我的建议设置唯一约束,但不关心检测到重复插入时引发的异常和
  • 在提交时禁用客户端上的提交按钮。

这样,即使有人操纵了您的 JS 代码,您也可以确保不会将无效数据存储在您的数据库中。同时,您不需要使数据持久化逻辑过于复杂。


推荐阅读