Javaインタビューアサルトシリーズ(7):分散システムのべき等とシーケンスおよび分散ロック

分散システムと分散ロックのべき等とシーケンス

分散サービスインターフェイスのべき等を設計する方法

べき等とは

分散システムの特定のインターフェースのべき等性を確保するにはどうすればよいですか?この問題は、実際には、分散システムを実行するときに考慮する必要がある本番環境の技術的な問題です。どういう意味ですか?

ご覧のとおり、インターフェースを提供するサービスがある場合、そのサービスは5台のマシンにデプロイされ、次のインターフェースは支払いインターフェースです。次に、他のユーザーがフロントエンドで操作しているとき、理由がわかりません。つまり、注文が誤って2つの支払い要求を開始し、これら2つの要求が、サービスによってデプロイされた異なるマシンに分散されます。 1つの注文。2つの控除?恥ずかしい。

または、注文システムが支払いシステムを呼び出して支払いを行い、ネットワークがタイムアウトしたために偶発的な結果が発生した後、注文システムが前に見た再試行メカニズムを実行し、クリックして再試行しました。システムは支払いを受け取りました。2回要求されましたが、負荷分散アルゴリズムが異なるマシンに適用されたため、恥ずかしかったです。

したがって、これを知っておく必要があります。そうしないと、構築する分散システムが簡単に穴を埋めてしまう可能性があります。

ネットワークの問題は非常に一般的です。100件のリクエストはすべて問題ありません。10,000回、タイムアウト後に1回再試行される可能性があります。100,000回、10回、100万回、100回、100回のリクエストが繰り返された場合、処理しなかったため、結果として注文は2回差し引かれ、100件の注文は誤って差し引かれ、100人のユーザーが毎日不平を言い、3000人のユーザーが月に不平を言いました

これは本番環境で以前に発生したことがあります。データベースにデータを書き込むことです。リクエストを繰り返すと、データが間違っていることがよくあります。データの重複が発生すると、問題が発生します。

01_分散システムインターフェースのべき等

スタンドアロン環境の場合は、1つのマップまたはセットのみを維持する必要があり、そのたびに注文IDが支払われたかどうかが判断されます。

これは技術的な問題ではありません。普遍的な方法はありません。これは、ビジネスと組み合わせてべき等性を確保する方法の経験に基づいています。

いわゆるべき等とは、インターフェースが同じ要求を複数回開始することを意味します。インターフェースは、結果が正確であることを確認する必要があります。たとえば、これ以上の控除、データの挿入、統計値の追加はできません。 1で。これはべき等であり、学期はお伝えしません。

べき等性を保証する

実際、べき等性を確保するための3つの主要なポイントがあります。

  • リクエストごとに一意の識別子が必要です。例:注文支払いリクエストには注文IDを含める必要があります。注文IDは最大で1回しか支払うことができません。
  • 各リクエストが処理された後、リクエストが処理されたことを示すレコードが必要です。たとえば、一般的な解決策は、支払い前にこの注文の支払いフローを記録するなど、mysqlにステータスを記録することです。集めました。
  • リクエストを受信するたびに、論理処理を実行して、以前に処理されたかどうかを判断する必要があります。たとえば、注文が支払われた場合、すでに支払いフローがあります。このリクエストが繰り返し送信される場合、支払いフローは次のようになります。最初に挿入され、orderIdはすでに存在し、一意キー制約が有効になり、エラーメッセージを挿入できません。そうすれば、もう差し引く必要はありません。
  • 上記はすべての人の例にすぎません。実際の操作プロセスでは、redisとorderIdを一意のキーとして使用するなど、独自のビジネスを組み合わせる必要があります。この支払いストリームを正常に挿入することによってのみ、実際の支払い控除を実行できます。

要件は注文の支払いです。一意のキー、一意のキーを作成するには、支払いストリームorder_idを挿入する必要があります

したがって、注文の支払いを行う前に、最初に支払いストリームを挿入すると、order_idはすでにに含まれています。

ロゴをredisに書き込んで、order_id payedを設定できます。次にリクエストを繰り返すときは、最初にredisのorder_idに対応する値を確認します。支払い済みの場合は、すでに支払い済みであることを意味するため、再度支払いを行わないでください。

