Gerenciar simultaneidade no armazenamento de Blob

Os aplicativos modernos geralmente têm vários usuários visualizando e atualizando dados simultaneamente. Os desenvolvedores de aplicativos precisam pensar cuidadosamente sobre como fornecer uma experiência previsível para seus usuários finais, especialmente para cenários em que vários usuários podem atualizar os mesmos dados. Há três estratégias principais de simultaneidade de dados que os desenvolvedores normalmente consideram:

  • Simultaneidade otimista: um aplicativo que executa uma atualização determinará, como parte de sua atualização, se os dados foram alterados desde a última vez que o aplicativo leu esses dados. Por exemplo, se dois usuários que visualizam uma página wiki fizerem uma atualização para essa página, a plataforma wiki deve garantir que a segunda atualização não substitua a primeira. Ele também deve garantir que ambos os usuários entendam se a atualização foi bem-sucedida. Esta estratégia é mais frequentemente usada em aplicações web.

  • Simultaneidade pessimista: um aplicativo que deseja executar uma atualização faz um bloqueio em 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 executa atualizações, o primário normalmente mantém um bloqueio exclusivo nos dados por um longo período de tempo para garantir que ninguém mais possa atualizá-los.

  • Last writer wins: uma abordagem que permite que as operações de atualização prossigam sem primeiro determinar se outro aplicativo atualizou os dados desde que foram lidos. Essa abordagem geralmente é usada quando os dados são particionados de tal forma que vários usuários não estão acessando os mesmos dados ao mesmo tempo. Também pode ser útil quando 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 projetado para adotar um modelo de consistência forte que garante que, depois que o serviço executa uma operação de inserção ou atualização, as operações subsequentes de leitura ou lista retornam a atualização mais recente.

Além de selecionar uma estratégia de simultaneidade apropriada, os desenvolvedores também devem estar cientes de como uma plataforma de armazenamento isola as alterações, particularmente as alterações no mesmo objeto nas transações. O Armazenamento do Azure usa o isolamento de instantâneo para permitir operações de leitura simultaneamente com 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 modelos de simultaneidade otimistas ou pessimistas para gerenciar o acesso a blobs e contêineres. Se você não especificar explicitamente uma estratégia, então, por padrão, o último gravador ganha.

Simultaneidade otimista

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

Um cliente que está executando uma atualização pode enviar o ETag original junto com um cabeçalho condicional para garantir que uma atualização só ocorra se uma determinada condição tiver sido atendida. Por exemplo, se o cabeçalho If-Match for especificado, o Armazenamento do Azure verificará se o valor do ETag especificado na solicitação de atualização é o mesmo que o ETag para o objeto que está sendo atualizado. Para obter mais informações sobre cabeçalhos condicionais, consulte Especificando cabeçalhos condicionais para operações de serviço de Blob.

O esboço deste processo é o seguinte:

  1. Recupere um blob do Armazenamento do Azure. A resposta inclui um valor HTTP ETag Header que identifica a versão atual do objeto.
  2. Ao atualizar o blob, inclua o valor ETag recebido na etapa 1 no cabeçalho condicional If-Match da solicitação de gravação. O Armazenamento do Azure compara o valor ETag na solicitação com o valor ETag atual do blob.
  3. Se o valor ETag atual do blob for diferente do valor ETag 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 ao cliente que outro processo atualizou o blob desde que o cliente 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 ETag atual do blob for a mesma versão que o ETag no cabeçalho condicional If-Match na solicitação, o Armazenamento do Azure executará a operação solicitada e atualizará o valor 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 ETag para um blob. O Armazenamento do Azure avalia se o ETag atual do blob é o mesmo que o ETag fornecido na solicitação e executa a operação de gravação somente se os dois valores ETag corresponderem. Se outro processo tiver atualizado o blob nesse ínterim, o Armazenamento do Azure retornará 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, consulte Especificando cabeçalhos condicionais para operações de serviço de Blob.

Simultaneidade pessimista para bolhas

Para bloquear um blob para uso exclusivo, você pode adquirir um contrato de locação sobre ele. Ao adquirir a concessão, você especifica a duração da concessão. Uma locação finita pode ser válida entre 15 e 60 segundos. Um contrato de arrendamento também pode ser infinito, o que equivale a uma fechadura exclusiva. Você pode renovar uma concessão finita para estendê-la e pode liberá-la quando terminar de usá-la. O Armazenamento do Azure libera automaticamente concessões finitas quando elas expiram.

As concessões permitem que diferentes estratégias de sincronização sejam suportadas, incluindo operações exclusivas de gravação/leitura compartilhada, operações exclusivas de gravação/leitura exclusiva e operações de gravação compartilhada/leitura exclusiva. Quando existe uma concessão, o Armazenamento do Azure impõe acesso exclusivo para operações de gravação para o titular da concessão. No entanto, garantir a exclusividade para operações de leitura exige que o desenvolvedor se certifique de 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 um 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 o 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 contentores

As concessões em contêineres permitem as mesmas estratégias de sincronização suportadas para 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 é aplicado apenas em operações de exclusão. Para excluir um contêiner com uma concessão ativa, um cliente deve incluir a ID de concessão ativa com a solicitação de exclusão. Todas as outras operações de contêiner são bem-sucedidas em um contêiner alugado sem o ID de locação.

Próximos passos

Recursos

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