基于C++11的数据库连接池【C++/数据库/多线程/MySQL】

一、概述

概述:数据库连接池可提前把多个数据库连接建立起来,然后把它放到一个池子里边,就是放到一个容器里边进行维护。这样的话就能够避免数据库连接的频繁的创建和销毁,从而提高程序的效率。线程池其实也是同样的思路,也是为了避免线程的重复的创建和销毁。

这个图模拟的是一个网络通信,在左侧有多个客户端,客户端给服务器发数据,发数据可以理解为就是发送一个请求,比如说请求登录,请求注册,请求下载或者上传某些文件。

我们还需要做身份的验证,或者说做数据的读和写操作,那这个数据在哪里呢? 

  • 数据在一个数据库里边,这个数据库它其实位于服务器端。

在服务器端,其实并不是只有一个应用程序。和服务器端程序一块部署的还可以有其他模块,比如关系型数据库的服务器,非关系型数据库的服务器,或者说部署一些分布式的文件系统。

当客户端向服务器发起了某个请求之后,比如说它要登录,那么此时服务器就会连接数据库。从数据库里面去查询一下这个用户的数据是否存在。如果存在,允许它登录;如果不存在,就不允许它登录。所以在这个网络通信的服务器端可以看成是数据库的客户端。如果是多个客户端同时向服务器发起请求,此时服务器就需要建立多个连接去进行数据库的查询。当查询完之后,它需要断开这个连接。

小细节:这个连接一直保持着可以吗?

  • 如果一直保持着这个数据库连接,但是你又不进行数据库操作,就浪费了资源。所以在服务器端在进行数据库操作的时候,一般的处理思路就是用的时候去连接,用完了之后就把它销毁掉。
  • 如果在网络通信的服务器端,频繁地进行数据库地连接和断开,这很显然是非常的浪费时间以及浪费资源的,所以就可以在网络通信的服务器端去维护一个连接池。当需要连接数据库的时候,就从这个连接池中拿出一个连接,用完之后再把这个连接还给连接池。

有了数据库连接池的好处是什么?

  • 就是避免了这个数据库连接的频繁创建和销毁,使用这种方式在访问量比较高的情况下,就能够节省一部分时间。如果访问量很少,有无连接池其实我们是看不到任何的区别的。
  • 如果在高并发情况下,这些需要频繁处理的操作就会消耗很多的资源和时间。

涉及的技术点:

C++11新特性

  1. 多线程编程
  2. 线程同步(互斥锁的使用)
  3. 处理时间和日期的chrono库
  4. 条件变量
  5. 智能指针(帮我们释放一块内存)
  6. lambda表达式
  7. 使用=delete删除函数(在实现数据库连接池的时候,需要创建一个单例模式的类,某些函数是不允许存在的,比如说拷贝构造函数,还有=赋值操作符重载函数,可以用=delete删除,它们也就不允许使用了。如果不使用=delete删除,访问权限设置为私有也可以)

其他知识点:

  1. MYSQL数据库查询,主要是官方API的封装和使用
  2. 单例模式
  3. STL容器
  4. 生产者和消费者模型
  5. Jsoncpp库的使用(解析配置文件中的数据库相关信息)

 二、Mysql API

MySQL API 使用详解 | 爱编程的大丙 (subingwen.cn)icon-default.png?t=N7T8https://subingwen.cn/mysql/mysql-api/VsCode + CMake构建项目 C/C++连接Mysql数据库 | 数据库增删改查C++封装 | 信息管理系统通用代码 ---- 课程笔记-CSDN博客icon-default.png?t=N7T8https://blog.csdn.net/weixin_41987016/article/details/135668803?spm=1001.2014.3001.5501

  • MysqlConn.h
#pragma once
#include <mysql.h>
#include <string>
#include <chrono>
using namespace std;
using namespace std::chrono;
class MysqlConn {
public:
    // 初始化数据库连接
    MysqlConn();
    // 释放数据库连接
    ~MysqlConn();
    // 连接数据库
    bool connect(string user, string passwd, string dbName, string ip, unsigned short port = 3306);
    // 更新数据库: select,update,delete
    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 freeResult();
    MYSQL* m_conn = nullptr; // 数据库连接
    MYSQL_RES* m_result = nullptr;
    MYSQL_ROW m_row = nullptr;
    steady_clock::time_point m_aliveTime;
};
  •  MysqlConn.cpp
#include "MysqlConn.h"

// 初始化数据库连接
MysqlConn::MysqlConn() {
    m_conn = mysql_init(nullptr);
    mysql_set_character_set(m_conn, "GBK"); // 设置字符集
}

// 释放数据库连接
MysqlConn::~MysqlConn() {
    if (m_conn != nullptr) {
        mysql_close(m_conn);
    }
    freeResult();
}

// 连接数据库
bool MysqlConn::connect(string user, string passwd, string dbName, string ip, unsigned short port) {
    MYSQL* ptr = mysql_real_connect(m_conn, ip.c_str(), user.c_str(), passwd.c_str(), dbName.c_str(), port, nullptr, 0);
    return ptr != nullptr;
}

// 更新数据库:insert,update,delete
bool MysqlConn::update(string sql) {
    if (mysql_query(m_conn, sql.c_str())) {
        return false;
    }
    return true;
}

// 查询数据库
bool MysqlConn::query(string sql) {
    freeResult(); // 释放之前查询的结果集
    if (mysql_query(m_conn, sql.c_str())) {
        return false;
    }
    m_result = mysql_store_result(m_conn); // 获取查询结果
    return true;
}

// 遍历查询得到的结果集
bool MysqlConn::next() {
    if (m_result != nullptr) {
        m_row = mysql_fetch_row(m_result);
        if (m_row != nullptr) {
            return true;
        }
    }
    return false;
}

// 得到结果集中的字段值
string MysqlConn::value(int index) {
    int rowCount = mysql_num_fields(m_result);
    if (index >= rowCount || index < 0) {
        return string();
    }
    char* val = m_row[index];
    unsigned long length = mysql_fetch_lengths(m_result)[index];
    return string(val, length);
}

