Управление параллелизмом в хранилище BLOB-объектов

В современных интернет-приложениях данные могут одновременно просматривать и обновлять несколько пользователей. Это требует от разработчиков приложений осторожного подхода, чтобы взаимодействие с конечным пользователем было предсказуемым, в частности для сценариев с одновременным обновлением данных несколькими пользователями. Обычно разработчики рассматривают три следующие основные стратегии конфликтов данных:

  • Оптимистичный параллелизм — обновляемое приложение при обновлении определяет, произошли ли изменения данных с момента последнего считывания данных приложением. Например, если два пользователя, просматривающие вики-страницу, обновляют ее, платформа вики-сайта должна убедиться, что второе обновление не перезаписывает первое. Кроме того, оба пользователя должны понимать, успешно ли применено их обновление. Стратегия наиболее часто применяется в веб-приложениях.

  • Пессимистичный параллелизм. Приложение, которое хочет выполнить обновление, блокирует объект , не позволяя другим пользователям обновлять данные до тех пор, пока блокировка не будет снята. Например, в сценарии репликации данных основное/вторичное, где данные обновляются только основным приложением, вторичное применяет монопольную блокировку данных на длительный период времени для предотвращения его изменений кем-либо еще.

  • Побеждает последняя запись — подход, который позволяет выполнять обновления, не определяя сначала, применялись ли обновления другим приложением после считывания данных. Этот подход обычно используется, когда данные секционируются таким образом, что несколько пользователей не обращаются к одинаковым данным одновременно. Она также применяется при обработке кратковременных потоков данных.

Служба хранилища Azure поддерживает все три стратегии, хотя она отличается своей способностью обеспечивать полную поддержку оптимистичного и пессимистичного параллелизма. Служба хранилища Azure была разработана для использования модели строгой согласованности, которая гарантирует, что после выполнения службой операции вставки или обновления последующие операции чтения или перечисления возвращают последнее обновление.

Кроме выбора соответствующей стратегии параллелизма, разработчики должны иметь представление о том, как платформа службы хранилища изолирует изменения, в частности изменения одних и тех же объектов между транзакциями. Служба хранилища Azure использует изоляцию моментального снимка, чтобы обеспечить одновременное выполнение операций чтения и записи в рамках одного раздела. Изоляция моментальных снимков гарантирует, что все операции чтения возвращают согласованный моментальный снимок данных даже во время обновления.

Вы можете выбрать модель оптимистичного или пессимистичного параллелизма для управления доступом к BLOB-объектам и контейнерам. Если стратегия не задана явно, по умолчанию используется последний модуль записи.

Оптимистическая блокировка

Служба хранилища Azure присваивает идентификатор каждому сохраненному объекту. Идентификатор обновляется при каждой операции записи для объекта. Идентификатор возвращается клиенту как часть ответа HTTP GET в заголовке ETag, определенном протоколом HTTP.

Клиент, выполняющий обновление, может отправить исходный ETag вместе с условным заголовком, чтобы гарантировать, что обновление происходит только при выполнении определенного условия. Например, если указан заголовок If-Match, служба хранилища Azure проверяет, совпадает ли значение ETag, указанное в запросе на обновление, со значением ETag для обновляемого объекта. Дополнительные сведения об условных заголовках см. в статье Определение условных заголовков для операций службы BLOB-объектов.

Для этой команды используется следующая структура:

  1. Скачивание BLOB-объекта из службы хранилища Azure. Ответ содержит значение заголовка HTTP ETag, указывающее текущую версию объекта.
  2. При обновлении BLOB-объекта включите полученное после выполнения шага 1 значение ETag в условный заголовок If-Match запроса на запись. Служба хранилища Azure сравнивает значение ETag запроса с текущим значением ETag BLOB-объекта.
  3. Если текущее значение ETag большого двоичного объекта отличается от значения ETag, указанного в условном заголовке If-Match , предоставленном в запросе, служба хранилища Azure возвращает код состояния HTTP 412 (сбой условия). Эта ошибка указывает клиенту на то, что после извлечения клиентом BLOB-объекта он был обновлен другим процессом. Клиент должен снова получить большой двоичный объект, чтобы получить обновленное содержимое и свойства.
  4. Если текущее значение ETag BLOB-объекта совпадает со значением ETag в условном заголовке запроса If-Match, служба хранилища Azure выполняет запрошенную операцию и обновляет текущее значение ETag BLOB-объекта.

В следующих примерах кода показано, как создать условие If-Match для запроса на запись, который проверяет значение ETag для BLOB-объекта. Служба хранилища Azure оценивает, совпадает ли текущий ETag BLOB-объекта с тегом ETag, указанным в запросе, и выполняет операцию записи, только если два значения ETag совпадают. Если в это время произошло обновление BLOB-объекта другим процессом, служба хранилища Azure возвращает сообщение о состоянии HTTP 412 (необходимое условие не выполнено).

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 также поддерживает другие условные заголовки, включая If-Modified-Since, If-Unmodified-Since и If-None-Match. Дополнительные сведения см. в статье Указание условных заголовков для операций службы BLOB-объектов.

Пессимистичный параллелизм для BLOB-объектов

Для блокировки BLOB-объекта в целях монопольного использования вы можете получить его в аренду . При получении аренды указывается ее длительность. Конечная аренда может быть действительной в диапазоне от 15 до 60 секунд. Аренда также может быть бесконечной при монопольной блокировке. Вы можете обновить ограниченную аренду, продлив ее, а также освободиться от нее, если она вам более не нужна. Служба хранилища Azure автоматически освобождается от ограниченной аренды после завершения ее срока.

Аренда разрешает поддержку различных стратегий синхронизации, включая монопольную запись/общее чтение, монопольную запись/монопольное чтение и общую запись/монопольное чтение. При наличии аренды служба хранилища Azure обеспечивает монопольный доступ к операциям записи для держателя аренды. Однако, чтобы сделать возможным монопольное чтение, разработчику необходимо обеспечить использование всеми клиентскими приложениями идентификатора аренды, а также наличие действительного идентификатора аренды одновременно только у одного клиента. Операции чтения, не включающие идентификатор аренды, приводят к общим операциям чтения.

В следующих примерах кода показано, как получить монопольную аренду для BLOB-объекта, обновить его содержимое, указав идентификатор аренды, а затем освободить аренду. Если аренда активна, а идентификатор аренды не указан в запросе на запись, операция записи завершается ошибкой с кодом 412 (сбой предварительного условия).

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

Пессимистичный параллелизм для контейнеров

Аренда контейнеров обеспечивает поддержку тех же стратегий синхронизации, включая монопольную запись/общее чтение, монопольную запись/монопольное чтение и общую запись/монопольное чтение. Однако для контейнеров монопольная блокировка применяется только к операциям удаления. Для удаления контейнера с активной арендой клиент наряду с запросом на удаление должен ввести активный идентификатор аренды. Все остальные операции с контейнерами успешно выполняются в арендованном контейнере без идентификатора аренды.

Дальнейшие действия

Ресурсы

Примеры кода, использующие устаревшие пакеты SDK для .NET версии 11.x, см. в статье Примеры кода с использованием .NET версии 11.x.