Gestire la concorrenza nell'archiviazione BLOB

Le applicazioni moderne hanno spesso più utenti che visualizzano e aggiornano i dati contemporaneamente. Ciò richiede agli sviluppatori di applicazioni un'attenta riflessione su come offrire un'esperienza prevedibile ai propri utenti finali, 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 verificherà, nell'ambito di tale attività, se i dati siano cambiati rispetto all'ultima lettura. 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 applica un blocco a un oggetto, impedendo ad altri utenti di aggiornare i dati fino a quando il blocco non viene rimosso. Ad esempio, in uno scenario di replica dei dati primario/secondario in cui esegue solo gli aggiornamenti primari, il database primario in genere mantiene un blocco esclusivo sui dati per un lungo periodo di tempo per garantire che nessun altro possa aggiornarlo.

  • Prevalenza dell'ultima scrittura: un approccio che consente di eseguire le operazioni di aggiornamento senza prima verificare 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 esegua un'operazione di inserimento o aggiornamento, le operazioni di lettura o elenco successive restituiscono l'aggiornamento più recente.

Oltre a scegliere una strategia di concorrenza appropriata, gli sviluppatori devono inoltre conoscere il modo in cui una piattaforma di archiviazione isola le modifiche, in particolare quelle apportate allo stesso oggetto in più transazioni. Archiviazione di Azure usa l'isolamento degli snapshot per consentire l'esecuzione simultanea di operazioni di lettura e di scrittura nell'ambito 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. L'identificatore viene aggiornato ogni volta che un'operazione di scrittura viene eseguita 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 assicurarsi che si verifichi un aggiornamento solo se è stata soddisfatta una determinata condizione. Ad esempio, se si specifica l'intestazione If-Match, Archiviazione di Azure verifica che il valore dell'ETag specificato nella richiesta di aggiornamento sia uguale a quello dell'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 al 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 è differente 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 rispetto a quando il client lo ha recuperato. 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, specificare 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 e rilasciare il lease al termine dell'uso. Archiviazione di Azure rilascia automaticamente i lease finiti alla scadenza.

I lease abilitano il supporto di strategie di sincronizzazione diverse, tra cui operazioni di scrittura esclusiva/lettura condivisa, scrittura esclusiva/lettura esclusiva e scrittura condivisa/lettura esclusiva. 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. Le operazioni di lettura che non includono un ID lease determinano letture condivise.

Gli esempi di codice seguenti illustrano come acquisire un lease esclusivo in un BLOB, aggiornare il contenuto del BLOB fornendo l'ID lease 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 hanno esito positivo su un contenitore con lease senza ID del lease.

Passaggi successivi

Risorse

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