BoltDB

Материал из Национальной библиотеки им. Н. Э. Баумана
Последнее изменение этой страницы: 17:20, 17 января 2019.
BoltDB
BoltDB logo.png
Разработчики: Ben Johnson (@benbjohnson)
Выпущена: 2013
Постоянный выпуск: v1.2.1
Состояние разработки: Active
Написана на: Go (язык программирования)
Операционная система: FreeBSD, Linux, OS X, Solaris, Windows
Локализация: English
Тип ПО: Встраиваемая СУБД
Лицензия: MIT License
Веб-сайт github.com/boltdb/bolt

BoltDB - это простое встраиваемое хранилище, построенное по принципу ключ/значение, полностью написанное на языке программирования Go (язык программирования), что в значительной степени упрощает его поддержку. Изначально целью проекта было создать простое, быстрое и надежное хранилище, которое может быть использовано в проектах, которые не требуют полноценного сервера базы данных такого как PostgreSQL или MySQL.

BoltDB для хранения данных использует один файл, который блокируется на время использования приложением. При этом файл данных может спокойно достигать размера в 1 ТБ. В качестве структуры хранения данных используется B+ дерево. Поэтому BoltDB быстрее чем RocksDB/LevelDB (построенных на основе LSM-деревьев) в случае чтения, но значительно медленнее, когда дело касается большого количества операций записи.

Обзор

В BoltDB структурой данных выступает B+ дерево. Эффективность их строения и малая занимаемая площадь обеспечивают хорошую производительность записи данных. BoltDB рассматривает память компьютера как одно адресное пространство, разделяемое несколькими процессами или потоками, используя разделяемую память с семантикой копирования на запись (исторически известной как одноуровневое хранилище). Новые данные записываются без перезаписи или перемещения существующих данных. Это гарантирует целостность и надежность данных без использования журналов транзакций или служб очистки. Семантика копирования при записи помогает обеспечить целостность данных, а также обеспечивает транзакционные гарантии и одновременный доступ, не требуя блокировки. Новые страницы памяти, необходимые для внутренней модификации данных, выделяются семантикой копирования при записи базовой ОС: сама библиотека BoltDB никогда не модифицирует старые данные, к которым обращаются за чтением, потому что она просто не может этого сделать: любые обновления в общей памяти автоматически создают полностью независимую копию страницы памяти, которая изменяется. Поскольку BoltDB отображается в память, он может возвращать прямые указатели на адреса памяти ключей и значений через свой API, тем самым избегая ненужного и дорогого копирования памяти. Это приводит к значительному повышению производительности (особенно когда хранимые значения чрезвычайно велики) и расширяет возможности использования BoltDB.

BoltDB также отслеживает неиспользуемые страницы памяти, используя B+ дерево. Благодаря отслеживанию неиспользуемых страниц, потребность в сборке мусора (и фазе сбора мусора, которая будет потреблять процессорное время) полностью исключается. Транзакция, которые нуждаются в новых страницах, сначала получают страницы из списка неиспользуемых страниц. В современной файловой системе с разреженной поддержкой файлов это помогает минимизировать фактическое использование диска. Как и LMDB (Lightning Memory-Mapped Database), и в отличие от LevelDB, BoltDB поддерживает транзакции ACID. В отличие от SQLite, у него нет языка запросов, и его гораздо проще использовать для обычных целей. Кроме того, время загрузки Bolt лучше, особенно во время восстановления после сбоя, поскольку ему не нужно читать журнал (у него его нет), чтобы найти последнюю успешную транзакцию: он просто читает идентификаторы для двух корней дерева B+, и использует имеющий больший ID.[Источник 1]

Архитектура

Для хранения данных в BoltDB используются buckets. Bucket - это просто именованная коллекция пар ключ-значение, как и структура map в Go (язык программирования). Имя сегмента, ключи и значения имеют тип byte[]. Сегменты могут содержать другие сегменты, также снабженные ключом byte[].

BoltDB - это в основном множество вложенных map. И эта простота делает его таким простым в использовании. Там нет таблиц для настройки, нет схем, нет сложного языка запросов.[Источник 2]

