前言
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在高並發場景,如果承受不住會宕機,這點在設計上需要注意。