C++11代码实战经典—MySQL数据库连接池

news2025/1/12 0:55:22

课程总目录


文章目录

  • 一、项目介绍
    • 1.1 关键技术点
    • 1.2 项目背景
    • 1.3 连接池功能点介绍
    • 1.4 MySQL Server参数介绍
    • 1.5 项目功能点设计和技术细节
  • 二、MySQL数据库编程
  • 三、项目代码逐步实现
    • 3.1 连接池单例模式实现
    • 3.2 实现加载配置项
    • 3.3 连接池的构造函数
    • 3.4 实现生产者
    • 3.5 实现消费者
    • 3.6 实现回收超过最大空闲时间的连接
  • 四、连接池压力测试
  • 五、完整源码下载


一、项目介绍

1.1 关键技术点

  • MySQL数据库编程
  • 单例模式
  • queue队列容器
  • C++11多线程编程
  • 线程互斥
  • 线程同步通信和unique_lock
  • 基于CAS的原子整形
  • 智能指针shared_ptr
  • lambda表达式
  • 生产者-消费者线程模型

1.2 项目背景

为了提高MySQL数据库(基于C/S设计)的访问瓶颈,除了在服务器端增加缓存服务器缓存常用的数据之外(例如redis),还可以增加连接池,来提高 MySQL Server 的访问效率,在高并发情况下,大量的TCP三次握手、MySQL Server 连接认证、MySQL Server 关闭连接回收资源和TCP四次挥手所耗费的性能时间也是很明显的,增加连接池就是为了减少这一部分的性能损耗
在这里插入图片描述
在市场上比较流行的连接池包括阿里的druid、c3p0以及apache的dbcp连接池,它们对于短时间内大量 的数据库增删改查操作性能的提升是很明显的,但是它们有一个共同点是全部由Java实现的。

那么本项目就是为了在 C/C++ 项目中,提高 MySQL Server 的访问效率,实现基于C++代码的数据库连接池模块

1.3 连接池功能点介绍

连接池一般包含了数据库连接所用的 ip 地址、port 端口号、用户名和密码以及其它的性能参数,例如初始连接量、最大连接量、最大空闲时间、连接超时时间等,该项目是基于C++语言实现的连接池,主要也是实现以上几个所有连接池都支持的通用基础功能

  • 初始连接量(initSize):表示连接池事先会和 MySQL Server 创建initSize个数量的connection连接,当应用发起 MySQL 访问时,不用再创建和 MySQL Server 新的连接,直接从连接池中获取一个可用的连接就可以了,使用完成后,并不去释放connection,而是把当前connection再归还到连接池当中
  • 最大连接量(maxSize):当并发访问 MySQL Server 的请求增多时,初始连接量initSize已经不够使用了,此时会根据新的请求数量去创建更多的连接给应用去使用,但是新创建的连接数量上限是maxSize,不能无限制的创建连接,因为每一个连接都会占用一个socket资源,一般连接池和服务器程序是部署在一台主机上的,如果连接池占用过多的socket资源,那么服务器就不能接收太多的客户端请求了。当这些连接使用完成后,再次归还到连接池当中来维护
  • 最大空闲时间(maxIdleTime):当访问 MySQL 的并发请求多了以后,连接池里面的连接数量会动态增加,上限是maxSize个,当这些连接用完会再次归还到连接池当中。如果在指定的maxIdleTime里面,这些新增加的连接都没有被再次使用过,那么新增加的这些连接资源就要被回收掉,只需要保持初始连接量initSize个连接就可以了
  • 连接超时时间(connectionTimeout):当 MySQL 的并发请求量过大,连接池中的连接数量已经到达最大连接量maxSize了,而此时没有空闲的连接可供使用,那么此时应用从连接池获取连接无法成功,会通过阻塞的方式等待获取连接,获取连接的时间如果超过connectionTimeout时间,那么获取连接失败,无法访问数据库

1.4 MySQL Server参数介绍

show variables like 'max_connections';

