我一直在阅读有关如何在EF中实现if-exists-insert-else-update语义的其他问题,但要么我不理解答案如何工作,要么他们实际上没有解决问题。提供的常见解决方案是将工作包装在事务范围内(例如:Implementing if-not-exists-insert using Entity Framework without race conditions):
using (var scope = new TransactionScope()) // default isolation level is serializable
using(var context = new MyEntities())
{
var user = context.Users.SingleOrDefault(u => u.Id == userId); // *
if (user != null)
{
// update the user
user.property = newProperty;
context.SaveChanges();
}
else
{
user = new User
{
// etc
};
context.Users.AddObject(user);
context.SaveChanges();
}
}
但是我没有看到它如何解决任何问题,因为这个工作,如果第二个线程试图访问相同的用户ID,我上面列出的行阻止,只有在第一个线程已完成其工作。然而,使用事务将不导致这种情况,并且由于第二个线程尝试第二次创建同一用户时发生的密钥违规,我们将抛出UpdateException。
不是捕捉由比赛条件引起的异常,而是首先防止比赛条件发生。实现此目的的一种方法是使星号行在数据库行上取出与其条件匹配的独占锁,这意味着在此块的上下文中,一次只有一个线程可以与用户一起使用。
对于EF用户来说,这似乎是一个常见的问题,所以我正在寻找一种可以在任何地方使用的干净,通用的解决方案。
如果可能的话,我真的想避免使用存储过程来创建我的用户。
有什么想法吗?
编辑:我尝试使用相同的用户ID在两个不同的线程上同时执行上述代码,尽管取出了可序列化的事务,但他们都能够同时进入临界区(*)。这导致在第二个线程尝试插入与第一个刚刚插入的用户ID相同的用户ID时抛出UpdateException。这是因为,正如下面Ladislav所指出的,可序列化事务只有在开始修改数据而不是读取数据后才会进行独占锁定。
答案 0 :(得分:12)
使用可序列化事务时SQL Server会在读取记录/表上发出共享锁。共享锁不允许其他事务修改锁定数据(事务将阻塞),但它允许其他事务在发出锁的事务开始修改数据之前读取数据。这就是为什么示例不起作用的原因 - 在第一个事务开始修改数据之前,共享锁允许并发读取。
您需要隔离,其中select命令专门为单个客户端锁定整个表。它必须锁定整个表,否则它将无法解决插入“相同”记录的并发性。使用提示时,可以通过select命令锁定记录或表的粒度控制,但必须编写直接的SQL查询才能使用它们--EF不支持它。我描述了专门锁定该表here的方法,但它就像创建对表的顺序访问一样,它会影响访问该表的所有其他客户端。
如果您确实只是在单个方法中发生此操作,并且没有其他应用程序使用您的数据库,您只需将代码放入关键部分(例如使用lock
进行.NET同步)并确保在.NET方面,只有一个线程可以访问关键部分。这不是那么可靠的解决方案,但任何玩锁和事务级别都会对数据库性能和吞吐量产生很大影响。您可以将此方法与乐观并发(唯一约束,时间戳等)结合使用。
答案 1 :(得分:1)
只是为了增加我的方式,而不是它确实处理异常被抛出的烦恼和交易并没有将其作为一种可扩展的解决方案,但它确实避免了竞争条件导致无法实现锁定类型解决方案的问题(易于管理) )例如在分布式系统中。
我非常简单地使用异常并首先尝试插入。我使用原始代码的修改作为示例:
using(var context = new MyEntities())
{
EntityEntry entityUser = null;
try
{
user = new User
{
// etc
};
entityUser = context.Users.Add(user);
context.SaveChanges(); // Will throw if the entity already exists
}
catch (DbUpdateException x)
when (x.InnerException != null && x.InnerException.Message.StartsWith("Cannot insert duplicate key row in object"))
{
if (entityUser != null)
{
// Detach the entity to stop it hanging around on the context
entityUser.State = EntityState.Detached;
}
var user = context.Users.Find(userId);
if (user != null) // just in case someone deleted it in the mean time
{
// update the user
user.property = newProperty;
context.SaveChanges();
}
}
}
它不漂亮,但它有效,可能对某人有用。
答案 2 :(得分:0)
您可以使用TransactionScope为TransactionScope更改事务隔离级别更严格(我想,对于您的情况,它是RepeatableRead或Serializable),但请记住,任何锁定都会降低可伸缩性。
提供这种级别的并发控制真的很重要吗?您的应用程序是否会在生产环境中的相同情况下使用? Here is Udi Dahan关于种族状况的好帖子。
答案 3 :(得分:0)
也许我错过了一些东西,但是当我在SQL Management Studio中模拟上面的例子时,这是按预期工作的。
两个Serializable事务都检查userId是否存在,并获取指定选择的范围锁。
假设此userId不存在,则两个事务都尝试使用userId插入新记录 - 这是不可能的。由于它们的Serializable隔离级别,两个事务都无法在users表中插入新记录,因为这会为其他事务引入幻像读取。
因此,由于范围锁定,这种情况会导致死锁。你最终会陷入僵局,一个交易将成为受害者,另一个交易将成功。
实体框架处理这个有什么不同吗?我怀疑你最终得到一个UpdateException
,其中嵌套SqlException
标识了死锁。