InnoDB все запросы выполняет только в транзакции. Если открытой транзакции не было, то этот запрос неявно оборачивается в транзакцию.
Вот только сами по себе транзакции не помогут, надо ещё правильно ими пользоваться. Тут я бы хотел дополнительно обратить внимание
Андрей:
Попробуйте в двух терминалах написать:
сначала begin; в обоих
потом select из таблички. Значения одинаковые, правда?
потом update этой же таблички, сделайте set fieldname = разные значения в терминалах. Второй терминал запрос принял, но не вернул управление, верно?
потом commit; в первом терминале. update из второго терминала сразу же ответил OK.
Теперь сделайте commit во втором терминале и посмотрите, что произошло с данными. Это то, на что вы рассчитывали? Или всё-таки не совсем?
N пользователей запрашивают услугу одновременно, корректно ли произойдет списание средств?
Нет, некорректно, если только вы не в одной транзакции и не читаете баланс специально с select .. for update.
Потому что для выполнения действия $this->balance -= $sum; у вас уже должен быть известен баланс, но это ещё не операция записи.
В итоге у вас было 1000 рублей.
Пришёл один клиент, прочитал баланс, хочет списать 200 рублей. Обновил циферку в PHP, никто ему не мешает.
Пришёл второй клиент, прочитал баланс, хочет списать 100 рублей. Обновил циферку в PHP, никто ему тоже не мешает.
И на шаге save оба отправили запросы на update: один клиент считает, что на балансе осталось 800 рублей, второй - что 900.
Сколько запишется на баланс? 800 или 900, как повезёт. Правильно ли это? Сколько должно было быть? 700 ведь.
Потому что клиенты не мешали друг другу обновлять циферку в PHP.
Как же заставить клиентов не делать глупости?
В простом случае:
update tablename set balance = balance - :amount where balance >= :amount and user_id=:uid
И на приложении проверять affected_rows. Если строка изменена - у пользователя достаточно денег, платёж прошёл. Если изменённых строк нет - вероятно, у пользователя нет столько денег. СУБД разберётся с очерёдностью исполнения и в результате на балансе будет правильная сумма, сколько бы параллельных запросов ни пришло. И, что не менее важно - приложение ответит на все запросы корректно, кому денег хватило, а кому - уже нет.
В более сложных случаях - можно самому попросить СУБД взять блокировку на строку, о чём чуть ранее я уже заикался:
begin;
select balance from tablename where user_id=:uid for update; -- все параллельные транзакции будут выстраиваться в очередь здесь
/* произвольные запросы (в mysql кроме вызова хранимок, DDL - они делают неявный коммит). Среди этих запросов - обновляете баланс */
commit; -- только здесь эта транзакция освобождает блокировку select .. for update и с этой строкой начинает работать следующий запрос
Как вообще работать с деньгами и делать это правильно -
Кирилл даёт правильное направление. Изучите, как это делает бухгалтерия, за многие десятилетия работы они придумали, как обходить много странных граблей.