次に、この注文の支払いを繰り返すと、支払いストリームを書き込んで挿入しようとすると、データベースから、一意のキーが競合しているというエラーが報告され、トランザクション全体をロールバックできます。

処理されたIDを保存することも可能であり、サービスのさまざまなインスタンスがredisを一緒に操作できます。

分散サービスインターフェイス要求の順序を確認するにはどうすればよいですか?

実際、分散システムインターフェイスの呼び出しシーケンスも問題です。一般的に、シーケンスは保証されていません。しかし、時にはそれは確かに厳格な注文保証を必要とするかもしれません。例を挙げると、サービスAはサービスBを呼び出し、挿入してから削除します。さて、2つのリクエストが渡され、異なるマシンに到達しました。おそらく、何らかの理由で挿入リクエストの実行が遅くなり、削除リクエストが最初に実行されました。このとき、データがなかったため、効果はありませんでした。その結果、 、この時点で挿入要求が来ました。さて、データが挿入されると、恥ずかしいことになります。

最初に挿入->次に削除するはずでした。このデータは削除する必要がありますが、最初に削除->次に挿入します。データはまだ存在します。結局、何が起こったのか理解できません。したがって、これらは分散システムで非常に一般的な問題です。

01_分散システムインターフェース呼び出しシーケンス

まず、一般的に言って、ビジネスロジックから最適に設計するシステムでは、この注文保証は必要ありません。注文保証が導入されると、システムの複雑さが増し、これにより、低効率、ホットデータへの過度のプレッシャーなどの問題について。

私たちが使用したソリューションを紹介します。簡単に言うと、最初にdubboのコンシステントハッシュ負荷分散戦略を使用して、たとえば、特定の注文IDに対応するリクエストを特定のマシンに配布し、次にその上で配布する必要があります。マシンは複数のスレッドによって同時に実行される可能性があり、特定の注文IDに対応するリクエストをすぐにメモリキューに入れ、キューにそれらの注文を確実にするように強制する必要がある場合があります。

しかし、これには多くのフォローアップの問題があります。たとえば、特定の注文が多くのリクエストに対応し、特定のマシンがホットスポットになる場合はどうなりますか?これらの問題を解決するには、一連の複雑な技術的解決策を開く必要があります。この種の問題は私たちに多くの頭痛の種を引き起こしました、それで提案は何ですか?

この種の問題を回避することをお勧めします。たとえば、注文の挿入操作と削除操作を1つの操作、つまり削除などに組み合わせることができるかどうかなどです。

MQとメモリキューを使用して解決する

方法1、そして最もフレンドリーな方法は、メッセージキューとメモリキューを使用して問題を解決することです。最初に行う必要があるのは、シーケンスする必要のある要求をハッシュアルゴリズムを介して同じ特定のマシンに分散することです。マシンは要求を内部に配置していますメモリキューでは、スレッドはメモリキューから消費を取得して、スレッドの順序を確認します

ただし、この方法では順序の99%を解決できますが、要求123を231に変更するなど、アクセスサービスに問題があり、MQキューの順序に一貫性がなくなる可能性があります。

分散ロックを使用して解決する

分散ロックは強力な一貫性を保証できますが、この重い同期メカニズムの導入により、頻繁なロック取得およびロック解放操作が必要になるため、同時実行の量が大幅に削減されます。

Dubboに似たRPCフレームワークを設計する方法

