Golang Mysql筆記(三)--- Prepared剖析

## prepare

前面我們已經(jīng)學(xué)習(xí)了sql的基本curd操作抗悍〗稍ǎ總體而言,有兩類操作Query和Exec。前者返回?cái)?shù)據(jù)庫記錄田柔,后者返回?cái)?shù)據(jù)庫影響或插入相關(guān)的結(jié)果俐巴。上面兩種操作,多數(shù)是針對單次操作的查詢硬爆。如果需要批量插入一堆數(shù)據(jù)欣舵,就可以使用Prepared語句。golang處理prepared語句有其獨(dú)特的行為缀磕,了解其底層的實(shí)現(xiàn)缘圈,對于用好它十分重要。

查詢

我們可以使用Query方式查詢記錄袜蚕,Query函數(shù)提供了兩種選擇糟把,第一種情況下參數(shù)是拼接好的sql牲剃,另外一種情況遣疯,第一參數(shù)是帶有占位符的sql,第二個(gè)參數(shù)為sql的實(shí)際參數(shù)凿傅。

rows, err := db.Query("SELECT * FROM user WHERE gid = 1")
 
rows, err := db.Query("SELECT * FROM user WHERE gid = ?", 1)

上面兩種方式都能獲取數(shù)據(jù)缠犀,那么他們的底層實(shí)現(xiàn)是一樣的么?實(shí)際上聪舒,上面兩種方式的底層通信不完全一樣辨液。一種你是plaintext方式,另外一種是prepared方式箱残。

prepared

所謂prepared滔迈,即帶有占位符的sql語句,客戶端將該語句和參數(shù)發(fā)給mysql服務(wù)器。mysql服務(wù)器編譯成一個(gè)prepared語句亡鼠,這個(gè)語句可以根據(jù)不同的參數(shù)多次調(diào)用赏殃。prepared語句執(zhí)行的方式如下:

  1. 準(zhǔn)備prepare語句
  2. 執(zhí)行prepared語句和參數(shù)
  3. 關(guān)閉prepared語句

之所以會出現(xiàn)prepare語句方式,主要因?yàn)檫@樣有下面的兩個(gè)好處:

  1. 避免通過引號組裝拼接sql語句间涵。避免sql注入帶來的安全風(fēng)險(xiǎn)
  2. 可以多次執(zhí)行的sql語句仁热。

單純的看prepared語句發(fā)好處,會下意識的覺得既然如此勾哩,都使用prepared語句查詢不就好了么抗蠢?其實(shí)不然。關(guān)于prepared語句注意事項(xiàng)思劳,稍后再討論迅矛。

golang的pliantext和prepare查詢方式

現(xiàn)在我們再回顧上面調(diào)用Qeury函數(shù)的兩個(gè)操作。對于第一個(gè)操作潜叛,執(zhí)行pliantext的sql語句秽褒。先看db.Query方法:

// Query executes a query that returns rows, typically a SELECT.
// The args are for any placeholder parameters in the query.
func (db *DB) Query(query string, args ...interface{}) (*Rows, error) {
 var rows *Rows
 var err error
 for i := 0; i < maxBadConnRetries; i++ {
  rows, err = db.query(query, args, cachedOrNewConn)    // 查詢
  if err != driver.ErrBadConn {
   break
  }
 }
 if err == driver.ErrBadConn {
  return db.query(query, args, alwaysNewConn)
 }
 return rows, err
}

Query方法我們很熟悉了,它的內(nèi)部調(diào)用了db.query方法威兜,并且根據(jù)連接重連的狀況選擇是cachedOrNewConn模式還是alwaysNewConn模式销斟。前者會從返回一個(gè)cached連接或者等待一個(gè)可用連接,甚至也可能建立一個(gè)新的連接椒舵;后者表示打開連接時(shí)的策略為每次建立一個(gè)新的連接蚂踊。這就是簽名所說的retry10次連接。

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)
}

query方法邏輯很簡單笔宿,通過db.conn方法返回一個(gè)新創(chuàng)建或者緩存的空閑連接犁钟。driverConn。隨機(jī)調(diào)用queryConn方法泼橘。

// queryConn executes a query on the given connection.
// The connection gets released by the releaseConn function.
func (db *DB) queryConn(dc *driverConn, releaseConn func(error), query string, args []interface{}) (*Rows, error) {

 // 判斷驅(qū)動是否實(shí)現(xiàn)了Queryer
 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)  // 調(diào)用驅(qū)動的查詢方法  connection.go 第305行
  dc.Unlock()
  if err != driver.ErrSkip {  // 不帶參數(shù)的返回
   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)  // 帶參數(shù)的返回涝动,創(chuàng)建prepare對象
 dc.Unlock()
 if err != nil {
  releaseConn(err)
  return nil, err
 }

 ds := driverStmt{dc, si}
 rowsi, err := rowsiFromStatement(ds, args...)   // 執(zhí)行語句
 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函數(shù)內(nèi)容比較多。先判斷驅(qū)動是否實(shí)現(xiàn)了Queryer侥加,如果實(shí)現(xiàn)了即調(diào)用其Query方法捧存。方法會針對sql查詢語句做查詢。例如mysql的驅(qū)動如下担败,connection.go 第305行左右昔穴,即:

