跟MySQL打交道这些年,我一直觉得性能问题不是某一个点的玄学,而是一条完整的链路:表结构设计、索引是否合理、SQL写得怎么样、InnoDB参数有没有跟上业务形态、并发场景下事务和锁有没有互相拖后腿,再到部署环境本身是不是就埋了雷。很多项目初期跑得飞快,数据量一旦上了几百万行,慢查询、锁等待、连接超时全都冒出来。这篇总结是我把线上压测和多个生产故障的排查经验重新梳理了一遍,围绕索引优化、SQL改写、参数调优、表设计、事务锁机制和部署排错六个方向展开,适合后端开发、初级DBA和技术负责人直接对照自己的项目排查。
1. 索引优化:性能的第一道坎
1.1 联合索引的最左前缀法则,别等到索引失效才后悔
我见过太多慢查询,根因就一句话:索引建了,但查询条件根本没按索引的结构来。联合索引的匹配规则是最左前缀,比如我建了一个联合索引idx_user_status_time(user_id, status, create_time),查询能用到这个索引的前提条件是从最左边开始连续匹配字段。WHERE user_id = 100 AND status = 1能走索引,WHERE status = 1 AND create_time > '2024-01-01'就走不了,因为跳过了最左边的user_id。
这里有个容易被忽略的细节:一旦查询条件里出现范围判断,范围列右边的字段就无法继续使用联合索引。比如WHERE user_id = 100 AND status > 1 AND create_time > '2024-01-01',status用了范围之后,create_time基本只能靠回表过滤,索引只能帮到status这一层。所以设计联合索引时,一定要把等值判断的字段放在前面,范围查询的字段放在后面。
我在实际调优时会用EXPLAIN里的key_len来验证联合索引到底用到了几列。key_len的计算逻辑不复杂:utf8mb4字符集下,varchar(100)每个字符最多占4字节,再加2字节长度标记,如果字段允许为NULL还要再加1字节。比如user_id如果是int,key_len就是4;status是tinyint,就是1;create_time是datetime,就是5。看EXPLAIN输出的key_len是4还是5,就能确认是不是只走到了第一列。
注意:隐式类型转换是索引杀手。
WHERE user_id = '100'如果user_id是整型,MySQL 会尝试把字符串转成数字,索引照样能走;但反过来WHERE phone = 13800138000而phone是varchar,MySQL 会把传入的数值转成字符串再比较,大概率直接放弃索引。字符集不一致的关联字段也容易出同样的问题,做表设计时尽量统一。
1.2 覆盖索引与回表:为什么“别老用 select *”
InnoDB 的主键索引和二级索引结构不一样。主键索引的叶子节点存的是整行数据,二级索引的叶子节点存的是主键值和被索引的列。如果你通过二级索引查询,但SELECT的列不在索引里,MySQL 就得拿着主键去主键索引里再查一次,这个过程叫回表。回表次数一多,查询自然慢。
覆盖索引的意思是查询需要的所有列都包含在同一个二级索引中,MySQL 可以直接从索引里拿数据,不用回表。在EXPLAIN的Extra列看到Using index,就说明走的是覆盖索引。我压测过一个订单流水查询,原SQL是SELECT * FROM order_flow WHERE user_id = ? ORDER BY create_time DESC,在(user_id, create_time)索引下仍需回表拿全列数据,耗时1200ms左右。改成只查业务需要的列SELECT id, order_no, amount, create_time,并把索引调整成(user_id, create_time, order_no, amount),耗时降到8ms。排序字段如果也在索引里,连filesort都能省掉。
这里有一个延伸优化:MySQL 5.6 以上支持索引下推(Index Condition Pushdown)。以前二级索引查询时,必须回表之后才能对非索引列做过滤;启用ICP后,存储引擎层就能先过滤掉不符合条件的记录,减少回表次数。大多数场景下默认开启,但如果你排查时发现Extra里出现Using index condition,不用慌,这说明ICP在工作,回表量已经处于比较小的状态。
注意:覆盖索引不是建得越宽越好。每个索引都会占用额外存储空间,而且写入时要同步更新,索引列太多会明显拉低写性能。我一般建议覆盖索引只覆盖“高频且列数可控”的查询。
1.3 索引数量取舍与写放大控制
索引不是装饰品,每个索引都会带来写放大。一张表如果建了七八个索引,插入一行数据可能要同时更新七八棵B+树,线上写入量大时,binlog、redo log和刷盘压力都会上来。我在朋友的一个订单系统里见过一张表建了11个索引,一秒才写入几百行,但磁盘IO已经很高,后来砍到5个索引,写入性能提升非常明显。
建索引前先问自己几个问题:这个字段的区分度高不高?是不是高频查询条件?这个索引是不是冗余的?比如(a, b)联合索引存在时,单独的(a)索引就是冗余的,因为左前缀已经覆盖了a的查询场景。性别、状态这类取值很少的字段单独建索引基本没有意义,优化器算一下区分度就知道全表扫更划算。
更新频繁的字段要慎重加索引,尤其是那种每次UPDATE都会改变索引键值的字段。索引页会不停分裂和合并,产生大量随机IO。如果实在需要按这个字段查询,可以考虑把字段拆到单独的表里,用冗余ID关联,把热点更新和查询逻辑错开。
2. SQL 改写与执行计划分析
2.1 慢查询日志:定位问题SQL的第一手段
做性能优化,第一步永远是找到慢SQL,而不是凭感觉调参数。MySQL 的慢查询日志是必备的基础设施,建议直接打开。配置写在my.cnf或my.ini的[mysqld]段下:
[mysqld] slow_query_log = 1 slow_query_log_file = /var/log/mysql/slow.log long_query_time = 1 log_queries_not_using_indexes = 1long_query_time我习惯设置为1秒,这样能把阈值压到1秒内,再小的漏网SQL也能被记录。log_queries_not_using_indexes这个参数容易被忽略,打开之后,任何没走索引的查询都会被记进慢日志,哪怕单条执行时间不长。它能帮你发现很多潜在的索引失效问题。
慢日志是文本文件,行数多了以后直接用grep和sort不好分析。我常用mysqldumpslow自带工具做聚合,比如mysqldumpslow -s t -t 20 /var/log/mysql/slow.log能按执行时间倒序取前20条,快速看出哪类SQL是重点。如果项目有权限装第三方工具,pt-query-digest的分析报告会更直观,能按总耗时、平均耗时、扫描行数做统计。没有这些工具的时候,直接在高峰期执行SHOW FULL PROCESSLIST,连续抓几次,也能看到当前在跑的慢查询长什么样。
注意:慢日志文件会持续增长,长期不轮转可能撑爆磁盘。生产环境建议配合
logrotate做切割,或者定期手动归档。同时,long_query_time不要设成0,否则所有查询都进慢日志,分析噪声巨大。
2.2 EXPLAIN 核心字段实战解读
拿到慢SQL后,用EXPLAIN看执行计划是基本功。我重点看这几个字段:type、rows、filtered、Extra。
type的优劣顺序一般是:const>eq_ref>ref>range>index>ALL。走全表扫描的ALL是最需要警惕的。range表示用索引做了范围扫描,在分页和区间查询里很常见,可以接受。index虽然也是索引扫描,但如果它遍历了整个索引树的叶子节点,性能并不比全表扫好多少,经常出现在ORDER BY和GROUP BY无法利用索引顺序的情况里。
rows是优化器预估的需要扫描的行数,filtered是过滤比例。我遇到过一个典型问题:两张表关联查询,A表10万行,B表1000万行,SQL写成了FROM a JOIN b ON a.bid = b.id,优化器选错了驱动表,导致小表驱动大表变成了大表扫描。后来改成EXPLAIN观察,发现关联字段虽然有索引,但一方字符集不同导致隐式转换,索引失效。统一字符集之后,执行计划才算正常。记住一个原则:查询总要先从小结果集出发,被驱动的表必须有高效索引。
Extra里出现Using temporary和Using filesort是两个危险信号。Using temporary说明GROUP BY或DISTINCT操作建了临时表,数据量大时会落盘;Using filesort说明排序无法利用索引,需要在内存或磁盘排序。看到这两项,优先检查排序字段、分组字段是否在索引里,以及查询条件是否满足最左前缀。
EXPLAIN SELECT s.name, c.course_name, sc.score FROM score sc JOIN student s ON sc.student_id = s.id JOIN course c ON sc.course_id = c.id WHERE sc.course_id = 10 ORDER BY sc.score DESC LIMIT 20;这条语句在score表上有(course_id, score)联合索引时,type会走到ref,ORDER BY可以避免filesort,整个联查性能会好很多。索引设计和SQL最终是互相成全的,单独看哪个都没意义。
2.3 排序、limit与深分页优化
ORDER BY想走索引,有两个条件:排序字段必须在索引中,且排序方向一致。比如索引是(user_id, create_time),查询条件是WHERE user_id = ? ORDER BY create_time DESC,因为是等值条件命中user_id,create_time在索引里已经有序,排序就能直接复用索引顺序,省掉filesort。但如果查询条件变成WHERE user_id IN (1,2,3) ORDER BY create_time,多个等值条件的组合会导致区间跳跃,排序就未必能走索引。
深分页是我在业务系统里见到的重灾区。LIMIT 100000, 20看起来只是取20条,但MySQL要先把前100000行全部扫描并跳过,扫描代价非常高。我优化过一个列表接口,数据量200万行,用户翻到第5000页时接口超时,SQL是SELECT * FROM orders ORDER BY id LIMIT 100000, 20。改成延迟关联后:
SELECT t.* FROM orders t INNER JOIN ( SELECT id FROM orders ORDER BY id LIMIT 100000, 20 ) tmp ON t.id = tmp.id;内层只查主键,扫描代价大幅降低,外层再用主键回表取完整数据。另一个更常见的做法是游标式分页,前端把上一页最后一条记录的id传回来:
SELECT * FROM orders WHERE id > 100000 ORDER BY id LIMIT 20;这种方式每页只扫描20行,性能极其稳定。前提是业务允许按主键顺序翻页,并且排序字段稳定。
注意:分页不能依赖
LIMIT加随机顺序。没有任何ORDER BY的LIMIT,返回顺序在MySQL内部是不保证稳定的,用户翻页时会出现数据重复或丢失。
3. 配置参数与高并发架构调优
3.1 InnoDB缓冲池、日志与关键内存参数
MySQL 的默认配置通常是偏爱保守的,适合机器配置不确定的场景,但上了生产就必须按实际业务调整。我最先调整的是innodb_buffer_pool_size,这个参数决定 InnoDB 缓存数据和索引的内存大小。一般建议设置为物理内存的60%~70%,但不能无脑照搬,还要看服务器是否只跑MySQL。比如一台32G内存的机器专门跑MySQL,设20G左右比较合理;如果机器上还部署了应用服务,可能要降到40%左右,避免内存不足触发系统交换。
MySQL 8.0 的innodb_buffer_pool_size支持运行时动态调整,可以先用小值启动,再逐步调大并观察内存压力:
SET GLOBAL innodb_buffer_pool_size = 8 * 1024 * 1024 * 1024;innodb_flush_log_at_trx_commit这个参数直接影响事务提交时的刷盘方式。默认值是1,每个事务提交都会把redo log刷到磁盘,安全性最高,但压力也最大。设置为0或2时,性能会明显提升,事务提交时只写日志缓冲或操作系统缓存,崩溃时可能会丢失最后1秒左右的事务。金融、支付类系统必须用1,日志类、报表类业务可以折中选2。
我常用的一个基线配置如下:
[mysqld] innodb_buffer_pool_size = 8G innodb_log_file_size = 512M innodb_log_buffer_size = 16M innodb_flush_log_at_trx_commit = 1 sync_binlog = 1 max_connections = 500innodb_log_file_size也不能忽略。redo log太小会导致刷盘过于频繁,性能打折;太大则崩溃恢复时间变长。5.7 版本以后常见设置为512M,具体要看写入量。
3.2 连接数、线程池与高并发连接管理
每次连接都会占用线程和内存资源,连接数设得太大并不会让性能变好,反而可能拖垮系统。max_connections默认值151,这个数值对很多高并发场景是不够的,但也不是设成5000就万事大吉。连接数再多,CPU和磁盘才是瓶颈。我在生产环境一般先看SHOW STATUS LIKE 'Threads%'里的Threads_connected,观察实际连接峰值,再留出30%~50%的余量设置max_connections。
高并发下最怕出现连接堆积。应用连接池配置和数据库连接数必须联动,我用 HikariCP 时习惯设置maximumPoolSize在20~50之间,minimumIdle在5~10之间,不要动不动就开到几百。连接数暴涨时,先SHOW PROCESSLIST看看是应用没释放连接,还是有慢SQL占着连接不松手。遇到Too many connections报错,第一步是登录不上数据库的,可以在命令行通过mysql -u root -p --max_connections=1000这类方式先抢一个连接进去,杀掉一堆Sleep状态的连接,再排查根因。
wait_timeout和interactive_timeout控制非交互连接和交互连接的等待时长。我见过把wait_timeout设成默认8小时的情况,半夜高峰期几千个空闲连接全部挂在数据库上,直接把连接吃满。业务系统建议配合连接池的空闲回收机制,设置在600秒上下比较合理。
3.3 主从复制、读写分离与分库分表:什么时候才该上
单机性能打满之后,很多团队会直接上主从复制加读写分离。主从复制的原理其实不复杂:主库把变更写到binlog,从库的IO线程拉取binlog并写入中继日志,SQL线程再串行回放中继日志。这个链路听起来简单,真正的坑在于复制延迟。主库写入压力一大,从库回放来不及,就会导致刚写入的数据在从库查不到。
减少延迟的几个常用手段:启用并行复制,MySQL 5.7 后可以配置slave_parallel_workers,让多个线程并行应用日志;binlog_format设置为ROW能减少部分主从不一致问题;核心业务读写分离时把强一致性的读请求强制走主库。如果延迟依然很高,就要考虑是不是从库的磁盘和CPU跟不上主库。
分库分表要更谨慎。我始终觉得,分库分表是最后的手段,不是一开始就该上的架构。单表数据量过了千万级、索引优化和冷热分离都做过了,性能依然不达标,才值得考虑。分片键的选择很关键,选了错误的键会导致数据倾斜,比如订单表按用户ID分片,某个大客户的数据量会把单个分片打爆。使用sharding-jdbc或Mycat这类中间件时,也需要提前考虑跨分片查询、全局主键、分布式事务这些复杂问题,不建议在业务早期就背上这套复杂度。
4. 表设计与字段类型优化
4.1 字段设计原则:小而简单才是王道
表设计对性能的影响是前置性的,等上线后再改结构成本就高了。我的核心原则是:能用小类型绝不用大类型,能定长就定长,能非空就非空。
整型字段按需选择,别动不动就是bigint。布尔值用tinyint(1),状态码用smallint或tinyint,主键用bigint还是int要看实际量级。字符类型上,char是定长,适合长度稳定的字段,比如订单号、手机号,注意char(11)定长存的手机号在InnoDB里索引效率更高;varchar是变长,适合用户名、备注这类长度不确定的内容。有一个常见反例:给varchar(255)的字段建索引,排序和内存开销都比varchar(64)大得多,但业务根本不需要那么宽。
时间字段同样有讲究。timestamp占用4字节,但有2038年的存储上限,而且受时区设置影响;datetime占用8字节,范围更大。新系统我一般直接用datetime,省得未来还要做迁移。价格字段必须用decimal,float和double有精度问题,账算不清迟早出事。
一个很容易踩的坑是用 NULL 来表示“没有值”。NULL 在索引中处理更复杂,查询条件写起来也别扭,聚合函数还会忽略 NULL。建议用明确的默认值替代,比如订单金额默认0,状态默认0。
4.2 反范式设计与冷热数据分离
数据库设计的教科书里都在讲范式,但实际高性能系统反而要做一定程度的反范式设计。JOIN 是很贵的,尤其是跨大表的 JOIN,需要额外的内存、临时表和扫描代价。我经常把一些查询频繁的冗余字段直接放进业务表里,比如订单表里冗余存储商品名称和当前单价,下单时快照下来,查询时不用再去关联商品表。
反范式设计要付出的代价是数据一致性维护。商品改名后,历史订单里的冗余商品名不会自动更新,这可能正是业务需要的快照语义。如果你的业务要求同步更新,那就必须由应用层在同一个事务里一起更新,复杂度随之上升。所以反范式设计要挑场景,适合读多写少、对历史快照有需求的模块。
冷热数据分离是我在数据量增长后最常用的手段。一张订单表跑了两年,积累了大量历史订单,但这些历史订单几乎不会再被查询。可以按月或按年归档到历史表,业务主表只保留最近3~6个月的数据,查询性能立竿见影。归档之后记得用mysqldump备份,必要时重建主表索引,把因为碎片化导致的性能劣化也一并解决。
4.3 实例:学生课程成绩表的索引与结构设计
热搜里有一个“学生课程成绩信息实体表设计mysql”,我顺手把这个经典模型拆一下。成绩系统最常见的需求是:查某个学生的全部成绩、查某门课程的排名、查某门课程的平均分。如果只有一张大宽表,字段堆在一起,JOIN 虽少但冗余很大。更合理的做法是拆成三张表:student、course、score。
CREATE TABLE `student` ( `id` int NOT NULL AUTO_INCREMENT, `student_no` varchar(32) NOT NULL, `name` varchar(32) NOT NULL, `class_name` varchar(32) NOT NULL DEFAULT '', PRIMARY KEY (`id`), UNIQUE KEY `uk_student_no` (`student_no`) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4; CREATE TABLE `course` ( `id` int NOT NULL AUTO_INCREMENT, `course_no` varchar(32) NOT NULL, `course_name` varchar(64) NOT NULL, PRIMARY KEY (`id`), UNIQUE KEY `uk_course_no` (`course_no`) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4; CREATE TABLE `score` ( `id` bigint NOT NULL AUTO_INCREMENT, `student_id` int NOT NULL, `course_id` int NOT NULL, `score` decimal(5,1) NOT NULL, `exam_time` datetime NOT NULL, PRIMARY KEY (`id`), UNIQUE KEY `uk_student_course` (`student_id`, `course_id`), KEY `idx_course_score` (`course_id`, `score`) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;score表用自增主键id加联合唯一键(student_id, course_id),既保证同一学生同一课程只保留一条主成绩记录,又方便按学生查询。要查某门课的排行榜时,走idx_course_score,MySQL 可以直接用索引排序出成绩从高到低的顺序,无需额外排序。这个设计就是覆盖索引、联合索引和业务模型结合的一个典型例子。
5. 事务与锁:并发控制实战
5.1 事务隔离级别选型与MVCC
事务隔离级别决定了一个事务能看到其他事务的哪些修改。MySQL InnoDB 默认隔离级别是REPEATABLE READ,也就是可重复读。它通过 MVCC 机制让普通查询读的是快照,同一事务内多次查询结果一致。快照读不会加锁,这也是为什么 InnoDB 在高并发下还能保持不错性能的原因。
隔离级别和并发问题的对应关系如下:
| 隔离级别 | 脏读 | 不可重复读 | 幻读 |
|---|---|---|---|
| READ UNCOMMITTED | 可能 | 可能 | 可能 |
| READ COMMITTED | 不可能 | 可能 | 可能 |
| REPEATABLE READ | 不可能 | 不可能 | 可能(InnoDB通过间隙锁基本解决) |
| SERIALIZABLE | 不可能 | 不可能 | 不可能 |
MVCC 的核心是每行记录隐藏了两个版本字段:事务ID和回滚指针。更新操作不是直接覆盖旧值,而是生成新版本,旧版本留在undo log里。读操作通过read view判断当前事务能看到哪个版本,从而实现不加锁的隔离。
实际项目中,READ COMMITTED在高并发下往往有更低的开销,因为间隙锁的使用会减少很多。但有两点要注意:RC 级别下不可重复读依然存在,跨多次查询的业务必须自己做好控制;binlog_format=ROW时RC和RR都安全,但如果是STATEMENT格式,RC 会产生主从不一致,必须配合RR使用。
注意:事务里跑了大量查询、长事务长时间不提交,MVCC 的
undo log会不断膨胀,严重时造成“版本链过长”,拖慢所有查询和清理线程。写代码一定要控制事务边界,查询类的操作不要塞进写事务里。
5.2 行锁、间隙锁与死锁排查
InnoDB 的锁可以分为共享锁(S锁)和排他锁(X锁),读操作默认不加锁,SELECT ... FOR UPDATE和UPDATE、DELETE才会加X锁。意向锁是表级别的标识,用来快速判断表里是否存在行锁,避免每次加表锁都要遍历所有行。
更常见的问题是间隙锁。RR 隔离级别下,InnoDB 使用Next-Key Lock(记录锁+间隙锁)防止幻读。它锁住的不仅是匹配的记录,还包括记录之间的间隙。两个事务在同一个间隙上插入新记录时,就会互相阻塞,这个现象在业务日志里通常表现为Lock wait timeout exceeded。
我之前遇到过一个死锁案例:事务A先更新id=10的记录,再更新id=20的记录;事务B先更新id=20,再更新id=10。如果两个事务并发执行,A拿到10的锁等20,B拿到20的锁等10,死锁就出现了。排查时执行SHOW ENGINE INNODB STATUS,在LATEST DETECTED DEADLOCK部分能看到两个事务各自持有和等待的锁。修复方案很简单:规范所有业务的更新顺序,都按id从小到大执行,死锁自然消失。
还有一个容易被忽略的问题:更新条件如果没走索引,InnoDB 会锁定全表记录。比如UPDATE t SET status=1 WHERE status=0且status没有索引,哪怕只是想把status=0的几行更新掉,实际锁范围超大,几乎所有并发更新都会被堵住。这种问题的解法不是改锁行为,而是必须给status加上合适的索引,让更新操作精准命中少量行。
5.3 高并发写入的减锁策略:以库存扣减为例
库存扣减是高并发写入的经典场景。最容易出问题的写法是:先SELECT查库存,判断足够,再UPDATE扣减。两个事务同时读到的库存都是10,各自扣1,最终库存变成9而不是8,这就是典型的超卖。
标准做法是直接条件更新,锁的粒度最小:
UPDATE stock SET num = num - 1 WHERE id = ? AND num > 0;这条SQL利用行锁保证同一时间只有一个事务能成功更新同一行,num > 0作为业务条件防止扣成负数,受影响行数为1表示扣减成功,为0表示库存不足。这个方案在大多数业务里足够,不需要额外加分布式锁。
如果需要严格控制库存不被其他条件覆盖,可以再加版本号字段做乐观锁:
UPDATE stock SET num = num - 1, version = version + 1 WHERE id = ? AND version = ?;接下来就根据更新影响行数判断是否重试或提示失败。乐观锁适合冲突概率低的场景,冲突多时重试次数剧增,反而浪费资源。
写入优化的另外两个原则:批量操作不要一条条反复提交,尽量用一条INSERT ... VALUES (...), (...), (...)或LOAD DATA减少事务数;事务要短,持锁时间要短,任何长时间持锁的操作都会放大阻塞面。比如在事务里调用外部HTTP接口这种事,我见过不止一次,轻则锁等待,重则整个业务链路雪崩,绝对要避免。
6. 常见问题与部署细节实录
6.1 MySQL 5.7/8.0安装差异与docker部署要点
安装MySQL最容易被卡住的不是安装本身,而是版本差异带来的连锁反应。5.7 的默认认证插件是mysql_native_password,8.0 改成了caching_sha2_password。客户端版本太老,连接8.0数据库时会直接报Authentication plugin 'caching_sha2_password' cannot be loaded。解决方案要么升级客户端驱动,要么创建用户时指定IDENTIFIED WITH mysql_native_password BY 'xxx'。新项目我建议直接上8.0并用新版驱动,老项目迁移8.0时一定要先检查驱动版本。
Windows 安装时,很多人会提到“安装没有develop选项”。MySQL Installer 里的Developer Default是一整套开发组件,如果只需要数据库服务,直接选Server only就够了,装完去Services.msc确认MySQL80服务是否已启动。服务没启动时,命令行报错是最常见的:mysql 服务正在启动... 服务无法启动,这种情况先看data目录下的.err日志,多半是配置文件路径不对、数据目录权限不对或者端口被占用。
Docker 部署 MySQL 是目前最省心的方式之一,但要注意参数传递。一个常用命令:
docker run -d --name mysql8 \ -p 3306:3306 \ -e MYSQL_ROOT_PASSWORD=YourPasswd \ -e TZ=Asia/Shanghai \ -v /data/mysql8/conf:/etc/mysql/conf.d \ -v /data/mysql8/data:/var/lib/mysql \ mysql:8.0容器里的数据目录一定要挂载出来,否则容器删了数据全丢。时区建议显式设置TZ=Asia/Shanghai,否则写入的datetime和系统当前时间可能会差8小时。访问容器内MySQL时,应用既可以用宿主机IP加映射端口,也可以使用容器网络;如果容器和宿主机端口映射没生效,检查防火墙和docker ps的端口绑定情况。
6.2 连接类报错排查:2003、SSL、服务起不来
ERROR 2003 (HY000): Can't connect to MySQL server on 'localhost:3306' (10061)是一个高频报错。10061表示连接被拒绝,最常见的原因是服务根本没启动。先确认服务状态,Windows 下用services.msc,Linux 下用systemctl status mysqld。服务确实在跑还报错,再依次检查端口、绑定地址和防火墙。MySQL 默认只监听本机,如果my.ini或my.cnf里有bind-address=127.0.0.1,其他机器就连不上,需要改成0.0.0.0或具体的网卡地址。
SSL 连接错误是8.0时代的新常客。JDBC 连接串里如果写了useSSL=false但服务端强制SSL,或者反过来,都可能报 SSL 相关错误。本地开发环境我一般把 SSL 关了,mysql -u root -p --ssl-mode=DISABLED,生产环境则建议开启SSL并配置正确证书。
服务起不来还有一个很隐蔽的原因:磁盘满了或者/tmp目录不可写。MySQL 启动时需要写socket文件和临时表,磁盘空间不足时日志里不会直接提示“磁盘满”,而是出现各种奇怪的初始化失败。排查这类问题,先df -h看空间,再df -i看 inode,很多时候是二选一的问题。
6.3 常用命令、脚本与存储过程高频注意点
排查问题时,下面这些命令我几乎天天用:
| 场景 | 命令 |
|---|---|
| 查看当前连接 | SHOW PROCESSLIST;/SHOW FULL PROCESSLIST; |
| 杀掉阻塞查询 | KILL <thread_id>; |
| 查看InnoDB状态 | SHOW ENGINE INNODB STATUS\G |
| 查看全局状态 | SHOW GLOBAL STATUS LIKE 'Threads%'; |
| 查看行数估算 | SHOW TABLE STATUS LIKE 'orders'; |
| 导出数据库 | mysqldump -uroot -p dbname > backup.sql |
| 导入SQL脚本 | mysql -uroot -p dbname < backup.sql |
mysqldump导入大SQL时,直接命令行重定向往往比客户端工具的批量执行更高效,同时在客户端里执行source /path/to/backup.sql也是常用方式。执行大脚本前先把autocommit关掉,或者用事务一次性提交,能大幅减少磁盘IO。
存储过程在 MySQL 里可用,但我比较克制。存储过程确实能减少网络往返,适合批量数据处理,但它把业务逻辑藏进了数据库,排查问题时要同时翻应用代码和数据库脚本,维护成本很高。触发器我基本不用,因为触发器里的隐性操作很难被开发者感知,容易在批量导入时产生意料之外的锁和性能损耗。如果确实要写存储过程,注意使用DELIMITER改变语句分隔符,并且尽量只做小而明确的批量任务:
DELIMITER $$ CREATE PROCEDURE proc_course_avg(IN p_course_id INT, OUT p_avg DECIMAL(5,2)) BEGIN SELECT AVG(score) INTO p_avg FROM score WHERE course_id = p_course_id; END$$ DELIMITER ;调用时用CALL proc_course_avg(10, @avg); SELECT @avg;可以看到输出。这类统计型存储过程适合低频运维操作,高频业务接口里还是建议通过应用层查询。
还有一些高频SQL写法需要形成肌肉记忆:排序用ORDER BY注意方向;LIMIT必须搭配稳定的排序条件;DISTINCT去重要注意它走的是临时表机制,大数据量下性能很差;OR条件在两个字段时尽量拆成UNION ALL,否则索引优化器可能直接放弃索引。MySQL 没有DATEPART函数,类似逻辑用EXTRACT(YEAR FROM create_time)或YEAR(create_time)、MONTH(create_time)。自增ID重置可以用ALTER TABLE t AUTO_INCREMENT = 1,但前提是表中没有更大的ID值,否则主键冲突。给用户指定库的权限则是GRANT SELECT, INSERT, UPDATE, DELETE ON dbname.* TO 'user'@'host';加FLUSH PRIVILEGES;生效。
我个人在实际操作中最大的体会是:性能优化永远先做基线,再动刀。慢查询日志、EXPLAIN、SHOW PROCESSLIST这三板斧能解决80%的性能问题,真正需要动my.cnf参数的场景并没有想象中多。每次调整参数之后,一定要用压测脚本或者线上真实流量做前后对比,否则很容易出现“调完参数感觉快了,但其实是缓存热度上来了”的错觉。还有一个长期有效的习惯:线上大表结构变更尽量选在业务低峰期执行,8.0 的ALTER TABLE虽然支持了INSTANT算法,但所有操作最好先用SHOW PROCESSLIST确认没有长事务,避免元数据锁把整个表的读写全卡住。优化是一条持续迭代的路,先把最基础的索引和SQL写好,MySQL会回报你远超预期的稳定。