mysql 不存在行时插入行,无死锁

xt0899hw  于 2023-01-01  发布在  Mysql
关注(0)|答案(4)|浏览(162)

我有一张简单的table

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上的排它锁似乎不起作用。
    如何编写此事务而不导致重复行和死锁?
vqlkdk9b

vqlkdk9b1#

稍微调整架构:

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)
pzfprimi

pzfprimi2#

也许这个...

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。

siv3szwd

siv3szwd3#

注意InnoDB有两种类型的排他锁:一个用于更新和删除,另一个用于插入。因此,要执行SELECT FOR UPDATE事务,InnoDB必须首先在一个事务中获取更新锁,然后第二个事务将尝试获取相同的锁,并阻塞等待第一个事务(不可能如你在问题中所说成功),则当第一事务试图执行INSERT时,它必须将其锁从用于更新的锁改变为用于插入的锁。InnoDB唯一能做的就是先将锁降级为共享锁,然后再将其升级回插入锁。而且当有另一个事务也在等待获取排它锁时,它不能降级锁。这就是为什么在这种情况下会出现死锁错误的原因。
正确执行此操作的唯一方法是在col上使用唯一索引,尝试INSERT col = 4的行(如果不想在INSERT之前生成伪数据,可以放置伪数据),然后在重复键错误回滚的情况下,如果INSERT成功,您可以使用正确的数据更新该行。但请注意,如果您没有'如果不想招致不必要地生成数据的成本,则这可能意味着生成数据花费很长时间,在此期间,您将保持一个插入col = 4的行的打开事务,这将保持所有其他试图插入同一行的进程挂起。我不确定这是否比先生成数据再插入数据好得多。

ws51t4hk

ws51t4hk4#

如果您的目标是只让一个会话插入缺失的行,而任何其他会话甚至不尝试插入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)。它永远不会到达update语句,所以不会做任何代价高昂的事情来计算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;

相关问题