Verwalten von Parallelität in Blob Storage

Moderne Anwendungen haben häufig mehrere Benutzer, die Daten gleichzeitig anzeigen und aktualisieren. Daher müssen Anwendungsentwickler sorgfältig überlegen, wie sie ihren Endbenutzern ein vorhersagbares Erlebnis gewährleisten können, insbesondere in Situationen, in denen mehrere Benutzer die gleichen Daten aktualisieren können. Von Entwicklern werden für gewöhnlich die drei folgenden Hauptstrategien für die Datenparallelität in Betracht gezogen:

  • Optimistische Parallelität: Eine Anwendung, die eine Aktualisierung ausführt, prüft während dieses Vorgangs, ob die Daten seit dem letzten Lesen geändert wurden. Wenn z. B. zwei Benutzer dieselbe Wiki-Seite aufrufen und aktualisieren, muss die Wiki-Plattform sicherstellen, dass die erste Aktualisierung nicht von der zweiten überschrieben wird. Außerdem muss sichergestellt werden, dass beide Benutzer wissen, ob ihre Aktualisierung erfolgreich war. Diese Strategie wird in Webanwendung sehr häufig verwendet.

  • Pessimistische Nebenläufigkeit: Eine Anwendung, die eine Aktualisierung ausführt, sperrt ein Objekt, sodass andere Benutzer die Daten erst aktualisieren können, wenn die Sperre aufgehoben wird. Bei einer Datenreplikation zwischen Primär- und Sekundärreplikat, bei der nur das Primärreplikat Aktualisierungen vornimmt, richtet dieses beispielsweise in der Regel für einen bestimmten Zeitraum eine Exklusivsperre der Daten ein, damit sie von keinem anderen Benutzer aktualisiert werden können.

  • Letzter Schreiber gewinnt: Bei diesem Ansatz dürfen Aktualisierungsvorgänge fortgesetzt werden, ohne dass zunächst festgestellt wird, ob eine andere Anwendung die Daten seit dem Lesen aktualisiert hat. Diese Vorgehensweise wird normalerweise verwendet, wenn die Daten so partitioniert sind, dass nicht mehrere Benutzer gleichzeitig auf dieselben Daten zugreifen. Diese Strategie kann auch bei der Verarbeitung kurzlebiger Datenströme sinnvoll sein.

Azure Storage unterstützt alle drei Strategien, obwohl der Dienst besonders mit seiner Fähigkeit heraussticht, vollständige Unterstützung für optimistische und pessimistische Nebenläufigkeit bereitzustellen. Azure Storage wurde anhand eines Modells mit starker Konsistenz entwickelt, mit dem sichergestellt wird, dass nach dem Ausführen eines Einfüge- oder Aktualisierungsvorgangs durch den Dienst nachfolgende Lese- oder Listenvorgänge die aktuellsten Daten zurückgeben.

Entwickler müssen nicht nur die entsprechende Parallelitätsstrategie auswählen, sondern auch wissen, wie eine Speicherplattform Änderungen isoliert, insbesondere Änderungen, die in mehreren Transaktionen an demselben Objekt vorgenommen wurden. Azure Storage verwendet die Momentaufnahmeisolation, damit Lesevorgänge innerhalb einer einzelnen Partition gleichzeitig mit Schreibvorgängen ausgeführt werden können. Die Momentaufnahmeisolation gewährleistet, dass alle Lesevorgänge eine konsistente Momentaufnahme der Daten zurückgeben, auch während Aktualisierungen durchgeführt werden.

Für die Verwaltung des Zugriffs auf Blobs und Container können Sie entweder das optimistische oder das pessimistische Parallelitätsmodell verwenden. Wenn Sie keine Strategie explizit festlegen, wird standardmäßig die Strategie „Letzter Schreiber gewinnt“ angewandt.

Optimistische Parallelität

Azure Storage weist jedem gespeicherten Objekt einen Bezeichner zu. Dieser Bezeichner wird jedes Mal aktualisiert, wenn für ein Objekt ein Schreibvorgang ausgeführt wird. Der Bezeichner wird als Teil einer HTTP GET-Antwort an den Client zurückgegeben. Dabei wird der ETag-Header verwendet, der im HTTP-Protokoll definiert ist.

Ein Client, der eine Aktualisierung ausführt, kann das ursprüngliche ETag zusammen mit einem bedingten Header senden, um sicherzustellen, dass die Aktualisierung nur dann erfolgt, wenn eine bestimmte Bedingung erfüllt ist. Wenn beispielsweise der If-Match-Header angegeben ist, überprüft Azure Storage, ob der Wert des in der Aktualisierungsanforderung angegebenen ETags mit dem ETag des zu aktualisierenden Objekts übereinstimmt. Weitere Informationen zu bedingten Headern finden Sie unter Angeben von bedingten Headern für Vorgänge des Blob-Diensts.