このような問題に遭遇したときは、少なくともあなたが知っている同様のフレームワークの原則から始めて、ダボの原則を参照してそれについて話します。それを設計することができます。たとえば、ダボにはそれほど多くのレイヤーがありませんか?そして、各レイヤーが何をするのか、あなたはおそらく知っていますか?この考えに基づいて大まかに話しましょう。

  • サービスを登録するには、登録センターに行く必要があります。各サービスに関する情報を保持する登録センターが必要ですか?動物園の飼育係を使用して登録できますか?
  • 次に、消費者は登録センターにアクセスして、対応するサービス情報を取得する必要があります。各サービスは複数のマシンに存在する可能性があります。
  • 次に、リクエストを開始します。どのようにリクエストを開始しますか?私は閉じ込められていますよね?もちろん、これは動的プロキシに基づいています。インターフェイスの動的プロキシを取得します。この動的プロキシはインターフェイスのローカルプロキシであり、プロキシはサービスに対応するマシンアドレスを検索します。
  • 次に、どのマシンでリクエストを送信しますか?次に、負荷分散アルゴリズムが必要です。たとえば、最も単純なアルゴリズムをランダムにポーリングできますよね?
  • 次に、マシンを見つけて、彼にリクエストを送信できます。最初の質問を送信するにはどうすればよいですか。netty、nioを使用したと言えます。2番目の質問は、どの形式のデータを送信する必要があるかということです。ヘシアンシリアル化プロトコルが使用されている、または他の何かが正しいと言えます。その後、リクエストは合格しました。
  • サーバー側も同じです。独自のサービスの動的プロキシを生成し、特定のネットワークポートをリッスンしてから、ローカルサービスコードをプロキシする必要があります。リクエストを受信すると、対応するサービスコードが呼び出されます。

Zookeeperの使用シナリオは何ですか?

分散ロックは非常に一般的に使用されます。Javaシステム開発、分散システムを実行している場合、使用されるシナリオがいくつかある可能性があります。最も一般的に使用される分散ロックは、分散ロックを作成するための飼育係です。

実際、正直に言うと、この質問をすることは、zkが分散システムで非常に一般的な基本システムであるため、zkを理解しているかどうかを確認することです。そして、あなたが尋ねるとき、zkの使用シナリオは何ですか?いくつかの基本的な使用シナリオを知っているかどうかを確認してください。しかし実際、zkが深く掘り下げた場合、非常に深く尋ねるのは自然なことです。

分散調整

これは実際にはzkの非常に古典的な使用法です。簡単に言えば、システムAがmqに要求を送信し、メッセージが消費された後、メッセージがBによって処理されるようなものです。AシステムはBシステムの処理結果をどのように知るのですか?zkを使用すると、分散システム間の調整を実現できます。システムAはリクエストを送信した後、zk上の特定のノードの値にリスナーを登録できます。システムBがそれを処理したら、そのノードzkの値を変更すると、Aはすぐに通知を受信できます。これは完璧なソリューションです。 。

01_zookeeperの分散調整シナリオ

分散ロック

特定のデータに対して2つの連続した変更操作が発行され、2つのマシンが同時に要求を受信しますが、最初に他のマシンを実行してから実行できるのは1つのマシンだけです。次に、この時点でzk分散ロックを使用できます。マシンが要求を受信した後、最初にzkの分散ロックを取得します。つまり、znodeを作成してから操作を実行できます。次に、別のマシンもznodeの作成を試みます。 、他の人が作成したため作成できなかったことが判明しました。待つことしかできません。最初のマシンが実行を終了するまで待ってから、自分で実行してください。

02_zookeeperの分散ロックシナリオ

メタデータ/構成情報の管理

Zkは、多くのシステムの構成情報を管理するために使用できます。たとえば、kafka、stormなどの多くの分散システムは、zkを使用して一部のメタデータと構成情報を管理します。dubboレジストリはzkもサポートしていません。

03_zookeepermetadata_configuration管理シナリオ

HAの高可用性

これは非常に一般的です。たとえば、hadoop、hdfs、yarnなどの多くのビッグデータシステムは、zkに基づくHA高可用性メカニズムの開発を選択します。つまり、重要なプロセスは通常、メインとバックアップの2つです。メインプロセスはすぐにzkによって検出されます。スタンバイプロセスに切り替えます04_zookeeperのHA高可用性シナリオ

分散ロック

面接の質問

  • 分散ロックを実装する一般的な方法は何ですか?
  • redisを使用して分散ロックを設計するにはどうすればよいですか?
  • zkを使用して分散ロックを設計しても大丈夫ですか?
  • 分散ロックの2つの実装のどちらがより効率的ですか?

Redisは分散ロックを実装しています

正式にはRedLockアルゴリズムと呼ばれ、redisによって公式にサポートされている分散ロックアルゴリズムです。

この分散ロックには、相互排除(1つのクライアントのみがロックを取得できる)、デッドロックなし、フォールトトレランス(ほとんどのredisノードまたはこのロックを追加および解放できる)という3つの重要な考慮事項があります。

