Administrar la simultaneidad en almacenamiento de blobs

A menudo, las aplicaciones modernas tienen varios usuarios que ven y actualizan datos simultáneamente. Los desarrolladores de aplicaciones deben pensar detenidamente cómo proporcionar una experiencia predecible a sus usuarios finales, especialmente para escenarios en los que varios usuarios pueden actualizar los mismos datos. Hay tres estrategias principales de simultaneidad de datos que normalmente tienen en cuenta los desarrolladores:

  • Simultaneidad optimista: una aplicación que realiza una actualización determinará, como parte de dicha actualización, si los datos han cambiado desde la última vez que los leyó. Por ejemplo, si dos usuarios que ven una página wiki realizan una actualización en la misma página, la plataforma wiki deberá asegurarse de que la segunda actualización no sobrescriba la primera. y que ambos usuarios comprenden si su actualización se realizó correctamente. Esta estrategia se usa con más frecuencia en aplicaciones web.

  • Simultaneidad pesimista: una aplicación que pretende realizar una actualización realizará un bloqueo en un objeto evitando que otros usuarios actualicen los datos hasta que el bloqueo se libere. Por ejemplo, en un escenario de replicación de datos principal/secundario, donde solamente el principal realiza actualizaciones, el principal normalmente mantendrá un bloqueo exclusivo en los datos durante un período de tiempo extendido para garantizar que ninguna otra persona puede actualizarlos.

  • Últimos casos de escritura correcta: un enfoque que permite que las operaciones de actualización continúen sin determinar primero si otra aplicación actualizó los datos desde que se leyeron. Este enfoque se suele utilizar cuando se hacen particiones de los datos de forma que varios usuarios no tengan acceso a ellos al mismo tiempo. También puede resultar útil donde se procesen transmisiones de datos de corta duración.

Azure Storage admite las tres estrategias, aunque se distingue por su capacidad para proporcionar soporte completo para la simultaneidad optimista y pesimista. Azure Storage se diseñó para adoptar un modelo de coherencia sólido que garantiza que, después de que el servicio realice una operación de inserción o actualización, las operaciones de lectura o lista posteriores devuelven la actualización más reciente.

Además de seleccionar una estrategia de simultaneidad adecuada, los desarrolladores también deben saber cómo una plataforma de almacenamiento aísla los cambios, especialmente aquellos del mismo objeto en todas las transacciones. Azure Storage usa el aislamiento de instantáneas para permitir que las operaciones de lectura tengan lugar simultáneamente con operaciones de escritura dentro de una sola partición. El aislamiento de instantáneas garantiza que todas las operaciones de lectura devuelvan una instantánea coherente de los datos incluso mientras se realicen actualizaciones.

Puede optar por usar modelos de simultaneidad optimista o pesimista para administrar el acceso a los blobs y los contenedores. Si no especifica explícitamente ninguna estrategia, de manera predeterminada la última escritura será la correcta.

Simultaneidad optimista

Azure Storage asigna un identificador a cada objeto almacenado. Este se actualiza cada vez que se realiza una operación de escritura en un objeto. El identificador se devuelve al cliente como parte de una respuesta HTTP GET en el encabezado ETag que se define mediante el protocolo HTTP.

Un cliente que realiza una actualización puede enviar la etiqueta ETag original junto con un encabezado condicional a fin de garantizar que solo se realizará una actualización si se cumple una determinada condición. Por ejemplo, si se especifica el encabezado If-Match, Azure Storage verificará que el valor de ETag especificado en la solicitud de actualización sea el mismo que el de la etiqueta de entidad del objeto que se está actualizando. Para obtener más información sobre los encabezados condicionales, consulte Especificación de encabezados condicionales para las operaciones de Blob service.

