Skip to content

Commit 6798a05

Browse files
committed
docs:高并发部分文章优化完善
1 parent 9923b26 commit 6798a05

File tree

4 files changed

+121
-58
lines changed

4 files changed

+121
-58
lines changed

docs/high-performance/cdn.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -70,7 +70,7 @@ CDN 缓存的完整生命周期如下图所示:
7070

7171
![CDN 缓存的完整生命周期](https://oss.javaguide.cn/github/javaguide/high-performance/cdn/cdn-full-life-cycle-of-cdn-cache.png)
7272

73-
如果资源有更新,可以对其进行**刷新(Purge)**操作,删除 CDN 节点上缓存的旧资源,并强制 CDN 节点在下次请求时回源获取最新资源。
73+
如果资源有更新,可以对其进行**刷新**操作,删除 CDN 节点上缓存的旧资源,并强制 CDN 节点在下次请求时回源获取最新资源。
7474

7575
几乎所有云厂商提供的 CDN 服务都具备缓存的刷新和预热功能(下图是阿里云 CDN 服务提供的相应功能):
7676

docs/high-performance/deep-pagination-optimization.md

Lines changed: 60 additions & 32 deletions
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,7 @@ head:
1010

1111
<!-- @include: @small-advertisement.snippet.md -->
1212

13-
## 深度分页介绍
13+
## 什么是深度分页?怎么导致的?
1414

1515
查询偏移量过大的场景我们称为深度分页,这会导致查询性能较低,例如:
1616

@@ -19,9 +19,9 @@ head:
1919
SELECT * FROM t_order ORDER BY id LIMIT 1000000, 10
2020
```
2121

22-
## 深度分页问题的原因
22+
当查询偏移量过大时,MySQL 的查询优化器可能会选择全表扫描而不是利用索引来优化查询。
2323

24-
当查询偏移量过大时,MySQL 的查询优化器可能会选择全表扫描而不是利用索引来优化查询。这是因为扫描索引和跳过大量记录可能比直接全表扫描更耗费资源
24+
**深度分页变慢的根本原因**在于 MySQL 的执行机制:对于 `LIMIT offset, N`,MySQL 并非直接跳到 `offset` 处,而是必须从头扫描 `offset + N` 条记录。如果查询依赖二级索引且不满足覆盖索引,这意味着 MySQL 需要对前 `offset` 条记录执行毫无意义的**回表查询(产生海量的随机 I/O)**,最后再将这些辛苦查出的数据丢弃。即便优化器最终因代价过高退化为全表扫描,顺序扫描百万行的成本依然巨大
2525

2626
![深度分页问题](https://oss.javaguide.cn/github/javaguide/mysql/deep-pagination-phenomenon.png)
2727

@@ -33,24 +33,26 @@ MySQL 的查询优化器采用基于成本的策略来选择最优的查询执
3333

3434
## 深度分页优化建议
3535

36-
这里以 MySQL 数据库为例介绍一下如何优化深度分页
36+
> **本文基于 MySQL 8.0 + InnoDB 存储引擎**,不同版本优化器行为可能存在差异
3737
38-
### 范围查询
38+
### 范围查询(游标分页)
3939

40-
当可以保证 ID 的连续性时,根据 ID 范围进行分页是比较好的解决方案
40+
通过记录上一页最后一条记录的 ID,使用 `WHERE id > last_id LIMIT n` 获取下一页数据
4141

4242
```sql
43-
# 查询指定 ID 范围的数据
44-
SELECT * FROM t_order WHERE id > 100000 AND id <= 100010 ORDER BY id
45-
# 也可以通过记录上次查询结果的最后一条记录的ID进行下一页的查询:
46-
SELECT * FROM t_order WHERE id > 100000 LIMIT 10
43+
# 通过记录上次查询结果的最后一条记录的 ID 进行下一页的查询
44+
SELECT * FROM t_order WHERE id > 100000 ORDER BY id LIMIT 10
4745
```
4846

49-
这种基于 ID 范围的深度分页优化方式存在很大限制:
47+
**游标分页的核心优势****不依赖 ID 的连续性**。MySQL 只需要在 B+ 树上定位到 `last_id` 的位置,然后顺序向后读取 `n` 条记录即可,中间是否有断层(如 ID 被删除)完全不影响结果的准确性和性能。
5048

51-
1. **ID 连续性要求高**: 实际项目中,数据库自增 ID 往往因为各种原因(例如删除数据、事务回滚等)导致 ID 不连续,难以保证连续性。
52-
2. **排序问题**: 如果查询需要按照其他字段(例如创建时间、更新时间等)排序,而不是按照 ID 排序,那么这种方法就不再适用。
53-
3. **并发场景**: 在高并发场景下,单纯依赖记录上次查询的最后一条记录的 ID 进行分页,容易出现数据重复或遗漏的问题。
49+
这种方式的限制:
50+
51+
1. **不支持跳页**:无法直接跳转到第 N 页,只能逐页向后(或向前)翻页。
52+
2. **排序字段受限**:如果查询需要按照其他字段(如创建时间)排序而非 ID 排序,需使用联合游标 `(sort_field, id)` 保证唯一性和顺序。
53+
3. **并发场景**:当分页查询期间有新数据插入或删除时,可能出现:
54+
- **数据遗漏**:查询第二页时,有新数据插入到第一页范围内,导致该数据被"挤"到第二页,但第二页查询已基于旧的最后 ID 跳过它。
55+
- **数据重复**:查询第二页时,第一页末尾有数据被删除,原第二页的第一条数据"升"到第一页末尾,导致第二页查询再次返回它。
5456

5557
### 子查询
5658

@@ -64,15 +66,20 @@ SELECT * FROM t_order WHERE id > 100000 LIMIT 10
6466
6567
```sql
6668
-- 先通过子查询在主键索引上进行偏移,快速找到起始ID
67-
SELECT * FROM t_order WHERE id >= (SELECT id FROM t_order LIMIT 1000000, 1) LIMIT 10;
69+
SELECT * FROM t_order
70+
WHERE id >= (
71+
SELECT id FROM t_order ORDER BY id LIMIT 1000000, 1
72+
) ORDER BY id LIMIT 10;
6873
```
6974

7075
**工作原理**:
7176

72-
1. 子查询 `(SELECT id FROM t_order where id > 1000000 limit 1)` 会利用主键索引快速定位到第 1000001 条记录,并返回其 ID 值。
73-
2. 主查询 `SELECT * FROM t_order WHERE id >= ... LIMIT 10` 将子查询返回的起始 ID 作为过滤条件,使用 `id >=` 获取从该 ID 开始的后续 10 条记录。
77+
1. 子查询 `(SELECT id FROM t_order ORDER BY id LIMIT 1000000, 1)` 利用主键索引扫描并跳过前 1000000 条记录,返回第 1000001 条记录的主键值。
78+
2. 主查询 `SELECT * FROM t_order WHERE id >= ... ORDER BY id LIMIT 10` 以该主键为起点,获取后续 10 条完整记录。
79+
80+
不过,某些情况下子查询可能会产生临时表,影响性能,因此在复杂查询中建议优先考虑延迟关联。
7481

75-
不过,子查询的结果会产生一张新表,会影响性能,应该尽量避免大量使用子查询。并且,这种方法只适用于 ID 是正序的。在复杂分页场景,往往需要通过过滤条件,筛选到符合条件的 ID,此时的 ID 是离散且不连续的
82+
> **复杂过滤场景**:在包含复杂过滤条件的分页场景中(如 `WHERE status = 1 ORDER BY id LIMIT 1000000, 10`),符合条件的 ID 往往是离散的。此时子查询的优势更加明显:通过在子查询中利用联合索引(如 `(status, id)`)实现覆盖索引扫描,可以高效地跳过前 100 万条符合条件的记录,定位到目标 ID 后,主查询只需回表 10 次
7683
7784
当然,我们也可以利用子查询先去获取目标分页的 ID 集合,然后再根据 ID 集合获取内容,但这种写法非常繁琐,不如使用 INNER JOIN 延迟关联。
7885

@@ -86,22 +93,24 @@ SELECT t1.*
8693
FROM t_order t1
8794
INNER JOIN (
8895
-- 这里的子查询可以利用覆盖索引,性能极高
89-
SELECT id FROM t_order LIMIT 1000000, 10
90-
) t2 ON t1.id = t2.id;
96+
SELECT id FROM t_order ORDER BY id LIMIT 1000000, 10
97+
) t2 ON t1.id = t2.id
98+
ORDER BY t1.id;
9199
```
92100

93101
**工作原理**:
94102

95-
1. 子查询 `(SELECT id FROM t_order where id > 1000000 LIMIT 10)` 利用主键索引快速定位目标分页的 10 条记录的 ID。
103+
1. 子查询 `(SELECT id FROM t_order ORDER BY id LIMIT 1000000, 10)` 利用主键索引扫描并跳过前 1000000 条记录,返回目标分页的 10 条记录的 ID。
96104
2. 通过 `INNER JOIN` 将子查询结果与主表 `t_order` 关联,获取完整的记录数据。
97105

98106
除了使用 INNER JOIN 之外,还可以使用逗号连接子查询。
99107

100108
```sql
101109
-- 使用逗号进行延迟关联
102110
SELECT t1.* FROM t_order t1,
103-
(SELECT id FROM t_order where id > 1000000 LIMIT 10) t2
104-
WHERE t1.id = t2.id;
111+
(SELECT id FROM t_order ORDER BY id LIMIT 1000000, 10) t2
112+
WHERE t1.id = t2.id
113+
ORDER BY t1.id;
105114
```
106115

107116
**注意**: 虽然逗号连接子查询也能实现类似的效果,但为了代码可读性和可维护性,建议使用更规范的 `INNER JOIN` 语法。
@@ -112,11 +121,14 @@ WHERE t1.id = t2.id;
112121

113122
**覆盖索引的好处:**
114123

115-
- **避免 InnoDB 表进行索引的二次查询,也就是回表操作:** InnoDB 是以聚集索引的顺序来存储的,对于 InnoDB 来说,二级索引在叶子节点中所保存的是行的主键信息,如果是用二级索引查询数据的话,在查找到相应的键值后,还要通过主键进行二次查询才能获取我们真实所需要的数据。而在覆盖索引中,二级索引的键值中可以获取所有的数据,避免了对主键的二次查询(回表),减少了 IO 操作,提升了查询效率。
116-
- **可以把随机 IO 变成顺序 IO 加快查询效率:** 由于覆盖索引是按键值的顺序存储的,对于 IO 密集型的范围查找来说,对比随机从磁盘读取每一行的数据 IO 要少的多,因此利用覆盖索引在访问时也可以把磁盘的随机读取的 IO 转变成索引查找的顺序 IO。
124+
- **避免 InnoDB 表进行索引的二次查询,也就是回表操作**:InnoDB 是以聚集索引的顺序来存储的,对于 InnoDB 来说,二级索引在叶子节点中所保存的是行的主键信息,如果是用二级索引查询数据的话,在查找到相应的键值后,还要通过主键进行二次查询才能获取我们真实所需要的数据。而在覆盖索引中,二级索引的键值中可以获取所有的数据,避免了对主键的二次查询(回表),减少了 IO 操作,提升了查询效率。
125+
- **减少回表带来的随机 IO**:通过覆盖索引直接返回数据,避免了根据二级索引的主键值回表查询聚簇索引的随机 IO 操作。回表时每次按主键值查找聚簇索引,本质上是随机 IO。
126+
127+
假设建立了 `(code, type)` 联合索引,下面的查询即可使用覆盖索引:
117128

118129
```sql
119-
# 如果只需要查询 id, code, type 这三列,可建立 code 和 type 的覆盖索引
130+
# 在 InnoDB 中,辅助索引天然包含主键 id
131+
# 如果只需要查询 id, code, type 这三列,只需建立 (code, type) 的联合索引即可实现覆盖
120132
SELECT id, code, type FROM t_order
121133
ORDER BY code
122134
LIMIT 1000000, 10;
@@ -127,18 +139,34 @@ LIMIT 1000000, 10;
127139
- 当查询的结果集占表的总行数的很大一部分时,MySQL 查询优化器可能选择放弃使用索引,自动转换为全表扫描。
128140
- 虽然可以使用 `FORCE INDEX` 强制查询优化器走索引,但这种方式可能会导致查询优化器无法选择更优的执行计划,效果并不总是理想。
129141

142+
## 生产落地建议
143+
144+
### 监控与告警
145+
146+
- **慢查询监控**:监控慢查询日志中 `LIMIT` 偏移量过大的 SQL,及时发现问题。
147+
- **阈值告警**:设置 `long_query_time` 阈值捕获深度分页查询。
148+
- **执行计划检查**:使用 `EXPLAIN` 定期检查关键分页 SQL 的执行计划,确保优化器按预期使用索引。
149+
150+
### 常见误区
151+
152+
| 误区 | 事实 |
153+
| --------------------------------- | ---------------------------------------------------- |
154+
| 认为 `FORCE INDEX` 能解决所有问题 | 强制索引可能阻止优化器选择更优计划,应谨慎使用 |
155+
| 认为覆盖索引适用于所有场景 | 字段过多时索引维护成本高,且大结果集仍可能走全表扫描 |
156+
| 认为游标分页能解决所有问题 | 游标分页不支持跳页,且只能按特定字段顺序翻页 |
157+
130158
## 总结
131159

132160
深度分页问题的根本原因在于:当 `LIMIT` 的偏移量过大时,MySQL 需要扫描并跳过大量记录才能获取目标数据,查询优化器可能放弃索引而选择全表扫描。此时即使有索引,也无法避免大量的回表操作,导致查询性能急剧下降。
133161

134162
本文介绍了四种常见的深度分页优化方案,各方案的特点及适用场景对比如下:
135163

136-
| 优化方案 | 核心思路 | 适用场景 | 限制 |
137-
| ------------ | ------------------------------------------------------------------- | ----------------------------------- | ------------------------------------------------ |
138-
| **范围查询** | 记录上一页最后一条 ID,通过 `WHERE id > last_id LIMIT n` 获取下一页 | ID 连续、按 ID 排序、允许游标式翻页 | 不支持跳页、ID 不连续时失效、非 ID 排序不适用 |
139-
| **子查询** | 先通过子查询获取起始主键,再根据主键过滤 | 需要支持传统 OFFSET 翻页 | 子查询可能产生临时表、仅适用于 ID 正序 |
140-
| **延迟关联** |`INNER JOIN` 将分页转移到主键索引,减少回表 | 大数据量分页、需要传统翻页逻辑 | SQL 相对复杂 |
141-
| **覆盖索引** | 建立包含查询字段的联合索引,避免回表 | 查询字段固定、可建立合适索引 | 字段较多时索引维护成本高、大结果集可能走全表扫描 |
164+
| 优化方案 | 核心思路 | 适用场景 | 限制 |
165+
| ------------ | ------------------------------------------------------------------- | ------------------------------ | ------------------------------------------------ |
166+
| **范围查询** | 记录上一页最后一条 ID,通过 `WHERE id > last_id LIMIT n` 获取下一页 | 按 ID 排序、允许游标式翻页 | 不支持跳页、非 ID 排序需使用联合游标 |
167+
| **子查询** | 先通过子查询获取起始主键,再根据主键过滤 | 需要支持传统 OFFSET 翻页 | 子查询可能产生临时表、依赖排序字段的索引 |
168+
| **延迟关联** |`INNER JOIN` 将分页转移到主键索引,减少回表 | 大数据量分页、需要传统翻页逻辑 | SQL 相对复杂 |
169+
| **覆盖索引** | 建立包含查询字段的联合索引,避免回表 | 查询字段固定、可建立合适索引 | 字段较多时索引维护成本高、大结果集可能走全表扫描 |
142170

143171
**方案选择建议**
144172

0 commit comments

Comments
 (0)