Русское сообщество разработки на PHP-фреймворке Laravel.
Ты не вошёл. Вход тут.
Страницы 1
Доброго времени суток! Есть ли у laravel возможность сделать так, чтобы при вызове авторизованного пользователя, нельзя было сделать новый вызов до полной отработки предыдущего запроса? Спасибо.
Вообще задача такая - запретить обработку множественных нажатий на кнопки, так как сервер не успевает отрабатывать и принимает много запросов, а самый первый всё ещё выполняется.
Изменено WarShumer (26.01.2017 17:28:18)
Не в сети
нельзя в принципе архитектуры языка.
решение
1 - блокируй нажатую кнопку яваскриптом
2 - при обращении, в контроллере в бд пиши флаг "занято", как - придумай сам, по окончании работы скрипта/контроллера = снимай этот флаг.
3 - в кроне поставь снимать все флаги старше 15 минут (неотработанные до конца по какой-то причине, например вылету по эксепшену)
Не в сети
Не в сети
Вполне можно с помощью flock()
что-то колхозное решение. когда нужны блокировки, я первым делом смотрю на row-based locking в mysql. блокировать обращение к конкретным ресурсам – пользователям, заказам, счётчикам – проще в контексте транзакций, так как во-первых, блокировки получаются всегда предельно локализованными (не влияют на те обработчики, которые не зависят от обрабатываемых данных), а во-вторых не бывает «зависших» блокировок – когда транзакция завершилась (коммитом или роллбэком – неважно), блокировка обязательно снимется. бонусом (поскольку используются транзакции) гарантируется непротиворечивое состояние данных в базе – даже если код покрэшится в процессе работы, никаких изменений не будет произведено, и плюс возможность отменить все изменения если в процессе обработки обнаружилась ошибка (элементарным способом – просто достаточно кинуть исключение).
в ларавеле эти механики обеспечиваются наверное наиболее удобным для разработчика способом – DB::transaction() и lockForUpdate() на построителе запросов. это реально намного проще, удобнее и при этом – надёжнее, чем блокировки на файлах
Не в сети
Транзакции и блокировки файлов существуют для совершенно разных целей. Транзакции — для целостности данных, а flock — для общего механизма «ждать пока работает соседний процесс». Если нужно запретить два параллельных запроса, то каким образом ты предлагаешь использовать транзакции?
- что-то колхозное решение.
Чем оно колхозное? Используется абсолютно везде в Linux, хоть в том же apt.
- предельно локализованными (не влияют на те обработчики, которые не зависят от обрабатываемых данных)
if ($needToBlock) {
$h = fopen('my.lock', 'r+');
flock($h, LOCK_SH);
// wait
// do
fclose($h);
}
// don't want
Ещё интересное применение — т.к. другие процессы могут читать файл, но не писать, то можно в нём держать состояния выполнение, например, дату/время запроса, который сейчас обрабатывается. Если блокировка висит долго это поможет понять, чем она занята, без прерывания процесса.
- не бывает «зависших» блокировок
- On versions of PHP before 5.3.2, the lock is released also by fclose() (which is also called automatically when script finished).
После 5.3.2 можно вызвать fclose без снятия flock, но в любом случае с flock тоже не бывает зависших блокировок, т.к. при завершении процесса (потока) PHP, даже если он упал, система освобождает его ресурсы, вместе с этим освобождая и замок на файле.
- бонусом (поскольку используются транзакции) гарантируется непротиворечивое состояние данных в базе – даже если код покрэшится в процессе работы, никаких изменений не будет произведено, и плюс возможность отменить все изменения если в процессе обработки обнаружилась ошибка (элементарным способом – просто достаточно кинуть исключение).
Это не бонус, это задача транзакций. Конечно, flock’ом такого не добиться. Но не понятно, зачем дёргать БД, если этого явно не требуется по задаче. Это два разных механизма под разные цели.
- это реально намного проще, удобнее и при этом – надёжнее, чем блокировки на файлах
Не в сети
Чем оно колхозное? Используется абсолютно везде в Linux, хоть в том же apt.
ок, возможно с «колхозным» я погорячился. но вот этот ответ совершенно точно отвечает на вопрос где самое подходящее место для блокировок на файлах – в юзерленде, в утилитах, которые именно с файлами и работают. в вебе нам эти механизмы конечно тоже доступны, но есть способ проще. давай лучше на примере.
допустим у нас есть модель Something с полями вида key varchar(255) и value int. и мы пишем экшен вида:
public function addValue() {
app('db')->transaction(function () {
$record = \App\Something::whereKey('somekey')->first();
usleep(10000);
$record->value++;
$record->save();
});
return ['success' => true];
}
немного примитивно, в реальном приложении логика будет более сложной, но для демонстрации подойдёт. и вот выпускаем мы этот код в продакшен, и тут от клиента приходит баг-репорт: «Вы знаете, если пользователь дважды нажмёт на кнопку, иногда значение увеличивается только на 1 а не на 2 как должно». сложно ли добавить в этот код блокировки? можно конечно наворотить с файлами, но (!!!) нам нужно будет в имя файла добавить все идентификаторы всех сущностей, на которые распространяется эта блокировка. если мы по тупому сделаем один файл блокировки «на всё» – мы заблокируем в том числе и другие действия, которые вообще-то не должны блокироваться , так как никак не связаны с логикой этого экшена.
и вот тут есть другое решение, которое недоступно «везде в Linux, хоть в том же apt». для него нам нужно внести просто «гигантские» изменения в вышеприведённые код:
public function addValue() {
app('db')->transaction(function () {
$record = \App\Something::whereKey('somekey')->lockForUpdate()->first();
usleep(10000);
$record->value++;
$record->save();
});
return ['success' => true];
}
всё.
в реальном коде, если нам нужны блокировки по пользователям – достаточно выбрать запись пользователя с lockForUpdate, для блокировок счётчиков – выбрать счётчик. собственно эти выборки в коде уже есть, раз эти данные обрабатываются, остаётся только выбрать их чуть иначе и обернуть код в транзакцию
mysql тут немного подводит – в нём транзакции не могут быть вложенными, но ларавель – спасает. он отслеживает начало и конец транзакций на подключениях и если открывается транзакция внутри транзакции – просто увеличивает внутренний счётчик. в итоге всё работает так как будто вложенные транзакции поддерживаются в БД. это позволяет не задумываться о таких вещах и спокойно использовать транзакции в любых методах любых классов и спокойно вызывать эти методы внутри транзакций – всё будет работать так как и ожидаешь
это просто намного проще и быстрее. блокировки на файлах по сравнению с этим не дают никаких преимуществ, разве что только устроить глобальную блокировку приложения проще, но – смысл?
Изменено constb (28.01.2017 14:10:32)
Не в сети
- в реальном коде, если нам нужны блокировки по пользователям – достаточно выбрать запись пользователя с lockForUpdate, для блокировок счётчиков – выбрать счётчик. собственно эти выборки в коде уже есть, раз эти данные обрабатываются, остаётся только выбрать их чуть иначе и обернуть код в транзакцию
Всё правильно, ты првёл отличный пример транзакций, но не ответил на мой конкретный вопрос:
- Если нужно запретить два параллельных запроса, то каким образом ты предлагаешь использовать транзакции?
Ещё раз — транзакции и блокировки на файлах это совершенно разные вещи. flock — более общий механизм, на нём можно реализовать и транзакции, конечно, но если БД уже есть — то велосипед изобретать не нужно, используешь транзакции БД и всё.
Транзакции предполагают параллельную работу, в отличии от flock, где она последовательна. (Я сейчас говорю о БД-транзакциях; в Redis, например, «транзакция» это блокировка всех потоков, как flock.)
Я к чему. Это два разных механизма и использовать их надо в разных случаях. flock очень легковесный механизм. Не надо пугаться, что «в моём любимом фреймворке нет фасада для блокировок на файлах». Тут три вызова, аналог BEGIN-COMMIT/ROLLBACK, путаться реально негде. Другое дело, что в большинстве приложений нужны именно транзакции. Но мы говорим о конкретном use case, когда нужно заблокировать параллельную работу запросов. Если не обсуждать постановку вопроса (для чего такая блокировка нужна) — то flock намного адекватней, чем велосипед на транзакциях.
Не в сети
Есть сайт и есть тяжёлая задача, которая блокирует его работу целиком. Например, загружает 100500 записей в БД. Можно использовать транзакции, но это вызовет дикие задержки — сначала БД должна хранить все эти новые записи в журнале, потом при коммите перекачивать их в основное состояние, при этом отслеживая мелкие изменения от обычных запросов, делая двойную работу. Если это и не будет тормозить, то свалится с transaction timeout или deadlock. Поэтому проще отключать сайт минут на 10 ночью, и дать БД спокойно пережевать все данные в один поток.
Как это сделать? Можно создать файл типа maintenance-in-progress.html (который nginx будет автоматом отдавать в ответ на любой вопрос, если этот файл существует) или установить в БД какой-то флаг. А что делать, если процесс упал? Держать запущенным ещё watchdog, который удалит файл/сбросит флаг через 10 минут? А если 10 минут мало, надо 11? Или процесс завершился за 5 минут, а мы ещё 5 будем ждать, прежде чем открыть сайт?
Простое решение — flock. Что бы с сервером ни случилось, когда процесс завершится, блокировка уйдёт. Если процесс упал и не смог удалить файл — watchdog пытается его удалить каждую минуту и как только получилось — значит, процесса нет, сайт открылся.
Не в сети
Есть сайт и есть тяжёлая задача, которая блокирует его работу целиком
ну это вообще говоря вполне себе такой edge case. я же про задачи вполне повседневные. фактически ты описываешь свой собственный maintenance mode, ну так artisan включает и выключает его фактически создавая файл в storage/framework – если этот файл flock-ать, то получится то же самое, только с автоматической разблокировкой по окончанию выполнения
я сразу могу сказать где ещё ларавель использует блокировки на файлах – cron scheduler, если на таску сделать withoutOverlapping, создаст лок по имени таски. понятно почему он делает это именно так: потому что базы может не быть, редиса может не быть, ничего может не быть, но файлы есть 100%
но вот рассмотрим другой пример – нам нужно оформить заказ в магазине. во-первых, оформление заказа содержит множество операций – проверка баланса, проверка наличия на складе, списание с баланса, списание со склада, создание заказа (модель Order), создание связанных записей товары заказа (OrderItems), очистка корзины – это минимум. чтобы при крэше на полпути нам не получить базу в противоречивом состоянии – использование транзакций обязательно. так? так.
далее, во-вторых, надо проверить баланс пользователя. если мы обратимся к Request::user()->balance мы можем получить race condition, так как мы не можем стартовать блокировку _до_ выборки Request::user(), а значит баланс до блокировки может уже быть изменён. то есть нам надо установить блокировку и потом заново запросить юзера из базы. так? так.
в-третьих операции с корзиной и списанием товара – эти данные тоже должны быть выбраны из базы после того как блокировка установлена, и тут сразу вопрос – а на что именно блокировка? если мы блокируем на файлах по имени например storage/app/orderProcessing – блокировка получается чрезмерно пессимистичной (пока идёт оформление заказа одним пользователем, у других процесс висит), а хуже того – все операции меняющие баланс, товары и корзину – сюрприз! – тоже должны устанавливать эту блокировку, что совсем уже контринтуитивно!
значит получается нам нужно разделить блокировку и устанавливать их несколько. блокировка на user-$user_id, на корзину и на каждый из товаров? фактически получается нам нужно _продублировать_ уже имеющийся функционал mysql, наплодить файлов и в итоге всё равно получить вагон багов – потому что если мы ставим блокировку на user-$user_id – везде в коде где выбираются пользователи, нужно эту блокировку проверить! и так далее – либо global lock, либо колоссальное количество работы либо вагон багов
а можно проще – раз мы и так выбираем весь обрабатываемый набор данных из базы в начале транзакции, мы можем просто выбрать их с lockForUpdate и быть уверенными, что оно просто отработает само, правильно и нигде ничего не вылезет – все остальные запросы, которые попробуют обратиться к тем же данным – просто немного подождут. блокировки получатся минимальными (только на конкретные данные) и главное – практически бесплатно! потому что механизм row-based locking фактически является основой механизма транзакций и не требует дополнительных накладных расходов
что касается задачи, перелопачивающей всю базу, то как раз тут транзакции во-первых не дадут «перелопатить её наполовину а потом бросить как получилось из-за эксепшена», а во-вторых, сайт будет продолжать работать пока идёт «перелопачивание». что касается накатывания изменений из журнала, я уверен, что mysql ничего никуда не копирует. просто где-то меняется запись «эти данные лежат вот тут» на «эти данные лежат вон там», а прежде занятые страницы помечаются как свободные. в любом случае тут надо смотреть на конкретный кейс и возможно сама архитектура, требующая таких «перелопачиваний» нуждается в изменении
в любом случае блокировка не означает обязательно global lock, блокировки могут и должны быть минимальными насколько возможно – только на используемые ресурсы, и прозрачными для остального кода
что насчёт редиса, я сомневаюсь что редис однопоточен и использует глобальные блокировки – такое вообще-то не следует логически из того что все операции в нём атомарны. по-моему он вполне себе многопоточен и блокировки выставляет только на обрабатываемые в данный момент времени ключи
Не в сети
- то как раз тут транзакции во-первых не дадут «перелопатить её наполовину а потом бросить как получилось из-за эксепшена»
Проблема не в откате результатов, а в скорости импорта под нагрузкой и слияния после COMMIT.
- а во-вторых, сайт будет продолжать работать пока идёт «перелопачивание».
Не будет. То есть, конечно, разные сайты бывают, но мой опыт говорит, что при очень больших таблицах (от пары млн записей) и связях в них импорт даже десяток тысяч записей вешает большую часть страниц. Другое дело, что архитектура должна позволять вносить изменения без блокировки половины таблиц, но это уже как повезёт.
Вообще, ты мне доказываешь то, с чем я не спорил. Транзакции покрывают большую часть случаев, а те что не покрывают ты и сам назвал.
- что насчёт редиса, я сомневаюсь что редис однопоточен и использует глобальные блокировки – такое вообще-то не следует логически из того что все операции в нём атомарны. по-моему он вполне себе многопоточен и блокировки выставляет только на обрабатываемые в данный момент времени ключи
Атомарность никак не связана с многопоточностью.
- All the commands in a transaction are serialized and executed sequentially. It can never happen that a request issued by another client is served in the middle of the execution of a Redis transaction. This guarantees that the commands are executed as a single isolated operation.
- Redis uses the same Lua interpreter to run all the commands. Also Redis guarantees that a script is executed in an atomic way: no other script or Redis command will be executed while a script is being executed. This semantic is similar to the one of MULTI / EXEC. From the point of view of all the other clients the effects of a script are either still not visible or already completed.
- Scripts are also subject to a maximum execution time (five seconds by default). This default timeout is huge since a script should usually run in under a millisecond. The limit is mostly to handle accidental infinite loops created during development.
EVAL
Redis построен на однопоточной модели, даже скрипты (Lua) там однопоточные — как JavaScript. Это делает его очень простым и на удивление быстрым за счёт очень быстрых базовых операций, которые блокируют весь сервер только на крайне короткое время.
Не в сети
я сразу могу сказать где ещё ларавель использует блокировки на файлах – cron scheduler, если на таску сделать withoutOverlapping, создаст лок по имени таски. понятно почему он делает это именно так: потому что базы может не быть, редиса может не быть, ничего может не быть, но файлы есть 100%
я кстати ошибся, сегодня залез в код Illuminate\Console\Scheduling\Event – его в какой-то версии успели переписать чтобы он создавал блокировки в кэше – так что теперь файлы он создаёт на withoutOverlapping только если используется файловый кэш
Не в сети
Страницы 1