同時実行シナリオにおけるべき等性の問題 - 分散ロックの詳細な説明

同時実行シナリオにおけるべき等性の問題 - 分散ロックの詳細な説明

前書き: この記事で説明するべき等性の問題はすべて、並行シナリオにおけるべき等性の問題です。つまり、システムは元々べき等設計でしたが、同時実行シナリオでは失敗しました。

1. 要約

この記事では、DingTalk の実在人物認証シナリオにおけるデータ重複の問題から始め、問題の原因を分析し、同時実行性がべき等性の障害を引き起こすことを突き止め、べき等性の概念を紹介します。

並行シナリオにおけるべき等性の問題に対応して、べき等性を実現するための実行可能な方法論が提案されています。アドレス帳に人を追加するというビジネス シナリオと組み合わせて、データベースの冪等性の問題を簡単に分析し、分散ロックを使用して冪等性を実現する方法について詳細に説明します。

この論文では、単一点障害、ネットワーク タイムアウト、他者のロックの不正な解放、ロックの早期解放、分散ロックの単一点障害など、分散シナリオにおけるロックに存在する問題を分析し、対応するソリューションを提案し、対応するソリューションの具体的な実装を紹介します。

2番目の質問

DingTalkの実名認証事業では、データ重複の問題がある。

1 問題現象

通常の状況では、データベース内に成功した実在の人物の認証レコードは 1 つだけ存在する必要がありますが、実際には特定のユーザーには複数のレコードが存在する場合があります。

2 原因

並行性は非べき等性につながります。

まず、べき等性の概念を確認しましょう。

べき等性は数学とコンピュータ サイエンスの概念であり、抽象代数でよく見られます。

プログラミングにおいて、べき等な操作とは、何度実行しても、その効果が 1 回実行した場合と同じになる操作のことです。

--百度百科事典より

実在人物による認証はビジネスにおいてべき等的な設計になっており、その一般的なプロセスは次のとおりです。

1) ユーザーが本人認証を選択すると、サーバー上でレコードが初期化されます。

2) ユーザーは、DingTalk モバイル端末の指示に従って顔照合を完了します。

3) 比較が完了したら、サーバーにアクセスしてデータベースの状態を変更します。

ステップ 3 では、データベースのステータスを変更する前に、データベースが初期化されているかどうか、実際の人物が認証されているかどうか、および Zhike が認証成功を返して冪等性を保証するかどうかを確認します。リクエストが初めてサーバーにアクセスしてデータベースの状態を変更しようとする場合にのみ、べき等性判定条件が満たされ、データベースの状態が変更されます。残りのリクエストは、データベースの状態に影響を与えることなく直接返されます。サーバーへの複数のアクセスを要求した結果は、サーバーへの最初のアクセスを要求した結果と同じになります。したがって、実在の人物による認証が成功しているという前提では、データベースには成功した認証レコードが 1 つだけ存在するはずです。

しかし、実際には、同じリクエストによってデータベースの状態が複数回変更され、システムが期待どおりに冪等性を達成していないことがわかりました。その理由は、同時アクセス要求により、最初の要求がサーバーの状態の変更を完了する前に、他の同時要求と最初の要求が冪等性判定を通過し、データベースの状態を複数回変更してしまうためです。

同時実行により、元のべき等設計が失敗します。

並行性は非べき等性につながります。

3つの解決策

同時実行シナリオで冪等性の問題を解決する鍵は、一意の制約を見つけ、一意性チェックを実行し、同じデータを 1 回保存し、同じ要求操作を 1 回実行することです。

サーバーへのアクセス要求により、次のやり取りが発生する可能性があります。

  1. データベースのステータスの変更などのデータ ソースを操作します。
  2. ダウンストリーム サービスの呼び出しやメッセージの送信など、他のビジネス システムと対話します。

リクエストには、1 つのインタラクションのみ、または複数のインタラクションを含めることができます。たとえば、リクエストによってデータベースのステータスを 1 回だけ変更したり、データベースのステータスが変更された後にデータベースのステータスが正常に変更されたことを示すメッセージを送信したりできます。

