Как стать автором
Обновить
104
0
Валера Леонтьев @feedbee

Пользователь

Отправить сообщение

Ваша статья очень непрофессиональна, там бОльшая часть информации не соответствует действительности (почитайте комменты). Ее лучше удалить вообще и не вводить людей в заблуждения. Два совета, которые реально опасны для здоровья — передачи (убитые на всю жизнь колени) и шлем (травма головы в безобидной ситуации). Удалите, или напишите диклеймер поэро личное мнение и чтобы смотрели комменты. Сейчас вы несёте не пользу людям, а вред.

Этот код будет отлично работать на больших нагрузках. Потому что по одному счету не будет даже 100 обращений в секунду (если будет, то это становится центральным моментом проектирования и код нужно писать иначе, а здесь в примере — это погрешность — это код для 98% биллингов, но не для высокочастотного трейдинга). А заявление о том, что что-то дорого само по себе — ни о чем. Дорого или дешево может быть только относительно.

А спорить о транзакциях и блокировках с вами я больше не буду — это долго и бессмысленно. Проходили, знаем.
Ответ псевдокодом (субъективно):

function transferMoney(value, fromWalet, toWalet) {
  lock(fromWalet); // throws CantAquireLockException after timeout
  try {
    if (!fromWalet.hasFunds(value)) {
      throw new InsufficientFundsException(fromWalet, value);
    }
    try {
      SomeTransactionalSystem.begin();
      fromWalet.addFunds(-value);
      toWalet.addFunds(value);
      SomeTransactionalSystem.commit();
    } catch (Exception e) {
      SomeTransactionalSystem.rollback();
      throw e;
    }
  } catch (Exception e) {
    unlock(fromWalet);
    throw e;
  }
  unlock(fromWalet);
}


Таким образом transferMoney становится полностью самостоятельной процедурой, которая делает свою работу, ничего не возвращает и кидает исключения, когда не может ее сделать. Она не нуждается в блокировках на уровне выше. При этом никто не мешает на уроне выше повторить проверку fromWalet.hasFunds(value), что бы не доводить до исключения. Просто это будет вне блокировки и при параллельной работе с одним fromWalet может возникнуть ситуация, когда InsufficientFundsException все же вылетит.

или возвращать класс, содержащий информацию об ошибке?

Возвращать что угодно, содержащее сообщение об ошибке, должен только метод на подобии getError или getLastError. Есть 2 варианта — проверка (валидация) (никаких исключений и возврат true/false) и выполнение (в случае ошибки — исключение). В данном случае я бы предпочел сочетание обоих, т.е. сначала проверить fromWalet.funds < value, потом вызвать transferMoney. Проверка прямо скажет true/false
Вот то, что я рассказал бы про исключения тому, кто пока не умеет ими пользоваться.

  • Исключения нужно использовать тогда и только тогда, когда возникает развитие событий, не предусмотренное нормальным ходом работы приложения — исключительной ситуации. При этом причина может быть как статической (например, логическая ошибка в коде), так и динамическая (например, недоступность ресурсов).
  • Исключения нужно кидать максимально точно (узко) типизированными.
  • Исключения замечательны для решения своей задачи — прерывания процесса с информированием о возникшей проблеме — причине прерывания, потому что они всплывают по стеку до нужного места. Для других задач они не подходят.
  • Обрабатывать исключения нужно там, где их одновременно возможно и уместно обработать.
  • В прикладном ПО большинство бросаемых на практике исключений не обрабатываются, перехватываются в самой высокой точке стека и попадают в лог, а пользователь получает ошибку 500 «Что-то пошло не так».
