问题
我正在尝试弄清楚如何在数据库中正确设置事务,并考虑潜在的延迟。
设置
在我的示例中,我有一个表 users
, keys
,其中每个用户可以拥有多个 key ,以及 config
表规定每个用户允许拥有多少个 key 。
我想运行一个存储过程:
- 确定是否允许给定用户请求 key 。
- 获取一把无人认领的可用 key 。
- 尝试为给定用户兑换 key 。
该过程的伪代码为:
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>)
),当给定用户只有权兑换一个 key 时,他们可以兑换多个 key ,方法是在第一个过程完成 sleep 之前在多个 session 中运行该过程(这又是为了模拟滞后)
这是 the Tables 的代码和 the 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
尽管配置中的最大值设置为每个用户 3 个,但最终会分配给他 4 个键。
问题
- 避免此类问题的最佳方法是什么?我正在尝试使用事务,但我觉得它不会专门帮助解决这个问题,并且可能会错误地实现。
- 我意识到解决问题的一种可能方法是将所有内容封装在一个大型更新查询中,但我宁愿避免这种情况,因为我喜欢能够设置单独的过程,其中每个过程仅用于执行单个任务。
- 此示例背后的数据库旨在供许多(数千)个并发用户使用。因此,最好是尝试兑换代码的一个用户不会阻止所有其他用户兑换代码。我可以更改我的代码,以便在其他用户已领取 key 时再次尝试兑换,但绝对不应该发生用户仅有权获取一个代码时可以兑换两个代码的情况。
最佳答案
如果您不想将所有内容封装在一个大型查询中,您就摆脱了困境,因为这实际上也无法解决任何问题,只会降低解决问题的可能性。
您需要的是对行的锁定,或者对将插入新行的索引的锁定。
InnoDB uses an algorithm called next-key locking that combines index-row locking with gap locking. InnoDB performs row-level locking in such a way that when it searches or scans a table index, it sets shared or exclusive locks on the index records it encounters. Thus, the row-level locks are actually index-record locks. In addition, a next-key lock on an index record also affects the “gap” before that index record. That is, a next-key lock is an index-record lock plus a gap lock on the gap preceding the index record. If one session has a shared or exclusive lock on record R in an index, another session cannot insert a new index record in the gap immediately before R in the index order.
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);
...我没有收到提示...没有响应...因为另一个 session 也有锁...但是:
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 个条目。发生死锁是因为两个 session 需要发生不兼容的事情:mysql1 需要 mysql2 释放其锁才能提交,而 mysql2 需要 mysql1 释放其锁才能插入。必须有人输掉这场战斗,通常,完成最少工作的线程就是失败者。
但是,如果当我执行 SELECT ... FOR UPDATE
时已经存在 1 行或多行怎么办?在这种情况下,锁将锁定在行上,因此尝试 SELECT
的第二个 session 实际上会阻塞等待 SELECT
,直到第一个 session 决定COMMIT
或 ROLLBACK
,此时第二个 session 将看到行数的准确计数(包括第一个 session 插入或删除的任何行数),并且可以准确地决定用户已经达到了允许的最大值。
你无法超越竞争条件,但你可以将它们锁定。
关于mysql - 处理 MySQL 事务中的延迟,我们在Stack Overflow上找到一个类似的问题: https://stackoverflow.com/questions/18623967/