したがって、次のような結論を導き出すことができます。並行シナリオでは、システムが依存するコンポーネントがべき等であれば、システムは自然にべき等です。

データベースを例にとると、データに対するリクエストの影響が新しいデータの追加である場合、一意のインデックスはべき等性の問題の解決策になります。データベースは、同じデータが繰り返し保存されないように一意性チェックを実行するのに役立ちます。

DingTalk アドレス帳に人を追加すると、データベースの一意のインデックスを使用してべき等性の問題が解決されます。 DingTalk アドレス帳に人を追加する場合を例にとると、データベースにデータを書き込む前に、まずそのデータがデータベース内に既に存在するかどうかを判断します。そうでない場合、追加要求によって最終的にデータベースの従業員テーブルにデータが挿入されます。アドレス帳に人を追加するための同一の同時要求が多数発生すると、システムのべき等設計が失敗する可能性があります。人物を追加するリクエストでは、(組織 ID、従業員番号) によってリクエストを一意にマークできます。データベースには、(組織ID、従業員番号)の一意のインデックスも存在します。したがって、複数の同一のユーザー追加要求によってデータベースのステータスが 1 回だけ変更され、レコードが追加されることを保証できます。

依存コンポーネントが自然にべき等であれば、問題は単純になりますが、実際の状況はより複雑になることがよくあります。並行シナリオでは、システムが依存するコンポーネントがべき等性を持たない場合、べき等性を実現するために追加の手段を使用する必要があります。

一般的に使用される方法は、分散ロックを使用することです。分散ロックを実装する方法は多数ありますが、最も一般的に使用されるのはキャッシュされた分散ロックです。

4つの分散ロック

「Java 分散ロックとは何ですか?」には、次のような一節があります。

コンピュータ サイエンスでは、ロックは、異なるスレッドが同じリソースを操作するのを防ぐためのマルチスレッド環境のメカニズムです。ロックを使用すると、リソースは特定のスレッドによるアクセスに対して「ロック」され、リソースが解放された後にのみ別のスレッドからアクセスできるようになります。ロックにはいくつかの利点があります。2 つのスレッドが同じ作業を行うことを防止し、2 つのスレッドが同じリソースを同時に使用しようとしたときにエラーやデータの破損を防ぎます。

Java の分散ロックは、同じマシン上で実行されている複数のスレッドだけでなく、分散システム内の異なるマシン上のクライアントで実行されているスレッドでも機能するロックです。これらの個別のマシン上のスレッドは、他のマシンによってロックされているリソースにいずれのスレッドもアクセスしないように、通信して調整する必要があります。

これらの段落では、ロックの本質は共有リソースへの相互排他的アクセスであり、分散ロックは分散システムにおける共有リソースへの相互排他的アクセスの問題を解決することが説明されています。

java.util.concurrent.locks パッケージは、フェア ロック/アンフェア ロック、ブロッキング ロック/非ブロッキング ロック、読み取り/書き込みロック、再入可能ロックなど、豊富なロック実装を提供します。

分散ロックをどのように実装するのでしょうか?

解決策1

分散システムには、2 つの一般的な問題があります。

1) 単一点障害問題、つまり、ロックを保持しているアプリケーションで単一点障害が発生すると、ロックは長時間無効になります。

2) ネットワークタイムアウトの問題。つまり、クライアントネットワークがタイムアウトしてもロックが実際に成功した場合、再度正しくロックすることができない。

ロックを取得します。

問題 1 を解決するための簡単な解決策は、有効期限 (リース時間) を導入することです。保持されるロックには時間制限があります。アプリケーションで単一点障害が発生した場合、そのアプリケーションが保持しているロックは自動的に解除されます。

問題 2 を解決するための簡単な解決策は、再入可能性をサポートすることです。ロックを取得するクライアントごとに一意の識別子 (通常は UUID) を構成します。ロックが正常にロックされると、ロックにはクライアントの識別子が保持されます。ロックが実際に成功し、クライアントがタイムアウト後に再試行すると、ロックがすでにクライアントによって保持されていると判断し、成功を返します。

