想像一下您正在開發一個電子商務系統,成千上萬的人試圖同時購買最後剩餘的產品。然而,他們中的許多人可以繼續結帳並完成訂單。當您檢查庫存時,您的產品數量為負數。這是怎麼可能的,你該如何解決這個問題?
讓我們來編碼吧!您可能想到的第一件事是在結帳前檢查庫存。也許是這樣的:
public void validateAndDecreaseSolution(long productId, int quantity { Optional<StockEntity> stockByProductId = stockRepository.findStockByProductId(productId); int stock = stockByProductId.orElseThrow().getStock(); int possibleStock = stock - quantity; if (stock <= 0 || possibleStock < 0) { throw new OutOfStockException("Out of stock"); } stockRepository.decreaseStock(productId, quantity); }
您可以使用此驗證,但是當我們談論每秒數百、數千、數百萬甚至數十個請求時,此驗證是不夠的。當 10 個請求同時到達這段程式碼並且資料庫為 stockByProductId 傳回相同的值時,您的程式碼將會崩潰。在我們進行驗證時,您需要一種方法來阻止其他請求。
在 SELECT 上新增鎖定語句。在此範例中,我使用 Spring Data 的 FOR UPDATE 來完成此操作。正如 PostgreSQL 文件所述
FOR UPDATE 會導致 SELECT 語句擷取的資料行被鎖定,就像要進行更新一樣。這可以防止它們被其他交易修改或刪除,直到當前交易結束。
@Query(value = "SELECT * FROM stocks s WHERE s.product_id = ?1 FOR UPDATE", nativeQuery = true) Optional<StockEntity> findStockByProductIdWithLock(Long productId);
public void validateAndDecreaseSolution1(long productId, int quantity) { Optional<StockEntity> stockByProductId = stockRepository.findStockByProductIdWithLock(productId); // ... validate stockRepository.decreaseStock(productId, quantity); }
所有使用產品ID對stocks表的請求都會等待,直到實際交易完成。這裡的目標是確保您獲得股票的最新更新價值。
此解決方案與上一個類似,但您可以選擇鎖定鍵是什麼。我們將鎖定整個交易,直到完成所有驗證和庫存減量的處理。
public void acquireLockAndDecreaseSolution2(long productId, int quantity) { Query nativeQuery = entityManager.createNativeQuery("select pg_advisory_xact_lock(:lockId)"); nativeQuery.setParameter("lockId", productId); nativeQuery.getSingleResult(); Optional<StockEntity> stockByProductId = stockRepository.findStockByProductId(productId); // check stock and throws exception if it is necessary stockRepository.decreaseStock(productId, quantity); }
本次交易結束後,下一次請求只會與同ID的產品互動。
在這種情況下,我們不會鎖定行或交易。讓我們允許此事務繼續進行,直到更新語句為止。注意最後一個條件:庫存> 0. 這不會允許我們的庫存小於零。因此,如果兩個人嘗試同時購買,其中一個人會收到錯誤,因為我們的資料庫不允許庫存
@Transactional @Modifying @Query(nativeQuery = true, value = "UPDATE stocks SET stock = stock - :quantity WHERE product_id = :productId AND stock > 0") int decreaseStockWhereQuantityGreaterThanZero(@Param("productId") Long productId, @Param("quantity") int quantity);
第一個和第二個解決方案使用悲觀鎖定作為策略。第三是樂觀鎖。當您在執行涉及某個資源的任何任務時希望限制對該資源的存取權時,可以使用悲觀鎖定策略。在您完成進程之前,目標資源將被鎖定以進行任何其他存取。小心死鎖!
使用樂觀鎖,您可以對相同資源執行各種查詢,而不會出現任何阻塞。當衝突不太可能發生時使用它。通常,您會有一個與您的行相關的版本,當您更新該行時,資料庫會將您的行版本與資料庫中的行版本進行比較。如果兩者相等,則變更將成功。如果沒有,您必須重試。正如您所看到的,我在本文中沒有使用任何版本行,但我的第三個解決方案不會阻止任何請求並使用 stock > 控制並發性。 0 條件。
如果你想看完整的程式碼,可以查看我的GitHub。
還有許多其他策略來實現悲觀鎖定和樂觀鎖定,例如您可以搜尋更多有關 FOR UPDATE WITH SKIP LOCKED 的內容。
以上是如何使用 Java 和 PostgreSQL 處理競爭條件的詳細內容。更多資訊請關注PHP中文網其他相關文章!