池式组件-Mysql连接池的原理与实现

本文详细介绍了Mysql连接池的使用原因,包括减少网络开销、提高性能和管理数据库连接。讨论了连接池与线程池的关系,并展示了C++实现的连接池代码,包括构造、初始化、获取和归还连接的细节。此外,还提及了连接池的命名用途、重连机制以及如何根据任务特性选择合适的线程/连接数量。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

一、为什么要使用连接池?

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

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

菊头蝙蝠

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值