Мне пришла задача на исследование — выбрать хранилище для промышленной телеметрии. Я раньше с таким вообще не работал. Ни с временными рядами, ни с реактивным программированием, ни с WebSocket. Работал только с Postgres.

Слышал про Redis, но не применял. На выбор — SQLite или Redis, надо исследовать что лучше. Пошёл читать. Нашёл TimescaleDB и ещё кучу вариантов — у каждого свои плюсы.

Основной плюс SQLite — там можно писать запросы, SQL знакомый, Spring интеграция есть. Скорость записи меня вообще удивила — и у Redis, и у SQLite. Я не ожидал таких цифр.

Долго не мог понять зачем вообще Redis, если там нет нормального поиска. Просто хэши ключ-значение. Хочешь найти все датчики по типу объекта — не работает. Я такой: ладно, Redis минус, берём SQLite.

Сделал прототипы. Потестировал один поток — SQLite летит, 370 000 вставок в секунду. Redis ещё быстрее — 650 000. Нашёл RediSearch с поиском по индексам — оч круто, но скорость падает до 150 000. SQLite снова выглядит выигрышнее.

И вот я запускаю 100 потоков на SQLite.

В логах появляется:

org.sqlite.SQLiteException: [SQLITE_BUSY]
The database file is locked

Не один раз. Постоянно. Скорость падает с 370 000 до 98 000 вставок в секунду. Данные теряются.

Я потратил несколько дней на исследование, написал два прототипа, нашёл RediSearch — и в итоге выбирал не по тому критерию. Скорость на одном потоке вообще не важна, когда система параллельная.

Вот тогда я и понял что выбирал не по тем критериям. Делюсь цифрами — чтобы вы не повторили тот же путь.

Задача

Промышленный сервис телеметрии. Данные от датчиков летят через WebSocket и REST. Каждое измерение — тип объекта, ID, ID параметра, момент времени и значение (число, строка или дата).

Нужно:

  • быстро писать — десятки и сотни тысяч вставок в секунду

  • держать параллельную запись от множества потоков

  • искать по типу, ID, диапазону значений

  • хранить последнее значение каждого датчика

Стек — Spring WebFlux, реактивный. Это важно для выбора инструмента.

Два прототипа

Сделал два одинаковых прототипа с одинаковыми данными — один на SQLite, второй на Redis. Просто чтобы пощупать.

Скорость записи SQLite на диске с WAL — 370 000 вставок/сек. Я не ожидал такого от файловой базы.

Скорость записи Redis (чистый HSET, без индексов) — 650 000 вставок/сек. Redis работает в памяти, поэтому понятно почему так.

Но поиска в Redis нет. Просто HSET cur:1:100:5 value 42.5 — и всё. Хочешь найти все датчики с typeObj=1 — нет, так не работает. Я такой: ладно, Redis минус, берём SQLite.

Нахожу RediSearch

Продолжаю копать и нахожу модуль — RediSearch. Устанавливаю, настраиваю индексы:

FT.CREATE idx:cur ON HASH PREFIX 1 cur:
  SCHEMA typeObj NUMERIC objId NUMERIC parId NUMERIC

Теперь поиск по нескольким полям работает:

FT.SEARCH idx:cur "@typeObj:[1 1] @objId:[100 100]" LIMIT 0 1000

Оч круто. Есть поиск, есть скорость. Начинаю тестировать производительность с индексами.

Скорость записи RediSearch — 150 000–170 000 вставок/сек.

Упала в 4 раза по сравнению с чистым Redis. Индексы дорого стоят.

Смотрю на SQLite — 370 000 на диске, 700 000 in-memory. Выглядит выгоднее. Начинаю склоняться к SQLite.

Тестирую многопоточность — и всё ломается

Запускаю 100 потоков. Смотрю в логи.

org.sqlite.SQLiteException: [SQLITE_BUSY]
The database file is locked (database is locked)

Не один раз. Каждые несколько секунд.