要約すると、リースベースの分散ロックソリューションを提案しました。パフォーマンス上の理由から、キャッシュはロックの記憶媒体として使用され、共有リソースへの相互排他アクセスの問題を解決するために MVCC (マルチバージョン同時実行制御) メカニズムが使用されます。具体的な実装は付録のコードで確認できます。

分散ロックの一般的な使用法は次のとおりです。

  • 分散ロックファクトリーを初期化する
  • ファクトリを使用して分散ロックインスタンスを生成する
  • この分散インスタンスを使用して操作をロックおよびロック解除します
  1. @テスト
  2. パブリックボイドテストトライロック(){
  3.  
  4. // ファクトリーを初期化する
  5. MdbDistributeLockFactory を新規 MdbDistributeLockFactory() に追加します。
  6. mdbDistributeLockFactory.setNamespace(603);
  7. mdbDistributeLockFactory.setMtairManager(新しい MultiClusterTairManager());
  8.  
  9. //ロックを取得する
  10. DistributeLock ロック = mdbDistributeLockFactory.getLock( "TestLock" );
  11.  
  12. //ロックとロック解除の操作
  13. ブール型ロック = lock.tryLock();
  14. (!ロックされている場合){
  15. 戻る;
  16. }
  17. 試す {
  18. //何かをする
  19. ついに
  20. ロックを解除します。
  21. }
  22. }

このソリューションはシンプルで使いやすいですが、問題点も明らかです。例えば、ロックを解除する際には、キャッシュ内のキーが単純に無効化されるため、誤って他人が保持しているロックを解除してしまうという問題があります。幸いなことに、ロックのリース期間が十分に長く設定されている限り、この問題が発生する可能性は十分に低くなります。

この問題を説明するために、Martin Kleppmann 氏の記事「分散ロックの実行方法」の図を使用します。

ロックを保持しているクライアント 1 がロックを解放する前にロックの有効期限が切れ、クライアント 2 がロックを取得する状況を想像してください。このとき、ロックはクライアント 2 によって保持されますが、クライアント 1 が誤ってロックを解除する可能性があります。より良い解決策は、ロックごとに ID を設定することです。ロックを解除するときは、1)まずロックが自分のものかどうかを確認し、2)自分のものであればロックを解除します。実装上の制限により、手順 1 と 2 はアトミック操作ではありません。手順 1 と 2 の間でロックの有効期限が切れ、他のクライアントによってロックが取得された場合、他のクライアントのロックが誤って解除されます。

解決策2

Redis の Lua スクリプトを使用すると、他のユーザーが保持しているロックを誤って解放してしまう問題を完全に解決できます。 「Redis を使用した分散ロック」の記事の「単一インスタンスによる正しい実装」セクションでは、分散ロックの実装方法という必要な答えが得られます。

ロックを取得したい場合は、次のメソッドを実行します。

  1. リソース名をランダム値に設定NX PX 30000

ロックを解除したいときは、次のLuaスクリプトを実行します。

  1. redis.call( "get" ,KEYS[1]) == ARGV[1]の場合 
  2. redis.call( "del" ,KEYS[1])を返す
  3. それ以外 
  4. 0を返す
  5. 終わり 

オプション3

ソリューション 1 とソリューション 2 の議論の中で、ロックの自動解除という 1 つの問題が繰り返し言及されました。

これは諸刃の剣です。

1) 一方で、ロックを保持しているクライアントの単一障害点の問題を解決します。

2) 一方、ロックが早期に解除されると、ロックは誤って保持されます。

この時点で、Watch Dog の自動更新メカニズムを導入できます。 Redisson がこれをどのように実装するかについては、以下を参照してください。

