首页  >  问答  >  正文

PHP 和 MySQL 如何处理并发请求?

<p>我一定遗漏了一些关于 PHP/Symfony 如何处理并发请求的信息,或者可能如何处理数据库上的潜在并发查询......</p> <p>这段代码似乎在做不可能的事情 - 它随机(大约每月一次)在底部创建新实体的副本。我的结论是,当两个客户端两次发出相同的请求,并且两个线程同时执行 SELECT 查询,选取 stop == NULL 的条目,然后它们都(?)设置该条目的停止时间时,一定会发生这种情况,他们都写了一个新条目。</p> <p>据我所知,这是我的逻辑大纲:</p> <ol> <li>获取所有停止时间为 NULL 的条目</li> <li>循环这些条目</li> <li>仅当输入日期 (UTC) 与当前日期 (UTC) 不同时才继续</li> <li>将打开条目的停止时间设置为 23:59:59 并刷新到数据库</li> <li>构建一个新条目,开始时间为第二天的 00:00:00</li> <li>断言该位置没有其他未结条目</li> <li>断言该位置没有未来的条目</li> <li>只有这样 - 将新条目刷新到数据库</li> </ol> <p>控制器自动关闭和打开</p> <pre class="brush:php;toolbar:false;">//if entry spans daybreak (midnight) close it and open a new entry at the beginning of next day private function autocloseAndOpen($units) { $now = new \DateTime("now", new \DateTimeZone("UTC")); $repository = $this->em->getRepository('App\Entity\Poslog\Entry'); $query = $repository->createQueryBuilder('e') ->where('e.stop is NULL') ->getQuery(); $results = $query->getResult(); if (!isset($results[0])) { return null; //there are no open entries at all }$em = $this->em; $messages = “”; foreach($结果为$r){ if ($r->getPosition()->getACRGroup() == $unit) { //只触及用户自己的条目 $start = $r->getStart(); //断言跨越日期的条目 $startStr = $start->format(“Y-m-d”); //比较所必需的,如果在比较子句中放入$start->format(“Y-m-d”) PHP仍然会比较正在格式化的日期时间对象,而不是格式化的输出。 $nowStr = $now->format(“Y-m-d”); //比较所必需的,如果在比较子句中放入$start->format(“Y-m-d”) PHP仍然会比较正在格式化的日期时间对象,而不是格式化的输出。 if ($startStr < $nowStr) { $stop = new \DateTimeImmutable($start->format("Y-m-d")."23:59:59", new \DateTimeZone("UTC")); $r->setStop($stop); $em->flush(); $txt = $unit->getName() 。 ”在位置 (" . $r->getPosition()-> getName() . ") 中有一个跨越日期中断 (UTC) 的条目。自动关闭于“” 。 $stop->format(“Y-m-d H:i:s”) 。 “z”。 $messages .= “

” 。 $txt 。 “</p>”; //打开新条目 $newStartTime = $stop->modify('+1 秒'); $entry = 新条目(); $entry->setStart( $newStartTime ); $entry->setOperator( $r->getOperator() ); $entry->setPosition( $r->getPosition() ); $entry->setStudent( $r->getStudent() ); $em->坚持($entry); //在自动打开新条目之前断言没有未来的条目 $futureE = $this->checkFutureEntries($r->getPosition(),true); $openE = $this->checkOpenEntries($r->getPosition(), true); if ($futureE !== 0 || $openE !== 0) { $txt =“尝试为”打开一个新条目。 $r->getOperator()->getSignature() 。 ”第二天在同一位置(“.$r->getPosition()->getName().”),但存在冲突的条目。”; $messages .= “

” 。 $txt 。 “</p>”; }else { $em->flush(); //store to DB $txt = "A new entry was opened for " . $r->getOperator()->getSignature() . " in the same position (" . $r->getPosition()->getName() . ")"; $messages .= "<p>" . $txt . "</p>"; } } } } return $messages; }</pre> <p>我什至在这里使用 checkOpenEntries() 运行额外的检查,以查看此时该位置是否存在任何 stoptime == NULL 的条目。最初,我认为这是多余的,因为我认为如果一个请求正在数据库上运行和操作,则另一个请求只有在第一个请求完成后才会启动。</p> <pre class="brush:php;toolbar:false;">private function checkOpenEntries($position,$checkRelatives = false) { $positionsToCheck = array(); if ($checkRelatives == true) { $positionsToCheck = $position->getRelatedPositions(); $positionsToCheck[] = $position; } else { $positionsToCheck = array($position); } //Get all open entries for position $repository = $this->em->getRepository('App\Entity\Poslog\Entry'); $query = $repository->createQueryBuilder('e') ->where('e.stop is NULL and e.position IN (:positions)') ->setParameter('positions', $positionsToCheck) ->getQuery(); $results = $query->getResult(); if(!isset($results[0])) { return 0; //tells caller that there are no open entries } else { if (count($results) === 1) { return $results[0]; //if exactly one open entry, return that object to caller } else { $body = 'Found more than 1 open log entry for position ' . $position->getName() . ' in ' . $position->getACRGroup()->getName() . ' this should not be possible, there appears to be corrupt data in the database.'; $this->email($body); $output['success'] = false; $output['message'] = $body . ' An automatic email has been sent to ' . $this->globalParameters->get('poslog-email-to') . ' to notify of the problem, manual inspection is required.'; $output['logdata'] = null; return $this->prepareResponse($output); } } }</pre> <p>我是否需要使用某种“锁定数据库”方法来启动此功能才能实现我想要做的事情?</p> <p>我已经测试了所有功能,并且当我模拟各种状态时(即使不应该如此,也为停止时间输入 NULL 等),一切都正常。大多数情况下,一切都运行良好,但在月中的某一天,这种事情发生了......</p>

P粉037215587P粉037215587427 天前539

全部回复(1)我来回复

  • P粉921165181

    P粉9211651812023-09-06 11:35:04

    您永远无法保证顺序(或隐式独占访问)。尝试一下,你就会把自己挖掘得越来越深。

    正如 Matt 和 KIKO 在评论中提到的,您可以使用约束和事务,这些应该会有很大帮助,因为您的数据库将保持干净,但请记住您的应用程序需要能够捕获数据库层产生的错误。 绝对值得首先尝试。

    处理此问题的另一种方法是强制数据库/应用程序级别锁定。

    数据库级锁定更加粗糙,如果您在某个地方忘记释放锁定(在长时间运行的脚本中),则非常不可原谅。

    MySQL 文档:

    锁定整个表通常是一个坏主意,但它是可行的。这很大程度上取决于应用程序。

    一些开箱即用的 ORM 支持对象版本控制,如果版本在执行过程中发生更改,则会抛出异常。理论上,您的应用程序会遇到异常,重试时会发现其他人已经填充了该字段,并且不再是更新的候选者。

    应用程序级锁定更加细粒度,但代码中的所有点都需要遵守锁定,否则,您将回到方#1。如果您的应用程序是分布式的(比如 K8S,或者只是部署在多个服务器上),那么您的锁定机制也必须是分布式的(不是实例本地的)

    回复
    0
  • 取消回复