数据库高性能设计
2023年618大促,我们团队卖出了 3 万台 iPhone。
但有 300 个用户在下单后,收到了系统自动取消的通知——他们被超卖了。
排查了 6 个小时,根因让人哭笑不得:数据库主从延迟了 8 秒。这 8 秒内,用户从结算页看到有库存,下单时写到了主库,但库存扣减的读请求打到了从库——从库还没同步完,主库已经从 0 扣到了 -300。
这就是数据库高性能设计最残酷的一面:一个小延迟,可以让你卖出 300 台不存在的 iPhone。
问题背景
数据库是几乎所有系统的最后一道防线。不管你的缓存多厚、异步化做得多好,最终的用户数据、交易数据、核心业务数据都在数据库里。
数据库的性能瓶颈主要来自三个方向:
- IO 瓶颈:磁盘读写速度远低于 CPU 和内存,数据从磁盘到内存这一跳就是毫秒级
- CPU 瓶颈:复杂查询、函数计算、锁竞争都会消耗 CPU
- 连接瓶颈:每个连接都是一个线程,连接数是有限的资源
互联网公司里,80% 的数据库性能问题都可以归结为:不该查的数据查了,不该排序的排了,不该 JOIN 的 JOIN 了。
核心设计手段
索引设计:数据结构视角
MySQL 的 InnoDB 使用 B+ 树作为索引结构。B+ 树的特点是:
- 所有数据都在叶子节点,叶子节点之间用链表相连
- 查询复杂度
O(log n),非常稳定 - 范围查询友好,因为叶子节点有序
联合索引的最左前缀原则:
Explain 是分析 SQL 性能的最好工具。每个 DDL 上线前,必须用 EXPLAIN 跑一遍,确认走了正确的索引。生产事故里,至少有 30% 是因为索引问题导致的。
覆盖索引:如果查询的所有列都在索引里,MySQL 就不需要回表,性能提升巨大。
读写分离:架构视角
读写分离的核心价值:把读流量卸载到从库,让主库专心处理写流量。
但读写分离带来一个致命问题:主从延迟。
主从延迟的原因:
- 主库写 binlog,从库 IO 线程拉取(网络延迟)
- 从库 SQL 线程重放 binlog(CPU 消耗)
- 从库与主库硬件配置不一致(从库通常弱于主库)
延迟的控制策略:
分库分表:数据层视角
当单表数据量超过 5000 万条,或者单库 QPS 超过 1 万时,就必须考虑分库分表了。
分库分表的核心是选择一个分片键:
分库分表后的跨分片查询:
这是分库分表最大的坑。假设按 user_id 分了 8 库 8 表,用户想查"所有商品类别中最受欢迎的前 10 个"——这种查询在单库时代一个 SQL 就搞定,分库后需要:
- 分散查询到 64 个分片
- 收集 64 个结果
- 归并排序
- 返回 top 10
这就是异构索引表方案诞生的原因——把需要聚合的字段冗余到 ES/HBase 等支持聚合查询的存储中。
生产避坑
坑1:索引不是越多越好
每个索引都是一棵 B+ 树。索引越多,插入/更新/删除的性能越差,因为每次数据变更都要同时更新所有相关索引。
我见过一张表 15 个字段,建了 12 个索引。查询是快了,但插入一条数据要操作 13 个索引(主键索引 + 12 个二级索引),性能直接退化 10 倍。
索引评审规范:每加一个索引,必须回答三个问题:这个索引服务哪个查询场景?这个查询的 QPS 是多少?如果去掉这个索引会怎样?
坑2:慢查询不慢
SQL 执行时间超过 1s 才算慢查询?错。在高并发场景下,100ms 的全表扫描就足以拖垮整个系统。
MySQL 默认配置下,单个查询可以占用 200MB 内存。如果有 10 个并发慢查询,内存直接爆掉。
生产监控规范:
- 慢查询日志:超过 100ms 的查询都要记录
- 连接数监控:连接数超过最大连接数的 80% 就告警
- 主从延迟监控:从库延迟超过 5s 就触发告警
坑3:分库分表后的分布式 ID
分库分表后,自增主键会冲突,必须用分布式 ID 生成方案。
工程代价评估
【架构权衡】 数据库高性能设计的本质是:在数据一致性、性能和复杂度之间找平衡。读写分离牺牲了强一致性,换取了吞吐量的提升。分库分表牺牲了跨分片查询的灵活性,换取了数据容量和写入性能。每一个设计决策背后都有代价,理解代价比理解方案更重要。