【问题标题】:How do I lock on an InnoDB row that doesn't exist yet?如何锁定尚不存在的 InnoDB 行?
【发布时间】:2013-06-08 18:05:06
【问题描述】:

如何保证我可以搜索数据库中是否存在用户名,然后将该用户名作为新行插入数据库中,而不会在SELECTINSERT 语句之间进行任何拦截?

几乎就像我锁定了一个不存在的行。我想用用户名“Foo”锁定不存在的行,这样我现在可以检查它是否存在于数据库中,如果它不存在则将其插入数据库任何中断。

我知道使用 LOCK IN SHARE MODEFOR UPDATE 存在,但据我所知,这仅适用于已经存在的行。我不知道在这种情况下该怎么办。

【问题讨论】:

    标签: mysql locking innodb rowlocking database-locking


    【解决方案1】:

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

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

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

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

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

    只要我的 2 美分 ...

    【讨论】:

    • 另一种选择,虽然不一定在所有情况下都是理想的,但跳过 SELECT ... FOR UPDATE 并执行 INSERT 然后处理产生的重复键错误(根据我的经验,这更加一致当插入是执行的第一个操作时)。我敢肯定会有性能损失,但是,在很多情况下,与执行的其他操作相比,它可以忽略不计,并且可以省去创建互斥表的麻烦。
    • @Sanuden 在我的应用程序中,从数据库中获取错误总是意味着数据库有问题或 (SQL) 代码有问题,但绝不意味着 数据有问题本身。这是我很久以前出于严重原因做出的架构决定。例如,当你按照自己的方式进行操作时,我必须在每个地方检查从数据库返回的错误号(以确定它实际上是什么类型的错误),我必须跟踪 MySQL 的进一步开发以确保错误编号不会改变,移植会很困难。
    • 是的,但是我们在这里讨论的是在任何情况下都希望避免这种情况的情况。作为一个非常简单的示例,想象一个用户数据库,其中电子邮件地址必须是唯一的。您真的希望使用某个电子邮件地址的已注册用户的数据被试图使用相同电子邮件地址注册的人覆盖吗?我强烈认为答案是“不”......
    • 我面临同样的问题,我认为两个事务都可以在 select for update 不返回任何行并且在插入之前尝试获得咨询锁 (dev.mysql.com/doc/refman/5.7/en/locking-functions.html)。正如答案中所建议的,锁定整个表或使用信号量表也可以。
    • 我指的是您在之前评论中提供的链接中详细描述的限制和偏差行为。该链接详细列出了 5.7+ 版本和 5.7 之前的版本之间的差异。
    【解决方案2】:

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

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

    此锁将一直保留到发出SELECT ... FOR UPDATE 的事务结束。

    关于这个主题的一些非常有趣的信息可以在these manual pages找到。

    * 我说高效,因为事实上a record lock is actually a lock on index records。当没有找到合适的索引时,只能使用默认的clustered index,并且会被全锁。

    【讨论】:

    • 如果我为没有索引的东西添加新行怎么办?它会在没有索引的情况下锁定整个表吗?
    • 是的,我忘了提。如果没有为记录锁找到合适的索引,则整个表将变为只读。
    • 这完全取决于SELECT ... FOR UPDATE语句中使用的过滤条件。如果可以使用索引(想想EXPLAIN),那么这个索引将用于锁定。如果不是,则整个表将被锁定。 MySQL 对FOR UPDATE 语句相当保守。无论表上的索引如何,此操作都是 100% 安全的。请注意,当您认为您只锁定一行时,您可能会锁定整个表。
    • 是的,这应该锁定整个表。查看执行计划:这样的查询不会使用索引。你应该摆弄你的数据并自己检查。尝试各种组合,您可以通过从第二个控制台发出SHOW ENGINE INNODB STATUS 来监控锁。检查“交易”部分。 innotop 是另一个非常有趣的工具。
    • 这似乎对我不起作用。我通过启动两个事务进行测试,在事务1中执行“SELECT ... FOR UPDATE”,然后在事务2中插入一条记录,发现事务2没有被阻塞。到目前为止,我发现的唯一解决方案是在每个 DELETE 语句前加上适当的 INSERT 语句,以确保在执行 DELETE 之前存在一行(因此可以锁定)。 FWIW,我的数据库处于 READ_COMMITTED 模式。
    【解决方案3】:

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

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

    【讨论】:

      【解决方案4】:

      你在“正常化”?也就是说,该表是一对 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 并避免了任何运行时错误。

      【讨论】:

        【解决方案5】:

        不直接回答问题,但使用 Serializable 隔离级别是否可以实现最终目标?假设最终目标是避免重复名称。来自Hermitage

        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
        

        【讨论】:

        • 嗯,目标不仅仅是防止重复。我们希望防止重复和相关错误在它们发生之前。也就是说,我们正在寻找一种干净的方法,它使我们能够安全地插入一行而不会发生运行时错误(如死锁、重复键等)。
        猜你喜欢
        • 1970-01-01
        • 1970-01-01
        • 1970-01-01
        • 1970-01-01
        • 2011-10-03
        • 1970-01-01
        • 1970-01-01
        • 1970-01-01
        • 2013-11-06
        相关资源
        最近更新 更多