日本免费高清视频-国产福利视频导航-黄色在线播放国产-天天操天天操天天操天天操|www.shdianci.com

學無先后,達者為師

網(wǎng)站首頁 編程語言 正文

Go語言配置數(shù)據(jù)庫連接池的實現(xiàn)_Golang

作者:Go語言由淺入深 ? 更新時間: 2021-12-05 編程語言

開始本文之前,我們看一段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)化其性能。

注意:本文包含了很多的理論,雖然它很有趣,但對應用程序的構(gòu)建并不重要。如果你覺得它太枯燥,可以先瀏覽一下,然后再回頭看。

那么sql.DB連接池是如何工作的呢?

需要理解的最重要一點是,sql.DB池包含兩種類型的連接——“正在使用”連接和“空閑”連接。當您使用連接執(zhí)行數(shù)據(jù)庫任務(例如執(zhí)行SQL語句或查詢行)時,該連接被標記為正在使用,任務完成后,該連接被標記為空閑。

當您使用Go執(zhí)行數(shù)據(jù)庫操作時,它將首先檢查池中是否有可用的空閑連接。如果有可用的連接,那么Go將重用這個現(xiàn)有連接,并在任務期間將其標記為正在使用。如果在您需要空閑連接時池中沒有空閑連接,那么Go將創(chuàng)建一個新的連接。

當Go重用池中的空閑連接時,與該連接有關(guān)的任何問題都會被優(yōu)雅地處理。異常連接將在放棄之前自動重試兩次,這時Go將從池中刪除異常連接并創(chuàng)建一個新的連接來執(zhí)行該任務。

配置連接池

連接池有四個方法,我們可以使用它們來配置連接池的行為。讓我們一個一個地來討論。

SetMaxOpenConns方法

SetMaxOpenConns()方法允許您設置池中“打開”連接(使用中+空閑連接)數(shù)量的上限。默認情況下,打開的連接數(shù)是無限的。

注意“打開”連接等于“正在使用”加上“空閑”連接,不僅僅是“正在使用”連接。

一般來說,MaxOpenConns設置得越大,可以并發(fā)執(zhí)行的數(shù)據(jù)庫查詢就越多,連接池本身成為應用程序中的瓶頸的風險就越低。

但讓它無限并不是最好的選擇。默認情況下,PostgreSQL最多100個打開連接的硬限制,如果達到這個限制的話,它將導致pq驅(qū)動返回"sorry, too many clients already"錯誤。

注意:最大打開連接數(shù)限制可以在postgresql.conf文件中對max_connections設置來更改。

為了避免這個錯誤,將池中打開的連接數(shù)量限制在100以下是有意義的,可以為其他需要使用PostgreSQL的應用程序或會話留下足夠的空間。

設置MaxOpenConns限制的另一個好處是,它充當一個非常基本的限流器,防止數(shù)據(jù)庫同時被大量任務壓垮。

但設定上限有一個重要的警告。如果達到MaxOpenConns限制,并且所有連接都在使用中,那么任何新的數(shù)據(jù)庫任務將被迫等待,直到有連接空閑。在我們的API上下文中,用戶的HTTP請求可能在等待空閑連接時無限期地“掛起”。因此,為了緩解這種情況,使用上下文為數(shù)據(jù)庫任務設置超時是很重要的。我們將在書的后面解釋如何處理。

SetMaxIdleConns方法

SetMaxIdleConns()方法的作用是:設置池中空閑連接數(shù)的上限。缺省情況下,最大空閑連接數(shù)為2。

理論上,在池中允許更多的空閑連接將增加性能。因為它減少了從頭建立新連接發(fā)生概率—,因此有助于節(jié)省資源。

但要意識到保持空閑連接是有代價的。它占用了本來可以用于應用程序和數(shù)據(jù)庫的內(nèi)存,而且如果一個連接空閑時間過長,它也可能變得不可用。例如,默認情況下MySQL會自動關(guān)閉任何8小時未使用的連接。

因此,與使用更小的空閑連接池相比,將MaxIdleConns設置得過高可能會導致更多的連接變得不可用,浪費資源。因此保持適量的空閑連接是必要的。理想情況下,你只希望保持一個連接空閑,可以快速使用。

另一件要指出的事情是MaxIdleConns值應該總是小于或等于MaxOpenConns。Go會強制保證這點,并在必要時自動減少MaxIdleConns值。

SetConnMaxLifetime方法

SetConnMaxLifetime()方法用于設置ConnMaxLifetime的極限值,表示一個連接保持可用的最長時間。默認連接的存活時間沒有限制,永久可用。