该命令可以查看 MySQL Server 所支持的最大连接个数,超过max_connections数量的连接,MySQL Server 会直接拒绝,所以在使用连接池增加连接数量的时候,MySQL Server 的max_connections参数也要适当的进行调整,以适配连接池的连接上限
在这里插入图片描述

1.5 项目功能点设计和技术细节

项目结构:

  • ConnectionPool.cppConnectionPool.h:连接池的代码实现
  • Connection.cppConnection.h:数据库操作代码、增删改查代码实现

连接池主要包含了以下功能点:

  1. 连接池只需要一个实例,所以ConnectionPool懒汉式单例模式进行设计
  2. ConnectionPool中可以获取和 MySQL 的Connection连接
  3. 空闲的Connection连接全部维护在一个线程安全的盛放Connection连接的queue队列中,使用线程互斥锁保证队列的线程安全
  4. 如果Connection队列为空,还需要再获取连接,此时需要动态创建连接,上限数量是最大连接量maxSize
  5. 队列中空闲连接的时间超过最大空闲时间maxIdleTime时就要被释放掉,只保留初始连接量initSize个连接就可以了,这个功能点需要放在独立的线程中去做
  6. 如果Connection队列为空,而此时连接的数量已达上限最大连接量maxSize,那么等待connectionTimeout时间(是等待这么多时间的期间获取连接,不是等待这么多时间之后再开始获取连接)如果还获取不到空闲的连接,那么获取连接失败,此处从Connection队列获取空闲连接,可以使用带超时时间的mutex互斥锁来实现连接超时时间
  7. 用户获取的连接用shared_ptr智能指针来管理,用lambda表达式定制连接释放的功能(不真正释放 连接,而是把连接归还到连接池中)
  8. 连接的生产和连接的消费采用生产者-消费者线程模型来设计,使用了线程间的同步通信机制的条件变量和互斥锁

在这里插入图片描述

二、MySQL数据库编程

安装MySQL这里就不赘述了,在做项目之前先检查MySQL是否正在运行
在这里插入图片描述
创建chat数据库

create database chat;
use chat;

创建user

CREATE TABLE user (  
    id INT NOT NULL AUTO_INCREMENT,  
    name VARCHAR(50) NOT NULL,  
    age INT NOT NULL,  
    sex ENUM('male', 'female') NOT NULL,  
    PRIMARY KEY (id)  
);

在这里插入图片描述

项目结构如图:
在这里插入图片描述

由于MySQL安装的64位版本的,库也都是64位版本,我们的项目一定要设置为64位的
在这里插入图片描述

接下来配置头文件和库文件:

  1. 在『C/C++ → \to 常规 → \to 附加包含目录』或『VC++目录 → \to 包含目录』中,填写 mysql.h 头文件的路径在这里插入图片描述
  2. 把 libmysql.dll 动态链接库(Linux下后缀名是.so库)放在工程目录下
    在这里插入图片描述
  3. 『链接器 → \to 常规 → \to 附加库目录』,填写 libmysql.lib 的路径在这里插入图片描述
  4. 『链接器 → \to 输入 → \to 附加依赖项』,填写 libmysql.lib 库的名字
    在这里插入图片描述

接下来,我们来进行MySQL数据库编程的测试

直接上代码,涉及到 public.h、Connection.h、Connection.cpp、main.cpp 五个文件

public.h:

#pragma once
#include <iostream>
#define LOG(str) \
    std::cout << __FILE__ << ":"<<__LINE__<<"  " \
    << __TIMESTAMP__ << ":"<<str <<std::endl;

Connection.h:

#pragma once
#include <mysql.h>
#include <string>
#include "public.h"
using namespace std;

// 实现MySQL数据库的操作
class Connection
{
public:
	// 构造:初始化数据库连接
	Connection();
	// 析构:释放数据库连接资源
	~Connection();

	// 连接数据库
	bool connect(string ip,
		unsigned short port,
		string user,
		string password,
		string dbname);

	// 更新操作:insert、delete、update
	bool update(string sql);

