English | 简体中文 | 繁體中文 | Русский язык | Français | Español | Português | Deutsch | 日本語 | 한국어 | Italiano | بالعربية

Redis分散ロックの正しい実現方法をJava言語で説明

分布式ロックは一般的に3つの実装方法があります:1.データベースの楽観ロック;2.Redisに基づく分布式ロック;3.ZooKeeperに基づく分布式ロック。このブログでは、Redisに基づく分布式ロックの実装について紹介する第二种方法です。ネット上にはさまざまなRedis分布式ロックの実装に関するブログがありますが、その実装にはさまざまな問題があります。誤解を避けるために、このブログではRedis分布式ロックの正しい実装方法を詳細に説明します。

信頼性

まず、分布式ロックが利用可能であることを確保するために、以下の4つの条件を同時に満たすロックの実装を少なくとも確保する必要があります:

排他性があります。任意の瞬間で、ロックを保持しているクライアントは常に1つです。

死锁は発生しません。ロックを保持しているクライアントがクラッシュして自動的にロックを解除しなかった場合でも、後続の他のクライアントがロックを取得できることを保証します。

エラトロピー性があります。ほとんどのRedisノードが正常に動作している限り、クライアントはロックをかけたり解除したりできます。

解き放ちは、ロックをかけた人自身が行う必要があります。クライアントは他人がかけたロックを解除することはできません。

コード実装

コンポーネント依存関係

まず、JedisオープンソースコンポーネントをMavenを通じて導入する必要があります。pom.xmlファイルに以下のコードを追加します:

<dependency> 
  <groupId>redis.clients</groupId> 
  <artifactId>jedis</artifactId> 
  <version>2.9.0</version> 
</dependency> 

ロックコード

正しい方法

言葉は安い、コードを見せてくれ。なぜそのように実装するか説明する前に、まずコードを展示する:

public class RedisTool {
	private static final String LOCK_SUCCESS = "OK";
	private static final String SET_IF_NOT_EXIST = "NX";
	private static final String SET_WITH_EXPIRE_TIME = "PX";
	/** 
   * 分布式ロックを取得しようとする 
   * @param jedis Redisクライアント 
   * @param lockKey ロック 
   * @param requestId 请求識別子 
   * @param expireTime 超過時間 
   * @return は否則取得成功 
   */
	public static Boolean tryGetDistributedLock(Jedis jedis, String lockKey, String requestId, int expireTime) {
		String result = jedis.set(lockKey, requestId, SET_IF_NOT_EXIST, SET_WITH_EXPIRE_TIME, expireTime);
		if (LOCK_SUCCESS.equals(result)) {
			return true;
		}
		return false;
	}
}

加锁は以下の一行のコードで実行できます:jedis.set(String key, String value, String nxxx, String expx, int time)。このset()メソッドには5つの引数があります:

1番目のkeyは、ロックとして使用します。なぜなら、keyはユニークだからです。

2番目のvalueは、requestIdを渡します。多くの生徒がkeyでロックが十分ではないかと疑問に思うかもしれませんが、なぜvalueを使う必要があるのでしょうか?その理由は、上記で述べた信頼性の部分で、分散ロックは4番目の条件「解き鍵は鍵に縛られています」を満たす必要があるからです。valueにrequestIdを割り当てることで、このロックがどのリクエストでロックされたかを知り、ロックを解除する際に基準となります。requestIdはUUID.randomUUID().toString()メソッドで生成できます。

3番目のnxxxは、NXという名前で、SETIFNOTEXISTを意味し、keyが存在しない場合にset操作を行い、keyが既に存在する場合は何も行いません。

4番目のexpxは、PXという名前で、keyに有効期限を設定する設定を意味します。具体的な時間は5番目のパラメータで決定されます。

5番目のtimeは、4番目のパラメータと連動しており、keyの有効期限を表します。

上記のset()メソッドを実行すると、結果は2種類しかありません:1.現在のロック(keyが存在しない)がない場合、ロック操作を行い、ロックに有効期限を設定します。同時にvalueはロックするクライアントを表します。2.既にロックが存在する場合は、何も行いません。

