Том V. Экспертные темы и edge cases
Уровень: для продвинутых пользователей. Цель тома: вы знаете тонкости и грабли, умеете отлаживать проблемы, принимать архитектурные решения, мигрировать с LMDB и строить устойчивые паттерны.
Глава 27. Правила и советы из базы знаний
Полный перечень правил и советов — в базе знаний (knowledge-base/rules-and-checklist/,
knowledge-base/tips/). Здесь — группировка по темам с акцентами критичности.
27.1. Транзакции и потоки (критично)
- Одна транзакция — один поток. Передача объекта транзакции другому потоку — UB/ошибка
(
MDBX_THREAD_MISMATCH). - Не передавайте транзакцию между потоками даже с
NOSTICKYTHREADS, если другой поток может синхронно вызвать write-функцию среды — deadlock. - Не используйте объекты транзакций из двух потоков одновременно.
- После
commit/abortуказатели на данные в mmap недействительны.
27.2. Жизненный цикл и целостность
- Не открывайте БД дважды в одном процессе;
fork()— только сresurrect_after_forkв наследнике. - Незакрытая read-транзакция «морозит» переработку страниц — закрывайте/паркуйте.
- Никогда не копируйте файл базы «на лету» — используйте
mdbx_copy. MDBX_UTTERLY_NOSYNC— только для некритичных данных.
27.3. Пространство и рост
- Задавайте геометрию один раз до open;
upperне занижать. - Уменьшать
rp_augment_limit— только вместе сgc_time_limit. MDBX_ENABLE_REFUND=0в production не использовать (отладочная опция).- Длинные значения: увеличивайте страницу (до 64 КБ), дробите записи.
27.4. Операции
- Удаления делайте до вставок в той же транзакции; массовые удаления —
bunch_delete/delete_range. - Не стройте
std::mapповерх БД ради упорядоченности — выигрыш обычно меньше затрат. - Для DUPSORT-удаления при итерации — два курсора.
27.5. Типичные контрпримеры
- Нарушение «одна транзакция — один поток» → случайные
BAD_RSLOT/пересечения. - Открытие БД дважды → гонки и повреждение регистраций.
- Забытый читатель → рост файла до
MDBX_MAP_FULL. - Отладочная сборка в проде → кратные замедления.
Фрагмент из examples/c++/34-rules-counter.c++ — контрпример «забытого» read-снапшота: поток открыл снапшот (env.start_read()) и держит его (abort() только по внешнему флагу); пока читатель жив, писатель упирается в MDBX_MAP_FULL (GC заморожен, файл растёт):
std::atomic<bool> reader_ready{false};
std::atomic<bool> release_reader{false};
std::thread holder([&] {
auto rtxn = env.start_read();
auto table = rtxn.open_map(nullptr);
(void)rtxn.get(table, mdbx::slice("k0"));
reader_ready = true;
while (!release_reader.load())
std::this_thread::yield();
rtxn.abort();
});
while (!reader_ready.load())
std::this_thread::yield();
Полный код: 34-rules-counter.c++.
Примеры к главе:
examples/c++/34-rules-counter.c++.
27.6. Резюме главы 27
- Одна транзакция — один поток: передача транзакции между потоками — UB/ошибка (
MDBX_THREAD_MISMATCH), даже сNOSTICKYTHREADSвозможен deadlock. - Незакрытая read-транзакция «морозит» переработку страниц — файл растёт до
MDBX_MAP_FULL; закрывайте/паркуйте. - Не открывайте БД дважды в одном процессе;
fork()— только сresurrect_after_forkв наследнике; копирование — толькоmdbx_copy. MDBX_UTTERLY_NOSYNCи отладочные опции (MDBX_ENABLE_REFUND=0) в production не использовать.- Геометрию задавайте один раз до open,
upperне занижать; длинные значения — крупнее страница (до 64 КБ) или дробите записи. - Удаления — до вставок; массовые удаления —
bunch_delete/delete_range; DUPSORT-удаление при итерации — два курсора. - Типичные контрпримеры: нарушение потоковой дисциплины (
BAD_RSLOT), двойное открытие, забытый читатель, отладочная сборка в проде.
27.7. Чек-лист главы 27
- [ ] Убедиться, что транзакции не передаются между потоками и не используются из двух потоков одновременно.
- [ ] Закрывать/парковать read-транзакции сразу после использования.
- [ ] Копировать файл базы только через
mdbx_copy. - [ ] Задать геометрию до open и не занижать
upper. - [ ] Исключить
MDBX_UTTERLY_NOSYNCиMDBX_ENABLE_REFUND=0из production-конфигурации. - [ ] Выполнять удаления до вставок; массовые удаления — через
bunch_delete/delete_range. - [ ] Для DUPSORT-удаления при итерации использовать два курсора.
Глава 28. Платформенные нюансы
28.1. Linux
boot_idучитывается при откате слабых мет; в LXC — общий/отсутствует./dev/shm(tmpfs): создание с запасом;ENOSPCотfallocate()можно игнорировать; с 0.13.8 fallocate защищает от SIGBUS.- Page cache: некогерентность unified page cache (#269) —
MDBX_FORCE_CHECK_MMAP_COHERENCY. mincore/madvise(MADV_NOHUGEPAGE)— отказ от THP.- Linux < 4.x:
mdbx_env_create()→MDBX_INCOMPATIBLE.
28.2. Windows
LockFileExмедленнее именованных мьютексов; мелкие транзакции дороги.- 32-бит: лимиты адресного пространства;
/LARGEADDRESSAWARE(+1 ГБ кMAX_MAPSIZE32). - Сжатие файла — только в однопроцессном сценарии; расширение через Native API; изменение геометрии приостанавливает потоки (SRWL — Windows Slim Reader/Writer Lock, тонкозернистая блокировка Windows).
- WSL1:
ENOLCK— работа принципиально невозможна.
28.3. macOS / iOS
fcntl(F_FULLFSYNC)по умолчанию (максимум долговечности);MDBX_APPLE_SPEED_INSTEADOF_DURABILITY— жертва durability ради скорости.- SysV-семафоры по умолчанию (flavour
SYSV). fcntl(F_PREALLOCATE)резервирует в конец — БД растёт вдвое быстрее штатного.
28.4. Android / bionic
- TLS и атомики отличаются от glibc; 32-бит Android — возможное зависание при откате коммита (исторически; с 0.11.7 проблем нет).
28.5. Контейнеры
- LXC: boot_id; Docker — PID uniqueness.
- tmpfs: ENOSPC от fallocate (игнорировать).
- NFS/CIFS/SMB: только эксклюзивный режим или кооперативный read-only; копирование работает.
28.6. Wine
- Не реализованы механизмы динамического изменения размера БД — задавайте фиксированный
достаточный размер;
mdbx_module_handler(...)при запуске для статической линковки.
Фрагмент из examples/c++/35-platform-notes.c++ — платформенная оговорка в коде: «абсурдный» запрос геометрии обрабатывается через try/catch, потому что на tmpfs/WSL1/32-битной Windows ENOSPC/ENOLCK — штатная реакция, а не фатальный сбой:
// Проверка границ геометрии не обязана падать: запрашиваем «абсурдную»
// верхнюю границу и аккуратно обрабатываем результат.
mdbx::env::geometry probe;
probe.size_upper = intptr_t(8) * mdbx::env::geometry::TB;
try {
env.set_geometry(probe);
std::cout << "geometry probe: accepted\n";
} catch (const std::exception &ex) {
// На tmpfs/WSL1/32-битной Windows это может завершиться ошибкой —
// для примера важно, что она перехвачена, а не уронила процесс.
std::cout << "geometry probe: rejected (" << ex.what() << ")\n";
}
Полный код: 35-platform-notes.c++.
Примеры к главе:
examples/c++/35-platform-notes.c++.
28.7. Резюме главы 28
- Платформенные различия касаются блокировок, синхронизации и размеров адресного пространства.
- LXC/boot_id и page-cache — специфика контейнеров/Linux.
- Windows LockFileEx и F_FULLFSYNC на macOS — главные платформенные «грабли».
28.8. Чек-лист главы 28
- [ ] Linux: учесть
boot_id(LXC), tmpfsENOSPCот fallocate, при необходимости отказаться от THP. - [ ] Windows: помнить о дорогих мелких транзакциях и лимитах 32-бит (
/LARGEADDRESSAWARE). - [ ] macOS: осознанно выбрать между
fcntl(F_FULLFSYNC)иMDBX_APPLE_SPEED_INSTEADOF_DURABILITY. - [ ] Android/bionic: проверить поведение на 32-бит (историческое зависание при откате коммита — до 0.11.7).
- [ ] Контейнеры: LXC — boot_id, Docker — PID uniqueness; NFS/CIFS/SMB — только эксклюзивный или кооперативный read-only.
- [ ] Wine: задать фиксированный достаточный размер БД (динамическое изменение размера не работает).
- [ ] Обрабатывать
ENOSPC/ENOLCKна tmpfs/WSL1/32-бит Windows как штатные ошибки, а не фатальные сбои.
Глава 29. Handle-Slow-Readers (HSR)
29.1. Что такое HSR и зачем он нужен
Когда база заполнена из-за долгих читателей (GC заморожен), библиотека вызывает колбэк HSR — единственный штатный способ приложения повлиять на исход конфликта «долгий читатель против писателя».
29.2. Сигнатура и параметры
typedef int (*MDBX_hsr_func)(const MDBX_env *env, const MDBX_txn *txn,
mdbx_pid_t pid, mdbx_tid_t tid,
uint64_t laggard, unsigned gap,
size_t space, int retry);
laggard— отставание проблемного читателя;gap— число попыток;space— объём, который освободится после завершения читателя;retry— счётчик повторных вызовов.
29.3. Возвращаемые значения
| Значение | Действие |
|---|---|
0 |
Колбэк подождал/решил; libmdbx пересканирует RLT и повторяет |
1 |
Читательская транзакция прервана асинхронно; слот очистить немедленно |
2+ |
Процесс-читатель убит; libmdbx сбрасывает его регистрацию |
Вызывается только когда база заполнена из-за читателей.
29.4. Типовые реализации
- Логирование + возврат 0 (ждать).
- Мягкое завершение: пометить читателю флаг, вернуть 1.
- Принудительное: убить процесс читателя, вернуть 2.
- Согласие на рост: если позволяет геометрия — увеличить
upperи вернуть 0.
29.5. HSR и SAFE_NOSYNC
В SAFE_NOSYNC «квази-долгий читатель» — это steady-коммит, а не живой слот: HSR на него не
влияет. Управлять ростом нужно авто-sync (syncbytes/syncperiod).
Фрагмент из examples/c++/36-hsr.c++ — HSR-колбэк: сообщает о проблемном читателе (pid/lag/space), просит его освободить снапшот и ждёт фактического освобождения, после чего возвращает MDBX_RESULT_TRUE. Регистрация — env.set_HandleSlowReaders(hsr_callback); вместе с SAFE_NOSYNC колбэк остаётся главным средством против долгих читателей:
// Колбэк вызывается, когда БД «упирается» в предел из-за читателей,
// удерживающих старые снапшоты. Здесь: сообщаем о событии, просим читателя
// освободить снапшот и ждём его, после чего возвращаем MDBX_RESULT_TRUE
// («проблема устранена» — писатель продолжит).
int hsr_callback(const MDBX_env *, const MDBX_txn *, mdbx_pid_t pid, mdbx_tid_t tid, uint64_t laggard,
unsigned gap, size_t space, int retry) noexcept {
std::cout << "hsr invoked: laggard pid=" << pid << " tid=" << tid << " lag=" << laggard << " gap=" << gap
<< " space=" << space << " retry=" << retry << "\n";
hsr_called = true;
hsr_release_reader = true;
while (!reader_released.load())
std::this_thread::yield(); // дождаться фактического освобождения
return MDBX_RESULT_TRUE;
}
Полный код: 36-hsr.c++.
29.6. Пример реализации
Фрагмент (C, иллюстрация): полная компилируемая версия — в
examples/c++/36-hsr.c++.
static int my_hsr(const MDBX_env *env, const MDBX_txn *txn,
mdbx_pid_t pid, mdbx_tid_t tid,
uint64_t laggard, unsigned gap,
size_t space, int retry) {
fprintf(stderr, "HSR: reader pid=%d lag=%llu space=%zu retry=%d\n",
(int)pid, (unsigned long long)laggard, space, retry);
if (retry > 3)
return 2; /* убить проблемного читателя */
return 0; /* ждём ещё */
}
/* установка */
mdbx_env_set_hsr(env, my_hsr);
Примеры к главе:
examples/c++/36-hsr.c++.
29.7. Резюме главы 29
- HSR — колбэк разрешения конфликта с долгими читателями.
- Параметры: laggard/gap/space/retry; возвраты 0/1/2+.
- Типовые стратегии: ждать/убить/расти.
- С SAFE_NOSYNC управляйте ростом через авто-sync.
29.8. Чек-лист главы 29
- [ ] Установить HSR-колбэк (
mdbx_env_set_hsr/set_HandleSlowReaders). - [ ] Определить политику по
retry: логирование, мягкое завершение, принудительное убийство, согласие на рост. - [ ] Использовать параметры
laggard/gap/space/retryдля принятия решения в колбэке. - [ ] Помнить семантику возвратов: 0 — ждать, 1 — прервать читателя, 2+ — убить процесс-читатель.
- [ ] При
SAFE_NOSYNCнастроить авто-sync (syncbytes/syncperiod) — HSR на steady-коммит не влияет.
Глава 30. Диагностика и отладка
30.1. Инструменты
mdbx_chk — проверка целостности. Ключевые опции:
| Опция | Что делает |
|---|---|
-v…-vvvvv |
Подробность вывода |
-q |
Полная тишина |
-c |
Кооперативный (не эксклюзивный) режим; полная проверка — только эксклюзивный |
-w |
Read-write: откат к steady, проверка txnid мет |
-d |
Постраничный обход B+tree; без него нельзя найти lost/double-used страницы |
-i |
Игнор ложных ошибок порядка (кастомные компараторы) |
-s table |
Проверить только таблицу |
-0/-1/-2 |
Конкретная мета; -t/-T — переключиться на неё |
Exit-код: 0 = ошибок нет.
MDBX_ENABLE_PROFGC — профиль GC в commit_latency.gc_prof. Ключевые поля:
max_reader_lag, max_retained_pages, work_rtime_monotonic/work_xtime_cpu,
work_rsteps/work_xpages, work_majflt, self_*, wloops, flushes, kicks.
MDBX_commit_latency — стадии коммита: preparation, gc_wallclock, audit, write, sync,
ending, whole, gc_cputime.
mdbx_txn_info() — txn_reader_lag, txn_space_used/limit_soft/limit_hard/retired/leftover/dirty.
Для долгих читателей важны txn_space_retired и txn_space_leftover.
Отладочная сборка — MDBX_DEBUG/MDBX_CHECKING (−1..3); MDBX_FORCE_ASSERTIONS deprecated.
Санитайзеры: ENABLE_ASAN/ENABLE_UBSAN/ENABLE_MEMCHECK; TSAN в CMake нет (вручную).
Фрагмент из examples/c++/37-diagnostics.c++ — чтение mdbx_txn_info (через txn.get_info(...): id/лаг/использованное/dirty) и списка читателей (mdbx_reader_list, здесь env.enumerate_readers(...)) — те самые данные для алгоритмов «рост БД» и MDBX_MAP_FULL выше:
// Информация о текущей пишущей транзакции.
const auto info = txn.get_info(true /* scan_rlt */);
std::cout << "txn_info: id=" << info.txn_id << " lag=" << info.txn_reader_lag << " used=" << info.txn_space_used
<< " dirty=" << info.txn_space_dirty << "\n";
txn.commit();
}
// Список читателей (RLT).
struct visitor {
int operator()(const mdbx::env::reader_info &ri, int) {
std::cout << "reader slot=" << ri.slot << " pid=" << ri.pid << " tid=" << ri.thread
<< " txnid=" << ri.transaction_id << " lag=" << ri.transaction_lag << "\n";
return mdbx::continue_loop;
}
} v;
env.enumerate_readers(v);
Полный код: 37-diagnostics.c++.
30.2. Пошаговый алгоритм: рост БД
mdbx_env_info_ex()— меты и геометрия (size_nowvssize_upper).mdbx_reader_list()/mdbx_reader_check()— кто держит снапшоты.mdbx_stat -r— retained.- PROFGC:
max_reader_lag,max_retained_pages,kicks. mdbx_stat -p—newlyvscow(новые страницы вместо переиспользования).- Решение: парковка/HSR/steady-point/дефрагментация; при
SAFE_NOSYNC— авто-sync.
30.3. Пошаговый алгоритм: тормоз коммита
mdbx_txn_commit_ex()— разложитьwholeна стадии.syncдоминирует → режим долговечности; слишком частые fsync → батч илиNOMETASYNC/SAFE_NOSYNC.gc_wallclockвелик → PROFGC:work_rsteps/work_xpages/work_majflt(фрагментация/большие значения).writeвелик при малых данных → спилл рано (dp_limit) или дублирование грязных страниц.- WRITEMAP + page-fault'ы →
prefault_write_enable. mdbx_stat -p— msync/fsync счётчики.
30.4. Пошаговый алгоритм: MDBX_MAP_FULL
- Abort текущей write-транзакции (продолжение после resize →
BAD_TXN). mdbx_txn_info()—txn_space_limit_hardvs used.mdbx_reader_list()— старые снапшоты пинят пространство.- PROFGC —
kicks,max_reader_lag,max_retained_pages. - Проверить HSR (
mdbx_env_set_hsr), парковку, геометрию (upperзанижен?). - Решение: HSR (ждать/убить/расти), выселение припаркованных, новый steady, увеличение
upperдо open, дефрагментация.
30.5. Пошаговый алгоритм: deadlock / MDBX_BUSY
- Проверить дисциплину потоков: коды
THREAD_MISMATCH/TXN_OVERLAPPING/BAD_RSLOT. - При
NOSTICKYTHREADS— исключить синхронные write-функции (set_option/set_flags/set_geometry/ sync/stat/defrag/close) из потока, не владеющего пишущей транзакцией. - Аудит
fork()(resurrect) и повторныхenv_open. mdbx_reader_list()покажет чужих владельцев.
30.6. Пошаговый алгоритм: повреждение БД
mdbx_chk -w -vvv— локализовать (мета? дерево? GC? порядок?).- Проверить среду:
boot_id(LXC?), некогерентность page cache (#269,incoherence). - Повреждена одна мета →
mdbx_chk -1/-2+-T(переключиться на валидную). MDBX_WANNA_RECOVERY→ read-write илиmdbx_env_open_for_recovery()(с 0.12.7 не изменяет базу).- Recovery-проверки безопасны; восстановление из бэкапа (
mdbx_copy).
30.7. Пошаговый алгоритм: утечка слотов читателей
mdbx_reader_check(env, &dead)— сколько мёртвых слотов.- Причина: потоки завершились без очистки (TLS-деструктор; glibc #21031/#21032; DSO-выгрузка).
- Решение: явная
mdbx_thread_register/unregister; паттернreset+renew;resurrect_after_fork.
30.8. Чек-листы перед production-деплоем
- [ ] Геометрия задана до open (
upperадекватен; движок сам выбирает дефолт ≈ золотое сечение ОЗУ). - [ ] HSR-колбэк установлен.
- [ ] Режим синхронизации осознанно выбран.
- [ ] Release-сборка без ассертов.
- [ ]
maxreadersпокрывает число потоков. - [ ] Долгие чтения используют парковку/reset+renew.
- [ ] Тест после сбоя: kill -9 + открытие +
mdbx_chk.
Примеры к главе:
examples/c++/37-diagnostics.c++.
30.9. Резюме главы 30
- Инструменты: chk, PROFGC, commit_latency, txn_info, reader_check, отладочная сборка.
- 6 пошаговых алгоритмов: рост, тормоз коммита, MAP_FULL, deadlock, повреждение, утечка слотов.
- Чек-лист перед деплоем обязателен.
30.10. Чек-лист главы 30
- [ ] Освоить
mdbx_chk(опции-d,-w,-0/-1/-2/-T) как основной инструмент целостности. - [ ] Диагностировать рост БД по алгоритму §30.2 (
mdbx_reader_list,mdbx_stat -r/-p, PROFGC). - [ ] Тормоз коммита раскладывать по стадиям
MDBX_commit_latency(mdbx_txn_commit_ex) — §30.3. - [ ] При
MDBX_MAP_FULL— abort транзакции, проверка читателей, HSR и геометрии — §30.4. - [ ] Deadlock/
MDBX_BUSY— аудит потоковой дисциплины иNOSTICKYTHREADS— §30.5. - [ ] Повреждение БД —
mdbx_chk -w -vvv, восстановление из бэкапа (mdbx_copy) — §30.6. - [ ] Утечку слотов читателей проверять через
mdbx_reader_check— §30.7. - [ ] Пройти чек-лист production-деплоя §30.8 (включая kill -9 + открытие +
mdbx_chk).
Глава 31. Миграция с LMDB
31.1. Зачем мигрировать
libmdbx — переработанный LMDB: больше режимов долговечности, GC вместо free-list, жёстче гарантии восстановления, выше производительность записи при батчинге. Формат данных не совместим с LMDB: тройка мета-страниц, двухфазный коммит, контрольные суммы и GC-дерево — собственные изменения; файлы LMDB напрямую не открываются, миграция выполняется переносом данных.
31.2. Совместимость форматов
- Файлы LMDB не открываются libmdbx напрямую: форматы разные (тройка мета-страниц, двухфазный коммит, контрольные суммы, GC-дерево).
- Между собой совместимы БД libmdbx 0.11.x ↔ 0.12.x (формат заморожен с v11.3).
- Миграция с LMDB — перенос данных (
mdbx_dump/mdbx_load,mdbx_copy), а не переименование файла.
31.3. Breaking changes
MDBX_NOLOCKубран.MDBX_NOTLS→MDBX_NOSTICKYTHREADS.- Другие переименования и новые требования к флагам — сверяйте с
mdbx.h.
31.4. Таблица соответствия mdb_ → mdbx_
| LMDB | libmdbx |
|---|---|
mdb_env_create |
mdbx_env_create |
mdb_env_open |
mdbx_env_open |
mdb_txn_begin |
mdbx_txn_begin |
mdb_dbi_open |
mdbx_dbi_open |
mdb_put/get/del |
mdbx_put/get/del |
mdb_cursor_get |
mdbx_cursor_get |
MDB_NOTLS |
MDBX_NOSTICKYTHREADS |
Фрагмент из examples/c/38-migration.c — карта соответствия mdb_* → mdbx_* из заголовка примера:
// Карта макросов/имён при переносе кода с LMDB (полная таблица — в §31.4):
// mdb_env_create → mdbx_env_create
// mdb_env_open → mdbx_env_open
// mdb_env_close → mdbx_env_close
// mdb_env_set_mapsize → mdbx_env_set_geometry (другой API!)
// mdb_txn_begin → mdbx_txn_begin
// mdb_txn_commit → mdbx_txn_commit
// mdb_txn_abort → mdbx_txn_abort
// mdb_dbi_open → mdbx_dbi_open
// mdb_get/put/del → mdbx_get/mdbx_put/mdbx_del
// mdb_cursor_open/get → mdbx_cursor_open/mdbx_cursor_get
// MDB_NOTLS → MDBX_NOSTICKYTHREADS
// MDB_NOSYNC → MDBX_SAFE_NOSYNC или MDBX_UTTERLY_NOSYNC
// MDB_APPEND → MDBX_APPEND
// MDB_INTEGERKEY → MDBX_INTEGERKEY
// mdb_strerror → mdbx_strerror
Полный код: 38-migration.c.
31.5. Поведенческие отличия
- Три меты vs две; двухфазный коммит.
- Контрольные суммы страниц.
- Больше диагностических кодов (
WANNA_RECOVERY,MVCC_RETARDEDи др.). - Пустые ключи/значения разрешены.
31.6. Чек-лист миграции
- Сделайте бэкап (
mdbx_copy -cили dump+load). - Откройте базу в read-only — проверьте
mdbx_chk. - Замените вызовы по таблице соответствия.
- Прогоните тесты под ASAN/UBSAN.
- Настройте геометрию/режимы под новый движок (не переносите вслепую LMDB-настройки).
31.7. Резюме главы 31
- Формат данных libmdbx не совместим с LMDB: тройка мета-страниц, двухфазный коммит, контрольные суммы, GC-дерево; файлы LMDB напрямую не открываются.
- Миграция — перенос данных (
mdbx_dump/mdbx_load,mdbx_copy), а не переименование файла; между собой совместимы БД 0.11.x ↔ 0.12.x (формат заморожен с v11.3). - Breaking changes:
MDBX_NOLOCKубран,MDBX_NOTLS→MDBX_NOSTICKYTHREADS; остальные переименования сверяйте сmdbx.h. - Имена функций меняются предсказуемо (
mdb_env_create→mdbx_env_create), ноmdb_env_set_mapsize→mdbx_env_set_geometry— другой API. - Поведенческие отличия: три меты vs две, контрольные суммы страниц, больше диагностических кодов, разрешены пустые ключи/значения.
- Порядок миграции: бэкап → read-only проверка
mdbx_chk→ замена вызовов → тесты под ASAN/UBSAN → настройка геометрии/режимов заново.
31.8. Чек-лист главы 31
- [ ] Сделать бэкап (
mdbx_copy -cили dump+load) до любых изменений. - [ ] Открыть базу в read-only и проверить её
mdbx_chk. - [ ] Заменить вызовы по таблице соответствия §31.4, включая
mdb_env_set_mapsize→mdbx_env_set_geometry. - [ ] Заменить флаги:
MDBX_NOLOCKубран,MDBX_NOTLS→MDBX_NOSTICKYTHREADS. - [ ] Прогнать тесты под ASAN/UBSAN.
- [ ] Настроить геометрию и режимы под libmdbx заново, не перенося LMDB-настройки вслепую.
Примеры к главе:
examples/c/38-migration.c(карта соответствия mdb_ → mdbx_ — в заголовке файла).
Глава 32. Паттерны проектирования на libmdbx
32.1. Паттерн 1: Key-value с автоинкрементным ID
Для монотонных ID есть и встроенный механизм — mdbx_dbi_sequence() (атомарный счётчик таблицы),
и классический переносимый подход — счётчик в служебной таблице. Пример ниже демонстрирует второй
вариант: он одинаково работает во всех версиях libmdbx и не зависит от MDBX_LIFORECLAIM-тонкостей.
Фрагмент из examples/c++/39-pattern-sequence-id.c++ — автоинкрементный ID: счётчик лежит в таблице meta и инкрементируется в той же пишущей транзакции, что и вставка записи:
uint64_t next_id(mdbx::txn_managed &txn, const mdbx::map_handle &meta) {
constexpr auto counter_key = mdbx::slice("seq");
uint64_t current = 0;
try {
current = txn.get(meta, counter_key).as_uint64();
} catch (const mdbx::not_found &) {
current = 0;
}
++current;
txn.upsert(meta, counter_key, mdbx::slice::wrap(current));
return current;
}
Полный код: 39-pattern-sequence-id.c++.
32.2. Паттерн 2: Вторичный индекс (DUPSORT)
Основная таблица + индекс «поле → список ID» (Том II, глава 8). Обновлять в одной транзакции.
Фрагмент из examples/c++/40-pattern-secondary-index.c++ — вторичный индекс «поле → список ID»: основная таблица users и индекс by_role (DUPSORT, value_mode::multi) обновляются в одной транзакции:
auto txn = env.start_write();
// Основная таблица: id → {name, role}.
auto users = txn.create_map("users", mdbx::key_mode::ordinal, mdbx::value_mode::single);
// Индекс: role → список id (мультизначения).
auto by_role = txn.create_map("by_role", mdbx::key_mode::usual, mdbx::value_mode::multi);
struct rec {
uint64_t id;
const char *name;
const char *role;
};
static const rec records[] = {{1, "alice", "admin"}, {2, "bob", "dev"}, {3, "carol", "admin"}};
for (const auto &r : records) {
txn.upsert(users, mdbx::slice::wrap(r.id), mdbx::slice(std::string(r.name) + "|" + r.role));
txn.upsert(by_role, mdbx::slice(r.role), mdbx::slice::wrap(r.id));
}
txn.commit();
Полный код: 40-pattern-secondary-index.c++.
32.3. Паттерн 3: Составной ключ
Конкатенация полей + компаратор (или big-endian для чисел). Поиск по префиксу — SET_RANGE.
Фрагмент из examples/c++/41-pattern-composite-key.c++ — составной ключ: поля фиксированной ширины упаковываются в big-endian, при котором лексикографический порядок совпадает с числовым (диапазонный поиск — SET_RANGE):
uint64_t pack(uint16_t year, uint16_t month) {
const uint64_t value = (uint64_t(year) << 48) | (uint64_t(month) << 32);
#if defined(__BYTE_ORDER__) && __BYTE_ORDER__ == __ORDER_LITTLE_ENDIAN__
return __builtin_bswap64(value);
#else
return value;
#endif
}
uint16_t decode_year(const mdbx::slice &key) {
const auto *p = static_cast<const uint8_t *>(key.data());
return uint16_t((uint16_t(p[0]) << 8) | p[1]);
}
uint16_t decode_month(const mdbx::slice &key) {
const auto *p = static_cast<const uint8_t *>(key.data());
return uint16_t((uint16_t(p[2]) << 8) | p[3]);
}
Полный код: 41-pattern-composite-key.c++.
32.4. Паттерн 4: Очередь задач (table-as-queue)
MDBX_DUPSORT + sequence: ключ — приоритет/номер, значение — задача. Естественный порядок выдачи.
Фрагмент из examples/c++/42-pattern-queue.c++ — очередь задач: ключ — монотонный номер (ordinal), потребитель забирает первый элемент курсором и удаляет его:
// Потребитель: забирает первый элемент через курсор и удаляет.
auto txn = env.start_write();
auto queue = txn.open_map("queue", mdbx::key_mode::ordinal, mdbx::value_mode::single);
std::string dequeued;
auto cur = txn.open_cursor(queue);
while (true) {
auto r = cur.to_first(false);
if (!r)
break;
const auto id = r.key.as_uint64();
const auto task = r.value.as_string();
dequeued += std::to_string(id) + ":" + std::string(task) + " ";
cur.erase(false);
}
txn.commit();
std::cout << "dequeued: " << dequeued << "\n";
Полный код: 42-pattern-queue.c++.
32.5. Паттерн 5: Кольцевой буфер
Ограниченный размер: при превышении удаляем самые старые ключи (курсор + del).
Фрагмент из examples/c++/43-pattern-ring-buffer.c++ — кольцевой буфер последних N: фиксированные слоты 0..N-1, запись перезаписывает counter % N:
// Пишем 12 элементов в буфер на 5 слотов.
{
auto txn = env.start_write();
auto ring = txn.create_map("ring", mdbx::key_mode::ordinal, mdbx::value_mode::single);
for (unsigned i = 0; i < 12; ++i) {
const unsigned slot = i % kSlots;
txn.upsert(ring, buffer::key_from_u64(slot), mdbx::slice("s" + std::to_string(i)));
}
txn.commit();
}
Полный код: 43-pattern-ring-buffer.c++.
32.6. Паттерн 6: Полносканирующий итератор
Курсор + get_batch/bunch_delete для массовой обработки.
Фрагмент из examples/c++/44-pattern-full-scan.c++ — полное сканирование курсором to_first() → to_next() с фильтром:
auto rtxn = env.start_read();
auto table = rtxn.open_map(nullptr);
auto cur = rtxn.open_cursor(table);
// Полное сканирование с фильтром «чётные ключи».
size_t scanned = 0, matched = 0;
for (auto r = cur.to_first(); r; r = cur.to_next(false)) {
++scanned;
if (r.key.as_string().size() % 2 == 0)
++matched;
}
std::cout << "scanned " << scanned << " entries\n";
Полный код: 44-pattern-full-scan.c++.
32.7. Паттерн 7: Репликация через per-table txnid
Маркеры изменений: хранить номер транзакции последнего изменения; вторичный процесс опрашивает и копирует дельты.
Фрагмент из examples/c++/45-pattern-replication.c++ — репликация через per-table txnid: значение хранит версию (data|N), реплика читает только записи новее своего last_seen:
// Реплика читает всё новее своего последнего txnid.
auto replicate = [&](uint64_t last_seen) -> size_t {
auto rtxn = env.start_read();
auto table = rtxn.open_map(nullptr);
size_t n = 0;
auto cur = rtxn.open_cursor(table);
for (auto r = cur.to_first(); r; r = cur.to_next(false)) {
if (version_of(r.value) > last_seen)
++n;
}
rtxn.abort();
return n;
};
Полный код: 45-pattern-replication.c++.
32.8. Паттерн 8: Read-your-writes через clone / embark_read
mdbx_txn_clone/embark_read — собственные записи видны немедленно в той же логической
операции без отдельной транзакции.
Фрагмент из examples/c++/46-pattern-read-your-writes.c++ — read-your-writes: та же пишущая транзакция немедленно читает собственное изменение; после commit оно видно и свежему читателю:
// Пишущая транзакция сразу читает собственное изменение.
{
auto txn = env.start_write();
auto table = txn.open_map(nullptr);
txn.insert(table, mdbx::slice("key"), mdbx::slice("v1"));
std::cout << "within txn sees: " << txn.get(table, mdbx::slice("key")).as_string() << "\n";
txn.commit();
}
// После коммита изменение видно и свежему читателю.
{
auto rtxn = env.start_read();
auto table = rtxn.open_map(nullptr);
std::cout << "after commit via fresh txn sees: " << rtxn.get(table, mdbx::slice("key")).as_string() << "\n";
rtxn.abort();
}
Полный код: 46-pattern-read-your-writes.c++.
Каждый паттерн: постановка задачи → решение с кодом → компромиссы → альтернативы (в полной версии
базы знаний — knowledge-base/).
32.9. Резюме главы 32
- Автоинкрементный ID: счётчик в служебной таблице, инкрементируется в той же write-транзакции, что и вставка; альтернатива — встроенный
mdbx_dbi_sequence(). - Вторичный индекс (DUPSORT) «поле → список ID» обновляется в одной транзакции с основной таблицей.
- Составной ключ: поля фиксированной ширины в big-endian (лексикографический порядок = числовой), диапазонный поиск —
SET_RANGE. - Очередь задач: ordinal-ключ (монотонный номер), потребитель забирает первый элемент курсором и удаляет.
- Кольцевой буфер: фиксированные слоты
0..N-1, запись перезаписываетcounter % N. - Полное сканирование — курсор
to_first()→to_next(), массовая обработка —get_batch/bunch_delete. - Репликация через per-table txnid (маркер версии в значении) и read-your-writes через
mdbx_txn_clone/embark_read.
32.10. Чек-лист главы 32
- [ ] Для монотонных ID выбрать
mdbx_dbi_sequence()или счётчик в служебной таблице (переносимый вариант). - [ ] Вторичный индекс (DUPSORT,
value_mode::multi) обновлять в той же транзакции, что и основную запись. - [ ] Для составных ключей использовать big-endian/компаратор и диапазонный поиск
SET_RANGE. - [ ] Очередь задач реализовать через ordinal-ключ и удаление первого элемента курсором.
- [ ] Для ограниченных структур (кольцевой буфер) удалять самые старые записи при переполнении.
- [ ] Массовые выборки/удаления выполнять через
get_batch/bunch_delete. - [ ] Репликацию строить на маркерах txnid, реплику — на read-only снапшоте с фильтром по
last_seen.
Примеры к главе:
examples/c++/39-pattern-sequence-id.c++;examples/c++/40-pattern-secondary-index.c++;examples/c++/41-pattern-composite-key.c++;examples/c++/42-pattern-queue.c++;examples/c++/43-pattern-ring-buffer.c++;examples/c++/44-pattern-full-scan.c++;examples/c++/45-pattern-replication.c++;examples/c++/46-pattern-read-your-writes.c++.
Глава 33. Roadmap и будущее: MithrilDB
33.1. Текущее состояние
libmdbx на 2026 год — зрелый движок (0.15.x devel-канон), формат БД заморожен с 2018 года. Фундаментальные улучшения GC/freelist планируются только в следующем поколении.
33.2. MithrilDB
Общий API для нескольких форматов хранения; амальгамация упрощает распространение.
33.3. Репликация
Предпосылки: подписка на изменения, ранняя очистка GC (2025), нелинейная обработка GC (сквозное отслеживание использования страниц читаемыми снапшотами).
33.4. Прочее в roadmap
- Подписка на изменения (почтовые ящики, кольцевые буферы).
- Шифрование и сжатие на уровне движка.
- Потоковые BLOB.
- SWIG и кроссязыковое взаимодействие.
33.5. Резюме главы 33
- libmdbx на 2026 год — зрелый движок (0.15.x devel-канон); формат БД заморожен с 2018 года.
- Фундаментальные улучшения GC/freelist планируются только в следующем поколении — MithrilDB: общий API для нескольких форматов хранения, амальгамация упрощает распространение.
- Предпосылки репликации: подписка на изменения, ранняя очистка GC (2025), нелинейная обработка GC (сквозное отслеживание использования страниц читаемыми снапшотами).
- Прочее в roadmap: шифрование и сжатие на уровне движка, потоковые BLOB, SWIG и кроссязыковое взаимодействие.
33.6. Чек-лист главы 33
- [ ] При долгосрочном планировании учитывать, что формат БД заморожен с 2018 года (совместимость 0.11.x ↔ 0.12.x).
- [ ] Не ждать фундаментальных улучшений GC/freelist в текущем поколении — следить за MithrilDB.
- [ ] Для репликации опираться на доступные предпосылки (подписка на изменения, ранняя очистка GC).
- [ ] Оценивать roadmap-фичи (шифрование, сжатие, потоковые BLOB, SWIG) как будущие, а не текущие возможности.
Итог тома
Вы знаете правила и советы, платформенные нюансы, HSR, диагностику по проверенным алгоритмам, миграцию с LMDB и паттерны проектирования.
Что дальше: Том VI — привязки (bindings): как использовать libmdbx из Rust, Go, Python, Node.js, .NET, C++, Dart, Nim, Java, Haskell, Ruby, Scala.