BLOB ストレージ内でのコンカレンシーの管理

最新のアプリケーションでは、複数のユーザーが同時にデータを表示し、更新することがよくあります。 アプリケーション開発者は、予測可能なエクスペリエンスをエンド ユーザーに提供する方法を注意深く検討する必要があります。特に、複数のユーザーが同じデータを更新できる場合はこれが重要になります。 開発者が通常検討するデータのコンカレンシー制御戦略には、主に次の 3 つがあります。

  • オプティミスティック コンカレンシー: アプリケーションがデータを更新する場合、更新処理の一部として、データがそのアプリケーションに最後に読み込まれた後に更新されていないか確認します。 たとえば、wiki のページを表示している 2 人のユーザーが同じページを更新しようとしている場合、wiki のプラットフォームは、2 番目の更新が最初の更新を上書きしないようにする必要があります。 また、両方のユーザーが自身の更新の成否を把握できるようにする必要もあります。 この戦略は、Web アプリケーションで最も広く使用されています。

  • ペシミスティック コンカレンシー制御: 更新を実行しようとするアプリケーションがオブジェクトをロックし、ロックが解除されるまで他のユーザーがデータを更新できないようにします。 たとえば、プライマリとセカンダリの間でデータを複製する場合、プライマリのみが更新を実行するため、通常は、プライマリがデータを一定期間、排他的にロックして、該当するデータを他から更新できないようにします。

  • 最終書き込み者優先: データの読み取り後に別のアプリケーションがそのデータを更新したかどうかを最初に確認せずに、更新操作を続行できるようにする方法です。 この方法は、通常、複数のユーザーが同時に同じデータにアクセスしないように、データをパーティション分割する場合に使用します。 また、有効期限が短いデータ ストリームの処理にも有効です。

Azure Storage によって 3 つの方法すべてがサポートされていますが、オプティミスティックとペシミスティックのコンカレンシーの完全なサポートを提供できるという特徴があります。 Azure Storage は、サービスで挿入または更新操作を実行した後、後続の読み取りまたは一覧表示操作によって最新の更新が返されることを保証する、強力な整合性モデルを採用して設計されています。

また、開発者は、適切なコンカレンシー戦略を選択する以外にも、特に複数のトランザクションで同一のオブジェクトを変更する場合に、ストレージ プラットフォームで変更がどのように分離されるかについて把握しておく必要があります。 Azure Storage にはスナップショット分離が使用されており、同一パーティション内での読み取り処理と書き込み処理を同時に実行できるようになっています。 スナップショット分離では、更新が行われている間でも、すべての読み取り操作がデータの一貫したスナップショットを返すことが保証されます。

BLOB およびコンテナーへのアクセスを管理する場合、オプティミスティック コンカレンシーとペシミスティック コンカレンシーのいずれかのモデルを使用できます。 明示的に戦略を指定しない場合は、最終書き込み者優先が既定となります。

オプティミスティック コンカレンシー

Azure Storage では、格納されているすべてのオブジェクトに識別子が割り当てられます。 オブジェクトで書き込み処理が実行されるたびに、この識別子が更新されます。 この識別子は、HTTP プロトコルで定義されている ETag ヘッダーを使用して、HTTP GET 応答の一部としてクライアントに返されます。

更新を実行しているクライアントは、元の ETag を条件ヘッダーとともに送信して、特定の条件が満たされた場合にのみ更新が行われるようにすることができます。 たとえば、If-Match ヘッダーが指定されている場合は、更新要求で指定された ETag の値が更新されるオブジェクトの ETag と同じであることを、Azure Storage で検証します。 条件ヘッダーの詳細については、「Blob service 操作の条件ヘッダーの指定」を参照してください。