Забыл сразу посчитать стоимость за публичный IP, так что вношу изменения в калькуляцию и итоговый текст. :(

1 ядро CPU — 370 руб.
1 Гб памяти — 148 руб.
8 Гб диск — 54 руб. (для 323 руб. для быстрого)
10 Гб трафика — 0 руб.
1 IP — 101 руб.
(Цены я округлил до рублей.)

Итог — 681 рубль с базовыми дисками. Т.е. получается цена примерно то на то как и в старом облаке, но заметно медленнее, особенно (я предполагаю) по диску. С быстрым диском получается 941 рублей, что уже на 30 % дороже, чем старое облако. Т.е. уже ощутимо дороже, но по эластичности машины только по процессору просадка. А если брать 2 ядра (для обеспечения работы на пиках), то получится совсем некрасивая цифра больше 1000.

Вот это мне и не нравится больше всего в новом облаке. Т.е. цены там нормальные более-менее, НО эластичность машины намного хуже. Т.е. отсутствует очень клевая фича — масштабирование слабой машины без участия пользователя. Понятно, что когда у вас парк машин, вы просто включаете и выключаете машины для масштабирвоания. А вот когда машина одна, нужно уже внутри нее регулировать нагрузку. С VPC это невозможно.
Могу предложить сравнение по цене для моего мелкого сервера, который работает в старом облаке у кушает 650 рублей в месяц.

Потребление процессора в среднем около 10 % от одного ядра. В пиках больше 1 ядра не жрет, так что можно рассчитывать на 1 ядро для VPC, хотя может это и не корректно, учитывая что в старом облаке мне были доступны 8 ядер в любой момент времени, а тут будет только одно, чего может оказаться мало при резкой нагрузке.

Память. Используется MoD, но в среднем на 1 Гб висит.

Диск — 8 Гб. Запросы к диску и трафик с ним значения для сравнения цены с VPC не имеют, но так как нагрузка малая, сравнивать буду с базовым диском. Впрочем, это тоже не совсем корректно, потому что скорость диска в старом облаке явно выше скорости базового диска в новом.

Сетевой трафик — ~10 Гб, использовался 1 IP.

Этот сервер, напомню, ежемесячно обходится в среднем в 650 рублей.

Теперь рассмотрим во что обойдется аналогичный сервер (только менее резиновый) в случае VPC.

1 ядро CPU — 370 руб.
1 Гб памяти — 148 руб.
8 Гб диск — 54 руб. (для 323 руб. для быстрого)
10 Гб трафика — 0 руб.
(Цены я округлил до рублей.)

Итог — 570 рублей с базовыми дисками. Т.е. получается чутка дешевле, чем в старом облаке, но заметно медленнее, особенно (я предполагаю по диску). С быстрым диском получается 840 рублей. Т.е. уже ощутимо дороже, но по эластичности машины только по процессору просадка. А если брать 2 ядра (для обеспечения работы на пиках), то получится совсем некрасивая цифра больше 1000.

Вот это мне и не нравится больше всего в новом облаке. Т.е. цены там нормальные, получается даже ниже, чем в старом, НО эластичность машины намного хуже. Т.е. отсутствует очень клевая фича — масштабирование слабой машины без участия пользователя. Понятно, что когда у вас парк машин, вы просто включаете и выключаете машины для масштабирвоания. А вот когда машина одна, нужно уже внутри нее регулировать нагрузку. С VPC это невозможно.
Жалко, что убиваете «Облачный сервер». Для вас эта услуга, наверное, не сильно выгодна. А вот для клиента как раз наоборот — выгодна. Оплата только за фактические потребленные ресурсы — очень хорошая штука для машин с малой степенью утилизации, но с большими пиковыми нагрузками. Там было 8 ядер на машину доступны, а в VPC если машину на 8 ядер брать, это будет просто золотая машина — будет дороже более чем в 10 раз при малом потреблении. Т.е. «Облачный сервер» — это была ниша. И было бы круто ее оставить. Но, повторюсь, для вас (как и для других облачных провайдеров — я не знаю, у кого еще такое есть) скорее всего такой сервис малорентабелен. MoD тоже хорошая штука.
Есть такое белорусское имя — Ян. Так вот хорошее название для машины жены или любовницы мужчины с таким именем.
Ага, по крону удалить успешно закачанный файл…

А представьте, что мы не файлы закачиваем, а серверы выдаем. Или что-то еще дороже.

Нет здесь косяка — надежное соблюдение бизнес-логики — это обязательное требование. Или можете считать так: в моем примере это именно обязательное требование со стороны бизнеса.
Вытекающие будут из serializable. А на счет read committed + select with shared lock, это уже несколько другая плоскость. Тут требуются явные дополнительные действия (как и в случае с обычными писсимистическими блокировками вне БД). Ну и требуется отлавливать падения на дедлоках (именно так оно выглядит в случае MySQL) и запускать транзакцию снова и снова, пока она таки не пройдет успешно. Об этом надо как минимум знать. И это не всегда удобно. А у 51 % опрошенных «этот вопрос вообще никак не стоит».

Оба решения имеют свои плюсы и минусы. Оптимистические блокировки не всегда лучше, чем писсимистические. А когда мы выходим за пределы БД, использование транзакций для синхронизации может стать проблемой. Так что в идеале нужно выбирать каждый раз индивидуально. И с этим могут быть проблемы, потому что мало кто из PHP-программистов на столько хорошо владеет темой, как, например, вы.
Ну то есть либо выбрать другой уровень изоляции (для MySQL в данном случае потребуется SERIALIZABLE со всеми вытекающими), либо выбрать другое решение.
Ниже уже разобрались, что для MySQL с уровнем изоляции по умолчанию это не так.
Истину глаголите в частном случае. Но в общем случае, надо отметить, что данное ограничение на количество висящих коннектов у меня существует не для и не только из-за блокировок. Даже если бы не было ни единой блокировки, все равно была бы эта защита от DoS. Т.е. я не плачу дополнительно, а просто пользуюсь и так работающим функционалом.
Если это веб-интерфейс, то скорее всего лучше будут асинхронные запросы на проверку каждого агента отдельно. Тогда если один или несколько агентов будут долго отвечать, это будет видно интерактивно.

А вот если это консольное приложение, демон (сервис)… Тут момент такой, что ждать ответа по сети можно и асинхронно. Для этого не нужно заводить треды. Тот же eventloop отлично подойдет. Особенно, если агентов много (ну сотни, например). Ну под линуксом во всяком случае. Треды они все же нужны для активной работы, а не пассивной, имхо. Но тут я могу быть неправым, т.к. сам-то их никогда почти и не юзал…
Если погрешность допустима, то можно просто использовать неблокирующее решение, в чем проблема-то?

Погрешность допустима при подсчете количества запросов, которые могут работать одновременно для пользователя и/или IP. Будет их 10 или 12, разницы никакой (ну ~100 Кб памяти разница на время таймаута блокировки). А вот если применить неблокирующее решение задачи, то мы получим взлом системы — юзер загрузит больше файлов, чем можно. Это уже проблема. Ну или я неправильно вас понял. Потому что если под неблокирующим решением вы понимали lock-free алгоритмы, т.е. тот же CAS, то такое решение я уже выдавал в самом начале — UPDATE table SET fact = fact + 1 WHERE id=1 AND limit > fact + affected rows.
А блокировки можно делать штатными средствами MySQL, без flock и т.д.

Ну это если MySQL есть :) Ну и возможности блокировок там все же ограничены.

