1. 这不是教科书里的MySQL,而是我在线上系统崩溃前夜亲手调通的ACID与索引真相
你有没有过这样的经历:凌晨两点,监控告警疯狂闪烁,订单支付成功率从99.9%断崖式跌到32%,DBA在群里甩出一句“查了,是事务卡住了”,然后所有人盯着慢查询日志里那条看似普通的UPDATE order SET status='paid' WHERE id=123456发呆?我有。那一次,我们花了7小时定位问题——不是SQL写错了,不是索引没建,而是隔离级别在高并发下悄悄“背叛”了业务逻辑。后来我才明白,所谓MySQL的“核心特性”,从来不是文档里几行定义能概括的。ACID不是四个字母,是数据库在数据洪流中维持秩序的四根锚链;隔离级别不是READ UNCOMMITTED到SERIALIZABLE的线性阶梯,而是一张用锁和MVCC编织的、充满取舍与代价的权衡之网;索引不是“加个索引就快”,而是B+树在磁盘寻道、内存缓存、数据分布三重约束下的精密工程;视图更不是简单的“SELECT封装”,它是查询重写器、权限过滤器、甚至可能是性能陷阱的隐形开关。这篇内容,就是我把过去十年在电商、金融、SaaS系统里踩过的所有坑、调过的所有参数、画过的所有执行计划树,浓缩成的一份“实战派MySQL内核手记”。它不讲抽象理论,只说“为什么线上会崩”“为什么加了索引反而更慢”“为什么视图里改不了数据”。如果你正被死锁日志折磨,被慢查询拖垮服务,或者只是想真正看懂EXPLAIN输出里那个type: ALL到底意味着什么——这篇文章,就是为你写的。
2. ACID:从纸面定义到线上生死线的四重穿透
2.1 原子性(Atomicity):不是“全做或全不做”,而是“Undo Log的精准外科手术”
很多人把原子性理解为“事务要么全部成功,要么全部失败”,这没错,但太浅。真正的关键,在于MySQL如何实现“全部失败”——靠的是Undo Log,而且是分段式、可回滚的Undo Log。我见过太多人以为ROLLBACK就是清空所有变更,结果在生产环境误操作后发现回滚耗时长达47分钟,服务雪崩。问题出在哪?出在Undo Log的物理结构上。
MySQL的Undo Log不是一块连续内存,而是存储在共享表空间(ibdata1)或独立Undo Tablespace中的链表结构。每个事务在修改数据前,会先将修改前的旧值(Before Image)写入Undo Log,并打上事务ID(TRX_ID)和回滚指针(ROLL_PTR)。这个过程是顺序写入,所以极快。但回滚时,MySQL必须沿着ROLL_PTR链表,逐条读取旧值,再反向应用。如果一个事务更新了10万行,Undo Log链表就长达10万节点,回滚就是10万次随机IO——这就是为什么大事务回滚慢如蜗牛。
提示:线上严禁执行
UPDATE table SET col='new' WHERE 1=1这类无条件全表更新。哪怕加了WHERE,也要确认条件是否走索引。我曾在一个用户中心表上执行UPDATE user SET last_login_time=NOW() WHERE status='active',结果status字段无索引,触发全表扫描+全表更新,Undo Log暴涨2GB,回滚用了22分钟,期间所有依赖该表的API全部超时。
实操中,我强制团队遵守三条铁律:
- 单事务修改行数上限:在应用层埋点,任何事务修改超过500行自动告警并拒绝提交;
- 批量更新必分页:用
WHERE id BETWEEN ? AND ?分片,每片≤500行,配合SELECT ... FOR UPDATE加锁范围最小化; - Undo Tablespace必须独立:在
my.cnf中配置innodb_undo_tablespaces = 4,避免Undo Log与数据页争抢ibdata1的IO资源。
2.2 一致性(Consistency):ACID里最狡猾的“幽灵”,它由其他三者共同守护
一致性常被误解为“数据符合业务规则”,比如“余额不能为负”。但MySQL层面的“一致性”,本质是数据库状态在事务前后都满足预设的约束(Constraint)和规则(Rule)。它本身不提供实现,而是由原子性、隔离性、持久性共同保障的结果。举个血泪案例:某支付系统要求“扣款成功后,订单状态必须变为‘已支付’”,开发写了两个独立事务:T1扣款,T2改状态。结果T1成功,T2因网络超时失败,数据就“不一致”了——但这不是MySQL的问题,是业务设计违反了原子性原则。
真正让一致性落地的,是MySQL内置的约束机制:
- 主键约束(PRIMARY KEY):强制唯一且非空,底层通过聚簇索引实现,插入时自动校验;
- 外键约束(FOREIGN KEY):在InnoDB中,外键检查会触发额外的索引查找(
SELECT ... FROM parent WHERE id=?),这是性能杀手。我们线上所有外键在MySQL 5.7后全部移除,改由应用层强校验+最终一致性补偿; - CHECK约束(MySQL 8.0.16+):终于支持了!但注意,它只在DML时校验,不校验已有脏数据。上线前必须跑
ALTER TABLE t ADD CHECK (age >= 0),否则老数据可能直接报错。
注意:
UNIQUE KEY和PRIMARY KEY在InnoDB中都是B+树索引,但主键是聚簇索引(数据行物理存储在索引叶节点),唯一键是非聚簇索引(叶节点只存主键值)。这意味着,通过唯一键查询,要先查唯一索引拿到主键,再回表查数据——两次B+树搜索。而主键查询只需一次。所以,高频查询字段,优先设为主键或联合主键的一部分。
2.3 隔离性(Isolation):MVCC与锁的双轨制,不是选择题而是组合拳
隔离性是ACID里最易被误解的。很多人以为“设置SET TRANSACTION ISOLATION LEVEL READ COMMITTED就万事大吉”,却不知READ COMMITTED下仍可能产生不可重复读,而REPEATABLE READ下也可能遭遇幻读。真相是:MySQL的隔离级别,是MVCC(多版本并发控制)与锁机制的混合体,不同级别下,两者的权重天差地别。
- READ UNCOMMITTED:纯读未提交,不加任何锁,也不用MVCC。读取的是最新数据页,脏读、不可重复读、幻读全开。线上绝对禁用。
- READ COMMITTED(RC):每次SELECT都生成新Read View。这意味着,同一事务内,两次
SELECT * FROM t WHERE id=1,如果中间有其他事务提交了修改,第二次会看到新值——不可重复读。但RC下,间隙锁(Gap Lock)被禁用,只加记录锁(Record Lock)。这极大降低了死锁概率,是我们电商库存扣减的默认级别。 - REPEATABLE READ(RR):事务启动时生成一个Read View,贯穿整个事务。所以同一事务内多次读,看到的数据版本一致。但RR下,InnoDB默认启用Next-Key Lock(记录锁+间隙锁),用于防止幻读。问题来了:间隙锁会锁住索引区间,比如
WHERE id > 10 AND id < 20,会锁住(10,20)这个范围,导致大量无关插入被阻塞。
我做过一个压测:在RR级别下,对order表按create_time范围查询(WHERE create_time BETWEEN '2023-01-01' AND '2023-01-31'),由于create_time无索引,MySQL被迫全表扫描并加Next-Key Lock,结果插入新订单的线程全部卡死。切换到RC后,问题消失——因为RC不加间隙锁。
实操心得:RR不是“更安全”的代名词。在高并发写场景(如秒杀、抢券),RR的间隙锁是性能毒药。我们的解决方案是:业务表必须有高选择性索引(如订单号、用户ID),确保WHERE条件能走索引,让Next-Key Lock锁定范围最小化;同时,对纯读场景(报表、BI),才使用RR保证一致性。
2.4 持久性(Durability):Redo Log的WAL哲学,不是“刷盘”而是“日志先行”
持久性常被等同于“数据落盘”,但MySQL的实现精髓是WAL(Write-Ahead Logging):所有数据修改,必须先写Redo Log,再修改Buffer Pool中的数据页。Redo Log是循环写入的物理日志,记录的是“在某个数据页的某个偏移量,写入了什么字节”,而非逻辑SQL。
Redo Log的持久性保障,取决于innodb_flush_log_at_trx_commit参数:
- =1(默认):每次事务提交,都强制将Redo Log刷盘(fsync)。最安全,但IO压力最大;
- =0:每秒刷盘一次,事务提交时不刷。崩溃最多丢失1秒数据;
- =2:每次提交写入OS Cache,但不fsync。崩溃不丢数据(OS Cache未丢),但服务器断电可能丢失。
我们线上采用**=1 +sync_binlog=1** 的强一致组合。但有个致命细节:innodb_log_file_size(Redo Log文件大小)必须合理。太小(如默认48MB),会导致频繁的checkpoint,即强制将Buffer Pool中脏页刷回磁盘,引发IO尖峰。我们根据QPS和TPS计算:innodb_log_file_size = (平均每秒Redo Log写入量) × 60。例如,日均Redo写入1GB,则每秒约12KB,innodb_log_file_size设为720MB(12KB×60秒),足够撑住1分钟的峰值写入,避免checkpoint风暴。
3. 隔离级别:从理论模型到线上选型的决策树
3.1 四级隔离的本质差异:一张表看穿所有幻觉
| 隔离级别 | 脏读 | 不可重复读 | 幻读 | 核心机制 | 典型场景 | 线上风险 |
|---|---|---|---|---|---|---|
| READ UNCOMMITTED | ✅ | ✅ | ✅ | 无锁,无MVCC | 仅调试 | 数据错乱,绝对禁用 |
| READ COMMITTED | ❌ | ✅ | ✅ | 每次SELECT新建Read View;仅记录锁 | 高并发扣库存、实时风控 | 同一事务内数据“漂移” |
| REPEATABLE READ | ❌ | ❌ | ⚠️(部分) | 事务启动时建Read View;Next-Key Lock | 报表统计、财务对账 | 间隙锁导致插入阻塞、死锁高发 |
| SERIALIZABLE | ❌ | ❌ | ❌ | 所有SELECT自动转为SELECT ... LOCK IN SHARE MODE | 极端一致性要求(如银行核心) | 性能归零,几乎不用 |
关键洞察:幻读在RR级别并未完全消除。标准定义中,幻读指“同一查询返回行数不同”。但在RR下,如果INSERT新行满足WHERE条件,SELECT仍能看到——这算幻读。InnoDB的解决方式是:对SELECT加临键锁,对INSERT/UPDATE/DELETE也加临键锁。但若WHERE条件无法走索引,锁升级为全表锁,性能灾难。
3.2 线上选型决策树:三步定乾坤
我们团队沉淀出一套隔离级别选型流程,已在5个核心系统验证:
第一步:识别业务语义
- 如果业务要求“绝对不能看到未提交数据”(如支付确认页),排除RU;
- 如果业务允许“同一事务内读到不同版本”(如商品详情页的库存数字,刷新后变),RC足够;
- 如果业务要求“事务内数据绝对静止”(如生成月度销售报表),必须RR。
第二步:评估写冲突模式
- 高频
INSERT(如日志表、消息队列)→ RC(无间隙锁); - 高频
UPDATEon indexed column(如UPDATE user SET points=points+10 WHERE uid=123)→ RC或RR均可,但需确保uid有索引; - 高频
UPDATEon non-indexed column(如UPDATE order SET remark='xxx' WHERE status='pending')→必须加索引!否则RR下间隙锁锁全表。
第三步:压测验证瓶颈
- 在RC下,用
sysbench模拟1000并发UPDATE,观察SHOW ENGINE INNODB STATUS\G中的TRANSACTIONS部分,重点看lock struct(s)数量; - 在RR下,同样压测,对比
LOCK WAIT线程数。若RR下等待线程超RC的3倍,果断切RC。
实操案例:某优惠券系统,原用RR,
SELECT * FROM coupon WHERE status='unused' AND type='cash' LIMIT 1 FOR UPDATE常导致死锁。分析发现status和type无联合索引,RR下对全表加Next-Key Lock。解决方案:创建联合索引(status,type),并将隔离级别降为RC。死锁率从0.8%降至0.002%,TPS提升37%。
4. 索引:B+树的物理世界,不是“建了就快”,而是“建对才快”
4.1 B+树的物理真相:为什么索引能加速,又为何会失效
索引加速的本质,是用空间换时间,用有序结构换随机IO。B+树的物理结构决定了它的能力边界:
- 非叶子节点只存索引键和指针,不存数据,所以树高极低(通常2~4层);
- 叶子节点用双向链表连接,支持高效范围查询;
- 数据行物理存储在聚簇索引的叶子节点,所以主键索引即数据本身。
但B+树的“有序”是脆弱的。当WHERE条件无法利用索引的最左前缀原则时,索引立即失效。例如,对(a,b,c)建联合索引:
WHERE a=1 AND b=2→ 走索引;WHERE a=1 AND c=3→ 只用到a,c失效(b未指定,无法跳到c);WHERE b=2 AND c=3→全表扫描(a未指定,无法定位树根)。
更隐蔽的失效场景:隐式类型转换。user_id是VARCHAR(32),但代码中传入数字123,MySQL会将user_id列隐式转为数字比较,导致索引失效。EXPLAIN显示type: ALL。解决方案:代码中严格保持类型一致,或在SQL中显式CAST。
注意:
LIKE查询中,'%abc'(前导%)必然全表扫描;'abc%'可走索引;'%abc%'则取决于统计信息,MySQL可能认为全表更快而放弃索引。我们线上所有模糊搜索,强制要求前端输入≥2字符,且后端SQL用LIKE 'abc%',绝不接受前导%。
4.2 索引设计黄金法则:从“能用”到“最优”的五步法
我们团队索引设计遵循严格五步法,杜绝“先建再说”:
第一步:抓取真实慢查询
- 开启
slow_query_log,long_query_time=1; - 用
pt-query-digest分析,聚焦Rows_examined远大于Rows_sent的SQL(如Rows_examined=10000, Rows_sent=1)。
第二步:分析执行计划(EXPLAIN)
- 关键看
type:const(主键等值)>ref(非主键等值)>range(范围)>index(全索引扫描)>ALL(全表扫描); key_len:显示实际用到的索引字节数,NULL表示未用索引;Extra:出现Using filesort或Using temporary,说明排序/分组未走索引。
第三步:确定驱动表与连接顺序
JOIN时,永远让结果集最小的表做驱动表。用EXPLAIN FORMAT=JSON看rows估算值;- 对
LEFT JOIN,左表必须是驱动表,右表索引必须覆盖ON和WHERE条件。
第四步:构建联合索引
- 三星索引原则:
① 索引包含所有WHERE条件列(过滤);
② 索引包含所有JOIN和ORDER BY列(连接与排序);
③ 索引包含所有SELECT需要的列(覆盖索引,避免回表)。 - 顺序按区分度(Cardinality)从高到低排列。例如用户表,
uid区分度100%,city区分度0.1%,索引应为(uid, city),而非(city, uid)。
第五步:验证与压测
CREATE INDEX idx_name ON table (col1, col2) ALGORITHM=INPLACE;(MySQL 5.6+支持在线加索引);- 用
SELECT * FROM table WHERE col1=? AND col2=?测试,EXPLAIN确认type=ref; sysbench压测,对比QPS和Innodb_buffer_pool_read_requests(逻辑读)与Innodb_buffer_pool_reads(物理读)比值,理想值>99%。
实操心得:我们曾为一个日志表
(app_id, event_type, create_time)建索引,按区分度排为(app_id, event_type, create_time)。但压测发现WHERE event_type='click' ORDER BY create_time DESC依然慢。原因:event_type区分度低(只有5种值),导致B+树分支少,深度大。最终方案:拆分为两个索引(event_type, create_time)(覆盖排序)和(app_id)(高频过滤),成本增加,但性能提升4倍。
4.3 索引维护:不是建完就结束,而是持续的“体检”
索引会“生病”:
- 数据倾斜:
user_status字段,95%是active,5%是inactive。对status建索引,MySQL可能认为全表扫描更快(因为active行太多); - 统计信息过期:
ANALYZE TABLE未执行,优化器基于过时的行数估算,选错执行计划; - 碎片化:
DELETE大量数据后,B+树叶节点出现空洞,Data_free增大。
我们的索引健康检查清单:
- 每周自动执行
ANALYZE TABLE(在低峰期); - 监控
information_schema.TABLES中DATA_FREE,若>表大小的25%,执行OPTIMIZE TABLE(注意:会锁表); - 用
SELECT COUNT(*) FROM table WHERE col='value'vsSELECT COUNT(*) FROM table,若比值>0.3,该列不适合单独建索引。
5. 视图:不只是“虚拟表”,而是查询重写器与权限防火墙
5.1 视图的底层机制:MERGE算法 vs TEMPTABLE算法
视图不是物化存储,而是查询模板。MySQL执行视图时,有两种算法:
- MERGE(默认):将视图定义与外部查询合并,生成一个新SQL执行。例如视图
v_user_active AS SELECT id,name FROM user WHERE status='active',执行SELECT * FROM v_user_active WHERE id>100,会被重写为SELECT id,name FROM user WHERE status='active' AND id>100。性能好,可走索引。 - TEMPTABLE:先执行视图SQL,结果存入临时表,再对外部条件过滤。
SELECT * FROM v_user_active WHERE id>100会先SELECT id,name FROM user WHERE status='active'生成临时表,再WHERE id>100。性能差,无法利用原表索引。
触发TEMPTABLE的条件(官方文档明确列出):
- 视图中含
DISTINCT、GROUP BY、HAVING、UNION、SUBQUERY; - 视图中含聚合函数(
COUNT()、SUM()); - 视图中含
LIMIT。
提示:
CREATE VIEW v_order_summary AS SELECT user_id, COUNT(*) cnt FROM order GROUP BY user_id,这个视图必然用TEMPTABLE,且无法走order表的索引。正确做法:用物化视图替代(MySQL 8.0+不支持,需用CREATE TABLE ... AS SELECT定期刷新)或应用层聚合。
5.2 视图的权限控制:比GRANT更细粒度的“数据围栏”
视图是权限管理的利器。例如,客服只能看到自己负责的用户订单:
CREATE VIEW v_customer_orders AS SELECT o.id, o.amount, o.status, u.name, u.phone FROM `order` o JOIN user u ON o.user_id = u.id WHERE u.assign_to = CURRENT_USER();然后GRANT SELECT ON v_customer_orders TO 'cs@%'。这样,客服登录后SELECT * FROM v_customer_orders,自动只看到分配给自己的用户数据,且无法绕过——因为WHERE u.assign_to = CURRENT_USER()在视图定义中,用户无法修改。
但要注意:视图的DEFINER权限决定执行时的权限。如果DEFINER='root@localhost',则视图以root权限执行,可能访问用户无权访问的表。我们线上强制SQL SECURITY DEFINER,且DEFINER必须是专用账号(如view_executor),该账号只拥有视图所需表的SELECT权限,绝不给root。
5.3 视图的性能陷阱:那些让你的查询慢10倍的“隐形杀手”
视图最大的坑,是嵌套视图。例如:
CREATE VIEW v1 AS SELECT id, name FROM user WHERE status='active'; CREATE VIEW v2 AS SELECT id, name, COUNT(*) cnt FROM v1 JOIN order o ON v1.id=o.user_id GROUP BY id; CREATE VIEW v3 AS SELECT * FROM v2 WHERE cnt > 10;执行SELECT * FROM v3,MySQL会层层展开,最终SQL可能变成:
SELECT id, name, COUNT(*) cnt FROM (SELECT id, name FROM user WHERE status='active') v1 JOIN order o ON v1.id=o.user_id GROUP BY id HAVING cnt > 10;这个SQL无法利用user表的索引(因为子查询),也无法利用order表的索引(因为JOIN条件在子查询外)。我们线上禁止三层以上视图嵌套,所有复杂逻辑下沉到存储过程或应用层。
实操避坑:某BI系统用视图
v_sales_daily(含SUM()和GROUP BY)做仪表盘,响应时间23秒。EXPLAIN显示type: ALL。解决方案:删除视图,改用定时任务(每小时)执行CREATE TABLE t_sales_daily AS SELECT ...物化结果,仪表盘查t_sales_daily,响应降至0.2秒。物化视图虽牺牲实时性,但换来百倍性能。
6. 常见问题与排查技巧实录:来自凌晨三点的故障笔记
6.1 “加载 web 视图时出错: error: could not register service worker: invalidstatee” —— 这根本不是MySQL问题!
这个错误在热搜词里高频出现,但它100%与MySQL无关。这是前端JavaScript的Service Worker注册失败,常见原因:
- 页面未通过HTTPS访问(Service Worker强制要求HTTPS);
navigator.serviceWorker.register()调用时机错误(如在DOMContentLoaded前);- 浏览器不支持(IE全系、老版Safari)。
为什么会被误认为MySQL问题?因为开发者在调试Web应用时,后端用MySQL,错误日志混在一起。我的建议:遇到前端报错,先打开浏览器DevTools的Console和Network标签页,确认错误来源。不要一看到“web视图”就往数据库上猜。
6.2 “创建视图权限不足” —— 权限检查的五个层级
创建视图失败,不是简单GRANT CREATE VIEW就行。MySQL会检查五层权限:
- 用户账号是否存在:
SELECT User,Host FROM mysql.user WHERE User='xxx'; - CREATE VIEW权限:
SHOW GRANTS FOR 'user'@'host'; - 视图定义中涉及的所有表,用户必须有SELECT权限;
- 如果视图含
ORDER BY,用户还需FILE权限(MySQL Bug,8.0.21已修复); max_connections未超限(视图创建会占用连接)。
排查命令:
-- 查看用户权限 SHOW GRANTS FOR 'app_user'@'%'; -- 查看视图定义涉及的表 SELECT TABLE_NAME FROM information_schema.VIEW_TABLE_USAGE WHERE VIEW_SCHEMA='db_name' AND VIEW_NAME='v_name'; -- 检查该用户对这些表是否有SELECT SELECT TABLE_SCHEMA, TABLE_NAME, PRIVILEGE_TYPE FROM information_schema.ROLE_TABLE_GRANTS WHERE GRANTEE="'app_user'@'%'" AND PRIVILEGE_TYPE='SELECT';6.3 “mysql安装教程”与“mysql下载官网” —— 生产环境安装的硬性红线
网上教程教你怎么在Windows上双击安装包,但生产环境必须:
- Linux用YUM/APT:
yum install mysql-community-server(官方源),禁用tar.gz手动安装(版本管理混乱); - 禁用
mysql_secure_installation的交互式提问:用脚本自动化,mysql -u root -e "ALTER USER 'root'@'localhost' IDENTIFIED WITH mysql_native_password BY 'StrongPass123!';"; my.cnf必须分离配置:[mysqld]下只放核心参数(datadir,socket,port),其他如innodb_buffer_pool_size放在/etc/my.cnf.d/production.cnf,便于环境区分;- 安装后立即执行:
# 创建监控用户 mysql -u root -e "CREATE USER 'monitor'@'localhost' IDENTIFIED BY 'mon123'; GRANT PROCESS, REPLICATION CLIENT ON *.* TO 'monitor'@'localhost';" # 初始化SSL(强制) mysql_ssl_rsa_setup --datadir=/var/lib/mysql
6.4 “pg栏位索引超过许可范围” —— MySQL与PostgreSQL的索引长度陷阱
这个错误属于PostgreSQL(PG),但常被MySQL开发者混淆。PG的索引键长度默认限制为2712字节(B-tree),而MySQL的InnoDB索引长度限制是767字节(utf8mb4)。如果你在MySQL建索引时报Specified key was too long,解决方案:
- 缩短字段长度:
VARCHAR(255)→VARCHAR(191)(因为utf8mb4下1字符占4字节,191×4=764<767); - 降低字符集:
utf8mb4→utf8(但utf8不支持emoji,不推荐); - 使用前缀索引:
INDEX idx_title (title(100)),但前缀索引无法用于ORDER BY和GROUP BY。
我的教训:曾为一个
content TEXT字段建全文索引,ALTER TABLE t ADD FULLTEXT(content),结果mysqld启动失败。原因:FULLTEXT索引要求字段必须有长度限制。解决方案:改用VARCHAR(3000)+ 前缀索引,或迁移到Elasticsearch处理长文本。
6.5 “error 2002 (hy000): can't connect to local mysql server through socket '/tmp/mysql.sock'” —— Socket路径的三重迷宫
这个错误90%是因为Socket路径不匹配。排查步骤:
- 确认MySQL服务是否运行:
systemctl status mysqld或ps aux | grep mysqld; - 查看MySQL实际Socket路径:
# 方法1:登录MySQL后查 mysql -u root -p -e "SHOW VARIABLES LIKE 'socket';" # 方法2:查配置文件 grep "socket" /etc/my.cnf # 方法3:查进程 lsof -i | grep mysqld | grep unix - 客户端指定正确Socket:
# 如果Socket在/var/lib/mysql/mysql.sock mysql -u root -p --socket=/var/lib/mysql/mysql.sock # 或在~/.my.cnf中配置 [client] socket = /var/lib/mysql/mysql.sock
最坑的情况:Docker容器中,宿主机/tmp/mysql.sock映射到容器内/var/run/mysqld/mysqld.sock,但应用代码里硬编码/tmp/mysql.sock。解决方案:所有连接字符串用host=127.0.0.1(走TCP),禁用localhost(走Socket)。
7. 最后分享一个我压箱底的技巧:用INFORMATION_SCHEMA实时诊断索引健康度
不要等慢查询爆发才行动。我每天用这个SQL扫描所有表的索引效率:
SELECT t.TABLE_SCHEMA, t.TABLE_NAME, s.INDEX_NAME, s.COLUMN_NAME, s.SEQ_IN_INDEX, t.TABLE_ROWS, s.CARDINALITY, ROUND(s.CARDINALITY/t.TABLE_ROWS*100,2) AS selectivity_pct, CASE WHEN s.CARDINALITY/t.TABLE_ROWS < 0.01 THEN 'LOW' WHEN s.CARDINALITY/t.TABLE_ROWS < 0.1 THEN 'MEDIUM' ELSE 'HIGH' END AS selectivity_level FROM INFORMATION_SCHEMA.STATISTICS s JOIN INFORMATION_SCHEMA.TABLES t ON s.TABLE_SCHEMA = t.TABLE_SCHEMA AND s.TABLE_NAME = t.TABLE_NAME WHERE t.TABLE_SCHEMA NOT IN ('mysql','information_schema','performance_schema','sys') AND s.INDEX_NAME != 'PRIMARY' AND t.TABLE_ROWS > 1000 ORDER BY selectivity_pct ASC LIMIT 20;这个查询会列出选择度最低的20个索引(selectivity_pct越低,索引越无效)。如果发现user_status索引的选择度只有0.5%,立刻标记为“待清理”。我们每月自动清理选择度<1%的索引,平均减少15%的磁盘IO和5%的Buffer Pool压力。记住,索引不是越多越好,而是越精越好——就像你的工具箱,不需要100把螺丝刀,只需要一把能拧紧所有螺丝的。