【C++】基于C++11的数据库连接池

提示:写完文章后,目录可以自动生成,如何生成可参考右边的帮助文档

文章目录

  • 前言
  • 一、连接池设计
    • [1.1 功能设计](#1.1 功能设计)
    • [1.2 封装设计](#1.2 封装设计)
  • 二、连接数据库步骤
  • 三、封装编码
    • [3.1 VS配置](#3.1 VS配置)
      • [3.1.1 MySQL环境](#3.1.1 MySQL环境)
      • [3.1.2 jsoncpp环境](#3.1.2 jsoncpp环境)
    • [3.2 连接类代码设计](#3.2 连接类代码设计)
      • [3.2.1 连接类MysqlConn.h](#3.2.1 连接类MysqlConn.h)
      • [3.2.1 连接类MysqlConn.cpp](#3.2.1 连接类MysqlConn.cpp)
    • [3.3 连接池代码设计](#3.3 连接池代码设计)
      • [3.3.1 连接池connectionPool.h](#3.3.1 连接池connectionPool.h)
      • [3.3.2 连接池connectionPool.cpp](#3.3.2 连接池connectionPool.cpp)
    • [3.4 测试函数](#3.4 测试函数)
  • [四、 总结](#四、 总结)
  • 问题总结
    • [MySQL "error: 'fd': 未知重写说明符"](#MySQL “error: ‘fd’: 未知重写说明符”)
    • [LNK2019 无法解析的外部符号 _mysql_set_character_set@8,](#LNK2019 无法解析的外部符号 _mysql_set_character_set@8,)
    • [由于找不到libmysql.dIl, 无法继续执行代码。重新安装程序可能会解决此问题](#由于找不到libmysql.dIl, 无法继续执行代码。重新安装程序可能会解决此问题)

前言

在进行数据库操作的时候为了提高数据库(关系型数据库)的访问瓶颈,除了在服务器端增加缓存服务器(例如redis)缓存常用的数据之外,还可以增加连接池,来提高数据库服务器的访问效率。

一般来说,对于数据库操作都是在访问数据库的时候创建连接,访问完毕断开连接 。但是如果在高并发情况下,有些需要频繁处理的操作就会消耗很多的资源和时间,比如:

  • 建立通信连接的TCP三次握手
  • 数据库服务器的连接认证
  • 数据库服务器关闭连接时的资源回收
  • 断开通信连接的TCP四次挥手

如果使用数据库连接池会减少这一部分的性能损耗

编写代码需要的头文件:

#include<mysql.h>

#include<json/json.h>

#include<jsoncpp.h>

API对应的MySQL动态库

Windows:libmysql.dll

Linux:libmysqlclient.os


一、连接池设计

1.1 功能设计

要设计一个数据库连接池,我们需要实现以下几个功能点:

  1. 连接池只需要一个实例,所以连接池类应该是一个单例模式的类 (此为设计模式)
  2. 所有的数据库连接应该维护到一个安全的队列中
    • 使用队列的目的是方便连接的添加和删除
    • 安全指的是线程安全,也就是说需要使用互斥锁来保护队列数据的读写。
  3. 在需要的时候可以从连接池中得到一个或多个可用的数据库连接
    • 如果有可用连接,直接取出
    • 如果没有可用连接,阻塞等待一定时长然后再重试
    • 如果队列中没有多余的可用连接,需要动态的创建新连接
    • 如果队列中空闲的连接太多,需要动态的销毁一部分
  4. 数据库操作完毕,需要将连接归还到连接池中

1.2 封装设计

  • 数据库连接的存储 :可用使用STL中的队列queue
  • 连接池连接的动态创建:这部分工作需要交给一个单独的线程来处理
  • 连接池连接的动态销毁:这部分工作需要交给一个单独的线程来处理
  • 数据库连接的添加和归还 :这是一个典型的生产者和消费者模型
    • 消费者:需要访问数据库的线程,数据库连接被取出(消费)
    • 生产者:专门负责创建数据库连接的线程
    • 处理生产者和消费者模型需要使用条件变量阻塞线程
  • 连接池的默认连接数量:连接池中提供的可用连接的最小数量
    • 如果不够就动态创建
    • 如果太多就动态销毁
  • 连接池的最大连接数量:能够创建的最大有效数据库连接上限
  • 最大空闲时间:创建出的数据库连接在指定时间长度内一直未被使用,此时就需要销毁该连接。
  • 连接超时:消费者线程无法获取到可用连接是,阻塞等待的时间长度

综上所述,数据库连接池对应的单例模式的类的设计如下:

cpp 复制代码
using namespace std;
/*
* 数据库连接池: 单例模式
* MySqlConn 是一个连接MySQL数据库的类
*/
class ConnectionPool {
public:
    // 得到单例对象
    static ConnectionPool* getConnectPool();
    // 从连接池中取出一个连接
    shared_ptr<MySqlConn> getConnection();
    // 删除拷贝构造和拷贝赋值运算符重载函数
    ConnectionPool(const ConnectionPool& obj) = delete;
    ConnectionPool& operator=(const ConnectionPool& obj) = delete;

private:
    // 构造函数私有化
    ConnectionPool();
    bool parseJsonFile();
    void produceConnection();
    void recycleConnection();
    void addConnection();

    string m_ip;             // 数据库服务器ip地址
    string m_user;           // 数据库服务器用户名
    string m_dbName;         // 数据库服务器的数据库名
    string m_passwd;         // 数据库服务器密码
    unsigned short m_port;   // 数据库服务器绑定的端口
    int m_minSize;           // 连接池维护的最小连接数
    int m_maxSize;           // 连接池维护的最大连接数
    int m_maxIdleTime;       // 连接池中连接的最大空闲时长
    int m_timeout;           // 连接池获取连接的超时时长

    queue<MySqlConn*> m_connectionQ; // 连接队列
    mutex m_mutexQ;  // 互斥锁
    condition_variable m_cond; // 条件变量
};

二、连接数据库步骤

在程序中连接MySql服务器,主要分为已经几个步骤:

  1. 初始化连接环境
  2. 连接mysql的服务器,需要提供如下连接数据:
  • 服务器的IP地址 服务器监听的端口(默认端口是3306)
  • 连接服务器使用的用户名(默认是 root),和这个用户对应的密码
  • 要操作的数据库的名字
  1. 连接已经建立, 后续操作就是对数据库数据的添删查改

  2. 如果要进行数据 添加/ 删除/ 更新,需要进行事务的处理

    • 需要对执行的结果进行判断
      • 成功:提交事务
      • 失败:数据回滚
  3. 数据库的读操作 -> 查询 -> 得到结果集

  4. 遍历结果集 -> 得到了要查询的数据

  5. 释放资源

三、封装编码

3.1 VS配置

3.1.1 MySQL环境

打开项目的属性窗口,在属性页配置MySQL头文件目录和MySQL的库目录

将下载的mysql路径下的include和lib文件目录分别写入包含目录和库目录中

在VS项目的属性页窗口指定要加载的动态库对应的导入库(xxx.lib)

我们在调用MySQL API的使用需要加载的动态库为libmysql.dll,它对应的导入库为libmysql.lib,在该窗口的附加依赖项位置指定的就是这个导入库的名字。

3.1.2 jsoncpp环境

参考博客:jsoncpp的编译和使用

3.2 连接类代码设计

3.2.1 连接类MysqlConn.h

cpp 复制代码
#pragma once
#include <string>
#include <WinSock2.h>//必须要有,解决"fd":未知重写说明符
#include <algorithm>
#include<chrono>
#include<mysql.h>
using namespace std;
using namespace chrono;
class MysqlConn
{
public:
	// 初始化数据库连接
	MysqlConn();
	// 释放数据库连接
	~MysqlConn();
	// 连接数据库
	bool connect(string userName,string passwd,string dbName,string ip,unsigned short port=3306);
	// 更新数据库
	bool update(string sql);
	// 查询数据库
	bool query(string sql);
	// 遍历查询得到的结果集
	bool next();
	// 得到结果集中的字段值
	string value(int index);
	// 事务操作
	bool transaction();
	// 提交事务
	bool commit();
	// 事务回滚
	bool rollback();
	// 刷新起始的空闲时间点
	void refreshAliveTime();
	// 计算连接存活的时长
	long long getAliveTime();

private:
	void freeResource();  // 释放资源
	MYSQL* m_conn = nullptr; // 保存初始化数据库时返回的地址
	MYSQL_RES* m_result = nullptr; // 保存查询结果
	MYSQL_ROW m_row = nullptr; // 保存当前记录中每个字段的值
	steady_clock::time_point m_alivetime; // 存活时间
};

3.2.1 连接类MysqlConn.cpp

cpp 复制代码
#include "MysqlConn.h"

 // 初始化数据库连接
MysqlConn::MysqlConn()
{
	// 初始化数据库
	this->m_conn = mysql_init(nullptr);
	// 设置接口的字符编码,防止在数据库操作时中文乱码
	mysql_set_character_set(this->m_conn, "utf8");
}


// 释放数据库连接
MysqlConn::~MysqlConn()
{
	// 释放内存
	if (this->m_conn != nullptr) {
		mysql_close(this->m_conn);
	}
	this->freeResource();
}


// 连接数据库
bool MysqlConn::connect(string userName, string passwd, string dbName, string ip, unsigned short port)
{
	// c_str()函数返回一个指向正规C字符串的指针常量, 内容与本string串相同。
	// 因为调用的MysqlAPI是C语言规范
	MYSQL* ptr = mysql_real_connect(this->m_conn, ip.c_str(), userName.c_str(), passwd.c_str(), dbName.c_str(), port, nullptr, 0);
	return ptr!=nullptr;
}


// 更新数据库
bool MysqlConn::update(string sql)
{
	// 执行失败返回非0,执行成功返回0
	if (mysql_query(this->m_conn,sql.c_str()))
	{
		return false;
	}

	return true;
}


// 查询数据库
bool MysqlConn::query(string sql)
{
	// 先清空上次查询的数据结果
	this->freeResource();

	if (mysql_query(this->m_conn, sql.c_str()))
	{
		return false;
	}
	// 保存查询结果
	// mysql_store_result()将查询的全部结果读取到客户端,分配1个MYSQL_RES结构,并将结果置于该结构中。
	this->m_result = mysql_store_result(this->m_conn);
	return false;
}


// 遍历查询得到的结果集
bool MysqlConn::next()
{
	if (this->m_result!=nullptr)
	{
		// 会返回MYSQL_ROW类型的地址,是一个二级指针,指向一个指针数组,指针数组中的元素指向值
		this->m_row = mysql_fetch_row(this->m_result);
	}
	return false;
}


// 得到结果集中的字段值
string MysqlConn::value(int index)
{
	// 得到查询结果的列数
	int columnCount = mysql_num_fields(this->m_result);
	// 如果查询的索引越界
	if (index >= columnCount || index < 0)
	{
		return string();
	}
	// 获取想要查询的值
	char* val = m_row[index];
	/*return string(val);*/  // 但是查询的结果中可能存在'\0'这样的字符,此时string()就会出问题
	
	// 所有需要得到当前值的长度
	unsigned long len = mysql_fetch_lengths(this->m_result)[index];
	return string(val, len);
}


// 事务操作
bool MysqlConn::transaction()
{
	// 第二个参数设置为false,为手动提交
	return mysql_autocommit(this->m_conn,false);
}


// 提交事务
bool MysqlConn::commit()
{
	return mysql_commit(this->m_conn);
}


// 事务回滚
bool MysqlConn::rollback()
{
	return mysql_rollback(m_conn);
}


// 刷新起始的空闲时间点
void MysqlConn::refreshAliveTime()
{
	// 得到当前的时间戳
	this->m_alivetime = steady_clock::now();
}


// 计算连接存活的时长
long long MysqlConn::getAliveTime()
{
	nanoseconds res = steady_clock::now() - this->m_alivetime;
	milliseconds millisec = duration_cast<milliseconds>(res); // 类型转换,将低精度往高精度转换
	return millisec.count(); // 计算millisec中有几个毫秒
}


// 释放资源
void MysqlConn::freeResource()
{
	if (this->m_result)
	{
		mysql_free_result(this->m_result);
		this->m_result = nullptr;
	}
}

3.3 连接池代码设计

3.3.1 连接池connectionPool.h

cpp 复制代码
#pragma once
#include<memory>
#include<queue>
#include<mutex>
#include<chrono>
#include"MysqlConn.h"
using namespace std;
using namespace chrono;
/*
* 数据库连接池: 单例模式
* MySqlConn 是一个连接MySQL数据库的类
*/
class connectionPool {
public:
    // 通过静态方法得到单例对象
    static connectionPool* getConnectPool();
    // 从连接池中取出一个连接
    shared_ptr<MysqlConn> getConnection();
    // 析构函数
    ~connectionPool();
    // 删除拷贝构造和拷贝赋值运算符重载函数,防止类的复制,实现单例模式
    connectionPool(const connectionPool& obj) = delete;
    connectionPool& operator=(const connectionPool& obj) = delete;

private:
    // 构造函数私有化,防止外界可以实例化对象,打破单例模式
    connectionPool();
    // 解析json文件的函数
    bool parseJsonFile();
    // 生产数据库连接的函数
    void produceConnection();
    // 销毁数据库连接的函数
    void recycleConnection();
    // 创建新的连接对象
    void addConnection();

    string m_ip;             // 数据库服务器ip地址
    string m_user;           // 数据库服务器用户名
    string m_dbName;         // 数据库服务器的数据库名
    string m_passwd;         // 数据库服务器密码
    unsigned short m_port;   // 数据库服务器绑定的端口
    int m_minSize;           // 连接池维护的最小连接数
    int m_maxSize;           // 连接池维护的最大连接数
    int m_maxIdleTime;       // 连接池中连接的最大空闲时长
    int m_timeout;           // 连接池获取连接的超时时长

    queue<MysqlConn*> m_connectionQ; // 连接队列,连接数据库时可以在此进行连接
    mutex m_mutexQ;  // 互斥锁
    condition_variable m_cond; // 条件变量

};

3.3.2 连接池connectionPool.cpp

cpp 复制代码
#include "connectionPool.h"
#include<fstream>
#include<json/json.h>
#include<thread>
#include<iostream>
using namespace Json;


// 通过静态方法得到单例对象,饿汉模式
connectionPool* connectionPool::getConnectPool()
{
    static connectionPool pool;
    return &pool;
}

// 进行连接
shared_ptr<MysqlConn> connectionPool::getConnection()
{
    unique_lock<mutex> locker(this->m_mutexQ);
    // 判断连接队列是否为空
    while (this->m_connectionQ.empty())
    {
        // wait_for() 当时间片转完还是没有被唤醒,则会返回timeout状态
        // if判断语句为真,则说明连接队列为空
        if (cv_status::timeout == this->m_cond.wait_for(locker, chrono::milliseconds(this->m_timeout)))// 为空就阻塞进程一定时间
        {
            if (this->m_connectionQ.empty()) // 保险起见又做一次判断
            {
                continue;
            }
        }
    }

    // 利用智能指针取出连接对象,智能指针可以直接指定删除器进行连接对象回收
    shared_ptr<MysqlConn> connptr(this->m_connectionQ.front(), [this](MysqlConn* conn) { // 指定匿名删除器
        // lock_guard在加锁后,可以自动在生命周期结束时对互斥锁进行解锁,一般是在函数结束时
        lock_guard<mutex> locker(this->m_mutexQ); 
        // 重新指定被放回队列的时间
        conn->refreshAliveTime();
        // 放回连接队列
        this->m_connectionQ.push(conn);
    });
    // 弹出被取出的连接对象
    this->m_connectionQ.pop();
    // 唤醒生产者线程
    this->m_cond.notify_all();

    return connptr;
}

connectionPool::~connectionPool()
{
    while (!this->m_connectionQ.empty())
    {
        MysqlConn* conn = this->m_connectionQ.front();
        this->m_connectionQ.pop();
        delete conn;
        conn = nullptr;
    }
}


// 解析json文件的函数
bool connectionPool::parseJsonFile()
{
    ifstream ifs("dbconn.config.json");
    Reader rd;
    Value root;
    rd.parse(ifs, root); // 将ifs流对象中的json数据解析到root中

    // 判断转换的是否是json数据,成功转化后,将其存储到成员变量中,并返回true
    if (root.isObject())
    {
        this->m_ip = root["ip"].asString();
        this->m_port = root["port"].asInt();
        this->m_user = root["userName"].asString();
        this->m_passwd = root["password"].asString();
        this->m_dbName = root["dbName"].asString();
        this->m_minSize = root["minsize"].asInt();
        this->m_maxSize = root["maxsize"].asInt();
        this->m_maxIdleTime = root["maxsize"].asInt();
        this->m_timeout = root["timeout"].asInt();
        return true;
    }
    return false;
}


// 当连接队列中连接对象太少时,需要创建一部分连接对象
void connectionPool::produceConnection()
{
    while (true)
    {
        // unique_lock是个类模板
        //mutex是参数的类型,unique_lock包装了一个互斥锁对象,当函数结束时,此unique_lock析构,进行解锁
        unique_lock<mutex> locker(this->m_mutexQ);
        while (this->m_connectionQ.size() >= this->m_minSize) // 当连接池中的连接数量大于最小数量时,就阻塞不需要生产连接 
        {
            // 阻塞进程,不生产连接
            m_cond.wait(locker);
        }
        // 生产连接
        this->addConnection();
        // 唤醒因没有连接对象而阻塞的线程
        this->m_cond.notify_all();
    }
}


// 当连接队列中连接对象太多时,需要销毁一部分连接对象释放资源
void connectionPool::recycleConnection()
{
    while (true)
    {
        // 设置为每隔1检测一次
        this_thread::sleep_for(chrono::seconds(1));
        lock_guard<mutex> locker(this->m_mutexQ);
        // 当连接队列元素个数大于最小连接数时
        while (this->m_connectionQ.size() >= this->m_minSize)
        {
            // 检测连接对象的空闲时长,这个是从未使用开始计算的(即从被连接池中返还的连接队列时)
            MysqlConn* conn = m_connectionQ.front();
            if (conn->getAliveTime() >= this->m_maxIdleTime) // 检测到队头连接对象存活时间大于了最大空闲时间
            {
                this->m_connectionQ.pop();
                delete conn;
            }
            else
            {
                break;
            }
        }
    }
}


// 创建新的连接对象,并添加到连接队列中
void connectionPool::addConnection()
{
    MysqlConn* conn = new MysqlConn;
    conn->connect(m_user, m_passwd, m_dbName, m_ip, m_port);
    conn->refreshAliveTime();
    this->m_connectionQ.push(conn);
}


// 实现构造函数
connectionPool::connectionPool() 
{
    // 加载配置文件
    if (!this->parseJsonFile())
    {
        // 未加载成功
        return;
    }
    int num = 0;
    // 初始化最小连接数的连接对象
    for (int i = 0; i < this->m_minSize; i++)
    {
        this->addConnection();
        cout << ++num << endl;
    }
    // 将生产/销毁连接交给子线程去做
    // 这里将子线程的任务函数设置为类的非静态成员函数
    thread producer(&connectionPool::produceConnection,this);
    thread recycler(&connectionPool::recycleConnection,this);
    // 将主线程和子线程分离,避免阻塞主线程
    producer.detach();
    recycler.detach();
}

3.4 测试函数

cpp 复制代码
#include<iostream>
#include<string>
#include"MysqlConn.h"
#include"connectionPool.h"
using namespace std;

// @file:connectionPool
// @author:IdealSanX_T
// @date:2024/7/3 9:56:19
// @brief:连接线程池测试函数

// 单线程不使用线程池
void op1(int begin, int end)
{
	for (int i = begin; i < end; ++i)
	{
		MysqlConn conn;
		conn.connect("root", "123456", "504", "127.0.0.1");
		char sql[1024] = { 0 };
		sprintf(sql, "insert into test values (%d,25,' man', 'tom')",i);
		conn.update(sql);
	}
}

// 单线程使用线程池
void op2(connectionPool* pool,int begin, int end)
{
	for (int i = begin; i < end; ++i)
	{
		shared_ptr<MysqlConn> conn = pool->getConnection();
		char sql[1024] = { 0 };
		sprintf(sql, "insert into test values (%d,25,' man', 'tom')", i);
		conn->update(sql);
		cout << "插入数据" << endl;
	}
}

// 单线程测试函数
void test01()
{
#if 0
	// 非连接池,单线程,用时:4592767400纳秒,4592毫秒
	steady_clock::time_point begin = steady_clock::now(); 
	op1(0, 5000);
	steady_clock::time_point end = steady_clock:: now();
	auto length = end - begin; 
	cout << "非连接池,单线程,用时:" << length.count() << "纳秒,"
		<< length.count() / 1000000 << "毫秒" << endl;
#else
	// 连接池,单线程,用时:2199883200纳秒,2199毫秒
	connectionPool* pool = connectionPool::getConnectPool();
	steady_clock::time_point begin = steady_clock::now();
	op2(pool, 0, 5000);
	steady_clock::time_point end = steady_clock::now();
	auto length = end - begin;
	cout << "连接池,单线程,用时:" << length.count() << "纳秒,"
		<< length.count() / 1000000 << "毫秒" << endl;
#endif
}

// 多线程测试函数
void test02()
{
#if 0
	// 非连接池,多单线程,用时:2791891000纳秒,2791毫秒
	MysqlConn conn;
	conn.connect("root", "123456", "504", "127.0.0.1");
	steady_clock::time_point begin = steady_clock::now();
	thread t1(op1, 0, 1000);
	thread t2(op1, 1000, 2000); 
	thread t3(op1, 2000, 3000); 
	thread t4(op1, 3000, 4000); 
	thread t5(op1, 4000, 5000); 
	t1.join();
	t2.join(); 
	t3.join(); 
	t4.join(); 
	t5.join();
	steady_clock::time_point end = steady_clock::now(); 
	auto length = end - begin;
	cout << "非连接池,多单线程,用时:" << length.count()<<"纳秒,"
		<< length.count() / 1000000 << "毫秒" <<endl;

#else
	// 连接池,多单线程,用时:1974331000纳秒,1974毫秒
	connectionPool* pool = connectionPool::getConnectPool();
	steady_clock::time_point begin = steady_clock::now(); 
	thread t1(op2, pool, 0, 1000);
	thread t2(op2, pool, 1000, 2000); 
	thread t3(op2, pool, 2000, 3000); 
	thread t4(op2, pool, 3000, 4000); 
	thread t5(op2, pool, 4000, 5000); 
	t1.join();
	t2.join(); 
	t3.join(); 
	t4.join(); 
	t5.join();
	steady_clock::time_point end = steady_clock::now(); 
	auto length = end - begin;
	cout << "连接池,多单线程,用时:" << length.count() <<"纳秒,"
		 << length.count() / 1000000 << "毫秒" << endl;
#endif
}
int main() {
	//test01();
	test02();
	system("pause");
	return 0;
}

四、 总结

使用连接池时,比不使用连接池所用时更短,效果更好。

可以在消费者/生产者线程中进行代码优化。

问题总结

MySQL "error: 'fd': 未知重写说明符"

增加引用头文件

当用C/C++ 连接数据库并且采用ODBC(Open DataBases Connection) 肯定会出现

#include 这个头文件,关键就是这个头文件的问题,

在预编译 #include <mysql.h> 一定要先包含 #include <winsock.h> 这个头文件才不会出现刚才的问题。

winsock.h这个头文件一定要在mysql.h的头文件前面

原因:#include "mysql.h"中调用了mysql_com.h,而mysql_com.h使用了有关网络套接字的fd,所以如果没有网络通信的头文件的话,就会报错。

cpp 复制代码
#include <WinSock2.h>//必须要有,解决"fd":未知重写说明符

借鉴博客

LNK2019 无法解析的外部符号 _mysql_set_character_set@8,

原因如下,系统是win10x64,MySQL 64位的lib也是64位的接口。所以解决方法如下:

.项目->属性->配置管理器

活动解决方案平台,下拉选新建,出现一个新的对号框,在键入选择新平台中选择X64

然后把VS配置的include目录和lib目录等再次配置一下。

重新编译 成功~~~

由于找不到libmysql.dIl, 无法继续执行代码。重新安装程序可能会解决此问题

解决方法:

1.首先找到 libmysql.dll 文件。(在MySQL解压后的文件夹中的lib文件夹目录下)

2.然后直接将"libmysql.dll"复制到项目同级目录下,问题解决。

( 我的项目名为SQLConnectionPool,我就把文件复制到SQLConnectionPool目录下,如图所示)

相关推荐
legend_jz17 分钟前
【Linux】线程控制
linux·服务器·开发语言·c++·笔记·学习·学习方法
Karoku06625 分钟前
【企业级分布式系统】ELK优化
运维·服务器·数据库·elk·elasticsearch
嘿BRE27 分钟前
【C++】几个基本容器的模拟实现(string,vector,list,stack,queue,priority_queue)
c++
ö Constancy1 小时前
c++ 笔记
开发语言·c++
fengbizhe1 小时前
笔试-笔记2
c++·笔记
徐霞客3202 小时前
Qt入门1——认识Qt的几个常用头文件和常用函数
开发语言·c++·笔记·qt
fpcc2 小时前
redis6.0之后的多线程版本的问题
c++·redis
小技与小术2 小时前
数据库表设计范式
数据库·mysql
安迁岚2 小时前
【SQL Server】华中农业大学空间数据库实验报告 实验三 数据操作
运维·服务器·数据库·sql·mysql
安迁岚2 小时前
【SQL Server】华中农业大学空间数据库实验报告 实验九 触发器
数据库·sql·mysql·oracle·实验报告