あなたが電子商取引システムに取り組んでいて、何千人もの人々が最後に残った製品を同時に購入しようとしていると想像してください。ただし、多くの人はチェックアウトに進み、注文を完了することができました。在庫を確認すると、マイナスの数量の商品があります。これはどのようにして可能でしょうか?また、どうすれば解決できますか?
コーディングしてみましょう!まず最初に考えられるのは、チェックアウト前に在庫を確認することです。おそらく次のようなものでしょう:
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); }
この検証を使用することはできますが、1 秒あたり数百、数千、数百万、さらには数十のリクエストについて話す場合、この検証では十分ではありません。 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 を使用した Stock テーブルへのすべてのリクエストは、実際のトランザクションが終了するまで待機します。ここでの目的は、株価の最新の更新値を確実に取得することです。
この解決策は前の解決策と似ていますが、ロックキーを選択できます。検証と在庫削減のすべての処理が完了するまで、トランザクション全体をロックします。
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. これにより、在庫がゼロ未満になることは許可されません。したがって、2 人が同時に購入しようとすると、データベースでは在庫
@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);
1 番目と 2 番目の解決策は、戦略として悲観的ロックを使用します。 3 つ目は楽観的ロックです。悲観的ロック戦略は、リソースに関係するタスクを実行する際に、リソースへのアクセスを制限する場合に使用されます。プロセスが完了するまで、ターゲット リソースは他のアクセスに対してロックされます。デッドロックに注意してください!
楽観的ロックを使用すると、ブロックなしで同じリソースに対してさまざまなクエリを実行できます。競合が発生する可能性が低い場合に使用されます。通常、行に関連するバージョンがあり、この行を更新すると、データベースは行のバージョンとデータベース内の行のバージョンを比較します。両方が等しい場合、変更は成功します。そうでない場合は、再試行する必要があります。ご覧のとおり、この記事ではバージョン行を使用していませんが、3 番目の解決策はリクエストをブロックせず、ストック > を使用して同時実行を制御します。 0 条件。
完全なコードを見たい場合は、私の GitHub で確認できます。
悲観的ロックと楽観的ロックを実装するための戦略は他にもたくさんあります。たとえば、FOR UPDATE WITH SKIP LOCKED について詳しく検索できます。
以上がJava と PostgreSQL を使用して競合状態に対処する方法の詳細内容です。詳細については、PHP 中国語 Web サイトの他の関連記事を参照してください。