Gestione della concorrenza nell'archiviazione BLOB

Le applicazioni moderne hanno spesso più utenti che visualizzano e aggiornano i dati contemporaneamente. Gli sviluppatori di applicazioni devono considerare attentamente come offrire agli utenti finali un'esperienza prevedibile, in particolare per gli scenari in cui più utenti possono aggiornare gli stessi dati. Gli sviluppatori in genere prendono in considerazione tre strategie principali di concorrenza dei dati:

  • Concorrenza ottimistica: un'applicazione che esegue un aggiornamento, come parte dell'aggiornamento, determina se i dati sono stati modificati dopo l'ultima lettura dei dati da parte dell'applicazione. Ad esempio, se due utenti che visualizzano una pagina wiki effettuano un aggiornamento a tale pagina, la piattaforma wiki deve assicurarsi che il secondo aggiornamento non sovrascriva il primo aggiornamento. Deve anche assicurarsi che entrambi gli utenti comprendano se l'aggiornamento è riuscito. Questa strategia viene usata con maggiore frequenza nelle applicazioni Web.

  • Concorrenza pessimistica: un'applicazione che cerca di eseguire un aggiornamento accetta un blocco su un oggetto impedendo ad altri utenti di aggiornare i dati fino al rilascio del blocco. Ad esempio, in uno scenario di replica dei dati primario/secondario in cui esegue solo gli aggiornamenti primari, il primario in genere mantiene un blocco esclusivo sui dati per un lungo periodo di tempo per garantire che nessun altro possa aggiornarlo.

  • L'ultimo writer vince: un approccio che consente alle operazioni di aggiornamento di procedere senza prima determinare se un'altra applicazione ha aggiornato i dati dopo la lettura. Questo approccio viene in genere usato quando i dati vengono partizionati in modo che più utenti non accedano contemporaneamente agli stessi dati. Può inoltre essere utile per l'elaborazione di flussi dei dati di breve durata.

Archiviazione di Azure supporta tutte e tre le strategie, anche se è distintiva della capacità di fornire supporto completo per la concorrenza ottimistica e pessimistica. Archiviazione di Azure è stata progettata per adottare un modello di coerenza assoluta che garantisce che dopo che il servizio esegue un'operazione di inserimento o aggiornamento, le operazioni di lettura o elenco successive restituiscono l'aggiornamento più recente.

Oltre a selezionare una strategia di concorrenza appropriata, gli sviluppatori devono anche essere consapevoli del modo in cui una piattaforma di archiviazione isola le modifiche, in particolare le modifiche apportate allo stesso oggetto tra le transazioni. Archiviazione di Azure usa l'isolamento dello snapshot per consentire operazioni di lettura simultaneamente con operazioni di scrittura all'interno di una singola partizione. L'isolamento dello snapshot garantisce che tutte le operazioni di lettura restituiscono uno snapshot coerente dei dati anche durante l'esecuzione degli aggiornamenti.

È possibile scegliere di usare modelli di concorrenza ottimistica o pessimistica per gestire l'accesso a BLOB e contenitori. Se non si specifica in modo esplicito una strategia, per impostazione predefinita l'ultimo writer vince.

Concorrenza ottimistica

Archiviazione di Azure assegna un identificatore a ogni oggetto archiviato. Questo identificatore viene aggiornato ogni volta che viene eseguita un'operazione di scrittura su un oggetto . L'identificatore viene restituito al client come parte di una risposta HTTP GET nell'intestazione ETag definita dal protocollo HTTP.

Un client che esegue un aggiornamento può inviare l'ETag originale insieme a un'intestazione condizionale per garantire che si verifichi un aggiornamento solo se è stata soddisfatta una determinata condizione. Ad esempio, se viene specificata l'intestazione If-Match , Archiviazione di Azure verifica che il valore dell'ETag specificato nella richiesta di aggiornamento corrisponda all'ETag per l'oggetto da aggiornare. Per altre informazioni sulle intestazioni condizionali, vedere Specifica delle intestazioni condizionali per le operazioni del servizio BLOB.

