数据库连接池

  为了避免频繁的创建、释放连接引起的性能开销,于是引入了连接池来得到资源的服用,能更快的系统响应,以及统一的连接管理,避免了数据库连接泄露。

  数据库连接池设计大同小异,主要考虑几个问题:如果通过队列管理连接、如何获取连接、如何归还连接、如何处理扩容问题等。解决以上问题,基本就可以实现一个简单的连接池。

  首先明确一点,就是连接池只是负责管理连接对象,而连接对象才是真正干活的部分。

  (1)连接对象设计

    这里的连接对象(文中为 CDBConnect)中包括连接建立、释放、数据库表的操作(增删改查)、事务(开启、提交、回滚)等方法的实现。

//初始化建立连接
int CDBConnect::Init(const char* db_server_ip, uint16_t db_server_port,const char* username, const char* password, const char* db_name)
{
	m_mysql = mysql_init(NULL);	// mysql_标准的mysql c client对应的api
	if (!m_mysql)
	{
		log_error("mysql_init failed\n");
		return -1;
	}

	if (!db_server_ip)
	{
		log_error("db_server_ip is null\n");
		return -1;
	}
	
	if (!username)
	{
		log_error("username is null\n");
		return -1;
	}

	if (!password)
	{
		log_error("password is null\n");
		return -1;
	}

	if (!db_name)
	{
		log_error("db_name is null\n");
		return -1;
	}

	bool reconnect = true;
	mysql_options(m_mysql, MYSQL_OPT_RECONNECT, &reconnect);	// 配合mysql_ping实现自动重连
	mysql_options(m_mysql, MYSQL_SET_CHARSET_NAME, "utf8mb4");	// utf8mb4和utf8区别

	// ip、用户名、密码、数据库名、端口
	if (!mysql_real_connect(m_mysql, db_server_ip, username, password, db_name, db_server_port, NULL, 0))
	{
		log_error("mysql_real_connect failed: %s\n", mysql_error(m_mysql));
		return -1;
	}

	return 0;
}

  执行查询,首先对返回结果对象,单独经过封装,实现表中字段类型值的封装,便于获取值。

//结果集
CResultSet::CResultSet(MYSQL_RES *res)
{
	m_res = res;

	// map table field key to index in the result array
	int num_fields = mysql_num_fields(m_res);
	MYSQL_FIELD *fields = mysql_fetch_fields(m_res);
	for (int i = 0; i < num_fields; i++)
	{
		// 多行
		m_key_map.insert(make_pair(fields[i].name, i));
	}
}

CResultSet::~CResultSet()
{
	if (m_res)
	{
		mysql_free_result(m_res);
		m_res = NULL;
	}
}

bool CResultSet::Next()
{
	m_row = mysql_fetch_row(m_res);
	if (m_row)
	{
		return true;
	}
	else
	{
		return false;
	}
}

int CResultSet::_GetIndex(const char *key)
{
	map<string, int>::iterator it = m_key_map.find(key);
	if (it == m_key_map.end())
	{
		return -1;
	}
	else
	{
		return it->second;
	}
}

int CResultSet::GetInt(const char *key)
{
	int idx = _GetIndex(key);
	if (idx == -1)
	{
		return 0;
	}
	else
	{
		return atoi(m_row[idx]); // 有索引
	}
}

char *CResultSet::GetString(const char *key)
{
	int idx = _GetIndex(key);
	if (idx == -1)
	{
		return NULL;
	}
	else
	{
		return m_row[idx];		// 列
	}
}

  查询实现如下:

//执行查询,在使用完结果集CResultSet后记得释放
CResultSet *CDBConnect::ExecuteQuery(const char *sql_query)
{
	mysql_ping(m_mysql);

	if (mysql_real_query(m_mysql, sql_query, strlen(sql_query)))
	{
		log_error("mysql_real_query failed: %s, sql: %s\n", mysql_error(m_mysql), sql_query);
		return NULL;
	}
	// 返回结果
	MYSQL_RES *res = mysql_store_result(m_mysql);	// 返回结果
	if (!res)
	{
		log_error("mysql_store_result failed: %s\n", mysql_error(m_mysql));
		return NULL;
	}

	CResultSet *result_set = new CResultSet(res);	// 存储到CResultSet
	return result_set;
}

 事务操作如下:

//开启事务
bool CDBConnect::StartTransaction()
{
	mysql_ping(m_mysql);

	if (mysql_real_query(m_mysql, "start transaction\n", 17))
	{
		log_error("mysql_real_query failed: %s, sql: start transaction\n", mysql_error(m_mysql));
		return false;
	}

	return true;
}

