Вопрос или проблема
Я пытаюсь научить своих студентов условиям гонки в интернете, и для этой цели я использую простой банковский пример, в котором мы переводим сумму с счета человека A на счет человека B. Если мы используем Burp и отправим одновременные запросы, например, 20-30, операция добавления завершится; однако операция вычитания завершится позже. Итак, если у человека A есть 20 долларов, а у человека B 0 долларов. Как только я отправлю запрос на перевод 20 долларов с A на B (20 одновременных запросов), есть вероятность, что счет человека B получит 20 долларов более одного раза. Вот мой код
if user['balance'] >= amount:
# Пополнить счет целевого пользователя
conn.execute('UPDATE users SET balance = balance + ? WHERE account_number = ?',
(amount, target_account_number))
conn.close() # Немедленно закрыть соединение, чтобы принудительно выполнить запись
# Симулировать задержку для возникновения условия гонки
import time
time.sleep(2) # Симулировать задержку
# Открыть новое соединение для операции дебета
conn = get_db()
# Снять средства со счета отправителя
conn.execute('UPDATE users SET balance = balance - ? WHERE account_number = ?',
(amount, session['user']))
conn.close() # Закрыть соединение, чтобы немедленно применить изменения
flash(f'Успех! Переведено ${amount} на счет {target_account_number}.')
else:
flash('Недостаточно средств.')
return redirect(url_for('dashboard'))
Есть что-то, что я делаю неправильно в условиях гонки? Мой бэкенд – SQLITE
Ответ или решение
В вашем коде действительно присутствует уязвимость к состоянию гонки (race condition), которая может привести к неправильному обновлению баланса пользователей. Давайте разберем, в чем проблема, и как можно ее решить.
Проблема
-
Отсутствие атомарных операций: Ваш код выполняет две основные операции — списание средств с одного аккаунта (A) и зачисление средств на другой аккаунт (B). Однако они не находятся в одной атомарной транзакции, что позволяет одновременно выполнять несколько запросов и нарушать целостность данных.
-
Закрытие соединения: Закрытие соединения сразу после зачисления также может привести к тому, что у других потоков появляется возможность выполнить свои операции, не дожидаясь завершения предыдущих.
-
Искусственная задержка: Использование
time.sleep(2)
для симуляции задержки только усугубляет проблему состояния гонки, так как это позволяет другим запросам проходить в это время.
Решение
Для решения этой проблемы вам следует использовать механизмы, обеспечивающие атомарные операции и защиту от одновременного доступа. Вот несколько подходов:
-
Использование транзакций: Сворачивайте операции зачисления и списания в транзакцию, чтобы они выполнялись атомарно.
-
Блокировка на уровне базы данных: Используйте механизмы блокировок, предоставляемые SQLite, для предотвращения одновременного доступа к одним и тем же данным.
Ниже приведен пример, как можно переписать ваш код с учетом указанных выше советов:
def transfer_funds(target_account_number, amount):
conn = get_db()
try:
conn.execute('BEGIN TRANSACTION')
# Получите баланс отправителя
user_balance = conn.execute('SELECT balance FROM users WHERE account_number = ?', (session['user'],)).fetchone()[0]
if user_balance >= amount:
# Списываем со счета отправителя
conn.execute('UPDATE users SET balance = balance - ? WHERE account_number = ?', (amount, session['user']))
# Зачисляем на счет получателя
conn.execute('UPDATE users SET balance = balance + ? WHERE account_number = ?', (amount, target_account_number))
conn.execute('COMMIT') # Зафиксировать транзакцию
flash(f'Успешно перевели ${amount} на счет {target_account_number}.')
else:
flash('Недостаточно средств.')
conn.execute('ROLLBACK') # Откатить транзакцию в случае ошибки
except Exception as e:
conn.execute('ROLLBACK') # В случае исключения откатить
flash(f'Произошла ошибка: {e}')
finally:
conn.close() # Закрыть соединение
return redirect(url_for('dashboard'))
Итоги
В этом коде используется транзакция, которая гарантирует, что операции списания и зачисления выполняются атомарно. Если одна из операций не проходит, она откатывается, и данные остаются целостными. Также убедитесь, что ваша база данных настроена на использование блокировок, чтобы предотвратить конфликты между одновременными операциями. Это сократит вероятность возникновения состояния гонки и обеспечит корректность данных в вашем приложении.