DBを使用して分散ロックを実装するためのアイデア

DBを使用して分散ロックを実装するためのアイデア

[[225288]]

概要

以前、在庫管理システムに参加したことがあります。ビジネスの複雑さのため、それをサポートするために多くのアプリケーションが開発されました。この方法では、複数のアプリケーションが同時に在庫データを変更できるようになります。たとえば、スケジュールされたタスク ドメイン xx.cron と SystemA ドメインおよび SystemB ドメインには複数の JAVA アプリケーションがあり、同じインベントリ データを同時に変更する可能性があります。調整が行われない場合、ダーティデータが表示されます。 JAVA プロセス間のスレッド調整には、DB や Redis などの外部環境を使用できます。以下では、DB を使用して分散ロックを実装する方法について説明します。

デザイン

この記事で設計した分散ロックの相互作用モードは次のとおりです。

1. ビジネスフィールドに基づいてtransaction_idを生成し、スレッドセーフな方法でロックリソースを作成する

2. transaction_idに基づいてロックを申請する

3. ロックを解除する

ロックリソースを動的に作成する

synchronized キーワードを使用する場合は、ロック オブジェクトを指定する必要があります。

同期化(obj) { }

プロセス内のスレッドは、obj に基づいて同期できます。ここでの obj はロック オブジェクトとして理解できます。スレッドが同期コード ブロックに入る場合は、まず obj オブジェクトのロックを保持する必要があります。このロックは JAVA に組み込まれたロックであり、作成プロセスはスレッドセーフです。それでは、DB の助けを借りて、ロックを作成するプロセスがスレッドセーフであることをどのように保証できるでしょうか? DB で UNIQUE KEY 機能を使用できます。重複キーが出現すると、UNIQUE KEY の一意性により例外がスローされます。 JAVA では、SQLIntegrityConstraintViolationException です。

  1. 作成する テーブル分散ロック(  
  2. id BIGINT符号なしプライマリ  KEY AUTO_INCREMENT COMMENT '自動増分主キー' ,  
  3. トランザクションID varchar (128) NOT   NULL  デフォルト  '' COMMENT 'トランザクションID'  
  4. 最終更新時刻タイムスタンプ デフォルト 現在のタイムスタンプ の上 アップデート 現在のタイムスタンプ ない  NULLコメント'***更新時間'  
  5. create_time タイムスタンプ デフォルト  '0000-00-00 00:00:00'  ない  NULLコメント「作成時間」  
  6. 個性的 キー`idx_transaction_id` (`transaction_id`)  

transaction_idはトランザクションIDです。例えば、

倉庫 + バーコード + 販売モデル

特定の倉庫内の特定の販売モデルのバーコード リソースを表す transaction_id を組み立てます。もちろん、バーコードが異なれば、transaction_id も異なります。 2 つのアプリケーションが同じ transaction_id を使用してロック リソースを作成する場合、正常に作成できるのは 1 つのアプリケーションだけです。

distribution_lock レコードが正常に挿入された場合、ロック リソースが正常に作成されたことを意味します。

DB接続プールリストの設計

書き込み操作が頻繁に行われるビジネス システムでは、通常、単一のデータベースへの書き込みの負荷を軽減し、書き込み操作のスループットを向上させるために、データベース シャーディングが実行されます。サブデータベースを使用すると、ビジネスデータは当然各データベースに分散されます。この水平にセグメント化されたマルチデータベースで DB 分散ロックを使用して、DataSouce リストをカスタマイズできます。また、transactionId に応じて対応する Connection を見つけるために getConnection(String transactionId) メソッドを公開します。

実装コードは次のとおりです。

  1. パッケージ dlock;  
  2. com.alibaba.druid.pool.DruidDataSource をインポートします。  
  3. org.springframework.stereotype.Component をインポートします。  
  4. javax.annotation.PostConstruct をインポートします。  
  5. java.io.FileInputStreamをインポートします。  
  6. java.io.IOException をインポートします。  
  7. java.sql.Connectionをインポートします  
  8. java.util.ArrayList をインポートします。  
  9. java.util.List をインポートします。  
  10. java.util.Properties をインポートします。   
  11.  
  12. @成分 
  13. パブリッククラス DataSourcePool {  
  14. プライベートリスト dlockDataSources = new ArrayList();   
  15.  
  16. @投稿コンストラクト 
  17. プライベートvoid initDataSourceList()はIOExceptionをスローします{  
  18. プロパティ properties = new Properties();  
  19. FileInputStream fis = 新しい FileInputStream( "db.properties" );  
  20. プロパティをロードします(fis);
  21.  
  22. 整数lockNum = Integer .valueOf(properties.getProperty( "DLOCK_NUM" ));  
  23. ( int i = 0; i "DLOCK_USER_" + i) ;  
  24. 文字列パスワード= properties.getProperty( "DLOCK_PASS_" + i);  
  25. 整数initSize =整数.valueOf(properties.getProperty( "DLOCK_INIT_SIZE_" + i));  
  26. 整数maxSize = Integer .valueOf(properties.getProperty( "DLOCK_MAX_SIZE_" + i));  
  27. 文字列 url = properties.getProperty( "DLOCK_URL_" + i);   
  28.  
  29. DruidDataSource dataSource = createDataSource(ユーザーパスワード、initSize、maxSize、url);  
  30. dlockDataSources.add (データソース);  
  31. }  
  32. }    
  33.  
  34. プライベート DruidDataSource createDataSource(String user 、String password Integer initSize、 Integer maxSize、String url) {  
  35. DruidDataSource データソース = 新しい DruidDataSource();  
  36. データソース.setDriverClassName( "com.mysql.jdbc.Driver" );  
  37. dataSource.setUsername(ユーザー);  
  38. dataSource.setPassword(パスワード);  
  39. データソースのURLを設定します。  
  40. dataSource.setInitialSize(initSize);  
  41. データソースの最大サイズを設定します。   
  42.  
  43. データソースを返します  
  44. }
  45.  
  46. 公共 接続getConnection(String transactionId) は例外をスローします {  
  47. dlockDataSources.size () が 0 の場合 
  48. 戻る ヌル;  
  49. }   
  50.  
  51. トランザクションIDがnullの場合トランザクションIDは""です。  
  52. 新しい RuntimeException をスローします ( "transactionId が必要です" );  
  53. }
  54. int hascode = トランザクションID.hashCode();  
  55. (コードが0の場合) {  
  56. hascode = -hascode;  
  57. }
  58. dlockDataSources.get( hascode % dlockDataSources.size ()).getConnection()を返します  
  59. }  
  60. }