Il processo è il seguente:

  1. Recuperare un BLOB da Archiviazione di Azure. La risposta include un valore di intestazione ETag HTTP che identifica la versione corrente dell'oggetto.
  2. Quando si aggiorna il BLOB, includere il valore ETag ricevuto nel passaggio 1 nell'intestazione condizionale If-Match della richiesta di scrittura. Archiviazione di Azure confronta il valore ETag nella richiesta con il valore ETag corrente del BLOB.
  3. Se il valore ETag corrente del BLOB è diverso dal valore ETag specificato nell'intestazione condizionale If-Match fornita nella richiesta, Archiviazione di Azure restituisce il codice di stato HTTP 412 (Precondizione non riuscita). Questo errore indica al client che un altro processo ha aggiornato il BLOB dal primo recupero del client. Il client deve recuperare nuovamente il BLOB per ottenere il contenuto e le proprietà aggiornati.
  4. Se il valore ETag corrente del BLOB è la stessa versione dell'ETag nell'intestazione condizionale If-Match nella richiesta, Archiviazione di Azure esegue l'operazione richiesta e aggiorna il valore ETag corrente del BLOB.

Gli esempi di codice seguenti illustrano come costruire una condizione If-Match nella richiesta di scrittura che controlla il valore ETag per un BLOB. Archiviazione di Azure valuta se l'ETag corrente del BLOB è uguale all'ETag fornito nella richiesta ed esegue l'operazione di scrittura solo se i due valori ETag corrispondono. Se un altro processo ha aggiornato il BLOB nel frattempo, Archiviazione di Azure restituisce un messaggio di stato HTTP 412 (precondizione non riuscita).

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

Archiviazione di Azure supporta anche altre intestazioni condizionali, tra cui If-Modified-Since, If-Unmodified-Since e If-None-Match. Per altre informazioni, vedere Specifica di intestazioni condizionali per le operazioni del servizio BLOB.

Concorrenza pessimistica per i BLOB

Per bloccare un BLOB al fine di usarlo in modo esclusivo, è possibile acquisire un lease su di esso. Quando si acquisisce il lease, si specifica la durata del lease. Un lease finito può essere valido da 15 a 60 secondi. Un lease può anche essere infinito, che equivale a un blocco esclusivo. È possibile rinnovare un lease finito per estenderlo ed è possibile rilasciare il lease al termine dell'operazione. Archiviazione di Azure rilascia automaticamente lease finiti alla scadenza.

I lease consentono di supportare diverse strategie di sincronizzazione, tra cui operazioni di lettura esclusive di scrittura/condivisione, operazioni di lettura esclusive di scrittura/esclusiva e operazioni di lettura condivise/scrittura esclusive. Quando esiste un lease, Archiviazione di Azure applica l'accesso esclusivo alle operazioni di scrittura per il titolare del lease. Tuttavia, garantire l'esclusività per le operazioni di lettura richiede allo sviluppatore di assicurarsi che tutte le applicazioni client usino un ID lease e che un solo client alla volta abbia un ID lease valido. Operazioni di lettura che non includono un ID lease generano letture condivise.

Gli esempi di codice seguenti illustrano come acquisire un lease esclusivo in un BLOB, aggiornare il contenuto del BLOB specificando l'ID lease e quindi rilasciare il lease. Se il lease è attivo e l'ID lease non viene fornito in una richiesta di scrittura, l'operazione di scrittura ha esito negativo con codice di errore 412 (precondizione non riuscita).

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

Concorrenza pessimistica per i contenitori

I lease nei contenitori consentono le stesse strategie di sincronizzazione supportate per i BLOB, tra cui lettura esclusiva in scrittura/condivisa, lettura esclusiva in scrittura/esclusiva e lettura condivisa/esclusiva. Per i contenitori, tuttavia, il blocco esclusivo viene applicato solo alle operazioni di eliminazione. Per eliminare un contenitore con un lease attivo, un client deve includere l'ID lease attivo con la richiesta di eliminazione. Tutte le altre operazioni sui contenitori hanno esito positivo in un contenitore con lease senza l'ID lease.

Passaggi successivi

Risorse

Per esempi di codice correlati che usano SDK .NET versione 11.x deprecati, vedere Esempi di codice con .NET versione 11.x.