,JavaGuide 对其做了补充完善。
+
+
## 引言
所谓排序,就是使一串记录,按照其中的某个或某些关键字的大小,递增或递减的排列起来的操作。排序算法,就是如何使得记录按照要求排列的方法。排序算法在很多领域得到相当地重视,尤其是在大量数据的处理方面。一个优秀的算法可以节省大量的资源。在各个领域中考虑到数据的各种限制和规范,要得到一个符合实际的优秀算法,得经过大量的推理和分析。
@@ -355,9 +362,14 @@ public static int[] merge(int[] arr_1, int[] arr_2) {
快速排序使用[分治法](https://zh.wikipedia.org/wiki/分治法)(Divide and conquer)策略来把一个序列分为较小和较大的 2 个子序列,然后递归地排序两个子序列。具体算法描述如下:
-1. 从序列中**随机**挑出一个元素,做为 “基准”(`pivot`);
-2. 重新排列序列,将所有比基准值小的元素摆放在基准前面,所有比基准值大的摆在基准的后面(相同的数可以到任一边)。在这个操作结束之后,该基准就处于数列的中间位置。这个称为分区(partition)操作;
-3. 递归地把小于基准值元素的子序列和大于基准值元素的子序列进行快速排序。
+1. **选择基准(Pivot)** :从数组中选一个元素作为基准。为了避免最坏情况,通常会随机选择。
+2. **分区(Partition)** :重新排列序列,将所有比基准值小的元素摆放在基准前面,所有比基准值大的摆在基准的后面(相同的数可以到任一边)。在这个操作结束之后,该基准就处于数列的中间位置。
+3. **递归(Recurse)** :递归地把小于基准值元素的子序列和大于基准值元素的子序列进行快速排序。
+
+**关于性能,这也是它与归并排序的关键区别:**
+
+- **平均和最佳情况:** 它的时间复杂度是 $O(nlogn)$。这种情况发生在每次分区都能把数组分成均等的两半。
+- **最坏情况:** 它的时间复杂度会退化到 $O(n^2)$。这发生在每次我们选的基准都是当前数组的最小值或最大值时,比如对一个已经排好序的数组,每次都选第一个元素做基准,这就会导致分区极其不均,算法退化成类似冒泡排序。这就是为什么**随机选择基准**非常重要。
### 图解算法
@@ -365,31 +377,60 @@ public static int[] merge(int[] arr_1, int[] arr_2) {
### 代码实现
-> 来源:[使用 Java 实现快速排序(详解)](https://segmentfault.com/a/1190000040022056)
-
```java
-public static int partition(int[] array, int low, int high) {
- int pivot = array[high];
- int pointer = low;
- for (int i = low; i < high; i++) {
- if (array[i] <= pivot) {
- int temp = array[i];
- array[i] = array[pointer];
- array[pointer] = temp;
- pointer++;
+import java.util.concurrent.ThreadLocalRandom;
+
+class Solution {
+ public int[] sortArray(int[] a) {
+ quick(a, 0, a.length - 1);
+ return a;
+ }
+
+ // 快速排序的核心递归函数
+ void quick(int[] a, int left, int right) {
+ if (left >= right) { // 递归终止条件:区间只有一个或没有元素
+ return;
}
- System.out.println(Arrays.toString(array));
+ int p = partition(a, left, right); // 分区操作,返回分区点索引
+ quick(a, left, p - 1); // 对左侧子数组递归排序
+ quick(a, p + 1, right); // 对右侧子数组递归排序
}
- int temp = array[pointer];
- array[pointer] = array[high];
- array[high] = temp;
- return pointer;
-}
-public static void quickSort(int[] array, int low, int high) {
- if (low < high) {
- int position = partition(array, low, high);
- quickSort(array, low, position - 1);
- quickSort(array, position + 1, high);
+
+ // 分区函数:将数组分为两部分,小于基准值的在左,大于基准值的在右
+ int partition(int[] a, int left, int right) {
+ // 随机选择一个基准点,避免最坏情况(如数组接近有序)
+ int idx = ThreadLocalRandom.current().nextInt(right - left + 1) + left;
+ swap(a, left, idx); // 将基准点放在数组的最左端
+ int pv = a[left]; // 基准值
+ int i = left + 1; // 左指针,指向当前需要检查的元素
+ int j = right; // 右指针,从右往左寻找比基准值小的元素
+
+ while (i <= j) {
+ // 左指针向右移动,直到找到一个大于等于基准值的元素
+ while (i <= j && a[i] < pv) {
+ i++;
+ }
+ // 右指针向左移动,直到找到一个小于等于基准值的元素
+ while (i <= j && a[j] > pv) {
+ j--;
+ }
+ // 如果左指针尚未越过右指针,交换两个不符合位置的元素
+ if (i <= j) {
+ swap(a, i, j);
+ i++;
+ j--;
+ }
+ }
+ // 将基准值放到分区点位置,使得基准值左侧小于它,右侧大于它
+ swap(a, j, left);
+ return j;
+ }
+
+ // 交换数组中两个元素的位置
+ void swap(int[] a, int i, int j) {
+ int t = a[i];
+ a[i] = a[j];
+ a[j] = t;
}
}
```
diff --git a/docs/cs-basics/algorithms/classical-algorithm-problems-recommendations.md b/docs/cs-basics/algorithms/classical-algorithm-problems-recommendations.md
index 3a6a01a210f..0e6f56f74f5 100644
--- a/docs/cs-basics/algorithms/classical-algorithm-problems-recommendations.md
+++ b/docs/cs-basics/algorithms/classical-algorithm-problems-recommendations.md
@@ -1,8 +1,13 @@
---
title: 经典算法思想总结(含LeetCode题目推荐)
+description: 总结常见算法思想与解题模板,配合典型题目推荐,强调思维路径与复杂度权衡,快速构建解题体系。
category: 计算机基础
tag:
- 算法
+head:
+ - - meta
+ - name: keywords
+ content: 贪心,分治,回溯,动态规划,二分,双指针,算法思想,题目推荐
---
## 贪心算法
diff --git a/docs/cs-basics/algorithms/common-data-structures-leetcode-recommendations.md b/docs/cs-basics/algorithms/common-data-structures-leetcode-recommendations.md
index 51d9225730f..bb73a2d917e 100644
--- a/docs/cs-basics/algorithms/common-data-structures-leetcode-recommendations.md
+++ b/docs/cs-basics/algorithms/common-data-structures-leetcode-recommendations.md
@@ -1,8 +1,13 @@
---
title: 常见数据结构经典LeetCode题目推荐
+description: 按数据结构类别整理经典 LeetCode 题目清单,聚焦高频与核心考点,助力系统化刷题与巩固。
category: 计算机基础
tag:
- 算法
+head:
+ - - meta
+ - name: keywords
+ content: LeetCode,数组,链表,栈,队列,二叉树,题目推荐,刷题
---
## 数组
diff --git a/docs/cs-basics/algorithms/linkedlist-algorithm-problems.md b/docs/cs-basics/algorithms/linkedlist-algorithm-problems.md
index b50b1b1b00f..8d412e43840 100644
--- a/docs/cs-basics/algorithms/linkedlist-algorithm-problems.md
+++ b/docs/cs-basics/algorithms/linkedlist-algorithm-problems.md
@@ -1,10 +1,17 @@
---
title: 几道常见的链表算法题
+description: 精选链表高频题的思路与实现,覆盖两数相加、反转、环检测等场景,强调边界处理与复杂度分析。
category: 计算机基础
tag:
- 算法
+head:
+ - - meta
+ - name: keywords
+ content: 链表算法,两数相加,反转链表,环检测,合并链表,复杂度分析
---
+
+
## 1. 两数相加
### 题目描述
diff --git a/docs/cs-basics/algorithms/string-algorithm-problems.md b/docs/cs-basics/algorithms/string-algorithm-problems.md
index 796fe7bf986..b528a03affe 100644
--- a/docs/cs-basics/algorithms/string-algorithm-problems.md
+++ b/docs/cs-basics/algorithms/string-algorithm-problems.md
@@ -1,8 +1,13 @@
---
title: 几道常见的字符串算法题
+description: 总结字符串高频算法与题型,重点讲解 KMP/BM 原理、滑动窗口等技巧,助力高效匹配与实现。
category: 计算机基础
tag:
- 算法
+head:
+ - - meta
+ - name: keywords
+ content: 字符串算法,KMP,BM,滑动窗口,子串,匹配,复杂度
---
> 作者:wwwxmu
diff --git a/docs/cs-basics/algorithms/the-sword-refers-to-offer.md b/docs/cs-basics/algorithms/the-sword-refers-to-offer.md
index 73d296d0dc3..37266eba58e 100644
--- a/docs/cs-basics/algorithms/the-sword-refers-to-offer.md
+++ b/docs/cs-basics/algorithms/the-sword-refers-to-offer.md
@@ -1,8 +1,13 @@
---
title: 剑指offer部分编程题
+description: 选编《剑指 Offer》常见编程题,给出递归与迭代等多种思路与示例,实现对高频题型的高效复盘。
category: 计算机基础
tag:
- 算法
+head:
+ - - meta
+ - name: keywords
+ content: 剑指Offer,斐波那契,递归,迭代,链表,数组,面试题
---
## 斐波那契数列
diff --git a/docs/cs-basics/data-structure/bloom-filter.md b/docs/cs-basics/data-structure/bloom-filter.md
index be17c1a53aa..a7db31e1f40 100644
--- a/docs/cs-basics/data-structure/bloom-filter.md
+++ b/docs/cs-basics/data-structure/bloom-filter.md
@@ -1,8 +1,13 @@
---
title: 布隆过滤器
+description: 解析 Bloom Filter 的原理与误判特性,结合哈希与位数组实现,适用于海量数据去重与缓存穿透防护。
category: 计算机基础
tag:
- 数据结构
+head:
+ - - meta
+ - name: keywords
+ content: 布隆过滤器,Bloom Filter,误判率,哈希函数,位数组,去重,缓存穿透
---
布隆过滤器相信大家没用过的话,也已经听过了。
diff --git a/docs/cs-basics/data-structure/graph.md b/docs/cs-basics/data-structure/graph.md
index e9860c240d5..b292a30a939 100644
--- a/docs/cs-basics/data-structure/graph.md
+++ b/docs/cs-basics/data-structure/graph.md
@@ -1,8 +1,13 @@
---
title: 图
+description: 介绍图的基本概念与常用表示,结合 DFS/BFS 等核心算法与应用场景,掌握图论入门必备知识。
category: 计算机基础
tag:
- 数据结构
+head:
+ - - meta
+ - name: keywords
+ content: 图,邻接表,邻接矩阵,DFS,BFS,度,有向图,无向图,连通性
---
图是一种较为复杂的非线性结构。 **为啥说其较为复杂呢?**
diff --git a/docs/cs-basics/data-structure/heap.md b/docs/cs-basics/data-structure/heap.md
index 5de2e5f2ee2..cfa1b29eee9 100644
--- a/docs/cs-basics/data-structure/heap.md
+++ b/docs/cs-basics/data-structure/heap.md
@@ -1,7 +1,12 @@
---
+description: 解析堆的性质与操作,理解优先队列实现与堆排序性能优势,掌握插入/删除的复杂度与实践场景。
category: 计算机基础
tag:
- 数据结构
+head:
+ - - meta
+ - name: keywords
+ content: 堆,最大堆,最小堆,优先队列,堆化,上浮,下沉,堆排序
---
# 堆
diff --git a/docs/cs-basics/data-structure/linear-data-structure.md b/docs/cs-basics/data-structure/linear-data-structure.md
index cc5cc6a5db2..f56511882ff 100644
--- a/docs/cs-basics/data-structure/linear-data-structure.md
+++ b/docs/cs-basics/data-structure/linear-data-structure.md
@@ -1,8 +1,13 @@
---
title: 线性数据结构
+description: 总结数组/链表/栈/队列的特性与操作,配合复杂度分析与典型应用,掌握线性结构的选型与实现。
category: 计算机基础
tag:
- 数据结构
+head:
+ - - meta
+ - name: keywords
+ content: 数组,链表,栈,队列,双端队列,复杂度分析,随机访问,插入删除
---
## 1. 数组
@@ -326,9 +331,9 @@ myStack.pop();//报错:java.lang.IllegalArgumentException: Stack is empty.
当我们需要按照一定顺序来处理数据的时候可以考虑使用队列这个数据结构。
- **阻塞队列:** 阻塞队列可以看成在队列基础上加了阻塞操作的队列。当队列为空的时候,出队操作阻塞,当队列满的时候,入队操作阻塞。使用阻塞队列我们可以很容易实现“生产者 - 消费者“模型。
-- **线程池中的请求/任务队列:** 线程池中没有空闲线程时,新的任务请求线程资源时,线程池该如何处理呢?答案是将这些请求放在队列中,当有空闲线程的时候,会循环中反复从队列中获取任务来执行。队列分为无界队列(基于链表)和有界队列(基于数组)。无界队列的特点就是可以一直入列,除非系统资源耗尽,比如:`FixedThreadPool` 使用无界队列 `LinkedBlockingQueue`。但是有界队列就不一样了,当队列满的话后面再有任务/请求就会拒绝,在 Java 中的体现就是会抛出`java.util.concurrent.RejectedExecutionException` 异常。
-- 栈:双端队列天生便可以实现栈的全部功能(`push`、`pop` 和 `peek`),并且在 Deque 接口中已经实现了相关方法。Stack 类已经和 Vector 一样被遗弃,现在在 Java 中普遍使用双端队列(Deque)来实现栈。
-- 广度优先搜索(BFS),在图的广度优先搜索过程中,队列被用于存储待访问的节点,保证按照层次顺序遍历图的节点。
+- **线程池中的请求/任务队列:** 当线程池中没有空闲线程时,新的任务请求线程资源会被如何处理呢?答案是这些任务会被放入任务队列中,等待线程池中的线程空闲后再从队列中取出任务执行。任务队列分为无界队列(基于链表实现)和有界队列(基于数组实现)。无界队列的特点是队列容量理论上没有限制,任务可以持续入队,直到系统资源耗尽。例如:`FixedThreadPool` 使用的阻塞队列 `LinkedBlockingQueue`,其默认容量为 `Integer.MAX_VALUE`,因此可以被视为“无界队列”。而有界队列则不同,当队列已满时,如果再有新任务提交,由于队列无法继续容纳任务,线程池会拒绝这些任务,并抛出 `java.util.concurrent.RejectedExecutionException` 异常。
+- **栈**:双端队列天生便可以实现栈的全部功能(`push`、`pop` 和 `peek`),并且在 Deque 接口中已经实现了相关方法。Stack 类已经和 Vector 一样被遗弃,现在在 Java 中普遍使用双端队列(Deque)来实现栈。
+- **广度优先搜索(BFS)**:在图的广度优先搜索过程中,队列被用于存储待访问的节点,保证按照层次顺序遍历图的节点。
- Linux 内核进程队列(按优先级排队)
- 现实生活中的派对,播放器上的播放列表;
- 消息队列
diff --git a/docs/cs-basics/data-structure/red-black-tree.md b/docs/cs-basics/data-structure/red-black-tree.md
index c7040eb6ebc..e6e31ef3758 100644
--- a/docs/cs-basics/data-structure/red-black-tree.md
+++ b/docs/cs-basics/data-structure/red-black-tree.md
@@ -1,8 +1,13 @@
---
title: 红黑树
+description: 深入讲解红黑树的五大性质与旋转调整过程,理解自平衡机制及在标准库与索引结构中的应用。
category: 计算机基础
tag:
- 数据结构
+head:
+ - - meta
+ - name: keywords
+ content: 红黑树,自平衡,旋转,插入删除,性质,黑高,时间复杂度
---
## 红黑树介绍
@@ -26,7 +31,7 @@ tag:
1. 每个节点非红即黑。黑色决定平衡,红色不决定平衡。这对应了 2-3 树中一个节点内可以存放 1~2 个节点。
2. 根节点总是黑色的。
3. 每个叶子节点都是黑色的空节点(NIL 节点)。这里指的是红黑树都会有一个空的叶子节点,是红黑树自己的规则。
-4. 如果节点是红色的,则它的子节点必须是黑色的(反之不一定)。通常这条规则也叫不会有连续的红色节点。一个节点最多临时会有 3 个节点,中间是黑色节点,左右是红色节点。
+4. 如果节点是红色的,则它的子节点必须是黑色的(反之不一定)。通常这条规则也叫不会有连续的红色节点。一个节点最多临时会有 3 个子节点,中间是黑色节点,左右是红色节点。
5. 从任意节点到它的叶子节点或空子节点的每条路径,必须包含相同数目的黑色节点(即相同的黑色高度)。每一层都只是有一个节点贡献了树高决定平衡性,也就是对应红黑树中的黑色节点。
正是这些特点才保证了红黑树的平衡,让红黑树的高度不会超过 2log(n+1)。
diff --git a/docs/cs-basics/data-structure/tree.md b/docs/cs-basics/data-structure/tree.md
index de9c6eb6a27..267c44d5fef 100644
--- a/docs/cs-basics/data-structure/tree.md
+++ b/docs/cs-basics/data-structure/tree.md
@@ -1,8 +1,13 @@
---
title: 树
+description: 系统讲解树与二叉树的核心概念与遍历方法,结合高度/深度等指标,夯实数据结构基础与算法思维。
category: 计算机基础
tag:
- 数据结构
+head:
+ - - meta
+ - name: keywords
+ content: 树,二叉树,二叉搜索树,平衡树,遍历,前序,中序,后序,层序,高度,深度
---
树就是一种类似现实生活中的树的数据结构(倒置的树)。任何一颗非空树只有一个根节点。
diff --git a/docs/cs-basics/network/application-layer-protocol.md b/docs/cs-basics/network/application-layer-protocol.md
index 5764a72c020..aacb598a991 100644
--- a/docs/cs-basics/network/application-layer-protocol.md
+++ b/docs/cs-basics/network/application-layer-protocol.md
@@ -1,8 +1,13 @@
---
title: 应用层常见协议总结(应用层)
+description: 汇总应用层常见协议的核心概念与典型场景,重点对比 HTTP 与 WebSocket 的通信模型与能力边界。
category: 计算机基础
tag:
- 计算机网络
+head:
+ - - meta
+ - name: keywords
+ content: 应用层协议,HTTP,WebSocket,DNS,SMTP,FTP,特性,场景
---
## HTTP:超文本传输协议
@@ -15,7 +20,7 @@ HTTP 使用客户端-服务器模型,客户端向服务器发送 HTTP Request
HTTP 协议基于 TCP 协议,发送 HTTP 请求之前首先要建立 TCP 连接也就是要经历 3 次握手。目前使用的 HTTP 协议大部分都是 1.1。在 1.1 的协议里面,默认是开启了 Keep-Alive 的,这样的话建立的连接就可以在多次请求中被复用了。
-另外, HTTP 协议是”无状态”的协议,它无法记录客户端用户的状态,一般我们都是通过 Session 来记录客户端用户的状态。
+另外, HTTP 协议是“无状态”的协议,它无法记录客户端用户的状态,一般我们都是通过 Session 来记录客户端用户的状态。
## Websocket:全双工通信协议
diff --git a/docs/cs-basics/network/arp.md b/docs/cs-basics/network/arp.md
index c4ece76011c..10c01312b06 100644
--- a/docs/cs-basics/network/arp.md
+++ b/docs/cs-basics/network/arp.md
@@ -1,8 +1,13 @@
---
title: ARP 协议详解(网络层)
+description: 讲解 ARP 的地址解析机制与报文流程,结合 ARP 表与广播/单播详解常见攻击与防御策略。
category: 计算机基础
tag:
- 计算机网络
+head:
+ - - meta
+ - name: keywords
+ content: ARP,地址解析,IP到MAC,广播问询,单播响应,ARP表,欺骗
---
每当我们学习一个新的网络协议的时候,都要把他结合到 OSI 七层模型中,或者是 TCP/IP 协议栈中来学习,一是要学习该协议在整个网络协议栈中的位置,二是要学习该协议解决了什么问题,地位如何?三是要学习该协议的工作原理,以及一些更深入的细节。
@@ -21,7 +26,7 @@ tag:
MAC 地址的全称是 **媒体访问控制地址(Media Access Control Address)**。如果说,互联网中每一个资源都由 IP 地址唯一标识(IP 协议内容),那么一切网络设备都由 MAC 地址唯一标识。
-
+
可以理解为,MAC 地址是一个网络设备真正的身份证号,IP 地址只是一种不重复的定位方式(比如说住在某省某市某街道的张三,这种逻辑定位是 IP 地址,他的身份证号才是他的 MAC 地址),也可以理解为 MAC 地址是身份证号,IP 地址是邮政地址。MAC 地址也有一些别称,如 LAN 地址、物理地址、以太网地址等。
diff --git a/docs/cs-basics/network/computer-network-xiexiren-summary.md b/docs/cs-basics/network/computer-network-xiexiren-summary.md
index ac7e5d18b97..35bd988e6a5 100644
--- a/docs/cs-basics/network/computer-network-xiexiren-summary.md
+++ b/docs/cs-basics/network/computer-network-xiexiren-summary.md
@@ -1,8 +1,13 @@
---
title: 《计算机网络》(谢希仁)内容总结
+description: 基于《计算机网络》教材的学习笔记,梳理术语与分层模型等核心知识点,便于期末复习与面试巩固。
category: 计算机基础
tag:
- 计算机网络
+head:
+ - - meta
+ - name: keywords
+ content: 计算机网络,谢希仁,术语,分层模型,链路,主机,教材总结
---
本文是我在大二学习计算机网络期间整理, 大部分内容都来自于谢希仁老师的[《计算机网络》第七版](https://www.elias.ltd/usr/local/etc/%E8%AE%A1%E7%AE%97%E6%9C%BA%E7%BD%91%E7%BB%9C%EF%BC%88%E7%AC%AC7%E7%89%88%EF%BC%89%E8%B0%A2%E5%B8%8C%E4%BB%81.pdf)这本书。为了内容更容易理解,我对之前的整理进行了一波重构,并配上了一些相关的示意图便于理解。
@@ -20,36 +25,36 @@ tag:
3. **主机(host)**:连接在因特网上的计算机。
4. **ISP(Internet Service Provider)**:因特网服务提供者(提供商)。
-
+ 
5. **IXP(Internet eXchange Point)**:互联网交换点 IXP 的主要作用就是允许两个网络直接相连并交换分组,而不需要再通过第三个网络来转发分组。
-
+ 
-https://labs.ripe.net/Members/fergalc/ixp-traffic-during-stratos-skydive
+ https://labs.ripe.net/Members/fergalc/ixp-traffic-during-stratos-skydive
6. **RFC(Request For Comments)**:意思是“请求评议”,包含了关于 Internet 几乎所有的重要的文字资料。
7. **广域网 WAN(Wide Area Network)**:任务是通过长距离运送主机发送的数据。
8. **城域网 MAN(Metropolitan Area Network)**:用来将多个局域网进行互连。
9. **局域网 LAN(Local Area Network)**:学校或企业大多拥有多个互连的局域网。
-
+ 
-http://conexionesmanwman.blogspot.com/
+ http://conexionesmanwman.blogspot.com/
10. **个人区域网 PAN(Personal Area Network)**:在个人工作的地方把属于个人使用的电子设备用无线技术连接起来的网络 。
-
+ 
-https://www.itrelease.com/2018/07/advantages-and-disadvantages-of-personal-area-network-pan/
+ https://www.itrelease.com/2018/07/advantages-and-disadvantages-of-personal-area-network-pan/
-12. **分组(packet )**:因特网中传送的数据单元。由首部 header 和数据段组成。分组又称为包,首部可称为包头。
-13. **存储转发(store and forward )**:路由器收到一个分组,先检查分组是否正确,并过滤掉冲突包错误。确定包正确后,取出目的地址,通过查找表找到想要发送的输出端口地址,然后将该包发送出去。
+11. **分组(packet )**:因特网中传送的数据单元。由首部 header 和数据段组成。分组又称为包,首部可称为包头。
+12. **存储转发(store and forward )**:路由器收到一个分组,先检查分组是否正确,并过滤掉冲突包错误。确定包正确后,取出目的地址,通过查找表找到想要发送的输出端口地址,然后将该包发送出去。
-
+ 
-14. **带宽(bandwidth)**:在计算机网络中,表示在单位时间内从网络中的某一点到另一点所能通过的“最高数据率”。常用来表示网络的通信线路所能传送数据的能力。单位是“比特每秒”,记为 b/s。
-15. **吞吐量(throughput )**:表示在单位时间内通过某个网络(或信道、接口)的数据量。吞吐量更经常地用于对现实世界中的网络的一种测量,以便知道实际上到底有多少数据量能够通过网络。吞吐量受网络的带宽或网络的额定速率的限制。
+13. **带宽(bandwidth)**:在计算机网络中,表示在单位时间内从网络中的某一点到另一点所能通过的“最高数据率”。常用来表示网络的通信线路所能传送数据的能力。单位是“比特每秒”,记为 b/s。
+14. **吞吐量(throughput )**:表示在单位时间内通过某个网络(或信道、接口)的数据量。吞吐量更经常地用于对现实世界中的网络的一种测量,以便知道实际上到底有多少数据量能够通过网络。吞吐量受网络的带宽或网络的额定速率的限制。
### 1.2. 重要知识点总结
@@ -81,11 +86,11 @@ tag:
5. **半双工(half duplex )**:通信的双方都可以发送信息,但不能双方同时发送(当然也就不能同时接收)。
6. **全双工(full duplex)**:通信的双方可以同时发送和接收信息。
-
+ 
7. **失真**:失去真实性,主要是指接受到的信号和发送的信号不同,有磨损和衰减。影响失真程度的因素:1.码元传输速率 2.信号传输距离 3.噪声干扰 4.传输媒体质量
-
+ 
8. **奈氏准则**:在任何信道中,码元的传输的效率是有上限的,传输速率超过此上限,就会出现严重的码间串扰问题,使接收端对码元的判决(即识别)成为不可能。
9. **香农定理**:在带宽受限且有噪声的信道中,为了不产生误差,信息的数据传输速率有上限值。
@@ -95,7 +100,7 @@ tag:
13. **信噪比(signal-to-noise ratio )**:指信号的平均功率和噪声的平均功率之比,记为 S/N。信噪比(dB)=10\*log10(S/N)。
14. **信道复用(channel multiplexing )**:指多个用户共享同一个信道。(并不一定是同时)。
-
+ 
15. **比特率(bit rate )**:单位时间(每秒)内传送的比特数。
16. **波特率(baud rate)**:单位时间载波调制状态改变的次数。针对数据信号对载波的调制速率。
@@ -145,13 +150,13 @@ tag:
2. **数据链路(data link)**:把实现控制数据运输的协议的硬件和软件加到链路上就构成了数据链路。
3. **循环冗余检验 CRC(Cyclic Redundancy Check)**:为了保证数据传输的可靠性,CRC 是数据链路层广泛使用的一种检错技术。
4. **帧(frame)**:一个数据链路层的传输单元,由一个数据链路层首部和其携带的封包所组成协议数据单元。
-5. **MTU(Maximum Transfer Uint )**:最大传送单元。帧的数据部分的的长度上限。
+5. **MTU(Maximum Transfer Uint )**:最大传送单元。帧的数据部分的长度上限。
6. **误码率 BER(Bit Error Rate )**:在一段时间内,传输错误的比特占所传输比特总数的比率。
7. **PPP(Point-to-Point Protocol )**:点对点协议。即用户计算机和 ISP 进行通信时所使用的数据链路层协议。以下是 PPP 帧的示意图:

8. **MAC 地址(Media Access Control 或者 Medium Access Control)**:意译为媒体访问控制,或称为物理地址、硬件地址,用来定义网络设备的位置。在 OSI 模型中,第三层网络层负责 IP 地址,第二层数据链路层则负责 MAC 地址。因此一个主机会有一个 MAC 地址,而每个网络位置会有一个专属于它的 IP 地址 。地址是识别某个系统的重要标识符,“名字指出我们所要寻找的资源,地址指出资源所在的地方,路由告诉我们如何到达该处。”
-
+ 
9. **网桥(bridge)**:一种用于数据链路层实现中继,连接两个或多个局域网的网络互连设备。
10. **交换机(switch )**:广义的来说,交换机指的是一种通信系统中完成信息交换的设备。这里工作在数据链路层的交换机指的是交换式集线器,其实质是一个多接口的网桥
@@ -191,7 +196,7 @@ tag:
5. **子网掩码(subnet mask )**:它是一种用来指明一个 IP 地址的哪些位标识的是主机所在的子网以及哪些位标识的是主机的位掩码。子网掩码不能单独存在,它必须结合 IP 地址一起使用。
6. **CIDR( Classless Inter-Domain Routing )**:无分类域间路由选择 (特点是消除了传统的 A 类、B 类和 C 类地址以及划分子网的概念,并使用各种长度的“网络前缀”(network-prefix)来代替分类地址中的网络号和子网号)。
7. **默认路由(default route)**:当在路由表中查不到能到达目的地址的路由时,路由器选择的路由。默认路由还可以减小路由表所占用的空间和搜索路由表所用的时间。
-8. **路由选择算法(Virtual Circuit)**:路由选择协议的核心部分。因特网采用自适应的,分层次的路由选择协议。
+8. **路由选择算法(Routing Algorithm)**:路由选择协议的核心部分。因特网采用自适应的,分层次的路由选择协议。
### 4.2. 重要知识点总结
@@ -218,7 +223,7 @@ tag:
4. **TCP(Transmission Control Protocol)**:传输控制协议。
5. **UDP(User Datagram Protocol)**:用户数据报协议。
-
+ 
6. **端口(port)**:端口的目的是为了确认对方机器的哪个进程在与自己进行交互,比如 MSN 和 QQ 的端口不同,如果没有端口就可能出现 QQ 进程和 MSN 交互错误。端口又称协议端口号。
7. **停止等待协议(stop-and-wait)**:指发送方每发送完一个分组就停止发送,等待对方确认,在收到确认之后在发送下一个分组。
@@ -267,34 +272,34 @@ tag:
1. **域名系统(DNS)**:域名系统(DNS,Domain Name System)将人类可读的域名 (例如,www.baidu.com) 转换为机器可读的 IP 地址 (例如,220.181.38.148)。我们可以将其理解为专为互联网设计的电话薄。
-
+ 
-https://www.seobility.net/en/wiki/HTTP_headers
+ https://www.seobility.net/en/wiki/HTTP_headers
2. **文件传输协议(FTP)**:FTP 是 File Transfer Protocol(文件传输协议)的英文简称,而中文简称为“文传协议”。用于 Internet 上的控制文件的双向传输。同时,它也是一个应用程序(Application)。基于不同的操作系统有不同的 FTP 应用程序,而所有这些应用程序都遵守同一种协议以传输文件。在 FTP 的使用当中,用户经常遇到两个概念:"下载"(Download)和"上传"(Upload)。 "下载"文件就是从远程主机拷贝文件至自己的计算机上;"上传"文件就是将文件从自己的计算机中拷贝至远程主机上。用 Internet 语言来说,用户可通过客户机程序向(从)远程主机上传(下载)文件。
-
+ 
3. **简单文件传输协议(TFTP)**:TFTP(Trivial File Transfer Protocol,简单文件传输协议)是 TCP/IP 协议族中的一个用来在客户机与服务器之间进行简单文件传输的协议,提供不复杂、开销不大的文件传输服务。端口号为 69。
4. **远程终端协议(TELNET)**:Telnet 协议是 TCP/IP 协议族中的一员,是 Internet 远程登陆服务的标准协议和主要方式。它为用户提供了在本地计算机上完成远程主机工作的能力。在终端使用者的电脑上使用 telnet 程序,用它连接到服务器。终端使用者可以在 telnet 程序中输入命令,这些命令会在服务器上运行,就像直接在服务器的控制台上输入一样。可以在本地就能控制服务器。要开始一个 telnet 会话,必须输入用户名和密码来登录服务器。Telnet 是常用的远程控制 Web 服务器的方法。
5. **万维网(WWW)**:WWW 是环球信息网的缩写,(亦作“Web”、“WWW”、“'W3'”,英文全称为“World Wide Web”),中文名字为“万维网”,"环球网"等,常简称为 Web。分为 Web 客户端和 Web 服务器程序。WWW 可以让 Web 客户端(常用浏览器)访问浏览 Web 服务器上的页面。是一个由许多互相链接的超文本组成的系统,通过互联网访问。在这个系统中,每个有用的事物,称为一样“资源”;并且由一个全局“统一资源标识符”(URI)标识;这些资源通过超文本传输协议(Hypertext Transfer Protocol)传送给用户,而后者通过点击链接来获得资源。万维网联盟(英语:World Wide Web Consortium,简称 W3C),又称 W3C 理事会。1994 年 10 月在麻省理工学院(MIT)计算机科学实验室成立。万维网联盟的创建者是万维网的发明者蒂姆·伯纳斯-李。万维网并不等同互联网,万维网只是互联网所能提供的服务其中之一,是靠着互联网运行的一项服务。
6. **万维网的大致工作工程:**
-
+ 
7. **统一资源定位符(URL)**:统一资源定位符是对可以从互联网上得到的资源的位置和访问方法的一种简洁的表示,是互联网上标准资源的地址。互联网上的每个文件都有一个唯一的 URL,它包含的信息指出文件的位置以及浏览器应该怎么处理它。
8. **超文本传输协议(HTTP)**:超文本传输协议(HTTP,HyperText Transfer Protocol)是互联网上应用最为广泛的一种网络协议。所有的 WWW 文件都必须遵守这个标准。设计 HTTP 最初的目的是为了提供一种发布和接收 HTML 页面的方法。1960 年美国人 Ted Nelson 构思了一种通过计算机处理文本信息的方法,并称之为超文本(hypertext),这成为了 HTTP 超文本传输协议标准架构的发展根基。
-HTTP 协议的本质就是一种浏览器与服务器之间约定好的通信格式。HTTP 的原理如下图所示:
+ HTTP 协议的本质就是一种浏览器与服务器之间约定好的通信格式。HTTP 的原理如下图所示:
-
+ 
-10. **代理服务器(Proxy Server)**:代理服务器(Proxy Server)是一种网络实体,它又称为万维网高速缓存。 代理服务器把最近的一些请求和响应暂存在本地磁盘中。当新请求到达时,若代理服务器发现这个请求与暂时存放的的请求相同,就返回暂存的响应,而不需要按 URL 的地址再次去互联网访问该资源。代理服务器可在客户端或服务器工作,也可以在中间系统工作。
-11. **简单邮件传输协议(SMTP)** : SMTP(Simple Mail Transfer Protocol)即简单邮件传输协议,它是一组用于由源地址到目的地址传送邮件的规则,由它来控制信件的中转方式。 SMTP 协议属于 TCP/IP 协议簇,它帮助每台计算机在发送或中转信件时找到下一个目的地。 通过 SMTP 协议所指定的服务器,就可以把 E-mail 寄到收信人的服务器上了,整个过程只要几分钟。SMTP 服务器则是遵循 SMTP 协议的发送邮件服务器,用来发送或中转发出的电子邮件。
+9. **代理服务器(Proxy Server)**:代理服务器(Proxy Server)是一种网络实体,它又称为万维网高速缓存。 代理服务器把最近的一些请求和响应暂存在本地磁盘中。当新请求到达时,若代理服务器发现这个请求与暂时存放的请求相同,就返回暂存的响应,而不需要按 URL 的地址再次去互联网访问该资源。代理服务器可在客户端或服务器工作,也可以在中间系统工作。
+10. **简单邮件传输协议(SMTP)** : SMTP(Simple Mail Transfer Protocol)即简单邮件传输协议,它是一组用于由源地址到目的地址传送邮件的规则,由它来控制信件的中转方式。 SMTP 协议属于 TCP/IP 协议簇,它帮助每台计算机在发送或中转信件时找到下一个目的地。 通过 SMTP 协议所指定的服务器,就可以把 E-mail 寄到收信人的服务器上了,整个过程只要几分钟。SMTP 服务器则是遵循 SMTP 协议的发送邮件服务器,用来发送或中转发出的电子邮件。
-
+ 
-https://www.campaignmonitor.com/resources/knowledge-base/what-is-the-code-that-makes-bcc-or-cc-operate-in-an-email/
+ https://www.campaignmonitor.com/resources/knowledge-base/what-is-the-code-that-makes-bcc-or-cc-operate-in-an-email/
11. **搜索引擎** :搜索引擎(Search Engine)是指根据一定的策略、运用特定的计算机程序从互联网上搜集信息,在对信息进行组织和处理后,为用户提供检索服务,将用户检索相关的信息展示给用户的系统。搜索引擎包括全文索引、目录索引、元搜索引擎、垂直搜索引擎、集合式搜索引擎、门户搜索引擎与免费链接列表等。
diff --git a/docs/cs-basics/network/dns.md b/docs/cs-basics/network/dns.md
index 3d3ef0e2254..1563fb4fcbe 100644
--- a/docs/cs-basics/network/dns.md
+++ b/docs/cs-basics/network/dns.md
@@ -1,8 +1,13 @@
---
title: DNS 域名系统详解(应用层)
+description: 详解 DNS 的层次结构与解析流程,覆盖递归/迭代、缓存与权威服务器,明确应用层端口与性能优化要点。
category: 计算机基础
tag:
- 计算机网络
+head:
+ - - meta
+ - name: keywords
+ content: DNS,域名解析,递归查询,迭代查询,缓存,权威DNS,端口53,UDP
---
DNS(Domain Name System)域名管理系统,是当用户使用浏览器访问网址之后,使用的第一个重要协议。DNS 要解决的是**域名和 IP 地址的映射问题**。
diff --git a/docs/cs-basics/network/http-status-codes.md b/docs/cs-basics/network/http-status-codes.md
index e281f44ca19..bd2bcd99c3d 100644
--- a/docs/cs-basics/network/http-status-codes.md
+++ b/docs/cs-basics/network/http-status-codes.md
@@ -1,8 +1,13 @@
---
title: HTTP 常见状态码总结(应用层)
+description: 汇总常见 HTTP 状态码含义与使用场景,强调 201/204 等易混淆点,提升接口设计与调试效率。
category: 计算机基础
tag:
- 计算机网络
+head:
+ - - meta
+ - name: keywords
+ content: HTTP 状态码,2xx,3xx,4xx,5xx,重定向,错误码,201 Created,204 No Content
---
HTTP 状态码用于描述 HTTP 请求的结果,比如 2xx 就代表请求被成功处理。
@@ -15,10 +20,14 @@ HTTP 状态码用于描述 HTTP 请求的结果,比如 2xx 就代表请求被
### 2xx Success(成功状态码)
-- **200 OK**:请求被成功处理。比如我们发送一个查询用户数据的 HTTP 请求到服务端,服务端正确返回了用户数据。这个是我们平时最常见的一个 HTTP 状态码。
-- **201 Created**:请求被成功处理并且在服务端创建了一个新的资源。比如我们通过 POST 请求创建一个新的用户。
-- **202 Accepted**:服务端已经接收到了请求,但是还未处理。
-- **204 No Content**:服务端已经成功处理了请求,但是没有返回任何内容。
+- **200 OK**:请求被成功处理。例如,发送一个查询用户数据的 HTTP 请求到服务端,服务端正确返回了用户数据。这个是我们平时最常见的一个 HTTP 状态码。
+- **201 Created**:请求被成功处理并且在服务端创建了~~一个新的资源~~。例如,通过 POST 请求创建一个新的用户。
+- **202 Accepted**:服务端已经接收到了请求,但是还未处理。例如,发送一个需要服务端花费较长时间处理的请求(如报告生成、Excel 导出),服务端接收了请求但尚未处理完毕。
+- **204 No Content**:服务端已经成功处理了请求,但是没有返回任何内容。例如,发送请求删除一个用户,服务器成功处理了删除操作但没有返回任何内容。
+
+🐛 修正(参见:[issue#2458](https://github.com/Snailclimb/JavaGuide/issues/2458)):201 Created 状态码更准确点来说是创建一个或多个新的资源,可以参考:。
+
+
这里格外提一下 204 状态码,平时学习/工作中见到的次数并不多。
diff --git a/docs/cs-basics/network/http-vs-https.md b/docs/cs-basics/network/http-vs-https.md
index 71c224f1be4..36691de06b3 100644
--- a/docs/cs-basics/network/http-vs-https.md
+++ b/docs/cs-basics/network/http-vs-https.md
@@ -1,8 +1,13 @@
---
title: HTTP vs HTTPS(应用层)
+description: 对比 HTTP 与 HTTPS 的协议与安全机制,解析 SSL/TLS 工作原理与握手流程,明确应用层安全落地细节。
category: 计算机基础
tag:
- 计算机网络
+head:
+ - - meta
+ - name: keywords
+ content: HTTP,HTTPS,SSL,TLS,加密,认证,端口,安全性,握手流程
---
## HTTP 协议
diff --git a/docs/cs-basics/network/http1.0-vs-http1.1.md b/docs/cs-basics/network/http1.0-vs-http1.1.md
index eab4c324a51..430437585d3 100644
--- a/docs/cs-basics/network/http1.0-vs-http1.1.md
+++ b/docs/cs-basics/network/http1.0-vs-http1.1.md
@@ -1,8 +1,13 @@
---
title: HTTP 1.0 vs HTTP 1.1(应用层)
+description: 细致对比 HTTP/1.0 与 HTTP/1.1 的协议差异,涵盖长连接、管道化、缓存与状态码增强等关键变更与实践影响。
category: 计算机基础
tag:
- 计算机网络
+head:
+ - - meta
+ - name: keywords
+ content: HTTP/1.0,HTTP/1.1,长连接,管道化,缓存,状态码,Host,带宽优化
---
这篇文章会从下面几个维度来对比 HTTP 1.0 和 HTTP 1.1:
@@ -70,9 +75,70 @@ Host: example1.org
HTTP/1.1 引入了范围请求(range request)机制,以避免带宽的浪费。当客户端想请求一个文件的一部分,或者需要继续下载一个已经下载了部分但被终止的文件,HTTP/1.1 可以在请求中加入`Range`头部,以请求(并只能请求字节型数据)数据的一部分。服务器端可以忽略`Range`头部,也可以返回若干`Range`响应。
-如果一个响应包含部分数据的话,那么将带有`206 (Partial Content)`状态码。该状态码的意义在于避免了 HTTP/1.0 代理缓存错误地把该响应认为是一个完整的数据响应,从而把他当作为一个请求的响应缓存。
+`206 (Partial Content)` 状态码的主要作用是确保客户端和代理服务器能正确识别部分内容响应,避免将其误认为完整资源并错误地缓存。这对于正确处理范围请求和缓存管理非常重要。
-在范围响应中,`Content-Range`头部标志指示出了该数据块的偏移量和数据块的长度。
+一个典型的 HTTP/1.1 范围请求示例:
+
+```bash
+# 获取一个文件的前 1024 个字节
+GET /z4d4kWk.jpg HTTP/1.1
+Host: i.imgur.com
+Range: bytes=0-1023
+```
+
+`206 Partial Content` 响应:
+
+```bash
+
+HTTP/1.1 206 Partial Content
+Content-Range: bytes 0-1023/146515
+Content-Length: 1024
+…
+(二进制内容)
+```
+
+简单解释一下 HTTP 范围响应头部中的字段:
+
+- **`Content-Range` 头部**:指示返回数据在整个资源中的位置,包括起始和结束字节以及资源的总长度。例如,`Content-Range: bytes 0-1023/146515` 表示服务器端返回了第 0 到 1023 字节的数据(共 1024 字节),而整个资源的总长度是 146,515 字节。
+- **`Content-Length` 头部**:指示此次响应中实际传输的字节数。例如,`Content-Length: 1024` 表示服务器端传输了 1024 字节的数据。
+
+`Range` 请求头不仅可以请求单个字节范围,还可以一次性请求多个范围。这种方式被称为“多重范围请求”(multiple range requests)。
+
+客户端想要获取资源的第 0 到 499 字节以及第 1000 到 1499 字节:
+
+```bash
+GET /path/to/resource HTTP/1.1
+Host: example.com
+Range: bytes=0-499,1000-1499
+```
+
+服务器端返回多个字节范围,每个范围的内容以分隔符分开:
+
+```bash
+HTTP/1.1 206 Partial Content
+Content-Type: multipart/byteranges; boundary=3d6b6a416f9b5
+Content-Length: 376
+
+--3d6b6a416f9b5
+Content-Type: application/octet-stream
+Content-Range: bytes 0-99/2000
+
+(第 0 到 99 字节的数据块)
+
+--3d6b6a416f9b5
+Content-Type: application/octet-stream
+Content-Range: bytes 500-599/2000
+
+(第 500 到 599 字节的数据块)
+
+--3d6b6a416f9b5
+Content-Type: application/octet-stream
+Content-Range: bytes 1000-1099/2000
+
+(第 1000 到 1099 字节的数据块)
+
+--3d6b6a416f9b5--
+```
### 状态码 100
diff --git a/docs/cs-basics/network/images/arp/2008410143049281.png b/docs/cs-basics/network/images/arp/2008410143049281.png
deleted file mode 100644
index 759fb441f6c..00000000000
Binary files a/docs/cs-basics/network/images/arp/2008410143049281.png and /dev/null differ
diff --git a/docs/cs-basics/network/nat.md b/docs/cs-basics/network/nat.md
index c9c879b4293..51443c259b5 100644
--- a/docs/cs-basics/network/nat.md
+++ b/docs/cs-basics/network/nat.md
@@ -1,8 +1,13 @@
---
title: NAT 协议详解(网络层)
+description: 解析 NAT 的地址转换与端口映射机制,结合 LAN/WAN 通信与转换表,理解家庭与企业网络的实践细节。
category: 计算机基础
tag:
- 计算机网络
+head:
+ - - meta
+ - name: keywords
+ content: NAT,地址转换,端口映射,LAN,WAN,连接跟踪,DHCP
---
## 应用场景
@@ -45,7 +50,7 @@ SOHO 子网的“代理人”,也就是和外界的窗口,通常由路由器
针对以上过程,有以下几个重点需要强调:
1. 当请求报文到达路由器,并被指定了新端口号时,由于端口号有 16 位,因此,通常来说,一个路由器管理的 LAN 中的最大主机数 $≈65500$($2^{16}$ 的地址空间),但通常 SOHO 子网内不会有如此多的主机数量。
-2. 对于目的服务器来说,从来不知道“到底是哪个主机给我发送的请求”,它只知道是来自`138.76.29.7:5001`的路由器转发的请求。因此,可以说,**路由器在 WAN 和 LAN 之间起到了屏蔽作用,**所有内部主机发送到外部的报文,都具有同一个 IP 地址(不同的端口号),所有外部发送到内部的报文,也都只有一个目的地(不同端口号),是经过了 NAT 转换后,外部报文才得以正确地送达内部主机。
+2. 对于目的服务器来说,从来不知道“到底是哪个主机给我发送的请求”,它只知道是来自`138.76.29.7:5001`的路由器转发的请求。因此,可以说,**路由器在 WAN 和 LAN 之间起到了屏蔽作用**,所有内部主机发送到外部的报文,都具有同一个 IP 地址(不同的端口号),所有外部发送到内部的报文,也都只有一个目的地(不同端口号),是经过了 NAT 转换后,外部报文才得以正确地送达内部主机。
3. 在报文穿过路由器,发生 NAT 转换时,如果 LAN 主机 IP 已经在 NAT 转换表中注册过了,则不需要路由器新指派端口,而是直接按照转换记录穿过路由器。同理,外部报文发送至内部时也如此。
总结 NAT 协议的特点,有以下几点:
@@ -55,6 +60,6 @@ SOHO 子网的“代理人”,也就是和外界的窗口,通常由路由器
3. WAN 的 ISP 变更接口地址时,无需通告 LAN 内主机。
4. LAN 主机对 WAN 不可见,不可直接寻址,可以保证一定程度的安全性。
-然而,NAT 协议由于其独特性,存在着一些争议。比如,可能你已经注意到了,NAT 协议在 LAN 以外,标识一个内部主机时,使用的是端口号,因为 IP 地址都是相同的。 这种将端口号作为主机寻址的行为,可能会引发一些误会。此外,路由器作为网络层的设备,修改了传输层的分组内容(修改了源 IP 地址和端口号),同样是不规范的行为。但是,尽管如此,NAT 协议作为 IPv4 时代的产物,极大地方便了一些本来棘手的问题,一直被沿用至今。
+然而,NAT 协议由于其独特性,存在着一些争议。比如,可能你已经注意到了,**NAT 协议在 LAN 以外,标识一个内部主机时,使用的是端口号,因为 IP 地址都是相同的**。这种将端口号作为主机寻址的行为,可能会引发一些误会。此外,路由器作为网络层的设备,修改了传输层的分组内容(修改了源 IP 地址和端口号),同样是不规范的行为。但是,尽管如此,NAT 协议作为 IPv4 时代的产物,极大地方便了一些本来棘手的问题,一直被沿用至今。
diff --git a/docs/cs-basics/network/network-attack-means.md b/docs/cs-basics/network/network-attack-means.md
index 7f142af00c3..b6026a2c699 100644
--- a/docs/cs-basics/network/network-attack-means.md
+++ b/docs/cs-basics/network/network-attack-means.md
@@ -1,8 +1,13 @@
---
title: 网络攻击常见手段总结
+description: 总结常见 TCP/IP 攻击与防护思路,覆盖 DDoS、IP/ARP 欺骗、中间人等手段,强调工程防护实践。
category: 计算机基础
tag:
- 计算机网络
+head:
+ - - meta
+ - name: keywords
+ content: 网络攻击,DDoS,IP 欺骗,ARP 欺骗,中间人攻击,扫描,防护
---
> 本文整理完善自[TCP/IP 常见攻击手段 - 暖蓝笔记 - 2021](https://mp.weixin.qq.com/s/AZwWrOlLxRSSi-ywBgZ0fA)这篇文章。
@@ -357,17 +362,17 @@ DES 使用的密钥表面上是 64 位的,然而只有其中的 56 位被实
这个大家应该更加熟悉了,比如我们平常使用的 MD5 校验,在很多时候,我并不是拿来进行加密,而是用来获得唯一性 ID。在做系统的过程中,存储用户的各种密码信息,通常都会通过散列算法,最终存储其散列值。
-**MD5**
+**MD5**(不推荐)
MD5 可以用来生成一个 128 位的消息摘要,它是目前应用比较普遍的散列算法,具体的应用场景你可以自行 参阅。虽然,因为算法的缺陷,它的唯一性已经被破解了,但是大部分场景下,这并不会构成安全问题。但是,如果不是长度受限(32 个字符),我还是不推荐你继续使用 **MD5** 的。
**SHA**
-安全散列算法。**SHA** 分为 **SHA1** 和 **SH2** 两个版本。该算法的思想是接收一段明文,然后以一种不可逆的方式将它转换成一段(通常更小)密文,也可以简单的理解为取一串输入码(称为预映射或信息),并把它们转化为长度较短、位数固定的输出序列即散列值(也称为信息摘要或信息认证代码)的过程。
+安全散列算法。**SHA** 包括**SHA-1**、**SHA-2**和**SHA-3**三个版本。该算法的基本思想是:接收一段明文数据,通过不可逆的方式将其转换为固定长度的密文。简单来说,SHA 将输入数据(即预映射或消息)转化为固定长度、较短的输出值,称为散列值(或信息摘要、信息认证码)。SHA-1 已被证明不够安全,因此逐渐被 SHA-2 取代,而 SHA-3 则作为 SHA 系列的最新版本,采用不同的结构(Keccak 算法)提供更高的安全性和灵活性。
**SM3**
-国密算法**SM3**。加密强度和 SHA-256 想不多。主要是收到国家的支持。
+国密算法**SM3**。加密强度和 SHA-256 算法 相差不多。主要是受到了国家的支持。
**总结**:
diff --git a/docs/cs-basics/network/osi-and-tcp-ip-model.md b/docs/cs-basics/network/osi-and-tcp-ip-model.md
index 34092a336b6..85b842efcf5 100644
--- a/docs/cs-basics/network/osi-and-tcp-ip-model.md
+++ b/docs/cs-basics/network/osi-and-tcp-ip-model.md
@@ -1,8 +1,13 @@
---
title: OSI 和 TCP/IP 网络分层模型详解(基础)
+description: 详解 OSI 与 TCP/IP 的分层模型与职责划分,结合历史与实践对比两者差异与工程取舍。
category: 计算机基础
tag:
- 计算机网络
+head:
+ - - meta
+ - name: keywords
+ content: OSI 七层,TCP/IP 四层,分层模型,职责划分,协议栈,对比
---
## OSI 七层模型
diff --git a/docs/cs-basics/network/other-network-questions.md b/docs/cs-basics/network/other-network-questions.md
index 8650ad5c283..22abf27cb31 100644
--- a/docs/cs-basics/network/other-network-questions.md
+++ b/docs/cs-basics/network/other-network-questions.md
@@ -1,8 +1,13 @@
---
title: 计算机网络常见面试题总结(上)
+description: 最新计算机网络高频面试题总结(上):TCP/IP四层模型、HTTP全版本对比、TCP三次握手、DNS解析、WebSocket/SSE实时推送等,附图解+⭐️重点标注,一文搞定应用层&传输层&网络层核心考点,快速备战后端面试!
category: 计算机基础
tag:
- 计算机网络
+head:
+ - - meta
+ - name: keywords
+ content: 计算机网络面试题,TCP/IP四层模型,HTTP面试,HTTPS vs HTTP,HTTP/1.1 vs HTTP/2,HTTP/3 QUIC,TCP三次握手,UDP区别,DNS解析,WebSocket vs SSE,GET vs POST,应用层协议,网络分层,队头阻塞,PING命令,ARP协议
---
@@ -27,7 +32,7 @@ tag:

-#### TCP/IP 四层模型是什么?每一层的作用是什么?
+#### ⭐️TCP/IP 四层模型是什么?每一层的作用是什么?
**TCP/IP 四层模型** 是目前被广泛采用的一种模型,我们可以将 TCP / IP 模型看作是 OSI 七层模型的精简版本,由以下 4 层组成:
@@ -40,7 +45,7 @@ tag:

-关于每一层作用的详细介绍,请看 [OSI 和 TCP/IP 网络分层模型详解(基础)](./osi-and-tcp-ip-model.md) 这篇文章。
+关于每一层作用的详细介绍,请看 [OSI 和 TCP/IP 网络分层模型详解(基础)](https://javaguide.cn/cs-basics/network/osi-and-tcp-ip-model.html) 这篇文章。
#### 为什么网络要分层?
@@ -64,7 +69,7 @@ tag:
### 常见网络协议
-#### 应用层有哪些常见的协议?
+#### ⭐️应用层有哪些常见的协议?

@@ -100,7 +105,7 @@ tag:
## HTTP
-### 从输入 URL 到页面展示到底发生了什么?(非常重要)
+### ⭐️从输入 URL 到页面展示到底发生了什么?(非常重要)
> 类似的问题:打开一个网页,整个过程会使用哪些协议?
@@ -120,54 +125,54 @@ tag:
6. 浏览器收到 HTTP 响应报文后,解析响应体中的 HTML 代码,渲染网页的结构和样式,同时根据 HTML 中的其他资源的 URL(如图片、CSS、JS 等),再次发起 HTTP 请求,获取这些资源的内容,直到网页完全加载显示。
7. 浏览器在不需要和服务器通信时,可以主动关闭 TCP 连接,或者等待服务器的关闭请求。
-详细介绍可以查看这篇文章:[访问网页的全过程(知识串联)](./the-whole-process-of-accessing-web-pages.md)(强烈推荐)。
+详细介绍可以查看这篇文章:[访问网页的全过程(知识串联)](https://javaguide.cn/cs-basics/network/the-whole-process-of-accessing-web-pages.html)(强烈推荐)。
-### HTTP 状态码有哪些?
+### ⭐️HTTP 状态码有哪些?
HTTP 状态码用于描述 HTTP 请求的结果,比如 2xx 就代表请求被成功处理。

-关于 HTTP 状态码更详细的总结,可以看我写的这篇文章:[HTTP 常见状态码总结(应用层)](./http-status-codes.md)。
+关于 HTTP 状态码更详细的总结,可以看我写的这篇文章:[HTTP 常见状态码总结(应用层)](https://javaguide.cn/cs-basics/network/http-status-codes.html)。
### HTTP Header 中常见的字段有哪些?
-| 请求头字段名 | 说明 | 示例 |
-| :------------------ | :---------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | :-------------------------------------------------------------------------------------- |
-| Accept | 能够接受的回应内容类型(Content-Types)。 | Accept: text/plain |
-| Accept-Charset | 能够接受的字符集 | Accept-Charset: utf-8 |
-| Accept-Datetime | 能够接受的按照时间来表示的版本 | Accept-Datetime: Thu, 31 May 2007 20:35:00 GMT |
-| Accept-Encoding | 能够接受的编码方式列表。参考 HTTP 压缩。 | Accept-Encoding: gzip, deflate |
-| Accept-Language | 能够接受的回应内容的自然语言列表。 | Accept-Language: en-US |
-| Authorization | 用于超文本传输协议的认证的认证信息 | Authorization: Basic QWxhZGRpbjpvcGVuIHNlc2FtZQ== |
-| Cache-Control | 用来指定在这次的请求/响应链中的所有缓存机制 都必须 遵守的指令 | Cache-Control: no-cache |
-| Connection | 该浏览器想要优先使用的连接类型 | Connection: keep-alive |
-| Content-Length | 以八位字节数组(8 位的字节)表示的请求体的长度 | Content-Length: 348 |
-| Content-MD5 | 请求体的内容的二进制 MD5 散列值,以 Base64 编码的结果 | Content-MD5: Q2hlY2sgSW50ZWdyaXR5IQ== |
-| Content-Type | 请求体的多媒体类型(用于 POST 和 PUT 请求中) | Content-Type: application/x-www-form-urlencoded |
-| Cookie | 之前由服务器通过 Set-Cookie(下文详述)发送的一个超文本传输协议 Cookie | Cookie: $Version=1; Skin=new; |
-| Date | 发送该消息的日期和时间(按照 RFC 7231 中定义的"超文本传输协议日期"格式来发送) | Date: Tue, 15 Nov 1994 08:12:31 GMT |
-| Expect | 表明客户端要求服务器做出特定的行为 | Expect: 100-continue |
-| From | 发起此请求的用户的邮件地址 | From: [user@example.com](mailto:user@example.com) |
-| Host | 服务器的域名(用于虚拟主机),以及服务器所监听的传输控制协议端口号。如果所请求的端口是对应的服务的标准端口,则端口号可被省略。 | Host: en.wikipedia.org |
-| If-Match | 仅当客户端提供的实体与服务器上对应的实体相匹配时,才进行对应的操作。主要作用是用于像 PUT 这样的方法中,仅当从用户上次更新某个资源以来,该资源未被修改的情况下,才更新该资源。 | If-Match: "737060cd8c284d8af7ad3082f209582d" |
-| If-Modified-Since | 允许服务器在请求的资源自指定的日期以来未被修改的情况下返回 `304 Not Modified` 状态码 | If-Modified-Since: Sat, 29 Oct 1994 19:43:31 GMT |
-| If-None-Match | 允许服务器在请求的资源的 ETag 未发生变化的情况下返回 `304 Not Modified` 状态码 | If-None-Match: "737060cd8c284d8af7ad3082f209582d" |
-| If-Range | 如果该实体未被修改过,则向我发送我所缺少的那一个或多个部分;否则,发送整个新的实体 | If-Range: "737060cd8c284d8af7ad3082f209582d" |
-| If-Unmodified-Since | 仅当该实体自某个特定时间以来未被修改的情况下,才发送回应。 | If-Unmodified-Since: Sat, 29 Oct 1994 19:43:31 GMT |
-| Max-Forwards | 限制该消息可被代理及网关转发的次数。 | Max-Forwards: 10 |
-| Origin | 发起一个针对跨来源资源共享的请求。 | Origin: [http://www.example-social-network.com](http://www.example-social-network.com/) |
-| Pragma | 与具体的实现相关,这些字段可能在请求/回应链中的任何时候产生多种效果。 | Pragma: no-cache |
-| Proxy-Authorization | 用来向代理进行认证的认证信息。 | Proxy-Authorization: Basic QWxhZGRpbjpvcGVuIHNlc2FtZQ== |
-| Range | 仅请求某个实体的一部分。字节偏移以 0 开始。参见字节服务。 | Range: bytes=500-999 |
-| Referer | 表示浏览器所访问的前一个页面,正是那个页面上的某个链接将浏览器带到了当前所请求的这个页面。 | Referer: http://en.wikipedia.org/wiki/Main_Page |
-| TE | 浏览器预期接受的传输编码方式:可使用回应协议头 Transfer-Encoding 字段中的值; | TE: trailers, deflate |
-| Upgrade | 要求服务器升级到另一个协议。 | Upgrade: HTTP/2.0, SHTTP/1.3, IRC/6.9, RTA/x11 |
-| User-Agent | 浏览器的浏览器身份标识字符串 | User-Agent: Mozilla/5.0 (X11; Linux x86_64; rv:12.0) Gecko/20100101 Firefox/21.0 |
-| Via | 向服务器告知,这个请求是由哪些代理发出的。 | Via: 1.0 fred, 1.1 example.com (Apache/1.1) |
-| Warning | 一个一般性的警告,告知,在实体内容体中可能存在错误。 | Warning: 199 Miscellaneous warning |
-
-### HTTP 和 HTTPS 有什么区别?(重要)
+| 请求头字段名 | 说明 | 示例 |
+| :------------------ | :---------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | :------------------------------------------------------------------------------- |
+| Accept | 能够接受的回应内容类型(Content-Types)。 | Accept: text/plain |
+| Accept-Charset | 能够接受的字符集 | Accept-Charset: utf-8 |
+| Accept-Datetime | 能够接受的按照时间来表示的版本 | Accept-Datetime: Thu, 31 May 2007 20:35:00 GMT |
+| Accept-Encoding | 能够接受的编码方式列表。参考 HTTP 压缩。 | Accept-Encoding: gzip, deflate |
+| Accept-Language | 能够接受的回应内容的自然语言列表。 | Accept-Language: en-US |
+| Authorization | 用于超文本传输协议的认证的认证信息 | Authorization: Basic QWxhZGRpbjpvcGVuIHNlc2FtZQ== |
+| Cache-Control | 用来指定在这次的请求/响应链中的所有缓存机制 都必须 遵守的指令 | Cache-Control: no-cache |
+| Connection | 该浏览器想要优先使用的连接类型 | Connection: keep-alive |
+| Content-Length | 以八位字节数组(8 位的字节)表示的请求体的长度 | Content-Length: 348 |
+| Content-MD5 | 请求体的内容的二进制 MD5 散列值,以 Base64 编码的结果 | Content-MD5: Q2hlY2sgSW50ZWdyaXR5IQ== |
+| Content-Type | 请求体的多媒体类型(用于 POST 和 PUT 请求中) | Content-Type: application/x-www-form-urlencoded |
+| Cookie | 之前由服务器通过 Set-Cookie(下文详述)发送的一个超文本传输协议 Cookie | Cookie: $Version=1; Skin=new; |
+| Date | 发送该消息的日期和时间(按照 RFC 7231 中定义的"超文本传输协议日期"格式来发送) | Date: Tue, 15 Nov 1994 08:12:31 GMT |
+| Expect | 表明客户端要求服务器做出特定的行为 | Expect: 100-continue |
+| From | 发起此请求的用户的邮件地址 | From: `user@example.com` |
+| Host | 服务器的域名(用于虚拟主机),以及服务器所监听的传输控制协议端口号。如果所请求的端口是对应的服务的标准端口,则端口号可被省略。 | Host: en.wikipedia.org |
+| If-Match | 仅当客户端提供的实体与服务器上对应的实体相匹配时,才进行对应的操作。主要作用是用于像 PUT 这样的方法中,仅当从用户上次更新某个资源以来,该资源未被修改的情况下,才更新该资源。 | If-Match: "737060cd8c284d8af7ad3082f209582d" |
+| If-Modified-Since | 允许服务器在请求的资源自指定的日期以来未被修改的情况下返回 `304 Not Modified` 状态码 | If-Modified-Since: Sat, 29 Oct 1994 19:43:31 GMT |
+| If-None-Match | 允许服务器在请求的资源的 ETag 未发生变化的情况下返回 `304 Not Modified` 状态码 | If-None-Match: "737060cd8c284d8af7ad3082f209582d" |
+| If-Range | 如果该实体未被修改过,则向我发送我所缺少的那一个或多个部分;否则,发送整个新的实体 | If-Range: "737060cd8c284d8af7ad3082f209582d" |
+| If-Unmodified-Since | 仅当该实体自某个特定时间以来未被修改的情况下,才发送回应。 | If-Unmodified-Since: Sat, 29 Oct 1994 19:43:31 GMT |
+| Max-Forwards | 限制该消息可被代理及网关转发的次数。 | Max-Forwards: 10 |
+| Origin | 发起一个针对跨来源资源共享的请求。 | `Origin: http://www.example-social-network.com` |
+| Pragma | 与具体的实现相关,这些字段可能在请求/回应链中的任何时候产生多种效果。 | Pragma: no-cache |
+| Proxy-Authorization | 用来向代理进行认证的认证信息。 | Proxy-Authorization: Basic QWxhZGRpbjpvcGVuIHNlc2FtZQ== |
+| Range | 仅请求某个实体的一部分。字节偏移以 0 开始。参见字节服务。 | Range: bytes=500-999 |
+| Referer | 表示浏览器所访问的前一个页面,正是那个页面上的某个链接将浏览器带到了当前所请求的这个页面。 | `Referer: http://en.wikipedia.org/wiki/Main_Page` |
+| TE | 浏览器预期接受的传输编码方式:可使用回应协议头 Transfer-Encoding 字段中的值; | TE: trailers, deflate |
+| Upgrade | 要求服务器升级到另一个协议。 | Upgrade: HTTP/2.0, SHTTP/1.3, IRC/6.9, RTA/x11 |
+| User-Agent | 浏览器的浏览器身份标识字符串 | User-Agent: Mozilla/5.0 (X11; Linux x86_64; rv:12.0) Gecko/20100101 Firefox/21.0 |
+| Via | 向服务器告知,这个请求是由哪些代理发出的。 | Via: 1.0 fred, 1.1 example.com (Apache/1.1) |
+| Warning | 一个一般性的警告,告知,在实体内容体中可能存在错误。 | Warning: 199 Miscellaneous warning |
+
+### ⭐️HTTP 和 HTTPS 有什么区别?(重要)

@@ -176,7 +181,7 @@ HTTP 状态码用于描述 HTTP 请求的结果,比如 2xx 就代表请求被
- **安全性和资源消耗**:HTTP 协议运行在 TCP 之上,所有传输的内容都是明文,客户端和服务器端都无法验证对方的身份。HTTPS 是运行在 SSL/TLS 之上的 HTTP 协议,SSL/TLS 运行在 TCP 之上。所有传输的内容都经过加密,加密采用对称加密,但对称加密的密钥用服务器方的证书进行了非对称加密。所以说,HTTP 安全性没有 HTTPS 高,但是 HTTPS 比 HTTP 耗费更多服务器资源。
- **SEO(搜索引擎优化)**:搜索引擎通常会更青睐使用 HTTPS 协议的网站,因为 HTTPS 能够提供更高的安全性和用户隐私保护。使用 HTTPS 协议的网站在搜索结果中可能会被优先显示,从而对 SEO 产生影响。
-关于 HTTP 和 HTTPS 更详细的对比总结,可以看我写的这篇文章:[HTTP vs HTTPS(应用层)](./http-vs-https.md) 。
+关于 HTTP 和 HTTPS 更详细的对比总结,可以看我写的这篇文章:[HTTP vs HTTPS(应用层)](https://javaguide.cn/cs-basics/network/http-vs-https.html) 。
### HTTP/1.0 和 HTTP/1.1 有什么区别?
@@ -188,14 +193,15 @@ HTTP 状态码用于描述 HTTP 请求的结果,比如 2xx 就代表请求被
- **带宽**:HTTP/1.0 中,存在一些浪费带宽的现象,例如客户端只是需要某个对象的一部分,而服务器却将整个对象送过来了,并且不支持断点续传功能,HTTP/1.1 则在请求头引入了 range 头域,它允许只请求资源的某个部分,即返回码是 206(Partial Content),这样就方便了开发者自由的选择以便于充分利用带宽和连接。
- **Host 头(Host Header)处理** :HTTP/1.1 引入了 Host 头字段,允许在同一 IP 地址上托管多个域名,从而支持虚拟主机的功能。而 HTTP/1.0 没有 Host 头字段,无法实现虚拟主机。
-关于 HTTP/1.0 和 HTTP/1.1 更详细的对比总结,可以看我写的这篇文章:[HTTP/1.0 vs HTTP/1.1(应用层)](./http1.0-vs-http1.1.md) 。
+关于 HTTP/1.0 和 HTTP/1.1 更详细的对比总结,可以看我写的这篇文章:[HTTP/1.0 vs HTTP/1.1(应用层)](https://javaguide.cn/cs-basics/network/http1.0-vs-http1.1.html) 。
-### HTTP/1.1 和 HTTP/2.0 有什么区别?
+### ⭐️HTTP/1.1 和 HTTP/2.0 有什么区别?

-- **多路复用(Multiplexing)**:HTTP/2.0 在同一连接上可以同时传输多个请求和响应(可以看作是 HTTP/1.1 中长链接的升级版本),互不干扰。HTTP/1.1 则使用串行方式,每个请求和响应都需要独立的连接,而浏览器为了控制资源会有 6-8 个 TCP 连接都限制。。这使得 HTTP/2.0 在处理多个请求时更加高效,减少了网络延迟和提高了性能。
+- **多路复用(Multiplexing)**:HTTP/2.0 在同一连接上可以同时传输多个请求和响应(可以看作是 HTTP/1.1 中长链接的升级版本),互不干扰。HTTP/1.1 则使用串行方式,每个请求和响应都需要独立的连接,而浏览器为了控制资源会有 6-8 个 TCP 连接的限制。这使得 HTTP/2.0 在处理多个请求时更加高效,减少了网络延迟和提高了性能。
- **二进制帧(Binary Frames)**:HTTP/2.0 使用二进制帧进行数据传输,而 HTTP/1.1 则使用文本格式的报文。二进制帧更加紧凑和高效,减少了传输的数据量和带宽消耗。
+- **队头阻塞**:HTTP/2 引入了多路复用技术,允许多个请求和响应在单个 TCP 连接上并行交错传输,解决了 HTTP/1.1 应用层的队头阻塞问题,但 HTTP/2 依然受到 TCP 层队头阻塞 的影响。
- **头部压缩(Header Compression)**:HTTP/1.1 支持`Body`压缩,`Header`不支持压缩。HTTP/2.0 支持对`Header`压缩,使用了专门为`Header`压缩而设计的 HPACK 算法,减少了网络开销。
- **服务器推送(Server Push)**:HTTP/2.0 支持服务器推送,可以在客户端请求一个资源时,将其他相关资源一并推送给客户端,从而减少了客户端的请求次数和延迟。而 HTTP/1.1 需要客户端自己发送请求来获取相关资源。
@@ -203,7 +209,7 @@ HTTP/2.0 多路复用效果图(图源: [HTTP/2 For Web Developers](https://b

-可以看到,HTTP/2.0 的多路复用使得不同的请求可以共用一个 TCP 连接,避免建立多个连接带来不必要的额外开销,而 HTTP/1.1 中的每个请求都会建立一个单独的连接
+可以看到,HTTP/2 的多路复用机制允许多个请求和响应共享一个 TCP 连接,从而避免了 HTTP/1.1 在应对并发请求时需要建立多个并行连接的情况,减少了重复连接建立和维护的额外开销。而在 HTTP/1.1 中,尽管支持持久连接,但为了缓解队头阻塞问题,浏览器通常会为同一域名建立多个并行连接。
### HTTP/2.0 和 HTTP/3.0 有什么区别?
@@ -211,25 +217,110 @@ HTTP/2.0 多路复用效果图(图源: [HTTP/2 For Web Developers](https://b
- **传输协议**:HTTP/2.0 是基于 TCP 协议实现的,HTTP/3.0 新增了 QUIC(Quick UDP Internet Connections) 协议来实现可靠的传输,提供与 TLS/SSL 相当的安全性,具有较低的连接和传输延迟。你可以将 QUIC 看作是 UDP 的升级版本,在其基础上新增了很多功能比如加密、重传等等。HTTP/3.0 之前名为 HTTP-over-QUIC,从这个名字中我们也可以发现,HTTP/3 最大的改造就是使用了 QUIC。
- **连接建立**:HTTP/2.0 需要经过经典的 TCP 三次握手过程(由于安全的 HTTPS 连接建立还需要 TLS 握手,共需要大约 3 个 RTT)。由于 QUIC 协议的特性(TLS 1.3,TLS 1.3 除了支持 1 个 RTT 的握手,还支持 0 个 RTT 的握手)连接建立仅需 0-RTT 或者 1-RTT。这意味着 QUIC 在最佳情况下不需要任何的额外往返时间就可以建立新连接。
+- **头部压缩**:HTTP/2.0 使用 HPACK 算法进行头部压缩,而 HTTP/3.0 使用更高效的 QPACK 头压缩算法。
- **队头阻塞**:HTTP/2.0 多请求复用一个 TCP 连接,一旦发生丢包,就会阻塞住所有的 HTTP 请求。由于 QUIC 协议的特性,HTTP/3.0 在一定程度上解决了队头阻塞(Head-of-Line blocking, 简写:HOL blocking)问题,一个连接建立多个不同的数据流,这些数据流之间独立互不影响,某个数据流发生丢包了,其数据流不受影响(本质上是多路复用+轮询)。
+- **连接迁移**:HTTP/3.0 支持连接迁移,因为 QUIC 使用 64 位 ID 标识连接,只要 ID 不变就不会中断,网络环境改变时(如从 Wi-Fi 切换到移动数据)也能保持连接。而 TCP 连接是由(源 IP,源端口,目的 IP,目的端口)组成,这个四元组中一旦有一项值发生改变,这个连接也就不能用了。
- **错误恢复**:HTTP/3.0 具有更好的错误恢复机制,当出现丢包、延迟等网络问题时,可以更快地进行恢复和重传。而 HTTP/2.0 则需要依赖于 TCP 的错误恢复机制。
-- **安全性**:HTTP/2.0 和 HTTP/3.0 在安全性上都有较高的要求,支持加密通信,但在实现上有所不同。HTTP/2.0 使用 TLS 协议进行加密,而 HTTP/3.0 基于 QUIC 协议,包含了内置的加密和身份验证机制,可以提供更强的安全性。
+- **安全性**:在 HTTP/2.0 中,TLS 用于加密和认证整个 HTTP 会话,包括所有的 HTTP 头部和数据负载。TLS 的工作是在 TCP 层之上,它加密的是在 TCP 连接中传输的应用层的数据,并不会对 TCP 头部以及 TLS 记录层头部进行加密,所以在传输的过程中 TCP 头部可能会被攻击者篡改来干扰通信。而 HTTP/3.0 的 QUIC 对整个数据包(包括报文头和报文体)进行了加密与认证处理,保障安全性。
HTTP/1.0、HTTP/2.0 和 HTTP/3.0 的协议栈比较:

+下图是一个更详细的 HTTP/2.0 和 HTTP/3.0 对比图:
+
+
+
+从上图可以看出:
+
+- **HTTP/2.0**:使用 TCP 作为传输协议、使用 HPACK 进行头部压缩、依赖 TLS 进行加密。
+- **HTTP/3.0**:使用基于 UDP 的 QUIC 协议、使用更高效的 QPACK 进行头部压缩、在 QUIC 中直接集成了 TLS。QUIC 协议具备连接迁移、拥塞控制与避免、流量控制等特性。
+
关于 HTTP/1.0 -> HTTP/3.0 更详细的演进介绍,推荐阅读[HTTP1 到 HTTP3 的工程优化](https://dbwu.tech/posts/http_evolution/)。
-### HTTP 是不保存状态的协议, 如何保存用户状态?
+### HTTP/1.1 和 HTTP/2.0 的队头阻塞有什么不同?
+
+HTTP/1.1 队头阻塞的主要原因是无法多路复用:
+
+- 在一个 TCP 连接中,资源的请求和响应是按顺序处理的。如果一个大的资源(如一个大文件)正在传输,后续的小资源(如较小的 CSS 文件)需要等待前面的资源传输完成后才能被发送。
+- 如果浏览器需要同时加载多个资源(如多个 CSS、JS 文件等),它通常会开启多个并行的 TCP 连接(一般限制为 6 个)。但每个连接仍然受限于顺序的请求-响应机制,因此仍然会发生 **应用层的队头阻塞**。
+
+虽然 HTTP/2.0 引入了多路复用技术,允许多个请求和响应在单个 TCP 连接上并行交错传输,解决了 **HTTP/1.1 应用层的队头阻塞问题**,但 HTTP/2.0 依然受到 **TCP 层队头阻塞** 的影响:
+
+- HTTP/2.0 通过帧(frame)机制将每个资源分割成小块,并为每个资源分配唯一的流 ID,这样多个资源的数据可以在同一 TCP 连接中交错传输。
+- TCP 作为传输层协议,要求数据按顺序交付。如果某个数据包在传输过程中丢失,即使后续的数据包已经到达,也必须等待丢失的数据包重传后才能继续处理。这种传输层的顺序性导致了 **TCP 层的队头阻塞**。
+- 举例来说,如果 HTTP/2 的一个 TCP 数据包中携带了多个资源的数据(例如 JS 和 CSS),而该数据包丢失了,那么后续数据包中的所有资源数据都需要等待丢失的数据包重传回来,导致所有流(streams)都被阻塞。
+
+最后,来一张表格总结补充一下:
+
+| **方面** | **HTTP/1.1 的队头阻塞** | **HTTP/2.0 的队头阻塞** |
+| -------------- | ---------------------------------------- | ---------------------------------------------------------------- |
+| **层级** | 应用层(HTTP 协议本身的限制) | 传输层(TCP 协议的限制) |
+| **根本原因** | 无法多路复用,请求和响应必须按顺序传输 | TCP 要求数据包按顺序交付,丢包时阻塞整个连接 |
+| **受影响范围** | 单个 HTTP 请求/响应会阻塞后续请求/响应。 | 单个 TCP 包丢失会影响所有 HTTP/2.0 流(依赖于同一个底层 TCP 连接) |
+| **缓解方法** | 开启多个并行的 TCP 连接 | 减少网络掉包或者使用基于 UDP 的 QUIC 协议 |
+| **影响场景** | 每次都会发生,尤其是大文件阻塞小文件时。 | 丢包率较高的网络环境下更容易发生。 |
+
+### ⭐️HTTP 是不保存状态的协议, 如何保存用户状态?
+
+HTTP 协议本身是 **无状态的 (stateless)** 。这意味着服务器默认情况下无法区分两个连续的请求是否来自同一个用户,或者同一个用户之前的操作是什么。这就像一个“健忘”的服务员,每次你跟他说话,他都不知道你是谁,也不知道你之前点过什么菜。
+
+但在实际的 Web 应用中,比如网上购物、用户登录等场景,我们显然需要记住用户的状态(例如购物车里的商品、用户的登录信息)。为了解决这个问题,主要有以下几种常用机制:
+
+**方案一:Session (会话) 配合 Cookie (主流方式):**
+
+
+
+这可以说是最经典也是最常用的方法了。基本流程是这样的:
+
+1. 用户向服务器发送用户名、密码、验证码用于登陆系统。
+2. 服务器验证通过后,会为这个用户创建一个专属的 Session 对象(可以理解为服务器上的一块内存,存放该用户的状态数据,如购物车、登录信息等)存储起来,并给这个 Session 分配一个唯一的 `SessionID`。
+3. 服务器通过 HTTP 响应头中的 `Set-Cookie` 指令,把这个 `SessionID` 发送给用户的浏览器。
+4. 浏览器接收到 `SessionID` 后,会将其以 Cookie 的形式保存在本地。当用户保持登录状态时,每次向该服务器发请求,浏览器都会自动带上这个存有 `SessionID` 的 Cookie。
+5. 服务器收到请求后,从 Cookie 中拿出 `SessionID`,就能找到之前保存的那个 Session 对象,从而知道这是哪个用户以及他之前的状态了。
+
+使用 Session 的时候需要注意下面几个点:
+
+- **客户端 Cookie 支持**:依赖 Session 的核心功能要确保用户浏览器开启了 Cookie。
+- **Session 过期管理**:合理设置 Session 的过期时间,平衡安全性和用户体验。
+- **Session ID 安全**:为包含 `SessionID` 的 Cookie 设置 `HttpOnly` 标志可以防止客户端脚本(如 JavaScript)窃取,设置 Secure 标志可以保证 `SessionID` 只在 HTTPS 连接下传输,增加安全性。
+
+Session 数据本身存储在服务器端。常见的存储方式有:
-HTTP 是一种不保存状态,即无状态(stateless)协议。也就是说 HTTP 协议自身不对请求和响应之间的通信状态进行保存。那么我们如何保存用户状态呢?Session 机制的存在就是为了解决这个问题,Session 的主要作用就是通过服务端记录用户的状态。典型的场景是购物车,当你要添加商品到购物车的时候,系统不知道是哪个用户操作的,因为 HTTP 协议是无状态的。服务端给特定的用户创建特定的 Session 之后就可以标识这个用户并且跟踪这个用户了(一般情况下,服务器会在一定时间内保存这个 Session,过了时间限制,就会销毁这个 Session)。
+- **服务器内存**:实现简单,访问速度快,但服务器重启数据会丢失,且不利于多服务器间的负载均衡。这种方式适合简单且用户量不大的业务场景。
+- **数据库 (如 MySQL, PostgreSQL)**:数据持久化,但读写性能相对较低,一般不会使用这种方式。
+- **分布式缓存 (如 Redis)**:性能高,支持分布式部署,是目前大规模应用中非常主流的方案。
-在服务端保存 Session 的方法很多,最常用的就是内存和数据库(比如是使用内存数据库 redis 保存)。既然 Session 存放在服务器端,那么我们如何实现 Session 跟踪呢?大部分情况下,我们都是通过在 Cookie 中附加一个 Session ID 来方式来跟踪。
+**方案二:当 Cookie 被禁用时:URL 重写 (URL Rewriting)**
-**Cookie 被禁用怎么办?**
+如果用户的浏览器禁用了 Cookie,或者某些情况下不便使用 Cookie,还有一种备选方案是 URL 重写。这种方式会将 `SessionID` 直接附加到 URL 的末尾,作为参数传递。例如:。服务器端会解析 URL 中的 `sessionid` 参数来获取 `SessionID`,进而找到对应的 Session 数据。
-最常用的就是利用 URL 重写把 Session ID 直接附加在 URL 路径的后面。
+这种方法一般不会使用,存在以下缺点:
+
+- URL 会变长且不美观;
+- `SessionID` 暴露在 URL 中,安全性较低(容易被复制、分享或记录在日志中);
+- 对搜索引擎优化 (SEO) 可能不友好。
+
+**方案三:Token-based 认证 (如 JWT - JSON Web Tokens)**
+
+这是一种越来越流行的无状态认证方式,尤其适用于前后端分离的架构和微服务。
+
+
+
+以 JWT 为例(普通 Token 方案也可以),简化后的步骤如下
+
+1. 用户向服务器发送用户名、密码以及验证码用于登陆系统;
+2. 如果用户用户名、密码以及验证码校验正确的话,服务端会返回已经签名的 Token,也就是 JWT;
+3. 客户端收到 Token 后自己保存起来(比如浏览器的 `localStorage` );
+4. 用户以后每次向后端发请求都在 Header 中带上这个 JWT ;
+5. 服务端检查 JWT 并从中获取用户相关信息。
+
+JWT 详细介绍可以查看这两篇文章:
+
+- [JWT 基础概念详解](https://javaguide.cn/system-design/security/jwt-intro.html)
+- [JWT 身份认证优缺点分析](https://javaguide.cn/system-design/security/advantages-and-disadvantages-of-jwt.html)
+
+总结来说,虽然 HTTP 本身是无状态的,但通过 Cookie + Session、URL 重写或 Token 等机制,我们能够有效地在 Web 应用中跟踪和管理用户状态。其中,**Cookie + Session 是最传统也最广泛使用的方式,而 Token-based 认证则在现代 Web 应用中越来越受欢迎。**
### URI 和 URL 的区别是什么?
@@ -240,9 +331,9 @@ URI 的作用像身份证号一样,URL 的作用更像家庭住址一样。URL
### Cookie 和 Session 有什么区别?
-准确点来说,这个问题属于认证授权的范畴,你可以在 [认证授权基础概念详解](../../system-design/security/basis-of-authority-certification.md) 这篇文章中找到详细的答案。
+准确点来说,这个问题属于认证授权的范畴,你可以在 [认证授权基础概念详解](https://javaguide.cn/system-design/security/basis-of-authority-certification.html) 这篇文章中找到详细的答案。
-### GET 和 POST 的区别
+### ⭐️GET 和 POST 的区别
这个问题在知乎上被讨论的挺火热的,地址: 。
@@ -279,7 +370,7 @@ WebSocket 协议本质上是应用层的协议,用于弥补 HTTP 协议在持
- 社交聊天
- ……
-### WebSocket 和 HTTP 有什么区别?
+### ⭐️WebSocket 和 HTTP 有什么区别?
WebSocket 和 HTTP 两者都是基于 TCP 的应用层协议,都可以在网络中传输数据。
@@ -301,23 +392,70 @@ WebSocket 的工作过程可以分为以下几个步骤:
另外,建立 WebSocket 连接之后,通过心跳机制来保持 WebSocket 连接的稳定性和活跃性。
-### SSE 与 WebSocket 有什么区别?
+### ⭐️WebSocket 与短轮询、长轮询的区别
+
+这三种方式,都是为了解决“**客户端如何及时获取服务器最新数据,实现实时更新**”的问题。它们的实现方式和效率、实时性差异较大。
+
+**1.短轮询(Short Polling)**
+
+- **原理**:客户端每隔固定时间(如 5 秒)发起一次 HTTP 请求,询问服务器是否有新数据。服务器收到请求后立即响应。
+- **优点**:实现简单,兼容性好,直接用常规 HTTP 请求即可。
+- **缺点**:
+ - **实时性一般**:消息可能在两次轮询间到达,用户需等到下次请求才知晓。
+ - **资源浪费大**:反复建立/关闭连接,且大多数请求收到的都是“无新消息”,极大增加服务器和网络压力。
+
+**2.长轮询(Long Polling)**
+
+- **原理**:客户端发起请求后,若服务器暂时无新数据,则会保持连接,直到有新数据或超时才响应。客户端收到响应后立即发起下一次请求,实现“伪实时”。
+- **优点**:
+ - **实时性较好**:一旦有新数据可立即推送,无需等待下次定时请求。
+ - **空响应减少**:减少了无效的空响应,提升了效率。
+- **缺点**:
+ - **服务器资源占用高**:需长时间维护大量连接,消耗服务器线程/连接数。
+ - **资源浪费大**:每次响应后仍需重新建立连接,且依然基于 HTTP 单向请求-响应机制。
+
+**3. WebSocket**
+
+- **原理**:客户端与服务器通过一次 HTTP Upgrade 握手后,建立一条持久的 TCP 连接。之后,双方可以随时、主动地发送数据,实现真正的全双工、低延迟通信。
+- **优点**:
+ - **实时性强**:数据可即时双向收发,延迟极低。
+ - **资源效率高**:连接持续,无需反复建立/关闭,减少资源消耗。
+ - **功能强大**:支持服务端主动推送消息、客户端主动发起通信。
+- **缺点**:
+ - **使用限制**:需要服务器和客户端都支持 WebSocket 协议。对连接管理有一定要求(如心跳保活、断线重连等)。
+ - **实现麻烦**:实现起来比短轮询和长轮询要更麻烦一些。
+
+
+
+### ⭐️SSE 与 WebSocket 有什么区别?
+
+SSE (Server-Sent Events) 和 WebSocket 都是用来实现服务器向浏览器实时推送消息的技术,让网页内容能自动更新,而不需要用户手动刷新。虽然目标相似,但它们在工作方式和适用场景上有几个关键区别:
-> 摘自[Web 实时消息推送详解](https://javaguide.cn/system-design/web-real-time-message-push.html)。
+1. **通信方式:**
+ - **SSE:** **单向通信**。只有服务器能向客户端(浏览器)发送数据。客户端不能通过同一个连接向服务器发送数据(需要发起新的 HTTP 请求)。
+ - **WebSocket:** **双向通信 (全双工)**。客户端和服务器可以随时互相发送消息,实现真正的实时交互。
+2. **底层协议:**
+ - **SSE:** 基于**标准的 HTTP/HTTPS 协议**。它本质上是一个“长连接”的 HTTP 请求,服务器保持连接打开并持续发送事件流。不需要特殊的服务器或协议支持,现有的 HTTP 基础设施就能用。
+ - **WebSocket:** 使用**独立的 ws:// 或 wss:// 协议**。它需要通过一个特定的 HTTP "Upgrade" 请求来建立连接,并且服务器需要明确支持 WebSocket 协议来处理连接和消息帧。
+3. **实现复杂度和成本:**
+ - **SSE:** **实现相对简单**,主要在服务器端处理。浏览器端有标准的 EventSource API,使用方便。开发和维护成本较低。
+ - **WebSocket:** **稍微复杂一些**。需要服务器端专门处理 WebSocket 连接和协议,客户端也需要使用 WebSocket API。如果需要考虑兼容性、心跳、重连等,开发成本会更高。
+4. **断线重连:**
+ - **SSE:** **浏览器原生支持**。EventSource API 提供了自动断线重连的机制。
+ - **WebSocket:** **需要手动实现**。开发者需要自己编写逻辑来检测断线并进行重连尝试。
+5. **数据类型:**
+ - **SSE:** **主要设计用来传输文本** (UTF-8 编码)。如果需要传输二进制数据,需要先进行 Base64 等编码转换成文本。
+ - **WebSocket:** **原生支持传输文本和二进制数据**,无需额外编码。
-SSE 与 WebSocket 作用相似,都可以建立服务端与浏览器之间的通信,实现服务端向客户端推送消息,但还是有些许不同:
+为了提供更好的用户体验和利用其简单、高效、基于标准 HTTP 的特性,**Server-Sent Events (SSE) 是目前大型语言模型 API(如 OpenAI、DeepSeek 等)实现流式响应的常用甚至可以说是标准的技术选择**。
-- SSE 是基于 HTTP 协议的,它们不需要特殊的协议或服务器实现即可工作;WebSocket 需单独服务器来处理协议。
-- SSE 单向通信,只能由服务端向客户端单向通信;WebSocket 全双工通信,即通信的双方可以同时发送和接受信息。
-- SSE 实现简单开发成本低,无需引入其他组件;WebSocket 传输数据需做二次解析,开发门槛高一些。
-- SSE 默认支持断线重连;WebSocket 则需要自己实现。
-- SSE 只能传送文本消息,二进制数据需要经过编码后传送;WebSocket 默认支持传送二进制数据。
+这里以 DeepSeek 为例,我们发送一个请求并打开浏览器控制台验证一下:
-**SSE 与 WebSocket 该如何选择?**
+
-SSE 好像一直不被大家所熟知,一部分原因是出现了 WebSocket,这个提供了更丰富的协议来执行双向、全双工通信。对于游戏、即时通信以及需要双向近乎实时更新的场景,拥有双向通道更具吸引力。
+
-但是,在某些情况下,不需要从客户端发送数据。而你只需要一些服务器操作的更新。比如:站内信、未读消息数、状态更新、股票行情、监控数量等场景,SSE 不管是从实现的难易和成本上都更加有优势。此外,SSE 具有 WebSocket 在设计上缺乏的多种功能,例如:自动重新连接、事件 ID 和发送任意事件的能力。
+可以看到,响应头应里包含了 `text/event-stream`,说明使用的确实是 SSE。并且,响应数据也确实是持续分块传输。
## PING
@@ -386,15 +524,15 @@ DNS 服务器自底向上可以依次分为以下几个层级(所有 DNS 服务
- 权威 DNS 服务器。在因特网上具有公共可访问主机的每个组织机构必须提供公共可访问的 DNS 记录,这些记录将这些主机的名字映射为 IP 地址。
- 本地 DNS 服务器。每个 ISP(互联网服务提供商)都有一个自己的本地 DNS 服务器。当主机发出 DNS 请求时,该请求被发往本地 DNS 服务器,它起着代理的作用,并将该请求转发到 DNS 层次结构中。严格说来,不属于 DNS 层级结构
-世界上并不是只有 13 台根服务器,这是很多人普遍的误解,网上很多文章也是这么写的。实际上,现在根服务器数量远远超过这个数量。最初确实是为 DNS 根服务器分配了 13 个 IP 地址,每个 IP 地址对应一个不同的根 DNS 服务器。然而,由于互联网的快速发展和增长,这个原始的架构变得不太适应当前的需求。为了提高 DNS 的可靠性、安全性和性能,目前这 13 个 IP 地址中的每一个都有多个服务器,截止到 2023 年底,所有根服务器之和达到了 600 多台,未来还会继续增加。
+世界上并不是只有 13 台根服务器,这是很多人普遍的误解,网上很多文章也是这么写的。实际上,现在根服务器数量远远超过这个数量。最初确实是为 DNS 根服务器分配了 13 个 IP 地址,每个 IP 地址对应一个不同的根 DNS 服务器。然而,由于互联网的快速发展和增长,这个原始的架构变得不太适应当前的需求。为了提高 DNS 的可靠性、安全性和性能,目前这 13 个 IP 地址中的每一个都有多个服务器,截止到 2023 年底,所有根服务器之和达到了 1700 多台,未来还会继续增加。
-### DNS 解析的过程是什么样的?
+### ⭐️DNS 解析的过程是什么样的?
-整个过程的步骤比较多,我单独写了一篇文章详细介绍:[DNS 域名系统详解(应用层)](./dns.md) 。
+整个过程的步骤比较多,我单独写了一篇文章详细介绍:[DNS 域名系统详解(应用层)](https://javaguide.cn/cs-basics/network/dns.html) 。
### DNS 劫持了解吗?如何应对?
-DNS 劫持是一种网络攻击,它通过修改 DNS 服务器的解析结果,使用户访问的域名指向错误的 IP 地址,从而导致用户无法访问正常的网站,或者被引导到恶意的网站。DNS 劫持有时也被称为 DNS 重定向、DNS 欺骗或 DNS 污染。DNS 劫持详细介绍可以参考:[黑客技术?没你想象的那么难!——DNS 劫持篇](https://cloud.tencent.com/developer/article/1197474)。
+DNS 劫持是一种网络攻击,它通过修改 DNS 服务器的解析结果,使用户访问的域名指向错误的 IP 地址,从而导致用户无法访问正常的网站,或者被引导到恶意的网站。DNS 劫持有时也被称为 DNS 重定向、DNS 欺骗或 DNS 污染。
## 参考
diff --git a/docs/cs-basics/network/other-network-questions2.md b/docs/cs-basics/network/other-network-questions2.md
index d5054865497..0a75cd7d0f8 100644
--- a/docs/cs-basics/network/other-network-questions2.md
+++ b/docs/cs-basics/network/other-network-questions2.md
@@ -1,41 +1,78 @@
---
title: 计算机网络常见面试题总结(下)
+description: 最新计算机网络高频面试题总结(下):TCP/UDP深度对比、三次握手四次挥手、HTTP/3 QUIC优化、IPv6优势、NAT/ARP详解,附表格+⭐️重点标注,一文掌握传输层&网络层核心考点,快速通关后端技术面试!
category: 计算机基础
tag:
- 计算机网络
+head:
+ - - meta
+ - name: keywords
+ content: 计算机网络面试题,TCP vs UDP,TCP三次握手,HTTP/3 QUIC,IPv4 vs IPv6,TCP可靠性,IP地址,NAT协议,ARP协议,传输层面试,网络层高频题,基于TCP协议,基于UDP协议,队头阻塞,四次挥手
---
+
+
下篇主要是传输层和网络层相关的内容。
## TCP 与 UDP
-### TCP 与 UDP 的区别(重要)
-
-1. **是否面向连接**:UDP 在传送数据之前不需要先建立连接。而 TCP 提供面向连接的服务,在传送数据之前必须先建立连接,数据传送结束后要释放连接。
-2. **是否是可靠传输**:远地主机在收到 UDP 报文后,不需要给出任何确认,并且不保证数据不丢失,不保证是否顺序到达。TCP 提供可靠的传输服务,TCP 在传递数据之前,会有三次握手来建立连接,而且在数据传递时,有确认、窗口、重传、拥塞控制机制。通过 TCP 连接传输的数据,无差错、不丢失、不重复、并且按序到达。
-3. **是否有状态**:这个和上面的“是否可靠传输”相对应。TCP 传输是有状态的,这个有状态说的是 TCP 会去记录自己发送消息的状态比如消息是否发送了、是否被接收了等等。为此 ,TCP 需要维持复杂的连接状态表。而 UDP 是无状态服务,简单来说就是不管发出去之后的事情了(**这很渣男!**)。
-4. **传输效率**:由于使用 TCP 进行传输的时候多了连接、确认、重传等机制,所以 TCP 的传输效率要比 UDP 低很多。
-5. **传输形式**:TCP 是面向字节流的,UDP 是面向报文的。
-6. **首部开销**:TCP 首部开销(20 ~ 60 字节)比 UDP 首部开销(8 字节)要大。
-7. **是否提供广播或多播服务**:TCP 只支持点对点通信,UDP 支持一对一、一对多、多对一、多对多;
+### ⭐️TCP 与 UDP 的区别(重要)
+
+1. **是否面向连接**:
+ - TCP 是面向连接的。在传输数据之前,必须先通过“三次握手”建立连接;数据传输完成后,还需要通过“四次挥手”来释放连接。这保证了双方都准备好通信。
+ - UDP 是无连接的。发送数据前不需要建立任何连接,直接把数据包(数据报)扔出去。
+2. **是否是可靠传输**:
+ - TCP 提供可靠的数据传输服务。它通过序列号、确认应答 (ACK)、超时重传、流量控制、拥塞控制等一系列机制,来确保数据能够无差错、不丢失、不重复且按顺序地到达目的地。
+ - UDP 提供不可靠的传输。它尽最大努力交付 (best-effort delivery),但不保证数据一定能到达,也不保证到达的顺序,更不会自动重传。收到报文后,接收方也不会主动发确认。
+3. **是否有状态**:
+ - TCP 是有状态的。因为要保证可靠性,TCP 需要在连接的两端维护连接状态信息,比如序列号、窗口大小、哪些数据发出去了、哪些收到了确认等。
+ - UDP 是无状态的。它不维护连接状态,发送方发出数据后就不再关心它是否到达以及如何到达,因此开销更小(**这很“渣男”!**)。
+4. **传输效率**:
+ - TCP 因为需要建立连接、发送确认、处理重传等,其开销较大,传输效率相对较低。
+ - UDP 结构简单,没有复杂的控制机制,开销小,传输效率更高,速度更快。
+5. **传输形式**:
+ - TCP 是面向字节流 (Byte Stream) 的。它将应用程序交付的数据视为一连串无结构的字节流,可能会对数据进行拆分或合并。
+ - UDP 是面向报文 (Message Oriented) 的。应用程序交给 UDP 多大的数据块,UDP 就照样发送,既不拆分也不合并,保留了应用程序消息的边界。
+6. **首部开销**:
+ - TCP 的头部至少需要 20 字节,如果包含选项字段,最多可达 60 字节。
+ - UDP 的头部非常简单,固定只有 8 字节。
+7. **是否提供广播或多播服务**:
+ - TCP 只支持点对点 (Point-to-Point) 的单播通信。
+ - UDP 支持一对一 (单播)、一对多 (多播/Multicast) 和一对所有 (广播/Broadcast) 的通信方式。
8. ……
-我把上面总结的内容通过表格形式展示出来了!确定不点个赞嘛?
+为了更直观地对比,可以看下面这个表格:
+
+| 特性 | TCP | UDP |
+| ------------ | -------------------------- | ----------------------------------- |
+| **连接性** | 面向连接 | 无连接 |
+| **可靠性** | 可靠 | 不可靠 (尽力而为) |
+| **状态维护** | 有状态 | 无状态 |
+| **传输效率** | 较低 | 较高 |
+| **传输形式** | 面向字节流 | 面向数据报 (报文) |
+| **头部开销** | 20 - 60 字节 | 8 字节 |
+| **通信模式** | 点对点 (单播) | 单播、多播、广播 |
+| **常见应用** | HTTP/HTTPS, FTP, SMTP, SSH | DNS, DHCP, SNMP, TFTP, VoIP, 视频流 |
-| | TCP | UDP |
-| ---------------------- | -------------- | ---------- |
-| 是否面向连接 | 是 | 否 |
-| 是否可靠 | 是 | 否 |
-| 是否有状态 | 是 | 否 |
-| 传输效率 | 较慢 | 较快 |
-| 传输形式 | 字节流 | 数据报文段 |
-| 首部开销 | 20 ~ 60 bytes | 8 bytes |
-| 是否提供广播或多播服务 | 否 | 是 |
+### ⭐️什么时候选择 TCP,什么时候选 UDP?
-### 什么时候选择 TCP,什么时候选 UDP?
+选择 TCP 还是 UDP,主要取决于你的应用**对数据传输的可靠性要求有多高,以及对实时性和效率的要求有多高**。
-- **UDP 一般用于即时通信**,比如:语音、 视频、直播等等。这些场景对传输数据的准确性要求不是特别高,比如你看视频即使少个一两帧,实际给人的感觉区别也不大。
-- **TCP 用于对传输准确性要求特别高的场景**,比如文件传输、发送和接收邮件、远程登录等等。
+当**数据准确性和完整性至关重要,一点都不能出错**时,通常选择 TCP。因为 TCP 提供了一整套机制(三次握手、确认应答、重传、流量控制等)来保证数据能够可靠、有序地送达。典型应用场景如下:
+
+- **Web 浏览 (HTTP/HTTPS):** 网页内容、图片、脚本必须完整加载才能正确显示。
+- **文件传输 (FTP, SCP):** 文件内容不允许有任何字节丢失或错序。
+- **邮件收发 (SMTP, POP3, IMAP):** 邮件内容需要完整无误地送达。
+- **远程登录 (SSH, Telnet):** 命令和响应需要准确传输。
+- ……
+
+当**实时性、速度和效率优先,并且应用能容忍少量数据丢失或乱序**时,通常选择 UDP。UDP 开销小、传输快,没有建立连接和保证可靠性的复杂过程。典型应用场景如下:
+
+- **实时音视频通信 (VoIP, 视频会议, 直播):** 偶尔丢失一两个数据包(可能导致画面或声音短暂卡顿)通常比因为等待重传(TCP 机制)导致长时间延迟更可接受。应用层可能会有自己的补偿机制。
+- **在线游戏:** 需要快速传输玩家位置、状态等信息,对实时性要求极高,旧的数据很快就没用了,丢失少量数据影响通常不大。
+- **DHCP (动态主机配置协议):** 客户端在请求 IP 时自身没有 IP 地址,无法满足 TCP 建立连接的前提条件,并且 DHCP 有广播需求、交互模式简单以及自带可靠性机制。
+- **物联网 (IoT) 数据上报:** 某些场景下,传感器定期上报数据,丢失个别数据点可能不影响整体趋势分析。
+- ……
### HTTP 基于 TCP 还是 UDP?
@@ -43,9 +80,21 @@ tag:
🐛 修正(参见 [issue#1915](https://github.com/Snailclimb/JavaGuide/issues/1915)):
-HTTP/3.0 之前是基于 TCP 协议的,而 HTTP/3.0 将弃用 TCP,改用 **基于 UDP 的 QUIC 协议** 。
+HTTP/3.0 之前是基于 TCP 协议的,而 HTTP/3.0 将弃用 TCP,改用 **基于 UDP 的 QUIC 协议** :
+
+- **HTTP/1.x 和 HTTP/2.0**:这两个版本的 HTTP 协议都明确建立在 TCP 之上。TCP 提供了可靠的、面向连接的传输,确保数据按序、无差错地到达,这对于网页内容的正确展示非常重要。发送 HTTP 请求前,需要先通过 TCP 的三次握手建立连接。
+- **HTTP/3.0**:这是一个重大的改变。HTTP/3 弃用了 TCP,转而使用 QUIC 协议,而 QUIC 是构建在 UDP 之上的。
-此变化解决了 HTTP/2 中存在的队头阻塞问题。队头阻塞是指在 HTTP/2.0 中,多个 HTTP 请求和响应共享一个 TCP 连接,如果其中一个请求或响应因为网络拥塞或丢包而被阻塞,那么后续的请求或响应也无法发送,导致整个连接的效率降低。这是由于 HTTP/2.0 在单个 TCP 连接上使用了多路复用,受到 TCP 拥塞控制的影响,少量的丢包就可能导致整个 TCP 连接上的所有流被阻塞。HTTP/3.0 在一定程度上解决了队头阻塞问题,一个连接建立多个不同的数据流,这些数据流之间独立互不影响,某个数据流发生丢包了,其数据流不受影响(本质上是多路复用+轮询)。
+
+
+**为什么 HTTP/3 要做这个改变呢?主要有两大原因:**
+
+1. 解决队头阻塞 (Head-of-Line Blocking,简写:HOL blocking) 问题。
+2. 减少连接建立的延迟。
+
+下面我们来详细介绍这两大优化。
+
+在 HTTP/2 中,虽然可以在一个 TCP 连接上并发传输多个请求/响应流(多路复用),但 TCP 本身的特性(保证有序、可靠)意味着如果其中一个流的某个 TCP 报文丢失或延迟,整个 TCP 连接都会被阻塞,等待该报文重传。这会导致所有在这个 TCP 连接上的 HTTP/2 流都受到影响,即使其他流的数据包已经到达。**QUIC (运行在 UDP 上) 解决了这个问题**。QUIC 内部实现了自己的多路复用和流控制机制。不同的 HTTP 请求/响应流在 QUIC 层面是真正独立的。如果一个流的数据包丢失,它只会阻塞该流,而不会影响同一 QUIC 连接上的其他流(本质上是多路复用+轮询),大大提高了并发传输的效率。
除了解决队头阻塞问题,HTTP/3.0 还可以减少握手过程的延迟。在 HTTP/2.0 中,如果要建立一个安全的 HTTPS 连接,需要经过 TCP 三次握手和 TLS 握手:
@@ -59,27 +108,42 @@ HTTP/3.0 之前是基于 TCP 协议的,而 HTTP/3.0 将弃用 TCP,改用 **
-
-
-### 使用 TCP 的协议有哪些?使用 UDP 的协议有哪些?
+### 你知道哪些基于 TCP/UDP 的协议?
-**运行于 TCP 协议之上的协议**:
+TCP (传输控制协议) 和 UDP (用户数据报协议) 是互联网传输层的两大核心协议,它们为各种应用层协议提供了基础的通信服务。以下是一些常见的、分别构建在 TCP 和 UDP 之上的应用层协议:
-1. **HTTP 协议(HTTP/3.0 之前)**:超文本传输协议(HTTP,HyperText Transfer Protocol)是一种用于传输超文本和多媒体内容的协议,主要是为 Web 浏览器与 Web 服务器之间的通信而设计的。当我们使用浏览器浏览网页的时候,我们网页就是通过 HTTP 请求进行加载的。
-2. **HTTPS 协议**:更安全的超文本传输协议(HTTPS,Hypertext Transfer Protocol Secure),身披 SSL 外衣的 HTTP 协议
-3. **FTP 协议**:文件传输协议 FTP(File Transfer Protocol)是一种用于在计算机之间传输文件的协议,可以屏蔽操作系统和文件存储方式。注意 ⚠️:FTP 是一种不安全的协议,因为它在传输过程中不会对数据进行加密。建议在传输敏感数据时使用更安全的协议,如 SFTP。
-4. **SMTP 协议**:简单邮件传输协议(SMTP,Simple Mail Transfer Protocol)的缩写,是一种用于发送电子邮件的协议。注意 ⚠️:SMTP 协议只负责邮件的发送,而不是接收。要从邮件服务器接收邮件,需要使用 POP3 或 IMAP 协议。
-5. **POP3/IMAP 协议**:两者都是负责邮件接收的协议。IMAP 协议是比 POP3 更新的协议,它在功能和性能上都更加强大。IMAP 支持邮件搜索、标记、分类、归档等高级功能,而且可以在多个设备之间同步邮件状态。几乎所有现代电子邮件客户端和服务器都支持 IMAP。
-6. **Telnet 协议**:用于通过一个终端登陆到其他服务器。Telnet 协议的最大缺点之一是所有数据(包括用户名和密码)均以明文形式发送,这有潜在的安全风险。这就是为什么如今很少使用 Telnet,而是使用一种称为 SSH 的非常安全的网络传输协议的主要原因。
-7. **SSH 协议** : SSH( Secure Shell)是目前较可靠,专为远程登录会话和其他网络服务提供安全性的协议。利用 SSH 协议可以有效防止远程管理过程中的信息泄露问题。SSH 建立在可靠的传输协议 TCP 之上。
-8. ……
+**运行于 TCP 协议之上的协议 (强调可靠、有序传输):**
+
+| 中文全称 (缩写) | 英文全称 | 主要用途 | 说明与特性 |
+| -------------------------- | ---------------------------------- | ---------------------------- | --------------------------------------------------------------------------------------------------------------------------- |
+| 超文本传输协议 (HTTP) | HyperText Transfer Protocol | 传输网页、超文本、多媒体内容 | **HTTP/1.x 和 HTTP/2 基于 TCP**。早期版本不加密,是 Web 通信的基础。 |
+| 安全超文本传输协议 (HTTPS) | HyperText Transfer Protocol Secure | 加密的网页传输 | 在 HTTP 和 TCP 之间增加了 SSL/TLS 加密层,确保数据传输的机密性和完整性。 |
+| 文件传输协议 (FTP) | File Transfer Protocol | 文件传输 | 传统的 FTP **明文传输**,不安全。推荐使用其安全版本 **SFTP (SSH File Transfer Protocol)** 或 **FTPS (FTP over SSL/TLS)** 。 |
+| 简单邮件传输协议 (SMTP) | Simple Mail Transfer Protocol | **发送**电子邮件 | 负责将邮件从客户端发送到服务器,或在邮件服务器之间传递。可通过 **STARTTLS** 升级到加密传输。 |
+| 邮局协议第 3 版 (POP3) | Post Office Protocol version 3 | **接收**电子邮件 | 通常将邮件从服务器**下载到本地设备后删除服务器副本** (可配置保留)。**POP3S** 是其 SSL/TLS 加密版本。 |
+| 互联网消息访问协议 (IMAP) | Internet Message Access Protocol | **接收和管理**电子邮件 | 邮件保留在服务器,支持多设备同步邮件状态、文件夹管理、在线搜索等。**IMAPS** 是其 SSL/TLS 加密版本。现代邮件服务首选。 |
+| 远程终端协议 (Telnet) | Teletype Network | 远程终端登录 | **明文传输**所有数据 (包括密码),安全性极差,基本已被 SSH 完全替代。 |
+| 安全外壳协议 (SSH) | Secure Shell | 安全远程管理、加密数据传输 | 提供了加密的远程登录和命令执行,以及安全的文件传输 (SFTP) 等功能,是 Telnet 的安全替代品。 |
+
+**运行于 UDP 协议之上的协议 (强调快速、低开销传输):**
+
+| 中文全称 (缩写) | 英文全称 | 主要用途 | 说明与特性 |
+| ----------------------- | ------------------------------------- | -------------------------- | ------------------------------------------------------------------------------------------------------------ |
+| 超文本传输协议 (HTTP/3) | HyperText Transfer Protocol version 3 | 新一代网页传输 | 基于 **QUIC** 协议 (QUIC 本身构建于 UDP 之上),旨在减少延迟、解决 TCP 队头阻塞问题,支持 0-RTT 连接建立。 |
+| 动态主机配置协议 (DHCP) | Dynamic Host Configuration Protocol | 动态分配 IP 地址及网络配置 | 客户端从服务器自动获取 IP 地址、子网掩码、网关、DNS 服务器等信息。 |
+| 域名系统 (DNS) | Domain Name System | 域名到 IP 地址的解析 | **通常使用 UDP** 进行快速查询。当响应数据包过大或进行区域传送 (AXFR) 时,会**切换到 TCP** 以保证数据完整性。 |
+| 实时传输协议 (RTP) | Real-time Transport Protocol | 实时音视频数据流传输 | 常用于 VoIP、视频会议、直播等。追求低延迟,允许少量丢包。通常与 RTCP 配合使用。 |
+| RTP 控制协议 (RTCP) | RTP Control Protocol | RTP 流的质量监控和控制信息 | 配合 RTP 工作,提供丢包、延迟、抖动等统计信息,辅助流量控制和拥塞管理。 |
+| 简单文件传输协议 (TFTP) | Trivial File Transfer Protocol | 简化的文件传输 | 功能简单,常用于局域网内无盘工作站启动、网络设备固件升级等小文件传输场景。 |
+| 简单网络管理协议 (SNMP) | Simple Network Management Protocol | 网络设备的监控与管理 | 允许网络管理员查询和修改网络设备的状态信息。 |
+| 网络时间协议 (NTP) | Network Time Protocol | 同步计算机时钟 | 用于在网络中的计算机之间同步时间,确保时间的一致性。 |
-**运行于 UDP 协议之上的协议**:
+**总结一下:**
-1. **HTTP 协议(HTTP/3.0 )**: HTTP/3.0 弃用 TCP,改用基于 UDP 的 QUIC 协议 。
-2. **DHCP 协议**:动态主机配置协议,动态配置 IP 地址
-3. **DNS**:域名系统(DNS,Domain Name System)将人类可读的域名 (例如,www.baidu.com) 转换为机器可读的 IP 地址 (例如,220.181.38.148)。 我们可以将其理解为专为互联网设计的电话薄。实际上,DNS 同时支持 UDP 和 TCP 协议。
-4. ……
+- **TCP** 更适合那些对数据**可靠性、完整性和顺序性**要求高的应用,如网页浏览 (HTTP/HTTPS)、文件传输 (FTP/SFTP)、邮件收发 (SMTP/POP3/IMAP)。
+- **UDP** 则更适用于那些对**实时性要求高、能容忍少量数据丢失**的应用,如域名解析 (DNS)、实时音视频 (RTP)、在线游戏、网络管理 (SNMP) 等。
-### TCP 三次握手和四次挥手(非常重要)
+### ⭐️TCP 三次握手和四次挥手(非常重要)
**相关面试题**:
@@ -90,11 +154,11 @@ HTTP/3.0 之前是基于 TCP 协议的,而 HTTP/3.0 将弃用 TCP,改用 **
- 如果第二次挥手时服务器的 ACK 没有送达客户端,会怎样?
- 为什么第四次挥手客户端需要等待 2\*MSL(报文段最长寿命)时间后才进入 CLOSED 状态?
-**参考答案**:[TCP 三次握手和四次挥手(传输层)](./tcp-connection-and-disconnection.md) 。
+**参考答案**:[TCP 三次握手和四次挥手(传输层)](https://javaguide.cn/cs-basics/network/tcp-connection-and-disconnection.html) 。
-### TCP 如何保证传输的可靠性?(重要)
+### ⭐️TCP 如何保证传输的可靠性?(重要)
-[TCP 传输可靠性保障(传输层)](./tcp-reliability-guarantee.md)
+[TCP 传输可靠性保障(传输层)](https://javaguide.cn/cs-basics/network/tcp-reliability-guarantee.html)
## IP
@@ -122,7 +186,7 @@ HTTP/3.0 之前是基于 TCP 协议的,而 HTTP/3.0 将弃用 TCP,改用 **
IP 地址过滤是一种简单的网络安全措施,实际应用中一般会结合其他网络安全措施,如认证、授权、加密等一起使用。单独使用 IP 地址过滤并不能完全保证网络的安全。
-### IPv4 和 IPv6 有什么区别?
+### ⭐️IPv4 和 IPv6 有什么区别?
**IPv4(Internet Protocol version 4)** 是目前广泛使用的 IP 地址版本,其格式是四组由点分隔的数字,例如:123.89.46.72。IPv4 使用 32 位地址作为其 Internet 地址,这意味着共有约 42 亿( 2^32)个可用 IP 地址。
@@ -167,7 +231,7 @@ NAT 不光可以缓解 IPv4 地址资源短缺的问题,还可以隐藏内部

-相关阅读:[NAT 协议详解(网络层)](./nat.md)。
+相关阅读:[NAT 协议详解(网络层)](https://javaguide.cn/cs-basics/network/nat.html)。
## ARP
@@ -175,25 +239,25 @@ NAT 不光可以缓解 IPv4 地址资源短缺的问题,还可以隐藏内部
MAC 地址的全称是 **媒体访问控制地址(Media Access Control Address)**。如果说,互联网中每一个资源都由 IP 地址唯一标识(IP 协议内容),那么一切网络设备都由 MAC 地址唯一标识。
-
+
可以理解为,MAC 地址是一个网络设备真正的身份证号,IP 地址只是一种不重复的定位方式(比如说住在某省某市某街道的张三,这种逻辑定位是 IP 地址,他的身份证号才是他的 MAC 地址),也可以理解为 MAC 地址是身份证号,IP 地址是邮政地址。MAC 地址也有一些别称,如 LAN 地址、物理地址、以太网地址等。
> 还有一点要知道的是,不仅仅是网络资源才有 IP 地址,网络设备也有 IP 地址,比如路由器。但从结构上说,路由器等网络设备的作用是组成一个网络,而且通常是内网,所以它们使用的 IP 地址通常是内网 IP,内网的设备在与内网以外的设备进行通信时,需要用到 NAT 协议。
-MAC 地址的长度为 6 字节(48 比特),地址空间大小有 280 万亿之多($2^{48}$),MAC 地址由 IEEE 统一管理与分配,理论上,一个网络设备中的网卡上的 MAC 地址是永久的。不同的网卡生产商从 IEEE 那里购买自己的 MAC 地址空间(MAC 的前 24 比特),也就是前 24 比特由 IEEE 统一管理,保证不会重复。而后 24 比特,由各家生产商自己管理,同样保证生产的两块网卡的 MAC 地址不会重复。
+MAC 地址的长度为 6 字节(48 比特),地址空间大小有 280 万亿之多( $2^{48}$ ),MAC 地址由 IEEE 统一管理与分配,理论上,一个网络设备中的网卡上的 MAC 地址是永久的。不同的网卡生产商从 IEEE 那里购买自己的 MAC 地址空间(MAC 的前 24 比特),也就是前 24 比特由 IEEE 统一管理,保证不会重复。而后 24 比特,由各家生产商自己管理,同样保证生产的两块网卡的 MAC 地址不会重复。
MAC 地址具有可携带性、永久性,身份证号永久地标识一个人的身份,不论他到哪里都不会改变。而 IP 地址不具有这些性质,当一台设备更换了网络,它的 IP 地址也就可能发生改变,也就是它在互联网中的定位发生了变化。
最后,记住,MAC 地址有一个特殊地址:FF-FF-FF-FF-FF-FF(全 1 地址),该地址表示广播地址。
-### ARP 协议解决了什么问题?
+### ⭐️ARP 协议解决了什么问题?
ARP 协议,全称 **地址解析协议(Address Resolution Protocol)**,它解决的是网络层地址和链路层地址之间的转换问题。因为一个 IP 数据报在物理上传输的过程中,总是需要知道下一跳(物理上的下一个目的地)该去往何处,但 IP 地址属于逻辑地址,而 MAC 地址才是物理地址,ARP 协议解决了 IP 地址转 MAC 地址的一些问题。
### ARP 协议的工作原理?
-[ARP 协议详解(网络层)](./arp.md)
+[ARP 协议详解(网络层)](https://javaguide.cn/cs-basics/network/arp.html)
## 复习建议
diff --git a/docs/cs-basics/network/tcp-connection-and-disconnection.md b/docs/cs-basics/network/tcp-connection-and-disconnection.md
index 63bc97f82c9..b60e69075a2 100644
--- a/docs/cs-basics/network/tcp-connection-and-disconnection.md
+++ b/docs/cs-basics/network/tcp-connection-and-disconnection.md
@@ -1,11 +1,16 @@
---
title: TCP 三次握手和四次挥手(传输层)
+description: 一文讲清 TCP 三次握手与四次挥手:SEQ/ACK/SYN/FIN 如何同步,TIME_WAIT 与 2MSL 的原因,半连接队列(SYN Queue)与全连接队列(Accept Queue)的工作机制,以及 backlog/somaxconn/syncookies 在高并发与 SYN Flood 下的影响。
category: 计算机基础
tag:
- 计算机网络
+head:
+ - - meta
+ - name: keywords
+ content: TCP,三次握手,四次挥手,三次握手为什么,四次挥手为什么,TIME_WAIT,CLOSE_WAIT,2MSL,状态机,SEQ,ACK,SYN,FIN,RST,半连接队列,全连接队列,SYN队列,Accept队列,backlog,somaxconn,SYN Flood,syncookies
---
-为了准确无误地把数据送达目标处,TCP 协议采用了三次握手策略。
+TCP(Transmission Control Protocol)是一种**面向连接**、**可靠**的传输层协议。所谓“可靠”,通常体现在:按序交付、差错检测、丢包重传、流量控制与拥塞控制等。为了在不可靠的网络之上建立一条逻辑可靠的端到端连接,TCP 在传输数据前必须先完成连接建立过程,即 **三次握手(Three-way Handshake)**。
## 建立连接-TCP 三次握手
@@ -13,36 +18,148 @@ tag:
建立一个 TCP 连接需要“三次握手”,缺一不可:
-- **一次握手**:客户端发送带有 SYN(SEQ=x) 标志的数据包 -> 服务端,然后客户端进入 **SYN_SEND** 状态,等待服务端的确认;
-- **二次握手**:服务端发送带有 SYN+ACK(SEQ=y,ACK=x+1) 标志的数据包 –> 客户端,然后服务端进入 **SYN_RECV** 状态;
-- **三次握手**:客户端发送带有 ACK(ACK=y+1) 标志的数据包 –> 服务端,然后客户端和服务端都进入**ESTABLISHED** 状态,完成 TCP 三次握手。
+1. **第一次握手 (SYN)**: 客户端向服务端发送一个 SYN(Synchronize Sequence Numbers)报文段,其中包含一个由客户端随机生成的初始序列号(Initial Sequence Number, ISN),例如 seq=x。发送后,客户端进入 **SYN_SENT** 状态,等待服务端的确认。
+2. **第二次握手 (SYN+ACK)**: 服务端收到 SYN 报文段后,如果同意建立连接,会向客户端回复一个确认报文段。该报文段包含两个关键信息:
+ - **SYN**:服务端也需要同步自己的初始序列号,因此报文段中也包含一个由服务端随机生成的初始序列号,例如 seq=y。
+ - **ACK** (Acknowledgement):用于确认收到了客户端的请求。其确认号被设置为客户端初始序列号加一,即 ack=x+1。
+ - 发送该报文段后,服务端进入 **SYN_RCVD** (也称 SYN_RECV)状态。
+3. **第三次握手 (ACK)**: 客户端收到服务端的 SYN+ACK 报文段后,会向服务端发送一个最终的确认报文段。该报文段包含确认号 ack=y+1。发送后,客户端进入 **ESTABLISHED** 状态。服务端收到这个 ACK 报文段后,也进入 **ESTABLISHED** 状态。
-当建立了 3 次握手之后,客户端和服务端就可以传输数据啦!
+至此,双方都确认了连接的建立,TCP 连接成功创建,可以开始进行双向数据传输。
### 什么是半连接队列和全连接队列?
-在 TCP 三次握手过程中,Linux 内核会维护两个队列来管理连接请求:
+```mermaid
+sequenceDiagram
+ autonumber
+ participant C as 客户端 Client
+ participant K as 服务端内核 TCP
+ box 服务端内核队列
+ participant SQ as 半连接队列 SYN queue
+ participant AQ as 全连接队列 Accept queue
+ end
+ participant App as 用户态应用 Server app
-1. **半连接队列**(也称 SYN Queue):当服务端收到客户端的 SYN 请求时,此时双方还没有完全建立连接,它会把半连接状态的连接放在半连接队列。
-2. **全连接队列**(也称 Accept Queue):当服务端收到客户端对 ACK 响应时,意味着三次握手成功完成,服务端会将该连接从半连接队列移动到全连接队列。如果未收到客户端的 ACK 响应,会进行重传,重传的等待时间通常是指数增长的。如果重传次数超过系统规定的最大重传次数,系统将从半连接队列中删除该连接信息。
+ C->>K: SYN
+ K-->>C: SYN 加 ACK
+ Note over SQ: 内核为该连接创建请求条目 连接状态 SYN_RCVD 放入 SYN queue
-这两个队列的存在是为了处理并发连接请求,确保服务端能够有效地管理新的连接请求。另外,新的连接请求被拒绝或忽略除了和每个队列的大小限制有关系之外,还和很多其他因素有关系,这里就不详细介绍了,整体逻辑比较复杂。
+ C->>K: ACK 第三次握手
+ Note over SQ,AQ: 内核收到 ACK 后完成握手 将连接从 SYN queue 迁移到 Accept queue 队列未满才可进入
+ Note over AQ: 连接已完成 可被 accept 连接状态 ESTABLISHED
+
+ App->>K: accept
+ K-->>App: 返回已就绪的 socket
+ Note over AQ: 该连接从 Accept queue 移除
+```
+
+在 TCP 三次握手过程中,服务端内核通常会用两个队列来管理连接请求(不同操作系统/内核版本实现细节可能略有差异,下面以常见 Linux 行为为例):
+
+1. **半连接队列**(也称 SYN Queue):
+ - 保存“握手未完成”的请求:服务端收到 SYN 并回 SYN+ACK 后,连接进入 SYN_RCVD,等待客户端最终 ACK。
+ - 如果一直收不到 ACK,内核会按重传策略重发 SYN+ACK,最终超时清理。
+ - 常见相关参数:`net.ipv4.tcp_max_syn_backlog`;在 SYN Flood 场景下可配合 `net.ipv4.tcp_syncookies`。
+2. **全连接队列**(也称 Accept Queue):
+ - 保存“握手已完成但应用还没 accept”的连接:服务端收到最终 ACK 后连接变为 `ESTABLISHED`,并进入 全连接队列,等待应用层 `accept()` 取走。
+ - 队列容量受 `listen(fd, backlog)` 与系统上限 `net.core.somaxconn` 共同影响;实践中常见有效上限近似为 `min(backlog, somaxconn)`(具体行为与内核版本相关)。
+
+总结:
+
+| 队列 | 作用 | 状态 | 移出条件 |
+| -------------------------- | ------------------ | ----------- | ----------------------- |
+| 半连接队列(SYN Queue) | 保存未完成握手连接 | SYN_RCVD | 收到 ACK / 超时重传失败 |
+| 全连接队列(Accept Queue) | 保存已完成握手连接 | ESTABLISHED | 被应用层 accept() 取出 |
+
+当全连接队列满时,`net.ipv4.tcp_abort_on_overflow` 会影响处理策略:
+
+- `0`(默认):通常不会立刻让连接快速失败,给应用留缓冲时间(可能表现为客户端重试/超时)。
+- `1`:直接对客户端回复 `RST`,让连接快速失败。
+
+当半连接队列满时,如果开启了 `tcp_syncookies`,服务端可能不会为该连接在半连接队列中分配常规条目,而是计算并返回一个 **SYN Cookie**。只有当收到合法的最终 `ACK` 时,才“重建”必要的连接信息。这是抵御 **SYN Flood** 的核心手段之一。
### 为什么要三次握手?
-三次握手的目的是建立可靠的通信信道,说到通讯,简单来说就是数据的发送与接收,而三次握手最主要的目的就是双方确认自己与对方的发送与接收是正常的。
+TCP 三次握手的核心目的是为了在客户端和服务器之间建立一个**可靠的**、**全双工的**通信信道。这需要实现两个主要目标:
+
+**1. 确认双方的收发能力,并同步初始序列号 (ISN)**
+
+```mermaid
+sequenceDiagram
+ autonumber
+ participant C as 客户端 Client
+ participant S as 服务端 Server
+
+ Note over C,S: 目标 同步双方 ISN 并确认双向可达
+
+ C->>S: SYN seq=ISN_C
+ Note right of S: 服务端确认 客户端到服务端方向可达
+ Note right of S: 服务端状态 SYN_RCVD
+
+ S->>C: SYN 加 ACK seq=ISN_S ack=ISN_C+1
+ Note left of C: 客户端确认 1 服务端到客户端方向可达 2 服务端已收到客户端 SYN 3 获得 ISN_S
+
+ C->>S: ACK seq=ISN_C+1 ack=ISN_S+1
+ Note left of C: 客户端状态 ESTABLISHED
+ Note right of S: 服务端确认 客户端已收到 SYN 加 ACK 双方 ISN 同步完成
+ Note right of S: 服务端状态 ESTABLISHED
+
+ Note over C,S: 连接建立 可以开始传输数据
+```
+
+TCP 依赖序列号(SEQ)与确认号(ACK)保证数据**有序、无重复、可重传**。三次握手通过交换并确认双方的 ISN,使两端对“从哪一个序号开始收发数据”达成一致,同时让握手过程形成闭环,避免仅凭单向信息就进入已建立状态。
+
+经过这三次交互,双方都确认了彼此的收发功能完好,并完成了初始序列号的同步,为后续可靠的数据传输奠定了基础。
+
+三次握手能力确认速记:
+
+1. C→S:SYN → S 确认:C 能发,S 能收(C→S 通)。
+2. S→C:SYN+ACK → C 确认:S 能发,C 能收,且 S 已收到 C 的 SYN(对方 SEQ + 1)。
+3. C→S:ACK → S 确认:C 已收到 S 的 SYN+ACK,握手闭环,连接建立。
-1. **第一次握手**:Client 什么都不能确认;Server 确认了对方发送正常,自己接收正常
-2. **第二次握手**:Client 确认了:自己发送、接收正常,对方发送、接收正常;Server 确认了:对方发送正常,自己接收正常
-3. **第三次握手**:Client 确认了:自己发送、接收正常,对方发送、接收正常;Server 确认了:自己发送、接收正常,对方发送、接收正常
+**2. 防止已失效的连接请求被错误地建立**
-三次握手就能确认双方收发功能都正常,缺一不可。
+```mermaid
+sequenceDiagram
+ participant C as 客户端 (Client)
+ participant S as 服务端 (Server)
-更详细的解答可以看这个:[TCP 为什么是三次握手,而不是两次或四次? - 车小胖的回答 - 知乎](https://www.zhihu.com/question/24853633/answer/115173386) 。
+ Note over C,S: 场景:旧的 SYN 报文在网络中滞留
+
+ C->>S: 1. 发送 SYN (旧请求 - 滞留中)
+ Note over C: 客户端超时,放弃该请求
+
+ C->>S: 2. 发送 SYN (新请求)
+ S-->>C: 3. 建立连接并正常释放...
+
+ rect rgb(255, 240, 240)
+ Note right of S: 此时,旧的 SYN 终于到达服务端
+ S->>C: 4. 发送 SYN+ACK (针对旧请求)
+
+ alt 如果是【两次握手】
+ Note right of S: (假设服务端在回复 SYN+ACK 后即认为连接建立)
+ Note right of S: ❌ 错误建立连接 (Ghost Connection) 分配内存/资源,造成浪费
+ else 如果是【三次握手】
+ Note left of C: 客户端无该连接状态 / 非期望报文
+ C->>S: 5. 发送 RST (重置报文) 或 直接丢弃
+
+ Note right of S: 【服务端结果】 收到 RST 立即清理; 或未收到 ACK 则重传并最终超时清理
+ Note right of S: ✅ 避免错误建连,保护资源
+ end
+ end
+```
+
+设想一个场景:客户端发送的第一个连接请求(SYN1)因网络延迟而滞留,于是客户端重发了第二个请求(SYN2)并成功建立了连接,数据传输完毕后连接被释放。此时,延迟的 SYN1 才到达服务端。
+
+- **如果是两次握手**:服务端收到这个失效的 SYN1 后,会误认为是一个新的连接请求,并立即分配资源、建立连接。但这将导致服务端单方面维持一个无效连接,白白浪费系统资源,因为客户端并不会有任何响应。
+- **有了第三次握手**:服务端收到失效的 SYN1 并回复 SYN+ACK 后,会等待客户端的最终确认(ACK)。由于客户端当前并没有发起连接的意图,它会忽略这个 SYN+ACK 或者发送一个 RST (Reset) 报文。这样,服务端就无法收到第三次握手的 ACK,最终会超时关闭这个错误的连接,从而避免了资源浪费。
+
+因此,三次握手是确保 TCP 连接可靠性的**最小且必需**的步骤。它不仅确认了双方的通信能力,更重要的是增加了一个最终确认环节,以防止网络中延迟、重复的历史请求对连接建立造成干扰。
### 第 2 次握手传回了 ACK,为什么还要传回 SYN?
-服务端传回发送端所发送的 ACK 是为了告诉客户端:“我接收到的信息确实就是你所发送的信号了”,这表明从客户端到服务端的通信是正常的。回传 SYN 则是为了建立并确认从服务端到客户端的通信。
+第二次握手里的 ACK 是为了确认“服务端确实收到了客户端的 SYN”(即确认 C→S 的请求到达)。而同时携带 SYN 是为了把服务端自己的 ISN 也同步给客户端,并要求客户端对其进行确认(即建立并确认 S→C 方向的建立过程)。只有双方的 ISN 都同步完成,后续的可靠传输(按序、重传、去重)才有共同起点。
+
+简言之:ACK 用于“我收到了你的 SYN”,SYN 用于“我也要发起我的同步,请你确认”。
> SYN 同步序列编号(Synchronize Sequence Numbers) 是 TCP/IP 建立连接时使用的握手信号。在客户机和服务端之间建立正常的 TCP 网络连接时,客户机首先发出一个 SYN 消息,服务端使用 SYN-ACK 应答表示接收到了这个消息,最后客户机再以 ACK(Acknowledgement)消息响应。这样在客户机和服务端之间才能建立起可靠的 TCP 连接,数据才可以在客户机和服务端之间传递。
@@ -58,31 +175,65 @@ tag:
断开一个 TCP 连接则需要“四次挥手”,缺一不可:
-1. **第一次挥手**:客户端发送一个 FIN(SEQ=x) 标志的数据包->服务端,用来关闭客户端到服务端的数据传送。然后客户端进入 **FIN-WAIT-1** 状态。
-2. **第二次挥手**:服务端收到这个 FIN(SEQ=X) 标志的数据包,它发送一个 ACK (ACK=x+1)标志的数据包->客户端 。然后服务端进入 **CLOSE-WAIT** 状态,客户端进入 **FIN-WAIT-2** 状态。
-3. **第三次挥手**:服务端发送一个 FIN (SEQ=y)标志的数据包->客户端,请求关闭连接,然后服务端进入 **LAST-ACK** 状态。
-4. **第四次挥手**:客户端发送 ACK (ACK=y+1)标志的数据包->服务端,然后客户端进入**TIME-WAIT**状态,服务端在收到 ACK (ACK=y+1)标志的数据包后进入 CLOSE 状态。此时如果客户端等待 **2MSL** 后依然没有收到回复,就证明服务端已正常关闭,随后客户端也可以关闭连接了。
+1. **第一次挥手 (FIN)**:当客户端(或任何一方)决定关闭连接时,它会向服务端发送一个 **FIN**(Finish)标志的报文段,表示自己已经没有数据要发送了。该报文段包含一个序列号 seq=u。发送后,客户端进入 **FIN-WAIT-1** 状态。
+2. **第二次挥手 (ACK)**:服务端收到 FIN 报文段后,会立即回复一个 **ACK** 确认报文段。其确认号为 ack=u+1。发送后,服务端进入 **CLOSE-WAIT** 状态。客户端收到这个 ACK 后,进入 **FIN-WAIT-2** 状态。此时,TCP 连接处于**半关闭(Half-Close)**状态:客户端到服务端的发送通道已关闭,但服务端到客户端的发送通道仍然可以传输数据。
+3. **第三次挥手 (FIN)**:当服务端确认所有待发送的数据都已发送完毕后,它也会向客户端发送一个 **FIN** 报文段,表示自己也准备关闭连接。该报文段同样包含一个序列号 seq=y。发送后,服务端进入 **LAST-ACK** 状态,等待客户端的最终确认。
+4. **第四次挥手**:客户端收到服务端的 FIN 报文段后,会回复一个最终的 **ACK** 确认报文段,确认号为 ack=y+1。发送后,客户端进入 **TIME-WAIT** 状态。服务端在收到这个 ACK 后,立即进入 **CLOSED** 状态,完成连接关闭。客户端则会在 **TIME-WAIT** 状态下等待 **2MSL**(Maximum Segment Lifetime,报文段最大生存时间)后,才最终进入 **CLOSED** 状态。
-**只要四次挥手没有结束,客户端和服务端就可以继续传输数据!**
+四次挥手期间连接可能处于**半关闭(Half-Close)**:**先发送 FIN 的一方不再发送应用数据**,但**另一方仍可继续发送剩余数据**,直到它也发送 FIN 并完成后续 ACK。
### 为什么要四次挥手?
-TCP 是全双工通信,可以双向传输数据。任何一方都可以在数据传送结束后发出连接释放的通知,待对方确认后进入半关闭状态。当另一方也没有数据再发送的时候,则发出连接释放通知,对方确认后就完全关闭了 TCP 连接。
+TCP 是全双工通信:两端的发送方向彼此独立。断开连接时,往往需要“我不发了”与“你也不发了”分别被对方确认,因此通常表现为四个报文段(FIN/ACK/FIN/ACK)。这也对应了现实世界的“双方分别确认挂断”的过程。
举个例子:A 和 B 打电话,通话即将结束后。
-1. **第一次挥手**:A 说“我没啥要说的了”
-2. **第二次挥手**:B 回答“我知道了”,但是 B 可能还会有要说的话,A 不能要求 B 跟着自己的节奏结束通话
-3. **第三次挥手**:于是 B 可能又巴拉巴拉说了一通,最后 B 说“我说完了”
-4. **第四次挥手**:A 回答“知道了”,这样通话才算结束。
+1. **第一次挥手**:A 说“我没啥要说的了”(A 发 FIN)
+2. **第二次挥手**:B 回答“我知道了”,但是 B 可能还会有要说的话,A 不能要求 B 跟着自己的节奏结束通话(B 回 ACK,但可能还有话要说)
+3. **第三次挥手**:于是 B 可能又巴拉巴拉说了一通,最后 B 说“我说完了”(B 发 FIN)
+4. **第四次挥手**:A 回答“知道了”,这样通话才算结束(A 回 ACK)。
### 为什么不能把服务端发送的 ACK 和 FIN 合并起来,变成三次挥手?
-因为服务端收到客户端断开连接的请求时,可能还有一些数据没有发完,这时先回复 ACK,表示接收到了断开连接的请求。等到数据发完之后再发 FIN,断开服务端到客户端的数据传送。
+```mermaid
+sequenceDiagram
+ autonumber
+ participant C as 客户端
+ participant K as 服务端内核
+ participant A as 服务端应用
+
+ Note over C,K: 客户端发起关闭
+ C->>K: FIN
+ Note right of K: 内核立即回复 ACK 用于确认对端 FIN
+ K-->>C: ACK
+ Note right of K: 服务端状态变为 CLOSE_WAIT
+
+ Note over K,A: 应用处理阶段
+ K->>A: 通知本端应用对端已关闭发送方向 例如 read 返回 0
+ A->>A: 读取和处理剩余数据
+ A->>A: 发送最后响应
+ A->>K: 调用 close 或 shutdown
+
+ Note right of K: 发送本端 FIN 并进入 LAST_ACK
+ K-->>C: FIN
+ Note left of C: 客户端回复 ACK 并进入 TIME_WAIT
+ C->>K: ACK
+ Note right of K: 服务端收到最终 ACK 后进入 CLOSED
+
+
+```
+
+关键原因是:**回复 ACK** 与 **发送 FIN** 的触发时机往往不同步。
+
+- 当服务端收到客户端 FIN 时,内核协议栈会立即回 ACK,用于确认“我收到了你要关闭的请求”。此时服务端进入 CLOSE_WAIT,等待本端应用把剩余事情处理完。
+- 只有当服务端应用处理完毕并调用 `close()/shutdown()` 后,内核才会发送本端的 FIN。
+- 因此“内核自动回 ACK”和“应用决定发 FIN”在时间上是解耦的,通常无法合并。只有在服务端恰好也准备立即关闭时,才可能出现 FIN+ACK 合并在一个报文段中的情况。
### 如果第二次挥手时服务端的 ACK 没有送达客户端,会怎样?
-客户端没有收到 ACK 确认,会重新发送 FIN 请求。
+- **客户端状态**:客户端发送第一次 `FIN` 后进入 **FIN_WAIT_1** 并启动重传计时器。
+- **重传逻辑**:若在超时时间内未收到对端对该 `FIN` 的确认 `ACK`,客户端会重传 `FIN`。
+- **服务端处理**:服务端若收到重复 `FIN`,通常会再次发送 `ACK`。如果由于网络问题 ACK 一直到不了,客户端在达到一定重试/超时阈值后可能报错或放弃(具体由实现与参数如 `tcp_retries2` 等影响)。
### 为什么第四次挥手客户端需要等待 2\*MSL(报文段最长寿命)时间后才进入 CLOSED 状态?
@@ -93,11 +244,8 @@ TCP 是全双工通信,可以双向传输数据。任何一方都可以在数
## 参考
- 《计算机网络(第 7 版)》
-
- 《图解 HTTP》
-
- TCP and UDP Tutorial:
-
- 从一次线上问题说起,详解 TCP 半连接队列、全连接队列:
diff --git a/docs/cs-basics/network/tcp-reliability-guarantee.md b/docs/cs-basics/network/tcp-reliability-guarantee.md
index d4c9bea80ed..5be11655bf4 100644
--- a/docs/cs-basics/network/tcp-reliability-guarantee.md
+++ b/docs/cs-basics/network/tcp-reliability-guarantee.md
@@ -1,8 +1,13 @@
---
title: TCP 传输可靠性保障(传输层)
+description: 系统梳理 TCP 的可靠性保障机制,覆盖重传/选择确认、流量与拥塞控制,明确端到端可靠传输的实现要点。
category: 计算机基础
tag:
- 计算机网络
+head:
+ - - meta
+ - name: keywords
+ content: TCP,可靠性,重传,SACK,流量控制,拥塞控制,滑动窗口,校验和
---
## TCP 如何保证传输的可靠性?
diff --git a/docs/cs-basics/network/the-whole-process-of-accessing-web-pages.md b/docs/cs-basics/network/the-whole-process-of-accessing-web-pages.md
index c09c10e6ab8..2bacba2fdb1 100644
--- a/docs/cs-basics/network/the-whole-process-of-accessing-web-pages.md
+++ b/docs/cs-basics/network/the-whole-process-of-accessing-web-pages.md
@@ -1,11 +1,16 @@
---
title: 访问网页的全过程(知识串联)
+description: 串联从输入 URL 到页面渲染的完整链路,涵盖 DNS、TCP、HTTP 与静态资源加载,助力面试与实践理解。
category: 计算机基础
tag:
- 计算机网络
+head:
+ - - meta
+ - name: keywords
+ content: 访问网页流程,DNS,TCP 建连,HTTP 请求,资源加载,渲染,关闭连接
---
-开发岗中总是会考很多计算机网络的知识点,但如果让面试官只靠一道题,便涵盖最多的计网知识点,那可能就是 **网页浏览的全过程** 了。本篇文章将带大家从头到尾过一遍这道被考烂的面试题,必会!!!
+开发岗中总是会考很多计算机网络的知识点,但如果让面试官只考一道题,便涵盖最多的计网知识点,那可能就是 **网页浏览的全过程** 了。本篇文章将带大家从头到尾过一遍这道被考烂的面试题,必会!!!
总的来说,网络通信模型可以用下图来表示,也就是大家只要熟记网络结构五层模型,按照这个体系,很多知识点都能顺出来了。访问网页的过程也是如此。
@@ -71,9 +76,9 @@ TCP 协议保证了数据传输的可靠性,是数据包传输的主力协议
终于,来到网络层,此时我们的主机不再是和另一台主机进行交互了,而是在和中间系统进行交互。也就是说,应用层和传输层都是端到端的协议,而网络层及以下都是中间件的协议了。
-**网络层的的核心功能——转发与路由**,必会!!!如果面试官问到了网络层,而你恰好又什么都不会的话,最最起码要说出这五个字——**转发与路由**。
+**网络层的核心功能——转发与路由**,必会!!!如果面试官问到了网络层,而你恰好又什么都不会的话,最最起码要说出这五个字——**转发与路由**。
- 转发:将分组从路由器的输入端口转移到合适的输出端口。
- 路由:确定分组从源到目的经过的路径。
-所以到目前为止,我们的数据包经过了应用层、传输层的封装,来到了网络层,终于开始准备在物理层面传输了,第一个要解决的问题就是——**往哪里传输?或者说,要把数据包发到哪个路由器上?**这便是 BGP 协议要解决的问题。
+所以到目前为止,我们的数据包经过了应用层、传输层的封装,来到了网络层,终于开始准备在物理层面传输了,第一个要解决的问题就是——**往哪里传输?或者说,要把数据包发到哪个路由器上?** 这便是 BGP 协议要解决的问题。
diff --git a/docs/cs-basics/operating-system/linux-intro.md b/docs/cs-basics/operating-system/linux-intro.md
index ead135776cc..acd46480bf9 100644
--- a/docs/cs-basics/operating-system/linux-intro.md
+++ b/docs/cs-basics/operating-system/linux-intro.md
@@ -1,13 +1,14 @@
---
title: Linux 基础知识总结
+description: 简单介绍一下 Java 程序员必知的 Linux 的一些概念以及常见命令。
category: 计算机基础
tag:
- 操作系统
- Linux
head:
- - meta
- - name: description
- content: 简单介绍一下 Java 程序员必知的 Linux 的一些概念以及常见命令。
+ - name: keywords
+ content: Linux,基础命令,发行版,文件系统,权限,进程,网络
---
@@ -70,7 +71,7 @@ inode 是 Linux/Unix 文件系统的基础。那 inode 到是什么?有什么作
通过以下五点可以概括 inode 到底是什么:
-1. 硬盘的最小存储单位是扇区(Sector),块(block)由多个扇区组成。文件数据存储在块中。块的最常见的大小是 4kb,约为 8 个连续的扇区组成(每个扇区存储 512 字节)。一个文件可能会占用多个 block,但是一个块只能存放一个文件。虽然,我们将文件存储在了块(block)中,但是我们还需要一个空间来存储文件的 **元信息 metadata**:如某个文件被分成几块、每一块在的地址、文件拥有者,创建时间,权限,大小等。这种 **存储文件元信息的区域就叫 inode**,译为索引节点:**i(index)+node**。 **每个文件都有一个唯一的 inode,存储文件的元信息。**
+1. 硬盘以扇区 (Sector) 为最小物理存储单位,而操作系统和文件系统以块 (Block) 为单位进行读写,块由多个扇区组成。文件数据存储在这些块中。现代硬盘扇区通常为 4KB,与一些常见块大小相同,但操作系统也支持更大的块大小,以提升大文件读写性能。文件元信息(例如权限、大小、修改时间以及数据块位置)存储在 inode(索引节点)中。每个文件都有唯一的 inode。inode 本身不存储文件数据,而是存储指向数据块的指针,操作系统通过这些指针找到并读取文件数据。 固态硬盘 (SSD) 虽然没有物理扇区,但使用逻辑块,其概念与传统硬盘的块类似。
2. inode 是一种固定大小的数据结构,其大小在文件系统创建时就确定了,并且在文件的生命周期内保持不变。
3. inode 的访问速度非常快,因为系统可以直接通过 inode 号码定位到文件的元数据信息,无需遍历整个文件系统。
4. inode 的数量是有限的,每个文件系统只能包含固定数量的 inode。这意味着当文件系统中的 inode 用完时,无法再创建新的文件或目录,即使磁盘上还有可用空间。因此,在创建文件系统时,需要根据文件和目录的预期数量来合理分配 inode 的数量。
@@ -185,8 +186,8 @@ Linux 使用一种称为目录树的层次结构来组织文件和目录。目
### 目录操作
- `ls`:显示目录中的文件和子目录的列表。例如:`ls /home`,显示 `/home` 目录下的文件和子目录列表。
-- `ll`:`ll` 是 `ls -l` 的别名,ll 命令可以看到该目录下的所有目录和文件的详细信息
-- `mkdir [选项] 目录名`:创建新目录(增)。例如:`mkdir -m 755 my_directory`,创建一个名为 `my_directory` 的新目录,并将其权限设置为 755,即所有用户对该目录有读、写和执行的权限。
+- `ll`:`ll` 是 `ls -l` 的别名,ll 命令可以看到该目录下的所有目录和文件的详细信息。
+- `mkdir [选项] 目录名`:创建新目录(增)。例如:`mkdir -m 755 my_directory`,创建一个名为 `my_directory` 的新目录,并将其权限设置为 755,其中所有者拥有读、写、执行权限,所属组和其他用户只有读、执行权限,无法修改目录内容(如创建或删除文件)。如果希望所有用户(包括所属组和其他用户)对目录都拥有读、写、执行权限,则应设置权限为 `777`,即:`mkdir -m 777 my_directory`。
- `find [路径] [表达式]`:在指定目录及其子目录中搜索文件或目录(查),非常强大灵活。例如:① 列出当前目录及子目录下所有文件和文件夹: `find .`;② 在`/home`目录下查找以 `.txt` 结尾的文件名:`find /home -name "*.txt"` ,忽略大小写: `find /home -i name "*.txt"` ;③ 当前目录及子目录下查找所有以 `.txt` 和 `.pdf` 结尾的文件:`find . \( -name "*.txt" -o -name "*.pdf" \)`或`find . -name "*.txt" -o -name "*.pdf"`。
- `pwd`:显示当前工作目录的路径。
- `rmdir [选项] 目录名`:删除空目录(删)。例如:`rmdir -p my_directory`,删除名为 `my_directory` 的空目录,并且会递归删除`my_directory`的空父目录,直到遇到非空目录或根目录。
@@ -285,11 +286,11 @@ Linux 中的打包文件一般是以 `.tar` 结尾的,压缩的命令一般是
需要注意的是:**超级用户可以无视普通用户的权限,即使文件目录权限是 000,依旧可以访问。**
-**在 linux 中的每个用户必须属于一个组,不能独立于组外。在 linux 中每个文件有所有者、所在组、其它组的概念。**
+**在 Linux 中的每个用户必须属于一个组,不能独立于组外。在 linux 中每个文件有所有者、所在组、其它组的概念。**
-- **所有者(u)**:一般为文件的创建者,谁创建了该文件,就天然的成为该文件的所有者,用 `ls ‐ahl` 命令可以看到文件的所有者 也可以使用 chown 用户名 文件名来修改文件的所有者 。
-- **文件所在组(g)**:当某个用户创建了一个文件后,这个文件的所在组就是该用户所在的组用 `ls ‐ahl`命令可以看到文件的所有组也可以使用 chgrp 组名 文件名来修改文件所在的组。
-- **其它组(o)**:除开文件的所有者和所在组的用户外,系统的其它用户都是文件的其它组。
+- **所有者(u)** :一般为文件的创建者,谁创建了该文件,就天然的成为该文件的所有者,用 `ls ‐ahl` 命令可以看到文件的所有者 也可以使用 chown 用户名 文件名来修改文件的所有者 。
+- **文件所在组(g)** :当某个用户创建了一个文件后,这个文件的所在组就是该用户所在的组用 `ls ‐ahl`命令可以看到文件的所有组也可以使用 chgrp 组名 文件名来修改文件所在的组。
+- **其它组(o)** :除开文件的所有者和所在组的用户外,系统的其它用户都是文件的其它组。
> 我们再来看看如何修改文件/目录的权限。
@@ -355,11 +356,13 @@ Linux 系统是一个多用户多任务的分时操作系统,任何一个要
- `ifconfig` 或 `ip`:用于查看系统的网络接口信息,包括网络接口的 IP 地址、MAC 地址、状态等。
- `netstat [选项]`:用于查看系统的网络连接状态和网络统计信息,可以查看当前的网络连接情况、监听端口、网络协议等。
- `ss [选项]`:比 `netstat` 更好用,提供了更快速、更详细的网络连接信息。
+- `nload`:`sar` 和 `nload` 都可以监控网络流量,但`sar` 的输出是文本形式的数据,不够直观。`nload` 则是一个专门用于实时监控网络流量的工具,提供图形化的终端界面,更加直观。不过,`nload` 不保存历史数据,所以它不适合用于长期趋势分析。并且,系统并没有默认安装它,需要手动安装。
+- `sudo hostnamectl set-hostname 新主机名`:更改主机名,并且重启后依然有效。`sudo hostname 新主机名`也可以更改主机名。不过需要注意的是,使用 `hostname` 命令直接更改主机名只是临时生效,系统重启后会恢复为原来的主机名。
### 其他
- `sudo + 其他命令`:以系统管理者的身份执行指令,也就是说,经由 sudo 所执行的指令就好像是 root 亲自执行。
-- `grep 要搜索的字符串 要搜索的文件 --color`:搜索命令,--color 代表高亮显示。
+- `grep [选项] "搜索内容" 文件路径`:非常强大且常用的文本搜索命令,它可以根据指定的字符串或正则表达式,在文件或命令输出中进行匹配查找,适用于日志分析、文本过滤、快速定位等多种场景。示例:忽略大小写搜索 syslog 中所有包含 error 的行:`grep -i "error" /var/log/syslog`,查找所有与 java 相关的进程:`ps -ef | grep "java"`。
- `kill -9 进程的pid`:杀死进程(-9 表示强制终止)先用 ps 查找进程,然后用 kill 杀掉。
- `shutdown`:`shutdown -h now`:指定现在立即关机;`shutdown +5 "System will shutdown after 5 minutes"`:指定 5 分钟后关机,同时送出警告信息给登入用户。
- `reboot`:`reboot`:重开机。`reboot -w`:做个重开机的模拟(只有纪录并不会真的重开机)。
diff --git a/docs/cs-basics/operating-system/operating-system-basic-questions-01.md b/docs/cs-basics/operating-system/operating-system-basic-questions-01.md
index ad102982456..61810c94a7b 100644
--- a/docs/cs-basics/operating-system/operating-system-basic-questions-01.md
+++ b/docs/cs-basics/operating-system/operating-system-basic-questions-01.md
@@ -1,15 +1,13 @@
---
title: 操作系统常见面试题总结(上)
+description: 最新操作系统高频面试题总结(上):用户态/内核态切换、进程线程区别、死锁四条件、系统调用详解、调度算法对比,附图表+⭐️重点标注,一文掌握OS核心考点,快速通关后端技术面试!
category: 计算机基础
tag:
- 操作系统
head:
- - meta
- name: keywords
- content: 操作系统,进程,进程通信方式,死锁,操作系统内存管理,块表,多级页表,虚拟内存,页面置换算法
- - - meta
- - name: description
- content: 很多读者抱怨计算操作系统的知识点比较繁杂,自己也没有多少耐心去看,但是面试的时候又经常会遇到。所以,我带着我整理好的操作系统的常见问题来啦!这篇文章总结了一些我觉得比较重要的操作系统相关的问题比如进程管理、内存管理、虚拟内存等等。
+ content: 操作系统面试题,用户态 vs 内核态,进程 vs 线程,死锁必要条件,系统调用过程,进程调度算法,PCB进程控制块,进程间通信IPC,死锁预防避免,操作系统基础高频题,虚拟内存管理
---
@@ -98,15 +96,17 @@ _玩玩电脑游戏还是必须要有 Windows 的,所以我现在是一台 Win
根据进程访问资源的特点,我们可以把进程在系统上的运行分为两个级别:
-- **用户态(User Mode)** : 用户态运行的进程可以直接读取用户程序的数据,拥有较低的权限。当应用程序需要执行某些需要特殊权限的操作,例如读写磁盘、网络通信等,就需要向操作系统发起系统调用请求,进入内核态。
-- **内核态(Kernel Mode)**:内核态运行的进程几乎可以访问计算机的任何资源包括系统的内存空间、设备、驱动程序等,不受限制,拥有非常高的权限。当操作系统接收到进程的系统调用请求时,就会从用户态切换到内核态,执行相应的系统调用,并将结果返回给进程,最后再从内核态切换回用户态。
-

+- **用户态(User Mode)** : 用户态运行的进程可以直接读取用户程序的数据,拥有较低的权限。当应用程序需要执行某些需要特殊权限的操作,例如读写磁盘、网络通信等,就需要向操作系统发起系统调用请求,进入内核态。
+- **内核态(Kernel Mode)** :内核态运行的进程几乎可以访问计算机的任何资源包括系统的内存空间、设备、驱动程序等,不受限制,拥有非常高的权限。当操作系统接收到进程的系统调用请求时,就会从用户态切换到内核态,执行相应的系统调用,并将结果返回给进程,最后再从内核态切换回用户态。
+
内核态相比用户态拥有更高的特权级别,因此能够执行更底层、更敏感的操作。不过,由于进入内核态需要付出较高的开销(需要进行一系列的上下文切换和权限检查),应该尽量减少进入内核态的次数,以提高系统的性能和稳定性。
#### 为什么要有用户态和内核态?只有一个内核态不行么?
+这样设计主要是为了**安全**和**稳定**。
+
- 在 CPU 的所有指令中,有一些指令是比较危险的比如内存分配、设置时钟、IO 处理等,如果所有的程序都能使用这些指令的话,会对系统的正常运行造成灾难性地影响。因此,我们需要限制这些危险指令只能内核态运行。这些只能由操作系统内核态执行的指令也被叫做 **特权指令** 。
- 如果计算机系统中只有一个内核态,那么所有程序或进程都必须共享系统资源,例如内存、CPU、硬盘等,这将导致系统资源的竞争和冲突,从而影响系统性能和效率。并且,这样也会让系统的安全性降低,毕竟所有程序或进程都具有相同的特权级别和访问权限。
@@ -118,12 +118,14 @@ _玩玩电脑游戏还是必须要有 Windows 的,所以我现在是一台 Win
用户态切换到内核态的 3 种方式:
-1. **系统调用(Trap)**:用户态进程 **主动** 要求切换到内核态的一种方式,主要是为了使用内核态才能做的事情比如读取磁盘资源。系统调用的机制其核心还是使用了操作系统为用户特别开放的一个中断来实现。
-2. **中断(Interrupt)**:当外围设备完成用户请求的操作后,会向 CPU 发出相应的中断信号,这时 CPU 会暂停执行下一条即将要执行的指令转而去执行与中断信号对应的处理程序,如果先前执行的指令是用户态下的程序,那么这个转换的过程自然也就发生了由用户态到内核态的切换。比如硬盘读写操作完成,系统会切换到硬盘读写的中断处理程序中执行后续操作等。
-3. **异常(Exception)**:当 CPU 在执行运行在用户态下的程序时,发生了某些事先不可知的异常,这时会触发由当前运行进程切换到处理此异常的内核相关程序中,也就转到了内核态,比如缺页异常。
+1. **系统调用(Trap)**:这是最主要的方式,是应用程序**主动**发起的。比如,当我们的程序需要读取一个文件或者发送网络数据时,它无法直接操作磁盘或网卡,就必须调用操作系统提供的接口(如 `read()`,`send()`), 这会触发一次从用户态到内核态的切换。
+2. **中断(Interrupt)**:这是**被动**的,由外部硬件设备触发。比如,当硬盘完成了数据读取,会向 CPU 发送一个中断信号,CPU 会暂停当前用户态的程序,切换到内核态去处理这个中断。
+3. **异常(Exception)**:这也是**被动**的,由程序自身错误引起。比如,我们的代码执行了一个除以零的操作,或者访问了一个非法的内存地址(缺页异常),CPU 会捕获这个异常,并切换到内核态去处理它。
在系统的处理上,中断和异常类似,都是通过中断向量表来找到相应的处理程序进行处理。区别在于,中断来自处理器外部,不是由任何一条专门的指令造成,而异常是执行当前指令的结果。
+最后,需要强调的是,这种**状态切换是有性能开销的**。因为它涉及到保存用户态的上下文(寄存器等)、切换到内核态执行、再恢复用户态的上下文。因此,在高性能编程中,我们常常需要考虑如何减少这种切换次数,比如通过缓冲 I/O 来批量读写文件,就是一个典型的例子。
+
### 系统调用
#### 什么是系统调用?
@@ -151,18 +153,23 @@ _玩玩电脑游戏还是必须要有 Windows 的,所以我现在是一台 Win
1. 用户态的程序发起系统调用,因为系统调用中涉及一些特权指令(只能由操作系统内核态执行的指令),用户态程序权限不足,因此会中断执行,也就是 Trap(Trap 是一种中断)。
2. 发生中断后,当前 CPU 执行的程序会中断,跳转到中断处理程序。内核程序开始执行,也就是开始处理系统调用。
-3. 内核处理完成后,主动触发 Trap,这样会再次发生中断,切换回用户态工作。
+3. 当系统调用处理完成后,操作系统使用特权指令(如 `iret`、`sysret` 或 `eret`)切换回用户态,恢复用户态的上下文,继续执行用户程序。

## 进程和线程
-### 什么是进程和线程?
+### 进程和线程的区别是什么?
-- **进程(Process)** 是指计算机中正在运行的一个程序实例。举例:你打开的微信就是一个进程。
-- **线程(Thread)** 也被称为轻量级进程,更加轻量。多个线程可以在同一个进程中同时执行,并且共享进程的资源比如内存空间、文件句柄、网络连接等。举例:你打开的微信里就有一个线程专门用来拉取别人发你的最新的消息。
+进程和线程是操作系统中并发执行的两个核心概念,它们的关系可以理解为 **工厂和工人** 的关系。
-### 进程和线程的区别是什么?
+**进程(Process)就像一个工厂**。操作系统在分配资源时,是以进程为基本单位的。比如,当我启动一个微信,操作系统就为它建立了一个独立的工厂,分配给它专属的内存空间、文件句柄等资源。这个工厂与其他工厂(比如我打开的浏览器进程)是严格隔离的。
+
+**线程(Thread)则像是工厂里的工人**。一个工厂里可以有很多工人,他们共享这个工厂的资源,但每个工人有自己的工具箱和任务清单,让他们可以独立地执行不同的任务。比如微信这个工厂里,可以有一个工人(线程)负责接收消息,一个工人负责渲染界面。
+
+这是我用 AI 绘制的一张图片,可以说是非常形象了:
+
+
下图是 Java 内存区域,我们从 JVM 的角度来说一下线程和进程之间的关系吧!
@@ -170,18 +177,17 @@ _玩玩电脑游戏还是必须要有 Windows 的,所以我现在是一台 Win
从上图可以看出:一个进程中可以有多个线程,多个线程共享进程的**堆**和**方法区 (JDK1.8 之后的元空间)**资源,但是每个线程有自己的**程序计数器**、**虚拟机栈** 和 **本地方法栈**。
-**总结:**
+这里从 3 个角度总结下线程和进程的核心区别:
-- 线程是进程划分成的更小的运行单位,一个进程在其执行的过程中可以产生多个线程。
-- 线程和进程最大的不同在于基本上各进程是独立的,而各线程则不一定,因为同一进程中的线程极有可能会相互影响。
-- 线程执行开销小,但不利于资源的管理和保护;而进程正相反。
+1. **资源所有权:** 进程是资源分配的基本单位,拥有独立的地址空间;而线程是 CPU 调度的基本单位,几乎不拥有系统资源,只保留少量私有数据(PC、栈、寄存器),主要共享其所属进程的资源。
+2. **开销:** 创建或销毁一个工厂(进程)的开销很大,需要分配独立的资源。而雇佣或解雇一个工人(线程)的开销就小得多。同理,进程间的上下文切换开销远大于线程间的切换。
+3. **健壮性:** 工厂之间是隔离的,一个工厂倒闭(进程崩溃)不会影响其他工厂。但一个工厂内的工人之间是共享资源的,一个工人操作失误(比如一个线程访问了非法内存)可能会导致整个工厂停工(整个进程崩溃)。
### 有了进程为什么还需要线程?
-- 进程切换是一个开销很大的操作,线程切换的成本较低。
-- 线程更轻量,一个进程可以创建多个线程。
-- 多个线程可以并发处理不同的任务,更有效地利用了多处理器和多核计算机。而进程只能在一个时间干一件事,如果在执行过程中遇到阻塞问题比如 IO 阻塞就会挂起直到结果返回。
-- 同一进程内的线程共享内存和文件,因此它们之间相互通信无须调用内核。
+核心原因就是**为了在单个应用内实现低开销、高效率的并发**。如果我想让微信同时接收消息和发送文件,如果用两个进程来实现,不仅资源开销巨大,它们之间通信还非常麻烦(需要 IPC)。而使用两个线程,它们不仅切换成本低,还能直接通过共享内存高效通信,从而能更好地利用多核 CPU,提升应用的响应速度和吞吐量。
+
+再那我们上面举的工厂和工人为例:线程=同一屋檐下的轻量级工人,切换成本低、共享内存零拷贝;若换成两个独立进程,就得各建一座工厂(独立地址空间),既费砖又费电(资源与 IPC 开销)。
### 为什么要使用多线程?
@@ -201,10 +207,10 @@ _玩玩电脑游戏还是必须要有 Windows 的,所以我现在是一台 Win
下面是几种常见的线程同步的方式:
-1. **互斥锁(Mutex)**:采用互斥对象机制,只有拥有互斥对象的线程才有访问公共资源的权限。因为互斥对象只有一个,所以可以保证公共资源不会被多个线程同时访问。比如 Java 中的 `synchronized` 关键词和各种 `Lock` 都是这种机制。
-2. **读写锁(Read-Write Lock)**:允许多个线程同时读取共享资源,但只有一个线程可以对共享资源进行写操作。
-3. **信号量(Semaphore)**:它允许同一时刻多个线程访问同一资源,但是需要控制同一时刻访问此资源的最大线程数量。
-4. **屏障(Barrier)**:屏障是一种同步原语,用于等待多个线程到达某个点再一起继续执行。当一个线程到达屏障时,它会停止执行并等待其他线程到达屏障,直到所有线程都到达屏障后,它们才会一起继续执行。比如 Java 中的 `CyclicBarrier` 是这种机制。
+1. **互斥锁(Mutex)** :采用互斥对象机制,只有拥有互斥对象的线程才有访问公共资源的权限。因为互斥对象只有一个,所以可以保证公共资源不会被多个线程同时访问。比如 Java 中的 `synchronized` 关键词和各种 `Lock` 都是这种机制。
+2. **读写锁(Read-Write Lock)** :允许多个线程同时读取共享资源,但只有一个线程可以对共享资源进行写操作。
+3. **信号量(Semaphore)** :它允许同一时刻多个线程访问同一资源,但是需要控制同一时刻访问此资源的最大线程数量。
+4. **屏障(Barrier)** :屏障是一种同步原语,用于等待多个线程到达某个点再一起继续执行。当一个线程到达屏障时,它会停止执行并等待其他线程到达屏障,直到所有线程都到达屏障后,它们才会一起继续执行。比如 Java 中的 `CyclicBarrier` 是这种机制。
5. **事件(Event)** :Wait/Notify:通过通知操作的方式来保持多线程同步,还可以方便的实现多线程优先级的比较操作。
### PCB 是什么?包含哪些信息?
@@ -238,48 +244,49 @@ PCB 主要包含下面几部分的内容:
> 下面这部分总结参考了:[《进程间通信 IPC (InterProcess Communication)》](https://www.jianshu.com/p/c1015f5ffa74) 这篇文章,推荐阅读,总结的非常不错。
-1. **管道/匿名管道(Pipes)**:用于具有亲缘关系的父子进程间或者兄弟进程之间的通信。
+1. **管道/匿名管道(Pipes)** :用于具有亲缘关系的父子进程间或者兄弟进程之间的通信。
2. **有名管道(Named Pipes)** : 匿名管道由于没有名字,只能用于亲缘关系的进程间通信。为了克服这个缺点,提出了有名管道。有名管道严格遵循 **先进先出(First In First Out)** 。有名管道以磁盘文件的方式存在,可以实现本机任意两个进程通信。
-3. **信号(Signal)**:信号是一种比较复杂的通信方式,用于通知接收进程某个事件已经发生;
-4. **消息队列(Message Queuing)**:消息队列是消息的链表,具有特定的格式,存放在内存中并由消息队列标识符标识。管道和消息队列的通信数据都是先进先出的原则。与管道(无名管道:只存在于内存中的文件;命名管道:存在于实际的磁盘介质或者文件系统)不同的是消息队列存放在内核中,只有在内核重启(即,操作系统重启)或者显式地删除一个消息队列时,该消息队列才会被真正的删除。消息队列可以实现消息的随机查询,消息不一定要以先进先出的次序读取,也可以按消息的类型读取.比 FIFO 更有优势。**消息队列克服了信号承载信息量少,管道只能承载无格式字 节流以及缓冲区大小受限等缺点。**
-5. **信号量(Semaphores)**:信号量是一个计数器,用于多进程对共享数据的访问,信号量的意图在于进程间同步。这种通信方式主要用于解决与同步相关的问题并避免竞争条件。
-6. **共享内存(Shared memory)**:使得多个进程可以访问同一块内存空间,不同进程可以及时看到对方进程中对共享内存中数据的更新。这种方式需要依靠某种同步操作,如互斥锁和信号量等。可以说这是最有用的进程间通信方式。
+3. **信号(Signal)** :信号是一种比较复杂的通信方式,用于通知接收进程某个事件已经发生;
+4. **消息队列(Message Queuing)** :消息队列是消息的链表,具有特定的格式,存放在内存中并由消息队列标识符标识。管道和消息队列的通信数据都是先进先出的原则。与管道(无名管道:只存在于内存中的文件;命名管道:存在于实际的磁盘介质或者文件系统)不同的是消息队列存放在内核中,只有在内核重启(即,操作系统重启)或者显式地删除一个消息队列时,该消息队列才会被真正的删除。消息队列可以实现消息的随机查询,消息不一定要以先进先出的次序读取,也可以按消息的类型读取.比 FIFO 更有优势。消息队列克服了信号承载信息量少,管道只能承载无格式字 节流以及缓冲区大小受限等缺点。
+5. **信号量(Semaphores)** :信号量是一个计数器,用于多进程对共享数据的访问,信号量的意图在于进程间同步。这种通信方式主要用于解决与同步相关的问题并避免竞争条件。
+6. **共享内存(Shared memory)** :使得多个进程可以访问同一块内存空间,不同进程可以及时看到对方进程中对共享内存中数据的更新。这种方式需要依靠某种同步操作,如互斥锁和信号量等。可以说这是最有用的进程间通信方式。
7. **套接字(Sockets)** : 此方法主要用于在客户端和服务器之间通过网络进行通信。套接字是支持 TCP/IP 的网络通信的基本操作单元,可以看做是不同主机之间的进程进行双向通信的端点,简单的说就是通信的两方的一种约定,用套接字中的相关函数来完成通信过程。
### 进程的调度算法有哪些?

-这是一个很重要的知识点!为了确定首先执行哪个进程以及最后执行哪个进程以实现最大 CPU 利用率,计算机科学家已经定义了一些算法,它们是:
+进程调度算法的核心目标是决定就绪队列中的哪个进程应该获得 CPU 资源,其设计目标通常是在**吞吐量、周转时间、响应时间**和**公平性**之间做权衡。
-- **先到先服务调度算法(FCFS,First Come, First Served)** : 从就绪队列中选择一个最先进入该队列的进程为之分配资源,使它立即执行并一直执行到完成或发生某事件而被阻塞放弃占用 CPU 时再重新调度。
-- **短作业优先的调度算法(SJF,Shortest Job First)** : 从就绪队列中选出一个估计运行时间最短的进程为之分配资源,使它立即执行并一直执行到完成或发生某事件而被阻塞放弃占用 CPU 时再重新调度。
-- **时间片轮转调度算法(RR,Round-Robin)** : 时间片轮转调度是一种最古老,最简单,最公平且使用最广的算法。每个进程被分配一个时间段,称作它的时间片,即该进程允许运行的时间。
-- **多级反馈队列调度算法(MFQ,Multi-level Feedback Queue)**:前面介绍的几种进程调度的算法都有一定的局限性。如**短进程优先的调度算法,仅照顾了短进程而忽略了长进程** 。多级反馈队列调度算法既能使高优先级的作业得到响应又能使短作业(进程)迅速完成,因而它是目前**被公认的一种较好的进程调度算法**,UNIX 操作系统采取的便是这种调度算法。
-- **优先级调度算法(Priority)**:为每个流程分配优先级,首先执行具有最高优先级的进程,依此类推。具有相同优先级的进程以 FCFS 方式执行。可以根据内存要求,时间要求或任何其他资源要求来确定优先级。
+我习惯将这些算法分为两大类:**非抢占式**和**抢占式**。
-### 什么是僵尸进程和孤儿进程?
+**第一类:非抢占式调度 (Non-Preemptive)**
-在 Unix/Linux 系统中,子进程通常是通过 fork()系统调用创建的,该调用会创建一个新的进程,该进程是原有进程的一个副本。子进程和父进程的运行是相互独立的,它们各自拥有自己的 PCB,即使父进程结束了,子进程仍然可以继续运行。
+这种方式下,一旦 CPU 分配给一个进程,它就会一直运行下去,直到任务完成或主动放弃(比如等待 I/O)。
-当一个进程调用 exit()系统调用结束自己的生命时,内核会释放该进程的所有资源,包括打开的文件、占用的内存等,但是该进程对应的 PCB 依然存在于系统中。这些信息只有在父进程调用 wait()或 waitpid()系统调用时才会被释放,以便让父进程得到子进程的状态信息。
+1. **先到先服务调度算法(FCFS,First Come, First Served)** : 这是最简单的,就像排队,谁先来谁先用。优点是公平、实现简单。但缺点很明显,如果一个很长的任务先到了,后面无数个短任务都得等着,这会导致平均等待时间很长,我们称之为“护航效应”。
+2. **短作业优先的调度算法(SJF,Shortest Job First)** : 从就绪队列中选出一个估计运行时间最短的进程为之分配资源。理论上,它的平均等待时间是最短的,吞吐量很高。但缺点是,它需要预测运行时间,这很难做到,而且可能会导致长作业“饿死”,永远得不到执行。
-这样的设计可以让父进程在子进程结束时得到子进程的状态信息,并且可以防止出现“僵尸进程”(即子进程结束后 PCB 仍然存在但父进程无法得到状态信息的情况)。
+**第二类:抢占式调度 (Preemptive)**
-- **僵尸进程**:子进程已经终止,但是其父进程仍在运行,且父进程没有调用 wait()或 waitpid()等系统调用来获取子进程的状态信息,释放子进程占用的资源,导致子进程的 PCB 依然存在于系统中,但无法被进一步使用。这种情况下,子进程被称为“僵尸进程”。避免僵尸进程的产生,父进程需要及时调用 wait()或 waitpid()系统调用来回收子进程。
-- **孤儿进程**:一个进程的父进程已经终止或者不存在,但是该进程仍在运行。这种情况下,该进程就是孤儿进程。孤儿进程通常是由于父进程意外终止或未及时调用 wait()或 waitpid()等系统调用来回收子进程导致的。为了避免孤儿进程占用系统资源,操作系统会将孤儿进程的父进程设置为 init 进程(进程号为 1),由 init 进程来回收孤儿进程的资源。
+操作系统可以强制剥夺当前进程的 CPU 使用权,分配给其他更重要的进程。现代操作系统基本都采用这种方式。
-### 如何查看是否有僵尸进程?
+- **时间片轮转调度算法(RR,Round-Robin)** : 这是最经典、最公平的抢占式算法。它给每个进程分配一个固定的时间片,用完了就把它放到队尾,切换到下一个进程。它非常适合分时系统,保证了每个进程都能得到响应,但时间片的设置很关键:太长了退化成 FCFS,太短了则会导致过于频繁的上下文切换,增加系统开销。
+- **优先级调度算法(Priority)**:每个进程都有一个优先级,进程调度器总是选择优先级最高的进程,具有相同优先级的进程以 FCFS 方式执行。这很灵活,可以根据内存要求,时间要求或任何其他资源要求来确定优先级,但同样可能导致低优先级进程“饿死”。
-Linux 下可以使用 Top 命令查找,`zombie` 值表示僵尸进程的数量,为 0 则代表没有僵尸进程。
+前面介绍的几种进程调度的算法都有一定的局限性,如:**短进程优先的调度算法,仅照顾了短进程而忽略了长进程** 。那有没有一种结合了上面这些进程调度算法优点的呢?
-
+**多级反馈队列调度算法(MFQ,Multi-level Feedback Queue)** 是现实世界中最常用的一种算法,比如早期的 UNIX。它非常聪明,结合了 RR 和优先级调度。它设置了多个不同优先级的队列,每个队列使用 RR 调度,时间片大小也不同。新进程先进入最高优先级队列;如果在一个时间片内没执行完,就会被降级到下一个队列。这样既照顾了短作业(在高优先级队列中快速完成),也保证了长作业不会饿死(最终会在低优先级队列中得到执行),是一种非常均衡的方案。
-下面这个命令可以定位僵尸进程以及该僵尸进程的父进程:
+### 那究竟是谁来调度这个进程呢?
-```bash
-ps -A -ostat,ppid,pid,cmd |grep -e '^[Zz]'
-```
+负责进程调度的核心是操作系统内核中的两个紧密协作的组件:**调度程序(Scheduler)** 和 **分派程序(Dispatcher)**。我们可以把它们理解成一个团队:
+
+- **调度程序 (Scheduler):** 可以看作是决策者。当需要进行调度时,调度程序会被激活,它会根据预设的调度算法(比如我们前面聊到的多级反馈队列),从就绪队列中挑选出下一个应该占用 CPU 的进程。
+- **分派程序 (Dispatcher):** 可以看作是执行者。它负责完成具体的“交接”工作,也就是**上下文切换**。这个过程非常底层,主要包括:
+ - 保存当前进程的上下文(CPU 寄存器状态、程序计数器等)到其进程控制块(PCB)中。
+ - 加载下一个被选中进程的上下文,从其 PCB 中读取状态,恢复到 CPU 寄存器。
+ - 将 CPU 的控制权正式移交给新进程,让它开始运行。
## 死锁
@@ -287,23 +294,23 @@ ps -A -ostat,ppid,pid,cmd |grep -e '^[Zz]'
死锁(Deadlock)描述的是这样一种情况:多个进程/线程同时被阻塞,它们中的一个或者全部都在等待某个资源被释放。由于进程/线程被无限期地阻塞,因此程序不可能正常终止。
-### 能列举一个操作系统发生死锁的例子吗?
+一个最经典的例子就是**“交叉持锁”**。想象有两个线程和两个锁:
-假设有两个进程 A 和 B,以及两个资源 X 和 Y,它们的分配情况如下:
+- 线程 1 先拿到了锁 A,然后尝试去获取锁 B。
+- 几乎同时,线程 2 拿到了锁 B,然后尝试去获取锁 A。
-| 进程 | 占用资源 | 需求资源 |
-| ---- | -------- | -------- |
-| A | X | Y |
-| B | Y | X |
+这时,线程 1 等着线程 2 释放锁 B,而线程 2 等着线程 1 释放锁 A,双方都持有对方需要的资源,并等待对方释放,就形成了一个“死结”。
-此时,进程 A 占用资源 X 并且请求资源 Y,而进程 B 已经占用了资源 Y 并请求资源 X。两个进程都在等待对方释放资源,无法继续执行,陷入了死锁状态。
+
### 产生死锁的四个必要条件是什么?
+死锁的发生并不是偶然的,它需要同时满足**四个必要条件**:
+
1. **互斥**:资源必须处于非共享模式,即一次只有一个进程可以使用。如果另一进程申请该资源,那么必须等待直到该资源被释放为止。
2. **占有并等待**:一个进程至少应该占有一个资源,并等待另一资源,而该资源被其他进程所占有。
3. **非抢占**:资源不能被抢占。只能在持有资源的进程完成任务后,该资源才会被释放。
-4. **循环等待**:有一组等待进程 `{P0, P1,..., Pn}`, `P0` 等待的资源被 `P1` 占有,`P1` 等待的资源被 `P2` 占有,……,`Pn-1` 等待的资源被 `Pn` 占有,`Pn` 等待的资源被 `P0` 占有。
+4. **循环等待**:有一组等待进程 {P0, P1,..., Pn}, P0 等待的资源被 P1 占有,P1 等待的资源被 P2 占有,……,Pn-1 等待的资源被 Pn 占有,Pn 等待的资源被 P0 占有。
**注意 ⚠️**:这四个条件是产生死锁的 **必要条件** ,也就是说只要系统发生死锁,这些条件必然成立,而只要上述条件之一不满足,就不会发生死锁。
@@ -371,12 +378,9 @@ Thread[线程 2,5,main]waiting get resource1
解决死锁的方法可以从多个角度去分析,一般的情况下,有**预防,避免,检测和解除四种**。
-- **预防** 是采用某种策略,**限制并发进程对资源的请求**,从而使得死锁的必要条件在系统执行的任何时间上都不满足。
-
-- **避免**则是系统在分配资源时,根据资源的使用情况**提前做出预测**,从而**避免死锁的发生**
-
-- **检测**是指系统设有**专门的机构**,当死锁发生时,该机构能够检测死锁的发生,并精确地确定与死锁有关的进程和资源。
-- **解除** 是与检测相配套的一种措施,用于**将进程从死锁状态下解脱出来**。
+- **死锁预防:** 这是我们程序员最常用的方法。通过编码规范来破坏条件。最经典的就是**破坏循环等待**,比如规定所有线程都必须**按相同的顺序**来获取锁(比如先 A 后 B),这样就不会形成环路。
+- **死锁避免:** 这是一种更动态的方法,比如操作系统的**银行家算法**。它会在分配资源前进行预测,如果这次分配可能导致未来发生死锁,就拒绝分配。但这种方法开销很大,在通用系统中用得比较少。
+- **死锁检测与解除:** 这是一种“事后补救”的策略,就像乐观锁。系统允许死锁发生,但会有一个后台线程(或机制)定期检测是否存在死锁环路(比如通过分析线程等待图)。一旦发现,就会采取措施解除,比如**强制剥夺某个线程的资源或直接终止它**。数据库系统中的死锁处理就常常采用这种方式。
#### 死锁的预防
@@ -424,7 +428,7 @@ Thread[线程 2,5,main]waiting get resource1
操作系统中的每一刻时刻的**系统状态**都可以用**进程-资源分配图**来表示,进程-资源分配图是描述进程和资源申请及分配关系的一种有向图,可用于**检测系统是否处于死锁状态**。
-用一个方框表示每一个资源类,方框中的黑点表示该资源类中的各个资源,每个键进程用一个圆圈表示,用 **有向边** 来表示**进程申请资源和资源被分配的情况**。
+用一个方框表示每一个资源类,方框中的黑点表示该资源类中的各个资源,用一个圆圈表示每一个进程,用 **有向边** 来表示**进程申请资源和资源被分配的情况**。
图中 2-21 是**进程-资源分配图**的一个例子,其中共有三个资源类,每个进程的资源占有和申请情况已清楚地表示在图中。在这个例子中,由于存在 **占有和等待资源的环路** ,导致一组进程永远处于等待资源的状态,发生了 **死锁**。
diff --git a/docs/cs-basics/operating-system/operating-system-basic-questions-02.md b/docs/cs-basics/operating-system/operating-system-basic-questions-02.md
index 3f50115d01d..51ed5fd65c3 100644
--- a/docs/cs-basics/operating-system/operating-system-basic-questions-02.md
+++ b/docs/cs-basics/operating-system/operating-system-basic-questions-02.md
@@ -1,17 +1,17 @@
---
title: 操作系统常见面试题总结(下)
+description: 最新操作系统高频面试题总结(下):虚拟内存映射、内存碎片/伙伴系统、TLB+页缺失处理、分页分段对比、页面置换算法详解、文件系统&磁盘调度,附图表+⭐️重点标注,一文掌握OS内存/文件考点,快速通关后端面试!
category: 计算机基础
tag:
- 操作系统
head:
- - meta
- name: keywords
- content: 操作系统,进程,进程通信方式,死锁,操作系统内存管理,块表,多级页表,虚拟内存,页面置换算法
- - - meta
- - name: description
- content: 很多读者抱怨计算操作系统的知识点比较繁杂,自己也没有多少耐心去看,但是面试的时候又经常会遇到。所以,我带着我整理好的操作系统的常见问题来啦!这篇文章总结了一些我觉得比较重要的操作系统相关的问题比如进程管理、内存管理、虚拟内存等等。
+ content: 操作系统面试题,虚拟内存详解,分页 vs 分段,页面置换算法,内存碎片,伙伴系统,TLB快表,页缺失,文件系统基础,磁盘调度算法,硬链接 vs 软链接
---
+
+
## 内存管理
### 内存管理主要做了什么?
@@ -68,7 +68,7 @@ head:
非连续内存管理存在下面 3 种方式:
-- **段式管理**:以段(—段连续的物理内存)的形式管理/分配物理内存。应用程序的虚拟地址空间被分为大小不等的段,段是有实际意义的,每个段定义了一组逻辑信息,例如有主程序段 MAIN、子程序段 X、数据段 D 及栈段 S 等。
+- **段式管理**:以段(一段连续的物理内存)的形式管理/分配物理内存。应用程序的虚拟地址空间被分为大小不等的段,段是有实际意义的,每个段定义了一组逻辑信息,例如有主程序段 MAIN、子程序段 X、数据段 D 及栈段 S 等。
- **页式管理**:把物理内存分为连续等长的物理页,应用程序的虚拟地址空间也被划分为连续等长的虚拟页,是现代操作系统广泛使用的一种内存管理方式。
- **段页式管理机制**:结合了段式管理和页式管理的一种内存管理机制,把物理内存先分成若干段,每个段又继续分成若干大小相等的页。
@@ -131,7 +131,7 @@ MMU 将虚拟地址翻译为物理地址的主要机制有 3 种:
### 分段机制
-**分段机制(Segmentation)** 以段(—段 **连续** 的物理内存)的形式管理/分配物理内存。应用程序的虚拟地址空间被分为大小不等的段,段是有实际意义的,每个段定义了一组逻辑信息,例如有主程序段 MAIN、子程序段 X、数据段 D 及栈段 S 等。
+**分段机制(Segmentation)** 以段(一段 **连续** 的物理内存)的形式管理/分配物理内存。应用程序的虚拟地址空间被分为大小不等的段,段是有实际意义的,每个段定义了一组逻辑信息,例如有主程序段 MAIN、子程序段 X、数据段 D 及栈段 S 等。
#### 段表有什么用?地址翻译过程是怎样的?
@@ -188,7 +188,7 @@ MMU 将虚拟地址翻译为物理地址的主要机制有 3 种:

-在分页机制下,每个应用程序都会有一个对应的页表。
+在分页机制下,每个进程都会有一个对应的页表。
分页机制下的虚拟地址由两部分组成:
@@ -211,11 +211,11 @@ MMU 将虚拟地址翻译为物理地址的主要机制有 3 种:
#### 单级页表有什么问题?为什么需要多级页表?
-以 32 位的环境为例,虚拟地址空间范围共有 2^32(4G)。假设 一个页的大小是 2^12(4KB),那页表项共有 4G / 4K = 2^20 个。每个页表项为一个地址,占用 4 字节,`2^20 * 2^2 / 1024 * 1024= 4MB`。也就是说一个程序啥都不干,页表大小就得占用 4M。
+以 32 位的环境为例,虚拟地址空间范围共有 2^32(4G)。假设 一个页的大小是 2^12(4KB),那页表项共有 4G / 4K = 2^20 个。每个页表项为一个地址,占用 4 字节,`(2^20 * 2^2) / (1024 * 1024)= 4MB`。也就是说一个程序啥都不干,页表大小就得占用 4M。
系统运行的应用程序多起来的话,页表的开销还是非常大的。而且,绝大部分应用程序可能只能用到页表中的几项,其他的白白浪费了。
-为了解决这个问题,操作系统引入了 **多级页表** ,多级页表对应多个页表,每个页表也前一个页表相关联。32 位系统一般为二级页表,64 位系统一般为四级页表。
+为了解决这个问题,操作系统引入了 **多级页表** ,多级页表对应多个页表,每个页表与前一个页表相关联。32 位系统一般为二级页表,64 位系统一般为四级页表。
这里以二级页表为例进行介绍:二级列表分为一级页表和二级页表。一级页表共有 1024 个页表项,一级页表又关联二级页表,二级页表同样共有 1024 个页表项。二级页表中的一级页表项是一对多的关系,二级页表按需加载(只会用到很少一部分二级页表),进而节省空间占用。
@@ -317,12 +317,16 @@ LRU 算法是实际使用中应用的比较多,也被认为是最接近 OPT
### 段页机制
-结合了段式管理和页式管理的一种内存管理机制,把物理内存先分成若干段,每个段又继续分成若干大小相等的页。
+结合了段式管理和页式管理的一种内存管理机制。程序视角中,内存被划分为多个逻辑段,每个逻辑段进一步被划分为固定大小的页。
在段页式机制下,地址翻译的过程分为两个步骤:
-1. 段式地址映射。
-2. 页式地址映射。
+1. **段式地址映射(虚拟地址 → 线性地址):**
+ - 虚拟地址 = 段选择符(段号)+ 段内偏移。
+ - 根据段号查段表,找到段基址,加上段内偏移得到线性地址。
+2. **页式地址映射(线性地址 → 物理地址):**
+ - 线性地址 = 页号 + 页内偏移。
+ - 根据页号查页表,找到物理页框号,加上页内偏移得到物理地址。
### 局部性原理
@@ -375,7 +379,7 @@ LRU 算法是实际使用中应用的比较多,也被认为是最接近 OPT
### 提高文件系统性能的方式有哪些?
-- **优化硬件**:使用高速硬件设备(如 SSD、NVMe)替代传统的机械硬盘,使用 RAID(Redundant Array of Inexpensive Disks)等技术提高磁盘性能。
+- **优化硬件**:使用高速硬件设备(如 SSD、NVMe)替代传统的机械硬盘,使用 RAID(Redundant Array of Independent Disks)等技术提高磁盘性能。
- **选择合适的文件系统选型**:不同的文件系统具有不同的特性,对于不同的应用场景选择合适的文件系统可以提高系统性能。
- **运用缓存**:访问磁盘的效率比较低,可以运用缓存来减少磁盘的访问次数。不过,需要注意缓存命中率,缓存命中率过低的话,效果太差。
- **避免磁盘过度使用**:注意磁盘的使用率,避免将磁盘用满,尽量留一些剩余空间,以免对文件系统的性能产生负面影响。
diff --git a/docs/cs-basics/operating-system/shell-intro.md b/docs/cs-basics/operating-system/shell-intro.md
index 48066214c23..d3bf6da4024 100644
--- a/docs/cs-basics/operating-system/shell-intro.md
+++ b/docs/cs-basics/operating-system/shell-intro.md
@@ -1,13 +1,14 @@
---
title: Shell 编程基础知识总结
+description: Shell 编程在我们的日常开发工作中非常实用,目前 Linux 系统下最流行的运维自动化语言就是 Shell 和 Python 了。这篇文章我会简单总结一下 Shell 编程基础知识,带你入门 Shell 编程!
category: 计算机基础
tag:
- 操作系统
- Linux
head:
- - meta
- - name: description
- content: Shell 编程在我们的日常开发工作中非常实用,目前 Linux 系统下最流行的运维自动化语言就是 Shell 和 Python 了。这篇文章我会简单总结一下 Shell 编程基础知识,带你入门 Shell 编程!
+ - name: keywords
+ content: Shell,脚本,命令,自动化,运维,Linux,基础语法
---
Shell 编程在我们的日常开发工作中非常实用,目前 Linux 系统下最流行的运维自动化语言就是 Shell 和 Python 了。
diff --git a/docs/database/basis.md b/docs/database/basis.md
index 34ee65f1242..868435d38e8 100644
--- a/docs/database/basis.md
+++ b/docs/database/basis.md
@@ -1,8 +1,13 @@
---
title: 数据库基础知识总结
+description: 数据库基础知识总结,包括数据库、DBMS、数据库系统、DBA的概念区别,DBMS核心功能,元组、码、主键外键等关系型数据库核心概念,以及ER图的使用方法。
category: 数据库
tag:
- 数据库基础
+head:
+ - - meta
+ - name: keywords
+ content: 数据库,数据库管理系统,DBMS,数据库系统,DBA,SQL,DDL,DML,数据模型,关系型数据库,主键,外键,ER图
---
@@ -11,20 +16,189 @@ tag:
## 什么是数据库, 数据库管理系统, 数据库系统, 数据库管理员?
-- **数据库** : 数据库(DataBase 简称 DB)就是信息的集合或者说数据库是由数据库管理系统管理的数据的集合。
-- **数据库管理系统** : 数据库管理系统(Database Management System 简称 DBMS)是一种操纵和管理数据库的大型软件,通常用于建立、使用和维护数据库。
-- **数据库系统** : 数据库系统(Data Base System,简称 DBS)通常由软件、数据库和数据管理员(DBA)组成。
-- **数据库管理员** : 数据库管理员(Database Administrator, 简称 DBA)负责全面管理和控制数据库系统。
+这四个概念描述了从数据本身到管理整个体系的不同层次,我们常用一个图书馆的例子来把它们串联起来理解。
+
+- **数据库 (Database - DB):** 它就像是图书馆里,书架上存放的所有书籍和资料。从技术上讲,数据库就是按照一定数据模型组织、描述和储存起来的、可以被各种用户共享的结构化数据的集合。它就是我们最终要存取的核心——信息本身。
+- **数据库管理系统 (Database Management System - DBMS):** 它就像是整个图书馆的管理系统,包括图书的分类编目规则、借阅归还流程、安全检查系统等等。从技术上讲,DBMS 是一种大型软件,比如我们常用的 MySQL、Oracle、PostgreSQL 软件。它的核心职责是科学地组织和存储数据、高效地获取和维护数据;为我们屏蔽了底层文件操作的复杂性,提供了一套标准接口(如 SQL)来操纵数据,并负责并发控制、事务管理、权限控制等复杂问题。
+- **数据库系统 (Database System - DBS):** 它就是整个正常运转的图书馆。这是一个更大的概念,不仅包括书(DB)和管理系统(DBMS),还包括了硬件、应用和使用的人。
+- **数据库管理员 (Database Administrator - DBA ):** 他就是图书馆的馆长,负责整个数据库系统正常运行。他的职责非常广泛,包括数据库的设计、安装、监控、性能调优、备份与恢复、安全管理等等,确保整个系统的稳定、高效和安全。
+
+DB 和 DBMS 我们通常会搞混,这里再简单提一下:**通常我们说“用 MySQL 数据库”,其实是用 MySQL(DBMS)来管理一个或多个数据库(DB)。**
+
+## DBMS 有哪些主要的功能
+
+```mermaid
+graph TD
+ DBMS["🗄️ DBMS数据库管理系统 "]
+
+ subgraph define["数据定义"]
+ DDL["📐 DDL Data Definition Language"]
+ DDL_Items["• 创建/修改/删除对象 • 定义表结构 • 定义视图、索引 • 定义触发器 • 定义存储过程"]
+ end
+
+ subgraph operate["数据操作"]
+ DML["⚡ DML Data Manipulation Language"]
+ CRUD["CRUD 操作 • Create 创建 • Read 读取 • Update 更新 • Delete 删除"]
+ end
+
+ subgraph control["数据控制"]
+ DCL["🔐 数据控制功能"]
+ Control_Items["• 并发控制 • 事务管理 • 完整性约束 • 权限控制 • 安全性限制"]
+ end
+
+ subgraph maintain["数据库维护"]
+ Maintenance["🛠️ 维护功能"]
+ Maintain_Items["• 数据导入/导出 • 备份与恢复 • 性能监控与分析 • 系统日志管理"]
+ end
+
+ DBMS --> DDL
+ DBMS --> DML
+ DBMS --> DCL
+ DBMS --> Maintenance
+
+ DDL --> DDL_Items
+ DML --> CRUD
+ DCL --> Control_Items
+ Maintenance --> Maintain_Items
+
+ style DBMS fill:#005D7B,stroke:#00838F,stroke-width:4px,color:#fff
+
+ style DDL fill:#4CA497,stroke:#00838F,stroke-width:3px,color:#fff
+ style DDL_Items fill:#f0fffe,stroke:#4CA497,stroke-width:2px,color:#333
+
+ style DML fill:#E99151,stroke:#C44545,stroke-width:3px,color:#fff
+ style CRUD fill:#fff5e6,stroke:#E99151,stroke-width:2px,color:#333
+
+ style DCL fill:#00838F,stroke:#005D7B,stroke-width:3px,color:#fff
+ style Control_Items fill:#e6f7ff,stroke:#00838F,stroke-width:2px,color:#333
+
+ style Maintenance fill:#C44545,stroke:#8B0000,stroke-width:3px,color:#fff
+ style Maintain_Items fill:#ffe6e6,stroke:#C44545,stroke-width:2px,color:#333
+
+ style define fill:#E4C189,stroke:#E99151,stroke-width:2px,stroke-dasharray: 5 5,opacity:0.3
+ style operate fill:#E4C189,stroke:#E99151,stroke-width:2px,stroke-dasharray: 5 5,opacity:0.3
+ style control fill:#E4C189,stroke:#E99151,stroke-width:2px,stroke-dasharray: 5 5,opacity:0.3
+ style maintain fill:#E4C189,stroke:#E99151,stroke-width:2px,stroke-dasharray: 5 5,opacity:0.3
+```
+
+DBMS 通常提供四大核心功能:
+
+1. **数据定义:** 这是 DBMS 的基础。它提供了一套数据定义语言(Data Definition Language - DDL),让我们能够创建、修改和删除数据库中的各种对象。这不仅仅是定义表的结构(比如字段名、数据类型),还包括定义视图、索引、触发器、存储过程等。
+2. **数据操作:** 这是我们作为开发者日常使用最多的功能。它提供了一套数据操作语言(Data Manipulation Language - DML),核心就是我们熟悉的增、删、改、查(CRUD)操作。它让我们能够方便地对数据库中的数据进行操作和检索。
+3. **数据控制:** 这是保证数据正确、安全、可靠的关键。通常包含并发控制、事务管理、完整性约束、权限控制、安全性限制等功能。
+4. **数据库维护:** 这部分功能是为了保障数据库系统的长期稳定运行。它包括了数据的导入导出、数据库的备份与恢复、性能监控与分析、以及系统日志管理等。
+
+## 你知道哪些类型的 DBMS?
+
+### 关系型数据库
+
+除了我们最常用的关系型数据库(RDBMS),比如 MySQL(开源首选)、PostgreSQL(功能最全)、Oracle(企业级),它们基于严格的表结构和 SQL,非常适合结构化数据和需要事务保证的场景,例如银行交易、订单系统。
+
+近年来,为了应对互联网应用带来的海量数据、高并发和多样化数据结构的需求,涌现出了一大批 NoSQL 和 NewSQL 数据库。
+
+### NoSQL 数据库
+
+它们的共同特点是为了极致的性能和水平扩展能力,在某些方面(通常是事务)做了妥协。
+
+**1. 键值数据库,代表是 Redis。**
+
+- **特点:** 数据模型极其简单,就是一个巨大的 Map,通过 Key 来存取 Value。内存操作,性能极高。
+- **适用场景:** 非常适合做缓存、会话存储、计数器等对读写性能要求极高的场景。
+
+**2. 文档数据库,代表是 MongoDB。**
+
+- **特点:** 它存储的是半结构化的文档(比如 JSON/BSON),结构灵活,不需要预先定义表结构。
+- **适用场景:** 特别适合那些数据结构多变、快速迭代的业务,比如用户画像、内容管理系统、日志存储等。
+
+**3. 列式数据库,代表是 HBase, Cassandra。**
+
+- **特点:** 数据是按列族而不是按行来存储的。这使得它在对大量行进行少量列的读取时,性能极高。
+- **适用场景:** 专为海量数据存储和分析设计,非常适合做大数据分析、监控数据存储、推荐系统等需要高吞吐量写入和范围扫描的场景。
+
+**4. 图形数据库,代表是 Neo4j。**
+
+- **特点:** 数据模型是节点(Nodes)和边(Edges),专门用来存储和查询实体之间的复杂关系。
+- **适用场景:** 在社交网络(好友关系)、推荐引擎(用户-商品关系)、知识图谱、欺诈检测(资金流动关系)等场景下,表现远超关系型数据库。
+
+### NewSQL 数据库
+
+由于 NoSQL 不支持事务,很多对于数据安全要求非常高的系统(比如财务系统、订单系统、交易系统)就不太适合使用了。不过,这类系统往往有存储大量数据的需求。
+
+这些系统往往只能通过购买性能更强大的计算机,或者通过数据库中间件来提高存储能力。不过,前者的金钱成本太高,后者的开发成本太高。
+
+于是,**NewSQL** 就来了!
+
+简单来说,NewSQL 就是:**分布式存储+SQL+事务** 。NewSQL 不仅具有 NoSQL 对海量数据的存储管理能力,还保持了传统数据库支持 ACID 和 SQL 等特性。因此,NewSQL 也可以称为 **分布式关系型数据库**。
+
+NewSQL 数据库设计的一些目标:
+
+1. 横向扩展(Scale Out) : 通过增加机器的方式来提高系统的负载能力。与之类似的是 Scale Up(纵向扩展),升级硬件设备的方式来提高系统的负载能力。
+2. 强一致性(Strict Consistency):在任意时刻,所有节点中的数据是一样的。
+3. 高可用(High Availability):系统几乎可以一直提供服务。
+4. 支持标准 SQL(Structured Query Language) :PostgreSQL、MySQL、Oracle 等关系型数据库都支持 SQL 。
+5. 事务(ACID) : 原子性(Atomicity)、一致性(Consistency)、 隔离性(Isolation); 持久性(Durability)。
+6. 兼容主流关系型数据库 : 兼容 MySQL、Oracle、PostgreSQL 等常用关系型数据库。
+7. 云原生 (Cloud Native):可在公有云、私有云、混合云中实现部署工具化、自动化。
+8. HTAP(Hybrid Transactional/Analytical Processing) :支持 OLTP 和 OLAP 混合处理。
+
+NewSQL 数据库代表:Google 的 F1/Spanner、阿里的 [OceanBase](https://open.oceanbase.com/)、PingCAP 的 [TiDB](https://pingcap.com/zh/product-community/) 。
## 什么是元组, 码, 候选码, 主码, 外码, 主属性, 非主属性?
-- **元组**:元组(tuple)是关系数据库中的基本概念,关系是一张表,表中的每行(即数据库中的每条记录)就是一个元组,每列就是一个属性。 在二维表里,元组也称为行。
-- **码**:码就是能唯一标识实体的属性,对应表中的列。
-- **候选码**:若关系中的某一属性或属性组的值能唯一的标识一个元组,而其任何、子集都不能再标识,则称该属性组为候选码。例如:在学生实体中,“学号”是能唯一的区分学生实体的,同时又假设“姓名”、“班级”的属性组合足以区分学生实体,那么{学号}和{姓名,班级}都是候选码。
-- **主码** : 主码也叫主键。主码是从候选码中选出来的。 一个实体集中只能有一个主码,但可以有多个候选码。
-- **外码** : 外码也叫外键。如果一个关系中的一个属性是另外一个关系中的主码则这个属性为外码。
-- **主属性**:候选码中出现过的属性称为主属性。比如关系 工人(工号,身份证号,姓名,性别,部门). 显然工号和身份证号都能够唯一标示这个关系,所以都是候选码。工号、身份证号这两个属性就是主属性。如果主码是一个属性组,那么属性组中的属性都是主属性。
-- **非主属性:** 不包含在任何一个候选码中的属性称为非主属性。比如在关系——学生(学号,姓名,年龄,性别,班级)中,主码是“学号”,那么其他的“姓名”、“年龄”、“性别”、“班级”就都可以称为非主属性。
+在关系型数据库理论中,理解元组、码、候选码、主码、外码、主属性和非主属性这些核心概念,对于数据库设计和规范化至关重要。这些概念构成了关系数据库的理论基础。
+
+```mermaid
+graph TD
+ A[关系数据库概念] --> B[数据组织]
+ A --> C[码的类型]
+ A --> D[属性分类]
+
+ B --> B1[元组 表中的行记录]
+ B --> B2[属性 表中的列]
+
+ C --> C1[码 唯一标识]
+ C1 --> C2[候选码 最小唯一标识集]
+ C2 --> C3[主码 选定的候选码]
+ C1 --> C4[外码 引用其他表主码]
+
+ D --> D1[主属性 候选码中的属性]
+ D --> D2[非主属性 不在候选码中的属性]
+
+ C3 -.关联.-> C4
+ C2 -.构成.-> D1
+
+ style A fill:#4CA497,stroke:#00838F,stroke-width:3px,color:#fff
+ style B fill:#00838F,stroke:#005D7B,stroke-width:2px,color:#fff
+ style C fill:#E99151,stroke:#005D7B,stroke-width:2px,color:#fff
+ style D fill:#005D7B,stroke:#00838F,stroke-width:2px,color:#fff
+
+ style B1 fill:#E4C189,stroke:#00838F,stroke-width:1px
+ style B2 fill:#E4C189,stroke:#00838F,stroke-width:1px
+
+ style C1 fill:#E4C189,stroke:#E99151,stroke-width:1px
+ style C2 fill:#E4C189,stroke:#E99151,stroke-width:1px
+ style C3 fill:#C44545,stroke:#005D7B,stroke-width:2px,color:#fff
+ style C4 fill:#E4C189,stroke:#E99151,stroke-width:1px
+
+ style D1 fill:#E4C189,stroke:#005D7B,stroke-width:1px
+ style D2 fill:#E4C189,stroke:#005D7B,stroke-width:1px
+```
+
+### 基础概念
+
+- **元组(Tuple):** 元组是关系数据库中的基本单位,在二维表中对应一行记录。每个元组包含了一个实体的完整信息。例如,在学生表中,每个学生的完整信息(学号、姓名、年龄等)构成一个元组。
+- **码(Key):** 码是能够唯一标识关系中元组的一个或多个属性的集合。码的主要作用是保证数据的唯一性和完整性。
+
+### 码的分类
+
+- **候选码(Candidate Key):** 候选码是能够唯一标识元组的最小属性集合,其任何真子集都不能唯一标识元组。一个关系可能有多个候选码。例如,在学生表中,如果"学号"能唯一标识学生,同时"身份证号"也能唯一标识学生,那么{学号}和{身份证号}都是候选码。
+- **主码/主键(Primary Key):** 主码是从候选码中选择的一个,用于唯一标识关系中的元组。每个关系只能有一个主码,但可以有多个候选码。选择主码时通常考虑:简单性、稳定性、无业务含义等因素。
+- **外码/外键(Foreign Key):** 外码是一个关系中的属性或属性组,它对应另一个关系的主码。外码用于建立和维护两个关系之间的联系,是实现参照完整性的重要机制。例如,在选课表中的"学号"如果引用学生表的主码"学号",则选课表中的"学号"就是外码。
+
+### 属性分类
+
+- **主属性(Prime Attribute):** 主属性是包含在任何一个候选码中的属性。如果一个关系有多个候选码,那么这些候选码中出现的所有属性都是主属性。例如,工人关系(工号,身份证号,姓名,性别,部门)中,如果{工号}和{身份证号}都是候选码,那么"工号"和"身份证号"都是主属性。
+- **非主属性(Non-prime Attribute):** 非主属性是不包含在任何候选码中的属性。这些属性完全依赖于候选码来确定其值。在上述工人关系中,"姓名"、"性别"、"部门"都是非主属性。
## 什么是 ER 图?
@@ -40,7 +214,37 @@ ER 图由下面 3 个要素组成:
下图是一个学生选课的 ER 图,每个学生可以选若干门课程,同一门课程也可以被若干人选择,所以它们之间的关系是多对多(M: N)。另外,还有其他两种实体之间的关系是:1 对 1(1:1)、1 对多(1: N)。
-
+```mermaid
+erDiagram
+ STUDENT {
+ string student_id PK "学号"
+ string name "姓名"
+ string gender "性别"
+ date birth_date "出生日期"
+ string department "学院名称"
+ }
+
+ COURSE {
+ string course_id PK "课程编号"
+ string course_name "课程名称"
+ string location "课程地点"
+ string instructor "开课教师"
+ float credits "成绩"
+ }
+
+ ENROLLMENT {
+ string student_id FK "学号"
+ string course_id FK "课程编号"
+ float grade "成绩"
+ }
+
+ STUDENT ||--o{ ENROLLMENT : "选课"
+ COURSE ||--o{ ENROLLMENT : "被选"
+
+ style STUDENT fill:#4CA497,stroke:#00838F,stroke-width:2px
+ style COURSE fill:#005D7B,stroke:#00838F,stroke-width:2px
+ style ENROLLMENT fill:#E99151,stroke:#C44545,stroke-width:2px
+```
## 数据库范式了解吗?
@@ -73,8 +277,20 @@ ER 图由下面 3 个要素组成:
## 主键和外键有什么区别?
-- **主键(主码)**:主键用于唯一标识一个元组,不能有重复,不允许为空。一个表只能有一个主键。
-- **外键(外码)**:外键用来和其他表建立联系用,外键是另一表的主键,外键是可以有重复的,可以是空值。一个表可以有多个外键。
+从定义和属性上看,它们的区别是:
+
+- **主键 (Primary Key):** 它的核心作用是唯一标识表中的每一行数据。因此,主键列的值必须是唯一的 (Unique) 且不能为空 (Not Null)。一张表只能有一个主键。主键保证了实体完整性。
+- **外键 (Foreign Key):** 它的核心作用是建立并强制两张表之间的关联关系。一张表中的外键列,其值必须对应另一张表中某行的主键值(或者是一个 NULL 值)。因此,外键的值可以重复,也可以为空。一张表可以有多个外键,分别关联到不同的表。外键保证了引用完整性。
+
+用一个简单的电商例子来说明:假设我们有两张表:`users` (用户表) 和 `orders` (订单表)。
+
+- 在 `users` 表中,`user_id` 列是**主键**。每个用户的 `user_id` 都是独一无二的,我们用它来区分张三和李四。
+- 在 `orders` 表中,`order_id` 是它自己的**主键**。同时,它会有一个 `user_id` 列,这个列就是一个**外键**,它引用了 `users` 表的 `user_id` 主键。
+
+这个外键约束就保证了:
+
+1. 你不能创建一个不属于任何已知用户的订单( `user_id` 在 `users` 表中不存在)。
+2. 你不能删除一个已经下了订单的用户(除非设置了级联删除等特殊规则)。
## 为什么不推荐使用外键与级联?
@@ -86,8 +302,8 @@ ER 图由下面 3 个要素组成:
为什么不要用外键呢?大部分人可能会这样回答:
-1. **增加了复杂性:** a. 每次做 DELETE 或者 UPDATE 都必须考虑外键约束,会导致开发的时候很痛苦, 测试数据极为不方便; b. 外键的主从关系是定的,假如那天需求有变化,数据库中的这个字段根本不需要和其他表有关联的话就会增加很多麻烦。
-2. **增加了额外工作**:数据库需要增加维护外键的工作,比如当我们做一些涉及外键字段的增,删,更新操作之后,需要触发相关操作去检查,保证数据的的一致性和正确性,这样会不得不消耗数据库资源。如果在应用层面去维护的话,可以减小数据库压力;
+1. **增加了复杂性:** a. 每次做 DELETE 或者 UPDATE 都必须考虑外键约束,会导致开发的时候很痛苦, 测试数据极为不方便; b. 外键的主从关系是定的,假如哪天需求有变化,数据库中的这个字段根本不需要和其他表有关联的话就会增加很多麻烦。
+2. **增加了额外工作**:数据库需要增加维护外键的工作,比如当我们做一些涉及外键字段的增,删,更新操作之后,需要触发相关操作去检查,保证数据的一致性和正确性,这样会不得不消耗数据库资源。如果在应用层面去维护的话,可以减小数据库压力;
3. **对分库分表不友好**:因为分库分表下外键是无法生效的。
4. ……
@@ -101,53 +317,239 @@ ER 图由下面 3 个要素组成:
## 什么是存储过程?
-我们可以把存储过程看成是一些 SQL 语句的集合,中间加了点逻辑控制语句。存储过程在业务比较复杂的时候是非常实用的,比如很多时候我们完成一个操作可能需要写一大串 SQL 语句,这时候我们就可以写有一个存储过程,这样也方便了我们下一次的调用。存储过程一旦调试完成通过后就能稳定运行,另外,使用存储过程比单纯 SQL 语句执行要快,因为存储过程是预编译过的。
+```mermaid
+graph LR
+ A[存储过程] --> B[定义特征]
+ A --> C[优势]
+ A --> D[劣势]
+ A --> E[应用现状]
+
+ B --> B1[SQL语句集合]
+ B --> B2[包含逻辑控制]
+ B --> B3[预编译机制]
+
+ C --> C1[执行速度快]
+ C --> C2[运行稳定]
+ C --> C3[简化复杂操作]
+
+ D --> D1[调试困难]
+ D --> D2[扩展性差]
+ D --> D3[无移植性]
+ D --> D4[占用数据库资源]
+
+ E --> E1[传统企业 使用较多]
+ E --> E2[互联网公司 很少使用]
+ E --> E3[阿里规范 明确禁用]
+
+ style A fill:#4CA497,stroke:#00838F,stroke-width:3px,color:#fff
+ style B fill:#00838F,stroke:#005D7B,stroke-width:2px,color:#fff
+ style C fill:#E99151,stroke:#C44545,stroke-width:2px,color:#fff
+ style D fill:#C44545,stroke:#005D7B,stroke-width:2px,color:#fff
+ style E fill:#005D7B,stroke:#00838F,stroke-width:2px,color:#fff
+
+ style B1 fill:#E4C189,stroke:#00838F,stroke-width:1px
+ style B2 fill:#E4C189,stroke:#00838F,stroke-width:1px
+ style B3 fill:#E4C189,stroke:#00838F,stroke-width:1px
-存储过程在互联网公司应用不多,因为存储过程难以调试和扩展,而且没有移植性,还会消耗数据库资源。
+ style C1 fill:#E4C189,stroke:#E99151,stroke-width:1px
+ style C2 fill:#E4C189,stroke:#E99151,stroke-width:1px
+ style C3 fill:#E4C189,stroke:#E99151,stroke-width:1px
-阿里巴巴 Java 开发手册里要求禁止使用存储过程。
+ style D1 fill:#E4C189,stroke:#C44545,stroke-width:1px
+ style D2 fill:#E4C189,stroke:#C44545,stroke-width:1px
+ style D3 fill:#E4C189,stroke:#C44545,stroke-width:1px
+ style D4 fill:#E4C189,stroke:#C44545,stroke-width:1px
+
+ style E1 fill:#E4C189,stroke:#005D7B,stroke-width:1px
+ style E2 fill:#E4C189,stroke:#005D7B,stroke-width:1px
+ style E3 fill:#E4C189,stroke:#005D7B,stroke-width:1px
+```
+
+存储过程是数据库中预编译的SQL语句集合,它将多条SQL语句和程序逻辑控制语句(如IF-ELSE、WHILE循环等)封装在一起,形成一个可重复调用的数据库对象。
+
+**存储过程的优势:**
+
+在传统企业级应用中,存储过程具有一定的实用价值。当业务逻辑复杂时,需要执行大量SQL语句才能完成一个业务操作,此时可以将这些语句封装成存储过程,简化调用过程。由于存储过程在创建时就已经编译并存储在数据库中,执行时无需重新编译,因此相比动态SQL语句具有更好的执行性能。同时,一旦存储过程调试完成,其运行相对稳定可靠。
+
+**存储过程的局限性:**
+
+然而,在现代互联网架构中,存储过程的使用越来越少。主要原因包括:调试困难,缺乏成熟的调试工具;扩展性差,修改业务逻辑需要直接修改数据库对象;移植性差,不同数据库系统的存储过程语法差异较大;占用数据库资源,增加数据库服务器负担;版本管理困难,不便于进行代码版本控制。
+
+**行业规范:**
+
+基于以上原因,许多互联网公司的开发规范中明确限制或禁止使用存储过程。例如,《阿里巴巴Java开发手册》中明确规定禁止使用存储过程,推荐将业务逻辑放在应用层实现,保持数据库的简单和高效。

-## drop、delete 与 truncate 区别?
+## DROP、DELETE、TRUNCATE 有什么区别?
-### 用法不同
+在数据库操作中,`DROP`、`DELETE` 和 `TRUNCATE` 是三个常用的数据删除命令,它们在功能、性能和使用场景上存在显著差异。
-- `drop`(丢弃数据): `drop table 表名` ,直接将表都删除掉,在删除表的时候使用。
-- `truncate` (清空数据) : `truncate table 表名` ,只删除表中的数据,再插入数据的时候自增长 id 又从 1 开始,在清空表中数据的时候使用。
-- `delete`(删除数据) : `delete from 表名 where 列名=值`,删除某一行的数据,如果不加 `where` 子句和`truncate table 表名`作用类似。
+**DROP命令:**
-`truncate` 和不带 `where`子句的 `delete`、以及 `drop` 都会删除表内的数据,但是 **`truncate` 和 `delete` 只删除数据不删除表的结构(定义),执行 `drop` 语句,此表的结构也会删除,也就是执行`drop` 之后对应的表不复存在。**
+- 语法:`DROP TABLE 表名`
+- 作用:完全删除整个表,包括表结构、数据、索引、触发器、约束等所有相关对象
+- 使用场景:当表不再需要时使用
-### 属于不同的数据库语言
+**TRUNCATE命令:**
-`truncate` 和 `drop` 属于 DDL(数据定义语言)语句,操作立即生效,原数据不放到 rollback segment 中,不能回滚,操作不触发 trigger。而 `delete` 语句是 DML (数据库操作语言)语句,这个操作会放到 rollback segment 中,事务提交之后才生效。
+- 语法:`TRUNCATE TABLE 表名`
+- 作用:清空表中所有数据,但保留表结构
+- 特点:自增长字段(AUTO_INCREMENT)会重置为初始值(通常为1)
+- 使用场景:需要快速清空表数据但保留表结构时使用
-**DML 语句和 DDL 语句区别:**
+**DELETE命令:**
-- DML 是数据库操作语言(Data Manipulation Language)的缩写,是指对数据库中表记录的操作,主要包括表记录的插入、更新、删除和查询,是开发人员日常使用最频繁的操作。
-- DDL (Data Definition Language)是数据定义语言的缩写,简单来说,就是对数据库内部的对象进行创建、删除、修改的操作语言。它和 DML 语言的最大区别是 DML 只是对表内部数据的操作,而不涉及到表的定义、结构的修改,更不会涉及到其他对象。DDL 语句更多的被数据库管理员(DBA)所使用,一般的开发人员很少使用。
+- 语法:`DELETE FROM 表名 WHERE 条件`
+- 作用:删除满足条件的数据行,不带WHERE子句时删除所有数据
+- 特点:自增长字段不会重置,继续从之前的值递增
+- 使用场景:需要有选择地删除部分数据时使用
+
+`TRUNCATE` 和不带 `WHERE`子句的 `DELETE`、以及 `DROP` 都会删除表内的数据,但是 **`TRUNCATE` 和 `DELETE` 只删除数据不删除表的结构(定义),执行 `DROP` 语句,此表的结构也会删除,也就是执行`DROP` 之后对应的表不复存在。**
-另外,由于`select`不会对表进行破坏,所以有的地方也会把`select`单独区分开叫做数据库查询语言 DQL(Data Query Language)。
+### 对表结构的影响
-### 执行速度不同
+- `DROP`:删除表结构和所有数据,表将不复存在
+- `TRUNCATE`:仅删除数据,保留表结构和定义
+- `DELETE`:仅删除数据,保留表结构和定义
-一般来说:`drop` > `truncate` > `delete`(这个我没有实际测试过)。
+### 触发器
-- `delete`命令执行的时候会产生数据库的`binlog`日志,而日志记录是需要消耗时间的,但是也有个好处方便数据回滚恢复。
-- `truncate`命令执行的时候不会产生数据库日志,因此比`delete`要快。除此之外,还会把表的自增值重置和索引恢复到初始大小等。
-- `drop`命令会把表占用的空间全部释放掉。
+- `DELETE` 操作会触发相关的DELETE触发器
+- `TRUNCATE` 和 `DROP` 不会触发DELETE触发器
+
+### 事务和回滚
+
+- `DROP` 和 `TRUNCATE` 属于DDL操作,执行后立即生效,不能回滚
+- `DELETE` 属于DML操作,可以回滚(在事务中)
+
+### 执行速度
+
+一般来说:`DROP` > `TRUNCATE` > `DELETE`(这个我没有实际测试过)。
+
+- `DELETE`命令执行的时候会产生数据库的`binlog`日志,而日志记录是需要消耗时间的,但是也有个好处方便数据回滚恢复。
+- `TRUNCATE`命令执行的时候不会产生数据库日志,因此比`DELETE`要快。除此之外,还会把表的自增值重置和索引恢复到初始大小等。
+- `DROP`命令会把表占用的空间全部释放掉。
Tips:你应该更多地关注在使用场景上,而不是执行效率。
+## DML 语句和 DDL 语句区别是?
+
+- DML 是数据库操作语言(Data Manipulation Language)的缩写,是指对数据库中表记录的操作,主要包括表记录的插入、更新、删除和查询,是开发人员日常使用最频繁的操作。
+- DDL (Data Definition Language)是数据定义语言的缩写,简单来说,就是对数据库内部的对象进行创建、删除、修改的操作语言。它和 DML 语言的最大区别是 DML 只是对表内部数据的操作,而不涉及到表的定义、结构的修改,更不会涉及到其他对象。DDL 语句更多的被数据库管理员(DBA)所使用,一般的开发人员很少使用。
+
+另外,由于`SELECT`不会对表进行破坏,所以有的地方也会把`SELECT`单独区分开叫做数据库查询语言 DQL(Data Query Language)。
+
## 数据库设计通常分为哪几步?
-1. **需求分析** : 分析用户的需求,包括数据、功能和性能需求。
-2. **概念结构设计** : 主要采用 E-R 模型进行设计,包括画 E-R 图。
-3. **逻辑结构设计** : 通过将 E-R 图转换成表,实现从 E-R 模型到关系模型的转换。
-4. **物理结构设计** : 主要是为所设计的数据库选择合适的存储结构和存取路径。
-5. **数据库实施** : 包括编程、测试和试运行
-6. **数据库的运行和维护** : 系统的运行与数据库的日常维护。
+```mermaid
+graph TD
+ A[数据库设计流程] --> B[1.需求分析]
+ B --> C[2.概念结构设计]
+ C --> D[3.逻辑结构设计]
+ D --> E[4.物理结构设计]
+ E --> F[5.数据库实施]
+ F --> G[6.运行和维护]
+
+ B --> B1[数据需求 功能需求 性能需求]
+ C --> C1[E-R建模 实体关系图]
+ D --> D1[关系模型 表结构设计 规范化]
+ E --> E1[存储结构 索引设计 分区策略]
+ F --> F1[编程开发 测试部署 数据迁移]
+ G --> G1[性能监控 备份恢复 优化调整]
+
+ G -.反馈.-> B
+
+ style A fill:#4CA497,stroke:#00838F,stroke-width:3px,color:#fff
+ style B fill:#00838F,stroke:#005D7B,stroke-width:2px,color:#fff
+ style C fill:#E99151,stroke:#005D7B,stroke-width:2px,color:#fff
+ style D fill:#005D7B,stroke:#00838F,stroke-width:2px,color:#fff
+ style E fill:#C44545,stroke:#005D7B,stroke-width:2px,color:#fff
+ style F fill:#E99151,stroke:#005D7B,stroke-width:2px,color:#fff
+ style G fill:#00838F,stroke:#005D7B,stroke-width:2px,color:#fff
+
+ style B1 fill:#E4C189,stroke:#00838F,stroke-width:1px
+ style C1 fill:#E4C189,stroke:#E99151,stroke-width:1px
+ style D1 fill:#E4C189,stroke:#005D7B,stroke-width:1px
+ style E1 fill:#E4C189,stroke:#C44545,stroke-width:1px
+ style F1 fill:#E4C189,stroke:#E99151,stroke-width:1px
+ style G1 fill:#E4C189,stroke:#00838F,stroke-width:1px
+```
+
+### 1. 需求分析阶段
+
+**目标:** 深入了解和分析用户需求,明确系统边界
+**主要工作:**
+
+- 收集和分析数据需求:确定需要存储哪些数据,数据量大小,数据更新频率
+- 明确功能需求:系统需要支持哪些业务操作,各操作的优先级
+- 定义性能需求:响应时间要求,并发用户数,数据吞吐量
+- 确定安全需求:数据访问权限,加密要求,审计要求
+ **产出物:** 需求规格说明书、数据字典初稿
+
+### 2. 概念结构设计阶段
+
+**目标:** 将需求转化为信息世界的概念模型
+**主要工作:**
+
+- 识别实体:确定系统中的主要对象
+- 定义属性:明确每个实体的特征
+- 建立联系:确定实体之间的关系(一对一、一对多、多对多)
+- 绘制E-R图(实体-关系图)
+ **产出物:** E-R图、概念数据模型文档
+
+### 3. 逻辑结构设计阶段
+
+**目标:** 将概念模型转换为特定DBMS支持的逻辑模型
+**主要工作:**
+
+- E-R图向关系模型转换:将实体转换为表,属性转换为字段
+- 规范化处理:通过范式化消除数据冗余和更新异常(通常达到3NF)
+- 定义完整性约束:主键、外键、唯一性约束、检查约束
+- 优化模型:根据性能需求进行适当的反规范化
+ **产出物:** 逻辑数据模型、表结构设计文档
+
+### 4. 物理结构设计阶段
+
+**目标:** 确定数据的物理存储方案和访问方法
+**主要工作:**
+
+- 选择存储引擎:如MySQL的InnoDB、MyISAM等
+- 设计索引策略:确定需要建立的索引类型和字段
+- 分区设计:对大表进行分区以提高性能
+- 确定存储参数:表空间大小、数据文件位置、缓冲区配置
+- 制定备份策略:全量备份、增量备份的频率和方式
+ **产出物:** 物理设计文档、索引设计方案
+
+### 5. 数据库实施阶段
+
+**目标:** 将设计转化为实际运行的数据库系统
+**主要工作:**
+
+- 创建数据库和表结构:编写和执行DDL语句
+- 开发存储过程和触发器(如需要)
+- 编写应用程序接口
+- 导入初始数据
+- 系统集成测试:功能测试、性能测试、压力测试
+- 用户培训和文档编写
+ **产出物:** 数据库脚本、测试报告、用户手册
+
+### 6. 运行和维护阶段
+
+**目标:** 确保数据库系统稳定高效运行
+**主要工作:**
+
+- 日常监控:性能监控、空间监控、错误日志分析
+- 性能优化:查询优化、索引调整、参数调优
+- 数据备份和恢复:定期备份、恢复演练
+- 安全管理:权限管理、安全补丁更新、审计
+- 容量规划:预测数据增长,提前扩容
+- 变更管理:需求变更的评估和实施
+ **产出物:** 运维报告、优化方案、变更记录
+
+### 设计原则
+
+在整个设计过程中应遵循:数据独立性原则、完整性原则、安全性原则、可扩展性原则和标准化原则。
## 参考
diff --git a/docs/database/character-set.md b/docs/database/character-set.md
index e462a5c97e3..2e65c74a5b6 100644
--- a/docs/database/character-set.md
+++ b/docs/database/character-set.md
@@ -1,8 +1,13 @@
---
title: 字符集详解
+description: 详解字符集与字符编码原理,深入分析ASCII、GB2312、GBK、UTF-8、UTF-16等常见编码,解释MySQL中utf8与utf8mb4的区别以及emoji存储问题的解决方案。
category: 数据库
tag:
- 数据库基础
+head:
+ - - meta
+ - name: keywords
+ content: 字符集,字符编码,UTF-8,UTF-16,GBK,GB2312,utf8mb4,ASCII,Unicode,MySQL字符集,emoji存储
---
MySQL 字符编码集中有两套 UTF-8 编码实现:**`utf8`** 和 **`utf8mb4`**。
diff --git a/docs/database/elasticsearch/elasticsearch-questions-01.md b/docs/database/elasticsearch/elasticsearch-questions-01.md
index fe6daa6926c..2db51f16d7a 100644
--- a/docs/database/elasticsearch/elasticsearch-questions-01.md
+++ b/docs/database/elasticsearch/elasticsearch-questions-01.md
@@ -1,9 +1,14 @@
---
title: Elasticsearch常见面试题总结(付费)
+description: Elasticsearch常见面试题总结,涵盖ES核心概念、倒排索引原理、分片与副本机制、查询DSL、聚合分析、集群调优等高频面试知识点。
category: 数据库
tag:
- NoSQL
- Elasticsearch
+head:
+ - - meta
+ - name: keywords
+ content: Elasticsearch面试题,ES索引,倒排索引,分片副本,全文搜索,聚合查询,Lucene,ELK
---
**Elasticsearch** 相关的面试题为我的[知识星球](../../about-the-author/zhishixingqiu-two-years.md)(点击链接即可查看详细介绍以及加入方法)专属内容,已经整理到了[《Java 面试指北》](../../zhuanlan/java-mian-shi-zhi-bei.md)中。
@@ -11,5 +16,3 @@ tag:

-
-
diff --git a/docs/database/mongodb/mongodb-questions-01.md b/docs/database/mongodb/mongodb-questions-01.md
index 81b7db98890..f60be69b0fb 100644
--- a/docs/database/mongodb/mongodb-questions-01.md
+++ b/docs/database/mongodb/mongodb-questions-01.md
@@ -1,9 +1,14 @@
---
title: MongoDB常见面试题总结(上)
+description: MongoDB常见面试题总结上篇,详解MongoDB基础概念、存储结构、数据类型、副本集高可用、分片集群水平扩展等核心知识点,助力后端面试准备。
category: 数据库
tag:
- NoSQL
- MongoDB
+head:
+ - - meta
+ - name: keywords
+ content: MongoDB面试题,文档数据库,BSON,副本集,分片集群,MongoDB索引,WiredTiger,聚合管道
---
> 少部分内容参考了 MongoDB 官方文档的描述,在此说明一下。
diff --git a/docs/database/mongodb/mongodb-questions-02.md b/docs/database/mongodb/mongodb-questions-02.md
index dcd90d72c4d..4a7af767d16 100644
--- a/docs/database/mongodb/mongodb-questions-02.md
+++ b/docs/database/mongodb/mongodb-questions-02.md
@@ -1,9 +1,14 @@
---
title: MongoDB常见面试题总结(下)
+description: MongoDB常见面试题总结下篇,深入讲解MongoDB各类索引(单字段、复合、多键、文本、地理位置、TTL)的原理、使用场景和查询优化技巧。
category: 数据库
tag:
- NoSQL
- MongoDB
+head:
+ - - meta
+ - name: keywords
+ content: MongoDB索引,复合索引,多键索引,文本索引,地理位置索引,TTL索引,MongoDB查询优化,索引设计
---
## MongoDB 索引
diff --git a/docs/database/mysql/a-thousand-lines-of-mysql-study-notes.md b/docs/database/mysql/a-thousand-lines-of-mysql-study-notes.md
index 42384aaec7e..b62c108458b 100644
--- a/docs/database/mysql/a-thousand-lines-of-mysql-study-notes.md
+++ b/docs/database/mysql/a-thousand-lines-of-mysql-study-notes.md
@@ -1,8 +1,13 @@
---
title: 一千行 MySQL 学习笔记
+description: 一千行MySQL学习笔记精华总结,涵盖数据库操作、表管理、SQL语法、索引、视图、存储过程、触发器等核心知识点,适合快速查阅和复习。
category: 数据库
tag:
- MySQL
+head:
+ - - meta
+ - name: keywords
+ content: MySQL学习笔记,MySQL命令大全,SQL语法,数据库操作,表操作,索引,视图,存储过程,触发器
---
> 原文地址: ,JavaGuide 对本文进行了简答排版,新增了目录。
@@ -621,7 +626,7 @@ CREATE [OR REPLACE] [ALGORITHM = {UNDEFINED | MERGE | TEMPTABLE}] VIEW view_name
### 锁表
-```mysql
+```sql
/* 锁表 */
表锁定只用于防止其它客户端进行不正当地读取和写入
MyISAM 支持表锁,InnoDB 支持行锁
@@ -633,7 +638,7 @@ MyISAM 支持表锁,InnoDB 支持行锁
### 触发器
-```mysql
+```sql
/* 触发器 */ ------------------
触发程序是与表有关的命名数据库对象,当该表出现特定事件时,将激活该对象
监听:记录的增加、修改、删除。
@@ -686,7 +691,7 @@ end
### SQL 编程
-```mysql
+```sql
/* SQL编程 */ ------------------
--// 局部变量 ----------
-- 变量声明
@@ -821,7 +826,7 @@ INOUT,表示混合型
### 存储过程
-```mysql
+```sql
/* 存储过程 */ ------------------
存储过程是一段可执行性代码的集合。相比函数,更偏向于业务逻辑。
调用:CALL 过程名
@@ -842,7 +847,7 @@ END
### 用户和权限管理
-```mysql
+```sql
/* 用户和权限管理 */ ------------------
-- root密码重置
1. 停止MySQL服务
@@ -924,7 +929,7 @@ GRANT OPTION -- 允许授予权限
### 表维护
-```mysql
+```sql
/* 表维护 */
-- 分析和存储表的关键字分布
ANALYZE [LOCAL | NO_WRITE_TO_BINLOG] TABLE 表名 ...
@@ -937,7 +942,7 @@ OPTIMIZE [LOCAL | NO_WRITE_TO_BINLOG] TABLE tbl_name [, tbl_name] ...
### 杂项
-```mysql
+```sql
/* 杂项 */ ------------------
1. 可用反引号(`)为标识符(库名、表名、字段名、索引、别名)包裹,以避免与关键字重名!中文也可以作为标识符!
2. 每个库目录存在一个保存当前数据库的选项文件db.opt。
diff --git a/docs/database/mysql/how-sql-executed-in-mysql.md b/docs/database/mysql/how-sql-executed-in-mysql.md
index 0b01d9a4da3..13349f408b9 100644
--- a/docs/database/mysql/how-sql-executed-in-mysql.md
+++ b/docs/database/mysql/how-sql-executed-in-mysql.md
@@ -1,8 +1,13 @@
---
title: SQL语句在MySQL中的执行过程
+description: 详解SQL语句在MySQL中的完整执行流程,从连接器身份认证、查询缓存、分析器语法解析、优化器生成执行计划到执行器调用存储引擎的全过程。
category: 数据库
tag:
- MySQL
+head:
+ - - meta
+ - name: keywords
+ content: MySQL执行流程,SQL执行过程,连接器,解析器,优化器,执行器,Server层,存储引擎,InnoDB
---
> 本文来自[木木匠](https://github.com/kinglaw1204)投稿。
diff --git a/docs/database/mysql/images/ACID.drawio b/docs/database/mysql/images/ACID.drawio
deleted file mode 100644
index e8805b8d958..00000000000
--- a/docs/database/mysql/images/ACID.drawio
+++ /dev/null
@@ -1 +0,0 @@
-7Zhdb5swFIZ/jS9b8R24BAJdp1Wamotply6Yj9VgZkyT7tfPBjvAIFK7LcqqNZHAfo99bJ/zxLIDzLA63FDYFHckRRgYWnoA5hYYhq4ZHn8J5XlQPM0chJyWqWw0CrvyB1I9pdqVKWpnDRkhmJXNXExIXaOEzTRIKdnPm2UEz0dtYI4Wwi6BeKl+KVNWDKprbEb9AyrzQo2sO3LBDzB5zCnpajkeMMzYiePYHcwVVL7kQtsCpmQ/kcwImCElhA2l6hAiLGKrwjb0i09Yj/OmqGYv6fD5/ttDl3wtnor7T9S9tRq/C6/swcsTxB1Sy+gny55VgPolIuFEB2awL0qGdg1MhHXPkeBawSoszVmJcUgwoX1f0wp9y9twvWWUPCJlqUmNhKgioonKI2JJISs5hm0ryxmp2cTl8JF6DKsSC/A+IhZQWNYtn/sdqYm070hH+5kWjHGeDNv0+YOHSDxEg/Y6JyTHCDZle52Qqjckbd80zgbvvDj1bxuBHGGZAZmUJ0QZOkwkmZEbRCrEKHepKasp6Xj+pb4fWTRtqRUTDg1LilDynx99jwzwgsTgFUjoK0g4mMmIzthwvndEGa7a/qfN46vpXnMYjbyUi7cPohgEIXBdENnAjYAXi4K/BZ4GIge4GvA3qo2nxgRDnpSTv0pnZovvKTpPcfdyat8+nZs5nYa2pFM3Vuh0zgWncSY4wwmcFghcQWPEnxsQWJeBM4XIzZI/3zrfPISm9a9BaJ4JwtsJhB7wfOBx9jbAd0AQjBCeYm+Q2wbWvz+DNcIHj+fYfrPMSN4JX9lmvUsTbp2J8O2EcM4zL+j9fusB177QGcBN0DuEa9vsxSF0FhD64e12kX2+PjZP8fpxbpJ3KUFc5jWvJjxqiOuBiFbJr4a+NFRlmophVpkaqVMMyMut8Z+dEo8EqDuMvSTHW7vCvB4cXh0vzL1t8q+EGf0E
\ No newline at end of file
diff --git a/docs/database/mysql/images/AID-C.drawio b/docs/database/mysql/images/AID-C.drawio
deleted file mode 100644
index 4ac724c8404..00000000000
--- a/docs/database/mysql/images/AID-C.drawio
+++ /dev/null
@@ -1 +0,0 @@
-5ZjZcpswFIafRpfJAGK9BBvStM1Fm+mS3skglkYgCvKWp68EwoCNM0k6HmdqZwZL/znazvmkIAM4yzc3FSrTOxphAjQl2gA4B5qmKprDv4SybRVHga2QVFkknXrhPnvCXUupLrMI1yNHRilhWTkWQ1oUOGQjDVUVXY/dYkrGo5YowQfCfYjIofoji1jaqrZm9foHnCVpN7JqygUvUPiYVHRZyPGABgMzCAK7Neeo60sutE5RRNcDCfoAzipKWVvKNzNMRGy7sLXtgiPW3bwrXLAXNSBfy+/et6cCXX26ib/o33+pqyvZS822XTxwxMMjq7RiKU1ogYjfq16zZix6VXit9/lMaclFlYu/MWNbmWu0ZJRLKcuJtOJNxn6K5teGrD0MLPON7LmpbLtKwartoJGoPgxtfbOm1rWrHzELU1mJacEClGdEWD9i5lUoK2q+/jtaUGm/p8sqFPNOGeMIagZ0+YNHVTyEQ32dUJoQjMqsvg5p3hjCunEN4rZ3Xhz2b2jecIQ2LqpYcM0q+rgDjzPhHaa1y1E3sWO51OTuQVWC2TN+eusnEj0YQEJzg2mOeQC5Q4UJYtlqvE+Q3G7Jzq9Hjhckda8gUM56hcgSdxtpD8keOBGydZoxfF+iJhZrfiiN4dpts4PsJwTVdUdCRsiMElo1I0B95uqOtcvHwGI2H5m5gd5+3gVTR4lZ4YrhzbM5llYNykNNHuKaIg+tdX8kQkNq6eA41HTlRFzACS5MwmRER4CYf5a0M1zVze7i8VVUp9z0Rl5KxLcL/AB4M2DbwDeA7QMnEAV3DhwF+CawFeBanY/TjQnaPHWd/Buie/DFhvh7E3wvhP2/QNTWx4hah4iq2gSi5qkI1U9E6GxAqA48WyDp86cFPP08hEYI23E4SWho40V8WSRCY0yioZ6bRONEJN4OSHSA4wKHA2gB1wSe15N4DMBWrktUvH0GU5i3PZ7iII5jLZzEPDIXpmFeGubjdwJonhtz80SYzweYc6h5QW1OXgfYxpleCewQT5O4sA3dUC6LRB2+t3/91gGJ7u38gAC+PLZ3ORmls6AF3su9lBDJkoJXQx40zHVPBCsLEXGlIc+iqLmMT3E1vqAPrpzahb0zQmsPHO0QHGfqVvN6bni1/ymnsQ1+L4P+Xw==
\ No newline at end of file
diff --git a/docs/database/mysql/images/concurrency-consistency-issues-dirty-reading.drawio b/docs/database/mysql/images/concurrency-consistency-issues-dirty-reading.drawio
deleted file mode 100644
index 6e4e61ba50c..00000000000
--- a/docs/database/mysql/images/concurrency-consistency-issues-dirty-reading.drawio
+++ /dev/null
@@ -1 +0,0 @@
-7Vpbk6I4FP41eWxLQG6P4KV3e2d2ZrZramYfIwZINxIWYqv96zeBIGKi7Uwp2jOWVZqchJPL+c53TiLAGM5X9znM4o9khhKg92crYIyArpuuzr65YF0JbMOsBFGOZ5VIawSP+BUJYV9IF3iGilZHSkhCcdYWBiRNUUBbMpjnZNnuFpKkPWoGIyQJHgOYyNJveEbjSurodiP/A+EorkfWLLdqmcLgOcrJIhXjAd2YWJPJxKma57DWJRZaxHBGllsiYwyMYU4IrUrz1RAlfGvrbauem+xp3cw7Ryk95oE//x4YTj8YYPr11f7y9OmfuWPdibUUdF3vB5qx7RFVktOYRCSFybiR+uWaEdfaZ7WmzwdCMibUmPAJUboWtoYLSpgopvNEtLIJ5+vv4vmy8i+v9My6OlptN47WohaSlAqlminqEzjHCe/wgKifQ5wWbDkfSUrq/mSRB/yJmFKGKN00PPbFNol/8Q5FLyIkShDMcNELyLxsCIqy6ySstLPitn5T98UIsg2EWYp62H0bX0Md5hGiB/oNqn7cKlsDCAvfIzJHbIdYhxwlkOKXNqih8I1o06/BBysIiPwAXITeF5gsUI36XfwkCXNdjpNljCl6zGC5D0tGHm0UwCKr/DnEK44mP8RJMiQJyUtFRhgiKwiYvKA5eUZ1S0pS9L6w8IJyilYHrSdaN1whuFSzRH3ZMFMtirdIqZad3N7Gzd7ntLejXZm96/nc4kHX8WBwZDwwryseuBJBeHWQ2MHQBzhliWObERIcpawcsN1CzNd97jmYpWaeaJjj2ayCGCrwK5yW+rjlM4KZpbhy0wfmSIGFhA/nb1K0LZ4RSdp1wuWgV0rEssmKxda0MksV4dz1ezzKtEmnqh0NGKH8MzdBo9lU6awfJ2FYMFTvom0zv58H4EDGH/vRZRprSEp7O1RJkSnUf7PIZFg7kcmWI5OtiEyDE0SmEcqGeP0AA/8B+/lf3/wv0+fa0N0Gpl3TKrn35/jelPleuW69I3pXDm7K6d/YBK4P3DEYW8BnBe8SZH8kucumO5pB20bdj8iDTGu3SdE4CdFaKp3nJ1oZCpxoNfn+4Ea0P0S05u6Rr0OiVRraVvj8APgecHzu/I4HPE3O8Nhqadu0ahNu2VuIjucFFZbaFP9LIkQzdhFiSghRHRKNcyHEOQIh8j3BDSGdIUR3ukOIMjSqT4Dv5BbhhEle7QZvnuo1s6M07+A0b9d83Vzr6ooY3+01n/OeHfSKMfE2IyjOfeqOtpKHLkYRt/z/TPm/uxu7L5z/a4oDwC0YnO8/n4sHg80S2rc8LIf0hmViPwSuXUomwGeFSSnxHuALvOfvbgDdSnhmP81bILH+W/A3G0qj3BWlFZkl+pqZrcqNrNtZKSp/xzbwR8DVeMHzgetuhnpiQ5WvifSC9ErPFXtupA4AecsL/PJT6qSMy0kqlBbPiAYxaO4i3z349V22G8jg1xwF+rVzHVWMS2RCpzly7L8vPiK/MDrKJg5N8vavjexeihtqCQZ73cuyO7tMZNXm1bXqkrl5PdAY/w8=
\ No newline at end of file
diff --git a/docs/database/mysql/images/concurrency-consistency-issues-dirty-reading.png b/docs/database/mysql/images/concurrency-consistency-issues-dirty-reading.png
deleted file mode 100644
index db90c6ea22c..00000000000
Binary files a/docs/database/mysql/images/concurrency-consistency-issues-dirty-reading.png and /dev/null differ
diff --git a/docs/database/mysql/images/concurrency-consistency-issues-missing-modifications.drawio b/docs/database/mysql/images/concurrency-consistency-issues-missing-modifications.drawio
deleted file mode 100644
index 68c79b9da73..00000000000
--- a/docs/database/mysql/images/concurrency-consistency-issues-missing-modifications.drawio
+++ /dev/null
@@ -1 +0,0 @@
-7VrbcqM4EP0aPcZl7vjR+DI7U3PZ3dTW7j4qWIASgRiQYztfvxJIXAzOkKnx4JqlUuVILalB3UenW10AYxUf32UwjT7RHSJAn++OwFgDXbfnC/4rBKdS4BhaKQgzvCtFDcE9fkFSOJfSPd6hvDWRUUoYTttCnyYJ8llLBrOMHtrTAkraT01hiDqCex+SrvRvvGNRKXV1p5b/hnAYqSdrttzwA/SfwozuE/k8oBtbe7vduuVwDJUuudE8gjt6aIiMDTBWGaWsbMXHFSLCtMps5brthdHqvTOUsCEL3n82DXfum5j99eL88fjlz9i17+RecnZS9kA7bh7ZpRmLaEgTSDa11Cv2jITWOe/Vcz5SmnKhxoWPiLGT9DXcM8pFEYuJHOUvnJ3+keuLzr+iM7NUd31sDq5PshfQhEmlmiX7WxhjIiZ8QMzLIE5yvp1PNKFqPt1nvlgRMcYRpVvGkv9wI4kfMSGfhZSGBMEU5zOfxsWAnxdTt0GpnTeb+i3dk08orSdMdtEpUpSr97jkCYV9mIWIvTLPrKDDTySiMeIW4usyRCDDz+33gPJshNW8Gh+8ISHyBrhIvc+Q7JFC/Tl+COFHV+DkEGGG7lNYbPvAyaONApin5XkO8FGgyQswIStKaFYoMoIA2b7P5TnL6BNSIwlN0A1j4RllDB1fR0PXe3JBxRWSSzVb9g81MylR1CAlJfvh/jYmf1/T37Z7Y/5WQXmKB6PHA3NgPLBGjQeLDkEsVZA4w9BH+MATxzYjEBwmvO1z4yB+1j1xmDBPzZZyIMa7XQkxlOMX+FDoE55PKeaeEsotD1jrHiwQ8TivStEaPCOTtNuBSw+FyGRX7rjOIZtAeuUAXyScu/lMRJk26ZS9wYCRyn8XLqg1W3061XIaBDkH8Tnaqvf7fgCaXfzxf3qXxmqS0r4dqjqRKdD/Z5HJsM8ik9ONTE5PZDKvFpnsKTLdSGSyBkYmY8zIZPUSg9a9707E8CZisM6vKGMTg9O9omxM4C2B64GNBdwlWGrdjIQbgLVd2+/Chr+laHjS0oelNiP9kgjRjHOEWIMuNca1EOIOQAifYBOBiIeMt0JWmWLCzBiY0d2RMdNDGVO6MVJhVB+Yb2jjlkb1Ls1MtbLr1Ub1nsTj59bKjIkiboUihhbLtFGrZQrBU7XsVqplF66odbVMU4Wt7y2PXb8Epk01sOtEHFNZtkpKx66BuVPEuZWIM7QIpjmjJqVTGexKZTD3/MI6Njf01MGm68ePc7hzXqEY+/qhttAqa1nAtcByVdS3VmDhFJIt8HhjW0iWH+AzfCc+uWsWvBogsb/uxQdphVPu8sKL3BNzzUiPhSHVeFUl2zjAW4OFJhpLDywW1aMe+aOKr/tmftLB4m0U0y7kvT1ANmS/cQq84q/QyTiX00QqzZ8Q8yNFlb8C+PXFGfjNLvg1twf92tvRz7v1N5Fljlx/d2ps/gM=
\ No newline at end of file
diff --git a/docs/database/mysql/images/concurrency-consistency-issues-missing-modifications.png b/docs/database/mysql/images/concurrency-consistency-issues-missing-modifications.png
deleted file mode 100644
index 1718e7ce0dc..00000000000
Binary files a/docs/database/mysql/images/concurrency-consistency-issues-missing-modifications.png and /dev/null differ
diff --git a/docs/database/mysql/images/concurrency-consistency-issues-phantom-read.drawio b/docs/database/mysql/images/concurrency-consistency-issues-phantom-read.drawio
deleted file mode 100644
index 350470274c5..00000000000
--- a/docs/database/mysql/images/concurrency-consistency-issues-phantom-read.drawio
+++ /dev/null
@@ -1 +0,0 @@
-7Vptb+I4EP41lvZOKsIJeftIgHRV7UordU/78uXkBudl68TZxBS4X3+2YxNCQkvvSqG7CAnsx/bYmXk8M3YA5iRbXZeoSD7SOSbAGM5XwJwCw4BwaPMfgaxrxDGtGojLdK46NcBt+g9W4FChi3SOq1ZHRilhadEGQ5rnOGQtDJUlXba7RZS0Zy1QjDvAbYhIF/2SzllSo67hNPh7nMaJnhnaXt1yh8L7uKSLXM0HDDOwgyBw6+YMaVnqQasEzelyCzJnwJyUlLK6lK0mmAjdarXV44I9rZt1lzhnhwy4uV0Ff/3tf86+uHH86frm+3v680pJeUBkofQBZjZwA+DxggVcC4wtgXgW8IeyaQrGM/VAbK2ViOdcp6pKS5bQmOaIzBrUl4rCYilDXmv6fKC04CDk4A/M2FoRBC0Y5VDCMqJa+VOW669qvKx8E5WBpavT1XbjdK1qBN1h4m9sNaGElnLR2lqmH9GcqXnhSNUDlKVEyLjBzC9Rmlf8iT/SnOr+dFGGYkTCGGeqYZlj/sWVL75Eh2oQUxoTjIq0GoQ0kw1hJbsGUS2dF7flW4avZqgVLLS619gKqvQ69lnYUHsKlTFmj/QzN5TkWx3TDHMl8nFqn18NB+bIVrJKTBBLH9prQ2ofxpuxG3GfaMpX3XShUVTxtWyRlRe2Zm0gSeFn0Nno0rlDVUK4axGUXCYpw7cFkupbcu/WJhyqitrfROlKENePUkK26BNF2A5DjlespPdYt+Q0xzucss6IUw+4ZHj1OKv2sgBaypcpXw9tVV82nlNDyZbT1FgfR1oUeK69zYu9j2lvxzwze496wtUI+GPg+jJcjcEYwg4FuAJY29b9Jtyyt4IQSeOcV0OuMMxxX6gz5cnDWDVk6XxO9pGrHfN+SYbYOwRxrIMIYh6LINYBBOn6iAtBXosghntigthvKXc9V0a8SGLqHpiYwj0EOzgL/V98cXoyDJsIb1EVKG8xyf65EGc5aYWrSpqNq34IR8VKak6381IsfkNOK/buzz+0QL6+Wmbd3EvUD+I0s5O3HOyCSsxXhe6kPEGvQuTlUmGWD6zp2z4s9XgpdWWgnnhzQG6xdb+TeOwsBA3Xbvu1cz8ZuZdM+TVPRoZz4kzZe2tx7hwo8CJxTR9Ang5s1ikDm75/vUS23ymyeU9GNgeOWp5MXxH/19CmF270Sj1+4IPdK+58kfHC5l6+4WHjAOHTUbAT9CLjNwt65m7Qc7tBz+kJeqNjBT1NskuW8zr3gSfPcmDPBbB6bTWRFz8T4DkSCYDPC4FExjfoAV2L9446QN2VB8U7c0+8AzMH+FPgQVEY+8DzNlP94FPJV5yDMO9w8TzunfaExh4im6q+tQt8+ZEyGY8PNFdCq3vMwkS7yl+B/ObuVdaoS37o9rAfHo39o1Mk+edizJdJ1q1Dk3XjpMl6915bJTHdtx2XJOZZ29rq3FAfLYnh1ebvHnV+2/ynxpz9Cw==
\ No newline at end of file
diff --git a/docs/database/mysql/images/concurrency-consistency-issues-phantom-read.png b/docs/database/mysql/images/concurrency-consistency-issues-phantom-read.png
deleted file mode 100644
index 4bea3c32953..00000000000
Binary files a/docs/database/mysql/images/concurrency-consistency-issues-phantom-read.png and /dev/null differ
diff --git a/docs/database/mysql/images/concurrency-consistency-issues-unrepeatable-read.drawio b/docs/database/mysql/images/concurrency-consistency-issues-unrepeatable-read.drawio
deleted file mode 100644
index 212d68f7f92..00000000000
--- a/docs/database/mysql/images/concurrency-consistency-issues-unrepeatable-read.drawio
+++ /dev/null
@@ -1 +0,0 @@
-7Vptc5s4EP41+hiPQYDxR/BLern27nq5m14/3cggQImMKMiOnV9fCYQBQxy3jR3SejyDpZVYIe2jfXYFAE6Wm+sUJdEH5mMK9KG/AXAKdH1kQHGVgq0SQLMQhCnxC5FWCW7JI1bCoZKuiI+zRkfOGOUkaQo9FsfY4w0ZSlP20OwWMNocNUEhbgluPUTb0k/E51EhtfVRJX+HSRiVI2vWuGhZIO8+TNkqVuMBHc6t+XxuF81LVOpSE80i5LOHmgjOAJykjPGitNxMMJVLWy5bcd/8idbdc6c45sfc8NsfBrSHnkH4v4+jj3d//r20rSs1l4xvy/XAvlgeVWUpj1jIYkRnldTN54yl1qGoVX3eM5YIoSaEd5jzrbI1WnEmRBFfUtUqHjjd/qfuzyufZWVgltXppt443apawGKulGqmqs/RklDZ4QZzN0UkzsR0PrCYlf3ZKvXkHRHnAlG6CR1xEYskL7JDNggZCylGCckGHlvmDV6Wd50HhXZRrOs3dVeN0LaBMktWDvvUwpdQR2mI+YF+RtFPWqU2gLLwNWZLLFZIdEgxRZysm6BGam+Eu34VPkRBQeQb4KL0rhFd4RL1+/ihVGxdiZOHiHB8m6B8HR6E82iiAGVJsZ8DspFocgNC6YRRluaKYBBgy/OEPOMpu8dlS8xi/LawsMYpx5uD1lOtO1+hfKlmqfpD5ZlKUVRzSqXsxe0NL/Y+pb1trWf2Lp/nwgfn5gPjSD4w+8UH45aDcEqS2MPQe7QQgWPTI1ASxqLsidXCYq+7cucQEZo5qmFJfL+AGM7II1rk+qTlE0aEpaRy0wXmtAMLVA7n7kK0mp9RQVo/4XJwV7Ycyy4qVkvTiCy7HM7VcCBZpul0itrRgFHK/5ImqDSbXTrL21kQZALV+2jbPd/3A9Bo40/86W03Vjkp7XmqajFToP9izAStPWYatZlp1MFMxgsw05c7///P0EE4SG+tNWf/DBm/0joij/4y05vwPnXgwh8iL/NI8oK9Ii+z03do7ZT44ju+yXeYr+g7Og09amcxMwO4DrBdMDOB7QBHawctYra8adpuE9bsrUTHxzVdWGo6rZ8SIRrcR4h5VN4DT4UQ+wiEtAnogpCzIUS3z4eQKU4mZHuDPPeGuOnvn9yPi/snkpqehh/7kPh+Zi9R/yy1az1LTPX2jr6cXJ3upFLv4PhTnVx15wfGW9qgv1h+oHUkCN1W7FeGoHWnCJfjhZc+XtDtV04RtI4c4cIXp3vTcU6+6DT4bgr1kF9E+iZwJnnsPwHjUS6ZA1cU5rnEuUFrdC2/WAC6RWXwv0gbILG+rOT7/NwoV1luRWGJoQaTTb6QZbsohfn/bATcKRhrsuC4YDzeDXUnhso/jhh4cU9TjydorAPIUNVru8DNf7lOLnw5i5XS7B5zLypd5c8Afn28B36jDX7N7kC/drJoybxES32Ilg4GQc/nXKPXipYOPvflQPWF/Ydlni1aEtXqi7TiJV311R+cfQU=
\ No newline at end of file
diff --git a/docs/database/mysql/images/concurrency-consistency-issues-unrepeatable-read.png b/docs/database/mysql/images/concurrency-consistency-issues-unrepeatable-read.png
deleted file mode 100644
index c734138cf8e..00000000000
Binary files a/docs/database/mysql/images/concurrency-consistency-issues-unrepeatable-read.png and /dev/null differ
diff --git "a/docs/database/mysql/images/\344\272\213\345\212\241\347\244\272\346\204\217\345\233\276.drawio" "b/docs/database/mysql/images/\344\272\213\345\212\241\347\244\272\346\204\217\345\233\276.drawio"
deleted file mode 100644
index 4f649c952cc..00000000000
--- "a/docs/database/mysql/images/\344\272\213\345\212\241\347\244\272\346\204\217\345\233\276.drawio"
+++ /dev/null
@@ -1 +0,0 @@
-7XtZd6u4tu6vyRj3PtQedAb8KNEZMGBs09hvmEZgYzCNafzrj5Q4WWsl2VW176m656FO1soApsSUNNtvCuWFla6T1ka33KqTtHxhqGR6YeUXhqEpZokvhDK/UZYU+0ZAbZE8O/0g7IpH+v7mk3ovkrT7pWNf12Vf3H4lxnVVpXH/Cy1q23r8tVtWl7+OeotQ+oWwi6PyKzUokj5/o4qM8IO+SguUv49M888Fn6L4gtr6Xj3He2FYlVdVVXxrvkbvvJ4L7fIoqcefSKzywkptXfdvd9dJSksi23exvb2n/pvWj3m3adX/mRe0zJgy2oFeyjRhNdZm84C/MfwbmyEq7+n7Ol5n28/vEsITv5HbeC6LKknbFxaOedGnu1sUE/qIDQPT8v5a4ica356IVNJkffogfMjKufeYS/qkZ3XVP02CXuDn7pL2MREbRRqLspTqsm5fp8GqqsJLEqZ/XfhTFkPa9un0E+kpCC2tr2nfzrjLs5VfPtX7NFqWFv+1eKOMP4yAZp+qy382gMWzY/Q0PPTB/Yfw8c1T/v+BLoRvVMGXPRHLLap+0Qnf3InZwPhNPAA3tuj0fxZ41Xhs6sf1/77KiyJy/i2LrkU5v/W+pm1bjCm2UazP176YaXQlenyyvvVkxLQtss8tb/3xsltURN+/3BdX7NMMVaUjmVp9JfP/jkvXd3WFnvevg/2Y73O5ZLpV3V6j8q2tTPs+bX/DMokL/OqXdmwC/W/EUIl1kEbqNv3U0rdR1WW4//ubxBhJ61i3ya9cf30xSeO6jfqirj6/mRTdrYyeki2qN/t+XUZZR/3n3j984bdP6mM4/k0SDLGx1xtOfNMg/yE1Hj2vr5ZBBPWtZbxJ8NW1CHt68VzLF0bK4gVSL6L6ovAvS/FFVF4U8QXwLyL9oggvEL4slz/1wRTwAhjSB8ovQHq94Uh/PAZFUcQWcG8R/2ff54id4W2av04dk9/s+p38KewQof9xqPndiPEWjp9h5sn5m7j5H4cPnOR+DR889TV4LL8JHu8B5S+PHe/Z9Cf5pQnOc8/Huu3zGtVVVCo/qLB9C9NP4f3os67r21O8Z+xs8zNCR/e+/h3hd31bXz5yKPN7gZpM7Xfl3KYl9rTh1xT9ndCer27q4tUP3vUjfNLP8lPM7up7G6fPt37OmZ8YceIfMOqjFqX9F0avOvxYz39Drdwfp+cfWqT/2F0+eUiWZUwcfyjvp5aEP/EL/rOO/wLfEahPqVf8JvHS3/jO4m/zncVXIX+NeCTJwY8o932s+tk1fhHoM/r/LP0nKSoLVBFwhSX6Cq2IJAsMT8Gz4Vokyau3fqfZXz34d9HUX6K65TtKeiqPo79RHrX4qjzmb1PeNwD2a0pjqN/+4ap7DxwfgexrxhK/cbq/T2/i/yasn+uRL3nmX38u03xhteD+kNW/yX5/VdL6sM4/UVMW19fy/MN11tEpLTd1V7zhbPlU932NoTosSQP8gM6/5jD884379cQoYNTd3rYNsmIipgNfhwTvVOqdgu+TqI8wXH57ZNTba2FS+NDZjpSpoRrgH3vn5YqH8J1OHiX874Cvsry5rtb4RpW8UnH9LRfemXiIls5u2mvZZGB+INH0XNkXV8q97IrGlmpmU2/2W/6+M8eWVm6Zv9UbrfBNVBtZfM03uW6Aws435GXj6IjobpfsmXrwi4Jets7JW8zHPkjTZdI9Hsub8ChLIGid34HsAK8jKC/8FWetccHJ9rg6IWs4dMPILy+9JLdTH8Pbwcbtlih0sAJa+kjwiuF+7egAjnCNVBlbjnrAvzGcOaWIBIuaLo2CHvlmPu8zjhhIc4ndw21qz6ZGBuO3QEZaqM0X3GZrUwu2W2AjfaM1HqbA/dUS4F02OCQhiUU2ZQOvyC9nRaTk/JpgDkoB1uBI5rXUmSTc7kRXB5vRrmRd4qTtyBY5ozLKGazgIDbrldsDYVT49bhmMVEGKSMZlwgOyPG17tTp2qiBhV30LZkRcJsBbWLXB2v8tEFIrlGytrS1q/qSextRjZRHPgPbFIhjMOoeT4S9GzstKPLo7hzSXPX0oFDWO2hvJrZh1CAfhd3d5lbLc2Az6syZwLiDfGT9fRSW6R3exkWe5HUtudAIK85aK3qnuMYNcy5GK+dPg8S4eh+GkQEv0HTPrXU58Erpz2fHPc/+cdaBuVvcYivmfblm88ttpx+GlVvbA+aRbeid1QAy2eoeHGdP2wuTvV1m80Oc0+0hlW+WWE2aZ+l0MS7AXd/R1Q2AyGTqjkXSqFZSgJYhWG8DWr2IAHvHIWJW5ggjE/OvuwrBsWzlYJw80G59Wu3EM7DuYBXLwRzjLv50a4hyHU6SZ2Iv7NRmJ1qQ03GDQAUOgTwPZoXTkJshs5ILefkadxgYUEsQQGJm614JcKZWI/wbYn+BYRFt8X1vr/DDdWNXzYLCSlhuV8YydSFSMjB0pv64+SIMOGlTHeiWWgERGDgswEU8lO7JiQbxdH+MSw44IIpXpCU+t/W+T6HL2Xxinw5ghLcmZWn5sc0SAdGyt7bAKC3vGWEDqsGlVp6P3V3bJBtZq+7IH4E1k0khGhnoomIXZwfXU3uQHRcJXgDMedGupe2FHwAEIEetly8Oq8bOxn7SKEwwBV1NwQ5HDhjUZFZsrvu67xnLip4aftXODDJWe3MECjIVaRmtSpDWtA9x3DMVEZkrECDFmjwmX7iKBbMjaEfgAv1g0nrGiFA2wHJfMadAw3ljWAfyZQlwBAuk2l3RdXqAvJx5xazmMpaljE6epOlNq1Irjasa5M8nVwE69LCHwzscWTx6+xDUm0xzm1A9wzlFAZm8V8s8tcETzKt6ezTnfKNL+ajWULZCv5jhOd6zrjlhYegKaGAWIu40g42/SBoGC0ltpUla7uNWuXsD3WrSIrwbD0HZHULtUuyw+ofSXoV0Omruno6Vsz8JSJ3NHXYe9UJV/EI8nfnM3fG7yT4y3dJHlSOrLljkO0na9jWZv3zdBki7FgaJwqvh1nrHVqcqYWn6YKrReR0wb2P1/mzTmY/HelAX5YwFswDa1ZokKPRmOay3WdbRI63oYGFYvruze5DUUg/CmugceLDQuxPvC2lwfQse5ilvdA/MUtyiC1m3DkQkv143M449vLRxw4usKO7bYKsLeLtewd2dlGalrh/Yt4bF1UKLRAOFlDUuimZ3b0KrVmb0fsWOAj0KXC0rP7W9oZTDac1Qx9V6p3OsTZ6Pj9IWRA+EwJG3Oy1COE4EaL5IPtrVsu9GjbyZITiW4BpLIrgxrCmx1W7o6XWXjlxwUWenw2t2fvRX3WMjG+4xkk3XCKAy+h5M80e85hTJNLcNgj6IaskHvnVIwdqYwxCFRrehPZI6wc7zna25kA66TjL5Xw96GfoT6F3wX0Gv+BX0vtP+ctD7DoT+kcjILC4fyCg5CoDebjVtNY8jif2g23ruDfqWkdW6oQB/O+nugYAB1Oy2KrQiXdl6W/dhgmI8AMU49sDbKopCKRMqr+No6UCTJX8zzzR0Gj8mScRrwmFYdg9H2JwojCzUdhBS5siH1b3q2bS6C34hURsQHgDOcJ1kjo8g5exRIc6841YbghGgLhyi0heSiNIJgFFlZKXrxZQFsbwdYaiRkeRG8RROCkHOrbw+oaQBPLidcU7s3XihvYt24Z1mgCoXiOJxtg5C2t/qxWDowBnXmVm4ZMce8k0/GjVOyEomnd1bN1hCLJl0pB45vQbGuD6poguMakdQmnbPA4a3phgnwhj69GkWNu02nDAE2uB0aEMTrEeYqnHJzIK1RTK6XK7oxh3l9SgxbbA6FCJFcBUMzPvOZUW7VTOjR/eH4squwna4NoAuQY9pwZ5X94cD/Bw/kqR9AKkpUYdReFQcSK/TPpfPZD9XUh8q8OUuJHFW0M4k3srRI6djHdclGloh3hahgxa7++3AeJvNPDF8nMLQbU+JFt8lhOfOLg7Uuuf7cDyIG0eySyYqkAkqeSBcjRkGkgv6zbiTdiu+HWRGVkUm9S4UWLvG7n50d2XhtLKJhTs4hVsAujle5CJQN91SR1uBK6FLMMaZWASN4QRe12ExRvutoVT7XGhu/UbKHjdDouN53vsYN5m3rRsjVo/8sAcxsvX0FD/SlS5rdvOgEicG5eZYE9xzugJ5xxyioHLGFPVKfOoe8apeK7b5YBPHlcpN0hwG/wA8bTtsGNhylnJ+3VlS84gzB2DF8mZ7W6wFIusTUKQTbhJLwzAJrEKoizxpP90jWqawo9mxsCxTXCXggsK1zS4FGVJjeXq0LMakQH7cU26xcUXAg40Tt9xwTB6Lue5DkjBO12IcuOkKuqk152BgzHUZApWzKmcvbtglk1adbDxiBsMzuJV2F6cTWAEsYol3xymIwZgYK0oUjbtkN/eTWKEVNhLOywKfnipUizBNvEfYpbWNbAaSDbSuS/ZSm46wkuVj9OAUEYSQ5dQ7aKo7szZrHKFheUbrmerG3ALmoF3ZDTb/ZrqoR5pa0xzwdG/YZmNR7EylyHe8ikTQAlVc0VI/UOf5wMAzR+qalNiqB5HpQHoqktsm6YYdCSqPKM+CO1VwagY2l5MsjEkaLd0E498OGJw5m5eAihQ725YiMfdiSlgG6wE60mXdaM7N75VOXXTedpKPzAgGZHdR+eCyg5yBoJPZEaTH9Xwv7Rs9whMQOIVGx7udt+J6Y0W1I5wjbCg1cA75lIeDs3GIp6kLRCCnQ9+SfQqncTwG48ldeco6WjecEDRX9ayFtFGtgUxV1azG6WkALh9RiU9seZ3a9CLZMePDXQV9lBjGofTqXmxzo7KcwlmcnQBItR/fg7z0CRJm16fQe9AbByxg3pW+zK1O61TR005cN/2B2m+Fc2CBWJbHfedYfuTLVEwi3jrSBHHe+g9NanruthsePJ3DcCGukz0pGYtAE6eEiq45jgdnow/pnptbm3OHhJkTjVj8rVR620v5bNEycQB6Xwoav1tqYnjIS526OUtljXg3kun20KfnA0ddpeEsti0yLQsOrurR4nwnlrlGkENhXLhAylnhfJabTlxV19C9cE1RrMSVcN17ezoUDFYOc61I6Sahk+7aDo7LyxjHmleuvaycUO6Ir2mJTGq/lkTkjXymkzmwzQvcu4t9FyWVy59W9Na3B9kajM0o0X1pe7HvHqUabgDbhMJklLR01i+zYY5m4RdN6NlW32chrpEgGphwhBSvVorTqRvTvQ61jFZA4rSNtouG67osFvy1uHOnVL02+nCvMl10xjAn5Wsk5HdSkUIP0RxdJjUwtV1zVi9LaidWd/9KF8U+WN/La7aTkpCU0ycUWwLdMOxq5bGYhe/Qq5QbQ9gBOVwOLqm5djuW8j1uXC6EOVshuK/ykHjipnAkYpkS8V2r80UjkPZwuIbTiZcdNuxjBxuHBDcBLl0hxWl2tLQokVS291C+puTaZ42wjTPncY0jLFO1SC7M0t2d7zEx9htBUj7cTaETHserluuAOC2AChXKqhXsTRB2ubobwBAnekZHqVuQotQMTLfnzMzrKeryhrbhMq205lpuHtgQSKxx4brt+fkibOn9gfJrxerIkIc+EfVGIf6cPuj0vgxbRc/68mEE2K/iWL+iNT+d55UZZudUA5tQa20VrDntpIZrfUszw8iJUbSbmvFx0pC8geuFO21tph5ve5MeE7esZVfyj6qiJ950nBUxjV1Bm4LRRMcOsXSkLPR07VnuaXsar+BarDJfy6q8zvcTf72zYw1cbit6CybRZsNZRWmToQ5EOs/6G18aKnUpRsPx/sAZWT12hzYYIZoNaaAComJP7GwlSh2PYjNnlPnLarvvkyC0TtN4MAOrP2h9ChJ0NuDA5ydK2mrVufRj9mGB8wiJAlYPNXFHIsVKPs/n5hFau+Y4yhqGc9fTRXroAU5WUk6HUnVZH8rdcAxn9rjlYAzlWU5ofdFZStgpHPDPBtVchWH2htxAxHr7CU8ylcIU7YEnIjbxnEiaVAFQYEGwzdJZnuoH7pPEm1ide8YwlyCBVLDyG194LBKvRBuFps5mnU0Zvz+ih+3nzs32FsMdZ4i7lI3Tkm/FwEftweXdZmOzoAPH0RTO90h1kNOyonpPvDnsyA4HBNDnYtUa6+NEbJTpjXznewfLxHKCgOQ/JxXso0x85YTnDkj96T28IallsKID4itX5ZCbj0ggD/EOEeyYlEvjXi2Y/gA9e2svs0t/QXdi4XlcO3m5C1ACtP2FT67BNgTQW16D2uWLINnu/WjLAJ0DrLZndhIHTHe7uK5Ma9EnmJtFQehTHeG2682mrko92lIgYi7Myedw5m/c+np0MWShgXq4Jk6Ii3kZJ98Hnd1q5bbyQKLcDk1gBbPrgxun+WLuv85nPuahFsZXdYS+HVxKJwnrgmw0tb3Lg2gwjjPPXkAj85vGChPlkQappbauPfZUSTbVuo6PkVw3ZX5P1nZmHwYc71WLIJrl7Xqbbnx51SwHKLeNx7NyB3UIbO+YmTDPWR14VhXWYbfXjsfdvvBwIdFGN7WRw6Muo019bIrcD/QLQBv/ESMOCIR7tSy8B0CwWQTm7X6jZGQ28Xra0YLTt+UyWQEHKgeN7AYkrAUc6YqRQ55Lw8LTkYSSgltp5XwCIizWaWe5QnBWkAIIUGRwdIYSFmMkd7EO1FyQ0hh0NmjvBobtKh/vjlsEkUrhqllldoCDOzoJLb2ZFOBgo4Ox7SxceVwfCCA+KDapI6SK8F+mW79YXu6zi1EM56YTzZ5xTlMnAQLZQ95e1U71BY6yb5x4fmbldcaJAK0DLwQc2B9xucaBIy60BEDqsoZeBp6Ve8AOsU89brQ8Ui3B84t+5Ph9EYkdRtp7Q4e7NpROw+PC+SrwwMpSE+qknpqVJ6nN9cYaBOon7YxAYzNd1edrFq2oY7tSebn3UtfYAhvAfYvjs1uCXZ495r73HuzoAYsbVc3W8XTc0qUObcILVUrYeTWWXTSbsXSxLZ7t0rvRjQXG/Py4KmvuKII+TOnVebLj9WWgdwisNLmebyUPaclBuybMjwfMA/FF40mKeXgEVrOPozsljFJMZIqqg5IhVj0khxvNlvIA6BrMqWojgauVuTnRnFn5OEJCgcJ1FgKLKVTRCVHuZbVf0n278iXPhdjX4H4jhQNYG7dNmDfLUywMeD68eU8GVARQAc/ty9t9Gm8bXj4RAMY2bpHOg0FqYACNrbdQ2ouBEHrd3vg7dji4zwcdFsKXHQ7hmx0O4W/b4WD/1OfYf+Q3WIblf1UW+/UbLPf/8xss883pkm8OPvwjlcWJy1+VxfxPK+u746Gfv6BXCSBHoomMy6jrivjT6Z9PQkunog+fbeT+QOT3r/cWeXqK8/Vhfv9q/v/4Cf3to/PvnQjg/uS39p80sPidc0L/zU/y4uLX0MpRnzT7Z8+QLanPMfoTo7/5DBkjfuPlOBK/Ojq+EeUXoLz6PXhZsv9Qdxc+n5pgv34q+M7Y/j53X36jtY+DudwLXL6I4ouyJOdsofyqx+ULEF770C+i9EoRX5Zvx3GxZnFEV1+g9Nr0H/FZvADcjX4/6bv4h1rIx1+E/E5C+KtMhOw+ffxdyFsc+PHHN6zyXw==
\ No newline at end of file
diff --git "a/docs/database/mysql/images/\346\225\260\346\215\256\345\272\223\344\272\213\345\212\241\347\244\272\346\204\217\345\233\276.drawio" "b/docs/database/mysql/images/\346\225\260\346\215\256\345\272\223\344\272\213\345\212\241\347\244\272\346\204\217\345\233\276.drawio"
deleted file mode 100644
index 6ab55e05746..00000000000
--- "a/docs/database/mysql/images/\346\225\260\346\215\256\345\272\223\344\272\213\345\212\241\347\244\272\346\204\217\345\233\276.drawio"
+++ /dev/null
@@ -1 +0,0 @@
-7Zpbc5s4GIZ/jS6TscCAuAQb0sm0s9tmOtvuTUcBGdPIyAtyYu+vX0kIc3TidJOYepLMJNKnE0iP3k8HgDlbba9yvF5+YjGhwJjEW2DOgWHAieGKf9KyKy3uxCwNSZ7GOlNtuEn/JVVJbd2kMSlaGTljlKfrtjFiWUYi3rLhPGcP7WwLRtutrnFCeoabCNO+9a805svSigyntn8gabKsWoa2fuFbHN0lOdtkuj1gmKEdhiEqk1e4qku/aLHEMXtomMwAmLOcMV6GVtsZobJvq24ry4UHUvfPnZOMH1OAJjfh359XhXPNrY9WEH0prr9cyAKymntMN6R6D/W0fFf1kHpHImuBwPQfliknN2scydQHwYSwLfmK6uQFy7geZGiJ+P6tJzJyR3i01JFFSumMUZarVsy5FaD5VGbiObsjVUrGMqKrDfEqpZKya8L9HKdZIZ7zE8tY1Szb5OqplpwLeAzL9MQf0R/yj8xQXCaMJZTgdVpcRmylEqJCZQ0XZe0i2KzfMnzdQr+79Qjck5yTbcOku/+KsBXhuahysq2mRllCzxRHRx8a2CFtWzaQM21txBr1ZF9zPdwioEf8GaNv98aaxGJy6CjL+ZIlLMM0qK1+TYMcxDrPR8bWmoGfhPOdhgBvOGsTIvov333T5VXku4xcWlV0vm0mzncVPAdJKompZq8xblyKqtlDg6Llk+M8IfyRfFaZT47Yo/DlhGKe3rfl7sVRMgd0xKZc93ULMvufDasSLgqFiej5CXTW2zpRhBL1P7CAPwNiXoiAmEBeCIIp8D2AVBLygAerlkA5bvuiPbgpFV6FHCFibWmKMUGL6JA0PaJw46XweNGCVlu19v6sIVvQGJAt57VUC55Eto53bOclRy8uM7ronyxVuqAxc6ZtzGDX6ZV6qEt1CNo/xv+AyjmJL9ym/Fsj3PCEIlY7Qhmp/OA7iK8LYkfvTPM4EL08x7tGtrXMUBxux+4Ab1hWe/n+RP7quep5UD7Bi84K66V3B23HulgsjOgXHGtCcVGck5PdQ9ZVv4aT3e+Ym04Wuq/mZWFvqEflZUc87k9KmXPkEh+Naonv9MTg5vNHENjANQGaq2X5HLizPjdiEvD2MA/v8RvioE2YpkkmopHoYiLsvpxSaYSppxNWaRzTQyv6No1ngdYzJKVz2gDRwHHDgKIYryYofd/xpqcN9QHDd9A8Xxg+bTgLWp4UInSkEFWuZyRKhI5Toj5w70o0AiUyzFMr0eQ4frJ3fsbIjwlPzU//sPNS/fwmvOSMC5Fmshb38ZP18+AHdfRn6OLlTfk54L8sgCzgIRC4AJkqYAPkAs8BAZLH3GimLEieg5dn3274myB35oh1D2fgyV2c20NM+beZBEr6N6SuVFTA87T367ImYBQZoLT4U+BZ76yNgrXuAaV1YtaM/nLqpe7+BJOhUjt15SdofL/7e0vUHLsjaye/+9u/w2vcM7s+cAMJnS8C3tuztkAR+ZXj8PNgzenI2uQNWfPx2vpw95XAmJtft1v7jx8/0osB1PQSbabYmAHXUZYQ+CIQKot3je/xlfymraLlNj+KSltQqXDsgemo3SiUAU+A6e6b+imaUp/PXUZj3apSfEuov/9SbuCrrspz2zremA+++m1vWFrwwxHB3yN9YD4chH8Kp22hHdjiwiGnDp8Pv4jWHx2WF4X1l51m8B8=
\ No newline at end of file
diff --git a/docs/database/mysql/index-invalidation-caused-by-implicit-conversion.md b/docs/database/mysql/index-invalidation-caused-by-implicit-conversion.md
index 377460c66a6..69375c00a0b 100644
--- a/docs/database/mysql/index-invalidation-caused-by-implicit-conversion.md
+++ b/docs/database/mysql/index-invalidation-caused-by-implicit-conversion.md
@@ -1,9 +1,14 @@
---
title: MySQL隐式转换造成索引失效
+description: 深入分析MySQL中隐式类型转换导致索引失效的原因和场景,通过实际案例演示字符串与数字比较时的性能问题,并给出避免索引失效的最佳实践。
category: 数据库
tag:
- MySQL
- 性能优化
+head:
+ - - meta
+ - name: keywords
+ content: MySQL隐式转换,索引失效,类型转换,MySQL性能优化,数据类型不匹配,全表扫描,SQL优化
---
> 本次测试使用的 MySQL 版本是 `5.7.26`,随着 MySQL 版本的更新某些特性可能会发生改变,本文不代表所述观点和结论于 MySQL 所有版本均准确无误,版本差异请自行甄别。
diff --git a/docs/database/mysql/innodb-implementation-of-mvcc.md b/docs/database/mysql/innodb-implementation-of-mvcc.md
index a2e19998d71..b4df7745026 100644
--- a/docs/database/mysql/innodb-implementation-of-mvcc.md
+++ b/docs/database/mysql/innodb-implementation-of-mvcc.md
@@ -1,8 +1,13 @@
---
title: InnoDB存储引擎对MVCC的实现
+description: 深入剖析InnoDB存储引擎MVCC的实现原理,详解隐藏列、undo log版本链、ReadView机制,以及快照读与当前读的区别,理解MySQL如何实现事务隔离。
category: 数据库
tag:
- MySQL
+head:
+ - - meta
+ - name: keywords
+ content: MVCC,多版本并发控制,InnoDB,快照读,当前读,一致性视图,ReadView,undo log,隐藏列,事务隔离
---
## 多版本并发控制 (Multi-Version Concurrency Control)
diff --git a/docs/database/mysql/mysql-auto-increment-primary-key-continuous.md b/docs/database/mysql/mysql-auto-increment-primary-key-continuous.md
index ec900188610..029f7dd1243 100644
--- a/docs/database/mysql/mysql-auto-increment-primary-key-continuous.md
+++ b/docs/database/mysql/mysql-auto-increment-primary-key-continuous.md
@@ -1,9 +1,14 @@
---
title: MySQL自增主键一定是连续的吗
+description: 详解MySQL自增主键不连续的原因,分析唯一键冲突、事务回滚、批量插入等场景下自增值的分配机制,以及InnoDB自增锁模式的配置与影响。
category: 数据库
tag:
- MySQL
- 大厂面试
+head:
+ - - meta
+ - name: keywords
+ content: MySQL自增主键,AUTO_INCREMENT,主键不连续,事务回滚,批量插入,唯一键冲突,innodb_autoinc_lock_mode
---
> 作者:飞天小牛肉
diff --git a/docs/database/mysql/mysql-high-performance-optimization-specification-recommendations.md b/docs/database/mysql/mysql-high-performance-optimization-specification-recommendations.md
index c402fcff3e8..00e783de034 100644
--- a/docs/database/mysql/mysql-high-performance-optimization-specification-recommendations.md
+++ b/docs/database/mysql/mysql-high-performance-optimization-specification-recommendations.md
@@ -1,8 +1,13 @@
---
title: MySQL高性能优化规范建议总结
+description: MySQL高性能优化规范建议总结,涵盖数据库命名规范、表设计规范、字段设计规范、索引设计规范、SQL编写规范等,帮助你构建高效稳定的数据库系统。
category: 数据库
tag:
- MySQL
+head:
+ - - meta
+ - name: keywords
+ content: MySQL优化规范,数据库设计规范,索引设计,SQL编写规范,慢查询优化,字段类型选择,表结构设计
---
> 作者: 听风 原文地址: 。
@@ -11,17 +16,17 @@ tag:
## 数据库命名规范
-- 所有数据库对象名称必须使用小写字母并用下划线分割
-- 所有数据库对象名称禁止使用 MySQL 保留关键字(如果表名中包含关键字查询时,需要将其用单引号括起来)
-- 数据库对象的命名要能做到见名识意,并且最后不要超过 32 个字符
-- 临时库表必须以 `tmp_` 为前缀并以日期为后缀,备份表必须以 `bak_` 为前缀并以日期 (时间戳) 为后缀
-- 所有存储相同数据的列名和列类型必须一致(一般作为关联列,如果查询时关联列类型不一致会自动进行数据类型隐式转换,会造成列上的索引失效,导致查询效率降低)
+- 所有数据库对象名称必须使用小写字母并用下划线分割。
+- 所有数据库对象名称禁止使用 MySQL 保留关键字(如果表名中包含关键字查询时,需要将其用单引号括起来)。
+- 数据库对象的命名要能做到见名识义,并且最好不要超过 32 个字符。
+- 临时库表必须以 `tmp_` 为前缀并以日期为后缀,备份表必须以 `bak_` 为前缀并以日期 (时间戳) 为后缀。
+- 所有存储相同数据的列名和列类型必须一致(一般作为关联列,如果查询时关联列类型不一致会自动进行数据类型隐式转换,会造成列上的索引失效,导致查询效率降低)。
## 数据库基本设计规范
### 所有表必须使用 InnoDB 存储引擎
-没有特殊要求(即 InnoDB 无法满足的功能如:列存储,存储空间数据等)的情况下,所有表必须使用 InnoDB 存储引擎(MySQL5.5 之前默认使用 Myisam,5.6 以后默认的为 InnoDB)。
+没有特殊要求(即 InnoDB 无法满足的功能如:列存储、存储空间数据等)的情况下,所有表必须使用 InnoDB 存储引擎(MySQL5.5 之前默认使用 MyISAM,5.6 以后默认的为 InnoDB)。
InnoDB 支持事务,支持行级锁,更好的恢复性,高并发下性能更好。
@@ -33,19 +38,19 @@ InnoDB 支持事务,支持行级锁,更好的恢复性,高并发下性能
### 所有表和字段都需要添加注释
-使用 comment 从句添加表和列的备注,从一开始就进行数据字典的维护
+使用 comment 从句添加表和列的备注,从一开始就进行数据字典的维护。
### 尽量控制单表数据量的大小,建议控制在 500 万以内
500 万并不是 MySQL 数据库的限制,过大会造成修改表结构,备份,恢复都会有很大的问题。
-可以用历史数据归档(应用于日志数据),分库分表(应用于业务数据)等手段来控制数据量大小
+可以用历史数据归档(应用于日志数据),分库分表(应用于业务数据)等手段来控制数据量大小。
### 谨慎使用 MySQL 分区表
-分区表在物理上表现为多个文件,在逻辑上表现为一个表;
+分区表在物理上表现为多个文件,在逻辑上表现为一个表。
-谨慎选择分区键,跨分区查询效率可能更低;
+谨慎选择分区键,跨分区查询效率可能更低。
建议采用物理分表的方式管理大数据。
@@ -71,7 +76,7 @@ InnoDB 支持事务,支持行级锁,更好的恢复性,高并发下性能
### 禁止在线上做数据库压力测试
-### 禁止从开发环境,测试环境直接连接生产环境数据库
+### 禁止从开发环境、测试环境直接连接生产环境数据库
安全隐患极大,要对生产环境抱有敬畏之心!
@@ -79,22 +84,22 @@ InnoDB 支持事务,支持行级锁,更好的恢复性,高并发下性能
### 优先选择符合存储需要的最小的数据类型
-存储字节越小,占用也就空间越小,性能也越好。
+存储字节越小,占用空间也就越小,性能也越好。
-**a.某些字符串可以转换成数字类型存储比如可以将 IP 地址转换成整型数据。**
+**a.某些字符串可以转换成数字类型存储,比如可以将 IP 地址转换成整型数据。**
数字是连续的,性能更好,占用空间也更小。
-MySQL 提供了两个方法来处理 ip 地址
+MySQL 提供了两个方法来处理 ip 地址:
-- `INET_ATON()`:把 ip 转为无符号整型 (4-8 位)
-- `INET_NTOA()` :把整型的 ip 转为地址
+- `INET_ATON()`:把 ip 转为无符号整型 (4-8 位);
+- `INET_NTOA()`:把整型的 ip 转为地址。
-插入数据前,先用 `INET_ATON()` 把 ip 地址转为整型,显示数据时,使用 `INET_NTOA()` 把整型的 ip 地址转为地址显示即可。
+插入数据前,先用 `INET_ATON()` 把 ip 地址转为整型;显示数据时,使用 `INET_NTOA()` 把整型的 ip 地址转为地址显示即可。
-**b.对于非负型的数据 (如自增 ID,整型 IP,年龄) 来说,要优先使用无符号整型来存储。**
+**b.对于非负型的数据 (如自增 ID、整型 IP、年龄) 来说,要优先使用无符号整型来存储。**
-无符号相对于有符号可以多出一倍的存储空间
+无符号相对于有符号可以多出一倍的存储空间:
```sql
SIGNED INT -2147483648~2147483647
@@ -103,7 +108,7 @@ UNSIGNED INT 0~4294967295
**c.小数值类型(比如年龄、状态表示如 0/1)优先使用 TINYINT 类型。**
-### 避免使用 TEXT,BLOB 数据类型,最常见的 TEXT 类型可以存储 64k 的数据
+### 避免使用 TEXT、BLOB 数据类型,最常见的 TEXT 类型可以存储 64k 的数据
**a. 建议把 BLOB 或是 TEXT 列分离到单独的扩展表中。**
@@ -113,30 +118,30 @@ MySQL 内存临时表不支持 TEXT、BLOB 这样的大数据类型,如果查
**2、TEXT 或 BLOB 类型只能使用前缀索引**
-因为 MySQL 对索引字段长度是有限制的,所以 TEXT 类型只能使用前缀索引,并且 TEXT 列上是不能有默认值的
+因为 MySQL 对索引字段长度是有限制的,所以 TEXT 类型只能使用前缀索引,并且 TEXT 列上是不能有默认值的。
### 避免使用 ENUM 类型
-- 修改 ENUM 值需要使用 ALTER 语句;
-- ENUM 类型的 ORDER BY 操作效率低,需要额外操作;
-- ENUM 数据类型存在一些限制比如建议不要使用数值作为 ENUM 的枚举值。
+- 修改 ENUM 值需要使用 ALTER 语句。
+- ENUM 类型的 ORDER BY 操作效率低,需要额外操作。
+- ENUM 数据类型存在一些限制,比如建议不要使用数值作为 ENUM 的枚举值。
相关阅读:[是否推荐使用 MySQL 的 enum 类型? - 架构文摘 - 知乎](https://www.zhihu.com/question/404422255/answer/1661698499) 。
### 尽可能把所有列定义为 NOT NULL
-除非有特别的原因使用 NULL 值,应该总是让字段保持 NOT NULL。
+除非有特别的原因使用 NULL 值,否则应该总是让字段保持 NOT NULL。
-- 索引 NULL 列需要额外的空间来保存,所以要占用更多的空间;
+- 索引 NULL 列需要额外的空间来保存,所以要占用更多的空间。
- 进行比较和计算时要对 NULL 值做特别的处理。
相关阅读:[技术分享 | MySQL 默认值选型(是空,还是 NULL)](https://opensource.actionsky.com/20190710-mysql/) 。
### 一定不要用字符串存储日期
-对于日期类型来说, 一定不要用字符串存储日期。可以考虑 DATETIME、TIMESTAMP 和 数值型时间戳。
+对于日期类型来说,一定不要用字符串存储日期。可以考虑 DATETIME、TIMESTAMP 和数值型时间戳。
-这三种种方式都有各自的优势,根据实际场景选择最合适的才是王道。下面再对这三种方式做一个简单的对比,以供大家实际开发中选择正确的存放时间的数据类型:
+这三种种方式都有各自的优势,根据实际场景选择最合适的才是王道。下面再对这三种方式做一个简单的对比,以供大家在实际开发中选择正确的存放时间的数据类型:
| 类型 | 存储空间 | 日期格式 | 日期范围 | 是否带时区信息 |
| ------------ | -------- | ------------------------------ | ------------------------------------------------------------ | -------------- |
@@ -148,10 +153,10 @@ MySQL 时间类型选择的详细介绍请看这篇:[MySQL 时间类型数据
### 同财务相关的金额类数据必须使用 decimal 类型
-- **非精准浮点**:float,double
+- **非精准浮点**:float、double
- **精准浮点**:decimal
-decimal 类型为精准浮点数,在计算时不会丢失精度。占用空间由定义的宽度决定,每 4 个字节可以存储 9 位数字,并且小数点要占用一个字节。并且,decimal 可用于存储比 bigint 更大的整型数据
+decimal 类型为精准浮点数,在计算时不会丢失精度。占用空间由定义的宽度决定,每 4 个字节可以存储 9 位数字,并且小数点要占用一个字节。并且,decimal 可用于存储比 bigint 更大的整型数据。
不过, 由于 decimal 需要额外的空间和计算开销,应该尽量只在需要对数据进行精确计算时才使用 decimal 。
@@ -161,13 +166,13 @@ decimal 类型为精准浮点数,在计算时不会丢失精度。占用空间
## 索引设计规范
-### 限制每张表上的索引数量,建议单张表索引不超过 5 个
+### 限制每张表上的索引数量,建议单张表索引不超过 5 个
-索引并不是越多越好!索引可以提高效率同样可以降低效率。
+索引并不是越多越好!索引可以提高效率,同样可以降低效率。
索引可以增加查询效率,但同样也会降低插入和更新的效率,甚至有些情况下会降低查询效率。
-因为 MySQL 优化器在选择如何优化查询时,会根据统一信息,对每一个可以用到的索引来进行评估,以生成出一个最好的执行计划,如果同时有很多个索引都可以用于查询,就会增加 MySQL 优化器生成执行计划的时间,同样会降低查询性能。
+因为 MySQL 优化器在选择如何优化查询时,会根据统一信息,对每一个可以用到的索引来进行评估,以生成出一个最好的执行计划。如果同时有很多个索引都可以用于查询,就会增加 MySQL 优化器生成执行计划的时间,同样会降低查询性能。
### 禁止使用全文索引
@@ -175,46 +180,46 @@ decimal 类型为精准浮点数,在计算时不会丢失精度。占用空间
### 禁止给表中的每一列都建立单独的索引
-5.6 版本之前,一个 sql 只能使用到一个表中的一个索引,5.6 以后,虽然有了合并索引的优化方式,但是还是远远没有使用一个联合索引的查询方式好。
+5.6 版本之前,一个 sql 只能使用到一个表中的一个索引;5.6 以后,虽然有了合并索引的优化方式,但是还是远远没有使用一个联合索引的查询方式好。
### 每个 InnoDB 表必须有个主键
InnoDB 是一种索引组织表:数据的存储的逻辑顺序和索引的顺序是相同的。每个表都可以有多个索引,但是表的存储顺序只能有一种。
-InnoDB 是按照主键索引的顺序来组织表的
+InnoDB 是按照主键索引的顺序来组织表的。
-- 不要使用更新频繁的列作为主键,不使用多列主键(相当于联合索引)
-- 不要使用 UUID,MD5,HASH,字符串列作为主键(无法保证数据的顺序增长)
-- 主键建议使用自增 ID 值
+- 不要使用更新频繁的列作为主键,不使用多列主键(相当于联合索引)。
+- 不要使用 UUID、MD5、HASH、字符串列作为主键(无法保证数据的顺序增长)。
+- 主键建议使用自增 ID 值。
### 常见索引列建议
-- 出现在 SELECT、UPDATE、DELETE 语句的 WHERE 从句中的列
-- 包含在 ORDER BY、GROUP BY、DISTINCT 中的字段
-- 并不要将符合 1 和 2 中的字段的列都建立一个索引, 通常将 1、2 中的字段建立联合索引效果更好
-- 多表 join 的关联列
+- 出现在 SELECT、UPDATE、DELETE 语句的 WHERE 从句中的列。
+- 包含在 ORDER BY、GROUP BY、DISTINCT 中的字段。
+- 不要将符合 1 和 2 中的字段的列都建立一个索引,通常将 1、2 中的字段建立联合索引效果更好。
+- 多表 join 的关联列。
### 如何选择索引列的顺序
-建立索引的目的是:希望通过索引进行数据查找,减少随机 IO,增加查询性能 ,索引能过滤出越少的数据,则从磁盘中读入的数据也就越少。
+建立索引的目的是:希望通过索引进行数据查找,减少随机 IO,增加查询性能,索引能过滤出越少的数据,则从磁盘中读入的数据也就越少。
-- 区分度最高的放在联合索引的最左侧(区分度=列中不同值的数量/列的总行数)
-- 尽量把字段长度小的列放在联合索引的最左侧(因为字段长度越小,一页能存储的数据量越大,IO 性能也就越好)
-- 使用最频繁的列放到联合索引的左侧(这样可以比较少的建立一些索引)
+- **区分度最高的列放在联合索引的最左侧**:这是最重要的原则。区分度越高,通过索引筛选出的数据就越少,I/O 操作也就越少。计算区分度的方法是 `count(distinct column) / count(*)`。
+- **最频繁使用的列放在联合索引的左侧**:这符合最左前缀匹配原则。将最常用的查询条件列放在最左侧,可以最大程度地利用索引。
+- **字段长度**:字段长度对联合索引非叶子节点的影响很小,因为它存储了所有联合索引字段的值。字段长度主要影响主键和包含在其他索引中的字段的存储空间,以及这些索引的叶子节点的大小。因此,在选择联合索引列的顺序时,字段长度的优先级最低。对于主键和包含在其他索引中的字段,选择较短的字段长度可以节省存储空间和提高 I/O 性能。
### 避免建立冗余索引和重复索引(增加了查询优化器生成执行计划的时间)
-- 重复索引示例:primary key(id)、index(id)、unique index(id)
-- 冗余索引示例:index(a,b,c)、index(a,b)、index(a)
+- 重复索引示例:primary key(id)、index(id)、unique index(id)。
+- 冗余索引示例:index(a,b,c)、index(a,b)、index(a)。
-### 对于频繁的查询优先考虑使用覆盖索引
+### 对于频繁的查询,优先考虑使用覆盖索引
-> 覆盖索引:就是包含了所有查询字段 (where,select,order by,group by 包含的字段) 的索引
+> 覆盖索引:就是包含了所有查询字段 (where、select、order by、group by 包含的字段) 的索引
-**覆盖索引的好处:**
+**覆盖索引的好处**:
-- **避免 InnoDB 表进行索引的二次查询,也就是回表操作:** InnoDB 是以聚集索引的顺序来存储的,对于 InnoDB 来说,二级索引在叶子节点中所保存的是行的主键信息,如果是用二级索引查询数据的话,在查找到相应的键值后,还要通过主键进行二次查询才能获取我们真实所需要的数据。而在覆盖索引中,二级索引的键值中可以获取所有的数据,避免了对主键的二次查询(回表),减少了 IO 操作,提升了查询效率。
-- **可以把随机 IO 变成顺序 IO 加快查询效率:** 由于覆盖索引是按键值的顺序存储的,对于 IO 密集型的范围查找来说,对比随机从磁盘读取每一行的数据 IO 要少的多,因此利用覆盖索引在访问时也可以把磁盘的随机读取的 IO 转变成索引查找的顺序 IO。
+- **避免 InnoDB 表进行索引的二次查询,也就是回表操作**:InnoDB 是以聚集索引的顺序来存储的,对于 InnoDB 来说,二级索引在叶子节点中所保存的是行的主键信息,如果是用二级索引查询数据的话,在查找到相应的键值后,还要通过主键进行二次查询才能获取我们真实所需要的数据。而在覆盖索引中,二级索引的键值中可以获取所有的数据,避免了对主键的二次查询(回表),减少了 IO 操作,提升了查询效率。
+- **可以把随机 IO 变成顺序 IO 加快查询效率**:由于覆盖索引是按键值的顺序存储的,对于 IO 密集型的范围查找来说,对比随机从磁盘读取每一行的数据 IO 要少的多,因此利用覆盖索引在访问时也可以把磁盘的随机读取的 IO 转变成索引查找的顺序 IO。
---
@@ -222,9 +227,9 @@ InnoDB 是按照主键索引的顺序来组织表的
**尽量避免使用外键约束**
-- 不建议使用外键约束(foreign key),但一定要在表与表之间的关联键上建立索引
-- 外键可用于保证数据的参照完整性,但建议在业务端实现
-- 外键会影响父表和子表的写操作从而降低性能
+- 不建议使用外键约束(foreign key),但一定要在表与表之间的关联键上建立索引。
+- 外键可用于保证数据的参照完整性,但建议在业务端实现。
+- 外键会影响父表和子表的写操作从而降低性能。
## 数据库 SQL 开发规范
@@ -238,7 +243,7 @@ InnoDB 是按照主键索引的顺序来组织表的
### 充分利用表上已经存在的索引
-避免使用双%号的查询条件。如:`a like '%123%'`,(如果无前置%,只有后置%,是可以用到列上的索引的)
+避免使用双%号的查询条件。如:`a like '%123%'`(如果无前置%,只有后置%,是可以用到列上的索引的)。
一个 SQL 只能利用到复合索引中的一列进行范围查询。如:有 a,b,c 列的联合索引,在查询条件中有 a 列的范围查询,则在 b,c 列上的索引将不会被用到。
@@ -248,18 +253,18 @@ InnoDB 是按照主键索引的顺序来组织表的
- `SELECT *` 会消耗更多的 CPU。
- `SELECT *` 无用字段增加网络带宽资源消耗,增加数据传输时间,尤其是大字段(如 varchar、blob、text)。
-- `SELECT *` 无法使用 MySQL 优化器覆盖索引的优化(基于 MySQL 优化器的“覆盖索引”策略又是速度极快,效率极高,业界极为推荐的查询优化方式)
-- `SELECT <字段列表>` 可减少表结构变更带来的影响、
+- `SELECT *` 无法使用 MySQL 优化器覆盖索引的优化(基于 MySQL 优化器的“覆盖索引”策略又是速度极快、效率极高、业界极为推荐的查询优化方式)。
+- `SELECT <字段列表>` 可减少表结构变更带来的影响。
### 禁止使用不含字段列表的 INSERT 语句
-如:
+**不推荐**:
```sql
insert into t values ('a','b','c');
```
-应使用:
+**推荐**:
```sql
insert into t(c1,c2,c3) values ('a','b','c');
@@ -273,7 +278,7 @@ insert into t(c1,c2,c3) values ('a','b','c');
### 避免数据类型的隐式转换
-隐式转换会导致索引失效如:
+隐式转换会导致索引失效,如:
```sql
select name,phone from customer where id = '111';
@@ -283,9 +288,9 @@ select name,phone from customer where id = '111';
### 避免使用子查询,可以把子查询优化为 join 操作
-通常子查询在 in 子句中,且子查询中为简单 SQL(不包含 union、group by、order by、limit 从句) 时,才可以把子查询转化为关联查询进行优化。
+通常子查询在 in 子句中,且子查询中为简单 SQL(不包含 union、group by、order by、limit 从句) 时,才可以把子查询转化为关联查询进行优化。
-**子查询性能差的原因:** 子查询的结果集无法使用索引,通常子查询的结果集会被存储到临时表中,不论是内存临时表还是磁盘临时表都不会存在索引,所以查询性能会受到一定的影响。特别是对于返回结果集比较大的子查询,其对查询性能的影响也就越大。由于子查询会产生大量的临时表也没有索引,所以会消耗过多的 CPU 和 IO 资源,产生大量的慢查询。
+**子查询性能差的原因**:子查询的结果集无法使用索引,通常子查询的结果集会被存储到临时表中,不论是内存临时表还是磁盘临时表都不会存在索引,所以查询性能会受到一定的影响。特别是对于返回结果集比较大的子查询,其对查询性能的影响也就越大。由于子查询会产生大量的临时表也没有索引,所以会消耗过多的 CPU 和 IO 资源,产生大量的慢查询。
### 避免使用 JOIN 关联太多的表
@@ -293,7 +298,7 @@ select name,phone from customer where id = '111';
在 MySQL 中,对于同一个 SQL 多关联(join)一个表,就会多分配一个关联缓存,如果在一个 SQL 中关联的表越多,所占用的内存也就越大。
-如果程序中大量的使用了多表关联的操作,同时 join_buffer_size 设置的也不合理的情况下,就容易造成服务器内存溢出的情况,就会影响到服务器数据库性能的稳定性。
+如果程序中大量地使用了多表关联的操作,同时 join_buffer_size 设置得也不合理,就容易造成服务器内存溢出的情况,就会影响到服务器数据库性能的稳定性。
同时对于关联操作来说,会产生临时表操作,影响查询效率,MySQL 最多允许关联 61 个表,建议不超过 5 个。
@@ -303,25 +308,25 @@ select name,phone from customer where id = '111';
### 对应同一列进行 or 判断时,使用 in 代替 or
-in 的值不要超过 500 个,in 操作可以更有效的利用索引,or 大多数情况下很少能利用到索引。
+in 的值不要超过 500 个。in 操作可以更有效的利用索引,or 大多数情况下很少能利用到索引。
### 禁止使用 order by rand() 进行随机排序
-order by rand() 会把表中所有符合条件的数据装载到内存中,然后在内存中对所有数据根据随机生成的值进行排序,并且可能会对每一行都生成一个随机值,如果满足条件的数据集非常大,就会消耗大量的 CPU 和 IO 及内存资源。
+order by rand() 会把表中所有符合条件的数据装载到内存中,然后在内存中对所有数据根据随机生成的值进行排序,并且可能会对每一行都生成一个随机值。如果满足条件的数据集非常大,就会消耗大量的 CPU 和 IO 及内存资源。
推荐在程序中获取一个随机值,然后从数据库中获取数据的方式。
### WHERE 从句中禁止对列进行函数转换和计算
-对列进行函数转换或计算时会导致无法使用索引
+对列进行函数转换或计算时会导致无法使用索引。
-**不推荐:**
+**不推荐**:
```sql
where date(create_time)='20190101'
```
-**推荐:**
+**推荐**:
```sql
where create_time >= '20190101' and create_time < '20190102'
@@ -329,43 +334,43 @@ where create_time >= '20190101' and create_time < '20190102'
### 在明显不会有重复值时使用 UNION ALL 而不是 UNION
-- UNION 会把两个结果集的所有数据放到临时表中后再进行去重操作
-- UNION ALL 不会再对结果集进行去重操作
+- UNION 会把两个结果集的所有数据放到临时表中后再进行去重操作。
+- UNION ALL 不会再对结果集进行去重操作。
### 拆分复杂的大 SQL 为多个小 SQL
-- 大 SQL 逻辑上比较复杂,需要占用大量 CPU 进行计算的 SQL
-- MySQL 中,一个 SQL 只能使用一个 CPU 进行计算
-- SQL 拆分后可以通过并行执行来提高处理效率
+- 大 SQL 逻辑上比较复杂,需要占用大量 CPU 进行计算的 SQL。
+- MySQL 中,一个 SQL 只能使用一个 CPU 进行计算。
+- SQL 拆分后可以通过并行执行来提高处理效率。
### 程序连接不同的数据库使用不同的账号,禁止跨库查询
-- 为数据库迁移和分库分表留出余地
-- 降低业务耦合度
-- 避免权限过大而产生的安全风险
+- 为数据库迁移和分库分表留出余地。
+- 降低业务耦合度。
+- 避免权限过大而产生的安全风险。
## 数据库操作行为规范
-### 超 100 万行的批量写 (UPDATE,DELETE,INSERT) 操作,要分批多次进行操作
+### 超 100 万行的批量写 (UPDATE、DELETE、INSERT) 操作,要分批多次进行操作
**大批量操作可能会造成严重的主从延迟**
-主从环境中,大批量操作可能会造成严重的主从延迟,大批量的写操作一般都需要执行一定长的时间,而只有当主库上执行完成后,才会在其他从库上执行,所以会造成主库与从库长时间的延迟情况
+主从环境中,大批量操作可能会造成严重的主从延迟,大批量的写操作一般都需要执行一定长的时间,而只有当主库上执行完成后,才会在其他从库上执行,所以会造成主库与从库长时间的延迟情况。
**binlog 日志为 row 格式时会产生大量的日志**
-大批量写操作会产生大量日志,特别是对于 row 格式二进制数据而言,由于在 row 格式中会记录每一行数据的修改,我们一次修改的数据越多,产生的日志量也就会越多,日志的传输和恢复所需要的时间也就越长,这也是造成主从延迟的一个原因
+大批量写操作会产生大量日志,特别是对于 row 格式二进制数据而言,由于在 row 格式中会记录每一行数据的修改,我们一次修改的数据越多,产生的日志量也就会越多,日志的传输和恢复所需要的时间也就越长,这也是造成主从延迟的一个原因。
**避免产生大事务操作**
大批量修改数据,一定是在一个事务中进行的,这就会造成表中大批量数据进行锁定,从而导致大量的阻塞,阻塞会对 MySQL 的性能产生非常大的影响。
-特别是长时间的阻塞会占满所有数据库的可用连接,这会使生产环境中的其他应用无法连接到数据库,因此一定要注意大批量写操作要进行分批
+特别是长时间的阻塞会占满所有数据库的可用连接,这会使生产环境中的其他应用无法连接到数据库,因此一定要注意大批量写操作要进行分批。
### 对于大表使用 pt-online-schema-change 修改表结构
-- 避免大表修改产生的主从延迟
-- 避免在对表字段进行修改时进行锁表
+- 避免大表修改产生的主从延迟。
+- 避免在对表字段进行修改时进行锁表。
对大表数据结构的修改一定要谨慎,会造成严重的锁表操作,尤其是生产环境,是不能容忍的。
@@ -373,13 +378,13 @@ pt-online-schema-change 它会首先建立一个与原表结构相同的新表
### 禁止为程序使用的账号赋予 super 权限
-- 当达到最大连接数限制时,还运行 1 个有 super 权限的用户连接
-- super 权限只能留给 DBA 处理问题的账号使用
+- 当达到最大连接数限制时,还运行 1 个有 super 权限的用户连接。
+- super 权限只能留给 DBA 处理问题的账号使用。
-### 对于程序连接数据库账号,遵循权限最小原则
+### 对于程序连接数据库账号,遵循权限最小原则
-- 程序使用数据库账号只能在一个 DB 下使用,不准跨库
-- 程序使用的账号原则上不准有 drop 权限
+- 程序使用数据库账号只能在一个 DB 下使用,不准跨库。
+- 程序使用的账号原则上不准有 drop 权限。
## 推荐阅读
diff --git a/docs/database/mysql/mysql-index.md b/docs/database/mysql/mysql-index.md
index c948e88ee22..4e3b671b09f 100644
--- a/docs/database/mysql/mysql-index.md
+++ b/docs/database/mysql/mysql-index.md
@@ -1,8 +1,13 @@
---
title: MySQL索引详解
+description: MySQL索引详解,深入剖析B+树索引结构、聚簇索引与二级索引的区别、联合索引与最左前缀原则、覆盖索引与索引下推优化,以及常见的索引失效场景。
category: 数据库
tag:
- MySQL
+head:
+ - - meta
+ - name: keywords
+ content: MySQL索引,B+树索引,聚簇索引,覆盖索引,联合索引,索引下推,回表查询,索引失效,最左前缀原则
---
> 感谢[WT-AHA](https://github.com/WT-AHA)对本文的完善,相关 PR: 。
@@ -15,31 +20,37 @@ tag:
**索引是一种用于快速查询和检索数据的数据结构,其本质可以看成是一种排序好的数据结构。**
-索引的作用就相当于书的目录。打个比方: 我们在查字典的时候,如果没有目录,那我们就只能一页一页的去找我们需要查的那个字,速度很慢。如果有目录了,我们只需要先去目录里查找字的位置,然后直接翻到那一页就行了。
+索引的作用就相当于书的目录。打个比方:我们在查字典的时候,如果没有目录,那我们就只能一页一页地去找我们需要查的那个字,速度很慢;如果有目录了,我们只需要先去目录里查找字的位置,然后直接翻到那一页就行了。
-索引底层数据结构存在很多种类型,常见的索引结构有: B 树, B+树 和 Hash、红黑树。在 MySQL 中,无论是 Innodb 还是 MyIsam,都使用了 B+树作为索引结构。
+索引底层数据结构存在很多种类型,常见的索引结构有:B 树、 B+ 树 和 Hash、红黑树。在 MySQL 中,无论是 Innodb 还是 MyISAM,都使用了 B+ 树作为索引结构。
## 索引的优缺点
-**优点**:
+**索引的优点:**
-- 使用索引可以大大加快数据的检索速度(大大减少检索的数据量), 减少 IO 次数,这也是创建索引的最主要的原因。
-- 通过创建唯一性索引,可以保证数据库表中每一行数据的唯一性。
+1. **查询速度起飞 (主要目的)**:通过索引,数据库可以**大幅减少需要扫描的数据量**,直接定位到符合条件的记录,从而显著加快数据检索速度,减少磁盘 I/O 次数。
+2. **保证数据唯一性**:通过创建**唯一索引 (Unique Index)**,可以确保表中的某一列(或几列组合)的值是独一无二的,比如用户 ID、邮箱等。主键本身就是一种唯一索引。
+3. **加速排序和分组**:如果查询中的 ORDER BY 或 GROUP BY 子句涉及的列建有索引,数据库往往可以直接利用索引已经排好序的特性,避免额外的排序操作,从而提升性能。
-**缺点**:
+**索引的缺点:**
+
+1. **创建和维护耗时**:创建索引本身需要时间,特别是对大表操作时。更重要的是,当对表中的数据进行**增、删、改 (DML 操作)** 时,不仅要操作数据本身,相关的索引也必须动态更新和维护,这会**降低这些 DML 操作的执行效率**。
+2. **占用存储空间**:索引本质上也是一种数据结构,需要以物理文件(或内存结构)的形式存储,因此会**额外占用一定的磁盘空间**。索引越多、越大,占用的空间也就越多。
+3. **可能被误用或失效**:如果索引设计不当,或者查询语句写得不好,数据库优化器可能不会选择使用索引(或者选错索引),反而导致性能下降。
-- 创建索引和维护索引需要耗费许多时间。当对表中的数据进行增删改的时候,如果数据有索引,那么索引也需要动态的修改,会降低 SQL 执行效率。
-- 索引需要使用物理文件存储,也会耗费一定空间。
+**那么,用了索引就一定能提高查询性能吗?**
-但是,**使用索引一定能提高查询性能吗?**
+**不一定。** 大多数情况下,合理使用索引确实比全表扫描快得多。但也有例外:
-大多数情况下,索引查询都是比全表扫描要快的。但是如果数据库的数据量不大,那么使用索引也不一定能够带来很大提升。
+- **数据量太小**:如果表里的数据非常少(比如就几百条),全表扫描可能比通过索引查找更快,因为走索引本身也有开销。
+- **查询结果集占比过大**:如果要查询的数据占了整张表的大部分(比如超过 20%-30%),优化器可能会认为全表扫描更划算,因为通过索引多次回表(随机 I/O)的成本可能高于一次顺序的全表扫描。
+- **索引维护不当或统计信息过时**:导致优化器做出错误判断。
## 索引底层数据结构选型
### Hash 表
-哈希表是键值对的集合,通过键(key)即可快速取出对应的值(value),因此哈希表可以快速检索数据(接近 O(1))。
+哈希表是键值对的集合,通过键(key)即可快速取出对应的值(value),因此哈希表可以快速检索数据(接近 O(1))。
**为何能够通过 key 快速取出 value 呢?** 原因在于 **哈希算法**(也叫散列算法)。通过哈希算法,我们可以快速找到 key 对应的 index,找到了 index 也就找到了对应的 value。
@@ -50,7 +61,7 @@ index = hash % array_size

-但是!哈希算法有个 **Hash 冲突** 问题,也就是说多个不同的 key 最后得到的 index 相同。通常情况下,我们常用的解决办法是 **链地址法**。链地址法就是将哈希冲突数据存放在链表中。就比如 JDK1.8 之前 `HashMap` 就是通过链地址法来解决哈希冲突的。不过,JDK1.8 以后`HashMap`为了减少链表过长的时候搜索时间过长引入了红黑树。
+但是!哈希算法有个 **Hash 冲突** 问题,也就是说多个不同的 key 最后得到的 index 相同。通常情况下,我们常用的解决办法是 **链地址法**。链地址法就是将哈希冲突数据存放在链表中。就比如 JDK1.8 之前 `HashMap` 就是通过链地址法来解决哈希冲突的。不过,JDK1.8 以后`HashMap`为了提高链表过长时的搜索效率,引入了红黑树。

@@ -60,15 +71,15 @@ MySQL 的 InnoDB 存储引擎不直接支持常规的哈希索引,但是,Inn
既然哈希表这么快,**为什么 MySQL 没有使用其作为索引的数据结构呢?** 主要是因为 Hash 索引不支持顺序和范围查询。假如我们要对表中的数据进行排序或者进行范围查询,那 Hash 索引可就不行了。并且,每次 IO 只能取一个。
-试想一种情况:
+试想一种情况:
```java
SELECT * FROM tb1 WHERE id < 500;
```
-在这种范围查询中,优势非常大,直接遍历比 500 小的叶子节点就够了。而 Hash 索引是根据 hash 算法来定位的,难不成还要把 1 - 499 的数据,每个都进行一次 hash 计算来定位吗?这就是 Hash 最大的缺点了。
+在这种范围查询中,优势非常大,直接遍历比 500 小的叶子节点就够了。而 Hash 索引是根据 hash 算法来定位的,难不成还要把 1 - 499 的数据,每个都进行一次 hash 计算来定位吗?这就是 Hash 最大的缺点了。
-### 二叉查找树(BST)
+### 二叉查找树(BST)
二叉查找树(Binary Search Tree)是一种基于二叉树的数据结构,它具有以下特点:
@@ -76,7 +87,7 @@ SELECT * FROM tb1 WHERE id < 500;
2. 右子树所有节点的值均大于根节点的值。
3. 左右子树也分别为二叉查找树。
-当二叉查找树是平衡的时候,也就是树的每个节点的左右子树深度相差不超过 1 的时候,查询的时间复杂度为 O(log2(N)),具有比较高的效率。然而,当二叉查找树不平衡时,例如在最坏情况下(有序插入节点),树会退化成线性链表(也被称为斜树),导致查询效率急剧下降,时间复杂退化为 O(N)。
+当二叉查找树是平衡的时候,也就是树的每个节点的左右子树深度相差不超过 1 的时候,查询的时间复杂度为 O(log2(N)),具有比较高的效率。然而,当二叉查找树不平衡时,例如在最坏情况下(有序插入节点),树会退化成线性链表(也被称为斜树),导致查询效率急剧下降,时间复杂退化为 O(N)。

@@ -88,11 +99,11 @@ SELECT * FROM tb1 WHERE id < 500;
AVL 树是计算机科学中最早被发明的自平衡二叉查找树,它的名称来自于发明者 G.M. Adelson-Velsky 和 E.M. Landis 的名字缩写。AVL 树的特点是保证任何节点的左右子树高度之差不超过 1,因此也被称为高度平衡二叉树,它的查找、插入和删除在平均和最坏情况下的时间复杂度都是 O(logn)。
-
+
AVL 树采用了旋转操作来保持平衡。主要有四种旋转操作:LL 旋转、RR 旋转、LR 旋转和 RL 旋转。其中 LL 旋转和 RR 旋转分别用于处理左左和右右失衡,而 LR 旋转和 RL 旋转则用于处理左右和右左失衡。
-由于 AVL 树需要频繁地进行旋转操作来保持平衡,因此会有较大的计算开销进而降低了数据库写操作的性能。并且, 在使用 AVL 树时,每个树节点仅存储一个数据,而每次进行磁盘 IO 时只能读取一个节点的数据,如果需要查询的数据分布在多个节点上,那么就需要进行多次磁盘 IO。 **磁盘 IO 是一项耗时的操作,在设计数据库索引时,我们需要优先考虑如何最大限度地减少磁盘 IO 操作的次数。**
+由于 AVL 树需要频繁地进行旋转操作来保持平衡,因此会有较大的计算开销进而降低了数据库写操作的性能。并且, 在使用 AVL 树时,每个树节点仅存储一个数据,而每次进行磁盘 IO 时只能读取一个节点的数据,如果需要查询的数据分布在多个节点上,那么就需要进行多次磁盘 IO。**磁盘 IO 是一项耗时的操作,在设计数据库索引时,我们需要优先考虑如何最大限度地减少磁盘 IO 操作的次数。**
实际应用中,AVL 树使用的并不多。
@@ -112,26 +123,26 @@ AVL 树采用了旋转操作来保持平衡。主要有四种旋转操作:LL
**红黑树的应用还是比较广泛的,TreeMap、TreeSet 以及 JDK1.8 的 HashMap 底层都用到了红黑树。对于数据在内存中的这种情况来说,红黑树的表现是非常优异的。**
-### B 树& B+树
+### B 树& B+ 树
-B 树也称 B-树,全称为 **多路平衡查找树** ,B+ 树是 B 树的一种变体。B 树和 B+树中的 B 是 `Balanced` (平衡)的意思。
+B 树也称 B- 树,全称为 **多路平衡查找树**,B+ 树是 B 树的一种变体。B 树和 B+ 树中的 B 是 `Balanced`(平衡)的意思。
目前大部分数据库系统及文件系统都采用 B-Tree 或其变种 B+Tree 作为索引结构。
-**B 树& B+树两者有何异同呢?**
+**B 树& B+ 树两者有何异同呢?**
-- B 树的所有节点既存放键(key) 也存放数据(data),而 B+树只有叶子节点存放 key 和 data,其他内节点只存放 key。
-- B 树的叶子节点都是独立的;B+树的叶子节点有一条引用链指向与它相邻的叶子节点。
-- B 树的检索的过程相当于对范围内的每个节点的关键字做二分查找,可能还没有到达叶子节点,检索就结束了。而 B+树的检索效率就很稳定了,任何查找都是从根节点到叶子节点的过程,叶子节点的顺序检索很明显。
-- 在 B 树中进行范围查询时,首先找到要查找的下限,然后对 B 树进行中序遍历,直到找到查找的上限;而 B+树的范围查询,只需要对链表进行遍历即可。
+- B 树的所有节点既存放键(key)也存放数据(data),而 B+ 树只有叶子节点存放 key 和 data,其他内节点只存放 key。
+- B 树的叶子节点都是独立的;B+ 树的叶子节点有一条引用链指向与它相邻的叶子节点。
+- B 树的检索的过程相当于对范围内的每个节点的关键字做二分查找,可能还没有到达叶子节点,检索就结束了。而 B+ 树的检索效率就很稳定了,任何查找都是从根节点到叶子节点的过程,叶子节点的顺序检索很明显。
+- 在 B 树中进行范围查询时,首先找到要查找的下限,然后对 B 树进行中序遍历,直到找到查找的上限;而 B+ 树的范围查询,只需要对链表进行遍历即可。
-综上,B+树与 B 树相比,具备更少的 IO 次数、更稳定的查询效率和更适于范围查询这些优势。
+综上,B+ 树与 B 树相比,具备更少的 IO 次数、更稳定的查询效率和更适于范围查询这些优势。
在 MySQL 中,MyISAM 引擎和 InnoDB 引擎都是使用 B+Tree 作为索引结构,但是,两者的实现方式不太一样。(下面的内容整理自《Java 工程师修炼之道》)
> MyISAM 引擎中,B+Tree 叶节点的 data 域存放的是数据记录的地址。在索引检索的时候,首先按照 B+Tree 搜索算法搜索索引,如果指定的 Key 存在,则取出其 data 域的值,然后以 data 域的值为地址读取相应的数据记录。这被称为“**非聚簇索引(非聚集索引)**”。
>
-> InnoDB 引擎中,其数据文件本身就是索引文件。相比 MyISAM,索引文件和数据文件是分离的,其表数据文件本身就是按 B+Tree 组织的一个索引结构,树的叶节点 data 域保存了完整的数据记录。这个索引的 key 是数据表的主键,因此 InnoDB 表数据文件本身就是主索引。这被称为“**聚簇索引(聚集索引)**”,而其余的索引都作为 **辅助索引** ,辅助索引的 data 域存储相应记录主键的值而不是地址,这也是和 MyISAM 不同的地方。在根据主索引搜索时,直接找到 key 所在的节点即可取出数据;在根据辅助索引查找时,则需要先取出主键的值,再走一遍主索引。 因此,在设计表的时候,不建议使用过长的字段作为主键,也不建议使用非单调的字段作为主键,这样会造成主索引频繁分裂。
+> InnoDB 引擎中,其数据文件本身就是索引文件。相比 MyISAM,索引文件和数据文件是分离的,其表数据文件本身就是按 B+Tree 组织的一个索引结构,树的叶节点 data 域保存了完整的数据记录。这个索引的 key 是数据表的主键,因此 InnoDB 表数据文件本身就是主索引。这被称为“**聚簇索引(聚集索引)**”,而其余的索引都作为 **辅助索引**,辅助索引的 data 域存储相应记录主键的值而不是地址,这也是和 MyISAM 不同的地方。在根据主索引搜索时,直接找到 key 所在的节点即可取出数据;在根据辅助索引查找时,则需要先取出主键的值,再走一遍主索引。 因此,在设计表的时候,不建议使用过长的字段作为主键,也不建议使用非单调的字段作为主键,这样会造成主索引频繁分裂。
## 索引类型总结
@@ -140,12 +151,12 @@ B 树也称 B-树,全称为 **多路平衡查找树** ,B+ 树是 B 树的一
- BTree 索引:MySQL 里默认和最常用的索引类型。只有叶子节点存储 value,非叶子节点只有指针和 key。存储引擎 MyISAM 和 InnoDB 实现 BTree 索引都是使用 B+Tree,但二者实现方式不一样(前面已经介绍了)。
- 哈希索引:类似键值对的形式,一次即可定位。
- RTree 索引:一般不会使用,仅支持 geometry 数据类型,优势在于范围查找,效率较低,通常使用搜索引擎如 ElasticSearch 代替。
-- 全文索引:对文本的内容进行分词,进行搜索。目前只有 `CHAR`、`VARCHAR` ,`TEXT` 列上可以创建全文索引。一般不会使用,效率较低,通常使用搜索引擎如 ElasticSearch 代替。
+- 全文索引:对文本的内容进行分词,进行搜索。目前只有 `CHAR`、`VARCHAR`、`TEXT` 列上可以创建全文索引。一般不会使用,效率较低,通常使用搜索引擎如 ElasticSearch 代替。
按照底层存储方式角度划分:
- 聚簇索引(聚集索引):索引结构和数据一起存放的索引,InnoDB 中的主键索引就属于聚簇索引。
-- 非聚簇索引(非聚集索引):索引结构和数据分开存放的索引,二级索引(辅助索引)就属于非聚簇索引。MySQL 的 MyISAM 引擎,不管主键还是非主键,使用的都是非聚簇索引。
+- 非聚簇索引(非聚集索引):索引结构和数据分开存放的索引,二级索引(辅助索引)就属于非聚簇索引。MySQL 的 MyISAM 引擎,不管主键还是非主键,使用的都是非聚簇索引。
按照应用维度划分:
@@ -154,7 +165,8 @@ B 树也称 B-树,全称为 **多路平衡查找树** ,B+ 树是 B 树的一
- 唯一索引:加速查询 + 列值唯一(可以有 NULL)。
- 覆盖索引:一个索引包含(或者说覆盖)所有需要查询的字段的值。
- 联合索引:多列值组成一个索引,专门用于组合搜索,其效率大于索引合并。
-- 全文索引:对文本的内容进行分词,进行搜索。目前只有 `CHAR`、`VARCHAR` ,`TEXT` 列上可以创建全文索引。一般不会使用,效率较低,通常使用搜索引擎如 ElasticSearch 代替。
+- 全文索引:对文本的内容进行分词,进行搜索。目前只有 `CHAR`、`VARCHAR`、`TEXT` 列上可以创建全文索引。一般不会使用,效率较低,通常使用搜索引擎如 ElasticSearch 代替。
+- 前缀索引:对文本的前几个字符创建索引,相比普通索引建立的数据更小,因为只取前几个字符。
MySQL 8.x 中实现的索引新特性:
@@ -162,7 +174,7 @@ MySQL 8.x 中实现的索引新特性:
- 降序索引:之前的版本就支持通过 desc 来指定索引为降序,但实际上创建的仍然是常规的升序索引。直到 MySQL 8.x 版本才开始真正支持降序索引。另外,在 MySQL 8.x 版本中,不再对 GROUP BY 语句进行隐式排序。
- 函数索引:从 MySQL 8.0.13 版本开始支持在索引中使用函数或者表达式的值,也就是在索引中可以包含函数或者表达式。
-## 主键索引(Primary Key)
+## 主键索引(Primary Key)
数据表的主键列使用的就是主键索引。
@@ -176,16 +188,16 @@ MySQL 8.x 中实现的索引新特性:
二级索引(Secondary Index)的叶子节点存储的数据是主键的值,也就是说,通过二级索引可以定位主键的位置,二级索引又称为辅助索引/非主键索引。
-唯一索引,普通索引,前缀索引等索引都属于二级索引。
+唯一索引、普通索引、前缀索引等索引都属于二级索引。
-PS: 不懂的同学可以暂存疑,慢慢往下看,后面会有答案的,也可以自行搜索。
+PS:不懂的同学可以暂存疑,慢慢往下看,后面会有答案的,也可以自行搜索。
-1. **唯一索引(Unique Key)**:唯一索引也是一种约束。唯一索引的属性列不能出现重复的数据,但是允许数据为 NULL,一张表允许创建多个唯一索引。 建立唯一索引的目的大部分时候都是为了该属性列的数据的唯一性,而不是为了查询效率。
-2. **普通索引(Index)**:普通索引的唯一作用就是为了快速查询数据,一张表允许创建多个普通索引,并允许数据重复和 NULL。
-3. **前缀索引(Prefix)**:前缀索引只适用于字符串类型的数据。前缀索引是对文本的前几个字符创建索引,相比普通索引建立的数据更小,因为只取前几个字符。
-4. **全文索引(Full Text)**:全文索引主要是为了检索大文本数据中的关键字的信息,是目前搜索引擎数据库使用的一种技术。Mysql5.6 之前只有 MYISAM 引擎支持全文索引,5.6 之后 InnoDB 也支持了全文索引。
+1. **唯一索引(Unique Key)**:唯一索引也是一种约束。唯一索引的属性列不能出现重复的数据,但是允许数据为 NULL,一张表允许创建多个唯一索引。 建立唯一索引的目的大部分时候都是为了该属性列的数据的唯一性,而不是为了查询效率。
+2. **普通索引(Index)**:普通索引的唯一作用就是为了快速查询数据。一张表允许创建多个普通索引,并允许数据重复和 NULL。
+3. **前缀索引(Prefix)**:前缀索引只适用于字符串类型的数据。前缀索引是对文本的前几个字符创建索引,相比普通索引建立的数据更小,因为只取前几个字符。
+4. **全文索引(Full Text)**:全文索引主要是为了检索大文本数据中的关键字的信息,是目前搜索引擎数据库使用的一种技术。Mysql5.6 之前只有 MyISAM 引擎支持全文索引,5.6 之后 InnoDB 也支持了全文索引。
-二级索引:
+二级索引:

@@ -197,25 +209,25 @@ PS: 不懂的同学可以暂存疑,慢慢往下看,后面会有答案的,
聚簇索引(Clustered Index)即索引结构和数据一起存放的索引,并不是一种单独的索引类型。InnoDB 中的主键索引就属于聚簇索引。
-在 MySQL 中,InnoDB 引擎的表的 `.ibd`文件就包含了该表的索引和数据,对于 InnoDB 引擎表来说,该表的索引(B+树)的每个非叶子节点存储索引,叶子节点存储索引和索引对应的数据。
+在 MySQL 中,InnoDB 引擎的表的 `.ibd`文件就包含了该表的索引和数据,对于 InnoDB 引擎表来说,该表的索引(B+ 树)的每个非叶子节点存储索引,叶子节点存储索引和索引对应的数据。
#### 聚簇索引的优缺点
**优点**:
-- **查询速度非常快**:聚簇索引的查询速度非常的快,因为整个 B+树本身就是一颗多叉平衡树,叶子节点也都是有序的,定位到索引的节点,就相当于定位到了数据。相比于非聚簇索引, 聚簇索引少了一次读取数据的 IO 操作。
+- **查询速度非常快**:聚簇索引的查询速度非常的快,因为整个 B+ 树本身就是一颗多叉平衡树,叶子节点也都是有序的,定位到索引的节点,就相当于定位到了数据。相比于非聚簇索引, 聚簇索引少了一次读取数据的 IO 操作。
- **对排序查找和范围查找优化**:聚簇索引对于主键的排序查找和范围查找速度非常快。
**缺点**:
-- **依赖于有序的数据**:因为 B+树是多路平衡树,如果索引的数据不是有序的,那么就需要在插入时排序,如果数据是整型还好,否则类似于字符串或 UUID 这种又长又难比较的数据,插入或查找的速度肯定比较慢。
+- **依赖于有序的数据**:因为 B+ 树是多路平衡树,如果索引的数据不是有序的,那么就需要在插入时排序,如果数据是整型还好,否则类似于字符串或 UUID 这种又长又难比较的数据,插入或查找的速度肯定比较慢。
- **更新代价大**:如果对索引列的数据被修改时,那么对应的索引也将会被修改,而且聚簇索引的叶子节点还存放着数据,修改代价肯定是较大的,所以对于主键索引来说,主键一般都是不可被修改的。
### 非聚簇索引(非聚集索引)
#### 非聚簇索引介绍
-非聚簇索引(Non-Clustered Index)即索引结构和数据分开存放的索引,并不是一种单独的索引类型。二级索引(辅助索引)就属于非聚簇索引。MySQL 的 MyISAM 引擎,不管主键还是非主键,使用的都是非聚簇索引。
+非聚簇索引(Non-Clustered Index)即索引结构和数据分开存放的索引,并不是一种单独的索引类型。二级索引(辅助索引)就属于非聚簇索引。MySQL 的 MyISAM 引擎,不管主键还是非主键,使用的都是非聚簇索引。
非聚簇索引的叶子节点并不一定存放数据的指针,因为二级索引的叶子节点就存放的是主键,根据主键再回表查数据。
@@ -223,22 +235,22 @@ PS: 不懂的同学可以暂存疑,慢慢往下看,后面会有答案的,
**优点**:
-更新代价比聚簇索引要小 。非聚簇索引的更新代价就没有聚簇索引那么大了,非聚簇索引的叶子节点是不存放数据的。
+更新代价比聚簇索引要小。非聚簇索引的更新代价就没有聚簇索引那么大了,非聚簇索引的叶子节点是不存放数据的。
**缺点**:
-- **依赖于有序的数据**:跟聚簇索引一样,非聚簇索引也依赖于有序的数据
-- **可能会二次查询(回表)**:这应该是非聚簇索引最大的缺点了。 当查到索引对应的指针或主键后,可能还需要根据指针或主键再到数据文件或表中查询。
+- **依赖于有序的数据**:跟聚簇索引一样,非聚簇索引也依赖于有序的数据。
+- **可能会二次查询(回表)**:这应该是非聚簇索引最大的缺点了。当查到索引对应的指针或主键后,可能还需要根据指针或主键再到数据文件或表中查询。
-这是 MySQL 的表的文件截图:
+这是 MySQL 的表的文件截图:

-聚簇索引和非聚簇索引:
+聚簇索引和非聚簇索引:

-#### 非聚簇索引一定回表查询吗(覆盖索引)?
+#### 非聚簇索引一定回表查询吗(覆盖索引)?
**非聚簇索引不一定回表查询。**
@@ -250,7 +262,7 @@ PS: 不懂的同学可以暂存疑,慢慢往下看,后面会有答案的,
那么这个索引的 key 本身就是 name,查到对应的 name 直接返回就行了,无需回表查询。
-即使是 MYISAM 也是这样,虽然 MYISAM 的主键索引确实需要回表,因为它的主键索引的叶子节点存放的是指针。但是!**如果 SQL 查的就是主键呢?**
+即使是 MyISAM 也是这样,虽然 MyISAM 的主键索引确实需要回表,因为它的主键索引的叶子节点存放的是指针。但是!**如果 SQL 查的就是主键呢?**
```sql
SELECT id FROM table WHERE id=1;
@@ -262,7 +274,7 @@ SELECT id FROM table WHERE id=1;
### 覆盖索引
-如果一个索引包含(或者说覆盖)所有需要查询的字段的值,我们就称之为 **覆盖索引(Covering Index)** 。
+如果一个索引包含(或者说覆盖)所有需要查询的字段的值,我们就称之为 **覆盖索引(Covering Index)**。
在 InnoDB 存储引擎中,非主键索引的叶子节点包含的是主键的值。这意味着,当使用非主键索引进行查询时,数据库会先找到对应的主键值,然后再通过主键索引来定位和检索完整的行数据。这个过程被称为“回表”。
@@ -275,7 +287,7 @@ SELECT id FROM table WHERE id=1;
我们这里简单演示一下覆盖索引的效果。
-1、创建一个名为 `cus_order` 的表,来实际测试一下这种排序方式。为了测试方便, `cus_order` 这张表只有 `id`、`score`、`name`这 3 个字段。
+1、创建一个名为 `cus_order` 的表,来实际测试一下这种排序方式。为了测试方便,`cus_order` 这张表只有 `id`、`score`、`name` 这 3 个字段。
```sql
CREATE TABLE `cus_order` (
@@ -319,7 +331,7 @@ CALL BatchinsertDataToCusOder(1, 1000000); # 插入100w+的随机数据
SELECT `score`,`name` FROM `cus_order` ORDER BY `score` DESC;
```
-使用 `EXPLAIN` 命令分析这条 SQL 语句,通过 `Extra` 这一列的 `Using filesort` ,我们发现是没有用到覆盖索引的。
+使用 `EXPLAIN` 命令分析这条 SQL 语句,通过 `Extra` 这一列的 `Using filesort`,我们发现是没有用到覆盖索引的。

@@ -335,7 +347,7 @@ ALTER TABLE `cus_order` ADD INDEX id_score_name(score, name);

-通过 `Extra` 这一列的 `Using index` ,说明这条 SQL 语句成功使用了覆盖索引。
+通过 `Extra` 这一列的 `Using index`,说明这条 SQL 语句成功使用了覆盖索引。
关于 `EXPLAIN` 命令的详细介绍请看:[MySQL 执行计划分析](./mysql-query-execution-plan.md)这篇文章。
@@ -355,13 +367,13 @@ ALTER TABLE `cus_order` ADD INDEX id_score_name(score, name);
最左匹配原则会一直向右匹配,直到遇到范围查询(如 >、<)为止。对于 >=、<=、BETWEEN 以及前缀匹配 LIKE 的范围查询,不会停止匹配(相关阅读:[联合索引的最左匹配原则全网都在说的一个错误结论](https://mp.weixin.qq.com/s/8qemhRg5MgXs1So5YCv0fQ))。
-假设有一个联合索引`(column1, column2, column3)`,其从左到右的所有前缀为`(column1)`、`(column1, column2)`、`(column1, column2, column3)`(创建 1 个联合索引相当于创建了 3 个索引),包含这些列的所有查询都会走索引而不会全表扫描。
+假设有一个联合索引 `(column1, column2, column3)`,其从左到右的所有前缀为 `(column1)`、`(column1, column2)`、`(column1, column2, column3)`(创建 1 个联合索引相当于创建了 3 个索引),包含这些列的所有查询都会走索引而不会全表扫描。
我们在使用联合索引时,可以将区分度高的字段放在最左边,这也可以过滤更多数据。
我们这里简单演示一下最左前缀匹配的效果。
-1、创建一个名为 `student` 的表,这张表只有 `id`、`name`、`class`这 3 个字段。
+1、创建一个名为 `student` 的表,这张表只有 `id`、`name`、`class` 这 3 个字段。
```sql
CREATE TABLE `student` (
@@ -385,21 +397,22 @@ EXPLAIN SELECT * FROM student WHERE name = 'Anne Henry' AND class = 'lIrm08RYVk'
SELECT * FROM student WHERE class = 'lIrm08RYVk';
```
-再来看一个常见的面试题:如果有索引 `联合索引(a,b,c)`,查询 `a=1 AND c=1`会走索引么?`c=1` 呢?`b=1 AND c=1`呢?
+再来看一个常见的面试题:如果有索引 `联合索引(a,b,c)`,查询 `a=1 AND c=1` 会走索引么?`c=1` 呢?`b=1 AND c=1` 呢? `b = 1 AND a = 1 AND c = 1` 呢?
先不要往下看答案,给自己 3 分钟时间想一想。
1. 查询 `a=1 AND c=1`:根据最左前缀匹配原则,查询可以使用索引的前缀部分。因此,该查询仅在 `a=1` 上使用索引,然后对结果进行 `c=1` 的过滤。
-2. 查询 `c=1` :由于查询中不包含最左列 `a`,根据最左前缀匹配原则,整个索引都无法被使用。
-3. 查询`b=1 AND c=1`:和第二种一样的情况,整个索引都不会使用。
+2. 查询 `c=1`:由于查询中不包含最左列 `a`,根据最左前缀匹配原则,整个索引都无法被使用。
+3. 查询 `b=1 AND c=1`:和第二种一样的情况,整个索引都不会使用。
+4. 查询 `b=1 AND a=1 AND c=1`:这个查询是可以用到索引的。查询优化器分析 SQL 语句时,对于联合索引,会对查询条件进行重排序,以便用到索引。会将 `b=1` 和 `a=1` 的条件进行重排序,变成 `a=1 AND b=1 AND c=1`。
MySQL 8.0.13 版本引入了索引跳跃扫描(Index Skip Scan,简称 ISS),它可以在某些索引查询场景下提高查询效率。在没有 ISS 之前,不满足最左前缀匹配原则的联合索引查询中会执行全表扫描。而 ISS 允许 MySQL 在某些情况下避免全表扫描,即使查询条件不符合最左前缀。不过,这个功能比较鸡肋, 和 Oracle 中的没法比,MySQL 8.0.31 还报告了一个 bug:[Bug #109145 Using index for skip scan cause incorrect result](https://bugs.mysql.com/bug.php?id=109145)(后续版本已经修复)。个人建议知道有这个东西就好,不需要深究,实际项目也不一定能用上。
## 索引下推
-**索引下推(Index Condition Pushdown,简称 ICP)** 是 **MySQL 5.6** 版本中提供的一项索引优化功能,它允许存储引擎在索引遍历过程中,执行部分 `WHERE`字句的判断条件,直接过滤掉不满足条件的记录,从而减少回表次数,提高查询效率。
+**索引下推(Index Condition Pushdown,简称 ICP)** 是 **MySQL 5.6** 版本中提供的一项索引优化功能,它允许存储引擎在索引遍历过程中,执行部分 `WHERE` 字句的判断条件,直接过滤掉不满足条件的记录,从而减少回表次数,提高查询效率。
-假设我们有一个名为 `user` 的表,其中包含 `id`, `username`, `zipcode`和 `birthdate` 4 个字段,创建了联合索引`(zipcode, birthdate)`。
+假设我们有一个名为 `user` 的表,其中包含 `id`、`username`、`zipcode` 和 `birthdate` 4 个字段,创建了联合索引 `(zipcode, birthdate)`。
```sql
CREATE TABLE `user` (
@@ -416,7 +429,7 @@ SELECT * FROM user WHERE zipcode = '431200' AND MONTH(birthdate) = 3;
```
- 没有索引下推之前,即使 `zipcode` 字段利用索引可以帮助我们快速定位到 `zipcode = '431200'` 的用户,但我们仍然需要对每一个找到的用户进行回表操作,获取完整的用户数据,再去判断 `MONTH(birthdate) = 3`。
-- 有了索引下推之后,存储引擎会在使用`zipcode` 字段索引查找`zipcode = '431200'` 的用户时,同时判断`MONTH(birthdate) = 3`。这样,只有同时满足条件的记录才会被返回,减少了回表次数。
+- 有了索引下推之后,存储引擎会在使用 `zipcode` 字段索引查找 `zipcode = '431200'` 的用户时,同时判断 `MONTH(birthdate) = 3`。这样,只有同时满足条件的记录才会被返回,减少了回表次数。

@@ -428,14 +441,14 @@ SELECT * FROM user WHERE zipcode = '431200' AND MONTH(birthdate) = 3;
MySQL 可以简单分为 Server 层和存储引擎层这两层。Server 层处理查询解析、分析、优化、缓存以及与客户端的交互等操作,而存储引擎层负责数据的存储和读取,MySQL 支持 InnoDB、MyISAM、Memory 等多种存储引擎。
-索引下推的**下推**其实就是指将部分上层(Server 层)负责的事情,交给了下层(存储引擎层)去处理。
+索引下推的 **下推** 其实就是指将部分上层(Server 层)负责的事情,交给了下层(存储引擎层)去处理。
我们这里结合索引下推原理再对上面提到的例子进行解释。
没有索引下推之前:
- 存储引擎层先根据 `zipcode` 索引字段找到所有 `zipcode = '431200'` 的用户的主键 ID,然后二次回表查询,获取完整的用户数据;
-- 存储引擎层把所有 `zipcode = '431200'` 的用户数据全部交给 Server 层,Server 层根据`MONTH(birthdate) = 3`这一条件再进一步做筛选。
+- 存储引擎层把所有 `zipcode = '431200'` 的用户数据全部交给 Server 层,Server 层根据 `MONTH(birthdate) = 3` 这一条件再进一步做筛选。
有了索引下推之后:
@@ -448,8 +461,8 @@ MySQL 可以简单分为 Server 层和存储引擎层这两层。Server 层处
最后,总结一下索引下推应用范围:
1. 适用于 InnoDB 引擎和 MyISAM 引擎的查询。
-2. 适用于执行计划是 range, ref, eq_ref, ref_or_null 的范围查询。
-3. 对于 InnoDB 表,仅用于非聚簇索引。索引下推的目标是减少全行读取次数,从而减少 I/O 操作。对于 InnoDB 聚集索引,完整的记录已经读入 InnoDB 缓冲区。在这种情况下使用索引下推 不会减少 I/O。
+2. 适用于执行计划是 range、ref、eq_ref、ref_or_null 的范围查询。
+3. 对于 InnoDB 表,仅用于非聚簇索引。索引下推的目标是减少全行读取次数,从而减少 I/O 操作。对于 InnoDB 聚集索引,完整的记录已经读入 InnoDB 缓冲区。在这种情况下使用索引下推不会减少 I/O。
4. 子查询不能使用索引下推,因为子查询通常会创建临时表来处理结果,而这些临时表是没有索引的。
5. 存储过程不能使用索引下推,因为存储引擎无法调用存储函数。
@@ -457,7 +470,7 @@ MySQL 可以简单分为 Server 层和存储引擎层这两层。Server 层处
### 选择合适的字段创建索引
-- **不为 NULL 的字段**:索引字段的数据应该尽量不为 NULL,因为对于数据为 NULL 的字段,数据库较难优化。如果字段频繁被查询,但又避免不了为 NULL,建议使用 0,1,true,false 这样语义较为清晰的短值或短字符作为替代。
+- **不为 NULL 的字段**:索引字段的数据应该尽量不为 NULL,因为对于数据为 NULL 的字段,数据库较难优化。如果字段频繁被查询,但又避免不了为 NULL,建议使用 0、1、true、false 这样语义较为清晰的短值或短字符作为替代。
- **被频繁查询的字段**:我们创建索引的字段应该是查询操作非常频繁的字段。
- **被作为条件查询的字段**:被作为 WHERE 条件查询的字段,应该被考虑建立索引。
- **频繁需要排序的字段**:索引已经排序,这样查询可以利用索引的排序,加快排序查询时间。
@@ -469,19 +482,19 @@ MySQL 可以简单分为 Server 层和存储引擎层这两层。Server 层处
### 限制每张表上的索引数量
-索引并不是越多越好,建议单张表索引不超过 5 个!索引可以提高效率同样可以降低效率。
+索引并不是越多越好,建议单张表索引不超过 5 个!索引可以提高效率,同样可以降低效率。
索引可以增加查询效率,但同样也会降低插入和更新的效率,甚至有些情况下会降低查询效率。
-因为 MySQL 优化器在选择如何优化查询时,会根据统一信息,对每一个可以用到的索引来进行评估,以生成出一个最好的执行计划,如果同时有很多个索引都可以用于查询,就会增加 MySQL 优化器生成执行计划的时间,同样会降低查询性能。
+因为 MySQL 优化器在选择如何优化查询时,会根据统计信息,对每一个可以用到的索引来进行评估,以生成出一个最好的执行计划,如果同时有很多个索引都可以用于查询,就会增加 MySQL 优化器生成执行计划的时间,同样会降低查询性能。
### 尽可能的考虑建立联合索引而不是单列索引
-因为索引是需要占用磁盘空间的,可以简单理解为每个索引都对应着一颗 B+树。如果一个表的字段过多,索引过多,那么当这个表的数据达到一个体量后,索引占用的空间也是很多的,且修改索引时,耗费的时间也是较多的。如果是联合索引,多个字段在一个索引上,那么将会节约很大磁盘空间,且修改数据的操作效率也会提升。
+因为索引是需要占用磁盘空间的,可以简单理解为每个索引都对应着一颗 B+ 树。如果一个表的字段过多,索引过多,那么当这个表的数据达到一个体量后,索引占用的空间也是很多的,且修改索引时,耗费的时间也是较多的。如果是联合索引,多个字段在一个索引上,那么将会节约很大磁盘空间,且修改数据的操作效率也会提升。
### 注意避免冗余索引
-冗余索引指的是索引的功能相同,能够命中索引(a, b)就肯定能命中索引(a) ,那么索引(a)就是冗余索引。如(name,city )和(name )这两个索引就是冗余索引,能够命中前者的查询肯定是能够命中后者的 在大多数情况下,都应该尽量扩展已有的索引而不是创建新索引。
+冗余索引指的是索引的功能相同,能够命中索引(a, b)就肯定能命中索引(a) ,那么索引(a)就是冗余索引。如(name,city)和(name)这两个索引就是冗余索引,能够命中前者的查询肯定是能够命中后者的。在大多数情况下,都应该尽量扩展已有的索引而不是创建新索引。
### 字符串类型的字段使用前缀索引代替普通索引
@@ -491,13 +504,13 @@ MySQL 可以简单分为 Server 层和存储引擎层这两层。Server 层处
索引失效也是慢查询的主要原因之一,常见的导致索引失效的情况有下面这些:
-- ~~使用 `SELECT *` 进行查询;~~ `SELECT *` 不会直接导致索引失效(如果不走索引大概率是因为 where 查询范围过大导致的),但它可能会带来一些其他的性能问题比如造成网络传输和数据处理的浪费、无法使用索引覆盖;
-- 创建了组合索引,但查询条件未遵守最左匹配原则;
-- 在索引列上进行计算、函数、类型转换等操作;
-- 以 % 开头的 LIKE 查询比如 `LIKE '%abc';`;
-- 查询条件中使用 OR,且 OR 的前后条件中有一个列没有索引,涉及的索引都不会被使用到;
-- IN 的取值范围较大时会导致索引失效,走全表扫描(NOT IN 和 IN 的失效场景相同);
-- 发生[隐式转换](https://javaguide.cn/database/mysql/index-invalidation-caused-by-implicit-conversion.html);
+- ~~使用 `SELECT *` 进行查询;~~ `SELECT *` 不会直接导致索引失效(如果不走索引大概率是因为 where 查询范围过大导致的),但它可能会带来一些其他的性能问题比如造成网络传输和数据处理的浪费、无法使用索引覆盖;
+- 创建了组合索引,但查询条件未遵守最左匹配原则;
+- 在索引列上进行计算、函数、类型转换等操作;
+- 以 % 开头的 LIKE 查询比如 `LIKE '%abc';`;
+- 查询条件中使用 OR,且 OR 的前后条件中有一个列没有索引,涉及的索引都不会被使用到;
+- IN 的取值范围较大时会导致索引失效,走全表扫描(NOT IN 和 IN 的失效场景相同);
+- 发生[隐式转换](https://javaguide.cn/database/mysql/index-invalidation-caused-by-implicit-conversion.html);
- ……
推荐阅读这篇文章:[美团暑期实习一面:MySQl 索引失效的场景有哪些?](https://mp.weixin.qq.com/s/mwME3qukHBFul57WQLkOYg)。
@@ -508,7 +521,7 @@ MySQL 可以简单分为 Server 层和存储引擎层这两层。Server 层处
MySQL 5.7 可以通过查询 `sys` 库的 `schema_unused_indexes` 视图来查询哪些索引从未被使用。
-### 知道如何分析语句是否走索引查询
+### 知道如何分析 SQL 语句是否走索引查询
我们可以使用 `EXPLAIN` 命令来分析 SQL 的 **执行计划** ,这样就知道语句是否命中索引了。执行计划是指一条 SQL 语句在经过 MySQL 查询优化器的优化会后,具体的执行方式。
diff --git a/docs/database/mysql/mysql-logs.md b/docs/database/mysql/mysql-logs.md
index ac7e29db2f3..bc484746517 100644
--- a/docs/database/mysql/mysql-logs.md
+++ b/docs/database/mysql/mysql-logs.md
@@ -1,8 +1,13 @@
---
title: MySQL三大日志(binlog、redo log和undo log)详解
+description: 深入解析MySQL三大日志binlog、redo log和undo log的作用与原理,详解两阶段提交保证数据一致性的机制,以及日志在崩溃恢复和主从复制中的应用。
category: 数据库
tag:
- MySQL
+head:
+ - - meta
+ - name: keywords
+ content: MySQL日志,binlog,redo log,undo log,两阶段提交,崩溃恢复,主从复制,WAL,事务日志
---
> 本文来自公号程序猿阿星投稿,JavaGuide 对其做了补充完善。
@@ -41,16 +46,16 @@ MySQL 中数据是以页为单位,你查询一条记录,会从硬盘把一
### 刷盘时机
-InnoDB 刷新重做日志的时机有几种情况:
+在 InnoDB 存储引擎中,**redo log buffer**(重做日志缓冲区)是一块用于暂存 redo log 的内存区域。为了确保事务的持久性和数据的一致性,InnoDB 会在特定时机将这块缓冲区中的日志数据刷新到磁盘上的 redo log 文件中。这些时机可以归纳为以下六种:
-InnoDB 将 redo log 刷到磁盘上有几种情况:
-
-1. 事务提交:当事务提交时,log buffer 里的 redo log 会被刷新到磁盘(可以通过`innodb_flush_log_at_trx_commit`参数控制,后文会提到)。
-2. log buffer 空间不足时:log buffer 中缓存的 redo log 已经占满了 log buffer 总容量的大约一半左右,就需要把这些日志刷新到磁盘上。
-3. 事务日志缓冲区满:InnoDB 使用一个事务日志缓冲区(transaction log buffer)来暂时存储事务的重做日志条目。当缓冲区满时,会触发日志的刷新,将日志写入磁盘。
-4. Checkpoint(检查点):InnoDB 定期会执行检查点操作,将内存中的脏数据(已修改但尚未写入磁盘的数据)刷新到磁盘,并且会将相应的重做日志一同刷新,以确保数据的一致性。
-5. 后台刷新线程:InnoDB 启动了一个后台线程,负责周期性(每隔 1 秒)地将脏页(已修改但尚未写入磁盘的数据页)刷新到磁盘,并将相关的重做日志一同刷新。
-6. 正常关闭服务器:MySQL 关闭的时候,redo log 都会刷入到磁盘里去。
+1. **事务提交时(最核心)**:当事务提交时,log buffer 里的 redo log 会被刷新到磁盘(可以通过`innodb_flush_log_at_trx_commit`参数控制,后文会提到)。
+2. **redo log buffer 空间不足时**:这是 InnoDB 的一种主动容量管理策略,旨在避免因缓冲区写满而导致用户线程阻塞。
+ - 当 redo log buffer 的已用空间超过其总容量的**一半 (50%)** 时,后台线程会**主动**将这部分日志刷新到磁盘,为后续的日志写入腾出空间,这是一种“未雨绸缪”的优化。
+ - 如果因为大事务或 I/O 繁忙导致 buffer 被**完全写满**,那么所有试图写入新日志的用户线程都会被**阻塞**,并强制进行一次同步刷盘,直到有可用空间为止。这种情况会影响数据库性能,应尽量避免。
+3. **触发检查点 (Checkpoint) 时**:Checkpoint 是 InnoDB 为了缩短崩溃恢复时间而设计的核心机制。当 Checkpoint 被触发时,InnoDB 需要将在此检查点之前的所有脏页刷写到磁盘。根据 **Write-Ahead Logging (WAL)** 原则,数据页写入磁盘前,其对应的 redo log 必须先落盘。因此,执行 Checkpoint 操作必然会确保相关的 redo log 也已经被刷新到了磁盘。
+4. **后台线程周期性刷新**:InnoDB 有一个后台的 master thread,它会大约每秒执行一次例行任务,其中就包括将 redo log buffer 中的日志刷新到磁盘。这个机制是 `innodb_flush_log_at_trx_commit` 设置为 0 或 2 时的主要持久化保障。
+5. **正常关闭服务器**:在 MySQL 服务器正常关闭的过程中,为了确保所有已提交事务的数据都被完整保存,InnoDB 会执行一次最终的刷盘操作,将 redo log buffer 中剩余的全部日志都清空并写入磁盘文件。
+6. **binlog 切换时**:当开启 binlog 后,在 MySQL 采用 `innodb_flush_log_at_trx_commit=1` 和 `sync_binlog=1` 的 双一配置下,为了保证 redo log 和 binlog 之间状态的一致性(用于崩溃恢复或主从复制),在 binlog 文件写满或者手动执行 flush logs 进行切换时,会触发 redo log 的刷盘动作。
总之,InnoDB 在多种情况下会刷新重做日志,以保证数据的持久性和一致性。
diff --git a/docs/database/mysql/mysql-query-cache.md b/docs/database/mysql/mysql-query-cache.md
index cdc49b2c59c..c98c5bdaf81 100644
--- a/docs/database/mysql/mysql-query-cache.md
+++ b/docs/database/mysql/mysql-query-cache.md
@@ -1,15 +1,13 @@
---
title: MySQL查询缓存详解
+description: 深入解析MySQL查询缓存的工作原理、配置管理及其优缺点,分析为什么MySQL 8.0移除了查询缓存功能,以及生产环境中的最佳实践建议。
category: 数据库
tag:
- MySQL
head:
- - meta
- name: keywords
- content: MySQL查询缓存,MySQL缓存机制中的内存管理
- - - meta
- - name: description
- content: 为了提高完全相同的查询语句的响应速度,MySQL Server 会对查询语句进行 Hash 计算得到一个 Hash 值。MySQL Server 不会对 SQL 做任何处理,SQL 必须完全一致 Hash 值才会一样。得到 Hash 值之后,通过该 Hash 值到查询缓存中匹配该查询的结果。MySQL 中的查询缓存虽然能够提升数据库的查询性能,但是查询同时也带来了额外的开销,每次查询后都要做一次缓存操作,失效后还要销毁。
+ content: MySQL查询缓存,Query Cache,MySQL缓存机制,缓存失效,MySQL 8.0,查询性能优化,MySQL内存管理
---
缓存是一个有效且实用的系统性能优化的手段,不论是操作系统还是各种软件和网站或多或少都用到了缓存。
diff --git a/docs/database/mysql/mysql-query-execution-plan.md b/docs/database/mysql/mysql-query-execution-plan.md
index 48eb5a1e5ed..6357163badd 100644
--- a/docs/database/mysql/mysql-query-execution-plan.md
+++ b/docs/database/mysql/mysql-query-execution-plan.md
@@ -1,15 +1,13 @@
---
title: MySQL执行计划分析
+description: 详解MySQL EXPLAIN执行计划的各列含义,包括id、select_type、type、key、rows、Extra等关键字段解读,帮助你分析SQL性能瓶颈并进行针对性优化。
category: 数据库
tag:
- MySQL
head:
- - meta
- name: keywords
- content: MySQL基础,MySQL执行计划,EXPLAIN,查询优化器
- - - meta
- - name: description
- content: 执行计划是指一条 SQL 语句在经过MySQL 查询优化器的优化会后,具体的执行方式。优化 SQL 的第一步应该是读懂 SQL 的执行计划。
+ content: MySQL执行计划,EXPLAIN,查询优化器,SQL性能分析,索引命中,type访问类型,Extra字段,慢查询优化
---
> 本文来自公号 MySQL 技术,JavaGuide 对其做了补充完善。原文地址:
@@ -18,7 +16,7 @@ head:
## 什么是执行计划?
-**执行计划** 是指一条 SQL 语句在经过 **MySQL 查询优化器** 的优化会后,具体的执行方式。
+**执行计划** 是指一条 SQL 语句在经过 **MySQL 查询优化器** 的优化后,具体的执行方式。
执行计划通常用于 SQL 性能分析、优化等场景。通过 `EXPLAIN` 的结果,可以了解到如数据表的查询顺序、数据查询操作的操作类型、哪些索引可以被命中、哪些索引实际会命中、每个数据表有多少行记录被查询等信息。
@@ -69,7 +67,7 @@ mysql> explain SELECT * FROM dept_emp WHERE emp_no IN (SELECT emp_no FROM dept_e
### id
-SELECT 标识符,是查询中 SELECT 的序号,用来标识整个查询中 SELELCT 语句的顺序。
+`SELECT` 标识符,用于标识每个 `SELECT` 语句的执行顺序。
id 如果相同,从上往下依次执行。id 不同,id 值越大,执行优先级越高,如果行引用其他行的并集结果,则该值可以为 NULL。
@@ -89,12 +87,14 @@ id 如果相同,从上往下依次执行。id 不同,id 值越大,执行
查询用到的表名,每行都有对应的表名,表名除了正常的表之外,也可能是以下列出的值:
- **``** : 本行引用了 id 为 M 和 N 的行的 UNION 结果;
-- **``** : 本行引用了 id 为 N 的表所产生的的派生表结果。派生表有可能产生自 FROM 语句中的子查询。
-- **``** : 本行引用了 id 为 N 的表所产生的的物化子查询结果。
+- **``** : 本行引用了 id 为 N 的表所产生的派生表结果。派生表有可能产生自 FROM 语句中的子查询。
+- **``** : 本行引用了 id 为 N 的表所产生的物化子查询结果。
### type(重要)
-查询执行的类型,描述了查询是如何执行的。所有值的顺序从最优到最差排序为:system > const > eq_ref > ref > fulltext > ref_or_null > index_merge > unique_subquery > index_subquery > range > index > ALL
+查询执行的类型,描述了查询是如何执行的。所有值的顺序从最优到最差排序为:
+
+system > const > eq_ref > ref > fulltext > ref_or_null > index_merge > unique_subquery > index_subquery > range > index > ALL
常见的几种类型具体含义如下:
@@ -109,7 +109,7 @@ id 如果相同,从上往下依次执行。id 不同,id 值越大,执行
### possible_keys
-possible_keys 列表示 MySQL 执行查询时可能用到的索引。如果这一列为 NULL ,则表示没有可能用到的索引;这种情况下,需要检查 WHERE 语句中所使用的的列,看是否可以通过给这些列中某个或多个添加索引的方法来提高查询性能。
+possible_keys 列表示 MySQL 执行查询时可能用到的索引。如果这一列为 NULL ,则表示没有可能用到的索引;这种情况下,需要检查 WHERE 语句中所使用的列,看是否可以通过给这些列中某个或多个添加索引的方法来提高查询性能。
### key(重要)
diff --git a/docs/database/mysql/mysql-questions-01.md b/docs/database/mysql/mysql-questions-01.md
index ebc6f4a26ce..99a0aa9e14f 100644
--- a/docs/database/mysql/mysql-questions-01.md
+++ b/docs/database/mysql/mysql-questions-01.md
@@ -1,5 +1,6 @@
---
title: MySQL常见面试题总结
+description: MySQL高频面试题精讲:基础架构、InnoDB引擎、索引原理、B+树、事务ACID、MVCC、redo/undo/binlog日志、行锁/表锁、慢查询优化,一文速通大厂必考点!
category: 数据库
tag:
- MySQL
@@ -7,10 +8,7 @@ tag:
head:
- - meta
- name: keywords
- content: MySQL基础,MySQL基础架构,MySQL存储引擎,MySQL查询缓存,MySQL事务,MySQL锁等内容。
- - - meta
- - name: description
- content: 一篇文章总结MySQL常见的知识点和面试题,涵盖MySQL基础、MySQL基础架构、MySQL存储引擎、MySQL查询缓存、MySQL事务、MySQL锁等内容。
+ content: MySQL面试题,MySQL基础架构,InnoDB存储引擎,MySQL索引,B+树索引,事务隔离级别,redo log,undo log,binlog,MVCC,行级锁,慢查询优化
---
@@ -55,20 +53,30 @@ SQL 可以帮助我们:
由于 MySQL 是开源免费并且比较成熟的数据库,因此,MySQL 被大量使用在各种系统中。任何人都可以在 GPL(General Public License) 的许可下下载并根据个性化的需要对其进行修改。MySQL 的默认端口号是**3306**。
-### MySQL 有什么优点?
+### ⭐️MySQL 有什么优点?
这个问题本质上是在问 MySQL 如此流行的原因。
-MySQL 主要具有下面这些优点:
+MySQL 成功可以归功于在**生态、功能和运维**这三个层面上的综合优势。
+
+**第一,从生态和成本角度看,它的护城河非常深。**
+
+- **开源免费:** 这是它得以广泛普及的基石。任何公司和个人都可以免费使用,极大地降低了技术门槛和初期成本。
+- **社区庞大,生态完善:** 经过几十年的发展,MySQL 拥有极其活跃的社区和丰富的生态系统。这意味着无论你遇到什么问题,几乎都能在网上找到解决方案;同时,市面上所有的主流编程语言、框架、ORM 工具、监控系统都对 MySQL 有完美的支持。它的文档也非常丰富,学习资源唾手可得。
+
+**第二,从核心技术功能上看,它非常强大且均衡。**
+
+- **强大的事务支持:** 这是它作为关系型数据库的立身之本。值得一提的是,InnoDB 默认的可重复读(REPEATABLE-READ)隔离级别,通过 MVCC 和 Next-Key Lock 机制,很大程度上避免了幻读问题,这在很多其他数据库中都需要更高的隔离级别才能做到,兼顾了性能和一致性。详细介绍可以阅读笔者写的这篇文章:[MySQL 事务隔离级别详解](https://javaguide.cn/database/mysql/transaction-isolation-level.html)。
+- **优秀的性能和可扩展性:** MySQL 本身经过了海量互联网业务的严酷考验,单机性能非常出色。更重要的是,它围绕着水平扩展,形成了一套非常成熟的架构方案,比如主从复制、读写分离、以及通过中间件实现的分库分表。这让它能够支撑从初创公司到大型互联网平台的各种规模的业务。
+
+**第三,从运维和使用角度看,它非常‘亲民’。**
+
+- **开箱即用,上手简单:** 相比于 Oracle 等大型商业数据库,MySQL 的安装、配置和日常使用都非常简单直观,学习曲线平缓,对于开发者和初级 DBA 非常友好。
+- **维护成本低:** 由于其简单性和庞大的社区,找到相关的运维人才和解决方案都相对容易,整体的维护成本也更低。
+
+值得一提的是最近几年,PostgreSQL 的势头很猛,甚至压过了 MySQL。网上出现了很多抨击诋毁 MySQL 的文章,笔者认为任何无脑抨击其中一方或者吹捧另外一方的行为都是不可取的。
-1. 成熟稳定,功能完善。
-2. 开源免费。
-3. 文档丰富,既有详细的官方文档,又有非常多优质文章可供参考学习。
-4. 开箱即用,操作简单,维护成本低。
-5. 兼容性好,支持常见的操作系统,支持多种开发语言。
-6. 社区活跃,生态完善。
-7. 事务支持优秀, InnoDB 存储引擎默认使用 REPEATABLE-READ 并不会有任何性能损失,并且,InnoDB 实现的 REPEATABLE-READ 隔离级别其实是可以解决幻读问题发生的。
-8. 支持分库分表、读写分离、高可用。
+笔者也写过一篇文章分享对这两个关系型数据库代表的看法,感兴趣的可以看看:[MySQL 被干成老二了?](https://mp.weixin.qq.com/s/APWD-PzTcTqGUuibAw7GGw)。
## MySQL 字段类型
@@ -86,7 +94,7 @@ MySQL 字段类型比较多,我这里会挑选一些日常开发使用很频
另外,推荐阅读一下《高性能 MySQL(第三版)》的第四章,有详细介绍 MySQL 字段类型优化。
-### 整数类型的 UNSIGNED 属性有什么用?
+### ⭐️整数类型的 UNSIGNED 属性有什么用?
MySQL 中的整数类型可以使用可选的 UNSIGNED 属性来表示不允许负值的无符号整数。使用 UNSIGNED 属性可以将正整数的上限提高一倍,因为它不需要存储负数值。
@@ -152,31 +160,75 @@ BLOB 类型主要用于存储二进制大对象,例如图片、音视频等文
- 可能导致表上的 DML 操作变慢。
- ……
-### DATETIME 和 TIMESTAMP 的区别是什么?
+### ⭐️DATETIME 和 TIMESTAMP 的区别是什么?如何选择?
DATETIME 类型没有时区信息,TIMESTAMP 和时区有关。
TIMESTAMP 只需要使用 4 个字节的存储空间,但是 DATETIME 需要耗费 8 个字节的存储空间。但是,这样同样造成了一个问题,Timestamp 表示的时间范围更小。
-- DATETIME:1000-01-01 00:00:00 ~ 9999-12-31 23:59:59
-- Timestamp:1970-01-01 00:00:01 ~ 2037-12-31 23:59:59
+- DATETIME:'1000-01-01 00:00:00.000000' 到 '9999-12-31 23:59:59.999999'
+- Timestamp:'1970-01-01 00:00:01.000000' UTC 到 '2038-01-19 03:14:07.999999' UTC
-关于两者的详细对比,请参考我写的[MySQL 时间类型数据存储建议](./some-thoughts-on-database-storage-time.md)。
+`TIMESTAMP` 的核心优势在于其内建的时区处理能力。数据库负责 UTC 存储和基于会话时区的自动转换,简化了需要处理多时区应用的开发。如果应用需要处理多时区,或者希望数据库能自动管理时区转换,`TIMESTAMP` 是自然的选择(注意其时间范围限制,也就是 2038 年问题)。
-### NULL 和 '' 的区别是什么?
+如果应用场景不涉及时区转换,或者希望应用程序完全控制时区逻辑,并且需要表示 2038 年之后的时间,`DATETIME` 是更稳妥的选择。
+
+关于两者的详细对比以及日期存储类型选择建议,请参考我写的这篇文章: [MySQL 时间类型数据存储建议](./some-thoughts-on-database-storage-time.md)。
-`NULL` 跟 `''`(空字符串)是两个完全不一样的值,区别如下:
+### NULL 和 '' 的区别是什么?
-- `NULL` 代表一个不确定的值,就算是两个 `NULL`,它俩也不一定相等。例如,`SELECT NULL=NULL`的结果为 false,但是在我们使用`DISTINCT`,`GROUP BY`,`ORDER BY`时,`NULL`又被认为是相等的。
-- `''`的长度是 0,是不占用空间的,而`NULL` 是需要占用空间的。
-- `NULL` 会影响聚合函数的结果。例如,`SUM`、`AVG`、`MIN`、`MAX` 等聚合函数会忽略 `NULL` 值。 `COUNT` 的处理方式取决于参数的类型。如果参数是 `*`(`COUNT(*)`),则会统计所有的记录数,包括 `NULL` 值;如果参数是某个字段名(`COUNT(列名)`),则会忽略 `NULL` 值,只统计非空值的个数。
-- 查询 `NULL` 值时,必须使用 `IS NULL` 或 `IS NOT NULLl` 来判断,而不能使用 =、!=、 <、> 之类的比较运算符。而`''`是可以使用这些比较运算符的。
+`NULL` 和 `''` (空字符串) 是两个完全不同的值,它们分别表示不同的含义,并在数据库中有着不同的行为。`NULL` 代表缺失或未知的数据,而 `''` 表示一个已知存在的空字符串。它们的主要区别如下:
+
+1. **含义**:
+ - `NULL` 代表一个不确定的值,它不等于任何值,包括它自身。因此,`SELECT NULL = NULL` 的结果是 `NULL`,而不是 `true` 或 `false`。 `NULL` 意味着缺失或未知的信息。虽然 `NULL` 不等于任何值,但在某些操作中,数据库系统会将 `NULL` 值视为相同的类别进行处理,例如:`DISTINCT`,`GROUP BY`,`ORDER BY`。需要注意的是,这些操作将 `NULL` 值视为相同的类别进行处理,并不意味着 `NULL` 值之间是相等的。 它们只是在特定操作中被特殊处理,以保证结果的正确性和一致性。 这种处理方式是为了方便数据操作,而不是改变了 `NULL` 的语义。
+ - `''` 表示一个空字符串,它是一个已知的值。
+2. **存储空间**:
+ - `NULL` 的存储空间占用取决于数据库的实现,通常需要一些空间来标记该值为空。
+ - `''` 的存储空间占用通常较小,因为它只存储一个空字符串的标志,不需要存储实际的字符。
+3. **比较运算**:
+ - 任何值与 `NULL` 进行比较(例如 `=`, `!=`, `>`, `<` 等)的结果都是 `NULL`,表示结果不确定。要判断一个值是否为 `NULL`,必须使用 `IS NULL` 或 `IS NOT NULL`。
+ - `''` 可以像其他字符串一样进行比较运算。例如,`'' = ''` 的结果是 `true`。
+4. **聚合函数**:
+ - 大多数聚合函数(例如 `SUM`, `AVG`, `MIN`, `MAX`)会忽略 `NULL` 值。
+ - `COUNT(*)` 会统计所有行数,包括包含 `NULL` 值的行。`COUNT(列名)` 会统计指定列中非 `NULL` 值的行数。
+ - 空字符串 `''` 会被聚合函数计算在内。例如,`SUM` 会将其视为 0,`MIN` 和 `MAX` 会将其视为一个空字符串。
看了上面的介绍之后,相信你对另外一个高频面试题:“为什么 MySQL 不建议使用 `NULL` 作为列默认值?”也有了答案。
-### Boolean 类型如何表示?
+### ⭐️Boolean 类型如何表示?
+
+MySQL 中没有专门的布尔类型,而是用 `TINYINT(1)` 类型来表示布尔值。`TINYINT(1)` 类型可以存储 0 或 1,分别对应 false 或 true。
+
+### ⭐️手机号存储用 INT 还是 VARCHAR?
+
+存储手机号,**强烈推荐使用 VARCHAR 类型**,而不是 INT 或 BIGINT。主要原因如下:
+
+1. **格式兼容性与完整性:**
+ - 手机号可能包含前导零(如某些地区的固话区号)、国家代码前缀('+'),甚至可能带有分隔符('-' 或空格)。INT 或 BIGINT 这种数字类型会自动丢失这些重要的格式信息(比如前导零会被去掉,'+' 和 '-' 无法存储)。
+ - VARCHAR 可以原样存储各种格式的号码,无论是国内的 11 位手机号,还是带有国家代码的国际号码,都能完美兼容。
+2. **非算术性:**手机号虽然看起来是数字,但我们从不对它进行数学运算(比如求和、平均值)。它本质上是一个标识符,更像是一个字符串。用 VARCHAR 更符合其数据性质。
+3. **查询灵活性:**
+ - 业务中常常需要根据号段(前缀)进行查询,例如查找所有 "138" 开头的用户。使用 VARCHAR 类型配合 `LIKE '138%'` 这样的 SQL 查询既直观又高效。
+ - 如果使用数字类型,进行类似的前缀匹配通常需要复杂的函数转换(如 CAST 或 SUBSTRING),或者使用范围查询(如 `WHERE phone >= 13800000000 AND phone < 13900000000`),这不仅写法繁琐,而且可能无法有效利用索引,导致性能下降。
+4. **加密存储的要求(非常关键):**
+ - 出于数据安全和隐私合规的要求,手机号这类敏感个人信息通常必须加密存储在数据库中。
+ - 加密后的数据(密文)是一长串字符串(通常由字母、数字、符号组成,或经过 Base64/Hex 编码),INT 或 BIGINT 类型根本无法存储这种密文。只有 VARCHAR、TEXT 或 BLOB 等类型可以。
-MySQL 中没有专门的布尔类型,而是用 TINYINT(1) 类型来表示布尔值。TINYINT(1) 类型可以存储 0 或 1,分别对应 false 或 true。
+**关于 VARCHAR 长度的选择:**
+
+- **如果不加密存储(强烈不推荐!):** 考虑到国际号码和可能的格式符,VARCHAR(20) 到 VARCHAR(32) 通常是一个比较安全的范围,足以覆盖全球绝大多数手机号格式。VARCHAR(15) 可能对某些带国家码和格式符的号码来说不够用。
+- **如果进行加密存储(推荐的标准做法):** 长度必须根据所选加密算法产生的密文最大长度,以及可能的编码方式(如 Base64 会使长度增加约 1/3)来精确计算和设定。通常会需要更长的 VARCHAR 长度,例如 VARCHAR(128), VARCHAR(256) 甚至更长。
+
+最后,来一张表格总结一下:
+
+| 对比维度 | VARCHAR 类型(推荐) | INT/BIGINT 类型(不推荐) | 说明/备注 |
+| ---------------- | --------------------------------- | ---------------------------- | --------------------------------------------------------------------------- |
+| **格式兼容性** | ✔ 能存前导零、"+"、"-"、空格等 | ✘ 自动丢失前导零,不能存符号 | VARCHAR 能原样存储各种手机号格式,INT/BIGINT 只支持单纯数字,且前导零会消失 |
+| **完整性** | ✔ 不丢失任何格式信息 | ✘ 丢失格式信息 | 例如 "013800012345" 存进 INT 会变成 13800012345,"+" 也无法存储 |
+| **非算术性** | ✔ 适合存储“标识符” | ✘ 只适合做数值运算 | 手机号本质是字符串标识符,不做数学运算,VARCHAR 更贴合实际用途 |
+| **查询灵活性** | ✔ 支持 `LIKE '138%'` 等 | ✘ 查询前缀不方便或性能差 | 使用 VARCHAR 可高效按号段/前缀查询,数字类型需转为字符串或其他复杂处理 |
+| **加密存储支持** | ✔ 可存储加密密文(字母、符号等) | ✘ 无法存储密文 | 加密手机号后密文是字符串/二进制,只有 VARCHAR、TEXT、BLOB 等能兼容 |
+| **长度设置建议** | 15~20(未加密),加密视情况而定 | 无意义 | 不加密时 VARCHAR(15~20) 通用,加密后长度取决于算法和编码方式 |
## MySQL 基础架构
@@ -193,7 +245,7 @@ MySQL 中没有专门的布尔类型,而是用 TINYINT(1) 类型来表示布
- **分析器:** 没有命中缓存的话,SQL 语句就会经过分析器,分析器说白了就是要先看你的 SQL 语句要干嘛,再检查你的 SQL 语句语法是否正确。
- **优化器:** 按照 MySQL 认为最优的方案去执行。
- **执行器:** 执行语句,然后从存储引擎返回数据。 执行语句之前会先判断是否有权限,如果没有权限的话,就会报错。
-- **插件式存储引擎**:主要负责数据的存储和读取,采用的是插件式架构,支持 InnoDB、MyISAM、Memory 等多种存储引擎。
+- **插件式存储引擎**:主要负责数据的存储和读取,采用的是插件式架构,支持 InnoDB、MyISAM、Memory 等多种存储引擎。InnoDB 是 MySQL 的默认存储引擎,绝大部分场景使用 InnoDB 就是最好的选择。
## MySQL 存储引擎
@@ -249,7 +301,7 @@ mysql> SHOW VARIABLES LIKE '%storage_engine%';
MySQL 存储引擎采用的是 **插件式架构** ,支持多种存储引擎,我们甚至可以为不同的数据库表设置不同的存储引擎以适应不同场景的需要。**存储引擎是基于表的,而不是数据库。**
-下图展示了具有可插拔存储引擎的 MySQL 架构():
+下图展示了具有可插拔存储引擎的 MySQL 架构:

@@ -257,7 +309,7 @@ MySQL 存储引擎采用的是 **插件式架构** ,支持多种存储引擎
MySQL 官方文档也有介绍到如何编写一个自定义存储引擎,地址: 。
-### MyISAM 和 InnoDB 有什么区别?
+### ⭐️MyISAM 和 InnoDB 有什么区别?
MySQL 5.5 之前,MyISAM 引擎是 MySQL 的默认存储引擎,可谓是风光一时。
@@ -341,19 +393,171 @@ InnoDB 使用缓冲池(Buffer Pool)缓存数据页和索引页,MyISAM 使
### MyISAM 和 InnoDB 如何选择?
-大多数时候我们使用的都是 InnoDB 存储引擎,在某些读密集的情况下,使用 MyISAM 也是合适的。不过,前提是你的项目不介意 MyISAM 不支持事务、崩溃恢复等缺点(可是~我们一般都会介意啊!)。
+大多数时候我们使用的都是 InnoDB 存储引擎,在某些读密集的情况下,使用 MyISAM 也是合适的。不过,前提是你的项目不介意 MyISAM 不支持事务、崩溃恢复等缺点(可是~我们一般都会介意啊)。
《MySQL 高性能》上面有一句话这样写到:
> 不要轻易相信“MyISAM 比 InnoDB 快”之类的经验之谈,这个结论往往不是绝对的。在很多我们已知场景中,InnoDB 的速度都可以让 MyISAM 望尘莫及,尤其是用到了聚簇索引,或者需要访问的数据都可以放入内存的应用。
-一般情况下我们选择 InnoDB 都是没有问题的,但是某些情况下你并不在乎可扩展能力和并发能力,也不需要事务支持,也不在乎崩溃后的安全恢复问题的话,选择 MyISAM 也是一个不错的选择。但是一般情况下,我们都是需要考虑到这些问题的。
+因此,对于咱们日常开发的业务系统来说,你几乎找不到什么理由使用 MyISAM 了,老老实实用默认的 InnoDB 就可以了!
+
+## ⭐️MySQL 索引
+
+MySQL 索引相关的问题比较多,也非常重要,更详细的介绍可以阅读笔者写的这篇文章:[MySQL 索引详解](./mysql-index.md) 。
+
+### 索引是什么?
+
+**索引是一种用于快速查询和检索数据的数据结构,其本质可以看成是一种排序好的数据结构。**
+
+索引的作用就相当于书的目录。打个比方:我们在查字典的时候,如果没有目录,那我们就只能一页一页地去找我们需要查的那个字,速度很慢;如果有目录了,我们只需要先去目录里查找字的位置,然后直接翻到那一页就行了。
+
+索引底层数据结构存在很多种类型,常见的索引结构有:B 树、 B+ 树 和 Hash、红黑树。在 MySQL 中,无论是 Innodb 还是 MyISAM,都使用了 B+ 树作为索引结构。
+
+**索引的优点:**
+
+1. **查询速度起飞 (主要目的)**:通过索引,数据库可以**大幅减少需要扫描的数据量**,直接定位到符合条件的记录,从而显著加快数据检索速度,减少磁盘 I/O 次数。
+2. **保证数据唯一性**:通过创建**唯一索引 (Unique Index)**,可以确保表中的某一列(或几列组合)的值是独一无二的,比如用户 ID、邮箱等。主键本身就是一种唯一索引。
+3. **加速排序和分组**:如果查询中的 ORDER BY 或 GROUP BY 子句涉及的列建有索引,数据库往往可以直接利用索引已经排好序的特性,避免额外的排序操作,从而提升性能。
+
+**索引的缺点:**
+
+1. **创建和维护耗时**:创建索引本身需要时间,特别是对大表操作时。更重要的是,当对表中的数据进行**增、删、改 (DML 操作)** 时,不仅要操作数据本身,相关的索引也必须动态更新和维护,这会**降低这些 DML 操作的执行效率**。
+2. **占用存储空间**:索引本质上也是一种数据结构,需要以物理文件(或内存结构)的形式存储,因此会**额外占用一定的磁盘空间**。索引越多、越大,占用的空间也就越多。
+3. **可能被误用或失效**:如果索引设计不当,或者查询语句写得不好,数据库优化器可能不会选择使用索引(或者选错索引),反而导致性能下降。
+
+**那么,用了索引就一定能提高查询性能吗?**
+
+**不一定。** 大多数情况下,合理使用索引确实比全表扫描快得多。但也有例外:
+
+- **数据量太小**:如果表里的数据非常少(比如就几百条),全表扫描可能比通过索引查找更快,因为走索引本身也有开销。
+- **查询结果集占比过大**:如果要查询的数据占了整张表的大部分(比如超过 20%-30%),优化器可能会认为全表扫描更划算,因为通过索引多次回表(随机 I/O)的成本可能高于一次顺序的全表扫描。
+- **索引维护不当或统计信息过时**:导致优化器做出错误判断。
+
+### 索引为什么快?
+
+索引之所以快,核心原因是它**大大减少了磁盘 I/O 的次数**。
+
+它的本质是一种**排好序的数据结构**,就像书的目录,让我们不用一页一页地翻(全表扫描)。
+
+在 MySQL 中,这个数据结构是**B+树**。B+树结构主要从两方面做了优化:
+
+1. B+树的特点是“矮胖”,一个千万数据的表,索引树的高度可能只有 3-4 层。这意味着,最多只需要**3-4 次磁盘 I/O**,就能精确定位到我想要的数据,而全表扫描可能需要成千上万次,所以速度极快。
+2. B+树的叶子节点是**用链表连起来的**。找到开头后,就能顺着链表**顺序读**下去,这对磁盘非常友好,还能触发预读。
+
+### MySQL 索引底层数据结构是什么?
+
+在 MySQL 中,MyISAM 引擎和 InnoDB 引擎都是使用 B+Tree 作为索引结构,详细介绍可以参考笔者写的这篇文章:[MySQL 索引详解](https://javaguide.cn/database/mysql/mysql-index.html)。
+
+### 为什么 InnoDB 没有使用哈希作为索引的数据结构?
+
+> 我发现很多求职者甚至是面试官对这个问题都有误解,他们相当然的认为 MySQL 底层并没有使用哈希或者 B 树作为索引的数据结构。
+>
+> 实际上,不论是提问还是回答这个问题都要区分好存储引擎。像 MEMORY 引擎就同时支持哈希和 B 树。
+
+哈希索引的底层是哈希表。它的优点是,在进行**精确的等值查询**时,理论上时间复杂度是 **O(1)** ,速度极快。比如 `WHERE id = 123`。
+
+但是,它有几个对于通用数据库来说是致命的缺点:
+
+1. **不支持范围查询:** 这是最主要的原因。哈希函数的一个特点是它会把相邻的输入值(比如 `id=100` 和 `id=101`)映射到哈希表中完全不相邻的位置。这种顺序的破坏,使得我们无法处理像 `WHERE age > 30` 或 `BETWEEN 100 AND 200`这样的范围查询。要完成这种查询,哈希索引只能退化为全表扫描。
+2. **不支持排序:** 同理,因为哈希值是无序的,所以我们无法利用哈希索引来优化 `ORDER BY` 子句。
+3. **不支持部分索引键查询:** 对于联合索引,比如`(col1, col2)`,哈希索引必须使用所有索引列进行查询,它无法单独利用 `col1` 来加速查询。
+4. **哈希冲突问题:** 当不同的键产生相同的哈希值时,需要额外的链表或开放寻址来解决,这会降低性能。
+
+鉴于数据库查询中范围查询和排序是极其常见的操作,一个不支持这些功能的索引结构,显然不能作为默认的、通用的索引类型。
+
+### 为什么 InnoDB 没有使用 B 树作为索引的数据结构?
+
+B 树和 B+树都是优秀的多路平衡搜索树,非常适合磁盘存储,因为它们都很“矮胖”,能最大化地利用每一次磁盘 I/O。
+
+但 B+树是 B 树的一个增强版,它针对数据库场景做了几个关键优化:
+
+1. **I/O 效率更高:** 在 B+树中,只有叶子节点才存储数据(或数据指针),而非叶子节点只存储索引键。因为非叶子节点不存数据,所以它们可以容纳更多的索引键。这意味着 B+树的“扇出”更大,在同样的数据量下,B+树通常会比 B 树更矮,也就意味着查找数据所需的磁盘 I/O 次数更少。
+2. **查询性能更稳定:** 在 B+树中,任何一次查询都必须从根节点走到叶子节点才能找到数据,所以查询路径的长度是固定的。而在 B 树中,如果运气好,可能在非叶子节点就找到了数据,但运气不好也得走到叶子,这导致查询性能不稳定。
+3. **对范围查询极其友好:** 这是 B+树最核心的优势。它的所有叶子节点之间通过一个双向链表连接。当我们执行一个范围查询(比如 `WHERE id > 100`)时,只需要通过树形结构找到 `id=100` 的叶子节点,然后就可以沿着链表向后顺序扫描,而无需再回溯到上层节点。这使得范围查询的效率大大提高。
+
+### 什么是覆盖索引?
+
+如果一个索引包含(或者说覆盖)所有需要查询的字段的值,我们就称之为 **覆盖索引(Covering Index)**。
+
+在 InnoDB 存储引擎中,非主键索引的叶子节点包含的是主键的值。这意味着,当使用非主键索引进行查询时,数据库会先找到对应的主键值,然后再通过主键索引来定位和检索完整的行数据。这个过程被称为“回表”。
+
+**覆盖索引即需要查询的字段正好是索引的字段,那么直接根据该索引,就可以查到数据了,而无需回表查询。**
+
+### 请解释一下 MySQL 的联合索引及其最左前缀原则
+
+使用表中的多个字段创建索引,就是 **联合索引**,也叫 **组合索引** 或 **复合索引**。
+
+以 `score` 和 `name` 两个字段建立联合索引:
+
+```sql
+ALTER TABLE `cus_order` ADD INDEX id_score_name(score, name);
+```
+
+最左前缀匹配原则指的是在使用联合索引时,MySQL 会根据索引中的字段顺序,从左到右依次匹配查询条件中的字段。如果查询条件与索引中的最左侧字段相匹配,那么 MySQL 就会使用索引来过滤数据,这样可以提高查询效率。
+
+最左匹配原则会一直向右匹配,直到遇到范围查询(如 >、<)为止。对于 >=、<=、BETWEEN 以及前缀匹配 LIKE 的范围查询,不会停止匹配(相关阅读:[联合索引的最左匹配原则全网都在说的一个错误结论](https://mp.weixin.qq.com/s/8qemhRg5MgXs1So5YCv0fQ))。
+
+假设有一个联合索引 `(column1, column2, column3)`,其从左到右的所有前缀为 `(column1)`、`(column1, column2)`、`(column1, column2, column3)`(创建 1 个联合索引相当于创建了 3 个索引),包含这些列的所有查询都会走索引而不会全表扫描。
+
+我们在使用联合索引时,可以将区分度高的字段放在最左边,这也可以过滤更多数据。
+
+我们这里简单演示一下最左前缀匹配的效果。
+
+1、创建一个名为 `student` 的表,这张表只有 `id`、`name`、`class` 这 3 个字段。
+
+```sql
+CREATE TABLE `student` (
+ `id` int NOT NULL,
+ `name` varchar(100) DEFAULT NULL,
+ `class` varchar(100) DEFAULT NULL,
+ PRIMARY KEY (`id`),
+ KEY `name_class_idx` (`name`,`class`)
+) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
+```
+
+2、下面我们分别测试三条不同的 SQL 语句。
+
+
+
+```sql
+# 可以命中索引
+SELECT * FROM student WHERE name = 'Anne Henry';
+EXPLAIN SELECT * FROM student WHERE name = 'Anne Henry' AND class = 'lIrm08RYVk';
+# 无法命中索引
+SELECT * FROM student WHERE class = 'lIrm08RYVk';
+```
+
+再来看一个常见的面试题:如果有索引 `联合索引(a,b,c)`,查询 `a=1 AND c=1` 会走索引么?`c=1` 呢?`b=1 AND c=1` 呢? `b = 1 AND a = 1 AND c = 1` 呢?
+
+先不要往下看答案,给自己 3 分钟时间想一想。
+
+1. 查询 `a=1 AND c=1`:根据最左前缀匹配原则,查询可以使用索引的前缀部分。因此,该查询仅在 `a=1` 上使用索引,然后对结果进行 `c=1` 的过滤。
+2. 查询 `c=1`:由于查询中不包含最左列 `a`,根据最左前缀匹配原则,整个索引都无法被使用。
+3. 查询 `b=1 AND c=1`:和第二种一样的情况,整个索引都不会使用。
+4. 查询 `b=1 AND a=1 AND c=1`:这个查询是可以用到索引的。查询优化器分析 SQL 语句时,对于联合索引,会对查询条件进行重排序,以便用到索引。会将 `b=1` 和 `a=1` 的条件进行重排序,变成 `a=1 AND b=1 AND c=1`。
+
+MySQL 8.0.13 版本引入了索引跳跃扫描(Index Skip Scan,简称 ISS),它可以在某些索引查询场景下提高查询效率。在没有 ISS 之前,不满足最左前缀匹配原则的联合索引查询中会执行全表扫描。而 ISS 允许 MySQL 在某些情况下避免全表扫描,即使查询条件不符合最左前缀。不过,这个功能比较鸡肋, 和 Oracle 中的没法比,MySQL 8.0.31 还报告了一个 bug:[Bug #109145 Using index for skip scan cause incorrect result](https://bugs.mysql.com/bug.php?id=109145)(后续版本已经修复)。个人建议知道有这个东西就好,不需要深究,实际项目也不一定能用上。
+
+### SELECT \* 会导致索引失效吗?
+
+`SELECT *` 不会直接导致索引失效(如果不走索引大概率是因为 where 查询范围过大导致的),但它可能会带来一些其他的性能问题比如造成网络传输和数据处理的浪费、无法使用索引覆盖。
+
+### 哪些字段适合创建索引?
-因此,对于咱们日常开发的业务系统来说,你几乎找不到什么理由再使用 MyISAM 作为自己的 MySQL 数据库的存储引擎。
+- **不为 NULL 的字段**:索引字段的数据应该尽量不为 NULL,因为对于数据为 NULL 的字段,数据库较难优化。如果字段频繁被查询,但又避免不了为 NULL,建议使用 0,1,true,false 这样语义较为清晰的短值或短字符作为替代。
+- **被频繁查询的字段**:我们创建索引的字段应该是查询操作非常频繁的字段。
+- **被作为条件查询的字段**:被作为 WHERE 条件查询的字段,应该被考虑建立索引。
+- **频繁需要排序的字段**:索引已经排序,这样查询可以利用索引的排序,加快排序查询时间。
+- **被经常频繁用于连接的字段**:经常用于连接的字段可能是一些外键列,对于外键列并不一定要建立外键,只是说该列涉及到表与表的关系。对于频繁被连接查询的字段,可以考虑建立索引,提高多表连接查询的效率。
-## MySQL 索引
+### 索引失效的原因有哪些?
-MySQL 索引相关的问题比较多,对于面试和工作都比较重要,于是,我单独抽了一篇文章专门来总结 MySQL 索引相关的知识点和问题:[MySQL 索引详解](./mysql-index.md) 。
+1. 创建了组合索引,但查询条件未遵守最左匹配原则;
+2. 在索引列上进行计算、函数、类型转换等操作;
+3. 以 % 开头的 LIKE 查询比如 `LIKE '%abc';`;
+4. 查询条件中使用 OR,且 OR 的前后条件中有一个列没有索引,涉及的索引都不会被使用到;
+5. IN 的取值范围较大时会导致索引失效,走全表扫描(NOT IN 和 IN 的失效场景相同);
+6. 发生[隐式转换](https://javaguide.cn/database/mysql/index-invalidation-caused-by-implicit-conversion.html "隐式转换");
## MySQL 查询缓存
@@ -391,26 +595,17 @@ MySQL 5.6 开始,查询缓存已默认禁用。MySQL 8.0 开始,已经不再

-## MySQL 日志
+## ⭐️MySQL 日志
-MySQL 日志常见的面试题有:
-
-- MySQL 中常见的日志有哪些?
-- 慢查询日志有什么用?
-- binlog 主要记录了什么?
-- redo log 如何保证事务的持久性?
-- 页修改之后为什么不直接刷盘呢?
-- binlog 和 redolog 有什么区别?
-- undo log 如何保证事务的原子性?
-- ……
-
-上诉问题的答案可以在[《Java 面试指北》(付费)](../../zhuanlan/java-mian-shi-zhi-bei.md) 的 **「技术面试题篇」** 中找到。
+上诉问题的答案可以在[《Java 面试指北》(付费,点击链接领取优惠卷)](https://javaguide.cn/zhuanlan/java-mian-shi-zhi-bei.html) 的 **「技术面试题篇」** 中找到。

-## MySQL 事务
+文章地址: (密码获取:)。
+
+## ⭐️MySQL 事务
-### 何谓事务?
+### 什么是事务?
我们设想一个场景,这个场景中我们需要插入多条相关联的数据到数据库,不幸的是,这个过程可能会遇到下面这些问题:
@@ -432,7 +627,7 @@ MySQL 日志常见的面试题有:

-### 何谓数据库事务?
+### 什么是数据库事务?
大多数情况下,我们在谈论事务的时候,如果没有特指**分布式事务**,往往指的就是**数据库事务**。
@@ -489,7 +684,7 @@ COMMIT;
例如:事务 1 读取某表中的数据 A=20,事务 1 修改 A=A-1,事务 2 读取到 A = 19,事务 1 回滚导致对 A 的修改并未提交到数据库, A 的值还是 20。
-
+
#### 丢失修改(Lost to modify)
@@ -497,7 +692,7 @@ COMMIT;
例如:事务 1 读取某表中的数据 A=20,事务 2 也读取 A=20,事务 1 先修改 A=A-1,事务 2 后来也修改 A=A-1,最终结果 A=19,事务 1 的修改被丢失。
-
+
#### 不可重复读(Unrepeatable read)
@@ -505,7 +700,7 @@ COMMIT;
例如:事务 1 读取某表中的数据 A=20,事务 2 也读取 A=20,事务 1 修改 A=A-1,事务 2 再次读取 A =19,此时读取的结果和第一次读取的结果不同。
-
+
#### 幻读(Phantom read)
@@ -513,7 +708,7 @@ COMMIT;
例如:事务 2 读取某个范围的数据,事务 1 在这个范围插入了新的数据,事务 2 再次读取这个范围的数据发现相比于第一次读取的结果多了新的数据。
-
+
### 不可重复读和幻读有什么区别?
@@ -546,31 +741,26 @@ MVCC 在 MySQL 中实现所依赖的手段主要是: **隐藏字段、read view
### SQL 标准定义了哪些事务隔离级别?
-SQL 标准定义了四个隔离级别:
+SQL 标准定义了四种事务隔离级别,用来平衡事务的隔离性(Isolation)和并发性能。级别越高,数据一致性越好,但并发性能可能越低。这四个级别是:
-- **READ-UNCOMMITTED(读取未提交)** :最低的隔离级别,允许读取尚未提交的数据变更,可能会导致脏读、幻读或不可重复读。
-- **READ-COMMITTED(读取已提交)** :允许读取并发事务已经提交的数据,可以阻止脏读,但是幻读或不可重复读仍有可能发生。
-- **REPEATABLE-READ(可重复读)** :对同一字段的多次读取结果都是一致的,除非数据是被本身事务自己所修改,可以阻止脏读和不可重复读,但幻读仍有可能发生。
+- **READ-UNCOMMITTED(读取未提交)** :最低的隔离级别,允许读取尚未提交的数据变更,可能会导致脏读、幻读或不可重复读。这种级别在实际应用中很少使用,因为它对数据一致性的保证太弱。
+- **READ-COMMITTED(读取已提交)** :允许读取并发事务已经提交的数据,可以阻止脏读,但是幻读或不可重复读仍有可能发生。这是大多数数据库(如 Oracle, SQL Server)的默认隔离级别。
+- **REPEATABLE-READ(可重复读)** :对同一字段的多次读取结果都是一致的,除非数据是被本身事务自己所修改,可以阻止脏读和不可重复读,但幻读仍有可能发生。MySQL InnoDB 存储引擎的默认隔离级别正是 REPEATABLE READ。并且,InnoDB 在此级别下通过 MVCC(多版本并发控制) 和 Next-Key Locks(间隙锁+行锁) 机制,在很大程度上解决了幻读问题。
- **SERIALIZABLE(可串行化)** :最高的隔离级别,完全服从 ACID 的隔离级别。所有的事务依次逐个执行,这样事务之间就完全不可能产生干扰,也就是说,该级别可以防止脏读、不可重复读以及幻读。
----
-
-| 隔离级别 | 脏读 | 不可重复读 | 幻读 |
-| :--------------: | :--: | :--------: | :--: |
-| READ-UNCOMMITTED | √ | √ | √ |
-| READ-COMMITTED | × | √ | √ |
-| REPEATABLE-READ | × | × | √ |
-| SERIALIZABLE | × | × | × |
-
-### MySQL 的隔离级别是基于锁实现的吗?
-
-MySQL 的隔离级别基于锁和 MVCC 机制共同实现的。
-
-SERIALIZABLE 隔离级别是通过锁来实现的,READ-COMMITTED 和 REPEATABLE-READ 隔离级别是基于 MVCC 实现的。不过, SERIALIZABLE 之外的其他隔离级别可能也需要用到锁机制,就比如 REPEATABLE-READ 在当前读情况下需要使用加锁读来保证不会出现幻读。
+| 隔离级别 | 脏读 (Dirty Read) | 不可重复读 (Non-Repeatable Read) | 幻读 (Phantom Read) |
+| ---------------- | ----------------- | -------------------------------- | ---------------------- |
+| READ UNCOMMITTED | √ | √ | √ |
+| READ COMMITTED | × | √ | √ |
+| REPEATABLE READ | × | × | √ (标准) / ≈× (InnoDB) |
+| SERIALIZABLE | × | × | × |
### MySQL 的默认隔离级别是什么?
-MySQL InnoDB 存储引擎的默认支持的隔离级别是 **REPEATABLE-READ(可重读)**。我们可以通过`SELECT @@tx_isolation;`命令来查看,MySQL 8.0 该命令改为`SELECT @@transaction_isolation;`
+MySQL InnoDB 存储引擎的默认隔离级别是 **REPEATABLE READ**。可以通过以下命令查看:
+
+- MySQL 8.0 之前:`SELECT @@tx_isolation;`
+- MySQL 8.0 及之后:`SELECT @@transaction_isolation;`
```sql
mysql> SELECT @@tx_isolation;
@@ -583,6 +773,12 @@ mysql> SELECT @@tx_isolation;
关于 MySQL 事务隔离级别的详细介绍,可以看看我写的这篇文章:[MySQL 事务隔离级别详解](./transaction-isolation-level.md)。
+### MySQL 的隔离级别是基于锁实现的吗?
+
+MySQL 的隔离级别基于锁和 MVCC 机制共同实现的。
+
+SERIALIZABLE 隔离级别是通过锁来实现的,READ-COMMITTED 和 REPEATABLE-READ 隔离级别是基于 MVCC 实现的。不过, SERIALIZABLE 之外的其他隔离级别可能也需要用到锁机制,就比如 REPEATABLE-READ 在当前读情况下需要使用加锁读来保证不会出现幻读。
+
## MySQL 锁
锁是一种常见的并发事务的控制方式。
@@ -608,14 +804,12 @@ InnoDB 的行锁是针对索引字段加的锁,表级锁是针对非索引字
InnoDB 行锁是通过对索引数据页上的记录加锁实现的,MySQL InnoDB 支持三种行锁定方式:
-- **记录锁(Record Lock)**:也被称为记录锁,属于单个行记录上的锁。
+- **记录锁(Record Lock)**:属于单个行记录上的锁。
- **间隙锁(Gap Lock)**:锁定一个范围,不包括记录本身。
- **临键锁(Next-Key Lock)**:Record Lock+Gap Lock,锁定一个范围,包含记录本身,主要目的是为了解决幻读问题(MySQL 事务部分提到过)。记录锁只能锁住已经存在的记录,为了避免插入新记录,需要依赖间隙锁。
**在 InnoDB 默认的隔离级别 REPEATABLE-READ 下,行锁默认使用的是 Next-Key Lock。但是,如果操作的索引是唯一索引或主键,InnoDB 会对 Next-Key Lock 进行优化,将其降级为 Record Lock,即仅锁住索引本身,而不是范围。**
-一些大厂面试中可能会问到 Next-Key Lock 的加锁范围,这里推荐一篇文章:[MySQL next-key lock 加锁范围是什么? - 程序员小航 - 2021](https://segmentfault.com/a/1190000040129107) 。
-
### 共享锁和排他锁呢?
不论是表级锁还是行级锁,都存在共享锁(Share Lock,S 锁)和排他锁(Exclusive Lock,X 锁)这两类:
@@ -650,7 +844,7 @@ SELECT ... FOR UPDATE;
- **意向共享锁(Intention Shared Lock,IS 锁)**:事务有意向对表中的某些记录加共享锁(S 锁),加共享锁前必须先取得该表的 IS 锁。
- **意向排他锁(Intention Exclusive Lock,IX 锁)**:事务有意向对表中的某些记录加排他锁(X 锁),加排他锁之前必须先取得该表的 IX 锁。
-**意向锁是由数据引擎自己维护的,用户无法手动操作意向锁,在为数据行加共享/排他锁之前,InooDB 会先获取该数据行所在在数据表的对应意向锁。**
+**意向锁是由数据引擎自己维护的,用户无法手动操作意向锁,在为数据行加共享/排他锁之前,InnoDB 会先获取该数据行所在在数据表的对应意向锁。**
意向锁之间是互相兼容的。
@@ -743,7 +937,7 @@ CREATE TABLE `sequence_id` (
最后,再推荐一篇文章:[为什么 MySQL 的自增主键不单调也不连续](https://draveness.me/whys-the-design-mysql-auto-increment/) 。
-## MySQL 性能优化
+## ⭐️MySQL 性能优化
关于 MySQL 性能优化的建议总结,请看这篇文章:[MySQL 高性能优化规范建议总结](./mysql-high-performance-optimization-specification-recommendations.md) 。
@@ -759,8 +953,6 @@ CREATE TABLE `sequence_id` (
**数据库只存储文件地址信息,文件由文件存储服务负责存储。**
-相关阅读:[Spring Boot 整合 MinIO 实现分布式文件服务](https://www.51cto.com/article/716978.html) 。
-
### MySQL 如何存储 IP 地址?
可以将 IP 地址转换成整形数据存储,性能更好,占用空间也更小。
@@ -774,10 +966,12 @@ MySQL 提供了两个方法来处理 ip 地址
### 有哪些常见的 SQL 优化手段?
-[《Java 面试指北》(付费)](../../zhuanlan/java-mian-shi-zhi-bei.md) 的 **「技术面试题篇」** 有一篇文章详细介绍了常见的 SQL 优化手段,非常全面,清晰易懂!
+[《Java 面试指北》(付费)](https://javaguide.cn/zhuanlan/java-mian-shi-zhi-bei.html) 的 **「技术面试题篇」** 有一篇文章详细介绍了常见的 SQL 优化手段,非常全面,清晰易懂!

+文章地址:https://www.yuque.com/snailclimb/mf2z3k/abc2sv (密码获取:)。
+
### 如何分析 SQL 的性能?
我们可以使用 `EXPLAIN` 命令来分析 SQL 的 **执行计划** 。执行计划是指一条 SQL 语句在经过 MySQL 查询优化器的优化会后,具体的执行方式。
@@ -831,15 +1025,44 @@ mysql> EXPLAIN SELECT `score`,`name` FROM `cus_order` ORDER BY `score` DESC;
[数据冷热分离详解](../../high-performance/data-cold-hot-separation.md)
-### 常见的数据库优化方法有哪些?
+### MySQL 性能怎么优化?
+
+MySQL 性能优化是一个系统性工程,涉及多个方面,在面试中不可能面面俱到。因此,建议按照“点-线-面”的思路展开,从核心问题入手,再逐步扩展,展示出你对问题的思考深度和解决能力。
+
+**1. 抓住核心:慢 SQL 定位与分析**
+
+性能优化的第一步永远是找到瓶颈。面试时,建议先从 **慢 SQL 定位和分析** 入手,这不仅能展示你解决问题的思路,还能体现你对数据库性能监控的熟练掌握:
+
+- **监控工具:** 介绍常用的慢 SQL 监控工具,如 **MySQL 慢查询日志**、**Performance Schema** 等,说明你对这些工具的熟悉程度以及如何通过它们定位问题。
+- **EXPLAIN 命令:** 详细说明 `EXPLAIN` 命令的使用,分析查询计划、索引使用情况,可以结合实际案例展示如何解读分析结果,比如执行顺序、索引使用情况、全表扫描等。
+
+**2. 由点及面:索引、表结构和 SQL 优化**
+
+定位到慢 SQL 后,接下来就要针对具体问题进行优化。 这里可以重点介绍索引、表结构和 SQL 编写规范等方面的优化技巧:
+
+- **索引优化:** 这是 MySQL 性能优化的重点,可以介绍索引的创建原则、覆盖索引、最左前缀匹配原则等。如果能结合你项目的实际应用来说明如何选择合适的索引,会更加分一些。
+- **表结构优化:** 优化表结构设计,包括选择合适的字段类型、避免冗余字段、合理使用范式和反范式设计等等。
+- **SQL 优化:** 避免使用 `SELECT *`、尽量使用具体字段、使用连接查询代替子查询、合理使用分页查询、批量操作等,都是 SQL 编写过程中需要注意的细节。
+
+**3. 进阶方案:架构优化**
+
+当面试官对基础优化知识比较满意时,可能会深入探讨一些架构层面的优化方案。以下是一些常见的架构优化策略:
+
+- **读写分离:** 将读操作和写操作分离到不同的数据库实例,提升数据库的并发处理能力。
+- **分库分表:** 将数据分散到多个数据库实例或数据表中,降低单表数据量,提升查询效率。但要权衡其带来的复杂性和维护成本,谨慎使用。
+- **数据冷热分离**:根据数据的访问频率和业务重要性,将数据分为冷数据和热数据,冷数据一般存储在低成本、低性能的介质中,热数据存储在高性能存储介质中。
+- **缓存机制:** 使用 Redis 等缓存中间件,将热点数据缓存到内存中,减轻数据库压力。这个非常常用,提升效果非常明显,性价比极高!
+
+**4. 其他优化手段**
+
+除了慢 SQL 定位、索引优化和架构优化,还可以提及一些其他优化手段,展示你对 MySQL 性能调优的全面理解:
+
+- **连接池配置:** 配置合理的数据库连接池(如 **连接池大小**、**超时时间** 等),能够有效提升数据库连接的效率,避免频繁的连接开销。
+- **硬件配置:** 提升硬件性能也是优化的重要手段之一。使用高性能服务器、增加内存、使用 **SSD** 硬盘等硬件升级,都可以有效提升数据库的整体性能。
+
+**5.总结**
-- [索引优化](./mysql-index.md)
-- [读写分离和分库分表](../../high-performance/read-and-write-separation-and-library-subtable.md)
-- [数据冷热分离](../../high-performance/data-cold-hot-separation.md)
-- [SQL 优化](../../high-performance/sql-optimization.md)
-- [深度分页优化](../../high-performance/deep-pagination-optimization.md)
-- 适当冗余数据
-- 使用更高的硬件配置
+在面试中,建议按优先级依次介绍慢 SQL 定位、[索引优化](./mysql-index.md)、表结构设计和 [SQL 优化](../../high-performance/sql-optimization.md)等内容。架构层面的优化,如[读写分离和分库分表](../../high-performance/read-and-write-separation-and-library-subtable.md)、[数据冷热分离](../../high-performance/data-cold-hot-separation.md) 应作为最后的手段,除非在特定场景下有明显的性能瓶颈,否则不应轻易使用,因其引入的复杂性会带来额外的维护成本。
## MySQL 学习资料推荐
diff --git a/docs/database/mysql/some-thoughts-on-database-storage-time.md b/docs/database/mysql/some-thoughts-on-database-storage-time.md
index 75329434c1b..2cca50eb2a8 100644
--- a/docs/database/mysql/some-thoughts-on-database-storage-time.md
+++ b/docs/database/mysql/some-thoughts-on-database-storage-time.md
@@ -1,32 +1,46 @@
---
title: MySQL日期类型选择建议
+description: 深入对比MySQL中DATETIME和TIMESTAMP的区别,分析时区处理、存储空间、取值范围等差异,给出日期类型选择的最佳实践建议。
category: 数据库
tag:
- MySQL
+head:
+ - - meta
+ - name: keywords
+ content: MySQL时间存储,DATETIME,TIMESTAMP,时间戳,时区处理,日期类型选择,MySQL日期函数
---
-我们平时开发中不可避免的就是要存储时间,比如我们要记录操作表中这条记录的时间、记录转账的交易时间、记录出发时间、用户下单时间等等。你会发现时间这个东西与我们开发的联系还是非常紧密的,用的好与不好会给我们的业务甚至功能带来很大的影响。所以,我们有必要重新出发,好好认识一下这个东西。
+在日常的软件开发工作中,存储时间是一项基础且常见的需求。无论是记录数据的操作时间、金融交易的发生时间,还是行程的出发时间、用户的下单时间等等,时间信息与我们的业务逻辑和系统功能紧密相关。因此,正确选择和使用 MySQL 的日期时间类型至关重要,其恰当与否甚至可能对业务的准确性和系统的稳定性产生显著影响。
+
+本文旨在帮助开发者重新审视并深入理解 MySQL 中不同的时间存储方式,以便做出更合适项目业务场景的选择。
## 不要用字符串存储日期
-和绝大部分对数据库不太了解的新手一样,我在大学的时候就这样干过,甚至认为这样是一个不错的表示日期的方法。毕竟简单直白,容易上手。
+和许多数据库初学者一样,笔者在早期学习阶段也曾尝试使用字符串(如 VARCHAR)类型来存储日期和时间,甚至一度认为这是一种简单直观的方法。毕竟,'YYYY-MM-DD HH:MM:SS' 这样的格式看起来清晰易懂。
但是,这是不正确的做法,主要会有下面两个问题:
-1. 字符串占用的空间更大!
-2. 字符串存储的日期效率比较低(逐个字符进行比对),无法用日期相关的 API 进行计算和比较。
+1. **空间效率**:与 MySQL 内建的日期时间类型相比,字符串通常需要占用更多的存储空间来表示相同的时间信息。
+2. **查询与计算效率低下**:
+ - **比较操作复杂且低效**:基于字符串的日期比较需要按照字典序逐字符进行,这不仅不直观(例如,'2024-05-01' 会小于 '2024-1-10'),而且效率远低于使用原生日期时间类型进行的数值或时间点比较。
+ - **计算功能受限**:无法直接利用数据库提供的丰富日期时间函数进行运算(例如,计算两个日期之间的间隔、对日期进行加减操作等),需要先转换格式,增加了复杂性。
+ - **索引性能不佳**:基于字符串的索引在处理范围查询(如查找特定时间段内的数据)时,其效率和灵活性通常不如原生日期时间类型的索引。
-## Datetime 和 Timestamp 之间的抉择
+## DATETIME 和 TIMESTAMP 选择
-Datetime 和 Timestamp 是 MySQL 提供的两种比较相似的保存时间的数据类型,可以精确到秒。他们两者究竟该如何选择呢?
+`DATETIME` 和 `TIMESTAMP` 是 MySQL 中两种非常常用的、用于存储包含日期和时间信息的数据类型。它们都可以存储精确到秒(MySQL 5.6.4+ 支持更高精度的小数秒)的时间值。那么,在实际应用中,我们应该如何在这两者之间做出选择呢?
-下面我们来简单对比一下二者。
+下面我们从几个关键维度对它们进行对比:
### 时区信息
-**DateTime 类型是没有时区信息的(时区无关)** ,DateTime 类型保存的时间都是当前会话所设置的时区对应的时间。这样就会有什么问题呢?当你的时区更换之后,比如你的服务器更换地址或者更换客户端连接时区设置的话,就会导致你从数据库中读出的时间错误。
+`DATETIME` 类型存储的是**字面量的日期和时间值**,它本身**不包含任何时区信息**。当你插入一个 `DATETIME` 值时,MySQL 存储的就是你提供的那个确切的时间,不会进行任何时区转换。
+
+**这样就会有什么问题呢?** 如果你的应用需要支持多个时区,或者服务器、客户端的时区可能发生变化,那么使用 `DATETIME` 时,应用程序需要自行处理时区的转换和解释。如果处理不当(例如,假设所有存储的时间都属于同一个时区,但实际环境变化了),可能会导致时间显示或计算上的混乱。
-**Timestamp 和时区有关**。Timestamp 类型字段的值会随着服务器时区的变化而变化,自动换算成相应的时间,说简单点就是在不同时区,查询到同一个条记录此字段的值会不一样。
+**`TIMESTAMP` 和时区有关**。存储时,MySQL 会将当前会话时区下的时间值转换成 UTC(协调世界时)进行内部存储。当查询 `TIMESTAMP` 字段时,MySQL 又会将存储的 UTC 时间转换回当前会话所设置的时区来显示。
+
+这意味着,对于同一条记录的 `TIMESTAMP` 字段,在不同的会话时区设置下查询,可能会看到不同的本地时间表示,但它们都对应着同一个绝对时间点(UTC 时间)。这对于需要全球化、多时区支持的应用来说非常有用。
下面实际演示一下!
@@ -41,16 +55,16 @@ CREATE TABLE `time_zone_test` (
) ENGINE=InnoDB DEFAULT CHARSET=utf8;
```
-插入数据:
+插入一条数据(假设当前会话时区为系统默认,例如 UTC+0)::
```sql
INSERT INTO time_zone_test(date_time,time_stamp) VALUES(NOW(),NOW());
```
-查看数据:
+查询数据(在同一时区会话下):
```sql
-select date_time,time_stamp from time_zone_test;
+SELECT date_time, time_stamp FROM time_zone_test;
```
结果:
@@ -63,17 +77,16 @@ select date_time,time_stamp from time_zone_test;
+---------------------+---------------------+
```
-现在我们运行
-
-修改当前会话的时区:
+现在,修改当前会话的时区为东八区 (UTC+8):
```sql
-set time_zone='+8:00';
+SET time_zone = '+8:00';
```
-再次查看数据:
+再次查询数据:
-```plain
+```bash
+# TIMESTAMP 的值自动转换为 UTC+8 时间
+---------------------+---------------------+
| date_time | time_stamp |
+---------------------+---------------------+
@@ -81,7 +94,7 @@ set time_zone='+8:00';
+---------------------+---------------------+
```
-**扩展:一些关于 MySQL 时区设置的一个常用 sql 命令**
+**扩展:MySQL 时区设置常用 SQL 命令**
```sql
# 查看当前会话时区
@@ -102,28 +115,26 @@ SET GLOBAL time_zone = 'Europe/Helsinki';

-在 MySQL 5.6.4 之前,DateTime 和 Timestamp 的存储空间是固定的,分别为 8 字节和 4 字节。但是从 MySQL 5.6.4 开始,它们的存储空间会根据毫秒精度的不同而变化,DateTime 的范围是 5~8 字节,Timestamp 的范围是 4~7 字节。
+在 MySQL 5.6.4 之前,DateTime 和 TIMESTAMP 的存储空间是固定的,分别为 8 字节和 4 字节。但是从 MySQL 5.6.4 开始,它们的存储空间会根据毫秒精度的不同而变化,DateTime 的范围是 5~8 字节,TIMESTAMP 的范围是 4~7 字节。
### 表示范围
-Timestamp 表示的时间范围更小,只能到 2038 年:
+`TIMESTAMP` 表示的时间范围更小,只能到 2038 年:
-- DateTime:1000-01-01 00:00:00.000000 ~ 9999-12-31 23:59:59.499999
-- Timestamp:1970-01-01 00:00:01.000000 ~ 2038-01-19 03:14:07.499999
+- `DATETIME`:'1000-01-01 00:00:00.000000' 到 '9999-12-31 23:59:59.999999'
+- `TIMESTAMP`:'1970-01-01 00:00:01.000000' UTC 到 '2038-01-19 03:14:07.999999' UTC
### 性能
-由于 TIMESTAMP 需要根据时区进行转换,所以从毫秒数转换到 TIMESTAMP 时,不仅要调用一个简单的函数,还要调用操作系统底层的系统函数。这个系统函数为了保证操作系统时区的一致性,需要进行加锁操作,这就降低了效率。
-
-DATETIME 不涉及时区转换,所以不会有这个问题。
+由于 `TIMESTAMP` 在存储和检索时需要进行 UTC 与当前会话时区的转换,这个过程可能涉及到额外的计算开销,尤其是在需要调用操作系统底层接口获取或处理时区信息时。虽然现代数据库和操作系统对此进行了优化,但在某些极端高并发或对延迟极其敏感的场景下,`DATETIME` 因其不涉及时区转换,处理逻辑相对更简单直接,可能会表现出微弱的性能优势。
-为了避免 TIMESTAMP 的时区转换问题,建议使用指定的时区,而不是依赖于操作系统时区。
+为了获得可预测的行为并可能减少 `TIMESTAMP` 的转换开销,推荐的做法是在应用程序层面统一管理时区,或者在数据库连接/会话级别显式设置 `time_zone` 参数,而不是依赖服务器的默认或操作系统时区。
## 数值时间戳是更好的选择吗?
-很多时候,我们也会使用 int 或者 bigint 类型的数值也就是数值时间戳来表示时间。
+除了上述两种类型,实践中也常用整数类型(`INT` 或 `BIGINT`)来存储所谓的“Unix 时间戳”(即从 1970 年 1 月 1 日 00:00:00 UTC 起至目标时间的总秒数,或毫秒数)。
-这种存储方式的具有 Timestamp 类型的所具有一些优点,并且使用它的进行日期排序以及对比等操作的效率会更高,跨系统也很方便,毕竟只是存放的数值。缺点也很明显,就是数据的可读性太差了,你无法直观的看到具体时间。
+这种存储方式的具有 `TIMESTAMP` 类型的所具有一些优点,并且使用它的进行日期排序以及对比等操作的效率会更高,跨系统也很方便,毕竟只是存放的数值。缺点也很明显,就是数据的可读性太差了,你无法直观的看到具体时间。
时间戳的定义如下:
@@ -132,7 +143,8 @@ DATETIME 不涉及时区转换,所以不会有这个问题。
数据库中实际操作:
```sql
-mysql> select UNIX_TIMESTAMP('2020-01-11 09:53:32');
+-- 将日期时间字符串转换为 Unix 时间戳 (秒)
+mysql> SELECT UNIX_TIMESTAMP('2020-01-11 09:53:32');
+---------------------------------------+
| UNIX_TIMESTAMP('2020-01-11 09:53:32') |
+---------------------------------------+
@@ -140,7 +152,8 @@ mysql> select UNIX_TIMESTAMP('2020-01-11 09:53:32');
+---------------------------------------+
1 row in set (0.00 sec)
-mysql> select FROM_UNIXTIME(1578707612);
+-- 将 Unix 时间戳 (秒) 转换为日期时间格式
+mysql> SELECT FROM_UNIXTIME(1578707612);
+---------------------------+
| FROM_UNIXTIME(1578707612) |
+---------------------------+
@@ -149,13 +162,26 @@ mysql> select FROM_UNIXTIME(1578707612);
1 row in set (0.01 sec)
```
+## PostgreSQL 中没有 DATETIME
+
+由于有读者提到 PostgreSQL(PG) 的时间类型,因此这里拓展补充一下。PG 官方文档对时间类型的描述地址:。
+
+
+
+可以看到,PG 没有名为 `DATETIME` 的类型:
+
+- PG 的 `TIMESTAMP WITHOUT TIME ZONE`在功能上最接近 MySQL 的 `DATETIME`。它存储日期和时间,但不包含任何时区信息,存储的是字面值。
+- PG 的`TIMESTAMP WITH TIME ZONE` (或 `TIMESTAMPTZ`) 相当于 MySQL 的 `TIMESTAMP`。它在存储时会将输入值转换为 UTC,并在检索时根据当前会话的时区进行转换显示。
+
+对于绝大多数需要记录精确发生时间点的应用场景,`TIMESTAMPTZ`是 PostgreSQL 中最推荐、最健壮的选择,因为它能最好地处理时区复杂性。
+
## 总结
-MySQL 中时间到底怎么存储才好?Datetime?Timestamp?还是数值时间戳?
+MySQL 中时间到底怎么存储才好?`DATETIME`?`TIMESTAMP`?还是数值时间戳?
并没有一个银弹,很多程序员会觉得数值型时间戳是真的好,效率又高还各种兼容,但是很多人又觉得它表现的不够直观。
-《高性能 MySQL 》这本神书的作者就是推荐 Timestamp,原因是数值表示时间不够直观。下面是原文:
+《高性能 MySQL 》这本神书的作者就是推荐 TIMESTAMP,原因是数值表示时间不够直观。下面是原文:
@@ -167,4 +193,10 @@ MySQL 中时间到底怎么存储才好?Datetime?Timestamp?还是数值时间
| TIMESTAMP | 4~7 字节 | YYYY-MM-DD hh:mm:ss[.fraction] | 1970-01-01 00:00:01[.000000] ~ 2038-01-19 03:14:07[.999999] | 是 |
| 数值型时间戳 | 4 字节 | 全数字如 1578707612 | 1970-01-01 00:00:01 之后的时间 | 否 |
+**选择建议小结:**
+
+- `TIMESTAMP` 的核心优势在于其内建的时区处理能力。数据库负责 UTC 存储和基于会话时区的自动转换,简化了需要处理多时区应用的开发。如果应用需要处理多时区,或者希望数据库能自动管理时区转换,`TIMESTAMP` 是自然的选择(注意其时间范围限制,也就是 2038 年问题)。
+- 如果应用场景不涉及时区转换,或者希望应用程序完全控制时区逻辑,并且需要表示 2038 年之后的时间,`DATETIME` 是更稳妥的选择。
+- 如果极度关注比较性能,或者需要频繁跨系统传递时间数据,并且可以接受可读性的牺牲(或总是在应用层转换),数值时间戳是一个强大的选项。
+
diff --git a/docs/database/mysql/transaction-isolation-level.md b/docs/database/mysql/transaction-isolation-level.md
index 52ad40f4a47..4ee2cabc95a 100644
--- a/docs/database/mysql/transaction-isolation-level.md
+++ b/docs/database/mysql/transaction-isolation-level.md
@@ -1,8 +1,13 @@
---
title: MySQL事务隔离级别详解
+description: 详解MySQL四种事务隔离级别(读未提交、读已提交、可重复读、串行化)的特点与区别,分析脏读、不可重复读、幻读等并发问题,以及InnoDB如何通过MVCC和锁机制解决幻读。
category: 数据库
tag:
- MySQL
+head:
+ - - meta
+ - name: keywords
+ content: MySQL事务隔离级别,读未提交,读已提交,可重复读,串行化,脏读,不可重复读,幻读,MVCC,间隙锁
---
> 本文由 [SnailClimb](https://github.com/Snailclimb) 和 [guang19](https://github.com/guang19) 共同完成。
@@ -11,43 +16,46 @@ tag:
## 事务隔离级别总结
-SQL 标准定义了四个隔离级别:
+SQL 标准定义了四种事务隔离级别,用来平衡事务的隔离性(Isolation)和并发性能。级别越高,数据一致性越好,但并发性能可能越低。这四个级别是:
-- **READ-UNCOMMITTED(读取未提交)** :最低的隔离级别,允许读取尚未提交的数据变更,可能会导致脏读、幻读或不可重复读。
-- **READ-COMMITTED(读取已提交)** :允许读取并发事务已经提交的数据,可以阻止脏读,但是幻读或不可重复读仍有可能发生。
-- **REPEATABLE-READ(可重复读)** :对同一字段的多次读取结果都是一致的,除非数据是被本身事务自己所修改,可以阻止脏读和不可重复读,但幻读仍有可能发生。
+- **READ-UNCOMMITTED(读取未提交)** :最低的隔离级别,允许读取尚未提交的数据变更,可能会导致脏读、幻读或不可重复读。这种级别在实际应用中很少使用,因为它对数据一致性的保证太弱。
+- **READ-COMMITTED(读取已提交)** :允许读取并发事务已经提交的数据,可以阻止脏读,但是幻读或不可重复读仍有可能发生。这是大多数数据库(如 Oracle, SQL Server)的默认隔离级别。
+- **REPEATABLE-READ(可重复读)** :对同一字段的多次读取结果都是一致的,除非数据是被本身事务自己所修改,可以阻止脏读和不可重复读,但幻读仍有可能发生。MySQL InnoDB 存储引擎的默认隔离级别正是 REPEATABLE READ。并且,InnoDB 在此级别下通过 MVCC(多版本并发控制) 和 Next-Key Locks(间隙锁+行锁) 机制,在很大程度上解决了幻读问题。
- **SERIALIZABLE(可串行化)** :最高的隔离级别,完全服从 ACID 的隔离级别。所有的事务依次逐个执行,这样事务之间就完全不可能产生干扰,也就是说,该级别可以防止脏读、不可重复读以及幻读。
----
+| 隔离级别 | 脏读 (Dirty Read) | 不可重复读 (Non-Repeatable Read) | 幻读 (Phantom Read) |
+| ---------------- | ----------------- | -------------------------------- | ---------------------- |
+| READ UNCOMMITTED | √ | √ | √ |
+| READ COMMITTED | × | √ | √ |
+| REPEATABLE READ | × | × | √ (标准) / ≈× (InnoDB) |
+| SERIALIZABLE | × | × | × |
-| 隔离级别 | 脏读 | 不可重复读 | 幻读 |
-| :--------------: | :--: | :--------: | :--: |
-| READ-UNCOMMITTED | √ | √ | √ |
-| READ-COMMITTED | × | √ | √ |
-| REPEATABLE-READ | × | × | √ |
-| SERIALIZABLE | × | × | × |
+**默认级别查询:**
-MySQL InnoDB 存储引擎的默认支持的隔离级别是 **REPEATABLE-READ(可重读)**。我们可以通过`SELECT @@tx_isolation;`命令来查看,MySQL 8.0 该命令改为`SELECT @@transaction_isolation;`
+MySQL InnoDB 存储引擎的默认隔离级别是 **REPEATABLE READ**。可以通过以下命令查看:
-```sql
-MySQL> SELECT @@tx_isolation;
-+-----------------+
-| @@tx_isolation |
-+-----------------+
-| REPEATABLE-READ |
-+-----------------+
+- MySQL 8.0 之前:`SELECT @@tx_isolation;`
+- MySQL 8.0 及之后:`SELECT @@transaction_isolation;`
+
+```bash
+mysql> SELECT @@transaction_isolation;
++-------------------------+
+| @@transaction_isolation |
++-------------------------+
+| REPEATABLE-READ |
++-------------------------+
```
-从上面对 SQL 标准定义了四个隔离级别的介绍可以看出,标准的 SQL 隔离级别定义里,REPEATABLE-READ(可重复读)是不可以防止幻读的。
+**InnoDB 的 REPEATABLE READ 对幻读的处理:**
-但是!InnoDB 实现的 REPEATABLE-READ 隔离级别其实是可以解决幻读问题发生的,主要有下面两种情况:
+标准的 SQL 隔离级别定义里,REPEATABLE READ 是无法防止幻读的。但 InnoDB 的实现通过以下机制很大程度上避免了幻读:
-- **快照读**:由 MVCC 机制来保证不出现幻读。
-- **当前读**:使用 Next-Key Lock 进行加锁来保证不出现幻读,Next-Key Lock 是行锁(Record Lock)和间隙锁(Gap Lock)的结合,行锁只能锁住已经存在的行,为了避免插入新行,需要依赖间隙锁。
+- **快照读 (Snapshot Read)**:普通的 SELECT 语句,通过 **MVCC** 机制实现。事务启动时创建一个数据快照,后续的快照读都读取这个版本的数据,从而避免了看到其他事务新插入的行(幻读)或修改的行(不可重复读)。
+- **当前读 (Current Read)**:像 `SELECT ... FOR UPDATE`, `SELECT ... LOCK IN SHARE MODE`, `INSERT`, `UPDATE`, `DELETE` 这些操作。InnoDB 使用 **Next-Key Lock** 来锁定扫描到的索引记录及其间的范围(间隙),防止其他事务在这个范围内插入新的记录,从而避免幻读。Next-Key Lock 是行锁(Record Lock)和间隙锁(Gap Lock)的组合。
-因为隔离级别越低,事务请求的锁越少,所以大部分数据库系统的隔离级别都是 **READ-COMMITTED** ,但是你要知道的是 InnoDB 存储引擎默认使用 **REPEATABLE-READ** 并不会有任何性能损失。
+值得注意的是,虽然通常认为隔离级别越高、并发性越差,但 InnoDB 存储引擎通过 MVCC 机制优化了 REPEATABLE READ 级别。对于许多常见的只读或读多写少的场景,其性能**与 READ COMMITTED 相比可能没有显著差异**。不过,在写密集型且并发冲突较高的场景下,RR 的间隙锁机制可能会比 RC 带来更多的锁等待。
-InnoDB 存储引擎在分布式事务的情况下一般会用到 SERIALIZABLE 隔离级别。
+此外,在某些特定场景下,如需要严格一致性的分布式事务(XA Transactions),InnoDB 可能要求或推荐使用 SERIALIZABLE 隔离级别来确保全局数据的一致性。
《MySQL 技术内幕:InnoDB 存储引擎(第 2 版)》7.7 章这样写到:
diff --git a/docs/database/nosql.md b/docs/database/nosql.md
index d5ca59698bd..3a7e7929057 100644
--- a/docs/database/nosql.md
+++ b/docs/database/nosql.md
@@ -1,10 +1,15 @@
---
title: NoSQL基础知识总结
+description: NoSQL数据库基础知识总结,包括NoSQL与SQL的区别、NoSQL的优势、四种NoSQL数据库类型(键值、文档、图形、宽列)及其代表产品Redis、MongoDB、Neo4j等的应用场景。
category: 数据库
tag:
- NoSQL
- MongoDB
- Redis
+head:
+ - - meta
+ - name: keywords
+ content: NoSQL,Redis,MongoDB,HBase,Cassandra,键值数据库,文档数据库,图数据库,宽列存储,SQL与NoSQL区别
---
## NoSQL 是什么?
diff --git a/docs/database/redis/3-commonly-used-cache-read-and-write-strategies.md b/docs/database/redis/3-commonly-used-cache-read-and-write-strategies.md
index 7ad88958704..be12e83c288 100644
--- a/docs/database/redis/3-commonly-used-cache-read-and-write-strategies.md
+++ b/docs/database/redis/3-commonly-used-cache-read-and-write-strategies.md
@@ -1,8 +1,13 @@
---
title: 3种常用的缓存读写策略详解
+description: 深入对比 Cache Aside、Read/Write Through、Write Behind 三种缓存读写策略,附详细时序图、一致性问题分析及生产级解决方案,Redis 实战必备!
category: 数据库
tag:
- Redis
+head:
+ - - meta
+ - name: keywords
+ content: 缓存读写策略,Cache Aside,Read Through,Write Through,Write Behind,Write Back,缓存一致性,缓存失效,旁路缓存,读写穿透,异步缓存写入,Redis缓存策略,缓存更新策略
---
看到很多小伙伴简历上写了“**熟练使用缓存**”,但是被我问到“**缓存常用的 3 种读写策略**”的时候却一脸懵逼。
@@ -15,26 +20,28 @@ tag:
### Cache Aside Pattern(旁路缓存模式)
-**Cache Aside Pattern 是我们平时使用比较多的一个缓存读写模式,比较适合读请求比较多的场景。**
+这是我们日常开发中**最常用、最经典**的一种模式,几乎是互联网应用缓存方案的事实标准,尤其适合**读多写少**的业务场景。
-Cache Aside Pattern 中服务端需要同时维系 db 和 cache,并且是以 db 的结果为准。
+这个模式之所以被称为**“旁路”(Aside)**,是因为应用程序的**写操作完全绕过了缓存,直接操作数据库**。
+
+应用程序扮演了数据流转的“指挥官”,需要同时维护 Cache 和 DB 两个数据源。
下面我们来看一下这个策略模式下的缓存读写步骤。
-**写**:
+**写操作 :**
-- 先更新 db
-- 然后直接删除 cache 。
+1. 应用**先更新 DB**。
+2. 然后**直接删除 Cache**中对应的数据。
简单画了一张图帮助大家理解写的步骤。

-**读** :
+**读操作:**
-- 从 cache 中读取数据,读取到就直接返回
-- cache 中读取不到的话,就从 db 中读取数据返回
-- 再把数据放到 cache 中。
+1. 应用先从 Cache 读取数据。
+2. 如果命中(Hit),则直接返回。
+3. 如果未命中(Miss),则从 DB 读取数据,成功读取后,**将数据写回 Cache**,然后返回。
简单画了一张图帮助大家理解读的步骤。
@@ -42,49 +49,69 @@ Cache Aside Pattern 中服务端需要同时维系 db 和 cache,并且是以 d
你仅仅了解了上面这些内容的话是远远不够的,我们还要搞懂其中的原理。
-比如说面试官很可能会追问:“**在写数据的过程中,可以先删除 cache ,后更新 db 么?**”
+比如说面试官很可能会追问:
+
+1. 为什么写操作是“先更新 DB,后删除 Cache”?顺序能反过来吗?
+2. 那“先更新 DB,后删除 Cache”就绝对安全吗?
+3. 为什么是“删除 Cache”,而不是“更新 Cache”?
-**答案:** 那肯定是不行的!因为这样可能会造成 **数据库(db)和缓存(Cache)数据不一致**的问题。
+接下来我会以此分析解答这些问题。
-举例:请求 1 先写数据 A,请求 2 随后读数据 A 的话,就很有可能产生数据不一致性的问题。
+**1. 为什么写操作是“先更新 DB,后删除 Cache”?顺序能反过来吗?**
-这个过程可以简单描述为:
+**答:** 绝对不能。如果“先删 Cache,后更新 DB”,在高并发下会引入经典的数据不一致问题。
-> 请求 1 先把 cache 中的 A 数据删除 -> 请求 2 从 db 中读取数据->请求 1 再把 db 中的 A 数据更新
+- **时序分析 (请求 A 写, 请求 B 读):**
+ 1. 请求 A: 先将 Cache 中的数据删除。
+ 2. 请求 B: 此时发现 Cache 为空,于是去 DB 读取**旧值**,并准备写入 Cache。
+ 3. 请求 A : 将**新值**写入 DB。
+ 4. 请求 B: 将之前读到的**旧值**写入了 Cache。
+- **结果:** DB 中是新值,而 Cache 中是旧值,数据不一致。
-当你这样回答之后,面试官可能会紧接着就追问:“**在写数据的过程中,先更新 db,后删除 cache 就没有问题了么?**”
+**2. 那“先更新 DB,后删除 Cache”就绝对安全吗?**
-**答案:** 理论上来说还是可能会出现数据不一致性的问题,不过概率非常小,因为缓存的写入速度是比数据库的写入速度快很多。
+**答案:** 也不是绝对安全的!因为这样也可能会造成 **数据库和缓存数据不一致**的问题。
-举例:请求 1 先读数据 A,请求 2 随后写数据 A,并且数据 A 在请求 1 请求之前不在缓存中的话,也有可能产生数据不一致性的问题。
+- **时序分析 (请求 A 读, 请求 B 写):**
+ 1. 请求 A : 缓存未命中,从 DB 读取到**旧值**。
+ 2. 请求 B: 迅速完成了 DB 的更新,并将 Cache 删除。
+ 3. 请求 A : 将自己之前拿到的**旧值**写入了 Cache。
+- **结果:** DB 中是新值,Cache 中又是旧值。
+- **为什么概率极小?** 这个问题本质上是一个并发时序问题:只要“读 DB → 写 Cache”这段时间窗口内,恰好有写请求完成了 DB 更新,就有可能产生不一致。在大多数业务里,这个窗口时间相对较短,而且还需要与写请求并发“撞车”,所以发生概率不算高,但绝不是不可能。
-这个过程可以简单描述为:
+**3. 为什么是“删除 Cache”,而不是“更新 Cache”?**
-> 请求 1 从 db 读数据 A-> 请求 2 更新 db 中的数据 A(此时缓存中无数据 A ,故不用执行删除缓存操作 ) -> 请求 1 将数据 A 写入 cache
+- **性能开销:** 写操作往往只更新了对象的部分字段,如果为了“更新 Cache”而去重新查询或计算整个缓存对象,开销可能很大。相比之下,“删除”是一个轻量级操作。
+- **懒加载思想:** “删除”操作遵循懒加载原则。只有当数据下一次被真正需要(被读取)时,才触发从 DB 加载并写入缓存,避免了无效的缓存更新。
+- **并发安全:** “更新缓存”在高并发下可能出现更新顺序错乱的问题导致脏数据的概率会更大。
+
+当然,这一切都建立在一个重要的前提之上:我们缓存的数据,是可以通过数据库进行确定性重建的,并且业务上可以容忍从‘缓存删除’到‘下一次读取并回填’之间这个极短时间窗口内的数据不一致。
现在我们再来分析一下 **Cache Aside Pattern 的缺陷**。
-**缺陷 1:首次请求数据一定不在 cache 的问题**
+**缺陷 1:首次请求数据一定不在 Cache 的问题**
-解决办法:可以将热点数据可以提前放入 cache 中。
+解决办法:对于访问量巨大的热点数据,可以在系统启动或低峰期进行缓存预热。
-**缺陷 2:写操作比较频繁的话导致 cache 中的数据会被频繁被删除,这样会影响缓存命中率 。**
+**缺陷 2:写操作比较频繁的话导致 Cache 中的数据会被频繁被删除,这样会影响缓存命中率 。**
解决办法:
-- 数据库和缓存数据强一致场景:更新 db 的时候同样更新 cache,不过我们需要加一个锁/分布式锁来保证更新 cache 的时候不存在线程安全问题。
-- 可以短暂地允许数据库和缓存数据不一致的场景:更新 db 的时候同样更新 cache,但是给缓存加一个比较短的过期时间,这样的话就可以保证即使数据不一致的话影响也比较小。
+- 数据库和缓存数据强一致场景:更新 DB 的时候同样更新 Cache,不过我们需要加一个锁/分布式锁来保证更新 Cache 的时候不存在线程安全问题。
+- 可以短暂地允许数据库和缓存数据不一致的场景:更新 DB 的时候同样更新 Cache,但是给缓存加一个比较短的过期时间(如 1 分钟),这样的话就可以保证即使数据不一致的话影响也比较小。
### Read/Write Through Pattern(读写穿透)
-Read/Write Through Pattern 中服务端把 cache 视为主要数据存储,从中读取数据并将数据写入其中。cache 服务负责将此数据读取和写入 db,从而减轻了应用程序的职责。
+在这种模式下,应用程序将**Cache 视为唯一的、主要的存储**。所有的读写请求都直接打向 Cache,而 Cache 服务自身负责与 DB 进行数据同步。
+
+对应用程序**透明**,应用开发者无需关心 DB 的存在。
-这种缓存读写策略小伙伴们应该也发现了在平时在开发过程中非常少见。抛去性能方面的影响,大概率是因为我们经常使用的分布式缓存 Redis 并没有提供 cache 将数据写入 db 的功能。
+这种缓存读写策略小伙伴们应该也发现了在平时在开发过程中非常少见。抛去性能方面的影响,大概率是因为我们经常使用的分布式缓存 Redis 本身并没有提供 Cache 将数据写入 DB 的功能,需要我们在业务侧或中间件里自己实现。
**写(Write Through):**
-- 先查 cache,cache 中不存在,直接更新 db。
-- cache 中存在,则先更新 cache,然后 cache 服务自己更新 db(**同步更新 cache 和 db**)。
+- 先查 Cache,Cache 中不存在,直接更新 DB。
+- Cache 中存在,则先更新 Cache,然后 Cache 服务自己更新 DB。只有当 Cache 和 DB 都写入成功后,才向上层返回成功。
简单画了一张图帮助大家理解写的步骤。
@@ -92,27 +119,38 @@ Read/Write Through Pattern 中服务端把 cache 视为主要数据存储,从
**读(Read Through):**
-- 从 cache 中读取数据,读取到就直接返回 。
-- 读取不到的话,先从 db 加载,写入到 cache 后返回响应。
+- 应用从 Cache 读取数据。
+- 如果命中,直接返回。
+- 如果未命中,由**Cache 服务自己**负责从 DB 加载数据,加载成功后先写入自身,再返回给应用。
简单画了一张图帮助大家理解读的步骤。

-Read-Through Pattern 实际只是在 Cache-Aside Pattern 之上进行了封装。在 Cache-Aside Pattern 下,发生读请求的时候,如果 cache 中不存在对应的数据,是由客户端自己负责把数据写入 cache,而 Read Through Pattern 则是 cache 服务自己来写入缓存的,这对客户端是透明的。
+Read-Through 实际只是在 Cache-Aside 之上进行了封装。在 Cache-Aside 下,发生读请求的时候,如果 Cache 中不存在对应的数据,是由客户端自己负责把数据写入 Cache,而 Read Through 则是 Cache 服务自己来写入缓存的,这对客户端是透明的。
-和 Cache Aside Pattern 一样, Read-Through Pattern 也有首次请求数据一定不再 cache 的问题,对于热点数据可以提前放入缓存中。
+从实现角度看,Read-Through 本质上是把 Cache-Aside 中“读 Miss → 读 DB → 回填 Cache”的逻辑,下沉到了缓存服务内部,对客户端透明。
+
+和 Cache Aside 一样, Read-Through 也有首次请求数据一定不再 Cache 的问题,对于热点数据可以提前放入缓存中。
### Write Behind Pattern(异步缓存写入)
-Write Behind Pattern 和 Read/Write Through Pattern 很相似,两者都是由 cache 服务来负责 cache 和 db 的读写。
+Write Behind(也常被称为 Write-Back) Pattern 和 Read/Write Through Pattern 很相似,两者都是由 Cache 服务来负责 Cache 和 DB 的读写。
+
+但是,两个又有很大的不同:**Read/Write Through 是同步更新 Cache 和 DB,而 Write Behind 则是只更新缓存,不直接更新 DB,而是改为异步批量的方式来更新 DB。**
+
+**写操作 (Write Behind):**
-但是,两个又有很大的不同:**Read/Write Through 是同步更新 cache 和 db,而 Write Behind 则是只更新缓存,不直接更新 db,而是改为异步批量的方式来更新 db。**
+1. 应用将数据写入 Cache,然后**立即返回**。
+2. Cache 服务将这个写操作放入一个队列中。
+3. 通过一个独立的异步线程/任务,将队列中的写操作**批量地、合并地**写入 DB。
-很明显,这种方式对数据一致性带来了更大的挑战,比如 cache 数据可能还没异步更新 db 的话,cache 服务可能就就挂掉了。
+这种模式对数据一致性带来了挑战(例如:Cache 中的数据还没来得及写回 DB,系统就宕机了),因此不适用于需要强一致性的场景(如交易、库存)。
-这种策略在我们平时开发过程中也非常非常少见,但是不代表它的应用场景少,比如消息队列中消息的异步写入磁盘、MySQL 的 Innodb Buffer Pool 机制都用到了这种策略。
+但是,它的异步和批量特性,带来了**无与伦比的写性能**。它在很多高性能系统中都有广泛应用:
-Write Behind Pattern 下 db 的写性能非常高,非常适合一些数据经常变化又对数据一致性要求没那么高的场景,比如浏览量、点赞量。
+- **MySQL 的 InnoDB Buffer Pool 机制:** 数据修改先在内存 Buffer Pool 中完成,然后由后台线程异步刷写到磁盘。
+- **操作系统的页缓存(Page Cache):** 文件写入也是先写到内存,再由操作系统异步刷盘。
+- **高频计数场景:** 对于文章浏览量、帖子点赞数这类允许短暂数据不一致、但写入极其频繁的场景,可以先在 Redis 中快速累加,再通过定时任务异步同步回数据库。
diff --git a/docs/database/redis/cache-basics.md b/docs/database/redis/cache-basics.md
index 391e5bec82d..c72ec83879f 100644
--- a/docs/database/redis/cache-basics.md
+++ b/docs/database/redis/cache-basics.md
@@ -1,8 +1,13 @@
---
title: 缓存基础常见面试题总结(付费)
+description: 缓存基础常见面试题总结,深入讲解缓存穿透、缓存击穿、缓存雪崩的原因和解决方案,以及缓存一致性、淘汰策略等核心知识点。
category: 数据库
tag:
- Redis
+head:
+ - - meta
+ - name: keywords
+ content: 缓存基础,缓存穿透,缓存击穿,缓存雪崩,缓存一致性,缓存淘汰策略,布隆过滤器,分布式缓存
---
**缓存基础** 相关的面试题为我的 [知识星球](../../about-the-author/zhishixingqiu-two-years.md)(点击链接即可查看详细介绍以及加入方法)专属内容,已经整理到了[《Java 面试指北》](../../zhuanlan/java-mian-shi-zhi-bei.md)中。
@@ -10,5 +15,3 @@ tag:

-
-
diff --git a/docs/database/redis/images/why-redis-so-fast.png b/docs/database/redis/images/why-redis-so-fast.png
deleted file mode 100644
index 279e1955473..00000000000
Binary files a/docs/database/redis/images/why-redis-so-fast.png and /dev/null differ
diff --git a/docs/database/redis/redis-cluster.md b/docs/database/redis/redis-cluster.md
index 60cd7dda858..4d8b2ead452 100644
--- a/docs/database/redis/redis-cluster.md
+++ b/docs/database/redis/redis-cluster.md
@@ -1,14 +1,17 @@
---
title: Redis集群详解(付费)
+description: Redis集群相关面试题详解,包括Redis Sentinel哨兵模式、Redis Cluster分片集群的原理、配置和使用,以及主从复制、故障转移等高可用方案。
category: 数据库
tag:
- Redis
+head:
+ - - meta
+ - name: keywords
+ content: Redis集群,Redis Cluster,Redis Sentinel,主从复制,哨兵模式,分片集群,高可用
---
**Redis 集群** 相关的面试题为我的 [知识星球](../../about-the-author/zhishixingqiu-two-years.md)(点击链接即可查看详细介绍以及加入方法)专属内容,已经整理到了[《Java 面试指北》](../../zhuanlan/java-mian-shi-zhi-bei.md)中。
-
+
-
-
diff --git a/docs/database/redis/redis-common-blocking-problems-summary.md b/docs/database/redis/redis-common-blocking-problems-summary.md
index 9aec17fc0cc..95041edee60 100644
--- a/docs/database/redis/redis-common-blocking-problems-summary.md
+++ b/docs/database/redis/redis-common-blocking-problems-summary.md
@@ -1,10 +1,17 @@
---
title: Redis常见阻塞原因总结
+description: 全面总结Redis常见的阻塞原因,包括O(n)复杂度命令、bigkey操作、AOF日志刷盘、RDB快照创建、主从同步等场景,帮助你排查和预防Redis性能问题。
category: 数据库
tag:
- Redis
+head:
+ - - meta
+ - name: keywords
+ content: Redis阻塞,Redis性能问题,O(n)命令,bigkey,AOF刷盘,RDB快照,主从同步,内存达上限
---
+
+
> 本文整理完善自: ,作者:阿 Q 说代码
这篇文章会详细总结一下可能导致 Redis 阻塞的情况,这些情况也是影响 Redis 性能的关键因素,使用 Redis 的时候应该格外注意!
diff --git a/docs/database/redis/redis-data-structures-01.md b/docs/database/redis/redis-data-structures-01.md
index 9dfb0c3eaa5..64468c03f02 100644
--- a/docs/database/redis/redis-data-structures-01.md
+++ b/docs/database/redis/redis-data-structures-01.md
@@ -1,15 +1,13 @@
---
title: Redis 5 种基本数据类型详解
+description: 详解Redis五种基本数据类型String、List、Set、Hash、Zset的使用方法和应用场景,深入分析SDS、跳表、压缩列表等底层数据结构实现原理。
category: 数据库
tag:
- Redis
head:
- - meta
- name: keywords
- content: Redis常见数据类型
- - - meta
- - name: description
- content: Redis基础数据类型总结:String(字符串)、List(列表)、Set(集合)、Hash(散列)、Zset(有序集合)
+ content: Redis数据类型,String,List,Set,Hash,Zset,SDS,跳表,压缩列表,Redis命令
---
Redis 共有 5 种基本数据类型:String(字符串)、List(列表)、Set(集合)、Hash(散列)、Zset(有序集合)。
@@ -182,7 +180,7 @@ Redis 中的 List 其实就是链表数据结构的实现。我在 [线性数据
"value3"
```
-我专门画了一个图方便大家理解 `RPUSH` , `LPOP` , `lpush` , `RPOP` 命令:
+我专门画了一个图方便大家理解 `RPUSH` , `LPOP` , `LPUSH` , `RPOP` 命令:

diff --git a/docs/database/redis/redis-data-structures-02.md b/docs/database/redis/redis-data-structures-02.md
index 9961acbacaa..78e98365bff 100644
--- a/docs/database/redis/redis-data-structures-02.md
+++ b/docs/database/redis/redis-data-structures-02.md
@@ -1,15 +1,13 @@
---
title: Redis 3 种特殊数据类型详解
+description: 详解Redis三种特殊数据类型Bitmap、HyperLogLog、GEO的使用方法和应用场景,包括签到统计、UV统计、附近的人等典型业务场景实现。
category: 数据库
tag:
- Redis
head:
- - meta
- name: keywords
- content: Redis常见数据类型
- - - meta
- - name: description
- content: Redis特殊数据类型总结:HyperLogLogs(基数统计)、Bitmap (位存储)、Geospatial (地理位置)。
+ content: Redis特殊数据类型,Bitmap,HyperLogLog,GEO,位图,基数统计,地理位置,签到统计,UV统计
---
除了 5 种基本的数据类型之外,Redis 还支持 3 种特殊的数据类型:Bitmap、HyperLogLog、GEO。
@@ -36,7 +34,7 @@ Bitmap 存储的是连续的二进制数字(0 和 1),通过 Bitmap, 只需
| ------------------------------------- | ---------------------------------------------------------------- |
| SETBIT key offset value | 设置指定 offset 位置的值 |
| GETBIT key offset | 获取指定 offset 位置的值 |
-| BITCOUNT key start end | 获取 start 和 end 之前值为 1 的元素个数 |
+| BITCOUNT key start end | 获取 start 和 end 之间值为 1 的元素个数 |
| BITOP operation destkey key1 key2 ... | 对一个或多个 Bitmap 进行运算,可用运算符有 AND, OR, XOR 以及 NOT |
**Bitmap 基本操作演示**:
@@ -123,9 +121,9 @@ HyperLogLog 相关的命令非常少,最常用的也就 3 个。
### 应用场景
-**数量量巨大(百万、千万级别以上)的计数场景**
+**数量巨大(百万、千万级别以上)的计数场景**
-- 举例:热门网站每日/每周/每月访问 ip 数统计、热门帖子 uv 统计、
+- 举例:热门网站每日/每周/每月访问 ip 数统计、热门帖子 uv 统计。
- 相关命令:`PFADD`、`PFCOUNT` 。
## Geospatial (地理位置)
diff --git a/docs/database/redis/redis-delayed-task.md b/docs/database/redis/redis-delayed-task.md
index bd95cb46bbb..35c14ab7329 100644
--- a/docs/database/redis/redis-delayed-task.md
+++ b/docs/database/redis/redis-delayed-task.md
@@ -1,8 +1,13 @@
---
title: 如何基于Redis实现延时任务
+description: 详解基于Redis实现延时任务的两种方案:过期事件监听和Redisson延时队列,分析各方案的优缺点、可靠性问题和适用场景。
category: 数据库
tag:
- Redis
+head:
+ - - meta
+ - name: keywords
+ content: Redis延时任务,延时队列,过期事件监听,Redisson DelayedQueue,订单超时,定时任务
---
基于 Redis 实现延时任务的功能无非就下面两种方案:
@@ -33,7 +38,7 @@ Redis 中有很多默认的 channel,这些 channel 是由 Redis 本身向它
我们只需要监听这个 channel,就可以拿到过期的 key 的消息,进而实现了延时任务功能。
-这个功能被 Redis 官方称为 **keyspace notifications** ,作用是实时监控实时监控 Redis 键和值的变化。
+这个功能被 Redis 官方称为 **keyspace notifications** ,作用是实时监控 Redis 键和值的变化。
### Redis 过期事件监听实现延时任务功能有什么缺陷?
@@ -72,7 +77,7 @@ Redisson 是一个开源的 Java 语言 Redis 客户端,提供了很多开箱
Redisson 的延迟队列 RDelayedQueue 是基于 Redis 的 SortedSet 来实现的。SortedSet 是一个有序集合,其中的每个元素都可以设置一个分数,代表该元素的权重。Redisson 利用这一特性,将需要延迟执行的任务插入到 SortedSet 中,并给它们设置相应的过期时间作为分数。
-Redisson 使用 `zrangebyscore` 命令扫描 SortedSet 中过期的元素,然后将这些过期元素从 SortedSet 中移除,并将它们加入到就绪消息列表中。就绪消息列表是一个阻塞队列,有消息进入就会被监听到。这样做可以避免对整个 SortedSet 进行轮询,提高了执行效率。
+Redisson 定期使用 `zrangebyscore` 命令扫描 SortedSet 中过期的元素,然后将这些过期元素从 SortedSet 中移除,并将它们加入到就绪消息列表中。就绪消息列表是一个阻塞队列,有消息进入就会被消费者监听到。这样做可以避免消费者对整个 SortedSet 进行轮询,提高了执行效率。
相比于 Redis 过期事件监听实现延时任务功能,这种方式具备下面这些优势:
diff --git a/docs/database/redis/redis-memory-fragmentation.md b/docs/database/redis/redis-memory-fragmentation.md
index cb2da7476d1..e2d0ea272b9 100644
--- a/docs/database/redis/redis-memory-fragmentation.md
+++ b/docs/database/redis/redis-memory-fragmentation.md
@@ -1,8 +1,13 @@
---
title: Redis内存碎片详解
+description: 深入解析Redis内存碎片产生的原因、判断方法和优化方案,包括内存碎片率计算、jemalloc分配器原理、自动内存碎片清理配置等。
category: 数据库
tag:
- Redis
+head:
+ - - meta
+ - name: keywords
+ content: Redis内存碎片,内存碎片率,jemalloc,内存分配,activedefrag,内存优化,Redis内存管理
---
## 什么是内存碎片?
diff --git a/docs/database/redis/redis-persistence.md b/docs/database/redis/redis-persistence.md
index 1e51df93448..e15e3d0d16c 100644
--- a/docs/database/redis/redis-persistence.md
+++ b/docs/database/redis/redis-persistence.md
@@ -1,15 +1,13 @@
---
title: Redis持久化机制详解
+description: 深入解析Redis三种持久化机制RDB快照、AOF日志和混合持久化的工作原理、配置方法和优缺点对比,帮助你选择适合业务场景的持久化策略。
category: 数据库
tag:
- Redis
head:
- - meta
- name: keywords
- content: Redis持久化机制详解
- - - meta
- - name: description
- content: Redis 不同于 Memcached 的很重要一点就是,Redis 支持持久化,而且支持 3 种持久化方式:快照(snapshotting,RDB)、只追加文件(append-only file, AOF)、RDB 和 AOF 的混合持久化(Redis 4.0 新增)。
+ content: Redis持久化,RDB,AOF,混合持久化,bgsave,数据恢复,Redis备份,fork子进程
---
使用缓存的时候,我们经常需要对内存中的数据进行持久化也就是将内存中的数据写入到硬盘中。大部分原因是为了之后重用数据(比如重启机器、机器故障之后恢复数据),或者是为了做数据同步(比如 Redis 集群的主从节点通过 RDB 文件同步数据)。
@@ -71,7 +69,7 @@ AOF 持久化功能的实现可以简单分为 5 步:
1. **命令追加(append)**:所有的写命令会追加到 AOF 缓冲区中。
2. **文件写入(write)**:将 AOF 缓冲区的数据写入到 AOF 文件中。这一步需要调用`write`函数(系统调用),`write`将数据写入到了系统内核缓冲区之后直接返回了(延迟写)。注意!!!此时并没有同步到磁盘。
-3. **文件同步(fsync)**:AOF 缓冲区根据对应的持久化方式( `fsync` 策略)向硬盘做同步操作。这一步需要调用 `fsync` 函数(系统调用), `fsync` 针对单个文件操作,对其进行强制硬盘同步,`fsync` 将阻塞直到写入磁盘完成后返回,保证了数据持久化。
+3. **文件同步(fsync)**:这一步才是持久化的核心!根据你在 `redis.conf` 文件里 `appendfsync` 配置的策略,Redis 会在不同的时机,调用 `fsync` 函数(系统调用)。`fsync` 针对单个文件操作,对其进行强制硬盘同步(文件在内核缓冲区里的数据写到硬盘),`fsync` 将阻塞直到写入磁盘完成后返回,保证了数据持久化。
4. **文件重写(rewrite)**:随着 AOF 文件越来越大,需要定期对 AOF 文件进行重写,达到压缩的目的。
5. **重启加载(load)**:当 Redis 重启时,可以加载 AOF 文件进行数据恢复。
@@ -90,9 +88,9 @@ AOF 工作流程图如下:
在 Redis 的配置文件中存在三种不同的 AOF 持久化方式( `fsync`策略),它们分别是:
-1. `appendfsync always`:主线程调用 `write` 执行写操作后,后台线程( `aof_fsync` 线程)立即会调用 `fsync` 函数同步 AOF 文件(刷盘),`fsync` 完成后线程返回,这样会严重降低 Redis 的性能(`write` + `fsync`)。
-2. `appendfsync everysec`:主线程调用 `write` 执行写操作后立即返回,由后台线程( `aof_fsync` 线程)每秒钟调用 `fsync` 函数(系统调用)同步一次 AOF 文件(`write`+`fsync`,`fsync`间隔为 1 秒)
-3. `appendfsync no`:主线程调用 `write` 执行写操作后立即返回,让操作系统决定何时进行同步,Linux 下一般为 30 秒一次(`write`但不`fsync`,`fsync` 的时机由操作系统决定)。
+1. `appendfsync always`:主线程调用 `write` 执行写操作后,会立刻调用 `fsync` 函数同步 AOF 文件(刷盘)。主线程会阻塞,直到 `fsync` 将数据完全刷到磁盘后才会返回。这种方式数据最安全,理论上不会有任何数据丢失。但因为每个写操作都会同步阻塞主线程,所以性能极差。
+2. `appendfsync everysec`:主线程调用 `write` 执行写操作后立即返回,由后台线程( `aof_fsync` 线程)每秒钟调用 `fsync` 函数(系统调用)同步一次 AOF 文件(`write`+`fsync`,`fsync`间隔为 1 秒)。这种方式主线程的性能基本不受影响。在性能和数据安全之间做出了绝佳的平衡。不过,在 Redis 异常宕机时,最多可能丢失最近 1 秒内的数据。
+3. `appendfsync no`:主线程调用 `write` 执行写操作后立即返回,让操作系统决定何时进行同步,Linux 下一般为 30 秒一次(`write`但不`fsync`,`fsync` 的时机由操作系统决定)。 这种方式性能最好,因为避免了 `fsync` 的阻塞。但数据安全性最差,宕机时丢失的数据量不可控,取决于操作系统上一次同步的时间点。
可以看出:**这 3 种持久化方式的主要区别在于 `fsync` 同步 AOF 文件的时机(刷盘)**。
@@ -153,9 +151,27 @@ Redis 7.0 版本之后,AOF 重写机制得到了优化改进。下面这段内
### AOF 校验机制了解吗?
-AOF 校验机制是 Redis 在启动时对 AOF 文件进行检查,以判断文件是否完整,是否有损坏或者丢失的数据。这个机制的原理其实非常简单,就是通过使用一种叫做 **校验和(checksum)** 的数字来验证 AOF 文件。这个校验和是通过对整个 AOF 文件内容进行 CRC64 算法计算得出的数字。如果文件内容发生了变化,那么校验和也会随之改变。因此,Redis 在启动时会比较计算出的校验和与文件末尾保存的校验和(计算的时候会把最后一行保存校验和的内容给忽略点),从而判断 AOF 文件是否完整。如果发现文件有问题,Redis 就会拒绝启动并提供相应的错误信息。AOF 校验机制十分简单有效,可以提高 Redis 数据的可靠性。
+纯 AOF 模式下,Redis 不会对整个 AOF 文件使用校验和(如 CRC64),而是通过逐条解析文件中的命令来验证文件的有效性。如果解析过程中发现语法错误(如命令不完整、格式错误),Redis 会终止加载并报错,从而避免错误数据载入内存。
+
+在 **混合持久化模式**(Redis 4.0 引入)下,AOF 文件由两部分组成:
+
+- **RDB 快照部分**:文件以固定的 `REDIS` 字符开头,存储某一时刻的内存数据快照,并在快照数据末尾附带一个 CRC64 校验和(位于 RDB 数据块尾部、AOF 增量部分之前)。
+- **AOF 增量部分**:紧随 RDB 快照部分之后,记录 RDB 快照生成后的增量写命令。这部分增量命令以 Redis 协议格式逐条记录,无整体或全局校验和。
+
+RDB 文件结构的核心部分如下:
+
+| **字段** | **解释** |
+| ----------------- | ---------------------------------------------- |
+| `"REDIS"` | 固定以该字符串开始 |
+| `RDB_VERSION` | RDB 文件的版本号 |
+| `DB_NUM` | Redis 数据库编号,指明数据需要存放到哪个数据库 |
+| `KEY_VALUE_PAIRS` | Redis 中具体键值对的存储 |
+| `EOF` | RDB 文件结束标志 |
+| `CHECK_SUM` | 8 字节确保 RDB 完整性的校验和 |
+
+Redis 启动并加载 AOF 文件时,首先会校验文件开头 RDB 快照部分的数据完整性,即计算该部分数据的 CRC64 校验和,并与紧随 RDB 数据之后、AOF 增量部分之前存储的 CRC64 校验和值进行比较。如果 CRC64 校验和不匹配,Redis 将拒绝启动并报告错误。
-类似地,RDB 文件也有类似的校验机制来保证 RDB 文件的正确性,这里就不重复进行介绍了。
+RDB 部分校验通过后,Redis 随后逐条解析 AOF 部分的增量命令。如果解析过程中出现错误(如不完整的命令或格式错误),Redis 会停止继续加载后续命令,并报告错误,但此时 Redis 已经成功加载了 RDB 快照部分的数据。
## Redis 4.0 对于持久化机制做了什么优化?
diff --git a/docs/database/redis/redis-questions-01.md b/docs/database/redis/redis-questions-01.md
index 4e848049bec..d8789bde3a6 100644
--- a/docs/database/redis/redis-questions-01.md
+++ b/docs/database/redis/redis-questions-01.md
@@ -1,15 +1,13 @@
---
title: Redis常见面试题总结(上)
+description: 最新Redis面试题总结(上):深入讲解Redis基础、五大常用数据结构、单线程模型原理、持久化机制、内存淘汰与过期策略、分布式锁与消息队列实现。适合准备后端面试的开发者!
category: 数据库
tag:
- Redis
head:
- - meta
- name: keywords
- content: Redis基础,Redis常见数据结构,Redis线程模型,Redis内存管理,Redis事务,Redis性能优化
- - - meta
- - name: description
- content: 一篇文章总结Redis常见的知识点和面试题,涵盖Redis基础、Redis常见数据结构、Redis线程模型、Redis内存管理、Redis事务、Redis性能优化等内容。
+ content: Redis面试题,Redis基础,Redis数据结构,Redis线程模型,Redis持久化,Redis内存管理,Redis性能优化,Redis分布式锁,Redis消息队列,Redis延时队列,Redis缓存策略,Redis单线程,Redis多线程,Redis过期策略,Redis淘汰策略
---
@@ -30,39 +28,50 @@ Redis 没有外部依赖,Linux 和 OS X 是 Redis 开发和测试最多的两

-全世界有非常多的网站使用到了 Redis ,[techstacks.io](https://techstacks.io/) 专门维护了一个[使用 Redis 的热门站点列表](https://techstacks.io/tech/redis) ,感兴趣的话可以看看。
+全世界有非常多的网站使用到了 Redis,[techstacks.io](https://techstacks.io/) 专门维护了一个[使用 Redis 的热门站点列表](https://techstacks.io/tech/redis),感兴趣的话可以看看。
+
+### ⭐️Redis 为什么这么快?
+
+Redis 内部做了非常多的性能优化,比较重要的有下面 4 点:
+
+1. **纯内存操作 (Memory-Based Storage)** :这是最主要的原因。Redis 数据读写操作都发生在内存中,访问速度是纳秒级别,而传统数据库频繁读写磁盘的速度是毫秒级别,两者相差数个数量级。
+2. **高效的 I/O 模型 (I/O Multiplexing & Single-Threaded Event Loop)** :Redis 使用单线程事件循环配合 I/O 多路复用技术,让单个线程可以同时处理多个网络连接上的 I/O 事件(如读写),避免了多线程模型中的上下文切换和锁竞争问题。虽然是单线程,但结合内存操作的高效性和 I/O 多路复用,使得 Redis 能轻松处理大量并发请求(Redis 线程模型会在后文中详细介绍到)。
+3. **优化的内部数据结构 (Optimized Data Structures)** :Redis 提供多种数据类型(如 String, List, Hash, Set, Sorted Set 等),其内部实现采用高度优化的编码方式(如 ziplist, quicklist, skiplist, hashtable 等)。Redis 会根据数据大小和类型动态选择最合适的内部编码,以在性能和空间效率之间取得最佳平衡。
+4. **简洁高效的通信协议 (Simple Protocol - RESP)** :Redis 使用的是自己设计的 RESP (REdis Serialization Protocol) 协议。这个协议实现简单、解析性能好,并且是二进制安全的。客户端和服务端之间通信的序列化/反序列化开销很小,有助于提升整体的交互速度。
-### Redis 为什么这么快?
+> 下面这张图片总结的挺不错的,分享一下,出自 [Why is Redis so fast?](https://twitter.com/alexxubyte/status/1498703822528544770)。
-Redis 内部做了非常多的性能优化,比较重要的有下面 3 点:
+
-1. Redis 基于内存,内存的访问速度比磁盘快很多;
-2. Redis 基于 Reactor 模式设计开发了一套高效的事件处理模型,主要是单线程事件循环和 IO 多路复用(Redis 线程模式后面会详细介绍到);
-3. Redis 内置了多种优化过后的数据类型/结构实现,性能非常高。
-4. Redis 通信协议实现简单且解析高效。
+那既然都这么快了,为什么不直接用 Redis 当主数据库呢?主要是因为内存成本太高,并且 Redis 提供的数据持久化仍然有数据丢失的风险。
-> 下面这张图片总结的挺不错的,分享一下,出自 [Why is Redis so fast?](https://twitter.com/alexxubyte/status/1498703822528544770) 。
+### 除了 Redis,你还知道其他分布式缓存方案吗?
-
+如果面试中被问到这个问题的话,面试官主要想看看:
-那既然都这么快了,为什么不直接用 Redis 当主数据库呢?主要是因为内存成本太高且 Redis 提供的数据持久化仍然有数据丢失的风险。
+1. 你在选择 Redis 作为分布式缓存方案时,是否是经过严谨的调研和思考,还是只是因为 Redis 是当前的“热门”技术。
+2. 你在分布式缓存方向的技术广度。
-### 分布式缓存常见的技术选型方案有哪些?
+如果你了解其他方案,并且能解释为什么最终选择了 Redis(更进一步!),这会对你面试表现加分不少!
+
+下面简单聊聊常见的分布式缓存技术选型。
分布式缓存的话,比较老牌同时也是使用的比较多的还是 **Memcached** 和 **Redis**。不过,现在基本没有看过还有项目使用 **Memcached** 来做缓存,都是直接用 **Redis**。
Memcached 是分布式缓存最开始兴起的那会,比较常用的。后来,随着 Redis 的发展,大家慢慢都转而使用更加强大的 Redis 了。
-有一些大厂也开源了类似于 Redis 的分布式高性能 KV 存储数据库,例如,腾讯开源的 [Tendis](https://github.com/Tencent/Tendis) 。Tendis 基于知名开源项目 [RocksDB](https://github.com/facebook/rocksdb) 作为存储引擎 ,100% 兼容 Redis 协议和 Redis4.0 所有数据模型。关于 Redis 和 Tendis 的对比,腾讯官方曾经发过一篇文章:[Redis vs Tendis:冷热混合存储版架构揭秘](https://mp.weixin.qq.com/s/MeYkfOIdnU6LYlsGb24KjQ) ,可以简单参考一下。
+有一些大厂也开源了类似于 Redis 的分布式高性能 KV 存储数据库,例如,腾讯开源的 [**Tendis**](https://github.com/Tencent/Tendis)。Tendis 基于知名开源项目 [RocksDB](https://github.com/facebook/rocksdb) 作为存储引擎 ,100% 兼容 Redis 协议和 Redis4.0 所有数据模型。关于 Redis 和 Tendis 的对比,腾讯官方曾经发过一篇文章:[Redis vs Tendis:冷热混合存储版架构揭秘](https://mp.weixin.qq.com/s/MeYkfOIdnU6LYlsGb24KjQ),可以简单参考一下。
不过,从 Tendis 这个项目的 Github 提交记录可以看出,Tendis 开源版几乎已经没有被维护更新了,加上其关注度并不高,使用的公司也比较少。因此,不建议你使用 Tendis 来实现分布式缓存。
目前,比较业界认可的 Redis 替代品还是下面这两个开源分布式缓存(都是通过碰瓷 Redis 火的):
- [Dragonfly](https://github.com/dragonflydb/dragonfly):一种针对现代应用程序负荷需求而构建的内存数据库,完全兼容 Redis 和 Memcached 的 API,迁移时无需修改任何代码,号称全世界最快的内存数据库。
-- [KeyDB](https://github.com/Snapchat/KeyDB): Redis 的一个高性能分支,专注于多线程、内存效率和高吞吐量。
+- [KeyDB](https://github.com/Snapchat/KeyDB):Redis 的一个高性能分支,专注于多线程、内存效率和高吞吐量。
+
+不过,个人还是建议分布式缓存首选 Redis,毕竟经过了这么多年的考验,生态非常优秀,资料也很全面!
-不过,个人还是建议分布式缓存首选 Redis ,毕竟经过这么多年的生考验,生态也这么优秀,资料也很全面。
+PS:篇幅问题,我这并没有对上面提到的分布式缓存选型做详细介绍和对比,感兴趣的话,可以自行研究一下。
### 说一下 Redis 和 Memcached 的区别和共同点
@@ -76,16 +85,16 @@ Memcached 是分布式缓存最开始兴起的那会,比较常用的。后来
**区别**:
-1. **数据类型**:Redis 支持更丰富的数据类型(支持更复杂的应用场景)。Redis 不仅仅支持简单的 k/v 类型的数据,同时还提供 list,set,zset,hash 等数据结构的存储。Memcached 只支持最简单的 k/v 数据类型。
-2. **数据持久化**:Redis 支持数据的持久化,可以将内存中的数据保持在磁盘中,重启的时候可以再次加载进行使用,而 Memcached 把数据全部存在内存之中。也就是说,Redis 有灾难恢复机制而 Memcached 没有。
-3. **集群模式支持**:Memcached 没有原生的集群模式,需要依靠客户端来实现往集群中分片写入数据;但是 Redis 自 3.0 版本起是原生支持集群模式的。
-4. **线程模型**:Memcached 是多线程,非阻塞 IO 复用的网络模型;Redis 使用单线程的多路 IO 复用模型。 (Redis 6.0 针对网络数据的读写引入了多线程)
+1. **数据类型**:Redis 支持更丰富的数据类型(支持更复杂的应用场景)。Redis 不仅仅支持简单的 k/v 类型的数据,同时还提供 list、set、zset、hash 等数据结构的存储;而 Memcached 只支持最简单的 k/v 数据类型。
+2. **数据持久化**:Redis 支持数据的持久化,可以将内存中的数据保持在磁盘中,重启的时候可以再次加载进行使用;而 Memcached 把数据全部存在内存之中。也就是说,Redis 有灾难恢复机制,而 Memcached 没有。
+3. **集群模式支持**:Memcached 没有原生的集群模式,需要依靠客户端来实现往集群中分片写入数据;而 Redis 自 3.0 版本起是原生支持集群模式的。
+4. **线程模型**:Memcached 是多线程、非阻塞 IO 复用的网络模型;而 Redis 使用单线程的多路 IO 复用模型(Redis 6.0 针对网络数据的读写引入了多线程)。
5. **特性支持**:Redis 支持发布订阅模型、Lua 脚本、事务等功能,而 Memcached 不支持。并且,Redis 支持更多的编程语言。
6. **过期数据删除**:Memcached 过期数据的删除策略只用了惰性删除,而 Redis 同时使用了惰性删除与定期删除。
相信看了上面的对比之后,我们已经没有什么理由可以选择使用 Memcached 来作为自己项目的分布式缓存了。
-### 为什么要用 Redis?
+### ⭐️为什么要用 Redis?
**1、访问速度更快**
@@ -93,7 +102,7 @@ Memcached 是分布式缓存最开始兴起的那会,比较常用的。后来
**2、高并发**
-一般像 MySQL 这类的数据库的 QPS 大概都在 4k 左右(4 核 8g) ,但是使用 Redis 缓存之后很容易达到 5w+,甚至能达到 10w+(就单机 Redis 的情况,Redis 集群的话会更高)。
+一般像 MySQL 这类的数据库的 QPS 大概都在 4k 左右(4 核 8g),但是使用 Redis 缓存之后很容易达到 5w+,甚至能达到 10w+(就单机 Redis 的情况,Redis 集群的话会更高)。
> QPS(Query Per Second):服务器每秒可以执行的查询次数;
@@ -103,9 +112,19 @@ Memcached 是分布式缓存最开始兴起的那会,比较常用的。后来
Redis 除了可以用作缓存之外,还可以用于分布式锁、限流、消息队列、延时队列等场景,功能强大!
+### ⭐️为什么用 Redis 而不用本地缓存呢?
+
+| 特性 | 本地缓存 | Redis |
+| ------------ | ------------------------------------ | -------------------------------- |
+| 数据一致性 | 多服务器部署时存在数据不一致问题 | 数据一致 |
+| 内存限制 | 受限于单台服务器内存 | 独立部署,内存空间更大 |
+| 数据丢失风险 | 服务器宕机数据丢失 | 可持久化,数据不易丢失 |
+| 管理维护 | 分散,管理不便 | 集中管理,提供丰富的管理工具 |
+| 功能丰富性 | 功能有限,通常只提供简单的键值对存储 | 功能丰富,支持多种数据结构和功能 |
+
### 常见的缓存读写策略有哪些?
-关于常见的缓存读写策略的详细介绍,可以看我写的这篇文章:[3 种常用的缓存读写策略详解](https://javaguide.cn/database/redis/3-commonly-used-cache-read-and-write-strategies.html) 。
+关于常见的缓存读写策略的详细介绍,可以看我写的这篇文章:[3 种常用的缓存读写策略详解](https://javaguide.cn/database/redis/3-commonly-used-cache-read-and-write-strategies.html)。
### 什么是 Redis Module?有什么用?
@@ -126,21 +145,21 @@ Redis 从 4.0 版本开始,支持通过 Module 来扩展其功能以满足特
关于 Redis 模块的详细介绍,可以查看官方文档:。
-## Redis 应用
+## ⭐️Redis 应用
### Redis 除了做缓存,还能做什么?
-- **分布式锁**:通过 Redis 来做分布式锁是一种比较常见的方式。通常情况下,我们都是基于 Redisson 来实现分布式锁。关于 Redis 实现分布式锁的详细介绍,可以看我写的这篇文章:[分布式锁详解](https://javaguide.cn/distributed-system/distributed-lock.html) 。
+- **分布式锁**:通过 Redis 来做分布式锁是一种比较常见的方式。通常情况下,我们都是基于 Redisson 来实现分布式锁。关于 Redis 实现分布式锁的详细介绍,可以看我写的这篇文章:[分布式锁详解](https://javaguide.cn/distributed-system/distributed-lock.html)。
- **限流**:一般是通过 Redis + Lua 脚本的方式来实现限流。如果不想自己写 Lua 脚本的话,也可以直接利用 Redisson 中的 `RRateLimiter` 来实现分布式限流,其底层实现就是基于 Lua 代码+令牌桶算法。
- **消息队列**:Redis 自带的 List 数据结构可以作为一个简单的队列使用。Redis 5.0 中增加的 Stream 类型的数据结构更加适合用来做消息队列。它比较类似于 Kafka,有主题和消费组的概念,支持消息持久化以及 ACK 机制。
- **延时队列**:Redisson 内置了延时队列(基于 Sorted Set 实现的)。
-- **分布式 Session** :利用 String 或者 Hash 数据类型保存 Session 数据,所有的服务器都可以访问。
-- **复杂业务场景**:通过 Redis 以及 Redis 扩展(比如 Redisson)提供的数据结构,我们可以很方便地完成很多复杂的业务场景比如通过 Bitmap 统计活跃用户、通过 Sorted Set 维护排行榜。
+- **分布式 Session**:利用 String 或者 Hash 数据类型保存 Session 数据,所有的服务器都可以访问。
+- **复杂业务场景**:通过 Redis 以及 Redis 扩展(比如 Redisson)提供的数据结构,我们可以很方便地完成很多复杂的业务场景,比如通过 Bitmap 统计活跃用户、通过 Sorted Set 维护排行榜、通过 HyperLogLog 统计网站 UV 和 PV。
- ……
### 如何基于 Redis 实现分布式锁?
-关于 Redis 实现分布式锁的详细介绍,可以看我写的这篇文章:[分布式锁详解](https://javaguide.cn/distributed-system/distributed-lock-implementations.html) 。
+关于 Redis 实现分布式锁的详细介绍,可以看我写的这篇文章:[分布式锁详解](https://javaguide.cn/distributed-system/distributed-lock-implementations.html)。
### Redis 可以做消息队列么?
@@ -150,7 +169,7 @@ Redis 从 4.0 版本开始,支持通过 Module 来扩展其功能以满足特
**Redis 2.0 之前,如果想要使用 Redis 来做消息队列的话,只能通过 List 来实现。**
-通过 `RPUSH/LPOP` 或者 `LPUSH/RPOP`即可实现简易版消息队列:
+通过 `RPUSH/LPOP` 或者 `LPUSH/RPOP` 即可实现简易版消息队列:
```bash
# 生产者生产消息
@@ -163,7 +182,7 @@ Redis 从 4.0 版本开始,支持通过 Module 来扩展其功能以满足特
"msg1"
```
-不过,通过 `RPUSH/LPOP` 或者 `LPUSH/RPOP`这样的方式存在性能问题,我们需要不断轮询去调用 `RPOP` 或 `LPOP` 来消费消息。当 List 为空时,大部分的轮询的请求都是无效请求,这种方式大量浪费了系统资源。
+不过,通过 `RPUSH/LPOP` 或者 `LPUSH/RPOP` 这样的方式存在性能问题,我们需要不断轮询去调用 `RPOP` 或 `LPOP` 来消费消息。当 List 为空时,大部分的轮询的请求都是无效请求,这种方式大量浪费了系统资源。
因此,Redis 还提供了 `BLPOP`、`BRPOP` 这种阻塞式读取的命令(带 B-Blocking 的都是阻塞式),并且还支持一个超时参数。如果 List 为空,Redis 服务端不会立刻返回结果,它会等待 List 中有新数据后再返回或者是等待最多一个超时时间后返回空。如果将超时时间设置为 0 时,即可无限等待,直到弹出消息
@@ -195,11 +214,11 @@ pub/sub 既能单播又能广播,还支持 channel 的简单正则匹配。不
为此,Redis 5.0 新增加的一个数据结构 `Stream` 来做消息队列。`Stream` 支持:
-- 发布 / 订阅模式
-- 按照消费者组进行消费(借鉴了 Kafka 消费者组的概念)
-- 消息持久化( RDB 和 AOF)
-- ACK 机制(通过确认机制来告知已经成功处理了消息)
-- 阻塞式获取消息
+- 发布 / 订阅模式;
+- 按照消费者组进行消费(借鉴了 Kafka 消费者组的概念);
+- 消息持久化( RDB 和 AOF);
+- ACK 机制(通过确认机制来告知已经成功处理了消息);
+- 阻塞式获取消息。
`Stream` 的结构如下:
@@ -209,7 +228,7 @@ pub/sub 既能单播又能广播,还支持 channel 的简单正则匹配。不
这里再对图中涉及到的一些概念,进行简单解释:
-- `Consumer Group`:消费者组用于组织和管理多个消费者。消费者组本身不处理消息,而是再将消息分发给消费者,由消费者进行真正的消费
+- `Consumer Group`:消费者组用于组织和管理多个消费者。消费者组本身不处理消息,而是再将消息分发给消费者,由消费者进行真正的消费。
- `last_delivered_id`:标识消费者组当前消费位置的游标,消费者组中任意一个消费者读取了消息都会使 last_delivered_id 往前移动。
- `pending_ids`:记录已经被客户端消费但没有 ack 的消息的 ID。
@@ -224,25 +243,25 @@ pub/sub 既能单播又能广播,还支持 channel 的简单正则匹配。不
- `XTRIM`:修剪流的长度,可以指定修建策略(`MAXLEN`/`MINID`)。
- `XLEN`:获取流的长度。
- `XGROUP CREATE`:创建消费者组。
-- `XGROUP DESTROY` : 删除消费者组
+- `XGROUP DESTROY`:删除消费者组。
- `XGROUP DELCONSUMER`:从消费者组中删除一个消费者。
-- `XGROUP SETID`:为消费者组设置新的最后递送消息 ID
+- `XGROUP SETID`:为消费者组设置新的最后递送消息 ID。
- `XACK`:确认消费组中的消息已被处理。
- `XPENDING`:查询消费组中挂起(未确认)的消息。
- `XCLAIM`:将挂起的消息从一个消费者转移到另一个消费者。
-- `XINFO`:获取流(`XINFO STREAM`)、消费组(`XINFO GROUPS`)或消费者(`XINFO CONSUMERS`)的详细信息。
+- `XINFO`:获取流(`XINFO STREAM`)、消费组(`XINFO GROUPS`)或消费者(`XINFO CONSUMERS`)的详细信息。
`Stream` 使用起来相对要麻烦一些,这里就不演示了。
-总的来说,`Stream` 已经可以满足一个消息队列的基本要求了。不过,`Stream` 在实际使用中依然会有一些小问题不太好解决比如在 Redis 发生故障恢复后不能保证消息至少被消费一次。
+总的来说,`Stream` 已经可以满足一个消息队列的基本要求了。不过,`Stream` 在实际使用中依然会有一些小问题不太好解决,比如在 Redis 发生故障恢复后不能保证消息至少被消费一次。
-综上,和专业的消息队列相比,使用 Redis 来实现消息队列还是有很多欠缺的地方比如消息丢失和堆积问题不好解决。因此,我们通常建议不要使用 Redis 来做消息队列,你完全可以选择市面上比较成熟的一些消息队列比如 RocketMQ、Kafka。不过,如果你就是想要用 Redis 来做消息队列的话,那我建议你优先考虑 `Stream`,这是目前相对最优的 Redis 消息队列实现。
+综上,和专业的消息队列相比,使用 Redis 来实现消息队列还是有很多欠缺的地方,比如消息丢失和堆积问题不好解决。因此,我们通常建议不要使用 Redis 来做消息队列,你完全可以选择市面上比较成熟的一些消息队列,比如 RocketMQ、Kafka。不过,如果你就是想要用 Redis 来做消息队列的话,那我建议你优先考虑 `Stream`,这是目前相对最优的 Redis 消息队列实现。
相关阅读:[Redis 消息队列发展历程 - 阿里开发者 - 2022](https://mp.weixin.qq.com/s/gCUT5TcCQRAxYkTJfTRjJw)。
### Redis 可以做搜索引擎么?
-Redis 是可以实现全文搜索引擎功能的,需要借助 **RediSearch** ,这是一个基于 Redis 的搜索引擎模块。
+Redis 是可以实现全文搜索引擎功能的,需要借助 **RediSearch**,这是一个基于 Redis 的搜索引擎模块。
RediSearch 支持中文分词、聚合统计、停用词、同义词、拼写检查、标签查询、向量相似度查询、多关键词搜索、分页搜索等功能,算是一个功能比较完善的全文搜索引擎了。
@@ -253,7 +272,7 @@ RediSearch 支持中文分词、聚合统计、停用词、同义词、拼写检
对于小型项目的简单搜索场景来说,使用 RediSearch 来作为搜索引擎还是没有问题的(搭配 RedisJSON 使用)。
-对于比较复杂或者数据规模较大的搜索场景还是不太建议使用 RediSearch 来作为搜索引擎,主要是因为下面这些限制和问题:
+对于比较复杂或者数据规模较大的搜索场景,还是不太建议使用 RediSearch 来作为搜索引擎,主要是因为下面这些限制和问题:
1. 数据量限制:Elasticsearch 可以支持 PB 级别的数据量,可以轻松扩展到多个节点,利用分片机制提高可用性和性能。RedisSearch 是基于 Redis 实现的,其能存储的数据量受限于 Redis 的内存容量,不太适合存储大规模的数据(内存昂贵,扩展能力较差)。
2. 分布式能力较差:Elasticsearch 是为分布式环境设计的,可以轻松扩展到多个节点。虽然 RedisSearch 支持分布式部署,但在实际应用中可能会面临一些挑战,如数据分片、节点间通信、数据一致性等问题。
@@ -271,10 +290,10 @@ Elasticsearch 适用于全文搜索、复杂查询、实时数据分析和聚合
基于 Redis 实现延时任务的功能无非就下面两种方案:
-1. Redis 过期事件监听
-2. Redisson 内置的延时队列
+1. Redis 过期事件监听。
+2. Redisson 内置的延时队列。
-Redis 过期事件监听的存在时效性较差、丢消息、多服务实例下消息重复消费等问题,不被推荐使用。
+Redis 过期事件监听存在时效性较差、丢消息、多服务实例下消息重复消费等问题,不被推荐使用。
Redisson 内置的延时队列具备下面这些优势:
@@ -283,9 +302,9 @@ Redisson 内置的延时队列具备下面这些优势:
关于 Redis 实现延时任务的详细介绍,可以看我写的这篇文章:[如何基于 Redis 实现延时任务?](./redis-delayed-task.md)。
-## Redis 数据类型
+## ⭐️Redis 数据类型
-关于 Redis 5 种基础数据类型和 3 种特殊数据类型的详细介绍请看下面这两篇文章以及 [Redis 官方文档](https://redis.io/docs/data-types/) :
+关于 Redis 5 种基础数据类型和 3 种特殊数据类型的详细介绍请看下面这两篇文章以及 [Redis 官方文档](https://redis.io/docs/data-types/):
- [Redis 5 种基本数据类型详解](https://javaguide.cn/database/redis/redis-data-structures-01.html)
- [Redis 3 种特殊数据类型详解](https://javaguide.cn/database/redis/redis-data-structures-02.html)
@@ -307,25 +326,32 @@ String 的常见应用场景如下:
- 常规数据(比如 Session、Token、序列化后的对象、图片的路径)的缓存;
- 计数比如用户单位时间的请求数(简单限流可以用到)、页面单位时间的访问数;
-- 分布式锁(利用 `SETNX key value` 命令可以实现一个最简易的分布式锁);
+- 分布式锁(利用 `SETNX key value` 命令可以实现一个最简易的分布式锁);
- ……
关于 String 的详细介绍请看这篇文章:[Redis 5 种基本数据类型详解](https://javaguide.cn/database/redis/redis-data-structures-01.html)。
### String 还是 Hash 存储对象数据更好呢?
-- String 存储的是序列化后的对象数据,存放的是整个对象。Hash 是对对象的每个字段单独存储,可以获取部分字段的信息,也可以修改或者添加部分字段,节省网络流量。如果对象中某些字段需要经常变动或者经常需要单独查询对象中的个别字段信息,Hash 就非常适合。
-- String 存储相对来说更加节省内存,缓存相同数量的对象数据,String 消耗的内存约是 Hash 的一半。并且,存储具有多层嵌套的对象时也方便很多。如果系统对性能和资源消耗非常敏感的话,String 就非常适合。
+简单对比一下二者:
-在绝大部分情况,我们建议使用 String 来存储对象数据即可!
+- **对象存储方式**:String 存储的是序列化后的对象数据,存放的是整个对象,操作简单直接。Hash 是对对象的每个字段单独存储,可以获取部分字段的信息,也可以修改或者添加部分字段,节省网络流量。如果对象中某些字段需要经常变动或者经常需要单独查询对象中的个别字段信息,Hash 就非常适合。
+- **内存消耗**:Hash 通常比 String 更节省内存,特别是在字段较多且字段长度较短时。Redis 对小型 Hash 进行优化(如使用 ziplist 存储),进一步降低内存占用。
+- **复杂对象存储**:String 在处理多层嵌套或复杂结构的对象时更方便,因为无需处理每个字段的独立存储和操作。
+- **性能**:String 的操作通常具有 O(1) 的时间复杂度,因为它存储的是整个对象,操作简单直接,整体读写的性能较好。Hash 由于需要处理多个字段的增删改查操作,在字段较多且经常变动的情况下,可能会带来额外的性能开销。
+
+总结:
+
+- 在绝大多数情况下,**String** 更适合存储对象数据,尤其是当对象结构简单且整体读写是主要操作时。
+- 如果你需要频繁操作对象的部分字段或节省内存,**Hash** 可能是更好的选择。
### String 的底层实现是什么?
-Redis 是基于 C 语言编写的,但 Redis 的 String 类型的底层实现并不是 C 语言中的字符串(即以空字符 `\0` 结尾的字符数组),而是自己编写了 [SDS](https://github.com/antirez/sds)(Simple Dynamic String,简单动态字符串) 来作为底层实现。
+Redis 是基于 C 语言编写的,但 Redis 的 String 类型的底层实现并不是 C 语言中的字符串(即以空字符 `\0` 结尾的字符数组),而是自己编写了 [SDS](https://github.com/antirez/sds)(Simple Dynamic String,简单动态字符串)来作为底层实现。
SDS 最早是 Redis 作者为日常 C 语言开发而设计的 C 字符串,后来被应用到了 Redis 上,并经过了大量的修改完善以适合高性能操作。
-Redis7.0 的 SDS 的部分源码如下():
+Redis7.0 的 SDS 的部分源码如下():
```c
/* Note: sdshdr5 is never used, we just access the flags byte directly.
@@ -360,7 +386,7 @@ struct __attribute__ ((__packed__)) sdshdr64 {
};
```
-通过源码可以看出,SDS 共有五种实现方式 SDS_TYPE_5(并未用到)、SDS_TYPE_8、SDS_TYPE_16、SDS_TYPE_32、SDS_TYPE_64,其中只有后四种实际用到。Redis 会根据初始化的长度决定使用哪种类型,从而减少内存的使用。
+通过源码可以看出,SDS 共有五种实现方式:SDS_TYPE_5(并未用到)、SDS_TYPE_8、SDS_TYPE_16、SDS_TYPE_32、SDS_TYPE_64,其中只有后四种实际用到。Redis 会根据初始化的长度决定使用哪种类型,从而减少内存的使用。
| 类型 | 字节 | 位 |
| -------- | ---- | --- |
@@ -372,10 +398,10 @@ struct __attribute__ ((__packed__)) sdshdr64 {
对于后四种实现都包含了下面这 4 个属性:
-- `len`:字符串的长度也就是已经使用的字节数
-- `alloc`:总共可用的字符空间大小,alloc-len 就是 SDS 剩余的空间大小
-- `buf[]`:实际存储字符串的数组
-- `flags`:低三位保存类型标志
+- `len`:字符串的长度也就是已经使用的字节数。
+- `alloc`:总共可用的字符空间大小,alloc-len 就是 SDS 剩余的空间大小。
+- `buf[]`:实际存储字符串的数组。
+- `flags`:低三位保存类型标志。
SDS 相比于 C 语言中的字符串有如下提升:
@@ -417,9 +443,9 @@ struct sdshdr {
### 使用 Redis 实现一个排行榜怎么做?
-Redis 中有一个叫做 `Sorted Set` (有序集合)的数据类型经常被用在各种排行榜的场景,比如直播间送礼物的排行榜、朋友圈的微信步数排行榜、王者荣耀中的段位排行榜、话题热度排行榜等等。
+Redis 中有一个叫做 `Sorted Set`(有序集合)的数据类型经常被用在各种排行榜的场景,比如直播间送礼物的排行榜、朋友圈的微信步数排行榜、王者荣耀中的段位排行榜、话题热度排行榜等等。
-相关的一些 Redis 命令: `ZRANGE` (从小到大排序)、 `ZREVRANGE` (从大到小排序)、`ZREVRANK` (指定元素排名)。
+相关的一些 Redis 命令:`ZRANGE`(从小到大排序)、`ZREVRANGE`(从大到小排序)、`ZREVRANK`(指定元素排名)。

@@ -427,15 +453,15 @@ Redis 中有一个叫做 `Sorted Set` (有序集合)的数据类型经常被

-### Redis 的有序集合底层为什么要用跳表,而不用平衡树、红黑树或者 B+树?
+### Redis 的有序集合底层为什么要用跳表,而不用平衡树、红黑树或者 B+ 树?
这道面试题很多大厂比较喜欢问,难度还是有点大的。
- 平衡树 vs 跳表:平衡树的插入、删除和查询的时间复杂度和跳表一样都是 **O(log n)**。对于范围查询来说,平衡树也可以通过中序遍历的方式达到和跳表一样的效果。但是它的每一次插入或者删除操作都需要保证整颗树左右节点的绝对平衡,只要不平衡就要通过旋转操作来保持平衡,这个过程是比较耗时的。跳表诞生的初衷就是为了克服平衡树的一些缺点。跳表使用概率平衡而不是严格强制的平衡,因此,跳表中的插入和删除算法比平衡树的等效算法简单得多,速度也快得多。
- 红黑树 vs 跳表:相比较于红黑树来说,跳表的实现也更简单一些,不需要通过旋转和染色(红黑变换)来保证黑平衡。并且,按照区间来查找数据这个操作,红黑树的效率没有跳表高。
-- B+树 vs 跳表:B+树更适合作为数据库和文件系统中常用的索引结构之一,它的核心思想是通过可能少的 IO 定位到尽可能多的索引来获得查询数据。对于 Redis 这种内存数据库来说,它对这些并不感冒,因为 Redis 作为内存数据库它不可能存储大量的数据,所以对于索引不需要通过 B+树这种方式进行维护,只需按照概率进行随机维护即可,节约内存。而且使用跳表实现 zset 时相较前者来说更简单一些,在进行插入时只需通过索引将数据插入到链表中合适的位置再随机维护一定高度的索引即可,也不需要像 B+树那样插入时发现失衡时还需要对节点分裂与合并。
+- B+ 树 vs 跳表:B+ 树更适合作为数据库和文件系统中常用的索引结构之一,它的核心思想是通过可能少的 IO 定位到尽可能多的索引来获得查询数据。对于 Redis 这种内存数据库来说,它对这些并不感冒,因为 Redis 作为内存数据库它不可能存储大量的数据,所以对于索引不需要通过 B+ 树这种方式进行维护,只需按照概率进行随机维护即可,节约内存。而且使用跳表实现 zset 时相较前者来说更简单一些,在进行插入时只需通过索引将数据插入到链表中合适的位置再随机维护一定高度的索引即可,也不需要像 B+ 树那样插入时发现失衡时还需要对节点分裂与合并。
-另外,我还单独写了一篇文章从有序集合的基本使用到跳表的源码分析和实现,让你会对 Redis 的有序集合底层实现的跳表有着更深刻的理解和掌握 :[Redis 为什么用跳表实现有序集合](./redis-skiplist.md)。
+另外,我还单独写了一篇文章从有序集合的基本使用到跳表的源码分析和实现,让你会对 Redis 的有序集合底层实现的跳表有着更深刻的理解和掌握:[Redis 为什么用跳表实现有序集合](https://javaguide.cn/database/redis/redis-skiplist.html)。
### Set 的应用场景是什么?
@@ -443,8 +469,8 @@ Redis 中 `Set` 是一种无序集合,集合中的元素没有先后顺序但
`Set` 的常见应用场景如下:
-- 存放的数据不能重复的场景:网站 UV 统计(数据量巨大的场景还是 `HyperLogLog`更适合一些)、文章点赞、动态点赞等等。
-- 需要获取多个数据源交集、并集和差集的场景:共同好友(交集)、共同粉丝(交集)、共同关注(交集)、好友推荐(差集)、音乐推荐(差集)、订阅号推荐(差集+交集) 等等。
+- 存放的数据不能重复的场景:网站 UV 统计(数据量巨大的场景还是 `HyperLogLog` 更适合一些)、文章点赞、动态点赞等等。
+- 需要获取多个数据源交集、并集和差集的场景:共同好友(交集)、共同粉丝(交集)、共同关注(交集)、好友推荐(差集)、音乐推荐(差集)、订阅号推荐(差集+交集)等等。
- 需要随机获取数据源中的元素的场景:抽奖系统、随机点名等等。
### 使用 Set 实现抽奖系统怎么做?
@@ -453,11 +479,11 @@ Redis 中 `Set` 是一种无序集合,集合中的元素没有先后顺序但
- `SADD key member1 member2 ...`:向指定集合添加一个或多个元素。
- `SPOP key count`:随机移除并获取指定集合中一个或多个元素,适合不允许重复中奖的场景。
-- `SRANDMEMBER key count` : 随机获取指定集合中指定数量的元素,适合允许重复中奖的场景。
+- `SRANDMEMBER key count`:随机获取指定集合中指定数量的元素,适合允许重复中奖的场景。
### 使用 Bitmap 统计活跃用户怎么做?
-Bitmap 存储的是连续的二进制数字(0 和 1),通过 Bitmap, 只需要一个 bit 位来表示某个元素对应的值或者状态,key 就是对应元素本身 。我们知道 8 个 bit 可以组成一个 byte,所以 Bitmap 本身会极大的节省储存空间。
+Bitmap 存储的是连续的二进制数字(0 和 1),通过 Bitmap,只需要一个 bit 位来表示某个元素对应的值或者状态,key 就是对应元素本身。我们知道 8 个 bit 可以组成一个 byte,所以 Bitmap 本身会极大的节省储存空间。
你可以将 Bitmap 看作是一个存储二进制数字(0 和 1)的数组,数组中每个元素的下标叫做 offset(偏移量)。
@@ -476,7 +502,7 @@ Bitmap 存储的是连续的二进制数字(0 和 1),通过 Bitmap, 只需
(integer) 0
```
-统计 20210308~20210309 总活跃用户数:
+统计 20210308~20210309 总活跃用户数:
```bash
> BITOP and desk1 20210308 20210309
@@ -485,7 +511,7 @@ Bitmap 存储的是连续的二进制数字(0 和 1),通过 Bitmap, 只需
(integer) 1
```
-统计 20210308~20210309 在线活跃用户数:
+统计 20210308~20210309 在线活跃用户数:
```bash
> BITOP or desk2 20210308 20210309
@@ -494,6 +520,27 @@ Bitmap 存储的是连续的二进制数字(0 和 1),通过 Bitmap, 只需
(integer) 2
```
+### HyperLogLog 适合什么场景?
+
+HyperLogLog (HLL) 是一种非常巧妙的概率性数据结构,它专门解决一类非常棘手的大数据问题:在海量数据中,用极小的内存,估算一个集合中不重复元素的数量,也就是我们常说的基数(Cardinality)
+
+HLL 做的最核心的权衡,就是用一点点精确度的损失,来换取巨大的内存空间节省。它给出的不是一个 100%精确的数字,而是一个带有很小标准误差(Redis 中默认是 0.81%)的近似值。
+
+**基于这个核心权衡,HyperLogLog 最适合以下特征的场景:**
+
+1. **数据量巨大,内存敏感:** 这是 HLL 的主战场。比如,要统计一个亿级日活 App 的每日独立访客数。如果用传统的 Set 来存储用户 ID,一个 ID 占几十个字节,上亿个 ID 可能需要几个 GB 甚至几十 GB 的内存,这在很多场景下是不可接受的。而 HLL,在 Redis 中只需要固定的 12KB 内存,就能处理天文数字级别的基数,这是一个颠覆性的优势。
+2. **对结果的精确度要求不是 100%:** 这是使用 HLL 的前提。比如,产品经理想知道一个热门帖子的 UV(独立访客数)是大约 1000 万还是 1010 万,这个细微的差别通常不影响商业决策。但如果场景是统计一个交易系统的准确交易笔数,那 HLL 就完全不适用,因为金融场景要求 100%的精确。
+
+**所以,HyperLogLog 具体的应用场景就非常清晰了:**
+
+- **网站/App 的 UV(Unique Visitor)统计:** 比如统计首页每天有多少个不同的 IP 或用户 ID 访问过。
+- **搜索引擎关键词统计:** 统计每天有多少个不同的用户搜索了某个关键词。
+- **社交网络互动统计:** 比如统计一条微博被多少个不同的用户转发过。
+
+在这些场景下,我们关心的是数量级和趋势,而不是个位数的差异。
+
+最后,Redis 的实现还非常智能,它内部会根据基数的大小,在**稀疏矩阵**(占用空间更小)和**稠密矩阵**(固定的 12KB)之间自动切换,进一步优化了内存使用。总而言之,当您需要对海量数据进行去重计数,并且可以接受微小误差时,HyperLogLog 就是不二之选。
+
### 使用 HyperLogLog 统计页面 UV 怎么做?
使用 HyperLogLog 统计页面 UV 主要需要用到下面这两个命令:
@@ -513,19 +560,31 @@ PFADD PAGE_1:UV USER1 USER2 ...... USERn
PFCOUNT PAGE_1:UV
```
-## Redis 持久化机制(重要)
+### 如果我想判断一个元素是否不在海量元素集合中,用什么数据类型?
+
+这是布隆过滤器的经典应用场景。布隆过滤器可以告诉你一个元素一定不存在或者可能存在,它也有极高的空间效率和一定的误判率,但绝不会漏报。也就是说,布隆过滤器说某个元素存在,小概率会误判。布隆过滤器说某个元素不在,那么这个元素一定不在。
+
+Bloom Filter 的简单原理图如下:
+
+
+
+当字符串存储要加入到布隆过滤器中时,该字符串首先由多个哈希函数生成不同的哈希值,然后将对应的位数组的下标设置为 1(当位数组初始化时,所有位置均为 0)。当第二次存储相同字符串时,因为先前的对应位置已设置为 1,所以很容易知道此值已经存在(去重非常方便)。
-Redis 持久化机制(RDB 持久化、AOF 持久化、RDB 和 AOF 的混合持久化) 相关的问题比较多,也比较重要,于是我单独抽了一篇文章来总结 Redis 持久化机制相关的知识点和问题:[Redis 持久化机制详解](https://javaguide.cn/database/redis/redis-persistence.html) 。
+如果我们需要判断某个字符串是否在布隆过滤器中时,只需要对给定字符串再次进行相同的哈希计算,得到值之后判断位数组中的每个元素是否都为 1,如果值都为 1,那么说明这个值在布隆过滤器中,如果存在一个值不为 1,说明该元素不在布隆过滤器中。
-## Redis 线程模型(重要)
+## ⭐️Redis 持久化机制(重要)
-对于读写命令来说,Redis 一直是单线程模型。不过,在 Redis 4.0 版本之后引入了多线程来执行一些大键值对的异步删除操作, Redis 6.0 版本之后引入了多线程来处理网络请求(提高网络 IO 读写性能)。
+Redis 持久化机制(RDB 持久化、AOF 持久化、RDB 和 AOF 的混合持久化)相关的问题比较多,也比较重要,于是我单独抽了一篇文章来总结 Redis 持久化机制相关的知识点和问题:[Redis 持久化机制详解](https://javaguide.cn/database/redis/redis-persistence.html)。
+
+## ⭐️Redis 线程模型(重要)
+
+对于读写命令来说,Redis 一直是单线程模型。不过,在 Redis 4.0 版本之后引入了多线程来执行一些大键值对的异步删除操作,Redis 6.0 版本之后引入了多线程来处理网络请求(提高网络 IO 读写性能)。
### Redis 单线程模型了解吗?
-**Redis 基于 Reactor 模式设计开发了一套高效的事件处理模型** (Netty 的线程模型也基于 Reactor 模式,Reactor 模式不愧是高性能 IO 的基石),这套事件处理模型对应的是 Redis 中的文件事件处理器(file event handler)。由于文件事件处理器(file event handler)是单线程方式运行的,所以我们一般都说 Redis 是单线程模型。
+**Redis 基于 Reactor 模式设计开发了一套高效的事件处理模型**(Netty 的线程模型也基于 Reactor 模式,Reactor 模式不愧是高性能 IO 的基石),这套事件处理模型对应的是 Redis 中的文件事件处理器(file event handler)。由于文件事件处理器(file event handler)是单线程方式运行的,所以我们一般都说 Redis 是单线程模型。
-《Redis 设计与实现》有一段话是如是介绍文件事件处理器的,我觉得写得挺不错。
+《Redis 设计与实现》有一段话是这样介绍文件事件处理器的,我觉得写得挺不错。
> Redis 基于 Reactor 模式开发了自己的网络事件处理器:这个处理器被称为文件事件处理器(file event handler)。
>
@@ -549,8 +608,6 @@ Redis 通过 **IO 多路复用程序** 来监听来自客户端的大量连接

-相关阅读:[Redis 事件机制详解](http://remcarpediem.net/article/1aa2da89/) 。
-
### Redis6.0 之前为什么不使用多线程?
虽然说 Redis 是单线程模型,但实际上,**Redis 在 4.0 之后的版本中就已经加入了对多线程的支持。**
@@ -570,10 +627,10 @@ Redis 通过 **IO 多路复用程序** 来监听来自客户端的大量连接
**那 Redis6.0 之前为什么不使用多线程?** 我觉得主要原因有 3 点:
- 单线程编程容易并且更容易维护;
-- Redis 的性能瓶颈不在 CPU ,主要在内存和网络;
+- Redis 的性能瓶颈不在 CPU,主要在内存和网络;
- 多线程就会存在死锁、线程上下文切换等问题,甚至会影响性能。
-相关阅读:[为什么 Redis 选择单线程模型?](https://draveness.me/whys-the-design-redis-single-thread/) 。
+相关阅读:[为什么 Redis 选择单线程模型?](https://draveness.me/whys-the-design-redis-single-thread/)。
### Redis6.0 之后为何引入了多线程?
@@ -592,13 +649,13 @@ io-threads 4 #设置1的话只会开启主线程,官网建议4核的机器建
- io-threads 的个数一旦设置,不能通过 config 动态设置。
- 当设置 ssl 后,io-threads 将不工作。
-开启多线程后,默认只会使用多线程进行 IO 写入 writes,即发送数据给客户端,如果需要开启多线程 IO 读取 reads,同样需要修改 redis 配置文件 `redis.conf` :
+开启多线程后,默认只会使用多线程进行 IO 写入 writes,即发送数据给客户端,如果需要开启多线程 IO 读取 reads,同样需要修改 redis 配置文件 `redis.conf`:
```bash
io-threads-do-reads yes
```
-但是官网描述开启多线程读并不能有太大提升,因此一般情况下并不建议开启
+但是官网描述开启多线程读并不能有太大提升,因此一般情况下并不建议开启。
相关阅读:
@@ -610,8 +667,8 @@ io-threads-do-reads yes
我们虽然经常说 Redis 是单线程模型(主要逻辑是单线程完成的),但实际还有一些后台线程用于执行一些比较耗时的操作:
- 通过 `bio_close_file` 后台线程来释放 AOF / RDB 等过程中产生的临时文件资源。
-- 通过 `bio_aof_fsync` 后台线程调用 `fsync` 函数将系统内核缓冲区还未同步到到磁盘的数据强制刷到磁盘( AOF 文件)。
-- 通过 `bio_lazy_free`后台线程释放大对象(已删除)占用的内存空间.
+- 通过 `bio_aof_fsync` 后台线程调用 `fsync` 函数将系统内核缓冲区还未同步到到磁盘的数据强制刷到磁盘(AOF 文件)。
+- 通过 `bio_lazy_free` 后台线程释放大对象(已删除)占用的内存空间.
在`bio.h` 文件中有定义(Redis 6.0 版本,源码地址:):
@@ -638,7 +695,7 @@ void bioKillThreads(void);
关于 Redis 后台线程的详细介绍可以查看 [Redis 6.0 后台线程有哪些?](https://juejin.cn/post/7102780434739626014) 这篇就文章。
-## Redis 内存管理
+## ⭐️Redis 内存管理
### Redis 给缓存数据设置过期时间有什么用?
@@ -657,7 +714,7 @@ OK
(integer) 56
```
-注意 ⚠️:Redis 中除了字符串类型有自己独有设置过期时间的命令 `setex` 外,其他方法都需要依靠 `expire` 命令来设置过期时间 。另外, `persist` 命令可以移除一个键的过期时间。
+注意 ⚠️:Redis 中除了字符串类型有自己独有设置过期时间的命令 `setex` 外,其他方法都需要依靠 `expire` 命令来设置过期时间 。另外,`persist` 命令可以移除一个键的过期时间。
**过期时间除了有助于缓解内存的消耗,还有什么其他用么?**
@@ -667,7 +724,7 @@ OK
### Redis 是如何判断数据是否过期的呢?
-Redis 通过一个叫做过期字典(可以看作是 hash 表)来保存数据过期的时间。过期字典的键指向 Redis 数据库中的某个 key(键),过期字典的值是一个 long long 类型的整数,这个整数保存了 key 所指向的数据库键的过期时间(毫秒精度的 UNIX 时间戳)。
+Redis 通过一个叫做过期字典(可以看作是 hash 表)来保存数据过期的时间。过期字典的键指向 Redis 数据库中的某个 key(键),过期字典的值是一个 long long 类型的整数,这个整数保存了 key 所指向的数据库键的过期时间(毫秒精度的 UNIX 时间戳)。

@@ -696,7 +753,7 @@ typedef struct redisDb {
3. **延迟队列**:把设置过期时间的 key 放到一个延迟队列里,到期之后就删除 key。这种方式可以保证每个过期 key 都能被删除,但维护延迟队列太麻烦,队列本身也要占用资源。
4. **定时删除**:每个设置了过期时间的 key 都会在设置的时间到达时立即被删除。这种方法可以确保内存中不会有过期的键,但是它对 CPU 的压力最大,因为它需要为每个键都设置一个定时器。
-**Redis 采用的那种删除策略呢?**
+**Redis 采用的是那种删除策略呢?**
Redis 采用的是 **定期删除+惰性/懒汉式删除** 结合的策略,这也是大部分缓存框架的选择。定期删除对内存更加友好,惰性删除对 CPU 更加友好。两者各有千秋,结合起来使用既能兼顾 CPU 友好,又能兼顾内存友好。
@@ -711,7 +768,7 @@ Redis 的定期删除过程是随机的(周期性地随机从设置了过期
Redis 7.2 版本的执行时间阈值是 **25ms**,过期 key 比例设定值是 **10%**。
-```java
+```c
#define ACTIVE_EXPIRE_CYCLE_FAST_DURATION 1000 /* Microseconds. */
#define ACTIVE_EXPIRE_CYCLE_SLOW_TIME_PERC 25 /* Max % of CPU to use. */
#define ACTIVE_EXPIRE_CYCLE_ACCEPTABLE_STALE 10 /* % of stale keys after which
@@ -720,7 +777,7 @@ Redis 7.2 版本的执行时间阈值是 **25ms**,过期 key 比例设定值
**每次随机抽查数量是多少?**
-`expire.c`中定义了每次随机抽查的数量,Redis 7.2 版本为 20 ,也就是说每次会随机选择 20 个设置了过期时间的 key 判断是否过期。
+`expire.c` 中定义了每次随机抽查的数量,Redis 7.2 版本为 20,也就是说每次会随机选择 20 个设置了过期时间的 key 判断是否过期。
```c
#define ACTIVE_EXPIRE_CYCLE_KEYS_PER_LOOP 20 /* Keys for each DB loop. */
@@ -730,7 +787,7 @@ Redis 7.2 版本的执行时间阈值是 **25ms**,过期 key 比例设定值
在 Redis 中,定期删除的频率是由 **hz** 参数控制的。hz 默认为 10,代表每秒执行 10 次,也就是每秒钟进行 10 次尝试来查找并删除过期的 key。
-hz 的取值范围为 1~500。增大 hz 参数的值会提升定期删除的频率。如果你想要更频繁地执行定期删除任务,可以适当增加 hz 的值,但这会加 CPU 的使用率。根据 Redis 官方建议,hz 的值不建议超过 100,对于大部分用户使用默认的 10 就足够了。
+hz 的取值范围为 1~500。增大 hz 参数的值会提升定期删除的频率。如果你想要更频繁地执行定期删除任务,可以适当增加 hz 的值,但这会增加 CPU 的使用率。根据 Redis 官方建议,hz 的值不建议超过 100,对于大部分用户使用默认的 10 就足够了。
下面是 hz 参数的官方注释,我翻译了其中的重要信息(Redis 7.2 版本)。
@@ -738,7 +795,7 @@ hz 的取值范围为 1~500。增大 hz 参数的值会提升定期删除的频
类似的参数还有一个 **dynamic-hz**,这个参数开启之后 Redis 就会在 hz 的基础上动态计算一个值。Redis 提供并默认启用了使用自适应 hz 值的能力,
-这两个参数都在 Redis 配置文件 `redis.conf`中:
+这两个参数都在 Redis 配置文件 `redis.conf` 中:
```properties
# 默认为 10
@@ -758,22 +815,27 @@ dynamic-hz yes
因为不太好办到,或者说这种删除方式的成本太高了。假如我们使用延迟队列作为删除策略,这样存在下面这些问题:
1. 队列本身的开销可能很大:key 多的情况下,一个延迟队列可能无法容纳。
-2. 维护延迟队列太麻烦:修改 key 的过期时间就需要调整期在延迟队列中的位置,并且,还需要引入并发控制。
+2. 维护延迟队列太麻烦:修改 key 的过期时间就需要调整其在延迟队列中的位置,并且还需要引入并发控制。
### 大量 key 集中过期怎么办?
-如果存在大量 key 集中过期的问题,可能会使 Redis 的请求延迟变高。可以采用下面的可选方案来应对:
+当 Redis 中存在大量 key 在同一时间点集中过期时,可能会导致以下问题:
+
+- **请求延迟增加**:Redis 在处理过期 key 时需要消耗 CPU 资源,如果过期 key 数量庞大,会导致 Redis 实例的 CPU 占用率升高,进而影响其他请求的处理速度,造成延迟增加。
+- **内存占用过高**:过期的 key 虽然已经失效,但在 Redis 真正删除它们之前,仍然会占用内存空间。如果过期 key 没有及时清理,可能会导致内存占用过高,甚至引发内存溢出。
+
+为了避免这些问题,可以采取以下方案:
-1. 尽量避免 key 集中过期,在设置键的过期时间时尽量随机一点。
-2. 对过期的 key 开启 lazyfree 机制(修改 `redis.conf` 中的 `lazyfree-lazy-expire`参数即可),这样会在后台异步删除过期的 key,不会阻塞主线程的运行。
+1. **尽量避免 key 集中过期**:在设置键的过期时间时尽量随机一点。
+2. **开启 lazy free 机制**:修改 `redis.conf` 配置文件,将 `lazyfree-lazy-expire` 参数设置为 `yes`,即可开启 lazy free 机制。开启 lazy free 机制后,Redis 会在后台异步删除过期的 key,不会阻塞主线程的运行,从而降低对 Redis 性能的影响。
### Redis 内存淘汰策略了解么?
> 相关问题:MySQL 里有 2000w 数据,Redis 中只存 20w 的数据,如何保证 Redis 中的数据都是热点数据?
-Redis 的内存淘汰策略只有在运行内存达到了配置的最大内存阈值时才会触发,这个阈值是通过`redis.conf`的`maxmemory`参数来定义的。64 位操作系统下,`maxmemory` 默认为 0 ,表示不限制内存大小。32 位操作系统下,默认的最大内存值是 3GB。
+Redis 的内存淘汰策略只有在运行内存达到了配置的最大内存阈值时才会触发,这个阈值是通过 `redis.conf` 的 `maxmemory` 参数来定义的。64 位操作系统下,`maxmemory` 默认为 0,表示不限制内存大小。32 位操作系统下,默认的最大内存值是 3GB。
-你可以使用命令 `config get maxmemory` 来查看 `maxmemory`的值。
+你可以使用命令 `config get maxmemory` 来查看 `maxmemory` 的值。
```bash
> config get maxmemory
@@ -797,7 +859,7 @@ Redis 提供了 6 种内存淘汰策略:
`allkeys-xxx` 表示从所有的键值中淘汰数据,而 `volatile-xxx` 表示从设置了过期时间的键值中淘汰数据。
-`config.c`中定义了内存淘汰策略的枚举数组:
+`config.c` 中定义了内存淘汰策略的枚举数组:
```c
configEnum maxmemory_policy_enum[] = {
@@ -821,7 +883,7 @@ maxmemory-policy
noeviction
```
-可以通过`config set maxmemory-policy 内存淘汰策略` 命令修改内存淘汰策略,立即生效,但这种方式重启 Redis 之后就失效了。修改 `redis.conf` 中的 `maxmemory-policy` 参数不会因为重启而失效,不过,需要重启之后修改才能生效。
+可以通过 `config set maxmemory-policy 内存淘汰策略` 命令修改内存淘汰策略,立即生效,但这种方式重启 Redis 之后就失效了。修改 `redis.conf` 中的 `maxmemory-policy` 参数不会因为重启而失效,不过,需要重启之后修改才能生效。
```properties
maxmemory-policy noeviction
diff --git a/docs/database/redis/redis-questions-02.md b/docs/database/redis/redis-questions-02.md
index f6b5fe270d1..7e68719b9c8 100644
--- a/docs/database/redis/redis-questions-02.md
+++ b/docs/database/redis/redis-questions-02.md
@@ -1,15 +1,13 @@
---
title: Redis常见面试题总结(下)
+description: 最新Redis面试题总结(下):深度剖析Redis事务原理、性能优化(pipeline/Lua/bigkey/hotkey)、缓存穿透/击穿/雪崩解决方案、慢查询与内存碎片、Redis Sentinel与Cluster集群详解。助你轻松应对后端技术面试!
category: 数据库
tag:
- Redis
head:
- - meta
- name: keywords
- content: Redis基础,Redis常见数据结构,Redis线程模型,Redis内存管理,Redis事务,Redis性能优化
- - - meta
- - name: description
- content: 一篇文章总结Redis常见的知识点和面试题,涵盖Redis基础、Redis常见数据结构、Redis线程模型、Redis内存管理、Redis事务、Redis性能优化等内容。
+ content: Redis面试题,Redis事务,Redis性能优化,Redis缓存穿透,Redis缓存击穿,Redis缓存雪崩,Redis bigkey,Redis hotkey,Redis慢查询,Redis内存碎片,Redis集群,Redis Sentinel,Redis Cluster,Redis pipeline,Redis Lua脚本
---
@@ -28,7 +26,7 @@ Redis 事务实际开发中使用的非常少,功能比较鸡肋,不要将
### 如何使用 Redis 事务?
-Redis 可以通过 **`MULTI`,`EXEC`,`DISCARD` 和 `WATCH`** 等命令来实现事务(Transaction)功能。
+Redis 可以通过 **`MULTI`、`EXEC`、`DISCARD` 和 `WATCH`** 等命令来实现事务(Transaction)功能。
```bash
> MULTI
@@ -47,8 +45,8 @@ QUEUED
这个过程是这样的:
1. 开始事务(`MULTI`);
-2. 命令入队(批量操作 Redis 的命令,先进先出(FIFO)的顺序执行);
-3. 执行事务(`EXEC`)。
+2. 命令入队(批量操作 Redis 的命令,先进先出(FIFO)的顺序执行);
+3. 执行事务(`EXEC`)。
你也可以通过 [`DISCARD`](https://redis.io/commands/discard) 命令取消一个事务,它会清空事务队列中保存的所有命令。
@@ -138,10 +136,10 @@ Redis 官网相关介绍 [https://redis.io/topics/transactions](https://redis.io
Redis 的事务和我们平时理解的关系型数据库的事务不同。我们知道事务具有四大特性:**1. 原子性**,**2. 隔离性**,**3. 持久性**,**4. 一致性**。
-1. **原子性(Atomicity):** 事务是最小的执行单位,不允许分割。事务的原子性确保动作要么全部完成,要么完全不起作用;
-2. **隔离性(Isolation):** 并发访问数据库时,一个用户的事务不被其他事务所干扰,各并发事务之间数据库是独立的;
-3. **持久性(Durability):** 一个事务被提交之后。它对数据库中数据的改变是持久的,即使数据库发生故障也不应该对其有任何影响。
-4. **一致性(Consistency):** 执行事务前后,数据保持一致,多个事务对同一个数据读取的结果是相同的;
+1. **原子性(Atomicity)**:事务是最小的执行单位,不允许分割。事务的原子性确保动作要么全部完成,要么完全不起作用;
+2. **隔离性(Isolation)**:并发访问数据库时,一个用户的事务不被其他事务所干扰,各并发事务之间数据库是独立的;
+3. **持久性(Durability)**:一个事务被提交之后。它对数据库中数据的改变是持久的,即使数据库发生故障也不应该对其有任何影响;
+4. **一致性(Consistency)**:执行事务前后,数据保持一致,多个事务对同一个数据读取的结果是相同的。
Redis 事务在运行错误的情况下,除了执行过程中出现错误的命令外,其他命令都能正常执行。并且,Redis 事务是不支持回滚(roll back)操作的。因此,Redis 事务其实是不满足原子性的。
@@ -149,28 +147,28 @@ Redis 官网也解释了自己为啥不支持回滚。简单来说就是 Redis

-**相关 issue** :
+**相关 issue**:
-- [issue#452: 关于 Redis 事务不满足原子性的问题](https://github.com/Snailclimb/JavaGuide/issues/452) 。
-- [Issue#491:关于 Redis 没有事务回滚?](https://github.com/Snailclimb/JavaGuide/issues/491)
+- [issue#452: 关于 Redis 事务不满足原子性的问题](https://github.com/Snailclimb/JavaGuide/issues/452)。
+- [Issue#491:关于 Redis 没有事务回滚?](https://github.com/Snailclimb/JavaGuide/issues/491)。
### Redis 事务支持持久性吗?
-Redis 不同于 Memcached 的很重要一点就是,Redis 支持持久化,而且支持 3 种持久化方式:
+Redis 不同于 Memcached 的很重要一点就是,Redis 支持持久化,而且支持 3 种持久化方式:
-- 快照(snapshotting,RDB)
-- 只追加文件(append-only file, AOF)
-- RDB 和 AOF 的混合持久化(Redis 4.0 新增)
+- 快照(snapshotting,RDB);
+- 只追加文件(append-only file,AOF);
+- RDB 和 AOF 的混合持久化(Redis 4.0 新增)。
-与 RDB 持久化相比,AOF 持久化的实时性更好。在 Redis 的配置文件中存在三种不同的 AOF 持久化方式( `fsync`策略),它们分别是:
+与 RDB 持久化相比,AOF 持久化的实时性更好。在 Redis 的配置文件中存在三种不同的 AOF 持久化方式(`fsync` 策略),它们分别是:
```bash
-appendfsync always #每次有数据修改发生时都会调用fsync函数同步AOF文件,fsync完成后线程返回,这样会严重降低Redis的速度
+appendfsync always #每次有数据修改发生时,都会调用fsync函数同步AOF文件,fsync完成后线程返回,这样会严重降低Redis的速度
appendfsync everysec #每秒钟调用fsync函数同步一次AOF文件
appendfsync no #让操作系统决定何时进行同步,一般为30秒一次
```
-AOF 持久化的`fsync`策略为 no、everysec 时都会存在数据丢失的情况 。always 下可以基本是可以满足持久性要求的,但性能太差,实际开发过程中不会使用。
+AOF 持久化的 `fsync` 策略为 no、everysec 时都会存在数据丢失的情况。always 下可以基本是可以满足持久性要求的,但性能太差,实际开发过程中不会使用。
因此,Redis 事务的持久性也是没办法保证的。
@@ -180,44 +178,44 @@ Redis 从 2.6 版本开始支持执行 Lua 脚本,它的功能和事务非常
一段 Lua 脚本可以视作一条命令执行,一段 Lua 脚本执行过程中不会有其他脚本或 Redis 命令同时执行,保证了操作不会被其他指令插入或打扰。
-不过,如果 Lua 脚本运行时出错并中途结束,出错之后的命令是不会被执行的。并且,出错之前执行的命令是无法被撤销的,无法实现类似关系型数据库执行失败可以回滚的那种原子性效果。因此, **严格来说的话,通过 Lua 脚本来批量执行 Redis 命令实际也是不完全满足原子性的。**
+不过,如果 Lua 脚本运行时出错并中途结束,出错之后的命令是不会被执行的。并且,出错之前执行的命令是无法被撤销的,无法实现类似关系型数据库执行失败可以回滚的那种原子性效果。因此,**严格来说的话,通过 Lua 脚本来批量执行 Redis 命令实际也是不完全满足原子性的。**
如果想要让 Lua 脚本中的命令全部执行,必须保证语句语法和命令都是对的。
-另外,Redis 7.0 新增了 [Redis functions](https://redis.io/docs/manual/programmability/functions-intro/) 特性,你可以将 Redis functions 看作是比 Lua 更强大的脚本。
+另外,Redis 7.0 新增了 [Redis functions](https://redis.io/docs/latest/develop/programmability/functions-intro/) 特性,你可以将 Redis functions 看作是比 Lua 更强大的脚本。
-## Redis 性能优化(重要)
+## ⭐️Redis 性能优化(重要)
除了下面介绍的内容之外,再推荐两篇不错的文章:
-- [你的 Redis 真的变慢了吗?性能优化如何做 - 阿里开发者](https://mp.weixin.qq.com/s/nNEuYw0NlYGhuKKKKoWfcQ)
-- [Redis 常见阻塞原因总结 - JavaGuide](https://javaguide.cn/database/redis/redis-common-blocking-problems-summary.html)
+- [你的 Redis 真的变慢了吗?性能优化如何做 - 阿里开发者](https://mp.weixin.qq.com/s/nNEuYw0NlYGhuKKKKoWfcQ)。
+- [Redis 常见阻塞原因总结 - JavaGuide](https://javaguide.cn/database/redis/redis-common-blocking-problems-summary.html)。
### 使用批量操作减少网络传输
一个 Redis 命令的执行可以简化为以下 4 步:
-1. 发送命令
-2. 命令排队
-3. 命令执行
-4. 返回结果
+1. 发送命令;
+2. 命令排队;
+3. 命令执行;
+4. 返回结果。
-其中,第 1 步和第 4 步耗费时间之和称为 **Round Trip Time (RTT,往返时间)** ,也就是数据在网络上传输的时间。
+其中,第 1 步和第 4 步耗费时间之和称为 **Round Trip Time(RTT,往返时间)**,也就是数据在网络上传输的时间。
使用批量操作可以减少网络传输次数,进而有效减小网络开销,大幅减少 RTT。
-另外,除了能减少 RTT 之外,发送一次命令的 socket I/O 成本也比较高(涉及上下文切换,存在`read()`和`write()`系统调用),批量操作还可以减少 socket I/O 成本。这个在官方对 pipeline 的介绍中有提到: 。
+另外,除了能减少 RTT 之外,发送一次命令的 socket I/O 成本也比较高(涉及上下文切换,存在 `read()` 和 `write()` 系统调用),批量操作还可以减少 socket I/O 成本。这个在官方对 pipeline 的介绍中有提到: 。
#### 原生批量操作命令
Redis 中有一些原生支持批量操作的命令,比如:
-- `MGET`(获取一个或多个指定 key 的值)、`MSET`(设置一个或多个指定 key 的值)、
-- `HMGET`(获取指定哈希表中一个或者多个指定字段的值)、`HMSET`(同时将一个或多个 field-value 对设置到指定哈希表中)、
+- `MGET`(获取一个或多个指定 key 的值)、`MSET`(设置一个或多个指定 key 的值)、
+- `HMGET`(获取指定哈希表中一个或者多个指定字段的值)、`HMSET`(同时将一个或多个 field-value 对设置到指定哈希表中)、
- `SADD`(向指定集合添加一个或多个元素)
- ……
-不过,在 Redis 官方提供的分片集群解决方案 Redis Cluster 下,使用这些原生批量操作命令可能会存在一些小问题需要解决。就比如说 `MGET` 无法保证所有的 key 都在同一个 **hash slot**(哈希槽)上,`MGET`可能还是需要多次网络传输,原子操作也无法保证了。不过,相较于非批量操作,还是可以节省不少网络传输次数。
+不过,在 Redis 官方提供的分片集群解决方案 Redis Cluster 下,使用这些原生批量操作命令可能会存在一些小问题需要解决。就比如说 `MGET` 无法保证所有的 key 都在同一个 **hash slot(哈希槽)** 上,`MGET`可能还是需要多次网络传输,原子操作也无法保证了。不过,相较于非批量操作,还是可以节省不少网络传输次数。
整个步骤的简化版如下(通常由 Redis 客户端实现,无需我们自己再手动实现):
@@ -227,15 +225,15 @@ Redis 中有一些原生支持批量操作的命令,比如:
如果想要解决这个多次网络传输的问题,比较常用的办法是自己维护 key 与 slot 的关系。不过这样不太灵活,虽然带来了性能提升,但同样让系统复杂性提升。
-> Redis Cluster 并没有使用一致性哈希,采用的是 **哈希槽分区** ,每一个键值对都属于一个 **hash slot**(哈希槽) 。当客户端发送命令请求的时候,需要先根据 key 通过上面的计算公示找到的对应的哈希槽,然后再查询哈希槽和节点的映射关系,即可找到目标 Redis 节点。
+> Redis Cluster 并没有使用一致性哈希,采用的是 **哈希槽分区**,每一个键值对都属于一个 **hash slot(哈希槽)**。当客户端发送命令请求的时候,需要先根据 key 通过上面的计算公式找到的对应的哈希槽,然后再查询哈希槽和节点的映射关系,即可找到目标 Redis 节点。
>
> 我在 [Redis 集群详解(付费)](https://javaguide.cn/database/redis/redis-cluster.html) 这篇文章中详细介绍了 Redis Cluster 这部分的内容,感兴趣地可以看看。
#### pipeline
-对于不支持批量操作的命令,我们可以利用 **pipeline(流水线)** 将一批 Redis 命令封装成一组,这些 Redis 命令会被一次性提交到 Redis 服务器,只需要一次网络传输。不过,需要注意控制一次批量操作的 **元素个数**(例如 500 以内,实际也和元素字节数有关),避免网络传输的数据量过大。
+对于不支持批量操作的命令,我们可以利用 **pipeline(流水线)** 将一批 Redis 命令封装成一组,这些 Redis 命令会被一次性提交到 Redis 服务器,只需要一次网络传输。不过,需要注意控制一次批量操作的 **元素个数**(例如 500 以内,实际也和元素字节数有关),避免网络传输的数据量过大。
-与`MGET`、`MSET`等原生批量操作命令一样,pipeline 同样在 Redis Cluster 上使用会存在一些小问题。原因类似,无法保证所有的 key 都在同一个 **hash slot**(哈希槽)上。如果想要使用的话,客户端需要自己维护 key 与 slot 的关系。
+与 `MGET`、`MSET` 等原生批量操作命令一样,pipeline 同样在 Redis Cluster 上使用会存在一些小问题。原因类似,无法保证所有的 key 都在同一个 **hash slot(哈希槽)** 上。如果想要使用的话,客户端需要自己维护 key 与 slot 的关系。
原生批量操作命令和 pipeline 的是有区别的,使用的时候需要注意:
@@ -252,18 +250,18 @@ Redis 中有一些原生支持批量操作的命令,比如:

-另外,pipeline 不适用于执行顺序有依赖关系的一批命令。就比如说,你需要将前一个命令的结果给后续的命令使用,pipeline 就没办法满足你的需求了。对于这种需求,我们可以使用 **Lua 脚本** 。
+另外,pipeline 不适用于执行顺序有依赖关系的一批命令。就比如说,你需要将前一个命令的结果给后续的命令使用,pipeline 就没办法满足你的需求了。对于这种需求,我们可以使用 **Lua 脚本**。
#### Lua 脚本
-Lua 脚本同样支持批量操作多条命令。一段 Lua 脚本可以视作一条命令执行,可以看作是 **原子操作** 。也就是说,一段 Lua 脚本执行过程中不会有其他脚本或 Redis 命令同时执行,保证了操作不会被其他指令插入或打扰,这是 pipeline 所不具备的。
+Lua 脚本同样支持批量操作多条命令。一段 Lua 脚本可以视作一条命令执行,可以看作是 **原子操作**。也就是说,一段 Lua 脚本执行过程中不会有其他脚本或 Redis 命令同时执行,保证了操作不会被其他指令插入或打扰,这是 pipeline 所不具备的。
并且,Lua 脚本中支持一些简单的逻辑处理比如使用命令读取值并在 Lua 脚本中进行处理,这同样是 pipeline 所不具备的。
不过, Lua 脚本依然存在下面这些缺陷:
- 如果 Lua 脚本运行时出错并中途结束,之后的操作不会进行,但是之前已经发生的写操作不会撤销,所以即使使用了 Lua 脚本,也不能实现类似数据库回滚的原子性。
-- Redis Cluster 下 Lua 脚本的原子操作也无法保证了,原因同样是无法保证所有的 key 都在同一个 **hash slot**(哈希槽)上。
+- Redis Cluster 下 Lua 脚本的原子操作也无法保证了,原因同样是无法保证所有的 key 都在同一个 **hash slot(哈希槽)** 上。
### 大量 key 集中过期问题
@@ -274,7 +272,7 @@ Lua 脚本同样支持批量操作多条命令。一段 Lua 脚本可以视作
**如何解决呢?** 下面是两种常见的方法:
1. 给 key 设置随机过期时间。
-2. 开启 lazy-free(惰性删除/延迟释放) 。lazy-free 特性是 Redis 4.0 开始引入的,指的是让 Redis 采用异步方式延迟释放 key 使用的内存,将该操作交给单独的子线程处理,避免阻塞主线程。
+2. 开启 lazy-free(惰性删除/延迟释放)。lazy-free 特性是 Redis 4.0 开始引入的,指的是让 Redis 采用异步方式延迟释放 key 使用的内存,将该操作交给单独的子线程处理,避免阻塞主线程。
个人建议不管是否开启 lazy-free,我们都尽量给 key 设置随机过期时间。
@@ -299,7 +297,7 @@ bigkey 通常是由于下面这些原因产生的:
bigkey 除了会消耗更多的内存空间和带宽,还会对性能造成比较大的影响。
-在 [Redis 常见阻塞原因总结](./redis-common-blocking-problems-summary.md)这篇文章中我们提到:大 key 还会造成阻塞问题。具体来说,主要体现在下面三个方面:
+在 [Redis 常见阻塞原因总结](./redis-common-blocking-problems-summary.md) 这篇文章中我们提到:大 key 还会造成阻塞问题。具体来说,主要体现在下面三个方面:
1. 客户端超时阻塞:由于 Redis 执行命令是单线程处理,然后在操作大 key 时会比较耗时,那么就会阻塞 Redis,从客户端这一视角看,就是很久很久都没有响应。
2. 网络阻塞:每次获取大 key 产生的网络流量较大,如果一个 key 的大小是 1 MB,每秒访问量为 1000,那么每秒会产生 1000MB 的流量,这对于普通千兆网卡的服务器来说是灾难性的。
@@ -339,13 +337,13 @@ Biggest string found '"ballcat:oauth:refresh_auth:f6cdb384-9a9d-4f2f-af01-dc3f28
0 zsets with 0 members (00.00% of keys, avg size 0.00
```
-从这个命令的运行结果,我们可以看出:这个命令会扫描(Scan) Redis 中的所有 key ,会对 Redis 的性能有一点影响。并且,这种方式只能找出每种数据结构 top 1 bigkey(占用内存最大的 String 数据类型,包含元素最多的复合数据类型)。然而,一个 key 的元素多并不代表占用内存也多,需要我们根据具体的业务情况来进一步判断。
+从这个命令的运行结果,我们可以看出:这个命令会扫描(Scan)Redis 中的所有 key,会对 Redis 的性能有一点影响。并且,这种方式只能找出每种数据结构 top 1 bigkey(占用内存最大的 String 数据类型,包含元素最多的复合数据类型)。然而,一个 key 的元素多并不代表占用内存也多,需要我们根据具体的业务情况来进一步判断。
在线上执行该命令时,为了降低对 Redis 的影响,需要指定 `-i` 参数控制扫描的频率。`redis-cli -p 6379 --bigkeys -i 3` 表示扫描过程中每次扫描后休息的时间间隔为 3 秒。
**2、使用 Redis 自带的 SCAN 命令**
-`SCAN` 命令可以按照一定的模式和数量返回匹配的 key。获取了 key 之后,可以利用 `STRLEN`、`HLEN`、`LLEN`等命令返回其长度或成员数量。
+`SCAN` 命令可以按照一定的模式和数量返回匹配的 key。获取了 key 之后,可以利用 `STRLEN`、`HLEN`、`LLEN` 等命令返回其长度或成员数量。
| 数据结构 | 命令 | 复杂度 | 结果(对应 key) |
| ---------- | ------ | ------ | ------------------ |
@@ -363,14 +361,14 @@ Biggest string found '"ballcat:oauth:refresh_auth:f6cdb384-9a9d-4f2f-af01-dc3f28
网上有现成的代码/工具可以直接拿来使用:
-- [redis-rdb-tools](https://github.com/sripathikrishnan/redis-rdb-tools):Python 语言写的用来分析 Redis 的 RDB 快照文件用的工具
-- [rdb_bigkeys](https://github.com/weiyanwei412/rdb_bigkeys) : Go 语言写的用来分析 Redis 的 RDB 快照文件用的工具,性能更好。
+- [redis-rdb-tools](https://github.com/sripathikrishnan/redis-rdb-tools):Python 语言写的用来分析 Redis 的 RDB 快照文件用的工具。
+- [rdb_bigkeys](https://github.com/weiyanwei412/rdb_bigkeys):Go 语言写的用来分析 Redis 的 RDB 快照文件用的工具,性能更好。
**4、借助公有云的 Redis 分析服务。**
如果你用的是公有云的 Redis 服务的话,可以看看其是否提供了 key 分析功能(一般都提供了)。
-这里以阿里云 Redis 为例说明,它支持 bigkey 实时分析、发现,文档地址: 。
+这里以阿里云 Redis 为例说明,它支持 bigkey 实时分析、发现,文档地址:。

@@ -381,7 +379,7 @@ bigkey 的常见处理以及优化办法如下(这些方法可以配合起来
- **分割 bigkey**:将一个 bigkey 分割为多个小 key。例如,将一个含有上万字段数量的 Hash 按照一定策略(比如二次哈希)拆分为多个 Hash。
- **手动清理**:Redis 4.0+ 可以使用 `UNLINK` 命令来异步删除一个或多个指定的 key。Redis 4.0 以下可以考虑使用 `SCAN` 命令结合 `DEL` 命令来分批次删除。
- **采用合适的数据结构**:例如,文件二进制数据不使用 String 保存、使用 HyperLogLog 统计页面 UV、Bitmap 保存状态信息(0/1)。
-- **开启 lazy-free(惰性删除/延迟释放)** :lazy-free 特性是 Redis 4.0 开始引入的,指的是让 Redis 采用异步方式延迟释放 key 使用的内存,将该操作交给单独的子线程处理,避免阻塞主线程。
+- **开启 lazy-free(惰性删除/延迟释放)**:lazy-free 特性是 Redis 4.0 开始引入的,指的是让 Redis 采用异步方式延迟释放 key 使用的内存,将该操作交给单独的子线程处理,避免阻塞主线程。
### Redis hotkey(热 Key)
@@ -432,13 +430,13 @@ maxmemory-policy allkeys-lfu
需要注意的是,`hotkeys` 参数命令也会增加 Redis 实例的 CPU 和内存消耗(全局扫描),因此需要谨慎使用。
-**2、使用`MONITOR` 命令。**
+**2、使用 `MONITOR` 命令。**
`MONITOR` 命令是 Redis 提供的一种实时查看 Redis 的所有操作的方式,可以用于临时监控 Redis 实例的操作情况,包括读写、删除等操作。
由于该命令对 Redis 性能的影响比较大,因此禁止长时间开启 `MONITOR`(生产环境中建议谨慎使用该命令)。
-```java
+```bash
# redis-cli
127.0.0.1:6379> MONITOR
OK
@@ -473,7 +471,7 @@ OK
如果你用的是公有云的 Redis 服务的话,可以看看其是否提供了 key 分析功能(一般都提供了)。
-这里以阿里云 Redis 为例说明,它支持 hotkey 实时分析、发现,文档地址: 。
+这里以阿里云 Redis 为例说明,它支持 hotkey 实时分析、发现,文档地址:。

@@ -497,16 +495,16 @@ hotkey 的常见处理以及优化办法如下(这些方法可以配合起来
我们知道一个 Redis 命令的执行可以简化为以下 4 步:
-1. 发送命令
-2. 命令排队
-3. 命令执行
-4. 返回结果
+1. 发送命令;
+2. 命令排队;
+3. 命令执行;
+4. 返回结果。
Redis 慢查询统计的是命令执行这一步骤的耗时,慢查询命令也就是那些命令执行时间较长的命令。
Redis 为什么会有慢查询命令呢?
-Redis 中的大部分命令都是 O(1)时间复杂度,但也有少部分 O(n) 时间复杂度的命令,例如:
+Redis 中的大部分命令都是 O(1) 时间复杂度,但也有少部分 O(n) 时间复杂度的命令,例如:
- `KEYS *`:会返回所有符合规则的 key。
- `HGETALL`:会返回一个 Hash 中所有的键值对。
@@ -517,23 +515,25 @@ Redis 中的大部分命令都是 O(1)时间复杂度,但也有少部分 O(n)
由于这些命令时间复杂度是 O(n),有时候也会全表扫描,随着 n 的增大,执行耗时也会越长。不过, 这些命令并不是一定不能使用,但是需要明确 N 的值。另外,有遍历的需求可以使用 `HSCAN`、`SSCAN`、`ZSCAN` 代替。
-除了这些 O(n)时间复杂度的命令可能会导致慢查询之外, 还有一些时间复杂度可能在 O(N) 以上的命令,例如:
+除了这些 O(n) 时间复杂度的命令可能会导致慢查询之外,还有一些时间复杂度可能在 O(N) 以上的命令,例如:
-- `ZRANGE`/`ZREVRANGE`:返回指定 Sorted Set 中指定排名范围内的所有元素。时间复杂度为 O(log(n)+m),n 为所有元素的数量, m 为返回的元素数量,当 m 和 n 相当大时,O(n) 的时间复杂度更小。
-- `ZREMRANGEBYRANK`/`ZREMRANGEBYSCORE`:移除 Sorted Set 中指定排名范围/指定 score 范围内的所有元素。时间复杂度为 O(log(n)+m),n 为所有元素的数量, m 被删除元素的数量,当 m 和 n 相当大时,O(n) 的时间复杂度更小。
+- `ZRANGE`/`ZREVRANGE`:返回指定 Sorted Set 中指定排名范围内的所有元素。时间复杂度为 O(log(n)+m),n 为所有元素的数量,m 为返回的元素数量,当 m 和 n 相当大时,O(n) 的时间复杂度更小。
+- `ZREMRANGEBYRANK`/`ZREMRANGEBYSCORE`:移除 Sorted Set 中指定排名范围/指定 score 范围内的所有元素。时间复杂度为 O(log(n)+m),n 为所有元素的数量,m 被删除元素的数量,当 m 和 n 相当大时,O(n) 的时间复杂度更小。
- ……
#### 如何找到慢查询命令?
+Redis 提供了一个内置的**慢查询日志 (Slow Log)** 功能,专门用来记录执行时间超过指定阈值的命令。这对于排查性能瓶颈、找出导致 Redis 阻塞的“慢”操作非常有帮助,原理和 MySQL 的慢查询日志类似。
+
在 `redis.conf` 文件中,我们可以使用 `slowlog-log-slower-than` 参数设置耗时命令的阈值,并使用 `slowlog-max-len` 参数设置耗时命令的最大记录条数。
-当 Redis 服务器检测到执行时间超过 `slowlog-log-slower-than`阈值的命令时,就会将该命令记录在慢查询日志(slow log) 中,这点和 MySQL 记录慢查询语句类似。当慢查询日志超过设定的最大记录条数之后,Redis 会把最早的执行命令依次舍弃。
+当 Redis 服务器检测到执行时间超过 `slowlog-log-slower-than` 阈值的命令时,就会将该命令记录在慢查询日志(slow log)中,这点和 MySQL 记录慢查询语句类似。当慢查询日志超过设定的最大记录条数之后,Redis 会把最早的执行命令依次舍弃。
-⚠️注意:由于慢查询日志会占用一定内存空间,如果设置最大记录条数过大,可能会导致内存占用过高的问题。
+⚠️ 注意:由于慢查询日志会占用一定内存空间,如果设置最大记录条数过大,可能会导致内存占用过高的问题。
-`slowlog-log-slower-than`和`slowlog-max-len`的默认配置如下(可以自行修改):
+`slowlog-log-slower-than` 和 `slowlog-max-len` 的默认配置如下(可以自行修改):
-```nginx
+```properties
# The following time is expressed in microseconds, so 1000000 is equivalent
# to one second. Note that a negative number disables the slow log, while
# a value of zero forces the logging of every command.
@@ -553,9 +553,9 @@ CONFIG SET slowlog-log-slower-than 10000
CONFIG SET slowlog-max-len 128
```
-获取慢查询日志的内容很简单,直接使用`SLOWLOG GET` 命令即可。
+获取慢查询日志的内容很简单,直接使用 `SLOWLOG GET` 命令即可。
-```java
+```bash
127.0.0.1:6379> SLOWLOG GET #慢日志查询
1) 1) (integer) 5
2) (integer) 1684326682
@@ -569,14 +569,14 @@ CONFIG SET slowlog-max-len 128
慢查询日志中的每个条目都由以下六个值组成:
-1. 唯一渐进的日志标识符。
-2. 处理记录命令的 Unix 时间戳。
-3. 执行所需的时间量,以微秒为单位。
-4. 组成命令参数的数组。
-5. 客户端 IP 地址和端口。
-6. 客户端名称。
+1. **唯一 ID**: 日志条目的唯一标识符。
+2. **时间戳 (Timestamp)**: 命令执行完成时的 Unix 时间戳。
+3. **耗时 (Duration)**: 命令执行所花费的时间,单位是**微秒**。
+4. **命令及参数 (Command)**: 执行的具体命令及其参数数组。
+5. **客户端信息 (Client IP:Port)**: 执行命令的客户端地址和端口。
+6. **客户端名称 (Client Name)**: 如果客户端设置了名称 (CLIENT SETNAME)。
-`SLOWLOG GET` 命令默认返回最近 10 条的的慢查询命令,你也自己可以指定返回的慢查询命令的数量 `SLOWLOG GET N`。
+`SLOWLOG GET` 命令默认返回最近 10 条的慢查询命令,你也自己可以指定返回的慢查询命令的数量 `SLOWLOG GET N`。
下面是其他比较常用的慢查询相关的命令:
@@ -593,18 +593,18 @@ OK
**相关问题**:
-1. 什么是内存碎片?为什么会有 Redis 内存碎片?
+1. 什么是内存碎片?为什么会有 Redis 内存碎片?
2. 如何清理 Redis 内存碎片?
**参考答案**:[Redis 内存碎片详解](https://javaguide.cn/database/redis/redis-memory-fragmentation.html)。
-## Redis 生产问题(重要)
+## ⭐️Redis 生产问题(重要)
### 缓存穿透
#### 什么是缓存穿透?
-缓存穿透说简单点就是大量请求的 key 是不合理的,**根本不存在于缓存中,也不存在于数据库中** 。这就导致这些请求直接到了数据库上,根本没有经过缓存这一层,对数据库造成了巨大的压力,可能直接就被这么多请求弄宕机了。
+缓存穿透说简单点就是大量请求的 key 是不合理的,**根本不存在于缓存中,也不存在于数据库中**。这就导致这些请求直接到了数据库上,根本没有经过缓存这一层,对数据库造成了巨大的压力,可能直接就被这么多请求弄宕机了。

@@ -616,9 +616,9 @@ OK
**1)缓存无效 key**
-如果缓存和数据库都查不到某个 key 的数据就写一个到 Redis 中去并设置过期时间,具体命令如下:`SET key value EX 10086` 。这种方式可以解决请求的 key 变化不频繁的情况,如果黑客恶意攻击,每次构建不同的请求 key,会导致 Redis 中缓存大量无效的 key 。很明显,这种方案并不能从根本上解决此问题。如果非要用这种方式来解决穿透问题的话,尽量将无效的 key 的过期时间设置短一点比如 1 分钟。
+如果缓存和数据库都查不到某个 key 的数据,就写一个到 Redis 中去并设置过期时间,具体命令如下:`SET key value EX 10086`。这种方式可以解决请求的 key 变化不频繁的情况,如果黑客恶意攻击,每次构建不同的请求 key,会导致 Redis 中缓存大量无效的 key。很明显,这种方案并不能从根本上解决此问题。如果非要用这种方式来解决穿透问题的话,尽量将无效的 key 的过期时间设置短一点,比如 1 分钟。
-另外,这里多说一嘴,一般情况下我们是这样设计 key 的:`表名:列名:主键名:主键值` 。
+另外,这里多说一嘴,一般情况下我们是这样设计 key 的:`表名:列名:主键名:主键值`。
如果用 Java 代码展示的话,差不多是下面这样的:
@@ -655,11 +655,11 @@ Bloom Filter 会使用一个较大的 bit 数组来保存所有的数据,数
具体是这样做的:把所有可能存在的请求的值都存放在布隆过滤器中,当用户请求过来,先判断用户发来的请求的值是否存在于布隆过滤器中。不存在的话,直接返回请求参数错误信息给客户端,存在的话才会走下面的流程。
-加入布隆过滤器之后的缓存处理流程图如下。
+加入布隆过滤器之后的缓存处理流程图如下:

-更多关于布隆过滤器的详细介绍可以看看我的这篇原创:[不了解布隆过滤器?一文给你整的明明白白!](https://javaguide.cn/cs-basics/data-structure/bloom-filter.html) ,强烈推荐。
+更多关于布隆过滤器的详细介绍可以看看我的这篇原创:[不了解布隆过滤器?一文给你整的明明白白!](https://javaguide.cn/cs-basics/data-structure/bloom-filter.html),强烈推荐。
**3)接口限流**
@@ -673,7 +673,7 @@ Bloom Filter 会使用一个较大的 bit 数组来保存所有的数据,数
#### 什么是缓存击穿?
-缓存击穿中,请求的 key 对应的是 **热点数据** ,该数据 **存在于数据库中,但不存在于缓存中(通常是因为缓存中的那份数据已经过期)** 。这就可能会导致瞬时大量的请求直接打到了数据库上,对数据库造成了巨大的压力,可能直接就被这么多请求弄宕机了。
+缓存击穿中,请求的 key 对应的是 **热点数据**,该数据 **存在于数据库中,但不存在于缓存中(通常是因为缓存中的那份数据已经过期)**。这就可能会导致瞬时大量的请求直接打到了数据库上,对数据库造成了巨大的压力,可能直接就被这么多请求弄宕机了。

@@ -703,19 +703,19 @@ Bloom Filter 会使用一个较大的 bit 数组来保存所有的数据,数

-举个例子:数据库中的大量数据在同一时间过期,这个时候突然有大量的请求需要访问这些过期的数据。这就导致大量的请求直接落到数据库上,对数据库造成了巨大的压力。
+举个例子:缓存中的大量数据在同一时间过期,这个时候突然有大量的请求需要访问这些过期的数据。这就导致大量的请求直接落到数据库上,对数据库造成了巨大的压力。
#### 有哪些解决办法?
-**针对 Redis 服务不可用的情况:**
+**针对 Redis 服务不可用的情况**:
1. **Redis 集群**:采用 Redis 集群,避免单机出现问题整个缓存服务都没办法使用。Redis Cluster 和 Redis Sentinel 是两种最常用的 Redis 集群实现方案,详细介绍可以参考:[Redis 集群详解(付费)](https://javaguide.cn/database/redis/redis-cluster.html)。
2. **多级缓存**:设置多级缓存,例如本地缓存+Redis 缓存的二级缓存组合,当 Redis 缓存出现问题时,还可以从本地缓存中获取到部分数据。
-**针对大量缓存同时失效的情况:**
+**针对大量缓存同时失效的情况**:
1. **设置随机失效时间**(可选):为缓存设置随机的失效时间,例如在固定过期时间的基础上加上一个随机值,这样可以避免大量缓存同时到期,从而减少缓存雪崩的风险。
-2. **提前预热**(推荐):针对热点数据提前预热,将其存入缓存中并设置合理的过期时间比如秒杀场景下的数据在秒杀结束之前不过期。
+2. **提前预热**(推荐):针对热点数据提前预热,将其存入缓存中并设置合理的过期时间,比如秒杀场景下的数据在秒杀结束之前不过期。
3. **持久缓存策略**(看情况):虽然一般不推荐设置缓存永不过期,但对于某些关键性和变化不频繁的数据,可以考虑这种策略。
#### 缓存预热如何实现?
@@ -727,44 +727,70 @@ Bloom Filter 会使用一个较大的 bit 数组来保存所有的数据,数
#### 缓存雪崩和缓存击穿有什么区别?
-缓存雪崩和缓存击穿比较像,但缓存雪崩导致的原因是缓存中的大量或者所有数据失效,缓存击穿导致的原因主要是某个热点数据不存在与缓存中(通常是因为缓存中的那份数据已经过期)。
+缓存雪崩和缓存击穿比较像,但缓存雪崩导致的原因是缓存中的大量或者所有数据失效,缓存击穿导致的原因主要是某个热点数据不存在于缓存中(通常是因为缓存中的那份数据已经过期)。
### 如何保证缓存和数据库数据的一致性?
-细说的话可以扯很多,但是我觉得其实没太大必要(小声 BB:很多解决方案我也没太弄明白)。我个人觉得引入缓存之后,如果为了短时间的不一致性问题,选择让系统设计变得更加复杂的话,完全没必要。
+缓存和数据库一致性是个挺常见的技术挑战。引入缓存主要是为了提升性能、减轻数据库压力,但确实会带来数据不一致的风险。绝对的一致性往往意味着更高的系统复杂度和性能开销,所以实践中我们通常会根据业务场景选择合适的策略,在性能和一致性之间找到一个平衡点。
+
+下面单独对 **Cache Aside Pattern(旁路缓存模式)** 来聊聊。这是非常常用的一种缓存读写策略,它的读写逻辑是这样的:
+
+- **读操作**:
+ 1. 先尝试从缓存读取数据。
+ 2. 如果缓存命中,直接返回数据。
+ 3. 如果缓存未命中,从数据库查询数据,将查到的数据放入缓存并返回数据。
+- **写操作**:
+ 1. 先更新数据库。
+ 2. 再直接删除缓存中对应的数据。
+
+图解如下:
-下面单独对 **Cache Aside Pattern(旁路缓存模式)** 来聊聊。
+
-Cache Aside Pattern 中遇到写请求是这样的:更新数据库,然后直接删除缓存 。
+
如果更新数据库成功,而删除缓存这一步失败的情况的话,简单说有两个解决方案:
-1. **缓存失效时间变短(不推荐,治标不治本)**:我们让缓存数据的过期时间变短,这样的话缓存就会从数据库中加载数据。另外,这种解决办法对于先操作缓存后操作数据库的场景不适用。
-2. **增加缓存更新重试机制(常用)**:如果缓存服务当前不可用导致缓存删除失败的话,我们就隔一段时间进行重试,重试次数可以自己定。不过,这里更适合引入消息队列实现异步重试,将删除缓存重试的消息投递到消息队列,然后由专门的消费者来重试,直到成功。虽然说多引入了一个消息队列,但其整体带来的收益还是要更高一些。
+1. **缓存失效时间(TTL - Time To Live)变短**(不推荐,治标不治本):我们让缓存数据的过期时间变短,这样的话缓存就会从数据库中加载数据。另外,这种解决办法对于先操作缓存后操作数据库的场景不适用。
+2. **增加缓存更新重试机制**(常用):如果缓存服务当前不可用导致缓存删除失败的话,我们就隔一段时间进行重试,重试次数可以自己定。不过,这里更适合引入消息队列实现异步重试,将删除缓存重试的消息投递到消息队列,然后由专门的消费者来重试,直到成功。虽然说多引入了一个消息队列,但其整体带来的收益还是要更高一些。
相关文章推荐:[缓存和数据库一致性问题,看这篇就够了 - 水滴与银弹](https://mp.weixin.qq.com/s?__biz=MzIyOTYxNDI5OA==&mid=2247487312&idx=1&sn=fa19566f5729d6598155b5c676eee62d&chksm=e8beb8e5dfc931f3e35655da9da0b61c79f2843101c130cf38996446975014f958a6481aacf1&scene=178&cur_album_id=1699766580538032128#rd)。
### 哪些情况可能会导致 Redis 阻塞?
-单独抽了一篇文章来总结可能会导致 Redis 阻塞的情况:[Redis 常见阻塞原因总结](https://javaguide.cn/database/redis/redis-common-blocking-problems-summary.html)。
+常见的导致 Redis 阻塞原因有:
+
+- `O(n)` 复杂度命令执行(如 `KEYS *`、`HGETALL`、`LRANGE`、`SMEMBERS` 等),随着数据量增大导致执行时间过长。
+- 执行 `SAVE` 命令生成 RDB 快照时同步阻塞主线程,而 `BGSAVE` 通过 `fork` 子进程避免阻塞。
+- AOF 记录日志在主线程中进行,可能因命令执行后写日志而阻塞后续命令。
+- AOF 刷盘(fsync)时后台线程同步到磁盘,磁盘压力大导致 `fsync` 阻塞,进而阻塞主线程 `write` 操作,尤其在 `appendfsync always` 或 `everysec` 配置下明显。
+- AOF 重写过程中将重写缓冲区内容追加到新 AOF 文件时产生阻塞。
+- 操作大 key(string > 1MB 或复合类型元素 > 5000)导致客户端超时、网络阻塞和工作线程阻塞。
+- 使用 `flushdb` 或 `flushall` 清空数据库时涉及大量键值对删除和内存释放,造成主线程阻塞。
+- 集群扩容缩容时数据迁移为同步操作,大 key 迁移导致两端节点长时间阻塞,可能触发故障转移
+- 内存不足触发 Swap,操作系统将 Redis 内存换出到硬盘,读写性能急剧下降。
+- 其他进程过度占用 CPU 导致 Redis 吞吐量下降。
+- 网络问题如连接拒绝、延迟高、网卡软中断等导致 Redis 阻塞。
+
+详细介绍可以阅读这篇文章:[Redis 常见阻塞原因总结](https://javaguide.cn/database/redis/redis-common-blocking-problems-summary.html)。
## Redis 集群
**Redis Sentinel**:
1. 什么是 Sentinel? 有什么用?
-2. Sentinel 如何检测节点是否下线?主观下线与客观下线的区别?
+2. Sentinel 如何检测节点是否下线?主观下线与客观下线的区别?
3. Sentinel 是如何实现故障转移的?
4. 为什么建议部署多个 sentinel 节点(哨兵集群)?
-5. Sentinel 如何选择出新的 master(选举机制)?
-6. 如何从 Sentinel 集群中选择出 Leader ?
+5. Sentinel 如何选择出新的 master(选举机制)?
+6. 如何从 Sentinel 集群中选择出 Leader?
7. Sentinel 可以防止脑裂吗?
**Redis Cluster**:
1. 为什么需要 Redis Cluster?解决了什么问题?有什么优势?
2. Redis Cluster 是如何分片的?
-3. 为什么 Redis Cluster 的哈希槽是 16384 个?
+3. 为什么 Redis Cluster 的哈希槽是 16384 个?
4. 如何确定给定 key 的应该分布到哪个哈希槽中?
5. Redis Cluster 支持重新分配哈希槽吗?
6. Redis Cluster 扩容缩容期间可以提供服务吗?
@@ -777,23 +803,21 @@ Cache Aside Pattern 中遇到写请求是这样的:更新数据库,然后直
实际使用 Redis 的过程中,我们尽量要准守一些常见的规范,比如:
1. 使用连接池:避免频繁创建关闭客户端连接。
-2. 尽量不使用 O(n)指令,使用 O(n) 命令时要关注 n 的数量:像 `KEYS *`、`HGETALL`、`LRANGE`、`SMEMBERS`、`SINTER`/`SUNION`/`SDIFF`等 O(n) 命令并非不能使用,但是需要明确 n 的值。另外,有遍历的需求可以使用 `HSCAN`、`SSCAN`、`ZSCAN` 代替。
-3. 使用批量操作减少网络传输:原生批量操作命令(比如 `MGET`、`MSET`等等)、pipeline、Lua 脚本。
-4. 尽量不适用 Redis 事务:Redis 事务实现的功能比较鸡肋,可以使用 Lua 脚本代替。
+2. 尽量不使用 O(n) 指令,使用 O(n) 命令时要关注 n 的数量:像 `KEYS *`、`HGETALL`、`LRANGE`、`SMEMBERS`、`SINTER`/`SUNION`/`SDIFF` 等 O(n) 命令并非不能使用,但是需要明确 n 的值。另外,有遍历的需求可以使用 `HSCAN`、`SSCAN`、`ZSCAN` 代替。
+3. 使用批量操作减少网络传输:原生批量操作命令(比如 `MGET`、`MSET` 等等)、pipeline、Lua 脚本。
+4. 尽量不使用 Redis 事务:Redis 事务实现的功能比较鸡肋,可以使用 Lua 脚本代替。
5. 禁止长时间开启 monitor:对性能影响比较大。
6. 控制 key 的生命周期:避免 Redis 中存放了太多不经常被访问的数据。
7. ……
-相关文章推荐:[阿里云 Redis 开发规范](https://developer.aliyun.com/article/531067) 。
-
## 参考
- 《Redis 开发与运维》
- 《Redis 设计与实现》
-- Redis Transactions :
+- Redis Transactions:
- What is Redis Pipeline:
- 一文详解 Redis 中 BigKey、HotKey 的发现与处理:
-- Bigkey 问题的解决思路与方式探索:
+- Bigkey 问题的解决思路与方式探索:
- Redis 延迟问题全面排障指南:
diff --git a/docs/database/redis/redis-skiplist.md b/docs/database/redis/redis-skiplist.md
index 8546aa8eb04..d50ee58706f 100644
--- a/docs/database/redis/redis-skiplist.md
+++ b/docs/database/redis/redis-skiplist.md
@@ -1,8 +1,13 @@
---
title: Redis为什么用跳表实现有序集合
+description: 深入讲解Redis有序集合Zset为何选择跳表而非红黑树、B+树实现,详解跳表的数据结构原理、时间复杂度分析和Redis源码实现。
category: 数据库
tag:
- Redis
+head:
+ - - meta
+ - name: keywords
+ content: Redis跳表,SkipList,有序集合,Zset,跳表原理,平衡树对比,Redis数据结构
---
## 前言
@@ -40,14 +45,14 @@ tag:
6) "60"
```
-此时我们通过 `object` 指令查看 zset 的数据结构,可以看到当前有序集合存储的还是是**ziplist(压缩列表)**。
+此时我们通过 `object` 指令查看 zset 的数据结构,可以看到当前有序集合存储的还是**ziplist(压缩列表)**。
```bash
127.0.0.1:6379> object encoding rankList
"ziplist"
```
-因为设计者考虑到 Redis 数据存放于内存,为了节约宝贵的内存空间在有序集合在元素小于 64 字节且个数小于 128 的时候,会使用 ziplist,而这个阈值的默认值的设置就来自下面这两个配置项。
+因为设计者考虑到 Redis 数据存放于内存,为了节约宝贵的内存空间,在有序集合元素小于 64 字节且个数小于 128 的时候,会使用 ziplist,而这个阈值的默认值的设置就来自下面这两个配置项。
```bash
zset-max-ziplist-value 64
@@ -78,7 +83,7 @@ zset-max-ziplist-entries 128
为了更好的回答上述问题以及更好的理解和掌握跳表,这里可以通过手写一个简单的跳表的形式来帮助读者理解跳表这个数据结构。
-我们都知道有序链表在添加、查询、删除的平均时间复杂都都是**O(n)**即线性增长,所以一旦节点数量达到一定体量后其性能表现就会非常差劲。而跳表我们完全可以理解为在原始链表基础上,建立多级索引,通过多级索引检索定位将增删改查的时间复杂度变为**O(log n)**。
+我们都知道有序链表在添加、查询、删除的平均时间复杂都都是 **O(n)** 即线性增长,所以一旦节点数量达到一定体量后其性能表现就会非常差劲。而跳表我们完全可以理解为在原始链表基础上,建立多级索引,通过多级索引检索定位将增删改查的时间复杂度变为 **O(log n)** 。
可能这里说的有些抽象,我们举个例子,以下图跳表为例,其原始链表存储按序存储 1-10,有 2 级索引,每级索引的索引个数都是基于下层元素个数的一半。
@@ -145,8 +150,8 @@ r=n/2^k
1. 跳表的高度计算从原始链表开始,即默认情况下插入的元素的高度为 1,代表没有索引,只有元素节点。
2. 设计一个为插入元素生成节点索引高度 level 的方法。
3. 进行一次随机运算,随机数值范围为 0-1 之间。
-4. 如果随机数大于 0.5 则为当前元素添加一级索引,自此我们保证生成一级索引的概率为**50%**,这也就保证了 1 级索引理想情况下只有一半的元素会生成索引。
-5. 同理后续每次随机算法得到的值大于 0.5 时,我们的索引高度就加 1,这样就可以保证节点生成的 2 级索引概率为**25%**,3 级索引为**12.5%**……
+4. 如果随机数大于 0.5 则为当前元素添加一级索引,自此我们保证生成一级索引的概率为 **50%** ,这也就保证了 1 级索引理想情况下只有一半的元素会生成索引。
+5. 同理后续每次随机算法得到的值大于 0.5 时,我们的索引高度就加 1,这样就可以保证节点生成的 2 级索引概率为 **25%** ,3 级索引为 **12.5%** ……
我们回过头,上述插入 7 之后,我们通过随机算法得到 2,即要为其建立 1 级索引:
@@ -241,41 +246,40 @@ private int levelCount = 1;
private Node h = new Node();
public void add(int value) {
-
- //随机生成高度
- int level = randomLevel();
+ int level = randomLevel(); // 新节点的随机高度
Node newNode = new Node();
newNode.data = value;
newNode.maxLevel = level;
- //创建一个node数组,用于记录小于当前value的最大值
- Node[] maxOfMinArr = new Node[level];
- //默认情况下指向头节点
+ // 用于记录每层前驱节点的数组
+ Node[] update = new Node[level];
for (int i = 0; i < level; i++) {
- maxOfMinArr[i] = h;
+ update[i] = h;
}
- //基于上述结果拿到当前节点的后继节点
Node p = h;
- for (int i = level - 1; i >= 0; i--) {
+ // 关键修正:从跳表的当前最高层开始查找
+ for (int i = levelCount - 1; i >= 0; i--) {
while (p.forwards[i] != null && p.forwards[i].data < value) {
p = p.forwards[i];
}
- maxOfMinArr[i] = p;
+ // 只记录需要更新的层的前驱节点
+ if (i < level) {
+ update[i] = p;
+ }
}
- //更新前驱节点的后继节点为当前节点newNode
+ // 插入新节点
for (int i = 0; i < level; i++) {
- newNode.forwards[i] = maxOfMinArr[i].forwards[i];
- maxOfMinArr[i].forwards[i] = newNode;
+ newNode.forwards[i] = update[i].forwards[i];
+ update[i].forwards[i] = newNode;
}
- //如果当前newNode高度大于跳表最高高度则更新levelCount
+ // 更新跳表的总高度
if (levelCount < level) {
levelCount = level;
}
-
}
```
@@ -283,33 +287,39 @@ public void add(int value) {
查询逻辑比较简单,从跳表最高级的索引开始定位找到小于要查的 value 的最大值,以下图为例,我们希望查找到节点 8:
-1. 跳表的 3 级索引首先找找到 5 的索引,5 的 3 级索引**forwards[3]**指向空,索引直接向下。
-2. 来到 5 的 2 级索引,其后继**forwards[2]**指向 8,继续向下。
-3. 5 的 1 级索引**forwards[1]**指向索引 6,继续向前。
-4. 索引 6 的**forwards[1]**指向索引 8,继续向下。
-5. 我们在原始节点向前找到节点 7。
-6. 节点 7 后续就是节点 8,继续向前为节点 8,无法继续向下,结束搜寻。
-7. 判断 7 的前驱,等于 8,查找结束。
-

+- **从最高层级开始 (3 级索引)** :查找指针 `p` 从头节点开始。在 3 级索引上,`p` 的后继 `forwards[2]`(假设最高 3 层,索引从 0 开始)指向节点 `5`。由于 `5 < 8`,指针 `p` 向右移动到节点 `5`。节点 `5` 在 3 级索引上的后继 `forwards[2]` 为 `null`(或指向一个大于 `8` 的节点,图中未画出)。当前层级向右查找结束,指针 `p` 保持在节点 `5`,**向下移动到 2 级索引**。
+- **在 2 级索引**:当前指针 `p` 为节点 `5`。`p` 的后继 `forwards[1]` 指向节点 `8`。由于 `8` 不小于 `8`(即 `8 < 8` 为 `false`),当前层级向右查找结束(`p` 不会移动到节点 `8`)。指针 `p` 保持在节点 `5`,**向下移动到 1 级索引**。
+- **在 1 级索引** :当前指针 `p` 为节点 `5`。`p` 的后继 `forwards[0]` 指向最底层的节点 `5`。由于 `5 < 8`,指针 `p` 向右移动到最底层的节点 `5`。此时,当前指针 `p` 为最底层的节点 `5`。其后继 `forwards[0]` 指向最底层的节点 `6`。由于 `6 < 8`,指针 `p` 向右移动到最底层的节点 `6`。当前指针 `p` 为最底层的节点 `6`。其后继 `forwards[0]` 指向最底层的节点 `7`。由于 `7 < 8`,指针 `p` 向右移动到最底层的节点 `7`。当前指针 `p` 为最底层的节点 `7`。其后继 `forwards[0]` 指向最底层的节点 `8`。由于 `8` 不小于 `8`(即 `8 < 8` 为 `false`),当前层级向右查找结束。此时,已经遍历完所有层级,`for` 循环结束。
+- **最终定位与检查** :经过所有层级的查找,指针 `p` 最终停留在最底层(0 级索引)的节点 `7`。这个节点是整个跳表中值小于目标值 `8` 的那个最大的节点。检查节点 `7` 的**后继节点**(即 `p.forwards[0]`):`p.forwards[0]` 指向节点 `8`。判断 `p.forwards[0].data`(即节点 `8` 的值)是否等于目标值 `8`。条件满足(`8 == 8`),**查找成功,找到节点 `8`**。
+
所以我们的代码实现也很上述步骤差不多,从最高级索引开始向前查找,如果不为空且小于要查找的值,则继续向前搜寻,遇到不小于的节点则继续向下,如此往复,直到得到当前跳表中小于查找值的最大节点,查看其前驱是否等于要查找的值:
```java
public Node get(int value) {
- Node p = h;
- //找到小于value的最大值
+ Node p = h; // 从头节点开始
+
+ // 从最高层级索引开始,逐层向下
for (int i = levelCount - 1; i >= 0; i--) {
+ // 在当前层级向右查找,直到 p.forwards[i] 为 null
+ // 或者 p.forwards[i].data 大于等于目标值 value
while (p.forwards[i] != null && p.forwards[i].data < value) {
- p = p.forwards[i];
+ p = p.forwards[i]; // 向右移动
}
+ // 此时 p.forwards[i] 为 null,或者 p.forwards[i].data >= value
+ // 或者 p 是当前层级中小于 value 的最大节点(如果存在这样的节点)
}
- //如果p的前驱节点等于value则直接返回
+
+ // 经过所有层级的查找,p 现在是原始链表(0级索引)中
+ // 小于目标值 value 的最大节点(或者头节点,如果所有元素都大于等于 value)
+
+ // 检查 p 在原始链表中的下一个节点是否是目标值
if (p.forwards[0] != null && p.forwards[0].data == value) {
- return p.forwards[0];
+ return p.forwards[0]; // 找到了,返回该节点
}
- return null;
+ return null; // 未找到
}
```
@@ -374,7 +384,7 @@ public class SkipList {
/**
* 每个节点添加一层索引高度的概率为二分之一
*/
- private static final float PROB = 0.5 f;
+ private static final float PROB = 0.5f;
/**
* 默认情况下的高度为1,即只有自己一个节点
@@ -386,9 +396,11 @@ public class SkipList {
*/
private Node h = new Node();
- public SkipList() {}
+ public SkipList() {
+ }
public class Node {
+
private int data = -1;
/**
*
@@ -398,58 +410,55 @@ public class SkipList {
@Override
public String toString() {
- return "Node{" +
- "data=" + data +
- ", maxLevel=" + maxLevel +
- '}';
+ return "Node{"
+ + "data=" + data
+ + ", maxLevel=" + maxLevel
+ + '}';
}
}
public void add(int value) {
-
- //随机生成高度
- int level = randomLevel();
+ int level = randomLevel(); // 新节点的随机高度
Node newNode = new Node();
newNode.data = value;
newNode.maxLevel = level;
- //创建一个node数组,用于记录小于当前value的最大值
- Node[] maxOfMinArr = new Node[level];
- //默认情况下指向头节点
+ // 用于记录每层前驱节点的数组
+ Node[] update = new Node[level];
for (int i = 0; i < level; i++) {
- maxOfMinArr[i] = h;
+ update[i] = h;
}
- //基于上述结果拿到当前节点的后继节点
Node p = h;
- for (int i = level - 1; i >= 0; i--) {
+ // 关键修正:从跳表的当前最高层开始查找
+ for (int i = levelCount - 1; i >= 0; i--) {
while (p.forwards[i] != null && p.forwards[i].data < value) {
p = p.forwards[i];
}
- maxOfMinArr[i] = p;
+ // 只记录需要更新的层的前驱节点
+ if (i < level) {
+ update[i] = p;
+ }
}
- //更新前驱节点的后继节点为当前节点newNode
+ // 插入新节点
for (int i = 0; i < level; i++) {
- newNode.forwards[i] = maxOfMinArr[i].forwards[i];
- maxOfMinArr[i].forwards[i] = newNode;
+ newNode.forwards[i] = update[i].forwards[i];
+ update[i].forwards[i] = newNode;
}
- //如果当前newNode高度大于跳表最高高度则更新levelCount
+ // 更新跳表的总高度
if (levelCount < level) {
levelCount = level;
}
-
}
/**
* 理论来讲,一级索引中元素个数应该占原始数据的 50%,二级索引中元素个数占 25%,三级索引12.5% ,一直到最顶层。
- * 因为这里每一层的晋升概率是 50%。对于每一个新插入的节点,都需要调用 randomLevel 生成一个合理的层数。
- * 该 randomLevel 方法会随机生成 1~MAX_LEVEL 之间的数,且 :
- * 50%的概率返回 1
- * 25%的概率返回 2
- * 12.5%的概率返回 3 ...
+ * 因为这里每一层的晋升概率是 50%。对于每一个新插入的节点,都需要调用 randomLevel 生成一个合理的层数。 该 randomLevel
+ * 方法会随机生成 1~MAX_LEVEL 之间的数,且 : 50%的概率返回 1 25%的概率返回 2 12.5%的概率返回 3 ...
+ *
* @return
*/
private int randomLevel() {
@@ -517,11 +526,11 @@ public class SkipList {
}
}
-
}
+
```
-对应测试代码和输出结果如下:
+测试代码:
```java
public static void main(String[] args) {
@@ -544,60 +553,11 @@ public static void main(String[] args) {
}
```
-输出结果:
+**Redis 跳表的特点**:
-```bash
-**********输出添加结果**********
-Node{data=0, maxLevel=2}
-Node{data=1, maxLevel=3}
-Node{data=2, maxLevel=1}
-Node{data=3, maxLevel=1}
-Node{data=4, maxLevel=2}
-Node{data=5, maxLevel=2}
-Node{data=6, maxLevel=2}
-Node{data=7, maxLevel=2}
-Node{data=8, maxLevel=4}
-Node{data=9, maxLevel=1}
-Node{data=10, maxLevel=1}
-Node{data=11, maxLevel=1}
-Node{data=12, maxLevel=1}
-Node{data=13, maxLevel=1}
-Node{data=14, maxLevel=1}
-Node{data=15, maxLevel=3}
-Node{data=16, maxLevel=4}
-Node{data=17, maxLevel=2}
-Node{data=18, maxLevel=1}
-Node{data=19, maxLevel=1}
-Node{data=20, maxLevel=1}
-Node{data=21, maxLevel=3}
-Node{data=22, maxLevel=1}
-Node{data=23, maxLevel=1}
-**********查询结果:Node{data=22, maxLevel=1} **********
-**********删除结果**********
-Node{data=0, maxLevel=2}
-Node{data=1, maxLevel=3}
-Node{data=2, maxLevel=1}
-Node{data=3, maxLevel=1}
-Node{data=4, maxLevel=2}
-Node{data=5, maxLevel=2}
-Node{data=6, maxLevel=2}
-Node{data=7, maxLevel=2}
-Node{data=8, maxLevel=4}
-Node{data=9, maxLevel=1}
-Node{data=10, maxLevel=1}
-Node{data=11, maxLevel=1}
-Node{data=12, maxLevel=1}
-Node{data=13, maxLevel=1}
-Node{data=14, maxLevel=1}
-Node{data=15, maxLevel=3}
-Node{data=16, maxLevel=4}
-Node{data=17, maxLevel=2}
-Node{data=18, maxLevel=1}
-Node{data=19, maxLevel=1}
-Node{data=20, maxLevel=1}
-Node{data=21, maxLevel=3}
-Node{data=23, maxLevel=1}
-```
+1. 采用**双向链表**,不同于上面的示例,存在一个回退指针。主要用于简化操作,例如删除某个元素时,还需要找到该元素的前驱节点,使用回退指针会非常方便。
+2. `score` 值可以重复,如果 `score` 值一样,则按照 ele(节点存储的值,为 sds)字典排序
+3. Redis 跳跃表默认允许最大的层数是 32,被源码中 `ZSKIPLIST_MAXLEVEL` 定义。
## 和其余三种数据结构的比较
@@ -605,7 +565,7 @@ Node{data=23, maxLevel=1}
### 平衡树 vs 跳表
-先来说说它和平衡树的比较,平衡树我们又会称之为 **AVL 树**,是一个严格的平衡二叉树,平衡条件必须满足(所有节点的左右子树高度差不超过 1,即平衡因子为范围为 `[-1,1]`)。平衡树的插入、删除和查询的时间复杂度和跳表一样都是 **O(log n)**。
+先来说说它和平衡树的比较,平衡树我们又会称之为 **AVL 树**,是一个严格的平衡二叉树,平衡条件必须满足(所有节点的左右子树高度差不超过 1,即平衡因子为范围为 `[-1,1]`)。平衡树的插入、删除和查询的时间复杂度和跳表一样都是 **O(log n)** 。
对于范围查询来说,它也可以通过中序遍历的方式达到和跳表一样的效果。但是它的每一次插入或者删除操作都需要保证整颗树左右节点的绝对平衡,只要不平衡就要通过旋转操作来保持平衡,这个过程是比较耗时的。
@@ -674,7 +634,7 @@ private Node add(Node node, K key, V value) {
### 红黑树 vs 跳表
-红黑树(Red Black Tree)也是一种自平衡二叉查找树,它的查询性能略微逊色于 AVL 树,但插入和删除效率更高。红黑树的插入、删除和查询的时间复杂度和跳表一样都是 **O(log n)**。
+红黑树(Red Black Tree)也是一种自平衡二叉查找树,它的查询性能略微逊色于 AVL 树,但插入和删除效率更高。红黑树的插入、删除和查询的时间复杂度和跳表一样都是 **O(log n)** 。
红黑树是一个**黑平衡树**,即从任意节点到另外一个叶子叶子节点,它所经过的黑节点是一样的。当对它进行插入操作时,需要通过旋转和染色(红黑变换)来保证黑平衡。不过,相较于 AVL 树为了维持平衡的开销要小一些。关于红黑树的详细介绍,可以查看这篇文章:[红黑树](https://javaguide.cn/cs-basics/data-structure/red-black-tree.html)。
@@ -726,7 +686,7 @@ private Node < K, V > add(Node < K, V > node, K key, V val) {
1. **多叉树结构**:它是一棵多叉树,每个节点可以包含多个子节点,减小了树的高度,查询效率高。
2. **存储效率高**:其中非叶子节点存储多个 key,叶子节点存储 value,使得每个节点更够存储更多的键,根据索引进行范围查询时查询效率更高。-
-3. **平衡性**:它是绝对的平衡,即树的各个分支高度相差不大,确保查询和插入时间复杂度为**O(log n)**。
+3. **平衡性**:它是绝对的平衡,即树的各个分支高度相差不大,确保查询和插入时间复杂度为 **O(log n)** 。
4. **顺序访问**:叶子节点间通过链表指针相连,范围查询表现出色。
5. **数据均匀分布**:B+树插入时可能会导致数据重新分布,使得数据在整棵树分布更加均匀,保证范围查询和删除效率。
diff --git a/docs/database/sql/sql-questions-01.md b/docs/database/sql/sql-questions-01.md
index 4bf08f0fa0b..b61d0f07c1e 100644
--- a/docs/database/sql/sql-questions-01.md
+++ b/docs/database/sql/sql-questions-01.md
@@ -1,9 +1,14 @@
---
title: SQL常见面试题总结(1)
+description: SQL常见面试题总结第一篇,涵盖SELECT检索数据、WHERE条件过滤、ORDER BY排序、DISTINCT去重、LIMIT分页等基础查询操作及牛客真题解析。
category: 数据库
tag:
- 数据库基础
- SQL
+head:
+ - - meta
+ - name: keywords
+ content: SQL面试题,SELECT查询,WHERE条件,ORDER BY排序,DISTINCT去重,LIMIT分页,SQL基础
---
> 题目来源于:[牛客题霸 - SQL 必知必会](https://www.nowcoder.com/exam/oj?page=1&tab=SQL%E7%AF%87&topicId=298)
diff --git a/docs/database/sql/sql-questions-02.md b/docs/database/sql/sql-questions-02.md
index 2a4a3e496c6..b0ce5fa2499 100644
--- a/docs/database/sql/sql-questions-02.md
+++ b/docs/database/sql/sql-questions-02.md
@@ -1,9 +1,14 @@
---
title: SQL常见面试题总结(2)
+description: SQL常见面试题总结第二篇,详解INSERT、UPDATE、DELETE等DML数据操作语句,包括批量插入、从其他表导入、带更新的插入等实战技巧。
category: 数据库
tag:
- 数据库基础
- SQL
+head:
+ - - meta
+ - name: keywords
+ content: SQL面试题,INSERT插入,UPDATE更新,DELETE删除,批量插入,REPLACE INTO,数据操作
---
> 题目来源于:[牛客题霸 - SQL 进阶挑战](https://www.nowcoder.com/exam/oj?page=1&tab=SQL%E7%AF%87&topicId=240)
diff --git a/docs/database/sql/sql-questions-03.md b/docs/database/sql/sql-questions-03.md
index f5acd8fc5c8..a445fef8280 100644
--- a/docs/database/sql/sql-questions-03.md
+++ b/docs/database/sql/sql-questions-03.md
@@ -1,9 +1,14 @@
---
title: SQL常见面试题总结(3)
+description: SQL常见面试题总结第三篇,深入讲解聚合函数COUNT、SUM、AVG、MAX、MIN的使用,以及GROUP BY分组、HAVING过滤、截断平均值计算等进阶技巧。
category: 数据库
tag:
- 数据库基础
- SQL
+head:
+ - - meta
+ - name: keywords
+ content: SQL面试题,聚合函数,COUNT,SUM,AVG,MAX,MIN,GROUP BY,HAVING,截断平均值
---
> 题目来源于:[牛客题霸 - SQL 进阶挑战](https://www.nowcoder.com/exam/oj?page=1&tab=SQL%E7%AF%87&topicId=240)
diff --git a/docs/database/sql/sql-questions-04.md b/docs/database/sql/sql-questions-04.md
index c15eb8e2243..8dd989cd9b0 100644
--- a/docs/database/sql/sql-questions-04.md
+++ b/docs/database/sql/sql-questions-04.md
@@ -1,9 +1,14 @@
---
title: SQL常见面试题总结(4)
+description: SQL常见面试题总结第四篇,详解MySQL 8.0窗口函数ROW_NUMBER、RANK、DENSE_RANK、NTILE、LAG、LEAD等的用法和应用场景。
category: 数据库
tag:
- 数据库基础
- SQL
+head:
+ - - meta
+ - name: keywords
+ content: SQL面试题,窗口函数,ROW_NUMBER,RANK,DENSE_RANK,NTILE,LAG,LEAD,MySQL 8.0
---
> 题目来源于:[牛客题霸 - SQL 进阶挑战](https://www.nowcoder.com/exam/oj?page=1&tab=SQL%E7%AF%87&topicId=240)
@@ -151,7 +156,7 @@ WHERE ranking <= 3
| 5 | 1003 | 9002 | 2021-09-01 12:01:01 | 2021-09-01 12:31:51 | 89 |
| 6 | 1004 | 9002 | 2021-09-01 19:01:01 | 2021-09-01 19:30:01 | 85 |
| 7 | 1005 | 9001 | 2021-09-01 12:01:01 | 2021-09-01 12:31:02 | 85 |
-| 8 | 1006 | 9001 | 2021-09-07 10:01:01 | 2021-09-07 10:21:01 | 84 |
+| 8 | 1006 | 9001 | 2021-09-07 10:02:01 | 2021-09-07 10:21:01 | 84 |
| 9 | 1003 | 9001 | 2021-09-08 12:01:01 | 2021-09-08 12:11:01 | 40 |
| 10 | 1003 | 9002 | 2021-09-01 14:01:01 | (NULL) | (NULL) |
| 11 | 1005 | 9001 | 2021-09-01 14:01:01 | (NULL) | (NULL) |
@@ -163,7 +168,7 @@ WHERE ranking <= 3
| ------- | -------- | ------------------- |
| 9001 | 60 | 2021-09-01 06:00:00 |
-**解释**:试卷 9001 被作答用时有 50 分钟、50 分钟、30 分 1 秒、11 分钟、10 分钟,第二快和第二慢用时之差为 50 分钟-11 分钟=39 分钟,试卷时长为 60 分钟,因此满足大于试卷时长一半的条件,输出试卷 ID、时长、发布时间。
+**解释**:试卷 9001 被作答用时有 50 分钟、58 分钟、30 分 1 秒、19 分钟、10 分钟,第二快和第二慢用时之差为 50 分钟-19 分钟=31 分钟,试卷时长为 60 分钟,因此满足大于试卷时长一半的条件,输出试卷 ID、时长、发布时间。
**思路:**
diff --git a/docs/database/sql/sql-questions-05.md b/docs/database/sql/sql-questions-05.md
index c20af2cad39..39171bfe08f 100644
--- a/docs/database/sql/sql-questions-05.md
+++ b/docs/database/sql/sql-questions-05.md
@@ -1,9 +1,14 @@
---
title: SQL常见面试题总结(5)
+description: SQL常见面试题总结第五篇,详解NULL空值处理技巧,包括IFNULL、COALESCE函数,以及使用CASE WHEN进行条件统计和完成率计算。
category: 数据库
tag:
- 数据库基础
- SQL
+head:
+ - - meta
+ - name: keywords
+ content: SQL面试题,NULL空值处理,IFNULL,COALESCE,CASE WHEN,条件统计,完成率计算
---
> 题目来源于:[牛客题霸 - SQL 进阶挑战](https://www.nowcoder.com/exam/oj?page=1&tab=SQL%E7%AF%87&topicId=240)
@@ -41,26 +46,53 @@ tag:
写法 1:
```sql
-SELECT exam_id,
- count(submit_time IS NULL OR NULL) incomplete_cnt,
- ROUND(count(submit_time IS NULL OR NULL) / count(*), 3) complete_rate
-FROM exam_record
-GROUP BY exam_id
-HAVING incomplete_cnt <> 0
+SELECT
+ exam_id,
+ (COUNT(*) - COUNT(submit_time)) AS incomplete_cnt,
+ ROUND((COUNT(*) - COUNT(submit_time)) / COUNT(*), 3) AS incomplete_rate
+FROM
+ exam_record
+GROUP BY
+ exam_id
+HAVING
+ (COUNT(*) - COUNT(submit_time)) > 0;
```
+利用 `COUNT(*)`统计分组内的总记录数,`COUNT(submit_time)` 只统计 `submit_time` 字段不为 NULL 的记录数(即已完成数)。两者相减,就是未完成数。
+
写法 2:
```sql
-SELECT exam_id,
- count(submit_time IS NULL OR NULL) incomplete_cnt,
- ROUND(count(submit_time IS NULL OR NULL) / count(*), 3) complete_rate
-FROM exam_record
-GROUP BY exam_id
-HAVING incomplete_cnt <> 0
+SELECT
+ exam_id,
+ COUNT(CASE WHEN submit_time IS NULL THEN 1 END) AS incomplete_cnt,
+ ROUND(COUNT(CASE WHEN submit_time IS NULL THEN 1 END) / COUNT(*), 3) AS incomplete_rate
+FROM
+ exam_record
+GROUP BY
+ exam_id
+HAVING
+ COUNT(CASE WHEN submit_time IS NULL THEN 1 END) > 0;
+```
+
+使用 `CASE` 表达式,当条件满足时返回一个非 `NULL` 值(例如 1),否则返回 `NULL`。然后用 `COUNT` 函数来统计非 `NULL` 值的数量。
+
+写法 3:
+
+```sql
+SELECT
+ exam_id,
+ SUM(submit_time IS NULL) AS incomplete_cnt,
+ ROUND(SUM(submit_time IS NULL) / COUNT(*), 3) AS incomplete_rate
+FROM
+ exam_record
+GROUP BY
+ exam_id
+HAVING
+ incomplete_cnt > 0;
```
-两种写法都可以,只有中间的写法不一样,一个是对符合条件的才`COUNT`,一个是直接上`IF`,后者更为直观,最后这个`having`解释一下, 无论是 `complete_rate` 还是 `incomplete_cnt`,只要不为 0 即可,不为 0 就意味着有未完成的。
+利用 `SUM` 函数对一个表达式求和。当 `submit_time` 为 `NULL` 时,表达式 `(submit_time IS NULL)` 的值为 1 (TRUE),否则为 0 (FALSE)。将这些 1 和 0 加起来,就得到了未完成的数量。
### 0 级用户高难度试卷的平均用时和平均得分
diff --git a/docs/database/sql/sql-syntax-summary.md b/docs/database/sql/sql-syntax-summary.md
index cff0b931495..679c1b59255 100644
--- a/docs/database/sql/sql-syntax-summary.md
+++ b/docs/database/sql/sql-syntax-summary.md
@@ -1,9 +1,14 @@
---
title: SQL语法基础知识总结
+description: SQL语法基础知识总结,系统讲解DDL数据定义、DML数据操作、DQL数据查询、DCL数据控制语言,涵盖表操作、约束、索引、事务、连接查询等核心知识点。
category: 数据库
tag:
- 数据库基础
- SQL
+head:
+ - - meta
+ - name: keywords
+ content: SQL语法,DDL,DML,DQL,DCL,CREATE,SELECT,INSERT,UPDATE,DELETE,JOIN连接,子查询
---
> 本文整理完善自下面这两份资料:
diff --git a/docs/distributed-system/api-gateway.md b/docs/distributed-system/api-gateway.md
index e9b9f27e6dd..25433a49ee4 100644
--- a/docs/distributed-system/api-gateway.md
+++ b/docs/distributed-system/api-gateway.md
@@ -1,5 +1,6 @@
---
title: API网关基础知识总结
+description: API网关基础知识详解,涵盖网关核心功能、请求转发、安全认证、流量控制及常见网关选型对比。
category: 分布式
---
diff --git a/docs/distributed-system/distributed-configuration-center.md b/docs/distributed-system/distributed-configuration-center.md
index 2e00aec70a3..0c71c519cdb 100644
--- a/docs/distributed-system/distributed-configuration-center.md
+++ b/docs/distributed-system/distributed-configuration-center.md
@@ -1,5 +1,6 @@
---
title: 分布式配置中心常见问题总结(付费)
+description: 分布式配置中心核心概念与面试题解析,涵盖Apollo、Nacos等主流配置中心原理与实践要点。
category: 分布式
---
@@ -8,5 +9,3 @@ category: 分布式

-
-
diff --git a/docs/distributed-system/distributed-id-design.md b/docs/distributed-system/distributed-id-design.md
index adbf61c9803..57077904251 100644
--- a/docs/distributed-system/distributed-id-design.md
+++ b/docs/distributed-system/distributed-id-design.md
@@ -1,5 +1,6 @@
---
title: 分布式ID设计指南
+description: 分布式ID设计实战指南,结合订单系统、优惠券等业务场景讲解分布式ID的设计要点与技术选型。
category: 分布式
---
@@ -99,7 +100,7 @@ UA 是一个特殊字符串头,服务器依次可以识别出客户使用的
abcdefghijklmnopqrstuvwxyzABCDEFGHJKLMNPQRSTUVWXZY0123456789
-之前说过,兑换码要求近可能简洁,那么设计时就需要考虑兑换码的字符数,假设上限为 12 位,而字符空间有 60 位,那么可以表示的空间范围为 60^12=130606940160000000000000(也就是可以 12 位的兑换码可以生成天量,应该够运营同学挥霍了),转换成 2 进制:
+之前说过,兑换码要求尽可能简洁,那么设计时就需要考虑兑换码的字符数,假设上限为 12 位,而字符空间有 60 位,那么可以表示的空间范围为 60^12=130606940160000000000000(也就是可以 12 位的兑换码可以生成天量,应该够运营同学挥霍了),转换成 2 进制:
1001000100000000101110011001101101110011000000000000000000000(61 位)
diff --git a/docs/distributed-system/distributed-id.md b/docs/distributed-system/distributed-id.md
index aa7d129faf3..13a1ceb720e 100644
--- a/docs/distributed-system/distributed-id.md
+++ b/docs/distributed-system/distributed-id.md
@@ -1,8 +1,11 @@
---
title: 分布式ID介绍&实现方案总结
+description: 分布式ID生成方案详解,涵盖UUID、数据库自增、号段模式、雪花算法等主流方案的原理与优缺点对比。
category: 分布式
---
+
+
## 分布式 ID 介绍
### 什么是 ID?
@@ -228,12 +231,16 @@ UUID.randomUUID()
我们这里重点关注一下这个 Version(版本),不同的版本对应的 UUID 的生成规则是不同的。
-5 种不同的 Version(版本)值分别对应的含义(参考[维基百科对于 UUID 的介绍](https://zh.wikipedia.org/wiki/通用唯一识别码)):
+8 种不同的 Version(版本)值分别对应的含义(参考[维基百科对于 UUID 的介绍](https://zh.wikipedia.org/wiki/通用唯一识别码)):
-- **版本 1** : UUID 是根据时间和节点 ID(通常是 MAC 地址)生成;
-- **版本 2** : UUID 是根据标识符(通常是组或用户 ID)、时间和节点 ID 生成;
-- **版本 3、版本 5** : 版本 5 - 确定性 UUID 通过散列(hashing)名字空间(namespace)标识符和名称生成;
-- **版本 4** : UUID 使用[随机性](https://zh.wikipedia.org/wiki/随机性)或[伪随机性](https://zh.wikipedia.org/wiki/伪随机性)生成。
+- **版本 1 (基于时间和节点 ID)** : 基于时间戳(通常是当前时间)和节点 ID(通常为设备的 MAC 地址)生成。当包含 MAC 地址时,可以保证全球唯一性,但也因此存在隐私泄露的风险。
+- **版本 2 (基于标识符、时间和节点 ID)** : 与版本 1 类似,也基于时间和节点 ID,但额外包含了本地标识符(例如用户 ID 或组 ID)。
+- **版本 3 (基于命名空间和名称的 MD5 哈希)**:使用 MD5 哈希算法,将命名空间标识符(一个 UUID)和名称字符串组合计算得到。相同的命名空间和名称总是生成相同的 UUID(**确定性生成**)。
+- **版本 4 (基于随机数)**:几乎完全基于随机数生成,通常使用伪随机数生成器(PRNG)或加密安全随机数生成器(CSPRNG)来生成。 虽然理论上存在碰撞的可能性,但理论上碰撞概率极低(2^122 的可能性),可以认为在实际应用中是唯一的。
+- **版本 5 (基于命名空间和名称的 SHA-1 哈希)**:类似于版本 3,但使用 SHA-1 哈希算法。
+- **版本 6 (基于时间戳、计数器和节点 ID)**:改进了版本 1,将时间戳放在最高有效位(Most Significant Bit,MSB),使得 UUID 可以直接按时间排序。
+- **版本 7 (基于时间戳和随机数据)**:基于 Unix 时间戳和随机数据生成。 由于时间戳位于最高有效位,因此支持按时间排序。并且,不依赖 MAC 地址或节点 ID,避免了隐私问题。
+- **版本 8 (自定义)**:允许用户根据自己的需求定义 UUID 的生成方式。其结构和内容由用户决定,提供更大的灵活性。
下面是 Version 1 版本下生成的 UUID 的示例:
@@ -261,7 +268,7 @@ int version = uuid.version();// 4
最后,我们再简单分析一下 **UUID 的优缺点** (面试的时候可能会被问到的哦!) :
-- **优点**:生成速度比较快、简单易用
+- **优点**:生成速度通常比较快、简单易用
- **缺点**:存储消耗空间大(32 个字符串,128 位)、 不安全(基于 MAC 地址生成 UUID 的算法会造成 MAC 地址泄露)、无序(非自增)、没有具体业务含义、需要解决重复 ID 问题(当机器时间不对的情况下,可能导致会产生重复 ID)
#### Snowflake(雪花算法)
diff --git a/docs/distributed-system/distributed-lock-implementations.md b/docs/distributed-system/distributed-lock-implementations.md
index cb4504c4a7a..d38726a4d63 100644
--- a/docs/distributed-system/distributed-lock-implementations.md
+++ b/docs/distributed-system/distributed-lock-implementations.md
@@ -1,5 +1,6 @@
---
title: 分布式锁常见实现方案总结
+description: 分布式锁常见实现方案详解,包括基于Redis、ZooKeeper实现分布式锁的原理、优缺点及最佳实践。
category: 分布式
---
@@ -372,10 +373,4 @@ private static class LockData
为了进一步提高系统的可靠性,建议引入一个兜底机制。例如,可以通过 **版本号(Fencing Token)机制** 来避免并发冲突。
-最后,再分享几篇我觉得写的还不错的文章:
-
-- [分布式锁实现原理与最佳实践 - 阿里云开发者](https://mp.weixin.qq.com/s/JzCHpIOiFVmBoAko58ZuGw)
-- [聊聊分布式锁 - 字节跳动技术团队](https://mp.weixin.qq.com/s/-N4x6EkxwAYDGdJhwvmZLw)
-- [Redis、ZooKeeper、Etcd,谁有最好用的分布式锁? - 腾讯云开发者](https://mp.weixin.qq.com/s/yZC6VJGxt1ANZkn0SljZBg)
-
diff --git a/docs/distributed-system/distributed-lock.md b/docs/distributed-system/distributed-lock.md
index ba53f443d03..1f48e5dc071 100644
--- a/docs/distributed-system/distributed-lock.md
+++ b/docs/distributed-system/distributed-lock.md
@@ -1,5 +1,6 @@
---
title: 分布式锁介绍
+description: 分布式锁基础概念详解,讲解为什么需要分布式锁、分布式锁的核心特性及常见应用场景分析。
category: 分布式
---
diff --git a/docs/distributed-system/distributed-process-coordination/zookeeper/zookeeper-in-action.md b/docs/distributed-system/distributed-process-coordination/zookeeper/zookeeper-in-action.md
index af6f3de5a21..06389b2986d 100644
--- a/docs/distributed-system/distributed-process-coordination/zookeeper/zookeeper-in-action.md
+++ b/docs/distributed-system/distributed-process-coordination/zookeeper/zookeeper-in-action.md
@@ -1,10 +1,13 @@
---
title: ZooKeeper 实战
+description: ZooKeeper实战教程,涵盖Docker安装部署、常用命令操作及Curator客户端的使用方法详解。
category: 分布式
tag:
- ZooKeeper
---
+
+
这篇文章简单给演示一下 ZooKeeper 常见命令的使用以及 ZooKeeper Java 客户端 Curator 的基本使用。介绍到的内容都是最基本的操作,能满足日常工作的基本需要。
如果文章有任何需要改善和完善的地方,欢迎在评论区指出,共同进步!
diff --git a/docs/distributed-system/distributed-process-coordination/zookeeper/zookeeper-intro.md b/docs/distributed-system/distributed-process-coordination/zookeeper/zookeeper-intro.md
index 955c5d2813a..8e812fac735 100644
--- a/docs/distributed-system/distributed-process-coordination/zookeeper/zookeeper-intro.md
+++ b/docs/distributed-system/distributed-process-coordination/zookeeper/zookeeper-intro.md
@@ -1,10 +1,13 @@
---
title: ZooKeeper相关概念总结(入门)
+description: ZooKeeper入门指南,讲解ZooKeeper核心概念、数据模型、Watcher机制及作为注册中心和分布式锁的应用。
category: 分布式
tag:
- ZooKeeper
---
+
+
相信大家对 ZooKeeper 应该不算陌生。但是你真的了解 ZooKeeper 到底有啥用不?如果别人/面试官让你给他讲讲对于 ZooKeeper 的认识,你能回答到什么地步呢?
拿我自己来说吧!我本人在大学曾经使用 Dubbo 来做分布式项目的时候,使用了 ZooKeeper 作为注册中心。为了保证分布式系统能够同步访问某个资源,我还使用 ZooKeeper 做过分布式锁。另外,我在学习 Kafka 的时候,知道 Kafka 很多功能的实现依赖了 ZooKeeper。
@@ -232,8 +235,8 @@ ZooKeeper 集群中的服务器状态有下面几种:
ZooKeeper 集群在宕掉几个 ZooKeeper 服务器之后,如果剩下的 ZooKeeper 服务器个数大于宕掉的个数的话整个 ZooKeeper 才依然可用。假如我们的集群中有 n 台 ZooKeeper 服务器,那么也就是剩下的服务数必须大于 n/2。先说一下结论,2n 和 2n-1 的容忍度是一样的,都是 n-1,大家可以先自己仔细想一想,这应该是一个很简单的数学问题了。
-比如假如我们有 3 台,那么最大允许宕掉 1 台 ZooKeeper 服务器,如果我们有 4 台的的时候也同样只允许宕掉 1 台。
-假如我们有 5 台,那么最大允许宕掉 2 台 ZooKeeper 服务器,如果我们有 6 台的的时候也同样只允许宕掉 2 台。
+比如假如我们有 3 台,那么最大允许宕掉 1 台 ZooKeeper 服务器,如果我们有 4 台的时候也同样只允许宕掉 1 台。
+假如我们有 5 台,那么最大允许宕掉 2 台 ZooKeeper 服务器,如果我们有 6 台的时候也同样只允许宕掉 2 台。
综上,何必增加那一个不必要的 ZooKeeper 呢?
diff --git a/docs/distributed-system/distributed-process-coordination/zookeeper/zookeeper-plus.md b/docs/distributed-system/distributed-process-coordination/zookeeper/zookeeper-plus.md
index 856378a0cd5..37b89d92cb3 100644
--- a/docs/distributed-system/distributed-process-coordination/zookeeper/zookeeper-plus.md
+++ b/docs/distributed-system/distributed-process-coordination/zookeeper/zookeeper-plus.md
@@ -1,5 +1,6 @@
---
title: ZooKeeper相关概念总结(进阶)
+description: ZooKeeper进阶详解,深入讲解ZAB协议、Leader选举机制、集群部署及与Eureka等注册中心的对比。
category: 分布式
tag:
- ZooKeeper
diff --git a/docs/distributed-system/distributed-transaction.md b/docs/distributed-system/distributed-transaction.md
index fa4c83c743c..cfb8ac6bde5 100644
--- a/docs/distributed-system/distributed-transaction.md
+++ b/docs/distributed-system/distributed-transaction.md
@@ -1,12 +1,11 @@
---
title: 分布式事务常见解决方案总结(付费)
+description: 分布式事务常见解决方案详解,包括2PC、3PC、TCC、Saga、本地消息表等方案的原理与适用场景分析。
category: 分布式
---
**分布式事务** 相关的面试题为我的[知识星球](https://javaguide.cn/about-the-author/zhishixingqiu-two-years.html)(点击链接即可查看详细介绍以及加入方法)专属内容,已经整理到了《Java 面试指北》中。
-
+
-
-
diff --git a/docs/distributed-system/protocol/cap-and-base-theorem.md b/docs/distributed-system/protocol/cap-and-base-theorem.md
index 36a2fa54d4a..c78fbc46a50 100644
--- a/docs/distributed-system/protocol/cap-and-base-theorem.md
+++ b/docs/distributed-system/protocol/cap-and-base-theorem.md
@@ -1,10 +1,13 @@
---
title: CAP & BASE理论详解
+description: CAP定理与BASE理论详解,深入讲解分布式系统一致性、可用性、分区容错性的权衡与实际应用。
category: 分布式
tag:
- 分布式理论
---
+
+
经历过技术面试的小伙伴想必对 CAP & BASE 这个两个理论已经再熟悉不过了!
我当年参加面试的时候,不夸张地说,只要问到分布式相关的内容,面试官几乎是必定会问这两个分布式相关的理论。一是因为这两个分布式基础理论是学习分布式知识的必备前置基础,二是因为很多面试官自己比较熟悉这两个理论(方便提问)。
diff --git a/docs/distributed-system/protocol/consistent-hashing.md b/docs/distributed-system/protocol/consistent-hashing.md
new file mode 100644
index 00000000000..10bebe8197c
--- /dev/null
+++ b/docs/distributed-system/protocol/consistent-hashing.md
@@ -0,0 +1,132 @@
+---
+title: 一致性哈希算法详解
+description: 一致性哈希算法原理详解,讲解哈希环、虚拟节点机制及在分布式缓存、负载均衡中的应用场景。
+category: 分布式
+tag:
+ - 分布式协议&算法
+ - 哈希算法
+---
+
+开始之前,先说两个常见的场景:
+
+1. **负载均衡**:由于访问人数太多,我们的网站部署了多台服务器个共同提供相同的服务,但每台服务器上存储的数据不同。为了保证请求的正确响应,相同参数(key)的请求(比如同个 IP 的请求、同一个用户的请求)需要发到同一台服务器处理。
+2. **分布式缓存**:由于缓存数据量太大,我们部署了多台缓存服务器共同提供缓存服务。缓存数据需要尽可能均匀地分布式在这些缓存服务器上,通过 key 可以找到对应的缓存服务器。
+
+这两种场景的本质,都是需要建立一个**从 key 到服务器/节点的稳定映射关系**。
+
+为了实现这个目标,你首先会想到什么方案呢?
+
+## 普通哈希算法
+
+相信大家很快就能想到 **“哈希+取模”** 这个经典组合。通过哈希函数计算出 key 的哈希值,再对服务器数量取模,从而将 key 映射到固定的服务器上。
+
+公式也很简单:
+
+```java
+node_number = hash(key) % N
+```
+
+- `hash(key)`: 使用哈希函数(建议使用性能较好的非加密哈希函数,例如 SipHash、MurMurHash3、CRC32、DJB)对唯一键进行哈希。
+- `% N`: 对哈希值取模,将哈希值映射到一个介于 0 到 N-1 之间的值,N 为节点数/服务器数。
+
+
+
+然而,传统的哈希取模算法有一个比较大的缺陷就是:**无法很好的解决机器/节点动态减少(比如某台机器宕机)或者增加的场景(比如又增加了一台机器)。**
+
+想象一下,服务器的初始数量为 4 台 (N = 4),如果其中一台服务器宕机,N 就变成了 3。此时,对于同一个 key,`hash(key) % 3` 的结果很可能与 `hash(key) % 4` 完全不同。
+
+
+
+这意味着几乎所有的数据映射关系都会错乱。在分布式缓存场景下,这会导致**大规模的缓存失效和缓存穿透**,瞬间将压力全部打到后端的数据库上,引发系统雪崩。
+
+据估算,当节点数量从 N 变为 N-1 时,平均有 (N-1)/N 比例的数据需要迁移,这个比例 **趋近于 100%** 。这种“牵一发而动全身”的效应,在生产环境中是完全不可接受的。
+
+为了更好地解决这个问题,一致性哈希算法诞生了。
+
+## 一致性哈希算法
+
+一致性哈希算法在 1997 年由麻省理工学院提出(这篇论文的 PDF 在线阅读地址:),是一种特殊的哈希算法,在移除或者添加一个服务器时,能够尽可能小地改变已存在的服务请求与处理请求服务器之间的映射关系。一致性哈希解决了传统哈希算法在分布式[哈希表](https://baike.baidu.com/item/哈希表/5981869)(Distributed Hash Table,DHT)中存在的动态伸缩等问题 。
+
+一致性哈希算法的底层原理也很简单,关键在于**哈希环**的引入。
+
+### 哈希环
+
+一致性哈希算法将哈希空间组织成一个环形结构,将数据和节点都映射到这个环上,然后根据顺时针的规则确定数据或请求应该分配到哪个节点上。通常情况下,哈希环的起点是 0,终点是 2^32 - 1,并且起点与终点连接,故这个环的整数分布范围是 **[0, 2^32-1]** 。
+
+传统哈希算法是对服务器数量取模,一致性哈希算法是对哈希环的范围取模,固定值,通常为 2^32:
+
+```java
+node_number = hash(key) % 2^32
+```
+
+服务器/节点如何映射到哈希环上呢?也是哈希取模。例如,一般我们会根据服务器的 IP 或者主机名进行哈希,然后再取模。
+
+```java
+hash(服务器ip)% 2^32
+```
+
+如下图所示:
+
+
+
+我们将数据和节点都映射到哈希环上,环上的每个节点都负责一个区间。对于上图来说,每个节点负责的数据情况如下:
+
+- **Node1:** 负责 Node4 到 Node1 之间的区域(包含 value6)。
+- **Node2:** 负责 Node1 到 Node2 之间的区域(包含 value1, value2)。
+- **Node3:** 负责 Node2 到 Node3 之间的区域(包含 value3)。
+- **Node4:** 负责 Node3 到 Node4 之间的区域(包含 value4, value5)。
+
+### 节点移除/增加
+
+新增节点和移除节点的情况下,哈希环的引入可以避免影响范围太大,减少需要迁移的数据。
+
+还是用上面分享的哈希环示意图为例,假设 Node2 节点被移除的话,那 Node3 就要负责 Node2 的数据,直接迁移 Node2 的数据到 Node3 即可,其他节点不受影响。
+
+
+
+同样地,如果我们在 Node1 和 Node2 之间新增一个节点 Node5,那么原本应该由 Node2 负责的一部分数据(即哈希值落在 Node1 和 Node5 之间的数据,如图中的 value1)现在会由 Node5 负责。我们只需要将这部分数据从 Node2 迁移到 Node5 即可,同样只影响了相邻的节点,影响范围非常小。
+
+
+
+### 数据倾斜问题
+
+理想情况下,节点在环上是均匀分布的。然而,现实可能并不是这样的,尤其是节点数量比较少的时候。节点可能被映射到附近的区域,这样的话,就会导致绝大部分数据都由其中一个节点负责。
+
+
+
+对于上图来说,每个节点负责的数据情况如下:
+
+- **Node1:** 负责 Node4 到 Node1 之间的区域(包含 value6)。
+- **Node2:** 负责 Node1 到 Node2 之间的区域(包含 value1)。
+- **Node3:** 负责 Node2 到 Node3 之间的区域(包含 value2,value3, value4, value5)。
+- **Node4:** 负责 Node3 到 Node4 之间的区域。
+
+除了数据倾斜问题,还有一个隐患。当新增或者删除节点的时候,数据分配不均衡。例如,Node3 被移除的话,Node3 负责的所有数据都要交给 Node4,随后所有的请求都要达到 Node4 上。假设 Node4 的服务器处理能力比较差的话,那可能直接就被干崩了。理想情况下,应该有更多节点来分担压力。
+
+如何解决这些问题呢?答案是引入**虚拟节点**。
+
+### 虚拟节点
+
+虚拟节点就是对真实的物理节点在哈希环上虚拟出几个它的分身节点。数据落到分身节点上实际上就是落到真实的物理节点上,通过将虚拟节点均匀分散在哈希环的各个部分。
+
+如下图所示,Node1、Node2、Node3、Node4 这 4 个节点都对应 3 个虚拟节点(下图只是为了演示,实际情况节点分布不会这么有规律)。
+
+
+
+对于上图来说,每个节点最终负责的数据情况如下:
+
+- **Node1**:value4
+- **Node2**:value1,value3
+- **Node3**:value5
+- **Node4**:value2,value6
+
+**引入虚拟节点的好处是巨大的:**
+
+1. **数据均衡:** 虚拟节点越多,环上的“服务器点”就越密集,数据分布自然就越均匀,从根本上解决了数据倾斜问题。通常,每个真实节点对应的虚拟节点数在 100 到 200 之间,例如 Nginx 选择为每个权重分配 160 个虚拟节点。这里的权重的是为了区分服务器,例如处理能力更强的服务器权重越高,进而导致对应的虚拟节点越多,被命中的概率越大。
+2. **容错性增强:** 这才是虚拟节点最精妙的地方。当一个物理节点宕机,它相当于在环上的多个虚拟节点同时下线。这些虚拟节点原本负责的数据和流量,会**自然地、均匀地分散**给环上其他**多个不同**的物理节点去接管,而不会将压力集中于某一个邻居节点。这极大地提升了系统的稳定性和容错能力。
+
+## 参考
+
+- 深入剖析 Nginx 负载均衡算法:
+- 读源码学架构系列:一致性哈希:
+- 一致性 Hash 算法原理总结:
diff --git a/docs/distributed-system/protocol/gossip-protocl.md b/docs/distributed-system/protocol/gossip-protocl.md
index 67c5c16139e..551422b2162 100644
--- a/docs/distributed-system/protocol/gossip-protocl.md
+++ b/docs/distributed-system/protocol/gossip-protocl.md
@@ -1,5 +1,6 @@
---
title: Gossip 协议详解
+description: Gossip协议原理详解,讲解去中心化信息传播机制、三种传播模式及在Redis Cluster等系统中的应用。
category: 分布式
tag:
- 分布式协议&算法
@@ -53,7 +54,7 @@ Redis Cluster 的节点之间会相互发送多种 Gossip 消息:

-有了 Redis Cluster 之后,不需要专门部署 Sentinel 集群服务了。Redis Cluster 相当于是内置了 Sentinel 机制,Redis Cluster 内部的各个 Redis 节点通过 Gossip 协议互相探测健康状态,在故障时可以自动切换。
+有了 Redis Cluster 之后,不需要专门部署 Sentinel 集群服务了。Redis Cluster 相当于是内置了 Sentinel 机制,Redis Cluster 内部的各个 Redis 节点通过 Gossip 协议共享集群内信息。
关于 Redis Cluster 的详细介绍,可以查看这篇文章 [Redis 集群详解(付费)](https://javaguide.cn/database/redis/redis-cluster.html) 。
diff --git a/docs/distributed-system/protocol/paxos-algorithm.md b/docs/distributed-system/protocol/paxos-algorithm.md
index c820209f4a8..1ab98fbc23a 100644
--- a/docs/distributed-system/protocol/paxos-algorithm.md
+++ b/docs/distributed-system/protocol/paxos-algorithm.md
@@ -1,5 +1,6 @@
---
title: Paxos 算法详解
+description: Paxos共识算法原理详解,涵盖Basic Paxos、Multi-Paxos的执行流程及在分布式系统中的应用。
category: 分布式
tag:
- 分布式协议&算法
diff --git a/docs/distributed-system/protocol/raft-algorithm.md b/docs/distributed-system/protocol/raft-algorithm.md
index 18d2c2eb0cb..59cb08af720 100644
--- a/docs/distributed-system/protocol/raft-algorithm.md
+++ b/docs/distributed-system/protocol/raft-algorithm.md
@@ -1,5 +1,6 @@
---
title: Raft 算法详解
+description: Raft共识算法原理详解,涵盖Leader选举、日志复制、安全性保证等核心机制及与Paxos的对比分析。
category: 分布式
tag:
- 分布式协议&算法
diff --git a/docs/distributed-system/rpc/dubbo.md b/docs/distributed-system/rpc/dubbo.md
index 3eaee38b50c..02cc37a8c0c 100644
--- a/docs/distributed-system/rpc/dubbo.md
+++ b/docs/distributed-system/rpc/dubbo.md
@@ -1,5 +1,6 @@
---
title: Dubbo常见问题总结
+description: Dubbo核心知识与面试题详解,涵盖Dubbo架构原理、SPI机制、负载均衡策略及服务治理等核心内容。
category: 分布式
tag:
- rpc
diff --git a/docs/distributed-system/rpc/http&rpc.md b/docs/distributed-system/rpc/http&rpc.md
index 35301d0bceb..324a68f8250 100644
--- a/docs/distributed-system/rpc/http&rpc.md
+++ b/docs/distributed-system/rpc/http&rpc.md
@@ -1,5 +1,6 @@
---
title: 有了 HTTP 协议,为什么还要有 RPC ?
+description: HTTP与RPC对比详解,讲解两种通信方式的本质区别、性能差异及在微服务架构中的选型建议。
category: 分布式
tag:
- rpc
diff --git a/docs/distributed-system/rpc/rpc-intro.md b/docs/distributed-system/rpc/rpc-intro.md
index d2c5fb5e9c7..1c2de76ef6a 100644
--- a/docs/distributed-system/rpc/rpc-intro.md
+++ b/docs/distributed-system/rpc/rpc-intro.md
@@ -1,5 +1,6 @@
---
title: RPC基础知识总结
+description: RPC远程过程调用基础详解,讲解RPC核心原理、调用流程、序列化协议及常见RPC框架对比分析。
category: 分布式
tag:
- rpc
diff --git a/docs/distributed-system/spring-cloud-gateway-questions.md b/docs/distributed-system/spring-cloud-gateway-questions.md
index 1e6e86845af..d05b3c15510 100644
--- a/docs/distributed-system/spring-cloud-gateway-questions.md
+++ b/docs/distributed-system/spring-cloud-gateway-questions.md
@@ -1,5 +1,6 @@
---
title: Spring Cloud Gateway常见问题总结
+description: Spring Cloud Gateway核心原理详解,包括路由配置、过滤器机制、限流熔断等常见面试题与实践要点。
category: 分布式
---
diff --git a/docs/high-availability/fallback-and-circuit-breaker.md b/docs/high-availability/fallback-and-circuit-breaker.md
index e9aa9188d4f..47695d7eaba 100644
--- a/docs/high-availability/fallback-and-circuit-breaker.md
+++ b/docs/high-availability/fallback-and-circuit-breaker.md
@@ -1,11 +1,12 @@
---
title: 降级&熔断详解(付费)
+description: 服务降级与熔断机制详解,讲解降级策略、熔断器原理及Hystrix、Sentinel等框架的应用实践。
category: 高可用
icon: circuit
---
**降级&熔断** 相关的面试题为我的[知识星球](https://javaguide.cn/about-the-author/zhishixingqiu-two-years.html)(点击链接即可查看详细介绍以及加入方法)专属内容,已经整理到了[《Java 面试指北》](https://javaguide.cn/zhuanlan/java-mian-shi-zhi-bei.html)中。
-
+
-
+
diff --git a/docs/high-availability/high-availability-system-design.md b/docs/high-availability/high-availability-system-design.md
index f461f93e99b..bb4fa8f40b0 100644
--- a/docs/high-availability/high-availability-system-design.md
+++ b/docs/high-availability/high-availability-system-design.md
@@ -1,5 +1,6 @@
---
title: 高可用系统设计指南
+description: 高可用系统设计核心指南,讲解系统可用性衡量标准、常见故障原因及提升可用性的架构设计方案。
category: 高可用
icon: design
---
diff --git a/docs/high-availability/idempotency.md b/docs/high-availability/idempotency.md
new file mode 100644
index 00000000000..d06e0002fa0
--- /dev/null
+++ b/docs/high-availability/idempotency.md
@@ -0,0 +1,12 @@
+---
+title: 接口幂等方案总结(付费)
+description: 接口幂等性设计详解,涵盖幂等性概念、常见实现方案及在支付、订单等场景中的应用实践。
+category: 高可用
+icon: security-fill
+---
+
+**接口幂等** 相关的面试题为我的[知识星球](https://javaguide.cn/about-the-author/zhishixingqiu-two-years.html)(点击链接即可查看详细介绍以及加入方法)专属内容,已经整理到了[《Java 面试指北》](https://javaguide.cn/zhuanlan/java-mian-shi-zhi-bei.html)中。
+
+
+
+
diff --git a/docs/high-availability/limit-request.md b/docs/high-availability/limit-request.md
index 22db662eedf..0123a4711f5 100644
--- a/docs/high-availability/limit-request.md
+++ b/docs/high-availability/limit-request.md
@@ -1,5 +1,6 @@
---
title: 服务限流详解
+description: 服务限流原理与实现详解,涵盖固定窗口、滑动窗口、令牌桶、漏桶等主流限流算法的原理与应用。
category: 高可用
icon: limit_rate
---
diff --git a/docs/high-availability/performance-test.md b/docs/high-availability/performance-test.md
index 47201441d7e..0076ebeff1e 100644
--- a/docs/high-availability/performance-test.md
+++ b/docs/high-availability/performance-test.md
@@ -1,5 +1,6 @@
---
title: 性能测试入门
+description: 性能测试入门指南,涵盖性能测试指标、压测工具选型、常见性能问题分析及调优方法详解。
category: 高可用
icon: et-performance
---
diff --git a/docs/high-availability/redundancy.md b/docs/high-availability/redundancy.md
index 9d14d726675..0c96bcb6980 100644
--- a/docs/high-availability/redundancy.md
+++ b/docs/high-availability/redundancy.md
@@ -1,5 +1,6 @@
---
title: 冗余设计详解
+description: 冗余设计原理详解,涵盖高可用集群、同城灾备、异地多活等冗余架构方案的设计与实现。
category: 高可用
icon: cluster
---
diff --git a/docs/high-availability/timeout-and-retry.md b/docs/high-availability/timeout-and-retry.md
index 3c7ba1ac9cd..6f823316ee1 100644
--- a/docs/high-availability/timeout-and-retry.md
+++ b/docs/high-availability/timeout-and-retry.md
@@ -1,5 +1,6 @@
---
title: 超时&重试详解
+description: 超时与重试机制详解,讲解超时设置原则、重试策略选择及在微服务系统中避免雪崩的最佳实践。
category: 高可用
icon: retry
---
diff --git a/docs/high-performance/cdn.md b/docs/high-performance/cdn.md
index f4ca0eab5f2..97f220aeabb 100644
--- a/docs/high-performance/cdn.md
+++ b/docs/high-performance/cdn.md
@@ -1,13 +1,11 @@
---
title: CDN工作原理详解
+description: CDN 就是将静态资源分发到多个不同的地方以实现就近访问,进而加快静态资源的访问速度,减轻服务器以及带宽的负担。
category: 高性能
head:
- - meta
- name: keywords
content: CDN,内容分发网络
- - - meta
- - name: description
- content: CDN 就是将静态资源分发到多个不同的地方以实现就近访问,进而加快静态资源的访问速度,减轻服务器以及带宽的负担。
---
## 什么是 CDN ?
diff --git a/docs/high-performance/data-cold-hot-separation.md b/docs/high-performance/data-cold-hot-separation.md
index cf706c68c9d..24f981d8c0f 100644
--- a/docs/high-performance/data-cold-hot-separation.md
+++ b/docs/high-performance/data-cold-hot-separation.md
@@ -1,13 +1,11 @@
---
title: 数据冷热分离详解
+description: 数据冷热分离是指根据数据的访问频率和业务重要性,将数据分为冷数据和热数据,冷数据一般存储在存储在低成本、低性能的介质中,热数据高性能存储介质中。
category: 高性能
head:
- - meta
- name: keywords
content: 数据冷热分离,冷数据迁移,冷数据存储
- - - meta
- - name: description
- content: 数据冷热分离是指根据数据的访问频率和业务重要性,将数据分为冷数据和热数据,冷数据一般存储在存储在低成本、低性能的介质中,热数据高性能存储介质中。
---
## 什么是数据冷热分离?
@@ -23,7 +21,7 @@ head:
1. **时间维度区分**:按照数据的创建时间、更新时间、过期时间等,将一定时间段内的数据视为热数据,超过该时间段的数据视为冷数据。例如,订单系统可以将 1 年前的订单数据作为冷数据,1 年内的订单数据作为热数据。这种方法适用于数据的访问频率和时间有较强的相关性的场景。
2. **访问频率区分**:将高频访问的数据视为热数据,低频访问的数据视为冷数据。例如,内容系统可以将浏览量非常低的文章作为冷数据,浏览量较高的文章作为热数据。这种方法需要记录数据的访问频率,成本较高,适合访问频率和数据本身有较强的相关性的场景。
-几年前的数据并不一定都是热数据,例如一些优质文章发表几年后依然有很多人访问,大部分普通用户新发表的文章却基本没什么人访问。
+几年前的数据并不一定都是冷数据,例如一些优质文章发表几年后依然有很多人访问,大部分普通用户新发表的文章却基本没什么人访问。
这两种区分冷热数据的方法各有优劣,实际项目中,可以将两者结合使用。
diff --git a/docs/high-performance/deep-pagination-optimization.md b/docs/high-performance/deep-pagination-optimization.md
index 8a419dadb84..0ab2f9079e3 100644
--- a/docs/high-performance/deep-pagination-optimization.md
+++ b/docs/high-performance/deep-pagination-optimization.md
@@ -1,13 +1,11 @@
---
title: 深度分页介绍及优化建议
+description: 查询偏移量过大的场景我们称为深度分页,这会导致查询性能较低。深度分页可以采用范围查询、子查询、INNER JOIN 延迟关联、覆盖索引等方法进行优化。
category: 高性能
head:
- - meta
- name: keywords
content: 深度分页
- - - meta
- - name: description
- content: 查询偏移量过大的场景我们称为深度分页,这会导致查询性能较低。深度分页可以采用范围查询、子查询、INNER JOIN 延迟关联、覆盖索引等方法进行优化。
---
## 深度分页介绍
@@ -19,6 +17,18 @@ head:
SELECT * FROM t_order ORDER BY id LIMIT 1000000, 10
```
+## 深度分页问题的原因
+
+当查询偏移量过大时,MySQL 的查询优化器可能会选择全表扫描而不是利用索引来优化查询。这是因为扫描索引和跳过大量记录可能比直接全表扫描更耗费资源。
+
+
+
+不同机器上这个查询偏移量过大的临界点可能不同,取决于多个因素,包括硬件配置(如 CPU 性能、磁盘速度)、表的大小、索引的类型和统计信息等。
+
+
+
+MySQL 的查询优化器采用基于成本的策略来选择最优的查询执行计划。它会根据 CPU 和 I/O 的成本来决定是否使用索引扫描或全表扫描。如果优化器认为全表扫描的成本更低,它就会放弃使用索引。不过,即使偏移量很大,如果查询中使用了覆盖索引(covering index),MySQL 仍然可能会使用索引,避免回表操作。
+
## 深度分页优化建议
这里以 MySQL 数据库为例介绍一下如何优化深度分页。
@@ -34,7 +44,11 @@ SELECT * FROM t_order WHERE id > 100000 AND id <= 100010 ORDER BY id
SELECT * FROM t_order WHERE id > 100000 LIMIT 10
```
-这种优化方式限制比较大,且一般项目的 ID 也没办法保证完全连续。
+这种基于 ID 范围的深度分页优化方式存在很大限制:
+
+1. **ID 连续性要求高**: 实际项目中,数据库自增 ID 往往因为各种原因(例如删除数据、事务回滚等)导致 ID 不连续,难以保证连续性。
+2. **排序问题**: 如果查询需要按照其他字段(例如创建时间、更新时间等)排序,而不是按照 ID 排序,那么这种方法就不再适用。
+3. **并发场景**: 在高并发场景下,单纯依赖记录上次查询的最后一条记录的 ID 进行分页,容易出现数据重复或遗漏的问题。
### 子查询
@@ -47,32 +61,49 @@ SELECT * FROM t_order WHERE id > 100000 LIMIT 10
> 
```sql
-# 通过子查询来获取 id 的起始值,把 limit 1000000 的条件转移到子查询
-SELECT * FROM t_order WHERE id >= (SELECT id FROM t_order limit 1000000, 1) LIMIT 10;
+-- 先通过子查询在主键索引上进行偏移,快速找到起始ID
+SELECT * FROM t_order WHERE id >= (SELECT id FROM t_order LIMIT 1000000, 1) LIMIT 10;
```
+**工作原理**:
+
+1. 子查询 `(SELECT id FROM t_order where id > 1000000 limit 1)` 会利用主键索引快速定位到第 1000001 条记录,并返回其 ID 值。
+2. 主查询 `SELECT * FROM t_order WHERE id >= ... LIMIT 10` 将子查询返回的起始 ID 作为过滤条件,使用 `id >=` 获取从该 ID 开始的后续 10 条记录。
+
不过,子查询的结果会产生一张新表,会影响性能,应该尽量避免大量使用子查询。并且,这种方法只适用于 ID 是正序的。在复杂分页场景,往往需要通过过滤条件,筛选到符合条件的 ID,此时的 ID 是离散且不连续的。
当然,我们也可以利用子查询先去获取目标分页的 ID 集合,然后再根据 ID 集合获取内容,但这种写法非常繁琐,不如使用 INNER JOIN 延迟关联。
### 延迟关联
-延迟关联的优化思路,跟子查询的优化思路其实是一样的:都是把条件转移到主键索引树,减少回表的次数。不同点是,延迟关联使用了 INNER JOIN(内连接) 包含子查询。
+延迟关联与子查询的优化思路类似,都是通过将 `LIMIT` 操作转移到主键索引树上,减少回表次数。相比直接使用子查询,延迟关联通过 `INNER JOIN` 将子查询结果集成到主查询中,避免了子查询可能产生的临时表。在执行 `INNER JOIN` 时,MySQL 优化器能够利用索引进行高效的连接操作(如索引扫描或其他优化策略),因此在深度分页场景下,性能通常优于直接使用子查询。
```sql
-SELECT t1.* FROM t_order t1
-INNER JOIN (SELECT id FROM t_order limit 1000000, 10) t2
-ON t1.id = t2.id;
+-- 使用 INNER JOIN 进行延迟关联
+SELECT t1.*
+FROM t_order t1
+INNER JOIN (
+ -- 这里的子查询可以利用覆盖索引,性能极高
+ SELECT id FROM t_order LIMIT 1000000, 10
+) t2 ON t1.id = t2.id;
```
+**工作原理**:
+
+1. 子查询 `(SELECT id FROM t_order where id > 1000000 LIMIT 10)` 利用主键索引快速定位目标分页的 10 条记录的 ID。
+2. 通过 `INNER JOIN` 将子查询结果与主表 `t_order` 关联,获取完整的记录数据。
+
除了使用 INNER JOIN 之外,还可以使用逗号连接子查询。
```sql
+-- 使用逗号进行延迟关联
SELECT t1.* FROM t_order t1,
-(SELECT id FROM t_order limit 1000000, 10) t2
+(SELECT id FROM t_order where id > 1000000 LIMIT 10) t2
WHERE t1.id = t2.id;
```
+**注意**: 虽然逗号连接子查询也能实现类似的效果,但为了代码可读性和可维护性,建议使用更规范的 `INNER JOIN` 语法。
+
### 覆盖索引
索引中已经包含了所有需要获取的字段的查询方式称为覆盖索引。
@@ -89,7 +120,19 @@ ORDER BY code
LIMIT 1000000, 10;
```
-不过,当查询的结果集占表的总行数的很大一部分时,可能就不会走索引了,自动转换为全表扫描。当然了,也可以通过 `FORCE INDEX` 来强制查询优化器走索引,但这种提升效果一般不明显。
+**⚠️注意**:
+
+- 当查询的结果集占表的总行数的很大一部分时,MySQL 查询优化器可能选择放弃使用索引,自动转换为全表扫描。
+- 虽然可以使用 `FORCE INDEX` 强制查询优化器走索引,但这种方式可能会导致查询优化器无法选择更优的执行计划,效果并不总是理想。
+
+## 总结
+
+本文总结了几种常见的深度分页优化方案:
+
+1. **范围查询**: 基于 ID 连续性进行分页,通过记录上一页最后一条记录的 ID 来获取下一页数据。适合 ID 连续且按 ID 查询的场景,但在 ID 不连续或需要按其他字段排序时存在局限。
+2. **子查询**: 先通过子查询获取分页的起始主键值,再根据主键进行筛选分页。利用主键索引提高效率,但子查询会生成临时表,复杂场景下性能不佳。
+3. **延迟关联 (INNER JOIN)**: 使用 `INNER JOIN` 将分页操作转移到主键索引上,减少回表次数。相比子查询,延迟关联的性能更优,适合大数据量的分页查询。
+4. **覆盖索引**: 通过索引直接获取所需字段,避免回表操作,减少 IO 开销,适合查询特定字段的场景。但当结果集较大时,MySQL 可能会选择全表扫描。
## 参考
diff --git a/docs/high-performance/load-balancing.md b/docs/high-performance/load-balancing.md
index a85b067a49c..ca1834e7a1f 100644
--- a/docs/high-performance/load-balancing.md
+++ b/docs/high-performance/load-balancing.md
@@ -1,13 +1,11 @@
---
title: 负载均衡原理及算法详解
+description: 负载均衡指的是将用户请求分摊到不同的服务器上处理,以提高系统整体的并发处理能力。负载均衡可以简单分为服务端负载均衡和客户端负载均衡 这两种。服务端负载均衡涉及到的知识点更多,工作中遇到的也比较多,因为,我会花更多时间来介绍。
category: 高性能
head:
- - meta
- name: keywords
content: 客户端负载均衡,服务负载均衡,Nginx,负载均衡算法,七层负载均衡,DNS解析
- - - meta
- - name: description
- content: 负载均衡指的是将用户请求分摊到不同的服务器上处理,以提高系统整体的并发处理能力。负载均衡可以简单分为服务端负载均衡和客户端负载均衡 这两种。服务端负载均衡涉及到的知识点更多,工作中遇到的也比较多,因为,我会花更多时间来介绍。
---
## 什么是负载均衡?
@@ -24,7 +22,7 @@ head:
负载均衡可以简单分为 **服务端负载均衡** 和 **客户端负载均衡** 这两种。
-服务端负载均衡涉及到的知识点更多,工作中遇到的也比较多,因为,我会花更多时间来介绍。
+服务端负载均衡涉及到的知识点更多,工作中遇到的也比较多,因此,我会花更多时间来介绍。
### 服务端负载均衡
diff --git a/docs/high-performance/message-queue/disruptor-questions.md b/docs/high-performance/message-queue/disruptor-questions.md
index 1881f6c2c79..662950ca69c 100644
--- a/docs/high-performance/message-queue/disruptor-questions.md
+++ b/docs/high-performance/message-queue/disruptor-questions.md
@@ -1,5 +1,6 @@
---
title: Disruptor常见问题总结
+description: "Disruptor常见问题总结:围绕高性能与性能优化实践梳理关键概念、常见问题与实践要点,帮助你高效学习与备战面试。"
category: 高性能
tag:
- 消息队列
@@ -49,7 +50,7 @@ Disruptor 主要解决了 JDK 内置线程安全队列的性能和内存安全
| `LinkedTransferQueue` | 无锁(`CAS`) | 无界 |
| `ConcurrentLinkedQueue` | 无锁(`CAS`) | 无界 |
-从上表中可以看出:这些队列要不就是加锁有界,要不就是无锁无界。而加锁的的队列势必会影响性能,无界的队列又存在内存溢出的风险。
+从上表中可以看出:这些队列要不就是加锁有界,要不就是无锁无界。而加锁的队列势必会影响性能,无界的队列又存在内存溢出的风险。
因此,一般情况下,我们都是不建议使用 JDK 内置线程安全队列。
diff --git a/docs/high-performance/message-queue/kafka-questions-01.md b/docs/high-performance/message-queue/kafka-questions-01.md
index 7e690399689..824d14642cf 100644
--- a/docs/high-performance/message-queue/kafka-questions-01.md
+++ b/docs/high-performance/message-queue/kafka-questions-01.md
@@ -1,5 +1,6 @@
---
title: Kafka常见问题总结
+description: "Kafka常见问题总结:围绕高性能与性能优化实践梳理关键概念、常见问题与实践要点,帮助你高效学习与备战面试。"
category: 高性能
tag:
- 消息队列
@@ -120,7 +121,7 @@ ZooKeeper 主要为 Kafka 提供元数据的管理的功能。
不过,要提示一下:**如果要使用 KRaft 模式的话,建议选择较高版本的 Kafka,因为这个功能还在持续完善优化中。Kafka 3.3.1 版本是第一个将 KRaft(Kafka Raft)共识协议标记为生产就绪的版本。**
-
+
## Kafka 消费顺序、消息丢失和重复消费
diff --git a/docs/high-performance/message-queue/message-queue.md b/docs/high-performance/message-queue/message-queue.md
index 50582cdd028..72734244dee 100644
--- a/docs/high-performance/message-queue/message-queue.md
+++ b/docs/high-performance/message-queue/message-queue.md
@@ -1,5 +1,6 @@
---
title: 消息队列基础知识总结
+description: "消息队列基础知识总结:围绕高性能与性能优化实践梳理关键概念、常见问题与实践要点,帮助你高效学习与备战面试。"
category: 高性能
tag:
- 消息队列
@@ -77,7 +78,7 @@ tag:
**消息队列使用发布-订阅模式工作,消息发送者(生产者)发布消息,一个或多个消息接受者(消费者)订阅消息。** 从上图可以看到**消息发送者(生产者)和消息接受者(消费者)之间没有直接耦合**,消息发送者将消息发送至分布式消息队列即结束对消息的处理,消息接受者从分布式消息队列获取该消息后进行后续处理,并不需要知道该消息从何而来。**对新增业务,只要对该类消息感兴趣,即可订阅该消息,对原有系统和业务没有任何影响,从而实现网站业务的可扩展性设计**。
-例如,我们商城系统分为用户、订单、财务、仓储、消息通知、物流、风控等多个服务。用户在完成下单后,需要调用财务(扣款)、仓储(库存管理)、物流(发货)、消息通知(通知用户发货)、风控(风险评估)等服务。使用消息队列后,下单操作和后续的扣款、发货、通知等操作就解耦了,下单完成发送一个消息到消息队列,需要用到的地方去订阅这个消息进行消息即可。
+例如,我们商城系统分为用户、订单、财务、仓储、消息通知、物流、风控等多个服务。用户在完成下单后,需要调用财务(扣款)、仓储(库存管理)、物流(发货)、消息通知(通知用户发货)、风控(风险评估)等服务。使用消息队列后,下单操作和后续的扣款、发货、通知等操作就解耦了,下单完成发送一个消息到消息队列,需要用到的地方去订阅这个消息进行消费即可。

@@ -101,7 +102,7 @@ RocketMQ、 Kafka、Pulsar、QMQ 都提供了事务相关的功能。事务允
### 延时/定时处理
-消息发送后不会立即被消费,而是指定一个时间,到时间后再消费。大部分消息队列,例如 RocketMQ、RabbitMQ、Pulsar、Kafka,都支持定时/延时消息。
+消息发送后不会立即被消费,而是指定一个时间,到时间后再消费。大部分消息队列,例如 RocketMQ、RabbitMQ、Pulsar,都支持定时/延时消息。

@@ -206,7 +207,7 @@ Kafka 是一个分布式系统,由通过高性能 TCP 网络协议进行通信
不过,要提示一下:**如果要使用 KRaft 模式的话,建议选择较高版本的 Kafka,因为这个功能还在持续完善优化中。Kafka 3.3.1 版本是第一个将 KRaft(Kafka Raft)共识协议标记为生产就绪的版本。**
-
+
Kafka 官网:
diff --git a/docs/high-performance/message-queue/rabbitmq-questions.md b/docs/high-performance/message-queue/rabbitmq-questions.md
index e971eaf3604..caad547f75d 100644
--- a/docs/high-performance/message-queue/rabbitmq-questions.md
+++ b/docs/high-performance/message-queue/rabbitmq-questions.md
@@ -1,5 +1,6 @@
---
title: RabbitMQ常见问题总结
+description: RabbitMQ 是一个在 AMQP(Advanced Message Queuing Protocol )基础上实现的,可复用的企业消息系统。它可以用于大型软件系统各个模块之间的高效通信,支持高并发,支持可扩展。它支持多种客户端如:Python、Ruby、.NET、Java、JMS、C、PHP、ActionScript、XMPP、STOMP等,支持AJAX,持久化,用于在分布式系统中存储转发消息,在易用性、扩展性、高可用性等方面表现不俗。
category: 高性能
tag:
- 消息队列
@@ -7,9 +8,6 @@ head:
- - meta
- name: keywords
content: RabbitMQ,AMQP,Broker,Exchange,优先级队列,延迟队列
- - - meta
- - name: description
- content: RabbitMQ 是一个在 AMQP(Advanced Message Queuing Protocol )基础上实现的,可复用的企业消息系统。它可以用于大型软件系统各个模块之间的高效通信,支持高并发,支持可扩展。它支持多种客户端如:Python、Ruby、.NET、Java、JMS、C、PHP、ActionScript、XMPP、STOMP等,支持AJAX,持久化,用于在分布式系统中存储转发消息,在易用性、扩展性、高可用性等方面表现不俗。
---
> 本篇文章由 JavaGuide 收集自网络,原出处不明。
diff --git a/docs/high-performance/message-queue/rocketmq-questions.md b/docs/high-performance/message-queue/rocketmq-questions.md
index 35f24726812..bed4b015df8 100644
--- a/docs/high-performance/message-queue/rocketmq-questions.md
+++ b/docs/high-performance/message-queue/rocketmq-questions.md
@@ -1,5 +1,6 @@
---
title: RocketMQ常见问题总结
+description: "RocketMQ常见问题总结:围绕高性能与性能优化实践梳理关键概念、常见问题与实践要点,帮助你高效学习与备战面试。"
category: 高性能
tag:
- RocketMQ
@@ -19,7 +20,7 @@ tag:
### 消息队列为什么会出现?
-消息队``列算是作为后端程序员的一个必备技能吧,因为**分布式应用必定涉及到各个系统之间的通信问题**,这个时候消息队列也应运而生了。可以说分布式的产生是消息队列的基础,而分布式怕是一个很古老的概念了吧,所以消息队列也是一个很古老的中间件了。
+消息队列算是作为后端程序员的一个必备技能吧,因为**分布式应用必定涉及到各个系统之间的通信问题**,这个时候消息队列也应运而生了。可以说分布式的产生是消息队列的基础,而分布式怕是一个很古老的概念了吧,所以消息队列也是一个很古老的中间件了。
### 消息队列能用来干什么?
diff --git a/docs/high-performance/read-and-write-separation-and-library-subtable.md b/docs/high-performance/read-and-write-separation-and-library-subtable.md
index da25f066e9e..0fd6fb6de08 100644
--- a/docs/high-performance/read-and-write-separation-and-library-subtable.md
+++ b/docs/high-performance/read-and-write-separation-and-library-subtable.md
@@ -1,13 +1,11 @@
---
title: 读写分离和分库分表详解
+description: 读写分离主要是为了将对数据库的读写操作分散到不同的数据库节点上。 这样的话,就能够小幅提升写性能,大幅提升读性能。 读写分离基于主从复制,MySQL 主从复制是依赖于 binlog 。分库就是将数据库中的数据分散到不同的数据库上。分表就是对单表的数据进行拆分,可以是垂直拆分,也可以是水平拆分。引入分库分表之后,需要系统解决事务、分布式 id、无法 join 操作问题。
category: 高性能
head:
- - meta
- name: keywords
content: 读写分离,分库分表,主从复制
- - - meta
- - name: description
- content: 读写分离主要是为了将对数据库的读写操作分散到不同的数据库节点上。 这样的话,就能够小幅提升写性能,大幅提升读性能。 读写分离基于主从复制,MySQL 主从复制是依赖于 binlog 。分库就是将数据库中的数据分散到不同的数据库上。分表就是对单表的数据进行拆分,可以是垂直拆分,也可以是水平拆分。引入分库分表之后,需要系统解决事务、分布式 id、无法 join 操作问题。
---
## 读写分离
diff --git a/docs/high-performance/sql-optimization.md b/docs/high-performance/sql-optimization.md
index 9aa94dfd528..0f3accde37e 100644
--- a/docs/high-performance/sql-optimization.md
+++ b/docs/high-performance/sql-optimization.md
@@ -1,13 +1,11 @@
---
title: 常见SQL优化手段总结(付费)
+description: SQL 优化是一个大家都比较关注的热门话题,无论你在面试,还是工作中,都很有可能会遇到。如果某天你负责的某个线上接口,出现了性能问题,需要做优化。那么你首先想到的很有可能是优化 SQL 优化,因为它的改造成本相对于代码来说也要小得多。
category: 高性能
head:
- - meta
- name: keywords
content: 分页优化,索引,Show Profile,慢 SQL
- - - meta
- - name: description
- content: SQL 优化是一个大家都比较关注的热门话题,无论你在面试,还是工作中,都很有可能会遇到。如果某天你负责的某个线上接口,出现了性能问题,需要做优化。那么你首先想到的很有可能是优化 SQL 优化,因为它的改造成本相对于代码来说也要小得多。
---
**常见 SQL 优化手段总结** 相关的内容为我的[知识星球](https://javaguide.cn/about-the-author/zhishixingqiu-two-years.html)(点击链接即可查看详细介绍以及加入方法)专属内容,已经整理到了[《Java 面试指北》](https://javaguide.cn/zhuanlan/java-mian-shi-zhi-bei.html)中。
@@ -15,5 +13,3 @@ head:

-
-
diff --git a/docs/high-quality-technical-articles/readme.md b/docs/high-quality-technical-articles/README.md
similarity index 100%
rename from docs/high-quality-technical-articles/readme.md
rename to docs/high-quality-technical-articles/README.md
diff --git a/docs/high-quality-technical-articles/advanced-programmer/20-bad-habits-of-bad-programmers.md b/docs/high-quality-technical-articles/advanced-programmer/20-bad-habits-of-bad-programmers.md
index 59191347757..e6dfd10f5d6 100644
--- a/docs/high-quality-technical-articles/advanced-programmer/20-bad-habits-of-bad-programmers.md
+++ b/docs/high-quality-technical-articles/advanced-programmer/20-bad-habits-of-bad-programmers.md
@@ -1,9 +1,14 @@
---
title: 糟糕程序员的 20 个坏习惯
+description: "糟糕程序员的 20 个坏习惯:围绕技术知识与面试总结梳理关键概念、常见问题与实践要点,帮助你高效学习与备战面试。"
category: 技术文章精选集
author: Kaito
tag:
- 练级攻略
+head:
+ - - meta
+ - name: keywords
+ content: 程序员坏习惯,编程规范,代码注释,技术文档,团队协作,代码提交,职业素养,编程修养
---
> **推荐语**:Kaito 大佬的一篇文章,很实用的建议!
diff --git a/docs/high-quality-technical-articles/advanced-programmer/meituan-three-year-summary-lesson-10.md b/docs/high-quality-technical-articles/advanced-programmer/meituan-three-year-summary-lesson-10.md
index 3ae2a5eac42..e43e0932b11 100644
--- a/docs/high-quality-technical-articles/advanced-programmer/meituan-three-year-summary-lesson-10.md
+++ b/docs/high-quality-technical-articles/advanced-programmer/meituan-three-year-summary-lesson-10.md
@@ -1,9 +1,14 @@
---
title: 美团三年,总结的10条血泪教训
+description: "美团三年,总结的10条血泪教训:围绕技术知识与面试总结梳理关键概念、常见问题与实践要点,帮助你高效学习与备战面试。"
category: 技术文章精选集
author: CityDreamer部落
tag:
- 练级攻略
+head:
+ - - meta
+ - name: keywords
+ content: 美团工作经验,职场成长,结构化思考,数据思维,职场沟通,金字塔原理,工作效率,职业发展
---
> **推荐语**:作者用了很多生动的例子和故事展示了自己在美团的成长和感悟,看了之后受益颇多!
@@ -25,7 +30,7 @@ tag:
>
> **原文地址**:
-在美团的三年多时光,如同一部悠长的交响曲,高高低低,而今离开已有一段时间。闲暇之余,梳理了三年多的收获与感慨,总结成 10 条,既是对过去一段时光的的一个深情回眸,也是对未来之路的一份期许。
+在美团的三年多时光,如同一部悠长的交响曲,高高低低,而今离开已有一段时间。闲暇之余,梳理了三年多的收获与感慨,总结成 10 条,既是对过去一段时光的一个深情回眸,也是对未来之路的一份期许。
倘若一些感悟能为刚步入职场的年轻人,或是刚在职业生涯中崭露头角的后起之秀,带来一点点启示与帮助,也是莫大的荣幸。
diff --git a/docs/high-quality-technical-articles/advanced-programmer/programmer-quickly-learn-new-technology.md b/docs/high-quality-technical-articles/advanced-programmer/programmer-quickly-learn-new-technology.md
index 3cd553a182e..13827588777 100644
--- a/docs/high-quality-technical-articles/advanced-programmer/programmer-quickly-learn-new-technology.md
+++ b/docs/high-quality-technical-articles/advanced-programmer/programmer-quickly-learn-new-technology.md
@@ -1,8 +1,13 @@
---
title: 程序员如何快速学习新技术
+description: "程序员如何快速学习新技术:围绕技术知识与面试总结梳理关键概念、常见问题与实践要点,帮助你高效学习与备战面试。"
category: 技术文章精选集
tag:
- 练级攻略
+head:
+ - - meta
+ - name: keywords
+ content: 程序员学习,技术学习方法,快速学习,官方文档,技术面试,八股文,知行合一,学习技巧
---
> **推荐语**:这是[《Java 面试指北》](https://javaguide.cn/zhuanlan/java-mian-shi-zhi-bei.html)练级攻略篇中的一篇文章,分享了我对于如何快速学习一门新技术的看法。
diff --git a/docs/high-quality-technical-articles/advanced-programmer/seven-tips-for-becoming-an-advanced-programmer.md b/docs/high-quality-technical-articles/advanced-programmer/seven-tips-for-becoming-an-advanced-programmer.md
index ef273f47b5c..b57e8e88664 100644
--- a/docs/high-quality-technical-articles/advanced-programmer/seven-tips-for-becoming-an-advanced-programmer.md
+++ b/docs/high-quality-technical-articles/advanced-programmer/seven-tips-for-becoming-an-advanced-programmer.md
@@ -1,9 +1,14 @@
---
title: 给想成长为高级别开发同学的七条建议
+description: "给想成长为高级别开发同学的七条建议:围绕技术知识与面试总结梳理关键概念、常见问题与实践要点,帮助你高效学习与备战面试。"
category: 技术文章精选集
author: Kaito
tag:
- 练级攻略
+head:
+ - - meta
+ - name: keywords
+ content: 程序员成长,高级开发,需求评审,技术内功,性能优化,线上问题排查,归纳总结,职业发展
---
> **推荐语**:普通程序员要想成长为高级程序员甚至是专家等更高级别,应该注意在哪些方面注意加强?开发内功修炼号主飞哥在这篇文章中就给出了七条实用的建议。
diff --git a/docs/high-quality-technical-articles/advanced-programmer/ten-years-of-dachang-growth-road.md b/docs/high-quality-technical-articles/advanced-programmer/ten-years-of-dachang-growth-road.md
index 045c2bfed66..1cb1bd2c706 100644
--- a/docs/high-quality-technical-articles/advanced-programmer/ten-years-of-dachang-growth-road.md
+++ b/docs/high-quality-technical-articles/advanced-programmer/ten-years-of-dachang-growth-road.md
@@ -1,9 +1,14 @@
---
title: 十年大厂成长之路
+description: "十年大厂成长之路:围绕技术知识与面试总结梳理关键概念、常见问题与实践要点,帮助你高效学习与备战面试。"
category: 技术文章精选集
author: CodingBetterLife
tag:
- 练级攻略
+head:
+ - - meta
+ - name: keywords
+ content: 大厂成长,程序员职业发展,技术专家,技术管理,转岗跳槽,职场选择,十年规划,技术领导
---
> **推荐语**:这篇文章的作者有着丰富的工作经验,曾在大厂工作了 12 年。结合自己走过的弯路和接触过的优秀技术人,他总结出了一些对于个人成长具有普遍指导意义的经验和特质。
diff --git a/docs/high-quality-technical-articles/advanced-programmer/the-growth-strategy-of-the-technological-giant.md b/docs/high-quality-technical-articles/advanced-programmer/the-growth-strategy-of-the-technological-giant.md
index 20aed477d3a..19084abe803 100644
--- a/docs/high-quality-technical-articles/advanced-programmer/the-growth-strategy-of-the-technological-giant.md
+++ b/docs/high-quality-technical-articles/advanced-programmer/the-growth-strategy-of-the-technological-giant.md
@@ -1,9 +1,14 @@
---
title: 程序员的技术成长战略
+description: "程序员的技术成长战略:围绕技术知识与面试总结梳理关键概念、常见问题与实践要点,帮助你高效学习与备战面试。"
category: 技术文章精选集
author: 波波微课
tag:
- 练级攻略
+head:
+ - - meta
+ - name: keywords
+ content: 技术成长战略,程序员成长,学习金字塔,刻意练习,技术大牛,职业规划,十年规划,持续产出
---
> **推荐语**:波波老师的一篇文章,写的非常好,不光是对技术成长有帮助,其他领域也是同样适用的!建议反复阅读,形成一套自己的技术成长策略。
diff --git a/docs/high-quality-technical-articles/advanced-programmer/thinking-about-technology-and-business-after-five-years-of-work.md b/docs/high-quality-technical-articles/advanced-programmer/thinking-about-technology-and-business-after-five-years-of-work.md
index d32f586b449..5933e7005bf 100644
--- a/docs/high-quality-technical-articles/advanced-programmer/thinking-about-technology-and-business-after-five-years-of-work.md
+++ b/docs/high-quality-technical-articles/advanced-programmer/thinking-about-technology-and-business-after-five-years-of-work.md
@@ -1,9 +1,14 @@
---
title: 工作五年之后,对技术和业务的思考
+description: "工作五年之后,对技术和业务的思考:围绕技术知识与面试总结梳理关键概念、常见问题与实践要点,帮助你高效学习与备战面试。"
category: 技术文章精选集
author: 知了一笑
tag:
- 练级攻略
+head:
+ - - meta
+ - name: keywords
+ content: 程序员五年,技术与业务,职业发展,能力积累,业务思维,技术深度,职场选择,二八原则
---
> **推荐语**:这是我在两年前看到的一篇对我触动比较深的文章。确实要学会适应变化,并积累能力。积累解决问题的能力,优化思考方式,拓宽自己的认知。
diff --git a/docs/high-quality-technical-articles/interview/how-to-examine-the-technical-ability-of-programmers-in-the-first-test-of-technology.md b/docs/high-quality-technical-articles/interview/how-to-examine-the-technical-ability-of-programmers-in-the-first-test-of-technology.md
index f96a20fec16..fee06e512ea 100644
--- a/docs/high-quality-technical-articles/interview/how-to-examine-the-technical-ability-of-programmers-in-the-first-test-of-technology.md
+++ b/docs/high-quality-technical-articles/interview/how-to-examine-the-technical-ability-of-programmers-in-the-first-test-of-technology.md
@@ -1,9 +1,14 @@
---
title: 如何在技术初试中考察程序员的技术能力
+description: "如何在技术初试中考察程序员的技术能力:围绕技术知识与面试总结梳理关键概念、常见问题与实践要点,帮助你高效学习与备战面试。"
category: 技术文章精选集
author: 琴水玉
tag:
- 面试
+head:
+ - - meta
+ - name: keywords
+ content: 技术面试,面试官技巧,技术考察,面试方法,技术基础,项目经历考察,面试题库,技术深度
---
> **推荐语**:从面试官和面试者两个角度探讨了技术面试!非常不错!
diff --git a/docs/high-quality-technical-articles/interview/my-personal-experience-in-2021.md b/docs/high-quality-technical-articles/interview/my-personal-experience-in-2021.md
index f7d62085553..b3f64e7d6c7 100644
--- a/docs/high-quality-technical-articles/interview/my-personal-experience-in-2021.md
+++ b/docs/high-quality-technical-articles/interview/my-personal-experience-in-2021.md
@@ -1,9 +1,14 @@
---
title: 校招进入飞书的个人经验
+description: "校招进入飞书的个人经验:围绕技术知识与面试总结梳理关键概念、常见问题与实践要点,帮助你高效学习与备战面试。"
category: 技术文章精选集
author: 月色真美
tag:
- 面试
+head:
+ - - meta
+ - name: keywords
+ content: 字节跳动面试,飞书校招,C++面试,春招实习,日常实习,暑期实习,面试技巧,算法刷题
---
> **推荐语**:这篇文章的作者校招最终去了飞书做开发。在这篇文章中,他分享了自己的校招经历以及个人经验。
diff --git a/docs/high-quality-technical-articles/interview/screen-candidates-for-packaging.md b/docs/high-quality-technical-articles/interview/screen-candidates-for-packaging.md
index 5b0ff739b34..b39fa6520f4 100644
--- a/docs/high-quality-technical-articles/interview/screen-candidates-for-packaging.md
+++ b/docs/high-quality-technical-articles/interview/screen-candidates-for-packaging.md
@@ -1,9 +1,14 @@
---
title: 如何甄别应聘者的包装程度
+description: "如何甄别应聘者的包装程度:围绕技术知识与面试总结梳理关键概念、常见问题与实践要点,帮助你高效学习与备战面试。"
category: 技术文章精选集
author: Coody
tag:
- 面试
+head:
+ - - meta
+ - name: keywords
+ content: 简历包装,面试官视角,简历甄别,技术面试,培训机构,项目经验,技术深度,面试技巧
---
> **推荐语**:经常听到培训班待过的朋友给我说他们的老师是怎么教他们“包装”自己的,不光是培训班,我认识的很多朋友也都会在面试之前“包装”一下自己,所以这个现象是普遍存在的。但是面试官也不都是傻子,通过下面这篇文章来看看面试官是如何甄别应聘者的包装程度。
diff --git a/docs/high-quality-technical-articles/interview/some-secrets-about-alibaba-interview.md b/docs/high-quality-technical-articles/interview/some-secrets-about-alibaba-interview.md
index 175efc3da14..82b3360ddf9 100644
--- a/docs/high-quality-technical-articles/interview/some-secrets-about-alibaba-interview.md
+++ b/docs/high-quality-technical-articles/interview/some-secrets-about-alibaba-interview.md
@@ -1,9 +1,14 @@
---
title: 阿里技术面试的一些秘密
+description: "阿里技术面试的一些秘密:围绕技术知识与面试总结梳理关键概念、常见问题与实践要点,帮助你高效学习与备战面试。"
category: 技术文章精选集
author: 龙叔
tag:
- 面试
+head:
+ - - meta
+ - name: keywords
+ content: 阿里面试,技术面试,简历筛选,面试技巧,基础知识,动手能力,八股文,校招面试
---
> **推荐语**:详细介绍了求职者在面试中应该具备哪些能力才会有更大概率脱颖而出。
diff --git a/docs/high-quality-technical-articles/interview/summary-of-spring-recruitment.md b/docs/high-quality-technical-articles/interview/summary-of-spring-recruitment.md
index 474434645a9..4345532f3cc 100644
--- a/docs/high-quality-technical-articles/interview/summary-of-spring-recruitment.md
+++ b/docs/high-quality-technical-articles/interview/summary-of-spring-recruitment.md
@@ -1,9 +1,14 @@
---
title: 普通人的春招总结(阿里、腾讯offer)
+description: "普通人的春招总结(阿里、腾讯offer):围绕技术知识与面试总结梳理关键概念、常见问题与实践要点,帮助你高效学习与备战面试。"
category: 技术文章精选集
author: 钟期既遇
tag:
- 面试
+head:
+ - - meta
+ - name: keywords
+ content: 春招经验,阿里面试,腾讯面试,Java学习路线,面试准备,项目经验,算法刷题,双非本科
---
> **推荐语**:牛客网热帖,写的很全面!暑期实习,投了阿里、腾讯、字节,拿到了阿里和腾讯的 offer。
diff --git a/docs/high-quality-technical-articles/interview/technical-preliminary-preparation.md b/docs/high-quality-technical-articles/interview/technical-preliminary-preparation.md
index ae4e95b1818..60e4f0363d8 100644
--- a/docs/high-quality-technical-articles/interview/technical-preliminary-preparation.md
+++ b/docs/high-quality-technical-articles/interview/technical-preliminary-preparation.md
@@ -1,9 +1,14 @@
---
title: 从面试官和候选者的角度谈如何准备技术初试
+description: "从面试官和候选者的角度谈如何准备技术初试:围绕技术知识与面试总结梳理关键概念、常见问题与实践要点,帮助你高效学习与备战面试。"
category: 技术文章精选集
author: 琴水玉
tag:
- 面试
+head:
+ - - meta
+ - name: keywords
+ content: 技术面试准备,面试官视角,候选人视角,技术基础,业务考察,面试技巧,技术深度广度,面试方法论
---
> **推荐语**:从面试官和面试者两个角度探讨了技术面试!非常不错!
diff --git a/docs/high-quality-technical-articles/interview/the-experience-and-thinking-of-an-interview-experienced-by-an-older-programmer.md b/docs/high-quality-technical-articles/interview/the-experience-and-thinking-of-an-interview-experienced-by-an-older-programmer.md
index 8aaa1d65aca..65ccd73d2aa 100644
--- a/docs/high-quality-technical-articles/interview/the-experience-and-thinking-of-an-interview-experienced-by-an-older-programmer.md
+++ b/docs/high-quality-technical-articles/interview/the-experience-and-thinking-of-an-interview-experienced-by-an-older-programmer.md
@@ -1,9 +1,14 @@
---
title: 一位大龄程序员所经历的面试的历炼和思考
+description: "一位大龄程序员所经历的面试的历炼和思考:围绕技术知识与面试总结梳理关键概念、常见问题与实践要点,帮助你高效学习与备战面试。"
category: 技术文章精选集
author: 琴水玉
tag:
- 面试
+head:
+ - - meta
+ - name: keywords
+ content: 大龄程序员面试,面试准备,简历优化,技术面试,面试心态,职业规划,面试技巧,技术原理
---
> **推荐语**:本文的作者,今年 36 岁,已有 8 年 JAVA 开发经验。在阿里云三年半,有赞四年半,已是标准的大龄程序员了。在这篇文章中,作者给出了一些关于面试和个人能力提升的一些小建议,非常实用!
diff --git a/docs/high-quality-technical-articles/interview/the-experience-of-get-offer-from-over-20-big-companies.md b/docs/high-quality-technical-articles/interview/the-experience-of-get-offer-from-over-20-big-companies.md
index 4cc38409fb7..5b307bae671 100644
--- a/docs/high-quality-technical-articles/interview/the-experience-of-get-offer-from-over-20-big-companies.md
+++ b/docs/high-quality-technical-articles/interview/the-experience-of-get-offer-from-over-20-big-companies.md
@@ -1,9 +1,14 @@
---
title: 斩获 20+ 大厂 offer 的面试经验分享
+description: "斩获 20+ 大厂 offer 的面试经验分享:围绕技术知识与面试总结梳理关键概念、常见问题与实践要点,帮助你高效学习与备战面试。"
category: 技术文章精选集
author: 业余码农
tag:
- 面试
+head:
+ - - meta
+ - name: keywords
+ content: 大厂面试,面试技巧,自我介绍,项目经历,技术面试,编码能力,HR面试,offer选择
---
> **推荐语**:很实用的面试经验分享!
diff --git a/docs/high-quality-technical-articles/personal-experience/8-years-programmer-work-summary.md b/docs/high-quality-technical-articles/personal-experience/8-years-programmer-work-summary.md
index 0af5480b58e..9c44705ef3a 100644
--- a/docs/high-quality-technical-articles/personal-experience/8-years-programmer-work-summary.md
+++ b/docs/high-quality-technical-articles/personal-experience/8-years-programmer-work-summary.md
@@ -1,9 +1,14 @@
---
title: 一个中科大差生的 8 年程序员工作总结
+description: "一个中科大差生的 8 年程序员工作总结:围绕技术知识与面试总结梳理关键概念、常见问题与实践要点,帮助你高效学习与备战面试。"
category: 技术文章精选集
author: 陈小房
tag:
- 个人经历
+head:
+ - - meta
+ - name: keywords
+ content: 中科大程序员,8年工作总结,航天研究所,华为工作,职业发展,买房经验,技术成长,人生复盘
---
> **推荐语**:这篇文章讲述了一位中科大的朋友 8 年的经历:从 2013 年毕业之后加入上海航天 x 院某卫星研究所,再到入职华为,从华为离职。除了丰富的经历之外,作者在文章还给出了很多自己对于工作/生活的思考。我觉得非常受用!我在这里,向这位作者表达一下衷心的感谢。
diff --git a/docs/high-quality-technical-articles/personal-experience/four-year-work-in-tencent-summary.md b/docs/high-quality-technical-articles/personal-experience/four-year-work-in-tencent-summary.md
index 4630bff560e..0860b2be859 100644
--- a/docs/high-quality-technical-articles/personal-experience/four-year-work-in-tencent-summary.md
+++ b/docs/high-quality-technical-articles/personal-experience/four-year-work-in-tencent-summary.md
@@ -1,9 +1,14 @@
---
title: 从校招入职腾讯的四年工作总结
+description: "从校招入职腾讯的四年工作总结:围绕技术知识与面试总结梳理关键概念、常见问题与实践要点,帮助你高效学习与备战面试。"
category: 技术文章精选集
author: pioneeryi
tag:
- 个人经历
+head:
+ - - meta
+ - name: keywords
+ content: 腾讯工作经验,四年总结,绩效考核,EPC度量,嫡系文化,职业发展,技术成长,互联网职场
---
程序员是一个流动性很大的职业,经常会有新面孔的到来,也经常会有老面孔的离开,有主动离开的,也有被动离职的。
@@ -74,7 +79,7 @@ PS:还好以前有奖杯,不然一点念想都没了。(现在腾讯似乎
但另一方面,后来我负责了团队内很重要的事情,应该是中心内都算很重要的事,我独自负责一个方向,直接向总监汇报,似乎又有点像。
-网上也有其他说法,一针见血,是不是嫡系,就看钱到不到位,这么说也有道理。我在 7 级时,就发了股票,自我感觉,还是不错的。我当时以为不出意外的话,我以后的钱途和发展是不是就会一帆风顺。不出意外就出了意外,第二年,EPC 不达预期,部门总经理和总监都被换了,中心来了一个新的的总监。
+网上也有其他说法,一针见血,是不是嫡系,就看钱到不到位,这么说也有道理。我在 7 级时,就发了股票,自我感觉,还是不错的。我当时以为不出意外的话,我以后的钱途和发展是不是就会一帆风顺。不出意外就出了意外,第二年,EPC 不达预期,部门总经理和总监都被换了,中心来了一个新的总监。
好吧,又要重新建立信任了。再到后来,是不是嫡系已经不重要了,因为大环境不好,又加上裁员,大家主动的被动的差不多都走了。
diff --git a/docs/high-quality-technical-articles/personal-experience/huawei-od-275-days.md b/docs/high-quality-technical-articles/personal-experience/huawei-od-275-days.md
index 419f364adcf..e546e03a54d 100644
--- a/docs/high-quality-technical-articles/personal-experience/huawei-od-275-days.md
+++ b/docs/high-quality-technical-articles/personal-experience/huawei-od-275-days.md
@@ -1,8 +1,13 @@
---
title: 华为 OD 275 天后,我进了腾讯!
+description: "华为 OD 275 天后,我进了腾讯!:围绕技术知识与面试总结梳理关键概念、常见问题与实践要点,帮助你高效学习与备战面试。"
category: 技术文章精选集
tag:
- 个人经历
+head:
+ - - meta
+ - name: keywords
+ content: 华为OD,腾讯面试,大数据开发,外包经历,面试经验,Java面试,职业发展,大厂面试
---
> **推荐语**:一位朋友的华为 OD 工作经历以及腾讯面试经历分享,内容很不错。
diff --git a/docs/high-quality-technical-articles/personal-experience/two-years-of-back-end-develop--experience-in-didi-and-toutiao.md b/docs/high-quality-technical-articles/personal-experience/two-years-of-back-end-develop--experience-in-didi-and-toutiao.md
index 5b6c47be7c7..18e6bd6e05a 100644
--- a/docs/high-quality-technical-articles/personal-experience/two-years-of-back-end-develop--experience-in-didi-and-toutiao.md
+++ b/docs/high-quality-technical-articles/personal-experience/two-years-of-back-end-develop--experience-in-didi-and-toutiao.md
@@ -1,8 +1,13 @@
---
title: 滴滴和头条两年后端工作经验分享
+description: "滴滴和头条两年后端工作经验分享:围绕技术知识与面试总结梳理关键概念、常见问题与实践要点,帮助你高效学习与备战面试。"
category: 技术文章精选集
tag:
- 个人经历
+head:
+ - - meta
+ - name: keywords
+ content: 滴滴工作经验,头条工作经验,后端开发,技术成长,职场经验,深入思考,总结沉淀,主动承担
---
> **推荐语**:很实用的工作经验分享,看完之后十分受用!
diff --git a/docs/high-quality-technical-articles/programmer/efficient-book-publishing-and-practice-guide.md b/docs/high-quality-technical-articles/programmer/efficient-book-publishing-and-practice-guide.md
index fad7bede853..17d17c638cf 100644
--- a/docs/high-quality-technical-articles/programmer/efficient-book-publishing-and-practice-guide.md
+++ b/docs/high-quality-technical-articles/programmer/efficient-book-publishing-and-practice-guide.md
@@ -1,9 +1,14 @@
---
title: 程序员高效出书避坑和实践指南
+description: "程序员高效出书避坑和实践指南:围绕技术知识与面试总结梳理关键概念、常见问题与实践要点,帮助你高效学习与备战面试。"
category: 技术文章精选集
author: hsm_computer
tag:
- 程序员
+head:
+ - - meta
+ - name: keywords
+ content: 程序员出书,出书避坑,稿酬收益,出版社编辑,图书公司,案例书写作,版权问题,技术写作
---
> **推荐语**:详细介绍了程序员出书的一些常见问题,强烈建议有出书想法的朋友看看这篇文章。
diff --git a/docs/high-quality-technical-articles/programmer/high-value-certifications-for-programmers.md b/docs/high-quality-technical-articles/programmer/high-value-certifications-for-programmers.md
index 7ea9f7932e0..b91b220e5f6 100644
--- a/docs/high-quality-technical-articles/programmer/high-value-certifications-for-programmers.md
+++ b/docs/high-quality-technical-articles/programmer/high-value-certifications-for-programmers.md
@@ -1,8 +1,13 @@
---
title: 程序员最该拿的几种高含金量证书
+description: "程序员最该拿的几种高含金量证书:围绕技术知识与面试总结梳理关键概念、常见问题与实践要点,帮助你高效学习与备战面试。"
category: 技术文章精选集
tag:
- 程序员
+head:
+ - - meta
+ - name: keywords
+ content: 程序员证书,软考,PMP认证,AWS认证,阿里云认证,华为认证,OCP认证,Kubernetes认证,职业资格证书
---
证书是能有效证明自己能力的好东西,它就是你实力的象征。在短短的面试时间内,证书可以为你加不少分。通过考证来提升自己,是一种性价比很高的办法。不过,相比金融、建筑、医疗等行业,IT 行业的职业资格证书并没有那么多。
diff --git a/docs/high-quality-technical-articles/programmer/how-do-programmers-publish-a-technical-book.md b/docs/high-quality-technical-articles/programmer/how-do-programmers-publish-a-technical-book.md
index 99dae8f9d72..3fcc17a191c 100644
--- a/docs/high-quality-technical-articles/programmer/how-do-programmers-publish-a-technical-book.md
+++ b/docs/high-quality-technical-articles/programmer/how-do-programmers-publish-a-technical-book.md
@@ -1,9 +1,14 @@
---
title: 程序员怎样出版一本技术书
+description: "程序员怎样出版一本技术书:围绕技术知识与面试总结梳理关键概念、常见问题与实践要点,帮助你高效学习与备战面试。"
category: 技术文章精选集
author: hsm_computer
tag:
- 程序员
+head:
+ - - meta
+ - name: keywords
+ content: 程序员出书,技术书籍出版,出版社合作,图书公司,写书技巧,稿酬收益,技术写作,畅销书
---
> **推荐语**:详细介绍了程序员应该如何从头开始出一本自己的书籍。
diff --git a/docs/high-quality-technical-articles/work/32-tips-improving-career.md b/docs/high-quality-technical-articles/work/32-tips-improving-career.md
index 78b0e66b122..2f54b7c2bf4 100644
--- a/docs/high-quality-technical-articles/work/32-tips-improving-career.md
+++ b/docs/high-quality-technical-articles/work/32-tips-improving-career.md
@@ -1,8 +1,13 @@
---
title: 32条总结教你提升职场经验
+description: "32条总结教你提升职场经验:围绕技术知识与面试总结梳理关键概念、常见问题与实践要点,帮助你高效学习与备战面试。"
category: 技术文章精选集
tag:
- 工作
+head:
+ - - meta
+ - name: keywords
+ content: 职场经验,程序员成长,向上管理,情绪控制,Leader能力,职业发展,阿里开发者,职场技巧
---
> **推荐语**:阿里开发者的一篇职场经验的分享。
diff --git a/docs/high-quality-technical-articles/work/employee-performance.md b/docs/high-quality-technical-articles/work/employee-performance.md
index af22114fb04..41d6eb8223a 100644
--- a/docs/high-quality-technical-articles/work/employee-performance.md
+++ b/docs/high-quality-technical-articles/work/employee-performance.md
@@ -1,8 +1,13 @@
---
title: 聊聊大厂的绩效考核
+description: "聊聊大厂的绩效考核:围绕技术知识与面试总结梳理关键概念、常见问题与实践要点,帮助你高效学习与备战面试。"
category: 技术文章精选集
tag:
- 工作
+head:
+ - - meta
+ - name: keywords
+ content: 大厂绩效,绩效考核,KPI,OKR,271制度,年终奖,职级晋升,向上管理
---
> **内容概览**:
diff --git a/docs/high-quality-technical-articles/work/get-into-work-mode-quickly-when-you-join-a-company.md b/docs/high-quality-technical-articles/work/get-into-work-mode-quickly-when-you-join-a-company.md
index 72c672a5f92..ce22412ea15 100644
--- a/docs/high-quality-technical-articles/work/get-into-work-mode-quickly-when-you-join-a-company.md
+++ b/docs/high-quality-technical-articles/work/get-into-work-mode-quickly-when-you-join-a-company.md
@@ -1,8 +1,13 @@
---
title: 新入职一家公司如何快速进入工作状态
+description: "新入职一家公司如何快速进入工作状态:围绕技术知识与面试总结梳理关键概念、常见问题与实践要点,帮助你高效学习与备战面试。"
category: 技术文章精选集
tag:
- 工作
+head:
+ - - meta
+ - name: keywords
+ content: 新入职,快速融入,工作状态,业务了解,技术熟悉,团队协作,跳槽适应,程序员入职
---
> **推荐语**:强烈建议每一位即将入职/在职的小伙伴看看这篇文章,看完之后可以帮助你少踩很多坑。整篇文章逻辑清晰,内容全面!
diff --git a/docs/home.md b/docs/home.md
index c504492d99d..e06d166ef8c 100644
--- a/docs/home.md
+++ b/docs/home.md
@@ -1,13 +1,22 @@
---
icon: creative
-title: JavaGuide(Java学习&面试指南)
+title: JavaGuide(Java 面试 & 后端通用面试指南)
+description: Java 面试指南(Java 八股文/面试题总结):覆盖 Java 基础、集合、并发、JVM、Spring、MySQL、Redis、系统设计与分布式等核心知识,适用于校招/社招后端面试复习。
+head:
+ - - meta
+ - name: keywords
+ content: Java面试,Java面试指南,Java八股文,Java面试题,Java基础面试,JVM面试,并发面试,线程池面试,Spring面试,MySQL面试,Redis面试,系统设计面试,分布式面试,后端面试
---
::: tip 友情提示
-- **面试专版**:准备 Java 面试的小伙伴可以考虑面试专版:**[《Java 面试指北 》](./zhuanlan/java-mian-shi-zhi-bei.md)** (质量很高,专为面试打造,配合 JavaGuide 食用)。
-- **知识星球**:专属面试小册/一对一交流/简历修改/专属求职指南,欢迎加入 **[JavaGuide 知识星球](./about-the-author/zhishixingqiu-two-years.md)**(点击链接即可查看星球的详细介绍,一定确定自己真的需要再加入)。
-- **使用建议** :有水平的面试官都是顺着项目经历挖掘技术问题。一定不要死记硬背技术八股文!详细的学习建议请参考:[JavaGuide 使用建议](./javaguide/use-suggestion.md)。
+- **实战项目**:
+ - [⭐AI 智能面试辅助平台 + RAG 知识库](https://javaguide.cn/zhuanlan/interview-guide.html):基于 Spring Boot 4.0 + Java 21 + Spring AI 2.0 开发。非常适合作为学习和简历项目,学习门槛低,帮助提升求职竞争力,是主打就业的实战项目。
+ - [手写 RPC 框架](https://javaguide.cn/zhuanlan/handwritten-rpc-framework.html):从零开始基于 Netty+Kyro+Zookeeper 实现一个简易的 RPC 框架。麻雀虽小五脏俱全,项目代码注释详细,结构清晰。
+- **面试资料补充**:
+ - [《Java 面试指北》](https://javaguide.cn/zhuanlan/java-mian-shi-zhi-bei.html):四年打磨,和 JavaGuide 开源版的内容互补,带你从零开始系统准备后端面试!
+ - [《后端面试高频系统设计&场景题》](https://javaguide.cn/zhuanlan/back-end-interview-high-frequency-system-design-and-scenario-questions.html):30+ 道高频系统设计和场景面试,助你应对当下中大厂面试趋势。
+- **使用建议** :有水平的面试官都是顺着项目经历挖掘技术问题。一定不要死记硬背技术八股文!详细的学习建议请参考:[JavaGuide 使用建议](https://javaguide.cn/javaguide/use-suggestion.html)。
- **求个 Star**:如果觉得 JavaGuide 的内容对你有帮助的话,还请点个免费的 Star,这是对我最大的鼓励,感谢各位一起同行,共勉!传送门:[GitHub](https://github.com/Snailclimb/JavaGuide) | [Gitee](https://gitee.com/SnailClimb/JavaGuide)。
- **转载须知**:以下所有文章如非文首说明为转载皆为 JavaGuide 原创,转载请在文首注明出处。如发现恶意抄袭/搬运,会动用法律武器维护自己的权益。让我们一起维护一个良好的技术创作环境!
@@ -72,7 +81,7 @@ title: JavaGuide(Java学习&面试指南)
**重要知识点详解**:
-- [乐观锁和悲观锁详解](./java/concurrent/jmm.md)
+- [乐观锁和悲观锁详解](./java/concurrent/optimistic-lock-and-pessimistic-lock.md)
- [CAS 详解](./java/concurrent/cas.md)
- [JMM(Java 内存模型)详解](./java/concurrent/jmm.md)
- **线程池**:[Java 线程池详解](./java/concurrent/java-thread-pool-summary.md)、[Java 线程池最佳实践](./java/concurrent/java-thread-pool-best-practices.md)
@@ -109,6 +118,9 @@ JVM 这部分内容主要参考 [JVM 虚拟机规范-Java8](https://docs.oracle.
- [Java 19 新特性概览](./java/new-features/java19.md)
- [Java 20 新特性概览](./java/new-features/java20.md)
- [Java 21 新特性概览](./java/new-features/java21.md)
+- [Java 22 & 23 新特性概览](./java/new-features/java22-23.md)
+- [Java 24 新特性概览](./java/new-features/java24.md)
+- [Java 25 新特性概览](./java/new-features/java25.md)
## 计算机基础
@@ -261,7 +273,7 @@ JVM 这部分内容主要参考 [JVM 虚拟机规范-Java8](https://docs.oracle.
### 基础
- [RestFul API 简明教程](./system-design/basis/RESTfulAPI.md)
-- [软件工程简明教程简明教程](./system-design/basis/software-engineering.md)
+- [软件工程简明教程](./system-design/basis/software-engineering.md)
- [代码命名指南](./system-design/basis/naming.md)
- [代码重构指南](./system-design/basis/refactoring.md)
- [单元测试指南](./system-design/basis/unit-test.md)
@@ -297,15 +309,13 @@ JVM 这部分内容主要参考 [JVM 虚拟机规范-Java8](https://docs.oracle.
- [JWT 优缺点分析以及常见问题解决方案](./system-design/security/advantages-and-disadvantages-of-jwt.md)
- [SSO 单点登录详解](./system-design/security/sso-intro.md)
- [权限系统设计详解](./system-design/security/design-of-authority-system.md)
-- [常见加密算法总结](./system-design/security/encryption-algorithms.md)
-
-#### 数据脱敏
-数据脱敏说的就是我们根据特定的规则对敏感信息数据进行变形,比如我们把手机号、身份证号某些位数使用 \* 来代替。
+#### 数据安全
-#### 敏感词过滤
-
-[敏感词过滤方案总结](./system-design/security/sentive-words-filter.md)
+- [常见加密算法总结](./system-design/security/encryption-algorithms.md)
+- [敏感词过滤方案总结](./system-design/security/sentive-words-filter.md)
+- [数据脱敏方案总结](./system-design/security/data-desensitization.md)
+- [为什么前后端都要做数据校验](./system-design/security/data-validation.md)
### 定时任务
@@ -323,6 +333,7 @@ JVM 这部分内容主要参考 [JVM 虚拟机规范-Java8](https://docs.oracle.
- [Paxos 算法解读](./distributed-system/protocol/paxos-algorithm.md)
- [Raft 算法解读](./distributed-system/protocol/raft-algorithm.md)
- [Gossip 协议详解](./distributed-system/protocol/gossip-protocl.md)
+- [一致性哈希算法详解](./distributed-system/protocol/consistent-hashing.md)
### RPC
@@ -382,7 +393,7 @@ JVM 这部分内容主要参考 [JVM 虚拟机规范-Java8](https://docs.oracle.
- [Disruptor 常见知识点&面试题总结](./high-performance/message-queue/disruptor-questions.md)
- [RabbitMQ 常见知识点&面试题总结](./high-performance/message-queue/rabbitmq-questions.md)
- [RocketMQ 常见知识点&面试题总结](./high-performance/message-queue/rocketmq-questions.md)
-- [Kafka 常常见知识点&面试题总结](./high-performance/message-queue/kafka-questions-01.md)
+- [Kafka 常见知识点&面试题总结](./high-performance/message-queue/kafka-questions-01.md)
## 高可用
@@ -412,7 +423,7 @@ JVM 这部分内容主要参考 [JVM 虚拟机规范-Java8](https://docs.oracle.
**灾备** = 容灾 + 备份。
-- **备份**:将系统所产生的的所有重要数据多备份几份。
+- **备份**:将系统所产生的所有重要数据多备份几份。
- **容灾**:在异地建立两个完全相同的系统。当某个地方的系统突然挂掉,整个应用系统可以切换到另一个,这样系统就可以正常提供服务了。
**异地多活** 描述的是将服务部署在异地并且服务同时对外提供服务。和传统的灾备设计的最主要区别在于“多活”,即所有站点都是同时在对外提供服务的。异地多活是为了应对突发状况比如火灾、地震等自然或者人为灾害。
diff --git a/docs/interview-preparation/how-to-handle-interview-nerves.md b/docs/interview-preparation/how-to-handle-interview-nerves.md
new file mode 100644
index 00000000000..c58fba1b0a3
--- /dev/null
+++ b/docs/interview-preparation/how-to-handle-interview-nerves.md
@@ -0,0 +1,74 @@
+---
+title: 面试太紧张怎么办?
+description: 面试太紧张影响发挥怎么办?从心态调整、提前准备到模拟面试与表达训练,提供一套可落地的方法,帮助你降低焦虑、提升临场表现,更稳定地通过技术面试。
+category: 面试准备
+icon: security-fill
+head:
+ - - meta
+ - name: keywords
+ content: 面试紧张,技术面试,面试心态,临场发挥,模拟面试,表达训练,面试准备,校招
+---
+
+
+
+很多小伙伴在第一次技术面试时都会感到紧张甚至害怕,面试结束后还会有种“懵懵的”感觉。我也经历过类似的状况,可以说是深有体会。其实,**紧张是很正常的**——它代表你对面试的重视,也来自于对未知结果的担忧。但如果过度紧张,反而会影响你的临场发挥。
+
+下面,我就分享一些自己的心得,帮大家更好地应对面试中的紧张情绪。
+
+## 试着接受紧张情绪,调整心态
+
+首先要明白,紧张是正常情绪,特别是初次或前几次面试时,多少都会有点忐忑。不要过分排斥这种情绪,可以适当地“拥抱”它:
+
+- **搞清楚面试的本质**:面试本质上是一场与面试官的深入交流,是一个双向选择的过程。面试失败并不意味着你的价值和努力被否定,而可能只是因为你与目标岗位暂时不匹配,或者仅仅是一次 KPI 面试,这家公司可能压根就没有真正的招聘需求。失败的原因也可能是某些知识点、项目经验或表达方式未能充分展现出你的能力。即便这次面试未通过,也不妨碍你继续尝试其他公司,完全不慌!
+- **不要害怕面试官**:很多求职者平时和同学朋友交流沟通的蛮好,一到面试就害怕了。面试官和求职者双方是平等的,以后说不定就是同事关系。也不要觉得面试官就很厉害,实际上,面试官的水平也参差不齐。他们提出的问题,可能自己也没有完全理解。
+- **给自己积极的心理暗示**:告诉自己“有点紧张没关系,这只能让我更专注,心跳加快是我在给自己打气,我一定可以回答的很好!”。
+
+## 提前准备,减少不确定性
+
+**不确定性越多,越容易紧张。** 如果你能够在面试前做充分的准备,很多“未知”就会消失,紧张情绪自然会减轻很多。
+
+### 认真准备技术面试
+
+- **优先梳理核心知识点**:比如计算基础、数据库、Java 基础、Java 集合、并发编程、SpringBoot(这里以 Java 后端方向为例)等。如果时间不够,可以分轻重缓急,有重点地复习。强烈推荐阅读一下 [Java 面试重点总结(重要)](https://javaguide.cn/interview-preparation/key-points-of-interview.html)这篇文章。
+- **精心准备项目经历**:认真思考你简历上最重要的项目(面试以前两个项目为主,尤其是第一个),它们的技术难点、业务逻辑、架构设计,以及可能被面试官深挖的点。把你的思考总结成可能出现的面试问题,并尝试回答。
+
+### 模拟面试和自测
+
+- **约朋友或同学互相提问**:以真实的面试场景来进行演练,并及时对回答进行诊断和反馈。
+- **线上练习**:很多平台都提供 AI 模拟面试,能比较真实地模拟面试官提问情境。
+- **面经**:平时可以多看一些前辈整理的面经,尤其是目标岗位或目标公司的面经,总结高频考点和常见问题。
+- **技术面试题自测**:在 [《Java 面试指北》](https://javaguide.cn/zhuanlan/java-mian-shi-zhi-bei.html) 的 「技术面试题自测篇」 ,我总结了 Java 面试中最重要的知识点的最常见的面试题并按照面试提问的方式展现出来。其中,每一个问题都有提示和重要程度说明,非常适合用来自测。
+
+[《Java 面试指北》](https://javaguide.cn/zhuanlan/java-mian-shi-zhi-bei.html) 的 「技术面试题自测篇」概览:
+
+
+
+### 多表达
+
+平时要多说,多表达出来,不要只是在心里面想,不然真正面试的时候会发现想的和说的不太一样。
+
+我前面推荐的模拟面试和自测,有一部分原因就是为了能够多多表达。
+
+### 多面试
+
+- **先小厂后大厂**:可以先去一些规模较小或者对你来说压力没那么大的公司试试手,积累一些实战经验,增加一些信心;等熟悉了面试流程、能够更从容地回答问题后,再去挑战自己心仪的大厂或热门公司。
+- **积累“失败经验”**:不要怕被拒,有些时候被拒绝却能从中学到更多。多复盘,多思考到底是哪个环节出了问题,再用更好的状态迎接下一次面试。
+
+### 保证休息
+
+- **留出充裕时间**:面试前尽量不要排太多事情,保证自己能有个好状态去参加面试。
+- **保证休息**:充足睡眠有助于情绪稳定,也能让你在面试时更清晰地思考问题。
+
+## 遇到不会的问题不要慌
+
+一场面试,不太可能面试官提的每一个问题你都能轻松应对,除非这场面试非常简单。
+
+在面试过程中,遇到不会的问题,首先要做的是快速回顾自己过往的知识,看是否能找到突破口。如果实在没有思路的话,可以真诚地向面试要一些提示比如谈谈你对这个问题的理解以及困惑点。一定不要觉得向面试官要提示很可耻,只要沟通没问题,这其实是很正常的。最怕的就是自己不会,还乱回答一通,这样会让面试官觉得你技术态度有问题。
+
+## 面试结束后的复盘
+
+很多人关注面试前的准备,却忽略了面试后的复盘,这一步真的非常非常非常重要:
+
+1. **记录面试中的问题**:无论回答得好坏,都把它们写下来。如果问到了一些没想过的问题,可以认真思考并在面试后补上答案。
+2. **反思自己的表现**:有没有遇到卡壳的地方?是知识没准备到还是过于紧张导致表达混乱?下次如何改进?
+3. **持续完善自己的“面试题库”**:把新的问题补充进去,不断拓展自己的知识面,也逐步降低对未知问题的恐惧感。
diff --git a/docs/interview-preparation/internship-experience.md b/docs/interview-preparation/internship-experience.md
new file mode 100644
index 00000000000..da6fb344a67
--- /dev/null
+++ b/docs/interview-preparation/internship-experience.md
@@ -0,0 +1,63 @@
+---
+title: 校招没有实习经历怎么办?
+description: 校招没有实习经历也能上岸:从补强项目经验、持续优化简历到系统准备技术面试,给出可执行的提升路径与注意事项,帮助你在没有大厂实习的情况下提高面试通过率。
+category: 面试准备
+icon: experience
+head:
+ - - meta
+ - name: keywords
+ content: 校招,实习经历,没有实习怎么办,项目经验,简历优化,技术面试准备,Java后端,秋招
+---
+
+
+
+由于目前的面试太卷,对于犹豫是否要找实习的同学来说,个人建议不论是本科生还是研究生都应该在参加校招面试之前,争取一下不错的实习机会,尤其是大厂的实习机会,日常实习或者暑期实习都可以。当然,如果大厂实习面不上,中小厂实习也是可以接受的。
+
+不过,现在的实习是真难找,今年有非常多的同学没有找到实习,有一部分甚至是 211/985 名校的同学。
+
+如果实在是找不到合适的实习的话,那也没办法,我们应该多花时间去把下面这三件事情给做好:
+
+1. 补强项目经历
+2. 持续完善简历
+3. 准备技术面试
+
+## 补强项目经历
+
+校招没有实习经历的话,找工作比较吃亏(没办法,太卷了),需要在项目经历部分多发力弥补一下。
+
+建议你尽全力地去补强自己的项目经历,完善现有的项目或者去做更有亮点的项目,尽可能地通过项目经历去弥补一些。
+
+你面试中的重点就是你的项目经历涉及到的知识点,如果你的项目经历比较简单的话,面试官直接不知道问啥了。另外,你的项目经历中不涉及的知识点,但在技能介绍中提到的知识点也很大概率会被问到。像 Redis 这种基本是面试 Java 后端岗位必备的技能,我觉得大部分面试官应该都会问。
+
+推荐阅读一下网站的这篇文章:[项目经验指南](https://javaguide.cn/interview-preparation/project-experience-guide.html)。
+
+## **完善简历**
+
+一定一定一定要重视简历啊!建议至少花 2~3 天时间来专门完善自己的简历。并且,后续还要持续完善。
+
+对于面试官来说,筛选简历的时候会比较看重下面这些维度:
+
+1. **实习/工作经历**:看你是否有不错的实习经历,大厂且与面试岗位相关的实习/工作经历最佳。
+2. **获奖经历**:如果有含金量比较高(知名度较高的赛事比如 ACM、阿里云天池)的获奖经历的话,也是加分点,尤其是对于校招来说,这类求职者属于是很多大厂争抢的对象(但不是说获奖了就能进大厂,还是要面试表现还可以)。对于社招来说,获奖经历作用相对较小,通常会更看重过往的工作经历和项目经验。
+3. **项目经验**:项目经验对于面试来说非常重要,面试官会重点关注,同时也是有水平的面试提问的重点。
+4. **技能匹配度**:看你的技能是否满足岗位的需求。在投递简历之前,一定要确认一下自己的技能介绍中是否缺少一些你要投递的对应岗位的技能要求。
+5. **学历**:相对其他行业来说,程序员求职面试对于学历的包容度还是比较高的,只要你在其他方面有过人之出的话,也是可以弥补一下学历的缺陷的。你要知道,很多行业比如律师、金融,学历就是敲门砖,学历没达到要求,直接面试机会都没有。不过,由于现在面试越来越卷,一些大厂、国企和研究所也开始卡学历了,很多岗位都要求 211/985,甚至必须需要硕士学历。总之,学历很难改变,学校较差的话,就投递那些对学历没有明确要求的公司即可,努力提升自己的其他方面的硬实力。
+
+对于大部分求职者来说,实习/工作经历、项目经验、技能匹配度更重要一些。不过,不排除一些公司会因为学历卡人。
+
+详细的程序员简历编写指南可以参考这篇文章:[程序员简历编写指南(重要)](https://javaguide.cn/interview-preparation/resume-guide.html)。
+
+## **准备技术面试**
+
+面试之前一定要提前准备一下常见的面试题也就是八股文:
+
+- 自己面试中可能涉及哪些知识点、那些知识点是重点。
+- 面试中哪些问题会被经常问到、面试中自己该如何回答。(强烈不推荐死记硬背,第一:通过背这种方式你能记住多少?能记住多久?第二:背题的方式的学习很难坚持下去!)
+
+Java 后端面试复习的重点请看这篇文章:[Java 后端的面试重点是什么?](https://javaguide.cn/interview-preparation/key-points-of-interview.html)。
+
+不同类型的公司对于技能的要求侧重点是不同的比如腾讯、字节可能更重视计算机基础比如网络、操作系统这方面的内容。阿里、美团这种可能更重视你的项目经历、实战能力。
+
+一定不要抱着一种思想,觉得八股文或者基础问题的考查意义不大。如果你抱着这种思想复习的话,那效果可能不会太好。实际上,个人认为还是很有意义的,八股文或者基础性的知识在日常开发中也会需要经常用到。例如,线程池这块的拒绝策略、核心参数配置什么的,如果你不了解,实际项目中使用线程池可能就用的不是很明白,容易出现问题。而且,其实这种基础性的问题是最容易准备的,像各种底层原理、系统设计、场景题以及深挖你的项目这类才是最难的!
+
+八股文资料首推我的 [《Java 面试指北》](https://javaguide.cn/zhuanlan/java-mian-shi-zhi-bei.html) 和 [JavaGuide](https://javaguide.cn/home.html) 。里面不仅仅是原创八股文,还有很多对实际开发有帮助的干货。除了我的资料之外,你还可以去网上找一些其他的优质的文章、视频来看。
diff --git a/docs/interview-preparation/interview-experience.md b/docs/interview-preparation/interview-experience.md
index 2b58e95df10..c5f08d174ae 100644
--- a/docs/interview-preparation/interview-experience.md
+++ b/docs/interview-preparation/interview-experience.md
@@ -1,12 +1,17 @@
---
title: 优质面经汇总(付费)
+description: 优质面经汇总:整理 30+ 篇高质量 Java 后端校招/社招面经与复盘,总结高频考点与面试策略,适合对照自测与查缺补漏。
category: 知识星球
icon: experience
+head:
+ - - meta
+ - name: keywords
+ content: Java面经,校招面经,社招面经,大厂面经,面试经验,面经汇总,Java后端面试,付费专栏
---
古人云:“**他山之石,可以攻玉**” 。善于学习借鉴别人的面试的成功经验或者失败的教训,可以让自己少走许多弯路。
-在 **[《Java 面试指北》](../zhuanlan/java-mian-shi-zhi-bei.md)** 的 **「面经篇」** ,我分享了 15+ 篇高质量的 Java 后端面经,有校招的,也有社招的,有大厂的,也有中小厂的。
+在 **[《Java 面试指北》](../zhuanlan/java-mian-shi-zhi-bei.md)** 的 **「面经篇」** ,我分享了 30+ 篇高质量的 Java 后端面经,有校招的,也有社招的,有大厂的,也有中小厂的。
如果你是非科班的同学,也能在这些文章中找到对应的非科班的同学写的面经。
diff --git a/docs/interview-preparation/java-roadmap.md b/docs/interview-preparation/java-roadmap.md
new file mode 100644
index 00000000000..4e234274c45
--- /dev/null
+++ b/docs/interview-preparation/java-roadmap.md
@@ -0,0 +1,42 @@
+---
+title: Java 学习路线(最新版,4w+字)
+description: Java学习路线最新版:结合当下 Java 后端招聘要求,提供从基础到进阶的系统学习路径与资料建议,覆盖Java核心、数据库、缓存、中间件、框架与面试重点,帮助高效规划与提速上岸。
+category: 面试准备
+icon: path
+head:
+ - - meta
+ - name: keywords
+ content: Java学习路线,Java后端路线,Java学习计划,校招准备,面试路线,Spring Boot,MySQL,Redis,JVM
+---
+
+
+
+::: tip 重要说明
+
+本学习路线保持**年度系统性修订**,严格同步 Java 技术生态与招聘市场的最新动态,**确保内容时效性与前瞻性**。
+
+:::
+
+历时一个月精心打磨,笔者基于当下 Java 后端开发岗位招聘的最新要求,对既有学习路线进行了全面升级。本次升级涵盖技术栈增删、学习路径优化、配套学习资源更新等维度,力争构建出更符合 Java 开发者成长曲线的知识体系。
+
+亮色板概览:
+
+
+
+暗色板概览:
+
+
+
+这可能是你见过的最用心、最全面的 Java 后端学习路线。这份学习路线共包含 **4w+** 字,但你完全不用担心内容过多而学不完。我会根据学习难度,划分出适合找小厂工作必学的内容,以及适合逐步提升 Java 后端开发能力的学习路径。
+
+
+
+对于初学者,你可以按照这篇文章推荐的学习路线和资料进行系统性的学习;对于有经验的开发者,你可以根据这篇文章更一步地深入学习 Java 后端开发,提升个人竞争力。
+
+在看这份学习路线的过程中,建议搭配 [Java 面试重点总结(重要)](https://javaguide.cn/interview-preparation/key-points-of-interview.html),可以让你在学习过程中更有目的性。
+
+由于这份学习路线内容太多,因此我将其整理成了 PDF 版本(共 **55** 页),方便大家阅读。这份 PDF 有黑夜和白天两种阅读版本,满足大家的不同需求。
+
+这份学习路线的获取方法很简单:直接在公众号「**JavaGuide**」后台回复“**路线**”即可获取。
+
+
diff --git a/docs/interview-preparation/key-points-of-interview.md b/docs/interview-preparation/key-points-of-interview.md
index 1d710456b86..addd94bedbd 100644
--- a/docs/interview-preparation/key-points-of-interview.md
+++ b/docs/interview-preparation/key-points-of-interview.md
@@ -1,17 +1,28 @@
---
-title: Java面试重点总结(重要)
+title: Java后端面试重点总结
+description: Java后端面试重点总结:梳理校招/社招高频考点与复习优先级,覆盖Java基础、集合、并发、MySQL、Redis、Spring/Spring Boot、JVM与项目经验准备,帮你抓重点高效备战。
category: 面试准备
icon: star
+head:
+ - - meta
+ - name: keywords
+ content: Java后端面试,面试重点,八股文,Java基础,Java集合,Java并发,MySQL,Redis,Spring Boot,项目经验
---
+
+
::: tip 友情提示
-本文节选自 **[《Java 面试指北》](../zhuanlan/java-mian-shi-zhi-bei.md)**。这是一份教你如何更高效地准备面试的小册,涵盖常见八股文(系统设计、常见框架、分布式、高并发 ……)、优质面经等内容。
+本文节选自 **[《Java 面试指北》](../zhuanlan/java-mian-shi-zhi-bei.md)**。这是一份教你如何更高效地准备面试的专栏,内容和 JavaGuide 互补,涵盖常见八股文(系统设计、常见框架、分布式、高并发 ……)、优质面经等内容。
:::
## Java 后端面试哪些知识点是重点?
**准备面试的时候,具体哪些知识点是重点呢?如何把握重点?**
+先来一张图(后续会详细解读):
+
+
+
给你几点靠谱的建议:
1. Java 基础、集合、并发、MySQL、Redis 、Spring、Spring Boot 这些 Java 后端开发必备的知识点(MySQL + Redis >= Java > Spring + Spring Boot)。大厂以及中小厂的面试问的比较多的就是这些知识点。Spring 和 Spring Boot 这俩框架类的知识点相对前面的知识点来说重要性要稍低一些,但一般面试也会问一些,尤其是中小厂。并发知识一般中大厂提问更多也更难,尤其是大厂喜欢深挖底层,很容易把人问倒。计算机基础相关的内容会在下面提到。
@@ -19,13 +30,15 @@ icon: star
3. 针对自身找工作的需求,你又可以适当地调整复习的重点。像中小厂一般问计算机基础比较少一些,有些大厂比如字节比较重视计算机基础尤其是算法。这样的话,如果你的目标是中小厂的话,计算机基础就准备面试来说不是那么重要了。如果复习时间不够的话,可以暂时先放放,腾出时间给其他重要的知识点。
4. 一般校招的面试不会强制要求你会分布式/微服务、高并发的知识(不排除个别岗位有这方面的硬性要求),所以到底要不要掌握还是要看你个人当前的实际情况。如果你会这方面的知识的话,对面试相对来说还是会更有利一些(想要让项目经历有亮点,还是得会一些性能优化的知识。性能优化的知识这也算是高并发知识的一个小分支了)。如果你的技能介绍或者项目经历涉及到分布式/微服务、高并发的知识,那建议你尽量也要抽时间去认真准备一下,面试中很可能会被问到,尤其是项目经历用到的时候。不过,也还是主要准备写在简历上的那些知识点就好。
5. JVM 相关的知识点,一般是大厂(例如美团、阿里)和一些不错的中厂(例如携程、顺丰、招银网络)才会问到,面试国企、差一点的中厂和小厂就没必要准备了。JVM 面试中比较常问的是 [Java 内存区域](https://javaguide.cn/java/jvm/memory-area.html)、[JVM 垃圾回收](https://javaguide.cn/java/jvm/jvm-garbage-collection.html)、[类加载器和双亲委派模型](https://javaguide.cn/java/jvm/classloader.html) 以及 JVM 调优和问题排查(我之前分享过一些[常见的线上问题案例](https://t.zsxq.com/0bsAac47U),里面就有 JVM 相关的)。
-6. 不同的大厂面试侧重点也会不同。比如说你要去阿里这种公司的话,项目和八股文就是重点,阿里笔试一般会有代码题,进入面试后就很少问代码题了,但是对原理性的问题问的比较深,经常会问一些你对技术的思考。再比如说你要面试字节这种公司,那计算机基础,尤其是算法是重点,字节的面试十分注重代码功底,有时候开始面试就会直接甩给你一道代码题,写出来再谈别的。也会问面试八股文,以及项目,不过,相对来说要少很多。建议你看一下这篇文章 [为了解开互联网大厂秋招内幕,我把他们全面了一遍](https://mp.weixin.qq.com/s/pBsGQNxvRupZeWt4qZReIA),了解一下常见大厂的面试题侧重点。
+6. 不同的大厂面试侧重点也会不同。比如说你要去阿里这种公司的话,项目和八股文就是重点,阿里笔试一般会有代码题,进入面试后就很少问代码题了,但是对原理性的问题问的比较深,经常会问一些你对技术的思考。再比如说你要面试字节这种公司,那计算机基础,尤其是算法是重点,字节的面试十分注重代码功底,有时候开始面试就会直接甩给你一道代码题,写出来再谈别的。也会问面试八股文,以及项目,不过,相对来说要少很多。
7. 多去找一些面经看看,尤其你目标公司或者类似公司对应岗位的面经。这样可以实现针对性的复习,还能顺便自测一波,检查一下自己的掌握情况。
看似 Java 后端八股文很多,实际把复习范围一缩小,重要的东西就是那些。考虑到时间问题,你不可能连一些比较冷门的知识点也给准备了。这没必要,主要精力先放在那些重要的知识点即可。
## 如何更高效地准备八股文?
+
+
对于技术八股文来说,尽量不要死记硬背,这种方式非常枯燥且对自身能力提升有限!但是!想要一点不背是不太现实的,只是说要结合实际应用场景和实战来理解记忆。
我一直觉得面试八股文最好是和实际应用场景和实战相结合。很多同学现在的方向都错了,上来就是直接背八股文,硬生生学成了文科,那当然无趣了。
diff --git a/docs/interview-preparation/project-experience-guide.md b/docs/interview-preparation/project-experience-guide.md
index f2e93df82b3..2b9a3c1026a 100644
--- a/docs/interview-preparation/project-experience-guide.md
+++ b/docs/interview-preparation/project-experience-guide.md
@@ -1,11 +1,16 @@
---
title: 项目经验指南
+description: 项目经验指南:针对没有项目/项目平淡的求职者,给出获取实战项目经验的方法与选择建议,并讲清如何做出项目亮点、如何复盘与表达,提升简历与面试竞争力。
category: 面试准备
icon: project
+head:
+ - - meta
+ - name: keywords
+ content: 项目经验,校招项目,实战项目,项目亮点,简历项目描述,后端项目,面试项目准备,项目复盘
---
::: tip 友情提示
-本文节选自 **[《Java 面试指北》](../zhuanlan/java-mian-shi-zhi-bei.md)**。这是一份教你如何更高效地准备面试的小册,涵盖常见八股文(系统设计、常见框架、分布式、高并发 ……)、优质面经等内容。
+本文节选自 **[《Java 面试指北》](../zhuanlan/java-mian-shi-zhi-bei.md)**。这是一份教你如何更高效地准备面试的专栏,内容和 JavaGuide 互补,涵盖常见八股文(系统设计、常见框架、分布式、高并发 ……)、优质面经等内容。
:::
## 没有项目经验怎么办?
@@ -72,9 +77,9 @@ GitHub 或者码云上面有很多实战类别项目,你可以选择一个来
## 有没有还不错的项目推荐?
-**[《Java 面试指北》](../zhuanlan/java-mian-shi-zhi-bei.md)** 的「面试准备篇」中有一篇文章专门整理了一些比较高质量的实战项目,非常适合用来学习或者作为项目经验。
+**[《Java 面试指北》](../zhuanlan/java-mian-shi-zhi-bei.md)** 的「面试准备篇」中有一篇文章专门整理了一些比较高质量的实战项目,包含业务项目、轮子项目、国外公开课 Lab 和视频类实战项目教程推荐,非常适合用来学习或者作为项目经验。
-
+
这篇文章一共推荐了 15+ 个实战项目,有业务类的,也有轮子类的,有开源项目、也有视频教程。对于参加校招的小伙伴,我更建议做一个业务类项目加上一个轮子类的项目。
diff --git a/docs/interview-preparation/resume-guide.md b/docs/interview-preparation/resume-guide.md
index 818274601e0..f0cd20b003b 100644
--- a/docs/interview-preparation/resume-guide.md
+++ b/docs/interview-preparation/resume-guide.md
@@ -1,7 +1,12 @@
---
-title: 程序员简历编写指南(重要)
+title: 程序员简历编写指南
+description: 程序员简历编写指南:从筛选逻辑出发讲清简历结构、项目经历与技能描述写法,提供简历模板与避坑建议,帮助你提高简历通过率并让面试官更好地深挖你的亮点。
category: 面试准备
icon: jianli
+head:
+ - - meta
+ - name: keywords
+ content: 程序员简历,Java简历,简历优化,项目经历写法,简历模板,校招简历,社招简历,面试准备
---
::: tip 友情提示
diff --git a/docs/interview-preparation/self-test-of-common-interview-questions.md b/docs/interview-preparation/self-test-of-common-interview-questions.md
index 85b0e236c01..c3d8c038eb2 100644
--- a/docs/interview-preparation/self-test-of-common-interview-questions.md
+++ b/docs/interview-preparation/self-test-of-common-interview-questions.md
@@ -1,19 +1,22 @@
---
title: 常见面试题自测(付费)
+description: 常见面试题自测:按面试提问方式整理Java后端高频问题,提供提示与重要程度标注,适合面试前自测、定位短板、针对性复习。
category: 知识星球
icon: security-fill
+head:
+ - - meta
+ - name: keywords
+ content: 面试题自测,Java面试题,八股文自测,查缺补漏,面试复习,高频考点,Java后端面试,付费内容
---
面试之前,强烈建议大家多拿常见的面试题来进行自测,检查一下自己的掌握情况,这是一种非常实用的备战技术面试的小技巧。
在 **[《Java 面试指北》](../zhuanlan/java-mian-shi-zhi-bei.md)** 的 **「技术面试题自测篇」** ,我总结了 Java 面试中最重要的知识点的最常见的面试题并按照面试提问的方式展现出来。
-
+
-每一道用于自测的面试题我都会给出重要程度,方便大家在时间比较紧张的时候根据自身情况来选择性自测。并且,我还会给出提示,方便你回忆起对应的知识点。
+每道题我都会给出**提示与思路**,并用 ⭐ 标注重要程度:⭐ 越多,说明面试越爱问,就越值得多花一些时间准备。
-在面试中如果你实在没有头绪的话,一个好的面试官也是会给你提示的。
-
-
+
diff --git a/docs/interview-preparation/teach-you-how-to-prepare-for-the-interview-hand-in-hand.md b/docs/interview-preparation/teach-you-how-to-prepare-for-the-interview-hand-in-hand.md
index 7cc0797bcf2..beba0d99cbd 100644
--- a/docs/interview-preparation/teach-you-how-to-prepare-for-the-interview-hand-in-hand.md
+++ b/docs/interview-preparation/teach-you-how-to-prepare-for-the-interview-hand-in-hand.md
@@ -1,20 +1,23 @@
---
-title: 手把手教你如何准备Java面试(重要)
+title: 如何高效准备Java面试?
+description: 如何高效准备Java面试:从求职导向学习、技能清单制定到简历优化与面试冲刺,提供系统化备战方法,帮助你少走弯路、提高面试通过率。
category: 知识星球
icon: path
+head:
+ - - meta
+ - name: keywords
+ content: Java面试准备,高效备战面试,求职导向学习,面试冲刺,简历优化,项目准备,校招,Java后端
---
::: tip 友情提示
-本文节选自 **[《Java 面试指北》](../zhuanlan/java-mian-shi-zhi-bei.md)**。这是一份教你如何更高效地准备面试的小册,涵盖常见八股文(系统设计、常见框架、分布式、高并发 ……)、优质面经等内容。
+本文节选自 **[《Java 面试指北》](../zhuanlan/java-mian-shi-zhi-bei.md)**。这是一份教你如何更高效地准备面试的专栏,内容和 JavaGuide 互补,涵盖常见八股文(系统设计、常见框架、分布式、高并发 ……)、优质面经等内容。
:::
-你的身边一定有很多编程比你厉害但是找的工作并没有你好的朋友!**技术面试不同于编程,编程厉害不代表技术面试就一定能过。**
+你身边是否有这样的朋友:编程能力比你强,求职结果却不如你?其实**技术好≠面试能过** —— 如今的面试早已不是 “会写代码就行”,不做准备就去面,大概率是 “撞枪口”。
-现在你去面个试,不认真准备一下,那简直就是往枪口上撞。我们大部分都只是普通人,没有发过顶级周刊或者获得过顶级大赛奖项。在这样一个技术面试氛围下,我们需要花费很多精力来准备面试,来提高自己的技术能力。“[面试造火箭,工作拧螺丝钉](https://mp.weixin.qq.com/s?__biz=Mzg2OTA0Njk0OA==&mid=2247491596&idx=1&sn=36fbf80922f71c200990de11514955f7&chksm=cea1afc7f9d626d1c70d5e54505495ac499ce6eb5e05ba4f4bb079a8563a84e27f17ceff38af&token=353590436&lang=zh_CN&scene=21#wechat_redirect)” 就是目前的一个常态,预计未来很长很长一段时间也还是会是这样。
+我们大多是普通开发者,没有顶会论文或竞赛大奖加持,面对 “面试造火箭,工作拧螺丝钉” 的常态,只能靠扎实准备突围。但准备面试不等于耍小聪明或者死记硬背面试题。 **一定不要对面试抱有侥幸心理。打铁还需自身硬!** 千万不要觉得自己看几篇面经,看几篇面试题解析就能通过面试了。一定要静下心来深入学习!
-准备面试不等于耍小聪明或者死记硬背面试题。 **一定不要对面试抱有侥幸心理。打铁还需自身硬!** 千万不要觉得自己看几篇面经,看几篇面试题解析就能通过面试了。一定要静下心来深入学习!
-
-这篇我会从宏观面出发简单聊聊如何准备 Java 面试,让你少走弯路!
+这篇文章就从宏观视角,带你搞懂程序员该如何系统准备面试:从求职导向学习,到简历优化、面试冲刺,帮你少走弯路,高效拿下心仪 offer。
## 尽早以求职为导向来学习
@@ -138,7 +141,7 @@ Java 后端面试复习的重点请看这篇文章:[Java 面试重点总结(
一定不要抱着一种思想,觉得八股文或者基础问题的考查意义不大。如果你抱着这种思想复习的话,那效果可能不会太好。实际上,个人认为还是很有意义的,八股文或者基础性的知识在日常开发中也会需要经常用到。例如,线程池这块的拒绝策略、核心参数配置什么的,如果你不了解,实际项目中使用线程池可能就用的不是很明白,容易出现问题。而且,其实这种基础性的问题是最容易准备的,像各种底层原理、系统设计、场景题以及深挖你的项目这类才是最难的!
-八股文资料首推我的 [《Java 面试指北》](https://t.zsxq.com/11rZ6D7Wk) (配合 JavaGuide 使用,会根据每一年的面试情况对内容进行更新完善)和 [JavaGuide](https://javaguide.cn/) 。里面不仅仅是原创八股文,还有很多对实际开发有帮助的干货。除了我的资料之外,你还可以去网上找一些其他的优质的文章、视频来看。
+八股文资料首推我的 [《Java 面试指北》](https://javaguide.cn/zhuanlan/java-mian-shi-zhi-bei.html) (配合 JavaGuide 使用,会根据每一年的面试情况对内容进行更新完善)和 [JavaGuide](https://javaguide.cn/) 。里面不仅仅是原创八股文,还有很多对实际开发有帮助的干货。除了我的资料之外,你还可以去网上找一些其他的优质的文章、视频来看。

diff --git a/docs/java/basis/bigdecimal.md b/docs/java/basis/bigdecimal.md
index 5af0c460579..84378588cec 100644
--- a/docs/java/basis/bigdecimal.md
+++ b/docs/java/basis/bigdecimal.md
@@ -1,8 +1,13 @@
---
title: BigDecimal 详解
+description: 详解BigDecimal使用方法:解决浮点数精度丢失问题,掌握加减乘除运算、RoundingMode舍入规则、compareTo比较方法,适用金融计算等高精度场景。
category: Java
tag:
- Java基础
+head:
+ - - meta
+ - name: keywords
+ content: BigDecimal,浮点数精度,小数运算,RoundingMode舍入模式,BigDecimal比较,金额计算,精度丢失
---
《阿里巴巴 Java 开发手册》中提到:“为了避免精度丢失,可以使用 `BigDecimal` 来进行浮点数的运算”。
@@ -21,7 +26,7 @@ System.out.println(a == b);// false
**为什么浮点数 `float` 或 `double` 运算的时候会有精度丢失的风险呢?**
-这个和计算机保存浮点数的机制有很大关系。我们知道计算机是二进制的,而且计算机在表示一个数字时,宽度是有限的,无限循环的小数存储在计算机时,只能被截断,所以就会导致小数精度发生损失的情况。这也就是解释了为什么浮点数没有办法用二进制精确表示。
+这个和计算机保存小数的机制有很大关系。我们知道计算机是二进制的,而且计算机在表示一个数字时,宽度是有限的,无限循环的小数存储在计算机时,只能被截断,所以就会导致小数精度发生损失的情况。这也就是解释了为什么十进制小数没有办法用二进制精确表示。
就比如说十进制下的 0.2 就没办法精确转换成二进制小数:
@@ -40,9 +45,9 @@ System.out.println(a == b);// false
## BigDecimal 介绍
-`BigDecimal` 可以实现对浮点数的运算,不会造成精度丢失。
+`BigDecimal` 可以实现对小数的运算,不会造成精度丢失。
-通常情况下,大部分需要浮点数精确运算结果的业务场景(比如涉及到钱的场景)都是通过 `BigDecimal` 来做的。
+通常情况下,大部分需要小数精确运算结果的业务场景(比如涉及到钱的场景)都是通过 `BigDecimal` 来做的。
《阿里巴巴 Java 开发手册》中提到:**浮点数之间的等值判断,基本数据类型不能用 == 来比较,包装数据类型不能用 equals 来判断。**
@@ -50,7 +55,7 @@ System.out.println(a == b);// false
具体原因我们在上面已经详细介绍了,这里就不多提了。
-想要解决浮点数运算精度丢失这个问题,可以直接使用 `BigDecimal` 来定义浮点数的值,然后再进行浮点数的运算操作即可。
+想要解决浮点数运算精度丢失这个问题,可以直接使用 `BigDecimal` 来定义小数的值,然后再进行小数的运算操作即可。
```java
BigDecimal a = new BigDecimal("1.0");
@@ -99,20 +104,20 @@ public BigDecimal divide(BigDecimal divisor, int scale, RoundingMode roundingMod
```java
public enum RoundingMode {
- // 2.5 -> 3 , 1.6 -> 2
- // -1.6 -> -2 , -2.5 -> -3
+ // 2.4 -> 3 , 1.6 -> 2
+ // -1.6 -> -2 , -2.4 -> -3
UP(BigDecimal.ROUND_UP),
- // 2.5 -> 2 , 1.6 -> 1
- // -1.6 -> -1 , -2.5 -> -2
+ // 2.4 -> 2 , 1.6 -> 1
+ // -1.6 -> -1 , -2.4 -> -2
DOWN(BigDecimal.ROUND_DOWN),
- // 2.5 -> 3 , 1.6 -> 2
- // -1.6 -> -1 , -2.5 -> -2
+ // 2.4 -> 3 , 1.6 -> 2
+ // -1.6 -> -1 , -2.4 -> -2
CEILING(BigDecimal.ROUND_CEILING),
// 2.5 -> 2 , 1.6 -> 1
// -1.6 -> -2 , -2.5 -> -3
FLOOR(BigDecimal.ROUND_FLOOR),
- // 2.5 -> 3 , 1.6 -> 2
- // -1.6 -> -2 , -2.5 -> -3
+ // 2.4 -> 2 , 1.6 -> 2
+ // -1.6 -> -2 , -2.4 -> -2
HALF_UP(BigDecimal.ROUND_HALF_UP),
//......
}
@@ -230,7 +235,7 @@ public class BigDecimalUtil {
/**
* 提供(相对)精确的除法运算,当发生除不尽的情况时,精确到
- * 小数点以后10位,以后的数字四舍五入。
+ * 小数点以后10位,以后的数字四舍六入五成双。
*
* @param v1 被除数
* @param v2 除数
@@ -242,7 +247,7 @@ public class BigDecimalUtil {
/**
* 提供(相对)精确的除法运算。当发生除不尽的情况时,由scale参数指
- * 定精度,以后的数字四舍五入。
+ * 定精度,以后的数字四舍六入五成双。
*
* @param v1 被除数
* @param v2 除数
@@ -260,11 +265,11 @@ public class BigDecimalUtil {
}
/**
- * 提供精确的小数位四舍五入处理。
+ * 提供精确的小数位四舍六入五成双处理。
*
- * @param v 需要四舍五入的数字
+ * @param v 需要四舍六入五成双的数字
* @param scale 小数点后保留几位
- * @return 四舍五入后的结果
+ * @return 四舍六入五成双后的结果
*/
public static double round(double v, int scale) {
if (scale < 0) {
@@ -283,18 +288,18 @@ public class BigDecimalUtil {
* @return 返回转换结果
*/
public static float convertToFloat(double v) {
- BigDecimal b = new BigDecimal(v);
+ BigDecimal b = BigDecimal.valueOf(v);
return b.floatValue();
}
/**
- * 提供精确的类型转换(Int)不进行四舍五入
+ * 提供精确的类型转换(Int)不进行四舍六入五成双
*
* @param v 需要被转换的数字
* @return 返回转换结果
*/
public static int convertsToInt(double v) {
- BigDecimal b = new BigDecimal(v);
+ BigDecimal b = BigDecimal.valueOf(v);
return b.intValue();
}
@@ -305,7 +310,7 @@ public class BigDecimalUtil {
* @return 返回转换结果
*/
public static long convertsToLong(double v) {
- BigDecimal b = new BigDecimal(v);
+ BigDecimal b = BigDecimal.valueOf(v);
return b.longValue();
}
@@ -317,8 +322,8 @@ public class BigDecimalUtil {
* @return 返回两个数中大的一个值
*/
public static double returnMax(double v1, double v2) {
- BigDecimal b1 = new BigDecimal(v1);
- BigDecimal b2 = new BigDecimal(v2);
+ BigDecimal b1 = BigDecimal.valueOf(v1);
+ BigDecimal b2 = BigDecimal.valueOf(v2);
return b1.max(b2).doubleValue();
}
@@ -330,8 +335,8 @@ public class BigDecimalUtil {
* @return 返回两个数中小的一个值
*/
public static double returnMin(double v1, double v2) {
- BigDecimal b1 = new BigDecimal(v1);
- BigDecimal b2 = new BigDecimal(v2);
+ BigDecimal b1 = BigDecimal.valueOf(v1);
+ BigDecimal b2 = BigDecimal.valueOf(v2);
return b1.min(b2).doubleValue();
}
diff --git a/docs/java/basis/generics-and-wildcards.md b/docs/java/basis/generics-and-wildcards.md
index f65781e01e6..3bf523ec0b7 100644
--- a/docs/java/basis/generics-and-wildcards.md
+++ b/docs/java/basis/generics-and-wildcards.md
@@ -1,20 +1,15 @@
---
title: 泛型&通配符详解
+description: 全面解析Java泛型与通配符:深入理解类型擦除机制、上界下界通配符用法、PECS原则应用,掌握泛型编程核心技巧。
category: Java
tag:
- Java基础
+head:
+ - - meta
+ - name: keywords
+ content: Java泛型,通配符,类型擦除,泛型边界,PECS原则,泛型方法,上界下界通配符,泛型接口
---
-**泛型&通配符** 相关的面试题为我的[知识星球](https://javaguide.cn/about-the-author/zhishixingqiu-two-years.html)(点击链接即可查看详细介绍以及加入方法)专属内容,已经整理到了[《Java 面试指北》](hhttps://javaguide.cn/zhuanlan/java-mian-shi-zhi-bei.html)(点击链接即可查看详细介绍以及获取方法)中。
-
-[《Java 面试指北》](hhttps://javaguide.cn/zhuanlan/java-mian-shi-zhi-bei.html) 的部分内容展示如下,你可以将其看作是 [JavaGuide](https://javaguide.cn/#/) 的补充完善,两者可以配合使用。
-
-
-
-[《Java 面试指北》](hhttps://javaguide.cn/zhuanlan/java-mian-shi-zhi-bei.html)只是星球内部众多资料中的一个,星球还有很多其他优质资料比如[专属专栏](https://javaguide.cn/zhuanlan/)、Java 编程视频、PDF 资料。
-
-
+**泛型&通配符** 相关的面试题为我的[知识星球](https://javaguide.cn/about-the-author/zhishixingqiu-two-years.html)(点击链接即可查看详细介绍以及加入方法)专属内容,已经整理到了[《Java 面试指北》](https://javaguide.cn/zhuanlan/java-mian-shi-zhi-bei.html)(点击链接即可查看详细介绍以及获取方法)中。
-
-
diff --git a/docs/java/basis/java-basic-questions-01.md b/docs/java/basis/java-basic-questions-01.md
index d140f9fcb0f..f10ac0d42d9 100644
--- a/docs/java/basis/java-basic-questions-01.md
+++ b/docs/java/basis/java-basic-questions-01.md
@@ -1,15 +1,13 @@
---
title: Java基础常见面试题总结(上)
category: Java
+description: Java基础常见面试题总结:包含Java语言特点、JVM/JDK/JRE区别、字节码详解、基本数据类型、自动装箱拆箱、方法重载与重写等核心知识点,助力Java开发者面试通关。
tag:
- Java基础
head:
- - meta
- name: keywords
- content: JVM,JDK,JRE,字节码详解,Java 基本数据类型,装箱和拆箱
- - - meta
- - name: description
- content: 全网质量最高的Java基础常见知识点和面试题总结,希望对你有帮助!
+ content: Java基础,JVM,JDK,JRE,Java SE,字节码,Java编译,自动装箱,基本数据类型,方法重载,Java面试题
---
@@ -44,7 +42,7 @@ head:
除了 Java SE 和 Java EE,还有一个 Java ME(Java Platform,Micro Edition)。Java ME 是 Java 的微型版本,主要用于开发嵌入式消费电子设备的应用程序,例如手机、PDA、机顶盒、冰箱、空调等。Java ME 无需重点关注,知道有这个东西就好了,现在已经用不上了。
-### JVM vs JDK vs JRE
+### ⭐️JVM vs JDK vs JRE
#### JVM
@@ -87,7 +85,7 @@ JRE 是运行已编译 Java 程序所需的环境,主要包含以下两个部
定制的、模块化的 Java 运行时映像有助于简化 Java 应用的部署和节省内存并增强安全性和可维护性。这对于满足现代应用程序架构的需求,如虚拟化、容器化、微服务和云原生开发,是非常重要的。
-### 什么是字节码?采用字节码的好处是什么?
+### ⭐️什么是字节码?采用字节码的好处是什么?
在 Java 中,JVM 可以理解的代码就叫做字节码(即扩展名为 `.class` 的文件),它不面向任何特定的处理器,只面向虚拟机。Java 语言通过字节码的方式,在一定程度上解决了传统解释型语言执行效率低的问题,同时又保留了解释型语言可移植的特点。所以, Java 程序运行时相对来说还是高效的(不过,和 C、 C++,Rust,Go 等语言还是有一定差距的),而且,由于字节码并不针对一种特定的机器,因此,Java 程序无须重新编译便可在多种不同操作系统的计算机上运行。
@@ -97,7 +95,10 @@ JRE 是运行已编译 Java 程序所需的环境,主要包含以下两个部
我们需要格外注意的是 `.class->机器码` 这一步。在这一步 JVM 类加载器首先加载字节码文件,然后通过解释器逐行解释执行,这种方式的执行速度会相对比较慢。而且,有些方法和代码块是经常需要被调用的(也就是所谓的热点代码),所以后面引进了 **JIT(Just in Time Compilation)** 编译器,而 JIT 属于运行时编译。当 JIT 编译器完成第一次编译后,其会将字节码对应的机器码保存下来,下次可以直接使用。而我们知道,机器码的运行效率肯定是高于 Java 解释器的。这也解释了我们为什么经常会说 **Java 是编译与解释共存的语言** 。
-> 🌈 拓展:[有关 JIT 的实现细节: JVM C1、C2 编译器](https://mp.weixin.qq.com/s/4haTyXUmh8m-dBQaEzwDJw)
+> 🌈 拓展阅读:
+>
+> - [基本功 | Java 即时编译器原理解析及实践 - 美团技术团队](https://tech.meituan.com/2020/10/22/java-jit-practice-in-meituan.html)
+> - [基于静态编译构建微服务应用 - 阿里巴巴中间件](https://mp.weixin.qq.com/s/4haTyXUmh8m-dBQaEzwDJw)

@@ -111,7 +112,7 @@ JDK、JRE、JVM、JIT 这四者的关系如下图所示。

-### 为什么说 Java 语言“编译与解释并存”?
+### ⭐️为什么说 Java 语言“编译与解释并存”?
其实这个问题我们讲字节码的时候已经提到过,因为比较重要,所以我们这里再提一下。
@@ -279,7 +280,7 @@ Java 中的注释有三种:
官方文档:[https://docs.oracle.com/javase/tutorial/java/nutsandbolts/\_keywords.html](https://docs.oracle.com/javase/tutorial/java/nutsandbolts/_keywords.html)
-### 自增自减运算符
+### ⭐️自增自减运算符
在写代码的过程中,常见的一种情况是需要某个整数类型变量增加 1 或减少 1。Java 提供了自增运算符 (`++`) 和自减运算符 (`--`) 来简化这种操作。
@@ -302,7 +303,7 @@ int e = --d;
答案:`a = 11` 、`b = 9` 、 `c = 10` 、 `d = 10` 、 `e = 10`。
-### 移位运算符
+### ⭐️移位运算符
移位运算符是最基本的运算符之一,几乎每种编程语言都包含这一运算符。移位操作中,被操作的数据被视为二进制数,移位就是将其向左或向右移动若干位的运算。
@@ -329,7 +330,7 @@ static final int hash(Object key) {
- **位字段管理**:例如存储和操作多个布尔值。
- **哈希算法和加密解密**:通过移位和与、或等操作来混淆数据。
- **数据压缩**:例如霍夫曼编码通过移位运算符可以快速处理和操作二进制数据,以生成紧凑的压缩格式。
-- **数据校验**:例如 CRC(循环冗余校验)通过移位和多项式除法生成和校验数据完整性。。
+- **数据校验**:例如 CRC(循环冗余校验)通过移位和多项式除法生成和校验数据完整性。
- **内存对齐**:通过移位操作,可以轻松计算和调整数据的对齐地址。
掌握最基本的移位运算符知识还是很有必要的,这不光可以帮助我们在代码中使用,还可以帮助我们理解源码中涉及到移位运算符的代码。
@@ -439,7 +440,7 @@ xixi
haha
```
-## 基本数据类型
+## ⭐️基本数据类型
### Java 中的几种基本数据类型了解么?
@@ -488,14 +489,14 @@ Java 中有 8 种基本数据类型,分别为:
**为什么说是几乎所有对象实例都存在于堆中呢?** 这是因为 HotSpot 虚拟机引入了 JIT 优化之后,会对对象进行逃逸分析,如果发现某一个对象并没有逃逸到方法外部,那么就可能通过标量替换来实现栈上分配,而避免堆上分配内存
-⚠️ 注意:**基本数据类型存放在栈中是一个常见的误区!** 基本数据类型的存储位置取决于它们的作用域和声明方式。如果它们是局部变量,那么它们会存放在栈中;如果它们是成员变量,那么它们会存放在堆中。
+⚠️ 注意:**基本数据类型存放在栈中是一个常见的误区!** 基本数据类型的存储位置取决于它们的作用域和声明方式。如果它们是局部变量,那么它们会存放在栈中;如果它们是成员变量,那么它们会存放在堆/方法区/元空间中。
```java
public class Test {
// 成员变量,存放在堆中
int a = 10;
- // 被 static 修饰,也存放在堆中,但属于类,不属于对象
- // JDK1.7 静态变量从永久代移动了 Java 堆中
+ // 被 static 修饰的成员变量,JDK 1.7 及之前位于方法区,1.8 后存放于元空间,均不存放于堆中。
+ // 变量属于类,不属于对象。
static int b = 20;
public void method() {
@@ -510,7 +511,11 @@ public class Test {
Java 基本数据类型的包装类型的大部分都用到了缓存机制来提升性能。
-`Byte`,`Short`,`Integer`,`Long` 这 4 种包装类默认创建了数值 **[-128,127]** 的相应类型的缓存数据,`Character` 创建了数值在 **[0,127]** 范围的缓存数据,`Boolean` 直接返回 `True` or `False`。
+`Byte`,`Short`,`Integer`,`Long` 这 4 种包装类默认创建了数值 **[-128,127]** 的相应类型的缓存数据,`Character` 创建了数值在 **[0,127]** 范围的缓存数据,`Boolean` 直接返回 `TRUE` or `FALSE`。
+
+对于 `Integer`,可以通过 JVM 参数 `-XX:AutoBoxCacheMax=` 修改缓存上限,但不能修改下限 -128。实际使用时,并不建议设置过大的值,避免浪费内存,甚至是 OOM。
+
+对于`Byte`,`Short`,`Long` ,`Character` 没有类似 `-XX:AutoBoxCacheMax` 参数可以修改,因此缓存范围是固定的,无法通过 JVM 参数调整。`Boolean` 则直接返回预定义的 `TRUE` 和 `FALSE` 实例,没有缓存范围的概念。
**Integer 缓存源码:**
@@ -665,12 +670,12 @@ private static long sum() {
```java
float a = 2.0f - 1.9f;
float b = 1.8f - 1.7f;
-System.out.println(a);// 0.100000024
+System.out.printf("%.9f",a);// 0.100000024
System.out.println(b);// 0.099999905
System.out.println(a == b);// false
```
-为什么会出现这个问题呢?
+**为什么会出现这个问题呢?**
这个和计算机保存浮点数的机制有很大关系。我们知道计算机是二进制的,而且计算机在表示一个数字时,宽度是有限的,无限循环的小数存储在计算机时,只能被截断,所以就会导致小数精度发生损失的情况。这也就是解释了为什么浮点数没有办法用二进制精确表示。
@@ -729,7 +734,9 @@ System.out.println(l + 1 == Long.MIN_VALUE); // true
## 变量
-### 成员变量与局部变量的区别?
+### ⭐️成员变量与局部变量的区别?
+
+
- **语法形式**:从语法形式上看,成员变量是属于类的,而局部变量是在代码块或方法中定义的变量或是方法的参数;成员变量可以被 `public`,`private`,`static` 等修饰符所修饰,而局部变量不能被访问控制修饰符及 `static` 所修饰;但是,成员变量和局部变量都能被 `final` 所修饰。
- **存储方式**:从变量在内存中的存储方式来看,如果成员变量是使用 `static` 修饰的,那么这个成员变量是属于类的,如果没有使用 `static` 修饰,这个成员变量是属于实例的。而对象存在于堆内存,局部变量则存在于栈内存。
@@ -738,11 +745,16 @@ System.out.println(l + 1 == Long.MIN_VALUE); // true
**为什么成员变量有默认值?**
-1. 先不考虑变量类型,如果没有默认值会怎样?变量存储的是内存地址对应的任意随机值,程序读取该值运行会出现意外。
+核心原因是为了保证对象状态的安全和可预测性。
+
+成员变量和局部变量在这个规则上不同,主要是因为它们的**生命周期**不一样,导致了编译器对它们的“控制力”也不同。
-2. 默认值有两种设置方式:手动和自动,根据第一点,没有手动赋值一定要自动赋值。成员变量在运行时可借助反射等方法手动赋值,而局部变量不行。
+- **局部变量**只活在一个方法里,编译器能清楚地看到它是否在使用前被赋值,所以编译器会强制你必须手动赋值,否则就报错。
+- **成员变量**是跟着对象走的,它的值可能在构造函数里赋,也可能在后面的某个 `setter` 方法里赋。编译器在编译时**无法预测**它到底什么时候会被赋值。
-3. 对于编译器(javac)来说,局部变量没赋值很好判断,可以直接报错。而成员变量可能是运行时赋值,无法判断,误报“没默认值”又会影响用户体验,所以采用自动赋默认值。
+并且,如果一个变量没有被初始化,它的内存里存放的就是“垃圾值”——之前那块内存遗留下的任意数据。如果程序读取并使用了这个垃圾值,就会产生完全不可预测的结果,比如一个数字变成了随机数,一个对象引用变成了非法地址,这会直接导致程序崩溃或出现诡异的 bug。
+
+为了避免你拿到一个含有“垃圾值”的危险对象,Java干脆为所有成员变量提供了一个安全的默认值(如 null 或 0),作为一种**安全兜底机制**。
成员变量与局部变量代码示例:
@@ -784,6 +796,8 @@ public class VariableExample {
静态变量也就是被 `static` 关键字修饰的变量。它可以被类的所有实例共享,无论一个类创建了多少个对象,它们都共享同一份静态变量。也就是说,静态变量只会被分配一次内存,即使创建多个对象,这样可以节省内存。
+
+
静态变量是通过类名来访问的,例如`StaticVariableExample.staticVar`(如果被 `private`关键字修饰就无法这样访问了)。
```java
@@ -907,7 +921,7 @@ public class Example {
}
```
-### 静态方法和实例方法有何不同?
+### ⭐️静态方法和实例方法有何不同?
**1、调用方式**
@@ -940,7 +954,7 @@ public class Person {
静态方法在访问本类的成员时,只允许访问静态成员(即静态成员变量和静态方法),不允许访问实例成员(即实例成员变量和实例方法),而实例方法不存在这个限制。
-### 重载和重写有什么区别?
+### ⭐️重载和重写有什么区别?
> 重载就是同样的一个方法能够根据输入数据的不同,做出不同的处理
>
@@ -977,14 +991,13 @@ public class Person {
综上:**重写就是子类对父类方法的重新改造,外部样子不能改变,内部逻辑可以改变。**
-| 区别点 | 重载方法 | 重写方法 |
-| :--------- | :------- | :--------------------------------------------------------------- |
-| 发生范围 | 同一个类 | 子类 |
-| 参数列表 | 必须修改 | 一定不能修改 |
-| 返回类型 | 可修改 | 子类方法返回值类型应比父类方法返回值类型更小或相等 |
-| 异常 | 可修改 | 子类方法声明抛出的异常类应比父类方法声明抛出的异常类更小或相等; |
-| 访问修饰符 | 可修改 | 一定不能做更严格的限制(可以降低限制) |
-| 发生阶段 | 编译期 | 运行期 |
+| 区别点 | 重载 (Overloading) | 重写 (Overriding) |
+| -------------- | ------------------------------------------------------------------------------------ | -------------------------------------------------------------------------------------------- |
+| **发生范围** | 同一个类中。 | 父类与子类之间(存在继承关系)。 |
+| **方法签名** | 方法名**必须相同**,但**参数列表必须不同**(参数的类型、个数或顺序至少有一项不同)。 | 方法名、参数列表**必须完全相同**。 |
+| **返回类型** | 与返回值类型**无关**,可以任意修改。 | 子类方法的返回类型必须与父类方法的返回类型**相同**,或者是其**子类**。 |
+| **访问修饰符** | 与访问修饰符**无关**,可以任意修改。 | 子类方法的访问权限**不能低于**父类方法的访问权限。(public > protected > default > private) |
+| **绑定时期** | 编译时绑定或称静态绑定 | 运行时绑定 (Run-time Binding) 或称动态绑定 |
**方法的重写要遵循“两同两小一大”**(以下内容摘录自《疯狂 Java 讲义》,[issue#892](https://github.com/Snailclimb/JavaGuide/issues/892) ):
diff --git a/docs/java/basis/java-basic-questions-02.md b/docs/java/basis/java-basic-questions-02.md
index ed85affa9a2..1279419a032 100644
--- a/docs/java/basis/java-basic-questions-02.md
+++ b/docs/java/basis/java-basic-questions-02.md
@@ -1,22 +1,20 @@
---
title: Java基础常见面试题总结(中)
+description: Java面向对象编程核心知识点总结:涵盖封装继承多态三大特性、接口与抽象类区别、Object类方法详解、深拷贝浅拷贝、String/StringBuffer/StringBuilder对比等,帮助快速掌握Java OOP精髓。
category: Java
tag:
- Java基础
head:
- - meta
- name: keywords
- content: 面向对象,构造方法,接口,抽象类,String,Object
- - - meta
- - name: description
- content: 全网质量最高的Java基础常见知识点和面试题总结,希望对你有帮助!
+ content: 面向对象,封装继承多态,接口,抽象类,深拷贝浅拷贝,Object类,equals,hashCode,String,字符串常量池,Java面试题
---
## 面向对象基础
-### 面向对象和面向过程的区别
+### ⭐️面向对象和面向过程的区别
面向过程编程(Procedural-Oriented Programming,POP)和面向对象编程(Object-Oriented Programming,OOP)是两种常见的编程范式,两者的主要区别在于解决问题的方式不同:
@@ -97,14 +95,14 @@ public class Main {
我们直接定义了圆的半径,并使用该半径直接计算出圆的面积和周长。
-### 创建一个对象用什么运算符?对象实体与对象引用有何不同?
+### 创建一个对象用什么运算符?对象实例与对象引用有何不同?
new 运算符,new 创建对象实例(对象实例在堆内存中),对象引用指向对象实例(对象引用存放在栈内存中)。
- 一个对象引用可以指向 0 个或 1 个对象(一根绳子可以不系气球,也可以系一个气球);
- 一个对象可以有 n 个引用指向它(可以用 n 条绳子系住一个气球)。
-### 对象的相等和引用相等的区别
+### ⭐️对象的相等和引用相等的区别
- 对象的相等一般比较的是内存中存放的内容是否相等。
- 引用相等一般比较的是他们指向的内存地址是否相等。
@@ -156,7 +154,7 @@ true
构造方法**不能被重写(override)**,但**可以被重载(overload)**。因此,一个类中可以有多个构造方法,这些构造方法可以具有不同的参数列表,以提供不同的对象初始化方式。
-### 面向对象三大特征
+### ⭐️面向对象三大特征
#### 封装
@@ -210,7 +208,7 @@ public class Student {
- 多态不能调用“只在子类存在但在父类不存在”的方法;
- 如果子类重写了父类的方法,真正执行的是子类重写的方法,如果子类没有重写父类的方法,执行的是父类的方法。
-### 接口和抽象类有什么共同点和区别?
+### ⭐️接口和抽象类有什么共同点和区别?
#### 接口和抽象类的共同点
@@ -363,7 +361,7 @@ System.out.println(person1.getAddress() == person1Copy.getAddress());

-## Object
+## ⭐️Object
### Object 类的常见方法有哪些?
@@ -455,7 +453,7 @@ System.out.println(42 == 42.0);// true
`String` 中的 `equals` 方法是被重写过的,因为 `Object` 的 `equals` 方法是比较的对象的内存地址,而 `String` 的 `equals` 方法比较的是对象的值。
-当创建 `String` 类型的对象时,虚拟机会在常量池中查找有没有已经存在的值和要创建的值相同的对象,如果有就把它赋给当前引用。如果没有就在常量池中重新创建一个 `String` 对象。
+当使用字符串字面量创建 `String` 类型的对象(如`String aa = "ab"`)时,虚拟机会在常量池中查找有没有已经存在的值和要创建的值相同的对象,如果有就把它赋给当前引用;如果没有,就在常量池中创建一个 `String` 对象并赋给当前引用。但当使用`new`关键字创建对象(如`String a = new String("ab")`)时,虚拟机总是会在堆内存中**创建一个新的对象**并使用常量池中的值(如果没有,会先在字符串常量池中创建字符串对象 "ab")进行初始化,然后赋给当前引用。
`String`类`equals()`方法:
@@ -491,7 +489,7 @@ public boolean equals(Object anObject) {
`hashCode()` 定义在 JDK 的 `Object` 类中,这就意味着 Java 中的任何类都包含有 `hashCode()` 函数。另外需要注意的是:`Object` 的 `hashCode()` 方法是本地方法,也就是用 C 语言或 C++ 实现的。
-> ⚠️ 注意:该方法在 **Oracle OpenJDK8** 中默认是 "使用线程局部状态来实现 Marsaglia's xor-shift 随机数生成", 并不是 "地址" 或者 "地址转换而来", 不同 JDK/VM 可能不同在 **Oracle OpenJDK8** 中有六种生成方式 (其中第五种是返回地址), 通过添加 VM 参数: -XX:hashCode=4 启用第五种。参考源码:
+> ⚠️ 注意:该方法在 **Oracle OpenJDK8** 中默认是 "使用线程局部状态来实现 Marsaglia's xor-shift 随机数生成", 并不是 "地址" 或者 "地址转换而来", 不同 JDK/VM 可能不同。在 **Oracle OpenJDK8** 中有六种生成方式 (其中第五种是返回地址), 通过添加 VM 参数: -XX:hashCode=4 启用第五种。参考源码:
>
> - (1127 行)
> - (537 行开始)
@@ -504,13 +502,18 @@ public native int hashCode();
### 为什么要有 hashCode?
-我们以“`HashSet` 如何检查重复”为例子来说明为什么要有 `hashCode`?
+我们以“HashSet 如何检查重复”为例子来说明为什么要有 hashCode?
-下面这段内容摘自我的 Java 启蒙书《Head First Java》:
+当我们把对象加入 HashSet 时,HashSet 会先调用对象的 `hashCode()` 方法,得到一个“哈希值”,并通过内部散列函数对这个哈希值再做一次简单的转换(比如取余),决定这条数据应该放进底层数组的哪一个桶(bucket,对应到底层数组的某个位置):
-> 当你把对象加入 `HashSet` 时,`HashSet` 会先计算对象的 `hashCode` 值来判断对象加入的位置,同时也会与其他已经加入的对象的 `hashCode` 值作比较,如果没有相符的 `hashCode`,`HashSet` 会假设对象没有重复出现。但是如果发现有相同 `hashCode` 值的对象,这时会调用 `equals()` 方法来检查 `hashCode` 相等的对象是否真的相同。如果两者相同,`HashSet` 就不会让其加入操作成功。如果不同的话,就会重新散列到其他位置。这样我们就大大减少了 `equals` 的次数,相应就大大提高了执行速度。
+1. 如果该桶当前是空的,就直接将对象对应的节点插入到这个桶中。
+2. 如果该桶中已经有其他元素,HashSet 会在这个桶对应的链表或红黑树中逐个比较:
+ - 对于**哈希值不同**的节点,直接跳过;
+ - 对于**哈希值相同**的节点,则会进一步调用 equals() 方法来检查这两个对象是否“相等”:
+ – 如果 `equals()` 返回 true,说明集合中已经存在与当前对象等价的元素,`HashSet` 就不会再次加入它;
+ – 如果返回 false, 则认为是新元素,会将该对象作为一个新节点加入到**同一个桶**的链表或红黑树中。
-其实, `hashCode()` 和 `equals()`都是用于比较两个对象是否相等。
+通过先利用 `hashCode()` 将候选范围缩小到同一个桶内,再在桶内少量元素上调用 `equals()` 做精确判断,`HashSet` 大大减少了 `equals()` 的调用次数,从而提高了查找和插入的执行效率。
**那为什么 JDK 还要同时提供这两个方法呢?**
@@ -551,7 +554,7 @@ public native int hashCode();
## String
-### String、StringBuffer、StringBuilder 的区别?
+### ⭐️String、StringBuffer、StringBuilder 的区别?
**可变性**
@@ -579,6 +582,8 @@ abstract class AbstractStringBuilder implements Appendable, CharSequence {
`String` 中的对象是不可变的,也就可以理解为常量,线程安全。`AbstractStringBuilder` 是 `StringBuilder` 与 `StringBuffer` 的公共父类,定义了一些字符串的基本操作,如 `expandCapacity`、`append`、`insert`、`indexOf` 等公共方法。`StringBuffer` 对方法加了同步锁或者对调用的方法加了同步锁,所以是线程安全的。`StringBuilder` 并没有对方法进行加同步锁,所以是非线程安全的。
+
+
**性能**
每次对 `String` 类型进行改变的时候,都会生成一个新的 `String` 对象,然后将指针指向新的 `String` 对象。`StringBuffer` 每次都会对 `StringBuffer` 对象本身进行操作,而不是生成新的对象并改变对象引用。相同情况下使用 `StringBuilder` 相比使用 `StringBuffer` 仅能获得 10%~15% 左右的性能提升,但却要冒多线程不安全的风险。
@@ -589,7 +594,7 @@ abstract class AbstractStringBuilder implements Appendable, CharSequence {
- 单线程操作字符串缓冲区下操作大量数据: 适用 `StringBuilder`
- 多线程操作字符串缓冲区下操作大量数据: 适用 `StringBuffer`
-### String 为什么是不可变的?
+### ⭐️String 为什么是不可变的?
`String` 类中使用 `final` 关键字修饰字符数组来保存字符串,~~所以`String` 对象是不可变的。~~
@@ -636,7 +641,7 @@ public final class String implements java.io.Serializable, Comparable, C
>
> 这是官方的介绍: 。
-### 字符串拼接用“+” 还是 StringBuilder?
+### ⭐️字符串拼接用“+” 还是 StringBuilder?
Java 语言本身并不支持运算符重载,“+”和“+=”是专门为 String 类重载过的运算符,也是 Java 中仅有的两个重载过的运算符。
@@ -689,26 +694,31 @@ System.out.println(s);
`String` 中的 `equals` 方法是被重写过的,比较的是 String 字符串的值是否相等。 `Object` 的 `equals` 方法是比较的对象的内存地址。
-### 字符串常量池的作用了解吗?
+### ⭐️字符串常量池的作用了解吗?
**字符串常量池** 是 JVM 为了提升性能和减少内存消耗针对字符串(String 类)专门开辟的一块区域,主要目的是为了避免字符串的重复创建。
```java
-// 在堆中创建字符串对象”ab“
-// 将字符串对象”ab“的引用保存在字符串常量池中
+// 1.在字符串常量池中查询字符串对象 "ab",如果没有则创建"ab"并放入字符串常量池
+// 2.将字符串对象 "ab" 的引用赋值给 aa
String aa = "ab";
-// 直接返回字符串常量池中字符串对象”ab“的引用
+// 直接返回字符串常量池中字符串对象 "ab",赋值给引用 bb
String bb = "ab";
-System.out.println(aa==bb);// true
+System.out.println(aa==bb); // true
```
更多关于字符串常量池的介绍可以看一下 [Java 内存区域详解](https://javaguide.cn/java/jvm/memory-area.html) 这篇文章。
-### String s1 = new String("abc");这句话创建了几个字符串对象?
+### ⭐️String s1 = new String("abc");这句话创建了几个字符串对象?
+
+先说答案:会创建 1 或 2 个字符串对象。
-会创建 1 或 2 个字符串对象。
+1. 字符串常量池中不存在 "abc":会创建 2 个 字符串对象。一个在字符串常量池中,由 `ldc` 指令触发创建。一个在堆中,由 `new String()` 创建,并使用常量池中的 "abc" 进行初始化。
+2. 字符串常量池中已存在 "abc":会创建 1 个 字符串对象。该对象在堆中,由 `new String()` 创建,并使用常量池中的 "abc" 进行初始化。
-1、如果字符串常量池中不存在字符串对象“abc”的引用,那么它会在堆上创建两个字符串对象,其中一个字符串对象的引用会被保存在字符串常量池中。
+下面开始详细分析。
+
+1、如果字符串常量池中不存在字符串对象 “abc”,那么它首先会在字符串常量池中创建字符串对象 "abc",然后在堆内存中再创建其中一个字符串对象 "abc"。
示例代码(JDK 1.8):
@@ -718,16 +728,40 @@ String s1 = new String("abc");
对应的字节码:
-
+```java
+// 在堆内存中分配一个尚未初始化的 String 对象。
+// #2 是常量池中的一个符号引用,指向 java/lang/String 类。
+// 在类加载的解析阶段,这个符号引用会被解析成直接引用,即指向实际的 java/lang/String 类。
+0 new #2
+// 复制栈顶的 String 对象引用,为后续的构造函数调用做准备。
+// 此时操作数栈中有两个相同的对象引用:一个用于传递给构造函数,另一个用于保持对新对象的引用,后续将其存储到局部变量表。
+3 dup
+// JVM 先检查字符串常量池中是否存在 "abc"。
+// 如果常量池中已存在 "abc",则直接返回该字符串的引用;
+// 如果常量池中不存在 "abc",则 JVM 会在常量池中创建该字符串字面量并返回它的引用。
+// 这个引用被压入操作数栈,用作构造函数的参数。
+4 ldc #3
+// 调用构造方法,使用从常量池中加载的 "abc" 初始化堆中的 String 对象
+// 新的 String 对象将包含与常量池中的 "abc" 相同的内容,但它是一个独立的对象,存储于堆中。
+6 invokespecial #4 : (Ljava/lang/String;)V>
+// 将堆中的 String 对象引用存储到局部变量表
+9 astore_1
+// 返回,结束方法
+10 return
+```
-`ldc` 命令用于判断字符串常量池中是否保存了对应的字符串对象的引用,如果保存了的话直接返回,如果没有保存的话,会在堆中创建对应的字符串对象并将该字符串对象的引用保存到字符串常量池中。
+`ldc (load constant)` 指令的确是从常量池中加载各种类型的常量,包括字符串常量、整数常量、浮点数常量,甚至类引用等。对于字符串常量,`ldc` 指令的行为如下:
-2、如果字符串常量池中已存在字符串对象“abc”的引用,则只会在堆中创建 1 个字符串对象“abc”。
+1. **从常量池加载字符串**:`ldc` 首先检查字符串常量池中是否已经有内容相同的字符串对象。
+2. **复用已有字符串对象**:如果字符串常量池中已经存在内容相同的字符串对象,`ldc` 会将该对象的引用加载到操作数栈上。
+3. **没有则创建新对象并加入常量池**:如果字符串常量池中没有相同内容的字符串对象,JVM 会在常量池中创建一个新的字符串对象,并将其引用加载到操作数栈中。
+
+2、如果字符串常量池中已存在字符串对象“abc”,则只会在堆中创建 1 个字符串对象“abc”。
示例代码(JDK 1.8):
```java
-// 字符串常量池中已存在字符串对象“abc”的引用
+// 字符串常量池中已存在字符串对象“abc”
String s1 = "abc";
// 下面这段代码只会在堆中创建 1 个字符串对象“abc”
String s2 = new String("abc");
@@ -735,35 +769,48 @@ String s2 = new String("abc");
对应的字节码:
-
+```java
+0 ldc #2
+2 astore_1
+3 new #3
+6 dup
+7 ldc #2
+9 invokespecial #4 : (Ljava/lang/String;)V>
+12 astore_2
+13 return
+```
这里就不对上面的字节码进行详细注释了,7 这个位置的 `ldc` 命令不会在堆中创建新的字符串对象“abc”,这是因为 0 这个位置已经执行了一次 `ldc` 命令,已经在堆中创建过一次字符串对象“abc”了。7 这个位置执行 `ldc` 命令会直接返回字符串常量池中字符串对象“abc”对应的引用。
### String#intern 方法有什么作用?
-`String.intern()` 是一个 native(本地)方法,其作用是将指定的字符串对象的引用保存在字符串常量池中,可以简单分为两种情况:
+`String.intern()` 是一个 `native` (本地) 方法,用来处理字符串常量池中的字符串对象引用。它的工作流程可以概括为以下两种情况:
+
+1. **常量池中已有相同内容的字符串对象**:如果字符串常量池中已经有一个与调用 `intern()` 方法的字符串内容相同的 `String` 对象,`intern()` 方法会直接返回常量池中该对象的引用。
+2. **常量池中没有相同内容的字符串对象**:如果字符串常量池中还没有一个与调用 `intern()` 方法的字符串内容相同的对象,`intern()` 方法会将当前字符串对象的引用添加到字符串常量池中,并返回该引用。
+
+总结:
-- 如果字符串常量池中保存了对应的字符串对象的引用,就直接返回该引用。
-- 如果字符串常量池中没有保存了对应的字符串对象的引用,那就在常量池中创建一个指向该字符串对象的引用并返回。
+- `intern()` 方法的主要作用是确保字符串引用在常量池中的唯一性。
+- 当调用 `intern()` 时,如果常量池中已经存在相同内容的字符串,则返回常量池中已有对象的引用;否则,将该字符串添加到常量池并返回其引用。
示例代码(JDK 1.8) :
```java
-// 在堆中创建字符串对象”Java“
-// 将字符串对象”Java“的引用保存在字符串常量池中
+// s1 指向字符串常量池中的 "Java" 对象
String s1 = "Java";
-// 直接返回字符串常量池中字符串对象”Java“对应的引用
+// s2 也指向字符串常量池中的 "Java" 对象,和 s1 是同一个对象
String s2 = s1.intern();
-// 会在堆中在单独创建一个字符串对象
+// 在堆中创建一个新的 "Java" 对象,s3 指向它
String s3 = new String("Java");
-// 直接返回字符串常量池中字符串对象”Java“对应的引用
+// s4 指向字符串常量池中的 "Java" 对象,和 s1 是同一个对象
String s4 = s3.intern();
-// s1 和 s2 指向的是堆中的同一个对象
+// s1 和 s2 指向的是同一个常量池中的对象
System.out.println(s1 == s2); // true
-// s3 和 s4 指向的是堆中不同的对象
+// s3 指向堆中的对象,s4 指向常量池中的对象,所以不同
System.out.println(s3 == s4); // false
-// s1 和 s4 指向的是堆中的同一个对象
-System.out.println(s1 == s4); //true
+// s1 和 s4 都指向常量池中的同一个对象
+System.out.println(s1 == s4); // true
```
### String 类型的变量和常量做“+”运算时发生了什么?
@@ -844,6 +891,7 @@ public static String getStr() {
## 参考
- 深入解析 String#intern:
+- Java String 源码解读:
- R 大(RednaxelaFX)关于常量折叠的回答:
diff --git a/docs/java/basis/java-basic-questions-03.md b/docs/java/basis/java-basic-questions-03.md
index 5c71236427a..a68ac71ca14 100644
--- a/docs/java/basis/java-basic-questions-03.md
+++ b/docs/java/basis/java-basic-questions-03.md
@@ -1,18 +1,16 @@
---
title: Java基础常见面试题总结(下)
+description: Java高级特性面试题总结:深入讲解异常处理机制、泛型原理、反射应用、注解使用、SPI机制、序列化、IO流模型(BIO/NIO/AIO)、语法糖等核心知识点。
category: Java
tag:
- Java基础
head:
- - meta
- name: keywords
- content: Java异常,泛型,反射,IO,注解
- - - meta
- - name: description
- content: 全网质量最高的Java基础常见知识点和面试题总结,希望对你有帮助!
+ content: Java异常,泛型,反射,注解,SPI,序列化,IO流,语法糖,try-with-resources,BIO NIO AIO,Java面试题
---
-
+
## 异常
@@ -25,9 +23,14 @@ head:
在 Java 中,所有的异常都有一个共同的祖先 `java.lang` 包中的 `Throwable` 类。`Throwable` 类有两个重要的子类:
- **`Exception`** :程序本身可以处理的异常,可以通过 `catch` 来进行捕获。`Exception` 又可以分为 Checked Exception (受检查异常,必须处理) 和 Unchecked Exception (不受检查异常,可以不处理)。
-- **`Error`**:`Error` 属于程序无法处理的错误 ,~~我们没办法通过 `catch` 来进行捕获~~不建议通过`catch`捕获 。例如 Java 虚拟机运行错误(`Virtual MachineError`)、虚拟机内存不够错误(`OutOfMemoryError`)、类定义错误(`NoClassDefFoundError`)等 。这些异常发生时,Java 虚拟机(JVM)一般会选择线程终止。
+- **`Error`** :`Error` 属于程序无法处理的错误 ,~~我们没办法通过 `catch` 来进行捕获~~不建议通过`catch`捕获 。例如 Java 虚拟机运行错误(`Virtual MachineError`)、虚拟机内存不够错误(`OutOfMemoryError`)、类定义错误(`NoClassDefFoundError`)等 。这些异常发生时,Java 虚拟机(JVM)一般会选择线程终止。
+
+### ClassNotFoundException 和 NoClassDefFoundError 的区别
+
+- `ClassNotFoundException` 是Exception,发生在使用反射等动态加载时找不到类,是可预期的,可以捕获处理。
+- `NoClassDefFoundError` 是Error,是编译时存在的类,在运行时链接不到了(比如 jar 包缺失),是环境问题,导致 JVM 无法继续。
-### Checked Exception 和 Unchecked Exception 有什么区别?
+### ⭐️Checked Exception 和 Unchecked Exception 有什么区别?
**Checked Exception** 即 受检查异常 ,Java 代码在编译过程中,如果受检查异常没有被 `catch`或者`throws` 关键字处理的话,就没办法通过编译。
@@ -53,10 +56,18 @@ head:

+### 你更倾向于使用 Checked Exception 还是 Unchecked Exception?
+
+默认使用 Unchecked Exception,只在必要时才用 Checked Exception。
+
+我们可以把 Unchecked Exception(比如 `NullPointerException`)看作是代码 Bug。对待 Bug,最好的方式是让它暴露出来然后去修复代码,而不是用 `try-catch` 去掩盖它。
+
+一般来说,只在一种情况下使用 Checked Exception:当这个异常是业务逻辑的一部分,并且调用方必须处理它时。比如说,一个余额不足异常。这不是 bug,而是一个正常的业务分支,我需要用 Checked Exception 来强制调用者去处理这种情况,比如提示用户去充值。这样就能在保证关键业务逻辑完整性的同时,让代码尽可能保持简洁。
+
### Throwable 类常用方法有哪些?
-- `String getMessage()`: 返回异常发生时的简要描述
-- `String toString()`: 返回异常发生时的详细信息
+- `String getMessage()`: 返回异常发生时的详细信息
+- `String toString()`: 返回异常发生时的简要描述
- `String getLocalizedMessage()`: 返回异常对象的本地化信息。使用 `Throwable` 的子类覆盖这个方法,可以生成本地化信息。如果子类没有覆盖该方法,则该方法返回的信息与 `getMessage()`返回的结果相同
- `void printStackTrace()`: 在控制台上打印 `Throwable` 对象封装的异常信息
@@ -89,14 +100,6 @@ Finally
**注意:不要在 finally 语句块中使用 return!** 当 try 语句和 finally 语句中都有 return 语句时,try 语句块中的 return 语句会被忽略。这是因为 try 语句中的 return 返回值会先被暂存在一个本地变量中,当执行到 finally 语句中的 return 之后,这个本地变量的值就变为了 finally 语句中的 return 返回值。
-[jvm 官方文档](https://docs.oracle.com/javase/specs/jvms/se7/html/jvms-4.html#jvms-4.10.2.5)中有明确提到:
-
-> If the `try` clause executes a _return_, the compiled code does the following:
->
-> 1. Saves the return value (if any) in a local variable.
-> 2. Executes a _jsr_ to the code for the `finally` clause.
-> 3. Upon return from the `finally` clause, returns the value saved in the local variable.
-
代码示例:
```java
@@ -213,11 +216,11 @@ catch (IOException e) {
}
```
-### 异常使用有哪些需要注意的地方?
+### ⭐️异常使用有哪些需要注意的地方?
- 不要把异常定义为静态变量,因为这样会导致异常栈信息错乱。每次手动抛出异常,我们都需要手动 new 一个异常对象抛出。
- 抛出的异常信息一定要有意义。
-- 建议抛出更加具体的异常比如字符串转换为数字格式错误的时候应该抛出`NumberFormatException`而不是其父类`IllegalArgumentException`。
+- 建议抛出更加具体的异常,比如字符串转换为数字格式错误的时候应该抛出`NumberFormatException`而不是其父类`IllegalArgumentException`。
- 避免重复记录日志:如果在捕获异常的地方已经记录了足够的信息(包括异常类型、错误信息和堆栈跟踪等),那么在业务代码中再次抛出这个异常时,就不应该再次记录相同的错误信息。重复记录日志会使得日志文件膨胀,并且可能会掩盖问题的实际原因,使得问题更难以追踪和解决。
- ……
@@ -325,56 +328,122 @@ printArray( stringArray );
- 构建集合工具类(参考 `Collections` 中的 `sort`, `binarySearch` 方法)。
- ……
-## 反射
+## ⭐️反射
+
+关于反射的详细解读,请看这篇文章 [Java 反射机制详解](https://javaguide.cn/java/basis/reflection.html) 。
-关于反射的详细解读,请看这篇文章 [Java 反射机制详解](./reflection.md) 。
+### 什么是反射?
-### 何谓反射?
+简单来说,Java 反射 (Reflection) 是一种**在程序运行时,动态地获取类的信息并操作类或对象(方法、属性)的能力**。
-如果说大家研究过框架的底层原理或者咱们自己写过框架的话,一定对反射这个概念不陌生。反射之所以被称为框架的灵魂,主要是因为它赋予了我们在运行时分析类以及执行类中方法的能力。通过反射你可以获取任意一个类的所有属性和方法,你还可以调用这些方法和属性。
+通常情况下,我们写的代码在编译时类型就已经确定了,要调用哪个方法、访问哪个字段都是明确的。但反射允许我们在**运行时**才去探知一个类有哪些方法、哪些属性、它的构造函数是怎样的,甚至可以动态地创建对象、调用方法或修改属性,哪怕这些方法或属性是私有的。
-### 反射的优缺点?
+正是这种在运行时“反观自身”并进行操作的能力,使得反射成为许多**通用框架和库的基石**。它让代码更加灵活,能够处理在编译时未知的类型。
-反射可以让我们的代码更加灵活、为各种框架提供开箱即用的功能提供了便利。
+### 反射有什么优缺点?
-不过,反射让我们在运行时有了分析操作类的能力的同时,也增加了安全问题,比如可以无视泛型参数的安全检查(泛型参数的安全检查发生在编译时)。另外,反射的性能也要稍差点,不过,对于框架来说实际是影响不大的。
+**优点:**
+
+1. **灵活性和动态性**:反射允许程序在运行时动态地加载类、创建对象、调用方法和访问字段。这样可以根据实际需求(如配置文件、用户输入、注解等)动态地适应和扩展程序的行为,显著提高了系统的灵活性和适应性。
+2. **框架开发的基础**:许多现代 Java 框架(如 Spring、Hibernate、MyBatis)都大量使用反射来实现依赖注入(DI)、面向切面编程(AOP)、对象关系映射(ORM)、注解处理等核心功能。反射是实现这些“魔法”功能不可或缺的基础工具。
+3. **解耦合和通用性**:通过反射,可以编写更通用、可重用和高度解耦的代码,降低模块之间的依赖。例如,可以通过反射实现通用的对象拷贝、序列化、Bean 工具等。
+
+**缺点:**
+
+1. **性能开销**:反射操作通常比直接代码调用要慢。因为涉及到动态类型解析、方法查找以及 JIT 编译器的优化受限等因素。不过,对于大多数框架场景,这种性能损耗通常是可以接受的,或者框架本身会做一些缓存优化。
+2. **安全性问题**:反射可以绕过 Java 语言的访问控制机制(如访问 `private` 字段和方法),破坏了封装性,可能导致数据泄露或程序被恶意篡改。此外,还可以绕过泛型检查,带来类型安全隐患。
+3. **代码可读性和维护性**:过度使用反射会使代码变得复杂、难以理解和调试。错误通常在运行时才会暴露,不像编译期错误那样容易发现。
相关阅读:[Java Reflection: Why is it so slow?](https://stackoverflow.com/questions/1392351/java-reflection-why-is-it-so-slow) 。
### 反射的应用场景?
-像咱们平时大部分时候都是在写业务代码,很少会接触到直接使用反射机制的场景。但是!这并不代表反射没有用。相反,正是因为反射,你才能这么轻松地使用各种框架。像 Spring/Spring Boot、MyBatis 等等框架中都大量使用了反射机制。
+我们平时写业务代码可能很少直接跟 Java 的反射(Reflection)打交道。但你可能没意识到,你天天都在享受反射带来的便利!**很多流行的框架,比如 Spring/Spring Boot、MyBatis 等,底层都大量运用了反射机制**,这才让它们能够那么灵活和强大。
+
+下面简单列举几个最场景的场景帮助大家理解。
+
+**1.依赖注入与控制反转(IoC)**
-**这些框架中也大量使用了动态代理,而动态代理的实现也依赖反射。**
+以 Spring/Spring Boot 为代表的 IoC 框架,会在启动时扫描带有特定注解(如 `@Component`, `@Service`, `@Repository`, `@Controller`)的类,利用反射实例化对象(Bean),并通过反射注入依赖(如 `@Autowired`、构造器注入等)。
-比如下面是通过 JDK 实现动态代理的示例代码,其中就使用了反射类 `Method` 来调用指定的方法。
+**2.注解处理**
+
+注解本身只是个“标记”,得有人去读这个标记才知道要做什么。反射就是那个“读取器”。框架通过反射检查类、方法、字段上有没有特定的注解,然后根据注解信息执行相应的逻辑。比如,看到 `@Value`,就用反射读取注解内容,去配置文件找对应的值,再用反射把值设置给字段。
+
+**3.动态代理与 AOP**
+
+想在调用某个方法前后自动加点料(比如打日志、开事务、做权限检查)?AOP(面向切面编程)就是干这个的,而动态代理是实现 AOP 的常用手段。JDK 自带的动态代理(Proxy 和 InvocationHandler)就离不开反射。代理对象在内部调用真实对象的方法时,就是通过反射的 `Method.invoke` 来完成的。
```java
public class DebugInvocationHandler implements InvocationHandler {
- /**
- * 代理类中的真实对象
- */
- private final Object target;
+ private final Object target; // 真实对象
- public DebugInvocationHandler(Object target) {
- this.target = target;
- }
+ public DebugInvocationHandler(Object target) { this.target = target; }
- public Object invoke(Object proxy, Method method, Object[] args) throws InvocationTargetException, IllegalAccessException {
- System.out.println("before method " + method.getName());
+ // proxy: 代理对象, method: 被调用的方法, args: 方法参数
+ public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
+ System.out.println("切面逻辑:调用方法 " + method.getName() + " 之前");
+ // 通过反射调用真实对象的同名方法
Object result = method.invoke(target, args);
- System.out.println("after method " + method.getName());
+ System.out.println("切面逻辑:调用方法 " + method.getName() + " 之后");
return result;
}
}
-
```
-另外,像 Java 中的一大利器 **注解** 的实现也用到了反射。
+**4.对象关系映射(ORM)**
+
+像 MyBatis、Hibernate 这种框架,能帮你把数据库查出来的一行行数据,自动变成一个个 Java 对象。它是怎么知道数据库字段对应哪个 Java 属性的?还是靠反射。它通过反射获取 Java 类的属性列表,然后把查询结果按名字或配置对应起来,再用反射调用 setter 或直接修改字段值。反过来,保存对象到数据库时,也是用反射读取属性值来拼 SQL。
+
+## 代理
+
+关于 Java 代理的详细介绍,可以看看笔者写的 [Java 代理模式详解](https://javaguide.cn/java/basis/proxy.html "Java 代理模式详解")这篇文章。
+
+### 如何实现动态代理?
+
+动态代理是一种非常强大的设计模式,它允许我们在**不修改源代码**的情况下,对一个类或对象的方法进行**功能增强(Enhancement)**。
+
+在 Java 中,实现动态代理最主流的方式有两种:**JDK 动态代理** 和 **CGLIB 动态代理**。
+
+**第一种:JDK 动态代理**
+
+Java 官方提供的,其核心要求是目标类必须实现一个或多个接口。JDK 动态代理在运行时,会利用 `Proxy.newProxyInstance()` 方法,动态地创建一个实现了这些接口的代理类的实例。这个代理类在内存中生成,你看不到它的 `.java` 或 `.class` 文件。
+
+当你调用代理对象的任何一个方法时,这个调用都会被转发到我们提供的一个 `InvocationHandler` 接口的 `invoke` 方法中。在 `invoke` 方法里,我们就可以在调用原始方法(目标方法)之前或之后,加入我们自己的增强逻辑。
+
+**第二种:CGLIB 动态代理**
+
+CGLIB 是一个第三方的代码生成库。它的原理与 JDK 完全不同,它不要求被代理的类实现接口。它在运行时,动态生成目标类的子类作为代理类(通过 ASM 字节码操作技术)。然后,它会重写父类(也就是被代理类)中所有非 `final`、`private` 和 `static` 的方法。
+
+当你调用代理对象的任何一个方法时,这个调用会被 CGLIB 的 `MethodInterceptor` 接口的 `intercept` 方法拦截。和 `InvocationHandler` 的 `invoke` 方法一样,我们可以在 `intercept` 方法里,在调用原始的父类方法之前或之后,加入我们的增强逻辑。
+
+### 静态代理和动态代理有什么区别?
+
+静态代理和动态代理的核心差异在于 **代理关系的确定时机、实现灵活性及维护成本** 。
+
+| 对比维度 | 静态代理 (Static Proxy) | 动态代理 (Dynamic Proxy) |
+| ---------------- | ---------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------ |
+| 代理关系确定时机 | 编译期(编译后生成固定的 `.class` 字节码文件) | 运行时(动态生成代理类字节码并加载到 JVM) |
+| 实现方式 | 手动编写代理类,需与目标类实现同一接口,一对一绑定 | 无需手动编写代理类,通过 `Handler`/`Interceptor` 封装增强逻辑,一对多复用 |
+| 接口依赖 | 必须实现接口(代理类与目标类遵循同一接口规范) | 支持代理接口或直接代理实现类 |
+| 代码量与维护性 | 代码量大(目标类越多,代理类越多),维护成本高;接口新增方法时,目标类与代理类需同步修改 | 代码量极少(通用增强逻辑可复用),维护性好;与接口解耦,接口变更不影响代理逻辑 |
+| 核心优势 | 实现简单、逻辑直观,无额外框架依赖 | 灵活性强、复用性高,降低重复编码,适配复杂场景 |
+| 典型应用场景 | 简单的装饰器模式、少量固定类的增强需求 | Spring AOP、RPC 框架(如 Dubbo)、ORM 框架 |
+
+### ⭐️JDK 动态代理和 CGLIB 动态代理有什么区别?
+
+1. JDK 动态代理是官方的,它要求被代理的类必须实现接口。它的原理是动态生成一个接口的实现类来作为代理。CGLIB 是第三方的,它不需要接口。它的原理是动态生成一个被代理类的子类来作为代理。但也正因为是继承,所以它不能代理 `final` 的类,被代理的方法也不能是 `final` 或 `private` 。
+2. 就二者的效率来说,大部分情况都是 JDK 动态代理更优秀,随着 JDK 版本的升级,这个优势更加明显。
+
+### ⭐️介绍一下动态代理在框架中的实际应用场景
+
+动态代理最典型的应用场景就是**Spring AOP**。
+
+AOP(Aspect-Oriented Programming:面向切面编程)能够将那些与业务无关,却为业务模块所共同调用的逻辑或责任(例如事务处理、日志管理、权限控制等)封装起来,便于减少系统的重复代码,降低模块间的耦合度,并有利于未来的可拓展性和可维护性。
-为什么你使用 Spring 的时候 ,一个`@Component`注解就声明了一个类为 Spring Bean 呢?为什么你通过一个 `@Value`注解就读取到配置文件中的值呢?究竟是怎么起作用的呢?
+Spring AOP 就是基于动态代理的,如果要代理的对象,实现了某个接口,那么 Spring AOP 会使用 **JDK Proxy**,去创建代理对象,而对于没有实现接口的对象,就无法使用 JDK Proxy 去进行代理了,这时候 Spring AOP 会使用 **Cglib** 生成一个被代理对象的子类来作为代理,如下图所示:
-这些都是因为你可以基于反射分析类,然后获取到类/属性/方法/方法的参数上的注解。你获取到注解之后,就可以做进一步的处理。
+
## 注解
@@ -405,9 +474,9 @@ JDK 提供了很多内置的注解(比如 `@Override`、`@Deprecated`),同
- **编译期直接扫描**:编译器在编译 Java 代码的时候扫描对应的注解并处理,比如某个方法使用`@Override` 注解,编译器在编译的时候就会检测当前的方法是否重写了父类对应的方法。
- **运行期通过反射处理**:像框架中自带的注解(比如 Spring 框架的 `@Value`、`@Component`)都是通过反射来进行处理的。
-## SPI
+## ⭐️SPI
-关于 SPI 的详细解读,请看这篇文章 [Java SPI 机制详解](./spi.md) 。
+关于 SPI 的详细解读,请看这篇文章 [Java SPI 机制详解](https://javaguide.cn/java/basis/spi.html) 。
### 何谓 SPI?
@@ -430,7 +499,7 @@ SPI 将服务接口和具体的服务实现分离开来,将服务调用方和
一般模块之间都是通过接口进行通讯,因此我们在服务调用方和服务实现方(也称服务提供者)之间引入一个“接口”。
- 当实现方提供了接口和实现,我们可以通过调用实现方的接口从而拥有实现方给我们提供的能力,这就是 **API**。这种情况下,接口和实现都是放在实现方的包中。调用方通过接口调用实现方的功能,而不需要关心具体的实现细节。
-- 当接口存在于调用方这边时,这就是 **SPI ** 。由接口调用方确定接口规则,然后由不同的厂商根据这个规则对这个接口进行实现,从而提供服务。
+- 当接口存在于调用方这边时,这就是 **SPI** 。由接口调用方确定接口规则,然后由不同的厂商根据这个规则对这个接口进行实现,从而提供服务。
举个通俗易懂的例子:公司 H 是一家科技公司,新设计了一款芯片,然后现在需要量产了,而市面上有好几家芯片制造业公司,这个时候,只要 H 公司指定好了这芯片生产的标准(定义好了接口标准),那么这些合作的芯片公司(服务提供者)就按照标准交付自家特色的芯片(提供不同方案的实现,但是给出来的结果是一样的)。
@@ -441,9 +510,9 @@ SPI 将服务接口和具体的服务实现分离开来,将服务调用方和
- 需要遍历加载所有的实现类,不能做到按需加载,这样效率还是相对较低的。
- 当多个 `ServiceLoader` 同时 `load` 时,会有并发问题。
-## 序列化和反序列化
+## ⭐️序列化和反序列化
-关于序列化和反序列化的详细解读,请看这篇文章 [Java 序列化详解](./serialization.md) ,里面涉及到的知识点和面试题更全面。
+关于序列化和反序列化的详细解读,请看这篇文章 [Java 序列化详解](https://javaguide.cn/java/basis/serialization.html) ,里面涉及到的知识点和面试题更全面。
### 什么是序列化?什么是反序列化?
@@ -451,8 +520,8 @@ SPI 将服务接口和具体的服务实现分离开来,将服务调用方和
简单来说:
-- **序列化**:将数据结构或对象转换成二进制字节流的过程
-- **反序列化**:将在序列化过程中所生成的二进制字节流转换成数据结构或者对象的过程
+- **序列化**:将数据结构或对象转换成可以存储或传输的形式,通常是二进制字节流,也可以是 JSON, XML 等文本格式
+- **反序列化**:将在序列化过程中所生成的数据转换为原始数据结构或者对象的过程
对于 Java 这种面向对象编程语言来说,我们序列化的都是对象(Object)也就是实例化后的类(Class),但是在 C++这种半面向对象的语言中,struct(结构体)定义的是数据结构类型,而 class 对应的是对象类型。
@@ -492,7 +561,7 @@ SPI 将服务接口和具体的服务实现分离开来,将服务调用方和
对于不想进行序列化的变量,使用 `transient` 关键字修饰。
-`transient` 关键字的作用是:阻止实例中那些用此关键字修饰的的变量序列化;当对象被反序列化时,被 `transient` 修饰的变量值不会被持久化和恢复。
+`transient` 关键字的作用是:阻止实例中那些用此关键字修饰的变量序列化;当对象被反序列化时,被 `transient` 修饰的变量值不会被持久化和恢复。
关于 `transient` 还有几点注意:
@@ -518,9 +587,9 @@ JDK 自带的序列化方式一般不会用 ,因为序列化效率低并且存
关于 I/O 的详细解读,请看下面这几篇文章,里面涉及到的知识点和面试题更全面。
-- [Java IO 基础知识总结](../io/io-basis.md)
-- [Java IO 设计模式总结](../io/io-design-patterns.md)
-- [Java IO 模型详解](../io/io-model.md)
+- [Java IO 基础知识总结](https://javaguide.cn/java/io/io-basis.html)
+- [Java IO 设计模式总结](https://javaguide.cn/java/io/io-design-patterns.html)
+- [Java IO 模型详解](https://javaguide.cn/java/io/io-model.html)
### Java IO 流了解吗?
@@ -542,11 +611,11 @@ Java IO 流的 40 多个类都是从如下 4 个抽象类基类中派生出来
### Java IO 中的设计模式有哪些?
-参考答案:[Java IO 设计模式总结](../io/io-design-patterns.md)
+参考答案:[Java IO 设计模式总结](https://javaguide.cn/java/io/io-design-patterns.html)
-### BIO、NIO 和 AIO 的区别?
+### ⭐️BIO、NIO 和 AIO 的区别?
-参考答案:[Java IO 模型详解](../io/io-model.md)
+参考答案:[Java IO 模型详解](https://javaguide.cn/java/io/io-model.html)
## 语法糖
diff --git a/docs/java/basis/java-keyword-summary.md b/docs/java/basis/java-keyword-summary.md
index daf13c9ec14..d69513a26c2 100644
--- a/docs/java/basis/java-keyword-summary.md
+++ b/docs/java/basis/java-keyword-summary.md
@@ -1,3 +1,15 @@
+---
+title: Java 关键字总结
+description: 系统总结Java常用关键字:详解final、static、this、super、volatile、transient、synchronized等关键字用法与区别,助力Java开发者掌握核心语法。
+category: Java
+tag:
+ - Java基础
+head:
+ - - meta
+ - name: keywords
+ content: Java关键字,final关键字,static关键字,this关键字,super关键字,volatile,transient,synchronized
+---
+
# final,static,this,super 关键字总结
## final 关键字
@@ -54,7 +66,7 @@ super 关键字用于从子类访问父类的变量和方法。 例如:
```java
public class Super {
protected int number;
- protected showNumber() {
+ protected void showNumber() {
System.out.println("number = " + number);
}
}
@@ -199,7 +211,7 @@ public class Singleton {
```java
//将Math中的所有静态资源导入,这时候可以直接使用里面的静态方法,而不用通过类名进行调用
//如果只想导入单一某个静态方法,只需要将*换成对应的方法名即可
-import static java.lang.Math.*;//换成import static java.lang.Math.max;具有一样的效果
+import static java.lang.Math.*;//换成import static java.lang.Math.max;即可指定单一静态方法max导入
public class Demo {
public static void main(String[] args) {
int max = max(1,2);
@@ -250,7 +262,7 @@ bar.method2();
不同点:静态代码块在非静态代码块之前执行(静态代码块 -> 非静态代码块 -> 构造方法)。静态代码块只在第一次 new 执行一次,之后不再执行,而非静态代码块在每 new 一次就执行一次。 非静态代码块可在普通方法中定义(不过作用不大);而静态代码块不行。
> **🐛 修正(参见:[issue #677](https://github.com/Snailclimb/JavaGuide/issues/677))**:静态代码块可能在第一次 new 对象的时候执行,但不一定只在第一次 new 的时候执行。比如通过 `Class.forName("ClassDemo")`创建 Class 对象的时候也会执行,即 new 或者 `Class.forName("ClassDemo")` 都会执行静态代码块。
-> 一般情况下,如果有些代码比如一些项目最常用的变量或对象必须在项目启动的时候就执行的时候,需要使用静态代码块,这种代码是主动执行的。如果我们想要设计不需要创建对象就可以调用类中的方法,例如:`Arrays` 类,`Character` 类,`String` 类等,就需要使用静态方法, 两者的区别是 静态代码块是自动执行的而静态方法是被调用的时候才执行的.
+> 一般情况下,如果有些代码比如一些项目最常用的变量或对象必须在项目启动的时候就执行,需要使用静态代码块,这种代码是主动执行的。如果我们想要设计不需要创建对象就可以调用类中的方法,例如:`Arrays` 类,`Character` 类,`String` 类等,就需要使用静态方法, 两者的区别是 静态代码块是自动执行的而静态方法是被调用的时候才执行的.
Example:
diff --git a/docs/java/basis/proxy.md b/docs/java/basis/proxy.md
index 615b0f00e42..1882d0f8c4e 100644
--- a/docs/java/basis/proxy.md
+++ b/docs/java/basis/proxy.md
@@ -1,8 +1,13 @@
---
title: Java 代理模式详解
+description: 详解Java代理模式原理与实现:对比静态代理与动态代理差异,深入分析JDK动态代理和CGLIB代理机制,理解AOP横切关注点实现。
category: Java
tag:
- Java基础
+head:
+ - - meta
+ - name: keywords
+ content: Java代理模式,静态代理,动态代理,JDK动态代理,CGLIB代理,AOP,设计模式,代理实现
---
## 1. 代理模式
@@ -21,7 +26,7 @@ tag:
## 2. 静态代理
-**静态代理中,我们对目标对象的每个方法的增强都是手动完成的(_后面会具体演示代码_),非常不灵活(_比如接口一旦新增加方法,目标对象和代理对象都要进行修改_)且麻烦(_需要对每个目标类都单独写一个代理类_)。** 实际应用场景非常非常少,日常开发几乎看不到使用静态代理的场景。
+静态代理中,我们对目标对象的每个方法的增强都是手动完成的(后面会具体演示代码),非常不灵活(比如接口一旦新增加方法,目标对象和代理对象都要进行修改)且麻烦(需要对每个目标类都单独写一个代理类)。 实际应用场景非常非常少,日常开发几乎看不到使用静态代理的场景。
上面我们是从实现和应用角度来说的静态代理,从 JVM 层面来说, **静态代理在编译时就将接口、实现类、代理类这些都变成了一个个实际的 class 文件。**
@@ -99,7 +104,7 @@ after method send()
## 3. 动态代理
-相比于静态代理来说,动态代理更加灵活。我们不需要针对每个目标类都单独创建一个代理类,并且也不需要我们必须实现接口,我们可以直接代理实现类( _CGLIB 动态代理机制_)。
+相比于静态代理来说,动态代理更加灵活。我们不需要针对每个目标类都单独创建一个代理类,并且也不需要我们必须实现接口,我们可以直接代理实现类( CGLIB 动态代理机制)。
**从 JVM 角度来说,动态代理是在运行时动态生成类字节码,并加载到 JVM 中的。**
@@ -330,10 +335,10 @@ public class DebugMethodInterceptor implements MethodInterceptor {
/**
- * @param o 被代理的对象(需要增强的对象)
+ * @param o 代理对象本身(注意不是原始对象,如果使用method.invoke(o, args)会导致循环调用)
* @param method 被拦截的方法(需要增强的方法)
* @param args 方法入参
- * @param methodProxy 用于调用原始方法
+ * @param methodProxy 高性能的方法调用机制,避免反射开销
*/
@Override
public Object intercept(Object o, Method method, Object[] args, MethodProxy methodProxy) throws Throwable {
@@ -387,13 +392,21 @@ after method send
### 3.3. JDK 动态代理和 CGLIB 动态代理对比
-1. **JDK 动态代理只能代理实现了接口的类或者直接代理接口,而 CGLIB 可以代理未实现任何接口的类。** 另外, CGLIB 动态代理是通过生成一个被代理类的子类来拦截被代理类的方法调用,因此不能代理声明为 final 类型的类和方法。
+1. JDK 动态代理是官方的,它要求被代理的类必须实现接口。它的原理是动态生成一个接口的实现类来作为代理。CGLIB 是第三方的,它不需要接口。它的原理是动态生成一个被代理类的子类来作为代理。但也正因为是继承,所以它不能代理 `final` 的类,被代理的方法也不能是 `final` 或 `private` 。
2. 就二者的效率来说,大部分情况都是 JDK 动态代理更优秀,随着 JDK 版本的升级,这个优势更加明显。
## 4. 静态代理和动态代理的对比
-1. **灵活性**:动态代理更加灵活,不需要必须实现接口,可以直接代理实现类,并且可以不需要针对每个目标类都创建一个代理类。另外,静态代理中,接口一旦新增加方法,目标对象和代理对象都要进行修改,这是非常麻烦的!
-2. **JVM 层面**:静态代理在编译时就将接口、实现类、代理类这些都变成了一个个实际的 class 文件。而动态代理是在运行时动态生成类字节码,并加载到 JVM 中的。
+静态代理和动态代理的核心差异在于 **代理关系的确定时机、实现灵活性及维护成本** 。
+
+| 对比维度 | 静态代理 (Static Proxy) | 动态代理 (Dynamic Proxy) |
+| ---------------- | ---------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------ |
+| 代理关系确定时机 | 编译期(编译后生成固定的 `.class` 字节码文件) | 运行时(动态生成代理类字节码并加载到 JVM) |
+| 实现方式 | 手动编写代理类,需与目标类实现同一接口,一对一绑定 | 无需手动编写代理类,通过 `Handler`/`Interceptor` 封装增强逻辑,一对多复用 |
+| 接口依赖 | 必须实现接口(代理类与目标类遵循同一接口规范) | 支持代理接口或直接代理实现类 |
+| 代码量与维护性 | 代码量大(目标类越多,代理类越多),维护成本高;接口新增方法时,目标类与代理类需同步修改 | 代码量极少(通用增强逻辑可复用),维护性好;与接口解耦,接口变更不影响代理逻辑 |
+| 核心优势 | 实现简单、逻辑直观,无额外框架依赖 | 灵活性强、复用性高,降低重复编码,适配复杂场景 |
+| 典型应用场景 | 简单的装饰器模式、少量固定类的增强需求 | Spring AOP、RPC 框架(如 Dubbo)、ORM 框架 |
## 5. 总结
diff --git a/docs/java/basis/reflection.md b/docs/java/basis/reflection.md
index 769d85e620f..c4a233e908f 100644
--- a/docs/java/basis/reflection.md
+++ b/docs/java/basis/reflection.md
@@ -1,8 +1,13 @@
---
title: Java 反射机制详解
+description: 深入讲解Java反射机制原理与应用:掌握Class、Method、Field核心API,理解反射在Spring、MyBatis等框架中的应用,学习动态代理实现。
category: Java
tag:
- Java基础
+head:
+ - - meta
+ - name: keywords
+ content: Java反射,反射机制,Class类,Method方法,Field字段,动态代理,框架原理,运行时操作
---
## 何为反射?
@@ -116,7 +121,7 @@ public class TargetObject {
}
```
-2. 使用反射操作这个类的方法以及参数
+2. 使用反射操作这个类的方法以及属性
```java
package cn.javaguide;
@@ -177,7 +182,7 @@ I love JavaGuide
value is JavaGuide
```
-**注意** : 有读者提到上面代码运行会抛出 `ClassNotFoundException` 异常,具体原因是你没有下面把这段代码的包名替换成自己创建的 `TargetObject` 所在的包 。
+**注意** : 有读者提到上面代码运行会抛出 `ClassNotFoundException` 异常,具体原因是你没有下面把这段代码的包名替换成自己创建的 `TargetObject` 所在的包 。
可以参考: 这篇文章。
```java
diff --git a/docs/java/basis/serialization.md b/docs/java/basis/serialization.md
index 3f0dd6f8a74..254032b9ef7 100644
--- a/docs/java/basis/serialization.md
+++ b/docs/java/basis/serialization.md
@@ -1,8 +1,13 @@
---
title: Java 序列化详解
+description: 深入解析Java序列化与反序列化机制:详解Serializable接口、transient关键字、serialVersionUID作用、序列化协议选择及RPC、缓存等应用场景。
category: Java
tag:
- Java基础
+head:
+ - - meta
+ - name: keywords
+ content: Java序列化,反序列化,Serializable接口,transient关键字,serialVersionUID,序列化协议,对象持久化
---
## 什么是序列化和反序列化?
@@ -11,8 +16,8 @@ tag:
简单来说:
-- **序列化**:将数据结构或对象转换成二进制字节流的过程
-- **反序列化**:将在序列化过程中所生成的二进制字节流转换成数据结构或者对象的过程
+- **序列化**:将数据结构或对象转换成可以存储或传输的形式,通常是二进制字节流,也可以是 JSON, XML 等文本格式
+- **反序列化**:将在序列化过程中所生成的数据转换为原始数据结构或者对象的过程
对于 Java 这种面向对象编程语言来说,我们序列化的都是对象(Object)也就是实例化后的类(Class),但是在 C++这种半面向对象的语言中,struct(结构体)定义的是数据结构类型,而 class 对应的是对象类型。
@@ -83,7 +88,11 @@ public class RpcRequest implements Serializable {
~~`static` 修饰的变量是静态变量,位于方法区,本身是不会被序列化的。 `static` 变量是属于类的而不是对象。你反序列之后,`static` 变量的值就像是默认赋予给了对象一样,看着就像是 `static` 变量被序列化,实际只是假象罢了。~~
-**🐛 修正(参见:[issue#2174](https://github.com/Snailclimb/JavaGuide/issues/2174))**:`static` 修饰的变量是静态变量,属于类而非类的实例,本身是不会被序列化的。然而,`serialVersionUID` 是一个特例,`serialVersionUID` 的序列化做了特殊处理。当一个对象被序列化时,`serialVersionUID` 会被写入到序列化的二进制流中;在反序列化时,也会解析它并做一致性判断,以此来验证序列化对象的版本一致性。如果两者不匹配,反序列化过程将抛出 `InvalidClassException`,因为这通常意味着序列化的类的定义已经发生了更改,可能不再兼容。
+**🐛 修正(参见:[issue#2174](https://github.com/Snailclimb/JavaGuide/issues/2174))**:
+
+通常情况下,`static` 变量是属于类的,不属于任何单个对象实例,所以它们本身不会被包含在对象序列化的数据流里。序列化保存的是对象的状态(也就是实例变量的值)。然而,`serialVersionUID` 是一个特例,`serialVersionUID` 的序列化做了特殊处理。关键在于,`serialVersionUID` 不是作为对象状态的一部分被序列化的,而是被序列化机制本身用作一个特殊的“指纹”或“版本号”。
+
+当一个对象被序列化时,`serialVersionUID` 会被写入到序列化的二进制流中(像是在保存一个版本号,而不是保存 `static` 变量本身的状态);在反序列化时,也会解析它并做一致性判断,以此来验证序列化对象的版本一致性。如果两者不匹配,反序列化过程将抛出 `InvalidClassException`,因为这通常意味着序列化的类的定义已经发生了更改,可能不再兼容。
官方说明如下:
@@ -91,13 +100,13 @@ public class RpcRequest implements Serializable {
>
> 如果想显式指定 `serialVersionUID` ,则需要在类中使用 `static` 和 `final` 关键字来修饰一个 `long` 类型的变量,变量名字必须为 `"serialVersionUID"` 。
-也就是说,`serialVersionUID` 只是用来被 JVM 识别,实际并没有被序列化。
+也就是说,`serialVersionUID` 本身(作为 static 变量)确实不作为对象状态被序列化。但是,它的值被 Java 序列化机制特殊处理了——作为一个版本标识符被读取并写入序列化流中,用于在反序列化时进行版本兼容性检查。
**如果有些字段不想进行序列化怎么办?**
对于不想进行序列化的变量,可以使用 `transient` 关键字修饰。
-`transient` 关键字的作用是:阻止实例中那些用此关键字修饰的的变量序列化;当对象被反序列化时,被 `transient` 修饰的变量值不会被持久化和恢复。
+`transient` 关键字的作用是:阻止实例中那些用此关键字修饰的变量序列化;当对象被反序列化时,被 `transient` 修饰的变量值不会被持久化和恢复。
关于 `transient` 还有几点注意:
diff --git a/docs/java/basis/spi.md b/docs/java/basis/spi.md
index a6388bfd248..7392d0f3168 100644
--- a/docs/java/basis/spi.md
+++ b/docs/java/basis/spi.md
@@ -1,15 +1,13 @@
---
title: Java SPI 机制详解
+description: 全面讲解Java SPI机制原理与应用:理解ServiceLoader服务发现机制、SPI在JDBC/Dubbo/Spring中的应用、与API对比及最佳实践。
category: Java
tag:
- Java基础
head:
- - meta
- name: keywords
- content: Java SPI机制
- - - meta
- - name: description
- content: SPI 即 Service Provider Interface ,字面意思就是:“服务提供者的接口”,我的理解是:专门提供给服务提供者或者扩展框架功能的开发者去使用的一个接口。SPI 将服务接口和具体的服务实现分离开来,将服务调用方和服务实现者解耦,能够提升程序的扩展性、可维护性。修改或者替换服务实现并不需要修改调用方。
+ content: Java SPI,SPI机制,ServiceLoader,服务发现,插件化,JDBC驱动加载,Dubbo扩展,SPI应用
---
> 本文来自 [Kingshion](https://github.com/jjx0708) 投稿。欢迎更多朋友参与到 JavaGuide 的维护工作,这是一件非常有意义的事情。详细信息请看:[JavaGuide 贡献指南](https://javaguide.cn/javaguide/contribution-guideline.html) 。
@@ -41,7 +39,7 @@ SPI 将服务接口和具体的服务实现分离开来,将服务调用方和
一般模块之间都是通过接口进行通讯,因此我们在服务调用方和服务实现方(也称服务提供者)之间引入一个“接口”。
- 当实现方提供了接口和实现,我们可以通过调用实现方的接口从而拥有实现方给我们提供的能力,这就是 **API**。这种情况下,接口和实现都是放在实现方的包中。调用方通过接口调用实现方的功能,而不需要关心具体的实现细节。
-- 当接口存在于调用方这边时,这就是 **SPI ** 。由接口调用方确定接口规则,然后由不同的厂商根据这个规则对这个接口进行实现,从而提供服务。
+- 当接口存在于调用方这边时,这就是 **SPI** 。由接口调用方确定接口规则,然后由不同的厂商根据这个规则对这个接口进行实现,从而提供服务。
举个通俗易懂的例子:公司 H 是一家科技公司,新设计了一款芯片,然后现在需要量产了,而市面上有好几家芯片制造业公司,这个时候,只要 H 公司指定好了这芯片生产的标准(定义好了接口标准),那么这些合作的芯片公司(服务提供者)就按照标准交付自家特色的芯片(提供不同方案的实现,但是给出来的结果是一样的)。
@@ -555,7 +553,7 @@ public class MyServiceLoader {
其实不难发现,SPI 机制的具体实现本质上还是通过反射完成的。即:**我们按照规定将要暴露对外使用的具体实现类在 `META-INF/services/` 文件下声明。**
-另外,SPI 机制在很多框架中都有应用:Spring 框架的基本原理也是类似的方式。还有 Dubbo 框架提供同样的 SPI 扩展机制,只不过 Dubbo 和 spring 框架中的 SPI 机制具体实现方式跟咱们今天学得这个有些细微的区别,不过整体的原理都是一致的,相信大家通过对 JDK 中 SPI 机制的学习,能够一通百通,加深对其他高深框的理解。
+另外,SPI 机制在很多框架中都有应用:Spring 框架的基本原理也是类似的方式。还有 Dubbo 框架提供同样的 SPI 扩展机制,只不过 Dubbo 和 spring 框架中的 SPI 机制具体实现方式跟咱们今天学得这个有些细微的区别,不过整体的原理都是一致的,相信大家通过对 JDK 中 SPI 机制的学习,能够一通百通,加深对其他高深框架的理解。
通过 SPI 机制能够大大地提高接口设计的灵活性,但是 SPI 机制也存在一些缺点,比如:
diff --git a/docs/java/basis/syntactic-sugar.md b/docs/java/basis/syntactic-sugar.md
index 64ffd4cde17..cc5eef45a45 100644
--- a/docs/java/basis/syntactic-sugar.md
+++ b/docs/java/basis/syntactic-sugar.md
@@ -1,15 +1,13 @@
---
title: Java 语法糖详解
+description: 深入剖析Java语法糖原理:详解自动装箱拆箱、泛型擦除、增强for、可变参数、枚举、Lambda等语法糖的编译期实现机制,避免使用误区。
category: Java
tag:
- Java基础
head:
- - meta
- name: keywords
- content: Java 语法糖
- - - meta
- - name: description
- content: 这篇文章介绍了 12 种 Java 中常用的语法糖。所谓语法糖就是提供给开发人员便于开发的一种语法而已。但是这种语法只有开发人员认识。要想被执行,需要进行解糖,即转成 JVM 认识的语法。当我们把语法糖解糖之后,你就会发现其实我们日常使用的这些方便的语法,其实都是一些其他更简单的语法构成的。有了这些语法糖,我们在日常开发的时候可以大大提升效率,但是同时也要避免过渡使用。使用之前最好了解下原理,避免掉坑。
+ content: Java语法糖,自动装箱拆箱,泛型擦除,增强for循环,可变参数,枚举,内部类,Lambda表达式,语法糖原理
---
> 作者:Hollis
@@ -246,7 +244,7 @@ public static transient void print(String strs[])
}
```
-从反编译后代码可以看出,可变参数在被使用的时候,他首先会创建一个数组,数组的长度就是调用该方法是传递的实参的个数,然后再把参数值全部放到这个数组当中,然后再把这个数组作为参数传递到被调用的方法中。(注:`trasient` 仅在修饰成员变量时有意义,此处 “修饰方法” 是由于在 javassist 中使用相同数值分别表示 `trasient` 以及 `vararg`,见 [此处](https://github.com/jboss-javassist/javassist/blob/7302b8b0a09f04d344a26ebe57f29f3db43f2a3e/src/main/javassist/bytecode/AccessFlag.java#L32)。)
+从反编译后代码可以看出,可变参数在被使用的时候,他首先会创建一个数组,数组的长度就是调用该方法是传递的实参的个数,然后再把参数值全部放到这个数组当中,然后再把这个数组作为参数传递到被调用的方法中。(注:`transient` 仅在修饰成员变量时有意义,此处 “修饰方法” 是由于在 javassist 中使用相同数值分别表示 `transient` 以及 `vararg`,见 [此处](https://github.com/jboss-javassist/javassist/blob/7302b8b0a09f04d344a26ebe57f29f3db43f2a3e/src/main/javassist/bytecode/AccessFlag.java#L32)。)
### 枚举
@@ -263,6 +261,7 @@ public enum t {
然后我们使用反编译,看看这段代码到底是怎么实现的,反编译后代码内容如下:
```java
+//Java编译器会自动将枚举名处理为合法类名(首字母大写): t -> T
public final class T extends Enum
{
private T(String s, int i)
@@ -308,7 +307,7 @@ public final class T extends Enum
**内部类之所以也是语法糖,是因为它仅仅是一个编译时的概念,`outer.java`里面定义了一个内部类`inner`,一旦编译成功,就会生成两个完全不同的`.class`文件了,分别是`outer.class`和`outer$inner.class`。所以内部类的名字完全可以和它的外部类名字相同。**
```java
-public class OutterClass {
+public class OuterClass {
private String userName;
public String getUserName() {
@@ -337,10 +336,10 @@ public class OutterClass {
}
```
-以上代码编译后会生成两个 class 文件:`OutterClass$InnerClass.class`、`OutterClass.class` 。当我们尝试对`OutterClass.class`文件进行反编译的时候,命令行会打印以下内容:`Parsing OutterClass.class...Parsing inner class OutterClass$InnerClass.class... Generating OutterClass.jad` 。他会把两个文件全部进行反编译,然后一起生成一个`OutterClass.jad`文件。文件内容如下:
+以上代码编译后会生成两个 class 文件:`OuterClass$InnerClass.class`、`OuterClass.class` 。当我们尝试对`OuterClass.class`文件进行反编译的时候,命令行会打印以下内容:`Parsing OuterClass.class...Parsing inner class OuterClass$InnerClass.class... Generating OuterClass.jad` 。他会把两个文件全部进行反编译,然后一起生成一个`OuterClass.jad`文件。文件内容如下:
```java
-public class OutterClass
+public class OuterClass
{
class InnerClass
{
@@ -353,16 +352,16 @@ public class OutterClass
this.name = name;
}
private String name;
- final OutterClass this$0;
+ final OuterClass this$0;
InnerClass()
{
- this.this$0 = OutterClass.this;
+ this.this$0 = OuterClass.this;
super();
}
}
- public OutterClass()
+ public OuterClass()
{
}
public String getUserName()
@@ -379,6 +378,83 @@ public class OutterClass
}
```
+**为什么内部类可以使用外部类的 private 属性**:
+
+我们在 InnerClass 中增加一个方法,打印外部类的 userName 属性
+
+```java
+//省略其他属性
+public class OuterClass {
+ private String userName;
+ ......
+ class InnerClass{
+ ......
+ public void printOut(){
+ System.out.println("Username from OuterClass:"+userName);
+ }
+ }
+}
+
+// 此时,使用javap -p命令对OuterClass反编译结果:
+public classOuterClass {
+ private String userName;
+ ......
+ static String access$000(OuterClass);
+}
+// 此时,InnerClass的反编译结果:
+class OuterClass$InnerClass {
+ final OuterClass this$0;
+ ......
+ public void printOut();
+}
+
+```
+
+实际上,在编译完成之后,inner 实例内部会有指向 outer 实例的引用`this$0`,但是简单的`outer.name`是无法访问 private 属性的。从反编译的结果可以看到,outer 中会有一个桥方法`static String access$000(OuterClass)`,恰好返回 String 类型,即 userName 属性。正是通过这个方法实现内部类访问外部类私有属性。所以反编译后的`printOut()`方法大致如下:
+
+```java
+public void printOut() {
+ System.out.println("Username from OuterClass:" + OuterClass.access$000(this.this$0));
+}
+```
+
+补充:
+
+1. 匿名内部类、局部内部类、静态内部类也是通过桥方法来获取 private 属性。
+2. 静态内部类没有`this$0`的引用
+3. 匿名内部类、局部内部类通过复制使用局部变量,该变量初始化之后就不能被修改。以下是一个案例:
+
+```java
+public class OuterClass {
+ private String userName;
+
+ public void test(){
+ //这里i初始化为1后就不能再被修改
+ int i=1;
+ class Inner{
+ public void printName(){
+ System.out.println(userName);
+ System.out.println(i);
+ }
+ }
+ }
+}
+```
+
+反编译后:
+
+```java
+//javap命令反编译Inner的结果
+//i被复制进内部类,且为final
+class OuterClass$1Inner {
+ final int val$i;
+ final OuterClass this$0;
+ OuterClass$1Inner();
+ public void printName();
+}
+
+```
+
### 条件编译
—般情况下,程序中的每一行代码都要参加编译。但有时候出于对程序代码优化的考虑,希望只对其中一部分内容进行编译,此时就需要在程序中加上条件,让编译器只对满足条件的代码进行编译,将不满足条件的代码舍弃,这就是条件编译。
@@ -423,7 +499,7 @@ public class ConditionalCompilation
首先,我们发现,在反编译后的代码中没有`System.out.println("Hello, ONLINE!");`,这其实就是条件编译。当`if(ONLINE)`为 false 的时候,编译器就没有对其内的代码进行编译。
-所以,**Java 语法的条件编译,是通过判断条件为常量的 if 语句实现的。其原理也是 Java 语言的语法糖。根据 if 判断条件的真假,编译器直接把分支为 false 的代码块消除。通过该方式实现的条件编译,必须在方法体内实现,而无法在正整个 Java 类的结构或者类的属性上进行条件编译,这与 C/C++的条件编译相比,确实更有局限性。在 Java 语言设计之初并没有引入条件编译的功能,虽有局限,但是总比没有更强。**
+所以,**Java 语法的条件编译,是通过判断条件为常量的 if 语句实现的。其原理也是 Java 语言的语法糖。根据 if 判断条件的真假,编译器直接把分支为 false 的代码块消除。通过该方式实现的条件编译,必须在方法体内实现,而无法在整个 Java 类的结构或者类的属性上进行条件编译,这与 C/C++的条件编译相比,确实更有局限性。在 Java 语言设计之初并没有引入条件编译的功能,虽有局限,但是总比没有更强。**
### 断言
@@ -624,7 +700,7 @@ public static transient void main(String args[])
}
else
br.close();
- break MISSING_BLOCK_LABEL_113;
+ break MISSING_BLOCK_LABEL_113; //该标签为反编译工具的生成错误,(不是Java语法本身的内容)属于反编译工具的临时占位符。正常情况下编译器生成的字节码不会包含这种无效标签。
Exception exception;
exception;
if(br != null)
diff --git a/docs/java/basis/unsafe.md b/docs/java/basis/unsafe.md
index c9e0c984f15..cc624113852 100644
--- a/docs/java/basis/unsafe.md
+++ b/docs/java/basis/unsafe.md
@@ -1,8 +1,13 @@
---
title: Java 魔法类 Unsafe 详解
+description: 深入解析Java魔法类Unsafe:讲解Unsafe直接内存操作、CAS原子操作、对象实例化等底层能力,理解JUC并发工具类实现原理及使用风险。
category: Java
tag:
- Java基础
+head:
+ - - meta
+ - name: keywords
+ content: Unsafe类,内存操作,CAS原子操作,堆外内存,直接内存,sun.misc.Unsafe,JUC底层实现
---
> 本文整理完善自下面这两篇优秀的文章:
@@ -10,6 +15,8 @@ tag:
> - [Java 魔法类:Unsafe 应用解析 - 美团技术团队 -2019](https://tech.meituan.com/2019/02/14/talk-about-java-magic-class-unsafe.html)
> - [Java 双刃剑之 Unsafe 类详解 - 码农参上 - 2021](https://xie.infoq.cn/article/8b6ed4195e475bfb32dacc5cb)
+
+
阅读过 JUC 源码的同学,一定会发现很多并发工具类都调用了一个叫做 `Unsafe` 的类。
那这个类主要是用来干什么的呢?有什么使用场景呢?这篇文章就带你搞清楚!
@@ -132,20 +139,34 @@ public native void freeMemory(long address);
```java
private void memoryTest() {
int size = 4;
- long addr = unsafe.allocateMemory(size);
- long addr3 = unsafe.reallocateMemory(addr, size * 2);
- System.out.println("addr: "+addr);
- System.out.println("addr3: "+addr3);
+ // 1. 分配初始内存
+ long oldAddr = unsafe.allocateMemory(size);
+ System.out.println("Initial address: " + oldAddr);
+
+ // 2. 向初始内存写入数据
+ unsafe.putInt(oldAddr, 16843009); // 写入 0x01010101
+ System.out.println("Value at oldAddr: " + unsafe.getInt(oldAddr));
+
+ // 3. 重新分配内存
+ long newAddr = unsafe.reallocateMemory(oldAddr, size * 2);
+ System.out.println("New address: " + newAddr);
+
+ // 4. reallocateMemory 已经将数据从 oldAddr 拷贝到 newAddr
+ // 所以 newAddr 的前4个字节应该和 oldAddr 的内容一样
+ System.out.println("Value at newAddr (first 4 bytes): " + unsafe.getInt(newAddr));
+
+ // 关键:之后所有操作都应该基于 newAddr,oldAddr 已失效!
try {
- unsafe.setMemory(null,addr ,size,(byte)1);
- for (int i = 0; i < 2; i++) {
- unsafe.copyMemory(null,addr,null,addr3+size*i,4);
- }
- System.out.println(unsafe.getInt(addr));
- System.out.println(unsafe.getLong(addr3));
- }finally {
- unsafe.freeMemory(addr);
- unsafe.freeMemory(addr3);
+ // 5. 在新内存块的后半部分写入新数据
+ unsafe.putInt(newAddr + size, 33686018); // 写入 0x02020202
+
+ // 6. 读取整个8字节的long值
+ System.out.println("Value at newAddr (full 8 bytes): " + unsafe.getLong(newAddr));
+
+ } finally {
+ // 7. 只释放最后有效的内存地址
+ unsafe.freeMemory(newAddr);
+ // 如果尝试 freeMemory(oldAddr),将会导致 double free 错误!
}
}
```
@@ -153,35 +174,56 @@ private void memoryTest() {
先看结果输出:
```plain
-addr: 2433733895744
-addr3: 2433733894944
-16843009
-72340172838076673
+Initial address: 140467048086752
+Value at oldAddr: 16843009
+New address: 140467048086752
+Value at newAddr (first 4 bytes): 16843009
+Value at newAddr (full 8 bytes): 144680345659310337
```
-分析一下运行结果,首先使用`allocateMemory`方法申请 4 字节长度的内存空间,调用`setMemory`方法向每个字节写入内容为`byte`类型的 1,当使用 Unsafe 调用`getInt`方法时,因为一个`int`型变量占 4 个字节,会一次性读取 4 个字节,组成一个`int`的值,对应的十进制结果为 16843009。
+`reallocateMemory` 的行为类似于 C 语言中的 realloc 函数,它会尝试在不移动数据的情况下扩展或收缩内存块。其行为主要有两种情况:
-你可以通过下图理解这个过程:
+1. **原地扩容**:如果当前内存块后面有足够的连续空闲空间,`reallocateMemory` 会直接在原地址上扩展内存,并返回原始地址。
+2. **异地扩容**:如果当前内存块后面空间不足,它会寻找一个新的、足够大的内存区域,将旧数据拷贝过去,然后释放旧的内存地址,并返回新地址。
-
+**结合本次的运行结果,我们可以进行如下分析:**
-在代码中调用`reallocateMemory`方法重新分配了一块 8 字节长度的内存空间,通过比较`addr`和`addr3`可以看到和之前申请的内存地址是不同的。在代码中的第二个 for 循环里,调用`copyMemory`方法进行了两次内存的拷贝,每次拷贝内存地址`addr`开始的 4 个字节,分别拷贝到以`addr3`和`addr3+4`开始的内存空间上:
+**第一步:初始分配与写入**
-
+- `unsafe.allocateMemory(size)` 分配了 4 字节的堆外内存,地址为 `140467048086752`。
+- `unsafe.putInt(oldAddr, 16843009)` 向该地址写入了 int 值 `16843009`,其十六进制表示为 `0x01010101`。`getInt` 读取正确,证明写入成功。
-拷贝完成后,使用`getLong`方法一次性读取 8 个字节,得到`long`类型的值为 72340172838076673。
+**第二步:原地内存扩容**
-需要注意,通过这种方式分配的内存属于 堆外内存 ,是无法进行垃圾回收的,需要我们把这些内存当做一种资源去手动调用`freeMemory`方法进行释放,否则会产生内存泄漏。通用的操作内存方式是在`try`中执行对内存的操作,最终在`finally`块中进行内存的释放。
+- `long newAddr = unsafe.reallocateMemory(oldAddr, size * 2)` 尝试将内存块扩容至 8 字节。
+- 观察输出 New address: `140467048086752`,我们发现 `newAddr` 与 `oldAddr` 的值**完全相同**。
+- 这表明本次操作触发了“原地扩容”。系统在原地址 `140467048086752` 后面找到了足够的空间,直接将内存块扩展到了 8 字节。在这个过程中,旧的地址 `oldAddr` 依然有效,并且就是 `newAddr`,数据也并未发生移动。
-**为什么要使用堆外内存?**
+**第三步:验证数据与写入新数据**
-- 对垃圾回收停顿的改善。由于堆外内存是直接受操作系统管理而不是 JVM,所以当我们使用堆外内存时,即可保持较小的堆内内存规模。从而在 GC 时减少回收停顿对于应用的影响。
-- 提升程序 I/O 操作的性能。通常在 I/O 通信过程中,会存在堆内内存到堆外内存的数据拷贝操作,对于需要频繁进行内存间数据拷贝且生命周期较短的暂存数据,都建议存储到堆外内存。
+- `unsafe.getInt(newAddr)` 再次读取前 4 个字节,结果仍是 `16843009`,验证了原数据完好无损。
+- `unsafe.putInt(newAddr + size, 33686018)` 在扩容出的后 4 个字节(偏移量为 4)写入了新的 int 值 `33686018`(十六进制为 `0x02020202`)。
+
+**第四步:读取完整数据**
+
+- `unsafe.getLong(newAddr)` 从起始地址读取一个 long 值(8 字节)。此时内存中的 8 字节内容为 `0x01010101` (低地址) 和 `0x02020202` (高地址) 的拼接。
+- 在小端字节序(Little-Endian)的机器上,这 8 字节在内存中会被解释为十六进制数 `0x0202020201010101`。
+- 这个十六进制数转换为十进制,结果正是 `144680345659310337`。这完美地解释了最终的输出结果。
+
+**第五步:安全的内存释放**
+
+- `finally` 块中,`unsafe.freeMemory(newAddr)` 安全地释放了整个 8 字节的内存块。
+- 由于本次是原地扩容(`oldAddr == newAddr`),所以即使错误地多写一句 `freeMemory(oldAddr)` 也会导致二次释放的严重错误。
#### 典型应用
`DirectByteBuffer` 是 Java 用于实现堆外内存的一个重要类,通常用在通信过程中做缓冲池,如在 Netty、MINA 等 NIO 框架中应用广泛。`DirectByteBuffer` 对于堆外内存的创建、使用、销毁等逻辑均由 Unsafe 提供的堆外内存 API 来实现。
+**为什么要使用堆外内存?**
+
+- 对垃圾回收停顿的改善。由于堆外内存是直接受操作系统管理而不是 JVM,所以当我们使用堆外内存时,即可保持较小的堆内内存规模。从而在 GC 时减少回收停顿对于应用的影响。
+- 提升程序 I/O 操作的性能。通常在 I/O 通信过程中,会存在堆内内存到堆外内存的数据拷贝操作,对于需要频繁进行内存间数据拷贝且生命周期较短的暂存数据,都建议存储到堆外内存。
+
下图为 `DirectByteBuffer` 构造函数,创建 `DirectByteBuffer` 的时候,通过 `Unsafe.allocateMemory` 分配内存、`Unsafe.setMemory` 进行内存初始化,而后构建 `Cleaner` 对象用于跟踪 `DirectByteBuffer` 对象的垃圾回收,以实现当 `DirectByteBuffer` 被垃圾回收时,分配的堆外内存一起被释放。
```java
@@ -514,11 +556,94 @@ private void increment(int x){
1 2 3 4 5 6 7 8 9
```
-在上面的例子中,使用两个线程去修改`int`型属性`a`的值,并且只有在`a`的值等于传入的参数`x`减一时,才会将`a`的值变为`x`,也就是实现对`a`的加一的操作。流程如下所示:
+如果你把上面这段代码贴到 IDE 中运行,会发现并不能得到目标输出结果。有朋友已经在 Github 上指出了这个问题:[issue#2650](https://github.com/Snailclimb/JavaGuide/issues/2650)。下面是修正后的代码:
+
+```java
+private volatile int a = 0; // 共享变量,初始值为 0
+private static final Unsafe unsafe;
+private static final long fieldOffset;
+
+static {
+ try {
+ // 获取 Unsafe 实例
+ Field theUnsafe = Unsafe.class.getDeclaredField("theUnsafe");
+ theUnsafe.setAccessible(true);
+ unsafe = (Unsafe) theUnsafe.get(null);
+ // 获取 a 字段的内存偏移量
+ fieldOffset = unsafe.objectFieldOffset(CasTest.class.getDeclaredField("a"));
+ } catch (Exception e) {
+ throw new RuntimeException("Failed to initialize Unsafe or field offset", e);
+ }
+}
+
+public static void main(String[] args) {
+ CasTest casTest = new CasTest();
+
+ Thread t1 = new Thread(() -> {
+ for (int i = 1; i <= 4; i++) {
+ casTest.incrementAndPrint(i);
+ }
+ });
+
+ Thread t2 = new Thread(() -> {
+ for (int i = 5; i <= 9; i++) {
+ casTest.incrementAndPrint(i);
+ }
+ });
+
+ t1.start();
+ t2.start();
+
+ // 等待线程结束,以便观察完整输出 (可选,用于演示)
+ try {
+ t1.join();
+ t2.join();
+ } catch (InterruptedException e) {
+ Thread.currentThread().interrupt();
+ }
+}
+
+// 将递增和打印操作封装在一个原子性更强的方法内
+private void incrementAndPrint(int targetValue) {
+ while (true) {
+ int currentValue = a; // 读取当前 a 的值
+ // 只有当 a 的当前值等于目标值的前一个值时,才尝试更新
+ if (currentValue == targetValue - 1) {
+ if (unsafe.compareAndSwapInt(this, fieldOffset, currentValue, targetValue)) {
+ // CAS 成功,说明成功将 a 更新为 targetValue
+ System.out.print(targetValue + " ");
+ break; // 成功更新并打印后退出循环
+ }
+ // 如果 CAS 失败,意味着在读取 currentValue 和执行 CAS 之间,a 的值被其他线程修改了,
+ // 此时 currentValue 已经不是 a 的最新值,需要重新读取并重试。
+ }
+ // 如果 currentValue != targetValue - 1,说明还没轮到当前线程更新,
+ // 或者已经被其他线程更新超过了,让出CPU给其他线程机会。
+ // 对于严格顺序递增的场景,如果 current > targetValue - 1,可能意味着逻辑错误或死循环,
+ // 但在此示例中,我们期望线程能按顺序执行。
+ Thread.yield(); // 提示CPU调度器可以切换线程,减少无效自旋
+ }
+}
+```
+
+在上述例子中,我们创建了两个线程,它们都尝试修改共享变量 a。每个线程在调用 `incrementAndPrint(targetValue)` 方法时:
+
+1. 会先读取 a 的当前值 `currentValue`。
+2. 检查 `currentValue` 是否等于 `targetValue - 1` (即期望的前一个值)。
+3. 如果条件满足,则调用`unsafe.compareAndSwapInt()` 尝试将 `a` 从 `currentValue` 更新到 `targetValue`。
+4. 如果 CAS 操作成功(返回 true),则打印 `targetValue` 并退出循环。
+5. 如果 CAS 操作失败,或者 `currentValue` 不满足条件,则当前线程会继续循环(自旋),并通过 `Thread.yield()` 尝试让出 CPU,直到成功更新并打印或者条件满足。
+
+这种机制确保了每个数字(从 1 到 9)只会被成功设置并打印一次,并且是按顺序进行的。

-需要注意的是,在调用`compareAndSwapInt`方法后,会直接返回`true`或`false`的修改结果,因此需要我们在代码中手动添加自旋的逻辑。在`AtomicInteger`类的设计中,也是采用了将`compareAndSwapInt`的结果作为循环条件,直至修改成功才退出死循环的方式来实现的原子性的自增操作。
+需要注意的是:
+
+1. **自旋逻辑:** `compareAndSwapInt` 方法本身只执行一次比较和交换操作,并立即返回结果。因此,为了确保操作最终成功(在值符合预期的情况下),我们需要在代码中显式地实现自旋逻辑(如 `while(true)` 循环),不断尝试直到 CAS 操作成功。
+2. **`AtomicInteger` 的实现:** JDK 中的 `java.util.concurrent.atomic.AtomicInteger` 类内部正是利用了类似的 CAS 操作和自旋逻辑来实现其原子性的 `getAndIncrement()`, `compareAndSet()` 等方法。直接使用 `AtomicInteger` 通常是更安全、更推荐的做法,因为它封装了底层的复杂性。
+3. **ABA 问题:** CAS 操作本身存在 ABA 问题(一个值从 A 变为 B,再变回 A,CAS 检查时会认为值没有变过)。在某些场景下,如果值的变化历史很重要,可能需要使用 `AtomicStampedReference` 来解决。但在本例的简单递增场景中,ABA 问题通常不构成影响。
+4. **CPU 消耗:** 长时间的自旋会消耗 CPU 资源。在竞争激烈或条件长时间不满足的情况下,可以考虑加入更复杂的退避策略(如 `Thread.sleep()` 或 `LockSupport.parkNanos()`)来优化。
### 线程调度
diff --git a/docs/java/basis/why-there-only-value-passing-in-java.md b/docs/java/basis/why-there-only-value-passing-in-java.md
index 296a6ec9c60..18cc70caee8 100644
--- a/docs/java/basis/why-there-only-value-passing-in-java.md
+++ b/docs/java/basis/why-there-only-value-passing-in-java.md
@@ -1,8 +1,13 @@
---
title: Java 值传递详解
+description: 详解Java为什么只有值传递:通过示例深入分析Java参数传递机制,澄清值传递与引用传递的常见误区,理解形参实参本质区别。
category: Java
tag:
- Java基础
+head:
+ - - meta
+ - name: keywords
+ content: Java值传递,引用传递,参数传递,形参实参,对象引用,方法调用,Java传参机制
---
开始之前,我们先来搞懂下面这两个概念:
@@ -32,9 +37,9 @@ void sayHello(String str) {
程序设计语言将实参传递给方法(或函数)的方式分为两种:
- **值传递**:方法接收的是实参值的拷贝,会创建副本。
-- **引用传递**:方法接收的直接是实参所引用的对象在堆中的地址,不会创建副本,对形参的修改将影响到实参。
+- **引用传递**:方法接收的直接是实参的地址,而不是实参内的值,这就是指针,此时形参就是实参,对形参的任何修改都会反应到实参,包括重新赋值。
-很多程序设计语言(比如 C++、 Pascal )提供了两种参数传递的方式,不过,在 Java 中只有值传递。
+很多程序设计语言(比如 C++、 Pascal)提供了两种参数传递的方式,不过,在 Java 中只有值传递。
## 为什么 Java 只有值传递?
diff --git a/docs/java/collection/arrayblockingqueue-source-code.md b/docs/java/collection/arrayblockingqueue-source-code.md
index 2eb9eb78455..24631e5954b 100644
--- a/docs/java/collection/arrayblockingqueue-source-code.md
+++ b/docs/java/collection/arrayblockingqueue-source-code.md
@@ -1,8 +1,13 @@
---
title: ArrayBlockingQueue 源码分析
+description: ArrayBlockingQueue源码深度解析:详解有界阻塞队列实现、生产者消费者模式应用、ReentrantLock+Condition并发控制、线程池工作队列机制。
category: Java
tag:
- Java集合
+head:
+ - - meta
+ - name: keywords
+ content: ArrayBlockingQueue源码,阻塞队列,有界队列,生产者消费者模式,ReentrantLock,Condition,线程池工作队列
---
## 阻塞队列简介
@@ -11,7 +16,7 @@ tag:
Java 阻塞队列的历史可以追溯到 JDK1.5 版本,当时 Java 平台增加了 `java.util.concurrent`,即我们常说的 JUC 包,其中包含了各种并发流程控制工具、并发容器、原子类等。这其中自然也包含了我们这篇文章所讨论的阻塞队列。
-为了解决高并发场景下多线程之间数据共享的问题,JDK1.5 版本中出现了 `ArrayBlockingQueue` 和 `LinkedBlockingQueue`,它们是带有生产者-消费者模式实现的并发容器。其中,`ArrayBlockingQueue` 是有界队列,即添加的元素达到上限之后,再次添加就会被阻塞或者抛出异常。而 `LinkedBlockingQueue` 则由链表构成的队列,正是因为链表的特性,所以 `LinkedBlockingQueue` 在添加元素上并不会向 `ArrayBlockingQueue` 那样有着较多的约束,所以 `LinkedBlockingQueue` 设置队列是否有界是可选的(注意这里的无界并不是指可以添加任务数量的元素,而是说队列的大小默认为 `Integer.MAX_VALUE`,近乎于无限大)。
+为了解决高并发场景下多线程之间数据共享的问题,JDK1.5 版本中出现了 `ArrayBlockingQueue` 和 `LinkedBlockingQueue`,它们是带有生产者-消费者模式实现的并发容器。其中,`ArrayBlockingQueue` 是有界队列,即添加的元素达到上限之后,再次添加就会被阻塞或者抛出异常。而 `LinkedBlockingQueue` 则由链表构成的队列,正是因为链表的特性,所以 `LinkedBlockingQueue` 在添加元素上并不会向 `ArrayBlockingQueue` 那样有着较多的约束,所以 `LinkedBlockingQueue` 设置队列是否有界是可选的(注意这里的无界并不是指可以添加任意数量的元素,而是说队列的大小默认为 `Integer.MAX_VALUE`,近乎于无限大)。
随着 Java 的不断发展,JDK 后续的几个版本又对阻塞队列进行了不少的更新和完善:
@@ -119,8 +124,8 @@ public class ProducerConsumerExample {
生产者添加元素:2
消费者取出元素:1
消费者取出元素:2
-消费者取出元素:3
生产者添加元素:3
+消费者取出元素:3
生产者添加元素:4
生产者添加元素:5
消费者取出元素:4
@@ -226,11 +231,11 @@ public class DrainToExample {

-从图中我们可以看出,`ArrayBlockingQueue` 继承了阻塞队列 `BlockingQueue` 这个接口,不难猜出通过继承 `BlockingQueue` 这个接口之后,`ArrayBlockingQueue` 就拥有了阻塞队列那些常见的操作行为。
+从图中我们可以看出,`ArrayBlockingQueue` 实现了阻塞队列 `BlockingQueue` 这个接口,不难猜出通过实现 `BlockingQueue` 这个接口之后,`ArrayBlockingQueue` 就拥有了阻塞队列那些常见的操作行为。
同时, `ArrayBlockingQueue` 还继承了 `AbstractQueue` 这个抽象类,这个继承了 `AbstractCollection` 和 `Queue` 的抽象类,从抽象类的特定和语义我们也可以猜出,这个继承关系使得 `ArrayBlockingQueue` 拥有了队列的常见操作。
-所以我们是否可以得出这样一个结论,通过继承 `AbstractQueue` 获得队列所有的操作模板,其实现的入队和出队操作的整体框架。然后 `ArrayBlockingQueue` 通过继承 `BlockingQueue` 获取到阻塞队列的常见操作并将这些操作实现,填充到 `AbstractQueue` 模板方法的细节中,由此 `ArrayBlockingQueue` 成为一个完整的阻塞队列。
+所以我们是否可以得出这样一个结论,通过继承 `AbstractQueue` 获得队列所有的操作模板,其实现的入队和出队操作的整体框架。然后 `ArrayBlockingQueue` 通过实现 `BlockingQueue` 获取到阻塞队列的常见操作并将这些操作实现,填充到 `AbstractQueue` 模板方法的细节中,由此 `ArrayBlockingQueue` 成为一个完整的阻塞队列。
为了印证这一点,我们到源码中一探究竟。首先我们先来看看 `AbstractQueue`,从类的继承关系我们可以大致得出,它通过 `AbstractCollection` 获得了集合的常见操作方法,然后通过 `Queue` 接口获得了队列的特性。
@@ -244,7 +249,7 @@ public abstract class AbstractQueue
对于集合的操作无非是增删改查,所以我们不妨从添加方法入手,从源码中我们可以看到,它实现了 `AbstractCollection` 的 `add` 方法,其内部逻辑如下:
-1. 调用继承 `Queue` 接口的来的 `offer` 方法,如果 `offer` 成功则返回 `true`。
+1. 调用继承 `Queue` 接口得来的 `offer` 方法,如果 `offer` 成功则返回 `true`。
2. 如果 `offer` 失败,即代表当前元素入队失败直接抛异常。
```java
@@ -258,7 +263,7 @@ public boolean add(E e) {
而 `AbstractQueue` 中并没有对 `Queue` 的 `offer` 的实现,很明显这样做的目的是定义好了 `add` 的核心逻辑,将 `offer` 的细节交由其子类即我们的 `ArrayBlockingQueue` 实现。
-到此,我们对于抽象类 `AbstractQueue` 的分析就结束了,我们继续看看 `ArrayBlockingQueue` 中另一个重要的继承接口 `BlockingQueue`。
+到此,我们对于抽象类 `AbstractQueue` 的分析就结束了,我们继续看看 `ArrayBlockingQueue` 中实现的另一个重要接口 `BlockingQueue`。
点开 `BlockingQueue` 之后,我们可以看到这个接口同样继承了 `Queue` 接口,这就意味着它也具备了队列所拥有的所有行为。同时,它还定义了自己所需要实现的方法。
@@ -302,11 +307,11 @@ public interface BlockingQueue extends Queue {
}
```
-了解了 `BlockingQueue` 的常见操作后,我们就知道了 `ArrayBlockingQueue` 通过继承 `BlockingQueue` 的方法并实现后,填充到 `AbstractQueue` 的方法上,由此我们便知道了上文中 `AbstractQueue` 的 `add` 方法的 `offer` 方法是哪里是实现的了。
+了解了 `BlockingQueue` 的常见操作后,我们就知道了 `ArrayBlockingQueue` 通过实现 `BlockingQueue` 的方法并重写后,填充到 `AbstractQueue` 的方法上,由此我们便知道了上文中 `AbstractQueue` 的 `add` 方法的 `offer` 方法是哪里是实现的了。
```java
public boolean add(E e) {
- //AbstractQueue的offer来自下层的ArrayBlockingQueue从BlockingQueue继承并实现的offer方法
+ //AbstractQueue的offer来自下层的ArrayBlockingQueue从BlockingQueue实现并重写的offer方法
if (offer(e))
return true;
else
diff --git a/docs/java/collection/arraylist-source-code.md b/docs/java/collection/arraylist-source-code.md
index 79bcc1f7d81..35437592b21 100644
--- a/docs/java/collection/arraylist-source-code.md
+++ b/docs/java/collection/arraylist-source-code.md
@@ -1,8 +1,13 @@
---
title: ArrayList 源码分析
+description: ArrayList源码深度解析:详解ArrayList底层数组结构、1.5倍扩容机制、RandomAccess快速随机访问、序列化实现及与Vector性能对比。
category: Java
tag:
- Java集合
+head:
+ - - meta
+ - name: keywords
+ content: ArrayList源码,ArrayList扩容机制,动态数组,RandomAccess,ArrayList序列化,ArrayList与Vector区别
---
@@ -159,19 +164,22 @@ public class ArrayList extends AbstractList
* @param minCapacity 所需的最小容量
*/
public void ensureCapacity(int minCapacity) {
- //如果是true,minExpand的值为0,如果是false,minExpand的值为10
+ // 如果不是默认空数组,则minExpand的值为0;
+ // 如果是默认空数组,则minExpand的值为10
int minExpand = (elementData != DEFAULTCAPACITY_EMPTY_ELEMENTDATA)
- // any size if not default element table
+ // 如果不是默认元素表,则可以使用任意大小
? 0
- // larger than default for default empty table. It's already
- // supposed to be at default size.
+ // 如果是默认空数组,它应该已经是默认大小
: DEFAULT_CAPACITY;
- //如果最小容量大于已有的最大容量
+
+ // 如果最小容量大于已有的最大容量
if (minCapacity > minExpand) {
+ // 根据需要的最小容量,确保容量足够
ensureExplicitCapacity(minCapacity);
}
}
+
// 根据给定的最小容量和当前数组元素来计算所需容量。
private static int calculateCapacity(Object[] elementData, int minCapacity) {
// 如果当前数组元素为空数组(初始情况),返回默认容量和最小容量中的较大值作为所需容量
@@ -305,8 +313,11 @@ public class ArrayList extends AbstractList
/**
* 以正确的顺序(从第一个到最后一个元素)返回一个包含此列表中所有元素的数组。
- * 返回的数组将是“安全的”,因为该列表不保留对它的引用。 (换句话说,这个方法必须分配一个新的数组)。
- * 因此,调用者可以自由地修改返回的数组。 此方法充当基于阵列和基于集合的API之间的桥梁。
+ * 返回的数组将是“安全的”,因为该列表不保留对它的引用。
+ * (换句话说,这个方法必须分配一个新的数组)。
+ * 因此,调用者可以自由地修改返回的数组结构。
+ * 注意:如果元素是引用类型,修改元素的内容会影响到原列表中的对象。
+ * 此方法充当基于数组和基于集合的API之间的桥梁。
*/
public Object[] toArray() {
return Arrays.copyOf(elementData, size);
@@ -426,8 +437,7 @@ public class ArrayList extends AbstractList
}
/*
- * Private remove method that skips bounds checking and does not
- * return the value removed.
+ * 该方法为私有的移除方法,跳过了边界检查,并且不返回被移除的值。
*/
private void fastRemove(int index) {
modCount++;
@@ -435,7 +445,7 @@ public class ArrayList extends AbstractList
if (numMoved > 0)
System.arraycopy(elementData, index + 1, elementData, index,
numMoved);
- elementData[--size] = null; // clear to let GC do its work
+ elementData[--size] = null; // 在移除元素后,将该位置的元素设为 null,以便垃圾回收器(GC)能够回收该元素。
}
/**
diff --git a/docs/java/collection/concurrent-hash-map-source-code.md b/docs/java/collection/concurrent-hash-map-source-code.md
index 32203cfef13..56ca5298373 100644
--- a/docs/java/collection/concurrent-hash-map-source-code.md
+++ b/docs/java/collection/concurrent-hash-map-source-code.md
@@ -1,11 +1,18 @@
---
title: ConcurrentHashMap 源码分析
+description: ConcurrentHashMap源码深入解析:对比JDK1.7分段锁Segment与JDK1.8 CAS+Synchronized实现,理解高并发Map的线程安全机制与性能优化。
category: Java
tag:
- Java集合
+head:
+ - - meta
+ - name: keywords
+ content: ConcurrentHashMap源码,线程安全Map,分段锁Segment,CAS操作,并发容器,JDK7与JDK8区别
---
-> 本文来自公众号:末读代码的投稿,原文地址: 。
+
+
+> 本文来自末读代码投稿: ,JavaGuide 对原文进行了大篇幅改进优化。
上一篇文章介绍了 HashMap 源码,反响不错,也有很多同学发表了自己的观点,这次又来了,这次是 `ConcurrentHashMap` 了,作为线程安全的 HashMap ,它的使用频率也是很高。那么它的存储结构和实现原理是怎么样的呢?
@@ -368,7 +375,19 @@ private void rehash(HashEntry node) {
}
```
-有些同学可能会对最后的两个 for 循环有疑惑,这里第一个 for 是为了寻找这样一个节点,这个节点后面的所有 next 节点的新位置都是相同的。然后把这个作为一个链表赋值到新位置。第二个 for 循环是为了把剩余的元素通过头插法插入到指定位置链表。这样实现的原因可能是基于概率统计,有深入研究的同学可以发表下意见。
+有些同学可能会对最后的两个 for 循环有疑惑,这里第一个 for 是为了寻找这样一个节点,这个节点后面的所有 next 节点的新位置都是相同的。然后把这个作为一个链表赋值到新位置。第二个 for 循环是为了把剩余的元素通过头插法插入到指定位置链表。~~这样实现的原因可能是基于概率统计,有深入研究的同学可以发表下意见。~~
+
+内部第二个 `for` 循环中使用了 `new HashEntry(h, p.key, v, n)` 创建了一个新的 `HashEntry`,而不是复用之前的,是因为如果复用之前的,那么会导致正在遍历(如正在执行 `get` 方法)的线程由于指针的修改无法遍历下去。正如注释中所说的:
+
+> 当它们不再被可能正在并发遍历表的任何读取线程引用时,被替换的节点将被垃圾回收。
+>
+> The nodes they replace will be garbage collectable as soon as they are no longer referenced by any reader thread that may be in the midst of concurrently traversing table
+
+为什么需要再使用一个 `for` 循环找到 `lastRun` ,其实是为了减少对象创建的次数,正如注解中所说的:
+
+> 从统计上看,在默认的阈值下,当表容量加倍时,只有大约六分之一的节点需要被克隆。
+>
+> Statistically, at the default threshold, only about one-sixth of them need cloning when a table doubles.
### 5. get
@@ -401,6 +420,8 @@ public V get(Object key) {
## 2. ConcurrentHashMap 1.8
+总的来说 ,`ConcurrentHashMap` 在 Java8 中相对于 Java7 来说变化还是挺大的,
+
### 1. 存储结构

@@ -578,9 +599,64 @@ public V get(Object key) {
3. 如果头节点 hash 值小于 0 ,说明正在扩容或者是红黑树,查找之。
4. 如果是链表,遍历查找之。
-总结:
+### 5. size 计数
+
+`ConcurrentHashMap` 的 `size()` 方法用来获取当前 Map 中元素的总数,但在高并发场景下,如何准确且高效地统计元素数量是一个技术难点。Java8 采用了一套精巧的分段计数机制来解决这个问题。
+
+#### 5.1 为什么需要分段计数
+
+在并发环境下,如果多个线程同时执行 `put` 操作,它们都需要更新元素总数。如果使用一个共享的计数器变量,就会导致激烈的竞争——所有线程都在争抢同一个变量的修改权,这会严重影响性能。
+
+为了解决这个问题,`ConcurrentHashMap` 采用了**分散热点**的设计思想:不使用单一计数器,而是将计数分散到多个变量中。就像银行不会只开一个窗口办业务,而是开多个窗口分流客户一样,这样可以大大减少冲突。
+
+#### 5.2 baseCount 和 counterCells 的设计
+
+`ConcurrentHashMap` 内部维护了两个关键的计数相关字段:
+
+- **baseCount**:基础计数器,在没有竞争的情况下,直接通过 CAS 更新这个变量。可以把它理解为"主计数器"。
+- **counterCells**:计数器数组。当多个线程竞争 `baseCount` 失败时,会尝试将计数增量分散到 `counterCells` 数组的不同位置。
+ - 每个线程根据自己的 **Probe 值**(可理解为线程 ID 生成的一种哈希码)映射到数组的某个槽位,优先在这个“偏向的格子”里进行累加。
+ - **注意**:这个格子并不是严格意义上的“线程私有”,当哈希冲突时,多个线程仍然可能映射到同一个槽位并发更新。
+
+**举个例子**:假设有 10 个线程同时往 Map 中添加元素。第一个线程成功通过 CAS 更新了 `baseCount`,但后面 9 个线程在更新 `baseCount` 时发现有竞争,就会转而去 `counterCells` 数组中找一个位置进行累加。这 9 个线程可能分散到数组的不同位置(比如线程 2 在 `counterCells[1]`,线程 3 在 `counterCells[2]`),从而将竞争从一个点分散到了多个点。。
+
+#### 5.3 put 元素时如何更新计数
+
+在 `putVal` 方法的最后,我们可以看到调用了 `addCount(1L, binCount)` 方法,这个方法就是用来更新元素计数的。
+
+`addCount` 的执行逻辑大致可以概括为:
+
+1. **优先尝试更新 baseCount**
+
+ - 如果当前还没有启用 `counterCells`(`counterCells == null`),线程会先尝试通过 CAS 直接更新 `baseCount`。
+ - 如果 CAS 成功,说明竞争不激烈,直接返回即可。
+
+2. **竞争出现时,转向 counterCells**
+
+ - 如果 CAS 更新 `baseCount` 失败(说明有其他线程在竞争),或者 `counterCells` 已经存在(说明系统之前已经遇到过竞争),线程就会尝试在 `counterCells` 中更新:
+ - 根据自己的 probe 值映射到某个槽位;
+ - 对该槽位对应的 `CounterCell` 做一次 CAS 累加。
+ - 如果这个槽位为空或 CAS 仍然冲突,就会进入一个更“重”的路径 `fullAddCount`,在里面负责初始化槽位、重新选择槽位等。
+
+3. **动态初始化与扩容 counterCells**
+ - 当检测到竞争比较激烈(例如:某个 cell 的 CAS 也频繁失败)时,`fullAddCount` 会在一个轻量级的自旋锁 `cellsBusy` 保护下:
+ - 如果 `counterCells` 还没初始化,就初始化一个较小的数组(比如长度 2);
+ - 如果已经存在并且长度还没达到上限(通常不超过 CPU 核数),就按 2 倍进行扩容,增加更多的计数槽位,把线程进一步打散。
+
+这种设计保证了:在低并发时只使用简单的 `baseCount`,路径非常短;在高并发时则自动切换到分段计数,通过 `counterCells` 和扩容机制摊薄竞争,兼顾了性能和准确性。
+
+#### 5.4 sumCount 如何计算元素总数
+
+当我们调用 `size()` 方法时,最终会调用 `sumCount()` 方法来计算元素总数。`sumCount()` 的逻辑非常简单直接:
+
+1. 读取 `baseCount` 的值作为基础值。
+2. 遍历 `counterCells` 数组,将所有非空位置的计数值累加到基础值上。
+3. 返回累加结果。
+
+**注意**:
-总的来说 `ConcurrentHashMap` 在 Java8 中相对于 Java7 来说变化还是挺大的,
+- **弱一致性**:`sumCount()` 全程**不加锁**。在计算期间如果有其他线程插入数据,返回的结果只是一个**近似值**。但在高并发场景下,追求“刹那间的精确总数”代价过大且无意义,近似值通常已足够。
+- **整型溢出**:`size()` 方法返回 `int` 类型。如果元素数量超过 `Integer.MAX_VALUE`,它只会返回 `Integer.MAX_VALUE`。如果需要获取精确的大容量计数,建议使用 Java 8 新增的 **`mappingCount()`** 方法,该方法返回 `long` 类型。
## 3. 总结
diff --git a/docs/java/collection/copyonwritearraylist-source-code.md b/docs/java/collection/copyonwritearraylist-source-code.md
index 9aceb83bc4e..8c5ec08be89 100644
--- a/docs/java/collection/copyonwritearraylist-source-code.md
+++ b/docs/java/collection/copyonwritearraylist-source-code.md
@@ -1,8 +1,13 @@
---
title: CopyOnWriteArrayList 源码分析
+description: CopyOnWriteArrayList源码深度解析:详解写时复制COW机制、适用读多写少场景、线程安全List实现、快照一致性保证及内存开销权衡。
category: Java
tag:
- Java集合
+head:
+ - - meta
+ - name: keywords
+ content: CopyOnWriteArrayList源码,写时复制COW,线程安全List,读多写少,并发容器,快照一致性
---
## CopyOnWriteArrayList 简介
diff --git a/docs/java/collection/delayqueue-source-code.md b/docs/java/collection/delayqueue-source-code.md
index 5fb6f4affad..9c5f0712c2e 100644
--- a/docs/java/collection/delayqueue-source-code.md
+++ b/docs/java/collection/delayqueue-source-code.md
@@ -1,8 +1,13 @@
---
title: DelayQueue 源码分析
+description: DelayQueue源码深度解析:详解延迟队列实现原理、Delayed接口使用、延时任务调度、订单超时取消等应用场景、基于PriorityQueue的线程安全设计。
category: Java
tag:
- Java集合
+head:
+ - - meta
+ - name: keywords
+ content: DelayQueue源码,延迟队列,Delayed接口,延时任务,定时任务,订单超时,PriorityQueue实现
---
## DelayQueue 简介
diff --git a/docs/java/collection/hashmap-source-code.md b/docs/java/collection/hashmap-source-code.md
index 0e9342f0edf..d9ea9caa6e7 100644
--- a/docs/java/collection/hashmap-source-code.md
+++ b/docs/java/collection/hashmap-source-code.md
@@ -1,8 +1,13 @@
---
title: HashMap 源码分析
+description: HashMap源码深度剖析:详解JDK1.7/1.8结构差异、hash扰动函数、0.75负载因子、扩容rehash机制、链表转红黑树阈值等HashMap核心原理。
category: Java
tag:
- Java集合
+head:
+ - - meta
+ - name: keywords
+ content: HashMap源码,哈希表,红黑树,链表,扰动函数,负载因子,HashMap扩容,哈希冲突,JDK1.8优化
---
@@ -217,7 +222,9 @@ HashMap 中有四个构造方法,它们分别如下:
}
```
-> 值得注意的是上述四个构造方法中,都初始化了负载因子 loadFactor,由于 HashMap 中没有 capacity 这样的字段,即使指定了初始化容量 initialCapacity ,也只是通过 tableSizeFor 将其扩容到与 initialCapacity 最接近的 2 的幂次方大小,然后暂时赋值给 threshold ,后续通过 resize 方法将 threshold 赋值给 newCap 进行 table 的初始化。
+> 需要特别注意的是:传入的 `initialCapacity` 并不是最终的数组容量。`HashMap` 会调用 `tableSizeFor()` 将其**向上取整为大于或等于该值的最小 2 的幂次方**,并暂时保存到 `threshold` 字段。真正的 `table` 数组会在第一次扩容(`resize()`)时才初始化为这个大小。
+>
+> 例如:`initialCapacity = 9` → `threshold = 16` → `table` 长度最终为 16。
**putMapEntries 方法:**
diff --git a/docs/java/collection/java-collection-precautions-for-use.md b/docs/java/collection/java-collection-precautions-for-use.md
index e636ff5a77a..2214ee59beb 100644
--- a/docs/java/collection/java-collection-precautions-for-use.md
+++ b/docs/java/collection/java-collection-precautions-for-use.md
@@ -1,8 +1,13 @@
---
title: Java集合使用注意事项总结
+description: Java集合使用注意事项总结:基于阿里巴巴开发手册梳理集合判空、Arrays.asList陷阱、subList问题、并发容器选择等最佳实践,避免常见错误。
category: Java
tag:
- Java集合
+head:
+ - - meta
+ - name: keywords
+ content: Java集合最佳实践,集合判空,Arrays.asList,subList,并发容器,集合使用注意事项,性能优化
---
这篇文章我根据《阿里巴巴 Java 开发手册》总结了关于集合使用常见的注意事项以及其具体原理。
@@ -15,35 +20,58 @@ tag:
> **判断所有集合内部的元素是否为空,使用 `isEmpty()` 方法,而不是 `size()==0` 的方式。**
-这是因为 `isEmpty()` 方法的可读性更好,并且时间复杂度为 O(1)。
+这是因为 `isEmpty()` 方法的可读性更好,并且时间复杂度为 `O(1)`。
-绝大部分我们使用的集合的 `size()` 方法的时间复杂度也是 O(1),不过,也有很多复杂度不是 O(1) 的,比如 `java.util.concurrent` 包下的某些集合(`ConcurrentLinkedQueue`、`ConcurrentHashMap`...)。
+绝大部分我们使用的集合的 `size()` 方法的时间复杂度也是 `O(1)`,不过,也有很多复杂度不是 `O(1)` 的,比如 `java.util.concurrent` 包下的 `ConcurrentLinkedQueue`。`ConcurrentLinkedQueue` 的 `isEmpty()` 方法通过 `first()` 方法进行判断,其中 `first()` 方法返回的是队列中第一个值不为 `null` 的节点(节点值为`null`的原因是在迭代器中使用的逻辑删除)
-下面是 `ConcurrentHashMap` 的 `size()` 方法和 `isEmpty()` 方法的源码。
+```java
+public boolean isEmpty() { return first() == null; }
+
+Node first() {
+ restartFromHead:
+ for (;;) {
+ for (Node h = head, p = h, q;;) {
+ boolean hasItem = (p.item != null);
+ if (hasItem || (q = p.next) == null) { // 当前节点值不为空 或 到达队尾
+ updateHead(h, p); // 将head设置为p
+ return hasItem ? p : null;
+ }
+ else if (p == q) continue restartFromHead;
+ else p = q; // p = p.next
+ }
+ }
+}
+```
+
+由于在插入与删除元素时,都会执行`updateHead(h, p)`方法,所以该方法的执行的时间复杂度可以近似为`O(1)`。而 `size()` 方法需要遍历整个链表,时间复杂度为`O(n)`
```java
public int size() {
- long n = sumCount();
- return ((n < 0L) ? 0 :
- (n > (long)Integer.MAX_VALUE) ? Integer.MAX_VALUE :
- (int)n);
+ int count = 0;
+ for (Node p = first(); p != null; p = succ(p))
+ if (p.item != null)
+ if (++count == Integer.MAX_VALUE)
+ break;
+ return count;
}
+```
+
+此外,在`ConcurrentHashMap` 1.7 中 `size()` 方法和 `isEmpty()` 方法的时间复杂度也不太一样。`ConcurrentHashMap` 1.7 将元素数量存储在每个`Segment` 中,`size()` 方法需要统计每个 `Segment` 的数量,而 `isEmpty()` 只需要找到第一个不为空的 `Segment` 即可。但是在`ConcurrentHashMap` 1.8 中的 `size()` 方法和 `isEmpty()` 都需要调用 `sumCount()` 方法,其时间复杂度与 `Node` 数组的大小有关。下面是 `sumCount()` 方法的源码:
+
+```java
final long sumCount() {
CounterCell[] as = counterCells; CounterCell a;
long sum = baseCount;
- if (as != null) {
- for (int i = 0; i < as.length; ++i) {
+ if (as != null)
+ for (int i = 0; i < as.length; ++i)
if ((a = as[i]) != null)
sum += a.value;
- }
- }
return sum;
}
-public boolean isEmpty() {
- return sumCount() <= 0L; // ignore transient negative values
-}
```
+这是因为在并发的环境下,`ConcurrentHashMap` 将每个 `Node` 中节点的数量存储在 `CounterCell[]` 数组中。在 `ConcurrentHashMap` 1.7 中,将元素数量存储在每个`Segment` 中,`size()` 方法需要统计每个 `Segment` 的数量,而 `isEmpty()` 只需要找到第一个不为空的 `Segment` 即可。
+
## 集合转 Map
《阿里巴巴 Java 开发手册》的描述如下:
@@ -112,6 +140,8 @@ public static T requireNonNull(T obj) {
}
```
+> `Collectors`也提供了无需 mergeFunction 的`toMap()`方法,但此时若出现 key 冲突,则会抛出`duplicateKeyException`异常,因此强烈建议使用`toMap()`方法必填 mergeFunction。
+
## 集合遍历
《阿里巴巴 Java 开发手册》的描述如下:
diff --git a/docs/java/collection/java-collection-questions-01.md b/docs/java/collection/java-collection-questions-01.md
index 5c22e96a2f0..6f521064b77 100644
--- a/docs/java/collection/java-collection-questions-01.md
+++ b/docs/java/collection/java-collection-questions-01.md
@@ -1,19 +1,19 @@
---
title: Java集合常见面试题总结(上)
+description: Java集合框架面试题总结:深入解析Collection/List/Set/Queue接口,对比ArrayList/LinkedList/HashMap等常用集合类,掌握集合底层数据结构与使用场景。
category: Java
tag:
- Java集合
head:
- - meta
- name: keywords
- content: Collection,List,Set,Queue,Deque,PriorityQueue
- - - meta
- - name: description
- content: Java集合常见知识点和面试题总结,希望对你有帮助!
+ content: Java集合,Collection,List,Set,Queue,ArrayList,LinkedList,HashMap,集合框架,Java面试题
---
+
+
## 集合概述
### Java 集合概览
@@ -26,7 +26,7 @@ Java 集合框架如下图所示:
注:图中只列举了主要的继承派生关系,并没有列举所有关系。比方省略了`AbstractList`, `NavigableSet`等抽象类以及其他的一些辅助类,如想深入了解,可自行查看源码。
-### 说说 List, Set, Queue, Map 四者的区别?
+### ⭐️说说 List, Set, Queue, Map 四者的区别?
- `List`(对付顺序的好帮手): 存储的元素是有序的、可重复的。
- `Set`(注重独一无二的性质): 存储的元素不可重复的。
@@ -77,7 +77,7 @@ Java 集合框架如下图所示:
## List
-### ArrayList 和 Array(数组)的区别?
+### ⭐️ArrayList 和 Array(数组)的区别?
`ArrayList` 内部基于动态数组实现,比 `Array`(静态数组) 使用起来更加灵活:
@@ -152,7 +152,7 @@ System.out.println(listOfStrings);
[null, java]
```
-### ArrayList 插入和删除元素的时间复杂度?
+### ⭐️ArrayList 插入和删除元素的时间复杂度?
对于插入:
@@ -186,13 +186,13 @@ System.out.println(listOfStrings);
0 1 2 3 4 5 6 7 8 9
```
-### LinkedList 插入和删除元素的时间复杂度?
+### ⭐️LinkedList 插入和删除元素的时间复杂度?
- 头部插入/删除:只需要修改头结点的指针即可完成插入/删除操作,因此时间复杂度为 O(1)。
- 尾部插入/删除:只需要修改尾结点的指针即可完成插入/删除操作,因此时间复杂度为 O(1)。
-- 指定位置插入/删除:需要先移动到指定位置,再修改指定节点的指针完成插入/删除,因此需要遍历平均 n/2 个元素,时间复杂度为 O(n)。
+- 指定位置插入/删除:需要先移动到指定位置,再修改指定节点的指针完成插入/删除,不过由于有头尾指针,可以从较近的指针出发,因此需要遍历平均 n/4 个元素,时间复杂度为 O(n)。
-这里简单列举一个例子:假如我们要删除节点 9 的话,需要先遍历链表找到该节点。然后,再执行相应节点指针指向的更改,具体的源码可以参考:[LinkedList 源码分析](./linkedlist-source-code.md) 。
+这里简单列举一个例子:假如我们要删除节点 9 的话,需要先遍历链表找到该节点。然后,再执行相应节点指针指向的更改,具体的源码可以参考:[LinkedList 源码分析](https://javaguide.cn/java/collection/linkedlist-source-code.html) 。

@@ -200,7 +200,7 @@ System.out.println(listOfStrings);
`RandomAccess` 是一个标记接口,用来表明实现该接口的类支持随机访问(即可以通过索引快速访问元素)。由于 `LinkedList` 底层数据结构是链表,内存地址不连续,只能通过指针来定位,不支持随机快速访问,所以不能实现 `RandomAccess` 接口。
-### ArrayList 与 LinkedList 区别?
+### ⭐️ArrayList 与 LinkedList 区别?
- **是否保证线程安全:** `ArrayList` 和 `LinkedList` 都是不同步的,也就是不保证线程安全;
- **底层数据结构:** `ArrayList` 底层使用的是 **`Object` 数组**;`LinkedList` 底层使用的是 **双向链表** 数据结构(JDK1.6 之前为循环链表,JDK1.7 取消了循环。注意双向链表和双向循环链表的区别,下面有介绍到!)
@@ -249,9 +249,131 @@ public interface RandomAccess {
`ArrayList` 实现了 `RandomAccess` 接口, 而 `LinkedList` 没有实现。为什么呢?我觉得还是和底层数据结构有关!`ArrayList` 底层是数组,而 `LinkedList` 底层是链表。数组天然支持随机访问,时间复杂度为 O(1),所以称为快速随机访问。链表需要遍历到特定位置才能访问特定位置的元素,时间复杂度为 O(n),所以不支持快速随机访问。`ArrayList` 实现了 `RandomAccess` 接口,就表明了他具有快速随机访问功能。 `RandomAccess` 接口只是标识,并不是说 `ArrayList` 实现 `RandomAccess` 接口才具有快速随机访问功能的!
-### 说一说 ArrayList 的扩容机制吧
+### ⭐️说一说 ArrayList 的扩容机制吧
+
+详见笔主的这篇文章: [ArrayList 扩容机制分析](https://javaguide.cn/java/collection/arraylist-source-code.html#arraylist-扩容机制分析)。
+
+### ⭐️集合中的 fail-fast 和 fail-safe 是什么?
+
+`fail-fast`(快速失败)和 `fail-safe`(安全失败)是Java集合框架在处理并发修改问题时,两种截然不同的设计哲学和容错策略。
+
+关于`fail-fast`引用`medium`中一篇文章关于`fail-fast`和`fail-safe`的说法:
+
+> Fail-fast systems are designed to immediately stop functioning upon encountering an unexpected condition. This immediate failure helps to catch errors early, making debugging more straightforward.
+
+快速失败的思想即针对可能发生的异常进行提前表明故障并停止运行,通过尽早的发现和停止错误,降低故障系统级联的风险。
+
+在`java.util`包下的大部分集合(如 `ArrayList`, `HashMap`)是不支持线程安全的,为了能够提前发现并发操作导致线程安全风险,提出通过维护一个`modCount`记录修改的次数,迭代期间通过比对预期修改次数`expectedModCount`和`modCount`是否一致来判断是否存在并发操作,从而实现快速失败,由此保证在避免在异常时执行非必要的复杂代码。
+
+**ArrayList (fail-fast) 示例:**
+
+```java
+ // 使用线程不安全的 ArrayList,它是一种 fail-fast 集合
+ List list = new ArrayList<>();
+ CountDownLatch latch = new CountDownLatch(2);
+
+ for (int i = 0; i < 5; i++) {
+ list.add(i);
+ }
+ System.out.println("Initial list: " + list);
+
+ Thread t1 = new Thread(() -> {
+ try {
+ for (Integer i : list) {
+ System.out.println("Iterator Thread (t1) sees: " + i);
+ Thread.sleep(100);
+ }
+ } catch (ConcurrentModificationException e) {
+ System.err.println("!!! Iterator Thread (t1) caught ConcurrentModificationException as expected.");
+ } catch (InterruptedException e) {
+ e.printStackTrace();
+ } finally {
+ latch.countDown();
+ }
+ });
+
+ Thread t2 = new Thread(() -> {
+ try {
+ Thread.sleep(50);
+ System.out.println("-> Modifier Thread (t2) is removing element 1...");
+ list.remove(Integer.valueOf(1));
+ System.out.println("-> Modifier Thread (t2) finished removal.");
+ } catch (InterruptedException e) {
+ e.printStackTrace();
+ } finally {
+ latch.countDown();
+ }
+ });
+
+ t1.start();
+ t2.start();
+ latch.await();
+
+ System.out.println("Final list state: " + list);
+```
+
+输出:
+
+```
+Initial list: [0, 1, 2, 3, 4]
+Iterator Thread (t1) sees: 0
+-> Modifier Thread (t2) is removing element 1...
+-> Modifier Thread (t2) finished removal.
+!!! Iterator Thread (t1) caught ConcurrentModificationException as expected.
+Final list state: [0, 2, 3, 4]
+```
+
+程序在线程 t2 修改列表后,线程 t1 的下一次迭代操作立刻就抛出了 `ConcurrentModificationException`。这是因为 ArrayList 的迭代器在每次 `next()` 调用时,都会检查 `modCount` 是否被改变。一旦发现集合在迭代器不知情的情况下被修改,它会立即“快速失败”,以防止在不一致的数据上继续操作导致不可预期的后果。
+
+对此我们也给出`for`循环底层迭代器获取下一个元素时的`next`方法,可以看到其内部的`checkForComodification`具有针对修改次数比对的逻辑:
+
+```java
+ public E next() {
+ //检查是否存在并发修改
+ checkForComodification();
+ //......
+ //返回下一个元素
+ return (E) elementData[lastRet = i];
+ }
+
+final void checkForComodification() {
+ //当前循环遍历次数和预期修改次数不一致时,就会抛出ConcurrentModificationException
+ if (modCount != expectedModCount)
+ throw new ConcurrentModificationException();
+ }
+
+```
+
+而`fail-safe`也就是安全失败的含义,它旨在即使面对意外情况也能恢复并继续运行,这使得它特别适用于不确定或者不稳定的环境:
+
+> Fail-safe systems take a different approach, aiming to recover and continue even in the face of unexpected conditions. This makes them particularly suited for uncertain or volatile environments.
+
+该思想常运用于并发容器,最经典的实现就是`CopyOnWriteArrayList`的实现,通过写时复制(Copy-On-Write)的思想保证在进行修改操作时复制出一份快照,基于这份快照完成添加或者删除操作后,将`CopyOnWriteArrayList`底层的数组引用指向这个新的数组空间,由此避免迭代时被并发修改所干扰所导致并发操作安全问题,当然这种做法也存在缺点,即进行遍历操作时无法获得实时结果:
+
+
+
+对应我们也给出`CopyOnWriteArrayList`实现`fail-safe`的核心代码,可以看到它的实现就是通过`getArray`获取数组引用然后通过`Arrays.copyOf`得到一个数组的快照,基于这个快照完成添加操作后,修改底层`array`变量指向的引用地址由此完成写时复制:
-详见笔主的这篇文章: [ArrayList 扩容机制分析](https://javaguide.cn/java/collection/arraylist-source-code.html#_3-1-%E5%85%88%E4%BB%8E-arraylist-%E7%9A%84%E6%9E%84%E9%80%A0%E5%87%BD%E6%95%B0%E8%AF%B4%E8%B5%B7)。
+```java
+public boolean add(E e) {
+ final ReentrantLock lock = this.lock;
+ lock.lock();
+ try {
+ //获取原有数组
+ Object[] elements = getArray();
+ int len = elements.length;
+ //基于原有数组复制出一份内存快照
+ Object[] newElements = Arrays.copyOf(elements, len + 1);
+ //进行添加操作
+ newElements[len] = e;
+ //array指向新的数组
+ setArray(newElements);
+ return true;
+ } finally {
+ lock.unlock();
+ }
+ }
+```
## Set
@@ -481,7 +603,7 @@ Java 中常用的阻塞队列实现类有以下几种:
日常开发中,这些队列使用的其实都不多,了解即可。
-### ArrayBlockingQueue 和 LinkedBlockingQueue 有什么区别?
+### ⭐️ArrayBlockingQueue 和 LinkedBlockingQueue 有什么区别?
`ArrayBlockingQueue` 和 `LinkedBlockingQueue` 是 Java 并发包中常用的两种阻塞队列实现,它们都是线程安全的。不过,不过它们之间也存在下面这些区别:
diff --git a/docs/java/collection/java-collection-questions-02.md b/docs/java/collection/java-collection-questions-02.md
index 569842e5b87..be5c61d7728 100644
--- a/docs/java/collection/java-collection-questions-02.md
+++ b/docs/java/collection/java-collection-questions-02.md
@@ -1,22 +1,20 @@
---
title: Java集合常见面试题总结(下)
+description: Java集合高频面试题:深入分析HashMap底层原理、红黑树转换、哈希冲突解决、ConcurrentHashMap线程安全机制、与Hashtable区别等核心知识点。
category: Java
tag:
- Java集合
head:
- - meta
- name: keywords
- content: HashMap,ConcurrentHashMap,Hashtable,List,Set
- - - meta
- - name: description
- content: Java集合常见知识点和面试题总结,希望对你有帮助!
+ content: HashMap,ConcurrentHashMap,Hashtable,红黑树,哈希冲突,线程安全,集合面试题
---
## Map(重要)
-### HashMap 和 Hashtable 的区别
+### ⭐️HashMap 和 Hashtable 的区别
- **线程是否安全:** `HashMap` 是非线程安全的,`Hashtable` 是线程安全的,因为 `Hashtable` 内部的方法基本都经过`synchronized` 修饰。(如果你要保证线程安全的话就使用 `ConcurrentHashMap` 吧!);
- **效率:** 因为线程安全的问题,`HashMap` 要比 `Hashtable` 效率高一点。另外,`Hashtable` 基本被淘汰,不要在代码中使用它;
@@ -48,18 +46,18 @@ head:
下面这个方法保证了 `HashMap` 总是使用 2 的幂作为哈希表的大小。
```java
- /**
- * Returns a power of two size for the given target capacity.
- */
- static final int tableSizeFor(int cap) {
- int n = cap - 1;
- n |= n >>> 1;
- n |= n >>> 2;
- n |= n >>> 4;
- n |= n >>> 8;
- n |= n >>> 16;
- return (n < 0) ? 1 : (n >= MAXIMUM_CAPACITY) ? MAXIMUM_CAPACITY : n + 1;
- }
+/**
+ * Returns a power of two size for the given target capacity.
+ */
+static final int tableSizeFor(int cap) {
+ int n = cap - 1;
+ n |= n >>> 1;
+ n |= n >>> 2;
+ n |= n >>> 4;
+ n |= n >>> 8;
+ n |= n >>> 16;
+ return (n < 0) ? 1 : (n >= MAXIMUM_CAPACITY) ? MAXIMUM_CAPACITY : n + 1;
+}
```
### HashMap 和 HashSet 区别
@@ -73,7 +71,7 @@ head:
| 调用 `put()`向 map 中添加元素 | 调用 `add()`方法向 `Set` 中添加元素 |
| `HashMap` 使用键(Key)计算 `hashcode` | `HashSet` 使用成员对象来计算 `hashcode` 值,对于两个对象来说 `hashcode` 可能相同,所以`equals()`方法用来判断对象的相等性 |
-### HashMap 和 TreeMap 区别
+### ⭐️HashMap 和 TreeMap 区别
`TreeMap` 和`HashMap` 都继承自`AbstractMap` ,但是需要注意的是`TreeMap`它还实现了`NavigableMap`接口和`SortedMap` 接口。
@@ -179,13 +177,13 @@ final V putVal(int hash, K key, V value, boolean onlyIfAbsent,
也就是说,在 JDK1.8 中,实际上无论`HashSet`中是否已经存在了某元素,`HashSet`都会直接插入,只是会在`add()`方法的返回值处告诉我们插入前是否存在相同元素。
-### HashMap 的底层实现
+### ⭐️HashMap 的底层实现
#### JDK1.8 之前
JDK1.8 之前 `HashMap` 底层是 **数组和链表** 结合在一起使用也就是 **链表散列**。HashMap 通过 key 的 `hashcode` 经过扰动函数处理过后得到 hash 值,然后通过 `(n - 1) & hash` 判断当前元素存放的位置(这里的 n 指的是数组的长度),如果当前位置存在元素的话,就判断该元素与要存入的元素的 hash 值以及 key 是否相同,如果相同的话,直接覆盖,不相同就通过拉链法解决冲突。
-所谓扰动函数指的就是 HashMap 的 `hash` 方法。使用 `hash` 方法也就是扰动函数是为了防止一些实现比较差的 `hashCode()` 方法 换句话说使用扰动函数之后可以减少碰撞。
+`HashMap` 中的扰动函数(`hash` 方法)是用来优化哈希值的分布。通过对原始的 `hashCode()` 进行额外处理,扰动函数可以减小由于糟糕的 `hashCode()` 实现导致的碰撞,从而提高数据的分布均匀性。
**JDK 1.8 HashMap 的 hash 方法源码:**
@@ -222,10 +220,23 @@ static int hash(int h) {
#### JDK1.8 之后
-相比于之前的版本, JDK1.8 之后在解决哈希冲突时有了较大的变化,当链表长度大于阈值(默认为 8)(将链表转换成红黑树前会判断,如果当前数组的长度小于 64,那么会选择先进行数组扩容,而不是转换为红黑树)时,将链表转化为红黑树,以减少搜索时间。
+相比于之前的版本, JDK1.8 之后在解决哈希冲突时有了较大的变化,当链表长度大于阈值(默认为 8)(将链表转换成红黑树前会判断,如果当前数组的长度小于 64,那么会选择先进行数组扩容,而不是转换为红黑树)时,将链表转化为红黑树。
+
+这样做的目的是减少搜索时间:链表的查询效率为 O(n)(n 是链表的长度),红黑树是一种自平衡二叉搜索树,其查询效率为 O(log n)。当链表较短时,O(n) 和 O(log n) 的性能差异不明显。但当链表变长时,查询性能会显著下降。

+**为什么优先扩容而非直接转为红黑树?**
+
+数组扩容能减少哈希冲突的发生概率(即将元素重新分散到新的、更大的数组中),这在多数情况下比直接转换为红黑树更高效。
+
+红黑树需要保持自平衡,维护成本较高。并且,过早引入红黑树反而会增加复杂度。
+
+**为什么选择阈值 8 和 64?**
+
+1. 泊松分布表明,链表长度达到 8 的概率极低(小于千万分之一)。在绝大多数情况下,链表长度都不会超过 8。阈值设置为 8,可以保证性能和空间效率的平衡。
+2. 数组长度阈值 64 同样是经过实践验证的经验值。在小数组中扩容成本低,优先扩容可以避免过早引入红黑树。数组大小达到 64 时,冲突概率较高,此时红黑树的性能优势开始显现。
+
> TreeMap、TreeSet 以及 JDK1.8 之后的 HashMap 底层都用到了红黑树。红黑树就是为了解决二叉查找树的缺陷,因为二叉查找树在某些情况下会退化成一个线性结构。
我们来结合源码分析一下 `HashMap` 链表到红黑树的转换。
@@ -284,15 +295,59 @@ final void treeifyBin(Node[] tab, int hash) {
将链表转换成红黑树前会判断,如果当前数组的长度小于 64,那么会选择先进行数组扩容,而不是转换为红黑树。
-### HashMap 的长度为什么是 2 的幂次方
+### ⭐️HashMap 的长度为什么是 2 的幂次方
-为了能让 HashMap 存取高效,尽量较少碰撞,也就是要尽量把数据分配均匀。我们上面也讲到了过了,Hash 值的范围值-2147483648 到 2147483647,前后加起来大概 40 亿的映射空间,只要哈希函数映射得比较均匀松散,一般应用是很难出现碰撞的。但问题是一个 40 亿长度的数组,内存是放不下的。所以这个散列值是不能直接拿来用的。用之前还要先做对数组的长度取模运算,得到的余数才能用来要存放的位置也就是对应的数组下标。
+为了让 `HashMap` 存取高效并减少碰撞,我们需要确保数据尽量均匀分布。哈希值在 Java 中通常使用 `int` 表示,其范围是 `-2147483648 ~ 2147483647`前后加起来大概 40 亿的映射空间,只要哈希函数映射得比较均匀松散,一般应用是很难出现碰撞的。但是,问题是一个 40 亿长度的数组,内存是放不下的。所以,这个散列值是不能直接拿来用的。用之前还要先做对数组的长度取模运算,得到的余数才能用来要存放的位置也就是对应的数组下标。
**这个算法应该如何设计呢?**
-我们首先可能会想到采用 % 取余的操作来实现。但是,重点来了:“**取余(%)操作中如果除数是 2 的幂次则等价于与其除数减一的与(&)操作**(也就是说 hash%length==hash&(length-1)的前提是 length 是 2 的 n 次方;)。” 并且 **采用二进制位操作 & 相对于 % 能够提高运算效率**,这就解释了 HashMap 的长度为什么是 2 的幂次方。
+我们首先可能会想到采用 % 取余的操作来实现。但是,重点来了:“**取余(%)操作中如果除数是 2 的幂次则等价于与其除数减一的与(&)操作**(也就是说 `hash%length==hash&(length-1)` 的前提是 length 是 2 的 n 次方)。” 并且,**采用二进制位操作 & 相对于 % 能够提高运算效率**。
+
+除了上面所说的位运算比取余效率高之外,我觉得更重要的一个原因是:**长度是 2 的幂次方,可以让 `HashMap` 在扩容的时候更均匀**。例如:
+
+- length = 8 时,length - 1 = 7 的二进制位`0111`
+- length = 16 时,length - 1 = 15 的二进制位`1111`
-### HashMap 多线程操作导致死循环问题
+这时候原本存在 `HashMap` 中的元素计算新的数组位置时 `hash&(length-1)`,取决 hash 的第四个二进制位(从右数),会出现两种情况:
+
+1. 第四个二进制位为 0,数组位置不变,也就是说当前元素在新数组和旧数组的位置相同。
+2. 第四个二进制位为 1,数组位置在新数组扩容之后的那一部分。
+
+这里列举一个例子:
+
+```plain
+假设有一个元素的哈希值为 10101100
+
+旧数组元素位置计算:
+hash = 10101100
+length - 1 = 00000111
+& -----------------
+index = 00000100 (4)
+
+新数组元素位置计算:
+hash = 10101100
+length - 1 = 00001111
+& -----------------
+index = 00001100 (12)
+
+看第四位(从右数):
+1.高位为 0:位置不变。
+2.高位为 1:移动到新位置(原索引位置+原容量)。
+```
+
+⚠️注意:这里列举的场景看的是第四个二进制位,更准确点来说看的是高位(从右数),例如 `length = 32` 时,`length - 1 = 31`,二进制为 `11111`,这里看的就是第五个二进制位。
+
+也就是说扩容之后,在旧数组元素 hash 值比较均匀(至于 hash 值均不均匀,取决于前面讲的对象的 `hashcode()` 方法和扰动函数)的情况下,新数组元素也会被分配的比较均匀,最好的情况是会有一半在新数组的前半部分,一半在新数组后半部分。
+
+这样也使得扩容机制变得简单和高效,扩容后只需检查哈希值高位的变化来决定元素的新位置,要么位置不变(高位为 0),要么就是移动到新位置(高位为 1,原索引位置+原容量)。
+
+最后,简单总结一下 `HashMap` 的长度是 2 的幂次方的原因:
+
+1. 位运算效率更高:位运算(&)比取余运算(%)更高效。当长度为 2 的幂次方时,`hash % length` 等价于 `hash & (length - 1)`。
+2. 可以更好地保证哈希值的均匀分布:扩容之后,在旧数组元素 hash 值比较均匀的情况下,新数组元素也会被分配的比较均匀,最好的情况是会有一半在新数组的前半部分,一半在新数组后半部分。
+3. 扩容机制变得简单和高效:扩容后只需检查哈希值高位的变化来决定元素的新位置,要么位置不变(高位为 0),要么就是移动到新位置(高位为 1,原索引位置+原容量)。
+
+### ⭐️HashMap 多线程操作导致死循环问题
JDK1.7 及之前版本的 `HashMap` 在多线程环境下扩容操作可能存在死循环问题,这是由于当一个桶位中有多个元素需要进行扩容时,多个线程同时对链表进行操作,头插法可能会导致链表中的节点指向错误的位置,从而形成一个环形链表,进而使得查询元素的操作陷入死循环无法结束。
@@ -300,9 +355,12 @@ JDK1.7 及之前版本的 `HashMap` 在多线程环境下扩容操作可能存
一般面试中这样介绍就差不多,不需要记各种细节,个人觉得也没必要记。如果想要详细了解 `HashMap` 扩容导致死循环问题,可以看看耗子叔的这篇文章:[Java HashMap 的死循环](https://coolshell.cn/articles/9606.html)。
-### HashMap 为什么线程不安全?
+### ⭐️HashMap 为什么线程不安全?
+
+`HashMap` 不是线程安全的。在多线程环境下对 `HashMap` 进行并发写操作,可能会导致两种主要问题:
-JDK1.7 及之前版本,在多线程环境下,`HashMap` 扩容时会造成死循环和数据丢失的问题。
+1. **数据丢失**:并发 `put` 操作可能导致一个线程的写入被另一个线程覆盖。
+2. **无限循环**:在 JDK 7 及以前的版本中,并发扩容时,由于头插法可能导致链表形成环,从而在 `get` 操作时引发无限循环,CPU 飙升至 100%。
数据丢失这个在 JDK1.7 和 JDK 1.8 中都存在,这里以 JDK 1.8 为例进行介绍。
@@ -384,11 +442,11 @@ Test.lambda avgt 5 1551065180.000 ± 19164407.426 ns/op
Test.parallelStream avgt 5 186345456.667 ± 3210435.590 ns/op
```
-### ConcurrentHashMap 和 Hashtable 的区别
+### ⭐️ConcurrentHashMap 和 Hashtable 的区别
`ConcurrentHashMap` 和 `Hashtable` 的区别主要体现在实现线程安全的方式上不同。
-- **底层数据结构:** JDK1.7 的 `ConcurrentHashMap` 底层采用 **分段的数组+链表** 实现,JDK1.8 采用的数据结构跟 `HashMap1.8` 的结构一样,数组+链表/红黑二叉树。`Hashtable` 和 JDK1.8 之前的 `HashMap` 的底层数据结构类似都是采用 **数组+链表** 的形式,数组是 HashMap 的主体,链表则是主要为了解决哈希冲突而存在的;
+- **底层数据结构:** JDK1.7 的 `ConcurrentHashMap` 底层采用 **分段的数组+链表** 实现,在 JDK1.8 中采用的数据结构跟 `HashMap` 的结构一样,数组+链表/红黑二叉树。`Hashtable` 和 JDK1.8 之前的 `HashMap` 的底层数据结构类似都是采用 **数组+链表** 的形式,数组是 HashMap 的主体,链表则是主要为了解决哈希冲突而存在的;
- **实现线程安全的方式(重要):**
- 在 JDK1.7 的时候,`ConcurrentHashMap` 对整个桶数组进行了分割分段(`Segment`,分段锁),每一把锁只锁容器其中一部分数据(下面有示意图),多线程访问容器里不同数据段的数据,就不会存在锁竞争,提高并发访问率。
- 到了 JDK1.8 的时候,`ConcurrentHashMap` 已经摒弃了 `Segment` 的概念,而是直接用 `Node` 数组+链表+红黑树的数据结构来实现,并发控制使用 `synchronized` 和 CAS 来操作。(JDK1.6 以后 `synchronized` 锁做了很多优化) 整个看起来就像是优化过且线程安全的 `HashMap`,虽然在 JDK1.8 中还能看到 `Segment` 的数据结构,但是已经简化了属性,只是为了兼容旧版本;
@@ -432,7 +490,7 @@ static final class TreeBin extends Node {
}
```
-### ConcurrentHashMap 线程安全的具体实现方式/底层具体实现
+### ⭐️ConcurrentHashMap 线程安全的具体实现方式/底层具体实现
#### JDK1.8 之前
@@ -463,7 +521,7 @@ Java 8 几乎完全重写了 `ConcurrentHashMap`,代码量从原来 Java 7 中
Java 8 中,锁粒度更细,`synchronized` 只锁定当前链表或红黑二叉树的首节点,这样只要 hash 不冲突,就不会产生并发,就不会影响其他 Node 的读写,效率大幅提升。
-### JDK 1.7 和 JDK 1.8 的 ConcurrentHashMap 实现有什么不同?
+### ⭐️JDK 1.7 和 JDK 1.8 的 ConcurrentHashMap 实现有什么不同?
- **线程安全实现方式**:JDK 1.7 采用 `Segment` 分段锁来保证安全, `Segment` 是继承自 `ReentrantLock`。JDK1.8 放弃了 `Segment` 分段锁的设计,采用 `Node + CAS + synchronized` 保证线程安全,锁粒度更细,`synchronized` 只锁定当前链表或红黑二叉树的首节点。
- **Hash 碰撞解决方法** : JDK 1.7 采用拉链法,JDK1.8 采用拉链法结合红黑树(链表长度超过一定阈值时,将链表转换为红黑树)。
@@ -500,7 +558,7 @@ public static final Object NULL = new Object();
翻译过来之后的,大致意思还是单线程下可以容忍歧义,而多线程下无法容忍。
-### ConcurrentHashMap 能保证复合操作的原子性吗?
+### ⭐️ConcurrentHashMap 能保证复合操作的原子性吗?
`ConcurrentHashMap` 是线程安全的,意味着它可以保证多个线程同时对它进行读写操作时,不会出现数据不一致的情况,也不会导致 JDK1.7 及之前版本的 `HashMap` 多线程操作导致死循环问题。但是,这并不意味着它可以保证所有的复合操作都是原子性的,一定不要搞混了!
diff --git a/docs/java/collection/linkedhashmap-source-code.md b/docs/java/collection/linkedhashmap-source-code.md
index 41d414610fa..c1c59d04d1f 100644
--- a/docs/java/collection/linkedhashmap-source-code.md
+++ b/docs/java/collection/linkedhashmap-source-code.md
@@ -1,8 +1,13 @@
---
title: LinkedHashMap 源码分析
+description: LinkedHashMap源码深度剖析:详解LinkedHashMap维护双向链表实现插入/访问有序、LRU缓存实现、与HashMap区别及遍历效率优化。
category: Java
tag:
- Java集合
+head:
+ - - meta
+ - name: keywords
+ content: LinkedHashMap源码,插入顺序,访问顺序,LRU缓存,双向链表,有序Map,LinkedHashMap实现原理
---
## LinkedHashMap 简介
@@ -258,7 +263,7 @@ public V get(Object key) {
```java
void afterNodeAccess(Node < K, V > e) { // move node to last
LinkedHashMap.Entry < K, V > last;
- //如果accessOrder 且当前节点不未链表尾节点
+ //如果accessOrder 且当前节点不为链表尾节点
if (accessOrder && (last = tail) != e) {
//获取当前节点、以及前驱节点和后继节点
@@ -454,7 +459,7 @@ void afterNodeInsertion(boolean evict) { // possibly remove eldest
## LinkedHashMap 和 HashMap 遍历性能比较
-`LinkedHashMap` 维护了一个双向链表来记录数据插入的顺序,因此在迭代遍历生成的迭代器的时候,是按照双向链表的路径进行遍历的。这一点相比于 `HashMap` 那种遍历整个 bucket 的方式来说,高效需多。
+`LinkedHashMap` 维护了一个双向链表来记录数据插入的顺序,因此在迭代遍历生成的迭代器的时候,是按照双向链表的路径进行遍历的。这一点相比于 `HashMap` 那种遍历整个 bucket 的方式来说,高效许多。
这一点我们可以从两者的迭代器中得以印证,先来看看 `HashMap` 的迭代器,可以看到 `HashMap` 迭代键值对时会用到一个 `nextNode` 方法,该方法会返回 next 指向的下一个元素,并会从 next 开始遍历 bucket 找到下一个 bucket 中不为空的元素 Node。
@@ -484,7 +489,7 @@ void afterNodeInsertion(boolean evict) { // possibly remove eldest
}
```
-相比之下 `LinkedHashMap` 的迭代器则是直接使用通过 `after` 指针快速定位到当前节点的后继节点,简洁高效需多。
+相比之下 `LinkedHashMap` 的迭代器则是直接使用通过 `after` 指针快速定位到当前节点的后继节点,简洁高效许多。
```java
final class LinkedEntryIterator extends LinkedHashIterator
@@ -550,7 +555,7 @@ System.out.println("linkedHashMap get time: " + (end - start));
System.out.println(num);
```
-从输出结果来看,因为 `LinkedHashMap` 需要维护双向链表的缘故,插入元素相较于 `HashMap` 会更耗时,但是有了双向链表明确的前后节点关系,迭代效率相对于前者高效了需多。不过,总体来说却别不大,毕竟数据量这么庞大。
+从输出结果来看,因为 `LinkedHashMap` 需要维护双向链表的缘故,插入元素相较于 `HashMap` 会更耗时,但是有了双向链表明确的前后节点关系,迭代效率相对于前者高效了许多。不过,总体来说却别不大,毕竟数据量这么庞大。
```bash
map time putVal: 5880
diff --git a/docs/java/collection/linkedlist-source-code.md b/docs/java/collection/linkedlist-source-code.md
index 343321419e4..b6d4c3d598c 100644
--- a/docs/java/collection/linkedlist-source-code.md
+++ b/docs/java/collection/linkedlist-source-code.md
@@ -1,8 +1,13 @@
---
title: LinkedList 源码分析
+description: LinkedList源码深度解析:剖析双向链表结构、Deque接口实现、头尾插入删除O(1)时间复杂度、与ArrayList性能对比及适用场景。
category: Java
tag:
- Java集合
+head:
+ - - meta
+ - name: keywords
+ content: LinkedList源码,双向链表,Deque接口,LinkedList与ArrayList区别,插入删除性能,链表实现
---
@@ -23,7 +28,7 @@ tag:
- 头部插入/删除:只需要修改头结点的指针即可完成插入/删除操作,因此时间复杂度为 O(1)。
- 尾部插入/删除:只需要修改尾结点的指针即可完成插入/删除操作,因此时间复杂度为 O(1)。
-- 指定位置插入/删除:需要先移动到指定位置,再修改指定节点的指针完成插入/删除,因此需要移动平均 n/2 个元素,时间复杂度为 O(n)。
+- 指定位置插入/删除:需要先移动到指定位置,再修改指定节点的指针完成插入/删除,不过由于有头尾指针,可以从较近的指针出发,因此需要遍历平均 n/4 个元素,时间复杂度为 O(n)。
### LinkedList 为什么不能实现 RandomAccess 接口?
@@ -99,7 +104,7 @@ public LinkedList(Collection extends E> c) {
`add()` 方法有两个版本:
- `add(E e)`:用于在 `LinkedList` 的尾部插入元素,即将新元素作为链表的最后一个元素,时间复杂度为 O(1)。
-- `add(int index, E element)`:用于在指定位置插入元素。这种插入方式需要先移动到指定位置,再修改指定节点的指针完成插入/删除,因此需要移动平均 n/2 个元素,时间复杂度为 O(n)。
+- `add(int index, E element)`:用于在指定位置插入元素。这种插入方式需要先移动到指定位置,再修改指定节点的指针完成插入/删除,因此需要移动平均 n/4 个元素,时间复杂度为 O(n)。
```java
// 在链表尾部插入元素
diff --git a/docs/java/collection/priorityqueue-source-code.md b/docs/java/collection/priorityqueue-source-code.md
index b38cae9bcb9..c3cd5c5a5a8 100644
--- a/docs/java/collection/priorityqueue-source-code.md
+++ b/docs/java/collection/priorityqueue-source-code.md
@@ -1,8 +1,13 @@
---
title: PriorityQueue 源码分析(付费)
+description: PriorityQueue源码深度解析:详解基于二叉堆的优先队列实现、堆化siftUp/siftDown操作、Comparator自定义排序、动态扩容机制。
category: Java
tag:
- Java集合
+head:
+ - - meta
+ - name: keywords
+ content: PriorityQueue源码,优先队列,二叉堆,小顶堆,堆排序,Comparator,优先级队列实现
---
**PriorityQueue 源码分析** 为我的[知识星球](https://javaguide.cn/about-the-author/zhishixingqiu-two-years.html)(点击链接即可查看详细介绍以及加入方法)专属内容,已经整理到了[《Java 必读源码系列》](https://javaguide.cn/zhuanlan/source-code-reading.html)中。
diff --git a/docs/java/concurrent/aqs.md b/docs/java/concurrent/aqs.md
index d95388f0e02..2ac1a44c594 100644
--- a/docs/java/concurrent/aqs.md
+++ b/docs/java/concurrent/aqs.md
@@ -1,10 +1,17 @@
---
title: AQS 详解
+description: AQS抽象队列同步器深度解析:详解AQS核心原理、CLH队列结构、独占锁与共享锁实现、ReentrantLock/Semaphore等同步器应用、线程阻塞唤醒机制。
category: Java
tag:
- Java并发
+head:
+ - - meta
+ - name: keywords
+ content: AQS,AbstractQueuedSynchronizer,队列同步器,独占锁,共享锁,CLH队列,ReentrantLock实现原理
---
+
+
## AQS 介绍
AQS 的全称为 `AbstractQueuedSynchronizer` ,翻译过来的意思就是抽象队列同步器。这个类在 `java.util.concurrent.locks` 包下面。
@@ -18,31 +25,88 @@ public abstract class AbstractQueuedSynchronizer extends AbstractOwnableSynchron
}
```
-AQS 为构建锁和同步器提供了一些通用功能的实现,因此,使用 AQS 能简单且高效地构造出应用广泛的大量的同步器,比如我们提到的 `ReentrantLock`,`Semaphore`,其他的诸如 `ReentrantReadWriteLock`,`SynchronousQueue`等等皆是基于 AQS 的。
+AQS 为构建锁和同步器提供了一些通用功能的实现。因此,使用 AQS 能简单且高效地构造出应用广泛的大量的同步器,比如我们提到的 `ReentrantLock`,`Semaphore`,其他的诸如 `ReentrantReadWriteLock`,`SynchronousQueue`等等皆是基于 AQS 的。
## AQS 原理
在面试中被问到并发知识的时候,大多都会被问到“请你说一下自己对于 AQS 原理的理解”。下面给大家一个示例供大家参考,面试不是背题,大家一定要加入自己的思想,即使加入不了自己的思想也要保证自己能够通俗的讲出来而不是背出来。
+### AQS 快速了解
+
+在真正讲解 AQS 源码之前,需要对 AQS 有一个整体层面的认识。这里会先通过几个问题,从整体层面上认识 AQS,了解 AQS 在整个 Java 并发中所位于的层面,之后在学习 AQS 源码的过程中,才能更加了解同步器和 AQS 之间的关系。
+
+#### AQS 的作用是什么?
+
+AQS 解决了开发者在实现同步器时的复杂性问题。它提供了一个通用框架,用于实现各种同步器,例如 **可重入锁**(`ReentrantLock`)、**信号量**(`Semaphore`)和 **倒计时器**(`CountDownLatch`)。通过封装底层的线程同步机制,AQS 将复杂的线程管理逻辑隐藏起来,使开发者只需专注于具体的同步逻辑。
+
+简单来说,AQS 是一个抽象类,为同步器提供了通用的 **执行框架**。它定义了 **资源获取和释放的通用流程**,而具体的资源获取逻辑则由具体同步器通过重写模板方法来实现。 因此,可以将 AQS 看作是同步器的 **基础“底座”**,而同步器则是基于 AQS 实现的 **具体“应用”**。
+
+#### AQS 为什么使用 CLH 锁队列的变体?
+
+CLH 锁是一种基于 **自旋锁** 的优化实现。
+
+先说一下自旋锁存在的问题:自旋锁通过线程不断对一个原子变量执行 `compareAndSet`(简称 `CAS`)操作来尝试获取锁。在高并发场景下,多个线程会同时竞争同一个原子变量,容易造成某个线程的 `CAS` 操作长时间失败,从而导致 **“饥饿”问题**(某些线程可能永远无法获取锁)。
+
+CLH 锁通过引入一个队列来组织并发竞争的线程,对自旋锁进行了改进:
+
+- 每个线程会作为一个节点加入到队列中,并通过自旋监控前一个线程节点的状态,而不是直接竞争共享变量。
+- 线程按顺序排队,确保公平性,从而避免了 “饥饿” 问题。
+
+AQS(AbstractQueuedSynchronizer)在 CLH 锁的基础上进一步优化,形成了其内部的 **CLH 队列变体**。主要改进点有以下两方面:
+
+1. **自旋 + 阻塞**: CLH 锁使用纯自旋方式等待锁的释放,但大量的自旋操作会占用过多的 CPU 资源。AQS 引入了 **自旋 + 阻塞** 的混合机制:
+ - 如果线程获取锁失败,会先短暂自旋尝试获取锁;
+ - 如果仍然失败,则线程会进入阻塞状态,等待被唤醒,从而减少 CPU 的浪费。
+2. **单向队列改为双向队列**:CLH 锁使用单向队列,节点只知道前驱节点的状态,而当某个节点释放锁时,需要通过队列唤醒后续节点。AQS 将队列改为 **双向队列**,新增了 `next` 指针,使得节点不仅知道前驱节点,也可以直接唤醒后继节点,从而简化了队列操作,提高了唤醒效率。
+
+#### AQS 的性能比较好,原因是什么?
+
+因为 AQS 内部大量使用了 `CAS` 操作。
+
+AQS 内部通过队列来存储等待的线程节点。由于队列是共享资源,在多线程场景下,需要保证队列的同步访问。
+
+AQS 内部通过 `CAS` 操作来控制队列的同步访问,`CAS` 操作主要用于控制 `队列初始化` 、 `线程节点入队` 两个操作的并发安全。虽然利用 `CAS` 控制并发安全可以保证比较好的性能,但同时会带来比较高的 **编码复杂度** 。
+
+#### AQS 中为什么 Node 节点需要不同的状态?
+
+AQS 中的 `waitStatus` 状态类似于 **状态机** ,通过不同状态来表明 Node 节点的不同含义,并且根据不同操作,来控制状态之间的流转。
+
+- 状态 `0` :新节点加入队列之后,初始状态为 `0` 。
+
+- 状态 `SIGNAL` :当有新的节点加入队列,此时新节点的前继节点状态就会由 `0` 更新为 `SIGNAL` ,表示前继节点释放锁之后,需要对新节点进行唤醒操作。如果唤醒 `SIGNAL` 状态节点的后续节点,就会将 `SIGNAL` 状态更新为 `0` 。即通过清除 `SIGNAL` 状态,表示已经执行了唤醒操作。
+
+- 状态 `CANCELLED` :如果一个节点在队列中等待获取锁锁时,因为某种原因失败了,该节点的状态就会变为 `CANCELLED` ,表明取消获取锁,这种状态的节点是异常的,无法被唤醒,也无法唤醒后继节点。
+
### AQS 核心思想
-AQS 核心思想是,如果被请求的共享资源空闲,则将当前请求资源的线程设置为有效的工作线程,并且将共享资源设置为锁定状态。如果被请求的共享资源被占用,那么就需要一套线程阻塞等待以及被唤醒时锁分配的机制,这个机制 AQS 是基于 **CLH 锁** (Craig, Landin, and Hagersten locks) 实现的。
+AQS 核心思想是,如果被请求的共享资源空闲,则将当前请求资源的线程设置为有效的工作线程,并且将共享资源设置为锁定状态。如果被请求的共享资源被占用,那么就需要一套线程阻塞等待以及被唤醒时锁分配的机制,这个机制 AQS 是基于 **CLH 锁** (Craig, Landin, and Hagersten locks) 进一步优化实现的。
+
+**CLH 锁** 对自旋锁进行了改进,是基于单链表的自旋锁。在多线程场景下,会将请求获取锁的线程组织成一个单向队列,每个等待的线程会通过自旋访问前一个线程节点的状态,前一个节点释放锁之后,当前节点才可以获取锁。**CLH 锁** 的队列结构如下图所示。
+
+
+
+AQS 中使用的 **等待队列** 是 CLH 锁队列的变体(接下来简称为 CLH 变体队列)。
+
+AQS 的 CLH 变体队列是一个双向队列,会暂时获取不到锁的线程将被加入到该队列中,CLH 变体队列和原本的 CLH 锁队列的区别主要有两点:
-CLH 锁是对自旋锁的一种改进,是一个虚拟的双向队列(虚拟的双向队列即不存在队列实例,仅存在结点之间的关联关系),暂时获取不到锁的线程将被加入到该队列中。AQS 将每条请求共享资源的线程封装成一个 CLH 队列锁的一个结点(Node)来实现锁的分配。在 CLH 队列锁中,一个节点表示一个线程,它保存着线程的引用(thread)、 当前节点在队列中的状态(waitStatus)、前驱节点(prev)、后继节点(next)。
+- 由 **自旋** 优化为 **自旋 + 阻塞** :自旋操作的性能很高,但大量的自旋操作比较占用 CPU 资源,因此在 CLH 变体队列中会先通过自旋尝试获取锁,如果失败再进行阻塞等待。
+- 由 **单向队列** 优化为 **双向队列** :在 CLH 变体队列中,会对等待的线程进行阻塞操作,当队列前边的线程释放锁之后,需要对后边的线程进行唤醒,因此增加了 `next` 指针,成为了双向队列。
-CLH 队列结构如下图所示:
+AQS 将每条请求共享资源的线程封装成一个 CLH 变体队列的一个结点(Node)来实现锁的分配。在 CLH 变体队列中,一个节点表示一个线程,它保存着线程的引用(thread)、 当前节点在队列中的状态(waitStatus)、前驱节点(prev)、后继节点(next)。
-
+AQS 中的 CLH 变体队列结构如下图所示:
+
+
关于 AQS 核心数据结构-CLH 锁的详细解读,强烈推荐阅读 [Java AQS 核心数据结构-CLH 锁 - Qunar 技术沙龙](https://mp.weixin.qq.com/s/jEx-4XhNGOFdCo4Nou5tqg) 这篇文章。
AQS(`AbstractQueuedSynchronizer`)的核心原理图:
-
+
AQS 使用 **int 成员变量 `state` 表示同步状态**,通过内置的 **FIFO 线程等待/等待队列** 来完成获取资源线程的排队工作。
-`state` 变量由 `volatile` 修饰,用于展示当前临界资源的获锁情况。
+`state` 变量由 `volatile` 修饰,用于展示当前临界资源的获取情况。
```java
// 共享变量,使用volatile修饰保证线程可见性
@@ -66,7 +130,7 @@ protected final boolean compareAndSetState(int expect, int update) {
}
```
-以可重入的互斥锁 `ReentrantLock` 为例,它的内部维护了一个 `state` 变量,用来表示锁的占用状态。`state` 的初始值为 0,表示锁处于未锁定状态。当线程 A 调用 `lock()` 方法时,会尝试通过 `tryAcquire()` 方法独占该锁,并让 `state` 的值加 1。如果成功了,那么线程 A 就获取到了锁。如果失败了,那么线程 A 就会被加入到一个等待队列(CLH 队列)中,直到其他线程释放该锁。假设线程 A 获取锁成功了,释放锁之前,A 线程自己是可以重复获取此锁的(`state` 会累加)。这就是可重入性的体现:一个线程可以多次获取同一个锁而不会被阻塞。但是,这也意味着,一个线程必须释放与获取的次数相同的锁,才能让 `state` 的值回到 0,也就是让锁恢复到未锁定状态。只有这样,其他等待的线程才能有机会获取该锁。
+以可重入的互斥锁 `ReentrantLock` 为例,它的内部维护了一个 `state` 变量,用来表示锁的占用状态。`state` 的初始值为 0,表示锁处于未锁定状态。当线程 A 调用 `lock()` 方法时,会尝试通过 `tryAcquire()` 方法独占该锁,并让 `state` 的值加 1。如果成功了,那么线程 A 就获取到了锁。如果失败了,那么线程 A 就会被加入到一个等待队列(CLH 变体队列)中,直到其他线程释放该锁。假设线程 A 获取锁成功了,释放锁之前,A 线程自己是可以重复获取此锁的(`state` 会累加)。这就是可重入性的体现:一个线程可以多次获取同一个锁而不会被阻塞。但是,这也意味着,一个线程必须释放与获取的次数相同的锁,才能让 `state` 的值回到 0,也就是让锁恢复到未锁定状态。只有这样,其他等待的线程才能有机会获取该锁。
线程 A 尝试获取锁的过程如下图所示(图源[从 ReentrantLock 的实现看 AQS 的原理及应用 - 美团技术团队](./reentrantlock.md)):
@@ -74,20 +138,39 @@ protected final boolean compareAndSetState(int expect, int update) {
再以倒计时器 `CountDownLatch` 以例,任务分为 N 个子线程去执行,`state` 也初始化为 N(注意 N 要与线程个数一致)。这 N 个子线程开始执行任务,每执行完一个子线程,就调用一次 `countDown()` 方法。该方法会尝试使用 CAS(Compare and Swap) 操作,让 `state` 的值减少 1。当所有的子线程都执行完毕后(即 `state` 的值变为 0),`CountDownLatch` 会调用 `unpark()` 方法,唤醒主线程。这时,主线程就可以从 `await()` 方法(`CountDownLatch` 中的`await()` 方法而非 AQS 中的)返回,继续执行后续的操作。
-### AQS 资源共享方式
+### Node 节点 waitStatus 状态含义
-AQS 定义两种资源共享方式:`Exclusive`(独占,只有一个线程能执行,如`ReentrantLock`)和`Share`(共享,多个线程可同时执行,如`Semaphore`/`CountDownLatch`)。
+AQS 中的 `waitStatus` 状态类似于 **状态机** ,通过不同状态来表明 Node 节点的不同含义,并且根据不同操作,来控制状态之间的流转。
-一般来说,自定义同步器的共享方式要么是独占,要么是共享,他们也只需实现`tryAcquire-tryRelease`、`tryAcquireShared-tryReleaseShared`中的一种即可。但 AQS 也支持自定义同步器同时实现独占和共享两种方式,如`ReentrantReadWriteLock`。
+| Node 节点状态 | 值 | 含义 |
+| ------------- | --- | ------------------------------------------------------------------------------------------------------------------------- |
+| `CANCELLED` | 1 | 表示线程已经取消获取锁。线程在等待获取资源时被中断、等待资源超时会更新为该状态。 |
+| `SIGNAL` | -1 | 表示后继节点需要当前节点唤醒。在当前线程节点释放锁之后,需要对后继节点进行唤醒。 |
+| `CONDITION` | -2 | 表示节点在等待 Condition。当其他线程调用了 Condition 的 `signal()` 方法后,节点会从等待队列转移到同步队列中等待获取资源。 |
+| `PROPAGATE` | -3 | 用于共享模式。在共享模式下,可能会出现线程在队列中无法被唤醒的情况,因此引入了 `PROPAGATE` 状态来解决这个问题。 |
+| | 0 | 加入队列的新节点的初始状态。 |
-### 自定义同步器
+在 AQS 的源码中,经常使用 `> 0` 、 `< 0` 来对 `waitStatus` 进行判断。
-同步器的设计是基于模板方法模式的,如果需要自定义同步器一般的方式是这样(模板方法模式很经典的一个应用):
+如果 `waitStatus > 0` ,表明节点的状态已经取消等待获取资源。
-1. 使用者继承 `AbstractQueuedSynchronizer` 并重写指定的方法。
-2. 将 AQS 组合在自定义同步组件的实现中,并调用其模板方法,而这些模板方法会调用使用者重写的方法。
+如果 `waitStatus < 0` ,表明节点的状态处于正常的状态,即没有取消等待。
-这和我们以往通过实现接口的方式有很大区别,这是模板方法模式很经典的一个运用。
+其中 `SIGNAL` 状态是最重要的,节点状态流转以及对应操作如下:
+
+| 状态流转 | 对应操作 |
+| ---------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------- |
+| `0` | 新节点入队时,初始状态为 `0` 。 |
+| `0 -> SIGNAL` | 新节点入队时,它的前继节点状态会由 `0` 更新为 `SIGNAL` 。`SIGNAL` 状态表明该节点的后续节点需要被唤醒。 |
+| `SIGNAL -> 0` | 在唤醒后继节点时,需要清除当前节点的状态。通常发生在 `head` 节点,比如 `head` 节点的状态由 `SIGNAL` 更新为 `0` ,表示已经对 `head` 节点的后继节点唤醒了。 |
+| `0 -> PROPAGATE` | AQS 内部引入了 `PROPAGATE` 状态,为了解决并发场景下,可能造成的线程节点无法唤醒的情况。(在 AQS 共享模式获取资源的源码分析会讲到) |
+
+### 自定义同步器
+
+基于 AQS 可以实现自定义的同步器, AQS 提供了 5 个模板方法(模板方法模式)。如果需要自定义同步器一般的方式是这样(模板方法模式很经典的一个应用):
+
+1. 自定义的同步器继承 `AbstractQueuedSynchronizer` 。
+2. 重写 AQS 暴露的模板方法。
**AQS 使用了模板方法模式,自定义同步器时需要重写下面几个 AQS 提供的钩子方法:**
@@ -110,6 +193,742 @@ protected boolean isHeldExclusively()
除了上面提到的钩子方法之外,AQS 类中的其他方法都是 `final` ,所以无法被其他类重写。
+### AQS 资源共享方式
+
+AQS 定义两种资源共享方式:`Exclusive`(独占,只有一个线程能执行,如`ReentrantLock`)和`Share`(共享,多个线程可同时执行,如`Semaphore`/`CountDownLatch`)。
+
+一般来说,自定义同步器的共享方式要么是独占,要么是共享,他们也只需实现`tryAcquire-tryRelease`、`tryAcquireShared-tryReleaseShared`中的一种即可。但 AQS 也支持自定义同步器同时实现独占和共享两种方式,如`ReentrantReadWriteLock`。
+
+### AQS 资源获取源码分析(独占模式)
+
+AQS 中以独占模式获取资源的入口方法是 `acquire()` ,如下:
+
+```JAVA
+// AQS
+public final void acquire(int arg) {
+ if (!tryAcquire(arg) &&
+ acquireQueued(addWaiter(Node.EXCLUSIVE), arg))
+ selfInterrupt();
+}
+```
+
+在 `acquire()` 中,线程会先尝试获取共享资源;如果获取失败,会将线程封装为 Node 节点加入到 AQS 的等待队列中;加入队列之后,会让等待队列中的线程尝试获取资源,并且会对线程进行阻塞操作。分别对应以下三个方法:
+
+- `tryAcquire()` :尝试获取锁(模板方法),`AQS` 不提供具体实现,由子类实现。
+- `addWaiter()` :如果获取锁失败,会将当前线程封装为 Node 节点加入到 AQS 的 CLH 变体队列中等待获取锁。
+- `acquireQueued()` :对线程进行阻塞,并调用 `tryAcquire()` 方法让队列中的线程尝试获取锁。
+
+#### `tryAcquire()` 分析
+
+AQS 中对应的 `tryAcquire()` 模板方法如下:
+
+```JAVA
+// AQS
+protected boolean tryAcquire(int arg) {
+ throw new UnsupportedOperationException();
+}
+```
+
+`tryAcquire()` 方法是 AQS 提供的模板方法,不提供默认实现。
+
+因此,这里分析 `tryAcquire()` 方法时,以 `ReentrantLock` 的非公平锁(独占锁)为例进行分析,`ReentrantLock` 内部实现的 `tryAcquire()` 会调用到下边的 `nonfairTryAcquire()` :
+
+```JAVA
+// ReentrantLock
+final boolean nonfairTryAcquire(int acquires) {
+ final Thread current = Thread.currentThread();
+ // 1、获取 AQS 中的 state 状态
+ int c = getState();
+ // 2、如果 state 为 0,证明锁没有被其他线程占用
+ if (c == 0) {
+ // 2.1、通过 CAS 对 state 进行更新
+ if (compareAndSetState(0, acquires)) {
+ // 2.2、如果 CAS 更新成功,就将锁的持有者设置为当前线程
+ setExclusiveOwnerThread(current);
+ return true;
+ }
+ }
+ // 3、如果当前线程和锁的持有线程相同,说明发生了「锁的重入」
+ else if (current == getExclusiveOwnerThread()) {
+ int nextc = c + acquires;
+ if (nextc < 0) // overflow
+ throw new Error("Maximum lock count exceeded");
+ // 3.1、将锁的重入次数加 1
+ setState(nextc);
+ return true;
+ }
+ // 4、如果锁被其他线程占用,就返回 false,表示获取锁失败
+ return false;
+}
+```
+
+在 `nonfairTryAcquire()` 方法内部,主要通过两个核心操作去完成资源的获取:
+
+- 通过 `CAS` 更新 `state` 变量。`state == 0` 表示资源没有被占用。`state > 0` 表示资源被占用,此时 `state` 表示重入次数。
+- 通过 `setExclusiveOwnerThread()` 设置持有资源的线程。
+
+如果线程更新 `state` 变量成功,就表明获取到了资源, 因此将持有资源的线程设置为当前线程即可。
+
+#### `addWaiter()` 分析
+
+在通过 `tryAcquire()` 方法尝试获取资源失败之后,会调用 `addWaiter()` 方法将当前线程封装为 Node 节点加入 `AQS` 内部的队列中。`addWaite()` 代码如下:
+
+```JAVA
+// AQS
+private Node addWaiter(Node mode) {
+ // 1、将当前线程封装为 Node 节点。
+ Node node = new Node(Thread.currentThread(), mode);
+ Node pred = tail;
+ // 2、如果 pred != null,则证明 tail 节点已经被初始化,直接将 Node 节点加入队列即可。
+ if (pred != null) {
+ node.prev = pred;
+ // 2.1、通过 CAS 控制并发安全。
+ if (compareAndSetTail(pred, node)) {
+ pred.next = node;
+ return node;
+ }
+ }
+ // 3、初始化队列,并将新创建的 Node 节点加入队列。
+ enq(node);
+ return node;
+}
+```
+
+**节点入队的并发安全:**
+
+在 `addWaiter()` 方法中,需要执行 Node 节点 **入队** 的操作。由于是在多线程环境下,因此需要通过 `CAS` 操作保证并发安全。
+
+通过 `CAS` 操作去更新 `tail` 指针指向新入队的 Node 节点,`CAS` 可以保证只有一个线程会成功修改 `tail` 指针,以此来保证 Node 节点入队时的并发安全。
+
+**AQS 内部队列的初始化:**
+
+在执行 `addWaiter()` 时,如果发现 `pred == null` ,即 `tail` 指针为 null,则证明队列没有初始化,需要调用 `enq()` 方法初始化队列,并将 `Node` 节点加入到初始化后的队列中,代码如下:
+
+```JAVA
+// AQS
+private Node enq(final Node node) {
+ for (;;) {
+ Node t = tail;
+ if (t == null) {
+ // 1、通过 CAS 操作保证队列初始化的并发安全
+ if (compareAndSetHead(new Node()))
+ tail = head;
+ } else {
+ // 2、与 addWaiter() 方法中节点入队的操作相同
+ node.prev = t;
+ if (compareAndSetTail(t, node)) {
+ t.next = node;
+ return t;
+ }
+ }
+ }
+}
+```
+
+在 `enq()` 方法中初始化队列,在初始化过程中,也需要通过 `CAS` 来保证并发安全。
+
+初始化队列总共包含两个步骤:初始化 `head` 节点、`tail` 指向 `head` 节点。
+
+**初始化后的队列如下图所示:**
+
+
+
+#### `acquireQueued()` 分析
+
+为了方便阅读,这里再贴一下 `AQS` 中 `acquire()` 获取资源的代码:
+
+```JAVA
+// AQS
+public final void acquire(int arg) {
+ if (!tryAcquire(arg) &&
+ acquireQueued(addWaiter(Node.EXCLUSIVE), arg))
+ selfInterrupt();
+}
+```
+
+在 `acquire()` 方法中,通过 `addWaiter()` 方法将 `Node` 节点加入队列之后,就会调用 `acquireQueued()` 方法。代码如下:
+
+```JAVA
+// AQS:令队列中的节点尝试获取锁,并且对线程进行阻塞。
+final boolean acquireQueued(final Node node, int arg) {
+ boolean failed = true;
+ try {
+ boolean interrupted = false;
+ for (;;) {
+ // 1、尝试获取锁。
+ final Node p = node.predecessor();
+ if (p == head && tryAcquire(arg)) {
+ setHead(node);
+ p.next = null; // help GC
+ failed = false;
+ return interrupted;
+ }
+ // 2、判断线程是否可以阻塞,如果可以,则阻塞当前线程。
+ if (shouldParkAfterFailedAcquire(p, node) &&
+ parkAndCheckInterrupt())
+ interrupted = true;
+ }
+ } finally {
+ // 3、如果获取锁失败,就会取消获取锁,将节点状态更新为 CANCELLED。
+ if (failed)
+ cancelAcquire(node);
+ }
+}
+```
+
+在 `acquireQueued()` 方法中,主要做两件事情:
+
+- **尝试获取资源:** 当前线程加入队列之后,如果发现前继节点是 `head` 节点,说明当前线程是队列中第一个等待的节点,于是调用 `tryAcquire()` 尝试获取资源。
+
+- **阻塞当前线程** :如果尝试获取资源失败,就需要阻塞当前线程,等待被唤醒之后获取资源。
+
+**1、尝试获取资源**
+
+在 `acquireQueued()` 方法中,尝试获取资源总共有 2 个步骤:
+
+- `p == head` :表明当前节点的前继节点为 `head` 节点。此时当前节点为 AQS 队列中的第一个等待节点。
+- `tryAcquire(arg) == true` :表明当前线程尝试获取资源成功。
+
+在成功获取资源之后,就需要将当前线程的节点 **从等待队列中移除** 。移除操作为:将当前等待的线程节点设置为 `head` 节点(`head` 节点是虚拟节点,并不参与排队获取资源)。
+
+**2、阻塞当前线程**
+
+在 `AQS` 中,当前节点的唤醒需要依赖于上一个节点。如果上一个节点取消获取锁,它的状态就会变为 `CANCELLED` ,`CANCELLED` 状态的节点没有获取到锁,也就无法执行解锁操作对当前节点进行唤醒。因此在阻塞当前线程之前,需要跳过 `CANCELLED` 状态的节点。
+
+通过 `shouldParkAfterFailedAcquire()` 方法来判断当前线程节点是否可以阻塞,如下:
+
+```JAVA
+// AQS:判断当前线程节点是否可以阻塞。
+private static boolean shouldParkAfterFailedAcquire(Node pred, Node node) {
+ int ws = pred.waitStatus;
+ // 1、前继节点状态正常,直接返回 true 即可。
+ if (ws == Node.SIGNAL)
+ return true;
+ // 2、ws > 0 表示前继节点的状态异常,即为 CANCELLED 状态,需要跳过异常状态的节点。
+ if (ws > 0) {
+ do {
+ node.prev = pred = pred.prev;
+ } while (pred.waitStatus > 0);
+ pred.next = node;
+ } else {
+ // 3、如果前继节点的状态不是 SIGNAL,也不是 CANCELLED,就将状态设置为 SIGNAL。
+ compareAndSetWaitStatus(pred, ws, Node.SIGNAL);
+ }
+ return false;
+}
+```
+
+`shouldParkAfterFailedAcquire()` 方法中的判断逻辑:
+
+- 如果发现前继节点的状态是 `SIGNAL` ,则可以阻塞当前线程。
+- 如果发现前继节点的状态是 `CANCELLED` ,则需要跳过 `CANCELLED` 状态的节点。
+- 如果发现前继节点的状态不是 `SIGNAL` 和 `CANCELLED` ,表明前继节点的状态处于正常等待资源的状态,因此将前继节点的状态设置为 `SIGNAL` ,表明该前继节点需要对后续节点进行唤醒。
+
+当判断当前线程可以阻塞之后,通过调用 `parkAndCheckInterrupt()` 方法来阻塞当前线程。内部使用了 `LockSupport` 来实现阻塞。`LockSupoprt` 底层是基于 `Unsafe` 类来阻塞线程,代码如下:
+
+```JAVA
+// AQS
+private final boolean parkAndCheckInterrupt() {
+ // 1、线程阻塞到这里
+ LockSupport.park(this);
+ // 2、线程被唤醒之后,返回线程中断状态
+ return Thread.interrupted();
+}
+```
+
+**为什么在线程被唤醒之后,要返回线程的中断状态呢?**
+
+在 `parkAndCheckInterrupt()` 方法中,当执行完 `LockSupport.park(this)` ,线程会被阻塞,代码如下:
+
+```JAVA
+// AQS
+private final boolean parkAndCheckInterrupt() {
+ LockSupport.park(this);
+ // 线程被唤醒之后,需要返回线程中断状态
+ return Thread.interrupted();
+}
+```
+
+当线程被唤醒之后,需要执行 `Thread.interrupted()` 来返回线程的中断状态,这是为什么呢?
+
+这个和线程的中断协作机制有关系,线程被唤醒之后,并不确定是被中断唤醒,还是被 `LockSupport.unpark()` 唤醒,因此需要通过线程的中断状态来判断。
+
+**在 `acquire()` 方法中,为什么需要调用 `selfInterrupt()` ?**
+
+`acquire()` 方法代码如下:
+
+```JAVA
+// AQS
+public final void acquire(int arg) {
+ if (!tryAcquire(arg) &&
+ acquireQueued(addWaiter(Node.EXCLUSIVE), arg))
+ selfInterrupt();
+}
+```
+
+在 `acquire()` 方法中,当 `if` 语句的条件返回 `true` 后,就会调用 `selfInterrupt()` ,该方法会中断当前线程,为什么需要中断当前线程呢?
+
+当 `if` 判断为 `true` 时,需要 `tryAcquire()` 返回 `false` ,并且 `acquireQueued()` 返回 `true` 。
+
+其中 `acquireQueued()` 方法返回的是线程被唤醒之后的 **中断状态** ,通过执行 `Thread.interrupted()` 来返回。该方法在返回中断状态的同时,会清除线程的中断状态。
+
+因此如果 `if` 判断为 `true` ,表明线程的中断状态为 `true` ,但是调用 `Thread.interrupted()` 之后,线程的中断状态被清除为 `false` ,因此需要重新执行 `selfInterrupt()` 来重新设置线程的中断状态。
+
+### AQS 资源释放源码分析(独占模式)
+
+AQS 中以独占模式释放资源的入口方法是 `release()` ,代码如下:
+
+```JAVA
+// AQS
+public final boolean release(int arg) {
+ // 1、尝试释放锁
+ if (tryRelease(arg)) {
+ Node h = head;
+ // 2、唤醒后继节点
+ if (h != null && h.waitStatus != 0)
+ unparkSuccessor(h);
+ return true;
+ }
+ return false;
+}
+```
+
+在 `release()` 方法中,主要做两件事:尝试释放锁和唤醒后继节点。对应方法如下:
+
+**1、尝试释放锁**
+
+通过 `tryRelease()` 方法尝试释放锁,该方法为模板方法,由自定义同步器实现,因此这里仍然以 `ReentrantLock` 为例来讲解。
+
+`ReentrantLock` 中实现的 `tryRelease()` 方法如下:
+
+```JAVA
+// ReentrantLock
+protected final boolean tryRelease(int releases) {
+ int c = getState() - releases;
+ // 1、判断持有锁的线程是否为当前线程
+ if (Thread.currentThread() != getExclusiveOwnerThread())
+ throw new IllegalMonitorStateException();
+ boolean free = false;
+ // 2、如果 state 为 0,则表明当前线程已经没有重入次数。因此将 free 更新为 true,表明该线程会释放锁。
+ if (c == 0) {
+ free = true;
+ // 3、更新持有资源的线程为 null
+ setExclusiveOwnerThread(null);
+ }
+ // 4、更新 state 值
+ setState(c);
+ return free;
+}
+```
+
+在 `tryRelease()` 方法中,会先计算释放锁之后的 `state` 值,判断 `state` 值是否为 0。
+
+- 如果 `state == 0` ,表明该线程没有重入次数了,更新 `free = true` ,并修改持有资源的线程为 null,表明该线程完全释放这把锁。
+- 如果 `state != 0` ,表明该线程还存在重入次数,因此不更新 `free` 值,`free` 值为 `false` 表明该线程没有完全释放这把锁。
+
+之后更新 `state` 值,并返回 `free` 值,`free` 值表明线程是否完全释放锁。
+
+**2、唤醒后继节点**
+
+如果 `tryRelease()` 返回 `true` ,表明线程已经没有重入次数了,锁已经被完全释放,因此需要唤醒后继节点。
+
+在唤醒后继节点之前,需要判断是否可以唤醒后继节点,判断条件为: `h != null && h.waitStatus != 0` 。这里解释一下为什么要这样判断:
+
+- `h == null` :表明 `head` 节点还没有被初始化,也就是 AQS 中的队列没有被初始化,因此无法唤醒队列中的线程节点。
+- `h != null && h.waitStatus == 0` :表明头节点刚刚初始化完毕(节点的初始化状态为 0),后继节点线程还没有成功入队,因此不需要对后续节点进行唤醒。(当后继节点入队之后,会将前继节点的状态修改为 `SIGNAL` ,表明需要对后继节点进行唤醒)
+- `h != null && h.waitStatus != 0` :其中 `waitStatus` 有可能大于 0,也有可能小于 0。其中 `> 0` 表明节点已经取消等待获取资源,`< 0` 表明节点处于正常等待状态。
+
+接下来进入 `unparkSuccessor()` 方法查看如何唤醒后继节点:
+
+```JAVA
+// AQS:这里的入参 node 为队列的头节点(虚拟头节点)
+private void unparkSuccessor(Node node) {
+ int ws = node.waitStatus;
+ // 1、将头节点的状态进行清除,为后续的唤醒做准备。
+ if (ws < 0)
+ compareAndSetWaitStatus(node, ws, 0);
+
+ Node s = node.next;
+ // 2、如果后继节点异常,则需要从 tail 向前遍历,找到正常状态的节点进行唤醒。
+ if (s == null || s.waitStatus > 0) {
+ s = null;
+ for (Node t = tail; t != null && t != node; t = t.prev)
+ if (t.waitStatus <= 0)
+ s = t;
+ }
+ if (s != null)
+ // 3、唤醒后继节点
+ LockSupport.unpark(s.thread);
+}
+```
+
+在 `unparkSuccessor()` 中,如果头节点的状态 `< 0` (在正常情况下,只要有后继节点,头节点的状态应该为 `SIGNAL` ,即 -1),表示需要对后继节点进行唤醒,因此这里提前清除头节点的状态标识,将状态修改为 0,表示已经执行了对后续节点唤醒的操作。
+
+如果 `s == null` 或者 `s.waitStatus > 0` ,表明后继节点异常,此时不能唤醒异常节点,而是要找到正常状态的节点进行唤醒。
+
+因此需要从 `tail` 指针向前遍历,来找到第一个状态正常(`waitStatus <= 0`)的节点进行唤醒。
+
+**为什么要从 `tail` 指针向前遍历,而不是从 `head` 指针向后遍历,寻找正常状态的节点呢?**
+
+遍历的方向和 **节点的入队操作** 有关。入队方法如下:
+
+```JAVA
+// AQS:节点入队方法
+private Node addWaiter(Node mode) {
+ Node node = new Node(Thread.currentThread(), mode);
+ Node pred = tail;
+ if (pred != null) {
+ // 1、先修改 prev 指针。
+ node.prev = pred;
+ if (compareAndSetTail(pred, node)) {
+ // 2、再修改 next 指针。
+ pred.next = node;
+ return node;
+ }
+ }
+ enq(node);
+ return node;
+}
+```
+
+在 `addWaiter()` 方法中,`node` 节点入队需要修改 `node.prev` 和 `pred.next` 两个指针,但是这两个操作并不是 **原子操作** ,先修改了 `node.prev` 指针,之后才修改 `pred.next` 指针。
+
+在极端情况下,可能会出现 `head` 节点的下一个节点状态为 `CANCELLED` ,此时新入队的节点仅更新了 `node.prev` 指针,还未更新 `pred.next` 指针,如下图:
+
+
+
+这样如果从 `head` 指针向后遍历,无法找到新入队的节点,因此需要从 `tail` 指针向前遍历找到新入队的节点。
+
+### 图解 AQS 工作原理(独占模式)
+
+至此,AQS 中以独占模式获取资源、释放资源的源码就讲完了。为了对 AQS 的工作原理、节点状态变化有一个更加清晰的认识,接下来会通过画图的方式来了解整个 AQS 的工作原理。
+
+由于 AQS 是底层同步工具,获取和释放资源的方法并没有提供具体实现,因此这里基于 `ReentrantLock` 来画图进行讲解。
+
+假设总共有 3 个线程尝试获取锁,线程分别为 `T1` 、 `T2` 和 `T3` 。
+
+此时,假设线程 `T1` 先获取到锁,线程 `T2` 排队等待获取锁。在线程 `T2` 进入队列之前,需要对 AQS 内部队列进行初始化。`head` 节点在初始化后状态为 `0` 。AQS 内部初始化后的队列如下图:
+
+
+
+此时,线程 `T2` 尝试获取锁。由于线程 `T1` 持有锁,因此线程 `T2` 会进入队列中等待获取锁。同时会将前继节点( `head` 节点)的状态由 `0` 更新为 `SIGNAL` ,表示需要对 `head` 节点的后继节点进行唤醒。此时,AQS 内部队列如下图所示:
+
+
+
+此时,线程 `T3` 尝试获取锁。由于线程 `T1` 持有锁,因此线程 `T3` 会进入队列中等待获取锁。同时会将前继节点(线程 `T2` 节点)的状态由 `0` 更新为 `SIGNAL` ,表示线程 `T2` 节点需要对后继节点进行唤醒。此时,AQS 内部队列如下图所示:
+
+
+
+此时,假设线程 `T1` 释放锁,会唤醒后继节点 `T2` 。线程 `T2` 被唤醒后获取到锁,并且会从等待队列中退出。
+
+这里线程 `T2` 节点退出等待队列并不是直接从队列移除,而是令线程 `T2` 节点成为新的 `head` 节点,以此来退出资源获取的等待。此时 AQS 内部队列如下所示:
+
+
+
+此时,假设线程 `T2` 释放锁,会唤醒后继节点 `T3` 。线程 `T3` 获取到锁之后,同样也退出等待队列,即将线程 `T3` 节点变为 `head` 节点来退出资源获取的等待。此时 AQS 内部队列如下所示:
+
+
+
+### AQS 资源获取源码分析(共享模式)
+
+AQS 中以共享模式获取资源的入口方法是 `acquireShared()` ,如下:
+
+```JAVA
+// AQS
+public final void acquireShared(int arg) {
+ if (tryAcquireShared(arg) < 0)
+ doAcquireShared(arg);
+}
+```
+
+在 `acquireShared()` 方法中,会先尝试获取共享锁,如果获取失败,则将当前线程加入到队列中阻塞,等待唤醒后尝试获取共享锁,分别对应一下两个方法:`tryAcquireShared()` 和 `doAcquireShared()` 。
+
+其中 `tryAcquireShared()` 方法是 AQS 提供的模板方法,由同步器来实现具体逻辑。因此这里以 `Semaphore` 为例,来分析共享模式下,如何获取资源。
+
+#### `tryAcquireShared()` 分析
+
+`Semaphore` 中实现了公平锁和非公平锁,接下来以非公平锁为例来分析 `tryAcquireShared()` 源码。
+
+`Semaphore` 中重写的 `tryAcquireShared()` 方法会调用下边的 `nonfairTryAcquireShared()` 方法:
+
+```JAVA
+// Semaphore 重写 AQS 的模板方法
+protected int tryAcquireShared(int acquires) {
+ return nonfairTryAcquireShared(acquires);
+}
+
+// Semaphore
+final int nonfairTryAcquireShared(int acquires) {
+ for (;;) {
+ // 1、获取可用资源数量。
+ int available = getState();
+ // 2、计算剩余资源数量。
+ int remaining = available - acquires;
+ // 3、如果剩余资源数量 < 0,则说明资源不足,直接返回;如果 CAS 更新 state 成功,则说明当前线程获取到了共享资源,直接返回。
+ if (remaining < 0 ||
+ compareAndSetState(available, remaining))
+ return remaining;
+ }
+}
+```
+
+在共享模式下,AQS 中的 `state` 值表示共享资源的数量。
+
+在 `nonfairTryAcquireShared()` 方法中,会在死循环中不断尝试获取资源,如果 「剩余资源数不足」 或者 「当前线程成功获取资源」 ,就退出死循环。方法返回 **剩余的资源数量** ,根据返回值的不同,分为 3 种情况:
+
+- **剩余资源数量 > 0** :表示成功获取资源,并且后续的线程也可以成功获取资源。
+- **剩余资源数量 = 0** :表示成功获取资源,但是后续的线程无法成功获取资源。
+- **剩余资源数量 < 0** :表示获取资源失败。
+
+#### `doAcquireShared()` 分析
+
+为了方便阅读,这里再贴一下获取资源的入口方法 `acquireShared()` :
+
+```JAVA
+// AQS
+public final void acquireShared(int arg) {
+ if (tryAcquireShared(arg) < 0)
+ doAcquireShared(arg);
+}
+```
+
+在 `acquireShared()` 方法中,会先通过 `tryAcquireShared()` 尝试获取资源。
+
+如果发现方法的返回值 `< 0` ,即剩余的资源数小于 0,则表明当前线程获取资源失败。因此会进入 `doAcquireShared()` 方法,将当前线程加入到 AQS 队列进行等待。如下:
+
+```JAVA
+// AQS
+private void doAcquireShared(int arg) {
+ // 1、将当前线程加入到队列中等待。
+ final Node node = addWaiter(Node.SHARED);
+ boolean failed = true;
+ try {
+ boolean interrupted = false;
+ for (;;) {
+ final Node p = node.predecessor();
+ if (p == head) {
+ // 2、如果当前线程是等待队列的第一个节点,则尝试获取资源。
+ int r = tryAcquireShared(arg);
+ if (r >= 0) {
+ // 3、将当前线程节点移出等待队列,并唤醒后续线程节点。
+ setHeadAndPropagate(node, r);
+ p.next = null; // help GC
+ if (interrupted)
+ selfInterrupt();
+ failed = false;
+ return;
+ }
+ }
+ if (shouldParkAfterFailedAcquire(p, node) &&
+ parkAndCheckInterrupt())
+ interrupted = true;
+ }
+ } finally {
+ // 3、如果获取资源失败,就会取消获取资源,将节点状态更新为 CANCELLED。
+ if (failed)
+ cancelAcquire(node);
+ }
+}
+```
+
+由于当前线程已经尝试获取资源失败了,因此在 `doAcquireShared()` 方法中,需要将当前线程封装为 Node 节点,加入到队列中进行等待。
+
+以 **共享模式** 获取资源和 **独占模式** 获取资源最大的不同之处在于:共享模式下,资源的数量可能会大于 1,即可以多个线程同时持有资源。
+
+因此在共享模式下,当线程线程被唤醒之后,获取到了资源,如果发现还存在剩余资源,就会尝试唤醒后边的线程去尝试获取资源。对应的 `setHeadAndPropagate()` 方法如下:
+
+```JAVA
+// AQS
+private void setHeadAndPropagate(Node node, int propagate) {
+ Node h = head;
+ // 1、将当前线程节点移出等待队列。
+ setHead(node);
+ // 2、唤醒后续等待节点。
+ if (propagate > 0 || h == null || h.waitStatus < 0 ||
+ (h = head) == null || h.waitStatus < 0) {
+ Node s = node.next;
+ if (s == null || s.isShared())
+ doReleaseShared();
+ }
+}
+```
+
+在 `setHeadAndPropagate()` 方法中,唤醒后续节点需要满足一定的条件,主要需要满足 2 个条件:
+
+- `propagate > 0` :`propagate` 代表获取资源之后剩余的资源数量,如果 `> 0` ,则可以唤醒后续线程去获取资源。
+- `h.waitStatus < 0` :这里的 `h` 节点是执行 `setHead()` 之前的 `head` 节点。判断 `head.waitStatus` 时使用 `< 0` ,主要为了确定 `head` 节点的状态为 `SIGNAL` 或 `PROPAGATE` 。如果 `head` 节点为 `SIGNAL` ,则可以唤醒后续节点;如果 `head` 节点状态为 `PROPAGATE` ,也可以唤醒后续节点(这是为了解决并发场景下出现的问题,后续会细讲)。
+
+代码中关于 **唤醒后续等待节点** 的 `if` 判断稍微复杂一些,这里来讲一下为什么这样写:
+
+```JAVA
+if (propagate > 0 || h == null || h.waitStatus < 0 ||
+ (h = head) == null || h.waitStatus < 0)
+```
+
+- `h == null || h.waitStatus < 0` : `h == null` 用于防止空指针异常。正常情况下 h 不会为 `null` ,因为执行到这里之前,当前节点已经加入到队列中了,队列不可能还没有初始化。
+
+ `h.waitStatus < 0` 主要判断 `head` 节点的状态是否为 `SIGNAL` 或者 `PROPAGATE` ,直接使用 `< 0` 来判断比较方便。
+
+- `(h = head) == null || h.waitStatus < 0` :如果到这里说明之前判断的 `h.waitStatus < 0` ,说明存在并发。
+
+ 同时存在其他线程在唤醒后续节点,已经将 `head` 节点的值由 `SIGNAL` 修改为 `0` 了。因此,这里重新获取新的 `head` 节点,这次获取的 `head` 节点为通过 `setHead()` 设置的当前线程节点,之后再次判断 `waitStatus` 状态。
+
+如果 `if` 条件判断通过,就会走到 `doReleaseShared()` 方法唤醒后续等待节点,如下:
+
+```JAVA
+private void doReleaseShared() {
+ for (;;) {
+ Node h = head;
+ // 1、队列中至少需要一个等待的线程节点。
+ if (h != null && h != tail) {
+ int ws = h.waitStatus;
+ // 2、如果 head 节点的状态为 SIGNAL,则可以唤醒后继节点。
+ if (ws == Node.SIGNAL) {
+ // 2.1 清除 head 节点的 SIGNAL 状态,更新为 0。表示已经唤醒该节点的后继节点了。
+ if (!compareAndSetWaitStatus(h, Node.SIGNAL, 0))
+ continue;
+ // 2.2 唤醒后继节点
+ unparkSuccessor(h);
+ }
+ // 3、如果 head 节点的状态为 0,则更新为 PROPAGATE。这是为了解决并发场景下存在的问题,接下来会细讲。
+ else if (ws == 0 &&
+ !compareAndSetWaitStatus(h, 0, Node.PROPAGATE))
+ continue;
+ }
+ if (h == head)
+ break;
+ }
+}
+```
+
+在 `doReleaseShared()` 方法中,会判断 `head` 节点的 `waitStatus` 状态来决定接下来的操作,有两种情况:
+
+- `head` 节点的状态为 `SIGNAL` :表明 `head` 节点存在后继节点需要唤醒,因此通过 `CAS` 操作将 `head` 节点的 `SIGNAL` 状态更新为 `0` 。通过清除 `SIGNAL` 状态来表示已经对 `head` 节点的后继节点进行唤醒操作了。
+- `head` 节点的状态为 `0` :表明存在并发情况,需要将 `0` 修改为 `PROPAGATE` 来保证在并发场景下可以正常唤醒线程。
+
+#### 为什么需要 `PROPAGATE` 状态?
+
+在 `doReleaseShared()` 释放资源时,第 3 步不太容易理解,即如果发现 `head` 节点的状态是 `0` ,就将 `head` 节点的状态由 `0` 更新为 `PROPAGATE` 。
+
+AQS 中,Node 节点的 `PROPAGATE` 就是为了处理并发场景下可能出现的无法唤醒线程节点的问题。`PROPAGATE` 只在 `doReleaseShared()` 方法中用到一次。
+
+**接下来通过案例分析,为什么需要 `PROPAGATE` 状态?**
+
+在共享模式下,线程获取和释放资源的方法调用链如下:
+
+- 线程获取资源的方法调用链为: `acquireShared() -> tryAcquireShared() -> 线程阻塞等待唤醒 -> tryAcquireShared() -> setHeadAndPropagate() -> if (剩余资源数 > 0) || (head.waitStatus < 0) 则唤醒后续节点` 。
+
+- 线程释放资源的方法调用链为: `releaseShared() -> tryReleaseShared() -> doReleaseShared()` 。
+
+**如果在释放资源时,没有将 `head` 节点的状态由 `0` 改为 `PROPAGATE` :**
+
+假设总共有 4 个线程尝试以共享模式获取资源,总共有 2 个资源。初始 `T3` 和 `T4` 线程获取到了资源,`T1` 和 `T2` 线程没有获取到,因此在队列中排队等候。
+
+- 在时刻 1 时,线程 `T1` 和 `T2` 在等待队列中,`T3` 和 `T4` 持有资源。此时等待队列内节点以及对应状态为(括号内为节点的 `waitStatus` 状态):
+
+ `head(-1) -> T1(-1) -> T2(0)` 。
+
+- 在时刻 2 时,线程 `T3` 释放资源,通过 `doReleaseShared()` 方法将 `head` 节点的状态由 `SIGNAL` 更新为 `0` ,并唤醒线程 `T1` ,之后线程 `T3` 退出。
+
+ 线程 `T1` 被唤醒之后,通过 `tryAcquireShared()` 获取到资源,但是此时还未来得及执行 `setHeadAndPropagate()` 将自己设置为 `head` 节点。此时等待队列内节点状态为:
+
+ `head(0) -> T1(-1) -> T2(0)` 。
+
+- 在时刻 3 时,线程 `T4` 释放资源, 由于此时 `head` 节点的状态为 `0` ,因此在 `doReleaseShared()` 方法中无法唤醒 `head` 的后继节点, 之后线程 `T4` 退出。
+
+- 在时刻 4 时,线程 `T1` 继续执行 `setHeadAndPropagate()` 方法将自己设置为 `head` 节点。
+
+ 但是此时由于线程 `T1` 执行 `tryAcquireShared()` 方法返回的剩余资源数为 `0` ,并且 `head` 节点的状态为 `0` ,因此线程 `T1` 并不会在 `setHeadAndPropagate()` 方法中唤醒后续节点。此时等待队列内节点状态为:
+
+ `head(-1,线程 T1 节点) -> T2(0)` 。
+
+此时,就导致线程 `T2` 节点在等待队列中,无法被唤醒。对应时刻表如下:
+
+| 时刻 | 线程 T1 | 线程 T2 | 线程 T3 | 线程 T4 | 等待队列 |
+| ------ | -------------------------------------------------------------- | -------- | ---------------- | ------------------------------------------------------------- | --------------------------------- |
+| 时刻 1 | 等待队列 | 等待队列 | 持有资源 | 持有资源 | `head(-1) -> T1(-1) -> T2(0)` |
+| 时刻 2 | (执行)被唤醒后,获取资源,但未来得及将自己设置为 `head` 节点 | 等待队列 | (执行)释放资源 | 持有资源 | `head(0) -> T1(-1) -> T2(0)` |
+| 时刻 3 | | 等待队列 | 已退出 | (执行)释放资源。但 `head` 节点状态为 `0` ,无法唤醒后继节点 | `head(0) -> T1(-1) -> T2(0)` |
+| 时刻 4 | (执行)将自己设置为 `head` 节点 | 等待队列 | 已退出 | 已退出 | `head(-1,线程 T1 节点) -> T2(0)` |
+
+**如果在线程释放资源时,将 `head` 节点的状态由 `0` 改为 `PROPAGATE` ,则可以解决上边出现的并发问题,如下:**
+
+- 在时刻 1 时,线程 `T1` 和 `T2` 在等待队列中,`T3` 和 `T4` 持有资源。此时等待队列内节点以及对应状态为:
+
+ `head(-1) -> T1(-1) -> T2(0)` 。
+
+- 在时刻 2 时,线程 `T3` 释放资源,通过 `doReleaseShared()` 方法将 `head` 节点的状态由 `SIGNAL` 更新为 `0` ,并唤醒线程 `T1` ,之后线程 `T3` 退出。
+
+ 线程 `T1` 被唤醒之后,通过 `tryAcquireShared()` 获取到资源,但是此时还未来得及执行 `setHeadAndPropagate()` 将自己设置为 `head` 节点。此时等待队列内节点状态为:
+
+ `head(0) -> T1(-1) -> T2(0)` 。
+
+- 在时刻 3 时,线程 `T4` 释放资源, 由于此时 `head` 节点的状态为 `0` ,因此在 `doReleaseShared()` 方法中会将 `head` 节点的状态由 `0` 更新为 `PROPAGATE` , 之后线程 `T4` 退出。此时等待队列内节点状态为:
+
+ `head(PROPAGATE) -> T1(-1) -> T2(0)` 。
+
+- 在时刻 4 时,线程 `T1` 继续执行 `setHeadAndPropagate()` 方法将自己设置为 `head` 节点。此时等待队列内节点状态为:
+
+ `head(-1,线程 T1 节点) -> T2(0)` 。
+
+- 在时刻 5 时,虽然此时由于线程 `T1` 执行 `tryAcquireShared()` 方法返回的剩余资源数为 `0` ,但是 `head` 节点状态为 `PROPAGATE < 0` (这里的 `head` 节点是老的 `head` 节点,而不是刚成为 `head` 节点的线程 `T1` 节点)。
+
+ 因此线程 `T1` 会在 `setHeadAndPropagate()` 方法中唤醒后续 `T2` 节点,并将 `head` 节点的状态由 `SIGNAL` 更新为 `0`。此时等待队列内节点状态为:
+
+ `head(0,线程 T1 节点) -> T2(0)` 。
+
+- 在时刻 6 时,线程 `T2` 被唤醒后,获取到资源,并将自己设置为 `head` 节点。此时等待队列内节点状态为:
+
+ `head(0,线程 T2 节点)` 。
+
+有了 `PROPAGATE` 状态,就可以避免线程 `T2` 无法被唤醒的情况。对应时刻表如下:
+
+| 时刻 | 线程 T1 | 线程 T2 | 线程 T3 | 线程 T4 | 等待队列 |
+| ------ | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ------------------------------------------------------------------ | ---------------- | ------------------------------------------------------------------- | ------------------------------------ |
+| 时刻 1 | 等待队列 | 等待队列 | 持有资源 | 持有资源 | `head(-1) -> T1(-1) -> T2(0)` |
+| 时刻 2 | (执行)被唤醒后,获取资源,但未来得及将自己设置为 `head` 节点 | 等待队列 | (执行)释放资源 | 持有资源 | `head(0) -> T1(-1) -> T2(0)` |
+| 时刻 3 | 未继续向下执行 | 等待队列 | 已退出 | (执行)释放资源。此时会将 `head` 节点状态由 `0` 更新为 `PROPAGATE` | `head(PROPAGATE) -> T1(-1) -> T2(0)` |
+| 时刻 4 | (执行)将自己设置为 `head` 节点 | 等待队列 | 已退出 | 已退出 | `head(-1,线程 T1 节点) -> T2(0)` |
+| 时刻 5 | (执行)由于 `head` 节点状态为 `PROPAGATE < 0` ,因此会在 `setHeadAndPropagate()` 方法中唤醒后续节点,此时将新的 `head` 节点的状态由 `SIGNAL` 更新为 `0` ,并唤醒线程 `T2` | 等待队列 | 已退出 | 已退出 | `head(0,线程 T1 节点) -> T2(0)` |
+| 时刻 6 | 已退出 | (执行)线程 `T2` 被唤醒后,获取到资源,并将自己设置为 `head` 节点 | 已退出 | 已退出 | `head(0,线程 T2 节点)` |
+
+### AQS 资源释放源码分析(共享模式)
+
+AQS 中以共享模式释放资源的入口方法是 `releaseShared()` ,代码如下:
+
+```JAVA
+// AQS
+public final boolean releaseShared(int arg) {
+ if (tryReleaseShared(arg)) {
+ doReleaseShared();
+ return true;
+ }
+ return false;
+}
+```
+
+其中 `tryReleaseShared()` 方法是 AQS 提供的模板方法,这里同样以 `Semaphore` 来讲解,如下:
+
+```JAVA
+// Semaphore
+protected final boolean tryReleaseShared(int releases) {
+ for (;;) {
+ int current = getState();
+ int next = current + releases;
+ if (next < current) // overflow
+ throw new Error("Maximum permit count exceeded");
+ if (compareAndSetState(current, next))
+ return true;
+ }
+}
+```
+
+在 `Semaphore` 实现的 `tryReleaseShared()` 方法中,会在死循环内不断尝试释放资源,即通过 `CAS` 操作来更新 `state` 值。
+
+如果更新成功,则证明资源释放成功,会进入到 `doReleaseShared()` 方法。
+
+`doReleaseShared()` 方法在前文获取资源(共享模式)的部分已进行了详细的源码分析,此处不再重复。
+
## 常见同步工具类
下面介绍几个基于 AQS 的常见同步工具类。
@@ -331,8 +1150,7 @@ semaphore.release(5);// 释放5个许可
[issue645 补充内容](https://github.com/Snailclimb/JavaGuide/issues/645):
-> `Semaphore` 与 `CountDownLatch` 一样,也是共享锁的一种实现。它默认构造 AQS 的 `state` 为 `permits`。当执行任务的线程数量超出 `permits`,那么多余的线程将会被放入等待队列 `Park`,并自旋判断 `state` 是否大于 0。只有当 `state` 大于 0 的时候,阻塞的线程才能继续执行,此时先前执行任务的线程继续执行 `release()` 方法,`release()` 方法使得 state 的变量会加 1,那么自旋的线程便会判断成功。
-> 如此,每次只有最多不超过 `permits` 数量的线程能自旋成功,便限制了执行任务线程的数量。
+> `Semaphore` 基于 AQS 实现,用于控制并发访问的线程数量,但它与共享锁的概念有所不同。`Semaphore` 的构造函数使用 `permits` 参数初始化 AQS 的 `state` 变量,该变量表示可用的许可数量。当线程调用 `acquire()` 方法尝试获取许可时,`state` 会原子性地减 1。如果 `state` 减 1 后大于等于 0,则 `acquire()` 成功返回,线程可以继续执行。如果 `state` 减 1 后小于 0,表示当前并发访问的线程数量已达到 `permits` 的限制,该线程会被放入 AQS 的等待队列并阻塞,**而不是自旋等待**。当其他线程完成任务并调用 `release()` 方法时,`state` 会原子性地加 1。`release()` 操作会唤醒 AQS 等待队列中的一个或多个阻塞线程。这些被唤醒的线程将再次尝试 `acquire()` 操作,竞争获取可用的许可。因此,`Semaphore` 通过控制许可数量来限制并发访问的线程数量,而不是通过自旋和共享锁机制。
### CountDownLatch (倒计时器)
@@ -406,7 +1224,7 @@ protected boolean tryReleaseShared(int releases) {
}
```
-以无参 `await`方法为例,当调用 `await()` 的时候,如果 `state` 不为 0,那就证明任务还没有执行完毕,`await()` 就会一直阻塞,也就是说 `await()` 之后的语句不会被执行(`main` 线程被加入到等待队列也就是 CLH 队列中了)。然后,`CountDownLatch` 会自旋 CAS 判断 `state == 0`,如果 `state == 0` 的话,就会释放所有等待的线程,`await()` 方法之后的语句得到执行。
+以无参 `await`方法为例,当调用 `await()` 的时候,如果 `state` 不为 0,那就证明任务还没有执行完毕,`await()` 就会一直阻塞,也就是说 `await()` 之后的语句不会被执行(`main` 线程被加入到等待队列也就是 变体 CLH 队列中了)。然后,`CountDownLatch` 会自旋 CAS 判断 `state == 0`,如果 `state == 0` 的话,就会释放所有等待的线程,`await()` 方法之后的语句得到执行。
```java
// 等待(也可以叫做加锁)
@@ -511,7 +1329,7 @@ for (int i = 0; i < threadCount-1; i++) {
`CyclicBarrier` 和 `CountDownLatch` 非常类似,它也可以实现线程间的技术等待,但是它的功能比 `CountDownLatch` 更加复杂和强大。主要应用场景和 `CountDownLatch` 类似。
-> `CountDownLatch` 的实现是基于 AQS 的,而 `CycliBarrier` 是基于 `ReentrantLock`(`ReentrantLock` 也属于 AQS 同步器)和 `Condition` 的。
+> `CountDownLatch` 的实现是基于 AQS 的,而 `CyclicBarrier` 是基于 `ReentrantLock`(`ReentrantLock` 也属于 AQS 同步器)和 `Condition` 的。
`CyclicBarrier` 的字面意思是可循环使用(Cyclic)的屏障(Barrier)。它要做的事情是:让一组线程到达一个屏障(也可以叫同步点)时被阻塞,直到最后一个线程到达屏障时,屏障才会开门,所有被屏障拦截的线程才会继续干活。
diff --git a/docs/java/concurrent/atomic-classes.md b/docs/java/concurrent/atomic-classes.md
index 9c118c11f87..2955d1aa33d 100644
--- a/docs/java/concurrent/atomic-classes.md
+++ b/docs/java/concurrent/atomic-classes.md
@@ -1,8 +1,13 @@
---
title: Atomic 原子类总结
+description: Java原子类详解:全面总结JUC包Atomic原子类体系、AtomicInteger/AtomicLong/AtomicReference等常用类、基于CAS的线程安全实现、使用场景与性能优势。
category: Java
tag:
- Java并发
+head:
+ - - meta
+ - name: keywords
+ content: Atomic原子类,AtomicInteger,AtomicLong,AtomicReference,CAS原子操作,JUC并发包,原子类使用
---
## Atomic 原子类介绍
@@ -341,7 +346,7 @@ Final Reference: Daisy, Final Mark: true
- `AtomicLongFieldUpdater`:原子更新长整形字段的更新器
- `AtomicReferenceFieldUpdater`:原子更新引用类型里的字段的更新器
-要想原子地更新对象的属性需要两步。第一步,因为对象的属性修改类型原子类都是抽象类,所以每次使用都必须使用静态方法 newUpdater()创建一个更新器,并且需要设置想要更新的类和属性。第二步,更新的对象属性必须使用 public volatile 修饰符。
+要想原子地更新对象的属性需要两步。第一步,因为对象的属性修改类型原子类都是抽象类,所以每次使用都必须使用静态方法 newUpdater()创建一个更新器,并且需要设置想要更新的类和属性。第二步,更新的对象属性必须使用 volatile int 修饰符。
上面三个类提供的方法几乎相同,所以我们这里以 `AtomicIntegerFieldUpdater`为例子来介绍。
@@ -351,8 +356,8 @@ Final Reference: Daisy, Final Mark: true
// Person 类
class Person {
private String name;
- // 要使用 AtomicIntegerFieldUpdater,字段必须是 public volatile
- private volatile int age;
+ // 要使用 AtomicIntegerFieldUpdater,字段必须是 volatile int
+ volatile int age;
//省略getter/setter和toString
}
diff --git a/docs/java/concurrent/cas.md b/docs/java/concurrent/cas.md
index af97f28d0c8..f7b795791a7 100644
--- a/docs/java/concurrent/cas.md
+++ b/docs/java/concurrent/cas.md
@@ -1,8 +1,13 @@
---
title: CAS 详解
+description: CAS比较并交换深度解析:详解CAS原子操作原理、Unsafe类实现、ABA问题及解决方案、自旋锁机制、与悲观锁性能对比。
category: Java
tag:
- Java并发
+head:
+ - - meta
+ - name: keywords
+ content: CAS,Compare-And-Swap,原子操作,ABA问题,自旋锁,乐观锁,Unsafe,CAS原理
---
乐观锁和悲观锁的介绍以及乐观锁常见实现方式可以阅读笔者写的这篇文章:[乐观锁和悲观锁详解](https://javaguide.cn/java/concurrent/optimistic-lock-and-pessimistic-lock.html)。
diff --git a/docs/java/concurrent/completablefuture-intro.md b/docs/java/concurrent/completablefuture-intro.md
index 6e130c98419..3061298348d 100644
--- a/docs/java/concurrent/completablefuture-intro.md
+++ b/docs/java/concurrent/completablefuture-intro.md
@@ -1,8 +1,13 @@
---
title: CompletableFuture 详解
+description: CompletableFuture异步编程详解:全面讲解CompletableFuture核心API、异步任务编排、thenCompose/thenCombine组合、allOf/anyOf聚合、线程池配置与最佳实践。
category: Java
tag:
- Java并发
+head:
+ - - meta
+ - name: keywords
+ content: CompletableFuture,异步编程,异步编排,Future,thenCompose,thenCombine,allOf,并行任务
---
实际项目中,一个接口可能需要同时获取多种不同的数据,然后再汇总返回,这种场景还是挺常见的。举个例子:用户请求获取订单信息,可能需要同时获取用户信息、商品详情、物流信息、商品推荐等数据。
@@ -65,7 +70,7 @@ public interface Future {
## CompletableFuture 介绍
-`Future` 在实际使用过程中存在一些局限性比如不支持异步任务的编排组合、获取计算结果的 `get()` 方法为阻塞调用。
+`Future` 在实际使用过程中存在一些局限性,比如不支持异步任务的编排组合、获取计算结果的 `get()` 方法为阻塞调用。
Java 8 才被引入`CompletableFuture` 类可以解决`Future` 的这些缺陷。`CompletableFuture` 除了提供了更为好用和强大的 `Future` 特性之外,还提供了函数式编程、异步任务编排组合(可以将多个异步任务串联起来,组成一个完整的链式调用)等能力。
@@ -80,8 +85,6 @@ public class CompletableFuture implements Future, CompletionStage {

-`CompletionStage` 接口描述了一个异步计算的阶段。很多计算可以分成多个阶段或步骤,此时可以通过它将所有步骤组合起来,形成异步计算的流水线。
-
`CompletableFuture` 除了提供了更为好用和强大的 `Future` 特性之外,还提供了函数式编程的能力。

@@ -653,7 +656,15 @@ abc
我们上面的代码示例中,为了方便,都没有选择自定义线程池。实际项目中,这是不可取的。
-`CompletableFuture` 默认使用`ForkJoinPool.commonPool()` 作为执行器,这个线程池是全局共享的,可能会被其他任务占用,导致性能下降或者饥饿。因此,建议使用自定义的线程池来执行 `CompletableFuture` 的异步任务,可以提高并发度和灵活性。
+`CompletableFuture` 默认使用全局共享的 `ForkJoinPool.commonPool()` 作为执行器,所有未指定执行器的异步任务都会使用该线程池。这意味着应用程序、多个库或框架(如 Spring、第三方库)若都依赖 `CompletableFuture`,默认情况下它们都会共享同一个线程池。
+
+虽然 `ForkJoinPool` 效率很高,但当同时提交大量任务时,可能会导致资源竞争和线程饥饿,进而影响系统性能。
+
+为避免这些问题,建议为 `CompletableFuture` 提供自定义线程池,带来以下优势:
+
+- **隔离性**:为不同任务分配独立的线程池,避免全局线程池资源争夺。
+- **资源控制**:根据任务特性调整线程池大小和队列类型,优化性能表现。
+- **异常处理**:通过自定义 `ThreadFactory` 更好地处理线程中的异常情况。
```java
private ThreadPoolExecutor executor = new ThreadPoolExecutor(10, 10,
diff --git "a/docs/java/concurrent/images/java-thread-pool-summary/threadpoolexecutor\346\236\204\351\200\240\345\207\275\346\225\260.png" "b/docs/java/concurrent/images/java-thread-pool-summary/threadpoolexecutor\346\236\204\351\200\240\345\207\275\346\225\260.png"
deleted file mode 100644
index 6e3c7082eed..00000000000
Binary files "a/docs/java/concurrent/images/java-thread-pool-summary/threadpoolexecutor\346\236\204\351\200\240\345\207\275\346\225\260.png" and /dev/null differ
diff --git a/docs/java/concurrent/java-concurrent-collections.md b/docs/java/concurrent/java-concurrent-collections.md
index 9a669d900e0..e1c05dbfab5 100644
--- a/docs/java/concurrent/java-concurrent-collections.md
+++ b/docs/java/concurrent/java-concurrent-collections.md
@@ -1,8 +1,13 @@
---
title: Java 常见并发容器总结
+description: Java并发容器全面总结:详解ConcurrentHashMap/CopyOnWriteArrayList/BlockingQueue等JUC线程安全容器特性、适用场景与性能对比。
category: Java
tag:
- Java并发
+head:
+ - - meta
+ - name: keywords
+ content: Java并发容器,ConcurrentHashMap,CopyOnWriteArrayList,BlockingQueue,ConcurrentLinkedQueue,线程安全容器
---
JDK 提供的这些容器大部分在 `java.util.concurrent` 包中。
@@ -15,13 +20,19 @@ JDK 提供的这些容器大部分在 `java.util.concurrent` 包中。
## ConcurrentHashMap
-我们知道 `HashMap` 不是线程安全的,在并发场景下如果要保证一种可行的方式是使用 `Collections.synchronizedMap()` 方法来包装我们的 `HashMap`。但这是通过使用一个全局的锁来同步不同线程间的并发访问,因此会带来不可忽视的性能问题。
+我们知道,`HashMap` 是线程不安全的,如果在并发场景下使用,一种常见的解决方式是通过 `Collections.synchronizedMap()` 方法对 `HashMap` 进行包装,使其变为线程安全。不过,这种方式是通过一个全局锁来同步不同线程间的并发访问,会导致严重的性能瓶颈,尤其是在高并发场景下。
-所以就有了 `HashMap` 的线程安全版本—— `ConcurrentHashMap` 的诞生。
+为了解决这一问题,`ConcurrentHashMap` 应运而生,作为 `HashMap` 的线程安全版本,它提供了更高效的并发处理能力。
在 JDK1.7 的时候,`ConcurrentHashMap` 对整个桶数组进行了分割分段(`Segment`,分段锁),每一把锁只锁容器其中一部分数据(下面有示意图),多线程访问容器里不同数据段的数据,就不会存在锁竞争,提高并发访问率。
-到了 JDK1.8 的时候,`ConcurrentHashMap` 已经摒弃了 `Segment` 的概念,而是直接用 `Node` 数组+链表+红黑树的数据结构来实现,并发控制使用 `synchronized` 和 CAS 来操作。(JDK1.6 以后 `synchronized` 锁做了很多优化) 整个看起来就像是优化过且线程安全的 `HashMap`,虽然在 JDK1.8 中还能看到 `Segment` 的数据结构,但是已经简化了属性,只是为了兼容旧版本。
+
+
+到了 JDK1.8 的时候,`ConcurrentHashMap` 取消了 `Segment` 分段锁,采用 `Node + CAS + synchronized` 来保证并发安全。数据结构跟 `HashMap` 1.8 的结构类似,数组+链表/红黑二叉树。Java 8 在链表长度超过一定阈值(8)时将链表(寻址时间复杂度为 O(N))转换为红黑树(寻址时间复杂度为 O(log(N)))。
+
+Java 8 中,锁粒度更细,`synchronized` 只锁定当前链表或红黑二叉树的首节点,这样只要 hash 不冲突,就不会产生并发,就不会影响其他 Node 的读写,效率大幅提升。
+
+
关于 `ConcurrentHashMap` 的详细介绍,请看我写的这篇文章:[`ConcurrentHashMap` 源码分析](./../collection/concurrent-hash-map-source-code.md)。
@@ -136,7 +147,7 @@ private static ArrayBlockingQueue blockingQueue = new ArrayBlockingQueu
最低层的链表维护了跳表内所有的元素,每上面一层链表都是下面一层的子集。
-跳表内的所有链表的元素都是排序的。查找时,可以从顶级链表开始找。一旦发现被查找的元素大于当前链表中的取值,就会转入下一层链表继续找。这也就是说在查找过程中,搜索是跳跃式的。如上图所示,在跳表中查找元素 18。
+跳表内的所有链表的元素都是排序的。查找时,可以从顶级链表开始找。一旦发现被查找的元素小于当前访问节点的后继节点(或后继节点为空),就会转入下一层链表继续找。这也就是说在查找过程中,搜索是跳跃式的。如上图所示,在跳表中查找元素 18。

diff --git a/docs/java/concurrent/java-concurrent-questions-01.md b/docs/java/concurrent/java-concurrent-questions-01.md
index 173b7a12ed9..1f8672db28a 100644
--- a/docs/java/concurrent/java-concurrent-questions-01.md
+++ b/docs/java/concurrent/java-concurrent-questions-01.md
@@ -1,22 +1,20 @@
---
title: Java并发常见面试题总结(上)
+description: Java并发编程基础面试题:深入讲解线程与进程区别、多线程创建方式、线程生命周期状态、死锁四个条件及预防、并发与并行概念等核心知识。
category: Java
tag:
- Java并发
head:
- - meta
- name: keywords
- content: 线程和进程,并发和并行,多线程,死锁,线程的生命周期
- - - meta
- - name: description
- content: Java并发常见知识点和面试题总结(含详细解答),希望对你有帮助!
+ content: Java并发,线程与进程,多线程,死锁,线程生命周期,并发编程,Java面试题,线程创建方式
---
## 线程
-### 什么是线程和进程?
+### ⭐️什么是线程和进程?
#### 何为进程?
@@ -84,7 +82,7 @@ JDK 1.2 之前,Java 线程是基于绿色线程(Green Threads)实现的,
在 Windows 和 Linux 等主流操作系统中,Java 线程采用的是一对一的线程模型,也就是一个 Java 线程对应一个系统内核线程。Solaris 系统是一个特例(Solaris 系统本身就支持多对多的线程模型),HotSpot VM 在 Solaris 上支持多对多和一对一。具体可以参考 R 大的回答: [JVM 中的线程模型是用户级的么?](https://www.zhihu.com/question/23096638/answer/29617153)。
-### 请简要描述线程与进程的关系,区别及优缺点?
+### ⭐️请简要描述线程与进程的关系,区别及优缺点?
下图是 Java 内存区域,通过下图我们从 JVM 的角度来说一下线程和进程之间的关系。
@@ -92,7 +90,7 @@ JDK 1.2 之前,Java 线程是基于绿色线程(Green Threads)实现的,
从上图可以看出:一个进程中可以有多个线程,多个线程共享进程的**堆**和**方法区 (JDK1.8 之后的元空间)**资源,但是每个线程有自己的**程序计数器**、**虚拟机栈** 和 **本地方法栈**。
-**总结:** **线程是进程划分成的更小的运行单位。线程和进程最大的不同在于基本上各进程是独立的,而各线程则不一定,因为同一进程中的线程极有可能会相互影响。线程执行开销小,但不利于资源的管理和保护;而进程正相反。**
+**总结:** 线程是进程划分成的更小的运行单位。线程和进程最大的不同在于基本上各进程是独立的,而各线程则不一定,因为同一进程中的线程极有可能会相互影响。线程执行开销小,但不利于资源的管理和保护;而进程正相反。
下面是该知识点的扩展内容!
@@ -128,9 +126,7 @@ JDK 1.2 之前,Java 线程是基于绿色线程(Green Threads)实现的,
严格来说,Java 就只有一种方式可以创建线程,那就是通过`new Thread().start()`创建。不管是哪种方式,最终还是依赖于`new Thread().start()`。
-关于这个问题的详细分析可以查看这篇文章:[大家都说 Java 有三种创建线程的方式!并发编程中的惊天骗局!](https://mp.weixin.qq.com/s/NspUsyhEmKnJ-4OprRFp9g)。
-
-### 说说线程的生命周期和状态?
+### ⭐️说说线程的生命周期和状态?
Java 线程在运行的生命周期中的指定时刻只可能处于下面 6 种不同状态的其中一个状态:
@@ -143,7 +139,7 @@ Java 线程在运行的生命周期中的指定时刻只可能处于下面 6 种
线程在生命周期中并不是固定处于某一个状态而是随着代码的执行在不同状态之间切换。
-Java 线程状态变迁图(图源:[挑错 |《Java 并发编程的艺术》中关于线程状态的三处错误](https://mp.weixin.qq.com/s/UOrXql_LhOD8dhTq_EPI0w)):
+Java 线程状态变迁图(图源:[挑错 |《Java 并发编程的艺术》中关于线程状态的三处错误](https://mp.weixin.qq.com/s/0UTyrJpRKaKhkhHcQtXAiA)):

@@ -160,8 +156,6 @@ Java 线程状态变迁图(图源:[挑错 |《Java 并发编程的艺术》中
- 当线程进入 `synchronized` 方法/块或者调用 `wait` 后(被 `notify`)重新进入 `synchronized` 方法/块,但是锁被其它线程占有,这个时候线程就会进入 **BLOCKED(阻塞)** 状态。
- 线程在执行完了 `run()`方法之后将会进入到 **TERMINATED(终止)** 状态。
-相关阅读:[线程的几种状态你真的了解么?](https://mp.weixin.qq.com/s/R5MrTsWvk9McFSQ7bS0W2w) 。
-
### 什么是线程上下文切换?
线程在执行过程中会有自己的运行条件和状态(也称上下文),比如上文所说到过的程序计数器,栈信息等。当出现如下情况的时候,线程会从占用 CPU 状态中退出。
@@ -198,7 +192,7 @@ Java 线程状态变迁图(图源:[挑错 |《Java 并发编程的艺术》中
这是另一个非常经典的 Java 多线程面试问题,而且在面试中会经常被问到。很简单,但是很多人都会答不上来!
-new 一个 `Thread`,线程进入了新建状态。调用 `start()`方法,会启动一个线程并使线程进入了就绪状态,当分配到时间片后就可以开始运行了。 `start()` 会执行线程的相应准备工作,然后自动执行 `run()` 方法的内容,这是真正的多线程工作。 但是,直接执行 `run()` 方法,会把 `run()` 方法当成一个 main 线程下的普通方法去执行,并不会在某个线程中执行它,所以这并不是多线程工作。
+new 一个 `Thread`,线程进入了新建状态。调用 `start()`方法,会启动一个线程并使线程进入了就绪状态,当分配到时间片后就可以开始运行了。 `start()` 会执行线程的相应准备工作,然后自动执行 `run()` 方法的内容,这是真正的多线程工作。 但是,直接执行 `run()` 方法,会把 `run()` 方法当成一个普通方法在调用该方法的线程去执行,所以这并不是多线程工作。
**总结:调用 `start()` 方法方可启动线程并使线程进入就绪状态,直接执行 `run()` 方法的话不会以多线程的方式执行。**
@@ -216,11 +210,11 @@ new 一个 `Thread`,线程进入了新建状态。调用 `start()`方法,会
- **同步**:发出一个调用之后,在没有得到结果之前, 该调用就不可以返回,一直等待。
- **异步**:调用在发出之后,不用等待返回结果,该调用直接返回。
-### 为什么要使用多线程?
+### ⭐️为什么要使用多线程?
先从总体上来说:
-- **从计算机底层来说:** 线程可以比作是轻量级的进程,是程序执行的最小单位,线程间的切换和调度的成本远远小于进程。另外,多核 CPU 时代意味着多个线程可以同时运行,这减少了线程上下文切换的开销。
+- **从计算机底层来说:** 线程可以比作是轻量级的进程,是程序执行的最小单位,线程间的切换和调度的成本远远小于进程。另外,多核 CPU 时代意味着多个线程可以同时运行,这减少了线程上下文切换的开销。
- **从当代互联网发展趋势来说:** 现在的系统动不动就要求百万级甚至千万级的并发量,而多线程并发编程正是开发高并发系统的基础,利用好多线程机制可以大大提高系统整体的并发能力以及性能。
再深入到计算机底层来探讨:
@@ -228,7 +222,7 @@ new 一个 `Thread`,线程进入了新建状态。调用 `start()`方法,会
- **单核时代**:在单核时代多线程主要是为了提高单进程利用 CPU 和 IO 系统的效率。 假设只运行了一个 Java 进程的情况,当我们请求 IO 的时候,如果 Java 进程中只有一个线程,此线程被 IO 阻塞则整个进程被阻塞。CPU 和 IO 设备只有一个在运行,那么可以简单地说系统整体效率只有 50%。当使用多线程的时候,一个线程被 IO 阻塞,其他线程还可以继续使用 CPU。从而提高了 Java 进程利用系统资源的整体效率。
- **多核时代**: 多核时代多线程主要是为了提高进程利用多核 CPU 的能力。举个例子:假如我们要计算一个复杂的任务,我们只用一个线程的话,不论系统有几个 CPU 核心,都只会有一个 CPU 核心被利用到。而创建多个线程,这些线程可以被映射到底层多个 CPU 核心上执行,在任务中的多个线程没有资源竞争的情况下,任务执行的效率会有显著性的提高,约等于(单核时执行时间/CPU 核心数)。
-### 单核 CPU 支持 Java 多线程吗?
+### ⭐️单核 CPU 支持 Java 多线程吗?
单核 CPU 是支持 Java 多线程的。操作系统通过时间片轮转的方式,将 CPU 的时间分配给不同的线程。尽管单核 CPU 一次只能执行一个任务,但通过快速在多个线程之间切换,可以让用户感觉多个任务是同时进行的。
@@ -241,7 +235,7 @@ new 一个 `Thread`,线程进入了新建状态。调用 `start()`方法,会
Java 使用的线程调度是抢占式的。也就是说,JVM 本身不负责线程的调度,而是将线程的调度委托给操作系统。操作系统通常会基于线程优先级和时间片来调度线程的执行,高优先级的线程通常获得 CPU 时间片的机会更多。
-### 单核 CPU 上运行多个线程效率一定会高吗?
+### ⭐️单核 CPU 上运行多个线程效率一定会高吗?
单核 CPU 同时运行多个线程的效率是否会高,取决于线程的类型和任务的性质。一般来说,有两种类型的线程:
@@ -263,9 +257,9 @@ Java 使用的线程调度是抢占式的。也就是说,JVM 本身不负责
- 线程安全指的是在多线程环境下,对于同一份数据,不管有多少个线程同时访问,都能保证这份数据的正确性和一致性。
- 线程不安全则表示在多线程环境下,对于同一份数据,多个线程同时访问时可能会导致数据混乱、错误或者丢失。
-## 死锁
+## ⭐️死锁
-### 什么是线程死锁?
+### 什么是线程死锁?
线程死锁描述的是这样一种情况:多个线程同时被阻塞,它们中的一个或者全部都在等待某个资源被释放。由于线程被无限期地阻塞,因此程序不可能正常终止。
@@ -323,14 +317,14 @@ Thread[线程 1,5,main]waiting get resource2
Thread[线程 2,5,main]waiting get resource1
```
-线程 A 通过 `synchronized (resource1)` 获得 `resource1` 的监视器锁,然后通过`Thread.sleep(1000);`让线程 A 休眠 1s 为的是让线程 B 得到执行然后获取到 resource2 的监视器锁。线程 A 和线程 B 休眠结束了都开始企图请求获取对方的资源,然后这两个线程就会陷入互相等待的状态,这也就产生了死锁。
+线程 A 通过 `synchronized (resource1)` 获得 `resource1` 的监视器锁,然后通过 `Thread.sleep(1000);` 让线程 A 休眠 1s,为的是让线程 B 得到执行然后获取到 resource2 的监视器锁。线程 A 和线程 B 休眠结束了都开始企图请求获取对方的资源,然后这两个线程就会陷入互相等待的状态,这也就产生了死锁。
上面的例子符合产生死锁的四个必要条件:
-1. 互斥条件:该资源任意一个时刻只由一个线程占用。
-2. 请求与保持条件:一个线程因请求资源而阻塞时,对已获得的资源保持不放。
-3. 不剥夺条件:线程已获得的资源在未使用完之前不能被其他线程强行剥夺,只有自己使用完毕后才释放资源。
-4. 循环等待条件:若干线程之间形成一种头尾相接的循环等待资源关系。
+1. **互斥条件**:该资源任意一个时刻只由一个线程占用。
+2. **请求与保持条件**:一个线程因请求资源而阻塞时,对已获得的资源保持不放。
+3. **不剥夺条件**:线程已获得的资源在未使用完之前不能被其他线程强行剥夺,只有自己使用完毕后才释放资源。
+4. **循环等待条件**:若干线程之间形成一种头尾相接的循环等待资源关系。
### 如何检测死锁?
@@ -403,16 +397,6 @@ Process finished with exit code 0
我们分析一下上面的代码为什么避免了死锁的发生?
-线程 1 首先获得到 resource1 的监视器锁,这时候线程 2 就获取不到了。然后线程 1 再去获取 resource2 的监视器锁,可以获取到。然后线程 1 释放了对 resource1、resource2 的监视器锁的占用,线程 2 获取到就可以执行了。这样就破坏了破坏循环等待条件,因此避免了死锁。
-
-## 虚拟线程
-
-虚拟线程在 Java 21 正式发布,这是一项重量级的更新。我写了一篇文章来总结虚拟线程常见的问题:[虚拟线程常见问题总结](./virtual-thread.md),包含下面这些问题:
-
-1. 什么是虚拟线程?
-2. 虚拟线程和平台线程有什么关系?
-3. 虚拟线程有什么优点和缺点?
-4. 如何创建虚拟线程?
-5. 虚拟线程的底层原理是什么?
+线程 1 首先获得到 resource1 的监视器锁,这时候线程 2 就获取不到了。然后线程 1 再去获取 resource2 的监视器锁,可以获取到。然后线程 1 释放了对 resource1、resource2 的监视器锁的占用,线程 2 获取到就可以执行了。这样就破坏了循环等待条件,因此避免了死锁。
diff --git a/docs/java/concurrent/java-concurrent-questions-02.md b/docs/java/concurrent/java-concurrent-questions-02.md
index b5edec98e98..8967e9cad3c 100644
--- a/docs/java/concurrent/java-concurrent-questions-02.md
+++ b/docs/java/concurrent/java-concurrent-questions-02.md
@@ -1,24 +1,22 @@
---
title: Java并发常见面试题总结(中)
+description: Java并发进阶面试题:深入解析synchronized与ReentrantLock区别、volatile可见性保证、JMM内存模型、happens-before原则等并发编程核心机制。
category: Java
tag:
- Java并发
head:
- - meta
- name: keywords
- content: 多线程,死锁,synchronized,ReentrantLock,volatile,ThreadLocal,线程池,CAS,AQS
- - - meta
- - name: description
- content: Java并发常见知识点和面试题总结(含详细解答)。
+ content: synchronized,ReentrantLock,volatile,JMM,happens-before,可见性,原子性,有序性,并发面试题
---
-## JMM(Java 内存模型)
+## ⭐️JMM(Java 内存模型)
-JMM(Java 内存模型)相关的问题比较多,也比较重要,于是我单独抽了一篇文章来总结 JMM 相关的知识点和问题:[JMM(Java 内存模型)详解](./jmm.md) 。
+JMM(Java 内存模型)相关的问题比较多,也比较重要,于是我单独抽了一篇文章来总结 JMM 相关的知识点和问题:[JMM(Java 内存模型)详解](https://javaguide.cn/java/concurrent/jmm.html) 。
-## volatile 关键字
+## ⭐️volatile 关键字
### 如何保证变量的可见性?
@@ -60,7 +58,7 @@ public class Singleton {
private Singleton() {
}
- public static Singleton getUniqueInstance() {
+ public static Singleton getUniqueInstance() {
//先判断对象是否已经实例过,没有实例化过才进入加锁代码
if (uniqueInstance == null) {
//类对象加锁
@@ -174,7 +172,7 @@ public void increase() {
}
```
-## 乐观锁和悲观锁
+## ⭐️乐观锁和悲观锁
### 什么是悲观锁?
@@ -423,8 +421,6 @@ CAS 经常会用到自旋操作来进行重试,也就是不成功就一直循
#### 只能保证一个共享变量的原子操作
-CAS 只对单个共享变量有效,当操作涉及跨多个共享变量时 CAS 无效。但是从 JDK 1.5 开始,提供了`AtomicReference`类来保证引用对象之间的原子性,你可以把多个变量放在一个对象里来进行 CAS 操作.所以我们可以使用锁或者利用`AtomicReference`类把多个共享变量合并成一个共享变量来操作。
-
CAS 操作仅能对单个共享变量有效。当需要操作多个共享变量时,CAS 就显得无能为力。不过,从 JDK 1.5 开始,Java 提供了`AtomicReference`类,这使得我们能够保证引用对象之间的原子性。通过将多个变量封装在一个对象中,我们可以使用`AtomicReference`来执行 CAS 操作。
除了 `AtomicReference` 这种方式之外,还可以利用加锁来保证。
@@ -477,8 +473,8 @@ synchronized static void method() {
对括号里指定的对象/类加锁:
-- `synchronized(object)` 表示进入同步代码库前要获得 **给定对象的锁**。
-- `synchronized(类.class)` 表示进入同步代码前要获得 **给定 Class 的锁**
+- `synchronized(object)` 表示进入同步代码块前要获得 **给定对象的锁**。
+- `synchronized(类.class)` 表示进入同步代码块前要获得 **给定 Class 的锁**
```java
synchronized(this) {
@@ -498,7 +494,7 @@ synchronized(this) {
另外,构造方法本身是线程安全的,但如果在构造方法中涉及到共享资源的操作,就需要采取适当的同步措施来保证整个构造过程的线程安全。
-### synchronized 底层原理了解吗?
+### ⭐️synchronized 底层原理了解吗?
synchronized 关键字底层原理属于 JVM 层面的东西。
@@ -532,13 +528,13 @@ public class SynchronizedDemo {

-对象锁的的拥有者线程才可以执行 `monitorexit` 指令来释放锁。在执行 `monitorexit` 指令后,将锁计数器设为 0,表明锁被释放,其他线程可以尝试获取锁。
+对象锁的拥有者线程才可以执行 `monitorexit` 指令来释放锁。在执行 `monitorexit` 指令后,将锁计数器设为 0,表明锁被释放,其他线程可以尝试获取锁。

如果获取对象锁失败,那当前线程就要阻塞等待,直到锁被另外一个线程释放为止。
-#### synchronized 修饰方法的的情况
+#### synchronized 修饰方法的情况
```java
public class SynchronizedDemo2 {
@@ -551,7 +547,7 @@ public class SynchronizedDemo2 {

-`synchronized` 修饰的方法并没有 `monitorenter` 指令和 `monitorexit` 指令,取得代之的确实是 `ACC_SYNCHRONIZED` 标识,该标识指明了该方法是一个同步方法。JVM 通过该 `ACC_SYNCHRONIZED` 访问标志来辨别一个方法是否声明为同步方法,从而执行相应的同步调用。
+`synchronized` 修饰的方法并没有 `monitorenter` 指令和 `monitorexit` 指令,取而代之的是 `ACC_SYNCHRONIZED` 标识,该标识指明了该方法是一个同步方法。JVM 通过该 `ACC_SYNCHRONIZED` 访问标志来辨别一个方法是否声明为同步方法,从而执行相应的同步调用。
如果是实例方法,JVM 会尝试获取实例对象的锁。如果是静态方法,JVM 会尝试获取当前 class 的锁。
@@ -559,9 +555,9 @@ public class SynchronizedDemo2 {
`synchronized` 同步语句块的实现使用的是 `monitorenter` 和 `monitorexit` 指令,其中 `monitorenter` 指令指向同步代码块的开始位置,`monitorexit` 指令则指明同步代码块的结束位置。
-`synchronized` 修饰的方法并没有 `monitorenter` 指令和 `monitorexit` 指令,取得代之的确实是 `ACC_SYNCHRONIZED` 标识,该标识指明了该方法是一个同步方法。
+`synchronized` 修饰的方法并没有 `monitorenter` 指令和 `monitorexit` 指令,取而代之的是 `ACC_SYNCHRONIZED` 标识,该标识指明了该方法是一个同步方法。
-**不过两者的本质都是对对象监视器 monitor 的获取。**
+**不过,两者的本质都是对对象监视器 monitor 的获取。**
相关推荐:[Java 锁与线程的那些事 - 有赞技术团队](https://tech.youzan.com/javasuo-yu-xian-cheng-de-na-xie-shi/) 。
@@ -575,7 +571,31 @@ public class SynchronizedDemo2 {
`synchronized` 锁升级是一个比较复杂的过程,面试也很少问到,如果你想要详细了解的话,可以看看这篇文章:[浅析 synchronized 锁升级的原理与实现](https://www.cnblogs.com/star95/p/17542850.html)。
-### synchronized 和 volatile 有什么区别?
+### synchronized 的偏向锁为什么被废弃了?
+
+Open JDK 官方声明:[JEP 374: Deprecate and Disable Biased Locking](https://openjdk.org/jeps/374)
+
+在 JDK15 中,偏向锁被默认关闭(仍然可以使用 `-XX:+UseBiasedLocking` 启用偏向锁),在 JDK18 中,偏向锁已经被彻底废弃(无法通过命令行打开)。
+
+在官方声明中,主要原因有两个方面:
+
+- **性能收益不明显:**
+
+偏向锁是 HotSpot 虚拟机的一项优化技术,可以提升单线程对同步代码块的访问性能。
+
+受益于偏向锁的应用程序通常使用了早期的 Java 集合 API,例如 HashTable、Vector,在这些集合类中通过 synchronized 来控制同步,这样在单线程频繁访问时,通过偏向锁会减少同步开销。
+
+随着 JDK 的发展,出现了 ConcurrentHashMap 高性能的集合类,在集合类内部进行了许多性能优化,此时偏向锁带来的性能收益就不明显了。
+
+偏向锁仅仅在单线程访问同步代码块的场景中可以获得性能收益。
+
+如果存在多线程竞争,就需要 **撤销偏向锁** ,这个操作的性能开销是比较昂贵的。偏向锁的撤销需要等待进入到全局安全点(safe point),该状态下所有线程都是暂停的,此时去检查线程状态并进行偏向锁的撤销。
+
+- **JVM 内部代码维护成本太高:**
+
+偏向锁将许多复杂代码引入到同步子系统,并且对其他的 HotSpot 组件也具有侵入性。这种复杂性为理解代码、系统重构带来了困难,因此, OpenJDK 官方希望禁用、废弃并删除偏向锁。
+
+### ⭐️synchronized 和 volatile 有什么区别?
`synchronized` 关键字和 `volatile` 关键字是两个互补的存在,而不是对立的存在!
@@ -613,7 +633,7 @@ public ReentrantLock(boolean fair) {
- **公平锁** : 锁被释放之后,先申请的线程先得到锁。性能较差一些,因为公平锁为了保证时间上的绝对顺序,上下文切换更频繁。
- **非公平锁**:锁被释放之后,后申请的线程可能会先获取到锁,是随机或者按照其他优先级排序的。性能更好,但可能会导致某些线程永远无法获取到锁。
-### synchronized 和 ReentrantLock 有什么区别?
+### ⭐️synchronized 和 ReentrantLock 有什么区别?
#### 两者都是可重入锁
@@ -642,15 +662,16 @@ public class SynchronizedDemo {
`synchronized` 是依赖于 JVM 实现的,前面我们也讲到了 虚拟机团队在 JDK1.6 为 `synchronized` 关键字进行了很多优化,但是这些优化都是在虚拟机层面实现的,并没有直接暴露给我们。
-`ReentrantLock` 是 JDK 层面实现的(也就是 API 层面,需要 lock() 和 unlock() 方法配合 try/finally 语句块来完成),所以我们可以通过查看它的源代码,来看它是如何实现的。
+`ReentrantLock` 是 JDK 层面实现的(也就是 API 层面,需要 `lock()` 和 `unlock()` 方法配合 `try/finally` 语句块来完成),所以我们可以通过查看它的源代码,来看它是如何实现的。
#### ReentrantLock 比 synchronized 增加了一些高级功能
相比`synchronized`,`ReentrantLock`增加了一些高级功能。主要来说主要有三点:
-- **等待可中断** : `ReentrantLock`提供了一种能够中断等待锁的线程的机制,通过 `lock.lockInterruptibly()` 来实现这个机制。也就是说正在等待的线程可以选择放弃等待,改为处理其他事情。
+- **等待可中断** : `ReentrantLock`提供了一种能够中断等待锁的线程的机制,通过 `lock.lockInterruptibly()` 来实现这个机制。也就是说当前线程在等待获取锁的过程中,如果其他线程中断当前线程「 `interrupt()` 」,当前线程就会抛出 `InterruptedException` 异常,可以捕捉该异常进行相应处理。
- **可实现公平锁** : `ReentrantLock`可以指定是公平锁还是非公平锁。而`synchronized`只能是非公平锁。所谓的公平锁就是先等待的线程先获得锁。`ReentrantLock`默认情况是非公平的,可以通过 `ReentrantLock`类的`ReentrantLock(boolean fair)`构造方法来指定是否是公平的。
-- **可实现选择性通知(锁可以绑定多个条件)**: `synchronized`关键字与`wait()`和`notify()`/`notifyAll()`方法相结合可以实现等待/通知机制。`ReentrantLock`类当然也可以实现,但是需要借助于`Condition`接口与`newCondition()`方法。
+- **通知机制更强大**:`ReentrantLock` 通过绑定多个 `Condition` 对象,可以实现分组唤醒和选择性通知。这解决了 `synchronized` 只能随机唤醒或全部唤醒的效率问题,为复杂的线程协作场景提供了强大的支持。
+- **支持超时** :`ReentrantLock` 提供了 `tryLock(timeout)` 的方法,可以指定等待获取锁的最长等待时间,如果超过了等待时间,就会获取锁失败,不会一直等待。
如果你想使用上述功能,那么选择 `ReentrantLock` 是一个不错的选择。
@@ -658,10 +679,95 @@ public class SynchronizedDemo {
> `Condition`是 JDK1.5 之后才有的,它具有很好的灵活性,比如可以实现多路通知功能也就是在一个`Lock`对象中可以创建多个`Condition`实例(即对象监视器),**线程对象可以注册在指定的`Condition`中,从而可以有选择性的进行线程通知,在调度线程上更加灵活。 在使用`notify()/notifyAll()`方法进行通知时,被通知的线程是由 JVM 选择的,用`ReentrantLock`类结合`Condition`实例可以实现“选择性通知”** ,这个功能非常重要,而且是 `Condition` 接口默认提供的。而`synchronized`关键字就相当于整个 `Lock` 对象中只有一个`Condition`实例,所有的线程都注册在它一个身上。如果执行`notifyAll()`方法的话就会通知所有处于等待状态的线程,这样会造成很大的效率问题。而`Condition`实例的`signalAll()`方法,只会唤醒注册在该`Condition`实例中的所有等待线程。
+关于 **等待可中断** 的补充:
+
+> `lockInterruptibly()` 会让获取锁的线程在阻塞等待的过程中可以响应中断,即当前线程在获取锁的时候,发现锁被其他线程持有,就会阻塞等待。
+>
+> 在阻塞等待的过程中,如果其他线程中断当前线程 `interrupt()` ,就会抛出 `InterruptedException` 异常,可以捕获该异常,做一些处理操作。
+>
+> 为了更好理解这个方法,借用 Stack Overflow 上的一个案例,可以更好地理解 `lockInterruptibly()` 可以响应中断:
+>
+> ```JAVA
+> public class MyRentrantlock {
+> Thread t = new Thread() {
+> @Override
+> public void run() {
+> ReentrantLock r = new ReentrantLock();
+> // 1.1、第一次尝试获取锁,可以获取成功
+> r.lock();
+>
+> // 1.2、此时锁的重入次数为 1
+> System.out.println("lock() : lock count :" + r.getHoldCount());
+>
+> // 2、中断当前线程,通过 Thread.currentThread().isInterrupted() 可以看到当前线程的中断状态为 true
+> interrupt();
+> System.out.println("Current thread is intrupted");
+>
+> // 3.1、尝试获取锁,可以成功获取
+> r.tryLock();
+> // 3.2、此时锁的重入次数为 2
+> System.out.println("tryLock() on intrupted thread lock count :" + r.getHoldCount());
+> try {
+> // 4、打印线程的中断状态为 true,那么调用 lockInterruptibly() 方法就会抛出 InterruptedException 异常
+> System.out.println("Current Thread isInterrupted:" + Thread.currentThread().isInterrupted());
+> r.lockInterruptibly();
+> System.out.println("lockInterruptibly() --NOt executable statement" + r.getHoldCount());
+> } catch (InterruptedException e) {
+> r.lock();
+> System.out.println("Error");
+> } finally {
+> r.unlock();
+> }
+>
+> // 5、打印锁的重入次数,可以发现 lockInterruptibly() 方法并没有成功获取到锁
+> System.out.println("lockInterruptibly() not able to Acqurie lock: lock count :" + r.getHoldCount());
+>
+> r.unlock();
+> System.out.println("lock count :" + r.getHoldCount());
+> r.unlock();
+> System.out.println("lock count :" + r.getHoldCount());
+> }
+> };
+> public static void main(String str[]) {
+> MyRentrantlock m = new MyRentrantlock();
+> m.t.start();
+> }
+> }
+> ```
+>
+> 输出:
+>
+> ```BASH
+> lock() : lock count :1
+> Current thread is intrupted
+> tryLock() on intrupted thread lock count :2
+> Current Thread isInterrupted:true
+> Error
+> lockInterruptibly() not able to Acqurie lock: lock count :2
+> lock count :1
+> lock count :0
+> ```
+
+关于 **支持超时** 的补充:
+
+> **为什么需要 `tryLock(timeout)` 这个功能呢?**
+>
+> `tryLock(timeout)` 方法尝试在指定的超时时间内获取锁。如果成功获取锁,则返回 `true`;如果在锁可用之前超时,则返回 `false`。此功能在以下几种场景中非常有用:
+>
+> - **防止死锁:** 在复杂的锁场景中,`tryLock(timeout)` 可以通过允许线程在合理的时间内放弃并重试来帮助防止死锁。
+> - **提高响应速度:** 防止线程无限期阻塞。
+> - **处理时间敏感的操作:** 对于具有严格时间限制的操作,`tryLock(timeout)` 允许线程在无法及时获取锁时继续执行替代操作。
+
### 可中断锁和不可中断锁有什么区别?
-- **可中断锁**:获取锁的过程中可以被中断,不需要一直等到获取锁之后 才能进行其他逻辑处理。`ReentrantLock` 就属于是可中断锁。
-- **不可中断锁**:一旦线程申请了锁,就只能等到拿到锁以后才能进行其他的逻辑处理。 `synchronized` 就属于是不可中断锁。
+它们的区别在于:**线程在获取锁的过程中被阻塞时,是否能够因为中断而提前放弃等待。**
+
+- **不可中断锁**:线程在等待锁期间即使收到中断信号,也不会退出阻塞状态,而是一直等待直到获得锁。中断状态会被保留,但不会影响锁的获取过程。
+ - `synchronized` 属于典型的不可中断锁。
+ - `ReentrantLock#lock()` 也是不可中断的。
+- **可中断锁**:线程在等待锁的过程中如果收到中断信号,会立即停止等待并抛出 `InterruptedException`,从而有机会进行取消或错误处理。
+ - `ReentrantLock#lockInterruptibly()` 实现了可中断锁。
+ - `ReentrantLock#tryLock(long time, TimeUnit unit)` (带超时的尝试获取)也是可中断的。
## ReentrantReadWriteLock
@@ -690,7 +796,7 @@ public interface ReadWriteLock {

-`ReentrantReadWriteLock` 也支持公平锁和非公平锁,默认使用非公平锁,可以通过构造器来显示的指定。
+`ReentrantReadWriteLock` 也支持公平锁和非公平锁,默认使用非公平锁,可以通过构造器来显式地指定。
```java
// 传入一个 boolean 值,true 时为公平锁,false 时为非公平锁
diff --git a/docs/java/concurrent/java-concurrent-questions-03.md b/docs/java/concurrent/java-concurrent-questions-03.md
index 1a014b86080..430c33f6999 100644
--- a/docs/java/concurrent/java-concurrent-questions-03.md
+++ b/docs/java/concurrent/java-concurrent-questions-03.md
@@ -1,15 +1,13 @@
---
title: Java并发常见面试题总结(下)
+description: Java并发高级面试题:详解ThreadLocal原理与内存泄漏、线程池参数配置与工作原理、Future/CompletableFuture异步编程、并发容器与工具类使用。
category: Java
tag:
- Java并发
head:
- - meta
- name: keywords
- content: 多线程,死锁,线程池,CAS,AQS
- - - meta
- - name: description
- content: Java并发常见知识点和面试题总结(含详细解答),希望对你有帮助!
+ content: ThreadLocal,线程池,Executor框架,Future,CompletableFuture,并发工具类,并发容器,并发面试题
---
@@ -18,93 +16,36 @@ head:
### ThreadLocal 有什么用?
-通常情况下,我们创建的变量是可以被任何一个线程访问并修改的。**如果想实现每一个线程都有自己的专属本地变量该如何解决呢?**
-
-JDK 中自带的`ThreadLocal`类正是为了解决这样的问题。 **`ThreadLocal`类主要解决的就是让每个线程绑定自己的值,可以将`ThreadLocal`类形象的比喻成存放数据的盒子,盒子中可以存储每个线程的私有数据。**
-
-如果你创建了一个`ThreadLocal`变量,那么访问这个变量的每个线程都会有这个变量的本地副本,这也是`ThreadLocal`变量名的由来。他们可以使用 `get()` 和 `set()` 方法来获取默认值或将其值更改为当前线程所存的副本的值,从而避免了线程安全问题。
+通常情况下,我们创建的变量可以被任何一个线程访问和修改。这在多线程环境中可能导致数据竞争和线程安全问题。那么,**如果想让每个线程都有自己的专属本地变量,该如何实现呢?**
-再举个简单的例子:两个人去宝屋收集宝物,这两个共用一个袋子的话肯定会产生争执,但是给他们两个人每个人分配一个袋子的话就不会出现这样的问题。如果把这两个人比作线程的话,那么 ThreadLocal 就是用来避免这两个线程竞争的。
+JDK 中提供的 `ThreadLocal` 类正是为了解决这个问题。**`ThreadLocal` 类允许每个线程绑定自己的值**,可以将其形象地比喻为一个“存放数据的盒子”。每个线程都有自己独立的盒子,用于存储私有数据,确保不同线程之间的数据互不干扰。
-### 如何使用 ThreadLocal?
+当你创建一个 `ThreadLocal` 变量时,每个访问该变量的线程都会拥有一个独立的副本。这也是 `ThreadLocal` 名称的由来。线程可以通过 `get()` 方法获取自己线程的本地副本,或通过 `set()` 方法修改该副本的值,从而避免了线程安全问题。
-相信看了上面的解释,大家已经搞懂 `ThreadLocal` 类是个什么东西了。下面简单演示一下如何在项目中实际使用 `ThreadLocal` 。
+举个简单的例子:假设有两个人去宝屋收集宝物。如果他们共用一个袋子,必然会产生争执;但如果每个人都有一个独立的袋子,就不会有这个问题。如果将这两个人比作线程,那么 `ThreadLocal` 就是用来避免这两个线程竞争同一个资源的方法。
```java
-import java.text.SimpleDateFormat;
-import java.util.Random;
-
-public class ThreadLocalExample implements Runnable{
-
- // SimpleDateFormat 不是线程安全的,所以每个线程都要有自己独立的副本
- private static final ThreadLocal formatter = ThreadLocal.withInitial(() -> new SimpleDateFormat("yyyyMMdd HHmm"));
+public class ThreadLocalExample {
+ private static ThreadLocal threadLocal = ThreadLocal.withInitial(() -> 0);
- public static void main(String[] args) throws InterruptedException {
- ThreadLocalExample obj = new ThreadLocalExample();
- for(int i=0 ; i<10; i++){
- Thread t = new Thread(obj, ""+i);
- Thread.sleep(new Random().nextInt(1000));
- t.start();
- }
- }
-
- @Override
- public void run() {
- System.out.println("Thread Name= "+Thread.currentThread().getName()+" default Formatter = "+formatter.get().toPattern());
- try {
- Thread.sleep(new Random().nextInt(1000));
- } catch (InterruptedException e) {
- e.printStackTrace();
- }
- //formatter pattern is changed here by thread, but it won't reflect to other threads
- formatter.set(new SimpleDateFormat());
-
- System.out.println("Thread Name= "+Thread.currentThread().getName()+" formatter = "+formatter.get().toPattern());
+ public static void main(String[] args) {
+ Runnable task = () -> {
+ int value = threadLocal.get();
+ value += 1;
+ threadLocal.set(value);
+ System.out.println(Thread.currentThread().getName() + " Value: " + threadLocal.get());
+ };
+
+ Thread thread1 = new Thread(task, "Thread-1");
+ Thread thread2 = new Thread(task, "Thread-2");
+
+ thread1.start(); // 输出: Thread-1 Value: 1
+ thread2.start(); // 输出: Thread-2 Value: 1
}
-
}
-
```
-输出结果 :
-
-```plain
-Thread Name= 0 default Formatter = yyyyMMdd HHmm
-Thread Name= 0 formatter = yy-M-d ah:mm
-Thread Name= 1 default Formatter = yyyyMMdd HHmm
-Thread Name= 2 default Formatter = yyyyMMdd HHmm
-Thread Name= 1 formatter = yy-M-d ah:mm
-Thread Name= 3 default Formatter = yyyyMMdd HHmm
-Thread Name= 2 formatter = yy-M-d ah:mm
-Thread Name= 4 default Formatter = yyyyMMdd HHmm
-Thread Name= 3 formatter = yy-M-d ah:mm
-Thread Name= 4 formatter = yy-M-d ah:mm
-Thread Name= 5 default Formatter = yyyyMMdd HHmm
-Thread Name= 5 formatter = yy-M-d ah:mm
-Thread Name= 6 default Formatter = yyyyMMdd HHmm
-Thread Name= 6 formatter = yy-M-d ah:mm
-Thread Name= 7 default Formatter = yyyyMMdd HHmm
-Thread Name= 7 formatter = yy-M-d ah:mm
-Thread Name= 8 default Formatter = yyyyMMdd HHmm
-Thread Name= 9 default Formatter = yyyyMMdd HHmm
-Thread Name= 8 formatter = yy-M-d ah:mm
-Thread Name= 9 formatter = yy-M-d ah:mm
-```
-
-从输出中可以看出,虽然 `Thread-0` 已经改变了 `formatter` 的值,但 `Thread-1` 默认格式化值与初始化值相同,其他线程也一样。
-
-上面有一段代码用到了创建 `ThreadLocal` 变量的那段代码用到了 Java8 的知识,它等于下面这段代码,如果你写了下面这段代码的话,IDEA 会提示你转换为 Java8 的格式(IDEA 真的不错!)。因为 ThreadLocal 类在 Java 8 中扩展,使用一个新的方法`withInitial()`,将 Supplier 功能接口作为参数。
-
-```java
-private static final ThreadLocal formatter = new ThreadLocal(){
- @Override
- protected SimpleDateFormat initialValue(){
- return new SimpleDateFormat("yyyyMMdd HHmm");
- }
-};
-```
-
-### ThreadLocal 原理了解吗?
+### ⭐️ThreadLocal 原理了解吗?
从 `Thread`类源代码入手。
@@ -161,15 +102,36 @@ ThreadLocalMap(ThreadLocal> firstKey, Object firstValue) {

-### ThreadLocal 内存泄露问题是怎么导致的?
+### ⭐️ThreadLocal 内存泄露问题是怎么导致的?
+
+`ThreadLocal` 内存泄漏的根本原因在于其内部实现机制。
+
+通过上面的内容我们已经知道:每个线程维护一个名为 `ThreadLocalMap` 的 map。 当你使用 `ThreadLocal` 存储值时,实际上是将值存储在当前线程的 `ThreadLocalMap` 中,其中 `ThreadLocal` 实例本身作为 key,而你要存储的值作为 value。
+
+`ThreadLocal` 的 `set()` 方法源码如下:
+
+```java
+public void set(T value) {
+ Thread t = Thread.currentThread(); // 获取当前线程
+ ThreadLocalMap map = getMap(t); // 获取当前线程的 ThreadLocalMap
+ if (map != null) {
+ map.set(this, value); // 设置值
+ } else {
+ createMap(t, value); // 创建新的 ThreadLocalMap
+ }
+}
+```
+
+`ThreadLocalMap` 的 `set()` 和 `createMap()` 方法中,并没有直接存储 `ThreadLocal` 对象本身,而是使用 `ThreadLocal` 的哈希值计算数组索引,最终存储于类型为`static class Entry extends WeakReference>`的数组中。
-`ThreadLocalMap` 中使用的 key 为 `ThreadLocal` 的弱引用,而 value 是强引用。所以,如果 `ThreadLocal` 没有被外部强引用的情况下,在垃圾回收的时候,key 会被清理掉,而 value 不会被清理掉。
+```java
+int i = key.threadLocalHashCode & (len-1);
+```
-这样一来,`ThreadLocalMap` 中就会出现 key 为 null 的 Entry。假如我们不做任何措施的话,value 永远无法被 GC 回收,这个时候就可能会产生内存泄露。`ThreadLocalMap` 实现中已经考虑了这种情况,在调用 `set()`、`get()`、`remove()` 方法的时候,会清理掉 key 为 null 的记录。使用完 `ThreadLocal`方法后最好手动调用`remove()`方法
+`ThreadLocalMap` 的 `Entry` 定义如下:
```java
static class Entry extends WeakReference> {
- /** The value associated with this ThreadLocal. */
Object value;
Entry(ThreadLocal> k, Object v) {
@@ -179,11 +141,91 @@ static class Entry extends WeakReference> {
}
```
-**弱引用介绍:**
+`ThreadLocalMap` 的 `key` 和 `value` 引用机制:
-> 如果一个对象只具有弱引用,那就类似于**可有可无的生活用品**。弱引用与软引用的区别在于:只具有弱引用的对象拥有更短暂的生命周期。在垃圾回收器线程扫描它 所管辖的内存区域的过程中,一旦发现了只具有弱引用的对象,不管当前内存空间足够与否,都会回收它的内存。不过,由于垃圾回收器是一个优先级很低的线程, 因此不一定会很快发现那些只具有弱引用的对象。
->
-> 弱引用可以和一个引用队列(ReferenceQueue)联合使用,如果弱引用所引用的对象被垃圾回收,Java 虚拟机就会把这个弱引用加入到与之关联的引用队列中。
+- **key 是弱引用**:`ThreadLocalMap` 中的 key 是 `ThreadLocal` 的弱引用 (`WeakReference>`)。 这意味着,如果 `ThreadLocal` 实例不再被任何强引用指向,垃圾回收器会在下次 GC 时回收该实例,导致 `ThreadLocalMap` 中对应的 key 变为 `null`。
+- **value 是强引用**:即使 `key` 被 GC 回收,`value` 仍然被 `ThreadLocalMap.Entry` 强引用存在,无法被 GC 回收。
+
+当 `ThreadLocal` 实例失去强引用后,其对应的 value 仍然存在于 `ThreadLocalMap` 中,因为 `Entry` 对象强引用了它。如果线程持续存活(例如线程池中的线程),`ThreadLocalMap` 也会一直存在,导致 key 为 `null` 的 entry 无法被垃圾回收,即会造成内存泄漏。
+
+也就是说,内存泄漏的发生需要同时满足两个条件:
+
+1. `ThreadLocal` 实例不再被强引用;
+2. 线程持续存活,导致 `ThreadLocalMap` 长期存在。
+
+虽然 `ThreadLocalMap` 在 `get()`, `set()` 和 `remove()` 操作时会尝试清理 key 为 null 的 entry,但这种清理机制是被动的,并不完全可靠。
+
+**如何避免内存泄漏的发生?**
+
+1. 在使用完 `ThreadLocal` 后,务必调用 `remove()` 方法。 这是最安全和最推荐的做法。 `remove()` 方法会从 `ThreadLocalMap` 中显式地移除对应的 entry,彻底解决内存泄漏的风险。 即使将 `ThreadLocal` 定义为 `static final`,也强烈建议在每次使用后调用 `remove()`。
+2. 在线程池等线程复用的场景下,使用 `try-finally` 块可以确保即使发生异常,`remove()` 方法也一定会被执行。
+
+### ⭐️如何跨线程传递 ThreadLocal 的值?
+
+由于 `ThreadLocal` 的变量值存放在 `Thread` 里,而父子线程属于不同的 `Thread` 的。因此在异步场景下,父子线程的 `ThreadLocal` 值无法进行传递。
+
+如果想要在异步场景下传递 `ThreadLocal` 值,有两种解决方案:
+
+- `InheritableThreadLocal` :`InheritableThreadLocal` 是 JDK1.2 提供的工具,继承自 `ThreadLocal` 。使用 `InheritableThreadLocal` 时,会在创建子线程时,令子线程继承父线程中的 `ThreadLocal` 值,但是无法支持线程池场景下的 `ThreadLocal` 值传递。
+- `TransmittableThreadLocal` : `TransmittableThreadLocal` (简称 TTL) 是阿里巴巴开源的工具类,继承并加强了`InheritableThreadLocal`类,可以在线程池的场景下支持 `ThreadLocal` 值传递。项目地址:。
+
+#### InheritableThreadLocal 原理
+
+`InheritableThreadLocal` 实现了创建异步线程时,继承父线程 `ThreadLocal` 值的功能。该类是 JDK 团队提供的,通过改造 JDK 源码包中的 `Thread` 类来实现创建线程时,`ThreadLocal` 值的传递。
+
+**`InheritableThreadLocal` 的值存储在哪里?**
+
+在 `Thread` 类中添加了一个新的 `ThreadLocalMap` ,命名为 `inheritableThreadLocals` ,该变量用于存储需要跨线程传递的 `ThreadLocal` 值。如下:
+
+```JAVA
+class Thread implements Runnable {
+ ThreadLocal.ThreadLocalMap threadLocals = null;
+ ThreadLocal.ThreadLocalMap inheritableThreadLocals = null;
+}
+```
+
+**如何完成 `ThreadLocal` 值的传递?**
+
+通过改造 `Thread` 类的构造方法来实现,在创建 `Thread` 线程时,拿到父线程的 `inheritableThreadLocals` 变量赋值给子线程即可。相关代码如下:
+
+```JAVA
+// Thread 的构造方法会调用 init() 方法
+private void init(/* ... */) {
+ // 1、获取父线程
+ Thread parent = currentThread();
+ // 2、将父线程的 inheritableThreadLocals 赋值给子线程
+ if (inheritThreadLocals && parent.inheritableThreadLocals != null)
+ this.inheritableThreadLocals =
+ ThreadLocal.createInheritedMap(parent.inheritableThreadLocals);
+}
+```
+
+#### TransmittableThreadLocal 原理
+
+JDK 默认没有支持线程池场景下 `ThreadLocal` 值传递的功能,因此阿里巴巴开源了一套工具 `TransmittableThreadLocal` 来实现该功能。
+
+阿里巴巴无法改动 JDK 的源码,因此他内部通过 **装饰器模式** 在原有的功能上做增强,以此来实现线程池场景下的 `ThreadLocal` 值传递。
+
+TTL 改造的地方有两处:
+
+- 实现自定义的 `Thread` ,在 `run()` 方法内部做 `ThreadLocal` 变量的赋值操作。
+
+- 基于 **线程池** 进行装饰,在 `execute()` 方法中,不提交 JDK 内部的 `Thread` ,而是提交自定义的 `Thread` 。
+
+如果想要查看相关源码,可以引入 Maven 依赖进行下载。
+
+```XML
+
+ com.alibaba
+ transmittable-thread-local
+ 2.12.0
+
+```
+
+#### 应用场景
+
+1. **压测流量标记**: 在压测场景中,使用 `ThreadLocal` 存储压测标记,用于区分压测流量和真实流量。如果标记丢失,可能导致压测流量被错误地当成线上流量处理。
+2. **上下文传递**:在分布式系统中,传递链路追踪信息(如 Trace ID)或用户上下文信息。
## 线程池
@@ -191,25 +233,27 @@ static class Entry extends WeakReference> {
顾名思义,线程池就是管理一系列线程的资源池。当有任务要处理时,直接从线程池中获取线程来处理,处理完之后线程并不会立即被销毁,而是等待下一个任务。
-### 为什么要用线程池?
+### ⭐️为什么要用线程池?
池化技术想必大家已经屡见不鲜了,线程池、数据库连接池、HTTP 连接池等等都是对这个思想的应用。池化技术的思想主要是为了减少每次获取资源的消耗,提高对资源的利用率。
-**线程池**提供了一种限制和管理资源(包括执行一个任务)的方式。 每个**线程池**还维护一些基本统计信息,例如已完成任务的数量。
-
-这里借用《Java 并发编程的艺术》提到的来说一下**使用线程池的好处**:
+线程池提供了一种限制和管理资源(包括执行一个任务)的方式。 每个线程池还维护一些基本统计信息,例如已完成任务的数量。使用线程池主要带来以下几个好处:
-- **降低资源消耗**。通过重复利用已创建的线程降低线程创建和销毁造成的消耗。
-- **提高响应速度**。当任务到达时,任务可以不需要等到线程创建就能立即执行。
-- **提高线程的可管理性**。线程是稀缺资源,如果无限制的创建,不仅会消耗系统资源,还会降低系统的稳定性,使用线程池可以进行统一的分配,调优和监控。
+1. **降低资源消耗**:线程池里的线程是可以重复利用的。一旦线程完成了某个任务,它不会立即销毁,而是回到池子里等待下一个任务。这就避免了频繁创建和销毁线程带来的开销。
+2. **提高响应速度**:因为线程池里通常会维护一定数量的核心线程(或者说“常驻工人”),任务来了之后,可以直接交给这些已经存在的、空闲的线程去执行,省去了创建线程的时间,任务能够更快地得到处理。
+3. **提高线程的可管理性**:线程池允许我们统一管理池中的线程。我们可以配置线程池的大小(核心线程数、最大线程数)、任务队列的类型和大小、拒绝策略等。这样就能控制并发线程的总量,防止资源耗尽,保证系统的稳定性。同时,线程池通常也提供了监控接口,方便我们了解线程池的运行状态(比如有多少活跃线程、多少任务在排队等),便于调优。
### 如何创建线程池?
-**方式一:通过`ThreadPoolExecutor`构造函数来创建(推荐)。**
+在 Java 中,创建线程池主要有两种方式:
-
+**方式一:通过 `ThreadPoolExecutor` 构造函数直接创建 (推荐)**
-**方式二:通过 `Executor` 框架的工具类 `Executors` 来创建。**
+
+
+这是最推荐的方式,因为它允许开发者明确指定线程池的核心参数,对线程池的运行行为有更精细的控制,从而避免资源耗尽的风险。
+
+**方式二:通过 `Executors` 工具类创建 (不推荐用于生产环境)**
`Executors`工具类提供的创建线程池的方法如下图所示:
@@ -222,7 +266,7 @@ static class Entry extends WeakReference> {
- `CachedThreadPool`: 可根据实际情况调整线程数量的线程池。线程池的线程数量不确定,但若有空闲线程可以复用,则会优先使用可复用的线程。若所有线程均在工作,又有新的任务提交,则会创建新的线程处理任务。所有线程在当前任务执行完毕后,将返回线程池进行复用。
- `ScheduledThreadPool`:给定的延迟后运行任务或者定期执行任务的线程池。
-### 为什么不推荐使用内置线程池?
+### ⭐️为什么不推荐使用内置线程池?
在《阿里巴巴 Java 开发手册》“并发处理”这一章节,明确指出线程资源必须通过线程池提供,不允许在应用中自行显式创建线程。
@@ -232,23 +276,21 @@ static class Entry extends WeakReference> {
另外,《阿里巴巴 Java 开发手册》中强制线程池不允许使用 `Executors` 去创建,而是通过 `ThreadPoolExecutor` 构造函数的方式,这样的处理方式让写的同学更加明确线程池的运行规则,规避资源耗尽的风险
-`Executors` 返回线程池对象的弊端如下:
+`Executors` 返回线程池对象的弊端如下(后文会详细介绍到):
-- `FixedThreadPool` 和 `SingleThreadExecutor`:使用的是有界阻塞队列是 `LinkedBlockingQueue` ,其任务队列的最大长度为 `Integer.MAX_VALUE` ,可能堆积大量的请求,从而导致 OOM。
+- `FixedThreadPool` 和 `SingleThreadExecutor`:使用的是阻塞队列 `LinkedBlockingQueue`,任务队列最大长度为 `Integer.MAX_VALUE`,可以看作是无界的,可能堆积大量的请求,从而导致 OOM。
- `CachedThreadPool`:使用的是同步队列 `SynchronousQueue`, 允许创建的线程数量为 `Integer.MAX_VALUE` ,如果任务数量过多且执行速度较慢,可能会创建大量的线程,从而导致 OOM。
-- `ScheduledThreadPool` 和 `SingleThreadScheduledExecutor` :使用的无界的延迟阻塞队列 `DelayedWorkQueue` ,任务队列最大长度为 `Integer.MAX_VALUE` ,可能堆积大量的请求,从而导致 OOM。
+- `ScheduledThreadPool` 和 `SingleThreadScheduledExecutor`:使用的无界的延迟阻塞队列`DelayedWorkQueue`,任务队列最大长度为 `Integer.MAX_VALUE`,可能堆积大量的请求,从而导致 OOM。
```java
-// 有界队列 LinkedBlockingQueue
public static ExecutorService newFixedThreadPool(int nThreads) {
-
+ // LinkedBlockingQueue 的默认长度为 Integer.MAX_VALUE,可以看作是无界的
return new ThreadPoolExecutor(nThreads, nThreads,0L, TimeUnit.MILLISECONDS,new LinkedBlockingQueue());
}
-// 无界队列 LinkedBlockingQueue
public static ExecutorService newSingleThreadExecutor() {
-
+ // LinkedBlockingQueue 的默认长度为 Integer.MAX_VALUE,可以看作是无界的
return new FinalizableDelegatedExecutorService (new ThreadPoolExecutor(1, 1,0L, TimeUnit.MILLISECONDS,new LinkedBlockingQueue()));
}
@@ -270,7 +312,7 @@ public ScheduledThreadPoolExecutor(int corePoolSize) {
}
```
-### 线程池常见参数有哪些?如何解释?
+### ⭐️线程池常见参数有哪些?如何解释?
```java
/**
@@ -308,7 +350,7 @@ public ScheduledThreadPoolExecutor(int corePoolSize) {
`ThreadPoolExecutor`其他常见参数 :
-- `keepAliveTime`:线程池中的线程数量大于 `corePoolSize` 的时候,如果这时没有新的任务提交,核心线程外的线程不会立即销毁,而是会等待,直到等待的时间超过了 `keepAliveTime`才会被回收销毁。
+- `keepAliveTime`:当线程池中的线程数量大于 `corePoolSize` ,即有非核心线程(线程池中核心线程以外的线程)时,这些非核心线程空闲后不会立即销毁,而是会等待,直到等待的时间超过了 `keepAliveTime`才会被回收销毁。
- `unit` : `keepAliveTime` 参数的时间单位。
- `threadFactory` :executor 创建新线程的时候会用到。
- `handler` :拒绝策略(后面会单独详细介绍一下)。
@@ -322,16 +364,85 @@ public ScheduledThreadPoolExecutor(int corePoolSize) {
`ThreadPoolExecutor` 默认不会回收核心线程,即使它们已经空闲了。这是为了减少创建线程的开销,因为核心线程通常是要长期保持活跃的。但是,如果线程池是被用于周期性使用的场景,且频率不高(周期之间有明显的空闲时间),可以考虑将 `allowCoreThreadTimeOut(boolean value)` 方法的参数设置为 `true`,这样就会回收空闲(时间间隔由 `keepAliveTime` 指定)的核心线程了。
```java
- ThreadPoolExecutor threadPoolExecutor = new ThreadPoolExecutor(4, 6, 6, TimeUnit.SECONDS, new SynchronousQueue<>());
- threadPoolExecutor.allowCoreThreadTimeOut(true);
+public void allowCoreThreadTimeOut(boolean value) {
+ // 核心线程的 keepAliveTime 必须大于 0 才能启用超时机制
+ if (value && keepAliveTime <= 0) {
+ throw new IllegalArgumentException("Core threads must have nonzero keep alive times");
+ }
+ // 设置 allowCoreThreadTimeOut 的值
+ if (value != allowCoreThreadTimeOut) {
+ allowCoreThreadTimeOut = value;
+ // 如果启用了超时机制,清理所有空闲的线程,包括核心线程
+ if (value) {
+ interruptIdleWorkers();
+ }
+ }
+}
+```
+
+### 核心线程空闲时处于什么状态?
+
+核心线程空闲时,其状态分为以下两种情况:
+
+- **设置了核心线程的存活时间** :核心线程在空闲时,会处于 `WAITING` 状态,等待获取任务。如果阻塞等待的时间超过了核心线程存活时间,则该线程会退出工作,将该线程从线程池的工作线程集合中移除,线程状态变为 `TERMINATED` 状态。
+- **没有设置核心线程的存活时间** :核心线程在空闲时,会一直处于 `WAITING` 状态,等待获取任务,核心线程会一直存活在线程池中。
+
+当队列中有可用任务时,会唤醒被阻塞的线程,线程的状态会由 `WAITING` 状态变为 `RUNNABLE` 状态,之后去执行对应任务。
+
+接下来通过相关源码,了解一下线程池内部是如何做的。
+
+线程在线程池内部被抽象为了 `Worker` ,当 `Worker` 被启动之后,会不断去任务队列中获取任务。
+
+在获取任务的时候,会根据 `timed` 值来决定从任务队列( `BlockingQueue` )获取任务的行为。
+
+如果「设置了核心线程的存活时间」或者「线程数量超过了核心线程数量」,则将 `timed` 标记为 `true` ,表明获取任务时需要使用 `poll()` 指定超时时间。
+
+- `timed == true` :使用 `poll(timeout, unit)` 来获取任务。使用 `poll(timeout, unit)` 方法获取任务超时的话,则当前线程会退出执行( `TERMINATED` ),该线程从线程池中被移除。
+- `timed == false` :使用 `take()` 来获取任务。使用 `take()` 方法获取任务会让当前线程一直阻塞等待(`WAITING`)。
+
+源码如下:
+
+```JAVA
+// ThreadPoolExecutor
+private Runnable getTask() {
+ boolean timedOut = false;
+ for (;;) {
+ // ...
+
+ // 1、如果「设置了核心线程的存活时间」或者是「线程数量超过了核心线程数量」,则 timed 为 true。
+ boolean timed = allowCoreThreadTimeOut || wc > corePoolSize;
+ // 2、扣减线程数量。
+ // wc > maximuimPoolSize:线程池中的线程数量超过最大线程数量。其中 wc 为线程池中的线程数量。
+ // timed && timeOut:timeOut 表示获取任务超时。
+ // 分为两种情况:核心线程设置了存活时间 && 获取任务超时,则扣减线程数量;线程数量超过了核心线程数量 && 获取任务超时,则扣减线程数量。
+ if ((wc > maximumPoolSize || (timed && timedOut))
+ && (wc > 1 || workQueue.isEmpty())) {
+ if (compareAndDecrementWorkerCount(c))
+ return null;
+ continue;
+ }
+ try {
+ // 3、如果 timed 为 true,则使用 poll() 获取任务;否则,使用 take() 获取任务。
+ Runnable r = timed ?
+ workQueue.poll(keepAliveTime, TimeUnit.NANOSECONDS) :
+ workQueue.take();
+ // 4、获取任务之后返回。
+ if (r != null)
+ return r;
+ timedOut = true;
+ } catch (InterruptedException retry) {
+ timedOut = false;
+ }
+ }
+}
```
-### 线程池的拒绝策略有哪些?
+### ⭐️线程池的拒绝策略有哪些?
如果当前同时运行的线程数量达到最大线程数量并且队列也已经被放满了任务时,`ThreadPoolExecutor` 定义一些策略:
- `ThreadPoolExecutor.AbortPolicy`:抛出 `RejectedExecutionException`来拒绝新任务的处理。
-- `ThreadPoolExecutor.CallerRunsPolicy`:调用执行自己的线程运行任务,也就是直接在调用`execute`方法的线程中运行(`run`)被拒绝的任务,如果执行程序已关闭,则会丢弃该任务。因此这种策略会降低对于新任务提交速度,影响程序的整体性能。如果你的应用程序可以承受此延迟并且你要求任何一个任务请求都要被执行的话,你可以选择这个策略。
+- `ThreadPoolExecutor.CallerRunsPolicy`:调用执行者自己的线程运行任务,也就是直接在调用`execute`方法的线程中运行(`run`)被拒绝的任务,如果执行程序已关闭,则会丢弃该任务。因此这种策略会降低对于新任务提交速度,影响程序的整体性能。如果你的应用程序可以承受此延迟并且你要求任何一个任务请求都要被执行的话,你可以选择这个策略。
- `ThreadPoolExecutor.DiscardPolicy`:不处理新任务,直接丢弃掉。
- `ThreadPoolExecutor.DiscardOldestPolicy`:此策略将丢弃最早的未处理的任务请求。
@@ -351,7 +462,7 @@ public static class CallerRunsPolicy implements RejectedExecutionHandler {
}
```
-### 如果不允许丢弃任务任务,应该选择哪个拒绝策略?
+### 如果不允许丢弃任务,应该选择哪个拒绝策略?
根据上面对线程池拒绝策略的介绍,相信大家很容易能够得出答案是:`CallerRunsPolicy` 。
@@ -437,7 +548,7 @@ public class ThreadPoolTest {
输出:
-```ba
+```bash
18:19:48.203 INFO [pool-1-thread-1] c.j.concurrent.ThreadPoolTest - 核心线程执行第一个任务
18:19:48.203 INFO [pool-1-thread-2] c.j.concurrent.ThreadPoolTest - 非核心线程处理第三个任务
18:19:48.203 INFO [main] c.j.concurrent.ThreadPoolTest - 主线程处理第四个任务
@@ -468,7 +579,7 @@ public class ThreadPoolTest {

-整个实现逻辑还是比较简单的,核心在于自定义拒绝策略和阻塞队列。如此一来,一旦我们的线程池中线程以达到满载时,我们就可以通过拒绝策略将最新任务持久化到 MySQL 数据库中,等到线程池有了有余力处理所有任务时,让其优先处理数据库中的任务以避免"饥饿"问题。
+整个实现逻辑还是比较简单的,核心在于自定义拒绝策略和阻塞队列。如此一来,一旦我们的线程池中线程达到满载时,我们就可以通过拒绝策略将最新任务持久化到 MySQL 数据库中,等到线程池有了有余力处理所有任务时,让其优先处理数据库中的任务以避免"饥饿"问题。
当然,对于这个问题,我们也可以参考其他主流框架的做法,以 Netty 为例,它的拒绝策略则是直接创建一个线程池以外的线程处理这些任务,为了保证任务的实时处理,这种做法可能需要良好的硬件设备且临时创建的线程无法做到准确的监控:
@@ -513,12 +624,12 @@ new RejectedExecutionHandler() {
不同的线程池会选用不同的阻塞队列,我们可以结合内置线程池来分析。
-- 容量为 `Integer.MAX_VALUE` 的 `LinkedBlockingQueue`(有界阻塞队列):`FixedThreadPool` 和 `SingleThreadExecutor` 。`FixedThreadPool`最多只能创建核心线程数的线程(核心线程数和最大线程数相等),`SingleThreadExecutor`只能创建一个线程(核心线程数和最大线程数都是 1),二者的任务队列永远不会被放满。
+- 容量为 `Integer.MAX_VALUE` 的 `LinkedBlockingQueue`(无界阻塞队列):`FixedThreadPool` 和 `SingleThreadExecutor` 。`FixedThreadPool`最多只能创建核心线程数的线程(核心线程数和最大线程数相等),`SingleThreadExecutor`只能创建一个线程(核心线程数和最大线程数都是 1),二者的任务队列永远不会被放满。
- `SynchronousQueue`(同步队列):`CachedThreadPool` 。`SynchronousQueue` 没有容量,不存储元素,目的是保证对于提交的任务,如果有空闲线程,则使用空闲线程来处理;否则新建一个线程来处理任务。也就是说,`CachedThreadPool` 的最大线程数是 `Integer.MAX_VALUE` ,可以理解为线程数是可以无限扩展的,可能会创建大量线程,从而导致 OOM。
-- `DelayedWorkQueue`(延迟队列):`ScheduledThreadPool` 和 `SingleThreadScheduledExecutor` 。`DelayedWorkQueue` 的内部元素并不是按照放入的时间排序,而是会按照延迟的时间长短对任务进行排序,内部采用的是“堆”的数据结构,可以保证每次出队的任务都是当前队列中执行时间最靠前的。`DelayedWorkQueue` 添加元素满了之后会自动扩容,增加原来容量的 50%,即永远不会阻塞,最大扩容可达 `Integer.MAX_VALUE`,所以最多只能创建核心线程数的线程。
+- `DelayedWorkQueue`(延迟队列):`ScheduledThreadPool` 和 `SingleThreadScheduledExecutor` 。`DelayedWorkQueue` 的内部元素并不是按照放入的时间排序,而是会按照延迟的时间长短对任务进行排序,内部采用的是“堆”的数据结构,可以保证每次出队的任务都是当前队列中执行时间最靠前的。`DelayedWorkQueue` 是一个无界队列。其底层虽然是数组,但当数组容量不足时,它会自动进行扩容,因此队列永远不会被填满。当任务不断提交时,它们会全部被添加到队列中。这意味着线程池的线程数量永远不会超过其核心线程数,最大线程数参数对于使用该队列的线程池来说是无效的。
- `ArrayBlockingQueue`(有界阻塞队列):底层由数组实现,容量一旦创建,就不能修改。
-### 线程池处理任务的流程了解吗?
+### ⭐️线程池处理任务的流程了解吗?

@@ -534,7 +645,7 @@ new RejectedExecutionHandler() {
- `prestartCoreThread()`:启动一个线程,等待任务,如果已达到核心线程数,这个方法返回 false,否则返回 true;
- `prestartAllCoreThreads()`:启动所有的核心线程,并返回启动成功的核心线程数。
-### 线程池中线程异常后,销毁还是复用?
+### ⭐️线程池中线程异常后,销毁还是复用?
直接说结论,需要分两种情况:
@@ -547,7 +658,7 @@ new RejectedExecutionHandler() {
具体的源码分析可以参考这篇:[线程池中线程异常后:销毁还是复用? - 京东技术](https://mp.weixin.qq.com/s/9ODjdUU-EwQFF5PrnzOGfw)。
-### 如何给线程池命名?
+### ⭐️如何给线程池命名?
初始化线程池的时候需要显示命名(设置线程池名称前缀),有利于定位问题。
@@ -606,7 +717,7 @@ public final class NamingThreadFactory implements ThreadFactory {
>
> Linux 相比与其他操作系统(包括其他类 Unix 系统)有很多的优点,其中有一项就是,其上下文切换和模式切换的时间消耗非常少。
-类比于实现世界中的人类通过合作做某件事情,我们可以肯定的一点是线程池大小设置过大或者过小都会有问题,合适的才是最好。
+类比于现实世界中的人类通过合作做某件事情,我们可以肯定的一点是线程池大小设置过大或者过小都会有问题,合适的才是最好。
- 如果我们设置的线程池数量太小的话,如果同一时间有大量任务/请求需要处理,可能会导致大量的请求/任务在任务队列中排队等待执行,甚至会出现任务队列满了之后任务/请求无法处理的情况,或者大量任务堆积在任务队列导致 OOM。这样很明显是有问题的,CPU 根本没有得到充分利用。
- 如果我们设置线程数量太大,大量线程可能会同时在争取 CPU 资源,这样会导致大量的上下文切换,从而增加线程的执行时间,影响了整体执行效率。
@@ -634,13 +745,13 @@ CPU 密集型简单理解就是利用 CPU 计算能力的任务比如你在内
公式也只是参考,具体还是要根据项目实际线上运行情况来动态调整。我在后面介绍的美团的线程池参数动态配置这种方案就非常不错,很实用!
-### 如何动态修改线程池的参数?
+### ⭐️如何动态修改线程池的参数?
美团技术团队在[《Java 线程池实现原理及其在美团业务中的实践》](https://tech.meituan.com/2020/04/02/java-pooling-pratice-in-meituan.html)这篇文章中介绍到对线程池参数实现可自定义配置的思路和方法。
美团技术团队的思路是主要对线程池的核心参数实现自定义可配置。这三个核心参数是:
-- **`corePoolSize` :** 核心线程数线程数定义了最小可以同时运行的线程数量。
+- **`corePoolSize` :** 核心线程数定义了最小可以同时运行的线程数量。
- **`maximumPoolSize` :** 当队列中存放的任务达到队列容量的时候,当前可以同时运行的线程数量变为最大线程数。
- **`workQueue`:** 当新任务来的时候会先判断当前运行的线程数量是否达到核心线程数,如果达到的话,新任务就会被存放在队列中。
@@ -660,14 +771,16 @@ CPU 密集型简单理解就是利用 CPU 计算能力的任务比如你在内

-还没看够?推荐 why 神的[如何设置线程池参数?美团给出了一个让面试官虎躯一震的回答。](https://mp.weixin.qq.com/s/9HLuPcoWmTqAeFKa1kj-_A)这篇文章,深度剖析,很不错哦!
+还没看够?我在[《后端面试高频系统设计&场景题》](https://javaguide.cn/zhuanlan/back-end-interview-high-frequency-system-design-and-scenario-questions.html)中详细介绍了如何设计一个动态线程池,这也是面试中常问的一道系统设计题。
+
+
如果我们的项目也想要实现这种效果的话,可以借助现成的开源项目:
- **[Hippo4j](https://github.com/opengoofy/hippo4j)**:异步线程池框架,支持线程池动态变更&监控&报警,无需修改代码轻松引入。支持多种使用模式,轻松引入,致力于提高系统运行保障能力。
- **[Dynamic TP](https://github.com/dromara/dynamic-tp)**:轻量级动态线程池,内置监控告警功能,集成三方中间件线程池管理,基于主流配置中心(已支持 Nacos、Apollo,Zookeeper、Consul、Etcd,可通过 SPI 自定义实现)。
-### 如何设计一个能够根据任务的优先级来执行的线程池?
+### ⭐️如何设计一个能够根据任务的优先级来执行的线程池?
这是一个常见的面试问题,本质其实还是在考察求职者对于线程池以及阻塞队列的掌握。
@@ -698,6 +811,10 @@ CPU 密集型简单理解就是利用 CPU 计算能力的任务比如你在内
## Future
+重点是要掌握 `CompletableFuture` 的使用以及常见面试题。
+
+除了下面的面试题之外,还推荐你看看我写的这篇文章: [CompletableFuture 详解](https://javaguide.cn/java/concurrent/completablefuture-intro.html)。
+
### Future 类有什么用?
`Future` 类是异步思想的典型运用,主要用在一些需要执行耗时任务的场景,避免程序一直原地等待耗时任务执行完成,执行效率太低。具体来说是这样的:当我们执行某一耗时的任务时,可以将这个耗时任务交给一个子线程去异步执行,同时我们可以干点其他事情,不用傻傻等待耗时任务执行完成。等我们的事情干完后,我们再通过 `Future` 类获取到耗时任务的执行结果。这样一来,程序的执行效率就明显提高了。
@@ -766,9 +883,11 @@ public FutureTask(Runnable runnable, V result) {
`FutureTask`相当于对`Callable` 进行了封装,管理着任务执行的情况,存储了 `Callable` 的 `call` 方法的任务执行结果。
+关于更多 `Future` 的源码细节,可以肝这篇万字解析,写的很清楚:[Java 是如何实现 Future 模式的?万字详解!](https://juejin.cn/post/6844904199625375757)。
+
### CompletableFuture 类有什么用?
-`Future` 在实际使用过程中存在一些局限性比如不支持异步任务的编排组合、获取计算结果的 `get()` 方法为阻塞调用。
+`Future` 在实际使用过程中存在一些局限性,比如不支持异步任务的编排组合、获取计算结果的 `get()` 方法为阻塞调用。
Java 8 才被引入`CompletableFuture` 类可以解决`Future` 的这些缺陷。`CompletableFuture` 除了提供了更为好用和强大的 `Future` 特性之外,还提供了函数式编程、异步任务编排组合(可以将多个异步任务串联起来,组成一个完整的链式调用)等能力。
@@ -789,36 +908,105 @@ public class CompletableFuture implements Future, CompletionStage {

-## AQS
+### ⭐️一个任务需要依赖另外两个任务执行完之后再执行,怎么设计?
-### AQS 是什么?
+这种任务编排场景非常适合通过`CompletableFuture`实现。这里假设要实现 T3 在 T2 和 T1 执行完后执行。
+
+代码如下(这里为了简化代码,用到了 Hutool 的线程工具类 `ThreadUtil` 和日期时间工具类 `DateUtil`):
+
+```java
+// T1
+CompletableFuture futureT1 = CompletableFuture.runAsync(() -> {
+ System.out.println("T1 is executing. Current time:" + DateUtil.now());
+ // 模拟耗时操作
+ ThreadUtil.sleep(1000);
+});
+// T2
+CompletableFuture futureT2 = CompletableFuture.runAsync(() -> {
+ System.out.println("T2 is executing. Current time:" + DateUtil.now());
+ ThreadUtil.sleep(1000);
+});
+
+// 使用allOf()方法合并T1和T2的CompletableFuture,等待它们都完成
+CompletableFuture bothCompleted = CompletableFuture.allOf(futureT1, futureT2);
+// 当T1和T2都完成后,执行T3
+bothCompleted.thenRunAsync(() -> System.out.println("T3 is executing after T1 and T2 have completed.Current time:" + DateUtil.now()));
+// 等待所有任务完成,验证效果
+ThreadUtil.sleep(3000);
+```
+
+通过 `CompletableFuture` 的 `allOf()` 这个静态方法来并行运行 T1 和 T2,当 T1 和 T2 都完成后,再执行 T3。
+
+### ⭐️使用 CompletableFuture,有一个任务失败,如何处理异常?
+
+使用 `CompletableFuture`的时候一定要以正确的方式进行异常处理,避免异常丢失或者出现不可控问题。
+
+下面是一些建议:
+
+- 使用 `whenComplete` 方法可以在任务完成时触发回调函数,并正确地处理异常,而不是让异常被吞噬或丢失。
+- 使用 `exceptionally` 方法可以处理异常并重新抛出,以便异常能够传播到后续阶段,而不是让异常被忽略或终止。
+- 使用 `handle` 方法可以处理正常的返回结果和异常,并返回一个新的结果,而不是让异常影响正常的业务逻辑。
+- 使用 `CompletableFuture.allOf` 方法可以组合多个 `CompletableFuture`,并统一处理所有任务的异常,而不是让异常处理过于冗长或重复。
+- ……
-AQS 的全称为 `AbstractQueuedSynchronizer` ,翻译过来的意思就是抽象队列同步器。这个类在 `java.util.concurrent.locks` 包下面。
+### ⭐️在使用 CompletableFuture 的时候为什么要自定义线程池?
-
+`CompletableFuture` 默认使用全局共享的 `ForkJoinPool.commonPool()` 作为执行器,所有未指定执行器的异步任务都会使用该线程池。这意味着应用程序、多个库或框架(如 Spring、第三方库)若都依赖 `CompletableFuture`,默认情况下它们都会共享同一个线程池。
-AQS 就是一个抽象类,主要用来构建锁和同步器。
+虽然 `ForkJoinPool` 效率很高,但当同时提交大量任务时,可能会导致资源竞争和线程饥饿,进而影响系统性能。
+
+为避免这些问题,建议为 `CompletableFuture` 提供自定义线程池,带来以下优势:
+
+- 隔离性:为不同任务分配独立的线程池,避免全局线程池资源争夺。
+- 资源控制:根据任务特性调整线程池大小和队列类型,优化性能表现。
+- 异常处理:通过自定义 `ThreadFactory` 更好地处理线程中的异常情况。
```java
-public abstract class AbstractQueuedSynchronizer extends AbstractOwnableSynchronizer implements java.io.Serializable {
-}
+private ThreadPoolExecutor executor = new ThreadPoolExecutor(10, 10,
+ 0L, TimeUnit.MILLISECONDS,
+ new LinkedBlockingQueue());
+
+CompletableFuture.runAsync(() -> {
+ //...
+}, executor);
```
-AQS 为构建锁和同步器提供了一些通用功能的实现,因此,使用 AQS 能简单且高效地构造出应用广泛的大量的同步器,比如我们提到的 `ReentrantLock`,`Semaphore`,其他的诸如 `ReentrantReadWriteLock`,`SynchronousQueue`等等皆是基于 AQS 的。
+## AQS
+
+关于 AQS 源码的详细分析,可以看看这一篇文章:[AQS 详解](https://javaguide.cn/java/concurrent/aqs.html)。
+
+### AQS 是什么?
+
+AQS (`AbstractQueuedSynchronizer` ,抽象队列同步器)是从 JDK1.5 开始提供的 Java 并发核心组件。
+
+AQS 解决了开发者在实现同步器时的复杂性问题。它提供了一个通用框架,用于实现各种同步器,例如 **可重入锁**(`ReentrantLock`)、**信号量**(`Semaphore`)和 **倒计时器**(`CountDownLatch`)。通过封装底层的线程同步机制,AQS 将复杂的线程管理逻辑隐藏起来,使开发者只需专注于具体的同步逻辑。
+
+简单来说,AQS 是一个抽象类,为同步器提供了通用的 **执行框架**。它定义了 **资源获取和释放的通用流程**,而具体的资源获取逻辑则由具体同步器通过重写模板方法来实现。 因此,可以将 AQS 看作是同步器的 **基础“底座”**,而同步器则是基于 AQS 实现的 **具体“应用”**。
+
+### ⭐️AQS 的原理是什么?
+
+AQS 核心思想是,如果被请求的共享资源空闲,则将当前请求资源的线程设置为有效的工作线程,并且将共享资源设置为锁定状态。如果被请求的共享资源被占用,那么就需要一套线程阻塞等待以及被唤醒时锁分配的机制,这个机制 AQS 是基于 **CLH 锁** (Craig, Landin, and Hagersten locks) 进一步优化实现的。
+
+**CLH 锁** 对自旋锁进行了改进,是基于单链表的自旋锁。在多线程场景下,会将请求获取锁的线程组织成一个单向队列,每个等待的线程会通过自旋访问前一个线程节点的状态,前一个节点释放锁之后,当前节点才可以获取锁。**CLH 锁** 的队列结构如下图所示。
+
+
+
+AQS 中使用的 **等待队列** 是 CLH 锁队列的变体(接下来简称为 CLH 变体队列)。
-### AQS 的原理是什么?
+AQS 的 CLH 变体队列是一个双向队列,会暂时获取不到锁的线程将被加入到该队列中,CLH 变体队列和原本的 CLH 锁队列的区别主要有两点:
-AQS 核心思想是,如果被请求的共享资源空闲,则将当前请求资源的线程设置为有效的工作线程,并且将共享资源设置为锁定状态。如果被请求的共享资源被占用,那么就需要一套线程阻塞等待以及被唤醒时锁分配的机制,这个机制 AQS 是用 **CLH 队列锁** 实现的,即将暂时获取不到锁的线程加入到队列中。
+- 由 **自旋** 优化为 **自旋 + 阻塞** :自旋操作的性能很高,但大量的自旋操作比较占用 CPU 资源,因此在 CLH 变体队列中会先通过自旋尝试获取锁,如果失败再进行阻塞等待。
+- 由 **单向队列** 优化为 **双向队列** :在 CLH 变体队列中,会对等待的线程进行阻塞操作,当队列前边的线程释放锁之后,需要对后边的线程进行唤醒,因此增加了 `next` 指针,成为了双向队列。
-CLH(Craig,Landin,and Hagersten) 队列是一个虚拟的双向队列(虚拟的双向队列即不存在队列实例,仅存在结点之间的关联关系)。AQS 是将每条请求共享资源的线程封装成一个 CLH 锁队列的一个结点(Node)来实现锁的分配。在 CLH 同步队列中,一个节点表示一个线程,它保存着线程的引用(thread)、 当前节点在队列中的状态(waitStatus)、前驱节点(prev)、后继节点(next)。
+AQS 将每条请求共享资源的线程封装成一个 CLH 变体队列的一个结点(Node)来实现锁的分配。在 CLH 变体队列中,一个节点表示一个线程,它保存着线程的引用(thread)、 当前节点在队列中的状态(waitStatus)、前驱节点(prev)、后继节点(next)。
-CLH 队列结构如下图所示:
+AQS 中的 CLH 变体队列结构如下图所示:
-
+
-AQS(`AbstractQueuedSynchronizer`)的核心原理图(图源[Java 并发之 AQS 详解](https://www.cnblogs.com/waterystone/p/4920797.html))如下:
+AQS(`AbstractQueuedSynchronizer`)的核心原理图:
-
+
AQS 使用 **int 成员变量 `state` 表示同步状态**,通过内置的 **线程等待队列** 来完成获取资源线程的排队工作。
@@ -848,7 +1036,7 @@ protected final boolean compareAndSetState(int expect, int update) {
以 `ReentrantLock` 为例,`state` 初始值为 0,表示未锁定状态。A 线程 `lock()` 时,会调用 `tryAcquire()` 独占该锁并将 `state+1` 。此后,其他线程再 `tryAcquire()` 时就会失败,直到 A 线程 `unlock()` 到 `state=`0(即释放锁)为止,其它线程才有机会获取该锁。当然,释放锁之前,A 线程自己是可以重复获取此锁的(`state` 会累加),这就是可重入的概念。但要注意,获取多少次就要释放多少次,这样才能保证 state 是能回到零态的。
-再以 `CountDownLatch` 以例,任务分为 N 个子线程去执行,`state` 也初始化为 N(注意 N 要与线程个数一致)。这 N 个子线程是并行执行的,每个子线程执行完后`countDown()` 一次,state 会 CAS(Compare and Swap) 减 1。等到所有子线程都执行完后(即 `state=0` ),会 `unpark()` 主调用线程,然后主调用线程就会从 `await()` 函数返回,继续后余动作。
+再以 `CountDownLatch` 以例,任务分为 N 个子线程去执行,`state` 也初始化为 N(注意 N 要与线程个数一致)。这 N 个子线程是并行执行的,每个子线程执行完后`countDown()` 一次,state 会 CAS(Compare and Swap) 减 1。等到所有子线程都执行完后(即 `state=0` ),会 `unpark()` 主调用线程,然后主调用线程就会从 `await()` 函数返回,继续后续动作。
### Semaphore 有什么用?
@@ -1029,7 +1217,7 @@ CompletableFuture allFutures = CompletableFuture.allOf(
`CyclicBarrier` 和 `CountDownLatch` 非常类似,它也可以实现线程间的技术等待,但是它的功能比 `CountDownLatch` 更加复杂和强大。主要应用场景和 `CountDownLatch` 类似。
-> `CountDownLatch` 的实现是基于 AQS 的,而 `CycliBarrier` 是基于 `ReentrantLock`(`ReentrantLock` 也属于 AQS 同步器)和 `Condition` 的。
+> `CountDownLatch` 的实现是基于 AQS 的,而 `CyclicBarrier` 是基于 `ReentrantLock`(`ReentrantLock` 也属于 AQS 同步器)和 `Condition` 的。
`CyclicBarrier` 的字面意思是可循环使用(Cyclic)的屏障(Barrier)。它要做的事情是:让一组线程到达一个屏障(也可以叫同步点)时被阻塞,直到最后一个线程到达屏障时,屏障才会开门,所有被屏障拦截的线程才会继续干活。
@@ -1159,9 +1347,13 @@ public int await() throws InterruptedException, BrokenBarrierException {
## 虚拟线程
-虚拟线程在 Java 21 正式发布,这是一项重量级的更新。
+虚拟线程在 Java 21 正式发布,这是一项重量级的更新。虽然目前面试中问的不多,但还是建议大家去简单了解一下。我写了一篇文章来总结虚拟线程常见的问题:[虚拟线程常见问题总结](https://javaguide.cn/java/concurrent/virtual-thread.html),包含下面这些问题:
-虽然目前面试中问的不多,但还是建议大家去简单了解一下,具体可以阅读这篇文章:[虚拟线程极简入门](./virtual-thread.md) 。重点搞清楚虚拟线程和平台线程的关系以及虚拟线程的优势即可。
+1. 什么是虚拟线程?
+2. 虚拟线程和平台线程有什么关系?
+3. 虚拟线程有什么优点和缺点?
+4. 如何创建虚拟线程?
+5. 虚拟线程的底层原理是什么?
## 参考
diff --git a/docs/java/concurrent/java-thread-pool-best-practices.md b/docs/java/concurrent/java-thread-pool-best-practices.md
index e4a4dd3ab79..7bbc5592871 100644
--- a/docs/java/concurrent/java-thread-pool-best-practices.md
+++ b/docs/java/concurrent/java-thread-pool-best-practices.md
@@ -1,8 +1,13 @@
---
title: Java 线程池最佳实践
+description: Java线程池最佳实践总结:详解线程池参数配置、避免Executors工厂方法OOM风险、拒绝策略选择、线程池监控、线程命名规范等生产级实践。
category: Java
tag:
- Java并发
+head:
+ - - meta
+ - name: keywords
+ content: 线程池最佳实践,ThreadPoolExecutor配置,Executors陷阱,OOM风险,拒绝策略,线程池监控,线程命名
---
简单总结一下我了解的使用线程池的时候应该注意的东西,网上似乎还没有专门写这方面的文章。
@@ -13,7 +18,7 @@ tag:
`Executors` 返回线程池对象的弊端如下(后文会详细介绍到):
-- **`FixedThreadPool` 和 `SingleThreadExecutor`**:使用的是有界阻塞队列 `LinkedBlockingQueue`,任务队列的默认长度和最大长度为 `Integer.MAX_VALUE`,可能堆积大量的请求,从而导致 OOM。
+- **`FixedThreadPool` 和 `SingleThreadExecutor`**:使用的是阻塞队列 `LinkedBlockingQueue`,任务队列的默认长度和最大长度为 `Integer.MAX_VALUE`,可以看作是无界队列,可能堆积大量的请求,从而导致 OOM。
- **`CachedThreadPool`**:使用的是同步队列 `SynchronousQueue`,允许创建的线程数量为 `Integer.MAX_VALUE` ,可能会创建大量线程,从而导致 OOM。
- **`ScheduledThreadPool` 和 `SingleThreadScheduledExecutor`** : 使用的无界的延迟阻塞队列`DelayedWorkQueue`,任务队列最大长度为 `Integer.MAX_VALUE`,可能堆积大量的请求,从而导致 OOM。
@@ -136,15 +141,20 @@ public final class NamingThreadFactory implements ThreadFactory {
>
> Linux 相比与其他操作系统(包括其他类 Unix 系统)有很多的优点,其中有一项就是,其上下文切换和模式切换的时间消耗非常少。
-类比于实现世界中的人类通过合作做某件事情,我们可以肯定的一点是线程池大小设置过大或者过小都会有问题,合适的才是最好。
+类比于现实世界中的人类通过合作做某件事情,我们可以肯定的一点是线程池大小设置过大或者过小都会有问题,合适的才是最好。
- 如果我们设置的线程池数量太小的话,如果同一时间有大量任务/请求需要处理,可能会导致大量的请求/任务在任务队列中排队等待执行,甚至会出现任务队列满了之后任务/请求无法处理的情况,或者大量任务堆积在任务队列导致 OOM。这样很明显是有问题的,CPU 根本没有得到充分利用。
- 如果我们设置线程数量太大,大量线程可能会同时在争取 CPU 资源,这样会导致大量的上下文切换,从而增加线程的执行时间,影响了整体执行效率。
有一个简单并且适用面比较广的公式:
-- **CPU 密集型任务(N+1):** 这种任务消耗的主要是 CPU 资源,可以将线程数设置为 N(CPU 核心数)+1。比 CPU 核心数多出来的一个线程是为了防止线程偶发的缺页中断,或者其它原因导致的任务暂停而带来的影响。一旦任务暂停,CPU 就会处于空闲状态,而在这种情况下多出来的一个线程就可以充分利用 CPU 的空闲时间。
-- **I/O 密集型任务(2N):** 这种任务应用起来,系统会用大部分的时间来处理 I/O 交互,而线程在处理 I/O 的时间段内不会占用 CPU 来处理,这时就可以将 CPU 交出给其它线程使用。因此在 I/O 密集型任务的应用中,我们可以多配置一些线程,具体的计算方法是 2N。
+- **CPU 密集型任务 (N):** 这种任务消耗的主要是 CPU 资源,线程数应设置为 N(CPU 核心数)。由于任务主要瓶颈在于 CPU 计算能力,与核心数相等的线程数能够最大化 CPU 利用率,过多线程反而会导致竞争和上下文切换开销。
+- **I/O 密集型任务(M \* N):** 这类任务大部分时间处理 I/O 交互,线程在等待 I/O 时不占用 CPU。 为了充分利用 CPU 资源,线程数可以设置为 M \* N,其中 N 是 CPU 核心数,M 是一个大于 1 的倍数,建议默认设置为 2 ,具体取值取决于 I/O 等待时间和任务特点,需要通过测试和监控找到最佳平衡点。
+
+CPU 密集型任务不再推荐 N+1,原因如下:
+
+- "N+1" 的初衷是希望预留线程处理突发暂停,但实际上,处理缺页中断等情况仍然需要占用 CPU 核心。
+- CPU 密集场景下,CPU 始终是瓶颈,预留线程并不能凭空增加 CPU 处理能力,反而可能加剧竞争。
**如何判断是 CPU 密集任务还是 IO 密集任务?**
@@ -170,7 +180,7 @@ IO 密集型任务下,几乎全是线程等待时间,从理论上来说,
美团技术团队的思路是主要对线程池的核心参数实现自定义可配置。这三个核心参数是:
-- **`corePoolSize` :** 核心线程数线程数定义了最小可以同时运行的线程数量。
+- **`corePoolSize` :** 核心线程数定义了最小可以同时运行的线程数量。
- **`maximumPoolSize` :** 当队列中存放的任务达到队列容量的时候,当前可以同时运行的线程数量变为最大线程数。
- **`workQueue`:** 当新任务来的时候会先判断当前运行的线程数量是否达到核心线程数,如果达到的话,新任务就会被存放在队列中。
@@ -268,7 +278,7 @@ public class ThreadPoolExecutorConfig {
int maxPoolSize = (int) (processNum / (1 - 0.5));
threadPoolExecutor.setCorePoolSize(corePoolSize); // 核心池大小
threadPoolExecutor.setMaxPoolSize(maxPoolSize); // 最大线程数
- threadPoolExecutor.setQueueCapacity(maxPoolSize * 1000); // 队列程度
+ threadPoolExecutor.setQueueCapacity(maxPoolSize * 1000); // 队列长度
threadPoolExecutor.setThreadPriority(Thread.MAX_PRIORITY);
threadPoolExecutor.setDaemon(false);
threadPoolExecutor.setKeepAliveSeconds(300);// 线程空闲时间
diff --git a/docs/java/concurrent/java-thread-pool-summary.md b/docs/java/concurrent/java-thread-pool-summary.md
index c467ac1ca8e..dd261743f1c 100644
--- a/docs/java/concurrent/java-thread-pool-summary.md
+++ b/docs/java/concurrent/java-thread-pool-summary.md
@@ -1,25 +1,30 @@
---
title: Java 线程池详解
+description: Java线程池详解:深入讲解ThreadPoolExecutor核心参数配置、Executor框架体系、任务队列选择、拒绝策略、线程池工作原理及最佳实践。
category: Java
tag:
- Java并发
+head:
+ - - meta
+ - name: keywords
+ content: Java线程池,ThreadPoolExecutor,Executor框架,线程池参数,拒绝策略,任务队列,线程池原理
---
+
+
池化技术想必大家已经屡见不鲜了,线程池、数据库连接池、HTTP 连接池等等都是对这个思想的应用。池化技术的思想主要是为了减少每次获取资源的消耗,提高对资源的利用率。
这篇文章我会详细介绍一下线程池的基本概念以及核心原理。
## 线程池介绍
-顾名思义,线程池就是管理一系列线程的资源池,其提供了一种限制和管理线程资源的方式。每个线程池还维护一些基本统计信息,例如已完成任务的数量。
-
-这里借用《Java 并发编程的艺术》书中的部分内容来总结一下使用线程池的好处:
+池化技术想必大家已经屡见不鲜了,线程池、数据库连接池、HTTP 连接池等等都是对这个思想的应用。池化技术的思想主要是为了减少每次获取资源的消耗,提高对资源的利用率。
-- **降低资源消耗**。通过重复利用已创建的线程降低线程创建和销毁造成的消耗。
-- **提高响应速度**。当任务到达时,任务可以不需要等到线程创建就能立即执行。
-- **提高线程的可管理性**。线程是稀缺资源,如果无限制的创建,不仅会消耗系统资源,还会降低系统的稳定性,使用线程池可以进行统一的分配,调优和监控。
+线程池提供了一种限制和管理资源(包括执行一个任务)的方式。 每个线程池还维护一些基本统计信息,例如已完成任务的数量。使用线程池主要带来以下几个好处:
-**线程池一般用于执行多个不相关联的耗时任务,没有多线程的情况下,任务顺序执行,使用了线程池的话可让多个不相关联的任务同时执行。**
+1. **降低资源消耗**:线程池里的线程是可以重复利用的。一旦线程完成了某个任务,它不会立即销毁,而是回到池子里等待下一个任务。这就避免了频繁创建和销毁线程带来的开销。
+2. **提高响应速度**:因为线程池里通常会维护一定数量的核心线程(或者说“常驻工人”),任务来了之后,可以直接交给这些已经存在的、空闲的线程去执行,省去了创建线程的时间,任务能够更快地得到处理。
+3. **提高线程的可管理性**:线程池允许我们统一管理池中的线程。我们可以配置线程池的大小(核心线程数、最大线程数)、任务队列的类型和大小、拒绝策略等。这样就能控制并发线程的总量,防止资源耗尽,保证系统的稳定性。同时,线程池通常也提供了监控接口,方便我们了解线程池的运行状态(比如有多少活跃线程、多少任务在排队等),便于调优。
## Executor 框架介绍
@@ -160,11 +165,15 @@ public static class CallerRunsPolicy implements RejectedExecutionHandler {
### 线程池创建的两种方式
-**方式一:通过`ThreadPoolExecutor`构造函数来创建(推荐)。**
+在 Java 中,创建线程池主要有两种方式:
+
+**方式一:通过 `ThreadPoolExecutor` 构造函数直接创建 (推荐)**
-
+
-**方式二:通过 `Executor` 框架的工具类 `Executors` 来创建。**
+这是最推荐的方式,因为它允许开发者明确指定线程池的核心参数,对线程池的运行行为有更精细的控制,从而避免资源耗尽的风险。
+
+**方式二:通过 `Executors` 工具类创建 (不推荐用于生产环境)**
`Executors`工具类提供的创建线程池的方法如下图所示:
@@ -181,21 +190,19 @@ public static class CallerRunsPolicy implements RejectedExecutionHandler {
`Executors` 返回线程池对象的弊端如下(后文会详细介绍到):
-- `FixedThreadPool` 和 `SingleThreadExecutor`:使用的是无界的 `LinkedBlockingQueue`,任务队列最大长度为 `Integer.MAX_VALUE`,可能堆积大量的请求,从而导致 OOM。
+- `FixedThreadPool` 和 `SingleThreadExecutor`:使用的是阻塞队列 `LinkedBlockingQueue`,任务队列最大长度为 `Integer.MAX_VALUE`,可以看作是无界的,可能堆积大量的请求,从而导致 OOM。
- `CachedThreadPool`:使用的是同步队列 `SynchronousQueue`, 允许创建的线程数量为 `Integer.MAX_VALUE` ,如果任务数量过多且执行速度较慢,可能会创建大量的线程,从而导致 OOM。
- `ScheduledThreadPool` 和 `SingleThreadScheduledExecutor`:使用的无界的延迟阻塞队列`DelayedWorkQueue`,任务队列最大长度为 `Integer.MAX_VALUE`,可能堆积大量的请求,从而导致 OOM。
```java
-// 无界队列 LinkedBlockingQueue
public static ExecutorService newFixedThreadPool(int nThreads) {
-
+ // LinkedBlockingQueue 的默认长度为 Integer.MAX_VALUE,可以看作是无界的
return new ThreadPoolExecutor(nThreads, nThreads,0L, TimeUnit.MILLISECONDS,new LinkedBlockingQueue());
}
-// 无界队列 LinkedBlockingQueue
public static ExecutorService newSingleThreadExecutor() {
-
+ // LinkedBlockingQueue 的默认长度为 Integer.MAX_VALUE,可以看作是无界的
return new FinalizableDelegatedExecutorService (new ThreadPoolExecutor(1, 1,0L, TimeUnit.MILLISECONDS,new LinkedBlockingQueue()));
}
@@ -522,7 +529,7 @@ Finished all threads // 任务全部执行完了才会跳出来,因为executo
}
```
-更多关于线程池源码分析的内容推荐这篇文章:硬核干货:[4W 字从源码上分析 JUC 线程池 ThreadPoolExecutor 的实现原理](https://www.throwx.cn/2020/08/23/java-concurrency-thread-pool-executor/)
+更多关于线程池源码分析的内容推荐这篇文章:硬核干货:[4W 字从源码上分析 JUC 线程池 ThreadPoolExecutor 的实现原理](https://www.cnblogs.com/throwable/p/13574306.html)。
现在,让我们在回到示例代码, 现在应该是不是很容易就可以搞懂它的原理了呢?
diff --git a/docs/java/concurrent/jmm.md b/docs/java/concurrent/jmm.md
index bdba79d816f..578381714cf 100644
--- a/docs/java/concurrent/jmm.md
+++ b/docs/java/concurrent/jmm.md
@@ -1,20 +1,20 @@
---
title: JMM(Java 内存模型)详解
+description: 深入解析Java内存模型JMM:详解CPU缓存模型、指令重排序机制、happens-before原则、内存可见性保证,理解多线程并发编程的底层规范。
category: Java
tag:
- Java并发
head:
- - meta
- name: keywords
- content: CPU 缓存模型,指令重排序,Java 内存模型(JMM),happens-before
- - - meta
- - name: description
- content: 对于 Java 来说,你可以把 JMM 看作是 Java 定义的并发编程相关的一组规范,除了抽象了线程和主内存之间的关系之外,其还规定了从 Java 源代码到 CPU 可执行指令的这个转化过程要遵守哪些和并发相关的原则和规范,其主要目的是为了简化多线程编程,增强程序可移植性的。
+ content: JMM,Java内存模型,CPU缓存,指令重排序,happens-before,内存可见性,并发编程模型
---
-JMM(Java 内存模型)主要定义了对于一个共享变量,当另一个线程对这个共享变量执行写操作后,这个线程对这个共享变量的可见性。
+对于 Java 来说,你可以把 **JMM(Java 内存模型)** 看作是 Java 定义的并发编程相关的一组规范。除了抽象了线程和主内存之间的关系之外,其还规定了从 Java 源代码到 CPU 可执行指令的转化过程要遵守哪些并发相关的原则和规范。其主要目的是为了**简化多线程编程**,**增强程序的可移植性**。
+
+JMM 主要定义了对于一个共享变量,当一个线程执行写操作后,该变量对其他线程的**可见性**。
-要想理解透彻 JMM(Java 内存模型),我们先要从 **CPU 缓存模型和指令重排序** 说起!
+要想透彻理解 JMM,我们需要从 **CPU 缓存模型**和**指令重排序**说起。
## 从 CPU 缓存模型说起
@@ -61,9 +61,13 @@ Java 源代码会经历 **编译器优化重排 —> 指令并行重排 —> 内
**指令重排序可以保证串行语义一致,但是没有义务保证多线程间的语义也一致** ,所以在多线程下,指令重排序可能会导致一些问题。
-编译器和处理器的指令重排序的处理方式不一样。对于编译器,通过禁止特定类型的编译器重排序的方式来禁止重排序。对于处理器,通过插入内存屏障(Memory Barrier,或有时叫做内存栅栏,Memory Fence)的方式来禁止特定类型的处理器重排序。指令并行重排和内存系统重排都属于是处理器级别的指令重排序。
+对于编译器优化重排和处理器的指令重排序(指令并行重排和内存系统重排都属于是处理器级别的指令重排序),处理该问题的方式不一样。
+
+- 对于编译器,通过禁止特定类型的编译器重排序的方式来禁止重排序。
-> 内存屏障(Memory Barrier,或有时叫做内存栅栏,Memory Fence)是一种 CPU 指令,用来禁止处理器指令发生重排序(像屏障一样),从而保障指令执行的有序性。另外,为了达到屏障的效果,它也会使处理器写入、读取值之前,将主内存的值写入高速缓存,清空无效队列,从而保障变量的可见性。
+- 对于处理器,通过插入内存屏障(Memory Barrier,或有时叫做内存栅栏,Memory Fence)的方式来禁止特定类型的处理器重排序。
+
+> 内存屏障(Memory Barrier,或有时叫做内存栅栏,Memory Fence)是一种 CPU 指令,用来禁止处理器指令发生重排序(像屏障一样),从而保障指令执行的有序性。另外,为了达到屏障的效果,它会在处理器写入值时,强制将写缓冲区中的数据刷新到主内存;在读取值之前,使处理器本地缓存中的相关数据失效,强制从主内存中加载最新值,从而保障变量的可见性。
## JMM(Java Memory Model)
@@ -90,7 +94,7 @@ JMM 说白了就是定义了一些规范来解决这些问题,开发者可以
**什么是主内存?什么是本地内存?**
- **主内存**:所有线程创建的实例对象都存放在主内存中,不管该实例对象是成员变量,还是局部变量,类信息、常量、静态变量都是放在主内存中。为了获取更好的运行速度,虚拟机及硬件系统可能会让工作内存优先存储于寄存器和高速缓存中。
-- **本地内存**:每个线程都有一个私有的本地内存,本地内存存储了该线程以读 / 写共享变量的副本。每个线程只能操作自己本地内存中的变量,无法直接访问其他线程的本地内存。如果线程间需要通信,必须通过主内存来进行。本地内存是 JMM 抽象出来的一个概念,并不真实存在,它涵盖了缓存、写缓冲区、寄存器以及其他的硬件和编译器优化。
+- **本地内存**:每个线程都有一个私有的本地内存,本地内存存储了该线程已读 / 写共享变量的副本。每个线程只能操作自己本地内存中的变量,无法直接访问其他线程的本地内存。如果线程间需要通信,必须通过主内存来进行。本地内存是 JMM 抽象出来的一个概念,并不真实存在,它涵盖了缓存、写缓冲区、寄存器以及其他的硬件和编译器优化。
Java 内存模型的抽象示意图如下:
@@ -148,9 +152,9 @@ JSR 133 引入了 happens-before 这个概念来描述两个操作之间的内
- 为了对编译器和处理器的约束尽可能少,只要不改变程序的执行结果(单线程程序和正确执行的多线程程序),编译器和处理器怎么进行重排序优化都行。
- 对于会改变程序执行结果的重排序,JMM 要求编译器和处理器必须禁止这种重排序。
-下面这张是 《Java 并发编程的艺术》这本书中的一张 JMM 设计思想的示意图,非常清晰。
+下面这张是我根据 《Java 并发编程的艺术》这本书中的一张 JMM 设计思想示意图重新绘制的。
-
+
了解了 happens-before 原则的设计思想,我们再来看看 JSR-133 对 happens-before 原则的定义:
@@ -189,9 +193,15 @@ happens-before 的规则就 8 条,说多不多,重点了解下面列举的 5
### happens-before 和 JMM 什么关系?
-happens-before 与 JMM 的关系用《Java 并发编程的艺术》这本书中的一张图就可以非常好的解释清楚。
+happens-before 与 JMM 的关系如下图所示:
+
+
+
+- JMM 向程序员提供了 **“ happens-before 规则 ”**(如程序顺序规则、`volatile` 变量规则等)。这是一种 **“ 强内存模型 ”** 的假象:程序员不需要关心底层复杂的重排序细节,只需要按照这些规则编写代码,就能保证多线程下的内存可见性。
+- JVM 在执行时,会将 happens-before 规则映射到具体的实现上。为了在保证正确性的前提下不丧失性能,JMM 只会 **“ 禁止影响执行结果的重排序 ”**。对于不影响单线程执行结果的重排序,JMM 是允许的。
+- 最底层是编译器和处理器真实的 **“ 重排序规则 ”**。
-
+总结来说,JMM 就像是一个中间层:它向上通过 happens-before 为程序员提供简单的编程模型;向下通过禁止特定重排序,利用底层硬件性能。这种设计既保证了多线程的安全性,又最大限度释放了硬件的性能。
## 再看并发编程三个重要特性
diff --git a/docs/java/concurrent/optimistic-lock-and-pessimistic-lock.md b/docs/java/concurrent/optimistic-lock-and-pessimistic-lock.md
index 116bb43f00b..ebbb8537cd7 100644
--- a/docs/java/concurrent/optimistic-lock-and-pessimistic-lock.md
+++ b/docs/java/concurrent/optimistic-lock-and-pessimistic-lock.md
@@ -1,11 +1,16 @@
---
title: 乐观锁和悲观锁详解
+description: 乐观锁与悲观锁深度对比:详解synchronized/ReentrantLock悲观锁实现、CAS/版本号乐观锁机制、适用场景分析、性能对比与选型建议。
category: Java
tag:
- Java并发
+head:
+ - - meta
+ - name: keywords
+ content: 乐观锁,悲观锁,synchronized,ReentrantLock,CAS,版本号机制,并发控制,锁优化
---
-如果将悲观锁(Pessimistic Lock)和乐观锁(PessimisticLock 或 OptimisticLock)对应到现实生活中来。悲观锁有点像是一位比较悲观(也可以说是未雨绸缪)的人,总是会假设最坏的情况,避免出现问题。乐观锁有点像是一位比较乐观的人,总是会假设最好的情况,在要出现问题之前快速解决问题。
+如果将悲观锁(Pessimistic Lock)和乐观锁(Optimistic Lock)对应到现实生活中来。悲观锁有点像是一位比较悲观(也可以说是未雨绸缪)的人,总是会假设最坏的情况,避免出现问题。乐观锁有点像是一位比较乐观的人,总是会假设最好的情况,在要出现问题之前快速解决问题。
## 什么是悲观锁?
@@ -67,7 +72,7 @@ sum.increment();
1. 操作员 A 此时将其读出( `version`=1 ),并从其帐户余额中扣除 $50( $100-\$50 )。
2. 在操作员 A 操作的过程中,操作员 B 也读入此用户信息( `version`=1 ),并从其帐户余额中扣除 $20 ( $100-\$20 )。
3. 操作员 A 完成了修改工作,将数据版本号( `version`=1 ),连同帐户扣除后余额( `balance`=\$50 ),提交至数据库更新,此时由于提交数据版本等于数据库记录当前版本,数据被更新,数据库记录 `version` 更新为 2 。
-4. 操作员 B 完成了操作,也将版本号( `version`=1 )试图向数据库提交数据( `balance`=\$80 ),但此时比对数据库记录版本时发现,操作员 B 提交的数据版本号为 1 ,数据库记录当前版本也为 2 ,不满足 “ 提交版本必须等于当前版本才能执行更新 “ 的乐观锁策略,因此,操作员 B 的提交被驳回。
+4. 操作员 B 完成了操作,也将版本号( `version`=1 )试图向数据库提交数据( `balance`=\$80 ),但此时比对数据库记录版本时发现,操作员 B 提交的数据版本号为 1 ,而数据库记录当前版本为 2 ,不满足 “ 提交版本必须等于当前版本才能执行更新 “ 的乐观锁策略,因此,操作员 B 的提交被驳回。
这样就避免了操作员 B 用基于 `version`=1 的旧数据修改的结果覆盖操作员 A 的操作结果的可能。
diff --git a/docs/java/concurrent/reentrantlock.md b/docs/java/concurrent/reentrantlock.md
index ef1cd38625c..7e4490057c9 100644
--- a/docs/java/concurrent/reentrantlock.md
+++ b/docs/java/concurrent/reentrantlock.md
@@ -1,8 +1,13 @@
---
title: 从ReentrantLock的实现看AQS的原理及应用
+description: ReentrantLock与AQS原理深度解析:详解ReentrantLock可重入锁实现、公平锁与非公平锁区别、基于AQS的加锁解锁流程、与synchronized性能对比。
category: Java
tag:
- Java并发
+head:
+ - - meta
+ - name: keywords
+ content: ReentrantLock,AQS,公平锁,非公平锁,可重入锁,lock unlock,ReentrantLock原理,synchronized对比
---
> 本文转载自:
@@ -503,9 +508,9 @@ private void setHead(Node node) {
// 靠前驱节点判断当前线程是否应该被阻塞
private static boolean shouldParkAfterFailedAcquire(Node pred, Node node) {
- // 获取头结点的节点状态
+ // 获取前驱结点的节点状态
int ws = pred.waitStatus;
- // 说明头结点处于唤醒状态
+ // 说明前驱结点处于唤醒状态
if (ws == Node.SIGNAL)
return true;
// 通过枚举值我们知道waitStatus>0是取消状态
diff --git a/docs/java/concurrent/threadlocal.md b/docs/java/concurrent/threadlocal.md
index 0cdaf0adfd6..5a92034bbb0 100644
--- a/docs/java/concurrent/threadlocal.md
+++ b/docs/java/concurrent/threadlocal.md
@@ -1,8 +1,13 @@
---
title: ThreadLocal 详解
+description: ThreadLocal深度解析:详解ThreadLocal线程本地变量原理、ThreadLocalMap实现机制、弱引用与内存泄漏问题、使用场景与最佳实践。
category: Java
tag:
- Java并发
+head:
+ - - meta
+ - name: keywords
+ content: ThreadLocal,线程本地变量,ThreadLocalMap,内存泄漏,弱引用,ThreadLocal原理,线程隔离
---
> 本文来自一枝花算不算浪漫投稿, 原文地址:[https://juejin.cn/post/6844904151567040519](https://juejin.cn/post/6844904151567040519)。
diff --git a/docs/java/concurrent/virtual-thread.md b/docs/java/concurrent/virtual-thread.md
index f7f889fb81f..c4dee66c5ec 100644
--- a/docs/java/concurrent/virtual-thread.md
+++ b/docs/java/concurrent/virtual-thread.md
@@ -1,8 +1,13 @@
---
title: 虚拟线程常见问题总结
+description: Java 21虚拟线程详解:全面解析Virtual Threads虚拟线程原理、与平台线程区别、Project Loom项目、适用IO密集型场景、使用注意事项与最佳实践。
category: Java
tag:
- Java并发
+head:
+ - - meta
+ - name: keywords
+ content: Java虚拟线程,Virtual Threads,Project Loom,Java 21新特性,轻量级线程,协程,虚拟线程原理
---
> 本文部分内容来自 [Lorin](https://github.com/Lorin-github) 的[PR](https://github.com/Snailclimb/JavaGuide/pull/2190)。
diff --git a/docs/java/io/io-basis.md b/docs/java/io/io-basis.md
index 0dd36b1c32d..4f5bd2f4232 100755
--- a/docs/java/io/io-basis.md
+++ b/docs/java/io/io-basis.md
@@ -1,9 +1,14 @@
---
title: Java IO 基础知识总结
+description: Java IO基础知识全面总结:详解字节流与字符流区别、InputStream/OutputStream字节流、Reader/Writer字符流、缓冲流优化、文件读写操作。
category: Java
tag:
- Java IO
- Java基础
+head:
+ - - meta
+ - name: keywords
+ content: Java IO,字节流,字符流,InputStream,OutputStream,Reader,Writer,文件操作,缓冲流
---
@@ -184,7 +189,9 @@ The content read from file:§å®¶å¥½
因此,I/O 流就干脆提供了一个直接操作字符的接口,方便我们平时对字符进行流操作。如果音频文件、图片等媒体文件用字节流比较好,如果涉及到字符的话使用字符流比较好。
-字符流默认采用的是 `Unicode` 编码,我们可以通过构造方法自定义编码。顺便分享一下之前遇到的笔试题:常用字符编码所占字节数?`utf8` :英文占 1 字节,中文占 3 字节,`unicode`:任何字符都占 2 个字节,`gbk`:英文占 1 字节,中文占 2 字节。
+字符流默认采用的是 `Unicode` 编码,我们可以通过构造方法自定义编码。
+
+Unicode 本身只是一种字符集,它为每个字符分配一个唯一的数字编号,并没有规定具体的存储方式。UTF-8、UTF-16、UTF-32 都是 Unicode 的编码方式,它们使用不同的字节数来表示 Unicode 字符。例如,UTF-8 :英文占 1 字节,中文占 3 字节。
### Reader(字符输入流)
@@ -428,7 +435,7 @@ class BufferedInputStream extends FilterInputStream {
### BufferedOutputStream(字节缓冲输出流)
-`BufferedOutputStream` 将数据(字节信息)写入到目的地(通常是文件)的过程中不会一个字节一个字节的写入,而是会先将要写入的字节存放在缓存区,并从内部缓冲区中单独写入字节。这样大幅减少了 IO 次数,提高了读取效率
+`BufferedOutputStream` 将数据(字节信息)写入到目的地(通常是文件)的过程中不会一个字节一个字节的写入,而是会先将要写入的字节存放在缓存区,并从内部缓冲区中单独写入字节。这样大幅减少了 IO 次数,提高了效率
```java
try (BufferedOutputStream bos = new BufferedOutputStream(new FileOutputStream("output.txt"))) {
diff --git a/docs/java/io/io-design-patterns.md b/docs/java/io/io-design-patterns.md
index 5408c06049b..616130530ce 100644
--- a/docs/java/io/io-design-patterns.md
+++ b/docs/java/io/io-design-patterns.md
@@ -1,9 +1,14 @@
---
title: Java IO 设计模式总结
+description: Java IO设计模式深度解析:详解装饰器模式在BufferedInputStream中应用、适配器模式InputStreamReader实现、模板方法模式InputStream设计,理解Java IO类库架构。
category: Java
tag:
- Java IO
- Java基础
+head:
+ - - meta
+ - name: keywords
+ content: Java IO设计模式,装饰器模式,适配器模式,模板方法模式,FilterInputStream,IO流设计
---
这篇文章我们简单来看看我们从 IO 中能够学习到哪些设计模式的应用。
@@ -118,8 +123,8 @@ BufferedReader bufferedReader = new BufferedReader(isr);
```java
public class InputStreamReader extends Reader {
- //用于解码的对象
- private final StreamDecoder sd;
+ //用于解码的对象
+ private final StreamDecoder sd;
public InputStreamReader(InputStream in) {
super(in);
try {
@@ -130,7 +135,7 @@ public class InputStreamReader extends Reader {
}
}
// 使用 StreamDecoder 对象做具体的读取工作
- public int read() throws IOException {
+ public int read() throws IOException {
return sd.read();
}
}
diff --git a/docs/java/io/io-model.md b/docs/java/io/io-model.md
index e6d48bc0439..3b24d33b90b 100644
--- a/docs/java/io/io-model.md
+++ b/docs/java/io/io-model.md
@@ -1,9 +1,14 @@
---
title: Java IO 模型详解
+description: Java IO模型详解:深入剖析BIO阻塞IO、NIO非阻塞IO、AIO异步IO三种模型、多路复用机制、Reactor/Proactor模式、同步异步阻塞非阻塞概念辨析。
category: Java
tag:
- Java IO
- Java基础
+head:
+ - - meta
+ - name: keywords
+ content: Java IO模型,BIO,NIO,AIO,阻塞IO,非阻塞IO,多路复用,Reactor模式,Proactor模式
---
IO 模型这块确实挺难理解的,需要太多计算机底层知识。写这篇文章用了挺久,就非常希望能把我所知道的讲出来吧!希望朋友们能有收获!为了写这篇文章,还翻看了一下《UNIX 网络编程》这本书,太难了,我滴乖乖!心痛~
@@ -87,6 +92,9 @@ Java 中的 NIO 可以看作是 **I/O 多路复用模型**。也有很多人认
相比于同步阻塞 IO 模型,同步非阻塞 IO 模型确实有了很大改进。通过轮询操作,避免了一直阻塞。
+> 同步非阻塞 IO,发起一个 read 调用,如果数据没有准备好,这个时候应用程序可以不阻塞等待,而是切换去做一些小的计算任务,然后很快回来继续发起 read 调用,也就是轮询。这个
+> 轮询不是持续不断发起的,会有间隙, 这个间隙的利用就是同步非阻塞 IO 比同步阻塞 IO 高效的地方。
+
但是,这种 IO 模型同样存在问题:**应用程序不断进行 I/O 系统调用轮询数据是否已经准备好的过程是十分消耗 CPU 资源的。**
这个时候,**I/O 多路复用模型** 就上场了。
diff --git a/docs/java/io/nio-basis.md b/docs/java/io/nio-basis.md
index 4cf9723ba37..485e9232584 100644
--- a/docs/java/io/nio-basis.md
+++ b/docs/java/io/nio-basis.md
@@ -1,9 +1,14 @@
---
title: Java NIO 核心知识总结
+description: Java NIO核心知识全面总结:详解Channel通道、Buffer缓冲区、Selector选择器三大核心组件、非阻塞IO实现、零拷贝技术、与传统IO性能对比。
category: Java
tag:
- Java IO
- Java基础
+head:
+ - - meta
+ - name: keywords
+ content: Java NIO,Channel,Buffer,Selector,非阻塞IO,多路复用,零拷贝,NIO核心组件
---
在学习 NIO 之前,需要先了解一下计算机 I/O 模型的基础理论知识。还不了解的话,可以参考我写的这篇文章:[Java IO 模型详解](https://javaguide.cn/java/io/io-model.html)。
diff --git a/docs/java/jvm/class-file-structure.md b/docs/java/jvm/class-file-structure.md
index 61a6c00dab9..cb778a85359 100644
--- a/docs/java/jvm/class-file-structure.md
+++ b/docs/java/jvm/class-file-structure.md
@@ -1,8 +1,13 @@
---
title: 类文件结构详解
+description: 介绍 Java 字节码 Class 文件结构与常量池等核心组成,辅助理解编译产物。
category: Java
tag:
- JVM
+head:
+ - - meta
+ - name: keywords
+ content: Class 文件,常量池,魔数,版本,字段,方法,属性
---
## 回顾一下字节码
@@ -73,7 +78,7 @@ ClassFile {
每当 Java 发布大版本(比如 Java 8,Java9)的时候,主版本号都会加 1。你可以使用 `javap -v` 命令来快速查看 Class 文件的版本号信息。
-高版本的 Java 虚拟机可以执行低版本编译器生成的 Class 文件,但是低版本的 Java 虚拟机不能执行高版本编译器生成的 Class 文件。所以,我们在实际开发的时候要确保开发的的 JDK 版本和生产环境的 JDK 版本保持一致。
+高版本的 Java 虚拟机可以执行低版本编译器生成的 Class 文件,但是低版本的 Java 虚拟机不能执行高版本编译器生成的 Class 文件。所以,我们在实际开发的时候要确保开发的 JDK 版本和生产环境的 JDK 版本保持一致。
### 常量池(Constant Pool)
@@ -84,7 +89,7 @@ ClassFile {
紧接着主次版本号之后的是常量池,常量池的数量是 `constant_pool_count-1`(**常量池计数器是从 1 开始计数的,将第 0 项常量空出来是有特殊考虑的,索引值为 0 代表“不引用任何一个常量池项”**)。
-常量池主要存放两大常量:字面量和符号引用。字面量比较接近于 Java 语言层面的的常量概念,如文本字符串、声明为 final 的常量值等。而符号引用则属于编译原理方面的概念。包括下面三类常量:
+常量池主要存放两大常量:字面量和符号引用。字面量比较接近于 Java 语言层面的常量概念,如文本字符串、声明为 final 的常量值等。而符号引用则属于编译原理方面的概念。包括下面三类常量:
- 类和接口的全限定名
- 字段的名称和描述符
@@ -149,7 +154,7 @@ Java 类的继承关系由类索引、父类索引和接口索引集合三项确
类索引用于确定这个类的全限定名,父类索引用于确定这个类的父类的全限定名,由于 Java 语言的单继承,所以父类索引只有一个,除了 `java.lang.Object` 之外,所有的 Java 类都有父类,因此除了 `java.lang.Object` 外,所有 Java 类的父类索引都不为 0。
-接口索引集合用来描述这个类实现了那些接口,这些被实现的接口将按 `implements` (如果这个类本身是接口的话则是`extends`) 后的接口顺序从左到右排列在接口索引集合中。
+接口索引集合用来描述这个类实现了哪些接口,这些被实现的接口将按 `implements` (如果这个类本身是接口的话则是`extends`) 后的接口顺序从左到右排列在接口索引集合中。
### 字段表集合(Fields)
@@ -174,7 +179,7 @@ Java 类的继承关系由类索引、父类索引和接口索引集合三项确
**字段的 access_flag 的取值:**
-
+
### 方法表集合(Methods)
@@ -193,7 +198,7 @@ Class 文件存储格式中对方法的描述与对字段的描述几乎采用
**方法表的 access_flag 取值:**
-
+
注意:因为`volatile`修饰符和`transient`修饰符不可以修饰方法,所以方法表的访问标志中没有这两个对应的标志,但是增加了`synchronized`、`native`、`abstract`等关键字修饰方法,所以也就多了这些关键字对应的标志。
diff --git a/docs/java/jvm/class-loading-process.md b/docs/java/jvm/class-loading-process.md
index 4cdb596581f..fa23fb178f2 100644
--- a/docs/java/jvm/class-loading-process.md
+++ b/docs/java/jvm/class-loading-process.md
@@ -1,8 +1,13 @@
---
title: 类加载过程详解
+description: 拆解 JVM 类加载的各阶段与关键细节,理解验证、准备、解析与初始化的具体行为。
category: Java
tag:
- JVM
+head:
+ - - meta
+ - name: keywords
+ content: 类加载,加载,验证,准备,解析,初始化,clinit,常量池
---
## 类的生命周期
@@ -33,7 +38,7 @@ tag:
虚拟机规范上面这 3 点并不具体,因此是非常灵活的。比如:"通过全类名获取定义此类的二进制字节流" 并没有指明具体从哪里获取( `ZIP`、 `JAR`、`EAR`、`WAR`、网络、动态代理技术运行时动态生成、其他文件生成比如 `JSP`...)、怎样获取。
-加载这一步主要是通过我们后面要讲到的 **类加载器** 完成的。类加载器有很多种,当我们想要加载一个类的时候,具体是哪个类加载器加载由 **双亲委派模型** 决定(不过,我们也能打破由双亲委派模型)。
+加载这一步主要是通过我们后面要讲到的 **类加载器** 完成的。类加载器有很多种,当我们想要加载一个类的时候,具体是哪个类加载器加载由 **双亲委派模型** 决定(不过,我们也能打破双亲委派模型)。
> 类加载器、双亲委派模型也是非常重要的知识点,这部分内容在[类加载器详解](https://javaguide.cn/java/jvm/classloader.html "类加载器详解")这篇文章中有详细介绍到。阅读本篇文章的时候,大家知道有这么个东西就可以了。
@@ -49,7 +54,7 @@ tag:
验证阶段这一步在整个类加载过程中耗费的资源还是相对较多的,但很有必要,可以有效防止恶意代码的执行。任何时候,程序安全都是第一位。
-不过,验证阶段也不是必须要执行的阶段。如果程序运行的全部代码(包括自己编写的、第三方包中的、从外部加载的、动态生成的等所有代码)都已经被反复使用和验证过,在生产环境的实施阶段就可以考虑使用 `-Xverify:none` 参数来关闭大部分的类验证措施,以缩短虚拟机类加载的时间。
+不过,验证阶段也不是必须要执行的阶段。如果程序运行的全部代码(包括自己编写的、第三方包中的、从外部加载的、动态生成的等所有代码)都已经被反复使用和验证过,在生产环境的实施阶段就可以考虑使用 `-Xverify:none` 参数来关闭大部分的类验证措施,以缩短虚拟机类加载的时间。但是需要注意的是 `-Xverify:none` 和 `-noverify` 在 JDK 13 中被标记为 deprecated ,在未来版本的 JDK 中可能会被移除。
验证阶段主要由四个检验阶段组成:
@@ -83,7 +88,7 @@ tag:
2. 从概念上讲,类变量所使用的内存都应当在 **方法区** 中进行分配。不过有一点需要注意的是:JDK 7 之前,HotSpot 使用永久代来实现方法区的时候,实现是完全符合这种逻辑概念的。 而在 JDK 7 及之后,HotSpot 已经把原本放在永久代的字符串常量池、静态变量等移动到堆中,这个时候类变量则会随着 Class 对象一起存放在 Java 堆中。相关阅读:[《深入理解 Java 虚拟机(第 3 版)》勘误#75](https://github.com/fenixsoft/jvm_book/issues/75 "《深入理解Java虚拟机(第3版)》勘误#75")
3. 这里所设置的初始值"通常情况"下是数据类型默认的零值(如 0、0L、null、false 等),比如我们定义了`public static int value=111` ,那么 value 变量在准备阶段的初始值就是 0 而不是 111(初始化阶段才会赋值)。特殊情况:比如给 value 变量加上了 final 关键字`public static final int value=111` ,那么准备阶段 value 的值就被赋值为 111。
-**基本数据类型的零值**:(图片来自《深入理解 Java 虚拟机》第 3 版 7.33 )
+**基本数据类型的零值**:(图片来自《深入理解 Java 虚拟机》第 3 版 7.3.3 )

@@ -91,7 +96,7 @@ tag:
**解析阶段是虚拟机将常量池内的符号引用替换为直接引用的过程。** 解析动作主要针对类或接口、字段、类方法、接口方法、方法类型、方法句柄和调用限定符 7 类符号引用进行。
-《深入理解 Java 虚拟机》7.34 节第三版对符号引用和直接引用的解释如下:
+《深入理解 Java 虚拟机》7.3.4 节第三版对符号引用和直接引用的解释如下:

@@ -109,16 +114,14 @@ tag:
对于初始化阶段,虚拟机严格规范了有且只有 6 种情况下,必须对类进行初始化(只有主动去使用类才会初始化类):
-1. 当遇到 `new`、 `getstatic`、`putstatic` 或 `invokestatic` 这 4 条字节码指令时,比如 `new` 一个类,读取一个静态字段(未被 final 修饰)、或调用一个类的静态方法时。
- - 当 jvm 执行 `new` 指令时会初始化类。即当程序创建一个类的实例对象。
- - 当 jvm 执行 `getstatic` 指令时会初始化类。即程序访问类的静态变量(不是静态常量,常量会被加载到运行时常量池)。
- - 当 jvm 执行 `putstatic` 指令时会初始化类。即程序给类的静态变量赋值。
- - 当 jvm 执行 `invokestatic` 指令时会初始化类。即程序调用类的静态方法。
+1. 遇到 `new`、`getstatic`、`putstatic` 或 `invokestatic` 这 4 条字节码指令时:
+ - `new`: 创建一个类的实例对象。
+ - `getstatic`、`putstatic`: 读取或设置一个类型的静态字段(被 `final` 修饰、已在编译期把结果放入常量池的静态字段除外)。
+ - `invokestatic`: 调用类的静态方法。
2. 使用 `java.lang.reflect` 包的方法对类进行反射调用时如 `Class.forName("...")`, `newInstance()` 等等。如果类没初始化,需要触发其初始化。
3. 初始化一个类,如果其父类还未初始化,则先触发该父类的初始化。
4. 当虚拟机启动时,用户需要定义一个要执行的主类 (包含 `main` 方法的那个类),虚拟机会先初始化这个类。
-5. `MethodHandle` 和 `VarHandle` 可以看作是轻量级的反射调用机制,而要想使用这 2 个调用,
- 就必须先使用 `findStaticVarHandle` 来初始化要调用的类。
+5. `MethodHandle` 和 `VarHandle` 可以看作是轻量级的反射调用机制,而要想使用这 2 个调用,就必须先使用 `findStaticVarHandle` 来初始化要调用的类。
6. **「补充,来自[issue745](https://github.com/Snailclimb/JavaGuide/issues/745 "issue745")」** 当一个接口中定义了 JDK8 新加入的默认方法(被 default 关键字修饰的接口方法)时,如果有这个接口的实现类发生了初始化,那该接口要在其之前被初始化。
## 类卸载
diff --git a/docs/java/jvm/classloader.md b/docs/java/jvm/classloader.md
index 97e12e86d60..1458ec1c504 100644
--- a/docs/java/jvm/classloader.md
+++ b/docs/java/jvm/classloader.md
@@ -1,8 +1,13 @@
---
title: 类加载器详解(重点)
+description: Java类加载器详解:深入剖析ClassLoader类加载机制、双亲委派模型原理、启动类加载器/扩展类加载器/应用类加载器、自定义类加载器实现、打破双亲委派场景。
category: Java
tag:
- JVM
+head:
+ - - meta
+ - name: keywords
+ content: 类加载器,ClassLoader,双亲委派模型,类加载过程,自定义类加载器,打破双亲委派
---
## 回顾一下类加载过程
@@ -58,7 +63,7 @@ class Class {
}
```
-简单来说,**类加载器的主要作用就是加载 Java 类的字节码( `.class` 文件)到 JVM 中(在内存中生成一个代表该类的 `Class` 对象)。** 字节码可以是 Java 源程序(`.java`文件)经过 `javac` 编译得来,也可以是通过工具动态生成或者通过网络下载得来。
+简单来说,**类加载器的主要作用就是动态加载 Java 类的字节码( `.class` 文件)到 JVM 中(在内存中生成一个代表该类的 `Class` 对象)。** 字节码可以是 Java 源程序(`.java`文件)经过 `javac` 编译得来,也可以是通过工具动态生成或者通过网络下载得来。
其实除了加载类之外,类加载器还可以加载 Java 应用所需的资源如文本、图像、配置文件、视频等等文件资源。本文只讨论其核心功能:加载类。
@@ -101,7 +106,7 @@ JVM 中内置了三个重要的 `ClassLoader`:
除了 `BootstrapClassLoader` 是 JVM 自身的一部分之外,其他所有的类加载器都是在 JVM 外部实现的,并且全都继承自 `ClassLoader`抽象类。这样做的好处是用户可以自定义类加载器,以便让应用程序自己决定如何去获取所需的类。
-每个 `ClassLoader` 可以通过`getParent()`获取其父 `ClassLoader`,如果获取到 `ClassLoader` 为`null`的话,那么该类是通过 `BootstrapClassLoader` 加载的。
+每个 `ClassLoader` 可以通过`getParent()`获取其父 `ClassLoader`,如果获取到 `ClassLoader` 为`null`的话,那么该类加载器的父类加载器是 `BootstrapClassLoader` 。
```java
public abstract class ClassLoader {
@@ -281,9 +286,50 @@ protected Class> loadClass(String name, boolean resolve)
### 双亲委派模型的好处
-双亲委派模型保证了 Java 程序的稳定运行,可以避免类的重复加载(JVM 区分不同类的方式不仅仅根据类名,相同的类文件被不同的类加载器加载产生的是两个不同的类),也保证了 Java 的核心 API 不被篡改。
+双亲委派模型是 Java 类加载机制的重要组成部分,它通过委派父加载器优先加载类的方式,实现了两个关键的安全目标:避免类的重复加载和防止核心 API 被篡改。
-如果没有使用双亲委派模型,而是每个类加载器加载自己的话就会出现一些问题,比如我们编写一个称为 `java.lang.Object` 类的话,那么程序运行的时候,系统就会出现两个不同的 `Object` 类。双亲委派模型可以保证加载的是 JRE 里的那个 `Object` 类,而不是你写的 `Object` 类。这是因为 `AppClassLoader` 在加载你的 `Object` 类时,会委托给 `ExtClassLoader` 去加载,而 `ExtClassLoader` 又会委托给 `BootstrapClassLoader`,`BootstrapClassLoader` 发现自己已经加载过了 `Object` 类,会直接返回,不会去加载你写的 `Object` 类。
+JVM 区分不同类的依据是类名加上加载该类的类加载器,即使类名相同,如果由不同的类加载器加载,也会被视为不同的类。 双亲委派模型确保核心类总是由 `BootstrapClassLoader` 加载,保证了核心类的唯一性。
+
+例如,当应用程序尝试加载 `java.lang.Object` 时,`AppClassLoader` 会首先将请求委派给 `ExtClassLoader`,`ExtClassLoader` 再委派给 `BootstrapClassLoader`。`BootstrapClassLoader` 会在 JRE 核心类库中找到并加载 `java.lang.Object`,从而保证应用程序使用的是 JRE 提供的标准版本。
+
+有很多小伙伴就要说了:“那我绕过双亲委派模型不就可以了么?”。
+
+然而,即使攻击者绕过了双亲委派模型,Java 仍然具备更底层的安全机制来保护核心类库。`ClassLoader` 的 `preDefineClass` 方法会在定义类之前进行类名校验。任何以 `"java."` 开头的类名都会触发 `SecurityException`,阻止恶意代码定义或加载伪造的核心类。
+
+JDK 8 中`ClassLoader#preDefineClass` 方法源码如下:
+
+```java
+private ProtectionDomain preDefineClass(String name,
+ ProtectionDomain pd)
+ {
+ // 检查类名是否合法
+ if (!checkName(name)) {
+ throw new NoClassDefFoundError("IllegalName: " + name);
+ }
+
+ // 防止在 "java.*" 包中定义类。
+ // 此检查对于安全性至关重要,因为它可以防止恶意代码替换核心 Java 类。
+ // JDK 9 利用平台类加载器增强了 preDefineClass 方法的安全性
+ if ((name != null) && name.startsWith("java.")) {
+ throw new SecurityException
+ ("禁止的包名: " +
+ name.substring(0, name.lastIndexOf('.')));
+ }
+
+ // 如果未指定 ProtectionDomain,则使用默认域(defaultDomain)。
+ if (pd == null) {
+ pd = defaultDomain;
+ }
+
+ if (name != null) {
+ checkCerts(name, pd.getCodeSource());
+ }
+
+ return pd;
+ }
+```
+
+JDK 9 中这部分逻辑有所改变,多了平台类加载器(`getPlatformClassLoader()` 方法获取),增强了 `preDefineClass` 方法的安全性。这里就不贴源码了,感兴趣的话,可以自己去看看。
### 打破双亲委派模型方法
@@ -313,20 +359,36 @@ Tomcat 这四个自定义的类加载器对应的目录如下:
从图中的委派关系中可以看出:
- `CommonClassLoader`作为 `CatalinaClassLoader` 和 `SharedClassLoader` 的父加载器。`CommonClassLoader` 能加载的类都可以被 `CatalinaClassLoader` 和 `SharedClassLoader` 使用。因此,`CommonClassLoader` 是为了实现公共类库(可以被所有 Web 应用和 Tomcat 内部组件使用的类库)的共享和隔离。
-- `CatalinaClassLoader` 和 `SharedClassLoader` 能加载的类则与对方相互隔离。`CatalinaClassLoader` 用于加载 Tomcat 自身的类,为了隔离 Tomcat 本身的类和 Web 应用的类。`SharedClassLoader` 作为 `WebAppClassLoader` 的父加载器,专门来加载 Web 应用之间共享的类比如 Spring、Mybatis。
+- `CatalinaClassLoader` 和 `SharedClassLoader` 能加载的类则与对方相互隔离。`CatalinaClassLoader` 用于加载 Tomcat 自身的类,为了隔离 Tomcat 本身的类和 Web 应用的类。`SharedClassLoader` 作为 `WebAppClassLoader` 的父加载器,专门来加载 Web 应用之间共享的类,但是在Tomcat的默认配置下`catalina.properties`配置文件的`shared.loader= `值为空,所以`SharedClassLoader` 并不生效,`SharedClassLoader` 实际上会退化为 `CommonClassLoader`,`SharedClassLoader`比较合适用来加载多个web应用间共享的类库,比如整个公司级别的监控、日志等。
- 每个 Web 应用都会创建一个单独的 `WebAppClassLoader`,并在启动 Web 应用的线程里设置线程线程上下文类加载器为 `WebAppClassLoader`。各个 `WebAppClassLoader` 实例之间相互隔离,进而实现 Web 应用之间的类隔。
单纯依靠自定义类加载器没办法满足某些场景的要求,例如,有些情况下,高层的类加载器需要加载低层的加载器才能加载的类。
比如,SPI 中,SPI 的接口(如 `java.sql.Driver`)是由 Java 核心库提供的,由`BootstrapClassLoader` 加载。而 SPI 的实现(如`com.mysql.cj.jdbc.Driver`)是由第三方供应商提供的,它们是由应用程序类加载器或者自定义类加载器来加载的。默认情况下,一个类及其依赖类由同一个类加载器加载。所以,加载 SPI 的接口的类加载器(`BootstrapClassLoader`)也会用来加载 SPI 的实现。按照双亲委派模型,`BootstrapClassLoader` 是无法找到 SPI 的实现类的,因为它无法委托给子类加载器去尝试加载。
+这里需要注意:JDK 9+ 之后引入模块化,JDBC API 被拆分到 `java.sql` 模块中,不再是 `BootstrapClassLoader` 直接加载,而是由 `PlatformClassLoader` 加载。
+
+```java
+public class ClassLoaderTest {
+ public static void main(String[] args) throws ClassNotFoundException {
+ Class> clazz = Class.forName("java.sql.Driver");
+ ClassLoader loader = clazz.getClassLoader();
+ System.out.println("Loader for java.sql.Driver: " + loader);
+
+ // .jdks/corretto-1.8.0_442/bin/java 环境下为 Loader for java.sql.Driver: null
+
+ // .jdks/jbr-17.0.12/bin/java 环境下为 Loader for java.sql.Driver: jdk.internal.loader.ClassLoaders$PlatformClassLoader@30f39991
+ }
+}
+```
+
再比如,假设我们的项目中有 Spring 的 jar 包,由于其是 Web 应用之间共享的,因此会由 `SharedClassLoader` 加载(Web 服务器是 Tomcat)。我们项目中有一些用到了 Spring 的业务类,比如实现了 Spring 提供的接口、用到了 Spring 提供的注解。所以,加载 Spring 的类加载器(也就是 `SharedClassLoader`)也会用来加载这些业务类。但是业务类在 Web 应用目录下,不在 `SharedClassLoader` 的加载路径下,所以 `SharedClassLoader` 无法找到业务类,也就无法加载它们。
如何解决这个问题呢? 这个时候就需要用到 **线程上下文类加载器(`ThreadContextClassLoader`)** 了。
拿 Spring 这个例子来说,当 Spring 需要加载业务类的时候,它不是用自己的类加载器,而是用当前线程的上下文类加载器。还记得我上面说的吗?每个 Web 应用都会创建一个单独的 `WebAppClassLoader`,并在启动 Web 应用的线程里设置线程线程上下文类加载器为 `WebAppClassLoader`。这样就可以让高层的类加载器(`SharedClassLoader`)借助子类加载器( `WebAppClassLoader`)来加载业务类,破坏了 Java 的类加载委托机制,让应用逆向使用类加载器。
-线程线程上下文类加载器的原理是将一个类加载器保存在线程私有数据里,跟线程绑定,然后在需要的时候取出来使用。这个类加载器通常是由应用程序或者容器(如 Tomcat)设置的。
+线程上下文类加载器的原理是将一个类加载器保存在线程私有数据里,跟线程绑定,然后在需要的时候取出来使用。这个类加载器通常是由应用程序或者容器(如 Tomcat)设置的。
`Java.lang.Thread` 中的`getContextClassLoader()`和 `setContextClassLoader(ClassLoader cl)`分别用来获取和设置线程的上下文类加载器。如果没有通过`setContextClassLoader(ClassLoader cl)`进行设置的话,线程将继承其父线程的上下文类加载器。
diff --git a/docs/java/jvm/jdk-monitoring-and-troubleshooting-tools.md b/docs/java/jvm/jdk-monitoring-and-troubleshooting-tools.md
index 33fc2d8767b..b2c1dc3c6a8 100644
--- a/docs/java/jvm/jdk-monitoring-and-troubleshooting-tools.md
+++ b/docs/java/jvm/jdk-monitoring-and-troubleshooting-tools.md
@@ -1,8 +1,13 @@
---
title: JDK监控和故障处理工具总结
+description: 汇总 JDK 常用监控与排错工具及使用示例,辅助定位与分析 JVM 问题。
category: Java
tag:
- JVM
+head:
+ - - meta
+ - name: keywords
+ content: JDK 工具,jps,jstat,jmap,jstack,jvisualvm,诊断,监控
---
## JDK 命令行工具
@@ -276,7 +281,7 @@ JConsole 可以显示当前内存的详细信息。不仅包括堆内存/非堆
点击右边的“执行 GC(G)”按钮可以强制应用程序执行一个 Full GC。
-> - **新生代 GC(Minor GC)**:指发生新生代的的垃圾收集动作,Minor GC 非常频繁,回收速度一般也比较快。
+> - **新生代 GC(Minor GC)**:指发生新生代的垃圾收集动作,Minor GC 非常频繁,回收速度一般也比较快。
> - **老年代 GC(Major GC/Full GC)**:指发生在老年代的 GC,出现了 Major GC 经常会伴随至少一次的 Minor GC(并非绝对),Major GC 的速度一般会比 Minor GC 的慢 10 倍以上。

diff --git a/docs/java/jvm/jvm-garbage-collection.md b/docs/java/jvm/jvm-garbage-collection.md
index 6a24320a7ae..3a57e613902 100644
--- a/docs/java/jvm/jvm-garbage-collection.md
+++ b/docs/java/jvm/jvm-garbage-collection.md
@@ -1,8 +1,13 @@
---
title: JVM垃圾回收详解(重点)
+description: JVM垃圾回收详解:全面讲解GC算法(标记清除、复制、标记整理)、分代回收机制、常用垃圾回收器(Serial、Parallel、CMS、G1、ZGC)、GC调优实践。
category: Java
tag:
- JVM
+head:
+ - - meta
+ - name: keywords
+ content: JVM垃圾回收,GC算法,垃圾回收器,分代回收,标记清除,复制算法,G1 GC,ZGC,GC调优
---
> 如果没有特殊说明,都是针对的是 HotSpot 虚拟机。
@@ -253,29 +258,58 @@ public class ReferenceCountingGc {
JDK1.2 之前,Java 中引用的定义很传统:如果 reference 类型的数据存储的数值代表的是另一块内存的起始地址,就称这块内存代表一个引用。
-JDK1.2 以后,Java 对引用的概念进行了扩充,将引用分为强引用、软引用、弱引用、虚引用四种(引用强度逐渐减弱)
+JDK1.2 以后,Java 对引用的概念进行了扩充,将引用分为强引用、软引用、弱引用、虚引用四种(引用强度逐渐减弱),强引用就是 Java 中普通的对象,而软引用、弱引用、虚引用在 JDK 中定义的类分别是 `SoftReference`、`WeakReference`、`PhantomReference`。

**1.强引用(StrongReference)**
-以前我们使用的大部分引用实际上都是强引用,这是使用最普遍的引用。如果一个对象具有强引用,那就类似于**必不可少的生活用品**,垃圾回收器绝不会回收它。当内存空间不足,Java 虚拟机宁愿抛出 OutOfMemoryError 错误,使程序异常终止,也不会靠随意回收具有强引用的对象来解决内存不足问题。
+强引用实际上就是程序代码中普遍存在的引用赋值,这是使用最普遍的引用,其代码如下
+
+```java
+String strongReference = new String("abc");
+```
+
+如果一个对象具有强引用,那就类似于**必不可少的生活用品**,垃圾回收器绝不会回收它。当内存空间不足,Java 虚拟机宁愿抛出 OutOfMemoryError 错误,使程序异常终止,也不会靠随意回收具有强引用的对象来解决内存不足问题。
**2.软引用(SoftReference)**
-如果一个对象只具有软引用,那就类似于**可有可无的生活用品**。如果内存空间足够,垃圾回收器就不会回收它,如果内存空间不足了,就会回收这些对象的内存。只要垃圾回收器没有回收它,该对象就可以被程序使用。软引用可用来实现内存敏感的高速缓存。
+如果一个对象只具有软引用,那就类似于**可有可无的生活用品**。软引用代码如下
+
+```java
+// 软引用
+String str = new String("abc");
+SoftReference softReference = new SoftReference(str);
+```
+
+如果内存空间足够,垃圾回收器就不会回收它,如果内存空间不足了,就会回收这些对象的内存。只要垃圾回收器没有回收它,该对象就可以被程序使用。软引用可用来实现内存敏感的高速缓存。
软引用可以和一个引用队列(ReferenceQueue)联合使用,如果软引用所引用的对象被垃圾回收,JAVA 虚拟机就会把这个软引用加入到与之关联的引用队列中。
**3.弱引用(WeakReference)**
-如果一个对象只具有弱引用,那就类似于**可有可无的生活用品**。弱引用与软引用的区别在于:只具有弱引用的对象拥有更短暂的生命周期。在垃圾回收器线程扫描它所管辖的内存区域的过程中,一旦发现了只具有弱引用的对象,不管当前内存空间足够与否,都会回收它的内存。不过,由于垃圾回收器是一个优先级很低的线程, 因此不一定会很快发现那些只具有弱引用的对象。
+如果一个对象只具有弱引用,那就类似于**可有可无的生活用品**。弱引用代码如下:
+
+```java
+String str = new String("abc");
+WeakReference weakReference = new WeakReference<>(str);
+str = null; //str变成软引用,可以被收集
+```
+
+弱引用与软引用的区别在于:只具有弱引用的对象拥有更短暂的生命周期。在垃圾回收器线程扫描它所管辖的内存区域的过程中,一旦发现了只具有弱引用的对象,不管当前内存空间足够与否,都会回收它的内存。不过,由于垃圾回收器是一个优先级很低的线程, 因此不一定会很快发现那些只具有弱引用的对象。
弱引用可以和一个引用队列(ReferenceQueue)联合使用,如果弱引用所引用的对象被垃圾回收,Java 虚拟机就会把这个弱引用加入到与之关联的引用队列中。
**4.虚引用(PhantomReference)**
-"虚引用"顾名思义,就是形同虚设,与其他几种引用都不同,虚引用并不会决定对象的生命周期。如果一个对象仅持有虚引用,那么它就和没有任何引用一样,在任何时候都可能被垃圾回收。
+"虚引用"顾名思义,就是形同虚设,与其他几种引用都不同,虚引用并不会决定对象的生命周期。如果一个对象仅持有虚引用,那么它就和没有任何引用一样,在任何时候都可能被垃圾回收。虚引用代码如下:
+
+```java
+String str = new String("abc");
+ReferenceQueue queue = new ReferenceQueue();
+// 创建虚引用,要求必须与一个引用队列关联
+PhantomReference pr = new PhantomReference(str, queue);
+```
**虚引用主要用来跟踪对象被垃圾回收的活动**。
@@ -322,7 +356,7 @@ JDK1.2 以后,Java 对引用的概念进行了扩充,将引用分为强引

-关于具体是标记可回收对象还是不可回收对象,众说纷纭,两种说法其实都没问题,我个人更倾向于是前者。
+关于具体是标记可回收对象(不可达对象)还是不可回收对象(可达对象),众说纷纭,两种说法其实都没问题,我个人更倾向于是后者。
如果按照前者的理解,整个标记-清除过程大致是这样的:
@@ -353,7 +387,7 @@ JDK1.2 以后,Java 对引用的概念进行了扩充,将引用分为强引
当前虚拟机的垃圾收集都采用分代收集算法,这种算法没有什么新的思想,只是根据对象存活周期的不同将内存分为几块。一般将 Java 堆分为新生代和老年代,这样我们就可以根据各个年代的特点选择合适的垃圾收集算法。
-比如在新生代中,每次收集都会有大量对象死去,所以可以选择”标记-复制“算法,只需要付出少量对象的复制成本就可以完成每次垃圾收集。而老年代的对象存活几率是比较高的,而且没有额外的空间对它进行分配担保,所以我们必须选择“标记-清除”或“标记-整理”算法进行垃圾收集。
+比如在新生代中,每次收集都会有大量对象死去,所以可以选择“复制”算法,只需要付出少量对象的复制成本就可以完成每次垃圾收集。而老年代的对象存活几率是比较高的,而且没有额外的空间对它进行分配担保,所以我们必须选择“标记-清除”或“标记-整理”算法进行垃圾收集。
**延伸面试问题:** HotSpot 为什么要分为新生代和老年代?
@@ -367,8 +401,8 @@ JDK1.2 以后,Java 对引用的概念进行了扩充,将引用分为强引
JDK 默认垃圾收集器(使用 `java -XX:+PrintCommandLineFlags -version` 命令查看):
-- JDK 8:Parallel Scavenge(新生代)+ Parallel Old(老年代)
-- JDK 9 ~ JDK20: G1
+- JDK 8: Parallel Scavenge(新生代)+ Parallel Old(老年代)
+- JDK 9 ~ JDK22: G1
### Serial 收集器
@@ -451,7 +485,7 @@ JDK1.8 默认使用的是 Parallel Scavenge + Parallel Old,如果指定了-XX:
从名字中的**Mark Sweep**这两个词可以看出,CMS 收集器是一种 **“标记-清除”算法**实现的,它的运作过程相比于前面几种垃圾收集器来说更加复杂一些。整个过程分为四个步骤:
-- **初始标记:** 暂停所有的其他线程,并记录下直接与 root 相连的对象,速度很快 ;
+- **初始标记:** 短暂停顿,标记直接与 root 相连的对象(根对象);
- **并发标记:** 同时开启 GC 和用户线程,用一个闭包结构去记录可达对象。但在这个阶段结束,这个闭包结构并不能保证包含当前所有的可达对象。因为用户线程可能会不断的更新引用域,所以 GC 线程无法保证可达性分析的实时性。所以这个算法里会跟踪记录这些发生引用更新的地方。
- **重新标记:** 重新标记阶段就是为了修正并发标记期间因为用户程序继续运行而导致标记产生变动的那一部分对象的标记记录,这个阶段的停顿时间一般会比初始标记阶段的时间稍长,远远比并发标记阶段时间短
- **并发清除:** 开启用户线程,同时 GC 线程开始对未标记的区域做清扫。
@@ -468,7 +502,7 @@ JDK1.8 默认使用的是 Parallel Scavenge + Parallel Old,如果指定了-XX:
### G1 收集器
-**G1 (Garbage-First) 是一款面向服务器的垃圾收集器,主要针对配备多颗处理器及大容量内存的机器. 以极高概率满足 GC 停顿时间要求的同时,还具备高吞吐量性能特征.**
+**G1 (Garbage-First) 是一款面向服务器的垃圾收集器,主要针对配备多颗处理器及大容量内存的机器. 以极高概率满足 GC 停顿时间要求的同时,还具备高吞吐量性能特征。**
被视为 JDK1.7 中 HotSpot 虚拟机的一个重要进化特征。它具备以下特点:
@@ -479,10 +513,10 @@ JDK1.8 默认使用的是 Parallel Scavenge + Parallel Old,如果指定了-XX:
G1 收集器的运作大致分为以下几个步骤:
-- **初始标记**
-- **并发标记**
-- **最终标记**
-- **筛选回收**
+- **初始标记**: 短暂停顿(Stop-The-World,STW),标记从 GC Roots 可直接引用的对象,即标记所有直接可达的活跃对象
+- **并发标记**:与应用并发运行,标记所有可达对象。 这一阶段可能持续较长时间,取决于堆的大小和对象的数量。
+- **最终标记**: 短暂停顿(STW),处理并发标记阶段结束后残留的少量未处理的引用变更。
+- **筛选回收**:根据标记结果,选择回收价值高的区域,复制存活对象到新区域,回收旧区域内存。这一阶段包含一个或多个停顿(STW),具体取决于回收的复杂度。

@@ -492,7 +526,7 @@ G1 收集器的运作大致分为以下几个步骤:
### ZGC 收集器
-与 CMS 中的 ParNew 和 G1 类似,ZGC 也采用标记-复制算法,不过 ZGC 对该算法做了重大改进。
+与 ParNew 和 G1 类似,ZGC 也采用标记-复制算法,不过 ZGC 对该算法做了重大改进。
ZGC 可以将暂停时间控制在几毫秒以内,且暂停时间不受堆内存大小的影响,出现 Stop The World 的情况会更少,但代价是牺牲了一些吞吐量。ZGC 最大支持 16TB 的堆内存。
@@ -512,7 +546,11 @@ java -XX:+UseZGC className
java -XX:+UseZGC -XX:+ZGenerational className
```
-关于 ZGC 收集器的详细介绍推荐阅读美团技术团队的 [新一代垃圾回收器 ZGC 的探索与实践](https://tech.meituan.com/2020/08/06/new-zgc-practice-in-meituan.html) 这篇文章。
+关于 ZGC 收集器的详细介绍推荐看看这几篇文章:
+
+- [从历代 GC 算法角度剖析 ZGC - 京东技术](https://mp.weixin.qq.com/s/ExkB40cq1_Z0ooDzXn7CVw)
+- [新一代垃圾回收器 ZGC 的探索与实践 - 美团技术团队](https://tech.meituan.com/2020/08/06/new-zgc-practice-in-meituan.html)
+- [极致八股文之 JVM 垃圾回收器 G1&ZGC 详解 - 阿里云开发者](https://mp.weixin.qq.com/s/Ywj3XMws0IIK-kiUllN87Q)
## 参考
diff --git a/docs/java/jvm/jvm-in-action.md b/docs/java/jvm/jvm-in-action.md
index 99b6fc6041d..99db69a6575 100644
--- a/docs/java/jvm/jvm-in-action.md
+++ b/docs/java/jvm/jvm-in-action.md
@@ -1,8 +1,13 @@
---
title: JVM线上问题排查和性能调优案例
+description: 汇集 JVM 在生产中的问题排查与优化案例,涵盖内存与 GC、工具使用等。
category: Java
tag:
- JVM
+head:
+ - - meta
+ - name: keywords
+ content: JVM 实战,线上排查,性能调优,内存分析,GC 优化,工具
---
JVM 线上问题排查和性能调优也是面试常问的一个问题,尤其是社招中大厂的面试。
diff --git a/docs/java/jvm/jvm-intro.md b/docs/java/jvm/jvm-intro.md
index 2fdb9b3e055..8d2c1b0bdf6 100644
--- a/docs/java/jvm/jvm-intro.md
+++ b/docs/java/jvm/jvm-intro.md
@@ -1,8 +1,13 @@
---
title: 大白话带你认识 JVM
+description: 用通俗方式介绍 JVM 的基本组成与类加载执行流程,帮助快速入门虚拟机原理。
category: Java
tag:
- JVM
+head:
+ - - meta
+ - name: keywords
+ content: JVM 基础,类加载,方法区,堆栈,程序计数器,运行时数据区
---
> 来自[说出你的愿望吧丷](https://juejin.im/user/5c2400afe51d45451758aa96)投稿,原文地址:。
diff --git a/docs/java/jvm/jvm-parameters-intro.md b/docs/java/jvm/jvm-parameters-intro.md
index a32dd513a26..fbe5533f729 100644
--- a/docs/java/jvm/jvm-parameters-intro.md
+++ b/docs/java/jvm/jvm-parameters-intro.md
@@ -1,83 +1,88 @@
---
title: 最重要的JVM参数总结
+description: 总结常用 JVM 参数与配置方法,结合内存与 GC 调优的实践建议。
category: Java
tag:
- JVM
+head:
+ - - meta
+ - name: keywords
+ content: JVM 参数,堆大小,栈大小,GC 设置,性能调优,XX 参数
---
> 本文由 JavaGuide 翻译自 [https://www.baeldung.com/jvm-parameters](https://www.baeldung.com/jvm-parameters),并对文章进行了大量的完善补充。
+> 文档参数 [https://docs.oracle.com/javase/8/docs/technotes/tools/unix/java.html](https://docs.oracle.com/javase/8/docs/technotes/tools/unix/java.html)
>
-> JDK 版本:1.8
+> JDK 版本:1.8 为主,也会补充新版本常用参数
-## 1.概述
+在本篇文章中,我们将一起掌握 Java 虚拟机(JVM)中最常用的一些参数配置,帮助你更好地理解和调优 Java 应用的运行环境。
-在本篇文章中,你将掌握最常用的 JVM 参数配置。
+## 堆内存相关
-## 2.堆内存相关
-
-> Java 虚拟机所管理的内存中最大的一块,Java 堆是所有线程共享的一块内存区域,在虚拟机启动时创建。**此内存区域的唯一目的就是存放对象实例,几乎所有的对象实例以及数组都在这里分配内存。**
+> Java 堆(Java Heap)是 JVM 所管理的内存中最大的一块区域,**所有线程共享**,在虚拟机启动时创建。**此内存区域的唯一目的就是存放对象实例,几乎所有的对象实例以及数组都要在堆上分配内存。**

-### 2.1.显式指定堆内存`–Xms`和`-Xmx`
+### 设置堆内存大小 (-Xms 和 -Xmx)
+
+根据应用程序的实际需求设置初始和最大堆内存大小,是性能调优中最常见的实践之一。**推荐显式设置这两个参数,并且通常建议将它们设置为相同的值**,以避免运行时堆内存的动态调整带来的性能开销。
-与性能有关的最常见实践之一是根据应用程序要求初始化堆内存。如果我们需要指定最小和最大堆大小(推荐显示指定大小),以下参数可以帮助你实现:
+使用以下参数进行设置:
```bash
--Xms[unit]
--Xmx[unit]
+-Xms[unit] # 设置 JVM 初始堆大小
+-Xmx[unit] # 设置 JVM 最大堆大小
```
-- **heap size** 表示要初始化内存的具体大小。
-- **unit** 表示要初始化内存的单位。单位为 **_“ g”_** (GB)、**_“ m”_**(MB)、**_“ k”_**(KB)。
+- ``: 指定内存的具体数值。
+- `[unit]`: 指定内存的单位,如 g (GB)、m (MB)、k (KB)。
-举个栗子 🌰,如果我们要为 JVM 分配最小 2 GB 和最大 5 GB 的堆内存大小,我们的参数应该这样来写:
+**示例:** 将 JVM 的初始堆和最大堆都设置为 4GB:
```bash
--Xms2G -Xmx5G
+-Xms4G -Xmx4G
```
-### 2.2.显式新生代内存(Young Generation)
+### 设置新生代内存大小 (Young Generation)
-根据[Oracle 官方文档](https://docs.oracle.com/javase/8/docs/technotes/guides/vm/gctuning/sizing.html),在堆总可用内存配置完成之后,第二大影响因素是为 `Young Generation` 在堆内存所占的比例。默认情况下,YG 的最小大小为 1310 _MB_,最大大小为 _无限制_。
+根据[Oracle 官方文档](https://docs.oracle.com/javase/8/docs/technotes/guides/vm/gctuning/sizing.html),在堆总可用内存配置完成之后,第二大影响因素是为 `Young Generation` 在堆内存所占的比例。默认情况下,YG 的最小大小为 **1310 MB**,最大大小为 **无限制**。
-一共有两种指定 新生代内存(Young Generation)大小的方法:
+可以通过以下两种方式设置新生代内存大小:
**1.通过`-XX:NewSize`和`-XX:MaxNewSize`指定**
```bash
--XX:NewSize=[unit]
--XX:MaxNewSize=[unit]
+-XX:NewSize=[unit] # 设置新生代初始大小
+-XX:MaxNewSize=[unit] # 设置新生代最大大小
```
-举个栗子 🌰,如果我们要为 新生代分配 最小 256m 的内存,最大 1024m 的内存我们的参数应该这样来写:
+**示例:** 设置新生代最小 512MB,最大 1024MB:
```bash
--XX:NewSize=256m
--XX:MaxNewSize=1024m
+-XX:NewSize=512m -XX:MaxNewSize=1024m
```
**2.通过`-Xmn[unit]`指定**
-举个栗子 🌰,如果我们要为 新生代分配 256m 的内存(NewSize 与 MaxNewSize 设为一致),我们的参数应该这样来写:
+**示例:** 将新生代大小固定为 512MB:
```bash
--Xmn256m
+-Xmn512m
```
GC 调优策略中很重要的一条经验总结是这样说的:
-> 将新对象预留在新生代,由于 Full GC 的成本远高于 Minor GC,因此尽可能将对象分配在新生代是明智的做法,实际项目中根据 GC 日志分析新生代空间大小分配是否合理,适当通过“-Xmn”命令调节新生代大小,最大限度降低新对象直接进入老年代的情况。
+> 尽量让新创建的对象在新生代分配内存并被回收,因为 Minor GC 的成本通常远低于 Full GC。通过分析 GC 日志,判断新生代空间分配是否合理。如果大量新对象过早进入老年代(Promotion),可以适当通过 `-Xmn` 或 -`XX:NewSize/-XX:MaxNewSize` 调整新生代大小,目标是最大限度地减少对象直接进入老年代的情况。
-另外,你还可以通过 **`-XX:NewRatio=`** 来设置老年代与新生代内存的比值。
+另外,你还可以通过 **`-XX:NewRatio=`** 参数来设置**老年代与新生代(不含 Survivor 区)的内存大小比例**。
-比如下面的参数就是设置老年代与新生代内存的比值为 1。也就是说老年代和新生代所占比值为 1:1,新生代占整个堆栈的 1/2。
+例如,`-XX:NewRatio=2` (默认值)表示老年代 : 新生代 = 2 : 1。即新生代占整个堆大小的 1/3。
-```plain
--XX:NewRatio=1
+```bash
+-XX:NewRatio=2
```
-### 2.3.显式指定永久代/元空间的大小
+### 设置永久代/元空间大小 (PermGen/Metaspace)
**从 Java 8 开始,如果我们没有指定 Metaspace 的大小,随着更多类的创建,虚拟机会耗尽所有可用的系统内存(永久代并不会出现这种情况)。**
@@ -101,7 +106,7 @@ JDK 1.8 之前永久代还没被彻底移除的时候通常通过下面这些参
**🐛 修正(参见:[issue#1947](https://github.com/Snailclimb/JavaGuide/issues/1947))**:
-1、Metaspace 的初始容量并不是 `-XX:MetaspaceSize` 设置,无论 `-XX:MetaspaceSize` 配置什么值,对于 64 位 JVM 来说,Metaspace 的初始容量都是 21807104(约 20.8m)。
+**1、`-XX:MetaspaceSize` 并非初始容量:** Metaspace 的初始容量并不是 `-XX:MetaspaceSize` 设置,无论 `-XX:MetaspaceSize` 配置什么值,对于 64 位 JVM,元空间的初始容量通常是一个固定的较小值(Oracle 文档提到约 12MB 到 20MB 之间,实际观察约 20.8MB)。
可以参考 Oracle 官方文档 [Other Considerations](https://docs.oracle.com/javase/8/docs/technotes/guides/vm/gctuning/considerations.html) 中提到的:
@@ -111,11 +116,7 @@ JDK 1.8 之前永久代还没被彻底移除的时候通常通过下面这些参
另外,还可以看一下这个试验:[JVM 参数 MetaspaceSize 的误解](https://mp.weixin.qq.com/s/jqfppqqd98DfAJHZhFbmxA)。
-2、Metaspace 由于使用不断扩容到`-XX:MetaspaceSize`参数指定的量,就会发生 FGC,且之后每次 Metaspace 扩容都会发生 Full GC。
-
-也就是说,MetaspaceSize 表示 Metaspace 使用过程中触发 Full GC 的阈值,只对触发起作用。
-
-垃圾搜集器内部是根据变量 `_capacity_until_GC`来判断 Metaspace 区域是否达到阈值的,初始化代码如下所示:
+**2、扩容与 Full GC:** 当 Metaspace 的使用量增长并首次达到`-XX:MetaspaceSize` 指定的阈值时,会触发一次 Full GC。在此之后,JVM 会动态调整这个触发 GC 的阈值。如果元空间继续增长,每次达到新的阈值需要扩容时,仍然可能触发 Full GC(具体行为与垃圾收集器和版本有关)。垃圾搜集器内部是根据变量 `_capacity_until_GC`来判断 Metaspace 区域是否达到阈值的,初始化代码如下所示:
```c
void MetaspaceGC::initialize() {
@@ -125,111 +126,120 @@ void MetaspaceGC::initialize() {
}
```
-相关阅读:[issue 更正:MaxMetaspaceSize 如果不指定大小的话,不会耗尽内存 #1204](https://github.com/Snailclimb/JavaGuide/issues/1204) 。
-
-## 3.垃圾收集相关
+**3、`-XX:MaxMetaspaceSize` 的重要性:**如果不显式设置 -`XX:MaxMetaspaceSize`,元空间的最大大小理论上受限于可用的本地内存。在极端情况下(如类加载器泄漏导致不断加载类),这确实**可能耗尽大量本地内存**。因此,**强烈建议设置一个合理的 `-XX:MaxMetaspaceSize` 上限**,以防止对系统造成影响。
-### 3.1.垃圾回收器
+相关阅读:[issue 更正:MaxMetaspaceSize 如果不指定大小的话,不会耗尽内存 #1204](https://github.com/Snailclimb/JavaGuide/issues/1204) 。
-为了提高应用程序的稳定性,选择正确的[垃圾收集](http://www.oracle.com/webfolder/technetwork/tutorials/obe/java/gc01/index.html)算法至关重要。
+## 垃圾收集相关
-JVM 具有四种类型的 GC 实现:
+### 选择垃圾回收器
-- 串行垃圾收集器
-- 并行垃圾收集器
-- CMS 垃圾收集器
-- G1 垃圾收集器
+选择合适的垃圾收集器(Garbage Collector, GC)对于应用的吞吐量和响应延迟至关重要。关于垃圾收集算法和收集器的详细介绍,可以看笔者写的这篇:[JVM 垃圾回收详解(重点)](https://javaguide.cn/java/jvm/jvm-garbage-collection.html)。
-可以使用以下参数声明这些实现:
+JVM 提供了多种 GC 实现,适用于不同的场景:
-```bash
--XX:+UseSerialGC
--XX:+UseParallelGC
--XX:+UseConcMarkSweepGC
--XX:+UseG1GC
-```
+- **Serial GC (串行垃圾收集器):** 单线程执行 GC,适用于客户端模式或单核 CPU 环境。参数:`-XX:+UseSerialGC`。
+- **Parallel GC (并行垃圾收集器):** 多线程执行新生代 GC (Minor GC),以及可选的多线程执行老年代 GC (Full GC,通过 `-XX:+UseParallelOldGC`)。关注吞吐量,是 JDK 8 的默认 GC。参数:`-XX:+UseParallelGC`。
+- **CMS GC (Concurrent Mark Sweep 并发标记清除收集器):** 以获取最短回收停顿时间为目标,大部分 GC 阶段可与用户线程并发执行。适用于对响应时间要求高的应用。在 JDK 9 中被标记为弃用,JDK 14 中被移除。参数:`-XX:+UseConcMarkSweepGC`。
+- **G1 GC (Garbage-First Garbage Collector):** JDK 9 及之后版本的默认 GC。将堆划分为多个 Region,兼顾吞吐量和停顿时间,试图在可预测的停顿时间内完成 GC。参数:`-XX:+UseG1GC`。
+- **ZGC:** 更新的低延迟 GC,目标是将 GC 停顿时间控制在几毫秒甚至亚毫秒级别,需要较新版本的 JDK 支持。参数(具体参数可能随版本变化):`-XX:+UseZGC`、`-XX:+UseShenandoahGC`。
-有关*垃圾回收*实施的更多详细信息,请参见[此处](https://github.com/Snailclimb/JavaGuide/blob/master/docs/java/jvm/JVM%E5%9E%83%E5%9C%BE%E5%9B%9E%E6%94%B6.md)。
+### GC 日志记录
-### 3.2.GC 日志记录
+在生产环境或进行 GC 问题排查时,**务必开启 GC 日志记录**。详细的 GC 日志是分析和解决 GC 问题的关键依据。
-生产环境上,或者其他要测试 GC 问题的环境上,一定会配置上打印 GC 日志的参数,便于分析 GC 相关的问题。
+以下是一些推荐配置的 GC 日志参数(适用于 JDK 8/11 等常见版本):
```bash
-# 必选
-# 打印基本 GC 信息
+# --- 推荐的基础配置 ---
+# 打印详细 GC 信息
-XX:+PrintGCDetails
+# 打印 GC 发生的时间戳 (相对于 JVM 启动时间)
+# -XX:+PrintGCTimeStamps
+# 打印 GC 发生的日期和时间 (更常用)
-XX:+PrintGCDateStamps
-# 打印对象分布
+# 指定 GC 日志文件的输出路径,%t 可以输出日期时间戳
+-Xloggc:/path/to/gc-%t.log
+
+# --- 推荐的进阶配置 ---
+# 打印对象年龄分布 (有助于判断对象晋升老年代的情况)
-XX:+PrintTenuringDistribution
-# 打印堆数据
+# 在 GC 前后打印堆信息
-XX:+PrintHeapAtGC
-# 打印Reference处理信息
-# 强引用/弱引用/软引用/虚引用/finalize 相关的方法
+# 打印各种类型引用 (强/软/弱/虚) 的处理信息
-XX:+PrintReferenceGC
-# 打印STW时间
+# 打印应用暂停时间 (Stop-The-World, STW)
-XX:+PrintGCApplicationStoppedTime
-# 可选
-# 打印safepoint信息,进入 STW 阶段之前,需要要找到一个合适的 safepoint
--XX:+PrintSafepointStatistics
--XX:PrintSafepointStatisticsCount=1
-
-# GC日志输出的文件路径
--Xloggc:/path/to/gc-%t.log
-# 开启日志文件分割
+# --- GC 日志文件滚动配置 ---
+# 启用 GC 日志文件滚动
-XX:+UseGCLogFileRotation
-# 最多分割几个文件,超过之后从头文件开始写
+# 设置滚动日志文件的数量 (例如,保留最近 14 个)
-XX:NumberOfGCLogFiles=14
-# 每个文件上限大小,超过就触发分割
+# 设置每个日志文件的最大大小 (例如,50MB)
-XX:GCLogFileSize=50M
+
+# --- 可选的辅助诊断配置 ---
+# 打印安全点 (Safepoint) 统计信息 (有助于分析 STW 原因)
+# -XX:+PrintSafepointStatistics
+# -XX:PrintSafepointStatisticsCount=1
```
-## 4.处理 OOM
+**注意:** JDK 9 及之后版本引入了统一的 JVM 日志框架 (`-Xlog`),配置方式有所不同,但上述 `-Xloggc` 和滚动参数通常仍然兼容或有对应的新参数。
+
+## 处理 OOM
对于大型应用程序来说,面对内存不足错误是非常常见的,这反过来会导致应用程序崩溃。这是一个非常关键的场景,很难通过复制来解决这个问题。
这就是为什么 JVM 提供了一些参数,这些参数将堆内存转储到一个物理文件中,以后可以用来查找泄漏:
```bash
+# 在发生 OOM 时生成堆转储文件
-XX:+HeapDumpOnOutOfMemoryError
--XX:HeapDumpPath=./java_pid.hprof
--XX:OnOutOfMemoryError="< cmd args >;< cmd args >"
+
+# 指定堆转储文件的输出路径。 会被替换为进程 ID
+-XX:HeapDumpPath=/path/to/heapdump/java_pid.hprof
+# 示例:-XX:HeapDumpPath=/data/dumps/
+
+# (可选) 在发生 OOM 时执行指定的命令或脚本
+# 例如,发送告警通知或尝试重启服务(需谨慎使用)
+# -XX:OnOutOfMemoryError=" "
+# 示例:-XX:OnOutOfMemoryError="sh /path/to/notify.sh"
+
+# (可选) 启用 GC 开销限制检查
+# 如果 GC 时间占总时间比例过高(默认 98%)且回收效果甚微(默认小于 2% 堆内存),
+# 会提前抛出 OOM,防止应用长时间卡死在 GC 中。
-XX:+UseGCOverheadLimit
```
-这里有几点需要注意:
-
-- **HeapDumpOnOutOfMemoryError** 指示 JVM 在遇到 **OutOfMemoryError** 错误时将 heap 转储到物理文件中。
-- **HeapDumpPath** 表示要写入文件的路径; 可以给出任何文件名; 但是,如果 JVM 在名称中找到一个 `` 标记,则当前进程的进程 id 将附加到文件名中,并使用`.hprof`格式
-- **OnOutOfMemoryError** 用于发出紧急命令,以便在内存不足的情况下执行; 应该在 `cmd args` 空间中使用适当的命令。例如,如果我们想在内存不足时重启服务器,我们可以设置参数: `-XX:OnOutOfMemoryError="shutdown -r"` 。
-- **UseGCOverheadLimit** 是一种策略,它限制在抛出 OutOfMemory 错误之前在 GC 中花费的 VM 时间的比例
-
-## 5.其他
-
-- `-server` : 启用“ Server Hotspot VM”; 此参数默认用于 64 位 JVM
-- `-XX:+UseStringDeduplication` : _Java 8u20_ 引入了这个 JVM 参数,通过创建太多相同 String 的实例来减少不必要的内存使用; 这通过将重复 String 值减少为单个全局 `char []` 数组来优化堆内存。
-- `-XX:+UseLWPSynchronization`: 设置基于 LWP (轻量级进程)的同步策略,而不是基于线程的同步。
-- `-XX:LargePageSizeInBytes`: 设置用于 Java 堆的较大页面大小; 它采用 GB/MB/KB 的参数; 页面大小越大,我们可以更好地利用虚拟内存硬件资源; 然而,这可能会导致 PermGen 的空间大小更大,这反过来又会迫使 Java 堆空间的大小减小。
-- `-XX:MaxHeapFreeRatio` : 设置 GC 后, 堆空闲的最大百分比,以避免收缩。
-- `-XX:SurvivorRatio` : eden/survivor 空间的比例, 例如`-XX:SurvivorRatio=6` 设置每个 survivor 和 eden 之间的比例为 1:6。
-- `-XX:+UseLargePages` : 如果系统支持,则使用大页面内存; 请注意,如果使用这个 JVM 参数,OpenJDK 7 可能会崩溃。
-- `-XX:+UseStringCache` : 启用 String 池中可用的常用分配字符串的缓存。
-- `-XX:+UseCompressedStrings` : 对 String 对象使用 `byte []` 类型,该类型可以用纯 ASCII 格式表示。
-- `-XX:+OptimizeStringConcat` : 它尽可能优化字符串串联操作。
-
-## 文章推荐
-
-这里推荐了非常多优质的 JVM 实践相关的文章,推荐阅读,尤其是 JVM 性能优化和问题排查相关的文章。
-
-- [JVM 参数配置说明 - 阿里云官方文档 - 2022](https://help.aliyun.com/document_detail/148851.html)
-- [JVM 内存配置最佳实践 - 阿里云官方文档 - 2022](https://help.aliyun.com/document_detail/383255.html)
-- [求你了,GC 日志打印别再瞎配置了 - 思否 - 2022](https://segmentfault.com/a/1190000039806436)
-- [一次大量 JVM Native 内存泄露的排查分析(64M 问题) - 掘金 - 2022](https://juejin.cn/post/7078624931826794503)
-- [一次线上 JVM 调优实践,FullGC40 次/天到 10 天一次的优化过程 - HeapDump - 2021](https://heapdump.cn/article/1859160)
-- [听说 JVM 性能优化很难?今天我小试了一把! - 陈树义 - 2021](https://shuyi.tech/archives/have-a-try-in-jvm-combat)
-- [你们要的线上 GC 问题案例来啦 - 编了个程 - 2021](https://mp.weixin.qq.com/s/df1uxHWUXzhErxW1sZ6OvQ)
-- [Java 中 9 种常见的 CMS GC 问题分析与解决 - 美团技术团队 - 2020](https://tech.meituan.com/2020/11/12/java-9-cms-gc.html)
-- [从实际案例聊聊 Java 应用的 GC 优化-美团技术团队 - 美团技术团队 - 2017](https://tech.meituan.com/2017/12/29/jvm-optimize.html)
+## 其他常用参数
+
+- `-server`: 明确启用 Server 模式的 HotSpot VM。(在 64 位 JVM 上通常是默认值)。
+- `-XX:+UseStringDeduplication`: (JDK 8u20+) 尝试识别并共享底层 `char[]` 数组相同的 String 对象,以减少内存占用。适用于存在大量重复字符串的场景。
+- `-XX:SurvivorRatio=`: 设置 Eden 区与单个 Survivor 区的大小比例。例如 `-XX:SurvivorRatio=8` 表示 Eden:Survivor = 8:1。
+- `-XX:MaxTenuringThreshold=`: 设置对象从新生代晋升到老年代的最大年龄阈值(对象每经历一次 Minor GC 且存活,年龄加 1)。默认值通常是 15。
+- `-XX:+DisableExplicitGC`: 禁止代码中显式调用 `System.gc()`。推荐开启,避免人为触发不必要的 Full GC。
+- `-XX:+UseLargePages`: (需要操作系统支持) 尝试使用大内存页(如 2MB 而非 4KB),可能提升内存密集型应用的性能,但需谨慎测试。
+- -`XX:MinHeapFreeRatio= / -XX:MaxHeapFreeRatio=`: 控制 GC 后堆内存保持空闲的最小/最大百分比,用于动态调整堆大小(如果 `-Xms` 和 `-Xmx` 不相等)。通常建议将 `-Xms` 和 `-Xmx` 设为一致,避免调整开销。
+
+**注意:** 以下参数在现代 JVM 版本中可能已**弃用、移除或默认开启且无需手动设置**:
+
+- `-XX:+UseLWPSynchronization`: 较旧的同步策略选项,现代 JVM 通常有更优化的实现。
+- `-XX:LargePageSizeInBytes`: 通常由 `-XX:+UseLargePages` 自动确定或通过 OS 配置。
+- `-XX:+UseStringCache`: 已被移除。
+- `-XX:+UseCompressedStrings`: 已被 Java 9 及之后默认开启的 Compact Strings 特性取代。
+- `-XX:+OptimizeStringConcat`: 字符串连接优化(invokedynamic)在 Java 9 及之后是默认行为。
+
+## 总结
+
+本文为 Java 开发者提供了一份实用的 JVM 常用参数配置指南,旨在帮助读者理解和优化 Java 应用的性能与稳定性。文章重点强调了以下几个方面:
+
+1. **堆内存配置:** 建议显式设置初始与最大堆内存 (`-Xms`, -`Xmx`,通常设为一致) 和新生代大小 (`-Xmn` 或 `-XX:NewSize/-XX:MaxNewSize`),这对 GC 性能至关重要。
+2. **元空间管理 (Java 8+):** 澄清了 `-XX:MetaspaceSize` 的实际作用(首次触发 Full GC 的阈值,而非初始容量),并强烈建议设置 `-XX:MaxMetaspaceSize` 以防止潜在的本地内存耗尽。
+3. **垃圾收集器选择与日志:**介绍了不同 GC 算法的适用场景,并强调在生产和测试环境中开启详细 GC 日志 (`-Xloggc`, `-XX:+PrintGCDetails` 等) 对于问题排查的必要性。
+4. **OOM 故障排查:** 说明了如何通过 `-XX:+HeapDumpOnOutOfMemoryError` 等参数在发生 OOM 时自动生成堆转储文件,以便进行后续的内存泄漏分析。
+5. **其他参数:** 简要介绍了如字符串去重等其他有用参数,并指出了部分旧参数的现状。
+
+具体的问题排查和调优案例,可以参考笔者整理的这篇文章:[JVM 线上问题排查和性能调优案例](https://javaguide.cn/java/jvm/jvm-in-action.html)。
diff --git a/docs/java/jvm/memory-area.md b/docs/java/jvm/memory-area.md
index 1d283e9e667..27e882cbbb7 100644
--- a/docs/java/jvm/memory-area.md
+++ b/docs/java/jvm/memory-area.md
@@ -1,8 +1,13 @@
---
title: Java内存区域详解(重点)
+description: JVM内存区域详解:深入剖析Java运行时数据区(堆、方法区、虚拟机栈、本地方法栈、程序计数器)、对象创建过程、内存分配策略、对象访问定位方式。
category: Java
tag:
- JVM
+head:
+ - - meta
+ - name: keywords
+ content: JVM内存区域,运行时数据区,堆内存,方法区,虚拟机栈,程序计数器,对象创建,Java内存模型
---
@@ -51,6 +56,43 @@ Java 虚拟机规范对于运行时数据区域的规定是相当宽松的。以
### 程序计数器
+```mermaid
+graph LR
+ %% 颜色定义
+ classDef main fill:#005D7B,stroke:#fff,stroke-width:2px,color:#fff;
+ classDef feature fill:#00838F,stroke:#fff,stroke-width:2px,color:#fff;
+ classDef function fill:#4CA497,stroke:#fff,stroke-width:2px,color:#fff;
+ classDef state fill:#E99151,stroke:#fff,stroke-width:2px,color:#fff;
+ classDef lifecycle fill:#E4C189,stroke:#333,stroke-width:2px,color:#333;
+ classDef warning fill:#C44545,stroke:#fff,stroke-width:2px,color:#fff;
+
+ %% 核心节点
+ Root(JVM 程序计数器):::main
+
+ %% 分支1:基本特性
+ Root --> Attr[核心特性]:::feature
+ Attr --> Attr1[线程私有/独立存储]:::feature
+ Attr --> Attr2[较小内存空间]:::feature
+
+ %% 分支2:核心功能
+ Root --> Func[主要功能]:::function
+ Func --> Func1[代码流程控制: 分支/循环/异常]:::function
+ Func --> Func2[线程恢复: 记录切换位置]:::function
+
+ %% 分支3:执行状态
+ Root --> Run[执行状态]:::state
+ Run --> Run1[Java方法: 记录字节码指令地址]:::state
+ Run --> Run2[Native方法: Undefined]:::state
+
+ %% 分支4:生命周期与异常
+ Root --> Life[生命周期与异常]:::lifecycle
+ Life --> Life1[随线程创建而创建/销毁]:::lifecycle
+ Life --> Life2[唯一不报 OutOfMemoryError 区域]:::warning
+
+ %% 线条样式
+ linkStyle default stroke:#005D7B,stroke-width:2px;
+```
+
程序计数器是一块较小的内存空间,可以看作是当前线程所执行的字节码的行号指示器。字节码解释器工作时通过改变这个计数器的值来选取下一条需要执行的字节码指令,分支、循环、跳转、异常处理、线程恢复等功能都需要依赖这个计数器来完成。
另外,为了线程切换后能恢复到正确的执行位置,每条线程都需要有一个独立的程序计数器,各线程之间计数器互不影响,独立存储,我们称这类内存区域为“线程私有”的内存。
@@ -60,10 +102,47 @@ Java 虚拟机规范对于运行时数据区域的规定是相当宽松的。以
- 字节码解释器通过改变程序计数器来依次读取指令,从而实现代码的流程控制,如:顺序执行、选择、循环、异常处理。
- 在多线程的情况下,程序计数器用于记录当前线程执行的位置,从而当线程被切换回来的时候能够知道该线程上次运行到哪儿了。
-⚠️ 注意:程序计数器是唯一一个不会出现 `OutOfMemoryError` 的内存区域,它的生命周期随着线程的创建而创建,随着线程的结束而死亡。
+程序计数器的生命周期与线程完全同步:
+
+- **创建**:随着线程的创建而创建。
+- **销毁**:随着线程的结束而销毁。
+
+在执行 **Java 方法**(非 native)时,程序计数器记录的是 **当前正在执行的 JVM 字节码指令的地址**。当线程执行的是一个 **native 方法**(本地方法)时,程序计数器的值为 **Undefined(未定义)**。这是因为 native 方法不执行 JVM 字节码,而是通过 JNI 调用本地平台的底层代码,JVM 无需再跟踪字节码地址。
+
+⚠️ 注意:程序计数器是 JVM 规范中唯一没有规定任何 `OutOfMemoryError` 情况的内存区域。这是因为它的内存占用极小且固定,不会出现内存溢出的情况。
### Java 虚拟机栈
+```mermaid
+graph LR
+ %% 颜色定义
+ classDef main fill:#005D7B,stroke:#fff,stroke-width:2px,color:#fff;
+ classDef compare fill:#00838F,stroke:#fff,stroke-width:2px,color:#fff;
+ classDef structure fill:#4CA497,stroke:#fff,stroke-width:2px,color:#fff;
+ classDef error fill:#C44545,stroke:#fff,stroke-width:2px,color:#fff;
+
+ %% 核心节点
+ Root(虚拟机栈 Java Stack):::main
+
+ %% 分支1:定义与对比
+ Root --> Comp[基本特征]:::compare
+ Comp --> Comp1[线程私有,随线程创建/销毁]:::compare
+ Comp --> Comp2[服务对象: Java 方法]:::compare
+ Comp --> Comp3[栈帧先进后出]:::compare
+
+ %% 分支2:栈帧结构
+ Root --> Struct[栈帧结构]:::structure
+ Struct --> S1[局部变量表、操作数栈、动态链接、出口信息]:::structure
+
+ %% 分支3:异常情况
+ Root --> Err[异常情况]:::error
+ Err --> Err1[StackOverflowError: 栈深度溢出]:::error
+ Err --> Err2[OutOfMemoryError: 内存扩展失败]:::error
+
+ %% 线条样式
+ linkStyle default stroke:#005D7B,stroke-width:2px;
+```
+
与程序计数器一样,Java 虚拟机栈(后文简称栈)也是线程私有的,它的生命周期和线程相同,随着线程的创建而创建,随着线程的死亡而死亡。
栈绝对算的上是 JVM 运行时数据区域的一个核心,除了一些 Native 方法调用是通过本地方法栈实现的(后面会提到),其他所有的 Java 方法调用都是通过栈来实现的(也需要和其他运行时数据区域比如程序计数器配合)。
@@ -80,25 +159,64 @@ Java 虚拟机规范对于运行时数据区域的规定是相当宽松的。以
**操作数栈** 主要作为方法调用的中转站使用,用于存放方法执行过程中产生的中间计算结果。另外,计算过程中产生的临时变量也会放在操作数栈中。
-**动态链接** 主要服务一个方法需要调用其他方法的场景。Class 文件的常量池里保存有大量的符号引用比如方法引用的符号引用。当一个方法要调用其他方法,需要将常量池中指向方法的符号引用转化为其在内存地址中的直接引用。动态链接的作用就是为了将符号引用转换为调用方法的直接引用,这个过程也被称为 **动态连接** 。
+**动态链接**是 Java 虚拟机实现方法调用的关键机制之一。在 Class 文件中,方法调用以**符号引用**的形式存在于常量池。为了执行调用,这些符号引用必须被转换为内存中的**直接引用**。这个转换过程分为两种情况:对于静态方法、私有方法等在编译期就能确定版本的方法,这个转换在**类加载的解析阶段**就完成了,这称为**静态解析**。而对于需要根据对象实际类型才能确定具体实现的**虚方法**(这是实现多态的基础),这个转换过程则被推迟到**程序运行期间**,由**动态链接**来完成。因此,**动态链接**的核心作用是**在运行时解析虚方法的调用点,将其链接到正确的方法版本上**。

栈空间虽然不是无限的,但一般正常调用的情况下是不会出现问题的。不过,如果函数调用陷入无限循环的话,就会导致栈中被压入太多栈帧而占用太多空间,导致栈空间过深。那么当线程请求栈的深度超过当前 Java 虚拟机栈的最大深度的时候,就抛出 `StackOverFlowError` 错误。
-Java 方法有两种返回方式,一种是 return 语句正常返回,一种是抛出异常。不管哪种返回方式,都会导致栈帧被弹出。也就是说, **栈帧随着方法调用而创建,随着方法结束而销毁。无论方法正常完成还是异常完成都算作方法结束。**
+**Java 方法有两种返回方式**:
+
+- **正常返回**:执行return语句,返回值传递给调用者。
+- **异常返回**:方法执行过程中抛出异常且未被捕获。
+
+不管哪种返回方式,都会导致栈帧被弹出。也就是说, **栈帧随着方法调用而创建,随着方法结束而销毁。无论方法正常完成还是异常完成都算作方法结束。**
-除了 `StackOverFlowError` 错误之外,栈还可能会出现`OutOfMemoryError`错误,这是因为如果栈的内存大小可以动态扩展, 如果虚拟机在动态扩展栈时无法申请到足够的内存空间,则抛出`OutOfMemoryError`异常。
+除了 `StackOverFlowError` 错误之外,栈还可能会出现`OutOfMemoryError`错误,这是因为如果栈的内存大小可以动态扩展, 那么当虚拟机在动态扩展栈时无法申请到足够的内存空间,则抛出`OutOfMemoryError`异常。
简单总结一下程序运行中栈可能会出现两种错误:
-- **`StackOverFlowError`:** 若栈的内存大小不允许动态扩展,那么当线程请求栈的深度超过当前 Java 虚拟机栈的最大深度的时候,就抛出 `StackOverFlowError` 错误。
-- **`OutOfMemoryError`:** 如果栈的内存大小可以动态扩展, 如果虚拟机在动态扩展栈时无法申请到足够的内存空间,则抛出`OutOfMemoryError`异常。
+- **`StackOverFlowError`:** 如果栈的内存大小不允许动态扩展,那么当线程请求栈的深度超过当前 Java 虚拟机栈的最大深度的时候,就抛出 `StackOverFlowError` 错误。
+- **`OutOfMemoryError`:** 如果栈的内存大小可以动态扩展, 那么当虚拟机在动态扩展栈时无法申请到足够的内存空间,则抛出`OutOfMemoryError`异常。

### 本地方法栈
+```mermaid
+graph LR
+ %% 颜色定义
+ classDef main fill:#005D7B,stroke:#fff,stroke-width:2px,color:#fff;
+ classDef compare fill:#00838F,stroke:#fff,stroke-width:2px,color:#fff;
+ classDef structure fill:#4CA497,stroke:#fff,stroke-width:2px,color:#fff;
+ classDef implement fill:#E99151,stroke:#fff,stroke-width:2px,color:#fff;
+ classDef error fill:#C44545,stroke:#fff,stroke-width:2px,color:#fff;
+
+ %% 核心节点
+ Root(本地方法栈):::main
+
+ %% 分支1:定义与对比
+ Root --> Comp[定义与对比]:::compare
+ Comp --> Comp1[作用与虚拟机栈相似]:::compare
+ Comp --> Comp2[服务对象: Native 方法]:::compare
+
+ %% 分支2:HotSpot 实现
+ Root --> Imp[虚拟机实现]:::implement
+ Imp --> Imp1[HotSpot 与虚拟机栈合二为一]:::implement
+
+ %% 分支3:栈帧结构
+ Root --> Struct[栈帧内容]:::structure
+ Struct --> S1[局部变量表、操作数栈、动态链接、出口信息]:::structure
+
+ %% 分支4:异常情况
+ Root --> Err[异常与内存]:::error
+ Err --> Err1[StackOverflowError: 栈深度溢出]:::error
+ Err --> Err2[OutOfMemoryError: 内存扩展失败]:::error
+
+ %% 线条样式
+ linkStyle default stroke:#005D7B,stroke-width:2px;
+```
+
和虚拟机栈所发挥的作用非常相似,区别是:**虚拟机栈为虚拟机执行 Java 方法 (也就是字节码)服务,而本地方法栈则为虚拟机使用到的 Native 方法服务。** 在 HotSpot 虚拟机中和 Java 虚拟机栈合二为一。
本地方法被执行的时候,在本地方法栈也会创建一个栈帧,用于存放该本地方法的局部变量表、操作数栈、动态链接、出口信息。
@@ -107,6 +225,42 @@ Java 方法有两种返回方式,一种是 return 语句正常返回,一种
### 堆
+```mermaid
+graph LR
+ %% 颜色定义
+ classDef main fill:#005D7B,stroke:#fff,stroke-width:2px,color:#fff;
+ classDef compare fill:#00838F,stroke:#fff,stroke-width:2px,color:#fff;
+ classDef structure fill:#4CA497,stroke:#fff,stroke-width:2px,color:#fff;
+ classDef implement fill:#E99151,stroke:#fff,stroke-width:2px,color:#fff;
+ classDef error fill:#C44545,stroke:#fff,stroke-width:2px,color:#fff;
+
+ %% 核心节点
+ Root(Java 堆):::main
+
+ %% 分支1:基本定义与地位
+ Root --> Def[定义与地位]:::compare
+ Def --> Def1[JVM 内存中最大区域]:::compare
+ Def --> Def2[所有线程共享]:::compare
+ Def --> Def3[虚拟机启动时创建,生命周期长]:::compare
+
+ %% 分支2:核心用途
+ Root --> Use[核心用途]:::structure
+ Use --> Use1[存放对象实例(非静态字段)]:::structure
+ Use --> Use2[存放数组数据]:::structure
+ Use --> Use3[对象内存统一管理]:::structure
+
+ %% 分3:分代结构 (GC 堆)
+ Root --> GC[分代结构]:::implement
+ Root --> GC[分代结构]:::implement
+ GC --> GC1[新生代:Eden 区 + 两个 Survivor 区]:::implement
+ GC --> GC2[老年代:Old Generation]:::implement
+ GC --> GC3[目的:优化垃圾回收效率]:::implement
+
+
+ %% 线条样式
+ linkStyle default stroke:#005D7B,stroke-width:2px;
+```
+
Java 虚拟机所管理的内存中最大的一块,Java 堆是所有线程共享的一块内存区域,在虚拟机启动时创建。**此内存区域的唯一目的就是存放对象实例,几乎所有的对象实例以及数组都在这里分配内存。**
Java 世界中“几乎”所有的对象都在堆中分配,但是,随着 JIT 编译器的发展与逃逸分析技术逐渐成熟,栈上分配、标量替换优化技术将会导致一些微妙的变化,所有的对象都分配到堆上也渐渐变得不那么“绝对”了。从 JDK 1.7 开始已经默认开启逃逸分析,如果某些方法中的对象引用没有被返回或者未被外面使用(也就是未逃逸出去),那么对象可以直接在栈上分配内存。
@@ -173,11 +327,57 @@ MaxTenuringThreshold of 20 is invalid; must be between 0 and 15
### 方法区
+```mermaid
+graph LR
+ %% 颜色定义
+ classDef main fill:#005D7B,stroke:#fff,stroke-width:2px,color:#fff;
+ classDef compare fill:#00838F,stroke:#fff,stroke-width:2px,color:#fff;
+ classDef structure fill:#4CA497,stroke:#fff,stroke-width:2px,color:#fff;
+ classDef implement fill:#E99151,stroke:#fff,stroke-width:2px,color:#fff;
+ classDef error fill:#C44545,stroke:#fff,stroke-width:2px,color:#fff;
+
+ %% 核心节点
+ Root(方法区):::main
+
+ %% 分支1:基本定义与地位
+ Root --> Def[定义与地位]:::compare
+ Def --> Def1[线程共享的内存区域]:::compare
+ Def --> Def2[JVM 规范定义的逻辑区域]:::compare
+ Def --> Def3[具体实现随虚拟机而异]:::compare
+
+ %% 分支2:核心存储内容
+ Root --> Store[核心存储内容]:::structure
+ Store --> Store1[类的元数据: 结构/字段/方法信息]:::structure
+ Store --> Store2[方法的字节码: 原始指令序列]:::structure
+ Store --> Store3[运行时常量池: 字面量与符号引用]:::structure
+
+ %% 分支3:HotSpot 位置演变 (JDK 7+)
+ Root --> Change[位置演变与例外]:::implement
+ Change --> Change1[静态变量: 移至 Java 堆(JDK 7)]:::implement
+ Change --> Change2[字符串常量池: 移至 Java 堆(JDK 7)]:::implement
+ Change --> Change3[JIT 代码缓存: 独立 Code Cache 区域]:::implement
+
+ %% 线条样式
+ linkStyle default stroke:#005D7B,stroke-width:2px;
+```
+
方法区属于是 JVM 运行时数据区域的一块逻辑区域,是各个线程共享的内存区域。
《Java 虚拟机规范》只是规定了有方法区这么个概念和它的作用,方法区到底要如何实现那就是虚拟机自己要考虑的事情了。也就是说,在不同的虚拟机实现上,方法区的实现是不同的。
-当虚拟机要使用一个类时,它需要读取并解析 Class 文件获取相关信息,再将信息存入到方法区。方法区会存储已被虚拟机加载的 **类信息、字段信息、方法信息、常量、静态变量、即时编译器编译后的代码缓存等数据**。
+当虚拟机加载一个类时,它会从 Class 文件中解析出相应的信息,并将这些**元数据**存入方法区。具体来说,方法区主要存储以下核心数据:
+
+1. **类的元数据**:包括类的完整结构,如类名、父类、实现的接口、访问修饰符,以及字段和方法的详细信息(名称、类型、修饰符等)。
+2. **方法的字节码**:每个方法的原始指令序列。
+3. **运行时常量池**:每个类独有的,由 Class 文件中的常量池转换而来,用于存放编译期生成的各种字面量和对类型、字段、方法的符号引用。
+
+需要特别注意的是,以下几类数据虽然在逻辑上与类相关,但在 HotSpot 虚拟机中,它们并不存储在方法区内:
+
+- **静态变量(Static Variables)**:自 JDK 7 起,静态变量已从方法区(永久代)**移至 Java 堆(Heap)中**,与该类的 `java.lang.Class` 对象一起存放。
+- **字符串常量池(String Pool)**:同样自 JDK 7 起,字符串常量池也**移至 Java 堆中**。
+- **即时编译器编译后的代码缓存(JIT Code Cache)**:JIT 编译器将热点方法的字节码编译成的本地机器码,存放在一个**独立的、名为“Code Cache”的内存区域**,而不是方法区本身。这样做是为了实现更高效的执行和内存管理。
+
+
**方法区和永久代以及元空间是什么关系呢?** 方法区和永久代以及元空间的关系很像 Java 中接口和类的关系,类实现了接口,这里的类就可以看作是永久代和元空间,接口可以看作是方法区,也就是说永久代以及元空间是 HotSpot 虚拟机对虚拟机规范中方法区的两种实现方式。并且,永久代是 JDK 1.8 之前的方法区实现,JDK 1.8 及以后方法区的实现变成了元空间。
@@ -223,6 +423,37 @@ JDK 1.8 的时候,方法区(HotSpot 的永久代)被彻底移除了(JDK1
### 运行时常量池
+```mermaid
+graph LR
+ %% 颜色定义
+ classDef main fill:#005D7B,stroke:#fff,stroke-width:2px,color:#fff;
+ classDef compare fill:#00838F,stroke:#fff,stroke-width:2px,color:#fff;
+ classDef structure fill:#4CA497,stroke:#fff,stroke-width:2px,color:#fff;
+ classDef implement fill:#E99151,stroke:#fff,stroke-width:2px,color:#fff;
+ classDef error fill:#C44545,stroke:#fff,stroke-width:2px,color:#fff;
+
+ %% 核心节点
+ Root(运行时常量池):::main
+
+ %% 分支1:来源与地位
+ Root --> Source[定义与地位]:::compare
+ Source --> Source1[源自 Class 文件的常量池表]:::compare
+ Source --> Source2[类加载后存入方法区]:::compare
+ Source --> Source3[功能类似于高级符号表]:::compare
+
+ %% 分支2:存储内容分类
+ Root --> Content[存储内容]:::structure
+ Content --> Content1[字面量: 文本字符串/常量值等]:::structure
+ Content --> Content2[符号引用: 类/字段/方法的描述]:::structure
+
+ %% 分支3:异常处理
+ Root --> Error[异常情况]:::error
+ Error --> Error2[无法申请内存时抛出 OutOfMemoryError]:::error
+
+ %% 线条样式
+ linkStyle default stroke:#005D7B,stroke-width:2px;
+```
+
Class 文件中除了有类的版本、字段、方法、接口等描述信息外,还有用于存放编译期生成的各种字面量(Literal)和符号引用(Symbolic Reference)的 **常量池表(Constant Pool Table)** 。
字面量是源代码中的固定值的表示法,即通过字面我们就能知道其值的含义。字面量包括整数、浮点数和字符串字面量。常见的符号引用包括类符号引用、字段符号引用、方法符号引用、接口方法符号。
@@ -239,20 +470,54 @@ Class 文件中除了有类的版本、字段、方法、接口等描述信息
### 字符串常量池
+```mermaid
+graph LR
+ %% 颜色定义
+ classDef main fill:#005D7B,stroke:#fff,stroke-width:2px,color:#fff;
+ classDef compare fill:#00838F,stroke:#fff,stroke-width:2px,color:#fff;
+ classDef structure fill:#4CA497,stroke:#fff,stroke-width:2px,color:#fff;
+ classDef implement fill:#E99151,stroke:#fff,stroke-width:2px,color:#fff;
+ classDef error fill:#C44545,stroke:#fff,stroke-width:2px,color:#fff;
+
+ %% 核心节点
+ Root(字符串常量池):::main
+
+ %% 分支1:内存位置演进
+ Root --> History[内存位置演进]:::compare
+ History --> Hist1[JDK 1.6: 存在于永久代 PermGen]:::compare
+ History --> Hist2[JDK 1.7+: 移至堆 Heap 中]:::compare
+ History --> Hist3[目的: 避免永久代 OOM 且方便 GC]:::compare
+
+ %% 分支2:底层实现结构
+ Root --> Impl[底层实现机制]:::structure
+ Impl --> Impl1[StringTable: 本质是 HashTable]:::structure
+ Impl --> Impl2[Key: 字符串内容 Hash / Value: 对象引用]:::structure
+ Impl --> Impl3[固定长度的数组 + 链表结构]:::structure
+
+ %% 分支3:风险与调优
+ Root --> Tuning[风险与调优]:::error
+ Tuning --> Risk1[StringTable 过小导致 Hash 冲突严重]:::error
+ Tuning --> Risk2[大量 intern 导致性能下降]:::error
+ Tuning --> Param[-XX:StringTableSize 调优参数]:::error
+
+ %% 线条样式
+ linkStyle default stroke:#005D7B,stroke-width:2px;
+```
+
**字符串常量池** 是 JVM 为了提升性能和减少内存消耗针对字符串(String 类)专门开辟的一块区域,主要目的是为了避免字符串的重复创建。
```java
-// 在堆中创建字符串对象”ab“
-// 将字符串对象”ab“的引用保存在字符串常量池中
+// 1.在字符串常量池中查询字符串对象 "ab",如果没有则创建"ab"并放入字符串常量池
+// 2.将字符串对象 "ab" 的引用赋值给 aa
String aa = "ab";
-// 直接返回字符串常量池中字符串对象”ab“的引用
+// 直接返回字符串常量池中字符串对象 "ab",赋值给引用 bb
String bb = "ab";
-System.out.println(aa==bb);// true
+System.out.println(aa==bb); // true
```
HotSpot 虚拟机中字符串常量池的实现是 `src/hotspot/share/classfile/stringTable.cpp` ,`StringTable` 可以简单理解为一个固定大小的`HashTable` ,容量为 `StringTableSize`(可以通过 `-XX:StringTableSize` 参数来设置),保存的是字符串(key)和 字符串对象的引用(value)的映射关系,字符串对象的引用指向堆中的字符串对象。
-JDK1.7 之前,字符串常量池存放在永久代。JDK1.7 字符串常量池和静态变量从永久代移动了 Java 堆中。
+JDK1.7 之前,字符串常量池存放在永久代。JDK1.7 字符串常量池和静态变量从永久代移动到了 Java 堆中。

@@ -270,6 +535,40 @@ JDK1.7 之前,字符串常量池存放在永久代。JDK1.7 字符串常量池
### 直接内存
+```mermaid
+graph LR
+ %% 颜色定义
+ classDef main fill:#005D7B,stroke:#fff,stroke-width:2px,color:#fff;
+ classDef compare fill:#00838F,stroke:#fff,stroke-width:2px,color:#fff;
+ classDef structure fill:#4CA497,stroke:#fff,stroke-width:2px,color:#fff;
+ classDef implement fill:#E99151,stroke:#fff,stroke-width:2px,color:#fff;
+ classDef error fill:#C44545,stroke:#fff,stroke-width:2px,color:#fff;
+
+ %% 核心节点
+ Root(直接内存):::main
+
+ %% 分支1:定义与地位
+ Root --> Source[定义与地位]:::compare
+ Source --> Source1[非运行时数据区的一部分]:::compare
+ Source --> Source2[非 JVM 规范定义的内存区域]:::compare
+ Source --> Source3[通过 JNI 在本地内存分配]:::compare
+
+ %% 分支2:核心优势
+ Root --> Advantage[核心优势]:::implement
+ Advantage --> Adv1[避免 Java 堆与 Native 堆来回复制数据]:::implement
+ Advantage --> Adv2[显著提高 I/O 性能]:::implement
+ Advantage --> Adv3[减少垃圾回收对应用的影响]:::implement
+
+ %% 分支3:限制与异常
+ Root --> Error[限制与异常]:::error
+ Error --> Error1[不受 Java 堆大小限制]:::error
+ Error --> Error2[受本机总内存及寻址空间限制]:::error
+ Error --> Error3[内存不足时抛出 OutOfMemoryError]:::error
+
+ %% 线条样式
+ linkStyle default stroke:#005D7B,stroke-width:2px;
+```
+
直接内存是一种特殊的内存缓冲区,并不在 Java 堆或方法区中分配的,而是通过 JNI 的方式在本地内存上分配的。
直接内存并不是虚拟机运行时数据区的一部分,也不是虚拟机规范中定义的内存区域,但是这部分内存也被频繁地使用。而且也可能导致 `OutOfMemoryError` 错误出现。
@@ -290,6 +589,45 @@ JDK1.4 中新加入的 **NIO(Non-Blocking I/O,也被称为 New I/O)**,
Java 对象的创建过程我建议最好是能默写出来,并且要掌握每一步在做什么。
+```mermaid
+graph TD
+ %% 颜色定义
+ classDef root fill:#004D61,stroke:#fff,stroke-width:2px,color:#fff;
+ classDef step fill:#005D7B,stroke:#fff,stroke-width:2px,color:#fff;
+ classDef detail fill:#4CA497,stroke:#fff,stroke-width:2px,color:#fff;
+ classDef logic fill:#E99151,stroke:#fff,stroke-width:2px,color:#fff;
+
+ %% 核心流程
+ Start(new 指令触发):::root
+
+ Start --> S1[Step 1: 类加载检查]:::step
+ S1 --> S1_1[检查常量池是否有类符号引用]:::detail
+ S1_1 --> S1_2[检查类是否已加载/解析/初始化]:::detail
+
+ S1_2 --> S2[Step 2: 分配内存]:::step
+ S2 --> S2_Method{分配方式}:::logic
+ S2_Method -->|堆内存规整| S2_A[指针碰撞]:::logic
+ S2_Method -->|堆内存交错| S2_B[空闲列表]:::logic
+ S2_A & S2_B --> S2_Safe[并发安全: TLAB 或 CAS 重试]:::detail
+
+ S2_Safe --> S3[Step 3: 初始化零值]:::step
+ S3 --> S3_1[将分配到的内存空间初始化为 0]:::detail
+ S3_1 --> S3_2[保证实例字段不赋初值即可直接使用]:::detail
+
+ S3_2 --> S4[Step 4: 设置对象头]:::step
+ S4 --> S4_1[Mark Word: 哈希码/GC分代年龄/锁状态]:::detail
+ S4_1 --> S4_2[Klass Pointer: 元数据指针指向类]:::detail
+
+ S4_2 --> S5[Step 5: 执行 init 方法]:::step
+ S5 --> S5_1[按照程序员意愿进行初始化]:::detail
+ S5_1 --> S5_2[执行构造方法]:::detail
+
+ S5_2 --> End((对象创建完成)):::root
+
+ %% 线条样式
+ linkStyle default stroke:#005D7B,stroke-width:2px;
+```
+
#### Step1:类加载检查
虚拟机遇到一条 new 指令时,首先将去检查这个指令的参数是否能在常量池中定位到这个类的符号引用,并且检查这个符号引用代表的类是否已被加载过、解析和初始化过。如果没有,那必须先执行相应的类加载过程。
@@ -337,7 +675,7 @@ Java 对象的创建过程我建议最好是能默写出来,并且要掌握每
对象头包括两部分信息:
1. 标记字段(Mark Word):用于存储对象自身的运行时数据, 如哈希码(HashCode)、GC 分代年龄、锁状态标志、线程持有的锁、偏向线程 ID、偏向时间戳等等。
-2. 类型指针(Klass Word):对象指向它的类元数据的指针,虚拟机通过这个指针来确定这个对象是哪个类的实例。
+2. 类型指针(Klass pointer):对象指向它的类元数据的指针,虚拟机通过这个指针来确定这个对象是哪个类的实例。
**实例数据部分是对象真正存储的有效信息**,也是在程序中所定义的各种类型的字段内容。
diff --git a/docs/java/new-features/java10.md b/docs/java/new-features/java10.md
index d52cac575b2..f3fae5934c5 100644
--- a/docs/java/new-features/java10.md
+++ b/docs/java/new-features/java10.md
@@ -1,8 +1,13 @@
---
title: Java 10 新特性概览
+description: 概览 JDK 10 的主要更新,重点介绍 var 类型推断与其他平台改进。
category: Java
tag:
- Java新特性
+head:
+ - - meta
+ - name: keywords
+ content: Java 10,JDK10,var 局部变量类型推断,垃圾回收改进,性能
---
**Java 10** 发布于 2018 年 3 月 20 日,最知名的特性应该是 `var` 关键字(局部变量类型推断)的引入了,其他还有垃圾收集器改善、GC 改进、性能提升、线程管控等一批新特性。
@@ -53,7 +58,7 @@ var 并不会改变 Java 是一门静态类型语言的事实,编译器负责
## G1 并行 Full GC
-从 Java9 开始 G1 就了默认的垃圾回收器,G1 是以一种低延时的垃圾回收器来设计的,旨在避免进行 Full GC,但是 Java9 的 G1 的 FullGC 依然是使用单线程去完成标记清除算法,这可能会导致垃圾回收期在无法回收内存的时候触发 Full GC。
+从 Java9 开始 G1 就成了默认的垃圾回收器,G1 是以一种低延时的垃圾回收器来设计的,旨在避免进行 Full GC,但是 Java9 的 G1 的 FullGC 依然是使用单线程去完成标记清除算法,这可能会导致垃圾回收期在无法回收内存的时候触发 Full GC。
为了最大限度地减少 Full GC 造成的应用停顿的影响,从 Java10 开始,G1 的 FullGC 改为并行的标记清除算法,同时会使用与年轻代回收和混合回收相同的并行工作线程数量,从而减少了 Full GC 的发生,以带来更好的性能提升、更大的吞吐量。
@@ -81,11 +86,11 @@ list.stream().collect(Collectors.toUnmodifiableSet());
## Optional 增强
-`Optional` 新增了`orElseThrow()`方法来在没有值时抛出指定的异常。
+`Optional` 新增了一个无参的 `orElseThrow()` 方法,作为带参数的 `orElseThrow(Supplier extends X> exceptionSupplier)` 的简化版本,在没有值时默认抛出一个 NoSuchElementException 异常。
```java
-Optional.ofNullable(cache.getIfPresent(key))
- .orElseThrow(() -> new PrestoException(NOT_FOUND, "Missing entry found for key: " + key));
+Optional optional = Optional.empty();
+String result = optional.orElseThrow();
```
## 应用程序类数据共享(扩展 CDS 功能)
diff --git a/docs/java/new-features/java11.md b/docs/java/new-features/java11.md
index f9d076c4b95..62d6d3e340c 100644
--- a/docs/java/new-features/java11.md
+++ b/docs/java/new-features/java11.md
@@ -1,8 +1,13 @@
---
title: Java 11 新特性概览
+description: 总结 JDK 11 的更新,关注新 HTTP 客户端与字符串增强等实用特性。
category: Java
tag:
- Java新特性
+head:
+ - - meta
+ - name: keywords
+ content: Java 11,JDK11,LTS,HTTP 客户端,字符串 API,移除特性
---
**Java 11** 于 2018 年 9 月 25 日正式发布,这是很重要的一个版本!Java 11 和 2017 年 9 月份发布的 Java 9 以及 2018 年 3 月份发布的 Java 10 相比,其最大的区别就是:在长期支持(Long-Term-Support)方面,**Oracle 表示会对 Java 11 提供大力支持,这一支持将会持续至 2026 年 9 月。这是据 Java 8 以后支持的首个长期版本。**
@@ -77,7 +82,7 @@ System.out.println(op.isEmpty());//判断指定的 Optional 对象是否为空
ZGC 主要为了满足如下目标进行设计:
- GC 停顿时间不超过 10ms
-- 即能处理几百 MB 的小堆,也能处理几个 TB 的大堆
+- 既能处理几百 MB 的小堆,也能处理几个 TB 的大堆
- 应用吞吐能力不会下降超过 15%(与 G1 回收算法相比)
- 方便在此基础上引入新的 GC 特性和利用 colored 针以及 Load barriers 优化奠定基础
- 当前只支持 Linux/x64 位平台
diff --git a/docs/java/new-features/java12-13.md b/docs/java/new-features/java12-13.md
index 8b3207ab4d5..31678e915a7 100644
--- a/docs/java/new-features/java12-13.md
+++ b/docs/java/new-features/java12-13.md
@@ -1,8 +1,13 @@
---
title: Java 12 & 13 新特性概览
+description: 归纳 JDK 12/13 的特性更新,包含字符串增强、switch 改进与 GC 调整等。
category: Java
tag:
- Java新特性
+head:
+ - - meta
+ - name: keywords
+ content: Java 12,Java 13,字符串增强,切换表达式,垃圾回收,JEP
---
## Java12
diff --git a/docs/java/new-features/java14-15.md b/docs/java/new-features/java14-15.md
index 415ca543e4b..5de09ec5b95 100644
--- a/docs/java/new-features/java14-15.md
+++ b/docs/java/new-features/java14-15.md
@@ -1,8 +1,13 @@
---
title: Java 14 & 15 新特性概览
+description: 概览 JDK 14/15 的关键特性,如 record、文本块与空指针精准提示等语言增强。
category: Java
tag:
- Java新特性
+head:
+ - - meta
+ - name: keywords
+ content: Java 14,Java 15,record,文本块,NullPointerException 细节,模式匹配,JEP
---
## Java14
diff --git a/docs/java/new-features/java16.md b/docs/java/new-features/java16.md
index 60906c40020..53651a48ca5 100644
--- a/docs/java/new-features/java16.md
+++ b/docs/java/new-features/java16.md
@@ -1,8 +1,13 @@
---
title: Java 16 新特性概览
+description: 介绍 JDK 16 的语言与平台更新,包含记录类与其他 JEP 改动。
category: Java
tag:
- Java新特性
+head:
+ - - meta
+ - name: keywords
+ content: Java 16,JDK16,记录类改进,新 API,JEP,性能
---
Java 16 在 2021 年 3 月 16 日正式发布,非长期支持(LTS)版本。
diff --git a/docs/java/new-features/java17.md b/docs/java/new-features/java17.md
index e478f1f5c43..ef5172a79d1 100644
--- a/docs/java/new-features/java17.md
+++ b/docs/java/new-features/java17.md
@@ -1,8 +1,13 @@
---
title: Java 17 新特性概览(重要)
+description: 总结 JDK 17 的重要更新与 JEP,涵盖密封类、记录类与模式匹配等特性。
category: Java
tag:
- Java新特性
+head:
+ - - meta
+ - name: keywords
+ content: Java 17,JDK17,LTS,密封类,记录类,模式匹配,API 更新,JEP
---
Java 17 在 2021 年 9 月 14 日正式发布,是一个长期支持(LTS)版本。
diff --git a/docs/java/new-features/java18.md b/docs/java/new-features/java18.md
index 40fa7bb61df..f47467e364e 100644
--- a/docs/java/new-features/java18.md
+++ b/docs/java/new-features/java18.md
@@ -1,8 +1,13 @@
---
title: Java 18 新特性概览
+description: 概览 JDK 18 的更新与预览特性,理解新 API 带来的改进。
category: Java
tag:
- Java新特性
+head:
+ - - meta
+ - name: keywords
+ content: Java 18,JDK18,预览特性,API 更新,JEP
---
Java 18 在 2022 年 3 月 22 日正式发布,非长期支持版本。
diff --git a/docs/java/new-features/java19.md b/docs/java/new-features/java19.md
index c1afe97f38c..b131e48658d 100644
--- a/docs/java/new-features/java19.md
+++ b/docs/java/new-features/java19.md
@@ -1,8 +1,13 @@
---
title: Java 19 新特性概览
+description: 介绍 JDK 19 的预览特性与并发相关更新,为后续虚拟线程铺垫。
category: Java
tag:
- Java新特性
+head:
+ - - meta
+ - name: keywords
+ content: Java 19,JDK19,虚拟线程预览,结构化并发,外部函数 API,JEP
---
JDK 19 定于 2022 年 9 月 20 日正式发布以供生产使用,非长期支持版本。不过,JDK 19 中有一些比较重要的新特性值得关注。
@@ -36,7 +41,7 @@ Java 程序可以通过该 API 与 Java 运行时之外的代码和数据进行
Foreign Function & Memory API (FFM API) 定义了类和接口:
-- 分配外部内存:`MemorySegment`、、`MemoryAddress`和`SegmentAllocator`);
+- 分配外部内存:`MemorySegment`、`MemoryAddress`和`SegmentAllocator`;
- 操作和访问结构化的外部内存:`MemoryLayout`, `VarHandle`;
- 控制外部内存的分配和释放:`MemorySession`;
- 调用外部函数:`Linker`、`FunctionDescriptor`和`SymbolLookup`。
diff --git a/docs/java/new-features/java20.md b/docs/java/new-features/java20.md
index 9dd86a71c70..21d06032ca9 100644
--- a/docs/java/new-features/java20.md
+++ b/docs/java/new-features/java20.md
@@ -1,8 +1,13 @@
---
title: Java 20 新特性概览
+description: 总结 JDK 20 的语言与并发改动,延续虚拟线程与模式匹配相关增强。
category: Java
tag:
- Java新特性
+head:
+ - - meta
+ - name: keywords
+ content: Java 20,JDK20,记录模式预览,虚拟线程改进,语言增强,JEP
---
JDK 20 于 2023 年 3 月 21 日发布,非长期支持版本。
diff --git a/docs/java/new-features/java21.md b/docs/java/new-features/java21.md
index fbafea43437..665a092088a 100644
--- a/docs/java/new-features/java21.md
+++ b/docs/java/new-features/java21.md
@@ -1,8 +1,13 @@
---
title: Java 21 新特性概览(重要)
+description: 概览 JDK 21 的关键新特性与实践影响,重点介绍字符串模板、Sequenced Collections、分代 ZGC、虚拟线程等。
category: Java
tag:
- Java新特性
+head:
+ - - meta
+ - name: keywords
+ content: Java 21,JDK21,LTS,字符串模板,Sequenced Collections,分代 ZGC,记录模式,switch 模式匹配,虚拟线程,外部函数与内存 API
---
JDK 21 于 2023 年 9 月 19 日 发布,这是一个非常重要的版本,里程碑式。
@@ -85,7 +90,7 @@ String name = "Lokesh";
String message = STR."Greetings \{name}.";
//FMT
-String message = STR."Greetings %-12s\{name}.";
+String message = FMT."Greetings %-12s\{name}.";
//RAW
StringTemplate st = RAW."Greetings \{name}.";
@@ -198,7 +203,7 @@ Integer lastElement = linkedHashSet.getLast(); // 3
linkedHashSet.addFirst(0); //List contains: [0, 1, 2, 3]
linkedHashSet.addLast(4); //List contains: [0, 1, 2, 3, 4]
-System.out.println(linkedHashSet.reversed()); //Prints [5, 3, 2, 1, 0]
+System.out.println(linkedHashSet.reversed()); //Prints [4, 3, 2, 1, 0]
```
`SequencedMap` 接口继承了 `Map`接口, 提供了在集合两端访问、添加或删除键值对、获取包含 key 的 `SequencedSet`、包含 value 的 `SequencedCollection`、包含 entry(键值对) 的 `SequencedSet`以及获取集合的反向视图的方法。
@@ -297,7 +302,7 @@ static String formatterPatternSwitch(Object obj) {
}
```
-## JEP 442: 外部函数和内存 API(第三次预览)
+## JEP 442:外部函数和内存 API(第三次预览)
Java 程序可以通过该 API 与 Java 运行时之外的代码和数据进行互操作。通过高效地调用外部函数(即 JVM 之外的代码)和安全地访问外部内存(即不受 JVM 管理的内存),该 API 使 Java 程序能够调用本机库并处理本机数据,而不会像 JNI 那样危险和脆弱。
@@ -346,7 +351,7 @@ switch (b) {
## JEP 445:未命名类和实例 main 方法 (预览)
-这个特性主要简化了 `main` 方法的的声明。对于 Java 初学者来说,这个 `main` 方法的声明引入了太多的 Java 语法概念,不利于初学者快速上手。
+这个特性主要简化了 `main` 方法的声明。对于 Java 初学者来说,这个 `main` 方法的声明引入了太多的 Java 语法概念,不利于初学者快速上手。
没有使用该特性之前定义一个 `main` 方法:
diff --git a/docs/java/new-features/java22-23.md b/docs/java/new-features/java22-23.md
new file mode 100644
index 00000000000..eda78c7e684
--- /dev/null
+++ b/docs/java/new-features/java22-23.md
@@ -0,0 +1,434 @@
+---
+title: Java 22 & 23 新特性概览
+description: 概览 JDK 22/23 的关键 JEP 与语言/平台增强,持续追踪性能与并发相关改动。
+category: Java
+tag:
+ - Java新特性
+head:
+ - - meta
+ - name: keywords
+ content: Java 22,Java 23,JEP,Markdown 文档注释,类文件 API,向量 API,结构化并发,作用域值
+---
+
+JDK 23 和 JDK 22 一样,这也是一个非 LTS(长期支持)版本,Oracle 仅提供六个月的支持。下一个长期支持版是 JDK 25,预计明年 9 月份发布。
+
+下图是从 JDK8 到 JDK 24 每个版本的更新带来的新特性数量和更新时间:
+
+
+
+由于 JDK 22 和 JDK 23 重合的新特性较多,这里主要以 JDK 23 为主介绍,会补充 JDK 22 独有的一些特性。
+
+JDK 23 一共有 12 个新特性:
+
+- [JEP 455: 模式中的原始类型、instanceof 和 switch(预览)](https://openjdk.org/jeps/455)
+- [JEP 456: 类文件 API(第二次预览)](https://openjdk.org/jeps/466)
+- [JEP 467:Markdown 文档注释](https://openjdk.org/jeps/467)
+- [JEP 469:向量 API(第八次孵化)](https://openjdk.org/jeps/469)
+- [JEP 473:流收集器(第二次预览)](https://openjdk.org/jeps/473)
+- [JEP 471:弃用 sun.misc.Unsafe 中的内存访问方法](https://openjdk.org/jeps/471)
+- [JEP 474:ZGC:默认的分代模式](https://openjdk.org/jeps/474)
+- [JEP 476:模块导入声明 (预览)](https://openjdk.org/jeps/476)
+- [JEP 477:未命名类和实例 main 方法 (第三次预览)](https://openjdk.org/jeps/477)
+- [JEP 480:结构化并发 (第三次预览)](https://openjdk.org/jeps/480)
+- [JEP 481:作用域值 (第三次预览)](https://openjdk.org/jeps/481)
+- [JEP 482:灵活的构造函数体(第二次预览)](https://openjdk.org/jeps/482)
+
+JDK 22 的新特性如下:
+
+
+
+其中,下面这 3 条新特性我会单独拎出来详细介绍一下:
+
+- [JEP 423:G1 垃圾收集器区域固定](https://openjdk.org/jeps/423)
+- [JEP 454:外部函数与内存 API](https://openjdk.org/jeps/454)
+- [JEP 456:未命名模式和变量](https://openjdk.org/jeps/456)
+- [JEP 458:启动多文件源代码程序](https://openjdk.org/jeps/458)
+
+## JDK 23
+
+### JEP 455: 模式中的原始类型、instanceof 和 switch(预览)
+
+在 JEP 455 之前, `instanceof` 只支持引用类型,`switch` 表达式和语句的 `case` 标签只能使用整数字面量、枚举常量和字符串字面量。
+
+JEP 455 的预览特性中,`instanceof` 和 `switch` 全面支持所有原始类型,包括 `byte`, `short`, `char`, `int`, `long`, `float`, `double`, `boolean`。
+
+```java
+// 传统写法
+if (i >= -128 && i <= 127) {
+ byte b = (byte)i;
+ ... b ...
+}
+
+// 使用 instanceof 改进
+if (i instanceof byte b) {
+ ... b ...
+}
+
+long v = ...;
+// 传统写法
+if (v == 1L) {
+ // ...
+} else if (v == 2L) {
+ // ...
+} else if (v == 10_000_000_000L) {
+ // ...
+}
+
+// 使用 long 类型的 case 标签
+switch (v) {
+ case 1L:
+ // ...
+ break;
+ case 2L:
+ // ...
+ break;
+ case 10_000_000_000L:
+ // ...
+ break;
+ default:
+ // ...
+}
+```
+
+### JEP 456: 类文件 API(第二次预览)
+
+类文件 API 在 JDK 22 进行了第一次预览,由 [JEP 457](https://openjdk.org/jeps/457) 提出。
+
+类文件 API 的目标是提供一套标准化的 API,用于解析、生成和转换 Java 类文件,取代过去对第三方库(如 ASM)在类文件处理上的依赖。
+
+```java
+// 创建一个 ClassFile 对象,这是操作类文件的入口。
+ClassFile cf = ClassFile.of();
+// 解析字节数组为 ClassModel
+ClassModel classModel = cf.parse(bytes);
+
+// 构建新的类文件,移除以 "debug" 开头的所有方法
+byte[] newBytes = cf.build(classModel.thisClass().asSymbol(),
+ classBuilder -> {
+ // 遍历所有类元素
+ for (ClassElement ce : classModel) {
+ // 判断是否为方法 且 方法名以 "debug" 开头
+ if (!(ce instanceof MethodModel mm
+ && mm.methodName().stringValue().startsWith("debug"))) {
+ // 添加到新的类文件中
+ classBuilder.with(ce);
+ }
+ }
+ });
+```
+
+### JEP 467:Markdown 文档注释
+
+在 JavaDoc 文档注释中可以使用 Markdown 语法,取代原本只能使用 HTML 和 JavaDoc 标签的方式。
+
+Markdown 更简洁易读,减少了手动编写 HTML 的繁琐,同时保留了对 HTML 元素和 JavaDoc 标签的支持。这个增强旨在让 API 文档注释的编写和阅读变得更加轻松,同时不会影响现有注释的解释。Markdown 提供了对常见文档元素(如段落、列表、链接等)的简化表达方式,提升了文档注释的可维护性和开发者体验。
+
+
+
+### JEP 469:向量 API(第八次孵化)
+
+向量计算由对向量的一系列操作组成。向量 API 用来表达向量计算,该计算可以在运行时可靠地编译为支持的 CPU 架构上的最佳向量指令,从而实现优于等效标量计算的性能。
+
+向量 API 的目标是为用户提供简洁易用且与平台无关的表达范围广泛的向量计算。
+
+这是对数组元素的简单标量计算:
+
+```java
+void scalarComputation(float[] a, float[] b, float[] c) {
+ for (int i = 0; i < a.length; i++) {
+ c[i] = (a[i] * a[i] + b[i] * b[i]) * -1.0f;
+ }
+}
+```
+
+这是使用 Vector API 进行的等效向量计算:
+
+```java
+static final VectorSpecies SPECIES = FloatVector.SPECIES_PREFERRED;
+
+void vectorComputation(float[] a, float[] b, float[] c) {
+ int i = 0;
+ int upperBound = SPECIES.loopBound(a.length);
+ for (; i < upperBound; i += SPECIES.length()) {
+ // FloatVector va, vb, vc;
+ var va = FloatVector.fromArray(SPECIES, a, i);
+ var vb = FloatVector.fromArray(SPECIES, b, i);
+ var vc = va.mul(va)
+ .add(vb.mul(vb))
+ .neg();
+ vc.intoArray(c, i);
+ }
+ for (; i < a.length; i++) {
+ c[i] = (a[i] * a[i] + b[i] * b[i]) * -1.0f;
+ }
+}
+```
+
+### JEP 473:流收集器(第二次预览)
+
+流收集器在 JDK 22 进行了第一次预览,由 [JEP 461](https://openjdk.org/jeps/457) 提出。
+
+这个改进使得 Stream API 可以支持自定义中间操作。
+
+```java
+source.gather(a).gather(b).gather(c).collect(...)
+```
+
+### JEP 471:弃用 sun.misc.Unsafe 中的内存访问方法
+
+JEP 471 提议弃用 `sun.misc.Unsafe` 中的内存访问方法,这些方法将来的版本中会被移除。
+
+这些不安全的方法已有安全高效的替代方案:
+
+- `java.lang.invoke.VarHandle` :JDK 9 (JEP 193) 中引入,提供了一种安全有效地操作堆内存的方法,包括对象的字段、类的静态字段以及数组元素。
+- `java.lang.foreign.MemorySegment` :JDK 22 (JEP 454) 中引入,提供了一种安全有效地访问堆外内存的方法,有时会与 `VarHandle` 协同工作。
+
+这两个类是 Foreign Function & Memory API(外部函数和内存 API) 的核心组件,分别用于管理和操作堆外内存。Foreign Function & Memory API 在 JDK 22 中正式转正,成为标准特性。
+
+```java
+import jdk.incubator.foreign.*;
+import java.lang.invoke.VarHandle;
+
+// 管理堆外整数数组的类
+class OffHeapIntBuffer {
+
+ // 用于访问整数元素的VarHandle
+ private static final VarHandle ELEM_VH = ValueLayout.JAVA_INT.arrayElementVarHandle();
+
+ // 内存管理器
+ private final Arena arena;
+
+ // 堆外内存段
+ private final MemorySegment buffer;
+
+ // 构造函数,分配指定数量的整数空间
+ public OffHeapIntBuffer(long size) {
+ this.arena = Arena.ofShared();
+ this.buffer = arena.allocate(ValueLayout.JAVA_INT, size);
+ }
+
+ // 释放内存
+ public void deallocate() {
+ arena.close();
+ }
+
+ // 以volatile方式设置指定索引的值
+ public void setVolatile(long index, int value) {
+ ELEM_VH.setVolatile(buffer, 0L, index, value);
+ }
+
+ // 初始化指定范围的元素为0
+ public void initialize(long start, long n) {
+ buffer.asSlice(ValueLayout.JAVA_INT.byteSize() * start,
+ ValueLayout.JAVA_INT.byteSize() * n)
+ .fill((byte) 0);
+ }
+
+ // 将指定范围的元素复制到新数组
+ public int[] copyToNewArray(long start, int n) {
+ return buffer.asSlice(ValueLayout.JAVA_INT.byteSize() * start,
+ ValueLayout.JAVA_INT.byteSize() * n)
+ .toArray(ValueLayout.JAVA_INT);
+ }
+}
+```
+
+### JEP 474:ZGC:默认的分代模式
+
+Z 垃圾回收器 (ZGC) 的默认模式切换为分代模式,并弃用非分代模式,计划在未来版本中移除。这是因为分代 ZGC 是大多数场景下的更优选择。
+
+### JEP 476:模块导入声明 (预览)
+
+模块导入声明允许在 Java 代码中简洁地导入整个模块的所有导出包,而无需逐个声明包的导入。这一特性简化了模块化库的重用,特别是在使用多个模块时,避免了大量的包导入声明,使得开发者可以更方便地访问第三方库和 Java 基本类。
+
+此特性对初学者和原型开发尤为有用,因为它无需开发者将自己的代码模块化,同时保留了对传统导入方式的兼容性,提升了开发效率和代码可读性。
+
+```java
+// 导入整个 java.base 模块,开发者可以直接访问 List、Map、Stream 等类,而无需每次手动导入相关包
+import module java.base;
+
+public class Example {
+ public static void main(String[] args) {
+ String[] fruits = { "apple", "berry", "citrus" };
+ Map fruitMap = Stream.of(fruits)
+ .collect(Collectors.toMap(
+ s -> s.toUpperCase().substring(0, 1),
+ Function.identity()));
+
+ System.out.println(fruitMap);
+ }
+}
+```
+
+### JEP 477:未命名类和实例 main 方法 (第三次预览)
+
+这个特性主要简化了 `main` 方法的声明。对于 Java 初学者来说,这个 `main` 方法的声明引入了太多的 Java 语法概念,不利于初学者快速上手。
+
+没有使用该特性之前定义一个 `main` 方法:
+
+```java
+public class HelloWorld {
+ public static void main(String[] args) {
+ System.out.println("Hello, World!");
+ }
+}
+```
+
+使用该新特性之后定义一个 `main` 方法:
+
+```java
+class HelloWorld {
+ void main() {
+ System.out.println("Hello, World!");
+ }
+}
+```
+
+进一步简化(未命名的类允许我们省略类名)
+
+```java
+void main() {
+ System.out.println("Hello, World!");
+}
+```
+
+### JEP 480:结构化并发 (第三次预览)
+
+Java 19 引入了结构化并发,一种多线程编程方法,目的是为了通过结构化并发 API 来简化多线程编程,并不是为了取代`java.util.concurrent`,目前处于孵化器阶段。
+
+结构化并发将不同线程中运行的多个任务视为单个工作单元,从而简化错误处理、提高可靠性并增强可观察性。也就是说,结构化并发保留了单线程代码的可读性、可维护性和可观察性。
+
+结构化并发的基本 API 是[`StructuredTaskScope`](https://download.java.net/java/early_access/loom/docs/api/jdk.incubator.concurrent/jdk/incubator/concurrent/StructuredTaskScope.html)。`StructuredTaskScope` 支持将任务拆分为多个并发子任务,在它们自己的线程中执行,并且子任务必须在主任务继续之前完成。
+
+`StructuredTaskScope` 的基本用法如下:
+
+```java
+ try (var scope = new StructuredTaskScope()) {
+ // 使用fork方法派生线程来执行子任务
+ Future future1 = scope.fork(task1);
+ Future future2 = scope.fork(task2);
+ // 等待线程完成
+ scope.join();
+ // 结果的处理可能包括处理或重新抛出异常
+ ... process results/exceptions ...
+ } // close
+```
+
+结构化并发非常适合虚拟线程,虚拟线程是 JDK 实现的轻量级线程。许多虚拟线程共享同一个操作系统线程,从而允许非常多的虚拟线程。
+
+### JEP 481:作用域值 (第三次预览)
+
+作用域值(Scoped Values)可以在线程内和线程间共享不可变的数据,优于线程局部变量,尤其是在使用大量虚拟线程时。
+
+```java
+final static ScopedValue<...> V = new ScopedValue<>();
+
+// In some method
+ScopedValue.where(V, )
+ .run(() -> { ... V.get() ... call methods ... });
+
+// In a method called directly or indirectly from the lambda expression
+... V.get() ...
+```
+
+作用域值允许在大型程序中的组件之间安全有效地共享数据,而无需求助于方法参数。
+
+### JEP 482:灵活的构造函数体(第二次预览)
+
+这个特性最初在 JDK 22 由 [JEP 447: Statements before super(...) (Preview)](https://openjdk.org/jeps/447)提出。
+
+Java 要求在构造函数中,`super(...)` 或 `this(...)` 调用必须作为第一条语句出现。这意味着我们无法在调用父类构造函数之前在子类构造函数中直接初始化字段。
+
+灵活的构造函数体解决了这一问题,它允许在构造函数体内,在调用 `super(..)` 或 `this(..)` 之前编写语句,这些语句可以初始化字段,但不能引用正在构造的实例。这样可以防止在父类构造函数中调用子类方法时,子类的字段未被正确初始化,增强了类构造的可靠性。
+
+这一特性解决了之前 Java 语法限制了构造函数代码组织的问题,让开发者能够更自由、更自然地表达构造函数的行为,例如在构造函数中直接进行参数验证、准备和共享,而无需依赖辅助方法或构造函数,提高了代码的可读性和可维护性。
+
+```java
+class Person {
+ private final String name;
+ private int age;
+
+ public Person(String name, int age) {
+ if (age < 0) {
+ throw new IllegalArgumentException("Age cannot be negative.");
+ }
+ this.name = name; // 在调用父类构造函数之前初始化字段
+ this.age = age;
+ // ... 其他初始化代码
+ }
+}
+
+class Employee extends Person {
+ private final int employeeId;
+
+ public Employee(String name, int age, int employeeId) {
+ this.employeeId = employeeId; // 在调用父类构造函数之前初始化字段
+ super(name, age); // 调用父类构造函数
+ // ... 其他初始化代码
+ }
+}
+```
+
+## JDK 22
+
+### JEP 423:G1 垃圾收集器区域固定
+
+JEP 423 提出在 G1 垃圾收集器中实现区域固定(Region Pinning)功能,旨在减少由于 Java Native Interface (JNI) 关键区域导致的延迟问题。
+
+JNI 关键区域内的对象不能在垃圾收集时被移动,因此 G1 以往通过禁用垃圾收集解决该问题,导致线程阻塞及严重的延迟。通过在 G1 的老年代和年轻代中引入区域固定机制,允许在关键区域内固定对象所在的内存区域,同时继续回收未固定的区域,避免了禁用垃圾回收的需求。这种改进有助于显著降低延迟,提升系统在与 JNI 交互时的吞吐量和稳定性。
+
+### JEP 454:外部函数和内存 API
+
+Java 程序可以通过该 API 与 Java 运行时之外的代码和数据进行互操作。通过高效地调用外部函数(即 JVM 之外的代码)和安全地访问外部内存(即不受 JVM 管理的内存),该 API 使 Java 程序能够调用本机库并处理本机数据,而不会像 JNI 那样危险和脆弱。
+
+外部函数和内存 API 在 Java 17 中进行了第一轮孵化,由 [JEP 412](https://openjdk.java.net/jeps/412) 提出。Java 18 中进行了第二次孵化,由[JEP 419](https://openjdk.org/jeps/419) 提出。Java 19 中是第一次预览,由 [JEP 424](https://openjdk.org/jeps/424) 提出。JDK 20 中是第二次预览,由 [JEP 434](https://openjdk.org/jeps/434) 提出。JDK 21 中是第三次预览,由 [JEP 442](https://openjdk.org/jeps/442) 提出。
+
+最终,该特性在 JDK 22 中顺利转正。
+
+在 [Java 19 新特性概览](./java19.md) 中,我有详细介绍到外部函数和内存 API,这里就不再做额外的介绍了。
+
+### JEP 456:未命名模式和变量
+
+未命名模式和变量在 JDK 21 中由 [JEP 443](https://openjdk.org/jeps/443)提出预览,JDK 22 中就已经转正。
+
+关于这个新特性的详细介绍,可以看看[Java 21 新特性概览(重要)](./java21.md)这篇文章中的介绍。
+
+### JEP 458:启动多文件源代码程序
+
+Java 11 引入了 [JEP 330:启动单文件源代码程序](https://openjdk.org/jeps/330),增强了 `java` 启动器的功能,使其能够直接运行单个 Java 源文件。通过命令 `java HelloWorld.java`,Java 可以在内存中隐式编译源代码并立即执行,而不需要在磁盘上生成 `.class` 文件。这简化了开发者在编写小型工具程序或学习 Java 时的工作流程,避免了手动编译的额外步骤。
+
+假设文件`Prog.java`声明了两个类:
+
+```java
+class Prog {
+ public static void main(String[] args) { Helper.run(); }
+}
+
+class Helper {
+ static void run() { System.out.println("Hello!"); }
+}
+```
+
+`java Prog.java`命令会在内存中编译两个类并执行`main`该文件中声明的第一个类的方法。
+
+这种方式有一个限制,程序的所有源代码必须放在一个`.java`文件中。
+
+[JEP 458:启动多文件源代码程序](https://openjdk.org/jeps/458) 是对 JEP 330 功能的扩展,允许直接运行由多个 Java 源文件组成的程序,而无需显式的编译步骤。
+
+假设一个目录中有两个 Java 源文件 `Prog.java` 和 `Helper.java`,每个文件各自声明了一个类:
+
+```java
+// Prog.java
+class Prog {
+ public static void main(String[] args) { Helper.run(); }
+}
+
+// Helper.java
+class Helper {
+ static void run() { System.out.println("Hello!"); }
+}
+```
+
+当你运行命令 `java Prog.java` 时,Java 启动器会在内存中编译并执行 `Prog` 类的 `main` 方法。由于 `Prog` 类中的代码引用了 `Helper` 类,启动器会自动在文件系统中找到 `Helper.java` 文件,编译其中的 `Helper` 类,并在内存中执行它。这个过程是自动的,开发者无需显式调用 `javac` 来编译所有源文件。
+
+这一特性使得从小型项目到大型项目的过渡更加平滑,开发者可以自由选择何时引入构建工具,避免在快速迭代时被迫设置复杂的项目结构。该特性消除了单文件的限制,进一步简化了从单一文件到多文件程序的开发过程,特别适合原型开发、快速实验以及早期项目的探索阶段。
diff --git a/docs/java/new-features/java24.md b/docs/java/new-features/java24.md
new file mode 100644
index 00000000000..f79ce111d9c
--- /dev/null
+++ b/docs/java/new-features/java24.md
@@ -0,0 +1,260 @@
+---
+title: Java 24 新特性概览
+description: 总结 JDK 24 的新特性与改动,便于跟踪 Java 演进。
+category: Java
+tag:
+ - Java新特性
+head:
+ - - meta
+ - name: keywords
+ content: Java 24,JDK24,JEP 更新,语言特性,GC 改进,平台增强
+---
+
+[JDK 24](https://openjdk.org/projects/jdk/24/) 是自 JDK 21 以来的第三个非长期支持版本,和 [JDK 22](https://javaguide.cn/java/new-features/java22-23.html)、[JDK 23](https://javaguide.cn/java/new-features/java22-23.html)一样。下一个长期支持版是 **JDK 25**,预计今年 9 月份发布。
+
+JDK 24 带来的新特性还是蛮多的,一共 24 个。JDK 22 和 JDK 23 都只有 12 个,JDK 24 的新特性相当于这两次的总和了。因此,这个版本还是非常有必要了解一下的。
+
+JDK 24 新特性概览:
+
+
+
+下图是从 JDK 8 到 JDK 24 每个版本的更新带来的新特性数量和更新时间:
+
+
+
+## JEP 478: 密钥派生函数 API(预览)
+
+密钥派生函数 API 是一种用于从初始密钥和其他数据派生额外密钥的加密算法。它的核心作用是为不同的加密目的(如加密、认证等)生成多个不同的密钥,避免密钥重复使用带来的安全隐患。 这在现代加密中是一个重要的里程碑,为后续新兴的量子计算环境打下了基础
+
+通过该 API,开发者可以使用最新的密钥派生算法(如 HKDF 和未来的 Argon2):
+
+```java
+// 创建一个 KDF 对象,使用 HKDF-SHA256 算法
+KDF hkdf = KDF.getInstance("HKDF-SHA256");
+
+// 创建 Extract 和 Expand 参数规范
+AlgorithmParameterSpec params =
+ HKDFParameterSpec.ofExtract()
+ .addIKM(initialKeyMaterial) // 设置初始密钥材料
+ .addSalt(salt) // 设置盐值
+ .thenExpand(info, 32); // 设置扩展信息和目标长度
+
+// 派生一个 32 字节的 AES 密钥
+SecretKey key = hkdf.deriveKey("AES", params);
+
+// 可以使用相同的 KDF 对象进行其他密钥派生操作
+```
+
+## JEP 483: 提前类加载和链接
+
+在传统 JVM 中,应用在每次启动时需要动态加载和链接类。这种机制对启动时间敏感的应用(如微服务或无服务器函数)带来了显著的性能瓶颈。该特性通过缓存已加载和链接的类,显著减少了重复工作的开销,显著减少 Java 应用程序的启动时间。测试表明,对大型应用(如基于 Spring 的服务器应用),启动时间可减少 40% 以上。
+
+这个优化是零侵入性的,对应用程序、库或框架的代码无需任何更改,启动也方式保持一致,仅需添加相关 JVM 参数(如 `-XX:+ClassDataSharing`)。
+
+## JEP 484: 类文件 API
+
+类文件 API 在 JDK 22 进行了第一次预览([JEP 457](https://openjdk.org/jeps/457)),在 JDK 23 进行了第二次预览并进一步完善([JEP 466](https://openjdk.org/jeps/466))。最终,该特性在 JDK 24 中顺利转正。
+
+类文件 API 的目标是提供一套标准化的 API,用于解析、生成和转换 Java 类文件,取代过去对第三方库(如 ASM)在类文件处理上的依赖。
+
+```java
+// 创建一个 ClassFile 对象,这是操作类文件的入口。
+ClassFile cf = ClassFile.of();
+// 解析字节数组为 ClassModel
+ClassModel classModel = cf.parse(bytes);
+
+// 构建新的类文件,移除以 "debug" 开头的所有方法
+byte[] newBytes = cf.build(classModel.thisClass().asSymbol(),
+ classBuilder -> {
+ // 遍历所有类元素
+ for (ClassElement ce : classModel) {
+ // 判断是否为方法 且 方法名以 "debug" 开头
+ if (!(ce instanceof MethodModel mm
+ && mm.methodName().stringValue().startsWith("debug"))) {
+ // 添加到新的类文件中
+ classBuilder.with(ce);
+ }
+ }
+ });
+```
+
+## JEP 485: 流收集器
+
+流收集器 `Stream::gather(Gatherer)` 是一个强大的新特性,它允许开发者定义自定义的中间操作,从而实现更复杂、更灵活的数据转换。`Gatherer` 接口是该特性的核心,它定义了如何从流中收集元素,维护中间状态,并在处理过程中生成结果。
+
+与现有的 `filter`、`map` 或 `distinct` 等内置操作不同,`Stream::gather` 使得开发者能够实现那些难以用标准 Stream 操作完成的任务。例如,可以使用 `Stream::gather` 实现滑动窗口、自定义规则的去重、或者更复杂的状态转换和聚合。 这种灵活性极大地扩展了 Stream API 的应用范围,使开发者能够应对更复杂的数据处理场景。
+
+基于 `Stream::gather(Gatherer)` 实现字符串长度的去重逻辑:
+
+```java
+var result = Stream.of("foo", "bar", "baz", "quux")
+ .gather(Gatherer.ofSequential(
+ HashSet::new, // 初始化状态为 HashSet,用于保存已经遇到过的字符串长度
+ (set, str, downstream) -> {
+ if (set.add(str.length())) {
+ return downstream.push(str);
+ }
+ return true; // 继续处理流
+ }
+ ))
+ .toList();// 转换为列表
+
+// 输出结果 ==> [foo, quux]
+```
+
+## JEP 486: 永久禁用安全管理器
+
+JDK 24 不再允许启用 `Security Manager`,即使通过 `java -Djava.security.manager`命令也无法启用,这是逐步移除该功能的关键一步。虽然 `Security Manager` 曾经是 Java 中限制代码权限(如访问文件系统或网络、读取或写入敏感文件、执行系统命令)的重要工具,但由于复杂性高、使用率低且维护成本大,Java 社区决定最终移除它。
+
+## JEP 487: 作用域值 (第四次预览)
+
+作用域值(Scoped Values)可以在线程内和线程间共享不可变的数据,优于线程局部变量,尤其是在使用大量虚拟线程时。
+
+```java
+final static ScopedValue<...> V = new ScopedValue<>();
+
+// In some method
+ScopedValue.where(V, )
+ .run(() -> { ... V.get() ... call methods ... });
+
+// In a method called directly or indirectly from the lambda expression
+... V.get() ...
+```
+
+作用域值允许在大型程序中的组件之间安全有效地共享数据,而无需求助于方法参数。
+
+## JEP 491: 虚拟线程的同步而不固定平台线程
+
+优化了虚拟线程与 `synchronized` 的工作机制。 虚拟线程在 `synchronized` 方法和代码块中阻塞时,通常能够释放其占用的操作系统线程(平台线程),避免了对平台线程的长时间占用,从而提升应用程序的并发能力。 这种机制避免了“固定 (Pinning)”——即虚拟线程长时间占用平台线程,阻止其服务于其他虚拟线程的情况。
+
+现有的使用 `synchronized` 的 Java 代码无需修改即可受益于虚拟线程的扩展能力。 例如,一个 I/O 密集型的应用程序,如果使用传统的平台线程,可能会因为线程阻塞而导致并发能力下降。 而使用虚拟线程,即使在 `synchronized` 块中发生阻塞,也不会固定平台线程,从而允许平台线程继续服务于其他虚拟线程,提高整体的并发性能。
+
+## JEP 493: 在没有 JMOD 文件的情况下链接运行时镜像
+
+默认情况下,JDK 同时包含运行时镜像(运行时所需的模块)和 JMOD 文件。这个特性使得 jlink 工具无需使用 JDK 的 JMOD 文件就可以创建自定义运行时镜像,减少了 JDK 的安装体积(约 25%)。
+
+说明:
+
+- Jlink 是随 Java 9 一起发布的新命令行工具。它允许开发人员为基于模块的 Java 应用程序创建自己的轻量级、定制的 JRE。
+- JMOD 文件是 Java 模块的描述文件,包含了模块的元数据和资源。
+
+## JEP 495: 简化的源文件和实例主方法(第四次预览)
+
+这个特性主要简化了 `main` 方法的声明。对于 Java 初学者来说,这个 `main` 方法的声明引入了太多的 Java 语法概念,不利于初学者快速上手。
+
+没有使用该特性之前定义一个 `main` 方法:
+
+```java
+public class HelloWorld {
+ public static void main(String[] args) {
+ System.out.println("Hello, World!");
+ }
+}
+```
+
+使用该新特性之后定义一个 `main` 方法:
+
+```java
+class HelloWorld {
+ void main() {
+ System.out.println("Hello, World!");
+ }
+}
+```
+
+进一步简化(未命名的类允许我们省略类名)
+
+```java
+void main() {
+ System.out.println("Hello, World!");
+}
+```
+
+## JEP 497: 量子抗性数字签名算法 (ML-DSA)
+
+JDK 24 引入了支持实施抗量子的基于模块晶格的数字签名算法 (Module-Lattice-Based Digital Signature Algorithm, **ML-DSA**),为抵御未来量子计算机可能带来的威胁做准备。
+
+ML-DSA 是美国国家标准与技术研究院(NIST)在 FIPS 204 中标准化的量子抗性算法,用于数字签名和身份验证。
+
+## JEP 498: 使用 `sun.misc.Unsafe` 内存访问方法时发出警告
+
+JDK 23([JEP 471](https://openjdk.org/jeps/471)) 提议弃用 `sun.misc.Unsafe` 中的内存访问方法,这些方法将来的版本中会被移除。在 JDK 24 中,当首次调用 `sun.misc.Unsafe` 的任何内存访问方法时,运行时会发出警告。
+
+这些不安全的方法已有安全高效的替代方案:
+
+- `java.lang.invoke.VarHandle` :JDK 9 (JEP 193) 中引入,提供了一种安全有效地操作堆内存的方法,包括对象的字段、类的静态字段以及数组元素。
+- `java.lang.foreign.MemorySegment` :JDK 22 (JEP 454) 中引入,提供了一种安全有效地访问堆外内存的方法,有时会与 `VarHandle` 协同工作。
+
+这两个类是 Foreign Function & Memory API(外部函数和内存 API) 的核心组件,分别用于管理和操作堆外内存。Foreign Function & Memory API 在 JDK 22 中正式转正,成为标准特性。
+
+```java
+import jdk.incubator.foreign.*;
+import java.lang.invoke.VarHandle;
+
+// 管理堆外整数数组的类
+class OffHeapIntBuffer {
+
+ // 用于访问整数元素的VarHandle
+ private static final VarHandle ELEM_VH = ValueLayout.JAVA_INT.arrayElementVarHandle();
+
+ // 内存管理器
+ private final Arena arena;
+
+ // 堆外内存段
+ private final MemorySegment buffer;
+
+ // 构造函数,分配指定数量的整数空间
+ public OffHeapIntBuffer(long size) {
+ this.arena = Arena.ofShared();
+ this.buffer = arena.allocate(ValueLayout.JAVA_INT, size);
+ }
+
+ // 释放内存
+ public void deallocate() {
+ arena.close();
+ }
+
+ // 以volatile方式设置指定索引的值
+ public void setVolatile(long index, int value) {
+ ELEM_VH.setVolatile(buffer, 0L, index, value);
+ }
+
+ // 初始化指定范围的元素为0
+ public void initialize(long start, long n) {
+ buffer.asSlice(ValueLayout.JAVA_INT.byteSize() * start,
+ ValueLayout.JAVA_INT.byteSize() * n)
+ .fill((byte) 0);
+ }
+
+ // 将指定范围的元素复制到新数组
+ public int[] copyToNewArray(long start, int n) {
+ return buffer.asSlice(ValueLayout.JAVA_INT.byteSize() * start,
+ ValueLayout.JAVA_INT.byteSize() * n)
+ .toArray(ValueLayout.JAVA_INT);
+ }
+}
+```
+
+## JEP 499: 结构化并发(第四次预览)
+
+JDK 19 引入了结构化并发,一种多线程编程方法,目的是为了通过结构化并发 API 来简化多线程编程,并不是为了取代`java.util.concurrent`,目前处于孵化器阶段。
+
+结构化并发将不同线程中运行的多个任务视为单个工作单元,从而简化错误处理、提高可靠性并增强可观察性。也就是说,结构化并发保留了单线程代码的可读性、可维护性和可观察性。
+
+结构化并发的基本 API 是`StructuredTaskScope`,它支持将任务拆分为多个并发子任务,在它们自己的线程中执行,并且子任务必须在主任务继续之前完成。
+
+`StructuredTaskScope` 的基本用法如下:
+
+```java
+ try (var scope = new StructuredTaskScope()) {
+ // 使用fork方法派生线程来执行子任务
+ Future future1 = scope.fork(task1);
+ Future future2 = scope.fork(task2);
+ // 等待线程完成
+ scope.join();
+ // 结果的处理可能包括处理或重新抛出异常
+ ... process results/exceptions ...
+ } // close
+```
+
+结构化并发非常适合虚拟线程,虚拟线程是 JDK 实现的轻量级线程。许多虚拟线程共享同一个操作系统线程,从而允许非常多的虚拟线程。
diff --git a/docs/java/new-features/java25.md b/docs/java/new-features/java25.md
new file mode 100644
index 00000000000..65e3e3f2441
--- /dev/null
+++ b/docs/java/new-features/java25.md
@@ -0,0 +1,43 @@
+---
+title: Java 25 新特性概览
+description: 概览 JDK 25 的关键新特性与预览改动,关注并发、GC 与语言/平台增强。
+category: Java
+tag:
+ - Java新特性
+head:
+ - - meta
+ - name: keywords
+ content: Java 25,JDK25,LTS,作用域值,紧凑对象头,分代 Shenandoah,模块导入,结构化并发
+---
+
+JDK 25 于 2025 年 9 月 16 日 发布,这是一个非常重要的版本,里程碑式。
+
+JDK 25 是 LTS(长期支持版),至此为止,目前有 JDK8、JDK11、JDK17、JDK21 和 JDK 25 这四个长期支持版了。
+
+JDK 21 共有 18 个新特性,这篇文章会挑选其中较为重要的一些新特性进行详细介绍:
+
+- [JEP 506: Scoped Values (作用域值)](https://openjdk.org/projects/jdk/25/)
+- [JEP 512: Compact Source Files and Instance Main Methods (紧凑源文件与实例主方法)](https://openjdk.org/jeps/512)
+- [JEP 519: Compact Object Headers (紧凑对象头)](https://openjdk.org/jeps/519)
+- [JEP 521: Generational Shenandoah (分代 Shenandoah GC)](https://openjdk.org/jeps/521)
+- [JEP 507: Primitive Types in Patterns, instanceof, and switch (模式匹配支持基本类型, 第三次预览)](https://openjdk.org/jeps/507)
+- [JEP 505: Structured Concurrency (结构化并发, 第五次预览)](https://openjdk.org/jeps/505)
+- [JEP 511: Module Import Declarations (模块导入声明)](https://openjdk.org/jeps/511)
+- [JEP 513: Flexible Constructor Bodies (灵活的构造函数体)](https://openjdk.org/jeps/513)
+- [JEP 508: Vector API (向量 API, 第十次孵化)](https://openjdk.org/jeps/508)
+
+下图是从 JDK 8 到 JDK 24 每个版本的更新带来的新特性数量和更新时间:
+
+
+
+## JEP 506: 作用域值
+
+作用域值(Scoped Values)可以在线程内和线程间共享不可变的数据,优于线程局部变量 `ThreadLocal` ,尤其是在使用大量虚拟线程时。
+
+```java
+final static ScopedValue<...> V = new ScopedValue<>();
+
+// In some method
+ScopedValue.where(V, )
+ .run(() -> { ... V.get() ... call methods ... });
+```
diff --git a/docs/java/new-features/java8-common-new-features.md b/docs/java/new-features/java8-common-new-features.md
index 04267d56af4..b1cec792071 100644
--- a/docs/java/new-features/java8-common-new-features.md
+++ b/docs/java/new-features/java8-common-new-features.md
@@ -1,12 +1,19 @@
---
title: Java8 新特性实战
+description: 实战讲解 Java 8 的核心新特性,包括 Lambda、Stream、Optional、日期时间 API 与接口默认方法等。
category: Java
tag:
- Java新特性
+head:
+ - - meta
+ - name: keywords
+ content: Java 8,Lambda,Stream API,Optional,Date/Time API,默认方法,函数式接口
---
> 本文来自[cowbi](https://github.com/cowbi)的投稿~
+
+
Oracle 于 2014 发布了 Java8(jdk1.8),诸多原因使它成为目前市场上使用最多的 jdk 版本。虽然发布距今已将近 7 年,但很多程序员对其新特性还是不够了解,尤其是用惯了 Java8 之前版本的老程序员,比如我。
为了不脱离队伍太远,还是有必要对这些新特性做一些总结梳理。它较 jdk.7 有很多变化或者说是优化,比如 interface 里可以有静态方法,并且可以有方法体,这一点就颠覆了之前的认知;`java.util.HashMap` 数据结构里增加了红黑树;还有众所周知的 Lambda 表达式等等。本文不能把所有的新特性都给大家一一分享,只列出比较常用的新特性给大家做详细讲解。更多相关内容请看[官网关于 Java8 的新特性的介绍](https://www.oracle.com/java/technologies/javase/8-whats-new.html)。
@@ -93,9 +100,7 @@ public class InterfaceNewImpl implements InterfaceNew , InterfaceNew1{
在 java 8 中专门有一个包放函数式接口`java.util.function`,该包下的所有接口都有 `@FunctionalInterface` 注解,提供函数式编程。
-在其他包中也有函数式接口,其中一些没有`@FunctionalInterface` 注解,但是只要符合函数式接口的定义就是函数式接口,与是否有
-
-`@FunctionalInterface`注解无关,注解只是在编译时起到强制规范定义的作用。其在 Lambda 表达式中有广泛的应用。
+在其他包中也有函数式接口,其中一些没有`@FunctionalInterface` 注解,但是只要符合函数式接口的定义就是函数式接口,与是否有`@FunctionalInterface`注解无关,注解只是在编译时起到强制规范定义的作用。其在 Lambda 表达式中有广泛的应用。
## Lambda 表达式
@@ -359,7 +364,7 @@ Stream limit(long maxSize);
Stream sorted(Comparator super T> comparator);
/**
-* 在丢弃流的第一个 n元素后,返回由该流的 n元素组成的流。
+* 丢弃此流中的前 n 个元素,返回由剩余元素组成的新流。
*/
Stream skip(long n);
@@ -857,7 +862,7 @@ LocalDate date = LocalDate.of(2021, 1, 26);
LocalDate.parse("2021-01-26");
LocalDateTime dateTime = LocalDateTime.of(2021, 1, 26, 12, 12, 22);
-LocalDateTime.parse("2021-01-26 12:12:22");
+LocalDateTime.parse("2021-01-26T12:12:22");
LocalTime time = LocalTime.of(12, 12, 22);
LocalTime.parse("12:12:22");
diff --git a/docs/java/new-features/java8-tutorial-translate.md b/docs/java/new-features/java8-tutorial-translate.md
index 9e0fd04ec70..f07d9d58a42 100644
--- a/docs/java/new-features/java8-tutorial-translate.md
+++ b/docs/java/new-features/java8-tutorial-translate.md
@@ -1,3 +1,15 @@
+---
+title: 《Java8 指南》中文翻译
+description: 翻译与整理 Java 8 教程,涵盖 Lambda、方法引用、接口默认方法、Stream 等新特性与示例代码。
+category: Java
+tag:
+ - Java新特性
+head:
+ - - meta
+ - name: keywords
+ content: Java 8,指南,Lambda,方法引用,默认方法,Stream API,函数式接口,Date/Time API
+---
+
# 《Java8 指南》中文翻译
随着 Java 8 的普及度越来越高,很多人都提到面试中关于 Java 8 也是非常常问的知识点。应各位要求和需要,我打算对这部分知识做一个总结。本来准备自己总结的,后面看到 GitHub 上有一个相关的仓库,地址:
diff --git a/docs/java/new-features/java9.md b/docs/java/new-features/java9.md
index 8fbce002f9d..b3f50d5850d 100644
--- a/docs/java/new-features/java9.md
+++ b/docs/java/new-features/java9.md
@@ -1,8 +1,13 @@
---
title: Java 9 新特性概览
+description: 解析 Java 9 的模块化系统与 jlink 等更新,理解对运行时镜像与库使用的影响。
category: Java
tag:
- Java新特性
+head:
+ - - meta
+ - name: keywords
+ content: Java 9,JDK9,模块化,JPMS,jlink,集合工厂方法,新 API
---
**Java 9** 发布于 2017 年 9 月 21 日 。作为 Java 8 之后 3 年半才发布的新版本,Java 9 带来了很多重大的变化其中最重要的改动是 Java 平台模块系统的引入,其他还有诸如集合、`Stream` 流……。
diff --git a/docs/javaguide/contribution-guideline.md b/docs/javaguide/contribution-guideline.md
index 55dd44d8f26..a51e19b4522 100644
--- a/docs/javaguide/contribution-guideline.md
+++ b/docs/javaguide/contribution-guideline.md
@@ -1,31 +1,95 @@
---
title: 贡献指南
+description: JavaGuide开源项目贡献指南,讲解如何参与项目维护、提交PR及成为Contributor的完整流程。
category: 走近项目
icon: guide
---
-欢迎参与 JavaGuide 的维护工作,这是一件非常有意义的事情。详细信息请看:[JavaGuide 贡献指南](https://zhuanlan.zhihu.com/p/464832264) 。
+你好,我是 Guide!欢迎来到 JavaGuide 的“开源实验室”。
-你可以从下面几个方向来做贡献:
+参与开源项目的维护,不仅是一次技术实战,更是一场“技术反哺”的修行。
-- 修改错别字,毕竟内容基本都是手敲,难免会有笔误。
-- 对原有内容进行修改完善,例如对某个面试问题的答案进行完善、对某篇文章的内容进行完善。
-- 新增内容,例如新增面试常问的问题、添加重要知识点的详解。
+在这里,你的每一行文字和代码,都会被全球几十万的开发者看到。
-目前的贡献奖励也比较丰富和完善,对于多次贡献的用户,有耳机、键盘等实物奖励以及现金奖励!
+## 为什么要参与 JavaGuide 的维护?
-一定一定一定要注意 **排版规范**:
+很多小伙伴觉得开源社区门槛高,其实不然。参与 JavaGuide 维护的收益非常务实:
-- [中文文案排版指北 - GitHub](https://github.com/sparanoid/chinese-copywriting-guidelines)
-- [写给大家看的中文排版指南 - 知乎](https://zhuanlan.zhihu.com/p/20506092)
-- [中文文案排版细则 - Dawner](https://dawner.top/posts/chinese-copywriting-rules/)
+1. **深度对齐知识点**:在纠错或完善内容的过程中,你会强迫自己进行“穿透式学习”,这种记忆远比死记硬背八股文要深刻。
+2. **影响力背书**:JavaGuide 已经接近 160k Star 了。如果你的 `PR` 被采纳,你的名字将永久留在 `Contributor` 列表中。这在求职面试时,是一份非常有说服力的**“开源实战证明”**。
+3. **实物奖励**:我会不定期给高频贡献的小伙伴寄送耳机、机械键盘等硬核周边,甚至还有直接的现金激励。
+
+## 可以从哪些方向进行贡献?
+
+你可以根据自己的精力,选择以下三个维度的贡献:
+
+- **纠错(初级)**:发现文档中的错别字、标点误用或代码格式混乱。这类贡献最简单,但最有温度。
+- **完善(进阶)**:对现有的面试题答案进行重构。比如某篇文章的逻辑有断层,或者缺少了最新的技术特性分析。
+- **新增(专家)**:根据大厂最新的面试动向,新增高频面试题详解或硬核知识点的深度剖析。
+
+## 如何丝滑地提交贡献?
+
+### 极简模式:点击“编辑此页”(3 分钟上手)
+
+本站每个页面的**左下角**都有一个 **「编辑此页」** 按钮。
+
+1. **点击跳转**:直接进入 GitHub 在线编辑界面。
+2. **在线修改**:在浏览器里直接改内容,省去 `git clone` 的麻烦。
+3. **提交申请**:填写提交信息(Commit Message),点击提交即可自动触发 `Pull Request`。
+
+这种方式最适合修正笔误或小范围的内容优化。
+
+
+
+### 进阶模式:Fork + PR(标准开源流程)
+
+如果你想进行大篇幅的重构或新增内容,建议走标准的 GitHub 工作流:
+
+1. **Fork 仓库**:点击[原仓库](https://github.com/Snailclimb/JavaGuide)右上角的 `Fork`,将 JavaGuide 复制一份副本到你的账户名下。
+2. **本地开发**:你可以将项目克隆到本地,在本地自由修改,编写内容。内容修改或者编写完成之后,直接提交到副本仓库即可。
+3. **发起 PR**:提交完成后,点击 `New Pull Request`,将你的修改请求合并到 JavaGuide 的主分支。
+
+
+
+Git 相关的技能非常重要,建议在正式工作之前一定要熟练掌握。
+
+我写过两篇相关的文章,推荐看看:
+
+- [Git 核心概念总结](https://javaguide.cn/tools/git/git-intro.html)
+- [Github 实用小技巧总结](https://javaguide.cn/tools/git/github-tips.html)
+
+### 提交 Issue 开启讨论
+
+如果你发现某些地方需要改进,但暂时没空写代码,或者想提议新增某个专题,请直接通过 **Issue** 发起讨论。
+
+推荐的模板如下:
+
+> **标题**:建议新增 Redis 与数据库双写一致性方案的对比与选型指南
+>
+> **内容描述**:缓存一致性是面试和实战中的重难点,目前文档中缺乏系统性的方案对比。建议补充:
+>
+> 1. **方案对比**:详细对比“先更新数据库再删缓存”、“延迟双删”、“订阅 binlog 异步删除”等方案的优缺点。
+> 2. **极端场景分析**:分析在主从延迟或网络抖动下,如何最大程度保障最终一致性。
+>
+> **认领意向**:我对该领域有深入研究,并整理了一份对比表格和流程图,希望能将其贡献到 JavaGuide。
+
+## 贡献要求
+
+### 排版是第一生产力
+
+中英文之间要加空格,标点符号要规范。请参考(任选一篇阅读即可):
+
+- [中文文案排版指北](https://github.com/sparanoid/chinese-copywriting-guidelines)
- [中文技术文档写作风格指南](https://github.com/yikeke/zh-style-guide/)
+- [中文文案排版细则 - Dawner](https://dawner.top/posts/chinese-copywriting-rules/)
+- [写给大家看的中文排版指南 - 知乎](https://zhuanlan.zhihu.com/p/20506092)
+
+### 内容原创
+
+你可以参考学习别人的文章,但**一定、一定、一定不要复制粘贴**!
-如果要提 issue/question 的话,强烈推荐阅读下面这些资料:
+你要做的不是“信息的搬运工”,而是“知识的过滤器”。用你自己的话讲出来,努力写得比别人更通俗易懂,突出核心重点。**这种“穿透式”的表达,才是对读者最大的负责。**
-- [《提问的智慧》](https://github.com/ryanhanwu/How-To-Ask-Questions-The-Smart-Way)
-- [《如何向开源社区提问题》](https://github.com/seajs/seajs/issues/545)
-- [《如何有效地报告 Bug》](http://www.chiark.greenend.org.uk/~sgtatham/bugs-cn.html)
-- [《如何向开源项目提交无法解答的问题》](https://zhuanlan.zhihu.com/p/25795393)。
+## 写在最后
-另外,你可以参考学习别人的文章,但一定一定一定不能复制粘贴别人的内容,努力比别人写的更容易理解,用自己的话讲出来,适当简化表达,突出重点!
+开源不是一个人的单打独斗,而是一群人的砥砺前行。 **准备 Java 面试,首选 JavaGuide!** 期待在 Contributor 列表中看到你的名字。
diff --git a/docs/javaguide/faq.md b/docs/javaguide/faq.md
index 37f9bc3a94d..9ffbbdfd31e 100644
--- a/docs/javaguide/faq.md
+++ b/docs/javaguide/faq.md
@@ -1,5 +1,6 @@
---
title: 常见问题
+description: JavaGuide常见问题解答,涵盖PDF版本获取、RSS订阅、项目使用等用户高频咨询问题汇总。
category: 走近项目
icon: help
---
diff --git a/docs/javaguide/history.md b/docs/javaguide/history.md
index 07dfcb883ac..c5f934e87f3 100644
--- a/docs/javaguide/history.md
+++ b/docs/javaguide/history.md
@@ -1,5 +1,6 @@
---
title: 网站历史
+description: JavaGuide网站发展历程记录,涵盖项目重要里程碑、版本更新及功能迭代的完整时间线。
category: 走近项目
---
diff --git a/docs/javaguide/intro.md b/docs/javaguide/intro.md
index 9eadc8a8949..60a097a3734 100644
--- a/docs/javaguide/intro.md
+++ b/docs/javaguide/intro.md
@@ -1,5 +1,6 @@
---
title: 项目介绍
+description: JavaGuide项目介绍,一个涵盖Java核心知识体系的学习与面试指南,助力Java开发者成长。
category: 走近项目
icon: about
---
diff --git a/docs/javaguide/use-suggestion.md b/docs/javaguide/use-suggestion.md
index e7ce843aaee..6b4cbbf0462 100644
--- a/docs/javaguide/use-suggestion.md
+++ b/docs/javaguide/use-suggestion.md
@@ -1,5 +1,6 @@
---
title: 使用建议
+description: JavaGuide使用建议,讲解如何高效利用本站内容进行Java学习与面试准备的方法指南。
category: 走近项目
icon: star
---
diff --git a/docs/open-source-project/README.md b/docs/open-source-project/README.md
new file mode 100644
index 00000000000..d79274825c6
--- /dev/null
+++ b/docs/open-source-project/README.md
@@ -0,0 +1,33 @@
+---
+title: Java 开源项目精选
+description: GitHub和Gitee上优质Java开源项目精选汇总,涵盖实战项目、工具库、系统设计等多种类型的开源资源推荐。
+category: 开源项目
+---
+
+
+
+精选 GitHub 和 Gitee 上优质的 Java 开源项目。
+
+灵感来源于[awesome-java](https://github.com/akullpp/awesome-java) 这个项目,可以看作是这个项目的中文本土版本,项目类型更全面且加入了更多中文开源项目。
+
+欢迎大家在项目 [issues 区](https://github.com/CodingDocs/awesome-java/issues)推荐自己认可的 Java 开源项目,让我们共同维护一个优质的 Java 开源项目精选集!
+
+- GitHub 地址:[https://github.com/CodingDocs/awesome-java](https://github.com/CodingDocs/awesome-java)
+- Gitee 地址:[https://gitee.com/SnailClimb/awesome-java](https://gitee.com/SnailClimb/awesome-java)
+
+内容概览:
+
+- [Java AI 相关优质开源项目](./machine-learning.md):Java AI 开发框架和实战项目推荐。
+- [Java 优质开源技术教程](./tutorial.md):优质面试资料/技术教程/学习路线整理,适合面试准备、系统学习与查缺补漏。
+- [Java 优质开源实战项目](./practical-project.md):简历友好、可落地的实战项目精选(后台管理、电商、权限、网盘、社区等)。
+- [Java 优质开源系统设计项目](./system-design.md):涵盖 Web 框架、微服务、消息队列、搜索引擎、数据库等基础架构组件精选。
+- [Java 优质开源工具类库](./tool-library.md):涵盖 Lombok、Guava、Hutool、Arthas 等提升开发效率和代码质量的常用工具。
+- [程序员必备开发工具](./tools.md):提升效率的开发工具与在线工具合集(IDE、调试、文档、效率等)。
+
+如果你想要快速挑项目做练手/写简历,优先看「[Java 优质开源实战项目](./practical-project.md)」;如果你在准备后端面试,优先看「[Java 优质开源技术教程](./tutorial.md)」。
+
+## 公众号
+
+最新更新会第一时间同步在公众号,推荐关注!另外,公众号上有很多干货不会同步在线阅读网站。
+
+
diff --git a/docs/open-source-project/big-data.md b/docs/open-source-project/big-data.md
index df1457bb203..54fe04d2f42 100644
--- a/docs/open-source-project/big-data.md
+++ b/docs/open-source-project/big-data.md
@@ -1,5 +1,6 @@
---
title: Java 优质开源大数据项目
+description: Java优质开源大数据项目推荐,涵盖Spark、Flink、HBase、Storm等主流大数据处理框架介绍与对比。
category: 开源项目
icon: big-data
---
diff --git a/docs/open-source-project/machine-learning.md b/docs/open-source-project/machine-learning.md
index 9feaaf4a769..2a8606e59f9 100644
--- a/docs/open-source-project/machine-learning.md
+++ b/docs/open-source-project/machine-learning.md
@@ -1,14 +1,109 @@
---
title: Java 优质开源 AI 项目
+description: Java优质开源AI项目推荐,涵盖Spring AI、LangChain4j、Deeplearning4j等Java人工智能和机器学习框架介绍。
category: 开源项目
icon: a-MachineLearning
---
-由于 Java 在 AI 领域应用较少,因此相关的开源项目也非常少:
+很多小伙伴私下问我:现在 AI 这么火,咱们写 Java 的是不是只能在旁边看戏?
-- [Spring AI](https://github.com/spring-projects/spring-ai):人工智能工程应用框架,为开发 AI 应用程序提供了 Spring 友好的 API 和抽象。
+**说实话,以前确实有点难受。** 毕竟主流的 AI 框架大多是 Python 的天下。但现在,时代变了!随着 Spring AI 以及各种 Java AI 框架的爆发,咱们 Java 开发者完全可以像平时写 CRUD 一样,优雅地把大模型集成到应用里。
+
+今天就带大家盘点一下,目前 Java 生态里最硬核的几个 AI 框架。
+
+## 基础框架
+
+### Spring AI
+
+[Spring AI](https://github.com/spring-projects/spring-ai) 是 Spring 官方亲自下场打造的 AI 应用开发框架 。它的核心哲学非常直观:**将 AI 能力无缝集成到 Spring 生态中** 。
+
+对于习惯了 Spring Boot 的开发者来说,这玩意儿几乎没有学习门槛。它提供了一套构建 AI 应用所需的“底层原子能力抽象” :
+
+- **模型通信 (ChatClient):** 提供了统一的接口与不同的大语言模型(如 OpenAI GPT、Ollama、Google Gemini)进行对话。
+- **提示词 (Prompt):** 结构化地管理和构建发送给模型的提示词。
+- **检索增强生成 (RAG):** 通过 `VectorStore` 等抽象,方便地实现 RAG 模式,将外部知识库与模型结合,提升回答的准确性和时效性。
+- **工具调用 (Function Calling):** 允许模型调用 Java 应用中定义好的方法,实现与外部世界的交互。
+- **记忆 (ChatMemory):** 管理多轮对话的上下文历史。
+
+官方文档:。
+
+### Spring AI Alibaba
+
+[Spring AI Alibaba](https://github.com/alibaba/spring-ai-alibaba) 集成 Spring AI 生态,它是一个专为多智能体系统和工作流编排设计的项目。项目从架构上包含如下三层:
+
+
+
+- **Agent Framework**:以 ReactAgent 设计理念为核心的 Agent 开发框架,构建具备自动上下文工程和人机交互能力的 Agent。
+- **Graph**:低级别的工作流和多代理协调框架,是 Agent Framework 的底层运行时基座,帮助实现复杂的应用程序编排。
+- **Augmented LLM**:基于 Spring AI 底层抽象,提供模型、工具、多模态组件(MCP)、向量存储等基础支持。
+
+另外它还有非常“工程化”的组件:
+
+- **Admin**:一站式 Agent 平台,支持可视化开发、可观测、评估、MCP 管理,甚至与 Dify 等低代码平台集成,支持 DSL 迁移。
+- **A2A(Agent-to-Agent)**:支持 Agent 间通信,并可与 Nacos 集成做分布式协调。
+
+官方文档: 。
+
+### LangChain4j
+
+如果说 Spring AI 是官方正规军,那 [LangChain4j](https://github.com/langchain4j/langchain4j) 就是目前社区里非常强势的 Java LLM 框架,它是 LangChain 的 Java 版本。
+
+它的优势在于功能全面,各种大模型的适配速度快得离谱,但在 Spring 体系里总有一种“外来客”的违和感。
+
+如果你追求“多模型快速切换 + 能力覆盖面广 + 原型推进快”,LangChain4j 通常是第一梯队选择;代价是你需要自己在工程结构、治理、可观测、平台化上多做一点“工程化拼装”。
+
+官方文档: 。
+
+### AgentScope
+
+[AgentScope](https://github.com/agentscope-ai/agentscope-java) 是一个多智能体框架,旨在提供一种简单高效的方式来构建基于大语言模型的智能体应用程序。
+
+如果说大模型(LLM)是 AI 应用的大脑,那么 AgentScope 就是它的“中枢神经系统”和“手脚”。它不仅提供了多智能体协作的架构,还内置了 ReAct 推理、工具调用、记忆管理等核心能力。
+
+AgentScope 提供了 Python 和 Java 版本,二者核心能力完全对齐!
+
+**AgentScope 也是阿里开源的,那和 Spring AI Alibaba 有何不同呢?**
+
+- **AgentScope Java**:原生为 **Agentic(智能体)范式**设计。它的核心是“Agent”,强调的是自主性、推理循环(ReAct)和多智能体之间的复杂博弈与协作。
+- **Spring AI Alibaba**:更侧重于 **Workflow(工作流)编排**。它基于 Spring AI 生态,擅长将 AI 能力作为工具融入到预定义的业务流中。
+
+官方文档:。
+
+### 其他
+
+- [Solon-AI](https://github.com/opensolon/solon-ai):Java AI 应用开发框架(支持 LLM,RAG,MCP,Agent),同时兼容 Java8 ~ Java25,支持 SpringBoot、jFinal、Vert.x、Quarkus 等框架。
+- [Agent-Flex](https://github.com/agents-flex/agents-flex):一个优雅的 LLM(大语言模型)应用开发框架,对标 LangChain、使用 Java 开发、简单、轻量。
- [Deeplearning4j](https://github.com/eclipse/deeplearning4j):Deeplearning4j 是第一个为 Java 和 Scala 编写的商业级,开源,分布式深度学习库。
- [Smile](https://github.com/haifengl/smile):基于 Java 和 Scala 的机器学习库。
- [GdxAI](https://github.com/libgdx/gdx-ai):完全用 Java 编写的人工智能框架,用于使用 libGDX 进行游戏开发。
-- [chatgpt-java](https://github.com/Grt1228/chatgpt-java):ChatGPT Java SDK。
-- [ai-beehive](https://github.com/hncboy/ai-beehive):AI 蜂巢,基于 Java 使用 Spring Boot 3 和 JDK 17,支持的功能有 ChatGPT、OpenAi Image、Midjourney、NewBing、文心一言等等。
+
+### 对比
+
+| **框架名称** | **核心特点** | **适用场景** |
+| --------------------- | ------------------------------------------------------------------------------------------------------------------ | ---------------------------------------------------------- |
+| **Spring AI** | Spring 官方底座:模型/向量库/工具调用/记忆/RAG/可观测/结构化输出;强调可移植与模块化 | 现有 Spring Boot 企业应用 AI 化 |
+| **Spring AI Alibaba** | 面向 Agentic/Workflow/Multi-agent 的生产级体系:Agent Framework + Graph Runtime + Admin/Studio;支持 MCP/A2A/Nacos | 多智能体编排、复杂工作流、平台化治理与迁移(含可视化) |
+| **LangChain4j** | 社区强势:统一 API 连接多模型/多向量库;Agents/Tools/RAG;支持 MCP;可集成 Spring/Quarkus/Helidon | 快速原型、强灵活性、多模型快速切换 |
+| **Solon-AI** | Java 8~25 兼容;LLM/RAG/MCP/Agent/Ai Flow 全链路;可嵌入多框架 | 历史系统/多框架场景、追求兼容性与全链路能力 |
+| **Agent-Flex** | 轻量优雅:LLM/Prompt/Tool/MCP/Memory/Embedding/VectorStore/文档处理;OpenTelemetry 可观测 | 追求简洁上手、可观测的 LLM 应用开发 |
+| **AgentScope Java** | Agentic 原生:ReAct + Tool + Memory + 多 Agent;MCP+A2A(Nacos);Reactor 响应式 + GraalVM Serverless | 自主智能体、分布式多 Agent、对生产可控性与性能要求高的场景 |
+
+## 实战
+
+### 智能面试平台
+
+[interview-guide](https://github.com/Snailclimb/interview-guide) 基于 Spring Boot 4.0 + Java 21 + Spring AI + PostgreSQL + pgvector + RustFS + Redis,实现简历智能分析、AI 模拟面试、知识库 RAG 检索等核心功能。非常适合作为学习和简历项目,学习门槛低。
+
+**系统架构如下**:
+
+> **提示**:架构图采用 draw.io 绘制,导出为 svg 格式,在 Github Dark 模式下的显示效果会有问题。
+
+
+
+### AI 工作流编排系统
+
+[PaiAgent](https://github.com/itwanger/PaiAgent) 是一个**企业级的 AI 工作流可视化编排平台**,让 AI 能力的组合和调度变得简单高效。通过直观的拖拽式界面,开发者和业务人员都能快速构建复杂的 AI 处理流程,无需编写代码即可实现多种大模型的协同工作。
+
+**系统架构如下**:
+
+
diff --git a/docs/open-source-project/practical-project.md b/docs/open-source-project/practical-project.md
index 4d63af8f082..e0424970c3b 100644
--- a/docs/open-source-project/practical-project.md
+++ b/docs/open-source-project/practical-project.md
@@ -1,9 +1,15 @@
---
title: Java 优质开源实战项目
+description: Java优质开源实战项目推荐,涵盖快速开发平台、电商系统、权限管理等可用于学习和简历的实战项目精选。
category: 开源项目
icon: project
---
+## AI
+
+- [interview-guide](https://github.com/Snailclimb/interview-guide):基于 Spring Boot 4.0 + Java 21 + Spring AI + PostgreSQL + pgvector + RustFS + Redis,实现简历智能分析、AI 模拟面试、知识库 RAG 检索等核心功能。非常适合作为学习和简历项目,学习门槛低。
+- [PaiAgent](https://github.com/itwanger/PaiAgent):一个企业级的 AI 工作流可视化编排平台,让 AI 能力的组合和调度变得简单高效。通过直观的拖拽式界面,开发者和业务人员都能快速构建复杂的 AI 处理流程,无需编写代码即可实现多种大模型的协同工作。
+
## 快速开发平台
- [Snowy](https://gitee.com/xiaonuobase/snowy):国内首个国密前后端分离快速开发平台。详细介绍:[5.1k!这是我见过最强的前后端分离快速开发脚手架!!](https://mp.weixin.qq.com/s?__biz=Mzg2OTA0Njk0OA==&mid=2247534316&idx=1&sn=69938397674fc33ecda43c8c9d0a4039&chksm=cea10927f9d68031bc862485c6be984ade5af233d4d871d498c38f22164a84314678c0c67cd7&token=1464380539&lang=zh_CN#rd)。
@@ -28,6 +34,7 @@ icon: project
- [paicoding](https://github.com/itwanger/paicoding):一款好用又强大的开源社区,基于 Spring Boot 系列主流技术栈,附详细的教程。
- [forest](https://github.com/rymcu/forest):下一代的知识社区系统,可以自定义专题和作品集。后端基于 SpringBoot + Shrio + MyBatis + JWT + Redis,前端基于 Vue + NuxtJS + Element-UI。
- [community](https://github.com/codedrinker/community):开源论坛、问答系统,现有功能提问、回复、通知、最新、最热、消除零回复功能。功能持续更新中…… 技术栈 Spring、Spring Boot、MyBatis、MySQL/H2、Bootstrap。
+- [OneBlog](https://gitee.com/yadong.zhang/DBlog):简洁美观、功能强大并且自适应的博客系统,支持广告位、SEO、实时通讯等功能。
- [VBlog](https://github.com/lenve/VBlog):V 部落,Vue+SpringBoot 实现的多用户博客管理平台!
- [My-Blog](https://github.com/ZHENFENG13/My-Blog): SpringBoot + Mybatis + Thymeleaf 等技术实现的 Java 博客系统,页面美观、功能齐全、部署简单及完善的代码,一定会给使用者无与伦比的体验。
@@ -38,6 +45,7 @@ icon: project
## 文件管理系统/网盘
+- [cloud-drive](https://gitee.com/SnailClimb/cloud-drive):一个极简的现代化云存储系统,基于阿里云 OSS,提供文件上传、下载、分享等功能。系统采用前后端分离架构,提供安全可靠的文件存储服务。
- [qiwen-file](https://gitee.com/qiwen-cloud/qiwen-file):基于 SpringBoot+Vue 实现的分布式文件系统,支持本地磁盘、阿里云 OSS 对象存储、FastDFS 存储、MinIO 存储等多种存储方式,支持 office 在线编辑、分片上传、技术秒传、断点续传等功能。
- [free-fs](https://gitee.com/dh_free/free-fs):基于 SpringBoot + MyBatis Plus + MySQL + Sa-Token + Layui 等搭配七牛云, 阿里云 OSS 实现的云存储管理系统。 包含文件上传、删除、在线预览、云资源列表查询、下载、文件移动、重命名、目录管理、登录、注册、以及权限控制等功能。
- [zfile](https://github.com/zfile-dev/zfile):基于 Spring Boot + Vue 实现的在线网盘,支持对接 S3、OneDrive、SharePoint、Google Drive、多吉云、又拍云、本地存储、FTP、SFTP 等存储源,支持在线浏览图片、播放音视频,文本文件、Office、obj(3d)等文件类型。
@@ -65,19 +73,14 @@ icon: project
## 售票系统
- [12306](https://gitee.com/nageoffer/12306) :基于 JDK17 + SpringBoot3 + SpringCloud 微服务架构的高并发 12306 购票服务。
-
-## 权限管理系统
-
-权限管理系统在企业级的项目中一般都是非常重要的,如果你需求去实际了解一个不错的权限系统是如何设计的话,推荐你可以参考下面这些开源项目。
-
-- [SpringBoot-Shiro-Vue](https://github.com/Heeexy/SpringBoot-Shiro-Vue):基于 Spring Boot-Shiro-Vue 的权限管理思路,前后端都加以控制,可以做到按钮/接口级别的权限。
-- [renren-security](https://gitee.com/renrenio/renren-security):一套灵活的权限控制系统,可控制到页面或按钮,满足绝大部分的权限需求
+- [大麦](https://gitee.com/java-up-up/damai):提供热门演唱会的购票功能,并且对如何解决高并发下的抢票而产生的各种问题,从而设计出了实际落地的解决方案。
## 造轮子
- [guide-rpc-framework](https://github.com/Snailclimb/guide-rpc-framework):一款基于 Netty+Kyro+Zookeeper 实现的自定义 RPC 框架-附详细实现过程和相关教程。
- [mini-spring](https://github.com/DerekYRC/mini-spring):简化版的 Spring 框架,能帮助你快速熟悉 Spring 源码和掌握 Spring 的核心原理。代码极度简化,保留了 Spring 的核心功能,如 IoC 和 AOP、资源加载器等核心功能。
- [mini-spring-cloud](https://github.com/DerekYRC/mini-spring-cloud):一个手写的简化版的 Spring Cloud,旨在帮助你快速熟悉 Spring Cloud 源码及掌握其核心原理。相关阅读:[手写一个简化版的 Spring Cloud!](https://mp.weixin.qq.com/s/v3FUp-keswE2EhcTaLpSMQ) 。
+- [haidnorJVM](https://github.com/FranzHaidnor/haidnorJVM):使用 Java 实现的简易版 Java 虚拟机,介绍:。
- [itstack-demo-jvm](https://github.com/fuzhengwei/itstack-demo-jvm):通过 Java 代码来实现 JVM 的基础功能(搜索解析 class 文件、字节码命令、运行时数据区等。相关阅读:[《zachaxy 的手写 JVM 系列》](https://zachaxy.github.io/tags/JVM/)。
- [Freedom](https://github.com/alchemystar/Freedom):自己 DIY 一个具有 ACID 的数据库。相关项目:[MYDB](https://github.com/CN-GuoZiyang/MYDB)(一个简单的数据库实现)、[toyDB](https://github.com/erikgrinaker/toydb)(Rust 实现的分布式 SQL 数据库)。
- [lu-raft-kv](https://github.com/stateIs0/lu-raft-kv):一个 Java 版本的 Raft(CP) KV 分布式存储实现,非常适合想要深入学习 Raft 协议的小伙伴研究。lu-raft-kv 已经实现了 Raft 协议其中的两个核心功能:leader 选举和日志复制。如果你想要学习这个项目的话,建议你提前看一下作者写的项目介绍,比较详细,地址: 。
diff --git a/docs/open-source-project/readme.md b/docs/open-source-project/readme.md
deleted file mode 100644
index bb8a79f43a9..00000000000
--- a/docs/open-source-project/readme.md
+++ /dev/null
@@ -1,51 +0,0 @@
----
-title: Java 开源项目精选
-category: 开源项目
----
-
-
-
-精选 GitHub 和 Gitee 上优质的 Java 开源项目。
-
-灵感来源于[awesome-java](https://github.com/akullpp/awesome-java) 这个项目,可以看作是这个项目的中文本土版本,项目类型更全面且加入了更多中文开源项目。
-
-欢迎大家在项目 [issues 区](https://github.com/CodingDocs/awesome-java/issues)推荐自己认可的 Java 开源项目,让我们共同维护一个优质的 Java 开源项目精选集!
-
-- GitHub 地址:[https://github.com/CodingDocs/awesome-java](https://github.com/CodingDocs/awesome-java)
-- Gitee 地址:[https://gitee.com/SnailClimb/awesome-java](https://gitee.com/SnailClimb/awesome-java)
-
-如果内容对你有帮助的话,欢迎给本项目点个 Star。我会用我的业余时间持续完善这份名单,感谢!
-
-另外,我的公众号还会定期分享优质开源项目,每月一期,每一期我都会精选 5 个高质量的 Java 开源项目。
-
-目前已经更新到了第 24 期:
-
-1. [一款基于 Spring Boot + Vue 的一站式开源持续测试平台](http://mp.weixin.qq.com/s?__biz=Mzg2OTA0Njk0OA==&mid=2247515383&idx=1&sn=ba7244020c05d966b483d8c302d54e85&chksm=cea1f33cf9d67a2a111bcf6cadc3cc1c44828ba2302cd3e13bbd88349e43d4254808e6434133&scene=21#wechat_redirect)。
-2. [用 Java 写个沙盒塔防游戏!已上架 Steam,Apple Store](https://mp.weixin.qq.com/s?__biz=Mzg2OTA0Njk0OA==&mid=2247515981&idx=1&sn=e4b9c06af65f739bdcdf76bdc35d59f6&chksm=cea1f086f9d679908bd6604b1c42d67580160d9789951f3707ad2f5de4d97aa72121d8fe777e&token=435278690&lang=zh_CN&scene=21#wechat_redirect)
-3. [一款基于 Java 的可视化 HTTP API 接口开发神器](https://mp.weixin.qq.com/s?__biz=Mzg2OTA0Njk0OA==&mid=2247516459&idx=1&sn=a86fefe083fa91c83638243d75500a04&chksm=cea1cee0f9d647f69237357e869f52e0903afad62f365e18b04ff1851aeb4c80c8d31a488fee&scene=21&cur_album_id=1345382825083895808#wechat_redirect)
-4. [一款对业务代码无侵入的可视化 Java 进程管理平台](https://mp.weixin.qq.com/s?__biz=Mzg2OTA0Njk0OA==&mid=2247518215&idx=1&sn=91e467f39322d2e7979b85fe235822d2&chksm=cea1c7ccf9d64edaf966c95923d72d337bf5e655a773a3d295d65fc92e4535ae5d8b0e6d9d86&token=660789642&lang=zh_CN#rd)
-5. [一个比 Spring 更轻量级的 Web 框架!!!微软、红帽都在用](https://mp.weixin.qq.com/s?__biz=Mzg2OTA0Njk0OA==&mid=2247519466&idx=1&sn=0dd412d5220444b37a1101f77ccdc65d&chksm=cea1c321f9d64a376ef7de329b5c91e593a32c7a8e5c179b7ab3619296feea35939deb1f6a3f&scene=178&cur_album_id=1345382825083895808#rd)
-6. [轻量!Google 开源了一个简易版 Spring !](https://mp.weixin.qq.com/s?__biz=Mzg2OTA0Njk0OA==&mid=2247519972&idx=1&sn=f03c67e6e24eda2ccf703c8a9bc8c8f8&chksm=cea1c12ff9d6483943f409e5ab50b773b5750b63d00950805fa340a67ad7b52ee74ff6651043&scene=178&cur_album_id=1345382825083895808#rd)
-7. [一款跨时代的高性能 Java 框架!启动速度快到飞起](https://mp.weixin.qq.com/s?__biz=Mzg2OTA0Njk0OA==&mid=2247520633&idx=1&sn=aec35af40e3ed3b1e844addd04e31af5&chksm=cea1deb2f9d657a46a0684bbcbcb2900cebff39a2b2746a4a809b6b5306bce08d4382efd5ca8&scene=178&cur_album_id=1345382825083895808#rd)
-8. [Spring Boot+MyBatis Plus+JWT 问卷系统!开源!](https://mp.weixin.qq.com/s/kRgqHt73ZJGFQ2XmKG4PXw)
-9. [手写一个简化版的 Spring Cloud!](https://mp.weixin.qq.com/s/v3FUp-keswE2EhcTaLpSMQ)
-10. [这个 SpringBoot+ Vue 开源博客系统太酷炫了!](https://mp.weixin.qq.com/s/CCzsX3Sn2Q3vhuBDEmRTlw)
-11. [手写一个简易版数据库!项目经验稳了](https://mp.weixin.qq.com/mp/appmsgalbum?__biz=Mzg2OTA0Njk0OA==&action=getalbum&album_id=1345382825083895808&scene=173&from_msgid=2247530323&from_itemidx=1&count=3&nolastread=1#wechat_redirect)
-12. [一款强大的快速开发脚手架,前后端分离,干掉 70% 重复工作!](https://mp.weixin.qq.com/s/Ecjm801RpS34Mhj02bIOsQ)
-13. [手写一个入门级编译器!YYDS!](https://mp.weixin.qq.com/s?__biz=Mzg2OTA0Njk0OA==&mid=2247530783&idx=1&sn=c9fdc0c71e2fc95d88ba954291b07e29&chksm=cea136d4f9d6bfc2931a18a42f7bd9903503963e8a85a318adcce579614c0831b1881be3267d&token=1811572747&lang=zh_CN#rd)
-14. [8.8k star,这可能是我见过最强的开源支付系统!!](https://mp.weixin.qq.com/s/vfPSXtOgefwonbnP53KlOQ)
-15. [31.2k!这是我见过最强的后台管理系统 !!](https://mp.weixin.qq.com/s/esaivn2z_66CcrRJlDYLEA)
-16. [14.3k star,这是我见过最强的第三方登录工具库!!](https://mp.weixin.qq.com/s/6-TnCHUMEIFWQVl-pIWBOA)
-17. [3.2k!这是我见过最强的消息推送平台!!](https://mp.weixin.qq.com/s/heag76H4UwZmr8oBY_2gcw)
-18. [好家伙,又一本技术书籍开源了!!](https://mp.weixin.qq.com/s/w-JuBlcqCeAZR0xUFWzvHQ)
-19. [开箱即用的 ChatGPT Java SDK!支持 GPT3.5、 GPT4 API](https://mp.weixin.qq.com/s/WhI2K1VF0h_57TEVGCwuCA)
-20. [这是我见过最强大的技术社区实战项目!!](https://mp.weixin.qq.com/s/tdBQ0Td_Gsev4AaIlq5ltg)
-21. [颜值吊打 Postman,这款开源 API 调试工具我超爱!!](https://mp.weixin.qq.com/s/_KXBGckyS--P97G48zXCrw)
-22. [轻量级 Spring,够优雅!!](https://mp.weixin.qq.com/s/tl2539hsYsvEm8wjmQwDEg)
-23. [这是我见过最强的 Java 版内网穿透神器!](https://mp.weixin.qq.com/s/4hyQsTICIUf9EvAVrC6wEg)
-
-推荐你在我的公众号“**JavaGuide**”回复“**开源**”在线阅读[「优质开源项目推荐」](https://mp.weixin.qq.com/mp/appmsgalbum?__biz=Mzg2OTA0Njk0OA==&action=getalbum&album_id=1345382825083895808&scene=173&from_msgid=2247516459&from_itemidx=1&count=3&nolastread=1#wechat_redirect)系列。
-
-
-
-
diff --git a/docs/open-source-project/system-design.md b/docs/open-source-project/system-design.md
index d3a3e92b56c..3afb3103dc8 100644
--- a/docs/open-source-project/system-design.md
+++ b/docs/open-source-project/system-design.md
@@ -1,5 +1,6 @@
---
title: Java 优质开源系统设计项目
+description: Java优质开源系统设计项目推荐,涵盖Web框架、微服务、消息队列、搜索引擎、数据库等基础架构组件精选。
category: 开源项目
icon: "xitongsheji"
---
@@ -10,6 +11,7 @@ icon: "xitongsheji"
- [Spring Boot](https://github.com/spring-projects/spring-boot "spring-boot"):Spring Boot 可以轻松创建独立的生产级基于 Spring 的应用程序,内置 web 服务器让你可以像运行普通 Java 程序一样运行项 目。另外,大部分 Spring Boot 项目只需要少量的配置即可,这有别于 Spring 的重配置。
- [SOFABoot](https://github.com/sofastack/sofa-boot):SOFABoot 基于 Spring Boot ,不过在其基础上增加了 Readiness Check,类隔离,日志空间隔离等等能力。 配套提供的还有:SOFARPC(RPC 框架)、SOFABolt(基于 Netty 的远程通信框架)、SOFARegistry(注册中心)...详情请参考:[SOFAStack](https://github.com/sofastack) 。
+- [Solon](https://gitee.com/opensolon/solon):国产面向全场景的 Java 企业级应用开发框架。
- [Javalin](https://github.com/tipsy/javalin):一个轻量级的 Web 框架,同时支持 Java 和 Kotlin,被微软、红帽、Uber 等公司使用。
- [Play Framework](https://github.com/playframework/playframework):面向 Java 和 Scala 的高速 Web 框架。
- [Blade](https://github.com/lets-blade/blade):一款追求简约、高效的 Web 框架,基于 Java8 + Netty4。
@@ -18,6 +20,7 @@ icon: "xitongsheji"
- [Armeria](https://github.com/line/armeria):适合任何情况的微服务框架。你可以用你喜欢的技术构建任何类型的微服务,包括[gRPC](https://grpc.io/)、 [Thrift](https://thrift.apache.org/)、[Kotlin](https://kotlinlang.org/)、 [Retrofit](https://square.github.io/retrofit/)、[Reactive Streams](https://www.reactive-streams.org/)、 [Spring Boot](https://spring.io/projects/spring-boot)和[Dropwizard](https://www.dropwizard.io/)
- [Quarkus](https://github.com/quarkusio/quarkus) : 用于编写 Java 应用程序的云原生和容器优先的框架。
+- [Helidon](https://github.com/helidon-io/helidon):一组用于编写微服务的 Java 库,支持 Helidon MP 和 Helidon SE 两种编程模型。
### API 文档
@@ -27,6 +30,7 @@ icon: "xitongsheji"
### Bean 映射
- [MapStruct](https://github.com/mapstruct/mapstruct)(推荐):满足 JSR269 规范的一个 Java 注解处理器,用于为 Java Bean 生成类型安全且高性能的映射。它基于编译阶段生成 get/set 代码,此实现过程中没有反射,不会造成额外的性能损失。
+- [MapStruct Plus](https://github.com/linpeilie/mapstruct-plus):MapStruct 增强版本,支持自动生成 Mapper 接口。
- [JMapper](https://github.com/jmapper-framework/jmapper-core) : 一个高性能且易于使用的 Bean 映射框架。
### 其他
@@ -72,7 +76,7 @@ icon: "xitongsheji"
- [MyBatis-Plus](https://github.com/baomidou/mybatis-plus) : [MyBatis](http://www.mybatis.org/mybatis-3/) 增强工具,在 MyBatis 的基础上只做增强不做改变,为简化开发、提高效率而生。
- [MyBatis-Flex](https://gitee.com/mybatis-flex/mybatis-flex):一个优雅的 MyBatis 增强框架,无其他任何第三方依赖,支持 CRUD、分页查询、多表查询、批量操作。
- [jOOQ](https://github.com/jOOQ/jOOQ):用 Java 编写 SQL 的最佳方式。
-- [Redisson](https://github.com/redisson/redisson "redisson"):Redis 基础上的一个 Java 驻内存数据网格(In-Memory Data Grid),支持超过 30 个对象和服务:`Set`,`SortedSet`, `Map`, `List`, `Queue`, `Deque` ……,并且提供了多种分布式锁的实现。更多介绍请看:[《Redisson 项目介绍》](https://github.com/redisson/redisson/wiki/Redisson%E9%A1%B9%E7%9B%AE%E4%BB%8B%E7%BB%8D "Redisson项目介绍")。
+- [Redisson](https://github.com/redisson/redisson "redisson"):Redisson 是一款架设在 Redis 基础之上的 Java 驻内存数据网格 (In-Memory Data Grid),它充分利用了 Redis 键值数据库的优势,为 Java 开发者提供了一系列具有分布式特性的常用工具类。例如,分布式 Java 对象(`Set`,`SortedSet`,`Map`,`List`,`Queue`,`Deque` 等)、分布式锁等。详细介绍请看:[Redisson 项目介绍](https://github.com/redisson/redisson/wiki/Redisson%E9%A1%B9%E7%9B%AE%E4%BB%8B%E7%BB%8D "Redisson项目介绍")。
### 数据同步
@@ -115,9 +119,12 @@ icon: "xitongsheji"
### API 调试
-- [Insomnia](https://insomnia.rest/) :像人类而不是机器人一样调试 API。我平时经常用的,界面美观且轻量,总之很喜欢。
-- [Postman](https://www.getpostman.com/):API 请求生成器。
-- [Postwoman](https://github.com/liyasthomas/postwoman "postwoman"):API 请求生成器-一个免费、快速、漂亮的 Postma 替代品。
+- [Reqable](https://reqable.com/zh-CN/):新一代开源 API 开发工具。Reqable = Fiddler + Charles + Postman, 让 API 调试更快。
+- [Insomnia](https://insomnia.rest/) :像人类而不是机器人一样调试 API。我平时经常用的一款 API 开发工具,界面美观且轻量,总之很喜欢。
+- [RapidAPI](https://paw.cloud/):一款功能齐全的 HTTP 客户端,但仅支持 Mac。
+- [Postcat](https://github.com/Postcatlab/postcat):一个可扩展的开源 API 工具平台。
+- [Postman](https://www.getpostman.com/):开发者最常用的 API 测试工具之一。
+- [Hoppscotch](https://github.com/liyasthomas/postwoman "postwoman")(原 Postwoman):开源 API 测试工具。官方定位是 Postman、Insomnia 等产品的开源替代品。
- [Restful Fast Request](https://gitee.com/dromara/fast-request):IDEA 版 Postman,API 调试工具 + API 管理工具 + API 搜索工具。
## 任务调度
@@ -125,9 +132,15 @@ icon: "xitongsheji"
- [Quartz](https://github.com/quartz-scheduler/quartz):一个很火的开源任务调度框架,Java 定时任务领域的老大哥或者说参考标准, 很多其他任务调度框架都是基于 `quartz` 开发的,比如当当网的`elastic-job`就是基于`quartz`二次开发之后的分布式调度解决方案
- [XXL-JOB](https://github.com/xuxueli/xxl-job) :XXL-JOB 是一个分布式任务调度平台,其核心设计目标是开发迅速、学习简单、轻量级、易扩展。现已开放源代码并接入多家公司线上产品线,开箱即用。
- [Elastic-Job](http://elasticjob.io/index_zh.html):Elastic-Job 是当当网开源的一个基于 Quartz 和 Zookeeper 的分布式调度解决方案,由两个相互独立的子项目 Elastic-Job-Lite 和 Elastic-Job-Cloud 组成,一般我们只要使用 Elastic-Job-Lite 就好。
-- [EasyScheduler](https://github.com/analysys/EasyScheduler "EasyScheduler") (已经更名为 DolphinScheduler,已经成为 Apache 孵化器项目):Easy Scheduler 是一个分布式工作流任务调度系统,主要解决“复杂任务依赖但无法直接监控任务健康状态”的问题。Easy Scheduler 以 DAG 方式组装任务,可以实时监控任务的运行状态。同时,它支持重试,重新运行等操作... 。
+- [EasyScheduler](https://github.com/analysys/EasyScheduler "EasyScheduler") (已经更名为 DolphinScheduler,已经成为 Apache 孵化器项目):分布式易扩展的可视化工作流任务调度平台,主要解决“复杂任务依赖但无法直接监控任务健康状态”的问题。
- [PowerJob](https://gitee.com/KFCFans/PowerJob):新一代分布式任务调度与计算框架,支持 CRON、API、固定频率、固定延迟等调度策略,提供工作流来编排任务解决依赖关系,使用简单,功能强大,文档齐全,欢迎各位接入使用! 。
-- [DolphinScheduler](https://github.com/apache/dolphinscheduler):分布式易扩展的可视化工作流任务调度平台。
+
+## 工作流
+
+1. [Flowable](https://github.com/flowable/flowable-engine) :Activiti5 的一个分支发展而来,功能丰富,在 Activiti 的基础上,引入了更多高级功能,如更强大的 CMMN(案例管理模型与符号)、DMN(决策模型与符号)支持,以及更灵活的集成选项。
+2. [Activiti](https://github.com/Activiti/Activiti):功能扩展相对保守,适合需要稳定 BPMN 2.0 工作流引擎的传统企业应用。
+3. [Warm-Flow](https://gitee.com/dromara/warm-flow):国产开源工作流引擎,其特点简洁轻量但又不简单,五脏俱全,组件独立,可扩展。
+4. [FlowLong](https://gitee.com/aizuda/flowlong):国产开源工作流引擎,专门中国特色流程审批打造。
## 分布式
@@ -153,6 +166,11 @@ icon: "xitongsheji"
相关阅读:[Skywalking 官网对于主流开源链路追踪系统的对比](https://skywalking.apache.org/zh/blog/2019-03-29-introduction-of-skywalking-and-simple-practice.html)
+### 分布式锁
+
+- [Lock4j](https://gitee.com/baomidou/lock4j):支持 Redisson、ZooKeeper 等不同方案的高性能分布式锁。
+- [Redisson](https://github.com/redisson/redisson "redisson"):Redisson 在分布式锁方面提供全面且强大的支持,超越了简单的 Redis 锁实现。
+
## 高性能
### 多线程
@@ -185,7 +203,7 @@ icon: "xitongsheji"
**分布式队列**:
- [RocketMQ](https://github.com/apache/rocketmq "RocketMQ"):阿里巴巴开源的一款高性能、高吞吐量的分布式消息中间件。
-- [Kafaka](https://github.com/apache/kafka "Kafaka"): Kafka 是一种分布式的,基于发布 / 订阅的消息系统。
+- [Kafka](https://github.com/apache/kafka "Kafka"): Kafka 是一种分布式的,基于发布 / 订阅的消息系统。
- [RabbitMQ](https://github.com/rabbitmq "RabbitMQ") :由 erlang 开发的基于 AMQP(Advanced Message Queue 高级消息队列协议)协议实现的消息队列。
**内存队列**:
diff --git a/docs/open-source-project/tool-library.md b/docs/open-source-project/tool-library.md
index 8609dde149e..ea473480df6 100644
--- a/docs/open-source-project/tool-library.md
+++ b/docs/open-source-project/tool-library.md
@@ -1,18 +1,19 @@
---
title: Java 优质开源工具类
+description: Java优质开源工具类库推荐,涵盖Lombok、Guava、Hutool、Arthas等提升开发效率和代码质量的常用工具。
category: 开源项目
icon: codelibrary-fill
---
## 代码质量
-- [lombok](https://github.com/rzwitserloot/lombok) :使用 Lombok 我们可以简化我们的 Java 代码,比如使用它之后我们通过注释就可以实现 getter/setter、equals 等方法。
-- [guava](https://github.com/google/guava "guava"):Guava 是一组核心库,其中包括新的集合类型(例如 multimap 和 multiset),不可变集合,图形库以及用于并发、I / O、哈希、原始类型、字符串等的实用程序!
-- [hutool](https://github.com/looly/hutool "hutool") : Hutool 是一个 Java 工具包,也只是一个工具包,它帮助我们简化每一行代码,减少每一个方法,让 Java 语言也可以“甜甜的”。
+- [Lombok](https://github.com/rzwitserloot/lombok) :一个能够简化 Java 代码的强大工具库。通过使用 Lombok 的注解,我们可以自动生成常用的代码逻辑,例如 `getter`、`setter`、`equals`、`hashCode`、`toString` 方法,以及构造器、日志变量等内容。
+- [Guava](https://github.com/google/guava "guava"): Google 开发的一组功能强大的核心库,扩展了 Java 的标准库功能。它提供了许多有用的工具类和集合类型,例如 `Multimap`(多值映射)、`Multiset`(多重集合)、`BiMap`(双向映射)和不可变集合,此外还包含图形处理库和并发工具。Guava 还支持 I/O 操作、哈希算法、字符串处理、缓存等多种实用功能。
+- [Hutool](https://github.com/looly/hutool "hutool") : 一个全面且用户友好的 Java 工具库,旨在通过最小的依赖简化开发任务。它封装了许多实用的功能,例如文件操作、缓存、加密/解密、日志、文件操作。
## 问题排查和性能优化
-- [arthas](https://github.com/alibaba/arthas "arthas"):Alibaba 开源的 Java 诊断工具,可以实时监控和诊断 Java 应用程序。它提供了丰富的命令和功能,用于分析应用程序的性能问题,包括启动过程中的资源消耗和加载时间。
+- [Arthas](https://github.com/alibaba/arthas "arthas"):Alibaba 开源的 Java 诊断工具,可以实时监控和诊断 Java 应用程序。它提供了丰富的命令和功能,用于分析应用程序的性能问题,包括启动过程中的资源消耗和加载时间。
- [Async Profiler](https://github.com/async-profiler/async-profiler):低开销的异步 Java 性能分析工具,用于收集和分析应用程序的性能数据。
- [Spring Boot Startup Report](https://github.com/maciejwalkowiak/spring-boot-startup-report):用于生成 Spring Boot 应用程序启动报告的工具。它可以提供详细的启动过程信息,包括每个 bean 的加载时间、自动配置的耗时等,帮助你分析和优化启动过程。
- [Spring Startup Analyzer](https://github.com/linyimin0812/spring-startup-analyzer/blob/main/README_ZH.md):采集 Spring 应用启动过程数据,生成交互式分析报告(HTML),用于分析 Spring 应用启动卡点,支持 Spring Bean 异步初始化,减少优化 Spring 应用启动时间。UI 参考[Spring Boot Startup Report](https://github.com/maciejwalkowiak/spring-boot-startup-report)实现。
@@ -25,9 +26,10 @@ icon: codelibrary-fill
### Excel
-- [easyexcel](https://github.com/alibaba/easyexcel) :快速、简单避免 OOM 的 Java 处理 Excel 工具。
-- [excel-streaming-reader](https://github.com/monitorjbl/excel-streaming-reader):Excel 流式代码风格读取工具(只支持读取 XLSX 文件),基于 Apache POI 封装,同时保留标准 POI API 的语法。
-- [myexcel](https://github.com/liaochong/myexcel):一个集导入、导出、加密 Excel 等多项功能的工具包。
+- [EasyExcel](https://github.com/alibaba/easyexcel) :快速、简单避免 OOM 的 Java 处理 Excel 工具。不过,这个个项目不再维护,迁移至了 [FastExcel](https://github.com/fast-excel/fastexcel)。
+- [Excel Spring Boot Starter](https://github.com/pig-mesh/excel-spring-boot-starter):基于 FastExcel 实现的 Spring Boot Starter,用于简化 Excel 的读写操作。
+- [Excel Streaming Reader](https://github.com/monitorjbl/excel-streaming-reader):Excel 流式代码风格读取工具(只支持读取 XLSX 文件),基于 Apache POI 封装,同时保留标准 POI API 的语法。
+- [MyExcel](https://github.com/liaochong/myexcel):一个集导入、导出、加密 Excel 等多项功能的工具包。
### Word
@@ -39,10 +41,12 @@ icon: codelibrary-fill
### PDF
-- [x-easypdf](https://gitee.com/dromara/x-easypdf):一个用搭积木的方式构建 PDF 的框架(基于 pdfbox/fop),支持 PDF 导出和编辑。
-- [pdfbox](https://github.com/apache/pdfbox) :用于处理 PDF 文档的开放源码 Java 工具。该项目允许创建新的 PDF 文档、对现有文档进行操作以及从文档中提取内容。PDFBox 还包括几个命令行实用程序。PDFBox 是在 Apache 2.0 版许可下发布的。
-- [OpenPDF](https://github.com/LibrePDF/OpenPDF):OpenPDF 是一个免费的 Java 库,用于使用 LGPL 和 MPL 开源许可创建和编辑 PDF 文件。OpenPDF 基于 iText 的一个分支。
-- [itext7](https://github.com/itext/itext7):一个用于创建、编辑和增强 PDF 文档的 Java 库。
+对于简单的 PDF 创建需求,OpenPDF 是一个不错的选择,它开源免费,API 简单易用。对于需要解析、转换和提取文本等操作的复杂场景,可以选择 Apache PDFBox。当然了,复杂场景如果不介意 LGPL 许可也可以选择 iText。
+
+- [x-easypdf](https://gitee.com/dromara/x-easypdf):一个用搭积木的方式构建 PDF 的框架(基于 pdfbox/fop),支持 PDF 导出和编辑,适合简单的 PDF 文档生成场景。
+- [iText](https://github.com/itext/itext7):一个用于创建、编辑和增强 PDF 文档的 Java 库。iText 7 社区版采用 AGPL 许可证,如果你的项目是闭源商业项目,需要购买商业许可证。 iText 5 仍然是 LGPL 许可,可以免费用于商业用途,但已经停止维护。
+- [OpenPDF](https://github.com/LibrePDF/OpenPDF):完全开源免费 (LGPL/MPL 双重许可),基于 iText 的一个分支,可以作为 iText 的替代品,简单易用,但功能相比于 iText 更少一些(对于大多数场景已经足够)。
+- [Apache PDFBox](https://github.com/apache/pdfbox) :完全开源免费 (Apache 许可证),功能强大,支持 PDF 的创建、解析、转换和提取文本等。不过,由于其功能过于丰富,因此 API 设计相对复杂,学习难度会大一些。
- [FOP](https://xmlgraphics.apache.org/fop/) : Apache FOP 用于将 XSL-FO(Extensible Stylesheet Language Formatting Objects)格式化对象转换为多种输出格式,最常见的是 PDF。
## 图片处理
@@ -63,7 +67,7 @@ icon: codelibrary-fill
## 在线支付
-- [jeepay](https://gitee.com/jeequan/jeepay):一套适合互联网企业使用的开源支付系统,已实现交易、退款、转账、分账等接口,支持服务商特约商户和普通商户接口。已对接微信,支付宝,云闪付官方接口,支持聚合码支付。
+- [Jeepay](https://gitee.com/jeequan/jeepay):一套适合互联网企业使用的开源支付系统,已实现交易、退款、转账、分账等接口,支持服务商特约商户和普通商户接口。已对接微信,支付宝,云闪付官方接口,支持聚合码支付。
- [YunGouOS-PAY-SDK](https://gitee.com/YunGouOS/YunGouOS-PAY-SDK):YunGouOS 微信支付接口、微信官方个人支付接口、非二维码收款,非第四方清算。个人用户可提交资料开通微信支付商户,完成对接。
- [IJPay](https://gitee.com/javen205/IJPay):聚合支付,IJPay 让支付触手可及,封装了微信支付、QQ 支付、支付宝支付、京东支付、银联支付、PayPal 支付等常用的支付方式以及各种常用的接口。
diff --git a/docs/open-source-project/tools.md b/docs/open-source-project/tools.md
index 56c6185bfcc..5ddc4de759a 100644
--- a/docs/open-source-project/tools.md
+++ b/docs/open-source-project/tools.md
@@ -1,5 +1,6 @@
---
title: Java 优质开源开发工具
+description: Java优质开源开发工具推荐,涵盖代码质量检查、项目构建、测试框架、容器化部署等开发必备工具精选。
category: 开源项目
icon: tool
---
diff --git a/docs/open-source-project/tutorial.md b/docs/open-source-project/tutorial.md
index 0cab3269eab..0ab347d95c0 100644
--- a/docs/open-source-project/tutorial.md
+++ b/docs/open-source-project/tutorial.md
@@ -1,5 +1,6 @@
---
title: Java 优质开源技术教程
+description: Java优质开源技术教程推荐,涵盖Java核心知识、计算机基础、算法、系统设计等领域的高质量学习资源汇总。
category: 开源项目
icon: "book"
---
diff --git a/docs/readme.md b/docs/readme.md
deleted file mode 100644
index ec8e0b87e00..00000000000
--- a/docs/readme.md
+++ /dev/null
@@ -1,40 +0,0 @@
----
-home: true
-icon: home
-title: Java 面试指南
-heroImage: /logo.svg
-heroText: JavaGuide
-tagline: 「Java学习 + 面试指南」涵盖 Java 程序员需要掌握的核心知识
-actions:
- - text: 开始阅读
- link: /home.md
- type: primary
- - text: 知识星球
- link: /about-the-author/zhishixingqiu-two-years.md
- type: default
-footer: |-
- 鄂ICP备2020015769号-1 | 主题: VuePress Theme Hope
----
-
-## 关于网站
-
-JavaGuide 已经持续维护 5 年多了,累计提交了 **5000+** commit ,共有 **440** 多位朋友参与维护。真心希望能够把这个项目做好,真正能够帮助到有需要的朋友!
-
-如果觉得 JavaGuide 的内容对你有帮助的话,还请点个免费的 Star(绝不强制点 Star,觉得内容不错有收货再点赞就好),这是对我最大的鼓励,感谢各位一路同行,共勉!传送门:[GitHub](https://github.com/Snailclimb/JavaGuide) | [Gitee](https://gitee.com/SnailClimb/JavaGuide)。
-
-- [项目介绍](./javaguide/intro.md)
-- [贡献指南](./javaguide/contribution-guideline.md)
-- [常见问题](./javaguide/faq.md)
-
-## 关于作者
-
-- [我曾经也是网瘾少年](./about-the-author/internet-addiction-teenager.md)
-- [害,毕业三年了!](./about-the-author/my-college-life.md)
-- [我的知识星球快 3 岁了!](./about-the-author/zhishixingqiu-two-years.md)
-- [坚持写技术博客六年了](./about-the-author/writing-technology-blog-six-years.md)
-
-## 公众号
-
-最新更新会第一时间同步在公众号,推荐关注!另外,公众号上有很多干货不会同步在线阅读网站。
-
-
diff --git a/docs/snippets/article-footer.snippet.md b/docs/snippets/article-footer.snippet.md
index 5ec368caefb..973986b80f2 100644
--- a/docs/snippets/article-footer.snippet.md
+++ b/docs/snippets/article-footer.snippet.md
@@ -1 +1,9 @@
-
+## 写在最后
+
+感谢你能看到这里,也希望这篇文章对你有点用。
+
+JavaGuide 坚持更新 6 年多,近 6000 次提交、600+ 位贡献者一起打磨。如果这些内容对你有帮助,非常欢迎点个免费的 Star 支持下(完全自愿,觉得有收获再点就好):[GitHub](https://github.com/Snailclimb/JavaGuide) | [Gitee](https://gitee.com/SnailClimb/JavaGuide)。
+
+如果你想要付费支持/面试辅导(比如实战项目、简历优化、一对一提问、高频考点突击资料等)的话,欢迎了解我的[知识星球](https://javaguide.cn/about-the-author/zhishixingqiu-two-years.html)。已经坚持维护六年,内容持续更新,虽白菜价(0.4元/天)但质量很高,主打一个良心!
+
+
diff --git a/docs/snippets/article-header.snippet.md b/docs/snippets/article-header.snippet.md
index 1bac94f1cb5..80097335d7d 100644
--- a/docs/snippets/article-header.snippet.md
+++ b/docs/snippets/article-header.snippet.md
@@ -1 +1,5 @@
-[](../about-the-author/zhishixingqiu-two-years.md)
+::: tip 实战项目推荐
+
+[基于 Spring Boot 4.0 + Java 21 + Spring AI 2.0 开发的 AI 智能面试辅助平台 + RAG 知识库已开源,附带系统学习教程!非常适合作为学习和简历项目,学习门槛低,帮助提升求职竞争力,是主打就业的实战项目。](https://javaguide.cn/zhuanlan/interview-guide.html)
+
+:::
diff --git a/docs/snippets/planet.snippet.md b/docs/snippets/planet.snippet.md
index b9f08320cde..d8f2ff7e6e8 100644
--- a/docs/snippets/planet.snippet.md
+++ b/docs/snippets/planet.snippet.md
@@ -1,24 +1,37 @@
+这本[《Java 面试指北》](../zhuanlan/java-mian-shi-zhi-bei.md)(后端面试通用)的内容经过反复打磨,质量极高,旨在帮助每一位 Java/后端求职者从容应对面试挑战。
+
+**用数据说话:** 截至目前,专栏累计阅读量已突破 **477.1W**,收获点赞 **5,118** 个,评论互动 **1,657** 条。值得一提的是,评论区不仅仅是留言板,更是答疑区——几乎每一条提问,我都会用心回复,确保无疑问遗留。
+
+
+
[《Java 面试指北》](../zhuanlan/java-mian-shi-zhi-bei.md)(点击链接即可查看详细介绍)的部分内容展示如下,你可以将其看作是 [JavaGuide](https://javaguide.cn/#/) 的补充完善,两者可以配合使用。
-
+
-为了帮助更多同学准备 Java 面试以及学习 Java ,我创建了一个纯粹的[Java 面试知识星球](../about-the-author/zhishixingqiu-two-years.md)。虽然收费只有培训班/训练营的百分之一,但是知识星球里的内容质量更高,提供的服务也更全面,非常适合准备 Java 面试和学习 Java 的同学。
+下面是[《Java 面试指北》](../zhuanlan/java-mian-shi-zhi-bei.md)收到的部分球友的真实反馈:
-**欢迎准备 Java 面试以及学习 Java 的同学加入我的 [知识星球](../about-the-author/zhishixingqiu-two-years.md),干货非常多,学习氛围也很不错!收费虽然是白菜价,但星球里的内容或许比你参加上万的培训班质量还要高。**
+
+
+如果需要面试辅导(比如简历优化、一对一模拟问答、高频考点突击资料等),欢迎了解我的[知识星球](../about-the-author/zhishixingqiu-two-years.md)。已经坚持维护六年,内容持续更新,虽然是白菜价(0.4 元/天),但质量很高、服务也很全面,主打一个良心!
下面是星球提供的部分服务(点击下方图片即可获取知识星球的详细介绍):
[](../about-the-author/zhishixingqiu-two-years.md)
-**我有自己的原则,不割韭菜,用心做内容,真心希望帮助到你!**
+下面是今年收到了部分好评,每一条都是真实存在的。我看到很多培训班或者机构通过虚构一些不存在的好评来欺骗他人购买高价服务(行业内非常常见),真的很难理解。
+
+
-如果你感兴趣的话,不妨花 3 分钟左右看看星球的详细介绍:[JavaGuide 知识星球详细介绍](../about-the-author/zhishixingqiu-two-years.md) 。
+**我有自己的原则,不割韭菜,用心做内容,真心希望帮助到你!** 如果你感兴趣的话,不妨花 3 分钟左右看看星球的详细介绍:[JavaGuide 知识星球详细介绍](../about-the-author/zhishixingqiu-two-years.md) 。
这里再送一张 **30** 元的星球专属优惠券,数量有限(价格即将上调。老用户续费半价 ,微信扫码即可续费)!

-进入星球之后,记得查看 **[星球使用指南](https://t.zsxq.com/0d18KSarv)** (一定要看!!!) 和 **[星球优质主题汇总](https://t.zsxq.com/12uSKgTIm)** ,干货多多!
+🚀 **入圈必做**(干货满满,一定要看!):
+
+1. [星球使用指南](https://t.zsxq.com/0d18KSarv)
+2. [优质主题汇总](https://t.zsxq.com/12uSKgTIm)
**无任何套路,无任何潜在收费项。用心做内容,不割韭菜!**
diff --git a/docs/snippets/planet2.snippet.md b/docs/snippets/planet2.snippet.md
index 891d58c8923..aeeef4aee8c 100644
--- a/docs/snippets/planet2.snippet.md
+++ b/docs/snippets/planet2.snippet.md
@@ -10,9 +10,11 @@
[](../about-the-author/zhishixingqiu-two-years.md)
-**我有自己的原则,不割韭菜,用心做内容,真心希望帮助到你!**
+下面是今年收到了部分好评,每一条都是真实存在的。我看到很多培训班或者机构通过虚构一些不存在的好评来欺骗他人购买高价服务(行业内非常常见),真的很难理解。
-如果你感兴趣的话,不妨花 3 分钟左右看看星球的详细介绍:[JavaGuide 知识星球详细介绍](../about-the-author/zhishixingqiu-two-years.md)。
+
+
+**我有自己的原则,不割韭菜,用心做内容,真心希望帮助到你!** 如果你感兴趣的话,不妨花 3 分钟左右看看星球的详细介绍:[JavaGuide 知识星球详细介绍](../about-the-author/zhishixingqiu-two-years.md) 。
## 星球限时优惠
@@ -20,7 +22,10 @@

-进入星球之后,记得查看 **[星球使用指南](https://t.zsxq.com/0d18KSarv)** (一定要看!!!) 和 **[星球优质主题汇总](https://www.yuque.com/snailclimb/rpkqw1/ncxpnfmlng08wlf1)** 。
+🚀 **入圈必做**(干货满满,一定要看!):
+
+1. [星球使用指南](https://t.zsxq.com/0d18KSarv)
+2. [优质主题汇总](https://t.zsxq.com/12uSKgTIm)
**无任何套路,无任何潜在收费项。用心做内容,不割韭菜!**
diff --git a/docs/snippets/small-advertisement.snippet.md b/docs/snippets/small-advertisement.snippet.md
index 03b14a8738c..1bac94f1cb5 100644
--- a/docs/snippets/small-advertisement.snippet.md
+++ b/docs/snippets/small-advertisement.snippet.md
@@ -1,6 +1 @@
-::: tip 这是一则或许对你有用的小广告
-
-- **面试专版**:准备 Java 面试的小伙伴可以考虑面试专版:**[《Java 面试指北 》](../zhuanlan/java-mian-shi-zhi-bei.md)** (质量非常高,专为面试打造,配合 JavaGuide 食用效果最佳)。
-- **知识星球**:技术专栏/一对一提问/简历修改/求职指南/面试打卡/不定时福利,欢迎加入 **[JavaGuide 官方知识星球](../about-the-author/zhishixingqiu-two-years.md)**。
-
-:::
+[](../about-the-author/zhishixingqiu-two-years.md)
diff --git "a/docs/system-design/J2EE\345\237\272\347\241\200\347\237\245\350\257\206.md" "b/docs/system-design/J2EE\345\237\272\347\241\200\347\237\245\350\257\206.md"
index 5e8e735af3a..c14e73c4f32 100644
--- "a/docs/system-design/J2EE\345\237\272\347\241\200\347\237\245\350\257\206.md"
+++ "b/docs/system-design/J2EE\345\237\272\347\241\200\347\237\245\350\257\206.md"
@@ -1,3 +1,13 @@
+---
+title: J2EE 基础知识
+description: J2EE基础知识详解,涵盖Servlet生命周期、请求转发与重定向、Session与Cookie机制等Java Web核心概念。
+category: 系统设计
+head:
+ - - meta
+ - name: keywords
+ content: J2EE,Java Web,Servlet,JSP,HTTP请求响应,Servlet生命周期,Session,Cookie
+---
+
# Servlet 总结
在 Java Web 程序中,**Servlet**主要负责接收用户请求 `HttpServletRequest`,在`doGet()`,`doPost()`中做相应的处理,并将回应`HttpServletResponse`反馈给用户。**Servlet** 可以设置初始化参数,供 Servlet 内部使用。一个 Servlet 类只会有一个实例,在它初始化时调用`init()`方法,销毁时调用`destroy()`方法**。**Servlet 需要在 web.xml 中配置(MyEclipse 中创建 Servlet 会自动配置),**一个 Servlet 可以设置多个 URL 访问**。**Servlet 不是线程安全**,因此要谨慎使用类变量。
diff --git a/docs/system-design/basis/RESTfulAPI.md b/docs/system-design/basis/RESTfulAPI.md
index 15671201961..eb0018d4d31 100644
--- a/docs/system-design/basis/RESTfulAPI.md
+++ b/docs/system-design/basis/RESTfulAPI.md
@@ -1,23 +1,24 @@
---
title: RestFul API 简明教程
+description: RESTful API设计规范详解,涵盖REST架构原则、资源路径设计、HTTP方法使用及状态码规范等内容。
category: 代码质量
+head:
+ - - meta
+ - name: keywords
+ content: RESTful API,REST,API设计,资源路径,HTTP方法,状态码,幂等性,接口规范
---
-
-
这篇文章简单聊聊后端程序员必备的 RESTful API 相关的知识。
开始正式介绍 RESTful API 之前,我们需要首先搞清:**API 到底是什么?**
## 何为 API?
-
-
**API(Application Programming Interface)** 翻译过来是应用程序编程接口的意思。
我们在进行后端开发的时候,主要的工作就是为前端或者其他后端服务提供 API 比如查询用户数据的 API 。
-
+
但是, API 不仅仅代表后端系统暴露的接口,像框架中提供的方法也属于 API 的范畴。
@@ -66,7 +67,7 @@ POST /classes:新建一个班级
## RESTful API 规范
-
+
### 动作
diff --git a/docs/system-design/basis/naming.md b/docs/system-design/basis/naming.md
index 4be3d038848..799b6bbfd03 100644
--- a/docs/system-design/basis/naming.md
+++ b/docs/system-design/basis/naming.md
@@ -1,6 +1,11 @@
---
title: 代码命名指南
+description: 代码命名规范指南,涵盖变量、方法、类的命名原则与技巧,提升代码可读性和可维护性。
category: 代码质量
+head:
+ - - meta
+ - name: keywords
+ content: 代码命名,命名规范,变量命名,函数命名,类命名,可读性,代码质量,Code Review
---
我还记得我刚工作那一段时间, 项目 Code Review 的时候,我经常因为变量命名不规范而被 “diss”!
diff --git a/docs/system-design/basis/refactoring.md b/docs/system-design/basis/refactoring.md
index eeecb0c239b..11c3a94285f 100644
--- a/docs/system-design/basis/refactoring.md
+++ b/docs/system-design/basis/refactoring.md
@@ -1,6 +1,11 @@
---
title: 代码重构指南
+description: 代码重构实践指南,涵盖重构定义、重构原则、代码坏味道识别及常用重构技巧与最佳实践。
category: 代码质量
+head:
+ - - meta
+ - name: keywords
+ content: 代码重构,重构技巧,重构原则,设计模式,SOLID,代码坏味道,可维护性,单元测试
---
前段时间重读了[《重构:改善代码既有设计》](https://book.douban.com/subject/30468597/),收货颇多。于是,简单写了一篇文章来聊聊我对重构的看法。
@@ -54,6 +59,12 @@ category: 代码质量
> 重构的唯一目的就是让我们开发更快,用更少的工作量创造更大的价值。
+## 性能优化就是重构吗?
+
+重构的目的是提高代码的可读性、可维护性和灵活性,它关注的是代码的内部结构——如何让开发者更容易理解代码,如何让后续的功能开发和维护更加高效。而性能优化则是为了让代码运行得更快、占用更少的资源,它关注的是程序的外部表现——如何减少响应时间、降低资源消耗、提升系统吞吐量。这两者看似对立,但实际上它们的目标是统一的,都是为了提高软件的整体质量。
+
+在实际开发中,理想的做法是首先**确保代码的可读性和可维护性**,然后根据实际需求选择合适的性能优化手段。优秀的软件设计不是一味追求性能最大化,而是要在可维护性和性能之间找到平衡。通过这种方式,我们可以打造既**易于管理**又具有**良好性能**的软件系统。
+
## 何时进行重构?
重构在是开发过程中随时可以进行的,见机行事即可,并不需要单独分配一两天的时间专门用来重构。
@@ -132,6 +143,7 @@ Code Review 可以非常有效提高代码的整体质量,它会帮助我们
除了可以在重构项目代码的过程中练习精进重构之外,你还可以有下面这些手段:
+- [当我重构时,我在想些什么](https://mp.weixin.qq.com/s/pFaFKMXzNCOuW2SD9Co40g):转转技术的这篇文章总结了常见的重构场景和重构方式。
- [重构实战练习](https://linesh.gitbook.io/refactoring/):通过几个小案例一步一步带你学习重构!
- [设计模式+重构学习网站](https://refactoringguru.cn/):免费在线学习代码重构、 设计模式、 SOLID 原则 (单一职责、 开闭原则、 里氏替换、 接口隔离以及依赖反转) 。
- [IDEA 官方文档的代码重构教程](https://www.jetbrains.com/help/idea/refactoring-source-code.html#popular-refactorings):教你如何使用 IDEA 进行重构。
diff --git a/docs/system-design/basis/software-engineering.md b/docs/system-design/basis/software-engineering.md
index c6cd4fa3188..5b95e03e72b 100644
--- a/docs/system-design/basis/software-engineering.md
+++ b/docs/system-design/basis/software-engineering.md
@@ -1,6 +1,11 @@
---
title: 软件工程简明教程
+description: 软件工程基础知识详解,涵盖软件危机、软件开发过程模型、瀑布模型、敏捷开发等软件工程核心概念。
category: 系统设计
+head:
+ - - meta
+ - name: keywords
+ content: 软件工程,软件危机,软件开发过程,瀑布模型,敏捷开发,需求分析,软件生命周期,工程化方法
---
大部分软件开发从业者,都会忽略软件开发中的一些最基础、最底层的一些概念。但是,这些软件开发的概念对于软件开发来说非常重要,就像是软件开发的基石一样。这也是我写这篇文章的原因。
@@ -15,7 +20,7 @@ category: 系统设计
简单来说,软件危机描述了当时软件开发的一个痛点:我们很难高效地开发出质量高的软件。
-Dijkstra(Dijkstra 算法的作者) 在 1972 年图灵奖获奖感言中也提高过软件危机,他是这样说的:“导致软件危机的主要原因是机器变得功能强大了几个数量级!坦率地说:只要没有机器,编程就完全没有问题。当我们有一些弱小的计算机时,编程成为一个温和的问题,而现在我们有了庞大的计算机,编程也同样成为一个巨大的问题”。
+Dijkstra(Dijkstra 算法的作者) 在 1972 年图灵奖获奖感言中也提到过软件危机,他是这样说的:“导致软件危机的主要原因是机器变得功能强大了几个数量级!坦率地说:只要没有机器,编程就完全没有问题。当我们有一些弱小的计算机时,编程成为一个温和的问题,而现在我们有了庞大的计算机,编程也同样成为一个巨大的问题”。
**说了这么多,到底什么是软件工程呢?**
@@ -38,7 +43,7 @@ Dijkstra(Dijkstra 算法的作者) 在 1972 年图灵奖获奖感言中也
- 交付:将做好的软件交付给客户。
- 维护:对软件进行维护比如解决 bug,完善功能。
-软件开发过程只是比较笼统的层面上,一定义了一个软件开发可能涉及到的一些流程。
+软件开发过程只是比较笼统的层面上,定义了一个软件开发可能涉及到的一些流程。
软件开发模型更具体地定义了软件开发过程,对开发过程提供了强有力的理论支持。
@@ -46,7 +51,7 @@ Dijkstra(Dijkstra 算法的作者) 在 1972 年图灵奖获奖感言中也
软件开发模型有很多种,比如瀑布模型(Waterfall Model)、快速原型模型(Rapid Prototype Model)、V 模型(V-model)、W 模型(W-model)、敏捷开发模型。其中最具有代表性的还是 **瀑布模型** 和 **敏捷开发** 。
-**瀑布模型** 定义了一套完成的软件开发周期,完整地展示了一个软件的的生命周期。
+**瀑布模型** 定义了一套完整的软件开发周期,完整地展示了一个软件的生命周期。

diff --git a/docs/system-design/basis/unit-test.md b/docs/system-design/basis/unit-test.md
index 3331eb2791c..697e5227b3e 100644
--- a/docs/system-design/basis/unit-test.md
+++ b/docs/system-design/basis/unit-test.md
@@ -1,6 +1,11 @@
---
title: 单元测试到底是什么?应该怎么做?
+description: 单元测试入门指南,涵盖单元测试概念、Mock与Stub技术、测试金字塔及JUnit测试框架使用方法。
category: 代码质量
+head:
+ - - meta
+ - name: keywords
+ content: 单元测试,Unit Testing,Mock,Stub,Fake,测试金字塔,可测试性,TDD,JUnit
---
> 本文重构完善自[谈谈为什么写单元测试 - 键盘男 - 2016](https://www.jianshu.com/p/fa41fb80d2b8)这篇文章。
diff --git a/docs/system-design/design-pattern.md b/docs/system-design/design-pattern.md
index 2b9541f8678..2b537f37654 100644
--- a/docs/system-design/design-pattern.md
+++ b/docs/system-design/design-pattern.md
@@ -1,14 +1,12 @@
---
title: 设计模式常见面试题总结
+description: 设计模式(Design pattern)代表了最佳的实践,通常被有经验的面向对象 的软件开发人员所采用。设计模式是软件开发人员在软件开发过程中面临 的一般问题的解决方案。这些解决方案是众多软件开发人员经过相当⻓的 一段时间的试验和错误总结出来的。
category: 系统设计
icon: "Tools"
head:
- - meta
- name: keywords
- content: 设计模式,单例模式,责任链模式,适配器模式,工厂模式,代理模式
- - - meta
- - name: description
- content: 设计模式(Design pattern)代表了最佳的实践,通常被有经验的面向对象 的软件开发人员所采用。设计模式是软件开发人员在软件开发过程中面临 的一般问题的解决方案。这些解决方案是众多软件开发人员经过相当⻓的 一段时间的试验和错误总结出来的。
+ content: 设计模式,单例模式,工厂模式,代理模式,责任链模式,策略模式,观察者模式,面试题
---
**设计模式** 相关的面试题已经整理到了 PDF 手册中,你可以在我的公众号“**JavaGuide**”后台回复“**PDF**” 获取。
diff --git a/docs/system-design/framework/mybatis/mybatis-interview.md b/docs/system-design/framework/mybatis/mybatis-interview.md
index 988111d777a..394dff6b037 100644
--- a/docs/system-design/framework/mybatis/mybatis-interview.md
+++ b/docs/system-design/framework/mybatis/mybatis-interview.md
@@ -1,5 +1,6 @@
---
title: MyBatis常见面试题总结
+description: MyBatis常见面试题详解,涵盖#{}与${}区别、动态SQL、一级二级缓存、分页插件及Mapper映射原理。
category: 框架
icon: "database"
tag:
@@ -7,10 +8,7 @@ tag:
head:
- - meta
- name: keywords
- content: MyBatis
- - - meta
- - name: description
- content: 几道常见的 MyBatis 常见
+ content: MyBatis,MyBatis面试题,#{}与${},动态SQL,一级缓存,二级缓存,分页插件,Mapper映射
---
@@ -82,7 +80,7 @@ public interface StuMapper {
能正常运行,并能得到相应的结果,这样就实现了在 Dao 接口中写重载方法。
-**Mybatis 的 Dao 接口可以有多个重载方法,但是多个接口对应的映射必须只有一个,否则启动会报错。**
+**Mybatis 的 Dao 接口可以有多个重载方法,但是多个方法对应的映射必须只有一个,否则启动会报错。**
相关 issue:[更正:Dao 接口里的方法可以重载,但是 Mybatis 的 xml 里面的 ID 不允许重复!](https://github.com/Snailclimb/JavaGuide/issues/1122)。
diff --git a/docs/system-design/framework/netty.md b/docs/system-design/framework/netty.md
index 1a0833f86e1..98a8315dd58 100644
--- a/docs/system-design/framework/netty.md
+++ b/docs/system-design/framework/netty.md
@@ -1,11 +1,16 @@
---
title: Netty常见面试题总结(付费)
+description: Netty高性能网络编程框架面试题详解,涵盖Reactor模型、事件循环、零拷贝、ChannelPipeline等核心原理。
category: 框架
icon: "network"
+head:
+ - - meta
+ - name: keywords
+ content: Netty,Netty面试题,网络编程,Reactor模型,事件循环,ChannelPipeline,零拷贝,高性能IO
---
**Netty** 相关的面试题为我的[知识星球](https://javaguide.cn/about-the-author/zhishixingqiu-two-years.html)(点击链接即可查看详细介绍以及加入方法)专属内容,已经整理到了[《Java 面试指北》](https://javaguide.cn/zhuanlan/java-mian-shi-zhi-bei.html)中。
-
+
-
+
diff --git a/docs/system-design/framework/spring/Async.md b/docs/system-design/framework/spring/Async.md
new file mode 100644
index 00000000000..79a78bbf8d9
--- /dev/null
+++ b/docs/system-design/framework/spring/Async.md
@@ -0,0 +1,726 @@
+---
+title: Async 注解原理分析
+description: Spring @Async异步注解原理详解,涵盖异步任务配置、线程池设置、@EnableAsync机制及常见使用问题。
+category: 框架
+tag:
+ - Spring
+head:
+ - - meta
+ - name: keywords
+ content: Spring异步,@Async,EnableAsync,线程池,TaskExecutor,异步任务,Spring注解,方法异步
+---
+
+`@Async` 注解由 Spring 框架提供,被该注解标注的类或方法会在 **异步线程** 中执行。这意味着当方法被调用时,调用者将不会等待该方法执行完成,而是可以继续执行后续的代码。
+
+`@Async` 注解的使用非常简单,需要两个步骤:
+
+1. 在启动类上添加注解 `@EnableAsync` ,开启异步任务。
+2. 在需要异步执行的方法或类上添加注解 `@Async` 。
+
+```java
+@SpringBootApplication
+// 开启异步任务
+@EnableAsync
+public class YourApplication {
+
+ public static void main(String[] args) {
+ SpringApplication.run(YourApplication.class, args);
+ }
+}
+
+// 异步服务类
+@Service
+public class MyService {
+
+ // 推荐使用自定义线程池,这里只是演示基本用法
+ @Async
+ public CompletableFuture doSomethingAsync() {
+
+ // 这里会有一些业务耗时操作
+ // ...
+ // 使用 CompletableFuture 可以更方便地处理异步任务的结果,避免阻塞主线程
+ return CompletableFuture.completedFuture("Async Task Completed");
+ }
+
+}
+```
+
+接下来,我们一起来看看 `@Async` 的底层原理。
+
+## @Async 原理分析
+
+`@Async` 可以异步执行任务,本质上是使用 **动态代理** 来实现的。通过 Spring 中的后置处理器 `BeanPostProcessor` 为使用 `@Async` 注解的类创建动态代理,之后 `@Async` 注解方法的调用会被动态代理拦截,在拦截器中将方法的执行封装为异步任务提交给线程池处理。
+
+接下来,我们来详细分析一下。
+
+### 开启异步
+
+使用 `@Async` 之前,需要在启动类上添加 `@EnableAsync` 来开启异步,`@EnableAsync` 注解如下:
+
+```JAVA
+// 省略其他注解 ...
+@Import(AsyncConfigurationSelector.class)
+public @interface EnableAsync { /* ... */ }
+```
+
+在 `@EnableAsync` 注解上通过 `@Import` 注解引入了 `AsyncConfigurationSelector` ,因此 Spring 会去加载通过 `@Import` 注解引入的类。
+
+`AsyncConfigurationSelector` 类实现了 `ImportSelector` 接口,因此在该类中会重写 `selectImports()` 方法来自定义加载 Bean 的逻辑,如下:
+
+```JAVA
+public class AsyncConfigurationSelector extends AdviceModeImportSelector {
+ @Override
+ @Nullable
+ public String[] selectImports(AdviceMode adviceMode) {
+ switch (adviceMode) {
+ // 基于 JDK 代理织入的通知
+ case PROXY:
+ return new String[] {ProxyAsyncConfiguration.class.getName()};
+ // 基于 AspectJ 织入的通知
+ case ASPECTJ:
+ return new String[] {ASYNC_EXECUTION_ASPECT_CONFIGURATION_CLASS_NAME};
+ default:
+ return null;
+ }
+ }
+}
+```
+
+在 `selectImports()` 方法中,会根据通知的不同类型来选择加载不同的类,其中 `adviceMode` 默认值为 `PROXY` 。
+
+这里以基于 JDK 代理的通知为例,此时会加载 `ProxyAsyncConfiguration` 类,如下:
+
+```JAVA
+@Configuration
+@Role(BeanDefinition.ROLE_INFRASTRUCTURE)
+public class ProxyAsyncConfiguration extends AbstractAsyncConfiguration {
+ @Bean(name = TaskManagementConfigUtils.ASYNC_ANNOTATION_PROCESSOR_BEAN_NAME)
+ @Role(BeanDefinition.ROLE_INFRASTRUCTURE)
+ public AsyncAnnotationBeanPostProcessor asyncAdvisor() {
+ // ...
+ // 加载后置处理器
+ AsyncAnnotationBeanPostProcessor bpp = new AsyncAnnotationBeanPostProcessor();
+
+ // ...
+ return bpp;
+ }
+}
+```
+
+### 后置处理器
+
+在 `ProxyAsyncConfiguration` 类中,会通过 `@Bean` 注解加载一个后置处理器 `AsyncAnnotationBeanPostProcessor` ,这个后置处理器是使 `@Async` 注解起作用的关键。
+
+如果某一个类或者方法上使用了 `@Async` 注解,`AsyncAnnotationBeanPostProcessor` 处理器就会为该类创建一个动态代理。
+
+该类的方法在执行时,会被代理对象的拦截器所拦截,其中被 `@Async` 注解标记的方法会异步执行。
+
+`AsyncAnnotationBeanPostProcessor` 代码如下:
+
+```JAVA
+public class AsyncAnnotationBeanPostProcessor extends AbstractBeanFactoryAwareAdvisingPostProcessor {
+ @Override
+ public void setBeanFactory(BeanFactory beanFactory) {
+ super.setBeanFactory(beanFactory);
+ // 创建 AsyncAnnotationAdvisor,它是一个 Advisor
+ // 用于拦截带有 @Async 注解的方法并将这些方法异步执行。
+ AsyncAnnotationAdvisor advisor = new AsyncAnnotationAdvisor(this.executor, this.exceptionHandler);
+ // 如果设置了自定义的 asyncAnnotationType,则将其设置到 advisor 中。
+ // asyncAnnotationType 用于指定自定义的异步注解,例如 @MyAsync。
+ if (this.asyncAnnotationType != null) {
+ advisor.setAsyncAnnotationType(this.asyncAnnotationType);
+ }
+ advisor.setBeanFactory(beanFactory);
+ this.advisor = advisor;
+ }
+}
+```
+
+`AsyncAnnotationBeanPostProcessor` 的父类实现了 `BeanFactoryAware` 接口,因此在该类中重写了 `setBeanFactory()` 方法作为扩展点,来加载 `AsyncAnnotationAdvisor` 。
+
+#### 创建 Advisor
+
+`Advisor` 是 `Spring AOP` 对 `Advice` 和 `Pointcut` 的抽象。`Advice` 为执行的通知逻辑,`Pointcut` 为通知执行的切入点。
+
+在后置处理器 `AsyncAnnotationBeanPostProcessor` 中会去创建 `AsyncAnnotationAdvisor` , 在它的构造方法中,会构建对应的 `Advice` 和 `Pointcut` ,如下:
+
+```JAVA
+public class AsyncAnnotationAdvisor extends AbstractPointcutAdvisor implements BeanFactoryAware {
+
+ private Advice advice; // 异步执行的 Advice
+ private Pointcut pointcut; // 匹配 @Async 注解方法的切点
+
+ // 构造函数
+ public AsyncAnnotationAdvisor(/* 参数省略 */) {
+ // 1. 创建 Advice,负责异步执行逻辑
+ this.advice = buildAdvice(executor, exceptionHandler);
+ // 2. 创建 Pointcut,选择要被增强的目标方法
+ this.pointcut = buildPointcut(asyncAnnotationTypes);
+ }
+
+ // 创建 Advice
+ protected Advice buildAdvice(/* 参数省略 */) {
+ // 创建处理异步执行的拦截器
+ AnnotationAsyncExecutionInterceptor interceptor = new AnnotationAsyncExecutionInterceptor(null);
+ // 使用执行器和异常处理器配置拦截器
+ interceptor.configure(executor, exceptionHandler);
+ return interceptor;
+ }
+
+ // 创建 Pointcut
+ protected Pointcut buildPointcut(Set> asyncAnnotationTypes) {
+ ComposablePointcut result = null;
+ for (Class extends Annotation> asyncAnnotationType : asyncAnnotationTypes) {
+ // 1. 类级别切点:如果类上有注解则匹配
+ Pointcut cpc = new AnnotationMatchingPointcut(asyncAnnotationType, true);
+ // 2. 方法级别切点:如果方法上有注解则匹配
+ Pointcut mpc = new AnnotationMatchingPointcut(null, asyncAnnotationType, true);
+
+ if (result == null) {
+ result = new ComposablePointcut(cpc);
+ } else {
+ // 使用 union 合并之前的切点
+ result.union(cpc);
+ }
+ // 将方法级别切点添加到组合切点
+ result = result.union(mpc);
+ }
+ // 返回组合切点,如果没有提供注解类型则返回 Pointcut.TRUE
+ return (result != null ? result : Pointcut.TRUE);
+ }
+}
+```
+
+`AsyncAnnotationAdvisor` 的核心在于构建 `Advice` 和 `Pointcut` :
+
+- 构建 `Advice` :会创建 `AnnotationAsyncExecutionInterceptor` 拦截器,在拦截器的 `invoke()` 方法中会执行通知的逻辑。
+- 构建 `Pointcut` :由 `ClassFilter` 和 `MethodMatcher` 组成,用于匹配哪些方法需要执行通知( `Advice` )的逻辑。
+
+#### 后置处理逻辑
+
+`AsyncAnnotationBeanPostProcessor` 后置处理器中实现的 `postProcessAfterInitialization()` 方法在其父类 `AbstractAdvisingBeanPostProcessor` 中,在 `Bean` 初始化之后,会进入到 `postProcessAfterInitialization()` 方法进行后置处理。
+
+在后置处理方法中,会判断 `Bean` 是否符合后置处理器中 `Advisor` 通知的条件,如果符合,则创建代理对象。如下:
+
+```JAVA
+// AbstractAdvisingBeanPostProcessor
+public Object postProcessAfterInitialization(Object bean, String beanName) {
+ if (this.advisor == null || bean instanceof AopInfrastructureBean) {
+ return bean;
+ }
+ if (bean instanceof Advised) {
+ Advised advised = (Advised) bean;
+ if (!advised.isFrozen() && isEligible(AopUtils.getTargetClass(bean))) {
+ if (this.beforeExistingAdvisors) {
+ advised.addAdvisor(0, this.advisor);
+ }
+ else {
+ advised.addAdvisor(this.advisor);
+ }
+ return bean;
+ }
+ }
+ // 判断给定的 Bean 是否符合后置处理器中 Advisor 通知的条件,符合的话,就创建代理对象。
+ if (isEligible(bean, beanName)) {
+ ProxyFactory proxyFactory = prepareProxyFactory(bean, beanName);
+ if (!proxyFactory.isProxyTargetClass()) {
+ evaluateProxyInterfaces(bean.getClass(), proxyFactory);
+ }
+ // 添加 Advisor。
+ proxyFactory.addAdvisor(this.advisor);
+ customizeProxyFactory(proxyFactory);
+ // 返回代理对象。
+ return proxyFactory.getProxy(getProxyClassLoader());
+ }
+ return bean;
+}
+```
+
+### @Async 注解方法的拦截
+
+`@Async` 注解方法的执行会在 `AnnotationAsyncExecutionInterceptor` 中被拦截,在 `invoke()` 方法中执行拦截器的逻辑。此时会将 `@Async` 注解标注的方法封装为异步任务,交给执行器来执行。
+
+`invoke()` 方法在 `AnnotationAsyncExecutionInterceptor` 的父类 `AsyncExecutionInterceptor` 中定义,如下:
+
+```JAVA
+public class AsyncExecutionInterceptor extends AsyncExecutionAspectSupport implements MethodInterceptor, Ordered {
+ @Override
+ @Nullable
+ public Object invoke(final MethodInvocation invocation) throws Throwable {
+ Class> targetClass = (invocation.getThis() != null ? AopUtils.getTargetClass(invocation.getThis()) : null);
+ Method specificMethod = ClassUtils.getMostSpecificMethod(invocation.getMethod(), targetClass);
+ final Method userDeclaredMethod = BridgeMethodResolver.findBridgedMethod(specificMethod);
+
+ // 1、确定异步任务执行器
+ AsyncTaskExecutor executor = determineAsyncExecutor(userDeclaredMethod);
+
+ // 2、将要执行的方法封装为 Callable 异步任务
+ Callable task = () -> {
+ try {
+ // 2.1、执行方法
+ Object result = invocation.proceed();
+ // 2.2、如果方法返回值是 Future 类型,阻塞等待结果
+ if (result instanceof Future) {
+ return ((Future>) result).get();
+ }
+ }
+ catch (ExecutionException ex) {
+ handleError(ex.getCause(), userDeclaredMethod, invocation.getArguments());
+ }
+ catch (Throwable ex) {
+ handleError(ex, userDeclaredMethod, invocation.getArguments());
+ }
+ return null;
+ };
+ // 3、提交任务
+ return doSubmit(task, executor, invocation.getMethod().getReturnType());
+ }
+}
+```
+
+在 `invoke()` 方法中,主要有 3 个步骤:
+
+1. 确定执行异步任务的执行器。
+2. 将 `@Async` 注解标注的方法封装为 `Callable` 异步任务。
+3. 将任务提交给执行器执行。
+
+#### 1、获取异步任务执行器
+
+在 `determineAsyncExecutor()` 方法中,会获取异步任务的执行器(即执行异步任务的 **线程池** )。代码如下:
+
+```JAVA
+// 确定异步任务的执行器
+protected AsyncTaskExecutor determineAsyncExecutor(Method method) {
+ // 1、先从缓存中获取。
+ AsyncTaskExecutor executor = this.executors.get(method);
+ if (executor == null) {
+ Executor targetExecutor;
+ // 2、获取执行器的限定符。
+ String qualifier = getExecutorQualifier(method);
+ if (StringUtils.hasLength(qualifier)) {
+ // 3、根据限定符获取对应的执行器。
+ targetExecutor = findQualifiedExecutor(this.beanFactory, qualifier);
+ }
+ else {
+ // 4、如果没有限定符,则使用默认的执行器。即 Spring 提供的默认线程池:SimpleAsyncTaskExecutor。
+ targetExecutor = this.defaultExecutor.get();
+ }
+ if (targetExecutor == null) {
+ return null;
+ }
+ // 5、将执行器包装为 TaskExecutorAdapter 适配器。
+ // TaskExecutorAdapter 是 Spring 对于 JDK 线程池做的一层抽象,还是继承自 JDK 的线程池 Executor。这里可以不用管太多,只要知道它是线程池就可以了。
+ executor = (targetExecutor instanceof AsyncListenableTaskExecutor ?
+ (AsyncListenableTaskExecutor) targetExecutor : new TaskExecutorAdapter(targetExecutor));
+ this.executors.put(method, executor);
+ }
+ return executor;
+}
+```
+
+在 `determineAsyncExecutor()` 方法中确定了异步任务的执行器(线程池),主要是通过 `@Async` 注解的 `value` 值来获取执行器的限定符,根据限定符再去 `BeanFactory` 中查找对应的执行器就可以了。
+
+如果在 `@Async` 注解中没有指定线程池,则会通过 `this.defaultExecutor.get()` 来获取默认的线程池,其中 `defaultExecutor` 在下边方法中进行赋值:
+
+```JAVA
+// AsyncExecutionInterceptor
+protected Executor getDefaultExecutor(@Nullable BeanFactory beanFactory) {
+ // 1、尝试从 beanFactory 中获取线程池。
+ Executor defaultExecutor = super.getDefaultExecutor(beanFactory);
+ // 2、如果 beanFactory 中没有,则创建 SimpleAsyncTaskExecutor 线程池。
+ return (defaultExecutor != null ? defaultExecutor : new SimpleAsyncTaskExecutor());
+}
+```
+
+其中 `super.getDefaultExecutor()` 会在 `beanFactory` 中尝试获取 `Executor` 类型的线程池。代码如下:
+
+```JAVA
+protected Executor getDefaultExecutor(@Nullable BeanFactory beanFactory) {
+ if (beanFactory != null) {
+ try {
+ // 1、从 beanFactory 中获取 TaskExecutor 类型的线程池。
+ return beanFactory.getBean(TaskExecutor.class);
+ }
+ catch (NoUniqueBeanDefinitionException ex) {
+ try {
+ // 2、如果有多个,则尝试从 beanFactory 中获取执行名称的 Executor 线程池。
+ return beanFactory.getBean(DEFAULT_TASK_EXECUTOR_BEAN_NAME, Executor.class);
+ }
+ catch (NoSuchBeanDefinitionException ex2) {
+ if (logger.isInfoEnabled()) {
+ // ...
+ }
+ }
+ }
+ catch (NoSuchBeanDefinitionException ex) {
+ try {
+ // 3、如果没有,则尝试从 beanFactory 中获取执行名称的 Executor 线程池。
+ return beanFactory.getBean(DEFAULT_TASK_EXECUTOR_BEAN_NAME, Executor.class);
+ }
+ catch (NoSuchBeanDefinitionException ex2) {
+ // ...
+ }
+ }
+ }
+ return null;
+}
+```
+
+在 `getDefaultExecutor()` 中,如果从 `beanFactory` 获取线程池失败的话,则会创建 `SimpleAsyncTaskExecutor` 线程池。
+
+该线程池的在每次执行异步任务时,都会创建一个新的线程去执行任务,并不会对线程进行复用,从而导致异步任务执行的开销很大。一旦在 `@Async` 注解标注的方法某一瞬间并发量剧增,应用就会大量创建线程,从而影响服务质量甚至出现服务不可用。
+
+同一时刻如果向 `SimpleAsyncTaskExecutor` 线程池提交 10000 个任务,那么该线程池就会创建 10000 个线程,其的 `execute()` 方法如下:
+
+```JAVA
+// SimpleAsyncTaskExecutor:execute() 内部会调用 doExecute()
+protected void doExecute(Runnable task) {
+ // 创建新线程
+ Thread thread = (this.threadFactory != null ? this.threadFactory.newThread(task) : createThread(task));
+ thread.start();
+}
+```
+
+**建议:在使用 `@Async` 时需要自己指定线程池,避免 Spring 默认线程池带来的风险。**
+
+在 `@Async` 注解中的 `value` 指定了线程池的限定符,根据限定符可以获取 **自定义的线程池** 。获取限定符的代码如下:
+
+```JAVA
+// AnnotationAsyncExecutionInterceptor
+protected String getExecutorQualifier(Method method) {
+ // 1.从方法上获取 Async 注解。
+ Async async = AnnotatedElementUtils.findMergedAnnotation(method, Async.class);
+ // 2. 如果方法上没有找到 @Async 注解,则尝试从方法所在的类上获取 @Async 注解。
+ if (async == null) {
+ async = AnnotatedElementUtils.findMergedAnnotation(method.getDeclaringClass(), Async.class);
+ }
+ // 3. 如果找到了 @Async 注解,则获取注解的 value 值并返回,作为线程池的限定符。
+ // 如果 "value" 属性值为空字符串,则使用默认的线程池。
+ // 如果没有找到 @Async 注解,则返回 null,同样使用默认的线程池。
+ return (async != null ? async.value() : null);
+}
+```
+
+#### 2、将方法封装为异步任务
+
+在 `invoke()` 方法获取执行器之后,会将方法封装为异步任务,代码如下:
+
+```JAVA
+// 将要执行的方法封装为 Callable 异步任务
+Callable task = () -> {
+ try {
+ // 2.1、执行被拦截的方法 (proceed() 方法是 AOP 中的核心方法,用于执行目标方法)
+ Object result = invocation.proceed();
+
+ // 2.2、如果被拦截方法的返回值是 Future 类型,则需要阻塞等待结果,
+ // 并将 Future 的结果作为异步任务的结果返回。 这是为了处理异步方法嵌套调用的情况。
+ // 例如,一个异步方法内部调用了另一个异步方法,则需要等待内部异步方法执行完成,
+ // 才能返回最终的结果。
+ if (result instanceof Future) {
+ return ((Future>) result).get(); // 阻塞等待 Future 的结果
+ }
+ }
+ catch (ExecutionException ex) {
+ // 2.3、处理 ExecutionException 异常。 ExecutionException 是 Future.get() 方法抛出的异常,
+ handleError(ex.getCause(), userDeclaredMethod, invocation.getArguments()); // 处理原始异常
+ }
+ catch (Throwable ex) {
+ // 2.4、处理其他类型的异常。 将异常、被拦截的方法和方法参数作为参数调用 handleError() 方法进行处理。
+ handleError(ex, userDeclaredMethod, invocation.getArguments());
+ }
+ // 2.5、如果方法返回值不是 Future 类型,或者发生异常,则返回 null。
+ return null;
+};
+```
+
+相比于 `Runnable` ,`Callable` 可以返回结果,并且抛出异常。
+
+将 `invocation.proceed()` 的执行(原方法的执行)封装为 `Callable` 异步任务。这里仅仅当 `result` (方法返回值)类型为 `Future` 才返回,如果是其他类型则直接返回 `null` 。
+
+因此使用 `@Async` 注解标注的方法如果使用 `Future` 类型之外的返回值,则无法获取方法的执行结果。
+
+#### 3、提交异步任务
+
+在 `AsyncExecutionInterceptor # invoke()` 中将要执行的方法封装为 Callable 任务之后,就会将任务交给执行器来执行。提交相关的代码如下:
+
+```JAVA
+protected Object doSubmit(Callable task, AsyncTaskExecutor executor, Class> returnType) {
+ // 根据方法的返回值类型,选择不同的异步执行方式并返回结果。
+ // 1. 如果方法返回值是 CompletableFuture 类型
+ if (CompletableFuture.class.isAssignableFrom(returnType)) {
+ // 使用 CompletableFuture.supplyAsync() 方法异步执行任务。
+ return CompletableFuture.supplyAsync(() -> {
+ try {
+ return task.call();
+ }
+ catch (Throwable ex) {
+ throw new CompletionException(ex); // 将异常包装为 CompletionException,以便在 future.get() 时抛出
+ }
+ }, executor);
+ }
+ // 2. 如果方法返回值是 ListenableFuture 类型
+ else if (ListenableFuture.class.isAssignableFrom(returnType)) {
+ // 将 AsyncTaskExecutor 强制转换为 AsyncListenableTaskExecutor,
+ // 并调用 submitListenable() 方法提交任务。
+ // AsyncListenableTaskExecutor 是 ListenableFuture 的专用异步执行器,
+ // 它可以返回一个 ListenableFuture 对象,允许添加回调函数来监听任务的完成。
+ return ((AsyncListenableTaskExecutor) executor).submitListenable(task);
+ }
+ // 3. 如果方法返回值是 Future 类型
+ else if (Future.class.isAssignableFrom(returnType)) {
+ // 直接调用 AsyncTaskExecutor 的 submit() 方法提交任务,并返回一个 Future 对象。
+ return executor.submit(task);
+ }
+ // 4. 如果方法返回值是 void 或其他类型
+ else {
+ // 直接调用 AsyncTaskExecutor 的 submit() 方法提交任务。
+ // 由于方法返回值是 void,因此不需要返回任何结果,直接返回 null。
+ executor.submit(task);
+ return null;
+ }
+}
+```
+
+在 `doSubmit()` 方法中,会根据 `@Async` 注解标注方法的返回值不同,来选择不同的任务提交方式,最后任务会由执行器(线程池)执行。
+
+### 总结
+
+
+
+理解 `@Async` 原理的核心在于理解 `@EnableAsync` 注解,该注解开启了异步任务的功能。
+
+主要流程如上图,会通过后置处理器来创建代理对象,之后代理对象中 `@Async` 方法的执行会走到 `Advice` 内部的拦截器中,之后将方法封装为异步任务,并提交线程池进行处理。
+
+## @Async 使用建议
+
+### 自定义线程池
+
+如果没有显式地配置线程池,在 `@Async` 底层会先在 `BeanFactory` 中尝试获取线程池,如果获取不到,则会创建一个 `SimpleAsyncTaskExecutor` 实现。`SimpleAsyncTaskExecutor` 本质上不算是一个真正的线程池,因为它对于每个请求都会启动一个新线程而不重用现有线程,这会带来一些潜在的问题,例如资源消耗过大。
+
+具体线程池获取可以参考这篇文章:[浅析 Spring 中 Async 注解底层异步线程池原理|得物技术](https://mp.weixin.qq.com/s/FySv5L0bCdrlb5MoSfQtAA)。
+
+一定要显式配置一个线程池,推荐`ThreadPoolTaskExecutor`。并且,还可以根据任务的性质和需求,为不同的异步方法指定不同的线程池。
+
+```java
+@Configuration
+@EnableAsync
+public class AsyncConfig {
+
+ @Bean(name = "executor1")
+ public Executor executor1() {
+ ThreadPoolTaskExecutor executor = new ThreadPoolTaskExecutor();
+ executor.setCorePoolSize(3);
+ executor.setMaxPoolSize(5);
+ executor.setQueueCapacity(50);
+ executor.setThreadNamePrefix("AsyncExecutor1-");
+ executor.initialize();
+ return executor;
+ }
+
+ @Bean(name = "executor2")
+ public Executor executor2() {
+ ThreadPoolTaskExecutor executor = new ThreadPoolTaskExecutor();
+ executor.setCorePoolSize(2);
+ executor.setMaxPoolSize(4);
+ executor.setQueueCapacity(100);
+ executor.setThreadNamePrefix("AsyncExecutor2-");
+ executor.initialize();
+ return executor;
+ }
+}
+```
+
+`@Async` 注解中指定线程池的 Bean 名称:
+
+```java
+@Service
+public class AsyncService {
+
+ @Async("executor1")
+ public void performTask1() {
+ // 任务1的逻辑
+ System.out.println("Executing Task1 with Executor1");
+ }
+
+ @Async("executor2")
+ public void performTask2() {
+ // 任务2的逻辑
+ System.out.println("Executing Task2 with Executor2");
+ }
+}
+```
+
+### 避免 @Async 注解失效
+
+`@Async` 注解会在以下几个场景失效,需要注意:
+
+**1、同一类中调用异步方法**
+
+如果你在同一个类内部调用一个`@Async`注解的方法,那这个方法将不会异步执行。
+
+```java
+@Service
+public class MyService {
+
+ public void myMethod() {
+ // 直接通过 this 引用调用,绕过了 Spring 的代理机制,异步执行失效
+ asyncMethod();
+ }
+
+ @Async
+ public void asyncMethod() {
+ // 异步执行的逻辑
+ }
+}
+```
+
+这是因为 Spring 的异步机制是通过 **代理** 实现的,而在同一个类内部的方法调用会绕过 Spring 的代理机制,也就是绕过了代理对象,直接通过 this 引用调用的。由于没有经过代理,所有的代理相关的处理(即将任务提交线程池异步执行)都不会发生。
+
+为了避免这个问题,比较推荐的做法是将异步方法移至另一个 Spring Bean 中。
+
+```java
+@Service
+public class AsyncService {
+ @Async
+ public void asyncMethod() {
+ // 异步执行的逻辑
+ }
+}
+
+@Service
+public class MyService {
+ @Autowired
+ private AsyncService asyncService;
+
+ public void myMethod() {
+ asyncService.asyncMethod();
+ }
+}
+```
+
+**2、使用 static 关键字修饰异步方法**
+
+如果`@Async`注解的方法被 `static` 关键字修饰,那这个方法将不会异步执行。
+
+这是因为 Spring 的异步机制是通过代理实现的,由于静态方法不属于实例而是属于类且不参与继承,Spring 的代理机制(无论是基于 JDK 还是 CGLIB)无法拦截静态方法来提供如异步执行这样的增强功能。
+
+篇幅问题,这里没有进一步详细介绍,不了解的代理机制的朋友,可以看看我写的 [Java 代理模式详解](https://javaguide.cn/java/basis/proxy.html)这篇文章。
+
+如果你需要异步执行一个静态方法的逻辑,可以考虑设计一个非静态的包装方法,这个包装方法使用 `@Async` 注解,并在其内部调用静态方法
+
+```java
+@Service
+public class AsyncService {
+
+ @Async
+ public void asyncWrapper() {
+ // 调用静态方法
+ SClass.staticMethod();
+ }
+}
+
+public class SClass {
+ public static void staticMethod() {
+ // 执行一些操作
+ }
+}
+```
+
+**3、忘记开启异步支持**
+
+Spring Boot 默认情况下不启用异步支持,确保在主配置类 `Application` 上添加`@EnableAsync`注解以启用异步功能。
+
+```java
+@SpringBootApplication
+@EnableAsync
+public class Application {
+ public static void main(String[] args) {
+ SpringApplication.run(Application.class, args);
+ }
+}
+```
+
+**4、`@Async` 注解的方法所在的类必须是 Spring Bean**
+
+`@Async` 注解的方法必须位于 Spring 管理的 Bean 中,只有这样,Spring 才能在创建 Bean 时应用代理,代理能够拦截方法调用并实现异步执行的逻辑。如果该方法不在 Spring 管理的 bean 中,Spring 就无法创建必要的代理,`@Async` 注解就不会产生任何效果。
+
+### 返回值类型
+
+建议将 `@Async` 注解方法的返回值类型定义为 `void` 和 `Future` 。
+
+- 如果不需要获取异步方法返回的结果,将返回值类型定义为 `void` 。
+- 如果需要获取异步方法返回的结果,将返回值类型定义为 `Future`(例如`CompletableFuture` 、 `ListenableFuture` )。
+
+如果将 `@Async` 注解方法的返回值定义为其他类型(如 `Object` 、 `String` 等等),则无法获取方法返回值。
+
+这种设计符合异步编程的基本原则,即调用者不应立即期待一个结果,而是应该能够在未来某个时间点获取结果。如果返回类型是 `Future`,调用者可以使用这个返回的 `Future` 对象来查询任务的状态,取消任务,或者在任务完成时获取结果。
+
+### 处理异步方法中的异常
+
+异步方法中抛出的异常默认不会被调用者捕获。为了管理这些异常,建议使用`CompletableFuture`的异常处理功能,或者配置一个全局的`AsyncUncaughtExceptionHandler`来处理没有正确捕获的异常。
+
+```java
+@Configuration
+@EnableAsync
+public class AsyncConfig implements AsyncConfigurer{
+
+ @Override
+ public AsyncUncaughtExceptionHandler getAsyncUncaughtExceptionHandler() {
+ return new CustomAsyncExceptionHandler();
+ }
+
+}
+
+// 自定义异常处理器
+class CustomAsyncExceptionHandler implements AsyncUncaughtExceptionHandler {
+
+ @Override
+ public void handleUncaughtException(Throwable ex, Method method, Object... params) {
+ // 日志记录或其他处理逻辑
+ }
+}
+```
+
+### 未考虑事务管理
+
+`@Async`注解的方法需要事务支持时,务必在该异步方法上独立使用。
+
+```java
+@Service
+public class AsyncTransactionalService {
+
+ @Async
+ // Propagation.REQUIRES_NEW 表示 Spring 在执行异步方法时开启一个新的、与当前事务无关的事务
+ @Transactional(propagation = Propagation.REQUIRES_NEW)
+ public void asyncTransactionalMethod() {
+ // 这里的操作会在新的事务中执行
+ // 执行一些数据库操作
+ }
+}
+```
+
+### 未指定异步方法执行顺序
+
+`@Async`注解的方法执行是非阻塞的,它们可能以任意顺序完成。如果需要按照特定的顺序处理结果,你可以将方法的返回值设定为 `Future` 或 `CompletableFuture` ,通过返回值对象来实现一个方法在另一个方法完成后再执行。
+
+```java
+@Async
+public CompletableFuture fetchDataAsync() {
+ return CompletableFuture.completedFuture("Data");
+}
+
+@Async
+public CompletableFuture processDataAsync(String data) {
+ return CompletableFuture.supplyAsync(() -> "Processed " + data);
+}
+```
+
+`processDataAsync` 方法在 `fetchDataAsync`后执行:
+
+```java
+CompletableFuture dataFuture = asyncService.fetchDataAsync();
+dataFuture.thenCompose(data -> asyncService.processDataAsync(data))
+ .thenAccept(result -> System.out.println(result));
+```
+
+##
diff --git a/docs/system-design/framework/spring/async.md b/docs/system-design/framework/spring/async.md
new file mode 100644
index 00000000000..79a78bbf8d9
--- /dev/null
+++ b/docs/system-design/framework/spring/async.md
@@ -0,0 +1,726 @@
+---
+title: Async 注解原理分析
+description: Spring @Async异步注解原理详解,涵盖异步任务配置、线程池设置、@EnableAsync机制及常见使用问题。
+category: 框架
+tag:
+ - Spring
+head:
+ - - meta
+ - name: keywords
+ content: Spring异步,@Async,EnableAsync,线程池,TaskExecutor,异步任务,Spring注解,方法异步
+---
+
+`@Async` 注解由 Spring 框架提供,被该注解标注的类或方法会在 **异步线程** 中执行。这意味着当方法被调用时,调用者将不会等待该方法执行完成,而是可以继续执行后续的代码。
+
+`@Async` 注解的使用非常简单,需要两个步骤:
+
+1. 在启动类上添加注解 `@EnableAsync` ,开启异步任务。
+2. 在需要异步执行的方法或类上添加注解 `@Async` 。
+
+```java
+@SpringBootApplication
+// 开启异步任务
+@EnableAsync
+public class YourApplication {
+
+ public static void main(String[] args) {
+ SpringApplication.run(YourApplication.class, args);
+ }
+}
+
+// 异步服务类
+@Service
+public class MyService {
+
+ // 推荐使用自定义线程池,这里只是演示基本用法
+ @Async
+ public CompletableFuture doSomethingAsync() {
+
+ // 这里会有一些业务耗时操作
+ // ...
+ // 使用 CompletableFuture 可以更方便地处理异步任务的结果,避免阻塞主线程
+ return CompletableFuture.completedFuture("Async Task Completed");
+ }
+
+}
+```
+
+接下来,我们一起来看看 `@Async` 的底层原理。
+
+## @Async 原理分析
+
+`@Async` 可以异步执行任务,本质上是使用 **动态代理** 来实现的。通过 Spring 中的后置处理器 `BeanPostProcessor` 为使用 `@Async` 注解的类创建动态代理,之后 `@Async` 注解方法的调用会被动态代理拦截,在拦截器中将方法的执行封装为异步任务提交给线程池处理。
+
+接下来,我们来详细分析一下。
+
+### 开启异步
+
+使用 `@Async` 之前,需要在启动类上添加 `@EnableAsync` 来开启异步,`@EnableAsync` 注解如下:
+
+```JAVA
+// 省略其他注解 ...
+@Import(AsyncConfigurationSelector.class)
+public @interface EnableAsync { /* ... */ }
+```
+
+在 `@EnableAsync` 注解上通过 `@Import` 注解引入了 `AsyncConfigurationSelector` ,因此 Spring 会去加载通过 `@Import` 注解引入的类。
+
+`AsyncConfigurationSelector` 类实现了 `ImportSelector` 接口,因此在该类中会重写 `selectImports()` 方法来自定义加载 Bean 的逻辑,如下:
+
+```JAVA
+public class AsyncConfigurationSelector extends AdviceModeImportSelector {
+ @Override
+ @Nullable
+ public String[] selectImports(AdviceMode adviceMode) {
+ switch (adviceMode) {
+ // 基于 JDK 代理织入的通知
+ case PROXY:
+ return new String[] {ProxyAsyncConfiguration.class.getName()};
+ // 基于 AspectJ 织入的通知
+ case ASPECTJ:
+ return new String[] {ASYNC_EXECUTION_ASPECT_CONFIGURATION_CLASS_NAME};
+ default:
+ return null;
+ }
+ }
+}
+```
+
+在 `selectImports()` 方法中,会根据通知的不同类型来选择加载不同的类,其中 `adviceMode` 默认值为 `PROXY` 。
+
+这里以基于 JDK 代理的通知为例,此时会加载 `ProxyAsyncConfiguration` 类,如下:
+
+```JAVA
+@Configuration
+@Role(BeanDefinition.ROLE_INFRASTRUCTURE)
+public class ProxyAsyncConfiguration extends AbstractAsyncConfiguration {
+ @Bean(name = TaskManagementConfigUtils.ASYNC_ANNOTATION_PROCESSOR_BEAN_NAME)
+ @Role(BeanDefinition.ROLE_INFRASTRUCTURE)
+ public AsyncAnnotationBeanPostProcessor asyncAdvisor() {
+ // ...
+ // 加载后置处理器
+ AsyncAnnotationBeanPostProcessor bpp = new AsyncAnnotationBeanPostProcessor();
+
+ // ...
+ return bpp;
+ }
+}
+```
+
+### 后置处理器
+
+在 `ProxyAsyncConfiguration` 类中,会通过 `@Bean` 注解加载一个后置处理器 `AsyncAnnotationBeanPostProcessor` ,这个后置处理器是使 `@Async` 注解起作用的关键。
+
+如果某一个类或者方法上使用了 `@Async` 注解,`AsyncAnnotationBeanPostProcessor` 处理器就会为该类创建一个动态代理。
+
+该类的方法在执行时,会被代理对象的拦截器所拦截,其中被 `@Async` 注解标记的方法会异步执行。
+
+`AsyncAnnotationBeanPostProcessor` 代码如下:
+
+```JAVA
+public class AsyncAnnotationBeanPostProcessor extends AbstractBeanFactoryAwareAdvisingPostProcessor {
+ @Override
+ public void setBeanFactory(BeanFactory beanFactory) {
+ super.setBeanFactory(beanFactory);
+ // 创建 AsyncAnnotationAdvisor,它是一个 Advisor
+ // 用于拦截带有 @Async 注解的方法并将这些方法异步执行。
+ AsyncAnnotationAdvisor advisor = new AsyncAnnotationAdvisor(this.executor, this.exceptionHandler);
+ // 如果设置了自定义的 asyncAnnotationType,则将其设置到 advisor 中。
+ // asyncAnnotationType 用于指定自定义的异步注解,例如 @MyAsync。
+ if (this.asyncAnnotationType != null) {
+ advisor.setAsyncAnnotationType(this.asyncAnnotationType);
+ }
+ advisor.setBeanFactory(beanFactory);
+ this.advisor = advisor;
+ }
+}
+```
+
+`AsyncAnnotationBeanPostProcessor` 的父类实现了 `BeanFactoryAware` 接口,因此在该类中重写了 `setBeanFactory()` 方法作为扩展点,来加载 `AsyncAnnotationAdvisor` 。
+
+#### 创建 Advisor
+
+`Advisor` 是 `Spring AOP` 对 `Advice` 和 `Pointcut` 的抽象。`Advice` 为执行的通知逻辑,`Pointcut` 为通知执行的切入点。
+
+在后置处理器 `AsyncAnnotationBeanPostProcessor` 中会去创建 `AsyncAnnotationAdvisor` , 在它的构造方法中,会构建对应的 `Advice` 和 `Pointcut` ,如下:
+
+```JAVA
+public class AsyncAnnotationAdvisor extends AbstractPointcutAdvisor implements BeanFactoryAware {
+
+ private Advice advice; // 异步执行的 Advice
+ private Pointcut pointcut; // 匹配 @Async 注解方法的切点
+
+ // 构造函数
+ public AsyncAnnotationAdvisor(/* 参数省略 */) {
+ // 1. 创建 Advice,负责异步执行逻辑
+ this.advice = buildAdvice(executor, exceptionHandler);
+ // 2. 创建 Pointcut,选择要被增强的目标方法
+ this.pointcut = buildPointcut(asyncAnnotationTypes);
+ }
+
+ // 创建 Advice
+ protected Advice buildAdvice(/* 参数省略 */) {
+ // 创建处理异步执行的拦截器
+ AnnotationAsyncExecutionInterceptor interceptor = new AnnotationAsyncExecutionInterceptor(null);
+ // 使用执行器和异常处理器配置拦截器
+ interceptor.configure(executor, exceptionHandler);
+ return interceptor;
+ }
+
+ // 创建 Pointcut
+ protected Pointcut buildPointcut(Set> asyncAnnotationTypes) {
+ ComposablePointcut result = null;
+ for (Class extends Annotation> asyncAnnotationType : asyncAnnotationTypes) {
+ // 1. 类级别切点:如果类上有注解则匹配
+ Pointcut cpc = new AnnotationMatchingPointcut(asyncAnnotationType, true);
+ // 2. 方法级别切点:如果方法上有注解则匹配
+ Pointcut mpc = new AnnotationMatchingPointcut(null, asyncAnnotationType, true);
+
+ if (result == null) {
+ result = new ComposablePointcut(cpc);
+ } else {
+ // 使用 union 合并之前的切点
+ result.union(cpc);
+ }
+ // 将方法级别切点添加到组合切点
+ result = result.union(mpc);
+ }
+ // 返回组合切点,如果没有提供注解类型则返回 Pointcut.TRUE
+ return (result != null ? result : Pointcut.TRUE);
+ }
+}
+```
+
+`AsyncAnnotationAdvisor` 的核心在于构建 `Advice` 和 `Pointcut` :
+
+- 构建 `Advice` :会创建 `AnnotationAsyncExecutionInterceptor` 拦截器,在拦截器的 `invoke()` 方法中会执行通知的逻辑。
+- 构建 `Pointcut` :由 `ClassFilter` 和 `MethodMatcher` 组成,用于匹配哪些方法需要执行通知( `Advice` )的逻辑。
+
+#### 后置处理逻辑
+
+`AsyncAnnotationBeanPostProcessor` 后置处理器中实现的 `postProcessAfterInitialization()` 方法在其父类 `AbstractAdvisingBeanPostProcessor` 中,在 `Bean` 初始化之后,会进入到 `postProcessAfterInitialization()` 方法进行后置处理。
+
+在后置处理方法中,会判断 `Bean` 是否符合后置处理器中 `Advisor` 通知的条件,如果符合,则创建代理对象。如下:
+
+```JAVA
+// AbstractAdvisingBeanPostProcessor
+public Object postProcessAfterInitialization(Object bean, String beanName) {
+ if (this.advisor == null || bean instanceof AopInfrastructureBean) {
+ return bean;
+ }
+ if (bean instanceof Advised) {
+ Advised advised = (Advised) bean;
+ if (!advised.isFrozen() && isEligible(AopUtils.getTargetClass(bean))) {
+ if (this.beforeExistingAdvisors) {
+ advised.addAdvisor(0, this.advisor);
+ }
+ else {
+ advised.addAdvisor(this.advisor);
+ }
+ return bean;
+ }
+ }
+ // 判断给定的 Bean 是否符合后置处理器中 Advisor 通知的条件,符合的话,就创建代理对象。
+ if (isEligible(bean, beanName)) {
+ ProxyFactory proxyFactory = prepareProxyFactory(bean, beanName);
+ if (!proxyFactory.isProxyTargetClass()) {
+ evaluateProxyInterfaces(bean.getClass(), proxyFactory);
+ }
+ // 添加 Advisor。
+ proxyFactory.addAdvisor(this.advisor);
+ customizeProxyFactory(proxyFactory);
+ // 返回代理对象。
+ return proxyFactory.getProxy(getProxyClassLoader());
+ }
+ return bean;
+}
+```
+
+### @Async 注解方法的拦截
+
+`@Async` 注解方法的执行会在 `AnnotationAsyncExecutionInterceptor` 中被拦截,在 `invoke()` 方法中执行拦截器的逻辑。此时会将 `@Async` 注解标注的方法封装为异步任务,交给执行器来执行。
+
+`invoke()` 方法在 `AnnotationAsyncExecutionInterceptor` 的父类 `AsyncExecutionInterceptor` 中定义,如下:
+
+```JAVA
+public class AsyncExecutionInterceptor extends AsyncExecutionAspectSupport implements MethodInterceptor, Ordered {
+ @Override
+ @Nullable
+ public Object invoke(final MethodInvocation invocation) throws Throwable {
+ Class> targetClass = (invocation.getThis() != null ? AopUtils.getTargetClass(invocation.getThis()) : null);
+ Method specificMethod = ClassUtils.getMostSpecificMethod(invocation.getMethod(), targetClass);
+ final Method userDeclaredMethod = BridgeMethodResolver.findBridgedMethod(specificMethod);
+
+ // 1、确定异步任务执行器
+ AsyncTaskExecutor executor = determineAsyncExecutor(userDeclaredMethod);
+
+ // 2、将要执行的方法封装为 Callable 异步任务
+ Callable task = () -> {
+ try {
+ // 2.1、执行方法
+ Object result = invocation.proceed();
+ // 2.2、如果方法返回值是 Future 类型,阻塞等待结果
+ if (result instanceof Future) {
+ return ((Future>) result).get();
+ }
+ }
+ catch (ExecutionException ex) {
+ handleError(ex.getCause(), userDeclaredMethod, invocation.getArguments());
+ }
+ catch (Throwable ex) {
+ handleError(ex, userDeclaredMethod, invocation.getArguments());
+ }
+ return null;
+ };
+ // 3、提交任务
+ return doSubmit(task, executor, invocation.getMethod().getReturnType());
+ }
+}
+```
+
+在 `invoke()` 方法中,主要有 3 个步骤:
+
+1. 确定执行异步任务的执行器。
+2. 将 `@Async` 注解标注的方法封装为 `Callable` 异步任务。
+3. 将任务提交给执行器执行。
+
+#### 1、获取异步任务执行器
+
+在 `determineAsyncExecutor()` 方法中,会获取异步任务的执行器(即执行异步任务的 **线程池** )。代码如下:
+
+```JAVA
+// 确定异步任务的执行器
+protected AsyncTaskExecutor determineAsyncExecutor(Method method) {
+ // 1、先从缓存中获取。
+ AsyncTaskExecutor executor = this.executors.get(method);
+ if (executor == null) {
+ Executor targetExecutor;
+ // 2、获取执行器的限定符。
+ String qualifier = getExecutorQualifier(method);
+ if (StringUtils.hasLength(qualifier)) {
+ // 3、根据限定符获取对应的执行器。
+ targetExecutor = findQualifiedExecutor(this.beanFactory, qualifier);
+ }
+ else {
+ // 4、如果没有限定符,则使用默认的执行器。即 Spring 提供的默认线程池:SimpleAsyncTaskExecutor。
+ targetExecutor = this.defaultExecutor.get();
+ }
+ if (targetExecutor == null) {
+ return null;
+ }
+ // 5、将执行器包装为 TaskExecutorAdapter 适配器。
+ // TaskExecutorAdapter 是 Spring 对于 JDK 线程池做的一层抽象,还是继承自 JDK 的线程池 Executor。这里可以不用管太多,只要知道它是线程池就可以了。
+ executor = (targetExecutor instanceof AsyncListenableTaskExecutor ?
+ (AsyncListenableTaskExecutor) targetExecutor : new TaskExecutorAdapter(targetExecutor));
+ this.executors.put(method, executor);
+ }
+ return executor;
+}
+```
+
+在 `determineAsyncExecutor()` 方法中确定了异步任务的执行器(线程池),主要是通过 `@Async` 注解的 `value` 值来获取执行器的限定符,根据限定符再去 `BeanFactory` 中查找对应的执行器就可以了。
+
+如果在 `@Async` 注解中没有指定线程池,则会通过 `this.defaultExecutor.get()` 来获取默认的线程池,其中 `defaultExecutor` 在下边方法中进行赋值:
+
+```JAVA
+// AsyncExecutionInterceptor
+protected Executor getDefaultExecutor(@Nullable BeanFactory beanFactory) {
+ // 1、尝试从 beanFactory 中获取线程池。
+ Executor defaultExecutor = super.getDefaultExecutor(beanFactory);
+ // 2、如果 beanFactory 中没有,则创建 SimpleAsyncTaskExecutor 线程池。
+ return (defaultExecutor != null ? defaultExecutor : new SimpleAsyncTaskExecutor());
+}
+```
+
+其中 `super.getDefaultExecutor()` 会在 `beanFactory` 中尝试获取 `Executor` 类型的线程池。代码如下:
+
+```JAVA
+protected Executor getDefaultExecutor(@Nullable BeanFactory beanFactory) {
+ if (beanFactory != null) {
+ try {
+ // 1、从 beanFactory 中获取 TaskExecutor 类型的线程池。
+ return beanFactory.getBean(TaskExecutor.class);
+ }
+ catch (NoUniqueBeanDefinitionException ex) {
+ try {
+ // 2、如果有多个,则尝试从 beanFactory 中获取执行名称的 Executor 线程池。
+ return beanFactory.getBean(DEFAULT_TASK_EXECUTOR_BEAN_NAME, Executor.class);
+ }
+ catch (NoSuchBeanDefinitionException ex2) {
+ if (logger.isInfoEnabled()) {
+ // ...
+ }
+ }
+ }
+ catch (NoSuchBeanDefinitionException ex) {
+ try {
+ // 3、如果没有,则尝试从 beanFactory 中获取执行名称的 Executor 线程池。
+ return beanFactory.getBean(DEFAULT_TASK_EXECUTOR_BEAN_NAME, Executor.class);
+ }
+ catch (NoSuchBeanDefinitionException ex2) {
+ // ...
+ }
+ }
+ }
+ return null;
+}
+```
+
+在 `getDefaultExecutor()` 中,如果从 `beanFactory` 获取线程池失败的话,则会创建 `SimpleAsyncTaskExecutor` 线程池。
+
+该线程池的在每次执行异步任务时,都会创建一个新的线程去执行任务,并不会对线程进行复用,从而导致异步任务执行的开销很大。一旦在 `@Async` 注解标注的方法某一瞬间并发量剧增,应用就会大量创建线程,从而影响服务质量甚至出现服务不可用。
+
+同一时刻如果向 `SimpleAsyncTaskExecutor` 线程池提交 10000 个任务,那么该线程池就会创建 10000 个线程,其的 `execute()` 方法如下:
+
+```JAVA
+// SimpleAsyncTaskExecutor:execute() 内部会调用 doExecute()
+protected void doExecute(Runnable task) {
+ // 创建新线程
+ Thread thread = (this.threadFactory != null ? this.threadFactory.newThread(task) : createThread(task));
+ thread.start();
+}
+```
+
+**建议:在使用 `@Async` 时需要自己指定线程池,避免 Spring 默认线程池带来的风险。**
+
+在 `@Async` 注解中的 `value` 指定了线程池的限定符,根据限定符可以获取 **自定义的线程池** 。获取限定符的代码如下:
+
+```JAVA
+// AnnotationAsyncExecutionInterceptor
+protected String getExecutorQualifier(Method method) {
+ // 1.从方法上获取 Async 注解。
+ Async async = AnnotatedElementUtils.findMergedAnnotation(method, Async.class);
+ // 2. 如果方法上没有找到 @Async 注解,则尝试从方法所在的类上获取 @Async 注解。
+ if (async == null) {
+ async = AnnotatedElementUtils.findMergedAnnotation(method.getDeclaringClass(), Async.class);
+ }
+ // 3. 如果找到了 @Async 注解,则获取注解的 value 值并返回,作为线程池的限定符。
+ // 如果 "value" 属性值为空字符串,则使用默认的线程池。
+ // 如果没有找到 @Async 注解,则返回 null,同样使用默认的线程池。
+ return (async != null ? async.value() : null);
+}
+```
+
+#### 2、将方法封装为异步任务
+
+在 `invoke()` 方法获取执行器之后,会将方法封装为异步任务,代码如下:
+
+```JAVA
+// 将要执行的方法封装为 Callable 异步任务
+Callable task = () -> {
+ try {
+ // 2.1、执行被拦截的方法 (proceed() 方法是 AOP 中的核心方法,用于执行目标方法)
+ Object result = invocation.proceed();
+
+ // 2.2、如果被拦截方法的返回值是 Future 类型,则需要阻塞等待结果,
+ // 并将 Future 的结果作为异步任务的结果返回。 这是为了处理异步方法嵌套调用的情况。
+ // 例如,一个异步方法内部调用了另一个异步方法,则需要等待内部异步方法执行完成,
+ // 才能返回最终的结果。
+ if (result instanceof Future) {
+ return ((Future>) result).get(); // 阻塞等待 Future 的结果
+ }
+ }
+ catch (ExecutionException ex) {
+ // 2.3、处理 ExecutionException 异常。 ExecutionException 是 Future.get() 方法抛出的异常,
+ handleError(ex.getCause(), userDeclaredMethod, invocation.getArguments()); // 处理原始异常
+ }
+ catch (Throwable ex) {
+ // 2.4、处理其他类型的异常。 将异常、被拦截的方法和方法参数作为参数调用 handleError() 方法进行处理。
+ handleError(ex, userDeclaredMethod, invocation.getArguments());
+ }
+ // 2.5、如果方法返回值不是 Future 类型,或者发生异常,则返回 null。
+ return null;
+};
+```
+
+相比于 `Runnable` ,`Callable` 可以返回结果,并且抛出异常。
+
+将 `invocation.proceed()` 的执行(原方法的执行)封装为 `Callable` 异步任务。这里仅仅当 `result` (方法返回值)类型为 `Future` 才返回,如果是其他类型则直接返回 `null` 。
+
+因此使用 `@Async` 注解标注的方法如果使用 `Future` 类型之外的返回值,则无法获取方法的执行结果。
+
+#### 3、提交异步任务
+
+在 `AsyncExecutionInterceptor # invoke()` 中将要执行的方法封装为 Callable 任务之后,就会将任务交给执行器来执行。提交相关的代码如下:
+
+```JAVA
+protected Object doSubmit(Callable task, AsyncTaskExecutor executor, Class> returnType) {
+ // 根据方法的返回值类型,选择不同的异步执行方式并返回结果。
+ // 1. 如果方法返回值是 CompletableFuture 类型
+ if (CompletableFuture.class.isAssignableFrom(returnType)) {
+ // 使用 CompletableFuture.supplyAsync() 方法异步执行任务。
+ return CompletableFuture.supplyAsync(() -> {
+ try {
+ return task.call();
+ }
+ catch (Throwable ex) {
+ throw new CompletionException(ex); // 将异常包装为 CompletionException,以便在 future.get() 时抛出
+ }
+ }, executor);
+ }
+ // 2. 如果方法返回值是 ListenableFuture 类型
+ else if (ListenableFuture.class.isAssignableFrom(returnType)) {
+ // 将 AsyncTaskExecutor 强制转换为 AsyncListenableTaskExecutor,
+ // 并调用 submitListenable() 方法提交任务。
+ // AsyncListenableTaskExecutor 是 ListenableFuture 的专用异步执行器,
+ // 它可以返回一个 ListenableFuture 对象,允许添加回调函数来监听任务的完成。
+ return ((AsyncListenableTaskExecutor) executor).submitListenable(task);
+ }
+ // 3. 如果方法返回值是 Future 类型
+ else if (Future.class.isAssignableFrom(returnType)) {
+ // 直接调用 AsyncTaskExecutor 的 submit() 方法提交任务,并返回一个 Future 对象。
+ return executor.submit(task);
+ }
+ // 4. 如果方法返回值是 void 或其他类型
+ else {
+ // 直接调用 AsyncTaskExecutor 的 submit() 方法提交任务。
+ // 由于方法返回值是 void,因此不需要返回任何结果,直接返回 null。
+ executor.submit(task);
+ return null;
+ }
+}
+```
+
+在 `doSubmit()` 方法中,会根据 `@Async` 注解标注方法的返回值不同,来选择不同的任务提交方式,最后任务会由执行器(线程池)执行。
+
+### 总结
+
+
+
+理解 `@Async` 原理的核心在于理解 `@EnableAsync` 注解,该注解开启了异步任务的功能。
+
+主要流程如上图,会通过后置处理器来创建代理对象,之后代理对象中 `@Async` 方法的执行会走到 `Advice` 内部的拦截器中,之后将方法封装为异步任务,并提交线程池进行处理。
+
+## @Async 使用建议
+
+### 自定义线程池
+
+如果没有显式地配置线程池,在 `@Async` 底层会先在 `BeanFactory` 中尝试获取线程池,如果获取不到,则会创建一个 `SimpleAsyncTaskExecutor` 实现。`SimpleAsyncTaskExecutor` 本质上不算是一个真正的线程池,因为它对于每个请求都会启动一个新线程而不重用现有线程,这会带来一些潜在的问题,例如资源消耗过大。
+
+具体线程池获取可以参考这篇文章:[浅析 Spring 中 Async 注解底层异步线程池原理|得物技术](https://mp.weixin.qq.com/s/FySv5L0bCdrlb5MoSfQtAA)。
+
+一定要显式配置一个线程池,推荐`ThreadPoolTaskExecutor`。并且,还可以根据任务的性质和需求,为不同的异步方法指定不同的线程池。
+
+```java
+@Configuration
+@EnableAsync
+public class AsyncConfig {
+
+ @Bean(name = "executor1")
+ public Executor executor1() {
+ ThreadPoolTaskExecutor executor = new ThreadPoolTaskExecutor();
+ executor.setCorePoolSize(3);
+ executor.setMaxPoolSize(5);
+ executor.setQueueCapacity(50);
+ executor.setThreadNamePrefix("AsyncExecutor1-");
+ executor.initialize();
+ return executor;
+ }
+
+ @Bean(name = "executor2")
+ public Executor executor2() {
+ ThreadPoolTaskExecutor executor = new ThreadPoolTaskExecutor();
+ executor.setCorePoolSize(2);
+ executor.setMaxPoolSize(4);
+ executor.setQueueCapacity(100);
+ executor.setThreadNamePrefix("AsyncExecutor2-");
+ executor.initialize();
+ return executor;
+ }
+}
+```
+
+`@Async` 注解中指定线程池的 Bean 名称:
+
+```java
+@Service
+public class AsyncService {
+
+ @Async("executor1")
+ public void performTask1() {
+ // 任务1的逻辑
+ System.out.println("Executing Task1 with Executor1");
+ }
+
+ @Async("executor2")
+ public void performTask2() {
+ // 任务2的逻辑
+ System.out.println("Executing Task2 with Executor2");
+ }
+}
+```
+
+### 避免 @Async 注解失效
+
+`@Async` 注解会在以下几个场景失效,需要注意:
+
+**1、同一类中调用异步方法**
+
+如果你在同一个类内部调用一个`@Async`注解的方法,那这个方法将不会异步执行。
+
+```java
+@Service
+public class MyService {
+
+ public void myMethod() {
+ // 直接通过 this 引用调用,绕过了 Spring 的代理机制,异步执行失效
+ asyncMethod();
+ }
+
+ @Async
+ public void asyncMethod() {
+ // 异步执行的逻辑
+ }
+}
+```
+
+这是因为 Spring 的异步机制是通过 **代理** 实现的,而在同一个类内部的方法调用会绕过 Spring 的代理机制,也就是绕过了代理对象,直接通过 this 引用调用的。由于没有经过代理,所有的代理相关的处理(即将任务提交线程池异步执行)都不会发生。
+
+为了避免这个问题,比较推荐的做法是将异步方法移至另一个 Spring Bean 中。
+
+```java
+@Service
+public class AsyncService {
+ @Async
+ public void asyncMethod() {
+ // 异步执行的逻辑
+ }
+}
+
+@Service
+public class MyService {
+ @Autowired
+ private AsyncService asyncService;
+
+ public void myMethod() {
+ asyncService.asyncMethod();
+ }
+}
+```
+
+**2、使用 static 关键字修饰异步方法**
+
+如果`@Async`注解的方法被 `static` 关键字修饰,那这个方法将不会异步执行。
+
+这是因为 Spring 的异步机制是通过代理实现的,由于静态方法不属于实例而是属于类且不参与继承,Spring 的代理机制(无论是基于 JDK 还是 CGLIB)无法拦截静态方法来提供如异步执行这样的增强功能。
+
+篇幅问题,这里没有进一步详细介绍,不了解的代理机制的朋友,可以看看我写的 [Java 代理模式详解](https://javaguide.cn/java/basis/proxy.html)这篇文章。
+
+如果你需要异步执行一个静态方法的逻辑,可以考虑设计一个非静态的包装方法,这个包装方法使用 `@Async` 注解,并在其内部调用静态方法
+
+```java
+@Service
+public class AsyncService {
+
+ @Async
+ public void asyncWrapper() {
+ // 调用静态方法
+ SClass.staticMethod();
+ }
+}
+
+public class SClass {
+ public static void staticMethod() {
+ // 执行一些操作
+ }
+}
+```
+
+**3、忘记开启异步支持**
+
+Spring Boot 默认情况下不启用异步支持,确保在主配置类 `Application` 上添加`@EnableAsync`注解以启用异步功能。
+
+```java
+@SpringBootApplication
+@EnableAsync
+public class Application {
+ public static void main(String[] args) {
+ SpringApplication.run(Application.class, args);
+ }
+}
+```
+
+**4、`@Async` 注解的方法所在的类必须是 Spring Bean**
+
+`@Async` 注解的方法必须位于 Spring 管理的 Bean 中,只有这样,Spring 才能在创建 Bean 时应用代理,代理能够拦截方法调用并实现异步执行的逻辑。如果该方法不在 Spring 管理的 bean 中,Spring 就无法创建必要的代理,`@Async` 注解就不会产生任何效果。
+
+### 返回值类型
+
+建议将 `@Async` 注解方法的返回值类型定义为 `void` 和 `Future` 。
+
+- 如果不需要获取异步方法返回的结果,将返回值类型定义为 `void` 。
+- 如果需要获取异步方法返回的结果,将返回值类型定义为 `Future`(例如`CompletableFuture` 、 `ListenableFuture` )。
+
+如果将 `@Async` 注解方法的返回值定义为其他类型(如 `Object` 、 `String` 等等),则无法获取方法返回值。
+
+这种设计符合异步编程的基本原则,即调用者不应立即期待一个结果,而是应该能够在未来某个时间点获取结果。如果返回类型是 `Future`,调用者可以使用这个返回的 `Future` 对象来查询任务的状态,取消任务,或者在任务完成时获取结果。
+
+### 处理异步方法中的异常
+
+异步方法中抛出的异常默认不会被调用者捕获。为了管理这些异常,建议使用`CompletableFuture`的异常处理功能,或者配置一个全局的`AsyncUncaughtExceptionHandler`来处理没有正确捕获的异常。
+
+```java
+@Configuration
+@EnableAsync
+public class AsyncConfig implements AsyncConfigurer{
+
+ @Override
+ public AsyncUncaughtExceptionHandler getAsyncUncaughtExceptionHandler() {
+ return new CustomAsyncExceptionHandler();
+ }
+
+}
+
+// 自定义异常处理器
+class CustomAsyncExceptionHandler implements AsyncUncaughtExceptionHandler {
+
+ @Override
+ public void handleUncaughtException(Throwable ex, Method method, Object... params) {
+ // 日志记录或其他处理逻辑
+ }
+}
+```
+
+### 未考虑事务管理
+
+`@Async`注解的方法需要事务支持时,务必在该异步方法上独立使用。
+
+```java
+@Service
+public class AsyncTransactionalService {
+
+ @Async
+ // Propagation.REQUIRES_NEW 表示 Spring 在执行异步方法时开启一个新的、与当前事务无关的事务
+ @Transactional(propagation = Propagation.REQUIRES_NEW)
+ public void asyncTransactionalMethod() {
+ // 这里的操作会在新的事务中执行
+ // 执行一些数据库操作
+ }
+}
+```
+
+### 未指定异步方法执行顺序
+
+`@Async`注解的方法执行是非阻塞的,它们可能以任意顺序完成。如果需要按照特定的顺序处理结果,你可以将方法的返回值设定为 `Future` 或 `CompletableFuture` ,通过返回值对象来实现一个方法在另一个方法完成后再执行。
+
+```java
+@Async
+public CompletableFuture fetchDataAsync() {
+ return CompletableFuture.completedFuture("Data");
+}
+
+@Async
+public CompletableFuture processDataAsync(String data) {
+ return CompletableFuture.supplyAsync(() -> "Processed " + data);
+}
+```
+
+`processDataAsync` 方法在 `fetchDataAsync`后执行:
+
+```java
+CompletableFuture dataFuture = asyncService.fetchDataAsync();
+dataFuture.thenCompose(data -> asyncService.processDataAsync(data))
+ .thenAccept(result -> System.out.println(result));
+```
+
+##
diff --git a/docs/system-design/framework/spring/images/async/async.png b/docs/system-design/framework/spring/images/async/async.png
new file mode 100644
index 00000000000..6b68fd35084
Binary files /dev/null and b/docs/system-design/framework/spring/images/async/async.png differ
diff --git a/docs/system-design/framework/spring/ioc-and-aop.md b/docs/system-design/framework/spring/ioc-and-aop.md
index 2e99a150c64..fcc5c2953cc 100644
--- a/docs/system-design/framework/spring/ioc-and-aop.md
+++ b/docs/system-design/framework/spring/ioc-and-aop.md
@@ -1,8 +1,13 @@
---
title: IoC & AOP详解(快速搞懂)
+description: Spring IoC与AOP核心原理详解,深入讲解控制反转、依赖注入、切面编程及动态代理的实现机制。
category: 框架
tag:
- Spring
+head:
+ - - meta
+ - name: keywords
+ content: IoC,DI,AOP,Spring IoC容器,依赖注入,切面编程,动态代理,Spring原理
---
这篇文章会从下面从以下几个问题展开对 IoC & AOP 的解释
@@ -36,7 +41,7 @@ IoC (Inversion of Control )即控制反转/反转控制。它是一种思想
- **控制** :指的是对象创建(实例化、管理)的权力
- **反转** :控制权交给外部环境(IoC 容器)
-
+
### IoC 解决了什么问题?
@@ -49,17 +54,15 @@ IoC 的思想就是两方之间不互相依赖,由第三方容器来管理相
在没有使用 IoC 思想的情况下,Service 层想要使用 Dao 层的具体实现的话,需要通过 new 关键字在`UserServiceImpl` 中手动 new 出 `IUserDao` 的具体实现类 `UserDaoImpl`(不能直接 new 接口类)。
-
-
很完美,这种方式也是可以实现的,但是我们想象一下如下场景:
开发过程中突然接到一个新的需求,针对`IUserDao` 接口开发出另一个具体实现类。因为 Server 层依赖了`IUserDao`的具体实现,所以我们需要修改`UserServiceImpl`中 new 的对象。如果只有一个类引用了`IUserDao`的具体实现,可能觉得还好,修改起来也不是很费力气,但是如果有许许多多的地方都引用了`IUserDao`的具体实现的话,一旦需要更换`IUserDao` 的实现方式,那修改起来将会非常的头疼。
-
+
使用 IoC 的思想,我们将对象的控制权(创建、管理)交由 IoC 容器去管理,我们在使用的时候直接向 IoC 容器 “要” 就可以了
-
+
### IoC 和 DI 有区别吗?
@@ -87,6 +90,8 @@ AOP 的目的是将横切关注点(如日志记录、事务管理、权限控
AOP 之所以叫面向切面编程,是因为它的核心思想就是将横切关注点从核心业务逻辑中分离出来,形成一个个的**切面(Aspect)**。
+
+
这里顺带总结一下 AOP 关键术语(不理解也没关系,可以继续往下看):
- **横切关注点(cross-cutting concerns)** :多个类或对象中的公共行为(如日志记录、事务管理、权限控制、接口限流、接口幂等等)。
@@ -94,7 +99,17 @@ AOP 之所以叫面向切面编程,是因为它的核心思想就是将横切
- **连接点(JoinPoint)**:连接点是方法调用或者方法执行时的某个特定时刻(如方法调用、异常抛出等)。
- **通知(Advice)**:通知就是切面在某个连接点要执行的操作。通知有五种类型,分别是前置通知(Before)、后置通知(After)、返回通知(AfterReturning)、异常通知(AfterThrowing)和环绕通知(Around)。前四种通知都是在目标方法的前后执行,而环绕通知可以控制目标方法的执行过程。
- **切点(Pointcut)**:一个切点是一个表达式,它用来匹配哪些连接点需要被切面所增强。切点可以通过注解、正则表达式、逻辑运算等方式来定义。比如 `execution(* com.xyz.service..*(..))`匹配 `com.xyz.service` 包及其子包下的类或接口。
-- **织入(Weaving)**:织入是将切面和目标对象连接起来的过程,也就是将通知应用到切点匹配的连接点上。常见的织入时机有两种,分别是编译期织入(AspectJ)和运行期织入(AspectJ)。
+- **织入(Weaving)**:织入是将切面和目标对象连接起来的过程,也就是将通知应用到切点匹配的连接点上。常见的织入时机有两种,分别是编译期织入(Compile-Time Weaving 如:AspectJ)和运行期织入(Runtime Weaving 如:AspectJ、Spring AOP)。
+
+### AOP 常见的通知类型有哪些?
+
+
+
+- **Before**(前置通知):目标对象的方法调用之前触发
+- **After** (后置通知):目标对象的方法调用之后触发
+- **AfterReturning**(返回通知):目标对象的方法调用完成,在返回结果值之后触发
+- **AfterThrowing**(异常通知):目标对象的方法运行中抛出 / 触发异常后触发。AfterReturning 和 AfterThrowing 两者互斥。如果方法调用成功无异常,则会有返回值;如果方法抛出了异常,则不会有返回值。
+- **Around** (环绕通知):编程式控制目标对象的方法调用。环绕通知是所有通知类型中可操作范围最大的一种,因为它可以直接拿到目标对象,以及要执行的方法,所以环绕通知可以任意的在目标对象的方法调用前后搞事,甚至不调用目标对象的方法
### AOP 解决了什么问题?
@@ -104,7 +119,7 @@ AOP 可以将横切关注点(如日志记录、事务管理、权限控制、

-以日志记录为例进行介绍,假如我们需要对某些方法进行统一格式的日志记录,没有使用 AOP 技术之前,我们需要挨个写日志记录的逻辑代码,全是重复的的逻辑。
+以日志记录为例进行介绍,假如我们需要对某些方法进行统一格式的日志记录,没有使用 AOP 技术之前,我们需要挨个写日志记录的逻辑代码,全是重复的逻辑。
```java
public CommonResponse method1() {
@@ -200,14 +215,80 @@ public CommonResponse method1() {
AOP 的常见实现方式有动态代理、字节码操作等方式。
-Spring AOP 就是基于动态代理的,如果要代理的对象,实现了某个接口,那么 Spring AOP 会使用 **JDK Proxy**,去创建代理对象,而对于没有实现接口的对象,就无法使用 JDK Proxy 去进行代理了,这时候 Spring AOP 会使用 **Cglib** 生成一个被代理对象的子类来作为代理,如下图所示:
+Spring AOP 就是基于动态代理的,如果要代理的对象,实现了某个接口,那么 Spring AOP 会使用 **JDK Proxy**,去创建代理对象,而对于没有实现接口的对象,就无法使用 JDK Proxy 去进行代理了,这时候 Spring AOP 会使用 CGLIB 生成一个被代理对象的子类来作为代理,如下图所示:

+**Spring Boot 和 Spring 的动态代理的策略是不是也是一样的呢?**其实不一样,很多人都理解错了。
+
+Spring Boot 2.0 之前,默认使用 **JDK 动态代理**。如果目标类没有实现接口,会抛出异常,开发者必须显式配置(`spring.aop.proxy-target-class=true`)使用 **CGLIB 动态代理** 或者注入接口来解决。Spring Boot 1.5.x 自动配置 AOP 代码如下:
+
+```java
+@Configuration
+@ConditionalOnClass({ EnableAspectJAutoProxy.class, Aspect.class, Advice.class })
+@ConditionalOnProperty(prefix = "spring.aop", name = "auto", havingValue = "true", matchIfMissing = true)
+public class AopAutoConfiguration {
+
+ @Configuration
+ @EnableAspectJAutoProxy(proxyTargetClass = false)
+ // 该配置类只有在 spring.aop.proxy-target-class=false 或未显式配置时才会生效。
+ // 也就是说,如果开发者未明确选择代理方式,Spring 会默认加载 JDK 动态代理。
+ @ConditionalOnProperty(prefix = "spring.aop", name = "proxy-target-class", havingValue = "false", matchIfMissing = true)
+ public static class JdkDynamicAutoProxyConfiguration {
+
+ }
+
+ @Configuration
+ @EnableAspectJAutoProxy(proxyTargetClass = true)
+ // 该配置类只有在 spring.aop.proxy-target-class=true 时才会生效。
+ // 即开发者通过属性配置明确指定使用 CGLIB 动态代理时,Spring 会加载这个配置类。
+ @ConditionalOnProperty(prefix = "spring.aop", name = "proxy-target-class", havingValue = "true", matchIfMissing = false)
+ public static class CglibAutoProxyConfiguration {
+
+ }
+
+}
+```
+
+Spring Boot 2.0 开始,如果用户什么都不配置的话,默认使用 **CGLIB 动态代理**。如果需要强制使用 JDK 动态代理,可以在配置文件中添加:`spring.aop.proxy-target-class=false`。Spring Boot 2.0 自动配置 AOP 代码如下:
+
+```java
+@Configuration
+@ConditionalOnClass({ EnableAspectJAutoProxy.class, Aspect.class, Advice.class,
+ AnnotatedElement.class })
+@ConditionalOnProperty(prefix = "spring.aop", name = "auto", havingValue = "true", matchIfMissing = true)
+public class AopAutoConfiguration {
+
+ @Configuration
+ @EnableAspectJAutoProxy(proxyTargetClass = false)
+ // 该配置类只有在 spring.aop.proxy-target-class=false 时才会生效。
+ // 即开发者通过属性配置明确指定使用 JDK 动态代理时,Spring 会加载这个配置类。
+ @ConditionalOnProperty(prefix = "spring.aop", name = "proxy-target-class", havingValue = "false", matchIfMissing = false)
+ public static class JdkDynamicAutoProxyConfiguration {
+
+ }
+
+ @Configuration
+ @EnableAspectJAutoProxy(proxyTargetClass = true)
+ // 该配置类只有在 spring.aop.proxy-target-class=true 或未显式配置时才会生效。
+ // 也就是说,如果开发者未明确选择代理方式,Spring 会默认加载 CGLIB 代理。
+ @ConditionalOnProperty(prefix = "spring.aop", name = "proxy-target-class", havingValue = "true", matchIfMissing = true)
+ public static class CglibAutoProxyConfiguration {
+
+ }
+
+}
+```
+
当然你也可以使用 **AspectJ** !Spring AOP 已经集成了 AspectJ ,AspectJ 应该算的上是 Java 生态系统中最完整的 AOP 框架了。
**Spring AOP 属于运行时增强,而 AspectJ 是编译时增强。** Spring AOP 基于代理(Proxying),而 AspectJ 基于字节码操作(Bytecode Manipulation)。
-Spring AOP 已经集成了 AspectJ ,AspectJ 应该算的上是 Java 生态系统中最完整的 AOP 框架了。AspectJ 相比于 Spring AOP 功能更加强大,但是 Spring AOP 相对来说更简单,
+Spring AOP 已经集成了 AspectJ ,AspectJ 应该算的上是 Java 生态系统中最完整的 AOP 框架了。AspectJ 相比于 Spring AOP 功能更加强大,但是 Spring AOP 相对来说更简单。
如果我们的切面比较少,那么两者性能差异不大。但是,当切面太多的话,最好选择 AspectJ ,它比 Spring AOP 快很多。
+
+## 参考
+
+- AOP in Spring Boot, is it a JDK dynamic proxy or a Cglib dynamic proxy?:
+- Spring Proxying Mechanisms:
diff --git a/docs/system-design/framework/spring/spring-boot-auto-assembly-principles.md b/docs/system-design/framework/spring/spring-boot-auto-assembly-principles.md
index 15da09e634a..147fe81ed58 100644
--- a/docs/system-design/framework/spring/spring-boot-auto-assembly-principles.md
+++ b/docs/system-design/framework/spring/spring-boot-auto-assembly-principles.md
@@ -1,8 +1,13 @@
---
title: SpringBoot 自动装配原理详解
+description: SpringBoot自动装配原理深度解析,详解@EnableAutoConfiguration、SpringFactories加载机制及条件注解工作原理。
category: 框架
tag:
- SpringBoot
+head:
+ - - meta
+ - name: keywords
+ content: Spring Boot自动装配,AutoConfiguration,EnableAutoConfiguration,SpringFactories,条件注解,Starter,Spring Boot原理
---
> 作者:[Miki-byte-1024](https://github.com/Miki-byte-1024) & [Snailclimb](https://github.com/Snailclimb)
diff --git a/docs/system-design/framework/spring/spring-common-annotations.md b/docs/system-design/framework/spring/spring-common-annotations.md
index 521d7cd4621..3a2b006c8ea 100644
--- a/docs/system-design/framework/spring/spring-common-annotations.md
+++ b/docs/system-design/framework/spring/spring-common-annotations.md
@@ -1,26 +1,29 @@
---
title: Spring&SpringBoot常用注解总结
+description: Spring和SpringBoot常用注解大全,涵盖@Autowired、@Component、@RequestMapping等核心注解的用法详解。
category: 框架
tag:
- SpringBoot
- Spring
+head:
+ - - meta
+ - name: keywords
+ content: Spring注解,Spring Boot注解,@SpringBootApplication,@Autowired,@RequestMapping,@Configuration,@Component,常用注解
---
-### 0.前言
-
-可以毫不夸张地说,这篇文章介绍的 Spring/SpringBoot 常用注解基本已经涵盖你工作中遇到的大部分常用的场景。对于每一个注解我都说了具体用法,掌握搞懂,使用 SpringBoot 来开发项目基本没啥大问题了!
+可以毫不夸张地说,这篇文章介绍的 Spring/SpringBoot 常用注解基本已经涵盖你工作中遇到的大部分常用的场景。对于每一个注解本文都提供了具体用法,掌握这些内容后,使用 Spring Boot 来开发项目基本没啥大问题了!
**为什么要写这篇文章?**
-最近看到网上有一篇关于 SpringBoot 常用注解的文章被转载的比较多,我看了文章内容之后属实觉得质量有点低,并且有点会误导没有太多实际使用经验的人(这些人又占据了大多数)。所以,自己索性花了大概 两天时间简单总结一下了。
+最近看到网上有一篇关于 Spring Boot 常用注解的文章被广泛转载,但文章内容存在一些误导性,可能对没有太多实际使用经验的开发者不太友好。于是我花了几天时间总结了这篇文章,希望能够帮助大家更好地理解和使用 Spring 注解。
-**因为我个人的能力和精力有限,如果有任何不对或者需要完善的地方,请帮忙指出!Guide 感激不尽!**
+**因为个人能力和精力有限,如果有任何错误或遗漏,欢迎指正!非常感激!**
-### 1. `@SpringBootApplication`
+## Spring Boot 基础注解
-这里先单独拎出`@SpringBootApplication` 注解说一下,虽然我们一般不会主动去使用它。
+`@SpringBootApplication` 是 Spring Boot 应用的核心注解,通常用于标注主启动类。
-_Guide:这个注解是 Spring Boot 项目的基石,创建 SpringBoot 项目之后会默认在主类加上。_
+示例:
```java
@SpringBootApplication
@@ -31,7 +34,13 @@ public class SpringSecurityJwtGuideApplication {
}
```
-我们可以把 `@SpringBootApplication`看作是 `@Configuration`、`@EnableAutoConfiguration`、`@ComponentScan` 注解的集合。
+我们可以把 `@SpringBootApplication`看作是下面三个注解的组合:
+
+- **`@EnableAutoConfiguration`**:启用 Spring Boot 的自动配置机制。
+- **`@ComponentScan`**:扫描 `@Component`、`@Service`、`@Repository`、`@Controller` 等注解的类。
+- **`@Configuration`**:允许注册额外的 Spring Bean 或导入其他配置类。
+
+源码如下:
```java
package org.springframework.boot.autoconfigure;
@@ -58,87 +67,233 @@ public @interface SpringBootConfiguration {
}
```
-根据 SpringBoot 官网,这三个注解的作用分别是:
+## Spring Bean
-- `@EnableAutoConfiguration`:启用 SpringBoot 的自动配置机制
-- `@ComponentScan`:扫描被`@Component` (`@Repository`,`@Service`,`@Controller`)注解的 bean,注解默认会扫描该类所在的包下所有的类。
-- `@Configuration`:允许在 Spring 上下文中注册额外的 bean 或导入其他配置类
+### 依赖注入(Dependency Injection, DI)
-### 2. Spring Bean 相关
-
-#### 2.1. `@Autowired`
-
-自动导入对象到类中,被注入进的类同样要被 Spring 容器管理比如:Service 类注入到 Controller 类中。
+`@Autowired` 用于自动注入依赖项(即其他 Spring Bean)。它可以标注在构造器、字段、Setter 方法或配置方法上,Spring 容器会自动查找匹配类型的 Bean 并将其注入。
```java
@Service
-public class UserService {
- ......
+public class UserServiceImpl implements UserService {
+ // ...
}
@RestController
-@RequestMapping("/users")
public class UserController {
- @Autowired
- private UserService userService;
- ......
+ // 字段注入
+ @Autowired
+ private UserService userService;
+ // ...
}
```
-#### 2.2. `@Component`,`@Repository`,`@Service`, `@Controller`
+当存在多个相同类型的 Bean 时,`@Autowired` 默认按类型注入可能产生歧义。此时,可以与 `@Qualifier` 结合使用,通过指定 Bean 的名称来精确选择需要注入的实例。
-我们一般使用 `@Autowired` 注解让 Spring 容器帮我们自动装配 bean。要想把类标识成可用于 `@Autowired` 注解自动装配的 bean 的类,可以采用以下注解实现:
+```java
+@Repository("userRepositoryA")
+public class UserRepositoryA implements UserRepository { /* ... */ }
-- `@Component`:通用的注解,可标注任意类为 `Spring` 组件。如果一个 Bean 不知道属于哪个层,可以使用`@Component` 注解标注。
-- `@Repository` : 对应持久层即 Dao 层,主要用于数据库相关操作。
-- `@Service` : 对应服务层,主要涉及一些复杂的逻辑,需要用到 Dao 层。
-- `@Controller` : 对应 Spring MVC 控制层,主要用于接受用户请求并调用 Service 层返回数据给前端页面。
+@Repository("userRepositoryB")
+public class UserRepositoryB implements UserRepository { /* ... */ }
-#### 2.3. `@RestController`
+@Service
+public class UserService {
+ @Autowired
+ @Qualifier("userRepositoryA") // 指定注入名为 "userRepositoryA" 的 Bean
+ private UserRepository userRepository;
+ // ...
+}
+```
-`@RestController`注解是`@Controller`和`@ResponseBody`的合集,表示这是个控制器 bean,并且是将函数的返回值直接填入 HTTP 响应体中,是 REST 风格的控制器。
+`@Primary`同样是为了解决同一类型存在多个 Bean 实例的注入问题。在 Bean 定义时(例如使用 `@Bean` 或类注解)添加 `@Primary` 注解,表示该 Bean 是**首选**的注入对象。当进行 `@Autowired` 注入时,如果没有使用 `@Qualifier` 指定名称,Spring 将优先选择带有 `@Primary` 的 Bean。
-_Guide:现在都是前后端分离,说实话我已经很久没有用过`@Controller`。如果你的项目太老了的话,就当我没说。_
+```java
+@Primary // 将 UserRepositoryA 设为首选注入对象
+@Repository("userRepositoryA")
+public class UserRepositoryA implements UserRepository { /* ... */ }
-单独使用 `@Controller` 不加 `@ResponseBody`的话一般是用在要返回一个视图的情况,这种情况属于比较传统的 Spring MVC 的应用,对应于前后端不分离的情况。`@Controller` +`@ResponseBody` 返回 JSON 或 XML 形式数据
+@Repository("userRepositoryB")
+public class UserRepositoryB implements UserRepository { /* ... */ }
-关于`@RestController` 和 `@Controller`的对比,请看这篇文章:[@RestController vs @Controller](https://mp.weixin.qq.com/s?__biz=Mzg2OTA0Njk0OA==&mid=2247485544&idx=1&sn=3cc95b88979e28fe3bfe539eb421c6d8&chksm=cea247a3f9d5ceb5e324ff4b8697adc3e828ecf71a3468445e70221cce768d1e722085359907&token=1725092312&lang=zh_CN#rd)。
+@Service
+public class UserService {
+ @Autowired // 会自动注入 UserRepositoryA,因为它是 @Primary
+ private UserRepository userRepository;
+ // ...
+}
+```
+
+`@Resource(name="beanName")`是 JSR-250 规范定义的注解,也用于依赖注入。它默认按**名称 (by Name)** 查找 Bean 进行注入,而 `@Autowired`默认按**类型 (by Type)** 。如果未指定 `name` 属性,它会尝试根据字段名或方法名查找,如果找不到,则回退到按类型查找(类似 `@Autowired`)。
+
+`@Resource`只能标注在字段 和 Setter 方法上,不支持构造器注入。
+
+```java
+@Service
+public class UserService {
+ @Resource(name = "userRepositoryA")
+ private UserRepository userRepository;
+ // ...
+}
+```
-#### 2.4. `@Scope`
+### Bean 作用域
-声明 Spring Bean 的作用域,使用方法:
+`@Scope("scopeName")` 定义 Spring Bean 的作用域,即 Bean 实例的生命周期和可见范围。常用的作用域包括:
+
+- **singleton** : IoC 容器中只有唯一的 bean 实例。Spring 中的 bean 默认都是单例的,是对单例设计模式的应用。
+- **prototype** : 每次获取都会创建一个新的 bean 实例。也就是说,连续 `getBean()` 两次,得到的是不同的 Bean 实例。
+- **request** (仅 Web 应用可用): 每一次 HTTP 请求都会产生一个新的 bean(请求 bean),该 bean 仅在当前 HTTP request 内有效。
+- **session** (仅 Web 应用可用) : 每一次来自新 session 的 HTTP 请求都会产生一个新的 bean(会话 bean),该 bean 仅在当前 HTTP session 内有效。
+- **application/global-session** (仅 Web 应用可用):每个 Web 应用在启动时创建一个 Bean(应用 Bean),该 bean 仅在当前应用启动时间内有效。
+- **websocket** (仅 Web 应用可用):每一次 WebSocket 会话产生一个新的 bean。
```java
-@Bean
-@Scope("singleton")
-public Person personSingleton() {
- return new Person();
+@Component
+// 每次获取都会创建新的 PrototypeBean 实例
+@Scope("prototype")
+public class PrototypeBean {
+ // ...
}
```
-**四种常见的 Spring Bean 的作用域:**
+### Bean 注册
+
+Spring 容器需要知道哪些类需要被管理为 Bean。除了使用 `@Bean` 方法显式声明(通常在 `@Configuration` 类中),更常见的方式是使用 Stereotype(构造型) 注解标记类,并配合组件扫描(Component Scanning)机制,让 Spring 自动发现并注册这些类作为 Bean。这些 Bean 后续可以通过 `@Autowired` 等方式注入到其他组件中。
+
+下面是常见的一些注册 Bean 的注解:
+
+- `@Component`:通用的注解,可标注任意类为 `Spring` 组件。如果一个 Bean 不知道属于哪个层,可以使用`@Component` 注解标注。
+- `@Repository` : 对应持久层即 Dao 层,主要用于数据库相关操作。
+- `@Service` : 对应服务层,主要涉及一些复杂的逻辑,需要用到 Dao 层。
+- `@Controller` : 对应 Spring MVC 控制层,主要用于接受用户请求并调用 Service 层返回数据给前端页面。
+- `@RestController`:一个组合注解,等效于 `@Controller` + `@ResponseBody`。它专门用于构建 RESTful Web 服务的控制器。标注了 `@RestController` 的类,其所有处理器方法(handler methods)的返回值都会被自动序列化(通常为 JSON)并写入 HTTP 响应体,而不是被解析为视图名称。
+
+`@Controller` vs `@RestController`:
-- singleton : 唯一 bean 实例,Spring 中的 bean 默认都是单例的。
-- prototype : 每次请求都会创建一个新的 bean 实例。
-- request : 每一次 HTTP 请求都会产生一个新的 bean,该 bean 仅在当前 HTTP request 内有效。
-- session : 每一个 HTTP Session 会产生一个新的 bean,该 bean 仅在当前 HTTP session 内有效。
+- `@Controller`:主要用于传统的 Spring MVC 应用,方法返回值通常是逻辑视图名,需要视图解析器配合渲染页面。如果需要返回数据(如 JSON),则需要在方法上额外添加 `@ResponseBody` 注解。
+- `@RestController`:专为构建返回数据的 RESTful API 设计。类上使用此注解后,所有方法的返回值都会默认被视为响应体内容(相当于每个方法都隐式添加了 `@ResponseBody`),通常用于返回 JSON 或 XML 数据。在现代前后端分离的应用中,`@RestController` 是更常用的选择。
-#### 2.5. `@Configuration`
+关于`@RestController` 和 `@Controller`的对比,请看这篇文章:[@RestController vs @Controller](https://mp.weixin.qq.com/s?__biz=Mzg2OTA0Njk0OA==&mid=2247485544&idx=1&sn=3cc95b88979e28fe3bfe539eb421c6d8&chksm=cea247a3f9d5ceb5e324ff4b8697adc3e828ecf71a3468445e70221cce768d1e722085359907&token=1725092312&lang=zh_CN#rd)。
+
+## 配置
+
+### 声明配置类
-一般用来声明配置类,可以使用 `@Component`注解替代,不过使用`@Configuration`注解声明配置类更加语义化。
+`@Configuration` 主要用于声明一个类是 Spring 的配置类。虽然也可以用 `@Component` 注解替代,但 `@Configuration` 能够更明确地表达该类的用途(定义 Bean),语义更清晰,也便于 Spring 进行特定的处理(例如,通过 CGLIB 代理确保 `@Bean` 方法的单例行为)。
```java
@Configuration
public class AppConfig {
+
+ // @Bean 注解用于在配置类中声明一个 Bean
@Bean
public TransferService transferService() {
return new TransferServiceImpl();
}
+ // 配置类中可以包含一个或多个 @Bean 方法。
}
```
-### 3. 处理常见的 HTTP 请求类型
+### 读取配置信息
+
+在应用程序开发中,我们经常需要管理一些配置信息,例如数据库连接细节、第三方服务(如阿里云 OSS、短信服务、微信认证)的密钥或地址等。通常,这些信息会**集中存放在配置文件**(如 `application.yml` 或 `application.properties`)中,方便管理和修改。
+
+Spring 提供了多种便捷的方式来读取这些配置信息。假设我们有如下 `application.yml` 文件:
+
+```yaml
+wuhan2020: 2020年初武汉爆发了新型冠状病毒,疫情严重,但是,我相信一切都会过去!武汉加油!中国加油!
+
+my-profile:
+ name: Guide哥
+ email: koushuangbwcx@163.com
+
+library:
+ location: 湖北武汉加油中国加油
+ books:
+ - name: 天才基本法
+ description: 二十二岁的林朝夕在父亲确诊阿尔茨海默病这天,得知自己暗恋多年的校园男神裴之即将出国深造的消息——对方考取的学校,恰是父亲当年为她放弃的那所。
+ - name: 时间的秩序
+ description: 为什么我们记得过去,而非未来?时间“流逝”意味着什么?是我们存在于时间之内,还是时间存在于我们之中?卡洛·罗韦利用诗意的文字,邀请我们思考这一亘古难题——时间的本质。
+ - name: 了不起的我
+ description: 如何养成一个新习惯?如何让心智变得更成熟?如何拥有高质量的关系? 如何走出人生的艰难时刻?
+```
+
+下面介绍几种常用的读取配置的方式:
+
+1、`@Value("${property.key}")` 注入配置文件(如 `application.properties` 或 `application.yml`)中的单个属性值。它还支持 Spring 表达式语言 (SpEL),可以实现更复杂的注入逻辑。
+
+```java
+@Value("${wuhan2020}")
+String wuhan2020;
+```
+
+2、`@ConfigurationProperties`可以读取配置信息并与 Bean 绑定,用的更多一些。
+
+```java
+@Component
+@ConfigurationProperties(prefix = "library")
+class LibraryProperties {
+ @NotEmpty
+ private String location;
+ private List books;
+
+ @Setter
+ @Getter
+ @ToString
+ static class Book {
+ String name;
+ String description;
+ }
+ 省略getter/setter
+ ......
+}
+```
+
+你可以像使用普通的 Spring Bean 一样,将其注入到类中使用。
+
+```java
+@Service
+public class LibraryService {
+
+ private final LibraryProperties libraryProperties;
+
+ @Autowired
+ public LibraryService(LibraryProperties libraryProperties) {
+ this.libraryProperties = libraryProperties;
+ }
+
+ public void printLibraryInfo() {
+ System.out.println(libraryProperties);
+ }
+}
+```
+
+### 加载指定的配置文件
+
+`@PropertySource` 注解允许加载自定义的配置文件。适用于需要将部分配置信息独立存储的场景。
+
+```java
+@Component
+@PropertySource("classpath:website.properties")
+
+class WebSite {
+ @Value("${url}")
+ private String url;
+
+ 省略getter/setter
+ ......
+}
+```
+
+**注意**:当使用 `@PropertySource` 时,确保外部文件路径正确,且文件在类路径(classpath)中。
+
+更多内容请查看我的这篇文章:[10 分钟搞定 SpringBoot 如何优雅读取配置文件?](https://mp.weixin.qq.com/s?__biz=Mzg2OTA0Njk0OA==&mid=2247486181&idx=2&sn=10db0ae64ef501f96a5b0dbc4bd78786&chksm=cea2452ef9d5cc384678e456427328600971180a77e40c13936b19369672ca3e342c26e92b50&token=816772476&lang=zh_CN#rd) 。
+
+## MVC
+
+### HTTP 请求
**5 种常见的请求类型:**
@@ -148,9 +303,9 @@ public class AppConfig {
- **DELETE**:从服务器删除特定的资源。举个例子:`DELETE /users/12`(删除编号为 12 的学生)
- **PATCH**:更新服务器上的资源(客户端提供更改的属性,可以看做作是部分更新),使用的比较少,这里就不举例子了。
-#### 3.1. GET 请求
+#### GET 请求
-`@GetMapping("users")` 等价于`@RequestMapping(value="/users",method=RequestMethod.GET)`
+`@GetMapping("users")` 等价于`@RequestMapping(value="/users",method=RequestMethod.GET)`。
```java
@GetMapping("/users")
@@ -159,11 +314,11 @@ public ResponseEntity> getAllUsers() {
}
```
-#### 3.2. POST 请求
+#### POST 请求
-`@PostMapping("users")` 等价于`@RequestMapping(value="/users",method=RequestMethod.POST)`
+`@PostMapping("users")` 等价于`@RequestMapping(value="/users",method=RequestMethod.POST)`。
-关于`@RequestBody`注解的使用,在下面的“前后端传值”这块会讲到。
+`@PostMapping` 通常与 `@RequestBody` 配合,用于接收 JSON 数据并映射为 Java 对象。
```java
@PostMapping("/users")
@@ -172,9 +327,9 @@ public ResponseEntity createUser(@Valid @RequestBody UserCreateRequest use
}
```
-#### 3.3. PUT 请求
+#### PUT 请求
-`@PutMapping("/users/{userId}")` 等价于`@RequestMapping(value="/users/{userId}",method=RequestMethod.PUT)`
+`@PutMapping("/users/{userId}")` 等价于`@RequestMapping(value="/users/{userId}",method=RequestMethod.PUT)`。
```java
@PutMapping("/users/{userId}")
@@ -184,7 +339,7 @@ public ResponseEntity updateUser(@PathVariable(value = "userId") Long user
}
```
-#### 3.4. **DELETE 请求**
+#### DELETE 请求
`@DeleteMapping("/users/{userId}")`等价于`@RequestMapping(value="/users/{userId}",method=RequestMethod.DELETE)`
@@ -195,7 +350,7 @@ public ResponseEntity deleteUser(@PathVariable(value = "userId") Long userId){
}
```
-#### 3.5. **PATCH 请求**
+#### PATCH 请求
一般实际项目中,我们都是 PUT 不够用了之后才用 PATCH 请求去更新数据。
@@ -207,32 +362,40 @@ public ResponseEntity deleteUser(@PathVariable(value = "userId") Long userId){
}
```
-### 4. 前后端传值
+### 参数绑定
-**掌握前后端传值的正确姿势,是你开始 CRUD 的第一步!**
+在处理 HTTP 请求时,Spring MVC 提供了多种注解用于绑定请求参数到方法参数中。以下是常见的参数绑定方式:
-#### 4.1. `@PathVariable` 和 `@RequestParam`
+#### 从 URL 路径中提取参数
-`@PathVariable`用于获取路径参数,`@RequestParam`用于获取查询参数。
-
-举个简单的例子:
+`@PathVariable` 用于从 URL 路径中提取参数。例如:
```java
@GetMapping("/klasses/{klassId}/teachers")
-public List getKlassRelatedTeachers(
- @PathVariable("klassId") Long klassId,
- @RequestParam(value = "type", required = false) String type ) {
-...
+public List getTeachersByClass(@PathVariable("klassId") Long klassId) {
+ return teacherService.findTeachersByClass(klassId);
}
```
-如果我们请求的 url 是:`/klasses/123456/teachers?type=web`
+若请求 URL 为 `/klasses/123/teachers`,则 `klassId = 123`。
+
+#### 绑定查询参数
+
+`@RequestParam` 用于绑定查询参数。例如:
+
+```java
+@GetMapping("/klasses/{klassId}/teachers")
+public List getTeachersByClass(@PathVariable Long klassId,
+ @RequestParam(value = "type", required = false) String type) {
+ return teacherService.findTeachersByClassAndType(klassId, type);
+}
+```
-那么我们服务获取到的数据就是:`klassId=123456,type=web`。
+若请求 URL 为 `/klasses/123/teachers?type=web`,则 `klassId = 123`,`type = web`。
-#### 4.2. `@RequestBody`
+#### 绑定请求体中的 JSON 数据
-用于读取 Request 请求(可能是 POST,PUT,DELETE,GET 请求)的 body 部分并且**Content-Type 为 application/json** 格式的数据,接收到数据之后会自动将数据绑定到 Java 对象上去。系统会使用`HttpMessageConverter`或者自定义的`HttpMessageConverter`将请求的 body 中的 json 字符串转换为 java 对象。
+`@RequestBody` 用于读取 Request 请求(可能是 POST,PUT,DELETE,GET 请求)的 body 部分并且**Content-Type 为 application/json** 格式的数据,接收到数据之后会自动将数据绑定到 Java 对象上去。系统会使用`HttpMessageConverter`或者自定义的`HttpMessageConverter`将请求的 body 中的 json 字符串转换为 java 对象。
我用一个简单的例子来给演示一下基本使用!
@@ -272,134 +435,73 @@ public class UserRegisterRequest {

-👉 需要注意的是:**一个请求方法只可以有一个`@RequestBody`,但是可以有多个`@RequestParam`和`@PathVariable`**。 如果你的方法必须要用两个 `@RequestBody`来接受数据的话,大概率是你的数据库设计或者系统设计出问题了!
-
-### 5. 读取配置信息
-
-**很多时候我们需要将一些常用的配置信息比如阿里云 oss、发送短信、微信认证的相关配置信息等等放到配置文件中。**
-
-**下面我们来看一下 Spring 为我们提供了哪些方式帮助我们从配置文件中读取这些配置信息。**
-
-我们的数据源`application.yml`内容如下:
-
-```yaml
-wuhan2020: 2020年初武汉爆发了新型冠状病毒,疫情严重,但是,我相信一切都会过去!武汉加油!中国加油!
-
-my-profile:
- name: Guide哥
- email: koushuangbwcx@163.com
-
-library:
- location: 湖北武汉加油中国加油
- books:
- - name: 天才基本法
- description: 二十二岁的林朝夕在父亲确诊阿尔茨海默病这天,得知自己暗恋多年的校园男神裴之即将出国深造的消息——对方考取的学校,恰是父亲当年为她放弃的那所。
- - name: 时间的秩序
- description: 为什么我们记得过去,而非未来?时间“流逝”意味着什么?是我们存在于时间之内,还是时间存在于我们之中?卡洛·罗韦利用诗意的文字,邀请我们思考这一亘古难题——时间的本质。
- - name: 了不起的我
- description: 如何养成一个新习惯?如何让心智变得更成熟?如何拥有高质量的关系? 如何走出人生的艰难时刻?
-```
+**注意**:
-#### 5.1. `@Value`(常用)
+- 一个方法只能有一个 `@RequestBody` 参数,但可以有多个 `@PathVariable` 和 `@RequestParam`。
+- 如果需要接收多个复杂对象,建议合并成一个单一对象。
-使用 `@Value("${property}")` 读取比较简单的配置信息:
+## 数据校验
-```java
-@Value("${wuhan2020}")
-String wuhan2020;
-```
-
-#### 5.2. `@ConfigurationProperties`(常用)
-
-通过`@ConfigurationProperties`读取配置信息并与 bean 绑定。
-
-```java
-@Component
-@ConfigurationProperties(prefix = "library")
-class LibraryProperties {
- @NotEmpty
- private String location;
- private List books;
+数据校验是保障系统稳定性和安全性的关键环节。即使在用户界面(前端)已经实施了数据校验,**后端服务仍必须对接收到的数据进行再次校验**。这是因为前端校验可以被轻易绕过(例如,通过开发者工具修改请求或使用 Postman、curl 等 HTTP 工具直接调用 API),恶意或错误的数据可能直接发送到后端。因此,后端校验是防止非法数据、维护数据一致性、确保业务逻辑正确执行的最后一道,也是最重要的一道防线。
- @Setter
- @Getter
- @ToString
- static class Book {
- String name;
- String description;
- }
- 省略getter/setter
- ......
-}
-```
+Bean Validation 是一套定义 JavaBean 参数校验标准的规范 (JSR 303, 349, 380),它提供了一系列注解,可以直接用于 JavaBean 的属性上,从而实现便捷的参数校验。
-你可以像使用普通的 Spring bean 一样,将其注入到类中使用。
+- **JSR 303 (Bean Validation 1.0):** 奠定了基础,引入了核心校验注解(如 `@NotNull`、`@Size`、`@Min`、`@Max` 等),定义了如何通过注解的方式对 JavaBean 的属性进行校验,并支持嵌套对象校验和自定义校验器。
+- **JSR 349 (Bean Validation 1.1):** 在 1.0 基础上进行扩展,例如引入了对方法参数和返回值校验的支持、增强了对分组校验(Group Validation)的处理。
+- **JSR 380 (Bean Validation 2.0):** 拥抱 Java 8 的新特性,并进行了一些改进,例如支持 `java.time` 包中的日期和时间类型、引入了一些新的校验注解(如 `@NotEmpty`, `@NotBlank`等)。
-#### 5.3. `@PropertySource`(不常用)
+Bean Validation 本身只是一套**规范(接口和注解)**,我们需要一个实现了这套规范的**具体框架**来执行校验逻辑。目前,**Hibernate Validator** 是 Bean Validation 规范最权威、使用最广泛的参考实现。
-`@PropertySource`读取指定 properties 文件
+- Hibernate Validator 4.x 实现了 Bean Validation 1.0 (JSR 303)。
+- Hibernate Validator 5.x 实现了 Bean Validation 1.1 (JSR 349)。
+- Hibernate Validator 6.x 及更高版本实现了 Bean Validation 2.0 (JSR 380)。
-```java
-@Component
-@PropertySource("classpath:website.properties")
+在 Spring Boot 项目中使用 Bean Validation 非常方便,这得益于 Spring Boot 的自动配置能力。关于依赖引入,需要注意:
-class WebSite {
- @Value("${url}")
- private String url;
+- 在较早版本的 Spring Boot(通常指 2.3.x 之前)中,`spring-boot-starter-web` 依赖默认包含了 hibernate-validator。因此,只要引入了 Web Starter,就无需额外添加校验相关的依赖。
+- 从 Spring Boot 2.3.x 版本开始,为了更精细化的依赖管理,校验相关的依赖被移出了 spring-boot-starter-web。如果你的项目使用了这些或更新的版本,并且需要 Bean Validation 功能,那么你需要显式地添加 `spring-boot-starter-validation` 依赖:
- 省略getter/setter
- ......
-}
+```xml
+
+ org.springframework.boot
+ spring-boot-starter-validation
+
```
-更多内容请查看我的这篇文章:[《10 分钟搞定 SpringBoot 如何优雅读取配置文件?》](https://mp.weixin.qq.com/s?__biz=Mzg2OTA0Njk0OA==&mid=2247486181&idx=2&sn=10db0ae64ef501f96a5b0dbc4bd78786&chksm=cea2452ef9d5cc384678e456427328600971180a77e40c13936b19369672ca3e342c26e92b50&token=816772476&lang=zh_CN#rd) 。
-
-### 6. 参数校验
-
-**数据的校验的重要性就不用说了,即使在前端对数据进行校验的情况下,我们还是要对传入后端的数据再进行一遍校验,避免用户绕过浏览器直接通过一些 HTTP 工具直接向后端请求一些违法数据。**
-
-**JSR(Java Specification Requests)** 是一套 JavaBean 参数校验的标准,它定义了很多常用的校验注解,我们可以直接将这些注解加在我们 JavaBean 的属性上面,这样就可以在需要校验的时候进行校验了,非常方便!
+
-校验的时候我们实际用的是 **Hibernate Validator** 框架。Hibernate Validator 是 Hibernate 团队最初的数据校验框架,Hibernate Validator 4.x 是 Bean Validation 1.0(JSR 303)的参考实现,Hibernate Validator 5.x 是 Bean Validation 1.1(JSR 349)的参考实现,目前最新版的 Hibernate Validator 6.x 是 Bean Validation 2.0(JSR 380)的参考实现。
+非 SpringBoot 项目需要自行引入相关依赖包,这里不多做讲解,具体可以查看我的这篇文章:[如何在 Spring/Spring Boot 中做参数校验?你需要了解的都在这里!](https://mp.weixin.qq.com/s?__biz=Mzg2OTA0Njk0OA==&mid=2247485783&idx=1&sn=a407f3b75efa17c643407daa7fb2acd6&chksm=cea2469cf9d5cf8afbcd0a8a1c9cc4294d6805b8e01bee6f76bb2884c5bc15478e91459def49&token=292197051&lang=zh_CN#rd)。
-SpringBoot 项目的 spring-boot-starter-web 依赖中已经有 hibernate-validator 包,不需要引用相关依赖。如下图所示(通过 idea 插件—Maven Helper 生成):
+👉 需要注意的是:所有的注解,推荐使用 JSR 注解,即`javax.validation.constraints`,而不是`org.hibernate.validator.constraints`
-**注**:更新版本的 spring-boot-starter-web 依赖中不再有 hibernate-validator 包(如 2.3.11.RELEASE),需要自己引入 `spring-boot-starter-validation` 依赖。
+### 一些常用的字段验证的注解
-
+Bean Validation 规范及其实现(如 Hibernate Validator)提供了丰富的注解,用于声明式地定义校验规则。以下是一些常用的注解及其说明:
-非 SpringBoot 项目需要自行引入相关依赖包,这里不多做讲解,具体可以查看我的这篇文章:《[如何在 Spring/Spring Boot 中做参数校验?你需要了解的都在这里!](https://mp.weixin.qq.com/s?__biz=Mzg2OTA0Njk0OA==&mid=2247485783&idx=1&sn=a407f3b75efa17c643407daa7fb2acd6&chksm=cea2469cf9d5cf8afbcd0a8a1c9cc4294d6805b8e01bee6f76bb2884c5bc15478e91459def49&token=292197051&lang=zh_CN#rd)》。
-
-👉 需要注意的是:**所有的注解,推荐使用 JSR 注解,即`javax.validation.constraints`,而不是`org.hibernate.validator.constraints`**
-
-#### 6.1. 一些常用的字段验证的注解
-
-- `@NotEmpty` 被注释的字符串的不能为 null 也不能为空
-- `@NotBlank` 被注释的字符串非 null,并且必须包含一个非空白字符
-- `@Null` 被注释的元素必须为 null
-- `@NotNull` 被注释的元素必须不为 null
-- `@AssertTrue` 被注释的元素必须为 true
-- `@AssertFalse` 被注释的元素必须为 false
-- `@Pattern(regex=,flag=)`被注释的元素必须符合指定的正则表达式
-- `@Email` 被注释的元素必须是 Email 格式。
-- `@Min(value)`被注释的元素必须是一个数字,其值必须大于等于指定的最小值
-- `@Max(value)`被注释的元素必须是一个数字,其值必须小于等于指定的最大值
-- `@DecimalMin(value)`被注释的元素必须是一个数字,其值必须大于等于指定的最小值
-- `@DecimalMax(value)` 被注释的元素必须是一个数字,其值必须小于等于指定的最大值
-- `@Size(max=, min=)`被注释的元素的大小必须在指定的范围内
-- `@Digits(integer, fraction)`被注释的元素必须是一个数字,其值必须在可接受的范围内
-- `@Past`被注释的元素必须是一个过去的日期
-- `@Future` 被注释的元素必须是一个将来的日期
+- `@NotNull`: 检查被注解的元素(任意类型)不能为 `null`。
+- `@NotEmpty`: 检查被注解的元素(如 `CharSequence`、`Collection`、`Map`、`Array`)不能为 `null` 且其大小/长度不能为 0。注意:对于字符串,`@NotEmpty` 允许包含空白字符的字符串,如 `" "`。
+- `@NotBlank`: 检查被注解的 `CharSequence`(如 `String`)不能为 `null`,并且去除首尾空格后的长度必须大于 0。(即,不能为空白字符串)。
+- `@Null`: 检查被注解的元素必须为 `null`。
+- `@AssertTrue` / `@AssertFalse`: 检查被注解的 `boolean` 或 `Boolean` 类型元素必须为 `true` / `false`。
+- `@Min(value)` / `@Max(value)`: 检查被注解的数字类型(或其字符串表示)的值必须大于等于 / 小于等于指定的 `value`。适用于整数类型(`byte`、`short`、`int`、`long`、`BigInteger` 等)。
+- `@DecimalMin(value)` / `@DecimalMax(value)`: 功能类似 `@Min` / `@Max`,但适用于包含小数的数字类型(`BigDecimal`、`BigInteger`、`CharSequence`、`byte`、`short`、`int`、`long`及其包装类)。 `value` 必须是数字的字符串表示。
+- `@Size(min=, max=)`: 检查被注解的元素(如 `CharSequence`、`Collection`、`Map`、`Array`)的大小/长度必须在指定的 `min` 和 `max` 范围之内(包含边界)。
+- `@Digits(integer=, fraction=)`: 检查被注解的数字类型(或其字符串表示)的值,其整数部分的位数必须 ≤ `integer`,小数部分的位数必须 ≤ `fraction`。
+- `@Pattern(regexp=, flags=)`: 检查被注解的 `CharSequence`(如 `String`)是否匹配指定的正则表达式 (`regexp`)。`flags` 可以指定匹配模式(如不区分大小写)。
+- `@Email`: 检查被注解的 `CharSequence`(如 `String`)是否符合 Email 格式(内置了一个相对宽松的正则表达式)。
+- `@Past` / `@Future`: 检查被注解的日期或时间类型(`java.util.Date`、`java.util.Calendar`、JSR 310 `java.time` 包下的类型)是否在当前时间之前 / 之后。
+- `@PastOrPresent` / `@FutureOrPresent`: 类似 `@Past` / `@Future`,但允许等于当前时间。
- ……
-#### 6.2. 验证请求体(RequestBody)
+### 验证请求体(RequestBody)
+
+当 Controller 方法使用 `@RequestBody` 注解来接收请求体并将其绑定到一个对象时,可以在该参数前添加 `@Valid` 注解来触发对该对象的校验。如果验证失败,它将抛出`MethodArgumentNotValidException`。
```java
@Data
@AllArgsConstructor
@NoArgsConstructor
public class Person {
-
@NotNull(message = "classId 不能为空")
private String classId;
@@ -414,17 +516,12 @@ public class Person {
@Email(message = "email 格式不正确")
@NotNull(message = "email 不能为空")
private String email;
-
}
-```
-我们在需要验证的参数上加上了`@Valid`注解,如果验证失败,它将抛出`MethodArgumentNotValidException`。
-```java
@RestController
@RequestMapping("/api")
public class PersonController {
-
@PostMapping("/person")
public ResponseEntity getPerson(@RequestBody @Valid Person person) {
return ResponseEntity.ok().body(person);
@@ -432,26 +529,45 @@ public class PersonController {
}
```
-#### 6.3. 验证请求参数(Path Variables 和 Request Parameters)
+### 验证请求参数(Path Variables 和 Request Parameters)
+
+对于直接映射到方法参数的简单类型数据(如路径变量 `@PathVariable` 或请求参数 `@RequestParam`),校验方式略有不同:
-**一定一定不要忘记在类上加上 `@Validated` 注解了,这个参数可以告诉 Spring 去校验方法参数。**
+1. **在 Controller 类上添加 `@Validated` 注解**:这个注解是 Spring 提供的(非 JSR 标准),它使得 Spring 能够处理方法级别的参数校验注解。**这是必需步骤。**
+2. **将校验注解直接放在方法参数上**:将 `@Min`, `@Max`, `@Size`, `@Pattern` 等校验注解直接应用于对应的 `@PathVariable` 或 `@RequestParam` 参数。
+
+一定一定不要忘记在类上加上 `@Validated` 注解了,这个参数可以告诉 Spring 去校验方法参数。
```java
@RestController
@RequestMapping("/api")
-@Validated
+@Validated // 关键步骤 1: 必须在类上添加 @Validated
public class PersonController {
@GetMapping("/person/{id}")
- public ResponseEntity getPersonByID(@Valid @PathVariable("id") @Max(value = 5,message = "超过 id 的范围了") Integer id) {
+ public ResponseEntity getPersonByID(
+ @PathVariable("id")
+ @Max(value = 5, message = "ID 不能超过 5") // 关键步骤 2: 校验注解直接放在参数上
+ Integer id
+ ) {
+ // 如果传入的 id > 5,Spring 会在进入方法体前抛出 ConstraintViolationException 异常。
+ // 全局异常处理器同样需要处理此异常。
return ResponseEntity.ok().body(id);
}
+
+ @GetMapping("/person")
+ public ResponseEntity findPersonByName(
+ @RequestParam("name")
+ @NotBlank(message = "姓名不能为空") // 同样适用于 @RequestParam
+ @Size(max = 10, message = "姓名长度不能超过 10")
+ String name
+ ) {
+ return ResponseEntity.ok().body("Found person: " + name);
+ }
}
```
-更多关于如何在 Spring 项目中进行参数校验的内容,请看《[如何在 Spring/Spring Boot 中做参数校验?你需要了解的都在这里!](https://mp.weixin.qq.com/s?__biz=Mzg2OTA0Njk0OA==&mid=2247485783&idx=1&sn=a407f3b75efa17c643407daa7fb2acd6&chksm=cea2469cf9d5cf8afbcd0a8a1c9cc4294d6805b8e01bee6f76bb2884c5bc15478e91459def49&token=292197051&lang=zh_CN#rd)》这篇文章。
-
-### 7. 全局处理 Controller 层异常
+## 全局异常处理
介绍一下我们 Spring 项目必备的全局处理 Controller 层异常。
@@ -482,34 +598,61 @@ public class GlobalExceptionHandler {
1. [SpringBoot 处理异常的几种常见姿势](https://mp.weixin.qq.com/s?__biz=Mzg2OTA0Njk0OA==&mid=2247485568&idx=2&sn=c5ba880fd0c5d82e39531fa42cb036ac&chksm=cea2474bf9d5ce5dcbc6a5f6580198fdce4bc92ef577579183a729cb5d1430e4994720d59b34&token=2133161636&lang=zh_CN#rd)
2. [使用枚举简单封装一个优雅的 Spring Boot 全局异常处理!](https://mp.weixin.qq.com/s?__biz=Mzg2OTA0Njk0OA==&mid=2247486379&idx=2&sn=48c29ae65b3ed874749f0803f0e4d90e&chksm=cea24460f9d5cd769ed53ad7e17c97a7963a89f5350e370be633db0ae8d783c3a3dbd58c70f8&token=1054498516&lang=zh_CN#rd)
-### 8. JPA 相关
+## 事务
+
+在要开启事务的方法上使用`@Transactional`注解即可!
-#### 8.1. 创建表
+```java
+@Transactional(rollbackFor = Exception.class)
+public void save() {
+ ......
+}
-`@Entity`声明一个类对应一个数据库实体。
+```
-`@Table` 设置表名
+我们知道 Exception 分为运行时异常 RuntimeException 和非运行时异常。在`@Transactional`注解中如果不配置`rollbackFor`属性,那么事务只会在遇到`RuntimeException`的时候才会回滚,加上`rollbackFor=Exception.class`,可以让事务在遇到非运行时异常时也回滚。
+
+`@Transactional` 注解一般可以作用在`类`或者`方法`上。
+
+- **作用于类**:当把`@Transactional` 注解放在类上时,表示所有该类的 public 方法都配置相同的事务属性信息。
+- **作用于方法**:当类配置了`@Transactional`,方法也配置了`@Transactional`,方法的事务会覆盖类的事务配置信息。
+
+更多关于 Spring 事务的内容请查看我的这篇文章:[可能是最漂亮的 Spring 事务管理详解](./spring-transaction.md) 。
+
+## JPA
+
+Spring Data JPA 提供了一系列注解和功能,帮助开发者轻松实现 ORM(对象关系映射)。
+
+### 创建表
+
+`@Entity` 用于声明一个类为 JPA 实体类,与数据库中的表映射。`@Table` 指定实体对应的表名。
```java
@Entity
@Table(name = "role")
public class Role {
+
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
+
private String name;
private String description;
- 省略getter/setter......
+
+ // 省略 getter/setter
}
```
-#### 8.2. 创建主键
+### 主键生成策略
-`@Id`:声明一个字段为主键。
+`@Id`声明字段为主键。`@GeneratedValue` 指定主键的生成策略。
-使用`@Id`声明之后,我们还需要定义主键的生成策略。我们可以使用 `@GeneratedValue` 指定主键生成策略。
+JPA 提供了 4 种主键生成策略:
-**1.通过 `@GeneratedValue`直接使用 JPA 内置提供的四种主键生成策略来指定主键生成策略。**
+- **`GenerationType.TABLE`**:通过数据库表生成主键。
+- **`GenerationType.SEQUENCE`**:通过数据库序列生成主键(适用于 Oracle 等数据库)。
+- **`GenerationType.IDENTITY`**:主键自增长(适用于 MySQL 等数据库)。
+- **`GenerationType.AUTO`**:由 JPA 自动选择合适的生成策略(默认策略)。
```java
@Id
@@ -517,51 +660,7 @@ public class Role {
private Long id;
```
-JPA 使用枚举定义了 4 种常见的主键生成策略,如下:
-
-_Guide:枚举替代常量的一种用法_
-
-```java
-public enum GenerationType {
-
- /**
- * 使用一个特定的数据库表格来保存主键
- * 持久化引擎通过关系数据库的一张特定的表格来生成主键,
- */
- TABLE,
-
- /**
- *在某些数据库中,不支持主键自增长,比如Oracle、PostgreSQL其提供了一种叫做"序列(sequence)"的机制生成主键
- */
- SEQUENCE,
-
- /**
- * 主键自增长
- */
- IDENTITY,
-
- /**
- *把主键生成策略交给持久化引擎(persistence engine),
- *持久化引擎会根据数据库在以上三种主键生成 策略中选择其中一种
- */
- AUTO
-}
-
-```
-
-`@GeneratedValue`注解默认使用的策略是`GenerationType.AUTO`
-
-```java
-public @interface GeneratedValue {
-
- GenerationType strategy() default AUTO;
- String generator() default "";
-}
-```
-
-一般使用 MySQL 数据库的话,使用`GenerationType.IDENTITY`策略比较普遍一点(分布式系统的话需要另外考虑使用分布式 ID)。
-
-**2.通过 `@GenericGenerator`声明一个主键策略,然后 `@GeneratedValue`使用这个策略**
+通过 `@GenericGenerator` 声明自定义主键生成策略:
```java
@Id
@@ -578,7 +677,7 @@ private Long id;
private Long id;
```
-jpa 提供的主键生成策略有如下几种:
+JPA 提供的主键生成策略有如下几种:
```java
public class DefaultIdentifierGeneratorFactory
@@ -613,148 +712,112 @@ public class DefaultIdentifierGeneratorFactory
}
```
-#### 8.3. 设置字段类型
-
-`@Column` 声明字段。
+### 字段映射
-**示例:**
+`@Column` 用于指定实体字段与数据库列的映射关系。
-设置属性 userName 对应的数据库字段名为 user_name,长度为 32,非空
+- **`name`**:指定数据库列名。
+- **`nullable`**:指定是否允许为 `null`。
+- **`length`**:设置字段的长度(仅适用于 `String` 类型)。
+- **`columnDefinition`**:指定字段的数据库类型和默认值。
```java
-@Column(name = "user_name", nullable = false, length=32)
+@Column(name = "user_name", nullable = false, length = 32)
private String userName;
-```
-设置字段类型并且加默认值,这个还是挺常用的。
-
-```java
@Column(columnDefinition = "tinyint(1) default 1")
private Boolean enabled;
```
-#### 8.4. 指定不持久化特定字段
+### 忽略字段
-`@Transient`:声明不需要与数据库映射的字段,在保存的时候不需要保存进数据库 。
-
-如果我们想让`secrect` 这个字段不被持久化,可以使用 `@Transient`关键字声明。
+`@Transient` 用于声明不需要持久化的字段。
```java
-@Entity(name="USER")
+@Entity
public class User {
- ......
@Transient
- private String secrect; // not persistent because of @Transient
-
+ private String temporaryField; // 不会映射到数据库表中
}
```
-除了 `@Transient`关键字声明, 还可以采用下面几种方法:
-
-```java
-static String secrect; // not persistent because of static
-final String secrect = "Satish"; // not persistent because of final
-transient String secrect; // not persistent because of transient
-```
+其他不被持久化的字段方式:
-一般使用注解的方式比较多。
+- **`static`**:静态字段不会被持久化。
+- **`final`**:最终字段不会被持久化。
+- **`transient`**:使用 Java 的 `transient` 关键字声明的字段不会被序列化或持久化。
-#### 8.5. 声明大字段
+### 大字段存储
-`@Lob`:声明某个字段为大字段。
+`@Lob` 用于声明大字段(如 `CLOB` 或 `BLOB`)。
```java
@Lob
-private String content;
-```
-
-更详细的声明:
-
-```java
-@Lob
-//指定 Lob 类型数据的获取策略, FetchType.EAGER 表示非延迟加载,而 FetchType.LAZY 表示延迟加载 ;
-@Basic(fetch = FetchType.EAGER)
-//columnDefinition 属性指定数据表对应的 Lob 字段类型
@Column(name = "content", columnDefinition = "LONGTEXT NOT NULL")
private String content;
```
-#### 8.6. 创建枚举类型的字段
+### 枚举类型映射
-可以使用枚举类型的字段,不过枚举字段要用`@Enumerated`注解修饰。
+`@Enumerated` 用于将枚举类型映射为数据库字段。
+
+- **`EnumType.ORDINAL`**:存储枚举的序号(默认)。
+- **`EnumType.STRING`**:存储枚举的名称(推荐)。
```java
public enum Gender {
- MALE("男性"),
- FEMALE("女性");
-
- private String value;
- Gender(String str){
- value=str;
- }
+ MALE,
+ FEMALE
}
-```
-```java
@Entity
-@Table(name = "role")
-public class Role {
- @Id
- @GeneratedValue(strategy = GenerationType.IDENTITY)
- private Long id;
- private String name;
- private String description;
+public class User {
+
@Enumerated(EnumType.STRING)
private Gender gender;
- 省略getter/setter......
}
```
-数据库里面对应存储的是 MALE/FEMALE。
+数据库中存储的值为 `MALE` 或 `FEMALE`。
+
+### 审计功能
-#### 8.7. 增加审计功能
+通过 JPA 的审计功能,可以在实体中自动记录创建时间、更新时间、创建人和更新人等信息。
-只要继承了 `AbstractAuditBase`的类都会默认加上下面四个字段。
+审计基类:
```java
@Data
-@AllArgsConstructor
-@NoArgsConstructor
@MappedSuperclass
-@EntityListeners(value = AuditingEntityListener.class)
+@EntityListeners(AuditingEntityListener.class)
public abstract class AbstractAuditBase {
@CreatedDate
@Column(updatable = false)
- @JsonIgnore
private Instant createdAt;
@LastModifiedDate
- @JsonIgnore
private Instant updatedAt;
@CreatedBy
@Column(updatable = false)
- @JsonIgnore
private String createdBy;
@LastModifiedBy
- @JsonIgnore
private String updatedBy;
}
-
```
-我们对应的审计功能对应地配置类可能是下面这样的(Spring Security 项目):
+配置审计功能:
```java
-
@Configuration
@EnableJpaAuditing
-public class AuditSecurityConfiguration {
+public class AuditConfig {
+
@Bean
- AuditorAware auditorAware() {
+ public AuditorAware auditorProvider() {
return () -> Optional.ofNullable(SecurityContextHolder.getContext())
.map(SecurityContext::getAuthentication)
.filter(Authentication::isAuthenticated)
@@ -766,101 +829,101 @@ public class AuditSecurityConfiguration {
简单介绍一下上面涉及到的一些注解:
1. `@CreatedDate`: 表示该字段为创建时间字段,在这个实体被 insert 的时候,会设置值
-2. `@CreatedBy` :表示该字段为创建人,在这个实体被 insert 的时候,会设置值
-
- `@LastModifiedDate`、`@LastModifiedBy`同理。
+2. `@CreatedBy` :表示该字段为创建人,在这个实体被 insert 的时候,会设置值 `@LastModifiedDate`、`@LastModifiedBy`同理。
+3. `@EnableJpaAuditing`:开启 JPA 审计功能。
-`@EnableJpaAuditing`:开启 JPA 审计功能。
+### 修改和删除操作
-#### 8.8. 删除/修改数据
-
-`@Modifying` 注解提示 JPA 该操作是修改操作,注意还要配合`@Transactional`注解使用。
+`@Modifying` 注解用于标识修改或删除操作,必须与 `@Transactional` 一起使用。
```java
@Repository
-public interface UserRepository extends JpaRepository {
+public interface UserRepository extends JpaRepository {
@Modifying
- @Transactional(rollbackFor = Exception.class)
+ @Transactional
void deleteByUserName(String userName);
}
```
-#### 8.9. 关联关系
+### 关联关系
-- `@OneToOne` 声明一对一关系
-- `@OneToMany` 声明一对多关系
-- `@ManyToOne` 声明多对一关系
-- `@ManyToMany` 声明多对多关系
+JPA 提供了 4 种关联关系的注解:
-更多关于 Spring Boot JPA 的文章请看我的这篇文章:[一文搞懂如何在 Spring Boot 正确中使用 JPA](https://mp.weixin.qq.com/s?__biz=Mzg2OTA0Njk0OA==&mid=2247485689&idx=1&sn=061b32c2222869932be5631fb0bb5260&chksm=cea24732f9d5ce24a356fb3675170e7843addbfcc79ee267cfdb45c83fc7e90babf0f20d22e1&token=292197051&lang=zh_CN#rd) 。
+- **`@OneToOne`**:一对一关系。
+- **`@OneToMany`**:一对多关系。
+- **`@ManyToOne`**:多对一关系。
+- **`@ManyToMany`**:多对多关系。
-### 9. 事务 `@Transactional`
+```java
+@Entity
+public class User {
-在要开启事务的方法上使用`@Transactional`注解即可!
+ @OneToOne
+ private Profile profile;
-```java
-@Transactional(rollbackFor = Exception.class)
-public void save() {
- ......
+ @OneToMany(mappedBy = "user")
+ private List orders;
}
-
```
-我们知道 Exception 分为运行时异常 RuntimeException 和非运行时异常。在`@Transactional`注解中如果不配置`rollbackFor`属性,那么事务只会在遇到`RuntimeException`的时候才会回滚,加上`rollbackFor=Exception.class`,可以让事务在遇到非运行时异常时也回滚。
-
-`@Transactional` 注解一般可以作用在`类`或者`方法`上。
-
-- **作用于类**:当把`@Transactional` 注解放在类上时,表示所有该类的 public 方法都配置相同的事务属性信息。
-- **作用于方法**:当类配置了`@Transactional`,方法也配置了`@Transactional`,方法的事务会覆盖类的事务配置信息。
+## JSON 数据处理
-更多关于 Spring 事务的内容请查看我的这篇文章:[可能是最漂亮的 Spring 事务管理详解](./spring-transaction.md) 。
+在 Web 开发中,经常需要处理 Java 对象与 JSON 格式之间的转换。Spring 通常集成 Jackson 库来完成此任务,以下是一些常用的 Jackson 注解,可以帮助我们定制化 JSON 的序列化(Java 对象转 JSON)和反序列化(JSON 转 Java 对象)过程。
-### 10. json 数据处理
+### 过滤 JSON 字段
-#### 10.1. 过滤 json 数据
+有时我们不希望 Java 对象的某些字段被包含在最终生成的 JSON 中,或者在将 JSON 转换为 Java 对象时不处理某些 JSON 属性。
-**`@JsonIgnoreProperties` 作用在类上用于过滤掉特定字段不返回或者不解析。**
+`@JsonIgnoreProperties` 作用在类上用于过滤掉特定字段不返回或者不解析。
```java
-//生成json时将userRoles属性过滤
+// 在生成 JSON 时忽略 userRoles 属性
+// 如果允许未知属性(即 JSON 中有而类中没有的属性),可以添加 ignoreUnknown = true
@JsonIgnoreProperties({"userRoles"})
public class User {
-
private String userName;
private String fullName;
private String password;
private List userRoles = new ArrayList<>();
+ // getters and setters...
}
```
-**`@JsonIgnore`一般用于类的属性上,作用和上面的`@JsonIgnoreProperties` 一样。**
+`@JsonIgnore`作用于字段或`getter/setter` 方法级别,用于指定在序列化或反序列化时忽略该特定属性。
```java
-
public class User {
-
private String userName;
private String fullName;
private String password;
- //生成json时将userRoles属性过滤
+
+ // 在生成 JSON 时忽略 userRoles 属性
@JsonIgnore
private List userRoles = new ArrayList<>();
+ // getters and setters...
}
```
-#### 10.2. 格式化 json 数据
+`@JsonIgnoreProperties` 更适用于在类定义时明确排除多个字段,或继承场景下的字段排除;`@JsonIgnore` 则更直接地用于标记单个具体字段。
-`@JsonFormat`一般用来格式化 json 数据。
+### 格式化 JSON 数据
+
+`@JsonFormat` 用于指定属性在序列化和反序列化时的格式。常用于日期时间类型的格式化。
比如:
```java
-@JsonFormat(shape=JsonFormat.Shape.STRING, pattern="yyyy-MM-dd'T'HH:mm:ss.SSS'Z'", timezone="GMT")
+// 指定 Date 类型序列化为 ISO 8601 格式字符串,并设置时区为 GMT
+@JsonFormat(shape = JsonFormat.Shape.STRING, pattern = "yyyy-MM-dd'T'HH:mm:ss.SSS'Z'", timezone = "GMT")
private Date date;
```
-#### 10.3. 扁平化对象
+### 扁平化 JSON 对象
+
+`@JsonUnwrapped` 注解作用于字段上,用于在序列化时将其嵌套对象的属性“提升”到当前对象的层级,反序列化时执行相反操作。这可以使 JSON 结构更扁平。
+
+假设有 `Account` 类,包含 `Location` 和 `PersonInfo` 两个嵌套对象。
```java
@Getter
@@ -888,7 +951,7 @@ public class Account {
```
-未扁平化之前:
+未扁平化之前的 JSON 结构:
```json
{
@@ -903,7 +966,7 @@ public class Account {
}
```
-使用`@JsonUnwrapped` 扁平对象之后:
+使用`@JsonUnwrapped` 扁平对象:
```java
@Getter
@@ -918,6 +981,8 @@ public class Account {
}
```
+扁平化后的 JSON 结构:
+
```json
{
"provinceName": "湖北",
@@ -927,36 +992,37 @@ public class Account {
}
```
-### 11. 测试相关
+## 测试
-**`@ActiveProfiles`一般作用于测试类上, 用于声明生效的 Spring 配置文件。**
+`@ActiveProfiles`一般作用于测试类上, 用于声明生效的 Spring 配置文件。
```java
-@SpringBootTest(webEnvironment = RANDOM_PORT)
+// 指定在 RANDOM_PORT 上启动应用上下文,并激活 "test" profile
+@SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT)
@ActiveProfiles("test")
@Slf4j
public abstract class TestBase {
- ......
+ // Common test setup or abstract methods...
}
```
-**`@Test`声明一个方法为测试方法**
+`@Test` 是 JUnit 框架(通常是 JUnit 5 Jupiter)提供的注解,用于标记一个方法为测试方法。虽然不是 Spring 自身的注解,但它是执行单元测试和集成测试的基础。
-**`@Transactional`被声明的测试方法的数据会回滚,避免污染测试数据。**
+`@Transactional`被声明的测试方法的数据会回滚,避免污染测试数据。
-**`@WithMockUser` Spring Security 提供的,用来模拟一个真实用户,并且可以赋予权限。**
+`@WithMockUser` 是 Spring Security Test 模块提供的注解,用于在测试期间模拟一个已认证的用户。可以方便地指定用户名、密码、角色(authorities)等信息,从而测试受安全保护的端点或方法。
```java
+public class MyServiceTest extends TestBase { // Assuming TestBase provides Spring context
+
@Test
- @Transactional
- @WithMockUser(username = "user-id-18163138155", authorities = "ROLE_TEACHER")
- void should_import_student_success() throws Exception {
- ......
+ @Transactional // 测试数据将回滚
+ @WithMockUser(username = "test-user", authorities = { "ROLE_TEACHER", "read" }) // 模拟一个名为 "test-user",拥有 TEACHER 角色和 read 权限的用户
+ void should_perform_action_requiring_teacher_role() throws Exception {
+ // ... 测试逻辑 ...
+ // 这里可以调用需要 "ROLE_TEACHER" 权限的服务方法
}
+}
```
-_暂时总结到这里吧!虽然花了挺长时间才写完,不过可能还是会一些常用的注解的被漏掉,所以,我将文章也同步到了 Github 上去,Github 地址: 欢迎完善!_
-
-本文已经收录进我的 75K Star 的 Java 开源项目 JavaGuide:[https://github.com/Snailclimb/JavaGuide](https://github.com/Snailclimb/JavaGuide)。
-
diff --git a/docs/system-design/framework/spring/spring-design-patterns-summary.md b/docs/system-design/framework/spring/spring-design-patterns-summary.md
index 9efa5caa3a4..8f43630691e 100644
--- a/docs/system-design/framework/spring/spring-design-patterns-summary.md
+++ b/docs/system-design/framework/spring/spring-design-patterns-summary.md
@@ -1,8 +1,13 @@
---
title: Spring 中的设计模式详解
+description: Spring框架设计模式详解,涵盖工厂模式、代理模式、单例模式、模板方法等在Spring源码中的应用实践。
category: 框架
tag:
- Spring
+head:
+ - - meta
+ - name: keywords
+ content: Spring设计模式,工厂模式,代理模式,模板方法,单例,策略模式,适配器模式,Spring源码
---
“JDK 中用到了哪些设计模式? Spring 中用到了哪些设计模式? ”这两个问题,在面试中比较常见。
@@ -130,7 +135,7 @@ public Object getSingleton(String beanName, ObjectFactory> singletonFactory) {
**AOP(Aspect-Oriented Programming,面向切面编程)** 能够将那些与业务无关,却为业务模块所共同调用的逻辑或责任(例如事务处理、日志管理、权限控制等)封装起来,便于减少系统的重复代码,降低模块间的耦合度,并有利于未来的可拓展性和可维护性。
-**Spring AOP 就是基于动态代理的**,如果要代理的对象,实现了某个接口,那么 Spring AOP 会使用 **JDK Proxy** 去创建代理对象,而对于没有实现接口的对象,就无法使用 JDK Proxy 去进行代理了,这时候 Spring AOP 会使用 **Cglib** 生成一个被代理对象的子类来作为代理,如下图所示:
+Spring AOP 就是基于动态代理的,如果要代理的对象,实现了某个接口,那么 Spring AOP 会使用 **JDK Proxy**,去创建代理对象,而对于没有实现接口的对象,就无法使用 JDK Proxy 去进行代理了,这时候 Spring AOP 会使用 **Cglib** 生成一个被代理对象的子类来作为代理,如下图所示:

@@ -142,7 +147,7 @@ public Object getSingleton(String beanName, ObjectFactory> singletonFactory) {
**Spring AOP 属于运行时增强,而 AspectJ 是编译时增强。** Spring AOP 基于代理(Proxying),而 AspectJ 基于字节码操作(Bytecode Manipulation)。
-Spring AOP 已经集成了 AspectJ ,AspectJ 应该算的上是 Java 生态系统中最完整的 AOP 框架了。AspectJ 相比于 Spring AOP 功能更加强大,但是 Spring AOP 相对来说更简单,
+Spring AOP 已经集成了 AspectJ ,AspectJ 应该算的上是 Java 生态系统中最完整的 AOP 框架了。AspectJ 相比于 Spring AOP 功能更加强大,但是 Spring AOP 相对来说更简单。
如果我们的切面比较少,那么两者性能差异不大。但是,当切面太多的话,最好选择 AspectJ ,它比 Spring AOP 快很多。
@@ -346,7 +351,6 @@ Spring 框架中用到了哪些设计模式?
- 《Spring 技术内幕》
-
--
-
-
-
diff --git a/docs/system-design/framework/spring/spring-knowledge-and-questions-summary.md b/docs/system-design/framework/spring/spring-knowledge-and-questions-summary.md
index ed4abf66ec8..ef65d2b9de5 100644
--- a/docs/system-design/framework/spring/spring-knowledge-and-questions-summary.md
+++ b/docs/system-design/framework/spring/spring-knowledge-and-questions-summary.md
@@ -1,8 +1,13 @@
---
title: Spring常见面试题总结
+description: Spring框架核心面试题详解,涵盖IoC容器、AOP原理、Bean生命周期、依赖注入等Spring核心知识点。
category: 框架
tag:
- Spring
+head:
+ - - meta
+ - name: keywords
+ content: Spring面试题,Spring框架,Bean生命周期,IoC,AOP,依赖注入,事务,Spring常见问题
---
@@ -88,7 +93,7 @@ Spring 团队提倡测试驱动开发(TDD)。有了控制反转 (IoC)的帮
Spring 的测试模块对 JUnit(单元测试框架)、TestNG(类似 JUnit)、Mockito(主要用来 Mock 对象)、PowerMock(解决 Mockito 的问题比如无法模拟 final, static, private 方法)等等常用的测试框架支持的都比较好。
-### Spring,Spring MVC,Spring Boot 之间什么关系?
+### ⭐️Spring,Spring MVC,Spring Boot 之间什么关系?
很多人对 Spring,Spring MVC,Spring Boot 这三者傻傻分不清楚!这里简单介绍一下这三者,其实很简单,没有什么高深的东西。
@@ -110,29 +115,44 @@ Spring Boot 只是简化了配置,如果你需要构建 MVC 架构的 Web 程
## Spring IoC
-### 谈谈自己对于 Spring IoC 的了解
+### ⭐️什么是 IoC?
-**IoC(Inversion of Control:控制反转)** 是一种设计思想,而不是一个具体的技术实现。IoC 的思想就是将原本在程序中手动创建对象的控制权,交由 Spring 框架来管理。不过, IoC 并非 Spring 特有,在其他语言中也有应用。
+IoC (Inversion of Control )即控制反转/反转控制。它是一种思想不是一个技术实现。描述的是:Java 开发领域对象的创建以及管理的问题。
-**为什么叫控制反转?**
+例如:现有类 A 依赖于类 B
-- **控制**:指的是对象创建(实例化、管理)的权力
-- **反转**:控制权交给外部环境(Spring 框架、IoC 容器)
+- **传统的开发方式** :往往是在类 A 中手动通过 new 关键字来 new 一个 B 的对象出来
+- **使用 IoC 思想的开发方式** :不通过 new 关键字来创建对象,而是通过 IoC 容器(Spring 框架) 来帮助我们实例化对象。我们需要哪个对象,直接从 IoC 容器里面去取即可。
-
+从以上两种开发方式的对比来看:我们 “丧失了一个权力” (创建、管理对象的权力),从而也得到了一个好处(不用再考虑对象的创建、管理等一系列的事情)
-将对象之间的相互依赖关系交给 IoC 容器来管理,并由 IoC 容器完成对象的注入。这样可以很大程度上简化应用的开发,把应用从复杂的依赖关系中解放出来。 IoC 容器就像是一个工厂一样,当我们需要创建一个对象的时候,只需要配置好配置文件/注解即可,完全不用考虑对象是如何被创建出来的。
+**为什么叫控制反转?**
-在实际项目中一个 Service 类可能依赖了很多其他的类,假如我们需要实例化这个 Service,你可能要每次都要搞清这个 Service 所有底层类的构造函数,这可能会把人逼疯。如果利用 IoC 的话,你只需要配置好,然后在需要的地方引用就行了,这大大增加了项目的可维护性且降低了开发难度。
+- **控制** :指的是对象创建(实例化、管理)的权力
+- **反转** :控制权交给外部环境(IoC 容器)
-在 Spring 中, IoC 容器是 Spring 用来实现 IoC 的载体, IoC 容器实际上就是个 Map(key,value),Map 中存放的是各种对象。
+
-Spring 时代我们一般通过 XML 文件来配置 Bean,后来开发人员觉得 XML 文件来配置不太好,于是 SpringBoot 注解配置就慢慢开始流行起来。
+### ⭐️IoC 解决了什么问题?
-相关阅读:
+IoC 的思想就是两方之间不互相依赖,由第三方容器来管理相关资源。这样有什么好处呢?
-- [IoC 源码阅读](https://javadoop.com/post/spring-ioc)
-- [IoC & AOP 详解(快速搞懂)](./ioc-and-aop.md)
+1. 对象之间的耦合度或者说依赖程度降低;
+2. 资源变的容易管理;比如你用 Spring 容器提供的话很容易就可以实现一个单例。
+
+例如:现有一个针对 User 的操作,利用 Service 和 Dao 两层结构进行开发
+
+在没有使用 IoC 思想的情况下,Service 层想要使用 Dao 层的具体实现的话,需要通过 new 关键字在`UserServiceImpl` 中手动 new 出 `IUserDao` 的具体实现类 `UserDaoImpl`(不能直接 new 接口类)。
+
+很完美,这种方式也是可以实现的,但是我们想象一下如下场景:
+
+开发过程中突然接到一个新的需求,针对`IUserDao` 接口开发出另一个具体实现类。因为 Server 层依赖了`IUserDao`的具体实现,所以我们需要修改`UserServiceImpl`中 new 的对象。如果只有一个类引用了`IUserDao`的具体实现,可能觉得还好,修改起来也不是很费力气,但是如果有许许多多的地方都引用了`IUserDao`的具体实现的话,一旦需要更换`IUserDao` 的实现方式,那修改起来将会非常的头疼。
+
+
+
+使用 IoC 的思想,我们将对象的控制权(创建、管理)交由 IoC 容器去管理,我们在使用的时候直接向 IoC 容器 “要” 就可以了
+
+
### 什么是 Spring Bean?
@@ -215,39 +235,50 @@ Spring 内置的 `@Autowired` 以及 JDK 内置的 `@Resource` 和 `@Inject` 都
`@Autowired` 和`@Resource`使用的比较多一些。
-### @Autowired 和 @Resource 的区别是什么?
+### ⭐️@Autowired 和 @Resource 的区别是什么?
-`Autowired` 属于 Spring 内置的注解,默认的注入方式为`byType`(根据类型进行匹配),也就是说会优先根据接口类型去匹配并注入 Bean (接口的实现类)。
+`@Autowired` 是 Spring 内置的注解,默认注入逻辑为**先按类型(byType)匹配,若存在多个同类型 Bean,则再尝试按名称(byName)筛选**。
-**这会有什么问题呢?** 当一个接口存在多个实现类的话,`byType`这种方式就无法正确注入对象了,因为这个时候 Spring 会同时找到多个满足条件的选择,默认情况下它自己不知道选择哪一个。
+具体来说:
-这种情况下,注入方式会变为 `byName`(根据名称进行匹配),这个名称通常就是类名(首字母小写)。就比如说下面代码中的 `smsService` 就是我这里所说的名称,这样应该比较好理解了吧。
+1. 优先根据接口 / 类的类型在 Spring 容器中查找匹配的 Bean。若只找到一个符合类型的 Bean,直接注入,无需考虑名称;
+2. 若找到多个同类型的 Bean(例如一个接口有多个实现类),则会尝试通过**属性名或参数名**与 Bean 的名称进行匹配(默认 Bean 名称为类名首字母小写,除非通过 `@Bean(name = "...")` 或 `@Component("...")` 显式指定)。
-```java
-// smsService 就是我们上面所说的名称
-@Autowired
-private SmsService smsService;
-```
+当一个接口存在多个实现类时:
+
+- 若属性名与某个 Bean 的名称一致,则注入该 Bean;
+- 若属性名与所有 Bean 名称都不匹配,会抛出 `NoUniqueBeanDefinitionException`,此时需要通过 `@Qualifier` 显式指定要注入的 Bean 名称。
-举个例子,`SmsService` 接口有两个实现类: `SmsServiceImpl1`和 `SmsServiceImpl2`,且它们都已经被 Spring 容器所管理。
+举例说明:
```java
-// 报错,byName 和 byType 都无法匹配到 bean
+// SmsService 接口有两个实现类:SmsServiceImpl1、SmsServiceImpl2(均被 Spring 管理)
+
+// 报错:byType 匹配到多个 Bean,且属性名 "smsService" 与两个实现类的默认名称(smsServiceImpl1、smsServiceImpl2)都不匹配
@Autowired
private SmsService smsService;
-// 正确注入 SmsServiceImpl1 对象对应的 bean
+
+// 正确:属性名 "smsServiceImpl1" 与实现类 SmsServiceImpl1 的默认名称匹配
@Autowired
private SmsService smsServiceImpl1;
-// 正确注入 SmsServiceImpl1 对象对应的 bean
-// smsServiceImpl1 就是我们上面所说的名称
+
+// 正确:通过 @Qualifier 显式指定 Bean 名称 "smsServiceImpl1"
@Autowired
@Qualifier(value = "smsServiceImpl1")
private SmsService smsService;
```
-我们还是建议通过 `@Qualifier` 注解来显式指定名称而不是依赖变量的名称。
+实际开发实践中,我们还是建议通过 `@Qualifier` 注解来显式指定名称而不是依赖变量的名称。
+
+`@Resource` 源自 **JSR-250** 规范(标准 Java 规范),在 JDK 6 到 JDK 10 中,它确实存在于 JDK 提供的包中。不过,从 JDK 11 开始,它不再默认存在于 JDK 内部,你需要引入额外的依赖 `javax.annotation-api`才能使用。
-`@Resource`属于 JDK 提供的注解,默认注入方式为 `byName`。如果无法通过名称匹配到对应的 Bean 的话,注入方式会变为`byType`。
+Spring 对 `@Resource`(无参数情况)的处理逻辑如下:
+
+1. **按名称(byName)匹配:** 默认取字段名(Field Name)作为 bean 的名称去容器中查找。如果找到了该名称的 Bean,则直接注入。
+2. **回退到按类型(byType)匹配:** 如果**没有**找到同名的 Bean,Spring 会退而求其次,尝试根据字段的**类型**去查找。**按类型匹配的结果判定**
+ - **找到 1 个 Bean**:注入成功。
+ - **找到 0 个 Bean**:抛出异常 (`NoSuchBeanDefinitionException`)。
+ - **找到 >1 个 Bean**:抛出异常 (`NoUniqueBeanDefinitionException`)。
`@Resource` 有两个比较重要且日常开发常用的属性:`name`(名称)、`type`(类型)。
@@ -272,14 +303,88 @@ private SmsService smsServiceImpl1;
private SmsService smsService;
```
-简单总结一下:
+**简单总结一下**:
- `@Autowired` 是 Spring 提供的注解,`@Resource` 是 JDK 提供的注解。
- `Autowired` 默认的注入方式为`byType`(根据类型进行匹配),`@Resource`默认注入方式为 `byName`(根据名称进行匹配)。
- 当一个接口存在多个实现类的情况下,`@Autowired` 和`@Resource`都需要通过名称才能正确匹配到对应的 Bean。`Autowired` 可以通过 `@Qualifier` 注解来显式指定名称,`@Resource`可以通过 `name` 属性来显式指定名称。
- `@Autowired` 支持在构造函数、方法、字段和参数上使用。`@Resource` 主要用于字段和方法上的注入,不支持在构造函数或参数上使用。
-### Bean 的作用域有哪些?
+考虑到 `@Resource` 的语义更清晰(名称优先),并且是 Java 标准,能减少对 Spring 框架的强耦合,我们通常**更推荐使用 `@Resource`**,尤其是在需要按名称注入的场景下。而 `@Autowired` 配合构造器注入,在实现依赖注入的不可变性和强制性方面有优势,也是一种非常好的实践。
+
+### 注入 Bean 的方式有哪些?
+
+依赖注入 (Dependency Injection, DI) 的常见方式:
+
+1. 构造函数注入:通过类的构造函数来注入依赖项。
+1. Setter 注入:通过类的 Setter 方法来注入依赖项。
+1. Field(字段) 注入:直接在类的字段上使用注解(如 `@Autowired` 或 `@Resource`)来注入依赖项。
+
+构造函数注入示例:
+
+```java
+@Service
+public class UserService {
+
+ private final UserRepository userRepository;
+
+ public UserService(UserRepository userRepository) {
+ this.userRepository = userRepository;
+ }
+
+ //...
+}
+```
+
+Setter 注入示例:
+
+```java
+@Service
+public class UserService {
+
+ private UserRepository userRepository;
+
+ // 在 Spring 4.3 及以后的版本,特定情况下 @Autowired 可以省略不写
+ @Autowired
+ public void setUserRepository(UserRepository userRepository) {
+ this.userRepository = userRepository;
+ }
+
+ //...
+}
+```
+
+Field 注入示例:
+
+```java
+@Service
+public class UserService {
+
+ @Autowired
+ private UserRepository userRepository;
+
+ //...
+}
+```
+
+### ⭐️构造函数注入还是 Setter 注入?
+
+Spring 官方有对这个问题的回答:。
+
+我这里主要提取总结完善一下 Spring 官方的建议。
+
+**Spring 官方推荐构造函数注入**,这种注入方式的优势如下:
+
+1. 依赖完整性:确保所有必需依赖在对象创建时就被注入,避免了空指针异常的风险。
+2. 不可变性:有助于创建不可变对象,提高了线程安全性。
+3. 初始化保证:组件在使用前已完全初始化,减少了潜在的错误。
+4. 测试便利性:在单元测试中,可以直接通过构造函数传入模拟的依赖项,而不必依赖 Spring 容器进行注入。
+
+构造函数注入适合处理**必需的依赖项**,而 **Setter 注入** 则更适合**可选的依赖项**,这些依赖项可以有默认值或在对象生命周期中动态设置。虽然 `@Autowired` 可以用于 Setter 方法来处理必需的依赖项,但构造函数注入仍然是更好的选择。
+
+在某些情况下(例如第三方类不提供 Setter 方法),构造函数注入可能是**唯一的选择**。
+
+### ⭐️Bean 的作用域有哪些?
Spring 中 Bean 的作用域通常有下面几种:
@@ -308,7 +413,7 @@ public Person personPrototype() {
}
```
-### Bean 是线程安全的吗?
+### ⭐️Bean 是线程安全的吗?
Spring 框架中的 Bean 是否线程安全,取决于其作用域和状态。
@@ -316,14 +421,70 @@ Spring 框架中的 Bean 是否线程安全,取决于其作用域和状态。
prototype 作用域下,每次获取都会创建一个新的 bean 实例,不存在资源竞争问题,所以不存在线程安全问题。singleton 作用域下,IoC 容器中只有唯一的 bean 实例,可能会存在资源竞争问题(取决于 Bean 是否有状态)。如果这个 bean 是有状态的话,那就存在线程安全问题(有状态 Bean 是指包含可变的成员变量的对象)。
+有状态 Bean 示例:
+
+```java
+// 定义了一个购物车类,其中包含一个保存用户的购物车里商品的 List
+@Component
+public class ShoppingCart {
+ private List items = new ArrayList<>();
+
+ public void addItem(String item) {
+ items.add(item);
+ }
+
+ public List getItems() {
+ return items;
+ }
+}
+```
+
不过,大部分 Bean 实际都是无状态(没有定义可变的成员变量)的(比如 Dao、Service),这种情况下, Bean 是线程安全的。
-对于有状态单例 Bean 的线程安全问题,常见的有两种解决办法:
+无状态 Bean 示例:
-1. 在 Bean 中尽量避免定义可变的成员变量。
-2. 在类中定义一个 `ThreadLocal` 成员变量,将需要的可变成员变量保存在 `ThreadLocal` 中(推荐的一种方式)。
+```java
+// 定义了一个用户服务,它仅包含业务逻辑而不保存任何状态。
+@Component
+public class UserService {
-### Bean 的生命周期了解么?
+ public User findUserById(Long id) {
+ //...
+ }
+ //...
+}
+```
+
+对于有状态单例 Bean 的线程安全问题,常见的三种解决办法是:
+
+1. **避免可变成员变量**: 尽量设计 Bean 为无状态。
+2. **使用`ThreadLocal`**: 将可变成员变量保存在 `ThreadLocal` 中,确保线程独立。
+3. **使用同步机制**: 利用 `synchronized` 或 `ReentrantLock` 来进行同步控制,确保线程安全。
+
+这里以 `ThreadLocal`为例,演示一下`ThreadLocal` 保存用户登录信息的场景:
+
+```java
+public class UserThreadLocal {
+
+ private UserThreadLocal() {}
+
+ private static final ThreadLocal LOCAL = ThreadLocal.withInitial(() -> null);
+
+ public static void put(SysUser sysUser) {
+ LOCAL.set(sysUser);
+ }
+
+ public static SysUser get() {
+ return LOCAL.get();
+ }
+
+ public static void remove() {
+ LOCAL.remove();
+ }
+}
+```
+
+### ⭐️Bean 的生命周期了解么?
1. **创建 Bean 的实例**:Bean 容器首先会找到配置文件中的 Bean 定义,然后使用 Java 反射 API 来创建 Bean 的实例。
2. **Bean 属性赋值/填充**:为 Bean 设置相关属性和依赖,例如`@Autowired` 等注解注入的对象、`@Value` 注入的值、`setter`方法或构造函数注入依赖和值、`@Resource`注入的各种资源。
@@ -432,7 +593,7 @@ public interface InitializingBean {
## Spring AOP
-### 谈谈自己对于 AOP 的了解
+### ⭐️谈谈自己对于 AOP 的了解
AOP(Aspect-Oriented Programming:面向切面编程)能够将那些与业务无关,却为业务模块所共同调用的逻辑或责任(例如事务处理、日志管理、权限控制等)封装起来,便于减少系统的重复代码,降低模块间的耦合度,并有利于未来的可拓展性和可维护性。
@@ -454,15 +615,26 @@ AOP 切面编程涉及到的一些专业术语:
| 切面(Aspect) | 切入点(Pointcut)+通知(Advice) |
| Weaving(织入) | 将通知应用到目标对象,进而生成代理对象的过程动作 |
-### Spring AOP 和 AspectJ AOP 有什么区别?
+### ⭐️Spring AOP 和 AspectJ AOP 有什么区别?
+
+| 特性 | Spring AOP | AspectJ |
+| -------------- | -------------------------------------------------------- | ------------------------------------------ |
+| **增强方式** | 运行时增强(基于动态代理) | 编译时增强、类加载时增强(直接操作字节码) |
+| **切入点支持** | 方法级(Spring Bean 范围内,不支持 final 和 staic 方法) | 方法级、字段、构造器、静态方法等 |
+| **性能** | 运行时依赖代理,有一定开销,切面多时性能较低 | 运行时无代理开销,性能更高 |
+| **复杂性** | 简单,易用,适合大多数场景 | 功能强大,但相对复杂 |
+| **使用场景** | Spring 应用下比较简单的 AOP 需求 | 高性能、高复杂度的 AOP 需求 |
-**Spring AOP 属于运行时增强,而 AspectJ 是编译时增强。** Spring AOP 基于代理(Proxying),而 AspectJ 基于字节码操作(Bytecode Manipulation)。
+**如何选择?**
-Spring AOP 已经集成了 AspectJ ,AspectJ 应该算的上是 Java 生态系统中最完整的 AOP 框架了。AspectJ 相比于 Spring AOP 功能更加强大,但是 Spring AOP 相对来说更简单,
+- **功能考量**:AspectJ 支持更复杂的 AOP 场景,Spring AOP 更简单易用。如果你需要增强 `final` 方法、静态方法、字段访问、构造器调用等,或者需要在非 Spring 管理的对象上应用增强逻辑,AspectJ 是唯一的选择。
+- **性能考量**:切面数量较少时两者性能差异不大,但切面较多时 AspectJ 性能更优。
-如果我们的切面比较少,那么两者性能差异不大。但是,当切面太多的话,最好选择 AspectJ ,它比 Spring AOP 快很多。
+**一句话总结**:简单场景优先使用 Spring AOP;复杂场景或高性能需求时,选择 AspectJ。
-### AspectJ 定义的通知类型有哪些?
+### ⭐️AOP 常见的通知类型有哪些?
+
+
- **Before**(前置通知):目标对象的方法调用之前触发
- **After** (后置通知):目标对象的方法调用之后触发
@@ -551,7 +723,7 @@ MVC 是一种设计模式,Spring MVC 是一款很优秀的 MVC 框架。Spring
- **`Handler`**:**请求处理器**,处理实际请求的处理器。
- **`ViewResolver`**:**视图解析器**,根据 `Handler` 返回的逻辑视图 / 视图,解析并渲染真正的视图,并传递给 `DispatcherServlet` 响应客户端
-### SpringMVC 工作原理了解吗?
+### ⭐️SpringMVC 工作原理了解吗?
**Spring MVC 原理如下图所示:**
@@ -569,6 +741,16 @@ MVC 是一种设计模式,Spring MVC 是一款很优秀的 MVC 框架。Spring
6. `DispaterServlet` 把返回的 `Model` 传给 `View`(视图渲染)。
7. 把 `View` 返回给请求者(浏览器)
+上述流程是传统开发模式(JSP,Thymeleaf 等)的工作原理。然而现在主流的开发方式是前后端分离,这种情况下 Spring MVC 的 `View` 概念发生了一些变化。由于 `View` 通常由前端框架(Vue, React 等)来处理,后端不再负责渲染页面,而是只负责提供数据,因此:
+
+- 前后端分离时,后端通常不再返回具体的视图,而是返回**纯数据**(通常是 JSON 格式),由前端负责渲染和展示。
+- `View` 的部分在前后端分离的场景下往往不需要设置,Spring MVC 的控制器方法只需要返回数据,不再返回 `ModelAndView`,而是直接返回数据,Spring 会自动将其转换为 JSON 格式。相应的,`ViewResolver` 也将不再被使用。
+
+怎么做到呢?
+
+- 使用 `@RestController` 注解代替传统的 `@Controller` 注解,这样所有方法默认会返回 JSON 格式的数据,而不是试图解析视图。
+- 如果你使用的是 `@Controller`,可以结合 `@ResponseBody` 注解来返回 JSON。
+
### 统一异常处理怎么做?
推荐使用注解的方式统一异常处理,具体会使用到 `@ControllerAdvice` + `@ExceptionHandler` 这两个注解 。
@@ -632,7 +814,7 @@ public class GlobalExceptionHandler {
- **适配器模式** : Spring AOP 的增强或通知(Advice)使用到了适配器模式、spring MVC 中也是用到了适配器模式适配`Controller`。
- ……
-## Spring 的循环依赖
+## ⭐️Spring 的循环依赖
### Spring 循环依赖了解吗,怎么解决?
@@ -734,7 +916,7 @@ class B {
- 那么此时就去三级缓存中调用 `getObject()` 方法去获取 A 的 **前期暴露的对象** ,也就是调用上边加入的 `getEarlyBeanReference()` 方法,生成一个 A 的 **前期暴露对象**;
- 然后就将这个 `ObjectFactory` 从三级缓存中移除,并且将前期暴露对象放入到二级缓存中,那么 B 就将这个前期暴露对象注入到依赖,来支持循环依赖。
-**只用两级缓存够吗?** 在没有 AOP 的情况下,确实可以只使用一级和三级缓存来解决循环依赖问题。但是,当涉及到 AOP 时,二级缓存就显得非常重要了,因为它确保了即使在 Bean 的创建过程中有多次对早期引用的请求,也始终只返回同一个代理对象,从而避免了同一个 Bean 有多个代理对象的问题。
+**只用两级缓存够吗?** 在没有 AOP 的情况下,确实可以只使用一级和二级缓存来解决循环依赖问题。但是,当涉及到 AOP 时,三级缓存就显得非常重要了,因为它确保了即使在 Bean 的创建过程中有多次对早期引用的请求,也始终只返回同一个代理对象,从而避免了同一个 Bean 有多个代理对象的问题。
**最后总结一下 Spring 如何解决三级缓存**:
@@ -746,7 +928,7 @@ class B {
`@Lazy` 用来标识类是否需要懒加载/延迟加载,可以作用在类上、方法上、构造器上、方法参数上、成员变量中。
-Spring Boot 2.2 新增了全局懒加载属性,开启后全局 bean 被设置为懒加载,需要时再去创建。
+Spring Boot 2.2 新增了**全局懒加载属性**,开启后全局 bean 被设置为懒加载,需要时再去创建。
配置文件配置全局懒加载:
@@ -773,11 +955,12 @@ springApplication.run(args);
- 由于在 A 上标注了 `@Lazy` 注解,因此 Spring 会去创建一个 B 的代理对象,将这个代理对象注入到 A 中的 B 属性;
- 之后开始执行 B 的实例化、初始化,在注入 B 中的 A 属性时,此时 A 已经创建完毕了,就可以将 A 给注入进去。
-通过 `@Lazy` 就解决了循环依赖的注入, 关键点就在于对 A 中的属性 B 进行注入时,注入的是 B 的代理对象,因此不会循环依赖。
+从上面的加载流程可以看出: `@Lazy` 解决循环依赖的关键点在于代理对象的使用。
-之前说的发生循环依赖是因为在对 A 中的属性 B 进行注入时,注入的是 B 对象,此时又会去初始化 B 对象,发现 B 又依赖了 A,因此才导致的循环依赖。
+- **没有 `@Lazy` 的情况下**:在 Spring 容器初始化 `A` 时会立即尝试创建 `B`,而在创建 `B` 的过程中又会尝试创建 `A`,最终导致循环依赖(即无限递归,最终抛出异常)。
+- **使用 `@Lazy` 的情况下**:Spring 不会立即创建 `B`,而是会注入一个 `B` 的代理对象。由于此时 `B` 仍未被真正初始化,`A` 的初始化可以顺利完成。等到 `A` 实例实际调用 `B` 的方法时,代理对象才会触发 `B` 的真正初始化。
-一般是不建议使用循环依赖的,但是如果项目比较复杂,可以使用 `@Lazy` 解决一部分循环依赖的问题。
+`@Lazy` 能够在一定程度上打破循环依赖链,允许 Spring 容器顺利地完成 Bean 的创建和注入。但这并不是一个根本性的解决方案,尤其是在构造函数注入、复杂的多级依赖等场景中,`@Lazy` 无法有效地解决问题。因此,最佳实践仍然是尽量避免设计上的循环依赖。
### SpringBoot 允许循环依赖发生么?
@@ -789,7 +972,7 @@ SpringBoot 2.6.x 以后,如果你不想重构循环依赖的代码的话,也
- 在导致循环依赖的 Bean 上添加 `@Lazy` 注解,这是一种比较推荐的方式。`@Lazy` 用来标识类是否需要懒加载/延迟加载,可以作用在类上、方法上、构造器上、方法参数上、成员变量中。
- ……
-## Spring 事务
+## ⭐️Spring 事务
关于 Spring 事务的详细介绍,可以看我写的 [Spring 事务详解](https://javaguide.cn/system-design/framework/spring/spring-transaction.html) 这篇文章。
@@ -1002,7 +1185,7 @@ Spring Security 重要的是实战,这里仅对小部分知识点进行总结
可以看看松哥的这篇文章:[Spring Security 中的 hasRole 和 hasAuthority 有区别吗?](https://mp.weixin.qq.com/s/GTNOa2k9_n_H0w24upClRw),介绍的比较详细。
-### 如何对密码进行加密?
+### ⭐️如何对密码进行加密?
如果我们需要保存密码这类敏感数据到数据库的话,需要先加密再保存。
diff --git a/docs/system-design/framework/spring/spring-transaction.md b/docs/system-design/framework/spring/spring-transaction.md
index 47eac108cb8..15dd5a11d90 100644
--- a/docs/system-design/framework/spring/spring-transaction.md
+++ b/docs/system-design/framework/spring/spring-transaction.md
@@ -1,8 +1,13 @@
---
title: Spring 事务详解
+description: Spring事务管理详解,涵盖@Transactional注解、事务传播行为、隔离级别、事务失效场景及回滚规则。
category: 框架
tag:
- Spring
+head:
+ - - meta
+ - name: keywords
+ content: Spring事务,@Transactional,事务传播,隔离级别,事务失效,回滚规则,声明式事务,AOP事务
---
前段时间答应读者的 **Spring 事务** 分析总结终于来了。这部分内容比较重要,不论是对于工作还是面试,但是网上比较好的参考资料比较少。
@@ -419,33 +424,71 @@ Class B {
**3.`TransactionDefinition.PROPAGATION_NESTED`**:
-如果当前存在事务,就在嵌套事务内执行;如果当前没有事务,就执行与`TransactionDefinition.PROPAGATION_REQUIRED`类似的操作。也就是说:
+如果当前存在事务,则创建一个事务作为当前事务的嵌套事务执行; 如果当前没有事务,就执行与`TransactionDefinition.PROPAGATION_REQUIRED`类似的操作。也就是说:
- 在外部方法开启事务的情况下,在内部开启一个新的事务,作为嵌套事务存在。
- 如果外部方法无事务,则单独开启一个事务,与 `PROPAGATION_REQUIRED` 类似。
-这里还是简单举个例子:如果 `bMethod()` 回滚的话,`aMethod()`不会回滚。如果 `aMethod()` 回滚的话,`bMethod()`会回滚。
+`TransactionDefinition.PROPAGATION_NESTED`代表的嵌套事务以父子关系呈现,其核心理念是子事务不会独立提交,依赖于父事务,在父事务中运行;当父事务提交时,子事务也会随着提交,理所当然的,当父事务回滚时,子事务也会回滚;
-```java
-@Service
-Class A {
- @Autowired
- B b;
- @Transactional(propagation = Propagation.REQUIRED)
- public void aMethod {
- //do something
- b.bMethod();
+> 与`TransactionDefinition.PROPAGATION_REQUIRES_NEW`区别于:`PROPAGATION_REQUIRES_NEW`是独立事务,不依赖于外部事务,以平级关系呈现,执行完就会立即提交,与外部事务无关;
+
+子事务也有自己的特性,可以独立进行回滚,不会引发父事务的回滚,但是前提是需要处理子事务的异常,避免异常被父事务感知导致外部事务回滚;
+
+举个例子:
+
+- 如果 `aMethod()` 回滚的话,作为嵌套事务的`bMethod()`会回滚。
+- 如果 `bMethod()` 回滚的话,`aMethod()`是否回滚,要看`bMethod()`的异常是否被处理:
+
+ - `bMethod()`的异常没有被处理,即`bMethod()`内部没有处理异常,且`aMethod()`也没有处理异常,那么`aMethod()`将感知异常致使整体回滚。
+
+ ```java
+ @Service
+ Class A {
+ @Autowired
+ B b;
+ @Transactional(propagation = Propagation.REQUIRED)
+ public void aMethod (){
+ //do something
+ b.bMethod();
+ }
}
-}
-@Service
-Class B {
- @Transactional(propagation = Propagation.NESTED)
- public void bMethod {
- //do something
+ @Service
+ Class B {
+ @Transactional(propagation = Propagation.NESTED)
+ public void bMethod (){
+ //do something and throw an exception
+ }
}
-}
-```
+ ```
+
+ - `bMethod()`处理异常或`aMethod()`处理异常,`aMethod()`不会回滚。
+
+ ```java
+ @Service
+ Class A {
+ @Autowired
+ B b;
+ @Transactional(propagation = Propagation.REQUIRED)
+ public void aMethod (){
+ //do something
+ try {
+ b.bMethod();
+ } catch (Exception e) {
+ System.out.println("方法回滚");
+ }
+ }
+ }
+
+ @Service
+ Class B {
+ @Transactional(propagation = Propagation.NESTED)
+ public void bMethod {
+ //do something and throw an exception
+ }
+ }
+ ```
**4.`TransactionDefinition.PROPAGATION_MANDATORY`**
diff --git a/docs/system-design/framework/spring/springboot-knowledge-and-questions-summary.md b/docs/system-design/framework/spring/springboot-knowledge-and-questions-summary.md
index 7ced5db9da2..98420552f34 100644
--- a/docs/system-design/framework/spring/springboot-knowledge-and-questions-summary.md
+++ b/docs/system-design/framework/spring/springboot-knowledge-and-questions-summary.md
@@ -1,12 +1,21 @@
---
title: SpringBoot常见面试题总结(付费)
+description: SpringBoot核心面试题详解,涵盖自动配置原理、Starter机制、配置文件加载及Actuator监控等核心知识。
category: 框架
tag:
- Spring
+head:
+ - - meta
+ - name: keywords
+ content: Spring Boot面试题,SpringBoot原理,自动配置,Starter,配置文件,Actuator,SpringBoot常见问题
---
**Spring Boot** 相关的面试题为我的[知识星球](https://javaguide.cn/about-the-author/zhishixingqiu-two-years.html)(点击链接即可查看详细介绍以及加入方法)专属内容,已经整理到了[《Java 面试指北》](https://javaguide.cn/zhuanlan/java-mian-shi-zhi-bei.html)中。
+很多 Spring Boot 重要的新特性都已经同步到了这篇文章中,质量很高,保证内容与时俱进!
+
+
+
diff --git a/docs/system-design/framework/spring/springboot-source-code.md b/docs/system-design/framework/spring/springboot-source-code.md
index d39f92c804a..f838dc0a9e1 100644
--- a/docs/system-design/framework/spring/springboot-source-code.md
+++ b/docs/system-design/framework/spring/springboot-source-code.md
@@ -1,8 +1,13 @@
---
title: Spring Boot核心源码解读(付费)
+description: Spring Boot核心源码深度解读,涵盖启动流程、自动配置机制、条件注解及SpringApplication源码分析。
category: 框架
tag:
- Spring
+head:
+ - - meta
+ - name: keywords
+ content: Spring Boot源码,启动流程,自动配置源码,SpringApplication,Bean加载,条件注解,源码解读
---
**Spring Boot 核心源码解读** 为我的[知识星球](https://javaguide.cn/about-the-author/zhishixingqiu-two-years.html)(点击链接即可查看详细介绍以及加入方法)专属内容,已经整理到了[《Java 必读源码系列》](https://javaguide.cn/zhuanlan/source-code-reading.html)中。
diff --git a/docs/system-design/schedule-task.md b/docs/system-design/schedule-task.md
index 99b3d574056..b3a91af8538 100644
--- a/docs/system-design/schedule-task.md
+++ b/docs/system-design/schedule-task.md
@@ -2,13 +2,11 @@
title: Java 定时任务详解
category: 系统设计
icon: "time"
+description: 系统讲解 Java 定时任务与延时任务:Timer、ScheduledThreadPoolExecutor、DelayQueue、时间轮、Spring @Scheduled(Cron 表达式),以及 Quartz、XXL-JOB、ElasticJob、PowerJob 等分布式任务调度框架的选型对比与适用场景(订单超时取消/定时备份/定时抓取)。
head:
- - meta
- name: keywords
content: 定时任务,Quartz,Elastic-Job,XXL-JOB,PowerJob
- - - meta
- - name: description
- content: XXL-JOB 2015 年推出,已经经过了很多年的考验。XXL-JOB 轻量级,并且使用起来非常简单。虽然存在性能瓶颈,但是,在绝大多数情况下,对于企业的基本需求来说是没有影响的。PowerJob 属于分布式任务调度领域里的新星,其稳定性还有待继续考察。ElasticJob 由于在架构设计上是基于 Zookeeper ,而 XXL-JOB 是基于数据库,性能方面的话,ElasticJob 略胜一筹。
---
## 为什么需要定时任务?
@@ -293,7 +291,7 @@ public class TestJob implements SimpleJob {
> Quartz 作为开源作业调度中的佼佼者,是作业调度的首选。但是集群环境中 Quartz 采用 API 的方式对任务进行管理,从而可以避免上述问题,但是同样存在以下问题:
>
-> - 问题一:调用 API 的的方式操作任务,不人性化;
+> - 问题一:调用 API 的方式操作任务,不人性化;
> - 问题二:需要持久化业务 QuartzJobBean 到底层数据表中,系统侵入性相当严重。
> - 问题三:调度逻辑和 QuartzJobBean 耦合在同一个项目中,这将导致一个问题,在调度任务数量逐渐增多,同时调度任务逻辑逐渐加重的情况下,此时调度系统的性能将大大受限于业务;
> - 问题四:quartz 底层以“抢占式”获取 DB 锁并由抢占成功节点负责运行任务,会导致节点负载悬殊非常大;而 XXL-JOB 通过执行器实现“协同分配式”运行任务,充分发挥集群优势,负载各节点均衡。
diff --git a/docs/system-design/security/advantages-and-disadvantages-of-jwt.md b/docs/system-design/security/advantages-and-disadvantages-of-jwt.md
index b177fa0e46e..8033d2a26ca 100644
--- a/docs/system-design/security/advantages-and-disadvantages-of-jwt.md
+++ b/docs/system-design/security/advantages-and-disadvantages-of-jwt.md
@@ -1,8 +1,13 @@
---
title: JWT 身份认证优缺点分析
+description: JWT身份认证优缺点深度分析,讲解JWT无法主动失效、Token续期等问题及对应的解决方案。
category: 系统设计
tag:
- 安全
+head:
+ - - meta
+ - name: keywords
+ content: JWT,Token认证,无状态认证,JWT缺点,刷新令牌,注销失效,安全风险,替代方案
---
校招面试中,遇到大部分的候选者认证登录这块用的都是 JWT。提问 JWT 的概念性问题以及使用 JWT 的原因,基本都能回答一些,但当问到 JWT 存在的一些问题和解决方案时,只有一小部分候选者回答的还可以。
@@ -17,7 +22,7 @@ JWT 不是银弹,也有很多缺陷,很多时候并不是最优的选择。
### 无状态
-JWT 自身包含了身份验证所需要的所有信息,因此,我们的服务器不需要存储 Session 信息。这显然增加了系统的可用性和伸缩性,大大减轻了服务端的压力。
+JWT 自身包含了身份验证所需要的所有信息,因此,我们的服务器不需要存储 JWT 信息。这显然增加了系统的可用性和伸缩性,大大减轻了服务端的压力。
不过,也正是由于 JWT 的无状态,也导致了它最大的缺点:**不可控!**
@@ -147,7 +152,7 @@ JWT 认证的话,我们应该如何解决续签问题呢?查阅了很多资
**2、每次请求都返回新 JWT(不推荐)**
-这种方案的的思路很简单,但是,开销会比较大,尤其是在服务端要存储维护 JWT 的情况下。
+这种方案的思路很简单,但是,开销会比较大,尤其是在服务端要存储维护 JWT 的情况下。
**3、JWT 有效期设置到半夜(不推荐)**
diff --git a/docs/system-design/security/basis-of-authority-certification.md b/docs/system-design/security/basis-of-authority-certification.md
index c21f80568d7..74f7ff646cd 100644
--- a/docs/system-design/security/basis-of-authority-certification.md
+++ b/docs/system-design/security/basis-of-authority-certification.md
@@ -1,8 +1,13 @@
---
title: 认证授权基础概念详解
+description: 认证与授权基础概念详解,讲解Authentication和Authorization的区别、Session、Token、OAuth2等核心知识。
category: 系统设计
tag:
- 安全
+head:
+ - - meta
+ - name: keywords
+ content: 认证,授权,Authentication,Authorization,Session,Token,OAuth2,权限控制,安全基础
---
## 认证 (Authentication) 和授权 (Authorization)的区别是什么?
@@ -137,15 +142,16 @@ public String readAllCookies(HttpServletRequest request) {

1. 用户向服务器发送用户名、密码、验证码用于登陆系统。
-2. 服务器验证通过后,服务器为用户创建一个 `Session`,并将 `Session` 信息存储起来。
-3. 服务器向用户返回一个 `SessionID`,写入用户的 `Cookie`。
-4. 当用户保持登录状态时,`Cookie` 将与每个后续请求一起被发送出去。
-5. 服务器可以将存储在 `Cookie` 上的 `SessionID` 与存储在内存中或者数据库中的 `Session` 信息进行比较,以验证用户的身份,返回给用户客户端响应信息的时候会附带用户当前的状态。
+2. 服务器验证通过后,会为这个用户创建一个专属的 Session 对象(可以理解为服务器上的一块内存,存放该用户的状态数据,如购物车、登录信息等)存储起来,并给这个 Session 分配一个唯一的 `SessionID`。
+3. 服务器通过 HTTP 响应头中的 `Set-Cookie` 指令,把这个 `SessionID` 发送给用户的浏览器。
+4. 浏览器接收到 `SessionID` 后,会将其以 Cookie 的形式保存在本地。当用户保持登录状态时,每次向该服务器发请求,浏览器都会自动带上这个存有 `SessionID` 的 Cookie。
+5. 服务器收到请求后,从 Cookie 中拿出 `SessionID`,就能找到之前保存的那个 Session 对象,从而知道这是哪个用户以及他之前的状态了。
-使用 `Session` 的时候需要注意下面几个点:
+使用 Session 的时候需要注意下面几个点:
-- 依赖 `Session` 的关键业务一定要确保客户端开启了 `Cookie`。
-- 注意 `Session` 的过期时间。
+- **客户端 Cookie 支持**:依赖 Session 的核心功能要确保用户浏览器开启了 Cookie。
+- **Session 过期管理**:合理设置 Session 的过期时间,平衡安全性和用户体验。
+- **Session ID 安全**:为包含 `SessionID` 的 Cookie 设置 `HttpOnly` 标志可以防止客户端脚本(如 JavaScript)窃取,设置 Secure 标志可以保证 `SessionID` 只在 HTTPS 连接下传输,增加安全性。
另外,Spring Session 提供了一种跨多个应用程序或实例管理用户会话信息的机制。如果想详细了解可以查看下面几篇很不错的文章:
diff --git a/docs/system-design/security/data-desensitization.md b/docs/system-design/security/data-desensitization.md
index 83b186a1433..9d59a825b88 100644
--- a/docs/system-design/security/data-desensitization.md
+++ b/docs/system-design/security/data-desensitization.md
@@ -1,8 +1,13 @@
---
title: 数据脱敏方案总结
+description: 数据脱敏方案详解,涵盖手机号、身份证、银行卡等敏感数据的脱敏规则及Hutool工具实现方法。
category: 系统设计
tag:
- 安全
+head:
+ - - meta
+ - name: keywords
+ content: 数据脱敏,隐私保护,手机号脱敏,身份证脱敏,掩码规则,敏感数据,测试数据,合规
---
@@ -368,21 +373,36 @@ FastJSON 实现数据脱敏的方式主要有两种:
- 基于注解 `@JSONField` 实现:需要自定义一个用于脱敏的序列化的类,然后在需要脱敏的字段上通过 `@JSONField` 中的 `serializeUsing` 指定为我们自定义的序列化类型即可。
- 基于序列化过滤器:需要实现 `ValueFilter` 接口,重写 `process` 方法完成自定义脱敏,然后在 JSON 转换时使用自定义的转换策略。具体实现可参考这篇文章: 。
-### Mybatis-mate
+### Mybatis-Mate
-MybatisPlus 也提供了数据脱敏模块 mybatis-mate。mybatis-mate 为 MybatisPlus 企业级模块,使用之前需要配置授权码(付费),旨在更敏捷优雅处理数据。
+先介绍一下 MyBatis、MyBatis-Plus 和 Mybatis-Mate 这三者的关系:
-配置内容如下所示:
+- MyBatis 是一款优秀的持久层框架,它支持定制化 SQL、存储过程以及高级映射。
+- MyBatis-Plus 是一个 MyBatis 的增强工具,能够极大地简化持久层的开发工作。
+- Mybatis-Mate 是为 MyBatis-Plus 提供的企业级模块,旨在更敏捷优雅处理数据。不过,使用之前需要配置授权码(付费)。
-```yaml
-# Mybatis Mate 配置
-mybatis-mate:
- cert:
- grant: jxftsdfggggx
- license: GKXP9r4MCJhGID/DTGigcBcLmZjb1YZGjE4GXaAoxbtGsPC20sxpEtiUr2F7Nb1ANTUekvF6Syo6DzraA4M4oacwoLVTglzfvaEfadfsd232485eLJK1QsskrSJmreMnEaNh9lsV7Lpbxy9JeGCeM0HPEbRvq8Y+8dUt5bQYLklsa3ZIBexir+4XykZY15uqn1pYIp4pEK0+aINTa57xjJNoWuBIqm7BdFIb4l1TAcPYMTsMXhF5hfMmKD2h391HxWTshJ6jbt4YqdKD167AgeoM+B+DE1jxlLjcpskY+kFs9piOS7RCcmKBBUOgX2BD/JxhR2gQ==
-```
+Mybatis-Mate 支持敏感词脱敏,内置手机号、邮箱、银行卡号等 9 种常用脱敏规则。
+
+```java
+@FieldSensitive("testStrategy")
+private String username;
+
+@Configuration
+public class SensitiveStrategyConfig {
+
+ /**
+ * 注入脱敏策略
+ */
+ @Bean
+ public ISensitiveStrategy sensitiveStrategy() {
+ // 自定义 testStrategy 类型脱敏处理
+ return new SensitiveStrategy().addStrategy("testStrategy", t -> t + "***test***");
+ }
+}
-具体实现可参考 baomidou 提供的如下代码: 。
+// 跳过脱密处理,用于编辑场景
+RequestDataTransfer.skipSensitive();
+```
### MyBatis-Flex
@@ -390,16 +410,6 @@ mybatis-mate:
MyBatis-Flex 提供了 `@ColumnMask()` 注解,以及内置的 9 种脱敏规则,开箱即用:
-- 用户名脱敏
-- 手机号脱敏
-- 固定电话脱敏
-- 身份证号脱敏
-- 车牌号脱敏
-- 地址脱敏
-- 邮件脱敏
-- 密码脱敏
-- 银行卡号脱敏
-
```java
/**
* 内置的数据脱敏方式
@@ -465,14 +475,57 @@ public class Account {
如果这些内置的脱敏规则不满足你的要求的话,你还可以自定义脱敏规则。
-## 总结
+1、通过 `MaskManager` 注册新的脱敏规则:
+
+```java
+MaskManager.registerMaskProcessor("自定义规则名称"
+ , data -> {
+ return data;
+ })
+```
-本文主要介绍了数据脱敏的相关内容,首先介绍了数据脱敏的概念,在此基础上介绍了常用的数据脱敏规则;随后介绍了本文的重点 Hutool 工具及其使用方法,在此基础上进行了实操,分别演示了使用 DesensitizedUtil 工具类、配合 Jackson 通过注解的方式完成数据脱敏;最后,介绍了一些常见的数据脱敏方法,并附上了对应的教程链接供大家参考,本文内容如有不当之处,还请大家批评指正。
+2、使用自定义的脱敏规则
+
+```java
+@Table("tb_account")
+public class Account {
+
+ @Id(keyType = KeyType.Auto)
+ private Long id;
+
+ @ColumnMask("自定义规则名称")
+ private String userName;
+}
+```
+
+并且,对于需要跳过脱密处理的场景,例如进入编辑页面编辑用户数据,MyBatis-Flex 也提供了对应的支持:
+
+1. **`MaskManager#execWithoutMask`**(推荐):该方法使用了模版方法设计模式,保障跳过脱敏处理并执行相关逻辑后自动恢复脱敏处理。
+2. **`MaskManager#skipMask`**:跳过脱敏处理。
+3. **`MaskManager#restoreMask`**:恢复脱敏处理,确保后续的操作继续使用脱敏逻辑。
+
+`MaskManager#execWithoutMask`方法实现如下:
+
+```java
+public static T execWithoutMask(Supplier supplier) {
+ try {
+ skipMask();
+ return supplier.get();
+ } finally {
+ restoreMask();
+ }
+}
+```
+
+`MaskManager` 的`skipMask`和`restoreMask`方法一般配套使用,推荐`try{...}finally{...}`模式。
+
+## 总结
-## 推荐阅读
+这篇文章主要介绍了:
-- [Spring Boot 日志、配置文件、接口数据如何脱敏?老鸟们都是这样玩的!](https://mp.weixin.qq.com/s/59osrnjyPJ7BV070x6ABwQ)
-- [大厂也在用的 6 种数据脱敏方案,严防泄露数据的“内鬼”](https://mp.weixin.qq.com/s/_Dgekk1AJsIx0TTlnH6kUA)
+- 数据脱敏的定义:数据脱敏是指对某些敏感信息通过脱敏规则进行数据的变形,实现敏感隐私数据的可靠保护。
+- 常用的脱敏规则:替换、删除、重排、加噪和加密。
+- 常用的脱敏工具:Hutool、Apache ShardingSphere、FastJSON、Mybatis-Mate 和 MyBatis-Flex。
## 参考
diff --git a/docs/system-design/security/data-validation.md b/docs/system-design/security/data-validation.md
new file mode 100644
index 00000000000..2d437e2b062
--- /dev/null
+++ b/docs/system-design/security/data-validation.md
@@ -0,0 +1,208 @@
+---
+title: 为什么前后端都要做数据校验
+description: 前后端数据校验必要性详解,讲解参数校验、权限校验的重要性及防止绕过前端校验的安全防护措施。
+category: 系统设计
+tag:
+ - 安全
+head:
+ - - meta
+ - name: keywords
+ content: 数据校验,前端校验,后端校验,参数校验,权限校验,输入验证,安全防护,防注入
+---
+
+> 相关面试题:
+>
+> - 前端做了校验,后端还还需要做校验吗?
+> - 前端已经做了数据校验,为什么后端还需要再做一遍同样(甚至更严格)的校验呢?
+> - 前端/后端需要对哪些内容进行校验?
+
+咱们平时做 Web 开发,不管是写前端页面还是后端接口,都离不开跟数据打交道。那怎么保证这些传来传去的数据是靠谱的、安全的呢?这就得靠**数据校验**了。而且,这活儿,前端得干,后端**更得干**,还得加上**权限校验**这道重要的“锁”,缺一不可!
+
+为啥这么说?你想啊,前端校验主要是为了用户体验和挡掉一些明显的“瞎填”数据,但懂点技术的人绕过前端校验简直不要太轻松(比如直接用 Postman 之类的工具发请求)。所以,**后端校验才是咱们系统安全和数据准确性的最后一道,也是最硬核的防线**。它得确保进到系统里的数据不仅格式对,还得符合业务规矩,最重要的是,执行这个操作的人得有**权限**!
+
+
+
+## 前端校验
+
+前端校验就像个贴心的门卫,主要目的是在用户填数据的时候,就赶紧告诉他哪儿不对,让他改,省得提交了半天,结果后端说不行,还得重来。这样做的好处显而易见:
+
+1. **用户体验好:** 输入时就有提示,错了马上知道,改起来方便,用户感觉流畅不闹心。
+2. **减轻后端压力:** 把一些明显格式错误、必填项没填的数据在前端就拦下来,减少了发往后端的无效请求,省了服务器资源和网络流量。需要注意的是,后端同样还是要校验,只是加上前端校验可以减少很多无效请求。
+
+那前端一般都得校验点啥呢?
+
+- **必填项校验:** 最基本的,该填的地儿可不能空着。
+- **格式校验:** 比如邮箱得像个邮箱样儿 (xxx@xx.com),手机号得是 11 位数字等。正则表达式这时候就派上用场了。
+- **重复输入校验:** 确保两次输入的内容一致,例如注册时的“确认密码”字段。
+- **范围/长度校验:** 年龄不能是负数吧?密码长度得在 6 到 20 位之间吧?这种都得看着。
+- **合法性/业务校验:** 比如用户名是不是已经被注册了?选的商品还有没有库存?这得根据具体业务来,需要配合后端来做。
+- **文件上传校验:**限制文件类型(如仅支持 `.jpg`、`.png` 格式)和文件大小。
+- **安全性校验:** 防范像 XSS(跨站脚本攻击)这种坏心思,对用户输入的东西做点处理,别让人家写的脚本在咱们页面上跑起来。
+- ...等等,根据业务需求来。
+
+总之,前端校验的核心是 **引导用户正确输入** 和 **提升交互体验**。
+
+## 后端校验
+
+前端校验只是第一道防线,虽然提升了用户体验,但毕竟可以被绕过,真正起决定性作用的是后端校验。后端需要对所有前端传来的数据都抱着“可能有问题”的态度,进行全面审查。后端校验不仅要覆盖前端的基本检查(如格式、范围、长度等),还需要更严格、更深入的验证,确保系统的安全性和数据的一致性。以下是后端校验的重点内容:
+
+1. **完整性校验:** 接口文档中明确要求的字段必须存在,例如 `userId` 和 `orderId`。如果缺失任何必需字段,后端应立即返回错误,拒绝处理请求。
+2. **合法性/存在性校验:** 验证传入的数据是否真实有效。例如,传过来的 `productId` 是否存在于数据库中?`couponId` 是否已经过期或被使用?这通常需要通过查库或调用其他服务来确认。
+3. **一致性校验:** 针对涉及多个数据对象的操作,验证它们是否符合业务逻辑。例如,更新订单状态前,需要确保订单的当前状态允许修改,不能直接从“未支付”跳到“已完成”。一致性校验是保证数据流转正确性的关键。
+4. **安全性校验:** 后端必须防范各种恶意攻击,包括但不限于 XSS、SQL 注入等。所有外部输入都应进行严格的过滤和验证,例如使用参数化查询防止 SQL 注入,或对返回的 HTML 数据进行转义,避免跨站脚本攻击。
+5. ...基本上,前端能做的校验,后端为了安全都得再来一遍。
+
+在 Java 后端,每次都手写 if-else 来做这些基础校验太累了。好在 Java 社区给我们提供了 **Bean Validation** 这套标准规范。它允许我们用**注解**的方式,直接在 JavaBean(比如我们的 DTO 对象)的属性上声明校验规则,非常方便。
+
+- **JSR 303 (1.0):** 打下了基础,引入了 `@NotNull`, `@Size`, `@Min`, `@Max` 这些老朋友。
+- **JSR 349 (1.1):** 增加了对方法参数和返回值的校验,还有分组校验等增强。
+- **JSR 380 (2.0):** 拥抱 Java 8,支持了新的日期时间 API,还加了 `@NotEmpty`, `@NotBlank`, `@Email` 等更实用的注解。
+
+早期的 Spring Boot (大概 2.3.x 之前): spring-boot-starter-web 里自带了 `hibernate-validator`,你啥都不用加。
+
+Spring Boot 2.3.x 及之后: 为了更灵活,校验相关的依赖被单独拎出来了。你需要手动添加 `spring-boot-starter-validation` 依赖:
+
+```xml
+
+ org.springframework.boot
+ spring-boot-starter-validation
+
+```
+
+Bean Validation 规范及其实现(如 Hibernate Validator)提供了丰富的注解,用于声明式地定义校验规则。以下是一些常用的注解及其说明:
+
+- `@NotNull`: 检查被注解的元素(任意类型)不能为 `null`。
+- `@NotEmpty`: 检查被注解的元素(如 `CharSequence`、`Collection`、`Map`、`Array`)不能为 `null` 且其大小/长度不能为 0。注意:对于字符串,`@NotEmpty` 允许包含空白字符的字符串,如 `" "`。
+- `@NotBlank`: 检查被注解的 `CharSequence`(如 `String`)不能为 `null`,并且去除首尾空格后的长度必须大于 0。(即,不能为空白字符串)。
+- `@Null`: 检查被注解的元素必须为 `null`。
+- `@AssertTrue` / `@AssertFalse`: 检查被注解的 `boolean` 或 `Boolean` 类型元素必须为 `true` / `false`。
+- `@Min(value)` / `@Max(value)`: 检查被注解的数字类型(或其字符串表示)的值必须大于等于 / 小于等于指定的 `value`。适用于整数类型(`byte`、`short`、`int`、`long`、`BigInteger` 等)。
+- `@DecimalMin(value)` / `@DecimalMax(value)`: 功能类似 `@Min` / `@Max`,但适用于包含小数的数字类型(`BigDecimal`、`BigInteger`、`CharSequence`、`byte`、`short`、`int`、`long`及其包装类)。 `value` 必须是数字的字符串表示。
+- `@Size(min=, max=)`: 检查被注解的元素(如 `CharSequence`、`Collection`、`Map`、`Array`)的大小/长度必须在指定的 `min` 和 `max` 范围之内(包含边界)。
+- `@Digits(integer=, fraction=)`: 检查被注解的数字类型(或其字符串表示)的值,其整数部分的位数必须 ≤ `integer`,小数部分的位数必须 ≤ `fraction`。
+- `@Pattern(regexp=, flags=)`: 检查被注解的 `CharSequence`(如 `String`)是否匹配指定的正则表达式 (`regexp`)。`flags` 可以指定匹配模式(如不区分大小写)。
+- `@Email`: 检查被注解的 `CharSequence`(如 `String`)是否符合 Email 格式(内置了一个相对宽松的正则表达式)。
+- `@Past` / `@Future`: 检查被注解的日期或时间类型(`java.util.Date`、`java.util.Calendar`、JSR 310 `java.time` 包下的类型)是否在当前时间之前 / 之后。
+- `@PastOrPresent` / `@FutureOrPresent`: 类似 `@Past` / `@Future`,但允许等于当前时间。
+- ……
+
+当 Controller 方法使用 `@RequestBody` 注解来接收请求体并将其绑定到一个对象时,可以在该参数前添加 `@Valid` 注解来触发对该对象的校验。如果验证失败,它将抛出`MethodArgumentNotValidException`。
+
+```java
+@Data
+@AllArgsConstructor
+@NoArgsConstructor
+public class Person {
+ @NotNull(message = "classId 不能为空")
+ private String classId;
+
+ @Size(max = 33)
+ @NotNull(message = "name 不能为空")
+ private String name;
+
+ @Pattern(regexp = "((^Man$|^Woman$|^UGM$))", message = "sex 值不在可选范围")
+ @NotNull(message = "sex 不能为空")
+ private String sex;
+
+ @Email(message = "email 格式不正确")
+ @NotNull(message = "email 不能为空")
+ private String email;
+}
+
+
+@RestController
+@RequestMapping("/api")
+public class PersonController {
+ @PostMapping("/person")
+ public ResponseEntity getPerson(@RequestBody @Valid Person person) {
+ return ResponseEntity.ok().body(person);
+ }
+}
+```
+
+对于直接映射到方法参数的简单类型数据(如路径变量 `@PathVariable` 或请求参数 `@RequestParam`),校验方式略有不同:
+
+1. **在 Controller 类上添加 `@Validated` 注解**:这个注解是 Spring 提供的(非 JSR 标准),它使得 Spring 能够处理方法级别的参数校验注解。**这是必需步骤。**
+2. **将校验注解直接放在方法参数上**:将 `@Min`, `@Max`, `@Size`, `@Pattern` 等校验注解直接应用于对应的 `@PathVariable` 或 `@RequestParam` 参数。
+
+一定一定不要忘记在类上加上 `@Validated` 注解了,这个参数可以告诉 Spring 去校验方法参数。
+
+```java
+@RestController
+@RequestMapping("/api")
+@Validated // 关键步骤 1: 必须在类上添加 @Validated
+public class PersonController {
+
+ @GetMapping("/person/{id}")
+ public ResponseEntity getPersonByID(
+ @PathVariable("id")
+ @Max(value = 5, message = "ID 不能超过 5") // 关键步骤 2: 校验注解直接放在参数上
+ Integer id
+ ) {
+ // 如果传入的 id > 5,Spring 会在进入方法体前抛出 ConstraintViolationException 异常。
+ // 全局异常处理器同样需要处理此异常。
+ return ResponseEntity.ok().body(id);
+ }
+
+ @GetMapping("/person")
+ public ResponseEntity findPersonByName(
+ @RequestParam("name")
+ @NotBlank(message = "姓名不能为空") // 同样适用于 @RequestParam
+ @Size(max = 10, message = "姓名长度不能超过 10")
+ String name
+ ) {
+ return ResponseEntity.ok().body("Found person: " + name);
+ }
+}
+```
+
+Bean Validation 主要解决的是**数据格式、语法层面**的校验。但光有这个还不够。
+
+## 权限校验
+
+数据格式都验过了,没问题。但是,**这个操作,当前登录的这个用户,他有权做吗?** 这就是**权限校验**要解决的问题。比如:
+
+- 普通用户能修改别人的订单吗?(不行)
+- 游客能访问管理员后台接口吗?(不行)
+- 游客能管理其他用户的信息吗?(不行)
+- VIP 用户能使用专属的优惠券吗?(可以)
+- ……
+
+权限校验发生在**数据校验之后**,它关心的是“**谁 (Who)** 能对 **什么资源 (What)** 执行 **什么操作 (Action)**”。
+
+**为啥权限校验这么重要?**
+
+- **安全基石:** 防止未经授权的访问和操作,保护用户数据和系统安全。
+- **业务隔离:** 确保不同角色(管理员、普通用户、VIP 用户等)只能访问和操作其权限范围内的功能。
+- **合规要求:** 很多行业法规对数据访问权限有严格要求。
+
+目前 Java 后端主流的方式是使用成熟的安全框架来实现权限校验,而不是自己手写(容易出错且难以维护)。
+
+1. **Spring Security (业界标准,推荐):** 基于过滤器链(Filter Chain)拦截请求,进行认证(Authentication - 你是谁?)和授权(Authorization - 你能干啥?)。Spring Security 功能强大、社区活跃、与 Spring 生态无缝集成。不过,配置相对复杂,学习曲线较陡峭。
+2. **Apache Shiro:** 另一个流行的安全框架,相对 Spring Security 更轻量级,API 更直观易懂。同样提供认证、授权、会话管理、加密等功能。对于不熟悉 Spring 或觉得 Spring Security 太重的项目,是一个不错的选择。
+3. **Sa-Token:** 国产的轻量级 Java 权限认证框架。支持认证授权、单点登录、踢人下线、自动续签等功能。相比于 Spring Security 和 Shiro 来说,Sa-Token 内置的开箱即用的功能更多,使用也更简单。
+4. **手动检查 (不推荐用于复杂场景):** 在 Service 层或 Controller 层代码里,手动获取当前用户信息(例如从 SecurityContextHolder 或 Session 中),然后 if-else 判断用户角色或权限。权限逻辑与业务逻辑耦合、代码重复、难以维护、容易遗漏。只适用于非常简单的权限场景。
+
+**权限模型简介:**
+
+- **RBAC (Role-Based Access Control):** 基于角色的访问控制。给用户分配角色,给角色分配权限。用户拥有其所有角色的权限总和。这是最常见的模型。
+- **ABAC (Attribute-Based Access Control):** 基于属性的访问控制。决策基于用户属性、资源属性、操作属性和环境属性。更灵活但也更复杂。
+
+一般情况下,绝大部分系统都使用的是 RBAC 权限模型或者其简化版本。用一个图来描述如下:
+
+
+
+关于权限系统设计的详细介绍,可以看这篇文章:[权限系统设计详解](https://javaguide.cn/system-design/security/design-of-authority-system.html)。
+
+## 总结
+
+总而言之,要想构建一个安全、稳定、用户体验好的 Web 应用,前后端数据校验和后端权限校验这三道关卡,都得设好,而且各有侧重:
+
+- **前端数据校验:** 提升用户体验,减少无效请求,是第一道“友好”的防线。
+- **后端数据校验:** 保证数据格式正确、符合业务规则,是防止“脏数据”入库的“技术”防线。 Bean Validation 允许我们用注解的方式,直接在 JavaBean(比如我们的 DTO 对象)的属性上声明校验规则,非常方便。
+- **后端权限校验:** 确保“对的人”做“对的事”,是防止越权操作的“安全”防线。Spring Security、Shiro、Sa-Token 等框架可以帮助我们实现权限校验。
+
+## 参考
+
+- 为什么前后端都需要进行数据校验?:
+- 权限系统设计详解:
diff --git a/docs/system-design/security/design-of-authority-system.md b/docs/system-design/security/design-of-authority-system.md
index ef619abf66c..6263d654bfe 100644
--- a/docs/system-design/security/design-of-authority-system.md
+++ b/docs/system-design/security/design-of-authority-system.md
@@ -1,15 +1,13 @@
---
title: 权限系统设计详解
+description: 基于角色的访问控制(Role-Based Access Control,简称 RBAC)指的是通过用户的角色(Role)授权其相关权限,实现了灵活的访问控制,相比直接授予用户权限,要更加简单、高效、可扩展。
category: 系统设计
tag:
- 安全
head:
- - meta
- name: keywords
- content: 权限系统设计,RBAC,ABAC
- - - meta
- - name: description
- content: 基于角色的访问控制(Role-Based Access Control,简称 RBAC)指的是通过用户的角色(Role)授权其相关权限,实现了灵活的访问控制,相比直接授予用户权限,要更加简单、高效、可扩展。
+ content: 权限系统设计,RBAC,ABAC,用户角色权限,资源权限,权限模型,权限校验,授权系统
---
diff --git a/docs/system-design/security/encryption-algorithms.md b/docs/system-design/security/encryption-algorithms.md
index 7f5e6c932be..3e8591a78cd 100644
--- a/docs/system-design/security/encryption-algorithms.md
+++ b/docs/system-design/security/encryption-algorithms.md
@@ -1,8 +1,14 @@
---
title: 常见加密算法总结
+description: 常见加密算法详解,涵盖AES、RSA等对称与非对称加密算法及MD5、SHA等哈希算法的原理与应用场景。
category: 系统设计
tag:
- 安全
+ - 哈希算法
+head:
+ - - meta
+ - name: keywords
+ content: 加密算法,AES,RSA,哈希算法,摘要算法,HTTPS,对称加密,非对称加密,BCrypt
---
加密算法是一种用数学方法对数据进行变换的技术,目的是保护数据的安全,防止被未经授权的人读取或修改。加密算法可以分为三大类:对称加密算法、非对称加密算法和哈希算法(也叫摘要算法)。
@@ -366,8 +372,8 @@ DSA 算法签名过程:
## 参考
-- 深入理解完美哈希 - 腾讯技术工程:https://mp.weixin.qq.com/s/M8Wcj8sZ7UF1CMr887Puog
-- 写给开发人员的实用密码学(二)—— 哈希函数:https://thiscute.world/posts/practical-cryptography-basics-2-hash/
+- 深入理解完美哈希 - 腾讯技术工程:
+- 写给开发人员的实用密码学(二)—— 哈希函数:
- 奇妙的安全旅行之 DSA 算法:
- AES-GCM 加密简介:
- Java AES 256 GCM Encryption and Decryption Example | JCE Unlimited Strength:
diff --git a/docs/system-design/security/jwt-intro.md b/docs/system-design/security/jwt-intro.md
index cd580a7a597..e006c1805ca 100644
--- a/docs/system-design/security/jwt-intro.md
+++ b/docs/system-design/security/jwt-intro.md
@@ -1,8 +1,13 @@
---
title: JWT 基础概念详解
+description: JWT基础概念详解,涵盖JSON Web Token的组成结构、签名算法、工作原理及在登录鉴权中的应用。
category: 系统设计
tag:
- 安全
+head:
+ - - meta
+ - name: keywords
+ content: JWT,JSON Web Token,Token认证,无状态,Header Payload Signature,签名算法,登录鉴权,CSRF
---
@@ -127,16 +132,17 @@ HMACSHA256(
## 如何基于 JWT 进行身份验证?
-在基于 JWT 进行身份验证的的应用程序中,服务器通过 Payload、Header 和 Secret(密钥)创建 JWT 并将 JWT 发送给客户端。客户端接收到 JWT 之后,会将其保存在 Cookie 或者 localStorage 里面,以后客户端发出的所有请求都会携带这个令牌。
+在基于 JWT 进行身份验证的应用程序中,服务器通过 Payload、Header 和 Secret(密钥)创建 JWT 并将 JWT 发送给客户端。客户端接收到 JWT 之后,会将其保存在 Cookie 或者 localStorage 里面,以后客户端发出的所有请求都会携带这个令牌。

简化后的步骤如下:
-1. 用户向服务器发送用户名、密码以及验证码用于登陆系统。
-2. 如果用户用户名、密码以及验证码校验正确的话,服务端会返回已经签名的 Token,也就是 JWT。
-3. 用户以后每次向后端发请求都在 Header 中带上这个 JWT 。
-4. 服务端检查 JWT 并从中获取用户相关信息。
+1. 用户向服务器发送用户名、密码以及验证码用于登陆系统;
+2. 如果用户用户名、密码以及验证码校验正确的话,服务端会返回已经签名的 Token,也就是 JWT;
+3. 客户端收到 Token 后自己保存起来(比如浏览器的 `localStorage` );
+4. 用户以后每次向后端发请求都在 Header 中带上这个 JWT ;
+5. 服务端检查 JWT 并从中获取用户相关信息。
两点建议:
@@ -162,7 +168,7 @@ HMACSHA256(
3. JWT 存放在 localStorage 中而不是 Cookie 中,避免 CSRF 风险。
4. 一定不要将隐私信息存放在 Payload 当中。
5. 密钥一定保管好,一定不要泄露出去。JWT 安全的核心在于签名,签名安全的核心在密钥。
-6. Payload 要加入 `exp` (JWT 的过期时间),永久有效的 JWT 不合理。并且,JWT 的过期时间不易过长。
+6. Payload 要加入 `exp` (JWT 的过期时间),永久有效的 JWT 不合理。并且,JWT 的过期时间不宜过长。
7. ……
diff --git a/docs/system-design/security/sentive-words-filter.md b/docs/system-design/security/sentive-words-filter.md
index d4e8a53a26c..adbef873278 100644
--- a/docs/system-design/security/sentive-words-filter.md
+++ b/docs/system-design/security/sentive-words-filter.md
@@ -1,8 +1,13 @@
---
title: 敏感词过滤方案总结
+description: 敏感词过滤方案详解,涵盖Trie树、DFA算法等高性能敏感词匹配算法的原理与实现方法。
category: 系统设计
tag:
- 安全
+head:
+ - - meta
+ - name: keywords
+ content: 敏感词过滤,Trie树,DFA算法,字符串匹配,内容安全,关键词过滤,文本审核,高性能匹配
---
系统需要对用户输入的文本进行敏感词过滤如色情、政治、暴力相关的词汇。
diff --git a/docs/system-design/security/sso-intro.md b/docs/system-design/security/sso-intro.md
index b0b00552045..68bf4c135ad 100644
--- a/docs/system-design/security/sso-intro.md
+++ b/docs/system-design/security/sso-intro.md
@@ -1,8 +1,13 @@
---
title: SSO 单点登录详解
+description: SSO单点登录原理详解,涵盖统一认证中心设计、CAS协议、跨域登录实现及登录态同步机制。
category: 系统设计
tag:
- 安全
+head:
+ - - meta
+ - name: keywords
+ content: SSO,单点登录,统一认证,登录态,票据,TGT,ST,CAS协议,跨域登录
---
> 本文授权转载自: 作者:ken.io
diff --git a/docs/system-design/system-design-questions.md b/docs/system-design/system-design-questions.md
index e34d5cc479c..c7bae516676 100644
--- a/docs/system-design/system-design-questions.md
+++ b/docs/system-design/system-design-questions.md
@@ -1,13 +1,26 @@
---
title: 系统设计常见面试题总结(付费)
+description: 系统设计高频面试题解析,涵盖短链系统、秒杀系统、海量数据处理等场景题的设计思路与解决方案。
category: Java面试指北
icon: "design"
+head:
+ - - meta
+ - name: keywords
+ content: 系统设计面试题,场景题,短链系统,秒杀系统,海量数据,限流,缓存,分布式锁,一致性
---
-**系统设计** 相关的面试题为我的[知识星球](https://javaguide.cn/about-the-author/zhishixingqiu-two-years.html)(点击链接即可查看详细介绍以及加入方法)专属内容,已经整理到了[《Java 面试指北》](https://javaguide.cn/zhuanlan/java-mian-shi-zhi-bei.html)中。
+**系统设计** 相关的面试题为我的[知识星球](https://javaguide.cn/about-the-author/zhishixingqiu-two-years.html)(点击链接即可查看详细介绍以及加入方法)专属内容,已经整理到了[《后端面试高频系统设计&场景题》](https://javaguide.cn/zhuanlan/back-end-interview-high-frequency-system-design-and-scenario-questions.html)中。
-
+**《后端面试高频系统设计&场景题》** 包含了常见的系统设计案例比如短链系统、秒杀系统以及高频的场景题比如海量数据去重、第三方授权登录。
-
+
+
+近年来,随着国内的技术面试越来越卷,越来越多的公司开始在面试中考察系统设计和场景问题,以此来更全面的考察求职者,不论是校招还是社招。不过,正常面试全是场景题的情况还是极少的,面试官一般会在面试中穿插一两个系统设计和场景题来考察你。
+
+于是,我总结了这份《后端面试高频系统设计&场景题》,包含了常见的系统设计案例比如短链系统、秒杀系统以及高频的场景题比如海量数据去重、第三方授权登录。
-
+即使不是准备面试,我也强烈推荐你认真阅读这一系列文章,这对于提升自己系统设计思维和解决实际问题的能力还是非常有帮助的。并且,涉及到的很多案例都可以用到自己的项目上比如抽奖系统设计、第三方授权登录、Redis 实现延时任务的正确方式。
+
+《后端面试高频系统设计&场景题》本身是属于[《Java 面试指北》](https://javaguide.cn/zhuanlan/java-mian-shi-zhi-bei.html)的一部分,后面由于内容篇幅较多,因此被单独提了出来。
+
+
diff --git a/docs/system-design/web-real-time-message-push.md b/docs/system-design/web-real-time-message-push.md
index f08e1b2e716..c571f76ce9c 100644
--- a/docs/system-design/web-real-time-message-push.md
+++ b/docs/system-design/web-real-time-message-push.md
@@ -1,14 +1,12 @@
---
title: Web 实时消息推送详解
+description: 消息推送通常是指网站的运营工作等人员,通过某种工具对用户当前网页或移动设备 APP 进行的主动消息推送。
category: 系统设计
icon: "messages"
head:
- - meta
- name: keywords
- content: 消息推送,短轮询,长轮询,SSE,Websocket,MQTT
- - - meta
- - name: description
- content: 消息推送通常是指网站的运营工作等人员,通过某种工具对用户当前网页或移动设备 APP 进行的主动消息推送。
+ content: Web消息推送,实时消息,WebSocket,SSE,长轮询,短轮询,MQTT,实时通信方案
---
> 原文地址: 对本文进行了完善总结。
@@ -221,7 +219,7 @@ SSE 与 WebSocket 作用相似,都可以建立服务端与浏览器之间的
SSE 好像一直不被大家所熟知,一部分原因是出现了 WebSocket,这个提供了更丰富的协议来执行双向、全双工通信。对于游戏、即时通信以及需要双向近乎实时更新的场景,拥有双向通道更具吸引力。
-但是,在某些情况下,不需要从客户端发送数据。而你只需要一些服务器操作的更新。比如:站内信、未读消息数、状态更新、股票行情、监控数量等场景,SEE 不管是从实现的难易和成本上都更加有优势。此外,SSE 具有 WebSocket 在设计上缺乏的多种功能,例如:自动重新连接、事件 ID 和发送任意事件的能力。
+但是,在某些情况下,不需要从客户端发送数据。而你只需要一些服务器操作的更新。比如:站内信、未读消息数、状态更新、股票行情、监控数量等场景,SSE 不管是从实现的难易和成本上都更加有优势。此外,SSE 具有 WebSocket 在设计上缺乏的多种功能,例如:自动重新连接、事件 ID 和发送任意事件的能力。
前端只需进行一次 HTTP 请求,带上唯一 ID,打开事件流,监听服务端推送的事件就可以了
diff --git a/docs/tools/docker/docker-in-action.md b/docs/tools/docker/docker-in-action.md
index 3c7198cf96b..abc1850f97d 100644
--- a/docs/tools/docker/docker-in-action.md
+++ b/docs/tools/docker/docker-in-action.md
@@ -1,8 +1,13 @@
---
title: Docker实战
+description: 通过实战理解 Docker 的镜像与容器管理,解决环境一致性与交付效率问题,提升开发测试部署的协同效率。
category: 开发工具
tag:
- Docker
+head:
+ - - meta
+ - name: keywords
+ content: Docker 实战,镜像构建,容器管理,环境一致性,部署,性能
---
## Docker 介绍
diff --git a/docs/tools/docker/docker-intro.md b/docs/tools/docker/docker-intro.md
index 60270429bfb..426a83c7b53 100644
--- a/docs/tools/docker/docker-intro.md
+++ b/docs/tools/docker/docker-intro.md
@@ -1,8 +1,13 @@
---
title: Docker核心概念总结
+description: 梳理 Docker 的核心概念与容器/虚拟机差异,掌握镜像、容器与仓库的关系及在交付部署中的实际价值。
category: 开发工具
tag:
- Docker
+head:
+ - - meta
+ - name: keywords
+ content: Docker,容器,镜像,仓库,引擎,隔离,虚拟机对比,部署
---
本文只是对 Docker 的概念做了较为详细的介绍,并不涉及一些像 Docker 环境的安装以及 Docker 的一些常见操作和命令。
@@ -83,7 +88,7 @@ tag:
**Docker 思想**:
- **集装箱**:就像海运中的集装箱一样,Docker 容器包含了应用程序及其所有依赖项,确保在任何环境中都能以相同的方式运行。
-- **标准化:**运输方式、存储方式、API 接口。
+- **标准化**:运输方式、存储方式、API 接口。
- **隔离**:每个 Docker 容器都在自己的隔离环境中运行,与宿主机和其他容器隔离。
### Docker 容器的特点
diff --git a/docs/tools/git/git-intro.md b/docs/tools/git/git-intro.md
index c2cf8000570..66de9bdc3da 100644
--- a/docs/tools/git/git-intro.md
+++ b/docs/tools/git/git-intro.md
@@ -1,8 +1,13 @@
---
title: Git核心概念总结
+description: 总结 Git 的核心概念与工作流,涵盖分支与合并、提交管理与冲突解决,助力团队协作与代码质量提升。
category: 开发工具
tag:
- Git
+head:
+ - - meta
+ - name: keywords
+ content: Git,版本控制,分布式,分支,提交,合并,冲突解决,工作流
---
## 版本控制
diff --git a/docs/tools/git/github-tips.md b/docs/tools/git/github-tips.md
index 25df8592c71..a6fea00237a 100644
--- a/docs/tools/git/github-tips.md
+++ b/docs/tools/git/github-tips.md
@@ -1,8 +1,13 @@
---
title: Github实用小技巧总结
+description: 汇总 Github 的高效使用技巧,包括个性化主页、自动简历与统计展示,提升个人品牌与开源协作体验。
category: 开发工具
tag:
- Git
+head:
+ - - meta
+ - name: keywords
+ content: Github 技巧,个人主页,README,统计信息,开源贡献,简历
---
我使用 Github 已经有 6 年多了,今天毫无保留地把自己觉得比较有用的 Github 小技巧送给关注 JavaGuide 的各位小伙伴。
diff --git a/docs/tools/gradle/gradle-core-concepts.md b/docs/tools/gradle/gradle-core-concepts.md
index d699351f315..81ce9a1421d 100644
--- a/docs/tools/gradle/gradle-core-concepts.md
+++ b/docs/tools/gradle/gradle-core-concepts.md
@@ -1,13 +1,11 @@
---
title: Gradle核心概念总结
+description: Gradle 就是一个运行在 JVM 上的自动化的项目构建工具,用来帮助我们自动构建项目。
category: 开发工具
head:
- - meta
- name: keywords
content: Gradle,Groovy,Gradle Wrapper,Gradle 包装器,Gradle 插件
- - - meta
- - name: description
- content: Gradle 就是一个运行在 JVM 上的自动化的项目构建工具,用来帮助我们自动构建项目。
---
> 这部分内容主要根据 Gradle 官方文档整理,做了对应的删减,主要保留比较重要的部分,不涉及实战,主要是一些重要概念的介绍。
@@ -78,7 +76,7 @@ Gradle Wrapper 会给我们带来下面这些好处:
### 生成 Gradle Wrapper
-如果想要生成 Gradle Wrapper 的话,需要本地配置好 Gradle 环境变量。Gradle 中已经内置了内置了 Wrapper Task,在项目根目录执行执行`gradle wrapper`命令即可帮助我们生成 Gradle Wrapper。
+如果想要生成 Gradle Wrapper 的话,需要本地配置好 Gradle 环境变量。Gradle 中已经内置了 Wrapper Task,在项目根目录执行执行`gradle wrapper`命令即可帮助我们生成 Gradle Wrapper。
执行命令 `gradle wrapper` 命令时可以指定一些参数来控制 wrapper 的生成。具体有如下两个配置参数:
diff --git a/docs/tools/maven/maven-best-practices.md b/docs/tools/maven/maven-best-practices.md
index 120c194e772..cce228577d8 100644
--- a/docs/tools/maven/maven-best-practices.md
+++ b/docs/tools/maven/maven-best-practices.md
@@ -1,13 +1,11 @@
---
title: Maven最佳实践
+description: Maven 是一种广泛使用的 Java 项目构建自动化工具。它简化了构建过程并帮助管理依赖关系,使开发人员的工作更轻松。在这篇博文中,我们将讨论一些最佳实践、提示和技巧,以优化我们在项目中对 Maven 的使用并改善我们的开发体验。
category: 开发工具
head:
- - meta
- name: keywords
content: Maven坐标,Maven仓库,Maven生命周期,Maven多模块管理
- - - meta
- - name: description
- content: Maven 是一种广泛使用的 Java 项目构建自动化工具。它简化了构建过程并帮助管理依赖关系,使开发人员的工作更轻松。在这篇博文中,我们将讨论一些最佳实践、提示和技巧,以优化我们在项目中对 Maven 的使用并改善我们的开发体验。
---
> 本文由 JavaGuide 翻译并完善,原文地址: 。
@@ -23,12 +21,12 @@ Maven 遵循标准目录结构来保持项目之间的一致性。遵循这种
Maven 项目的标准目录结构如下:
```groovy
-src /
- main /
+src/
+ main/
java/
resources/
- test/ java
- /
+ test/
+ java/
resources/
pom.xml
```
diff --git a/docs/tools/maven/maven-core-concepts.md b/docs/tools/maven/maven-core-concepts.md
index 07f46bc5cb2..8711f7076ff 100644
--- a/docs/tools/maven/maven-core-concepts.md
+++ b/docs/tools/maven/maven-core-concepts.md
@@ -1,13 +1,11 @@
---
title: Maven核心概念总结
+description: Apache Maven 的本质是一个软件项目管理和理解工具。基于项目对象模型 (Project Object Model,POM) 的概念,Maven 可以从一条中心信息管理项目的构建、报告和文档。
category: 开发工具
head:
- - meta
- name: keywords
content: Maven坐标,Maven仓库,Maven生命周期,Maven多模块管理
- - - meta
- - name: description
- content: Apache Maven 的本质是一个软件项目管理和理解工具。基于项目对象模型 (Project Object Model,POM) 的概念,Maven 可以从一条中心信息管理项目的构建、报告和文档。
---
> 这部分内容主要根据 Maven 官方文档整理,做了对应的删减,主要保留比较重要的部分,不涉及实战,主要是一些重要概念的介绍。
@@ -197,7 +195,7 @@ Maven 在遇到这种问题的时候,会遵循 **路径最短优先** 和 **
根据路径最短优先原则,X(1.0) 会被解析使用,也就是说实际用的是 1.0 版本的 X。
-但是!!!这会一些问题:如果 D 依赖用到了 1.5 版本的 X 中才有的一个类,运行项目就会报`NoClassDefFoundError`错误。如果 D 依赖用到了 1.5 版本的 X 中才有的一个方法,运行项目就会报`NoSuchMethodError`错误。
+但是!!!这会一些问题:如果 C 依赖用到了 1.5 版本的 X 中才有的一个类,运行项目就会报`NoClassDefFoundError`错误。如果 C 依赖用到了 1.5 版本的 X 中才有的一个方法,运行项目就会报`NoSuchMethodError`错误。
现在知道为什么你的 Maven 项目总是会报`NoClassDefFoundError`和`NoSuchMethodError`错误了吧?
@@ -217,7 +215,7 @@ Maven 在遇到这种问题的时候,会遵循 **路径最短优先** 和 **
一般我们在解决依赖冲突的时候,都会优先保留版本较高的。这是因为大部分 jar 在升级的时候都会做到向下兼容。
-如果高版本修改了低版本的一些类或者方法的话,这个时候就能直接保留高版本了,而是应该考虑优化上层依赖,比如升级上层依赖的版本。
+如果高版本修改了低版本的一些类或者方法的话,这个时候就不能直接保留高版本了,而是应该考虑优化上层依赖,比如升级上层依赖的版本。
还是上面的例子:
diff --git a/docs/zhuanlan/README.md b/docs/zhuanlan/README.md
new file mode 100644
index 00000000000..8117e32e918
--- /dev/null
+++ b/docs/zhuanlan/README.md
@@ -0,0 +1,20 @@
+---
+title: 星球专属优质专栏概览
+description: JavaGuide知识星球专属专栏汇总,包含Java面试指北、手写RPC框架、源码解读等优质学习资源。
+category: 知识星球
+---
+
+这部分的内容为我的[知识星球](../about-the-author/zhishixingqiu-two-years.md)专属,目前已经更新了下面这些专栏:
+
+- [《Java 面试指北》](./java-mian-shi-zhi-bei.md) : 与 JavaGuide 开源版的内容互补!
+- [⭐AI 智能面试辅助平台 + RAG 知识库](./interview-guide.md):基于 Spring Boot 4.0 + Java 21 + Spring AI 2.0 开发。非常适合作为学习和简历项目,学习门槛低,帮助提升求职竞争力,是主打就业的实战项目。
+- [《后端面试高频系统设计&场景题》](./back-end-interview-high-frequency-system-design-and-scenario-questions.md) : 包含了常见的系统设计案例比如短链系统、秒杀系统以及高频的场景题比如海量数据去重、第三方授权登录。
+- [《手写 RPC 框架》](./java-mian-shi-zhi-bei.md) : 从零开始基于 Netty+Kyro+Zookeeper 实现一个简易的 RPC 框架。
+- [《Java 必读源码系列》](./source-code-reading.md):目前已经整理了 Dubbo 2.6.x、Netty 4.x、SpringBoot 2.1 等框架/中间件的源码
+- ……
+
+欢迎准备 Java 面试以及学习 Java 的同学加入我的[知识星球](../about-the-author/zhishixingqiu-two-years.md),干货非常多!收费虽然是白菜价,但星球里的内容比你参加几万的培训班质量还要高。
+
+我有自己的原则,不割韭菜,用心做内容,真心希望帮助到你!
+
+
diff --git a/docs/zhuanlan/back-end-interview-high-frequency-system-design-and-scenario-questions.md b/docs/zhuanlan/back-end-interview-high-frequency-system-design-and-scenario-questions.md
index ee848d003b8..4d66adcd0cc 100644
--- a/docs/zhuanlan/back-end-interview-high-frequency-system-design-and-scenario-questions.md
+++ b/docs/zhuanlan/back-end-interview-high-frequency-system-design-and-scenario-questions.md
@@ -1,5 +1,6 @@
---
title: 《后端面试高频系统设计&场景题》
+description: 后端面试高频系统设计与场景题专栏,涵盖秒杀系统、短链系统、海量数据处理等30+道经典面试题解析。
category: 知识星球
---
diff --git a/docs/zhuanlan/handwritten-rpc-framework.md b/docs/zhuanlan/handwritten-rpc-framework.md
index 5a055d56a31..adfefa9740a 100644
--- a/docs/zhuanlan/handwritten-rpc-framework.md
+++ b/docs/zhuanlan/handwritten-rpc-framework.md
@@ -1,5 +1,6 @@
---
title: 《手写 RPC 框架》
+description: 手写RPC框架实战教程,基于Netty+Kyro+Zookeeper从零实现RPC框架,深入理解RPC底层原理。
category: 知识星球
---
diff --git a/docs/zhuanlan/interview-guide.md b/docs/zhuanlan/interview-guide.md
new file mode 100644
index 00000000000..b7bc5fdb0d3
--- /dev/null
+++ b/docs/zhuanlan/interview-guide.md
@@ -0,0 +1,555 @@
+---
+title: 《SpringAI 智能面试平台+RAG 知识库》
+description: Spring AI智能面试平台实战项目,基于Spring Boot 4.0和Spring AI 2.0开发,集成RAG知识库和简历分析功能。
+category: 知识星球
+star: 5
+---
+
+很多小伙伴跟我反馈:“我的简历上全是增删改查(CRUD),面试官看都不看,怎么办?”
+
+既然 AI 浪潮已至,我们就直接把大模型能力、向量数据库、RAG 架构装进你的项目里。
+
+## 项目介绍
+
+这是一个基于 Spring Boot 4.0 + Java 21 + Spring AI 2.0 的 AI 智能面试辅助平台 + RAG 知识库。系统提供三大核心功能:
+
+1. **智能简历分析**:上传简历后,AI 自动进行多维度评分并给出改进建议。
+2. **模拟面试系统**:基于简历内容生成个性化面试题,支持实时问答和答案评估。
+3. **RAG 知识库问答**:上传你的私人技术文档,利用 **PGvector** 构建向量索引,彻底解决大模型的“幻觉”问题。
+
+**开源地址(欢迎 Star 鼓励):**
+
+- Github:
+- Gitee:
+
+**承诺**:全功能免费开源,没有任何所谓的 Pro 版或付费套路!
+
+## 配套教程内容安排
+
+本项目专为求职者和开发者设计,旨在解决当前 AI 应用开发中的核心痛点。通过“保姆级”的保姆级教程,我们将从零构建一个融合了 **大模型集成、RAG(检索增强生成)、高性能对象存储与向量数据库**的完整系统。
+
+无论你是想学习 **Spring AI** 的前沿应用,还是需要一个**高含金量的简历项目**,本项目都将为你提供从基建搭建、业务攻坚到面试话术复盘的全方位指导。
+
+**内容安排如下(正在持续更新中)**:
+
+### 环境构建篇
+
+1. 本地搭建 PostgreSQL + PGvector 向量数据库
+2. Spring Boot + RustFS 构建高性能 S3 兼容的对象存储服务
+3. 大模型 API 申请和 Ollama 部署本地模型
+4. 环境搭建终章与项目启动
+
+### 核心功能开发篇
+
+1. 简历上传、多格式内容提取与解析
+2. Spring AI 与大模型集成
+3. 手把手教你写出生产级结构化 Prompt
+4. AI 模拟面试功能
+5. PDF 报告导出功能
+6. 知识库 RAG 问答
+7. 基于 SSE(Server-Sent Events)的打字机效果输出
+8. Docker Compose 一键部署
+
+### 进阶优化篇
+
+1. 统一异常处理与业务错误码设计
+2. MapStruct 实体映射最佳实践
+3. 基于 Redis Stream 的异步任务处理实现
+4. Spring Boot 4.0 升级指南
+5. Docker Compose 一键部署
+
+### 面试篇(重点)
+
+1. 面试官问“这个项目哪里来的”时,如何回答?
+2. 如何在简历上写这个项目?(多种写法参考)
+3. Redis 面试问题挖掘
+4. Spring AI 面试问题挖掘
+5. 文件上传和 PDF 到处面试问题挖掘
+6. 知识库 RAG 面试问题挖掘
+
+### 内容获取
+
+**本项目为 [JavaGuide 知识星球](https://javaguide.cn/about-the-author/zhishixingqiu-two-years.html) 内部专属实战项目,通过语雀文档在线阅读学习,不单独对外开放。**
+
+之所以选择在星球内部发布,是为了确保每一位学习者都能获得**深度的技术答疑**和**完整的求职配套服务**。
+
+整个项目教程预计在 **1-2** 个月内更完。我坚持“慢工出细活”,每一篇文章(不提供视频,浪费时间且不利于学习能力提高)都经过反复推敲,确保**高质量、零门槛**,即便是基础薄弱的同学也能跟着文档从零跑通。
+
+这只是开始。后续星球还会持续推出更多贴合企业真实业务场景的 **Java 实战项目**。
+
+并且,我的星球还有很多其他服务(比如简历优化、一对一提问、高频考点突击资料等),欢迎详细了解我的[知识星球](https://javaguide.cn/about-the-author/zhishixingqiu-two-years.html)。
+
+已经坚持维护六年,内容持续更新,虽白菜价(**0.4 元/天**)但质量很高,主打一个良心!
+
+仅需 **149**(价格即将上调,老用户续费半价 ,微信扫码即可续费),两本书的价格,换取上万培训班级别的服务!
+
+
+
+用心做事,坚持本心,共勉!
+
+## 系统架构
+
+**提示**:架构图采用 draw.io 绘制,导出为 svg 格式,在 Dark 模式下的显示效果会有问题。
+
+系统采用前后端分离架构,整体分为三层:前端展示层、后端服务层、数据存储层。
+
+
+
+**后端层**:
+
+- REST Controllers:统一的 API 入口,处理 HTTP 请求
+- 业务服务层:
+ - Resume Service:简历上传、解析、AI 分析
+ - Interview Service:面试会话管理、问题生成、答案评估
+ - Knowledge Service:知识库上传、文本分块、向量化
+ - RAG Chat Service:检索增强生成,流式问答
+- 异步处理层:基于 Redis Stream 的消费者,异步处理耗时的 AI 任务(如简历分析、向量化、面试评估)
+- AI 集成层:Spring AI + DashScope(通义千问)。统一的 LLM 调用接口,支持对话生成和文本向量化。
+
+**数据存储层**:
+
+- PostgreSQL + pgvector:
+ - 关系数据:简历、面试记录、知识库元数据
+ - 向量检索:存储文档向量,支持相似度搜索
+- Redis:
+
+ - 会话缓存:面试会话状态
+ - 消息队列:Redis Stream 实现异步任务队列
+
+- RustFS/MinIO (S3):原始文件(简历 PDF、知识库文档)
+
+**异步处理流程**:
+
+简历分析、知识库向量化和面试报告生成采用 Redis Stream 异步处理,这里以简历分析和知识库向量化为例介绍一下整体流程:
+
+```
+上传请求 → 保存文件 → 发送消息到 Stream → 立即返回
+ ↓
+ Consumer 消费消息
+ ↓
+ 执行分析/向量化任务
+ ↓
+ 更新数据库状态
+ ↓
+ 前端轮询获取最新状态
+```
+
+状态流转: `PENDING` → `PROCESSING` → `COMPLETED` / `FAILED`
+
+**知识库问答处理流程**:
+
+```
+知识库问答 → 问题向量化 → pgvector 相似度搜索 → 检索相关文档
+ ↓
+ 构建 Prompt → LLM 生成回答 → SSE 流式返回
+```
+
+## 技术栈概览
+
+### 后端技术
+
+| 技术 | 版本 | 说明 |
+| --------------------- | ----- | ------------------------- |
+| Spring Boot | 4.0 | 应用框架 |
+| Java | 21 | 开发语言 |
+| Spring AI | 2.0 | AI 集成框架 |
+| PostgreSQL + pgvector | 14+ | 关系数据库 + 向量存储 |
+| Redis | 6+ | 缓存 + 消息队列(Stream) |
+| Apache Tika | 2.9.2 | 文档解析 |
+| iText 8 | 8.0.5 | PDF 导出 |
+| MapStruct | 1.6.3 | 对象映射 |
+| Gradle | 8.14 | 构建工具 |
+
+### 前端技术
+
+| 技术 | 版本 | 说明 |
+| ------------- | ----- | -------- |
+| React | 18.3 | UI 框架 |
+| TypeScript | 5.6 | 开发语言 |
+| Vite | 5.4 | 构建工具 |
+| Tailwind CSS | 4.1 | 样式框架 |
+| React Router | 7.11 | 路由管理 |
+| Framer Motion | 12.23 | 动画库 |
+| Recharts | 3.6 | 图表库 |
+| Lucide React | 0.468 | 图标库 |
+
+## 技术选型常见问题解答
+
+这里只是简单介绍,后续我会分享文章详细拷打技术选型。
+
+### 为什么选择 Spring AI?
+
+Spring AI 是 Spring 官方推出的 AI 集成框架,提供了统一的 LLM 调用抽象。选择它的原因:
+
+1. 统一抽象:一套代码支持多种 LLM 提供商(OpenAI、阿里云 DashScope、Ollama 等),切换模型只需修改配置
+2. Spring 生态集成:与 Spring Boot 无缝集成,支持自动配置、依赖注入、声明式调用
+3. 内置向量存储支持:原生支持 pgvector、Milvus、Pinecone 等向量数据库,简化 RAG 开发
+4. 结构化输出:通过 `BeanOutputConverter` 将 LLM 输出直接映射为 Java 对象,无需手动解析 JSON
+
+```java
+// 示例:Spring AI 结构化输出
+var converter = new BeanOutputConverter<>(ResumeAnalysisDTO.class);
+String result = chatClient.prompt()
+ .system(systemPrompt)
+ .user(userPrompt + converter.getFormat())
+ .call()
+ .content();
+return converter.convert(result); // 直接得到 Java 对象
+```
+
+### 数据存储为什么选择 PostgreSQL + pgvector?
+
+本项目需要同时存储结构化数据(简历、面试记录)和向量数据(文档 Embedding)。方案对比:
+
+| 方案 | 优点 | 缺点 |
+| --------------------- | ------------------------ | -------------------------- |
+| PostgreSQL + pgvector | 一套数据库搞定,运维简单 | 向量检索性能不如专业向量库 |
+| PostgreSQL + Milvus | 向量检索性能更好 | 多一个组件,运维复杂度增加 |
+| PostgreSQL + Pinecone | 云托管,无需运维 | 成本高,数据在第三方 |
+
+**选择 pgvector 的理由**:
+
+- 架构简单:不引入额外组件,降低部署和运维复杂度
+- 性能够用:HNSW 索引支持毫秒级检索,万级文档场景完全够用
+- 事务一致性:向量数据和业务数据在同一数据库,天然支持事务
+- SQL 查询:可以结合 WHERE 条件过滤,比如"只在某个分类的知识库中检索"
+
+```sql
+-- pgvector 相似度搜索示例
+SELECT content, 1 - (embedding <=> $1) as similarity
+FROM vector_store
+WHERE metadata->>'category' = 'Java'
+ORDER BY embedding <=> $1
+LIMIT 5;
+```
+
+**为什么不选择 MySQL 搭配向量数据库呢?**
+
+PostgreSQL 最大的优势,也是它在 AI 时代甩开对手的“王牌”,就是其强大的可扩展性。开发者可以在不修改内核的情况下,像“即插即用”一样为数据库安装各种功能强大的插件,这让 PostgreSQL 变成了一个无所不能的“数据瑞士军刀”。
+
+- **AI 向量检索?** 有官方推荐的 **pgvector** 扩展,性能强大,生态成熟,足以媲美专业的向量数据库。
+- **全文搜索?** 内置支持(能满足基础需求),或使用 **pg_bm25** 等扩展。
+- **时序数据?** 有顶级的 **TimescaleDB** 扩展。
+- **地理信息?** 有行业标准的 **PostGIS** 扩展。
+
+这种“一站式”解决能力,正是其魅力所在。它意味着许多项目不再需要依赖 Elasticsearch、Milvus 等大量外部中间件,仅凭一个增强版的 PostgreSQL 即可满足多样化需求,从而极大地简化了技术栈,降低了开发和运维的复杂度与成本。
+
+关于 MySQL 和 PostgreSQL 的详细对比,可以参考我写的这篇文章:[MySQL vs PostgreSQL,如何选择?](https://mp.weixin.qq.com/s/APWD-PzTcTqGUuibAw7GGw)。
+
+### 为什么引入 Redis?
+
+本项目主要有两个场景用到了 Redis:
+
+1. Redis 替代 `ConcurrentHashMap` 实现会话的缓存。
+2. 基于 Redis Stream 实现简历分析、知识库向量化等场景的异步(还能解耦,分析和向量化可以使用其他编程语言来做)。
+
+**为什么引入 Redis Stream?为何不选择 Kafka、RabbitMQ 等更成熟的消息队列?**
+
+简历分析、知识库向量化等 AI 任务耗时较长(10-60 秒),不适合同步处理。需要消息队列实现异步解耦。
+
+| 维度 | Redis Stream | RabbitMQ | Kafka | 内存队列 |
+| :--------------- | :-------------------------------- | :----------------------------- | :--------------------------- | :--------------------------------- |
+| **吞吐量** | 高(十万级 QPS) | 中(万级 QPS) | 极高(百万级,水平扩展) | 极高(千万级/秒,受限于 CPU/内存) |
+| **延迟** | 极低(亚毫秒级) | 低(毫秒级) | 中(毫秒到十毫秒级) | 极低(纳秒/微秒级) |
+| **持久化** | 支持(RDB/AOF) | 支持(Mnesia/磁盘) | 强支持(原生分段日志) | 无(进程终止即失) |
+| **消息堆积能力** | 一般(受限于内存) | 中(磁盘堆积,性能下降明显) | 极强(TB 级磁盘存储) | 差(受限于堆内存) |
+| **消费模式** | 发布订阅 / 消费者组 | 灵活路由 / 多种交换机模式 | 发布订阅 / 消费者组 | 点对点 / 多消费者(取决于实现) |
+| **消息回溯** | 支持(按 ID / 时间范围) | 不支持 | 强支持(按 Offset / 时间戳) | 不支持 |
+| **消息顺序性** | 单 Stream 有序 | 单队列有序 | 单 Partition 有序 | 有序(单队列) |
+| **可靠性** | 中(异步复制可能丢失) | 高(Publisher Confirm / 事务) | 极高(多副本 ISR + acks) | 低(无持久化、无确认) |
+| **运维复杂度** | 低 | 中 | 高(KRaft 模式已简化) | 极低 |
+| **适用场景** | 轻量级流处理、已有 Redis 基础设施 | 复杂路由、企业级集成 | 大数据流、事件溯源、日志聚合 | 进程内解耦、极致性能场景 |
+
+选择 Redis Stream 的理由:
+
+- 复用现有组件:Redis 已用于会话缓存,无需引入新中间件
+- 功能满足需求:支持消费者组、消息确认(ACK)、持久化
+- 运维简单:对于中小型项目,Redis Stream 完全够用
+
+### 构建工具为什么选择 Gradle?
+
+SpringBoot 官方现在用的就是 Gradle,加上国内现在都是 Maven 更多,换个 Gradle 还更新颖一些。
+
+个人也更喜欢用 Gradle,也写过相关的文章:[Gradle 核心概念总结](https://javaguide.cn/tools/gradle/gradle-core-concepts.html)。
+
+### 为什么使用 MapStruct?
+
+项目中有大量 Entity ↔ DTO 转换需求,MapStruct 是编译时代码生成的对象映射框架:
+
+| 方案 | 性能 | 类型安全 | 使用复杂度 |
+| ----------- | ------------ | ---------- | ------------ |
+| MapStruct | 零反射,最快 | 编译时检查 | 定义接口即可 |
+| BeanUtils | 反射,慢 | 运行时报错 | 一行代码 |
+| ModelMapper | 反射,较慢 | 运行时报错 | 配置复杂 |
+| 手写转换 | 最快 | 编译时检查 | 重复代码多 |
+
+### 为什么使用 Apache Tika?
+
+系统需要解析多种格式的文档(PDF、Word、TXT),Apache Tika 是 Apache 基金会的文档解析库:
+
+- 格式支持全:PDF、DOCX、DOC、TXT、HTML、Markdown 等上百种格式
+- 自动识别:根据文件内容自动检测格式,无需依赖文件扩展名
+- 文本提取:统一的 API 提取纯文本,屏蔽格式差异
+
+```java
+// Tika 解析示例
+Tika tika = new Tika();
+String content = tika.parseToString(inputStream); // 自动识别格式并提取文本
+```
+
+### 为什么使用 SSE 而不是 WebSocket?
+
+知识库问答需要流式输出(像 ChatGPT 那样逐字显示),有两种技术选择:
+
+| 方案 | 优点 | 缺点 |
+| --------- | ------------------------- | -------------------------- |
+| SSE | 简单,基于 HTTP,单向推送 | 仅支持服务端 → 客户端 |
+| WebSocket | 双向通信,功能强大 | 协议复杂,需要维护连接状态 |
+
+选择 SSE 的理由:
+
+- 场景匹配:LLM 流式输出是单向的(服务端 → 客户端),不需要双向通信
+- 实现简单:基于 HTTP,天然支持重连、跨域
+- Spring 支持好:`Flux>` 一行代码搞定
+
+### 前端为什么选择 React + TypeScript + Tailwind CSS?
+
+| 技术 | 选择理由 |
+| ------------ | ------------------------------------------ |
+| React | 生态最成熟,组件化开发,社区资源丰富 |
+| TypeScript | 类型安全,IDE 智能提示,减少运行时错误 |
+| Vite | 开发服务器启动快(秒级),HMR 热更新体验好 |
+| Tailwind CSS | 原子化 CSS,快速开发,无需写 CSS 文件 |
+
+## 效果展示
+
+### 简历与面试
+
+简历库:
+
+
+
+简历上传分析:
+
+
+
+简历分析详情:
+
+
+
+面试记录:
+
+
+
+面试详情:
+
+
+
+模拟面试:
+
+
+
+### 知识库
+
+知识库管理:
+
+
+
+问答助手:
+
+
+
+## 项目结构
+
+**这是项目前后端的结构说明**:
+
+```
+ interview-guide/
+ ├── app/ # 后端 Spring Boot 应用
+ │ ├── src/main/java/interview/guide/
+ │ │ ├── App.java # 启动类
+ │ │ │
+ │ │ ├── common/ # 公共模块
+ │ │ │ ├── config/ # 配置类(CORS、S3存储等)
+ │ │ │ ├── constant/ # 常量定义
+ │ │ │ ├── exception/ # 全局异常处理
+ │ │ │ ├── model/ # 公共模型(如异步任务状态)
+ │ │ │ └── result/ # 统一响应封装 Result
+ │ │ │
+ │ │ ├── infrastructure/ # 基础设施层
+ │ │ │ ├── file/ # 文件处理(解析、存储、校验)
+ │ │ │ │ ├── DocumentParseService # Apache Tika 文档解析
+ │ │ │ │ ├── FileStorageService # S3 文件存储
+ │ │ │ │ ├── FileHashService # SHA-256 哈希计算
+ │ │ │ │ └── FileValidationService # 文件类型/大小校验
+ │ │ │ ├── redis/ # Redis 操作封装
+ │ │ │ │ ├── RedisService # 通用 Redis 操作 + Stream 消费
+ │ │ │ │ └── InterviewSessionCache # 面试会话缓存
+ │ │ │ ├── mapper/ # MapStruct 对象映射
+ │ │ │ └── export/ # PDF 导出服务
+ │ │ │
+ │ │ └── modules/ # 业务模块
+ │ │ ├── resume/ # 简历模块
+ │ │ │ ├── ResumeController # REST API
+ │ │ │ ├── model/ # Entity + DTO
+ │ │ │ ├── repository/ # JPA Repository
+ │ │ │ ├── service/ # 业务逻辑
+ │ │ │ │ ├── ResumeUploadService # 上传处理
+ │ │ │ │ ├── ResumeGradingService # AI 分析评分
+ │ │ │ │ └── ResumePersistenceService # 持久化
+ │ │ │ └── listener/ # 异步消费者
+ │ │ │ ├── AnalyzeStreamProducer # 发送分析任务
+ │ │ │ └── AnalyzeStreamConsumer # 消费并执行分析
+ │ │ │
+ │ │ ├── interview/ # 面试模块
+ │ │ │ ├── InterviewController
+ │ │ │ ├── model/
+ │ │ │ ├── repository/
+ │ │ │ ├── service/
+ │ │ │ │ ├── InterviewSessionService # 会话管理
+ │ │ │ │ ├── InterviewQuestionService # 问题生成
+ │ │ │ │ └── AnswerEvaluationService # 答案评估
+ │ │ │ └── listener/
+ │ │ │ └── EvaluateStreamConsumer # 异步评估
+ │ │ │
+ │ │ └── knowledgebase/ # 知识库模块
+ │ │ ├── KnowledgeBaseController # 知识库管理 API
+ │ │ ├── RagChatController # RAG 问答 API(支持 SSE)
+ │ │ ├── model/
+ │ │ ├── repository/
+ │ │ │ └── VectorRepository # 向量数据操作
+ │ │ ├── service/
+ │ │ │ ├── KnowledgeBaseUploadService # 上传处理
+ │ │ │ ├── KnowledgeBaseVectorService # 向量化
+ │ │ │ ├── KnowledgeBaseQueryService # RAG 检索 + 生成
+ │ │ │ └── RagChatSessionService # 聊天会话管理
+ │ │ └── listener/
+ │ │ └── VectorizeStreamConsumer # 异步向量化
+ │ │
+ │ └── src/main/resources/
+ │ ├── application.yml # 主配置文件
+ │ └── prompts/ # AI 提示词模板
+ │ ├── resume-analysis-*.st # 简历分析提示词
+ │ ├── interview-question-*.st # 面试问题生成提示词
+ │ ├── interview-evaluation-*.st # 面试评估提示词
+ │ └── knowledgebase-query-*.st # RAG 问答提示词
+ │
+ ├── frontend/ # 前端 React 应用
+ │ ├── src/
+ │ │ ├── main.tsx # 入口文件
+ │ │ ├── App.tsx # 根组件 + 路由配置
+ │ │ │
+ │ │ ├── api/ # API 请求层
+ │ │ │ ├── request.ts # Axios 封装 + 拦截器
+ │ │ │ ├── resume.ts # 简历 API
+ │ │ │ ├── interview.ts # 面试 API
+ │ │ │ ├── knowledgebase.ts # 知识库 API
+ │ │ │ └── ragChat.ts # RAG 聊天 API(含 SSE)
+ │ │ │
+ │ │ ├── pages/ # 页面组件
+ │ │ │ ├── UploadPage.tsx # 简历上传
+ │ │ │ ├── HistoryPage.tsx # 简历列表
+ │ │ │ ├── ResumeDetailPage.tsx # 简历详情 + 分析结果
+ │ │ │ ├── InterviewPage.tsx # 模拟面试
+ │ │ │ ├── InterviewHistoryPage.tsx # 面试记录
+ │ │ │ ├── KnowledgeBaseUploadPage.tsx # 知识库上传
+ │ │ │ ├── KnowledgeBaseManagePage.tsx # 知识库管理
+ │ │ │ └── KnowledgeBaseQueryPage.tsx # 知识库问答
+ │ │ │
+ │ │ ├── components/ # 通用组件
+ │ │ │ ├── Layout.tsx # 页面布局(侧边栏 + 内容区)
+ │ │ │ ├── FileUploadCard.tsx # 文件上传卡片
+ │ │ │ ├── AnalysisPanel.tsx # 分析结果展示
+ │ │ │ ├── InterviewChatPanel.tsx # 面试问答交互
+ │ │ │ ├── RadarChart.tsx # 雷达图(Recharts)
+ │ │ │ ├── CodeBlock.tsx # 代码块高亮
+ │ │ │ └── ConfirmDialog.tsx # 确认弹窗
+ │ │ │
+ │ │ ├── types/ # TypeScript 类型定义
+ │ │ └── utils/ # 工具函数
+ │ │
+ │ ├── package.json
+ │ └── vite.config.ts
+ │
+ ├── docker/ # Docker 相关
+ │ └── postgres/init.sql # 数据库初始化(pgvector 扩展)
+ │
+ ├── docker-compose.yml # 一键部署编排
+ ├── .env.example # 环境变量模板
+ └── README.md
+```
+
+**后端分层详细介绍**:
+
+| 层级 | 目录 | 职责 |
+| -------------- | ----------------------- | ------------------------------------- |
+| Controller | `modules/*/Controller` | REST API 入口,参数校验,调用 Service |
+| Service | `modules/*/service/` | 业务逻辑,事务管理 |
+| Repository | `modules/*/repository/` | 数据访问,JPA 查询 |
+| Model | `modules/*/model/` | Entity 实体 + DTO 传输对象 |
+| Listener | `modules/*/listener/` | Redis Stream 异步消费者 |
+| Infrastructure | `infrastructure/` | 通用基础设施(文件、缓存、导出) |
+| Common | `common/` | 公共配置、异常、常量 |
+
+## 学习本项目你将获得什么?
+
+本项目采用最新主流的 Java 技术栈,是市面上第一个基于 SpringBoot4.x+SpringAI2.x 的实战项目以及教程。
+
+项目整体难易程度一般,即使你的编程基础一般,按照项目教程也能顺利走完。
+
+通过学习这个项目,你不仅能掌握最新的 Java AI 生态工具,还能学会如何构建一个生产级别的大模型应用。
+
+### 深度掌握 AI 应用开发核心范式
+
+本项目是学习 **LLM 应用开发**的绝佳实践案例:
+
+- **Spring AI 2.0 实战**:掌握如何通过统一抽象接口对接多种大模型(如通义千问、OpenAI),实现“零成本”模型切换。
+- **Prompt Engineering(提示词工程)**:学习如何编写结构化的 System/User Prompt,并利用 `BeanOutputConverter` 实现 **LLM 输出到 Java 对象的自动化映射**,告别繁琐的 JSON 手动解析。
+- **RAG 全链路实现**:深度理解“文档解析 -> 文本分块 -> 向量化 (Embedding) -> 向量数据库存储 -> 相似度检索 -> 上下文增强生成”的完整闭环。
+
+### 现代化的 Java 后端架构思维
+
+本项目采用了目前 Java 社区最前沿的技术组合:
+
+- **拥抱 Java 21 & Spring Boot 4.0**:提前布局最新版本的特性(如 record、虚拟线程等),让你的技术栈领先市场。
+- **模块化单体架构**:学习如何通过清晰的分层逻辑(Modules + Infrastructure + Common)组织代码,使系统既具备微服务的解耦特性,又保留单体应用的部署便捷性。
+- **高性能对象转换**:通过 **MapStruct** 替代传统的反射拷贝,学习在追求极致性能的场景下如何优雅地处理 Entity 与 DTO 的映射。
+
+### 高级数据存储与中间件应用
+
+避开盲目引入复杂中间件的坑,学习“最合适”的选型逻辑:
+
+- **PostgreSQL + pgvector 的“一站式”方案**:学习如何使用一套数据库同时处理关系型业务数据和 AI 向量数据,掌握 **HNSW 索引**在万级文档场景下的优化实践。
+- **Redis Stream 异步任务处理**:深入理解为什么在 AI 耗时任务(10-60s)场景下选择 Redis Stream 而非 Kafka。掌握基于 **消息队列** 实现系统解耦和流量削峰的真实手段。
+- **文件处理专家系统**:利用 **Apache Tika** 构建通用的文档解析引擎,处理 PDF、Word、TXT 等多种格式,提升处理非结构化数据的能力。
+
+### 工程化与部署能力
+
+- **标准化交付**:学习使用 **Gradle** 进行项目构建,掌握其相比 Maven 的灵活性优势。
+- **容器化部署**:通过 **Docker Compose** 实现数据库(含扩展)、缓存、后端应用的一键式环境搭建,理解生产环境下的基础设施配置。
+
+### 前端工程化与交互设计
+
+对于后端同学,这也是一次绝佳的“全栈能力”升级机会:
+
+- **SSE (Server-Sent Events) 流式渲染**:掌握像 ChatGPT 一样逐字输出回答的实现技术,理解其相比 WebSocket 在单向推送场景下的优势。
+- **响应式与动画设计**:使用 **Tailwind CSS** 快速构建美观的 UI,结合 **Framer Motion** 提升用户交互体验。
+- **数据可视化**:通过 **Recharts** 将 AI 分析后的简历评分、维度对比以直观的雷达图等形式展现。
+
+## 总结
+
+很多 AI 项目只停留在调用一个 API。而本项目带你解决的是**真实工程问题**:
+
+- 如何处理大模型响应慢的问题?(**异步处理 + Redis Stream**)
+- 如何让大模型输出格式固定的数据?(**结构化 Prompt + MapStruct**)
+- 如何让大模型基于私有文档回答?(**RAG + pgvector**)
+
+**本项目为 [JavaGuide 知识星球](https://javaguide.cn/about-the-author/zhishixingqiu-two-years.html) 内部专属实战项目,通过语雀文档在线阅读学习,不单独对外开放。**
+
+除了实战项目之外,我的星球还有很多其他服务(比如简历优化、一对一提问、高频考点突击资料等),欢迎详细了解我的[知识星球](https://javaguide.cn/about-the-author/zhishixingqiu-two-years.html)。
+
+已经坚持维护六年,内容持续更新,虽白菜价(**0.4 元/天**)但质量很高,主打一个良心!
+
+仅需 **149**(价格即将上调,老用户续费半价 ,微信扫码即可续费),两本书的价格,就能让你拥有上万培训班的服务!
+
+
diff --git a/docs/zhuanlan/java-mian-shi-zhi-bei.md b/docs/zhuanlan/java-mian-shi-zhi-bei.md
index 752a6bcea71..43562ff63d9 100644
--- a/docs/zhuanlan/java-mian-shi-zhi-bei.md
+++ b/docs/zhuanlan/java-mian-shi-zhi-bei.md
@@ -1,14 +1,21 @@
---
title: 《Java 面试指北》
+description: Java面试指北专栏,四年打磨的Java后端面试指南,涵盖核心知识点与高频面试题系统讲解。
category: 知识星球
star: 5
---
-我花费了三年的时间,写了一本针对 Java 面试的《Java 面试指北》,内容质量非常高,非常适合准备 Java 面试的朋友使用!
+**四年磨一剑,只为打造最优质的 Java 面试指南。**
-目前的成绩:累计阅读 **270w+** ,点赞 **3550+** ,评论 **1130+** (几乎每一条提问类型的评论我看到后都会用心回复)。
+这本《Java 面试指北》(后端面试通用)的内容经过反复打磨,质量极高,旨在帮助每一位 Java/后端求职者从容应对面试挑战。
-
+**用数据说话:** 截至目前,专栏累计阅读量已突破 **477.1W**,收获点赞 **5,118** 个,评论互动 **1,657** 条。值得一提的是,评论区不仅仅是留言板,更是答疑区——几乎每一条提问,我都会用心回复,确保无疑问遗留。
+
+
+
+📅 **增长见证:** 下图记录了 2024 年时的成绩。对比当下,你会发现其增长速度可以用“惊人”来形容,这不仅是数据的攀升,更是无数读者认可的证明!
+
+
## 介绍
@@ -22,6 +29,10 @@ star: 5
《Java 面试指北》 会根据每一年的面试情况对内容进行更新完善,保证内容质量的时效性。并且,只需要加入[知识星球](../about-the-author/zhishixingqiu-two-years.md)一次,即可永久获取《Java 面试指北》的访问权限,持续同步更新完善。
+下面是《Java 面试指北》 收到的部分球友的真实反馈:
+
+
+
## 内容概览

@@ -32,7 +43,11 @@ star: 5

-另外,考虑到很多小伙伴缺少项目经历,我还推荐了很多小众但优质的实战项目,有视频也有开源项目,有业务系统,也有各种含金量比较高的轮子类项目。
+其中的 **「⭐Java 面试准备常见问题解答(补充)」** 和 **「⭐ 项目经验常见问题解答(补充)」** 强烈建议必看,信息密度非常高!
+
+
+
+另外,考虑到很多同学项目经历不足,我还专门整理了一批**小众但优质的实战项目**:既有配套视频,也有高质量开源仓库,既包含完整业务系统,也有技术含量很高的轮子类项目,方便你快速补齐项目短板。

@@ -46,23 +61,30 @@ star: 5
古人云:“**他山之石,可以攻玉**” 。善于学习借鉴别人的面试的成功经验或者失败的教训,可以让自己少走许多弯路。
-**「面经篇」** 主要会分享一些高质量的 Java 后端面经,有校招的,也有社招的,有大厂的,也有中小厂的。
+**「面经篇」** 主打高质量 Java 后端真实面经:校招 / 社招全覆盖,大厂、中小厂、央国企、外企,连大厂内包都有,不管你是哪种求职方向,都能找到适配的面经参考。
+
+
-如果你是非科班的同学,也能在这些文章中找到对应的非科班的同学写的面经。
+**为何选择《Java 面试指北》的面经?**
-
+相比于网络上海量但杂乱的面经信息,《Java 面试指北》中提供的面经在质量筛选和价值挖掘上投入了更多精力。每一份收录的面经均力求做到:
-相比于牛客网或者其他网站的面经,《Java 面试指北》中整理的面经质量更高,并且,我会提供优质的参考资料。
+- **内容真实、有启发性**: 优先选择那些能反映实际面试场景、考察重点和面试官思路的经验。
+- **提供深度学习资源**: 拒绝“只有问题没有答案”的焦虑。针对面经中的高频/核心难题,我精心关联了高质量的参考资料(通常是我撰写的深度解析文章)或直接提供核心参考答案,助你知其然更知其所以然。
-另外,[知识星球](https://javaguide.cn/about-the-author/zhishixingqiu-two-years.html)还有专门分享面经和面试题的专题,里面会分享很多优质的面经和面试题。
+另外,[知识星球](https://javaguide.cn/about-the-author/zhishixingqiu-two-years.html)还有专门分享面经和面试题的专题,持续更新优质的面经和面试题。
-
+
### 技术面试题自测篇
-为了让小伙伴们自测以检查自己的掌握情况,我还推出了 **「技术面试题自测」** 系列。不过,目前只更新了 Java 和数据库的自测,正在持续更新中。
+为了让小伙伴们自测以检查自己的掌握情况,我还推出了 **「技术面试题自测」** 系列。目前已经覆盖 Java 后端的核心高频考点,并在持续迭代更新中。
+
+
+
+每道题我都会给出**提示与思路**,并用 ⭐ 标注重要程度:⭐ 越多,说明面试越爱问,就越值得多花一些时间准备。
-
+
高效准备技术八股文的技巧之一在于多多自测,查漏补缺。
diff --git a/docs/zhuanlan/readme.md b/docs/zhuanlan/readme.md
deleted file mode 100644
index 9175b71ff3c..00000000000
--- a/docs/zhuanlan/readme.md
+++ /dev/null
@@ -1,18 +0,0 @@
----
-title: 星球专属优质专栏概览
-category: 知识星球
----
-
-这部分的内容为我的[知识星球](../about-the-author/zhishixingqiu-two-years.md)专属,目前已经更新了下面这些专栏:
-
-- **[《Java 面试指北》](./java-mian-shi-zhi-bei.md)** : 与 JavaGuide 开源版的内容互补!
-- **[《后端面试高频系统设计&场景题》](./back-end-interview-high-frequency-system-design-and-scenario-questions.md)** : 包含了常见的系统设计案例比如短链系统、秒杀系统以及高频的场景题比如海量数据去重、第三方授权登录。
-- **[《手写 RPC 框架》](./java-mian-shi-zhi-bei.md)** : 从零开始基于 Netty+Kyro+Zookeeper 实现一个简易的 RPC 框架。
-- **[《Java 必读源码系列》](./source-code-reading.md)**:目前已经整理了 Dubbo 2.6.x、Netty 4.x、SpringBoot 2.1 等框架/中间件的源码
-- ……
-
-欢迎准备 Java 面试以及学习 Java 的同学加入我的[知识星球](../about-the-author/zhishixingqiu-two-years.md),干货非常多!收费虽然是白菜价,但星球里的内容比你参加几万的培训班质量还要高。
-
-我有自己的原则,不割韭菜,用心做内容,真心希望帮助到你!
-
-
diff --git a/docs/zhuanlan/source-code-reading.md b/docs/zhuanlan/source-code-reading.md
index 2441f2e7adc..445990e7256 100644
--- a/docs/zhuanlan/source-code-reading.md
+++ b/docs/zhuanlan/source-code-reading.md
@@ -1,5 +1,6 @@
---
title: 《Java 必读源码系列》
+description: Java必读源码系列专栏,涵盖Dubbo、Netty、SpringBoot等主流框架源码解析,助力深入理解底层原理。
category: 知识星球
star: true
---
@@ -20,4 +21,4 @@ star: true
除了《Java 必读源码系列》之外,我的知识星球还有 [《Java 面试指北》](https://mp.weixin.qq.com/s?__biz=Mzg2OTA0Njk0OA==&mid=2247536358&idx=2&sn=a6098093107d596d3c426c9e71e871b8&chksm=cea1012df9d6883b95aab61fd815a238c703b2d4b36d78901553097a4939504e3e6d73f2b14b&token=710779655&lang=zh_CN#rd)**、**[《后端面试高频系统设计&场景题》](https://mp.weixin.qq.com/s?__biz=Mzg2OTA0Njk0OA==&mid=2247536451&idx=1&sn=5eae2525ac3d79591dd86c6051522c0b&chksm=cea10088f9d6899e0aee4146de162a6de6ece71ba4c80c23f04d12b1fd48c087a31bc7d413f4&token=710779655&lang=zh_CN#rd)、《手写 RPC 框架》等多个专栏。进入星球之后,统统都可以免费阅读。
-
+
diff --git a/package.json b/package.json
index 2dab8c67c23..78a440fb220 100644
--- a/package.json
+++ b/package.json
@@ -5,6 +5,14 @@
"description": "javaguide",
"license": "MIT",
"author": "Guide",
+ "pnpm": {
+ "overrides": {
+ "vite": ">=7.0.8",
+ "undici": ">=7.18.2",
+ "mdast-util-to-hast": ">=13.2.1",
+ "markdownlint-cli2>js-yaml": ">=4.1.1"
+ }
+ },
"scripts": {
"docs:build": "vuepress build docs",
"docs:dev": "vuepress dev docs",
@@ -19,20 +27,22 @@
"**/*": "prettier --write --ignore-unknown",
".md": "markdownlint-cli2"
},
- "packageManager": "pnpm@8.15.1",
"dependencies": {
- "@vuepress/bundler-vite": "2.0.0-rc.9",
- "@vuepress/plugin-copyright": "2.0.0-rc.21",
- "@vuepress/plugin-feed": "2.0.0-rc.21",
- "@vuepress/plugin-search": "2.0.0-rc.21",
- "husky": "9.0.10",
- "markdownlint-cli2": "0.12.1",
+ "@vuepress/bundler-vite": "2.0.0-rc.26",
+ "@vuepress/plugin-feed": "2.0.0-rc.121",
+ "@vuepress/plugin-search": "2.0.0-rc.121",
+ "husky": "9.1.7",
+ "markdownlint-cli2": "0.17.1",
"mathjax-full": "3.2.2",
"nano-staged": "0.8.0",
- "nodejs-jieba": "0.1.2",
- "prettier": "3.2.5",
- "vue": "^3.4.21",
- "vuepress": "2.0.0-rc.9",
- "vuepress-theme-hope": "2.0.0-rc.32"
+ "prettier": "3.4.2",
+ "sass-embedded": "1.97.2",
+ "vue": "^3.5.26",
+ "vuepress": "2.0.0-rc.26",
+ "vuepress-theme-hope": "2.0.0-rc.102"
+ },
+ "packageManager": "pnpm@10.0.0",
+ "devDependencies": {
+ "mermaid": "^11.12.2"
}
}
diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml
index dfad3795844..6fd4af6c69f 100644
--- a/pnpm-lock.yaml
+++ b/pnpm-lock.yaml
@@ -1,2480 +1,5211 @@
-lockfileVersion: '6.1'
+lockfileVersion: '9.0'
settings:
autoInstallPeers: true
excludeLinksFromLockfile: false
-dependencies:
- '@vuepress/bundler-vite':
- specifier: 2.0.0-rc.9
- version: 2.0.0-rc.9
- '@vuepress/plugin-copyright':
- specifier: 2.0.0-rc.21
- version: 2.0.0-rc.21(vuepress@2.0.0-rc.9)
- '@vuepress/plugin-feed':
- specifier: 2.0.0-rc.21
- version: 2.0.0-rc.21(vuepress@2.0.0-rc.9)
- '@vuepress/plugin-search':
- specifier: 2.0.0-rc.21
- version: 2.0.0-rc.21(vuepress@2.0.0-rc.9)
- husky:
- specifier: 9.0.10
- version: 9.0.10
- markdownlint-cli2:
- specifier: 0.12.1
- version: 0.12.1
- mathjax-full:
- specifier: 3.2.2
- version: 3.2.2
- nano-staged:
- specifier: 0.8.0
- version: 0.8.0
- nodejs-jieba:
- specifier: 0.1.2
- version: 0.1.2
- prettier:
- specifier: 3.2.5
- version: 3.2.5
- vue:
- specifier: ^3.4.21
- version: 3.4.21
- vuepress:
- specifier: 2.0.0-rc.9
- version: 2.0.0-rc.9(@vuepress/bundler-vite@2.0.0-rc.9)(vue@3.4.21)
- vuepress-theme-hope:
- specifier: 2.0.0-rc.32
- version: 2.0.0-rc.32(@vuepress/plugin-feed@2.0.0-rc.21)(@vuepress/plugin-search@2.0.0-rc.21)(markdown-it@14.1.0)(mathjax-full@3.2.2)(nodejs-jieba@0.1.2)(vuepress@2.0.0-rc.9)
+overrides:
+ vite: '>=7.0.8'
+ undici: '>=7.18.2'
+ mdast-util-to-hast: '>=13.2.1'
+ markdownlint-cli2>js-yaml: '>=4.1.1'
+
+importers:
+
+ .:
+ dependencies:
+ '@vuepress/bundler-vite':
+ specifier: 2.0.0-rc.26
+ version: 2.0.0-rc.26(@types/node@25.0.9)(sass-embedded@1.97.2)(sass@1.97.2)
+ '@vuepress/plugin-feed':
+ specifier: 2.0.0-rc.121
+ version: 2.0.0-rc.121(vuepress@2.0.0-rc.26(@vuepress/bundler-vite@2.0.0-rc.26(@types/node@25.0.9)(sass-embedded@1.97.2)(sass@1.97.2))(vue@3.5.26))
+ '@vuepress/plugin-search':
+ specifier: 2.0.0-rc.121
+ version: 2.0.0-rc.121(vuepress@2.0.0-rc.26(@vuepress/bundler-vite@2.0.0-rc.26(@types/node@25.0.9)(sass-embedded@1.97.2)(sass@1.97.2))(vue@3.5.26))
+ husky:
+ specifier: 9.1.7
+ version: 9.1.7
+ markdownlint-cli2:
+ specifier: 0.17.1
+ version: 0.17.1
+ mathjax-full:
+ specifier: 3.2.2
+ version: 3.2.2
+ nano-staged:
+ specifier: 0.8.0
+ version: 0.8.0
+ prettier:
+ specifier: 3.4.2
+ version: 3.4.2
+ sass-embedded:
+ specifier: 1.97.2
+ version: 1.97.2
+ vue:
+ specifier: ^3.5.26
+ version: 3.5.26
+ vuepress:
+ specifier: 2.0.0-rc.26
+ version: 2.0.0-rc.26(@vuepress/bundler-vite@2.0.0-rc.26(@types/node@25.0.9)(sass-embedded@1.97.2)(sass@1.97.2))(vue@3.5.26)
+ vuepress-theme-hope:
+ specifier: 2.0.0-rc.102
+ version: 2.0.0-rc.102(@vuepress/plugin-feed@2.0.0-rc.121(vuepress@2.0.0-rc.26(@vuepress/bundler-vite@2.0.0-rc.26(@types/node@25.0.9)(sass-embedded@1.97.2)(sass@1.97.2))(vue@3.5.26)))(@vuepress/plugin-search@2.0.0-rc.121(vuepress@2.0.0-rc.26(@vuepress/bundler-vite@2.0.0-rc.26(@types/node@25.0.9)(sass-embedded@1.97.2)(sass@1.97.2))(vue@3.5.26)))(katex@0.16.27)(markdown-it@14.1.0)(mermaid@11.12.2)(sass-embedded@1.97.2)(sass@1.97.2)(vuepress@2.0.0-rc.26(@vuepress/bundler-vite@2.0.0-rc.26(@types/node@25.0.9)(sass-embedded@1.97.2)(sass@1.97.2))(vue@3.5.26))
+ devDependencies:
+ mermaid:
+ specifier: ^11.12.2
+ version: 11.12.2
packages:
- /@babel/helper-string-parser@7.24.1:
- resolution: {integrity: sha512-2ofRCjnnA9y+wk8b9IAREroeUP02KHp431N2mhKniy2yKIDKpbrHv9eXwm8cBeWQYcJmzv5qKCu65P47eCF7CQ==}
+ '@antfu/install-pkg@1.1.0':
+ resolution: {integrity: sha512-MGQsmw10ZyI+EJo45CdSER4zEb+p31LpDAFp2Z3gkSd1yqVZGi0Ebx++YTEMonJy4oChEMLsxZ64j8FH6sSqtQ==}
+
+ '@babel/helper-string-parser@7.27.1':
+ resolution: {integrity: sha512-qMlSxKbpRlAridDExk92nSobyDdpPijUq2DW6oDnUqd0iOGxmQjyqhMIihI9+zv4LPyZdRje2cavWPbCbWm3eA==}
engines: {node: '>=6.9.0'}
- dev: false
- /@babel/helper-validator-identifier@7.22.20:
- resolution: {integrity: sha512-Y4OZ+ytlatR8AI+8KZfKuL5urKp7qey08ha31L8b3BwewJAoJamTzyvxPR/5D+KkdJCGPq/+8TukHBlY10FX9A==}
+ '@babel/helper-validator-identifier@7.28.5':
+ resolution: {integrity: sha512-qSs4ifwzKJSV39ucNjsvc6WVHs6b7S03sOh2OcHF9UHfVPqWWALUsNUVzhSBiItjRZoLHx7nIarVjqKVusUZ1Q==}
engines: {node: '>=6.9.0'}
- dev: false
- /@babel/parser@7.24.1:
- resolution: {integrity: sha512-Zo9c7N3xdOIQrNip7Lc9wvRPzlRtovHVE4lkz8WEDr7uYh/GMQhSiIgFxGIArRHYdJE5kxtZjAf8rT0xhdLCzg==}
+ '@babel/parser@7.28.6':
+ resolution: {integrity: sha512-TeR9zWR18BvbfPmGbLampPMW+uW1NZnJlRuuHso8i87QZNq2JRF9i6RgxRqtEq+wQGsS19NNTWr2duhnE49mfQ==}
engines: {node: '>=6.0.0'}
hasBin: true
- dependencies:
- '@babel/types': 7.24.0
- dev: false
- /@babel/types@7.24.0:
- resolution: {integrity: sha512-+j7a5c253RfKh8iABBhywc8NSfP5LURe7Uh4qpsh6jc+aLJguvmIUBdjSdEMQv2bENrCR5MfRdjGo7vzS/ob7w==}
+ '@babel/types@7.28.6':
+ resolution: {integrity: sha512-0ZrskXVEHSWIqZM/sQZ4EV3jZJXRkio/WCxaqKZP1g//CEWEPSfeZFcms4XeKBCHU0ZKnIkdJeU/kF+eRp5lBg==}
engines: {node: '>=6.9.0'}
- dependencies:
- '@babel/helper-string-parser': 7.24.1
- '@babel/helper-validator-identifier': 7.22.20
- to-fast-properties: 2.0.0
- dev: false
- /@esbuild/aix-ppc64@0.20.2:
- resolution: {integrity: sha512-D+EBOJHXdNZcLJRBkhENNG8Wji2kgc9AZ9KiPr1JuZjsNtyHzrsfLRrY0tk2H2aoFu6RANO1y1iPPUCDYWkb5g==}
- engines: {node: '>=12'}
+ '@braintree/sanitize-url@7.1.1':
+ resolution: {integrity: sha512-i1L7noDNxtFyL5DmZafWy1wRVhGehQmzZaz1HiN5e7iylJMSZR7ekOV7NsIqa5qBldlLrsKv4HbgFUVlQrz8Mw==}
+
+ '@bufbuild/protobuf@2.10.2':
+ resolution: {integrity: sha512-uFsRXwIGyu+r6AMdz+XijIIZJYpoWeYzILt5yZ2d3mCjQrWUTVpVD9WL/jZAbvp+Ed04rOhrsk7FiTcEDseB5A==}
+
+ '@chevrotain/cst-dts-gen@11.0.3':
+ resolution: {integrity: sha512-BvIKpRLeS/8UbfxXxgC33xOumsacaeCKAjAeLyOn7Pcp95HiRbrpl14S+9vaZLolnbssPIUuiUd8IvgkRyt6NQ==}
+
+ '@chevrotain/gast@11.0.3':
+ resolution: {integrity: sha512-+qNfcoNk70PyS/uxmj3li5NiECO+2YKZZQMbmjTqRI3Qchu8Hig/Q9vgkHpI3alNjr7M+a2St5pw5w5F6NL5/Q==}
+
+ '@chevrotain/regexp-to-ast@11.0.3':
+ resolution: {integrity: sha512-1fMHaBZxLFvWI067AVbGJav1eRY7N8DDvYCTwGBiE/ytKBgP8azTdgyrKyWZ9Mfh09eHWb5PgTSO8wi7U824RA==}
+
+ '@chevrotain/types@11.0.3':
+ resolution: {integrity: sha512-gsiM3G8b58kZC2HaWR50gu6Y1440cHiJ+i3JUvcp/35JchYejb2+5MVeJK0iKThYpAa/P2PYFV4hoi44HD+aHQ==}
+
+ '@chevrotain/utils@11.0.3':
+ resolution: {integrity: sha512-YslZMgtJUyuMbZ+aKvfF3x1f5liK4mWNxghFRv7jqRR9C3R3fAOGTTKvxXDa2Y1s9zSbcpuO0cAxDYsc9SrXoQ==}
+
+ '@esbuild/aix-ppc64@0.25.12':
+ resolution: {integrity: sha512-Hhmwd6CInZ3dwpuGTF8fJG6yoWmsToE+vYgD4nytZVxcu1ulHpUQRAB1UJ8+N1Am3Mz4+xOByoQoSZf4D+CpkA==}
+ engines: {node: '>=18'}
cpu: [ppc64]
os: [aix]
- requiresBuild: true
- dev: false
- optional: true
- /@esbuild/android-arm64@0.20.2:
- resolution: {integrity: sha512-mRzjLacRtl/tWU0SvD8lUEwb61yP9cqQo6noDZP/O8VkwafSYwZ4yWy24kan8jE/IMERpYncRt2dw438LP3Xmg==}
- engines: {node: '>=12'}
+ '@esbuild/aix-ppc64@0.27.2':
+ resolution: {integrity: sha512-GZMB+a0mOMZs4MpDbj8RJp4cw+w1WV5NYD6xzgvzUJ5Ek2jerwfO2eADyI6ExDSUED+1X8aMbegahsJi+8mgpw==}
+ engines: {node: '>=18'}
+ cpu: [ppc64]
+ os: [aix]
+
+ '@esbuild/android-arm64@0.25.12':
+ resolution: {integrity: sha512-6AAmLG7zwD1Z159jCKPvAxZd4y/VTO0VkprYy+3N2FtJ8+BQWFXU+OxARIwA46c5tdD9SsKGZ/1ocqBS/gAKHg==}
+ engines: {node: '>=18'}
cpu: [arm64]
os: [android]
- requiresBuild: true
- dev: false
- optional: true
- /@esbuild/android-arm@0.20.2:
- resolution: {integrity: sha512-t98Ra6pw2VaDhqNWO2Oph2LXbz/EJcnLmKLGBJwEwXX/JAN83Fym1rU8l0JUWK6HkIbWONCSSatf4sf2NBRx/w==}
- engines: {node: '>=12'}
+ '@esbuild/android-arm64@0.27.2':
+ resolution: {integrity: sha512-pvz8ZZ7ot/RBphf8fv60ljmaoydPU12VuXHImtAs0XhLLw+EXBi2BLe3OYSBslR4rryHvweW5gmkKFwTiFy6KA==}
+ engines: {node: '>=18'}
+ cpu: [arm64]
+ os: [android]
+
+ '@esbuild/android-arm@0.25.12':
+ resolution: {integrity: sha512-VJ+sKvNA/GE7Ccacc9Cha7bpS8nyzVv0jdVgwNDaR4gDMC/2TTRc33Ip8qrNYUcpkOHUT5OZ0bUcNNVZQ9RLlg==}
+ engines: {node: '>=18'}
cpu: [arm]
os: [android]
- requiresBuild: true
- dev: false
- optional: true
- /@esbuild/android-x64@0.20.2:
- resolution: {integrity: sha512-btzExgV+/lMGDDa194CcUQm53ncxzeBrWJcncOBxuC6ndBkKxnHdFJn86mCIgTELsooUmwUm9FkhSp5HYu00Rg==}
- engines: {node: '>=12'}
+ '@esbuild/android-arm@0.27.2':
+ resolution: {integrity: sha512-DVNI8jlPa7Ujbr1yjU2PfUSRtAUZPG9I1RwW4F4xFB1Imiu2on0ADiI/c3td+KmDtVKNbi+nffGDQMfcIMkwIA==}
+ engines: {node: '>=18'}
+ cpu: [arm]
+ os: [android]
+
+ '@esbuild/android-x64@0.25.12':
+ resolution: {integrity: sha512-5jbb+2hhDHx5phYR2By8GTWEzn6I9UqR11Kwf22iKbNpYrsmRB18aX/9ivc5cabcUiAT/wM+YIZ6SG9QO6a8kg==}
+ engines: {node: '>=18'}
cpu: [x64]
os: [android]
- requiresBuild: true
- dev: false
- optional: true
- /@esbuild/darwin-arm64@0.20.2:
- resolution: {integrity: sha512-4J6IRT+10J3aJH3l1yzEg9y3wkTDgDk7TSDFX+wKFiWjqWp/iCfLIYzGyasx9l0SAFPT1HwSCR+0w/h1ES/MjA==}
- engines: {node: '>=12'}
+ '@esbuild/android-x64@0.27.2':
+ resolution: {integrity: sha512-z8Ank4Byh4TJJOh4wpz8g2vDy75zFL0TlZlkUkEwYXuPSgX8yzep596n6mT7905kA9uHZsf/o2OJZubl2l3M7A==}
+ engines: {node: '>=18'}
+ cpu: [x64]
+ os: [android]
+
+ '@esbuild/darwin-arm64@0.25.12':
+ resolution: {integrity: sha512-N3zl+lxHCifgIlcMUP5016ESkeQjLj/959RxxNYIthIg+CQHInujFuXeWbWMgnTo4cp5XVHqFPmpyu9J65C1Yg==}
+ engines: {node: '>=18'}
cpu: [arm64]
os: [darwin]
- requiresBuild: true
- dev: false
- optional: true
- /@esbuild/darwin-x64@0.20.2:
- resolution: {integrity: sha512-tBcXp9KNphnNH0dfhv8KYkZhjc+H3XBkF5DKtswJblV7KlT9EI2+jeA8DgBjp908WEuYll6pF+UStUCfEpdysA==}
- engines: {node: '>=12'}
+ '@esbuild/darwin-arm64@0.27.2':
+ resolution: {integrity: sha512-davCD2Zc80nzDVRwXTcQP/28fiJbcOwvdolL0sOiOsbwBa72kegmVU0Wrh1MYrbuCL98Omp5dVhQFWRKR2ZAlg==}
+ engines: {node: '>=18'}
+ cpu: [arm64]
+ os: [darwin]
+
+ '@esbuild/darwin-x64@0.25.12':
+ resolution: {integrity: sha512-HQ9ka4Kx21qHXwtlTUVbKJOAnmG1ipXhdWTmNXiPzPfWKpXqASVcWdnf2bnL73wgjNrFXAa3yYvBSd9pzfEIpA==}
+ engines: {node: '>=18'}
cpu: [x64]
os: [darwin]
- requiresBuild: true
- dev: false
- optional: true
- /@esbuild/freebsd-arm64@0.20.2:
- resolution: {integrity: sha512-d3qI41G4SuLiCGCFGUrKsSeTXyWG6yem1KcGZVS+3FYlYhtNoNgYrWcvkOoaqMhwXSMrZRl69ArHsGJ9mYdbbw==}
- engines: {node: '>=12'}
+ '@esbuild/darwin-x64@0.27.2':
+ resolution: {integrity: sha512-ZxtijOmlQCBWGwbVmwOF/UCzuGIbUkqB1faQRf5akQmxRJ1ujusWsb3CVfk/9iZKr2L5SMU5wPBi1UWbvL+VQA==}
+ engines: {node: '>=18'}
+ cpu: [x64]
+ os: [darwin]
+
+ '@esbuild/freebsd-arm64@0.25.12':
+ resolution: {integrity: sha512-gA0Bx759+7Jve03K1S0vkOu5Lg/85dou3EseOGUes8flVOGxbhDDh/iZaoek11Y8mtyKPGF3vP8XhnkDEAmzeg==}
+ engines: {node: '>=18'}
cpu: [arm64]
os: [freebsd]
- requiresBuild: true
- dev: false
- optional: true
- /@esbuild/freebsd-x64@0.20.2:
- resolution: {integrity: sha512-d+DipyvHRuqEeM5zDivKV1KuXn9WeRX6vqSqIDgwIfPQtwMP4jaDsQsDncjTDDsExT4lR/91OLjRo8bmC1e+Cw==}
- engines: {node: '>=12'}
+ '@esbuild/freebsd-arm64@0.27.2':
+ resolution: {integrity: sha512-lS/9CN+rgqQ9czogxlMcBMGd+l8Q3Nj1MFQwBZJyoEKI50XGxwuzznYdwcav6lpOGv5BqaZXqvBSiB/kJ5op+g==}
+ engines: {node: '>=18'}
+ cpu: [arm64]
+ os: [freebsd]
+
+ '@esbuild/freebsd-x64@0.25.12':
+ resolution: {integrity: sha512-TGbO26Yw2xsHzxtbVFGEXBFH0FRAP7gtcPE7P5yP7wGy7cXK2oO7RyOhL5NLiqTlBh47XhmIUXuGciXEqYFfBQ==}
+ engines: {node: '>=18'}
cpu: [x64]
os: [freebsd]
- requiresBuild: true
- dev: false
- optional: true
- /@esbuild/linux-arm64@0.20.2:
- resolution: {integrity: sha512-9pb6rBjGvTFNira2FLIWqDk/uaf42sSyLE8j1rnUpuzsODBq7FvpwHYZxQ/It/8b+QOS1RYfqgGFNLRI+qlq2A==}
- engines: {node: '>=12'}
+ '@esbuild/freebsd-x64@0.27.2':
+ resolution: {integrity: sha512-tAfqtNYb4YgPnJlEFu4c212HYjQWSO/w/h/lQaBK7RbwGIkBOuNKQI9tqWzx7Wtp7bTPaGC6MJvWI608P3wXYA==}
+ engines: {node: '>=18'}
+ cpu: [x64]
+ os: [freebsd]
+
+ '@esbuild/linux-arm64@0.25.12':
+ resolution: {integrity: sha512-8bwX7a8FghIgrupcxb4aUmYDLp8pX06rGh5HqDT7bB+8Rdells6mHvrFHHW2JAOPZUbnjUpKTLg6ECyzvas2AQ==}
+ engines: {node: '>=18'}
cpu: [arm64]
os: [linux]
- requiresBuild: true
- dev: false
- optional: true
- /@esbuild/linux-arm@0.20.2:
- resolution: {integrity: sha512-VhLPeR8HTMPccbuWWcEUD1Az68TqaTYyj6nfE4QByZIQEQVWBB8vup8PpR7y1QHL3CpcF6xd5WVBU/+SBEvGTg==}
- engines: {node: '>=12'}
+ '@esbuild/linux-arm64@0.27.2':
+ resolution: {integrity: sha512-hYxN8pr66NsCCiRFkHUAsxylNOcAQaxSSkHMMjcpx0si13t1LHFphxJZUiGwojB1a/Hd5OiPIqDdXONia6bhTw==}
+ engines: {node: '>=18'}
+ cpu: [arm64]
+ os: [linux]
+
+ '@esbuild/linux-arm@0.25.12':
+ resolution: {integrity: sha512-lPDGyC1JPDou8kGcywY0YILzWlhhnRjdof3UlcoqYmS9El818LLfJJc3PXXgZHrHCAKs/Z2SeZtDJr5MrkxtOw==}
+ engines: {node: '>=18'}
cpu: [arm]
os: [linux]
- requiresBuild: true
- dev: false
- optional: true
- /@esbuild/linux-ia32@0.20.2:
- resolution: {integrity: sha512-o10utieEkNPFDZFQm9CoP7Tvb33UutoJqg3qKf1PWVeeJhJw0Q347PxMvBgVVFgouYLGIhFYG0UGdBumROyiig==}
- engines: {node: '>=12'}
+ '@esbuild/linux-arm@0.27.2':
+ resolution: {integrity: sha512-vWfq4GaIMP9AIe4yj1ZUW18RDhx6EPQKjwe7n8BbIecFtCQG4CfHGaHuh7fdfq+y3LIA2vGS/o9ZBGVxIDi9hw==}
+ engines: {node: '>=18'}
+ cpu: [arm]
+ os: [linux]
+
+ '@esbuild/linux-ia32@0.25.12':
+ resolution: {integrity: sha512-0y9KrdVnbMM2/vG8KfU0byhUN+EFCny9+8g202gYqSSVMonbsCfLjUO+rCci7pM0WBEtz+oK/PIwHkzxkyharA==}
+ engines: {node: '>=18'}
cpu: [ia32]
os: [linux]
- requiresBuild: true
- dev: false
- optional: true
- /@esbuild/linux-loong64@0.20.2:
- resolution: {integrity: sha512-PR7sp6R/UC4CFVomVINKJ80pMFlfDfMQMYynX7t1tNTeivQ6XdX5r2XovMmha/VjR1YN/HgHWsVcTRIMkymrgQ==}
- engines: {node: '>=12'}
+ '@esbuild/linux-ia32@0.27.2':
+ resolution: {integrity: sha512-MJt5BRRSScPDwG2hLelYhAAKh9imjHK5+NE/tvnRLbIqUWa+0E9N4WNMjmp/kXXPHZGqPLxggwVhz7QP8CTR8w==}
+ engines: {node: '>=18'}
+ cpu: [ia32]
+ os: [linux]
+
+ '@esbuild/linux-loong64@0.25.12':
+ resolution: {integrity: sha512-h///Lr5a9rib/v1GGqXVGzjL4TMvVTv+s1DPoxQdz7l/AYv6LDSxdIwzxkrPW438oUXiDtwM10o9PmwS/6Z0Ng==}
+ engines: {node: '>=18'}
cpu: [loong64]
os: [linux]
- requiresBuild: true
- dev: false
- optional: true
- /@esbuild/linux-mips64el@0.20.2:
- resolution: {integrity: sha512-4BlTqeutE/KnOiTG5Y6Sb/Hw6hsBOZapOVF6njAESHInhlQAghVVZL1ZpIctBOoTFbQyGW+LsVYZ8lSSB3wkjA==}
- engines: {node: '>=12'}
+ '@esbuild/linux-loong64@0.27.2':
+ resolution: {integrity: sha512-lugyF1atnAT463aO6KPshVCJK5NgRnU4yb3FUumyVz+cGvZbontBgzeGFO1nF+dPueHD367a2ZXe1NtUkAjOtg==}
+ engines: {node: '>=18'}
+ cpu: [loong64]
+ os: [linux]
+
+ '@esbuild/linux-mips64el@0.25.12':
+ resolution: {integrity: sha512-iyRrM1Pzy9GFMDLsXn1iHUm18nhKnNMWscjmp4+hpafcZjrr2WbT//d20xaGljXDBYHqRcl8HnxbX6uaA/eGVw==}
+ engines: {node: '>=18'}
cpu: [mips64el]
os: [linux]
- requiresBuild: true
- dev: false
- optional: true
- /@esbuild/linux-ppc64@0.20.2:
- resolution: {integrity: sha512-rD3KsaDprDcfajSKdn25ooz5J5/fWBylaaXkuotBDGnMnDP1Uv5DLAN/45qfnf3JDYyJv/ytGHQaziHUdyzaAg==}
- engines: {node: '>=12'}
+ '@esbuild/linux-mips64el@0.27.2':
+ resolution: {integrity: sha512-nlP2I6ArEBewvJ2gjrrkESEZkB5mIoaTswuqNFRv/WYd+ATtUpe9Y09RnJvgvdag7he0OWgEZWhviS1OTOKixw==}
+ engines: {node: '>=18'}
+ cpu: [mips64el]
+ os: [linux]
+
+ '@esbuild/linux-ppc64@0.25.12':
+ resolution: {integrity: sha512-9meM/lRXxMi5PSUqEXRCtVjEZBGwB7P/D4yT8UG/mwIdze2aV4Vo6U5gD3+RsoHXKkHCfSxZKzmDssVlRj1QQA==}
+ engines: {node: '>=18'}
cpu: [ppc64]
os: [linux]
- requiresBuild: true
- dev: false
- optional: true
- /@esbuild/linux-riscv64@0.20.2:
- resolution: {integrity: sha512-snwmBKacKmwTMmhLlz/3aH1Q9T8v45bKYGE3j26TsaOVtjIag4wLfWSiZykXzXuE1kbCE+zJRmwp+ZbIHinnVg==}
- engines: {node: '>=12'}
+ '@esbuild/linux-ppc64@0.27.2':
+ resolution: {integrity: sha512-C92gnpey7tUQONqg1n6dKVbx3vphKtTHJaNG2Ok9lGwbZil6DrfyecMsp9CrmXGQJmZ7iiVXvvZH6Ml5hL6XdQ==}
+ engines: {node: '>=18'}
+ cpu: [ppc64]
+ os: [linux]
+
+ '@esbuild/linux-riscv64@0.25.12':
+ resolution: {integrity: sha512-Zr7KR4hgKUpWAwb1f3o5ygT04MzqVrGEGXGLnj15YQDJErYu/BGg+wmFlIDOdJp0PmB0lLvxFIOXZgFRrdjR0w==}
+ engines: {node: '>=18'}
cpu: [riscv64]
os: [linux]
- requiresBuild: true
- dev: false
- optional: true
- /@esbuild/linux-s390x@0.20.2:
- resolution: {integrity: sha512-wcWISOobRWNm3cezm5HOZcYz1sKoHLd8VL1dl309DiixxVFoFe/o8HnwuIwn6sXre88Nwj+VwZUvJf4AFxkyrQ==}
- engines: {node: '>=12'}
+ '@esbuild/linux-riscv64@0.27.2':
+ resolution: {integrity: sha512-B5BOmojNtUyN8AXlK0QJyvjEZkWwy/FKvakkTDCziX95AowLZKR6aCDhG7LeF7uMCXEJqwa8Bejz5LTPYm8AvA==}
+ engines: {node: '>=18'}
+ cpu: [riscv64]
+ os: [linux]
+
+ '@esbuild/linux-s390x@0.25.12':
+ resolution: {integrity: sha512-MsKncOcgTNvdtiISc/jZs/Zf8d0cl/t3gYWX8J9ubBnVOwlk65UIEEvgBORTiljloIWnBzLs4qhzPkJcitIzIg==}
+ engines: {node: '>=18'}
cpu: [s390x]
os: [linux]
- requiresBuild: true
- dev: false
- optional: true
- /@esbuild/linux-x64@0.20.2:
- resolution: {integrity: sha512-1MdwI6OOTsfQfek8sLwgyjOXAu+wKhLEoaOLTjbijk6E2WONYpH9ZU2mNtR+lZ2B4uwr+usqGuVfFT9tMtGvGw==}
- engines: {node: '>=12'}
+ '@esbuild/linux-s390x@0.27.2':
+ resolution: {integrity: sha512-p4bm9+wsPwup5Z8f4EpfN63qNagQ47Ua2znaqGH6bqLlmJ4bx97Y9JdqxgGZ6Y8xVTixUnEkoKSHcpRlDnNr5w==}
+ engines: {node: '>=18'}
+ cpu: [s390x]
+ os: [linux]
+
+ '@esbuild/linux-x64@0.25.12':
+ resolution: {integrity: sha512-uqZMTLr/zR/ed4jIGnwSLkaHmPjOjJvnm6TVVitAa08SLS9Z0VM8wIRx7gWbJB5/J54YuIMInDquWyYvQLZkgw==}
+ engines: {node: '>=18'}
cpu: [x64]
os: [linux]
- requiresBuild: true
- dev: false
- optional: true
- /@esbuild/netbsd-x64@0.20.2:
- resolution: {integrity: sha512-K8/DhBxcVQkzYc43yJXDSyjlFeHQJBiowJ0uVL6Tor3jGQfSGHNNJcWxNbOI8v5k82prYqzPuwkzHt3J1T1iZQ==}
- engines: {node: '>=12'}
+ '@esbuild/linux-x64@0.27.2':
+ resolution: {integrity: sha512-uwp2Tip5aPmH+NRUwTcfLb+W32WXjpFejTIOWZFw/v7/KnpCDKG66u4DLcurQpiYTiYwQ9B7KOeMJvLCu/OvbA==}
+ engines: {node: '>=18'}
cpu: [x64]
+ os: [linux]
+
+ '@esbuild/netbsd-arm64@0.25.12':
+ resolution: {integrity: sha512-xXwcTq4GhRM7J9A8Gv5boanHhRa/Q9KLVmcyXHCTaM4wKfIpWkdXiMog/KsnxzJ0A1+nD+zoecuzqPmCRyBGjg==}
+ engines: {node: '>=18'}
+ cpu: [arm64]
os: [netbsd]
- requiresBuild: true
- dev: false
- optional: true
- /@esbuild/openbsd-x64@0.20.2:
- resolution: {integrity: sha512-eMpKlV0SThJmmJgiVyN9jTPJ2VBPquf6Kt/nAoo6DgHAoN57K15ZghiHaMvqjCye/uU4X5u3YSMgVBI1h3vKrQ==}
- engines: {node: '>=12'}
+ '@esbuild/netbsd-arm64@0.27.2':
+ resolution: {integrity: sha512-Kj6DiBlwXrPsCRDeRvGAUb/LNrBASrfqAIok+xB0LxK8CHqxZ037viF13ugfsIpePH93mX7xfJp97cyDuTZ3cw==}
+ engines: {node: '>=18'}
+ cpu: [arm64]
+ os: [netbsd]
+
+ '@esbuild/netbsd-x64@0.25.12':
+ resolution: {integrity: sha512-Ld5pTlzPy3YwGec4OuHh1aCVCRvOXdH8DgRjfDy/oumVovmuSzWfnSJg+VtakB9Cm0gxNO9BzWkj6mtO1FMXkQ==}
+ engines: {node: '>=18'}
+ cpu: [x64]
+ os: [netbsd]
+
+ '@esbuild/netbsd-x64@0.27.2':
+ resolution: {integrity: sha512-HwGDZ0VLVBY3Y+Nw0JexZy9o/nUAWq9MlV7cahpaXKW6TOzfVno3y3/M8Ga8u8Yr7GldLOov27xiCnqRZf0tCA==}
+ engines: {node: '>=18'}
cpu: [x64]
+ os: [netbsd]
+
+ '@esbuild/openbsd-arm64@0.25.12':
+ resolution: {integrity: sha512-fF96T6KsBo/pkQI950FARU9apGNTSlZGsv1jZBAlcLL1MLjLNIWPBkj5NlSz8aAzYKg+eNqknrUJ24QBybeR5A==}
+ engines: {node: '>=18'}
+ cpu: [arm64]
os: [openbsd]
- requiresBuild: true
- dev: false
- optional: true
- /@esbuild/sunos-x64@0.20.2:
- resolution: {integrity: sha512-2UyFtRC6cXLyejf/YEld4Hajo7UHILetzE1vsRcGL3earZEW77JxrFjH4Ez2qaTiEfMgAXxfAZCm1fvM/G/o8w==}
- engines: {node: '>=12'}
+ '@esbuild/openbsd-arm64@0.27.2':
+ resolution: {integrity: sha512-DNIHH2BPQ5551A7oSHD0CKbwIA/Ox7+78/AWkbS5QoRzaqlev2uFayfSxq68EkonB+IKjiuxBFoV8ESJy8bOHA==}
+ engines: {node: '>=18'}
+ cpu: [arm64]
+ os: [openbsd]
+
+ '@esbuild/openbsd-x64@0.25.12':
+ resolution: {integrity: sha512-MZyXUkZHjQxUvzK7rN8DJ3SRmrVrke8ZyRusHlP+kuwqTcfWLyqMOE3sScPPyeIXN/mDJIfGXvcMqCgYKekoQw==}
+ engines: {node: '>=18'}
+ cpu: [x64]
+ os: [openbsd]
+
+ '@esbuild/openbsd-x64@0.27.2':
+ resolution: {integrity: sha512-/it7w9Nb7+0KFIzjalNJVR5bOzA9Vay+yIPLVHfIQYG/j+j9VTH84aNB8ExGKPU4AzfaEvN9/V4HV+F+vo8OEg==}
+ engines: {node: '>=18'}
+ cpu: [x64]
+ os: [openbsd]
+
+ '@esbuild/openharmony-arm64@0.25.12':
+ resolution: {integrity: sha512-rm0YWsqUSRrjncSXGA7Zv78Nbnw4XL6/dzr20cyrQf7ZmRcsovpcRBdhD43Nuk3y7XIoW2OxMVvwuRvk9XdASg==}
+ engines: {node: '>=18'}
+ cpu: [arm64]
+ os: [openharmony]
+
+ '@esbuild/openharmony-arm64@0.27.2':
+ resolution: {integrity: sha512-LRBbCmiU51IXfeXk59csuX/aSaToeG7w48nMwA6049Y4J4+VbWALAuXcs+qcD04rHDuSCSRKdmY63sruDS5qag==}
+ engines: {node: '>=18'}
+ cpu: [arm64]
+ os: [openharmony]
+
+ '@esbuild/sunos-x64@0.25.12':
+ resolution: {integrity: sha512-3wGSCDyuTHQUzt0nV7bocDy72r2lI33QL3gkDNGkod22EsYl04sMf0qLb8luNKTOmgF/eDEDP5BFNwoBKH441w==}
+ engines: {node: '>=18'}
cpu: [x64]
os: [sunos]
- requiresBuild: true
- dev: false
- optional: true
- /@esbuild/win32-arm64@0.20.2:
- resolution: {integrity: sha512-GRibxoawM9ZCnDxnP3usoUDO9vUkpAxIIZ6GQI+IlVmr5kP3zUq+l17xELTHMWTWzjxa2guPNyrpq1GWmPvcGQ==}
- engines: {node: '>=12'}
+ '@esbuild/sunos-x64@0.27.2':
+ resolution: {integrity: sha512-kMtx1yqJHTmqaqHPAzKCAkDaKsffmXkPHThSfRwZGyuqyIeBvf08KSsYXl+abf5HDAPMJIPnbBfXvP2ZC2TfHg==}
+ engines: {node: '>=18'}
+ cpu: [x64]
+ os: [sunos]
+
+ '@esbuild/win32-arm64@0.25.12':
+ resolution: {integrity: sha512-rMmLrur64A7+DKlnSuwqUdRKyd3UE7oPJZmnljqEptesKM8wx9J8gx5u0+9Pq0fQQW8vqeKebwNXdfOyP+8Bsg==}
+ engines: {node: '>=18'}
cpu: [arm64]
os: [win32]
- requiresBuild: true
- dev: false
- optional: true
- /@esbuild/win32-ia32@0.20.2:
- resolution: {integrity: sha512-HfLOfn9YWmkSKRQqovpnITazdtquEW8/SoHW7pWpuEeguaZI4QnCRW6b+oZTztdBnZOS2hqJ6im/D5cPzBTTlQ==}
- engines: {node: '>=12'}
+ '@esbuild/win32-arm64@0.27.2':
+ resolution: {integrity: sha512-Yaf78O/B3Kkh+nKABUF++bvJv5Ijoy9AN1ww904rOXZFLWVc5OLOfL56W+C8F9xn5JQZa3UX6m+IktJnIb1Jjg==}
+ engines: {node: '>=18'}
+ cpu: [arm64]
+ os: [win32]
+
+ '@esbuild/win32-ia32@0.25.12':
+ resolution: {integrity: sha512-HkqnmmBoCbCwxUKKNPBixiWDGCpQGVsrQfJoVGYLPT41XWF8lHuE5N6WhVia2n4o5QK5M4tYr21827fNhi4byQ==}
+ engines: {node: '>=18'}
cpu: [ia32]
os: [win32]
- requiresBuild: true
- dev: false
- optional: true
- /@esbuild/win32-x64@0.20.2:
- resolution: {integrity: sha512-N49X4lJX27+l9jbLKSqZ6bKNjzQvHaT8IIFUy+YIqmXQdjYCToGWwOItDrfby14c78aDd5NHQl29xingXfCdLQ==}
- engines: {node: '>=12'}
+ '@esbuild/win32-ia32@0.27.2':
+ resolution: {integrity: sha512-Iuws0kxo4yusk7sw70Xa2E2imZU5HoixzxfGCdxwBdhiDgt9vX9VUCBhqcwY7/uh//78A1hMkkROMJq9l27oLQ==}
+ engines: {node: '>=18'}
+ cpu: [ia32]
+ os: [win32]
+
+ '@esbuild/win32-x64@0.25.12':
+ resolution: {integrity: sha512-alJC0uCZpTFrSL0CCDjcgleBXPnCrEAhTBILpeAp7M/OFgoqtAetfBzX0xM00MUsVVPpVjlPuMbREqnZCXaTnA==}
+ engines: {node: '>=18'}
cpu: [x64]
os: [win32]
- requiresBuild: true
- dev: false
- optional: true
- /@isaacs/cliui@8.0.2:
- resolution: {integrity: sha512-O8jcjabXaleOG9DQ0+ARXWZBTfnP4WNAqzuiJK7ll44AmxGKv/J2M4TPjxjY3znBCfvBXFzucm1twdyFybFqEA==}
- engines: {node: '>=12'}
- dependencies:
- string-width: 5.1.2
- string-width-cjs: /string-width@4.2.3
- strip-ansi: 7.1.0
- strip-ansi-cjs: /strip-ansi@6.0.1
- wrap-ansi: 8.1.0
- wrap-ansi-cjs: /wrap-ansi@7.0.0
- dev: false
+ '@esbuild/win32-x64@0.27.2':
+ resolution: {integrity: sha512-sRdU18mcKf7F+YgheI/zGf5alZatMUTKj/jNS6l744f9u3WFu4v7twcUI9vu4mknF4Y9aDlblIie0IM+5xxaqQ==}
+ engines: {node: '>=18'}
+ cpu: [x64]
+ os: [win32]
- /@jridgewell/sourcemap-codec@1.4.15:
- resolution: {integrity: sha512-eF2rxCRulEKXHTRiDrDy6erMYWqNw4LPdQ8UQA4huuxaQsVeRPFl2oM8oDGxMFhJUWZf9McpLtJasDDZb/Bpeg==}
- dev: false
+ '@iconify/types@2.0.0':
+ resolution: {integrity: sha512-+wluvCrRhXrhyOmRDJ3q8mux9JkKy5SJ/v8ol2tu4FVjyYvtEzkc/3pK15ET6RKg4b4w4BmTk1+gsCUhf21Ykg==}
- /@lit-labs/ssr-dom-shim@1.2.0:
- resolution: {integrity: sha512-yWJKmpGE6lUURKAaIltoPIE/wrbY3TEkqQt+X0m+7fQNnAv0keydnYvbiJFP1PnMhizmIWRWOG5KLhYyc/xl+g==}
- dev: false
+ '@iconify/utils@3.1.0':
+ resolution: {integrity: sha512-Zlzem1ZXhI1iHeeERabLNzBHdOa4VhQbqAcOQaMKuTuyZCpwKbC2R4Dd0Zo3g9EAc+Y4fiarO8HIHRAth7+skw==}
- /@lit/reactive-element@2.0.4:
- resolution: {integrity: sha512-GFn91inaUa2oHLak8awSIigYz0cU0Payr1rcFsrkf5OJ5eSPxElyZfKh0f2p9FsTiZWXQdWGJeXZICEfXXYSXQ==}
- dependencies:
- '@lit-labs/ssr-dom-shim': 1.2.0
- dev: false
+ '@jridgewell/sourcemap-codec@1.5.5':
+ resolution: {integrity: sha512-cYQ9310grqxueWbl+WuIUIaiUaDcj7WOq5fVhEljNVgRfOUhY9fy2zTvfoqWsnebh8Sl70VScFbICvJnLKB0Og==}
- /@mapbox/node-pre-gyp@1.0.11:
- resolution: {integrity: sha512-Yhlar6v9WQgUp/He7BdgzOz8lqMQ8sU+jkCq7Wx8Myc5YFJLbEe7lgui/V7G1qB1DJykHSGwreceSaD60Y0PUQ==}
- hasBin: true
- dependencies:
- detect-libc: 2.0.3
- https-proxy-agent: 5.0.1
- make-dir: 3.1.0
- node-fetch: 2.7.0
- nopt: 5.0.0
- npmlog: 5.0.1
- rimraf: 3.0.2
- semver: 7.6.0
- tar: 6.2.1
- transitivePeerDependencies:
- - encoding
- - supports-color
- dev: false
+ '@lit-labs/ssr-dom-shim@1.5.1':
+ resolution: {integrity: sha512-Aou5UdlSpr5whQe8AA/bZG0jMj96CoJIWbGfZ91qieWu5AWUMKw8VR/pAkQkJYvBNhmCcWnZlyyk5oze8JIqYA==}
- /@mdit-vue/plugin-component@2.0.0:
- resolution: {integrity: sha512-cTRxlocav/+mfgDcp0P2z/gWuWBez+iNuN4D+b74LpX4AR6UAx2ZvWtCrUZ8VXrO4eCt1/G0YC/Af7mpIb3aoQ==}
- dependencies:
- '@types/markdown-it': 13.0.7
- markdown-it: 14.1.0
- dev: false
+ '@lit/reactive-element@2.1.2':
+ resolution: {integrity: sha512-pbCDiVMnne1lYUIaYNN5wrwQXDtHaYtg7YEFPeW+hws6U47WeFvISGUWekPGKWOP1ygrs0ef0o1VJMk1exos5A==}
- /@mdit-vue/plugin-frontmatter@2.0.0:
- resolution: {integrity: sha512-/LrT6E60QI4XV4mqx3J87hqYXlR7ZyMvndmftR2RGz7cRAwa/xL+kyFLlgrMxkBIKitOShKa3LS/9Ov9b0fU+g==}
- dependencies:
- '@mdit-vue/types': 2.0.0
- '@types/markdown-it': 13.0.7
- gray-matter: 4.0.3
- markdown-it: 14.1.0
- dev: false
+ '@mdit-vue/plugin-component@3.0.2':
+ resolution: {integrity: sha512-Fu53MajrZMOAjOIPGMTdTXgHLgGU9KwTqKtYc6WNYtFZNKw04euSfJ/zFg8eBY/2MlciVngkF7Gyc2IL7e8Bsw==}
+ engines: {node: '>=20.0.0'}
- /@mdit-vue/plugin-headers@2.0.0:
- resolution: {integrity: sha512-ITMMPCnLEYHHgj3XEUL2l75jsNn8guxNqr26YrMSi1f5zcgq4XVy1LIvfwvJ1puqM6Cc5v4BHk3oAyorAi7l1A==}
- dependencies:
- '@mdit-vue/shared': 2.0.0
- '@mdit-vue/types': 2.0.0
- '@types/markdown-it': 13.0.7
- markdown-it: 14.1.0
- dev: false
+ '@mdit-vue/plugin-frontmatter@3.0.2':
+ resolution: {integrity: sha512-QKKgIva31YtqHgSAz7S7hRcL7cHXiqdog4wxTfxeQCHo+9IP4Oi5/r1Y5E93nTPccpadDWzAwr3A0F+kAEnsVQ==}
+ engines: {node: '>=20.0.0'}
- /@mdit-vue/plugin-sfc@2.0.0:
- resolution: {integrity: sha512-OXrMXOyk0iwdIou2jRoIHIbjskwghkO14C9/OjgVHXSSX+iM/WQ4l4yi1aWmNlbQNjtP8IXcVAyJB9K0DFYmLg==}
- dependencies:
- '@mdit-vue/types': 2.0.0
- '@types/markdown-it': 13.0.7
- markdown-it: 14.1.0
- dev: false
+ '@mdit-vue/plugin-headers@3.0.2':
+ resolution: {integrity: sha512-Z3PpDdwBTO5jlW2r617tQibkwtCc5unTnj/Ew1SCxTQaXjtKgwP9WngdSN+xxriISHoNOYzwpoUw/1CW8ntibA==}
+ engines: {node: '>=20.0.0'}
- /@mdit-vue/plugin-title@2.0.0:
- resolution: {integrity: sha512-eqBoETPVkMXNLvwFshz/A2+Cz81VB5HEkXDm0tt6RBW/rTvnoWmGJ1Z+mvcjR5ck5W4nYdIyT68oHxX2JI2M4g==}
- dependencies:
- '@mdit-vue/shared': 2.0.0
- '@mdit-vue/types': 2.0.0
- '@types/markdown-it': 13.0.7
- markdown-it: 14.1.0
- dev: false
+ '@mdit-vue/plugin-sfc@3.0.2':
+ resolution: {integrity: sha512-dhxIrCGu5Nd4Cgo9JJHLjdNy2lMEv+LpimetBHDSeEEJxJBC4TPN0Cljn+3/nV1uJdGyw33UZA86PGdgt1LsoA==}
+ engines: {node: '>=20.0.0'}
- /@mdit-vue/plugin-toc@2.0.0:
- resolution: {integrity: sha512-PKQ8sZna3D5chTnt2lxL+ddpyXd++6Nyc0l8VXCeDgStlySQwiP9jaLeeC88oqY4BtRu4cAmILmxDrvuX0Rrdg==}
- dependencies:
- '@mdit-vue/shared': 2.0.0
- '@mdit-vue/types': 2.0.0
- '@types/markdown-it': 13.0.7
- markdown-it: 14.1.0
- dev: false
+ '@mdit-vue/plugin-title@3.0.2':
+ resolution: {integrity: sha512-KTDP7s68eKTwy4iYp5UauQuVJf+tDMdJZMO6K4feWYS8TX95ItmcxyX7RprfBWLTUwNXBYOifsL6CkIGlWcNjA==}
+ engines: {node: '>=20.0.0'}
- /@mdit-vue/shared@2.0.0:
- resolution: {integrity: sha512-PdxpQpbyTazeo2JT87qms6RPZIzyJd+gwuB+1jSwLDI7+0u5g79y2XgTAbZromSVgY2f3UU5HWdwaLbV9w4uOw==}
- dependencies:
- '@mdit-vue/types': 2.0.0
- '@types/markdown-it': 13.0.7
- markdown-it: 14.1.0
- dev: false
+ '@mdit-vue/plugin-toc@3.0.2':
+ resolution: {integrity: sha512-Dz0dURjD5wR4nBxFMiqb0BTGRAOkCE60byIemqLqnkF6ORKKJ8h5aLF5J5ssbLO87hwu81IikHiaXvqoiEneoQ==}
+ engines: {node: '>=20.0.0'}
+
+ '@mdit-vue/shared@3.0.2':
+ resolution: {integrity: sha512-anFGls154h0iVzUt5O43EaqYvPwzfUxQ34QpNQsUQML7pbEJMhcgkRNvYw9hZBspab+/TP45agdPw5joh6/BBA==}
+ engines: {node: '>=20.0.0'}
+
+ '@mdit-vue/types@3.0.2':
+ resolution: {integrity: sha512-00aAZ0F0NLik6I6Yba2emGbHLxv+QYrPH00qQ5dFKXlAo1Ll2RHDXwY7nN2WAfrx2pP+WrvSRFTGFCNGdzBDHw==}
+ engines: {node: '>=20.0.0'}
- /@mdit-vue/types@2.0.0:
- resolution: {integrity: sha512-1BeEB+DbtmDMUAfvbNUj5Hso8cSl2sBVK2iTyOMAqhfDVLdh+/9+D0JmQHaCeUk/vuJoMhOwbweZvh55wHxm4w==}
- dev: false
+ '@mdit/helper@0.22.1':
+ resolution: {integrity: sha512-lDpajcdAk84aYCNAM/Mi3djw38DJq7ocLw5VOSMu/u2YKX3/OD37a6Qb59in8Uyp4SiAbQoSHa8px6hgHEpB5g==}
+ engines: {node: '>= 18'}
+ peerDependencies:
+ markdown-it: ^14.1.0
+ peerDependenciesMeta:
+ markdown-it:
+ optional: true
- /@mdit/plugin-alert@0.8.0(markdown-it@14.1.0):
- resolution: {integrity: sha512-mxA/lhOyDDR6/qSAegGG/XZRjUbr1wjwdULudbpkA/CCQi6piW9D0Z8crDQGYz4KPQM9Bgx4Ac81QFSzHOV66Q==}
+ '@mdit/plugin-alert@0.22.3':
+ resolution: {integrity: sha512-9g99rjLCFd8upA/DXbhGmEM7GMFocy6SRk4OekxuAy9t1aDOE/r5IJgUbBIvc9kMkg39ug0yXtMkKwAt2zp5Hg==}
peerDependencies:
- markdown-it: ^14.0.0
+ markdown-it: ^14.1.0
peerDependenciesMeta:
markdown-it:
optional: true
- dependencies:
- '@types/markdown-it': 13.0.7
- markdown-it: 14.1.0
- dev: false
- /@mdit/plugin-align@0.8.0(markdown-it@14.1.0):
- resolution: {integrity: sha512-OJPYzSdmT0UZj/QTvnKYE4GelAL0OD8bNIPxpidXbFd3IqYv/8+xMjT6XeR+R3oZEvtbYSc2e1MmO5fo3DopJA==}
+ '@mdit/plugin-align@0.23.0':
+ resolution: {integrity: sha512-6EhhXZr+ts9z28NadaUEkKv7oaLo90fa9Cx0bz3zf0n4BqjEYHIT7yh8L9AfjIz06aEuHrjjLZKc+AfK0rLLrA==}
engines: {node: '>= 18'}
peerDependencies:
- markdown-it: ^14.0.0
+ markdown-it: ^14.1.0
peerDependenciesMeta:
markdown-it:
optional: true
- dependencies:
- '@mdit/plugin-container': 0.8.0(markdown-it@14.1.0)
- '@types/markdown-it': 13.0.7
- markdown-it: 14.1.0
- dev: false
- /@mdit/plugin-attrs@0.8.0(markdown-it@14.1.0):
- resolution: {integrity: sha512-ewmx5i+b3M4CRJNDpDNBA0YTHa1snn+adDsDDpDtPPSzCH1NhtWXdzwI0TrcCQUnueeSEEWX/wY4ESo+NRkBNQ==}
+ '@mdit/plugin-attrs@0.24.1':
+ resolution: {integrity: sha512-/zHY5+DM8wrDhvVVET9jj9vx3m72JnspoT5VPqVuZpBT2nf5GChM38J4lbn9fCXgBSZLkPfYcDEU6LaTlDMOfA==}
engines: {node: '>= 18'}
peerDependencies:
- markdown-it: ^14.0.0
+ markdown-it: ^14.1.0
peerDependenciesMeta:
markdown-it:
optional: true
- dependencies:
- '@types/markdown-it': 13.0.7
- markdown-it: 14.1.0
- dev: false
- /@mdit/plugin-container@0.8.0(markdown-it@14.1.0):
- resolution: {integrity: sha512-uWK3t0CWssintcmT5PTJVhAwbstcD+SrtijQKs6BhLRtGGgHJ9mOf0ybGjlJhn4077yFFTHmaCIT3K+n5ZVjPg==}
+ '@mdit/plugin-container@0.22.2':
+ resolution: {integrity: sha512-QBBti5EyQzVl/qzFAD9YAhiAB9S2zF/4MPAS4kwm7VkmeYrcj2HpZpA7snMjnWh3CtriDcaIMInhg0vDtDwyfA==}
engines: {node: '>= 18'}
peerDependencies:
- markdown-it: ^14.0.0
+ markdown-it: ^14.1.0
peerDependenciesMeta:
markdown-it:
optional: true
- dependencies:
- '@types/markdown-it': 13.0.7
- markdown-it: 14.1.0
- dev: false
- /@mdit/plugin-demo@0.8.0(markdown-it@14.1.0):
- resolution: {integrity: sha512-yFRXnp3Lj0g4H9ImzHKQwwgtSykrL/BDNEQzql9fdA9FbSygfu0CIxfm+A8lsVos8cAvdsgxy3gILySxpfR89g==}
+ '@mdit/plugin-demo@0.22.3':
+ resolution: {integrity: sha512-pK/iJVNPqflo72ZFHbf3a+H6R+l741SPXRnaftZ3ihiT2hlaizg2097eBz2llNkHpFtb3luapux0s/o9AZvA5g==}
peerDependencies:
- markdown-it: ^14.0.0
+ markdown-it: ^14.1.0
peerDependenciesMeta:
markdown-it:
optional: true
- dependencies:
- '@types/markdown-it': 13.0.7
- markdown-it: 14.1.0
- dev: false
- /@mdit/plugin-figure@0.8.0(markdown-it@14.1.0):
- resolution: {integrity: sha512-/o4RoKjnkdWc+K7m6mR7BAu2J79yYE38s8HUc8iKk9v+e9j1E+6LeXcpx1LoPnHzUhT4EO2QmUsv+kAaPFfZYw==}
+ '@mdit/plugin-figure@0.22.2':
+ resolution: {integrity: sha512-mCbrhfbP8VopTzYHw1OnUAEnhh1C24Sx8ExAJpHgnM7HnNF54a+MXbywXZZJAbRZ22l3J2wrxL+IOxKYgNlgdg==}
engines: {node: '>= 18'}
peerDependencies:
- markdown-it: ^14.0.0
+ markdown-it: ^14.1.0
peerDependenciesMeta:
markdown-it:
optional: true
- dependencies:
- '@types/markdown-it': 13.0.7
- markdown-it: 14.1.0
- dev: false
- /@mdit/plugin-footnote@0.8.0(markdown-it@14.1.0):
- resolution: {integrity: sha512-AaX1rfkJwq9vLX+H/a+XQ3ZxahOXrnMLr5dVZfNdazjqdDEJ7Cc/A7UFtLfOM19F2w3EgvcHR1gbINxIVDn/eg==}
+ '@mdit/plugin-footnote@0.22.3':
+ resolution: {integrity: sha512-4hkki9vlIsRDhb7BZLL53s/htRHcubOkjakHPa7Jkj8BZ8/C++0wF13dr73OXcLNVKe/3JWE6pEl1aKETG20Gw==}
engines: {node: '>= 18'}
peerDependencies:
- markdown-it: ^14.0.0
- dependencies:
- '@types/markdown-it': 13.0.7
- markdown-it: 14.1.0
- dev: false
+ markdown-it: ^14.1.0
- /@mdit/plugin-img-lazyload@0.8.0(markdown-it@14.1.0):
- resolution: {integrity: sha512-Rrlf2FzOxxyszbv3DpkIwEgmYKmtwHdxIO+Whkn0a9QckxnEKkaGl5KARCnM7LqX2fhEyFLgnfkr3onVOJG54g==}
- engines: {node: '>= 18'}
+ '@mdit/plugin-icon@0.23.0':
+ resolution: {integrity: sha512-cuK5WhNu/BGbDlfruhTq7O3W0TcLlXIanK6m9hr5pNSqh8i/j/e+kGsn4RFX1aM56EAp69m//n5yg8QgYed1FQ==}
peerDependencies:
- markdown-it: ^14.0.0
+ markdown-it: ^14.1.0
peerDependenciesMeta:
markdown-it:
optional: true
- dependencies:
- '@types/markdown-it': 13.0.7
- markdown-it: 14.1.0
- dev: false
- /@mdit/plugin-img-mark@0.8.0(markdown-it@14.1.0):
- resolution: {integrity: sha512-4P6z2QOfLHLMSXUP4mB/2Rnd6KeHmJBkUXJWJhybcXoIG5S5FDTFHJxOycSP4eGzfdOYAWSlkx6XwXEUGGZz5w==}
+ '@mdit/plugin-img-lazyload@0.22.1':
+ resolution: {integrity: sha512-ombpBQqR1zYjtr4/7s8EvIVx/ymtiflWksXropYz81o0I9Bm9Os1UPuNgjwfT/DEhIit4HMaJhjpKhGkYrOKgA==}
engines: {node: '>= 18'}
peerDependencies:
- markdown-it: ^14.0.0
+ markdown-it: ^14.1.0
peerDependenciesMeta:
markdown-it:
optional: true
- dependencies:
- '@types/markdown-it': 13.0.7
- markdown-it: 14.1.0
- dev: false
- /@mdit/plugin-img-size@0.8.0(markdown-it@14.1.0):
- resolution: {integrity: sha512-r+LbAizP/hw5SisY44VbHEnR7XUKpcHM2k2fwu5wb1+V1crxeigG4sa8rzrJEddU+k6uCl27yL5FTGbHjAl82Q==}
+ '@mdit/plugin-img-mark@0.22.2':
+ resolution: {integrity: sha512-+dfw7HBSg9/ETWguCbhudpIEIsWN81Ro23agEuU8JO1RDpkiMAFVBcUAFqUWr9+4KHQhiBtyEWn1Y7l+d17RXg==}
engines: {node: '>= 18'}
peerDependencies:
- markdown-it: ^14.0.0
+ markdown-it: ^14.1.0
peerDependenciesMeta:
markdown-it:
optional: true
- dependencies:
- '@types/markdown-it': 13.0.7
- markdown-it: 14.1.0
- dev: false
- /@mdit/plugin-include@0.8.0(markdown-it@14.1.0):
- resolution: {integrity: sha512-e8Z8q5VkJ6UX04tTgELraupB/MdHio7hkdYT71wBJ6UQuhSmFv/xMOxFfTcGKH5yzsbEM45BtAFHzSXIi3dMCw==}
+ '@mdit/plugin-img-size@0.22.4':
+ resolution: {integrity: sha512-+hZqo4Ngo6300Jj/pnrcGs0Pn0Jw5qCA8oLtzJqwn+vZHCqxEiyIN/5FJp8etth0aoIyR2K32WhAf5CC2iRCrg==}
+ engines: {node: '>= 18'}
peerDependencies:
- markdown-it: ^14.0.0
+ markdown-it: ^14.1.0
peerDependenciesMeta:
markdown-it:
optional: true
- dependencies:
- '@types/markdown-it': 13.0.7
- markdown-it: 14.1.0
- upath: 2.0.1
- dev: false
- /@mdit/plugin-katex@0.8.0(markdown-it@14.1.0):
- resolution: {integrity: sha512-u7CX3Xv5nuc2bu2sHrk1nil83/9ETKTBMmy0icbW8zlqBC0ykLo1xTCEBXmdhXtnJtPi9f/wUZVs6iMZrJzbNg==}
- engines: {node: '>= 18'}
+ '@mdit/plugin-include@0.22.3':
+ resolution: {integrity: sha512-v28gdUTUCykFE+D9XoQrmO/S+K2kpl+i1f6f+blKfOXSnwT4+l1GqJkQLy1Zs21HUfWBwPmiIrZ0nnX2SO1dbw==}
peerDependencies:
- katex: ^0.16.9
- markdown-it: ^14.0.0
+ markdown-it: ^14.1.0
peerDependenciesMeta:
- katex:
+ markdown-it:
+ optional: true
+
+ '@mdit/plugin-katex-slim@0.25.1':
+ resolution: {integrity: sha512-p5VmsAZULsvPy/WDoS8jRwhCyoV3id11BhnwEHoe7BeCPmnCeOAbFIubR8U77AKed4Pgg7UaIa66SndC0WLavg==}
+ engines: {node: '>= 18'}
+ peerDependencies:
+ katex: ^0.16.25
+ markdown-it: ^14.1.0
+ peerDependenciesMeta:
+ katex:
optional: true
markdown-it:
optional: true
- dependencies:
- '@mdit/plugin-tex': 0.8.0(markdown-it@14.1.0)
- '@types/katex': 0.16.7
- '@types/markdown-it': 13.0.7
- markdown-it: 14.1.0
- dev: false
- /@mdit/plugin-mark@0.8.0(markdown-it@14.1.0):
- resolution: {integrity: sha512-1hImu8FskIZ9dumWD2VIyB5USyVGwGY2IuaPxYO25tFvMZkhu4rYBjkSK8x+vXExwp94OLzFUlGgVl94S+nw9w==}
+ '@mdit/plugin-mark@0.22.1':
+ resolution: {integrity: sha512-2blMM/gGyqPARvaal44mt0pOi+8phmFpj7D4suG4qMd1j8aGDZl9R7p8inbr3BePOady1eloh0SWSCdskmutZg==}
engines: {node: '>= 18'}
peerDependencies:
- markdown-it: ^14.0.0
+ markdown-it: ^14.1.0
peerDependenciesMeta:
markdown-it:
optional: true
- dependencies:
- '@types/markdown-it': 13.0.7
- markdown-it: 14.1.0
- dev: false
- /@mdit/plugin-mathjax@0.8.0(markdown-it@14.1.0)(mathjax-full@3.2.2):
- resolution: {integrity: sha512-y016KQHa3PoXDUIcQseISMAz5q2mZJ/qocEs2EABT4PjquXPEh/4rw7Ql7KX9gf/SQIUyzj8hYs4bHyRZc6x4w==}
+ '@mdit/plugin-mathjax-slim@0.24.1':
+ resolution: {integrity: sha512-jAT/iFXS4D8tSVdlkl4Uzl3JEYsAkvCWDLzNqYyRZD0TU/Wm5mAbLeTXU8hFOu5nKDRNRrF/iKE41Emy1UJUFg==}
engines: {node: '>= 18'}
peerDependencies:
- markdown-it: ^14.0.0
- mathjax-full: ^3.2.2
+ '@mathjax/src': ^4.0.0
+ markdown-it: ^14.1.0
peerDependenciesMeta:
+ '@mathjax/src':
+ optional: true
markdown-it:
optional: true
- mathjax-full:
+
+ '@mdit/plugin-plantuml@0.23.0':
+ resolution: {integrity: sha512-J72Xtuh1CqI7ntNoY2wNOskfxUNxbsdmIZS0uwLI3poSWohgmJe8ZKJpPSrWFxuW6Iiptie6tbynJ1NDr8jEAA==}
+ peerDependencies:
+ markdown-it: ^14.1.0
+ peerDependenciesMeta:
+ markdown-it:
optional: true
- dependencies:
- '@mdit/plugin-tex': 0.8.0(markdown-it@14.1.0)
- '@types/markdown-it': 13.0.7
- markdown-it: 14.1.0
- mathjax-full: 3.2.2
- upath: 2.0.1
- dev: false
- /@mdit/plugin-stylize@0.8.0(markdown-it@14.1.0):
- resolution: {integrity: sha512-oNFI3Z7UTxP8CKxS3CIuawLmsyrc0n9jIw9mPzUcPNp+LtYmLktfZc3FIRlqpUUq34YwHTH3yihayBRdSkVV6A==}
+ '@mdit/plugin-spoiler@0.22.2':
+ resolution: {integrity: sha512-XoL08KwYGaGeCzXuwvOcZLrRvvzvOAj96XF5iihbI1M5LSkzWLY0cWlfgF1mEM1+fAyauZxMYXOegKDqT/HRXg==}
engines: {node: '>= 18'}
peerDependencies:
- markdown-it: ^14.0.0
+ markdown-it: ^14.1.0
peerDependenciesMeta:
markdown-it:
optional: true
- dependencies:
- '@types/markdown-it': 13.0.7
- markdown-it: 14.1.0
- dev: false
- /@mdit/plugin-sub@0.8.0(markdown-it@14.1.0):
- resolution: {integrity: sha512-oqCcmJVJykESgNJ4fFmDKKxRRQddwkXWIT4PjF83XSeXHxTOz8gMfke/V1mE7BAfKKCLP4io8HbrYfvIiOTZ4A==}
+ '@mdit/plugin-stylize@0.22.3':
+ resolution: {integrity: sha512-DnymTaa212l0AkuwzDvaJ1V+pgiwIUuTMU+flNlt/1mKhFWuIFXq1VX+UqdqYB/3/GxuKGOuWjE0AyBo119BCA==}
engines: {node: '>= 18'}
peerDependencies:
- markdown-it: ^14.0.0
+ markdown-it: ^14.1.0
peerDependenciesMeta:
markdown-it:
optional: true
- dependencies:
- '@types/markdown-it': 13.0.7
- markdown-it: 14.1.0
- dev: false
- /@mdit/plugin-sup@0.8.0(markdown-it@14.1.0):
- resolution: {integrity: sha512-5/uE2lONNjCgGDXC8jZ265tzefjUNQNakmK4PSCI4D5jD80xFrxc6MKh70VLCOL8Xk6COK/K9f0SAU2lwa97Tg==}
+ '@mdit/plugin-sub@0.23.0':
+ resolution: {integrity: sha512-wlwIP2eiAvFOL73vgoZ9/6K9jaOc/GO4EvZKHthTT5CD48SORtncB4KOyX45NefVbnYekXWbKYowgKFkuODqnA==}
engines: {node: '>= 18'}
peerDependencies:
- markdown-it: ^14.0.0
+ markdown-it: ^14.1.0
peerDependenciesMeta:
markdown-it:
optional: true
- dependencies:
- '@types/markdown-it': 13.0.7
- markdown-it: 14.1.0
- dev: false
- /@mdit/plugin-tab@0.8.0(markdown-it@14.1.0):
- resolution: {integrity: sha512-SNa1S14Buuy564egiUTkU9HTTNFrEURJZLqA1+jr/2xYCdICPym0FWcB0cLtBl3lrQZkFtbxhzC6ws5JBt/ERQ==}
+ '@mdit/plugin-sup@0.23.0':
+ resolution: {integrity: sha512-T01JDAwHIbeAuW5CPhyVop0292dHPUlYHoUzt4G2UQauwKr66cKN5yuXsIAaqryzahwfwhAMndQ2qySIGYkViQ==}
+ engines: {node: '>= 18'}
peerDependencies:
- markdown-it: ^14.0.0
+ markdown-it: ^14.1.0
+ peerDependenciesMeta:
+ markdown-it:
+ optional: true
+
+ '@mdit/plugin-tab@0.23.0':
+ resolution: {integrity: sha512-x4eSljWYGge+3Kw+zfPnL35GMNiUsgW/kdlNmun9t/3X/hKvN6h53UDeuFM9hvVI0NjUN2VmgKi/QIa/P924ZQ==}
+ peerDependencies:
+ markdown-it: ^14.1.0
peerDependenciesMeta:
markdown-it:
optional: true
- dependencies:
- '@types/markdown-it': 13.0.7
- markdown-it: 14.1.0
- dev: false
- /@mdit/plugin-tasklist@0.8.0(markdown-it@14.1.0):
- resolution: {integrity: sha512-vfOTZdXIL/jk/ConUqCODI5WuqgB9qiBGc+wIa7UMhe73KcpwFeGFJVQZm9AvjhXDDYqznJxSMVRP/TN7TxVVw==}
+ '@mdit/plugin-tasklist@0.22.2':
+ resolution: {integrity: sha512-tYxp4tDomTb9NzIphoDXWJxjQZxFuqP4PjU0H9AecUyWuSRP+HICCqe/HVNTTpB0+WDeuVtnxAW9kX08ekxUWw==}
engines: {node: '>= 18'}
peerDependencies:
- markdown-it: ^14.0.0
+ markdown-it: ^14.1.0
peerDependenciesMeta:
markdown-it:
optional: true
- dependencies:
- '@types/markdown-it': 13.0.7
- markdown-it: 14.1.0
- dev: false
- /@mdit/plugin-tex@0.8.0(markdown-it@14.1.0):
- resolution: {integrity: sha512-uh4kOhwBVEESz6dMmHk4Hn/AVfVtUhMA1UKpwMc1EL9qelodJ0YzSYfNXp6d/PS+E1l53yp8nMZK90DUO+3vpA==}
+ '@mdit/plugin-tex@0.23.0':
+ resolution: {integrity: sha512-oiNlqzpa4S/6rGm5Ht5IvpzvVsDmm1kF95oxKR0ZQmkeMeSXJLVrYgxmMvt8Oj0D+/F5WJ4mYCD+kXDaLxI0gg==}
engines: {node: '>= 18'}
peerDependencies:
- markdown-it: ^14.0.0
+ markdown-it: ^14.1.0
peerDependenciesMeta:
markdown-it:
optional: true
- dependencies:
- '@types/markdown-it': 13.0.7
- markdown-it: 14.1.0
- dev: false
- /@mdit/plugin-uml@0.8.0(markdown-it@14.1.0):
- resolution: {integrity: sha512-6TOVxLhmdzV7bzjlJCRP5uCFq62Xwk2ZAeYUK3RLx9lgM3s2Mww5ENhdysnQMd7VQlUHsPmp4XIMBZZjPddg3g==}
+ '@mdit/plugin-uml@0.23.0':
+ resolution: {integrity: sha512-pxu5jSASNwHe6qWvicEpqo8Kp54onGgHDbO/enG+jURDv19bXHVhbyd7ac50g4ROb9rRS9aPTWZT+PxVBTLjXQ==}
engines: {node: '>= 18'}
peerDependencies:
- markdown-it: ^14.0.0
+ markdown-it: ^14.1.0
peerDependenciesMeta:
markdown-it:
optional: true
- dependencies:
- '@types/markdown-it': 13.0.7
- markdown-it: 14.1.0
- dev: false
- /@nodelib/fs.scandir@2.1.5:
+ '@mermaid-js/parser@0.6.3':
+ resolution: {integrity: sha512-lnjOhe7zyHjc+If7yT4zoedx2vo4sHaTmtkl1+or8BRTnCtDmcTpAjpzDSfCZrshM5bCoz0GyidzadJAH1xobA==}
+
+ '@nodelib/fs.scandir@2.1.5':
resolution: {integrity: sha512-vq24Bq3ym5HEQm2NKCr3yXDwjc7vTsEThRDnkp2DK9p1uqLR+DHurm/NOTo0KG7HYHU7eppKZj3MyqYuMBf62g==}
engines: {node: '>= 8'}
- dependencies:
- '@nodelib/fs.stat': 2.0.5
- run-parallel: 1.2.0
- dev: false
- /@nodelib/fs.stat@2.0.5:
+ '@nodelib/fs.stat@2.0.5':
resolution: {integrity: sha512-RkhPPp2zrqDAQA/2jNhnztcPAlv64XdhIp7a7454A5ovI7Bukxgt7MX7udwAu3zg1DcpPU0rz3VV1SeaqvY4+A==}
engines: {node: '>= 8'}
- dev: false
- /@nodelib/fs.walk@1.2.8:
+ '@nodelib/fs.walk@1.2.8':
resolution: {integrity: sha512-oGB+UxlgWcgQkgwo8GcEGwemoTFt3FIO9ababBmaGwXIoBKZ+GTy0pP185beGg7Llih/NSHSV2XAs1lnznocSg==}
engines: {node: '>= 8'}
- dependencies:
- '@nodelib/fs.scandir': 2.1.5
- fastq: 1.17.1
- dev: false
-
- /@npmcli/agent@2.2.1:
- resolution: {integrity: sha512-H4FrOVtNyWC8MUwL3UfjOsAihHvT1Pe8POj3JvjXhSTJipsZMtgUALCT4mGyYZNxymkUfOw3PUj6dE4QPp6osQ==}
- engines: {node: ^16.14.0 || >=18.0.0}
- dependencies:
- agent-base: 7.1.1
- http-proxy-agent: 7.0.2
- https-proxy-agent: 7.0.4
- lru-cache: 10.2.0
- socks-proxy-agent: 8.0.3
- transitivePeerDependencies:
- - supports-color
- dev: false
- /@npmcli/fs@3.1.0:
- resolution: {integrity: sha512-7kZUAaLscfgbwBQRbvdMYaZOWyMEcPTH/tJjnyAWJ/dvvs9Ef+CERx/qJb9GExJpl1qipaDGn7KqHnFGGixd0w==}
- engines: {node: ^14.17.0 || ^16.13.0 || >=18.0.0}
- dependencies:
- semver: 7.6.0
- dev: false
+ '@parcel/watcher-android-arm64@2.5.4':
+ resolution: {integrity: sha512-hoh0vx4v+b3BNI7Cjoy2/B0ARqcwVNrzN/n7DLq9ZB4I3lrsvhrkCViJyfTj/Qi5xM9YFiH4AmHGK6pgH1ss7g==}
+ engines: {node: '>= 10.0.0'}
+ cpu: [arm64]
+ os: [android]
- /@pkgjs/parseargs@0.11.0:
- resolution: {integrity: sha512-+1VkjdD0QBLPodGrJUeqarH8VAIvQODIbwh9XpP5Syisf7YoQgsJKPNFoqqLQlu+VQ/tVSshMR6loPMn8U+dPg==}
- engines: {node: '>=14'}
- requiresBuild: true
- dev: false
- optional: true
+ '@parcel/watcher-darwin-arm64@2.5.4':
+ resolution: {integrity: sha512-kphKy377pZiWpAOyTgQYPE5/XEKVMaj6VUjKT5VkNyUJlr2qZAn8gIc7CPzx+kbhvqHDT9d7EqdOqRXT6vk0zw==}
+ engines: {node: '>= 10.0.0'}
+ cpu: [arm64]
+ os: [darwin]
+
+ '@parcel/watcher-darwin-x64@2.5.4':
+ resolution: {integrity: sha512-UKaQFhCtNJW1A9YyVz3Ju7ydf6QgrpNQfRZ35wNKUhTQ3dxJ/3MULXN5JN/0Z80V/KUBDGa3RZaKq1EQT2a2gg==}
+ engines: {node: '>= 10.0.0'}
+ cpu: [x64]
+ os: [darwin]
+
+ '@parcel/watcher-freebsd-x64@2.5.4':
+ resolution: {integrity: sha512-Dib0Wv3Ow/m2/ttvLdeI2DBXloO7t3Z0oCp4bAb2aqyqOjKPPGrg10pMJJAQ7tt8P4V2rwYwywkDhUia/FgS+Q==}
+ engines: {node: '>= 10.0.0'}
+ cpu: [x64]
+ os: [freebsd]
+
+ '@parcel/watcher-linux-arm-glibc@2.5.4':
+ resolution: {integrity: sha512-I5Vb769pdf7Q7Sf4KNy8Pogl/URRCKu9ImMmnVKYayhynuyGYMzuI4UOWnegQNa2sGpsPSbzDsqbHNMyeyPCgw==}
+ engines: {node: '>= 10.0.0'}
+ cpu: [arm]
+ os: [linux]
+ libc: [glibc]
+
+ '@parcel/watcher-linux-arm-musl@2.5.4':
+ resolution: {integrity: sha512-kGO8RPvVrcAotV4QcWh8kZuHr9bXi9a3bSZw7kFarYR0+fGliU7hd/zevhjw8fnvIKG3J9EO5G6sXNGCSNMYPQ==}
+ engines: {node: '>= 10.0.0'}
+ cpu: [arm]
+ os: [linux]
+ libc: [musl]
+
+ '@parcel/watcher-linux-arm64-glibc@2.5.4':
+ resolution: {integrity: sha512-KU75aooXhqGFY2W5/p8DYYHt4hrjHZod8AhcGAmhzPn/etTa+lYCDB2b1sJy3sWJ8ahFVTdy+EbqSBvMx3iFlw==}
+ engines: {node: '>= 10.0.0'}
+ cpu: [arm64]
+ os: [linux]
+ libc: [glibc]
+
+ '@parcel/watcher-linux-arm64-musl@2.5.4':
+ resolution: {integrity: sha512-Qx8uNiIekVutnzbVdrgSanM+cbpDD3boB1f8vMtnuG5Zau4/bdDbXyKwIn0ToqFhIuob73bcxV9NwRm04/hzHQ==}
+ engines: {node: '>= 10.0.0'}
+ cpu: [arm64]
+ os: [linux]
+ libc: [musl]
+
+ '@parcel/watcher-linux-x64-glibc@2.5.4':
+ resolution: {integrity: sha512-UYBQvhYmgAv61LNUn24qGQdjtycFBKSK3EXr72DbJqX9aaLbtCOO8+1SkKhD/GNiJ97ExgcHBrukcYhVjrnogA==}
+ engines: {node: '>= 10.0.0'}
+ cpu: [x64]
+ os: [linux]
+ libc: [glibc]
+
+ '@parcel/watcher-linux-x64-musl@2.5.4':
+ resolution: {integrity: sha512-YoRWCVgxv8akZrMhdyVi6/TyoeeMkQ0PGGOf2E4omODrvd1wxniXP+DBynKoHryStks7l+fDAMUBRzqNHrVOpg==}
+ engines: {node: '>= 10.0.0'}
+ cpu: [x64]
+ os: [linux]
+ libc: [musl]
+
+ '@parcel/watcher-win32-arm64@2.5.4':
+ resolution: {integrity: sha512-iby+D/YNXWkiQNYcIhg8P5hSjzXEHaQrk2SLrWOUD7VeC4Ohu0WQvmV+HDJokZVJ2UjJ4AGXW3bx7Lls9Ln4TQ==}
+ engines: {node: '>= 10.0.0'}
+ cpu: [arm64]
+ os: [win32]
+
+ '@parcel/watcher-win32-ia32@2.5.4':
+ resolution: {integrity: sha512-vQN+KIReG0a2ZDpVv8cgddlf67J8hk1WfZMMP7sMeZmJRSmEax5xNDNWKdgqSe2brOKTQQAs3aCCUal2qBHAyg==}
+ engines: {node: '>= 10.0.0'}
+ cpu: [ia32]
+ os: [win32]
+
+ '@parcel/watcher-win32-x64@2.5.4':
+ resolution: {integrity: sha512-3A6efb6BOKwyw7yk9ro2vus2YTt2nvcd56AuzxdMiVOxL9umDyN5PKkKfZ/gZ9row41SjVmTVQNWQhaRRGpOKw==}
+ engines: {node: '>= 10.0.0'}
+ cpu: [x64]
+ os: [win32]
+
+ '@parcel/watcher@2.5.4':
+ resolution: {integrity: sha512-WYa2tUVV5HiArWPB3ydlOc4R2ivq0IDrlqhMi3l7mVsFEXNcTfxYFPIHXHXIh/ca/y/V5N4E1zecyxdIBjYnkQ==}
+ engines: {node: '>= 10.0.0'}
+
+ '@pkgr/core@0.2.9':
+ resolution: {integrity: sha512-QNqXyfVS2wm9hweSYD2O7F0G06uurj9kZ96TRQE5Y9hU7+tgdZwIkbAKc5Ocy1HxEY2kuDQa6cQ1WRs/O5LFKA==}
+ engines: {node: ^12.20.0 || ^14.18.0 || >=16.0.0}
- /@rollup/rollup-android-arm-eabi@4.13.2:
- resolution: {integrity: sha512-3XFIDKWMFZrMnao1mJhnOT1h2g0169Os848NhhmGweEcfJ4rCi+3yMCOLG4zA61rbJdkcrM/DjVZm9Hg5p5w7g==}
+ '@rolldown/pluginutils@1.0.0-beta.53':
+ resolution: {integrity: sha512-vENRlFU4YbrwVqNDZ7fLvy+JR1CRkyr01jhSiDpE1u6py3OMzQfztQU2jxykW3ALNxO4kSlqIDeYyD0Y9RcQeQ==}
+
+ '@rollup/rollup-android-arm-eabi@4.55.1':
+ resolution: {integrity: sha512-9R0DM/ykwfGIlNu6+2U09ga0WXeZ9MRC2Ter8jnz8415VbuIykVuc6bhdrbORFZANDmTDvq26mJrEVTl8TdnDg==}
cpu: [arm]
os: [android]
- requiresBuild: true
- dev: false
- optional: true
- /@rollup/rollup-android-arm64@4.13.2:
- resolution: {integrity: sha512-GdxxXbAuM7Y/YQM9/TwwP+L0omeE/lJAR1J+olu36c3LqqZEBdsIWeQ91KBe6nxwOnb06Xh7JS2U5ooWU5/LgQ==}
+ '@rollup/rollup-android-arm64@4.55.1':
+ resolution: {integrity: sha512-eFZCb1YUqhTysgW3sj/55du5cG57S7UTNtdMjCW7LwVcj3dTTcowCsC8p7uBdzKsZYa8J7IDE8lhMI+HX1vQvg==}
cpu: [arm64]
os: [android]
- requiresBuild: true
- dev: false
- optional: true
- /@rollup/rollup-darwin-arm64@4.13.2:
- resolution: {integrity: sha512-mCMlpzlBgOTdaFs83I4XRr8wNPveJiJX1RLfv4hggyIVhfB5mJfN4P8Z6yKh+oE4Luz+qq1P3kVdWrCKcMYrrA==}
+ '@rollup/rollup-darwin-arm64@4.55.1':
+ resolution: {integrity: sha512-p3grE2PHcQm2e8PSGZdzIhCKbMCw/xi9XvMPErPhwO17vxtvCN5FEA2mSLgmKlCjHGMQTP6phuQTYWUnKewwGg==}
cpu: [arm64]
os: [darwin]
- requiresBuild: true
- dev: false
- optional: true
- /@rollup/rollup-darwin-x64@4.13.2:
- resolution: {integrity: sha512-yUoEvnH0FBef/NbB1u6d3HNGyruAKnN74LrPAfDQL3O32e3k3OSfLrPgSJmgb3PJrBZWfPyt6m4ZhAFa2nZp2A==}
+ '@rollup/rollup-darwin-x64@4.55.1':
+ resolution: {integrity: sha512-rDUjG25C9qoTm+e02Esi+aqTKSBYwVTaoS1wxcN47/Luqef57Vgp96xNANwt5npq9GDxsH7kXxNkJVEsWEOEaQ==}
cpu: [x64]
os: [darwin]
- requiresBuild: true
- dev: false
- optional: true
- /@rollup/rollup-linux-arm-gnueabihf@4.13.2:
- resolution: {integrity: sha512-GYbLs5ErswU/Xs7aGXqzc3RrdEjKdmoCrgzhJWyFL0r5fL3qd1NPcDKDowDnmcoSiGJeU68/Vy+OMUluRxPiLQ==}
+ '@rollup/rollup-freebsd-arm64@4.55.1':
+ resolution: {integrity: sha512-+JiU7Jbp5cdxekIgdte0jfcu5oqw4GCKr6i3PJTlXTCU5H5Fvtkpbs4XJHRmWNXF+hKmn4v7ogI5OQPaupJgOg==}
+ cpu: [arm64]
+ os: [freebsd]
+
+ '@rollup/rollup-freebsd-x64@4.55.1':
+ resolution: {integrity: sha512-V5xC1tOVWtLLmr3YUk2f6EJK4qksksOYiz/TCsFHu/R+woubcLWdC9nZQmwjOAbmExBIVKsm1/wKmEy4z4u4Bw==}
+ cpu: [x64]
+ os: [freebsd]
+
+ '@rollup/rollup-linux-arm-gnueabihf@4.55.1':
+ resolution: {integrity: sha512-Rn3n+FUk2J5VWx+ywrG/HGPTD9jXNbicRtTM11e/uorplArnXZYsVifnPPqNNP5BsO3roI4n8332ukpY/zN7rQ==}
cpu: [arm]
os: [linux]
- requiresBuild: true
- dev: false
- optional: true
+ libc: [glibc]
+
+ '@rollup/rollup-linux-arm-musleabihf@4.55.1':
+ resolution: {integrity: sha512-grPNWydeKtc1aEdrJDWk4opD7nFtQbMmV7769hiAaYyUKCT1faPRm2av8CX1YJsZ4TLAZcg9gTR1KvEzoLjXkg==}
+ cpu: [arm]
+ os: [linux]
+ libc: [musl]
- /@rollup/rollup-linux-arm64-gnu@4.13.2:
- resolution: {integrity: sha512-L1+D8/wqGnKQIlh4Zre9i4R4b4noxzH5DDciyahX4oOz62CphY7WDWqJoQ66zNR4oScLNOqQJfNSIAe/6TPUmQ==}
+ '@rollup/rollup-linux-arm64-gnu@4.55.1':
+ resolution: {integrity: sha512-a59mwd1k6x8tXKcUxSyISiquLwB5pX+fJW9TkWU46lCqD/GRDe9uDN31jrMmVP3feI3mhAdvcCClhV8V5MhJFQ==}
cpu: [arm64]
os: [linux]
libc: [glibc]
- requiresBuild: true
- dev: false
- optional: true
- /@rollup/rollup-linux-arm64-musl@4.13.2:
- resolution: {integrity: sha512-tK5eoKFkXdz6vjfkSTCupUzCo40xueTOiOO6PeEIadlNBkadH1wNOH8ILCPIl8by/Gmb5AGAeQOFeLev7iZDOA==}
+ '@rollup/rollup-linux-arm64-musl@4.55.1':
+ resolution: {integrity: sha512-puS1MEgWX5GsHSoiAsF0TYrpomdvkaXm0CofIMG5uVkP6IBV+ZO9xhC5YEN49nsgYo1DuuMquF9+7EDBVYu4uA==}
cpu: [arm64]
os: [linux]
libc: [musl]
- requiresBuild: true
- dev: false
- optional: true
- /@rollup/rollup-linux-powerpc64le-gnu@4.13.2:
- resolution: {integrity: sha512-zvXvAUGGEYi6tYhcDmb9wlOckVbuD+7z3mzInCSTACJ4DQrdSLPNUeDIcAQW39M3q6PDquqLWu7pnO39uSMRzQ==}
- cpu: [ppc64le]
+ '@rollup/rollup-linux-loong64-gnu@4.55.1':
+ resolution: {integrity: sha512-r3Wv40in+lTsULSb6nnoudVbARdOwb2u5fpeoOAZjFLznp6tDU8kd+GTHmJoqZ9lt6/Sys33KdIHUaQihFcu7g==}
+ cpu: [loong64]
os: [linux]
libc: [glibc]
- requiresBuild: true
- dev: false
- optional: true
- /@rollup/rollup-linux-riscv64-gnu@4.13.2:
- resolution: {integrity: sha512-C3GSKvMtdudHCN5HdmAMSRYR2kkhgdOfye4w0xzyii7lebVr4riCgmM6lRiSCnJn2w1Xz7ZZzHKuLrjx5620kw==}
+ '@rollup/rollup-linux-loong64-musl@4.55.1':
+ resolution: {integrity: sha512-MR8c0+UxAlB22Fq4R+aQSPBayvYa3+9DrwG/i1TKQXFYEaoW3B5b/rkSRIypcZDdWjWnpcvxbNaAJDcSbJU3Lw==}
+ cpu: [loong64]
+ os: [linux]
+ libc: [musl]
+
+ '@rollup/rollup-linux-ppc64-gnu@4.55.1':
+ resolution: {integrity: sha512-3KhoECe1BRlSYpMTeVrD4sh2Pw2xgt4jzNSZIIPLFEsnQn9gAnZagW9+VqDqAHgm1Xc77LzJOo2LdigS5qZ+gw==}
+ cpu: [ppc64]
+ os: [linux]
+ libc: [glibc]
+
+ '@rollup/rollup-linux-ppc64-musl@4.55.1':
+ resolution: {integrity: sha512-ziR1OuZx0vdYZZ30vueNZTg73alF59DicYrPViG0NEgDVN8/Jl87zkAPu4u6VjZST2llgEUjaiNl9JM6HH1Vdw==}
+ cpu: [ppc64]
+ os: [linux]
+ libc: [musl]
+
+ '@rollup/rollup-linux-riscv64-gnu@4.55.1':
+ resolution: {integrity: sha512-uW0Y12ih2XJRERZ4jAfKamTyIHVMPQnTZcQjme2HMVDAHY4amf5u414OqNYC+x+LzRdRcnIG1YodLrrtA8xsxw==}
cpu: [riscv64]
os: [linux]
libc: [glibc]
- requiresBuild: true
- dev: false
- optional: true
- /@rollup/rollup-linux-s390x-gnu@4.13.2:
- resolution: {integrity: sha512-l4U0KDFwzD36j7HdfJ5/TveEQ1fUTjFFQP5qIt9gBqBgu1G8/kCaq5Ok05kd5TG9F8Lltf3MoYsUMw3rNlJ0Yg==}
+ '@rollup/rollup-linux-riscv64-musl@4.55.1':
+ resolution: {integrity: sha512-u9yZ0jUkOED1BFrqu3BwMQoixvGHGZ+JhJNkNKY/hyoEgOwlqKb62qu+7UjbPSHYjiVy8kKJHvXKv5coH4wDeg==}
+ cpu: [riscv64]
+ os: [linux]
+ libc: [musl]
+
+ '@rollup/rollup-linux-s390x-gnu@4.55.1':
+ resolution: {integrity: sha512-/0PenBCmqM4ZUd0190j7J0UsQ/1nsi735iPRakO8iPciE7BQ495Y6msPzaOmvx0/pn+eJVVlZrNrSh4WSYLxNg==}
cpu: [s390x]
os: [linux]
libc: [glibc]
- requiresBuild: true
- dev: false
- optional: true
- /@rollup/rollup-linux-x64-gnu@4.13.2:
- resolution: {integrity: sha512-xXMLUAMzrtsvh3cZ448vbXqlUa7ZL8z0MwHp63K2IIID2+DeP5iWIT6g1SN7hg1VxPzqx0xZdiDM9l4n9LRU1A==}
+ '@rollup/rollup-linux-x64-gnu@4.55.1':
+ resolution: {integrity: sha512-a8G4wiQxQG2BAvo+gU6XrReRRqj+pLS2NGXKm8io19goR+K8lw269eTrPkSdDTALwMmJp4th2Uh0D8J9bEV1vg==}
cpu: [x64]
os: [linux]
libc: [glibc]
- requiresBuild: true
- dev: false
- optional: true
- /@rollup/rollup-linux-x64-musl@4.13.2:
- resolution: {integrity: sha512-M/JYAWickafUijWPai4ehrjzVPKRCyDb1SLuO+ZyPfoXgeCEAlgPkNXewFZx0zcnoIe3ay4UjXIMdXQXOZXWqA==}
+ '@rollup/rollup-linux-x64-musl@4.55.1':
+ resolution: {integrity: sha512-bD+zjpFrMpP/hqkfEcnjXWHMw5BIghGisOKPj+2NaNDuVT+8Ds4mPf3XcPHuat1tz89WRL+1wbcxKY3WSbiT7w==}
cpu: [x64]
os: [linux]
libc: [musl]
- requiresBuild: true
- dev: false
- optional: true
- /@rollup/rollup-win32-arm64-msvc@4.13.2:
- resolution: {integrity: sha512-2YWwoVg9KRkIKaXSh0mz3NmfurpmYoBBTAXA9qt7VXk0Xy12PoOP40EFuau+ajgALbbhi4uTj3tSG3tVseCjuA==}
+ '@rollup/rollup-openbsd-x64@4.55.1':
+ resolution: {integrity: sha512-eLXw0dOiqE4QmvikfQ6yjgkg/xDM+MdU9YJuP4ySTibXU0oAvnEWXt7UDJmD4UkYialMfOGFPJnIHSe/kdzPxg==}
+ cpu: [x64]
+ os: [openbsd]
+
+ '@rollup/rollup-openharmony-arm64@4.55.1':
+ resolution: {integrity: sha512-xzm44KgEP11te3S2HCSyYf5zIzWmx3n8HDCc7EE59+lTcswEWNpvMLfd9uJvVX8LCg9QWG67Xt75AuHn4vgsXw==}
+ cpu: [arm64]
+ os: [openharmony]
+
+ '@rollup/rollup-win32-arm64-msvc@4.55.1':
+ resolution: {integrity: sha512-yR6Bl3tMC/gBok5cz/Qi0xYnVbIxGx5Fcf/ca0eB6/6JwOY+SRUcJfI0OpeTpPls7f194as62thCt/2BjxYN8g==}
cpu: [arm64]
os: [win32]
- requiresBuild: true
- dev: false
- optional: true
- /@rollup/rollup-win32-ia32-msvc@4.13.2:
- resolution: {integrity: sha512-2FSsE9aQ6OWD20E498NYKEQLneShWes0NGMPQwxWOdws35qQXH+FplabOSP5zEe1pVjurSDOGEVCE2agFwSEsw==}
+ '@rollup/rollup-win32-ia32-msvc@4.55.1':
+ resolution: {integrity: sha512-3fZBidchE0eY0oFZBnekYCfg+5wAB0mbpCBuofh5mZuzIU/4jIVkbESmd2dOsFNS78b53CYv3OAtwqkZZmU5nA==}
cpu: [ia32]
os: [win32]
- requiresBuild: true
- dev: false
- optional: true
- /@rollup/rollup-win32-x64-msvc@4.13.2:
- resolution: {integrity: sha512-7h7J2nokcdPePdKykd8wtc8QqqkqxIrUz7MHj6aNr8waBRU//NLDVnNjQnqQO6fqtjrtCdftpbTuOKAyrAQETQ==}
+ '@rollup/rollup-win32-x64-gnu@4.55.1':
+ resolution: {integrity: sha512-xGGY5pXj69IxKb4yv/POoocPy/qmEGhimy/FoTpTSVju3FYXUQQMFCaZZXJVidsmGxRioZAwpThl/4zX41gRKg==}
cpu: [x64]
os: [win32]
- requiresBuild: true
- dev: false
- optional: true
- /@sindresorhus/merge-streams@1.0.0:
- resolution: {integrity: sha512-rUV5WyJrJLoloD4NDN1V1+LDMDWOa4OTsT4yYJwQNpTU6FWxkxHpL7eu4w+DmiH8x/EAM1otkPE1+LaspIbplw==}
- engines: {node: '>=18'}
- dev: false
+ '@rollup/rollup-win32-x64-msvc@4.55.1':
+ resolution: {integrity: sha512-SPEpaL6DX4rmcXtnhdrQYgzQ5W2uW3SCJch88lB2zImhJRhIIK44fkUrgIV/Q8yUNfw5oyZ5vkeQsZLhCb06lw==}
+ cpu: [x64]
+ os: [win32]
+
+ '@shikijs/core@3.21.0':
+ resolution: {integrity: sha512-AXSQu/2n1UIQekY8euBJlvFYZIw0PHY63jUzGbrOma4wPxzznJXTXkri+QcHeBNaFxiiOljKxxJkVSoB3PjbyA==}
+
+ '@shikijs/engine-javascript@3.21.0':
+ resolution: {integrity: sha512-ATwv86xlbmfD9n9gKRiwuPpWgPENAWCLwYCGz9ugTJlsO2kOzhOkvoyV/UD+tJ0uT7YRyD530x6ugNSffmvIiQ==}
+
+ '@shikijs/engine-oniguruma@3.21.0':
+ resolution: {integrity: sha512-OYknTCct6qiwpQDqDdf3iedRdzj6hFlOPv5hMvI+hkWfCKs5mlJ4TXziBG9nyabLwGulrUjHiCq3xCspSzErYQ==}
+
+ '@shikijs/langs@3.21.0':
+ resolution: {integrity: sha512-g6mn5m+Y6GBJ4wxmBYqalK9Sp0CFkUqfNzUy2pJglUginz6ZpWbaWjDB4fbQ/8SHzFjYbtU6Ddlp1pc+PPNDVA==}
+
+ '@shikijs/themes@3.21.0':
+ resolution: {integrity: sha512-BAE4cr9EDiZyYzwIHEk7JTBJ9CzlPuM4PchfcA5ao1dWXb25nv6hYsoDiBq2aZK9E3dlt3WB78uI96UESD+8Mw==}
+
+ '@shikijs/transformers@3.21.0':
+ resolution: {integrity: sha512-CZwvCWWIiRRiFk9/JKzdEooakAP8mQDtBOQ1TKiCaS2E1bYtyBCOkUzS8akO34/7ufICQ29oeSfkb3tT5KtrhA==}
- /@sindresorhus/merge-streams@2.3.0:
+ '@shikijs/types@3.21.0':
+ resolution: {integrity: sha512-zGrWOxZ0/+0ovPY7PvBU2gIS9tmhSUUt30jAcNV0Bq0gb2S98gwfjIs1vxlmH5zM7/4YxLamT6ChlqqAJmPPjA==}
+
+ '@shikijs/vscode-textmate@10.0.2':
+ resolution: {integrity: sha512-83yeghZ2xxin3Nj8z1NMd/NCuca+gsYXswywDy5bHvwlWL8tpTQmzGeUuHd9FC3E/SBEMvzJRwWEOz5gGes9Qg==}
+
+ '@sindresorhus/merge-streams@2.3.0':
resolution: {integrity: sha512-LtoMMhxAlorcGhmFYI+LhPgbPZCkgP6ra1YL604EeF6U98pLlQ3iWIGMdWSC+vWmPBWBNgmDBAhnAobLROJmwg==}
engines: {node: '>=18'}
- dev: false
- /@stackblitz/sdk@1.9.0:
- resolution: {integrity: sha512-3m6C7f8pnR5KXys/Hqx2x6ylnpqOak6HtnZI6T5keEO0yT+E4Spkw37VEbdwuC+2oxmjdgq6YZEgiKX7hM1GmQ==}
- dev: false
+ '@stackblitz/sdk@1.11.0':
+ resolution: {integrity: sha512-DFQGANNkEZRzFk1/rDP6TcFdM82ycHE+zfl9C/M/jXlH68jiqHWHFMQURLELoD8koxvu/eW5uhg94NSAZlYrUQ==}
+
+ '@types/d3-array@3.2.2':
+ resolution: {integrity: sha512-hOLWVbm7uRza0BYXpIIW5pxfrKe0W+D5lrFiAEYR+pb6w3N2SwSMaJbXdUfSEv+dT4MfHBLtn5js0LAWaO6otw==}
+
+ '@types/d3-axis@3.0.6':
+ resolution: {integrity: sha512-pYeijfZuBd87T0hGn0FO1vQ/cgLk6E1ALJjfkC0oJ8cbwkZl3TpgS8bVBLZN+2jjGgg38epgxb2zmoGtSfvgMw==}
+
+ '@types/d3-brush@3.0.6':
+ resolution: {integrity: sha512-nH60IZNNxEcrh6L1ZSMNA28rj27ut/2ZmI3r96Zd+1jrZD++zD3LsMIjWlvg4AYrHn/Pqz4CF3veCxGjtbqt7A==}
+
+ '@types/d3-chord@3.0.6':
+ resolution: {integrity: sha512-LFYWWd8nwfwEmTZG9PfQxd17HbNPksHBiJHaKuY1XeqscXacsS2tyoo6OdRsjf+NQYeB6XrNL3a25E3gH69lcg==}
+
+ '@types/d3-color@3.1.3':
+ resolution: {integrity: sha512-iO90scth9WAbmgv7ogoq57O9YpKmFBbmoEoCHDB2xMBY0+/KVrqAaCDyCE16dUspeOvIxFFRI+0sEtqDqy2b4A==}
+
+ '@types/d3-contour@3.0.6':
+ resolution: {integrity: sha512-BjzLgXGnCWjUSYGfH1cpdo41/hgdWETu4YxpezoztawmqsvCeep+8QGfiY6YbDvfgHz/DkjeIkkZVJavB4a3rg==}
+
+ '@types/d3-delaunay@6.0.4':
+ resolution: {integrity: sha512-ZMaSKu4THYCU6sV64Lhg6qjf1orxBthaC161plr5KuPHo3CNm8DTHiLw/5Eq2b6TsNP0W0iJrUOFscY6Q450Hw==}
+
+ '@types/d3-dispatch@3.0.7':
+ resolution: {integrity: sha512-5o9OIAdKkhN1QItV2oqaE5KMIiXAvDWBDPrD85e58Qlz1c1kI/J0NcqbEG88CoTwJrYe7ntUCVfeUl2UJKbWgA==}
+
+ '@types/d3-drag@3.0.7':
+ resolution: {integrity: sha512-HE3jVKlzU9AaMazNufooRJ5ZpWmLIoc90A37WU2JMmeq28w1FQqCZswHZ3xR+SuxYftzHq6WU6KJHvqxKzTxxQ==}
+
+ '@types/d3-dsv@3.0.7':
+ resolution: {integrity: sha512-n6QBF9/+XASqcKK6waudgL0pf/S5XHPPI8APyMLLUHd8NqouBGLsU8MgtO7NINGtPBtk9Kko/W4ea0oAspwh9g==}
+
+ '@types/d3-ease@3.0.2':
+ resolution: {integrity: sha512-NcV1JjO5oDzoK26oMzbILE6HW7uVXOHLQvHshBUW4UMdZGfiY6v5BeQwh9a9tCzv+CeefZQHJt5SRgK154RtiA==}
+
+ '@types/d3-fetch@3.0.7':
+ resolution: {integrity: sha512-fTAfNmxSb9SOWNB9IoG5c8Hg6R+AzUHDRlsXsDZsNp6sxAEOP0tkP3gKkNSO/qmHPoBFTxNrjDprVHDQDvo5aA==}
+
+ '@types/d3-force@3.0.10':
+ resolution: {integrity: sha512-ZYeSaCF3p73RdOKcjj+swRlZfnYpK1EbaDiYICEEp5Q6sUiqFaFQ9qgoshp5CzIyyb/yD09kD9o2zEltCexlgw==}
+
+ '@types/d3-format@3.0.4':
+ resolution: {integrity: sha512-fALi2aI6shfg7vM5KiR1wNJnZ7r6UuggVqtDA+xiEdPZQwy/trcQaHnwShLuLdta2rTymCNpxYTiMZX/e09F4g==}
+
+ '@types/d3-geo@3.1.0':
+ resolution: {integrity: sha512-856sckF0oP/diXtS4jNsiQw/UuK5fQG8l/a9VVLeSouf1/PPbBE1i1W852zVwKwYCBkFJJB7nCFTbk6UMEXBOQ==}
+
+ '@types/d3-hierarchy@3.1.7':
+ resolution: {integrity: sha512-tJFtNoYBtRtkNysX1Xq4sxtjK8YgoWUNpIiUee0/jHGRwqvzYxkq0hGVbbOGSz+JgFxxRu4K8nb3YpG3CMARtg==}
+
+ '@types/d3-interpolate@3.0.4':
+ resolution: {integrity: sha512-mgLPETlrpVV1YRJIglr4Ez47g7Yxjl1lj7YKsiMCb27VJH9W8NVM6Bb9d8kkpG/uAQS5AmbA48q2IAolKKo1MA==}
+
+ '@types/d3-path@3.1.1':
+ resolution: {integrity: sha512-VMZBYyQvbGmWyWVea0EHs/BwLgxc+MKi1zLDCONksozI4YJMcTt8ZEuIR4Sb1MMTE8MMW49v0IwI5+b7RmfWlg==}
+
+ '@types/d3-polygon@3.0.2':
+ resolution: {integrity: sha512-ZuWOtMaHCkN9xoeEMr1ubW2nGWsp4nIql+OPQRstu4ypeZ+zk3YKqQT0CXVe/PYqrKpZAi+J9mTs05TKwjXSRA==}
+
+ '@types/d3-quadtree@3.0.6':
+ resolution: {integrity: sha512-oUzyO1/Zm6rsxKRHA1vH0NEDG58HrT5icx/azi9MF1TWdtttWl0UIUsjEQBBh+SIkrpd21ZjEv7ptxWys1ncsg==}
+
+ '@types/d3-random@3.0.3':
+ resolution: {integrity: sha512-Imagg1vJ3y76Y2ea0871wpabqp613+8/r0mCLEBfdtqC7xMSfj9idOnmBYyMoULfHePJyxMAw3nWhJxzc+LFwQ==}
+
+ '@types/d3-scale-chromatic@3.1.0':
+ resolution: {integrity: sha512-iWMJgwkK7yTRmWqRB5plb1kadXyQ5Sj8V/zYlFGMUBbIPKQScw+Dku9cAAMgJG+z5GYDoMjWGLVOvjghDEFnKQ==}
- /@types/debug@4.1.12:
+ '@types/d3-scale@4.0.9':
+ resolution: {integrity: sha512-dLmtwB8zkAeO/juAMfnV+sItKjlsw2lKdZVVy6LRr0cBmegxSABiLEpGVmSJJ8O08i4+sGR6qQtb6WtuwJdvVw==}
+
+ '@types/d3-selection@3.0.11':
+ resolution: {integrity: sha512-bhAXu23DJWsrI45xafYpkQ4NtcKMwWnAC/vKrd2l+nxMFuvOT3XMYTIj2opv8vq8AO5Yh7Qac/nSeP/3zjTK0w==}
+
+ '@types/d3-shape@3.1.8':
+ resolution: {integrity: sha512-lae0iWfcDeR7qt7rA88BNiqdvPS5pFVPpo5OfjElwNaT2yyekbM0C9vK+yqBqEmHr6lDkRnYNoTBYlAgJa7a4w==}
+
+ '@types/d3-time-format@4.0.3':
+ resolution: {integrity: sha512-5xg9rC+wWL8kdDj153qZcsJ0FWiFt0J5RB6LYUNZjwSnesfblqrI/bJ1wBdJ8OQfncgbJG5+2F+qfqnqyzYxyg==}
+
+ '@types/d3-time@3.0.4':
+ resolution: {integrity: sha512-yuzZug1nkAAaBlBBikKZTgzCeA+k1uy4ZFwWANOfKw5z5LRhV0gNA7gNkKm7HoK+HRN0wX3EkxGk0fpbWhmB7g==}
+
+ '@types/d3-timer@3.0.2':
+ resolution: {integrity: sha512-Ps3T8E8dZDam6fUyNiMkekK3XUsaUEik+idO9/YjPtfj2qruF8tFBXS7XhtE4iIXBLxhmLjP3SXpLhVf21I9Lw==}
+
+ '@types/d3-transition@3.0.9':
+ resolution: {integrity: sha512-uZS5shfxzO3rGlu0cC3bjmMFKsXv+SmZZcgp0KD22ts4uGXp5EVYGzu/0YdwZeKmddhcAccYtREJKkPfXkZuCg==}
+
+ '@types/d3-zoom@3.0.8':
+ resolution: {integrity: sha512-iqMC4/YlFCSlO8+2Ii1GGGliCAY4XdeG748w5vQUbevlbDu0zSjH/+jojorQVBK/se0j6DUFNPBGSqD3YWYnDw==}
+
+ '@types/d3@7.4.3':
+ resolution: {integrity: sha512-lZXZ9ckh5R8uiFVt8ogUNf+pIrK4EsWrx2Np75WvF/eTpJ0FMHNhjXk8CKEx/+gpHbNQyJWehbFaTvqmHWB3ww==}
+
+ '@types/debug@4.1.12':
resolution: {integrity: sha512-vIChWdVG3LG1SMxEvI/AK+FWJthlrqlTu7fbrlywTkkaONwk/UAGaULXRlf8vkzFBLVm0zkMdCquhL5aOjhXPQ==}
- dependencies:
- '@types/ms': 0.7.34
- dev: false
- /@types/estree@1.0.5:
- resolution: {integrity: sha512-/kYRxGDLWzHOB7q+wtSUQlFrtcdUccpfy+X+9iMBpHK8QLLhx2wIPYuS5DYtR9Wa/YlZAbIovy7qVdB1Aq6Lyw==}
- dev: false
+ '@types/estree@1.0.8':
+ resolution: {integrity: sha512-dWHzHa2WqEXI/O1E9OjrocMTKJl2mSrEolh1Iomrv6U+JuNwaHXsXx9bLu5gG7BUWFIN0skIQJQ/L1rIex4X6w==}
- /@types/fs-extra@11.0.4:
+ '@types/fs-extra@11.0.4':
resolution: {integrity: sha512-yTbItCNreRooED33qjunPthRcSjERP1r4MqCZc7wv0u2sUkzTFp45tgUfS5+r7FrZPdmCCNflLhVSP/o+SemsQ==}
- dependencies:
- '@types/jsonfile': 6.1.4
- '@types/node': 20.12.2
- dev: false
- /@types/hash-sum@1.0.2:
+ '@types/geojson@7946.0.16':
+ resolution: {integrity: sha512-6C8nqWur3j98U6+lXDfTUWIfgvZU+EumvpHKcYjujKH7woYyLj2sUmff0tRhrqM7BohUw7Pz3ZB1jj2gW9Fvmg==}
+
+ '@types/hash-sum@1.0.2':
resolution: {integrity: sha512-UP28RddqY8xcU0SCEp9YKutQICXpaAq9N8U2klqF5hegGha7KzTOL8EdhIIV3bOSGBzjEpN9bU/d+nNZBdJYVw==}
- dev: false
- /@types/jsonfile@6.1.4:
+ '@types/hast@3.0.4':
+ resolution: {integrity: sha512-WPs+bbQw5aCj+x6laNGWLH3wviHtoCv/P3+otBhbOhJgG8qtpdAMlTCxLtsTWA7LH1Oh/bFCHsBn0TPS5m30EQ==}
+
+ '@types/jsonfile@6.1.4':
resolution: {integrity: sha512-D5qGUYwjvnNNextdU59/+fI+spnwtTFmyQP0h+PfIOSkNfpU6AOICUOkm4i0OnSk+NyjdPJrxCDro0sJsWlRpQ==}
- dependencies:
- '@types/node': 20.12.2
- dev: false
- /@types/katex@0.16.7:
- resolution: {integrity: sha512-HMwFiRujE5PjrgwHQ25+bsLJgowjGjm5Z8FVSf0N6PwgJrwxH0QxzHYDcKsTfV3wva0vzrpqMTJS2jXPr5BMEQ==}
- dev: false
+ '@types/katex@0.16.8':
+ resolution: {integrity: sha512-trgaNyfU+Xh2Tc+ABIb44a5AYUpicB3uwirOioeOkNPPbmgRNtcWyDeeFRzjPZENO9Vq8gvVqfhaaXWLlevVwg==}
- /@types/linkify-it@3.0.5:
- resolution: {integrity: sha512-yg6E+u0/+Zjva+buc3EIb+29XEg4wltq7cSmd4Uc2EE/1nUVmxyzpX6gUXD0V8jIrG0r7YeOGVIbYRkxeooCtw==}
- dev: false
+ '@types/linkify-it@5.0.0':
+ resolution: {integrity: sha512-sVDA58zAw4eWAffKOaQH5/5j3XeayukzDk+ewSsnv3p4yJEZHCCzMDiZM8e0OUrRvmpGZ85jf4yDHkHsgBNr9Q==}
- /@types/markdown-it-emoji@2.0.4:
- resolution: {integrity: sha512-H6ulk/ZmbDxOayPwI/leJzrmoW1YKX1Z+MVSCHXuYhvqckV4I/c+hPTf6UiqJyn2avWugfj30XroheEb6/Ekqg==}
- dependencies:
- '@types/markdown-it': 13.0.7
- dev: false
+ '@types/markdown-it-emoji@3.0.1':
+ resolution: {integrity: sha512-cz1j8R35XivBqq9mwnsrP2fsz2yicLhB8+PDtuVkKOExwEdsVBNI+ROL3sbhtR5occRZ66vT0QnwFZCqdjf3pA==}
- /@types/markdown-it@13.0.7:
- resolution: {integrity: sha512-U/CBi2YUUcTHBt5tjO2r5QV/x0Po6nsYwQU4Y04fBS6vfoImaiZ6f8bi3CjTCxBPQSO1LMyUqkByzi8AidyxfA==}
- dependencies:
- '@types/linkify-it': 3.0.5
- '@types/mdurl': 1.0.5
- dev: false
+ '@types/markdown-it@14.1.2':
+ resolution: {integrity: sha512-promo4eFwuiW+TfGxhi+0x3czqTYJkG8qB17ZUJiVF10Xm7NLVRSLUsfRTU/6h1e24VvRnXCx+hG7li58lkzog==}
- /@types/mdurl@1.0.5:
- resolution: {integrity: sha512-6L6VymKTzYSrEf4Nev4Xa1LCHKrlTlYCBMTlQKFuddo1CvQcE52I0mwfOJayueUC7MJuXOeHTcIU683lzd0cUA==}
- dev: false
+ '@types/mdast@4.0.4':
+ resolution: {integrity: sha512-kGaNbPh1k7AFzgpud/gMdvIm5xuECykRR+JnWKQno9TAXVa6WIVCGTPvYGekIDL4uwCZQSYbUxNBSb1aUo79oA==}
- /@types/ms@0.7.34:
- resolution: {integrity: sha512-nG96G3Wp6acyAgJqGasjODb+acrI7KltPiRxzHPXnP3NgI28bpQDRv53olbqGXbfcgF5aiiHmO3xpwEpS5Ld9g==}
- dev: false
+ '@types/mdurl@2.0.0':
+ resolution: {integrity: sha512-RGdgjQUZba5p6QEFAVx2OGb8rQDL/cPRG7GiedRzMcJ1tYnUANBncjbSB1NRGwbvjcPeikRABz2nshyPk1bhWg==}
- /@types/node@17.0.45:
- resolution: {integrity: sha512-w+tIMs3rq2afQdsPJlODhoUEKzFP1ayaoyl1CcnwtIlsVe7K7bA1NGm4s3PraqTLlXnbIN84zuBlxBWo1u9BLw==}
- dev: false
+ '@types/ms@2.1.0':
+ resolution: {integrity: sha512-GsCCIZDE/p3i96vtEqx+7dBUGXrc7zeSK3wwPHIaRThS+9OhWIXRqzs4d6k1SVU8g91DrNRWxWUGhp5KXQb2VA==}
- /@types/node@20.12.2:
- resolution: {integrity: sha512-zQ0NYO87hyN6Xrclcqp7f8ZbXNbRfoGWNcMvHTPQp9UUrwI0mI7XBz+cu7/W6/VClYo2g63B0cjull/srU7LgQ==}
- dependencies:
- undici-types: 5.26.5
- dev: false
+ '@types/node@24.10.9':
+ resolution: {integrity: sha512-ne4A0IpG3+2ETuREInjPNhUGis1SFjv1d5asp8MzEAGtOZeTeHVDOYqOgqfhvseqg/iXty2hjBf1zAOb7RNiNw==}
+
+ '@types/node@25.0.9':
+ resolution: {integrity: sha512-/rpCXHlCWeqClNBwUhDcusJxXYDjZTyE8v5oTO7WbL8eij2nKhUeU89/6xgjU7N4/Vh3He0BtyhJdQbDyhiXAw==}
+
+ '@types/picomatch@4.0.2':
+ resolution: {integrity: sha512-qHHxQ+P9PysNEGbALT8f8YOSHW0KJu6l2xU8DYY0fu/EmGxXdVnuTLvFUvBgPJMSqXq29SYHveejeAha+4AYgA==}
- /@types/sax@1.2.7:
+ '@types/sax@1.2.7':
resolution: {integrity: sha512-rO73L89PJxeYM3s3pPPjiPgVVcymqU490g0YO5n5By0k2Erzj6tay/4lr1CHAAU4JyOWd1rpQ8bCf6cZfHU96A==}
- dependencies:
- '@types/node': 17.0.45
- dev: false
- /@types/trusted-types@2.0.7:
+ '@types/trusted-types@2.0.7':
resolution: {integrity: sha512-ScaPdn1dQczgbl0QFTeTOmVHFULt394XJgOQNoyVhZ6r2vLnMLJfBPd53SB52T/3G36VI1/g2MZaX0cwDuXsfw==}
- dev: false
- /@types/web-bluetooth@0.0.20:
- resolution: {integrity: sha512-g9gZnnXVq7gM7v3tJCWV/qw7w+KeOlSHAhgF9RytFyifW6AF61hdT2ucrYhPq9hLs5JIryeupHV3qGk95dH9ow==}
- dev: false
+ '@types/unist@2.0.11':
+ resolution: {integrity: sha512-CmBKiL6NNo/OqgmMn95Fk9Whlp2mtvIv+KNpQKN2F4SjvrEesubTRWGYSg+BnWZOnlCaSTU1sMpsBOzgbYhnsA==}
+
+ '@types/unist@3.0.3':
+ resolution: {integrity: sha512-ko/gIFJRv177XgZsZcBwnqJN5x/Gien8qNOn0D5bQU/zAzVf9Zt3BlcUiLqhV9y4ARk0GbT3tnUiPNgnTXzc/Q==}
+
+ '@types/web-bluetooth@0.0.21':
+ resolution: {integrity: sha512-oIQLCGWtcFZy2JW77j9k8nHzAOpqMHLQejDA48XXMWH6tjCQHz5RCFz1bzsmROyL6PUm+LLnUiI4BCn221inxA==}
+
+ '@ungap/structured-clone@1.3.0':
+ resolution: {integrity: sha512-WmoN8qaIAo7WTYWbAZuG8PYEhn5fkz7dZrqTBZ7dtt//lL2Gwms1IcnQ5yHqjDfX8Ft5j4YzDM23f87zBfDe9g==}
- /@vitejs/plugin-vue@5.0.4(vite@5.2.7)(vue@3.4.21):
- resolution: {integrity: sha512-WS3hevEszI6CEVEx28F8RjTX97k3KsrcY6kvTg7+Whm5y3oYvcqzVeGCU3hxSAn4uY2CLCkeokkGKpoctccilQ==}
- engines: {node: ^18.0.0 || >=20.0.0}
+ '@vitejs/plugin-vue@6.0.3':
+ resolution: {integrity: sha512-TlGPkLFLVOY3T7fZrwdvKpjprR3s4fxRln0ORDo1VQ7HHyxJwTlrjKU3kpVWTlaAjIEuCTokmjkZnr8Tpc925w==}
+ engines: {node: ^20.19.0 || >=22.12.0}
peerDependencies:
- vite: ^5.0.0
+ vite: '>=7.0.8'
vue: ^3.2.25
- dependencies:
- vite: 5.2.7
- vue: 3.4.21
- dev: false
- /@vue/compiler-core@3.4.21:
- resolution: {integrity: sha512-MjXawxZf2SbZszLPYxaFCjxfibYrzr3eYbKxwpLR9EQN+oaziSu3qKVbwBERj1IFIB8OLUewxB5m/BFzi613og==}
- dependencies:
- '@babel/parser': 7.24.1
- '@vue/shared': 3.4.21
- entities: 4.5.0
- estree-walker: 2.0.2
- source-map-js: 1.2.0
- dev: false
+ '@vue/compiler-core@3.5.26':
+ resolution: {integrity: sha512-vXyI5GMfuoBCnv5ucIT7jhHKl55Y477yxP6fc4eUswjP8FG3FFVFd41eNDArR+Uk3QKn2Z85NavjaxLxOC19/w==}
- /@vue/compiler-dom@3.4.21:
- resolution: {integrity: sha512-IZC6FKowtT1sl0CR5DpXSiEB5ayw75oT2bma1BEhV7RRR1+cfwLrxc2Z8Zq/RGFzJ8w5r9QtCOvTjQgdn0IKmA==}
- dependencies:
- '@vue/compiler-core': 3.4.21
- '@vue/shared': 3.4.21
- dev: false
+ '@vue/compiler-dom@3.5.26':
+ resolution: {integrity: sha512-y1Tcd3eXs834QjswshSilCBnKGeQjQXB6PqFn/1nxcQw4pmG42G8lwz+FZPAZAby6gZeHSt/8LMPfZ4Rb+Bd/A==}
- /@vue/compiler-sfc@3.4.21:
- resolution: {integrity: sha512-me7epoTxYlY+2CUM7hy9PCDdpMPfIwrOvAXud2Upk10g4YLv9UBW7kL798TvMeDhPthkZ0CONNrK2GoeI1ODiQ==}
- dependencies:
- '@babel/parser': 7.24.1
- '@vue/compiler-core': 3.4.21
- '@vue/compiler-dom': 3.4.21
- '@vue/compiler-ssr': 3.4.21
- '@vue/shared': 3.4.21
- estree-walker: 2.0.2
- magic-string: 0.30.8
- postcss: 8.4.38
- source-map-js: 1.2.0
- dev: false
+ '@vue/compiler-sfc@3.5.26':
+ resolution: {integrity: sha512-egp69qDTSEZcf4bGOSsprUr4xI73wfrY5oRs6GSgXFTiHrWj4Y3X5Ydtip9QMqiCMCPVwLglB9GBxXtTadJ3mA==}
- /@vue/compiler-ssr@3.4.21:
- resolution: {integrity: sha512-M5+9nI2lPpAsgXOGQobnIueVqc9sisBFexh5yMIMRAPYLa7+5wEJs8iqOZc1WAa9WQbx9GR2twgznU8LTIiZ4Q==}
- dependencies:
- '@vue/compiler-dom': 3.4.21
- '@vue/shared': 3.4.21
- dev: false
+ '@vue/compiler-ssr@3.5.26':
+ resolution: {integrity: sha512-lZT9/Y0nSIRUPVvapFJEVDbEXruZh2IYHMk2zTtEgJSlP5gVOqeWXH54xDKAaFS4rTnDeDBQUYDtxKyoW9FwDw==}
- /@vue/devtools-api@6.6.1:
- resolution: {integrity: sha512-LgPscpE3Vs0x96PzSSB4IGVSZXZBZHpfxs+ZA1d+VEPwHdOXowy/Y2CsvCAIFrf+ssVU1pD1jidj505EpUnfbA==}
- dev: false
+ '@vue/devtools-api@6.6.4':
+ resolution: {integrity: sha512-sGhTPMuXqZ1rVOk32RylztWkfXTRhuS7vgAKv0zjqk8gbsHkJ7xfFf+jbySxt7tWObEJwyKaHMikV/WGDiQm8g==}
- /@vue/reactivity@3.4.21:
- resolution: {integrity: sha512-UhenImdc0L0/4ahGCyEzc/pZNwVgcglGy9HVzJ1Bq2Mm9qXOpP8RyNTjookw/gOCUlXSEtuZ2fUg5nrHcoqJcw==}
- dependencies:
- '@vue/shared': 3.4.21
- dev: false
+ '@vue/devtools-api@8.0.5':
+ resolution: {integrity: sha512-DgVcW8H/Nral7LgZEecYFFYXnAvGuN9C3L3DtWekAncFBedBczpNW8iHKExfaM559Zm8wQWrwtYZ9lXthEHtDw==}
- /@vue/runtime-core@3.4.21:
- resolution: {integrity: sha512-pQthsuYzE1XcGZznTKn73G0s14eCJcjaLvp3/DKeYWoFacD9glJoqlNBxt3W2c5S40t6CCcpPf+jG01N3ULyrA==}
- dependencies:
- '@vue/reactivity': 3.4.21
- '@vue/shared': 3.4.21
- dev: false
+ '@vue/devtools-kit@8.0.5':
+ resolution: {integrity: sha512-q2VV6x1U3KJMTQPUlRMyWEKVbcHuxhqJdSr6Jtjz5uAThAIrfJ6WVZdGZm5cuO63ZnSUz0RCsVwiUUb0mDV0Yg==}
- /@vue/runtime-dom@3.4.21:
- resolution: {integrity: sha512-gvf+C9cFpevsQxbkRBS1NpU8CqxKw0ebqMvLwcGQrNpx6gqRDodqKqA+A2VZZpQ9RpK2f9yfg8VbW/EpdFUOJw==}
- dependencies:
- '@vue/runtime-core': 3.4.21
- '@vue/shared': 3.4.21
- csstype: 3.1.3
- dev: false
+ '@vue/devtools-shared@8.0.5':
+ resolution: {integrity: sha512-bRLn6/spxpmgLk+iwOrR29KrYnJjG9DGpHGkDFG82UM21ZpJ39ztUT9OXX3g+usW7/b2z+h46I9ZiYyB07XMXg==}
+
+ '@vue/reactivity@3.5.26':
+ resolution: {integrity: sha512-9EnYB1/DIiUYYnzlnUBgwU32NNvLp/nhxLXeWRhHUEeWNTn1ECxX8aGO7RTXeX6PPcxe3LLuNBFoJbV4QZ+CFQ==}
- /@vue/server-renderer@3.4.21(vue@3.4.21):
- resolution: {integrity: sha512-aV1gXyKSN6Rz+6kZ6kr5+Ll14YzmIbeuWe7ryJl5muJ4uwSwY/aStXTixx76TwkZFJLm1aAlA/HSWEJ4EyiMkg==}
+ '@vue/runtime-core@3.5.26':
+ resolution: {integrity: sha512-xJWM9KH1kd201w5DvMDOwDHYhrdPTrAatn56oB/LRG4plEQeZRQLw0Bpwih9KYoqmzaxF0OKSn6swzYi84e1/Q==}
+
+ '@vue/runtime-dom@3.5.26':
+ resolution: {integrity: sha512-XLLd/+4sPC2ZkN/6+V4O4gjJu6kSDbHAChvsyWgm1oGbdSO3efvGYnm25yCjtFm/K7rrSDvSfPDgN1pHgS4VNQ==}
+
+ '@vue/server-renderer@3.5.26':
+ resolution: {integrity: sha512-TYKLXmrwWKSodyVuO1WAubucd+1XlLg4set0YoV+Hu8Lo79mp/YMwWV5mC5FgtsDxX3qo1ONrxFaTP1OQgy1uA==}
peerDependencies:
- vue: 3.4.21
- dependencies:
- '@vue/compiler-ssr': 3.4.21
- '@vue/shared': 3.4.21
- vue: 3.4.21
- dev: false
+ vue: 3.5.26
- /@vue/shared@3.4.21:
- resolution: {integrity: sha512-PuJe7vDIi6VYSinuEbUIQgMIRZGgM8e4R+G+/dQTk0X1NEdvgvvgv7m+rfmDH1gZzyA1OjjoWskvHlfRNfQf3g==}
- dev: false
+ '@vue/shared@3.5.26':
+ resolution: {integrity: sha512-7Z6/y3uFI5PRoKeorTOSXKcDj0MSasfNNltcslbFrPpcw6aXRUALq4IfJlaTRspiWIUOEZbrpM+iQGmCOiWe4A==}
- /@vuepress/bundler-vite@2.0.0-rc.9:
- resolution: {integrity: sha512-GcM2eSqW2mPY5xXX4i5kuZujvwUeiTpsLX5kgau9LzPox+FdA3SMUkppCY3hsou2o2RxXPTfjocE7OlYQrUqvA==}
- dependencies:
- '@vitejs/plugin-vue': 5.0.4(vite@5.2.7)(vue@3.4.21)
- '@vuepress/client': 2.0.0-rc.9
- '@vuepress/core': 2.0.0-rc.9
- '@vuepress/shared': 2.0.0-rc.9
- '@vuepress/utils': 2.0.0-rc.9
- autoprefixer: 10.4.19(postcss@8.4.38)
- connect-history-api-fallback: 2.0.0
- postcss: 8.4.38
- postcss-load-config: 5.0.3(postcss@8.4.38)
- rollup: 4.13.2
- vite: 5.2.7
- vue: 3.4.21
- vue-router: 4.3.0(vue@3.4.21)
- transitivePeerDependencies:
- - '@types/node'
- - jiti
- - less
- - lightningcss
- - sass
- - stylus
- - sugarss
- - supports-color
- - terser
- - typescript
- dev: false
+ '@vuepress/bundler-vite@2.0.0-rc.26':
+ resolution: {integrity: sha512-4+YfKs2iOxuVSMW+L2tFzu2+X2HiGAREpo1DbkkYVDa5GyyPR+YsSueXNZMroTdzWDk5kAUz2Z1Tz1lIu7TO2g==}
- /@vuepress/cli@2.0.0-rc.9:
- resolution: {integrity: sha512-uv7Xmv3QmPpzCaUAq0oKEwp2tY64AO+7mxamgr7tr+t6FEnCYqr+X0nLlH17UtMkmGWIsbHLIlMjteprxGxIMg==}
+ '@vuepress/bundlerutils@2.0.0-rc.26':
+ resolution: {integrity: sha512-OnhUvzuJFEzPBjivZX7j6EhPE6sAwAIfyi3pAFmOpQDHPP7/l0q2I4bNVVGK4t9EZDu4N7Dl40/oFHhIMy5New==}
+
+ '@vuepress/cli@2.0.0-rc.26':
+ resolution: {integrity: sha512-63/4nIHrl9pbutUWs6SirWxmyykjvR9BWvu7bvczO1hAkWOyDQPcU18JXWy8q38CyMzPxCeedUfP3BQsZs3UgA==}
hasBin: true
- dependencies:
- '@vuepress/core': 2.0.0-rc.9
- '@vuepress/shared': 2.0.0-rc.9
- '@vuepress/utils': 2.0.0-rc.9
- cac: 6.7.14
- chokidar: 3.6.0
- envinfo: 7.11.1
- esbuild: 0.20.2
- transitivePeerDependencies:
- - supports-color
- - typescript
- dev: false
- /@vuepress/client@2.0.0-rc.9:
- resolution: {integrity: sha512-V5jA6L1nHQ8tXBshRHBJKei7HPFonGxFzmVK5yjj2Ho/Xtp/SD9rBS6dyYd5CSkKRGQDgy19Z+BUUPXtdI1qzg==}
- dependencies:
- '@vue/devtools-api': 6.6.1
- '@vuepress/shared': 2.0.0-rc.9
- vue: 3.4.21
- vue-router: 4.3.0(vue@3.4.21)
- transitivePeerDependencies:
- - typescript
- dev: false
+ '@vuepress/client@2.0.0-rc.26':
+ resolution: {integrity: sha512-+irF1HOTD6sAHdcTjp3yRcfuGlJYAW+YvDhq+7n3TPXeMH/wJbmGmAs2oRIDkx6Nlt3XkMMpFo7e9pOU22ut1w==}
- /@vuepress/core@2.0.0-rc.9:
- resolution: {integrity: sha512-uvMkIqYJ7vjfYEC91rMmT8YJt8xXnob5YYY3TzlwWUSEv4yoV3nlVu0l6Zfhenx/7FwKaxRJ/ePlUGIgUHBcBw==}
- dependencies:
- '@vuepress/client': 2.0.0-rc.9
- '@vuepress/markdown': 2.0.0-rc.9
- '@vuepress/shared': 2.0.0-rc.9
- '@vuepress/utils': 2.0.0-rc.9
- vue: 3.4.21
- transitivePeerDependencies:
- - supports-color
+ '@vuepress/core@2.0.0-rc.26':
+ resolution: {integrity: sha512-Wyiv9oRvdT0lAPGU0Pj1HetjKicbX8/gqbBVYv2MmL7Y4a3r0tyQ92IdZ8LHiAgPvzctntQr/JXIELedvU1t/w==}
+
+ '@vuepress/helper@2.0.0-rc.120':
+ resolution: {integrity: sha512-5hLgK8+ZNAi+QK7T7vxr8TwVhMOEQ2gSDkiNiyU9e7OK0U58z8ANLm/lRGbCEoh/TK40jFE/ZMke4WQ4Hj2Oaw==}
+ peerDependencies:
+ vuepress: 2.0.0-rc.26
+
+ '@vuepress/helper@2.0.0-rc.121':
+ resolution: {integrity: sha512-Jd67pS9n1BIy17hct+MRwhUoQz5Gu+mMllFoDRVg/0HIETJUjodOzJwR+NPWfGdHHHV8MELUMvuzEA80tOOv5w==}
+ peerDependencies:
+ vuepress: 2.0.0-rc.26
+
+ '@vuepress/highlighter-helper@2.0.0-rc.118':
+ resolution: {integrity: sha512-9LH7QrMPKzFB+XIWEwd8CY6CaPOTG6FE7RJ4Uj7iSNsjvUFCoMrxspvVpURoh/e12tRuSu3HGx3j02W8Vip/9g==}
+ peerDependencies:
+ '@vueuse/core': ^14.0.0
+ vuepress: 2.0.0-rc.26
+ peerDependenciesMeta:
+ '@vueuse/core':
+ optional: true
+
+ '@vuepress/markdown@2.0.0-rc.26':
+ resolution: {integrity: sha512-ZAXkRxqPDjxqcG4j4vN2ZL5gmuRmgGH7n0s/7pcWIGFH3BJodp/PXMYCklnne1VwARIim9rqE3FKPB/ifJX0yA==}
+
+ '@vuepress/plugin-active-header-links@2.0.0-rc.118':
+ resolution: {integrity: sha512-MtIUyzJnYR3iZFKqzax3/t+EuOQubIn3BbVYb5DZB8N0Hys+/LihzwSBF5AnVmecsLHOQ/b0V8blk/EOc5u/Kg==}
+ peerDependencies:
+ vuepress: 2.0.0-rc.26
+
+ '@vuepress/plugin-back-to-top@2.0.0-rc.121':
+ resolution: {integrity: sha512-obOrsmf1oPjS83XCHd942GLxzlHgLXEGFtS6IjzdaUbl/VRNpaBYzEGYBEiYVTLadSwtr+XktBggaz14rLuS8g==}
+ peerDependencies:
+ vuepress: 2.0.0-rc.26
+
+ '@vuepress/plugin-blog@2.0.0-rc.121':
+ resolution: {integrity: sha512-9ks/LD5Om887LOPMSbq2GK+fKJIfUBJohNwdRfXviqxu7EVK+Tf7GMPU4RPfJVCf49yyrWtrlP8C6Vetn8fIXw==}
+ peerDependencies:
+ vuepress: 2.0.0-rc.26
+
+ '@vuepress/plugin-catalog@2.0.0-rc.121':
+ resolution: {integrity: sha512-hMxJiLOMfoJk021Ln9i6wxBs7g+sYY8GE6U09mWvz15SfqYvpCCEZxcTCbEIhTiVLWca6tq68ukIz2/mihNk9A==}
+ peerDependencies:
+ vuepress: 2.0.0-rc.26
+
+ '@vuepress/plugin-comment@2.0.0-rc.121':
+ resolution: {integrity: sha512-LUAfz1XfwwmAThaOCD5IHpVztul31JLOaAwHIL01DKgIV4jluJJGtMRL1eDXrAEY4jYifDNS123bNz4jVCi2Pw==}
+ peerDependencies:
+ '@waline/client': ^3.7.1
+ artalk: ^2.9.1
+ twikoo: ^1.6.41
+ vuepress: 2.0.0-rc.26
+ peerDependenciesMeta:
+ '@waline/client':
+ optional: true
+ artalk:
+ optional: true
+ twikoo:
+ optional: true
+
+ '@vuepress/plugin-copy-code@2.0.0-rc.121':
+ resolution: {integrity: sha512-nZdel63vRNkVe0KPHQpfD2YVBItOEUyyJN/B+Bn6+WJPPdbFjcrP8A9glj9JbYLHE/R/4+dPpep4xCKebnJCnQ==}
+ peerDependencies:
+ vuepress: 2.0.0-rc.26
+
+ '@vuepress/plugin-copyright@2.0.0-rc.121':
+ resolution: {integrity: sha512-Kccuta9i533TjPwjepcgkweEug+4YBB2ThH/BA5qCJPsqZMnff9nK7Q1fUDWJHDxI8PUIMrclegF2IDtwQQGrw==}
+ peerDependencies:
+ vuepress: 2.0.0-rc.26
+
+ '@vuepress/plugin-feed@2.0.0-rc.121':
+ resolution: {integrity: sha512-Uw3vE1RtQUmnQBQ/bHcq7tm2XZ+u86apvvR9Q9D7KB5YG1RjDUXF3oEjEPkY3JB9mWnGEXyVfjZiaIHZKYDakg==}
+ peerDependencies:
+ vuepress: 2.0.0-rc.26
+
+ '@vuepress/plugin-git@2.0.0-rc.121':
+ resolution: {integrity: sha512-Y1FB96CPZkJ4rux8Z//CJb0BAEXLK9laYRS9BsU7OrqAY9ZwAIhdUsRCcpmJ61gruRVbeEVIm9VlFzdWXD8bGg==}
+ peerDependencies:
+ vuepress: 2.0.0-rc.26
+
+ '@vuepress/plugin-icon@2.0.0-rc.121':
+ resolution: {integrity: sha512-/WrvkLcAdLU/ypquoxq9C9emsyLdINOkNzk6VaxM6vnP/x1yjGa6GYfavTE0D0vOxfJHEzGxoMIbpjNWf5zrYA==}
+ peerDependencies:
+ vuepress: 2.0.0-rc.26
+
+ '@vuepress/plugin-links-check@2.0.0-rc.121':
+ resolution: {integrity: sha512-htIXm0+4CXjZXbFmM54sUgnA/nzdcJIq2SBZ7l+ZxqKD5jmtLmJclWIYOZ/OyHubEt8HjPfEE0KrQbu1yR+EmA==}
+ peerDependencies:
+ vuepress: 2.0.0-rc.26
+
+ '@vuepress/plugin-markdown-chart@2.0.0-rc.121':
+ resolution: {integrity: sha512-+REFOme7jHgrYv5J+Db99H+wcQtTQ5HuqEUEzo5nYWLe+KkenMO16Z2ai3RRJu+OOvhJgQeS9x+G18NOjCIAEA==}
+ peerDependencies:
+ chart.js: ^4.4.7
+ echarts: ^6.0.0
+ flowchart.ts: ^3.0.1
+ markmap-lib: ^0.18.11
+ markmap-toolbar: ^0.18.10
+ markmap-view: ^0.18.10
+ mermaid: ^11.12.0
+ vuepress: 2.0.0-rc.26
+ peerDependenciesMeta:
+ chart.js:
+ optional: true
+ echarts:
+ optional: true
+ flowchart.ts:
+ optional: true
+ markmap-lib:
+ optional: true
+ markmap-toolbar:
+ optional: true
+ markmap-view:
+ optional: true
+ mermaid:
+ optional: true
+
+ '@vuepress/plugin-markdown-ext@2.0.0-rc.121':
+ resolution: {integrity: sha512-c7yRSAkEYuj1l0fqSJl/VeR7og6vS1hjSajfVVeTP+cJPBPo3/nZjLIeyy6DcgwTMFTyDDz5voF4ASBcKNxoqA==}
+ peerDependencies:
+ vuepress: 2.0.0-rc.26
+
+ '@vuepress/plugin-markdown-hint@2.0.0-rc.121':
+ resolution: {integrity: sha512-bM+fbP/X1/Wtmb3vpt0Ef0i7/NIVg3kzU7oJfJRFP0OOgTHGnfmAzwOB1r/JFrMuHIHspFgg3gyAM4IP8LP9bg==}
+ peerDependencies:
+ vuepress: 2.0.0-rc.26
+
+ '@vuepress/plugin-markdown-image@2.0.0-rc.121':
+ resolution: {integrity: sha512-vDqLKiSHLi7lyoqdZNyzqLkiVmhnzd/IXxuGmtbrEy/qZwzQAWvyxOU9DOxfVseH8WkHcNUFe+iIXWr/VVDo4w==}
+ peerDependencies:
+ vuepress: 2.0.0-rc.26
+
+ '@vuepress/plugin-markdown-include@2.0.0-rc.121':
+ resolution: {integrity: sha512-79UkHK1ccNWxlvOl3k57J0bLoAVSklC+Qj7P6jMKk3/2BWPHob2GryXh+vVF9MT2CV7RgNaCCoqZ+e/IOeoc0Q==}
+ peerDependencies:
+ vuepress: 2.0.0-rc.26
+
+ '@vuepress/plugin-markdown-math@2.0.0-rc.121':
+ resolution: {integrity: sha512-K5zUaX9IIS6O9Y6A2lmFeIpq8CprKtjCcR/Hk706pNwneUSkRvc7HLbcXicWFaSSem/ITKzIxJuoQ708SZ5kbA==}
+ peerDependencies:
+ '@mathjax/src': ^4.0.0
+ katex: ^0.16.21
+ vuepress: 2.0.0-rc.26
+ peerDependenciesMeta:
+ '@mathjax/src':
+ optional: true
+ katex:
+ optional: true
+
+ '@vuepress/plugin-markdown-preview@2.0.0-rc.121':
+ resolution: {integrity: sha512-SzZTBYJgs+x44JkTrkiDjTFHtzbdGi9GYsrFv8QMLkE9vMHOA3kKInb8A7YwcQid9pmWOdYW/q4XIrnAat6SxA==}
+ peerDependencies:
+ vuepress: 2.0.0-rc.26
+
+ '@vuepress/plugin-markdown-stylize@2.0.0-rc.121':
+ resolution: {integrity: sha512-x/cwGUBtPs+803F+/Q5HYq+Xnr245GvFaQxWyGNuJPCBPQSUojW5Uyfit2y9cv4RvK75Kw9Bh6V1NQ+af/pJwQ==}
+ peerDependencies:
+ vuepress: 2.0.0-rc.26
+
+ '@vuepress/plugin-markdown-tab@2.0.0-rc.121':
+ resolution: {integrity: sha512-igcBp21EWWC8f6NwNtM/nhnphhjE2H8dxmnyO5pUgxwG6F7DRlGNLvkJB43D0w1McqHPfC1mdOa7I+n8ouYnKQ==}
+ peerDependencies:
+ vuepress: 2.0.0-rc.26
+
+ '@vuepress/plugin-notice@2.0.0-rc.121':
+ resolution: {integrity: sha512-Me4AKuTt5caDAbQ1jUKOZ+3DuJDde/H1ZM2KhawfR4pZNaqbiHcJjqkugpyicWsPFN6IILfC+YDEYkTYXgAyBQ==}
+ peerDependencies:
+ vuepress: 2.0.0-rc.26
+
+ '@vuepress/plugin-nprogress@2.0.0-rc.121':
+ resolution: {integrity: sha512-lLYIvL7x13wsEoZX/5Y9dYdqwVK3eSwPr4tTq143CYe5+H/InDZvL71NccjyJqUU8lUIWGmH6PaXnaSPBGLtvA==}
+ peerDependencies:
+ vuepress: 2.0.0-rc.26
+
+ '@vuepress/plugin-photo-swipe@2.0.0-rc.121':
+ resolution: {integrity: sha512-fgQifAz9g6otV25QG/Nkva/q3+4ImUE9lo94Wv/2JGhv56AODTJ6i7p+H9PBYqjDDVqDo14XRckoPU5uPLoTfA==}
+ peerDependencies:
+ vuepress: 2.0.0-rc.26
+
+ '@vuepress/plugin-reading-time@2.0.0-rc.121':
+ resolution: {integrity: sha512-+1/dWQyGLvx/etS9/fwgyjq5rYK+ymrTi04MUe3/RQ8W8JL66oQwmuI39hqhbZdw0fYia3iN60FlLDOBY0PenQ==}
+ peerDependencies:
+ vuepress: 2.0.0-rc.26
+
+ '@vuepress/plugin-redirect@2.0.0-rc.121':
+ resolution: {integrity: sha512-47Cke3dLmdwOmiCQGDoQOk6G07PSVkl5+QE6Kzq7ZT4GPrH96DeOs3Q3f2+JoYSmpVldRBADnsQaojp0fRUcJg==}
+ hasBin: true
+ peerDependencies:
+ vuepress: 2.0.0-rc.26
+
+ '@vuepress/plugin-rtl@2.0.0-rc.121':
+ resolution: {integrity: sha512-EeNyX8GnTQR00ubowSlWLdSGbUaKvy8Ul7mYTUuRTAVWvqN7LkwRCquhlb3/9WtnTsRO2L0UZ+KMsVGYaoPOMQ==}
+ peerDependencies:
+ vuepress: 2.0.0-rc.26
+
+ '@vuepress/plugin-sass-palette@2.0.0-rc.121':
+ resolution: {integrity: sha512-1QtkkltbPCEgY0heQMJEkfZLdc8lkntfpBUAUojYrexR5VAW5sutGfcblZXlM7ttbB8U98T/BtTuS+iBHImcmA==}
+ peerDependencies:
+ sass: ^1.95.0
+ sass-embedded: ^1.95.0
+ sass-loader: ^16.0.6
+ vuepress: 2.0.0-rc.26
+ peerDependenciesMeta:
+ sass:
+ optional: true
+ sass-embedded:
+ optional: true
+ sass-loader:
+ optional: true
+
+ '@vuepress/plugin-search@2.0.0-rc.121':
+ resolution: {integrity: sha512-TqNPmLvyjohD8MMgoQ53mFGKWqHfI7XvwmK+GPnZ0KQhGLYrfMVLapTh8XnbnHfTIDW590Xi+e6Hejl5ziEDug==}
+ peerDependencies:
+ vuepress: 2.0.0-rc.26
+
+ '@vuepress/plugin-seo@2.0.0-rc.121':
+ resolution: {integrity: sha512-wN6YJnEvGIzG3xuNmTmvpOP4CPgeYleiixZb85bDi+l92tfFBBZcB3dVmiMQKc5XEcuMhgxMa8uUhwrYQ73dGA==}
+ peerDependencies:
+ vuepress: 2.0.0-rc.26
+
+ '@vuepress/plugin-shiki@2.0.0-rc.121':
+ resolution: {integrity: sha512-GdiB5MstjswjoFel9rJCRePexYFPPZGCjf6goHR4w1Cror1qQG3dsblRKR2XDEpO+bcFo4pAi6PNKQP1H+5GSw==}
+ peerDependencies:
+ '@vuepress/shiki-twoslash': 2.0.0-rc.121
+ vuepress: 2.0.0-rc.26
+ peerDependenciesMeta:
+ '@vuepress/shiki-twoslash':
+ optional: true
+
+ '@vuepress/plugin-sitemap@2.0.0-rc.121':
+ resolution: {integrity: sha512-Tm2tElhcZ8DV8ZglkLgzC5NlfT0KVdzyYpjFQp9wRbgWsl+L9YngAe0SJ9OhpnVC2v9jyu4CyNOmffNgc1s2zg==}
+ peerDependencies:
+ vuepress: 2.0.0-rc.26
+
+ '@vuepress/plugin-theme-data@2.0.0-rc.120':
+ resolution: {integrity: sha512-5gYzDQ7tfA/57VzlsT2w4/8XORzGuWO+B2noKuZvv98kFo7BpFXPMBn1H225gcCgyY+lOXRXAtE0iFO69BznOQ==}
+ peerDependencies:
+ vuepress: 2.0.0-rc.26
+
+ '@vuepress/shared@2.0.0-rc.26':
+ resolution: {integrity: sha512-Zl9XNG/fYenZqzuYYGOfHzjmp1HCOj68gcJnJABOX1db0H35dkPSPsxuMjbTljClUqMlfj70CLeip/h04upGVw==}
+
+ '@vuepress/utils@2.0.0-rc.26':
+ resolution: {integrity: sha512-RWzZrGQ0WLSWdELuxg7c6q1D9I22T5PfK/qNFkOsv9eD3gpUsU4jq4zAoumS8o+NRIWHovCJ9WnAhHD0Ns5zAw==}
+
+ '@vueuse/core@14.1.0':
+ resolution: {integrity: sha512-rgBinKs07hAYyPF834mDTigH7BtPqvZ3Pryuzt1SD/lg5wEcWqvwzXXYGEDb2/cP0Sj5zSvHl3WkmMELr5kfWw==}
+ peerDependencies:
+ vue: ^3.5.0
+
+ '@vueuse/metadata@14.1.0':
+ resolution: {integrity: sha512-7hK4g015rWn2PhKcZ99NyT+ZD9sbwm7SGvp7k+k+rKGWnLjS/oQozoIZzWfCewSUeBmnJkIb+CNr7Zc/EyRnnA==}
+
+ '@vueuse/shared@14.1.0':
+ resolution: {integrity: sha512-EcKxtYvn6gx1F8z9J5/rsg3+lTQnvOruQd8fUecW99DCK04BkWD7z5KQ/wTAx+DazyoEE9dJt/zV8OIEQbM6kw==}
+ peerDependencies:
+ vue: ^3.5.0
+
+ '@xmldom/xmldom@0.9.8':
+ resolution: {integrity: sha512-p96FSY54r+WJ50FIOsCOjyj/wavs8921hG5+kVMmZgKcvIKxMXHTrjNJvRgWa/zuX3B6t2lijLNFaOyuxUH+2A==}
+ engines: {node: '>=14.6'}
+
+ acorn@8.15.0:
+ resolution: {integrity: sha512-NZyJarBfL7nWwIq+FDL6Zp/yHEhePMNnnJ0y3qfieCrmNvYct8uvtiV41UvlSe6apAfk0fY1FbWx+NwfmpvtTg==}
+ engines: {node: '>=0.4.0'}
+ hasBin: true
+
+ ansi-regex@5.0.1:
+ resolution: {integrity: sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==}
+ engines: {node: '>=8'}
+
+ ansi-regex@6.2.2:
+ resolution: {integrity: sha512-Bq3SmSpyFHaWjPk8If9yc6svM8c56dB5BAtW4Qbw5jHTwwXXcTLoRMkpDJp6VL0XzlWaCHTXrkFURMYmD0sLqg==}
+ engines: {node: '>=12'}
+
+ ansi-styles@4.3.0:
+ resolution: {integrity: sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==}
+ engines: {node: '>=8'}
+
+ arg@5.0.2:
+ resolution: {integrity: sha512-PYjyFOLKQ9y57JvQ6QLo8dAgNqswh8M1RMJYdQduT6xbWSgK36P/Z/v+p888pM69jMMfS8Xd8F6I1kQ/I9HUGg==}
+
+ argparse@1.0.10:
+ resolution: {integrity: sha512-o5Roy6tNG4SL/FOkCAN6RzjiakZS25RLYFrcMttJqbdd8BWrnA+fGz57iN5Pb06pvBGvl5gQ0B48dJlslXvoTg==}
+
+ argparse@2.0.1:
+ resolution: {integrity: sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q==}
+
+ autoprefixer@10.4.23:
+ resolution: {integrity: sha512-YYTXSFulfwytnjAPlw8QHncHJmlvFKtczb8InXaAx9Q0LbfDnfEYDE55omerIJKihhmU61Ft+cAOSzQVaBUmeA==}
+ engines: {node: ^10 || ^12 || >=14}
+ hasBin: true
+ peerDependencies:
+ postcss: ^8.1.0
+
+ bail@2.0.2:
+ resolution: {integrity: sha512-0xO6mYd7JB2YesxDKplafRpsiOzPt9V02ddPCLbY1xYGPOX24NTyN50qnUxgCPcSoYMhKpAuBTjQoRZCAkUDRw==}
+
+ balloon-css@1.2.0:
+ resolution: {integrity: sha512-urXwkHgwp6GsXVF+it01485Z2Cj4pnW02ICnM0TemOlkKmCNnDLmyy+ZZiRXBpwldUXO+aRNr7Hdia4CBvXJ5A==}
+
+ baseline-browser-mapping@2.9.14:
+ resolution: {integrity: sha512-B0xUquLkiGLgHhpPBqvl7GWegWBUNuujQ6kXd/r1U38ElPT6Ok8KZ8e+FpUGEc2ZoRQUzq/aUnaKFc/svWUGSg==}
+ hasBin: true
+
+ bcrypt-ts@8.0.0:
+ resolution: {integrity: sha512-v4X8KKKQfBQY5XHxrErsImUtDDGt53N6nKHgK9M72EN3GgJfxUimKCOGV9FTOPxVZzUdcyJEnmnpWMs3MgZq3w==}
+ engines: {node: '>=20'}
+
+ birpc@2.9.0:
+ resolution: {integrity: sha512-KrayHS5pBi69Xi9JmvoqrIgYGDkD6mcSe/i6YKi3w5kekCLzrX4+nawcXqrj2tIp50Kw/mT/s3p+GVK0A0sKxw==}
+
+ boolbase@1.0.0:
+ resolution: {integrity: sha512-JZOSA7Mo9sNGB8+UjSgzdLtokWAky1zbztM3WRLCbZ70/3cTANmQmOdR7y2g+J0e2WXywy1yS468tY+IruqEww==}
+
+ braces@3.0.3:
+ resolution: {integrity: sha512-yQbXgO/OSZVD2IsiLlro+7Hf6Q18EJrKSEsdoMzKePKXct3gvD8oLcOQdIzGupr5Fj+EDe8gO/lxc1BzfMpxvA==}
+ engines: {node: '>=8'}
+
+ browserslist@4.28.1:
+ resolution: {integrity: sha512-ZC5Bd0LgJXgwGqUknZY/vkUQ04r8NXnJZ3yYi4vDmSiZmC/pdSN0NbNRPxZpbtO4uAfDUAFffO8IZoM3Gj8IkA==}
+ engines: {node: ^6 || ^7 || ^8 || ^9 || ^10 || ^11 || ^12 || >=13.7}
+ hasBin: true
+
+ buffer-builder@0.2.0:
+ resolution: {integrity: sha512-7VPMEPuYznPSoR21NE1zvd2Xna6c/CloiZCfcMXR1Jny6PjX0N4Nsa38zcBFo/FMK+BlA+FLKbJCQ0i2yxp+Xg==}
+
+ cac@6.7.14:
+ resolution: {integrity: sha512-b6Ilus+c3RrdDk+JhLKUAQfzzgLEPy6wcXqS7f/xe1EETvsDP6GORG7SFuOs6cID5YkqchW/LXZbX5bc8j7ZcQ==}
+ engines: {node: '>=8'}
+
+ camelcase@5.3.1:
+ resolution: {integrity: sha512-L28STB170nwWS63UjtlEOE3dldQApaJXZkOI1uMFfzf3rRuPegHaHesyee+YxQ+W6SvRDQV6UrdOdRiR153wJg==}
+ engines: {node: '>=6'}
+
+ caniuse-lite@1.0.30001764:
+ resolution: {integrity: sha512-9JGuzl2M+vPL+pz70gtMF9sHdMFbY9FJaQBi186cHKH3pSzDvzoUJUPV6fqiKIMyXbud9ZLg4F3Yza1vJ1+93g==}
+
+ ccount@2.0.1:
+ resolution: {integrity: sha512-eyrF0jiFpY+3drT6383f1qhkbGsLSifNAjA61IUjZjmLCWjItY6LB9ft9YhoDgwfmclB2zhu51Lc7+95b8NRAg==}
+
+ chalk@5.6.2:
+ resolution: {integrity: sha512-7NzBL0rN6fMUW+f7A6Io4h40qQlG+xGmtMxfbnH/K7TAtt8JQWVQK+6g0UXKMeVJoyV5EkkNsErQ8pVD3bLHbA==}
+ engines: {node: ^12.17.0 || ^14.13 || >=16.0.0}
+
+ character-entities-html4@2.1.0:
+ resolution: {integrity: sha512-1v7fgQRj6hnSwFpq1Eu0ynr/CDEw0rXo2B61qXrLNdHZmPKgb7fqS1a2JwF0rISo9q77jDI8VMEHoApn8qDoZA==}
+
+ character-entities-legacy@3.0.0:
+ resolution: {integrity: sha512-RpPp0asT/6ufRm//AJVwpViZbGM/MkjQFxJccQRHmISF/22NBtsHqAWmL+/pmkPWoIUJdWyeVleTl1wydHATVQ==}
+
+ character-entities@2.0.2:
+ resolution: {integrity: sha512-shx7oQ0Awen/BRIdkjkvz54PnEEI/EjwXDSIZp86/KKdbafHh1Df/RYGBhn4hbe2+uKC9FnT5UCEdyPz3ai9hQ==}
+
+ character-reference-invalid@2.0.1:
+ resolution: {integrity: sha512-iBZ4F4wRbyORVsu0jPV7gXkOsGYjGHPmAyv+HiHG8gi5PtC9KI2j1+v8/tlibRvjoWX027ypmG/n0HtO5t7unw==}
+
+ cheerio-select@2.1.0:
+ resolution: {integrity: sha512-9v9kG0LvzrlcungtnJtpGNxY+fzECQKhK4EGJX2vByejiMX84MFNQw4UxPJl3bFbTMw+Dfs37XaIkCwTZfLh4g==}
+
+ cheerio@1.1.2:
+ resolution: {integrity: sha512-IkxPpb5rS/d1IiLbHMgfPuS0FgiWTtFIm/Nj+2woXDLTZ7fOT2eqzgYbdMlLweqlHbsZjxEChoVK+7iph7jyQg==}
+ engines: {node: '>=20.18.1'}
+
+ chevrotain-allstar@0.3.1:
+ resolution: {integrity: sha512-b7g+y9A0v4mxCW1qUhf3BSVPg+/NvGErk/dOkrDaHA0nQIQGAtrOjlX//9OQtRlSCy+x9rfB5N8yC71lH1nvMw==}
+ peerDependencies:
+ chevrotain: ^11.0.0
+
+ chevrotain@11.0.3:
+ resolution: {integrity: sha512-ci2iJH6LeIkvP9eJW6gpueU8cnZhv85ELY8w8WiFtNjMHA5ad6pQLaJo9mEly/9qUyCpvqX8/POVUTf18/HFdw==}
+
+ chokidar@4.0.3:
+ resolution: {integrity: sha512-Qgzu8kfBvo+cA4962jnP1KkS6Dop5NS6g7R5LFYJr4b8Ub94PPQXUksCw9PvXoeXPRRddRNC5C1JQUR2SMGtnA==}
+ engines: {node: '>= 14.16.0'}
+
+ chokidar@5.0.0:
+ resolution: {integrity: sha512-TQMmc3w+5AxjpL8iIiwebF73dRDF4fBIieAqGn9RGCWaEVwQ6Fb2cGe31Yns0RRIzii5goJ1Y7xbMwo1TxMplw==}
+ engines: {node: '>= 20.19.0'}
+
+ cli-cursor@5.0.0:
+ resolution: {integrity: sha512-aCj4O5wKyszjMmDT4tZj93kxyydN/K5zPWSCe6/0AV/AA1pqe5ZBIw0a2ZfPQV7lL5/yb5HsUreJ6UFAF1tEQw==}
+ engines: {node: '>=18'}
+
+ cli-spinners@3.4.0:
+ resolution: {integrity: sha512-bXfOC4QcT1tKXGorxL3wbJm6XJPDqEnij2gQ2m7ESQuE+/z9YFIWnl/5RpTiKWbMq3EVKR4fRLJGn6DVfu0mpw==}
+ engines: {node: '>=18.20'}
+
+ cliui@6.0.0:
+ resolution: {integrity: sha512-t6wbgtoCXvAzst7QgXxJYqPt0usEfbgQdftEPbLL/cvv6HPE5VgvqCuAIDR0NgU52ds6rFwqrgakNLrHEjCbrQ==}
+
+ color-convert@2.0.1:
+ resolution: {integrity: sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==}
+ engines: {node: '>=7.0.0'}
+
+ color-name@1.1.4:
+ resolution: {integrity: sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==}
+
+ colorjs.io@0.5.2:
+ resolution: {integrity: sha512-twmVoizEW7ylZSN32OgKdXRmo1qg+wT5/6C3xu5b9QsWzSFAhHLn2xd8ro0diCsKfCj1RdaTP/nrcW+vAoQPIw==}
+
+ comma-separated-tokens@2.0.3:
+ resolution: {integrity: sha512-Fu4hJdvzeylCfQPp9SGWidpzrMs7tTrlu6Vb8XGaRGck8QSNZJJp538Wrb60Lax4fPwR64ViY468OIUTbRlGZg==}
+
+ commander@13.1.0:
+ resolution: {integrity: sha512-/rFeCpNJQbhSZjGVwO9RFV3xPqbnERS8MmIQzCtD/zl6gpJuV/bMLuN92oG3F7d8oDEHHRrujSXNUr8fpjntKw==}
+ engines: {node: '>=18'}
+
+ commander@14.0.2:
+ resolution: {integrity: sha512-TywoWNNRbhoD0BXs1P3ZEScW8W5iKrnbithIl0YH+uCmBd0QpPOA8yc82DS3BIE5Ma6FnBVUsJ7wVUDz4dvOWQ==}
+ engines: {node: '>=20'}
+
+ commander@7.2.0:
+ resolution: {integrity: sha512-QrWXB+ZQSVPmIWIhtEO9H+gwHaMGYiF5ChvoJ+K9ZGHG/sVsa6yiesAD1GC/x46sET00Xlwo1u49RVVVzvcSkw==}
+ engines: {node: '>= 10'}
+
+ commander@8.3.0:
+ resolution: {integrity: sha512-OkTL9umf+He2DZkUq8f8J9of7yL6RJKI24dVITBmNfZBmri9zYZQrKkuXiKhyfPSu8tUhnVBB1iKXevvnlR4Ww==}
+ engines: {node: '>= 12'}
+
+ confbox@0.1.8:
+ resolution: {integrity: sha512-RMtmw0iFkeR4YV+fUOSucriAQNb9g8zFR52MWCtl+cCZOFRNL6zeB395vPzFhEjjn4fMxXudmELnl/KF/WrK6w==}
+
+ connect-history-api-fallback@2.0.0:
+ resolution: {integrity: sha512-U73+6lQFmfiNPrYbXqr6kZ1i1wiRqXnp2nhMsINseWXO8lDau0LGEffJ8kQi4EjLZympVgRdvqjAgiZ1tgzDDA==}
+ engines: {node: '>=0.8'}
+
+ copy-anything@4.0.5:
+ resolution: {integrity: sha512-7Vv6asjS4gMOuILabD3l739tsaxFQmC+a7pLZm02zyvs8p977bL3zEgq3yDk5rn9B0PbYgIv++jmHcuUab4RhA==}
+ engines: {node: '>=18'}
+
+ cose-base@1.0.3:
+ resolution: {integrity: sha512-s9whTXInMSgAp/NVXVNuVxVKzGH2qck3aQlVHxDCdAEPgtMKwc4Wq6/QKhgdEdgbLSi9rBTAcPoRa6JpiG4ksg==}
+
+ cose-base@2.2.0:
+ resolution: {integrity: sha512-AzlgcsCbUMymkADOJtQm3wO9S3ltPfYOFD5033keQn9NJzIbtnZj+UdBJe7DYml/8TdbtHJW3j58SOnKhWY/5g==}
+
+ create-codepen@2.0.0:
+ resolution: {integrity: sha512-ehJ0Zw5RSV2G4+/azUb7vEZWRSA/K9cW7HDock1Y9ViDexkgSJUZJRcObdw/YAWeXKjreEQV9l/igNSsJ1yw5A==}
+ engines: {node: '>=18'}
+
+ css-select@5.2.2:
+ resolution: {integrity: sha512-TizTzUddG/xYLA3NXodFM0fSbNizXjOKhqiQQwvhlspadZokn1KDy0NZFS0wuEubIYAV5/c1/lAr0TaaFXEXzw==}
+
+ css-what@6.2.2:
+ resolution: {integrity: sha512-u/O3vwbptzhMs3L1fQE82ZSLHQQfto5gyZzwteVIEyeaY5Fc7R4dapF/BvRoSYFeqfBk4m0V1Vafq5Pjv25wvA==}
+ engines: {node: '>= 6'}
+
+ csstype@3.2.3:
+ resolution: {integrity: sha512-z1HGKcYy2xA8AGQfwrn0PAy+PB7X/GSj3UVJW9qKyn43xWa+gl5nXmU4qqLMRzWVLFC8KusUX8T/0kCiOYpAIQ==}
+
+ cytoscape-cose-bilkent@4.1.0:
+ resolution: {integrity: sha512-wgQlVIUJF13Quxiv5e1gstZ08rnZj2XaLHGoFMYXz7SkNfCDOOteKBE6SYRfA9WxxI/iBc3ajfDoc6hb/MRAHQ==}
+ peerDependencies:
+ cytoscape: ^3.2.0
+
+ cytoscape-fcose@2.2.0:
+ resolution: {integrity: sha512-ki1/VuRIHFCzxWNrsshHYPs6L7TvLu3DL+TyIGEsRcvVERmxokbf5Gdk7mFxZnTdiGtnA4cfSmjZJMviqSuZrQ==}
+ peerDependencies:
+ cytoscape: ^3.2.0
+
+ cytoscape@3.33.1:
+ resolution: {integrity: sha512-iJc4TwyANnOGR1OmWhsS9ayRS3s+XQ185FmuHObThD+5AeJCakAAbWv8KimMTt08xCCLNgneQwFp+JRJOr9qGQ==}
+ engines: {node: '>=0.10'}
+
+ d3-array@2.12.1:
+ resolution: {integrity: sha512-B0ErZK/66mHtEsR1TkPEEkwdy+WDesimkM5gpZr5Dsg54BiTA5RXtYW5qTLIAcekaS9xfZrzBLF/OAkB3Qn1YQ==}
+
+ d3-array@3.2.4:
+ resolution: {integrity: sha512-tdQAmyA18i4J7wprpYq8ClcxZy3SC31QMeByyCFyRt7BVHdREQZ5lpzoe5mFEYZUWe+oq8HBvk9JjpibyEV4Jg==}
+ engines: {node: '>=12'}
+
+ d3-axis@3.0.0:
+ resolution: {integrity: sha512-IH5tgjV4jE/GhHkRV0HiVYPDtvfjHQlQfJHs0usq7M30XcSBvOotpmH1IgkcXsO/5gEQZD43B//fc7SRT5S+xw==}
+ engines: {node: '>=12'}
+
+ d3-brush@3.0.0:
+ resolution: {integrity: sha512-ALnjWlVYkXsVIGlOsuWH1+3udkYFI48Ljihfnh8FZPF2QS9o+PzGLBslO0PjzVoHLZ2KCVgAM8NVkXPJB2aNnQ==}
+ engines: {node: '>=12'}
+
+ d3-chord@3.0.1:
+ resolution: {integrity: sha512-VE5S6TNa+j8msksl7HwjxMHDM2yNK3XCkusIlpX5kwauBfXuyLAtNg9jCp/iHH61tgI4sb6R/EIMWCqEIdjT/g==}
+ engines: {node: '>=12'}
+
+ d3-color@3.1.0:
+ resolution: {integrity: sha512-zg/chbXyeBtMQ1LbD/WSoW2DpC3I0mpmPdW+ynRTj/x2DAWYrIY7qeZIHidozwV24m4iavr15lNwIwLxRmOxhA==}
+ engines: {node: '>=12'}
+
+ d3-contour@4.0.2:
+ resolution: {integrity: sha512-4EzFTRIikzs47RGmdxbeUvLWtGedDUNkTcmzoeyg4sP/dvCexO47AaQL7VKy/gul85TOxw+IBgA8US2xwbToNA==}
+ engines: {node: '>=12'}
+
+ d3-delaunay@6.0.4:
+ resolution: {integrity: sha512-mdjtIZ1XLAM8bm/hx3WwjfHt6Sggek7qH043O8KEjDXN40xi3vx/6pYSVTwLjEgiXQTbvaouWKynLBiUZ6SK6A==}
+ engines: {node: '>=12'}
+
+ d3-dispatch@3.0.1:
+ resolution: {integrity: sha512-rzUyPU/S7rwUflMyLc1ETDeBj0NRuHKKAcvukozwhshr6g6c5d8zh4c2gQjY2bZ0dXeGLWc1PF174P2tVvKhfg==}
+ engines: {node: '>=12'}
+
+ d3-drag@3.0.0:
+ resolution: {integrity: sha512-pWbUJLdETVA8lQNJecMxoXfH6x+mO2UQo8rSmZ+QqxcbyA3hfeprFgIT//HW2nlHChWeIIMwS2Fq+gEARkhTkg==}
+ engines: {node: '>=12'}
+
+ d3-dsv@3.0.1:
+ resolution: {integrity: sha512-UG6OvdI5afDIFP9w4G0mNq50dSOsXHJaRE8arAS5o9ApWnIElp8GZw1Dun8vP8OyHOZ/QJUKUJwxiiCCnUwm+Q==}
+ engines: {node: '>=12'}
+ hasBin: true
+
+ d3-ease@3.0.1:
+ resolution: {integrity: sha512-wR/XK3D3XcLIZwpbvQwQ5fK+8Ykds1ip7A2Txe0yxncXSdq1L9skcG7blcedkOX+ZcgxGAmLX1FrRGbADwzi0w==}
+ engines: {node: '>=12'}
+
+ d3-fetch@3.0.1:
+ resolution: {integrity: sha512-kpkQIM20n3oLVBKGg6oHrUchHM3xODkTzjMoj7aWQFq5QEM+R6E4WkzT5+tojDY7yjez8KgCBRoj4aEr99Fdqw==}
+ engines: {node: '>=12'}
+
+ d3-force@3.0.0:
+ resolution: {integrity: sha512-zxV/SsA+U4yte8051P4ECydjD/S+qeYtnaIyAs9tgHCqfguma/aAQDjo85A9Z6EKhBirHRJHXIgJUlffT4wdLg==}
+ engines: {node: '>=12'}
+
+ d3-format@3.1.2:
+ resolution: {integrity: sha512-AJDdYOdnyRDV5b6ArilzCPPwc1ejkHcoyFarqlPqT7zRYjhavcT3uSrqcMvsgh2CgoPbK3RCwyHaVyxYcP2Arg==}
+ engines: {node: '>=12'}
+
+ d3-geo@3.1.1:
+ resolution: {integrity: sha512-637ln3gXKXOwhalDzinUgY83KzNWZRKbYubaG+fGVuc/dxO64RRljtCTnf5ecMyE1RIdtqpkVcq0IbtU2S8j2Q==}
+ engines: {node: '>=12'}
+
+ d3-hierarchy@3.1.2:
+ resolution: {integrity: sha512-FX/9frcub54beBdugHjDCdikxThEqjnR93Qt7PvQTOHxyiNCAlvMrHhclk3cD5VeAaq9fxmfRp+CnWw9rEMBuA==}
+ engines: {node: '>=12'}
+
+ d3-interpolate@3.0.1:
+ resolution: {integrity: sha512-3bYs1rOD33uo8aqJfKP3JWPAibgw8Zm2+L9vBKEHJ2Rg+viTR7o5Mmv5mZcieN+FRYaAOWX5SJATX6k1PWz72g==}
+ engines: {node: '>=12'}
+
+ d3-path@1.0.9:
+ resolution: {integrity: sha512-VLaYcn81dtHVTjEHd8B+pbe9yHWpXKZUC87PzoFmsFrJqgFwDe/qxfp5MlfsfM1V5E/iVt0MmEbWQ7FVIXh/bg==}
+
+ d3-path@3.1.0:
+ resolution: {integrity: sha512-p3KP5HCf/bvjBSSKuXid6Zqijx7wIfNW+J/maPs+iwR35at5JCbLUT0LzF1cnjbCHWhqzQTIN2Jpe8pRebIEFQ==}
+ engines: {node: '>=12'}
+
+ d3-polygon@3.0.1:
+ resolution: {integrity: sha512-3vbA7vXYwfe1SYhED++fPUQlWSYTTGmFmQiany/gdbiWgU/iEyQzyymwL9SkJjFFuCS4902BSzewVGsHHmHtXg==}
+ engines: {node: '>=12'}
+
+ d3-quadtree@3.0.1:
+ resolution: {integrity: sha512-04xDrxQTDTCFwP5H6hRhsRcb9xxv2RzkcsygFzmkSIOJy3PeRJP7sNk3VRIbKXcog561P9oU0/rVH6vDROAgUw==}
+ engines: {node: '>=12'}
+
+ d3-random@3.0.1:
+ resolution: {integrity: sha512-FXMe9GfxTxqd5D6jFsQ+DJ8BJS4E/fT5mqqdjovykEB2oFbTMDVdg1MGFxfQW+FBOGoB++k8swBrgwSHT1cUXQ==}
+ engines: {node: '>=12'}
+
+ d3-sankey@0.12.3:
+ resolution: {integrity: sha512-nQhsBRmM19Ax5xEIPLMY9ZmJ/cDvd1BG3UVvt5h3WRxKg5zGRbvnteTyWAbzeSvlh3tW7ZEmq4VwR5mB3tutmQ==}
+
+ d3-scale-chromatic@3.1.0:
+ resolution: {integrity: sha512-A3s5PWiZ9YCXFye1o246KoscMWqf8BsD9eRiJ3He7C9OBaxKhAd5TFCdEx/7VbKtxxTsu//1mMJFrEt572cEyQ==}
+ engines: {node: '>=12'}
+
+ d3-scale@4.0.2:
+ resolution: {integrity: sha512-GZW464g1SH7ag3Y7hXjf8RoUuAFIqklOAq3MRl4OaWabTFJY9PN/E1YklhXLh+OQ3fM9yS2nOkCoS+WLZ6kvxQ==}
+ engines: {node: '>=12'}
+
+ d3-selection@3.0.0:
+ resolution: {integrity: sha512-fmTRWbNMmsmWq6xJV8D19U/gw/bwrHfNXxrIN+HfZgnzqTHp9jOmKMhsTUjXOJnZOdZY9Q28y4yebKzqDKlxlQ==}
+ engines: {node: '>=12'}
+
+ d3-shape@1.3.7:
+ resolution: {integrity: sha512-EUkvKjqPFUAZyOlhY5gzCxCeI0Aep04LwIRpsZ/mLFelJiUfnK56jo5JMDSE7yyP2kLSb6LtF+S5chMk7uqPqw==}
+
+ d3-shape@3.2.0:
+ resolution: {integrity: sha512-SaLBuwGm3MOViRq2ABk3eLoxwZELpH6zhl3FbAoJ7Vm1gofKx6El1Ib5z23NUEhF9AsGl7y+dzLe5Cw2AArGTA==}
+ engines: {node: '>=12'}
+
+ d3-time-format@4.1.0:
+ resolution: {integrity: sha512-dJxPBlzC7NugB2PDLwo9Q8JiTR3M3e4/XANkreKSUxF8vvXKqm1Yfq4Q5dl8budlunRVlUUaDUgFt7eA8D6NLg==}
+ engines: {node: '>=12'}
+
+ d3-time@3.1.0:
+ resolution: {integrity: sha512-VqKjzBLejbSMT4IgbmVgDjpkYrNWUYJnbCGo874u7MMKIWsILRX+OpX/gTk8MqjpT1A/c6HY2dCA77ZN0lkQ2Q==}
+ engines: {node: '>=12'}
+
+ d3-timer@3.0.1:
+ resolution: {integrity: sha512-ndfJ/JxxMd3nw31uyKoY2naivF+r29V+Lc0svZxe1JvvIRmi8hUsrMvdOwgS1o6uBHmiz91geQ0ylPP0aj1VUA==}
+ engines: {node: '>=12'}
+
+ d3-transition@3.0.1:
+ resolution: {integrity: sha512-ApKvfjsSR6tg06xrL434C0WydLr7JewBB3V+/39RMHsaXTOG0zmt/OAXeng5M5LBm0ojmxJrpomQVZ1aPvBL4w==}
+ engines: {node: '>=12'}
+ peerDependencies:
+ d3-selection: 2 - 3
+
+ d3-zoom@3.0.0:
+ resolution: {integrity: sha512-b8AmV3kfQaqWAuacbPuNbL6vahnOJflOhexLzMMNLga62+/nh0JzvJ0aO/5a5MVgUFGS7Hu1P9P03o3fJkDCyw==}
+ engines: {node: '>=12'}
+
+ d3@7.9.0:
+ resolution: {integrity: sha512-e1U46jVP+w7Iut8Jt8ri1YsPOvFpg46k+K8TpCb0P+zjCkjkPnV7WzfDJzMHy1LnA+wj5pLT1wjO901gLXeEhA==}
+ engines: {node: '>=12'}
+
+ dagre-d3-es@7.0.13:
+ resolution: {integrity: sha512-efEhnxpSuwpYOKRm/L5KbqoZmNNukHa/Flty4Wp62JRvgH2ojwVgPgdYyr4twpieZnyRDdIH7PY2mopX26+j2Q==}
+
+ dayjs@1.11.19:
+ resolution: {integrity: sha512-t5EcLVS6QPBNqM2z8fakk/NKel+Xzshgt8FFKAn+qwlD1pzZWxh0nVCrvFK7ZDb6XucZeF9z8C7CBWTRIVApAw==}
+
+ debug@4.4.3:
+ resolution: {integrity: sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==}
+ engines: {node: '>=6.0'}
+ peerDependencies:
+ supports-color: '*'
+ peerDependenciesMeta:
+ supports-color:
+ optional: true
+
+ decamelize@1.2.0:
+ resolution: {integrity: sha512-z2S+W9X73hAUUki+N+9Za2lBlun89zigOyGrsax+KUQ6wKW4ZoWpEYBkGhQjwAjjDCkWxhY0VKEhk8wzY7F5cA==}
+ engines: {node: '>=0.10.0'}
+
+ decode-named-character-reference@1.2.0:
+ resolution: {integrity: sha512-c6fcElNV6ShtZXmsgNgFFV5tVX2PaV4g+MOAkb8eXHvn6sryJBrZa9r0zV6+dtTyoCKxtDy5tyQ5ZwQuidtd+Q==}
+
+ delaunator@5.0.1:
+ resolution: {integrity: sha512-8nvh+XBe96aCESrGOqMp/84b13H9cdKbG5P2ejQCh4d4sK9RL4371qou9drQjMhvnPmhWl5hnmqbEE0fXr9Xnw==}
+
+ dequal@2.0.3:
+ resolution: {integrity: sha512-0je+qPKHEMohvfRTCEo3CrPG6cAzAYgmzKyxRiYSSDkS6eGJdyVJm7WaYA5ECaAD9wLB2T4EEeymA5aFVcYXCA==}
+ engines: {node: '>=6'}
+
+ detect-libc@2.1.2:
+ resolution: {integrity: sha512-Btj2BOOO83o3WyH59e8MgXsxEQVcarkUOpEYrubB0urwnN10yQ364rsiByU11nZlqWYZm05i/of7io4mzihBtQ==}
+ engines: {node: '>=8'}
+
+ devlop@1.1.0:
+ resolution: {integrity: sha512-RWmIqhcFf1lRYBvNmr7qTNuyCt/7/ns2jbpp1+PalgE/rDQcBT0fioSMUpJ93irlUhC5hrg4cYqe6U+0ImW0rA==}
+
+ dijkstrajs@1.0.3:
+ resolution: {integrity: sha512-qiSlmBq9+BCdCA/L46dw8Uy93mloxsPSbwnm5yrKn2vMPiy8KyAskTF6zuV/j5BMsmOGZDPs7KjU+mjb670kfA==}
+
+ dom-serializer@2.0.0:
+ resolution: {integrity: sha512-wIkAryiqt/nV5EQKqQpo3SToSOV9J0DnbJqwK7Wv/Trc92zIAYZ4FlMu+JPFW1DfGFt81ZTCGgDEabffXeLyJg==}
+
+ domelementtype@2.3.0:
+ resolution: {integrity: sha512-OLETBj6w0OsagBwdXnPdN0cnMfF9opN69co+7ZrbfPGrdpPVNBUj02spi6B1N7wChLQiPn4CSH/zJvXw56gmHw==}
+
+ domhandler@5.0.3:
+ resolution: {integrity: sha512-cgwlv/1iFQiFnU96XXgROh8xTeetsnJiDsTc7TYCLFd9+/WNkIqPTxiM/8pSd8VIrhXGTf1Ny1q1hquVqDJB5w==}
+ engines: {node: '>= 4'}
+
+ dompurify@3.3.1:
+ resolution: {integrity: sha512-qkdCKzLNtrgPFP1Vo+98FRzJnBRGe4ffyCea9IwHB1fyxPOeNTHpLKYGd4Uk9xvNoH0ZoOjwZxNptyMwqrId1Q==}
+
+ domutils@3.2.2:
+ resolution: {integrity: sha512-6kZKyUajlDuqlHKVX1w7gyslj9MPIXzIFiz/rGu35uC1wMi+kMhQwGhl4lt9unC9Vb9INnY9Z3/ZA3+FhASLaw==}
+
+ electron-to-chromium@1.5.267:
+ resolution: {integrity: sha512-0Drusm6MVRXSOJpGbaSVgcQsuB4hEkMpHXaVstcPmhu5LIedxs1xNK/nIxmQIU/RPC0+1/o0AVZfBTkTNJOdUw==}
+
+ emoji-regex@8.0.0:
+ resolution: {integrity: sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==}
+
+ encoding-sniffer@0.2.1:
+ resolution: {integrity: sha512-5gvq20T6vfpekVtqrYQsSCFZ1wEg5+wW0/QaZMWkFr6BqD3NfKs0rLCx4rrVlSWJeZb5NBJgVLswK/w2MWU+Gw==}
+
+ entities@4.5.0:
+ resolution: {integrity: sha512-V0hjH4dGPh9Ao5p0MoRY6BVqtwCjhz6vI5LT8AJ55H+4g9/4vbHx1I54fS0XuclLhDHArPQCiMjDxjaL8fPxhw==}
+ engines: {node: '>=0.12'}
+
+ entities@6.0.1:
+ resolution: {integrity: sha512-aN97NXWF6AWBTahfVOIrB/NShkzi5H7F9r1s9mD3cDj4Ko5f2qhhVoYMibXF7GlLveb/D2ioWay8lxI97Ven3g==}
+ engines: {node: '>=0.12'}
+
+ entities@7.0.0:
+ resolution: {integrity: sha512-FDWG5cmEYf2Z00IkYRhbFrwIwvdFKH07uV8dvNy0omp/Qb1xcyCWp2UDtcwJF4QZZvk0sLudP6/hAu42TaqVhQ==}
+ engines: {node: '>=0.12'}
+
+ envinfo@7.21.0:
+ resolution: {integrity: sha512-Lw7I8Zp5YKHFCXL7+Dz95g4CcbMEpgvqZNNq3AmlT5XAV6CgAAk6gyAMqn2zjw08K9BHfcNuKrMiCPLByGafow==}
+ engines: {node: '>=4'}
+ hasBin: true
+
+ esbuild@0.25.12:
+ resolution: {integrity: sha512-bbPBYYrtZbkt6Os6FiTLCTFxvq4tt3JKall1vRwshA3fdVztsLAatFaZobhkBC8/BrPetoa0oksYoKXoG4ryJg==}
+ engines: {node: '>=18'}
+ hasBin: true
+
+ esbuild@0.27.2:
+ resolution: {integrity: sha512-HyNQImnsOC7X9PMNaCIeAm4ISCQXs5a5YasTXVliKv4uuBo1dKrG0A+uQS8M5eXjVMnLg3WgXaKvprHlFJQffw==}
+ engines: {node: '>=18'}
+ hasBin: true
+
+ escalade@3.2.0:
+ resolution: {integrity: sha512-WUj2qlxaQtO4g6Pq5c29GTcWGDyd8itL8zTlipgECz3JesAiiOKotd8JU6otB3PACgG6xkJUyVhboMS+bje/jA==}
+ engines: {node: '>=6'}
+
+ esm@3.2.25:
+ resolution: {integrity: sha512-U1suiZ2oDVWv4zPO56S0NcR5QriEahGtdN2OR6FiOG4WJvcjBVFB0qI4+eKoWFH483PKGuLuu6V8Z4T5g63UVA==}
+ engines: {node: '>=6'}
+
+ esprima@4.0.1:
+ resolution: {integrity: sha512-eGuFFw7Upda+g4p+QHvnW0RyTX/SVeJBDM/gCtMARO0cLuT2HcEKnTPvhjV6aGeqrCB/sbNop0Kszm0jsaWU4A==}
+ engines: {node: '>=4'}
+ hasBin: true
+
+ estree-walker@2.0.2:
+ resolution: {integrity: sha512-Rfkk/Mp/DL7JVje3u18FxFujQlTNR2q6QfMSMB7AvCBx91NGj/ba3kCfza0f6dVDbw7YlRf/nDrn7pQrCCyQ/w==}
+
+ extend-shallow@2.0.1:
+ resolution: {integrity: sha512-zCnTtlxNoAiDc3gqY2aYAWFx7XWWiasuF2K8Me5WbN8otHKTUKBwjPtNpRs/rbUZm7KxWAaNj7P1a/p52GbVug==}
+ engines: {node: '>=0.10.0'}
+
+ extend@3.0.2:
+ resolution: {integrity: sha512-fjquC59cD7CyW6urNXK0FBufkZcoiGG80wTuPujX590cB5Ttln20E2UB4S/WARVqhXffZl2LNgS+gQdPIIim/g==}
+
+ fast-glob@3.3.3:
+ resolution: {integrity: sha512-7MptL8U0cqcFdzIzwOTHoilX9x5BrNqye7Z/LuC7kCMRio1EMSyqRK3BEAUD7sXRq4iT4AzTVuZdhgQ2TCvYLg==}
+ engines: {node: '>=8.6.0'}
+
+ fastq@1.20.1:
+ resolution: {integrity: sha512-GGToxJ/w1x32s/D2EKND7kTil4n8OVk/9mycTc4VDza13lOvpUZTGX3mFSCtV9ksdGBVzvsyAVLM6mHFThxXxw==}
+
+ fdir@6.5.0:
+ resolution: {integrity: sha512-tIbYtZbucOs0BRGqPJkshJUYdL+SDH7dVM8gjy+ERp3WAUjLEFJE+02kanyHtwjWOnwrKYBiwAmM0p4kLJAnXg==}
+ engines: {node: '>=12.0.0'}
+ peerDependencies:
+ picomatch: ^3 || ^4
+ peerDependenciesMeta:
+ picomatch:
+ optional: true
+
+ fflate@0.8.2:
+ resolution: {integrity: sha512-cPJU47OaAoCbg0pBvzsgpTPhmhqI5eJjh/JIu8tPj5q+T7iLvW/JAYUqmE7KOB4R1ZyEhzBaIQpQpardBF5z8A==}
+
+ fill-range@7.1.1:
+ resolution: {integrity: sha512-YsGpe3WHLK8ZYi4tWDg2Jy3ebRz2rXowDxnld4bkQB00cc/1Zw9AWnC0i9ztDJitivtQvaI9KaLyKrc+hBW0yg==}
+ engines: {node: '>=8'}
+
+ find-up@4.1.0:
+ resolution: {integrity: sha512-PpOwAdQ/YlXQ2vj8a3h8IipDuYRi3wceVQQGYWxNINccq40Anw7BlsEXCMbt1Zt+OLA6Fq9suIpIWD0OsnISlw==}
+ engines: {node: '>=8'}
+
+ fraction.js@5.3.4:
+ resolution: {integrity: sha512-1X1NTtiJphryn/uLQz3whtY6jK3fTqoE3ohKs0tT+Ujr1W59oopxmoEh7Lu5p6vBaPbgoM0bzveAW4Qi5RyWDQ==}
+
+ fs-extra@11.3.3:
+ resolution: {integrity: sha512-VWSRii4t0AFm6ixFFmLLx1t7wS1gh+ckoa84aOeapGum0h+EZd1EhEumSB+ZdDLnEPuucsVB9oB7cxJHap6Afg==}
+ engines: {node: '>=14.14'}
+
+ fsevents@2.3.3:
+ resolution: {integrity: sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==}
+ engines: {node: ^8.16.0 || ^10.6.0 || >=11.0.0}
+ os: [darwin]
+
+ get-caller-file@2.0.5:
+ resolution: {integrity: sha512-DyFP3BM/3YHTQOCUL/w0OZHR0lpKeGrxotcHWcqNEdnltqFwXVfhEBQ94eIo34AfQpo0rGki4cyIiftY06h2Fg==}
+ engines: {node: 6.* || 8.* || >= 10.*}
+
+ get-east-asian-width@1.4.0:
+ resolution: {integrity: sha512-QZjmEOC+IT1uk6Rx0sX22V6uHWVwbdbxf1faPqJ1QhLdGgsRGCZoyaQBm/piRdJy/D2um6hM1UP7ZEeQ4EkP+Q==}
+ engines: {node: '>=18'}
+
+ giscus@1.6.0:
+ resolution: {integrity: sha512-Zrsi8r4t1LVW950keaWcsURuZUQwUaMKjvJgTCY125vkW6OiEBkatE7ScJDbpqKHdZwb///7FVC21SE3iFK3PQ==}
+
+ glob-parent@5.1.2:
+ resolution: {integrity: sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow==}
+ engines: {node: '>= 6'}
+
+ globby@14.0.2:
+ resolution: {integrity: sha512-s3Fq41ZVh7vbbe2PN3nrW7yC7U7MFVc5c98/iTl9c2GawNMKx/J648KQRW6WKkuU8GIbbh2IXfIRQjOZnXcTnw==}
+ engines: {node: '>=18'}
+
+ graceful-fs@4.2.11:
+ resolution: {integrity: sha512-RbJ5/jmFcNNCcDV5o9eTnBLJ/HszWV0P73bc+Ff4nS/rJj+YaS6IGyiOL0VoBYX+l1Wrl3k63h/KrH+nhJ0XvQ==}
+
+ gray-matter@4.0.3:
+ resolution: {integrity: sha512-5v6yZd4JK3eMI3FqqCouswVqwugaA9r4dNZB1wwcmrD02QkV5H0y7XBQW8QwQqEaZY1pM9aqORSORhJRdNK44Q==}
+ engines: {node: '>=6.0'}
+
+ hachure-fill@0.5.2:
+ resolution: {integrity: sha512-3GKBOn+m2LX9iq+JC1064cSFprJY4jL1jCXTcpnfER5HYE2l/4EfWSGzkPa/ZDBmYI0ZOEj5VHV/eKnPGkHuOg==}
+
+ has-flag@4.0.0:
+ resolution: {integrity: sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==}
+ engines: {node: '>=8'}
+
+ hash-sum@2.0.0:
+ resolution: {integrity: sha512-WdZTbAByD+pHfl/g9QSsBIIwy8IT+EsPiKDs0KNX+zSHhdDLFKdZu0BQHljvO+0QI/BasbMSUa8wYNCZTvhslg==}
+
+ hast-util-from-html@2.0.3:
+ resolution: {integrity: sha512-CUSRHXyKjzHov8yKsQjGOElXy/3EKpyX56ELnkHH34vDVw1N1XSQ1ZcAvTyAPtGqLTuKP/uxM+aLkSPqF/EtMw==}
+
+ hast-util-from-parse5@8.0.3:
+ resolution: {integrity: sha512-3kxEVkEKt0zvcZ3hCRYI8rqrgwtlIOFMWkbclACvjlDw8Li9S2hk/d51OI0nr/gIpdMHNepwgOKqZ/sy0Clpyg==}
+
+ hast-util-parse-selector@4.0.0:
+ resolution: {integrity: sha512-wkQCkSYoOGCRKERFWcxMVMOcYE2K1AaNLU8DXS9arxnLOUEWbOXKXiJUNzEpqZ3JOKpnha3jkFrumEjVliDe7A==}
+
+ hast-util-sanitize@5.0.2:
+ resolution: {integrity: sha512-3yTWghByc50aGS7JlGhk61SPenfE/p1oaFeNwkOOyrscaOkMGrcW9+Cy/QAIOBpZxP1yqDIzFMR0+Np0i0+usg==}
+
+ hast-util-to-html@9.0.5:
+ resolution: {integrity: sha512-OguPdidb+fbHQSU4Q4ZiLKnzWo8Wwsf5bZfbvu7//a9oTYoqD/fWpe96NuHkoS9h0ccGOTe0C4NGXdtS0iObOw==}
+
+ hast-util-whitespace@3.0.0:
+ resolution: {integrity: sha512-88JUN06ipLwsnv+dVn+OIYOvAuvBMy/Qoi6O7mQHxdPXpjy+Cd6xRkWwux7DKO+4sYILtLBRIKgsdpS2gQc7qw==}
+
+ hastscript@9.0.1:
+ resolution: {integrity: sha512-g7df9rMFX/SPi34tyGCyUBREQoKkapwdY/T04Qn9TDWfHhAYt4/I0gMVirzK5wEzeUqIjEB+LXC/ypb7Aqno5w==}
+
+ hookable@5.5.3:
+ resolution: {integrity: sha512-Yc+BQe8SvoXH1643Qez1zqLRmbA5rCL+sSmk6TVos0LWVfNIB7PGncdlId77WzLGSIB5KaWgTaNTs2lNVEI6VQ==}
+
+ html-void-elements@3.0.0:
+ resolution: {integrity: sha512-bEqo66MRXsUGxWHV5IP0PUiAWwoEjba4VCzg0LjFJBpchPaTfyfCKTG6bc5F8ucKec3q5y6qOdGyYTSBEvhCrg==}
+
+ htmlparser2@10.0.0:
+ resolution: {integrity: sha512-TwAZM+zE5Tq3lrEHvOlvwgj1XLWQCtaaibSN11Q+gGBAS7Y1uZSWwXXRe4iF6OXnaq1riyQAPFOBtYc77Mxq0g==}
+
+ husky@9.1.7:
+ resolution: {integrity: sha512-5gs5ytaNjBrh5Ow3zrvdUUY+0VxIuWVL4i9irt6friV+BqdCfmV11CQTWMiBYWHbXhco+J1kHfTOUkePhCDvMA==}
+ engines: {node: '>=18'}
+ hasBin: true
+
+ iconv-lite@0.6.3:
+ resolution: {integrity: sha512-4fCk79wshMdzMp2rH06qWrJE4iolqLhCUH+OiuIgU++RB0+94NlDL81atO7GX55uUKueo0txHNtvEyI6D7WdMw==}
+ engines: {node: '>=0.10.0'}
+
+ ignore@5.3.2:
+ resolution: {integrity: sha512-hsBTNUqQTDwkWtcdYI2i06Y/nUBEsNEDJKjWdigLvegy8kDuJAS8uRlpkkcQpyEXL0Z/pjDy5HBmMjRCJ2gq+g==}
+ engines: {node: '>= 4'}
+
+ immutable@5.1.4:
+ resolution: {integrity: sha512-p6u1bG3YSnINT5RQmx/yRZBpenIl30kVxkTLDyHLIMk0gict704Q9n+thfDI7lTRm9vXdDYutVzXhzcThxTnXA==}
+
+ internmap@1.0.1:
+ resolution: {integrity: sha512-lDB5YccMydFBtasVtxnZ3MRBHuaoE8GKsppq+EchKL2U4nK/DmEpPHNH8MZe5HkMtpSiTSOZwfN0tzYjO/lJEw==}
+
+ internmap@2.0.3:
+ resolution: {integrity: sha512-5Hh7Y1wQbvY5ooGgPbDaL5iYLAPzMTUrjMulskHLH6wnv/A+1q5rgEaiuqEjB+oxGXIVZs1FF+R/KPN3ZSQYYg==}
+ engines: {node: '>=12'}
+
+ is-alphabetical@2.0.1:
+ resolution: {integrity: sha512-FWyyY60MeTNyeSRpkM2Iry0G9hpr7/9kD40mD/cGQEuilcZYS4okz8SN2Q6rLCJ8gbCt6fN+rC+6tMGS99LaxQ==}
+
+ is-alphanumerical@2.0.1:
+ resolution: {integrity: sha512-hmbYhX/9MUMF5uh7tOXyK/n0ZvWpad5caBA17GsC6vyuCqaWliRG5K1qS9inmUhEMaOBIW7/whAnSwveW/LtZw==}
+
+ is-decimal@2.0.1:
+ resolution: {integrity: sha512-AAB9hiomQs5DXWcRB1rqsxGUstbRroFOPPVAomNk/3XHR5JyEZChOyTWe2oayKnsSsr/kcGqF+z6yuH6HHpN0A==}
+
+ is-extendable@0.1.1:
+ resolution: {integrity: sha512-5BMULNob1vgFX6EjQw5izWDxrecWK9AM72rugNr0TFldMOi0fj6Jk+zeKIt0xGj4cEfQIJth4w3OKWOJ4f+AFw==}
+ engines: {node: '>=0.10.0'}
+
+ is-extglob@2.1.1:
+ resolution: {integrity: sha512-SbKbANkN603Vi4jEZv49LeVJMn4yGwsbzZworEoyEiutsN3nJYdbO36zfhGJ6QEDpOZIFkDtnq5JRxmvl3jsoQ==}
+ engines: {node: '>=0.10.0'}
+
+ is-fullwidth-code-point@3.0.0:
+ resolution: {integrity: sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg==}
+ engines: {node: '>=8'}
+
+ is-glob@4.0.3:
+ resolution: {integrity: sha512-xelSayHH36ZgE7ZWhli7pW34hNbNl8Ojv5KVmkJD4hBdD3th8Tfk9vYasLM+mXWOZhFkgZfxhLSnrwRr4elSSg==}
+ engines: {node: '>=0.10.0'}
+
+ is-hexadecimal@2.0.1:
+ resolution: {integrity: sha512-DgZQp241c8oO6cA1SbTEWiXeoxV42vlcJxgH+B3hi1AiqqKruZR3ZGF8In3fj4+/y/7rHvlOZLZtgJ/4ttYGZg==}
+
+ is-interactive@2.0.0:
+ resolution: {integrity: sha512-qP1vozQRI+BMOPcjFzrjXuQvdak2pHNUMZoeG2eRbiSqyvbEf/wQtEOTOX1guk6E3t36RkaqiSt8A/6YElNxLQ==}
+ engines: {node: '>=12'}
+
+ is-number@7.0.0:
+ resolution: {integrity: sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng==}
+ engines: {node: '>=0.12.0'}
+
+ is-plain-obj@4.1.0:
+ resolution: {integrity: sha512-+Pgi+vMuUNkJyExiMBt5IlFoMyKnr5zhJ4Uspz58WOhBF5QoIZkFyNHIbBAtHwzVAgk5RtndVNsDRN61/mmDqg==}
+ engines: {node: '>=12'}
+
+ is-unicode-supported@2.1.0:
+ resolution: {integrity: sha512-mE00Gnza5EEB3Ds0HfMyllZzbBrmLOX3vfWoj9A9PEnTfratQ/BcaJOuMhnkhjXvb2+FkY3VuHqtAGpTPmglFQ==}
+ engines: {node: '>=18'}
+
+ is-what@5.5.0:
+ resolution: {integrity: sha512-oG7cgbmg5kLYae2N5IVd3jm2s+vldjxJzK1pcu9LfpGuQ93MQSzo0okvRna+7y5ifrD+20FE8FvjusyGaz14fw==}
+ engines: {node: '>=18'}
+
+ js-yaml@3.14.2:
+ resolution: {integrity: sha512-PMSmkqxr106Xa156c2M265Z+FTrPl+oxd/rgOQy2tijQeK5TxQ43psO1ZCwhVOSdnn+RzkzlRz/eY4BgJBYVpg==}
+ hasBin: true
+
+ js-yaml@4.1.1:
+ resolution: {integrity: sha512-qQKT4zQxXl8lLwBtHMWwaTcGfFOZviOJet3Oy/xmGk2gZH677CJM9EvtfdSkgWcATZhj/55JZ0rmy3myCT5lsA==}
+ hasBin: true
+
+ jsonc-parser@3.3.1:
+ resolution: {integrity: sha512-HUgH65KyejrUFPvHFPbqOY0rsFip3Bo5wb4ngvdi1EpCYWUQDC5V+Y7mZws+DLkr4M//zQJoanu1SP+87Dv1oQ==}
+
+ jsonfile@6.2.0:
+ resolution: {integrity: sha512-FGuPw30AdOIUTRMC2OMRtQV+jkVj2cfPqSeWXv1NEAJ1qZ5zb1X6z1mFhbfOB/iy3ssJCD+3KuZ8r8C3uVFlAg==}
+
+ katex@0.16.27:
+ resolution: {integrity: sha512-aeQoDkuRWSqQN6nSvVCEFvfXdqo1OQiCmmW1kc9xSdjutPv7BGO7pqY9sQRJpMOGrEdfDgF2TfRXe5eUAD2Waw==}
+ hasBin: true
+
+ khroma@2.1.0:
+ resolution: {integrity: sha512-Ls993zuzfayK269Svk9hzpeGUKob/sIgZzyHYdjQoAdQetRKpOLj+k/QQQ/6Qi0Yz65mlROrfd+Ev+1+7dz9Kw==}
+
+ kind-of@6.0.3:
+ resolution: {integrity: sha512-dcS1ul+9tmeD95T+x28/ehLgd9mENa3LsvDTtzm3vyBEO7RPptvAD+t44WVXaUjTBRcrpFeFlC8WCruUR456hw==}
+ engines: {node: '>=0.10.0'}
+
+ langium@3.3.1:
+ resolution: {integrity: sha512-QJv/h939gDpvT+9SiLVlY7tZC3xB2qK57v0J04Sh9wpMb6MP1q8gB21L3WIo8T5P1MSMg3Ep14L7KkDCFG3y4w==}
+ engines: {node: '>=16.0.0'}
+
+ layout-base@1.0.2:
+ resolution: {integrity: sha512-8h2oVEZNktL4BH2JCOI90iD1yXwL6iNW7KcCKT2QZgQJR2vbqDsldCTPRU9NifTCqHZci57XvQQ15YTu+sTYPg==}
+
+ layout-base@2.0.1:
+ resolution: {integrity: sha512-dp3s92+uNI1hWIpPGH3jK2kxE2lMjdXdr+DH8ynZHpd6PUlH6x6cbuXnoMmiNumznqaNO31xu9e79F0uuZ0JFg==}
+
+ lilconfig@3.1.3:
+ resolution: {integrity: sha512-/vlFKAoH5Cgt3Ie+JLhRbwOsCQePABiU3tJ1egGvyQ+33R/vcwM2Zl2QR/LzjsBeItPt3oSVXapn+m4nQDvpzw==}
+ engines: {node: '>=14'}
+
+ linkify-it@5.0.0:
+ resolution: {integrity: sha512-5aHCbzQRADcdP+ATqnDuhhJ/MRIqDkZX5pyjFHRRysS8vZ5AbqGEoFIb6pYHPZ+L/OC2Lc+xT8uHVVR5CAK/wQ==}
+
+ lit-element@4.2.2:
+ resolution: {integrity: sha512-aFKhNToWxoyhkNDmWZwEva2SlQia+jfG0fjIWV//YeTaWrVnOxD89dPKfigCUspXFmjzOEUQpOkejH5Ly6sG0w==}
+
+ lit-html@3.3.2:
+ resolution: {integrity: sha512-Qy9hU88zcmaxBXcc10ZpdK7cOLXvXpRoBxERdtqV9QOrfpMZZ6pSYP91LhpPtap3sFMUiL7Tw2RImbe0Al2/kw==}
+
+ lit@3.3.2:
+ resolution: {integrity: sha512-NF9zbsP79l4ao2SNrH3NkfmFgN/hBYSQo90saIVI1o5GpjAdCPVstVzO1MrLOakHoEhYkrtRjPK6Ob521aoYWQ==}
+
+ locate-path@5.0.0:
+ resolution: {integrity: sha512-t7hw9pI+WvuwNJXwk5zVHpyhIqzg2qTlklJOf0mVxGSbe3Fp2VieZcduNYjaLDoy6p9uGpQEGWG87WpMKlNq8g==}
+ engines: {node: '>=8'}
+
+ lodash-es@4.17.21:
+ resolution: {integrity: sha512-mKnC+QJ9pWVzv+C4/U3rRsHapFfHvQFoFB92e52xeyGMcX6/OlIl78je1u8vePzYZSkkogMPJ2yjxxsb89cxyw==}
+
+ lodash-es@4.17.22:
+ resolution: {integrity: sha512-XEawp1t0gxSi9x01glktRZ5HDy0HXqrM0x5pXQM98EaI0NxO6jVM7omDOxsuEo5UIASAnm2bRp1Jt/e0a2XU8Q==}
+
+ log-symbols@7.0.1:
+ resolution: {integrity: sha512-ja1E3yCr9i/0hmBVaM0bfwDjnGy8I/s6PP4DFp+yP+a+mrHO4Rm7DtmnqROTUkHIkqffC84YY7AeqX6oFk0WFg==}
+ engines: {node: '>=18'}
+
+ magic-string@0.30.21:
+ resolution: {integrity: sha512-vd2F4YUyEXKGcLHoq+TEyCjxueSeHnFxyyjNp80yg0XV4vUhnDer/lvvlqM/arB5bXQN5K2/3oinyCRyx8T2CQ==}
+
+ markdown-it-anchor@9.2.0:
+ resolution: {integrity: sha512-sa2ErMQ6kKOA4l31gLGYliFQrMKkqSO0ZJgGhDHKijPf0pNFM9vghjAh3gn26pS4JDRs7Iwa9S36gxm3vgZTzg==}
+ peerDependencies:
+ '@types/markdown-it': '*'
+ markdown-it: '*'
+
+ markdown-it-emoji@3.0.0:
+ resolution: {integrity: sha512-+rUD93bXHubA4arpEZO3q80so0qgoFJEKRkRbjKX8RTdca89v2kfyF+xR3i2sQTwql9tpPZPOQN5B+PunspXRg==}
+
+ markdown-it@14.1.0:
+ resolution: {integrity: sha512-a54IwgWPaeBCAAsv13YgmALOF1elABB08FxO9i+r4VFk5Vl4pKokRPeX8u5TCgSsPi6ec1otfLjdOpVcgbpshg==}
+ hasBin: true
+
+ markdownlint-cli2-formatter-default@0.0.5:
+ resolution: {integrity: sha512-4XKTwQ5m1+Txo2kuQ3Jgpo/KmnG+X90dWt4acufg6HVGadTUG5hzHF/wssp9b5MBYOMCnZ9RMPaU//uHsszF8Q==}
+ peerDependencies:
+ markdownlint-cli2: '>=0.0.4'
+
+ markdownlint-cli2@0.17.1:
+ resolution: {integrity: sha512-n1Im9lhKJJE12/u2N0GWBwPqeb0HGdylN8XpSFg9hbj35+QalY9Vi6mxwUQdG6wlSrrIq9ZDQ0Q85AQG9V2WOg==}
+ engines: {node: '>=18'}
+ hasBin: true
+
+ markdownlint@0.37.3:
+ resolution: {integrity: sha512-eoQqH0291YCCjd+Pe1PUQ9AmWthlVmS0XWgcionkZ8q34ceZyRI+pYvsWksXJJL8OBkWCPwp1h/pnXxrPFC4oA==}
+ engines: {node: '>=18'}
+
+ marked@16.4.2:
+ resolution: {integrity: sha512-TI3V8YYWvkVf3KJe1dRkpnjs68JUPyEa5vjKrp1XEEJUAOaQc+Qj+L1qWbPd0SJuAdQkFU0h73sXXqwDYxsiDA==}
+ engines: {node: '>= 20'}
+ hasBin: true
+
+ mathjax-full@3.2.2:
+ resolution: {integrity: sha512-+LfG9Fik+OuI8SLwsiR02IVdjcnRCy5MufYLi0C3TdMT56L/pjB0alMVGgoWJF8pN9Rc7FESycZB9BMNWIid5w==}
+ deprecated: Version 4 replaces this package with the scoped package @mathjax/src
+
+ mdast-util-to-hast@13.2.1:
+ resolution: {integrity: sha512-cctsq2wp5vTsLIcaymblUriiTcZd0CwWtCbLvrOzYCDZoWyMNV8sZ7krj09FSnsiJi3WVsHLM4k6Dq/yaPyCXA==}
+
+ mdurl@2.0.0:
+ resolution: {integrity: sha512-Lf+9+2r+Tdp5wXDXC4PcIBjTDtq4UKjCPMQhKIuzpJNW0b96kVqSwW0bT7FhRSfmAiFYgP+SCRvdrDozfh0U5w==}
+
+ merge2@1.4.1:
+ resolution: {integrity: sha512-8q7VEgMJW4J8tcfVPy8g09NcQwZdbwFEqhe/WZkoIzjn/3TGDwtOCYtXGxA3O8tPzpczCCDgv+P2P5y00ZJOOg==}
+ engines: {node: '>= 8'}
+
+ mermaid@11.12.2:
+ resolution: {integrity: sha512-n34QPDPEKmaeCG4WDMGy0OT6PSyxKCfy2pJgShP+Qow2KLrvWjclwbc3yXfSIf4BanqWEhQEpngWwNp/XhZt6w==}
+
+ mhchemparser@4.2.1:
+ resolution: {integrity: sha512-kYmyrCirqJf3zZ9t/0wGgRZ4/ZJw//VwaRVGA75C4nhE60vtnIzhl9J9ndkX/h6hxSN7pjg/cE0VxbnNM+bnDQ==}
+
+ micromark-core-commonmark@2.0.2:
+ resolution: {integrity: sha512-FKjQKbxd1cibWMM1P9N+H8TwlgGgSkWZMmfuVucLCHaYqeSvJ0hFeHsIa65pA2nYbes0f8LDHPMrd9X7Ujxg9w==}
+
+ micromark-extension-directive@3.0.2:
+ resolution: {integrity: sha512-wjcXHgk+PPdmvR58Le9d7zQYWy+vKEU9Se44p2CrCDPiLr2FMyiT4Fyb5UFKFC66wGB3kPlgD7q3TnoqPS7SZA==}
+
+ micromark-extension-gfm-autolink-literal@2.1.0:
+ resolution: {integrity: sha512-oOg7knzhicgQ3t4QCjCWgTmfNhvQbDDnJeVu9v81r7NltNCVmhPy1fJRX27pISafdjL+SVc4d3l48Gb6pbRypw==}
+
+ micromark-extension-gfm-footnote@2.1.0:
+ resolution: {integrity: sha512-/yPhxI1ntnDNsiHtzLKYnE3vf9JZ6cAisqVDauhp4CEHxlb4uoOTxOCJ+9s51bIB8U1N1FJ1RXOKTIlD5B/gqw==}
+
+ micromark-extension-gfm-table@2.1.0:
+ resolution: {integrity: sha512-Ub2ncQv+fwD70/l4ou27b4YzfNaCJOvyX4HxXU15m7mpYY+rjuWzsLIPZHJL253Z643RpbcP1oeIJlQ/SKW67g==}
+
+ micromark-extension-math@3.1.0:
+ resolution: {integrity: sha512-lvEqd+fHjATVs+2v/8kg9i5Q0AP2k85H0WUOwpIVvUML8BapsMvh1XAogmQjOCsLpoKRCVQqEkQBB3NhVBcsOg==}
+
+ micromark-factory-destination@2.0.1:
+ resolution: {integrity: sha512-Xe6rDdJlkmbFRExpTOmRj9N3MaWmbAgdpSrBQvCFqhezUn4AHqJHbaEnfbVYYiexVSs//tqOdY/DxhjdCiJnIA==}
+
+ micromark-factory-label@2.0.1:
+ resolution: {integrity: sha512-VFMekyQExqIW7xIChcXn4ok29YE3rnuyveW3wZQWWqF4Nv9Wk5rgJ99KzPvHjkmPXF93FXIbBp6YdW3t71/7Vg==}
+
+ micromark-factory-space@2.0.1:
+ resolution: {integrity: sha512-zRkxjtBxxLd2Sc0d+fbnEunsTj46SWXgXciZmHq0kDYGnck/ZSGj9/wULTV95uoeYiK5hRXP2mJ98Uo4cq/LQg==}
+
+ micromark-factory-title@2.0.1:
+ resolution: {integrity: sha512-5bZ+3CjhAd9eChYTHsjy6TGxpOFSKgKKJPJxr293jTbfry2KDoWkhBb6TcPVB4NmzaPhMs1Frm9AZH7OD4Cjzw==}
+
+ micromark-factory-whitespace@2.0.1:
+ resolution: {integrity: sha512-Ob0nuZ3PKt/n0hORHyvoD9uZhr+Za8sFoP+OnMcnWK5lngSzALgQYKMr9RJVOWLqQYuyn6ulqGWSXdwf6F80lQ==}
+
+ micromark-util-character@2.1.1:
+ resolution: {integrity: sha512-wv8tdUTJ3thSFFFJKtpYKOYiGP2+v96Hvk4Tu8KpCAsTMs6yi+nVmGh1syvSCsaxz45J6Jbw+9DD6g97+NV67Q==}
+
+ micromark-util-chunked@2.0.1:
+ resolution: {integrity: sha512-QUNFEOPELfmvv+4xiNg2sRYeS/P84pTW0TCgP5zc9FpXetHY0ab7SxKyAQCNCc1eK0459uoLI1y5oO5Vc1dbhA==}
+
+ micromark-util-classify-character@2.0.1:
+ resolution: {integrity: sha512-K0kHzM6afW/MbeWYWLjoHQv1sgg2Q9EccHEDzSkxiP/EaagNzCm7T/WMKZ3rjMbvIpvBiZgwR3dKMygtA4mG1Q==}
+
+ micromark-util-combine-extensions@2.0.1:
+ resolution: {integrity: sha512-OnAnH8Ujmy59JcyZw8JSbK9cGpdVY44NKgSM7E9Eh7DiLS2E9RNQf0dONaGDzEG9yjEl5hcqeIsj4hfRkLH/Bg==}
+
+ micromark-util-decode-numeric-character-reference@2.0.2:
+ resolution: {integrity: sha512-ccUbYk6CwVdkmCQMyr64dXz42EfHGkPQlBj5p7YVGzq8I7CtjXZJrubAYezf7Rp+bjPseiROqe7G6foFd+lEuw==}
+
+ micromark-util-encode@2.0.1:
+ resolution: {integrity: sha512-c3cVx2y4KqUnwopcO9b/SCdo2O67LwJJ/UyqGfbigahfegL9myoEFoDYZgkT7f36T0bLrM9hZTAaAyH+PCAXjw==}
+
+ micromark-util-html-tag-name@2.0.1:
+ resolution: {integrity: sha512-2cNEiYDhCWKI+Gs9T0Tiysk136SnR13hhO8yW6BGNyhOC4qYFnwF1nKfD3HFAIXA5c45RrIG1ub11GiXeYd1xA==}
+
+ micromark-util-normalize-identifier@2.0.1:
+ resolution: {integrity: sha512-sxPqmo70LyARJs0w2UclACPUUEqltCkJ6PhKdMIDuJ3gSf/Q+/GIe3WKl0Ijb/GyH9lOpUkRAO2wp0GVkLvS9Q==}
+
+ micromark-util-resolve-all@2.0.1:
+ resolution: {integrity: sha512-VdQyxFWFT2/FGJgwQnJYbe1jjQoNTS4RjglmSjTUlpUMa95Htx9NHeYW4rGDJzbjvCsl9eLjMQwGeElsqmzcHg==}
+
+ micromark-util-sanitize-uri@2.0.1:
+ resolution: {integrity: sha512-9N9IomZ/YuGGZZmQec1MbgxtlgougxTodVwDzzEouPKo3qFWvymFHWcnDi2vzV1ff6kas9ucW+o3yzJK9YB1AQ==}
+
+ micromark-util-subtokenize@2.1.0:
+ resolution: {integrity: sha512-XQLu552iSctvnEcgXw6+Sx75GflAPNED1qx7eBJ+wydBb2KCbRZe+NwvIEEMM83uml1+2WSXpBAcp9IUCgCYWA==}
+
+ micromark-util-symbol@2.0.1:
+ resolution: {integrity: sha512-vs5t8Apaud9N28kgCrRUdEed4UJ+wWNvicHLPxCa9ENlYuAY31M0ETy5y1vA33YoNPDFTghEbnh6efaE8h4x0Q==}
+
+ micromark-util-types@2.0.1:
+ resolution: {integrity: sha512-534m2WhVTddrcKVepwmVEVnUAmtrx9bfIjNoQHRqfnvdaHQiFytEhJoTgpWJvDEXCO5gLTQh3wYC1PgOJA4NSQ==}
+
+ micromark@4.0.1:
+ resolution: {integrity: sha512-eBPdkcoCNvYcxQOAKAlceo5SNdzZWfF+FcSupREAzdAh9rRmE239CEQAiTwIgblwnoM8zzj35sZ5ZwvSEOF6Kw==}
+
+ micromatch@4.0.8:
+ resolution: {integrity: sha512-PXwfBhYu0hBCPw8Dn0E+WDYb7af3dSLVWKi3HGv84IdF4TyFoC0ysxFd0Goxw7nSv4T/PzEJQxsYsEiFCKo2BA==}
+ engines: {node: '>=8.6'}
+
+ mimic-function@5.0.1:
+ resolution: {integrity: sha512-VP79XUPxV2CigYP3jWwAUFSku2aKqBH7uTAapFWCBqutsbmDo96KY5o8uh6U+/YSIn5OxJnXp73beVkpqMIGhA==}
+ engines: {node: '>=18'}
+
+ mitt@3.0.1:
+ resolution: {integrity: sha512-vKivATfr97l2/QBCYAkXYDbrIWPM2IIKEl7YPhjCvKlG3kE2gm+uBo6nEXK3M5/Ffh/FLpKExzOQ3JJoJGFKBw==}
+
+ mj-context-menu@0.6.1:
+ resolution: {integrity: sha512-7NO5s6n10TIV96d4g2uDpG7ZDpIhMh0QNfGdJw/W47JswFcosz457wqz/b5sAKvl12sxINGFCn80NZHKwxQEXA==}
+
+ mlly@1.8.0:
+ resolution: {integrity: sha512-l8D9ODSRWLe2KHJSifWGwBqpTZXIXTeo8mlKjY+E2HAakaTeNpqAyBZ8GSqLzHgw4XmHmC8whvpjJNMbFZN7/g==}
+
+ ms@2.1.3:
+ resolution: {integrity: sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==}
+
+ nano-staged@0.8.0:
+ resolution: {integrity: sha512-QSEqPGTCJbkHU2yLvfY6huqYPjdBrOaTMKatO1F8nCSrkQGXeKwtCiCnsdxnuMhbg3DTVywKaeWLGCE5oJpq0g==}
+ engines: {node: ^12.20.0 || ^14.13.1 || >=16.0.0}
+ hasBin: true
+
+ nanoid@3.3.11:
+ resolution: {integrity: sha512-N8SpfPUnUp1bK+PMYW8qSWdl9U+wwNWI4QKxOYDy9JAro3WMX7p2OeVRF9v+347pnakNevPmiHhNmZ2HbFA76w==}
+ engines: {node: ^10 || ^12 || ^13.7 || ^14 || >=15.0.1}
+ hasBin: true
+
+ nanoid@5.1.6:
+ resolution: {integrity: sha512-c7+7RQ+dMB5dPwwCp4ee1/iV/q2P6aK1mTZcfr1BTuVlyW9hJYiMPybJCcnBlQtuSmTIWNeazm/zqNoZSSElBg==}
+ engines: {node: ^18 || >=20}
+ hasBin: true
+
+ node-addon-api@7.1.1:
+ resolution: {integrity: sha512-5m3bsyrjFWE1xf7nz7YXdN4udnVtXK6/Yfgn5qnahL6bCkf2yKt4k3nuTKAtT4r3IG8JNR2ncsIMdZuAzJjHQQ==}
+
+ node-releases@2.0.27:
+ resolution: {integrity: sha512-nmh3lCkYZ3grZvqcCH+fjmQ7X+H0OeZgP40OierEaAptX4XofMh5kwNbWh7lBduUzCcV/8kZ+NDLCwm2iorIlA==}
+
+ nth-check@2.1.1:
+ resolution: {integrity: sha512-lqjrjmaOoAnWfMmBPL+XNnynZh2+swxiX3WUE0s4yEHI6m+AwrK2UZOimIRl3X/4QctVqS8AiZjFqyOGrMXb/w==}
+
+ onetime@7.0.0:
+ resolution: {integrity: sha512-VXJjc87FScF88uafS3JllDgvAm+c/Slfz06lorj2uAY34rlUu0Nt+v8wreiImcrgAjjIHp1rXpTDlLOGw29WwQ==}
+ engines: {node: '>=18'}
+
+ oniguruma-parser@0.12.1:
+ resolution: {integrity: sha512-8Unqkvk1RYc6yq2WBYRj4hdnsAxVze8i7iPfQr8e4uSP3tRv0rpZcbGUDvxfQQcdwHt/e9PrMvGCsa8OqG9X3w==}
+
+ oniguruma-to-es@4.3.4:
+ resolution: {integrity: sha512-3VhUGN3w2eYxnTzHn+ikMI+fp/96KoRSVK9/kMTcFqj1NRDh2IhQCKvYxDnWePKRXY/AqH+Fuiyb7VHSzBjHfA==}
+
+ ora@9.0.0:
+ resolution: {integrity: sha512-m0pg2zscbYgWbqRR6ABga5c3sZdEon7bSgjnlXC64kxtxLOyjRcbbUkLj7HFyy/FTD+P2xdBWu8snGhYI0jc4A==}
+ engines: {node: '>=20'}
+
+ p-limit@2.3.0:
+ resolution: {integrity: sha512-//88mFWSJx8lxCzwdAABTJL2MyWB12+eIY7MDL2SqLmAkeKU9qxRvWuSyTjm3FUmpBEMuFfckAIqEaVGUDxb6w==}
+ engines: {node: '>=6'}
+
+ p-locate@4.1.0:
+ resolution: {integrity: sha512-R79ZZ/0wAxKGu3oYMlz8jy/kbhsNrS7SKZ7PxEHBgJ5+F2mtFW2fK2cOtBh1cHYkQsbzFV7I+EoRKe6Yt0oK7A==}
+ engines: {node: '>=8'}
+
+ p-try@2.2.0:
+ resolution: {integrity: sha512-R4nPAVTAU0B9D35/Gk3uJf/7XYbQcyohSKdvAxIRSNghFl4e71hVoGnBNQz9cWaXxO2I10KTC+3jMdvvoKw6dQ==}
+ engines: {node: '>=6'}
+
+ package-manager-detector@1.6.0:
+ resolution: {integrity: sha512-61A5ThoTiDG/C8s8UMZwSorAGwMJ0ERVGj2OjoW5pAalsNOg15+iQiPzrLJ4jhZ1HJzmC2PIHT2oEiH3R5fzNA==}
+
+ parse-entities@4.0.2:
+ resolution: {integrity: sha512-GG2AQYWoLgL877gQIKeRPGO1xF9+eG1ujIb5soS5gPvLQ1y2o8FL90w2QWNdf9I361Mpp7726c+lj3U0qK1uGw==}
+
+ parse5-htmlparser2-tree-adapter@7.1.0:
+ resolution: {integrity: sha512-ruw5xyKs6lrpo9x9rCZqZZnIUntICjQAd0Wsmp396Ul9lN/h+ifgVV1x1gZHi8euej6wTfpqX8j+BFQxF0NS/g==}
+
+ parse5-parser-stream@7.1.2:
+ resolution: {integrity: sha512-JyeQc9iwFLn5TbvvqACIF/VXG6abODeB3Fwmv/TGdLk2LfbWkaySGY72at4+Ty7EkPZj854u4CrICqNk2qIbow==}
+
+ parse5@7.3.0:
+ resolution: {integrity: sha512-IInvU7fabl34qmi9gY8XOVxhYyMyuH2xUNpb2q8/Y+7552KlejkRvqvD19nMoUW/uQGGbqNpA6Tufu5FL5BZgw==}
+
+ path-data-parser@0.1.0:
+ resolution: {integrity: sha512-NOnmBpt5Y2RWbuv0LMzsayp3lVylAHLPUTut412ZA3l+C4uw4ZVkQbjShYCQ8TCpUMdPapr4YjUqLYD6v68j+w==}
+
+ path-exists@4.0.0:
+ resolution: {integrity: sha512-ak9Qy5Q7jYb2Wwcey5Fpvg2KoAc/ZIhLSLOSBmRmygPsGwkVVt0fZa0qrtMz+m6tJTAHfZQ8FnmB4MG4LWy7/w==}
+ engines: {node: '>=8'}
+
+ path-type@5.0.0:
+ resolution: {integrity: sha512-5HviZNaZcfqP95rwpv+1HDgUamezbqdSYTyzjTvwtJSnIH+3vnbmWsItli8OFEndS984VT55M3jduxZbX351gg==}
+ engines: {node: '>=12'}
+
+ pathe@2.0.3:
+ resolution: {integrity: sha512-WUjGcAqP1gQacoQe+OBJsFA7Ld4DyXuUIjZ5cc75cLHvJ7dtNsTugphxIADwspS+AraAUePCKrSVtPLFj/F88w==}
+
+ perfect-debounce@2.0.0:
+ resolution: {integrity: sha512-fkEH/OBiKrqqI/yIgjR92lMfs2K8105zt/VT6+7eTjNwisrsh47CeIED9z58zI7DfKdH3uHAn25ziRZn3kgAow==}
+
+ photoswipe@5.4.4:
+ resolution: {integrity: sha512-WNFHoKrkZNnvFFhbHL93WDkW3ifwVOXSW3w1UuZZelSmgXpIGiZSNlZJq37rR8YejqME2rHs9EhH9ZvlvFH2NA==}
+ engines: {node: '>= 0.12.0'}
+
+ picocolors@1.1.1:
+ resolution: {integrity: sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==}
+
+ picomatch@2.3.1:
+ resolution: {integrity: sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA==}
+ engines: {node: '>=8.6'}
+
+ picomatch@4.0.3:
+ resolution: {integrity: sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q==}
+ engines: {node: '>=12'}
+
+ pkg-types@1.3.1:
+ resolution: {integrity: sha512-/Jm5M4RvtBFVkKWRu2BLUTNP8/M2a+UwuAX+ae4770q1qVGtfjG+WTCupoZixokjmHiry8uI+dlY8KXYV5HVVQ==}
+
+ pngjs@5.0.0:
+ resolution: {integrity: sha512-40QW5YalBNfQo5yRYmiw7Yz6TKKVr3h6970B2YE+3fQpsWcrbj1PzJgxeJ19DRQjhMbKPIuMY8rFaXc8moolVw==}
+ engines: {node: '>=10.13.0'}
+
+ points-on-curve@0.2.0:
+ resolution: {integrity: sha512-0mYKnYYe9ZcqMCWhUjItv/oHjvgEsfKvnUTg8sAtnHr3GVy7rGkXCb6d5cSyqrWqL4k81b9CPg3urd+T7aop3A==}
+
+ points-on-path@0.2.1:
+ resolution: {integrity: sha512-25ClnWWuw7JbWZcgqY/gJ4FQWadKxGWk+3kR/7kD0tCaDtPPMj7oHu2ToLaVhfpnHrZzYby2w6tUA0eOIuUg8g==}
+
+ postcss-load-config@6.0.1:
+ resolution: {integrity: sha512-oPtTM4oerL+UXmx+93ytZVN82RrlY/wPUV8IeDxFrzIjXOLF1pN+EmKPLbubvKHT2HC20xXsCAH2Z+CKV6Oz/g==}
+ engines: {node: '>= 18'}
+ peerDependencies:
+ jiti: '>=1.21.0'
+ postcss: '>=8.0.9'
+ tsx: ^4.8.1
+ yaml: ^2.4.2
+ peerDependenciesMeta:
+ jiti:
+ optional: true
+ postcss:
+ optional: true
+ tsx:
+ optional: true
+ yaml:
+ optional: true
+
+ postcss-value-parser@4.2.0:
+ resolution: {integrity: sha512-1NNCs6uurfkVbeXG4S8JFT9t19m45ICnif8zWLd5oPSZ50QnwMfK+H3jv408d4jw/7Bttv5axS5IiHoLaVNHeQ==}
+
+ postcss@8.5.6:
+ resolution: {integrity: sha512-3Ybi1tAuwAP9s0r1UQ2J4n5Y0G05bJkpUIO0/bI9MhwmD70S5aTWbXGBwxHrelT+XM1k6dM0pk+SwNkpTRN7Pg==}
+ engines: {node: ^10 || ^12 || >=14}
+
+ prettier@3.4.2:
+ resolution: {integrity: sha512-e9MewbtFo+Fevyuxn/4rrcDAaq0IYxPGLvObpQjiZBMAzB9IGmzlnG9RZy3FFas+eBMu2vA0CszMeduow5dIuQ==}
+ engines: {node: '>=14'}
+ hasBin: true
+
+ property-information@7.1.0:
+ resolution: {integrity: sha512-TwEZ+X+yCJmYfL7TPUOcvBZ4QfoT5YenQiJuX//0th53DE6w0xxLEtfK3iyryQFddXuvkIk51EEgrJQ0WJkOmQ==}
+
+ punycode.js@2.3.1:
+ resolution: {integrity: sha512-uxFIHU0YlHYhDQtV4R9J6a52SLx28BCjT+4ieh7IGbgwVJWO+km431c4yRlREUAsAmt/uMjQUyQHNEPf0M39CA==}
+ engines: {node: '>=6'}
+
+ qrcode@1.5.4:
+ resolution: {integrity: sha512-1ca71Zgiu6ORjHqFBDpnSMTR2ReToX4l1Au1VFLyVeBTFavzQnv5JxMFr3ukHVKpSrSA2MCk0lNJSykjUfz7Zg==}
+ engines: {node: '>=10.13.0'}
+ hasBin: true
+
+ queue-microtask@1.2.3:
+ resolution: {integrity: sha512-NuaNSa6flKT5JaSYQzJok04JzTL1CA6aGhv5rfLW3PgqA+M2ChpZQnAC8h8i4ZFkBS8X5RqkDBHA7r4hej3K9A==}
+
+ readdirp@4.1.2:
+ resolution: {integrity: sha512-GDhwkLfywWL2s6vEjyhri+eXmfH6j1L7JE27WhqLeYzoh/A3DBaYGEj2H/HFZCn/kMfim73FXxEJTw06WtxQwg==}
+ engines: {node: '>= 14.18.0'}
+
+ readdirp@5.0.0:
+ resolution: {integrity: sha512-9u/XQ1pvrQtYyMpZe7DXKv2p5CNvyVwzUB6uhLAnQwHMSgKMBR62lc7AHljaeteeHXn11XTAaLLUVZYVZyuRBQ==}
+ engines: {node: '>= 20.19.0'}
+
+ regex-recursion@6.0.2:
+ resolution: {integrity: sha512-0YCaSCq2VRIebiaUviZNs0cBz1kg5kVS2UKUfNIx8YVs1cN3AV7NTctO5FOKBA+UT2BPJIWZauYHPqJODG50cg==}
+
+ regex-utilities@2.3.0:
+ resolution: {integrity: sha512-8VhliFJAWRaUiVvREIiW2NXXTmHs4vMNnSzuJVhscgmGav3g9VDxLrQndI3dZZVVdp0ZO/5v0xmX516/7M9cng==}
+
+ regex@6.1.0:
+ resolution: {integrity: sha512-6VwtthbV4o/7+OaAF9I5L5V3llLEsoPyq9P1JVXkedTP33c7MfCG0/5NOPcSJn0TzXcG9YUrR0gQSWioew3LDg==}
+
+ rehype-parse@9.0.1:
+ resolution: {integrity: sha512-ksCzCD0Fgfh7trPDxr2rSylbwq9iYDkSn8TCDmEJ49ljEUBxDVCzCHv7QNzZOfODanX4+bWQ4WZqLCRWYLfhag==}
+
+ rehype-sanitize@6.0.0:
+ resolution: {integrity: sha512-CsnhKNsyI8Tub6L4sm5ZFsme4puGfc6pYylvXo1AeqaGbjOYyzNv3qZPwvs0oMJ39eryyeOdmxwUIo94IpEhqg==}
+
+ rehype-stringify@10.0.1:
+ resolution: {integrity: sha512-k9ecfXHmIPuFVI61B9DeLPN0qFHfawM6RsuX48hoqlaKSF61RskNjSm1lI8PhBEM0MRdLxVVm4WmTqJQccH9mA==}
+
+ require-directory@2.1.1:
+ resolution: {integrity: sha512-fGxEI7+wsG9xrvdjsrlmL22OMTTiHRwAMroiEeMgq8gzoLC/PQr7RsRDSTLUg/bZAZtF+TVIkHc6/4RIKrui+Q==}
+ engines: {node: '>=0.10.0'}
+
+ require-main-filename@2.0.0:
+ resolution: {integrity: sha512-NKN5kMDylKuldxYLSUfrbo5Tuzh4hd+2E8NPPX02mZtn1VuREQToYe/ZdlJy+J3uCpfaiGF05e7B8W0iXbQHmg==}
+
+ restore-cursor@5.1.0:
+ resolution: {integrity: sha512-oMA2dcrw6u0YfxJQXm342bFKX/E4sG9rbTzO9ptUcR/e8A33cHuvStiYOwH7fszkZlZ1z/ta9AAoPk2F4qIOHA==}
+ engines: {node: '>=18'}
+
+ reusify@1.1.0:
+ resolution: {integrity: sha512-g6QUff04oZpHs0eG5p83rFLhHeV00ug/Yf9nZM6fLeUrPguBTkTQOdpAWWspMh55TZfVQDPaN3NQJfbVRAxdIw==}
+ engines: {iojs: '>=1.0.0', node: '>=0.10.0'}
+
+ rfdc@1.4.1:
+ resolution: {integrity: sha512-q1b3N5QkRUWUl7iyylaaj3kOpIT0N2i9MqIEQXP73GVsN9cw3fdx8X63cEmWhJGi2PPCF23Ijp7ktmd39rawIA==}
+
+ robust-predicates@3.0.2:
+ resolution: {integrity: sha512-IXgzBWvWQwE6PrDI05OvmXUIruQTcoMDzRsOd5CDvHCVLcLHMTSYvOK5Cm46kWqlV3yAbuSpBZdJ5oP5OUoStg==}
+
+ rollup@4.55.1:
+ resolution: {integrity: sha512-wDv/Ht1BNHB4upNbK74s9usvl7hObDnvVzknxqY/E/O3X6rW1U1rV1aENEfJ54eFZDTNo7zv1f5N4edCluH7+A==}
+ engines: {node: '>=18.0.0', npm: '>=8.0.0'}
+ hasBin: true
+
+ roughjs@4.6.6:
+ resolution: {integrity: sha512-ZUz/69+SYpFN/g/lUlo2FXcIjRkSu3nDarreVdGGndHEBJ6cXPdKguS8JGxwj5HA5xIbVKSmLgr5b3AWxtRfvQ==}
+
+ run-parallel@1.2.0:
+ resolution: {integrity: sha512-5l4VyZR86LZ/lDxZTR6jqL8AFE2S0IFLMP26AbjsLVADxHdhB/c0GUsH+y39UfCi3dzz8OlQuPmnaJOMoDHQBA==}
+
+ rw@1.3.3:
+ resolution: {integrity: sha512-PdhdWy89SiZogBLaw42zdeqtRJ//zFd2PgQavcICDUgJT5oW10QCRKbJ6bg4r0/UY2M6BWd5tkxuGFRvCkgfHQ==}
+
+ rxjs@7.8.2:
+ resolution: {integrity: sha512-dhKf903U/PQZY6boNNtAGdWbG85WAbjT/1xYoZIC7FAY0yWapOBQVsVrDl58W86//e1VpMNBtRV4MaXfdMySFA==}
+
+ safer-buffer@2.1.2:
+ resolution: {integrity: sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==}
+
+ sass-embedded-all-unknown@1.97.2:
+ resolution: {integrity: sha512-Fj75+vOIDv1T/dGDwEpQ5hgjXxa2SmMeShPa8yrh2sUz1U44bbmY4YSWPCdg8wb7LnwiY21B2KRFM+HF42yO4g==}
+ cpu: ['!arm', '!arm64', '!riscv64', '!x64']
+
+ sass-embedded-android-arm64@1.97.2:
+ resolution: {integrity: sha512-pF6I+R5uThrscd3lo9B3DyNTPyGFsopycdx0tDAESN6s+dBbiRgNgE4Zlpv50GsLocj/lDLCZaabeTpL3ubhYA==}
+ engines: {node: '>=14.0.0'}
+ cpu: [arm64]
+ os: [android]
+
+ sass-embedded-android-arm@1.97.2:
+ resolution: {integrity: sha512-BPT9m19ttY0QVHYYXRa6bmqmS3Fa2EHByNUEtSVcbm5PkIk1ntmYkG9fn5SJpIMbNmFDGwHx+pfcZMmkldhnRg==}
+ engines: {node: '>=14.0.0'}
+ cpu: [arm]
+ os: [android]
+
+ sass-embedded-android-riscv64@1.97.2:
+ resolution: {integrity: sha512-fprI8ZTJdz+STgARhg8zReI2QhhGIT9G8nS7H21kc3IkqPRzhfaemSxEtCqZyvDbXPcgYiDLV7AGIReHCuATog==}
+ engines: {node: '>=14.0.0'}
+ cpu: [riscv64]
+ os: [android]
+
+ sass-embedded-android-x64@1.97.2:
+ resolution: {integrity: sha512-RswwSjURZxupsukEmNt2t6RGvuvIw3IAD5sDq1Pc65JFvWFY3eHqCmH0lG0oXqMg6KJcF0eOxHOp2RfmIm2+4w==}
+ engines: {node: '>=14.0.0'}
+ cpu: [x64]
+ os: [android]
+
+ sass-embedded-darwin-arm64@1.97.2:
+ resolution: {integrity: sha512-xcsZNnU1XZh21RE/71OOwNqPVcGBU0qT9A4k4QirdA34+ts9cDIaR6W6lgHOBR/Bnnu6w6hXJR4Xth7oFrefPA==}
+ engines: {node: '>=14.0.0'}
+ cpu: [arm64]
+ os: [darwin]
+
+ sass-embedded-darwin-x64@1.97.2:
+ resolution: {integrity: sha512-T/9DTMpychm6+H4slHCAsYJRJ6eM+9H9idKlBPliPrP4T8JdC2Cs+ZOsYqrObj6eOtAD0fGf+KgyNhnW3xVafA==}
+ engines: {node: '>=14.0.0'}
+ cpu: [x64]
+ os: [darwin]
+
+ sass-embedded-linux-arm64@1.97.2:
+ resolution: {integrity: sha512-Wh+nQaFer9tyE5xBPv5murSUZE/+kIcg8MyL5uqww6be9Iq+UmZpcJM7LUk+q8klQ9LfTmoDSNFA74uBqxD6IA==}
+ engines: {node: '>=14.0.0'}
+ cpu: [arm64]
+ os: [linux]
+ libc: glibc
+
+ sass-embedded-linux-arm@1.97.2:
+ resolution: {integrity: sha512-yDRe1yifGHl6kibkDlRIJ2ZzAU03KJ1AIvsAh4dsIDgK5jx83bxZLV1ZDUv7a8KK/iV/80LZnxnu/92zp99cXQ==}
+ engines: {node: '>=14.0.0'}
+ cpu: [arm]
+ os: [linux]
+ libc: glibc
+
+ sass-embedded-linux-musl-arm64@1.97.2:
+ resolution: {integrity: sha512-NfUqZSjHwnHvpSa7nyNxbWfL5obDjNBqhHUYmqbHUcmqBpFfHIQsUPgXME9DKn1yBlBc3mWnzMxRoucdYTzd2Q==}
+ engines: {node: '>=14.0.0'}
+ cpu: [arm64]
+ os: [linux]
+ libc: musl
+
+ sass-embedded-linux-musl-arm@1.97.2:
+ resolution: {integrity: sha512-GIO6xfAtahJAWItvsXZ3MD1HM6s8cKtV1/HL088aUpKJaw/2XjTCveiOO2AdgMpLNztmq9DZ1lx5X5JjqhS45g==}
+ engines: {node: '>=14.0.0'}
+ cpu: [arm]
+ os: [linux]
+ libc: musl
+
+ sass-embedded-linux-musl-riscv64@1.97.2:
+ resolution: {integrity: sha512-qtM4dJ5gLfvyTZ3QencfNbsTEShIWImSEpkThz+Y2nsCMbcMP7/jYOA03UWgPfEOKSehQQ7EIau7ncbFNoDNPQ==}
+ engines: {node: '>=14.0.0'}
+ cpu: [riscv64]
+ os: [linux]
+ libc: musl
+
+ sass-embedded-linux-musl-x64@1.97.2:
+ resolution: {integrity: sha512-ZAxYOdmexcnxGnzdsDjYmNe3jGj+XW3/pF/n7e7r8y+5c6D2CQRrCUdapLgaqPt1edOPQIlQEZF8q5j6ng21yw==}
+ engines: {node: '>=14.0.0'}
+ cpu: [x64]
+ os: [linux]
+ libc: musl
+
+ sass-embedded-linux-riscv64@1.97.2:
+ resolution: {integrity: sha512-reVwa9ZFEAOChXpDyNB3nNHHyAkPMD+FTctQKECqKiVJnIzv2EaFF6/t0wzyvPgBKeatA8jszAIeOkkOzbYVkQ==}
+ engines: {node: '>=14.0.0'}
+ cpu: [riscv64]
+ os: [linux]
+ libc: glibc
+
+ sass-embedded-linux-x64@1.97.2:
+ resolution: {integrity: sha512-bvAdZQsX3jDBv6m4emaU2OMTpN0KndzTAMgJZZrKUgiC0qxBmBqbJG06Oj/lOCoXGCxAvUOheVYpezRTF+Feog==}
+ engines: {node: '>=14.0.0'}
+ cpu: [x64]
+ os: [linux]
+ libc: glibc
+
+ sass-embedded-unknown-all@1.97.2:
+ resolution: {integrity: sha512-86tcYwohjPgSZtgeU9K4LikrKBJNf8ZW/vfsFbdzsRlvc73IykiqanufwQi5qIul0YHuu9lZtDWyWxM2dH/Rsg==}
+ os: ['!android', '!darwin', '!linux', '!win32']
+
+ sass-embedded-win32-arm64@1.97.2:
+ resolution: {integrity: sha512-Cv28q8qNjAjZfqfzTrQvKf4JjsZ6EOQ5FxyHUQQeNzm73R86nd/8ozDa1Vmn79Hq0kwM15OCM9epanDuTG1ksA==}
+ engines: {node: '>=14.0.0'}
+ cpu: [arm64]
+ os: [win32]
+
+ sass-embedded-win32-x64@1.97.2:
+ resolution: {integrity: sha512-DVxLxkeDCGIYeyHLAvWW3yy9sy5Ruk5p472QWiyfyyG1G1ASAR8fgfIY5pT0vE6Rv+VAKVLwF3WTspUYu7S1/Q==}
+ engines: {node: '>=14.0.0'}
+ cpu: [x64]
+ os: [win32]
+
+ sass-embedded@1.97.2:
+ resolution: {integrity: sha512-lKJcskySwAtJ4QRirKrikrWMFa2niAuaGenY2ElHjd55IwHUiur5IdKu6R1hEmGYMs4Qm+6rlRW0RvuAkmcryg==}
+ engines: {node: '>=16.0.0'}
+ hasBin: true
+
+ sass@1.97.2:
+ resolution: {integrity: sha512-y5LWb0IlbO4e97Zr7c3mlpabcbBtS+ieiZ9iwDooShpFKWXf62zz5pEPdwrLYm+Bxn1fnbwFGzHuCLSA9tBmrw==}
+ engines: {node: '>=14.0.0'}
+ hasBin: true
+
+ sax@1.4.4:
+ resolution: {integrity: sha512-1n3r/tGXO6b6VXMdFT54SHzT9ytu9yr7TaELowdYpMqY/Ao7EnlQGmAQ1+RatX7Tkkdm6hONI2owqNx2aZj5Sw==}
+ engines: {node: '>=11.0.0'}
+
+ section-matter@1.0.0:
+ resolution: {integrity: sha512-vfD3pmTzGpufjScBh50YHKzEu2lxBWhVEHsNGoEXmCmn2hKGfeNLYMzCJpe8cD7gqX7TJluOVpBkAequ6dgMmA==}
+ engines: {node: '>=4'}
+
+ set-blocking@2.0.0:
+ resolution: {integrity: sha512-KiKBS8AnWGEyLzofFfmvKwpdPzqiy16LvQfK3yv/fVH7Bj13/wl3JSR1J+rfgRE9q7xUJK4qvgS8raSOeLUehw==}
+
+ shiki@3.21.0:
+ resolution: {integrity: sha512-N65B/3bqL/TI2crrXr+4UivctrAGEjmsib5rPMMPpFp1xAx/w03v8WZ9RDDFYteXoEgY7qZ4HGgl5KBIu1153w==}
+
+ signal-exit@4.1.0:
+ resolution: {integrity: sha512-bzyZ1e88w9O1iNJbKnOlvYTrWPDl46O1bG0D3XInv+9tkPrxrN8jUUTiFlDkkmKWgn1M6CfIA13SuGqOa9Korw==}
+ engines: {node: '>=14'}
+
+ sitemap@9.0.0:
+ resolution: {integrity: sha512-J/SU27FJ+I52TcDLKZzPRRVQUMj0Pp1i/HLb2lrkU+hrMLM+qdeRjdacrNxnSW48Waa3UcEOGOdX1+0Lob7TgA==}
+ engines: {node: '>=20.19.5', npm: '>=10.8.2'}
+ hasBin: true
+
+ slash@5.1.0:
+ resolution: {integrity: sha512-ZA6oR3T/pEyuqwMgAKT0/hAv8oAXckzbkmR0UkUosQ+Mc4RxGoJkRmwHgHufaenlyAgE1Mxgpdcrf75y6XcnDg==}
+ engines: {node: '>=14.16'}
+
+ source-map-js@1.2.1:
+ resolution: {integrity: sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA==}
+ engines: {node: '>=0.10.0'}
+
+ space-separated-tokens@2.0.2:
+ resolution: {integrity: sha512-PEGlAwrG8yXGXRjW32fGbg66JAlOAwbObuqVoJpv/mRgoWDQfgH1wDPvtzWyUSNAXBGSk8h755YDbbcEy3SH2Q==}
+
+ speakingurl@14.0.1:
+ resolution: {integrity: sha512-1POYv7uv2gXoyGFpBCmpDVSNV74IfsWlDW216UPjbWufNf+bSU6GdbDsxdcxtfwb4xlI3yxzOTKClUosxARYrQ==}
+ engines: {node: '>=0.10.0'}
+
+ speech-rule-engine@4.1.2:
+ resolution: {integrity: sha512-S6ji+flMEga+1QU79NDbwZ8Ivf0S/MpupQQiIC0rTpU/ZTKgcajijJJb1OcByBQDjrXCN1/DJtGz4ZJeBMPGJw==}
+ hasBin: true
+
+ sprintf-js@1.0.3:
+ resolution: {integrity: sha512-D9cPgkvLlV3t3IzL0D0YLvGA9Ahk4PcvVwUbN0dSGr1aP0Nrt4AEnTUbuGvquEC0mA64Gqt1fzirlRs5ibXx8g==}
+
+ stdin-discarder@0.2.2:
+ resolution: {integrity: sha512-UhDfHmA92YAlNnCfhmq0VeNL5bDbiZGg7sZ2IvPsXubGkiNa9EC+tUTsjBRsYUAz87btI6/1wf4XoVvQ3uRnmQ==}
+ engines: {node: '>=18'}
+
+ string-width@4.2.3:
+ resolution: {integrity: sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==}
+ engines: {node: '>=8'}
+
+ string-width@8.1.0:
+ resolution: {integrity: sha512-Kxl3KJGb/gxkaUMOjRsQ8IrXiGW75O4E3RPjFIINOVH8AMl2SQ/yWdTzWwF3FevIX9LcMAjJW+GRwAlAbTSXdg==}
+ engines: {node: '>=20'}
+
+ stringify-entities@4.0.4:
+ resolution: {integrity: sha512-IwfBptatlO+QCJUo19AqvrPNqlVMpW9YEL2LIVY+Rpv2qsjCGxaDLNRgeGsQWJhfItebuJhsGSLjaBbNSQ+ieg==}
+
+ strip-ansi@6.0.1:
+ resolution: {integrity: sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==}
+ engines: {node: '>=8'}
+
+ strip-ansi@7.1.2:
+ resolution: {integrity: sha512-gmBGslpoQJtgnMAvOVqGZpEz9dyoKTCzy2nfz/n8aIFhN/jCE/rCmcxabB6jOOHV+0WNnylOxaxBQPSvcWklhA==}
+ engines: {node: '>=12'}
+
+ strip-bom-string@1.0.0:
+ resolution: {integrity: sha512-uCC2VHvQRYu+lMh4My/sFNmF2klFymLX1wHJeXnbEJERpV/ZsVuonzerjfrGpIGF7LBVa1O7i9kjiWvJiFck8g==}
+ engines: {node: '>=0.10.0'}
+
+ stylis@4.3.6:
+ resolution: {integrity: sha512-yQ3rwFWRfwNUY7H5vpU0wfdkNSnvnJinhF9830Swlaxl03zsOjCfmX0ugac+3LtK0lYSgwL/KXc8oYL3mG4YFQ==}
+
+ superjson@2.2.6:
+ resolution: {integrity: sha512-H+ue8Zo4vJmV2nRjpx86P35lzwDT3nItnIsocgumgr0hHMQ+ZGq5vrERg9kJBo5AWGmxZDhzDo+WVIJqkB0cGA==}
+ engines: {node: '>=16'}
+
+ supports-color@8.1.1:
+ resolution: {integrity: sha512-MpUEN2OodtUzxvKQl72cUF7RQ5EiHsGvSsVG0ia9c5RbWGL2CI4C7EpPS8UTBIplnlzZiNuV56w+FuNxy3ty2Q==}
+ engines: {node: '>=10'}
+
+ sync-child-process@1.0.2:
+ resolution: {integrity: sha512-8lD+t2KrrScJ/7KXCSyfhT3/hRq78rC0wBFqNJXv3mZyn6hW2ypM05JmlSvtqRbeq6jqA94oHbxAr2vYsJ8vDA==}
+ engines: {node: '>=16.0.0'}
+
+ sync-message-port@1.1.3:
+ resolution: {integrity: sha512-GTt8rSKje5FilG+wEdfCkOcLL7LWqpMlr2c3LRuKt/YXxcJ52aGSbGBAdI4L3aaqfrBt6y711El53ItyH1NWzg==}
+ engines: {node: '>=16.0.0'}
+
+ synckit@0.11.12:
+ resolution: {integrity: sha512-Bh7QjT8/SuKUIfObSXNHNSK6WHo6J1tHCqJsuaFDP7gP0fkzSfTxI8y85JrppZ0h8l0maIgc2tfuZQ6/t3GtnQ==}
+ engines: {node: ^14.18.0 || >=16.0.0}
+
+ tinyexec@1.0.2:
+ resolution: {integrity: sha512-W/KYk+NFhkmsYpuHq5JykngiOCnxeVL8v8dFnqxSD8qEEdRfXk1SDM6JzNqcERbcGYj9tMrDQBYV9cjgnunFIg==}
+ engines: {node: '>=18'}
+
+ tinyglobby@0.2.15:
+ resolution: {integrity: sha512-j2Zq4NyQYG5XMST4cbs02Ak8iJUdxRM0XI5QyxXuZOzKOINmWurp3smXu3y5wDcJrptwpSjgXHzIQxR0omXljQ==}
+ engines: {node: '>=12.0.0'}
+
+ to-regex-range@5.0.1:
+ resolution: {integrity: sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ==}
+ engines: {node: '>=8.0'}
+
+ trim-lines@3.0.1:
+ resolution: {integrity: sha512-kRj8B+YHZCc9kQYdWfJB2/oUl9rA99qbowYYBtr4ui4mZyAQ2JpvVBd/6U2YloATfqBhBTSMhTpgBHtU0Mf3Rg==}
+
+ trough@2.2.0:
+ resolution: {integrity: sha512-tmMpK00BjZiUyVyvrBK7knerNgmgvcV/KLVyuma/SC+TQN167GrMRciANTz09+k3zW8L8t60jWO1GpfkZdjTaw==}
+
+ ts-dedent@2.2.0:
+ resolution: {integrity: sha512-q5W7tVM71e2xjHZTlgfTDoPF/SmqKG5hddq9SzR49CH2hayqRKJtQ4mtRlSxKaJlR/+9rEM+mnBHf7I2/BQcpQ==}
+ engines: {node: '>=6.10'}
+
+ tslib@2.8.1:
+ resolution: {integrity: sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==}
+
+ uc.micro@2.1.0:
+ resolution: {integrity: sha512-ARDJmphmdvUk6Glw7y9DQ2bFkKBHwQHLi2lsaH6PPmz/Ka9sFOBsBluozhDltWmnv9u/cF6Rt87znRTPV+yp/A==}
+
+ ufo@1.6.3:
+ resolution: {integrity: sha512-yDJTmhydvl5lJzBmy/hyOAA0d+aqCBuwl818haVdYCRrWV84o7YyeVm4QlVHStqNrrJSTb6jKuFAVqAFsr+K3Q==}
+
+ undici-types@7.16.0:
+ resolution: {integrity: sha512-Zz+aZWSj8LE6zoxD+xrjh4VfkIG8Ya6LvYkZqtUQGJPZjYl53ypCaUwWqo7eI0x66KBGeRo+mlBEkMSeSZ38Nw==}
+
+ undici@7.18.2:
+ resolution: {integrity: sha512-y+8YjDFzWdQlSE9N5nzKMT3g4a5UBX1HKowfdXh0uvAnTaqqwqB92Jt4UXBAeKekDs5IaDKyJFR4X1gYVCgXcw==}
+ engines: {node: '>=20.18.1'}
+
+ unicorn-magic@0.1.0:
+ resolution: {integrity: sha512-lRfVq8fE8gz6QMBuDM6a+LO3IAzTi05H6gCVaUpir2E1Rwpo4ZUog45KpNXKC/Mn3Yb9UDuHumeFTo9iV/D9FQ==}
+ engines: {node: '>=18'}
+
+ unified@11.0.5:
+ resolution: {integrity: sha512-xKvGhPWw3k84Qjh8bI3ZeJjqnyadK+GEFtazSfZv/rKeTkTjOJho6mFqh2SM96iIcZokxiOpg78GazTSg8+KHA==}
+
+ unist-util-is@6.0.1:
+ resolution: {integrity: sha512-LsiILbtBETkDz8I9p1dQ0uyRUWuaQzd/cuEeS1hoRSyW5E5XGmTzlwY1OrNzzakGowI9Dr/I8HVaw4hTtnxy8g==}
+
+ unist-util-position@5.0.0:
+ resolution: {integrity: sha512-fucsC7HjXvkB5R3kTCO7kUjRdrS0BJt3M/FPxmHMBOm8JQi2BsHAHFsy27E0EolP8rp0NzXsJ+jNPyDWvOJZPA==}
+
+ unist-util-stringify-position@4.0.0:
+ resolution: {integrity: sha512-0ASV06AAoKCDkS2+xw5RXJywruurpbC4JZSm7nr7MOt1ojAzvyyaO+UxZf18j8FCF6kmzCZKcAgN/yu2gm2XgQ==}
+
+ unist-util-visit-parents@6.0.2:
+ resolution: {integrity: sha512-goh1s1TBrqSqukSc8wrjwWhL0hiJxgA8m4kFxGlQ+8FYQ3C/m11FcTs4YYem7V664AhHVvgoQLk890Ssdsr2IQ==}
+
+ unist-util-visit@5.0.0:
+ resolution: {integrity: sha512-MR04uvD+07cwl/yhVuVWAtw+3GOR/knlL55Nd/wAdblk27GCVt3lqpTivy/tkJcZoNPzTwS1Y+KMojlLDhoTzg==}
+
+ universalify@2.0.1:
+ resolution: {integrity: sha512-gptHNQghINnc/vTGIk0SOFGFNXw7JVrlRUtConJRlvaw6DuX0wO5Jeko9sWrMBhh+PsYAZ7oXAiOnf/UKogyiw==}
+ engines: {node: '>= 10.0.0'}
+
+ upath@2.0.1:
+ resolution: {integrity: sha512-1uEe95xksV1O0CYKXo8vQvN1JEbtJp7lb7C5U9HMsIp6IVwntkH/oNUzyVNQSd4S1sYk2FpSSW44FqMc8qee5w==}
+ engines: {node: '>=4'}
+
+ update-browserslist-db@1.2.3:
+ resolution: {integrity: sha512-Js0m9cx+qOgDxo0eMiFGEueWztz+d4+M3rGlmKPT+T4IS/jP4ylw3Nwpu6cpTTP8R1MAC1kF4VbdLt3ARf209w==}
+ hasBin: true
+ peerDependencies:
+ browserslist: '>= 4.21.0'
+
+ uuid@11.1.0:
+ resolution: {integrity: sha512-0/A9rDy9P7cJ+8w1c9WD9V//9Wj15Ce2MPz8Ri6032usz+NfePxx5AcN3bN+r6ZL6jEo066/yNYB3tn4pQEx+A==}
+ hasBin: true
+
+ varint@6.0.0:
+ resolution: {integrity: sha512-cXEIW6cfr15lFv563k4GuVuW/fiwjknytD37jIOLSdSWuOI6WnO/oKwmP2FQTU2l01LP8/M5TSAJpzUaGe3uWg==}
+
+ vfile-location@5.0.3:
+ resolution: {integrity: sha512-5yXvWDEgqeiYiBe1lbxYF7UMAIm/IcopxMHrMQDq3nvKcjPKIhZklUKL+AE7J7uApI4kwe2snsK+eI6UTj9EHg==}
+
+ vfile-message@4.0.3:
+ resolution: {integrity: sha512-QTHzsGd1EhbZs4AsQ20JX1rC3cOlt/IWJruk893DfLRr57lcnOeMaWG4K0JrRta4mIJZKth2Au3mM3u03/JWKw==}
+
+ vfile@6.0.3:
+ resolution: {integrity: sha512-KzIbH/9tXat2u30jf+smMwFCsno4wHVdNmzFyL+T/L3UGqqk6JKfVqOFOZEpZSHADH1k40ab6NUIXZq422ov3Q==}
+
+ vite@7.3.1:
+ resolution: {integrity: sha512-w+N7Hifpc3gRjZ63vYBXA56dvvRlNWRczTdmCBBa+CotUzAPf5b7YMdMR/8CQoeYE5LX3W4wj6RYTgonm1b9DA==}
+ engines: {node: ^20.19.0 || >=22.12.0}
+ hasBin: true
+ peerDependencies:
+ '@types/node': ^20.19.0 || >=22.12.0
+ jiti: '>=1.21.0'
+ less: ^4.0.0
+ lightningcss: ^1.21.0
+ sass: ^1.70.0
+ sass-embedded: ^1.70.0
+ stylus: '>=0.54.8'
+ sugarss: ^5.0.0
+ terser: ^5.16.0
+ tsx: ^4.8.1
+ yaml: ^2.4.2
+ peerDependenciesMeta:
+ '@types/node':
+ optional: true
+ jiti:
+ optional: true
+ less:
+ optional: true
+ lightningcss:
+ optional: true
+ sass:
+ optional: true
+ sass-embedded:
+ optional: true
+ stylus:
+ optional: true
+ sugarss:
+ optional: true
+ terser:
+ optional: true
+ tsx:
+ optional: true
+ yaml:
+ optional: true
+
+ vscode-jsonrpc@8.2.0:
+ resolution: {integrity: sha512-C+r0eKJUIfiDIfwJhria30+TYWPtuHJXHtI7J0YlOmKAo7ogxP20T0zxB7HZQIFhIyvoBPwWskjxrvAtfjyZfA==}
+ engines: {node: '>=14.0.0'}
+
+ vscode-languageserver-protocol@3.17.5:
+ resolution: {integrity: sha512-mb1bvRJN8SVznADSGWM9u/b07H7Ecg0I3OgXDuLdn307rl/J3A9YD6/eYOssqhecL27hK1IPZAsaqh00i/Jljg==}
+
+ vscode-languageserver-textdocument@1.0.12:
+ resolution: {integrity: sha512-cxWNPesCnQCcMPeenjKKsOCKQZ/L6Tv19DTRIGuLWe32lyzWhihGVJ/rcckZXJxfdKCFvRLS3fpBIsV/ZGX4zA==}
+
+ vscode-languageserver-types@3.17.5:
+ resolution: {integrity: sha512-Ld1VelNuX9pdF39h2Hgaeb5hEZM2Z3jUrrMgWQAu82jMtZp7p3vJT3BzToKtZI7NgQssZje5o0zryOrhQvzQAg==}
+
+ vscode-languageserver@9.0.1:
+ resolution: {integrity: sha512-woByF3PDpkHFUreUa7Hos7+pUWdeWMXRd26+ZX2A8cFx6v/JPTtd4/uN0/jB6XQHYaOlHbio03NTHCqrgG5n7g==}
+ hasBin: true
+
+ vscode-uri@3.0.8:
+ resolution: {integrity: sha512-AyFQ0EVmsOZOlAnxoFOGOq1SQDWAB7C6aqMGS23svWAllfOaxbuFvcT8D1i8z3Gyn8fraVeZNNmN6e9bxxXkKw==}
+
+ vue-router@4.6.4:
+ resolution: {integrity: sha512-Hz9q5sa33Yhduglwz6g9skT8OBPii+4bFn88w6J+J4MfEo4KRRpmiNG/hHHkdbRFlLBOqxN8y8gf2Fb0MTUgVg==}
+ peerDependencies:
+ vue: ^3.5.0
+
+ vue@3.5.26:
+ resolution: {integrity: sha512-SJ/NTccVyAoNUJmkM9KUqPcYlY+u8OVL1X5EW9RIs3ch5H2uERxyyIUI4MRxVCSOiEcupX9xNGde1tL9ZKpimA==}
+ peerDependencies:
+ typescript: '*'
+ peerDependenciesMeta:
+ typescript:
+ optional: true
+
+ vuepress-plugin-components@2.0.0-rc.102:
+ resolution: {integrity: sha512-OXktm4WpjE2rfja7kA+rSw/meqrDrUECuXlzJyR1ZQ3ft3kSTU+tsW6+KqsTbsKRajNQsu6r0VeRCaLujQQaFw==}
+ engines: {node: '>=20.19.0', npm: '>=8', pnpm: '>=7', yarn: '>=2'}
+ peerDependencies:
+ artplayer: ^5.0.0
+ dashjs: 4.7.4
+ hls.js: ^1.4.12
+ mpegts.js: ^1.7.3
+ sass: ^1.97.1
+ sass-embedded: ^1.97.1
+ sass-loader: ^16.0.6
+ vidstack: ^1.12.9
+ vuepress: 2.0.0-rc.26
+ peerDependenciesMeta:
+ artplayer:
+ optional: true
+ dashjs:
+ optional: true
+ hls.js:
+ optional: true
+ mpegts.js:
+ optional: true
+ sass:
+ optional: true
+ sass-embedded:
+ optional: true
+ sass-loader:
+ optional: true
+ vidstack:
+ optional: true
+
+ vuepress-plugin-md-enhance@2.0.0-rc.102:
+ resolution: {integrity: sha512-UluC0p39wpBQWrvjiwQSbiHHIl63uOwRQSAtqLbRjm5MRvlPYPPbqwfCwbTqQkt+yKjKZY/JuW81EcbSGbHkNg==}
+ engines: {node: '>= 20.19.0', npm: '>=8', pnpm: '>=7', yarn: '>=2'}
+ peerDependencies:
+ '@vue/repl': ^4.1.1
+ kotlin-playground: ^1.23.0
+ sandpack-vue3: ^3.0.0
+ sass: ^1.97.1
+ sass-embedded: ^1.97.1
+ sass-loader: ^16.0.6
+ vuepress: 2.0.0-rc.26
+ peerDependenciesMeta:
+ '@vue/repl':
+ optional: true
+ kotlin-playground:
+ optional: true
+ sandpack-vue3:
+ optional: true
+ sass:
+ optional: true
+ sass-embedded:
+ optional: true
+ sass-loader:
+ optional: true
+
+ vuepress-shared@2.0.0-rc.99:
+ resolution: {integrity: sha512-ErCf4m4eMn/0K8NqyhD8cqmkxM7ZtsHBr2iBUvfBdgHkl2iS/Higbr4Pc+ekOW160ahxlOS63b1fl+z+YA/zxA==}
+ engines: {node: '>= 20.19.0', npm: '>=8', pnpm: '>=7', yarn: '>=2'}
+ peerDependencies:
+ vuepress: 2.0.0-rc.26
+
+ vuepress-theme-hope@2.0.0-rc.102:
+ resolution: {integrity: sha512-VrUdxNGdXD34RRmAvaQybf+TNdD7uXr/71tZLNHQID607sj9IlMfz77/ySBnNrFTQIteGyWfVHvsuj1tU2XxGg==}
+ engines: {node: '>= 20.19.0', npm: '>=8', pnpm: '>=7', yarn: '>=2'}
+ peerDependencies:
+ '@vuepress/plugin-docsearch': 2.0.0-rc.121
+ '@vuepress/plugin-feed': 2.0.0-rc.121
+ '@vuepress/plugin-meilisearch': 2.0.0-rc.121
+ '@vuepress/plugin-prismjs': 2.0.0-rc.121
+ '@vuepress/plugin-pwa': 2.0.0-rc.121
+ '@vuepress/plugin-revealjs': 2.0.0-rc.121
+ '@vuepress/plugin-search': 2.0.0-rc.121
+ '@vuepress/plugin-slimsearch': 2.0.0-rc.121
+ '@vuepress/plugin-watermark': 2.0.0-rc.121
+ '@vuepress/shiki-twoslash': 2.0.0-rc.121
+ sass: ^1.97.1
+ sass-embedded: ^1.97.1
+ sass-loader: ^16.0.6
+ vuepress: 2.0.0-rc.26
+ peerDependenciesMeta:
+ '@vuepress/plugin-docsearch':
+ optional: true
+ '@vuepress/plugin-feed':
+ optional: true
+ '@vuepress/plugin-meilisearch':
+ optional: true
+ '@vuepress/plugin-prismjs':
+ optional: true
+ '@vuepress/plugin-pwa':
+ optional: true
+ '@vuepress/plugin-revealjs':
+ optional: true
+ '@vuepress/plugin-search':
+ optional: true
+ '@vuepress/plugin-slimsearch':
+ optional: true
+ '@vuepress/plugin-watermark':
+ optional: true
+ '@vuepress/shiki-twoslash':
+ optional: true
+ sass:
+ optional: true
+ sass-embedded:
+ optional: true
+ sass-loader:
+ optional: true
+
+ vuepress@2.0.0-rc.26:
+ resolution: {integrity: sha512-ztTS3m6Q2MAb6D26vM2UyU5nOuxIhIk37SSD3jTcKI00x4ha0FcwY3Cm0MAt6w58REBmkwNLPxN5iiulatHtbw==}
+ engines: {node: ^20.9.0 || >=22.0.0}
+ hasBin: true
+ peerDependencies:
+ '@vuepress/bundler-vite': 2.0.0-rc.26
+ '@vuepress/bundler-webpack': 2.0.0-rc.26
+ vue: ^3.5.22
+ peerDependenciesMeta:
+ '@vuepress/bundler-vite':
+ optional: true
+ '@vuepress/bundler-webpack':
+ optional: true
+
+ web-namespaces@2.0.1:
+ resolution: {integrity: sha512-bKr1DkiNa2krS7qxNtdrtHAmzuYGFQLiQ13TsorsdT6ULTkPLKuu5+GsFpDlg6JFjUTwX2DyhMPG2be8uPrqsQ==}
+
+ whatwg-encoding@3.1.1:
+ resolution: {integrity: sha512-6qN4hJdMwfYBtE3YBTTHhoeuUrDBPZmbQaxWAqSALV/MeEnR5z1xd8UKud2RAkFoPkmB+hli1TZSnyi84xz1vQ==}
+ engines: {node: '>=18'}
+
+ whatwg-mimetype@4.0.0:
+ resolution: {integrity: sha512-QaKxh0eNIi2mE9p2vEdzfagOKHCcj1pJ56EEHGQOVxp8r9/iszLUUV7v89x9O1p/T+NlTM5W7jW6+cz4Fq1YVg==}
+ engines: {node: '>=18'}
+
+ which-module@2.0.1:
+ resolution: {integrity: sha512-iBdZ57RDvnOR9AGBhML2vFZf7h8vmBjhoaZqODJBFWHVtKkDmKuHai3cx5PgVMrX5YDNp27AofYbAwctSS+vhQ==}
+
+ wicked-good-xpath@1.3.0:
+ resolution: {integrity: sha512-Gd9+TUn5nXdwj/hFsPVx5cuHHiF5Bwuc30jZ4+ronF1qHK5O7HD0sgmXWSEgwKquT3ClLoKPVbO6qGwVwLzvAw==}
+
+ wrap-ansi@6.2.0:
+ resolution: {integrity: sha512-r6lPcBGxZXlIcymEu7InxDMhdW0KDxpLgoFLcguasxCaJ/SOIZwINatK9KY/tf+ZrlywOKU0UDj3ATXUBfxJXA==}
+ engines: {node: '>=8'}
+
+ xml-js@1.6.11:
+ resolution: {integrity: sha512-7rVi2KMfwfWFl+GpPg6m80IVMWXLRjO+PxTq7V2CDhoGak0wzYzFgUY2m4XJ47OGdXd8eLE8EmwfAmdjw7lC1g==}
+ hasBin: true
+
+ y18n@4.0.3:
+ resolution: {integrity: sha512-JKhqTOwSrqNA1NY5lSztJ1GrBiUodLMmIZuLiDaMRJ+itFd+ABVE8XBjOvIWL+rSqNDC74LCSFmlb/U4UZ4hJQ==}
+
+ yargs-parser@18.1.3:
+ resolution: {integrity: sha512-o50j0JeToy/4K6OZcaQmW6lyXXKhq7csREXcDwk2omFPJEwUNOVtJKvmDr9EI1fAJZUyZcRF7kxGBWmRXudrCQ==}
+ engines: {node: '>=6'}
+
+ yargs@15.4.1:
+ resolution: {integrity: sha512-aePbxDmcYW++PaqBsJ+HYUFwCdv4LVvdnhBy78E57PIor8/OVvhMrADFFEDh8DHDFRv/O9i3lPhsENjO7QX0+A==}
+ engines: {node: '>=8'}
+
+ yoctocolors@2.1.2:
+ resolution: {integrity: sha512-CzhO+pFNo8ajLM2d2IW/R93ipy99LWjtwblvC1RsoSUMZgyLbYFr221TnSNT7GjGdYui6P459mw9JH/g/zW2ug==}
+ engines: {node: '>=18'}
+
+ zwitch@2.0.4:
+ resolution: {integrity: sha512-bXE4cR/kVZhKZX/RjPEflHaKVhUVl85noU3v6b8apfQEc1x4A+zBxjZ4lN8LqGd6WZ3dl98pY4o717VFmoPp+A==}
+
+snapshots:
+
+ '@antfu/install-pkg@1.1.0':
+ dependencies:
+ package-manager-detector: 1.6.0
+ tinyexec: 1.0.2
+
+ '@babel/helper-string-parser@7.27.1': {}
+
+ '@babel/helper-validator-identifier@7.28.5': {}
+
+ '@babel/parser@7.28.6':
+ dependencies:
+ '@babel/types': 7.28.6
+
+ '@babel/types@7.28.6':
+ dependencies:
+ '@babel/helper-string-parser': 7.27.1
+ '@babel/helper-validator-identifier': 7.28.5
+
+ '@braintree/sanitize-url@7.1.1': {}
+
+ '@bufbuild/protobuf@2.10.2': {}
+
+ '@chevrotain/cst-dts-gen@11.0.3':
+ dependencies:
+ '@chevrotain/gast': 11.0.3
+ '@chevrotain/types': 11.0.3
+ lodash-es: 4.17.21
+
+ '@chevrotain/gast@11.0.3':
+ dependencies:
+ '@chevrotain/types': 11.0.3
+ lodash-es: 4.17.21
+
+ '@chevrotain/regexp-to-ast@11.0.3': {}
+
+ '@chevrotain/types@11.0.3': {}
+
+ '@chevrotain/utils@11.0.3': {}
+
+ '@esbuild/aix-ppc64@0.25.12':
+ optional: true
+
+ '@esbuild/aix-ppc64@0.27.2':
+ optional: true
+
+ '@esbuild/android-arm64@0.25.12':
+ optional: true
+
+ '@esbuild/android-arm64@0.27.2':
+ optional: true
+
+ '@esbuild/android-arm@0.25.12':
+ optional: true
+
+ '@esbuild/android-arm@0.27.2':
+ optional: true
+
+ '@esbuild/android-x64@0.25.12':
+ optional: true
+
+ '@esbuild/android-x64@0.27.2':
+ optional: true
+
+ '@esbuild/darwin-arm64@0.25.12':
+ optional: true
+
+ '@esbuild/darwin-arm64@0.27.2':
+ optional: true
+
+ '@esbuild/darwin-x64@0.25.12':
+ optional: true
+
+ '@esbuild/darwin-x64@0.27.2':
+ optional: true
+
+ '@esbuild/freebsd-arm64@0.25.12':
+ optional: true
+
+ '@esbuild/freebsd-arm64@0.27.2':
+ optional: true
+
+ '@esbuild/freebsd-x64@0.25.12':
+ optional: true
+
+ '@esbuild/freebsd-x64@0.27.2':
+ optional: true
+
+ '@esbuild/linux-arm64@0.25.12':
+ optional: true
+
+ '@esbuild/linux-arm64@0.27.2':
+ optional: true
+
+ '@esbuild/linux-arm@0.25.12':
+ optional: true
+
+ '@esbuild/linux-arm@0.27.2':
+ optional: true
+
+ '@esbuild/linux-ia32@0.25.12':
+ optional: true
+
+ '@esbuild/linux-ia32@0.27.2':
+ optional: true
+
+ '@esbuild/linux-loong64@0.25.12':
+ optional: true
+
+ '@esbuild/linux-loong64@0.27.2':
+ optional: true
+
+ '@esbuild/linux-mips64el@0.25.12':
+ optional: true
+
+ '@esbuild/linux-mips64el@0.27.2':
+ optional: true
+
+ '@esbuild/linux-ppc64@0.25.12':
+ optional: true
+
+ '@esbuild/linux-ppc64@0.27.2':
+ optional: true
+
+ '@esbuild/linux-riscv64@0.25.12':
+ optional: true
+
+ '@esbuild/linux-riscv64@0.27.2':
+ optional: true
+
+ '@esbuild/linux-s390x@0.25.12':
+ optional: true
+
+ '@esbuild/linux-s390x@0.27.2':
+ optional: true
+
+ '@esbuild/linux-x64@0.25.12':
+ optional: true
+
+ '@esbuild/linux-x64@0.27.2':
+ optional: true
+
+ '@esbuild/netbsd-arm64@0.25.12':
+ optional: true
+
+ '@esbuild/netbsd-arm64@0.27.2':
+ optional: true
+
+ '@esbuild/netbsd-x64@0.25.12':
+ optional: true
+
+ '@esbuild/netbsd-x64@0.27.2':
+ optional: true
+
+ '@esbuild/openbsd-arm64@0.25.12':
+ optional: true
+
+ '@esbuild/openbsd-arm64@0.27.2':
+ optional: true
+
+ '@esbuild/openbsd-x64@0.25.12':
+ optional: true
+
+ '@esbuild/openbsd-x64@0.27.2':
+ optional: true
+
+ '@esbuild/openharmony-arm64@0.25.12':
+ optional: true
+
+ '@esbuild/openharmony-arm64@0.27.2':
+ optional: true
+
+ '@esbuild/sunos-x64@0.25.12':
+ optional: true
+
+ '@esbuild/sunos-x64@0.27.2':
+ optional: true
+
+ '@esbuild/win32-arm64@0.25.12':
+ optional: true
+
+ '@esbuild/win32-arm64@0.27.2':
+ optional: true
+
+ '@esbuild/win32-ia32@0.25.12':
+ optional: true
+
+ '@esbuild/win32-ia32@0.27.2':
+ optional: true
+
+ '@esbuild/win32-x64@0.25.12':
+ optional: true
+
+ '@esbuild/win32-x64@0.27.2':
+ optional: true
+
+ '@iconify/types@2.0.0': {}
+
+ '@iconify/utils@3.1.0':
+ dependencies:
+ '@antfu/install-pkg': 1.1.0
+ '@iconify/types': 2.0.0
+ mlly: 1.8.0
+
+ '@jridgewell/sourcemap-codec@1.5.5': {}
+
+ '@lit-labs/ssr-dom-shim@1.5.1': {}
+
+ '@lit/reactive-element@2.1.2':
+ dependencies:
+ '@lit-labs/ssr-dom-shim': 1.5.1
+
+ '@mdit-vue/plugin-component@3.0.2':
+ dependencies:
+ '@types/markdown-it': 14.1.2
+ markdown-it: 14.1.0
+
+ '@mdit-vue/plugin-frontmatter@3.0.2':
+ dependencies:
+ '@mdit-vue/types': 3.0.2
+ '@types/markdown-it': 14.1.2
+ gray-matter: 4.0.3
+ markdown-it: 14.1.0
+
+ '@mdit-vue/plugin-headers@3.0.2':
+ dependencies:
+ '@mdit-vue/shared': 3.0.2
+ '@mdit-vue/types': 3.0.2
+ '@types/markdown-it': 14.1.2
+ markdown-it: 14.1.0
+
+ '@mdit-vue/plugin-sfc@3.0.2':
+ dependencies:
+ '@mdit-vue/types': 3.0.2
+ '@types/markdown-it': 14.1.2
+ markdown-it: 14.1.0
+
+ '@mdit-vue/plugin-title@3.0.2':
+ dependencies:
+ '@mdit-vue/shared': 3.0.2
+ '@mdit-vue/types': 3.0.2
+ '@types/markdown-it': 14.1.2
+ markdown-it: 14.1.0
+
+ '@mdit-vue/plugin-toc@3.0.2':
+ dependencies:
+ '@mdit-vue/shared': 3.0.2
+ '@mdit-vue/types': 3.0.2
+ '@types/markdown-it': 14.1.2
+ markdown-it: 14.1.0
+
+ '@mdit-vue/shared@3.0.2':
+ dependencies:
+ '@mdit-vue/types': 3.0.2
+ '@types/markdown-it': 14.1.2
+ markdown-it: 14.1.0
+
+ '@mdit-vue/types@3.0.2': {}
+
+ '@mdit/helper@0.22.1(markdown-it@14.1.0)':
+ dependencies:
+ '@types/markdown-it': 14.1.2
+ optionalDependencies:
+ markdown-it: 14.1.0
+
+ '@mdit/plugin-alert@0.22.3(markdown-it@14.1.0)':
+ dependencies:
+ '@types/markdown-it': 14.1.2
+ optionalDependencies:
+ markdown-it: 14.1.0
+
+ '@mdit/plugin-align@0.23.0(markdown-it@14.1.0)':
+ dependencies:
+ '@mdit/plugin-container': 0.22.2(markdown-it@14.1.0)
+ '@types/markdown-it': 14.1.2
+ optionalDependencies:
+ markdown-it: 14.1.0
+
+ '@mdit/plugin-attrs@0.24.1(markdown-it@14.1.0)':
+ dependencies:
+ '@mdit/helper': 0.22.1(markdown-it@14.1.0)
+ '@types/markdown-it': 14.1.2
+ optionalDependencies:
+ markdown-it: 14.1.0
+
+ '@mdit/plugin-container@0.22.2(markdown-it@14.1.0)':
+ dependencies:
+ '@types/markdown-it': 14.1.2
+ optionalDependencies:
+ markdown-it: 14.1.0
+
+ '@mdit/plugin-demo@0.22.3(markdown-it@14.1.0)':
+ dependencies:
+ '@types/markdown-it': 14.1.2
+ optionalDependencies:
+ markdown-it: 14.1.0
+
+ '@mdit/plugin-figure@0.22.2(markdown-it@14.1.0)':
+ dependencies:
+ '@types/markdown-it': 14.1.2
+ optionalDependencies:
+ markdown-it: 14.1.0
+
+ '@mdit/plugin-footnote@0.22.3(markdown-it@14.1.0)':
+ dependencies:
+ '@types/markdown-it': 14.1.2
+ markdown-it: 14.1.0
+
+ '@mdit/plugin-icon@0.23.0(markdown-it@14.1.0)':
+ dependencies:
+ '@mdit/helper': 0.22.1(markdown-it@14.1.0)
+ '@types/markdown-it': 14.1.2
+ optionalDependencies:
+ markdown-it: 14.1.0
+
+ '@mdit/plugin-img-lazyload@0.22.1(markdown-it@14.1.0)':
+ dependencies:
+ '@types/markdown-it': 14.1.2
+ optionalDependencies:
+ markdown-it: 14.1.0
+
+ '@mdit/plugin-img-mark@0.22.2(markdown-it@14.1.0)':
+ dependencies:
+ '@types/markdown-it': 14.1.2
+ optionalDependencies:
+ markdown-it: 14.1.0
+
+ '@mdit/plugin-img-size@0.22.4(markdown-it@14.1.0)':
+ dependencies:
+ '@types/markdown-it': 14.1.2
+ optionalDependencies:
+ markdown-it: 14.1.0
+
+ '@mdit/plugin-include@0.22.3(markdown-it@14.1.0)':
+ dependencies:
+ '@mdit/helper': 0.22.1(markdown-it@14.1.0)
+ '@types/markdown-it': 14.1.2
+ upath: 2.0.1
+ optionalDependencies:
+ markdown-it: 14.1.0
+
+ '@mdit/plugin-katex-slim@0.25.1(katex@0.16.27)(markdown-it@14.1.0)':
+ dependencies:
+ '@mdit/helper': 0.22.1(markdown-it@14.1.0)
+ '@mdit/plugin-tex': 0.23.0(markdown-it@14.1.0)
+ '@types/markdown-it': 14.1.2
+ optionalDependencies:
+ katex: 0.16.27
+ markdown-it: 14.1.0
+
+ '@mdit/plugin-mark@0.22.1(markdown-it@14.1.0)':
+ dependencies:
+ '@types/markdown-it': 14.1.2
+ optionalDependencies:
+ markdown-it: 14.1.0
+
+ '@mdit/plugin-mathjax-slim@0.24.1(markdown-it@14.1.0)':
+ dependencies:
+ '@mdit/plugin-tex': 0.23.0(markdown-it@14.1.0)
+ '@types/markdown-it': 14.1.2
+ optionalDependencies:
+ markdown-it: 14.1.0
+
+ '@mdit/plugin-plantuml@0.23.0(markdown-it@14.1.0)':
+ dependencies:
+ '@mdit/plugin-uml': 0.23.0(markdown-it@14.1.0)
+ '@types/markdown-it': 14.1.2
+ optionalDependencies:
+ markdown-it: 14.1.0
+
+ '@mdit/plugin-spoiler@0.22.2(markdown-it@14.1.0)':
+ dependencies:
+ '@types/markdown-it': 14.1.2
+ optionalDependencies:
+ markdown-it: 14.1.0
+
+ '@mdit/plugin-stylize@0.22.3(markdown-it@14.1.0)':
+ dependencies:
+ '@types/markdown-it': 14.1.2
+ optionalDependencies:
+ markdown-it: 14.1.0
+
+ '@mdit/plugin-sub@0.23.0(markdown-it@14.1.0)':
+ dependencies:
+ '@mdit/helper': 0.22.1(markdown-it@14.1.0)
+ '@types/markdown-it': 14.1.2
+ optionalDependencies:
+ markdown-it: 14.1.0
+
+ '@mdit/plugin-sup@0.23.0(markdown-it@14.1.0)':
+ dependencies:
+ '@mdit/helper': 0.22.1(markdown-it@14.1.0)
+ '@types/markdown-it': 14.1.2
+ optionalDependencies:
+ markdown-it: 14.1.0
+
+ '@mdit/plugin-tab@0.23.0(markdown-it@14.1.0)':
+ dependencies:
+ '@mdit/helper': 0.22.1(markdown-it@14.1.0)
+ '@types/markdown-it': 14.1.2
+ optionalDependencies:
+ markdown-it: 14.1.0
+
+ '@mdit/plugin-tasklist@0.22.2(markdown-it@14.1.0)':
+ dependencies:
+ '@types/markdown-it': 14.1.2
+ optionalDependencies:
+ markdown-it: 14.1.0
+
+ '@mdit/plugin-tex@0.23.0(markdown-it@14.1.0)':
+ dependencies:
+ '@types/markdown-it': 14.1.2
+ optionalDependencies:
+ markdown-it: 14.1.0
+
+ '@mdit/plugin-uml@0.23.0(markdown-it@14.1.0)':
+ dependencies:
+ '@mdit/helper': 0.22.1(markdown-it@14.1.0)
+ '@types/markdown-it': 14.1.2
+ optionalDependencies:
+ markdown-it: 14.1.0
+
+ '@mermaid-js/parser@0.6.3':
+ dependencies:
+ langium: 3.3.1
+
+ '@nodelib/fs.scandir@2.1.5':
+ dependencies:
+ '@nodelib/fs.stat': 2.0.5
+ run-parallel: 1.2.0
+
+ '@nodelib/fs.stat@2.0.5': {}
+
+ '@nodelib/fs.walk@1.2.8':
+ dependencies:
+ '@nodelib/fs.scandir': 2.1.5
+ fastq: 1.20.1
+
+ '@parcel/watcher-android-arm64@2.5.4':
+ optional: true
+
+ '@parcel/watcher-darwin-arm64@2.5.4':
+ optional: true
+
+ '@parcel/watcher-darwin-x64@2.5.4':
+ optional: true
+
+ '@parcel/watcher-freebsd-x64@2.5.4':
+ optional: true
+
+ '@parcel/watcher-linux-arm-glibc@2.5.4':
+ optional: true
+
+ '@parcel/watcher-linux-arm-musl@2.5.4':
+ optional: true
+
+ '@parcel/watcher-linux-arm64-glibc@2.5.4':
+ optional: true
+
+ '@parcel/watcher-linux-arm64-musl@2.5.4':
+ optional: true
+
+ '@parcel/watcher-linux-x64-glibc@2.5.4':
+ optional: true
+
+ '@parcel/watcher-linux-x64-musl@2.5.4':
+ optional: true
+
+ '@parcel/watcher-win32-arm64@2.5.4':
+ optional: true
+
+ '@parcel/watcher-win32-ia32@2.5.4':
+ optional: true
+
+ '@parcel/watcher-win32-x64@2.5.4':
+ optional: true
+
+ '@parcel/watcher@2.5.4':
+ dependencies:
+ detect-libc: 2.1.2
+ is-glob: 4.0.3
+ node-addon-api: 7.1.1
+ picomatch: 4.0.3
+ optionalDependencies:
+ '@parcel/watcher-android-arm64': 2.5.4
+ '@parcel/watcher-darwin-arm64': 2.5.4
+ '@parcel/watcher-darwin-x64': 2.5.4
+ '@parcel/watcher-freebsd-x64': 2.5.4
+ '@parcel/watcher-linux-arm-glibc': 2.5.4
+ '@parcel/watcher-linux-arm-musl': 2.5.4
+ '@parcel/watcher-linux-arm64-glibc': 2.5.4
+ '@parcel/watcher-linux-arm64-musl': 2.5.4
+ '@parcel/watcher-linux-x64-glibc': 2.5.4
+ '@parcel/watcher-linux-x64-musl': 2.5.4
+ '@parcel/watcher-win32-arm64': 2.5.4
+ '@parcel/watcher-win32-ia32': 2.5.4
+ '@parcel/watcher-win32-x64': 2.5.4
+ optional: true
+
+ '@pkgr/core@0.2.9': {}
+
+ '@rolldown/pluginutils@1.0.0-beta.53': {}
+
+ '@rollup/rollup-android-arm-eabi@4.55.1':
+ optional: true
+
+ '@rollup/rollup-android-arm64@4.55.1':
+ optional: true
+
+ '@rollup/rollup-darwin-arm64@4.55.1':
+ optional: true
+
+ '@rollup/rollup-darwin-x64@4.55.1':
+ optional: true
+
+ '@rollup/rollup-freebsd-arm64@4.55.1':
+ optional: true
+
+ '@rollup/rollup-freebsd-x64@4.55.1':
+ optional: true
+
+ '@rollup/rollup-linux-arm-gnueabihf@4.55.1':
+ optional: true
+
+ '@rollup/rollup-linux-arm-musleabihf@4.55.1':
+ optional: true
+
+ '@rollup/rollup-linux-arm64-gnu@4.55.1':
+ optional: true
+
+ '@rollup/rollup-linux-arm64-musl@4.55.1':
+ optional: true
+
+ '@rollup/rollup-linux-loong64-gnu@4.55.1':
+ optional: true
+
+ '@rollup/rollup-linux-loong64-musl@4.55.1':
+ optional: true
+
+ '@rollup/rollup-linux-ppc64-gnu@4.55.1':
+ optional: true
+
+ '@rollup/rollup-linux-ppc64-musl@4.55.1':
+ optional: true
+
+ '@rollup/rollup-linux-riscv64-gnu@4.55.1':
+ optional: true
+
+ '@rollup/rollup-linux-riscv64-musl@4.55.1':
+ optional: true
+
+ '@rollup/rollup-linux-s390x-gnu@4.55.1':
+ optional: true
+
+ '@rollup/rollup-linux-x64-gnu@4.55.1':
+ optional: true
+
+ '@rollup/rollup-linux-x64-musl@4.55.1':
+ optional: true
+
+ '@rollup/rollup-openbsd-x64@4.55.1':
+ optional: true
+
+ '@rollup/rollup-openharmony-arm64@4.55.1':
+ optional: true
+
+ '@rollup/rollup-win32-arm64-msvc@4.55.1':
+ optional: true
+
+ '@rollup/rollup-win32-ia32-msvc@4.55.1':
+ optional: true
+
+ '@rollup/rollup-win32-x64-gnu@4.55.1':
+ optional: true
+
+ '@rollup/rollup-win32-x64-msvc@4.55.1':
+ optional: true
+
+ '@shikijs/core@3.21.0':
+ dependencies:
+ '@shikijs/types': 3.21.0
+ '@shikijs/vscode-textmate': 10.0.2
+ '@types/hast': 3.0.4
+ hast-util-to-html: 9.0.5
+
+ '@shikijs/engine-javascript@3.21.0':
+ dependencies:
+ '@shikijs/types': 3.21.0
+ '@shikijs/vscode-textmate': 10.0.2
+ oniguruma-to-es: 4.3.4
+
+ '@shikijs/engine-oniguruma@3.21.0':
+ dependencies:
+ '@shikijs/types': 3.21.0
+ '@shikijs/vscode-textmate': 10.0.2
+
+ '@shikijs/langs@3.21.0':
+ dependencies:
+ '@shikijs/types': 3.21.0
+
+ '@shikijs/themes@3.21.0':
+ dependencies:
+ '@shikijs/types': 3.21.0
+
+ '@shikijs/transformers@3.21.0':
+ dependencies:
+ '@shikijs/core': 3.21.0
+ '@shikijs/types': 3.21.0
+
+ '@shikijs/types@3.21.0':
+ dependencies:
+ '@shikijs/vscode-textmate': 10.0.2
+ '@types/hast': 3.0.4
+
+ '@shikijs/vscode-textmate@10.0.2': {}
+
+ '@sindresorhus/merge-streams@2.3.0': {}
+
+ '@stackblitz/sdk@1.11.0': {}
+
+ '@types/d3-array@3.2.2': {}
+
+ '@types/d3-axis@3.0.6':
+ dependencies:
+ '@types/d3-selection': 3.0.11
+
+ '@types/d3-brush@3.0.6':
+ dependencies:
+ '@types/d3-selection': 3.0.11
+
+ '@types/d3-chord@3.0.6': {}
+
+ '@types/d3-color@3.1.3': {}
+
+ '@types/d3-contour@3.0.6':
+ dependencies:
+ '@types/d3-array': 3.2.2
+ '@types/geojson': 7946.0.16
+
+ '@types/d3-delaunay@6.0.4': {}
+
+ '@types/d3-dispatch@3.0.7': {}
+
+ '@types/d3-drag@3.0.7':
+ dependencies:
+ '@types/d3-selection': 3.0.11
+
+ '@types/d3-dsv@3.0.7': {}
+
+ '@types/d3-ease@3.0.2': {}
+
+ '@types/d3-fetch@3.0.7':
+ dependencies:
+ '@types/d3-dsv': 3.0.7
+
+ '@types/d3-force@3.0.10': {}
+
+ '@types/d3-format@3.0.4': {}
+
+ '@types/d3-geo@3.1.0':
+ dependencies:
+ '@types/geojson': 7946.0.16
+
+ '@types/d3-hierarchy@3.1.7': {}
+
+ '@types/d3-interpolate@3.0.4':
+ dependencies:
+ '@types/d3-color': 3.1.3
+
+ '@types/d3-path@3.1.1': {}
+
+ '@types/d3-polygon@3.0.2': {}
+
+ '@types/d3-quadtree@3.0.6': {}
+
+ '@types/d3-random@3.0.3': {}
+
+ '@types/d3-scale-chromatic@3.1.0': {}
+
+ '@types/d3-scale@4.0.9':
+ dependencies:
+ '@types/d3-time': 3.0.4
+
+ '@types/d3-selection@3.0.11': {}
+
+ '@types/d3-shape@3.1.8':
+ dependencies:
+ '@types/d3-path': 3.1.1
+
+ '@types/d3-time-format@4.0.3': {}
+
+ '@types/d3-time@3.0.4': {}
+
+ '@types/d3-timer@3.0.2': {}
+
+ '@types/d3-transition@3.0.9':
+ dependencies:
+ '@types/d3-selection': 3.0.11
+
+ '@types/d3-zoom@3.0.8':
+ dependencies:
+ '@types/d3-interpolate': 3.0.4
+ '@types/d3-selection': 3.0.11
+
+ '@types/d3@7.4.3':
+ dependencies:
+ '@types/d3-array': 3.2.2
+ '@types/d3-axis': 3.0.6
+ '@types/d3-brush': 3.0.6
+ '@types/d3-chord': 3.0.6
+ '@types/d3-color': 3.1.3
+ '@types/d3-contour': 3.0.6
+ '@types/d3-delaunay': 6.0.4
+ '@types/d3-dispatch': 3.0.7
+ '@types/d3-drag': 3.0.7
+ '@types/d3-dsv': 3.0.7
+ '@types/d3-ease': 3.0.2
+ '@types/d3-fetch': 3.0.7
+ '@types/d3-force': 3.0.10
+ '@types/d3-format': 3.0.4
+ '@types/d3-geo': 3.1.0
+ '@types/d3-hierarchy': 3.1.7
+ '@types/d3-interpolate': 3.0.4
+ '@types/d3-path': 3.1.1
+ '@types/d3-polygon': 3.0.2
+ '@types/d3-quadtree': 3.0.6
+ '@types/d3-random': 3.0.3
+ '@types/d3-scale': 4.0.9
+ '@types/d3-scale-chromatic': 3.1.0
+ '@types/d3-selection': 3.0.11
+ '@types/d3-shape': 3.1.8
+ '@types/d3-time': 3.0.4
+ '@types/d3-time-format': 4.0.3
+ '@types/d3-timer': 3.0.2
+ '@types/d3-transition': 3.0.9
+ '@types/d3-zoom': 3.0.8
+
+ '@types/debug@4.1.12':
+ dependencies:
+ '@types/ms': 2.1.0
+
+ '@types/estree@1.0.8': {}
+
+ '@types/fs-extra@11.0.4':
+ dependencies:
+ '@types/jsonfile': 6.1.4
+ '@types/node': 25.0.9
+
+ '@types/geojson@7946.0.16': {}
+
+ '@types/hash-sum@1.0.2': {}
+
+ '@types/hast@3.0.4':
+ dependencies:
+ '@types/unist': 3.0.3
+
+ '@types/jsonfile@6.1.4':
+ dependencies:
+ '@types/node': 25.0.9
+
+ '@types/katex@0.16.8': {}
+
+ '@types/linkify-it@5.0.0': {}
+
+ '@types/markdown-it-emoji@3.0.1':
+ dependencies:
+ '@types/markdown-it': 14.1.2
+
+ '@types/markdown-it@14.1.2':
+ dependencies:
+ '@types/linkify-it': 5.0.0
+ '@types/mdurl': 2.0.0
+
+ '@types/mdast@4.0.4':
+ dependencies:
+ '@types/unist': 3.0.3
+
+ '@types/mdurl@2.0.0': {}
+
+ '@types/ms@2.1.0': {}
+
+ '@types/node@24.10.9':
+ dependencies:
+ undici-types: 7.16.0
+
+ '@types/node@25.0.9':
+ dependencies:
+ undici-types: 7.16.0
+
+ '@types/picomatch@4.0.2': {}
+
+ '@types/sax@1.2.7':
+ dependencies:
+ '@types/node': 24.10.9
+
+ '@types/trusted-types@2.0.7': {}
+
+ '@types/unist@2.0.11': {}
+
+ '@types/unist@3.0.3': {}
+
+ '@types/web-bluetooth@0.0.21': {}
+
+ '@ungap/structured-clone@1.3.0': {}
+
+ '@vitejs/plugin-vue@6.0.3(vite@7.3.1(@types/node@25.0.9)(sass-embedded@1.97.2)(sass@1.97.2))(vue@3.5.26)':
+ dependencies:
+ '@rolldown/pluginutils': 1.0.0-beta.53
+ vite: 7.3.1(@types/node@25.0.9)(sass-embedded@1.97.2)(sass@1.97.2)
+ vue: 3.5.26
+
+ '@vue/compiler-core@3.5.26':
+ dependencies:
+ '@babel/parser': 7.28.6
+ '@vue/shared': 3.5.26
+ entities: 7.0.0
+ estree-walker: 2.0.2
+ source-map-js: 1.2.1
+
+ '@vue/compiler-dom@3.5.26':
+ dependencies:
+ '@vue/compiler-core': 3.5.26
+ '@vue/shared': 3.5.26
+
+ '@vue/compiler-sfc@3.5.26':
+ dependencies:
+ '@babel/parser': 7.28.6
+ '@vue/compiler-core': 3.5.26
+ '@vue/compiler-dom': 3.5.26
+ '@vue/compiler-ssr': 3.5.26
+ '@vue/shared': 3.5.26
+ estree-walker: 2.0.2
+ magic-string: 0.30.21
+ postcss: 8.5.6
+ source-map-js: 1.2.1
+
+ '@vue/compiler-ssr@3.5.26':
+ dependencies:
+ '@vue/compiler-dom': 3.5.26
+ '@vue/shared': 3.5.26
+
+ '@vue/devtools-api@6.6.4': {}
+
+ '@vue/devtools-api@8.0.5':
+ dependencies:
+ '@vue/devtools-kit': 8.0.5
+
+ '@vue/devtools-kit@8.0.5':
+ dependencies:
+ '@vue/devtools-shared': 8.0.5
+ birpc: 2.9.0
+ hookable: 5.5.3
+ mitt: 3.0.1
+ perfect-debounce: 2.0.0
+ speakingurl: 14.0.1
+ superjson: 2.2.6
+
+ '@vue/devtools-shared@8.0.5':
+ dependencies:
+ rfdc: 1.4.1
+
+ '@vue/reactivity@3.5.26':
+ dependencies:
+ '@vue/shared': 3.5.26
+
+ '@vue/runtime-core@3.5.26':
+ dependencies:
+ '@vue/reactivity': 3.5.26
+ '@vue/shared': 3.5.26
+
+ '@vue/runtime-dom@3.5.26':
+ dependencies:
+ '@vue/reactivity': 3.5.26
+ '@vue/runtime-core': 3.5.26
+ '@vue/shared': 3.5.26
+ csstype: 3.2.3
+
+ '@vue/server-renderer@3.5.26(vue@3.5.26)':
+ dependencies:
+ '@vue/compiler-ssr': 3.5.26
+ '@vue/shared': 3.5.26
+ vue: 3.5.26
+
+ '@vue/shared@3.5.26': {}
+
+ '@vuepress/bundler-vite@2.0.0-rc.26(@types/node@25.0.9)(sass-embedded@1.97.2)(sass@1.97.2)':
+ dependencies:
+ '@vitejs/plugin-vue': 6.0.3(vite@7.3.1(@types/node@25.0.9)(sass-embedded@1.97.2)(sass@1.97.2))(vue@3.5.26)
+ '@vuepress/bundlerutils': 2.0.0-rc.26
+ '@vuepress/client': 2.0.0-rc.26
+ '@vuepress/core': 2.0.0-rc.26
+ '@vuepress/shared': 2.0.0-rc.26
+ '@vuepress/utils': 2.0.0-rc.26
+ autoprefixer: 10.4.23(postcss@8.5.6)
+ connect-history-api-fallback: 2.0.0
+ postcss: 8.5.6
+ postcss-load-config: 6.0.1(postcss@8.5.6)
+ rollup: 4.55.1
+ vite: 7.3.1(@types/node@25.0.9)(sass-embedded@1.97.2)(sass@1.97.2)
+ vue: 3.5.26
+ vue-router: 4.6.4(vue@3.5.26)
+ transitivePeerDependencies:
+ - '@types/node'
+ - jiti
+ - less
+ - lightningcss
+ - sass
+ - sass-embedded
+ - stylus
+ - sugarss
+ - supports-color
+ - terser
+ - tsx
- typescript
- dev: false
+ - yaml
- /@vuepress/helper@2.0.0-rc.21(vuepress@2.0.0-rc.9):
- resolution: {integrity: sha512-yKIG8hwsrA63uWo9hx9u7KBc0HvotKe2/0wVZtUdvdsibG3UYNI9enYQNa8MdqbxF92mmlFkPZdosGjUTA+BYw==}
- peerDependencies:
- vuepress: 2.0.0-rc.9
+ '@vuepress/bundlerutils@2.0.0-rc.26':
+ dependencies:
+ '@vuepress/client': 2.0.0-rc.26
+ '@vuepress/core': 2.0.0-rc.26
+ '@vuepress/shared': 2.0.0-rc.26
+ '@vuepress/utils': 2.0.0-rc.26
+ vue: 3.5.26
+ vue-router: 4.6.4(vue@3.5.26)
+ transitivePeerDependencies:
+ - supports-color
+ - typescript
+
+ '@vuepress/cli@2.0.0-rc.26':
+ dependencies:
+ '@vuepress/core': 2.0.0-rc.26
+ '@vuepress/shared': 2.0.0-rc.26
+ '@vuepress/utils': 2.0.0-rc.26
+ cac: 6.7.14
+ chokidar: 4.0.3
+ envinfo: 7.21.0
+ esbuild: 0.25.12
+ transitivePeerDependencies:
+ - supports-color
+ - typescript
+
+ '@vuepress/client@2.0.0-rc.26':
+ dependencies:
+ '@vue/devtools-api': 8.0.5
+ '@vue/devtools-kit': 8.0.5
+ '@vuepress/shared': 2.0.0-rc.26
+ vue: 3.5.26
+ vue-router: 4.6.4(vue@3.5.26)
+ transitivePeerDependencies:
+ - typescript
+
+ '@vuepress/core@2.0.0-rc.26':
+ dependencies:
+ '@vuepress/client': 2.0.0-rc.26
+ '@vuepress/markdown': 2.0.0-rc.26
+ '@vuepress/shared': 2.0.0-rc.26
+ '@vuepress/utils': 2.0.0-rc.26
+ vue: 3.5.26
+ transitivePeerDependencies:
+ - supports-color
+ - typescript
+
+ '@vuepress/helper@2.0.0-rc.120(vuepress@2.0.0-rc.26(@vuepress/bundler-vite@2.0.0-rc.26(@types/node@25.0.9)(sass-embedded@1.97.2)(sass@1.97.2))(vue@3.5.26))':
+ dependencies:
+ '@vue/shared': 3.5.26
+ '@vueuse/core': 14.1.0(vue@3.5.26)
+ cheerio: 1.1.2
+ fflate: 0.8.2
+ gray-matter: 4.0.3
+ vue: 3.5.26
+ vuepress: 2.0.0-rc.26(@vuepress/bundler-vite@2.0.0-rc.26(@types/node@25.0.9)(sass-embedded@1.97.2)(sass@1.97.2))(vue@3.5.26)
+ transitivePeerDependencies:
+ - typescript
+
+ '@vuepress/helper@2.0.0-rc.121(vuepress@2.0.0-rc.26(@vuepress/bundler-vite@2.0.0-rc.26(@types/node@25.0.9)(sass-embedded@1.97.2)(sass@1.97.2))(vue@3.5.26))':
dependencies:
- '@vue/shared': 3.4.21
- cheerio: 1.0.0-rc.12
+ '@vue/shared': 3.5.26
+ '@vueuse/core': 14.1.0(vue@3.5.26)
+ cheerio: 1.1.2
fflate: 0.8.2
gray-matter: 4.0.3
- vue: 3.4.21
- vuepress: 2.0.0-rc.9(@vuepress/bundler-vite@2.0.0-rc.9)(vue@3.4.21)
+ vue: 3.5.26
+ vuepress: 2.0.0-rc.26(@vuepress/bundler-vite@2.0.0-rc.26(@types/node@25.0.9)(sass-embedded@1.97.2)(sass@1.97.2))(vue@3.5.26)
transitivePeerDependencies:
- typescript
- dev: false
-
- /@vuepress/markdown@2.0.0-rc.9:
- resolution: {integrity: sha512-e7as2ar3RQp0bUyMiwBPi7L/G2fzscb3s0BywNcAwubFR22o0/dBEYRYdrN0clPQ2FXpPxF6AFj4aD7O1heCbw==}
- dependencies:
- '@mdit-vue/plugin-component': 2.0.0
- '@mdit-vue/plugin-frontmatter': 2.0.0
- '@mdit-vue/plugin-headers': 2.0.0
- '@mdit-vue/plugin-sfc': 2.0.0
- '@mdit-vue/plugin-title': 2.0.0
- '@mdit-vue/plugin-toc': 2.0.0
- '@mdit-vue/shared': 2.0.0
- '@mdit-vue/types': 2.0.0
- '@types/markdown-it': 13.0.7
- '@types/markdown-it-emoji': 2.0.4
- '@vuepress/shared': 2.0.0-rc.9
- '@vuepress/utils': 2.0.0-rc.9
+
+ '@vuepress/highlighter-helper@2.0.0-rc.118(@vueuse/core@14.1.0(vue@3.5.26))(vuepress@2.0.0-rc.26(@vuepress/bundler-vite@2.0.0-rc.26(@types/node@25.0.9)(sass-embedded@1.97.2)(sass@1.97.2))(vue@3.5.26))':
+ dependencies:
+ vuepress: 2.0.0-rc.26(@vuepress/bundler-vite@2.0.0-rc.26(@types/node@25.0.9)(sass-embedded@1.97.2)(sass@1.97.2))(vue@3.5.26)
+ optionalDependencies:
+ '@vueuse/core': 14.1.0(vue@3.5.26)
+
+ '@vuepress/markdown@2.0.0-rc.26':
+ dependencies:
+ '@mdit-vue/plugin-component': 3.0.2
+ '@mdit-vue/plugin-frontmatter': 3.0.2
+ '@mdit-vue/plugin-headers': 3.0.2
+ '@mdit-vue/plugin-sfc': 3.0.2
+ '@mdit-vue/plugin-title': 3.0.2
+ '@mdit-vue/plugin-toc': 3.0.2
+ '@mdit-vue/shared': 3.0.2
+ '@mdit-vue/types': 3.0.2
+ '@types/markdown-it': 14.1.2
+ '@types/markdown-it-emoji': 3.0.1
+ '@vuepress/shared': 2.0.0-rc.26
+ '@vuepress/utils': 2.0.0-rc.26
markdown-it: 14.1.0
- markdown-it-anchor: 8.6.7(@types/markdown-it@13.0.7)(markdown-it@14.1.0)
+ markdown-it-anchor: 9.2.0(@types/markdown-it@14.1.2)(markdown-it@14.1.0)
markdown-it-emoji: 3.0.0
mdurl: 2.0.0
transitivePeerDependencies:
- supports-color
- dev: false
- /@vuepress/plugin-active-header-links@2.0.0-rc.21(vuepress@2.0.0-rc.9):
- resolution: {integrity: sha512-6i9TfGDV1zfszQ5aw6bV+/UvPdBWt3VxN2WB4Dg5o1g8Qn4z5CI6AW6VfLKRyaKUD+Rzj6W+Ikgx4xnF5RZAdA==}
- peerDependencies:
- vuepress: 2.0.0-rc.9
+ '@vuepress/plugin-active-header-links@2.0.0-rc.118(vuepress@2.0.0-rc.26(@vuepress/bundler-vite@2.0.0-rc.26(@types/node@25.0.9)(sass-embedded@1.97.2)(sass@1.97.2))(vue@3.5.26))':
dependencies:
- '@vueuse/core': 10.9.0(vue@3.4.21)
- vue: 3.4.21
- vuepress: 2.0.0-rc.9(@vuepress/bundler-vite@2.0.0-rc.9)(vue@3.4.21)
+ '@vueuse/core': 14.1.0(vue@3.5.26)
+ vue: 3.5.26
+ vuepress: 2.0.0-rc.26(@vuepress/bundler-vite@2.0.0-rc.26(@types/node@25.0.9)(sass-embedded@1.97.2)(sass@1.97.2))(vue@3.5.26)
transitivePeerDependencies:
- - '@vue/composition-api'
- typescript
- dev: false
- /@vuepress/plugin-back-to-top@2.0.0-rc.21(vuepress@2.0.0-rc.9):
- resolution: {integrity: sha512-qEWu0BFvadJZRH1r1UQW4mHvBYwHGSJtwNv14C/Qmxuvv2UQnpl8T2qbvPAntUWMdy94wVhr2YWCfyLh7TSEOA==}
- peerDependencies:
- vuepress: 2.0.0-rc.9
+ '@vuepress/plugin-back-to-top@2.0.0-rc.121(vuepress@2.0.0-rc.26(@vuepress/bundler-vite@2.0.0-rc.26(@types/node@25.0.9)(sass-embedded@1.97.2)(sass@1.97.2))(vue@3.5.26))':
dependencies:
- '@vuepress/helper': 2.0.0-rc.21(vuepress@2.0.0-rc.9)
- '@vueuse/core': 10.9.0(vue@3.4.21)
- vue: 3.4.21
- vuepress: 2.0.0-rc.9(@vuepress/bundler-vite@2.0.0-rc.9)(vue@3.4.21)
+ '@vuepress/helper': 2.0.0-rc.121(vuepress@2.0.0-rc.26(@vuepress/bundler-vite@2.0.0-rc.26(@types/node@25.0.9)(sass-embedded@1.97.2)(sass@1.97.2))(vue@3.5.26))
+ '@vueuse/core': 14.1.0(vue@3.5.26)
+ vue: 3.5.26
+ vuepress: 2.0.0-rc.26(@vuepress/bundler-vite@2.0.0-rc.26(@types/node@25.0.9)(sass-embedded@1.97.2)(sass@1.97.2))(vue@3.5.26)
transitivePeerDependencies:
- - '@vue/composition-api'
- typescript
- dev: false
- /@vuepress/plugin-blog@2.0.0-rc.21(vuepress@2.0.0-rc.9):
- resolution: {integrity: sha512-LCju1cC7vFA4WMaYbcnkhnEuiGno0JcavcDRFS6Np4hlVKhvh5Zoi47R3FBuF88X2OdbvBog63k4m5Jp60f+aQ==}
- peerDependencies:
- vuepress: 2.0.0-rc.9
+ '@vuepress/plugin-blog@2.0.0-rc.121(vuepress@2.0.0-rc.26(@vuepress/bundler-vite@2.0.0-rc.26(@types/node@25.0.9)(sass-embedded@1.97.2)(sass@1.97.2))(vue@3.5.26))':
dependencies:
- '@vuepress/helper': 2.0.0-rc.21(vuepress@2.0.0-rc.9)
- chokidar: 3.6.0
- vue: 3.4.21
- vuepress: 2.0.0-rc.9(@vuepress/bundler-vite@2.0.0-rc.9)(vue@3.4.21)
+ '@vuepress/helper': 2.0.0-rc.121(vuepress@2.0.0-rc.26(@vuepress/bundler-vite@2.0.0-rc.26(@types/node@25.0.9)(sass-embedded@1.97.2)(sass@1.97.2))(vue@3.5.26))
+ chokidar: 4.0.3
+ vue: 3.5.26
+ vuepress: 2.0.0-rc.26(@vuepress/bundler-vite@2.0.0-rc.26(@types/node@25.0.9)(sass-embedded@1.97.2)(sass@1.97.2))(vue@3.5.26)
transitivePeerDependencies:
- typescript
- dev: false
- /@vuepress/plugin-catalog@2.0.0-rc.21(vuepress@2.0.0-rc.9):
- resolution: {integrity: sha512-qWb0K/clF9o0bW8eeiu/30f5MWNt4MZI3jt6pLL67sAOyiMXqlHSMCKI/WYZhzgnPOQi9cnIAWoF4ePsApIzAA==}
- peerDependencies:
- vuepress: 2.0.0-rc.9
+ '@vuepress/plugin-catalog@2.0.0-rc.121(vuepress@2.0.0-rc.26(@vuepress/bundler-vite@2.0.0-rc.26(@types/node@25.0.9)(sass-embedded@1.97.2)(sass@1.97.2))(vue@3.5.26))':
dependencies:
- '@vuepress/helper': 2.0.0-rc.21(vuepress@2.0.0-rc.9)
- vue: 3.4.21
- vuepress: 2.0.0-rc.9(@vuepress/bundler-vite@2.0.0-rc.9)(vue@3.4.21)
+ '@vuepress/helper': 2.0.0-rc.121(vuepress@2.0.0-rc.26(@vuepress/bundler-vite@2.0.0-rc.26(@types/node@25.0.9)(sass-embedded@1.97.2)(sass@1.97.2))(vue@3.5.26))
+ vue: 3.5.26
+ vuepress: 2.0.0-rc.26(@vuepress/bundler-vite@2.0.0-rc.26(@types/node@25.0.9)(sass-embedded@1.97.2)(sass@1.97.2))(vue@3.5.26)
transitivePeerDependencies:
- typescript
- dev: false
- /@vuepress/plugin-comment@2.0.0-rc.21(vuepress@2.0.0-rc.9):
- resolution: {integrity: sha512-17Qpwz/W/B3cVLiCTq0UO18dy1ejf563mURas7XgqIcIibGE3G1k5kTftDbwbpehQ0e/4okdIwnkSxPpeGPOFw==}
- peerDependencies:
- '@waline/client': ^3.1.0
- artalk: ^2.7.3
- sass-loader: ^14.0.0
- twikoo: ^1.5.0
- vuepress: 2.0.0-rc.9
- peerDependenciesMeta:
- '@waline/client':
- optional: true
- artalk:
- optional: true
- sass-loader:
- optional: true
- twikoo:
- optional: true
+ '@vuepress/plugin-comment@2.0.0-rc.121(vuepress@2.0.0-rc.26(@vuepress/bundler-vite@2.0.0-rc.26(@types/node@25.0.9)(sass-embedded@1.97.2)(sass@1.97.2))(vue@3.5.26))':
dependencies:
- '@vuepress/helper': 2.0.0-rc.21(vuepress@2.0.0-rc.9)
- giscus: 1.5.0
- vue: 3.4.21
- vuepress: 2.0.0-rc.9(@vuepress/bundler-vite@2.0.0-rc.9)(vue@3.4.21)
+ '@vuepress/helper': 2.0.0-rc.121(vuepress@2.0.0-rc.26(@vuepress/bundler-vite@2.0.0-rc.26(@types/node@25.0.9)(sass-embedded@1.97.2)(sass@1.97.2))(vue@3.5.26))
+ '@vueuse/core': 14.1.0(vue@3.5.26)
+ giscus: 1.6.0
+ vue: 3.5.26
+ vuepress: 2.0.0-rc.26(@vuepress/bundler-vite@2.0.0-rc.26(@types/node@25.0.9)(sass-embedded@1.97.2)(sass@1.97.2))(vue@3.5.26)
transitivePeerDependencies:
- typescript
- dev: false
- /@vuepress/plugin-copy-code@2.0.0-rc.21(vuepress@2.0.0-rc.9):
- resolution: {integrity: sha512-280jsPmI/YeKBnXt/MCw/nrv9pUou+zhHK5mOU3ecVYfY7Pu2Xi1zdZ2kK0Ri02Txm5AwLb5YWeSac349JuUUA==}
- peerDependencies:
- vuepress: 2.0.0-rc.9
+ '@vuepress/plugin-copy-code@2.0.0-rc.121(vuepress@2.0.0-rc.26(@vuepress/bundler-vite@2.0.0-rc.26(@types/node@25.0.9)(sass-embedded@1.97.2)(sass@1.97.2))(vue@3.5.26))':
dependencies:
- '@vuepress/helper': 2.0.0-rc.21(vuepress@2.0.0-rc.9)
- '@vueuse/core': 10.9.0(vue@3.4.21)
- vue: 3.4.21
- vuepress: 2.0.0-rc.9(@vuepress/bundler-vite@2.0.0-rc.9)(vue@3.4.21)
+ '@vuepress/helper': 2.0.0-rc.121(vuepress@2.0.0-rc.26(@vuepress/bundler-vite@2.0.0-rc.26(@types/node@25.0.9)(sass-embedded@1.97.2)(sass@1.97.2))(vue@3.5.26))
+ '@vueuse/core': 14.1.0(vue@3.5.26)
+ vue: 3.5.26
+ vuepress: 2.0.0-rc.26(@vuepress/bundler-vite@2.0.0-rc.26(@types/node@25.0.9)(sass-embedded@1.97.2)(sass@1.97.2))(vue@3.5.26)
transitivePeerDependencies:
- - '@vue/composition-api'
- typescript
- dev: false
- /@vuepress/plugin-copyright@2.0.0-rc.21(vuepress@2.0.0-rc.9):
- resolution: {integrity: sha512-KFDnWkyz3tdts7uuCyVawYr2SdkNjDFJ93CtPYPDounnEJDC9OxQk0d7QSMYHotTZUzmfOiQNmJL/FngEVaDQQ==}
- peerDependencies:
- vuepress: 2.0.0-rc.9
+ '@vuepress/plugin-copyright@2.0.0-rc.121(vuepress@2.0.0-rc.26(@vuepress/bundler-vite@2.0.0-rc.26(@types/node@25.0.9)(sass-embedded@1.97.2)(sass@1.97.2))(vue@3.5.26))':
dependencies:
- '@vuepress/helper': 2.0.0-rc.21(vuepress@2.0.0-rc.9)
- '@vueuse/core': 10.9.0(vue@3.4.21)
- vue: 3.4.21
- vuepress: 2.0.0-rc.9(@vuepress/bundler-vite@2.0.0-rc.9)(vue@3.4.21)
+ '@vuepress/helper': 2.0.0-rc.121(vuepress@2.0.0-rc.26(@vuepress/bundler-vite@2.0.0-rc.26(@types/node@25.0.9)(sass-embedded@1.97.2)(sass@1.97.2))(vue@3.5.26))
+ '@vueuse/core': 14.1.0(vue@3.5.26)
+ vue: 3.5.26
+ vuepress: 2.0.0-rc.26(@vuepress/bundler-vite@2.0.0-rc.26(@types/node@25.0.9)(sass-embedded@1.97.2)(sass@1.97.2))(vue@3.5.26)
transitivePeerDependencies:
- - '@vue/composition-api'
- typescript
- dev: false
- /@vuepress/plugin-external-link-icon@2.0.0-rc.21(vuepress@2.0.0-rc.9):
- resolution: {integrity: sha512-Wt7hjWpoUccJHj5KHK24Uks+6oWug6y5cw9QzWlNgiCyg+hvII7I+FdORRvibPUG2ndymi6ZOFyJZcR072kbKA==}
- peerDependencies:
- vuepress: 2.0.0-rc.9
+ '@vuepress/plugin-feed@2.0.0-rc.121(vuepress@2.0.0-rc.26(@vuepress/bundler-vite@2.0.0-rc.26(@types/node@25.0.9)(sass-embedded@1.97.2)(sass@1.97.2))(vue@3.5.26))':
dependencies:
- vue: 3.4.21
- vuepress: 2.0.0-rc.9(@vuepress/bundler-vite@2.0.0-rc.9)(vue@3.4.21)
+ '@vuepress/helper': 2.0.0-rc.121(vuepress@2.0.0-rc.26(@vuepress/bundler-vite@2.0.0-rc.26(@types/node@25.0.9)(sass-embedded@1.97.2)(sass@1.97.2))(vue@3.5.26))
+ vuepress: 2.0.0-rc.26(@vuepress/bundler-vite@2.0.0-rc.26(@types/node@25.0.9)(sass-embedded@1.97.2)(sass@1.97.2))(vue@3.5.26)
+ xml-js: 1.6.11
transitivePeerDependencies:
- typescript
- dev: false
- /@vuepress/plugin-feed@2.0.0-rc.21(vuepress@2.0.0-rc.9):
- resolution: {integrity: sha512-gtfBDSKw2b1c7aMPah4FvoaHxYj8CKCctsYtRHbuwl7xDLxj1BnRD9tBPag9llTiq7RYxHm1EK7ovezRa1hZfQ==}
- peerDependencies:
- vuepress: 2.0.0-rc.9
+ '@vuepress/plugin-git@2.0.0-rc.121(vuepress@2.0.0-rc.26(@vuepress/bundler-vite@2.0.0-rc.26(@types/node@25.0.9)(sass-embedded@1.97.2)(sass@1.97.2))(vue@3.5.26))':
dependencies:
- '@vuepress/helper': 2.0.0-rc.21(vuepress@2.0.0-rc.9)
- cheerio: 1.0.0-rc.12
- vuepress: 2.0.0-rc.9(@vuepress/bundler-vite@2.0.0-rc.9)(vue@3.4.21)
- xml-js: 1.6.11
+ '@vuepress/helper': 2.0.0-rc.121(vuepress@2.0.0-rc.26(@vuepress/bundler-vite@2.0.0-rc.26(@types/node@25.0.9)(sass-embedded@1.97.2)(sass@1.97.2))(vue@3.5.26))
+ '@vueuse/core': 14.1.0(vue@3.5.26)
+ rehype-parse: 9.0.1
+ rehype-sanitize: 6.0.0
+ rehype-stringify: 10.0.1
+ unified: 11.0.5
+ vue: 3.5.26
+ vuepress: 2.0.0-rc.26(@vuepress/bundler-vite@2.0.0-rc.26(@types/node@25.0.9)(sass-embedded@1.97.2)(sass@1.97.2))(vue@3.5.26)
transitivePeerDependencies:
- typescript
- dev: false
- /@vuepress/plugin-git@2.0.0-rc.21(vuepress@2.0.0-rc.9):
- resolution: {integrity: sha512-Xgrqv86bjrBPFrJr69b1KQlDUhAGhWfBRIGM3GQOI98mOi2VKCX9P4xyWK/lIpn8eVB3s0lY1KewhkXgy7UITg==}
- peerDependencies:
- vuepress: 2.0.0-rc.9
+ '@vuepress/plugin-icon@2.0.0-rc.121(markdown-it@14.1.0)(vuepress@2.0.0-rc.26(@vuepress/bundler-vite@2.0.0-rc.26(@types/node@25.0.9)(sass-embedded@1.97.2)(sass@1.97.2))(vue@3.5.26))':
dependencies:
- execa: 8.0.1
- vuepress: 2.0.0-rc.9(@vuepress/bundler-vite@2.0.0-rc.9)(vue@3.4.21)
- dev: false
+ '@mdit/plugin-icon': 0.23.0(markdown-it@14.1.0)
+ '@vuepress/helper': 2.0.0-rc.121(vuepress@2.0.0-rc.26(@vuepress/bundler-vite@2.0.0-rc.26(@types/node@25.0.9)(sass-embedded@1.97.2)(sass@1.97.2))(vue@3.5.26))
+ '@vueuse/core': 14.1.0(vue@3.5.26)
+ vue: 3.5.26
+ vuepress: 2.0.0-rc.26(@vuepress/bundler-vite@2.0.0-rc.26(@types/node@25.0.9)(sass-embedded@1.97.2)(sass@1.97.2))(vue@3.5.26)
+ transitivePeerDependencies:
+ - markdown-it
+ - typescript
- /@vuepress/plugin-links-check@2.0.0-rc.21(vuepress@2.0.0-rc.9):
- resolution: {integrity: sha512-3dIXKJILTDP7RoPVmhtq/RfytZqX1sCA9Bf++DlgQV6jp2ctcTf4F9I5J/2wQce8yuLogO8fHnWhEgO2rgQXLw==}
- peerDependencies:
- vuepress: 2.0.0-rc.9
+ '@vuepress/plugin-links-check@2.0.0-rc.121(vuepress@2.0.0-rc.26(@vuepress/bundler-vite@2.0.0-rc.26(@types/node@25.0.9)(sass-embedded@1.97.2)(sass@1.97.2))(vue@3.5.26))':
dependencies:
- '@vuepress/helper': 2.0.0-rc.21(vuepress@2.0.0-rc.9)
- vuepress: 2.0.0-rc.9(@vuepress/bundler-vite@2.0.0-rc.9)(vue@3.4.21)
+ '@vuepress/helper': 2.0.0-rc.121(vuepress@2.0.0-rc.26(@vuepress/bundler-vite@2.0.0-rc.26(@types/node@25.0.9)(sass-embedded@1.97.2)(sass@1.97.2))(vue@3.5.26))
+ vuepress: 2.0.0-rc.26(@vuepress/bundler-vite@2.0.0-rc.26(@types/node@25.0.9)(sass-embedded@1.97.2)(sass@1.97.2))(vue@3.5.26)
transitivePeerDependencies:
- typescript
- dev: false
- /@vuepress/plugin-nprogress@2.0.0-rc.21(vuepress@2.0.0-rc.9):
- resolution: {integrity: sha512-qpGA76195SyfpuQC1Pb9LwgCYIp/zg+BBDnexukJMdLjP1KnaU7HLhS5NnRNIWv8E+IC61zLvlh/wRox17QE+w==}
- peerDependencies:
- vuepress: 2.0.0-rc.9
+ '@vuepress/plugin-markdown-chart@2.0.0-rc.121(markdown-it@14.1.0)(mermaid@11.12.2)(vuepress@2.0.0-rc.26(@vuepress/bundler-vite@2.0.0-rc.26(@types/node@25.0.9)(sass-embedded@1.97.2)(sass@1.97.2))(vue@3.5.26))':
+ dependencies:
+ '@mdit/plugin-container': 0.22.2(markdown-it@14.1.0)
+ '@mdit/plugin-plantuml': 0.23.0(markdown-it@14.1.0)
+ '@vuepress/helper': 2.0.0-rc.121(vuepress@2.0.0-rc.26(@vuepress/bundler-vite@2.0.0-rc.26(@types/node@25.0.9)(sass-embedded@1.97.2)(sass@1.97.2))(vue@3.5.26))
+ '@vueuse/core': 14.1.0(vue@3.5.26)
+ vue: 3.5.26
+ vuepress: 2.0.0-rc.26(@vuepress/bundler-vite@2.0.0-rc.26(@types/node@25.0.9)(sass-embedded@1.97.2)(sass@1.97.2))(vue@3.5.26)
+ optionalDependencies:
+ mermaid: 11.12.2
+ transitivePeerDependencies:
+ - markdown-it
+ - typescript
+
+ '@vuepress/plugin-markdown-ext@2.0.0-rc.121(markdown-it@14.1.0)(vuepress@2.0.0-rc.26(@vuepress/bundler-vite@2.0.0-rc.26(@types/node@25.0.9)(sass-embedded@1.97.2)(sass@1.97.2))(vue@3.5.26))':
dependencies:
- vue: 3.4.21
- vuepress: 2.0.0-rc.9(@vuepress/bundler-vite@2.0.0-rc.9)(vue@3.4.21)
+ '@mdit/plugin-container': 0.22.2(markdown-it@14.1.0)
+ '@mdit/plugin-footnote': 0.22.3(markdown-it@14.1.0)
+ '@mdit/plugin-tasklist': 0.22.2(markdown-it@14.1.0)
+ '@types/markdown-it': 14.1.2
+ '@vuepress/helper': 2.0.0-rc.121(vuepress@2.0.0-rc.26(@vuepress/bundler-vite@2.0.0-rc.26(@types/node@25.0.9)(sass-embedded@1.97.2)(sass@1.97.2))(vue@3.5.26))
+ js-yaml: 4.1.1
+ vuepress: 2.0.0-rc.26(@vuepress/bundler-vite@2.0.0-rc.26(@types/node@25.0.9)(sass-embedded@1.97.2)(sass@1.97.2))(vue@3.5.26)
transitivePeerDependencies:
+ - markdown-it
- typescript
- dev: false
- /@vuepress/plugin-photo-swipe@2.0.0-rc.21(vuepress@2.0.0-rc.9):
- resolution: {integrity: sha512-2zCljlDTlSXUUeYy3Z74zkkAc6uEzV0Tos2HL89RZEhiH5tMyvNJaG4+dTVy7uODcQoe+Fcv1hovdbC6LKCFoQ==}
- peerDependencies:
- vuepress: 2.0.0-rc.9
+ '@vuepress/plugin-markdown-hint@2.0.0-rc.121(markdown-it@14.1.0)(vue@3.5.26)(vuepress@2.0.0-rc.26(@vuepress/bundler-vite@2.0.0-rc.26(@types/node@25.0.9)(sass-embedded@1.97.2)(sass@1.97.2))(vue@3.5.26))':
dependencies:
- '@vuepress/helper': 2.0.0-rc.21(vuepress@2.0.0-rc.9)
- '@vueuse/core': 10.9.0(vue@3.4.21)
- photoswipe: 5.4.3
- vue: 3.4.21
- vuepress: 2.0.0-rc.9(@vuepress/bundler-vite@2.0.0-rc.9)(vue@3.4.21)
+ '@mdit/plugin-alert': 0.22.3(markdown-it@14.1.0)
+ '@mdit/plugin-container': 0.22.2(markdown-it@14.1.0)
+ '@types/markdown-it': 14.1.2
+ '@vuepress/helper': 2.0.0-rc.121(vuepress@2.0.0-rc.26(@vuepress/bundler-vite@2.0.0-rc.26(@types/node@25.0.9)(sass-embedded@1.97.2)(sass@1.97.2))(vue@3.5.26))
+ '@vueuse/core': 14.1.0(vue@3.5.26)
+ vuepress: 2.0.0-rc.26(@vuepress/bundler-vite@2.0.0-rc.26(@types/node@25.0.9)(sass-embedded@1.97.2)(sass@1.97.2))(vue@3.5.26)
transitivePeerDependencies:
- - '@vue/composition-api'
+ - markdown-it
- typescript
- dev: false
+ - vue
- /@vuepress/plugin-prismjs@2.0.0-rc.21(vuepress@2.0.0-rc.9):
- resolution: {integrity: sha512-dMTCu/TZ1QCmTHXL4THVeh9gWzuqkJV8qhck5U77OP1qmgyf+r529A+MTOgp3ddcph1Yzb/FRb2orlefHk+yNQ==}
- peerDependencies:
- vuepress: 2.0.0-rc.9
+ '@vuepress/plugin-markdown-image@2.0.0-rc.121(markdown-it@14.1.0)(vuepress@2.0.0-rc.26(@vuepress/bundler-vite@2.0.0-rc.26(@types/node@25.0.9)(sass-embedded@1.97.2)(sass@1.97.2))(vue@3.5.26))':
dependencies:
- prismjs: 1.29.0
- vuepress: 2.0.0-rc.9(@vuepress/bundler-vite@2.0.0-rc.9)(vue@3.4.21)
- dev: false
+ '@mdit/plugin-figure': 0.22.2(markdown-it@14.1.0)
+ '@mdit/plugin-img-lazyload': 0.22.1(markdown-it@14.1.0)
+ '@mdit/plugin-img-mark': 0.22.2(markdown-it@14.1.0)
+ '@mdit/plugin-img-size': 0.22.4(markdown-it@14.1.0)
+ '@types/markdown-it': 14.1.2
+ '@vuepress/helper': 2.0.0-rc.121(vuepress@2.0.0-rc.26(@vuepress/bundler-vite@2.0.0-rc.26(@types/node@25.0.9)(sass-embedded@1.97.2)(sass@1.97.2))(vue@3.5.26))
+ vuepress: 2.0.0-rc.26(@vuepress/bundler-vite@2.0.0-rc.26(@types/node@25.0.9)(sass-embedded@1.97.2)(sass@1.97.2))(vue@3.5.26)
+ transitivePeerDependencies:
+ - markdown-it
+ - typescript
- /@vuepress/plugin-reading-time@2.0.0-rc.21(vuepress@2.0.0-rc.9):
- resolution: {integrity: sha512-8ByRTp6z4z0d9W1evfeVyOx55V9OYqxJJeC7KBiJuiA2GP8QkY9b8ajbQ8SWeCjzDLl4H0KudSU1KqH2kqw/zQ==}
- peerDependencies:
- vuepress: 2.0.0-rc.9
+ '@vuepress/plugin-markdown-include@2.0.0-rc.121(markdown-it@14.1.0)(vuepress@2.0.0-rc.26(@vuepress/bundler-vite@2.0.0-rc.26(@types/node@25.0.9)(sass-embedded@1.97.2)(sass@1.97.2))(vue@3.5.26))':
+ dependencies:
+ '@mdit/plugin-include': 0.22.3(markdown-it@14.1.0)
+ '@types/markdown-it': 14.1.2
+ '@vuepress/helper': 2.0.0-rc.121(vuepress@2.0.0-rc.26(@vuepress/bundler-vite@2.0.0-rc.26(@types/node@25.0.9)(sass-embedded@1.97.2)(sass@1.97.2))(vue@3.5.26))
+ vuepress: 2.0.0-rc.26(@vuepress/bundler-vite@2.0.0-rc.26(@types/node@25.0.9)(sass-embedded@1.97.2)(sass@1.97.2))(vue@3.5.26)
+ transitivePeerDependencies:
+ - markdown-it
+ - typescript
+
+ '@vuepress/plugin-markdown-math@2.0.0-rc.121(katex@0.16.27)(markdown-it@14.1.0)(vuepress@2.0.0-rc.26(@vuepress/bundler-vite@2.0.0-rc.26(@types/node@25.0.9)(sass-embedded@1.97.2)(sass@1.97.2))(vue@3.5.26))':
+ dependencies:
+ '@mdit/plugin-katex-slim': 0.25.1(katex@0.16.27)(markdown-it@14.1.0)
+ '@mdit/plugin-mathjax-slim': 0.24.1(markdown-it@14.1.0)
+ '@types/markdown-it': 14.1.2
+ '@vuepress/helper': 2.0.0-rc.121(vuepress@2.0.0-rc.26(@vuepress/bundler-vite@2.0.0-rc.26(@types/node@25.0.9)(sass-embedded@1.97.2)(sass@1.97.2))(vue@3.5.26))
+ vue: 3.5.26
+ vuepress: 2.0.0-rc.26(@vuepress/bundler-vite@2.0.0-rc.26(@types/node@25.0.9)(sass-embedded@1.97.2)(sass@1.97.2))(vue@3.5.26)
+ optionalDependencies:
+ katex: 0.16.27
+ transitivePeerDependencies:
+ - markdown-it
+ - typescript
+
+ '@vuepress/plugin-markdown-preview@2.0.0-rc.121(markdown-it@14.1.0)(vuepress@2.0.0-rc.26(@vuepress/bundler-vite@2.0.0-rc.26(@types/node@25.0.9)(sass-embedded@1.97.2)(sass@1.97.2))(vue@3.5.26))':
+ dependencies:
+ '@mdit/helper': 0.22.1(markdown-it@14.1.0)
+ '@mdit/plugin-demo': 0.22.3(markdown-it@14.1.0)
+ '@types/markdown-it': 14.1.2
+ '@vuepress/helper': 2.0.0-rc.121(vuepress@2.0.0-rc.26(@vuepress/bundler-vite@2.0.0-rc.26(@types/node@25.0.9)(sass-embedded@1.97.2)(sass@1.97.2))(vue@3.5.26))
+ '@vueuse/core': 14.1.0(vue@3.5.26)
+ vue: 3.5.26
+ vuepress: 2.0.0-rc.26(@vuepress/bundler-vite@2.0.0-rc.26(@types/node@25.0.9)(sass-embedded@1.97.2)(sass@1.97.2))(vue@3.5.26)
+ transitivePeerDependencies:
+ - markdown-it
+ - typescript
+
+ '@vuepress/plugin-markdown-stylize@2.0.0-rc.121(markdown-it@14.1.0)(vuepress@2.0.0-rc.26(@vuepress/bundler-vite@2.0.0-rc.26(@types/node@25.0.9)(sass-embedded@1.97.2)(sass@1.97.2))(vue@3.5.26))':
+ dependencies:
+ '@mdit/plugin-align': 0.23.0(markdown-it@14.1.0)
+ '@mdit/plugin-attrs': 0.24.1(markdown-it@14.1.0)
+ '@mdit/plugin-mark': 0.22.1(markdown-it@14.1.0)
+ '@mdit/plugin-spoiler': 0.22.2(markdown-it@14.1.0)
+ '@mdit/plugin-stylize': 0.22.3(markdown-it@14.1.0)
+ '@mdit/plugin-sub': 0.23.0(markdown-it@14.1.0)
+ '@mdit/plugin-sup': 0.23.0(markdown-it@14.1.0)
+ '@types/markdown-it': 14.1.2
+ '@vuepress/helper': 2.0.0-rc.121(vuepress@2.0.0-rc.26(@vuepress/bundler-vite@2.0.0-rc.26(@types/node@25.0.9)(sass-embedded@1.97.2)(sass@1.97.2))(vue@3.5.26))
+ vuepress: 2.0.0-rc.26(@vuepress/bundler-vite@2.0.0-rc.26(@types/node@25.0.9)(sass-embedded@1.97.2)(sass@1.97.2))(vue@3.5.26)
+ transitivePeerDependencies:
+ - markdown-it
+ - typescript
+
+ '@vuepress/plugin-markdown-tab@2.0.0-rc.121(markdown-it@14.1.0)(vuepress@2.0.0-rc.26(@vuepress/bundler-vite@2.0.0-rc.26(@types/node@25.0.9)(sass-embedded@1.97.2)(sass@1.97.2))(vue@3.5.26))':
+ dependencies:
+ '@mdit/plugin-tab': 0.23.0(markdown-it@14.1.0)
+ '@types/markdown-it': 14.1.2
+ '@vuepress/helper': 2.0.0-rc.121(vuepress@2.0.0-rc.26(@vuepress/bundler-vite@2.0.0-rc.26(@types/node@25.0.9)(sass-embedded@1.97.2)(sass@1.97.2))(vue@3.5.26))
+ '@vueuse/core': 14.1.0(vue@3.5.26)
+ vue: 3.5.26
+ vuepress: 2.0.0-rc.26(@vuepress/bundler-vite@2.0.0-rc.26(@types/node@25.0.9)(sass-embedded@1.97.2)(sass@1.97.2))(vue@3.5.26)
+ transitivePeerDependencies:
+ - markdown-it
+ - typescript
+
+ '@vuepress/plugin-notice@2.0.0-rc.121(vuepress@2.0.0-rc.26(@vuepress/bundler-vite@2.0.0-rc.26(@types/node@25.0.9)(sass-embedded@1.97.2)(sass@1.97.2))(vue@3.5.26))':
+ dependencies:
+ '@vuepress/helper': 2.0.0-rc.121(vuepress@2.0.0-rc.26(@vuepress/bundler-vite@2.0.0-rc.26(@types/node@25.0.9)(sass-embedded@1.97.2)(sass@1.97.2))(vue@3.5.26))
+ '@vueuse/core': 14.1.0(vue@3.5.26)
+ chokidar: 4.0.3
+ vue: 3.5.26
+ vuepress: 2.0.0-rc.26(@vuepress/bundler-vite@2.0.0-rc.26(@types/node@25.0.9)(sass-embedded@1.97.2)(sass@1.97.2))(vue@3.5.26)
+ transitivePeerDependencies:
+ - typescript
+
+ '@vuepress/plugin-nprogress@2.0.0-rc.121(vuepress@2.0.0-rc.26(@vuepress/bundler-vite@2.0.0-rc.26(@types/node@25.0.9)(sass-embedded@1.97.2)(sass@1.97.2))(vue@3.5.26))':
+ dependencies:
+ '@vuepress/helper': 2.0.0-rc.121(vuepress@2.0.0-rc.26(@vuepress/bundler-vite@2.0.0-rc.26(@types/node@25.0.9)(sass-embedded@1.97.2)(sass@1.97.2))(vue@3.5.26))
+ vue: 3.5.26
+ vuepress: 2.0.0-rc.26(@vuepress/bundler-vite@2.0.0-rc.26(@types/node@25.0.9)(sass-embedded@1.97.2)(sass@1.97.2))(vue@3.5.26)
+ transitivePeerDependencies:
+ - typescript
+
+ '@vuepress/plugin-photo-swipe@2.0.0-rc.121(vuepress@2.0.0-rc.26(@vuepress/bundler-vite@2.0.0-rc.26(@types/node@25.0.9)(sass-embedded@1.97.2)(sass@1.97.2))(vue@3.5.26))':
+ dependencies:
+ '@vuepress/helper': 2.0.0-rc.121(vuepress@2.0.0-rc.26(@vuepress/bundler-vite@2.0.0-rc.26(@types/node@25.0.9)(sass-embedded@1.97.2)(sass@1.97.2))(vue@3.5.26))
+ '@vueuse/core': 14.1.0(vue@3.5.26)
+ photoswipe: 5.4.4
+ vue: 3.5.26
+ vuepress: 2.0.0-rc.26(@vuepress/bundler-vite@2.0.0-rc.26(@types/node@25.0.9)(sass-embedded@1.97.2)(sass@1.97.2))(vue@3.5.26)
+ transitivePeerDependencies:
+ - typescript
+
+ '@vuepress/plugin-reading-time@2.0.0-rc.121(vuepress@2.0.0-rc.26(@vuepress/bundler-vite@2.0.0-rc.26(@types/node@25.0.9)(sass-embedded@1.97.2)(sass@1.97.2))(vue@3.5.26))':
+ dependencies:
+ '@vuepress/helper': 2.0.0-rc.121(vuepress@2.0.0-rc.26(@vuepress/bundler-vite@2.0.0-rc.26(@types/node@25.0.9)(sass-embedded@1.97.2)(sass@1.97.2))(vue@3.5.26))
+ vue: 3.5.26
+ vuepress: 2.0.0-rc.26(@vuepress/bundler-vite@2.0.0-rc.26(@types/node@25.0.9)(sass-embedded@1.97.2)(sass@1.97.2))(vue@3.5.26)
+ transitivePeerDependencies:
+ - typescript
+
+ '@vuepress/plugin-redirect@2.0.0-rc.121(vuepress@2.0.0-rc.26(@vuepress/bundler-vite@2.0.0-rc.26(@types/node@25.0.9)(sass-embedded@1.97.2)(sass@1.97.2))(vue@3.5.26))':
+ dependencies:
+ '@vuepress/helper': 2.0.0-rc.121(vuepress@2.0.0-rc.26(@vuepress/bundler-vite@2.0.0-rc.26(@types/node@25.0.9)(sass-embedded@1.97.2)(sass@1.97.2))(vue@3.5.26))
+ '@vueuse/core': 14.1.0(vue@3.5.26)
+ commander: 14.0.2
+ vue: 3.5.26
+ vuepress: 2.0.0-rc.26(@vuepress/bundler-vite@2.0.0-rc.26(@types/node@25.0.9)(sass-embedded@1.97.2)(sass@1.97.2))(vue@3.5.26)
+ transitivePeerDependencies:
+ - typescript
+
+ '@vuepress/plugin-rtl@2.0.0-rc.121(vuepress@2.0.0-rc.26(@vuepress/bundler-vite@2.0.0-rc.26(@types/node@25.0.9)(sass-embedded@1.97.2)(sass@1.97.2))(vue@3.5.26))':
+ dependencies:
+ '@vuepress/helper': 2.0.0-rc.121(vuepress@2.0.0-rc.26(@vuepress/bundler-vite@2.0.0-rc.26(@types/node@25.0.9)(sass-embedded@1.97.2)(sass@1.97.2))(vue@3.5.26))
+ '@vueuse/core': 14.1.0(vue@3.5.26)
+ vue: 3.5.26
+ vuepress: 2.0.0-rc.26(@vuepress/bundler-vite@2.0.0-rc.26(@types/node@25.0.9)(sass-embedded@1.97.2)(sass@1.97.2))(vue@3.5.26)
+ transitivePeerDependencies:
+ - typescript
+
+ '@vuepress/plugin-sass-palette@2.0.0-rc.121(sass-embedded@1.97.2)(sass@1.97.2)(vuepress@2.0.0-rc.26(@vuepress/bundler-vite@2.0.0-rc.26(@types/node@25.0.9)(sass-embedded@1.97.2)(sass@1.97.2))(vue@3.5.26))':
+ dependencies:
+ '@vuepress/helper': 2.0.0-rc.121(vuepress@2.0.0-rc.26(@vuepress/bundler-vite@2.0.0-rc.26(@types/node@25.0.9)(sass-embedded@1.97.2)(sass@1.97.2))(vue@3.5.26))
+ chokidar: 4.0.3
+ vuepress: 2.0.0-rc.26(@vuepress/bundler-vite@2.0.0-rc.26(@types/node@25.0.9)(sass-embedded@1.97.2)(sass@1.97.2))(vue@3.5.26)
+ optionalDependencies:
+ sass: 1.97.2
+ sass-embedded: 1.97.2
+ transitivePeerDependencies:
+ - typescript
+
+ '@vuepress/plugin-search@2.0.0-rc.121(vuepress@2.0.0-rc.26(@vuepress/bundler-vite@2.0.0-rc.26(@types/node@25.0.9)(sass-embedded@1.97.2)(sass@1.97.2))(vue@3.5.26))':
+ dependencies:
+ '@vuepress/helper': 2.0.0-rc.121(vuepress@2.0.0-rc.26(@vuepress/bundler-vite@2.0.0-rc.26(@types/node@25.0.9)(sass-embedded@1.97.2)(sass@1.97.2))(vue@3.5.26))
+ chokidar: 4.0.3
+ vue: 3.5.26
+ vuepress: 2.0.0-rc.26(@vuepress/bundler-vite@2.0.0-rc.26(@types/node@25.0.9)(sass-embedded@1.97.2)(sass@1.97.2))(vue@3.5.26)
+ transitivePeerDependencies:
+ - typescript
+
+ '@vuepress/plugin-seo@2.0.0-rc.121(vuepress@2.0.0-rc.26(@vuepress/bundler-vite@2.0.0-rc.26(@types/node@25.0.9)(sass-embedded@1.97.2)(sass@1.97.2))(vue@3.5.26))':
+ dependencies:
+ '@vuepress/helper': 2.0.0-rc.121(vuepress@2.0.0-rc.26(@vuepress/bundler-vite@2.0.0-rc.26(@types/node@25.0.9)(sass-embedded@1.97.2)(sass@1.97.2))(vue@3.5.26))
+ vuepress: 2.0.0-rc.26(@vuepress/bundler-vite@2.0.0-rc.26(@types/node@25.0.9)(sass-embedded@1.97.2)(sass@1.97.2))(vue@3.5.26)
+ transitivePeerDependencies:
+ - typescript
+
+ '@vuepress/plugin-shiki@2.0.0-rc.121(@vueuse/core@14.1.0(vue@3.5.26))(vuepress@2.0.0-rc.26(@vuepress/bundler-vite@2.0.0-rc.26(@types/node@25.0.9)(sass-embedded@1.97.2)(sass@1.97.2))(vue@3.5.26))':
+ dependencies:
+ '@shikijs/transformers': 3.21.0
+ '@vuepress/helper': 2.0.0-rc.121(vuepress@2.0.0-rc.26(@vuepress/bundler-vite@2.0.0-rc.26(@types/node@25.0.9)(sass-embedded@1.97.2)(sass@1.97.2))(vue@3.5.26))
+ '@vuepress/highlighter-helper': 2.0.0-rc.118(@vueuse/core@14.1.0(vue@3.5.26))(vuepress@2.0.0-rc.26(@vuepress/bundler-vite@2.0.0-rc.26(@types/node@25.0.9)(sass-embedded@1.97.2)(sass@1.97.2))(vue@3.5.26))
+ nanoid: 5.1.6
+ shiki: 3.21.0
+ synckit: 0.11.12
+ vuepress: 2.0.0-rc.26(@vuepress/bundler-vite@2.0.0-rc.26(@types/node@25.0.9)(sass-embedded@1.97.2)(sass@1.97.2))(vue@3.5.26)
+ transitivePeerDependencies:
+ - '@vueuse/core'
+ - typescript
+
+ '@vuepress/plugin-sitemap@2.0.0-rc.121(vuepress@2.0.0-rc.26(@vuepress/bundler-vite@2.0.0-rc.26(@types/node@25.0.9)(sass-embedded@1.97.2)(sass@1.97.2))(vue@3.5.26))':
dependencies:
- '@vuepress/helper': 2.0.0-rc.21(vuepress@2.0.0-rc.9)
- vue: 3.4.21
- vuepress: 2.0.0-rc.9(@vuepress/bundler-vite@2.0.0-rc.9)(vue@3.4.21)
+ '@vuepress/helper': 2.0.0-rc.121(vuepress@2.0.0-rc.26(@vuepress/bundler-vite@2.0.0-rc.26(@types/node@25.0.9)(sass-embedded@1.97.2)(sass@1.97.2))(vue@3.5.26))
+ sitemap: 9.0.0
+ vuepress: 2.0.0-rc.26(@vuepress/bundler-vite@2.0.0-rc.26(@types/node@25.0.9)(sass-embedded@1.97.2)(sass@1.97.2))(vue@3.5.26)
transitivePeerDependencies:
- typescript
- dev: false
- /@vuepress/plugin-rtl@2.0.0-rc.21(vuepress@2.0.0-rc.9):
- resolution: {integrity: sha512-r+4aP898KsFbF6m1J0e+776ZlSE9yaHr9zsMlib1GEUDcqP/OykMYaNKwRsOMB1eFXNmymgHlXFvswBGEHxS7w==}
- peerDependencies:
- vuepress: 2.0.0-rc.9
+ '@vuepress/plugin-theme-data@2.0.0-rc.120(vuepress@2.0.0-rc.26(@vuepress/bundler-vite@2.0.0-rc.26(@types/node@25.0.9)(sass-embedded@1.97.2)(sass@1.97.2))(vue@3.5.26))':
+ dependencies:
+ '@vue/devtools-api': 8.0.5
+ vue: 3.5.26
+ vuepress: 2.0.0-rc.26(@vuepress/bundler-vite@2.0.0-rc.26(@types/node@25.0.9)(sass-embedded@1.97.2)(sass@1.97.2))(vue@3.5.26)
+ transitivePeerDependencies:
+ - typescript
+
+ '@vuepress/shared@2.0.0-rc.26':
+ dependencies:
+ '@mdit-vue/types': 3.0.2
+
+ '@vuepress/utils@2.0.0-rc.26':
+ dependencies:
+ '@types/debug': 4.1.12
+ '@types/fs-extra': 11.0.4
+ '@types/hash-sum': 1.0.2
+ '@types/picomatch': 4.0.2
+ '@vuepress/shared': 2.0.0-rc.26
+ debug: 4.4.3
+ fs-extra: 11.3.3
+ hash-sum: 2.0.0
+ ora: 9.0.0
+ picocolors: 1.1.1
+ picomatch: 4.0.3
+ tinyglobby: 0.2.15
+ upath: 2.0.1
+ transitivePeerDependencies:
+ - supports-color
+
+ '@vueuse/core@14.1.0(vue@3.5.26)':
+ dependencies:
+ '@types/web-bluetooth': 0.0.21
+ '@vueuse/metadata': 14.1.0
+ '@vueuse/shared': 14.1.0(vue@3.5.26)
+ vue: 3.5.26
+
+ '@vueuse/metadata@14.1.0': {}
+
+ '@vueuse/shared@14.1.0(vue@3.5.26)':
+ dependencies:
+ vue: 3.5.26
+
+ '@xmldom/xmldom@0.9.8': {}
+
+ acorn@8.15.0: {}
+
+ ansi-regex@5.0.1: {}
+
+ ansi-regex@6.2.2: {}
+
+ ansi-styles@4.3.0:
+ dependencies:
+ color-convert: 2.0.1
+
+ arg@5.0.2: {}
+
+ argparse@1.0.10:
+ dependencies:
+ sprintf-js: 1.0.3
+
+ argparse@2.0.1: {}
+
+ autoprefixer@10.4.23(postcss@8.5.6):
+ dependencies:
+ browserslist: 4.28.1
+ caniuse-lite: 1.0.30001764
+ fraction.js: 5.3.4
+ picocolors: 1.1.1
+ postcss: 8.5.6
+ postcss-value-parser: 4.2.0
+
+ bail@2.0.2: {}
+
+ balloon-css@1.2.0: {}
+
+ baseline-browser-mapping@2.9.14: {}
+
+ bcrypt-ts@8.0.0: {}
+
+ birpc@2.9.0: {}
+
+ boolbase@1.0.0: {}
+
+ braces@3.0.3:
+ dependencies:
+ fill-range: 7.1.1
+
+ browserslist@4.28.1:
+ dependencies:
+ baseline-browser-mapping: 2.9.14
+ caniuse-lite: 1.0.30001764
+ electron-to-chromium: 1.5.267
+ node-releases: 2.0.27
+ update-browserslist-db: 1.2.3(browserslist@4.28.1)
+
+ buffer-builder@0.2.0: {}
+
+ cac@6.7.14: {}
+
+ camelcase@5.3.1: {}
+
+ caniuse-lite@1.0.30001764: {}
+
+ ccount@2.0.1: {}
+
+ chalk@5.6.2: {}
+
+ character-entities-html4@2.1.0: {}
+
+ character-entities-legacy@3.0.0: {}
+
+ character-entities@2.0.2: {}
+
+ character-reference-invalid@2.0.1: {}
+
+ cheerio-select@2.1.0:
+ dependencies:
+ boolbase: 1.0.0
+ css-select: 5.2.2
+ css-what: 6.2.2
+ domelementtype: 2.3.0
+ domhandler: 5.0.3
+ domutils: 3.2.2
+
+ cheerio@1.1.2:
+ dependencies:
+ cheerio-select: 2.1.0
+ dom-serializer: 2.0.0
+ domhandler: 5.0.3
+ domutils: 3.2.2
+ encoding-sniffer: 0.2.1
+ htmlparser2: 10.0.0
+ parse5: 7.3.0
+ parse5-htmlparser2-tree-adapter: 7.1.0
+ parse5-parser-stream: 7.1.2
+ undici: 7.18.2
+ whatwg-mimetype: 4.0.0
+
+ chevrotain-allstar@0.3.1(chevrotain@11.0.3):
dependencies:
- vue: 3.4.21
- vuepress: 2.0.0-rc.9(@vuepress/bundler-vite@2.0.0-rc.9)(vue@3.4.21)
- transitivePeerDependencies:
- - typescript
- dev: false
+ chevrotain: 11.0.3
+ lodash-es: 4.17.22
- /@vuepress/plugin-search@2.0.0-rc.21(vuepress@2.0.0-rc.9):
- resolution: {integrity: sha512-Dci/gN0ScbQXOKIB2JnUv8x4Ea+0wmU3xKe1IalyKdyg7cnayNqbmXmpqd32p4tlRXcFj9qhipa6AxvdhPcXqA==}
- peerDependencies:
- vuepress: 2.0.0-rc.9
+ chevrotain@11.0.3:
dependencies:
- chokidar: 3.6.0
- vue: 3.4.21
- vuepress: 2.0.0-rc.9(@vuepress/bundler-vite@2.0.0-rc.9)(vue@3.4.21)
- transitivePeerDependencies:
- - typescript
- dev: false
+ '@chevrotain/cst-dts-gen': 11.0.3
+ '@chevrotain/gast': 11.0.3
+ '@chevrotain/regexp-to-ast': 11.0.3
+ '@chevrotain/types': 11.0.3
+ '@chevrotain/utils': 11.0.3
+ lodash-es: 4.17.21
- /@vuepress/plugin-seo@2.0.0-rc.21(vuepress@2.0.0-rc.9):
- resolution: {integrity: sha512-q8pXhXn5OL0QG6KN9rjyXngj2km5eRDK0VL8ShLrTD9fAwvjhujhjHpI/DRHg6ScWlMDKY7ncEOmslDCBuKLtg==}
- peerDependencies:
- vuepress: 2.0.0-rc.9
+ chokidar@4.0.3:
dependencies:
- '@vuepress/helper': 2.0.0-rc.21(vuepress@2.0.0-rc.9)
- vuepress: 2.0.0-rc.9(@vuepress/bundler-vite@2.0.0-rc.9)(vue@3.4.21)
- transitivePeerDependencies:
- - typescript
- dev: false
+ readdirp: 4.1.2
- /@vuepress/plugin-sitemap@2.0.0-rc.21(vuepress@2.0.0-rc.9):
- resolution: {integrity: sha512-YbotKptHfifjwmXhj4kX6iA8tCGp7gTZAHm9YiPDr/8dYzBkkQ4oC84JCifkZYt3fWkVqq/Qa0vpJfnKPGOidg==}
- peerDependencies:
- vuepress: 2.0.0-rc.9
+ chokidar@5.0.0:
dependencies:
- '@vuepress/helper': 2.0.0-rc.21(vuepress@2.0.0-rc.9)
- sitemap: 7.1.1
- vuepress: 2.0.0-rc.9(@vuepress/bundler-vite@2.0.0-rc.9)(vue@3.4.21)
- transitivePeerDependencies:
- - typescript
- dev: false
+ readdirp: 5.0.0
- /@vuepress/plugin-theme-data@2.0.0-rc.21(vuepress@2.0.0-rc.9):
- resolution: {integrity: sha512-vLXvTKx4gWXY6oVaJ9Z2ECnojnKQuXBIe1ZGIAwJdxCYfr6aaqggrVvmphB8BwTURh0XAuis/l6YTcMrs0bX8Q==}
- peerDependencies:
- vuepress: 2.0.0-rc.9
+ cli-cursor@5.0.0:
dependencies:
- '@vue/devtools-api': 6.6.1
- vue: 3.4.21
- vuepress: 2.0.0-rc.9(@vuepress/bundler-vite@2.0.0-rc.9)(vue@3.4.21)
- transitivePeerDependencies:
- - typescript
- dev: false
+ restore-cursor: 5.1.0
- /@vuepress/shared@2.0.0-rc.9:
- resolution: {integrity: sha512-XfI6CWNv4/Vp9Iew6GJil9RUSy1rM7zGdjwikr0j3Rkh55q3f00w1wud47wE9kxRqsZ0PIvsMget5CxEn5rA/w==}
- dependencies:
- '@mdit-vue/types': 2.0.0
- dev: false
+ cli-spinners@3.4.0: {}
- /@vuepress/utils@2.0.0-rc.9:
- resolution: {integrity: sha512-qk6Pel4JVKYKxp3bWxyvnwchvx3QaCWc7SqUw7L6qUo/um+0U2U45L0anWoAfckw12RXYhoIEbJ9UZpueiKOPg==}
+ cliui@6.0.0:
dependencies:
- '@types/debug': 4.1.12
- '@types/fs-extra': 11.0.4
- '@types/hash-sum': 1.0.2
- '@vuepress/shared': 2.0.0-rc.9
- debug: 4.3.4
- fs-extra: 11.2.0
- globby: 14.0.1
- hash-sum: 2.0.0
- ora: 8.0.1
- picocolors: 1.0.0
- upath: 2.0.1
- transitivePeerDependencies:
- - supports-color
- dev: false
+ string-width: 4.2.3
+ strip-ansi: 6.0.1
+ wrap-ansi: 6.2.0
- /@vueuse/core@10.9.0(vue@3.4.21):
- resolution: {integrity: sha512-/1vjTol8SXnx6xewDEKfS0Ra//ncg4Hb0DaZiwKf7drgfMsKFExQ+FnnENcN6efPen+1kIzhLQoGSy0eDUVOMg==}
+ color-convert@2.0.1:
dependencies:
- '@types/web-bluetooth': 0.0.20
- '@vueuse/metadata': 10.9.0
- '@vueuse/shared': 10.9.0(vue@3.4.21)
- vue-demi: 0.14.7(vue@3.4.21)
- transitivePeerDependencies:
- - '@vue/composition-api'
- - vue
- dev: false
+ color-name: 1.1.4
- /@vueuse/metadata@10.9.0:
- resolution: {integrity: sha512-iddNbg3yZM0X7qFY2sAotomgdHK7YJ6sKUvQqbvwnf7TmaVPxS4EJydcNsVejNdS8iWCtDk+fYXr7E32nyTnGA==}
- dev: false
+ color-name@1.1.4: {}
- /@vueuse/shared@10.9.0(vue@3.4.21):
- resolution: {integrity: sha512-Uud2IWncmAfJvRaFYzv5OHDli+FbOzxiVEQdLCKQKLyhz94PIyFC3CHcH7EDMwIn8NPtD06+PNbC/PiO0LGLtw==}
- dependencies:
- vue-demi: 0.14.7(vue@3.4.21)
- transitivePeerDependencies:
- - '@vue/composition-api'
- - vue
- dev: false
+ colorjs.io@0.5.2: {}
- /abbrev@1.1.1:
- resolution: {integrity: sha512-nne9/IiQ/hzIhY6pdDnbBtz7DjPTKrY00P/zvPSm5pOFkl6xuGrGnXn/VtTNNfNtAfZ9/1RtehkszU9qcTii0Q==}
- dev: false
+ comma-separated-tokens@2.0.3: {}
- /abbrev@2.0.0:
- resolution: {integrity: sha512-6/mh1E2u2YgEsCHdY0Yx5oW+61gZU+1vXaoiHHrpKeuRNNgFvS+/jrwHiQhB5apAf5oB7UB7E19ol2R2LKH8hQ==}
- engines: {node: ^14.17.0 || ^16.13.0 || >=18.0.0}
- dev: false
+ commander@13.1.0: {}
- /agent-base@6.0.2:
- resolution: {integrity: sha512-RZNwNclF7+MS/8bDg70amg32dyeZGZxiDuQmZxKLAlQjr3jGyLx+4Kkk58UO7D2QdgFIQCovuSuZESne6RG6XQ==}
- engines: {node: '>= 6.0.0'}
- dependencies:
- debug: 4.3.4
- transitivePeerDependencies:
- - supports-color
- dev: false
+ commander@14.0.2: {}
- /agent-base@7.1.1:
- resolution: {integrity: sha512-H0TSyFNDMomMNJQBn8wFV5YC/2eJ+VXECwOadZJT554xP6cODZHPX3H9QMQECxvrgiSOP1pHjy1sMWQVYJOUOA==}
- engines: {node: '>= 14'}
- dependencies:
- debug: 4.3.4
- transitivePeerDependencies:
- - supports-color
- dev: false
+ commander@7.2.0: {}
- /aggregate-error@3.1.0:
- resolution: {integrity: sha512-4I7Td01quW/RpocfNayFdFVk1qSuoh0E7JrbRJ16nH01HhKFQ88INq9Sd+nd72zqRySlr9BmDA8xlEJ6vJMrYA==}
- engines: {node: '>=8'}
- dependencies:
- clean-stack: 2.2.0
- indent-string: 4.0.0
- dev: false
+ commander@8.3.0: {}
- /ansi-regex@5.0.1:
- resolution: {integrity: sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==}
- engines: {node: '>=8'}
- dev: false
+ confbox@0.1.8: {}
- /ansi-regex@6.0.1:
- resolution: {integrity: sha512-n5M855fKb2SsfMIiFFoVrABHJC8QtHwVx+mHWP3QcEqBHYienj5dHSgjbxtC0WEZXYt4wcD6zrQElDPhFuZgfA==}
- engines: {node: '>=12'}
- dev: false
+ connect-history-api-fallback@2.0.0: {}
- /ansi-styles@4.3.0:
- resolution: {integrity: sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==}
- engines: {node: '>=8'}
+ copy-anything@4.0.5:
dependencies:
- color-convert: 2.0.1
- dev: false
+ is-what: 5.5.0
- /ansi-styles@6.2.1:
- resolution: {integrity: sha512-bN798gFfQX+viw3R7yrGWRqnrN2oRkEkUjjl4JNn4E8GxxbjtG3FbrEIIY3l8/hrwUwIeCZvi4QuOTP4MErVug==}
- engines: {node: '>=12'}
- dev: false
+ cose-base@1.0.3:
+ dependencies:
+ layout-base: 1.0.2
- /anymatch@3.1.3:
- resolution: {integrity: sha512-KMReFUr0B4t+D+OBkjR3KYqvocp2XaSzO55UcB6mgQMd3KbcE+mWTyvVV7D/zsdEbNnV6acZUutkiHQXvTr1Rw==}
- engines: {node: '>= 8'}
+ cose-base@2.2.0:
dependencies:
- normalize-path: 3.0.0
- picomatch: 2.3.1
- dev: false
+ layout-base: 2.0.1
- /aproba@2.0.0:
- resolution: {integrity: sha512-lYe4Gx7QT+MKGbDsA+Z+he/Wtef0BiwDOlK/XkBrdfsh9J/jPPXbX0tE9x9cl27Tmu5gg3QUbUrQYa/y+KOHPQ==}
- dev: false
+ create-codepen@2.0.0: {}
- /are-we-there-yet@2.0.0:
- resolution: {integrity: sha512-Ci/qENmwHnsYo9xKIcUJN5LeDKdJ6R1Z1j9V/J5wyq8nh/mYPEpIKJbBZXtZjG04HiK7zV/p6Vs9952MrMeUIw==}
- engines: {node: '>=10'}
+ css-select@5.2.2:
dependencies:
- delegates: 1.0.0
- readable-stream: 3.6.2
- dev: false
+ boolbase: 1.0.0
+ css-what: 6.2.2
+ domhandler: 5.0.3
+ domutils: 3.2.2
+ nth-check: 2.1.1
- /arg@5.0.2:
- resolution: {integrity: sha512-PYjyFOLKQ9y57JvQ6QLo8dAgNqswh8M1RMJYdQduT6xbWSgK36P/Z/v+p888pM69jMMfS8Xd8F6I1kQ/I9HUGg==}
- dev: false
+ css-what@6.2.2: {}
- /argparse@1.0.10:
- resolution: {integrity: sha512-o5Roy6tNG4SL/FOkCAN6RzjiakZS25RLYFrcMttJqbdd8BWrnA+fGz57iN5Pb06pvBGvl5gQ0B48dJlslXvoTg==}
+ csstype@3.2.3: {}
+
+ cytoscape-cose-bilkent@4.1.0(cytoscape@3.33.1):
dependencies:
- sprintf-js: 1.0.3
- dev: false
+ cose-base: 1.0.3
+ cytoscape: 3.33.1
- /argparse@2.0.1:
- resolution: {integrity: sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q==}
- dev: false
+ cytoscape-fcose@2.2.0(cytoscape@3.33.1):
+ dependencies:
+ cose-base: 2.2.0
+ cytoscape: 3.33.1
- /autoprefixer@10.4.19(postcss@8.4.38):
- resolution: {integrity: sha512-BaENR2+zBZ8xXhM4pUaKUxlVdxZ0EZhjvbopwnXmxRUfqDmwSpC2lAi/QXvx7NRdPCo1WKEcEF6mV64si1z4Ew==}
- engines: {node: ^10 || ^12 || >=14}
- hasBin: true
- peerDependencies:
- postcss: ^8.1.0
+ cytoscape@3.33.1: {}
+
+ d3-array@2.12.1:
dependencies:
- browserslist: 4.23.0
- caniuse-lite: 1.0.30001603
- fraction.js: 4.3.7
- normalize-range: 0.1.2
- picocolors: 1.0.0
- postcss: 8.4.38
- postcss-value-parser: 4.2.0
- dev: false
+ internmap: 1.0.1
- /balanced-match@1.0.2:
- resolution: {integrity: sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==}
- dev: false
+ d3-array@3.2.4:
+ dependencies:
+ internmap: 2.0.3
- /balloon-css@1.2.0:
- resolution: {integrity: sha512-urXwkHgwp6GsXVF+it01485Z2Cj4pnW02ICnM0TemOlkKmCNnDLmyy+ZZiRXBpwldUXO+aRNr7Hdia4CBvXJ5A==}
- dev: false
+ d3-axis@3.0.0: {}
- /bcrypt-ts@5.0.2:
- resolution: {integrity: sha512-gDwQ5784AkkfhHACh3jGcg1hUubyZyeq9AtVd5gXkcyHGVOC+mORjRIHSj+fHfqwY5vxwyBLXQpcfk8MpK0ROg==}
- engines: {node: '>=18'}
- dev: false
+ d3-brush@3.0.0:
+ dependencies:
+ d3-dispatch: 3.0.1
+ d3-drag: 3.0.0
+ d3-interpolate: 3.0.1
+ d3-selection: 3.0.0
+ d3-transition: 3.0.1(d3-selection@3.0.0)
- /binary-extensions@2.3.0:
- resolution: {integrity: sha512-Ceh+7ox5qe7LJuLHoY0feh3pHuUDHAcRUeyL2VYghZwfpkNIy/+8Ocg0a3UuSoYzavmylwuLWQOf3hl0jjMMIw==}
- engines: {node: '>=8'}
- dev: false
+ d3-chord@3.0.1:
+ dependencies:
+ d3-path: 3.1.0
- /boolbase@1.0.0:
- resolution: {integrity: sha512-JZOSA7Mo9sNGB8+UjSgzdLtokWAky1zbztM3WRLCbZ70/3cTANmQmOdR7y2g+J0e2WXywy1yS468tY+IruqEww==}
- dev: false
+ d3-color@3.1.0: {}
- /brace-expansion@1.1.11:
- resolution: {integrity: sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA==}
+ d3-contour@4.0.2:
dependencies:
- balanced-match: 1.0.2
- concat-map: 0.0.1
- dev: false
+ d3-array: 3.2.4
- /brace-expansion@2.0.1:
- resolution: {integrity: sha512-XnAIvQ8eM+kC6aULx6wuQiwVsnzsi9d3WxzV3FpWTGA19F621kwdbsAcFKXgKUHZWsy+mY6iL1sHTxWEFCytDA==}
+ d3-delaunay@6.0.4:
dependencies:
- balanced-match: 1.0.2
- dev: false
+ delaunator: 5.0.1
- /braces@3.0.2:
- resolution: {integrity: sha512-b8um+L1RzM3WDSzvhm6gIz1yfTbBt6YTlcEKAvsmqCZZFw46z626lVj9j1yEPW33H5H+lBQpZMP1k8l+78Ha0A==}
- engines: {node: '>=8'}
+ d3-dispatch@3.0.1: {}
+
+ d3-drag@3.0.0:
dependencies:
- fill-range: 7.0.1
- dev: false
+ d3-dispatch: 3.0.1
+ d3-selection: 3.0.0
- /browserslist@4.23.0:
- resolution: {integrity: sha512-QW8HiM1shhT2GuzkvklfjcKDiWFXHOeFCIA/huJPwHsslwcydgk7X+z2zXpEijP98UCY7HbubZt5J2Zgvf0CaQ==}
- engines: {node: ^6 || ^7 || ^8 || ^9 || ^10 || ^11 || ^12 || >=13.7}
- hasBin: true
+ d3-dsv@3.0.1:
dependencies:
- caniuse-lite: 1.0.30001603
- electron-to-chromium: 1.4.722
- node-releases: 2.0.14
- update-browserslist-db: 1.0.13(browserslist@4.23.0)
- dev: false
+ commander: 7.2.0
+ iconv-lite: 0.6.3
+ rw: 1.3.3
- /cac@6.7.14:
- resolution: {integrity: sha512-b6Ilus+c3RrdDk+JhLKUAQfzzgLEPy6wcXqS7f/xe1EETvsDP6GORG7SFuOs6cID5YkqchW/LXZbX5bc8j7ZcQ==}
- engines: {node: '>=8'}
- dev: false
-
- /cacache@18.0.2:
- resolution: {integrity: sha512-r3NU8h/P+4lVUHfeRw1dtgQYar3DZMm4/cm2bZgOvrFC/su7budSOeqh52VJIC4U4iG1WWwV6vRW0znqBvxNuw==}
- engines: {node: ^16.14.0 || >=18.0.0}
- dependencies:
- '@npmcli/fs': 3.1.0
- fs-minipass: 3.0.3
- glob: 10.3.12
- lru-cache: 10.2.0
- minipass: 7.0.4
- minipass-collect: 2.0.1
- minipass-flush: 1.0.5
- minipass-pipeline: 1.2.4
- p-map: 4.0.0
- ssri: 10.0.5
- tar: 6.2.1
- unique-filename: 3.0.0
- dev: false
-
- /camelcase@5.3.1:
- resolution: {integrity: sha512-L28STB170nwWS63UjtlEOE3dldQApaJXZkOI1uMFfzf3rRuPegHaHesyee+YxQ+W6SvRDQV6UrdOdRiR153wJg==}
- engines: {node: '>=6'}
- dev: false
+ d3-ease@3.0.1: {}
+
+ d3-fetch@3.0.1:
+ dependencies:
+ d3-dsv: 3.0.1
- /caniuse-lite@1.0.30001603:
- resolution: {integrity: sha512-iL2iSS0eDILMb9n5yKQoTBim9jMZ0Yrk8g0N9K7UzYyWnfIKzXBZD5ngpM37ZcL/cv0Mli8XtVMRYMQAfFpi5Q==}
- dev: false
+ d3-force@3.0.0:
+ dependencies:
+ d3-dispatch: 3.0.1
+ d3-quadtree: 3.0.1
+ d3-timer: 3.0.1
- /chalk@5.3.0:
- resolution: {integrity: sha512-dLitG79d+GV1Nb/VYcCDFivJeK1hiukt9QjRNVOsUtTy1rR1YJsmpGGTZ3qJos+uw7WmWF4wUwBd9jxjocFC2w==}
- engines: {node: ^12.17.0 || ^14.13 || >=16.0.0}
- dev: false
+ d3-format@3.1.2: {}
- /cheerio-select@2.1.0:
- resolution: {integrity: sha512-9v9kG0LvzrlcungtnJtpGNxY+fzECQKhK4EGJX2vByejiMX84MFNQw4UxPJl3bFbTMw+Dfs37XaIkCwTZfLh4g==}
+ d3-geo@3.1.1:
dependencies:
- boolbase: 1.0.0
- css-select: 5.1.0
- css-what: 6.1.0
- domelementtype: 2.3.0
- domhandler: 5.0.3
- domutils: 3.1.0
- dev: false
+ d3-array: 3.2.4
- /cheerio@1.0.0-rc.12:
- resolution: {integrity: sha512-VqR8m68vM46BNnuZ5NtnGBKIE/DfN0cRIzg9n40EIq9NOv90ayxLBXA8fXC5gquFRGJSTRqBq25Jt2ECLR431Q==}
- engines: {node: '>= 6'}
+ d3-hierarchy@3.1.2: {}
+
+ d3-interpolate@3.0.1:
dependencies:
- cheerio-select: 2.1.0
- dom-serializer: 2.0.0
- domhandler: 5.0.3
- domutils: 3.1.0
- htmlparser2: 8.0.2
- parse5: 7.1.2
- parse5-htmlparser2-tree-adapter: 7.0.0
- dev: false
-
- /chokidar@3.6.0:
- resolution: {integrity: sha512-7VT13fmjotKpGipCW9JEQAusEPE+Ei8nl6/g4FBAmIm0GOOLMua9NDDo/DWp0ZAxCr3cPq5ZpBqmPAQgDda2Pw==}
- engines: {node: '>= 8.10.0'}
- dependencies:
- anymatch: 3.1.3
- braces: 3.0.2
- glob-parent: 5.1.2
- is-binary-path: 2.1.0
- is-glob: 4.0.3
- normalize-path: 3.0.0
- readdirp: 3.6.0
- optionalDependencies:
- fsevents: 2.3.3
- dev: false
+ d3-color: 3.1.0
- /chownr@2.0.0:
- resolution: {integrity: sha512-bIomtDF5KGpdogkLd9VspvFzk9KfpyyGlS8YFVZl7TGPBHL5snIOnxeshwVgPteQ9b4Eydl+pVbIyE1DcvCWgQ==}
- engines: {node: '>=10'}
- dev: false
+ d3-path@1.0.9: {}
- /clean-stack@2.2.0:
- resolution: {integrity: sha512-4diC9HaTE+KRAMWhDhrGOECgWZxoevMc5TlkObMqNSsVU62PYzXZ/SMTjzyGAFF1YusgxGcSWTEXBhp0CPwQ1A==}
- engines: {node: '>=6'}
- dev: false
+ d3-path@3.1.0: {}
- /cli-cursor@4.0.0:
- resolution: {integrity: sha512-VGtlMu3x/4DOtIUwEkRezxUZ2lBacNJCHash0N0WeZDBS+7Ux1dm3XWAgWYxLJFMMdOeXMHXorshEFhbMSGelg==}
- engines: {node: ^12.20.0 || ^14.13.1 || >=16.0.0}
- dependencies:
- restore-cursor: 4.0.0
- dev: false
+ d3-polygon@3.0.1: {}
- /cli-spinners@2.9.2:
- resolution: {integrity: sha512-ywqV+5MmyL4E7ybXgKys4DugZbX0FC6LnwrhjuykIjnK9k8OQacQ7axGKnjDXWNhns0xot3bZI5h55H8yo9cJg==}
- engines: {node: '>=6'}
- dev: false
+ d3-quadtree@3.0.1: {}
- /cliui@6.0.0:
- resolution: {integrity: sha512-t6wbgtoCXvAzst7QgXxJYqPt0usEfbgQdftEPbLL/cvv6HPE5VgvqCuAIDR0NgU52ds6rFwqrgakNLrHEjCbrQ==}
+ d3-random@3.0.1: {}
+
+ d3-sankey@0.12.3:
dependencies:
- string-width: 4.2.3
- strip-ansi: 6.0.1
- wrap-ansi: 6.2.0
- dev: false
+ d3-array: 2.12.1
+ d3-shape: 1.3.7
- /color-convert@2.0.1:
- resolution: {integrity: sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==}
- engines: {node: '>=7.0.0'}
+ d3-scale-chromatic@3.1.0:
dependencies:
- color-name: 1.1.4
- dev: false
+ d3-color: 3.1.0
+ d3-interpolate: 3.0.1
- /color-name@1.1.4:
- resolution: {integrity: sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==}
- dev: false
+ d3-scale@4.0.2:
+ dependencies:
+ d3-array: 3.2.4
+ d3-format: 3.1.2
+ d3-interpolate: 3.0.1
+ d3-time: 3.1.0
+ d3-time-format: 4.1.0
- /color-support@1.1.3:
- resolution: {integrity: sha512-qiBjkpbMLO/HL68y+lh4q0/O1MZFj2RX6X/KmMa3+gJD3z+WwI1ZzDHysvqHGS3mP6mznPckpXmw1nI9cJjyRg==}
- hasBin: true
- dev: false
+ d3-selection@3.0.0: {}
- /commander@9.2.0:
- resolution: {integrity: sha512-e2i4wANQiSXgnrBlIatyHtP1odfUp0BbV5Y5nEGbxtIrStkEOAAzCUirvLBNXHLr7kwLvJl6V+4V3XV9x7Wd9w==}
- engines: {node: ^12.20.0 || >=14}
- dev: false
+ d3-shape@1.3.7:
+ dependencies:
+ d3-path: 1.0.9
- /concat-map@0.0.1:
- resolution: {integrity: sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg==}
- dev: false
+ d3-shape@3.2.0:
+ dependencies:
+ d3-path: 3.1.0
- /connect-history-api-fallback@2.0.0:
- resolution: {integrity: sha512-U73+6lQFmfiNPrYbXqr6kZ1i1wiRqXnp2nhMsINseWXO8lDau0LGEffJ8kQi4EjLZympVgRdvqjAgiZ1tgzDDA==}
- engines: {node: '>=0.8'}
- dev: false
+ d3-time-format@4.1.0:
+ dependencies:
+ d3-time: 3.1.0
- /console-control-strings@1.1.0:
- resolution: {integrity: sha512-ty/fTekppD2fIwRvnZAVdeOiGd1c7YXEixbgJTNzqcxJWKQnjJ/V1bNEEE6hygpM3WjwHFUVK6HTjWSzV4a8sQ==}
- dev: false
+ d3-time@3.1.0:
+ dependencies:
+ d3-array: 3.2.4
- /create-codepen@1.0.1:
- resolution: {integrity: sha512-XzSWwGCFNeOnNGp3KdCDGaKq4Cp1SvjzpPGQqO0tj1HT3BhksLdl/xQ2ZEY4+0MQ3m1I/K1Fvpm4GGMthtamyA==}
- dev: false
+ d3-timer@3.0.1: {}
- /cross-spawn@7.0.3:
- resolution: {integrity: sha512-iRDPJKUPVEND7dHPO8rkbOnPpyDygcDFtWjpeWNCgy8WP2rXcxXL8TskReQl6OrB2G7+UJrags1q15Fudc7G6w==}
- engines: {node: '>= 8'}
+ d3-transition@3.0.1(d3-selection@3.0.0):
dependencies:
- path-key: 3.1.1
- shebang-command: 2.0.0
- which: 2.0.2
- dev: false
+ d3-color: 3.1.0
+ d3-dispatch: 3.0.1
+ d3-ease: 3.0.1
+ d3-interpolate: 3.0.1
+ d3-selection: 3.0.0
+ d3-timer: 3.0.1
- /css-select@5.1.0:
- resolution: {integrity: sha512-nwoRF1rvRRnnCqqY7updORDsuqKzqYJ28+oSMaJMMgOauh3fvwHqMS7EZpIPqK8GL+g9mKxF1vP/ZjSeNjEVHg==}
+ d3-zoom@3.0.0:
dependencies:
- boolbase: 1.0.0
- css-what: 6.1.0
- domhandler: 5.0.3
- domutils: 3.1.0
- nth-check: 2.1.1
- dev: false
+ d3-dispatch: 3.0.1
+ d3-drag: 3.0.0
+ d3-interpolate: 3.0.1
+ d3-selection: 3.0.0
+ d3-transition: 3.0.1(d3-selection@3.0.0)
- /css-what@6.1.0:
- resolution: {integrity: sha512-HTUrgRJ7r4dsZKU6GjmpfRK1O76h97Z8MfS1G0FozR+oF2kG6Vfe8JE6zwrkbxigziPHinCJ+gCPjA9EaBDtRw==}
- engines: {node: '>= 6'}
- dev: false
+ d3@7.9.0:
+ dependencies:
+ d3-array: 3.2.4
+ d3-axis: 3.0.0
+ d3-brush: 3.0.0
+ d3-chord: 3.0.1
+ d3-color: 3.1.0
+ d3-contour: 4.0.2
+ d3-delaunay: 6.0.4
+ d3-dispatch: 3.0.1
+ d3-drag: 3.0.0
+ d3-dsv: 3.0.1
+ d3-ease: 3.0.1
+ d3-fetch: 3.0.1
+ d3-force: 3.0.0
+ d3-format: 3.1.2
+ d3-geo: 3.1.1
+ d3-hierarchy: 3.1.2
+ d3-interpolate: 3.0.1
+ d3-path: 3.1.0
+ d3-polygon: 3.0.1
+ d3-quadtree: 3.0.1
+ d3-random: 3.0.1
+ d3-scale: 4.0.2
+ d3-scale-chromatic: 3.1.0
+ d3-selection: 3.0.0
+ d3-shape: 3.2.0
+ d3-time: 3.1.0
+ d3-time-format: 4.1.0
+ d3-timer: 3.0.1
+ d3-transition: 3.0.1(d3-selection@3.0.0)
+ d3-zoom: 3.0.0
- /csstype@3.1.3:
- resolution: {integrity: sha512-M1uQkMl8rQK/szD0LNhtqxIPLpimGm8sOBwU7lLnCpSbTyY3yeU1Vc7l4KT5zT4s/yOxHH5O7tIuuLOCnLADRw==}
- dev: false
+ dagre-d3-es@7.0.13:
+ dependencies:
+ d3: 7.9.0
+ lodash-es: 4.17.22
- /dayjs@1.11.10:
- resolution: {integrity: sha512-vjAczensTgRcqDERK0SR2XMwsF/tSvnvlv6VcF2GIhg6Sx4yOIt/irsr1RDJsKiIyBzJDpCoXiWWq28MqH2cnQ==}
- dev: false
+ dayjs@1.11.19: {}
- /debug@4.3.4:
- resolution: {integrity: sha512-PRWFHuSU3eDtQJPvnNY7Jcket1j0t5OuOsFzPPzsekD52Zl8qUfFIPEiswXqIvHWGVHOgX+7G/vCNNhehwxfkQ==}
- engines: {node: '>=6.0'}
- peerDependencies:
- supports-color: '*'
- peerDependenciesMeta:
- supports-color:
- optional: true
+ debug@4.4.3:
dependencies:
- ms: 2.1.2
- dev: false
+ ms: 2.1.3
- /decamelize@1.2.0:
- resolution: {integrity: sha512-z2S+W9X73hAUUki+N+9Za2lBlun89zigOyGrsax+KUQ6wKW4ZoWpEYBkGhQjwAjjDCkWxhY0VKEhk8wzY7F5cA==}
- engines: {node: '>=0.10.0'}
- dev: false
+ decamelize@1.2.0: {}
- /delegates@1.0.0:
- resolution: {integrity: sha512-bd2L678uiWATM6m5Z1VzNCErI3jiGzt6HGY8OVICs40JQq/HALfbyNJmp0UDakEY4pMMaN0Ly5om/B1VI/+xfQ==}
- dev: false
+ decode-named-character-reference@1.2.0:
+ dependencies:
+ character-entities: 2.0.2
- /detect-libc@2.0.3:
- resolution: {integrity: sha512-bwy0MGW55bG41VqxxypOsdSdGqLwXPI/focwgTYCFMbdUiBAxLg9CFzG08sz2aqzknwiX7Hkl0bQENjg8iLByw==}
- engines: {node: '>=8'}
- dev: false
+ delaunator@5.0.1:
+ dependencies:
+ robust-predicates: 3.0.2
- /dijkstrajs@1.0.3:
- resolution: {integrity: sha512-qiSlmBq9+BCdCA/L46dw8Uy93mloxsPSbwnm5yrKn2vMPiy8KyAskTF6zuV/j5BMsmOGZDPs7KjU+mjb670kfA==}
- dev: false
+ dequal@2.0.3: {}
- /dom-serializer@2.0.0:
- resolution: {integrity: sha512-wIkAryiqt/nV5EQKqQpo3SToSOV9J0DnbJqwK7Wv/Trc92zIAYZ4FlMu+JPFW1DfGFt81ZTCGgDEabffXeLyJg==}
+ detect-libc@2.1.2:
+ optional: true
+
+ devlop@1.1.0:
+ dependencies:
+ dequal: 2.0.3
+
+ dijkstrajs@1.0.3: {}
+
+ dom-serializer@2.0.0:
dependencies:
domelementtype: 2.3.0
domhandler: 5.0.3
entities: 4.5.0
- dev: false
- /domelementtype@2.3.0:
- resolution: {integrity: sha512-OLETBj6w0OsagBwdXnPdN0cnMfF9opN69co+7ZrbfPGrdpPVNBUj02spi6B1N7wChLQiPn4CSH/zJvXw56gmHw==}
- dev: false
+ domelementtype@2.3.0: {}
- /domhandler@5.0.3:
- resolution: {integrity: sha512-cgwlv/1iFQiFnU96XXgROh8xTeetsnJiDsTc7TYCLFd9+/WNkIqPTxiM/8pSd8VIrhXGTf1Ny1q1hquVqDJB5w==}
- engines: {node: '>= 4'}
+ domhandler@5.0.3:
dependencies:
domelementtype: 2.3.0
- dev: false
- /domutils@3.1.0:
- resolution: {integrity: sha512-H78uMmQtI2AhgDJjWeQmHwJJ2bLPD3GMmO7Zja/ZZh84wkm+4ut+IUnUdRa8uCGX88DiVx1j6FRe1XfxEgjEZA==}
+ dompurify@3.3.1:
+ optionalDependencies:
+ '@types/trusted-types': 2.0.7
+
+ domutils@3.2.2:
dependencies:
dom-serializer: 2.0.0
domelementtype: 2.3.0
domhandler: 5.0.3
- dev: false
-
- /eastasianwidth@0.2.0:
- resolution: {integrity: sha512-I88TYZWc9XiYHRQ4/3c5rjjfgkjhLyW2luGIheGERbNQ6OY7yTybanSpDXZa8y7VUP9YmDcYa+eyq4ca7iLqWA==}
- dev: false
-
- /electron-to-chromium@1.4.722:
- resolution: {integrity: sha512-5nLE0TWFFpZ80Crhtp4pIp8LXCztjYX41yUcV6b+bKR2PqzjskTMOOlBi1VjBHlvHwS+4gar7kNKOrsbsewEZQ==}
- dev: false
-
- /emoji-regex@10.3.0:
- resolution: {integrity: sha512-QpLs9D9v9kArv4lfDEgg1X/gN5XLnf/A6l9cs8SPZLRZR3ZkY9+kwIQTxm+fsSej5UMYGE8fdoaZVIBlqG0XTw==}
- dev: false
- /emoji-regex@8.0.0:
- resolution: {integrity: sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==}
- dev: false
-
- /emoji-regex@9.2.2:
- resolution: {integrity: sha512-L18DaJsXSUk2+42pv8mLs5jJT2hqFkFE4j21wOmgbUqsZ2hL72NsUU785g9RXgo3s0ZNgVl42TiHp3ZtOv/Vyg==}
- dev: false
+ electron-to-chromium@1.5.267: {}
- /encode-utf8@1.0.3:
- resolution: {integrity: sha512-ucAnuBEhUK4boH2HjVYG5Q2mQyPorvv0u/ocS+zhdw0S8AlHYY+GOFhP1Gio5z4icpP2ivFSvhtFjQi8+T9ppw==}
- dev: false
+ emoji-regex@8.0.0: {}
- /encoding@0.1.13:
- resolution: {integrity: sha512-ETBauow1T35Y/WZMkio9jiM0Z5xjHHmJ4XmjZOq1l/dXz3lr2sRn87nJy20RupqSh1F2m3HHPSp8ShIPQJrJ3A==}
- requiresBuild: true
+ encoding-sniffer@0.2.1:
dependencies:
iconv-lite: 0.6.3
- dev: false
- optional: true
+ whatwg-encoding: 3.1.1
- /entities@4.5.0:
- resolution: {integrity: sha512-V0hjH4dGPh9Ao5p0MoRY6BVqtwCjhz6vI5LT8AJ55H+4g9/4vbHx1I54fS0XuclLhDHArPQCiMjDxjaL8fPxhw==}
- engines: {node: '>=0.12'}
- dev: false
+ entities@4.5.0: {}
- /env-paths@2.2.1:
- resolution: {integrity: sha512-+h1lkLKhZMTYjog1VEpJNG7NZJWcuc2DDk/qsqSTRRCOXiLjeQ1d1/udrUGhqMxUgAlwKNZ0cf2uqan5GLuS2A==}
- engines: {node: '>=6'}
- dev: false
+ entities@6.0.1: {}
- /envinfo@7.11.1:
- resolution: {integrity: sha512-8PiZgZNIB4q/Lw4AhOvAfB/ityHAd2bli3lESSWmWSzSsl5dKpy5N1d1Rfkd2teq/g9xN90lc6o98DOjMeYHpg==}
- engines: {node: '>=4'}
- hasBin: true
- dev: false
+ entities@7.0.0: {}
- /err-code@2.0.3:
- resolution: {integrity: sha512-2bmlRpNKBxT/CRmPOlyISQpNj+qSeYvcym/uT0Jx2bMOlKLtSy1ZmLuVxSEKKyor/N5yhvp/ZiG1oE3DEYMSFA==}
- dev: false
+ envinfo@7.21.0: {}
- /esbuild@0.20.2:
- resolution: {integrity: sha512-WdOOppmUNU+IbZ0PaDiTst80zjnrOkyJNHoKupIcVyU8Lvla3Ugx94VzkQ32Ijqd7UhHJy75gNWDMUekcrSJ6g==}
- engines: {node: '>=12'}
- hasBin: true
- requiresBuild: true
+ esbuild@0.25.12:
optionalDependencies:
- '@esbuild/aix-ppc64': 0.20.2
- '@esbuild/android-arm': 0.20.2
- '@esbuild/android-arm64': 0.20.2
- '@esbuild/android-x64': 0.20.2
- '@esbuild/darwin-arm64': 0.20.2
- '@esbuild/darwin-x64': 0.20.2
- '@esbuild/freebsd-arm64': 0.20.2
- '@esbuild/freebsd-x64': 0.20.2
- '@esbuild/linux-arm': 0.20.2
- '@esbuild/linux-arm64': 0.20.2
- '@esbuild/linux-ia32': 0.20.2
- '@esbuild/linux-loong64': 0.20.2
- '@esbuild/linux-mips64el': 0.20.2
- '@esbuild/linux-ppc64': 0.20.2
- '@esbuild/linux-riscv64': 0.20.2
- '@esbuild/linux-s390x': 0.20.2
- '@esbuild/linux-x64': 0.20.2
- '@esbuild/netbsd-x64': 0.20.2
- '@esbuild/openbsd-x64': 0.20.2
- '@esbuild/sunos-x64': 0.20.2
- '@esbuild/win32-arm64': 0.20.2
- '@esbuild/win32-ia32': 0.20.2
- '@esbuild/win32-x64': 0.20.2
- dev: false
-
- /escalade@3.1.2:
- resolution: {integrity: sha512-ErCHMCae19vR8vQGe50xIsVomy19rg6gFu3+r3jkEO46suLMWBksvVyoGgQV+jOfl84ZSOSlmv6Gxa89PmTGmA==}
- engines: {node: '>=6'}
- dev: false
-
- /esm@3.2.25:
- resolution: {integrity: sha512-U1suiZ2oDVWv4zPO56S0NcR5QriEahGtdN2OR6FiOG4WJvcjBVFB0qI4+eKoWFH483PKGuLuu6V8Z4T5g63UVA==}
- engines: {node: '>=6'}
- dev: false
-
- /esprima@4.0.1:
- resolution: {integrity: sha512-eGuFFw7Upda+g4p+QHvnW0RyTX/SVeJBDM/gCtMARO0cLuT2HcEKnTPvhjV6aGeqrCB/sbNop0Kszm0jsaWU4A==}
- engines: {node: '>=4'}
- hasBin: true
- dev: false
-
- /estree-walker@2.0.2:
- resolution: {integrity: sha512-Rfkk/Mp/DL7JVje3u18FxFujQlTNR2q6QfMSMB7AvCBx91NGj/ba3kCfza0f6dVDbw7YlRf/nDrn7pQrCCyQ/w==}
- dev: false
-
- /execa@8.0.1:
- resolution: {integrity: sha512-VyhnebXciFV2DESc+p6B+y0LjSm0krU4OgJN44qFAhBY0TJ+1V61tYD2+wHusZ6F9n5K+vl8k0sTy7PEfV4qpg==}
- engines: {node: '>=16.17'}
- dependencies:
- cross-spawn: 7.0.3
- get-stream: 8.0.1
- human-signals: 5.0.0
- is-stream: 3.0.0
- merge-stream: 2.0.0
- npm-run-path: 5.3.0
- onetime: 6.0.0
- signal-exit: 4.1.0
- strip-final-newline: 3.0.0
- dev: false
-
- /exponential-backoff@3.1.1:
- resolution: {integrity: sha512-dX7e/LHVJ6W3DE1MHWi9S1EYzDESENfLrYohG2G++ovZrYOkm4Knwa0mc1cn84xJOR4KEU0WSchhLbd0UklbHw==}
- dev: false
-
- /extend-shallow@2.0.1:
- resolution: {integrity: sha512-zCnTtlxNoAiDc3gqY2aYAWFx7XWWiasuF2K8Me5WbN8otHKTUKBwjPtNpRs/rbUZm7KxWAaNj7P1a/p52GbVug==}
- engines: {node: '>=0.10.0'}
+ '@esbuild/aix-ppc64': 0.25.12
+ '@esbuild/android-arm': 0.25.12
+ '@esbuild/android-arm64': 0.25.12
+ '@esbuild/android-x64': 0.25.12
+ '@esbuild/darwin-arm64': 0.25.12
+ '@esbuild/darwin-x64': 0.25.12
+ '@esbuild/freebsd-arm64': 0.25.12
+ '@esbuild/freebsd-x64': 0.25.12
+ '@esbuild/linux-arm': 0.25.12
+ '@esbuild/linux-arm64': 0.25.12
+ '@esbuild/linux-ia32': 0.25.12
+ '@esbuild/linux-loong64': 0.25.12
+ '@esbuild/linux-mips64el': 0.25.12
+ '@esbuild/linux-ppc64': 0.25.12
+ '@esbuild/linux-riscv64': 0.25.12
+ '@esbuild/linux-s390x': 0.25.12
+ '@esbuild/linux-x64': 0.25.12
+ '@esbuild/netbsd-arm64': 0.25.12
+ '@esbuild/netbsd-x64': 0.25.12
+ '@esbuild/openbsd-arm64': 0.25.12
+ '@esbuild/openbsd-x64': 0.25.12
+ '@esbuild/openharmony-arm64': 0.25.12
+ '@esbuild/sunos-x64': 0.25.12
+ '@esbuild/win32-arm64': 0.25.12
+ '@esbuild/win32-ia32': 0.25.12
+ '@esbuild/win32-x64': 0.25.12
+
+ esbuild@0.27.2:
+ optionalDependencies:
+ '@esbuild/aix-ppc64': 0.27.2
+ '@esbuild/android-arm': 0.27.2
+ '@esbuild/android-arm64': 0.27.2
+ '@esbuild/android-x64': 0.27.2
+ '@esbuild/darwin-arm64': 0.27.2
+ '@esbuild/darwin-x64': 0.27.2
+ '@esbuild/freebsd-arm64': 0.27.2
+ '@esbuild/freebsd-x64': 0.27.2
+ '@esbuild/linux-arm': 0.27.2
+ '@esbuild/linux-arm64': 0.27.2
+ '@esbuild/linux-ia32': 0.27.2
+ '@esbuild/linux-loong64': 0.27.2
+ '@esbuild/linux-mips64el': 0.27.2
+ '@esbuild/linux-ppc64': 0.27.2
+ '@esbuild/linux-riscv64': 0.27.2
+ '@esbuild/linux-s390x': 0.27.2
+ '@esbuild/linux-x64': 0.27.2
+ '@esbuild/netbsd-arm64': 0.27.2
+ '@esbuild/netbsd-x64': 0.27.2
+ '@esbuild/openbsd-arm64': 0.27.2
+ '@esbuild/openbsd-x64': 0.27.2
+ '@esbuild/openharmony-arm64': 0.27.2
+ '@esbuild/sunos-x64': 0.27.2
+ '@esbuild/win32-arm64': 0.27.2
+ '@esbuild/win32-ia32': 0.27.2
+ '@esbuild/win32-x64': 0.27.2
+
+ escalade@3.2.0: {}
+
+ esm@3.2.25: {}
+
+ esprima@4.0.1: {}
+
+ estree-walker@2.0.2: {}
+
+ extend-shallow@2.0.1:
dependencies:
is-extendable: 0.1.1
- dev: false
- /fast-glob@3.3.2:
- resolution: {integrity: sha512-oX2ruAFQwf/Orj8m737Y5adxDQO0LAB7/S5MnxCdTNDd4p6BsyIVsv9JQsATbTSq8KHRpLwIHbVlUNatxd+1Ow==}
- engines: {node: '>=8.6.0'}
+ extend@3.0.2: {}
+
+ fast-glob@3.3.3:
dependencies:
'@nodelib/fs.stat': 2.0.5
'@nodelib/fs.walk': 1.2.8
glob-parent: 5.1.2
merge2: 1.4.1
- micromatch: 4.0.5
- dev: false
+ micromatch: 4.0.8
- /fastq@1.17.1:
- resolution: {integrity: sha512-sRVD3lWVIXWg6By68ZN7vho9a1pQcN/WBFaAAsDDFzlJjvoGx0P8z7V1t72grFJfJhu3YPZBuu25f7Kaw2jN1w==}
+ fastq@1.20.1:
dependencies:
- reusify: 1.0.4
- dev: false
+ reusify: 1.1.0
- /fflate@0.8.2:
- resolution: {integrity: sha512-cPJU47OaAoCbg0pBvzsgpTPhmhqI5eJjh/JIu8tPj5q+T7iLvW/JAYUqmE7KOB4R1ZyEhzBaIQpQpardBF5z8A==}
- dev: false
+ fdir@6.5.0(picomatch@4.0.3):
+ optionalDependencies:
+ picomatch: 4.0.3
- /fill-range@7.0.1:
- resolution: {integrity: sha512-qOo9F+dMUmC2Lcb4BbVvnKJxTPjCm+RRpe4gDuGrzkL7mEVl/djYSu2OdQ2Pa302N4oqkSg9ir6jaLWJ2USVpQ==}
- engines: {node: '>=8'}
+ fflate@0.8.2: {}
+
+ fill-range@7.1.1:
dependencies:
to-regex-range: 5.0.1
- dev: false
- /find-up@4.1.0:
- resolution: {integrity: sha512-PpOwAdQ/YlXQ2vj8a3h8IipDuYRi3wceVQQGYWxNINccq40Anw7BlsEXCMbt1Zt+OLA6Fq9suIpIWD0OsnISlw==}
- engines: {node: '>=8'}
+ find-up@4.1.0:
dependencies:
locate-path: 5.0.0
path-exists: 4.0.0
- dev: false
-
- /foreground-child@3.1.1:
- resolution: {integrity: sha512-TMKDUnIte6bfb5nWv7V/caI169OHgvwjb7V4WkeUvbQQdjr5rWKqHFiKWb/fcOwB+CzBT+qbWjvj+DVwRskpIg==}
- engines: {node: '>=14'}
- dependencies:
- cross-spawn: 7.0.3
- signal-exit: 4.1.0
- dev: false
- /fraction.js@4.3.7:
- resolution: {integrity: sha512-ZsDfxO51wGAXREY55a7la9LScWpwv9RxIrYABrlvOFBlH/ShPnrtsXeuUIfXKKOVicNxQ+o8JTbJvjS4M89yew==}
- dev: false
+ fraction.js@5.3.4: {}
- /fs-extra@11.2.0:
- resolution: {integrity: sha512-PmDi3uwK5nFuXh7XDTlVnS17xJS7vW36is2+w3xcv8SVxiB4NyATf4ctkVY5bkSjX0Y4nbvZCq1/EjtEyr9ktw==}
- engines: {node: '>=14.14'}
+ fs-extra@11.3.3:
dependencies:
graceful-fs: 4.2.11
- jsonfile: 6.1.0
+ jsonfile: 6.2.0
universalify: 2.0.1
- dev: false
-
- /fs-minipass@2.1.0:
- resolution: {integrity: sha512-V/JgOLFCS+R6Vcq0slCuaeWEdNC3ouDlJMNIsacH2VtALiu9mV4LPrHc5cDl8k5aw6J8jwgWWpiTo5RYhmIzvg==}
- engines: {node: '>= 8'}
- dependencies:
- minipass: 3.3.6
- dev: false
-
- /fs-minipass@3.0.3:
- resolution: {integrity: sha512-XUBA9XClHbnJWSfBzjkm6RvPsyg3sryZt06BEQoXcF7EK/xpGaQYJgQKDJSUH5SGZ76Y7pFx1QBnXz09rU5Fbw==}
- engines: {node: ^14.17.0 || ^16.13.0 || >=18.0.0}
- dependencies:
- minipass: 7.0.4
- dev: false
- /fs.realpath@1.0.0:
- resolution: {integrity: sha512-OO0pH2lK6a0hZnAdau5ItzHPI6pUlvI7jMVnxUQRtw4owF2wk8lOSabtGDCTP4Ggrg2MbGnWO9X8K1t4+fGMDw==}
- dev: false
-
- /fsevents@2.3.3:
- resolution: {integrity: sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==}
- engines: {node: ^8.16.0 || ^10.6.0 || >=11.0.0}
- os: [darwin]
- requiresBuild: true
- dev: false
+ fsevents@2.3.3:
optional: true
- /gauge@3.0.2:
- resolution: {integrity: sha512-+5J6MS/5XksCuXq++uFRsnUd7Ovu1XenbeuIuNRJxYWjgQbPuFhT14lAvsWfqfAmnwluf1OwMjz39HjfLPci0Q==}
- engines: {node: '>=10'}
- dependencies:
- aproba: 2.0.0
- color-support: 1.1.3
- console-control-strings: 1.1.0
- has-unicode: 2.0.1
- object-assign: 4.1.1
- signal-exit: 3.0.7
- string-width: 4.2.3
- strip-ansi: 6.0.1
- wide-align: 1.1.5
- dev: false
-
- /get-caller-file@2.0.5:
- resolution: {integrity: sha512-DyFP3BM/3YHTQOCUL/w0OZHR0lpKeGrxotcHWcqNEdnltqFwXVfhEBQ94eIo34AfQpo0rGki4cyIiftY06h2Fg==}
- engines: {node: 6.* || 8.* || >= 10.*}
- dev: false
-
- /get-east-asian-width@1.2.0:
- resolution: {integrity: sha512-2nk+7SIVb14QrgXFHcm84tD4bKQz0RxPuMT8Ag5KPOq7J5fEmAg0UbXdTOSHqNuHSU28k55qnceesxXRZGzKWA==}
- engines: {node: '>=18'}
- dev: false
+ get-caller-file@2.0.5: {}
- /get-stream@8.0.1:
- resolution: {integrity: sha512-VaUJspBffn/LMCJVoMvSAdmscJyS1auj5Zulnn5UoYcY531UWmdwhRWkcGKnGU93m5HSXP9LP2usOryrBtQowA==}
- engines: {node: '>=16'}
- dev: false
+ get-east-asian-width@1.4.0: {}
- /giscus@1.5.0:
- resolution: {integrity: sha512-t3LL0qbSO3JXq3uyQeKpF5CegstGfKX/0gI6eDe1cmnI7D56R7j52yLdzw4pdKrg3VnufwCgCM3FDz7G1Qr6lg==}
+ giscus@1.6.0:
dependencies:
- lit: 3.1.2
- dev: false
+ lit: 3.3.2
- /glob-parent@5.1.2:
- resolution: {integrity: sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow==}
- engines: {node: '>= 6'}
+ glob-parent@5.1.2:
dependencies:
is-glob: 4.0.3
- dev: false
-
- /glob@10.3.12:
- resolution: {integrity: sha512-TCNv8vJ+xz4QiqTpfOJA7HvYv+tNIRHKfUWw/q+v2jdgN4ebz+KY9tGx5J4rHP0o84mNP+ApH66HRX8us3Khqg==}
- engines: {node: '>=16 || 14 >=14.17'}
- hasBin: true
- dependencies:
- foreground-child: 3.1.1
- jackspeak: 2.3.6
- minimatch: 9.0.4
- minipass: 7.0.4
- path-scurry: 1.10.2
- dev: false
-
- /glob@7.2.3:
- resolution: {integrity: sha512-nFR0zLpU2YCaRxwoCJvL6UvCH2JFyFVIvwTLsIf21AuHlMskA1hhTdk+LlYJtOlYt9v6dvszD2BGRqBL+iQK9Q==}
- dependencies:
- fs.realpath: 1.0.0
- inflight: 1.0.6
- inherits: 2.0.4
- minimatch: 3.1.2
- once: 1.4.0
- path-is-absolute: 1.0.1
- dev: false
-
- /globby@14.0.0:
- resolution: {integrity: sha512-/1WM/LNHRAOH9lZta77uGbq0dAEQM+XjNesWwhlERDVenqothRbnzTrL3/LrIoEPPjeUHC3vrS6TwoyxeHs7MQ==}
- engines: {node: '>=18'}
- dependencies:
- '@sindresorhus/merge-streams': 1.0.0
- fast-glob: 3.3.2
- ignore: 5.3.1
- path-type: 5.0.0
- slash: 5.1.0
- unicorn-magic: 0.1.0
- dev: false
- /globby@14.0.1:
- resolution: {integrity: sha512-jOMLD2Z7MAhyG8aJpNOpmziMOP4rPLcc95oQPKXBazW82z+CEgPFBQvEpRUa1KeIMUJo4Wsm+q6uzO/Q/4BksQ==}
- engines: {node: '>=18'}
+ globby@14.0.2:
dependencies:
'@sindresorhus/merge-streams': 2.3.0
- fast-glob: 3.3.2
- ignore: 5.3.1
+ fast-glob: 3.3.3
+ ignore: 5.3.2
path-type: 5.0.0
slash: 5.1.0
unicorn-magic: 0.1.0
- dev: false
- /graceful-fs@4.2.11:
- resolution: {integrity: sha512-RbJ5/jmFcNNCcDV5o9eTnBLJ/HszWV0P73bc+Ff4nS/rJj+YaS6IGyiOL0VoBYX+l1Wrl3k63h/KrH+nhJ0XvQ==}
- dev: false
+ graceful-fs@4.2.11: {}
- /gray-matter@4.0.3:
- resolution: {integrity: sha512-5v6yZd4JK3eMI3FqqCouswVqwugaA9r4dNZB1wwcmrD02QkV5H0y7XBQW8QwQqEaZY1pM9aqORSORhJRdNK44Q==}
- engines: {node: '>=6.0'}
+ gray-matter@4.0.3:
dependencies:
- js-yaml: 3.14.1
+ js-yaml: 3.14.2
kind-of: 6.0.3
section-matter: 1.0.0
strip-bom-string: 1.0.0
- dev: false
- /has-unicode@2.0.1:
- resolution: {integrity: sha512-8Rf9Y83NBReMnx0gFzA8JImQACstCYWUplepDa9xprwwtmgEZUF0h/i5xSA625zB/I37EtrswSST6OXxwaaIJQ==}
- dev: false
+ hachure-fill@0.5.2: {}
- /hash-sum@2.0.0:
- resolution: {integrity: sha512-WdZTbAByD+pHfl/g9QSsBIIwy8IT+EsPiKDs0KNX+zSHhdDLFKdZu0BQHljvO+0QI/BasbMSUa8wYNCZTvhslg==}
- dev: false
+ has-flag@4.0.0: {}
- /htmlparser2@8.0.2:
- resolution: {integrity: sha512-GYdjWKDkbRLkZ5geuHs5NY1puJ+PXwP7+fHPRz06Eirsb9ugf6d8kkXav6ADhcODhFFPMIXyxkxSuMf3D6NCFA==}
+ hash-sum@2.0.0: {}
+
+ hast-util-from-html@2.0.3:
dependencies:
- domelementtype: 2.3.0
- domhandler: 5.0.3
- domutils: 3.1.0
- entities: 4.5.0
- dev: false
+ '@types/hast': 3.0.4
+ devlop: 1.1.0
+ hast-util-from-parse5: 8.0.3
+ parse5: 7.3.0
+ vfile: 6.0.3
+ vfile-message: 4.0.3
- /http-cache-semantics@4.1.1:
- resolution: {integrity: sha512-er295DKPVsV82j5kw1Gjt+ADA/XYHsajl82cGNQG2eyoPkvgUhX+nDIyelzhIWbbsXP39EHcI6l5tYs2FYqYXQ==}
- dev: false
+ hast-util-from-parse5@8.0.3:
+ dependencies:
+ '@types/hast': 3.0.4
+ '@types/unist': 3.0.3
+ devlop: 1.1.0
+ hastscript: 9.0.1
+ property-information: 7.1.0
+ vfile: 6.0.3
+ vfile-location: 5.0.3
+ web-namespaces: 2.0.1
- /http-proxy-agent@7.0.2:
- resolution: {integrity: sha512-T1gkAiYYDWYx3V5Bmyu7HcfcvL7mUrTWiM6yOfa3PIphViJ/gFPbvidQ+veqSOHci/PxBcDabeUNCzpOODJZig==}
- engines: {node: '>= 14'}
+ hast-util-parse-selector@4.0.0:
dependencies:
- agent-base: 7.1.1
- debug: 4.3.4
- transitivePeerDependencies:
- - supports-color
- dev: false
+ '@types/hast': 3.0.4
- /https-proxy-agent@5.0.1:
- resolution: {integrity: sha512-dFcAjpTQFgoLMzC2VwU+C/CbS7uRL0lWmxDITmqm7C+7F0Odmj6s9l6alZc6AELXhrnggM2CeWSXHGOdX2YtwA==}
- engines: {node: '>= 6'}
+ hast-util-sanitize@5.0.2:
dependencies:
- agent-base: 6.0.2
- debug: 4.3.4
- transitivePeerDependencies:
- - supports-color
- dev: false
+ '@types/hast': 3.0.4
+ '@ungap/structured-clone': 1.3.0
+ unist-util-position: 5.0.0
- /https-proxy-agent@7.0.4:
- resolution: {integrity: sha512-wlwpilI7YdjSkWaQ/7omYBMTliDcmCN8OLihO6I9B86g06lMyAoqgoDpV0XqoaPOKj+0DIdAvnsWfyAAhmimcg==}
- engines: {node: '>= 14'}
+ hast-util-to-html@9.0.5:
dependencies:
- agent-base: 7.1.1
- debug: 4.3.4
- transitivePeerDependencies:
- - supports-color
- dev: false
+ '@types/hast': 3.0.4
+ '@types/unist': 3.0.3
+ ccount: 2.0.1
+ comma-separated-tokens: 2.0.3
+ hast-util-whitespace: 3.0.0
+ html-void-elements: 3.0.0
+ mdast-util-to-hast: 13.2.1
+ property-information: 7.1.0
+ space-separated-tokens: 2.0.2
+ stringify-entities: 4.0.4
+ zwitch: 2.0.4
- /human-signals@5.0.0:
- resolution: {integrity: sha512-AXcZb6vzzrFAUE61HnN4mpLqd/cSIwNQjtNWR0euPm6y0iqx3G4gOXaIDdtdDwZmhwe82LA6+zinmW4UBWVePQ==}
- engines: {node: '>=16.17.0'}
- dev: false
+ hast-util-whitespace@3.0.0:
+ dependencies:
+ '@types/hast': 3.0.4
- /husky@9.0.10:
- resolution: {integrity: sha512-TQGNknoiy6bURzIO77pPRu+XHi6zI7T93rX+QnJsoYFf3xdjKOur+IlfqzJGMHIK/wXrLg+GsvMs8Op7vI2jVA==}
- engines: {node: '>=18'}
- hasBin: true
- dev: false
+ hastscript@9.0.1:
+ dependencies:
+ '@types/hast': 3.0.4
+ comma-separated-tokens: 2.0.3
+ hast-util-parse-selector: 4.0.0
+ property-information: 7.1.0
+ space-separated-tokens: 2.0.2
- /iconv-lite@0.6.3:
- resolution: {integrity: sha512-4fCk79wshMdzMp2rH06qWrJE4iolqLhCUH+OiuIgU++RB0+94NlDL81atO7GX55uUKueo0txHNtvEyI6D7WdMw==}
- engines: {node: '>=0.10.0'}
- requiresBuild: true
+ hookable@5.5.3: {}
+
+ html-void-elements@3.0.0: {}
+
+ htmlparser2@10.0.0:
+ dependencies:
+ domelementtype: 2.3.0
+ domhandler: 5.0.3
+ domutils: 3.2.2
+ entities: 6.0.1
+
+ husky@9.1.7: {}
+
+ iconv-lite@0.6.3:
dependencies:
safer-buffer: 2.1.2
- dev: false
- optional: true
-
- /ignore@5.3.1:
- resolution: {integrity: sha512-5Fytz/IraMjqpwfd34ke28PTVMjZjJG2MPn5t7OE4eUCUNf8BAa7b5WUS9/Qvr6mwOQS7Mk6vdsMno5he+T8Xw==}
- engines: {node: '>= 4'}
- dev: false
- /immutable@4.3.5:
- resolution: {integrity: sha512-8eabxkth9gZatlwl5TBuJnCsoTADlL6ftEr7A4qgdaTsPyreilDSnUk57SO+jfKcNtxPa22U5KK6DSeAYhpBJw==}
- dev: false
+ ignore@5.3.2: {}
- /imurmurhash@0.1.4:
- resolution: {integrity: sha512-JmXMZ6wuvDmLiHEml9ykzqO6lwFbof0GG4IkcGaENdCRDDmMVnny7s5HsIgHCbaq0w2MyPhDqkhTUgS2LU2PHA==}
- engines: {node: '>=0.8.19'}
- dev: false
+ immutable@5.1.4: {}
- /indent-string@4.0.0:
- resolution: {integrity: sha512-EdDDZu4A2OyIK7Lr/2zG+w5jmbuk1DVBnEwREQvBzspBJkCEbRa8GxU1lghYcaGJCnRWibjDXlq779X1/y5xwg==}
- engines: {node: '>=8'}
- dev: false
+ internmap@1.0.1: {}
- /inflight@1.0.6:
- resolution: {integrity: sha512-k92I/b08q4wvFscXCLvqfsHCrjrF7yiXsQuIVvVE7N82W3+aqpzuUdBbfhWcy/FZR3/4IgflMgKLOsvPDrGCJA==}
- dependencies:
- once: 1.4.0
- wrappy: 1.0.2
- dev: false
+ internmap@2.0.3: {}
- /inherits@2.0.4:
- resolution: {integrity: sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==}
- dev: false
+ is-alphabetical@2.0.1: {}
- /ip-address@9.0.5:
- resolution: {integrity: sha512-zHtQzGojZXTwZTHQqra+ETKd4Sn3vgi7uBmlPoXVWZqYvuKmtI0l/VZTjqGmJY9x88GGOaZ9+G9ES8hC4T4X8g==}
- engines: {node: '>= 12'}
+ is-alphanumerical@2.0.1:
dependencies:
- jsbn: 1.1.0
- sprintf-js: 1.1.3
- dev: false
+ is-alphabetical: 2.0.1
+ is-decimal: 2.0.1
- /is-binary-path@2.1.0:
- resolution: {integrity: sha512-ZMERYes6pDydyuGidse7OsHxtbI7WVeUEozgR/g7rd0xUimYNlvZRE/K2MgZTjWy725IfelLeVcEM97mmtRGXw==}
- engines: {node: '>=8'}
- dependencies:
- binary-extensions: 2.3.0
- dev: false
+ is-decimal@2.0.1: {}
- /is-extendable@0.1.1:
- resolution: {integrity: sha512-5BMULNob1vgFX6EjQw5izWDxrecWK9AM72rugNr0TFldMOi0fj6Jk+zeKIt0xGj4cEfQIJth4w3OKWOJ4f+AFw==}
- engines: {node: '>=0.10.0'}
- dev: false
+ is-extendable@0.1.1: {}
- /is-extglob@2.1.1:
- resolution: {integrity: sha512-SbKbANkN603Vi4jEZv49LeVJMn4yGwsbzZworEoyEiutsN3nJYdbO36zfhGJ6QEDpOZIFkDtnq5JRxmvl3jsoQ==}
- engines: {node: '>=0.10.0'}
- dev: false
+ is-extglob@2.1.1: {}
- /is-fullwidth-code-point@3.0.0:
- resolution: {integrity: sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg==}
- engines: {node: '>=8'}
- dev: false
+ is-fullwidth-code-point@3.0.0: {}
- /is-glob@4.0.3:
- resolution: {integrity: sha512-xelSayHH36ZgE7ZWhli7pW34hNbNl8Ojv5KVmkJD4hBdD3th8Tfk9vYasLM+mXWOZhFkgZfxhLSnrwRr4elSSg==}
- engines: {node: '>=0.10.0'}
+ is-glob@4.0.3:
dependencies:
is-extglob: 2.1.1
- dev: false
-
- /is-interactive@2.0.0:
- resolution: {integrity: sha512-qP1vozQRI+BMOPcjFzrjXuQvdak2pHNUMZoeG2eRbiSqyvbEf/wQtEOTOX1guk6E3t36RkaqiSt8A/6YElNxLQ==}
- engines: {node: '>=12'}
- dev: false
-
- /is-lambda@1.0.1:
- resolution: {integrity: sha512-z7CMFGNrENq5iFB9Bqo64Xk6Y9sg+epq1myIcdHaGnbMTYOxvzsEtdYqQUylB7LxfkvgrrjP32T6Ywciio9UIQ==}
- dev: false
-
- /is-number@7.0.0:
- resolution: {integrity: sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng==}
- engines: {node: '>=0.12.0'}
- dev: false
- /is-stream@3.0.0:
- resolution: {integrity: sha512-LnQR4bZ9IADDRSkvpqMGvt/tEJWclzklNgSw48V5EAaAeDd6qGvN8ei6k5p0tvxSR171VmGyHuTiAOfxAbr8kA==}
- engines: {node: ^12.20.0 || ^14.13.1 || >=16.0.0}
- dev: false
+ is-hexadecimal@2.0.1: {}
- /is-unicode-supported@1.3.0:
- resolution: {integrity: sha512-43r2mRvz+8JRIKnWJ+3j8JtjRKZ6GmjzfaE/qiBJnikNnYv/6bagRJ1kUhNk8R5EX/GkobD+r+sfxCPJsiKBLQ==}
- engines: {node: '>=12'}
- dev: false
+ is-interactive@2.0.0: {}
- /is-unicode-supported@2.0.0:
- resolution: {integrity: sha512-FRdAyx5lusK1iHG0TWpVtk9+1i+GjrzRffhDg4ovQ7mcidMQ6mj+MhKPmvh7Xwyv5gIS06ns49CA7Sqg7lC22Q==}
- engines: {node: '>=18'}
- dev: false
+ is-number@7.0.0: {}
- /isexe@2.0.0:
- resolution: {integrity: sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==}
- dev: false
+ is-plain-obj@4.1.0: {}
- /isexe@3.1.1:
- resolution: {integrity: sha512-LpB/54B+/2J5hqQ7imZHfdU31OlgQqx7ZicVlkm9kzg9/w8GKLEcFfJl/t7DCEDueOyBAD6zCCwTO6Fzs0NoEQ==}
- engines: {node: '>=16'}
- dev: false
+ is-unicode-supported@2.1.0: {}
- /jackspeak@2.3.6:
- resolution: {integrity: sha512-N3yCS/NegsOBokc8GAdM8UcmfsKiSS8cipheD/nivzr700H+nsMOxJjQnvwOcRYVuFkdH0wGUvW2WbXGmrZGbQ==}
- engines: {node: '>=14'}
- dependencies:
- '@isaacs/cliui': 8.0.2
- optionalDependencies:
- '@pkgjs/parseargs': 0.11.0
- dev: false
+ is-what@5.5.0: {}
- /js-yaml@3.14.1:
- resolution: {integrity: sha512-okMH7OXXJ7YrN9Ok3/SXrnu4iX9yOk+25nqX4imS2npuvTYDmo/QEZoqwZkYaIDk3jVvBOTOIEgEhaLOynBS9g==}
- hasBin: true
+ js-yaml@3.14.2:
dependencies:
argparse: 1.0.10
esprima: 4.0.1
- dev: false
- /js-yaml@4.1.0:
- resolution: {integrity: sha512-wpxZs9NoxZaJESJGIZTyDEaYpl0FKSA+FB9aJiyemKhMwkxQg63h4T1KJgUGHpTqPDNRcmmYLugrRjJlBtWvRA==}
- hasBin: true
+ js-yaml@4.1.1:
dependencies:
argparse: 2.0.1
- dev: false
-
- /jsbn@1.1.0:
- resolution: {integrity: sha512-4bYVV3aAMtDTTu4+xsDYa6sy9GyJ69/amsu9sYF2zqjiEoZA5xJi3BrfX3uY+/IekIu7MwdObdbDWpoZdBv3/A==}
- dev: false
- /jsonc-parser@3.2.0:
- resolution: {integrity: sha512-gfFQZrcTc8CnKXp6Y4/CBT3fTc0OVuDofpre4aEeEpSBPV5X5v4+Vmx+8snU7RLPrNHPKSgLxGo9YuQzz20o+w==}
- dev: false
+ jsonc-parser@3.3.1: {}
- /jsonfile@6.1.0:
- resolution: {integrity: sha512-5dgndWOriYSm5cnYaJNhalLNDKOqFwyDB/rr1E9ZsGciGvKPs8R2xYGCacuf3z6K1YKDz182fd+fY3cn3pMqXQ==}
+ jsonfile@6.2.0:
dependencies:
universalify: 2.0.1
optionalDependencies:
graceful-fs: 4.2.11
- dev: false
- /kind-of@6.0.3:
- resolution: {integrity: sha512-dcS1ul+9tmeD95T+x28/ehLgd9mENa3LsvDTtzm3vyBEO7RPptvAD+t44WVXaUjTBRcrpFeFlC8WCruUR456hw==}
- engines: {node: '>=0.10.0'}
- dev: false
+ katex@0.16.27:
+ dependencies:
+ commander: 8.3.0
- /lilconfig@3.1.1:
- resolution: {integrity: sha512-O18pf7nyvHTckunPWCV1XUNXU1piu01y2b7ATJ0ppkUkk8ocqVWBrYjJBCwHDjD/ZWcfyrA0P4gKhzWGi5EINQ==}
- engines: {node: '>=14'}
- dev: false
+ khroma@2.1.0: {}
- /linkify-it@5.0.0:
- resolution: {integrity: sha512-5aHCbzQRADcdP+ATqnDuhhJ/MRIqDkZX5pyjFHRRysS8vZ5AbqGEoFIb6pYHPZ+L/OC2Lc+xT8uHVVR5CAK/wQ==}
+ kind-of@6.0.3: {}
+
+ langium@3.3.1:
+ dependencies:
+ chevrotain: 11.0.3
+ chevrotain-allstar: 0.3.1(chevrotain@11.0.3)
+ vscode-languageserver: 9.0.1
+ vscode-languageserver-textdocument: 1.0.12
+ vscode-uri: 3.0.8
+
+ layout-base@1.0.2: {}
+
+ layout-base@2.0.1: {}
+
+ lilconfig@3.1.3: {}
+
+ linkify-it@5.0.0:
dependencies:
uc.micro: 2.1.0
- dev: false
- /lit-element@4.0.4:
- resolution: {integrity: sha512-98CvgulX6eCPs6TyAIQoJZBCQPo80rgXR+dVBs61cstJXqtI+USQZAbA4gFHh6L/mxBx9MrgPLHLsUgDUHAcCQ==}
+ lit-element@4.2.2:
dependencies:
- '@lit-labs/ssr-dom-shim': 1.2.0
- '@lit/reactive-element': 2.0.4
- lit-html: 3.1.2
- dev: false
+ '@lit-labs/ssr-dom-shim': 1.5.1
+ '@lit/reactive-element': 2.1.2
+ lit-html: 3.3.2
- /lit-html@3.1.2:
- resolution: {integrity: sha512-3OBZSUrPnAHoKJ9AMjRL/m01YJxQMf+TMHanNtTHG68ubjnZxK0RFl102DPzsw4mWnHibfZIBJm3LWCZ/LmMvg==}
+ lit-html@3.3.2:
dependencies:
'@types/trusted-types': 2.0.7
- dev: false
- /lit@3.1.2:
- resolution: {integrity: sha512-VZx5iAyMtX7CV4K8iTLdCkMaYZ7ipjJZ0JcSdJ0zIdGxxyurjIn7yuuSxNBD7QmjvcNJwr0JS4cAdAtsy7gZ6w==}
+ lit@3.3.2:
dependencies:
- '@lit/reactive-element': 2.0.4
- lit-element: 4.0.4
- lit-html: 3.1.2
- dev: false
+ '@lit/reactive-element': 2.1.2
+ lit-element: 4.2.2
+ lit-html: 3.3.2
- /locate-path@5.0.0:
- resolution: {integrity: sha512-t7hw9pI+WvuwNJXwk5zVHpyhIqzg2qTlklJOf0mVxGSbe3Fp2VieZcduNYjaLDoy6p9uGpQEGWG87WpMKlNq8g==}
- engines: {node: '>=8'}
+ locate-path@5.0.0:
dependencies:
p-locate: 4.1.0
- dev: false
-
- /log-symbols@6.0.0:
- resolution: {integrity: sha512-i24m8rpwhmPIS4zscNzK6MSEhk0DUWa/8iYQWxhffV8jkI4Phvs3F+quL5xvS0gdQR0FyTCMMH33Y78dDTzzIw==}
- engines: {node: '>=18'}
- dependencies:
- chalk: 5.3.0
- is-unicode-supported: 1.3.0
- dev: false
- /lru-cache@10.2.0:
- resolution: {integrity: sha512-2bIM8x+VAf6JT4bKAljS1qUWgMsqZRPGJS6FSahIMPVvctcNhyVp7AJu7quxOW9jwkryBReKZY5tY5JYv2n/7Q==}
- engines: {node: 14 || >=16.14}
- dev: false
+ lodash-es@4.17.21: {}
- /lru-cache@6.0.0:
- resolution: {integrity: sha512-Jo6dJ04CmSjuznwJSS3pUeWmd/H0ffTlkXXgwZi+eq1UCmqQwCh+eLsYOYCwY991i2Fah4h1BEMCx4qThGbsiA==}
- engines: {node: '>=10'}
- dependencies:
- yallist: 4.0.0
- dev: false
+ lodash-es@4.17.22: {}
- /magic-string@0.30.8:
- resolution: {integrity: sha512-ISQTe55T2ao7XtlAStud6qwYPZjE4GK1S/BeVPus4jrq6JuOnQ00YKQC581RWhR122W7msZV263KzVeLoqidyQ==}
- engines: {node: '>=12'}
+ log-symbols@7.0.1:
dependencies:
- '@jridgewell/sourcemap-codec': 1.4.15
- dev: false
+ is-unicode-supported: 2.1.0
+ yoctocolors: 2.1.2
- /make-dir@3.1.0:
- resolution: {integrity: sha512-g3FeP20LNwhALb/6Cz6Dd4F2ngze0jz7tbzrD2wAV+o9FeNHe4rL+yK2md0J/fiSf1sa1ADhXqi5+oVwOM/eGw==}
- engines: {node: '>=8'}
+ magic-string@0.30.21:
dependencies:
- semver: 6.3.1
- dev: false
-
- /make-fetch-happen@13.0.0:
- resolution: {integrity: sha512-7ThobcL8brtGo9CavByQrQi+23aIfgYU++wg4B87AIS8Rb2ZBt/MEaDqzA00Xwv/jUjAjYkLHjVolYuTLKda2A==}
- engines: {node: ^16.14.0 || >=18.0.0}
- dependencies:
- '@npmcli/agent': 2.2.1
- cacache: 18.0.2
- http-cache-semantics: 4.1.1
- is-lambda: 1.0.1
- minipass: 7.0.4
- minipass-fetch: 3.0.4
- minipass-flush: 1.0.5
- minipass-pipeline: 1.2.4
- negotiator: 0.6.3
- promise-retry: 2.0.1
- ssri: 10.0.5
- transitivePeerDependencies:
- - supports-color
- dev: false
+ '@jridgewell/sourcemap-codec': 1.5.5
- /markdown-it-anchor@8.6.7(@types/markdown-it@13.0.7)(markdown-it@14.1.0):
- resolution: {integrity: sha512-FlCHFwNnutLgVTflOYHPW2pPcl2AACqVzExlkGQNsi4CJgqOHN7YTgDd4LuhgN1BFO3TS0vLAruV1Td6dwWPJA==}
- peerDependencies:
- '@types/markdown-it': '*'
- markdown-it: '*'
+ markdown-it-anchor@9.2.0(@types/markdown-it@14.1.2)(markdown-it@14.1.0):
dependencies:
- '@types/markdown-it': 13.0.7
+ '@types/markdown-it': 14.1.2
markdown-it: 14.1.0
- dev: false
- /markdown-it-emoji@3.0.0:
- resolution: {integrity: sha512-+rUD93bXHubA4arpEZO3q80so0qgoFJEKRkRbjKX8RTdca89v2kfyF+xR3i2sQTwql9tpPZPOQN5B+PunspXRg==}
- dev: false
+ markdown-it-emoji@3.0.0: {}
- /markdown-it@14.0.0:
- resolution: {integrity: sha512-seFjF0FIcPt4P9U39Bq1JYblX0KZCjDLFFQPHpL5AzHpqPEKtosxmdq/LTVZnjfH7tjt9BxStm+wXcDBNuYmzw==}
- hasBin: true
+ markdown-it@14.1.0:
dependencies:
argparse: 2.0.1
entities: 4.5.0
@@ -2482,1191 +5213,932 @@ packages:
mdurl: 2.0.0
punycode.js: 2.3.1
uc.micro: 2.1.0
- dev: false
- /markdown-it@14.1.0:
- resolution: {integrity: sha512-a54IwgWPaeBCAAsv13YgmALOF1elABB08FxO9i+r4VFk5Vl4pKokRPeX8u5TCgSsPi6ec1otfLjdOpVcgbpshg==}
- hasBin: true
+ markdownlint-cli2-formatter-default@0.0.5(markdownlint-cli2@0.17.1):
dependencies:
- argparse: 2.0.1
- entities: 4.5.0
- linkify-it: 5.0.0
- mdurl: 2.0.0
- punycode.js: 2.3.1
- uc.micro: 2.1.0
- dev: false
+ markdownlint-cli2: 0.17.1
- /markdownlint-cli2-formatter-default@0.0.4(markdownlint-cli2@0.12.1):
- resolution: {integrity: sha512-xm2rM0E+sWgjpPn1EesPXx5hIyrN2ddUnUwnbCsD/ONxYtw3PX6LydvdH6dciWAoFDpwzbHM1TO7uHfcMd6IYg==}
- peerDependencies:
- markdownlint-cli2: '>=0.0.4'
+ markdownlint-cli2@0.17.1:
dependencies:
- markdownlint-cli2: 0.12.1
- dev: false
+ globby: 14.0.2
+ js-yaml: 4.1.1
+ jsonc-parser: 3.3.1
+ markdownlint: 0.37.3
+ markdownlint-cli2-formatter-default: 0.0.5(markdownlint-cli2@0.17.1)
+ micromatch: 4.0.8
+ transitivePeerDependencies:
+ - supports-color
- /markdownlint-cli2@0.12.1:
- resolution: {integrity: sha512-RcK+l5FjJEyrU3REhrThiEUXNK89dLYNJCYbvOUKypxqIGfkcgpz8g08EKqhrmUbYfYoLC5nEYQy53NhJSEtfQ==}
- engines: {node: '>=18'}
- hasBin: true
+ markdownlint@0.37.3:
dependencies:
- globby: 14.0.0
- jsonc-parser: 3.2.0
- markdownlint: 0.33.0
- markdownlint-cli2-formatter-default: 0.0.4(markdownlint-cli2@0.12.1)
- micromatch: 4.0.5
- yaml: 2.3.4
- dev: false
-
- /markdownlint-micromark@0.1.8:
- resolution: {integrity: sha512-1ouYkMRo9/6gou9gObuMDnvZM8jC/ly3QCFQyoSPCS2XV1ZClU0xpKbL1Ar3bWWRT1RnBZkWUEiNKrI2CwiBQA==}
- engines: {node: '>=16'}
- dev: false
+ markdown-it: 14.1.0
+ micromark: 4.0.1
+ micromark-core-commonmark: 2.0.2
+ micromark-extension-directive: 3.0.2
+ micromark-extension-gfm-autolink-literal: 2.1.0
+ micromark-extension-gfm-footnote: 2.1.0
+ micromark-extension-gfm-table: 2.1.0
+ micromark-extension-math: 3.1.0
+ micromark-util-types: 2.0.1
+ transitivePeerDependencies:
+ - supports-color
- /markdownlint@0.33.0:
- resolution: {integrity: sha512-4lbtT14A3m0LPX1WS/3d1m7Blg+ZwiLq36WvjQqFGsX3Gik99NV+VXp/PW3n+Q62xyPdbvGOCfjPqjW+/SKMig==}
- engines: {node: '>=18'}
- dependencies:
- markdown-it: 14.0.0
- markdownlint-micromark: 0.1.8
- dev: false
+ marked@16.4.2: {}
- /mathjax-full@3.2.2:
- resolution: {integrity: sha512-+LfG9Fik+OuI8SLwsiR02IVdjcnRCy5MufYLi0C3TdMT56L/pjB0alMVGgoWJF8pN9Rc7FESycZB9BMNWIid5w==}
+ mathjax-full@3.2.2:
dependencies:
esm: 3.2.25
mhchemparser: 4.2.1
mj-context-menu: 0.6.1
- speech-rule-engine: 4.0.7
- dev: false
-
- /mdurl@2.0.0:
- resolution: {integrity: sha512-Lf+9+2r+Tdp5wXDXC4PcIBjTDtq4UKjCPMQhKIuzpJNW0b96kVqSwW0bT7FhRSfmAiFYgP+SCRvdrDozfh0U5w==}
- dev: false
-
- /merge-stream@2.0.0:
- resolution: {integrity: sha512-abv/qOcuPfk3URPfDzmZU1LKmuw8kT+0nIHvKrKgFrwifol/doWcdA4ZqsWQ8ENrFKkd67Mfpo/LovbIUsbt3w==}
- dev: false
-
- /merge2@1.4.1:
- resolution: {integrity: sha512-8q7VEgMJW4J8tcfVPy8g09NcQwZdbwFEqhe/WZkoIzjn/3TGDwtOCYtXGxA3O8tPzpczCCDgv+P2P5y00ZJOOg==}
- engines: {node: '>= 8'}
- dev: false
-
- /mhchemparser@4.2.1:
- resolution: {integrity: sha512-kYmyrCirqJf3zZ9t/0wGgRZ4/ZJw//VwaRVGA75C4nhE60vtnIzhl9J9ndkX/h6hxSN7pjg/cE0VxbnNM+bnDQ==}
- dev: false
+ speech-rule-engine: 4.1.2
+
+ mdast-util-to-hast@13.2.1:
+ dependencies:
+ '@types/hast': 3.0.4
+ '@types/mdast': 4.0.4
+ '@ungap/structured-clone': 1.3.0
+ devlop: 1.1.0
+ micromark-util-sanitize-uri: 2.0.1
+ trim-lines: 3.0.1
+ unist-util-position: 5.0.0
+ unist-util-visit: 5.0.0
+ vfile: 6.0.3
+
+ mdurl@2.0.0: {}
+
+ merge2@1.4.1: {}
+
+ mermaid@11.12.2:
+ dependencies:
+ '@braintree/sanitize-url': 7.1.1
+ '@iconify/utils': 3.1.0
+ '@mermaid-js/parser': 0.6.3
+ '@types/d3': 7.4.3
+ cytoscape: 3.33.1
+ cytoscape-cose-bilkent: 4.1.0(cytoscape@3.33.1)
+ cytoscape-fcose: 2.2.0(cytoscape@3.33.1)
+ d3: 7.9.0
+ d3-sankey: 0.12.3
+ dagre-d3-es: 7.0.13
+ dayjs: 1.11.19
+ dompurify: 3.3.1
+ katex: 0.16.27
+ khroma: 2.1.0
+ lodash-es: 4.17.22
+ marked: 16.4.2
+ roughjs: 4.6.6
+ stylis: 4.3.6
+ ts-dedent: 2.2.0
+ uuid: 11.1.0
+
+ mhchemparser@4.2.1: {}
+
+ micromark-core-commonmark@2.0.2:
+ dependencies:
+ decode-named-character-reference: 1.2.0
+ devlop: 1.1.0
+ micromark-factory-destination: 2.0.1
+ micromark-factory-label: 2.0.1
+ micromark-factory-space: 2.0.1
+ micromark-factory-title: 2.0.1
+ micromark-factory-whitespace: 2.0.1
+ micromark-util-character: 2.1.1
+ micromark-util-chunked: 2.0.1
+ micromark-util-classify-character: 2.0.1
+ micromark-util-html-tag-name: 2.0.1
+ micromark-util-normalize-identifier: 2.0.1
+ micromark-util-resolve-all: 2.0.1
+ micromark-util-subtokenize: 2.1.0
+ micromark-util-symbol: 2.0.1
+ micromark-util-types: 2.0.1
+
+ micromark-extension-directive@3.0.2:
+ dependencies:
+ devlop: 1.1.0
+ micromark-factory-space: 2.0.1
+ micromark-factory-whitespace: 2.0.1
+ micromark-util-character: 2.1.1
+ micromark-util-symbol: 2.0.1
+ micromark-util-types: 2.0.1
+ parse-entities: 4.0.2
- /micromatch@4.0.5:
- resolution: {integrity: sha512-DMy+ERcEW2q8Z2Po+WNXuw3c5YaUSFjAO5GsJqfEl7UjvtIuFKO6ZrKvcItdy98dwFI2N1tg3zNIdKaQT+aNdA==}
- engines: {node: '>=8.6'}
+ micromark-extension-gfm-autolink-literal@2.1.0:
dependencies:
- braces: 3.0.2
- picomatch: 2.3.1
- dev: false
-
- /mimic-fn@2.1.0:
- resolution: {integrity: sha512-OqbOk5oEQeAZ8WXWydlu9HJjz9WVdEIvamMCcXmuqUYjTknH/sqsWvhQ3vgwKFRR1HpjvNBKQ37nbJgYzGqGcg==}
- engines: {node: '>=6'}
- dev: false
+ micromark-util-character: 2.1.1
+ micromark-util-sanitize-uri: 2.0.1
+ micromark-util-symbol: 2.0.1
+ micromark-util-types: 2.0.1
- /mimic-fn@4.0.0:
- resolution: {integrity: sha512-vqiC06CuhBTUdZH+RYl8sFrL096vA45Ok5ISO6sE/Mr1jRbGH4Csnhi8f3wKVl7x8mO4Au7Ir9D3Oyv1VYMFJw==}
- engines: {node: '>=12'}
- dev: false
+ micromark-extension-gfm-footnote@2.1.0:
+ dependencies:
+ devlop: 1.1.0
+ micromark-core-commonmark: 2.0.2
+ micromark-factory-space: 2.0.1
+ micromark-util-character: 2.1.1
+ micromark-util-normalize-identifier: 2.0.1
+ micromark-util-sanitize-uri: 2.0.1
+ micromark-util-symbol: 2.0.1
+ micromark-util-types: 2.0.1
- /minimatch@3.1.2:
- resolution: {integrity: sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==}
+ micromark-extension-gfm-table@2.1.0:
dependencies:
- brace-expansion: 1.1.11
- dev: false
+ devlop: 1.1.0
+ micromark-factory-space: 2.0.1
+ micromark-util-character: 2.1.1
+ micromark-util-symbol: 2.0.1
+ micromark-util-types: 2.0.1
- /minimatch@9.0.4:
- resolution: {integrity: sha512-KqWh+VchfxcMNRAJjj2tnsSJdNbHsVgnkBhTNrW7AjVo6OvLtxw8zfT9oLw1JSohlFzJ8jCoTgaoXvJ+kHt6fw==}
- engines: {node: '>=16 || 14 >=14.17'}
+ micromark-extension-math@3.1.0:
dependencies:
- brace-expansion: 2.0.1
- dev: false
+ '@types/katex': 0.16.8
+ devlop: 1.1.0
+ katex: 0.16.27
+ micromark-factory-space: 2.0.1
+ micromark-util-character: 2.1.1
+ micromark-util-symbol: 2.0.1
+ micromark-util-types: 2.0.1
- /minipass-collect@2.0.1:
- resolution: {integrity: sha512-D7V8PO9oaz7PWGLbCACuI1qEOsq7UKfLotx/C0Aet43fCUB/wfQ7DYeq2oR/svFJGYDHPr38SHATeaj/ZoKHKw==}
- engines: {node: '>=16 || 14 >=14.17'}
+ micromark-factory-destination@2.0.1:
dependencies:
- minipass: 7.0.4
- dev: false
+ micromark-util-character: 2.1.1
+ micromark-util-symbol: 2.0.1
+ micromark-util-types: 2.0.1
- /minipass-fetch@3.0.4:
- resolution: {integrity: sha512-jHAqnA728uUpIaFm7NWsCnqKT6UqZz7GcI/bDpPATuwYyKwJwW0remxSCxUlKiEty+eopHGa3oc8WxgQ1FFJqg==}
- engines: {node: ^14.17.0 || ^16.13.0 || >=18.0.0}
+ micromark-factory-label@2.0.1:
dependencies:
- minipass: 7.0.4
- minipass-sized: 1.0.3
- minizlib: 2.1.2
- optionalDependencies:
- encoding: 0.1.13
- dev: false
+ devlop: 1.1.0
+ micromark-util-character: 2.1.1
+ micromark-util-symbol: 2.0.1
+ micromark-util-types: 2.0.1
- /minipass-flush@1.0.5:
- resolution: {integrity: sha512-JmQSYYpPUqX5Jyn1mXaRwOda1uQ8HP5KAT/oDSLCzt1BYRhQU0/hDtsB1ufZfEEzMZ9aAVmsBw8+FWsIXlClWw==}
- engines: {node: '>= 8'}
+ micromark-factory-space@2.0.1:
dependencies:
- minipass: 3.3.6
- dev: false
+ micromark-util-character: 2.1.1
+ micromark-util-types: 2.0.1
- /minipass-pipeline@1.2.4:
- resolution: {integrity: sha512-xuIq7cIOt09RPRJ19gdi4b+RiNvDFYe5JH+ggNvBqGqpQXcru3PcRmOZuHBKWK1Txf9+cQ+HMVN4d6z46LZP7A==}
- engines: {node: '>=8'}
+ micromark-factory-title@2.0.1:
dependencies:
- minipass: 3.3.6
- dev: false
+ micromark-factory-space: 2.0.1
+ micromark-util-character: 2.1.1
+ micromark-util-symbol: 2.0.1
+ micromark-util-types: 2.0.1
- /minipass-sized@1.0.3:
- resolution: {integrity: sha512-MbkQQ2CTiBMlA2Dm/5cY+9SWFEN8pzzOXi6rlM5Xxq0Yqbda5ZQy9sU75a673FE9ZK0Zsbr6Y5iP6u9nktfg2g==}
- engines: {node: '>=8'}
+ micromark-factory-whitespace@2.0.1:
dependencies:
- minipass: 3.3.6
- dev: false
+ micromark-factory-space: 2.0.1
+ micromark-util-character: 2.1.1
+ micromark-util-symbol: 2.0.1
+ micromark-util-types: 2.0.1
- /minipass@3.3.6:
- resolution: {integrity: sha512-DxiNidxSEK+tHG6zOIklvNOwm3hvCrbUrdtzY74U6HKTJxvIDfOUL5W5P2Ghd3DTkhhKPYGqeNUIh5qcM4YBfw==}
- engines: {node: '>=8'}
+ micromark-util-character@2.1.1:
dependencies:
- yallist: 4.0.0
- dev: false
+ micromark-util-symbol: 2.0.1
+ micromark-util-types: 2.0.1
- /minipass@5.0.0:
- resolution: {integrity: sha512-3FnjYuehv9k6ovOEbyOswadCDPX1piCfhV8ncmYtHOjuPwylVWsghTLo7rabjC3Rx5xD4HDx8Wm1xnMF7S5qFQ==}
- engines: {node: '>=8'}
- dev: false
+ micromark-util-chunked@2.0.1:
+ dependencies:
+ micromark-util-symbol: 2.0.1
- /minipass@7.0.4:
- resolution: {integrity: sha512-jYofLM5Dam9279rdkWzqHozUo4ybjdZmCsDHePy5V/PbBcVMiSZR97gmAy45aqi8CK1lG2ECd356FU86avfwUQ==}
- engines: {node: '>=16 || 14 >=14.17'}
- dev: false
+ micromark-util-classify-character@2.0.1:
+ dependencies:
+ micromark-util-character: 2.1.1
+ micromark-util-symbol: 2.0.1
+ micromark-util-types: 2.0.1
- /minizlib@2.1.2:
- resolution: {integrity: sha512-bAxsR8BVfj60DWXHE3u30oHzfl4G7khkSuPW+qvpd7jFRHm7dLxOjUk1EHACJ/hxLY8phGJ0YhYHZo7jil7Qdg==}
- engines: {node: '>= 8'}
+ micromark-util-combine-extensions@2.0.1:
dependencies:
- minipass: 3.3.6
- yallist: 4.0.0
- dev: false
+ micromark-util-chunked: 2.0.1
+ micromark-util-types: 2.0.1
- /mj-context-menu@0.6.1:
- resolution: {integrity: sha512-7NO5s6n10TIV96d4g2uDpG7ZDpIhMh0QNfGdJw/W47JswFcosz457wqz/b5sAKvl12sxINGFCn80NZHKwxQEXA==}
- dev: false
+ micromark-util-decode-numeric-character-reference@2.0.2:
+ dependencies:
+ micromark-util-symbol: 2.0.1
- /mkdirp@1.0.4:
- resolution: {integrity: sha512-vVqVZQyf3WLx2Shd0qJ9xuvqgAyKPLAiqITEtqW0oIUjzo3PePDd6fW9iFz30ef7Ysp/oiWqbhszeGWW2T6Gzw==}
- engines: {node: '>=10'}
- hasBin: true
- dev: false
+ micromark-util-encode@2.0.1: {}
- /ms@2.1.2:
- resolution: {integrity: sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==}
- dev: false
+ micromark-util-html-tag-name@2.0.1: {}
- /nano-staged@0.8.0:
- resolution: {integrity: sha512-QSEqPGTCJbkHU2yLvfY6huqYPjdBrOaTMKatO1F8nCSrkQGXeKwtCiCnsdxnuMhbg3DTVywKaeWLGCE5oJpq0g==}
- engines: {node: ^12.20.0 || ^14.13.1 || >=16.0.0}
- hasBin: true
+ micromark-util-normalize-identifier@2.0.1:
dependencies:
- picocolors: 1.0.0
- dev: false
-
- /nanoid@3.3.7:
- resolution: {integrity: sha512-eSRppjcPIatRIMC1U6UngP8XFcz8MQWGQdt1MTBQ7NaAmvXDfvNxbvWV3x2y6CdEUciCSsDHDQZbhYaB8QEo2g==}
- engines: {node: ^10 || ^12 || ^13.7 || ^14 || >=15.0.1}
- hasBin: true
- dev: false
+ micromark-util-symbol: 2.0.1
- /negotiator@0.6.3:
- resolution: {integrity: sha512-+EUsqGPLsM+j/zdChZjsnX51g4XrHFOIXwfnCVPGlQk/k5giakcKsuxCObBRu6DSm9opw/O6slWbJdghQM4bBg==}
- engines: {node: '>= 0.6'}
- dev: false
-
- /node-addon-api@7.1.0:
- resolution: {integrity: sha512-mNcltoe1R8o7STTegSOHdnJNN7s5EUvhoS7ShnTHDyOSd+8H+UdWODq6qSv67PjC8Zc5JRT8+oLAMCr0SIXw7g==}
- engines: {node: ^16 || ^18 || >= 20}
- dev: false
+ micromark-util-resolve-all@2.0.1:
+ dependencies:
+ micromark-util-types: 2.0.1
- /node-fetch@2.7.0:
- resolution: {integrity: sha512-c4FRfUm/dbcWZ7U+1Wq0AwCyFL+3nt2bEw05wfxSz+DWpWsitgmSgYmy2dQdWyKC1694ELPqMs/YzUSNozLt8A==}
- engines: {node: 4.x || >=6.0.0}
- peerDependencies:
- encoding: ^0.1.0
- peerDependenciesMeta:
- encoding:
- optional: true
+ micromark-util-sanitize-uri@2.0.1:
dependencies:
- whatwg-url: 5.0.0
- dev: false
+ micromark-util-character: 2.1.1
+ micromark-util-encode: 2.0.1
+ micromark-util-symbol: 2.0.1
- /node-gyp@10.1.0:
- resolution: {integrity: sha512-B4J5M1cABxPc5PwfjhbV5hoy2DP9p8lFXASnEN6hugXOa61416tnTZ29x9sSwAd0o99XNIcpvDDy1swAExsVKA==}
- engines: {node: ^16.14.0 || >=18.0.0}
- hasBin: true
+ micromark-util-subtokenize@2.1.0:
dependencies:
- env-paths: 2.2.1
- exponential-backoff: 3.1.1
- glob: 10.3.12
- graceful-fs: 4.2.11
- make-fetch-happen: 13.0.0
- nopt: 7.2.0
- proc-log: 3.0.0
- semver: 7.6.0
- tar: 6.2.1
- which: 4.0.0
- transitivePeerDependencies:
- - supports-color
- dev: false
+ devlop: 1.1.0
+ micromark-util-chunked: 2.0.1
+ micromark-util-symbol: 2.0.1
+ micromark-util-types: 2.0.1
+
+ micromark-util-symbol@2.0.1: {}
- /node-releases@2.0.14:
- resolution: {integrity: sha512-y10wOWt8yZpqXmOgRo77WaHEmhYQYGNA6y421PKsKYWEK8aW+cqAphborZDhqfyKrbZEN92CN1X2KbafY2s7Yw==}
- dev: false
+ micromark-util-types@2.0.1: {}
- /nodejs-jieba@0.1.2:
- resolution: {integrity: sha512-VOBhc1muuH00apYn78bVJSPUn7xakyawJNTl+3jVu3038FpxKBp4qxfGkpb07DXWFf12IlcIT4UzmhqXzASKxg==}
- engines: {node: ^18.0.0 || ^20.0.0}
- requiresBuild: true
+ micromark@4.0.1:
dependencies:
- '@mapbox/node-pre-gyp': 1.0.11
- node-addon-api: 7.1.0
- node-gyp: 10.1.0
+ '@types/debug': 4.1.12
+ debug: 4.4.3
+ decode-named-character-reference: 1.2.0
+ devlop: 1.1.0
+ micromark-core-commonmark: 2.0.2
+ micromark-factory-space: 2.0.1
+ micromark-util-character: 2.1.1
+ micromark-util-chunked: 2.0.1
+ micromark-util-combine-extensions: 2.0.1
+ micromark-util-decode-numeric-character-reference: 2.0.2
+ micromark-util-encode: 2.0.1
+ micromark-util-normalize-identifier: 2.0.1
+ micromark-util-resolve-all: 2.0.1
+ micromark-util-sanitize-uri: 2.0.1
+ micromark-util-subtokenize: 2.1.0
+ micromark-util-symbol: 2.0.1
+ micromark-util-types: 2.0.1
transitivePeerDependencies:
- - encoding
- supports-color
- dev: false
- /nopt@5.0.0:
- resolution: {integrity: sha512-Tbj67rffqceeLpcRXrT7vKAN8CwfPeIBgM7E6iBkmKLV7bEMwpGgYLGv0jACUsECaa/vuxP0IjEont6umdMgtQ==}
- engines: {node: '>=6'}
- hasBin: true
+ micromatch@4.0.8:
dependencies:
- abbrev: 1.1.1
- dev: false
+ braces: 3.0.3
+ picomatch: 2.3.1
- /nopt@7.2.0:
- resolution: {integrity: sha512-CVDtwCdhYIvnAzFoJ6NJ6dX3oga9/HyciQDnG1vQDjSLMeKLJ4A93ZqYKDrgYSr1FBY5/hMYC+2VCi24pgpkGA==}
- engines: {node: ^14.17.0 || ^16.13.0 || >=18.0.0}
- hasBin: true
- dependencies:
- abbrev: 2.0.0
- dev: false
+ mimic-function@5.0.1: {}
- /normalize-path@3.0.0:
- resolution: {integrity: sha512-6eZs5Ls3WtCisHWp9S2GUy8dqkpGi4BVSz3GaqiE6ezub0512ESztXUwUB6C6IKbQkY2Pnb/mD4WYojCRwcwLA==}
- engines: {node: '>=0.10.0'}
- dev: false
+ mitt@3.0.1: {}
- /normalize-range@0.1.2:
- resolution: {integrity: sha512-bdok/XvKII3nUpklnV6P2hxtMNrCboOjAcyBuQnWEhO665FwrSNRxU+AqpsyvO6LgGYPspN+lu5CLtw4jPRKNA==}
- engines: {node: '>=0.10.0'}
- dev: false
+ mj-context-menu@0.6.1: {}
- /npm-run-path@5.3.0:
- resolution: {integrity: sha512-ppwTtiJZq0O/ai0z7yfudtBpWIoxM8yE6nHi1X47eFR2EWORqfbu6CnPlNsjeN683eT0qG6H/Pyf9fCcvjnnnQ==}
- engines: {node: ^12.20.0 || ^14.13.1 || >=16.0.0}
+ mlly@1.8.0:
dependencies:
- path-key: 4.0.0
- dev: false
+ acorn: 8.15.0
+ pathe: 2.0.3
+ pkg-types: 1.3.1
+ ufo: 1.6.3
- /npmlog@5.0.1:
- resolution: {integrity: sha512-AqZtDUWOMKs1G/8lwylVjrdYgqA4d9nu8hc+0gzRxlDb1I10+FHBGMXs6aiQHFdCUUlqH99MUMuLfzWDNDtfxw==}
- dependencies:
- are-we-there-yet: 2.0.0
- console-control-strings: 1.1.0
- gauge: 3.0.2
- set-blocking: 2.0.0
- dev: false
+ ms@2.1.3: {}
- /nth-check@2.1.1:
- resolution: {integrity: sha512-lqjrjmaOoAnWfMmBPL+XNnynZh2+swxiX3WUE0s4yEHI6m+AwrK2UZOimIRl3X/4QctVqS8AiZjFqyOGrMXb/w==}
+ nano-staged@0.8.0:
dependencies:
- boolbase: 1.0.0
- dev: false
+ picocolors: 1.1.1
- /object-assign@4.1.1:
- resolution: {integrity: sha512-rJgTQnkUnH1sFw8yT6VSU3zD3sWmu6sZhIseY8VX+GRu3P6F7Fu+JNDoXfklElbLJSnc3FUQHVe4cU5hj+BcUg==}
- engines: {node: '>=0.10.0'}
- dev: false
+ nanoid@3.3.11: {}
+
+ nanoid@5.1.6: {}
+
+ node-addon-api@7.1.1:
+ optional: true
+
+ node-releases@2.0.27: {}
- /once@1.4.0:
- resolution: {integrity: sha512-lNaJgI+2Q5URQBkccEKHTQOPaXdUxnZZElQTZY0MFUAuaEqe1E+Nyvgdz/aIyNi6Z9MzO5dv1H8n58/GELp3+w==}
+ nth-check@2.1.1:
dependencies:
- wrappy: 1.0.2
- dev: false
+ boolbase: 1.0.0
- /onetime@5.1.2:
- resolution: {integrity: sha512-kbpaSSGJTWdAY5KPVeMOKXSrPtr8C8C7wodJbcsd51jRnmD+GZu8Y0VoU6Dm5Z4vWr0Ig/1NKuWRKf7j5aaYSg==}
- engines: {node: '>=6'}
+ onetime@7.0.0:
dependencies:
- mimic-fn: 2.1.0
- dev: false
+ mimic-function: 5.0.1
- /onetime@6.0.0:
- resolution: {integrity: sha512-1FlR+gjXK7X+AsAHso35MnyN5KqGwJRi/31ft6x0M194ht7S+rWAvd7PHss9xSKMzE0asv1pyIHaJYq+BbacAQ==}
- engines: {node: '>=12'}
+ oniguruma-parser@0.12.1: {}
+
+ oniguruma-to-es@4.3.4:
dependencies:
- mimic-fn: 4.0.0
- dev: false
+ oniguruma-parser: 0.12.1
+ regex: 6.1.0
+ regex-recursion: 6.0.2
- /ora@8.0.1:
- resolution: {integrity: sha512-ANIvzobt1rls2BDny5fWZ3ZVKyD6nscLvfFRpQgfWsythlcsVUC9kL0zq6j2Z5z9wwp1kd7wpsD/T9qNPVLCaQ==}
- engines: {node: '>=18'}
+ ora@9.0.0:
dependencies:
- chalk: 5.3.0
- cli-cursor: 4.0.0
- cli-spinners: 2.9.2
+ chalk: 5.6.2
+ cli-cursor: 5.0.0
+ cli-spinners: 3.4.0
is-interactive: 2.0.0
- is-unicode-supported: 2.0.0
- log-symbols: 6.0.0
+ is-unicode-supported: 2.1.0
+ log-symbols: 7.0.1
stdin-discarder: 0.2.2
- string-width: 7.1.0
- strip-ansi: 7.1.0
- dev: false
+ string-width: 8.1.0
+ strip-ansi: 7.1.2
- /p-limit@2.3.0:
- resolution: {integrity: sha512-//88mFWSJx8lxCzwdAABTJL2MyWB12+eIY7MDL2SqLmAkeKU9qxRvWuSyTjm3FUmpBEMuFfckAIqEaVGUDxb6w==}
- engines: {node: '>=6'}
+ p-limit@2.3.0:
dependencies:
p-try: 2.2.0
- dev: false
- /p-locate@4.1.0:
- resolution: {integrity: sha512-R79ZZ/0wAxKGu3oYMlz8jy/kbhsNrS7SKZ7PxEHBgJ5+F2mtFW2fK2cOtBh1cHYkQsbzFV7I+EoRKe6Yt0oK7A==}
- engines: {node: '>=8'}
+ p-locate@4.1.0:
dependencies:
p-limit: 2.3.0
- dev: false
- /p-map@4.0.0:
- resolution: {integrity: sha512-/bjOqmgETBYB5BoEeGVea8dmvHb2m9GLy1E9W43yeyfP6QQCZGFNa+XRceJEuDB6zqr+gKpIAmlLebMpykw/MQ==}
- engines: {node: '>=10'}
- dependencies:
- aggregate-error: 3.1.0
- dev: false
+ p-try@2.2.0: {}
- /p-try@2.2.0:
- resolution: {integrity: sha512-R4nPAVTAU0B9D35/Gk3uJf/7XYbQcyohSKdvAxIRSNghFl4e71hVoGnBNQz9cWaXxO2I10KTC+3jMdvvoKw6dQ==}
- engines: {node: '>=6'}
- dev: false
+ package-manager-detector@1.6.0: {}
+
+ parse-entities@4.0.2:
+ dependencies:
+ '@types/unist': 2.0.11
+ character-entities-legacy: 3.0.0
+ character-reference-invalid: 2.0.1
+ decode-named-character-reference: 1.2.0
+ is-alphanumerical: 2.0.1
+ is-decimal: 2.0.1
+ is-hexadecimal: 2.0.1
- /parse5-htmlparser2-tree-adapter@7.0.0:
- resolution: {integrity: sha512-B77tOZrqqfUfnVcOrUvfdLbz4pu4RopLD/4vmu3HUPswwTA8OH0EMW9BlWR2B0RCoiZRAHEUu7IxeP1Pd1UU+g==}
+ parse5-htmlparser2-tree-adapter@7.1.0:
dependencies:
domhandler: 5.0.3
- parse5: 7.1.2
- dev: false
+ parse5: 7.3.0
- /parse5@7.1.2:
- resolution: {integrity: sha512-Czj1WaSVpaoj0wbhMzLmWD69anp2WH7FXMB9n1Sy8/ZFF9jolSQVMu1Ij5WIyGmcBmhk7EOndpO4mIpihVqAXw==}
+ parse5-parser-stream@7.1.2:
dependencies:
- entities: 4.5.0
- dev: false
+ parse5: 7.3.0
- /path-exists@4.0.0:
- resolution: {integrity: sha512-ak9Qy5Q7jYb2Wwcey5Fpvg2KoAc/ZIhLSLOSBmRmygPsGwkVVt0fZa0qrtMz+m6tJTAHfZQ8FnmB4MG4LWy7/w==}
- engines: {node: '>=8'}
- dev: false
+ parse5@7.3.0:
+ dependencies:
+ entities: 6.0.1
- /path-is-absolute@1.0.1:
- resolution: {integrity: sha512-AVbw3UJ2e9bq64vSaS9Am0fje1Pa8pbGqTTsmXfaIiMpnr5DlDhfJOuLj9Sf95ZPVDAUerDfEk88MPmPe7UCQg==}
- engines: {node: '>=0.10.0'}
- dev: false
+ path-data-parser@0.1.0: {}
- /path-key@3.1.1:
- resolution: {integrity: sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q==}
- engines: {node: '>=8'}
- dev: false
+ path-exists@4.0.0: {}
- /path-key@4.0.0:
- resolution: {integrity: sha512-haREypq7xkM7ErfgIyA0z+Bj4AGKlMSdlQE2jvJo6huWD1EdkKYV+G/T4nq0YEF2vgTT8kqMFKo1uHn950r4SQ==}
- engines: {node: '>=12'}
- dev: false
+ path-type@5.0.0: {}
- /path-scurry@1.10.2:
- resolution: {integrity: sha512-7xTavNy5RQXnsjANvVvMkEjvloOinkAjv/Z6Ildz9v2RinZ4SBKTWFOVRbaF8p0vpHnyjV/UwNDdKuUv6M5qcA==}
- engines: {node: '>=16 || 14 >=14.17'}
- dependencies:
- lru-cache: 10.2.0
- minipass: 7.0.4
- dev: false
+ pathe@2.0.3: {}
- /path-type@5.0.0:
- resolution: {integrity: sha512-5HviZNaZcfqP95rwpv+1HDgUamezbqdSYTyzjTvwtJSnIH+3vnbmWsItli8OFEndS984VT55M3jduxZbX351gg==}
- engines: {node: '>=12'}
- dev: false
+ perfect-debounce@2.0.0: {}
- /photoswipe@5.4.3:
- resolution: {integrity: sha512-9UC6oJBK4oXFZ5HcdlcvGkfEHsVrmE4csUdCQhEjHYb3PvPLO3PG7UhnPuOgjxwmhq5s17Un5NUdum01LgBDng==}
- engines: {node: '>= 0.12.0'}
- dev: false
+ photoswipe@5.4.4: {}
- /picocolors@1.0.0:
- resolution: {integrity: sha512-1fygroTLlHu66zi26VoTDv8yRgm0Fccecssto+MhsZ0D/DGW2sm8E8AjW7NU5VVTRt5GxbeZ5qBuJr+HyLYkjQ==}
- dev: false
+ picocolors@1.1.1: {}
- /picomatch@2.3.1:
- resolution: {integrity: sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA==}
- engines: {node: '>=8.6'}
- dev: false
+ picomatch@2.3.1: {}
- /pngjs@5.0.0:
- resolution: {integrity: sha512-40QW5YalBNfQo5yRYmiw7Yz6TKKVr3h6970B2YE+3fQpsWcrbj1PzJgxeJ19DRQjhMbKPIuMY8rFaXc8moolVw==}
- engines: {node: '>=10.13.0'}
- dev: false
+ picomatch@4.0.3: {}
- /postcss-load-config@5.0.3(postcss@8.4.38):
- resolution: {integrity: sha512-90pBBI5apUVruIEdCxZic93Wm+i9fTrp7TXbgdUCH+/L+2WnfpITSpq5dFU/IPvbv7aNiMlQISpUkAm3fEcvgQ==}
- engines: {node: '>= 18'}
- peerDependencies:
- jiti: '>=1.21.0'
- postcss: '>=8.0.9'
- peerDependenciesMeta:
- jiti:
- optional: true
- postcss:
- optional: true
+ pkg-types@1.3.1:
dependencies:
- lilconfig: 3.1.1
- postcss: 8.4.38
- yaml: 2.4.1
- dev: false
+ confbox: 0.1.8
+ mlly: 1.8.0
+ pathe: 2.0.3
- /postcss-value-parser@4.2.0:
- resolution: {integrity: sha512-1NNCs6uurfkVbeXG4S8JFT9t19m45ICnif8zWLd5oPSZ50QnwMfK+H3jv408d4jw/7Bttv5axS5IiHoLaVNHeQ==}
- dev: false
+ pngjs@5.0.0: {}
- /postcss@8.4.38:
- resolution: {integrity: sha512-Wglpdk03BSfXkHoQa3b/oulrotAkwrlLDRSOb9D0bN86FdRyE9lppSp33aHNPgBa0JKCoB+drFLZkQoRRYae5A==}
- engines: {node: ^10 || ^12 || >=14}
- dependencies:
- nanoid: 3.3.7
- picocolors: 1.0.0
- source-map-js: 1.2.0
- dev: false
+ points-on-curve@0.2.0: {}
- /prettier@3.2.5:
- resolution: {integrity: sha512-3/GWa9aOC0YeD7LUfvOG2NiDyhOWRvt1k+rcKhOuYnMY24iiCphgneUfJDyFXd6rZCAnuLBv6UeAULtrhT/F4A==}
- engines: {node: '>=14'}
- hasBin: true
- dev: false
+ points-on-path@0.2.1:
+ dependencies:
+ path-data-parser: 0.1.0
+ points-on-curve: 0.2.0
- /prismjs@1.29.0:
- resolution: {integrity: sha512-Kx/1w86q/epKcmte75LNrEoT+lX8pBpavuAbvJWRXar7Hz8jrtF+e3vY751p0R8H9HdArwaCTNDDzHg/ScJK1Q==}
- engines: {node: '>=6'}
- dev: false
+ postcss-load-config@6.0.1(postcss@8.5.6):
+ dependencies:
+ lilconfig: 3.1.3
+ optionalDependencies:
+ postcss: 8.5.6
- /proc-log@3.0.0:
- resolution: {integrity: sha512-++Vn7NS4Xf9NacaU9Xq3URUuqZETPsf8L4j5/ckhaRYsfPeRyzGw+iDjFhV/Jr3uNmTvvddEJFWh5R1gRgUH8A==}
- engines: {node: ^14.17.0 || ^16.13.0 || >=18.0.0}
- dev: false
+ postcss-value-parser@4.2.0: {}
- /promise-retry@2.0.1:
- resolution: {integrity: sha512-y+WKFlBR8BGXnsNlIHFGPZmyDf3DFMoLhaflAnyZgV6rG6xu+JwesTo2Q9R6XwYmtmwAFCkAk3e35jEdoeh/3g==}
- engines: {node: '>=10'}
+ postcss@8.5.6:
dependencies:
- err-code: 2.0.3
- retry: 0.12.0
- dev: false
+ nanoid: 3.3.11
+ picocolors: 1.1.1
+ source-map-js: 1.2.1
- /punycode.js@2.3.1:
- resolution: {integrity: sha512-uxFIHU0YlHYhDQtV4R9J6a52SLx28BCjT+4ieh7IGbgwVJWO+km431c4yRlREUAsAmt/uMjQUyQHNEPf0M39CA==}
- engines: {node: '>=6'}
- dev: false
+ prettier@3.4.2: {}
- /qrcode@1.5.3:
- resolution: {integrity: sha512-puyri6ApkEHYiVl4CFzo1tDkAZ+ATcnbJrJ6RiBM1Fhctdn/ix9MTE3hRph33omisEbC/2fcfemsseiKgBPKZg==}
- engines: {node: '>=10.13.0'}
- hasBin: true
+ property-information@7.1.0: {}
+
+ punycode.js@2.3.1: {}
+
+ qrcode@1.5.4:
dependencies:
dijkstrajs: 1.0.3
- encode-utf8: 1.0.3
pngjs: 5.0.0
yargs: 15.4.1
- dev: false
- /queue-microtask@1.2.3:
- resolution: {integrity: sha512-NuaNSa6flKT5JaSYQzJok04JzTL1CA6aGhv5rfLW3PgqA+M2ChpZQnAC8h8i4ZFkBS8X5RqkDBHA7r4hej3K9A==}
- dev: false
+ queue-microtask@1.2.3: {}
- /readable-stream@3.6.2:
- resolution: {integrity: sha512-9u/sniCrY3D5WdsERHzHE4G2YCXqoG5FTHUiCC4SIbr6XcLZBY05ya9EKjYek9O5xOAwjGq+1JdGBAS7Q9ScoA==}
- engines: {node: '>= 6'}
+ readdirp@4.1.2: {}
+
+ readdirp@5.0.0: {}
+
+ regex-recursion@6.0.2:
dependencies:
- inherits: 2.0.4
- string_decoder: 1.3.0
- util-deprecate: 1.0.2
- dev: false
+ regex-utilities: 2.3.0
+
+ regex-utilities@2.3.0: {}
- /readdirp@3.6.0:
- resolution: {integrity: sha512-hOS089on8RduqdbhvQ5Z37A0ESjsqz6qnRcffsMU3495FuTdqSm+7bhJ29JvIOsBDEEnan5DPu9t3To9VRlMzA==}
- engines: {node: '>=8.10.0'}
+ regex@6.1.0:
dependencies:
- picomatch: 2.3.1
- dev: false
+ regex-utilities: 2.3.0
- /require-directory@2.1.1:
- resolution: {integrity: sha512-fGxEI7+wsG9xrvdjsrlmL22OMTTiHRwAMroiEeMgq8gzoLC/PQr7RsRDSTLUg/bZAZtF+TVIkHc6/4RIKrui+Q==}
- engines: {node: '>=0.10.0'}
- dev: false
+ rehype-parse@9.0.1:
+ dependencies:
+ '@types/hast': 3.0.4
+ hast-util-from-html: 2.0.3
+ unified: 11.0.5
- /require-main-filename@2.0.0:
- resolution: {integrity: sha512-NKN5kMDylKuldxYLSUfrbo5Tuzh4hd+2E8NPPX02mZtn1VuREQToYe/ZdlJy+J3uCpfaiGF05e7B8W0iXbQHmg==}
- dev: false
+ rehype-sanitize@6.0.0:
+ dependencies:
+ '@types/hast': 3.0.4
+ hast-util-sanitize: 5.0.2
- /restore-cursor@4.0.0:
- resolution: {integrity: sha512-I9fPXU9geO9bHOt9pHHOhOkYerIMsmVaWB0rA2AI9ERh/+x/i7MV5HKBNrg+ljO5eoPVgCcnFuRjJ9uH6I/3eg==}
- engines: {node: ^12.20.0 || ^14.13.1 || >=16.0.0}
+ rehype-stringify@10.0.1:
dependencies:
- onetime: 5.1.2
- signal-exit: 3.0.7
- dev: false
+ '@types/hast': 3.0.4
+ hast-util-to-html: 9.0.5
+ unified: 11.0.5
- /retry@0.12.0:
- resolution: {integrity: sha512-9LkiTwjUh6rT555DtE9rTX+BKByPfrMzEAtnlEtdEwr3Nkffwiihqe2bWADg+OQRjt9gl6ICdmB/ZFDCGAtSow==}
- engines: {node: '>= 4'}
- dev: false
+ require-directory@2.1.1: {}
- /reusify@1.0.4:
- resolution: {integrity: sha512-U9nH88a3fc/ekCF1l0/UP1IosiuIjyTh7hBvXVMHYgVcfGvt897Xguj2UOLDeI5BG2m7/uwyaLVT6fbtCwTyzw==}
- engines: {iojs: '>=1.0.0', node: '>=0.10.0'}
- dev: false
+ require-main-filename@2.0.0: {}
- /rimraf@3.0.2:
- resolution: {integrity: sha512-JZkJMZkAGFFPP2YqXZXPbMlMBgsxzE8ILs4lMIX/2o0L9UBw9O/Y3o6wFw/i9YLapcUJWwqbi3kdxIPdC62TIA==}
- hasBin: true
+ restore-cursor@5.1.0:
dependencies:
- glob: 7.2.3
- dev: false
+ onetime: 7.0.0
+ signal-exit: 4.1.0
- /rollup@4.13.2:
- resolution: {integrity: sha512-MIlLgsdMprDBXC+4hsPgzWUasLO9CE4zOkj/u6j+Z6j5A4zRY+CtiXAdJyPtgCsc42g658Aeh1DlrdVEJhsL2g==}
- engines: {node: '>=18.0.0', npm: '>=8.0.0'}
- hasBin: true
+ reusify@1.1.0: {}
+
+ rfdc@1.4.1: {}
+
+ robust-predicates@3.0.2: {}
+
+ rollup@4.55.1:
dependencies:
- '@types/estree': 1.0.5
+ '@types/estree': 1.0.8
optionalDependencies:
- '@rollup/rollup-android-arm-eabi': 4.13.2
- '@rollup/rollup-android-arm64': 4.13.2
- '@rollup/rollup-darwin-arm64': 4.13.2
- '@rollup/rollup-darwin-x64': 4.13.2
- '@rollup/rollup-linux-arm-gnueabihf': 4.13.2
- '@rollup/rollup-linux-arm64-gnu': 4.13.2
- '@rollup/rollup-linux-arm64-musl': 4.13.2
- '@rollup/rollup-linux-powerpc64le-gnu': 4.13.2
- '@rollup/rollup-linux-riscv64-gnu': 4.13.2
- '@rollup/rollup-linux-s390x-gnu': 4.13.2
- '@rollup/rollup-linux-x64-gnu': 4.13.2
- '@rollup/rollup-linux-x64-musl': 4.13.2
- '@rollup/rollup-win32-arm64-msvc': 4.13.2
- '@rollup/rollup-win32-ia32-msvc': 4.13.2
- '@rollup/rollup-win32-x64-msvc': 4.13.2
+ '@rollup/rollup-android-arm-eabi': 4.55.1
+ '@rollup/rollup-android-arm64': 4.55.1
+ '@rollup/rollup-darwin-arm64': 4.55.1
+ '@rollup/rollup-darwin-x64': 4.55.1
+ '@rollup/rollup-freebsd-arm64': 4.55.1
+ '@rollup/rollup-freebsd-x64': 4.55.1
+ '@rollup/rollup-linux-arm-gnueabihf': 4.55.1
+ '@rollup/rollup-linux-arm-musleabihf': 4.55.1
+ '@rollup/rollup-linux-arm64-gnu': 4.55.1
+ '@rollup/rollup-linux-arm64-musl': 4.55.1
+ '@rollup/rollup-linux-loong64-gnu': 4.55.1
+ '@rollup/rollup-linux-loong64-musl': 4.55.1
+ '@rollup/rollup-linux-ppc64-gnu': 4.55.1
+ '@rollup/rollup-linux-ppc64-musl': 4.55.1
+ '@rollup/rollup-linux-riscv64-gnu': 4.55.1
+ '@rollup/rollup-linux-riscv64-musl': 4.55.1
+ '@rollup/rollup-linux-s390x-gnu': 4.55.1
+ '@rollup/rollup-linux-x64-gnu': 4.55.1
+ '@rollup/rollup-linux-x64-musl': 4.55.1
+ '@rollup/rollup-openbsd-x64': 4.55.1
+ '@rollup/rollup-openharmony-arm64': 4.55.1
+ '@rollup/rollup-win32-arm64-msvc': 4.55.1
+ '@rollup/rollup-win32-ia32-msvc': 4.55.1
+ '@rollup/rollup-win32-x64-gnu': 4.55.1
+ '@rollup/rollup-win32-x64-msvc': 4.55.1
fsevents: 2.3.3
- dev: false
- /run-parallel@1.2.0:
- resolution: {integrity: sha512-5l4VyZR86LZ/lDxZTR6jqL8AFE2S0IFLMP26AbjsLVADxHdhB/c0GUsH+y39UfCi3dzz8OlQuPmnaJOMoDHQBA==}
+ roughjs@4.6.6:
+ dependencies:
+ hachure-fill: 0.5.2
+ path-data-parser: 0.1.0
+ points-on-curve: 0.2.0
+ points-on-path: 0.2.1
+
+ run-parallel@1.2.0:
+ dependencies:
+ queue-microtask: 1.2.3
+
+ rw@1.3.3: {}
+
+ rxjs@7.8.2:
+ dependencies:
+ tslib: 2.8.1
+
+ safer-buffer@2.1.2: {}
+
+ sass-embedded-all-unknown@1.97.2:
+ dependencies:
+ sass: 1.97.2
+ optional: true
+
+ sass-embedded-android-arm64@1.97.2:
+ optional: true
+
+ sass-embedded-android-arm@1.97.2:
+ optional: true
+
+ sass-embedded-android-riscv64@1.97.2:
+ optional: true
+
+ sass-embedded-android-x64@1.97.2:
+ optional: true
+
+ sass-embedded-darwin-arm64@1.97.2:
+ optional: true
+
+ sass-embedded-darwin-x64@1.97.2:
+ optional: true
+
+ sass-embedded-linux-arm64@1.97.2:
+ optional: true
+
+ sass-embedded-linux-arm@1.97.2:
+ optional: true
+
+ sass-embedded-linux-musl-arm64@1.97.2:
+ optional: true
+
+ sass-embedded-linux-musl-arm@1.97.2:
+ optional: true
+
+ sass-embedded-linux-musl-riscv64@1.97.2:
+ optional: true
+
+ sass-embedded-linux-musl-x64@1.97.2:
+ optional: true
+
+ sass-embedded-linux-riscv64@1.97.2:
+ optional: true
+
+ sass-embedded-linux-x64@1.97.2:
+ optional: true
+
+ sass-embedded-unknown-all@1.97.2:
dependencies:
- queue-microtask: 1.2.3
- dev: false
+ sass: 1.97.2
+ optional: true
- /safe-buffer@5.2.1:
- resolution: {integrity: sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ==}
- dev: false
+ sass-embedded-win32-arm64@1.97.2:
+ optional: true
- /safer-buffer@2.1.2:
- resolution: {integrity: sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==}
- requiresBuild: true
- dev: false
+ sass-embedded-win32-x64@1.97.2:
optional: true
- /sass@1.72.0:
- resolution: {integrity: sha512-Gpczt3WA56Ly0Mn8Sl21Vj94s1axi9hDIzDFn9Ph9x3C3p4nNyvsqJoQyVXKou6cBlfFWEgRW4rT8Tb4i3XnVA==}
- engines: {node: '>=14.0.0'}
- hasBin: true
+ sass-embedded@1.97.2:
dependencies:
- chokidar: 3.6.0
- immutable: 4.3.5
- source-map-js: 1.2.0
- dev: false
+ '@bufbuild/protobuf': 2.10.2
+ buffer-builder: 0.2.0
+ colorjs.io: 0.5.2
+ immutable: 5.1.4
+ rxjs: 7.8.2
+ supports-color: 8.1.1
+ sync-child-process: 1.0.2
+ varint: 6.0.0
+ optionalDependencies:
+ sass-embedded-all-unknown: 1.97.2
+ sass-embedded-android-arm: 1.97.2
+ sass-embedded-android-arm64: 1.97.2
+ sass-embedded-android-riscv64: 1.97.2
+ sass-embedded-android-x64: 1.97.2
+ sass-embedded-darwin-arm64: 1.97.2
+ sass-embedded-darwin-x64: 1.97.2
+ sass-embedded-linux-arm: 1.97.2
+ sass-embedded-linux-arm64: 1.97.2
+ sass-embedded-linux-musl-arm: 1.97.2
+ sass-embedded-linux-musl-arm64: 1.97.2
+ sass-embedded-linux-musl-riscv64: 1.97.2
+ sass-embedded-linux-musl-x64: 1.97.2
+ sass-embedded-linux-riscv64: 1.97.2
+ sass-embedded-linux-x64: 1.97.2
+ sass-embedded-unknown-all: 1.97.2
+ sass-embedded-win32-arm64: 1.97.2
+ sass-embedded-win32-x64: 1.97.2
+
+ sass@1.97.2:
+ dependencies:
+ chokidar: 4.0.3
+ immutable: 5.1.4
+ source-map-js: 1.2.1
+ optionalDependencies:
+ '@parcel/watcher': 2.5.4
+ optional: true
- /sax@1.3.0:
- resolution: {integrity: sha512-0s+oAmw9zLl1V1cS9BtZN7JAd0cW5e0QH4W3LWEK6a4LaLEA2OTpGYWDY+6XasBLtz6wkm3u1xRw95mRuJ59WA==}
- dev: false
+ sax@1.4.4: {}
- /section-matter@1.0.0:
- resolution: {integrity: sha512-vfD3pmTzGpufjScBh50YHKzEu2lxBWhVEHsNGoEXmCmn2hKGfeNLYMzCJpe8cD7gqX7TJluOVpBkAequ6dgMmA==}
- engines: {node: '>=4'}
+ section-matter@1.0.0:
dependencies:
extend-shallow: 2.0.1
kind-of: 6.0.3
- dev: false
- /semver@6.3.1:
- resolution: {integrity: sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==}
- hasBin: true
- dev: false
+ set-blocking@2.0.0: {}
- /semver@7.6.0:
- resolution: {integrity: sha512-EnwXhrlwXMk9gKu5/flx5sv/an57AkRplG3hTK68W7FRDN+k+OWBj65M7719OkA82XLBxrcX0KSHj+X5COhOVg==}
- engines: {node: '>=10'}
- hasBin: true
+ shiki@3.21.0:
dependencies:
- lru-cache: 6.0.0
- dev: false
+ '@shikijs/core': 3.21.0
+ '@shikijs/engine-javascript': 3.21.0
+ '@shikijs/engine-oniguruma': 3.21.0
+ '@shikijs/langs': 3.21.0
+ '@shikijs/themes': 3.21.0
+ '@shikijs/types': 3.21.0
+ '@shikijs/vscode-textmate': 10.0.2
+ '@types/hast': 3.0.4
- /set-blocking@2.0.0:
- resolution: {integrity: sha512-KiKBS8AnWGEyLzofFfmvKwpdPzqiy16LvQfK3yv/fVH7Bj13/wl3JSR1J+rfgRE9q7xUJK4qvgS8raSOeLUehw==}
- dev: false
+ signal-exit@4.1.0: {}
- /shebang-command@2.0.0:
- resolution: {integrity: sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA==}
- engines: {node: '>=8'}
+ sitemap@9.0.0:
dependencies:
- shebang-regex: 3.0.0
- dev: false
+ '@types/node': 24.10.9
+ '@types/sax': 1.2.7
+ arg: 5.0.2
+ sax: 1.4.4
- /shebang-regex@3.0.0:
- resolution: {integrity: sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A==}
- engines: {node: '>=8'}
- dev: false
+ slash@5.1.0: {}
- /signal-exit@3.0.7:
- resolution: {integrity: sha512-wnD2ZE+l+SPC/uoS0vXeE9L1+0wuaMqKlfz9AMUo38JsyLSBWSFcHR1Rri62LZc12vLr1gb3jl7iwQhgwpAbGQ==}
- dev: false
+ source-map-js@1.2.1: {}
- /signal-exit@4.1.0:
- resolution: {integrity: sha512-bzyZ1e88w9O1iNJbKnOlvYTrWPDl46O1bG0D3XInv+9tkPrxrN8jUUTiFlDkkmKWgn1M6CfIA13SuGqOa9Korw==}
- engines: {node: '>=14'}
- dev: false
+ space-separated-tokens@2.0.2: {}
- /sitemap@7.1.1:
- resolution: {integrity: sha512-mK3aFtjz4VdJN0igpIJrinf3EO8U8mxOPsTBzSsy06UtjZQJ3YY3o3Xa7zSc5nMqcMrRwlChHZ18Kxg0caiPBg==}
- engines: {node: '>=12.0.0', npm: '>=5.6.0'}
- hasBin: true
+ speakingurl@14.0.1: {}
+
+ speech-rule-engine@4.1.2:
dependencies:
- '@types/node': 17.0.45
- '@types/sax': 1.2.7
- arg: 5.0.2
- sax: 1.3.0
- dev: false
+ '@xmldom/xmldom': 0.9.8
+ commander: 13.1.0
+ wicked-good-xpath: 1.3.0
- /slash@5.1.0:
- resolution: {integrity: sha512-ZA6oR3T/pEyuqwMgAKT0/hAv8oAXckzbkmR0UkUosQ+Mc4RxGoJkRmwHgHufaenlyAgE1Mxgpdcrf75y6XcnDg==}
- engines: {node: '>=14.16'}
- dev: false
+ sprintf-js@1.0.3: {}
- /smart-buffer@4.2.0:
- resolution: {integrity: sha512-94hK0Hh8rPqQl2xXc3HsaBoOXKV20MToPkcXvwbISWLEs+64sBq5kFgn2kJDHb1Pry9yrP0dxrCI9RRci7RXKg==}
- engines: {node: '>= 6.0.0', npm: '>= 3.0.0'}
- dev: false
+ stdin-discarder@0.2.2: {}
- /socks-proxy-agent@8.0.3:
- resolution: {integrity: sha512-VNegTZKhuGq5vSD6XNKlbqWhyt/40CgoEw8XxD6dhnm8Jq9IEa3nIa4HwnM8XOqU0CdB0BwWVXusqiFXfHB3+A==}
- engines: {node: '>= 14'}
+ string-width@4.2.3:
dependencies:
- agent-base: 7.1.1
- debug: 4.3.4
- socks: 2.8.1
- transitivePeerDependencies:
- - supports-color
- dev: false
+ emoji-regex: 8.0.0
+ is-fullwidth-code-point: 3.0.0
+ strip-ansi: 6.0.1
- /socks@2.8.1:
- resolution: {integrity: sha512-B6w7tkwNid7ToxjZ08rQMT8M9BJAf8DKx8Ft4NivzH0zBUfd6jldGcisJn/RLgxcX3FPNDdNQCUEMMT79b+oCQ==}
- engines: {node: '>= 10.0.0', npm: '>= 3.0.0'}
+ string-width@8.1.0:
dependencies:
- ip-address: 9.0.5
- smart-buffer: 4.2.0
- dev: false
-
- /source-map-js@1.2.0:
- resolution: {integrity: sha512-itJW8lvSA0TXEphiRoawsCksnlf8SyvmFzIhltqAHluXd88pkCd+cXJVHTDwdCr0IzwptSm035IHQktUu1QUMg==}
- engines: {node: '>=0.10.0'}
- dev: false
+ get-east-asian-width: 1.4.0
+ strip-ansi: 7.1.2
- /speech-rule-engine@4.0.7:
- resolution: {integrity: sha512-sJrL3/wHzNwJRLBdf6CjJWIlxC04iYKkyXvYSVsWVOiC2DSkHmxsqOhEeMsBA9XK+CHuNcsdkbFDnoUfAsmp9g==}
- hasBin: true
+ stringify-entities@4.0.4:
dependencies:
- commander: 9.2.0
- wicked-good-xpath: 1.3.0
- xmldom-sre: 0.1.31
- dev: false
-
- /sprintf-js@1.0.3:
- resolution: {integrity: sha512-D9cPgkvLlV3t3IzL0D0YLvGA9Ahk4PcvVwUbN0dSGr1aP0Nrt4AEnTUbuGvquEC0mA64Gqt1fzirlRs5ibXx8g==}
- dev: false
+ character-entities-html4: 2.1.0
+ character-entities-legacy: 3.0.0
- /sprintf-js@1.1.3:
- resolution: {integrity: sha512-Oo+0REFV59/rz3gfJNKQiBlwfHaSESl1pcGyABQsnnIfWOFt6JNj5gCog2U6MLZ//IGYD+nA8nI+mTShREReaA==}
- dev: false
+ strip-ansi@6.0.1:
+ dependencies:
+ ansi-regex: 5.0.1
- /ssri@10.0.5:
- resolution: {integrity: sha512-bSf16tAFkGeRlUNDjXu8FzaMQt6g2HZJrun7mtMbIPOddxt3GLMSz5VWUWcqTJUPfLEaDIepGxv+bYQW49596A==}
- engines: {node: ^14.17.0 || ^16.13.0 || >=18.0.0}
+ strip-ansi@7.1.2:
dependencies:
- minipass: 7.0.4
- dev: false
+ ansi-regex: 6.2.2
- /stdin-discarder@0.2.2:
- resolution: {integrity: sha512-UhDfHmA92YAlNnCfhmq0VeNL5bDbiZGg7sZ2IvPsXubGkiNa9EC+tUTsjBRsYUAz87btI6/1wf4XoVvQ3uRnmQ==}
- engines: {node: '>=18'}
- dev: false
+ strip-bom-string@1.0.0: {}
- /string-width@4.2.3:
- resolution: {integrity: sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==}
- engines: {node: '>=8'}
+ stylis@4.3.6: {}
+
+ superjson@2.2.6:
dependencies:
- emoji-regex: 8.0.0
- is-fullwidth-code-point: 3.0.0
- strip-ansi: 6.0.1
- dev: false
+ copy-anything: 4.0.5
- /string-width@5.1.2:
- resolution: {integrity: sha512-HnLOCR3vjcY8beoNLtcjZ5/nxn2afmME6lhrDrebokqMap+XbeW8n9TXpPDOqdGK5qcI3oT0GKTW6wC7EMiVqA==}
- engines: {node: '>=12'}
+ supports-color@8.1.1:
dependencies:
- eastasianwidth: 0.2.0
- emoji-regex: 9.2.2
- strip-ansi: 7.1.0
- dev: false
+ has-flag: 4.0.0
- /string-width@7.1.0:
- resolution: {integrity: sha512-SEIJCWiX7Kg4c129n48aDRwLbFb2LJmXXFrWBG4NGaRtMQ3myKPKbwrD1BKqQn74oCoNMBVrfDEr5M9YxCsrkw==}
- engines: {node: '>=18'}
+ sync-child-process@1.0.2:
dependencies:
- emoji-regex: 10.3.0
- get-east-asian-width: 1.2.0
- strip-ansi: 7.1.0
- dev: false
+ sync-message-port: 1.1.3
- /string_decoder@1.3.0:
- resolution: {integrity: sha512-hkRX8U1WjJFd8LsDJ2yQ/wWWxaopEsABU1XfkM8A+j0+85JAGppt16cr1Whg6KIbb4okU6Mql6BOj+uup/wKeA==}
+ sync-message-port@1.1.3: {}
+
+ synckit@0.11.12:
dependencies:
- safe-buffer: 5.2.1
- dev: false
+ '@pkgr/core': 0.2.9
- /strip-ansi@6.0.1:
- resolution: {integrity: sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==}
- engines: {node: '>=8'}
+ tinyexec@1.0.2: {}
+
+ tinyglobby@0.2.15:
dependencies:
- ansi-regex: 5.0.1
- dev: false
+ fdir: 6.5.0(picomatch@4.0.3)
+ picomatch: 4.0.3
- /strip-ansi@7.1.0:
- resolution: {integrity: sha512-iq6eVVI64nQQTRYq2KtEg2d2uU7LElhTJwsH4YzIHZshxlgZms/wIc4VoDQTlG/IvVIrBKG06CrZnp0qv7hkcQ==}
- engines: {node: '>=12'}
+ to-regex-range@5.0.1:
dependencies:
- ansi-regex: 6.0.1
- dev: false
+ is-number: 7.0.0
- /strip-bom-string@1.0.0:
- resolution: {integrity: sha512-uCC2VHvQRYu+lMh4My/sFNmF2klFymLX1wHJeXnbEJERpV/ZsVuonzerjfrGpIGF7LBVa1O7i9kjiWvJiFck8g==}
- engines: {node: '>=0.10.0'}
- dev: false
+ trim-lines@3.0.1: {}
- /strip-final-newline@3.0.0:
- resolution: {integrity: sha512-dOESqjYr96iWYylGObzd39EuNTa5VJxyvVAEm5Jnh7KGo75V43Hk1odPQkNDyXNmUR6k+gEiDVXnjB8HJ3crXw==}
- engines: {node: '>=12'}
- dev: false
+ trough@2.2.0: {}
- /tar@6.2.1:
- resolution: {integrity: sha512-DZ4yORTwrbTj/7MZYq2w+/ZFdI6OZ/f9SFHR+71gIVUZhOQPHzVCLpvRnPgyaMpfWxxk/4ONva3GQSyNIKRv6A==}
- engines: {node: '>=10'}
- dependencies:
- chownr: 2.0.0
- fs-minipass: 2.1.0
- minipass: 5.0.0
- minizlib: 2.1.2
- mkdirp: 1.0.4
- yallist: 4.0.0
- dev: false
+ ts-dedent@2.2.0: {}
- /to-fast-properties@2.0.0:
- resolution: {integrity: sha512-/OaKK0xYrs3DmxRYqL/yDc+FxFUVYhDlXMhRmv3z915w2HF1tnN1omB354j8VUGO/hbRzyD6Y3sA7v7GS/ceog==}
- engines: {node: '>=4'}
- dev: false
+ tslib@2.8.1: {}
- /to-regex-range@5.0.1:
- resolution: {integrity: sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ==}
- engines: {node: '>=8.0'}
+ uc.micro@2.1.0: {}
+
+ ufo@1.6.3: {}
+
+ undici-types@7.16.0: {}
+
+ undici@7.18.2: {}
+
+ unicorn-magic@0.1.0: {}
+
+ unified@11.0.5:
dependencies:
- is-number: 7.0.0
- dev: false
+ '@types/unist': 3.0.3
+ bail: 2.0.2
+ devlop: 1.1.0
+ extend: 3.0.2
+ is-plain-obj: 4.1.0
+ trough: 2.2.0
+ vfile: 6.0.3
- /tr46@0.0.3:
- resolution: {integrity: sha512-N3WMsuqV66lT30CrXNbEjx4GEwlow3v6rr4mCcv6prnfwhS01rkgyFdjPNBYd9br7LpXV1+Emh01fHnq2Gdgrw==}
- dev: false
+ unist-util-is@6.0.1:
+ dependencies:
+ '@types/unist': 3.0.3
- /uc.micro@2.1.0:
- resolution: {integrity: sha512-ARDJmphmdvUk6Glw7y9DQ2bFkKBHwQHLi2lsaH6PPmz/Ka9sFOBsBluozhDltWmnv9u/cF6Rt87znRTPV+yp/A==}
- dev: false
+ unist-util-position@5.0.0:
+ dependencies:
+ '@types/unist': 3.0.3
- /undici-types@5.26.5:
- resolution: {integrity: sha512-JlCMO+ehdEIKqlFxk6IfVoAUVmgz7cU7zD/h9XZ0qzeosSHmUJVOzSQvvYSYWXkFXC+IfLKSIffhv0sVZup6pA==}
- dev: false
+ unist-util-stringify-position@4.0.0:
+ dependencies:
+ '@types/unist': 3.0.3
- /unicorn-magic@0.1.0:
- resolution: {integrity: sha512-lRfVq8fE8gz6QMBuDM6a+LO3IAzTi05H6gCVaUpir2E1Rwpo4ZUog45KpNXKC/Mn3Yb9UDuHumeFTo9iV/D9FQ==}
- engines: {node: '>=18'}
- dev: false
+ unist-util-visit-parents@6.0.2:
+ dependencies:
+ '@types/unist': 3.0.3
+ unist-util-is: 6.0.1
- /unique-filename@3.0.0:
- resolution: {integrity: sha512-afXhuC55wkAmZ0P18QsVE6kp8JaxrEokN2HGIoIVv2ijHQd419H0+6EigAFcIzXeMIkcIkNBpB3L/DXB3cTS/g==}
- engines: {node: ^14.17.0 || ^16.13.0 || >=18.0.0}
+ unist-util-visit@5.0.0:
dependencies:
- unique-slug: 4.0.0
- dev: false
+ '@types/unist': 3.0.3
+ unist-util-is: 6.0.1
+ unist-util-visit-parents: 6.0.2
- /unique-slug@4.0.0:
- resolution: {integrity: sha512-WrcA6AyEfqDX5bWige/4NQfPZMtASNVxdmWR76WESYQVAACSgWcR6e9i0mofqqBxYFtL4oAxPIptY73/0YE1DQ==}
- engines: {node: ^14.17.0 || ^16.13.0 || >=18.0.0}
+ universalify@2.0.1: {}
+
+ upath@2.0.1: {}
+
+ update-browserslist-db@1.2.3(browserslist@4.28.1):
dependencies:
- imurmurhash: 0.1.4
- dev: false
+ browserslist: 4.28.1
+ escalade: 3.2.0
+ picocolors: 1.1.1
- /universalify@2.0.1:
- resolution: {integrity: sha512-gptHNQghINnc/vTGIk0SOFGFNXw7JVrlRUtConJRlvaw6DuX0wO5Jeko9sWrMBhh+PsYAZ7oXAiOnf/UKogyiw==}
- engines: {node: '>= 10.0.0'}
- dev: false
+ uuid@11.1.0: {}
- /upath@2.0.1:
- resolution: {integrity: sha512-1uEe95xksV1O0CYKXo8vQvN1JEbtJp7lb7C5U9HMsIp6IVwntkH/oNUzyVNQSd4S1sYk2FpSSW44FqMc8qee5w==}
- engines: {node: '>=4'}
- dev: false
+ varint@6.0.0: {}
- /update-browserslist-db@1.0.13(browserslist@4.23.0):
- resolution: {integrity: sha512-xebP81SNcPuNpPP3uzeW1NYXxI3rxyJzF3pD6sH4jE7o/IX+WtSpwnVU+qIsDPyk0d3hmFQ7mjqc6AtV604hbg==}
- hasBin: true
- peerDependencies:
- browserslist: '>= 4.21.0'
+ vfile-location@5.0.3:
dependencies:
- browserslist: 4.23.0
- escalade: 3.1.2
- picocolors: 1.0.0
- dev: false
+ '@types/unist': 3.0.3
+ vfile: 6.0.3
- /util-deprecate@1.0.2:
- resolution: {integrity: sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw==}
- dev: false
+ vfile-message@4.0.3:
+ dependencies:
+ '@types/unist': 3.0.3
+ unist-util-stringify-position: 4.0.0
- /vite@5.2.7:
- resolution: {integrity: sha512-k14PWOKLI6pMaSzAuGtT+Cf0YmIx12z9YGon39onaJNy8DLBfBJrzg9FQEmkAM5lpHBZs9wksWAsyF/HkpEwJA==}
- engines: {node: ^18.0.0 || >=20.0.0}
- hasBin: true
- peerDependencies:
- '@types/node': ^18.0.0 || >=20.0.0
- less: '*'
- lightningcss: ^1.21.0
- sass: '*'
- stylus: '*'
- sugarss: '*'
- terser: ^5.4.0
- peerDependenciesMeta:
- '@types/node':
- optional: true
- less:
- optional: true
- lightningcss:
- optional: true
- sass:
- optional: true
- stylus:
- optional: true
- sugarss:
- optional: true
- terser:
- optional: true
+ vfile@6.0.3:
dependencies:
- esbuild: 0.20.2
- postcss: 8.4.38
- rollup: 4.13.2
+ '@types/unist': 3.0.3
+ vfile-message: 4.0.3
+
+ vite@7.3.1(@types/node@25.0.9)(sass-embedded@1.97.2)(sass@1.97.2):
+ dependencies:
+ esbuild: 0.27.2
+ fdir: 6.5.0(picomatch@4.0.3)
+ picomatch: 4.0.3
+ postcss: 8.5.6
+ rollup: 4.55.1
+ tinyglobby: 0.2.15
optionalDependencies:
+ '@types/node': 25.0.9
fsevents: 2.3.3
- dev: false
+ sass: 1.97.2
+ sass-embedded: 1.97.2
- /vue-demi@0.14.7(vue@3.4.21):
- resolution: {integrity: sha512-EOG8KXDQNwkJILkx/gPcoL/7vH+hORoBaKgGe+6W7VFMvCYJfmF2dGbvgDroVnI8LU7/kTu8mbjRZGBU1z9NTA==}
- engines: {node: '>=12'}
- hasBin: true
- requiresBuild: true
- peerDependencies:
- '@vue/composition-api': ^1.0.0-rc.1
- vue: ^3.0.0-0 || ^2.6.0
- peerDependenciesMeta:
- '@vue/composition-api':
- optional: true
+ vscode-jsonrpc@8.2.0: {}
+
+ vscode-languageserver-protocol@3.17.5:
dependencies:
- vue: 3.4.21
- dev: false
+ vscode-jsonrpc: 8.2.0
+ vscode-languageserver-types: 3.17.5
- /vue-router@4.3.0(vue@3.4.21):
- resolution: {integrity: sha512-dqUcs8tUeG+ssgWhcPbjHvazML16Oga5w34uCUmsk7i0BcnskoLGwjpa15fqMr2Fa5JgVBrdL2MEgqz6XZ/6IQ==}
- peerDependencies:
- vue: ^3.2.0
+ vscode-languageserver-textdocument@1.0.12: {}
+
+ vscode-languageserver-types@3.17.5: {}
+
+ vscode-languageserver@9.0.1:
dependencies:
- '@vue/devtools-api': 6.6.1
- vue: 3.4.21
- dev: false
+ vscode-languageserver-protocol: 3.17.5
- /vue@3.4.21:
- resolution: {integrity: sha512-5hjyV/jLEIKD/jYl4cavMcnzKwjMKohureP8ejn3hhEjwhWIhWeuzL2kJAjzl/WyVsgPY56Sy4Z40C3lVshxXA==}
- peerDependencies:
- typescript: '*'
- peerDependenciesMeta:
- typescript:
- optional: true
+ vscode-uri@3.0.8: {}
+
+ vue-router@4.6.4(vue@3.5.26):
dependencies:
- '@vue/compiler-dom': 3.4.21
- '@vue/compiler-sfc': 3.4.21
- '@vue/runtime-dom': 3.4.21
- '@vue/server-renderer': 3.4.21(vue@3.4.21)
- '@vue/shared': 3.4.21
- dev: false
+ '@vue/devtools-api': 6.6.4
+ vue: 3.5.26
- /vuepress-plugin-components@2.0.0-rc.32(vuepress@2.0.0-rc.9):
- resolution: {integrity: sha512-ndu3lXnmS3AzYwqXn+mTJNItXux0MiPj2BzE8uH5HL473uAtzE8nS3q0gvcAt8CNbOSElJt+I5Hz4AO8/hbtAA==}
- engines: {node: '>=18.16.0', npm: '>=8', pnpm: '>=7', yarn: '>=2'}
- peerDependencies:
- artplayer: ^5.0.0
- dashjs: 4.7.4
- hls.js: ^1.4.12
- mpegts.js: ^1.7.3
- plyr: ^3.7.8
- sass-loader: ^14.0.0
- vidstack: ^1.9.0
- vuepress: 2.0.0-rc.9
- peerDependenciesMeta:
- artplayer:
- optional: true
- dashjs:
- optional: true
- hls.js:
- optional: true
- mpegts.js:
- optional: true
- plyr:
- optional: true
- sass-loader:
- optional: true
- vidstack:
- optional: true
+ vue@3.5.26:
dependencies:
- '@stackblitz/sdk': 1.9.0
- '@vuepress/helper': 2.0.0-rc.21(vuepress@2.0.0-rc.9)
- '@vueuse/core': 10.9.0(vue@3.4.21)
+ '@vue/compiler-dom': 3.5.26
+ '@vue/compiler-sfc': 3.5.26
+ '@vue/runtime-dom': 3.5.26
+ '@vue/server-renderer': 3.5.26(vue@3.5.26)
+ '@vue/shared': 3.5.26
+
+ vuepress-plugin-components@2.0.0-rc.102(sass-embedded@1.97.2)(sass@1.97.2)(vuepress@2.0.0-rc.26(@vuepress/bundler-vite@2.0.0-rc.26(@types/node@25.0.9)(sass-embedded@1.97.2)(sass@1.97.2))(vue@3.5.26)):
+ dependencies:
+ '@stackblitz/sdk': 1.11.0
+ '@vuepress/helper': 2.0.0-rc.121(vuepress@2.0.0-rc.26(@vuepress/bundler-vite@2.0.0-rc.26(@types/node@25.0.9)(sass-embedded@1.97.2)(sass@1.97.2))(vue@3.5.26))
+ '@vuepress/plugin-sass-palette': 2.0.0-rc.121(sass-embedded@1.97.2)(sass@1.97.2)(vuepress@2.0.0-rc.26(@vuepress/bundler-vite@2.0.0-rc.26(@types/node@25.0.9)(sass-embedded@1.97.2)(sass@1.97.2))(vue@3.5.26))
+ '@vueuse/core': 14.1.0(vue@3.5.26)
balloon-css: 1.2.0
- create-codepen: 1.0.1
- qrcode: 1.5.3
- vue: 3.4.21
- vuepress: 2.0.0-rc.9(@vuepress/bundler-vite@2.0.0-rc.9)(vue@3.4.21)
- vuepress-plugin-sass-palette: 2.0.0-rc.32(vuepress@2.0.0-rc.9)
- vuepress-shared: 2.0.0-rc.32(vuepress@2.0.0-rc.9)
+ create-codepen: 2.0.0
+ qrcode: 1.5.4
+ vue: 3.5.26
+ vuepress: 2.0.0-rc.26(@vuepress/bundler-vite@2.0.0-rc.26(@types/node@25.0.9)(sass-embedded@1.97.2)(sass@1.97.2))(vue@3.5.26)
+ vuepress-shared: 2.0.0-rc.99(vuepress@2.0.0-rc.26(@vuepress/bundler-vite@2.0.0-rc.26(@types/node@25.0.9)(sass-embedded@1.97.2)(sass@1.97.2))(vue@3.5.26))
+ optionalDependencies:
+ sass: 1.97.2
+ sass-embedded: 1.97.2
transitivePeerDependencies:
- - '@vue/composition-api'
- typescript
- dev: false
- /vuepress-plugin-md-enhance@2.0.0-rc.32(markdown-it@14.1.0)(mathjax-full@3.2.2)(vuepress@2.0.0-rc.9):
- resolution: {integrity: sha512-zZK8aEfbq26J5w8o9xGWXCHHrL3PYk25tloTPcx96nZWYPeD+5fMFAtVpHte0rXBWUf0MBtDQxddSeATteBE7Q==}
- engines: {node: '>=18.16.0', npm: '>=8', pnpm: '>=7', yarn: '>=2'}
- peerDependencies:
- '@types/reveal.js': ^5.0.0
- '@vue/repl': ^4.1.1
- chart.js: ^4.0.0
- echarts: ^5.0.0
- flowchart.ts: ^2.0.0 || ^3.0.0
- katex: ^0.16.0
- kotlin-playground: ^1.23.0
- markmap-lib: ^0.15.5 || ^0.16.0
- markmap-toolbar: ^0.15.5 || ^0.16.0
- markmap-view: ^0.15.5 || ^0.16.0
- mathjax-full: ^3.2.2
- mermaid: ^10.8.0
- reveal.js: ^5.0.0
- sandpack-vue3: ^3.0.0
- sass-loader: ^14.0.0
- vuepress: 2.0.0-rc.9
- peerDependenciesMeta:
- '@types/reveal.js':
- optional: true
- '@vue/repl':
- optional: true
- chart.js:
- optional: true
- echarts:
- optional: true
- flowchart.ts:
- optional: true
- katex:
- optional: true
- kotlin-playground:
- optional: true
- markmap-lib:
- optional: true
- markmap-toolbar:
- optional: true
- markmap-view:
- optional: true
- mathjax-full:
- optional: true
- mermaid:
- optional: true
- reveal.js:
- optional: true
- sandpack-vue3:
- optional: true
- sass-loader:
- optional: true
+ vuepress-plugin-md-enhance@2.0.0-rc.102(markdown-it@14.1.0)(sass-embedded@1.97.2)(sass@1.97.2)(vuepress@2.0.0-rc.26(@vuepress/bundler-vite@2.0.0-rc.26(@types/node@25.0.9)(sass-embedded@1.97.2)(sass@1.97.2))(vue@3.5.26)):
dependencies:
- '@mdit/plugin-alert': 0.8.0(markdown-it@14.1.0)
- '@mdit/plugin-align': 0.8.0(markdown-it@14.1.0)
- '@mdit/plugin-attrs': 0.8.0(markdown-it@14.1.0)
- '@mdit/plugin-container': 0.8.0(markdown-it@14.1.0)
- '@mdit/plugin-demo': 0.8.0(markdown-it@14.1.0)
- '@mdit/plugin-figure': 0.8.0(markdown-it@14.1.0)
- '@mdit/plugin-footnote': 0.8.0(markdown-it@14.1.0)
- '@mdit/plugin-img-lazyload': 0.8.0(markdown-it@14.1.0)
- '@mdit/plugin-img-mark': 0.8.0(markdown-it@14.1.0)
- '@mdit/plugin-img-size': 0.8.0(markdown-it@14.1.0)
- '@mdit/plugin-include': 0.8.0(markdown-it@14.1.0)
- '@mdit/plugin-katex': 0.8.0(markdown-it@14.1.0)
- '@mdit/plugin-mark': 0.8.0(markdown-it@14.1.0)
- '@mdit/plugin-mathjax': 0.8.0(markdown-it@14.1.0)(mathjax-full@3.2.2)
- '@mdit/plugin-stylize': 0.8.0(markdown-it@14.1.0)
- '@mdit/plugin-sub': 0.8.0(markdown-it@14.1.0)
- '@mdit/plugin-sup': 0.8.0(markdown-it@14.1.0)
- '@mdit/plugin-tab': 0.8.0(markdown-it@14.1.0)
- '@mdit/plugin-tasklist': 0.8.0(markdown-it@14.1.0)
- '@mdit/plugin-tex': 0.8.0(markdown-it@14.1.0)
- '@mdit/plugin-uml': 0.8.0(markdown-it@14.1.0)
- '@types/markdown-it': 13.0.7
- '@vuepress/helper': 2.0.0-rc.21(vuepress@2.0.0-rc.9)
- '@vueuse/core': 10.9.0(vue@3.4.21)
+ '@mdit/plugin-container': 0.22.2(markdown-it@14.1.0)
+ '@mdit/plugin-demo': 0.22.3(markdown-it@14.1.0)
+ '@types/markdown-it': 14.1.2
+ '@vuepress/helper': 2.0.0-rc.121(vuepress@2.0.0-rc.26(@vuepress/bundler-vite@2.0.0-rc.26(@types/node@25.0.9)(sass-embedded@1.97.2)(sass@1.97.2))(vue@3.5.26))
+ '@vuepress/plugin-sass-palette': 2.0.0-rc.121(sass-embedded@1.97.2)(sass@1.97.2)(vuepress@2.0.0-rc.26(@vuepress/bundler-vite@2.0.0-rc.26(@types/node@25.0.9)(sass-embedded@1.97.2)(sass@1.97.2))(vue@3.5.26))
+ '@vueuse/core': 14.1.0(vue@3.5.26)
balloon-css: 1.2.0
- js-yaml: 4.1.0
- mathjax-full: 3.2.2
- vue: 3.4.21
- vuepress: 2.0.0-rc.9(@vuepress/bundler-vite@2.0.0-rc.9)(vue@3.4.21)
- vuepress-plugin-sass-palette: 2.0.0-rc.32(vuepress@2.0.0-rc.9)
- vuepress-shared: 2.0.0-rc.32(vuepress@2.0.0-rc.9)
+ js-yaml: 4.1.1
+ vue: 3.5.26
+ vuepress: 2.0.0-rc.26(@vuepress/bundler-vite@2.0.0-rc.26(@types/node@25.0.9)(sass-embedded@1.97.2)(sass@1.97.2))(vue@3.5.26)
+ vuepress-shared: 2.0.0-rc.99(vuepress@2.0.0-rc.26(@vuepress/bundler-vite@2.0.0-rc.26(@types/node@25.0.9)(sass-embedded@1.97.2)(sass@1.97.2))(vue@3.5.26))
+ optionalDependencies:
+ sass: 1.97.2
+ sass-embedded: 1.97.2
transitivePeerDependencies:
- - '@vue/composition-api'
- markdown-it
- typescript
- dev: false
- /vuepress-plugin-sass-palette@2.0.0-rc.32(vuepress@2.0.0-rc.9):
- resolution: {integrity: sha512-3tHHQU1E5cCo/udiZ4t0PL6OrWMxuP67+/tj3U5R9Zls6zM1aNE+Dw8I6/Of+HRTzctsQFxGPH0lZa05gS49UQ==}
- engines: {node: '>=18.16.0', npm: '>=8', pnpm: '>=7', yarn: '>=2'}
- peerDependencies:
- sass-loader: ^14.0.0
- vuepress: 2.0.0-rc.9
- peerDependenciesMeta:
- sass-loader:
- optional: true
+ vuepress-shared@2.0.0-rc.99(vuepress@2.0.0-rc.26(@vuepress/bundler-vite@2.0.0-rc.26(@types/node@25.0.9)(sass-embedded@1.97.2)(sass@1.97.2))(vue@3.5.26)):
dependencies:
- '@vuepress/helper': 2.0.0-rc.21(vuepress@2.0.0-rc.9)
- chokidar: 3.6.0
- sass: 1.72.0
- vuepress: 2.0.0-rc.9(@vuepress/bundler-vite@2.0.0-rc.9)(vue@3.4.21)
- vuepress-shared: 2.0.0-rc.32(vuepress@2.0.0-rc.9)
+ '@vuepress/helper': 2.0.0-rc.120(vuepress@2.0.0-rc.26(@vuepress/bundler-vite@2.0.0-rc.26(@types/node@25.0.9)(sass-embedded@1.97.2)(sass@1.97.2))(vue@3.5.26))
+ '@vueuse/core': 14.1.0(vue@3.5.26)
+ vue: 3.5.26
+ vuepress: 2.0.0-rc.26(@vuepress/bundler-vite@2.0.0-rc.26(@types/node@25.0.9)(sass-embedded@1.97.2)(sass@1.97.2))(vue@3.5.26)
transitivePeerDependencies:
- - '@vue/composition-api'
- typescript
- dev: false
- /vuepress-shared@2.0.0-rc.32(vuepress@2.0.0-rc.9):
- resolution: {integrity: sha512-wGPPUoP6rxy7QnulD0sNz3r7iEHwp+TJroNmI+AT6FuCo1WCD4ZWfnRnm3dVcAkobc3nuOvSmDTvM9pIDCaxsA==}
- engines: {node: '>=18.16.0', npm: '>=8', pnpm: '>=7', yarn: '>=2'}
- peerDependencies:
- vuepress: 2.0.0-rc.9
- dependencies:
- '@vuepress/helper': 2.0.0-rc.21(vuepress@2.0.0-rc.9)
- '@vueuse/core': 10.9.0(vue@3.4.21)
- cheerio: 1.0.0-rc.12
- dayjs: 1.11.10
- execa: 8.0.1
- fflate: 0.8.2
- gray-matter: 4.0.3
- semver: 7.6.0
- vue: 3.4.21
- vuepress: 2.0.0-rc.9(@vuepress/bundler-vite@2.0.0-rc.9)(vue@3.4.21)
- transitivePeerDependencies:
- - '@vue/composition-api'
- - typescript
- dev: false
-
- /vuepress-theme-hope@2.0.0-rc.32(@vuepress/plugin-feed@2.0.0-rc.21)(@vuepress/plugin-search@2.0.0-rc.21)(markdown-it@14.1.0)(mathjax-full@3.2.2)(nodejs-jieba@0.1.2)(vuepress@2.0.0-rc.9):
- resolution: {integrity: sha512-5S5qg5xa0TErqVjpubhBN5oy0QmROd+ja5EQKfORUCKdXKQvx0soojQZnCPkXsUEXonwiZ12oCLN2+UGO03eng==}
- engines: {node: '>=18.16.0', npm: '>=8', pnpm: '>=7', yarn: '>=2'}
- peerDependencies:
- '@vuepress/plugin-docsearch': 2.0.0-rc.21
- '@vuepress/plugin-feed': 2.0.0-rc.21
- '@vuepress/plugin-pwa': 2.0.0-rc.21
- '@vuepress/plugin-redirect': 2.0.0-rc.21
- '@vuepress/plugin-search': 2.0.0-rc.21
- nodejs-jieba: ^0.1.2
- sass-loader: ^14.0.0
- vuepress: 2.0.0-rc.9
- vuepress-plugin-search-pro: 2.0.0-rc.32
- peerDependenciesMeta:
- '@vuepress/plugin-docsearch':
- optional: true
- '@vuepress/plugin-feed':
- optional: true
- '@vuepress/plugin-pwa':
- optional: true
- '@vuepress/plugin-redirect':
- optional: true
- '@vuepress/plugin-search':
- optional: true
- nodejs-jieba:
- optional: true
- sass-loader:
- optional: true
- vuepress-plugin-search-pro:
- optional: true
- dependencies:
- '@vuepress/helper': 2.0.0-rc.21(vuepress@2.0.0-rc.9)
- '@vuepress/plugin-active-header-links': 2.0.0-rc.21(vuepress@2.0.0-rc.9)
- '@vuepress/plugin-back-to-top': 2.0.0-rc.21(vuepress@2.0.0-rc.9)
- '@vuepress/plugin-blog': 2.0.0-rc.21(vuepress@2.0.0-rc.9)
- '@vuepress/plugin-catalog': 2.0.0-rc.21(vuepress@2.0.0-rc.9)
- '@vuepress/plugin-comment': 2.0.0-rc.21(vuepress@2.0.0-rc.9)
- '@vuepress/plugin-copy-code': 2.0.0-rc.21(vuepress@2.0.0-rc.9)
- '@vuepress/plugin-copyright': 2.0.0-rc.21(vuepress@2.0.0-rc.9)
- '@vuepress/plugin-external-link-icon': 2.0.0-rc.21(vuepress@2.0.0-rc.9)
- '@vuepress/plugin-feed': 2.0.0-rc.21(vuepress@2.0.0-rc.9)
- '@vuepress/plugin-git': 2.0.0-rc.21(vuepress@2.0.0-rc.9)
- '@vuepress/plugin-links-check': 2.0.0-rc.21(vuepress@2.0.0-rc.9)
- '@vuepress/plugin-nprogress': 2.0.0-rc.21(vuepress@2.0.0-rc.9)
- '@vuepress/plugin-photo-swipe': 2.0.0-rc.21(vuepress@2.0.0-rc.9)
- '@vuepress/plugin-prismjs': 2.0.0-rc.21(vuepress@2.0.0-rc.9)
- '@vuepress/plugin-reading-time': 2.0.0-rc.21(vuepress@2.0.0-rc.9)
- '@vuepress/plugin-rtl': 2.0.0-rc.21(vuepress@2.0.0-rc.9)
- '@vuepress/plugin-search': 2.0.0-rc.21(vuepress@2.0.0-rc.9)
- '@vuepress/plugin-seo': 2.0.0-rc.21(vuepress@2.0.0-rc.9)
- '@vuepress/plugin-sitemap': 2.0.0-rc.21(vuepress@2.0.0-rc.9)
- '@vuepress/plugin-theme-data': 2.0.0-rc.21(vuepress@2.0.0-rc.9)
- '@vueuse/core': 10.9.0(vue@3.4.21)
+ vuepress-theme-hope@2.0.0-rc.102(@vuepress/plugin-feed@2.0.0-rc.121(vuepress@2.0.0-rc.26(@vuepress/bundler-vite@2.0.0-rc.26(@types/node@25.0.9)(sass-embedded@1.97.2)(sass@1.97.2))(vue@3.5.26)))(@vuepress/plugin-search@2.0.0-rc.121(vuepress@2.0.0-rc.26(@vuepress/bundler-vite@2.0.0-rc.26(@types/node@25.0.9)(sass-embedded@1.97.2)(sass@1.97.2))(vue@3.5.26)))(katex@0.16.27)(markdown-it@14.1.0)(mermaid@11.12.2)(sass-embedded@1.97.2)(sass@1.97.2)(vuepress@2.0.0-rc.26(@vuepress/bundler-vite@2.0.0-rc.26(@types/node@25.0.9)(sass-embedded@1.97.2)(sass@1.97.2))(vue@3.5.26)):
+ dependencies:
+ '@vuepress/helper': 2.0.0-rc.121(vuepress@2.0.0-rc.26(@vuepress/bundler-vite@2.0.0-rc.26(@types/node@25.0.9)(sass-embedded@1.97.2)(sass@1.97.2))(vue@3.5.26))
+ '@vuepress/plugin-active-header-links': 2.0.0-rc.118(vuepress@2.0.0-rc.26(@vuepress/bundler-vite@2.0.0-rc.26(@types/node@25.0.9)(sass-embedded@1.97.2)(sass@1.97.2))(vue@3.5.26))
+ '@vuepress/plugin-back-to-top': 2.0.0-rc.121(vuepress@2.0.0-rc.26(@vuepress/bundler-vite@2.0.0-rc.26(@types/node@25.0.9)(sass-embedded@1.97.2)(sass@1.97.2))(vue@3.5.26))
+ '@vuepress/plugin-blog': 2.0.0-rc.121(vuepress@2.0.0-rc.26(@vuepress/bundler-vite@2.0.0-rc.26(@types/node@25.0.9)(sass-embedded@1.97.2)(sass@1.97.2))(vue@3.5.26))
+ '@vuepress/plugin-catalog': 2.0.0-rc.121(vuepress@2.0.0-rc.26(@vuepress/bundler-vite@2.0.0-rc.26(@types/node@25.0.9)(sass-embedded@1.97.2)(sass@1.97.2))(vue@3.5.26))
+ '@vuepress/plugin-comment': 2.0.0-rc.121(vuepress@2.0.0-rc.26(@vuepress/bundler-vite@2.0.0-rc.26(@types/node@25.0.9)(sass-embedded@1.97.2)(sass@1.97.2))(vue@3.5.26))
+ '@vuepress/plugin-copy-code': 2.0.0-rc.121(vuepress@2.0.0-rc.26(@vuepress/bundler-vite@2.0.0-rc.26(@types/node@25.0.9)(sass-embedded@1.97.2)(sass@1.97.2))(vue@3.5.26))
+ '@vuepress/plugin-copyright': 2.0.0-rc.121(vuepress@2.0.0-rc.26(@vuepress/bundler-vite@2.0.0-rc.26(@types/node@25.0.9)(sass-embedded@1.97.2)(sass@1.97.2))(vue@3.5.26))
+ '@vuepress/plugin-git': 2.0.0-rc.121(vuepress@2.0.0-rc.26(@vuepress/bundler-vite@2.0.0-rc.26(@types/node@25.0.9)(sass-embedded@1.97.2)(sass@1.97.2))(vue@3.5.26))
+ '@vuepress/plugin-icon': 2.0.0-rc.121(markdown-it@14.1.0)(vuepress@2.0.0-rc.26(@vuepress/bundler-vite@2.0.0-rc.26(@types/node@25.0.9)(sass-embedded@1.97.2)(sass@1.97.2))(vue@3.5.26))
+ '@vuepress/plugin-links-check': 2.0.0-rc.121(vuepress@2.0.0-rc.26(@vuepress/bundler-vite@2.0.0-rc.26(@types/node@25.0.9)(sass-embedded@1.97.2)(sass@1.97.2))(vue@3.5.26))
+ '@vuepress/plugin-markdown-chart': 2.0.0-rc.121(markdown-it@14.1.0)(mermaid@11.12.2)(vuepress@2.0.0-rc.26(@vuepress/bundler-vite@2.0.0-rc.26(@types/node@25.0.9)(sass-embedded@1.97.2)(sass@1.97.2))(vue@3.5.26))
+ '@vuepress/plugin-markdown-ext': 2.0.0-rc.121(markdown-it@14.1.0)(vuepress@2.0.0-rc.26(@vuepress/bundler-vite@2.0.0-rc.26(@types/node@25.0.9)(sass-embedded@1.97.2)(sass@1.97.2))(vue@3.5.26))
+ '@vuepress/plugin-markdown-hint': 2.0.0-rc.121(markdown-it@14.1.0)(vue@3.5.26)(vuepress@2.0.0-rc.26(@vuepress/bundler-vite@2.0.0-rc.26(@types/node@25.0.9)(sass-embedded@1.97.2)(sass@1.97.2))(vue@3.5.26))
+ '@vuepress/plugin-markdown-image': 2.0.0-rc.121(markdown-it@14.1.0)(vuepress@2.0.0-rc.26(@vuepress/bundler-vite@2.0.0-rc.26(@types/node@25.0.9)(sass-embedded@1.97.2)(sass@1.97.2))(vue@3.5.26))
+ '@vuepress/plugin-markdown-include': 2.0.0-rc.121(markdown-it@14.1.0)(vuepress@2.0.0-rc.26(@vuepress/bundler-vite@2.0.0-rc.26(@types/node@25.0.9)(sass-embedded@1.97.2)(sass@1.97.2))(vue@3.5.26))
+ '@vuepress/plugin-markdown-math': 2.0.0-rc.121(katex@0.16.27)(markdown-it@14.1.0)(vuepress@2.0.0-rc.26(@vuepress/bundler-vite@2.0.0-rc.26(@types/node@25.0.9)(sass-embedded@1.97.2)(sass@1.97.2))(vue@3.5.26))
+ '@vuepress/plugin-markdown-preview': 2.0.0-rc.121(markdown-it@14.1.0)(vuepress@2.0.0-rc.26(@vuepress/bundler-vite@2.0.0-rc.26(@types/node@25.0.9)(sass-embedded@1.97.2)(sass@1.97.2))(vue@3.5.26))
+ '@vuepress/plugin-markdown-stylize': 2.0.0-rc.121(markdown-it@14.1.0)(vuepress@2.0.0-rc.26(@vuepress/bundler-vite@2.0.0-rc.26(@types/node@25.0.9)(sass-embedded@1.97.2)(sass@1.97.2))(vue@3.5.26))
+ '@vuepress/plugin-markdown-tab': 2.0.0-rc.121(markdown-it@14.1.0)(vuepress@2.0.0-rc.26(@vuepress/bundler-vite@2.0.0-rc.26(@types/node@25.0.9)(sass-embedded@1.97.2)(sass@1.97.2))(vue@3.5.26))
+ '@vuepress/plugin-notice': 2.0.0-rc.121(vuepress@2.0.0-rc.26(@vuepress/bundler-vite@2.0.0-rc.26(@types/node@25.0.9)(sass-embedded@1.97.2)(sass@1.97.2))(vue@3.5.26))
+ '@vuepress/plugin-nprogress': 2.0.0-rc.121(vuepress@2.0.0-rc.26(@vuepress/bundler-vite@2.0.0-rc.26(@types/node@25.0.9)(sass-embedded@1.97.2)(sass@1.97.2))(vue@3.5.26))
+ '@vuepress/plugin-photo-swipe': 2.0.0-rc.121(vuepress@2.0.0-rc.26(@vuepress/bundler-vite@2.0.0-rc.26(@types/node@25.0.9)(sass-embedded@1.97.2)(sass@1.97.2))(vue@3.5.26))
+ '@vuepress/plugin-reading-time': 2.0.0-rc.121(vuepress@2.0.0-rc.26(@vuepress/bundler-vite@2.0.0-rc.26(@types/node@25.0.9)(sass-embedded@1.97.2)(sass@1.97.2))(vue@3.5.26))
+ '@vuepress/plugin-redirect': 2.0.0-rc.121(vuepress@2.0.0-rc.26(@vuepress/bundler-vite@2.0.0-rc.26(@types/node@25.0.9)(sass-embedded@1.97.2)(sass@1.97.2))(vue@3.5.26))
+ '@vuepress/plugin-rtl': 2.0.0-rc.121(vuepress@2.0.0-rc.26(@vuepress/bundler-vite@2.0.0-rc.26(@types/node@25.0.9)(sass-embedded@1.97.2)(sass@1.97.2))(vue@3.5.26))
+ '@vuepress/plugin-sass-palette': 2.0.0-rc.121(sass-embedded@1.97.2)(sass@1.97.2)(vuepress@2.0.0-rc.26(@vuepress/bundler-vite@2.0.0-rc.26(@types/node@25.0.9)(sass-embedded@1.97.2)(sass@1.97.2))(vue@3.5.26))
+ '@vuepress/plugin-seo': 2.0.0-rc.121(vuepress@2.0.0-rc.26(@vuepress/bundler-vite@2.0.0-rc.26(@types/node@25.0.9)(sass-embedded@1.97.2)(sass@1.97.2))(vue@3.5.26))
+ '@vuepress/plugin-shiki': 2.0.0-rc.121(@vueuse/core@14.1.0(vue@3.5.26))(vuepress@2.0.0-rc.26(@vuepress/bundler-vite@2.0.0-rc.26(@types/node@25.0.9)(sass-embedded@1.97.2)(sass@1.97.2))(vue@3.5.26))
+ '@vuepress/plugin-sitemap': 2.0.0-rc.121(vuepress@2.0.0-rc.26(@vuepress/bundler-vite@2.0.0-rc.26(@types/node@25.0.9)(sass-embedded@1.97.2)(sass@1.97.2))(vue@3.5.26))
+ '@vuepress/plugin-theme-data': 2.0.0-rc.120(vuepress@2.0.0-rc.26(@vuepress/bundler-vite@2.0.0-rc.26(@types/node@25.0.9)(sass-embedded@1.97.2)(sass@1.97.2))(vue@3.5.26))
+ '@vueuse/core': 14.1.0(vue@3.5.26)
balloon-css: 1.2.0
- bcrypt-ts: 5.0.2
- cheerio: 1.0.0-rc.12
- chokidar: 3.6.0
- gray-matter: 4.0.3
- nodejs-jieba: 0.1.2
- vue: 3.4.21
- vuepress: 2.0.0-rc.9(@vuepress/bundler-vite@2.0.0-rc.9)(vue@3.4.21)
- vuepress-plugin-components: 2.0.0-rc.32(vuepress@2.0.0-rc.9)
- vuepress-plugin-md-enhance: 2.0.0-rc.32(markdown-it@14.1.0)(mathjax-full@3.2.2)(vuepress@2.0.0-rc.9)
- vuepress-plugin-sass-palette: 2.0.0-rc.32(vuepress@2.0.0-rc.9)
- vuepress-shared: 2.0.0-rc.32(vuepress@2.0.0-rc.9)
+ bcrypt-ts: 8.0.0
+ chokidar: 5.0.0
+ vue: 3.5.26
+ vuepress: 2.0.0-rc.26(@vuepress/bundler-vite@2.0.0-rc.26(@types/node@25.0.9)(sass-embedded@1.97.2)(sass@1.97.2))(vue@3.5.26)
+ vuepress-plugin-components: 2.0.0-rc.102(sass-embedded@1.97.2)(sass@1.97.2)(vuepress@2.0.0-rc.26(@vuepress/bundler-vite@2.0.0-rc.26(@types/node@25.0.9)(sass-embedded@1.97.2)(sass@1.97.2))(vue@3.5.26))
+ vuepress-plugin-md-enhance: 2.0.0-rc.102(markdown-it@14.1.0)(sass-embedded@1.97.2)(sass@1.97.2)(vuepress@2.0.0-rc.26(@vuepress/bundler-vite@2.0.0-rc.26(@types/node@25.0.9)(sass-embedded@1.97.2)(sass@1.97.2))(vue@3.5.26))
+ vuepress-shared: 2.0.0-rc.99(vuepress@2.0.0-rc.26(@vuepress/bundler-vite@2.0.0-rc.26(@types/node@25.0.9)(sass-embedded@1.97.2)(sass@1.97.2))(vue@3.5.26))
+ optionalDependencies:
+ '@vuepress/plugin-feed': 2.0.0-rc.121(vuepress@2.0.0-rc.26(@vuepress/bundler-vite@2.0.0-rc.26(@types/node@25.0.9)(sass-embedded@1.97.2)(sass@1.97.2))(vue@3.5.26))
+ '@vuepress/plugin-search': 2.0.0-rc.121(vuepress@2.0.0-rc.26(@vuepress/bundler-vite@2.0.0-rc.26(@types/node@25.0.9)(sass-embedded@1.97.2)(sass@1.97.2))(vue@3.5.26))
+ sass: 1.97.2
+ sass-embedded: 1.97.2
transitivePeerDependencies:
- - '@types/reveal.js'
- - '@vue/composition-api'
+ - '@mathjax/src'
- '@vue/repl'
- '@waline/client'
- artalk
@@ -3682,158 +6154,58 @@ packages:
- markmap-lib
- markmap-toolbar
- markmap-view
- - mathjax-full
- mermaid
- mpegts.js
- - plyr
- - reveal.js
- sandpack-vue3
- twikoo
- typescript
- vidstack
- dev: false
- /vuepress@2.0.0-rc.9(@vuepress/bundler-vite@2.0.0-rc.9)(vue@3.4.21):
- resolution: {integrity: sha512-jT1ln2lawdph+vVI6n2JfEUhQIcyc1RQWDdQu9DffhJGywJunFcumnUJudpqd1SNIES2Fz1hVCD6gdrE/rVKOQ==}
- engines: {node: '>=18.16.0'}
- hasBin: true
- peerDependencies:
- '@vuepress/bundler-vite': 2.0.0-rc.9
- '@vuepress/bundler-webpack': 2.0.0-rc.9
- vue: ^3.4.0
- peerDependenciesMeta:
- '@vuepress/bundler-vite':
- optional: true
- '@vuepress/bundler-webpack':
- optional: true
+ vuepress@2.0.0-rc.26(@vuepress/bundler-vite@2.0.0-rc.26(@types/node@25.0.9)(sass-embedded@1.97.2)(sass@1.97.2))(vue@3.5.26):
dependencies:
- '@vuepress/bundler-vite': 2.0.0-rc.9
- '@vuepress/cli': 2.0.0-rc.9
- '@vuepress/client': 2.0.0-rc.9
- '@vuepress/core': 2.0.0-rc.9
- '@vuepress/markdown': 2.0.0-rc.9
- '@vuepress/shared': 2.0.0-rc.9
- '@vuepress/utils': 2.0.0-rc.9
- vue: 3.4.21
+ '@vuepress/cli': 2.0.0-rc.26
+ '@vuepress/client': 2.0.0-rc.26
+ '@vuepress/core': 2.0.0-rc.26
+ '@vuepress/markdown': 2.0.0-rc.26
+ '@vuepress/shared': 2.0.0-rc.26
+ '@vuepress/utils': 2.0.0-rc.26
+ vue: 3.5.26
+ optionalDependencies:
+ '@vuepress/bundler-vite': 2.0.0-rc.26(@types/node@25.0.9)(sass-embedded@1.97.2)(sass@1.97.2)
transitivePeerDependencies:
- supports-color
- typescript
- dev: false
-
- /webidl-conversions@3.0.1:
- resolution: {integrity: sha512-2JAn3z8AR6rjK8Sm8orRC0h/bcl/DqL7tRPdGZ4I1CjdF+EaMLmYxBHyXuKL849eucPFhvBoxMsflfOb8kxaeQ==}
- dev: false
-
- /whatwg-url@5.0.0:
- resolution: {integrity: sha512-saE57nupxk6v3HY35+jzBwYa0rKSy0XR8JSxZPwgLr7ys0IBzhGviA1/TUGJLmSVqs8pb9AnvICXEuOHLprYTw==}
- dependencies:
- tr46: 0.0.3
- webidl-conversions: 3.0.1
- dev: false
-
- /which-module@2.0.1:
- resolution: {integrity: sha512-iBdZ57RDvnOR9AGBhML2vFZf7h8vmBjhoaZqODJBFWHVtKkDmKuHai3cx5PgVMrX5YDNp27AofYbAwctSS+vhQ==}
- dev: false
- /which@2.0.2:
- resolution: {integrity: sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA==}
- engines: {node: '>= 8'}
- hasBin: true
- dependencies:
- isexe: 2.0.0
- dev: false
+ web-namespaces@2.0.1: {}
- /which@4.0.0:
- resolution: {integrity: sha512-GlaYyEb07DPxYCKhKzplCWBJtvxZcZMrL+4UkrTSJHHPyZU4mYYTv3qaOe77H7EODLSSopAUFAc6W8U4yqvscg==}
- engines: {node: ^16.13.0 || >=18.0.0}
- hasBin: true
+ whatwg-encoding@3.1.1:
dependencies:
- isexe: 3.1.1
- dev: false
+ iconv-lite: 0.6.3
- /wicked-good-xpath@1.3.0:
- resolution: {integrity: sha512-Gd9+TUn5nXdwj/hFsPVx5cuHHiF5Bwuc30jZ4+ronF1qHK5O7HD0sgmXWSEgwKquT3ClLoKPVbO6qGwVwLzvAw==}
- dev: false
+ whatwg-mimetype@4.0.0: {}
- /wide-align@1.1.5:
- resolution: {integrity: sha512-eDMORYaPNZ4sQIuuYPDHdQvf4gyCF9rEEV/yPxGfwPkRodwEgiMUUXTx/dex+Me0wxx53S+NgUHaP7y3MGlDmg==}
- dependencies:
- string-width: 4.2.3
- dev: false
+ which-module@2.0.1: {}
- /wrap-ansi@6.2.0:
- resolution: {integrity: sha512-r6lPcBGxZXlIcymEu7InxDMhdW0KDxpLgoFLcguasxCaJ/SOIZwINatK9KY/tf+ZrlywOKU0UDj3ATXUBfxJXA==}
- engines: {node: '>=8'}
- dependencies:
- ansi-styles: 4.3.0
- string-width: 4.2.3
- strip-ansi: 6.0.1
- dev: false
+ wicked-good-xpath@1.3.0: {}
- /wrap-ansi@7.0.0:
- resolution: {integrity: sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q==}
- engines: {node: '>=10'}
+ wrap-ansi@6.2.0:
dependencies:
ansi-styles: 4.3.0
string-width: 4.2.3
strip-ansi: 6.0.1
- dev: false
-
- /wrap-ansi@8.1.0:
- resolution: {integrity: sha512-si7QWI6zUMq56bESFvagtmzMdGOtoxfR+Sez11Mobfc7tm+VkUckk9bW2UeffTGVUbOksxmSw0AA2gs8g71NCQ==}
- engines: {node: '>=12'}
- dependencies:
- ansi-styles: 6.2.1
- string-width: 5.1.2
- strip-ansi: 7.1.0
- dev: false
-
- /wrappy@1.0.2:
- resolution: {integrity: sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ==}
- dev: false
- /xml-js@1.6.11:
- resolution: {integrity: sha512-7rVi2KMfwfWFl+GpPg6m80IVMWXLRjO+PxTq7V2CDhoGak0wzYzFgUY2m4XJ47OGdXd8eLE8EmwfAmdjw7lC1g==}
- hasBin: true
+ xml-js@1.6.11:
dependencies:
- sax: 1.3.0
- dev: false
-
- /xmldom-sre@0.1.31:
- resolution: {integrity: sha512-f9s+fUkX04BxQf+7mMWAp5zk61pciie+fFLC9hX9UVvCeJQfNHRHXpeo5MPcR0EUf57PYLdt+ZO4f3Ipk2oZUw==}
- engines: {node: '>=0.1'}
- dev: false
-
- /y18n@4.0.3:
- resolution: {integrity: sha512-JKhqTOwSrqNA1NY5lSztJ1GrBiUodLMmIZuLiDaMRJ+itFd+ABVE8XBjOvIWL+rSqNDC74LCSFmlb/U4UZ4hJQ==}
- dev: false
-
- /yallist@4.0.0:
- resolution: {integrity: sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A==}
- dev: false
+ sax: 1.4.4
- /yaml@2.3.4:
- resolution: {integrity: sha512-8aAvwVUSHpfEqTQ4w/KMlf3HcRdt50E5ODIQJBw1fQ5RL34xabzxtUlzTXVqc4rkZsPbvrXKWnABCD7kWSmocA==}
- engines: {node: '>= 14'}
- dev: false
+ y18n@4.0.3: {}
- /yaml@2.4.1:
- resolution: {integrity: sha512-pIXzoImaqmfOrL7teGUBt/T7ZDnyeGBWyXQBvOVhLkWLN37GXv8NMLK406UY6dS51JfcQHsmcW5cJ441bHg6Lg==}
- engines: {node: '>= 14'}
- hasBin: true
- dev: false
-
- /yargs-parser@18.1.3:
- resolution: {integrity: sha512-o50j0JeToy/4K6OZcaQmW6lyXXKhq7csREXcDwk2omFPJEwUNOVtJKvmDr9EI1fAJZUyZcRF7kxGBWmRXudrCQ==}
- engines: {node: '>=6'}
+ yargs-parser@18.1.3:
dependencies:
camelcase: 5.3.1
decamelize: 1.2.0
- dev: false
- /yargs@15.4.1:
- resolution: {integrity: sha512-aePbxDmcYW++PaqBsJ+HYUFwCdv4LVvdnhBy78E57PIor8/OVvhMrADFFEDh8DHDFRv/O9i3lPhsENjO7QX0+A==}
- engines: {node: '>=8'}
+ yargs@15.4.1:
dependencies:
cliui: 6.0.0
decamelize: 1.2.0
@@ -3846,4 +6218,7 @@ packages:
which-module: 2.0.1
y18n: 4.0.3
yargs-parser: 18.1.3
- dev: false
+
+ yoctocolors@2.1.2: {}
+
+ zwitch@2.0.4: {}
diff --git a/translate_repo.py b/translate_repo.py
new file mode 100755
index 00000000000..41828334976
--- /dev/null
+++ b/translate_repo.py
@@ -0,0 +1,318 @@
+#!/usr/bin/env python3
+"""
+Batch Translation Tool for Repository Documentation
+
+Translates all markdown files in docs/ folder to target language.
+Preserves directory structure and saves to docs_{lang}/ folder.
+"""
+
+import os
+import sys
+import time
+import json
+from pathlib import Path
+from deep_translator import GoogleTranslator
+
+# Language configurations
+LANGUAGES = {
+ '1': {'name': 'English', 'code': 'en', 'suffix': 'en'},
+ '2': {'name': 'Chinese (Simplified)', 'code': 'zh-CN', 'suffix': 'zh'},
+ '3': {'name': 'Spanish', 'code': 'es', 'suffix': 'es'},
+ '4': {'name': 'French', 'code': 'fr', 'suffix': 'fr'},
+ '5': {'name': 'Portuguese', 'code': 'pt', 'suffix': 'pt'},
+ '6': {'name': 'German', 'code': 'de', 'suffix': 'de'},
+ '7': {'name': 'Japanese', 'code': 'ja', 'suffix': 'ja'},
+ '8': {'name': 'Korean', 'code': 'ko', 'suffix': 'ko'},
+ '9': {'name': 'Russian', 'code': 'ru', 'suffix': 'ru'},
+ '10': {'name': 'Italian', 'code': 'it', 'suffix': 'it'},
+ '11': {'name': 'Arabic', 'code': 'ar', 'suffix': 'ar'},
+ '12': {'name': 'Hindi', 'code': 'hi', 'suffix': 'hi'},
+ '13': {'name': 'Turkish', 'code': 'tr', 'suffix': 'tr'},
+ '14': {'name': 'Vietnamese', 'code': 'vi', 'suffix': 'vi'},
+ '15': {'name': 'Polish', 'code': 'pl', 'suffix': 'pl'},
+ '16': {'name': 'Dutch', 'code': 'nl', 'suffix': 'nl'},
+ '17': {'name': 'Indonesian', 'code': 'id', 'suffix': 'id'},
+ '18': {'name': 'Thai', 'code': 'th', 'suffix': 'th'},
+ '19': {'name': 'Swedish', 'code': 'sv', 'suffix': 'sv'},
+ '20': {'name': 'Greek', 'code': 'el', 'suffix': 'el'},
+}
+
+CHUNK_SIZE = 4000 # Characters per chunk
+PROGRESS_FILE = '.translation_progress.json'
+
+
+def print_header():
+ print("=" * 70)
+ print("Repository Documentation Translation Tool")
+ print("=" * 70)
+ print()
+
+
+def select_language():
+ """Let user select target language"""
+ print("=" * 70)
+ print("Select target language:")
+ print("=" * 70)
+
+ for num, lang in LANGUAGES.items():
+ print(f" {num:>2}. {lang['name']}")
+
+ print()
+ while True:
+ choice = input("Enter choice (1-20): ").strip()
+ if choice in LANGUAGES:
+ return LANGUAGES[choice]
+ print("❌ Invalid choice. Please enter a number between 1-20.")
+
+
+def find_markdown_files(repo_path):
+ """Find all markdown files in docs/ folder and README.md"""
+ repo_path = Path(repo_path)
+ docs_path = repo_path / 'docs'
+
+ files = []
+
+ # Add README.md if exists
+ readme = repo_path / 'README.md'
+ if readme.exists():
+ files.append(readme)
+
+ # Add all .md files in docs/
+ if docs_path.exists():
+ for md_file in docs_path.rglob('*.md'):
+ files.append(md_file)
+
+ return sorted(files)
+
+
+def get_output_path(input_path, repo_path, lang_suffix):
+ """
+ Convert input path to output path.
+ docs/java/basics.md -> docs_en/java/basics.en.md
+ README.md -> README.en.md
+ """
+ repo_path = Path(repo_path)
+ input_path = Path(input_path)
+
+ # Handle README.md
+ if input_path.name == 'README.md':
+ return repo_path / f'README.{lang_suffix}.md'
+
+ # Handle docs/ files
+ relative = input_path.relative_to(repo_path / 'docs')
+
+ # Change extension: file.md -> file.{lang}.md
+ stem = relative.stem
+ new_name = f'{stem}.{lang_suffix}.md'
+
+ output_path = repo_path / f'docs_{lang_suffix}' / relative.parent / new_name
+ return output_path
+
+
+def split_content(content, chunk_size=CHUNK_SIZE):
+ """Split content into chunks, preserving code blocks"""
+ chunks = []
+ current_chunk = ""
+ in_code_block = False
+
+ lines = content.split('\n')
+
+ for line in lines:
+ # Track code blocks
+ if line.strip().startswith('```'):
+ in_code_block = not in_code_block
+
+ # If adding this line exceeds chunk size and we're not in a code block
+ if len(current_chunk) + len(line) > chunk_size and not in_code_block and current_chunk:
+ chunks.append(current_chunk)
+ current_chunk = line + '\n'
+ else:
+ current_chunk += line + '\n'
+
+ if current_chunk:
+ chunks.append(current_chunk)
+
+ return chunks
+
+
+def translate_text(text, target_lang):
+ """Translate text using Google Translate"""
+ try:
+ translator = GoogleTranslator(source='auto', target=target_lang)
+ translated = translator.translate(text)
+ return translated
+ except Exception as e:
+ print(f"\n⚠️ Translation error: {e}")
+ return text # Return original on error
+
+
+def translate_file(input_path, output_path, lang_code):
+ """Translate a single markdown file"""
+ # Read input
+ with open(input_path, 'r', encoding='utf-8') as f:
+ content = f.read()
+
+ # Split into chunks
+ chunks = split_content(content)
+
+ # Translate each chunk
+ translated_chunks = []
+ for i, chunk in enumerate(chunks, 1):
+ print(f" Chunk {i}/{len(chunks)}... ", end='', flush=True)
+ translated = translate_text(chunk, lang_code)
+ translated_chunks.append(translated)
+ print("✅")
+ time.sleep(1) # Rate limiting
+
+ # Combine translated chunks
+ translated_content = ''.join(translated_chunks)
+
+ # Create output directory
+ output_path.parent.mkdir(parents=True, exist_ok=True)
+
+ # Write output
+ with open(output_path, 'w', encoding='utf-8') as f:
+ f.write(translated_content)
+
+ return len(content), len(translated_content)
+
+
+def load_progress(repo_path):
+ """Load translation progress"""
+ progress_file = Path(repo_path) / PROGRESS_FILE
+ if progress_file.exists():
+ with open(progress_file, 'r') as f:
+ return json.load(f)
+ return {'completed': [], 'failed': []}
+
+
+def save_progress(repo_path, progress):
+ """Save translation progress"""
+ progress_file = Path(repo_path) / PROGRESS_FILE
+ with open(progress_file, 'w') as f:
+ json.dump(progress, f, indent=2)
+
+
+def main():
+ print_header()
+
+ # Get repository path
+ repo_path = input("Enter repository path (default: current directory): ").strip()
+ if not repo_path:
+ repo_path = '.'
+
+ repo_path = Path(repo_path).resolve()
+
+ if not repo_path.exists():
+ print(f"❌ Repository path does not exist: {repo_path}")
+ sys.exit(1)
+
+ print(f"📁 Repository: {repo_path}")
+ print()
+
+ # Select language
+ lang_config = select_language()
+ print(f"\n✨ Selected: {lang_config['name']}")
+ print()
+
+ # Find all markdown files
+ print("🔍 Finding markdown files...")
+ md_files = find_markdown_files(repo_path)
+
+ if not md_files:
+ print("❌ No markdown files found in docs/ folder or README.md")
+ sys.exit(1)
+
+ print(f"📄 Found {len(md_files)} markdown files")
+ print()
+
+ # Load progress
+ progress = load_progress(repo_path)
+
+ # Filter out already completed files
+ files_to_translate = []
+ for f in md_files:
+ output_path = get_output_path(f, repo_path, lang_config['suffix'])
+ if output_path.exists():
+ print(f"⏭️ Skipping (exists): {f.relative_to(repo_path)}")
+ elif str(f) in progress['completed']:
+ print(f"⏭️ Skipping (completed): {f.relative_to(repo_path)}")
+ else:
+ files_to_translate.append(f)
+
+ if not files_to_translate:
+ print("\n✅ All files already translated!")
+ sys.exit(0)
+
+ print(f"\n📝 Files to translate: {len(files_to_translate)}")
+ print()
+
+ # Confirm
+ confirm = input(f"Translate {len(files_to_translate)} files to {lang_config['name']}? (y/n): ").strip().lower()
+ if confirm != 'y':
+ print("❌ Translation cancelled")
+ sys.exit(0)
+
+ print()
+ print("=" * 70)
+ print(f"Translating to {lang_config['name']}...")
+ print("=" * 70)
+ print()
+
+ # Translate files
+ total_input_chars = 0
+ total_output_chars = 0
+ failed_files = []
+
+ for idx, input_path in enumerate(files_to_translate, 1):
+ relative_path = input_path.relative_to(repo_path)
+ output_path = get_output_path(input_path, repo_path, lang_config['suffix'])
+
+ print(f"[{idx}/{len(files_to_translate)}] {relative_path}")
+ print(f" → {output_path.relative_to(repo_path)}")
+
+ try:
+ input_chars, output_chars = translate_file(input_path, output_path, lang_config['code'])
+ total_input_chars += input_chars
+ total_output_chars += output_chars
+
+ # Mark as completed
+ progress['completed'].append(str(input_path))
+ save_progress(repo_path, progress)
+
+ print(f" ✅ Translated ({input_chars} → {output_chars} chars)")
+ print()
+
+ except Exception as e:
+ print(f" ❌ Failed: {e}")
+ failed_files.append((str(relative_path), str(e)))
+ progress['failed'].append(str(input_path))
+ save_progress(repo_path, progress)
+ print()
+
+ # Summary
+ print("=" * 70)
+ print("Translation Complete!")
+ print("=" * 70)
+ print(f"✅ Translated: {len(files_to_translate) - len(failed_files)} files")
+ print(f"📊 Input: {total_input_chars:,} characters")
+ print(f"📊 Output: {total_output_chars:,} characters")
+
+ if failed_files:
+ print(f"\n❌ Failed: {len(failed_files)} files")
+ for file, error in failed_files:
+ print(f" - {file}: {error}")
+
+ print(f"\n📁 Output directory: docs_{lang_config['suffix']}/")
+ print(f"📁 README: README.{lang_config['suffix']}.md")
+ print()
+ print("💡 Next steps:")
+ print(f" 1. Review translated files in docs_{lang_config['suffix']}/")
+ print(f" 2. git add docs_{lang_config['suffix']}/ README.{lang_config['suffix']}.md")
+ print(f" 3. git commit -m 'Add {lang_config['name']} translation'")
+ print(" 4. Create PR")
+ print()
+
+
+if __name__ == "__main__":
+ main()