Blot
Bolt — это чистая база данных ключей/значений Go, вдохновленная проектом Говарда Чу LMDB (https://symas.com/lmdb/technical/). Цель этого проекта — предоставить простую, быструю и надежную базу данных для проектов, которым не требуется полноценный сервер базы данных, такой как Postgres или MySQL.
Поскольку Bolt используется как такая функция низкого уровня, простота является ключевым моментом. API будет небольшим и сосредоточится только на получении и установке значений.
статус проекта
Блот стабилен, исправлен API и исправлен формат файла. Обеспечьте непротиворечивость базы данных и безопасность потоков благодаря полному покрытию модульными тестами и выборочному тестированию методом «черного ящика». Блоты в настоящее время используются в производственных средах с высокой нагрузкой до 1 ТБ. Многие компании, такие как Shopify и Heroku, используют Bolt каждый день для предоставления своих услуг.
A message from the author
Первоначальная цель Bolt состояла в том, чтобы предоставить простое хранилище ключей/значений на чистом Go без лишних функций кода. В этом плане проект удался. Однако этот ограниченный объем также означает, что проект завершен.
Поддержание базы данных с открытым исходным кодом требует много времени и усилий. Изменения в коде могут иметь неожиданные или даже катастрофические последствия, поэтому даже простые изменения требуют часов тщательного тестирования и проверки.
К сожалению, у меня больше нет ни времени, ни сил продолжать эту работу. Блот находится в стабильном состоянии и имеет многолетнюю успешную производственную эксплуатацию. Поэтому я считаю, что оставить его в его нынешнем состоянии будет наиболее разумно.
Если вы заинтересованы в использовании более функциональной версии Bolt, я предлагаю вам взглянуть на ответвление CoreOS под названием bbolt.
Getting Started
Установить
Чтобы использовать Blot, сначала установите среду go, а затем выполните следующую команду:
$ go get github.com/boltdb/bolt/...
скопировать код
Эта команда извлечет библиотеку и установит исполняемый файл Blot по пути $GOBIN.
Открыть БлотБД
Объект верхнего уровня в Bolt — это БД. Он представлен как один файл на диске, представляющий собой непротиворечивый снимок данных.
Чтобы открыть базу данных, просто используйтеbolt.Open()
функция:
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()
...
}
скопировать код
Пожалуйста, обрати внимание:Bolt заблокирует файл данных, поэтому несколько процессов не смогут открыть одну и ту же базу данных одновременно. Открытие уже открытой базы данных Bolt приведет к ее зависанию до тех пор, пока ее не закроет другой процесс. Чтобы предотвратить бесконечное ожидание, вы можете передать опцию тайм-аутаOpen()
функция:
db, err := bolt.Open("my.db", 0600, &bolt.Options{Timeout: 1 * time.Second})
скопировать код
дела
Bolt допускает одновременно только одну транзакцию чтения-записи, но одновременно несколько транзакций только чтения. Каждая транзакция имеет согласованное представление данных.
Отдельные транзакции и все созданные из них объекты (например, сегмент, ключ) не являются потокобезопасными. Чтобы обрабатывать данные в нескольких горутинах, вы должны запустить транзакцию для каждой горутины или использовать блокировки, чтобы гарантировать, что только одна горутина имеет доступ к транзакции в каждый момент времени. Создание транзакций из БД является потокобезопасным.
Транзакции только для чтения и транзакции чтения-записи не должны зависеть друг от друга и, как правило, не должны открываться одновременно в одной и той же процедуре. Это может привести к взаимоблокировкам, поскольку транзакциям чтения-записи необходимо периодически переназначать файлы данных, но только тогда, когда открыты транзакции только для чтения.
чтение и запись транзакций
Чтобы начать транзакцию чтения-записи, вы можете использоватьDB.Update()
функция:
err := db.Update(func(tx *bolt.Tx) error {
...
return nil
})
скопировать код
Внутри закрытия у вас есть согласованное представление базы данных. Вы завершаете транзакцию, возвращая ноль. Вы также можете в любой момент откатить транзакцию, вернув ошибку. Все операции с базой данных разрешены в рамках транзакции чтения-записи.
Всегда проверяйте наличие ошибок возврата, так как он сообщит о любых сбоях диска, которые могут привести к тому, что ваша транзакция не будет завершена. Если вы вернете ошибку в замыкании, она будет передана.
транзакция только для чтения
Чтобы начать транзакцию только для чтения, вы можете использоватьDB.View()
функция:
err := db.View(func(tx *bolt.Tx) error {
...
return nil
})
скопировать код
Вы также можете получить согласованное представление базы данных в этом закрытии, однако операции мутации не разрешены в транзакциях только для чтения. Вы можете только получить хранилище, получить значения или реплицировать базу данных в транзакции только для чтения.
Массовое чтение и запись транзакций
каждыйDB.Update()
Ожидание, пока диск зафиксирует запись. Комбинируя несколько обновлений сDB.Batch()
Связывание функций может минимизировать эти накладные расходы:
err := db.Batch(func(tx *bolt.Tx) error {
...
return nil
})
скопировать код
Параллельные пакетные вызовы могут быть объединены в более крупные транзакции. Пакетная обработка полезна только при наличии нескольких вызовов горутины.
Пакет может вызывать данную функцию несколько раз, если часть транзакции завершается сбоем. Функция должна быть идемпотентной, только если она успешна изDB.Batch()
Он вступит в силу после возвращения.
Например: вместо того, чтобы отображать сообщение внутри функции, установите переменную в охватывающей области:
var id uint64
err := db.Batch(func(tx *bolt.Tx) error {
// Find last key in bucket, decode as bigendian uint64, increment
// by one, encode back to []byte, and add new key.
...
id = newValue
return nil
})
if err != nil {
return ...
}
fmt.Println("Allocated ID %d", id)
скопировать код
Управляйте транзакциями вручную
DB.View()
иDB.Update()
функцияDB.Begin()
Обертка для функции. Эти вспомогательные функции запускают транзакцию, выполняют функцию, а затем безопасно закрывают транзакцию при возврате ошибки. Это рекомендуемый способ торговли с Bolt.
Однако иногда вам может потребоваться вручную запускать и закрывать сделки. вы можете напрямую использоватьDB.Begin()
функцию, но не забудьте закрыть транзакцию.
// Start a writable transaction.
tx, err := db.Begin(true)
if err != nil {
return err
}
defer tx.Rollback()
// Use the transaction...
_, err := tx.CreateBucket([]byte("MyBucket"))
if err != nil {
return err
}
// Commit the transaction and check for error.
if err := tx.Commit(); err != nil {
return err
}
скопировать код
DB.Begin()
Первый параметр — логическое значение, указывающее, доступна ли транзакция для записи.
использовать ведра
Ведро — это набор пар ключ/значение в базе данных. Все ключи в сегменте должны быть уникальными. ты можешь использовать этоDB.CreateBucket()
Функция создает корзину:
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.CreateBucketIfNotExists()
Если функция не существует, можно создать корзину. Обычно эту функцию вызывают для всех сегментов верхнего уровня после открытия базы данных, поэтому вы можете гарантировать, что они существуют для будущих транзакций.
Чтобы удалить ведро, просто позвонитеTx.DeleteBucket()
функция.
Используйте пары ключ/значение
Чтобы сохранить пары ключ/значение в корзину, используйтеBucket.Put()
функция:
db.Update(func(tx *bolt.Tx) error {
b := tx.Bucket([]byte("MyBucket"))
err := b.Put([]byte("answer"), []byte("42"))
return err
})
скопировать код
Это установит значение ключа «ответ» на «42» в корзине MyBucket. Чтобы получить это значение, мы можем использоватьBucket.Get()
функция:
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
})
скопировать код
Get()
Функция не возвращает ошибку, потому что ее операции гарантированно работают (если только не произошел какой-либо системный сбой). Если ключ существует, он вернет значение своего байтового фрагмента. Возвращает ноль, если не существует. Обратите внимание, что вы можете установить значение нулевой длины для ключа, отличного от несуществующего ключа.
использоватьBucket.Delete()
Функция удаляет ключ из корзины.
Обратите внимание, что изGet()
Возвращаемое значение действительно только тогда, когда транзакция открыта. Если вам нужно использовать значение вне транзакции, вы должны использоватьcopy()
Скопируйте его в другой байтовый сегмент.
Автоматически увеличивать количество ведер
используяNextSequence()
функции, вы можете попросить Bolt определить последовательность, которая может использоваться в качестве уникального идентификатора для пары ключ/значение. См. пример ниже.
// CreateUser saves u to the store. The new user ID is set on u once the data is persisted.
func (s *Store) CreateUser(u *User) error {
return s.db.Update(func(tx *bolt.Tx) error {
// Retrieve the users bucket.
// This should be created when the DB is first opened.
b := tx.Bucket([]byte("users"))
// Generate ID for the user.
// This returns an error only if the Tx is closed or not writeable.
// That can't happen in an Update() call so I ignore the error check.
id, _ := b.NextSequence()
u.ID = int(id)
// Marshal user data into bytes.
buf, err := json.Marshal(u)
if err != nil {
return err
}
// Persist bytes to users bucket.
return b.Put(itob(u.ID), buf)
})
}
// itob returns an 8-byte big endian representation of v.
func itob(v int) []byte {
b := make([]byte, 8)
binary.BigEndian.PutUint64(b, uint64(v))
return b
}
type User struct {
ID int
...
}
скопировать код
повторять ключи
Bolt хранит ключи в корзине в порядке байтов. Это делает последовательную итерацию по этим ключам очень быстрой. Чтобы перебирать ключи, мы будем использовать курсор:
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
})
скопировать код
Курсор позволяет перемещаться в определенную точку в списке клавиш и перемещаться вперед или назад по одной клавише за раз.
В курсоре доступны следующие функции:
First() Move to the first key.
Last() Move to the last key.
Seek() Move to a specific key.
Next() Move to the next key.
Prev() Move to the previous key.
скопировать код
Каждая функция имеет сигнатуру возврата (key[]byte, value[]byte). Когда вы итерируете до конца курсора,Next()
вернет нулевой ключ. вызовNext()
илиPrev()
Раньше приходилось использоватьFirst()
,Last()
илиSeek()
чтобы найти местоположение. Если вы не ищете позицию, эти функции вернут нулевой ключ.
Во время итерации, если ключ не равен нулю, а значение равно нулю, это означает, что ключ относится к корзине, а не к значению. использоватьBucket.Bucket()
Доступ к подсегментам.
сканирование префикса
Префикс ключевого слова итерации, вы можете поставитьSeek()
иbytes.HasPrefix()
Объедините это:
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
})
скопировать код
сканирование диапазона
Другим распространенным вариантом использования является сканирование диапазона, например диапазона времени. Если вы используете сортируемую кодировку времени (например, RFC3339), вы можете запросить определенный диапазон дат следующим образом:
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
})
скопировать код
Обратите внимание, что, хотя RFC3339 можно сортировать, реализация RFC3339Nano на Golang не использует фиксированное количество цифр после запятой, поэтому его нельзя сортировать.
ForEach()
вы также можете использовать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
})
скопировать код
Пожалуйста, обрати внимание,ForEach()
Ключи и значения в действительны только пока транзакция открыта. Если вам нужно использовать ключи или значения вне транзакции, вы должны использоватьcopy()
Скопируйте его в другой фрагмент байта.
вложенные сегменты
Вы также можете сохранить ведро в ключе для создания вложенных ведер. API иDB
API управления корзинами для объектов тот же:
func (*Bucket) CreateBucket(key []byte) (*Bucket, error)
func (*Bucket) CreateBucketIfNotExists(key []byte) (*Bucket, error)
func (*Bucket) DeleteBucket(key []byte) error
скопировать код
Предположим, у вас есть мультитенантное приложение, в котором корзина корневого уровня — это корзина учетной записи. Внутри этого ведра находится ряд учетных записей, которые сами по себе являются ведром. Принимая во внимание, что в последовательности сегментов может быть много сегментов, связанных с самой учетной записью (пользователи, заметки и т. д.), разделяющих информацию на логические группы.
// createUser creates a new user in the given account.
func createUser(accountID int, u *User) error {
// Start the transaction.
tx, err := db.Begin(true)
if err != nil {
return err
}
defer tx.Rollback()
// Retrieve the root bucket for the account.
// Assume this has already been created when the account was set up.
root := tx.Bucket([]byte(strconv.FormatUint(accountID, 10)))
// Setup the users bucket.
bkt, err := root.CreateBucketIfNotExists([]byte("USERS"))
if err != nil {
return err
}
// Generate an ID for the new user.
userID, err := bkt.NextSequence()
if err != nil {
return err
}
u.ID = userID
// Marshal and save the encoded user.
if buf, err := json.Marshal(u); err != nil {
return err
} else if err := bkt.Put([]byte(strconv.FormatUint(u.ID, 10)), buf); err != nil {
return err
}
// Commit the transaction.
if err := tx.Commit(); err != nil {
return err
}
return nil
}
скопировать код
резервное копирование базы данных
Блот представляет собой один файл, поэтому его легко сделать резервную копию. ты можешь использовать этоTx.WriteTo()
Функция записывает согласованное представление базы данных в место назначения. Если вы вызовете его из транзакции только для чтения, он выполнит «горячее» резервное копирование, не блокируя другие операции чтения и записи базы данных.
По умолчанию он будет использовать обычный дескриптор файла, чтобы воспользоваться преимуществами кэша страниц операционной системы. См. документацию Tx для получения информации об оптимизации наборов данных больше, чем RAM.
Распространенным вариантом использования является резервное копирование через HTTP, поэтому вы можете использовать такой инструмент, как cURL, для резервного копирования базы данных:
func BackupHandleFunc(w http.ResponseWriter, req *http.Request) {
err := db.View(func(tx *bolt.Tx) error {
w.Header().Set("Content-Type", "application/octet-stream")
w.Header().Set("Content-Disposition", `attachment; filename="my.db"`)
w.Header().Set("Content-Length", strconv.Itoa(int(tx.Size())))
_, err := tx.WriteTo(w)
return err
})
if err != nil {
http.Error(w, err.Error(), http.StatusInternalServerError)
}
}
скопировать код
Затем вы можете сделать резервную копию с помощью этой команды:
$ curl http://localhost/backup > my.db
скопировать код
Или вы можете открыть свой браузер, чтобыhttp://localhost/backup
, он будет загружен автоматически. Если вы хотите сделать резервную копию в другой файл, вы можете использоватьTx.CopyFile()
Вспомогательная функция.
Сравните с другими базами данных
Postgres, MySQL, & other relational databases
Реляционные базы данных структурируют данные в строки, и доступ к ним возможен только с помощью SQL. Этот подход обеспечивает гибкость в том, как данные хранятся и запрашиваются, но также влечет за собой накладные расходы на синтаксический анализ и планирование операторов SQL. Bolt получает доступ ко всем данным через ключи среза байтов. Это позволяет Bolt быстро читать и записывать данные, но не обеспечивает встроенной поддержки объединения значений.
Большинство реляционных баз данных (кроме SQLite) являются автономными серверами, независимыми от сервера. Это дает вашей системе гибкость для подключения нескольких серверов приложений к одному серверу базы данных, но также увеличивает накладные расходы на сериализацию и передачу данных по сети. Bolt работает как библиотека, включенная в приложение, поэтому весь доступ к данным должен проходить через процесс приложения. Это приближает данные к вашему приложению, но ограничивает многопроцессный доступ к данным.
LevelDB, RocksDB
LevelDB и ее производные (RocksDB, HyperLevelDB) похожи на Bolt тем, что они связаны с приложениями, но их основная структура представляет собой дерево слияния с логарифмической структурой (LSM-дерево). Деревья LSM оптимизируют случайную запись, используя журналы с упреждающей записью и многоуровневые отсортированные файлы, называемые SSTables.Bolt использует внутреннее дерево B+ и имеет только один файл.У обоих подходов есть компромиссы.
LevelDB может быть хорошим выбором, если вам нужна высокая скорость произвольной записи (> 10 000 Вт/сек) или если вам нужно использовать вращающиеся диски. Если ваше приложение активно считывается или выполняет большое сканирование диапазонов, Bolt может быть хорошим выбором.
Еще одно важное соображение заключается в том, что в LevelDB нет транзакций. Он поддерживает пакетную запись пар ключ/значение, поддерживает чтение моментальных снимков, но не позволяет безопасно выполнять операции сравнения и замены.Bolt поддерживает полностью сериализуемые транзакции ACID.
LMDB
Bolt изначально был аналогичной реализацией LMDB, поэтому он похож по структуре. Оба используют деревья B+, имеют семантику ACID для полностью сериализуемых транзакций и используют один модуль записи и несколько модулей чтения для поддержки MVCC без блокировок.
Эти два проекта несколько расходятся. LMDB ориентирован в первую очередь на чистую производительность, а Bolt — на простоту и удобство использования. Например, LMDB допускает некоторые небезопасные операции, такие как операции прямой записи.Bolt решил запретить операции, которые могут оставить базу данных в поврежденном состоянии. Единственным исключением в Bolt является DB.NoSync.
Есть также некоторые отличия в API. Для LMDB требуется наибольший размер mmap при открытии mdb_env, и Bolt автоматически обрабатывает увеличение размера mmap. LMDB перегружает функции получения и установки несколькими флагами, в то время как Bolt разбивает эти особые случаи на отдельные функции.
Примечания и ограничения
Выбор правильного инструмента очень важен, и Bolt не исключение. Вот несколько вещей, о которых следует помнить при оценке и использовании Bolt:
-
Bolt подходит для рабочих нагрузок с интенсивным чтением. Производительность последовательной записи также высока, но случайная запись может быть медленной. ты можешь использовать это
DB.Batch()
Или добавьте журналы с упреждающей записью, чтобы решить эту проблему. -
Bolt использует деревья B+ для внутреннего использования, поэтому может быть много случайных посещений страниц. По сравнению с вращающимися дисками твердотельные накопители могут значительно повысить производительность.
-
Старайтесь избегать длительных транзакций чтения. Использование болта
copy-on-write
Технология, старые транзакции используются, старые страницы не могут быть восстановлены. -
Срезы байтов, возвращаемые Bolt, действительны только во время транзакции. После фиксации или отката транзакций память, на которую они указывают, может быть повторно использована новыми страницами или может быть удалена из виртуальной памяти, и при доступе будет замечена неожиданная паника адреса ошибки.
-
Bolt использует эксклюзивные блокировки записи в файлах базы данных, поэтому не может использоваться совместно несколькими процессами.
-
использовать
Bucket.FillPercent
будь осторожен. Установка высокого процента заполнения для сегментов со случайными вставками приводит к плохому использованию страниц базы данных. -
Обычно используйте ведра большего размера. Меньшие сегменты приводят к плохому использованию страницы, если они превышают размер страницы (обычно 4 КБ).
-
Загрузка большого пакета случайных записей в новое хранилище может быть медленной, поскольку страницы не разделяются до тех пор, пока транзакция не будет зафиксирована. Не рекомендуется случайным образом вставлять более 100 000 пар ключ/значение в одну новую корзину за одну транзакцию.
-
Bolt использует файлы с отображением памяти, так что базовая операционная система обрабатывает кэширование данных. Обычно операционная система кэширует как можно больше файлов и при необходимости освобождает память для других процессов. Это означает, что Bolt может демонстрировать очень высокое использование памяти при работе с большими базами данных. Однако это ожидаемо, и ОС будет освобождать память по мере необходимости. Bolt может работать с базами данных, размер которых намного превышает объем доступной физической памяти, если его карта памяти соответствует виртуальному адресному пространству процесса. Это может быть проблематично в 32-битных системах.
-
Структуры данных в базе данных Bolt отображаются в памяти, поэтому файлы данных будут специфичными для байтов. Это означает, что вы не можете скопировать файл Bolt с машины с прямым порядком байтов на машину с прямым порядком байтов и заставить его работать. Для большинства пользователей это не проблема, так как большинство современных процессоров имеют обратный порядок байтов.
-
Из-за того, как страницы расположены на диске, Bolt не может обрезать файл данных и вернуть свободные страницы на диск. Вместо этого Bolt хранит свободный список неиспользуемых страниц в своем файле данных. Эти бесплатные страницы можно повторно использовать для будущих транзакций. Поскольку базы данных обычно растут, это хороший подход для многих случаев использования, однако важно отметить, что удаление больших блоков данных не позволит вам освободить место на диске.
Эта статья написанаCopernicus 团队 冉小龙
Перевод, перепечатка без разрешения.