	// 查询操作:select
	MYSQL_RES* query(string sql);

private:
	MYSQL* _conn; // 表示和MySQLServer的一条连接
};

Connection.cpp:

#include "public.h"
#include "Connection.h"

Connection::Connection()
{
	// 初始化数据库连接
	_conn = mysql_init(nullptr);
}

Connection::~Connection()
{
	// 释放数据库连接资源
	if (_conn != nullptr)
		mysql_close(_conn);
}

bool Connection::connect(string ip, unsigned short port,
	string user, string password, string dbname)
{
	// 连接数据库
	MYSQL* p = mysql_real_connect(_conn, ip.c_str(), user.c_str(),
		password.c_str(), dbname.c_str(), port, nullptr, 0);
	return p != nullptr;
}

bool Connection::update(string sql)
{
	// 更新操作:insert、delete、update
	if (mysql_real_query(_conn, sql.c_str(), (unsigned long)sql.length()))
	{
		LOG("更新失败:" + sql);
		cout<< mysql error(_conn) << endl;
		return false;
	}
	return true;
}

MYSQL_RES* Connection::query(string sql)
{
	// 查询操作:select
	if (mysql_real_query(_conn, sql.c_str(), (unsigned long)sql.length()))
	{
		LOG("查询失败:" + sql);
		cout<< mysql error(_conn) << endl;
		return nullptr;
	}
	return mysql_use_result(_conn);
}

main.cpp:

#include <iostream>
#include "Connection.h"
using namespace std;

int main()
{
	Connection conn;
	//string sql = "insert into user(name,age,sex) values('zhang san', 20, 'male');";
	char sql[1024] = { 0 };
	sprintf(sql, "insert into user(name,age,sex) values('%s','%d','%s');",
		"zhang san", 20, "male");
	conn.connect("127.0.0.1", 3306, "root", "123456", "chat");
	conn.update(sql); //更新sql语句

	return 0;
}

可以在数据库中看到插入成功
在这里插入图片描述

三、项目代码逐步实现

先来把DBConnectionPool.h设计好:

// 实现连接池功能模块
class ConnectionPool
{
public:
	// 获取连接池对象实例:用静态方法来获取唯一实例
	static ConnectionPool* getConnectionPool();

	// 给外部提供接口,从连接池中获取一个可用的空闲连接(消费者)
	shared_ptr<Connection> getConnection();

private:
	// 单例模式:构造函数私有化
	ConnectionPool();

	// 单例模式:删除拷贝构造和赋值函数
	ConnectionPool(const ConnectionPool&) = delete;
	ConnectionPool& operator=(const ConnectionPool&) = delete;

	// 从配置文件中加载配置项
	bool loadConfigFile();

	// 生产者线程函数:运行在独立的线程中,专门负责生产新连接
	// 写成成员方法,方便访问成员变量
	void produceConnectionTask();
	
	// 定时线程函数:扫描超过maxIdleTime的空闲连接,进行连接回收
	void scannerConnectionTask();

	// 成员变量
	string			   _ip;  // mysql的ip地址
	unsigned short   _port;  // mysql的端口号,默认3306
	string		 _username;  // mysql登录用户名
	string       _password;  // mysql登录密码
	string		   _dbname;  // 连接的数据库名称
	int          _initSize;  // 连接池的初始连接量
	int           _maxSize;  // 连接池的最大连接量
	int       _maxIdleTime;  // 连接池最大空闲时间(秒)
	int _connectionTimeout;  // 连接池获取连接的超时时间(毫秒)

	// 存储mysql连接的队列
	queue<Connection*> _connectionQue;
	// 维护连接队列的线程安全互斥锁
	mutex _queueMutex;
	// 记录连接所创建的connection连接的总数量,线程安全的原子类型
	atomic_int _connectionCnt;
	// 设置条件变量,用于连接生产线程和消费线程的通信
	condition_variable cv;
};

3.1 连接池单例模式实现

之前在OOP经典设计模式讲解过单例模式,主要有四点需要注意的:

  1. 构造函数私有化
  2. 定义该类的唯一实例
  3. 通过静态方法返回唯一实例
  4. 删除拷贝构造和赋值运算符重载