Простейшая пара ключ-значение в BoltDB:

key := []byte(hello)
value := []byte(Hello World!)

Данная база данных не поддерживает репликацию типа Master-Master и Master-Slave.

Начало работы

Для начала работы с хранилищем установим библиотеку командой:

go get github.com/boltdb/bolt

Для начала работы приложение должно открыть хранилище BoltDB:

package main

import (
	"log"

	"github.com/boltdb/bolt"
)

func main() {
	// Open the my.db data file in your current directory.
	// It will be created if it doesn't exist.
	db, err := bolt.Open("my.db", 0600, nil)
	if err != nil {
		log.Fatal(err)
	}
	defer db.Close()

	...
}

В коде выше открывается хранилище my.db, ему выставляются права доступа 0600. Если файл хранилища не существует, то он будет создан. Важным моментом является то, что только одно приложение может работать с файлом хранилища, поэтому для избежания deadlock-а можно задать время, ожидания хранилища:

db, err := bolt.Open("my.db", 0600, &bolt.Options{Timeout: 1 * time.Second})

Примеры работы

BoltDB поддерживает транзакции. Одновременно может выполнятся одна read-write транзакция и несколько read-only транзакций. Read/write транзакция инициализируется при помощи функции db.Update следующим образом:

err := db.Update(func(tx *bolt.Tx) error {
	...
	return nil
})

Read-only транзакция инициализируется при помощи db.View, как показано ниже:

err := db.View(func(tx *bolt.Tx) error {
	...
	return nil
})

Разработчики рекомендуют работать с транзакциями через вызовы db.View/db.Update. Но бывают ситуации, когда нужно управлять транзакцией вручную, это тоже возможно:

// Начало read-write транзакции
tx, err := db.Begin(true)
if err != nil {
    return err
}
defer tx.Rollback()

// Работаем внутри транзакции
_, err := tx.CreateBucket([]byte("MyBucket"))
if err != nil {
    return err
}

// Commit транзакции
if err := tx.Commit(); err != nil {
    return err
}

BoltDB хранит все данные в коллекциях Bucket, каждая из которых представляет собой множество ключ-значение. Важно отметить, что в разных коллекциях могут быть одинаковые ключи, которые имеют разные значения. Для созданий коллекции используется метод CreateBucket или CreateBuckerIfNotExists. Пример создания коллекции ниже:

db.Update(func(tx *bolt.Tx) error {
	b, err := tx.CreateBucket([]byte("MyBucket"))
	if err != nil {
		return fmt.Errorf("create bucket: %s", err)
	}
	return nil
})

Для удаления коллекции существует метод Tx.DeleteBucket, а для получения существующей Tx.Bucket:

db.View(func(tx *bolt.Tx) error {
	b := tx.Bucket([]byte("MyBucket"))
	v := b.Get([]byte("answer"))
	fmt.Printf("The answer is: %s\n", v)
	return nil
})

Как говорилось выше, все ключи хранятся в коллекция, поэтому доступ к значениям ключей также осуществляется через них, что видно из примера выше b.Get. Если в коллекции такого ключа нет, то возвращается значение nil. Для добавления ключа в коллекцию используется метод Put:

db.Update(func(tx *bolt.Tx) error {
	b := tx.Bucket([]byte("MyBucket"))
	err := b.Put([]byte("answer"), []byte("42"))
	return err
})

Исходя из того, что данные хранятся в B+дереве, все ключи отсортированы, поэтому последовательный проход по всем значениям нересурсоемкая операция. Для этого используются курсоры, которые позволяют переходить на первый элемент (First), последний (Last), на указанное значение (Seek), предыдущее (Prev) и следующее за текущим (Next). Ниже приведен пример прохода по всем ключам коллекции:

db.View(func(tx *bolt.Tx) error {
	// Assume bucket exists and has keys
	b := tx.Bucket([]byte("MyBucket"))

	c := b.Cursor()

	for k, v := c.First(); k != nil; k, v = c.Next() {
		fmt.Printf("key=%s, value=%s\n", k, v)
	}

	return nil
})

Немного адаптировав предыдущий пример при помощи метода Seek и пакета bytes, можно извлечь все ключи начинающиеся с заданного префикса:

db.View(func(tx *bolt.Tx) error {
	// Assume bucket exists and has keys
	c := tx.Bucket([]byte("MyBucket")).Cursor()

	prefix := []byte("1234")
	for k, v := c.Seek(prefix); k != nil && bytes.HasPrefix(k, prefix); k, v = c.Next() {
		fmt.Printf("key=%s, value=%s\n", k, v)
	}

	return nil
})

Путем замены метода bytes.HasPrefix на bytes.Compare можно организовать вывод всех ключей в диапазоне от min до max:

db.View(func(tx *bolt.Tx) error {
	// Assume our events bucket exists and has RFC3339 encoded time keys.
	c := tx.Bucket([]byte("Events")).Cursor()

	// Our time range spans the 90's decade.
	min := []byte("1990-01-01T00:00:00Z")
	max := []byte("2000-01-01T00:00:00Z")

	// Iterate over the 90's.
	for k, v := c.Seek(min); k != nil && bytes.Compare(k, max) <= 0; k, v = c.Next() {
		fmt.Printf("%s: %s\n", k, v)
	}

	return nil
})

Для прохода по всем ключам и значениям в рамках конкретной коллекции вы можете воспользоваться методом ForEach:

db.View(func(tx *bolt.Tx) error {
	// Assume bucket exists and has keys
	b := tx.Bucket([]byte("MyBucket"))

	b.ForEach(func(k, v []byte) error {
		fmt.Printf("key=%s, value=%s\n", k, v)
		return nil
	})
	return nil
})

Установка

В представленном ролике можно найти процесс установки BoltDB и пример работы с данной базой данных .

Предостережения и ограничения

Вот несколько вещей, на которые следует обратить внимание при использовании BoltDB:

  • Высока производительность чтения при интенсивных рабочих нагрузках. Производительность последовательной записи также высока, но случайная запись может быть медленной. Вы можете использовать DB.Batch для решения этой проблемы.
  • Bolt использует внутреннее B+ дерево, поэтому возможен произвольный доступ к страницам памяти. SSD обеспечивают значительное повышение производительности по сравнению с HDD.
  • Старайтесь избегать длительных транзакций чтения. Bolt использует копирование при записи, поэтому старые страницы не могут быть восстановлены, пока они используются старой транзакцией.
  • Bolt использует блокировку записи в файл базы данных, поэтому он не может использоваться несколькими процессами.
  • Будьте осторожны при использовании Bucket.FillPercent. Установка высокого процента заполнения для блоков, в которых есть случайные вставки, приведет к очень плохому использованию страницы памяти в вашей базе данных.
  • Массовая загрузка большого количества случайных записей в новый buckets может быть медленной, поскольку страница не будет разделяться, пока транзакция не будет зафиксирована. Случайное добавление более 100 000 пар ключ-значение в один новый buckets в одной транзакции не рекомендуется.
  • Bolt использует файл с отображением в память, поэтому основная операционная система обрабатывает кеширование данных. Как правило, ОС кэширует в памяти столько файлов, сколько может, и освобождает память по мере необходимости для других процессов. Это означает, что Bolt может показывать очень высокое использование памяти при работе с большими базами данных. Однако это ожидаемо, и ОС освободит память по мере необходимости. Bolt может работать с базами данных, намного превышающими объем доступной физической памяти, при условии, что его память помещается в виртуальное адресное пространство процесса. Но это может быть проблематично в 32-битных системах.[Источник 3]

Источники

  1. Intro to BoltDB // Progville [2015-2018]. Дата обновления: 19.11.2015‎. URL: https://www.progville.com/go/bolt-embedded-db-golang/ (дата обращения 28.12.2018)
  2. FaunaDB Documentation // Fauna [2014-2018]. Дата обновления: 30.07.2014‎. URL: https://npf.io/2014/07/intro-to-boltdb-painless-performant-persistence/ (дата обращения 28.12.2018)
  3. BoltDB // GitHub [2013-2018]. Дата обновления: 02.03.2018‎. URL: https://github.com/boltdb/bolt (дата обращения 28.12.2018)