Go語言配置數(shù)據(jù)庫連接池的實現(xiàn)
開始本文之前,我們看一段Go連接數(shù)據(jù)庫的代碼:
//openDB()函數(shù)返回一個sql.DB連接池。 func openDB() (*sql.DB, error) { //使用sql.Open()創(chuàng)建一個空連接池 db, err := sql.Open("postgres", "postgres://username:password@localhost/db_name") if err != nil { return nil, err } //創(chuàng)建一個具有5秒超時期限的上下文。 ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second) defer cancel() //使用PingContext()建立到數(shù)據(jù)庫的新連接,并傳入上下文信息,連接超時就返回 err = db.PingContext(ctx) if err != nil { return nil, err } // 返回sql.DB連接池 return db, nil }
本文內(nèi)容我們將解釋連接池背后是如何工作的,并探索如何配置數(shù)據(jù)庫能改變或優(yōu)化其性能。
注意:本文包含了很多的理論,雖然它很有趣,但對應(yīng)用程序的構(gòu)建并不重要。如果你覺得它太枯燥,可以先瀏覽一下,然后再回頭看。
那么sql.DB連接池是如何工作的呢?
需要理解的最重要一點(diǎn)是,sql.DB池包含兩種類型的連接——“正在使用”連接和“空閑”連接。當(dāng)您使用連接執(zhí)行數(shù)據(jù)庫任務(wù)(例如執(zhí)行SQL語句或查詢行)時,該連接被標(biāo)記為正在使用,任務(wù)完成后,該連接被標(biāo)記為空閑。
當(dāng)您使用Go執(zhí)行數(shù)據(jù)庫操作時,它將首先檢查池中是否有可用的空閑連接。如果有可用的連接,那么Go將重用這個現(xiàn)有連接,并在任務(wù)期間將其標(biāo)記為正在使用。如果在您需要空閑連接時池中沒有空閑連接,那么Go將創(chuàng)建一個新的連接。
當(dāng)Go重用池中的空閑連接時,與該連接有關(guān)的任何問題都會被優(yōu)雅地處理。異常連接將在放棄之前自動重試兩次,這時Go將從池中刪除異常連接并創(chuàng)建一個新的連接來執(zhí)行該任務(wù)。
配置連接池
連接池有四個方法,我們可以使用它們來配置連接池的行為。讓我們一個一個地來討論。
SetMaxOpenConns方法
SetMaxOpenConns()方法允許您設(shè)置池中“打開”連接(使用中+空閑連接)數(shù)量的上限。默認(rèn)情況下,打開的連接數(shù)是無限的。
注意“打開”連接等于“正在使用”加上“空閑”連接,不僅僅是“正在使用”連接。
一般來說,MaxOpenConns設(shè)置得越大,可以并發(fā)執(zhí)行的數(shù)據(jù)庫查詢就越多,連接池本身成為應(yīng)用程序中的瓶頸的風(fēng)險就越低。
但讓它無限并不是最好的選擇。默認(rèn)情況下,PostgreSQL最多100個打開連接的硬限制,如果達(dá)到這個限制的話,它將導(dǎo)致pq驅(qū)動返回"sorry, too many clients already"錯誤。
注意:最大打開連接數(shù)限制可以在postgresql.conf文件中對max_connections設(shè)置來更改。
為了避免這個錯誤,將池中打開的連接數(shù)量限制在100以下是有意義的,可以為其他需要使用PostgreSQL的應(yīng)用程序或會話留下足夠的空間。
設(shè)置MaxOpenConns限制的另一個好處是,它充當(dāng)一個非?;镜南蘖髌鳎乐箶?shù)據(jù)庫同時被大量任務(wù)壓垮。
但設(shè)定上限有一個重要的警告。如果達(dá)到MaxOpenConns限制,并且所有連接都在使用中,那么任何新的數(shù)據(jù)庫任務(wù)將被迫等待,直到有連接空閑。在我們的API上下文中,用戶的HTTP請求可能在等待空閑連接時無限期地“掛起”。因此,為了緩解這種情況,使用上下文為數(shù)據(jù)庫任務(wù)設(shè)置超時是很重要的。我們將在書的后面解釋如何處理。
SetMaxIdleConns方法
SetMaxIdleConns()方法的作用是:設(shè)置池中空閑連接數(shù)的上限。缺省情況下,最大空閑連接數(shù)為2。
理論上,在池中允許更多的空閑連接將增加性能。因為它減少了從頭建立新連接發(fā)生概率—,因此有助于節(jié)省資源。
但要意識到保持空閑連接是有代價的。它占用了本來可以用于應(yīng)用程序和數(shù)據(jù)庫的內(nèi)存,而且如果一個連接空閑時間過長,它也可能變得不可用。例如,默認(rèn)情況下MySQL會自動關(guān)閉任何8小時未使用的連接。
因此,與使用更小的空閑連接池相比,將MaxIdleConns設(shè)置得過高可能會導(dǎo)致更多的連接變得不可用,浪費(fèi)資源。因此保持適量的空閑連接是必要的。理想情況下,你只希望保持一個連接空閑,可以快速使用。
另一件要指出的事情是MaxIdleConns值應(yīng)該總是小于或等于MaxOpenConns。Go會強(qiáng)制保證這點(diǎn),并在必要時自動減少M(fèi)axIdleConns值。
SetConnMaxLifetime方法
SetConnMaxLifetime()方法用于設(shè)置ConnMaxLifetime的極限值,表示一個連接保持可用的最長時間。默認(rèn)連接的存活時間沒有限制,永久可用。
如果設(shè)置ConnMaxLifetime的值為1小時,意味著所有的連接在創(chuàng)建后,經(jīng)過一個小時就會被標(biāo)記為失效連接,標(biāo)志后就不可復(fù)用。但需要注意:
- 這并不能保證一個連接將在池中存在一整個小時;有可能某個連接由于某種原因變得不可用,并在此之前自動關(guān)閉。
- 連接在創(chuàng)建后一個多小時內(nèi)仍然可以被使用—只是在這個時間之后它不能被重用。
- 這不是一個空閑超時。連接將在創(chuàng)建后一小時過期,而不是在空閑后一小時過期。
- Go每秒運(yùn)行一次后臺清理操作,從池中刪除過期的連接。
理論上,ConnMaxLifetime為無限大(或設(shè)置為很長生命周期)將提升性能,因為這樣可以減少新建連接。但是在某些情況下,設(shè)置短期存活時間有用。比如:
- 如果SQL數(shù)據(jù)庫對連接強(qiáng)制設(shè)置最大存活時間,這時將ConnMaxLifetime設(shè)置稍短時間更合理。
- 有助于數(shù)據(jù)庫替換
如果您決定對連接池設(shè)置ConnMaxLifetime,那么一定要記住連接過期(然后重新創(chuàng)建)的頻率。例如,如果連接池中有100個打開的連接,而ConnMaxLifetime為1分鐘,那么您的應(yīng)用程序平均每秒可以殺死并重新創(chuàng)建多達(dá)1.67個連接。您不希望頻率太大而最終影響性能吧。
SetConnMaxIdleTime方法
SetConnMaxIdleTime()方法在Go 1.15版本引入對ConnMaxIdleTime進(jìn)行配置。其效果和ConnMaxLifeTime類似,但這里設(shè)置的是:在被標(biāo)記為失效之前一個連接最長空閑時間。例如,如果我們將ConnMaxIdleTime設(shè)置為1小時,那么自上次使用以后在池中空閑了1小時的任何連接都將被標(biāo)記為過期并被后臺清理操作刪除。
這個配置非常有用,因為它意味著我們可以對池中空閑連接的數(shù)量設(shè)置相對較高的限制,但可以通過刪除不再真正使用的空閑連接來周期性地釋放資源。
實操一波
所以有很多信息要吸收。這在實踐中意味著什么?我們把以上所有的內(nèi)容總結(jié)成一些可行的要點(diǎn)。
1、根據(jù)經(jīng)驗,您應(yīng)該顯式地設(shè)置MaxOpenConns值。這個值應(yīng)該低于數(shù)據(jù)庫和操作系統(tǒng)對連接數(shù)量的硬性限制,您還可以考慮將其保持在相當(dāng)?shù)偷乃?,以充?dāng)基本的限流作用。
對于本書中的項目,我們將MaxOpenConns限制為25個連接。我發(fā)現(xiàn)這對于小型到中型的web應(yīng)用程序和API來說是一個合理的初始值,但理想情況下,您應(yīng)該根據(jù)基準(zhǔn)測試和壓測結(jié)果調(diào)整這個值。
2、通常,更大的MaxOpenConns和MaxIdleConns值會帶來更好的性能。但是,效果是逐漸降低的,而且您應(yīng)該注意,太多的空閑連接(連接沒有被復(fù)用)實際上會導(dǎo)致性能下降和不必要的資源消耗。
因為MaxIdleConns應(yīng)該總是小于或等于MaxOpenConns,所以對于這個項目,我們還將MaxIdleConns限制為25個連接。
3、為了降低上面第2點(diǎn)的風(fēng)險,通常應(yīng)該設(shè)置ConnMaxIdleTime值來刪除長時間未使用的空閑連接。在這個項目中,我們將設(shè)置ConnMaxIdleTime持續(xù)時間為15分鐘。
4、ConnMaxLifetime默認(rèn)設(shè)置為無限大是可以的,除非您的數(shù)據(jù)庫對連接生命周期施加了硬限制,或者您需要它協(xié)助一些操作,比如優(yōu)雅地交換數(shù)據(jù)庫。這些都不適用于本項目,所以我們將保留這個默認(rèn)的無限制配置。
配置連接池
與其硬編碼這些配置,不如更新cmd/api/main.go文件通過命令行參數(shù)讀取配置。
ConnMaxIdleTime值比較有意思,因為我們希望它傳遞一段時間,最終需要將其轉(zhuǎn)換為Go的time.Duration類型。這里有幾個選擇:
1、我們可以使用一個整數(shù)來表示秒(或分鐘)的數(shù)量,并將其轉(zhuǎn)換為time.Duration。
2、我們可以使用一個表示持續(xù)時間的字符串——比如“5s”(5秒)或“10m”(10分鐘)——然后使用time.ParseDuration()函數(shù)解析它。
3、兩種方法都可以很好地工作,但是在這個項目中我們將使用選項2。繼續(xù)并更新cmd/api/main.go文件如下:
File: cmd/api/main.go
package main import ( "context" "database/sql" "flag" "fmt" "log" "net/http" "os" "time" _ "github.com/lib/pq" ) const version = "1.0.0" //添加maxOpenConns, maxIdleConns和maxIdleTime字段來存放連接池配置 type config struct { port int env string db struct { dsn string maxOpenConns int maxIdleConns int maxIdleTime int } } type application struct { config config logger *log.Logger } func main() { var cfg config flag.IntVar(&cfg.port, "port", 4000, "API server port") flag.StringVar(&cfg.env, "env", "development", "Environment (development|staging|production)") flag.StringVar(&cfg.db.dsn, "db-dsn", "postgres://username:password@localhost/dbname", "PostgreSQL DSN") //從命令參數(shù)中讀取連接池配置到config結(jié)構(gòu)體中 flag.IntVar(&cfg.db.maxOpenConns, "db-max-open-conns", 25, "PostgreSQL max open connections") flag.IntVar(&cfg.db.maxIdleConns, "db-max-idle-conns", 25, "PostgreSQL max idle connections") flag.StringVar(&cfg.db.maxIdleTime, "db-max-idle-time", "15m", "PostgreSQL max connection idle time") flag.Parse() logger := log.New(os.Stdout, "", log.Ldate|log.Ltime) //調(diào)用openDB()幫助函數(shù)(參見下面)來創(chuàng)建連接池 db, err := openDB(cfg) if err != nil { logger.Fatal(err) } // defer調(diào)用, 以便main()函數(shù)退出之前關(guān)閉連接池。 defer db.Close() //打印連接數(shù)據(jù)庫成功日志 logger.Printf("database connection pool established") app := &application{config: cfg, logger: logger} srv := &http.Server{ Addr: fmt.Sprintf(":%d", cfg.port), Handler: app.routes(), IdleTimeout: time.Minute, ReadTimeout: 10 * time.Second, WriteTimeout: 30 * time.Second, } logger.Printf("starting %s server on %s", cfg.env, srv.Addr) err = srv.ListenAndServe() logger.Fatal(err) } func openDB(cfg config) (*sql.DB, error) { db, err := sql.Open("postgres", cfg.db.dsn) if err != nil { return nil, err } //設(shè)置最大開放連接數(shù),注意該值為小于0或等于0指的是無限制連接數(shù) db.SetMaxOpenConns(cfg.db.maxOpenConns) //設(shè)置空閑連接數(shù),小于等于0表示無限制 db.SetMaxIdleConns(cfg.db.maxIdleConns) //將空閑時間字符串解析為time.Duration類型 duration, err := time.ParseDuration(cfg.db.maxIdleTime) if err != nil { return nil, err } //設(shè)置最大空閑超時 db.SetConnMaxIdleTime(duration) ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second) defer cancel() err = db.PingContext(ctx) if err != nil { return nil, err } return db, nil }
到此這篇關(guān)于Go語言配置數(shù)據(jù)庫連接池的實現(xiàn)的文章就介紹到這了,更多相關(guān)Go語言 數(shù)據(jù)庫連接池內(nèi)容請搜索腳本之家以前的文章或繼續(xù)瀏覽下面的相關(guān)文章希望大家以后多多支持腳本之家!
相關(guān)文章
淺談golang package中init方法的多處定義及運(yùn)行順序問題
這篇文章主要介紹了淺談golang package中init方法的多處定義及運(yùn)行順序問題,具有很好的參考價值,希望對大家有所幫助。一起跟隨小編過來看看吧2021-05-05