Qt应用程序启动时的一些思路:从单实例到性能优化的处理方案
程序启动时优化的价值
在桌面软件开发领域,应用程序的启动过程就像音乐的序曲,决定了用户对软件品质的第一印象。比如首次启动等待超过3秒时,会让大多数用户产生负面看法,而专业工具软件的容忍阈值甚至更低。Qt框架作为跨平台开发的利器,其启动过程的优化不仅关乎用户体验,更直接影响软件的稳定性和可维护性。
本文将从工程实践角度出发,深入剖析Qt应用程序启动阶段的五个关键技术点。
一、单实例运行的工程级解决方案
1.1 行业标准实现方案对比
- 共享内存方案(QSharedMemory)
- 本地Socket方案(QLocalServer)
- 文件锁方案(QFileLock)
- 进程枚举法(QProcess)
1.2 混合型单实例防护体系
采用自己写一个检测程序来监听是否单实例。
class InstanceGuard : public QObject {//使用Qt的共享内存QSharedMemory m_sharedMem;QLocalServer m_localServer;
public:explicit InstanceGuard(const QString& appKey) {// 双重检测机制m_sharedMem.setKey(appKey + "_mem");if(m_sharedMem.attach()) {m_sharedMem.detach();return;}m_localServer.listen(appKey + "_sock");connect(&m_localServer, &QLocalServer::newConnection, [=]{// 激活现有实例的处理逻辑});}
};
1.3 单实例模型类
也可以自己设计一个类,继承自QApplication,使用本地服务的形式,完成单实例的功能,然后让主程序继承字这个类。
#include "singleapplication.h"#include <QLocalServer>
#include <QLocalSocket>
#include <QFile>
#include <QFileInfo>
#include <QTextStream>SingleApplication::SingleApplication(int &argc, char **argv): QApplication(argc, argv),m_bRunning(false)
{QCoreApplication::setOrganizationName("SmartSafe");QCoreApplication::setApplicationName("TreadCheck313");QString strServerName = QCoreApplication::organizationName() + QCoreApplication::applicationName();//strServerName = QFileInfo(QCoreApplication::applicationFilePath()).fileName();QLocalSocket socket;socket.connectToServer(strServerName);if (socket.waitForConnected(500)){QTextStream stream(&socket);QStringList args = QCoreApplication::arguments();QString strArg = (args.count() > 1) ? args.last() : "";stream << strArg;stream.flush();qDebug() << "Have already connected to server.";socket.waitForBytesWritten();m_bRunning = true;}else{// 如果不能连接到服务器,则创建一个m_pServer = new QLocalServer(this);connect(m_pServer, SIGNAL(newConnection()), this, SLOT(newLocalConnection()));if (m_pServer->listen(strServerName)){// 放置程序崩溃,残留进程服务,直接移除if ((m_pServer->serverError() == QAbstractSocket::AddressInUseError) && QFile::exists(m_pServer->serverName())){QFile::remove(m_pServer->serverName());m_pServer->listen(strServerName);}}}
}SingleApplication::~SingleApplication()
{//ShutDownLog4QtByCoding(); //exec()执行完成后,才关闭logger
}void SingleApplication::newLocalConnection()
{QLocalSocket *pSocket = m_pServer->nextPendingConnection();if (pSocket != NULL){pSocket->waitForReadyRead(1000);QTextStream in(pSocket);QString strValue;in >> strValue;qDebug() << QString("The value is: %1").arg(strValue);delete pSocket;pSocket = NULL;}
}bool SingleApplication::isRunning()
{return m_bRunning;
}int main(int argc, char *argv[])
{//不用原本的QApplication ,改为使用自定义的类//QApplication a(argc, argv);SingleApplication a(argc, argv);MainWindow w;w.show();return a.exec();;}
1.4 使用共享内存的方式实现单实例
我们可以使用共享内存的方式,实现一个简单的单实例,保证主程序的开启只有一份。
int main(int argc, char *argv[])
{QApplication a(argc, argv);// 创建一个唯一的共享内存段QSharedMemory sharedMemory("MyApp");if (!sharedMemory.create(1)) {// 共享内存已存在,说明应用程序已经在运行QMessageBox::information(nullptr, QStringLiteral("提示"), QStringLiteral("应用程序已经在运行!"));return 0;}MainWindow w;w.show();int result = a.exec();return result;}
二、心跳监护系统的架构设计
2.1 监护系统结构说明
- 主进程(Main Process):应用程序的核心业务模块,负责向心跳服务注册自身状态。
- 监护进程(Guardian Process):独立于主进程的守护程序,持续发送心跳信号。
- 心跳服务(Heartbeat Service):中央协调者,监听所有节点状态,执行异常处理。
2.3 Qt实现的核心模块
服务端实现
class HeartbeatServer : public QObject {Q_OBJECT
public:explicit HeartbeatServer(quint16 port, QObject* parent=nullptr): QObject(parent), m_port(port) {m_udpSocket.bind(m_port);connect(&m_udpSocket, &QUdpSocket::readyRead,this, &HeartbeatServer::processDatagrams);m_checkTimer.start(1000); // 1秒检查间隔connect(&m_checkTimer, &QTimer::timeout,this, &HeartbeatServer::checkNodes);}private slots:void processDatagrams() {while(m_udpSocket.hasPendingDatagrams()) {QByteArray datagram;datagram.resize(m_udpSocket.pendingDatagramSize());QHostAddress sender;quint16 senderPort;m_udpSocket.readDatagram(datagram.data(), datagram.size(),&sender, &senderPort);if(validatePacket(datagram)) {m_nodes[sender.toString()] = QDateTime::currentDateTime();}}}void checkNodes() {auto now = QDateTime::currentDateTime();auto it = m_nodes.begin();while(it != m_nodes.end()) {if(it->secsTo(now) > TIMEOUT_THRESHOLD) {emit nodeTimeout(it.key());it = m_nodes.erase(it);} else {++it;}}}signals:void nodeTimeout(const QString& address);private:QUdpSocket m_udpSocket;QTimer m_checkTimer;QMap<QString, QDateTime> m_nodes;quint16 m_port;
};
客户端实现
class HeartbeatClient : public QObject {Q_OBJECT
public:explicit HeartbeatClient(const QHostAddress& serverAddr, quint16 port,QObject* parent=nullptr): QObject(parent), m_serverAddr(serverAddr), m_port(port) {m_timer.start(HEARTBEAT_INTERVAL);connect(&m_timer, &QTimer::timeout,this, &HeartbeatClient::sendHeartbeat);}private slots:void sendHeartbeat() {HeartbeatPacket packet;packet.timestamp = QDateTime::currentSecsSinceEpoch();packet.processId = QCoreApplication::applicationPid();packet.statusFlags = calculateStatus();packet.crc = calculateCRC(packet);QByteArray data(reinterpret_cast<char*>(&packet), sizeof(packet));m_udpSocket.writeDatagram(data, m_serverAddr, m_port);}private:QUdpSocket m_udpSocket;QTimer m_timer;QHostAddress m_serverAddr;quint16 m_port;
};
三、配置类的加载
3.1 分级配置体系设计
- 系统级配置(/etc)
- 用户级配置(~/.config)
- 临时配置(内存存储)
- 命令行覆盖配置
如果时单个应用程序,往往一些简单的配置文件居多,即便这样,也建议做好配置类的管理和加载时的统筹规划设计。
3.2 分类加载
有时候一些配置类的文件或者字段,不是需要软件开启时就用的,这些配置,就可以延后加载,软件开启只加载跟开启有关的,必要的配置。
3.3 读写的控制
对于一些配置需要可读可写的情况,特别需要注意多线程情况下冲突,可以在处理配置文件或配置数据的读写上加锁,避免数据异常。比如:
QString ConfigControl::getWarnValue()
{//读写要加锁控制QMutexLocker locker(m_pMutex);return m_cfgParam->m_strWarningValue;
}
int ConfigControl::setWarnValue(const QString& str)
{//读写要加锁控制QMutexLocker locker(m_pMutex);m_cfgParam->m_strWarningValue = str;if(!Utils::util_setting::writeInit(QString(CONFIG_PATH_TARGET), QString("AppConfig"), QString("WarningValue"), str))return 1;return 0;
}
四、日志系统的及时介入
4.1 传统日志方案的瓶颈
虽然一些微小程序可以自己随便写个txt充当日志,但你就需要考虑以下这些问题:
- 同步写操作的性能损耗
- 多线程竞争问题
- 日志丢失风险
- 磁盘IO阻塞
4.2 log4qt日志库的使用
这里由于我最近都是做Qt的开发,习惯使用log4qt日志库。
这里点击获取log4qt库,包含源码,dll库和初始化配置代码
//这是为调用log4qt库写的初始化配置,
#include "log4qt/helper/log4qt_init_helper_by_coding.h"
#include "log4qt/include/log4qt/logger.h" // 每个使用log4qt的类都需要包含此头文件// 在类的cpp文件中,使用此静态方法声明logger(此方法比较通用)
// 第二个参数写类名字,因此,输出的log条目中包含其对应的类名
LOG4QT_DECLARE_STATIC_LOGGER(logger, main)void setupLog()
{QString strLogPath = QCoreApplication::applicationDirPath() + "/Log";qDebug() << "logs path:" << strLogPath;SetupLog4QtByCodingWithLogSavingDirAbsPath(strLogPath);logger()->info() << __FUNCTION__ << ", logs path: " << strLogPath;
}int main(int argc, char *argv[])
{QApplication a(argc, argv);//设置日志setupLog();MainWindow w;w.show();int result = a.exec();// 关闭logger日志ShutDownLog4QtByCoding(); return result;}
五、启动画面的深度优化
有时候,我们不是简单的一个小程序,有可能我们得根据用户明确的目标:他们希望启动画面不仅美观,还要高效,减少启动时间,提升用户体验。可能的应用场景包括工业软件、医疗系统或其他需要快速启动的专业工具。这时候,就得在界面启动上下功夫,想一些解决方案了。
比如一些实现方法:多线程加载资源、使用OpenGL加速、进度条的设计等。
5.1 渐进式加载策略
比如我们设定程序开启后一些流程如下:
- start:显示静态LOGO;
- fork:预加载核心字体;
- fork again:初始化OpenGL上下文;
- fork again :加载基础样式表;
- end fork:显示进度条(30%);
- fork :异步加载业务模块;
- fork again:建立数据库连接;
- fork again :初始化网络组件;
- end fork
- 显示进度条(70%);
- 完成剩余初始化;
- 进入主界面(100%);
比如通过线程池来加载一些资源:
class ResourceLoader : public QRunnable {QString m_resourcePath;QAtomicInt* m_progress;
public:ResourceLoader(const QString& path, QAtomicInt* progress) : m_resourcePath(path), m_progress(progress) {}void run() override {QImage img(m_resourcePath);QMetaObject::invokeMethod(qApp, [this, img](){QPixmap::fromImage(img); // 传递到主线程m_progress->fetchAndAddRelaxed(10);}, Qt::QueuedConnection);}
};// 启动加载任务
QThreadPool pool;
QAtomicInt progress(0);
pool.start(new ResourceLoader(":/images/bg.jpg", &progress));
pool.start(new ResourceLoader(":/icons/set1.png", &progress));
pool.start(new ResourceLoader(":/icons/set2.png", &progress));
5.2 硬件加速渲染
可以考虑以下几种方式:
5.2.1 OpenGL优化
class GLWidget : public QOpenGLWidget {
protected:void initializeGL() override {initializeOpenGLFunctions();glEnable(GL_BLEND);glBlendFunc(GL_SRC_ALPHA, GL_ONE_MINUS_SRC_ALPHA);}void paintGL() override {glClear(GL_COLOR_BUFFER_BIT);// 使用VBO绘制预加载的几何图形drawCachedGeometry();}private:GLuint m_vbo = 0;
};
5.2.2 多缓冲技术实现
class DoubleBufferSplash : public QSplashScreen {QPixmap m_frontBuffer;QPixmap m_backBuffer;QMutex m_mutex;public:void updateDisplay() {QMutexLocker locker(&m_mutex);qSwap(m_frontBuffer, m_backBuffer);setPixmap(m_frontBuffer);}void asyncRender() {QFuture<void> future = QtConcurrent::run([this](){QPainter painter(&m_backBuffer);renderComplexFrame(painter); // 后台渲染updateDisplay();});}
};
5.2.3 字体预处理优化
// 启动时预生成字体纹理
void preloadFontTextures() {QOpenGLTexture* texture = new QOpenGLTexture(QOpenGLTexture::Target2D);texture->setFormat(QOpenGLTexture::RGBA8_UNorm);QFont font("Arial", 12);QFontMetrics fm(font);QSize atlasSize(1024, 1024);QImage image(atlasSize, QImage::Format_RGBA8888);QPainter painter(&image);painter.setFont(font);// 生成常用字符集纹理for(int i=32; i<127; ++i) {QRect rect = fm.boundingRect(QChar(i));painter.drawText(rect, Qt::AlignLeft, QChar(i));}texture->setData(image);texture->generateMipMaps();
}
5.3 用户体验增强设计
5.3.1 初始化的进度进行智能预估
class ProgressEstimator {QVector<qint64> m_timeSamples;int m_currentStep = 0;public:void recordStepTime(qint64 ms) {m_timeSamples.append(ms);}int estimateRemaining() {if(m_timeSamples.isEmpty()) return 0;// 指数平滑预测double alpha = 0.2;double estimate = m_timeSamples.first();for(qint64 t : m_timeSamples) {estimate = alpha * t + (1 - alpha) * estimate;}return estimate * (TOTAL_STEPS - m_currentStep);}
};
5.3.2 友好型互动设计
void SafeSplashScreen::handleInitError(ErrorCode code) {switch(code) {case GPU_INIT_FAILED:showMessage(tr("正在切换软件渲染模式..."));disableHardwareAcceleration();break;case RESOURCE_LOAD_FAILED:showMessage(tr("使用备用资源继续加载..."));loadFallbackResources();break;case LICENSE_INVALID:QTimer::singleShot(2000, []{ QApplication::exit(EXIT_LICENSE); });break;}
}
5.4 性能优化指标体系
5.4.1 核心性能指标
一些性能指标参考
5.4.2 性能优化技巧
这里可以根据一些性能指标,使用一些方法进行相对的优化,比如:
预编译QML:使用qmlcachegen生成二进制缓存;
延迟加载策略:使用是写方法策略延迟加载部分暂不使用的模块(同上文);
资源压缩优化:比如对一些资源图片进行压缩处理,以优化加载和渲染时间;
最终,我们通过对程序开启时的一些操作,以及优化启动过程的"最后一公里"打磨,使应用程序在最大限度地满足需求。
相关文章:

Qt应用程序启动时的一些思路:从单实例到性能优化的处理方案
程序启动时优化的价值 在桌面软件开发领域,应用程序的启动过程就像音乐的序曲,决定了用户对软件品质的第一印象。比如首次启动等待超过3秒时,会让大多数用户产生负面看法,而专业工具软件的容忍阈值甚至更低。Qt框架作为跨平台开发…...
【前端三剑客】Ajax技术实现前端开发
目录 一、原生AJAX 1.1AJAX 简介 1.2XML 简介 1.3AJAX 的特点 1.3.1AJAX 的优点 1.3.2AJAX 的缺点 1.4AJAX 的使用 1.4.1核心对象 1.4.2使用步骤 1.4.3解决IE 缓存问题 1.4.4AJAX 请求状态 二、jQuery 中的AJAX 2.1 get 请求 2.2 post 请求 三、跨域 3.1同源策略…...

一文详解Spring Boot如何配置日志
一、写在前面 对于日志文件,相信大家都并不陌生,通过在关键位置打印相关的日志,有利于快速跟踪和定位软件系统运行中存在的问题。 在之前的 Java 实现日志记录的文章中,我们介绍了能实现日志记录的主流框架有 Log4j、Log4j2、Lo…...

Springboot | 如何上传文件
文章目录 1. 核心上传逻辑:FileUploadController2. 使文件系统中的文件可通过 HTTP 访问:WebConfig3. 安全性配置:WebSecurityConfig4. 前端实现(这里用的是Angular) 在许多应用程序开发中,我们经常需要实现…...
axios结合AbortController取消文件上传
<template><div><input type"file" multiple change"handleFileUpload" /><button click"cancelUpload" :disabled"!isUploading">取消上传</button><div>总进度:{{ totalProgress }}…...

spring中的@Async注解详解
一、核心功能与作用 Async 是Spring框架提供的异步方法执行注解,用于将方法标记为异步任务,使其在独立线程中执行,从而提升应用的响应速度和吞吐量。其主要作用包括: 非阻塞调用:主线程调用被标记方法后立即返回&…...

MyBatis 报错:Column count doesn‘t match value count at row 1 详解与解决
本文适用于使用 MyBatis MySQL 开发中出现 “Column count doesnt match value count at row 1” 报错的朋友,尤其是在批量插入或更新数据时,遇到 XML 映射文件中 insert 标签报错的问题。 一、遇到的问题: 二、错误原因分析 列数与值数量不…...
第四天——贪心算法——种花
1. 题目 有一个花坛,其中0 表示该位置是空的,可以种花。1 表示该位置已经有花,不能种花。 规则:新种的花不能种在相邻的位置(即如果某个位置已经种了花,它的左右两个相邻位置不能再种花)。给定…...

【人工智能】自然语言编程革命:腾讯云CodeBuddy实战5步搭建客户管理系统,效率飙升90%
CodeBuddy 导读一、产品介绍1.1 **什么是腾讯云代码助手?**1.2 插件安装1.2.1 IDE版本要求1.2.2 注意事项1.2.4 插件安装1.2.4.1 环境安装1.2.4.2 安装腾讯云AI代码助手** 1.2.5 功能介绍1.2.5.1 Craft(智能代码生成)1.2.5.2 Chat(…...

麦肯锡110页PPT企业组织效能提升调研与诊断分析指南
“战略清晰、团队拼命、资源充足,但业绩就是卡在瓶颈期上不去……”这是许多中国企业面临的真实困境。表面看似健康的企业,往往隐藏着“组织亚健康”问题——跨部门扯皮、人才流失、决策迟缓、市场反应滞后……麦肯锡最新研究揭示:组织健康度…...

【MySQL】第二弹——MySQL表的增删改查(CRUD)初阶
文章目录 🎓一. CRUD🎓二. 新增(Create)🎓三. 查询(Rertieve)📖1. 全列查询📖2. 指定列查询📖3. 查询带有表达式📖4. 起别名查询(as )📖 5. 去重查询(distinct)📖6. 排序…...
内存、磁盘、CPU区别,Hadoop/Spark与哪个联系密切
1. 内存、磁盘、CPU的区别和作用 1.1 内存(Memory) 作用: 内存是计算机的短期存储器,用于存储正在运行的程序和数据。它的访问速度非常快,比磁盘快几个数量级。在分布式计算中,内存用于缓存中间结果、存储…...
hz2新建Keyword页面
新建一个single-keywords.php即可,需要筛选项再建taxonomy-knowledge-category.php 参考:https://www.tkwlkj.com/customize-wordpress-category-pages.html WordPress中使用了ACF创建了自定义产品分类products,现在想实现自定义产品分类下的…...

离散制造企业WMS+MES+QMS+条码管理系统高保真原型全解析
在离散型制造企业的生产过程中,库存管理混乱、生产进度不透明、质检流程繁琐等问题常常成为制约企业发展的瓶颈。为了帮助企业实现全流程数字化管控,我们精心打造了一款基于离散型制造企业(涵盖单件生产、批量生产、混合生产模式)…...
【并发编程基石】CAS无锁算法详解:原理、实现与应用场景
一、什么是CAS? CAS(Compare-And-Swap) 是现代并发编程的核心算法之一,它通过处理器指令级的原子操作实现线程安全,无需传统锁机制。其核心逻辑可以用一个公式表示: CAS(V, E, N) {if (V E) { // 比较当…...
(自用)Java学习-5.8(总结,springboot)
一、MySQL 数据库 表关系 一对一、一对多、多对多关系设计外键约束与级联操作 DML 操作 INSERT INTO table VALUES(...) DELETE FROM table WHERE... UPDATE table SET colval WHERE...DQL 查询 基础查询:SELECT * FROM table WHERE...聚合函数:COUNT()…...
GOOSE 协议中MAC配置
在 GOOSE(Generic Object Oriented Substation Event)协议中,主站(Publisher)发送的 MAC 地址不需要与从站(Listener)的 MAC 地址一致,其通信机制与 MAC 地址的匹配逻辑取决于 GOOSE…...
机器学习之决策树与决策森林:机器学习中的强大工具
机器学习之决策树与决策森林:机器学习中的强大工具 摘要:本文深入探讨决策树和决策森林在机器学习中的应用优势及其适用场景。决策树凭借其易于配置、原生处理多种数据类型、鲁棒性及可解释性等特点,在小数据集和表格数据处理方面表现卓越。…...
【Redis】谈谈Redis的设计
Redis(Remote Dictionary Service)是一个高性能的内存键值数据库,其设计核心是速度、简单性和灵活性。以下从架构、数据结构、持久化、网络模型等方面解析 Redis 的设计实现原理: 1. 核心设计思想 内存优先:数据主要存…...
【C++】流(Stream)详解:标准流、文件流和字符串流
【C】流(Stream)详解:标准流、文件流和字符串流 在C编程中,流(Stream)是一个非常重要的概念,它为我们提供了统一的数据输入输出接口。本文将详细介绍C中的三种主要流类型:标准流、文件流和字符串流。 一、标准流(Standard Strea…...

基于 Spring Boot 瑞吉外卖系统开发(十三)
基于 Spring Boot 瑞吉外卖系统开发(十三) 查询套餐 在查询套餐信息时包含套餐的分类名,分类名称在category表中,因此这里需要进行两表关联查询。 自定义SQL如下: select s.* ,c.name as category_name from setmeal…...

POSE识别 神经网络
Pose 识别模型介绍 Pose 识别是计算机视觉领域的一个重要研究方向,其目标是从图像或视频中检测出人体的关键点位置,从而估计出人体的姿态。这项技术在许多领域都有广泛的应用,如动作捕捉、人机交互、体育分析、安防监控等。 Pose 识别模型的…...
CSS3 基础知识、原理及与CSS的区别
CSS3 基础知识、原理及与CSS的区别 CSS3 基础知识 CSS3 是 Cascading Style Sheets 的第3个版本,是CSS技术的升级版本,于1999年开始制订,2001年5月23日W3C完成了CSS3的工作草案。 CSS3 主要模块 选择器:更强大的元素选择方式盒…...
电能质量扰动信号信号通过hilbert变换得到瞬时频率
利用Hilbert变换从电能质量扰动信号中提取瞬时频率、瞬时幅值、Hilbert谱和边际谱的详细步骤及MATLAB代码实现。该流程适用于电压暂降、暂升、谐波、闪变等扰动分析。 1. Hilbert变换与特征提取流程 1.1 基本步骤 信号预处理:滤波去噪(如小波去噪&…...
Linux工作台文件操作命令全流程解析(高级篇之awk精讲)
全文目录 1 工具介绍2 核心优势3 命令格式3.1 命令格式说明3.2 组成部分详解3.2.1 选项3.2.2 模式3.2.3 动作3.2.4 输入文件 4 使用说明4.1 常用示例4.2 awk 编程解析4.2.1 基础说明4.2.2 编程进阶 4.3 温馨提示 5 内置变量6 参考文献 写在前面 前面一篇《Linux工作台文件操作命…...

力扣119题:杨辉三角II(滚动数组)
小学生一枚,自学信奥中,没参加培训机构,所以命名不规范、代码不优美是在所难免的,欢迎指正。 标签: 杨辉三角、滚动数组 语言: C 题目: 给定一个非负索引 rowIndex,返回「杨辉三角…...
c++:算法(Algorithms)
目录 常用 STL 算法 1️⃣ std::sort(排序) 2️⃣ std::find(查找等于某值的元素) 3️⃣ std::count(统计出现次数) 4️⃣ std::next(获取迭代器的下一个位置) 5️⃣ .erase(…...

大疆无人机(全系列,包括mini)拉流至电脑,实现直播
参考视频 【保姆级教程】大疆无人机rtmp推流直播教程_哔哩哔哩_bilibili VLC使用教程: VLC工具使用指南-CSDN博客 目录 实现效果: 电脑端 编辑 编辑 无人机端 VLC拉流 分析 实现效果: (实验机型:大疆mini4kRC-N2遥控器、大…...

uniapp-商城-54-后台 新增商品(页面布局)
后台页面中还存在商品信息的添加和修改等。接下来我们逐步进行分析和展开。包含页面布局和数据库逻辑等等。 1、整体效果 样式效果如下,依然采用了表单形式来完成和商家信息差不多,但在商品属性上多做了一些弹窗等界面,样式和功能点表多。 …...
深入浅出MySQL 8.0:新特性与最佳实践
MySQL作为开源关系型数据库的佼佼者,近年来持续更新迭代,尤其是在8.0版本中引入了一系列令人兴奋的新特性。本文将介绍一些MySQL 8.0的关键新功能,并提供最佳实践,旨在帮助开发人员和DBA更好地利用这一强大的数据库管理系统。 一…...