管理 Blob 儲存體中的並行存取

新式應用程式通常會有多位使用者同時查看和更新資料。 應用程式開發人員需要認真思考如何為其使用者提供可預測的使用經驗,尤其是有多個使用者可更新相同資料的案例。 開發人員通常會考量三個主要的資料並行存取策略:

  • 開放式並行存取:執行更新的應用程式將在其更新的過程中,驗證在應用程式上次讀取資料後,該資料是否有所變更。 例如,如果兩個使用者看到 wiki 頁面對該頁面進行更新,則 wiki 平臺必須確保第二個更新不會覆寫第一個更新。 它也必須確保這兩個使用者都能瞭解其更新是否成功。 此策略最常用在 Web 應用程式中。

  • 封閉式並行存取:要執行更新的應用程式會鎖定物件以防止其他使用者更新資料,直到鎖定解除為止。 例如,在只有主要複本執行更新的主要/次要資料複寫案例中,主要複本通常會保留資料的獨佔鎖定一段長時間,以確保沒有其他人可以更新它。

  • 最後寫入者獲勝:一種方法,可讓更新作業繼續進行,而不需要先判斷是否有其他應用程式在讀取之後更新資料。 當資料分割時,通常會使用這種方法,讓多個使用者不會同時存取相同的資料。 在處理暫時性資料串流的情況中,也可以利用此策略。

Azure 儲存體支援這三個策略,雖然它的功能是提供開放式和封閉式平行存取的完整支援。 Azure 儲存體的設計目的是採用強式一致性模型,以保證在服務執行插入或更新作業之後,後續的讀取或列出作業會傳回最新的更新。

除了選取適當的並行存取策略以外,開發人員也應注意儲存體平台隔離變更的方式,尤其是在不同交易間對相同物件的變更。 Azure 儲存體會使用快照隔離,讓讀取作業與寫入作業在單一資料分割內同時執行。 快照集隔離可保證即使在進行更新時,所有讀取作業仍會傳回一致的資料快照集。

您可以選擇使用開放式或封閉式並行存取模型,以管理如何存取 Blob 和容器。 如果您未明確指定策略,則預設為最後一個寫入器獲勝。

開放式並行存取

Azure 儲存體會為每個儲存的物件指派識別碼。 此識別碼會在每次對物件執行更新作業時更新。 此識別碼會使用在 HTTP 通訊協定內定義的 ETag 標頭,隨附在 HTTP GET 回應中傳回至用戶端。

執行更新的用戶端可以將原始 ETag 與條件式標頭一起傳送,以確保只有在符合特定條件時才會進行更新。 例如,如果指定了if-match標頭,Azure 儲存體會確認更新要求中指定的 etag 值與要更新之物件的 etag 相同。 如需使用條件式標頭的詳細資訊,請參閱指定 Blob 服務作業的條件式標頭

此程序大致如下:

  1. 從 Azure 儲存體擷取 Blob 回應包含 HTTP ETag 標頭值,可識別物件的目前版本。
  2. 當您更新 Blob 時,請將您在步驟 1 中接收到的 ETag 值納入要求的 If-Match 條件式標頭中。 Azure 儲存體會比較要求中的 ETag 值與 Blob 目前的 ETag 值。
  3. 如果 Blob 目前的 ETag 值與要求中所提供之if-match條件標頭中指定的 ETag 值不同,則 Azure 儲存體會傳回 HTTP 狀態碼 412 (前置條件失敗)。 這錯誤會向用戶端指出在用戶端先擷取 Blob 後,有另一個程序更新過該 Blob。 用戶端應該再次擷取 Blob,以取得更新的內容和屬性。
  4. 如果 Blob 目前的 ETag 值與要求中的 If-Match 條件式標頭內的 ETag 是相同版本,Azure 儲存體將會執行要求的作業,並更新 Blob 目前的 ETag 值。

下列程式碼範例示範如何在檢查 blob ETag 值的寫入要求上,建立 if-match 條件。 Azure 儲存體會評估 blob 的目前 ETag 是否與要求所提供的 etag 相同,而且只有在兩個 ETag 值相符時,才會執行寫入作業。 如果有其他程序在中間已更新 Blob,Azure 儲存體將會傳回 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 儲存體也支援其他條件式標頭,包括如同是否已修改-自和- None 相符。 如需詳細資訊,請參閱指定 Blob 服務作業的條件式標頭

Blob 的封閉式並行存取

若要鎖定 Blob 以進行獨佔使用,您可以取得其租用。 當您取得租用時,您會指定租用的持續時間。 有限租用的有效時間可能介於 15 到 60 秒之間。 租用也可以是無限的,其數量為獨佔鎖定。 您可以更新有限租用而加以延伸,而且您可以在用完任何租用後加以釋放。 當有限租用到期時,Azure 儲存體會自動釋放租用。

租用可讓不同的同步處理策略獲得支援,包括獨佔寫入/共用讀取作業、獨佔寫入/獨佔讀取作業和共用寫入/獨佔讀取作業。 當租用存在時,Azure 儲存體會對租用持有者強制執行寫入作業的獨佔存取權。 不過,若要確保讀取作業的獨佔性,開發人員必須確保所有用戶端應用程式都使用租用識別碼,且一次只有一個用戶端具有有效的租用識別碼。 未包含租用識別碼的讀取作業將會導致共用讀取。

下列程式碼範例示範如何取得 blob 的獨佔租用、提供租用識別碼以更新 blob 的內容,然後釋放租用。 如果租用處於作用中狀態,且寫入要求未提供租用識別碼,則寫入作業會失敗,錯誤碼為 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();
    }
}

容器的封閉式並行存取

容器租用可支援啟用相同的處理策略獲得,包括獨佔寫入/共用讀取、獨佔寫入/獨佔讀取和共用寫入/獨佔讀取。 但是針對容器,只會在刪除作業上強制執行獨佔鎖定。 若要刪除具有作用中租用的容器,用戶端必須使用刪除要求納入作用中的租用識別碼。 在未包含租用識別碼的情況下,所有其他容器作業都可以在租用的容器上成功執行。

下一步

資源

如需使用已被取代之 .NET 11.x 版 SDK 的相關程式碼範例,請參閱使用 .NET 11.x 版的程式碼範例