Joe*_*moe 4 c# sql-server concurrency entity-framework entity-framework-core
我将 Entity Framework Core 3.1.8 与 SQL Server 2016 结合使用。
考虑以下示例(为了清晰起见,进行了简化):
数据库表定义如下:
CREATE TABLE [dbo].[Product]
(
[Id] INT IDENTITY(1,1) NOT NULL ,
[ProductName] NVARCHAR(500) NOT NULL,
CONSTRAINT [PK_Product] PRIMARY KEY CLUSTERED (Id ASC) WITH (FILLFACTOR=80),
CONSTRAINT [UQ_ProductName] UNIQUE NONCLUSTERED (ProductName ASC) WITH (FILLFACTOR=80)
)
Run Code Online (Sandbox Code Playgroud)
以及以下 C# 程序:
using System;
using System.Linq;
using System.Reflection;
namespace CcTest
{
class Program
{
static int Main(string[] args)
{
Product product = null;
string newProductName = "Basketball";
using (CcTestContext context = new CcTestContext())
using (var transaction = context.Database.BeginTransaction())
{
try
{
product = context.Product.Where(p => p.ProductName == newProductName).SingleOrDefault();
if (product is null)
{
product = new Product { ProductName = newProductName };
context.Product.Add(product);
context.SaveChanges();
transaction.Commit();
}
}
catch(Exception ex)
{
transaction.Rollback();
}
}
if (product is null)
return -1;
else
return product.Id;
}
}
}
Run Code Online (Sandbox Code Playgroud)
在测试期间一切都按预期工作 - 如果新产品尚不存在,则将其插入到表中。因此,我希望 [UQ_ProductName] 约束永远不会受到影响,因为一切都是作为单个事务完成的。
然而,实际上这段代码是连接到Web API的业务逻辑的一部分。所发生的情况是,使用相同新产品名称的该代码的 10 个实例几乎同时执行(执行时间在百分之一秒内相同,我们将其保存在日志表中)。其中一个成功了(新产品名称被插入到表中),但其余的失败了,出现以下异常:
违反 UNIQUE KEY 约束“UQ_ProductName”。无法在对象“dbo.Product”中插入重复的键。重复的键值为(篮球)。该语句已终止。
为什么会发生这种情况?这不正是我使用交易应该防止的吗?也就是说,我认为检查具有此类值的行是否已经存在,如果不存在则插入应该是原子操作。执行第一个 API 调用并插入行后,其余 API 调用应该检测到值已存在,并且不会尝试插入重复项。
有人可以解释一下我的实现中是否有错误吗?显然它没有按照我期望的方式工作。
TLDR:单独使用事务(在任何隔离级别)并不能解决问题。
问题的根本原因在这里得到了完美的解释:https ://stackoverflow.com/a/6173482/412352
当使用可序列化事务时,SQL Server 会在读取记录/表上发出共享锁。共享锁不允许其他事务修改锁定的数据(事务将阻塞),但它允许其他事务在发出锁的事务开始修改数据之前读取数据。这就是该示例不起作用的原因 - 允许使用共享锁进行并发读取,直到第一个事务开始修改数据。
下面是始终会重现问题的代码块,并且修复也在那里(已注释掉):
using System;
using System.Linq;
using System.Threading.Tasks;
using Microsoft.EntityFrameworkCore;
namespace CcTest
{
class Program
{
static void Main(string[] args)
{
object Lock = new object();
// Parallel loop below will reproduce the issue all the time
// (if lock (Lock) is commented out)
// when in function AddProduct() you assign
// newProductName value that currently doesn't exist in the database
Parallel.For(0, 10, index =>
{
//lock (Lock) // uncomment this to resolve the issue
{
AddProduct(index);
}
});
// Sequential loop below will aways work as expected
//for (int index = 0; index < 10; index++)
// AddProduct(index);
Console.ReadKey();
}
static void AddProduct( int index)
{
Product product = null;
string newProductName = "Basketball"; // specify something that doesn't exist in database table
using (CcTestContext context = new CcTestContext())
using (var transaction = context.Database.BeginTransaction())
{
try
{
product = context.Product.FirstOrDefault(p => p.ProductName == newProductName);
if (product is null)
{
product = new Product { ProductName = newProductName };
context.Product.Add(product);
context.SaveChanges();
transaction.Commit();
Console.WriteLine($"API call #{index}, Time {DateTime.Now:ss:fffffff}: Product inserted. Id={product.Id}\n");
}
else
Console.WriteLine($"API call #{index}, Time {DateTime.Now:ss:fffffff}: Product already exists. Id={product.Id}\n");
}
catch(DbUpdateException dbuex)
{
transaction.Rollback();
if (dbuex.InnerException != null)
Console.WriteLine($"API call #{index}, Time {DateTime.Now:ss:fffffff}: Exception DbUpdateException caught, Inner Exception Message: {dbuex.InnerException.Message}\n");
else
Console.WriteLine($"API call #{index}, Time {DateTime.Now:ss:fffffff}: Exception DbUpdateException caught, Exception Message: {dbuex.Message}\n");
}
catch (Exception ex)
{
transaction.Rollback();
Console.WriteLine($"API call #{index}, Time {DateTime.Now:ss:fffffff}: Exception caught: {ex.Message}\n");
}
}
}
}
}
Run Code Online (Sandbox Code Playgroud)
正如您所看到的,解决该问题的方法之一是将代码放置在关键部分。
另一种方法是不将代码放在临界区中,而是捕获异常并检查它是否是DbUpdateException. 然后您可以检查内部错误消息是否包含与违反约束相关的内容,如果是,请尝试从数据库中重新读取。
另一种方法是使用原始 SQL 并指定 SELECT 锁定提示:
https://weblogs.sqlteam.com/dang/2007/10/28/conditional-insertupdate-race-condition/
请注意:任何方法都可能产生负面影响(例如性能下降)。
其他有用的页面值得一看:
https://weblogs.sqlteam.com/dang/2009/01/31/upsert-race-condition-with-merge/
| 归档时间: |
|
| 查看次数: |
4663 次 |
| 最近记录: |