【连接池】为什么要使用连接池(一)
全文共 2167 字,全篇阅读时间约 10 分钟
为什么要使用连接池?它有什么优点?
这个问题你真的能回答到点子上了吗?
据我所知,有不少人仅仅只是听说连接池好,就说好。至于问他连接池哪里好,他只能给你说连接复用啊,不用重复创建连接啊这种。但是看完这篇文章,你可以在别人问你连接池为什么好的时候,把这篇文章链接甩给他,不要再问我连接池怎么好了可以么?
下文以redis连接池作为案例,一步一步进行分析。
简单的redis连接操作
分析之前,先贴上一段redis的连接代码
func do() {
rds, err := redis.Dial("tcp", "127.0.0.1:6379")
if err != nil {
panic(err.Error())
}
defer rds.Close()
key := random.String(10)
_, _ = rds.Do("set", key, time.Now().UnixNano())
rly, err := rds.Do("get", key)
_, _ = rds.Do("del", key)
if err != nil {
panic(err.Error())
}
s, _ := redis.String(rly, err)
fmt.Println(s)
}
从代码上可以看出,代码连接到redis后设置一个kv,获取kv内容,然后删除这个key,最后他还关闭了这个连接。而能够完成这一整套操作,则归功于redis.Dial("tcp", "127.0.0.1:6379")
建立了一个tcp连接。在tcp连接后,进行了数据传输,所以才能完成设置key的内容,获取key的内容,删除这个key一系列操作。
现在看来,代码并没有什么地方存在问题。这是一个连接的创建,使用,关闭非常标准的流程。现在,我们可以通过代码去操作redis了。
但是,如果我作为一个在线服务,每秒需要承受10000个用户的请求。每一个请求,我都要重复上面的操作,建立tcp连接,通过tcp连接传输我需要的操作,然后我要关闭tcp连接。这看上去像是在做重复的工作,并且当10000个用户同时进入,建立了10000个tcp连接,redis服务器是否可以支撑这么多的连接,也需要注意一下。
实现一个redis连接的单例
经过观察可以发现,发送N次数据这个我们是无法掌握的,根据业务他需要发送各种数据。但是连接和关闭这两个动作是一直在重复执行的,每一次的连接,我们都需要经过7次握手(3次握手连接,4次握手关闭)【1】。
我们需要想办法把
创建
和关闭
连接的握手次数减少
在设计模式中有一个非常简单的设计模式“单例”。它可以重复使用已经实例化好的对象,这似乎可以满足我们不重复干一件事情的需求。
那我们来调整一下代码
var instance sync.Once
var redisSingleton redis.Conn
func do() {
rds := getInstance()
key := random.String(10)
_, _ = rds.Do("set", key, time.Now().UnixNano())
rly, err := rds.Do("get", key)
_, _ = rds.Do("del", key)
if err != nil {
panic(err.Error())
}
s, _ := redis.String(rly, err)
fmt.Println(s)
}
func getInstance() redis.Conn {
instance.Do(func() {
var err error
redisSingleton, err = redis.Dial("tcp", "127.0.0.1:6379")
if err != nil {
panic(err.Error())
}
})
return redisSingleton
}
通过sync.Once来实现一个单例,似乎让上面重复的操作得到缓解。并且,因为单例的限制,连接到redis服务器的连接数始终不会超过单例的个数。
但是,这个时候1000个用户涌进来了。单例在tcp数据交换,到redis服务器响应,到完成操作这个过程需要耗时3ms。1000个用户进来后,处理掉这1000个用户的请求,起码要花掉3秒钟。后面处理的用户显然是可以感受到延迟很长。
该如何提升1000个用户同时请求的响应能力?
处理单例排队时间过长的问题
单例是在使用连接之前就准备好了这个连接,等待被调用,使一个连接可以被重复使用。那我们提前创建N个连接,等待被调用,是不是这个处理能力就可以达到N倍了呢?我们来做个小学数学题。
单例:1000 / 1 * 3 = 3000 ms
连接池:1000 / N * 3 = 3000 / N ms
从数学上看去,这个N越大,响应能力越强,好像挺不错。(N可以多大?太大redis能不能扛住?)
设计一个连接池
理论上连接池很美,那我们来设计一个。
-
首先连接池要可以控制住数量,不能随着请求数增多无限制地扩张,否则数据服务器肯定会被打崩 -
提前生成好连接,放入池中 -
要让拿不到连接的请求等待,避免在连接数可调配的情况下出现请求数据操作失败的情况 -
连接可回收,反复使用资源
根据上面的需求确定了一下接口
type Interface interface {
Factory() interface{} // 用于创建连接
Get() (interface{}, error) // 获取一个连接
Release(interface{}) // 释放一个连接
ForceCloseAll() // 强制关闭所有连接
}
实现这些接口后,我们可以让请求从池子中拿到连接后进行redis操作,操作完成后释放这个连接,让它重新回到池子中等待被下一次使用。
实测结果
类型 | 总量 | 并发量 | 耗时 | redis服务器显示连接数 | 备注 |
---|---|---|---|---|---|
(串行)普通连接 | 1000 | 1 | 4.206786074s | 1 | |
(并行)普通连接 | 1000 | 1 | 0 | 0 | 连接数过多,客户端断开 |
(串行)单例连接 | 1000 | 1 | 4.028845233s | 1 | |
(并行)单例连接 | 1000 | 1 | 4.028845233s | 1 | |
连接池 | 1000 | 1 | 3.636580809s | 1 | 连接池大小为1 |
连接池 | 1000 | 5 | 1.192311131s | 5 | 连接池大小为5 |
连接池 | 1000 | 10 | 870.164852ms | 10 | 连接池大小为10 |
连接池 | 1000 | 20 | 700.151678ms | 20 | 连接池大小为20 |
连接池 | 1000 | 50 | 667.814726ms | 50 | 连接池大小为50 |
连接池 | 1000 | 100 | 638.407439ms | 100 | 连接池大小为100 |
总结
从上面的分析来看,可以得到以下结论:
-
普通连接可以保证每一次请求的连接是可用的,但耗时过长,请求量过大,对redis的压力很大 -
单例连接可以有效解决连接过程重复的问题,但是会使请求进入串行化,并且长时间使用一个连接无法保证连接存活 -
连接池可以解决单例的串行化问题,提升请求响应能力,控制连接数,避免redis服务器连接数过多,但是与单例存在相同的问题,长时间使用连接无法保证连接存活
下一篇需要处理连接池的问题
-
确认程序与redis的连接是否有时间限制 -
如果有时间限制,超时了如何处理 -
redis重启了,连接不可用如何处理 -
如何确认连接是否存活
引用:
【1】TCP 的特性(https://hit-alibaba.github.io/interview/basic/network/TCP.html)
【2】文章相关代码 (https://github.com/jinfeijie/blog-demo/tree/master/post-835)
下一篇还在撰写中,期待