redigo連接池源碼解析

何為連接池

連接池是負責分配叹坦、管理和釋放連接,它允許應(yīng)用程序重復使用池中的空閑的連接据块,而不是每次都重新建立一個連接。 本質(zhì)就是管理了一堆長鏈接折剃,提供給需求方相應(yīng)的句柄使用另假。

連接池有何用

  • 減少網(wǎng)絡(luò)io開銷
    減少了每次連接三次握手和四次揮手的開銷。連接復用怕犁,自然減少了創(chuàng)建边篮,關(guān)閉套接字等流程。提升系統(tǒng)性能

  • 控制資源
    如果沒有連接池管理奏甫,如每次請求戈轿,協(xié)程都創(chuàng)建一個連接,那么當請求量巨大時阵子,產(chǎn)生非常大的浪費并且可能會導致高負載下的異常發(fā)生思杯,最終導致所有服務(wù)都不可用。這就是為什么很多存儲都會有一層proxy來管理挠进,不讓業(yè)務(wù)服務(wù)直接和存儲連接色乾。

  • 簡化編程
    使用者只需關(guān)心如何獲取和返回的方法,無需關(guān)心底層連接领突、避免資源泄漏等問題

redigo是如何實現(xiàn)v1.8.4

首先redigo不支持cluster暖璧,作者也不打算支持,所以建議還是選擇go-redis

package main
import (
    "fmt"
    red "github.com/gomodule/redigo/redis"
    "time"
)

type Redis struct {
    pool *red.Pool
}

var redis *Redis
func Exec(cmd string, key interface{}, args ...interface{}) (interface{}, error) {
    con := redis.pool.Get()
    // connct
    if err := con.Err(); err != nil {
        return nil, err
    }
    defer con.Close()
    parmas := make([]interface{}, 0)
    parmas = append(parmas, key)

    if len(args) > 0 {
        for _, v := range args {
            parmas = append(parmas, v)
        }
    }
    return con.Do(cmd, parmas...)
}

func initRedis() {
    redis = new(Redis)
    redis.pool = &red.Pool{
        MaxIdle:     2, //空閑數(shù)
        IdleTimeout: 240 * time.Second,
        MaxActive:   0, //最大數(shù)
        Dial: func() (red.Conn, error) {
            c, err := red.Dial("tcp", "127.0.0.1:6379")
            if err != nil {
                return nil, err
            }
            return c, err
        },
        TestOnBorrow: func(c red.Conn, t time.Time) error {
            _, err := c.Do("PING")
            return err
        },
    }
}

func main() {
    initRedis()
    Exec("set", "dandy", "hello")
    result, err := Exec("get", "dandy")
    if err != nil {
        fmt.Print(err.Error())
    }
    str, _ := red.String(result, err)
    fmt.Print(str)
    redis.pool.Close()
}

初始化redis.pool

type Pool struct {
    // Dial conn中Dial調(diào)用初始化
    Dial func() (Conn, error)
    // 帶有context的Dial,2選1即可
    DialContext func(ctx context.Context) (Conn, error)
    // 獲取連接池中君旦,校驗連接是否可用澎办,一般和PING、PONG使用
    TestOnBorrow func(c Conn, t time.Time) error
    // 連接池中最大空閑數(shù)
    MaxIdle int
    // 連接池中保持活躍的數(shù)于宙,0沒有限制
    MaxActive int
    // 空閑檢查時間
    IdleTimeout time.Duration
    // wait設(shè)置為true并且pool中活躍數(shù)到達設(shè)置的最大值,直到連接池中有可用連接浮驳,get()才返回 
    Wait bool
    //  設(shè)置連接最大存活時間 0無限制
    MaxConnLifetime time.Duration
    // 統(tǒng)計、隊列等使用
    mu           sync.Mutex    // mu protects the following fields
    closed       bool          // set to true when the pool is closed.
    active       int           // the number of open connections in the pool
    initOnce     sync.Once     // the init ch once func
    ch           chan struct{} // limits open connections when p.Wait is true
    idle         idleList      // idle connections
    waitCount    int64         // total number of connections waited for.
    waitDuration time.Duration // total time waited for new connections.
}