まず、initDataSourceList メソッドを記述し、Spring の PostConstruct アノテーションを使用して DataSource リストを初期化します。関連する DB 構成は db.properties から読み取られます。

  1. DLOCK_NUM=2  
  2. DLOCK_USER_0 = "ユーザー1"    
  3. DLOCK_PASS_0= "パス1"    
  4. DLOCK_INIT_SIZE_0=2  
  5. DLOCK_MAX_SIZE_0=10  
  6. DLOCK_URL_0 = "jdbc:mysql://localhost:3306/test1"  
  7.   
  8.  
  9. DLOCK_USER_1 = "ユーザー1"  
  10. DLOCK_PASS_1= "パス1"    
  11. DLOCK_INIT_SIZE_1=2  
  12. DLOCK_MAX_SIZE_1=10  
  13. DLOCK_URL_1 = "jdbc:mysql://localhost:3306/test2"  

DataSource は Alibaba の DruidDataSource を使用します。

次に最も重要なのは、getConnection(String transactionId) メソッドを実装することです。実装原理は非常にシンプルです。 transactionId のハッシュコードを取得し、DataSource の長さを法として計算します。

接続プール リストを設計したら、distributed_lock テーブルにデータを挿入できます。

  1. パッケージ dlock;  
  2. org.springframework.beans.factory.annotation.Autowired をインポートします。  
  3. org.springframework.stereotype.Component をインポートします。   
  4.  
  5. java.sql.* をインポートします。   
  6.  
  7. @成分 
  8. パブリッククラスDistributedLock {  
  9. オートワイヤード 
  10. プライベート DataSourcePool dataSourcePool;    
  11.  
  12. /**  
  13. * トランザクションIDに基づいてロックリソースを作成する 
  14. */  
  15. パブリックString createLock(String transactionId) は例外をスローします{  
  16. トランザクションIDnullの場合 
  17. 新しい RuntimeException をスローします ( "transactionId が必要です" );  
  18. }
  19.  繋がり 接続= null ;  
  20. ステートメント statement = null ;  
  21. 試す {  
  22. 接続= dataSourcePool.getConnection(トランザクションID);  
  23. 接続.setAutoCommit( false );  
  24. ステートメント =接続.createStatement();  
  25. ステートメント.executeUpdate( "INSERT INTOdistributed_lock(transaction_id) VALUES ('" + transactionId + "')" );  
  26. 繋がり専念();  
  27. トランザクションIDを返します  
  28. }  
  29. キャッチ(SQLIntegrityConstraintViolationException icv){  
  30. // 説明はすでに生成されています。  
  31. if (接続!= null ) {  
  32. 接続.ロールバック( ) ;  
  33. }  
  34. トランザクションIDを返します  
  35. }  
  36. キャッチ(例外e){  
  37. if (接続!= null ) {  
  38. 接続.ロールバック( ) ;  
  39. }  
  40. eを投げる;  
  41. }  
  42. ついに {  
  43. if (ステートメント != null ) {  
  44. ステートメント.close () ;  
  45. }  
  46.  
  47. if (接続!= null ) {  
  48. 接続を閉じます  
  49. }  
  50. }  
  51. }  
  52. }

