PHP / Mysql并发数据库调用问题

3

使用innoDB表和mysqli包装器在PHP中进行查询。

我们目前遇到了一个问题,即我们正在经历一次流量激增,每秒请求同一脚本1,500次。

情况是最先访问该脚本的X个用户赢得奖品。

奖品是“prizes”表上的一条记录,其中包含要求数量和分配数量的计数。

一旦使用的数量> = 分配的数量,我们就停止颁发奖品。

所发生的事情是,脚本的若干请求同时读取行,而其他脚本实例还没有更新该行,因此向它们显示仍有一定数量的奖品可供领取。这导致我们颁发的奖品超过了分配的数量。

有什么想法可以规避这种情况吗?


1
请分享您的架构、PHP代码和SQL查询。 - Alex
请分享您的代码。 - ryantxr
欢迎来到竞态条件的世界。 - tadman
3个回答

1

没错,你描述的是一个典型的竞态条件。

一种解决方案是在更新之前使用SELECT...FOR UPDATE在奖品表中建立锁定行。 InnoDB将确定锁定请求的顺序,使每个请求等待其轮到它获取锁定。

然而,这不是一个好的解决方案,因为它会导致每个用户的浏览器旋转和旋转,等待响应。在服务器上,您很快就会得到1500个锁定请求每秒排队。即使每个会话只需要10毫秒来执行SELECT FOR UPDATE和随后的UPDATE(这已经相当雄心勃勃了),仍然需要每秒15.0秒的工作量。到第2秒,您将需要30.0秒的工作量。

同时,用户正在查看他们的浏览器挂起,直到轮到他们为止。这基本上是一个设计上的破绽。

基本上,您需要一些解决方案来建立请求顺序,该方案具有以下特点:

  • 全局
  • 原子性
  • 快速
  • 异步
你可以让每个并发请求都执行一个带有AUTO_INCREMENT键的插入表操作,这将保证它们的顺序。然后,一旦有X行,后续请求就不再插入任何行。
另一种方法是使用消息队列。每个请求只需将自己的请求推送到队列中。然后单个消费者从队列中提取前X个请求,并向它们授予奖品。队列中其余的请求将被丢弃,它们将无法获得奖品。

1
谢谢,我们使用了这个的变体并加以调整,使其按我们需要的方式运行。 - Giancarlo Massaro

1

如果您没有提供代码、表结构或更深入的数据库信息,那么在这种情况下的第一个建议是使用 LOCK

如果您的表是 innoDB 类型,则可以受益于行级锁定,但如果该表只有一行,则这将无关紧要。

在伪代码中,每次点击时,您需要:

LOCK TABLE prizes
SELECT claimed, alloted FROM prizes

if claimed < alloted award prize
UPDATE prizes set claimed = claimed +1

else
do_nothing
UNLOCK TABLE prizes

<<after unlocking>>
if the user got an award, do whatever you need to do to award the prize which is not "inventory-sensitive" and can be done asynchronously

这个程序的运行时间应该在毫秒级别,所以如果您的数据库服务器运转良好,所有的命中都排队等待不应该成为问题,尽管您的应用程序服务器可能会达到进程限制或连接限制,因此这样的情况需要进行一些压力测试。
这可能会很棘手、复杂、易错...
更简单的方法是:
设置一个“claim_attempt”表,具有自动递增的主键和一个引用用户信息的字段。
每次点击时,无论库存是否可用,都插入一条记录并检索插入的行的ID。之后,将检索到的ID与分配的奖励号码进行比较。如果id≤alloted,则运行必要的过程以向用户提供奖品。如果id>alloted,则打印“下次再试”的消息。

0
解决这个问题的一种方法是有一个包含个人奖项记录的表,并尝试使用查询“认领”其中一个,例如:
UPDATE prizes SET claimed_by=? WHERE prize_type=? AND claimed_by IS NULL LIMIT 1

如果在 prize_typeclaimed_by 上强制实施 UNIQUE 约束,则意味着没有人可以领取同一类型的多个奖品。这是在数据库级别上执行的,无法通过时间问题规避。
当您调用该更新时,您将获得零行或一行修改。检查结果的更新行数以查看是否成功领取奖励。

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