Всем привет!

Идея LensDB родилась с простого поста моего друга. Он делился своим опытом создания Shared Memory кэша для своего пет-проекта на C#. В этом посте он написал:

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

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

Почему я решил сделать именно так?

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

В традиционных NoSQL‑системах, таких как Redis, данные передаются в виде строк или сериализованных объектов, что требует дополнительных операций на каждом запросе (парсинг, сериализация и т. д.). В результате это замедляет скорость работы и увеличивает использование памяти.

Что если можно сделать всё проще? Что если можно обрабатывать данные прямо как бинарные данные, без преобразования в текст и обратно? Это позволило бы:

  • в 30 раз большую скорость, чем Redis, при обработке определённых типов запросов.

  • меньше потребления памяти — данные занимают до 30% меньше места.

  • быстрее сериализация и десериализация данных благодаря использованию MemoryPack и работы с битами, что значительно снижает накладные расходы по сравнению с JSON.

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

Что такое LensDB?

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

Основные особенности LensDB:

  • отказ от JSON и текстовых форматов: работа с данными происходит через сырые байты, что ускоряет операции и сокращает использование памяти

  • максимальная производительность: благодаря использованию MemoryPack и битовой сериализации, процесс сериализации и десериализации ускоряется в несколько раз по сравнению с традиционными подходами

  • простота и интеграция: LensDB сохраняет привычный интерфейс key-value и может быть легко интегрирован в существующие системы, но с гораздо большей производительностью

Почему я выбрал Haskell?

Для реализации LensDB я выбрал Haskell, потому что он идеально подходит для создания надёжных и высокопроизводительных систем. Haskell даёт возможность:

  • писать эффективный и безопасный код с минимальными накладными расходами

  • использовать параллелизм для обработки большого количества запросов одновременно

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

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

Что дальше?

Проект LensDB ещё находится на стадии разработки, и я активно работаю над улучшением функционала. Альфа‑версия уже доступна на GitHub, и я буду рад любым предложениям, багрепортам и pull‑request'ам.

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

Ссылку на проект и его исходники можно найти здесь: GitHub.


LensDB — это не просто эксперимент. это попытка показать, как можно создать систему хранения данных с фокусом на высокую производительность и простоту интеграции, при этом значительно улучшив скорость и эффективность по сравнению с традиционными решениями, такими как Redis.