Dieser Prozess ist folgendermaßen gegliedert:

  1. Es wird ein Blob aus Azure Storage abgerufen. Die Antwort enthält einen HTTP-ETag-Headerwert, der die aktuelle Version des Objekts bezeichnet.
  2. Wenn Sie das Blob aktualisieren, fügen Sie den in Schritt 1 erhaltenen ETag-Wert in den bedingten If-Match-Header der Schreibanforderung ein. Azure Storage vergleicht den ETag-Wert in der Anforderung mit dem aktuellen ETag-Wert des Blobs.
  3. Wenn sich der aktuelle ETag-Wert des Blobs von dem ETag-Wert im bedingten If-Match-Header der Anforderung unterscheidet, gibt Azure Storage den HTTP-Statuscode 412 (Vorbedingung nicht erfüllt) zurück. Dieser Fehler teilt dem Client mit, dass das Blob von einem anderen Prozess geändert wurde, nachdem es vom Client das erste Mal abgerufen wurde. Der Client sollte das Blob erneut abrufen, um die aktualisierten Inhalte und Eigenschaften abzurufen.
  4. Wenn der aktuelle ETag-Wert des Blobs dieselbe Version wie das ETag im bedingten If-Match-Header der Anforderung aufweist, führt Azure Storage den angeforderten Vorgang aus und aktualisiert den aktuellen ETag-Wert des Blobs.

In den folgenden Codebeispielen wird gezeigt, wie Sie eine If-Match-Bedingung für die Schreibanforderung erstellen, die den ETag-Wert für ein Blob überprüft. Azure Storage überprüft, ob das aktuelle ETag des Blobs mit dem ETag übereinstimmt, das in der Anforderung bereitgestellt wurde, und führt den Schreibvorgang nur aus, wenn die beiden ETag-Werte übereinstimmen. Falls das Blob in der Zwischenzeit von einem anderen Prozess aktualisiert wurde, gibt Azure Storage die Statusmeldung HTTP 412 (Vorbedingung nicht erfüllt) zurück.

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 unterstützt auch andere bedingte Header, einschließlich If-Modified-Since, If-Unmodified-Since und If-None-Match. Weitere Informationen finden Sie unter Specifying Conditional Headers for Blob Service Operations (Angeben von bedingten Headern für Vorgänge des Blob-Diensts).

Pessimistische Nebenläufigkeit für Blobs

Um ein Blob für die exklusive Verwendung zu sperren, können Sie dafür eine Lease abrufen. Wenn Sie die Lease abrufen, geben Sie die Dauer der Lease an. Eine zeitlich begrenzte Lease kann zwischen 15 und 60 Sekunden gültig sein. Eine Lease kann auch unendlich gültig sein und wird dann zu einer Exklusivsperre. Sie können eine begrenzte Lease erneuern, um sie zu verlängern, und Sie können eine Lease freigeben, wenn sie nicht mehr benötigt wird. Azure Storage gibt begrenzte Leases automatisch frei, sobald sie ablaufen.

Leases ermöglichen die Unterstützung verschiedener Synchronisierungsstrategien, einschließlich exklusiver Schreib-/gemeinsamer Lesevorgänge, exklusiver Schreib-/exklusiver Lesevorgänge und gemeinsamer Schreib-/exklusiver Lesevorgänge. Wenn eine Lease vorhanden ist, erzwingt Azure Storage den exklusiven Zugriff auf Schreibvorgänge für den Leaseinhaber. Um die Exklusivität für Lesevorgänge zu gewährleisten, muss der Entwickler jedoch sicherstellen, dass alle Clientanwendungen eine Lease-ID verwenden und dass jederzeit nur ein Client über eine gültige Lease-ID verfügt. Lesevorgänge, die keine Lease-ID enthalten, führen zu gemeinsamen Lesevorgängen.

In den folgenden Codebeispielen wird veranschaulicht, wie Sie eine exklusive Lease für ein Blob abrufen, den Inhalt des Blobs aktualisieren, indem Sie die Lease-ID angeben, und dann die Lease freigeben. Wenn die Lease aktiv ist und die Lease-ID für eine Schreibanforderung nicht angegeben ist, führt der Schreibvorgang zum Fehlercode 412 (Vorbedingung nicht erfüllt).

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

Pessimistische Nebenläufigkeit für Container

Leases für Container ermöglichen dieselben Synchronisierungsstrategien wie für Blobs, einschließlich exklusiver Schreib-/gemeinsamer Lesevorgänge, exklusiver Schreib-/exklusiver Lesevorgänge und gemeinsamer Schreib-/exklusiver Lesevorgänge. Bei Containern wird die exklusive Sperre jedoch nur bei Löschvorgängen erzwungen. Um einen Container mit einer aktiven Lease zu löschen, muss ein Client die ID der aktiven Lease bei der Löschanforderung angeben. Alle anderen Containervorgänge werden für einen geleasten Container auch ohne Angabe der Lease-ID erfolgreich ausgeführt.

Nächste Schritte

Ressourcen

Verwandte Code-Beispiele, in denen veraltete SDKs der .NET-Version 11.x verwendet werden, finden Sie unter Code-Beispiele mit der .NET-Version 11.x.