ARTICLE DETAIL

资讯详情

深耕郑州网站建设与运营推广的一线实战洞察。

MySQL技术体系与性能调优实战:从安装、索引到主从复制

MySQL技术体系与性能调优实战:从安装、索引到主从复制 MySQL 技术体系这个东西我最早是真没当回事以为“装个库、写几条SQL、不行就加个索引”完事了。后来线上一条排序慢查询把连接池打满业务整整停了十分钟我才意识到MySQL 的部署安装、存储引擎、索引结构、锁与事务、日志与备份、主从复制每一块都是大坑而性能调优的本质就是把这一整套体系里最薄弱的那一环找出来补上。这篇指南不打算做文档翻译而是把我实际踩过的坑、验证过的方法、能直接抄作业的参数组合整理出来尽量把 MySQL 技术体系和实战调优讲透适合正在学 MySQL、准备接手数据库运维、或者面试前想系统过一遍的朋友。1. MySQL 技术体系全景这不是一个黑盒1.1 一条SQL语句的完整旅程把 MySQL 拆开看它不只是一个存数据的文件而是一套分层明显的服务端程序。我习惯用下面这张示意图去跟新人讲整体结构虽然简单但比啃官方文档高效得多客户端 | v ------------------------------- | 连接管理 / 认证 / 权限 | ------------------------------- | 查询解析器词法、语法分析 | | 查询优化器生成执行计划 | | 执行器调用引擎接口 | ------------------------------- | 存储引擎层InnoDB / MyISAM | ------------------------------- | 物理文件ibd / redo / undo | | / binlog / 系统表空间 |一次普通的 SELECT 查询先要经过客户端和服务器之间的连接器完成用户名密码校验、权限检查然后再进入服务层。服务层里解析器会检查SQL语法把一条语句拆成可识别的结构优化器负责选择索引、决定连接顺序、生成执行计划执行器拿到计划后再一层层调用存储引擎接口。你会发现真正读磁盘、读内存、加锁、记录日志的其实是存储引擎而 SQL 的解析、优化、权限控制都在上面的 Server 层完成。这套分层设计的最大好处是解耦。你可以在不改动上层逻辑的情况下把 MyISAM 换成 InnoDB也可以针对不同业务表选择不同引擎。很多问题排查也依赖这种分层思想SQL 慢先看是不是 optimizer 选错了索引连接失败先看连接器和权限磁盘读写异常再看引擎层的物理文件状态。1.2 InnoDB 为何能成为默认存储引擎早期 MySQL 的默认引擎是 MyISAM那时候很多生产库连事务都没有一台机器崩了恢复基本靠备份。后来 InnoDB 成为绝对主力默认引擎也换成了它。为什么因为 InnoDB 提供了一整套让数据更可靠、并发更高的能力。简单对比一下对比项InnoDBMyISAM事务支持支持 ACID不支持锁粒度行锁 表锁仅表锁MVCC支持不支持数据存储聚簇索引数据与主键索引绑定索引与数据分离崩溃恢复redo log doublewrite依赖修复工具缓存数据页缓存 索引缓存仅索引缓存外键支持不支持我最看重的是 InnoDB 的崩溃恢复能力。它把每一次数据页的修改通过 redo log 先落盘就算数据库突然断电重启后也可以根据 redo log 把数据恢复到“崩溃前一刻”的状态。而 MyISAM 一旦索引文件或数据文件损坏可能连表都打不开得用 myisamchk 慢慢修。选错引擎的教训我见过太多有人拿 MyISAM 存订单业务量一上来整表锁死接口全部卡住。现在的原则其实很简单业务数据表一律 InnoDB除非你明确知道自己在做什么。1.3 三种日志与崩溃恢复日志是 MySQL 技术体系里最容易被忽略、又最影响可靠性和主从复制的部分。很多新手分不清 redo log、undo log、binlog我按“谁产生的、记录什么、用来干嘛”做了张对照表日志所属层级记录内容核心作用redo logInnoDB物理页变更崩溃恢复保证事务持久性undo logInnoDB行记录变更前的版本事务回滚、MVCC 快照读binlogServer 层逻辑 SQL 或行事件主从复制、时间点恢复redo log 是 InnoDB 自己维护的记录的是“数据页第几页第几条记录改成什么”采用循环写的方式不需要无限增长。每次事务提交时默认会把这个事务产生的 redo 刷到磁盘这就是参数 innodb_flush_log_at_trx_commit1 的语义。binlog 则是 Server 层的逻辑日志记录的是“哪条语句把哪一行改成了什么”主从复制和基于 binlog 的恢复全靠它。很多复制延迟、数据不一致问题本质上是三种日志配合出了问题。比如主库 binlog 格式用了 STATEMENT一条带 limit 的 update 落到从库执行就可能产生不同结果再比如没有开启 binlog 的生产库一旦误删数据基本只能找快照时间点恢复无从谈起。所以我的建议是从搭建第一天就开 binlog且格式用 ROW。2. 部署与安装从裸机到生产环境2.1 Linux RPM 方式部署 MySQL 5.7/8.0生产环境最常用的安装方式之一就是 RPM。以 CentOS/Rocky 这类系统为例千万别一上来就 yum install mysql-server那装的很可能是系统自带兼容包或者 MariaDB版本和路径都不对。正确流程大致是这样先确认系统里没有自带数据库有 MariaDB 就卸掉然后下载官方 MySQL 社区版 RPM 包或者配置官方 Yum 源。# 检查是否已有 mysql/mariadb rpm -qa | grep -i mysql rpm -qa | grep -i mariadb # 下载官方 rpm 源包以 8.0 为例 wget https://dev.mysql.com/get/mysql80-community-release-el7-7.noarch.rpm rpm -ivh mysql80-community-release-el7-7.noarch.rpm # 安装服务端和客户端 yum install -y mysql-community-server mysql-community-client # 初始化并查看临时密码 mysqld --initialize --usermysql grep temporary password /var/log/mysqld.log # 启动并登录 systemctl start mysqld mysql -uroot -p注意 8.0 初始化之后 root 账号默认带临时密码且密码策略默认是中等的第一次改密码要满足大小写、数字、特殊字符要求。如果只想本地测试可以顺手把密码策略调低set global validate_password.policyLOW;但生产环境不建议。关于 MySQL 5.7社区版最后的版本其实是 5.7.44之后官方不再对 5.7 系列做维护更新。所以网上搜索时看到 5.7.43、5.7.44 不必困惑5.7.44 就是收官版。如果是从 5.7 升级 8.0RPM 包目录和默认参数差异很大升级前一定要备份。2.2 Docker 部署 MySQL 与镜像拉取失败排查Docker 部署 MySQL 最大的优势是环境隔离、数据目录挂在宿主机换机器方便。常规命令docker run -d --name mysql8 \ -p 3306:3306 \ -e MYSQL_ROOT_PASSWORD123456 \ -v /data/mysql:/var/lib/mysql \ -v /etc/mysql/conf.d:/etc/mysql/conf.d \ mysql:8.0挂在宿主机的数据目录非常重要否则容器一删数据全没。第一次启动时容器会自动执行初始化脚本创建 root 用户和默认数据库之后重启不会再初始化。但 Docker 拉取镜像和启动容器时经常翻车。比如网上很多人遇到的failed to decode referrers index: invalid这多半是 Docker Desktop 或 Docker Engine 版本太旧对镜像仓库新接口协议支持不完整。解决办法很简单升级 Docker Desktop 到最新版然后重新 pull 一次。如果一直失败可以先docker pull mysql:8.0.36指定具体版本避免 latest 标签解析问题。容器启动失败更常见的是端口占用、数据卷权限不对、或者宿主机的 selinux 拦截。遇到这种问题先别急着删容器执行docker logs mysql8看最后几行。如果是Cant create/write to file /var/lib/mysql/多半是挂载目录权限不够执行chown -R 999:999 /data/mysql再重启。这也是为什么我不建议一上来用--privileged粗暴绕过的原因。2.3 离线环境与 ARM 架构部署有些内网环境不能访问外网没法用 Yum 源这时候需要在一台能联网的机器上把 RPM 包和依赖一起导出然后拷进内网。离线安装的坑在于依赖关系MySQL 的 RPM 包依赖于libaio、perl等系统包只拷 mysql 开头的几个 rpm 不够启动时会报error while loading shared libraries: libaio.so.1。建议用yumdownloader --resolve mysql-community-server把依赖全部拉到同一个目录再到目标机上rpm -Uvh *.rpm。ARM 架构的服务器近几年越来越多。MySQL 官方镜像本身提供 ARM64 版本直接用 MySQL 8.0 官方 Docker 镜像一般没问题。但如果是基于 Oracle Linux 的镜像旧版本可能在部分 ARM 环境上兼容性差可以改用 MariaDB 或者用 MySQL 官方提供的 ARM 二进制包。再一个容易忽略的点是客户端工具链Windows 上连接 ARM 服务器时ODBC 驱动必须选对应架构。MySQL Connector/ODBC 8.0 装不上经常是因为系统缺少 Microsoft Visual C 2015-2022 Redistributable x64先装运行库再装驱动顺序反了一堆莫名其妙的问题。2.4 安装后的初始化与安全基线数据库装好能连上只算完成了 30%。我接手过的系统里安全问题和参数问题都集中在这个环节。推荐安装后立刻执行几件事第一运行安全初始化脚本删掉匿名账号和空密码账号第二root 账号只在本地使用业务账号权限最小化第三确认字符集和排序规则。-- 创建业务专用账号避免 root 上应用 CREATE USER app_user192.168.%.% IDENTIFIED BY StrongPass_123; GRANT SELECT, INSERT, UPDATE, DELETE ON mydb.* TO app_user192.168.%.%; -- 修改字段默认值为 0 ALTER TABLE t MODIFY status INT NOT NULL DEFAULT 0;热搜里有人问“mysql 设置默认值为 0”这个在项目里常有用户状态字段新建时默认 0。要注意如果表里已有大量数据直接加 DEFAULT 0 不会反填历史数据只影响新插入的行真想把存量值刷成 0还得单独执行 UPDATE。安全方面MySQL 8.0 默认的认证插件是 caching_sha2_password老客户端连接会报 SSL/认证相关错误我在后面故障排查一节里单独讲。3. 性能调优实战先定位后动刀3.1 打开慢查询日志用 EXPLAIN 定位真凶性能调优最忌讳一上来就改参数。我见过有人把 max_connections 调得很大结果数据库连接更多慢查询更慢最终直接把实例打挂。正确做法是先给问题“定位”Step 1 永远是打开慢查询日志。-- 临时开启慢查询日志 SET GLOBAL slow_query_log ON; SET GLOBAL long_query_time 1; SET GLOBAL log_output TABLE; -- 查询最近慢查询 SELECT * FROM mysql.slow_log ORDER BY start_time DESC LIMIT 10;日志开了之后把执行时间超过 1 秒的 SQL 揪出来对每条慢 SQL 执行 EXPLAIN。EXPLAIN 的结果里我最关注四列type、key、rows、Extra。type 从全表扫描 ALL 到索引点查 const基本能判断这条 SQL 缺不缺索引rows 是估算扫描行数数值越大通常越慢Extra 出现 Using filesort 或 Using temporary 基本意味着 sort buffer 和临时表参与进来了这种 SQL 值得重构。举个真实例子一张 500 万行的订单表按下单时间排序分页SELECT * FROM orders WHERE status 1 ORDER BY create_time DESC LIMIT 20;刚开始 type 是 ALLExtra 是 Using filesort页面打开要 3 秒。加了一个(status, create_time)联合索引之后type 变成 range扫描行数从 500 万降到几千查询 30ms 返回。调优不是靠玄学EXPLAIN 就是数据库在告诉你问题在哪。3.2 内存与并发核心参数调优定位到问题后才是参数调整环节。MySQL 参数很多但真正影响绝大部分业务的关键参数没几个。首先是 InnoDB 缓冲池innodb_buffer_pool_size。这个参数决定了 InnoDB 把多少热数据页缓存在内存里是数据库最大的内存消费者。我的经验值纯 MySQL 实例按物理内存的 50%~70% 设置如果机器上还跑 Redis、Nginx就降到 40%~50%。在线调整示例-- 查看当前值 SHOW VARIABLES LIKE innodb_buffer_pool_size; -- 8.0 支持在线调整 SET GLOBAL innodb_buffer_pool_size 8589934592;其次是日志相关参数。innodb_flush_log_at_trx_commit 有三个值1 表示每次事务提交都刷 redo 到磁盘安全性最高、性能最慢0 每秒刷一次最快但可能丢 1 秒数据2 每次提交写操作系统缓存但每秒刷盘。金融类业务必须 1日志系统和允许少量丢失的业务可考虑 2既要性能又不想丢太多数据时2 是折中选择。连接数和并发参数也很容易踩坑。max_connections 不是越大越好连接数一旦超过数据库能同时处理的线程数请求会全部堆积。常用检查思路跑show status like Threads_connected;如果接近 max_connections 且 CPU 已经很高不是调大连接数而是该查慢 SQL 和连接池配置。3.3 索引设计与 SQL 写法优化索引是 MySQL 性能调优的核心中的核心。只要查询条件带 where、需要排序、需要去重都应该先想想有没有合适的索引可用。但索引也不是越多越好每个索引都占用写入和存储成本。联合索引遵循最左前缀原则索引(a, b, c)能用到 a、ab、abc但直接查 b 或 c 用不上这个索引。所以写 SQL 时where 条件里等式列的顺序、以及 order by 的列最好都能和联合索引建立方向一致。我调优时经常干一件事把慢 SQL 里的 where 列和 order by 列提出来设计一个多列索引让 where 筛选和排序都能走索引。SQL 写法方面热搜里“mysql 的 or 能去重吗”这个问题很有代表性。答案是or 本身不会去重它只是连接多个条件如果两个条件查出同一行结果集里还是会重复。去重用DISTINCT或GROUP BYSELECT DISTINCT user_id FROM orders WHERE status 0 OR pay_type 1;注意 or 有时会让优化器放弃索引尤其是不同列 or 条件。遇到这类情况可以改写为 union all 去重或者用 in 替代等值 or效果往往更好。排序相关的另一个坑是字符集排序规则同一张表字段用 utf8mb4_general_ci 和 utf8mb4_unicode_ci排序结果可能不一样。如果业务要求严格的大小写敏感排序干脆把字段 collation 设成 *_bin。3.4 存储过程在实际业务中的合理用法存储过程现在确实不像十年前那么流行很多公司甚至禁用因为业务逻辑放在数据库里不好调试、不好扩展。但在批处理、数据迁移、报表统计等固定流程里存储过程仍然很香。比如批量初始化几百万行记录的状态DELIMITER // CREATE PROCEDURE batch_update_status() BEGIN DECLARE v_i INT DEFAULT 0; WHILE v_i 100 DO UPDATE orders SET status 1 WHERE id BETWEEN v_i * 10000 1 AND (v_i 1) * 10000; SET v_i v_i 1; END WHILE; END // DELIMITER ; CALL batch_update_status();这里有个关键经验大批量 update 别一次性更新全表不然会锁大量行、撑爆 undo log甚至会阻塞其他事务。我习惯按主键分段处理每段几万行每段之间稍微停顿这样对主库的影响可控很多。存储过程里也尽量别拼动态 SQL因为一个错误不容易定位还会带来注入风险。如果业务逻辑频繁变化还是建议挪到应用层存储过程只留给固定的后台任务。4. 事务、锁与并发控制高性能的第一道门槛4.1 MySQL 锁体系速查并发性能出问题十有八九是锁没玩明白。InnoDB 的锁类型不少我把实际工作中最重要的整理成一张速查表锁类型锁粒度作用场景常见问题全局锁整个实例全库备份阻塞所有写操作表锁整张表DDL、MyISAM写并发直接挂起元数据锁表结构DDL/DML 并发长事务阻塞 DDL意向锁表级行锁和表锁协调通常无感知行锁单行普通 DML行竞争间隙锁区间RR 隔离级别防止幻读锁范围扩大临键锁索引区间RR 默认容易死锁自增锁表级自增列插入批量插入性能下降我的经验是大多数“锁表”问题不是真的 LOCK TABLES 锁而是行锁没释放某个事务 update 了一批行却一直不提交其他事务要改同一行就只能一直等待。排查时别光看表要看事务。4.2 隔离级别与 MVCCInnoDB 是通过 MVCC 和锁共同实现事务隔离的。四个隔离级别分别解决不同的一致性问题隔离级别脏读不可重复读幻读READ UNCOMMITTED可能可能可能READ COMMITTED否可能可能REPEATABLE READ否否可能但 InnoDB 间隙锁解决SERIALIZABLE否否否MySQL 默认是 REPEATABLE READ但 InnoDB 用间隙锁把幻读挡掉了大半所以实际使用和整体一致性体验接近快照读。MVCC 的原理可以理解为每行记录在 undo log 里保存历史版本链事务读数据时通过版本链看到“自己启动那一刻的快照”这样读操作不会被写操作阻塞。这也是为什么我在前面反复强调 undo log 不能暴涨。有一次我处理一个跑了 5 小时没提交的事务undo 把磁盘塞满所有 delete/update 全部卡死就是 MVCC 快照版本被长事务一直拽住无法清理。这种场景下的解决方案很明确先查 longest transaction然后和应用确认能否提交或 kill 掉。4.3 锁表、锁等待与死锁排查实录排查锁问题有现成的表和命令。MySQL 8.0 的 performance_schema 和 sys 库能直接告诉你是谁在等、谁在锁-- 当前事务列表 SELECT * FROM information_schema.innodb_trx\G -- 锁等待关系 SELECT * FROM sys.innodb_lock_waits\G -- 主从库上的锁等待数量 SHOW STATUS LIKE Innodb_row_lock_waits;从 innodb_trx 里能看到 trx_started、trx_state、trx_query如果某个事务开始时间很早、状态是 RUNNING且 SQL 迟迟没返回基本就是它没提交导致后续事务排队。确定之后联系业务确认必要时KILL thread_id。死锁和锁等待不一样。锁等待是“一个等一个”死锁是“你等我、我等你”InnoDB 默认开启死锁检测检测到会自动回滚代价小的事务。真遇到报错Deadlock found when trying to get lock不要一上来就调参先去 MySQL 错误日志里看死锁详情看涉及哪两张表、哪些行、什么 SQL。我处理过一个高频死锁两个事务分别按不同顺序更新 a、b 两张表互相持锁等待。修复方法很简单所有事务统一按 id 从小到大顺序更新死锁自然消失。5. 故障排查实录这些坑我替你们踩过了5.1 mysql SSL 连接错误怎么解决MySQL 8.0 一个高频报错是客户端连不上像SSL connection error: unknown error或者Access denied for user ... using password: YES。导致这个问题的原因通常不是密码错而是 8.0 默认认证插件 caching_sha2_password 在非 SSL 连接下需要额外的 RSA 公钥交换老客户端或某些驱动不支持。临时排查手段是显式跳过 SSLmysql -uroot -p -h127.0.0.1 --ssl-modeDISABLED如果能连上说明问题确实在 SSL/认证流程。正式解法有三条一是升级客户端驱动到支持 caching_sha2_password 和 SSL 的版本二是连接串上配置useSSLtrueverifyServerCertificatefalse三是把账号改回 mysql_native_password。第三种方式在 MySQL 8.0 里还可用但不推荐因为 mysql_native_password 也逐步被官方标记为废弃。我自己的习惯是能升级驱动就升级驱动生产环境保持默认认证插件不动不要为了省事关掉 SSL那等于把数据库密码裸奔在内网上。5.2 Windows 下 net start mysql 服务无法启动Windows 上安装 MySQL 后net start mysql报“服务无法启动”很常见。这个报错信息本身没有任何价值真正的问题在 error log。默认日志文件一般在 MySQL 数据目录下名为hostname.err打开后往往能看到以下几类日志关键字真正原因处理方式Cant create/write to file数据目录无权限以管理员运行或重新设置目录权限unknown variablemy.ini 配置项写错修正配置删掉不认识的参数Cannot find file服务关联的 mysqld.exe 路径不对重新配置服务指定完整路径InnoDB: Unable to lock数据目录被占用确认没有残留 mysqld 进程这类问题的通用解法先重新初始化一份干净的 data 目录命令是mysqld --initialize-insecure空密码 root再用mysqld --install MySQL --defaults-fileC:\...\my.ini重建服务。最坑的是手写 my.ini 时把datadir路径写到了不存在的位置MySQL 不会告诉你“路径不存在”只会在启动时默默失败看错误日志才能发现。5.3 升级报错 Invalid MySQL server upgrade升级 MySQL 8.0 时日志里出现[ERROR] [MY-014060] [Server] Invalid MySQL server upgrade:这类提示说明数据目录版本和当前二进制版本不匹配或者升级流程没走完。我遇到过的典型场景用 8.0.35 的 mysqld 启动了一个之前由 8.0.20 初始化过的数据目录MySQL 检测到需要升级但升级没有自动跑成功进程直接拒绝启动。处理思路是这样备份数据目录后确认二进制版本是 8.0 且高于旧版本然后显式触发升级# 停库后执行 mysqld --upgradeFORCE --usermysqlMySQL 8.0.16 之后官方已经不建议单独执行 mysql_upgrade 命令而是用上面的 mysqld 启动参数。升级完成后注意检查系统表中是否有版本记录比如SELECT * FROM mysql.user能否正常查询。这类错误最怕硬着头皮反复启动可能导致系统表进一步损坏风险很大生产环境升级一定先做全量备份。5.4 Xtrabackup 备份与 GTID 主从同步真正生产级的高可用方案里Percona Xtrabackup 是备份 MySQL 最常用的工具尤其面对大实例时物理备份比 mysqldump 快好几个量级。备份和恢复的套路如下# 全量备份 xtrabackup --backup --target-dir/backup/full \ --userbackup_user --passwordxxx --host127.0.0.1 # prepare使备份可恢复 xtrabackup --prepare --target-dir/backup/full # 恢复到新实例 xtrabackup --copy-back --target-dir/backup/full主从同步现在基本都用 GTID比传统 filepos 方式更省心因为同步位点由事务 ID 自动管理不怕找错 binlog 文件名。启用 GTID 需要在主从的 my.cnf 里都配置server-id 1001 log-bin mysql-bin gtid_mode ON enforce_gtid_consistency ON从库恢复好备份后直接 change master 指向主库CHANGE MASTER TO MASTER_HOST192.168.1.20, MASTER_USERrepl_user, MASTER_PASSWORDxxx, MASTER_AUTO_POSITION1; START SLAVE; SHOW SLAVE STATUS\G看Replica_IO_Running和Replica_SQL_Running都是 Yes这两个线程才表示同步正常。GTID 方案最常用的排错技巧就是对比主从的gtid_executed集合如果从库多了一段主库没有的 GTID基本就是手工在从库执行过写入这种不一致会越积越深必须尽早解决。5.5 高频问题速查表最后把我在社区和群聊里经常看到的问题整理成一张速查表按场景定位很快场景报错现象常见原因处理命令/方案Sqoop 抽数连不上 MySQLJDBC 驱动未放入 lib、host 不一致下载 mysql-connector-java 并置于 sqoop/lib命令行执行 SQL一直等待无返回连接超时参数过小、有锁等待调大 connect_timeout / net_read_timeout查 innodb_trxMySQL 排序结果和 App 预期不一致collation 不是预期规则改字段 collation 为 utf8mb4_bin修改表结构执行卡住元数据锁被长事务阻塞查 innodb_trxkill 长事务UPDATE 误操作数据被大批量改错忘写 where利用备份binlog 做时间点恢复Docker 映射端口容器起不来3306 被占用netstat -ano 查占用进程换端口Yum 安装版本不对装了系统自带 mariadb先卸载再装官方 repo这张表里每条都是真实生产环境的反馈不是理论推导。我尤其想强调最后一行很多人贪方便选 Yum 源可能装了 MariaDB 或旧版本后面字符集、认证方式全对不上排查成本比安装成本高得多。6. 写在最后的个人体会这些内容写到最后我自己最大的体会是MySQL 的性能问题很少是单个参数造成的绝大多数是表结构、索引、SQL 和执行计划共同作用的结果。调优的第一步永远是先看慢查询日志第二步是 EXPLAIN第三步才谈参数。如果只能留一条建议我大概会先说把 innodb_buffer_pool_size 设到物理内存的 50%~70%然后老老实实学会看执行计划剩下的都是在这个基础上查漏补缺。另外还想分享一个细节不要觉得生产环境“加一台从库”就能解决所有慢查询。从库也要跑同样的 SQL索引没建好一样会卡主从延迟还会引出一堆强一致性问题。真正稳妥的做法是把技术体系里的每个环节都过一遍从存储引擎到日志、从锁到索引、从备份到同步每块都知道它是怎么工作的、会在哪里坏。遇到问题的时候这些东西串起来答案往往自己就浮出来了。
返回列表