Блокировка строки FOR UPDATE

Теги: mysql

Допустим мы получили данные о текущем состоянии счёта пользователя (по первичному ключу) из mysql выполнив запрос:

SELECT * FROM `balance` WHERE `user_id` = 1;

Теперь, когда мы знаем сколько у пользователя на счету, нам, например, нужно добавить на счёт определённую сумму. Считаем новую сумму и отправляем запрос на обновление строки:

UPDATE `balance` SET `balance` = 999 WHERE `user_id` = 1;

Mysql выполняет запросы с той последовательность, с которой они приходят. Приходить они могут от множества сессий, вперемешку. Поэтому может случиться коллизия, когда одна сессия получила счёт пользователя, вторая сессия получила счёт пользователя, первая сессия обновила счёт пользователя пересчитав сумму от первоначальных данных, и вторая сессия обновила счёт пользователя от тех же самых первоначальных данных, при этом как бы затерев изменения от первой сессии. Ситуация редкая, но при высоких нагрузках такое может случиться.

И тут нам на помощь приходят блокировки. Нужно, чтобы первая сессия заблокировала строку до того времени как она не изменит данные или пока сама эта сессия не закончится, после чего к строке получит доступ вторая сессия.

C mysql таблицами на движке InnoDB это проще всего сделать с помощью блокировки "FOR UPDATE". Перепишем наши запросы с её использованием:

/* получаем счёт */
SELECT * FROM `balance` WHERE `user_id` = 1 FOR UPDATE;
/* тут пройдёт какое-то время пока мы на стороне php получим ответ, проведём все проверки, посчитаем новую сумму и отправим запрос на изменение */
/* обновляем счёт */
UPDATE `balance` SET `balance` = 999 WHERE `user_id` = 1;

Но сработает это только если обернуть оба запроса в транзакцию:

Start transaction;
/* получаем счёт */
SELECT * FROM `balance` WHERE `user_id` = 1 FOR UPDATE;
/* тут пройдёт какое-то время пока мы на стороне php получим ответ, проведём все проверки,
посчитаем новую сумму и отправим запрос на изменение */
/* обновляем счёт */
UPDATE `balance` SET `balance` = 999 WHERE `user_id` = 1;
Commit; /* или Rollback */

Данные под защитой :)