前言

golang可以輕易制造高並發,在某些場景很合適,比如爬蟲的時候可以爬的更加高效。但是對應某些場景,如文件讀寫,數據庫訪問等IO為瓶頸的場合,就沒有什么優勢了。

前提基礎

1、golang數據庫訪問

在golang中數據庫訪問使用”database/sql”包提供的接口,不同的數據庫,比如pg、mysql只需要提供對應的驅動就可以了。注意”database/sql”包提供的接口只針對關系型數據庫,nosql如redis和mongodb都是直接使用對應的客戶端包,不實現”database/sql”包提供的接口。關於”database/sql”包,這里不做講述,后續在基礎回顧上鞏固下。總體上就是提供了連接、事務處理、還有就是打開的時候注意打開的時候並沒有連接,而是產生一個池,每次有交互的時候才產生一個連接(事務交互除外)。

2、數據庫插入優化基礎

1)插入無索引表會比插入有索引的表快,畢竟建立索引總是要增加一些額外操作

2)插入小表比插入大表快,業務一般插入速度是以條數計算,大表一條記錄比較大,需要IO的時間比較長。

3)多個連接一起插入會比單連接快,因為mysql不是單線程。

4)日志緩存增大可以加快插入速度,因為減少了IO訪問次數。

5)一次插入多條數據可以加快插入速度。

實踐經驗

ps:以小表做實驗,都用一個環境,比較差異。

表結構:

create table lamp( id bigint not null primary key, state char(1), collecttime timestamp);

1、無任何優化,一條條插入,且使用同一個鏈接

代碼片段:

fmt.Println(time.Now().Unix())

_, err = db.Prepare("INSERT INTO lamp (id, state, collecttime)VALUES(?,'0', '20180103002930')")

if err != nil {

fmt.Println(err)

return

}

for i := 0; i < 100000; i++ {

_, err := db.Exec(execstring + data)

if err != nil {

fmt.Println(err)

return

}

}

fmt.Println(time.Now().Unix())

結果:

1514911765

1514912248

使用了483s 平均100000/500 大概是200次每秒。

2、單連接,使用事務。

fmt.Println(time.Now().Unix())

insert, err = db.Prepare("INSERT INTO lamp (id, state, collecttime)VALUES(?,'0', '20180103002930')")

if err != nil {

fmt.Println(err)

return

}

begin, err := db.Begin()

if err != nil {

fmt.Println(err)

return

}

for i := 0; i < 100000; i++ {

_, err := begin.Stmt(insert).Exec(i)

if err != nil {

fmt.Println(err)

return

}

}

err = begin.Commit()

if err != nil {

fmt.Println(err)

return

}

fmt.Println(time.Now().Unix())

運行結果

1514910923

1514911049

使用了129s 平均100000/125, 約為800次每秒

3、批量插入,每1W條執行一次插入操作。,注意max_allowed_packet要設置的足夠大

fmt.Println(time.Now().Unix())

for i := 0; i < 1000; i++ {

for j := i * 10000; j < i*10000+10000; j++ {

if j < i*10000+9999 {

id := strconv.Itoa(j)

onedata := "(" + id + ", '0', '20180103002930'), "

data = data + onedata

} else {

id := strconv.Itoa(j)

onedata := "(" + id + ",'0', '20180103002930')"

data = data + onedata

}

}

_, err := db.Exec(execstring + data)

if err != nil {

fmt.Println(err)

return

}

}

fmt.Println(time.Now().Unix())

結果:

1514969811

1514970318

使用了507s 平均10000000/500, 約為2W次每秒

4、並發插入,使用100個協程插入

fmt.Println(time.Now().Unix())

intertnumber := 0

for i := 0; i < 10; i++ {

value := i

go func() {

execstring := "INSERT INTO lamp (id, state, collecttime)VALUES"

for k := value; k < 1000; k = k + 10 {

data := " "

for j := k * 10000; j < k*10000+10000; j++ {

if j < k*10000+9999 {

id := strconv.Itoa(j)

onedata := "(" + id + ", '0', '20180103002930'), "

data = data + onedata

} else {

id := strconv.Itoa(j)

onedata := "(" + id + ",'0', '20180103002930')"

data = data + onedata

}

}

//fmt.Println(execstring + data)

_, err := db.Exec(execstring + data)

if err != nil {

fmt.Println(err)

return

}

intertnumber = intertnumber + 10000

}

}()

}

for intertnumber < 9999999 {

time.Sleep(1 * time.Second)

}

fmt.Println(time.Now().Unix())

運行結果:

1514974432

1514974796

使用了363s 平均10000000/500, 約為2.7W次每秒

4、1千W條數據,開1000個協程做插入操作,每次插入1W條數據。mysql最大連接數設置為2048

運行結果:

mysql宕機,CPU,MEM使用過高,IO使用並不高。

總結:

從程序層面上看:

1、使用事務會比較快一些。

2、多連接插入會快很多,當讀寫成為瓶頸的時候,效果就不太明顯。

3、一次插入多條數據也會快很多。

4、高並發大量插入請求,mysql服務的應對措施是宕機,而不是拒絕請求。(這個跟筆者代碼也有一定關系,因為100個協程前面都是再搶CPU構造插入請求,幾乎都是同時向mysql請求),mysql在高並發場景,如果承受不住會宕機,這點在設計上需要注意。