//回滚
bool CDBConnect::Rollback()
{
	mysql_ping(m_mysql);

	if (mysql_real_query(m_mysql, "rollback\n", 8))
	{
		log_error("mysql_real_query failed: %s, sql: rollback\n", mysql_error(m_mysql));
		return false;
	}

	return true;
}

//提交
bool CDBConnect::Commit()
{
	mysql_ping(m_mysql);

	if (mysql_real_query(m_mysql, "commit\n", 6))
	{
		log_error("mysql_real_query failed: %s, sql: commit\n", mysql_error(m_mysql));
		return false;
	}

	return true;
}

 (2)队列管理

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; // 最小连接数量
}

// 释放连接池
CDBPool::~CDBPool()
{
	std::lock_guard<std::mutex> lock(m_mutex);
	m_abort_request = true;
	m_cond_var.notify_all();		// 通知所有在等待的

	//强制删除已用的连接
	for (list<CDBConnect *>::iterator it = m_used_list.begin(); it != m_used_list.end(); it++)
	{
		CDBConnect *pConn = *it;
		delete pConn;
	}
	m_used_list.clear();

	//删除空闲的连接
	for (list<CDBConnect *>::iterator it = m_free_list.begin(); it != m_free_list.end(); it++)
	{
		CDBConnect *pConn = *it;
		delete pConn;
	}
	m_free_list.clear();
	
}

int CDBPool::Init()
{
	// 创建固定最小的连接数量
	for (int i = 0; i < m_db_cur_conn_cnt; i++)
	{
		CDBConnect *pDBConn = new CDBConnect();
		int ret = pDBConn->Init(m_db_server_ip.c_str(),m_db_server_port,m_username.c_str(),m_password.c_str(),m_db_name.c_str());
		if (ret < 0)
		{
			delete pDBConn;
			return ret;
		}
		//添加到空闲的链表
		m_free_list.push_back(pDBConn);
	}
	return 0;
}

/*
 * timeout_ms默认为-1死等
 * timeout_ms >=0 则为等待的时间
 */
CDBConnect *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] 
				{
					// 当前连接数量小于最大连接数量 或者请求释放连接池时退出
					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] {
					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 // 还没有到最大连接则创建连接,扩容
		{
			CDBConnect *pDBConn = new CDBConnect();	//新建连接
			int ret = pDBConn->Init(m_db_server_ip.c_str(),m_db_server_port,m_username.c_str(),m_password.c_str(),m_db_name.c_str());
			if (ret < 0)
			{
				log_error("Init DBConnecton failed\n\n");
				delete pDBConn;
				return NULL;
			}

			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);
		}
	}

	CDBConnect *pConn = m_free_list.front();// 获取连接
	m_free_list.pop_front();	// 从空闲队列删除
	// pConn->setCurrentTime();  // 设置连接使用的开始时间
	m_used_list.push_back(pConn);
	return pConn;
}

void CDBPool::RelDBConn(CDBConnect *pConn)
{
	std::lock_guard<std::mutex> lock(m_mutex);

	list<CDBConnect *>::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");
	}
}
// 遍历检测是否超时未归还
// pConn->isTimeout(); // 当前时间 - 被请求的时间
// 强制回收  从m_used_list 放回 m_free_list

  以上代码中的初始化、释放都比较好理解;对于从连接池获取一个连接,考虑到扩容处理,当空闲链表为空时,需要扩容,先判断当前连接数量是否已经大于连接数量上限,如果不大于,则新建一个连接,并添加到空闲链表中,如果大于,则进行超时处理;如果空闲链表不为空,则从空闲链表头部,取出一个连接,此处使用了另外一个链表m_used_list,保存已使用的连接,便于做连接使用超时处理。连接使用完后,进行归还操作,首先在空闲链表中遍历归还的连接是否存在,避免重复归还,如果在空闲链表中找不到,则把连接重新添加回空闲链表,并从已使用链表中删除该连接。

  对于扩展进行连接超时处理,单独开启一个线程检测链表m_used_list中连接使用时间,如果超时,则强制归还,从m_used_list 放回 m_free_list中。

   以上是MySql的连接池,如果是Redis,连接池的管理不变,主要变化就是连接对象的实现(包括常用指令对应的实现方法:get、set、mget、incr、decr、hget、hset、hmset、hmget、lpush、rpush、lrange.....等),使用开源hiredis进行封装,hiredis提供了C语言版本访问redis的常用操作,并且支持一次发送多条指令,所有结果一次返回(异步请求方式),不像MySQL单个连接执行一条指令,必须等待响应返回后,才能执行下一条指令(同步请求方式)。

 

posted @ 2022-07-01 11:37  MrJuJu  阅读(74)  评论(0编辑  收藏  举报