All streams
Search
Write a publication
Pull to refresh
63
0.5
Михаил @michael_v89

Программист

Send message

потому, что используют пулы соединений и блокировать процессы нельзя

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

потому что так быстрее

Можно тогда просто рандомные значения писать, еще быстрее будет.
Это не вопрос какой-то оптимизации, это вопрос правильной обработки данных. Поймите это.
Вы сами в статье писали "Для увеличения гранулярности блокировок можно было пойти дальше и дать возможность транзакциям, меняющим разные столбцы, обновлять совместно строку, но это не реализовано и вряд ли будет реализовано." Тут то же самое, только на уровне бизнес-процессов.
Нельзя делать бизнес-проверки и принимать решения об изменениях по неактуальным данным, которые могли измениться микросекунду назад в другом процессе. Сначала надо гарантировать, что данные будут актуальны до завершения всех проверок и изменений.

Логика «зачем процессу напрягаться»

У меня нет логики "зачем процессу напрягаться". Вы даже не понимаете о чем идет речь.

код на javascript, даже похоже на Node.js

Node.js это серверный процесс, но у меня просто псевдокод для пояснения.

Зачем решать блокировками то, что решается констрейнтами?

Потому что блокировки это и есть инструмент для решения вопросов параллельного доступа, их для этого и придумали.
Я же вам объяснил, что это не решается констрейнтами. Ваш констрейнт "больше нуля" не предотвратит одновременное списание со 100 2 раза по 10, без блокировок будет 90, а не 80. А с блокировками констрейнт не нужен.

что и делает блокировка, создавая узкое место

Еще раз объясняю, это не узкое место, оно так и должно работать. А вы хотите сделать как в анекдоте "Я печатаю 1000 знаков в минуту, но такая фигня получается".

Это блокировка не на всю таблицу, а на одну конкретную строку. Процесс, который хочет прочитать другую строку, не будут приостановлен. Никакого узкого места тут нет, даже при большой нагрузке с одной конкретной строкой обычно работает не так много процессов, в большинстве случаев один. Два их становится обычно при намеренном действии.

В статье написано: update account set balance = balance -10. Будет 80.

Ну дак я о том и говорю, что защищает не констрейнт.

Конкретно с этим запросом да, будет 80, только вы еще рекомендуете добавлять условие WHERE balance >= 100. Поэтому первый процесс спишет 10, а во втором "balance >= 100" будет false, и списание не произойдет. Поэтому будет 90, хоть и не по той причине, которую я имел в виду.
Я имел в виду, что если делать так, как сделали FlexCoin, но добавить констрейнт. Если бы он защищал, то этого было бы достаточно.

Кроме того, с вашим запросом во втором процессе обновится 0 строк без всякой ошибки. То есть надо еще и в приложении проверять сколько строк обновилось и бросать исключение если 0.
Только смысла в этом нет, потому что если между ними будет еще третий процесс, который добавит 10 на баланс, то второй процесс списания прочитает 100, WHERE будет true, и второе списание произойдет. То есть вы создали плавающий баг на ровном месте.

Это решается 2 простыми строками, и стабильно работает с любой последовательностью действий, мне непонятно почему вы не хотите использовать предназначенный для этого инструмент, и хотите вместо этого усложнять запросы и объявления таблиц.

this.lockService.lock('Account', id);
...
this.lockService.unlock('Account', id);

Как минимум, две команды SELECT и UPDATE можно было бы заменить на UPDATE

Нет, нельзя. Кроме обновления строки нам в этом if нужно сделать еще кучу действий. Все они зависят от значения флага is_bonus_received. Некоторые из них могут быть нетранзакционными.
А перед выполнением этого действия нужно сначала прочитать пользователя из базы, вернуть 404 если его нет, проверить права доступа пользователя на это действие, вернуть 403 если их недостаточно, проверить значение is_bonus_received, если true показать понятную ошибку "Вы уже использовали бонус", и только потом делать начисление бонуса с обновлением строки.

FOR UPDATE и ORM тут излишни

Поэтому нет, не излишни. FOR UPDATE придуман именно для таких случаев.

Двойного списания уже не было бы.

Тут нет двойного списания, тут надо предотвратить двойное зачисление. Оно производится в addBonusForUser и может включать обращение к платежному провайдеру по сети.

в одном (или больше) потоков будет заблокирован целый серверный процесс (или процессы)

Дак в том и смысл, чтобы другие процессы остановились и не работали с этой записью и даже не читали ее, пока первый процесс не произведет все вычисления. Зачем им читать заведомо неактуальное значение и потом выполнять действия, которые от него зависят?

чтобы фронтэндер не занимался тем, чем ему не нужно заниматься

Какой еще фронтендер?) Это серверный код, фронтенд не отправляет запросы в базу.