// 事务操作
bool MysqlConn::transaction() {
    return mysql_autocommit(m_conn, false);
}

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

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

// 刷新起始的空闲时间点
void MysqlConn::refreshAliveTime() {
    // 这个时间戳就是某个数据库连接,它起始存活的时间点
    // 这个时间点通过时间类就可以得到了
    m_aliveTime = steady_clock::now();
}

// 计算连接存活的总时长
long long MysqlConn::getAliveTime() {
    nanoseconds duration = steady_clock::now() - m_aliveTime;
    milliseconds millsec = duration_cast<milliseconds>(duration);
    return millsec.count();
}

void MysqlConn::freeResult() {
    if (m_result != nullptr) {
        mysql_free_result(m_result);
        m_result = nullptr;
    }
}

数据库操作类封装好了之后,看一下如何编写一个数据库连接池。

三、数据库连接池

关于数据库连接池,它主要是用在网络通信的服务器端,在网络通信的服务器端有可能同时接收多个客户端请求,并且客户端请求的数据它并不在服务器上,而是在服务器对应的那个数据库服务器上。

一定要注意:网络通信的服务器端,它有可能部署了多个组件,数据库服务器是这多个组件里边的其中一个。又因为这个请求是多个客户端发送过来的,所以服务器端为了提高读取数据的效率,它需要通过多线程去访问数据库服务器。

在服务器端如果想要连接数据库服务器,需要做四步操作:

  1. 第一步:通过tcp进行三次握手。因为网络通信的服务器端其实就是数据库的客户端。数据库的客户端连接数据库的服务器端,其实是一个tcp通信,tcp通信的第一步就是需要进行三次握手。
  2. 第二步:数据库服务器的连接认证。当连接建立之后,数据库的服务器需要验证客户端的身份,就是验证用户名和密码。验证成功之后,数据库的客户端就可以通过sql语句去服务器端读取或者更新一些数据,
  3. 第三步:数据库服务器关闭连接时的资源回收。当操作完成之后,客户端和服务器需要断开连接,在断开连接的时候需要进行资源的释放。
  4. 第四步:断开通信连接的TCP四次挥手。数据库资源释放完了之后,这个tcp通信还需要进行四次挥手。

把这四步操作做完了之后,数据库的客户端和数据库的服务器端,它们之间的通信也就结束了。如果说这个客户端和服务器端需要频繁地进行这四步操作,很显然是非常的浪费时间的,因此就提供了一个解决方案:使用数据库连接池。

在这个数据库连接池里边,我们需要事先和数据库的服务器建立若干个连接,当需要将进行数据库操作的时候,套接字通信的服务器端通过线程去连接池里取出一个可用的连接。取出这个连接之后,就可以和数据库的服务器直接通信了。通信结束之后,这个连接是不需要进行断开的,网络通信的服务器端对应的这个线程,把这个数据库连接就还给数据库连接池。因此我们就可以实现数据库连接的复用。通过这样的操作,大大的降低了数据库连接的创建和销毁的次数,对应的时间也就被节省出来了。

如果要实现一个数据库连接池,它都需要有哪些组成部分?

这个数据库连接池,它在程序中对应的肯定是一个对象。那么这个数据库连接池的对象有一个还是多个呢?很显然,一个就够了,因为在数据库连接池里边,它可以给我们提供若干个连接,只需要一个数据库连接池对象就可以把多个连接取出来,故这个对象一个就够。

因此,在编写数据库连接池对应的类的时候,这个类应该是一个单例模式的类提供一个单例模式的类,可以避免出创建多个数据库连接池对象。

分析在这个单例模式的类里边,我们需要给数据库连接池提供哪些属性呢?

// 连接服务器所需信息
string m_ip;            // 数据库服务器ip地址
string m_user;          // 数据库服务器用户名
string m_dbName;        // 数据库服务器的数据库名
string m_passwd;        // 数据库服务器密码
unsigned short m_port;  // 数据库服务器绑定的端口

数据库连接池里边,它存储的是数据库连接,如果我们要成功地建立一个数据库连接,那么就需要提供一系列的属性,这个属性包括数据库服务器对应的IP和它的端口Port。另外在连接数据库服务器的时候,需要提供数据库服务器对应的用户名和密码。因为在一个mysql数据库的服务器上,可以创建多个数据库,每个数据库都有对应的自己的名字,故还需要指定dbName。有了这些属性,就可以成功的去建立一个数据库连接,如果要建立多个数据库连接,那么这个连接是需要存储起来的。

// 连接池信息
queue<MysqlConn*> m_connQ;
unsigned int m_maxSize; // 连接数上限值
unsigned int m_minSize; // 连接数下限值
int m_timeout; // 连接超时时长
int m_maxIdleTime; // 最大的空闲时长
mutex m_mutexQ; // 独占互斥锁
condition_variable m_cond; // 条件变量

STL中我们可以选择队列,存储数据库连接对象。在这个队列里边存储连接的时候,是有上限的,故需要给这个队列存储的连接指定一个最大值。

如果服务器端对应的多个线程从数据库连接池里边取连接的时候,这个连接已经用完了,我们可以给这个线程指定一个超时的时长。如果没有连接了,可以等待一定的时长。如果等待了一定的时间长度还没有拿到可用的连接,有两种处理方式:

  1. 一种处理方式就是告诉这个线程已经没有可用的连接了。
  2. 另一种处理方式让这个线程继续等,直到这个数据库连接池里边有新连接了,那么把这个新连接给到对应的线程。

除此之外,还有一种情况就是这个数据库里边的空闲连接太多了,如果空闲的连接太多了,我们就需要销毁一部分,那么我们怎么知道要销毁哪些数据库连接呢?

