TinywebServer代码详解– 数据库连接池(11)

该blog内容转自:最新版Web服务器项目详解 - 11 数据库连接池

该blog对上述内容进行补充(在本人的角度)

结合此前记录的blog一起学习:牛客WebServer项目实战(点击跳转)

结合此前记录的blog一起学习:多线程与线程同步(点击跳转)

关于数据线程池相关blog(点击跳转)

原项目地址(点击跳转)

博主添加注释后项目地址(点击跳转)



一、基础知识

1.数据库连接池

池是一组资源的集合,这组资源在服务器启动之初就被完全创建好并初始化。通俗来说,池是资源的容器,本质上是对资源的复用

连接池中的资源为一组数据库连接,由程序动态地对池中的连接进行使用,释放。数据库连接池是一种技术,主要用于管理数据库连接,以提高数据库操作的效率和性能。它允许系统预先创建一定数量的数据库连接,并将这些连接存储在一个“池”中。当应用程序需要访问数据库时,它可以直接从池中取出一个现有的连接,使用完毕后再放回池中,而不是每次需要时都创建和销毁连接

主要优势

资源重用:重复使用现有的数据库连接,减少创建和销毁连接的开销

提高性能:减少数据库连接的开销可以显著提高应用程序的性能

控制并发:连接池可以限制系统中数据库连接的最大数量,避免过多的并发连接导致数据库崩溃

简化连接管理:开发者只需关心如何从连接池中获取和返回连接,无需手动管理每个数据库连接的生命周期



2.数据库访问流程

连接数据库

  • 首先,需要通过数据库驱动或API建立与数据库的连接。在使用连接池的情况下,这通常意味着从连接池中获取一个可用的连接

执行查询或更新

  • 通过连接执行SQL命令。这些命令可以是数据查询(SELECT)、数据更新(INSERT、UPDATE、DELETE)或其他数据库操作(如事务控制)
  • SQL命令可以直接编写,或者通过使用预编译的查询来提高性能和安全性(例如,使用预编译的语句可以防止SQL注入攻击)

处理结果

  • 如果执行的是查询操作,需要处理返回的数据。这通常涉及从数据库获取结果集,并在应用程序中对其进行迭代,以读取单行或多行数据
  • 对于更新操作,可能需要检查操作的影响(例如,影响的行数)

关闭连接

  • 在数据访问完成后,及时关闭数据库连接是很重要的。这通常意味着将连接返回到连接池中,供后续使用
  • 确保释放资源,如结果集和数据库连接对象,以避免资源泄露和其他潜在问题

异常处理

  • 在整个数据库操作过程中,应妥善处理可能出现的异常或错误,例如连接失败、SQL错误等
  • 通常需要在代码中添加错误处理逻辑,以确保即使在出现错误的情况下,程序也能正常运行,同时保证所有资源都被正确清理

事务管理

  • 对于需要多个步骤共同完成的数据库操作,常常需要用到事务管理。事务确保这些步骤要么全部完成,要么全部不做,这是通过事务的提交(commit)和回滚(rollback)操作来控制的


3.为什么创建数据库连接池

从一般流程中可以看出,若系统需要频繁访问数据库,则需要频繁创建和断开数据库连接,而创建数据库连接是一个很耗时的操作,也容易对数据库造成安全隐患

在程序初始化的时候,集中创建多个数据库连接,并把他们集中管理,供程序使用,可以保证较快的数据库读写速度,更加安全可靠。可以实现资源重用、提高性能、控制并发




二、章节内容

1.整体概述

池可以看做资源的容器,所以多种实现方法,比如数组、链表、队列等。这里,使用单例模式和链表创建数据库连接池,实现对数据库连接资源的复用

项目中的数据库模块分为两部分

其一,是数据库连接池的定义

其二,是利用连接池完成登录和注册的校验功能

具体的,工作线程从数据库连接池取得一个连接,访问数据库中的数据,访问完毕后将连接交还连接池



2.本文内容

本篇将介绍数据库连接池的定义,具体的涉及到单例模式创建、连接池代码实现、RAII机制释放数据库连接

单例模式创建,结合代码描述连接池的单例实现

连接池代码实现,结合代码对连接池的外部访问接口进行详解

