开篇:那个让DBA半夜惊醒的下午
我见过太多这样的场景:周五下午四点,运营说“双11预热活动提前了,流量马上进来”,你刚端起咖啡,监控大屏上的QPS从2000瞬间飙到15000,MySQL的CPU直接打到95%,慢查询日志像雪片一样飞,连接数爆满,错误码Too many connections刷屏。这时候,谁教你“优化索引”都是马后炮,因为问题从来不只是“一个SQL写坏了”。
真实生产环境的性能瓶颈,是系统性的。它像一台精密但脆弱的钟表,任何一颗螺丝松动,整个系统都会卡顿。今天,我们不讲教科书式的理论,而是把这台钟拆开,看看里面的齿轮是怎么咬合的,哪里容易卡死,以及如何用真实的策略——从最底层的索引,到中间的连接池,再到顶层的架构——把它修好,甚至让它跑得更快。
第一层:索引优化——别让你的查询在表里“跑步”
索引是MySQL性能优化的基石,也是最容易被误解的地方。很多人以为“加索引就行”,结果加了索引,性能反而下降。为什么?因为索引不是免费的午餐,它需要维护空间、需要时间。
1.1 联合索引的“最左前缀”陷阱
假设你有一张订单表orders,有user_id、status、create_time三个字段。查询场景是:“查询某个用户(user_id=100)在某个状态(status='paid')下的最近10条订单”。
新手会这么建索引:
CREATE INDEX idx_user_status ON orders(user_id, status);
看起来合理?但等一下,如果查询是:
SELECT * FROM orders WHERE user_id = 100 AND status = 'paid' ORDER BY create_time DESC LIMIT 10;
这个查询用得上idx_user_status吗?用得上,但它只能过滤user_id和status,排序还是要全表扫描create_time。这时候,如果你把索引改成:
CREATE INDEX idx_user_status_time ON orders(user_id, status, create_time);
那么,查询就可以利用索引完成过滤和排序,避免filesort。这就是最左前缀原则:索引列的顺序必须匹配查询的WHERE条件和ORDER BY顺序。
真实案例:某电商平台的订单查询,原本需要0.8秒,加上create_time到联合索引末尾后,降到了0.02秒。为什么?因为原本需要回表查10万行数据再排序,现在直接通过索引扫描就能拿到结果。
1.2 覆盖索引:避免“回表”的终极武器
回表,是指通过索引找到主键后,再去聚簇索引里查其他字段。这个过程很贵,尤其是当查询需要返回多个非索引字段时。
覆盖索引的概念是:索引包含了查询所需的所有字段,这样就不需要回表。
例如,查询:
SELECT order_id, status, create_time FROM orders WHERE user_id = 100 AND status = 'paid';
如果你建的索引是:
CREATE INDEX idx_user_status ON orders(user_id, status, order_id, create_time);
那么,这个查询就可以完全通过索引完成,无需回表。在MySQL中,你可以用EXPLAIN看到Extra列显示Using index,这就是覆盖索引的标识。
注意:覆盖索引的字段顺序很重要。user_id和status用于过滤,order_id和create_time用于返回,这样的顺序是最优的。
1.3 索引选择性的艺术
不是所有列都适合建索引。选择性低的列(比如性别gender,只有‘男’和‘女’两个值)建索引意义不大,因为MySQL优化器可能觉得全表扫描更快。
如何判断选择性?用这个公式:
SELECT COUNT(DISTINCT column_name) / COUNT(*) AS selectivity FROM table_name;
选择性越接近1,索引效果越好。对于性别这种列,选择性可能只有0.5,优化器可能会跳过索引。
真实建议:对于高并发的生产环境,优先在选择性高、查询频繁、过滤条件多的列上建索引。同时,定期用SHOW INDEX FROM table_name检查索引使用情况,删除未被使用的索引,因为它们会增加写入开销。
第二层:连接与事务——别让“握手”成为瓶颈
高并发下,MySQL的连接数往往是第一个崩溃点。每个连接都需要内存、CPU上下文切换,以及事务管理的开销。
2.1 连接池:拒绝“短连接”的野蛮生长
短连接,是指每次查询都新建连接、用完就关。这在低并发时没问题,但在高并发下,连接建立和关闭的开销会拖垮数据库。
解决方案:使用连接池。在应用层,使用像HikariCP、Druid这样的连接池,复用连接。
// HikariCP配置示例
HikariConfig config = new HikariConfig();
config.setJdbcUrl("jdbc:mysql://localhost:3306/mydb");
config.setUsername("user");
config.setPassword("pass");
config.setMaximumPoolSize(20); // 最大连接数
config.setMinimumIdle(5); // 最小空闲连接
config.setIdleTimeout(30000); // 空闲超时30秒
config.setMaxLifetime(600000); // 最大生存时间10分钟
连接池的核心思想是:预创建连接,复用连接,避免频繁创建和销毁。
2.2 事务粒度:越小越好
高并发下,事务持有锁的时间越长,并发冲突就越严重。因此,事务应该尽可能短,只包含必要的SQL操作。
反例:
BEGIN;
SELECT * FROM user WHERE id = 1 FOR UPDATE; -- 加锁
// 做一些复杂计算,耗时10秒
UPDATE user SET balance = balance - 100 WHERE id = 1;
COMMIT;
这个事务持锁10秒,其他需要修改同一行的事务都要排队。
正例:
BEGIN;
SELECT balance FROM user WHERE id = 1; -- 只读,不加锁
// 计算
UPDATE user SET balance = balance - 100 WHERE id = 1;
COMMIT;
或者,使用乐观锁(版本号)代替悲观锁:
UPDATE user SET balance = balance - 100, version = version + 1
WHERE id = 1 AND version = 5;
这样,事务只需持有锁一瞬间,并发性能大幅提升。
2.3 慢查询日志:找到真正的“慢”
高并发下,不是所有慢查询都是问题。有些查询偶尔慢,可能是因为数据倾斜;有些查询一直慢,才是真问题。
开启慢查询日志:
SET GLOBAL slow_query_log = 'ON';
SET GLOBAL long_query_time = 2; -- 超过2秒的查询记录
然后,用mysqldumpslow分析:
mysqldumpslow -s t -t 10 /var/log/mysql/slow.log
这会列出执行时间最长的10条SQL,让你优先优化真正影响性能的部分。
第三层:架构升级——读写分离与分库分表
当单库性能遇到瓶颈,索引优化已经到头时,就需要架构层面的升级。
3.1 读写分离:把读压力分散出去
MySQL的主从复制是读写分离的基础。主库负责写入,从库负责读取。这样,读压力就被分散到多个从库上。
架构示意:
应用 --> 主库(写) --> 从库1(读)
--> 从库2(读)
--> 从库3(读)
关键点:
- 主从延迟:从库复制主库的数据有延迟,可能导致读到的数据不是最新的。解决方案:对于强一致性的查询,强制读主库;对于最终一致性的查询,读从库。
- 连接路由:在应用层,根据SQL类型(SELECT/UPDATE)自动路由到主库或从库。可以使用中间件如MyCat、ShardingSphere,或者在代码中手动判断。
真实案例:某社交平台,用户信息读取量远大于写入量。实施读写分离后,主库QPS从5000降到1000,从库分担了大部分读取压力,系统整体响应时间下降了60%。
3.2 分库分表:突破单机瓶颈
当单表数据量超过千万级,或者单库QPS超过2万,分库分表就成为必选项。
垂直分库:按业务模块拆分。例如,用户库、订单库、商品库分开,各自独立。这样,每个库的数据量和压力都更小。
水平分表:按数据范围或哈希拆分。例如,订单表按user_id % 100拆分成100张表,分散到多个库中。
ShardingSphere示例(使用Spring Boot):
@Configuration
public class ShardingConfig {
@Bean
public DataSource dataSource() throws SQLException {
ShardingRuleConfiguration shardingRuleConfig = new ShardingRuleConfiguration();
// 配置订单表的分片策略
TableRuleConfiguration orderTableRule = new TableRuleConfiguration("t_order", "ds0.t_order_$->{0..1}");
orderTableRule.setDatabaseShardingStrategyConfig(new StandardShardingStrategyConfiguration(
"user_id", new OnlineShardingTableAlgorithm()
));
shardingRuleConfig.getTableRuleConfigs().add(orderTableRule);
// 配置数据源
Map<String, DataSource> dataSourceMap = new HashMap<>();
dataSourceMap.put("ds0", createDataSource());
return ShardingDataSourceFactory.createDataSource(dataSourceMap, shardingRuleConfig, new Properties());
}
}
注意事项:
- 跨分片查询:尽量避免,性能会很差。如果必须,使用广播表或关联查询。
- 分片键选择:分片键应该是查询中常用的过滤条件,避免跨分片扫描。
- 扩容复杂度:分库分表后,扩容需要迁移数据,成本高。建议在设计初期就考虑好分片策略。
3.3 缓存层:Redis是最后的防线
在高并发场景下,数据库不是唯一的瓶颈,磁盘I/O和内存访问也是。引入缓存层,可以将热点数据留在内存中,大幅减少数据库压力。
缓存策略:
- Cache-Aside:先读缓存,缓存没有则读数据库,再写入缓存。
- Write-Through:写入时同时更新缓存。
- Write-Behind:写入时只更新缓存,异步刷新到数据库。
Redis示例:
// 查询用户信息
public User getUser(long userId) {
String key = "user:" + userId;
User user = redisTemplate.opsForValue().get(key);
if (user == null) {
user = userDao.selectById(userId);
if (user != null) {
redisTemplate.opsForValue().set(key, user, 10, TimeUnit.MINUTES);
}
}
return user;
}
关键点:缓存击穿(热点key过期)、缓存穿透(查询不存在的数据)、缓存雪崩(大量key同时过期)都需要应对策略。
第四层:监控与调优——让性能可视化
高并发系统最怕的是“黑盒”。你不知道哪里出了问题,只能靠运气。因此,监控和调优是持续的过程。
4.1 关键指标监控
- QPS/TPS:每秒查询/事务数,反映系统负载。
- 连接数:当前连接数、最大连接数,连接数接近上限时预警。
- 慢查询数:每天慢查询的数量趋势,异常增长需排查。
- CPU/内存使用率:数据库服务器的资源使用情况。
- 主从延迟:从库复制延迟秒数,超过阈值需告警。
Prometheus + Grafana示例:
# prometheus.yml
scrape_configs:
- job_name: 'mysql'
static_configs:
- targets: ['mysql-exporter:9104']
通过导出器(如mysqld_exporter)将MySQL指标暴露给Prometheus,再用Grafana可视化,你可以实时看到QPS曲线、慢查询分布、连接池使用情况等。
4.2 性能调优参数
MySQL的配置文件my.cnf中,有几个关键参数:
[mysqld]
# 连接相关
max_connections = 1000 # 最大连接数,根据应用需求调整
wait_timeout = 600 # 空闲连接超时时间,避免连接堆积
# 缓存相关
innodb_buffer_pool_size = 8G # InnoDB缓冲池大小,建议设为物理内存的50-70%
query_cache_size = 0 # MySQL 8.0已移除查询缓存,不推荐启用
# 日志相关
slow_query_log = 1 # 开启慢查询日志
long_query_time = 2 # 慢查询阈值
log_queries_not_using_indexes = 1 # 记录未使用索引的查询
# 主从复制
server-id = 1 # 主库ID
log-bin = mysql-bin # 开启二进制日志
调优原则:没有银弹,需要根据实际负载调整。建议先 baseline,再逐一调整参数,观察效果。
结语:高并发是一场持久战
处理MySQL高并发,不是一蹴而就的。它需要你:
- 理解业务:哪些查询是热点?哪些数据是敏感的?
- 优化索引:让查询跑得更快,减少回表。
- 管理连接:避免短连接,控制事务粒度。
- 架构升级:读写分离、分库分表、引入缓存。
- 持续监控:用数据说话,及时发现异常。
记住,性能优化是一个迭代过程。每一次优化,都要有度量、有验证。不要为了优化而优化,而是为了解决真实的生产问题。
希望这篇分享能帮你理清思路。高并发不可怕,可怕的是没有策略。从今天开始,检查你的慢查询日志,看看你的索引是否合理,考虑一下是否需要读写分离。一步步来,你的MySQL会感谢你的。
