Поделиться через


Управление одновременным доступом в хранилище объектов BLOB

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

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

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

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

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

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

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

Оптимистическая конкурентность

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

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

Описание этого процесса следующее:

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

В следующих примерах кода показано, как создать условие If-Match для запроса на запись, который проверяет значение ETag для BLOB-объекта. Azure Storage оценивает, совпадает ли текущий ETag блоба с 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 обеспечивает монопольный доступ к операциям записи для держателя аренды. Однако, чтобы обеспечить исключительность операций чтения, разработчику необходимо убедиться, что все приложения-клиенты используют идентификатор аренды и что только один клиент в данный момент обладает действительным идентификатором аренды. Операции чтения, не содержащие идентификатор аренды, приводят к разделяемым считываниям.

В следующих примерах кода показано, как получить эксклюзивную аренду для блоба, обновить его содержимое, указав идентификатор аренды, а затем освободить аренду. Если аренда активна, а идентификатор аренды не указан в запросе на запись, операция записи завершается ошибкой с кодом ошибки 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();
    }
}

Пессимистичная конкурентность для контейнеров

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

Следующие шаги

Ресурсы

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