我一定遺漏了一些關於 PHP/Symfony 如何處理並發請求的信息,或者可能如何處理數據庫上的潛在並發查詢......
這段程式碼似乎在做不可能的事情 - 它隨機(大約每月一次)在底部創建新實體的副本。我的結論是,當兩個客戶端兩次發出相同的請求,並且兩個線程同時執行SELECT 查詢,選取stop == NULL 的條目,然後它們都(?)設定該條目的停止時間時,一定會發生這種情況,他們都寫了一個新條目。
據我所知,這是我的邏輯大綱:
控制器自動關閉與開啟
//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 。 “
”; //開啟新條目 $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 。 “
”; } 別的 { $em->flush(); //儲存到資料庫 $txt =「為」開啟了一個新條目。 $r->getOperator()->getSignature() 。 」在相同位置 (" . $r->getPosition()->getName() . ")"; $messages .= “” 。 $txt 。 “
”; } } } } 返回$訊息; }
我甚至在這裡使用 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 等),一切都正常。大多數情況下,一切都運作良好,但在月中的某一天,這種事情發生了...
您永遠無法保證順序(或隱式獨佔存取)。嘗試一下,你就會把自己挖掘得越來越深。
正如Matt 和KIKO 在評論中提到的,您可以使用約束和事務,這些應該會有很大幫助,因為您的資料庫將保持乾淨,但請記住您的應用程式需要能夠捕獲資料庫層產生的錯誤。絕對值得先嘗試。
處理此問題的另一種方法是強制資料庫/應用程式層級鎖定。
資料庫層級鎖定更粗糙,如果您在某個地方忘記釋放鎖定(在長時間運行的腳本中),則非常不可原諒。
MySQL 文件:
#鎖定整個表通常是一個壞主意,但它是可行的。這很大程度上取決於應用程式。
一些開箱即用的 ORM 支援物件版本控制,如果版本在執行過程中發生更改,則會拋出異常。理論上,您的應用程式會遇到異常,重試時會發現其他人已經填充了該字段,並且不再是更新的候選者。
應用程式層級鎖定更細粒度,但程式碼中的所有點都需要遵守鎖定,否則,您將回到方#1。如果您的應用程式是分散式的(例如 K8S,或只是部署在多個伺服器上),那麼您的鎖定機制也必須是分散式的(不是實例本地的)