Gestion de l’accès concurrentiel dans Blob Storage

Dans les applications modernes, les données sont souvent consultées et mises à jour par plusieurs utilisateurs à la fois. Les développeurs d'applications doivent donc bien réfléchir à la manière de proposer une expérience prévisible à leurs utilisateurs finaux, notamment lorsque plusieurs utilisateurs peuvent mettre à jour les mêmes données. Les développeurs prennent généralement en compte trois grandes stratégies d’accès concurrentiel aux données :

  • Accès concurrentiel optimiste : Une application procédant à une mise à jour vérifie, dans le cadre de la mise à jour, si les données n'ont pas été modifiées depuis la dernière lecture. Par exemple, si deux utilisateurs qui consultent une page wiki procèdent à une mise à jour de cette page, la plateforme wiki doit veiller à ce que la deuxième mise à jour n'écrase pas la première. Ils doivent également veiller à ce que les deux utilisateurs sachent si leur mise à jour a fonctionné ou non. Cette stratégie est la plus souvent utilisée dans les applications web.

  • Accès concurrentiel pessimiste : l’application qui cherche à procéder à une mise à jour verrouille l'objet, ce qui empêche les autres utilisateurs de mettre les données à jour jusqu'à ce qu'elles soient déverrouillées. Par exemple, dans un scénario de réplication de données avec réplicas principal/secondaire où seul le réplica principal procède aux mises à jour, celui-ci verrouille généralement les données de manière exclusive pendant une période prolongée de manière à ce que personne d'autre ne puisse les mettre à jour.

  • Dernière écriture prioritaire : Une approche permettant aux opérations de mise à jour de continuer sans déterminer d’abord si une autre application a mis à jour les données depuis leur lecture. Cette approche est généralement utilisée lorsque les données sont partitionnées de manière à ce que plusieurs utilisateurs n’accèdent pas simultanément aux mêmes données. Elle peut également être utile lors du traitement de flux de données à durée de vie limitée.

Stockage Azure prend en charge les trois stratégies, bien qu’il se distingue par sa capacité à prendre en charge pleinement l’accès concurrentiel optimiste et pessimiste. Stockage Azure a été conçu pour appliquer un modèle de cohérence fort qui garantit qu’une fois que le service a effectué une opération d’insertion ou de mise à jour, les opérations de lecture ou de création de liste suivantes retournent la dernière mise à jour.

Parallèlement à la sélection d'une stratégie d'accès concurrentiel adaptée, les développeurs doivent savoir comment la plateforme de stockage isole les changements, notamment ceux apportés à un même objet au fil des transactions. Stockage Azure utilise l’isolement de capture instantanée pour permettre l’exécution simultanée des opérations de lecture et d’écriture au sein d’une même partition. L’isolement d’instantané garantit que toutes les opérations de lecture retournent un instantané cohérent des données, même pendant les mises à jour.

Vous pouvez choisir d’utiliser des modèles d’accès concurrentiel optimiste ou pessimiste pour gérer l’accès aux objets blob et aux conteneurs. Si vous ne spécifiez pas une stratégie de manière explicite, la dernière écriture prévaut par défaut.

Accès concurrentiel optimiste

Stockage Azure attribue un identificateur à chaque objet stocké. Cet identificateur est mis à jour chaque fois qu'une opération d’écriture est effectuée sur un objet. L'identificateur est renvoyé au client en tant que réponse HTTP GET dans l'en-tête ETag défini par le protocole HTTP.

Un client qui effectue une mise à jour peut envoyer la valeur ETag d’origine avec un en-tête conditionnel pour s’assurer qu’une mise à jour se produit uniquement si une certaine condition est remplie. Par exemple, si l’en-tête If-Match est spécifié, Stockage Azure vérifie que la valeur ETag spécifiée dans la demande de mise à jour est identique à la valeur ETag de l’objet en cours de mise à jour. Pour plus d’informations sur les en-têtes conditionnels, consultez Spécification des en-têtes conditionnels pour les opérations du service Blob.