ロックが正常に取得されると、Redisson は renewExpiration() メソッドを呼び出して Watch Dog スレッドを開始し、ロックを自動的に更新します。 1/3 ごとに 1 回更新します。成功した場合は、次の更新に進みます。失敗した場合は、更新操作をキャンセルします。

Redissonがどのようにリニューアルされたのか見てみましょう。 renewExpiration() メソッドの 17 行目 renewExpirationAsync() は、ロックの更新を実行するための重要な操作です。メソッドに入ると、Redisson も Lua スクリプトを使用してロックを更新していることがわかります。1) ロックが存在するかどうかを判断します。 2) 存在する場合は有効期限をリセットします。

  1. プライベートvoid renewExpiration() {
  2. 有効期限エントリ ee = EXPIRATION_RENEWAL_MAP.get(getEntryName());
  3. ee == nullの場合{
  4. 戻る;
  5. }
  6.  
  7. タイムアウトタスク = commandExecutor.getConnectionManager().newTimeout(timeout -> {
  8. 有効期限エントリ ent = EXPIRATION_RENEWAL_MAP.get(getEntryName());
  9. ent == nullの場合{
  10. 戻る;
  11. }
  12. ロングスレッドId = ent.getFirstThreadId();
  13. スレッドIDnullの場合
  14. 戻る;
  15. }
  16.  
  17. RFuture<ブール値> future = renewExpirationAsync(threadId);
  18. future.onComplete((res, e) -> {
  19. e != null の場合
  20. log.error( "ロック " + getRawName() + " の有効期限を更新できません" , e);
  21. EXPIRATION_RENEWAL_MAP.remove(getEntryName());
  22. 戻る;
  23. }
  24.  
  25. もし(res){
  26. // スケジュールを再設定する
  27. 有効期限を更新します。
  28. }それ以外{
  29. 有効期限更新をキャンセルします( null );
  30. }
  31. });
  32. }, internalLockLeaseTime / 3, TimeUnit.MILLISECONDS);
  33.  
  34. タスクのタイムアウトを設定します。
  35. }
  1. 保護された RFuture<Boolean> renewExpirationAsync(long threadId) {
  2. evalWriteAsync(getRawName(), LongCodec.INSTANCE, RedisCommands.EVAL_BOOLEAN,を返します
  3. 「(redis.call('hexists', KEYS[1], ARGV[2]) == 1) の場合」 +
  4. "redis.call('pexpire', KEYS[1], ARGV[1]); " +
  5. "1を返す; " +
  6. 「終了;」 +
  7. "0を返します。"
  8. コレクション.singletonList(getRawName())、
  9. 内部LockLeaseTime、getLockName(threadId));
  10. }

オプション4

Redisson の自動更新メカニズムにより、ロックの自動解除について心配する必要がなくなりました。しかし、議論のこの時点では、まだ問題に直面しています。分散ロック自体は分散アプリケーションではないのです。 Redis サーバーが正常に動作しなくなると、分散ロック全体がサービスを提供できなくなります。

さらに、「Redis を使用した分散ロック」の記事で説明されている Redlock アルゴリズムとその実装を見てみましょう。

Redlock アルゴリズムは万能薬ではなく、その長所と短所については多くの議論があります。

分散ロックを実行する方法:

https://martin.kleppmann.com/2016/02/08/how-to-do-distributed-locking.html

Redlock は安全ですか?

http://antirez.com/news/101

Martin Kleppmann と Antirez が Redlock について議論:

https://news.ycombinator.com/item

参考文献

Java 分散ロックとは何ですか?

https://redisson.org/glossary/java-distributed-lock.html

分散ロックとシンクロナイザー:

https://github.com/redisson/redisson/wiki/8.-distributed-locks-and-synchronizers

Redis による分散ロック:

https://redis.io/topics/distlock?spm=ata.21736010.0.0.31f77e3aFs96rz

付録