如果設置ConnMaxLifetime的值為1小時,意味著所有的連接在創(chuàng)建后,經(jīng)過一個小時就會被標記為失效連接,標志后就不可復用。但需要注意:

  • 這并不能保證一個連接將在池中存在一整個小時;有可能某個連接由于某種原因變得不可用,并在此之前自動關(guān)閉。
  • 連接在創(chuàng)建后一個多小時內(nèi)仍然可以被使用—只是在這個時間之后它不能被重用。
  • 這不是一個空閑超時。連接將在創(chuàng)建后一小時過期,而不是在空閑后一小時過期。
  • Go每秒運行一次后臺清理操作,從池中刪除過期的連接。

理論上,ConnMaxLifetime為無限大(或設置為很長生命周期)將提升性能,因為這樣可以減少新建連接。但是在某些情況下,設置短期存活時間有用。比如:

  • 如果SQL數(shù)據(jù)庫對連接強制設置最大存活時間,這時將ConnMaxLifetime設置稍短時間更合理。
  • 有助于數(shù)據(jù)庫替換

如果您決定對連接池設置ConnMaxLifetime,那么一定要記住連接過期(然后重新創(chuàng)建)的頻率。例如,如果連接池中有100個打開的連接,而ConnMaxLifetime為1分鐘,那么您的應用程序平均每秒可以殺死并重新創(chuàng)建多達1.67個連接。您不希望頻率太大而最終影響性能吧。

SetConnMaxIdleTime方法

SetConnMaxIdleTime()方法在Go 1.15版本引入對ConnMaxIdleTime進行配置。其效果和ConnMaxLifeTime類似,但這里設置的是:在被標記為失效之前一個連接最長空閑時間。例如,如果我們將ConnMaxIdleTime設置為1小時,那么自上次使用以后在池中空閑了1小時的任何連接都將被標記為過期并被后臺清理操作刪除。

這個配置非常有用,因為它意味著我們可以對池中空閑連接的數(shù)量設置相對較高的限制,但可以通過刪除不再真正使用的空閑連接來周期性地釋放資源。

實操一波

所以有很多信息要吸收。這在實踐中意味著什么?我們把以上所有的內(nèi)容總結(jié)成一些可行的要點。

1、根據(jù)經(jīng)驗,您應該顯式地設置MaxOpenConns值。這個值應該低于數(shù)據(jù)庫和操作系統(tǒng)對連接數(shù)量的硬性限制,您還可以考慮將其保持在相當?shù)偷乃剑猿洚敾镜南蘖髯饔谩?/p>

對于本書中的項目,我們將MaxOpenConns限制為25個連接。我發(fā)現(xiàn)這對于小型到中型的web應用程序和API來說是一個合理的初始值,但理想情況下,您應該根據(jù)基準測試和壓測結(jié)果調(diào)整這個值。

2、通常,更大的MaxOpenConns和MaxIdleConns值會帶來更好的性能。但是,效果是逐漸降低的,而且您應該注意,太多的空閑連接(連接沒有被復用)實際上會導致性能下降和不必要的資源消耗。

因為MaxIdleConns應該總是小于或等于MaxOpenConns,所以對于這個項目,我們還將MaxIdleConns限制為25個連接。

3、為了降低上面第2點的風險,通常應該設置ConnMaxIdleTime值來刪除長時間未使用的空閑連接。在這個項目中,我們將設置ConnMaxIdleTime持續(xù)時間為15分鐘。

4、ConnMaxLifetime默認設置為無限大是可以的,除非您的數(shù)據(jù)庫對連接生命周期施加了硬限制,或者您需要它協(xié)助一些操作,比如優(yōu)雅地交換數(shù)據(jù)庫。這些都不適用于本項目,所以我們將保留這個默認的無限制配置。

配置連接池

與其硬編碼這些配置,不如更新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ù),注意該值為小于0或等于0指的是無限制連接數(shù)
        db.SetMaxOpenConns(cfg.db.maxOpenConns)

        //設置空閑連接數(shù),小于等于0表示無限制
        db.SetMaxIdleConns(cfg.db.maxIdleConns)

        //將空閑時間字符串解析為time.Duration類型
        duration, err := time.ParseDuration(cfg.db.maxIdleTime)
        if err != nil {
            return nil, err
        }

        //設置最大空閑超時
        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
}

原文鏈接:https://www.jianshu.com/p/cbfc398bd4d6

欄目分類
最近更新