解决方案:当我们创建出一个数据库之后,它肯定是空闲的,把这个时间点记录下来,给它一个时间戳,然后再提供一个线程专门的去检测这些数据库连接,它们都空闲了多长的时间,就是以现在的这个时间点减去对应的那个起始时间点,如果这个时间长度超过了我们规定的时长,那么就把对应的这个数据库连接给销毁掉。如果没有超过规定的时长,就让这个数据库连接继续存活。另外,在实现这个数据库连接池的时候,还涉及到一些线程相关的操作。因为多个线程它们需要访问这个数据库连接池,因此数据库连接池就是多个线程对应的共享资源。如果涉及到共享资源的访问,就需要使用互斥锁。

其中,共享资源就是数据库连接池队列,要使用互斥锁来保证连接池队列里边的数据不发生混乱

网络通信的服务器端,它提供的这多个线程在访问数据库连接池的时候,把可用的数据库连接从连接池里边拿出去。如果用完了,之后再把这个可用的连接还给数据库连接池。因此我们可以把这两部分(多线程和连接池->网络服务器)看成是一个生产者和消费者模型。对应的消费者就是网络通信的服务器端提供的这多个线程。生产者是我们在实现数据库连接的时候,提供的一个额外的线程,这个线程需要检测连接池里边的连接数量是否足够,如果说这个连接已经不够了,这个线程它就去生成新的连接,如果说连接池里边的连接是足够的,专门用于生产数据库连接的线程就不工作了。如果它不工作了,就需要让线程阻塞。C++11里边,让线程池阻塞需要用到条件变量

超时时长就是让线程阻塞等待的时间长度,当线程等待了这个时间段之后,再让它尝试去连接池里取出对应的连接。如果有就给它,如果没有,可以让它去等待,或者直接告诉它没有了。

在连接池队列里边,可用的有效连接太多了,大部分都处于空闲的状态,在这种情况下,可以销毁一部分有效的数据库连接。销毁哪一个呢?

那就看哪一个数据库连接它的空闲时长到了我们指定的时间长度。如果到了就把它关闭,相当于这个数据库连接就被我们认为释放了。故还需要在这个连接池里边给它添加两个属性,

  1. 一个是超时时长:m_timeout
  2. 一个是最大的空闲时长:m_maxIdleTime

>>定义数据库连接池

#pragma once
#include <queue>
#include <mutex>
#include <condition_variable>//条件变量
#include "MysqlConn.h"
using namespace std;
class ConnPool {
public:
    static ConnPool* getConnPool();// 获得单例对象
    ConnPool(const ConnPool& obj) = delete; // 删除拷贝构造函数
    ConnPool& operator=(const ConnPool& obj) = delete; // 删除拷贝赋值运算符重载函数
    shared_ptr<MysqlConn> getConn(); // 从连接池中取出一个连接
    ~ConnPool(); // 析构函数
private:
    ConnPool(); // 构造函数私有化
    bool parseJsonFile(); // 解析json格式文件
    void produceConn(); // 生产数据库连接
    void recycleConn(); // 销毁数据库连接
    void addConn(); // 添加数据库连接

    // 连接服务器所需信息
    string m_ip;            // 数据库服务器ip地址
    string m_user;          // 数据库服务器用户名
    string m_dbName;        // 数据库服务器的数据库名
    string m_passwd;        // 数据库服务器密码
    unsigned short m_port;  // 数据库服务器绑定的端口

    // 连接池信息
    queue<MysqlConn*> m_connQ;
    unsigned int m_maxSize; // 连接数上限值
    unsigned int m_minSize; // 连接数下限值
    int m_timeout; // 连接超时时长
    int m_maxIdleTime; // 最大的空闲时长
    mutex m_mutexQ; // 独占互斥锁
    condition_variable m_cond; // 条件变量
};

>>单例类的构造函数处理

单例模式有两种实现:

(1)懒汉模式:在使用这个实例对象的时候,才去创建它。单例模式的类肯定是有且只有一个的。懒汉模式的实现方式有很多种方式:

  1. 使用静态的局部变量;(在C++11里边使用静态局部变量是没有线程安全问题的)
  2. 互斥锁(保证创建出来的实例对象有且仅有一个)
  3. call_once函数(C++11提供的),可以实现懒汉模式的单例类

(2)饿汉模式:不管我们用不用这个实例对象,只要这个类被创建出来了,那么对应的这个实例对象也就有了。饿汉模式是没有线程安全的问题的。因为当我们把这个饿汉模式的实例对象创建出来此时如果有多个线程来访问这个单例对象,不涉及到对象的创建。因此多线程在访问这个单例对象的时候,肯定也是线程安全的。

把构造函数设为私有的(private),那如果我们想要得到这样的一个类的实例对象,可以通过类名来获取到这个类的实例,它能够访问的肯定是静态的函数或者静态变量。所以可以给这个类添加一个静态方法getConnPool();

ConnPool* ConnPool::getConnPool() {
    static ConnPool pool;
    return &pool;
}

C++11中实现懒汉模式,在getConnPool()函数里使用静态的局部变量是安全的。

静态局部变量的访问范围为当前的函数,但生命周期有多长呢?应用程序结束之后,pool指向的这块内存才能够被析构。不是每调用一次getConnPool函数,这个pool对象就会被创建一次。当第一次调用getConnPool函数的时候,这个静态的局部对象被创建出来了,第二次调用这个函数的时候,这个静态的局部对象它已经存在了。所以不管后边对这个函数调用多少次,和第一次调用的时候是一样的,得到的是同一块内存地址。只不过,这个ConnPool类型的对象的访问类型是受限的。只能够在当前的函数里边被访问。调用getConnPool函数,把这个pool地址返回给函数的调用者,就可以得到这个类唯一的实例。

另外,在实现一个单例类的时候,除了把构造函数设为私有的(private)。还有需要注意一点:通过拷贝构造函数也可以创建出对应的实例对象,所以应该把拷贝构造函数也设置成私有的(private)。或者说把这个拷贝构造函数直接删除掉。

