连接池的定义与主要功能

在软件工程中,连接池(英语:connection pool)是维护的数据库连接的缓存,以便在将来需要对数据库发出请求时可以重用连接。连接池用于提高在数据库上执行命令的性能。为每个用户打开和维护数据库连接,尤其是对动态数据库驱动的网站应用程序发出的请求,既昂贵又浪费资源。在连接池中,创建连接之后,将连接放在池中并再次使用,这样就不必建立新的连接。如果所有连接都正在使用,则创建一个新连接并将其添加到池中。连接池还减少了用户必须等待建立与数据库的连接的时间。1

连接池的数据结构

一般来说连接池的大小是一个动态的范围,连接池内部的连接实例2数量会随着应用程序的峰值而变化; 高峰时期连接池内连接实例2数量高,低峰时期连接池内连接实例2数量低;在应用程序的高低峰 转变过程中,利用堆栈3的数据结构(先入后出)快速标记出闲置实例,方便定期清理;

结构示意图

数据结构

连接池的使用

连接池的使用我个人认为有两种方法:第一种是块使用法,第二种是行使用法;其中的的定义标准是以代码作用域为参考标准;

块使用法

块使用法就是我们在使用连接池时候将连接实例取出后自行放回; 该种使用方法一般在代码中代码作用域比较大块;

伪代码如下类似:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
package main

import "time"

func main() {
    // 从连接池中取出实例
    instance := pool.pop()
  
    defer pool.push(instance)
    
    instance.do("hello world 1")
    time.Sleep(time.Millisecond * 10)

    instance.do("hello world 2")
    time.Sleep(time.Millisecond * 10)

    instance.do("hello world 3")
    time.Sleep(time.Millisecond * 10)
}

行使用法

行使用法是我们利用代理类或代理方法接管对连接实例的操作; 在业务代码上是无需从连接池中取出实例再操作;

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
package main

func proxy(command string) {
    instance := pool.pop()
    defer pool.push(instance)

    // proxy command
    instance.do(command)
}

func main() {
    proxy("hello world 1")
    time.Sleep(time.Millisecond * 10)

    proxy("hello world 2")
    time.Sleep(time.Millisecond * 10)

    proxy("hello world 3")
    time.Sleep(time.Millisecond * 10)
}

两种使用法优缺点对比

块使用法优点是在对连接实例的密集操作中减少了对连接池的取出放置操作(仅1次); 但是缺点也显而易见,连接实例被长时间(相对来说)持有,如果处于高并发接口,很容易吃空连接池,导致连接池资源耗尽;

行使用法的优缺点与块使用法的优缺点是相对的;理念是对连接实例的快拿快放,执行完单行指令立马将连实例回收,保障其他资源在从连接池取资源时等待时间更短; 更像是交替使用;

两种使用法对比示意图

示意图

两种使用法推荐场景

块使用法在连接池达到最大连接数之前的执行时间是快于行使用法;但是行使用法在高并发场景下,总体的执行时间是远快于块使用法;

  • 块使用法:推荐在子进程中长时间持有单个实例连接的持续性高频操作;
  • 行使用法:在高并发业务场景加快对资源的释放,避免由于连接池资源耗尽导致问题;

并且当并发高于甚至远高于连接池最大连接数时;两种使用方法的差距会非常明显;可以参考BenchMark;

Benchmark

环境:

1
2
3
goos: darwin
goarch: amd64
cpu: Intel(R) Core(TM) i9-9880H CPU @ 2.30GHz

样本

  • 测试1:最小连接数10,最大连接数30
  • 测试2:最小连接数10,最大连接数100
  • 测试3:最小连接数10,最大连接数300

对比图

y轴越高速度越慢,ns/op指每次处理需要多少纳秒

benchmark

样本结果

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
# 样本一
goos: darwin
goarch: amd64
cpu: Intel(R) Core(TM) i9-9880H CPU @ 2.30GHz
BenchmarkConnectionBlock
BenchmarkConnectionBlock-16              810       1452375 ns/op
BenchmarkConnectionInline
BenchmarkConnectionInline-16            2670        488782 ns/op