最初の最も一般的な実装は、ロックするRedisでキーを作成することです

SET my:lockランダム値NX PX 30000、このコマンドは問題ありません。このNXは、キーが存在しない場合にのみ設定が成功することを意味し、PX 30000は、30秒後にロックが自動的に解放されることを意味します。他の誰かがそれを作成するとき、それがすでに存在していることに気付いた場合、彼らはそれをロックすることはできません。

ロックを解除することはキーを削除することですが、通常はluaスクリプトを使用してキーを削除し、値が同じ場合にのみ削除できます。

最も一般的な分散ロックの01_redis実装の原則

redisがluaスクリプトを実行する方法については、Baiduを自分で

if redis.call("get",KEYS[1]) == ARGV[1] then

return redis.call("del",KEYS[1])

else

  return 0

end

なぜランダムな値を使用するのですか?クライアントがロックを取得したが、実行が完了する前に長時間ブロックされていた場合、この時点でロックが自動的に解放された可能性があるため、この時点で他のクライアントがロックを取得した可能性があります。キーを削除するとこの時点で直接問題が発生するため、ランダムな値と上記のluaスクリプトを使用してロックを解除する必要があります。

しかし、これは間違いなく機能しません。これは、通常のredis単一インスタンスである場合、単一障害点であるためです。または、通常のマスタースレーブをredisしてから、マスタースレーブ非同期レプリケーションをredisします。マスターノードがハングした場合、キーはスレーブノードに同期されていません。この時点で、スレーブノードはマスターノードに切り替わり、他のノードはロックを取得します。

2番目の質問、RedLockアルゴリズム

  • このシナリオは、5つのredisマスターインスタンスを持つredisクラスターがあることを前提としています。次に、次の手順を実行してロックを取得します。
  • 現在のタイムスタンプをミリ秒単位で取得します
  • 上記と同様に、各マスターノードに順番にロックを作成してみてください。有効期限は比較的短く、通常は数十ミリ秒です。
  • ほとんどのノードでロックを確立してみてください。たとえば、5つのノードには3つのノードが必要です(n / 2 +1)
  • 確立が成功した場合でも、ロックを確立する時間がタイムアウト時間よりも短い場合、クライアントはロックを確立する時間を計算します
  • ロックの確立に失敗した場合は、ロックを順番に削除します
  • 他の誰かが分散ロックを確立している限り、ロックを取得するためにポーリングを続ける必要があります

02_RedLockアルゴリズム

ZKは分散ロックを実装しています

実際、zk分散ロックは比較的簡単に実行できます。つまり、ノードは一時的なznodeを作成しようとし、作成が成功するとロックが取得されます。このとき、他のクライアントはロックの作成に失敗し、リスナーを登録することしかできませんこのロックを聞いてください。ロックを解除するには、znodeを削除します。解除されると、クライアントに通知され、待機中のクライアントは再度ロックできます。

03_zookeeperの分散ロックの原則

ZKは分散ロックを実装します。つまり、ポーリングアルゴリズムを実行する必要はありませんが、リスナーを登録しますが、誰かがロックを解放すると、ロックを取得する必要があるプロセスに通知します。

同時に、ZKがロックを取得すると、実際に一時ノードが作成されます。一時ノードが以前に存在していなかった場合は、正常に作成されます。つまり、ロックはスレッドに属します。

同時に、他のスレッドが同じ名前の一時ノードを作成しようとします。すでに存在する場合は、他の誰かがすでにロックを持っていることを意味し、作成は失敗します。

一時ノードが削除されると、zkは、ロックが解放されたことを他のユーザーに通知します。これは、ロックが解放されたことと同じです。

この時点でロックを保持しているサーバーがダウンしていると仮定すると、Zookeeperは自動的にロックを解除します。

ZKは分散ロックコードを実装しています

/**
 * ZooKeeperSession
 * @author Administrator
 *
 */
public class ZooKeeperSession {
    
    
	
	private static CountDownLatch connectedSemaphore = new CountDownLatch(1);
	
	private ZooKeeper zookeeper;
private CountDownLatch latch;

