Merge pull request #1462 from CyC2018/add-high-frequency-topics

补充线程池、Spring、RPC、深分页等内容
This commit is contained in:
CyC2018
2026-10-26 03:49:19 +08:00
committed by GitHub
23 changed files with 1217 additions and 0 deletions
+1
View File
@@ -53,6 +53,7 @@
- [Java 并发](https://github.com/CyC2018/CS-Notes/blob/master/notes/Java%20并发.md)
- [Java 虚拟机](https://github.com/CyC2018/CS-Notes/blob/master/notes/Java%20虚拟机.md)
- [Java I/O](https://github.com/CyC2018/CS-Notes/blob/master/notes/Java%20IO.md)
- [Spring](https://github.com/CyC2018/CS-Notes/blob/master/notes/Spring.md)
## :bulb: 系统设计
+37
View File
@@ -481,6 +481,40 @@ Session 可以存储在服务器上的文件、数据库或者内存中。也可
- Cookie 存储在浏览器中,容易被恶意查看。如果非要将一些隐私数据存在 Cookie 中,可以将 Cookie 值进行加密,然后在服务器进行解密;
- 对于大型网站,如果用户所有的信息都存储在 Session 中,那么开销是非常大的,因此不建议将所有的用户信息都存储到 Session 中。
#### 11. JWT
JSON Web Token(JWT)是一种自带签名的令牌。用户登录成功后,服务器把用户 ID、过期时间等信息签名后生成 JWT 返回给客户端,客户端之后在请求的 Authorization 首部中携带它。服务器验证签名,就能确认令牌是自己签发的、内容没有被修改,不需要像 Session 那样在服务器保存会话。
JWT 由三部分组成,用 . 连接:
```html
Authorization: Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJ1aWQiOjEwMjQsInJvbGUiOiJ1c2VyIiwiZXhwIjoxNzY3MjI1NjAwfQ.35OYguRMXfGxb4cYZPiP9KDH3-Ny6vMNwrPe-lNHBBw
```
- 头部:{"alg":"HS256","typ":"JWT"} 的 Base64URL 编码,表示用 HMAC-SHA256 计算签名;
- 载荷:{"uid":1024,"role":"user","exp":1767225600} 的 Base64URL 编码,exp 是过期时间 2026-01-01 00:00:00(UTC);
- 签名:用只有服务器知道的密钥,对“头部.载荷”计算 HMAC-SHA256。
头部和载荷只是编码而不是加密,任何人都能解码看到内容,所以载荷中不能放密码等敏感信息。签名的作用是防篡改:如果把载荷中的 role 改成 admin,没有密钥就算不出新的签名,服务器按篡改后的内容重新计算的签名与令牌中的不一致,请求会被拒绝:
```text
令牌中的签名 35OYguRMXfGxb4cYZPiP9KDH3-Ny6vMNwrPe-lNHBBw
按篡改后的内容计算 Az4UO74FtqslT1MDLrbi7CMlKtd4KICR1jcwA7rJE4U
```
JWT 与 Session 的区别:
- Session 的状态保存在服务器,删除 Session 就能让用户立即下线;集群中需要共享 Session,见 [集群下的 Session 管理](集群.md#二集群下的-session-管理);
- JWT 的状态保存在令牌中,任何持有密钥的服务都能独立验证,适合多个服务共用登录状态;代价是令牌签发后在过期前一直有效,服务器无法主动让它失效。
无法主动失效是 JWT 最常被追问的问题,常用的解决方法是双令牌:访问令牌(Access Token)的有效期很短,例如 15 分钟,每次请求都携带;刷新令牌(Refresh Token)的有效期较长,服务器记录已签发的刷新令牌,只在访问令牌过期时用它换取新的访问令牌。用户退出或者被封禁时撤销刷新令牌,已经发出的访问令牌最多 15 分钟后自然失效。需要立即失效时,可以把令牌的 ID 放入 Redis 黑名单,过期时间设为令牌的剩余有效期,但这又回到了在服务器保存状态。
令牌在浏览器中的存放位置:放在 localStorage 中,页面上的任何脚本都能读取,一旦存在 XSS 漏洞,令牌就会被盗走;放在 HttpOnly 的 Cookie 中,脚本读不到,但浏览器会自动携带 Cookie,需要防范 [跨站请求伪造](攻击技术.md#二跨站请求伪造)。
需要注意的是,服务器验证时要固定使用的签名算法,不能接受头部中 alg 为 none 的令牌;HMAC 的密钥要随机生成,长度不少于 256 位。
JWT 只是令牌的格式,而 OAuth 2.0 是授权框架,规定第三方应用如何获得访问令牌,这个访问令牌可以用 JWT 表示。
### 缓存
#### 1. 优点
@@ -920,6 +954,9 @@ DELETE /idX/delete HTTP/1.1 -> Returns 404
## 参考资料
- [RFC 7519: JSON Web Token (JWT)](https://www.rfc-editor.org/rfc/rfc7519)
- [OWASP: JSON Web Token Cheat Sheet](https://cheatsheetseries.owasp.org/cheatsheets/JSON_Web_Token_Cheat_Sheet.html)
- [OWASP: HTML5 Security Cheat Sheet](https://cheatsheetseries.owasp.org/cheatsheets/HTML5_Security_Cheat_Sheet.html)
- 上野宣. 图解 HTTP[M]. 人民邮电出版社, 2014.
- [MDN : HTTP](https://developer.mozilla.org/en-US/docs/Web/HTTP)
- [HTTP/2 简介](https://developers.google.com/web/fundamentals/performance/http2/?hl=zh-cn)
+147
View File
@@ -8,6 +8,7 @@
* [实现接口 VS 继承 Thread](#实现接口-vs-继承-thread)
* [二、基础线程机制](#二基础线程机制)
* [Executor](#executor)
* [ThreadPoolExecutor](#threadpoolexecutor)
* [Daemon](#daemon)
* [sleep()](#sleep)
* [yield()](#yield)
@@ -24,6 +25,7 @@
* [join()](#join)
* [wait() notify() notifyAll()](#wait-notify-notifyall)
* [await() signal() signalAll()](#await-signal-signalall)
* [交替打印](#交替打印)
* [六、线程状态](#六线程状态)
* [新建(NEW)](#新建new)
* [可运行(RUNNABLE)](#可运行runnable)
@@ -167,6 +169,78 @@ public static void main(String[] args) {
}
```
### ThreadPoolExecutor
上面的三种 Executor 都是通过 ThreadPoolExecutor 创建的,它的构造函数有以下参数:
- corePoolSize:核心线程数,核心线程空闲时也不会被回收,除非调用了 allowCoreThreadTimeOut(true);
- maximumPoolSize:最大线程数;
- keepAliveTime 和 unit:线程数超过核心线程数时,多出的线程空闲超过这个时间就会被回收;
- workQueue:保存等待执行的任务的阻塞队列;
- threadFactory:创建线程的工厂,常用来给线程起有意义的名字,方便排查问题;
- handler:拒绝策略,线程数达到最大并且队列已满时,用来处理新提交的任务。
提交一个任务时,线程池按以下顺序处理:
1. 线程数小于核心线程数时,新建线程执行任务,即使其它线程是空闲的;
2. 否则把任务放入队列;
3. 队列已满时,如果线程数小于最大线程数,新建线程执行这个任务;
4. 否则执行拒绝策略。
例如核心线程数为 2、最大线程数为 4、队列容量为 2,依次提交 7 个各执行 1 秒的任务:
<div align="center"> <img src="pics/thread-pool.png" width="700px"/> </div><br>
```java
ThreadPoolExecutor pool = new ThreadPoolExecutor(2, 4, 60, TimeUnit.SECONDS,
new ArrayBlockingQueue<>(2), new ThreadPoolExecutor.AbortPolicy());
for (int i = 1; i <= 7; i++) {
try {
pool.execute(() -> {
try {
Thread.sleep(1000);
} catch (InterruptedException ignored) {
}
});
System.out.println("提交任务 " + i + ":线程数 " + pool.getPoolSize()
+ ",队列中 " + pool.getQueue().size() + " 个");
} catch (RejectedExecutionException e) {
System.out.println("提交任务 " + i + ":被拒绝");
}
}
```
```text
提交任务 1:线程数 1,队列中 0 个
提交任务 2:线程数 2,队列中 0 个
提交任务 3:线程数 2,队列中 1 个
提交任务 4:线程数 2,队列中 2 个
提交任务 5:线程数 3,队列中 2 个
提交任务 6:线程数 4,队列中 2 个
提交任务 7:被拒绝
```
需要注意的是,任务 5、6 比先提交的任务 3、4 更早执行:队列满后新建的线程直接执行当前提交的任务,队列中的任务要等有线程空闲后才会被取出。
拒绝策略:
- AbortPolicy:默认策略,抛出 RejectedExecutionException;
- CallerRunsPolicy:由提交任务的线程自己执行这个任务,提交方被拖慢,从而降低了提交速度;
- DiscardPolicy:直接丢弃任务;
- DiscardOldestPolicy:丢弃队列中最早的任务,再重新提交当前任务。
不建议用 Executors 创建线程池,而是直接创建 ThreadPoolExecutor 并使用有界队列:
- newFixedThreadPool() 和 newSingleThreadExecutor() 使用没有容量限制的 LinkedBlockingQueue,任务堆积时可能耗尽内存,最大线程数也不再起作用;
- newCachedThreadPool() 的最大线程数为 Integer.MAX_VALUE,任务多时会创建大量线程。
线程数可以按任务类型估算:
- CPU 密集型:取 CPU 核数 + 1,多出的一个线程用于在某个线程因缺页等原因暂停时继续使用 CPU;
- I/O 密集型:线程等待 I/O 时不占用 CPU,可以按 核数 × (1 + 等待时间 / 计算时间) 估算。例如 8 核 CPU,每个任务计算 10 ms、等待数据库 90 ms,约为 8 × (1 + 90 / 10) = 80 个线程。
估算值只是起点,最终要通过压测确定。上线后监控活跃线程数、队列长度和拒绝次数,必要时调用 setCorePoolSize()、setMaximumPoolSize() 在运行时调整。
### Daemon
守护线程是程序运行时在后台提供服务的线程,不属于程序中不可或缺的部分。
@@ -678,6 +752,76 @@ before
after
```
### 交替打印
多个线程按固定顺序轮流执行是常见的手写题,例如三个线程依次打印 A、B、C,共打印 10 轮。思路是用一个共享变量记录轮到哪个线程:没轮到的线程等待,轮到的线程打印后把轮次交给下一个线程,并唤醒等待的线程。
```java
public class PrintABC {
private final Object lock = new Object();
private int turn = 0; // 轮到第几个线程:0、1、2
public void print(int id, char c, int times) throws InterruptedException {
for (int i = 0; i < times; i++) {
synchronized (lock) {
while (turn != id) {
lock.wait();
}
System.out.print(c);
turn = (turn + 1) % 3;
lock.notifyAll();
}
}
}
public static void main(String[] args) {
PrintABC p = new PrintABC();
for (int i = 0; i < 3; i++) {
int id = i;
new Thread(() -> {
try {
p.print(id, (char) ('A' + id), 10);
} catch (InterruptedException ignored) {
}
}).start();
}
}
}
```
```text
ABCABCABCABCABCABCABCABCABCABC
```
有两处容易写错:
- 等待条件要用 while 而不是 if:线程被唤醒时不一定轮到了自己,例如和其它线程一起被 notifyAll() 唤醒,或者发生了虚假唤醒,醒来后必须重新检查条件;
- 要用 notifyAll() 而不是 notify():notify() 只唤醒任意一个等待的线程,例如 B 打印完后唤醒的是 A,A 发现没轮到自己又继续等待,C 就再也不会被唤醒,三个线程都在等待。
把上面代码中的 notifyAll() 改成 notify() 后运行 100 次,每次都在打印出几个字母后卡住;把 while 改成 if 后,输出变成了 ABAABAAB 这样的乱序。
用 Semaphore 实现更直观:每个线程对应一个信号量,只有第一个线程的许可为 1。线程打印前获取自己的许可,打印后释放下一个线程的许可,就像传递接力棒:
```java
Semaphore[] s = {new Semaphore(1), new Semaphore(0), new Semaphore(0)};
for (int i = 0; i < 3; i++) {
int id = i;
new Thread(() -> {
for (int k = 0; k < 10; k++) {
try {
s[id].acquire();
} catch (InterruptedException e) {
return;
}
System.out.print((char) ('A' + id));
s[(id + 1) % 3].release();
}
}).start();
}
```
两个线程交替打印奇偶数也是同样的做法,把轮次改为两个即可。使用 ReentrantLock 时,可以为每个线程创建一个 Condition,打印后只唤醒下一个线程。
## 六、线程状态
一个线程只能处于一种状态,并且这里的线程状态特指 Java 虚拟机的线程状态,不能反映线程在特定操作系统下的状态。
@@ -1621,6 +1765,9 @@ JDK 1.6 引入了偏向锁和轻量级锁,从而让锁拥有了四个状态:
## 参考资料
- [Object (Java SE 21)](https://docs.oracle.com/en/java/javase/21/docs/api/java.base/java/lang/Object.html)
- Brian Goetz. Java 并发编程实战 [M]. 机械工业出版社, 2012.
- [ThreadPoolExecutor (Java SE 21)](https://docs.oracle.com/en/java/javase/21/docs/api/java.base/java/util/concurrent/ThreadPoolExecutor.html)
- BruceEckel. Java 编程思想: 第 4 版 [M]. 机械工业出版社, 2007.
- 周志明. 深入理解 Java 虚拟机 [M]. 机械工业出版社, 2011.
- [Threads and Locks](https://docs.oracle.com/javase/specs/jvms/se6/html/Threads.doc.html)
+84
View File
@@ -26,6 +26,10 @@
* [类加载器分类](#类加载器分类)
* [双亲委派模型](#双亲委派模型)
* [自定义类加载器实现](#自定义类加载器实现)
* [五、线上问题排查](#五线上问题排查)
* [内存溢出与内存泄漏](#内存溢出与内存泄漏)
* [CPU 占用过高](#cpu-占用过高)
* [死锁](#死锁)
* [参考资料](#参考资料)
<!-- GFM-TOC -->
@@ -738,8 +742,88 @@ public class FileSystemClassLoader extends ClassLoader {
}
```
## 五、线上问题排查
### 内存溢出与内存泄漏
OutOfMemoryError 的详细信息指明了是哪一块内存不足,常见的有:
- Java heap space:堆中无法再分配对象,可能是堆设置得太小,也可能是内存泄漏;
- GC overhead limit exceeded:GC 占用了约 98% 的时间,却只回收了不到 2% 的堆,说明存活的对象几乎占满了堆;
- Metaspace:加载的类太多,元空间超过了 MaxMetaspaceSize,常见于运行时动态生成大量的类。
内存泄漏指的是对象已经不再使用,但仍然被引用着,GC 无法回收,堆内存持续增长,最终导致内存溢出。常见原因:
- 静态集合只添加不清理,例如用 static Map 做缓存却没有淘汰策略;
- 线程池中的线程使用 ThreadLocal 后没有调用 remove(),线程一直存活,值也一直无法被回收;
- 连接、流等资源没有关闭;
- 注册的监听器、回调没有注销。
排查步骤:
1. 启动参数加上 -XX:+HeapDumpOnOutOfMemoryError,发生内存溢出时自动保存堆转储,用 -XX:HeapDumpPath 指定保存位置;进程还在运行时,可以用 jcmd <pid> GC.heap_dump <文件名> 手动导出;
2. 用 Eclipse MAT 等工具打开堆转储,找到占用内存最多的对象,查看它们到 GC Roots 的引用链,确定是谁持有了这些对象;
3. 引用链指向不应该存活的对象(例如上面的静态集合)就是内存泄漏,需要修改代码;否则说明确实需要这么多内存,应该增大堆或者减少一次加载的数据量。
例如下面的程序不断向静态列表中添加 1 MB 的数组,静态变量引用的对象属于 GC Roots,这些数组都无法被回收:
```java
public class Leak {
static final List<byte[]> CACHE = new ArrayList<>();
public static void main(String[] args) {
while (true) {
CACHE.add(new byte[1024 * 1024]);
}
}
}
```
```text
$ java -Xmx32m -XX:+HeapDumpOnOutOfMemoryError Leak
java.lang.OutOfMemoryError: Java heap space
Dumping heap to java_pid24607.hprof ...
Heap dump file created [19154891 bytes in 0.007 secs]
```
### CPU 占用过高
1. 用 top 找到 CPU 占用高的 Java 进程;
2. 用 top -H -p <pid> 查看这个进程中每个线程的 CPU 占用,记下占用最高的线程 ID;
3. 用 jstack <pid> 或者 jcmd <pid> Thread.print 导出所有线程的调用栈,按线程 ID 找到对应的线程;
4. 查看这个线程正在执行的代码,常见原因有死循环、频繁 GC(这时占用 CPU 的是 GC 线程)。
调用栈中的 nid 就是操作系统中的线程 ID。JDK 18 起 nid 以十进制显示,可以直接和 top 中的线程 ID 对应;更早的版本以 0x 开头的十六进制显示,需要先用 printf '%x\n' <线程 ID> 转换。JDK 11 起每个线程还带有累计占用的 CPU 时间 cpu=。例如一个执行死循环的线程:
```text
"busy-worker" #20 [26883] prio=5 os_prio=31 cpu=2148.39ms elapsed=2.15s tid=0x000000012f811200 nid=26883 runnable [0x000000016dfda000]
java.lang.Thread.State: RUNNABLE
at Busy.lambda$main$0(Busy.java:3)
```
### 死锁
jstack 会自动检测死锁,在输出的末尾给出互相等待的线程,以及每个线程持有和等待的锁:
```text
Found one Java-level deadlock:
=============================
"t1":
waiting to lock monitor 0x0000000140e63bb0 (object 0x000000061fc144c8, a java.lang.Object),
which is held by "t2"
"t2":
waiting to lock monitor 0x0000000141818190 (object 0x000000061fc144b8, a java.lang.Object),
which is held by "t1"
```
后面还会列出这两个线程的调用栈,指出它们在哪一行获取锁。避免死锁的方法见 [计算机操作系统 - 死锁](计算机操作系统%20-%20死锁.md)。
## 参考资料
- [Troubleshooting Memory Leaks (Java SE 21)](https://docs.oracle.com/en/java/javase/21/troubleshoot/troubleshooting-memory-leaks.html)
- [JDK-8268425: Show decimal nid of OSThread instead of hex format one](https://bugs.openjdk.org/browse/JDK-8268425)
- [JDK-8200720: Print additional information in thread dump](https://bugs.openjdk.org/browse/JDK-8200720)
- 周志明. 深入理解 Java 虚拟机 [M]. 机械工业出版社, 2011.
- [Chapter 2. The Structure of the Java Virtual Machine](https://docs.oracle.com/javase/specs/jvms/se8/html/jvms-2.html#jvms-2.5.4)
- [Jvm memory](https://www.slideshare.net/benewu/jvm-memory)
+59
View File
@@ -11,6 +11,7 @@
* [使用 Explain 进行分析](#使用-explain-进行分析)
* [优化数据访问](#优化数据访问)
* [重构查询方式](#重构查询方式)
* [深分页](#深分页)
* [三、存储引擎](#三存储引擎)
* [InnoDB](#innodb)
* [MyISAM](#myisam)
@@ -202,8 +203,14 @@ Explain 用来分析 SELECT 查询语句,开发人员可以通过分析 Explai
比较重要的字段有:
- select_type : 查询类型,有简单查询、联合查询、子查询等
- type : 访问类型,从好到差常见的有 const、eq_ref、ref、range、index、ALL。const 表示通过主键或唯一索引最多匹配一行,ref 表示通过普通索引等值匹配,range 表示索引范围扫描,index 表示扫描整个索引树,ALL 表示全表扫描
- key : 使用的索引
- rows : 扫描的行数
- Extra : 额外信息,Using index 表示使用了覆盖索引,不需要回表;Using index condition 表示使用了索引下推;Using filesort 表示无法利用索引的顺序,需要额外排序;Using temporary 表示使用了临时表,常见于 GROUP BY 和 ORDER BY 的列不一致
出现 ALL、Using filesort 或 Using temporary,并且 rows 很大时,通常需要调整索引或者改写查询。
找出慢查询可以使用慢查询日志:开启 slow_query_log 后,执行时间超过 long_query_time(默认 10 秒)的语句会被记录下来,再用 mysqldumpslow 按执行时间、次数汇总分析。
### 优化数据访问
@@ -258,6 +265,54 @@ SELECT * FROM tag_post WHERE tag_id=1234;
SELECT * FROM post WHERE post.id IN (123,456,567,9098,8904);
```
### 深分页
深分页指的是偏移量很大的分页查询。例如 200 万行的订单表 orders,create_time 上有普通索引 idx_create_time,按创建时间翻到第 100001 页:
```sql
SELECT * FROM orders ORDER BY create_time LIMIT 1000000, 10;
```
MySQL 没办法直接跳到第 1000001 行,只能先找出排在前面的 1000010 行,丢弃前 1000000 行,再返回最后 10 行,偏移量越大越慢。
<div align="center"> <img src="pics/deep-pagination.png" width="720px"/> </div><br>
这条查询的执行计划中 type 为 ALL、Extra 为 Using filesort,也就是全表扫描后排序。这是因为如果走 idx_create_time,读出的 1000010 条索引项都要回表取整行,产生上百万次随机读,优化器判断这样还不如全表扫描。
延迟关联指的是先在索引上查出这一页的主键,再根据主键回表。子查询只用到 create_time 和 id,idx_create_time 就是覆盖索引(Extra 为 Using index),虽然仍要扫描 1000010 条索引项,但不需要回表,只有最后 10 行需要回表:
```sql
SELECT o.* FROM orders o
JOIN (SELECT id FROM orders ORDER BY create_time LIMIT 1000000, 10) t ON o.id = t.id
ORDER BY o.create_time;
```
游标分页(也叫书签分页)指的是记住上一页最后一行的排序值,下一页从它之后开始查。这样可以直接在索引上定位到上一页结束的位置,只读 10 行:
```sql
-- 上一页最后一行:create_time = '2025-01-12 13:46:39',id = 982321
SELECT * FROM orders
WHERE create_time > '2025-01-12 13:46:39'
OR (create_time = '2025-01-12 13:46:39' AND id > 982321)
ORDER BY create_time, id
LIMIT 10;
```
排序列的值可能重复,所以要再按主键排序,并把主键也作为游标的一部分,否则时间相同的行会被跳过或者重复返回。游标分页的代价是不能直接跳到任意一页,只能逐页翻,适合 App 下拉加载、按时间浏览记录这类场景。
在 MySQL 8.0 上实测的结果如下(数据全部在内存中,取 5 次中最快的一次),三种写法返回的 10 行相同:
| 写法 | 耗时 | 读取的数据 |
| --- | --- | --- |
| LIMIT 0, 10 | 0.05 ms | 10 行 |
| LIMIT 1000000, 10 | 1321 ms | 全表 200 万行,再排序 |
| 延迟关联 | 88 ms | 1000010 条索引项,回表 10 次 |
| 游标分页 | 0.13 ms | 10 行 |
还可以从需求上限制翻页的深度,例如只允许翻到前 100 页。Elasticsearch 默认也不允许 from + size 超过 10000,更深的翻页要用 search_after,原理与游标分页相同。
需要注意的是,分页时常常还要显示总记录数。InnoDB 没有保存表的行数,因为并发的事务看到的行数可能不同,COUNT(*) 需要扫描整个索引,上面的表执行一次需要 136 ms。数据量大时可以缓存总数、显示估算值,或者只提供“下一页”。
## 三、存储引擎
### InnoDB
@@ -414,6 +469,10 @@ TIMESTAMP 比 DATETIME 少占用 1 字节,但是只能表示到 2038 年,需
## 参考资料
- [MySQL 8.0: InnoDB Restrictions and Limitations](https://dev.mysql.com/doc/refman/8.0/en/innodb-restrictions-limitations.html)
- [Elasticsearch: Paginate search results](https://www.elastic.co/docs/reference/elasticsearch/rest-apis/paginate-search-results)
- [MySQL 8.0: EXPLAIN Output Format](https://dev.mysql.com/doc/refman/8.0/en/explain-output.html)
- [MySQL 8.0: The Slow Query Log](https://dev.mysql.com/doc/refman/8.0/en/slow-query-log.html)
- Baron Schwartz, Peter Zaitsev, Vadim Tkachenko, 等. 高性能 MySQL[M]. 电子工业出版社, 2013.
- 姜承尧. MySQL 技术内幕: InnoDB 存储引擎 [M]. 机械工业出版社, 2011.
- [20+ 条 MySQL 性能优化的最佳经验](https://www.jfox.info/20-tiao-mysql-xing-nen-you-hua-de-zui-jia-jing-yan.html)
+205
View File
@@ -0,0 +1,205 @@
# Spring
<!-- GFM-TOC -->
* [Spring](#spring)
* [一、IoC 与 Bean](#一ioc-与-bean)
* [IoC 与依赖注入](#ioc-与依赖注入)
* [Bean 的生命周期](#bean-的生命周期)
* [循环依赖](#循环依赖)
* [二、AOP](#二aop)
* [动态代理](#动态代理)
* [自调用](#自调用)
* [三、事务](#三事务)
* [声明式事务](#声明式事务)
* [传播行为](#传播行为)
* [事务失效](#事务失效)
* [四、Spring Boot 自动配置](#四spring-boot-自动配置)
* [参考资料](#参考资料)
<!-- GFM-TOC -->
## 一、IoC 与 Bean
### IoC 与依赖注入
控制反转(Inversion of Control, IoC)指的是对象不再自己创建所依赖的对象,而是由容器创建好之后交给它。例如 OrderService 需要 OrderRepository,它不在内部 new 一个,而是声明自己需要,由 Spring 容器把创建好的对象传进来,这个过程称为依赖注入(Dependency Injection, DI)。
```java
@Service
public class OrderService {
private final OrderRepository repository;
public OrderService(OrderRepository repository) { // 构造器注入
this.repository = repository;
}
}
```
这样做的好处:
- 对象只依赖接口,替换实现、在测试中注入模拟对象都不需要修改代码;
- 对象的创建、作用域和生命周期由容器统一管理,事务、缓存这类功能也是容器在创建对象时织入的。
依赖注入有构造器注入、Setter 注入和字段注入(把 @Autowired 加在字段上)三种方式。Spring 官方推荐构造器注入:依赖可以声明为 final,对象创建出来就是完整可用的,缺少依赖时启动就会失败。
### Bean 的生命周期
一个单例 Bean 从创建到销毁会经过以下步骤:
1. 实例化:调用构造函数创建对象;
2. 属性填充:注入依赖;
3. Aware 回调:例如 BeanNameAware 把 Bean 的名字传给它;
4. BeanPostProcessor 的 postProcessBeforeInitialization();
5. 初始化:依次调用 @PostConstruct 方法、InitializingBean 的 afterPropertiesSet() 和配置的 init-method;
6. BeanPostProcessor 的 postProcessAfterInitialization(),AOP 就是在这一步把 Bean 替换为代理对象的;
7. 容器关闭时销毁:依次调用 @PreDestroy 方法、DisposableBean 的 destroy() 和配置的 destroy-method。
让一个 Bean 实现上面所有的回调,实际运行打印的顺序如下:
```text
1 构造函数
2 依赖注入
3 BeanNameAware: orderService
4 BeanPostProcessor before
5 @PostConstruct
6 afterPropertiesSet
7 init-method
8 BeanPostProcessor after
9 @PreDestroy
10 DisposableBean.destroy
```
### 循环依赖
循环依赖指的是两个 Bean 互相依赖,例如 A 依赖 B,B 又依赖 A。
对于通过字段或者 Setter 注入的单例 Bean,Spring 通过提前暴露还没有初始化完成的对象来解决。DefaultSingletonBeanRegistry 中有三个缓存:
- 一级缓存 singletonObjects:初始化完成的 Bean;
- 二级缓存 earlySingletonObjects:提前暴露的、还没有初始化完成的 Bean;
- 三级缓存 singletonFactories:用于生成提前引用的工厂。
<div align="center"> <img src="pics/spring-circular.png" width="720px"/> </div><br>
需要三级缓存而不是两级,是因为 A 可能需要被 AOP 代理,而代理正常是在初始化的最后一步才创建的。三级缓存中的工厂被调用时,会判断 A 是否需要代理,需要的话提前创建代理返回给 B,之后初始化完成时不再重复创建。这样 B 拿到的对象与最终放入容器的对象是同一个;没有循环依赖时工厂不会被调用,代理仍按正常流程创建。
以下情况无法解决,启动时抛出 BeanCurrentlyInCreationException:
- 构造器注入的循环依赖:创建 A 时必须先有 B,A 还没有实例化,没有可以提前暴露的对象;
- 原型(prototype)作用域的 Bean:每次获取都创建新对象,不会放入缓存。
需要注意的是,Spring Boot 2.6 起默认禁止循环依赖,出现时直接启动失败,设置 spring.main.allow-circular-references=true 才能恢复。循环依赖通常说明职责划分有问题,更好的做法是调整依赖关系。
## 二、AOP
### 动态代理
面向切面编程(Aspect-Oriented Programming, AOP)用于把日志、事务、权限校验这类分散在各处的逻辑从业务代码中抽离出来。Spring AOP 基于动态代理实现:容器中保存的不是原始对象,而是代理对象,调用方法时先执行代理中的切面逻辑,再调用原始对象的方法。
两种代理方式:
- JDK 动态代理:目标类实现了接口时可以使用,代理类实现同样的接口,只能代理接口中的方法;
- CGLIB:在运行时生成目标类的子类,通过重写方法来代理,因此 final 类、final 方法和 private 方法无法被代理。
Spring Framework 在目标类实现了接口时默认使用 JDK 动态代理;Spring Boot 默认使用 CGLIB,可以通过 spring.aop.proxy-target-class 修改。
JDK 动态代理的原理可以用下面的代码说明,切面逻辑写在 InvocationHandler 中:
```java
public interface OrderService {
void create();
}
OrderService target = () -> System.out.println("创建订单");
OrderService proxy = (OrderService) Proxy.newProxyInstance(
OrderService.class.getClassLoader(),
new Class<?>[]{OrderService.class},
(p, method, args) -> {
System.out.println("开启事务");
Object result = method.invoke(target, args);
System.out.println("提交事务");
return result;
});
proxy.create();
```
```text
开启事务
创建订单
提交事务
```
### 自调用
代理只能拦截从外部经过代理的调用。在目标对象内部调用自己的另一个方法时,等价于 this.inner(),调用的是原始对象,不经过代理,切面不会生效:
<div align="center"> <img src="pics/spring-aop-proxy.png" width="680px"/> </div><br>
```java
@Service
public class OrderService {
public void outer() {
inner(); // 自调用,inner() 上的事务不生效
}
@Transactional
public void inner() {
}
}
```
这是 @Transactional、@Async、@Cacheable 等注解失效最常见的原因。解决方法是把被调用的方法移到另一个 Bean 中,或者注入自身的代理,通过代理调用。
## 三、事务
### 声明式事务
@Transactional 同样基于 AOP 实现:代理在方法执行前开启事务,方法正常返回时提交,抛出异常时回滚。
需要注意的是,默认只有 RuntimeException 和 Error 会触发回滚,受检异常(例如 IOException)不会回滚,需要通过 rollbackFor 指定。
### 传播行为
传播行为指的是一个事务方法调用另一个事务方法时,事务如何传递。常用的有以下三种:
- REQUIRED:默认值,当前有事务就加入,没有就新建。内外两个方法属于同一个事务,一起提交或者一起回滚;
- REQUIRES_NEW:挂起当前事务,新建一个独立的事务,两个事务分别提交和回滚,例如无论业务成功与否都要保存的操作日志;
- NESTED:在当前事务中创建一个保存点,内层回滚时只回滚到保存点,外层回滚时内层也一起回滚。
REQUIRED 有一个常见的问题:内层方法抛出异常后,即使外层捕获了这个异常,整个事务也已经被标记为只能回滚,外层方法结束时事务回滚,并抛出 UnexpectedRollbackException。
### 事务失效
事务不生效的常见原因:
- 自调用:同一个类中的方法互相调用,不经过代理;
- 方法无法被代理:final 方法、static 方法和 private 方法;Spring 6.0 之前,非 public 方法上的注解也会被忽略;
- 异常被吞掉:方法内部捕获了异常而没有重新抛出,代理认为方法正常结束,提交了事务;
- 抛出的是受检异常,而没有配置 rollbackFor;
- 对象不是由 Spring 管理的,例如自己 new 出来的对象;
- 在新线程中操作数据库:事务绑定在当前线程上,其它线程中的操作不属于这个事务;
- 数据库不支持事务,例如 MySQL 的 MyISAM 引擎。
## 四、Spring Boot 自动配置
Spring Boot 在 Spring 的基础上提供了自动配置和 starter:starter 把一组常用的依赖打包在一起,例如 spring-boot-starter-web 引入了 Spring MVC、内嵌的 Tomcat 和 JSON 库;自动配置则根据引入的依赖自动创建所需的 Bean,不需要手动配置就能启动一个 Web 应用。
自动配置的过程:
1. 启动类上的 @SpringBootApplication 包含了 @EnableAutoConfiguration;
2. 启动时读取所有 jar 包中的 META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports 文件,得到自动配置类的列表,Spring Boot 2.7 之前写在 META-INF/spring.factories 中;
3. 每个自动配置类都带有条件注解,条件满足时才生效。例如 @ConditionalOnClass 要求类路径上存在某个类,@ConditionalOnMissingBean 要求用户没有定义同类型的 Bean,@ConditionalOnProperty 根据配置项决定。
例如引入 spring-boot-starter-jdbc 和数据库驱动后,数据源的自动配置类就会生效,按配置文件中的地址创建一个 DataSource;如果用户自己定义了 DataSource,由于 @ConditionalOnMissingBean,自动配置不再创建。因此用户的配置总是优先于自动配置。
## 参考资料
- [Spring Framework: Dependency Injection](https://docs.spring.io/spring-framework/reference/core/beans/dependencies/factory-collaborators.html)
- [Spring Framework: Customizing the Nature of a Bean](https://docs.spring.io/spring-framework/reference/core/beans/factory-nature.html)
- [Spring Framework: Container Extension Points](https://docs.spring.io/spring-framework/reference/core/beans/factory-extension.html)
- [Spring Framework: Proxying Mechanisms](https://docs.spring.io/spring-framework/reference/core/aop/proxying.html)
- [Spring Framework: Using @Transactional](https://docs.spring.io/spring-framework/reference/data-access/transaction/declarative/annotations.html)
- [Spring Framework: Transaction Propagation](https://docs.spring.io/spring-framework/reference/data-access/transaction/declarative/tx-propagation.html)
- [Spring Framework 源码:DefaultSingletonBeanRegistry](https://github.com/spring-projects/spring-framework/blob/main/spring-beans/src/main/java/org/springframework/beans/factory/support/DefaultSingletonBeanRegistry.java)
- [Spring Boot: Creating Your Own Auto-configuration](https://docs.spring.io/spring-boot/reference/features/developing-auto-configuration.html)
- [Spring Boot 2.6 Release Notes](https://github.com/spring-projects/spring-boot/wiki/Spring-Boot-2.6-Release-Notes)
- [Spring Boot 2.7 Release Notes](https://github.com/spring-projects/spring-boot/wiki/Spring-Boot-2.7-Release-Notes)
Binary file not shown.

After

Width:  |  Height:  |  Size: 108 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 135 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 168 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 126 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 94 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 162 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 176 KiB

+104
View File
@@ -28,6 +28,11 @@
* [单个 Candidate 的竞选](#单个-candidate-的竞选)
* [多个 Candidate 竞选](#多个-candidate-竞选)
* [日志复制](#日志复制)
* [七、RPC](#七rpc)
* [调用过程](#调用过程)
* [服务发现](#服务发现)
* [RPC 与 HTTP](#rpc-与-http)
* [超时与重试](#超时与重试)
* [参考](#参考)
<!-- GFM-TOC -->
@@ -416,8 +421,107 @@ Leader 会周期性的发送心跳包给 Follower。每个 Follower 都设置了
发生网络分区时,少数派一侧的旧 Leader 得不到大多数节点的确认,无法提交日志,多数派一侧会选出新的 Leader。分区恢复后,旧 Leader 发现更大的任期号就会退回 Follower,它未提交的日志被覆盖,因此不会出现脑裂。
## 七、RPC
远程过程调用(Remote Procedure Call, RPC)指的是像调用本地方法一样调用另一台机器上的服务。例如订单服务调用库存服务扣减库存:
```java
boolean ok = inventoryService.deduct(10086, 2);
```
### 调用过程
调用方写的是普通的方法调用,RPC 框架在背后完成了网络通信:
<div align="center"> <img src="pics/rpc-call.png" width="740px"/> </div><br>
1. inventoryService 是框架用动态代理生成的对象,调用它的方法时,代理拿到接口名、方法名和参数;
2. 把这些信息序列化成字节,加上请求 ID 等首部组成请求帧,从注册中心获取的地址列表中按负载均衡选出一个实例,通过长连接发送;
3. 服务方解码请求,找到对应的实现类,通过反射调用 deduct(),再把返回值按同样的方式发回;
4. 一个连接上可能同时有多个请求在等待响应,调用方根据响应中的请求 ID 把结果交给对应的调用。
去掉工程上的细节,核心只有两部分:调用方用动态代理把方法调用转成网络请求,服务方用反射把网络请求转成方法调用。
```java
// 调用方:生成接口的代理对象,调用方法时把方法名和参数发给服务方
@SuppressWarnings("unchecked")
static <T> T refer(Class<T> api, String host, int port) {
return (T) Proxy.newProxyInstance(api.getClassLoader(), new Class<?>[]{api}, (proxy, method, args) -> {
try (Socket socket = new Socket(host, port)) {
ObjectOutputStream out = new ObjectOutputStream(socket.getOutputStream());
out.writeUTF(method.getName());
out.writeObject(method.getParameterTypes());
out.writeObject(args);
out.flush();
return new ObjectInputStream(socket.getInputStream()).readObject();
}
});
}
// 服务方:读出方法名和参数,通过反射调用本地的实现
static void export(Object impl, int port) throws Exception {
try (ServerSocket server = new ServerSocket(port)) {
while (true) {
try (Socket socket = server.accept()) {
ObjectInputStream in = new ObjectInputStream(socket.getInputStream());
Method method = impl.getClass().getMethod(in.readUTF(), (Class<?>[]) in.readObject());
Object result = method.invoke(impl, (Object[]) in.readObject());
ObjectOutputStream out = new ObjectOutputStream(socket.getOutputStream());
out.writeObject(result);
out.flush();
}
}
}
}
```
```java
// 服务方
export(new InventoryServiceImpl(), 20880);
// 调用方
InventoryService inventoryService = refer(InventoryService.class, "10.0.0.8", 20880);
inventoryService.deduct(10086, 2); // 实际执行的是服务方的 InventoryServiceImpl.deduct()
```
这段代码每次调用都新建连接,使用 Java 自带的序列化,也没有超时处理。Dubbo、gRPC 等框架在此基础上实现了连接复用、高效的序列化、服务发现、负载均衡、超时重试和熔断,设计一个 RPC 框架时也是从这几个模块考虑。
### 服务发现
服务的实例会扩容、缩容或者宕机,地址不能写死在调用方。注册中心(例如 Nacos、ZooKeeper)保存每个服务当前可用的实例:
- 服务方启动时把自己的地址注册到注册中心,之后定期发送心跳,长时间没有心跳的实例会被删除;
- 调用方订阅需要的服务,把地址列表缓存在本地,实例变化时由注册中心推送通知;
- 每次调用都从本地缓存的列表中选择实例,注册中心不转发请求。
因此注册中心全部宕机时,调用方仍然可以按缓存的地址继续调用,只是新上线的实例无法被发现。某个实例宕机后、被注册中心删除之前,发往它的请求会失败,需要调用方重试其它实例。
### RPC 与 HTTP
HTTP 是一种协议,RPC 是一种调用方式,两者不是同一层面的概念:RPC 框架可以在 TCP 上使用自定义的协议,例如 Dubbo 协议;也可以基于 HTTP,例如 gRPC 基于 HTTP/2,Spring Cloud 的 OpenFeign 用动态代理把接口调用转换成 HTTP 请求。
面试中通常问的是:为什么对外接口常用 HTTP + JSON,内部服务之间却常用 RPC 框架。这是因为内部调用量大、对延迟敏感,RPC 框架在这些方面做得更好:
- 序列化更紧凑:例如参数 {"skuId":10086,"count":2},JSON 需要 25 字节;Protobuf 不传字段名,只传字段编号和值,编码为 08 E6 4E 10 02,共 5 字节,其中 08 表示字段 1,E6 4E 是 10086 的变长编码,10 02 表示字段 2 的值为 2;
- 连接复用:在少量长连接上同时发送多个请求,用请求 ID 区分响应,省去频繁建立连接的开销;
- 服务治理:内置服务发现、负载均衡、超时重试和熔断;
- 接口约定:用 Java 接口或者 IDL 文件定义服务,自动生成调用方的代码,参数类型在编译期就能检查。
HTTP + JSON 的优势在于通用:浏览器和各种语言都能直接调用,报文可读,便于调试,适合对外开放的接口。
### 超时与重试
- 每次调用都要设置超时时间,否则下游变慢时,调用方的线程会一直等待,最终被耗尽;
- 调用方超时不代表服务方没有执行:请求可能在网络中延迟,或者在服务方的线程池中排队、遇到 GC 停顿,这些时间都计入了调用方的耗时,却不在服务方记录的处理时间里,于是调用方超时了,服务方却执行成功了;
- 因此只有幂等的接口才能自动重试,例如查询;扣库存这类写操作要带上请求 ID,由服务方去重,否则重试会扣减两次;
- 调用链较长时,下游的超时时间应该比上游短。gRPC 会把截止时间(deadline)随请求传给服务方,超时后调用方收到 DEADLINE_EXCEEDED,服务方也可以检查调用是否已经超时,从而停止处理。
## 参考
- [Apache Dubbo: Architecture](https://dubbo.apache.org/docs/v2.7/user/preface/architecture/)
- [Protocol Buffers: Encoding](https://protobuf.dev/programming-guides/encoding/)
- [gRPC: Core concepts](https://grpc.io/docs/what-is-grpc/core-concepts/)
- [gRPC on HTTP/2](https://grpc.io/blog/grpc-on-http2/)
- 倪超. 从 Paxos 到 ZooKeeper : 分布式一致性原理与实践 [M]. 电子工业出版社, 2015.
- [Designing Data-Intensive Applications 中文版](https://github.com/vonng/ddia)
- [Distributed locks with Redis](https://redis.io/topics/distlock)
+86
View File
@@ -6,6 +6,8 @@
* [三、扩展性](#三扩展性)
* [四、可用性](#四可用性)
* [五、安全性](#五安全性)
* [六、秒杀系统](#六秒杀系统)
* [七、海量数据处理](#七海量数据处理)
* [参考资料](#参考资料)
<!-- GFM-TOC -->
@@ -123,7 +125,91 @@
要求系统在应对各种攻击手段时能够有可靠的应对措施,常见的攻击手段和防范方法见 [攻击技术](攻击技术.md)。
## 六、秒杀系统
秒杀指的是少量商品在某一时刻开售,大量用户同时抢购,例如 100 件商品在开售瞬间收到 10 万个请求。难点在于:
- 瞬时流量远超平时,但绝大多数请求注定失败;
- 库存不能超卖,一个用户只能买一件;
- 抢购不能拖垮正常的业务。
解决思路是层层过滤,使最终到达数据库的请求接近库存数量:
<div align="center"> <img src="pics/seckill.png" width="720px"/> </div><br>
1. 前端和网关:开售前按钮置灰,提交后禁止重复点击,网关按用户和接口限流,拦截刷单请求;
2. 库存预扣:开售前把库存加载到 Redis 中,用 Lua 脚本在 Redis 中判断并扣减库存,没有库存的请求直接返回已抢完;
3. 异步下单:扣减成功的请求写入消息队列,订单服务按自己的处理能力消费,扣减数据库中的库存并创建订单,用户通过轮询等方式查询结果;
4. 隔离:秒杀服务独立部署,避免影响正常的业务。
预扣库存的 Lua 脚本如下:
```lua
-- KEYS[1]:库存,KEYS[2]:已购用户集合,ARGV[1]:用户 ID
if redis.call('SISMEMBER', KEYS[2], ARGV[1]) == 1 then
return 2 -- 一人一单,重复购买
end
if tonumber(redis.call('GET', KEYS[1])) <= 0 then
return 1 -- 库存不足
end
redis.call('DECR', KEYS[1])
redis.call('SADD', KEYS[2], ARGV[1])
return 0 -- 抢购成功
```
Redis 执行 Lua 脚本时不会穿插执行其它命令,判断是否已购、判断库存、扣减库存和记录用户这四步作为一个整体完成。如果在应用中先 GET 库存、判断大于 0 之后再 DECR,两个请求可能同时读到库存为 1,并且都扣减成功。用 64 个并发客户端实测,库存为 100、1000 个用户各请求 2 次:Lua 脚本恰好卖出 100 件;先查询后扣减的写法卖出了 104 件,库存变成了 -4。
数据库也要兜底。扣减库存时把库存大于 0 作为条件,影响的行数为 0 说明库存不足:
```sql
UPDATE goods SET stock = stock - 1 WHERE id = 1001 AND stock > 0;
```
一人一单可以用订单表上(用户 ID,商品 ID)的唯一索引保证,消息被重复消费时插入失败,也就实现了幂等。Redis 中扣减成功、但订单最终创建失败时,需要把 Redis 中的库存和购买记录恢复,并定期与数据库对账。
## 七、海量数据处理
这类问题的特点是数据量远大于内存,例如“100 GB 的文件,内存只有 4 GB”。常用的思路是分而治之:先把数据切成能放进内存的小块,分别处理后再合并结果。
### Top K
从 1 亿个整数中找出最大的 10 个:维护一个大小为 10 的小顶堆,堆顶是当前第 10 大的数。依次读入每个数,比堆顶大就替换堆顶并调整堆,最后堆中的 10 个数就是答案。时间复杂度为 O(NlogK),内存中只需保存 10 个数,数据可以一边读一边处理;而全部排序需要 O(NlogN) 的时间,还要把 400 MB 的数据都读入内存。Java 实现见 [Leetcode 题解 - 排序](Leetcode%20题解%20-%20排序.md#1-kth-element)。
### 出现次数最多的 K 个
100 GB 的日志文件中每行是一个 URL,内存只有 4 GB,找出出现次数最多的 10 个 URL。直接用 HashMap 统计所有 URL 会超出内存,需要先按哈希切分:
<div align="center"> <img src="pics/massive-topk.png" width="720px"/> </div><br>
1. 逐行读取,按 hash(url) % 100 写入 100 个小文件,平均每个约 1 GB;
2. 依次把每个小文件读入内存,用 HashMap 统计每个 URL 的次数,再用小顶堆得到这个文件中出现次数最多的 10 个;
3. 用一个小顶堆从 100 × 10 个候选中选出全局的前 10 个。
这样做的关键是相同的 URL 一定落在同一个小文件中,所以每个文件单独统计出的次数就是这个 URL 的总次数。HashMap 占用的内存取决于文件中不同 URL 的数量,如果某个文件中不同的 URL 仍然太多,可以换一个哈希函数对它再次切分。
### 两个大文件的交集
两个文件各有 50 亿个 URL,每个 URL 64 字节,找出两个文件共同的 URL。每个文件有 320 GB,用同一个哈希函数把两个文件各切成 1000 份,每份约 320 MB。相同的 URL 一定落在编号相同的一对小文件中,只需要依次把 a<sub>i</sub> 读入 HashSet,再逐行检查 b<sub>i</sub>。
如果允许少量误判,可以用 [布隆过滤器](缓存.md#缓存穿透):把 a 中的 URL 都加入布隆过滤器,再逐个检查 b 中的 URL。4 GB 内存约有 344 亿位,每个 URL 平均 6.9 位,使用 5 个哈希函数时误判率约为 3.7%;要把误判率降到 1%,每个 URL 需要约 9.6 位,共需 5.6 GB。
### 去重与判断是否存在
数据是整数时可以用位图,每个整数只用 1 位表示是否出现。int 共有 2<sup>32</sup> 个取值,位图只需要 512 MB。
例如在 2 亿个整数中找出只出现一次的数,可以给每个整数分配 2 位:00 表示没出现,01 表示出现一次,10 表示出现多次,共需 1 GB。读完所有数据后扫描位图,取出状态为 01 的数。
### 外部排序
100 GB 的数据排序,内存只有 1 GB:
1. 分段排序:每次读入 1 GB 数据,在内存中排序后写回磁盘,得到 100 个有序的文件;
2. 多路归并:同时打开这 100 个文件,用大小为 100 的小顶堆,每次取出最小的元素写入结果文件,再从它所在的文件读入下一个元素。
如果数据的取值范围很小,例如 1 亿个 10000 到 20000 之间的整数,一共只有 10001 种取值,用长度为 10001 的数组统计每个值出现的次数,一次扫描就能完成排序,也就是计数排序。
## 参考资料
- [Redis: Scripting with Lua](https://redis.io/docs/latest/develop/programmability/eval-intro/)
- 大型网站技术架构:核心原理与案例分析
- [Designing Data-Intensive Applications 中文版](https://github.com/vonng/ddia)
@@ -5,6 +5,7 @@
* [1. 进程](#1-进程)
* [2. 线程](#2-线程)
* [3. 区别](#3-区别)
* [4. 协程](#4-协程)
* [进程状态的切换](#进程状态的切换)
* [进程调度算法](#进程调度算法)
* [1. 批处理系统](#1-批处理系统)
@@ -68,6 +69,18 @@ QQ 和浏览器是两个进程,浏览器进程里面有很多线程,例如 H
线程间可以通过直接读写同一进程中的数据进行通信,但是进程通信需要借助 IPC。
### 4. 协程
协程(Coroutine)是在用户态实现的轻量级线程,由程序自己调度,而不是由操作系统内核调度。协程在等待 I/O 时主动让出执行权,运行时再把其它协程调度到同一个线程上执行。
与线程相比:
- 切换在用户态完成,只需要保存少量寄存器,不需要陷入内核,开销小;
- 占用的内存少,可以同时存在大量协程,而每个线程都要预留独立的栈,例如 64 位 Linux 上 Java 线程的栈默认为 1 MB,线程数量有限;
- 同一个线程上的协程不能并行执行,要利用多核仍然需要多个线程;协程中如果执行了阻塞的系统调用,所在的线程会被阻塞,所以协程的运行时通常把 I/O 改为非阻塞的方式。
Java 21 正式引入的虚拟线程([JEP 444](https://openjdk.org/jeps/444))就是 JVM 实现的协程:虚拟线程由 JVM 调度到少量的平台线程上运行,执行阻塞 I/O 时从平台线程上卸载,让平台线程去执行其它虚拟线程。虚拟线程的创建成本很低,不需要用线程池复用,需要限制并发数时使用信号量。在 JDK 21 中,虚拟线程在 synchronized 块内阻塞时无法卸载,JDK 24 起([JEP 491](https://openjdk.org/jeps/491))基本消除了这个限制。
## 进程状态的切换
<div align="center"> <img src="https://cs-notes-1256109796.cos.ap-guangzhou.myqcloud.com/ProcessState.png" width="500"/> </div><br>
+48
View File
@@ -0,0 +1,48 @@
<svg xmlns="http://www.w3.org/2000/svg" width="720" height="300" viewBox="0 0 720 300">
<style>
text { font-family: "PingFang SC", "Microsoft YaHei", Helvetica, Arial, sans-serif; fill: #666; font-size: 12px; }
.title { font-weight: bold; font-size: 15px; fill: #777; text-anchor: middle; }
.b { font-weight: bold; }
.c { text-anchor: middle; }
.e { text-anchor: end; }
.note { fill: #C0605A; }
.ok { fill: #6A9F68; }
.sub { font-size: 11px; fill: #888; }
.mono { font-family: Menlo, Consolas, monospace; }
.arrow { stroke: #666; stroke-width: 1.3; fill: none; marker-end: url(#a); }
.red { stroke: #C0605A; marker-end: url(#r); }
.green { stroke: #6A9F68; marker-end: url(#g); }
.dash { stroke-dasharray: 5 4; }
.line { stroke: #666; stroke-width: 1.3; fill: none; }
.frame { fill: none; stroke: #999; stroke-width: 1; stroke-dasharray: 4 3; }
.shadow { fill: #DADADA; }
</style>
<defs>
<marker id="a" viewBox="0 0 10 10" refX="9" refY="5" markerWidth="7" markerHeight="7" orient="auto-start-reverse">
<path d="M1,1 L9,5 L1,9" fill="none" stroke="#666" stroke-width="1.6"/>
</marker>
<marker id="r" viewBox="0 0 10 10" refX="9" refY="5" markerWidth="7" markerHeight="7" orient="auto-start-reverse">
<path d="M1,1 L9,5 L1,9" fill="none" stroke="#C0605A" stroke-width="1.6"/>
</marker>
<marker id="g" viewBox="0 0 10 10" refX="9" refY="5" markerWidth="7" markerHeight="7" orient="auto-start-reverse">
<path d="M1,1 L9,5 L1,9" fill="none" stroke="#6A9F68" stroke-width="1.6"/>
</marker>
</defs>
<rect width="100%" height="100%" fill="#FFFFFF"/>
<text x="360" y="28" class="title">深分页为什么慢</text>
<text x="20" y="62" class="b">LIMIT 1000000, 10:从头数过 1000000 条,丢弃后再取 10 条</text>
<rect x="20" y="76" width="600" height="26" rx="4" fill="#F2F2F2" stroke="#DDD"/>
<rect x="20" y="76" width="560" height="26" rx="4" fill="#E6E6E6"/>
<text x="300" y="93" class="c sub">读取后丢弃 1000000 条</text>
<rect x="580" y="76" width="12" height="26" fill="#9188D8"/>
<line x1="26" y1="116" x2="574" y2="116" class="arrow "/>
<text x="630" y="93" class="sub">返回 10 条</text>
<text x="20" y="160" class="b">游标分页:从上一页最后一条之后开始,直接定位</text>
<path d="M50,210 C220,170 490,170 582,214" class="arrow"/>
<text x="310" y="178" class="c sub">沿 B+ 树从根节点向下查找,只读几个节点</text>
<rect x="20" y="216" width="600" height="26" rx="4" fill="#F2F2F2" stroke="#DDD"/>
<rect x="580" y="216" width="12" height="26" fill="#9188D8"/>
<text x="630" y="233" class="sub">返回 10 条</text>
<text x="20" y="284" class="sub">200 万行的表上实测:LIMIT 1000000, 10 耗时 1321 ms,游标分页 0.13 ms,并且与翻到第几页无关。</text>
</svg>

After

Width:  |  Height:  |  Size: 2.8 KiB

+84
View File
@@ -0,0 +1,84 @@
<svg xmlns="http://www.w3.org/2000/svg" width="720" height="300" viewBox="0 0 720 300">
<style>
text { font-family: "PingFang SC", "Microsoft YaHei", Helvetica, Arial, sans-serif; fill: #666; font-size: 12px; }
.title { font-weight: bold; font-size: 15px; fill: #777; text-anchor: middle; }
.b { font-weight: bold; }
.c { text-anchor: middle; }
.e { text-anchor: end; }
.note { fill: #C0605A; }
.ok { fill: #6A9F68; }
.sub { font-size: 11px; fill: #888; }
.mono { font-family: Menlo, Consolas, monospace; }
.arrow { stroke: #666; stroke-width: 1.3; fill: none; marker-end: url(#a); }
.red { stroke: #C0605A; marker-end: url(#r); }
.green { stroke: #6A9F68; marker-end: url(#g); }
.dash { stroke-dasharray: 5 4; }
.line { stroke: #666; stroke-width: 1.3; fill: none; }
.frame { fill: none; stroke: #999; stroke-width: 1; stroke-dasharray: 4 3; }
.shadow { fill: #DADADA; }
</style>
<defs>
<marker id="a" viewBox="0 0 10 10" refX="9" refY="5" markerWidth="7" markerHeight="7" orient="auto-start-reverse">
<path d="M1,1 L9,5 L1,9" fill="none" stroke="#666" stroke-width="1.6"/>
</marker>
<marker id="r" viewBox="0 0 10 10" refX="9" refY="5" markerWidth="7" markerHeight="7" orient="auto-start-reverse">
<path d="M1,1 L9,5 L1,9" fill="none" stroke="#C0605A" stroke-width="1.6"/>
</marker>
<marker id="g" viewBox="0 0 10 10" refX="9" refY="5" markerWidth="7" markerHeight="7" orient="auto-start-reverse">
<path d="M1,1 L9,5 L1,9" fill="none" stroke="#6A9F68" stroke-width="1.6"/>
</marker>
</defs>
<rect width="100%" height="100%" fill="#FFFFFF"/>
<text x="360" y="28" class="title">100 GB 日志中出现次数最多的 10 个 URL(内存 4 GB)</text>
<rect x="22" y="112" width="110" height="70" rx="6" class="shadow"/>
<rect x="20" y="110" width="110" height="70" rx="6" fill="#E6E6E6"/>
<text x="75" y="143" class="b c">100 GB 日志</text>
<text x="75" y="158" class="sub c">每行一个 URL</text>
<text x="75" y="204" class="c sub mono">hash(url) % 100</text>
<rect x="197" y="58" width="80" height="34" rx="6" class="shadow"/>
<rect x="195" y="56" width="80" height="34" rx="6" fill="#B1DDF0"/>
<text x="235" y="77.5" class="b c">文件 0</text>
<line x1="133" y1="145" x2="191" y2="73" class="arrow "/>
<rect x="320" y="56" width="150" height="34" rx="4" fill="#FFFFFF" stroke="#C8C8C8"/>
<text x="395" y="77.5" class="c sub">HashMap 计数</text>
<line x1="278" y1="73" x2="316" y2="73" class="arrow "/>
<rect x="507" y="58" width="70" height="34" rx="6" class="shadow"/>
<rect x="505" y="56" width="70" height="34" rx="6" fill="#DDD7F6"/>
<text x="540" y="77.5" class="b c">前 10</text>
<line x1="473" y1="73" x2="501" y2="73" class="arrow "/>
<line x1="578" y1="73" x2="618" y2="145" class="arrow "/>
<rect x="197" y="110" width="80" height="34" rx="6" class="shadow"/>
<rect x="195" y="108" width="80" height="34" rx="6" fill="#B1DDF0"/>
<text x="235" y="129.5" class="b c">文件 1</text>
<line x1="133" y1="145" x2="191" y2="125" class="arrow "/>
<rect x="320" y="108" width="150" height="34" rx="4" fill="#FFFFFF" stroke="#C8C8C8"/>
<text x="395" y="129.5" class="c sub">HashMap 计数</text>
<line x1="278" y1="125" x2="316" y2="125" class="arrow "/>
<rect x="507" y="110" width="70" height="34" rx="6" class="shadow"/>
<rect x="505" y="108" width="70" height="34" rx="6" fill="#DDD7F6"/>
<text x="540" y="129.5" class="b c">前 10</text>
<line x1="473" y1="125" x2="501" y2="125" class="arrow "/>
<line x1="578" y1="125" x2="618" y2="145" class="arrow "/>
<text x="235" y="182" class="c b">⋮</text>
<rect x="197" y="214" width="80" height="34" rx="6" class="shadow"/>
<rect x="195" y="212" width="80" height="34" rx="6" fill="#B1DDF0"/>
<text x="235" y="233.5" class="b c">文件 99</text>
<line x1="133" y1="145" x2="191" y2="229" class="arrow "/>
<rect x="320" y="212" width="150" height="34" rx="4" fill="#FFFFFF" stroke="#C8C8C8"/>
<text x="395" y="233.5" class="c sub">HashMap 计数</text>
<line x1="278" y1="229" x2="316" y2="229" class="arrow "/>
<rect x="507" y="214" width="70" height="34" rx="6" class="shadow"/>
<rect x="505" y="212" width="70" height="34" rx="6" fill="#DDD7F6"/>
<text x="540" y="233.5" class="b c">前 10</text>
<line x1="473" y1="229" x2="501" y2="229" class="arrow "/>
<line x1="578" y1="229" x2="618" y2="145" class="arrow "/>
<rect x="624" y="118" width="80" height="58" rx="6" class="shadow"/>
<rect x="622" y="116" width="80" height="58" rx="6" fill="#B0E3E6"/>
<text x="662" y="143" class="b c">小顶堆</text>
<text x="662" y="158" class="sub c">全局前 10</text>
<text x="235" y="262" class="c sub">① 约 1 GB / 份</text>
<text x="395" y="262" class="c sub">② 每次只把一份读入内存</text>
<text x="560" y="262" class="c sub">③ 合并 100 × 10 个候选</text>
<text x="360" y="288" class="c note">相同的 URL 一定落在同一份中,所以每份单独统计的次数就是它的总次数</text>
</svg>

After

Width:  |  Height:  |  Size: 5.0 KiB

+77
View File
@@ -0,0 +1,77 @@
<svg xmlns="http://www.w3.org/2000/svg" width="740" height="400" viewBox="0 0 740 400">
<style>
text { font-family: "PingFang SC", "Microsoft YaHei", Helvetica, Arial, sans-serif; fill: #666; font-size: 12px; }
.title { font-weight: bold; font-size: 15px; fill: #777; text-anchor: middle; }
.b { font-weight: bold; }
.c { text-anchor: middle; }
.e { text-anchor: end; }
.note { fill: #C0605A; }
.ok { fill: #6A9F68; }
.sub { font-size: 11px; fill: #888; }
.mono { font-family: Menlo, Consolas, monospace; }
.arrow { stroke: #666; stroke-width: 1.3; fill: none; marker-end: url(#a); }
.red { stroke: #C0605A; marker-end: url(#r); }
.green { stroke: #6A9F68; marker-end: url(#g); }
.dash { stroke-dasharray: 5 4; }
.line { stroke: #666; stroke-width: 1.3; fill: none; }
.frame { fill: none; stroke: #999; stroke-width: 1; stroke-dasharray: 4 3; }
.shadow { fill: #DADADA; }
</style>
<defs>
<marker id="a" viewBox="0 0 10 10" refX="9" refY="5" markerWidth="7" markerHeight="7" orient="auto-start-reverse">
<path d="M1,1 L9,5 L1,9" fill="none" stroke="#666" stroke-width="1.6"/>
</marker>
<marker id="r" viewBox="0 0 10 10" refX="9" refY="5" markerWidth="7" markerHeight="7" orient="auto-start-reverse">
<path d="M1,1 L9,5 L1,9" fill="none" stroke="#C0605A" stroke-width="1.6"/>
</marker>
<marker id="g" viewBox="0 0 10 10" refX="9" refY="5" markerWidth="7" markerHeight="7" orient="auto-start-reverse">
<path d="M1,1 L9,5 L1,9" fill="none" stroke="#6A9F68" stroke-width="1.6"/>
</marker>
</defs>
<rect width="100%" height="100%" fill="#FFFFFF"/>
<text x="370" y="28" class="title">一次 RPC 调用</text>
<rect x="282" y="48" width="180" height="48" rx="6" class="shadow"/>
<rect x="280" y="46" width="180" height="48" rx="6" fill="#E6E6E6"/>
<text x="370" y="68" class="b c">注册中心</text>
<text x="370" y="83" class="sub c">库存服务:10.0.0.8、10.0.0.9</text>
<line x1="330" y1="98" x2="160" y2="158" class="arrow dash"/>
<text x="128" y="126" class="sub">① 订阅地址,缓存在本地</text>
<line x1="590" y1="158" x2="410" y2="98" class="arrow dash"/>
<text x="500" y="126" class="sub">启动时注册,定期发送心跳</text>
<rect x="20" y="164" width="270" height="196" rx="8" class="frame"/>
<text x="30" y="182" class="b">订单服务(调用方)</text>
<rect x="36" y="194" width="238" height="34" rx="4" fill="#FFFFFF" stroke="#C8C8C8"/>
<text x="155" y="215.5" class="c sub mono">inventoryService.deduct(10086, 2)</text>
<rect x="38" y="248" width="238" height="40" rx="6" class="shadow"/>
<rect x="36" y="246" width="238" height="40" rx="6" fill="#DDD7F6"/>
<text x="155" y="264" class="b c">代理</text>
<text x="155" y="279" class="sub c">取得方法名和参数</text>
<rect x="38" y="306" width="238" height="40" rx="6" class="shadow"/>
<rect x="36" y="304" width="238" height="40" rx="6" fill="#B1DDF0"/>
<text x="155" y="322" class="b c">序列化、选择实例</text>
<text x="155" y="337" class="sub c">组成带请求 ID 的请求帧</text>
<line x1="155" y1="231" x2="155" y2="243" class="arrow "/>
<line x1="155" y1="289" x2="155" y2="301" class="arrow "/>
<rect x="450" y="164" width="270" height="196" rx="8" class="frame"/>
<text x="460" y="182" class="b">库存服务(服务方)</text>
<rect x="468" y="306" width="238" height="40" rx="6" class="shadow"/>
<rect x="466" y="304" width="238" height="40" rx="6" fill="#B1DDF0"/>
<text x="585" y="322" class="b c">解码请求</text>
<text x="585" y="337" class="sub c">得到方法名和参数</text>
<rect x="468" y="248" width="238" height="40" rx="6" class="shadow"/>
<rect x="466" y="246" width="238" height="40" rx="6" fill="#DDD7F6"/>
<text x="585" y="264" class="b c">反射调用</text>
<text x="585" y="279" class="sub c">InventoryServiceImpl.deduct()</text>
<rect x="466" y="194" width="238" height="34" rx="4" fill="#FFFFFF" stroke="#C8C8C8"/>
<text x="585" y="215.5" class="c sub mono">返回 true</text>
<line x1="585" y1="301" x2="585" y2="289" class="arrow "/>
<line x1="585" y1="243" x2="585" y2="231" class="arrow "/>
<line x1="277" y1="324" x2="462" y2="324" class="arrow "/>
<text x="370" y="316" class="c sub">② 请求帧</text>
<text x="370" y="342" class="c sub">请求 ID 7 + 方法 + 参数</text>
<line x1="462" y1="211" x2="277" y2="211" class="arrow dash"/>
<text x="370" y="203" class="c sub">③ 响应帧</text>
<text x="370" y="229" class="c sub">请求 ID 7 + 结果</text>
<text x="370" y="386" class="c sub">注册中心只负责注册和发现,不转发请求;调用方按请求 ID 把响应交给对应的等待线程</text>
</svg>

After

Width:  |  Height:  |  Size: 4.7 KiB

+72
View File
@@ -0,0 +1,72 @@
<svg xmlns="http://www.w3.org/2000/svg" width="720" height="250" viewBox="0 0 720 250">
<style>
text { font-family: "PingFang SC", "Microsoft YaHei", Helvetica, Arial, sans-serif; fill: #666; font-size: 12px; }
.title { font-weight: bold; font-size: 15px; fill: #777; text-anchor: middle; }
.b { font-weight: bold; }
.c { text-anchor: middle; }
.e { text-anchor: end; }
.note { fill: #C0605A; }
.ok { fill: #6A9F68; }
.sub { font-size: 11px; fill: #888; }
.mono { font-family: Menlo, Consolas, monospace; }
.arrow { stroke: #666; stroke-width: 1.3; fill: none; marker-end: url(#a); }
.red { stroke: #C0605A; marker-end: url(#r); }
.green { stroke: #6A9F68; marker-end: url(#g); }
.dash { stroke-dasharray: 5 4; }
.line { stroke: #666; stroke-width: 1.3; fill: none; }
.frame { fill: none; stroke: #999; stroke-width: 1; stroke-dasharray: 4 3; }
.shadow { fill: #DADADA; }
</style>
<defs>
<marker id="a" viewBox="0 0 10 10" refX="9" refY="5" markerWidth="7" markerHeight="7" orient="auto-start-reverse">
<path d="M1,1 L9,5 L1,9" fill="none" stroke="#666" stroke-width="1.6"/>
</marker>
<marker id="r" viewBox="0 0 10 10" refX="9" refY="5" markerWidth="7" markerHeight="7" orient="auto-start-reverse">
<path d="M1,1 L9,5 L1,9" fill="none" stroke="#C0605A" stroke-width="1.6"/>
</marker>
<marker id="g" viewBox="0 0 10 10" refX="9" refY="5" markerWidth="7" markerHeight="7" orient="auto-start-reverse">
<path d="M1,1 L9,5 L1,9" fill="none" stroke="#6A9F68" stroke-width="1.6"/>
</marker>
</defs>
<rect width="100%" height="100%" fill="#FFFFFF"/>
<text x="360" y="28" class="title">秒杀:层层过滤请求</text>
<rect x="22" y="70" width="96" height="56" rx="6" class="shadow"/>
<rect x="20" y="68" width="96" height="56" rx="6" fill="#DDD7F6"/>
<text x="68" y="94" class="b c">10 万个请求</text>
<text x="68" y="109" class="sub c">同时到达</text>
<rect x="152" y="62" width="120" height="72" rx="6" class="shadow"/>
<rect x="150" y="60" width="120" height="72" rx="6" fill="#B1DDF0"/>
<text x="210" y="86" class="c b">前端与网关</text>
<text x="210" y="106" class="c sub">按钮防重复提交</text>
<text x="210" y="122" class="c sub">按用户限流</text>
<rect x="306" y="62" width="140" height="72" rx="6" class="shadow"/>
<rect x="304" y="60" width="140" height="72" rx="6" fill="#B0E3E6"/>
<text x="374" y="86" class="c b">Redis + Lua</text>
<text x="374" y="106" class="c sub">判断库存和是否已购</text>
<text x="374" y="122" class="c sub">原子扣减,库存 100 → 0</text>
<rect x="482" y="70" width="90" height="56" rx="6" class="shadow"/>
<rect x="480" y="68" width="90" height="56" rx="6" fill="#E6E6E6"/>
<text x="525" y="94" class="b c">消息队列</text>
<text x="525" y="109" class="sub c">100 条</text>
<rect x="606" y="62" width="100" height="72" rx="6" class="shadow"/>
<rect x="604" y="60" width="100" height="72" rx="6" fill="#B1DDF0"/>
<text x="654" y="86" class="c b">订单服务</text>
<text x="654" y="106" class="c sub">扣数据库库存</text>
<text x="654" y="122" class="c sub">创建订单</text>
<line x1="119" y1="96" x2="146" y2="96" class="arrow "/>
<line x1="273" y1="96" x2="300" y2="96" class="arrow "/>
<line x1="447" y1="96" x2="476" y2="96" class="arrow green"/>
<text x="461" y="88" class="c ok">成功</text>
<line x1="573" y1="96" x2="600" y2="96" class="arrow "/>
<line x1="374" y1="136" x2="374" y2="172" class="arrow red"/>
<text x="382" y="160" class="note">其余请求</text>
<rect x="310" y="176" width="128" height="34" rx="6" fill="#F3C6C2"/>
<text x="374" y="197.5" class="c sub">直接返回“已抢完”</text>
<line x1="210" y1="136" x2="210" y2="172" class="arrow red"/>
<rect x="150" y="176" width="120" height="34" rx="6" fill="#F3C6C2"/>
<text x="210" y="197.5" class="c sub">拦截重复和超限请求</text>
<text x="654" y="156" class="c sub">库存大于 0 才扣减</text>
<text x="654" y="174" class="c sub">唯一索引保证一人一单</text>
<text x="20" y="236" class="sub">到达数据库的请求接近库存数量;订单最终创建失败时,要把 Redis 中的库存和购买记录恢复。</text>
</svg>

After

Width:  |  Height:  |  Size: 4.2 KiB

+54
View File
@@ -0,0 +1,54 @@
<svg xmlns="http://www.w3.org/2000/svg" width="680" height="230" viewBox="0 0 680 230">
<style>
text { font-family: "PingFang SC", "Microsoft YaHei", Helvetica, Arial, sans-serif; fill: #666; font-size: 12px; }
.title { font-weight: bold; font-size: 15px; fill: #777; text-anchor: middle; }
.b { font-weight: bold; }
.c { text-anchor: middle; }
.e { text-anchor: end; }
.note { fill: #C0605A; }
.ok { fill: #6A9F68; }
.sub { font-size: 11px; fill: #888; }
.mono { font-family: Menlo, Consolas, monospace; }
.arrow { stroke: #666; stroke-width: 1.3; fill: none; marker-end: url(#a); }
.red { stroke: #C0605A; marker-end: url(#r); }
.green { stroke: #6A9F68; marker-end: url(#g); }
.dash { stroke-dasharray: 5 4; }
.line { stroke: #666; stroke-width: 1.3; fill: none; }
.frame { fill: none; stroke: #999; stroke-width: 1; stroke-dasharray: 4 3; }
.shadow { fill: #DADADA; }
</style>
<defs>
<marker id="a" viewBox="0 0 10 10" refX="9" refY="5" markerWidth="7" markerHeight="7" orient="auto-start-reverse">
<path d="M1,1 L9,5 L1,9" fill="none" stroke="#666" stroke-width="1.6"/>
</marker>
<marker id="r" viewBox="0 0 10 10" refX="9" refY="5" markerWidth="7" markerHeight="7" orient="auto-start-reverse">
<path d="M1,1 L9,5 L1,9" fill="none" stroke="#C0605A" stroke-width="1.6"/>
</marker>
<marker id="g" viewBox="0 0 10 10" refX="9" refY="5" markerWidth="7" markerHeight="7" orient="auto-start-reverse">
<path d="M1,1 L9,5 L1,9" fill="none" stroke="#6A9F68" stroke-width="1.6"/>
</marker>
</defs>
<rect width="100%" height="100%" fill="#FFFFFF"/>
<text x="340" y="28" class="title">自调用不经过代理</text>
<rect x="22" y="84" width="90" height="50" rx="6" class="shadow"/>
<rect x="20" y="82" width="90" height="50" rx="6" fill="#DDD7F6"/>
<text x="65" y="111.5" class="b c">调用方</text>
<rect x="192" y="64" width="150" height="90" rx="6" class="shadow"/>
<rect x="190" y="62" width="150" height="90" rx="6" fill="#B0E3E6"/>
<text x="265" y="92" class="c b">代理对象</text>
<text x="265" y="114" class="c sub">执行切面逻辑</text>
<text x="265" y="132" class="c sub">例如开启、提交事务</text>
<rect x="402" y="52" width="260" height="140" rx="6" class="shadow"/>
<rect x="400" y="50" width="260" height="140" rx="6" fill="#B1DDF0"/>
<text x="412" y="72" class="b">原始对象</text>
<rect x="420" y="86" width="200" height="34" rx="4" fill="#FFFFFF" stroke="#C8C8C8"/>
<text x="520" y="107.5" class="c mono sub">outer()</text>
<rect x="420" y="140" width="200" height="34" rx="4" fill="#FFFFFF" stroke="#C8C8C8"/>
<text x="520" y="161.5" class="c mono sub">@Transactional inner()</text>
<line x1="113" y1="107" x2="186" y2="107" class="arrow "/>
<text x="150" y="98" class="c sub mono">outer()</text>
<line x1="343" y1="103" x2="416" y2="103" class="arrow "/>
<path d="M620,108 C650,116 650,148 624,156" class="arrow red"/>
<text x="20" y="214" class="note">只有从外部经过代理的调用,切面才会生效;outer() 中的 this.inner() 直接调用原始对象,事务不生效。</text>
</svg>

After

Width:  |  Height:  |  Size: 3.1 KiB

+70
View File
@@ -0,0 +1,70 @@
<svg xmlns="http://www.w3.org/2000/svg" width="720" height="400" viewBox="0 0 720 400">
<style>
text { font-family: "PingFang SC", "Microsoft YaHei", Helvetica, Arial, sans-serif; fill: #666; font-size: 12px; }
.title { font-weight: bold; font-size: 15px; fill: #777; text-anchor: middle; }
.b { font-weight: bold; }
.c { text-anchor: middle; }
.e { text-anchor: end; }
.note { fill: #C0605A; }
.ok { fill: #6A9F68; }
.sub { font-size: 11px; fill: #888; }
.mono { font-family: Menlo, Consolas, monospace; }
.arrow { stroke: #666; stroke-width: 1.3; fill: none; marker-end: url(#a); }
.red { stroke: #C0605A; marker-end: url(#r); }
.green { stroke: #6A9F68; marker-end: url(#g); }
.dash { stroke-dasharray: 5 4; }
.line { stroke: #666; stroke-width: 1.3; fill: none; }
.frame { fill: none; stroke: #999; stroke-width: 1; stroke-dasharray: 4 3; }
.shadow { fill: #DADADA; }
</style>
<defs>
<marker id="a" viewBox="0 0 10 10" refX="9" refY="5" markerWidth="7" markerHeight="7" orient="auto-start-reverse">
<path d="M1,1 L9,5 L1,9" fill="none" stroke="#666" stroke-width="1.6"/>
</marker>
<marker id="r" viewBox="0 0 10 10" refX="9" refY="5" markerWidth="7" markerHeight="7" orient="auto-start-reverse">
<path d="M1,1 L9,5 L1,9" fill="none" stroke="#C0605A" stroke-width="1.6"/>
</marker>
<marker id="g" viewBox="0 0 10 10" refX="9" refY="5" markerWidth="7" markerHeight="7" orient="auto-start-reverse">
<path d="M1,1 L9,5 L1,9" fill="none" stroke="#6A9F68" stroke-width="1.6"/>
</marker>
</defs>
<rect width="100%" height="100%" fill="#FFFFFF"/>
<text x="360" y="28" class="title">三级缓存解决 A、B 的循环依赖</text>
<text x="430" y="58" class="c b">一级缓存</text>
<text x="430" y="74" class="c sub">完整的 Bean</text>
<text x="540" y="58" class="c b">二级缓存</text>
<text x="540" y="74" class="c sub">提前暴露的对象</text>
<text x="650" y="58" class="c b">三级缓存</text>
<text x="650" y="74" class="c sub">生成提前引用的工厂</text>
<rect x="20" y="90" width="680" height="50" rx="6" fill="#F7F7F7"/>
<text x="32" y="120">① 实例化 A,把 A 的工厂放入三级缓存</text>
<rect x="602" y="102" width="96" height="26" rx="4" fill="#DDD7F6" stroke="#C8C8C8"/>
<text x="650" y="119.5" class="c sub">A 的工厂</text>
<rect x="20" y="146" width="680" height="50" rx="6" fill="#FFFFFF"/>
<text x="32" y="176">② 填充 A 的属性时需要 B,实例化 B</text>
<rect x="602" y="150" width="96" height="20" rx="4" fill="#DDD7F6" stroke="#C8C8C8"/>
<text x="650" y="164.5" class="c sub">A 的工厂</text>
<rect x="602" y="172" width="96" height="20" rx="4" fill="#DDD7F6" stroke="#C8C8C8"/>
<text x="650" y="186.5" class="c sub">B 的工厂</text>
<rect x="20" y="202" width="680" height="50" rx="6" fill="#F7F7F7"/>
<text x="32" y="222">③ 填充 B 时需要 A:调用 A 的工厂,</text>
<text x="54" y="240">得到 A 的提前引用,移入二级缓存</text>
<rect x="492" y="214" width="96" height="26" rx="4" fill="#DDD7F6" stroke="#C8C8C8"/>
<text x="540" y="231.5" class="c sub">A(未完成)</text>
<rect x="602" y="214" width="96" height="26" rx="4" fill="#DDD7F6" stroke="#C8C8C8"/>
<text x="650" y="231.5" class="c sub">B 的工厂</text>
<rect x="20" y="258" width="680" height="50" rx="6" fill="#FFFFFF"/>
<text x="32" y="288">④ B 拿到 A,完成初始化</text>
<rect x="382" y="270" width="96" height="26" rx="4" fill="#B1DDF0" stroke="#C8C8C8"/>
<text x="430" y="287.5" class="c sub">B</text>
<rect x="492" y="270" width="96" height="26" rx="4" fill="#DDD7F6" stroke="#C8C8C8"/>
<text x="540" y="287.5" class="c sub">A(未完成)</text>
<rect x="20" y="314" width="680" height="50" rx="6" fill="#F7F7F7"/>
<text x="32" y="344">⑤ A 拿到 B,完成初始化</text>
<rect x="382" y="318" width="96" height="20" rx="4" fill="#B1DDF0" stroke="#C8C8C8"/>
<text x="430" y="332.5" class="c sub">A</text>
<rect x="382" y="340" width="96" height="20" rx="4" fill="#B1DDF0" stroke="#C8C8C8"/>
<text x="430" y="354.5" class="c sub">B</text>
<text x="20" y="384" class="sub">A 需要 AOP 代理时,工厂在第 ③ 步直接返回 A 的代理,B 拿到的就是最终放入容器的对象。</text>
</svg>

After

Width:  |  Height:  |  Size: 4.3 KiB

+76
View File
@@ -0,0 +1,76 @@
<svg xmlns="http://www.w3.org/2000/svg" width="700" height="360" viewBox="0 0 700 360">
<style>
text { font-family: "PingFang SC", "Microsoft YaHei", Helvetica, Arial, sans-serif; fill: #666; font-size: 12px; }
.title { font-weight: bold; font-size: 15px; fill: #777; text-anchor: middle; }
.b { font-weight: bold; }
.c { text-anchor: middle; }
.e { text-anchor: end; }
.note { fill: #C0605A; }
.ok { fill: #6A9F68; }
.sub { font-size: 11px; fill: #888; }
.mono { font-family: Menlo, Consolas, monospace; }
.arrow { stroke: #666; stroke-width: 1.3; fill: none; marker-end: url(#a); }
.red { stroke: #C0605A; marker-end: url(#r); }
.green { stroke: #6A9F68; marker-end: url(#g); }
.dash { stroke-dasharray: 5 4; }
.line { stroke: #666; stroke-width: 1.3; fill: none; }
.frame { fill: none; stroke: #999; stroke-width: 1; stroke-dasharray: 4 3; }
.shadow { fill: #DADADA; }
</style>
<defs>
<marker id="a" viewBox="0 0 10 10" refX="9" refY="5" markerWidth="7" markerHeight="7" orient="auto-start-reverse">
<path d="M1,1 L9,5 L1,9" fill="none" stroke="#666" stroke-width="1.6"/>
</marker>
<marker id="r" viewBox="0 0 10 10" refX="9" refY="5" markerWidth="7" markerHeight="7" orient="auto-start-reverse">
<path d="M1,1 L9,5 L1,9" fill="none" stroke="#C0605A" stroke-width="1.6"/>
</marker>
<marker id="g" viewBox="0 0 10 10" refX="9" refY="5" markerWidth="7" markerHeight="7" orient="auto-start-reverse">
<path d="M1,1 L9,5 L1,9" fill="none" stroke="#6A9F68" stroke-width="1.6"/>
</marker>
</defs>
<rect width="100%" height="100%" fill="#FFFFFF"/>
<text x="350" y="28" class="title">线程池处理任务的顺序</text>
<text x="20" y="58" class="sub">核心线程数 2,最大线程数 4,队列容量 2,依次提交 7 个各执行 1 秒的任务</text>
<text x="20" y="94" class="b">① 线程数小于核心线程数</text>
<text x="20" y="112" class="sub">新建核心线程执行</text>
<line x1="230" y1="98" x2="262" y2="98" class="arrow "/>
<rect x="268" y="80" width="200" height="40" rx="6" class="shadow"/>
<rect x="266" y="78" width="200" height="40" rx="6" fill="#B1DDF0"/>
<text x="278" y="103" class="b">核心线程</text>
<rect x="478" y="85" width="64" height="26" rx="4" fill="#FFFFFF" stroke="#C8C8C8"/>
<text x="510" y="102.5" class="c sub">任务 1</text>
<rect x="550" y="85" width="64" height="26" rx="4" fill="#FFFFFF" stroke="#C8C8C8"/>
<text x="582" y="102.5" class="c sub">任务 2</text>
<text x="20" y="150" class="b">② 核心线程已满</text>
<text x="20" y="168" class="sub">放入队列等待</text>
<line x1="230" y1="154" x2="262" y2="154" class="arrow "/>
<rect x="268" y="136" width="200" height="40" rx="6" class="shadow"/>
<rect x="266" y="134" width="200" height="40" rx="6" fill="#E6E6E6"/>
<text x="278" y="159" class="b">队列(容量 2)</text>
<rect x="478" y="141" width="64" height="26" rx="4" fill="#FFFFFF" stroke="#C8C8C8"/>
<text x="510" y="158.5" class="c sub">任务 3</text>
<rect x="550" y="141" width="64" height="26" rx="4" fill="#FFFFFF" stroke="#C8C8C8"/>
<text x="582" y="158.5" class="c sub">任务 4</text>
<text x="20" y="206" class="b">③ 队列已满,线程数小于 4</text>
<text x="20" y="224" class="sub">新建非核心线程执行</text>
<line x1="230" y1="210" x2="262" y2="210" class="arrow "/>
<rect x="268" y="192" width="200" height="40" rx="6" class="shadow"/>
<rect x="266" y="190" width="200" height="40" rx="6" fill="#B1DDF0"/>
<text x="278" y="215" class="b">非核心线程</text>
<rect x="478" y="197" width="64" height="26" rx="4" fill="#FFFFFF" stroke="#C8C8C8"/>
<text x="510" y="214.5" class="c sub">任务 5</text>
<rect x="550" y="197" width="64" height="26" rx="4" fill="#FFFFFF" stroke="#C8C8C8"/>
<text x="582" y="214.5" class="c sub">任务 6</text>
<text x="20" y="262" class="b">④ 线程数已达 4 且队列已满</text>
<text x="20" y="280" class="sub">执行拒绝策略</text>
<line x1="230" y1="266" x2="262" y2="266" class="arrow "/>
<rect x="268" y="248" width="200" height="40" rx="6" class="shadow"/>
<rect x="266" y="246" width="200" height="40" rx="6" fill="#F3C6C2"/>
<text x="278" y="271" class="b">拒绝</text>
<rect x="478" y="253" width="64" height="26" rx="4" fill="#FFFFFF" stroke="#C8C8C8"/>
<text x="510" y="270.5" class="c sub">任务 7</text>
<line x1="20" y1="306" x2="680" y2="306" stroke="#DDD"/>
<text x="20" y="328" class="sub">实际执行顺序:0 秒时任务 1、2、5、6 开始执行,1 秒后才轮到队列中的任务 3、4。</text>
<text x="20" y="348" class="note">队列满后新建的线程直接执行当前提交的任务,所以后提交的任务 5、6 反而先执行。</text>
</svg>

After

Width:  |  Height:  |  Size: 4.7 KiB