【连接池】为什么要使用连接池(一)

为什么要使用连接池?它有什么优点?

这个问题你真的能回答到点子上了吗?

据我所知,有不少人仅仅只是听说连接池好,就说好。至于问他连接池哪里好,他只能给你说连接复用啊,不用重复创建连接啊这种。但是看完这篇文章,你可以在别人问你连接池为什么好的时候,把这篇文章链接甩给他,不要再问我连接池怎么好了可以么?

下文以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能不能扛住?)

设计一个连接池

理论上连接池很美,那我们来设计一个。

  1. 首先连接池要可以控制住数量,不能随着请求数增多无限制地扩张,否则数据服务器肯定会被打崩
  2. 提前生成好连接,放入池中
  3. 要让拿不到连接的请求等待,避免在连接数可调配的情况下出现请求数据操作失败的情况
  4. 连接可回收,反复使用资源

根据上面的需求确定了一下接口

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

总结

从上面的分析来看,可以得到以下结论:

  1. 普通连接可以保证每一次请求的连接是可用的,但耗时过长,请求量过大,对redis的压力很大
  2. 单例连接可以有效解决连接过程重复的问题,但是会使请求进入串行化,并且长时间使用一个连接无法保证连接存活
  3. 连接池可以解决单例的串行化问题,提升请求响应能力,控制连接数,避免redis服务器连接数过多,但是与单例存在相同的问题,长时间使用连接无法保证连接存活

下一篇需要处理连接池的问题

  1. 确认程序与redis的连接是否有时间限制
  2. 如果有时间限制,超时了如何处理
  3. redis重启了,连接不可用如何处理
  4. 如何确认连接是否存活

引用:

【1】TCP 的特性

【2】文章相关代码

发表评论

电子邮件地址不会被公开。 必填项已用*标注

评论审核已启用。您的评论可能需要一段时间后才能被显示。