トランザクションIDに基づいてスレッドをロックする

次に、DB の更新選択機能を使用してスレッドをロックします。複数のスレッドが同じ transactionId に基づいて同時に更新の選択操作を実行する場合、成功できるのは 1 つのスレッドのみであり、更新の選択を正常に実行したスレッドがコミット操作を使用するまで、他のスレッドはブロックされます。そうして初めて、ブロックされたスレッドの 1 つが動作を開始できるようになります。上記の DistributedLock クラスにロック メソッドを作成します。

  1. パブリックブールロック(String transactionId)は例外をスローします{  
  2. 繋がり 接続= null ;  
  3. 準備されたステートメント 準備されたステートメント = null ;  
  4. 結果セット resultSet = null ;  
  5. 試す {  
  6. 接続= dataSourcePool.getConnection(トランザクションID);  
  7. preparedStatement = connection.prepareStatement ( "SELECT * FROM distribution_lock WHERE transaction_id = ? FOR UPDATE " );  
  8. 準備されたステートメント。setString(1、トランザクションID);  
  9. 結果セット = preparedStatement.executeQuery();  
  10. if (! resultSet.next ()) {  
  11. 接続.ロールバック( ) ;  
  12. 戻る 間違い;  
  13. }  
  14. 戻る 真実;  
  15. } キャッチ (例外 e) {  
  16. if (接続!= null ) {  
  17. 接続.ロールバック( ) ;  
  18. }  
  19. eを投げる;  
  20. }  
  21. ついに {  
  22. 準備されたステートメントがnull場合 
  23. 準備されたステートメントを閉じます() ;  
  24. }   
  25.  
  26. 結果セットがnull場合 
  27. 結果セットを閉じます() ;  
  28. }
  29.  
  30. if (接続!= null ) {  
  31. 接続を閉じます  
  32. }  
  33. }  
  34. }

ロック解除操作を実装する

スレッドがタスクを完了したら、以前にロックされていたスレッドが引き続き動作できるように、手動でロックを解除する必要があります。上記の実装では、実際には、その時点で更新に正常に選択されたスレッドに対応する接続​​を取得し、コミット操作を実行するだけです。

それでどうやってそれを手に入れるのですか? ThreadLocal を使用できます。まず、DistributedLockクラスで定義します

  1. プライベート ThreadLocal threadLocalConn = new ThreadLocal();

lock メソッドが呼び出されるたびに、Connection は ThreadLocal に配置されます。ロック方法を変更します。

  1. パブリックブールロック(String transactionId)は例外をスローします{  
  2. 繋がり 接続= null ;  
  3. 準備されたステートメント 準備されたステートメント = null ;  
  4. 結果セット resultSet = null ;  
  5. 試す {  
  6. 接続= dataSourcePool.getConnection(トランザクションID);  
  7. threadLocalConn.set (接続) ;  
  8. preparedStatement = connection.prepareStatement ( "SELECT * FROM distribution_lock WHERE transaction_id = ? FOR UPDATE " );  
  9. 準備されたステートメント。setString(1、トランザクションID);  
  10. 結果セット = preparedStatement.executeQuery();  
  11. if (! resultSet.next ()) {  
  12. 接続.ロールバック( ) ;  
  13. スレッドLocalConnを削除します。  
  14. 戻る 間違い;  
  15. }  
  16. 戻る 真実;  
  17. } キャッチ (例外 e) {  
  18. if (接続!= null ) {  
  19. 接続.ロールバック( ) ;  
  20. スレッドLocalConnを削除します。  
  21. }  
  22. eを投げる;  
  23. }  
  24. ついに {  
  25. 準備されたステートメントがnull場合 
  26. 準備されたステートメントを閉じます() ;  
  27. }   
  28. 結果セットがnull場合 
  29. 結果セットを閉じます() ;  
  30. }
  31.  
  32. if (接続!= null ) {  
  33. 接続を閉じます  
  34. }  
  35. }  
  36. }

