Атомный учет ресурсов в DynamoDB

Вопрос или проблема

Ссылаясь на “Подход 2 – Оптимистичное управление конкурентностью” – https://aws.amazon.com/blogs/database/implement-resource-counters-with-amazon-dynamodb/

У меня есть таблица, в которой я отслеживаю счет в вложенном поле. Я устанавливаю счет равным x и запускаю x заданий для обработки элементов. Каждое задание уменьшает счет на 1. Это мой вызов UpdateItem, где я проверяю, что

current_count = get_current_count_from_db # Чтение элемента по хеш-ключу и с использованием согласованного чтения

expression_attribute_names = { '#some_id' => some_id, '#another_id' => another_id, '#count' => 'count' }

condition_expression = 'metadata.something.#some_id.#another_id.#count = :curr'

update_expression = 'SET metadata.something.#some_id.#another_id.#count = metadata.something.#some_id.#another_id.#count - :dec'

expression_attribute_values: { ':dec' => 1, ':curr' => current_count }

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

Я также устанавливаю return_values: 'UPDATED_NEW'.

Тем не менее, иногда я сталкиваюсь с ситуациями, когда мой счетчик не уменьшается до 0. Я вижу в логах 2 задания, уменьшающих счет с одного и того же текущего значения до одного и того же нового значения. Например, я вижу 2 лога, уменьшающих счетчик с 5 до 4.

Я подтвердил, что начальный счетчик и количество заданий правильные.

Есть идеи, что я не понимаю? Моя цель – установить счет и запустить несколько заданий, чтобы точно уменьшить счет до 0.

Я также прочитал Является ли обновление DynamoDB сильно согласованным? и считаю, что мой подход правильный.

Ответ или решение

Ваша проблема с подсчетом ресурсов в DynamoDB может быть связана с тем, как вы обрабатываете конкурентные обновления. Хотя ваша реализация использует оптимистическую блокировку, есть несколько факторов, на которые стоит обратить внимание.

Проблемы, которые могут возникнуть:

  1. Состояние гонки: Когда несколько потоков (или рабочих процессов) одновременно читают одно и то же значение счетчика перед его обновлением, они могут получить одно и то же исходное значение. Например, если несколько процессов одновременно считывают значение счетчика равным 5 и затем пытаются его уменьшить, то эти процессы не будут знать, что значение было изменено другим процессом. В результате оба процесса могут попытаться обновить счетчик с использованием устаревшего значения.

  2. Неправильная установка условий: Убедитесь, что ваше условие (condition_expression) правильно проверяет текущее значение счетчика. Если какой-либо из ваших потоков не выполняет условие, то произойдет ошибка ConditionalCheckFailedException, и процесс должен будет повторить чтение и обновление.

Рекомендации по решению проблемы:

  1. Проверка чтения перед обновлением: Убедитесь, что каждый рабочий процесс считывает значение счетчика непосредственно перед его уменьшением и что у вас есть правильное значение. Вы можете добавить задержки между попытками обновления, чтобы уменьшить вероятность гонки.

  2. Использование полного механизма Exponential Backoff: Если вы получаете исключение ConditionalCheckFailedException, вы уже правильно реализовали повторные попытки с использованием экспоненциального увеличения. Однако убедитесь, что вы не просто повторяете попытку, а также заново считываете текущее значение счетчика.

  3. Логирование и мониторинг: Добавьте дополнительные логи для отслеживания значений счетчиков перед и после обновления, чтобы лучше понять, где происходит расхождение. Это поможет в диагностике проблемы.

Пример обновленного подхода:

def decrement_counter(some_id, another_id):
    max_attempts = 5
    for attempt in range(max_attempts):
        current_count = get_current_count_from_db(some_id, another_id)

        expression_attribute_names = {
            '#some_id': some_id,
            '#another_id': another_id,
            '#count': 'count'
        }

        condition_expression = 'metadata.something.#some_id.#another_id.#count = :curr'
        update_expression = 'SET metadata.something.#some_id.#another_id.#count = metadata.something.#some_id.#another_id.#count - :dec'

        expression_attribute_values = {
            ':dec': 1,
            ':curr': current_count
        }

        try:
            response = dynamodb.update_item(
                TableName='YourTableName',
                Key={'YourPrimaryKey': 'value'},
                UpdateExpression=update_expression,
                ConditionExpression=condition_expression,
                ExpressionAttributeNames=expression_attribute_names,
                ExpressionAttributeValues=expression_attribute_values,
                ReturnValues='UPDATED_NEW'
            )
            # Успешное уменьшение счетчика
            break  # Выход из цикла, если обновление прошло успешно
        except dynamodb.exceptions.ConditionalCheckFailedException:
            # Условие не выполнено, повторяем с новым значением
            time.sleep(2 ** attempt)  # экспоненциальная задержка

    else:
        # Обработка случая, когда было превышено максимальное количество попыток
        raise Exception('Failed to decrement counter after maximum attempts')

Заключение

Ваше понимание механизмов работы с DynamoDB и использование оптимистической блокировки верны, но важно также учитывать проблемы, связанные с конкурентными запросами. Следуя этим рекомендациям, вы сможете более эффективно управлять подсчетом ресурсов в вашей базе данных и избегать случаев, когда счетчик не уменьшается до нуля.

Оцените материал
Добавить комментарий

Капча загружается...