2

问题

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


设置

在我的示例中,我有一个 , 表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;

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

这是过程的代码 (注意:对于这个小例子,我不关心索引和外键,但显然我在实际项目中使用它们)。


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

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

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

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

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


问题

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

1 回答 1

2

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

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

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 在它能够插入之前释放它的锁。有人必须输掉这场战斗,通常做最少工作的线程就是失败者。

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

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

于 2013-09-05T00:08:55.203 回答