Gerenciando Simultaneidade em Armazenamento de Blobs

Os aplicativos modernos consistem normalmente em vários usuários exibindo e atualizando dados de modo simultâneo. Os desenvolvedores de aplicativos precisam pensar cuidadosamente sobre como fornecer uma experiência previsível para os usuários finais, especialmente para cenários em que vários usuários podem atualizar os mesmos dados. Existem três estratégias de simultaneidade de dados principais que os desenvolvedores normalmente consideram:

  • Simultaneidade otimista: um aplicativo realizando uma atualização determinará, como parte da atualização, se os dados foram alterados desde a última vez que o aplicativo leu tais dados. Por exemplo, se dois usuários que visualizam uma página wiki fizerem uma atualização nessa página, a plataforma wiki deverá garantir que a segunda atualização não substitua a primeira. Também deverá ser garantido que ambos os usuários entendam se a atualização foi bem-sucedida. Essa estratégia é usada com mais frequência em aplicativos Web.

  • Simultaneidade pessimista: Um aplicativo procurando realizar uma atualização bloqueará um objeto, impedindo que outros usuários atualizem os dados até que o bloqueio seja liberado. Por exemplo, em um cenário de replicação de dados primário/secundário em que apenas o primário realizará atualizações, normalmente o primário manterá um bloqueio exclusivo nos dados por um período prolongado nos dados para garantir que nenhuma outra pessoa possa atualizá-los.

  • Último gravador vence: uma abordagem que permite que as operações de atualização continuem sem primeiro determinar se outro aplicativo atualizou os dados desde que foram lidos. Normalmente, essa abordagem é usada quando os dados são particionados de modo que vários usuários não acessem os mesmos dados ao mesmo tempo. Ela também pode ser útil em locais em que fluxos de dados de curta duração estão sendo processados.

O Armazenamento do Azure dá suporte às três estratégias, embora seja distintivo na capacidade de fornecer suporte completo para simultaneidade otimista e pessimista. O Armazenamento do Azure foi projetado para adotar um modelo de consistência forte que garante que, depois que o serviço executar uma operação de inserção ou atualização, as operações de leitura ou lista subsequentes retornarão a atualização mais recente.

Além de selecionar uma estratégia de simultaneidade adequada, os desenvolvedores também devem estar cientes de como uma plataforma de armazenamento isola as alterações, especialmente as alterações no mesmo objeto entre as transações. O Armazenamento do Azure usa o isolamento de instantâneo para permitir que operações de leitura simultaneamente às operações de gravação em uma única partição. O isolamento de instantâneo garante que todas as operações de leitura retornem um instantâneo consistente dos dados mesmo enquanto as atualizações estão ocorrendo.

Você pode optar por usar os modelos de simultaneidade otimista ou pessimista para gerenciar o acesso a blobs e contêineres. Se você não especificar uma estratégia explicitamente, por padrão, o último gravador vencerá.

Simultaneidade otimista

O Armazenamento do Azure atribui um identificador a todo objeto armazenado. Esse identificador é atualizado sempre que uma operação de gravação é realizada em um objeto. O identificador é retornado para o cliente como parte de uma resposta de HTTP GET usando o cabeçalho ETag definido no protocolo HTTP.

Um cliente que está executando uma atualização pode enviar a ETag original junto com um cabeçalho condicional para garantir que uma atualização ocorre somente se uma determinada condição for atendida. Por exemplo, se o cabeçalho If-Match for especificado, o Armazenamento do Azure verificará se o valor da ETag especificado na solicitação de atualização é o mesmo que a ETag para o objeto que está sendo atualizado. Para saber mais sobre como usar cabeçalhos condicionais, consulte Especificar cabeçalhos condicionais para operações de serviço Blob.

A estrutura desse processo é a seguinte:

  1. Recuperar um blob do Armazenamento do Azure. A resposta inclui um valor de cabeçalho HTTP ETag que identifica a versão atual do objeto.
  2. Ao atualizar o blob, inclua o valor da ETag recebido na etapa 1 no cabeçalho condicional If-Match da solicitação de gravação. O Armazenamento do Azure compara o valor da ETag na solicitação com o valor da ETag atual do blob.
  3. Se o valor da ETag atual do blob for diferente daquele especificado no cabeçalho condicional If-Match fornecido na solicitação, o Armazenamento do Azure retornará o código de status HTTP 412 (falha na pré-condição). Esse erro indica para o cliente que outro processo atualizou o blob desde que ele o recuperou pela primeira vez. O cliente deve buscar o blob novamente para obter o conteúdo e as propriedades atualizados.
  4. Se o valor atual da ETag do blob for da mesma versão que a ETag no cabeçalho condicional If-Match na solicitação, o Armazenamento do Azure realizará a operação solicitada e atualizará o valor da ETag atual do blob.

Os exemplos de código a seguir mostram como construir uma condição If-Match na solicitação de gravação que verifica o valor da ETag para um blob. O Armazenamento do Azure avalia se a ETag atual do blob é a mesma que a ETag fornecida na solicitação e executa a operação de gravação somente se os dois valores de ETag correspondem. Se outro processo atualizou o blob nesse intervalo, o Armazenamento do Azure retorna uma mensagem de status 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 dá suporte a outros cabeçalhos condicionais, incluindo If-Modified-Since, If-Unmodified-Since e If-None-Match. Para obter mais informações, confira Como especificar cabeçalhos condicionais para operações de serviço Blob.

Simultaneidade pessimista para blobs

Para bloquear um blob para uso exclusivo, é possível obter uma concessão sobre ele. Ao adquirir a concessão, você especifica 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, que significa um bloqueio exclusivo. Você pode renovar uma concessão finita para estendê-la e pode liberar a concessão quando terminar de trabalhar com ela. O Armazenamento do Azure libera as concessões finitas automaticamente quando elas expiram.

As concessões permitem que diferentes estratégias de sincronização tenham suporte, incluindo operações de gravação exclusiva/leitura compartilhada, gravação exclusiva/leitura exclusiva e gravação compartilhada/leitura exclusiva. Quando existe uma concessão, o Armazenamento do Azure impõe o acesso exclusivo às operações de gravação para o titular da concessão. No entanto, garantir a exclusividade para operações de leitura requer que o desenvolvedor garanta que todos os aplicativos cliente usem uma ID de concessão e que apenas um cliente de cada vez tenha uma ID de concessão válida. As operações de leitura que não incluem uma ID de concessão resultam em leituras compartilhadas.

Os exemplos de código a seguir mostram como adquirir uma concessão exclusiva em um blob, atualizar o conteúdo do blob fornecendo a ID de concessão e, em seguida, liberar a concessão. Se a concessão estiver ativa e a ID da concessão não for fornecida em uma solicitação de gravação, a operação de gravação 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 contêineres

As concessões em contêineres permitem as mesmas estratégias de sincronização com suporte nos blobs, incluindo gravação exclusiva/leitura compartilhada, gravação exclusiva/leitura exclusiva e gravação compartilhada/leitura exclusiva. Para contêineres, no entanto, o bloqueio exclusivo é imposto somente em operações de exclusão. Para excluir um contêiner com uma concessão ativa, o cliente deve incluir a ID da concessão ativa com a solicitação de exclusão. Todas as outras operações de contêiner são realizadas com êxito em um contêiner concedido sem a ID da concessão.

Próximas etapas

Recursos

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