這是一個建立於 的文章,其中的資訊可能已經有所發展或是發生改變。
上一章中我們一起探討了golangdatabase/sql
包中如何擷取一個真實的資料庫連接。當我們拿到一個資料庫連接之後就可以開始真正的資料庫操作了。本章講繼續深入,一起探討底層是如何進行資料庫操作的。
上一章中我們說到:
db.Query()
實際上分為兩步:
- 擷取資料庫連接
- 在此串連上利用driver進行實際的DB操作
func (db *DB) query(query string, args []interface{}, strategy connReuseStrategy) (*Rows, error) { ci, err := db.conn(strategy) if err != nil { return nil, err } return db.queryConn(ci, ci.releaseConn, query, args)}
那我們就一起來看看db.queryConn
其實sql包最核心的就是維護了串連池,對於實際的操作,都是利用Driver去完成。因此代碼實現也一樣,堅持一個原則:
組裝Driver需要的參數,執行Driver的方法
db.queryConn
虛擬碼如下:
func (db *DB) queryConn(dc *driverConn, releaseConn func(error), query string, args []interface{}) (*Rows, error) { if queryer, ok := dc.ci.(driver.Queryer); ok { dargs, err := driverArgs(nil, args) if err != nil { releaseConn(err) return nil, err } dc.Lock() rowsi, err := queryer.Query(query, dargs) dc.Unlock() if err != driver.ErrSkip { if err != nil { releaseConn(err) return nil, err } // Note: ownership of dc passes to the *Rows, to be freed // with releaseConn. rows := &Rows{ dc: dc, releaseConn: releaseConn, rowsi: rowsi, } return rows, nil } } dc.Lock() si, err := dc.ci.Prepare(query) dc.Unlock() if err != nil { releaseConn(err) return nil, err } ds := driverStmt{dc, si} rowsi, err := rowsiFromStatement(ds, args...) if err != nil { dc.Lock() si.Close() dc.Unlock() releaseConn(err) return nil, err } // Note: ownership of ci passes to the *Rows, to be freed // with releaseConn. rows := &Rows{ dc: dc, releaseConn: releaseConn, rowsi: rowsi, closeStmt: si, } return rows, nil}
queryConn的實現可以分為兩部分來看:
- Driver實現了
Queryer
介面
- Driver沒有實現該介面,走
Stmt三部曲
Queryer
Queryer
介面很能體現golang內部命名interface的風格,比如Reader
、Writer
等,Queryer
要求實現一個Query
方法。如果Driver實現了這個Query
方法,那麼sql包只需要把它需要的參數準備好然後傳給它就行了。
driverArgs
用來準備Query
需要的參數,實際上就是把各種類型的值利用反射轉換成它所在類型的最大類型
。這句話有點不好理解,簡單點講就是把int int8 uint uint16 int16
等轉換為int64
,把floatX
轉換為float64
。最終,driverArgs
會把所有類型轉化為以下幾種
[]byte
bool
float64
int64
string
time.Time
思考①:
為什麼要進行資料轉換
準備好參數之後就調用Driver實現好的Query方法。
dc.Lock()rowsi, err := queryer.Query(query, dargs)dc.Unlock()
最終的請求很簡單,因為工作量都在driver,但是問題也來了
問題②:
這裡為什麼要加鎖?
每個Query
都會先擷取串連再進行Query,如果串連池是安全執行緒的,對於取到串連的後續行為還需要加鎖嗎?
調用Driver的Query方法執行完Query請求就拿到了rowsi(Driver.Rows
),將它包一層包成sql.Rows
返回給caller。
// Note: ownership of dc passes to the *Rows, to be freed// with releaseConn.rows := &Rows{ dc: dc, releaseConn: releaseConn, rowsi: rowsi,}return rows, nil
至此呢,一個真實的請求就處理完畢了。實際上對於sql包來說非常簡單,工作量都在各種不同的Driver裡。
Stmt
正如文檔所說,Queryer
介面是可選的:
Queryer is an optional interface that may be implemented by a Conn.
If a Conn does not implement Queryer, the sql package's DB.Query will first prepare a query, execute the statement, and then close the statement.
所以對於那些偷懶的Driver來說,執行一個Query請求就得用Stmt
了。
dc.Lock()si, err := dc.ci.Prepare(query)dc.Unlock()
Prepare
方法產生一個Stmt
。當然這裡同樣有相同的問題需要你思考一下,這裡加鎖是否有必要。可以先看看Stmt
的定義:
// Stmt is a prepared statement. It is bound to a Conn and not// used by multiple goroutines concurrently.type Stmt interface { // Close closes the statement. // // As of Go 1.1, a Stmt will not be closed if it's in use // by any queries. Close() error // NumInput returns the number of placeholder parameters. // // If NumInput returns >= 0, the sql package will sanity check // argument counts from callers and return errors to the caller // before the statement's Exec or Query methods are called. // // NumInput may also return -1, if the driver doesn't know // its number of placeholders. In that case, the sql package // will not sanity check Exec or Query argument counts. NumInput() int // Exec executes a query that doesn't return rows, such // as an INSERT or UPDATE. Exec(args []Value) (Result, error) // Query executes a query that may return rows, such as a // SELECT. Query(args []Value) (Rows, error)}
可以看到Stmt
的方法也很簡單,Exec
和Query
是最終執行請求會需要用到的方法。NumInput
用來統計sql語句中預留位置的數量。
很多人之前可能都比較疑惑Stmt
是用來幹什麼的,看到這裡應該明白了。事實上Stmt
就是一個sql語句的模板,模板固定,只是參數在變化,這種情境就特別適合用Stmt
,你不再需要把sql語句複製幾遍。
拿到Stmt
之後,通過執行Stmt
的Query
方法,也能拿到結果rows。進行Query之前也需要buildParams以及檢查參數和sql語句的placeholder是否匹配等,所以進行了一個簡單封裝:
ds := driverStmt{dc, si}rowsi, err := rowsiFromStatement(ds, args...)
si就是Stmt
了為什麼還要包成driverStmt
,而driverStmt
又是什麼呢?其實主要還是為了在rowsiFromStatement
方法中執行Query
是加鎖。參照Queryer
中的代碼,執行Query
時是需要加鎖的,這把鎖是dc提供的,所以封裝一個driverStmt
變相讓Stmt
有了加鎖的方法:
// driverStmt associates a driver.Stmt with the// *driverConn from which it came, so the driverConn's lock can be// held during calls.type driverStmt struct { sync.Locker // the *driverConn si driver.Stmt}
rowsiFromStatement
內部執行完Query後也拿到了Driver.Rows
,如之前一樣封裝成sql.Rows
返回給caller
就好。
至此,我們已經一起探究了golang的sql包是如何處理Query請求的了。但是還是有一個問題一直貫穿著整個過程,就是:
為什麼要加鎖
如果只是看Query方法可以還不好理解,但是看了Stmt之後應該就可以理解了。Stmt是可以多次利用的,每個Stmt包含了conn,可以把一個Stmt看成一個資料庫連接。有了資料庫連接的概念,使用者如果在多個goroutine中使用這個Stmt,就會有並發的問題,因此通過Stmt進行Query或者Exec是需要加鎖的。
但是對於實現了Queryer
介面的Driver來說,使用者調用db.Query
後每次都會取新的串連然後再進行Query,最後返回一個Rows。對使用者來說直接Query的整個過程並沒有串連的概念,因此我個人覺得是安全的。這裡需不需要加鎖有待商榷。如果覺得需要加鎖歡迎留言和我討論
Tx
Tx
實際上和上面是一樣的,主要也是建立時先請求一個conn,然後基於這個conn封裝一個Tx對象。後續的操作都要依賴於底層的資料庫。
Tx需要特別注意的是:
如果後端的資料庫proxy,就不能使用資料庫事務
這和golang無關,所有語言都一樣。因為我們無法保證我們對一個事務的請求都落到同一台機器。
關於golang的sql包,到這兒也將告一段落了。其實它的核心就是:
- 維護了資料庫連接池
- 定義了一系列介面規範,讓Driver可以面向介面進行開發
接下來有時間的話,我寫一篇文章來分析go-sql-driver/mysql
,不過底層的實現相對而言會比較無聊,主要都是實現mysql通訊協定
的規範,按照規範收發報文。
golang1.8 sql包中新增了不少介面,這很令人期待,更簡化了我們對於資料庫的使用,方便進行一些進階的封裝,而不用層層反射。不過目前各Driver的支援是一個大問題。