站长必避:3个致MySQL半年崩一次的设计陷阱
|
去年一月十八号凌晨两点零七分,MySQL进程挂了第三次——不是告警邮件里那种“CPU飙到98%”的优雅预警,是`mysqld`直接没了,连`systemctl status mysql`都返回`Unit mysql.service could not be found`。运维小哥在钉钉发了个截图:控制台报错里那行`InnoDB: Failing assertion: buf_block_get_state(block) == BUF_BLOCK_FILE_PAGE`底下,还叠着一行我昨天下午六点四十一分手抖提交的记录:`feat: 加个用户标签字段(varchar(255) → text)`。会议室白板没擦干净,上头还留着三天前产品画的ER图,箭头全指向同一个user表,中间打了个叉。 先扔结论:半年崩一次不是偶然,是三处设计动作像三根锈钉,一根钉进索引,一根钉进JOIN,一根钉进事务边界——全卡在业务量刚过临界点、监控还没拉响的时候。你不用查慢日志,也不用看执行计划;你只要在`EXPLAIN FORMAT=JSON`里扫一眼`key_len: 767`,再瞅瞅`rows_examined: 382416`,心里就该有数了:这不是并发高,是表早被自己写瘸了。 第一处,是那个“全站统一用户ID”字段——当初为图省事,所有业务表都加了`user_id VARCHAR(64)`当外键,没建索引,也没强制非空。上周我改活动页,在`order_detail`里加了个`LEFT JOIN user ON order_detail.user_id = user.id`,结果`user.id`是`CHAR(32)`带索引,`order_detail.user_id`却是`VARCHAR(64)`且无索引。`EXPLAIN`里显示`type: ALL`,`key: NULL`。键盘上‘M’键掉了,我敲`ALTER TABLE order_detail ADD INDEX idx_user_id (user_id);`时手指悬空顿了半秒——这索引早就该有,但没人动,因为每次上线都说“先跑着,下个迭代优化”。下个迭代?下个迭代产品经理又塞进来一个“好友关系图谱”,要求实时查出“共同好友的最近三条动态”。
文章配图,仅供参考 第二处,是那个`activity_log`表。字段名看着体面:`id, user_id, activity_type, payload JSON, created_at, updated_at`。问题出在`payload`。设计稿标注里写“存行为上下文”,后端接口文档写“结构化数据”,实际呢?上周导出200万条,一半`payload`里塞的是base64图片,三分之一是序列化的前端Vue组件状态。我本地跑`SELECT FROM activity_log WHERE user_id = 'xxx' ORDER BY created_at DESC LIMIT 10`,不出意外,`Handler_read_rnd_next: 9.2M`。有人端着杯子站了很久,盯着我屏幕右下角的`Performance`面板——那行`MySQL Handler_read_rnd_next`正往12M跳。他问“能拆?”我说“能。但现在要改`SHOW CREATE TABLE`里的`ROW_FORMAT=COMPACT`成`DYNAMIC`,再加`innodb_file_per_table=ON`,再`ALTER TABLE...ROW_FORMAT=DYNAMIC`……然后等三小时。”他放下杯子走了。外卖到了没人去拿,塑料袋在工位旁堆成小山。 第三处,是那个“订单同步中台”的事务块。代码里写着`START TRANSACTION; INSERT INTO order_main; INSERT INTO order_item; UPDATE inventory; COMMIT;`,看似干净。但`inventory`表是另一套DB集群,靠`INSERT DELAYED`+定时任务反向刷,中间夹着MQ重试和幂等校验。问题不在这儿。问题是——`order_item`表的`product_sku_id`字段,当初建表时定义为`VARCHAR(128)`,可中台那边传过来的数据是`product_sku_id` + `tenant_id`拼的,比如`SKU-789-TN2`。我们这边没拆,直接当主键存。于是`SELECT ... FROM order_item WHERE product_sku_id LIKE 'SKU-789%'`这种查询,`key_len`永远踩不满索引。谁在背后走过碰了椅子,发出“哐”一声,我手一抖删掉一句`/ TODO: 拆租户字段 /`的注释。第二天监控告警:`innodb_row_lock_time_avg`从4ms冲到382ms。工位的显示器是竖着的,方便我看`pt-query-digest`输出的长SQL列表,其中第七行就是那个`LIKE`查询,`Rows_examined`后面跟着`1.2e6`。 我没骂人,也没摔键盘。只是把`mysql.slow_log`截了图,发到技术群里,配字:“别再提‘加个字段’‘改个类型’‘顺手优化’了。上次崩是在七月,这次是一月,下次可能就在三月十五号下午三点——那天我们上线春节H5抽奖,页面按钮点了没反应,首屏转圈,控制台一片红。”群里静了五分钟。然后后端老张回:“我改下`activity_log.payload`的写法,只存必要字段。”产品经理没吭声,但第二天晨会,他拿着笔把白板上那个“好友关系图谱”的需求框涂掉了大半,只留下一行小字:“先读本地缓存”。 我现在每晚下班前,`grep -r "ALTER TABLE.user_id" ./migrations/` ——就查这三个字。查到就停,看`up.sql`里有没有`ADD INDEX`,没有就补。补完再跑一遍`pt-online-schema-change --dry-run`,盯着输出里那行`Creating new table... OK`。不为别的,就为下个月不用被叫醒两次。如果哪天又崩了,我知道得先翻`SHOW ENGINE INNODB STATUS\\G`里`TRANSACTIONS`那段,而不是打开微信看是谁先@我。下周一,我把`order_item.product_sku_id`拆成两个字段的事提给DBA,带一份`CREATE TABLE order_item_new`的SQL草案。草案第一页顶头写着:`-- 不要再碰user表了,它已经是个神龛`。 (编辑:站长网) 【声明】本站内容均来自网络,其相关言论仅代表作者个人观点,不代表本站立场。若无意侵犯到您的权利,请及时与联系站长删除相关内容! |


鸿蒙开发者必修:MySQL运维设计实战
MySQL高可用架构实战:14年经验谈自动容灾设计
14年老兵亲授:MySQL慢查询毫秒级优化实战
MySQL事务控制无障碍设计实战指南