C++ MySQL数据库连接池

新手学了C++多线程,看了些资料练手写了C++数据库连接池小项目,自己的源码地址

关键技术点

MySQL数据库编程、单例模式、queue队列容器、C++11多线程编程、线程互斥、线程同步通信和

unique_lock、基于CAS的原子整形、智能指针shared_ptr、lambda表达式、生产者-消费者线程模型

连接池项目

为了提高MySQL数据库(基于C/S设计)的访问瓶颈,除了在服务器端增加缓存服务器缓存常用的数据之外(例如redis),还可以增加连接池,来提高MySQL Server的访问效率,在高并发情况下,大量的TCP三次握手、MySQL Server连接认证、MySQL Server关闭连接回收资源和TCP四次挥手所耗费的性能时间也是很明显的,增加连接池就是为了减少这一部分的性能损耗。

在市场上比较流行的连接池包括阿里的druid,c3p0以及apache dbcp连接池,它们对于短时间内大量的数据库增删改查操作性能的提升是很明显的,但是它们有一个共同点就是,全部由Java实现的。

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

连接池功能点介绍

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

  • 初始连接量(initSize):表示连接池事先会和MySQL Server创建initSize个数的connection连接,当应用发起MySQL访问时,不用再创建和MySQL Server新的连接,直接从连接池中获取一个可用的连接就可以,使用完成后,并不去释放connection,而是把当前connection再归还到连接池当中。

  • 最大连接量(maxSize):当并发访问MySQL Server的请求增多时,初始连接量已经不够使用了,此时会根据新的请求数量去创建更多的连接给应用去使用,但是新创建的连接数量上限是maxSize,不能无限制的创建连接,因为每一个连接都会占用一个socket资源,一般连接池和服务器程序是部署在一台主机上的,如果连接池占用过多的socket资源,那么服务器就不能接收太多的客户端请求了。当这些连接使用完成后,再次归还到连接池当中来维护。

  • 最大空闲时间(maxIdleTime):当访问MySQL的并发请求多了以后,连接池里面的连接数量会动态增加,上限是maxSize个,当这些连接用完再次归还到连接池当中。如果在指定的maxIdleTime里面,这些新增加的连接都没有被再次使用过,那么新增加的这些连接资源就要被回收掉,只需要保持初始连接量initSize个连接就可以了。

  • 连接超时时间(connectionTimeout):当MySQL的并发请求量过大,连接池中的连接数量已经到达maxSize了,而此时没有空闲的连接可供使用,那么此时应用从连接池获取连接无法成功,它通过阻塞的方式获取连接的时间如果超connectionTimeout时间,那么获取连接失败,无法访问数据库。

实现的逻辑图片

数据表的结构

我们建立一个mysql数据库的数据表来演示后面如何用C++连接数据表,并且写SQL.

先进入mysql,输入密码

mysql -u root -p

创建一个数据库名叫chat,同时创建数据表

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

如果输出OK就代表创建user好了,我们来看看数据表

desc user;
+-------+-----------------------+------+-----+---------+----------------+
| Field | Type | Null | Key | Default | Extra |
+-------+-----------------------+------+-----+---------+----------------+
| id | int | NO | PRI | NULL | auto_increment |
| name | varchar(50) | NO | | NULL | |
| age | int | NO | | NULL | |
| sex | enum('male','female') | NO | | NULL | |
+-------+-----------------------+------+-----+---------+----------------+

查看一下内容,没有

mysql> select * from user;
Empty set (0.19 sec)

到这里我们MySQL表就创建好了,我们不用管他,我们进行编写CPP连接数据库代码

连接数据库,并且执行sql语句

打开VS2019,并且创建一个控制台项目,项目结构如图

main.cpp负责执行主函数代码,Connect负责编写封装数据库的连接和sql操作,mysqlPool负责编写数据库连接池。

