Gerir a Simultaneidade no Armazenamento de Blobs

Muitas vezes, as aplicações modernas têm vários utilizadores a visualizar e a atualizar dados em simultâneo. Os programadores de aplicações têm de pensar cuidadosamente sobre como fornecer uma experiência previsível aos seus utilizadores finais, especialmente para cenários em que vários utilizadores podem atualizar os mesmos dados. Existem três estratégias de simultaneidade de dados principais que os programadores normalmente consideram:

  • Simultaneidade otimista: uma aplicação que executa uma atualização irá, como parte da atualização, determinar se os dados foram alterados desde a última vez que a aplicação leu esses dados. Por exemplo, se dois utilizadores que visualizam uma página wiki fizerem uma atualização para essa página, a plataforma wiki tem de garantir que a segunda atualização não substitui a primeira atualização. Tem também de garantir que ambos os utilizadores compreendem se a atualização foi efetuada com êxito. Esta estratégia é utilizada mais frequentemente em aplicações Web.

  • Simultaneidade pessimista: uma aplicação que pretenda efetuar uma atualização bloqueia um objeto que impede outros utilizadores de atualizar os dados até que o bloqueio seja lançado. Por exemplo, num cenário de replicação de dados primário/secundário no qual apenas o principal executa atualizações, o principal normalmente contém um bloqueio exclusivo nos dados durante um longo período de tempo para garantir que mais ninguém pode atualizá-lo.

  • O último escritor ganha: uma abordagem que permite que as operações de atualização prossigam sem determinar primeiro se outra aplicação atualizou os dados desde que foi lida. Normalmente, esta abordagem é utilizada quando os dados são particionados de forma a que vários utilizadores não acedam aos mesmos dados ao mesmo tempo. Também pode ser útil onde os fluxos de dados de curta duração estão a ser processados.

O Armazenamento do Azure suporta as três estratégias, embora seja distinto na sua capacidade de fornecer suporte total para simultaneidade otimista e pessimista. O Armazenamento do Azure foi concebido para adotar um modelo de consistência forte que garante que, após o serviço realizar uma operação de inserção ou atualização, as operações de leitura ou lista subsequentes devolvem a atualização mais recente.

Além de selecionar uma estratégia de simultaneidade adequada, os programadores também devem estar cientes de como uma plataforma de armazenamento isola as alterações, particularmente as alterações ao mesmo objeto entre transações. O Armazenamento do Azure utiliza o isolamento de instantâneos para permitir operações de leitura em simultâneo com operações de escrita numa única partição. O isolamento de instantâneos garante que todas as operações de leitura devolvem um instantâneo consistente dos dados, mesmo quando as atualizações estão a ocorrer.

Pode optar por utilizar modelos de simultaneidade otimistas ou pessimistas para gerir o acesso a blobs e contentores. Se não especificar explicitamente uma estratégia, o último escritor ganha por predefinição.

Simultaneidade otimista

O Armazenamento do Azure atribui um identificador a todos os objetos armazenados. Este identificador é atualizado sempre que uma operação de escrita é efetuada num objeto. O identificador é devolvido ao cliente como parte de uma resposta HTTP GET no cabeçalho ETag definido pelo protocolo HTTP.

Um cliente que está a executar uma atualização pode enviar o ETag original juntamente com um cabeçalho condicional para garantir que uma atualização só ocorre se uma determinada condição tiver sido cumprida. Por exemplo, se o cabeçalho If-Match for especificado, o Armazenamento do Azure verifica se o valor do ETag especificado no pedido de atualização é o mesmo que o ETag para o objeto que está a ser atualizado. Para obter mais informações sobre cabeçalhos condicionais, veja Especificar cabeçalhos condicionais para operações do serviço Blob.

