Мне пришла задача на исследование — выбрать хранилище для промышленной телеметрии. Я раньше с таким вообще не работал. Ни с временными рядами, ни с реактивным программированием, ни с 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 (in-memory) |
~700 000 |
Теряет до 10% данных без оптимизации |
Redis (чистый HSET, без индексов) |
~650 000 |
Нет проблем — однопоточный обработчик |
RediSearch (с индексами) |
~150 000–170 000 |
Нет проблем; при росте потоков результат улучшается |
Таблица рисков
Риск |
SQLite |
RediSearch |
|---|---|---|
Многопоточная запись |
Единственный писатель; |
Нет блокировок; предсказуемая скорость |
Объём данных |
Неограничен (диск); деградация после 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)

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

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

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

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

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, и не жалею.

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

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;На крайний случай, создавать временные файлы бд которые дублируют структуру таблицы с которой работаете, и через время импортировать в основную базу.

diafour
13.05.2026 14:50Вот тогда я и понял что выбирал не по тем критериям.
В общем да, скорости многопоточной записи и возможности "поиска датчика sql запросом" как будто мало для выбора бд к "промышленной телеметрии".
Вы же данные не просто собираете, надо их будет анализировать как-то? Prometheus (и аналоги victoria, prompp), influx, clickhouse, упомянутый вами же timescaleDB на основе postgres — у них всех из коробки есть аналитические функции. Дальше нужно будет строить графики, тут пригодятся функции для работы с интервалами, группировки с округлением времени, rate/irate/rate с поддержкой отсутствующих значений.
А ещё ha режим, бэкапы, шардирование, агрегация данных увеличенным интервалом для истории, алерты и наверняка что-то ещё забыл.
С sqlite это всё нужно будет писать самому.
P.S. Признаюсь, я и сам использовал sqlite для хранения таймсерий (и оно живёт в продах), но это было промежуточное хранение значений, чтобы дальше отдать в prometheus.

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