RAII机制释放数据库连接,描述连接释放的封装逻辑




三、代码实现

1.数据库连接池的定义

使用局部静态变量的懒汉式单例模式,创建数据库连接池,C++11之后局部静态变量是线程安全的

CGImysql/sql_connection_pool.h

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
#pragma once
#include <stdio.h>
#include <list>
#include <mysql/mysql.h>
#include <string.h>
#include <string>
#include "../lock/locker.h"
#include "../log/log.h"


class connection_pool
{
public:
// 获取唯一的数据库连接池的静态接口
static connection_pool* GetInstance();
// 从池中获取空闲连接
MYSQL *GetConnection();
// 释放连接,将操作完毕的连接归还至连接队列中
bool ReleaseConnection(MYSQL *conn);
// 获取池中空闲连接数
int GetFreeConn();
// 销毁所有连接
void DestroyPool();
// 初始化连接池相关属性
void init(std::string url, std::string User, std::string PassWord,
std::string DataBaseName, int Port, int MaxConn, int close_log);

private:
// 构造函数私有化
connection_pool();
~connection_pool();
// 复制构造函数删除
connection_pool(const connection_pool &) = delete;
// 复制运算符删除
connection_pool& operator=(const connection_pool &) = delete;

// 连接池中最大连接数
int m_MaxConn;
// 当前已使用的连接数
int m_CurConn;
// 当前空闲的连接数
int m_FreeConn;
// 互斥锁,访问共享资源,连接队列,保证线程安全
locker lock;
// 信号量
sem reserve;
// 连接队列,使用list STL实现
list<MYSQL *> connList;

public:
// 数据库服务器ip
string m_url;
// 数据库端口号 默认为3306
string m_Port;
// 登陆数据库用户名
string m_User;
// 登陆数据库密码
string m_PassWord;
// 使用数据库名
string m_DatabaseName;
// 日志开关
int m_close_log;
};


2.连接池代码实现

连接池的功能主要有:初始化,获取连接、释放连接,销毁连接池

(1)初始化

CGImysql/sql_connection_pool.cpp

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
#include <stdio.h>
#include <string>
#include <string.h>
#include <stdlib.h>
#include <pthread.h>
#include <iostream>
#include "sql_connection_pool.h"

using namespace std;


/*
* func: 数据池私有构造函数
*/
connection_pool::connection_pool()
{
m_CurConn = 0;
m_FreeConn = 0;
}


/*
* func: 获取唯一连接池实例的静态接口
* note: 使用局部静态变量实现懒汉式单例模式
* C+11 局部静态变量是线程安全的
*/
connection_pool *connection_pool::GetInstance()
{
// 局部静态变量实现单例模式
static connection_pool connPool;
return &connPool;
}


/*
* func: 初始化连接池相关属性
* parameter:
* url:数据库服务器ip(若数据库服务器与webServer服务器位于一台服务器上,即主机ip)
* User:数据库服务器用户名
* PassWord:数据库服务器密码
* DBName:数据库服务器中数据库名称
* Port:数据库服务器端口号,默认为3306
* MaxConn:池中最大连接数
* close_log:是否关闭日志标志
*/
void connection_pool::init(string url, string User, string PassWord, string DBName, int Port, int MaxConn, int close_log)
{
m_url = url;
m_Port = Port;
m_User = User;
m_PassWord = PassWord;
m_DatabaseName = DBName;
m_close_log = close_log;

// 初始创建一定数量的数据库连接,放入连接队列中
for (int i = 0; i < MaxConn; i++)
{
MYSQL *con = NULL;
// 初始化MYSQL对象
con = mysql_init(con);

if (con == NULL)
{
LOG_ERROR("MySQL Error");
exit(1);
}

// 连接MYSQL服务器
con = mysql_real_connect(con, url.c_str(), User.c_str(), PassWord.c_str(), DBName.c_str(), Port, NULL, 0);

if (con == NULL)
{
LOG_ERROR("MySQL Error");
exit(1);
}

// 数据库连接对象,装载至队列中
connList.push_back(con);
// 空闲连接数量+1
++m_FreeConn;
}

// 初始化信号量,信号量数量为空闲连接数量
reserve = sem(m_FreeConn);
m_MaxConn = m_FreeConn;
}


