说实话,做后端开发的谁没经历过那种“深夜报警铃响,打开监控一看QPS飙到天际,MySQL连接数直接打满,CPU 100%”的恐怖时刻?尤其是大促或者活动上线的时候,订单系统一旦超时,用户点不进去,运营那边电话能打到爆。
今天咱们不整那些虚头巴脑的概念,就聊聊当MySQL在高并发下快要“炸裂”的时候,我们到底该怎么做。我会从一个真实的事故复盘角度,把连接池、索引优化、读写分离以及性能瓶颈排查,掰开了揉碎了讲给你听。
第一阶段:当订单开始超时,你的第一反应是什么?
假设你现在是一家电商公司的后端负责人。突然,客服反馈大量用户投诉:“提交订单后一直转圈,最后提示失败。”与此同时,你的监控大屏上,订单服务的响应时间从正常的200ms飙升到了5秒以上,甚至有部分请求直接超时。
这时候,千万别慌,更别急着重启数据库——重启可能缓解一时,但下次流量再来照样崩,而且重启过程中的短暂中断可能让情况更糟。
首先,你需要确认:是MySQL挂了,还是应用层挂了?
很多时候,用户感知的“慢”,根源并不在数据库。比如:
- 检查应用日志:看是否有大量的
SocketTimeoutException或者Connection pool exhausted错误。如果连接池满了,说明应用获取不到数据库连接,这往往意味着数据库处理不过来。 - 检查数据库状态:登录MySQL,执行
SHOW PROCESSLIST;。你会看到一堆状态为Sending data、Locked或者Query end的慢查询。如果Threads_connected接近你设置的最大连接数,那基本上可以确定是数据库连接成为瓶颈。 - 检查系统资源:用
top或htop看看CPU、内存、IO等待(iowait)。如果CPU高但IO低,可能是复杂查询或锁竞争;如果IO高,可能是磁盘读写瓶颈。
在这个阶段,最快的止血办法往往是:临时增加连接池大小,或者降级非核心功能。比如,暂停推荐商品的加载,只保留核心的下单逻辑,减少数据库的压力。
第二阶段:连接池优化——别让连接成为瓶颈
连接池,是高并发架构中的“阀门”。如果阀门太小,水流不过来;阀门太大,又会导致数据库端连接数爆炸,上下文切换开销巨大。
1. 常见的连接池配置误区
很多团队在初始化连接池时,直接用了框架的默认值,或者凭感觉设置。比如,Spring Boot 默认的 HikariCP 连接池最大连接数可能只有10个,这对于高并发场景简直是灾难。
错误示范:
spring:
datasource:
hikari:
maximum-pool-size: 10 # 默认值,在高并发下远远不够
minimum-idle: 5
connection-timeout: 30000
2. 如何科学设置连接池大小?
连接池大小的设置,没有银弹,但有一个经验公式可以参考:
最佳连接数 ≈ (CPU核心数 * 2) + 有效磁盘数
但这只是起点。更准确的方式是通过压力测试来确定。你可以逐步增加 maximum-pool-size,观察响应时间和吞吐量。当响应时间开始急剧上升,而吞吐量不再增加时,就说明连接数过多了。
优化示例:
spring:
datasource:
hikari:
maximum-pool-size: 50 # 根据服务器配置调整,比如8核16G服务器
minimum-idle: 10
idle-timeout: 600000 # 10分钟无使用则释放连接
max-lifetime: 1800000 # 连接最大存活时间30分钟
connection-timeout: 5000 # 获取连接超时时间5秒
leak-detection-threshold: 60000 # 检测连接泄漏,60秒未归还则报警
关键点:
leak-detection-threshold:这个参数非常有用。如果设置得当,它可以帮你发现代码中忘记关闭连接的问题,避免连接泄漏导致连接池枯竭。max-lifetime:建议设置为略小于数据库端的wait_timeout,避免应用端持有已失效的连接。
3. 连接池不够用时怎么办?
如果调大了连接池,数据库还是扛不住,那说明问题不在连接池,而在SQL本身或者数据库性能。这时候,你需要考虑读写分离和分库分表。
第三阶段:索引瘦身——让查询快如闪电
在高并发场景下,最致命的不是大表,而是没有索引的查询或者索引失效的查询。
1. 常见的索引问题
问题一:过度索引
很多开发同学觉得,加索引总是好的。于是,一张表上加了十几个索引。结果呢?每次插入、更新、删除,数据库都要维护这些索引,导致写性能大幅下降。在高并发的订单系统中,写操作频繁,过多的索引会让写入成为瓶颈。
问题二:索引失效
这是最常见的问题。比如,你对 created_time 字段加了索引,但查询时用了函数:
-- 索引失效!
SELECT * FROM orders WHERE YEAR(created_time) = 2023;
或者,对字段进行了隐式类型转换:
-- 假设 phone 是字符串类型,但查询时传了数字
SELECT * FROM users WHERE phone = 13800138000;
这些都会导致全表扫描,在高并发下,几十个并发全表扫描,数据库直接跪。
2. 索引优化实战
步骤一:使用 EXPLAIN 分析慢查询
当发现慢查询时,先用 EXPLAIN 看看执行计划:
EXPLAIN SELECT * FROM orders WHERE user_id = 12345 AND status = 1;
关注以下几点:
type:最好是ref或range,避免ALL(全表扫描)。key:实际使用的索引。rows:扫描的行数,越少越好。Extra:如果出现Using filesort或Using temporary,说明有性能问题。
步骤二:遵循最左前缀原则
对于联合索引 (a, b, c),查询条件必须包含 a,否则索引失效。比如:
-- 索引有效
SELECT * FROM orders WHERE user_id = 123 AND status = 1;
-- 索引可能部分有效,但不会用到 status 的索引部分
SELECT * FROM orders WHERE status = 1;
步骤三:覆盖索引
如果查询的字段都在索引中,MySQL可以直接从索引中获取数据,无需回表,效率极高。
-- 假设有一个联合索引 (user_id, status, order_id)
SELECT order_id FROM orders WHERE user_id = 123 AND status = 1;
这个查询就可以使用覆盖索引,避免回表。
步骤四:删除冗余索引
定期检查索引使用情况,删除从未被使用过的索引。可以通过 sys.schema_unused_indexes 视图来查看(MySQL 5.7+)。
第四阶段:死锁爆表——高并发的噩梦
在高并发下,死锁(Deadlock)是order系统最常见的问题之一。尤其是当多个订单同时更新同一商品库存时,很容易产生死锁。
1. 死锁产生的经典场景
假设你有两张表:orders(订单表)和 inventory(库存表)。
事务A:
- 更新订单
order_id=1的状态。 - 更新商品
product_id=100的库存。
事务B:
- 更新订单
order_id=2的状态。 - 更新商品
product_id=100的库存。
如果事务A先锁住了 product_id=100,事务B也想锁 product_id=100,但发现事务A已经持有,于是等待。同时,事务A在执行第二步时,发现事务B已经锁住了它需要的行,也进入等待。这就形成了死锁。
2. 如何排查和解决死锁?
排查步骤:
查看死锁日志:
SHOW ENGINE INNODB STATUS;在
LATEST DETECTED DEADLOCK部分,你会看到详细的死锁信息,包括涉及的SQL、锁定的行等。开启死锁日志记录: 在MySQL配置文件中设置:
innodb_deadlock_detect = ON innodb_lock_wait_timeout = 50
解决方案:
- 统一加锁顺序:确保所有事务都以相同的顺序访问资源。比如,总是先更新订单,再更新库存。
- 减小事务范围:尽量让事务简短,快速提交。不要在事务中做太多无关的操作。
- 使用
SELECT ... FOR UPDATE时谨慎:如果必须加锁,尽量只锁需要的行,而不是整张表。 - 重试机制:对于死锁,最好的办法是捕获异常并重试。比如:
try { // 执行业务逻辑 } catch (DeadlockLoserDataAccessException e) { log.warn("Deadlock detected, retrying..."); // 重试逻辑 }
第五阶段:读写分离——分担数据库压力
当单台MySQL扛不住读压力时,读写分离是最常见的解决方案。原理很简单:主库负责写,从库负责读。通过复制机制,从库同步主库的数据。
1. 读写分离的配置
以MySQL主从复制为例:
主库配置:
[mysqld]
server-id=1
log-bin=mysql-bin
binlog-format=ROW
从库配置:
[mysqld]
server-id=2
relay-log=relay-log
然后,在从库上执行:
CHANGE MASTER TO
MASTER_HOST='主库IP',
MASTER_USER='复制用户',
MASTER_PASSWORD='密码',
MASTER_LOG_FILE='mysql-bin.000001',
MASTER_LOG_POS=154;
START SLAVE;
2. 应用层如何实现读写分离?
在Spring Boot中,可以通过配置多个数据源来实现:
@Configuration
public class DataSourceConfig {
@Bean
@Primary
@ConfigurationProperties("spring.datasource.master")
public DataSource masterDataSource() {
return DataSourceBuilder.create().build();
}
@Bean
@ConfigurationProperties("spring.datasource.slave")
public DataSource slaveDataSource() {
return DataSourceBuilder.create().build();
}
@Bean
public AbstractRoutingDataSource routingDataSource() {
Map<Object, Object> targetDataSources = new HashMap<>();
targetDataSources.put("master", masterDataSource());
targetDataSources.put("slave", slaveDataSource());
AbstractRoutingDataSource routingDataSource = new AbstractRoutingDataSource() {
@Override
protected Object determineCurrentLookupKey() {
return DataSourceContextHolder.getDataSourceType();
}
};
routingDataSource.setTargetDataSources(targetDataSources);
return routingDataSource;
}
}
关键点:
- 主从延迟:读写分离最大的问题是主从延迟。用户刚写完数据,立刻去读,可能读不到最新数据。解决方案包括:强制读主库、缩短主从延迟、或者使用binlog解析实现近实时的数据同步。
- 故障转移:需要从库自动切换为主库的机制,比如使用MHA或Keepalived。
第六阶段:性能瓶颈综合排查——不只是数据库
有时候,MySQL慢,不一定是数据库的问题。可能是应用层、网络层或者操作系统的问题。
1. 排查工具
top:查看CPU和内存使用情况。iostat:查看磁盘IO使用情况。vmstat:查看系统整体资源使用情况。netstat:查看网络连接情况。mysqldumpslow:分析慢查询日志,找出最慢的SQL。
2. 常见瓶颈及解决方案
| 瓶颈 | 表现 | 解决方案 |
|---|---|---|
| CPU高 | top 显示CPU使用率接近100% |
优化SQL,避免复杂计算;增加索引;升级CPU |
| IO高 | iostat 显示 %util 接近100% |
增加SSD;优化查询,减少磁盘读写;使用缓存 |
| 内存不足 | top 显示内存使用率高 |
增加内存;调整MySQL缓存参数;优化查询,减少临时表 |
| 网络瓶颈 | 延迟高,带宽打满 | 优化网络;使用本地机房;压缩数据传输 |
结语:高并发下的数据库运维心法
最后,我想说的是,高并发下的MySQL优化,不是一蹴而就的,而是一个持续的过程。你需要:
- 监控先行:建立完善的监控体系,及时发现异常。
- 预案充分:制定详细的应急预案,知道什么时候该做什么。
- 持续优化:定期review慢查询,优化索引,调整参数。
- 架构演进:随着业务发展,适时引入分库分表、缓存、消息队列等技术。
记住,最好的优化,是在问题发生之前。所以,平时多做一些压测,多了解一些理论知识,关键时刻才能从容应对。
希望这篇文章能帮你在高并发的战场上,少掉几根头发。如果还有具体问题,欢迎随时交流!
