Вопрос или проблема
Ссылаясь на “Подход 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 может быть связана с тем, как вы обрабатываете конкурентные обновления. Хотя ваша реализация использует оптимистическую блокировку, есть несколько факторов, на которые стоит обратить внимание.
Проблемы, которые могут возникнуть:
-
Состояние гонки: Когда несколько потоков (или рабочих процессов) одновременно читают одно и то же значение счетчика перед его обновлением, они могут получить одно и то же исходное значение. Например, если несколько процессов одновременно считывают значение счетчика равным 5 и затем пытаются его уменьшить, то эти процессы не будут знать, что значение было изменено другим процессом. В результате оба процесса могут попытаться обновить счетчик с использованием устаревшего значения.
-
Неправильная установка условий: Убедитесь, что ваше условие (
condition_expression
) правильно проверяет текущее значение счетчика. Если какой-либо из ваших потоков не выполняет условие, то произойдет ошибкаConditionalCheckFailedException
, и процесс должен будет повторить чтение и обновление.
Рекомендации по решению проблемы:
-
Проверка чтения перед обновлением: Убедитесь, что каждый рабочий процесс считывает значение счетчика непосредственно перед его уменьшением и что у вас есть правильное значение. Вы можете добавить задержки между попытками обновления, чтобы уменьшить вероятность гонки.
-
Использование полного механизма Exponential Backoff: Если вы получаете исключение
ConditionalCheckFailedException
, вы уже правильно реализовали повторные попытки с использованием экспоненциального увеличения. Однако убедитесь, что вы не просто повторяете попытку, а также заново считываете текущее значение счетчика. -
Логирование и мониторинг: Добавьте дополнительные логи для отслеживания значений счетчиков перед и после обновления, чтобы лучше понять, где происходит расхождение. Это поможет в диагностике проблемы.
Пример обновленного подхода:
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 и использование оптимистической блокировки верны, но важно также учитывать проблемы, связанные с конкурентными запросами. Следуя этим рекомендациям, вы сможете более эффективно управлять подсчетом ресурсов в вашей базе данных и избегать случаев, когда счетчик не уменьшается до нуля.