如果没有死锁,则插入行

时间:2017-03-02 05:38:49

标签: mysql sql innodb mariadb deadlock

我有一张简单的表

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;

我正在尝试确保同时运行此事务的两个副本导致没有重复行且没有死锁。我也不想承担为data多次生成col = 4的费用。

我试过了:

  1. SELECT ..(不包含FOR UPDATELOCK IN SHARE MODE):

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

    < / LI>
  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上的独占锁似乎不起作用。

  4. 如何在不导致重复行且没有死锁的情况下编写此事务?

3 个答案:

答案 0 :(得分:0)

稍微调整您的架构:

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)

答案 1 :(得分:0)

也许这......

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,则可以“烧掉”#39}。一个身份。

答案 2 :(得分:0)

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

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