处理MySQL事务中的延迟

时间:2013-09-04 21:18:17

标签: mysql stored-procedures transactions latency

问题

我正在试图找出如何在数据库中正确设置事务,并考虑潜在的延迟。


设置

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

我想运行一个存储过程:

  1. 确定是否允许给定用户请求密钥。
  2. 获得一张可用的,无人认领的钥匙。
  3. 尝试兑换给定用户的密钥。
  4. 程序的伪代码是:

        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个密钥,尽管配置中的最大值设置为每个用户3个。


    问题

    1. 避免这样的问题的最佳方法是什么?我正在尝试使用一个事务,但我觉得它不会特别帮助解决这个问题,并且可能正在实现这个错误。
      • 我意识到解决问题的一种可能方法是将所有内容封装在一个大型更新查询中,但我宁愿避免这种情况,因为我希望能够设置单独的程序,其中每个程序仅用于做一项任务。
    2. 此示例背后的数据库旨在供许多(数千)并发用户使用。因此,如果一个尝试兑换代码的用户不阻止所有其他用户兑换一个用户,那将是最好的。如果其他用户已经声明了密钥,我可以将代码更改为再次尝试再次兑换,但如果用户只有权获得一个密码,则绝对不会发生这种情况。

1 个答案:

答案 0 :(得分:2)

你不想在一个大型查询中封装所有内容,因为它实际上也不能解决任何问题,只是降低了它的可能性。

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

  

InnoDB使用一种称为下一键锁定的算法,它将索引行锁定与间隙锁定相结合。 InnoDB以这样一种方式执行行级锁定:当它搜索或扫描表索引时,它会在遇到的索引记录上设置共享锁或排它锁。因此,行级锁实际上是索引记录锁。此外,索引记录上的下一键锁定也会影响该索引记录之前的“间隙”。也就是说,下一键锁定是索引记录锁定加上索引记录之前的间隙上的间隙锁定。如果一个会话在索引中的记录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个条目的用户插入多个条目。发生死锁是因为两个会话都需要发生不兼容的事情:mysql1需要mysql2才能释放锁定才能提交,而mysql2需要mysql1才能释放锁定才能插入。有人不得不失去这场斗争,而且做得最少的线程通常是失败者。

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

你不能超越竞争条件,但你可以锁定它们。