一、为什么要使用连接池?
在一般情况下,客户端向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等待 和 只建立连接不执行任务,时间上的区别,然后选择合适的线程个数。