我总是使用类似下面的东西来实现它:
INSERT INTO TheTable
SELECT
@primaryKey,
@value1,
@value2
WHERE
NOT EXISTS
(SELECT
NULL
FROM
TheTable
WHERE
PrimaryKey = @primaryKey)
Run Code Online (Sandbox Code Playgroud)
...但是一旦加载,就会发生主键违规.这是唯一插入此表的语句.那么这是否意味着上述陈述不是原子的?
问题是,这几乎不可能随意重建.
也许我可以将其更改为以下内容:
INSERT INTO TheTable
WITH
(HOLDLOCK,
UPDLOCK,
ROWLOCK)
SELECT
@primaryKey,
@value1,
@value2
WHERE
NOT EXISTS
(SELECT
NULL
FROM
TheTable
WITH
(HOLDLOCK,
UPDLOCK,
ROWLOCK)
WHERE
PrimaryKey = @primaryKey)
Run Code Online (Sandbox Code Playgroud)
虽然,也许我使用错误的锁或使用过多的锁定或其他东西.
我在stackoverflow.com上看到了其他问题,其中答案是建议"IF(SELECT COUNT(*)... INSERT"等),但我总是在(可能是不正确的)假设单个SQL语句是原子的.
有没有人有任何想法?
我一直在阅读有关如何在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所指出的,可序列化事务只有在开始修改数据而不是读取数据之后才会进行独占锁定.