Go组件学习——手写连接池并没有那么简单
1、背景
前段时间在看gorm,发现gorm是复用database/sql的连接池。
于是翻了下database/sql的数据库连接池的代码实现,看完代码,好像也不是很复杂,但是总觉得理解不够深刻,于是萌生了自己想写个连接池的想法。(最后也验证了,看源码的理解确实不够深刻,一看就会,一做就跪)
2、连接池的实现原理
什么是连接池
- 顾名思义是一个池子
- 池子里面存放有限数量即时可用的连接,减少创建连接和关闭连接的时间
- 连接是有存活时间的
具体到数据库连接池,我根据自己的理解画了一张获取连接的流程图

从上图我们可以看出,除了连接池的容量大小,我们还有一个最大连接数的限制。池子里的连接让我们不用频繁的创建和关闭连接,同时应该也要有最大连接的限制,避免无限制的创建连接导致服务器资源耗尽,拖垮服务不可用。
池子中的连接也有存活时间,如果超过存活时间则会销毁连接。
3、实现连接池我们需要考虑哪些问题
3.1 功能点
获取连接
释放连接
Ping
关闭连接池
设置最大连接数和连接池容量(连接存活时间等等)
3.2 实现细节
- 连接应该有哪些属性,比如最大连接数、连接池容量、连接创建时间和存活时间
- 如何模拟使用连接池以及超过最大连接数后等待其他连接释放
- 如何保证在多协程操作下数据的一致性
- 如果实现连接的超时监听和通知
4、具体实现
这里的连接池实现包括
- 设置最大连接数和连接池容量
- 获取连接
- 释放连接
4.1 结构定义
定义Conn结构体,这里包含了几乎所有的有关连接需要的信息属性
type Conn struct {
maxConn int // 最大连接数
maxIdle int // 最大可用连接数
freeConn int // 线程池空闲连接数
connPool []int // 连接池
openCount int // 已经打开的连接数
waitConn map[int]chan Permission // 排队等待的连接队列
waitCount int // 等待个数
lock sync.Mutex // 锁
nextConnIndex NextConnIndex // 下一个连接的ID标识(用于区分每个ID)
freeConns map[int]Permission // 连接池的连接
}
这里并不会创建一个真正的数据库连接,而是使用一个非空的Permission表示拿到了连接。拿到一个非空的Permission才有资格执行后面类似增删改查的操作。
Permission对应的结构体如下
type Permission struct {
NextConnIndex // 对应Conn中的NextConnIndex
Content string // 通行证的具体内容,比如"PASSED"表示成功获取
CreatedAt time.Time // 创建时间,即连接的创建时间
MaxLifeTime time.Duration // 连接的存活时间,本次没有用到这个属性,保留
}
NextConnIndex对应的结构体如下
type NextConnIndex struct {
Index int
}
还有一个用来设置最大连接数以及连接池最大连接数的Config
type Config struct {
MaxConn int
MaxIdle int
}
4.2 初始化连接池参数
func Prepare(ctx context.Context, config *Config) (conn *Conn) {
// go func() {
//for {
//conn.expiredCh = make(chan string, len(conn.freeConns))
//for _, value := range conn.freeConns {
// if value.CreatedAt.Add(value.MaxLifeTime).Before(nowFunc()) {
// conn.expiredCh <- "CLOSE"
// }
//}
// }()
return &Conn{
maxConn: config.MaxConn,
maxIdle: config.MaxIdle,
openCount: 0,
connPool: []int{},
waitConn: make(map[int]chan Permission),
waitCount: 0,
freeConns: make(map[int]Permission),
}
}
这里主要是初始化上面的Conn结构体参数。
注释的部分,主要想通过启动一个监听协程,用于监听已经过期的连接,并通过channel发送。(这块还有一些细节没有想清楚,先搁置)
4.3 设置MaxConn和MaxIdle
在main.go中添加代码
ctx := context.Background()
config := &custom_pool.Config{
MaxConn: 2,
MaxIdle: 1,
}
这里意味连接池只能缓存一个连接,最大新建连接数为2,超过则要加入等待队列。
4.4 获取连接
// 创建连接
func (conn *Conn) New(ctx context.Context) (permission Permission, err error) {
/**
1、如果当前连接池已满,即len(freeConns)=0
2、判定openConn是否大于maxConn,如果大于,则丢弃获取加入队列进行等待
3、如果小于,则考虑创建新连接
*/
conn.lock.Lock() select {
default:
case <-ctx.Done(): // context取消或超时,则退出
conn.lock.Unlock() return Permission{}, errors.New("new conn failed, context cancelled!")
} // 连接池不为空,从连接池获取连接
if len(conn.freeConns) > 0 {
var (
popPermission Permission
popReqKey int
) // 获取其中一个连接
for popReqKey, popPermission = range conn.freeConns {
break
}
// 从连接池删除
delete(conn.freeConns, popReqKey)
fmt.Println("log", "use free conn!!!!!", "openCount: ", conn.openCount, " freeConns: ", conn.freeConns)
conn.lock.Unlock()
return popPermission, nil
} if conn.openCount >= conn.maxConn { // 当前连接数大于上限,则加入等待队列
nextConnIndex := getNextConnIndex(conn) req := make(chan Permission, 1)
conn.waitConn[nextConnIndex] = req
conn.waitCount++
conn.lock.Unlock() select {
// 如果在等待指定超时时间后,仍然无法获取释放连接,则放弃获取连接,这里如果不在超时时间后退出会一直阻塞
case <-time.After(time.Second * time.Duration(3)):
fmt.Println("超时,通知主线程退出")
return
case ret, ok := <-req: // 有放回的连接, 直接拿来用
if !ok {
return Permission{}, errors.New("new conn failed, no available conn release")
}
fmt.Println("log", "received released conn!!!!!", "openCount: ", conn.openCount, " freeConns: ", conn.freeConns)
return ret, nil
}
return Permission{}, errors.New("new conn failed")
} // 新建连接
conn.openCount++
conn.lock.Unlock()
permission = Permission{NextConnIndex: NextConnIndex{nextConnIndex},
Content: "PASSED", CreatedAt: nowFunc(), MaxLifeTime: time.Second * 5}
fmt.Println("log", "create conn!!!!!", "openCount: ", conn.openCount, " freeConns: ", conn.freeConns)
return permission, nil
}
这里主要分为三个部分
如果连接池不为空,则直接从池子里面获取连接使用即可
如果连接池为空,且当前的连接数已经超过最大连接数maxConn,则会将当前任务加入等待队列,同时监听是否有释放的可用连接,如果有则拿来直接用,如果超过指定等待时间后仍然取不到连接则退出阻塞返回。
如果连接池为空,且尚未达到最大连接数maxConn,则新建一个新连接。
getNextConnIndex函数
func getNextConnIndex(conn *Conn) int {
currentIndex := conn.nextConnIndex.Index
conn.nextConnIndex.Index = currentIndex + 1
return conn.nextConnIndex.Index
}
4.5 释放连接
// 释放连接
func (conn *Conn) Release(ctx context.Context) (result bool, err error) {
conn.lock.Lock()
// 如果等待队列有等待任务,则通知正在阻塞等待获取连接的进程(即New方法中"<-req"逻辑)
// 这里没有做指定连接的释放,只是保证释放的连接会被利用起来
if len(conn.waitConn) > 0 {
var req chan Permission
var reqKey int
for reqKey, req = range conn.waitConn {
break
}
// 假定释放的连接就是下面新建的连接
permission := Permission{NextConnIndex: NextConnIndex{reqKey},
Content: "PASSED", CreatedAt: nowFunc(), MaxLifeTime: time.Second * 5}
req <- permission
conn.waitCount--
delete(conn.waitConn, reqKey)
conn.lock.Unlock()
} else {
if conn.openCount > 0 {
conn.openCount-- if len(conn.freeConns) < conn.maxIdle { // 确保连接池大小不会超过maxIdle
nextConnIndex := getNextConnIndex(conn)
permission := Permission{NextConnIndex: NextConnIndex{nextConnIndex},
Content: "PASSED", CreatedAt: nowFunc(), MaxLifeTime: time.Second * 5}
conn.freeConns[nextConnIndex] = permission
}
}
conn.lock.Unlock()
}
return
}
这里主要分为两部分
- 如果释放连接的时候发现等待队列有任务在等待,则将释放的连接通过channel发送,给正在等待连接释放的阻塞任务使用,同时从等待队列中删除该任务。
- 如果当前无等待任务,则将连接放入连接池
这里的nowFunc
var nowFunc = time.Now
5、Case模拟
5.1 无释放创建连接
即只有创建连接,拿到连接也不会释放连接
package main import (
"context"
custom_pool "go-demo/main/src/custom-pool"
) func main() { ctx := context.Background()
config := &custom_pool.Config{
MaxConn: 2,
MaxIdle: 1,
}
conn := custom_pool.Prepare(ctx, config)
if _, err := conn.New(ctx); err != nil {
return
}
if _, err := conn.New(ctx); err != nil {
return
}
if _, err := conn.New(ctx); err != nil {
return
}
if _, err := conn.New(ctx); err != nil {
return
}
if _, err := conn.New(ctx); err != nil {
return
}
}
执行结果如下