Скорость падает с 370 000 до 98 000 вставок/сек — в почти 4 раза. А данные при этом теряются.

Почему SQLite теряет данные на многопоточке

SQLite работает с блокировкой на уровне файла. Даже в WAL-режиме — только один писатель в каждый момент. Остальные потоки ждут или получают SQLITE_BUSY и пропускают запись.

Представьте один кассир в супермаркете. Пока покупатель один — быстро. Выстроились 100 человек — кто-то уйдёт без покупок, потому что магазин закроется раньше чем до него дойдёт очередь.

Race Condition — это когда два потока одновременно пытаются изменить одни данные, и результат зависит от того, кто успеет первым. У SQLite в многопоточной записи именно это и происходит.

Можно настроить PRAGMA busy_timeout, выстроить очередь — но это уже костыли. Инструмент не для этого.

SQLite in-memory ещё хуже: каждое соединение получает изолированную базу, при рестарте все данные исчезают, при многопоточной записи теряется до 10% данных без специальной оптимизации.

Почему RediSearch выигрывает несмотря на меньшую скорость

Redis обрабатывает команды в одном потоке. Звучит как ограничение — на деле это гарантия: никаких блокировок, никакого Race Condition.

При росте числа потоков RediSearch не падает. Результаты даже слегка улучшаются — очередь команд выстраивается эффективнее. Никаких SQLITE_BUSY. Данные не теряются.

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

Важно знать: FT.SEARCH по умолчанию возвращает не больше 10 000 записей. Нужна пагинация через LIMIT offset count. JOIN между разными индексами нет — придётся делать два запроса и связывать на стороне приложения. Стандартный паттерн для NoSQL, просто держите в голове.

Цифры нагрузочного тестирования

Инструмент

Запись зап./сек

Многопоточность

SQLite (диск, WAL, кэш 400 МБ)

~370 000

При 100 потоках падает до ~98 000; SQLITE_BUSY в логах

SQLite (in-memory)

~700 000

Теряет до 10% данных без оптимизации

Redis (чистый HSET, без индексов)

~650 000

Нет проблем — однопоточный обработчик

RediSearch (с индексами)

~150 000–170 000

Нет проблем; при росте потоков результат улучшается

Таблица рисков

Риск

SQLite

RediSearch

Многопоточная запись

Единственный писатель; SQLITE_BUSY; потери данных

Нет блокировок; предсказуемая скорость

Объём данных

Неограничен (диск); деградация после 50–100 ГБ

Ограничен RAM; индексы занимают в 2–5 раз больше данных

Потеря данных при сбое

Полный ACID; WAL защищает

Без AOF/репликации — потеря последних операций

Поиск и JOIN

Полный SQL + JOIN

Нет JOIN; нужно два запроса + связка в коде

Масштабирование

Вертикально, один файл

Redis Cluster, горизонтально

Eviction

При переполнении памяти удаляет старые ключи по LRU

Итог

SQLite хорош. Для одного-двух потоков, небольших объёмов, простой аналитики — отличный выбор. Как только появляется конкурентная запись от сотни потоков — начинаются проблемы.

RediSearch медленнее на бумаге: 150 000 против 700 000. Но это предсказуемые 150 000 без потерь на любом числе потоков. Против непредсказуемого SQLite, который теряет данные и падает до 98 000 под нагрузкой.

Для высоконагруженной системы с множеством параллельных источников — RediSearch надёжнее. Даже с более низкой пиковой скоростью.

Если интересно следить за процессом — канал @java_quant

