数据库是大多数企业应用的核心,也是性能问题最密集的一层。一个慢查询可能拖垮整个服务,而连接池配置不当则会让系统在流量高峰时完全不可用。本节围绕索引设计、SQL 优化和连接池调优三个最关键的环节,提供可直接落地的实践方案。
21.2.1 索引设计:让查询“看见”你的数据
1. 索引的基础原理
数据库索引类似书籍的目录,本质是一种有序的数据结构(InnoDB 中为 B+ 树)。它允许数据库引擎在 O(log n) 时间内定位到目标行,而不必全表扫描。但索引不是免费的:它占用磁盘空间,并会降低写入性能(INSERT/UPDATE/DELETE 时需要同时更新索引)。因此,索引设计永远是读与写的权衡。
2. 常见索引类型与适用场景
- 主键索引(聚簇索引):InnoDB 中的数据按主键顺序物理存储。主键应尽可能短且有序,推荐使用自增 ID 或有序 UUID,避免随机 UUID 导致页分裂。
- 普通索引(二级索引):允许重复值,是最常用的查询加速手段。
- 唯一索引:保证列值唯一,同时具备加速查询的功能,适合身份证号、邮箱等字段。
- 联合索引:多列组合建立索引,可支持复杂条件的查询。最左前缀原则是关键——索引 (a,b,c) 能加速
a=?、a=? AND b=?、a=? AND b=? AND c=?的查询,但无法单独靠 b 或 c 高效过滤。 - 覆盖索引:查询所需的所有列都包含在索引中,无需回表读取完整数据行,性能提升极为明显。例如为
SELECT name, age FROM user WHERE email=?建立 (email, name, age) 联合索引。
3. 索引设计的实用原则
- 高频查询字段优先建索引:分析业务 SQL,给 WHERE、JOIN、ORDER BY、GROUP BY 中频繁出现的列加索引。
- 选择区分度高的列:索引字段的基数(cardinality)越大,过滤效果越好。不要在性别、状态这种仅有几个值的字段上建单列索引。
- 避免冗余和重复索引:
(a)和(a, b)是冗余的,前者可被后者覆盖;完全相同的索引更是浪费。 - 控制索引数量:单表索引一般不建议超过 5-6 个。每个新增索引都会拖累写入并增加优化器选择负担。
- 长字段使用前缀索引:对于 VARCHAR/TEXT 大字段,可只对前 N 个字符建索引,如
INDEX (description(100)),需确保前缀足够区分多数数据。 - 定期分析并清理无用索引:利用
sys.schema_unused_indexes(MySQL 8.0+)或慢查询日志定位从未使用或低效的索引。
4. 真实案例:订单列表查询优化
慢查询:
SELECT * FROM orders WHERE user_id = 123 AND status = 'PAID'
ORDER BY create_time DESC LIMIT 10;
无合适索引时,数据库扫描数十万行。建立联合索引 (user_id, status, create_time) 后,查询可以按索引排序直接取前 10 条,耗时从数秒降至毫秒级。若将 SELECT * 改为只取必要列并用覆盖索引 (user_id, status, create_time, amount) 进一步优化,可连回表都省掉。
21.2.2 SQL 优化:写出高效的数据请求
1. 必备工具:EXPLAIN 解读
在任何优化前,先用 EXPLAIN(或 EXPLAIN ANALYZE 在 MySQL 8.0.18+)分析查询执行计划。关注的关键字段:
- type:连接类型,从优到差依次为
const、eq_ref、ref、range、index、ALL。至少应达到range级别,出现ALL(全表扫描)需立即优化。 - key:实际使用的索引,若为 NULL 说明未走索引。
- rows:预估扫描行数,越小越好。
- Extra:
Using index表示覆盖索引,性能良好;Using filesort表示需要额外排序,可考虑索引优化;Using temporary表示用到临时表,通常需要改写 SQL 或增加索引。
2. 常见 SQL 优化规则
- 避免 SELECT *:只查询需要的列,减少数据传输量,也更易利用覆盖索引。
- 警惕隐式类型转换:
WHERE phone = 13800138000如果 phone 是 VARCHAR 类型,会导致索引失效。传入参数类型必须与字段一致。 - 负向条件慎用:
!=、NOT IN、NOT EXISTS常导致全表扫描。尝试用正向条件改写或结合业务逻辑避免。 - LIKE 前置模糊查询:
LIKE '%keyword'无法使用索引。如果必须做模糊搜索,考虑使用全文索引(FULLTEXT)或 Elasticsearch 等专用搜索引擎。 - JOIN 优化:小表驱动大表,确保 JOIN 的关联列上有索引。避免超过三个表的 JOIN,复杂关联可拆分成多次查询在应用层组装。
- 子查询优化:尽量将子查询改写为 JOIN,现代优化器有时能自动转换,但手写 JOIN 更具可控性。
- 合理分页:
LIMIT 100000,10会扫描并丢弃前 10 万行,性能极差。改进方法: - 使用游标分页:记录上一页最后一条记录的 ID,用
WHERE id > last_id LIMIT 10加速。 - 或采用延迟关联:先通过覆盖索引查出主键,再关联原表获取所需列。
- 批量操作代替循环:多次单条 INSERT 会带来大量网络和事务开销,使用批量插入(一次插入多行)可提升数十倍性能。MyBatis 的
foreach或 JPA 的saveAll针对少量数据可行,大批量建议直接使用 JDBC 批量或LOAD DATA。
3. 真实优化示例:统计报表查询重构
原始统计 SQL:
SELECT DATE(create_time), COUNT(*) FROM large_table
WHERE status = 'FINISHED' GROUP BY DATE(create_time);
即使 status 有索引,全表分组计算依然很慢。优化手段:增加一个报表汇总表,通过定时任务或触发器每小时累加数据,查询直接读取汇总表,性能提升 100 倍以上。该思路体现了 以空间换时间 和 业务预聚合 的常见优化策略。
21.2.3 连接池调优:把控数据库的“命脉”
1. 为什么需要连接池
数据库连接创建昂贵(TCP 握手、认证、线程分配)。应用中每次请求都新建连接再关闭,既低效又容易耗尽数据库资源。连接池复用一定数量的持久连接,让高频访问成为可能。Spring Boot 中默认集成 HikariCP,它是目前 Java 生态中性能最好的连接池。
2. HikariCP 关键参数调优
调整连接池参数前,务必先理解:池子大小并非越大越好。过多连接会导致数据库 CPU、内存竞争,上下文切换开销增加,反而降低吞吐。一个经验公式:
池大小 ≈ ((物理核数 × 2) + 有效磁盘数)
对于多数 Web 应用,10~30 个连接是合理起点。
核心配置项(application.yml):
spring:
datasource:
hikari:
# 连接池最大连接数,默认 10
maximum-pool-size: 20
# 最小空闲连接数,默认与 maximum-pool-size 相同
minimum-idle: 5
# 连接超时时间(毫秒),默认 30000。超时未获取到连接则抛异常
connection-timeout: 30000
# 空闲连接最大存活时间,超过则会被释放
idle-timeout: 600000
# 连接最大生命周期,应比数据库的 wait_timeout 短几分钟,防止使用已断开的连接
max-lifetime: 1800000
# 连接测试查询(MySQL 8 驱动已不需要设置,会自动使用 ping)
connection-test-query: SELECT 1
调优步骤:
- 监控应用的实际并发请求量和数据库的
Threads_connected指标。 - 压力测试时逐步增加
maximum-pool-size,观察 TPS 和响应时间。当吞吐不再增长甚至开始下降时,即为当前硬件下的最优池大小。 - 确认
max-lifetime小于数据库wait_timeout,避免半开连接错误。 - 针对连接泄漏保护,可设置
leak-detection-threshold(如 10000 毫秒),开发阶段快速发现未关闭的连接。
3. 连接池常见问题定位
- 连接不够用:日志中出现
Connection is not available, request timed out after 30000ms。先检查是否有慢查询占用了连接长时间不归还;若无,再考虑增大池子或扩容数据库。 - 连接泄漏:应用获取连接后未正确关闭(通常在
finally中归还)。使用 Spring 的@Transactional或JdbcTemplate会自动管理连接,泄漏多见于手动从DataSource.getConnection()获取的场景。 - 数据库超时断开:报
CommunicationsException: Communications link failure。检查max-lifetime是否超过数据库wait_timeout,以及 MySQL 的wait_timeout是否过小(通常设为 8 小时即可)。
4. 验证调优效果
在 Spring Boot 中,HikariCP 的运行时状态可通过 /actuator/health 和自定义 Metrics 暴露。配置暴露后,可以在 Prometheus + Grafana 中监控 hikaricp_connections_active、hikaricp_connections_pending 等指标,观察连接池的压力和等待情况,持续优化。
总结
数据库性能优化的核心在于让数据尽可能快地找到并返回,而且不给系统带来额外负担。索引设计从数据结构层面加速检索,SQL 优化确保每一次查询都高效,连接池调优则保障应用与数据库之间的通道顺畅。三项工作互为支撑,缺一不可。优先从 SQL 和索引入手(投入产出比最高),再配合连接池参数打磨,大部分性能问题都能迎刃而解。