1. MySQL那些年我们踩过的坑
从事数据库相关工作十多年,我见过太多团队在MySQL使用上栽跟头。有些错误就像定时炸弹,平时运行良好,一旦爆发就会造成灾难性后果。今天我们就来盘点那些最容易踩中的MySQL雷区,这些经验都是用真金白银的线上事故换来的。
2. 字符集与排序规则的隐形陷阱
2.1 字符集不一致导致的乱码问题
我见过最典型的案例是某电商平台用户昵称出现"???"乱码。排查发现应用层使用utf8而MySQL表是latin1,当用户输入emoji或生僻字时,数据直接损坏。解决方案:
-- 建表时显式指定字符集 CREATE TABLE users ( id INT PRIMARY KEY, name VARCHAR(100) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;关键点:永远使用utf8mb4而非utf8,前者支持完整的Unicode字符(包括emoji)
2.2 排序规则引发的查询异常
某次订单列表出现"iPhone 12"排在"iPhone 11"前面的诡异现象,原因是使用了utf8mb4_general_ci排序规则。改为utf8mb4_unicode_ci后解决:
-- 修改现有表的排序规则 ALTER TABLE products CONVERT TO CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci;3. 索引使用的经典误区
3.1 最左前缀原则的误解
开发团队曾抱怨"明明加了索引却没用",检查发现查询条件不符合最左前缀原则:
-- 复合索引 (a,b,c) SELECT * FROM table WHERE b = 1 AND c = 2; -- 无法使用索引正确做法是确保查询条件包含最左列:
SELECT * FROM table WHERE a = 1 AND b = 2; -- 能使用索引3.2 隐式类型转换导致索引失效
线上日志表突然变慢,发现是如下查询:
SELECT * FROM logs WHERE user_id = '10086'; -- user_id是INT类型字符串与数字比较导致全表扫描。解决方案:
SELECT * FROM logs WHERE user_id = 10086; -- 保持类型一致4. 事务隔离级别的坑
4.1 重复读导致的幻读问题
财务系统出现账户余额对不上的情况,原因是REPEATABLE READ隔离级别下:
-- 事务1 SELECT SUM(amount) FROM transactions WHERE account_id = 1; -- 返回1000 -- 事务2插入新记录 INSERT INTO transactions VALUES (1, 500); -- 事务1再次查询 SELECT SUM(amount) FROM transactions WHERE account_id = 1; -- 仍然返回1000解决方案是使用SERIALIZABLE或加间隙锁:
SELECT * FROM transactions WHERE account_id = 1 FOR UPDATE;4.2 长事务引发的锁等待
某次促销活动数据库连接爆满,发现是前端某个查询忘了关闭事务:
// 错误示例 connection.setAutoCommit(false); ResultSet rs = statement.executeQuery("SELECT * FROM products"); // 忘记commit或rollback经验法则:事务代码必须放在try-catch-finally块中确保释放
5. 表设计中的反模式
5.1 滥用ENUM类型
某用户属性表需要新增选项,但ENUM类型修改需要重建表:
-- 初始设计 CREATE TABLE users ( gender ENUM('male','female') ); -- 需要增加'other'选项导致锁表 ALTER TABLE users MODIFY gender ENUM('male','female','other');建议改用关联表或TINYINT:
CREATE TABLE gender_types ( id TINYINT PRIMARY KEY, name VARCHAR(10) ); INSERT INTO gender_types VALUES (1,'male'),(2,'female'),(3,'other');5.2 无限制的TEXT字段
商品描述表占用了80%的磁盘空间,发现开发人员把所有文本都塞进了LONGTEXT。优化方案:
-- 将大文本分离到单独表 CREATE TABLE product_descriptions ( product_id INT PRIMARY KEY, content TEXT, FULLTEXT INDEX (content) ) ENGINE=InnoDB;6. 配置参数的血泪教训
6.1 innodb_buffer_pool_size设置不当
某次服务器升级后性能反而下降,发现是buffer pool配置问题:
# 错误配置(使用默认值) innodb_buffer_pool_size = 128M # 正确做法(建议设为物理内存的70-80%) innodb_buffer_pool_size = 12G6.2 max_connections的陷阱
突发流量导致数据库连接耗尽,检查发现:
SHOW VARIABLES LIKE 'max_connections'; -- 默认151但更危险的是连接数暴增可能耗尽内存。应该配合连接池使用:
// HikariCP配置示例 HikariConfig config = new HikariConfig(); config.setMaximumPoolSize(50); // 远小于数据库max_connections7. 备份恢复的黑暗时刻
7.1 没有验证备份有效性
某次主库宕机后,发现备份已经损坏三个月。现在我们的检查流程:
# 备份时立即验证 mysqldump -u root -p dbname > backup.sql mysql -u root -p -e "USE dbname; SHOW TABLES;" < backup.sql7.2 大表ALTER TABLE操作
给亿级用户表加索引导致服务不可用,现在使用pt-online-schema-change:
pt-online-schema-change \ --alter "ADD INDEX idx_email (email)" \ D=testdb,t=users \ --execute8. 监控盲区的惨痛代价
8.1 忽略慢查询日志
直到用户投诉才发现某些查询执行超过10秒。现在我们的配置:
slow_query_log = 1 long_query_time = 1 log_queries_not_using_indexes = 18.2 没有监控复制延迟
从库同步延迟3小时未被发现,导致故障切换时数据丢失。现在使用:
SHOW SLAVE STATUS\G -- 检查Seconds_Behind_Master9. SQL优化的经典案例
9.1 COUNT(*)的性能谜题
某报表页面超时,原来是:
SELECT COUNT(*) FROM orders WHERE create_time > '2023-01-01'; -- 扫描500万行优化方案:
-- 使用估算值 EXPLAIN SELECT COUNT(*) FROM orders WHERE create_time > '2023-01-01'; -- 或维护计数表 CREATE TABLE order_stats ( date DATE PRIMARY KEY, count INT );9.2 LIMIT分页的深分页问题
翻页到第100页时超时:
SELECT * FROM products ORDER BY id LIMIT 10000, 20; -- 需要读取10020行优化方案:
SELECT * FROM products WHERE id > 10000 ORDER BY id LIMIT 20;10. 高可用架构的隐藏风险
10.1 主从切换的数据一致性
某次故障切换后,发现从库缺失部分数据。现在我们会:
-- 切换前检查 SHOW MASTER STATUS; SHOW SLAVE STATUS\G -- 使用GTID确保数据一致性 gtid_mode = ON enforce_gtid_consistency = ON10.2 云数据库的跨区延迟
使用云数据库时,应用服务器与数据库不在同一可用区,导致平均延迟增加15ms。解决方案:
# 应用端配置 spring.datasource.hikari.connection-timeout=30000 spring.datasource.hikari.maximum-pool-size=20这些经验教训告诉我们,MySQL的很多问题都是"温水煮青蛙"——平时不显山露水,一旦爆发就是大事故。最好的防御措施是建立完善的监控体系,定期进行故障演练,以及最重要的:保持对数据库的敬畏之心。