业务服务器与数据库提前建立好连接,并将这些连接一直保持,当有业务到来时,直接使用这些连接来处理业务,这样的技术叫连接池技术。使用连接池能够减少资源对象的创建次数,提⾼程序的响应性能,特别是在⾼并发下这种提⾼更加明显。比如在mysql连接池中,可以尽量避免连接三次握手四次挥手等非业务流程带来的损耗。池化技术,本质上说就是资源复用。
首先解决一个问题,为什么需要多个连接?对于数据库而言,每个连接都是独占的,如果一个业务占用连接,而又进入了IO等待,那么这个业务就会一直占用这个连接,其他业务也只能等待。如果开多条连接,不同的业务就可以利用不同的连接通道来处理。
一般情况下,线程池线程数量尽量与连接池中的连接数量一致。这样做可以避免不同线程之间抢资源或是资源过多线程处理不过来导致浪费的情况。注意这里说的一般情况,是业务服务器与数据库服务器性能差不多的情况。其实,数据库服务器也是使用多线程来处理业务,如果连接数过多,数据库会处理不过来。
另外,如果不使用连接池,就需要不同的线程去绑定不同的连接,使得整个系统的耦合度变高。还要注意一点的是,在一个数据库服务器中可能有多个连接池。比如,一个连接池对应写业务,与一个只处理写业务的线程池,共同处理写数据库,而再用另一套连接池只处理读业务,与只读数据库连接,这样做就实现了读写分离。
这里给出一种设计方法,是将连接和连接池分成两个类来定义。
class CDBConn {
public:
CDBConn(CDBPool* pDBPool);
virtual ~CDBConn();
int Init();
// 创建表
bool ExecuteCreate(const char* sql_query);
// 删除表
bool ExecuteDrop(const char* sql_query);
// 查询
CResultSet* ExecuteQuery(const char* sql_query);
//修改表中数据
bool ExecuteUpdate(const char* sql_query, bool care_affected_rows = true);
uint32_t GetInsertId();
// 开启事务
bool StartTransaction();
// 提交事务
bool Commit();
// 回滚事务
bool Rollback();
// 获取连接池名
const char* GetPoolName();
MYSQL* GetMysql() { return m_mysql; }
private:
CDBPool* m_pDBPool; // to get MySQL server information
MYSQL* m_mysql; // 对应一个连接
char m_escape_string[MAX_ESCAPE_STRING_LEN + 1];
};
class CDBPool { // 只是负责管理连接CDBConn,真正干活的是CDBConn
public:
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);
virtual ~CDBPool();
int Init(); // 连接数据库,创建连接
CDBConn* GetDBConn(const int timeout_ms = -1); // 获取连接资源
void RelDBConn(CDBConn* pConn); // 归还连接资源
const char* GetPoolName() { return m_pool_name.c_str(); }
const char* GetDBServerIP() { return m_db_server_ip.c_str(); }
uint16_t GetDBServerPort() { return m_db_server_port; }
const char* GetUsername() { return m_username.c_str(); }
const char* GetPasswrod() { return m_password.c_str(); }
const char* GetDBName() { return m_db_name.c_str(); }
private:
string m_pool_name; // 连接池名称
string m_db_server_ip; // 数据库ip
uint16_t m_db_server_port; // 数据库端口
string m_username; // 用户名
string m_password; // 用户密码
string m_db_name; // db名称
int m_db_cur_conn_cnt; // 当前启用的连接数量
int m_db_max_conn_cnt; // 最大连接数量
list<CDBConn*> m_free_list; // 空闲的连接
list<CDBConn*> m_used_list; // 记录已经被请求的连接
std::mutex m_mutex;
std::condition_variable m_cond_var;
bool m_abort_request = false;
// CThreadNotify m_free_notify; // 信号量
};
int CDBPool::Init()
{
// 创建固定最小的连接数量
for (int i = 0; i < m_db_cur_conn_cnt; i++)
{
CDBConn *pDBConn = new CDBConn(this);
int ret = pDBConn->Init();
if (ret)
{
delete pDBConn;
return ret;
}
m_free_list.push_back(pDBConn);
}
// log_error("db pool: %s, size: %d\n", m_pool_name.c_str(), (int)m_free_list.size());
return 0;
}
注意这里的Init()函数还具有建立连接的功能(主要是在CDBConn::Init()函数中)。然后还需要将建立好的连接放入容器进行管理。
应当注意,mysql中utf8编码为固定3字节,与正常utf8编码方式不一样,为保持一致,应使用utf8mb4编码。
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;
}
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;
}
}
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");
}
}
这里避免重复归还,主要是避免业务中出现逻辑问题而导致的错误。
这里补充一下,在mysql数据库中,默认8小时内连接没有数据到达的话就主动断开,所以需要连接池有重连功能。
mysql_ping(m_mysql); // 如果断开了,能够自动重连
这一点已在代码中体现
首先应检查业务代码,因为如果在数据库中增加检测机制,会使代码变得复杂。如果真要设计,思路是创建一个容器管理已被请求的连接,然后另起一个线程去遍历检测该容器,如果连接被请求的时间超过指定时间,则强制归还。
以上两个设计都是针对超时相关的统计。连接池的连接数如果不够用的话,业务就总是会处于等待,这时就需要增加连接数,以优化性能。
最后,还要再说明一点,以上是同步的做法,mysql也可以把业务处理的流程做成异步的,和这里的连接池的做法不同,需要一定的功底,这里就不介绍了。