我有一张简单的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
。
我试过:
SELECT ..
(不含FOR UPDATE
或LOCK IN SHARE MODE
):
两个事务都看到没有col = 4
的行(没有获取锁),并且都生成data
并插入col = 4
的行的两个副本。SELECT .. LOCK IN SHARE MODE
这两个事务都获取col = 4
上的共享锁,生成data
并尝试使用col = 4
插入行。这两个事务都等待另一个事务释放其共享锁,以便可以INSERT
,从而导致ERROR 1213 (40001): Deadlock found when trying to get lock; try restarting transaction
。SELECT .. FOR UPDATE
我"期望"一个事务的SELECT
将成功并获得col = 4
上的排他锁,而另一个事务的SELECT
将阻塞等待第一个。
相反,两个SELECT .. FOR UPDATE
查询都成功了,事务继续死锁,就像SELECT .. LOCK IN SHARE MODE
一样,col = 4
上的排它锁似乎不起作用。
如何编写此事务而不导致重复行和死锁?
4条答案
按热度按时间vqlkdk9b1#
稍微调整架构:
如果
col
是主键,则不能复制。然后使用
ON DUPLICATE KEY
功能:pzfprimi2#
也许这个...
注意:如果
PRIMARY KEY
是AUTO_INCREMENT
,这可能会“烧毁”一个ID。siv3szwd3#
注意InnoDB有两种类型的排他锁:一个用于更新和删除,另一个用于插入。因此,要执行SELECT FOR UPDATE事务,InnoDB必须首先在一个事务中获取更新锁,然后第二个事务将尝试获取相同的锁,并阻塞等待第一个事务(不可能如你在问题中所说成功),则当第一事务试图执行INSERT时,它必须将其锁从用于更新的锁改变为用于插入的锁。InnoDB唯一能做的就是先将锁降级为共享锁,然后再将其升级回插入锁。而且当有另一个事务也在等待获取排它锁时,它不能降级锁。这就是为什么在这种情况下会出现死锁错误的原因。
正确执行此操作的唯一方法是在col上使用唯一索引,尝试INSERT col = 4的行(如果不想在INSERT之前生成伪数据,可以放置伪数据),然后在重复键错误回滚的情况下,如果INSERT成功,您可以使用正确的数据更新该行。但请注意,如果您没有'如果不想招致不必要地生成数据的成本,则这可能意味着生成数据花费很长时间,在此期间,您将保持一个插入col = 4的行的打开事务,这将保持所有其他试图插入同一行的进程挂起。我不确定这是否比先生成数据再插入数据好得多。
ws51t4hk4#
如果您的目标是只让一个会话插入缺失的行,而任何其他会话甚至不尝试插入DATA就不执行任何操作,那么您需要锁定整个表(这会降低并发性),或者插入一个不完整的行并在其后进行更新。
A.在列COL上创建主键
代码:
尝试在第4列插入的第一个会话将成功,并继续进行更新,您可以在更新中执行昂贵的DATA计算。尝试执行此操作的任何其他会话都将引发PK违规(-00001或DUP_VAL_ON_INDEX),然后转到异常处理程序,该异常处理程序将捕获它,但不执行任何操作(NULL)。它永远不会到达update语句,所以不会做任何代价高昂的事情来计算DATA。
现在,这将导致另一个会话在第一个会话计算DATA并进行更新时等待。如果您不希望等待,可以使用NOWAIT命令,使滞后会话在该行被锁定时立即抛出异常。如果该行不存在,也会抛出异常,但会是另一个异常。对正常代码分支使用异常处理不是很好,但嘿,它应该可以工作。