但我们还不急着编写代码,先导入需要的外部库,在VS上需要进行相应的头文件和库文件的配置,如下:

  • 1.右键项目 - C/C++ - 常规 - 附加包含目录,填写mysql.h头文件的路径
  • 2.右键项目 - 链接器 - 常规 - 附加库目录,填写libmysql.lib的路径
  • 3.右键项目 - 链接器 - 输入 - 附加依赖项,填写libmysql.lib库的名字
  • 4.把libmysql.dll动态链接库(Linux下后缀名是.so库)放在工程目录下

如果你没有修改过MySQL路径,一般mysql.h在你的电脑路径如下

如果你没有修改过MySQL路径,一般libmysql.lib在你的电脑路径如下

libmysql.dll文件存放在你项目文件路径下面

1.封装Mysql.h的接口成connection类

接下来封装一下mysql的数据库连接代码,不懂的看看注释,也很简单的调用Mysql.h的接口,我们在connection中额外加入创建时间函数和存活时间函数,不能让空闲的线程存活时间超过定义的最大空闲时间

connect.h的代码如下

#pragma once
#include <mysql.h>
#include <string>
#include <ctime>
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); // 刷新一下连接的起始的空闲时间点
void refreshAliveTime() { _alivetime = clock(); }
// 返回存活的时间
clock_t getAliveeTime()const { return clock() - _alivetime; }
private:
MYSQL* _conn; // 表示和MySQL Server的一条连接
clock_t _alivetime; // 记录进入空闲状态后的起始存活时间
};

connect.cpp的代码如下


#include "public.h"
#include "Connect.h"
#include <iostream>
using namespace std; Connection::Connection()
{
// 初始化数据库连接
_conn = mysql_init(nullptr);
} Connection::~Connection()
{
// 释放数据库连接资源
if (_conn != nullptr)
mysql_close(_conn);
} bool Connection::connect(string ip, unsigned short port,
string username, string password, string dbname)
{
// 连接数据库
MYSQL* p = mysql_real_connect(_conn, ip.c_str(), username.c_str(),
password.c_str(), dbname.c_str(), port, nullptr, 0);
return p != nullptr;
} bool Connection::update(string sql)
{
// 更新操作 insert、delete、update
if (mysql_query(_conn, sql.c_str()))
{
LOG(+ "更新失败:" + sql);
return false;
}
return true;
} MYSQL_RES* Connection::query(string sql)
{
// 查询操作 select
if (mysql_query(_conn, sql.c_str()))
{
LOG("查询失败:" + sql);
return nullptr;
}
return mysql_use_result(_conn);
}

在public.h中编写的代码,帮助我们输出日志和警告

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

使用这个宏,你可以在代码中的任何地方轻松输出日志信息。

我们暂时用这个main先测试下connection类

main.cpp代码

#include <iostream>
#include "Connect.h"
int main()
{
Connection conn;
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;
}

如果你的vs2019给你报安全警告,应该是sprintf的问题,你右击项目,选择属性中C++的常规中SDL检查,设置为否。

编译运行后,我们返回MySQL的界面,发现数据已经插入成功了

mysql> select * from user;
+----+-----------+-----+------+
| id | name | age | sex |
+----+-----------+-----+------+
| 1 | zhang san | 20 | male |
+----+-----------+-----+------+
1 row in set (0.02 sec)

现在我们已经成功能调用外部接口来连接Mysql数据库了,接下来我们来编写连接池.

2.编写连接池

2.1MySQL配置文件和加载配置文件

我们来编写mySqlPool的代码,因为数据库连接池只有一个,所以我们写成单例模式。同时会有多个服务端进入连接池,所以我们要添加互斥锁来避免线程之间的冲突。

我们在项目中创建一个名叫mysql.ini配置文件存储数据库连接的信息,例如数据库ip地址,用户名,密码等

mysql.ini的内容如下,如果你的用户名和密码跟里面不同,请修改

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

我们把mysqlPool.h文件中需要的函数都声明好,等会在cpp中实现。