Pool獲取連接

Get獲取

源碼 pool.go
func (p *Pool) Get() Conn 
  • wait設(shè)置等待
select {
  case <-p.ch://當連接池滿時捞魁,會阻塞等待至会,直到有空閑連接
    select {
    case <-ctx.Done():
      p.ch <- struct{}{}
      return 0, ctx.Err()
    default:
    }
  case <-ctx.Done():
    return 0, ctx.Err()
}

當pool中設(shè)置了Wait,當連接滿時(p.ch獲取不到數(shù)據(jù))谱俭,會等待直到池中有空閑連接奉件,就會通知ch

看activeConn.close()會調(diào)用Pool.put(),此時連接池將會有空閑連接宵蛀,并且通知剛才等待的Wait ch

if p.ch != nil && !p.closed {
        // 通知等待ch
    p.ch <- struct{}{}
}
  • 空閑時間判斷
if p.IdleTimeout > 0 {
        n := p.idle.count
        // 只需從尾部back判斷即可驗證是否過期
        // 如果過期,刪除尾部县貌,釋放該連接术陶,并且繼續(xù)遍歷
        for i := 0; i < n && p.idle.back != nil && p.idle.back.t.Add(p.IdleTimeout).Before(nowFunc()); i++ {
            pc := p.idle.back
            p.idle.popBack()
            p.mu.Unlock()
            pc.c.Close()
            p.mu.Lock()
            p.active--
        }
}

我們可以先大致先看內(nèi)部連接池雙向鏈表的管理點擊跳轉(zhuǎn),也許這樣你會很容易的理解煤痕。

這里idletimeout遍歷整個鏈表梧宫,因為idle.back.t為最早插入的時間,所以只需要檢查尾部back即可。

  • 從連接池頭部后取空閑連接
for p.idle.front != nil {
    pc := p.idle.front
    // 取出頭部
    p.idle.popFront()
    p.mu.Unlock()
    // 校驗連接是否正常摆碉,一般我們設(shè)置回調(diào)ping取檢驗塘匣,
    // 自然每次都多了一次請求,性能消耗
    if (p.TestOnBorrow == nil || p.TestOnBorrow(pc.c, pc.t) == nil) &&
      (p.MaxConnLifetime == 0 || nowFunc().Sub(pc.created) < p.MaxConnLifetime) {
      // 返回可用的連接
      return &activeConn{p: p, pc: pc}, nil
    }
    // 校驗不通過巷帝,自然釋放該連接
    pc.c.Close()
    p.mu.Lock()
    p.active--
}

因為上面的條件如果都校驗成功忌卤,說明鏈表頭部有數(shù)據(jù),我們只需pop出來楞泼,之后返回activeConn驰徊,即我們成功后去了一個連接。注意堕阔,這里activeConn很關(guān)鍵棍厂,里頭將最早初始化創(chuàng)建的p(pool)存入,并且將pc(poolConn)即從鏈表中取出的數(shù)據(jù)存起來超陆⊙埃基本上ac(activeConn)涵蓋了后續(xù)所有可以操作的數(shù)據(jù)。 詳細pc我們可以看下面

  • 開始鏈表肯定是空的侥猬,如何獲取連接
p.active++
p.mu.Unlock()
// 這里撥號,即調(diào)用我們一開始注冊的回調(diào)pool中的Dial
// 這里就是創(chuàng)建線程池開始創(chuàng)建連接捐韩,即conn的管理
c, err := p.dial(ctx)
if err != nil {
    p.mu.Lock()
    p.active--
    if p.ch != nil && !p.closed {
        p.ch <- struct{}{}
    }
    p.mu.Unlock()
    // 返回錯誤連接
    return errorConn{err}, err
}
// pc中c為conn.go中conn的存儲退唠。
return &activeConn{p: p, pc: &poolConn{c: c, created: nowFunc()}}, nil