如何删除拷贝构造函数呢?在C++11里边提供了=delete可以把对应的函数显示的删除。当把这个函数删除了,我们就不能在程序中再次使用这个函数了。

除了删除构造函数,还有一个需要删除的就是拷贝赋值运算符重载函数。如果我们创建一个类,在这个类里边其实是给我们提供了六个默认的函数:

  • 无参的构造函数
  • 析构函数
  • 拷贝构造函数
  • 移动构造函数
  • 拷贝赋值运算符重载函数
  • 移动赋值操作符重载

通过这个拷贝赋值运算符重载函数,也就是通过等号=,我们就能够创建出一个新的对象,其实和拷贝构造函数实现的功能是一样的。

移动赋值操作符重载,它的功能呢和移动构造函数是差不多的,移动赋值操作符重载函数,它重载的也是等号操作符,只不过它和赋值操作符重载的函数的参数是不一样的。移动赋值操作符重载和移动构造函数,它们实现的是资源的转移,而不是进行数据的拷贝。所以要把拷贝赋值运算符重载对应的等号操作符函数删除掉。其实设置为私有也可以防止对象的复制。

class ConnPool {
public:
    static ConnPool* getConnPool();// 获得单例对象
    ConnPool(const ConnPool& obj) = delete; // 删除拷贝构造函数
    ConnPool& operator=(const ConnPool& obj) = delete; // 删除拷贝赋值运算符重载函数
private:
    ConnPool(); // 构造函数私有化
};

 >>添加和解析配置文件

jsoncpp的编译和使用 | 爱编程的大丙 (subingwen.cn)icon-default.png?t=N7T8https://www.subingwen.cn/cpp/jsoncpp/

bool parseJsonFile(); // 解析json格式文件
ConnPool::ConnPool() {
    // 加载配置文件
    if (!parseJsonFile()) {
        std::cout << "加载配置文件失败!!!" << std::endl;
        return;
    }
    for (int i = 0; i < m_minSize; ++i) {
        addConn();
    }
    thread producer(&ConnPool::produceConn, this);// 生产连接
    thread recycler(&ConnPool::recycleConn, this);// 销毁连接
    producer.detach();
    recycler.detach();
}

>>添加连接到连接池

void addConn(); // 添加数据库连接
// 添加连接到连接池
void ConnPool::addConn() {
    MysqlConn* conn = new MysqlConn;
    conn->connect(m_user, m_passwd, m_dbName, m_ip, m_port);
    conn->refreshAliveTime();// 记录建立连接的时候的对应的时间戳
    m_connQ.push(conn);
}

 >>单例类的构造函数处理

需要实时检测连接池里边的连接空闲数量是否太多了,如果太多了就需要销毁一部分。关于连接池里边的连接的创建和销毁需要交给两个线程去处理。通过这种方式,可以让数据库连接池里边的连接数量维持在一个合理的范围内。 

  • 一个创建新的连接,一个销毁空闲的连接。
  • 注:线程在阻塞的时候是不消耗CPU资源的。
ConnPool(); // 构造函数私有化
ConnPool::ConnPool() {
    // 加载配置文件
    if (!parseJsonFile()) {
        std::cout << "加载配置文件失败!!!" << std::endl;
        return;
    }
    for (int i = 0; i < m_minSize; ++i) {
        addConn();
    }
    thread producer(&ConnPool::produceConn, this);// 生产连接
    thread recycler(&ConnPool::recycleConn, this);// 销毁连接
    producer.detach();
    recycler.detach();
}

>>数据库连接池子线程任务函数的实现

(1)生产数据库连接

void produceConn(); // 生产数据库连接

当连接池队列里边的这个连接的个数小于最小连接数的时候,连接不够用了。因为连接池里边需要保持一个最小的连接数,当实际数量<最小连接数的时候,需要生产出新的连接。

  • 实际数量>=最小连接数的时候,生产连接的线程它就需要阻塞了。
void ConnPool::produceConn() {
    while (true) {  // 生产者线程不断生产连接,直到连接池达到最大值
        unique_lock<mutex> locker(m_mutexQ);  // 加锁,保证线程安全
        while (m_connQ.size() >= m_minSize) {
            m_cond.wait(locker);  // 等待消费者通知
        }
        addConn(); // 生产连接
        m_cond.notify_all();// 通知消费者(唤醒)
    }
}

 (2)销毁数据库连接

void recycleConn(); // 销毁数据库连接
// 回收数据库连接
void ConnPool::recycleConn() {
    while (true) {
        this_thread::sleep_for(chrono::milliseconds(500));// 每隔半秒钟检测一次
        lock_guard<mutex> locker(m_mutexQ);  // 加锁,保证线程安全
        while (m_connQ.size() > m_minSize) {  // 如果连接池中的连接数大于最小连接数,则回收连接
            MysqlConn* conn = m_connQ.front();  // 取出连接池中的连接
            if (conn->getAliveTime() >= m_maxIdleTime) {
                m_connQ.pop();  // 回收连接
                delete conn;  // 释放连接资源
            }
            else {
                break;  // 如果连接的空闲时间小于最大空闲时间,则跳出循环
            }
        }
    }
}

>>数据库连接的获取和回收

shared_ptr<MysqlConn> getConn(); // 从连接池中取出一个连接
// 从连接池中取出一个连接
shared_ptr<MysqlConn> ConnPool::getConn() {
    unique_lock<mutex> locker(m_mutexQ);
    while (m_connQ.empty()) {
        if (cv_status::timeout == m_cond.wait_for(locker, chrono::milliseconds(m_timeout))) {
            if (m_connQ.empty()) {
                //return nullptr;
                continue;
            }
        }
    }
    shared_ptr<MysqlConn>connptr(m_connQ.front(), [this](MysqlConn* conn) {
        lock_guard<mutex>locker(m_mutexQ); // 自动管理加锁和解锁
        conn->refreshAliveTime();// 更新连接的起始的空闲时间点
        m_connQ.push(conn); // 回收数据库连接,此时它再次处于空闲状态
        });// 智能指针
    m_connQ.pop();
    m_cond.notify_one(); // 本意是唤醒生产者
    return connptr;
}