このプロセスの概要は次のとおりです。

  1. Azure Storage から BLOB を取得します。 この応答に含まれる HTTP ETag ヘッダーの値が、オブジェクトの現在のバージョンを示します。
  2. BLOB を更新するときに、手順 1. で受け取った、書き込み要求の If-Match 条件ヘッダーで返された ETag の値を含めます。 Azure Storage によって、要求内の ETag の値と BLOB の現在の ETag の値が比較されます。
  3. BLOB の現在の ETag 値が、要求で指定された If-Match 条件ヘッダーで指定された ETag 値と異なる場合、Azure Storage は HTTP 状態コード 412 (必須条件に失敗) を返します。 このエラーは、クライアントが最初にこの BLOB を取得した後に、別のプロセスが更新したことを示しています。 クライアントは、更新されたコンテンツとプロパティを取得するために BLOB をもう一度フェッチする必要があります。
  4. BLOB の現在の ETag の値が、要求の If-Match 条件ヘッダーの ETag と同じバージョンである場合、Azure Storage は要求された処理を実行し、この BLOB の現在の ETag の値を更新します。

次のコード例は、BLOB の ETag 値をチェックする書き込み要求に対して If-Match 条件を構築する方法を示しています。 Azure Storage は、BLOB の現在の ETag が要求で提供される ETag と同じであるかどうかを評価し、2 つの ETag 値が一致する場合にのみ書き込み操作を実行します。 その間に別のプロセスが BLOB を更新した場合、Azure Storage によって HTTP 412 (必須条件に失敗) のステータス メッセージが返されます。

private static async Task DemonstrateOptimisticConcurrencyBlob(BlobClient blobClient)
{
    Console.WriteLine("Demonstrate optimistic concurrency");

    try
    {
        // Download a blob
        Response<BlobDownloadResult> response = await blobClient.DownloadContentAsync();
        BlobDownloadResult downloadResult = response.Value;
        string blobContents = downloadResult.Content.ToString();

        ETag originalETag = downloadResult.Details.ETag;
        Console.WriteLine("Blob ETag = {0}", originalETag);

        // This function simulates an external change to the blob after we've fetched it
        // The external change updates the contents of the blob and the ETag value
        await SimulateExternalBlobChangesAsync(blobClient);

        // Now try to update the blob using the original ETag value
        string blobContentsUpdate2 = $"{blobContents} Update 2. If-Match condition set to original ETag.";

        // Set the If-Match condition to the original ETag
        BlobUploadOptions blobUploadOptions = new()
        {
            Conditions = new BlobRequestConditions()
            {
                IfMatch = originalETag
            }
        };

        // This call should fail with error code 412 (Precondition Failed)
        BlobContentInfo blobContentInfo =
            await blobClient.UploadAsync(BinaryData.FromString(blobContentsUpdate2), blobUploadOptions);
    }
    catch (RequestFailedException e) when (e.Status == (int)HttpStatusCode.PreconditionFailed)
    {
        Console.WriteLine(
            @"Blob's ETag does not match ETag provided. Fetch the blob to get updated contents and properties.");
    }
}

private static async Task SimulateExternalBlobChangesAsync(BlobClient blobClient)
{
    // Simulates an external change to the blob for this example

    // Download a blob
    Response<BlobDownloadResult> response = await blobClient.DownloadContentAsync();
    BlobDownloadResult downloadResult = response.Value;
    string blobContents = downloadResult.Content.ToString();

    // Update the existing block blob contents
    // No ETag condition is provided, so original blob is overwritten and ETag is updated
    string blobContentsUpdate1 = $"{blobContents} Update 1";
    BlobContentInfo blobContentInfo =
        await blobClient.UploadAsync(BinaryData.FromString(blobContentsUpdate1), overwrite: true);
    Console.WriteLine("Blob update. Updated ETag = {0}", blobContentInfo.ETag);
}

Azure Storage は、If-Modified-SinceIf-Unmodified-SinceIf-None-Match など、他の条件ヘッダーもサポートします。 詳細については、「BLOB サービス操作の条件ヘッダーの指定」を参照してください。

BLOB でのペシミスティック コンカレンシー制御

BLOB をロックして排他的に使用する場合は、リースを取得します。 リースを取得するときは、リース期間を指定します。 有限リースは、15 - 60 秒の範囲で有効な場合があります。 リース期間は無限の場合もあり、その場合は排他ロックになります。 リース期間が有限の場合、延長することができます。また、完了したリースは解放できます。 期限が切れた有限のリースは、Azure Storage によって自動的に解放されます。