這里就是一開始,我們調(diào)用dial創(chuàng)建連接荤胁,并返回activConn得到連接瞧预。后續(xù)我們會分析conn.go

pool 中鏈表put的存放

func (p *Pool) put(pc *poolConn, forceClose bool) error {
   p.mu.Lock()
   if !p.closed && !forceClose {
       pc.t = nowFunc()
       // 報存隊列
       p.idle.pushFront(pc)
       // 超過設(shè)置,pop出時間有效時間最小的back連接
       if p.idle.count > p.MaxIdle {
           pc = p.idle.back
           p.idle.popBack()
       } else {
           pc = nil
       }
   }
   // back該連接不保存仅政,直接關(guān)閉
   if pc != nil {
       p.mu.Unlock()
       pc.c.Close()
       p.mu.Lock()
       p.active--
   }
   // 上述以說明垢油,配合wait設(shè)置使用
   if p.ch != nil && !p.closed {
       p.ch <- struct{}{}
   }
   p.mu.Unlock()
   return nil
}

當Pool.get獲取的連接,并沒有保存在連接池中圆丹,而是當activeConn.Close()時滩愁,才調(diào)用put,保存連接辫封。至此硝枉,pool中核心功能都已準備完畢廉丽。


idleList連接池管理

  • pushFront鏈表存儲
連接池插入.png
func (l *idleList) pushFront(pc *poolConn) {
    // 這里記住,idleList中front和back始終指向
    // 的是連接池中的頭部和尾部
    // 1 新的pc尾指針指向鏈表頭部
    pc.next = l.front
    pc.prev = nil
    if l.count == 0 {
        // 0.當連接池為空妻味,頭尾都指向改連接
        l.back = pc
    } else {
        // 2. 鏈表頭前驅(qū)指針指向pc
        l.front.prev = pc
    }
    // 3.修改l中front指向為新插入的pc
    l.front = pc
    l.count++
}
  • popFront刪除鏈表
    連接池刪除.png

conn.go

  • 連接創(chuàng)建
// 調(diào)用net/dial.go庫進行連接
netConn, err := do.dialContext(ctx, network, address)
if err != nil {
    return nil, err
}
c := &conn{
    // 暫時我們研究的是返回:TCPConn
    conn:         netConn,
    // bufio寫的也很好正压,后續(xù)對其分析
    bw:           bufio.NewWriter(netConn),
    br:           bufio.NewReader(netConn),
    readTimeout:  do.readTimeout,
    writeTimeout: do.writeTimeout,
}
  • 之后的activeConn調(diào)用的Do方法就是調(diào)用conn中的Do
if cmd != "" {
    // RESP協(xié)議組包
    if err := c.writeCommand(cmd, args); err != nil {
        return nil, c.fatal(err)
    }
}
// bufio用法,里頭Write為interface實際為TCPConn的操作
if err := c.bw.Flush(); err != nil {
    return nil, c.fatal(err)
}

var deadline time.Time
if readTimeout != 0 {
    deadline = time.Now().Add(readTimeout)
}
// read過期檢測
if err := c.conn.SetReadDeadline(deadline); err != nil {
    return nil, c.fatal(err)
}
var err error
var reply interface{}
for i := 0; i <= pending; i++ {
    var e error
    // 獲取redis服務(wù)回包數(shù)據(jù)
    if reply, e = c.readReply(); e != nil {
        return nil, c.fatal(e)
    }
    if e, ok := reply.(Error); ok && err == nil {
        err = e
    }
}

Do方法調(diào)用的是DoWithTimeout责球,這里發(fā)起RESP協(xié)議組包焦履,并發(fā)送數(shù)據(jù)給redis服務(wù)端,之后讀取redis服務(wù)器返回的數(shù)據(jù)雏逾。


