Русское сообщество разработки на PHP-фреймворке Laravel.
Ты не вошёл. Вход тут.
(лара 5.5)
Собственно 5 раз нажмем кнопку "Отправить" и 5 раз данные из формы летят в БД. редирект на эту же страницу даже не успевает отработать.
Кто как с этим боролся? (с аяксом все понятно..)
Не в сети
Не в сети
$('form').on('submit', function() {
$(this).find('.js-submit').prop('disabled', true);
});Как варриант
Собственно, это и единственный вариант. Блокировать повторную отправку, пока предыдущий результат не пришел. Только лучше блокировать не кнопку, а саму форму (нажатие на enter никто не отменял).
Не в сети
Не в сети
Собственно, это и единственный вариант.
Совершенно не единственный и даже не лучший:
Если форма/кнопка блокируется, то ее нужно и разблокировать по какому-то событию (не факт, что форма вообще дойдет до сервера и соединение не порвется, нужно обрабатывать ошибки и тайм-аут).
В Firefox (раньше, как сейчас не знаю) блокировка кнопки сохранялась при перезагрузке страницы по F5 (!).
JavaScript может быть вообще отключен или скрипты не загружены (при медленном интернете страница вполне может быть показана до загрузки стилей, не говоря уже о скриптах).
Могут быть и другие неожиданности. Плюс у такого подхода только один - простота реализации. И еще, пожалуй, понятность для пользователя, если блокировку формы сопровождать сообщением вроде "форма отправляется, подождите".
Поэтому лучший вариант - это дополнительно проверять дубликат на стороне сервера. Это делается двумя способами в зависимости от ситуации:
Если функция позволяет однозначно определить дублирование сообщения (или другой сущности, которая отправляется через форму) - например, заполнение анкеты, которую можно заполнить только один раз (у анкеты есть идентификатор, который передается из формы), голосование (тоже только один раз, тоже по некоему ID) и т.д. - то все просто: перед вставкой в БД проверяется наличие объекта. Или даже не проверяется, если в БД есть соответствующий уникальный ключ (UNIQUE), но тогда нужно показать осмысленное сообщение об ошибке, если вставка не пройдет.
В других случаях в форму добавляется случайно сгенерированный ID, заносится в базу (в Redis, что оптимально, т.к. данные временные, либо в отдельную таблицу, либо в ту же таблицу, что и сами данные формы, в отдельном поле вроде duplicate_code), и при обработке запроса делается проверка на существование этого ID. Есть - значит, форма уже была принята и обработана.
В обоих случаях нужно учесть возможную гонку (race conditions), т.е. вставлять в базу ID перед обработкой операции в транзакции, чтобы при двух одновременных запросах не получилось так, что код проверил отсутствие ID в базе, и первый, и второй запросы пошли обрабатываться дальше, но в итоге один из них вернулся с ошибкой, когда пришло время добавлять данные в таблицу - первый запрос уже успел это сделать и занять ID, что для второго непредвиденная ситуация, ведь ID им уже был проверен. Или не вернулся с ошибкой, а все равно добавил дубль, не смотря на наличие кода.
-----------
По-четному, остается еще один вопрос: а какую из версий данных считать правильной - ту, что пришла раньше и уже была обработана, или повторную (что логично - позже отправили, значит, актуальнее)? Здесь все сложнее - на порядок запросов полагаться нельзя (тот что пришел первым вполне мог быть отправлен позже второго). Например, при отправке сообщения на форуме, при задержках в сети пользователь вполне может успеть нажать "Отправить", отменить, подредактировать что-то и снова "Отправить" - на всё секунд 10. И в каком порядке такие запросы придут на сервер - не известно.
Можно пойти простым путем (на мой взгляд, подходит в большинстве случаев): игнорировать все, что пришло после первого запроса. Если же хочется большего и нужно затирать первоначально созданную сущность новыми данными (новым текстом, например), то можно положиться на время отправки формы, навесив на ее onsubmit JS, который будет выставлять значение скрытому полю с меткой времени при каждой отправке. Особо упорные личности могут попытаться взять время из пришедшего TCP-пакета... Впрочем, JS может быть отключен, а клиент - не поддерживать TCP Timestamps, но даже если не так, то в обоих случаях это время не монотонное и может прыгать.
Короче, способов надежно определить, какой из запросов пришел позже, я не вижу, и скорее всего толку от этой затеи будет меньше, чем потенциального вреда от проигнорированных повторных запросов, даже с отличиями. Так что проще - лучше.
Не в сети
Иными словами - реализовать мьютекс (в рамках клиента) на бэкенде своими силами.
Изменено covobo (05.12.2017 22:10:19)
Не в сети
Иными словами - реализовать мьютекс (в рамках клиента) на бэкенде своими силами.
Да. В зависимости от функции повторная обработка может быть опасна (повторное списание с баланса, платное обращение в техподдержку и т.д.). Никто же, надеюсь, не проверяет баланс перед покупкой только на стороне клиента? Здесь так же.
БД (бекенд) - это "последний страж", все важные проверки должны происходить на ней, а не на стороне клиента. JS - это только для удобства, чтобы не гонять лишние байты в случае заведомой ошибки в заполнении формы, например. Но не больше. Нельзя полностью полагаться на проверку той же формы на стороне клиента.
Не в сети
Не в сети
Да и мне тоже этот mutex нужно реализовать, но как лучше...
Вижу например вот такое https://github.com/arvenil/ninja-mutex с мемкешем..
Но может и самому написать.. Как я понимаю если скажем User может иметь только одну активную подписку Subscription и нужно избежать race condition во время ее создание. То блокируем user->id mutex'ом на создание новых подписок, в начале процедуры создания подписки и скажем на минуту. И при успешном создании убираем mutex.
А если например User может иметь несколько кредитных карт Cards и повторно добавляет одну и туже карту... Блокировать должны видимо именно на добавление данной карты, те записать в мемкеш ее уникальный токен из формы и не принимать с данным токеном запросы более
Правильно ли я все понял?
Не в сети
А еще оказалось семафор для блокировки прямо встроен в php http://php.net/manual/ru/function.sem-get.php
Нашел такой пример:
$sem = sem_get(1234, 1);
if (sem_acquire($sem)) {
//successful lock, go ahead
sem_release($sem);
} else {
//Something went wrong...
}
Изменено htclog81 (16.12.2017 01:20:57)
Не в сети
Можно рассматривать задачу не как защиту от дубля, а как защиту от флуда — слишком частой отправки сообщений конкретным пользователем. Введите правило, например, что следующее сообщение пользователя может быть не ранее, чем через 1 минуту. Отказывайте если таймаут ещё не прошёл. Время предыдущей отправки можно хранить в пользовательской сессии или в профиле пользователя.
Кстати, этот форум (движок fluxbb) имеет такой механизм из коробки. Каждой группе пользователей можно задать свой флуд-таймаут.
Изменено artoodetoo (18.12.2017 00:52:07)
There are two hard things in computer science: cache invalidation, naming things, and off-by-one errors.
Не в сети
Сессия не причем. А если из другого браузера или админ добавляет подписку одновременно с юзером. Тут нужен именно мутекс. А вот насчёт добавления карты повторного это похоже на защиту от флуда..
Не в сети
Может вы о чём-то другом, но изначально вот что спрашивалось и я об этом:
Собственно 5 раз нажмем кнопку "Отправить" и 5 раз данные из формы летят в БД. редирект на эту же страницу даже не успевает отработать.
не вижу здесь никаких пересечений с другим браузером и другим пользователем.
There are two hard things in computer science: cache invalidation, naming things, and off-by-one errors.
Не в сети
Что много раз жать на кнопку что одновременно нажать на нее в разных браузерах суть то одна повторные запросы..
Не в сети
Эммм... нет Вопрос про посетителя и конкретную форму. Если не уловили суть, уточните у автора вопроса.
There are two hard things in computer science: cache invalidation, naming things, and off-by-one errors.
Не в сети
То блокируем user->id mutex'ом на создание новых подписок, в начале процедуры создания подписки и скажем на минуту. И при успешном создании убираем mutex.
Во многих случаях что-либо блокировать вообще не нужно. С теми же подписками - ну добавишь ты подписку два раза, и какие проблемы? В БД это выглядит примерно так:
UPDATE users SET subscribed = 1 WHERE user_id = 123
Или так:
INSERT INTO user_subscriptions (user_id, topic_id) VALUES (123, 456)
В первом случае запрос вернет rowCount = 0, если подписка уже была выставлена. Во втором, учитывая, что user_id+topic_id это либо ПК, либо уникальный ключ - PDO выбросит исключение. Оба случая легко детектируются и не требуют блокировок. Даже если пользователь увидит два сообщения "вы подписаны" вместо одного, то ему будет все равно, если только за это не списывают деньги, конечно (тогда добавляешь транзакции, опять-таки без внешних mutex).
Введите правило, например, что следующее сообщение пользователя может быть не ранее, чем через 1 минуту.
О, аж пахнуло духом phpBB, IPB и прочими WordPress. Зачем это нужно? Если у кого-то задача завалить сайт - так он найдет с десяток других мест для DDoS. Например, авторизация по определению CPU-intensive, т.к. там шифрование и/или хэширование.
Кстати, этот форум (движок fluxbb) имеет такой механизм из коробки.
...Который я благополучно отключил. В современном мире ограничение бесполезное, равно как и пауза при редиректе на другую страницу.
Не в сети
С БД то и так все понятно... Вот в шлюзе (stripe or braintree)создать случайно две подписки одному юзеру значит они дважды будут снимать регулярные платежи...
Дело в том, что braintree позволяет множество подписок для юзера. При том что каждая снимает регулярные платежи. А вот как мы проектируем наш сайт это один юзер и одна активная подписка одновременно. А если использовать запрос сначала к braintree и затем в случае успеха insert в нашу БД, то реально возникает race condition и может наплодиться подписок в braintree. И проверка в самом начале процедуры записи в БД не помогает...
Ну пока лид сказал блокировать кнопку и игнорить... И потом еще подумаем
Изменено htclog81 (22.12.2017 16:34:40)
Не в сети
Вот в шлюзе (stripe or braintree)создать случайно две подписки одному юзеру значит они дважды будут снимать регулярные платежи...
Уж у кого, а у платежных шлюзов защита от двойной траты как раз таки настроена. А со стороны клиента (тебя) решение какое может быть? Ты не узнаешь, отменил пользователь оплату, ушел покурить и вернется, или забыл и уже не оплатит. Если блокировать оплату на 10 минут, например, то будут вопросы "ой мое окошко закрылось а заново я оплатить не могу у вас все сломано".
Два раза оплатить один счет шлюз не даст. Другое дело, если вы генерируете новые счета каждый раз - ну так этого не надо делать, есть открытый счет на ту же услугу - новый не выписывается.
Не в сети
Оплатить именно то есть сделать charge не даст. А вот добавить подписку конечно даст. С точки зрения шлюза юзер имеет право на кучу подписок...
Впрочем мы уже решили их подписками и планами регулярных платежей не пользоваться хоть оно и просто и удобно. А на своей стороне следить за регулярными платежами и делать из крона charge когда подходит время обновить подписку. По большей части нас это не нужно что бы подписка не зависела от одного конкретного шлюза типа braintree.
А у тебя есть опыт подобных разработок?
Изменено htclog81 (22.12.2017 17:17:20)
Не в сети
DB::transaction(function () {
DB::table('users')->update(['votes' => 1]);
DB::table('posts')->delete();
}, 5);
ну или бегин энд
DB::beginTransaction();
DB::commit();
для моей задачи хватило обернуть в транзакцию. странно, что никто из вас не упомянул об этом
-------------------------------------
если нужно заблочить кнопку, то в форму дописываем
<form ... onsubmit="return this.savebutton.disabled=true;">
и кнопка
<input type="submit" value="Сохранить" class="btn btn-success" name="savebutton"/>
и огонь до конца сабмита, кнопарь залочен.
Изменено sam (27.11.2018 17:18:54)
Не в сети
для моей задачи хватило обернуть в транзакцию. странно, что никто из вас не упомянул об этом smile
Странно что тебе этого хватило ведь твоё управление транзакцией происходит в пределах одного http запроса, а изначально ты сформулировал проблему как нечаянные несколько запросов. Казалось бы, при чем тут управление транзакциями!
Видимо есть важный контекст, который здесь не описан.
There are two hard things in computer science: cache invalidation, naming things, and off-by-one errors.
Не в сети