DBConnectionPool.cpp中实现getConnectionPool

// 线程安全的懒汉单例模式的函数接口
ConnectionPool* ConnectionPool::getConnectionPool()
{
	// !!!函数内的静态局部变量天然线程安全,编译器会自动lock/unlock
	static ConnectionPool pool;
	return &pool;
}

3.2 实现加载配置项

源文件中添加添加mysql.ini

# 数据库连接池的配置文件
ip = 127.0.0.1
port = 3306
username = root
password = 123456
dbname = chat
initSize = 10
maxSize = 1024
# 最大空闲时间默认单位是秒
maxIdleTime = 60
#连接超时时间单位是毫秒
connectionTimeOut = 100

DBConnectionPool.cpp中实现loadConfigFile

// 从配置文件中加载配置项
bool ConnectionPool::loadConfigFile()
{
	ifstream file("mysql.ini");	// 对象生命周期结束时自动关闭文件
	if (!file.is_open())
	{
		LOG("mysql.ini file is not exist!");
		return false;
	}

	string line;
	while (getline(file, line))
	{
		// 去掉行首的空白字符
		line.erase(0, line.find_first_not_of(" \t\n\r\f\v"));

		// 跳过注释行和空行
		if (line.empty() || line[0] == '#')
			continue;

		istringstream iss(line);
		string key, value;

		// '='之前的读给key,'='之后的读给value
		if (getline(iss, key, '=') && getline(iss, value))
		{
			// 去掉key和value两端的空白字符
			key.erase(0, key.find_first_not_of(" \t\n\r\f\v"));
			key.erase(key.find_last_not_of(" \t\n\r\f\v") + 1);
			value.erase(0, value.find_first_not_of(" \t\n\r\f\v"));
			value.erase(value.find_last_not_of(" \t\n\r\f\v") + 1);

			if (key == "ip")
				_ip = value;
			else if (key == "port")
				_port = std::stoi(value);
			else if (key == "username")
				_username = value;
			else if (key == "password")
				_password = value;
			else if (key == "dbname")
				_dbname = value;
			else if (key == "initSize")
				_initSize = std::stoi(value);
			else if (key == "maxSize")
				_maxSize = std::stoi(value);
			else if (key == "maxIdleTime")
				_maxIdleTime = std::stoi(value);
			else if (key == "connectionTimeOut")
				_connectionTimeout = std::stoi(value);
		}
	}
	return true;
}

3.3 连接池的构造函数

DBConnectionPool.cpp中实现ConnectionPool构造函数

// 连接池的构造
ConnectionPool::ConnectionPool()
{
	// 加载配置项
	if (!loadConfigFile())
		return;

	// 创建初始数量的连接
	for (int i = 0; i < _initSize; ++i)
	{
		Connection* p = new Connection();
		p->connect(_ip, _port, _username, _password, _dbname);
		_connectionQue.push(p);
		_connectionCnt++;
	}

	// 启动一个新的线程,作为连接的生产者
	thread producer(bind(&ConnectionPool::produceConnectionTask, this));
	producer.detach();
}

3.4 实现生产者

DBConnectionPool.cpp中实现produceConnectionTask

// 生产者:运行在独立的线程中,专门负责生产新连接
void ConnectionPool::produceConnectionTask()
{
	while (true)
	{
		unique_lock<mutex> lck(_queueMutex);
		while (!_connectionQue.empty())
			cv.wait(lck); // 若队列不空,此处生产线程进入等待状态

		// 连接数量没有到达上限,继续创建新的连接
		if (_connectionCnt < _maxSize)
		{
			// 同构造函数中的逻辑
			Connection* p = new Connection();
			p->connect(_ip, _port, _username, _password, _dbname);
			_connectionQue.push(p);
			_connectionCnt++;
		}

		// 通知消费者线程,可以消费连接了
		cv.notify_all();
	}
}

3.5 实现消费者

DBConnectionPool.cpp中实现getConnection

