你有没有经历过那种“周五下午五点,业务方突然说今晚可能要冲一波流量”的绝望时刻?或者更惨,半夜三点手机狂响,报警群炸了,核心DB CPU飙到100%,连接数直接熔断,整个系统瞬间雪崩。
我是Agnes,见过太多这样的案例。今天不跟你讲那些教科书上干巴巴的理论,咱们直接聊聊在淘宝双11这种千万级QPS(每秒查询率)的极端场景下,MySQL是怎么做到“不死”的。哪怕你的流量没有双11那么夸张,里面的套路也能让你少走很多弯路。
一、 先认清敌人:为什么MySQL会“撑爆”?
很多人以为数据库崩了就是“数据太多”或者“配置太差”。其实,90%的线上故障,根因都不是硬件瓶颈,而是架构设计和代码问题。
在双11的峰值时刻,MySQL面临的是三重夹击:
- 连接数爆炸:成千上万个业务线程同时涌入,试图建立数据库连接。如果每个请求都新建连接,MySQL的上下文切换开销足以把它压垮。
- 磁盘IO瓶颈:热点数据不在内存里,每次查询都要去磁盘读,而磁盘的随机读写性能远低于内存。
- 慢SQL累积:一条没走索引的全表扫描,在正常流量下可能只卡0.5秒,但在高并发下,它会迅速吃掉所有连接资源,形成“长尾效应”,拖死整个集群。
所以,优化 MySQL 高并发,本质上是在做资源隔离和流量削峰。
二、 第一道防线:缓存架构——把流量挡在数据库门外
这是最重要的一环。请记住一个原则:能不进数据库,就绝对不进。
2.1 多级缓存策略
在淘宝的实战中,他们采用的不是单一的缓存,而是多级缓存:
- 本地缓存(Caffeine/Guava):数据量小、读取频率极高、一致性要求不那么严苛的数据(如配置项、热点商品ID)。响应时间在微秒级,彻底不经过网络。
- 分布式缓存(Redis Cluster):存储海量热点数据。这是主战场。
- 数据库缓存(InnoDB Buffer Pool):MySQL 自身的内存缓存,用于兜底。
2.2 双11的实战技巧:缓存预热与穿透防护
很多系统上线后崩溃,是因为缓存穿透。黑客或者异常请求不断查询一个不存在的数据,导致每个请求都打到数据库。
解决方案:
- 布隆过滤器(Bloom Filter):在缓存层之前加一层布隆过滤器,判断 key 是否存在。如果不存在,直接返回空,不查数据库。
- 缓存空值:如果确定数据不存在,也缓存一个短 TTL(如30秒)的空对象,防止反复穿透。
缓存预热: 在双11开始前,技术团队会提前把可能成为热点的商品数据加载到 Redis 中。这就像战前囤粮,避免流量高峰时大量请求同时穿透到 DB,造成“缓存击穿”。
2.3 代码示例:带空值缓存的 Redis 工具类
public class RedisCacheService {
@Autowired
private RedisTemplate<String, Object> redisTemplate;
private static final long EMPTY_CACHE_TTL = 30; // 空值缓存30秒,防止穿透
public Object getFromCache(String key) {
Object value = redisTemplate.opsForValue().get(key);
// 如果拿到的是特殊标记,说明数据不存在,直接返回空
if (value instanceof NullObjectMarker) {
return null;
}
return value;
}
public void setWithExpire(String key, Object value, long ttl) {
if (value == null) {
// 缓存空对象,防止缓存穿透
redisTemplate.opsForValue().set(key, new NullObjectMarker(), EMPTY_CACHE_TTL, TimeUnit.SECONDS);
} else {
redisTemplate.opsForValue().set(key, value, ttl, TimeUnit.SECONDS);
}
}
// 内部类,作为空值标记
private static class NullObjectMarker {}
}
三、 连接池调优:别让连接数成为瓶颈
即使有了缓存,仍会有大量请求落到 MySQL。这时候,连接池就成了关键的资源控制器。
3.1 连接池的工作原理
应用服务器不会每次请求都新建数据库连接,而是维护一个连接池。请求来时,从池中借一个连接;请求完,归还。
常见配置参数:
maxTotal:最大连接数。maxIdle:最大空闲连接数。minIdle:最小空闲连接数。maxWaitMillis:获取连接的最大等待时间。
3.2 双11的坑:连接池配多大?
很多开发者的直觉是:“连接数越大越好”。错!
连接数过大,会导致 MySQL 服务器上下文切换开销剧增,反而降低吞吐量。MySQL 本身的 max_connections 默认是 151,在高并发下远远不够,但也不能无限调大。
实战建议:
- 计算合理连接数:根据业务公式估算。例如,假设每个连接平均处理请求时间为 50ms,QPS 为 1000,那么需要的连接数约为
1000 * 0.05 = 50。再加上缓冲,设置maxTotal为 100-200 通常是安全的。 - 设置超时时间:一定要配置
maxWaitMillis。如果 500ms 内拿不到连接,直接抛出异常或降级,而不是无限等待,占用线程资源。 - 区分读写连接池:对于写多读少或读写分离的场景,可以考虑将读写连接池分开,避免写操作占用读连接资源。
3.3 连接池监控与动态调整
使用 Druid 或 HikariCP 等成熟的连接池,它们都提供了丰富的监控指标。务必接入监控平台(如 Prometheus + Grafana),实时观察:
- 活跃连接数
- 等待获取连接的线程数
- 连接获取平均耗时
一旦发现“等待线程数”持续上升,说明连接池太小或下游 DB 响应变慢,需要立即扩容或排查慢 SQL。
四、 SQL 慢查询治理:消灭性能杀手
这是最基础,也最容易出问题的地方。一条烂 SQL,足以拖垮整个集群。
4.1 如何发现慢 SQL?
MySQL 内置了慢查询日志(Slow Query Log)。
-- 开启慢查询日志
SET GLOBAL slow_query_log = 'ON';
-- 设置慢查询阈值(例如超过 1 秒的记录)
SET GLOBAL long_query_time = 1;
-- 指定日志文件位置
SET GLOBAL slow_query_log_file = '/var/log/mysql/slow.log';
在生产环境,不要直接改全局配置,建议通过 MySQL 配置项 my.cnf 永久生效,或者使用 pt-query-digest 等工具分析日志。
4.2 优化三部曲:索引、查询结构、执行计划
1. 索引优化
- 最左前缀法则:联合索引
(a, b, c),查询条件必须包含a才能生效。 - 避免索引失效:不要在索引列上做计算、函数操作,或使用
OR连接非索引列。 - 覆盖索引:尽量只查询索引列,避免回表。例如
SELECT id FROM user WHERE name = 'xxx'比SELECT * FROM user WHERE name = 'xxx'快得多。
2. 查询结构优化
避免
SELECT *:只查需要的列,减少 IO 和网络传输。分页优化:
LIMIT 1000000, 10这种深分页非常慢。解决方案是使用“游标法”或子查询优化:-- 慢查询 SELECT * FROM orders LIMIT 1000000, 10; -- 优化后 SELECT * FROM orders WHERE id > 1000000 ORDER BY id ASC LIMIT 10;批量操作:将多条单条 INSERT/UPDATE 合并为批量操作,减少网络交互次数。
3. 使用 EXPLAIN 分析执行计划
每优化一条 SQL,必须先 EXPLAIN 查看执行计划:
type:是否为ref或range级别,避免ALL(全表扫描)。key:实际使用的索引。rows:扫描的行数,越少越好。Extra:是否有Using filesort或Using temporary,这些都需要避免。
4.3 代码示例:ORM 框架中的 SQL 优化
如果你使用 MyBatis 或 JPA,务必注意生成的 SQL。
<!-- 错误的写法:导致全表扫描 -->
<select id="findUsers" resultType="User">
SELECT * FROM user WHERE YEAR(create_time) = 2023
</select>
<!-- 正确的写法:利用索引范围查询 -->
<select id="findUsers" resultType="User">
SELECT id, name, status FROM user
WHERE create_time >= '2023-01-01 00:00:00'
AND create_time < '2024-01-01 00:00:00'
</select>
注意:这里去掉了 SELECT *,并避免了在索引列上使用函数,改为范围查询。
五、 架构级兜底:当 MySQL 真的撑不住时
即使做了上述所有优化,极端流量下仍可能出现瞬时峰值。这时候,我们需要架构级的兜底方案。
5.1 读写分离与分库分表
- 读写分离:主库负责写,多个从库负责读。通过中间件(如 ShardingSphere、MyCat)自动路由。
- 分库分表:当单表数据量超过千万级,或单库 QPS 达到瓶颈时,将数据拆分到多个数据库或表中。淘宝的双11,核心订单库就是按用户 ID 取模分库分表的。
5.2 限流与降级
这不是 MySQL 的优化,而是对整个系统的保护。
- 限流:使用 Sentinel 或 Hystrix 对接口进行 QPS 限流。超过阈值的请求直接返回“系统繁忙”,保护后端 DB。
- 降级:在非核心功能(如推荐算法、评论统计)上,高峰期可以暂时关闭,集中资源保障核心交易链路。
5.3 异步化与消息队列
对于非实时性要求高的操作(如发送短信、记录日志、更新统计),不要同步写 DB,而是通过 RocketMQ 或 Kafka 异步处理。这能将峰值流量削平,避免瞬间冲击数据库。
六、 给小朋友也能听懂的总结
想象一下,MySQL 是一个学校的图书馆管理员。
- 缓存就像是你把常用的书放在自己课桌抽屉里(本地缓存)和班级图书角(Redis),这样你就不用每次去图书馆借了,速度飞快。
- 连接池就像是图书馆的借书窗口数量。窗口开太少,大家排队太久;窗口开太多,管理员忙不过来,还容易乱。我们要找到最合适的人数。
- 慢 SQL 优化就像是给书编个好目录。如果目录清晰(索引好找),找书就快;如果乱七八糟(全表扫描),找一本书可能要翻遍整个书架。
- 限流就像是学校门口测体温、限流。人太多了,就先让后面的人等一下,保证里面秩序井然,不会因为太挤而出事。
七、 最后的话
数据库高并发优化,不是一个点,而是一套组合拳:缓存挡前面,连接池控资源,SQL 保效率,架构做兜底。
双11的千万级 QPS 并非遥不可及的传说,它的核心思想是“分层治理,提前预案”。即使你现在的流量只有一千 QPS,把这些最佳实践用起来,也能让你的系统更加健壮,不再为半夜的报警电话而焦虑。
记住,防患于未然,永远比亡羊补牢要轻松得多。 希望这篇文章能帮你建立起一套完整的高并发数据库优化思维框架。如果有具体的场景问题,欢迎随时交流!