Язык программирования Rust постепенно набирает популярность не только среди разработчиков, но и в среде менеджмента. Такая растущая популярность не появляется на пустом месте, особенно в случае технологий с высоким порогом входа. В данной статье я хочу проанализировать чем Rust заслужил такую популярность на примере компании, в которой я работаю и в которой Rust является основным языком разработки, а также одного из основных наших продуктов - биржи.

Как и при выборе любого инструмента разработки, выбор языка исходит из требований предъявляемых к разрабатываемому продукту. Какие требования предъявляются к бирже?

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

В дополнение к тестам хорошо себя показывают инструменты статического анализа. Они позволяют выявлять проблемы еще на этапе разработки. Хорошо, если такие инструменты встроены в компилятор языка. Идеальный язык в плане надежности стремится к парадигме “компилируется значит работает”, и хотя идеальных языков нет, есть те, что сильно выделяются из большинства языков, построенных на противоположной парадигме “работает значит компилируется”.

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

И хотя выбор языка является не единственным фактором влияющим на производительность, на таких высоких требованиях к ней имеет значение каждый из факторов. Языки, в рантайме которых происходит интерпретация, JIT-компиляция и/или сборка мусора, имеют менее предсказуемую производительность, скорее всего худшую чем языки имеющие более минималистичный рантайм. Следовательно для более высокой производительности следует выбрать язык с ручным управлением памятью и с AOT-компиляцией в машинный код.

В-третьих, важна скорость разработки. Данное требование важно практически для любого современного продукта, не только для биржи. Чем быстрее разработка сможет поставлять новые фичи, тем более конкурентным окажется продукт среди аналогов. На скорость разработки влияют множество факторов, которые отличаются у разных языков: наличие у языка высокоуровневых абстракций, развитая экосистема библиотек с готовыми решениями для типовых проблем, развитые инструменты разработки и отладки и т.д. Неплохо, если язык позволяет удобно работать с многопоточными и асинхронными вычислениями. Если мы боремся за производительность, то крайне желательно чтобы абстракции стоили как можно меньше, желательно чтоб они были zero-cost. А если мы хотим управлять памятью вручную, то хорошо бы иметь умные указатели и коллекции, которые будут управлять выделением и освобождением памяти за нас.

Сложите все требования воедино и выбор языка программирования сужается до очень короткого списка. С одним требованием производительности уже можно выбирать между C++ и Rust, ну может еще парочки языков, которые быстро будут отброшены или при анализе скорости разработки на них или при попытке кого-либо нанять. Добавим сюда надежность и Rust сильно опередит C++ одним только фактом, что в нем гарантированно нет undefined behaviour пока вы не напишите ключевое слово unsafe.

Многие IT бизнесы боятся внедрять у себя Rust, предпочитая делать свои сервисы на Node.js, Python или Go.

Кто-то боится, что не сможет никого нанять, а обучить имеющихся разработчиков будет крайне сложно из–за кривой входа. Но разработчики на Rust есть, многие знают несколько других языков и многие из них предпочтут Rust если дать им выбор.

Кто-то боится, что это всего лишь новомодная игрушка. Вот только Rust развивается примерно столько же как Node.js или Go, он готов к продакшену и вероятно что-то из того, чем вы пользуетесь ежедневно написано на Rust.

Кто-то смотрит на Rust как на язык исключительно для системной разработки. Хотя в его экосистеме есть библиотеки и практически для любой базы данных, почти любого брокера сообщений и почти любого формата сериализации (притом многие работают через единый интерфейс библиотеки serde). А писать rest api с помощью axum, rocket или actix-web ничуть не сложнее чем на django, flask, express или koa.

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