func (mc *mysqlConn) Query(query string, args []driver.Value) (driver.Rows, error) {
 if mc.netConn == nil {
  errLog.Print(ErrInvalidConn)
  return nil, driver.ErrBadConn
 }
 if len(args) != 0 {
  if !mc.cfg.InterpolateParams {
   return nil, driver.ErrSkip
  }
  // try client-side prepare to reduce roundtrip
  prepared, err := mc.interpolateParams(query, args)
  if err != nil {
   return nil, err
  }
  query = prepared
  args = nil
 }
 // Send command
 err := mc.writeCommandPacketStr(comQuery, query)
 if err == nil {
  // Read Result
  var resLen int
  resLen, err = mc.readResultSetHeaderPacket()
  if err == nil {
   rows := new(textRows)
   rows.mc = mc

   if resLen == 0 {
    // no columns, no more data
    return emptyRows{}, nil
   }
   // Columns
   rows.columns, err = mc.readColumns(resLen)
   return rows, err
  }
 }
 return nil, err
}

Query先檢查參數(shù)是否為0,然后調(diào)用writeCommandPacketStr方法執(zhí)行sql并通過readResultSetHeaderPacket讀取數(shù)據(jù)庫服務(wù)返回的結(jié)果提前。
如果參數(shù)不為0吗货,會先判斷是否是prepared語句。這里會返回一個(gè)driver.ErrSkip錯(cuò)誤狈网。把函數(shù)執(zhí)行權(quán)再返回到queryConn函數(shù)中宙搬。然后再調(diào)用si, err := dc.ci.Prepare(query)創(chuàng)建Stmt對象笨腥,接下來調(diào)用rowsiFromStatement執(zhí)行查詢:

func rowsiFromStatement(ds driverStmt, args ...interface{}) (driver.Rows, error) {
 ds.Lock()
 want := ds.si.NumInput()
 ds.Unlock()

 // -1 means the driver doesn't know how to count the number of
 // placeholders, so we won't sanity check input here and instead let the
 // driver deal with errors.
 if want != -1 && len(args) != want {
  return nil, fmt.Errorf("sql: statement expects %d inputs; got %d", want, len(args))
 }

 dargs, err := driverArgs(&ds, args)
 if err != nil {
  return nil, err
 }

 ds.Lock()
 rowsi, err := ds.si.Query(dargs)
 ds.Unlock()
 if err != nil {
  return nil, err
 }
 return rowsi, nil
}

rowsiFromStatement方法會調(diào)用驅(qū)動的ds.si.Query(dargs)方法,執(zhí)行最后的查詢勇垛。大概再statement.go的第84行

func (stmt *mysqlStmt) Query(args []driver.Value) (driver.Rows, error) {
 if stmt.mc.netConn == nil {
  errLog.Print(ErrInvalidConn)
  return nil, driver.ErrBadConn
 }

 // Send command
 err := stmt.writeExecutePacket(args)
 if err != nil {
  return nil, err
 }

 mc := stmt.mc

 // Read Result
 resLen, err := mc.readResultSetHeaderPacket()
 if err != nil {
  return nil, err
 }

 rows := new(binaryRows)

 if resLen > 0 {
  rows.mc = mc
  // Columns
  // If not cached, read them and cache them
  if stmt.columns == nil {
   rows.columns, err = mc.readColumns(resLen)
   stmt.columns = rows.columns
  } else {
   rows.columns = stmt.columns
   err = mc.readUntilEOF()
  }
 }

 return rows, err
}

調(diào)用 stmt和參數(shù)執(zhí)行sql查詢脖母。查詢完畢之后,返回到queryConn方法中闲孤,使用releaseConn釋放查詢的數(shù)據(jù)庫連接谆级。

自定義prepare 查詢

從query查詢可以看到,對于占位符的prepare語句讼积,go內(nèi)部通過的dc.ci.Prepare(query)會自動創(chuàng)建一個(gè) stmt對象肥照。其實(shí)我們也可以自定義stmt語句,使用方式如下:

 stmt, err := db.Prepare("SELECT * FROM user WHERE gid = ?")
 if err != nil {
  log.Fatalln(err)
 }
 defer stmt.Close()

 rows, err :=  stmt.Query(1)
 if err != nil{
  log.Fatalln(err)
 }

