常见陷阱与最佳实践
这一章是全手册的收束:把散落在 00~12、14 章的坑汇总成一张速查表,把该守的规则压成清单,最后给一张"出事了先查什么"的调试表。写法上不再展开原理(原理都在对应章节),只回答三个问题:坑是什么、后果长什么样、正确做法一句话。
版本基线与全手册一致:MySQL 8.4 LTS(兼顾 8.0 存量与 9.7 LTS);标注"实测"的结论出自本地 8.0.28 实例验证。
1. 陷阱汇总表
1.1 建表与数据类型
| 陷阱 | 后果 | 正确做法 | 详见 |
|---|---|---|---|
用 utf8 建库建表 |
utf8 只是 utf8mb3 的别名,存 𠮷、emoji 直接报 ERROR 1366 (HY000): Incorrect string value: '\xF0\xA0\xAE\xB7'(实测:客户端字符集正确时同样拒收,不是客户端的锅) |
一律显式 utf8mb4 + utf8mb4_0900_ai_ci,库/表/列三层都定 |
03 |
| 同一条 SQL 里混用不同 collation 的列做比较/拼接 | ERROR 1267 (HY000): Illegal mix of collations,迁移合并两套历史库时高发 |
新表统一 collation;临时用 COLLATE 显式转换,别依赖连接默认 |
03 |
| 金额用 FLOAT/DOUBLE | 二进制浮点存十进制小数天然有误差,对账对不平 | DECIMAL(M,D),钱的问题用精确小数 |
03 |
TIMESTAMP 存长期业务时间 |
2038 年上界、且随 time_zone 变量变化显示值 |
业务时间用 DATETIME(3);TIMESTAMP 只用于"记录此刻"的审计字段 |
03 |
| 主键用宽字符串(UUID 原文、业务编码) | 聚簇行变宽、每棵二级索引叶子都跟着变宽,扇出腰斩、树提前长高 | 代理主键 BIGINT UNSIGNED AUTO_INCREMENT,业务编号另建唯一索引 |
04 |
| VARCHAR 定义宽度按"占空间"来省着给 | 省不了多少(按实际长度存),但宽度参与索引成本估算和 key_len 计算 | 给到业务上限即可,别抠 20 和 32 的区别;真正要抠的是跨不跨 255 字节分界(utf8mb4 下 63→64 会让长度前缀从 1 字节变 2 字节,触发列改重建) | 03、09 |
INT(11)、ZEROFILL 当"宽度限制"写进新代码 |
显示宽度 8.0 起已弃用,纯噪音 | 写 INT 就好,宽度交给类型本身 |
03 |
| 无主键表 | 复制(ROW+where 全表定位)、RDS 工具链、在线 DDL 全部受影响 | 每张 InnoDB 表必须有主键 | 00、10 |
1.2 索引
| 陷阱 | 后果 | 正确做法 | 详见 |
|---|---|---|---|
对索引列做函数/运算(WHERE DATE(t)=...) |
索引失效,全表扫 | 改写成范围:t >= '2026-10-20' AND t < '2026-10-21';或 8.0 函数索引 |
08 |
WHERE varchar_col = 123(数字比对字符串) |
隐式类型转换按 DOUBLE 比较,该列索引整体失效 | 类型对齐是最便宜的优化;驱动层参数别偷懒 | 08 |
联合索引把范围列放前面((a,b) 但 WHERE a>? AND b=?) |
b 用不进索引,key_len 只到 a | 等值列在前、范围列在后;排序列放在范围列之后还能省 filesort | 04、08 |
EXPLAIN 的 key 显示 idx_a 就以为"走了索引" |
走了不等于用得值:命中 30% 以上行数时回表成本可能高于顺序扫 | 对比 rows 估算与实际,必要时 FORCE INDEX 验证两种计划的真实耗时 |
08 |
| 迷信 key_len 资料里"varchar ≤255 字节 +1 前缀" | 算出来的 key_len 永远对不上线上值 | EXPLAIN 口径一律 +2(长度前缀)+ 可空列每个 +1;1/2 字节分界是 InnoDB 磁盘记录格式的规则,两套口径别混(实测:utf8mb4 VARCHAR(20) 计 82、VARCHAR(63) 计 254,全部 +2) |
08 |
| 给低基数枚举列(status、性别)单独建索引 | 优化器永远不选它,白白摊薄 buffer pool、拖慢写入 | 要建就建联合(status+时间),或干脆不建靠过滤 |
04 |
冗余索引:(a) 与 (a,b) 并存、或 KEY 与同列 UNIQUE 并存 |
纯浪费写放大 | SHOW INDEX 定期体检,删被前缀覆盖的 |
04 |
| 删索引前直接 DROP | 万一是某条低频大 SQL 唯一的救命稻草,上线即事故 | 先 ALTER TABLE ... ALTER INDEX ... INVISIBLE 灰度几天,sys.schema_unused_indexes 无新增再 DROP |
04、08 |
1.3 SQL 写法
| 陷阱 | 后果 | 正确做法 | 详见 |
|---|---|---|---|
SELECT * 上生产 |
覆盖索引退化、网络浪费、表结构一变应用就炸 | 列清单显式写 | 02 |
深分页 LIMIT 1000000,20 |
扫 100 万条再丢掉,越翻越慢 | 游标式改写:WHERE id > 上次末尾id ORDER BY id LIMIT 20;或延迟关联(子查询分页拿主键再回表) |
08 |
COUNT(*) 恐慌 |
其实 InnoDB 就是扫最小的树数行,百万级毫秒~秒;真正慢的是 COUNT(*) 无 where 大表 + 高频调用 |
要"大概"用统计信息 information_schema.tables.rows 或 Redis 计数,要精确就接受扫描 |
02、08 |
GROUP_CONCAT 结果莫名被截断 |
默认 group_concat_max_len=1024(实测),拼接长列表悄悄丢尾 |
会话级调大,或改应用层聚合 | 02 |
| 递归 CTE 不设边界 | cte_max_recursion_depth 默认 1000,超了报 ERROR 3636(recursive query aborted after 1001 iterations,实测) |
层级不可控的树在 WHERE 里带深度计数 | 02 |
| 依赖 8.0 的 hash join 当"免费午餐" | join buffer 占内存(join_buffer_size 按线程分配),超大表 join 照样慢 |
先保证等值列有索引,hash join 是兜底不是常规 | 08 |
上线 ONLY_FULL_GROUP_BY 报错的旧 SQL 直接改全局 sql_mode 关掉它 |
掩盖了真实歧义:非聚合列在组内本来就不确定 | 修 SQL(进 GROUP BY 或套聚合函数),8.0 默认 sql_mode 别动(实测默认含 ONLY_FULL_GROUP_BY、STRICT_TRANS_TABLES) | 00、02 |
1.4 事务与锁
| 陷阱 | 后果 | 正确做法 | 详见 |
|---|---|---|---|
代码里 BEGIN 后异常路径没 COMMIT/ROLLBACK |
连接归还连接池后长悬挂:undo 膨胀、purge 停滞、复制延迟、DDL 被 MDL 卡死 | 事务边界包住整个逻辑;框架层兜底 rollback;information_schema.innodb_trx 上告警(>60s 就报警) |
05 |
UPDATE/DELETE 的 WHERE 没有可用索引 |
RR 下全表记录逐行加 X 锁,等同锁表;别的会话全部 1205 | 无索引的批量 UPDATE 是高危变更,上线前 EXPLAIN 必须看到 key 非 NULL | 05 |
| 把 1205 当死锁处理 | 两者语义不同:ERROR 1205 Lock wait timeout exceeded(实测)只回滚当前语句、事务还活着,直接重试原语句会拿半套状态重来 |
1205 → 查谁持锁(data_lock_waits/sys.innodb_lock_waits);ERROR 1213 Deadlock found(实测)才回滚整个事务,幂等重试 |
05 |
| 死锁慌了删索引"避锁" | 锁冲突的本质是交叉更新顺序,删索引只会把它变成全表锁 | 统一加锁顺序、拆小事务、innodb_print_all_deadlocks=ON 留现场 |
05 |
| RR 快照读之后紧跟一条 UPDATE | UPDATE 是 current read,改的可能正是"你的快照看不到的"别家已提交行,随后同事务再读它就"变"了 | 需要"读到什么就锁住什么"用 SELECT ... FOR UPDATE(且必须在 UPDATE 之前);业务对一致性敏感就直说用 RC + 应用层判 |
06 |
队列表 SELECT FOR UPDATE + 高并发取任务 |
gap/next-key 锁把"取一条"放大成"锁一段",插入也被挡 | 8.0 用 FOR UPDATE SKIP LOCKED(实测跳过被锁行返回下一批)或 NOWAIT(ERROR 3572 ... NOWAIT is set,实测) |
05 |
| 大事务一次改几十万行 | 行锁升级为长持锁 + binlog/undo 双膨胀 + 从库回放尖峰 | 分批(每批几千行、批间提交);定时任务错峰 | 05、07 |
1.5 DDL 与大表变更
| 陷阱 | 后果 | 正确做法 | 详见 |
|---|---|---|---|
ALTER 不写 ALGORITHM= 就上线 |
你以为 INSTANT,实际悄悄降级 COPY/INPLACE 重建,大表直接拖垮 IO | 变更单显式声明 ALGORITHM=INSTANT, LOCK=NONE,不支持就直接报错(ERROR 1845 ... Try ALGORITHM=COPY/INPLACE.,实测)反而安全 |
09 |
| 无脑反复 INSTANT ADD/DROP 列 | 行版本累积有上限,超限后连普通 DDL 都报 1799 类错误,被迫停机重建 | 定期把高频变更表排进"重建归零"维护窗口 | 09 |
| 大表加列同时加索引 | 两件事各自都在线,叠一起放大 row log 与 MDL 风险 | 拆两次变更:加列走 INSTANT,索引单独 CREATE INDEX ... ALGORITHM=INPLACE,各自有失败重试边界 |
09 |
| gh-ost/pt-osc 当万能 | 都要唯一键、都要磁盘余量、都吃从库/主库复制流量;pt-osc 依赖 trigger,和别的 trigger 冲突 | 变更 10 万行以下 INSTANT/INPLACE 直做,亿级才上工具,跑前在预发按同量级演练 | 09 |
| DDL 执行完卡在最后的 MDL 申请 | “卡住不报错"时全表读写排它后面排队,看起来像数据库挂了 | 先 kill 持锁长查询再重试 DDL;sys.schema_table_lock_waits 定位 |
05、09 |
1.6 复制与高可用
| 陷阱 | 后果 | 正确做法 | 详见 |
|---|---|---|---|
| 默认异步复制 + 主库宕机硬切换 | 未及传输的 binlog 随主机一起丢 | 核心业务半同步(rpl_semi_sync_*,8.0.29+ 用 AFTER_SYNC 即"lossless”)+ GTID |
10 |
| 应用直接读从库 | 复制延迟下"写完立刻读不到"(写后读不一致) | 下单/支付类强制读主;或事务内粘主;或 GTID 等待(rds_async/WAIT_FOR_EXECUTED_GTID_SET) |
10 |
| 从库调并行复制只调 worker 数 | replica_parallel_workers=8 但回放仍是串行 |
8.0.26+ 默认 WRITESET 依赖追踪即可并行;8.4 内部恒为 WRITESET,只留 replica_preserve_commit_order=ON(别关) |
10 |
| binlog 当备份 | ROW 格式大事务/无主键表 UPDATE 会把从库与备份一起打爆 | 备份用 xtrabackup/mysqldump,binlog 只做增量链与恢复点 | 07、11 |
binlog_row_image=minimal 想省空间 |
闪回/下游 CDC(Canal 类)拿不到 before image,兼容性碎一地 | 保持默认 FULL;省空间去调保留策略而不是行镜像 | 07、10 |
1.7 备份与恢复
| 陷阱 | 后果 | 正确做法 | 详见 |
|---|---|---|---|
mysqldump 不加 --single-transaction |
要么锁全表停写,要么备份不一致 | InnoDB 统一 --single-transaction --source-data=2(8.0.26+ 新名;旧 --master-data 仍可用) |
11 |
--single-transaction 当万能 |
库里混着 MyISAM/MEMORY 表照样不一致 | 迁平 InnoDB,或接受逻辑备份加读锁窗口 | 11 |
| 只备份、不备份 binlog | 恢复到"上次备份"为止,备份间隔内的数据全丢 | 全量 + 连续 binlog 归档 + 定期真实恢复演练;没演练过的备份等于没有备份 | 11 |
| Percona XtraBackup 版本乱配 | 8.0 用 xb8.0+、8.4 用 xb8.4/9.x,拿 5.7 的老工具直接失败 | 版本矩阵查官方,升级前先验证备份工具跟得上 | 11 |
| 恢复演练从不碰权限/账号 | 数据回来了应用连不上(账号表没跟着走) | 演练覆盖 mysql.user 等 sys schema 之外的库;克隆实例全链路验证 |
11 |
1.8 配置与资源
| 陷阱 | 后果 | 正确做法 | 详见 |
|---|---|---|---|
innodb_buffer_pool_size 默认值直接用 |
128MB 默认跑生产库,页永远在换进换出 | dedicated 机器给物理内存的 50%~70%;容器里注意被 OOMKill 的余量 | 00、11 |
调 sort_buffer_size/join_buffer_size 设"全局加大" |
他们是每连接/每操作分配,几百连接同时大排序直接把内存打爆 | 全局保持默认,会话级按需调 | 11 |
| redo 容量"越大越稳" | 崩溃恢复要重放的量与容量正相关;5.7 时代改大小还是停机事故(8.0.30+ 用 innodb_redo_log_capacity 在线改,旧参数已弃用未移除) |
用 checkpoint age 曲线定容量,别拍脑袋 | 07 |
关慢日志省 IO / long_query_time 设 10 秒 |
出事时没有现场证据 | slow_query_log=ON,生产常配 long_query_time=0.1~1 + log_queries_not_using_indexes(加 min_examined_row_limit 防刷屏) |
08、11 |
| 磁盘满不监控 | error log、binlog、临时表全写不进,实例直接不可用 | 数据盘/日志盘/binlog 盘分开规划 + 80% 告警;innodb_online_alter_log_max_size 打满同样是 DDL 失败现场 |
09、11 |
1.9 分库分表
| 陷阱 | 后果 | 正确做法 | 详见 |
|---|---|---|---|
| 分片键选错(按 user_id 分,却大量按 order_id 查) | 广播全分片查询,比不分还慢 | 分片键=最高频等值查询的维度;另一维度用基因法/异构索引表补 | 12 |
| 分片数拍脑袋定当前量 | 扩容 = 全网数据搬迁 | 按五年峰值定,宁大勿小;2 的幂便于翻倍迁移 | 12 |
跨分片 ORDER BY ... LIMIT 深分页 |
每片各取 offset+limit 条回内存归并,N 片 × 深页 = 内存爆炸 | 游标翻页或汇聚层(异构到 ES) | 12 |
| 雪花 ID 时钟回拨没处理 | 重号或抛异常,主键冲突在生产是 P0 | 回拨等待/借位报警,或数据库号段模式 | 12 |
| 跨库事务当真分布式事务做 | 2PC 性能与可用性双输 | 尽量路由单分片本地事务;确需跨片用最终一致(本地消息表/MQ) | 12 |
1.10 程序性对象与视图
| 陷阱 | 后果 | 正确做法 | 详见 |
|---|---|---|---|
备份不带 --routines --events |
mysqldump 默认不导出存储过程/函数/事件,恢复出来的库缺一块逻辑且要到调用时才炸 | 逻辑备份固定带这两个开关(触发器默认有,过程和事件没有) | 14、11 |
| 删账号前不盘 DEFINER | 视图/过程/函数/触发器/事件默认按创建者权限执行,definer 账号一删全变 ERROR 1449(实测) |
下线账号前 information_schema 盘 definer;共享对象用专门服务账号做 definer |
14 |
把视图 WITH CHECK OPTION 当约束 |
校验发生在 BEFORE 触发器改写之后——实测 amt=1 经触发器变 202,视图闸门照样放行 |
约束落在列定义/检查与应用层;视图闸门只做"直写不越界"的兜底 | 14 |
| binlog 开启就裸建函数 | ERROR 1418(实测原文:函数须声明 DETERMINISTIC/NO SQL/READS SQL DATA 之一);有人图省事开 log_bin_trust_function_creators 绕过 |
如实声明特性(写进元数据,SHOW CREATE FUNCTION 可见);trust 开关是"信任创建者"不是"关掉问题" |
14 |
| AFTER 触发器里改 NEW | 建触发器直接失败 ERROR 1362(实测,创建期就拦);改值请用 BEFORE |
NEW 可写性规则背下来:BEFORE 写 NEW,AFTER 只读 | 14 |
| 触发器回写触发它的基表 | ERROR 1442(实测):“本语句已占用的表不能在触发器里再动”;“插入后回写汇总"这类需求在 MySQL 触发器里无解 |
汇总写别的表(审计/聚合表)或放应用层同一事务 | 14 |
| 以为多个同类触发器有优先级语法 | MySQL 没有 Oracle 的 FOLLOWS/PRECEDES,同一时机+事件按创建顺序执行(实测),换序只能 DROP 重建 | 别让触发器互相依赖;链条超过两个就该合并成一个过程 | 14 |
SET GLOBAL event_scheduler=ON 当持久 |
默认 OFF 且 GLOBAL 不落盘,重启回到 OFF,“事件莫名其妙不再跑” | SET PERSIST event_scheduler=ON(8.0);且注意事件上一轮未跑完时错过的调度跳过不补,逻辑须幂等 |
14 |
| 万行游标循环当优化 | 逐行循环基本必慢,且过程里的 sql_mode 冻结在创建时,全局模式改了行为却不跟着改 |
集合 SQL 能替代的一律改写;过程行为诡异先查 information_schema.routines.SQL_MODE |
14 |
| XA prepared 悬挂没人裁决 | 分支占行锁和 undo 独立存活,协调者缺位就是事故;XA RECOVER 不巡检等于不管 |
用 XA 必须有超时兜底与裁决流程;没有就退回本地消息表最终一致(12 章) | 14、07 |
2. 六个最容易在生产炸掉的坑,展开讲
上面的表格适合事后检索,这六条适合在评审会上当面讲。它们的共同点:单看没错,量级一上来就错。
2.1 无索引的 UPDATE —— “我就改 5000 行”
UPDATE orders SET status='CLOSED' WHERE shop_id=42 AND created_at < '2026-01-01';shop_id 没索引,RR 下 InnoDB 沿聚簇索引逐行扫描并给每行加 X 锁(锁的是扫过的全部行,不是命中的 5000 行)。表 3000 万行,这条语句就锁了 3000 万行,还外加间隙锁——其他所有写入排队,监控上看到"锁等待风暴”,值班第一反应 kill 慢 SQL,kill 又要先回滚几十万 undo,等分钟级。正确姿势:写之前 EXPLAIN 确认 key 命中;确实没索引,先加 (shop_id, created_at) 再改,或分批按主键改。
2.2 长事务 + MDL —— “DDL 卡住了,全库都卡了”
事务 A 开着不动(BEGIN 后没提交),DDL 来申请排他 MDL 被挡,MDL 队列里 DDL 之后的所有新查询继续排在 DDL 后面——表象是"数据库整体变慢"。处置顺序固定:sys.schema_table_lock_waits 找到持锁事务 → kill → DDL 自己跑过去 → 队列疏通。预防靠 DDL 前置检查(活跃长事务监控、lock_wait_timeout 给 DDL 会话设小一点让它自己先放弃)。
2.3 key_len 两套口径 —— 把磁盘格式规则套到 EXPLAIN 上
流传极广的口诀"varchar 长度 ≤255 字节时长度前缀按 1 算"只对 InnoDB 磁盘记录格式成立;EXPLAIN 的 key_len 一律 +2。实测 utf8mb4 下 VARCHAR(20)=82、VARCHAR(63)=254(255 字节内),VARCHAR(64)=258,全部 +2。官方文档自己的例子(utf8 VARCHAR(20) NULL 计 63)也是 +2。口径混用的直接后果是"索引到底用到了第几列"判错,优化决策跟着错。
2.4 从库读"自己的写" —— 复制延迟是业务问题不是运维问题
延迟常态 0.5s 时看起来无害,直到有人把"提交订单 → 立刻跳转订单详情"的读指到从库。用户视角就是"我下了单呢?"。解法按成本排序:强制主读(写后关键读)→ 会话粘主 → WAIT_FOR_EXECUTED_GTID_SET 等位点 → 半同步 + 延迟告警。评审时先问一句"哪些读路径跨了读写分离",比事后加缓存便宜。
2.5 INSTANT 的账 —— 免费的也要还
每次 INSTANT 变更让行格式里多叠一个版本记录,单表累积有上限(量级几十次,具体随版本演进)。超限后 ALTER 报错、且表已经"脏"到只能重建——那时候重建就是又一场计划外停机。把"高频变更表每年重建归零"写进例行维护,而不是等报错。
2.6 备份没演练过 —— 薛定谔的备份
mysqldump 退出码 0 不代表能恢复:表结构不兼容、binlog 位点错位、账号缺失,都只在恢复那天爆。最低标准的演练:每季度在克隆机/独立实例上做一次全量 + binlog 回放到指定时间点,验证业务 SQL 能跑。顺带验证恢复时长是否满足 RTO。
3. 最佳实践清单
建表与数据类型
- 库/表/列三层显式
utf8mb4;所有表有BIGINT UNSIGNED自增主键 +created_at/updated_at DATETIME(3)。 - 金额 DECIMAL、时间 DATETIME(3)、状态 TINYINT + 注释枚举值;禁用显示宽度与 ZEROFILL。
- 单行目标 < 1KB:大字段垂直拆表,让 16KB 页装尽可能多的行。
- 生产表全部 InnoDB + 有主键,为复制、备份、在线 DDL 三件事同时铺路。
索引
- 联合索引口诀:等值在前、范围在后、排序列收尾;一个查询尽量被一个索引吃下(前缀 + 排序 + 覆盖)。
- 每个索引都要回答"哪条 SQL 用它";答不上来的季度体检删掉(先 INVISIBLE 灰度)。
- 二级索引叶子带主键:主键越窄,所有索引都跟着减肥。
- 上线索引用 EXPLAIN 验收四件事:
key非空、type不劣于 range、rows与预期同量级、Extra 没有Using filesort; Using temporary成对出现。
SQL
- 列清单显式;
WHERE列侧零函数零转换;分页用游标;大查询必带LIMIT。 - 上线 SQL 在预发用真实量级数据跑
EXPLAIN ANALYZE,估算 rows 与实际 rows 差 10 倍以上先ANALYZE TABLE再谈优化。 - 复杂报表 SQL 和联机交易 SQL 物理隔离(不同连接池/不同账号配额),慢查询不能拖死核心链路。
事务
- 事务里禁止人机交互(RPC 外部服务、发请求);默认 RR 的团队考虑统一改 RC + binlog ROW,换来更少的 gap 锁。
- 幂等重试是业务层标配:1213 回滚重跑、1205 先诊断再动。
autocommit=1保持默认,业务框架统一管边界;>60s 活跃事务必须告警。
DDL
- 变更单三件套:预估行数、期望 ALGORITHM(显式写死)、回滚方案。
ALGORITHM=INSTANT报错就是信息,不是故障。 - 大表(千万行以上)加索引/改类型用 gh-ost 或 pt-osc,窗口期 + 复制延迟监控 + 磁盘余量检查一次不少。
- OPTIMIZE/重建排进业务低峰,且知道它会占 MDL。
复制与 HA
- 开 GTID、ROW binlog、
enforce_gtid_consistency=ON;从库super_read_only=ON。 - 半同步 + AFTER_SYNC 覆盖核心链路;切换工具(MHA/Orchestrator/云托管)要定期演练真切。
- 读从库的每条路径先过"能不能容忍延迟"这一关;不容忍就回主。
- 并行复制:8.4 什么都不用配(内部恒 WRITESET),只调
replica_parallel_workers,replica_preserve_commit_order不关。
备份
- 全量(xtrabackup 或 mysqldump
--single-transaction)+ binlog 连续归档 + 异机存放,三者缺一不算备份。 - 每季度真实恢复演练一次,记录恢复时长对照 RTO/RPO。
- 备份账号权限最小化(
RELOAD/PROCESS/REPLICATION CLIENT+ 库级 SELECT),密码进密钥管理不进脚本。
配置与资源
innodb_buffer_pool_size= 物理内存的 50%~70%(dedicated 机器),上线前算过账而不是抄默认。- redo 用
innodb_redo_log_capacity管理并按 checkpoint age 定容;innodb_flush_log_at_trx_commit=1+sync_binlog=1是金融默认档,降档要写明丢了什么。 - 慢日志常开(
long_query_time≤1s)、innodb_print_all_deadlocks=ON;error log 磁盘水位 80% 告警。 - 连接数按"应用总连接 < max_connections × 70%“配,thread_cache 让建连不再成为毛刺。
4. 生产上线检查清单
新实例/新业务上线前逐条打钩:
- 字符集三层 utf8mb4,
sql_mode保持 8.0/8.4 默认(STRICT + ONLY_FULL_GROUP_BY 在线) - 所有表 InnoDB、有主键、无 MyISAM/MEMORY 混入
- 核心 SQL 全部有 EXPLAIN 凭证(改前/改后两份计划归档)
- 慢日志、error log、死锁日志开启且落盘位置有监控
- 备份任务 + binlog 归档已跑通一次真实恢复
- 复制:GTID、ROW、半同步(若要求)、
super_read_only、从库延迟告警 - MDL:DDL 前置检查脚本/流程就位,长事务告警阈值已设
- 磁盘:数据/日志/binlog 分盘或分目录,水位告警 80%
- 账号:应用账号无
SUPER/FILE;备份账号最小权限;无mysql_native_password遗留(8.4 默认已禁用,存量账号迁移 caching_sha2_password) - 参数:buffer pool、redo 容量、连接数三项有容量计算依据,不是"和测试环境一样”
- 大表变更预案:预估行数、ALGORITHM 声明、工具选型、回滚路径
- 分库分表(若用):分片键覆盖 95% 查询路由,全局 ID 时钟回拨策略验证过
5. 调试速查表
| 症状 | 第一步查什么 | 命令/入口 |
|---|---|---|
| 整体变慢、连接堆积 | 有没有长事务/锁等待风暴 | SELECT * FROM information_schema.innodb_trx ORDER BY trx_started;;sys.innodb_lock_waits |
| 一条 SQL 突然变慢 | 计划变没变、统计旧不旧 | EXPLAIN FORMAT=TREE 对比历史;EXPLAIN ANALYZE 对 actual/estimated rows;ANALYZE TABLE |
| 偶发 1205/1213 | 现场锁信息或历史死锁 | performance_schema.data_locks/data_lock_waits;error log(innodb_print_all_deadlocks=ON 后 LATEST DETECTED DEADLOCK) |
| DDL 卡住不动 | MDL 排队 | sys.schema_table_lock_waits;SHOW PROCESSLIST 看 “Waiting for table metadata lock” |
| 从库延迟尖峰 | 大事务 / 无主键表 UPDATE / 单线程回放 | error log 与 SHOW REPLICA STATUS\G(Seconds_Behind_Source、Last_Error);主库审计当时的大 DML |
| 写入周期性卡顿 | redo 逼近写满、刷脏跟不上 | SHOW ENGINE INNODB STATUS\G 的 checkpoint age;Innodb_buffer_pool_pages_flushed 突刺 |
| 内存告急 | buffer pool 外的大块会话缓冲 | 排序/临时表统计:Created_tmp_disk_tables、Sort_merge_passes |
| 备份失败 | 版本矩阵与磁盘 | xb 日志;df -h;检查备份窗口是否与 purge/长事务冲突 |
| 表膨胀、查询变慢但行数没涨 | 大量 UPDATE/DELETE 未回收空洞 | information_schema.tables 的 data_free;低峰 OPTIMIZE/重建 |
| 连不上/认证失败 | 插件与账号 | 8.4:mysql_native_password 默认禁用,老客户端需 --mysql-native-password=ON 或升级驱动用 caching_sha2 |
| 视图/存储过程"昨天还好好的"今天报 1449 | definer 账号是不是被删了、sql_mode 冻结值 | information_schema.{views,routines,triggers,events} 盘 DEFINER/SQL_MODE(见 14 章第 6 节) |
| 定时事件不再执行 | event_scheduler 掉回 OFF(重启)或上轮跳过 | SELECT @@event_scheduler;information_schema.events 看 STATUS/execute_at;改 SET PERSIST |
实战建议:这一章的正确用法不是通读,而是三件事——① 上线评审时把第 4 节清单过一遍;② 出事时跳到第 5 节按症状入口;③ 每季度把第 1 节表格里"我们踩过"的列做标记,踩过的坑比没踩的更值得复训。全手册各章(00~12、14)负责"为什么",这一章负责"记住"。