DynamoDB ロッククライアントによる分散ロック
従来のロック/取得/解除セマンティクスを必要とするアプリケーションの場合、DynamoDB ロッククライアントは、DynamoDB テーブルをロックストアとして使用して分散ロックを実装するオープンソースライブラリです。このアプローチは、複数のアプリケーションインスタンス間で外部リソース (S3 オブジェクトや共有設定など) へのアクセスを調整する必要がある場合に便利です。
ロッククライアントはオープンソースの Java ライブラリ
仕組み
ロッククライアントは、専用の DynamoDB テーブルを使用してロックを追跡します。各ロックは、次の主要属性を持つ項目として表されます。
ロックされているリソースを識別するパーティションキー。
ロックが有効である期間を指定するリース期間。ロックホルダーがクラッシュするか、応答しなくなった場合、ロックはリース期間後に自動的に期限切れになります。
ロックホルダーがリースを延長するために定期的に送信するハートビート。これにより、ホルダーがまだアクティブに処理している間にロックの有効期限が切れるのを防ぎます。
ロッククライアントは条件付き書き込みを使用して、一度に 1 つのプロセスのみがロックを取得できるようにします。ロックがすでに保持されている場合、呼び出し元は待機して再試行するか、すぐに失敗するかを選択できます。
どのような場合にロッククライアントを使用するか
ロッククライアントは、次の場合に適しています。
複数のアプリケーションインスタンスまたはマイクロサービス間で共有リソースへのアクセスを調整する必要があります。
クリティカルセクションは長時間 (数秒から数分) 実行されるため、競合時にオペレーション全体を再試行するとコストがかかります。
プロセスの失敗を適切に処理するには、自動ロックの有効期限が必要です。
一般的な例としては、分散ワークフローのオーケストレーション、複数のインスタンス間での cron ジョブの調整、共有外部リソースへのアクセスの管理などがあります。
トレードオフ
- 追加のインフラストラクチャ
ロック管理には専用の DynamoDB テーブルが必要で、ロック操作とハートビート用の追加の読み取りおよび書き込み容量も必要です。
- クロック依存性
ロックの有効期限はタイムスタンプに依存します。クライアント間で大幅なクロックスキューが発生すると、特にリース期間が短い場合、予期しない動作が発生する可能性があります。
- デッドロックリスク
アプリケーションが複数のリソースのロックを取得する場合、デッドロックを回避するために、一貫した順序でロックを取得する必要があります。リース期間は、応答しないホルダーからロックを自動的に解放することで、セーフティネットを提供します。
実装
次の例は、DynamoDB ロッククライアントを使用してロックを取得および解除する方法を示しています。
import java.io.IOException; import java.util.Optional; import java.util.concurrent.TimeUnit; import software.amazon.awssdk.services.dynamodb.DynamoDbClient; final DynamoDbClient dynamoDB = DynamoDbClient.builder() .region(Region.US_WEST_2) .build(); final AmazonDynamoDBLockClient lockClient = new AmazonDynamoDBLockClient( AmazonDynamoDBLockClientOptions.builder(dynamoDB, "Locks") .withTimeUnit(TimeUnit.SECONDS) .withLeaseDuration(10L) .withHeartbeatPeriod(3L) .withCreateHeartbeatBackgroundThread(true) .build()); // Try to acquire a lock on a resource final Optional<LockItem> lock = lockClient.tryAcquireLock(AcquireLockOptions.builder("my-shared-resource").build()); if (lock.isPresent()) { try { // Perform operations that require exclusive access processSharedResource(); } finally { // Always release the lock when done lockClient.releaseLock(lock.get()); } } else { System.out.println("Failed to acquire lock."); } lockClient.close();
重要
処理ロジックが例外をスローした場合でもロックが確実に解除されるように、常に finally ブロックでロックを解除します。解除されていないロックは、リースの有効期限が切れるまで他のプロセスをブロックします。
条件付き書き込みを直接使用して、ロッククライアントライブラリを使用せずシンプルなロックメカニズムを実装することもできます。次の例では、条件式を指定した UpdateItem を使用してロックを取得し、DeleteItem を使用してロックを解除します。
from datetime import datetime, timedelta from boto3.dynamodb.conditions import Attr def acquire_lock(table, resource_name, owner_id, ttl_seconds): """Attempt to acquire a lock. Returns True if successful.""" expiry = (datetime.now() + timedelta(seconds=ttl_seconds)).isoformat() now = datetime.now().isoformat() try: table.update_item( Key={'LockID': resource_name}, UpdateExpression='SET #owner = :owner, #expiry = :expiry', ConditionExpression=Attr('LockID').not_exists() | Attr('ExpiresAt').lt(now), ExpressionAttributeNames={'#owner': 'OwnerID', '#expiry': 'ExpiresAt'}, ExpressionAttributeValues={':owner': owner_id, ':expiry': expiry} ) return True except table.meta.client.exceptions.ConditionalCheckFailedException: return False def release_lock(table, resource_name, owner_id): """Release a lock. Only succeeds if the caller is the lock owner.""" try: table.delete_item( Key={'LockID': resource_name}, ConditionExpression=Attr('OwnerID').eq(owner_id) ) return True except table.meta.client.exceptions.ConditionalCheckFailedException: return False
このアプローチでは、条件式を使用して、ロックが存在しないか期限切れの場合にのみロックを取得でき、ロックを取得したプロセスによってのみロックが解除されることを保証します。期限切れのロック項目を自動的にクリーンアップするには、ロックテーブルで有効期限 (TTL) を有効にすることを検討してください。