🦤

go in-memory database generator

2022/03/24に公開

go の型安全かつスレッドセーフなインメモリデータベース生成パッケージを作成しました。
simpleDBsimdbという名前にしました。

go1.18 に沸く昨今時代錯誤かもしれないですが、MIT で公開してるので気に入ったら使ってみて下さい。

https://github.com/maru44/simdb

こういうのと組み合わせても面白いかも...
https://github.com/samber/lo

やってること

単純に設定(config)ファイルから必要な情報を取得して排他制御付きのmapを保持した型を生成します。同時にGetInsert等の単純なメソッドの実装も生成します。

例えばベンチマークの計測に使った構造体は以下のような形です。

_tests/bench/db.go
type (
	// カラムを纏めた構造体
	bench struct {
		Name    string
		Email   string
		age     uint
		IsValid bool
	}

	// テーブル キーで括られたカラムのまとまりを持ちつつ、
	// sync.RWMutexが埋め込まれているだけ
	benchs struct {
		data map[int]bench
		sync.RWMutex
	}
)

ただカラムを纏めた構造体を指定された型のキーでmapにしてその構造体を持ちつつ、排他制御用にsync.RWMutexを埋め込んであるだけです。

sync.Mapとの違いは型安全なことと、無駄なキャストいらずで少しだけパフォーマンスが高いことです。

パフォーマンス

sync.Mapとの簡単なベンチマーク比較です。生成されるテーブルのカラムの数などによってここは変わってくるとは思うのであくまで参考です。

一件インサート系

上がsimdb下がsync.Map
BenchmarkInsert-8                4487815               250.8 ns/op         246 B/op           0 allocs/op
BenchmarkInsert_SyncMap-8        2824500               432.6 ns/op         185 B/op           5 allocs/op

一件取得系

上2つがsimdb下がsync.Map
BenchmarkGet-8                  12561126               116.3 ns/op             0 B/op          0 allocs/op
BenchmarkLoad-8                 11806761               119.4 ns/op             0 B/op          0 allocs/op
BenchmarkLoad_SyncMap-8          7745020               177.0 ns/op             0 B/op          0 allocs/op

一件削除

上がsimdb下がsync.Map
BenchmarkDelete-8               11140561               125.1 ns/op             0 B/op          0 allocs/op
BenchmarkDelete_SyncMap-8        7712466               175.9 ns/op             0 B/op          0 allocs/op

生成の仕方

ザックリと使い方を説明します。

カラムとテーブルが定義された yaml や toml もしくは json 等を用意します。
デフォルトではsimdbという名前の設定ファイルを見に行くので、yaml であればsimdb.yamlという名前が無難です。一応生成時に--config=hogeと変更できますが、面倒ですよね...

simdb.yaml
name: table_a
is_private: true # trueだとtypeが小文字で始まります
key_type: string
columns:
  - name: name
    type: string
  - name: expired_at
    type: int64
  - name: isExpired
    type: bool

基本的にtypeによるバリデーションは設けていないので独自に定義した型定義も使えます。

同階層に generate 用の go ファイルを適当に用意してあげます

生成されるファイル名はデフォルトでdb.goになります
生成されるpackageはデフォルトだと main になります

gen.go
package db

//go:generate go run github.com/maru44/simdb --package=db

再帰実行でもなんでもとにかく実行します

go generate ./...

そうすると...
こんな感じのが生成されると思います

db.go
// Code generated by "github.com/maru44/simdb/gen"; DO NOT EDIT.

package db

import (
	"fmt"
	"sync"
)

type (
	tableA struct {
		Name      string
		ExpiredAt int64
		IsExpired bool
	}

	tableAs struct {
		data map[string]tableA
		sync.RWMutex
	}
)

func NewTableAs() tableAs {
	return tableAs{
		data: map[string]tableA{},
	}
}

func (t *tableAs) List() map[string]tableA {
	t.RLock()
	defer t.RUnlock()
	return t.data
}

...

開発経緯

誰も興味ないと思うけど、適当に時系列で書いときます。

  • 個人で作ろうと思った web サービスでワンタイムのランダムキーをとにかく頻繁に生成、破棄する仕組みが欲しい
  • redisのような KVS を使うか、インメモリで処理するか、、、
  • インメモリで処理したいなー
  • 爆速インメモリ DB 生成機作ったろ
  • スタック領域だけでできるだけ処理は済ませたいな
  • カラムやキーに使える型をbool~intだけに限定しよう
  • あれ、これ排他制御できなくない? (値参照だけじゃ厳しいか?)
  • 良くも悪くもスタックとヒープを勝手に割り当てる go の特徴を消してない?
  • スタック領域だけにこだわるならそもそもより低レイヤーな言語で作れば良くね?
  • うーん、スタック領域限定爆速インメモリ DB 生成機はいったん置いといてそれっぽく仕上げちゃおう

こんな感じで紆余曲折はありつつも、なんとかそれっぽく作りあげたという感じでした。

まとめ

良かったら使ってみて下さい。
自分も個人的に使う予定です。

考え方的にもコード的にも拙いところや考慮が不足している部分等たくさんあると思うので、指摘してくださるととても助かります 😋

https://github.com/maru44/simdb

GitHubで編集を提案

Discussion