【问题标题】: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://stackoverflow.com/a/1727788/14731 似乎表明如果您对不存在的行执行查询或更新语句,数据库将锁定索引从而防止并发插入。

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

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

【问题讨论】:

  • 那么您使用的是哪个 DBMS?不,SQL 标准不能保证“不存在”的行被锁定(实际上似乎是一个很奇怪的概念)
  • 查看今天早些时候的相关问题:stackoverflow.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,我很困惑。根据stackoverflow.com/a/6307884/14731 上的最后一条评论,您告诉我supplying NULL when using MERGE doesn't make sense,但现在您告诉我要这样做。如果误解围绕着key() 的使用,那么也许您可以更新答案的第一部分以使用它?

标签: sql insert locking h2


【解决方案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. 看看stackoverflow.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)。根据我收集到的信息,这个问题的唯一可移植解决方案:

  1. 选择要合并的键。这可能是主键或另一个唯一键,但它必须具有唯一约束。
  2. 尝试insert 一个新行。如果该行已经存在,您必须捕获将发生的错误。
  3. 困难的部分已经结束。此时,该行保证存在,并且由于您在其上持有写锁这一事实(由于上一步中的 insert),您可以免受竞争条件的影响。
  4. 继续,如果需要,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 语句。
    猜你喜欢
    • 2014-02-19
    • 1970-01-01
    • 2019-08-16
    • 1970-01-01
    • 1970-01-01
    • 2012-05-15
    • 2022-11-23
    • 2018-02-09
    • 1970-01-01
    相关资源
    最近更新 更多