PHP 和 MySQL 如何处理并发请求?
P粉037215587
2023-09-05 12:17:19
[PHP讨论组]
<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 ($results as $r) {
if ($r->getPosition()->getACRGroup() == $unit) { //only touch the user's own entries
$start = $r->getStart();
//Assert entry spanning datebreak
$startStr = $start->format("Y-m-d"); //Necessary for comparison, if $start->format("Y-m-d") is put in the comparison clause PHP will still compare the datetime object being formatted, not the output of the formatting.
$nowStr = $now->format("Y-m-d"); //Necessary for comparison, if $start->format("Y-m-d") is put in the comparison clause PHP will still compare the datetime object being formatted, not the output of the formatting.
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() . " had an entry in position (" . $r->getPosition()->getName() . ") spanning datebreak (UTC). Automatically closed at " . $stop->format("Y-m-d H:i:s") . "z.";
$messages .= "<p>" . $txt . "</p>";
//Open new entry
$newStartTime = $stop->modify('+1 second');
$entry = new Entry();
$entry->setStart( $newStartTime );
$entry->setOperator( $r->getOperator() );
$entry->setPosition( $r->getPosition() );
$entry->setStudent( $r->getStudent() );
$em->persist($entry);
//Assert that there are no future entries before autoopening a new entry
$futureE = $this->checkFutureEntries($r->getPosition(),true);
$openE = $this->checkOpenEntries($r->getPosition(), true);
if ($futureE !== 0 || $openE !== 0) {
$txt = "Tried to open a new entry for " . $r->getOperator()->getSignature() . " in the same position (" . $r->getPosition()->getName() . ") next day but there are conflicting entries.";
$messages .= "<p>" . $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>
您永远无法保证顺序(或隐式独占访问)。尝试一下,你就会把自己挖掘得越来越深。
正如 Matt 和 KIKO 在评论中提到的,您可以使用约束和事务,这些应该会有很大帮助,因为您的数据库将保持干净,但请记住您的应用程序需要能够捕获数据库层产生的错误。 绝对值得首先尝试。
处理此问题的另一种方法是强制数据库/应用程序级别锁定。
数据库级锁定更加粗糙,如果您在某个地方忘记释放锁定(在长时间运行的脚本中),则非常不可原谅。
MySQL 文档:
锁定整个表通常是一个坏主意,但它是可行的。这很大程度上取决于应用程序。
一些开箱即用的 ORM 支持对象版本控制,如果版本在执行过程中发生更改,则会抛出异常。理论上,您的应用程序会遇到异常,重试时会发现其他人已经填充了该字段,并且不再是更新的候选者。
应用程序级锁定更加细粒度,但代码中的所有点都需要遵守锁定,否则,您将回到方#1。如果您的应用程序是分布式的(比如 K8S,或者只是部署在多个服务器上),那么您的锁定机制也必须是分布式的(不是实例本地的)