当对应的线程把连接conn拿走之后,操作完了。数据库需要把这个连接还回来,有两种解决方案:

  1. 提供一个额外的函数,给这个函数指定一个参数,当这个线程把数据库连接用完之后,它通过参数把这个连接传进来,那么我们再把这个连接存储到数据库连接池对应的队列里边。
  2. 使用智能指针,就不需要提供一个额外的函数了。直接通过getConn函数就能够实现数据库连接回收这个功能了。

首先,把返回的指针通过智能指针管理起来,这个智能指针有两种:一种是共享的智能指针,还有一种是独占的智能指针。在本项目中这里就使用共享指针就可以了。

为了实现数据库连接的自动回收,当共享的智能指针对应的对象析构的时候,它会先析构它管理的指针,而这个指针指向的连接我们不需要让它析构的,因为我们使用数据库连接池的目的是避免数据库连接的频繁创建和销毁,那怎么办呢?

由于当共享指针对象析构的时候,不需要把智能指针管理的连接(地址)析构,而是回收。因此我们可以手动去指定这个共享的智能指针它的删除器对应的处理动作。 

在构造这个智能指针的时候,在它构造函数的第二个参数位置指定删除器,可以指定一个有名函数,也可以指定一个匿名函数。简单的方式就是写一个匿名函数。在匿名函数的中括号里边可以指定匿名函数内部它捕捉外部变量的方式 。如果指定this就在匿名函数内部可以使用当前类里边的所有的程序变量或者是程序函数。删除器的类型和它管理的这块内存地址的类型肯定是对应的。在匿名函数里边,并不是真正的要销毁这个指针指向的那块内存,而是要把它放到数据库连接池对应的队列里边。回收数据库连接,此时它再次处于空闲状态,还需要更新连接的起始的空闲时间点。 

>>数据库连接池的资源释放

// 释放数据库连接
~MysqlConn();
// 释放数据库连接
MysqlConn::~MysqlConn() {
    if (m_conn != nullptr) {
        mysql_close(m_conn);
    }
    freeResult();
}

>>测试数据库的数据插入和查询

int query() {
    MysqlConn conn;
    conn.connect("heheda", "123456", "test", "127.0.0.1", 3306);
    string sql = "insert into person values(6,35,'女','乌拉那拉皇后')";
    bool flag = conn.update(sql);
    if (flag) cout << "插入数据成功了!!!" << endl;
    else cout << "插入数据失败,耐心检查一下哪里有出差错喔!" << endl;

    sql = "select * from person";
    conn.query(sql);
    while (conn.next()) {
        cout << conn.value(0) << ", "
            << conn.value(1) << ", "
            << conn.value(2) << ", "
            << conn.value(3) << endl;
    }
    return 0;
}

>>两个op函数,用于测试的

void op1(int begin, int end) {
    for (int i = begin; i < end; ++i) {
        MysqlConn conn;
        conn.connect("heheda", "123456", "test", "127.0.0.1", 3306);
        char sql[1024] = { 0 };
        sprintf(sql, "insert into person (id,age,sex,name) values(%d,%d,'%s','%s')",
            i, 6, "女", "胧月大魔王");
        conn.update(sql);
    }
}

void op2(ConnPool* pool, int begin, int end) {
    for (int i = begin; i < end; ++i) {
        shared_ptr<MysqlConn> conn = pool->getConn();
        char sql[1024] = { 0 };
        sprintf(sql, "insert into person (id,age,sex,name) values(%d,%d,'%s','%s')",
            i, 19, "女", "安陵容");
        conn->update(sql);
    }
}

 >>单线程模式下连接池和非连接池测试

