Состояние гонки в Python

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

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

Проблема

  1. Отсутствие атомарных операций: Ваш код выполняет две основные операции — списание средств с одного аккаунта (A) и зачисление средств на другой аккаунт (B). Однако они не находятся в одной атомарной транзакции, что позволяет одновременно выполнять несколько запросов и нарушать целостность данных.

  2. Закрытие соединения: Закрытие соединения сразу после зачисления также может привести к тому, что у других потоков появляется возможность выполнить свои операции, не дожидаясь завершения предыдущих.

  3. Искусственная задержка: Использование time.sleep(2) для симуляции задержки только усугубляет проблему состояния гонки, так как это позволяет другим запросам проходить в это время.

Решение

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

  1. Использование транзакций: Сворачивайте операции зачисления и списания в транзакцию, чтобы они выполнялись атомарно.

  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'))

Итоги

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

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

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