Entity Framework 和 SQL 服务器中的奇怪 SaveChanges 行为

Strange SaveChanges behavior in Entity Framework and SQL Server

我有一些代码,你可以检查项目 github,错误包含在 UploadContoller 方法 GetExtensionId.

数据库图表:

代码(在这个控制器中我发送文件上传):

    [HttpPost]
    public ActionResult UploadFiles(HttpPostedFileBase[] files, int? folderid, string description)
    {
        foreach (HttpPostedFileBase file in files)
        {
            if (file != null)
            {
                string fileName = Path.GetFileNameWithoutExtension(file.FileName);
                string fileExt = Path.GetExtension(file.FileName)?.Remove(0, 1);
                
                int? extensionid = GetExtensionId(fileExt);
                
                if (CheckFileExist(fileName, fileExt, folderid))
                {
                    fileName = fileName + $" ({DateTime.Now.ToString("dd-MM-yy HH:mm:ss")})";
                }

                File dbFile = new File();
                dbFile.folderid = folderid;
                dbFile.displayname = fileName;
                dbFile.file_extensionid = extensionid;
                dbFile.file_content = GetFileBytes(file);
                dbFile.description = description;

                db.Files.Add(dbFile);
            }
        }
        db.SaveChanges();
        return RedirectToAction("Partial_UnknownErrorToast", "Toast");
    }

我想在数据库中创建扩展,如果它还不存在的话。我用 GetExtensionId:

    private static object locker = new object();
    private int? GetExtensionId(string name)
    {
        int? result = null;
        lock (locker)
        {
            var extItem = db.FileExtensions.FirstOrDefault(m => m.displayname == name);

            if (extItem != null) return extItem.file_extensionid;

            var fileExtension = new FileExtension()
            {
                displayname = name
            };
            db.FileExtensions.Add(fileExtension);
            db.SaveChanges();
            result = fileExtension.file_extensionid;
        }
        return result;
    }

在 SQL 服务器数据库中,我对 FileExtension 的显示名称列有唯一约束。

问题 只有当我上传几个具有相同扩展名的文件并且数据库中不存在该扩展名时才会出现。

如果我删除 lock,在 GetExtensionId 中将 Exception 关于唯一约束。

也许,出于某种原因,foreach 循环的下一次迭代会调用 GetExtensionId 而无需等待?我不知道。 但只有当我设置 lock 时,我的代码才能正常工作。

如果您知道为什么会发生,请解释。

这听起来像是一个简单的并发竞争条件。想象一下有两个请求同时进来;他们都检查 FirstOrDefault,这对两者都正确地说“不”。然后他们都尝试插入;一个赢了,一个失败了,因为数据库已经改变了。虽然 EF 围绕 SaveChanges 管理事务,但该事务并不是从您最初查询数据时开始的

lock 似乎有效,通过防止它们同时进入查找代码,但这通常不是一个可靠的解决方案,因为它只能在单个进程中工作,让单独节点.

所以:这里有几个选项:

  • 您的代码可以检测到外键违规异常并从头开始重新检查(FirstOrDefault 等),这使成功案例(大部分时间)的事情变得简单,并且在成功案例中也不会非常昂贵失败案例(只是一个例外和额外的数据库命中)- 足够务实
  • 您可以将“select 如果存在,如果不存在则插入”移动到数据库中的单个操作中在事务中(最好是可序列化的隔离级别, and/or 使用 UPDLOCK 提示) - 这需要自己编写 TSQL,而不是依赖 EF,但可以最大限度地减少往返并避免编写“检测故障并补偿”代码
  • 您可以通过 EF 在事务中执行 selects 和可能的插入 - 复杂而混乱,坦率地说:不要这样做(它会再次需要是可序列化的隔离级别,但现在可序列化的事务跨越多个往返,这可能会开始影响锁定,如果在规模上)