void test1() {
#if 1
    // 非连接池,单线程,用时:26276567700 纳秒, 26276 毫秒
    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
    //连接池,单线程,用时:6100671100 纳秒, 6100 毫秒
    ConnPool* pool = ConnPool::getConnPool();
    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 test2() {
#if 0
    // 非连接池,多线程,用时:10107791500 纳秒,10107 毫秒
    MysqlConn conn;
    conn.connect("heheda", "123456", "test", "127.0.0.1", 3306);
    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 
    // 连接池,多线程,用时:2411691800 纳秒,2411 毫秒
    ConnPool* pool = ConnPool::getConnPool();
    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
}

完整代码:

  • main.cpp
#include <iostream>
#include <memory>
#include "MysqlConn.h"
#include "ConnPool.h"
using namespace std;
// 1.单线程:使用/不适用连接池
// 2.多线程:使用/不适用连接池

void op1(int begin, int end) {
    for (int i = begin; i < end; ++i) {
        MysqlConn conn;
        conn.connect("heheda", "123456", "test", "127.0.0.1", 3306);
        char sql[1024] = { 0 };
        sprintf(sql, "insert into person (id,age,sex,name) values(%d,%d,'%s','%s')",
            i, 6, "女", "胧月大魔王");
        conn.update(sql);
    }
}

void op2(ConnPool* pool, int begin, int end) {
    for (int i = begin; i < end; ++i) {
        shared_ptr<MysqlConn> conn = pool->getConn();
        char sql[1024] = { 0 };
        sprintf(sql, "insert into person (id,age,sex,name) values(%d,%d,'%s','%s')",
            i, 19, "女", "安陵容");
        conn->update(sql);
    }
}

// 压力测试
void test1() {
#if 1
    // 非连接池,单线程,用时:26276567700 纳秒, 26276 毫秒
    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
    //连接池,单线程,用时:6100671100 纳秒, 6100 毫秒
    ConnPool* pool = ConnPool::getConnPool();
    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 test2() {
#if 0
    // 非连接池,多线程,用时:10107791500 纳秒,10107 毫秒
    MysqlConn conn;
    conn.connect("heheda", "123456", "test", "127.0.0.1", 3306);
    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 
    // 连接池,多线程,用时:2411691800 纳秒,2411 毫秒
    ConnPool* pool = ConnPool::getConnPool();
    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 query() {
    MysqlConn conn;
    conn.connect("heheda", "123456", "test", "127.0.0.1", 3306);
    string sql = "insert into person values(6,35,'女','乌拉那拉皇后')";
    bool flag = conn.update(sql);
    if (flag) cout << "插入数据成功了!!!" << endl;
    else cout << "插入数据失败,耐心检查一下哪里有出差错喔!" << endl;

    sql = "select * from person";
    conn.query(sql);
    while (conn.next()) {
        cout << conn.value(0) << ", "
            << conn.value(1) << ", "
            << conn.value(2) << ", "
            << conn.value(3) << endl;
    }
    return 0;
}

int main() {
    //SetConsoleOutputCP(CP_UTF8);
    //query();
    //test1();
    test2();
    return 0;
}
  • MysqlConn.h
#pragma once
#include <mysql.h>
#include <string>
#include <chrono>
using namespace std;
using namespace std::chrono;
class MysqlConn {
public:
    // 初始化数据库连接
    MysqlConn();
    // 释放数据库连接
    ~MysqlConn();
    // 连接数据库
    bool connect(string user, string passwd, string dbName, string ip, unsigned short port = 3306);
    // 更新数据库: select,update,delete
    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 freeResult();
    MYSQL* m_conn = nullptr; // 数据库连接
    MYSQL_RES* m_result = nullptr;
    MYSQL_ROW m_row = nullptr;
    steady_clock::time_point m_aliveTime;
};
  • MysqlConn.cpp

#include "MysqlConn.h"

// 初始化数据库连接
MysqlConn::MysqlConn() {
    m_conn = mysql_init(nullptr);
    mysql_set_character_set(m_conn, "GBK"); // 设置字符集
}

// 释放数据库连接
MysqlConn::~MysqlConn() {
    if (m_conn != nullptr) {
        mysql_close(m_conn);
    }
    freeResult();
}

// 连接数据库
bool MysqlConn::connect(string user, string passwd, string dbName, string ip, unsigned short port) {
    MYSQL* ptr = mysql_real_connect(m_conn, ip.c_str(), user.c_str(), passwd.c_str(), dbName.c_str(), port, nullptr, 0);
    return ptr != nullptr;
}

// 更新数据库:insert,update,delete
bool MysqlConn::update(string sql) {
    if (mysql_query(m_conn, sql.c_str())) {
        return false;
    }
    return true;
}

// 查询数据库
bool MysqlConn::query(string sql) {
    freeResult(); // 释放之前查询的结果集
    if (mysql_query(m_conn, sql.c_str())) {
        return false;
    }
    m_result = mysql_store_result(m_conn); // 获取查询结果
    return true;
}

// 遍历查询得到的结果集
bool MysqlConn::next() {
    if (m_result != nullptr) {
        m_row = mysql_fetch_row(m_result);
        if (m_row != nullptr) {
            return true;
        }
    }
    return false;
}

// 得到结果集中的字段值
string MysqlConn::value(int index) {
    int rowCount = mysql_num_fields(m_result);
    if (index >= rowCount || index < 0) {
        return string();
    }
    char* val = m_row[index];
    unsigned long length = mysql_fetch_lengths(m_result)[index];
    return string(val, length);
}

// 事务操作
bool MysqlConn::transaction() {
    return mysql_autocommit(m_conn, false);
}

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

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

// 刷新起始的空闲时间点
void MysqlConn::refreshAliveTime() {
    // 这个时间戳就是某个数据库连接,它起始存活的时间点
    // 这个时间点通过时间类就可以得到了
    m_aliveTime = steady_clock::now();
}

// 计算连接存活的总时长
long long MysqlConn::getAliveTime() {
    nanoseconds duration = steady_clock::now() - m_aliveTime;
    milliseconds millsec = duration_cast<milliseconds>(duration);
    return millsec.count();
}

void MysqlConn::freeResult() {
    if (m_result != nullptr) {
        mysql_free_result(m_result);
        m_result = nullptr;
    }
}
  • ConnPool.h
#pragma once
#include <queue>
#include <mutex>
#include <condition_variable>//条件变量
#include "MysqlConn.h"
using namespace std;
class ConnPool {
public:
    static ConnPool* getConnPool();// 获得单例对象
    ConnPool(const ConnPool& obj) = delete; // 删除拷贝构造函数
    ConnPool& operator=(const ConnPool& obj) = delete; // 删除拷贝赋值运算符重载函数
    shared_ptr<MysqlConn> getConn(); // 从连接池中取出一个连接
    ~ConnPool(); // 析构函数
private:
    ConnPool(); // 构造函数私有化
    bool parseJsonFile(); // 解析json格式文件
    void produceConn(); // 生产数据库连接
    void recycleConn(); // 销毁数据库连接
    void addConn(); // 添加数据库连接

    // 连接服务器所需信息
    string m_ip;            // 数据库服务器ip地址
    string m_user;          // 数据库服务器用户名
    string m_dbName;        // 数据库服务器的数据库名
    string m_passwd;        // 数据库服务器密码
    unsigned short m_port;  // 数据库服务器绑定的端口

    // 连接池信息
    queue<MysqlConn*> m_connQ;
    unsigned int m_maxSize; // 连接数上限值
    unsigned int m_minSize; // 连接数下限值
    int m_timeout; // 连接超时时长
    int m_maxIdleTime; // 最大的空闲时长
    mutex m_mutexQ; // 独占互斥锁
    condition_variable m_cond; // 条件变量
};
  • ConnPool.cpp
#include "ConnPool.h"
#include <json/json.h>
#include <fstream>
#include <thread>
#include <iostream>
using namespace Json;
ConnPool* ConnPool::getConnPool() {
    static ConnPool pool;
    return &pool;
}

// 从连接池中取出一个连接
shared_ptr<MysqlConn> ConnPool::getConn() {
    unique_lock<mutex> locker(m_mutexQ);
    while (m_connQ.empty()) {
        if (cv_status::timeout == m_cond.wait_for(locker, chrono::milliseconds(m_timeout))) {
            if (m_connQ.empty()) {
                //return nullptr;
                continue;
            }
        }
    }
    shared_ptr<MysqlConn>connptr(m_connQ.front(), [this](MysqlConn* conn) {
        lock_guard<mutex>locker(m_mutexQ); // 自动管理加锁和解锁
        conn->refreshAliveTime();// 更新连接的起始的空闲时间点
        m_connQ.push(conn); // 回收数据库连接,此时它再次处于空闲状态
        });// 智能指针
    m_connQ.pop();
    m_cond.notify_one(); // 本意是唤醒生产者
    return connptr;
}

ConnPool::~ConnPool() {
    while (!m_connQ.empty()) {
        MysqlConn* conn = m_connQ.front();
        m_connQ.pop();
        delete conn;
    }
}

ConnPool::ConnPool() {
    // 加载配置文件
    if (!parseJsonFile()) {
        std::cout << "加载配置文件失败!!!" << std::endl;
        return;
    }
    for (int i = 0; i < m_minSize; ++i) {
        addConn();
    }
    thread producer(&ConnPool::produceConn, this);// 生产连接
    thread recycler(&ConnPool::recycleConn, this);// 销毁连接
    producer.detach();
    recycler.detach();
}

bool ConnPool::parseJsonFile() {
    ifstream ifs("dbconf.json");
    Reader rd;
    Value root;
    rd.parse(ifs, root);
    if (root.isObject()) {
        std::cout << "开始解析配置文件..." << std::endl;
        m_ip = root["ip"].asString();
        m_port = root["port"].asInt();
        m_user = root["userName"].asString();
        m_passwd = root["password"].asString();
        m_dbName = root["dbName"].asString();
        m_minSize = root["minSize"].asInt();
        m_maxSize = root["maxSize"].asInt();
        m_maxIdleTime = root["maxIdleTime"].asInt();
        m_timeout = root["timeout"].asInt();
        return true;  // 解析成功返回true,否则返回false。
    }
    return false;
}

void ConnPool::produceConn() {
    while (true) {  // 生产者线程不断生产连接,直到连接池达到最大值
        unique_lock<mutex> locker(m_mutexQ);  // 加锁,保证线程安全
        while (m_connQ.size() >= m_minSize) {
            m_cond.wait(locker);  // 等待消费者通知
        }
        addConn(); // 生产连接
        m_cond.notify_all();// 通知消费者(唤醒)
    }
}

// 回收数据库连接
void ConnPool::recycleConn() {
    while (true) {
        this_thread::sleep_for(chrono::milliseconds(500));// 每隔半秒钟检测一次
        lock_guard<mutex> locker(m_mutexQ);  // 加锁,保证线程安全
        while (m_connQ.size() > m_minSize) {  // 如果连接池中的连接数大于最小连接数,则回收连接
            MysqlConn* conn = m_connQ.front();  // 取出连接池中的连接
            if (conn->getAliveTime() >= m_maxIdleTime) {
                m_connQ.pop();  // 回收连接
                delete conn;  // 释放连接资源
            }
            else {
                break;  // 如果连接的空闲时间小于最大空闲时间,则跳出循环
            }
        }
    }
}

// 添加连接到连接池
void ConnPool::addConn() {
    MysqlConn* conn = new MysqlConn;
    conn->connect(m_user, m_passwd, m_dbName, m_ip, m_port);
    conn->refreshAliveTime();// 记录建立连接的时候的对应的时间戳
    m_connQ.push(conn);
}

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:/a/336969.html

如若内容造成侵权/违法违规/事实不符,请联系我们进行投诉反馈qq邮箱809451989@qq.com,一经查实,立即删除!

相关文章

二叉树基础oj题目

二叉树基础oj题目及思路总结 前文中&#xff0c;介绍了二叉树的基本概念及基础操作&#xff0c;进一步对于二叉树的递归遍历及子问题的处理思想有了一定的了解。本文将带来几道二叉树经典的oj题目。 目录 二叉树基础oj题目 对称二叉树平衡二叉树二叉树的层序遍历 二叉树基…

基于一次应用卡死问题所做的前端性能评估与优化尝试

问题背景 在上个月&#xff0c;由于客户反馈客户端卡死现象但我们远程却难以复现此现象&#xff0c;于是我们组织了一次现场上门故障排查&#xff0c;并希望基于此次观察与优化&#xff0c;为客户端开发提供一些整体的优化升级。当然&#xff0c;在尝试过程中&#xff0c;也发…

使用docker配置semantic slam

一.Docker环境配置 1.拉取Docker镜像 sudo docker pull ubuntu:16.04拉取的为ununtu16版本镜像&#xff0c;环境十分干净&#xff0c;可以通过以下命令查看容器列表 sudo docker images 如果想删除多余的docker image&#xff0c;可以使用指令 sudo docker rmi -f <id&g…

黑马程序员-瑞吉外卖-day4

实现账号的启动禁止 EmployeeController PutMappingpublic R<String> update(RequestBody Employee employee){employeeService.updateById(employee);return R.success("员工信息修改成功");} 出错 解决 common目录下 引入JacksonObjectMapper package com…

Redis 面试题 | 02.精选Redis高频面试题

&#x1f90d; 前端开发工程师、技术日更博主、已过CET6 &#x1f368; 阿珊和她的猫_CSDN博客专家、23年度博客之星前端领域TOP1 &#x1f560; 牛客高级专题作者、打造专栏《前端面试必备》 、《2024面试高频手撕题》 &#x1f35a; 蓝桥云课签约作者、上架课程《Vue.js 和 E…

开源项目_大模型应用_Chat2DB

1 基本信息 项目地址&#xff1a;https://github.com/chat2db/Chat2DBStar&#xff1a;10.7K 2 功能 Chat2DB 是一个智能且多功能的 SQL 客户端和报表工具&#xff0c;适用于各种数据库。 对于那些平时会用到数据库&#xff0c;但又不是数据库专家的程序员来说&#xff0c;…

数据结构之树和二叉树定义

数据结构之树和二叉树定义 1、树的定义2、树的基本概念3、二叉树的定义 数据结构是程序设计的重要基础&#xff0c;它所讨论的内容和技术对从事软件项目的开发有重要作用。学习数据结构要达到的目标是学会从问题出发&#xff0c;分析和研究计算机加工的数据的特性&#xff0c;以…

【每日一题】按分隔符拆分字符串

文章目录 Tag题目来源解题思路方法一&#xff1a;遍历方法二&#xff1a;getline 写在最后 Tag 【遍历】【getline】【字符串】【2024-01-20】 题目来源 2788. 按分隔符拆分字符串 解题思路 方法一&#xff1a;遍历 思路 分隔符在字符串开始和结束位置时不需要处理。 分隔…

【JavaEE】_网络编程基础

目录 1. 网络编程基础 1.1 网络编程定义 1.2 网络编程中的基本概念 1.2.1 API 1.2.2.发送端和接收端 1.2.3 请求和响应 1.2.4 客户端和服务端 2. Socket 套接字 2.1 概念 2.2 分类 3. UDP数据报套接字编程 3.1 DatagramSocket API 3.1.1 含义 3.1.2 构造方法 3…

C++---判断闰年

一.闰年的定义 闰年是指在公历中&#xff0c;年份可以被4整除但不能被100整除的年份&#xff0c;或者可以被400整除的年份。简单来说&#xff0c;闰年是一个比平年多出一天的年份&#xff0c;即2月有29天。闰年的目的是校准公历与地球公转周期的差异&#xff0c;确保时间计算的…

记录一次QT乱码问题

问题描述 在敲陆文周的书《QT5开发及实例》的示例代码时&#xff0c;出现乱码&#xff0c;如下图所示 具体代码如下 Widget::Widget(QWidget *parent): QWidget(parent), ui(new Ui::Widget) {ui->setupUi(this);ui->treeWidget->clear();int groupSize 2;int ite…

【C++类与对象】继承

继承 一、继承基本语法二、继承方式1、公共继承public2、保护继承protected3、私有继承private 三、对象模型四、继承中的构造与析构顺序五、同名成员处理方式同名静态成员处理方法 六、多继承语法&#xff08;不建议使用&#xff09;七、菱形继承&#xff08;钻石继承&#xf…

R 语言学习 case3:柱状图(ggchart)

主要涉及到对图的优化&#xff0c;使用ggchart工具包 ggchart 链接&#xff1a;https://thomas-neitmann.github.io/ggcharts/index.html step1: 安装工具包 install.packages("ggcharts") install.packages("tidytext")step2: 导入工具包 library(dplyr…

高性能前端UI库 SolidJS | 超棒 NPM 库

SolidJS是一个声明式的、高效的、编译时优化的JavaScript库&#xff0c;用于构建用户界面。它的核心特点是让你能够编写的代码既接近原生JavaScript&#xff0c;又能够享受到现代响应式框架提供的便利。 SolidJS的设计哲学强调了性能与简洁性。它不使用虚拟DOM&#xff08;Vir…

makefile中的伪目标和模式匹配

文章目录 makefile&#xff0c;伪目标和模式匹配 makefile&#xff0c;伪目标和模式匹配 伪目标 .PHONY:clean 声明目标为伪目标之后&#xff0c;makefile将不会判断目标是否存在或该目标是否需要更新, 简单来说就是不会受到一些同名的文件的影响&#xff0c;也会进来makefi…

Ubuntu使用docker-compose安装mysql8或mysql5.7

ubuntu环境搭建专栏&#x1f517;点击跳转 Ubuntu系统环境搭建&#xff08;十四&#xff09;——使用docker-compose安装mysql8或mysql5.7 文章目录 Ubuntu系统环境搭建&#xff08;十四&#xff09;——使用docker-compose安装mysql8或mysql5.7MySQL81.新建文件夹2.创建docke…

ERP进出库+办公用品管理系统

系统架构 简介系统架构部分页面结构图UML逻辑图办公用品入出库 简介 本系统适用于ERP企业公司职员关于系统化的申请相关办公用品&#xff0c;提高整体系统整合行&#xff0c;加大上下级之间的联系&#xff0c;规避因人员过多&#xff0c;而浪费人力在简单重复的工作中&#xf…

Python项目——搞怪小程序(PySide6+Pyinstaller)

1、介绍 使用python编写一个小程序&#xff0c;回答你是猪吗。 点击“是”提交&#xff0c;弹窗并退出。 点击“不是”提交&#xff0c;等待5秒&#xff0c;重新选择。 并且隐藏了关闭按钮。 2、实现 新建一个项目。 2.1、设计UI 使用Qt designer设计一个UI界面&#xff0c…

【前后端分离与不分离的区别】

Web 应用的开发主要有两种模式&#xff1a; 前后端不分离 前后端分离 理解它们的区别有助于我们进行对应产品的测试工作。 前后端不分离 在早期&#xff0c;Web 应用开发主要采用前后端不分离的方式&#xff0c;它是以后端直接渲染模板完成响应为主的一种开发模式。以前后端不…

探索Vue3:深入理解响应式语法糖

🚀 欢迎来到我的专栏!专注于Vue3的实战总结和开发实践分享,让你轻松驾驭Vue3的奇妙世界! 🌈✨在这里,我将为你呈现最新的Vue3技术趋势,分享独家实用教程,并为你解析开发中的难题。让我们一起深入Vue3的魅力,助力你成为Vue大师! 👨‍💻💡不再徘徊,快来关注…