// 消费者:从连接池中获取一个可用的空闲连接
shared_ptr<Connection> ConnectionPool::getConnection()
{
	unique_lock<mutex> lck(_queueMutex);
	while (_connectionQue.empty())
	{
		if (cv_status::timeout ==
			cv.wait_for(lck, chrono::milliseconds(_connectionTimeout)))
		{
			LOG("获取空闲连接超时了...获取连接失败!");
			return nullptr;
		}
	}

	/*
	shared_ptr智能指针析构时,会把connection资源直接delete掉,相当于
	调用connection的析构函数,connection就被close掉了。
	所以这里需要自定义shared_ptr的释放资源的方式,把connection直接归还到queue当中
	*/
	shared_ptr<Connection> sp(_connectionQue.front(), [&](Connection* pcon)
		{
			// 这里是在服务器应用线程中调用的,所以一定要考虑队列的线程安全操作
			unique_lock<mutex> lock(_queueMutex);
			_connectionQue.push(pcon);
		});

	_connectionQue.pop();
	cv.notify_all(); // 消费完连接以后,通知生产者线程检查一下,看一下如果队列依旧为空,则继续生产连接

	return sp;
}

3.6 实现回收超过最大空闲时间的连接

Connection.hConnection类中增加一个起始时间变量和成员方法

class Connection
{
...
public:
	// 刷新一下连接的起始的空闲时间点
	void refreshStartTime() { _startTime = clock(); }
	
	// 返回存活的时间
	clock_t getAliveTime() const { return clock() - _startTime; }
	
private:
	clock_t _startTime; // 记录进入队列的起始时间
...
};

要在进入队列的时候记录连接的起始时间,ConnectionPool的构造函数和生产者的代码部分有连接进入队列的操作,分别把这两处的代码进行如下的修改:

Connection* p = new Connection();
p->connect(_ip, _port, _username, _password, _dbname);
p->refreshStartTime(); // 记录连接起始时间
_connectionQue.push(p);
_connectionCnt++;

消费者的代码中也有归还连接的操作,也需要加一下:

shared_ptr<Connection> sp(_connectionQue.front(), [&](Connection* pcon)
	{
		// 这里是在服务器应用线程中调用的,所以一定要考虑队列的线程安全操作
		unique_lock<mutex> lock(_queueMutex);
		pcon->refreshStartTime(); // 记录连接起始时间
		_connectionQue.push(pcon);
	});

ConnectionPool构造函数中要启动一个定时线程,扫描超过maxIdleTime的空闲连接,进行多余的连接回收

ConnectionPool::ConnectionPool()
{
	...
	// 启动一个新的定时线程,扫描超过maxIdleTime的空闲连接,进行多余的连接回收
	thread scanner(bind(&ConnectionPool::scannerConnectionTask, this));
	scanner.detach();
}

DBConnectionPool.cpp中实现scannerConnectionTask

// 扫描超过maxIdleTime时间的空闲连接,进行对于的连接回收
void ConnectionPool::scannerConnectionTask()
{
	while (true)
	{
		// 通过sleep模拟定时效果(秒)
		this_thread::sleep_for(chrono::seconds(_maxIdleTime));

		// 扫描整个队列,释放多余的连接
		unique_lock<mutex> lock(_queueMutex);
		while (_connectionCnt > _initSize)
		{
			Connection* p = _connectionQue.front();	// 取出队头(队尾入队,队头是时间最长的那一个)
			if (p->getAliveTime() >= (_maxIdleTime * CLOCKS_PER_SEC)) //ctime中clock计算的是时钟周期(clock ticks)
			{
				_connectionQue.pop();
				_connectionCnt--;
				delete p; // 调用~Connection()释放连接
			}
			else
				break;	// 队头的连接没有超过_maxIdleTime,其它的连接肯定没有
		}
	}
}

四、连接池压力测试

单线程未使用连接池测试:

