Том I. Основы
Уровень: для новичков — тех, кто никогда не работал с embedded key-value базами данных. Цель тома: вы понимаете, что такое libmdbx, умеете собрать её, открыть базу и выполнить первые операции чтения и записи в транзакциях. Сквозной проект: на протяжении томов I–II мы строим одно приложение — конфигуратор приложений (простое key-value хранилище параметров, которое постепенно обрастает индексами, многопоточностью и оптимизациями).
Прогрессия тома: концепция → практика → механизм → нюанс. Код примеров — полный и компилируемый; вспомогательные функции (
env_openв C++ /ex_env_openв C, а такжеcheck_rc,die) едины для всего учебника (см.examples/common/).
Глава 1. Что такое libmdbx
1.1. Встраиваемая база данных
Базы данных бывают двух принципиально разных видов: клиент-серверные и встраиваемые.
Клиент-серверная база (PostgreSQL, MySQL) — это отдельный процесс-сервер. Ваше приложение общается с ним по сети или сокету: отправляет SQL, получает ответы. Сервер владеет файлами данных, управляет кэшем, конкурентным доступом и журналом. Вы платите за это сложностью развёртывания, отдельным процессом и сетевыми накладными.
Встраиваемая база (SQLite, libmdbx) — это библиотека, которую вы линкуете прямо в свой процесс. Нет сервера, нет сети: ваше приложение вызывает функции, а те напрямую читают и пишут файл. Преимущества — простота (одна библиотека), скорость (нет сетевого слоя) и лёгкость распространения (приложение само носит свою базу). Плата — вы сами отвечаете за конкурентный доступ и резервное копирование.
libmdbx — встраиваемая транзакционная key-value база данных, глубоко переработанный потомок известной библиотеки LMDB. «Транзакционная» означает, что группа операций выполняется атомарно: либо все изменения применяются, либо ни одно.
Аналогия. SQLite — это «встраиваемый PostgreSQL». libmdbx — это «встраиваемый высокопроизводительный кэш-сервер»: она не знает SQL и таблиц в реляционном смысле, но зато умеет отдавать данные со скоростью обращения к памяти.
1.2. Key-value модель данных
Key-value база хранит простые пары: ключ → значение. Обе части — просто последовательности байтов, о смысле которых знает только ваше приложение.
"user:1001" → {"name": "Анна", "role": "admin"}
"user:1002" → {"name": "Борис", "role": "user"}
"theme" → "dark"
Отличие libmdbx от «обычного» словаря (std::map, HashMap):
- Ключи всегда упорядочены. Данные хранятся в сбалансированном дереве (B+tree), поэтому ключи можно обходить по порядку, искать диапазоны, находить «ближайший больший ключ». Это как массив с отсортированными ключами, а не как хэш-таблица.
- Значения могут быть большими — вплоть до ~2 ГБ (уходят на отдельные «overflow»-страницы).
- Данные лежат в файле, отображённом в память. Чтение ключа — это фактически разыменование указателя, без копирования и без сериализации.
1.3. Почему mmap: память как интерфейс к данным
Большинство баз данных держат собственный буферный кэш: при чтении — копируют страницу с диска в память библиотеки; при записи — накапливают изменения в этом кэше и периодически сбрасывают на диск.
libmdbx использует другой подход: memory-mapped файл (mmap). Операционная система отображает файл базы прямо в адресное пространство процесса. Данные выглядят как обычный массив байтов. Чтение — это обращение по адресу; страницу при необходимости подтянет ядро (page cache). Отдельного буферного кэша в библиотеке нет — его роль выполняет страничный кэш ОС.
Из этого следуют три важных свойства:
- Чтение не копирует данные. Вы получаете указатель прямо в mmap-области — никакой десериализации. Это источник рекордной скорости чтения (обычно миллионы операций в секунду).
- Запись идёт через Copy-on-Write (CoW). Меняя страницу, библиотека не трогает старую версию (её могут читать другие транзакции), а создаёт новую копию. Подробности — в Томе III.
- Операционная система сама решает, какие страницы держать в памяти. Библиотека не выбирает стратегию вытеснения — это делает ядро.
1.4. Место libmdbx в ландшафте
| База | Модель | Архитектурные свойства |
|---|---|---|
| LMDB | KV, mmap, MVCC | Родоначальник подхода; проще, меньше возможностей |
| libmdbx | KV, mmap, MVCC | Переработанный LMDB: больше режимов долговечности, GC вместо free-list, жёстче гарантии |
| BerkeleyDB | KV, классическая | Старшее поколение; блокировки уровня страниц, нет mmap |
| LevelDB/RocksDB | KV, LSM | Оптимизированы на запись, фоновое сжатие, плохая предсказуемость чтения |
| SQLite | Реляционная, встраиваемая | SQL, транзакции; медленнее на KV-сценариях |
Здесь MVCC (multi-version concurrency control, управление многоверсионностью) — способ одновременного чтения и записи без блокировок читателей: каждая транзакция видит неизменный снимок данных (подробно — глава 5 и том III). LSM (log-structured merge-tree) — альтернативная архитектура, оптимизированная под запись ценой предсказуемости чтения.
Сравнение на уровне свойств (не бенчмарков): если вам нужен упорядоченный key-value с транзакциями, молниеносным чтением и предсказуемой записью — выбирайте libmdbx. Если нужен SQL — SQLite. Если запись доминирует над чтением и можно терпеть фоновое сжатие — RocksDB.
1.5. Лицензия, экосистема, кто использует
libmdbx распространяется по разрешительной лицензии: исторически — OpenLDAP Public License (OLPL), на master — Apache-2.0 целиком (ре-лицензирование в 0.13.x). Позволяет коммерческое использование без открытия собственного кода.
Библиотеку используют в высоконагруженных проектах:
- Ethereum-экосистема: Erigon, Akula, Silkworm (все быстрые реализации Ethereum работают на libmdbx);
- Isar — локальная база Flutter-приложений;
- мессенджеры, аналитика, кэш-слои.
Экосистема биндингов покрывает Rust, Go, Python, Node.js, .NET, C++, Zig, Dart, Nim, Java, Haskell, Ruby, Scala (подробно — Том VI).
1.6. Резюме главы 1
- libmdbx — встраиваемая транзакционная key-value БД (библиотека, не сервер).
- Ключи упорядочены (B+tree); значения — произвольные байты до ~2 ГБ.
- Данные отображены в память (mmap): чтение без копирования, запись через Copy-on-Write.
- Свойства: wait-free читатели, один писатель, транзакции, отсутствие WAL (восстановление по мете).
- Apache-2.0 (master)/OLPL (исторически); используется в Ethereum-клиентах, Isar и других проектах.
1.7. Упражнения
- Чем клиент-серверная БД отличается от встраиваемой? Приведите пример, когда встраиваемая предпочтительнее.
- Что даёт упорядоченность ключей сверх обычного словаря?
- Почему чтение через mmap может быть быстрее, чем через собственный кэш БД?
1.8. Чек-лист главы 1
- [ ] я могу объяснить разницу между клиент-серверной и встраиваемой БД и сказать, когда какая предпочтительнее;
- [ ] понимаю, что libmdbx — упорядоченный key-value движок (B+tree), а не хэш-таблица и не SQL;
- [ ] могу описать роль mmap: чтение без копирования, запись через Copy-on-Write, вытеснение страниц делает ядро;
- [ ] знаю ключевые свойства: MVCC, wait-free читатели, один писатель, отсутствие WAL;
- [ ] помню лицензию (Apache-2.0 на master) и примеры проектов на libmdbx (Erigon, Isar).
1.9. Что дальше
Понимание «что такое libmdbx» пора превратить в работающий код. В главе 2 мы соберём библиотеку двумя способами (амальгама и CMake), подключим её к проекту и запустим первый полный пример «hello, libmdbx». Для сквозного проекта-конфигуратора это обязательный шаг: без собранной библиотеки и проверенного цикла open → put → get двигаться дальше нельзя.
Глава 2. Установка и первый запуск
2.1. Сборка из исходников
libmdbx можно собирать двумя способами:
- Амальгамированный (амальгама): один файл
mdbx.c+ заголовокmdbx.h, которые можно просто положить в ваш проект. Минимум зависимостей, идеально для встраивания. - Полный исходник: каталог с
CMakeLists.txt, тестами, примерами и утилитами (mdbx_stat,mdbx_chk,mdbx_copy,mdbx_dump,mdbx_load,mdbx_defrag).
Амальгама: скачайте mdbx.h и mdbx.c и добавьте mdbx.c в сборку вашего проекта. Больше ничего
не требуется — это настоящий «single-file» движок.
2.2. Подключение к проекту
CMake (полный исходник):
add_subdirectory(libmdbx)
target_link_libraries(yourapp PRIVATE mdbx)
или pkg-config:
pkg-config --cflags --libs libmdbx
Собираете сами:
make -j
make install # ставит libmdbx.a / libmdbx.so, mdbx.h, .pc
Статика vs динамика. Статическая линковка проще распространять (нет проблем с версиями .so) и даёт больше шансов оптимизации. Динамическая позволяет обновлять движок без пересборки приложения.
2.3. Ключевые опции сборки
| Опция | Смысл |
|---|---|
MDBX_LOCKING |
Реализация блокировок: POSIX2008 (Linux, по умолчанию), POSIX2001, SYSV (macOS по умолчанию), WIN32FILES (Windows) |
MDBX_CHECKING |
Уровень внутренних проверок/ассертов (−1…3). 2/3 сильно замедляют — только для отладки |
MDBX_FORCE_ASSERTIONS |
Устаревшая опция (эквивалент MDBX_CHECKING=2); лучше использовать MDBX_CHECKING |
MDBX_ENABLE_BIGFOOT |
Цепочки GC-записей для очень больших освобождений; включена по умолчанию на 64-битных сборках |
MDBX_DEBUG |
Логирование/ассерты/аудит (0 по умолчанию) |
MDBX_VALIDATION |
Дополнительные структурные проверки при каждой операции |
MDBX_ENABLE_PGOP_STAT |
Счётчики операций со страницами (для диагностики) |
Совет: для production собирайте с
MDBX_CHECKING=0(или не указывайте вовсе) иNDEBUG. Отладочная сборка с ассертами может быть в разы медленнее и маскировать реальные тайминги.
2.4. Минимальный пример
Первый полный листинг учебника. Он открывает базу, записывает ключ и читает его.
#include <stdio.h>
#include <string.h>
#include <mdbx.h>
static void die(const char *what, int rc) {
fprintf(stderr, "%s: %s (%d)\n", what, mdbx_strerror(rc), rc);
exit(1);
}
int main(void) {
MDBX_env *env = NULL;
MDBX_txn *txn = NULL;
MDBX_dbi dbi;
MDBX_val key, data;
int rc;
/* 1. Создаём окружение */
rc = mdbx_env_create(&env);
if (rc != MDBX_SUCCESS) die("env_create", rc);
/* 2. Открываем (или создаём) файл базы; геометрия по умолчанию */
rc = mdbx_env_open(env, "./demo.mdbx",
MDBX_NOSUBDIR, 0664);
if (rc != MDBX_SUCCESS) die("env_open", rc);
/* 3. Транзакция чтения-записи */
rc = mdbx_txn_begin(env, NULL, MDBX_TXN_READWRITE, &txn);
if (rc != MDBX_SUCCESS) die("txn_begin", rc);
/* 4. Открываем главную таблицу (имя NULL = главная) */
rc = mdbx_dbi_open(txn, NULL, 0, &dbi);
if (rc != MDBX_SUCCESS) die("dbi_open", rc);
/* 5. Записываем "greeting" -> "hello, libmdbx" */
const char *k = "greeting";
const char *v = "hello, libmdbx";
key.iov_len = strlen(k);
key.iov_base = (void *)k;
data.iov_len = strlen(v);
data.iov_base = (void *)v;
rc = mdbx_put(txn, dbi, &key, &data, 0);
if (rc != MDBX_SUCCESS) die("put", rc);
/* 6. Коммитим */
rc = mdbx_txn_commit(txn);
if (rc != MDBX_SUCCESS) die("txn_commit", rc);
/* 7. Читаем в новой read-only транзакции */
rc = mdbx_txn_begin(env, NULL, MDBX_TXN_RDONLY, &txn);
if (rc != MDBX_SUCCESS) die("txn_begin-ro", rc);
rc = mdbx_get(txn, dbi, &key, &data);
if (rc != MDBX_SUCCESS) die("get", rc);
printf("Значение: %.*s\n", (int)data.iov_len, (char *)data.iov_base);
mdbx_txn_abort(txn);
mdbx_env_close(env);
return 0;
}
Разберём по шагам.
mdbx_env_createсоздаёт пустой объект окружения — «соединение» с будущей базой.mdbx_env_openоткрывает файл базы; создание управляется параметромmode: ненулевой0664означает «создать, если базы ещё нет». ФлагMDBX_CREATEсюда не передавайте — он дляmdbx_dbi_open(), а его бит совпадает сMDBX_NOMETASYNC.MDBX_NOSUBDIRозначает, что имя файла дано напрямую, а не как префикс для парыданные+блокировки.
Важно: бит
MDBX_CREATE=0x40000— это тот же бит, что иMDBX_NOMETASYNC. Если передатьMDBX_CREATEвflagsуmdbx_env_open(), БД создастся (из-за ненулевогоmode), но приложение молча получит ослабленную долговечность (NOMETASYNC). Создание включается только черезmode != 0.
- Все изменения происходят внутри транзакции — до коммита они не видны другим читателям.
mdbx_dbi_open(txn, NULL, 0, &dbi)открывает главную таблицу (подробно — глава 3).mdbx_putкладёт пару;mdbx_getчитает её.MDBX_val— структура{ iov_base, iov_len }: указатель и длина байтового окна.
Фрагмент из examples/c++/01-hello.c++ — тот же цикл open → put → get → close на C++ API: RAII-объекты (env_managed, транзакции) сами закрывают окружение, а ключи и значения передаются как mdbx::slice:
#include <iostream>
#include <string>
#include <mdbx.h++>
#include "common/common.h++"
int main() {
try {
const std::string path = std::string(example::tmpdir()) + "/mdbx-01-hello.mdbx";
mdbx::env::remove(path); // стереть остатки предыдущего запуска
// Создание: конструктор с create_parameters создаёт БД, если её нет.
mdbx::env_managed env(path, mdbx::env_managed::create_parameters(), mdbx::env::operate_parameters());
{
auto txn = env.start_write();
auto table = txn.open_map(nullptr); // главная таблица
txn.insert(table, mdbx::slice("key"), mdbx::slice("hello, libmdbx"));
txn.commit();
}
{
auto rtxn = env.start_read();
auto table = rtxn.open_map(nullptr);
std::cout << "got: " << rtxn.get(table, mdbx::slice("key")).as_string() << "\n";
}
Полный код: 01-hello.c++ · C-версия
2.5. Сборка и запуск тестов
В полном исходнике:
cmake -S . -B build -DMDBX_ENABLE_WERROR=ON
cmake --build build -j
ctest --test-dir build
Полезен стохастический стресс-тест mdbx_test с фиксированным --prng-seed — он воспроизводим и
помогает отлаживать редкие ошибки (подробно в Томе V, глава 30).
2.6. Нюанс: файлы данных и блокировок
Одна «база» физически состоит из двух файлов: данных (demo.mdbx) и блокировок (demo.mdbx-lck).
Файл блокировок содержит мьютекс писателя и таблицу читателей — это межпроцессная инфраструктура.
Не удаляйте .lck при открытой базе; при копировании базы помните про пару файлов (подробно —
Том III, глава 19).
Примеры к главе:
examples/c++/01-hello.c++· C-версия; сквозной проект:config-store-02.c++.
2.7. Резюме главы 2
- Амальгама (
mdbx.c+mdbx.h) или полный исходник с CMake. - Ключевые опции сборки:
MDBX_LOCKING,MDBX_CHECKING,MDBX_ENABLE_BIGFOOT. - Минимальный цикл: create → open → txn_begin → dbi_open → put/get → commit → close.
- База — пара файлов: данные + блокировки.
- Отладочные проверки включайте осознанно — они замедляют.
2.8. Упражнения
- Соберите минимальный пример и запустите дважды — убедитесь, что значение переживает перезапуск.
- Добавьте второй ключ и прочитайте оба в одной транзакции.
- Что произойдёт, если при первом запуске (базы ещё нет) передать
mode = 0?
2.9. Чек-лист главы 2
- [ ] я умею собирать библиотеку двумя способами: амальгамой (
mdbx.c+mdbx.h) и полным исходником с CMake; - [ ] знаю ключевые опции сборки (
MDBX_LOCKING,MDBX_CHECKING,MDBX_ENABLE_BIGFOOT) и почему для production нужныMDBX_CHECKING=0иNDEBUG; - [ ] могу собрать и запустить минимальный пример, убедившись, что значение переживает перезапуск;
- [ ] знаю минимальный цикл работы: create → open → txn_begin → dbi_open → put/get → commit → close;
- [ ] помню, что база — это пара файлов (данные +
.lck), и не удаляю файл блокировок при открытой базе.
2.10. Что дальше
Следующая глава — о том, что именно вы записываете и читаете: базовая модель данных libmdbx.
Мы разберём MDBX_val, главную и именованные таблицы (MDBX_dbi), лимиты, integer-ключи и
пустые ключи/значения. Эти понятия определяют схему сквозного проекта-конфигуратора: какие
таблицы создавать и что в них хранить.
Глава 3. Базовая модель данных
3.1. MDBX_val: ключ и значение как байтовые строки
Все данные в libmdbx — последовательности байтов. Ни тип, ни кодировка, ни структура не
интересуют движок. Окно на такие данные — MDBX_val:
typedef struct MDBX_val {
size_t iov_len; /* длина в байтах */
void *iov_base; /* указатель на данные */
} MDBX_val;
Важно: iov_base обычно указывает прямо в mmap-область базы. Поэтому:
- читать значение можно без копирования;
- но после завершения транзакции указатель может стать недействительным — скопируйте данные, если собираетесь их хранить дольше транзакции.
Нюанс (C++):
mdbx::sliceиmdbx::buffer. В C++ API рольMDBX_valиграют два типа.mdbx::slice— невладеющее представление (аналогstd::string_viewнадMDBX_val): он лишь ссылается на чужие байты и ничего не копирует. Конструктор изstd::stringсделанexplicitнеслучайно:mdbx::slice(std::to_string(k))прямо в аргументе вызова — безопасно, потому что временная строка живёт до конца полного выражения (то есть пока не завершится весь вызовinsert()/get(), который копирует байты внутрь базы), а вот сохранить такой срез в переменную и использовать позже — висячая ссылка.mdbx::buffer— наоборот, владеющий контейнер: по умолчанию он копирует содержимое (перегрузки сmake_reference=trueвозвращаются к поведениюslice), поэтомуmdbx::buffer(std::to_string(k))не накладывает требований на время жизни источника.
3.2. Главная таблица и именованные таблицы (dbi)
Внутри одного файла базы живут несколько деревьев. Каждое дерево — «таблица» (в терминах API —
MDBX_dbi, map / sub-DB). Есть:
- главная таблица — дескрипторы всех остальных таблиц (имя → корень дерева, флаги, счётчики);
- именованные таблицы — ваши данные;
- служебные (например, GC-дерево для свободных страниц — подробно в Томе III).
Открытие таблицы в транзакции:
/* главная таблица */
mdbx_dbi_open(txn, NULL, 0, &dbi_main);
/* именованная таблица (создаётся при первом открытии с MDBX_CREATE) */
mdbx_dbi_open(txn, "users", MDBX_CREATE, &dbi_users);
MDBX_dbi — это не глобальный объект, а привязка к таблице внутри конкретной транзакции:
хендл валиден там, где его открыли, и наследуется вложенными транзакциями. Использование хендла из
другой транзакции/потока — ошибка (MDBX_BAD_DBI).
Нюанс: повторное открытие таблицы. Для уже существующей таблицы
mdbx_dbi_open()требует передачи её постоянных флагов (MDBX_INTEGERKEY,MDBX_DUPSORT,MDBX_DUPFIXED, …); расхождение даётMDBX_INCOMPATIBLE. Если флаги заранее неизвестны (таблица могла быть создана другим кодом), откройте её с флагомMDBX_DB_ACCEDE: таблица откроется с фактическими флагами, а узнать их можно черезmdbx_dbi_flags(). Аналогичный флагMDBX_ACCEDEесть уmdbx_env_open()— он открывает базу, уже используемую другим процессом в неизвестном режиме, без ошибкиMDBX_INCOMPATIBLE(Том II, глава 9).
3.3. Ограничения
| Параметр | Значение |
|---|---|
| Ключ | до ~половины страницы (зависит от размера страницы и режима таблицы); integer-ключи — 8 байт |
| Значение | до MDBX_MAXDATASIZE ≈ 2 ГБ |
| Таблиц | MDBX_MAX_DBI = 32765 |
| Читателей | настраивается (mdbx_env_set_maxreaders), до MDBX_READERS_LIMIT = 32767 |
Размер страницы выбирается при создании базы: 256…65536 байт, по умолчанию 4096, и далее не изменяется. Выбор влияет на производительность (Том IV).
Актуальные пределы движок отдаёт через семейство mdbx_limits_* — они зависят от размера страницы
и сборки, поэтому вместо «зашитых» чисел используйте функции: mdbx_limits_dbsize_min(),
mdbx_limits_dbsize_max(), mdbx_limits_keysize_min(), mdbx_limits_keysize_max(),
mdbx_limits_valsize_min(), mdbx_limits_valsize_max(), mdbx_limits_pgsize_min(),
mdbx_limits_pgsize_max(), mdbx_limits_txnsize_max(), mdbx_limits_pairsize4page_max(),
mdbx_limits_valsize4page_max(). Удобные алиасы для отдельных значений —
mdbx_env_get_maxkeysize(_ex) и mdbx_env_get_maxvalsize_ex.
3.4. Integer-ключи и нативный порядок байт
Для числовых ключей используйте специальные типы: MDBX_INTEGERKEY (таблица с целочисленными
ключами uint32_t/uint64_t нативного порядка) и MDBX_INTEGERDUP (целочисленные
мультизначения). Нативный порядок байт означает: ключ 42 в памяти как uint64_t — без
перестановок байт. Это критично для сравнений и курсоров.
Нестандартный порядок сортировки задаётся компаратором — колбэком типа MDBX_cmp_func.
Кастомные компараторы ключей/значений передаются при открытии таблицы в расширенных вариантах
mdbx_dbi_open_ex()/mdbx_dbi_open_ex2() (аргументы keycmp/datacmp). Стандартные компараторы
бинарных ключей/значений — mdbx_cmp()/mdbx_dcmp(); выбрать нужный под конкретный набор флагов
таблицы можно через mdbx_get_keycmp(flags)/mdbx_get_datacmp(flags).
3.5. Нулевые ключи и значения (отличие от LMDB)
В отличие от LMDB, libmdbx позволяет пустые ключи (длина 0) и пустые значения (длина 0).
Это удобно для маркеров и флагов-наличия. Пустое значение — это iov_len == 0 (указатель может
быть NULL). Различайте «ключа нет» (MDBX_NOTFOUND) и «ключ есть с пустым значением»
(MDBX_SUCCESS при iov_len == 0).
Фрагмент из examples/c++/02-data-model.c++ — integer-ключи (key_mode::ordinal) и нулевое значение (mdbx::slice()):
// Главная таблица (name == nullptr): обычные ключи и значения.
auto main = txn.open_map(nullptr);
txn.insert(main, mdbx::slice("key"), mdbx::slice("value"));
std::cout << "main[key] = " << txn.get(main, mdbx::slice("key")).as_string() << "\n";
// Именованная таблица: отдельное пространство ключей внутри той же БД.
auto named = txn.create_map("named", mdbx::key_mode::usual, mdbx::value_mode::single);
txn.insert(named, mdbx::slice("config"), mdbx::slice("42"));
std::cout << "named[config] = " << txn.get(named, mdbx::slice("config")).as_string() << "\n";
// Integer-ключи: таблица с key_mode::ordinal (MDBX_INTEGERKEY),
// ключи — uint64_t в нативном порядке байт.
auto ord = txn.create_map("ordinal", mdbx::key_mode::ordinal, mdbx::value_mode::single);
const uint64_t k1 = 1, k2 = 1000;
txn.insert(ord, buffer::key_from_u64(k1), mdbx::slice("one"));
txn.insert(ord, buffer::key_from_u64(k2), mdbx::slice("thousand"));
std::cout << "ordinal[1000] = " << txn.get(ord, buffer::key_from_u64(k2)).as_string() << "\n";
// Пустой ключ (нулевой длины) и пустое значение.
txn.insert(main, mdbx::slice(), mdbx::slice("empty-key-value"));
txn.insert(named, mdbx::slice("empty"), mdbx::slice());
std::cout << "empty value length = " << txn.get(named, mdbx::slice("empty")).size() << "\n";
Полный код: 02-data-model.c++ · C-версия
Примеры к главе:
examples/c++/02-data-model.c++· C-версия.
3.6. Резюме главы 3
MDBX_val= байтовое окно{len, ptr}; указатель может жить в mmap.- Один файл — много таблиц (деревьев); главная таблица хранит дескрипторы.
MDBX_dbi— привязка к таблице в конкретной транзакции.- Лимиты: ключ ~½ страницы, значение ~2 ГБ, таблиц 32765.
- Integer-ключи — нативный порядок; пустые ключи и значения разрешены.
3.7. Упражнения
- Создайте две именованные таблицы («settings», «counters») и положите по ключу в каждую.
- Попробуйте прочитать ключ из хендла, открытого в другой транзакции — что вернёт API?
- Проверьте поведение с пустым значением: положите
iov_len=0, прочитайте — как отличить «пустое значение» от «ключа нет»?
3.8. Чек-лист главы 3
- [ ] я понимаю, что
MDBX_val— это байтовое окно{iov_len, iov_base}без типа и кодировки; - [ ] умею открывать главную и именованные таблицы через
mdbx_dbi_open()и знаю роль флагаMDBX_CREATE; - [ ] понимаю, что
MDBX_dbiвалиден только в своей транзакции, и помню проMDBX_BAD_DBIиMDBX_DB_ACCEDE; - [ ] проверяю лимиты через функции
mdbx_limits_*, а не через зашитые числа; - [ ] знаю, зачем
MDBX_INTEGERKEY(нативный порядок байт) и как отличить пустое значение (iov_len == 0) от отсутствия ключа (MDBX_NOTFOUND).
3.9. Что дальше
Теперь, когда модель данных ясна, можно перейти к операциям над ней. В главе 4 мы освоим базовый
набор CRUD — mdbx_put, mdbx_get, mdbx_del, mdbx_replace — с флагами записи, обработкой
ожидаемых кодов и полным примером. Именно эти операции лягут в основу cfg_set/cfg_get
сквозного конфигуратора.
Глава 4. Базовые операции CRUD
4.1. mdbx_put — вставка и обновление
int mdbx_put(MDBX_txn *txn, MDBX_dbi dbi,
const MDBX_val *key, const MDBX_val *data, unsigned flags);
Флаги:
| Флаг | Смысл |
|---|---|
0 / MDBX_UPSERT |
Вставить или заменить значение |
MDBX_NOOVERWRITE |
Вставить только если ключа нет; иначе MDBX_KEYEXIST |
MDBX_NODUPDATA |
(DUPSORT) не добавлять дубликат значения |
MDBX_CURRENT |
Обновить текущее значение курсора |
4.2. mdbx_get — чтение по ключу
int mdbx_get(MDBX_txn *txn, MDBX_dbi dbi,
const MDBX_val *key, MDBX_val *data);
Возвращает MDBX_SUCCESS (нашли) или MDBX_NOTFOUND (ключа нет). Для DUPSORT-таблиц нужен
MDBX_GET_BOTH (искать конкретное значение) — подробно в Томе II, глава 7.
Расширенный вариант mdbx_get_ex(txn, dbi, &key, &data, &values_count) дополнительно возвращает
число значений ключа (для обычной таблицы — 1; для DUPSORT — сколько значений связано с ключом;
удобно без отдельного курсора и mdbx_cursor_count()).
4.3. mdbx_del — удаление
int mdbx_del(MDBX_txn *txn, MDBX_dbi dbi,
const MDBX_val *key, const MDBX_val *data);
Если data == NULL — удаляется весь ключ. Если задан и таблица DUPSORT — удаляется конкретное
значение.
Фрагмент из examples/c++/03-crud.c++ — флаги записи на C++ API: insert соответствует MDBX_NOOVERWRITE (при повторе — исключение mdbx::key_exists), upsert — MDBX_UPSERT, update — MDBX_CURRENT. Флаги MDBX_NODUPDATA/MDBX_ALLDUPS для мультизначений — в Томе II, главы 6–7:
// NOOVERWRITE: вставка уникального ключа.
txn.insert(table, mdbx::slice("key"), mdbx::slice("value1"));
// Повторная вставка того же ключа -> key_exists (MDBX_KEYEXIST).
try {
txn.insert(table, mdbx::slice("key"), mdbx::slice("value2"));
std::cerr << "FAIL: duplicate insert did not throw\n";
return EXIT_FAILURE;
} catch (const mdbx::key_exists &) {
std::cout << "insert duplicate -> MDBX_KEYEXIST as expected\n";
}
// try_insert не бросает исключение, а сообщает результат флагом done.
auto ins = txn.try_insert(table, mdbx::slice("key"), mdbx::slice("value2"));
std::cout << "try_insert duplicate -> done=" << (ins.done ? 1 : 0) << "\n";
// UPSERT: вставить или перезаписать.
txn.upsert(table, mdbx::slice("key"), mdbx::slice("value2"));
std::cout << "after upsert: " << txn.get(table, mdbx::slice("key")).as_string() << "\n";
// CURRENT (update): обновить только существующий ключ.
txn.update(table, mdbx::slice("key"), mdbx::slice("value3"));
std::cout << "update existing: " << txn.get(table, mdbx::slice("key")).as_string() << "\n";
Полный код: 03-crud.c++ · C-версия
4.4. mdbx_replace — условная замена
mdbx_replace объединяет put+get в одну операцию: заменить значение, узнать старое, вставить
только если совпадает текущее. Используется для атомарных «сравни и замени» без лишних поисков.
int mdbx_replace(MDBX_txn *txn, MDBX_dbi dbi,
const MDBX_val *key, const MDBX_val *new_data,
const MDBX_val *old_data /* может быть NULL */,
MDBX_val *old_value /* может быть NULL */, unsigned flags);
4.5. Чтение: mdbx_get vs курсор
mdbx_get — точечный поиск по точному ключу. Если нужно обойти данные, найти диапазон, «ближайший
больший ключ» — используйте курсор (Том II, глава 6). Для точного чтения mdbx_get достаточно.
4.6. Полный пример: CRUD
#include <stdio.h>
#include <string.h>
#include <mdbx.h>
static void die(const char *what, int rc) {
fprintf(stderr, "%s: %s (%d)\n", what, mdbx_strerror(rc), rc);
exit(1);
}
int main(void) {
MDBX_env *env = NULL; MDBX_txn *txn = NULL; MDBX_dbi dbi;
MDBX_val key, data, out;
int rc;
rc = mdbx_env_create(&env);
if (rc) die("create", rc);
rc = mdbx_env_open(env, "./crud.mdbx", MDBX_NOSUBDIR, 0664);
if (rc) die("open", rc);
rc = mdbx_txn_begin(env, NULL, MDBX_TXN_READWRITE, &txn);
if (rc) die("txn", rc);
rc = mdbx_dbi_open(txn, "kv", MDBX_CREATE, &dbi);
if (rc) die("dbi", rc);
/* insert */
const char *k1 = "alpha", *v1 = "1";
key.iov_base = (void *)k1; key.iov_len = strlen(k1);
data.iov_base = (void *)v1; data.iov_len = strlen(v1);
rc = mdbx_put(txn, dbi, &key, &data, MDBX_NOOVERWRITE);
if (rc != MDBX_SUCCESS && rc != MDBX_KEYEXIST) die("put", rc);
/* update */
const char *v2 = "2";
data.iov_base = (void *)v2; data.iov_len = strlen(v2);
rc = mdbx_put(txn, dbi, &key, &data, 0); /* UPSERT */
if (rc) die("put2", rc);
/* get */
out.iov_base = NULL; out.iov_len = 0;
rc = mdbx_get(txn, dbi, &key, &out);
if (rc) die("get", rc);
printf("alpha = %.*s\n", (int)out.iov_len, (char *)out.iov_base);
/* replace: прочитать старое значение и записать новое */
MDBX_val old = {0, NULL};
const char *v3 = "3";
data.iov_base = (void *)v3; data.iov_len = strlen(v3);
rc = mdbx_replace(txn, dbi, &key, &data, NULL, &old, 0);
if (rc) die("replace", rc);
printf("было: %.*s\n", (int)old.iov_len, (char *)old.iov_base);
/* del */
rc = mdbx_del(txn, dbi, &key, NULL);
if (rc) die("del", rc);
/* проверим удаление */
rc = mdbx_get(txn, dbi, &key, &out);
printf("get после удаления: %s (%d)\n",
rc == MDBX_NOTFOUND ? "NOTFOUND (ожидаемо)" : "неожиданно", rc);
mdbx_txn_commit(txn);
mdbx_env_close(env);
return 0;
}
Внимание:
old_valueизmdbx_replaceиoutизmdbx_getуказывают в mmap. Не храните эти указатели послеcommit/abort— данные могут быть переиспользованы.
4.7. Обработка ошибок
Коды возврата: MDBX_SUCCESS (0) — успех; MDBX_RESULT_TRUE/MDBX_RESULT_FALSE — специальные
результаты некоторых операций; отрицательные MDBX_* — ошибки.
Частые «ожидаемые» коды (это не сбои, а штатные ответы):
MDBX_KEYEXIST— ключ уже есть (приMDBX_NOOVERWRITE);MDBX_NOTFOUND— ключ (или значение) не найден;MDBX_BAD_DBI— невалидный/закрытый хендл таблицы.
Строковое описание: mdbx_strerror(rc). Есть и потокобезопасный вариант mdbx_strerror_r() —
используйте его в многопоточном коде (Том II, глава 12).
Примеры к главе:
examples/c++/03-crud.c++· C-версия; сквозной проект:config-store-04.c++.
4.8. Резюме главы 4
- put/get/del/replace — базовые операции; флаги управляют поведением при конфликтах.
mdbx_get— точечное чтение; для обхода/диапазонов — курсоры.MDBX_KEYEXIST/MDBX_NOTFOUND— ожидаемые состояния, а не ошибки.mdbx_replace— атомарная замена «чтение старого + запись нового» одним вызовом.- Указатели из mmap валидны только внутри транзакции.
4.9. Упражнения
- Добавьте обработку «expected error»: вставьте ключ повторно с
NOOVERWRITEи обработайтеMDBX_KEYEXISTбез выхода из программы. - Напишите цикл, который вставляет 10 000 ключей в одной транзакции и измеряет время.
- Что делает
mdbx_delсdata-аргументом в обычной (не DUPSORT) таблице?
4.10. Чек-лист главы 4
- [ ] я умею выполнять все базовые операции:
mdbx_put,mdbx_get,mdbx_delиmdbx_replace; - [ ] понимаю различие флагов записи
MDBX_NOOVERWRITE,MDBX_UPSERT,MDBX_CURRENTи выбираю их осознанно; - [ ] обрабатываю
MDBX_KEYEXISTиMDBX_NOTFOUNDкак штатные состояния, а не сбои; - [ ] понимаю, что
mdbx_replace— атомарное «сравнить и заменить» одним вызовом; - [ ] помню, что указатели из mmap (
out,old_value) нельзя использовать после commit/abort.
4.11. Что дальше
Одиночные операции мы уже умеем, но настоящая сила libmdbx — в транзакциях. Глава 5 вводит begin/commit/abort, различие read-only и read-write транзакций и интуицию MVCC — почему читатели не блокируют писателя. Для конфигуратора это фундамент: атомарная запись группы настроек и безопасная параллельная работа с базой.
Глава 5. Транзакции — первые шаги
5.1. Что такое транзакция и зачем она нужна
Транзакция — это группа операций, которая выполняется атомарно и изолированно:
- либо все изменения применяются, либо ни одно (атомарность);
- пока транзакция не закоммичена, её изменения не видны другим (изоляция).
Пример: перевод денег — списать со счёта A и зачислить на счёт B. Если это сделать двумя отдельными операциями, между ними можно потерять данные при сбое. В транзакции — безопасно.
5.2. begin / commit / abort
int mdbx_txn_begin(MDBX_env *env, MDBX_txn *parent,
unsigned flags, MDBX_txn **txn);
int mdbx_txn_commit(MDBX_txn *txn);
int mdbx_txn_abort(MDBX_txn *txn);
mdbx_txn_begin(env, NULL, flags, &txn)— начать транзакцию.flags == 0(то же, чтоMDBX_TXN_READWRITE) — читающая-пишущая; для транзакции только на чтение передавайтеMDBX_TXN_RDONLY.mdbx_txn_commit(txn)— применить изменения (для write) или просто закрыть (для read).mdbx_txn_abort(txn)— отбросить изменения и закрыть.
Совет: всегда закрывайте транзакцию — либо commit, либо abort. Незакрытая read-транзакция может «заморозить» переработку страниц (Том III, глава 16).
5.3. Read-only vs read-write
Read-only транзакции не блокируют друг друга и не требуют мьютекса писателя. Их можно иметь много одновременно, в любых потоках.
Read-write транзакция в любой момент времени в базе одна (глобальный мьютекс писателя). Если два потока попытаются начать пишущую транзакцию одновременно — один дождётся другого.
5.4. Почему читатели не блокируют друг друга (MVCC интуитивно)
libmdbx использует MVCC (multiversion concurrency control). Упрощённо:
- Каждая транзакция видит «снапшот» базы — состояние на момент её начала.
- Пишущая транзакция не изменяет существующие страницы, а создаёт новые версии (Copy-on-Write).
- Читатель, начавшийся раньше, продолжает видеть старые версии, пока жив его снапшот.
Поэтому читатели никогда не конфликтуют ни друг с другом, ни с писателем. Расплата — старые версии страниц занимают место, пока живы старые снапшоты (подробно — Том III, главы 14 и 16).
Фрагмент из examples/c++/04-transactions.c++ — read-only наблюдатель в отдельном потоке видит стабильный MVCC-снапшот, пока пишущая транзакция перезаписывает ключ и затем делает abort (откат):
{
auto txn = env.start_write();
auto table = txn.open_map(nullptr);
txn.insert(table, mdbx::slice("key"), mdbx::slice("v1"));
txn.commit();
}
// Read-only наблюдатель в отдельном потоке: фиксированный снапшот базы.
std::thread observer(observer_run, std::ref(env));
while (stage.load() != 1)
std::this_thread::yield();
// Пишущая транзакция перезаписывает ключ, но ещё не коммитит.
{
auto wtxn = env.start_write();
auto table = wtxn.open_map(nullptr);
wtxn.upsert(table, mdbx::slice("key"), mdbx::slice("v2"));
stage = 2; // наблюдатель проверяет снапшот при незакоммиченной записи
while (stage.load() != 3)
std::this_thread::yield();
wtxn.abort(); // откат: v2 не сохраняется
}
Полный код: 04-transactions.c++ · C-версия
5.5. Базовое правило: одна транзакция — один поток
Транзакция привязана к потоку, который её создал («sticky thread»). Использовать объект транзакции
из другого потока нельзя — это нарушение дисциплины (MDBX_THREAD_MISMATCH). Впрочем, есть режим
MDBX_NOSTICKYTHREADS для пулов потоков и корутин (Том II, глава 11).
5.6. Полный пример с транзакциями
#include <stdio.h>
#include <string.h>
#include <mdbx.h>
static void die(const char *what, int rc) {
fprintf(stderr, "%s: %s (%d)\n", what, mdbx_strerror(rc), rc);
exit(1);
}
int main(void) {
MDBX_env *env = NULL; MDBX_txn *txn = NULL; MDBX_dbi dbi;
MDBX_val key, data, out;
int rc;
rc = mdbx_env_create(&env);
if (rc) die("create", rc);
rc = mdbx_env_open(env, "./txn.mdbx", MDBX_NOSUBDIR, 0664);
if (rc) die("open", rc);
/* ---- Write-транзакция: два ключа атомарно ---- */
rc = mdbx_txn_begin(env, NULL, MDBX_TXN_READWRITE, &txn);
if (rc) die("txn_w", rc);
rc = mdbx_dbi_open(txn, "kv", MDBX_CREATE, &dbi);
if (rc) die("dbi", rc);
const char *ka = "a", *va = "1";
const char *kb = "b", *vb = "2";
key.iov_base = (void *)ka; key.iov_len = strlen(ka);
data.iov_base = (void *)va; data.iov_len = strlen(va);
mdbx_put(txn, dbi, &key, &data, 0);
key.iov_base = (void *)kb; key.iov_len = strlen(kb);
data.iov_base = (void *)vb; data.iov_len = strlen(vb);
mdbx_put(txn, dbi, &key, &data, 0);
rc = mdbx_txn_commit(txn);
if (rc) die("commit", rc);
/* ---- Read-only транзакция ---- */
rc = mdbx_txn_begin(env, NULL, MDBX_TXN_RDONLY, &txn);
if (rc) die("txn_r", rc);
out.iov_base = NULL; out.iov_len = 0;
key.iov_base = (void *)ka; key.iov_len = strlen(ka);
rc = mdbx_get(txn, dbi, &key, &out);
if (rc) die("get", rc);
printf("a = %.*s\n", (int)out.iov_len, (char *)out.iov_base);
mdbx_txn_abort(txn); /* read-only: просто закрываем */
/* ---- Abort: изменения отбрасываются ---- */
rc = mdbx_txn_begin(env, NULL, MDBX_TXN_READWRITE, &txn);
if (rc) die("txn_w2", rc);
const char *kc = "c", *vc = "3";
key.iov_base = (void *)kc; key.iov_len = strlen(kc);
data.iov_base = (void *)vc; data.iov_len = strlen(vc);
mdbx_put(txn, dbi, &key, &data, 0);
mdbx_txn_abort(txn); /* c не появится в базе */
/* проверяем, что "c" нет */
rc = mdbx_txn_begin(env, NULL, MDBX_TXN_RDONLY, &txn);
if (rc) die("txn_r2", rc);
rc = mdbx_get(txn, dbi, &key, &out);
printf("после abort: %s\n", rc == MDBX_NOTFOUND ? "ключа нет (верно)" : "неожиданно");
mdbx_txn_abort(txn);
mdbx_env_close(env);
return 0;
}
Продолжить чтение после коммита —
mdbx_txn_commit_embark_read(). Коммит в libmdbx «посаживает» транзакцию на свежий снапшот (детент — удержание читателя на снимке состояния, механизм раскрыт в томе III); специальная функцияmdbx_txn_commit_embark_read()делает то же и сразу возвращает новую read-only транзакцию на уже закоммиченном состоянии — без разрыва «коммит → новый begin». Полезно для паттернов read-your-writes и обработки очередей (Том V, глава 32). Идентификатор транзакции (порядковый номер в базе) можно узнать черезmdbx_txn_id().
5.7. Сквозной проект: начало
Наш конфигуратор приложений. Идея: хранить настройки в виде таблиц «секция → ключ → значение».
/* config_store.h — каркас сквозного проекта (главы 2–5) */
#ifndef CONFIG_STORE_H
#define CONFIG_STORE_H
#include <mdbx.h>
typedef struct {
MDBX_env *env;
MDBX_dbi main;
} config_store_t;
int cfg_open(config_store_t *cs, const char *path);
int cfg_set(config_store_t *cs, const char *key, const char *value);
int cfg_get(config_store_t *cs, const char *key, char *buf, size_t buflen);
int cfg_close(config_store_t *cs);
#endif
Реализация — следующими главами; к концу Тома II это будет полноценное приложение с индексами и многопоточностью.
Примеры к главе:
examples/c++/04-transactions.c++· C-версия; сквозной проект:config-store-05.c++.
5.8. Резюме главы 5
- Транзакция = атомарная, изолированная группа операций.
- begin (0/
MDBX_TXN_READWRITE— пишущая;MDBX_TXN_RDONLY— только чтение), commit, abort. - MVCC: читатели видят снапшот и не блокируют писателя.
- Одна транзакция — один поток (по умолчанию).
- Незакрытые read-транзакции «морозят» переработку страниц.
5.9. Упражнения
- Напишите функцию
cfg_set, которая используетMDBX_NOOVERWRITEдля создания и0для обновления — как их объединить в одну транзакцию? - Измерьте: сколько write-транзакций в секунду выдерживает ваш диск (без батчинга)?
- Почему начинать read-write транзакцию «на всякий случай» — плохая идея (подсказка: мьютекс писателя)?
5.10. Чек-лист главы 5
- [ ] я понимаю свойства транзакции: атомарность (всё или ничего) и изоляцию;
- [ ] умею начинать и корректно закрывать транзакции всех видов:
MDBX_TXN_READWRITE,MDBX_TXN_RDONLY,mdbx_txn_abort(); - [ ] понимаю, почему пишущая транзакция всегда одна, и как MVCC позволяет читателям не блокировать писателя;
- [ ] знаю правило «одна транзакция — один поток» и код
MDBX_THREAD_MISMATCH; - [ ] умею показать на примере, что abort отбрасывает изменения, а незакрытая read-транзакция мешает переработке страниц.
5.11. Что дальше
На этом Том I завершён: у вас есть работающий каркас конфигуратора. Том II доводит его до полноценного приложения: курсоры для обхода и диапазонов (глава 6), DUPSORT и вторичные индексы (главы 7–8), конфигурация окружения (глава 9), режимы долговечности (глава 10), многопоточность (глава 11) и обработка ошибок (глава 12). Начинаем с курсоров — без них не сделать ни обхода таблицы, ни поиска по диапазону.
Итог тома
Вы познакомились с базовой моделью libmdbx: что это, как собрать, как открыть базу, выполнить CRUD в транзакциях. У вас работает сквозной проект-«конфигуратор».
Что дальше: Том II — курсоры, мультизначения и DUPSORT, вторичные индексы, конфигурация окружения, режимы долговечности, многопоточность и обработка ошибок.