#pragma once
#include "public.h"
#include "Connect.h"
#include <queue>
#include <mutex>
#include <string>
#include <atomic>
#include <memory>
#include <functional>
#include <condition_variable>
//因为数据库连接池子只有一个,所以我们采用单例模式
class mySqlPool {
public:
//获取连接池对象实例
static mySqlPool* getMySqlPool();
std::shared_ptr<Connection> getConnection();//从连接池获取一个可用的空闲连接
private:
mySqlPool();//构造函数私有化
bool loadConfigFile();//从配置文件中加载配置项
void produceConnectionTask(); //运行在独立的线程中,专门负责生产新连接
//扫描超过maxIdleTime时间的空闲连接,进行队列的连接回收
void scannerConnectionTask(); std::string _ip;//mysql的ip地址
std::string _dbname;//数据库的名称
unsigned short _port; //mysql端口号3306
std::string _username;//mysql用户名
std::string _password;//mysql登陆密码
int _initSize;//连接池的初始连接量
int _maxSize;//连接池的最大连接量
int _maxIdleTime;//连接池最大空闲时间
int _connectionTimeOut;//连接池获取连接的超时时间 std::queue<Connection*> _connectionQue;//存储mysql连接队列
std::mutex _queueMutex; //维护连接队列的线程安全互斥锁
std::atomic_int _connectionCnt; //记录连接所创建的connect的数量
std::condition_variable cv;//设置条件变量,用于生产者线程和消费者线程的通信
};

编写mySqlPool.cpp 中加载我们上面.ini配置文件的函数