	public ZooKeeperSession() {
    
    
		try {
    
    
			this.zookeeper = new ZooKeeper(
					"192.168.31.187:2181,192.168.31.19:2181,192.168.31.227:2181", 
					50000, 
					new ZooKeeperWatcher());			
			try {
    
    
				connectedSemaphore.await();
			} catch(InterruptedException e) {
    
    
				e.printStackTrace();
			}

			System.out.println("ZooKeeper session established......");
		} catch (Exception e) {
    
    
			e.printStackTrace();
		}
	}
	
	/**
	 * 获取分布式锁
	 * @param productId
	 */
	public Boolean acquireDistributedLock(Long productId) {
    
    
		String path = "/product-lock-" + productId;
	
		try {
    
    
			zookeeper.create(path, "".getBytes(), 
					Ids.OPEN_ACL_UNSAFE, CreateMode.EPHEMERAL);
return true;
		} catch (Exception e) {
    
    
while(true) {
    
    
				try {
    
    
Stat stat = zk.exists(path, true); // 相当于是给node注册一个监听器,去看看这个监听器是否存在
if(stat != null) {
    
    
this.latch = new CountDownLatch(1);
this.latch.await(waitTime, TimeUnit.MILLISECONDS);
this.latch = null;
}
zookeeper.create(path, "".getBytes(), 
						Ids.OPEN_ACL_UNSAFE, CreateMode.EPHEMERAL);
return true;
} catch(Exception e) {
    
    
continue;
}
}

// 很不优雅,我呢就是给大家来演示这么一个思路
// 比较通用的,我们公司里我们自己封装的基于zookeeper的分布式锁,我们基于zookeeper的临时顺序节点去实现的,比较优雅的
		}
return true;
	}
	
	/**
	 * 释放掉一个分布式锁
	 * @param productId
	 */
	public void releaseDistributedLock(Long productId) {
    
    
		String path = "/product-lock-" + productId;
		try {
    
    
			zookeeper.delete(path, -1); 
			System.out.println("release the lock for product[id=" + productId + "]......");  
		} catch (Exception e) {
    
    
			e.printStackTrace();
		}
	}
	
	/**
	 * 建立zk session的watcher
	 * @author Administrator
	 *
	 */
	private class ZooKeeperWatcher implements Watcher {
    
    

		public void process(WatchedEvent event) {
    
    
			System.out.println("Receive watched event: " + event.getState());

			if(KeeperState.SyncConnected == event.getState()) {
    
    
				connectedSemaphore.countDown();
			} 

if(this.latch != null) {
    
      
this.latch.countDown();  
}
		}
		
	}
	
	/**
	 * 封装单例的静态内部类
	 * @author Administrator
	 *
	 */
	private static class Singleton {
    
    
		
		private static ZooKeeperSession instance;
		
		static {
    
    
			instance = new ZooKeeperSession();
		}
		
		public static ZooKeeperSession getInstance() {
    
    
			return instance;
		}
		
	}
	
	/**
	 * 获取单例
	 * @return
	 */
	public static ZooKeeperSession getInstance() {
    
    
		return Singleton.getInstance();
	}
	
	/**
	 * 初始化单例的便捷方法
	 */
	public static void init() {
    
    
		getInstance();
	}
	
}

Redis分散ロックとZK分散ロック

Redis分散ロック、実際には、自分でロックを取得しようとする必要があり、パフォーマンスが低下します

Zk分散ロック、ロックを取得できない、リスナーを登録するだけ、ロックを積極的に取得しようとする必要がなく、パフォーマンスのオーバーヘッドが小さい

もう1つのポイントは、ロックを再取得したクライアントにバグがあるかハングした場合、ロックはタイムアウト期間後にのみ解放できることです。zkの場合、一時的なznodeが作成されるため、クライアントがハングしている限り、 znodeは解放されません。、この時点でロックは自動的に解放されます

redis分散ロックは見つけるたびに面倒ですか?ロックをトラバースし、時間を計算します。Zkの分散ロックセマンティクスは明確で実装が簡単です

そこで、あまり分析せずに、この2点についてお話します。個人的には、zkの分散ロックはredisの分散ロックよりも信頼性が高く、モデルはシンプルで使いやすいと思います。

おすすめ

転載: blog.csdn.net/weixin_43314519/article/details/109767118