做过活动大促、抢购、或者被一篇爆款文章带上热搜的技术团队,都体会过那种心跳加速的时刻:监控面板上 QPS 曲线突然垂直上冲,数据库 CPU 瞬间打满,连接数飙升到上限,接口开始大面积超时。突发流量最可怕的地方是它来得快、峰值高,而数据库的扩容往往是分钟级甚至更慢,等扩回来,用户的耐心早就没了。
这篇文章讲的是"数据库服务器怎么扛住突发流量"。思路不是单靠加机器,而是用一整套组合拳:连接控制防止被打爆、缓存把读请求挡在前面、限流让系统有序降级、队列削峰、只读副本分流、以及非核心功能降级。文中给出可落地的参数和命令,数值都是经验参考,真正的阈值要靠压测确定。
突发流量为什么杀伤力这么大
先理解问题的本质。正常流量下,系统的资源占用是平稳的。突发流量下,请求量在短时间内放大几倍到几十倍,而数据库的处理能力(CPU 核数、磁盘 IOPS、可用连接数)是固定的。请求一旦超过处理能力,就会排队;排队导致响应变慢;响应变慢让连接被占用更久;连接被占满后新请求被拒绝或一直等待。这就是雪崩的起点。
更麻烦的是"重试放大"。当接口超时,客户端的重试机制、网关的重试、用户手动刷新,都会在原有流量上再乘一个系数,让本就过载的系统雪上加霜。所以应对突发流量,一方面要提高真实处理能力,另一方面要坚决避免让无效或重复的请求继续涌向数据库。
连接控制
连接是最直接的防线。数据库能承受的连接数是有限的,一旦被打满,连管理连接都建不上,问题就无法处理。
限制最大连接并预留管理连接
max_connections 是硬上限,但不要把它当成唯一防线。经验做法是把 max_connections 设在数据库能稳定支撑的范围内,同时通过 reserved 的思路给管理连接留一条通道。MySQL 本身没有独立的"预留连接数"参数,但可以控制应用侧连接池之和明显小于 max_connections,从而天然留出余量。
# 查看当前连接使用情况
mysql -uroot -p -e "SHOW STATUS LIKE 'Threads_connected';"
mysql -uroot -p -e "SHOW STATUS LIKE 'Threads_running';"
mysql -uroot -p -e "SHOW STATUS LIKE 'Max_used_connections';"
mysql -uroot -p -e "SHOW VARIABLES LIKE 'max_connections';"
经验上,让 Max_used_connections 长期明显低于 max_connections(比如六到七成)是比较稳的状态。如果它长期顶到上限,说明连接资源已经紧张。
# 配置文件调整(需重启生效,属于高风险操作,务必在维护窗口进行)
[mysqld]
max_connections = 500
风险提示:调大 max_connections 前要估算内存是否够用,因为每个连接都有开销;调小会拒绝现有连接。改完后要观察 Threads_connected、Max_used_connections 和内存,确认没有触发 OOM 或频繁交换。若要回滚,把配置改回原值并再次重启即可。
及时释放空闲连接
很多连接处于 Sleep 状态,占着名额却不干活。合理设置 wait_timeout(非交互连接超时)能让空闲连接被回收,腾出名额给真正需要的请求。默认值通常很长(比如 28800 秒),对突发场景可以适当调小。
mysql -uroot -p -e "SHOW VARIABLES LIKE 'wait_timeout';"
mysql -uroot -p -e "SHOW VARIABLES LIKE 'interactive_timeout';"
但要注意,wait_timeout 调小后,应用连接池的 maxLifetime 必须更小,否则会出现"池子里的连接被数据库单方面关闭又被使用"的偶发错误。
处理异常连接
突发流量中常常伴随被中断的连接。可以查看相关计数器,判断是否发生了连接风暴或认证失败。
mysql -uroot -p -e "SHOW STATUS LIKE 'Aborted_connects';"
mysql -uroot -p -e "SHOW STATUS LIKE 'Aborted_clients';"
mysql -uroot -p -e "SHOW STATUS LIKE 'Connections';"
Aborted_connects 持续增长通常意味着连接被拒绝或认证问题,可能与 max_connections 打满有关。
缓存
缓存是应对突发读流量性价比最高的手段。突发流量里,很大比例是重复的读请求——同一个热门商品、同一份榜单被反复查询。把这些结果放进缓存,数据库就不用每次都算一遍。
用 Redis 做数据缓存
把热点数据缓存在 Redis 里,应用先查缓存、命中就直接返回,未命中再查数据库并回写缓存。这样能把绝大部分读请求挡在数据库之外。要点是设置合理的 TTL、处理缓存穿透(查询不存在的数据反复打到库)、缓存击穿(热点 key 过期瞬间大量请求涌向数据库)和缓存雪崩(大量 key 同时过期)。
# Redis 安装示意,版本和包名以官方文档为准
# RHEL/CentOS/Rocky
sudo dnf install -y redis
# Debian/Ubuntu
sudo apt-get install -y redis-server
# 查看缓存命中率和内存使用
redis-cli INFO stats | grep -E "keyspace_hits|keyspace_misses"
redis-cli INFO memory | grep used_memory_human
redis-cli --bigkeys
命中率是缓存是否有效的关键指标。如果命中率很低,说明缓存没起到作用,要么 key 设计有问题,要么 TTL 太短。热点 key 可以通过本地缓存(进程内缓存)再加一层,减少对 Redis 的集中压力。
查询结果缓存与预计算
除了缓存原始数据,还可以缓存"查询结果",比如把耗时的统计查询结果定时预计算好、存入缓存或专门的汇总表,查询时直接读取。对报表类接口,这种预计算能把每次都要扫全表的昂贵查询变成一次简单的读取。
限流
限流是为了在系统过载时保护核心能力,宁可让一部分请求快速失败,也不要让所有请求都慢慢超时。限流可以放在应用层,也可以放在代理层。
应用层限流
在应用里对接口或资源做限流,常见算法有令牌桶、漏桶和滑动窗口。令牌桶允许一定程度的突发,漏桶则把流量整形得更平滑。对数据库相关操作,建议单独做一层限流,限制每秒钟打到数据库的请求数,避免应用把自己压垮。
代理层限流
网关或数据库代理层做限流,可以在流量进入后端之前就拦下来。代理层限流的好处是统一、对应用透明,但需要确保限流规则本身不会被绕过或误伤。在数据库代理(如 ProxySQL 这类)上,可以对特定查询做路由限流,把某些昂贵查询限制在更低的频率。
| 限流位置 | 优点 | 缺点 | 适用 |
|---|---|---|---|
| 应用层 | 灵活、可结合业务 | 每语言各自实现 | 业务细分限流 |
| 网关层 | 统一、透明 | 粒度较粗 | 全局入口限流 |
| 数据库代理 | 直接保护数据库 | 需维护规则 | 保护特定查询 |
限流的返回方式也要设计好。对非核心请求直接返回"系统繁忙"比让它排队超时体验更好,也能更快释放资源。核心请求则尽量保证成功率。
队列削峰
当写入量突然放大,与其让它直接砸向数据库,不如先塞进消息队列,由消费者按数据库能承受的速度慢慢消费。这就是"削峰填谷"。对外表现为"请求已受理,稍后处理",对数据库来说则是一条平稳的消费曲线。
队列适合容忍异步的业务,比如发送通知、记录日志、生成报表。对强实时的核心写入(比如下单扣库存),队列不能直接套用,否则用户看到的结果会延迟,需要谨慎设计或者用别的手段。队列本身也要防止积压过多导致内存或磁盘问题,并监控消费延迟。
只读副本分流
前面讲过读写分离,突发流量中读请求往往是主力,只读副本能显著分担读压力。当流量突增时,如果平时就维护了从库,可以临时把更多读流量导向从库,甚至快速再加一个从库(前提是数据量允许快速搭建)。
需要注意主从延迟。突发流量下,主库写入压力也大,从库可能追不上,读到旧数据的风险上升。所以分流时要判断哪些读是"可以接受旧数据"的,比如榜单、统计、列表页;哪些读必须走主库,比如写后立即读、强一致的校验。把不可接受延迟的读强制留在主库。
降级与非核心功能关闭
当系统真的过载时,最有效的办法之一就是"少干点事"。主动降级,把资源让给核心功能。常见的降级手段有:关闭实时统计和推荐等非核心接口、把精确查询降级为缓存结果、关闭耗资源的个性化功能、降低刷新频率、甚至对非核心页面返回静态兜底页。
降级要有开关,能快速开合,最好在配置中心或控制台一键切换,避免出事时还要改代码重新发版。降级策略应该提前设计好并演练过,明确"哪些能降、降了会怎样、怎么恢复"。
监控与告警
以上所有手段,前提都是"能第一时间发现问题"。没有监控,突发流量来了还浑然不觉,等到用户投诉已经晚了。
要重点监控的指标包括:数据库的 Threads_running、Threads_connected、慢查询数量、锁等待、以及 Innodb_row_lock_time;主从延迟;缓存命中率;接口的 QPS、延迟(尤其是尾延迟)和错误率;以及连接池的使用情况。
# 关键在线指标速查
mysql -uroot -p -e "SHOW STATUS LIKE 'Threads_running';"
mysql -uroot -p -e "SHOW STATUS LIKE 'Slow_queries';"
mysql -uroot -p -e "SHOW STATUS LIKE 'Innodb_row_lock_waits';"
mysql -uroot -p -e "SHOW STATUS LIKE 'Innodb_row_lock_time_avg';"
mysql -uroot -p -e "SHOW PROCESSLIST;" # 快速查看是否有异常长连接
告警要区分级别。CPU 到某阈值、尾延迟升高、错误率上升,都应该触发告警,但阈值要结合历史数据设定,避免误报造成"狼来了"。同时要能快速定位是哪个接口、哪类查询在放大流量,这决定了应对动作是否准确。
组合起来用
单个手段都有局限,真正扛住突发流量靠的是组合:缓存挡掉大部分重复读,限流和队列把请求整形到数据库能承受的水平,只读副本分担读压力,降级把非核心功能让路,监控提供实时反馈。还要提前做容量评估和压测,知道系统在什么流量下开始明显变慢,据此设定限流阈值和降级触发条件。
一个实用的准备清单:平时就维护好从库和缓存、把限流和降级开关做进系统、压测出容量上限、把关键指标接入告警、并针对大促制定预案并演练。
常见问题
加机器不是最快的办法吗?
加机器(尤其是加只读副本)能提升容量,但扩容通常是分钟级甚至更慢,而突发流量的峰值往往在几秒到几十秒内到达。所以不能只靠扩容,必须配合缓存、限流和降级来争取时间。
限流会把正常用户也挡掉吗?
会挡掉一部分。限流的目的是在过载时保住核心功能和整体可用性,而不是让所有请求都成功。关键是根据业务重要性分级,优先保住核心请求,并给用户可接受的失败提示。
缓存和数据库不一致怎么办?
这是缓存的固有代价。常见做法是"先更新数据库,再删除缓存",并设置合理 TTL 作为兜底,让不一致在 TTL 之后自动收敛。对强一致要求高的数据,则不适合缓存或需要更复杂的一致性方案。
队列会不会让用户等太久?
对异步业务,用户不需要立刻看到结果,队列延迟可以接受。对强实时业务,队列会带来可见延迟,应该改用限流或扩容,而不是硬套队列。
降级后怎么恢复?
降级开关要能快速关闭,恢复时建议逐步放开:先小流量验证核心接口正常,再完全恢复,并持续观察各项指标,避免恢复瞬间流量再次冲高造成二次雪崩。
总结
- 突发流量会引发排队、连接耗尽和重试放大的连锁反应,应对要靠组合手段而不是单一扩容。
- 连接控制是底线:控制
max_connections、让连接池之和不超限、及时回收空闲连接,并监控中断连接。 - 缓存是应对突发读流量的性价比之王,但要处理穿透、击穿、雪崩并盯住命中率。
- 限流保护系统有序降级,队列削峰填谷,两者都要区分核心与非核心业务。
- 只读副本能分担读压力,但要注意主从延迟,强一致的读必须留在主库。
- 降级和监控是兜底:非核心功能要有开关,关键指标要接入告警,并提前压测和演练预案。
企业QQ咨询




