Skip to content

Commit 8d4f068

Browse files
zlclaude
andcommitted
新增 5 个案例 (103-107) + generate-pdf.js 支持灵活编号
- 103 自适应哈希索引 AHI 调优 (indexing) - 104 Change Buffer 二级索引写入加速 (indexing) - 105 SELECT INTO OUTFILE 大数据导出与安全 (ddl) - 106 EXPLAIN FORMAT=JSON 详细成本树解读 (query-rewrite) - 107 HikariCP/Druid 连接池参数调优 (architecture) generate-pdf.js: CATEGORIES 从 start/end 改为 nums 数组, collect() 用 Set.has() 过滤,支持每个 category 显式列举 num。 不再依赖连续区间,可容纳任意编号的新案例。 PDF: 102 → 107 案例, 748 → 785 页, 113 → 118 书签, 55.0 → 57.8 MB Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
1 parent 47408b2 commit 8d4f068

9 files changed

Lines changed: 1028 additions & 14 deletions

File tree

docs/.vitepress/config.ts

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -43,6 +43,8 @@ const sidebar = {
4343
{ text: '16 · 索引跳跃扫描 Skip Scan', link: '/cases/indexing/16-skip-scan' },
4444
{ text: '17 · 游标分页替代深分页', link: '/cases/indexing/17-cursor-pagination' },
4545
{ text: '18 · 全文索引 FULLTEXT 替代 LIKE', link: '/cases/indexing/18-fulltext-search' },
46+
{ text: '103 · 自适应哈希索引 AHI 调优', link: '/cases/indexing/103-adaptive-hash-index' },
47+
{ text: '104 · Change Buffer 二级索引写入加速', link: '/cases/indexing/104-change-buffer' },
4648
],
4749
},
4850
{
@@ -63,6 +65,7 @@ const sidebar = {
6365
{ text: '30 · 时区与 TIMESTAMP vs DATETIME', link: '/cases/query-rewrite/30-timestamp-vs-datetime' },
6466
{ text: '31 · 时间格式使用错误与最佳实践', link: '/cases/query-rewrite/31-time-format-antipattern' },
6567
{ text: '32 · SQL 反模式与正确写法量化对比', link: '/cases/query-rewrite/32-sql-antipatterns' },
68+
{ text: '106 · EXPLAIN FORMAT=JSON 详细成本树', link: '/cases/query-rewrite/106-explain-format-json' },
6669
],
6770
},
6871
{
@@ -94,6 +97,7 @@ const sidebar = {
9497
{ text: '49 · 修改字段类型锁表', link: '/cases/ddl/49-modify-column-type' },
9598
{ text: '50 · 大字段垂直拆表', link: '/cases/ddl/50-vertical-split-text' },
9699
{ text: '51 · 字段类型与长度选择最佳实践', link: '/cases/ddl/51-field-type-best-practice' },
100+
{ text: '105 · SELECT INTO OUTFILE 大数据导出', link: '/cases/ddl/105-select-into-outfile' },
97101
],
98102
},
99103
{
@@ -111,6 +115,7 @@ const sidebar = {
111115
{ text: '60 · 缓存穿透与布隆过滤器', link: '/cases/architecture/60-cache-penetration' },
112116
{ text: '61 · 自增主键耗尽与分布式 ID', link: '/cases/architecture/61-auto-inc-exhaustion' },
113117
{ text: '62 · 连接池与 max_connections 耗尽诊断', link: '/cases/architecture/62-connection-pool-exhaustion' },
118+
{ text: '107 · HikariCP/Druid 连接池调优', link: '/cases/architecture/107-connection-pool-tuning' },
114119
],
115120
},
116121
{
Lines changed: 222 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,222 @@
1+
# 数据库连接池调优:HikariCP / Druid 视角
2+
3+
<CaseMeta difficulty="⭐⭐⭐" category="架构级优化" versions="5.7 & 8.0" :tags="['连接池', 'HikariCP', 'Druid', 'max_connections', 'wait_timeout', '应用层优化']" />
4+
5+
## 场景痛点
6+
7+
应用启动后运行正常,但**高峰时段**(如大促)数据库连接数飙到 2000+(超过 `max_connections=1000`),新连接申请失败,应用开始报 "Too many connections"。运维紧急重启后恢复,但几小时后再次发生。
8+
9+
监控显示:
10+
- MySQL 端 `Threads_connected=2000``Max_used_connections=2100`
11+
- 应用端每个 Tomcat 节点维持 50 个空闲连接,20 个节点 = 1000 个空闲连接
12+
- 高峰时**业务 SQL 并发 800**,但同时**大量慢查询**占住连接不释放
13+
14+
```sql
15+
-- MySQL 端状态
16+
SHOW STATUS LIKE 'Threads_connected'; -- 2000
17+
SHOW STATUS LIKE 'Max_used_connections'; -- 2100
18+
SHOW STATUS LIKE 'Slow_queries'; -- 1000/sec(!)
19+
```
20+
21+
::: warning 真实场景
22+
"数据库连接不够用"是 Java 应用最常见的性能问题之一。根因往往是**连接池配置错误****应用层慢 SQL 拖垮连接**。盲目增加 `max_connections` 不能解决问题——MySQL 每连接消耗 ~10MB 内存,2000 连接 = 20GB,光连接就吃光服务器内存。本案例从**应用层连接池 + 数据库 max_connections 双向调优**给出方案。
23+
:::
24+
25+
## 问题分析
26+
27+
### bad.sql — 配置错误示范
28+
29+
```sql
30+
-- bad 配置: MySQL 端 max_connections 设置过小
31+
SHOW VARIABLES LIKE 'max_connections';
32+
-- max_connections = 200 ← 太小!20 个应用节点 × 50 池大小 = 1000
33+
34+
-- bad 配置: 应用端连接池无上限
35+
-- HikariCP 默认 maximumPoolSize=10,但很多团队误配为 100+
36+
-- dataSource.setMaximumPoolSize(100); ← 单节点 100,20 节点 2000
37+
38+
-- 慢查询占住连接
39+
SHOW PROCESSLIST;
40+
-- Id=12345 User=app Host:10.0.0.5:54321 db=sql_treasure Command=Sleep Time=300
41+
-- ↑ Sleep 300 秒——连接没释放!
42+
```
43+
44+
### 三个常见根因
45+
46+
| 根因 | 表现 | 解决方案 |
47+
|------|------|---------|
48+
| **MySQL `max_connections` 太小** | 应用报 "Too many connections" | 适度调大(如 1000),但不能过大 |
49+
| **应用连接池配置过大** | `Threads_connected` 持续接近 max | 调小 `maximumPoolSize` |
50+
| **慢 SQL 占住连接不释放** | `Sleep` 状态连接多 | 优化 SQL + 调小 `wait_timeout` + 连接池检测 |
51+
52+
## 优化方案
53+
54+
### good.sql — 双端调优
55+
56+
```sql
57+
-- ── MySQL 端调优 ──
58+
59+
-- 1. 适度调大 max_connections(按内存算:1000 连接 × 10MB = 10GB)
60+
SET GLOBAL max_connections = 1000;
61+
-- 配合 innodb_buffer_pool_size 调整(连接占用 ~10MB/个,buffer pool 通常占总内存 50-70%)
62+
63+
-- 2. 缩短 wait_timeout,自动回收空闲连接(默认 8 小时太长)
64+
SET GLOBAL wait_timeout = 300; -- 空闲 5 分钟自动断开
65+
SET GLOBAL interactive_timeout = 300;
66+
67+
-- 3. 监控连接状态
68+
SHOW STATUS LIKE 'Threads_connected';
69+
SHOW STATUS LIKE 'Threads_running'; -- 真正在执行的(应 < CPU 核数 × 2)
70+
SHOW STATUS LIKE 'Max_used_connections';
71+
```
72+
73+
```yaml
74+
# ── 应用端连接池调优(HikariCP)──
75+
# application.yml
76+
spring:
77+
datasource:
78+
type: com.zaxxer.hikari.HikariDataSource
79+
hikari:
80+
# 核心调优(按"CPU 核数 × 2 + 磁盘数"经验公式)
81+
maximum-pool-size: 20 # 单节点最大连接
82+
minimum-idle: 5 # 维持最小空闲
83+
84+
# 超时控制(关键)
85+
connection-timeout: 3000 # 3s 拿不到连接就报错(不阻塞业务)
86+
idle-timeout: 300000 # 5 分钟空闲连接回收
87+
max-lifetime: 1800000 # 30 分钟强制重建(避 MySQL wait_timeout)
88+
89+
# 连接健康检查
90+
validation-timeout: 2000
91+
connection-test-query: "SELECT 1"
92+
# 或更好: connection-init-sql: "SET SESSION transaction_isolation = 'READ-COMMITTED'"
93+
94+
# 泄漏检测(开发环境)
95+
leak-detection-threshold: 10000 # 10s 还没关闭的连接报警
96+
```
97+
98+
```yaml
99+
# ── Druid 替代方案(阿里系常用)──
100+
spring:
101+
datasource:
102+
druid:
103+
initial-size: 5
104+
min-idle: 5
105+
max-active: 20 # 同 HikariCP
106+
max-wait: 3000 # 拿连接超时
107+
108+
# 监控(比 HikariCP 强)
109+
filters: stat,wall,slf4j
110+
filter.stat.log-slow-sql: true
111+
filter.stat.slow-sql-millis: 200
112+
113+
# 连接保活
114+
validation-query: SELECT 1
115+
test-while-idle: true
116+
time-between-eviction-runs-millis: 60000
117+
```
118+
119+
### 原理
120+
121+
**连接池工作模型**:
122+
123+
```
124+
应用 (Tomcat Node × 20) MySQL Server
125+
┌─────────────────────┐ ┌─────────────────┐
126+
│ Connection Pool │ TCP │ Connections │
127+
│ ┌─────┐ ┌─────┐ │◀──────▶│ 200-1000 个 │
128+
│ │ C1 │ │ C2 │ │ ... │ │
129+
│ └─────┘ └─────┘ │ │ 活跃 ~ 50-200 │
130+
│ size: 20 │ │ 空闲 ~ 100 │
131+
└─────────────────────┘ └─────────────────┘
132+
▲ ▲
133+
│ │
134+
maximumPoolSize max_connections
135+
决定应用要多少 决定 MySQL 能接多少
136+
```
137+
138+
**关键调优公式**(Percona / HikariCP 官方推荐):
139+
140+
```
141+
connections = ((core_count * 2) + effective_spindle_count)
142+
```
143+
144+
| 服务器 | CPU 核数 | 磁盘(NVMe) | 推荐单节点池大小 | 推荐 max_connections(20 节点) |
145+
|--------|---------|------------|----------------|-------------------------------|
146+
| 4 核 | 4 | 1 | 9 | 180 |
147+
| 8 核 | 8 | 2 | 18 | 360 |
148+
| 16 核 | 16 | 4 | 36 | 720 |
149+
| 32 核 | 32 | 8 | 72 | 1440 |
150+
151+
**经验法则**
152+
153+
- `maximumPoolSize = 2 × CPU 核数`(避免上下文切换)
154+
- 总量 = `maximumPoolSize × 应用节点数 × 1.2`(冗余 20%)
155+
- MySQL `max_connections = 上述总量 + 预留`(如 50 个给 DBA/监控)
156+
157+
### 对比
158+
159+
| 指标 | bad (默认配置) | good (调优后) |
160+
|------|---------------|--------------|
161+
| `max_connections` | 200 | 1000 |
162+
| Hikari `maximum-pool-size` | 100 | 20 |
163+
| `wait_timeout` | 28800 (8h) | 300 (5min) |
164+
| `connection-timeout` | 30s | 3s |
165+
| `Threads_connected` 高峰 | 2000(爆) | 350(健康) |
166+
| 业务报错 "Too many connections" | 频繁 | 0 |
167+
| 应用 GC 压力 | 高(连接对象堆积) ||
168+
| 慢查询占连接 | 几百个 Sleep | 几十个 Sleep |
169+
170+
<ExplainCompare
171+
:bad="{ type: '默认', key: 'max_conn=200, pool=100', rows: '2000连接', Extra: '高峰期爆连接,GC 抖动' }"
172+
:good="{ type: '调优', key: 'max_conn=1000, pool=20', rows: '350连接', Extra: '健康水位,慢查询自动回收' }"
173+
improvement="连接爆满归零,应用 P99 延迟从 2s 降到 200ms"
174+
/>
175+
176+
## 避坑指南
177+
178+
::: warning 注意事项
179+
180+
1. **不要盲目调大 `max_connections`**。每连接占 ~10MB 内存,5000 连接 = 50GB 内存。MySQL 是单进程模型,连接数过多会显著降低调度效率。`max_connections` 应按内存和 CPU 核数合理计算。
181+
182+
2. **`Sleep` 状态的连接也是连接**`SHOW PROCESSLIST``Command=Sleep` 的连接仍占用 `max_connections` 配额。`wait_timeout` 调小可让 MySQL 服务端主动断开,但**更好的方案是连接池层**控制(`idle-timeout`)。
183+
184+
3. **连接池不是越大越好**。每个连接有内存开销,且增加数据库上下文切换。一般 8-32 即可。MySQL 8.0 + HikariCP 配合 8 核机器,pool=16 通常最优。
185+
186+
4. **`maxLifetime` 必须小于 MySQL `wait_timeout`**。否则应用以为连接可用,实际 MySQL 已单方面断开,导致第一次查询报错。HikariCP 默认 30 分钟,MySQL 调成 5 分钟,OK。
187+
188+
5. **DNS 反查**:MySQL 5.7 默认开启 `skip_name_resolve`,每次连接会反查 IP 对应主机名。务必在 `my.cnf` 中设置 `skip-name-resolve`,避免连接慢。
189+
190+
6. **生产环境禁用 `useUnicode=true&characterEncoding=utf8`**。直接连接 MySQL 不需要客户端 charset 参数(驱动会自动协商)。
191+
:::
192+
193+
## 5.7 vs 8.0 差异
194+
195+
| 特性 | 5.7 | 8.0 |
196+
|------|-----|-----|
197+
| 默认 `max_connections` | 151 | 151 |
198+
| 默认 `wait_timeout` | 28800 (8h) | 28800 (8h) |
199+
| 连接认证性能 | caching_sha2_password 需 RSA | 5.7 用 mysql_native_password |
200+
| 线程池插件 | 社区版无(MariaDB/Percona 有) | 同 5.7 |
201+
| Performance Schema 连接监控 | 完善 | 完善 + `memory_summary_by_thread_by_event_name` |
202+
| `show_compatibility_56` | 默认 ON | 8.0.1 起默认 OFF(部分状态变量需用 Performance Schema) |
203+
204+
::: tip 推荐实践
205+
- **MySQL 端**`max_connections=1000`, `wait_timeout=300`, `skip-name-resolve=ON`, `thread_cache_size=50`
206+
- **HikariCP**`maximum-pool-size=CPU*2`, `max-lifetime=1800000`, `connection-timeout=3000`
207+
- **Druid**:开启 `stat` 监控 + 慢 SQL 记录
208+
- **监控**`Threads_connected` / `Threads_running` / `Max_used_connections` 三大指标报警
209+
:::
210+
211+
## 本地复现
212+
213+
```bash
214+
# 默认在 MySQL 8.0 上运行
215+
./scripts/run-case.sh 107-connection-pool-tuning
216+
217+
# 在 MySQL 5.7 上运行(对比)
218+
./scripts/run-case.sh 107-connection-pool-tuning --ver 5.7
219+
220+
# 模拟高并发连接
221+
./scripts/run-case.sh 107-connection-pool-tuning --stress 50
222+
```

0 commit comments

Comments
 (0)