大家如果覺得有啥疑惑或者不正確嘉裤,都可以在評論或者加微信(dandyhzh)一起談?wù)摗?/p>

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
  • 序言:七十年代末,一起剝皮案震驚了整個濱河市校套,隨后出現(xiàn)的幾起案子价脾,更是在濱河造成了極大的恐慌,老刑警劉巖笛匙,帶你破解...
    沈念sama閱讀 216,744評論 6 502
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件侨把,死亡現(xiàn)場離奇詭異,居然都是意外死亡妹孙,警方通過查閱死者的電腦和手機秋柄,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 92,505評論 3 392
  • 文/潘曉璐 我一進店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來蠢正,“玉大人骇笔,你說我怎么就攤上這事∠福” “怎么了笨触?”我有些...
    開封第一講書人閱讀 163,105評論 0 353
  • 文/不壞的土叔 我叫張陵,是天一觀的道長雹舀。 經(jīng)常有香客問我芦劣,道長,這世上最難降的妖魔是什么说榆? 我笑而不...
    開封第一講書人閱讀 58,242評論 1 292
  • 正文 為了忘掉前任虚吟,我火速辦了婚禮,結(jié)果婚禮上签财,老公的妹妹穿的比我還像新娘串慰。我一直安慰自己,他們只是感情好唱蒸,可當我...
    茶點故事閱讀 67,269評論 6 389
  • 文/花漫 我一把揭開白布邦鲫。 她就那樣靜靜地躺著,像睡著了一般油宜。 火紅的嫁衣襯著肌膚如雪掂碱。 梳的紋絲不亂的頭發(fā)上怜姿,一...
    開封第一講書人閱讀 51,215評論 1 299
  • 那天,我揣著相機與錄音疼燥,去河邊找鬼沧卢。 笑死,一個胖子當著我的面吹牛醉者,可吹牛的內(nèi)容都是我干的但狭。 我是一名探鬼主播,決...
    沈念sama閱讀 40,096評論 3 418
  • 文/蒼蘭香墨 我猛地睜開眼撬即,長吁一口氣:“原來是場噩夢啊……” “哼立磁!你這毒婦竟也來了?” 一聲冷哼從身側(cè)響起剥槐,我...
    開封第一講書人閱讀 38,939評論 0 274
  • 序言:老撾萬榮一對情侶失蹤唱歧,失蹤者是張志新(化名)和其女友劉穎,沒想到半個月后粒竖,有當?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體颅崩,經(jīng)...
    沈念sama閱讀 45,354評論 1 311
  • 正文 獨居荒郊野嶺守林人離奇死亡,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點故事閱讀 37,573評論 2 333
  • 正文 我和宋清朗相戀三年蕊苗,在試婚紗的時候發(fā)現(xiàn)自己被綠了沿后。 大學時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片。...
    茶點故事閱讀 39,745評論 1 348
  • 序言:一個原本活蹦亂跳的男人離奇死亡朽砰,死狀恐怖尖滚,靈堂內(nèi)的尸體忽然破棺而出,到底是詐尸還是另有隱情瞧柔,我是刑警寧澤漆弄,帶...
    沈念sama閱讀 35,448評論 5 344
  • 正文 年R本政府宣布渣慕,位于F島的核電站隔箍,受9級特大地震影響,放射性物質(zhì)發(fā)生泄漏输枯。R本人自食惡果不足惜备绽,卻給世界環(huán)境...
    茶點故事閱讀 41,048評論 3 327
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望鬓催。 院中可真熱鬧肺素,春花似錦、人聲如沸宇驾。這莊子的主人今日做“春日...
    開封第一講書人閱讀 31,683評論 0 22
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽课舍。三九已至塌西,卻和暖如春他挎,著一層夾襖步出監(jiān)牢的瞬間,已是汗流浹背捡需。 一陣腳步聲響...
    開封第一講書人閱讀 32,838評論 1 269
  • 我被黑心中介騙來泰國打工办桨, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留,地道東北人站辉。 一個月前我還...
    沈念sama閱讀 47,776評論 2 369
  • 正文 我出身青樓呢撞,卻偏偏與公主長得像,于是被迫代替她去往敵國和親饰剥。 傳聞我的和親對象是個殘疾皇子殊霞,可洞房花燭夜當晚...
    茶點故事閱讀 44,652評論 2 354

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