如果压测发现数据库是瓶颈,通常怎么优化?
结论:不要先拍脑袋加 Redis 或分库。先复现压测并定位是慢 SQL、缺索引、锁等待、连接耗尽、I/O、内存、写入放大还是容量问题;修正查询和事务后重新测,单机仍达不到目标才扩容、加只读副本或分片。
“数据库是瓶颈”必须由指标证明。应用线程池、网络、序列化或压测机先耗尽,也会表现为接口等待数据库;不真实的数据分布和无思考时间的压测则可能制造生产中不存在的热点。滚水科技会先固定版本、数据量、并发模型和通过标准,监控应用与数据库两端,再改变一项因素做对照。
在继续拆分功能、数据与验收场景时,还可以对照 复用现有订单或商城模块还算定制开发吗? 和 系统是否支持未来扩展?如果业务量扩大 10 倍,是否需要重构?;这些内容补充了需要放在同一项决策中考虑的上下文。
不同症状对应不同手段:
| 主要证据 | 常见根因 | 第一优先动作 | 不宜立即采用 |
|---|---|---|---|
| 少数 SQL 占大部分总耗时、扫描行远多于返回行 | 缺索引、条件不可索引、错误 join 或 N+1 | 看执行计划,改查询/索引和访问批次 | 先加机器掩盖慢 SQL |
| 锁等待和死锁上升、吞吐随并发下降 | 长事务、更新顺序不一、热点行 | 缩短事务、统一锁顺序、拆热点竞争 | 加缓存解决写锁 |
| 连接数打满但 CPU/I/O 不高 | 连接泄漏、池配置、请求阻塞 | 查连接生命周期、超时和池排队 | 无限制提高最大连接 |
| 磁盘延迟、IOPS 或 WAL/redo 压力高 | 随机访问、过多索引、批量小写、存储不足 | 合并写入、审查索引、优化存储与 checkpoint | 直接读写分离解决主库写压 |
| CPU 持续饱和且有效查询已优化 | 计算重、排序聚合或实例能力不足 | 减少无效计算、预聚合或纵向扩容 | 未验证就分库分表 |
| 读多写少且只读请求可接受短暂延迟 | 查询负载超过主库 | 缓存或只读副本并标明一致性 | 把到账/库存等强一致读路由从库 |
PostgreSQL 可使用 EXPLAIN/EXPLAIN ANALYZE、统计视图、慢查询扩展和锁视图定位,官方性能建议 解释了查询计划及其成本;MySQL 可用慢查询、Performance Schema 和 EXPLAIN 官方文档 查看访问路径。生产执行带实际运行的分析命令要谨慎,可能真的修改数据或增加负载,优先在等量测试副本复现。
索引不是越多越好。联合索引顺序应匹配过滤、排序和连接条件,并用真实基数验证;低选择性字段单独索引未必有用。每个索引增加写入、存储和维护成本,重复和未使用索引应审查。分页到深页时避免高 offset 扫描,可使用稳定排序键的游标分页。只取需要列,批量获取关联数据,消除循环中的 N+1 查询。
事务要尽量短,但不能为了快破坏业务一致性。不要在事务内等待用户、调用慢第三方或处理大文件;批量任务按可恢复小批次提交。所有并发更新使用一致锁顺序,并为重试设计幂等。库存扣减、收款核销等热点要用条件更新、队列或合适的并发控制验证,不能只提高隔离级别或加分布式锁而不测冲突率。
缓存适合变化较少、允许一定陈旧且读取频繁的数据。缓存键包含租户和版本,设置失效策略,防止穿透、击穿和同一时刻大量回源;写后失效和数据库提交顺序要明确。价格、权限、余额、库存和结算不能因命中缓存返回错误事实。每项缓存应记录命中率、节省的数据库时间和失效事故,不为“架构完整”缓存所有对象。
纵向扩容常是合理的中间步骤,但要看 CPU、内存、存储吞吐和成本哪项受限。只读副本解决可容忍复制延迟的查询,不能提升主库写能力;读写路由要处理“刚写完马上读”的一致性。归档历史数据要保留查询路径和恢复演练。水平分片没有通用“亿级阈值”,是否采用取决于单节点容量、热点、增长、团队和跨分片事务,而不是记录数一个数字。
压测报告至少记录数据规模与分布、并发和到达率、场景比例、缓存冷热、实例规格、软件版本、P50/P95/P99、吞吐、错误率、数据库 CPU/I/O/内存/连接/锁/复制延迟,以及 Top SQL 总耗时。优化后用同一模型重跑并给出前后对照,确认没有用更高错误率或错误数据换速度。容量还应保留业务峰值和故障降级余量。
滚水科技会优先做可逆、证据充分的查询和事务优化,再评估扩容与架构变化;可靠性方法可参考 Google SRE Book。