使用SELECT FOR UPDATE和INSERT INTO

11

我正在制作一款抽奖游戏,有人建议我从MyISAM转换到InnoDB并开始使用FOR UPDATE,这样彩票(范围为1-16)就不会被重复售出。

现在我想知道,FOR UPDATE是如何工作的。

我在网上看到类似以下内容:

SELECT * FROM [table] WHERE [column] = [value] FOR UPDATE
UPDATE [table] SET [column] = [new_value]

然而,我的问题是关于以下内容的:它是否也适用于 INSERT

我的数据库设计:

CREATE TABLE IF NOT EXISTS `Lottery` (
  `id` varchar(50) NOT NULL,
  `preferedLotteryId` varchar(50) NOT NULL,
  `winningTicketId` int(11) NOT NULL DEFAULT '-1',
  `createdOn` datetime NOT NULL DEFAULT '1001-00-00 00:00:00',
  `startedOn` datetime NOT NULL DEFAULT '1001-00-00 00:00:00',
  `finishedOn` datetime NOT NULL DEFAULT '1001-00-00 00:00:00',
  `active` tinyint(1) NOT NULL,
  `deliveredOn` datetime NOT NULL DEFAULT '1001-00-00 00:00:00',
  `preferedDeliverMethod` int(2) NOT NULL DEFAULT '-1',
  `deliveredMethod` int(2) NOT NULL DEFAULT '-1',
  `deliveredByAccountId` varchar(50) DEFAULT NULL,
  PRIMARY KEY (`id`)
) ENGINE=InnoDB DEFAULT CHARSET=latin1;

CREATE TABLE IF NOT EXISTS `LotteryBid` (
  `bidId` varchar(50) NOT NULL,
  `accountId` varchar(50) NOT NULL,
  `auctionId` varchar(50) NOT NULL,
  `ticketId` int(50) NOT NULL,
  `datetime` datetime NOT NULL DEFAULT '1001-00-00 00:00:00',
  PRIMARY KEY (`bidId`)
) ENGINE=InnoDB DEFAULT CHARSET=latin1;

这意味着这个设计需要类似于 FOR UPDATE 的东西:

SELECT * FROM Lottery WHERE id = [id] FOR UPDATE
INSERT INTO LotteryBids SET [LotteryBids.values & Lottery.id]
或者
SELECT * FROM Lottery WHERE id = [id] FOR UPDATE
INSERT INTO LotteryBids, Lottery SET [LotteryBids.values] WHERE Lottery.id = [id]

然而,我不知道使用 FOR UPDATE 的方式是否可行。

我对这种大规模数据交互相当陌生,不知道该从哪里开始。

希望你们中的一些人能够帮助我。

顺颂时祺, Larssy1


我认为以下想法可能会转化为解决方案:

想法1:

一个用户正在购买彩票,其彩票ID为5。目前此彩票已售出3张,因此我想锁定此彩票的行数,直到我完成此插入操作为止。

想法2:

一个用户正在购买彩票,其彩票ID为5。目前此拍卖没有彩票与彩票ID 5相对应,因此我想预留一个类似拍卖ID和彩票ID的条目,并防止进一步添加,直到此查询完成。


使用的查询

选择一个彩票(包括附加信息):

SELECT au.*, asp.* FROM Lottery au, LotteryPrefered asp
WHERE au.preferedAuctionId = asp.id AND au.id = '" . $_auctionId . "'

或(不包括额外信息)

SELECT au.* FROM Lottery au
WHERE au.id = '" . $_auctionId . "'
购买门票:
INSERT INTO LotteryBids (bidId, accountId, auctionId, ticketId, datetime)
VALUES ('" . $guid . "', '" . $_accountId . "', '" . $_auctionId . "',
  '" . $_ticketId . "', NOW())

我猜你的INSERT语句是伪代码,但是WHERE子句在INSERT语句中会起到什么作用? - Marcus Adams
这基本上只是为了给 FOR UPDATE 发送一个小的“敲击”,表示已经发生了“更新”。但我不知道它实际上是如何工作的。 - larssy1
2个回答

