tidwall/buntdb
GitHub: tidwall/buntdb
BuntDB 是一个纯 Go 实现的嵌入式内存键值数据库,提供 ACID 事务保障,并支持自定义索引、JSON 字段索引和地理空间查询。
Stars: 4863 | Forks: 308
BuntDB 是一个纯 Go 编写的低层级、内存级 key/value 存储。
它支持持久化到磁盘、兼容 ACID,并使用多读单写锁机制。
它支持自定义索引和地理空间数据。它非常适合那些需要可靠数据库且重速度胜于数据大小的项目。
# 功能
# 快速入门
## 安装
要开始使用 BuntDB,请安装 Go 并运行 `go get`:
```
$ go get -u github.com/tidwall/buntdb
```
这将获取该库。
## 打开数据库
BuntDB 中的主要对象是 `DB`。要打开或创建你的数据库,请使用 `buntdb.Open()` 函数:
```
package main
import (
"log"
"github.com/tidwall/buntdb"
)
func main() {
// Open the data.db file. It will be created if it doesn't exist.
db, err := buntdb.Open("data.db")
if err != nil {
log.Fatal(err)
}
defer db.Close()
...
}
```
也可以通过使用 `:memory:` 作为文件路径来打开一个不持久化到磁盘的数据库。
```
buntdb.Open(":memory:") // Open a file that does not persist to disk.
```
## 事务
事务运行在一个暴露 `Tx` 对象的函数中,该对象表示事务状态。在事务内部,所有数据库操作都应使用此对象执行。在事务内部,你绝对不应访问原始的 `DB` 对象。这样做可能会产生副作用,例如阻塞你的应用程序。
当事务失败时,它将回滚,并恢复该事务期间对数据库所做的所有更改。你可以使用一个单一的返回值来关闭事务。对于读/写事务,以这种方式返回错误将强制事务回滚。当读/写事务成功时,所有更改都将持久化到磁盘。
### 只读事务
当你不需要对数据进行更改时,应该使用只读事务。只读事务的优点是可以并发运行多个只读事务。
```
err := db.View(func(tx *buntdb.Tx) error {
...
return nil
})
```
### 读/写事务
当你需要对数据进行更改时,使用读/写事务。一次只能运行一个读/写事务。因此,请确保在完成后尽快关闭它。
```
err := db.Update(func(tx *buntdb.Tx) error {
...
return nil
})
```
## 设置和获取 key/value
要设置一个值,你必须打开一个读/写事务:
```
err := db.Update(func(tx *buntdb.Tx) error {
_, _, err := tx.Set("mykey", "myvalue", nil)
return err
})
```
获取该值:
```
err := db.View(func(tx *buntdb.Tx) error {
val, err := tx.Get("mykey")
if err != nil{
return err
}
fmt.Printf("value is %s\n", val)
return nil
})
```
获取不存在的值将导致 `ErrNotFound` 错误。
### 迭代
数据库中所有 key/value 对按键排序。要迭代键:
```
err := db.View(func(tx *buntdb.Tx) error {
err := tx.Ascend("", func(key, value string) bool {
fmt.Printf("key: %s, value: %s\n", key, value)
return true // continue iteration
})
return err
})
```
还有 `AscendGreaterOrEqual`、`AscendLessThan`、`AscendRange`、`AscendEqual`、`Descend`、`DescendLessOrEqual`、`DescendGreaterThan`、`DescendRange` 和 `DescendEqual`。有关这些函数的更多信息,请参见[文档](https://godoc.org/github.com/tidwall/buntdb)。
## 自定义索引
最初,所有数据都存储在单个 [B-tree](https://en.wikipedia.org/wiki/B-tree) 中,每个项都有一个键和一个值。所有这些项按键排序。这对于从键快速获取值或[迭代](#iterating)键非常有利。欢迎查阅 [B-tree 实现](https://github.com/tidwall/btree)。
你还可以创建允许对值进行排序和[迭代](#iterating)的自定义索引。自定义索引也使用 B-tree,但它更灵活,因为它允许自定义排序。
例如,假设你想创建一个用于对名称进行排序的索引:
```
db.CreateIndex("names", "*", buntdb.IndexString)
```
这将创建一个名为 `names` 的索引,用于存储和排序所有值。第二个参数是用于过滤键的模式。`*` 通配符参数表示我们要接受所有键。`IndexString` 是一个内置函数,用于对值执行不区分大小写的排序。
现在你可以添加各种名称:
```
db.Update(func(tx *buntdb.Tx) error {
tx.Set("user:0:name", "tom", nil)
tx.Set("user:1:name", "Randi", nil)
tx.Set("user:2:name", "jane", nil)
tx.Set("user:4:name", "Janet", nil)
tx.Set("user:5:name", "Paula", nil)
tx.Set("user:6:name", "peter", nil)
tx.Set("user:7:name", "Terri", nil)
return nil
})
```
最后,你可以迭代该索引:
```
db.View(func(tx *buntdb.Tx) error {
tx.Ascend("names", func(key, val string) bool {
fmt.Printf(buf, "%s %s\n", key, val)
return true
})
return nil
})
```
输出应为:
```
user:2:name jane
user:4:name Janet
user:5:name Paula
user:6:name peter
user:1:name Randi
user:7:name Terri
user:0:name tom
```
可以使用模式参数像这样过滤键:
```
db.CreateIndex("names", "user:*", buntdb.IndexString)
```
现在,只有键带有 `user:` 前缀的项才会被添加到 `names` 索引中。
### 内置类型
除了 `IndexString`,还有 `IndexInt`、`IndexUint` 和 `IndexFloat`。
这些是用于索引的内置类型。你可以选择使用这些类型或创建自己的类型。
因此,要创建一个在 age 键上按数字排序的索引,我们可以使用:
```
db.CreateIndex("ages", "user:*:age", buntdb.IndexInt)
```
然后添加值:
```
db.Update(func(tx *buntdb.Tx) error {
tx.Set("user:0:age", "35", nil)
tx.Set("user:1:age", "49", nil)
tx.Set("user:2:age", "13", nil)
tx.Set("user:4:age", "63", nil)
tx.Set("user:5:age", "8", nil)
tx.Set("user:6:age", "3", nil)
tx.Set("user:7:age", "16", nil)
return nil
})
```
```
db.View(func(tx *buntdb.Tx) error {
tx.Ascend("ages", func(key, val string) bool {
fmt.Printf(buf, "%s %s\n", key, val)
return true
})
return nil
})
```
输出应为:
```
user:6:age 3
user:5:age 8
user:2:age 13
user:7:age 16
user:0:age 35
user:1:age 49
user:4:age 63
```
## 空间索引
要创建空间索引,请使用 `CreateSpatialIndex` 函数:
```
db.CreateSpatialIndex("fleet", "fleet:*:pos", buntdb.IndexRect)
```
然后 `IndexRect` 是一个内置函数,它将矩形字符串转换为 R-tree 可以使用的格式。开箱即用使用此函数很容易,但你可能会发现创建一个从不同格式(例如 [Well-known text](https://en.wikipedia.org/wiki/Well-known_text) 或 [GeoJSON](http://geojson.org/))渲染的自定义函数会更好。
要向 `fleet` 索引添加一些经纬度点:
```
db.Update(func(tx *buntdb.Tx) error {
tx.Set("fleet:0:pos", "[-115.567 33.532]", nil)
tx.Set("fleet:1:pos", "[-116.671 35.735]", nil)
tx.Set("fleet:2:pos", "[-113.902 31.234]", nil)
return nil
})
```
然后你可以对该索引运行 `Intersects` 函数:
```
db.View(func(tx *buntdb.Tx) error {
tx.Intersects("fleet", "[-117 30],[-112 36]", func(key, val string) bool {
...
return true
})
return nil
})
```
这将获取全部三个位置。
### k-Nearest Neighbors
使用 `Nearby` 函数按从最近到最远的顺序获取所有位置:
```
db.View(func(tx *buntdb.Tx) error {
tx.Nearby("fleet", "[-113 33]", func(key, val string, dist float64) bool {
...
return true
})
return nil
})
```
### 空间括号语法
括号语法 `[-117 30],[-112 36]` 是 BuntDB 特有的,这也是内置矩形的处理方式。但是,你并不局限于这种语法。在 `CreateSpatialIndex` 期间你选择使用的任何 Rect 函数都将用于处理参数,在这个例子中是 `IndexRect`。
- **2D 矩形:** `[10 15],[20 25]`
*最小 XY: "10x15", 最大 XY: "20x25"*
- **3D 矩形:** `[10 15 12],[20 25 18]`
*最小 XYZ: "10x15x12", 最大 XYZ: "20x25x18"*
- **2D 点:** `[10 15]`
*XY: "10x15"*
- **LonLat 点:** `[-112.2693 33.5123]`
*LatLon: "33.5123 -112.2693"*
- **LonLat 边界框:** `[-112.26 33.51],[-112.18 33.67]`
*最小 LatLon: "33.51 -112.26", 最大 LatLon: "33.67 -112.18"*
**注意:** 经度是 Y 轴,位于左侧,纬度是 X 轴,位于右侧。
你还可以使用 `-inf` 和 `+inf` 来表示 `Infinity`。
例如,你可能拥有以下点(`[X Y M]`,其中 XY 是一个点,M 是时间戳):
```
[3 9 1]
[3 8 2]
[4 8 3]
[4 7 4]
[5 7 5]
[5 6 6]
```
然后你可以通过调用 `Intersects` 来搜索 `M` 在 2 到 4 之间的所有点。
```
tx.Intersects("points", "[-inf -inf 2],[+inf +inf 4]", func(key, val string) bool {
println(val)
return true
})
```
这将返回:
```
[3 8 2]
[4 8 3]
[4 7 4]
```
## JSON 索引
可以在 JSON 文档内的单个字段上创建索引。BuntDB 底层使用了 [GJSON](https://github.com/tidwall/gjson)。
例如:
```
package main
import (
"fmt"
"github.com/tidwall/buntdb"
)
func main() {
db, _ := buntdb.Open(":memory:")
db.CreateIndex("last_name", "*", buntdb.IndexJSON("name.last"))
db.CreateIndex("age", "*", buntdb.IndexJSON("age"))
db.Update(func(tx *buntdb.Tx) error {
tx.Set("1", `{"name":{"first":"Tom","last":"Johnson"},"age":38}`, nil)
tx.Set("2", `{"name":{"first":"Janet","last":"Prichard"},"age":47}`, nil)
tx.Set("3", `{"name":{"first":"Carol","last":"Anderson"},"age":52}`, nil)
tx.Set("4", `{"name":{"first":"Alan","last":"Cooper"},"age":28}`, nil)
return nil
})
db.View(func(tx *buntdb.Tx) error {
fmt.Println("Order by last name")
tx.Ascend("last_name", func(key, value string) bool {
fmt.Printf("%s: %s\n", key, value)
return true
})
fmt.Println("Order by age")
tx.Ascend("age", func(key, value string) bool {
fmt.Printf("%s: %s\n", key, value)
return true
})
fmt.Println("Order by age range 30-50")
tx.AscendRange("age", `{"age":30}`, `{"age":50}`, func(key, value string) bool {
fmt.Printf("%s: %s\n", key, value)
return true
})
return nil
})
}
```
结果:
```
Order by last name
3: {"name":{"first":"Carol","last":"Anderson"},"age":52}
4: {"name":{"first":"Alan","last":"Cooper"},"age":28}
1: {"name":{"first":"Tom","last":"Johnson"},"age":38}
2: {"name":{"first":"Janet","last":"Prichard"},"age":47}
Order by age
4: {"name":{"first":"Alan","last":"Cooper"},"age":28}
1: {"name":{"first":"Tom","last":"Johnson"},"age":38}
2: {"name":{"first":"Janet","last":"Prichard"},"age":47}
3: {"name":{"first":"Carol","last":"Anderson"},"age":52}
Order by age range 30-50
1: {"name":{"first":"Tom","last":"Johnson"},"age":38}
2: {"name":{"first":"Janet","last":"Prichard"},"age":47}
```
## 多值索引
使用 BuntDB,可以在单个索引上连接多个值。
这类似于传统 SQL 数据库中的[多列索引](http://dev.mysql.com/doc/refman/5.7/en/multiple-column-indexes.html)。
在此示例中,我们在 "name.last" 和 "age" 上创建多值索引:
```
db, _ := buntdb.Open(":memory:")
db.CreateIndex("last_name_age", "*", buntdb.IndexJSON("name.last"), buntdb.IndexJSON("age"))
db.Update(func(tx *buntdb.Tx) error {
tx.Set("1", `{"name":{"first":"Tom","last":"Johnson"},"age":38}`, nil)
tx.Set("2", `{"name":{"first":"Janet","last":"Prichard"},"age":47}`, nil)
tx.Set("3", `{"name":{"first":"Carol","last":"Anderson"},"age":52}`, nil)
tx.Set("4", `{"name":{"first":"Alan","last":"Cooper"},"age":28}`, nil)
tx.Set("5", `{"name":{"first":"Sam","last":"Anderson"},"age":51}`, nil)
tx.Set("6", `{"name":{"first":"Melinda","last":"Prichard"},"age":44}`, nil)
return nil
})
db.View(func(tx *buntdb.Tx) error {
tx.Ascend("last_name_age", func(key, value string) bool {
fmt.Printf("%s: %s\n", key, value)
return true
})
return nil
})
// Output:
// 5: {"name":{"first":"Sam","last":"Anderson"},"age":51}
// 3: {"name":{"first":"Carol","last":"Anderson"},"age":52}
// 4: {"name":{"first":"Alan","last":"Cooper"},"age":28}
// 1: {"name":{"first":"Tom","last":"Johnson"},"age":38}
// 6: {"name":{"first":"Melinda","last":"Prichard"},"age":44}
// 2: {"name":{"first":"Janet","last":"Prichard"},"age":47}
```
## 降序索引
任何索引都可以通过使用 `buntdb.Desc` 包装其 less 函数来进行降序排列。
```
db.CreateIndex("last_name_age", "*",
buntdb.IndexJSON("name.last"),
buntdb.Desc(buntdb.IndexJSON("age")),
)
```
这将创建一个名字升序而年龄降序的多值索引。
## 整理 i18n 索引
使用外部的 [collate package](https://github.com/tidwall/collate) 可以创建按指定语言排序的索引。这类似于传统数据库中发现的 [SQL COLLATE 关键字](https://msdn.microsoft.com/en-us/library/ms174596.aspx)。
安装:
```
go get -u github.com/tidwall/collate
```
例如:
```
import "github.com/tidwall/collate"
// To sort case-insensitive in French.
db.CreateIndex("name", "*", collate.IndexString("FRENCH_CI"))
// To specify that numbers should sort numerically ("2" < "12")
// and use a comma to represent a decimal point.
db.CreateIndex("amount", "*", collate.IndexString("FRENCH_NUM"))
```
```
db.CreateIndex("last_name", "*", collate.IndexJSON("CHINESE_CI", "name.last"))
```
查看 [collate 项目](https://github.com/tidwall/collate) 了解更多信息。
## 数据过期
可以通过在 `Set` 函数中使用 `SetOptions` 对象设置 `TTL` 来自动驱逐项。
```
db.Update(func(tx *buntdb.Tx) error {
tx.Set("mykey", "myval", &buntdb.SetOptions{Expires:true, TTL:time.Second})
return nil
})
```
现在 `mykey` 将在一秒钟后自动删除。你可以通过使用相同的 key/value 再次设置该值,并将 options 参数设置为 nil 来移除 TTL。
## 迭代时删除
```
var delkeys []string
tx.AscendKeys("object:*", func(k, v string) bool {
if someCondition(k) == true {
delkeys = append(delkeys, k)
}
return true // continue
})
for _, k := range delkeys {
if _, err = tx.Delete(k); err != nil {
return err
}
}
```
## Append-only File
BuntDB 使用 AOF (append-only file),这是记录像 `Set()` 和 `Delete()` 等操作导致的所有数据库更改的日志。
此文件的格式如下所示:
```
set key:1 value1
set key:2 value2
set key:1 value3
del key:2
...
```
当再次打开数据库时,它将回读 aof 文件并按确切顺序处理每个命令。
此读取过程在数据库打开时发生一次。
此后,该文件只会进行追加。
正如你可能猜到的那样,这个日志文件随着时间的推移会变得非常大。
有一个后台例程会在日志文件变得太大时自动缩小它。
还有一个 `Shrink()` 函数,它将重写 aof 文件,使其仅包含数据库中的项。
shrink 操作不会锁定数据库,因此在缩小过程中可以继续进行读写事务。
### 持久性和 fsync
默认情况下,BuntDB 每秒对 [aof 文件](#append-only-file) 执行一次 `fsync`。这仅意味着最多可能会丢失一秒钟的数据。如果你需要更高的持久性,有一个可选的数据库配置设置 `Config.SyncPolicy` 可以设置为 `Always`。
`Config.SyncPolicy` 具有以下选项:
- `Never` - fsync 由操作系统管理,安全性较低
- `EverySecond` - 每秒 fsync,快速且更安全,这是默认值
- `Always` - 每次写入后 fsync,非常持久,速度较慢
## 配置
以下是一些可用于更改数据库各种行为的配置选项。
- **SyncPolicy** 调整数据同步到磁盘的频率。此值可以为 Never、EverySecond 或 Always。默认值为 EverySecond。
- **AutoShrinkPercentage** 被后台进程使用,当文件大小大于之前缩小文件结果大小的百分比时,触发 aof 文件的 shrink。例如,如果此值为 100,并且上一次 shrink 过程产生了一个 100mb 的文件,则新 aof 文件必须达到 200mb 才会触发 shrink。默认值为 100。
- **AutoShrinkMinSize** 定义在自动 shrink 发生之前 aof 文件的最小大小。默认值为 32MB。
- **AutoShrinkDisabled** 关闭后台自动 shrink。默认值为 false。
要更新配置,你应该调用 `ReadConfig`,然后调用 `SetConfig`。例如:
```
var config buntdb.Config
if err := db.ReadConfig(&config); err != nil{
log.Fatal(err)
}
if err := db.SetConfig(config); err != nil{
log.Fatal(err)
}
```
## 性能
BuntDB 有多快?
以下是在 Raft Store 实现中使用 BuntDB 时的一些[基准测试](https://github.com/tidwall/raft-buntdb#raftstore-performance-comparison)示例。
你还可以从项目根目录运行标准 Go 基准测试工具:
```
go test --bench=.
```
### BuntDB-Benchmark
有一个专门为 BuntDB 创建的[自定义实用工具](https://github.com/tidwall/buntdb-benchmark)用于基准测试。
*这些是在 MacBook Pro 15" 2.8 GHz Intel Core i7 上运行基准测试的结果:*
```
$ buntdb-benchmark -q
GET: 4609604.74 operations per second
SET: 248500.33 operations per second
ASCEND_100: 2268998.79 operations per second
ASCEND_200: 1178388.14 operations per second
ASCEND_400: 679134.20 operations per second
ASCEND_800: 348445.55 operations per second
DESCEND_100: 2313821.69 operations per second
DESCEND_200: 1292738.38 operations per second
DESCEND_400: 675258.76 operations per second
DESCEND_800: 337481.67 operations per second
SPATIAL_SET: 134824.60 operations per second
SPATIAL_INTERSECTS_100: 939491.47 operations per second
SPATIAL_INTERSECTS_200: 561590.40 operations per second
SPATIAL_INTERSECTS_400: 306951.15 operations per second
SPATIAL_INTERSECTS_800: 159673.91 operations per second
```
安装此实用工具:
```
go get github.com/tidwall/buntdb-benchmark
```
## 联系方式
Josh Baker [@tidwall](http://twitter.com/tidwall)
## License
BuntDB 源代码在 MIT [许可证](/LICENSE)下提供。
标签:EVTX分析, 日志审计