3 回答
TA贡献1872条经验 获得超3个赞
你分享了吗DbContext
?DbContext 不是线程安全的。
尝试将插入操作包装在 的using块中DbContext,而不是重试:
using(var context = new DbContext)
{
// Insert operation here
}
这种冲突很容易理解,但首先你需要知道,当你await
调用时,线程立即返回到调用者。
想象一下这个场景,您有两个线程正在运行您的代码。这是执行顺序:
线程 1:
FirstOrDefault
返回null
.线程 2:
FirstOrDefault
返回null
.线程 1:
Add
运行。SQL 生成并在数据库服务器上排队。主题 1:
await context.SaveChangesAsync()
. 呼叫立即完成。数据库:线程 1 的调用已完成。
线程2:
Add
运行。SQL 生成并在数据库服务器上排队。主题 2:
await context.SaveChangesAsync()
. 呼叫立即完成。数据库:尝试从线程 2 进行调用,但无法完成它,因为之前插入了具有相同键值的行。
TA贡献2051条经验 获得超10个赞
如果数据库中有一条记录作为val1
键但val2
不同,firstOrDefault()
则不会返回值,并且您仍然无法插入新记录。
这也可能是缓存问题。您可以尝试添加AsNoTracking()
到您的查询中。
TA贡献2039条经验 获得超7个赞
重试不起作用,因为一旦您将条目添加到上下文并收到冲突错误,条目仍标记为已插入,因此您将在所有进一步的重试中尝试插入它。您需要使用新的上下文或将其分离才能使重试起作用。
交易
如果您想确保在尝试查找记录时没有人可以添加记录,那么您需要使用事务:
using (var context = new MyContext())
using (var transaction = context.Database.BeginTransaction(IsolationLevel.Serializable)) {
var saved = context.Table.FirstOrDefault(x => x.field1 == val1 && x.field2 == val2);
if (saved != null)
{
//edits saved
}
else
{
context.Table.Add(new Table
{
field1 = val1,
field2 = val2
});
}
await context.SaveChangesAsync();
transaction.Commit()
return Json(true);
}
我在这里使用最隔离的级别来锁定表并防止读取时的竞争条件。此方法会对性能产生影响,如果可以接受重试,您仍然可以遵循此方法。
更新插入
如果您拥有新实体所需的所有数据,那么您可以使用FlexLabs.Upsert -update
或者insert
将在单个事务中执行,这样您就不会再发生冲突。
重试
请注意,如果更新不是幂等的,您可能仍然存在竞争条件,但现在您将其移至数据库端:2 个线程找到一个项目,单独更新并保存。您可以按照本文所述使用并发令牌来避免此类冲突。请记住,如果您坚持重试选项,更新必须是幂等的,这意味着无论有多少线程都会更新实体 - 它将与第一次更新后相同。
有一个很棒的框架Polly.NET对您来说非常方便:
await Policy.Handle<DbUpdateException>() .RetryAsync(5) .ExecuteAsync(() => ...);
我不建议在 DbContext (或其他任何东西)上使用任何进程内锁,因为这会限制您使用此逻辑运行单个进程,而当您需要高可用性时,情况并非如此。
- 3 回答
- 0 关注
- 152 浏览
添加回答
举报