什么是连接池
在软件工程中,连接池是数据库连接的缓存,以便在将来需要数据库请求时可以重用这些连接
关键字提取:连接
什么是连接了?我们都知道,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")
}
}
抓包
![](https://img.haomeiwen.com/i44480/d5f61c97ae0e19c8.png)
从抓包情况可知,客户端为
fundeAir
, 服务端为raspberrypi
,客户一共向服务端发起次5次连接请求,端口分别是:54419、54421、54422、54418、54420
![](https://img.haomeiwen.com/i44480/9f4dcf168e1b0e87.png)
每一个端口都经历了完整的TCP3次握手过程来建立可靠的连接
![](https://img.haomeiwen.com/i44480/12290da5dfcca0b1.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
不是并发安全的,因而这里用到了锁机制。
为什么需要连接池
如果没有连接池,我的使用方式肯定是这样的
- 客户端建立连接
- 进行数据操作
- 关闭连接
这也就是说每一个需要使用Redis代码的地方,都需要进行三次握手建立连接
![](https://img.haomeiwen.com/i44480/52874ccbddc80456.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:
网友评论