数据库是不是总是在查询或更新后锁定不存在的行?
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
执行“首先”并因此引入了竞争条件。不:select
是insert
的一部分。插入是原子的。没有种族。
【讨论】:
我提问: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
),您可以免受竞争条件的影响。
继续,如果需要,update
或select
其主键。
【讨论】:
【参考方案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 语句。以上是关于数据库是不是总是在查询或更新后锁定不存在的行?的主要内容,如果未能解决你的问题,请参考以下文章