PHP 和 MySQL 如何处理并发请求?
P粉037215587
P粉037215587 2023-09-05 12:17:19
[PHP讨论组]

我一定遗漏了一些关于 PHP/Symfony 如何处理并发请求的信息,或者可能如何处理数据库上的潜在并发查询......

这段代码似乎在做不可能的事情 - 它随机(大约每月一次)在底部创建新实体的副本。我的结论是,当两个客户端两次发出相同的请求,并且两个线程同时执行 SELECT 查询,选取 stop == NULL 的条目,然后它们都(?)设置该条目的停止时间时,一定会发生这种情况,他们都写了一个新条目。

据我所知,这是我的逻辑大纲:

  1. 获取所有停止时间为 NULL 的条目
  2. 循环这些条目
  3. 仅当输入日期 (UTC) 与当前日期 (UTC) 不同时才继续
  4. 将打开条目的停止时间设置为 23:59:59 并刷新到数据库
  5. 构建一个新条目,开始时间为第二天的 00:00:00
  6. 断言该位置没有其他未结条目
  7. 断言该位置没有未来的条目
  8. 只有这样 - 将新条目刷新到数据库

控制器自动关闭和打开

//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;
}

我什至在这里使用 checkOpenEntries() 运行额外的检查,以查看此时该位置是否存在任何 stoptime == NULL 的条目。最初,我认为这是多余的,因为我认为如果一个请求正在数据库上运行和操作,则另一个请求只有在第一个请求完成后才会启动。

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);
        }
    }
}

我是否需要使用某种“锁定数据库”方法来启动此功能才能实现我想要做的事情?

我已经测试了所有功能,并且当我模拟各种状态时(即使不应该如此,也为停止时间输入 NULL 等),一切都正常。大多数情况下,一切都运行良好,但在月中的某一天,这种事情发生了......

P粉037215587
P粉037215587

全部回复(1)
P粉921165181

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

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

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

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

MySQL 文档:

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

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

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

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

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