Нужно просто для всех команд списания с баланса использовать мьютекс, внутри которого:
1. Получать текущий баланс.
2. Проверять, что он больше, либо равен сумме списания.
3. Вносить изменения в БД, если всё ок.
Третий шаг можно реализовать по-разному - через таблицу транзакций, через единственное поле баланса, как у вас сейчас или ещё как угодно. Эта часть совершенно не важна.
Важно только то, что функционал списания с баланса в целом становится однопоточным за счёт блокировки.
Нужно только учитывать, что блокировка должна быть распределённой. Стандартом является использование алгоритма Redlock, реализованном на базе Redis.
Ещё важно использовать один блокировщик именно для всех типов списаний. Если вы в buyProduct будете использовать один мьютекс, а в каком-нибудь buyService другой, то работать это правильно не будет.