Проверка баланса и списание в async-функции: откуда взялся отрицательный баланс
В платёжном сервисе списание реализовано так:
async def withdraw(session, user_id, amount):
balance = await get_balance(session, user_id)
if balance < amount:
raise NotEnoughMoney()
await set_balance(session, user_id, balance - amount)
При балансе 100 пользователь дважды почти одновременно нажал «Оплатить 80». Оба запроса прошли, баланс стал отрицательным.
В чём причина и что исправить?
- asyncio выполняет корутины строго по очереди, гонок в одном процессе не бывает — ищите баг в get_balance
- На каждом await управление уходит другой корутине: обе прочитали баланс 100. Нужно атомарное изменение в базе
- Достаточно завести один asyncio.Lock на приложение и оборачивать в него функцию списания целиком
- Нужно убрать async: в синхронной функции запросы обработаются последовательно и гонки исчезнут
