In Memory SQLite 的備份與還原
· 閱讀時間約 3 分鐘
近期在研究一個相對有趣的問題:如何讓 In-Memory SQLite 能夠在程式結束時進行自動備份,並且在程式啟動時自動套用最新的備份。
如果想要備份 SQLite,有幾種方式:
- 使用
.dump將指定表中資料匯出為 SQL- 優點:將資料轉存為 SQL,如果需要跨資料庫(例如 sqlite to mysql)的話相當方便
- 缺點:檔案為 UTF-8 純文字檔案,所需容量較大且需要轉換 SQL 故執行時間會較長
- 使用
VACUUM INTO指令,將 DB 檔案轉存為獨立的檔案- 優點:執行後會將資料碎片彙整,進一步降低所需容量;且生成的檔案可以直接被 sqlite 客戶端讀取
- 缺點:寫入時在極端情況下(例如當機或停電),並不保證資料能夠被完整寫入
- 使用 Online Backup API
- 優點:直接鏡像備份兩個 SQLite 連線;效率較
VACUUM來得高 - 缺點:沒有直接的指令,需使用 SQLite API 達成;因為是鏡像備份,所以包括源資料庫的資料碎片等亦會被保留
- 優點:直接鏡像備份兩個 SQLite 連線;效率較
備份
目的是在程式結束時自動備份,如果不考慮極端情況的話,完全可以用 VACUUM INTO 的方式進行。
func main() {
db, err := sql.Open("sqlite3", "file:/foobar?vfs=memdb")
if err != nil { panic(err) }
defer func() {
archive(db, "archive")
db.Close()
}()
for {
// do something...
}
}
func archive(db *sql.DB, dir string) (err error) {
_, err = db.Exec(fmt.Sprintf("VACUUM INTO '%s/%d.db';", dir, time.Now().UnixMicro()))
return
}
還原
在還原時,可以依賴 Online Backup API 來進行
func main() {
db, err := sql.Open("sqlite3", "file:/foobar?vfs=memdb")
if err != nil { panic(err) }
defer func() {
archive(db, "archive")
db.Close()
}()
if err := archiveLoad(conn(db), "archive"); err != nil {
panic(err) // archive load failed
}
for {
// do something...
}
}
func archiveLoad(dc *sqlite3.SQLiteConn, dir string) (err error) {
// 取得最新的 archive
var path string
if path, err = lastArchive(dir); err != nil {
return
}
var archive *sql.DB
if archive, err = sql.Open("sqlite3", path); err != nil {
return
}
// 取得 archive (source) 的 SQLite Connection
var sc *sqlite3.SQLiteConn
if sc, err = conn(archive); err != nil {
return
}
return syncAll(sc, dc)
}
func conn(db *sql.DB) (c *sqlite3.SQLiteConn, err error) {
var rawConn *sql.Conn
if rawConn, err = db.Conn(context.Background()); err != nil {
return
}
// 這其實是不好的示範,因為官方文件明確指出 driverConn 不應該在 function 以外被使用
err = rawConn.Raw(func(driverConn any) error {
var ok bool
if c, ok = driverConn.(*sqlite3.SQLiteConn); !ok {
return errors.New("failed to get sqlite3 connection")
}
return nil
})
return
}
func syncAll(src, dest *sqlite3.SQLiteConn) error {
backup, err := dest.Backup("main", src, "main")
if err != nil {
return err
}
defer func() {
if err = backup.Finish(); err != nil {
panic(err)
}
}()
var done bool
for !done {
// -1 代表備份整個資料庫
if done, err = backup.Step(-1); err != nil {
return err
}
}
return nil
}
