Комментарии 58
Для избегания дедлоков ещё есть трюк с сабселектом примерно такого вида
UPDATE table_name
SET ...
WHERE id in (
SELECT id
FROM table_name
WHERE ...
ORDER BY id
FOR UPDATE
)
тогда блокировки будут браться именно в порядке сортировки
Увы даже такой вариант у меня теряет обновления при использовании UNNEST. Я все что можно перепробовал.
А во всех остальных случаях код с ORDER BY ... FOR UPDATE работает не быстрее триггеров. Но FOR UPDATE нельзя написать на EF, только SQL.
Именно в сочетании с unnest? У нас такое в проде несколько лет уже крутится, проблем не замечено.
На EF можно сделать FOR UPDATE https://habr.com/ru/companies/skbkontur/articles/852022/
Сделал тест варианта с явными блокировками https://habr.com/ru/articles/963120/
Попытка впихнуть весь поток изменений товаров в одну БД это конечно страшно. Если мы точно знаем, что у нас на складах в разы больше товаров чем дневной заказ все что описано выше ведёт к затратам ресурсов совершенно бессмысленным. Гораздо дешевле разгребать очередь в фоне мелкими воркерами
В том то и дело - мы точно не знаем. Можно усложнить в стиле озона - сделать батч и перед выполнением посчитать и сравнить кол-ва на складе и во всех заказах. И если точно хватает, то можно и без блокировок.
Попытка впихнуть весь поток изменений товаров в одну БД это конечно страшно.
А что страшного? Нагрузка на железо небольшая, упирается все в блокировки.
Если мы точно знаем, что у нас на складах в разы больше товаров чем дневной заказ все что описано выше ведёт к затратам ресурсов совершенно бессмысленным
Какие траты вы называете бессмысленными?
Обновление резервов? Его в любом случае надо делать и в любом случае оно будет тормозить
Проверка остатков? Это же простое сравнение, если его отключить, то разница незаметна
Это я в рамках теста знаю, что все запросы выполнятся успешно, а в жизни вы такого не можете гарантировать. Код корректно отработает в любом случае.
Гораздо дешевле разгребать очередь в фоне мелкими воркерами
Если если вдруг окажется, что резерв в очереди не может быть сделан - не осталось запасов на складе, тогда что? А вы уже деньги у клиента списали и он ждет что его заказ скоро приедет.
Непонятно что вы сэконосить пытаетесь, если вы сделаете очередь и все запросы последовательно будете обрабатывать, то у вас пропускная способность (от вызова метода клиента до бронирования товара на складе) упадет раза в 4 по сравнению с параллельным случаем. Вы сможете с меньшими ресурсами принимать заказы, но без обновления остатков какой в этом смысл?
Нагрузка на железо небольшая, упирается все в блокировки.
Нет, конечно. Вы загнали систему в пик производительности, и когда 28 декабря норот ринется делать пиковые закупы вечером и и утром все это заклинит, а разгребать будет вторая линия поддержки из пару человек. И не надо писать, что надо заранее железо увеличить, не поможет, надо будет шардировать, отчего вся опитимизация пойдет погулять
Какие траты вы называете бессмысленными
Абсолютно все. Зачем мне обновлять резерв прямо сразу, если у меня может быть страховой остаток и мне надо только не давать за некоторое время превысить его.
Вместо этого делаются жесткие вставки, причем в реляционную БД, и разумеется для ускорения все это будет дерномализовано потому что форейн ключи в шардах будут работать не так что бы быстро.
Проверки хз зачем, причем непонятно как это будет работать загнаннное в десяток подов, раскиданных по датацентрам.
не осталось запасов на складе, тогда что? А вы уже деньги у клиента списали
Значит заказ отменится. Цель - что бы такое было гораздо реже чем покупка. Вы видно в торговле работали примерно никогда, товар может не приехать по такому количеству причин, что "запасы на складе по базе" это такая мизерная часть, что возиться с кровью из глаз что бы вот точно-точно совпало с цыферкой на складе - это наивность. Потому что с утра жахнули при разгрузке два холодоса об асфальт, а списать еще не списали. Защитой является тот самый страховой остаток, поэтому все эти приседания - бессмыслены и даже вредны.
если вы сделаете очередь и все запросы последовательно будете обрабатывать
Ну что мешает разгребать ее параллельно, группируя по типам товаров (да у разных групп будет разная логика обработки остатков, вы же учли в своей логике мерные товары и продажу упаковками, да?). Да и последовательно для некоторых товарцев можно, часто два пика закупа бывает- утро и вечер, остальное время стоит, зачем мне железо купленное под пиковую производительность ? Купим пожиже и за ночь разгребем
Нет, конечно.
Это вы о чем?
Вы загнали систему в пик производительности, и когда 28 декабря норот ринется делать пиковые закупы вечером и и утром все это заклинит, а разгребать будет вторая линия поддержки из пару человек.
Что значит "загнали систему в пик производительности" ? В коде из статьи быстродействие НЕ упирается в железо если что. Железо выдерживает в 5,5 раз больше.
И не надо писать, что надо заранее железо увеличить, не поможет, надо будет шардировать, отчего вся опитимизация пойдет погулять
Чему именно помешает шардирование?
Зачем мне обновлять резерв прямо сразу, если у меня может быть страховой остаток и мне надо только не давать за некоторое время превысить его.
Что делать если вы превысили страховой остаток? Да и в целом сам маркетплейс остатком не управляет, продавец отправляет на склад по мере готовности и спрос может превысить запасы.
Значит заказ отменится. Цель - что бы такое было гораздо реже чем покупка.
Если вы заказываете что-то в магазине, а ваш заказ отменяется, то будете ли вы еще что-то в нем заказывать?
товар может не приехать по такому количеству причин, что "запасы на складе по базе" это такая мизерная часть, что возиться с кровью из глаз что бы вот точно-точно совпало с цыферкой на складе - это наивность
Если вы всю обработку делаете в фоне, то количество таких причин конечно увеличится.
Защитой является тот самый страховой остаток, поэтому все эти приседания - бессмыслены и даже вредны.
Проблема в том, что в пики распродаж от момента поступления заказа до момента накладывания резерва могут заказать количество товаров в 10 раз превышающее страховой остаток. Вы же заранее не знаете как долго у вас будет очередь обрабатываться при нагрузке и как часто будут приходить заказы.
И еще раз напомню, что в случае маркетплейса сам маркетплейс пополняет остатки на складе, это делает только продавец. Поэтому у вас может просто не быть страхового остатка.
Вы видно в торговле работали примерно никогда, товар может не приехать по такому количеству причин, что "запасы на складе по базе" это такая мизерная часть, что возиться с кровью из глаз что бы вот точно-точно совпало с цыферкой на складе - это наивность.
Но вот конкретно резервирование можно было бы и нормально сделать, без отговорок.
Много информации и предположений, хотелось бы уточнить некоторые моменты
Сервис из статьи как раз может стоять после очереди и принимать не заказы, а список ItemId и Qty. И до этого может быть некая пред-обработка (упаковок, китов и так далее). Это же просто облегченный пример, чтобы показать, что обычное железо с простым кодом может сделать 600 заказов в секунду.
Если 600 заказов в секунду на один склад и система справляется - то зачем шардирование? Но все же хорошо бы узнать - сколько там на самом деле у озона, возможно у них больше 600 и их усложненное решение оправдано
В любом случае бутылочным горлышком будет не программный код или БД, а люди, которые бегают по складу и собирают заказ с полок. Сколько надо людских и иных ресурсов и складских площадей, чтобы физически обработать 600 заказов в секунду (это порядка 50 млн за 24 часа) на одном складе?
Со страховым остатком разумеется дельная штука - можете чуть детальней рассказать, как вы его будете пересчитывать, чтобы он не пошел в разнос при параллельной обработке? Есть же базовый пример для общепринятого языка программирования - счетчик с interlock или volatile. Тут придется сделать примерно то же самое. И какой будет механизм отката, когда число выйдет за обозначенные рамки?
"Купим пожиже и за ночь разгребем" - в некоторых случаях может быть определен SLA, типа обработать 200 тыщ заказов за 2-3 часа, а не ночью
И еще момент - конкретно в случае с маркет-плейсом, когда резерв товара происходит до момента оплаты и откат резерва, если оплата не прошла. С точки зрения пользователя и клиентской части - это синхронна обработка. Если обрабатывать через очередь, то надо будет усложнять решение, чтобы дружить синхронного клиента и асинхронную очередь. Поэтому альтернатива с синхронным резервом без очередей - вполне себе альтернатива.
Если мы точно знаем, что у нас на складах в разы больше товаров чем дневной заказ...
...то вы явно не понимаете бизнес-логику маркетплейса.
Теперь интересно, что на это скажет озон. Они кстати не показали в статье результаты нагрузочный тестов.
Нашел неподтвержденную инфу в интернетах
Крупный логистический центр Ozon может обрабатывать более 900 тысяч заказов в день, а распределительный центр Ozon обрабатывает до 600 тысяч заказов в сутки
Это скорее всего физическая отгрузка. Но для программной системы - это порядка 10 заказов в секунду.
Это физические ограничения склада. Озоновская система может только у одного пользователя сформировать несколько сотен заказов за клик, а реальная пиковая производительность их АПК скорее всего на порядок-другой выше.
Точных данных по rps я вам не скажу, но направлю на подумать.
1) Грубо прикидывать rps исходя из кол-ва заказов в день - неправильно, т.к. нагрузка в течении дня неравномернавя
2) Если перечитать статью, то можно заметить, что с обычной нагрузкой проблем не было. Проблемы были при старте продаж хамеров (товаров по супер низкой цене). Это когда условно 10к товаров сметают за пару секунд. 1 товар в одни руки. При этом далеко не у всех резерв завершается успешно как у вас.
Теперь вопрос, что будет с вашей системой, когда в неё прилетит под 10к RPS?
1) Не масштабируется горизонтально
2) Привязано к БД
3) Костыли, а не шины
4) Да вообще есть Orleans, для 1000 батчей по 1000 итемов каждый (и 10к RPS с ACID)
Тут ни озон, ни автор не смогли даже в подобие Highload.
А партиционирование по складам тут не поможет?
В реальном мире все сложнее, клиент в Москве, а два склада в Москве и Новосибирске - насколько будет дороже и дольше доставка?
Поэтому хорошо бы сначала выбрать оптимальный склад.
Вы наверное не до конца поняли проблему.
Чтобы сделать резервацию надо :
Найти строку в бд
Дождаться снятия блокировки обновления
Повесить свою блокировку обновления
Записать новую строку
И повторить это от 1 до 10 раз (сколько строк в заказе). И только в самом конце снимаются блокировки.
То есть следующая транзакция скорее всего зависнет на строке 2 на относительно долгое время. И транзакция за ней тоже будет висеть.
В тесте, когда выпадали дедлоки, я видел циклы ожиданий, которые состояли из 9 транзакций.
Блокировка происходит не на диапазонах, не на очереди записи на диск, а просто на ожидании снятия блокировки ОДНОЙ строки в один момент времени. Ни партицирование, ни шардирование тут не поможет.
Такова цена ACID гарантий при высокой конкурентности. И это самый лучший вариант на самом деле, все альтернативы еще более медленные, как в примере с однопоточной очередью.
В случае блокировок на 1 строке тут действительно вряд ли можно что-то придумать. Но такое партиционирование немного снизило бы накладные расходы на поиск и управление блокировками.
Встречался на практике с такой проблемой. Обычно помогает разбиение не более короткие транзакции. Ну например если типичный документ содержит 50 строк, то зачем делать резерв в одной транзакции? Можно по транзакции на строку.
А если на одной из строк споткнется, как откатить?
Не понял в чем затруднение.
Движок бд откатит транзакцию так же как и в случае одной транзакции на кучу строк.
Или вас беспокоит в каком статусе получится документ в целом, если в нем по части строк резерв прошел успешно а в части нет?
Так есть же try catch. Ситуация ловится и обрабатывается снова.
Кстати, встречал на практике документы и на 2 - 3 тысячи строк. Когда из магазинов заливали сгруппированные по магазинам данные реализации за весь день.
это имеет смысл если ошибка резервирования временная, тогда модно повторять операции, пока они не выполнятся. Но ошибка резерва не временная, если доступно меньше чем вы хотите зарезервировать, то повторять нет смысла, надо ВЕСЬ заказ отменить.
Это еще вы почему так решили?
Условия могуть быть разными и их определяет бизнес. Я 12 лет проработал в компании дистрибьюторе. Там сеть магазинов, например 7й континет делала регулярно кучу заказов по заказу на магазин. В каждом заказе десятки товаров, обычно от 40 до 100. Заказ можно было отгружать если процент зарезервированных строк был не меньше какого то числа. То ли 60% то ли 70. За давностью лет уже не помню. К тому же заказ делался за несколько дней до отгрузки так что в течение дня попытка резервирования могла повторяться несколько раз (чтобы повысить % резервирования в случае если где то отказались от резерва или новый товар поступил).
Если требования бизнеса такие как вы предполагаете, то нет никакой проблемы снять резерв (можно в блоке catch принять это решение).
Но опять же все зависит от поставленной задачи.
Так есть же try catch. Ситуация ловится и обрабатывается снова.
Обычно хочется транзакционность, чтобы состояние сущности в БД всегда было согласованным. Чтобы выключение питания (условно) сервера не оставляло в БД непонятное нечто.
Что значит согласованная сущность?
Почему вы решили что это достигается заключением обработки всех строк в одну транзакцию?
Все завсит от поставленной задачи.
Можно условиться так.
В шапке документа ставится пометка что начат резерв (например статус Начато резервирование)
Затем идет резервирование строк, в каждой транзакции обрабатывается Х строк.
В конце переводим статус в шапке "Зарезервировано"
если операция прервана, сервер бд упал, электричество вырубилось итп. В следующий раз мы увидим документ находящийся с большой вероятностью в промежуточном статусе "Начато резервирование". В такой ситуации можно продолжить процесс с того же места. Или если потребуется, запустить откат статуса - снятие резерва.
Зависит от обстоятельств все.
Я недавно, как раз подобную задачу решал.
Идет продажа. При пробитии чека обновляется аналитика "дата пролажи" для товара. И тут выяснилось что все виснет.
Оказывается спецы по олап системе решили раз в месяц закачивать измененные аналитики. Делали это через специальный документ - шапка и строки. В строках аналитика и товар. При утверждении документа, аналитики проливабтся из строк в свойства товаров.
Ну и олапщики закачали 110 тыщ строк в документ и запустили утверждение. На кассе все повисло.
Что тут быстро сделать?
Порезал процесс утверждения на множество транзакций по 50 строк на транзакцию. Все проблемы ушли.
Если утверждение прервалось, можно продолжить с того же места.
Проблема решена.
А в какой момент вы будете давать клиенту ответ?
Если клиент ждет пока статус документа не станет равен "зарезервировано", то вы просто изобрели свой WAL и поверх БД, у которой уже есть WAL. Ваш подход будет на порядок хуже использования транзакций.
Если клиент не ждет когда документ получит статус "зарезервировано", то я не понимаю как это должно работать. Предположим что у вас приложение, которое билеты в кино продает. Вы покупаете два билета, приложение вам говорит "Начато резервирование", так? А деньги когда списывать будет?
Да причем здесь WAL?
Это же другой уровень. Все в приложении делается. Промежуточный статус млжно клиенту не показывать.
Ну вот рассмотрим ваш пример.
Клиент делает запрос - зарезервировать Х билетов.
В вашем примере
код открывает транзакцию.
Начинает цикл по числу билетов.
На каждом шаге цикла выполняет резервирование.
В конце после цикла закрывает транзакцию.
Возвращает ответ клиенту об успехе (дальше пойдет оплатп)
А как я предлагаю
Начинает цикл по числу билетов
На каждом шаге цикла делаем
А. Открываем транзакцию
Б. Делаем резерв одного билета.
В. Закрываем транзакцию
После завершения цикла возвращаем ответ об успехе (дальше идет оплата)
По факту, 2 строки кода поменять и все.
Можно чуть усложнить, в каждой транзакции несколько билетов резервировать на гаге Б. - тогда будет меньше накладных расходов.
Да причем здесь WAL?
При том что вы пытаетесь его реализовать
Это же другой уровень. Все в приложении делается. Промежуточный статус млжно клиенту не показывать.
Если вы не показываете клиенту промежуточный статус, то это тот же самый уровень. По сути вы хотите дать приложению такие же гарантии как от транзакций, но без транзакций. Увы самодельная атомарность и консистентность это очень сложно. А независимость самопальных транзакций почти недостижима при нескольких экземплярах вашего приложения.
Отсутствие атомарности:
Предположим вы реализовали такой цикл с транзакциями внутри цикла. Внезапно на одной из итераций происходит падение БД (внезапная перезагрузка, oom-kill итд). Что будет делать ваш код? Клиент получит свою ошибку, но вам надо как-то откатить уже закомиченные транзакции. А если приложение падает в середине цикла, клиент получает отказ, а кто откатывать будет?
Отсутствие согласованности:
Кроме того можно рассмотреть сценарий параллельных резервирований:
Клиент А зарезервировал товар 1 (транзакция закоммичена)
Клиент Б зарезервировал товар 2 (транзакция закоммичена)
Клиент А пытается резервировать товар 2, но запасы кончились, резрв товара 1 нужно откатить
Отсутствие независимости:
Кроме того ваше приложение скорее всего постоянно отдает остатки, и вы уменьшаете остаток, снижая его на время незавершенных резервов.
"гораздо менее надёжный, чем использование транзакций."?
Вы серьезно?
Да вы видимо никогда не сталкивались с кучей блокировок из за больших транзакций. Вам что важно, надежность или мнимая "простота"? И зачем она будет если повиснет под нагрузкой.
Насчет резервов - можно предположить экзотический сценарий когда из за падения обработки останется зависший резерв по части строк. Так он по куче причин может быть. Например в вашем случае, резерв полностью прошел но оплата не прошла. Или электрисество вырубилось. Та же ситуация только больше зарезервированных строк.
Для разруливания таких ситуаций делают фоновые обработки, которые снимаю старые ненужные рещервы или такие вот аварийные заказы. Обычная ситуация. И вы без такой обработки не обойдетесь даже с вашим вариантом длинных транзакций.
Вы серьезно?
Да, у меня статья о том как эта проблема решается, если вы не заметили
Да вы видимо никогда не сталкивались с кучей блокировок из за больших транзакций. Вам что важно, надежность или мнимая "простота"? И зачем она будет если повиснет под нагрузкой.
В этой статье пример кода как раз сталкивается с большим количеством блокировок из-за больших транзакций.
Вам что важно, надежность или мнимая "простота"?
Конечно надежность, которая заключается как раз в соблюдении ACID на уровне запросов пользователей\клиентов сервиса.
Насчет резервов - можно предположить экзотический сценарий когда из за падения обработки останется зависший резерв по части строк. Так он по куче причин может быть.
Если используете транзакции, то не может быть.
Например в вашем случае, резерв полностью прошел но оплата не прошла.
Транзакция будет отменена, резерв не пройдет.
Или электрисество вырубилось.
Закоммиченная транзакция никуда не денется, незакоммиченная будет отмена. Это гарантия долговечности - D в ACID
Для разруливания таких ситуаций делают фоновые обработки, которые снимаю старые ненужные рещервы или такие вот аварийные заказы.
Это я и имею ввиду, когда говорю что "самопальные транзакции" гораздо более затратны. Кроме того у вас нет никакой защиты от Durty Reads.
И вы без такой обработки не обойдетесь даже с вашим вариантом длинных транзакций.
Пример в статье показывает как обойтись без такой обработки.
Вы вообще статью прочитали? Код смотрели?
Я вам пишу как можно улучшить и сделать надежнее несильно усложняя код.
Я этим больше 20 лет занимаюсь. Практикой все проверено.
А вы какими догмами рассуждаете.
Ярлыки навешиваете.
Ну, делайте как вам нравится. Никто не заставляет.
Я вам пишу как можно улучшить и сделать надежнее несильно усложняя код.
Что именно улучшить? Скорость работы? Надежность? Время написания?
Можете пример кода привести, который демонстрирует эти улучшения?
Я пока увидел, что для реализации цикла с транзакциями внутри, а не снаружи нужен фоновый процесс, который будет отменять незавершенные брони. Тогда как в моем примере из статьи такой процесс не нужен.
Я этим больше 20 лет занимаюсь. Практикой все проверено.
Это вообще ни о чем не говорит.
А вы какими догмами рассуждаете. Ярлыки навешиваете.
Если для вас ACID и гарантии СУБД это "догмы" и "ярлыки", тогда вы правы. Я вообще не считаю себя умнее разработчиков Postgres\SqlServer\MySQL и даже SQLite, чтобы думать что могу сделать ACID лучше, чем они за любое разумное время.
Ну, делайте как вам нравится. Никто не заставляет.
Code talks. Просто покажите пример.
>> Что именно улучшить? Скорость работы? Надежность? Время написания?
Снизить вероятность блокировок - я об этом явно написал.
Масштабируемость будет лучше. Если вдруг изменятся условия и в заявке для резервирования число строк увеличится например в 100 - 1000 раз, то работать будет норм. И примерно с той же скоростью на 1 строку (меньше будет ожидание разными сессиями друг друга). Конечно код будет сложнее, как вы справедливо указали. Но ничего бесплатно не бывает.
>> Можете пример кода привести, который демонстрирует эти улучшения?
Собственно, я описал идею и пошагово все привел. что там непонятного ? Или это придирка такая ? Ну кто хочет понять тот ищет возможности, кто не хочет - ищет причины.
>> Я пока увидел, что для реализации цикла с транзакциями внутри, а не снаружи нужен фоновый процесс, который будет отменять незавершенные брони. Тогда как в моем примере из статьи такой процесс не нужен.
Он потребуется для обработки аварийных ситуаций как минимум. Но и в вашем примере он нужен, о чем я указал.
Могу подробнее пояснить.
Вот приходит в вашем примере заявка - делается резерв, коммитится транзакция, а тут все падает (например электричество вырубилось или виртуальная машина грохнулась). Транзакция закоммичена. А оплата не прошла. Как в вашем примере решалась бы эта проблема ? Все равно нужно стороннее вмешательство для снятия резерва или восстановления сессии в которой можно было бы провести оплату.
>> Если для вас ACID и гарантии СУБД это "догмы" и "ярлыки", тогда вы правы.
Вы просто подменяете понятия, приписываете мне то что я не говорил, а потом "опровергаете". Гаденький, но детский приемчик. И совсем вас не украшает. На него никто уже не ведется.
Касательно транзакций и ACID - а кто вам сказал что вся обработка должна идти в одной транзакции ? Это совсем не обязательно. Все определяется задачей. Тем на какие по смыслу шаги можно разделить обработку (а в некоторых случаях и распараллелить, но это уже другая тема). В зависимости от условий, допустимо последовательность действий разбивать на несколько последовательных транзакций. И это не нарушение ACID. Нужно просто понимать что делаешь и соотносить с практикой.
>> Что именно улучшить? Скорость работы? Надежность? Время написания?
Добавлю еще к предыдущим.
Возможно, будет быстрее работать. Но тут может по-разному быть. Транзакций будет больше, поэтому может быть медленнее из-за более частых коммитов. Это фактор замедления.
Но!
Если строк в заявке будет много, то в случае одной транзакции (как у вас) растет время ожидания так как пока 1-я сессия не завершит резервирование всей заявки с Х строками, другая сессия которая, возможно, ее ждет, не начнет работу, ждет пока блокировки снимутся. В случае коротких транзакций они меньше будут друг другу мешать, меньше ожидание. Быстрее работа.
Какой из факторов сильнее скажется заранее сложно спрогнозировать. Если строк много и большая конкуренция за остатки то вариант с 1 транзакцией на строку будет быстрее. А если нет, то может быть обратный эффект.
Сделал тест такого варианта https://habr.com/ru/articles/963120/
Очень сильно проигрывает транзакциям в БД, даже без реализации отката при потере связи.
Рукопашные транзакции могут оказаться быстрее если время обработки значительное, но мы тогда приходим к архитектуре с очередями в базе и асинхронной обработке.
Сохранить синхронность и сделать рукопашные транзакции, чтобы уменьшить ожидания на блокировках, в большинстве случаев приведет к замедлению. А если нужно просто уменьшить степень конкурентности, то проще блокировку сделать advisory lock, взять блокировку таблицы или очередь как в этом посте.
Вместо самодельных очередей, надо вешать rate limiter на входе. Он поставит http-запросы в очередь, и пустит в параллель сколько скажешь.
Тогда, если разные куски когда будут бороться за коннекты, не получится что у тебя куча запросов ждет освободившегося коннекта чтобы завершится, а новые все прилетают - и тоже получают шанс сожрать коннект.
Rate Limiter же обычно 429 возвращает, а не ждет выполнения. Непонятно насколько это лучше, чем клиент просто подождет
А походу особо и не заморачиваются, как-то был случай с покупкой видеокарты на DNS - все просто подвисло, а потом когда отвисло, они кончились.
Там есть очередь же. Настраиваешь чтобы у тебя запросы не запускались в обработку больше, скажем, 10 одновременно. Остальные ждут в очереди сколько скажешь. И дальше крутишь ручки на перф-тесте. Обычно надо чтобы уперлось в CPU базы. Если под недонагружен по cpu, а база еще дышит - можно побольше в параллель навалить.
База, от того что ей в параллель надо кучу запросов вертеть, быстрее не работает - у нее тоже не бесконечное количество процов. Ей быстрее последовательно все делать плюс-минус по очереди.
Ну и исключается гонка за коннектами, сотни запросов ждущих блокировки и коннекты к БД. Все что система не переварит - видно по растущей очереди.
В оригинальной статье рассматривалась проблема высокой конкурентности за одну строку (один и тот же товар — условную сковородку, которую хотят купить все) и RPS описан как «тысячи» (но точного числа нет), а у вас вышло 700 RPS на разные товары.
Не умаляю ваши заслуги и приведенный код, но кажется, что ответ выше неполный :)
Во-первых в статье написать можно любую цифру
Во-вторых у меня код выдает 600 заказов в секунду и 300+ обновлений остатков в секунду на каждый товар. Я могу сделать по 100 товаров и 100 складов, распределив равномерно запросы между нему, тогда количество запросов в секунду будет около 2000, а количество броней на один товар в секунду - меньше 100.
В-третьих у меня код работает на одной машине, в docker в wsl вместе со средой разработки и тестирования. Мы же не знаем какое железо было в оригинальной статье.
ALTER TABLE stock ADD CONSTRAINT check_reserved CHECK (reserved <= quantity);
То есть перенесем один из инвариантов в СУБД
Убирает необходимость ручной проверки через AnyAsync(...)
Ещё можно
var stockRows = await ctx.Stock
.FromSqlRaw(@"
SELECT * FROM stock
WHERE (item_id, warehouse_id) IN ({0})
ORDER BY item_id, warehouse_id
FOR UPDATE",
string.Join(",", keys.Select(k => $"({k.ItemId},{k.WarehouseId})")))
.ToListAsync(ct);Полностью устраняет дедлоки, но это уже вышел за пределы ORM
А вы статью прочитали?
Вот тут поясню (у меня такая же задача, только у меня тут не озон конечно).
Конфигурация - MySql 8.0 / i7-8700k / 32Gb, на одном складе, 10 items, 100 потоков, 1000 requests
Вариант, когда я беру из БД с SELECT FOR UPDATE, обновляю в C# коде и потом сохраняю в БД - в логах консольки деадлоки все равно есть, retry в EF Core срабатывает, цифры в конце сходятся, скорость примерно 20 rps
Вариант с хранимкой с SELECT FOR UPDATE, в логах консольки деадлоки есть, retry в EF Core срабатывает, цифры в конце сходятся, скорость примерно 60-90 rps
Вариант с хранимкой БЕЗ SELECT FOR UPDATE + без проверки остатка в конце, в логах консольки пусто, цифры в конце сходятся, скорость 400-440 rps
Вариант с хранимкой БЕЗ SELECT FOR UPDATE + с проверкой остатка в конце, в логах консольки пусто, цифры в конце сходятся, скорость 300 rps
И что еще заметил - последние два варианта отрабатывают нормально даже на уровне ReadUncommitted. Меня это смущает, пока не понимаю почему.
Позже пожалуй поставлю postgresql и проверю тоже самое на нем, интересно узнать разницу между дефолтным mysql и дефолтным postgresql
Статья интересная, спасибо! Очень хорошо проиллюстрировала ситуацию с бигтехом, продуктами которого пользуются миллионы людей, далёких от понимания того, что там прячется за кнопкой "Заказать". Иллюстрирует суть профессии разработчика таких решений, кроющуюся в том, какие нужно пробивать стены и без того больной бизнесом головой, чтобы для конечного пользователя это всё было доступно за 2-3 клика по тачскрину в любое время суток, и чтобы он как избалованный ребёнок не побежал в другой маркетплейс, надеясь, что там база консистентная и пропускная способность выше. Бизнес голодный до денег абсолютно всегда, но ни бизнесу, ни конечному пользователю не донесёшь простую истину о том, что есть потолок. Вот и стреляем себе в ноги... Спасибо ещё раз за статью:)

Система резервации на 600 заказов в секунду без буферизации и другой дичи