O destaque deste processo é o seguinte:

  1. Obter um blob do Armazenamento do Azure. A resposta inclui um valor cabeçalho http ETag que identifica a versão atual do objeto.
  2. Quando atualizar o blob, inclua o valor ETag que recebeu no passo 1 no cabeçalho condicional If-Match do pedido de escrita. O Armazenamento do Azure compara o valor ETag no pedido com o valor ETag atual do blob.
  3. Se o valor ETag atual do blob diferir do valor ETag especificado no cabeçalho condicional If-Match fornecido no pedido, o Armazenamento do Azure devolve o código de estado HTTP 412 (Falha na Pré-condição). Este erro indica ao cliente que outro processo atualizou o blob desde que o cliente o obteve pela primeira vez. O cliente deve obter o blob novamente para obter os conteúdos e propriedades atualizados.
  4. Se o valor ETag atual do blob for a mesma versão do ETag no cabeçalho condicional If-Match no pedido, o Armazenamento do Azure efetua a operação pedida e atualiza o valor ETag atual do blob.

Os exemplos de código seguintes mostram como construir uma condição If-Match no pedido de escrita que verifica o valor ETag de um blob. O Armazenamento do Azure avalia se o ETag atual do blob é o mesmo que o ETag fornecido no pedido e executa a operação de escrita apenas se os dois valores ETag corresponderem. Se outro processo tiver atualizado o blob entretanto, o Armazenamento do Azure devolve uma mensagem de estado HTTP 412 (Falha na Pré-condição).

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);
}

O Armazenamento do Azure também suporta outros cabeçalhos condicionais, incluindo como If-Modified-Since, If-Unmodified-Since e If-None-Match. Para obter mais informações, veja Especificar Cabeçalhos Condicionais para Operações do Serviço blob.

Simultaneidade pessimista para blobs

Para bloquear um blob para utilização exclusiva, pode adquirir uma concessão. Quando adquirir a concessão, especifique a duração da concessão. Uma concessão finita pode ser válida entre 15 e 60 segundos. Uma concessão também pode ser infinita, o que equivale a um bloqueio exclusivo. Pode renovar uma concessão finita para a prolongar e pode libertar a concessão quando terminar a concessão. O Armazenamento do Azure lança automaticamente concessões finitas quando expiram.

As concessões permitem que sejam suportadas diferentes estratégias de sincronização, incluindo operações exclusivas de escrita/leitura partilhada, operações exclusivas de escrita/leitura exclusiva e operações de escrita/leitura exclusivas partilhadas. Quando existe uma concessão, o Armazenamento do Azure impõe acesso exclusivo às operações de escrita para o proprietário do contrato de concessão. No entanto, garantir a exclusividade das operações de leitura requer que o programador garanta que todas as aplicações cliente utilizam um ID de concessão e que apenas um cliente de cada vez tem um ID de concessão válido. Operações de leitura que não incluem um ID de concessão resultam em leituras partilhadas.

Os exemplos de código seguintes mostram como adquirir uma concessão exclusiva num blob, atualizar o conteúdo do blob ao fornecer o ID de concessão e, em seguida, libertar a concessão. Se a concessão estiver ativa e o ID de concessão não for fornecido num pedido de escrita, a operação de escrita falhará com o código de erro 412 (Falha na Pré-condição).

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();
    }
}

Simultaneidade pessimista para contentores

As concessões em contentores permitem as mesmas estratégias de sincronização que são suportadas para blobs, incluindo escrita/leitura partilhada exclusiva, escrita exclusiva/leitura exclusiva e escrita partilhada/leitura exclusiva. No entanto, para os contentores, o bloqueio exclusivo é imposto apenas nas operações de eliminação. Para eliminar um contentor com uma concessão ativa, um cliente tem de incluir o ID de concessão ativo com o pedido de eliminação. Todas as outras operações de contentor têm êxito num contentor arrendado sem o ID de concessão.

Passos seguintes

Recursos

Para exemplos de código relacionados com SDKs .NET preteridos versão 11.x, veja Exemplos de código com a versão 11.x do .NET.