//在mySqlPool.cpp中
//加载配置文件
bool mySqlPool::loadConfigFile()
{
FILE* pf = fopen("mysql.ini", "r");
if (pf == nullptr)
{
LOG("mysql.ini file is not exits!");
return false;
}
while (!feof(pf)) //遍历配置文件
{
char line[1024] = { 0 };
fgets(line, 1024, pf);
std::string str = line;
int idx = str.find('=', 0); //从0开始找'='符号的位置
if (idx == -1)continue;
int endidx = str.find('\n', idx);//从idx寻找'\n'的位置,也就是末尾
std::string key = str.substr(0, idx); //获取配置文件中=号左边的key
//从等号后到末尾,刚好是value的string形式
std::string value = str.substr(idx + 1, endidx - idx - 1);
if (key == "ip")
{
_ip = value;
}
else if (key == "port")
{
//字符串转换成unsigned short
_port = static_cast<unsigned short>(std::stoul(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;
}

这样我们加载配置文件就完成了

2.2编写连接池单例模式

单例模式确保数据库连接池在整个应用程序中只有一个实例。这样,所有需要数据库连接的线程或操作都可以从这个池中获取连接,而不是每次都创建新的连接。这大大减少了资源消耗和性能损耗。(如果不懂数据模式单例模式可以百度一下)

我们在.h文件中,我们先将构造函数private化,这样外部就只能通过接口来获取,我们在cpp中来编写具体的实现代码

构造方法

//mySqlPool.h
//构造方法
mySqlPool::mySqlPool()
{
if (!loadConfigFile())
{
LOG("load Config File is error!");
return;
}
//创建初始数量的连接
for (int i = 0; i < _initSize; ++i)
{
Connection* p = new Connection();
p->connect(_ip, _port, _username, _password, _dbname);
}
//启动一个新线程,作为连接的生产者
std::thread produce(std::bind(&mySqlPool::produceConnectionTask, this));
produce.detach();
//启动一个新线程,作为空闲连接超时的回收者
std::thread scanner(std::bind(&mySqlPool::scannerConnectionTask, this));
scanner.detach();
}

单例模式

//mySqlPool.h
//单例模式
mySqlPool* mySqlPool::getMySqlPool()
{
static mySqlPool pool;
return &pool;
}

现在我们已经成功的编写了单例模式,接下来我们开始获取数据库的连接。

数据库连接的线程通信

我们创建一个connect*线程队列queue来存放MySQL数据库的连接connect,同时我们还会额外创建两个线程。

一个线程是生产者,开始从Connect类中获取initSize个连接加入连接队列中准备着,当判断连接队列empty,又开始获取连接加入连接队列中 ,如果不为empty就进入阻塞状态。

生产者线程代码

//运行在独立的线程中,专门负责生产新连接
void mySqlPool::produceConnectionTask()
{
while (true)
{
std::unique_lock<std::mutex> lock(_queueMutex);
while (!_connectionQue.empty())
cv.wait(lock); //队列不为空不生产线程 //没有到上线就可以生产线程
if (_connectionCnt < _maxSize)
{
auto p = new Connection();
p->connect(_ip, _port, _username, _password, _dbname);
p->refreshAliveTime();//创建的时候刷新存活时间
_connectionQue.push(p);
++_connectionCnt;
}
cv.notify_all();
}
}

另外一个线程是消费者,如果服务端想要获取队列中的连接,消费者线程将会从队列中拿出connection来,如果队列为empty,线程会处于阻塞状态。

消费者线程代码

/从连接池获取一个可用的空闲连接
std::shared_ptr<Connection> mySqlPool::getConnection()
{
std::unique_lock<std::mutex> lock(_queueMutex);
while (_connectionQue.empty())
{
//如果超时没有获取可用的空闲连接返回空
if (std::cv_status::timeout == cv.wait_for(lock, std::chrono::milliseconds(100))) if (_connectionQue.empty())
{
LOG("get Connection error");
return nullptr;
}
}
std::shared_ptr<Connection> sp(_connectionQue.front(), [&](Connection* pcon) {
//保证只能同一时刻只能有一个线程归还连接给队列
std::unique_lock<std::mutex> lock(_queueMutex);
pcon->refreshAliveTime();//创建的时候刷新存活时间
_connectionQue.push(pcon);
});
_connectionQue.pop();
cv.notify_all();
return sp;
}

如果队列里面大于初始个数的新connection空闲时间大于最大空闲时间,我们将会回收该连接(但是不会完全释放,我们将其归还在连接池中)。

上面getConnection代码的这段就是实现了回收功能

std::shared_ptr<Connection> sp(_connectionQue.front(), [&](Connection* pcon) {
//保证只能同一时刻只能有一个线程归还连接给队列
std::unique_lock<std::mutex> lock(_queueMutex);
pcon->refreshAliveTime();//创建的时候刷新存活时间
_connectionQue.push(pcon);
});

扫描超过maxIdleTime时间的空闲连接,进行队列的连接回收

//连接线程回收
void mySqlPool::scannerConnectionTask()
{
while (true)
{
//通过sleep模拟定时效果,每_maxIdleTime检查一次
std::this_thread::sleep_for(std::chrono::seconds(_maxIdleTime)); //扫描整个队列释放多余的超时连接
std::unique_lock<std::mutex> lock(_queueMutex);
while (_connectionCnt > _initSize)
{
auto p = _connectionQue.front();
if (p->getAliveeTime() >= (_maxIdleTime * 1000))
{
_connectionQue.pop();
delete p;//这里会调用智能指针,回收到队列中
}
}
}
}

到这里,我们连接池的代码已经完成了,接下来是测试一下代码

连接池的压力测试

我们分别测试连接个数为10,100,1000时候的性能差异,创建一个test.h文件,编写测试代码

注意下面的测试可能根据不同的电脑性能,可能速度会有所差异。

普通连接

//test.h
//非线程池的连接
void testSql( int n)
{
clock_t begin = clock();
std::thread t([&n]() {
for (int i = 1; i < n; ++i)
{
Connection cnn;
char sql[1024] = { 0 };
sprintf(sql, "insert into user(name,age,sex) values('%s',%d,'%s')", "zhang san", 20, "male");
cnn.connect("127.0.0.1", 3306, "root", "123456", "chat");
cnn.update(sql);
}});
t.join();
clock_t end = clock();
std::cout << "普通连接数量为:" << n << "的sql执行时间:" << (end - begin) << "ms" << std::endl;
}

main.cpp中调用

#include <iostream>
#include "Connect.h"
#include "mySqlPool.h"
#include "test.h" int main()
{
testSql(10);//普通连接数量为 : 10的sql执行时间 : 2838ms
testSql(100);//普通连接数量为 : 100的sql执行时间: 12299
testSql(1000);//普通连接数量为 : 1000的sql执行时间 : 104528ms
return 0;
}

单线程的线程池

 //test.h
void f(int n)
{
mySqlPool* cp = mySqlPool::getMySqlPool();
for (int i = 1; i <= n; ++i)
{
std::shared_ptr<Connection> sp = cp->getConnection();
char sql[1024] = { 0 };
sprintf(sql, "insert into user(name,age,sex) values('%s',%d,'%s')", "zhang san", 20, "male");
sp->update(sql);
}
}
//测试连接池连接
void testSqlPool(int n)
{
clock_t begin = clock();
std::thread t1(f, n);
t1.join();
clock_t end = clock();
std::cout << "单线程采用数据库连接池,连接数量为:" << n << "的sql执行时间:" << (end - begin) << "ms" << std::endl;
}

main.cpp中调用

#include <iostream>
#include "Connect.h"
#include "mySqlPool.h"
#include "test.h" int main()
{
testSqlPool(10);//单线程 采用数据库连接池,连接数量为:10的sql执行时间:1745ms
testSqlPool(100);//单线程 采用数据库连接池,连接数量为:100的sql执行时间:9779ms
testSqlPool(1000);//单线程 采用数据库连接池,连接数量为:1000的sql执行时间 : 86016ms
return 0;
}

多线程的线程池

//test.h
//测试连接池连接 4线程
void testSqlPool4(int n)
{
int n2 = n / 4;
clock_t begin = clock();
std::thread t1(f, n2);
std::thread t2(f, n2);
std::thread t3(f, n2);
std::thread t4(f, n2);
t1.join();
t2.join();
t3.join();
t4.join();
clock_t end = clock();
std::cout << "四线程采用数据库连接池,连接数量为:" << n << "的sql执行时间:" << (end - begin) << "ms" << std::endl;
}

main.cpp中调用

#include <iostream>
#include "Connect.h"
#include "mySqlPool.h"
#include "test.h" int main()
{
testSqlPool4(100);//4条线程 采用数据库连接池,连接数量为:100的sql执行时间 : 3715ms
testSqlPool4(1000);//4条线程 采用数据库连接池,连接数量为:1000的sql执行时间 : 34686ms
return 0;
}

由上面测试数据可以得出,普通连接<单线程连接池<多线程连接池,连接池比普通连接还是优化很多的。

C++ mySQL数据库连接池(windows平台)的更多相关文章

  1. 一个简单的MySql数据库连接池的实现

    package cn.hc.connectionPool; import java.io.IOException; import java.io.InputStream; import java.sq ...

  2. Python实现Mysql数据库连接池

    python连接Mysql数据库: python编程中可以使用MySQLdb进行数据库的连接及诸如查询/插入/更新等操作,但是每次连接mysql数据库请求时,都是独立的去请求访问,相当浪费资源,而且访 ...

  3. mysql数据库连接池使用(三)数据库元数据信息反射数据库获取数据库信息

    1.1. mysql数据库连接池使用(三)数据库元数据信息反射数据库获取数据库信息 有时候我们想要获取到数据库的基本信息,当前程序连接的那个数据库,数据库的版本信息,数据库中有哪些表,表中都有什么字段 ...

  4. MySql数据库连接池专题

    MySql数据库连接池专题 - aspirant - 博客园https://www.cnblogs.com/aspirant/p/6747238.html

  5. python3 实现mysql数据库连接池

    首先声明一下,这篇博客进行了通过自己的代码方式,加上这篇博客,最后总结出这段代码.参考博客连接:http://blog.csdn.net/zbc1090549839/article/details/5 ...

  6. MySql数据库连接池

    1.传统链接(如下为示意图) 注意: (1).传统方式找DriverManager要连接,数目是有限的. (2).传统方式的close(),并没有将Connection重用,只是切断应用程序和数据库的 ...

  7. mysql数据库连接池使用(二)实现自己的数据库连接池

    上一个章节,我们讲了xml文件的解析框架XMLConfiguration的使用,不懂的可以参考 Apache Commons Configuration读取xml配置具体使用. 这个章节主要实现自己的 ...

  8. mysql数据库连接池使用(一)dbcp方式的配置

    Apache的数据库连接池 DBCP的常用配置说明,因为项目中用到了需要对其封装,所以必须先了解怎么配置以及各个配置字段的含义,理解的基础上开发我们自己的数据库连接池.可以参考官网dbcp官网. db ...

  9. node+mysql 数据库连接池

    1. 什么是数据库连接池? 数据库连接池是程序启动时建立足够的数据库连接,并将这些连接组成一个池,由程序动态地对池中的连接进行申请,使用和释放. 2. 使用数据库连接池原理及优点是什么? 数据库连接池 ...

  10. Tomcat中配置MySQL数据库连接池

    Web开发中与数据库的连接是必不可少的,而数据库连接池技术很好的优化了动态页与数据库的连接,相比单个连接数据库连接池节省了很大的资源.用一个通俗的比喻:如果一个人洗澡需花一桶水,那一百个人就要花一百桶 ...

随机推荐

  1. 图片三像素问题如何解决css

    一.提出问题 在浏览器中,图片有一个下间隙问题,有人也称之为图片3像素BUG 1.这并不是什么浏览器bug,而只是英文字母书写时有个基线的问题,基线决定了图片的对其方式.这才是造成浏览器中图片下间隙的 ...

  2. vim 从嫌弃到依赖(6)——插入模式

    插入模式是vim中主要用来处理输入的一种模式,在这种模式中,用户的输入的字符会显示在窗口中.该模式中的行为与在普通编辑器中输入类似.由于在该模式中输入的字符会被当做有效输入,因此该模式下涉及的到命令也 ...

  3. 释放搜索潜力:基于ES(ElasticSearch)打造高效的语义搜索系统,让信息尽在掌握

    释放搜索潜力:基于ES(ElasticSearch)打造高效的语义搜索系统,让信息尽在掌握[1.安装部署篇--简洁版],支持Linux/Windows部署安装 效果展示 PaddleNLP Pipel ...

  4. windows共享文件创建----局域网办公

    一.共享文件设置 1.选择要共享的文件夹-----右键点击属性--------在上方选项栏选择共享----然后点击"高级共享" 2.勾选"共享此文件夹"---- ...

  5. VB6的Office颜色菜单 - 开源研究系列文章

    今天把VB6里面的源码开源了( VB6各类源码开源 - 开源研究系列文章 ),这次把原来VB6里面的一个菜单控件进行介绍,需要的网友请下载安装: 1.看使用截图: 运行时截图: 设计时截图: 2.Of ...

  6. 教你用JavaScript实现表情评级

    案例介绍 欢迎来到我的小院,我是霍大侠,恭喜你今天又要进步一点点了!我们来用JavaScript编程实战案例,做一个表情评价程序.用户打星进行评价,表情会根据具体星星数量发生变化. 案例演示 点击星星 ...

  7. JS Leetcode 530. 二叉搜索树的最小绝对差 题解分析,再次了解中序遍历

    壹 ❀ 引 本题来自LeetCode 783. 二叉搜索树节点最小距离,题目描述如下: 给你一个二叉搜索树的根节点 root ,返回 树中任意两不同节点值之间的最小差值 . 示例 1: 输入:root ...

  8. NC14699 队伍配置

    题目链接 题目 题目描述 萌学姐在玩大型手游<futa go>,他现在准备进入作战环节,所以他准备安排自己的队伍. 队伍配置里,可供玩家选择的作战人物被称作"从者",玩 ...

  9. Goland 使用[临时]

    环境变量 因为module模式的引入, 多个项目可以共用同一套External Libraries, 在File->Settings->Go中, 设置GOROOT为go安装目录, 例如 / ...

  10. Laravel入坑指南(8)——控制台程序

    我们知道,php代码不仅可以用web的形式对外提供服务,同时也可以在命令行下执行. 对于原生的php来说,假设我们有一个php文件,名为Command.php,如果想要在控制台下执行这个文件,那么我们 ...