Если вам интересен этот проект, хотите поучаствовать или просто обсудить идеи — буду рад вашим комментариям и предложениям!

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


  1. Dhwtj
    07.01.2026 18:09

    Сделайте бенч


    1. CodWiz Автор
      07.01.2026 18:09

      Running 1 benchmarks...
      Benchmark lensdb-bench: RUNNING...
      benchmarking Core Operations/set operation
      time                 364.0 μs   (343.0 μs .. 397.6 μs)
                           0.863 R²   (0.766 R² .. 0.972 R²)
      mean                 434.6 μs   (401.7 μs .. 483.6 μs)
      std dev              143.0 μs   (99.53 μs .. 207.3 μs)
      variance introduced by outliers: 98% (severely inflated)
      
      benchmarking Core Operations/get operation
      time                 232.2 μs   (221.4 μs .. 248.3 μs)
                           0.969 R²   (0.943 R² .. 0.991 R²)
      mean                 249.4 μs   (239.2 μs .. 263.4 μs)
      std dev              40.42 μs   (30.57 μs .. 52.04 μs)
      variance introduced by outliers: 91% (severely inflated)
      
      benchmarking Core Operations/delete operation
      time                 519.6 μs   (492.0 μs .. 556.8 μs)
                           0.955 R²   (0.932 R² .. 0.978 R²)
      mean                 645.1 μs   (601.4 μs .. 700.3 μs)
      std dev              183.1 μs   (145.3 μs .. 227.1 μs)
      variance introduced by outliers: 97% (severely inflated)
      
      benchmarking Core Operations/exists operation
      time                 2.379 μs   (2.371 μs .. 2.386 μs)
                           1.000 R²   (1.000 R² .. 1.000 R²)
      mean                 2.392 μs   (2.378 μs .. 2.410 μs)
      std dev              48.11 ns   (33.22 ns .. 67.71 ns)
      variance introduced by outliers: 22% (moderately inflated)
      
      benchmarking Storage Operations/storage statistics
      time                 330.2 μs   (327.1 μs .. 333.1 μs)
                           0.999 R²   (0.998 R² .. 0.999 R²)
      mean                 329.8 μs   (326.9 μs .. 334.6 μs)
      std dev              12.01 μs   (8.662 μs .. 16.29 μs)
      variance introduced by outliers: 31% (moderately inflated)
      
      benchmarking Storage Operations/cleanup operations
      time                 332.0 μs   (326.5 μs .. 339.4 μs)
                           0.998 R²   (0.996 R² .. 0.999 R²)
      mean                 332.1 μs   (328.6 μs .. 336.2 μs)
      std dev              13.21 μs   (10.30 μs .. 17.36 μs)
      variance introduced by outliers: 35% (moderately inflated)
      
      benchmarking Protocol Operations/message serialization
      time                 546.5 ns   (471.6 ns .. 634.0 ns)
                           0.907 R²   (0.872 R² .. 0.971 R²)
      mean                 608.5 ns   (531.9 ns .. 771.8 ns)
      std dev              366.1 ns   (131.9 ns .. 595.8 ns)
      variance introduced by outliers: 100% (severely inflated)
      
      benchmarking Protocol Operations/message deserialization
      time                 776.0 ns   (591.8 ns .. 1.026 μs)
                           0.641 R²   (0.543 R² .. 0.813 R²)
      mean                 772.3 ns   (655.8 ns .. 957.5 ns)
      std dev              471.1 ns   (315.4 ns .. 772.8 ns)
      variance introduced by outliers: 100% (severely inflated)
      
      benchmarking Protocol Operations/response serialization
      time                 449.2 ns   (442.4 ns .. 457.3 ns)
                           0.997 R²   (0.995 R² .. 0.998 R²)
      mean                 458.9 ns   (448.0 ns .. 478.2 ns)
      std dev              49.68 ns   (27.01 ns .. 83.54 ns)
      variance introduced by outliers: 91% (severely inflated)
      
      benchmarking Concurrent Operations/concurrent sets
      time                 2.178 ms   (2.128 ms .. 2.230 ms)
                           0.997 R²   (0.995 R² .. 0.998 R²)
      mean                 2.185 ms   (2.161 ms .. 2.214 ms)
      std dev              90.47 μs   (74.03 μs .. 116.4 μs)
      variance introduced by outliers: 27% (moderately inflated)
      
      benchmarking Concurrent Operations/concurrent gets
      time                 748.2 μs   (725.9 μs .. 788.1 μs)
                           0.974 R²   (0.956 R² .. 0.991 R²)
      mean                 764.4 μs   (741.2 μs .. 799.7 μs)
      std dev              97.61 μs   (73.92 μs .. 126.4 μs)
      variance introduced by outliers: 83% (severely inflated)
      
      Benchmark lensdb-bench: FINISH
      


      это при альфе версии так, в будущем я думаю еще ускорить и оптимизировать


      1. Dhwtj
        07.01.2026 18:09

        В общем, не быстро: большой разброс и значительно медленнее хешмапы вроде dashmap (Rust). Причём, не похоже на время disk io.

        Советы от LLM (ну, чем богаты...)

        -- Вероятно, у тебя что-то вроде:
        atomically $ do
          store <- readTVar globalMap
          let newStore = Map.insert key (serialize value) store  -- ❌ Копирование всей Map!
          writeTVar globalMap newStore

        Проблема не в сериализации, а в:

        1. Иммутабельность + STM = копирование всей структуры при каждой записи (даже с ByteString)

        2. GC — видно по 98% outliers: каждая запись создаёт новые объекты, сборщик мусора "останавливает мир"

        3. Data.Map = дерево с логарифмической сложностью и аллокациями на каждый узел

        Я бы наоборот, взял dashmap и потихоньку шаг за шагом прикручивать сетевой io и персистентность


        1. Dhwtj
          07.01.2026 18:09

          Ну типа такого

          use dashmap::DashMap;
          use tokio::net::TcpListener;
          use bytes::Bytes;
          
          struct LensDB {
              store: DashMap<Bytes, Entry>,
              wal: WalWriter,  // добавишь потом
          }
          
          struct Entry {
              value: Bytes,
              expires_at: Option<Instant>,
          }
          
          impl LensDB {
              // Этап 0 — чистый DashMap
              fn get(&self, key: &[u8]) -> Option<Bytes> {
                  self.store.get(key).map(|e| e.value.clone())
              }
              
              fn set(&self, key: Bytes, value: Bytes) {
                  self.store.insert(key, Entry { value, expires_at: None });
              }
          }
          
          // Этап 1 — добавляем TCP
          async fn serve(db: Arc<LensDB>, addr: &str) -> Result<()> {
              let listener = TcpListener::bind(addr).await?;
              
              loop {
                  let (socket, _) = listener.accept().await?;
                  let db = db.clone();
                  
                  tokio::spawn(async move {
                      handle_connection(db, socket).await
                  });
              }
          }

          А потом усложнял бы

          use tokio::sync::mpsc;
          
          struct WalWriter {
              tx: mpsc::Sender<WalEntry>,
          }
          
          impl LensDB {
              async fn set_durable(&self, key: Bytes, value: Bytes) {
                  // 1. Сначала в память (быстро)
                  self.store.insert(key.clone(), Entry { value: value.clone(), .. });
                  
                  // 2. Асинхронно в WAL (не блокирует)
                  let _ = self.wal.tx.send(WalEntry::Set(key, value)).await;
              }
          }
          
          // Отдельный таск пишет на диск
          async fn wal_flusher(mut rx: mpsc::Receiver<WalEntry>, file: File) {
              let mut buffer = Vec::with_capacity(4096);
              
              loop {
                  // Собираем батч
                  while let Ok(entry) = rx.try_recv() {
                      entry.encode_into(&mut buffer);
                  }
                  
                  if !buffer.is_empty() {
                      file.write_all(&buffer).await?;
                      // fsync по таймеру или по размеру
                      buffer.clear();
                  }
                  
                  tokio::time::sleep(Duration::from_millis(1)).await;
              }
          }

          Ожидаемые результаты на уровне Redis (100-200 kops/sec).

          И затраты на сериализацию можно смотреть только если уже упёрлись в остальные ограничения.

          P.S. код довольно кривой, только для объяснения базового алгоритма


        1. gev
          07.01.2026 18:09

          Судя по зависимостям так и есть ))) Вот на тему как раз
          https://www.parsonsmatt.org/2025/12/17/the_subtle_footgun_of_tvar_(map____).html


  1. Belvarm
    07.01.2026 18:09

    Смотрели ли в сторону rocksdb? Интересно было бы сравнить


    1. mentin
      07.01.2026 18:09

      Тоже читал и пытался понять принципиальные отличия от RocksDB и прочих производных LevelDB.


    1. CodWiz Автор
      07.01.2026 18:09

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


  1. faxenoff
    07.01.2026 18:09

    Привет! Интересный прокет.

    Вопросы:

    • TTL для записей есть? как реализован, если есть? Мне надо избавляться от старых записей без лишних телодвижений.

    • Как настраивается соотношение "в памяти" к "на диске"? Если в кубере запускать, то не надо заходить за лимиты.


    1. CodWiz Автор
      07.01.2026 18:09

      1. пока что нет ttl-реализации, но в дальнейшем, думаю, добавлю

      2. контроль использования памяти/диска на уровне конфигурации есть частично (max_memory), но жёстких ограничений нет, это контролируется на уровне окружения (например, kubernetes)


  1. bolk
    07.01.2026 18:09

    Вы там memcached изобретаете что ли?


    1. Dhwtj
      07.01.2026 18:09

      Нет. Персистентность тоже в дизайне упомянута


  1. inklesspen
    07.01.2026 18:09

    В традиционных NoSQL‑системах, таких как Redis, данные передаются в виде строк или сериализованных объектов

    Не передаю́тся, а МОГУТ передаваться, что часто и делается, но это не является ограничением. Здесь например туда решили класть msgpack - бинарный формат сообщений, и сохранили 50% памяти.