リースでは、排他的書き込みと共有読み取りの操作、排他的書き込みと排他的読み取りの操作、共有書き込みと排他的読み取りの操作など、さまざまな同期戦略がサポートされています。 リースが存在する場合、Azure Storage はリース所有者の書き込み操作に排他的アクセスを適用します。 ただし、読み込み操作の排他性を保証するために、開発者は、すべてのクライアント アプリケーションがリース ID を使用し、また有効なリース ID は同時に 1 つのクライアントのみが保持するようにする必要があります。 読み込み操作にリース ID を使用しない場合、共有読み取りになります。

次のコード例では、BLOB に対して排他的リースを取得し、リース ID を指定して BLOB の内容を更新してから、リースを解放する方法を示します。 リースがアクティブで、書き込み要求でリース ID が指定されていない場合、その書き込み操作は失敗し、エラーコード 412 (必須条件に失敗) が表示されます。

public static async Task DemonstratePessimisticConcurrencyBlob(BlobClient blobClient)
{
    Console.WriteLine("Demonstrate pessimistic concurrency");

    BlobContainerClient containerClient = blobClient.GetParentBlobContainerClient();
    BlobLeaseClient blobLeaseClient = blobClient.GetBlobLeaseClient();

    try
    {
        // Create the container if it does not exist.
        await containerClient.CreateIfNotExistsAsync();

        // Upload text to a blob.
        string blobContents1 = "First update. Overwrite blob if it exists.";
        byte[] byteArray = Encoding.ASCII.GetBytes(blobContents1);
        using (MemoryStream stream = new MemoryStream(byteArray))
        {
            BlobContentInfo blobContentInfo = await blobClient.UploadAsync(stream, overwrite: true);
        }

        // Acquire a lease on the blob.
        BlobLease blobLease = await blobLeaseClient.AcquireAsync(TimeSpan.FromSeconds(15));
        Console.WriteLine("Blob lease acquired. LeaseId = {0}", blobLease.LeaseId);

        // Set the request condition to include the lease ID.
        BlobUploadOptions blobUploadOptions = new BlobUploadOptions()
        {
            Conditions = new BlobRequestConditions()
            {
                LeaseId = blobLease.LeaseId
            }
        };

        // Write to the blob again, providing the lease ID on the request.
        // The lease ID was provided, so this call should succeed.
        string blobContents2 = "Second update. Lease ID provided on request.";
        byteArray = Encoding.ASCII.GetBytes(blobContents2);

        using (MemoryStream stream = new MemoryStream(byteArray))
        {
            BlobContentInfo blobContentInfo = await blobClient.UploadAsync(stream, blobUploadOptions);
        }

        // This code simulates an update by another client.
        // The lease ID is not provided, so this call fails.
        string blobContents3 = "Third update. No lease ID provided.";
        byteArray = Encoding.ASCII.GetBytes(blobContents3);

        using (MemoryStream stream = new MemoryStream(byteArray))
        {
            // This call should fail with error code 412 (Precondition Failed).
            BlobContentInfo blobContentInfo = await blobClient.UploadAsync(stream);
        }
    }
    catch (RequestFailedException e)
    {
        if (e.Status == (int)HttpStatusCode.PreconditionFailed)
        {
            Console.WriteLine(
                @"Precondition failure as expected. The lease ID was not provided.");
        }
        else
        {
            Console.WriteLine(e.Message);
            throw;
        }
    }
    finally
    {
        await blobLeaseClient.ReleaseAsync();
    }
}

コンテナーでのペシミスティック コンカレンシー制御

コンテナーのリースでは、排他的書き込みと共有読み取り、排他的書き込みと排他的読み取り、共有書き込みと排他的読み取りなど、BLOB でサポートされるのと同じ同期戦略がサポートされます。 ただし、コンテナーの場合、排他的ロックは削除操作にのみ適用されます。 アクティブなリースを使用してコンテナーを削除するには、クライアントが削除要求に有効なリース ID を含める必要があります。 他のコンテナー操作については、リース ID なしでも、リースされたコンテナーで成功します。

次のステップ

リソース

非推奨の .NET バージョン 11.x SDK を使用する関連コード サンプルについて、詳しくは「.NET バージョン 11.x を使用したコード サンプル」をご覧ください。