在庫控除とロック

7682 ワード

シーン
アイテムWは現在在庫が1つ残っているが、ユーザーP 1,P 2が同時に購入すると、1人だけが購入に成功する.秒殺も同様の状況であり、1つの商品のみ、N人のユーザーが同時に購入し、1人だけが手に入れることができる.
一般的な実装方法は次のとおりです.
  • コード同期、例えばsynchronized,lock等の同期方法
  • を用いる.
  • .クエリーせずにupdate table set surplus=(surplus-buyQuantity)where id=xx and(surplus-buyQuantity)>0
  • を直接更新
  • CAS,update table set surplus=aa where id=xx and version=y
  • を使用
  • データベースロック、select xx for update
  • を使用
  • 分布式ロック(zookeeper,redis等)
  • を用いる.
    シナリオ1:synchronized,lockなどの同期方法を用いたコード同期
    面接の时、よくこの问题を闻いて、大部分の人はすべてこの方案で実现することに答えます.
    
    public synchronized void buy(String productName, Integer buyQuantity) {
        //     ...
        //       
        Product product  =          ;
        if (product.getSurplus < buyQuantity) {
            return "    ";
        }
        
        // set      
        product.setSurplus(product.getSurplus() - quantity);
        //      
        update(product);
        //     ...
        //     ...
    }
    

    メソッド宣言にsynchronizedのキーワードを加える同期を実現することで、2人のユーザが同時に購入し、buyメソッドになると同期実行し、2人目のユーザが実行すると、在庫が不足する.
    ええと、见ているのは合理的で、以前私もそうしました.だから今他の人に会ってこのように答えて、私は心の中で黙って考えます.若者はあなたはこの穴を踏んだことがありませんね.まずこの案の前提配置を言います.
  • spring宣言トランザクション管理
  • を使用
  • トランザクション伝播メカニズムデフォルト(PROPACATION_REQUIRD)
  • を使用
  • プロジェクトはcontroller-service-dao 3層に階層化され、トランザクション管理はservice層
  • にある.
    この案は実行できません.主に以下の点からです.
  • synchronizedの作用範囲は単一jvmインスタンスであり,クラスタ,分散などを行うと
  • は用いられない.
  • synchronizedはオブジェクトインスタンスに作用し、単一のインスタンスでない場合、複数のインスタンス間は同期しません(これは一般的にspringでbeanを管理し、デフォルトは単一のインスタンスです)
  • .
  • 単一jvmの場合、synchronizedは複数のデータベーストランザクションの独立性を保証することはできない.これはコード中のトランザクション伝播レベル、データベースのトランザクション独立性レベル、ロックタイミングなどに関連する.
  • 隔離レベルを先に言って、よく使うのはRead CommittedとRepeatable Readで、他の2種類はあまり使わないで言いません
  • RR(Repeatable Read)レベル.mysqlデフォルトはRRであり、トランザクションがオープンした後、他のトランザクションにコミットされたデータは読み込まれないことを前提として、buyメソッドの時にトランザクションがオープンすることを知っています.現在スレッドT 1,T 2が同時にbuyメソッドを実行していると仮定します.T 1が先に実行され、T 2が待機すると仮定します.springのトランザクションのオープンとコミットなどはaop(エージェント)によって実現されるので、buyメソッドを実行する前に、トランザクションが開始する.このときT 1,T 2は2つのトランザクションであり,T 1の実行が完了するとT 2が実行され,T 1がコミットするデータが読み取れないため問題が発生する.
  • RC(Read Committed)レベル.トランザクションがオープンすると、他のトランザクションにコミットされたデータを読み取ることができるようになります.T 2実行時には、T 1コミットの結果を読み取ることができます.しかし、問題は、T 2実行時にT 1のトランザクションがコミットされたことですか?トランザクションとロックのプロセスは次のとおりです.
  • オープントランザクション(aop)
  • ロック(synchronizedメソッドに入る)
  • ロック解除(synchronizedメソッドを終了)
  • コミットトランザクション(aop)

  • は先にロックを解除してからトランザクションを提出することを見ることができる.だからT 2はクエリーを実行して、やはりT 1が提出したデータを読んでいないかもしれないし、また問題が発生するのは隔離レベルの中の問題によって、主な矛盾はトランザクションの開始と提出のタイミングがロック解除のタイミングと一致しないことである.
  • トランザクションがオープンする前にロックをかけ、トランザクションがコミットされた後にロックを解除します.確かに可能です.これはトランザクションのシリアル化に相当します.パフォーマンスはともかく、どのように実現するかについて話します.デフォルトのトランザクション伝播メカニズムを使用すると、トランザクションがオープンする前にロックを解除することを保証します.トランザクションがコミットされた後にロックを解除するには、ロックを解除し、controller層にロックを解除する必要があります.これには潜在的な問題があります.在庫を操作する方法はすべてあります.すべて鍵をかけなければならなくて、しかも同じ鍵ならば、書くのはとても疲れます.しかもこのようにjvm.
  • にまたがることができません
  • 在庫照会、在庫控除の2ステップ操作を個別に抽出し、トランザクションを単独で使用し、トランザクション分離レベルをRCに設定する.これは実際には上の3-2-1と異曲同工であり、最終的にはトランザクションオープンコミットの外層にロックを解除することである.比較的良い点はエントリが少ないことである.controllerは処理しない.欠点は上のjvmにまたがることができないことを除いて、また、このメソッドは、別のサービスクラスに格納する必要があります.springを使用すると、同じbeanの内部メソッド呼び出しは、再エージェントされないため、構成された個別トランザクションなどは、別のサービスbeanに格納する必要があります.


  • シナリオ2クエリーなしで直接更新
    最初の案を見て、友达がいて言った.あなたの言うことはそんなに複雑で、そんなに多くの問題は、クエリーのデータが最新ではないからではないでしょうか.私たちはクエリーをしないで、直接更新すればいいのではないでしょうか.偽コードは以下の通りです.
    public synchronized void buy(String productName, Integer buyQuantity) {
        //     ...
        int      = update table set surplus = (surplus - buyQuantity) where id = 1 ;
        if (result < 0) {
            return "    ";
        }
        //     ...
        //     ...
    }
    

    テスト後、在庫が-1になっていることを発見し、引き続き改善します.
    
    public synchronized void buy(String productName, Integer buyQuantity) {
        //     ...
        int      = update table set surplus = (surplus - buyQuantity) where id = 1 and (surplus - buyQuantity) > 0 ;
        if (result < 0) {
            return "    ";
        }
        //     ...
        //     ...
    }
    

    テスト後、機能OK;これは確かに実現できますが、他にもいくつかの問題があります.
  • は汎用性を備える、例えばadd操作
  • である.
  • 在庫操作は一般的に操作前後の数量などを記録するので、
  • を記録することはできない.
  • その他...
  • しかし、この案によれば、案3を引き出すことができる.
    シナリオ3 CAS,update table set surplus=aa where id=xx and yy=y
    CASとはcompare/check and swap/setの意味がほとんどなく、どの単語なのかあまり悩む必要はありません
    上のsqlを修正します.
    int      = update table set surplus = newQuantity where id = 1 and surplus = oldQuantity ;
    

    このように、スレッドT 1の実行が完了すると、スレッドT 2が更新され、行数=0に影響すると、説明データが更新され、再クエリ判定が実行される.
    public void buy(String productName, Integer buyQuantity) {
        //     ...
        Product product = getByDB(productName);
        int      = update table set surplus = (surplus - buyQuantity) where id = 1 and surplus =         ;
        while (result == 0) {
            product = getByDB(productName);
            if (        > buyQuantity) {
                     = update table set surplus = (surplus - buyQuantity) where id = 1 and surplus =         ;
            } else {
                return "    ";
            }
        }
        
        //     ...
        //     ...
    }
    

    いくつかの字を再検索するのを見て、仲間たちはまた事務隔離レベルの問題を考えたはずだ.
    間違いないので、上記のコードのgetByDBメソッドでは、個別トランザクション(同じbean内の個別トランザクションが有効ではないことに注意してください)が必要であり、データベースのトランザクション独立性レベルはRCでなければなりません.
    さもないと上のコードはデッドサイクルになります.
    上の案では、CASの古典的な問題が発生する可能性があります.ABAの問題です.
    ABAとは、スレッドT 1クエリー、在庫残り100スレッドT 2クエリー、在庫残り100スレッドT 1実行subupdate t set surplus=90 where id=x and surplus=100を指す.スレッドT 3クエリ、在庫残り90スレッドT 3はadd update t set surplus=100 where id=x and surplus=90を実行する.スレッドT 2は、subupdate t set surplus=90 where id=x and surplus=100を実行する.
    ここでスレッドT 2が実行する場合、在庫の100は照会された100ではないが、この業務には影響しない.
    一般的なデザインではCASはversionで制御する.
    update t set surplus = 90 ,version = version+1 where id = x and version = oldVersion ;
    

    このようにversionを更新するたびに元の上に+1すればよい.
    CASを使うにはいくつかの点に注意し、
    1.失敗した再試行回数、制限が必要かどうか2.失敗した再試行はユーザーに対して透過的である
    シナリオ4:select xx for update
    シナリオの3種類のcasは、楽観的なロックの実現であり、select for udpateは悲観的なロックである.データを検索するとき、データをロックする.偽コードは以下の通りである.
    public void buy(String productName, Integer buyQuantity) {
        //     ...
        Product product = select * from table where name = productName for update;
        if (        > buyQuantity) {
                 = update table set surplus = (surplus - buyQuantity) where name = productName ;
        } else {
            return "    ";
        }
        
        //     ...
        //     ...
    }
    

    スレッドT 1はsubを行い、在庫残高100を問い合わせる
    スレッドT 2がsubを行う場合、スレッドT 1のトランザクションはまだコミットされておらず、スレッドT 2はスレッドT 1のトランザクションがコミットまたはロールバックするまで結果をクエリーすることができないブロックされる.
    したがって、スレッドT 2が検索するのは最新のデータに違いない.トランザクションのシリアル化に相当し、データ整合性の問題を解決する.
    select for updateについて注意すべき点は2点ある.
  • 統合エントリ:すべての在庫操作はselect for updateを統一的に使用する必要があります.そうすると、ブロックされます.別の方法が普通のselectであれば、ブロックされない
  • です.
  • ロック順序:複数のロックがある場合、ロック順序が一致しなければ、デッドロックが発生します.
  • シナリオ5:分散ロック(zookeeper,redisなど)の使用
    分散ロックを使用すると、原理はシナリオ1のsynchronizedと同じである.ただしsynchronizedのflagはjvmプロセス内でしか見られず、分散ロックのflagはグローバルで見られる.シナリオ4のselect for updateのflagもグローバルで見られる.
    分散ロックの実装案は、redisベース、zookeeperベース、データベースベースなど多くあります.前のブログでは、redisベースの簡易実装について書きました.
    redis setnxベースの簡易分散ロック
    分散ロックとsynchronizedロックを使用すると、同じ問題があります.ロックとトランザクションの順序です.これはシナリオ1で説明しました.繰り返しません.
    簡単なまとめをします.
    シナリオ1:synchronizedなどのjvm内部ロックはデータベースデータの整合性を保証するのに適しておらず、jvmにまたがるシナリオ2:汎用性がなく、操作前後のログを記録できないシナリオ3:推奨使用する.しかし、データの競争が激しいと、自動再試行回数が急激に上昇するので注意が必要である.シナリオ4:推奨使用.最も簡単なシナリオであるが、トランザクションが大きすぎると、パフォーマンスに問題があります.操作が不適切で、デッドロックの問題があります.シナリオ5:シナリオ1と似ていますが、jvmにまたがることができます.
    ————————————————著作権声明:本文はCSDNブロガーの「北京-小北」のオリジナル文章で、CC 4.0 BY-SA著作権協定に従い、転載は原文の出典リンクと本声明を添付してください.テキストリンク:https://blog.csdn.net/qq315737546/article/details/76850173