注意上面代码都是一直在获取连接,在获取连接后没有释放连接。
第一次获取,连接池为空,则新建连接
第二次获取,连接池为空,继续新建连接
第三次获取,连接池为空,同时已有连接数>=maxConn,所以会阻塞等待释放连接,但是因为没有连接释放,所以一直等待,直到3秒超时后退出。
所以第三次、第四次和第五次都是超时退出
5.2 释放连接
如果我们释放连接会怎么样,我们可以通过新启一个协程用于释放一个连接如下
package main import (
"context"
custom_pool "go-demo/main/src/custom-pool"
) func main() { ctx := context.Background()
config := &custom_pool.Config{
MaxConn: 2,
MaxIdle: 1,
}
conn := custom_pool.Prepare(ctx, config)
if _, err := conn.New(ctx); err != nil {
return
}
if _, err := conn.New(ctx); err != nil {
return
}
go conn.Release(ctx)
if _, err := conn.New(ctx); err != nil {
return
}
if _, err := conn.New(ctx); err != nil {
return
}
if _, err := conn.New(ctx); err != nil {
return
}
}
执行结果如下
log create conn!!!!! openCount: 1 freeConns: map[]
log create conn!!!!! openCount: 2 freeConns: map[]
log received released conn!!!!! openCount: 2 freeConns: map[]
超时,通知主线程退出
超时,通知主线程退出
前两次和上面一样,但是第三次获取的时候,会收到一个释放的连接,所以可以直接复用释放的连接返回。
但是第四次和第五次创建,因为没有释放的连接,所以都会因为等待超时后退出。
5.3 使用连接池
上面的两个case是在MaxConn=2,MaxIdle=1的情况下执行的。
下面我们看看如果基于以上两个参数设定,模拟出正好使用连接池的情况。
package main import (
"context"
custom_pool "go-demo/main/src/custom-pool"
) func main() { ctx := context.Background()
config := &custom_pool.Config{
MaxConn: 2,
MaxIdle: 1,
}
conn := custom_pool.Prepare(ctx, config)
if _, err := conn.New(ctx); err != nil {
return
}
go conn.Release(ctx)
if _, err := conn.New(ctx); err != nil {
return
}
go conn.Release(ctx)
if _, err := conn.New(ctx); err != nil {
return
}
go conn.Release(ctx)
if _, err := conn.New(ctx); err != nil {
return
}
go conn.Release(ctx)
if _, err := conn.New(ctx); err != nil {
return
}
}
即除了第一次,后面都会有连接释放。
执行结果可能情况如下
log create conn!!!!! openCount: 1 freeConns: map[]
log create conn!!!!! openCount: 2 freeConns: map[]
log use free conn!!!!! openCount: 1 freeConns: map[]
log use free conn!!!!! openCount: 0 freeConns: map[]
log create conn!!!!! openCount: 1 freeConns: map[]
从执行结果可以看出,这里有两次使用了连接池中的连接。
注意:因为释放是新启协程执行,所以无法保证执行顺序,不同的执行顺序,会有不同的执行结果。上面只是执行结果的一种。
以上完整代码参见https://github.com/DMinerJackie/go-demo/tree/master/main/src/custom-pool
6、总结和展望
6.1 总结
- 通过手写连接池加深对于连接池实现的理解
- 学会使用channel和协程
- 学会如何在channel阻塞指定时间后退出(设立超时时间)
- 学会对于共享资源加锁,比如nextConnIndex的获取和更新需要加锁
6.2 展望
- Close和Ping没有写(实现不难)
- 连接池连接需要有存活时间,并在连接过期的时候从连接池删除
- 实现使用的是普通的map集合,可以考虑并发安全的syncMap
- 代码实现比较简陋不够优雅,可以继续完善保证职责单一
如果您觉得阅读本文对您有帮助,请点一下“推荐”按钮,您的“推荐”将是我最大的写作动力!如果您想持续关注我的文章,请扫描二维码,关注JackieZheng的微信公众号,我会将我的文章推送给您,并和您一起分享我日常阅读过的优质文章。
Go组件学习——手写连接池并没有那么简单的更多相关文章
- 手写线程池,对照学习ThreadPoolExecutor线程池实现原理!
作者:小傅哥 博客:https://bugstack.cn Github:https://github.com/fuzhengwei/CodeGuide/wiki 沉淀.分享.成长,让自己和他人都能有 ...
- Java自己动手写连接池四
Java自己动手写连接池四 测试: package com.kama.cn; import java.sql.Connection; public class Test { public static ...
- Java自己动手写连接池三
Java自己动手写连接池三,核心代码; package com.kama.cn; import java.sql.Connection;import java.util.ArrayList;impor ...
- Java自己动手写连接池一
自己动手写连接池,废话不多说,直接上代码,读取配置文件 package com.kama.cn; import java.io.IOException;import java.io.InputStre ...
- MyBatis学习-使用Druid连接池将Maybatis整合到spring
目录 前言 什么是Druid连接池 Druid可以做什么? 导入库包 连接oracle 连接mysql 导入mybatis 导入druid 导入spring-jdbc包 导入spring包 导入spr ...
- spring boot 学习(四)Druid连接池的使用配置
Druid介绍 Druid是一个JDBC组件,druid 是阿里开源在 github 上面的数据库连接池,它包括三部分: * DruidDriver 代理Driver,能够提供基于Filter-Cha ...
- common-pool2 学习:thrift连接池的另一种实现
对象池是一种很实用的技术,经典的例子就是数据库连接池.去年曾经从零开始写过一个thrift客户端连接池.如果不想重造轮子,可以直接在apache开源项目commons-pool的基础上开发. 步骤: ...
- 深度学习---手写字体识别程序分析(python)
我想大部分程序员的第一个程序应该都是“hello world”,在深度学习领域,这个“hello world”程序就是手写字体识别程序. 这次我们详细的分析下手写字体识别程序,从而可以对深度学习建立一 ...
- MySQL学习(六)——自定义连接池
1.连接池概念 用池来管理Connection,这样可以重复使用Connection.有了池,我们就不用自己来创建Connection,而是通过池来获取Connection对象.当使用完Connect ...
随机推荐
- 小伙子,你真的清楚 JVM GC ?
序 正文 如何确定垃圾? 前面已经提到 JVM 可以采用 引用计数法 与 可达性分析算法 来确定需要回收的垃圾,我们来具体看一下这两种算法: 引用计数法 该方法实现为:给每个对象添加一个引用计数器,每 ...
- Kibana对数据的可视化
基于上一篇的操作,我们已经获得了数据,接下来我们就要处理数据,因此选用了Kibana 先来介绍一下, Kibana是一个针对Elasticsearch的开源分析及可视化平台,用来搜索.查看交互存储在E ...
- Button 使用详解
极力推荐文章:欢迎收藏 Android 干货分享 阅读五分钟,每日十点,和您一起终身学习,这里是程序员Android 本篇文章主要介绍 Android 开发中的部分知识点,通过阅读本篇文章,您将收获以 ...
- Broadcast 使用详解
极力推荐文章:欢迎收藏 Android 干货分享 阅读五分钟,每日十点,和您一起终身学习,这里是程序员Android 本篇文章主要介绍 Android 开发中的部分知识点,通过阅读本篇文章,您将收获以 ...
- 伽马变换(一些基本的灰度变换函数)基本原理及Python实现
1. 基本原理 变换形式 $$s=cr^{\gamma}$$ c与$\gamma$均为常数 可通过调整$\gamma$来调整该变换,最常用于伽马校正与对比度增强 2. 测试结果 图源自skimage ...
- 初识JavaScript和面向对象
1.javascript基本数据类型: number: 数值类型 string: 字符串类型 boolean: 布尔类型 null: 空类型 undefault:未定义类型 object: 基本数据类 ...
- Element-UI 表单验证规则rules 配置参数说明
官方文档 : https://github.com/yiminghe/async-validator
- Oracle中的转换函数
Oracle中的转换函数有三个,分别为to_char(),to_date(),to_number() 1.to_char()的用法 格式化当前的日期时间 select sysdate,to_char( ...
- Windos 上逆天又好用的软件有哪些?
谷歌浏览器 Chrome 浏览器是大名鼎鼎的科技公司谷歌开发的一款浏览器,国内的360浏览器等大多都是基于谷歌开源出的浏览器内核,然后给他穿了一层360的衣服.至于性能和启动速度上来讲,我个人觉得Ch ...
- python对接常用数据库,快速上手!
python对接常用数据库,快速上手! 很多同学在使用python进行自动化测试的时候,会涉及到数据库数据校验的问题,因为不知道如何在python中如何对数据库,这个时候会一脸茫然,今天在这里给大家汇 ...
