当我们谈连接池的时候,我们在聊什么?

2019-11-28  本文已影响0人  wu_sphinx

什么是连接池

在软件工程中,连接池是数据库连接的缓存,以便在将来需要数据库请求时可以重用这些连接

关键字提取:连接

什么是连接了?我们都知道,TCP是一种可靠的、一对一的、面向有连接的网络通信协议,UDP传输协议是一种不可靠的、面向无连接、可以实现多对一、一对多和一对一连接的通信协议,那么自然的,要实现连接的缓存,我们要选用TCP协议了。我们知道TCP协议的四要素是:

有了这个共识就好理解连接池了,其实就是类似于[src_ip:src_port, dst_ip:dst_port]这种结构的数据,不能光说不练,我们来看一个示例

Redis连接池

我们以常用的缓存数据库Redis作为示例来。

先启动redis-server

➜  ~ docker run  -p 6379:6379 redis:5-alpine

客户端代码如下

func ExampleNewClient() {
    _ = redis.NewClient(&redis.Options{
        Addr:         "192.168.43.116:6379",
        Password:     "", // no password set
        DB:           0,  // use default DB
        PoolSize:     10, // 连接池大小
        MinIdleConns: 5,  // 最小空闲连接
    })
}

func main() {
    ExampleNewClient()
    for i := 0; i < 10; i++ {
        time.Sleep(time.Second * 10)
        println("end")
    }

}

抓包

image.png
从抓包情况可知,客户端为fundeAir, 服务端为raspberrypi,客户一共向服务端发起次5次连接请求,端口分别是:54419、54421、54422、54418、54420
image.png

每一个端口都经历了完整的TCP3次握手过程来建立可靠的连接

image.png

这里需要解释一下,代码中我其实并没有发起对Redis的任何调用,只是进行了初始化客户端的操作,本来连接池是应该是10个,但是这里设置了最小空闲连接是5个,所以算起来,只建立了5个连接,就是因为我并没有使用对Redis进行任何操作。
我们来看一下里面比较关键的代码片断

func (p *ConnPool) addIdleConn() {
    cn, err := p.newConn(true)
    if err != nil {
        return
    }

    p.connsMu.Lock()
    p.conns = append(p.conns, cn)
    p.idleConns = append(p.idleConns, cn)
    p.connsMu.Unlock()
}

conns的定义为conns []*Conn,是一个slice, 建立新的连接也就是TCP三次握手的过程,互相之间并无影响,所以连接建立可以并发执行,但是append不是并发安全的,因而这里用到了锁机制。

为什么需要连接池

如果没有连接池,我的使用方式肯定是这样的

  1. 客户端建立连接
  2. 进行数据操作
  3. 关闭连接

这也就是说每一个需要使用Redis代码的地方,都需要进行三次握手建立连接

image.png

消耗的时间大约是:0.01s,这个时间看起来好像还能忍受,要知道,通常要求较高的接口的响应时间0.1s,在并发连接情况下,就需要频繁的建立连接,需要的时间就是n倍的0.1s

如果将连接作为一种资源,客户端作为消费者来消费这些资源,资源池的作用就是确保需要消费的时候尽可能即时给予消费者,为什么是尽可能即时,且看连接池代码:

func (p *ConnPool) Get() (*Conn, error) {
    if p.closed() {
        return nil, ErrClosed
    }

    err := p.waitTurn()
    if err != nil {
        return nil, err
    }

    for {
                // 我们连接池为slice, 这里取连接也需要用锁
        p.connsMu.Lock()
        cn := p.popIdle()
        p.connsMu.Unlock()
  
                // 若连接为空,则跳出建立新连接
        if cn == nil {
            break
        }

                // 若连接已过期, 则关闭该连接并重试
        if p.isStaleConn(cn) {
            _ = p.CloseConn(cn)
            continue
        }

        atomic.AddUint32(&p.stats.Hits, 1)
        return cn, nil
    }

    atomic.AddUint32(&p.stats.Misses, 1)
        // 新建连接,并加入连接池
    newcn, err := p._NewConn(true)
    if err != nil {
        p.freeTurn()
        return nil, err
    }

    return newcn, nil
}

连接用完之后释放

func (c *baseClient) releaseConn(cn *pool.Conn, err error) {
    if c.limiter != nil {
        c.limiter.ReportResult(err)
    }
        
        // 若连接未过期则重新放入连接池,否则删除该连接
    if internal.IsBadConn(err, false) {
        c.connPool.Remove(cn, err)
    } else {
        c.connPool.Put(cn)
    }
}

连接总有过期之时,资源也是有限的,先到行得,有人消费就有人等,连接池的作用就是尽可能减少等待时间,从而提高资源使用效率。连接池的流程还是清晰的,客户端初始化需要的连接,以备使用,连接过期或连接池为空还是需要新建连接。闲时这些资源池就是浪费,因为根本用不着,所以会有连接超时时间,忙时经常会不够用,因为需要的连接数会很大,所以根据不同的需求需要设定合理的连接池以及空闲连接,尽可能做到物尽其用。

Refer:

上一篇 下一篇

猜你喜欢

热点阅读