事情是这样的。

我们的项目使用的是sqlite3数据库。在一次常规模拟测试中,我们想了解,如果程序正在运行中,用户将sqlite3数据库文件删除,程序能否进入预设的重置流程。

结果,我们却让我们有些意外。

try

于是我写了一段go代码模拟一下该过程。

package main

import (
	"database/sql"
	"net/http"
	"time"

	_ "github.com/mattn/go-sqlite3"
)


func main() {
    // 首先,我们打开个数据库
	db, err := sql.Open("sqlite3", "file:test.sqlite")
	if err != nil {
		panic(err)
	}
	defer db.Close()

    // 建个表
	_, err = db.Exec(`CREATE TABLE IF NOT EXISTS test_t(
		a TEXT,
		create_at TIMESTAMP default CURRENT_TIMESTAMP
	);`)

	if err != nil {
		panic(err)
	}


    // 我们通过API来简单控制一些插入,查询
	http.HandleFunc("/test", func(w http.ResponseWriter, r *http.Request) {
		if r.FormValue("query") != "" {
			rows, err := db.Query("SELECT * FROM test_t")
			if err != nil {
				w.Write([]byte(err.Error()))
				return
			}

			for rows.Next() {
				var a string
				var t time.Time
				err := rows.Scan(&a, &t)
				if err != nil {
					w.Write([]byte(err.Error()))
					return
				}

				w.Write([]byte(a + " | " + t.Format("2006-01-02 15:04:05"+"\r\n")))
			}
			return
		}

		if v := r.FormValue("insert"); v != "" {
			_, err := db.Exec("INSERT INTO test_t (a) VALUES (?)", v)
			if err != nil {
				w.Write([]byte(err.Error()))
				return
			}
			w.Write([]byte("OK"))
			return
		}

		w.Write([]byte("NO"))
	})

	http.ListenAndServe(":10888", nil)
}

该段代码是简单的通过API来访问数据库内容。

我们的重点不是API部分,而是我们开启了一个数据库的连接,然后我们将数据库文件删除后会有什么现象发生呢?

首先,我们先向插入一些数据。

$ curl "http://127.0.0.1:10888/test?insert=ddd"
OK 

然后我们查询一下。

$ curl "http://127.0.0.1:10888/test?query=xxx"

ddd | 2018-09-10 16:24:32
ddd | 2018-09-10 16:24:33
ddd | 2018-09-10 16:24:34
ddd | 2018-09-10 16:24:34
ddd | 2018-09-10 16:24:35
ddd | 2018-09-10 16:24:35

我们看到已经能查到数据。

$ ls 
...
test.sqlite

我们看到,当前目录已经生成了一个test.sqlite文件。

我们看看该进程在系统的状态。

jietu20180911-003048

我看看到,该进程打开了.../go/src/test.sqlite数据库文件。

现在,我们将文件删了。

$ rm -rf test.sqlite

$ ls
...

我们再次调取查询接口。

$ curl "http://127.0.0.1:10888/test?query=xxx"

ddd | 2018-09-10 16:24:32
ddd | 2018-09-10 16:24:33
ddd | 2018-09-10 16:24:34
ddd | 2018-09-10 16:24:34
ddd | 2018-09-10 16:24:35
ddd | 2018-09-10 16:24:35

我们发现数据还在。

我们再试试插入数据。

$ curl "http://127.0.0.1:10888/test?insert=ddd"
attempt to write a readonly database

此时,我们发现,没有写入成功,而是显示了一个错误。

我们在查看一下进程打开的文件。

jietu20180911-003846

我们发现居然是一样的。

ls: .../go/src/test.sqlite: No such file or directory

我们发现并没有该文件。

try & try

我们先不忙者下结论,我们再看看另外一种写法。

那就是每次使用完数据库就关闭连接,下次使用再打开。

代码如下。

package main

import (
	"database/sql"
	"net/http"
	"time"

	_ "github.com/mattn/go-sqlite3"
)

func getDB() *sql.DB {
	db, err := sql.Open("sqlite3", "file:test.sqlite")
	if err != nil {
		panic(err)
	}
	return db
}

func main() {
	db := getDB()
	_, err := db.Exec(`CREATE TABLE IF NOT EXISTS test_t(
		a TEXT,
		create_at TIMESTAMP default CURRENT_TIMESTAMP
	);`)

	if err != nil {
		panic(err)
	}

	db.Close()

	http.HandleFunc("/test", func(w http.ResponseWriter, r *http.Request) {
		db := getDB()
		defer db.Close()
		if r.FormValue("query") != "" {
			rows, err := db.Query("SELECT * FROM test_t")
			if err != nil {
				w.Write([]byte(err.Error()))
				return
			}

			for rows.Next() {
				var a string
				var t time.Time
				err := rows.Scan(&a, &t)
				if err != nil {
					w.Write([]byte(err.Error()))
					return
				}

				w.Write([]byte(a + " | " + t.Format("2006-01-02 15:04:05"+"\r\n")))
			}
			return
		}

		if v := r.FormValue("insert"); v != "" {
			_, err := db.Exec("INSERT INTO test_t (a) VALUES (?)", v)
			if err != nil {
				w.Write([]byte(err.Error()))
				return
			}
			w.Write([]byte("OK"))
			return
		}

		w.Write([]byte("NO"))
	})

	http.ListenAndServe(":10888", nil)
}

我们先跑起来。

$ go run test-sqlite2.go

向其中插入一些数据。

$ curl "http://127.0.0.1:10888/test?insert=ddd"
OK

查询一下。

$ curl "http://127.0.0.1:10888/test?query=xxx"
ddd | 2018-09-10 23:29:42
ddd | 2018-09-10 23:29:43
ddd | 2018-09-10 23:29:43
ddd | 2018-09-10 23:29:44

我们将数据库删除。

$ rm test.sqlite 

再次查询。

$ curl "http://127.0.0.1:10888/test?query=xxx"
no such table: test_t

此时,给我们表现是,该表已经不存在了。

我们再查看一下数据库,发现数据库居然还在。

$ sqlite3 test.sqlite
SQLite version 3.19.3 2017-06-27 16:48:08
Enter ".help" for usage hints.
sqlite > select count(*) from sqlite_master;
0
sqlite >

虽然库文件依然存在(应该是查询是sql.Open创建的),但其中的数据,表都不存在了。

so

通过两种方法的对比,我们比较容易想到, 在第一种操作中,由于我们打开一个数据库连接,始终没有关闭,即使数据库删除后,其引用仍然存在,文件还没留在内存中。后一种,我们每次操作完成后会关闭数据库,导致数据库直接被删除了。

但,如果数据库很大,内存放不了那么多怎么办?

所以,这个还有待验证,先记录一下,留个坑给自己。