PHP 和 MySQL 如何处理并发请求?
P粉037215587
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(&quot;now&quot;, new \DateTimeZone(&quot;UTC&quot;)); $repository = $this-&gt;em-&gt;getRepository('App\Entity\Poslog\Entry'); $query = $repository-&gt;createQueryBuilder('e') -&gt;where('e.stop is NULL') -&gt;getQuery(); $results = $query-&gt;getResult(); if (!isset($results[0])) { return null; //there are no open entries at all } $em = $this-&gt;em; $messages = &quot;&quot;; foreach ($results as $r) { if ($r-&gt;getPosition()-&gt;getACRGroup() == $unit) { //only touch the user's own entries $start = $r-&gt;getStart(); //Assert entry spanning datebreak $startStr = $start-&gt;format(&quot;Y-m-d&quot;); //Necessary for comparison, if $start-&gt;format(&quot;Y-m-d&quot;) is put in the comparison clause PHP will still compare the datetime object being formatted, not the output of the formatting. $nowStr = $now-&gt;format(&quot;Y-m-d&quot;); //Necessary for comparison, if $start-&gt;format(&quot;Y-m-d&quot;) is put in the comparison clause PHP will still compare the datetime object being formatted, not the output of the formatting. if ($startStr &lt; $nowStr) { $stop = new \DateTimeImmutable($start-&gt;format(&quot;Y-m-d&quot;).&quot;23:59:59&quot;, new \DateTimeZone(&quot;UTC&quot;)); $r-&gt;setStop($stop); $em-&gt;flush(); $txt = $unit-&gt;getName() . &quot; had an entry in position (&quot; . $r-&gt;getPosition()-&gt;getName() . &quot;) spanning datebreak (UTC). Automatically closed at &quot; . $stop-&gt;format(&quot;Y-m-d H:i:s&quot;) . &quot;z.&quot;; $messages .= &quot;&lt;p&gt;&quot; . $txt . &quot;&lt;/p&gt;&quot;; //Open new entry $newStartTime = $stop-&gt;modify('+1 second'); $entry = new Entry(); $entry-&gt;setStart( $newStartTime ); $entry-&gt;setOperator( $r-&gt;getOperator() ); $entry-&gt;setPosition( $r-&gt;getPosition() ); $entry-&gt;setStudent( $r-&gt;getStudent() ); $em-&gt;persist($entry); //Assert that there are no future entries before autoopening a new entry $futureE = $this-&gt;checkFutureEntries($r-&gt;getPosition(),true); $openE = $this-&gt;checkOpenEntries($r-&gt;getPosition(), true); if ($futureE !== 0 || $openE !== 0) { $txt = &quot;Tried to open a new entry for &quot; . $r-&gt;getOperator()-&gt;getSignature() . &quot; in the same position (&quot; . $r-&gt;getPosition()-&gt;getName() . &quot;) next day but there are conflicting entries.&quot;; $messages .= &quot;&lt;p&gt;&quot; . $txt . &quot;&lt;/p&gt;&quot;; } else { $em-&gt;flush(); //store to DB $txt = &quot;A new entry was opened for &quot; . $r-&gt;getOperator()-&gt;getSignature() . &quot; in the same position (&quot; . $r-&gt;getPosition()-&gt;getName() . &quot;)&quot;; $messages .= &quot;&lt;p&gt;&quot; . $txt . &quot;&lt;/p&gt;&quot;; } } } } 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-&gt;getRelatedPositions(); $positionsToCheck[] = $position; } else { $positionsToCheck = array($position); } //Get all open entries for position $repository = $this-&gt;em-&gt;getRepository('App\Entity\Poslog\Entry'); $query = $repository-&gt;createQueryBuilder('e') -&gt;where('e.stop is NULL and e.position IN (:positions)') -&gt;setParameter('positions', $positionsToCheck) -&gt;getQuery(); $results = $query-&gt;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-&gt;getName() . ' in ' . $position-&gt;getACRGroup()-&gt;getName() . ' this should not be possible, there appears to be corrupt data in the database.'; $this-&gt;email($body); $output['success'] = false; $output['message'] = $body . ' An automatic email has been sent to ' . $this-&gt;globalParameters-&gt;get('poslog-email-to') . ' to notify of the problem, manual inspection is required.'; $output['logdata'] = null; return $this-&gt;prepareResponse($output); } } }</pre> <p>我是否需要使用某种“锁定数据库”方法来启动此功能才能实现我想要做的事情?</p> <p>我已经测试了所有功能,并且当我模拟各种状态时(即使不应该如此,也为停止时间输入 NULL 等),一切都正常。大多数情况下,一切都运行良好,但在月中的某一天,这种事情发生了......</p>
P粉037215587
P粉037215587

全部回复(1)
P粉921165181

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

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

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

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

MySQL 文档:

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

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

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

热门教程
更多>
最新下载
更多>
网站特效
网站源码
网站素材
前端模板
关于我们 免责申明 意见反馈 讲师合作 广告合作 最新更新
php中文网:公益在线php培训,帮助PHP学习者快速成长!
关注服务号 技术交流群
PHP中文网订阅号
每天精选资源文章推送
PHP中文网APP
随时随地碎片化学习
PHP中文网抖音号
发现有趣的

Copyright 2014-2025 https://www.php.cn/ All Rights Reserved | php.cn | 湘ICP备2023035733号