美文网首页
当我们谈连接池的时候,我们在聊什么?

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

作者: wu_sphinx | 来源:发表于2019-11-28 13:07 被阅读0次

    什么是连接池

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

    关键字提取:连接

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

    • 源ip-src_ip
    • 源端口-src_port
    • 目的ip-dst_ip
    • 目的端口-dst_port

    有了这个共识就好理解连接池了,其实就是类似于[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:

    相关文章

      网友评论

          本文标题:当我们谈连接池的时候,我们在聊什么?

          本文链接:https://www.haomeiwen.com/subject/nvbvwctx.html