【问题标题】:Logical Help Needed in a PHP ScriptPHP 脚本中需要的逻辑帮助
【发布时间】:2023-03-08 10:22:01
【问题描述】:

我正在编写一个小的 PHP 脚本,它只是使用以下查询从 MYSQL 表中返回数据

"SELECT * FROM data where status='0' limit 1";

读取数据后,我通过使用以下查询获取特定行的 ID 来更新状态

"Update data set status='1' WHERE id=" . $db_field['id'];

对于单个客户来说,一切都很好。现在我愿意为多个客户制作这个特定的页面。有超过 20 个客户端将在几乎相同的时间(24/7)连续访问同一页面。是否有可能两个或多个客户端从表中读取相同的数据?是的话怎么解决呢?

谢谢

【问题讨论】:

  • 您能否澄清一下这两个 SQL 调用是在单个 HTTP 请求中进行的,还是您选择的流,发送给用户,他们做某事,然后您更新他们的第二个 HTTP 请求.没有上下文,我无法推测。您提出的风险发生在任何一种情况下,但您的解决方案选择会因这两种情况而异。
  • 是的,在单个 HTTP 请求中。用户没有对数据接受读取做任何事情。
  • 在这种情况下,您有几个通常可以接受的选项 - 最简单的选项是您可以调整您的逻辑以使其成为单个更新命令,这样您就可以执行 update .... where status=0 limit 1 您也可以使用交易。我想第三种选择是进行初始更新,它将在状态上设置某种特定于客户端的信号量,选择该状态,做你的事情,然后再次更新状态。
  • 信号量概念在我下面的回答中进行了一些详细的阐述。

标签: php mysql localhost


【解决方案1】:

考虑并发是对的。除非您只有 1 个 PHP 线程响应客户端请求,否则实际上没有什么可以阻止它们从 data 分发同一行以进行处理 - 事实上,由于它们每个都将运行相同的查询,它们几乎每个当然分发同一行。

解决该问题的最简单方法是锁定,正如接受的答案中所建议的那样。如果 PHP 服务器线程运行 SELECT...FOR UPDATELOCK TABLE ... UNLOCK TABLES (非事务性)所花费的时间很短,那么这可能会起作用,这样其他线程可以在每个线程运行此代码时等待(它仍然是浪费的,因为它们可能是处理一些其他数据行,稍后会详细介绍)。

有一个更好的解决方案,尽管它需要更改架构。想象一下,你有一张这样的表:

CREATE TABLE `data` (
  `data_id` int(10) unsigned NOT NULL AUTO_INCREMENT,
  `data` blob,
  `status` tinyint(1) DEFAULT '0',
  PRIMARY KEY (`data_id`)
) ENGINE=InnoDB;

您无法以事务方式更新“下一条处理的记录”,因为您必须更新的唯一字段是 status。但是想象一下你的桌子看起来更像这样:

CREATE TABLE `data` (
  `data_id` int(10) unsigned NOT NULL AUTO_INCREMENT,
  `data` blob,
  `status` tinyint(1) DEFAULT '0',
  `processing_id` int(10) unsigned DEFAULT NULL,
  PRIMARY KEY (`data_id`)
) ENGINE=InnoDB;

然后您可以编写类似这样的查询来使用您的“处理 id”更新要处理的“下一个”列:

UPDATE data  
SET processing_id = @unique_processing_id 
WHERE processing_id IS NULL and status = 0 LIMIT 1;

而且任何值得一试的 SQL 引擎都将确保您没有 2 个不同的处理 ID 来说明要同时处理的同一记录。然后在你闲暇时,你可以

SELECT * FROM data WHERE processing_id = @unique_processing_id;

并且知道您每次都会获得独一无二的记录。

这种方法也很适合解决持久性问题;您基本上可以识别每个data 行的批处理运行,这意味着您可以考虑每个批处理作业,而在此之前您可能只考虑数据行。

我可能会通过为此元数据添加第二个表来实现@unique_processing_id(自动增量键是真正的技巧,但可以添加其他数据处理元数据):

CREATE TABLE `data_processing` (
  `id` int(10) unsigned NOT NULL AUTO_INCREMENT,
  `date` timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP,
  `data_id` int(10) unsigned DEFAULT NULL,
  PRIMARY KEY (`id`)
) ENGINE=InnoDB;

并将其用作您的唯一 ID 的来源,您最终可能会得到如下结果:

INSERT INTO data_processing SET date=NOW();
SET @unique_processing_id = (SELECT LAST_INSERT_ID());
UPDATE data 
SET processing_id = @unique_processing_id 
  WHERE status = 0 LIMIT 1;
UPDATE data 
    JOIN data_processing ON data_processing.id = data.processing_id
  SET data_processing.data_id = data.data_id;
SELECT * from data WHERE processing_id = @unique_processing_id;
-- you are now ready to marshal the data to the client ... and ... 
UPDATE data SET status = 1 
    WHERE status = 0 
    AND processing_id = @unique_processing_id
LIMIT 1;

从而解决您的并发问题,并让您更好地审核持久性,这取决于您如何设置data_processing 表;您可以跟踪线程 ID、处理状态等,以帮助验证数据是否真正完成处理。

还有其他解决方案 - 消息队列可能是理想的,它允许您将每个未处理的数据对象的 ID 直接(或通过 php 脚本)排队到客户端,然后为该数据提供一个接口,以便单独检索和标记处理从“下一个”数据的队列中。但就“仅限 m​​ysql”的解决方案而言,我在这里向您展示的概念应该可以很好地为您服务。

【讨论】:

    【解决方案2】:

    您寻求的答案可能是使用交易。我建议您阅读以下帖子及其接受的答案:

    PHP + MySQL transactions examples

    如果没有,您还应该查看表锁定:

    13.3.5 LOCK TABLES and UNLOCK TABLES

    【讨论】:

    • 锁定系统确实是我想要的。我认为特定行也可以被“SELECT * FROM data where status='0' FOR UPDATE limit 1”锁定;然后在读取 $db_field = mysql_fetch_assoc($result) 时,我将数据存储在一个变量中,然后对其进行更新。如果它会工作?
    • 事务方法可能有效(需要 InnoDB),但由于您根本没有指定订单,因此每个查询很可能会尝试选择同一行 - 因此基本上可以保证您将正在阻塞其他线程。
    • 我们锁定表,而不是行。但从你所说的你可能想要交易,而不是锁定。
    • 我对这一点被误解的频率感到惊讶。您可以锁定整个表,如果您正在使用例如MyISAM,那是您唯一的选择,但“真正的”事务处理仍然使用锁定。请参阅dev.mysql.com/doc/refman/5.0/en/innodb-lock-modes.html - 区别在于是否需要锁定整个表,或者是否可以逐行进行锁定。
    【解决方案3】:

    我建议您为此使用session... 您可以将 id 保存到会话中... 所以你可以检查如果一个客户正在检查该记录,那么你不能允许另一个客户访问它 ...

    【讨论】:

    • 也许我理解错了,但这真的能回答问题吗?
    • 感谢您的帮助,但这种解决方案无法在我的案例中使用,因为所有客户端都是机器人,并且他们分配了一个工作来从特定页面读取数据并进行处理。同时,数据不应被多次处理。为此,我在每次读取数据后更新行的状态。
    • 哦,我真的很抱歉..可能是我误解了这个问题..请查看edited answer...