В предыдущих сериях был реализован простейший движок на основе HashMap, в которой сохраняются данные key -> value, и в принципе была открыта дорога для написания сервера и клиента для тестов. Но я решил добавить в Space работу с распределенными (XA) транзакциями.
Наличие такого механизма обязательно приведет к деградации производительности. Закономерно возникает вопрос: для чего это было сделано? Memifydb - это распределенная БД и она должна обеспечивать конкурентный доступ к данным обеспечивая их целостность. Что проку если она будет работать быстро, но её содержимое будет - хаос? Ведь деже при создании простого приложения для обработки данных в нескольких потоках используется synchronized в java (и пр. механизмы синхронизации).
Второй момент состоит в том что я всегда работал с транзакциями на стороне клиента, т.е. работал с менеджером транзакции (TransactionManager). Теперь же я писал сервис и пришлось иметь место с менеджером ресурсов (XAResource), что тоже интересно (не забываем что проект модельный и исследовательский).
Итак, транзакционность добавил, пока что на уровне RAM без сохранения данных в долговременную помять для отката/восстановления. Следующий шаг это написание самого сервера, который будет управлять Space’ами и обработкой клиентских запросов.
Изначально строю некоторый каркас, который буду достраивать и наполнять содержанием. Это будет удобно для прототипирования.
Для работы с данными создам некоторый дополнительный уровнь абстракции, что бы не привязываться к конкретному формату данных/библиотеке и менять его налету для сравнения.
Сделано
Данные клиента будут храниться в space’ах - это будет аналог таблиц в БД. Для начала будет только key-value space что бы можно было подумать уже сейчас о WAL клиенской библиотеке для java и сетевом уровне в целом и конечно же о транзакциях.
В предыдущих постах Разбираемся в in-memory базах и Выбираем базу я написал, что собираюсь сделать исследовательский проект по in-memory базам данных и имя ему MemifyDB. Так же выбрал направление движения: это key-value хранилище, которое потом доработаю до документо-ориентированной системы.
Теперь ключевой вопрос: как клиенты будут с ней общаться?
Протокол обмена — это мост между сервером и клиентом. От его дизайна зависит скорость, удобство и даже то, какие фичи мы сможем реализовать.
Human readable (JSON, XML and etc.)
В современных системах активно используется как для транспорта так и для хранения текстовый формат данных, а точнее json и его вариации. У этого подхода есть несколько несомненных плюсов, например:
Простота реализации: не нужно поддерживать разные типы на уровне протокола, всё передаётся как строки, а клиент сам разбирается.
Гибкость: строкой можно закодировать что угодно — число, JSON, бинарные данные.
Но у этого подхода есть обратная сторона:
❌ Нет нативной поддержки типов: Клиент сам должен сериализовать/десериализовать.
❌ Оверхед на парсинг: Каждый раз нужно преобразовывать “42” в число и обратно.
❌ Неэффективное использование памяти: Число 123456 занимает 6 байт как строка, хотя в бинарном виде — 4 или 8.
❌ Невозможность частичного обновления сложных структур: Чтобы изменить одно поле в JSON-объекте, приходится переписывать весь объект (можно конечно поспорить).
## 🎯 Наш подход: типизированные данные с рождения В MemifyDB мы пойдём другим путём. Мы будем хранить данные в памяти в типизированном виде: строки, числа, списки, хеши, документы — каждый тип со своим внутренним представлением.
И протокол обмена с клиентом должен это отражать. Мы не хотим, чтобы клиент упаковывал число в строку только потому, что так проще. Мы хотим передавать по сети те же бинарные структуры, которые лежат в памяти.
Это даст:
Типизированность Формат должен явно различать типы данных: строки, числа (разной разрядности), булевы значения, null, массивы, объекты (документы). Это позволит серверу правильно интерпретировать данные без дополнительных метаданных.
Компактность Формат не должен раздувать данные. Число 42 должно занимать 8 байт (или 4, если это int32), а не 2 символа ASCII.
Быстрая навигация Мы должны иметь возможность быстро «прыгнуть» к определённому полю документа без полного парсинга. Это важно для частичных обновлений и запросов.
Потоковость Формат должен допускать частичную отправку/приём, чтобы можно было обрабатывать большие документы по частям.
Самодостаточность Данные должны содержать всю информацию для интерпретации, но при этом не дублировать имена полей без необходимости (как в JSON).
🔍 Что дальше?
Существуют несколько бинарных: CBOR, BSON, FlatBuffer и пр. Если у вас есть опыт работы с этим форматами, пишите в комментариях какие у них есть плюсы, минусы и подводные камни.
Прежде чем погружаться в код, нужно чётко ответить на вопрос: а что именно мы строим? Этот пост — не про хардкорный кодинг, а про так называемое видение системы (system vision). Мы разберём, какие вообще бывают in-memory базы данных, чем они отличаются и почему наш путь будет таким: key-value → документная БД.
Поехали!
📚 Типы in-memory баз данных
1. Key-Value хранилища
Самый простой и быстрый вид. Данные хранятся как пары «ключ — значение», где значение — обычно строка, число или бинарный объект.
Хранят сущности (узлы) и связи между ними (рёбра). Оптимизированы для обходов графа.
Примеры: RedisGraph (модуль Redis), Neo4j (с in-memory режимом).
Сценарии: социальные сети, рекомендательные системы, сети связей.
Плюсы: эффективные запросы связей, интуитивная модель для связанных данных.
Минусы: узкая ниша, сложность шардирования.
5. Time-series БД
Специализированы для временных рядов — метрик, логов, событий. Оптимизированы для записи и запросов по временным интервалам.
Примеры: InfluxDB, TimescaleDB (in-memory части).
Сценарии: мониторинг, IoT, финансовые тикеры.
Плюсы: высокая скорость записи, сжатие старых данных, встроенные функции по времени.
Минусы: слабо подходят для произвольных данных.
…## 🧭 Наш курс: от Key-Value к документам
Для MemifyDB я выбрал путь, который кажется самым прагматичным:
Создаём ядро Key-Value Это фундамент. Мы реализуем:
потокобезопасное in-memory хранилище;
поддержку разных типов значений (строки, списки, хеши, числа);
механизмы TTL и эвикшена (LRU);
бинарный протокол, близкий к внутреннему представлению.
Key-value движок даст нам максимальную скорость и стабильность, а также позволит отточить все низкоуровневые механизмы (аллокаторы, сериализацию, сетевое взаимодействие).
Надстраиваем документный слой Поверх key-value ядра мы добавляем возможность интерпретировать значение как JSON-подобный документ:
индексация по полям;
поддержка частичного обновления;
запросы с фильтрацией по содержимому.
При этом сами документы будут храниться в key-value хранилище как обычные значения, а индексы — как дополнительные структуры (хеш-таблицы, B-деревья) в памяти.
Такая двухслойная архитектура даёт:
гибкость — можно работать и как с обычным кэшем, и как с документной БД;
производительность — key-value ядро остаётся быстрым, а документные операции добавляются без потери эффективности;
расширяемость — позже можно добавить другие модели (например, колоночные агрегаты) как отдельные слои.
🧠 Разбираемся, как устроены in-memory БД: пишем MemifyDB с нуля.
Redis быстр, но не всегда удобен. SAP HANA — мощь, но ценник… А что, если заглянуть под капот и создать свою enterprise in-memory СУБД? Не чёрный ящик, а полностью прозрачную, современную, open-source — и при этом готовую к высоким нагрузкам. Разбираемся как это работает!
Знакомьтесь — MemifyDB.
📌 Что это будет? Настоящая in-memory система уровня enterprise, в которой мы разберёмся до винтика:
живёт в RAM, отвечает за микросекунды;
сохраняет данные на диск (RDB + WAL) — никакой потери после ребута;
реплицируется и шардируется «из коробки»;
поддерживает транзакции (не хуже MULTI/EXEC, но с возможным rollback);
и при этом не просит продать почку за лицензию.
Весь код — open source, все решения — с объяснениями.
🛠 Технический фундамент (выбираем стек вместе) Платформа — JVM. Я сейчас выбираю между Java 21 (Loom) и Scala 3 (ZIO / Akka).
Сеть: Netty или виртуальные потоки — посмотрим на бенчмарках.
Память: off-heap + собственный slab-аллокатор на ByteBuffer. GC не мешает, фрагментация под контролем.
Конкурентность: Lock-Free структуры данных, чтобы не блокировать потоки.
Протокол: RESP-совместимость — redis-cli сможет общаться с нами.
🗺 Дорожная карта: что и когда разберём
Ядро: потокобезопасное KV-хранилище в памяти. Как работают LRU и TTL?
Persistence: снапшоты и WAL. Как не потерять данные при краше?
Сеть: пишем TCP-сервер. Netty vs Loom — кто быстрее?
Транзакции: реализуем MULTI/EXEC, WATCH. Нужен ли MVCC?
Репликация и Raft: как достичь консенсуса в распределённой системе?
Каждый этап — открытый код, пост с разбором, грабли и профит.
🤔 Зачем я это делаю публично? Во‑первых, разобраться самому и дать шанс разобраться другим. Во‑вторых, фидбек сообщества ловит ошибки на берегу. В‑третьих, хочется сделать реально полезный инструмент, а не очередной pet‑project.
💬 Вопрос к залу: Какой стек предпочтительнее для enterprise in‑memory БД — Java 21 + Loom или Scala + ZIO/Akka? Какие фичи вы бы добавили в дорожную карту? Пишите в комментариях — лучшие идеи уйдут в реализацию!
👉 Подписывайтесь, чтобы не пропустить:
глубокий разбор off‑heap аллокатора;
сравнение моделей конкурентности на реальных бенчмарках;
историю о том, как одна строка unsafe кода валила прод три дня.