即通過Prepare方法創(chuàng)建一個(gè)stmt對象勤众,然后執(zhí)行stmt對象的Query(Exec)方法得到sql.Rows結(jié)果集舆绎。最后關(guān)閉stmt.Close。這個(gè)過程就和之前所說的prepare三步驟匹配了们颜。

創(chuàng)建stmt的preprea方式是golang的一個(gè)設(shè)計(jì)吕朵,其目的是Prepare once, execute many times。為了批量執(zhí)行sql語句窥突。但是通常會造成所謂的三次網(wǎng)絡(luò)請求( three network round-trips)边锁。即preparing executing和closing三次請求。

對于大多數(shù)數(shù)據(jù)庫波岛,prepread的過程都是,先發(fā)送一個(gè)帶占位符的sql語句到服務(wù)器音半,服務(wù)器返回一個(gè)statement id则拷,然后再把這個(gè)id和參數(shù)發(fā)送給服務(wù)器執(zhí)行,最后再發(fā)送關(guān)閉statement命令曹鸠。

golang的實(shí)現(xiàn)了連接池煌茬,處理prepare方式也需要特別注意。調(diào)用Prepare方法返回stmt的時(shí)候彻桃,golang會在某個(gè)空閑的連接上進(jìn)行prepare語句坛善,然后就把連接釋放回到連接池,可是golang會記住這個(gè)連接邻眷,當(dāng)需要執(zhí)行參數(shù)的時(shí)候眠屎,就再次找到之前記住的連接進(jìn)行執(zhí)行,等到stmt.Close調(diào)用的時(shí)候肆饶,再釋放該連接改衩。

在執(zhí)行參數(shù)的時(shí)候,如果記住的連接正處于忙碌階段驯镊,此時(shí)golang將會從新選一個(gè)新的空閑連接進(jìn)行prepare(re-prepare)葫督。當(dāng)然竭鞍,即使是重新reprepare,同樣也會遇到剛才的問題橄镜。那么將會一而再再而三的進(jìn)行reprepare偎快。直到找到空閑連接進(jìn)行查詢的時(shí)候。

這種情況將會導(dǎo)致leak連接的情況洽胶,尤其是再高并發(fā)的情景晒夹。將會導(dǎo)致大量的prepare過程。因此使用stmt的情況需要仔細(xì)考慮應(yīng)用場景妖异,通常在應(yīng)用程序中惋戏。多次執(zhí)行同一個(gè)sql語句的情況并不多,因此減少prepare語句的使用他膳。

之前有一個(gè)疑問响逢,是不是所有sql語句都不能帶占位符,因?yàn)檫@是prepare語句棕孙。只要看了一遍database/sql和驅(qū)動的源碼才恍然大悟舔亭,對于query(prepare, args)的方式,正如我們前面所分析的蟀俊,database/sql會使用ds.si.Query(dargs)創(chuàng)建stmt的钦铺,然后就立即執(zhí)行prepare和參數(shù),最后關(guān)閉stmt肢预。整個(gè)過程都是同一個(gè)連接上完成矛洞,因此不存在reprepare的情況。當(dāng)然也無法使用所謂的創(chuàng)建一次烫映,執(zhí)行多次的目沼本。

對于prepare的使用方式,基于其好處和缺點(diǎn)锭沟,我們將會再后面的最佳實(shí)踐再討論抽兆。目前需要注意的大致就是:

  1. 單次查詢不需要使用prepared,每次使用stmt語句都是三次網(wǎng)絡(luò)請求次數(shù)族淮,prepared execute close

  2. 不要循環(huán)中創(chuàng)建prepare語句

  3. 注意關(guān)閉 stmt

盡管會有reprepare過程辫红,這些操作依然是database/sql幫我們所做的,與連接retry10次一樣祝辣,開發(fā)者無需擔(dān)心贴妻。

對于Qeruy操作如此,同理Exec操作也一樣蝙斜。

總結(jié)

目前我們學(xué)習(xí)database/sql提供兩類查詢操作揍瑟,Query和Exec方法。他們都可以使用plaintext和preprea方式查詢乍炉。對于后者绢片,可以有效的避免數(shù)據(jù)庫注入滤馍。而prepare方式又可以有顯示的聲明stmt對象,也有隱藏的方式底循。顯示的創(chuàng)建stmt會有3次網(wǎng)絡(luò)請求巢株,創(chuàng)建->執(zhí)行->關(guān)閉,再批量操作可以考慮這種做法熙涤,另外一種方式創(chuàng)建prepare后就執(zhí)行阁苞,因此不會因?yàn)閞eprepare導(dǎo)致高并發(fā)下的leak連接問題。

