我正在试图找出如何在数据库中正确设置事务,并考虑潜在的延迟。
在我的示例中,我有一个users
,keys
的表,其中每个用户可以有多个密钥,以及一个config
表,用于指示每个用户可以拥有多少个密钥
我想运行一个存储过程:
程序的伪代码是:
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 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
将最终分配给他4个密钥,尽管配置中的最大值设置为每个用户3个。
答案 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}},此时第二个会话将看到行数的准确计数(包括第一个会话插入或删除的任何行),并且可以准确地确定用户已经具有允许的最大值。
你不能超越竞争条件,但你可以锁定它们。