分散ロックは、大規模なアプリケーションで最も広く採用されている機能の1つです。 さまざまな方法を使用して、Redisに分散ロックを実装できます。 このトピックでは、分散ロックを実装する一般的な方法と、Tairを使用して分散ロックを実装するためのベストプラクティスについて説明します。 これらのベストプラクティスは、Alibaba GroupのTairと分散ロックの使用経験に基づいて開発されています。
背景情報
分散ロックとその使用シナリオ
アプリケーション開発中に同じプロセス内の複数のスレッドが特定のリソースに同時にアクセスする場合は、ミューテックス (相互排他ロックとも呼ばれます) と読み書きロックを使用できます。 同じホスト上の複数のプロセスが特定のリソースに同時にアクセスする場合は、セマフォ、パイプライン、共有メモリなどのプロセス間同期プリミティブを使用できます。 ただし、特定のリソースに複数のホストが同時にアクセスする場合は、分散ロックを使用する必要があります。 分散ロックは、グローバルに存在する相互除外ロックです。 分散システムのリソースに分散ロックを適用して、リソースの競合によって引き起こされる可能性のある論理障害を防ぐことができます。
分散ロックの特徴
相互排他的
いつでも、1つのクライアントだけがロックを保持できます。
デッドロックなし
分散ロックはリースベースのロックメカニズムを使用します。 クライアントがロックを取得してから例外が発生した場合、ロックは一定期間後に自動的に解放されます。 これにより、リソースのデッドロックが防止されます。
一貫性
Tairの切り替えは、外部または内部エラーによってトリガーされる可能性があります。 外部エラーにはハードウェア障害とネットワーク例外が含まれ、内部エラーには低速クエリとシステム欠陥が含まれます。 切り替えがトリガーされると、高可用性 (HA) を確保するために、レプリカノードが新しいマスターノードに昇格されます。 このシナリオでは、ビジネスの相互排除要件が高い場合、切り替え後もロックは同じままである必要があります。
オープンソースRedisで分散ロックを実装する
このセクションで説明する方法は、ApsaraDB for Redis Community Editionにも適しています。
ロックを取得する
Redisでは、ロックを取得するにはSETコマンドを実行するだけです。 次のセクションでは、コマンドの例を示し、コマンドのパラメーターまたはオプションについて説明します。
SET resource_1 random_value NX EX 5
表 1. パラメータまたはオプション
パラメータ /オプション
説明
resource_1
分散ロックのキー。 キーが存在する場合、対応するリソースはロックされ、他のクライアントからアクセスできません。
random_value
ランダムな文字列。 値はクライアント間で一意である必要があります。
EX
キーの有効期間。 単位は秒です。 また、PXオプションを使用して、ミリ秒の正確な有効期間を設定することもできます。
NX
キーがRedisに存在しない場合にのみキーを設定します。
サンプルコードでは、resource_1キーの有効期間は5秒に設定されています。 クライアントがキーを解放しない場合、キーは5秒後に期限切れになり、ロックはシステムによって回収されます。 その後、他のクライアントがリソースをロックしてアクセスできます。
ロックを解放するRelease a lock
ほとんどの場合、DELコマンドを実行してロックを解除できます。 しかし、これは以下の問題を引き起こす可能性がある。
t1時点で、分散ロックのキーはアプリケーション1のresource_1であり、resource_1キーの有効期間は3秒に設定されています。
アプリケーション1は、長い応答時間などの理由で3秒を超えてブロックされたままです。 resource_1キーは期限切れになり、分散ロックはt2時点で自動的に解除されます。
t3時点で、アプリケーション2は分散ロックを取得する。
アプリケーション1は、ブロックされている状態から再開し、t4時点で
DEL resource_1
コマンドを実行して、アプリケーション2によって保持されている分散ロックを解放する。
この例は、ロックを設定するクライアントのみがロックを解放する必要があることを示しています。 したがって、クライアントがDELコマンドを実行してロックを解放する前に、クライアントはGETコマンドを実行してロックが自分で設定されているかどうかを確認する必要があります。 ほとんどの場合、クライアントはRedisで次のLuaスクリプトを使用して、クライアントによって設定されたロックを解放します。
if redis.call("get",KEYS[1]) == ARGV[1] then return redis.call("del",KEYS[1]) else return 0 end
ロックを更新する
クライアントがロックのリース時間内に必要な操作を完了できない場合、クライアントはロックを更新する必要があります。 ロックは、ロックを設定するクライアントによってのみ更新できます。 Redisでは、クライアントは次のLuaスクリプトを使用してロックを更新できます。
if redis.call("get",KEYS[1]) == ARGV[1] then return redis.call("expire",KEYS[1], ARGV[2]) else return 0 end
Tairに基づく分散ロックの実装
インスタンスがTair DRAMベースのインスタンスまたは永続メモリ最適化インスタンスの場合、Luaスクリプトを使用せずに文字列拡張コマンドを実行して分散ロックを実装できます。
ロックを取得する
Tairでロックを取得する方法は、オープンソースのRedisで使用されている方法と同じです。 このメソッドは、SETコマンドを実行するために使用されます。 サンプルコマンド:
SET resource_1 random_value NX EX 5
ロックを解放するRelease a lock
TairのCADコマンドは、ロックを解除するためのエレガントで効率的な方法を提供します。 CADコマンドの詳細については、「CAD」をご参照ください。 サンプルコマンド:
/* if (GET(resource_1) == my_random_value) DEL(resource_1) */ CAD resource_1 my_random_value
ロックを更新する
CASコマンドを実行して、ロックを更新できます。 詳細については、「CAS」をご参照ください。 サンプルコマンド:
CAS resource_1 my_random_value my_random_value EX 10
説明CASコマンドは、新しい値が元の値と同じかどうかをチェックしません。
Jedisに基づくサンプルコード
CASおよびCADコマンドの定義
enum TairCommand implements ProtocolCommand { CAD("CAD"), CAS("CAS"); private final byte[] raw; TairCommand(String alt) { raw = SafeEncoder.encode(alt); } @Override public byte[] getRaw() { return raw; } }
ロックを取得する
public boolean acquireDistributedLock(Jedis jedis,String resourceKey, String randomValue, int expireTime) { SetParams setParams = new SetParams(); setParams.nx().ex(expireTime); String result = jedis.set(resourceKey,randomValue,setParams); return "OK".equals(result); }
ロックを解放するRelease a lock
public boolean releaseDistributedLock(Jedis jedis,String resourceKey, String randomValue) { jedis.getClient().sendCommand(TairCommand.CAD,resourceKey,randomValue); Long ret = jedis.getClient().getIntegerReply(); return 1 == ret; }
ロックを更新する
public boolean renewDistributedLock(Jedis jedis,String resourceKey, String randomValue, int expireTime) { jedis.getClient().sendCommand(TairCommand.CAS,resourceKey,randomValue,randomValue,"EX",String.valueOf(expireTime)); Long ret = jedis.getClient().getIntegerReply(); return 1 == ret; }
ロックの一貫性を確保する方法
マスターノードとレプリカノード間のレプリケーションは非同期です。 データ変更がマスターノードに書き込まれた後にマスターノードがクラッシュし、HA切り替えがトリガーされた場合、バッファ内のデータ変更は新しいマスターノードにレプリケートされない可能性があります。 この結果、データの不整合が生じる。 新しいマスターノードは元のレプリカノードであることに注意してください。 失われたデータが分散ロックに関連する場合、ロック機構は故障し、サービス例外が発生する。 このセクションでは、ロックの一貫性を確保するために使用できる3つの方法について説明します。
Redlockアルゴリズムの使用
Redlockアルゴリズムは、ロックの一貫性を確保するために、オープンソースのRedisプロジェクトの創設者によって提案されています。 Redlockアルゴリズムは、確率の計算に基づいています。 単一のマスターレプリカRedisインスタンスは、HA切り替え中に
k %
の確率でロックを失う可能性があります。 Redlockアルゴリズムを使用して分散ロックを実装する場合、次の式に基づいて、N個の独立したマスターレプリカRedisインスタンスが同時にロックを失う確率を計算できます。ロックを失う確率=(k %)^ N
。 Redisの安定性が高いため、ロックの紛失はめったに発生せず、サービス要件を簡単に満たすことができます。説明Redlockアルゴリズムを実装する場合、N個のRedisインスタンスのすべてのロックが同時に有効になることを確認する必要はありません。 ほとんどの場合、
M
Redisノードのロックが同時に有効になるようにすると、Redlockアルゴリズムはビジネス要件を満たすことができます。 Mが1より大きく、N以下であることを確認する。Redlockアルゴリズムには次の問題があります。
クライアントがロックを取得または解放するのに時間がかかります。
クラスターまたは標準のマスターレプリカインスタンスでRedlockアルゴリズムを使用することはできません。
Redlockアルゴリズムは大量のリソースを消費します。 Redlockアルゴリズムを使用するには、複数の独立したApsaraDB for Redisインスタンスまたは自己管理Redisインスタンスを作成する必要があります。
WAITコマンドを使用する
RedisのWAITコマンドは、以前のすべての書き込みコマンドがマスターノードから指定された数のレプリカノードに同期されるまで、現在のクライアントをブロックします。 WAITコマンドでは, ミリ秒単位のタイムアウト時間を指定できます。 WAITコマンドは、分散ロックの一貫性を確保するためにApsaraDB for Redisで使用されます。 サンプルコマンド:
SET resource_1 random_value NX EX 5 WAIT 1 5000
WAITコマンドを実行すると、クライアントはロックを取得した後、2つのシナリオで他の操作を実行し続けます。 1つのシナリオは、データがレプリカノードに同期されることである。 もう1つのシナリオは、タイムアウト期間に達した場合です。 この例では、タイムアウト期間は5,000ミリ秒です。 WAITコマンドの出力が1の場合、マスターノードとレプリカノード間でデータが同期されます。 この場合、データの整合性が確保される。 WAITコマンドは、Redlockアルゴリズムよりもはるかに費用対効果が高くなります。
WAITコマンドを使用する前に、次の項目に注意してください。
WAITコマンドは、WAITコマンドを送信するクライアントのみをブロックし、他のクライアントには影響しません。
WAITコマンドが有効な値を返した場合、ロックはマスターノードからレプリカノードに同期されます。 ただし、コマンドが正常な応答を返す前にHAスイッチオーバーがトリガーされると、データが失われる可能性があります。 この場合、WAITコマンドの出力は、起こり得る同期障害を示すだけであり、データの完全性を保証することはできない。 WAITコマンドがエラーを返した後、ロックを再度取得するか、データを検証できます。
ロックを解除するためにWAITコマンドを実行する必要はありません。 これは、分散ロックが相互に排他的であるためです。 一定期間後にロックを解除しても論理障害は発生しません。
テアを使う
CASおよびCADコマンドは、分散ロックの開発と管理のコストを削減し、ロックのパフォーマンスを向上させるのに役立ちます。
Tair DRAMベースのインスタンスは、オープンソースのRedisの3倍のパフォーマンスを提供します。 DRAMベースのインスタンスを使用して同時実行性の高い分散ロックを実装する場合でも、サービスの継続性が保証されます。
Tair永続メモリ最適化インスタンスは、リアルタイムのデータ永続性を確保するために永続メモリを採用しています。 データ永続化の試行が成功した後、書き込み操作ごとに応答が返されます。 停電が発生してもデータの損失が防止される。 永続的なメモリ最適化インスタンスでは、マスターとレプリカの同期に半同期モードを指定することもできます。 このモードでは、データがマスターノードに書き込まれ、レプリカノードに同期された場合にのみ、成功した応答がクライアントに返されます。 これにより、HA切り替え後のデータ損失を防ぎます。 データ同期中にレプリカノード障害またはネットワーク例外が発生した場合、半同期モードは非同期モードに低下します。