数据库是不是总是在查询或更新后锁定不存在的行?

Posted

技术标签:

【中文标题】数据库是不是总是在查询或更新后锁定不存在的行?【英文标题】:Do databases always lock non-existent rows after a query or update?数据库是否总是在查询或更新后锁定不存在的行? 【发布时间】:2013-05-03 17:57:21 【问题描述】:

给定:

customer[id BIGINT AUTO_INCREMENT PRIMARY KEY, email VARCHAR(30), count INT]

我想自动执行以下操作:如果客户已经存在,则更新他;否则,插入一个新客户。

理论上这听起来很适合SQL-MERGE,但我使用的数据库不支持MERGE with AUTO_INCREMENT columns。

https://***.com/a/1727788/14731 似乎表明如果您对不存在的行执行查询或更新语句,数据库将锁定索引从而防止并发插入。

这种行为是否受到 SQL 标准的保证?有没有这样的数据库?

更新:对不起,我应该在前面提到这一点:解决方案必须使用 READ_COMMITTED 事务隔离除非在这种情况下这是不可能的,我将接受使用 SERIALIZABLE。

【问题讨论】:

那么您使用的是哪个 DBMS?不,SQL 标准不能保证“不存在”的行被锁定(实际上似乎是一个很奇怪的概念) 查看今天早些时候的相关问题:***.com/questions/16359900/… @a_horse_with_no_name,我正在使用H2,但我正在寻找一种可移植的解决方案。所以你是说SELECT FOR INSERT 没有什么?请发布正式答案,以便我们对其发表评论。 H2 特定的merge 应该可以工作:merge into customer(id, email, count) key(email) values(null, 'test@acme.com', 10)。但这不适用于其他数据库。 @ThomasMueller,我很困惑。根据***.com/a/6307884/14731 上的最后一条评论,您告诉我supplying NULL when using MERGE doesn't make sense,但现在您告诉我要这样做。如果误解围绕着key() 的使用,那么也许您可以更新答案的第一部分以使用它? 【参考方案1】:

这个问题大约每周在 SO 上被问一次,答案几乎总是错误的。

这是正确的。

insert customer (email, count) 
select 'foo@example.com', 0
where not exists (
      select 1 from customer
      where email = 'foo@example.com'
)

update customer set count = count + 1
where email = 'foo@example.com'

如果您愿意,您可以插入计数 1 并跳过 update 如果插入的行计数(无论在您的 DBMS 中表示)返回 1。

上面的语法是绝对标准的,对锁定机制或隔离级别没有任何假设。如果它不起作用,则您的 DBMS 已损坏。

许多人错误地认为select 执行“首先”并因此引入了竞争条件。不:selectinsert一部分。插入是原子的。没有种族。

【讨论】:

我提问:select is part of the insert. The insert is atomic. There is no race. 看看***.com/a/5598275/14731,它表明 SQL 联合不是原子的。我希望 insert+select 的行为与联合相同。 @Gili:该语句仅适用于 SQL Server。 Oracle 和 Postgres 保证 语句级别读取一致性。因此,整个语句(包括 UNION)会看到数据库的“快照”,它反映了语句 开始 时的状态。当语句运行时,它永远不会在那些 DBMS(或任何其他使用 MVCC 我假设的 DBMS)中看到任何更改(甚至没有提交的更改) @Gill,您可能会发现阅读文档可以更好地利用您的时间。我没有猜测,也没有期待。我在解释,因为我知道。而且,顺便说一句,UNION 在 SQL Server 上也是原子的。这是标准的一部分。您提到的 SO 答案是错误的,并且是基于对服务器行为的误解。 -1 您混淆了原子性和隔离性。如果没有对 customer(email) 的唯一约束,您可以最终得到重复的 as this user found out @MartinSmith,那么插入或更新的“正确”方式是什么?有没有办法做到这一点不依赖于特定于数据库的功能?【参考方案2】:

回答我自己的问题,因为围绕该主题似乎有很多困惑。看来:

-- BAD! DO NOT DO THIS! --
insert customer (email, count) 
select 'foo@example.com', 0
where not exists (
      select 1 from customer
      where email = 'foo@example.com'
)

对竞争条件开放(请参阅Only inserting a row if it's not already there)。根据我收集到的信息,这个问题的唯一可移植解决方案:

    选择要合并的键。这可能是主键或另一个唯一键,但它必须具有唯一约束。 尝试insert 一个新行。如果该行已经存在,您必须捕获将发生的错误。 困难的部分已经结束。此时,该行保证存在,并且由于您在其上持有写锁这一事实(由于上一步中的 insert),您可以免受竞争条件的影响。 继续,如果需要,updateselect 其主键。

【讨论】:

【参考方案3】:

使用 Russell Fox 的代码,但使用 SERIALIZABLE 隔离。这将采用范围锁定,以便在逻辑上锁定不存在的行(连同周围键范围中的所有其他不存在的行)。

所以它看起来像这样:

BEGIN TRAN
IF EXISTS (SELECT 1 FROM foo WITH (UPDLOCK, HOLDLOCK) WHERE [email] = 'thisemail')
BEGIN
    UPDATE foo...
END
ELSE
BEGIN
    INSERT INTO foo...
END
COMMIT

大部分代码取自他的回答,但固定为提供互斥语义。

【讨论】:

这是 SQL Server 语法。如果 Gili 使用它,他也可以使用 MERGE 语句。 我忽略了这一点。但是这个代码肯定可以翻译到他的系统吗?!关键是使用可序列化的隔离。 @usr,REPEATABLE_READ 不够吗?我需要防止插入特定值,而不是整个值范围。 @Gili REPEATABLE_READ 不会锁定不存在的行,也不会锁定行的范围(唉!我希望它这样做)。它不会阻止行稍后出现,它只会阻止行更改。【参考方案4】:
IF EXISTS (SELECT 1 FROM foo WHERE [email] = 'thisemail')
BEGIN
    UPDATE foo...
END
ELSE
BEGIN
    INSERT INTO foo...
END

【讨论】:

好的。现在,当从两个单独的线程运行相同的查询时会发生什么? :-) 对 [email] 的唯一约束? 您也可以将此代码放入一个代替触发器中 - 始终将其作为插入发送,但首先在触发器中检查它。 这是 SQL Server 语法。如果 Gili 使用它,他也可以使用 MERGE 语句。

以上是关于数据库是不是总是在查询或更新后锁定不存在的行?的主要内容,如果未能解决你的问题,请参考以下文章

CockroachDB 中的行级锁定

为啥更新数据库中的行后出现登录问题?

sql server 2005 2709 出错 是啥意思

如果表中的行不存在,我如何更新或插入它?

用ado对SQLSERVER数据库操作,删除一条记录后查询提示“句柄引用了一个已被删除的行或标示为删除的行”

更新具有特定ID的行