このように、Connection を取得した後、ThreadLocal に設定します。 lock メソッドで例外が発生した場合は、ThreadLocal から削除します。

これらの手順で、ロック解除操作を実装できます。 DistributedLock に unlock メソッドを追加します。

  1. パブリックvoid unlock()は例外をスローします{  
  2. 繋がり 接続= null ;  
  3. 試す {  
  4. 接続= threadLocalConn.get();  
  5. if (!接続.isClosed()) {  
  6. 繋がり専念();  
  7. 接続を閉じます  
  8. スレッドLocalConnを削除します。  
  9. }  
  10. } キャッチ (例外 e) {  
  11. if (接続!= null ) {  
  12. 接続.ロールバック( ) ;  
  13. 接続を閉じます  
  14. }  
  15. スレッドLocalConnを削除します。  
  16. eを投げる;  
  17. }  
  18. }

欠点

結局のところ、分散ロックを実装するために DB が使用されるため、DB には依然として一定の負荷がかかります。当時、配信に DB を使用することを検討した重要な理由は、私たちのアプリケーションがトラフィック量の少ないバックエンド アプリケーションであったことです。代わりに、在庫データの正確性を確保することが重要でした。ショッピングカートを追加して在庫を占有するなどのフロントエンド在庫システムでは、分散ロックを実装するために DB を使用しないことが最善です。

さらに考える

データの複数のコピーをロックするにはどうすればよいですか?たとえば、特定の在庫操作では、実在庫と仮想在庫の両方を変更する必要があります。実在庫と仮想在庫の両方をロックします。実のところ、それほど難しいことではありません。 lock メソッドを参照して、multiLock メソッドを記述し、複数の transactionId 入力パラメータを指定して、for ループで処理します。後で時間ができたらこれを補います。

<<:  クラウド戦略: ハイブリッドクラウドとマルチクラウドは異なる

>>:  従来のデータセンター市場の終焉?クラウドデータトラフィックは5年で大幅に増加する

推薦する

Beida Jade Bird 検索エンジン広告戦略のケーススタディ

北大玉鳥はIT教育分野で大きな影響力を持っており、その検索エンジン広告戦略も典型的です。新たな競争力...

最適化されたウェブマスターの垂直思考をお持ちですか?

ウェブサイトの最適化は、もはや神秘的なものではありません。多くのウェブマスターは、大量の外部リンクを...

SaaS をサービスの観点から見ると何がわかるでしょうか?

実際、サービス経済の到来により、労働力の大部分が農業や製造業からサービス産業に大規模に移行するのと同...

terrahost: ノルウェー VPS、月額 0.92 ユーロ、256M メモリ/1 コア/30g NVMe/1T トラフィック

2006 年にノルウェーのオスロで設立された Terrahost は、独自のデータ センターを所有お...

LLM大規模モデル最適化技術とエッジコンピューティング

LLM の最適化には通常、3 つの側面が含まれます。特定のタスクに合わせて LLM を微調整すること...

プロモーションのヒント集:主流ネットワークプロモーションチャネル16個を分析!

オンラインプロモーションには様々なチャネルがあります。今回は主流のオンラインプロモーションチャネル1...

Pinduoduoには価格競争はない

Pinduoduoの成功は価格の安さによるものではなく、無視されていた消費者市場を発掘し、商品の製造...

SEO 必読 - キーワードを素早くランク付けするための 5 つの要素

ウェブサイトのキーワードランキングについて言えば、正直言って、SEOにとっては本当に頭痛の種です。キ...

Kステーション100日:百度は私により良い道を歩ませた

Baidu の 6.28 地震により、私の通常のウェブサイトの 1 つが破壊されました。私の独創性が...

Kubernetes クラスタ管理者戦略: ポッドのスケジュールを理解する

このガイドでは、ポッド スケジューリングの技術を習得するために必要な知識とスキルを身に付けることがで...

新人Webサイト管理者が初期段階で直面する問題点を共有(第1部)

正直に言うと、私たちは何かをするとき、特にインターネットビジネスを始めるときは、他の人からの指導を受...

ロングテールキーワードの使い方を教える家具ネットワーク外部リンク戦略分析

ウェブサイトのロングテール キーワードの最適化は、ウェブサイト全体の最適化にとって非常に重要です。多...

Google 再審査リクエストを送信する

Google の検索結果に自分のウェブサイトが表示されなくなったり、以前と比べてウェブサイトのランキ...

中国の伝統文化をインターネットマーケティング理論に応用する

インターネットマーケターは時代の専門職であり、科学技術の発展によるマーケティングモデルの変革の必然的...