池式组件-Mysql连接池的原理与实现
一、为什么要使用连接池?
在一般情况下,客户端向Mysql服务器的连接,要经过以下几个步骤
1.TCP三次握手连接
2.Mysql认证的三次握手
3.真正的SQL执行
4.Mysql的关闭
5.TCP的四次握手关闭
这种方式,网络IO较多,带宽利用率低,每秒的查询次数(QPS)会降低,大量的时间花费在建立连接和关闭连接上面,在关闭连接后会出现大量TIME_WAIT的TCP状态。
使用连接池的好处
使用连接池,只需要在第一次访问的时候去建立连接,之后的访问,都会使用之前创建的连接,这样1.可降低网络开销,2.增加连接复用,有效减少连接数 3.提升性能,避免频繁新建与销毁连接 4.没有TIME_WAIT状态的问题。
可以通过连接池去管理这些连接。
msql连接是短连接,如果长时间不使用会断开,使用连接池,可以减少连接的数量,过长时间断开的情况减少,就会减少重连情况,提升效率
二、连接池与线程池的关系
当线程池中一个线程中的任务要使用连接,那么连接池会为它分配一个连接对象,使用完后,将连接对象归还致连接池。
一般,线程池中的线程数量与连接池中连接对象数量一致。线程任务执行完成要归还连接对象。
三、连接池的涉及要点
使用连接池首要先连接数据库
1.创建一个连接池(init),并连接到数据库(ip、port、user、password、dbname等),配置最小连接数,最大连接数
2.管理连接池的连接,比如list
3.获取连接对象
4.归还连接对象
5.连接池的名字
四、连接池代码实现
1、连接池构造函数
用于配置数据库连接的内容
// 构造函数
CDBPool::CDBPool(const char *pool_name, const char *db_server_ip, uint16_t db_server_port,
const char *username, const char *password, const char *db_name, int max_conn_cnt)
{
m_pool_name = pool_name;
m_db_server_ip = db_server_ip;
m_db_server_port = db_server_port;
m_username = username;
m_password = password;
m_db_name = db_name;
m_db_max_conn_cnt = max_conn_cnt; // 最大连接数
m_db_cur_conn_cnt = MIN_DB_CONN_CNT; // 最小连接数量(开始先创建最小连接数量个 连接)
}
2、连接池初始化
初始化连接,创建固定个连接对象,加入到空闲队列free_list
中,用于后续取用。
//连接池初始化
int CDBPool::Init()
{
// 创建固定最小的连接数量
for (int i = 0; i < m_db_cur_conn_cnt; i++)//创建最小连接数个连接放入,空闲队列free_list中
{
CDBConn *pDBConn = new CDBConn(this);//创建连接
int ret = pDBConn->Init();
if (ret)
{
delete pDBConn;
return ret;
}
m_free_list.push_back(pDBConn);//连接放入空闲队列
}
// log_info("db pool: %s, size: %d\n", m_pool_name.c_str(), (int)m_free_list.size());
return 0;
}
3、请求获取连接
注意要加锁:避免多线程获取到同样的对象,需要保证每个任务获得得连接是唯一的,不允许两个任务共用一个连接
/*
*TODO: 增加保护机制,把分配的连接加入另一个队列,这样获取连接时,如果没有空闲连接,
*TODO: 检查已经分配的连接多久没有返回,如果超过一定时间,则自动收回连接,放在用户忘了调用释放连接的接口
* timeout_ms默认为 0死等
* timeout_ms >0 则为等待的时间
*/
int wait_cout = 0;
CDBConn *CDBPool::GetDBConn(const int timeout_ms)//获得连接
{
std::unique_lock<std::mutex> lock(m_mutex);
if(m_abort_request)
{
log_warn("have aboort\n");
return NULL;
}
if (m_free_list.empty()) // 当没有连接可以用时
{
// 第一步先检测 当前连接数量是否达到最大的连接数量
if (m_db_cur_conn_cnt >= m_db_max_conn_cnt)
{
// 如果已经到达了,看看是否需要超时等待
if(timeout_ms <= 0) // 死等,直到有连接可以用 或者 连接池要退出
{
log_info("wait ms:%d\n", timeout_ms);
m_cond_var.wait(lock, [this]
{
// log_info("wait:%d, size:%d\n", wait_cout++, m_free_list.size());
// 当前连接数量小于最大连接数量 或者请求释放连接池时退出
return (!m_free_list.empty()) | m_abort_request;
});
} else {
// return如果返回 false,继续wait(或者超时), 如果返回true退出wait
// 1.m_free_list不为空
// 2.超时退出
// 3. m_abort_request被置为true,要释放整个连接池
m_cond_var.wait_for(lock, std::chrono::milliseconds(timeout_ms), [this] {
// log_info("wait_for:%d, size:%d\n", wait_cout++, m_free_list.size());
return (!m_free_list.empty()) | m_abort_request;
});
// 带超时功能时还要判断是否为空
if(m_free_list.empty()) // 如果连接池还是没有空闲则退出
{
return NULL;
}
}
if(m_abort_request)
{
log_warn("have aboort\n");
return NULL;
}
}
else // 还没有到最大连接则创建连接
{
CDBConn *pDBConn = new CDBConn(this); //新建连接
int ret = pDBConn->Init();
if (ret)
{
log_error("Init DBConnecton failed\n\n");
delete pDBConn;
return NULL;
}
else
{
m_free_list.push_back(pDBConn);
m_db_cur_conn_cnt++;
// log_info("new db connection: %s, conn_cnt: %d\n", m_pool_name.c_str(), m_db_cur_conn_cnt);
}
}
}
CDBConn *pConn = m_free_list.front(); // 获取连接
m_free_list.pop_front(); // STL 吐出连接,从空闲队列删除
// pConn->setCurrentTime(); // 伪代码
m_used_list.push_back(pConn);
return pConn;//返回 连接
}
4、归还连接
void CDBPool::RelDBConn(CDBConn *pConn)//归还连接
{
std::lock_guard<std::mutex> lock(m_mutex);
list<CDBConn *>::iterator it = m_free_list.begin();
for (; it != m_free_list.end(); it++) // 避免重复归还(虽然不会出现,但尽量避免)
{
if (*it == pConn) //重复归还 就break
{
break;
}
}
if (it == m_free_list.end())//确实没有归还
{
m_used_list.remove(pConn);
m_free_list.push_back(pConn);
m_cond_var.notify_one(); // 通知取队列
} else
{
log_error("RelDBConn failed\n");
}
}
// 遍历检测是否超时未归还
// pConn->isTimeout(); // 当前时间 - 被请求的时间
// 强制回收 从m_used_list 放回 m_free_list
5、析构连接池
遍历销毁连接,如果有连接还没有返回来,有泄漏问题。 如果真的要销毁连接池
1:先销毁线程池,确保所有任务退出
2:再去销毁连接池
连接需要归还了,才能去销毁
1.资源申请释放的顺序非常重要
2.异步编程时比较容易崩,资源被释放了异步函数可能还在使用
CDBPool::~CDBPool()
{
std::lock_guard<std::mutex> lock(m_mutex);
m_abort_request = true;
m_cond_var.notify_all(); // 通知所有在等待的
for (list<CDBConn *>::iterator it = m_free_list.begin(); it != m_free_list.end(); it++) //可能发生泄漏
{
CDBConn *pConn = *it;
delete pConn;
}
m_free_list.clear();
}
五、为什么要有连接池名
1.debug的时候,可以通过连接池名字识别连接
2.统一管理连接池中
可以通过连接池名,映射到指定的连接池
Map:连接池名—>连接池
比如:
GetConn("聊天消息连接池")
GetConn("用户信息连接池 ");
同一个接口返回的连接是不一样的。方便管理和使用。
六、重连机制
1.每次操作之前先去测试链路是否通(任务能更大概率执行成功)
本次mysql连接池中就使用这种方法。
2.先去执行任务,再去处理连接不同的问题
redis中重连采用这种办法
string CacheConn::get(string key)
{
string value;
if(Init())
{
return value;
}
}
其中CacheConn::Init()
1.检测连接是否初始化
2.如果没有初始化就重新初始化
3.如果已经初始化直接返回
七、对于连接池来说,选择多少线程/连接合适呢?
测试 连接+执行任务+IO等待 和 只建立连接不执行任务,时间上的区别,然后选择合适的线程个数。