Ce processus se déroule comme suit :

  1. Récupérez un objet blob à partir de Stockage Azure. La réponse inclut une valeur d'en-tête ETag HTTP qui identifie la version actuelle de l'objet.
  2. Lorsque vous mettez l'objet blob à jour, incluez la valeur ETag reçue à l'étape 1 dans l'en-tête conditionnel If-Match de la demande d’écriture. Stockage Azure compare la valeur ETag de la demande à la valeur ETag de l'objet blob.
  3. Si la valeur ETag actuelle de l’objet blob diffère de celle spécifiée dans l’en-tête conditionnel If-Match fourni dans la demande, Stockage Azure retourne le code d’état HTTP 412 (échec de la précondition). Cette erreur indique au client que l’objet blob a été mis à jour par un autre processus depuis la première récupération par le client. Le client doit récupérer à nouveau l’objet blob pour obtenir le contenu et les propriétés mis à jour.
  4. Si la valeur ETag actuelle de l'objet blob est la même que la balise ETag dans l'en-tête conditionnel If-Match de la demande, Stockage Azure effectue l'opération demandée et met la valeur ETag de l'objet blob à jour.

Les exemples de code suivants montrent comment construire une condition If-Match sur la demande d’écriture qui vérifie la valeur ETag pour un objet blob. Stockage Azure évalue si la valeur ETag actuelle de l’objet blob est identique à la valeur ETag fournie dans la demande, puis exécute l’opération d’écriture uniquement si les deux valeurs ETag correspondent. Si l’objet blob a été mis à jour entre-temps par un autre processus, Stockage Azure retourne un message d’état HTTP 412 (Échec de la condition préalable).

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

Stockage Azure prend également en charge d’autres en-têtes conditionnels, notamment If-Modified-Since, If-Unmodified-Since et If-None-Match. Pour plus d’informations, consultez Spécification des en-têtes conditionnels pour les opérations du service Blob.

Accès concurrentiel pessimiste pour les objets blob

Pour verrouiller un objet blob de manière à l'utiliser de manière exclusive, vous pouvez obtenir un bail pour l'objet blob. Lorsque vous acquérez le bail, vous spécifiez sa durée. Un bail fini peut être compris entre 15 et 60 secondes. Un bail peut également être infini, ce qui correspond à un verrou exclusif. Vous pouvez renouveler un bail à durée limitée et vous pouvez libérer le bail lorsque vous n'en avez plus besoin. Azure Stockage libère automatiquement les baux à durée limitée quand ils expirent.

Les baux permettent la prise en charge de différentes stratégies de synchronisation, dont des opérations d'écriture exclusive/de lecture partagée, d'écriture exclusive/de lecture exclusive et d'écriture partagée/de lecture exclusive. Si un bail existe, Stockage Azure applique l’accès exclusif aux opérations d’écriture pour le titulaire de ce bail. Cependant, pour garantir l’exclusivité des opérations de lecture, le développeur doit veiller à ce que toutes les applications clientes utilisent un identificateur de bail et à ce que seul un client à la fois dispose d’un identificateur de bail valable. Les opérations de lecture sans identificateur de bail entraînent l’application d’une stratégie de lecture partagée.

Les exemples de code suivants montrent comment acquérir un bail exclusif sur un objet blob, mettre à jour le contenu de cet objet blob en fournissant l’ID de bail, puis libérer le bail. Si le bail est actif et que l’ID de bail n’est pas fourni dans une demande d’écriture, l’opération d’écriture échoue avec le code d’erreur 412 (échec de la précondition).

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

Accès concurrentiel pessimiste pour les conteneurs

Les baux sur des conteneurs permettent les mêmes stratégies de synchronisation prises en charge pour les objets blob, dont des stratégies d'écriture exclusive/de lecture partagée, d'écriture exclusive/de lecture exclusive et d'écriture partagée/de lecture exclusive. Pour les conteneurs, toutefois, le verrou exclusif est appliqué uniquement aux opérations de suppression. Pour supprimer un conteneur avec un bail actif, le client doit inclure l'identificateur du bail actif dans la demande de suppression. Toutes les autres opérations de conteneur réussissent sur un conteneur loué sans ID de bail.

Étapes suivantes

Ressources

Pour obtenir des exemples de code associés utilisant des sdk .NET version 11.x dépréciés, consultez Exemples de code à l’aide de .NET version 11.x.