Redigo Pool 最重要的結(jié)構(gòu)
type Pool struct {
// 真正獲取跟redis-server連接的函數(shù), 必填參數(shù)
Dial func() (Conn, error)
// 這是個(gè)可選參數(shù), 用于在從 pool 獲取連接時(shí), 檢查這個(gè)連接是否正常使用. 所以這個(gè)參數(shù)一般是必填的
TestOnBorrow func(c Conn, t time.Time) error
// 最多有多少個(gè)空閑連接保留, 一般必填
MaxIdle int
// 最多有多少活躍的連接數(shù), 一般必填
MaxActive int
// 空閑連接最長(zhǎng)空閑時(shí)間, 一般必填
IdleTimeout time.Duration
// Pool 的活躍的連接數(shù)達(dá)到 MaxActive, 如果 Wait 為 true,
// 那么 Get() 將要等待一個(gè)連接放到 Pool中, 才會(huì)返回一個(gè)連接給使用方
Wait bool
// 設(shè)置連接最大存活時(shí)間
MaxConnLifetime time.Duration
chInitialized uint32 // set to 1 when field ch is initialized
mu sync.Mutex // mu protects the following fields
closed bool // 設(shè)置 Pool 是否關(guān)閉
active int // 當(dāng)前 Pool 的活躍連接數(shù)
ch chan struct{} // 配合 Wait 為 true 使用
idle idleList // 空閑隊(duì)列
}
Redigo 第二重要的結(jié)構(gòu): idleList
idleList 是個(gè)雙向鏈表
. 實(shí)現(xiàn)很簡(jiǎn)單. 只有三個(gè)方法: pushFront
, popFront
, popBack
初始化 idlelist
type idleList struct {
count int
front, back *poolConn
}
使用 Pool 不會(huì)明確的初始化 idle, 故當(dāng)初始化&Pool{....}
后, idle 就是默認(rèn)值. 即: count = 0, front = nil, back = nil.
如下圖:
頭部插入
idleList 只提供 pushFront 方法. 將 idleList 的 front 指針, 指向新的 Conn. 然后將新 Conn 與 之前節(jié)點(diǎn)連接即可, 如下圖
從頭部刪除
從尾部刪除
Conn
整個(gè) Pool 有兩個(gè) Conn: activeConn, errorConn, 他們都實(shí)現(xiàn)了 redis.Conn
接口.
type Conn interface {
// Close closes the connection.
Close() error
// Err returns a non-nil value when the connection is not usable.
Err() error
// Do sends a command to the server and returns the received reply.
Do(commandName string, args ...interface{}) (reply interface{}, err error)
// Send writes the command to the client's output buffer.
Send(commandName string, args ...interface{}) error
// Flush flushes the output buffer to the Redis server.
Flush() error
// Receive receives a single reply from the Redis server
Receive() (reply interface{}, err error)
}
不過(guò) errorConn 實(shí)現(xiàn)的所有的函數(shù)都會(huì)返回 err. 所以我們?cè)谑褂?Get() 獲取鏈接時(shí), 盡量要去判斷拿到的連接是否可用(不過(guò)這也不是絕對(duì), 我們也可以在使用 Conn 函數(shù)的時(shí)候去判斷).
activeConn 一般來(lái)說(shuō)是可用的連接, 我們也可以通過(guò) activeConn.Err()
來(lái)判斷獲取的連接是否可用.
activeConn 的函數(shù)都是調(diào)用 redis.Conn 的函數(shù)實(shí)現(xiàn)的. 唯一不同的地方在于, 所有的命令都會(huì)執(zhí)行LookupCommandInfo
這個(gè)函數(shù).
func LookupCommandInfo(commandName string) CommandInfo {
if ci, ok := commandInfos[commandName]; ok {
return ci
}
return commandInfos[strings.ToUpper(commandName)]
}
func (ac *activeConn) Do(commandName string, args ...interface{}) (reply interface{}, err error) {
pc := ac.pc
if pc == nil {
return nil, errConnClosed
}
ci := internal.LookupCommandInfo(commandName)
ac.state = (ac.state | ci.Set) &^ ci.Clear
return pc.c.Do(commandName, args...)
}
以Multi
, EXEC
舉例:
使用 Pool 獲取到的連接, 發(fā)送 Multi
完, 由于某種原因?qū)е鲁绦驁?zhí)行了 defer activeConn.Close()
, 由于 redis-server 沒(méi)有得到Exec
, 那么接下來(lái)的命令 redis-server 都會(huì)把其當(dāng)成事務(wù)的一部分. 所以通過(guò) LookupCommandInfo 函數(shù)能夠計(jì)算當(dāng)前某條命令執(zhí)行后 activeConn 的當(dāng)前狀態(tài), 當(dāng)執(zhí)行到 activeConn.Close()
時(shí)發(fā)現(xiàn)還沒(méi)有發(fā)送 EXEC
, 那么就會(huì)發(fā)送 DISCARD
命令來(lái)將事務(wù)取消.
LookupCommandInfo 支持的命令有: WATCH
, UNWATCH
, MULTI
, EXEC
, DISCARD
, PSUBSCRIBE
, SUBSCRIBE
, MONITOR
Redigo 靈魂函數(shù) -- get()
get() 主要提供給 Get() 函數(shù)調(diào)用, 是從 Pool 中獲取連接, Get()是我們使用者調(diào)用的函數(shù). 可以看到 Get() 由 errorConn
, activeConn
組成.
func (p *Pool) Get() Conn {
pc, err := p.get(nil)
if err != nil {
return errorConn{err}
}
return &activeConn{p: p, pc: pc}
}
Wait 配合 MaxActive 使用, 來(lái)保證 Get() 將要等待一個(gè)連接放到 Pool中, 才會(huì)返回一個(gè)連接給使用方
if p.Wait && p.MaxActive > 0 {
p.lazyInit()
if ctx == nil {
<-p.ch // <--- 這里會(huì)阻塞
} else {
// 下面的代碼不需要關(guān)于, Get() 傳遞下來(lái)的參數(shù)是 gnil
select {
case <-p.ch:
case <-ctx.Done():
return nil, ctx.Err()
}
}
}
設(shè)置 Wait 為 true 并且 MaxActive 設(shè)置有最大數(shù)時(shí), 如果時(shí)第一次獲取 Get(), 那么會(huì)調(diào)用lazyInit()
進(jìn)行初始化.
初始化一個(gè) MaxActive 的 channel. 利用 channel 來(lái)保證當(dāng) Get() 獲取到的連接數(shù)大于 MaxActive時(shí), 阻塞 Get() 函數(shù), 直到有連接使用完畢放入到 Pool 中.
循環(huán)idleList, 關(guān)閉空閑隊(duì)列中連接時(shí)長(zhǎng)大于 IdleTimeout 的連接
// Prune stale connections at the back of the idle list.
if p.IdleTimeout > 0 {
n := p.idle.count
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--
}
}
這段代碼會(huì)遍歷整個(gè) idleList, 從尾部拿出 activeConn
, activeConn.t 加上 IdleTimeout 時(shí)間, 跟當(dāng)前時(shí)間比較. 如果比 time.Now() 小, 則從 idleList 的尾部 pop
這個(gè)conn. 同時(shí)關(guān)閉這個(gè) activeConn, 讓 Pool.active--
不過(guò)這段代碼看上去比較奇怪, 需要耐心去看. 可以做個(gè)變形
for i := 0; i < n ; i++ {
if p.idle.back != nil && p.idle.back.t.Add(p.IdleTimeout).Before(nowFunc()) {
pc := p.idle.back
p.idle.popBack()
p.mu.Unlock()
pc.c.Close()
p.mu.Lock()
p.active--
}
}
當(dāng) idleList 不為空時(shí), 從頭部獲取 activeConn
for p.idle.front != nil {
pc := p.idle.front
p.idle.popFront()
p.mu.Unlock()
if (p.TestOnBorrow == nil || p.TestOnBorrow(pc.c, pc.t) == nil) &&
(p.MaxConnLifetime == 0 || nowFunc().Sub(pc.created) < p.MaxConnLifetime) {
return pc, nil
}
pc.c.Close()
p.mu.Lock()
p.active--
}
這段代碼比較簡(jiǎn)單.當(dāng) idleList 不為空時(shí), 從頭部 pop 出一個(gè) activeConn, 還有另外兩個(gè)功能:
- 通過(guò)
TestOnBorrow()
函數(shù)判斷當(dāng)前連接是否能正常使用 - 通過(guò)
MaxConnLifetime
參數(shù)判斷這個(gè)連接是否在 MaxConnLifetime 內(nèi)
判斷 Pool 是否關(guān)閉
if p.closed {
p.mu.Unlock()
return nil, errors.New("redigo: get on closed pool")
}
當(dāng)發(fā)現(xiàn) Pool 被調(diào)用 Pool.Close() 關(guān)閉了, 那么這里就會(huì)返回 errors.New("redigo: get on closed pool")
錯(cuò)誤
順帶說(shuō)一句在 `pool.go` 里面總共有兩個(gè) Close() 函數(shù):
1. func (p *Pool) Close() error {...}
這個(gè)函數(shù)是關(guān)閉 redigo 連接池的. 理論上可以不調(diào)用. 如果確實(shí)不放心, 需要在 main.go 里面 `defer pool.Close()`
來(lái)調(diào)用
2. func (ac *activeConn) Close() error { ...}
這個(gè)函數(shù)是用來(lái)將從 Pool 中獲取到的 activeConn 放回到 Pool 里面.
這個(gè)函數(shù)是我們需要頻繁調(diào)用的函數(shù). 如果程序里Get() 之后沒(méi)有 Close(), 那么就會(huì)造成 redis 連接泄漏.
更嚴(yán)重的情況, 如果Wait, MaxActive 都沒(méi)有設(shè)置, 那么你的程序就會(huì)將 redis 搞癱瘓, 這是很危險(xiǎn)的
真正從 redis-server 獲取連接
// Handle limit for p.Wait == false.
if !p.Wait && p.MaxActive > 0 && p.active >= p.MaxActive {
p.mu.Unlock()
return nil, ErrPoolExhausted
}
p.active++
p.mu.Unlock()
c, err := p.Dial()
if err != nil {
c = nil
p.mu.Lock()
p.active--
if p.ch != nil && !p.closed {
p.ch <- struct{}{}
}
p.mu.Unlock()
}
return &poolConn{c: c, created: nowFunc()}, err
- 判斷 pool 的 active 是否達(dá)到 MaxActive
- 通過(guò)參數(shù) p.Dial() 去 redis-server 獲取連接
Redigo 靈魂函數(shù) -- put()
put 函數(shù)主要提供給 activeConn.Close() 調(diào)用
Close() 函數(shù)就不在詳細(xì)說(shuō)明, 主要根據(jù) activeConn 的 stat, 判斷在關(guān)閉連接之前是否發(fā)送過(guò)WATCH
, MULTI
, PSUBSCRIBE
, SUBSCRIBE
, MONITOR
這些命令. 如果發(fā)送過(guò)就會(huì)把這些命令結(jié)束(具體原因上面已經(jīng)說(shuō)過(guò))
func (p *Pool) put(pc *poolConn, forceClose bool) error {
p.mu.Lock()
// 判斷 pool 是否關(guān)閉, 并且該命令是否需要強(qiáng)制關(guān)閉
if !p.closed && !forceClose {
pc.t = nowFunc()
// 將該 activeConn 壓入 idleList 中
p.idle.pushFront(pc)
// 如果 idleList 的 count 已經(jīng)大于 MaxIdle, 那么會(huì)將 idleList 的尾部的 activeConn pop 掉
if p.idle.count > p.MaxIdle {
pc = p.idle.back
p.idle.popBack()
} else {
pc = nil
}
}
// 如果是需要強(qiáng)制關(guān)閉或者是從尾部 pop 掉的 conn, 那么就會(huì)真正的關(guān)閉這個(gè)連接
if pc != nil {
p.mu.Unlock()
pc.c.Close()
p.mu.Lock()
p.active--
}
// 如果開(kāi)啟了 Wait = true, 那么往 channel 里面發(fā)送一個(gè)struct{}{}, 代表等待的客戶端可以獲取連接了
if p.ch != nil && !p.closed {
p.ch <- struct{}{}
}
p.mu.Unlock()
return nil
}
結(jié)尾
至此, redigo Pool 的源碼基本都過(guò)了一遍. 為什么我會(huì)心血來(lái)潮把其源碼讀一遍呢?
思考下面幾個(gè)問(wèn)題:
- redigo 是否能夠用于 codis?
- codis 的 golang 客戶端如何實(shí)現(xiàn) ?
- 如果不經(jīng)過(guò)任何加工, 直接用 redigo 去訪問(wèn) codis, 會(huì)出現(xiàn)什么樣的問(wèn)題?
這些問(wèn)題就是促使我讀redigo源碼的原因, 后續(xù)文章我會(huì)一一解答這些問(wèn)題