/*
* func: 获取池中空闲连接数
*/
int connection_pool::GetFreeConn()
{
return this->m_FreeConn;
}


/*
* func: 连接池的析构函数,销毁所有连接
*/
connection_pool::~connection_pool()
{
DestroyPool();
}

(2)获取、释放连接

当线程数量大于数据库连接数量时,使用信号量进行同步,每次取出连接,信号量原子减1,释放连接原子加1,若连接池内没有连接了,则阻塞等待

另外,由于多线程操作连接池,会造成竞争,这里使用互斥锁完成同步,具体的同步机制均使用lock.h中封装好的类

CGImysql/sql_connection_pool.cpp

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
/*
* func: 获取数据库连接
* note: 当有请求时,从数据库连接池中返回一个可用连接,更新使用和空闲连接数
* 若此时连接队列中无可用请求,使用信号量对请求线程进行阻塞
*/
MYSQL *connection_pool::GetConnection()
{
MYSQL *con = NULL;

if (0 == connList.size())
return NULL;

// 等待信号量,若队列中空闲信号量为0,则阻塞线程
reserve.wait();

lock.lock();

// 取出队列首部的连接
con = connList.front();
connList.pop_front();

--m_FreeConn;
++m_CurConn;

lock.unlock();
return con;
}



/*
* func: 释放连接,将操作完毕的连接归还至连接队列中
*/
bool connection_pool::ReleaseConnection(MYSQL *con)
{
if (NULL == con)
return false;

lock.lock();

connList.push_back(con);
++m_FreeConn;
--m_CurConn;

lock.unlock();

// 通知阻塞在连接队列的请求数据库连接的线程
reserve.post();
return true;
}

(3)销毁连接池

通过迭代器遍历连接池链表,关闭对应数据库连接,清空链表并重置空闲连接和现有连接数量

CGImysql/sql_connection_pool.cpp

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
/*
* func: 销毁所有连接
*/
void connection_pool::DestroyPool()
{

lock.lock();
if (connList.size() > 0)
{
list<MYSQL *>::iterator it;
for (it = connList.begin(); it != connList.end(); ++it)
{
MYSQL *con = *it;
// 关闭连接
mysql_close(con);
}
m_CurConn = 0;
m_FreeConn = 0;
connList.clear();
}
lock.unlock();
}


3.RAII机制释放数据库连接

在 C++ 中,RAIIResource Acquisition Is Initialization)是一种常用的编程模式,主要用于资源管理。RAII 通过将资源(如动态内存、文件句柄、网络连接等)的生命周期绑定到对象的生命周期,以确保在对象被销毁时资源能够自动释放

RAII 的核心思想是利用局部对象的构造和析构机制来管理资源当一个对象被创建时,它的构造函数负责获取必要的资源,并在对象的析构函数中释放这些资源。这样,无论函数通过何种路径退出(正常退出、返回前的退出、异常抛出),都能保证资源的正确释放

将数据库连接的获取与释放(归还)通过RAII机制封装,避免手动释放

(1)定义

这里需要注意的是,在获取连接时,通过有参构造对传入的参数进行修改。其中数据库连接本身是指针类型,所以参数需要通过双指针才能对其进行修改

通过RAII机制管理连接,当数据库连接完成任务之后,利用RAII机制的析构机制,将数据库连接归还至队列

CGImysql/sql_connection_pool.h

1
2
3
4
5
6
7
8
9
10
11
// RAII机制,归还数据库连接
class connectionRAII
{
public:
connectionRAII(MYSQL **con,connection_pool *connectionPool);
~connectionRAII();
private:
MYSQL *conRAII;
connection_pool *poolRAII;
};


(2)实现

CGImysql/sql_connection_pool.cpp

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
/*
* func: 通过RAII机制,控制连接,
* 当数据库连接操作完成后,将连接归还至连接队列
*/
connectionRAII::connectionRAII(MYSQL **SQL, connection_pool *connPool)
{
*SQL = connPool->GetConnection();
conRAII = *SQL;
poolRAII = connPool;
}

connectionRAII::~connectionRAII()
{
poolRAII->ReleaseConnection(conRAII);
}