21

使用 UPDATE 的 SELECT ... FOR UPDATE

在使用 InnoDB(关闭自动提交)的事务时,SELECT ... FOR UPDATE 允许一个会话暂时锁定特定的记录(或多个记录),以便其他会话无法更新它。然后,在同一事务中,该会话可以实际执行对同一记录的 UPDATE 并提交或回滚事务。这将使您能够锁定记录,以便其他会话不能更新它,同时您可能需要执行一些其他业务逻辑。

这是通过加锁实现的。InnoDB 利用索引来锁定记录,因此锁定现有记录似乎很容易 - 只需锁定该记录的索引即可。

使用 INSERT 的 SELECT ... FOR UPDATE

然而,要使用 SELECT ... FOR UPDATEINSERT,如何锁定尚不存在的记录的索引?如果您使用默认的隔离级别 REPEATABLE READ,则 InnoDB 还将利用间隙锁定。只要您知道要锁定的 id(甚至是 id 范围),那么 InnoDB 就可以锁定该间隙,从而使没有其他记录可以插入该间隙,直到我们完成为止。

如果您的 id 列是自动增量列,则使用 INSERT INTOSELECT ... FOR UPDATE 会存在问题,因为您不知道新的 id 是什么,直到您插入它。但是,由于您知道要插入的 id,所以 SELECT ... FOR UPDATEINSERT 将起作用。

示例

查看拍卖 12 中是否有可用的 ticketid 5

SELECT * FROM LotteryBids
WHERE auctionid = 12 AND ticketid = 5
FOR UPDATE
如果没有返回任何行,则表示它不存在。请插入它:
INSERT INTO LotteryBids (bidId, accountId, auctionId, ticketId, datetime)
VALUES ('abcd-efgh-ijkl-mnop', 100, 12, 5, NOW())

保持简单

处理并发问题可能难以理解,我经常看到人们过于复杂化事情。 我建议提出一个分开的问题,详细说明你想要实现什么,提供你的解决方案,并寻求建议。


我明白了,谢谢。ID列不是自动增量的,因为它是一个包含使用PHP类创建的GUID的varchar。如果我给你适当的数据,你能给我创建一个这样的FOR UPDATE示例吗?我知道网站主要不用于现成的案例,“让社区构建您的软件”。但是,如果有一个可以继续建立的示例,那将会受到高度赞赏。所以,如果愿意的话,您需要测试数据吗?--问题也已更新 - larssy1
1
@MarcusAdams 您的答案完全正确。我可能还要补充一点,即在这种情况下,“间隙”已被锁定,因此请确保这确实是您想要的。您将无法使用任何其他值在整个范围内插入数据,这可能是表的大部分。例如,在具有1、2、3、4、5的表中,如果您锁定10,则实际上会锁定从5-> supremum的范围,这意味着您无法锁定任何键>= 5。这通常与递增键一起使用。对于GUID / UUID,您将锁定表的基本上任意大的部分。 - jeremycole
1
值得注意的是,如果不使用“START TRANSACTION”和“COMMIT”或者针对相关会话设置“autocommit=0”,则所有这些操作都无法正常运行。(因为我注意到提问者没有提到这一点。) - jeremycole
@MarcusAdams 已添加了查询,如果您有任何遗漏,请告诉我。 - larssy1
@jeremycole,在这些查询中使用锁定是否可以防止任何更改,还是仅限于特定范围内的更改?如果我可以通过WHERE子句设置范围,那就太棒了。此外,我还没有看到那些“操作”,所以我没有包括它们。如果您有任何好建议或提示如何使其工作,请告诉我。我们非常乐意接受所有反馈 :) - larssy1
显示剩余3条评论

0

您可以尝试使用一种带有备用表的锁/互斥体:

请参见http://blog.udby.com/archives/14(实现简单的数据库信号量)

在开始处理给定类型之前,您可以通过在锁定表中进行INSERT + DELETE来锁定“类型”。


网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接