Приведу пример: допустим, нужно сделать чат, в котором отправка одного сообщения платная и составляет 1 рубль (первое условие). Второе условие заключается в том, что пользователю нельзя отправлять подряд более двух сообщений.
То есть, алгоритм отправки сообщения состоит в следующем:
1. Проверить, не отправил ли уже пользователь 2 сообщения, если отправил, выдать ошибку, иначе:
2. Снять средства с баланса (1 рубль), если средств недостаточно, выдать ошибку, если же достаточно, то:
3.Добавить сообщение в базу.
Проблема состоит в том, что при проверках запросы могут не успеть выполниться и пару запросов пройдет проверку и проскочит сквозь ограничения, пока старые запросы будут "заканчивать своё выполнение".
Данная ситуация лишь пример, и не стоит концентрироваться только на чате. Разберем пример с онлайн-рулеткой:
В одной комнате играет несколько пользователей (неважно количество). Лимит на сумму ставки -- от 10 до 50 рублей. Максимальное количество ставок за один раунд -- 5 ставок.
То есть, в данном примере также нужно сделать несколько проверок, чтобы потом добавить ставку в базу. Если ставка не успеет добавиться, а уже следующий запрос начнет выполняться, то в этом время проверка на количество ставок не будет пройдена корректно и лимит ставок может быть нарушен.
Подскажите, пожалуйста, что делать в этой ситуации?
Вам вроде нужен самый сильный уровень изоляции транзакций, SERIALIZABLE.
Более сложный, но более высокопроизводительный способ — SELECT FOR UPDATE. https://dev.mysql.com/doc/refman/8.0/en/innodb-loc...
Тут уровень изолированности транзакций не важен, поскольку вы сами выбираете, какую строку как изолировать.
CREATE TABLE `users` (
`id` INT PRIMARY KEY AUTO_INCREMENT,
`balance` INT
);
CREATE TABLE `messages` (
`user_id` INT NOT NULL,
`timestamp` TIMESTAMP,
`message` VARCHAR(128)
);
Триггер, снимающий сумму с баланса при отправке сообщения:
DELIMITER $$
CREATE TRIGGER `pay_message` AFTER INSERT ON `messages`
FOR EACH ROW BEGIN
UPDATE `users`
SET `balance` = `balance` - 1
WHERE `id` = NEW.`user_id`;
END$$
DELIMITER ;
Запрос на отправку сообщения. Если низкий баланс или за последние полчаса уже было два сообщения, то выдаст ошибку.
INSERT INTO `messages` (`user_id`, `timestamp`, `message`)
VALUES (
(SELECT `u`.`id`
FROM (
SELECT `id`
FROM `users`
WHERE `id` = :user
AND `balance` >= 1
) AS `u`
JOIN (
SELECT COUNT(*) AS `count`
FROM `messages`
WHERE `user_id` = :user
AND `timestamp` > NOW() - INTERVAL 30 MINUTE
) AS `m` ON `m`.`count` < 2
), NOW(), :message);