具體使用那種方式祠挫,還得基于應(yīng)用場景那槽,安全過濾和連接管理等考慮。至此等舔,關(guān)于查詢和執(zhí)行操作已經(jīng)介紹了很多骚灸。關(guān)系型數(shù)據(jù)庫的另外一個(gè)特性就是關(guān)系和事務(wù)處理。下一節(jié)慌植,我們將會討論database/sql的數(shù)據(jù)庫事務(wù)功能甚牲。

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
  • 序言:七十年代末,一起剝皮案震驚了整個(gè)濱河市蝶柿,隨后出現(xiàn)的幾起案子丈钙,更是在濱河造成了極大的恐慌,老刑警劉巖交汤,帶你破解...
    沈念sama閱讀 218,122評論 6 505
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件雏赦,死亡現(xiàn)場離奇詭異,居然都是意外死亡芙扎,警方通過查閱死者的電腦和手機(jī)喉誊,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 93,070評論 3 395
  • 文/潘曉璐 我一進(jìn)店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來纵顾,“玉大人,你說我怎么就攤上這事栋盹∈┯猓” “怎么了?”我有些...
    開封第一講書人閱讀 164,491評論 0 354
  • 文/不壞的土叔 我叫張陵例获,是天一觀的道長汉额。 經(jīng)常有香客問我,道長榨汤,這世上最難降的妖魔是什么蠕搜? 我笑而不...
    開封第一講書人閱讀 58,636評論 1 293
  • 正文 為了忘掉前任,我火速辦了婚禮收壕,結(jié)果婚禮上妓灌,老公的妹妹穿的比我還像新娘轨蛤。我一直安慰自己,他們只是感情好虫埂,可當(dāng)我...
    茶點(diǎn)故事閱讀 67,676評論 6 392
  • 文/花漫 我一把揭開白布祥山。 她就那樣靜靜地躺著,像睡著了一般掉伏。 火紅的嫁衣襯著肌膚如雪缝呕。 梳的紋絲不亂的頭發(fā)上,一...
    開封第一講書人閱讀 51,541評論 1 305
  • 那天斧散,我揣著相機(jī)與錄音供常,去河邊找鬼。 笑死鸡捐,一個(gè)胖子當(dāng)著我的面吹牛栈暇,可吹牛的內(nèi)容都是我干的。 我是一名探鬼主播闯参,決...
    沈念sama閱讀 40,292評論 3 418
  • 文/蒼蘭香墨 我猛地睜開眼瞻鹏,長吁一口氣:“原來是場噩夢啊……” “哼!你這毒婦竟也來了鹿寨?” 一聲冷哼從身側(cè)響起新博,我...
    開封第一講書人閱讀 39,211評論 0 276
  • 序言:老撾萬榮一對情侶失蹤,失蹤者是張志新(化名)和其女友劉穎脚草,沒想到半個(gè)月后赫悄,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體,經(jīng)...
    沈念sama閱讀 45,655評論 1 314
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡馏慨,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 37,846評論 3 336
  • 正文 我和宋清朗相戀三年埂淮,在試婚紗的時(shí)候發(fā)現(xiàn)自己被綠了。 大學(xué)時(shí)的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片写隶。...
    茶點(diǎn)故事閱讀 39,965評論 1 348
  • 序言:一個(gè)原本活蹦亂跳的男人離奇死亡倔撞,死狀恐怖,靈堂內(nèi)的尸體忽然破棺而出慕趴,到底是詐尸還是另有隱情痪蝇,我是刑警寧澤,帶...
    沈念sama閱讀 35,684評論 5 347
  • 正文 年R本政府宣布冕房,位于F島的核電站躏啰,受9級特大地震影響,放射性物質(zhì)發(fā)生泄漏耙册。R本人自食惡果不足惜给僵,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 41,295評論 3 329
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望详拙。 院中可真熱鬧帝际,春花似錦蔓同、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 31,894評論 0 22
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽。三九已至侧甫,卻和暖如春珊佣,著一層夾襖步出監(jiān)牢的瞬間,已是汗流浹背披粟。 一陣腳步聲響...
    開封第一講書人閱讀 33,012評論 1 269
  • 我被黑心中介騙來泰國打工咒锻, 沒想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留,地道東北人守屉。 一個(gè)月前我還...
    沈念sama閱讀 48,126評論 3 370
  • 正文 我出身青樓惑艇,卻偏偏與公主長得像,于是被迫代替她去往敵國和親拇泛。 傳聞我的和親對象是個(gè)殘疾皇子滨巴,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 44,914評論 2 355

推薦閱讀更多精彩內(nèi)容