Через несколько лет возможно появится в проектах.

Спрос очень мелкий… А проблем оно может притянуть массу. Так что может и не появиться. Вот какие реальные юзкейсы, где многопоточность выиграет у многозадачности (=многопроцессности)?
Так вот достаточно добавить ограничение на количество открытых динамических запросов с одного IP и/или от одного пользователя.
… и как вы его сделаете без транзакции?

Т.к. здесь допустима погрешность, при можно применить гарантированно быструю операцию без синхронизации. Но вообще, если уже придираться, то можно использовать атомарный инкремент в Memcache или Redis. Ну а можно и просто писать в общую память и использовать примитив синхронизации, если сервер один.

У меня такая штука сделана без синхронизации и без транзакции, я ее тестил через ab, результаты были очень хорошие.
В MySQL много чего кривого, но только работает она быстро и жрет относительно мало. Плюсов тоже хватает. И ей дефакто пользуются практически все PHP-девелоперы. А весь топик посвящен именно PHP-девелоперам.

Можно углубляться сколько угодно, только вот у задачи со счетчиком есть конкретное решение, которое будет прекрасно работать и под нагрузкой (и я не согласен, что нагрузка и блокировки не совместимы). Так как в данном примере мы блокируем только одного пользователя, и это защита от взлома, она не будет проявляться при нормальной работе. Так вот достаточно добавить ограничение на количество открытых динамических запросов с одного IP и/или от одного пользователя. Первые N запросов от него действительно будут поедать память сервера и «висеть» на блокировке, но после превышения лимита запросы будут сразу отваливаться. А ожидание блокировки должно быть определено таймаутом, после которого и ждущие будут тоже отваливаться. Ну и не нужно делать блокировки, которые будут висеть десятки секунд. И транзакции тут тем более не подойдут. Когда время ожидания большое, нужно действовать асинхронно.

