【问题标题】:Insert row if not exists without deadlock如果不存在则插入行且无死锁
【发布时间】:2017-07-21 17:15:16
【问题描述】:

我有一张简单的桌子

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;

我试图确保此事务的两个副本同时运行不会导致重复行和死锁。我也不想产生为col = 4 多次生成data 的成本。

我试过了:

  1. SELECT ..(没有FOR UPDATELOCK IN SHARE MODE):

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

  2. SELECT .. LOCK IN SHARE MODE

    两个事务都在col = 4 上获取共享锁,生成data 并尝试使用col = 4 插入一行。两个事务都等待对方释放它们的共享锁,以便它可以INSERT,从而产生ERROR 1213 (40001): Deadlock found when trying to get lock; try restarting transaction

  3. SELECT .. FOR UPDATE

    预计一个事务的SELECT 将成功并获得col = 4 的排他锁,而另一个事务的SELECT 将阻塞等待第一个事务。

    相反,SELECT .. FOR UPDATE 的两个查询都会成功,并且事务会像 SELECT .. LOCK IN SHARE MODE 一样进入死锁状态。 col = 4 的独占锁似乎不起作用。

我怎样才能写这个事务而不导致重复行和没有死锁?

【问题讨论】:

  • 不要这样做。请设置UNIQUE 索引并改用INSERT ... ON DUPLICATE KEYINSERT IGNORE
  • @tadman 如果我这样做INSERT .. ON DUPLICATE KEY UPDATE,那么这两个事务都会产生生成data 的成本,这是不必要的。如果我执行INSERT IGNORE ..,那么 MySQL 在运行INSERT 时遇到的 all 错误将被忽略(不仅仅是重复的键),这非常草率。
  • tx_isolation的值是多少?
  • @RickJames REPEATABLE READSERIALIZABLE 只会使SELECT .. 版本的行为与SELECT .. LOCK IN SHARE MODE 相同)

标签: mysql sql innodb mariadb deadlock


【解决方案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)

【讨论】:

  • 如果我做INSERT .. ON DUPLICATE KEY UPDATE,那么这两个交易都会产生生成data的成本,这是不必要的。我只希望其中一个继续生成data
  • 您在问题中给出的示例不会生成。它有一个简单的字符串。如果您想避免重复生成,请找出您需要为哪些 ID 生成数据并使用某种队列来安排这些操作。有许多工作队列系统可供选择。长时间保持打开锁是乞求死锁。
【解决方案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;

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

【讨论】:

  • INSERT IGNORE 忽略所有错误和警告,而不仅仅是重复的关键错误,因此它使我无法检测和报告在该查询期间可能发生的其他类型的错误和警告。
【解决方案3】:

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

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

【讨论】:

    猜你喜欢
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 2018-09-17
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 2012-12-17
    相关资源
    最近更新 更多