【问题标题】:Handeling Latency in MySQL Transactions处理 MySQL 事务中的延迟
【发布时间】:2013-09-08 13:24:00
【问题描述】:

问题

我正在尝试弄清楚如何在数据库中正确设置事务,并考虑潜在的延迟。


设置

在我的示例中,我有一个 userskeys 表,其中每个用户可以有多个密钥,还有一个 config 表,它指示每个用户可以拥有多少个密钥。

我想运行一个存储过程:

  1. 确定是否允许给定用户请求密钥。
  2. 获取可用的、无人认领的密钥。
  3. 尝试为给定用户兑换密钥。

该过程的伪代码是:

    START TRANSACTION
(1)     CALL check_permission(...,@result);
        IF (@result = 'has_permission') THEN
(2)         SET @unclaimed_key_id = (QUERY FOR RETURNING AVAILABLE KEY ID);
(3)         CALL claim_key(@unclaimed_key_id);
        END IF;
    COMMIT;

我遇到的问题是,当我在步骤1 之后模拟延迟时(通过使用SELECT SLEEP(<seconds>)),当给定用户只有兑换一个权限时,他们可以兑换多个密钥,通过在第一个过程完成睡眠之前在多个会话中运行该过程(这也是模拟延迟)

这是the Tablesthe Procedures 的代码 (注意:对于这个小例子,我没有考虑索引和外键,但显然我在实际项目中使用它们)。


要查看我的问题,只需在数据库中设置表和过程,然后打开两个 mysql 终端,并在第一次运行:

CALL `P_user_request_key`(10,1,@out);
SELECT @out;

然后在第二次运行时快速(你有 10 秒):

CALL `P_user_request_key`(0,1,@out);
SELECT @out;

两个查询都将成功返回 key_claimed 并且用户 Bob 最终将分配给他 4 个键,尽管 config 中的最大值设置为每个用户 3 个。


问题

  1. 避免此类问题的最佳方法是什么?我正在尝试使用事务,但我觉得它不会专门帮助解决这个问题,并且可能执行此错误。
    • 我意识到解决问题的一种可能方法是将所有内容封装在一个大型更新查询中,但我更愿意避免这种情况,因为我喜欢能够设置单独的过程,其中每个过程仅用于完成一项任务。
  2. 此示例背后的数据库旨在供许多(数千)并发用户使用。因此,如果一位用户尝试兑换代码不会阻止所有其他用户兑换代码,那将是最好的。如果另一个用户已经领取了密钥,我可以将我的代码更改为仅尝试再次兑换,但绝对不应该发生用户在只有获得一个权限的情况下可以兑换两个代码的情况。

【问题讨论】:

    标签: mysql stored-procedures transactions latency


    【解决方案1】:

    您不想将所有内容都封装在一个大型查询中,因此您已经摆脱困境,因为这实际上也不会解决任何问题,只会降低可能性。

    您需要的是行上的锁,或插入新行的索引上的锁。

    InnoDB 使用一种称为 next-key 锁定的算法,它结合了索引行锁定和间隙锁定。 InnoDB 执行行级锁定的方式是,当它搜索或扫描表索引时,它会在它遇到的索引记录上设置共享或排他锁。因此,行级锁实际上是索引记录锁。此外,索引记录上的 next-key 锁定也会影响该索引记录之前的“间隙”。也就是说,next-key 锁是索引记录锁加上索引记录前面的间隙上的间隙锁。如果一个会话在索引中的记录 R 上具有共享或排他锁,则另一个会话无法在索引顺序中 R 之前的间隙中插入新的索引记录。

    http://dev.mysql.com/doc/refman/5.5/en/innodb-next-key-locking.html

    那么我们如何获得排他锁呢?

    两个连接,mysql1 和 mysql2,每个都使用SELECT ... FOR UPDATE 请求排他锁。表 'history' 有一个列 'user_id' 被索引。 (它也是一个外键。)没有找到任何行,所以它们看起来都正常进行,好像不会发生任何异常。 user_id 2808 有效,但没有任何历史记录。

    mysql1> start transaction;
    Query OK, 0 rows affected (0.00 sec)
    
    mysql2> start transaction;
    Query OK, 0 rows affected (0.00 sec)
    
    mysql1> select * from history where user_id = 2808 for update;
    Empty set (0.00 sec)
    
    mysql2> select * from history where user_id = 2808 for update;
    Empty set (0.00 sec)
    
    mysql1> insert into history(user_id) values (2808);
    

    ...我没有得到我的提示...没有响应...因为另一个会话也有锁定...但是随后:

    mysql2> insert into history(user_id) values (2808);
    ERROR 1213 (40001): Deadlock found when trying to get lock; try restarting transaction
    

    然后mysql1在插入时立即返回成功。

    Query OK, 1 row affected (3.96 sec)
    

    剩下的就是 mysql1 到 COMMIT 了,神奇的是,我们阻止了 0 个条目的用户插入超过 1 个条目。发生死锁是因为两个会话都需要发生不兼容的事情:mysql1 需要 mysql2 在它能够提交之前释放它的锁,而 mysql2 需要 mysql1 在它能够插入之前释放它的锁。有人必须输掉这场战斗,通常做最少工作的线程就是失败者。

    但是如果在我执行SELECT ... FOR UPDATE 时已经存在 1 行或更多行怎么办?在这种情况下,锁将在行上,因此尝试SELECT 的第二个会话实际上会阻塞等待SELECT,直到第一个会话决定COMMITROLLBACK,此时第二个会话会看到准确的行数计数(包括第一个会话插入或删除的任何行),并且可以准确地确定用户已经拥有允许的最大值。

    您无法超越竞争条件,但可以将其锁定。

    【讨论】:

    • 首先,感谢您的快速回复和有用的反馈! - 在我的示例中,我不希望一个用户的请求阻止其他用户的其他请求。当我将FOR UPDATE 添加到我的权限查询时(当我设置@UserCount 时,它似乎阻止了所有用户,而不仅仅是给定的用户。示例:我为用户1 运行CALL P_user_request_key(5,1,@out); 等待5 秒,然后运行@987654335 @ 用于用户 2,并且用户 2 的请求在 5 秒结束之前被阻止。如何修改该查询以仅锁定与给定用户关联的行?
    • 您会看到,因为用户 id 是相邻/连续的...如果您分散值,并且在值之间的索引中有数据,则不应看到该行为。当我测试这个以生成我在答案中发布的输出时,这就是我观察到的行为。
    猜你喜欢
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 2021-09-25
    • 2021-10-05
    • 1970-01-01
    • 1970-01-01
    相关资源
    最近更新 更多