一:介绍
C/C++精品项目之图床共享云存储(1)
我们项目的第一个文章讲解了很多的基础组件,包括线程池。我们都知道线程池是为了资源的复用,提高效率。而我们的MySql连接池也是一样的,是为了维持管理固定数量的连接,复用连接资源。
维持:我们维持与数据库的TCP长连接。
为什么:为了资源复用,更快的系统相应。
为什么不创建多条链接:我们会并发执行SQL语句,带来的副作用就是需要考虑事务。我们在同一条连接上是串行执行SQL语句的。
二:同步连接和异步连接
通俗的来讲,同步就是阻塞等待,异步就是不需要等待。那么对于连接池来说的话,我们同步和异步都使用在哪里呢?
同步等待连接的返回:会阻塞当前线程进行等待。
异步获取连接的返回:我们另起一个线程进行等待,如异步事件等待:reactor,proactor。
在同步连接池中我们将会创建几个与数据库的连接,我们会同步创建,并且等他们全部返回,我们同步连接池中创建的数量是表示我们当前最多允许几个线程并发使用连接进行执行。(这些都会在后面的代码讲解中讲清楚)我们执行这些代码是在服务器初始化的时候,进行初始化资源。
在异步连接池中我们将创建一个线程池,在这个线程池中,每个线程都会被分配一个与数据库的连接,而在主线程中,我们接收客户端发送来的请求,并把他们发送来的请求全部放到一个队列中去,然后线程池从这个队列中进行获取数据。我们异步连接池中,表示当前最多允许几个连接同时进行执行SQL语句。我们这个代码是在启动服务之后。
在这个异步中,如果我们想获取他们SQL返回来的结果的话,那么我们需要使用future这个来获取他们的结果。
三:代码部分
1:与数据库进行连接的类 CDBConn
我们下面就是他的全部代码,其中包括一些初始化,以及连接上之后可以做出的命令:创建删除查找等。首先我们看一下成员变量,其中包括返回的行数,对应的连接池,以及连接的实例。这个连接池看不懂先不要管,在后面就懂了。
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 ExecutePassQuery(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 mysql_; }int GetRowNum() { return row_num; }private:int row_num = 0;CDBPool *db_pool_; // to get MySQL server informationMYSQL *mysql_; // 对应一个连接char escape_string_[MAX_ESCAPE_STRING_LEN + 1];
};
我们先看一下inti初始化函数,因为这个类是与数据库进行连接的类,所以我们在这里会进行连接的具体操作: 创建一个实例,设置他的自动重连,然后通过与他进行连接,并且把这个连接上来的实例进行保存。那么我们每次调用init就表示我们创建了一个与数据库连接的实例。
int CDBConn::Init() { //总的来说就是进行初始化一个实例,通过这个实例进行与数据库的连接。mysql_ = mysql_init(NULL); // mysql_标准的mysql c client对应的api 也就是进行与数据库的连接 进行初始化if (!mysql_) {LogError("mysql_init failed"); return 1;}//设置一些连接的选项。int reconnect = 1;mysql_options(mysql_, MYSQL_OPT_RECONNECT,&reconnect); // 配合mysql_ping实现自动重连mysql_options(mysql_, MYSQL_SET_CHARSET_NAME,"utf8mb4"); // utf8mb4和utf8区别// ip 端口 用户名 密码 数据库名 进行连接if (!mysql_real_connect(mysql_, db_pool_->GetDBServerIP(),db_pool_->GetUsername(), db_pool_->GetPasswrod(),db_pool_->GetDBName(), db_pool_->GetDBServerPort(),NULL, 0)) {LogError("mysql_real_connect failed: {}", mysql_error(mysql_));return 2;}return 0;
}
我们举一个与数据库操作的例子:比如说这个查询操作。我们首先先重连一下,然后直接执行我们的sql语句,由于是查询当然会返回一些结果,于是我们需要res来存储我们得到的结果。然后通过CResultSet来存储我们得到的结果。对于其他的操作都一样而且还比这个少。
//我们通过执行查询的语句,并且接收返回的结果,并用CResultSet存储返回的结果。
CResultSet *CDBConn::ExecuteQuery(const char *sql_query) {mysql_ping(mysql_);row_num = 0;if (mysql_real_query(mysql_, sql_query, strlen(sql_query))) {LogError("mysql_real_query failed: {}, sql:{}", mysql_error(mysql_), sql_query);return NULL;}// 返回结果MYSQL_RES *res = mysql_store_result(mysql_); // 返回结果 https://www.mysqlzh.com/api/66.htmlif (!res) // 如果查询未返回结果集和读取结果集失败都会返回NULL{LogError("mysql_store_result failed: {}", mysql_error(mysql_));return NULL;}row_num = mysql_num_rows(res);// LogInfo("row_num: {}", row_num;CResultSet *result_set = new CResultSet(res); // 存储到CResultSetreturn result_set;
}
2:连接池CDBPool
我们这个连接池中是管理CDBconn的,下面就是我们所看到的全部函数。首先看一下成员变量:包括数据库的ip端口用户名等,除了这些就是我们的连接数量和最大连接数量。以及两个list列表,空闲和繁忙。
//我们同步的连接池是在初始化的时候,我们服务器与数据库进行多个连接,然后分配到不同的线程中去,而主线程只是处理客户端发送来的消息,将他们分发到不同的线程中。
//我们创建一个连接池,这个连接池中包含好几个线程,而且每个线程配备一个与数据库的连接。
//我们的线程池被阻塞:没有任务会阻塞等待。而连接池被阻塞:没有任务和当前连接正在被占用。
//我们一个连接池只能是对应一个数据库,比如我们有data1,data2,data3,这三个数据库,那么我们创建一个连接池,只能连接其中的一个。
//想要全部连接,那么就需要创建三个连接池。
class CDBPool { // 只是负责管理连接CDBConn,真正干活的是CDBConnpublic: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 = 0); // 获取连接资源void RelDBConn(CDBConn *pConn); // 归还连接资源const char *GetPoolName() { return pool_name_.c_str(); }const char *GetDBServerIP() { return db_server_ip_.c_str(); }uint16_t GetDBServerPort() { return db_server_port_; }const char *GetUsername() { return username_.c_str(); }const char *GetPasswrod() { return password_.c_str(); }const char *GetDBName() { return db_name_.c_str(); }private:string pool_name_; // 连接池名称string db_server_ip_; // 数据库ipuint16_t db_server_port_; // 数据库端口string username_; // 用户名string password_; // 用户密码string db_name_; // db名称int db_cur_conn_cnt_; // 当前启用的连接数量int db_max_conn_cnt_; // 最大连接数量list<CDBConn *> free_list_; // 空闲的连接list<CDBConn *> used_list_; // 记录已经被请求的连接std::mutex mutex_;std::condition_variable cond_var_; //条件变量bool abort_request_ = false; // 终止请求
};
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) { //初始化连接池,将数据库的连接信息保存下来,并且指定多少个线程,或者说是多少个连接并发执行。pool_name_ = pool_name;db_server_ip_ = db_server_ip;db_server_port_ = db_server_port;username_ = username;password_ = password;db_name_ = db_name;db_max_conn_cnt_ = max_conn_cnt; //db_cur_conn_cnt_ = MIN_DB_CONN_CNT; // 最小连接数量
}
首先就是init初始化函数,我们看到根据最小的数量我们创建了好几个连接。
int CDBPool::Init() { // 创建固定最小的连接数量for (int i = 0; i < db_cur_conn_cnt_; i++) {CDBConn *db_conn = new CDBConn(this);int ret = db_conn->Init(); //创建一个与数据库的连接。if (ret) {delete db_conn;return ret;}free_list_.push_back(db_conn); //刚刚创建好的,肯定是空闲连接的啊}return 0;
}
这个函数中看似代码很多其实比较简单。首先大致讲一下,我们这个函数是我们要获取空闲连接,因为我们有空闲列表,我们与数据库进行连接肯定就是从空闲连接上进行发送数据。如果有空闲连接我们直接进行拿取,如果没有那么需要我们进行创建,如果没有满足最大数量,那么我们就创建,然后再拿取。如果已经满足最大数量了,那么就需要我们进行等待,这里就分为了无限等待和等待时间。
//梳理一下逻辑:首先就是看空闲队列中有没有,如果有,则直接拿取一个使用。如果没有,那就看看是否到最大连接了,如果没到最大连接//那就创建一个,放到空闲队列中,继续拿取。当到了最大连接了,那我们看看是无限等待还是等待一会儿,当无限等待,那就wait,如果等待时间,那就wait_for
CDBConn *CDBPool::GetDBConn(const int timeout_ms) { //拿取连接std::unique_lock<std::mutex> lock(mutex_);if (abort_request_) {LogWarn("have aboort"); return NULL;}if (free_list_.empty()) // 2 当没有连接可以用时{// 第一步先检测 当前连接数量是否达到最大的连接数量if (db_cur_conn_cnt_ >= db_max_conn_cnt_) // 等待的逻辑{// 如果已经到达了,看看是否需要超时等待if (timeout_ms <= 0) // 死等,直到有连接可以用 或者 连接池要退出{cond_var_.wait(lock, [this] {// 当前连接数量小于最大连接数量 或者请求释放连接池时退出return (!free_list_.empty()) | abort_request_;});} else {// return如果返回 false,继续wait(或者超时),// 如果返回true退出wait 1.m_free_list不为空 2.超时退出// 3. m_abort_request被置为true,要释放整个连接池cond_var_.wait_for(lock, std::chrono::milliseconds(timeout_ms),[this] { return (!free_list_.empty()) | abort_request_; });// 带超时功能时还要判断是否为空if (free_list_.empty()) // 如果连接池还是没有空闲则退出{return NULL;}}if (abort_request_) {LogWarn("have abort"); return NULL;}} else // 还没有到最大连接则创建连接{CDBConn *db_conn = new CDBConn(this); //新建连接int ret = db_conn->Init();if (ret) {LogError("Init DBConnecton failed"); delete db_conn;return NULL;} else {free_list_.push_back(db_conn); //创建成功则放入到空闲队列中,继续下面拿取的操作。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 = free_list_.front(); // 获取连接free_list_.pop_front(); // STL 吐出连接,从空闲队列删除return pConn;
}
这个函数就是我们进行将不用的连接进行返回,其中为了避免重复放回,需要进行一下判断。
//将不使用的连接,归还给连接池:空闲队列中。首先为了避免重复归还,我们将这个连接从空闲队列中找一下,如果找到则说明已经归还。
//如果不存在那么我们就插入到空闲队列中,并通知取队列:GetDBConn。
void CDBPool::RelDBConn(CDBConn *pConn) { std::lock_guard<std::mutex> lock(mutex_);list<CDBConn *>::iterator it = free_list_.begin();for (; it != free_list_.end(); it++) // 避免重复归还{if (*it == pConn) {break;}}if (it == free_list_.end()) {// used_list_.remove(pConn);free_list_.push_back(pConn);cond_var_.notify_one(); // 通知取队列} else {LogWarn("RelDBConn failed"); // 不再次回收连接}
}
3:连接池的管理者 CDBManager
我们上面已经说过,我们一个连接池只能连接一个数据库,因此如果我们需要连接多个数据库的话,那么就需要我们创建多个连接池,然后我们需要同一管理这些连接池,于是出现了CDBManager。
// manage db pool (master for write and slave for read)
class CDBManager {public:virtual ~CDBManager();static void SetConfPath(const char *conf_path);static CDBManager *getInstance();int Init();CDBConn *GetDBConn(const char *dbpool_name);void RelDBConn(CDBConn *pConn);private:CDBManager();private:static CDBManager *s_db_manager;map<string, CDBPool *> dbpool_map_;static std::string conf_path_;
};
在这个函数中我们会传进去一个参数配置文件,在这个文件中我们将存储数据库的ip端口等,以及全部需要连接的数据库等,我们再这个配置文件中将全部加载出来,然后直接创建多个连接池。然后将这些创建好的连接池,放入到一个map列表中。
int CDBManager::Init() {CConfigFileReader config_file; //读取配置文件if(config_file.ParseConf(conf_path_.c_str()) != 0) {std::cout << conf_path_ << " no exist\n";return -1;}char *db_instances = config_file.GetConfigName("DBInstances");if (!db_instances) {LogError("not configure DBInstances"); return 1;}char host[64];char port[64];char dbname[64];char username[64];char password[64];char maxconncnt[64];CStrExplode instances_name(db_instances, ',');for (uint32_t i = 0; i < instances_name.GetItemCnt(); i++) {char *pool_name = instances_name.GetItem(i);snprintf(host, 64, "%s_host", pool_name);snprintf(port, 64, "%s_port", pool_name);snprintf(dbname, 64, "%s_dbname", pool_name);snprintf(username, 64, "%s_username", pool_name);snprintf(password, 64, "%s_password", pool_name);snprintf(maxconncnt, 64, "%s_maxconncnt", pool_name);char *db_host = config_file.GetConfigName(host);char *str_db_port = config_file.GetConfigName(port);char *db_dbname = config_file.GetConfigName(dbname);char *db_username = config_file.GetConfigName(username);char *db_password = config_file.GetConfigName(password);char *str_maxconncnt = config_file.GetConfigName(maxconncnt);LogInfo("db_host:{}, db_port:{}, db_dbname:{}, db_username:{}, db_password:{}", db_host, str_db_port, db_dbname, db_username, db_password);if (!db_host || !str_db_port || !db_dbname || !db_username ||!db_password || !str_maxconncnt) {LogError("not configure db instance: {}", pool_name);return 2;}int db_port = atoi(str_db_port);int db_maxconncnt = atoi(str_maxconncnt);CDBPool *pDBPool = new CDBPool(pool_name, db_host, db_port, db_username,db_password, db_dbname, db_maxconncnt);if (pDBPool->Init()) {LogError("init db instance failed: {}", pool_name);return 3;}dbpool_map_.insert(make_pair(pool_name, pDBPool));}return 0;
}
这个就是在所有的连接池中封装的一层,直接从某一个连接池中拿取连接,封装这一层之后非常方便我们拿取连接。下面的归还连接也是一样的。
CDBConn *CDBManager::GetDBConn(const char *dbpool_name) {map<string, CDBPool *>::iterator it = dbpool_map_.find(dbpool_name); // 主从if (it == dbpool_map_.end()) {return NULL;} else {return it->second->GetDBConn();}
}void CDBManager::RelDBConn(CDBConn *pConn) {if (!pConn) {return;}map<string, CDBPool *>::iterator it = dbpool_map_.find(pConn->GetPoolName());if (it != dbpool_map_.end()) {it->second->RelDBConn(pConn);}
}
4:自动归还
这个类就是我们进行自动的归还,因为我们这个连接中含有全部的信息,包括这个连接所在的连接池。我们通过manger中的reldbcon,在里面通过调用conn的信息就可以进行归还操作。
// 目的是在函数退出后自动将连接归还连接池
class AutoRelDBCon {public:AutoRelDBCon(CDBManager *manger, CDBConn *conn): manger_(manger), conn_(conn) {}~AutoRelDBCon() {if (manger_) {manger_->RelDBConn(conn_);}} //在析构函数规划private:CDBManager *manger_ = NULL;CDBConn *conn_ = NULL;
};
四:整体图
回顾一下,我们通过连接池先init后,创建多个连接,将他们全部放入到空闲队列中,然后通过拿取空闲连接,放入到线程中(这里未体现)只是简单的拿出来了,还有个就是归还函数。后面就是Manger管理全部的连接池,通过解析配置文件加载所有的连接池,并且在这里封装了全部的连接池的拿取以及放回函数,非常的方便。
感谢大家的观看!https://github.com/0voice