插入行(如果不存在而没有死锁)

Insert row if not exists without deadlock

提问人:Jesse 提问时间:3/2/2017 最后编辑:Jesse 更新时间:3/22/2023 访问量:2527

问:

我有一张简单的桌子

CREATE TABLE test (
  col INT,
  data TEXT,
  KEY (col)
);

和一个简单的交易

START TRANSACTION;

SELECT * FROM test WHERE col = 4 FOR UPDATE;

-- If no results, generate data and insert
INSERT INTO test SET col = 4, data = 'data';

COMMIT;

我正在尝试确保同时运行的此事务的两个副本不会产生重复的行和死锁。我也不想承担多次发电的成本。datacol = 4

我试过:

  1. SELECT ..(不带或):FOR UPDATELOCK IN SHARE MODE

    两个事务都看到没有行 (没有获取锁),并且都生成并插入了 行的两个副本。col = 4datacol = 4

  2. SELECT .. LOCK IN SHARE MODE

    两个事务都获取了 上的共享锁,生成并尝试插入带有 的行。两个事务都等待另一个事务释放其共享锁,以便可以,从而导致 .col = 4datacol = 4INSERTERROR 1213 (40001): Deadlock found when trying to get lock; try restarting transaction

  3. SELECT .. FOR UPDATE

    我希望一个交易会成功并获得独占锁,而另一个交易会阻止等待第一个交易。SELECTcol = 4SELECT

    相反,两个查询都成功,事务继续死锁,就像 一样。独占锁定似乎不起作用。SELECT .. FOR UPDATESELECT .. LOCK IN SHARE MODEcol = 4

我怎样才能在不导致重复行和死锁的情况下编写此事务?

mysql sql innodb mariadb 死锁

评论

0赞 tadman 3/2/2017
别这样。请设置索引并使用 or 代替。UNIQUEINSERT ... ON DUPLICATE KEYINSERT IGNORE
0赞 Jesse 3/2/2017
@tadman 如果我这样做,那么两笔交易都将产生不必要的生成成本。如果我这样做,那么MySQL在运行时遇到的所有错误都将被忽略(而不仅仅是重复的键),这是非常草率的。INSERT .. ON DUPLICATE KEY UPDATEdataINSERT IGNORE ..INSERT
0赞 Rick James 3/4/2017
的价值是什么?tx_isolation
0赞 Jesse 3/4/2017
@RickJames ( 只会使版本的行为与REPEATABLE READSERIALIZABLESELECT ..SELECT .. LOCK IN SHARE MODE)
0赞 shehanpathi 6/7/2022
问得好。从来没想过。

答:

0赞 tadman 3/2/2017 #1

稍微调整架构:

CREATE TABLE test (
  col INT NOT NULL PRIMARY KEY,
  data TEXT
);

作为主键,它不能被复制。col

然后使用 ON DUPLICATE KEY 功能:

INSERT INTO test (col, data) VALUES (4, ...)
  ON DUPLICATE KEY UPDATE data=VALUES(data)

评论

0赞 Jesse 3/2/2017
如果我这样做,那么两笔交易都将产生不必要的生成成本。我只希望其中一个继续生成.INSERT .. ON DUPLICATE KEY UPDATEdatadata
2赞 tadman 3/2/2017
您在问题中给出的示例不会生成。它有一个简单的字符串。如果要避免重复生成,请找出需要为其生成数据的 ID,并使用某种队列来计划这些操作。有许多工作队列系统可供选择。长时间打开锁就是乞求死锁。
0赞 Rick James 3/4/2017 #2

也许这个......

START TRANSACTION;
INSERT IGNORE INTO test (col, data) VALUES (4, NULL);  -- or ''
-- if Rows_affected() == 0, generate data and replace `data`
    UPDATE test SET data = 'data' WHERE col = 4;
COMMIT;

注意:如果 是 ,这可能会“烧毁”一个 id。PRIMARY KEYAUTO_INCREMENT

评论

0赞 Jesse 3/17/2017
INSERT IGNORE忽略所有错误和警告,而不仅仅是重复的键错误,因此它使我无法检测和报告该查询期间可能发生的其他类型的错误和警告。
0赞 Pavel Ivanov 3/18/2017 #3

请注意,InnoDB 有 2 种类型的独占锁:一种用于更新和删除,另一种用于插入。因此,要执行您的 SELECT FOR UPDATE 事务,InnoDB 必须首先在一个事务中获取更新锁,然后第二个事务将尝试获取相同的锁并阻止等待第一个事务(它不可能像您在问题中声称的那样成功),然后当第一个事务尝试执行 INSERT 时,它必须将其锁从 update 的锁更改为插入。InnoDB可以做到这一点的唯一方法是首先将锁定降级为共享锁定,然后将其升级回锁定以进行插入。当有另一笔交易等待获取独占锁时,它不能降级锁。这就是为什么在这种情况下会出现死锁错误的原因。

正确执行此操作的唯一方法是在 col 上具有唯一索引,尝试使用 col = 4 插入行(如果您不想在 INSERT 之前生成虚拟数据,则可以放置虚拟数据),然后在重复键错误回滚的情况下,如果 INSERT 成功,您可以使用正确的数据更新行。 但请注意,如果您不想产生不必要的生成数据的成本,这可能意味着生成数据需要很长时间,并且所有这些时间您都会保留一个打开的事务,该事务插入了 col = 4 的行,这将保留所有其他试图插入相同行的进程挂起。我不确定这是否会比先生成数据然后插入数据要好得多。

评论

0赞 shehanpathi 6/7/2022
在这里的答案中有一些格式
0赞 Paul W 12/30/2022 #4

如果您的目标是只有一个会话插入缺失的行,而任何其他会话甚至不尝试插入 DATA 就不执行任何操作,那么您需要锁定整个表(这会降低您的并发性)或插入一个不完整的行并对其进行更新。

A.在列 COL 上创建主键

法典:

begin
  insert into test values (4,null);
  update test set data = ... where col = 4;
  commit;
exception
  when dup_val_on_index then
    null;
end;

尝试在列 4 上插入的第一个会话将成功并进入更新,您可以在其中对 DATA 进行昂贵的计算。尝试执行此操作的任何其他会话都将引发 PK 冲突(-00001 或 DUP_VAL_ON_INDEX),并转到捕获它并且不执行任何操作的异常处理程序 (NULL) 。它永远不会到达更新语句,因此不会做任何昂贵的事情来计算 DATA。

现在,这将导致另一个会话等待,而第一个会话计算 DATA 并执行更新。如果不想等待,可以使用 NOWAIT 使滞后会话在行被锁定时立即引发异常。如果该行不存在,也会引发异常,但会引发不同的异常。对普通代码分支使用异常处理不是很好,但是嘿,它应该可以工作。

declare
  var_junk number;
begin
  begin
    select col into var_junk from test where col = 4 for update nowait;
  exception
    when no_data_found then
      insert into test values (col,null);
      update test set data = ... where col = 4;
      commit;
    when others then
      null;
  end;
end;
  
0赞 vishun 3/21/2023 #5

然后使用:for update

  • 将隔离改为,可避免间隙锁定READ COMMITED
  • 或将隔离更改为SERIALIZABLE
0赞 ShivaPendem 3/22/2023 #6

这是现在使用的查询,即使在峰值负载下也能完美运行,

INSERT INTO users (users_email, users_name) SELECT * FROM (SELECT "[email protected]" as abc, "user" as abd) AS tmp WHERE NOT EXISTS ( SELECT users_id FROM users WHERE users_email = "[email protected]" ) LIMIT 1;