# 样本二
goos: darwin
goarch: amd64
cpu: Intel(R) Core(TM) i9-9880H CPU @ 2.30GHz
BenchmarkConnectionBlock
BenchmarkConnectionBlock-16             2161        538600 ns/op
BenchmarkConnectionInline
BenchmarkConnectionInline-16            3250        370422 ns/op

# 样本三
goos: darwin
goarch: amd64
cpu: Intel(R) Core(TM) i9-9880H CPU @ 2.30GHz
BenchmarkConnectionBlock
BenchmarkConnectionBlock-16             3247        376621 ns/op
BenchmarkConnectionInline
BenchmarkConnectionInline-16            3169        376633 ns/op

测试代码

example/connection-pool_benchmark.go

  1
  2
  3
  4
  5
  6
  7
  8
  9
 10
 11
 12
 13
 14
 15
 16
 17
 18
 19
 20
 21
 22
 23
 24
 25
 26
 27
 28
 29
 30
 31
 32
 33
 34
 35
 36
 37
 38
 39
 40
 41
 42
 43
 44
 45
 46
 47
 48
 49
 50
 51
 52
 53
 54
 55
 56
 57
 58
 59
 60
 61
 62
 63
 64
 65
 66
 67
 68
 69
 70
 71
 72
 73
 74
 75
 76
 77
 78
 79
 80
 81
 82
 83
 84
 85
 86
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
package connection_pool

import (
    "fmt"
    "github.com/gomodule/redigo/redis"
    "sync"
    "testing"
    "time"
)

func getPool() *redis.Pool {
    return &redis.Pool{
        MaxActive:   30,
        IdleTimeout: time.Second * 10,
        MaxIdle:     10,
        Wait:        true,
        Dial: func() (redis.Conn, error) {
            return redis.Dial("tcp", "localhost:6379")
        },
    }
}

//TestSingle 测试联通性
func TestSingle(t *testing.T) {
    pool := getPool()
    defer pool.Close()

    conn := pool.Get()
    defer conn.Close()

    _, err := conn.Do("INFO", "all")
    if err != nil {
        t.Error(err)
        return
    }
    //log.Println(err)
    //log.Println(string(res.([]byte)))
}

func BenchmarkConnectionBlock(b *testing.B) {
    pool := getPool()
    defer pool.Close()

    do := func(idx int) {
        conn := pool.Get()

        defer conn.Close()

        cacheKey := fmt.Sprintf("%s-%d", b.Name(), idx)

        conn.Do("SET", cacheKey, 0)
        <-time.After(time.Millisecond * 10)

        conn.Do("INCR", cacheKey, 1)
        <-time.After(time.Millisecond * 10)

        conn.Do("DECR", cacheKey, 1)
        <-time.After(time.Millisecond * 10)

        conn.Do("DEL", cacheKey)

    }

    //pool.Conn()
    b.ResetTimer()

    wg := sync.WaitGroup{}
    // 模拟并发请求
    for i := 0; i < b.N; i++ {
        wg.Add(1)
        go func(i int) {
            defer wg.Done()
            do(i)
        }(i)
    }

    wg.Wait()
}

func BenchmarkConnectionInline(b *testing.B) {
    pool := getPool()
    defer pool.Close()

    proxy := func(command string, args ...any) {
        conn := pool.Get()
        defer conn.Close()
        conn.Do(command, args...)
    }

    do := func(idx int) {
        cacheKey := fmt.Sprintf("%s-%d", b.Name(), idx)

        proxy("SET", cacheKey, 0)
        <-time.After(time.Millisecond * 10)

        proxy("INCR", cacheKey, 1)
        <-time.After(time.Millisecond * 10)

        proxy("DECR", cacheKey, 1)
        <-time.After(time.Millisecond * 10)

        proxy("DEL", cacheKey)
    }

    b.ResetTimer()

    wg := sync.WaitGroup{}
    // 模拟并发请求
    for i := 0; i < b.N; i++ {
        wg.Add(1)
        go func(i int) {
            defer wg.Done()
            do(i)
        }(i)
    }

    wg.Wait()
}

参考资料


  1. wiki-连接池 ↩︎

  2. 连接实例: 面向对象的一种抽象说法,将连接实例化; ↩︎ ↩︎ ↩︎

  3. wiki-堆栈 ↩︎