1. 程式人生 > >golang操作sqlite時database is locked 的坑以及rows.Close()

golang操作sqlite時database is locked 的坑以及rows.Close()

在最近的一個程式中,使用的是sqlite資料庫。涉及到多執行緒對資料庫的讀寫。因為sqlite本身有五個鎖狀態:unlocked,shared,reserved,pending,exclusive。每個事務都必須獲得相應的鎖才可以進行讀寫操作,所以一開始我自己的程式中是沒有加任何鎖的。具體sqlite的各種鎖狀態的知識百度一下就會很多很多。

我的原始碼時使用golang編寫的,使用了第三方的sqlite包github.com/mattn/go-sqlite3,但是並沒有匯入這個包,只是import _"github.com/mattn/go-sqlite3",呼叫了包裡的初始化函式,實現了database/sql包裡的相關介面,具體對於資料庫的操作還是使用的是sql包中的DB物件進行操作。

但是當代碼跑起來的時候,在進行寫資料庫操作的時候卻報了這樣的一個錯:error:database is locked。上網查了一下當事務進行併發寫操作的時候,是有可能產生死鎖的,一開始還以為是死鎖造成的,就在程式碼中在應用層對所有的事務都加了鎖,但是令人崩潰的是依舊報錯。

到最後我索性把資料庫所有的讀寫操作全部改成序列,但是令人難以置信的是,,,依然報錯。

嘗試一下呼叫rows.Close()

意思就是,當golang對關係型資料庫進行操作的時候,讀操作的程式碼一般是這樣子的:

rows,err:=db_driver.Query("select * from table")
if err!=nil{

}
for rows.Next(){
//讀取資料
}

他的建議就是呼叫rows.Close(),即:

rows,err:=db_driver.Query("select * from table")
defer rows.Close()
if err!=nil{

}
for rows.Next(){
//讀取資料
}

按照這樣的方法嘗試過以後,便不再報錯。看go的標準庫程式碼的時候發現,rows.Close()這個方法是冪等的,當rows.Next()返回false,即所有行資料都已經遍歷結束後,會自動呼叫rows.Close()方法。而我的程式碼裡面,有的地方,當rows.Next()返回true的時候,在迴圈體當中便有break程式碼,導致沒有呼叫rows.Close()方法。

為了弄清楚這其中的原理,我做了一個實驗,寫了這樣的一個小程式:

package main

import (
	"database/sql"

	"fmt"
	"time"

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

var db_driver *sql.DB

func DBInit() {
	var err error
	db_driver, err = sql.Open("sqlite3", "test.db")
	if err != nil {
		fmt.Println(err.Error())
		return
	}
}

func createDBTables() {
	create_table := `create table test(
		Seq integer primary key autoincrement,
		A text,
		B text
	);`
	_, err := db_driver.Exec(create_table)
	if err != nil {
		fmt.Println(err.Error())
		return
	}

	stmt, err := db_driver.Prepare("insert into test(A,B) values(?,?)")
	if err != nil {
		fmt.Println(err.Error())
		return
	}
	_, err = stmt.Exec("a1", "b1")
	if err != nil {
		fmt.Println(err.Error())
		return
	}

}

func main() {
	DBInit()
	createDBTables()
	rows, err := db_driver.Query("select * from test")
	if err != nil {
		fmt.Println(err.Error())
		return
	}
	for rows.Next() {
		break
	}

	stmt, err := db_driver.Prepare("insert into test (A,B) values(?,?)")
	if err != nil {
		fmt.Println(err.Error())
		return
	}

	fmt.Println("insert")
	fmt.Println(time.Now())
	_, err = stmt.Exec("a2", "b2")
	if err != nil {
		fmt.Println(time.Now())
		fmt.Println(err.Error())
		stmt.Close()
	}

	for {

	}
}

在資料庫初始化的時候,便在資料庫中插入一條記錄,在main方法中,序列地對資料庫進行讀操作和寫操作,其中,由於資料庫中已經有一條記錄,第一次呼叫rows.Next()返回的是true,此時直接break,即rows沒有呼叫Close()方法,此時再次進行寫操作,便報出了database is locked的錯誤。如果加上rows.Close()便不再報錯。

sql包中的Stmt結構體代表了一個事務,呼叫Close()方法代表關閉一個事務,而rows的資料型別是*sql.Rows,rows呼叫Close()方法代表讀結束。

sqlite使用的是粗放型的檔案鎖,對併發讀支援地很好而對於併發寫支援得並不好。在sqlite當中,寫事務需要等待所有的讀事務釋放了共享鎖(讀鎖)以後才可以進行寫操作,如果一個讀事務持續太長時間還沒有結束的話,其他等待的事務有可能就會一直停住。為了解決這個問題,引入了超時的機制,如果一個事務等待其他事務釋放鎖的時間超過5秒的話,就會丟擲database is locked的錯誤。

在上面的程式碼中,我從開始執行寫操作的時候列印了一個時間,在報錯的時候又列印了一個時間,這個時間差正好是5秒,於是便不難解釋這種現象:在main函式中,第一個讀事務拿到了sqlite的共享鎖(讀鎖)開始了讀操作,但是沒有呼叫rows.Close()方法,所以說第一個讀事務沒有釋放共享鎖,第二個寫事務必須要等待第一個事務釋放共享鎖。5秒之後第一個讀事務還沒有釋放共享鎖,所以直接報出database is locked的錯誤。

此時,第一個事務還沒有釋放共享鎖,但此時,其他讀事務依舊可以獲得共享鎖進行讀操作,而沒有辦法進行寫操作。從命令列訪問資料庫也驗證了這個猜想: