Redis は、データベース、キャッシュ、メッセージ ブローカーとして使用できるメモリ内データ構造ストレージ システムです。Redis は、その高いパフォーマンスと柔軟なデータ構造により、分散ロックの実装を含むさまざまなシナリオで広く使用されています。
分散ロックは、分散システムで相互排他的アクセスを実装するテクノロジーです。多くの実際のアプリケーション シナリオでは、共有リソースの更新、タスク キューの処理など、特定の操作が同時に 1 つのノードでのみ実行できるようにする必要があります。現時点では、分散ロックを使用する必要があります。
Redis は、分散ロックを実装するためのシンプルかつ効果的な方法を提供します。基本的な考え方は、Redis の SETNX コマンドを使用することです。このコマンドは、キーが存在しない場合は値を設定し、キーがすでに存在する場合は何も行いません。このアトミックな操作を通じて、複数のノード間で相互排他的アクセスを実現できます。
ただし、Redis 分散ロックの実装は比較的単純ですが、実際の使用では、ロックのタイムアウトと更新の問題、ロックの公平性の問題、ネットワーク分割の問題など、多くの問題を考慮する必要があります。次の記事では、これらの問題とその解決策について詳しく説明します。
記事ディレクトリ
1. Redis 分散ロックの概要
1.1. 分散ロックについて
分散システムでは、スレッドがデータの読み取りと変更を行う場合、読み取り、更新、保存はアトミックな操作ではないため、同時実行中に同時実行の問題が発生しやすくなり、その結果、不正確なデータが生成されます。このシナリオは、電子商取引のフラッシュ セール活動や在庫数量の更新など、非常に一般的です。スタンドアロン アプリケーションの場合は、ローカル ロックを直接使用することで回避できます。分散アプリケーションの場合、ローカル ロックは役に立たないため、問題を解決するには分散ロックを導入する必要があります。
一般に、分散ロックを実装するには次の方法があります。
- MySQLを使用する場合:データベース内に一意のインデックステーブルを作成し、データを挿入することでロックを取得する方法で、挿入が成功すればロックの取得は成功、そうでなければロックの取得は失敗します。ロックを解除する操作は、このデータを削除することです。この方法の利点は実装が簡単であることですが、欠点はデータベース操作を伴うためパフォーマンスが低いことです。
- ZooKeeper の使用: ZooKeeper はネイティブの分散ロック実装を提供します。基本的な考え方は、一時的に順序付けされたノードを作成し、そのノードがすべての子ノードの中で最も小さいシーケンス番号を持つかどうかを判断することです。そうであれば、ロックは正常に取得されます。それ以外の場合は、そのノード自体のシーケンス番号よりも小さいシーケンス番号を持つノードをリッスンし、ノードが削除されたときに、再度ロックの取得を試みます。この方法のメリットは公平性を確保できることですが、デメリットは実装が複雑になることです。
- Redis を使用する: このメソッドは、Redis の SETNX コマンドによって実装されます。このコマンドは、キーが存在しない場合に値を設定でき、キーがすでに存在する場合は何も行いません。このアトミックな操作を通じて、複数のノード間で相互排他的アクセスを実現できます。この方法の利点は、パフォーマンスが高く、実装が簡単であることですが、欠点は、ロックのタイムアウトと更新の問題に対処する必要があることです。
1.2. Redis 分散ロックの概要
Redis では、SETNX
コマンドを使用して分散ロックを実装できます。具体的な手順は次のとおりです。
- ロック: クライアントは
SETNX key value
コマンドを使用してキーの設定を試みます。 はkey
ロックの名前、 はvalue
ロックされたクライアントを識別するために使用される一意の識別子 (UUID など) です。キーが存在しない場合、SETNX
コマンドはキーの値を設定し、ロックが成功したことを示す 1 を返します。キーがすでに存在する場合、コマンドはSETNX
キーの値を変更せず、ロックが成功したことを示す 0 を返します。ロックに失敗しました。
- ビジネス操作の実行: ロックの取得に成功すると、クライアントは保護が必要なビジネス操作を実行できます。
- ロック解除: ビジネス操作の完了後、クライアントは他のクライアントがロックを取得できるようにロックを解放する必要があります。ロックされたクライアントのみがロックを解除できるようにするには、クライアントはまずロックの値 (つまり、一意の識別子) を取得し、次にロックの値を自身の一意の識別子と比較して、それらが同じかどうかを確認する必要があります。同一の場合は、コマンドでキーを削除してロックを解除してください
DEL key
。
2. Redis分散ロックの問題点と解決策
2.1. ロックタイムアウトメカニズム
以下は、Redis 分散ロックの基本的な使用プロセスです。
- クライアント A が SETNX lock.key コマンドを送信し、1 が返された場合、クライアント A はロックを取得します。
- クライアント A は実行が完了すると、DEL lock.key コマンドを使用してロックを解放します。
ただし、この最も基本的なロックには問題があります。つまり、クライアント A が実行完了後に何らかの理由 (クラッシュやネットワークの問題など) でロックを解放するための DEL コマンドを送信できない場合、他のクライアントは決して送信できなくなります。ロックしてください。この問題を解決するには、ロック タイムアウト メカニズムを導入する必要があります。
以下は、タイムアウト機構を備えた Redis 分散ロックの使用プロセスです。
- クライアント A が SETNX lock.key コマンドを送信し、1 が返された場合、クライアント A はロックを取得します。
- クライアント A は、EXPIRE lock.key timeout コマンドを通じてロック タイムアウトを設定します。
- クライアント A は実行が完了すると、DEL lock.key コマンドを使用してロックを解放します。
これにより、クライアント A が実行後にロックを解放できなかった場合でも、ロックがタイムアウトした後に他のクライアントがロックを取得できます。
2.2. ロック更新メカニズム
ただし、タイムアウト メカニズムを備えたこのロックには問題があります。つまり、ロックがタイムアウト寸前になったときにクライアント A がまだ実行している場合、ロックが他のクライアントによって取得される可能性があり、その結果、複数のクライアントがロックを保持することになります。同時にです。この問題を解決するには、ロック更新メカニズムを導入する必要があります。
以下は、更新メカニズムを備えた Redis 分散ロックの使用プロセスです。
- クライアント A が SETNX lock.key コマンドを送信し、1 が返された場合、クライアント A はロックを取得します。
- クライアント A は、EXPIRE lock.key timeout コマンドを通じてロック タイムアウトを設定します。
- 実行中、クライアント A は EXPIRE lock.key timeout コマンドによってロックを定期的に更新します。
- クライアント A は実行が完了すると、DEL lock.key コマンドを使用してロックを解放します。
このようにして、クライアント A の実行時間が初期タイムアウト時間を超えた場合でも、更新メカニズムによってロックの相互排他性が保証されます。
2.3. 誤ってロックを削除してしまう問題
ロック更新メカニズムを導入すると、早期にロックが期限切れになる問題は解決できますが、ロック解除時に他のスレッド ロックが削除される問題は解決されません。これは、更新メカニズムを使用しても、ロックの有効期限が切れるときにスレッド A がビジネス ロジックを実行している状況が依然として存在するためであり、この時点でロックの有効期限が切れ、スレッド B がロックを取得し、その後スレッド A が終了します。ビジネスロジックを実行中、ロックを削除しようとしましたが、スレッドBのロックが削除されてしまいます。
この問題を解決するには、Redis の Lua スクリプト機能を使用して、これら 3 つの操作を Lua スクリプトにカプセル化し、コマンドを使用してEVAL
Lua スクリプトを実行します。Redis はすべてのコマンドを 1 つのスレッドで順番に実行するため、EVAL
コマンドは Lua スクリプトでの操作がアトミックであることを保証できます。
以下は、Lua スクリプトを使用したロック解除の例です。
if redis.call("get",KEYS[1]) == ARGV[1] then
return redis.call("del",KEYS[1])
else
return 0
end
この Lua スクリプトでは、まずget
コマンドを使用してロックの値を取得し、次にロックの値とクライアントの一意の識別子を比較し、それらが同じであれば、コマンドを使用してロックを削除しますdel
。
クライアントは、次のコマンドを使用してこの Lua スクリプトを実行できます。
EVAL script 1 key value
その中には、script
Lua スクリプトの内容、key
ロックの名前、value
およびクライアントの一意の識別子があります。
2.4. スプリットブレイン問題とレッドロック
Redis クラスターでは、同期ロックがスレーブ ノードに転送される前にマスター ノードが停止した場合、スレーブ ノードはマスター ノードにアップグレードされた後にロックが存在しないと誤って認識し、他のクライアントがロックを取得できるようになります。複数のクライアントが同時にロックを保持するという問題。
この問題を解決するには、RedLock アルゴリズムを使用できます。RedLock は、Redis によって公式に推奨されている分散ロック実装アルゴリズムです。その基本的な考え方は、複数の独立した Redis ノードで同時にロックを取得しようとすることです。ほとんどの Redis ノードがロックの取得に成功した場合にのみ、操作全体が成功したとみなされます。
RedLock アルゴリズムの基本的な手順は次のとおりです。
- 現在の時刻をミリ秒単位で取得します。
- すべての Redis ノードのロックの取得が順番に試行され、試行ごとに固定のタイムアウトが設定されます。ロックの取得に失敗した場合は、すぐに戻り、他のノードを試行しません。
- ほとんどの Redis ノードのロックが正常に取得され、ロックの取得にかかる合計時間がロックの有効期間よりも短い場合、操作全体が成功したことになります。
- ロックを取得するまでの合計時間がロックの有効期間を超えた場合、またはほとんどの Redis ノードのロックが正常に取得されなかった場合、すべての Redis ノードでロックが解放されます。
- 操作全体が成功した場合、ロックの有効期間は、元の有効期間からロックを取得する合計時間を引いたものになります。
上記は RedLock アルゴリズムの基本的な手順です。RedLock アルゴリズムは、複数の独立した Redis ノードで同時にロックの取得を試みることにより、マスター ノードの障害によって引き起こされるロック損失の問題をある程度解決できます。ただし、RedLock アルゴリズムはロックのセキュリティを完全に保証するものではないことに注意してください。ネットワークが分断されたり、ノード時間が同期していない場合には、同じロックが複数のクライアントによって同時に保持される可能性があるためです。 。したがって、RedLockアルゴリズムを使用する場合には、実際の状況に基づいた詳細な設計とテストが必要です。
2.5. 公平性の問題
さらに、Redis 分散ロックの実装では、ロックの公平性が問題になる可能性があります。いわゆる公平性とは、複数のクライアントが同時にロックを要求した場合、ロックは要求の順序で割り当てられるべきであることを意味します。ただし、ネットワーク遅延と Redis のシングルスレッド モデルのため、Redis 分散ロックは公平性を保証できません。具体的には、複数のクライアントが同時にロックを要求した場合、ネットワークの遅延により、これらのリクエストは異なる時間に Redis に到着する可能性があり、Redis はリクエストが到着した順序でロックを割り当てますが、これはクライアントのリクエスト順序とは異なる場合があります。また、複数のリクエストが同時に Redis に到着した場合でも、Redis のシングルスレッド モデルにより、Redis はこれらのリクエストを順番に処理することしかできず、処理順序はクライアントのリクエスト順序と異なる場合があります。
したがって、アプリケーションで公平な分散ロックが必要な場合は、ZooKeeper に基づくものなど、他の分散ロック実装を使用する必要がある場合があります。ZooKeeper の分散ロックは、ロックされたノードの下に連続した一時ノードを作成し、自分のノードが最小のノードであるかどうかを比較してロックが取得されたかどうかを判断することで、ロックの公平性を保証します。
3. Java での Redis 分散ロックの実装
3.1. Jedis の実装
Java では、Jedis や Lettuce などの Redis クライアント ライブラリを使用して、Redis 分散ロックを実装できます。基本的な実装例を次に示します。
import redis.clients.jedis.Jedis;
public class RedisLock {
private Jedis jedis;
private String lockKey;
private String lockValue;
private int expireTime;
private boolean locked = false;
public RedisLock(Jedis jedis, String lockKey, int expireTime) {
this.jedis = jedis;
this.lockKey = lockKey;
this.expireTime = expireTime;
this.lockValue = Thread.currentThread().getId() + "-" + System.nanoTime();
}
public boolean lock() {
long startTime = System.currentTimeMillis();
while (true) {
String result = jedis.set(lockKey, lockValue, "NX", "PX", expireTime);
if ("OK".equals(result)) {
locked = true;
return true;
}
// 如果没有获取到锁,需要稍微等待一下再尝试
try {
Thread.sleep(100);
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
// 如果尝试获取锁超过了expireTime,那么返回失败
if (System.currentTimeMillis() - startTime > expireTime) {
return false;
}
}
}
public void unlock() {
if (!locked) {
return;
}
String script = "if redis.call('get', KEYS[1]) == ARGV[1] then return redis.call('del', KEYS[1]) else return 0 end";
jedis.eval(script, 1, lockKey, lockValue);
}
}
この例では、set
コマンドNX
の とオプションを使用してPX
ロックの取得とタイムアウト設定を実装し、Lua スクリプトを使用して安全なロック解除操作を実装します。また、while ループを使用して、ロックが正常に取得されるか、試行時間が期限切れになるまで、ロックの取得を継続的に試行します。
ただし、この例ではロック更新メカニズムは実装されていません。expire
更新の仕組みを実装するには、定期的に別スレッドでロックの残り時間を確認し、残り時間が足りない場合はコマンドでロックのタイムアウトをリセットする必要があります。これには、Java の ScheduledExecutorService を使用して更新操作を定期的に実行するなど、より複雑なコードを実装する必要があります。
3.2. SpringBoot の実装
Spring Boot では、Redis クライアント ライブラリである Redisson を使用して、Redis 分散ロックを実装できます。Redisson は、分散ロック、分散コレクション、分散キューなどを含む豊富な分散サービスのセットを提供します。また、Redisson にはロックのタイムアウトおよび更新メカニズムが組み込まれており、ロックを誤って削除する問題を解決します。
以下は、Redisson を使用して Redis 分散ロックを実装する基本的な例です。
import org.redisson.Redisson;
import org.redisson.api.RLock;
import org.redisson.api.RedissonClient;
import org.redisson.config.Config;
import org.springframework.stereotype.Component;
import javax.annotation.PostConstruct;
import java.util.concurrent.TimeUnit;
@Component
public class RedissonDistributedLocker {
private RedissonClient redissonClient;
@PostConstruct
public void init() {
Config config = new Config();
config.useSingleServer().setAddress("redis://127.0.0.1:6379");
redissonClient = Redisson.create(config);
}
public void lock(String lockKey) {
RLock lock = redissonClient.getLock(lockKey);
// Wait for 100 seconds and automatically unlock it after 10 seconds
lock.lock(10, TimeUnit.SECONDS);
}
public void unlock(String lockKey) {
RLock lock = redissonClient.getLock(lockKey);
lock.unlock();
}
}
この例では、まずinit
メソッド内で RedissonClient インスタンスを作成し、次にlock
メソッド内で RLock オブジェクトを取得し、そのlock
メソッドを呼び出してロックを取得します。このメソッドではunlock
、RLock オブジェクトも取得し、そのunlock
メソッドを呼び出してロックを解放します。
Redisson のメソッドは自動的に更新されることに注意してくださいlock
。ロックを保持しているスレッドがまだ実行されている限り、スレッドが終了するかメソッドが明示的にunlock
呼び出されるまで、ロックは更新されます。したがって、更新メカニズムを手動で実装する必要はありません。さらに、Redisson のunlock
方法では、現在のスレッドがロックを保持しているかどうかを確認し、ロックを保持しているスレッドのみがロックを解放できるため、誤ってロックを削除してしまう問題も解決されます。