// 1000次
clock_t begin = clock();
for (int i = 0; i < 1000; ++i)
{
	Connection conn;
	string sql = "insert into user(name,age,sex) values('zhang san', 20, 'male');";
	conn.connect("127.0.0.1", 3306, "root", "123456", "chat");
	conn.update(sql); //更新sql语句
}
clock_t end = clock();
cout << (end - begin) << "ms" << endl;

单线程使用连接池测试:

// 1000次
clock_t begin = clock();
ConnectionPool* cp = ConnectionPool::getConnectionPool();
for (int i = 0; i < 1000; ++i)
{
	shared_ptr<Connection> sp = cp->getConnection();
	string sql = "insert into user(name,age,sex) values('zhang san', 20, 'male');";
	sp->update(sql);
}
clock_t end = clock();
cout << (end - begin) << "ms" << endl;

多线程未使用连接池测试:

// 下面各线程都是同一个用户名密码,大量并发会出问题,要先连接一下就行了
Connection conn;
conn.connect("127.0.0.1", 3306, "root", "123456", "chat");

clock_t begin = clock();

thread t1([]() {
	for (int i = 0; i < 1250; ++i)
	{
		Connection conn;
		string sql = "insert into user(name,age,sex) values('zhang san', 20, 'male');";
		conn.connect("127.0.0.1", 3306, "root", "123456", "chat");
		conn.update(sql);
	}
	});

thread t2([]() {
	for (int i = 0; i < 1250; ++i)
	{
		Connection conn;
		string sql = "insert into user(name,age,sex) values('zhang san', 20, 'male');";
		conn.connect("127.0.0.1", 3306, "root", "123456", "chat");
		conn.update(sql);
	}
	});

thread t3([]() {
	for (int i = 0; i < 1250; ++i)
	{
		Connection conn;
		string sql = "insert into user(name,age,sex) values('zhang san', 20, 'male');";
		conn.connect("127.0.0.1", 3306, "root", "123456", "chat");
		conn.update(sql);
	}
	});

thread t4([]() {
	for (int i = 0; i < 1250; ++i)
	{
		Connection conn;
		string sql = "insert into user(name,age,sex) values('zhang san', 20, 'male');";
		conn.connect("127.0.0.1", 3306, "root", "123456", "chat");
		conn.update(sql);
	}
	});

t1.join();
t2.join();
t3.join();
t4.join();

clock_t end = clock();
cout << (end - begin) << "ms" << endl;

多线程使用连接池测试

clock_t begin = clock();
ConnectionPool* cp = ConnectionPool::getConnectionPool();

thread t1([=]() {
	for (int i = 0; i < 2500; ++i)
	{
		shared_ptr<Connection> sp = cp->getConnection();
		string sql = "insert into user(name,age,sex) values('zhang san', 20, 'male');";
		sp->update(sql);
	}
	});

thread t2([=]() {
	for (int i = 0; i < 2500; ++i)
	{
		shared_ptr<Connection> sp = cp->getConnection();
		string sql = "insert into user(name,age,sex) values('zhang san', 20, 'male');";
		sp->update(sql);
	}
	});

thread t3([=]() {
	for (int i = 0; i < 2500; ++i)
	{
		shared_ptr<Connection> sp = cp->getConnection();
		string sql = "insert into user(name,age,sex) values('zhang san', 20, 'male');";
		sp->update(sql);
	}
	});

thread t4([=]() {
	for (int i = 0; i < 2500; ++i)
	{
		shared_ptr<Connection> sp = cp->getConnection();
		string sql = "insert into user(name,age,sex) values('zhang san', 20, 'male');";
		sp->update(sql);
	}
	});

t1.join();
t2.join();
t3.join();
t4.join();

clock_t end = clock();
cout << (end - begin) << "ms" << endl;

压力测试结果:

数据总量未使用连接池花费时间使用连接池花费时间
单线程四线程单线程四线程
10009916ms4793ms1895ms792ms
500043621ms19913ms7842ms3510ms
1000087796ms37590ms14522ms7607ms

可以看到,使用连接池的提升是非常明显的!

五、完整源码下载

MySQL数据库连接池项目代码

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

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

其他浏览器正常,火狐浏览器ui-grid换行问题

