29

How can I guarantee that I can search if a username exists in my database, then insert that username into the database as a new row without any intercept between the SELECT and INSERT statements?

Almost as if I am locking on a row that doesn't exist. I want to lock on the non-existent row with the username "Foo", so that I can now check if it exists in the database AND insert it into the database if it doesn't already exist without any interruption.

I know that using LOCK IN SHARE MODE and FOR UPDATE exist but as far as I know, that only works on rows that already exist. I am not sure what to do in this situation.

4

5 回答 5

31

虽然上面的答案是正确的,因为 SELECT ... FOR UPDATE 将阻止并发会话/事务插入相同的记录,但这并不是全部事实。我目前正在与同样的问题作斗争,并且得出的结论是 SELECT ... FOR UPDATE 在这种情况下几乎没有用,原因如下:

并发事务/会话也可以对相同的记录/索引值执行 SELECT ... FOR UPDATE,MySQL 会很高兴地立即接受(非阻塞)并且不会抛出错误。当然,一旦其他会话完成了该操作,您的会话也无法再插入记录。您或其他会话/事务也不会获得有关情况的任何信息,并认为他们可以安全地插入记录,直到他们真正尝试这样做。然后尝试插入会导致死锁或重复键错误,具体取决于具体情况。

换句话说, SELECT ... FOR UPDATE 会阻止其他会话插入相应的记录,但是即使您执行 SELECT ... FOR UPDATE 并且找不到相应的记录,您实际上也可能无法插入该记录。恕我直言,这使得“先查询,然后插入”方法无用。

问题的原因是 MySQL 没有提供任何方法来真正锁定不存在的记录。两个并发会话/事务可以同时锁定不存在的记录“FOR UPDATE”,这实际上是不可能的,这使得开发变得更加困难。

解决此问题的唯一方法似乎是使用信号量表或在插入时锁定整个表。请参阅 MySQL 文档以获取有关锁定整个表或使用信号量表的进一步参考。

只是我的2美分...

于 2015-07-02T12:09:33.773 回答
27

如果有一个索引username(应该是这种情况,如果没有,添加一个,最好是UNIQUE一个),然后发出一个SELECT * FROM user_table WHERE username = 'foo' FOR UPDATE;将阻止任何并发事务创建这个用户(以及“上一个”和“下一个”非唯一索引的可能值)。

如果没有找到合适的索引(满足WHERE条件),那么有效的记录锁定是不可能的并且整个表被锁定*。

这个锁将一直持有到发出SELECT ... FOR UPDATE.

在这些手册页中可以找到关于这个主题的一些非常有趣的信息。

*我说高效,因为实际上记录锁实际上是对索引记录的锁。当没有找到合适的索引时,只能使用默认的聚集索引,并且会被全锁。

于 2013-06-12T15:23:37.140 回答
10

锁定不存在的记录在 MySQL 中不起作用。有几个关于它的错误报告:

一种解决方法是使用互斥表,其中现有记录将在插入新记录之前被锁定。例如,有两个表:sellers 和 products。卖家有很多产品,但不应有任何重复的产品。在这种情况下,卖家表可以用作互斥表。在插入新产品之前,会在卖家记录上创建一个锁。通过这个附加查询,可以保证在任何给定时间只有一个线程可以执行该操作。没有重复。没有僵局。

于 2016-06-12T03:56:05.533 回答
2

你在“正常化”?也就是说,该表是一对 id 和名称的列表?并且您正在插入一个新的“名称”(并且可能希望id在其他表中使用)?

然后拥有UNIQUE(name)和做

INSERT IGNORE INTO tbl (name) VALUES ($name);

这并没有解释id刚刚创建的方法,但是您没有问过这个问题。

请注意,id在发现是否需要之前分配“新”。因此,这可能会导致AUTO_INCREMENT价值迅速增加。

也可以看看

 INSERT ... ON DUPLICATE KEY UPDATE ...

VALUES()以及与和一起使用的技巧LAST_INSERT_ID(id)。但是,再一次,你没有在问题中说明真正的目的,所以我不想不必要地进入更多细节。

注意:上面不关心语句的值autocommit或语句是否在显式事务中。

为了一次性规范化一批“名称”,这里给出的 2 个 SQL 非常有效: http: //mysql.rjweb.org/doc.php/staging_table#normalization 该技术避免了“燃烧”ID,并避免了任何运行时错误。

于 2019-11-11T20:25:34.747 回答
0

没有直接回答这个问题,但是使用 Serializable 隔离级别是否可以实现最终目标?假设最终目标是避免重复名称。从冬宫

MySQL“可序列化”防止反依赖循环(G2):

set session transaction isolation level serializable; begin; -- T1
set session transaction isolation level serializable; begin; -- T2
select * from test where value % 3 = 0; -- T1
select * from test where value % 3 = 0; -- T2
insert into test (id, value) values(3, 30); -- T1, BLOCKS
insert into test (id, value) values(4, 42); -- T2, prints "ERROR 1213 (40001): Deadlock found when trying to get lock; try restarting transaction"
commit;   -- T1
rollback; -- T2
于 2018-08-26T03:28:02.957 回答