golang boltdb的學習和實踐
1. 安裝
go get github.com/boltdb/bolt
2.創建和啟動數據庫
db, err := bolt.Open("my.db", 0600, nil)
其中open
的第一個參數為路徑,如果數據庫不存在則會創建名為my.db的數據庫, 第二個為文件操作,第三個參數是可選參數, 內部可以配置只讀和超時時間等,
特別需要注意的地方就是因為boltdb是文件操作類型的數據庫,所以只能單點寫入和讀取,如果多個同時操作的話后者會被掛起直到前者關閉操作為止, boltdb一次只允許一個讀寫事務,但一次允許多個只讀事務。所以數據具有較強的一致性。
因此單個事務和從它們創建的所有對象(例如桶、鍵)都不是線程安全的。與數據在多個概念你必須為每一個或使用鎖機制來保證只有一個goroutine里操作改變數據。
只讀事務和讀寫事物通常不應該在同一個goroutine里同時打開。由于讀寫事務需要周期性地重新映射數據文件,這可能導致死鎖。
3.讀寫事務
boltdb的讀寫事務操作我們可以使用DB.Update()
來完成形如:
err := db.Update(func(tx *bolt.Tx) error {...return nil
})
在閉包fun中,在結束時返回nil來提交事務。您還可以通過返回一個錯誤在任何點回滾事務。所有數據庫操作都允許在讀寫事務中進行。
始終要關注err返回,因為它將報告導致您的事務不能完成的所有磁盤故障。
4.批量讀寫事物
每一次新的事物都需要等待上一次事物的結束,這種開銷我們可以通過DB.Batch()
批處理來完成
err := db.Batch(func(tx *bolt.Tx) error {...return nil
})
在批處理過程中如果某個事務失敗了,批處理會多次調用這個函數函數返回成功則成功。如果中途失敗了,則整個事務會回滾。
5.只讀事務
只讀事務可以使用DB.View()
來完成
err := db.View(func(tx *bolt.Tx) error {...return nil
})
不改變數據的操作都可以通過只讀事務來完成, 您只能檢索桶、檢索值,或在只讀事務中復制數據庫。
6.啟動事務
DB.Begin()
啟動函數包含在db.update和db.batch中,該函數啟動事務開始執行事務并返回結果關閉事務,這是boltdb推薦的方式,有時候你可能需要手動啟動事物你可以使用Tx.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
}
7.使用桶
桶是數據庫中鍵/值對的集合。桶中的所有鍵必須是唯一的。您可以使用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()
來完成
8.使用k-v對
存儲鍵值對到桶里可以使用Bucket.Put()
來完成:
db.Update(func(tx *bolt.Tx) error {b := tx.Bucket([]byte("MyFriendsBucket"))err := b.Put([]byte("one"), []byte("zhangsan"))return err
})
獲取鍵值Bucket.Get()
:
db.View(func(tx *bolt.Tx) error {b := tx.Bucket([]byte("MyFriendsBucket"))v := b.Get([]byte("one"))fmt.Printf("The answer is: %s\n", v)return nil
})
get()
函數不返回一個錯誤,因為它的運行是保證工作(除非有某種系統故障)。如果鍵存在,那么它將返回它的值。如果它不存在,那么它將返回nil。
還需要注意的是當事務打開都get返回的值時唯一有效的,如果你需要將該值用于其他事務,你可以通過copy
拷貝到其他的byte slice中
9.桶的自增
利用nextsequence()
功能,你可以讓boltdb生成序列作為你鍵值對的唯一標識。見下面的示例。
func (s *Store) CreateUser(u *User) error {return s.db.Update(func(tx *bolt.Tx) error {// 創建users桶b := tx.Bucket([]byte("users"))// 生成自增序列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...
}
10. 迭代鍵
boltdb以桶中的字節排序順序存儲鍵。這使得在這些鍵上的順序迭代非常快。要遍歷鍵,我們將使用游標Cursor()
:
db.View(func(tx *bolt.Tx) error {// Assume bucket exists and has keysb := 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
})
游標Cursor()
允許您移動到鍵列表中的特定點,并一次一個地通過操作鍵前進或后退。
光標上有以下函數:
First() 移動到第一個健.
Last() 移動到最后一個健.
Seek() 移動到特定的一個健.
Next() 移動到下一個健.
Prev() 移動到上一個健.
這些函數中的每一個都返回一個包含(key []byte, value []byte)的簽名。當你有光標迭代結束,next()將返回一個nil。在調用next()或prev()之前,你必須尋求一個位置使用first(),last(),或seek()。如果您不尋求位置,則這些函數將返回一個nil鍵。
在迭代過程中,如果鍵為非零,但值為0,則意味著鍵指向一個桶而不是一個值。用桶.bucket()訪問子桶。
11.前綴掃描
遍歷一個key的前綴,你可以結合seek()
和bytes.hasprefix()
:
db.View(func(tx *bolt.Tx) error {// Assume bucket exists and has keysc := tx.Bucket([]byte("MyBucket")).Cursor()prefix := []byte("1234")for k, v := c.Seek(prefix); bytes.HasPrefix(k, prefix); k, v = c.Next() {fmt.Printf("key=%s, value=%s\n", k, v)}return nil
})
12.范圍掃描
另一個常見的用例是掃描范圍,例如時間范圍。如果你使用一個合適的時間編碼,如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
})
13.循環遍歷每一個
如果你知道所在桶中擁有鍵,你也可以使用ForEach()
來迭代:
db.View(func(tx *bolt.Tx) error {// Assume bucket exists and has keysb := tx.Bucket([]byte("MyBucket"))b.ForEach(func(k, v []byte) error {fmt.Printf("key=%s, value=%s\n", k, v)return nil})return nil
})
14.嵌套桶
還可以在一個鍵中存儲一個桶,以創建嵌套的桶:
func (*Bucket) CreateBucket(key []byte) (*Bucket, error)
func (*Bucket) CreateBucketIfNotExists(key []byte) (*Bucket, error)
func (*Bucket) DeleteBucket(key []byte) error
15.數據庫備份
boltdb是一個單一的文件,所以很容易備份。你可以使用TX.writeto()
函數寫一致的數據庫。如果從只讀事務調用這個函數,它將執行熱備份,而不會阻塞其他數據庫的讀寫操作。
默認情況下,它將使用一個常規文件句柄,該句柄將利用操作系統的頁面緩存。有關優化大于RAM數據集的信息,請參見Tx
文檔。
一個常見的用例是在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()
輔助功能。
16.統計
數據庫對運行的許多內部操作保持一個運行計數,這樣您就可以更好地了解發生了什么。通過捕捉這些數據的快照,我們可以看到在這個時間范圍內執行了哪些操作。
例如,我們可以開始一個goroutine里記錄統計每10秒:
go func() {// Grab the initial stats.prev := db.Stats()for {// Wait for 10s.time.Sleep(10 * time.Second)// Grab the current stats and diff them.stats := db.Stats()diff := stats.Sub(&prev)// Encode stats to JSON and print to STDERR.json.NewEncoder(os.Stderr).Encode(diff)// Save stats for the next loop.prev = stats}
17.只讀模式
有時創建一個共享的只讀boltdb數據庫是有用的。對此,設置options.readonly國旗打開數據庫時。只讀模式使用共享鎖允許多個進程從數據庫中讀取,但它將阻塞任何以讀寫方式打開數據庫的進程。
db, err := bolt.Open("my.db", 0666, &bolt.Options{ReadOnly: true})
if err != nil {log.Fatal(err)
}
18.移動端支持(ios/android)
boltdb能夠運行在移動設備上利用的工具結合特征GoMobile。創建一個結構體,包含您的數據庫邏輯和參考一個bolt.db與初始化contstructor需要在文件路徑,數據庫文件將存儲。使用這種方法,Android和iOS都不需要額外的權限或清理。
func NewBoltDB(filepath string) *BoltDB {db, err := bolt.Open(filepath+"/demo.db", 0600, nil)if err != nil {log.Fatal(err)}return &BoltDB{db}
}type BoltDB struct {db *bolt.DB...
}func (b *BoltDB) Path() string {return b.db.Path()
}func (b *BoltDB) Close() {b.db.Close()
}
數據庫邏輯應定義為此包裝器結構中的方法。
要從本機語言初始化此結構(兩個平臺現在都將本地存儲與云同步)。這些片段禁用數據庫文件的功能):
Android
String path;
if (android.os.Build.VERSION.SDK_INT >=android.os.Build.VERSION_CODES.LOLLIPOP){path = getNoBackupFilesDir().getAbsolutePath();
} else{path = getFilesDir().getAbsolutePath();
}
Boltmobiledemo.BoltDB boltDB = Boltmobiledemo.NewBoltDB(path)
IOS
- (void)demo {NSString* path = [NSSearchPathForDirectoriesInDomains(NSLibraryDirectory,NSUserDomainMask,YES) objectAtIndex:0];GoBoltmobiledemoBoltDB * demo = GoBoltmobiledemoNewBoltDB(path);[self addSkipBackupAttributeToItemAtPath:demo.path];//Some DB Logic would go here[demo close];
}- (BOOL)addSkipBackupAttributeToItemAtPath:(NSString *) filePathString
{NSURL* URL= [NSURL fileURLWithPath: filePathString];assert([[NSFileManager defaultManager] fileExistsAtPath: [URL path]]);NSError *error = nil;BOOL success = [URL setResourceValue: [NSNumber numberWithBool: YES]forKey: NSURLIsExcludedFromBackupKey error: &error];if(!success){NSLog(@"Error excluding %@ from backup %@", [URL lastPathComponent], error);}return success;
}
19.查看工具
1.下載工具go get github.com/boltdb/boltd
然后編譯cmd下的main文件生成可執行文件改名為boltd
拷貝boltd到 *.db同級目錄,執行如下:
然后打開網站:
2.命令行工具
https://github.com/hasit/bolter
boltdb基礎學習暫時就這么多,下一章開始實踐