El esquema de este proceso es el siguiente:

  1. Recupere un blob de Azure Storage. La respuesta incluye un valor de encabezado ETag HTTP que identifica la versión actual del objeto.
  2. Cuando actualice el blob, incluya el valor de ETag recibido en el paso 1 en el encabezado condicional If-Match de la solicitud de escritura. Azure Storage comparará el valor de ETag de la solicitud con el valor de ETag actual del blob.
  3. Si el valor de ETag actual del blob difiere del valor ETag especificado en el encabezado condicional If-Match proporcionado en la solicitud, Azure Storage devolverá el código de estado HTTP 412 (error de condición previa). Este error indica al cliente que otro proceso ha actualizado el blob desde que el cliente lo recuperó por primera vez. El cliente debería recuperar el blob de nuevo para obtener el contenido y las propiedades actualizados.
  4. Si la versión del valor de ETag actual del blob es la misma que la del valor de ETag del encabezado condicional If-Match de la solicitud, Azure Storage realizará la operación solicitada y actualizará el valor de ETag actual del blob.

En los siguientes ejemplos de código se muestra cómo construir una condición If-Match en la solicitud de escritura que compruebe el valor de ETag para un blob. Azure Storage evalúa si el valor de ETag actual del blob es igual al de la etiqueta de entidad proporcionada en la solicitud y realiza la operación de escritura solo si ambos coinciden. Si otro proceso ha actualizado el blob provisionalmente, Blob Storage devuelve un mensaje de estado HTTP 412 (error en la condición previa).

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 también admite otros encabezados condicionales, incluidos If-Modified-Since, If-Unmodified-Since y If-None-Match. Para obtener más información, consulte Especificación de encabezados condicionales para las operaciones de Blob Service.

Simultaneidad pesimista para blobs

Para bloquear un blob para uso exclusivo, puede adquirir una concesión en él. Al adquirir la concesión, se especifica la duración de la concesión. Una concesión finita puede ser válida entre 15 y 60 segundos. Una concesión también puede ser infinita, lo que equivale a un bloqueo exclusivo. Puede renovar una concesión finita para extenderla y puede liberarla cuando haya terminado con ella. Azure Storage libera automáticamente concesiones finitas cuando expiran.

Las concesiones permiten que se admitan diferentes estrategias de sincronización, como, por ejemplo, las siguientes: operaciones de escritura exclusiva o lectura compartida, operaciones de escritura exclusiva o lectura exclusiva y operaciones de escritura compartida o lectura exclusiva. Cuando existe una concesión, Azure Storage fuerza el acceso exclusivo a las operaciones de escritura para el titular de la concesión. Sin embargo, el hecho de asegurar la exclusividad para las operaciones de lectura requiere que el desarrollador garantice que todas las aplicaciones cliente usan un identificador de concesión y que solamente un cliente tiene un identificador de concesión válido en cada momento. Las operaciones de lectura que no incluyen un identificador de concesión, dan lugar a lecturas compartidas.

En los siguientes ejemplos de código se muestra cómo adquirir una concesión exclusiva en un blob, actualizar el contenido del blob al proporcionar el identificador de la concesión y, a continuación, liberar la concesión. Si la concesión estuviera activa y el identificador de concesión no se proporcionase en una solicitud de escritura, se producirá un error en la operación de escritura con el código de error 412 (error de condición previa).

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

Simultaneidad pesimista para contenedores

Las concesiones en contenedores permiten las mismas estrategias de sincronización que se admiten para los blobs, incluidas las siguientes: escritura exclusiva/lectura compartida, escritura exclusiva/lectura exclusiva y escritura compartida/lectura exclusiva. Sin embargo, en el caso de los contenedores, el bloqueo exclusivo solo se aplica en las operaciones de eliminación. Para eliminar un contenedor con una concesión activa, un cliente debe incluir el identificador de concesión activo con la solicitud de eliminación. Todas las demás operaciones de contenedor se realizarán correctamente en un contenedor sujeto a una concesión sin necesidad de incluir el identificador de concesión.

Pasos siguientes

Recursos

Para obtener ejemplos de código relacionados con los SDK de .NET versión 11.x en desuso, consulte Ejemplos de código con la versión 11.x de .NET.