簡體   English   中英

PHP & MySQL 如何處理同時請求?

[英]How does PHP & MySQL handle simultaneous requests?

我一定遺漏了一些關於 PHP/Symfony 如何處理同時請求,或者可能如何處理數據庫上的潛在同時查詢的東西......

這段代碼似乎在做不可能的事情——它隨機地(大約每月一次)在底部創建一個新實體的副本。 我的結論是,當兩個客戶端兩次發出相同的請求時,這一定會發生,並且兩個線程同時執行 SELECT 查詢,選擇停止 == NULL 的條目,然后它們都(?)設置該條目的停止時間, 他們都寫了一個新條目。

據我了解,這是我的邏輯大綱:

  1. 獲取停止時間為 NULL 的所有條目
  2. 遍歷這些條目
  3. 僅當輸入日期 (UTC) 與當前日期 (UTC) 不同時才繼續
  4. 將開放條目的停止時間設置為 23:59:59 並刷新到 DB
  5. 新建一個開始時間為第二天 00:00:00 的條目
  6. 斷言 position 中沒有其他打開的條目
  7. 斷言 position 中沒有未來條目
  8. 只有這樣 - 將新條目刷新到數據庫

Controller 自動關閉和打開

//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 的條目在 position 中。最初,我認為這是多余的,因為我認為如果一個請求正在運行並在數據庫上操作,另一個請求將在第一個完成之前不會啟動。

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

我是否需要使用某種“鎖定數據庫”方法來啟動這個 function 來實現我想要做的事情?

我已經測試了所有功能,當我模擬各種狀態時(輸入 NULL 停止時間,即使它不應該是等)一切正常。 大多數時候一切都很好,但是在月中某天,這件事發生了……

您永遠無法保證順序(或隱式獨占訪問)。 試試看,你會把自己挖得越來越深。

正如 Matt 和 KIKO 在評論中提到的那樣,您可以使用約束和事務,它們應該會有很大幫助,因為您的數據庫將保持干凈,但請記住,您的應用程序需要能夠捕獲數據庫層產生的錯誤。 絕對值得一試。

另一種處理方法是強制執行數據庫/應用程序級鎖定。

如果您在某處忘記釋放鎖(在長時間運行的腳本中),則數據庫級鎖定更加粗糙且非常無情。

MySQL 文檔:

如果客戶端 session 的連接終止,無論是正常終止還是異常終止,服務器都會隱式釋放 session 持有的所有表鎖(事務性和非事務性)。 如果客戶端重新連接,則鎖定不再有效。

鎖定整個表通常是一個壞主意,但它是可行的。 這在很大程度上取決於應用程序。

一些開箱即用的 ORM 支持 object 版本控制,如果版本在執行期間發生變化,則會拋出異常。 理論上,您的應用程序會出現異常,並且在重試時會發現其他人已經填充了該字段並且不再是更新的候選者。

應用程序級鎖定更細粒度,但代碼中的所有點都需要遵守鎖定,否則,您將回到#1 方塊。 如果你的應用程序是分布式的(比如 K8S,或者只是部署在多個服務器上),你的鎖定機制也必須是分布式的(不是實例本地的)

暫無
暫無

聲明:本站的技術帖子網頁,遵循CC BY-SA 4.0協議,如果您需要轉載,請注明本站網址或者原文地址。任何問題請咨詢:yoyou2525@163.com.

 
粵ICP備18138465號  © 2020-2024 STACKOOM.COM