Про двойные нажатия - проблему встречал и отдельно описывалось, как фреймворк решает эту проблему.

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

Ну как это нет, если вы его и пытаетесь предотвратить. Зачем иначе вам нужны эти проверки?

то из этого не следует: не используйте констрейнты

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

Но компании так были поглощены модной в то время темой (за рамками статьи), что даже транзакции не использовали.

В описанном примере это связано не с транзакциями, а именно с отсутствием блокировок.

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

Ну я вам и объясняю, что нет. Уязвимость там была в том, что два процесса читали одинаковое изначальное значение баланса, и рассчитывали финальное значение без учета эффектов, которые другой процесс произведет после завершения. Если с баланса 100 списывать 2 раза по 10, то в этом случае финальное значение будет 90. Оно больше нуля, но оно неправильное, и ваша проверка "balance>=0" это не предотвратит.

Изменения ресурса надо делать только последовательно, это совершенно несложно, и не надо возиться с констрейнтами.

Причем тут PostgreSQL? Он говорит, что отрицательный баланс может быть разрешен по бизнес-требованиям.

чтобы баланс стал отрицательным

Я говорю про неправильную работу при положительном балансе. У меня на балансе миллион, я уменьшаю его на 1 в двух параллельных процессах. Ни при какой комбинации результат не будет меньше нуля, будет 999999 или 999998. При этом любой результат из этих двух может быть правильным в зависимости от сценария. Для оплаты за один заказ мы не хотим 2 списания, для снятия денег два раза хотим. Ваш constraint "balance >= 0" в этом случае не предотвращает неправильную обработку. Правильность зависит не от того, какой стал баланс, а от того, сколько раз нам надо выполнить действие.

В самом первом комментарии.

покажите, что не защитит

В смысле? Вы не верите, что результат выражения balance = balance - 1 будет больше нуля, если balance равен 2?)

Вы зачем это мне все рассказываете ?

Затем, что вы не понимаете, почему в приложении используется SELECT FOR UPDATE, и говорите, что его надо убрать. Убирать его нельзя. Можно использовать альтернативу, но если есть проблемы с SELECT, то они будут и с альтернативным решением.

По личному опыту, я твердо уверен - как работает СУБД они понятия не имеют, не знают и знать не хотят .

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

никто , ни разу, не обращался к DBA на этапе проектирования и разработки.

А что бы вы сделали на этапе проектирования и разработки, чего нельзя сделать после запуска?

у нас ничего не работает , все висит, мы уперлись в СУБД

Значит вы уперлись в СУБД. Иное можно сказать только после анализа последовательности запросов из приложения, чего как я понимаю вы не делаете. Это ваше дело сказать "Эти запросы надо переписать вот так, тут поменять условие, чтобы индекс работал, тут идут запросы в цикле, надо их убрать, тут не надо делать INSERT INTO ... SELECT". DBA в компаниях, где я работал, занимались именно этим.

Значит так и есть, они уперлись в СУБД. Еще раз, без этого из приложения работать нельзя, будут возможны race condition и двойные списания как в примере из статьи.

Может быть в приложении неправильно организован порядок этих SELECT FOR UPDATE, но без анализа кода приложения так сказать нельзя. Если сам фреймворк так делает, то мне это кажется маловероятным.

поток UPDATE в разных транзакциях и разных процессах на одной таблице

Мы говорим про SELECT FOR UPDATE, сначала происходит SELECT строки, потом UPDATE. В этом случае SELECT во всех процессах будет ждать своей очереди. Если у вас что-то падает, значит или процессов слишком много, или обработка в приложении делается слишком долго, и таймаут ожидания блокировки в базе для одного конкретного процесса истекает. Надо увеличить таймаут, или попробовать оптимизировать приложение. Также возможно базе не хватает ресурсов, и приложение долго работает из-за нее, обычно в рамках одного бизнес-процесса делается несколько запросов в базу, и все запросы после SELECT FOR UPDATE увеличивают время ожидания для остальных процессов.

Если у вас есть процессы где сразу идет UPDATE по id без предварительного SELECT, они тоже будут ждать. Если у вас есть массовые UPDATE не по id, а по условию, то так делать не надо, тут как раз возможны взаимоблокировки, потому что строки в разных процессах могут блокироваться в разном порядке.

При изменениях данных из приложения только так и нужно делать. Либо SELECT FOR UPDATE, либо блокировки вручную через pg_try_advisory_lock. При правильном подходе ничего не виснет, поэтому эта конструкция и добавлена в базу данных.

SELECT FOR UPDATE из приложения нужен, чтобы другой веб-запрос не читал ту же строку, пока обработка в первом не завершена. Допустим, у вас есть кнопка для новых пользователей "Получить бонус", и код в приложении:

