Go: BoltDB

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

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

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

go get github.com/boltdb/bolt

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

package main

import (
"log"
"github.com/boltdb/bolt"
)

func main() {
  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 {
  // transaction body
  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("MyCollection))
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("ошибка создания коллекции: %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 {
  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 {
  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 {
  c := tx.Bucket([]byte("Events")).Cursor()

  min := []byte("1990-01-01")
  max := []byte("2000-01-01")

  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 {
  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
})

За более подробной информацией можете обратиться к официальному руководству.

 

15.03.2018









 
архив

подписка