Сделав все правильно, оно будет хорошо работать под большой нагрузкой, под которую вообще имеет смысл писать PHP-приложение. Вон тот же Хабр, например.
А вот при попытке повторить тоже самое при SERIALIZABLE я уже как и ожидалось получаю блокировку, а в итоге второй запрос получает:

#1213 - Deadlock found when trying to get lock; try restarting transaction 


Причем, запрос на модификацию значения во второй транзакции висит на блокировке, пока не я изменю в первой транзакции значение. А как только выполнил UPDATE, в тот же момент вторая транзакция отвалилась с обозначенным выше сообщением о дедлоке.

Опять же копипаст из консоли
mysql> SET SESSION tx_isolation='SERIALIZABLE';
Query OK, 0 rows affected (0.00 sec)

mysql> START TRANSACTION;
Query OK, 0 rows affected (0.00 sec)

mysql> SELECT `limit`, fact FROM users WHERE id = 1;
+-------+------+
| limit | fact |
+-------+------+
|     2 |    3 |
+-------+------+
1 row in set (0.01 sec)

mysql> UPDATE users SET fact = fact + 1 WHERE id = 1;
Query OK, 1 row affected (0.00 sec)
Rows matched: 1  Changed: 1  Warnings: 0

mysql> COMMIT;
Query OK, 0 rows affected (0.00 sec)

mysql> SELECT `limit`, fact FROM users WHERE id = 1;
+-------+------+
| limit | fact |
+-------+------+
|     2 |    4 |
+-------+------+
1 row in set (0.00 sec)

mysql>



Так что точно могу сказать, что здесь все совершенно не тривиально, как это кажется изначально. SNAPSHOT isolation и READ COMMITTED SNAPSHOT в MySQL не. (MySQL — это default СУБД для PHP-разработчика.)
REPEATABLE READ ничего не блокирует. А SERIALIZABLE блокирует. Об этом написано. И это прослеживается на практике. Ниже в спойлере копипаст из консоли mysql. После первого селекта в другой консоли было изменено значение fact на 2. Как видно, UPDATE в данной странзакции сделал его 3-кой.

Копипаст из консоли mysql
mysql> START TRANSACTION;
Query OK, 0 rows affected (0.00 sec)

mysql> SELECT `limit`, fact FROM users WHERE id = 1;
+-------+------+
| limit | fact |
+-------+------+
|     2 |    1 |
+-------+------+
1 row in set (0.00 sec)

mysql> UPDATE users SET fact = fact + 1 WHERE id = 1;
Query OK, 1 row affected (0.00 sec)
Rows matched: 1  Changed: 1  Warnings: 0

mysql> COMMIT;
Query OK, 0 rows affected (0.00 sec)

mysql> SELECT `limit`, fact FROM users WHERE id = 1;
+-------+------+
| limit | fact |
+-------+------+
|     2 |    3 |
+-------+------+
1 row in set (0.00 sec)



И я еще раз подчеркиваю, что да, возможности синхронизации через БД тут есть, но:
  1. они не работают по умолчанию, т.е. нужно менять уровень изоляции;
  2. требуется такой уровень изоляции, который под нагрузкой все уложит и преведет к дедлокам на дедлоке в случае MySQL.


Так что не делая здесь хоть какую-то явную блокировку, получаем дырку. А теперь вопрос, в каком количестве случае это делается правильно, а об этом даже не думают?
1
23 ...

Информация

В рейтинге
Не участвует
Откуда
Минск, Минская обл., Беларусь
Дата рождения
Зарегистрирован
Активность