ui-grid火狐浏览器兼容性问题 ui-grid表格插件问题描述解决方案 ui-grid表格插件 火狐浏览器 UI-grid 兼容性问题 其他如Edge、谷歌、360浏览器正常情况下 火狐浏览器 问题描述 如上图一和图二显示&#xff0c;UI-gird在火狐换行了&#xff1a;从图片来看&#xff1b;后面…

【车载开发系列】ASPICE标准实践---使用Drome系统保证一致性

【车载开发系列】ASPICE标准实践—使用Drome系统保证一致性 【车载开发系列】ASPICE标准实践---使用Drome系统保证一致性 【车载开发系列】ASPICE标准实践---使用Drome系统保证一致性一、一致性的目的二、ASPICE标准三、ASPICE标准实施难点四、保证一致性的实践1. 参与评审2. 可…

ES6-ES13学习笔记

目录 初识ES6 变量声明 解构赋值 对象解构 ​编辑 数组解构 ​编辑模版字符串 字符串扩展 includes() repeat() startsWith() endsWith() 数值扩展 二进制和八进制表示法 &#xff08;Number.&#xff09;isFinite()与isNaN() Number.isInteger() Math.trunc …

vue前端可以完整的显示编辑子级部门,用户管理可以为用户分配角色和部门?

用户和角色是一对多的关系用户和部门是多对多得关系<template><div class="s"><!-- 操作按钮 --><div class="shang"><el-input v-model="searchText" placeholder="请输入搜索关键词" style="width:…

上海凯泉泵业入职测评北森题库题型分析、备考题库、高分攻略

上海凯泉泵业&#xff08;集团&#xff09;有限公司是一家大型综合性泵业公司&#xff0c;专注于设计、生产、销售泵、给水设备及其控制设备。作为中国泵行业的领军企业&#xff0c;凯泉集团拥有7家企业和5个工业园区&#xff0c;总资产达到25亿元&#xff0c;生产性建筑面积35…

Python 在PDF中添加条形码、二维码

在PDF中添加条码是一个常见需求&#xff0c;特别是在需要自动化处理、跟踪或检索PDF文件时。作为一种机器可读的标识符&#xff0c;PDF中的条码可以包含各种类型的信息&#xff0c;如文档的唯一标识、版本号、日期等。以下是一篇关于如何使用Python在PDF中添加条形码或二维码的…

Linux 【进程替换】详细讲解

替换原理 进程是由PCB和内核数据结构以及进程的代码和数据形成 用 fork 创建子进程后执行的是和父进程相同的程序 ( 但有可能执行不同的代码分支 ), 子进程往往要调用一种 exec 函数来进行进程替换 ,对子进程进行替换由于原先子进程与父进程使用的是同一物理内存空间&#xff0…

前端 JavaScript 的 _ 语法是个什么鬼?

前言 我们有时候会看这样的前端代码&#xff1a; const doubled _.map(numbers, function(num) { return num * 2; });刚接触前端的童鞋可能会有点惊奇&#xff0c;不知道这个 _ 是什么语法&#xff0c;为什么这么神通广大&#xff1f; 其实 _ 是 Lodash 或 Underscore.js …

Django Project | 云笔记练习项目

文章目录 功能整体架构流程搭建平台环境子功能先创建用户表 并同步到数据库1.用户注册密码存储 -- 哈希算法唯一索引引发的重复问题 try登陆状态保持 -- 详细看用户登录状态 2. 用户登录会话状态时间 cookie用户登录状态校验 3. 网站首页4.退出登录5.笔记模块 列表页添加笔记 …

AFSim 仿真系统----脚本

概述 脚本为用户提供了一种在模拟中基于发生的事件执行复杂指令集的方式。该语言类似于 C# 和 Java&#xff0c;对于具备基本编程技能的人来说应该会很熟悉。它采用块结构&#xff0c;包含熟悉的声明、赋值和控制流语句&#xff0c;允许用户检查和操作模拟环境。 脚本本质上是由…

【Linux】sersync 实时同步

