Mik*_*ain 29 .net entity-framework transactions race-condition
我一直在阅读有关如何在EF中实现if-exists-insert-else-update语义的其他问题,但要么我不理解答案如何工作,要么他们实际上没有解决问题.提供的常见解决方案是将工作包装在事务范围内(例如:使用没有竞争条件的实体框架实现if-not-exists-insert):
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();
}
}
Run Code Online (Sandbox Code Playgroud)
但我没有看到它如何解决任何问题,因为这个工作,我上面已经加星号的行应该阻止第二个线程是否尝试访问相同的用户ID,仅在第一个线程完成其工作时解除阻塞.但是,使用事务不会导致这种情况,并且由于第二个线程尝试第二次创建同一用户时发生的密钥违规,我们将抛出一个UpdateException.
而不是捕捉由竞争条件引起的异常,最好是防止竞争条件首先发生.执行此操作的一种方法是使星号行在数据库行上取出与其条件匹配的独占锁,这意味着在此块的上下文中,一次只有一个线程可以与用户一起使用.
对于EF的用户来说,这似乎是一个常见的问题,所以我正在寻找一个可以随处使用的干净,通用的解决方案.
我真的很想避免使用存储过程来创建我的用户.
有任何想法吗?
编辑:我尝试使用相同的用户ID在两个不同的线程上同时执行上述代码,尽管取出了可序列化的事务,但它们都能够同时进入临界区(*).这导致在第二个线程尝试插入与第一个刚刚插入的用户ID相同的用户ID时抛出UpdateException.这是因为,正如下面Ladislav所指出的,可序列化事务只有在开始修改数据而不是读取数据之后才会进行独占锁定.
Lad*_*nka 14
使用可序列化事务时,SQL Server会在读取记录/表上发出共享锁.共享锁不允许其他事务修改锁定数据(事务将阻塞),但它允许其他事务在发出锁的事务开始修改数据之前读取数据.这就是为什么示例不起作用的原因 - 在第一个事务开始修改数据之前,允许使用共享锁进行并发读取.
您需要隔离,其中select命令仅为单个客户端锁定整个表.它必须锁定整个表,否则它将无法解决插入"相同"记录的并发性.使用提示时,可以通过select命令锁定记录或表的粒度控制,但必须编写直接SQL查询才能使用它们--EF不支持.我描述了在这里专门锁定该表的方法,但它就像创建对表的顺序访问一样,它会影响访问该表的所有其他客户端.
如果您确实只是在单个方法中发生此操作并且没有其他应用程序使用您的数据库,您可以简单地将代码放入关键部分(例如,与.NET同步lock)并确保在.NET端只有单个线程可以访问关键部分.这不是那么可靠的解决方案,但任何玩锁和事务级别都会对数据库性能和吞吐量产生很大影响.您可以将此方法与乐观并发(唯一约束,时间戳等)结合使用.
只是补充一下我的方式,并不是说它真正处理抛出异常和事务的烦恼,并不能完全将其作为可扩展的解决方案,但它确实避免了竞争条件导致锁类型解决方案不可能(易于管理)的问题,例如在分布式系统中。
我非常简单地使用异常并首先尝试插入。我使用对原始代码的修改作为示例:
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();
}
}
}
Run Code Online (Sandbox Code Playgroud)
它并不漂亮,但它有效并且可能对某人有用。