isBonusReceived = query('SELECT is_bonus_received FROM users WHERE id = :userId', ['userId' => userId]);
if (isBonusReceived === false) {
  addBonusForUser(userId);
  query('UPDATE users SET is_bonus_received = true WHERE id = :userId', ['userId' => userId]);
}

Если пользователь отправит из консоли браузера 2 запроса в одну миллисекунду, то оба потока прочитают значение false, и он получит бонус 2 раза. Похожая ситуация описана в примере из статьи.
А с FOR UPDATE запрос SELECT в одном из 2 потоков будет ждать, пока строка освободится, и проверка сработает правильно.

Взаимоблокировки могут быть только если делается несколько блокирований в рамках одной транзакции, причем в разных процессах в разном порядке. Поэтому лучше делать одну на главный объект. Если вы сделали в одном процессе select for update, другой процесс при select этой же строки будет просто ждать, пока она освободится, он не будет блокировать первый процесс.

Это ужасный способ, ни в коем случае нельзя так делать. Как заметили выше, могут быть ситуации, когда такую проверку сделать нельзя. Но тут дело даже не в этом, а в том, что проверка "add check (balance>=0)" не защитит от точно такого же двойного списания, когда баланс равен 2.

Единственный правильный способ при работе с ресурсом - блокировать ресурс от любых изменений в параллельном процессе, пока с ним выполняются операции в текущем процессе. В данном случае ресурс это строка таблицы, которая представляет какой-то бизнес-объект. Не должно быть никаких потерянных апдейтов типа "ну тут результат тот же самый, так что все нормально", потому что приложение в этом сценарии может потом делать инсерты в другую таблицы, и там будут дубликаты.
Можно использовать SELECT FOR UPDATE или блокировки вручную через get_lock/pg_try_advisory_lock (с таймаутом). Блокировки вручную удобнее, потому что для SELECT FOR UPDATE нужна транзакция на всё время обработки в приложении, и там могут происходить сетевые запросы. Лучше делать только одну блокировку на главный ресурс (агрегат в терминах DDD), то есть например на order, но не на отдельные order item.

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

Если на эту область памяти больше никто не ссылается, мы ее освобождаем. Это эквивалентно предварительной записи null во все ссылочные поля этого объекта. То есть у всех ссылочных полей уменьшится refCnt для их областей памяти, и дальше работает та же логика. Если refCnt уменьшился, но не 0, то с полями ничего не делаем, они же указывают на другие области памяти, для них количество ссылок не изменилось.

Я же привел пример цикла, там 2 объекта ссылаются друг на друга. Ссылки подсчитываются каждый раз при записи в любую ссылочную переменную. Удаление стековых переменных эквивалентно записи null в них.

Не совсем рекурсивно. Проходим по всем ссылочным полям, уменьшаем refCnt. Если стало 0, удаляем. При удалении проходим по всем ссылочным полям, делаем то же самое. Обработка последовательная, можно обойтись циклом и даже без стека, нужен просто массив для добавления всех ссылочных полей текущего объекта. Дважды по ссылкам на одну область памяти как раз нужно проходить, так как refCnt соответствует их количеству, поэтому отслеживать это не надо. Сравнивать 2 количества не нужно, только refCnt с 0.

Это как бы garbadge collector, только распределенный по времени, каждый раз по чуть-чуть. Мне кажется, так задержки будут небольшие и более предсказуемые. Можно конечно представить linked list на миллион элементов, у которого мы удаляем первый, и остальные удаляются по цепочке, тут задержка будет выше среднего, но их и с явным управлением памятью нужно удалять.

UPD: Хм, наверно все-таки в массиве будут поля не только текущего объекта, но это будет просто вектор, куда поля для обработки добавляются в конец.

происходит только после разрыва циклической ссылки вручную

Не после разрыва циклической ссылки, а после достижения нулевого количества ссылок на область памяти.
Можно не возвращать b из func, тогда для нее будет такая же обработка, как для a, и память освободится для обоих, без записи null в поле.

// refCntNewB--; get ref for b->field; refCntNewA-- (0, 0)

удаление локальной переменой var b не обнуляет счетчик ссылок, так как последняя ссылка все еще находится тут

Удаление локальной переменной, которая в func, не обнуляет счетчик ссылок, потому что b возвращается как результат. Ссылка, которая в b, записывается во временную переменную для возврата, счетчик refCntNewB увеличивается, локальная переменная b удаляется, refCntNewB уменьшается. Поэтому там указаны одновременно "++" и "--".

Я же привел пример с динамическими объектами. Ссылка на динамический объект хранится в локальной переменной, у которой есть область видимости.

Information

Rating
1,945-th
Location
Россия
Registered
Activity