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

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

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

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つの新しいカスタムチップを発表

推薦する

クラウドコンピューティング契約に署名する際に注意すべきいくつかの点

Forrester Research の調査によると、クラウド コンピューティング市場は年間 22%...

hmbcloud: 新年の特別オファー、日本 CN2 VPS、100M 帯域幅が 12% オフ

Half Moon Bay (hmbcloud) は、春節期間中に日本 CN2 VPS の 12% ...

Baiduはあなたが知らない代替外部リンクの秘密を知っている

百度は外部リンクを張るには規模が大きすぎることを皆が知っています。質問に答えて、お互いに外部リンクを...

4月の海外ドメインホスティング会社トップ10:HostGatorが第4位、Yahooが第7位

IDC Review Network (idcps.com) は 5 月 4 日に次のように報告しま...

2022 年のエッジ コンピューティングの 5 つのトレンド

エッジ コンピューティングのトレンドは、エッジの導入があらゆる場所で行われるようになったため、ビジネ...

簡単な分析: オフシーズンのオンラインマーケティングプロモーション戦略

マーケティングを行う際は、過去数年間のデータをもとに翌年の市場動向を常に予測し、四半期ごと、月ごとな...

エッジ コンピューティングとクラウド コンピューティングを 1 つの記事で理解しましょう。

エッジ コンピューティングは時間に敏感なデータの処理に重点を置いていますが、クラウド コンピューティ...

Dockerビルドの最適化分析についてお話しましょう

この記事では、Docker イメージの開発とビルド プロセスを最適化するのに役立つ、見落とされがちな...

5Gとエッジコンピューティング、どのように選択するのでしょうか?

5G やエッジ コンピューティングなどの新しいトレンドが出現するにつれ、テクノロジーの専門家は、必要...

Tech Neo 8月号: 言語の選択

51CTO.com+プラットフォームは、オリジナルの技術コンテンツの選択と絶妙なレイアウトを通じて、...

Baidu入札クリエイティブのライティングスキルについての簡単な議論

多くの人が創造的な文章を書くために頭を悩ませます。実際、あなたの創造性が訪問者を引き付けるかどうかの...

モバイルソーシャルネットワーキングの進化:WeChatが王座に就き、Momoが誕生しました!

「ソーシャルネットワーキングと電子商取引のどちらかをマスターできれば、世界を支配できる。」インターネ...

ウェブマスターネットワークからの毎日のレポート:Sina Weiboの商業化は中小規模の電子商取引企業の運命を変える

1. 偽の「ビッグマウスモンキー」をネットで販売し、80万元の利益を得たとして、ネットショップのパー...

Baiduのホームページ上の「優良サイト」からユーザーエクスペリエンスを分析

Baidu は最近大きなアップデートがなく、いくつかのローカルなマイナーアップデートがあるだけです。...

Skywalking 分散リンク トレーシングの概要

今日は、分散リンクトラッキングソフトウェアを紹介します。リンクトラッキングを導入する必要があるのはな...