心细な生徒が気づくでしょう、私たちのロックコードは信頼性で述べた3つの条件を満たしています。まず、set()にはNXパラメータが追加され、既にkeyが存在する場合、関数が成功しません。つまり、ロックを保持するクライアントは1つだけであり、排他性が満たされます。次に、ロックに有効期限を設定しているため、ロックの保持者がクラッシュしてロックを解除しなかった場合でも、ロックは有効期限が切れると自動的に解除されます(keyが削除される)。これにより、死锁が発生しません。最後に、valueにrequestIdを割り当てることで、ロックを保持するクライアントのリクエスト識別子を表し、クライアントがロックを解除する際に同一クライアントであるかどうかを検証できます。私たちはRedisの単機デプロイメントシーンを考慮しているため、容错性については一時的に考慮しません。

エラーサンプル1

よくあるエラーサンプルとして、jedis.setnx()とjedis.expire()を組み合わせてロックを実現する方法があります。以下のコードです:

public static void wrongGetLock1(Jedis jedis, String lockKey, String requestId, int expireTime) {
	long result = jedis.setnx(lockKey, requestId);
	if (result == 1) {
		// もしここでプログラムがクラッシュすると、有効期限を設定することができず、死锁が発生します。 
		jedis.expire(lockKey, expireTime);
	}
}

setnx()メソッドの役割はSETIFNOTEXISTであり、expire()メソッドはロックに有効期限を設定します。一見すると前のset()メソッドの結果と同じように見えますが、これは2つのRedisコマンドであり、原子性がありません。もしsetnx()の実行中にプログラムがクラッシュすると、ロックに有効期限が設定されず、死锁が発生します。なぜそのように実装されているのかは、低バージョンのjedisが複数の引数を取るset()メソッドをサポートしていないためです。

エラーサンプル2

このようなエラーサンプルは問題を見つけるのが難しく、実装も複雑です。実装の思惑:jedis.setnx()コマンドを使用してロックを実現します。keyはロック、valueはロックの有効期限です。実行プロセス:1.setnx()メソッドを通じてロックを試みます。もし現在のロックが存在しない場合、ロックが成功したと返します。2.もしロックが既に存在する場合、ロックの有効期限を取得し、現在時刻と比較します。ロックが有効期限切れの場合、新しい有効期限を設定し、ロックが成功したと返します。以下のコードです:

public static Boolean wrongGetLock2(Jedis jedis, String lockKey, int expireTime) {
	long expires = System.currentTimeMillis() + expireTime;
	String expiresStr = String.valueOf(expires);
	// 現在のロックが存在しない場合、ロックを取得成功とみなします 
	if (jedis.setnx(lockKey, expiresStr) === 1) {
		return true;
	}
	// ロックが存在する場合、ロックの期限切れ時間を取得します 
	String currentValueStr = jedis.get(lockKey);
	if (currentValueStr != null && long.parselong(currentValueStr) < System.currentTimeMillis()) {
		// ロックが期限切れになった場合、前のロックの期限切れ時間を取得し、現在のロックの期限切れ時間を設定します 
		String oldValueStr = jedis.getSet(lockKey, expiresStr);
		if (oldValueStr != null && oldValueStr.equals(currentValueStr)) {
			// マルチスレッドのコンフリクトを考慮すると、現在の値と設定値が同じスレッドがあれば、そのスレッドだけがロックする権利があります。 
			return true;
		}
	}
	// 他のすべてのケースでは、ロックを取得失敗とみなします。 
	return false;
}

では、このコードにはどのような問題がありますか?1.クライアントが自分で期限切れ時間を生成するため、分布式環境の各クライアントの時間が同期する必要があります。2.ロックが期限切れになった場合、複数のクライアントが同時にjedis.getSet()メソッドを実行すると、最終的には1つのクライアントだけがロックを取得できますが、そのクライアントのロックの期限切れ時間が他のクライアントによって上書きされる可能性があります。3.ロックには所有者識別子がありません。つまり、どのクライアントでもロックを解除できます。

ロック解除コード

正しい方法

まずはコードを紹介して、その後、なぜそのように実装したのか説明しましょう:

public class RedisTool {
	private static final long RELEASE_SUCCESS = 1L;
	/** 
   * 分布式ロックの解放 
   * @param jedis Redisクライアント 
   * @param lockKey ロック 
   * @param requestId 请求識別子 
   * @return 释放成功かどうか 
   */
	public static Boolean releaseDistributedLock(Jedis jedis, String lockKey, String requestId) {
		String script = "if redis.call('get', KEYS[1]) == ARGV[1] then return redis.call('del', KEYS[1
		Object result = jedis.eval(script, Collections.singletonList(lockKey), Collections.singletonList(requestId));
		if (RELEASE_SUCCESS.equals(result)) {
			return true;
		}
		return false;
	}
}