分散ロック

  1. パブリッククラスMdbDistributeLockはDistributeLockを実装します{
  2.  
  3. /**
  4. * 名前空間をロックする
  5. */
  6. プライベートな最終int名前空間;
  7.  
  8. /**
  9. * ロックに対応するキャッシュキー 
  10. */
  11. プライベート最終文字列 lockName;
  12.  
  13. /**
  14. * ロックの一意の識別子。putが成功したがタイムアウトが返された場合に再入可能性を保証します。
  15. */
  16. プライベート最終文字列 lockId;
  17.  
  18. /**
  19. * ロックを保持するかどうか。: はい
  20. */
  21. プライベートブール値がロックされています。
  22.  
  23. /**
  24. * キャッシュインスタンス
  25. */
  26. プライベートファイナルTairManager tairManager;
  27.  
  28. パブリックMdbDistributeLock(TairManager tairManager、 int名前空間、String lockCacheKey) {
  29.  
  30. this.tairManager = tairManager;
  31. this.namespace = 名前空間;
  32. this.lockName = lockCacheKey;
  33. this.lockId = UUID.randomUUID().toString();
  34. }
  35.  
  36. @オーバーライド
  37. パブリックブール型tryLock() {
  38.  
  39. 試す {
  40. //ロックステータスを取得する
  41. Result<DataEntry> getResult = null ;
  42. 結果コード getResultCode = null ;
  43. ( int cnt = 0; cnt < DEFAULT_RETRY_TIMES; cnt++) {
  44. getResult = tairManager.get(名前空間、ロック名);
  45. getResultCode = getResult == null ? null : getResult.getRc();
  46. 再試行が必要ない場合(getResultCode) {
  47. 壊す;
  48. }
  49. }
  50.  
  51. // 再入力、ロックはすでに保持されている、成功を返す
  52. if (ResultCode.SUCCESS.equals(getResultCode)
  53. && getResult.getValue() != null && lockId.equals(getResult.getValue().getValue())) {
  54. ロック = true ;
  55. 戻る 真実;
  56. }
  57.  
  58. //ロックを取得できないため、失敗を返します
  59. if (!ResultCode.DATANOTEXSITS.equals(getResultCode)) {
  60. log.error( "tryLock 失敗コード={} ロック={} トレースId={}" 、getResultCode、this、EagleEye.getTraceId());
  61. 戻る 間違い;
  62. }
  63.  
  64. //ロックを取得しようとする
  65. 結果コード putResultCode = null ;
  66. ( int cnt = 0; cnt < DEFAULT_RETRY_TIMES; cnt++) {
  67. putResultCode = tairManager.put(名前空間、ロック名、ロックID、MDB_CACHE_VERSION、
  68. デフォルト値:
  69. 再試行が必要ない場合(putResultCode) {
  70. 壊す;
  71. }
  72. }
  73. if (!ResultCode.SUCCESS.equals(putResultCode)) {
  74. log.error( "tryLock 失敗コード={} ロック={} トレースId={}" 、getResultCode、this、EagleEye.getTraceId());
  75. 戻る 間違い;
  76. }
  77. ロック = true ;
  78. 戻る 真実;
  79.  
  80. } キャッチ (例外 e) {
  81. log.error( "DistributedLock.tryLock がロックに失敗しました = {}" 、 this、 e);
  82. }
  83. 戻る 間違い;
  84. }
  85.  
  86. @オーバーライド
  87. パブリックボイドロック解除() {
  88.  
  89. (!ロックされている場合){
  90. 戻る;
  91. }
  92. 結果コード resultCode = tairManager.invalid(namespace, lockName);
  93. 結果コードが成功の場合
  94. log.error( "DistributedLock.unlock 失敗 ロック={} resultCode={} traceId={}" , this, resultCode,
  95. EagleEye.getTraceId());
  96. }
  97. ロック = false ;
  98. }
  99.  
  100. /**
  101. * 再試行が必要かどうかを判断する
  102. *
  103. * @param resultCode キャッシュされた戻りコード
  104. * @戻る  true : 再試行しない
  105. */
  106. プライベートブール値 noNeedRetry(ResultCode 結果コード) {
  107. 結果コードがnull ではないこと、および !ResultCode.CONNERROR.equals(結果コード) および !ResultCode.TIMEOUT.equals(を返す
  108. 結果コード) && !結果コード.UNKNOW.equals(結果コード);
  109. }
  110.  
  111. }