Пообщаться или задать вопрос — @karim_product, на связи.

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


  1. DennisP
    13.05.2026 14:50

    Естественно, ведь SQlite это библиотека, а не север, соответственно нет никаких продвинутых механизмов типа row-level locking. А есть тупо блокирование всего файла, т.е. БД на запись. Ниша SQlite другая.


  1. kmatveev
    13.05.2026 14:50

    Вам не приходила в голову идея переложить через очередь в один поток, который будет сохранять? На reactor-e, который является основой webflux, это должно занять примерно 20 символов кода


    1. KarimAbushaev Автор
      13.05.2026 14:50

      Тогда теряется скорость записи, по факту тот же redis получается? Только он с этим справляется лучше


      1. Pcturl
        13.05.2026 14:50

        А вот опытные люди пишут, что он тоже не без нюансов:


  1. LeshaRB
    13.05.2026 14:50

    Промышленный сервис телеметрии. Данные от датчиков летят через WebSocket и REST. Слышал про Redis, но не применял. На выбор — SQLite или Redis, надо исследовать что лучше. Пошёл читать. Нашёл TimescaleDB и ещё кучу вариантов — у каждого свои плюсы.

    Так с постгресом, что не так! Чего его использовать нельзя?


  1. Ktulhy
    13.05.2026 14:50

    «Зумеры обнаружили, что sqlite однопоточный». Каждый раз. Без суеты. Твёрдо. Чётко. Ты «вскрыл» самую суть.


  1. EgorSharin
    13.05.2026 14:50

    У меня накопился немалый опыт работы с timeseries databases. Поэтому тем, кто это будет читать, если вам особенно важна скорость многопоточной записи, советую присмотреться к QuestDB и ScyllaDB.

    Помимо записи, QuestDB - в первую очередь, аналитическая база данных, и когда впервые запускаешь аналитическую query на примерно сотне миллионов строчек (каждая - 9 столбцов, на 4-х из них - или WHERE, или GROUP BY), с непривычки скорость выполнения запроса заставляет предположить, что я где-то лоханулся, и это - не настоящий результат, а какая-то фигня: безумно быстро (после многих лет работы с SQL Server и PostgreSQL). Очень рекомендую.

    ScyllaDB попроще и немножко “поглупее”, - никак не аналитика, но зато multi-master replication в бесплатной (!) версии. И оно реально работает.

    Я уже год как ушёл от PostgreSQL, и не жалею.


  1. boldape
    13.05.2026 14:50

    В скулайте просто задайте свой хук обработчик бизи который бесконечно ретраит и ни одно сообщение не потеряется. Скулайт прекрасен вы его не доготовили.


  1. inilim
    13.05.2026 14:50

    Есть куча мест для оптимизаций, например добавлять записи через json функции, это обеспечивает быстрое добавление в бд. (Чем быстрее вставка чем меньше блокировок) Этот трюк улучшает не только добавление но и выборку и обновление.

    db.execute("""
    WITH data AS (SELECT e ->> 'name' as name, e ->> 'email' as email FROM json_each(?) e)
    INSERT INTO users(name, email) SELECT data.name, data.email FROM data
    """, [jsonEncode(data)]);

    Еще можно создавать базу in-memory, вставлять ограниченное количество записей туда, и при достижении максимума, через присоединение БД (ATTACH), переносить строки в файл и очищать in-memory бд.

    ATTACH DATABASE file_name AS database_name;

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


  1. diafour
    13.05.2026 14:50

    Вот тогда я и понял что выбирал не по тем критериям.

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

    Вы же данные не просто собираете, надо их будет анализировать как-то? Prometheus (и аналоги victoria, prompp), influx, clickhouse, упомянутый вами же timescaleDB на основе postgres — у них всех из коробки есть аналитические функции. Дальше нужно будет строить графики, тут пригодятся функции для работы с интервалами, группировки с округлением времени, rate/irate/rate с поддержкой отсутствующих значений.

    А ещё ha режим, бэкапы, шардирование, агрегация данных увеличенным интервалом для истории, алерты и наверняка что-то ещё забыл.

    С sqlite это всё нужно будет писать самому.

    P.S. Признаюсь, я и сам использовал sqlite для хранения таймсерий (и оно живёт в продах), но это было промежуточное хранение значений, чтобы дальше отдать в prometheus.