アンラップルにはたった二行のコードが必要です!第一行のコードでは、シンプルなLuaスクリプトを書いています。このプログラミング言語は、最後に「ハッカーズと画家」で見たとき以来です。そして、このように実用的に使われるとは思わなかったのです。第二行のコードでは、Luaコードをjedis.eval()メソッドに渡し、引数KEYS[1】にlockKey、ARGV[1】にrequestIdを割り当てます。eval()メソッドはLuaコードをRedisサーバーに実行させるものです。

このLuaコードの機能は何でしょうか?実は非常にシンプルです。まず、ロックに対応するvalue値を取得し、requestIdと一致しているか確認します。一致している場合はロックを削除(アンラップル)します。なぜLua言語を使用する必要があるのでしょうか?それは、上記の操作が原子性を持つことを確保するためです。非原子性が引き起こす問題について詳しくは、【アンラップルコード】を参照してください。-エラーサンプル2】それでは、eval()メソッドが原子性を確保できる理由は何でしょうか?それはRedisの特性によるものです。以下は、evalコマンドに関する公式サイトの一部の説明です:

簡単に言えば、evalコマンドを実行する際に、Luaコードがコマンドとして実行され、evalコマンドが完了するまで、Redisは他のコマンドを実行しません。

エラーサンプル1

最も一般的なアンラップルのコードは、jedis.del()メソッドを使用して直接ロックを削除することです。このロックの所有者を事前に確認せずに直接アンラップルする方法は、どのクライアントでもいつでもアンラップルできるようにして、このロックが自分のものではない場合でもそれを実行できるようにします。

public static void wrongReleaseLock1(Jedis jedis, String lockKey) { 
  jedis.del(lockKey); 
} 

エラーサンプル2

このアンラップルのコードは一見問題ないように見えます。実際、私は以前もこのように実装するかもと思っていました。正しいポーズとほぼ同じですが、唯一の違いは、二つの命令に分けて実行することです。以下はそのコードです:

public static void wrongReleaseLock2(Jedis jedis, String lockKey, String requestId) {
	// ロックとアンロックが同じクライアントかどうかを判断 
	if (requestId.equals(jedis.get(lockKey))) {
		// この場合、このロックがこのクライアントのものではなくなった場合、誤解锁が発生します 
		jedis.del(lockKey);
	}
}

コードのコメントによると、jedis.del()メソッドを呼び出すときに、このロックが現在のクライアントに属していない場合、他人が加えたロックを解除する問題があります。本当にこのようなシーンがあるのでしょうか?答えはイエスです。例えば、クライアントAがロックをかけ、その後一段时间が経過するとクライアントAがアンロックし、jedis.del()を呼び出す前にロックが突然期限切れになる場合、クライアントBがロックをかけ成功し、クライアントAがdel()メソッドを実行すると、クライアントBのロックが解除されます。

まとめ

この記事では、Javaコードを使用してRedis分散ロックを正しく実現する方法について紹介し、ロックとアンロックのために2つの比較的典型的なエラーサンプルも提供しました。実際には、Redisを使用して分散ロックを実現することは難しくありません。信頼性の4つの条件を満たすだけで十分です。

分散ロックはどのようなシーンで使用されますか?データベースにデータを挿入する際に、データベースに似たデータがあるかどうかを事前に確認する必要がある場所、例えば、複数のリクエストが同時に挿入する場合、データベースがすべて似たデータがないと判断されると、すべてのリクエストが追加できます。この場合、シンクロニズム処理が必要ですが、データベースのロックテーブルを直接ロックするのは時間がかかりすぎるため、Redis分散ロックを使用し、同時にデータを挿入する操作を行うためのスレッドが1つだけであることを確保します。

これで、本文で説明したjava言語のRedis分散ロックの正しい実現方法に関するすべての内容が終わります。皆さんに役立つことを願っています。興味を持った方々は、このサイトの他の関連するトピックも参照してください。不十分な点があれば、コメントを残してください。皆様のサポートに感謝します!

声明:この記事の内容はインターネットから提供され、著作権者に帰属します。インターネットユーザーが自発的に提供し、自己でアップロードしたものであり、このサイトは所有権を持ちません。また、人工的な編集は行われていません。著作権侵害の疑いがある場合は、メールを送信してください:notice#oldtoolbag.com(メールを送信する際には、#を@に変更してください)で通報し、関連する証拠を提供してください。一旦確認ができたら、このサイトは即座に侵害疑いのコンテンツを削除します。

おすすめ