分散ロックファクトリー

  1. パブリッククラスMdbDistributeLockFactoryはDistributeLockFactoryを実装します{
  2.  
  3. /**
  4. * キャッシュ名前空間
  5. */
  6. @セッター
  7. プライベートint名前空間;
  8.  
  9. @セッター
  10. プライベート MultiClusterTairManager mtairManager;
  11.  
  12. @オーバーライド
  13. パブリックDistributeLock getLock(String lockName) {
  14. 新しい MdbDistributeLock(mtairManager、名前空間、lockName)を返します
  15. }
  16. }

<<:  マルチテナントクラウドプラットフォームの導入方法を学ぶ

>>:  アマゾンのクラウドコンピューティング部門は、インテル、NVIDIAに対抗するため2つの新しいカスタムチップを発表

推薦する

Doubanは今年、収益が8000万とほぼ黒字、平均日次PVが1億6000万になると予想している。

8月17日午前、豆瓣(douban.com)が本日公開した一部の運営データによると、同社の月間独立ユ...

忘れられない物語を伝えるにはどうすればいいでしょうか?

コンテンツ マーケティングの本質は自慢することです。たとえひざまずかなければならないとしても、自分が...

検索: 「FG戦争」は始まるのか?

国内のインターネットでは、有名な「3Q戦争」、「3B戦争」など、多くの大きな戦争がありましたが、その...

コーヒーショップに偽装したスタートアップインキュベーター:大物株主多数

3W Cafeではインターネットサロンを開催し、KPCBの投資マネージャーらが同社の事業紹介を行った...

2018 年の中国企業におけるクラウド コンピューティング アプリケーションの現状とニーズ

近年、デジタル変革の波を受けて、わが国のクラウドコンピューティングの発展は、爆発的な需要の時代を迎え...

spinservers: ダラスの格安サーバー、19 ドル / E5-2670 / 8g メモリ / 500g ハードドライブ / 10T トラフィック

spinserversは、2009年に設立された新しいブランドですが、実は25年間運営している、弊社...

入札コンテンツと最適化コンテンツを区別し、ウェブサイトのコンテンツを合理的に編集する

Baidu が最適化業界の是正を強化するにつれて、多くのウェブサイトが最適化をあきらめて入札に目を向...

ウクライナのライファイゼン銀行のCTOが戦時中のクラウド移行について語る

オーストリアのライファイゼン銀行インターナショナルの子会社であるライファイゼン銀行ウクライナは、ロシ...

ICBCは文書番号86を使用してAlipayに強く反応しました。多くの銀行が支払いインターフェースの整理を検討しています。

ICBC迅速な支払いは、アリペイと商業銀行システムとの全面的な対立を引き起こすドミノ倒しとなった。 ...

茅埔の興亡:その発展の方向を決める14のキーワード

Mop.com の創設者は湖南電信の社員だと言われており、サーバーや帯域幅のリソースを利用できました...

Yidiantianxia は Amazon クラウド テクノロジーを使用してクラウド広告およびマーケティング テクノロジーを革新します

アマゾンウェブサービスは2021年4月7日、著名なモバイルパフォーマンスマーケティングサービスプロバ...

hostodo: 年間 25 ドル、米国 VPS (データセンター 3 か所)、2G メモリ/2 コア/20GNVMe/5T トラフィック

Hostodo (~) は現在、ラスベガス、スポケーン、マイアミのデータセンターで、少なくとも 5T...

任正非のインタビュー全文2万字

1. 記者:まず、私は生放送の記者です。あなたは軍人なので時間を厳しく管理しています。私も時間を厳し...

個人サイト運営成功の秘訣はユーザーの「満足度」向上

インターネットの継続的な発展に伴い、インターネットはもはや少数の人々が競争する産業ではなくなりました...