Комментарии (44)


  1. tumbler
    19.10.2023 08:27
    +3

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

    А точно причина не фармацефтического свойства? :)

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


    1. simenoff
      19.10.2023 08:27

      Для синтаксиса есть IDE)


    1. Lewigh
      19.10.2023 08:27
      +22

      Каким там синтаксисом он больно сильно отталкивает? У Rust довольно типичный, современный синтаксис аля Kotlin, Swift, TypeScript и т.д. Местами аля Python и С#. Там почти вся экзотика упирается в :: и синтаксис замыканий |n| n+1. К этим вещам привыкаешь за неделю, и понимаешь что это даже удобно и создатели языка не дураки были когда так делали.

      Могу только посочувствовать тем разработчикам, если они будут изучать Python, Ruby, Haskell, Lisp и т.д.


  1. Rezzonans
    19.10.2023 08:27

    Потому что у вас традиционная ориентация, которая помешала вам выбрать swift?


    1. AnthonyMikh
      19.10.2023 08:27

      Счётчик ссылок на каждый нетривиальный тип - это не быстро


  1. SpiderEkb
    19.10.2023 08:27

    Если у вас

    Во-первых, важна надежность.

    Во-вторых, важна производительность.

    то

    Кто-то боится, что не сможет никого нанять

    вообще не аргумент.

    Честно скажу - с биржевой темой не знаком сильно, специфику не знаю. Могу сказать за банк.

    В целом, требования похожие - надежность + производительность (терабайты данных, сотни миллионов операций в сутки).

    С учетом специфики (огромное количество бизнес-процессов, каждый из которых работает с потоком данных по, в целом, общей схеме - выборка по набору условий + обработка результатов выборки), как это ни парадоксально, но "объектность" тут не заходит. По одной простой причине - для каждого процесса придется писать свой набор базовых классов и при том, время жизни объекта крайне мало - от момента попадания его в выборку до его обработки. Плюс (или минус) велика вероятность изменений логики процесса (изменение законодательства, требований регулятора и т.п.).

    Т.е. работа идет не с объектами, но с потоком данных. Немного иная модель.

    По нашему опыту залогом производительности тут является два фактора:

    1. Минимизация динамической работы с памятью, максимальное использование статики, максимум всевозможных инициализаций выносится в compile time, в рантайме остается минимум.

    2. Выбор такого языка, который нативно поддерживает все типы данных, присутствующих в БД - char, varchar, decimal, numeric, date, time, timestamp с тем, чтобы можно было описать структуру данных, прочитать в нее записи из БД и потом просто работать с ее полями напрямую, не оборачивая участки байтового буфера в какие-то "объекты" с которыми потом можно осмысленно работать. Т.е. тут тоже максимум выносится в compile time (описание статических структур).

    В нашем случае все это решалось изначально выбором очень специфической специализированной платформы на которой существует специфический язык, не являющийся "языком общего назначения", но ориентированный на быструю и эффективную работу с БД (как прямым доступом к записям, так и возможностью встраивать SQL запросы непосредственно в код, в том числе и "статический SQL" где план запроса формируется не в рантайме, но на этапе компиляции) и реализацию коммерческой бизнес-логики. Естественно, для работы с датой, временем, типами с фиксированной точкой тут есть свои типы данных (никаких дополнительных "объектов" не надо) и реализована вся арифметика для них (типа арифметики с датами/временем, различные операции с округлением для типов с фиксированной точкой и т.п.).

    Все это повышает и скорость разработки, снижает количество разных зависимостей, так и повышает эффективность работы готового кода.

    И да. "Готовых разработчиков" нет практически. Берем толковых людей и учим сами.


    1. Sabirman
      19.10.2023 08:27

      Это вы о каком языке говорите ? Накинуть указатели на готовую структуру, вроде, только в Си возможно.


      1. SpiderEkb
        19.10.2023 08:27
        +1

        Я говорю о специфической платформе IBM i (AS/400) - middleware коммерческие серверы от IBM и основном (более 80% кода тут на нем пишется) специализированном для коммерческих расчетов языке RPG.

        Ничего там "накидывать" не надо. Писал об этом тут (прямая работа с БД) и тут (работа с БД через встроенный в код SQL). Просто объявляем структуру с нужными полями и передаем ее в качестве буфера-приемника в функцию чтения записи из таблицы (или в качестве хост-переменной в SQL запрос).

        Результат читается в структуру и дальше с ней (с ее полями) сразу можно работать - никаких объектов, конструкторов, ничего не надо. Структура создается на этапе компиляции, чтение в нее идет как в байтовый буфер.

        Суть в том, что в языке есть типы данных с фиксированной точкой packed и zoned (100% аналоги decimal и numeric в SQL), есть varchar, date, time, timestamp (100% аналоги соотв. SQL типов). Т.е. не надо никаких дополнительных "объектов", никаких преобразований. Просто прочитали в буфер и работаем.

        Впрочем, "накинуть указатели на готовую структуру" (или переменную) тут тоже можно. Для этого при объявлении структуры (dcl-ds) или переменной (dcl-s) используется модификатор based:

        dcl-ds dsMyDS likeds(t_dsMyDS) based(pMyDS);
        dcl-s  zVar   zoned(7: 0)      based(pVar);

        Теперь достаточно присвоить указателям pMyDS или pVar адрес какого-то байтового буфера и с ним можно будет работать как с переменной соотв. типа dsMyDS или zVar.

        Вообще, структуры тут идеологически несколько отличаются от сишных. Тут это просто байтовый буфер с размеченными внутри него полями. Вы можете явно задать размер структуры (больший, нежели сумма размеров его полей), можете задавать положение поля внутри буфера (причем, поля могут перекрываться), можете объявить шаблон (template) структуры с указанием дефолтных значений каждого поля, а при объявлении переменной по этому шаблону сказать чтобы оно сразу приинициализировалось "как в шаблоне" - это будет сделано на этапе компиляции:

        // Шаблон структуры
        dcl-ds t_dsISODate qualified template;
          year    char(4)  inz('1900');
          *n      char(1)  inz('-');       // неименованое поле - разделитель
          month   char(2)  inz('01');
          *n      char(1)  inz('-');       // неименованое поле - разделитель
          day     char(2)  inz('01');
          ISODate char(10) pos(1);         // перекрывает все поля
        end-ds;
        
        dcl-ds t_dsEURDate qualified template;
          day     char(2)  inz('01');
          *n      char(1)  inz('.');       // неименованое поле - разделитель
          month   char(2)  inz('01');
          *n      char(1)  inz('.');       // неименованое поле - разделитель
          year    char(4)  inz('1900');
          EURDate char(10) pos(1);         // перекрывает все поля
        end-ds;
        
        // Переменная по шаблону с инициализацией "как в шаблоне"
        dcl-ds dsISODate likeds(t_dsISODate) inz(*likeds);
        dcl-ds dsEURDate likeds(t_dsEURDate) inz(*likeds);
        dsl-s  strDate   char(10)            based(pStr);
        
        // dsISODate.ISODate сразу содержит строку '1900-01-01'
        // dsEURDate.EURDate сразу содержит строку '01.01.1900'
        
        dsISODate.year  = '2023';
        dsISODate.month = '10';
        dsISODate.day   = '20';
        
        // dsISODate.ISODate теперь содержит строку '2023-10-20'
        
        // Присвоение значений полей одной структуры полям с такими же именами другой структуры
        eval-corr dsEURDate = dsISODate;
        
        // dsEURDate.EURDate теперь содержит строку '20.10.2023'
        
        pStr = %addr(dsISODate);
        
        // strDate теперь '2023-10-20'
        
        pStr = %addr(dsEURDate);
        
        // strDate теперь '20.10.2023'

        Ну вот как-то так...


  1. domix32
    19.10.2023 08:27
    +2

    что в нем гарантированно нет undefined behaviour

    Это не совсем верно - количество известных UB сокращено, однако существуют конструкции, которые пока всё ещё не определены - там проблемы из теории типов всплывают. Ну и честной спеки раста пока не существует, так что-то пока всё поведение - implementation defined.


    1. bingo347 Автор
      19.10.2023 08:27

      А можно пример UB в Rust без использования unsafe?


      1. domix32
        19.10.2023 08:27

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

        Можно посмотреть TWIR на предмет статей про сложные вещи уровня GAT (пример). Помню там было несколько статей про проблемы с асинхронностью и вроде ещё чем-то и какие UB они на текущий момент позволяют делать и какие варианты решения имеются. То есть сейчас оно в состоянии Pre-RFC, фактически и явным образом не определено (undefined). Конкретные выпуски не назову, но вроде в последние недель 8 (не считая текущую) были обсуждения по теме.


      1. funny_falcon
        19.10.2023 08:27
        -1

        Целочисленное переполнение в release сборке - объявлено документацией как UB.

        Или вы в проде debug сборку гоняете?


        1. domix32
          19.10.2023 08:27

          Компилятор про это знает и как минимум предлагает варианты поведения на выбор. Так что это скорее unspecified behavior, т.к. пользователь сам выбирает что с этим делать.


          1. funny_falcon
            19.10.2023 08:27

            Простите, но мне подход Swift и Crystal нравится больше: привычные операторы ВСЕГДА возвращают ошибки/кидают исключения на переполнение, и в релизной сборке тоже. А если алгоритму нужно переполнение, или хочется скорости, то пожалуйста, вот тебе специальные операторы.

            А «мы ассертим в debug, но в release вы сами по себе» - так себе подход.

            Кстати, а в чём разница между Undefined Behavior и Unspecified Behavior? Что-то моя голова не может уловить семантической разницы.


            1. domix32
              19.10.2023 08:27
              +1

              Undefined - когда нет явного объяснения что делать если сделали какую-то грязь, например разыменование nullptr или доступ к невыравненной памяти. Unspecified - когда есть варианты как некоторая грязь может выполнена, но явным образом не указано какое поведение правильное/дефолтное и каждый из имплементаторов волен выбирать своё поведение по-умолчанию. В частности как раз переполнение целых, штуки типа `i++ + ++i` (сравните python, c++ и c#) и т.п.


              1. funny_falcon
                19.10.2023 08:27

                Variably defined? Бог с вами.


                1. domix32
                  19.10.2023 08:27

                  Implementation defined же.


            1. qwerty19106
              19.10.2023 08:27
              +3

              Добавьте в Cargo.toml, и будет поведение как в debug. Аналогично в debug можно выключить эти проверки.

              [profile.release]

              overflow-checks = true


              1. funny_falcon
                19.10.2023 08:27
                +2

                Спасибо. Вы единственный, кто дал конструктивный ответ.


        1. bingo347 Автор
          19.10.2023 08:27

          Переполнение не является UB в rust.

          В debug паникует, в release переполняет, все конкретно определено.

          Ну и есть методы, с еще более конкретным поведением: checked_*, overflowing_*, wrapping_*


          1. funny_falcon
            19.10.2023 08:27
            -1

            Как хотите. Я не согласен. Я столько наелся assert, которые в проде ни чего не делали, что теперь от такого зубы сводит.


            1. bingo347 Автор
              19.10.2023 08:27
              +4

              assert одинаково работает и в релизе и в дебаге, а если использовать debug_assert - на то он и дебаг


              1. funny_falcon
                19.10.2023 08:27

                Я имел в виду проверку переполнения. По факту это - debug_assert. А таких ассертов я в C наелся.


      1. Kelbon
        19.10.2023 08:27

        Любой вызов функции, которая где-то там внутри имеет unsafe


  1. Alesh
    19.10.2023 08:27
    +1

    Высокая производительность сейчас, это способность ПО хорошо масштабироваться, а не скорость выполнения его кода. Биржа,это больше про операции ввода/вывода, чем про математические вычисления. Следовательно нужна масштабируемая асинхронщина.

    А сколько людей одинаково хорошо знают Rust и принципы построения масштабируемых асинхронных приложений? И могут на Rust, это запилить?

    А сколько таких людей, но с Python, Java, Node?

    Если ответить на эти вопросы, то станет понятно, почему "АйТи бизнеса боятся". От не боятся, они просто хорошо считают свои деньги и время. ;)


    1. SpiderEkb
      19.10.2023 08:27
      +2

      Высокая производительность сейчас, это способность ПО хорошо масштабироваться, а не скорость выполнения его кода.

      Точно? А если вам задается некоторый таймаут, в который вы должны уложить выполнение некоторой операции - это про масштабируемость или про скорость кода?

      Как пример - нужно сделать некий сервис-модуль, который по заданным на вход параметрам перелопачивает достаточно большой объем данных и выдает на выход некую выборку в виде резалтсета. Вы задачу успешно (как вам кажется) решаете и отдаете на нагрузочное тестирование. Вам ее возвращают не доработку - не копии промсреды ваш сервисмодуль отрабатывает за 3 секунды, что категорически не вписывается в требования - предельное время работы не должно превышать 300мсек.

      Где тут про "масштабирование"?

      От не боятся, они просто хорошо считают свои деньги и время. ;)

      Они считают те деньги, которые им платит бизнес за решения его, бизнеса, задач. И если ему надо чтобы ваш модуль выдавал результат за 300мс, то вы должны сделать так, чтобы он выдавал результат за 300мс. Как и на чем - это ваши проблемы (бизнесу все равно что там у вас внутри, ему важен функционал). Не умеете - быстро будет нечего считать.


      1. xSVPx
        19.10.2023 08:27
        -1

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


        1. SpiderEkb
          19.10.2023 08:27

          А если нет? Тогда что? "Извини, не смогла?"


        1. hrls
          19.10.2023 08:27

          Еще бы неплохо линеаризуемость соблюсти между этими подзадачами. Ну и в SLA бывает указан 99-процентиль по ответу в 60 микросекунд, например. 


      1. Alesh
        19.10.2023 08:27
        -1

        Вы собираетесь перелопачивать достаточной большой объем данных в памяти своего самописного на раст приложения? Не в какой нибудь модной БД с красивым название типо Кассандра, Хадуп ?) Нет?

        Ну тогда да. Только Rust.


        1. slonopotamus
          19.10.2023 08:27

          Хадуп - это вообще не про скорость.


        1. SpiderEkb
          19.10.2023 08:27

          Почему вы решили что я что-то там в памяти буду перелопачивать? И почему решили что на Rust?

          Я писал уже что у нас мощная специфическая платформа, мощный специфический язык, быстрая БД. Но. Не все можно распараллелить. Есть вещи, которые в принципе не параллелятся. Есть вещи, которые параллелить себе дороже. А есть которые можно легко распараллелить (когда много элементов и каждый обрабатывается независимо от остальных).

          Но в ряде случаев есть жесткие требования на максимальное время выполнения задачи (таймауты). И в них приходится укладываться. Любой ценой.

          Вот недавно делал оптимизацию. Есть некоторый запрос. Сложный, несколько таблиц, много условий, несколько подзапросов. Плюс distinct. Плюс агрегирование (group by) в одном из подзапросов. Еще и динамический (строка запроса формируется в рантайме, потом подготовка плана и выполнение). Задача выполняется почти 15 минут. При том данные начинают идти только через 13 минут после старта (т.е. 13 минут оно там что-то внутри себя крутит-вертит).

          Переделал запрос. Убрал distinct, убрал агрегирование. Сделал его статическим (строка задается сразу, план запроса готовится на этапе компиляции). Получил избыточный (по данным), но линейный запрос. Контроль дублей (вместо distinct) и процесс агрегирования вынес в процедуру обработки потока данных ("просеивание"). В результате время выполнения задачи сократилось в три раза - стало менее 5-ти минут.

          С памятью, кстати, проблем нет - 12Тб оперативки, 400Тб SSD массивы. Обычно используется модель памяти SINGLE LEVEL - там вы не знаете где выделена память - в оперативке или на диске, просто получаете указатель и работаете с ней. Ограничения - 16Мб статической памяти на задание (job) и не более 16Мб на один кусок динамической памяти (но кусков может быть сколько угодно). Если нужны большие куски - используем модель TERASPACE - там до 2Гб на кусок.

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


    1. mostodont32
      19.10.2023 08:27
      +5

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


  1. severgun
    19.10.2023 08:27
    +1

    Через пол года будет статья: Пук пук инвестор ушел, мы не уложились в сроки из-за скорости разработки на rust и написание асинхронного кода вызвало нервный срыв.


    1. Za4emsu
      19.10.2023 08:27

      Что же они по вашему должны были выбрать? Rust - язык быстроразвивающийся, проблем всё меньше, а новых фич больше. Да и что там с асинхронщиной? Уже анонсировали стабилизацию асинхронных функций в трейтах, так что единственный существенный аргумент против Rust скоро станет неактуальным.


    1. bingo347 Автор
      19.10.2023 08:27
      +1

      Это очень субъективный показатель, имхо. Я на достаточно хорошем уровне знаю как Rust так и Node.js + TypeScript и одну и ту же задачу сделаю на обоих за примерно одинаковое время, на Rust даже быстрее будет, хотя и больше текста набрать придется (ИИ в помощь) но за отладкой просижу в разы меньше.

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

      А насчет асинхронного кода, не понял в чем проблемы? Все с ним прекрасно в Rust.


  1. Siemargl
    19.10.2023 08:27
    +1

    Стартап-биржа использует стартап-язык для рекламы. Впрочем, какая разница для пузыря.


  1. AnthonyMikh
    19.10.2023 08:27

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


  1. Kelbon
    19.10.2023 08:27
    +1

     Rust постепенно набирает популярность ... в среде менеджмента

    Да, им легче всего на уши присесть


    1. Tuxman
      19.10.2023 08:27

      Отучаемся говорить за всех. Если бы это была педовикия, то вам бы уже накидали в тачанку, за отсутствие референсов - кто сказал, какой менеджемент?

      Я за менеджмент отвечу. Например, есть у вас легаси код, и библиотек сишных (плюсовых) разныз, и что вы будите делать в ресте, писать unsafe? В чём тогда безопасный код? А где вы будите нанимать людей? Прям на рынке такие люди есть? Так то на Golang можно за две недели переделать любого, Гуглом проверено, а ты попробуй на Раст переделать? ;-)


  1. coodi
    19.10.2023 08:27
    +4

    Много слов, мало фактов, замеров скорости, сравнения производительности. Сколько транзакций может выполнить сервер на расте в секунду?


    1. hrls
      19.10.2023 08:27

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

      Мы делали однажды похожее. Тоже на rust-lang. Наш pipeline пропускал 100k+ транзакций типовых как поставить заявку на исполнение (fix/fast шлюз, риск-менеджмент простой, matching, market data). С запасом и гарантиями.


      1. coodi
        19.10.2023 08:27

        Ну вот ссылка на Википедию: Транзакция. Что значит "пропускал"? Он их обрабатывает на расте, сохраняет изменения в базе, и отвечает, или просто насквозь пропускает?


        1. hrls
          19.10.2023 08:27

          Сохранял в базе, обрабатывал, отвечал, да