原理 rsync 是不支持实时同步的&#xff0c;通常我们借助于 inotify 这个软件来实时监控文件变化&#xff0c;一旦inotify 监控到文件变化&#xff0c;则立即调用 rsync 进行同步&#xff0c;推送到 rsync 服务端。 环境准备 步骤1&#xff1a;获取数据包 获取 sersync 的包…

UE5学习笔记12-为角色添加蹲下的动作

一、一点说明 1.蹲下使用了ACharacter类中Crouch();函数&#xff0c;函数功能是先检查是否存在运动组件&#xff0c;将bool类型的变量变为true&#xff0c;该变量代表是想要蹲下。 2.通过源码可知存在是否蹲下的bool变量bIsCrouched如图&#xff0c;如果对:1有疑问请搜索C位域 …

C++ | C++中的继承和组合:代码复用的艺术和应用

目录 一、继承&#xff1a;代码复用的艺术 1、继承概念 代码说明1&#xff1a;继承方式和访问控制 代码说明2&#xff1a;作用域与成员访问 代码说明3&#xff1a;构造函数和析构函数 2、基类和派生类对象赋值转换 派生类对象到基类对象的转换(向上转型)&#xff1a; 基…

Jmeter+Influxdb+Grafana平台监控性能测试过程(三种方式)

一、Jmeter自带插件监控 下载地址&#xff1a;Install :: JMeter-Plugins.org 安装&#xff1a;下载后文件为jmeter-plugins-manager-1.3.jar&#xff0c;将其放入jmeter安装目录下的lib/ext目录&#xff0c;然后重启jmeter&#xff0c;即可。 启动Jmeter&#xff0c;测试计…

python-opencv卷积计算代码

目录 # 尝试不同的卷积核 卷积图片如下&#xff1a; 卷积调用类如下&#xff1a; 当我们在图像上应用卷积时&#xff0c;我们在两个维度上执行卷积——水平和竖直方向。我们混合两桶信息&#xff1a;第一桶是输入的图像&#xff0c;由三个矩阵构成——RGB 三通道&#xff0c…

Cobalt—超简单下载器!!【送源码】

我们每天都在网上冲浪&#xff0c;遇到喜欢的视频、音频总想保存下来慢慢回味。很多平台并不直接提供下载功能&#xff0c;或者下载过程繁琐还伴有各种广告。之前了不起给大家介绍过不少开源的下载工具&#xff0c;如Gopeed、lux、Hitomi-Downloader&#xff0c;各有各的特色。…

机械学习—零基础学习日志(如何理解线性代数2)

零基础为了学人工智能&#xff0c;正在快乐学习&#xff0c;每天都长脑子 引言 在平面中&#xff0c;直线的定义可以理解为&#xff0c;任意缩放同一个平面向量得到所有点的集合。 所以要得到一个三维空间中的直线&#xff0c;只需要将这个向量改成三维向量即可。 什么是线…

Python | Leetcode Python题解之第337题打家劫舍III

题目&#xff1a; 题解&#xff1a; # Definition for a binary tree node. # class TreeNode: # def __init__(self, x): # self.val x # self.left None # self.right Noneclass Solution:def rob(self, root: TreeNode) -> int:def _rob…

数字图像处理(Matlab实践篇)专栏介绍

专栏导读 数字图像处理技术是计算机视觉、医学成像、遥感探测等领域的基石。Matlab&#xff0c;以其强大的数学计算能力和丰富的图像处理工具箱&#xff0c;成为学习和实践数字图像处理的理想选择。本专栏将带领读者从基础概念出发&#xff0c;逐步深入到高级技术&#xff0c;…

Redis:缓存击穿

缓存击穿 在某些 Key 属于极端热点数据&#xff0c;且并发量很大的情况下&#xff0c;如果这个 Key 过期&#xff0c;可能会在某个瞬间出现大量的并发请求同时回源&#xff0c;相当于大量的并发请求直接打到了数据库。这种情况&#xff0c;就是我们常说的缓存击穿或缓存并发问…