From a57a7ae0e82afcb7809669adc0f259e1ebf5da99 Mon Sep 17 00:00:00 2001 From: HaiBooLang <53350622+HaiBooLang@users.noreply.github.com> Date: Wed, 12 Feb 2025 15:14:49 +0800 Subject: [PATCH 01/74] =?UTF-8?q?fix:=E4=BF=AE=E6=AD=A3=E6=A0=87=E7=82=B9?= =?UTF-8?q?=E7=AC=A6=E5=8F=B7?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- docs/java/basis/java-basic-questions-03.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/java/basis/java-basic-questions-03.md b/docs/java/basis/java-basic-questions-03.md index 15f24b7d8fa..af8f2274054 100644 --- a/docs/java/basis/java-basic-questions-03.md +++ b/docs/java/basis/java-basic-questions-03.md @@ -217,7 +217,7 @@ catch (IOException e) { - 不要把异常定义为静态变量,因为这样会导致异常栈信息错乱。每次手动抛出异常,我们都需要手动 new 一个异常对象抛出。 - 抛出的异常信息一定要有意义。 -- 建议抛出更加具体的异常比如字符串转换为数字格式错误的时候应该抛出`NumberFormatException`而不是其父类`IllegalArgumentException`。 +- 建议抛出更加具体的异常,比如字符串转换为数字格式错误的时候应该抛出`NumberFormatException`而不是其父类`IllegalArgumentException`。 - 避免重复记录日志:如果在捕获异常的地方已经记录了足够的信息(包括异常类型、错误信息和堆栈跟踪等),那么在业务代码中再次抛出这个异常时,就不应该再次记录相同的错误信息。重复记录日志会使得日志文件膨胀,并且可能会掩盖问题的实际原因,使得问题更难以追踪和解决。 - …… From c96739f6fa112dc71902f1a7775928846d923f8e Mon Sep 17 00:00:00 2001 From: HaiBooLang <53350622+HaiBooLang@users.noreply.github.com> Date: Thu, 13 Feb 2025 18:52:10 +0800 Subject: [PATCH 02/74] =?UTF-8?q?fix:=E8=A1=A5=E5=85=85=E8=AF=B4=E6=98=8E?= =?UTF-8?q?=E5=9B=9B=E7=A7=8D=E5=BC=95=E7=94=A8=E7=9A=84=E6=A6=82=E5=BF=B5?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- docs/java/jvm/jvm-garbage-collection.md | 39 +++++++++++++++++++++---- 1 file changed, 34 insertions(+), 5 deletions(-) diff --git a/docs/java/jvm/jvm-garbage-collection.md b/docs/java/jvm/jvm-garbage-collection.md index 22c5ddf8a4b..79f201e03d7 100644 --- a/docs/java/jvm/jvm-garbage-collection.md +++ b/docs/java/jvm/jvm-garbage-collection.md @@ -253,29 +253,58 @@ public class ReferenceCountingGc { JDK1.2 之前,Java 中引用的定义很传统:如果 reference 类型的数据存储的数值代表的是另一块内存的起始地址,就称这块内存代表一个引用。 -JDK1.2 以后,Java 对引用的概念进行了扩充,将引用分为强引用、软引用、弱引用、虚引用四种(引用强度逐渐减弱) +JDK1.2 以后,Java 对引用的概念进行了扩充,将引用分为强引用、软引用、弱引用、虚引用四种(引用强度逐渐减弱),强引用就是Java中普通的对象,而软引用、弱引用、虚引用在JDK中定义的类分别是SoftReference、WeakReference、PhantomReference。 ![Java 引用类型总结](https://oss.javaguide.cn/github/javaguide/java/jvm/java-reference-type.png) **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); +``` **虚引用主要用来跟踪对象被垃圾回收的活动**。 From 8741faddd3c279b67f429ff942607b20ad7e5c77 Mon Sep 17 00:00:00 2001 From: HaiBooLang <53350622+HaiBooLang@users.noreply.github.com> Date: Thu, 13 Feb 2025 20:17:51 +0800 Subject: [PATCH 03/74] =?UTF-8?q?fix:=E4=BF=AE=E6=AD=A3=E6=84=8F=E5=A4=96?= =?UTF-8?q?=E6=8D=A2=E8=A1=8C?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- docs/java/new-features/java8-common-new-features.md | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/docs/java/new-features/java8-common-new-features.md b/docs/java/new-features/java8-common-new-features.md index dfcc4cfe3d2..e402ba5a882 100644 --- a/docs/java/new-features/java8-common-new-features.md +++ b/docs/java/new-features/java8-common-new-features.md @@ -95,9 +95,7 @@ public class InterfaceNewImpl implements InterfaceNew , InterfaceNew1{ 在 java 8 中专门有一个包放函数式接口`java.util.function`,该包下的所有接口都有 `@FunctionalInterface` 注解,提供函数式编程。 -在其他包中也有函数式接口,其中一些没有`@FunctionalInterface` 注解,但是只要符合函数式接口的定义就是函数式接口,与是否有 - -`@FunctionalInterface`注解无关,注解只是在编译时起到强制规范定义的作用。其在 Lambda 表达式中有广泛的应用。 +在其他包中也有函数式接口,其中一些没有`@FunctionalInterface` 注解,但是只要符合函数式接口的定义就是函数式接口,与是否有`@FunctionalInterface`注解无关,注解只是在编译时起到强制规范定义的作用。其在 Lambda 表达式中有广泛的应用。 ## Lambda 表达式 From 9edc188827b7bafcc1fd1712911b3b5af61ce104 Mon Sep 17 00:00:00 2001 From: yitacls <75364857+yitacls@users.noreply.github.com> Date: Thu, 13 Feb 2025 22:16:01 +0800 Subject: [PATCH 04/74] =?UTF-8?q?fix:=E4=BF=AE=E6=AD=A3=E4=BA=86=E4=B8=8D?= =?UTF-8?q?=E5=87=86=E7=A1=AE=E7=9A=84=E7=89=88=E6=9C=AC=E5=90=8D=E7=A7=B0?= =?UTF-8?q?=EF=BC=8C=E5=B9=B6=E8=A1=A5=E5=85=85=E4=BA=86SHA-3=E7=9A=84?= =?UTF-8?q?=E7=AE=80=E8=A6=81=E8=AF=B4=E6=98=8E?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- docs/cs-basics/network/network-attack-means.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/cs-basics/network/network-attack-means.md b/docs/cs-basics/network/network-attack-means.md index dfacb45b92c..c4a4da8f231 100644 --- a/docs/cs-basics/network/network-attack-means.md +++ b/docs/cs-basics/network/network-attack-means.md @@ -363,7 +363,7 @@ MD5 可以用来生成一个 128 位的消息摘要,它是目前应用比较 **SHA** -安全散列算法。**SHA** 分为 **SHA1** 和 **SH2** 两个版本。该算法的思想是接收一段明文,然后以一种不可逆的方式将它转换成一段(通常更小)密文,也可以简单的理解为取一串输入码(称为预映射或信息),并把它们转化为长度较短、位数固定的输出序列即散列值(也称为信息摘要或信息认证代码)的过程。 +安全散列算法。**SHA** 包括**SHA-1**、**SHA-2**和**SHA-3**三个版本。该算法的基本思想是:接收一段明文数据,通过不可逆的方式将其转换为固定长度的密文。简单来说,SHA将输入数据(即预映射或消息)转化为固定长度、较短的输出值,称为散列值(或信息摘要、信息认证码)。SHA-1已被证明不够安全,因此逐渐被SHA-2取代,而SHA-3则作为SHA系列的最新版本,采用不同的结构(Keccak算法)提供更高的安全性和灵活性。 **SM3** From 45f7d62695da291ed4f8d2a47511b1e8c6e1f225 Mon Sep 17 00:00:00 2001 From: HaiBooLang <53350622+HaiBooLang@users.noreply.github.com> Date: Thu, 13 Feb 2025 23:17:19 +0800 Subject: [PATCH 05/74] =?UTF-8?q?fix:=E4=BD=BF=E6=A6=82=E5=BF=B5=E8=A1=A8?= =?UTF-8?q?=E8=BF=B0=E6=9B=B4=E5=8A=A0=E4=B8=A5=E8=B0=A8?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- docs/java/jvm/classloader.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/java/jvm/classloader.md b/docs/java/jvm/classloader.md index 15895201a01..35a8bfd0eda 100644 --- a/docs/java/jvm/classloader.md +++ b/docs/java/jvm/classloader.md @@ -58,7 +58,7 @@ class Class { } ``` -简单来说,**类加载器的主要作用就是加载 Java 类的字节码( `.class` 文件)到 JVM 中(在内存中生成一个代表该类的 `Class` 对象)。** 字节码可以是 Java 源程序(`.java`文件)经过 `javac` 编译得来,也可以是通过工具动态生成或者通过网络下载得来。 +简单来说,**类加载器的主要作用就是动态加载 Java 类的字节码( `.class` 文件)到 JVM 中(在内存中生成一个代表该类的 `Class` 对象)。** 字节码可以是 Java 源程序(`.java`文件)经过 `javac` 编译得来,也可以是通过工具动态生成或者通过网络下载得来。 其实除了加载类之外,类加载器还可以加载 Java 应用所需的资源如文本、图像、配置文件、视频等等文件资源。本文只讨论其核心功能:加载类。 From 2aad0b550b63c5ef97f13baa51a50ebf6985bfa4 Mon Sep 17 00:00:00 2001 From: Guide Date: Fri, 14 Feb 2025 11:03:11 +0800 Subject: [PATCH 06/74] =?UTF-8?q?[docs=20fix]=E4=BF=AE=E6=AD=A3=E7=B3=BB?= =?UTF-8?q?=E7=BB=9F=E8=B0=83=E7=94=A8=E7=9A=84=E8=BF=87=E7=A8=8B=E6=8F=8F?= =?UTF-8?q?=E8=BF=B0=E5=92=8C=E5=9B=BE=E8=A7=A3?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../operating-system-basic-questions-01.md | 2 +- docs/database/mysql/mysql-questions-01.md | 21 +++++++++++++------ docs/java/jvm/jvm-garbage-collection.md | 2 +- 3 files changed, 17 insertions(+), 8 deletions(-) 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 ffab1671e35..db7fb7bfa23 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 @@ -151,7 +151,7 @@ _玩玩电脑游戏还是必须要有 Windows 的,所以我现在是一台 Win 1. 用户态的程序发起系统调用,因为系统调用中涉及一些特权指令(只能由操作系统内核态执行的指令),用户态程序权限不足,因此会中断执行,也就是 Trap(Trap 是一种中断)。 2. 发生中断后,当前 CPU 执行的程序会中断,跳转到中断处理程序。内核程序开始执行,也就是开始处理系统调用。 -3. 内核处理完成后,主动触发 Trap,这样会再次发生中断,切换回用户态工作。 +3. 当系统调用处理完成后,操作系统使用特权指令(如 `iret`、`sysret` 或 `eret`)切换回用户态,恢复用户态的上下文,继续执行用户程序。 ![系统调用的过程](https://oss.javaguide.cn/github/javaguide/cs-basics/operating-system/system-call-procedure.png) diff --git a/docs/database/mysql/mysql-questions-01.md b/docs/database/mysql/mysql-questions-01.md index 9991c752080..1f225d3702e 100644 --- a/docs/database/mysql/mysql-questions-01.md +++ b/docs/database/mysql/mysql-questions-01.md @@ -165,12 +165,21 @@ TIMESTAMP 只需要使用 4 个字节的存储空间,但是 DATETIME 需要耗 ### 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` 作为列默认值?”也有了答案。 diff --git a/docs/java/jvm/jvm-garbage-collection.md b/docs/java/jvm/jvm-garbage-collection.md index 79f201e03d7..88d182a9b59 100644 --- a/docs/java/jvm/jvm-garbage-collection.md +++ b/docs/java/jvm/jvm-garbage-collection.md @@ -253,7 +253,7 @@ public class ReferenceCountingGc { JDK1.2 之前,Java 中引用的定义很传统:如果 reference 类型的数据存储的数值代表的是另一块内存的起始地址,就称这块内存代表一个引用。 -JDK1.2 以后,Java 对引用的概念进行了扩充,将引用分为强引用、软引用、弱引用、虚引用四种(引用强度逐渐减弱),强引用就是Java中普通的对象,而软引用、弱引用、虚引用在JDK中定义的类分别是SoftReference、WeakReference、PhantomReference。 +JDK1.2 以后,Java 对引用的概念进行了扩充,将引用分为强引用、软引用、弱引用、虚引用四种(引用强度逐渐减弱),强引用就是 Java 中普通的对象,而软引用、弱引用、虚引用在JDK中定义的类分别是 `SoftReference`、`WeakReference`、`PhantomReference`。 ![Java 引用类型总结](https://oss.javaguide.cn/github/javaguide/java/jvm/java-reference-type.png) From 25de0a492ccabb9cdd28e348c823f5624b919b8d Mon Sep 17 00:00:00 2001 From: HaiBooLang <53350622+HaiBooLang@users.noreply.github.com> Date: Fri, 14 Feb 2025 14:20:58 +0800 Subject: [PATCH 07/74] =?UTF-8?q?fix:=E4=BF=AE=E6=AD=A3=E6=A0=87=E7=82=B9?= =?UTF-8?q?=E7=AC=A6=E5=8F=B7?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- docs/java/concurrent/java-concurrent-questions-01.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/java/concurrent/java-concurrent-questions-01.md b/docs/java/concurrent/java-concurrent-questions-01.md index d45c93092ab..d99c6527dea 100644 --- a/docs/java/concurrent/java-concurrent-questions-01.md +++ b/docs/java/concurrent/java-concurrent-questions-01.md @@ -220,7 +220,7 @@ new 一个 `Thread`,线程进入了新建状态。调用 `start()`方法,会 先从总体上来说: -- **从计算机底层来说:** 线程可以比作是轻量级的进程,是程序执行的最小单位,线程间的切换和调度的成本远远小于进程。另外,多核 CPU 时代意味着多个线程可以同时运行,这减少了线程上下文切换的开销。 +- **从计算机底层来说:** 线程可以比作是轻量级的进程,是程序执行的最小单位,线程间的切换和调度的成本远远小于进程。另外,多核 CPU 时代意味着多个线程可以同时运行,这减少了线程上下文切换的开销。 - **从当代互联网发展趋势来说:** 现在的系统动不动就要求百万级甚至千万级的并发量,而多线程并发编程正是开发高并发系统的基础,利用好多线程机制可以大大提高系统整体的并发能力以及性能。 再深入到计算机底层来探讨: From 2a6783d0e432ce6e471c56399051b25b4c02db14 Mon Sep 17 00:00:00 2001 From: HaiBooLang <53350622+HaiBooLang@users.noreply.github.com> Date: Fri, 14 Feb 2025 14:35:09 +0800 Subject: [PATCH 08/74] =?UTF-8?q?fix:=E4=BF=AE=E6=AD=A3=E6=A0=87=E7=82=B9?= =?UTF-8?q?=E7=AC=A6=E5=8F=B7?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- docs/java/concurrent/java-concurrent-questions-01.md | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/docs/java/concurrent/java-concurrent-questions-01.md b/docs/java/concurrent/java-concurrent-questions-01.md index d99c6527dea..a0b3657f51a 100644 --- a/docs/java/concurrent/java-concurrent-questions-01.md +++ b/docs/java/concurrent/java-concurrent-questions-01.md @@ -265,7 +265,7 @@ Java 使用的线程调度是抢占式的。也就是说,JVM 本身不负责 ## ⭐️死锁 -### 什么是线程死锁? +### 什么是线程死锁? 线程死锁描述的是这样一种情况:多个线程同时被阻塞,它们中的一个或者全部都在等待某个资源被释放。由于线程被无限期地阻塞,因此程序不可能正常终止。 @@ -323,14 +323,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. 循环等待条件:若干线程之间形成一种头尾相接的循环等待资源关系。 +3. 不剥夺条件:线程已获得的资源在未使用完之前不能被其他线程强行剥夺,只有自己使用完毕后才释放资源。 +4. 循环等待条件:若干线程之间形成一种头尾相接的循环等待资源关系。 ### 如何检测死锁? From 10b1ad27031f33641e07b7be137c4285b9a47ca4 Mon Sep 17 00:00:00 2001 From: HaiBooLang <53350622+HaiBooLang@users.noreply.github.com> Date: Fri, 14 Feb 2025 14:54:41 +0800 Subject: [PATCH 09/74] Update java-concurrent-questions-01.md --- docs/java/concurrent/java-concurrent-questions-01.md | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/docs/java/concurrent/java-concurrent-questions-01.md b/docs/java/concurrent/java-concurrent-questions-01.md index a0b3657f51a..50b3922baec 100644 --- a/docs/java/concurrent/java-concurrent-questions-01.md +++ b/docs/java/concurrent/java-concurrent-questions-01.md @@ -327,10 +327,10 @@ Thread[线程 2,5,main]waiting get resource1 上面的例子符合产生死锁的四个必要条件: -1. 互斥条件:该资源任意一个时刻只由一个线程占用。 -2. 请求与保持条件:一个线程因请求资源而阻塞时,对已获得的资源保持不放。 -3. 不剥夺条件:线程已获得的资源在未使用完之前不能被其他线程强行剥夺,只有自己使用完毕后才释放资源。 -4. 循环等待条件:若干线程之间形成一种头尾相接的循环等待资源关系。 +1. **互斥条件**:该资源任意一个时刻只由一个线程占用。 +2. **请求与保持条件**:一个线程因请求资源而阻塞时,对已获得的资源保持不放。 +3. **不剥夺条件**:线程已获得的资源在未使用完之前不能被其他线程强行剥夺,只有自己使用完毕后才释放资源。 +4. **循环等待条件**:若干线程之间形成一种头尾相接的循环等待资源关系。 ### 如何检测死锁? From 3104392b9a53167660d2c4e0132964d7a014d95d Mon Sep 17 00:00:00 2001 From: JoeyChan Date: Fri, 14 Feb 2025 22:46:50 +0800 Subject: [PATCH 10/74] =?UTF-8?q?[doc=20fix]=E4=BF=AE=E6=AD=A3=E4=BA=8B?= =?UTF-8?q?=E5=8A=A1=E4=BC=A0=E6=92=AD=E8=A1=8C=E4=B8=BA=E4=B8=ADTransacti?= =?UTF-8?q?onDefinition.PROPAGATION=5FNESTED=E6=A1=88=E4=BE=8B=E6=8F=8F?= =?UTF-8?q?=E8=BF=B0=E9=94=99=E8=AF=AF?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../framework/spring/spring-transaction.md | 68 +++++++++++++------ 1 file changed, 48 insertions(+), 20 deletions(-) diff --git a/docs/system-design/framework/spring/spring-transaction.md b/docs/system-design/framework/spring/spring-transaction.md index 47eac108cb8..b294d6f6052 100644 --- a/docs/system-design/framework/spring/spring-transaction.md +++ b/docs/system-design/framework/spring/spring-transaction.md @@ -424,28 +424,56 @@ Class B { - 在外部方法开启事务的情况下,在内部开启一个新的事务,作为嵌套事务存在。 - 如果外部方法无事务,则单独开启一个事务,与 `PROPAGATION_REQUIRED` 类似。 -这里还是简单举个例子:如果 `bMethod()` 回滚的话,`aMethod()`不会回滚。如果 `aMethod()` 回滚的话,`bMethod()`会回滚。 - -```java -@Service -Class A { - @Autowired - B b; - @Transactional(propagation = Propagation.REQUIRED) - public void aMethod { - //do something - b.bMethod(); +举个例子: +- 如果 `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`** From 610c103d306c818579c8b5a39bf21efeed22af44 Mon Sep 17 00:00:00 2001 From: Clear Date: Sat, 15 Feb 2025 13:52:54 +0800 Subject: [PATCH 11/74] =?UTF-8?q?java10.md-=E9=94=99=E8=AF=AF=E8=AE=A2?= =?UTF-8?q?=E6=AD=A3?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- docs/java/new-features/java10.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/java/new-features/java10.md b/docs/java/new-features/java10.md index d52cac575b2..ee5fbb18187 100644 --- a/docs/java/new-features/java10.md +++ b/docs/java/new-features/java10.md @@ -53,7 +53,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 的发生,以带来更好的性能提升、更大的吞吐量。 From 470c9cbfd3f67ed66d2e0a386a346e5b419b31ab Mon Sep 17 00:00:00 2001 From: Clear Date: Sat, 15 Feb 2025 13:55:47 +0800 Subject: [PATCH 12/74] =?UTF-8?q?Update=20java11.md-=E9=94=99=E5=88=AB?= =?UTF-8?q?=E5=AD=97=E4=BF=AE=E6=94=B9?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- docs/java/new-features/java11.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/java/new-features/java11.md b/docs/java/new-features/java11.md index f9d076c4b95..0f114047434 100644 --- a/docs/java/new-features/java11.md +++ b/docs/java/new-features/java11.md @@ -77,7 +77,7 @@ System.out.println(op.isEmpty());//判断指定的 Optional 对象是否为空 ZGC 主要为了满足如下目标进行设计: - GC 停顿时间不超过 10ms -- 即能处理几百 MB 的小堆,也能处理几个 TB 的大堆 +- 既能处理几百 MB 的小堆,也能处理几个 TB 的大堆 - 应用吞吐能力不会下降超过 15%(与 G1 回收算法相比) - 方便在此基础上引入新的 GC 特性和利用 colored 针以及 Load barriers 优化奠定基础 - 当前只支持 Linux/x64 位平台 From d2ce6448f5e4866cfec05c6da3c98f204b1d62ae Mon Sep 17 00:00:00 2001 From: JoeyChan Date: Sun, 16 Feb 2025 14:45:03 +0800 Subject: [PATCH 13/74] =?UTF-8?q?[doc=20perf]=E4=BC=98=E5=8C=96=E4=BA=8B?= =?UTF-8?q?=E5=8A=A1=E4=BC=A0=E6=92=AD=E8=A1=8C=E4=B8=BA=E4=B8=ADTransacti?= =?UTF-8?q?onDefinition.PROPAGATION=5FNESTED=E7=9A=84=E8=A7=A3=E9=87=8A?= =?UTF-8?q?=E8=AF=B4=E6=98=8E?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- docs/system-design/framework/spring/spring-transaction.md | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/docs/system-design/framework/spring/spring-transaction.md b/docs/system-design/framework/spring/spring-transaction.md index b294d6f6052..c9358ab2f9f 100644 --- a/docs/system-design/framework/spring/spring-transaction.md +++ b/docs/system-design/framework/spring/spring-transaction.md @@ -419,11 +419,16 @@ Class B { **3.`TransactionDefinition.PROPAGATION_NESTED`**: -如果当前存在事务,就在嵌套事务内执行;如果当前没有事务,就执行与`TransactionDefinition.PROPAGATION_REQUIRED`类似的操作。也就是说: +如果当前存在事务,则创建一个事务作为当前事务的嵌套事务执行; 如果当前没有事务,就执行与`TransactionDefinition.PROPAGATION_REQUIRED`类似的操作。也就是说: - 在外部方法开启事务的情况下,在内部开启一个新的事务,作为嵌套事务存在。 - 如果外部方法无事务,则单独开启一个事务,与 `PROPAGATION_REQUIRED` 类似。 +`TransactionDefinition.PROPAGATION_NESTED`代表的嵌套事务以父子关系呈现,其核心理念是子事务不会独立提交,依赖于父事务,在父事务中运行;当父事务提交时,子事务也会随着提交,理所当然的,当父事务回滚时,子事务也会回滚; +> 与`TransactionDefinition.PROPAGATION_REQUIRES_NEW`区别于:`PROPAGATION_REQUIRES_NEW`是独立事务,不依赖于外部事务,以平级关系呈现,执行完就会立即提交,与外部事务无关; + +子事务也有自己的特性,可以独立进行回滚,不会引发父事务的回滚,但是前提是需要处理子事务的异常,避免异常被父事务感知导致外部事务回滚; + 举个例子: - 如果 `aMethod()` 回滚的话,作为嵌套事务的`bMethod()`会回滚。 - 如果 `bMethod()` 回滚的话,`aMethod()`是否回滚,要看`bMethod()`的异常是否被处理: From 342526b2bc50e13f02b8c18001df116b080d8f37 Mon Sep 17 00:00:00 2001 From: HaiBooLang <53350622+HaiBooLang@users.noreply.github.com> Date: Sun, 16 Feb 2025 20:37:02 +0800 Subject: [PATCH 14/74] =?UTF-8?q?fix:=E7=A7=BB=E9=99=A4=E5=A4=9A=E4=BD=99?= =?UTF-8?q?=E6=A0=87=E7=82=B9=E7=AC=A6=E5=8F=B7?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- docs/cs-basics/network/other-network-questions.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/cs-basics/network/other-network-questions.md b/docs/cs-basics/network/other-network-questions.md index 2f3617dbe5b..24591862b69 100644 --- a/docs/cs-basics/network/other-network-questions.md +++ b/docs/cs-basics/network/other-network-questions.md @@ -194,7 +194,7 @@ HTTP 状态码用于描述 HTTP 请求的结果,比如 2xx 就代表请求被 ![HTTP/1.0 和 HTTP/1.1 对比](https://oss.javaguide.cn/github/javaguide/cs-basics/network/http1.1-vs-http2.0.png) -- **多路复用(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 则使用文本格式的报文。二进制帧更加紧凑和高效,减少了传输的数据量和带宽消耗。 - **头部压缩(Header Compression)**:HTTP/1.1 支持`Body`压缩,`Header`不支持压缩。HTTP/2.0 支持对`Header`压缩,使用了专门为`Header`压缩而设计的 HPACK 算法,减少了网络开销。 - **服务器推送(Server Push)**:HTTP/2.0 支持服务器推送,可以在客户端请求一个资源时,将其他相关资源一并推送给客户端,从而减少了客户端的请求次数和延迟。而 HTTP/1.1 需要客户端自己发送请求来获取相关资源。 From 9258fd29c3d83229d986fd8822f0933b43886449 Mon Sep 17 00:00:00 2001 From: HaiBooLang <53350622+HaiBooLang@users.noreply.github.com> Date: Mon, 17 Feb 2025 10:28:11 +0800 Subject: [PATCH 15/74] =?UTF-8?q?fix:=E4=BF=AE=E6=AD=A3=E6=A0=BC=E5=BC=8F?= =?UTF-8?q?=E9=94=99=E8=AF=AF?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- docs/cs-basics/network/nat.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/cs-basics/network/nat.md b/docs/cs-basics/network/nat.md index 5634ba07387..788a454ebd4 100644 --- a/docs/cs-basics/network/nat.md +++ b/docs/cs-basics/network/nat.md @@ -55,6 +55,6 @@ SOHO 子网的“代理人”,也就是和外界的窗口,通常由路由器 3. WAN 的 ISP 变更接口地址时,无需通告 LAN 内主机。 4. LAN 主机对 WAN 不可见,不可直接寻址,可以保证一定程度的安全性。 -然而,NAT 协议由于其独特性,存在着一些争议。比如,可能你已经注意到了,**NAT 协议在 LAN 以外,标识一个内部主机时,使用的是端口号,因为 IP 地址都是相同的。**这种将端口号作为主机寻址的行为,可能会引发一些误会。此外,路由器作为网络层的设备,修改了传输层的分组内容(修改了源 IP 地址和端口号),同样是不规范的行为。但是,尽管如此,NAT 协议作为 IPv4 时代的产物,极大地方便了一些本来棘手的问题,一直被沿用至今。 +然而,NAT 协议由于其独特性,存在着一些争议。比如,可能你已经注意到了,**NAT 协议在 LAN 以外,标识一个内部主机时,使用的是端口号,因为 IP 地址都是相同的**。这种将端口号作为主机寻址的行为,可能会引发一些误会。此外,路由器作为网络层的设备,修改了传输层的分组内容(修改了源 IP 地址和端口号),同样是不规范的行为。但是,尽管如此,NAT 协议作为 IPv4 时代的产物,极大地方便了一些本来棘手的问题,一直被沿用至今。 From 89d9ca7504faea5970c7503c652e5210bf77719d Mon Sep 17 00:00:00 2001 From: HaiBooLang <53350622+HaiBooLang@users.noreply.github.com> Date: Mon, 17 Feb 2025 10:29:47 +0800 Subject: [PATCH 16/74] =?UTF-8?q?fix:=E4=BF=AE=E6=AD=A3=E6=A0=BC=E5=BC=8F?= =?UTF-8?q?=E9=94=99=E8=AF=AF?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- docs/cs-basics/network/nat.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/cs-basics/network/nat.md b/docs/cs-basics/network/nat.md index 788a454ebd4..4567719b81e 100644 --- a/docs/cs-basics/network/nat.md +++ b/docs/cs-basics/network/nat.md @@ -45,7 +45,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 协议的特点,有以下几点: From 8d0221f701c89a75bef341d8ed5648bcc556a883 Mon Sep 17 00:00:00 2001 From: HaiBooLang <53350622+HaiBooLang@users.noreply.github.com> Date: Mon, 17 Feb 2025 19:38:46 +0800 Subject: [PATCH 17/74] =?UTF-8?q?fix:=E4=BF=AE=E6=AD=A3=E6=A0=87=E7=82=B9?= =?UTF-8?q?=E7=AC=A6=E5=8F=B7?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- docs/cs-basics/network/application-layer-protocol.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/cs-basics/network/application-layer-protocol.md b/docs/cs-basics/network/application-layer-protocol.md index 5764a72c020..cb809b9157d 100644 --- a/docs/cs-basics/network/application-layer-protocol.md +++ b/docs/cs-basics/network/application-layer-protocol.md @@ -15,7 +15,7 @@ HTTP 使用客户端-服务器模型,客户端向服务器发送 HTTP Request HTTP 协议基于 TCP 协议,发送 HTTP 请求之前首先要建立 TCP 连接也就是要经历 3 次握手。目前使用的 HTTP 协议大部分都是 1.1。在 1.1 的协议里面,默认是开启了 Keep-Alive 的,这样的话建立的连接就可以在多次请求中被复用了。 -另外, HTTP 协议是”无状态”的协议,它无法记录客户端用户的状态,一般我们都是通过 Session 来记录客户端用户的状态。 +另外, HTTP 协议是“无状态”的协议,它无法记录客户端用户的状态,一般我们都是通过 Session 来记录客户端用户的状态。 ## Websocket:全双工通信协议 From 34eb4d644a61c63e1fde5e9d162946e8218df4a3 Mon Sep 17 00:00:00 2001 From: MonsterFan <57214513+MonsterFanSec@users.noreply.github.com> Date: Fri, 21 Feb 2025 14:59:16 +0800 Subject: [PATCH 18/74] Update io-basis.md MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 笔误 - Java IO 基础知识总结 - BufferedOutputStream(字节缓冲输出流) BufferedOutputStream(字节缓冲输出流)这部分的描述,最后是“提高了读取效率”,这里应该是提高输出或者写出的效率吧? 解决方案:删除了“读取” --- docs/java/io/io-basis.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/docs/java/io/io-basis.md b/docs/java/io/io-basis.md index a2e6f21db9e..1ea1bcd3f86 100755 --- a/docs/java/io/io-basis.md +++ b/docs/java/io/io-basis.md @@ -20,7 +20,7 @@ Java IO 流的 40 多个类都是从如下 4 个抽象类基类中派生出来 ## 字节流 ### InputStream(字节输入流) - + `InputStream`用于从源头(通常是文件)读取数据(字节信息)到内存中,`java.io.InputStream`抽象类是所有字节输入流的父类。 `InputStream` 常用方法: @@ -430,7 +430,7 @@ class BufferedInputStream extends FilterInputStream { ### BufferedOutputStream(字节缓冲输出流) -`BufferedOutputStream` 将数据(字节信息)写入到目的地(通常是文件)的过程中不会一个字节一个字节的写入,而是会先将要写入的字节存放在缓存区,并从内部缓冲区中单独写入字节。这样大幅减少了 IO 次数,提高了读取效率 +`BufferedOutputStream` 将数据(字节信息)写入到目的地(通常是文件)的过程中不会一个字节一个字节的写入,而是会先将要写入的字节存放在缓存区,并从内部缓冲区中单独写入字节。这样大幅减少了 IO 次数,提高了效率 ```java try (BufferedOutputStream bos = new BufferedOutputStream(new FileOutputStream("output.txt"))) { From 18ea39f934a82692ec4e4cbec26aa440cd0f2d98 Mon Sep 17 00:00:00 2001 From: wayne <455234037@qq.com> Date: Sat, 22 Feb 2025 14:42:44 +0800 Subject: [PATCH 19/74] =?UTF-8?q?add:=20=E5=A2=9E=E5=8A=A0=20Future=20?= =?UTF-8?q?=E7=9A=84=E6=BA=90=E7=A0=81=E5=8F=82=E8=80=83=E6=96=87=E7=AB=A0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- docs/java/concurrent/java-concurrent-questions-03.md | 1 + 1 file changed, 1 insertion(+) diff --git a/docs/java/concurrent/java-concurrent-questions-03.md b/docs/java/concurrent/java-concurrent-questions-03.md index 5b1b0e217dd..de312bcf96f 100644 --- a/docs/java/concurrent/java-concurrent-questions-03.md +++ b/docs/java/concurrent/java-concurrent-questions-03.md @@ -885,6 +885,7 @@ public FutureTask(Runnable runnable, V result) { `FutureTask`相当于对`Callable` 进行了封装,管理着任务执行的情况,存储了 `Callable` 的 `call` 方法的任务执行结果。 +关于更多 `Future` 的源码细节,可以肝这篇万字解析,写的很清楚:[Java是如何实现Future模式的?万字详解!](https://juejin.cn/post/6844904199625375757)。 ### CompletableFuture 类有什么用? `Future` 在实际使用过程中存在一些局限性比如不支持异步任务的编排组合、获取计算结果的 `get()` 方法为阻塞调用。 From ffcccdca0d4b0ba0f8777b04e508260ef30814d0 Mon Sep 17 00:00:00 2001 From: Guide Date: Sat, 22 Feb 2025 15:35:59 +0800 Subject: [PATCH 20/74] =?UTF-8?q?[dcos=20update]=E4=BF=AE=E6=AD=A3?= =?UTF-8?q?=E7=BA=BF=E7=A8=8B=E6=B1=A0=E5=86=85=E5=AE=B9=E9=83=A8=E5=88=86?= =?UTF-8?q?=E6=8F=8F=E8=BF=B0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../data-structure/linear-data-structure.md | 6 +++--- docs/database/redis/redis-questions-01.md | 10 ++++++++++ docs/java/basis/java-basic-questions-03.md | 8 -------- .../java/concurrent/java-concurrent-questions-03.md | 13 ++++++------- .../concurrent/java-thread-pool-best-practices.md | 2 +- docs/java/concurrent/java-thread-pool-summary.md | 8 +++----- 6 files changed, 23 insertions(+), 24 deletions(-) diff --git a/docs/cs-basics/data-structure/linear-data-structure.md b/docs/cs-basics/data-structure/linear-data-structure.md index cc5cc6a5db2..e8ae63a19d5 100644 --- a/docs/cs-basics/data-structure/linear-data-structure.md +++ b/docs/cs-basics/data-structure/linear-data-structure.md @@ -326,9 +326,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/database/redis/redis-questions-01.md b/docs/database/redis/redis-questions-01.md index 3bed4651c32..e553f773c43 100644 --- a/docs/database/redis/redis-questions-01.md +++ b/docs/database/redis/redis-questions-01.md @@ -114,6 +114,16 @@ PS:篇幅问题,我这并没有对上面提到的分布式缓存选型做详 Redis 除了可以用作缓存之外,还可以用于分布式锁、限流、消息队列、延时队列等场景,功能强大! +### 为什么用 Redis 而不用本地缓存呢? + +| 特性 | 本地缓存 | Redis | +| ------------ | ------------------------------------ | -------------------------------- | +| 数据一致性 | 多服务器部署时存在数据不一致问题 | 数据一致 | +| 内存限制 | 受限于单台服务器内存 | 独立部署,内存空间更大 | +| 数据丢失风险 | 服务器宕机数据丢失 | 可持久化,数据不易丢失 | +| 管理维护 | 分散,管理不便 | 集中管理,提供丰富的管理工具 | +| 功能丰富性 | 功能有限,通常只提供简单的键值对存储 | 功能丰富,支持多种数据结构和功能 | + ### 常见的缓存读写策略有哪些? 关于常见的缓存读写策略的详细介绍,可以看我写的这篇文章:[3 种常用的缓存读写策略详解](https://javaguide.cn/database/redis/3-commonly-used-cache-read-and-write-strategies.html) 。 diff --git a/docs/java/basis/java-basic-questions-03.md b/docs/java/basis/java-basic-questions-03.md index af8f2274054..7bc956f78e8 100644 --- a/docs/java/basis/java-basic-questions-03.md +++ b/docs/java/basis/java-basic-questions-03.md @@ -89,14 +89,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 diff --git a/docs/java/concurrent/java-concurrent-questions-03.md b/docs/java/concurrent/java-concurrent-questions-03.md index de312bcf96f..9609d4d9aeb 100644 --- a/docs/java/concurrent/java-concurrent-questions-03.md +++ b/docs/java/concurrent/java-concurrent-questions-03.md @@ -276,23 +276,21 @@ TTL 改造的地方有两处: 另外,《阿里巴巴 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())); } @@ -886,6 +884,7 @@ public FutureTask(Runnable runnable, V result) { `FutureTask`相当于对`Callable` 进行了封装,管理着任务执行的情况,存储了 `Callable` 的 `call` 方法的任务执行结果。 关于更多 `Future` 的源码细节,可以肝这篇万字解析,写的很清楚:[Java是如何实现Future模式的?万字详解!](https://juejin.cn/post/6844904199625375757)。 + ### CompletableFuture 类有什么用? `Future` 在实际使用过程中存在一些局限性比如不支持异步任务的编排组合、获取计算结果的 `get()` 方法为阻塞调用。 diff --git a/docs/java/concurrent/java-thread-pool-best-practices.md b/docs/java/concurrent/java-thread-pool-best-practices.md index 5763e0268e7..04154bfa378 100644 --- a/docs/java/concurrent/java-thread-pool-best-practices.md +++ b/docs/java/concurrent/java-thread-pool-best-practices.md @@ -13,7 +13,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。 diff --git a/docs/java/concurrent/java-thread-pool-summary.md b/docs/java/concurrent/java-thread-pool-summary.md index ec1ae249ab0..48fb23d9db7 100644 --- a/docs/java/concurrent/java-thread-pool-summary.md +++ b/docs/java/concurrent/java-thread-pool-summary.md @@ -183,21 +183,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())); } From 90dc871ea873def18b9c789b3fd576ec12f635c2 Mon Sep 17 00:00:00 2001 From: flying pig <117554874+flying-pig-z@users.noreply.github.com> Date: Sun, 23 Feb 2025 16:49:11 +0800 Subject: [PATCH 21/74] =?UTF-8?q?=E5=88=A0=E9=99=A4RocketMQ=E6=96=87?= =?UTF-8?q?=E7=AB=A0=E4=B8=AD=E7=9A=84=E5=A4=9A=E4=BD=99=E5=AD=97=E7=AC=A6?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- docs/high-performance/message-queue/rocketmq-questions.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/high-performance/message-queue/rocketmq-questions.md b/docs/high-performance/message-queue/rocketmq-questions.md index 35f24726812..9591e5d2612 100644 --- a/docs/high-performance/message-queue/rocketmq-questions.md +++ b/docs/high-performance/message-queue/rocketmq-questions.md @@ -19,7 +19,7 @@ tag: ### 消息队列为什么会出现? -消息队``列算是作为后端程序员的一个必备技能吧,因为**分布式应用必定涉及到各个系统之间的通信问题**,这个时候消息队列也应运而生了。可以说分布式的产生是消息队列的基础,而分布式怕是一个很古老的概念了吧,所以消息队列也是一个很古老的中间件了。 +消息队列算是作为后端程序员的一个必备技能吧,因为**分布式应用必定涉及到各个系统之间的通信问题**,这个时候消息队列也应运而生了。可以说分布式的产生是消息队列的基础,而分布式怕是一个很古老的概念了吧,所以消息队列也是一个很古老的中间件了。 ### 消息队列能用来干什么? From 1b701c44d2992340cece0c479a6cedc1e5206066 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E8=B0=AD=E4=B9=9D=E9=BC=8E?= <109224573@qq.com> Date: Sun, 23 Feb 2025 20:56:18 +0800 Subject: [PATCH 22/74] fix md style --- docs/tools/docker/docker-intro.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/tools/docker/docker-intro.md b/docs/tools/docker/docker-intro.md index 60270429bfb..5db4f557784 100644 --- a/docs/tools/docker/docker-intro.md +++ b/docs/tools/docker/docker-intro.md @@ -83,7 +83,7 @@ tag: **Docker 思想**: - **集装箱**:就像海运中的集装箱一样,Docker 容器包含了应用程序及其所有依赖项,确保在任何环境中都能以相同的方式运行。 -- **标准化:**运输方式、存储方式、API 接口。 +- **标准化**:运输方式、存储方式、API 接口。 - **隔离**:每个 Docker 容器都在自己的隔离环境中运行,与宿主机和其他容器隔离。 ### Docker 容器的特点 From 5469d28feb0ed3690349a5b538390c32634b3280 Mon Sep 17 00:00:00 2001 From: XiangdongHe <81752066+XiangdongHe@users.noreply.github.com> Date: Mon, 24 Feb 2025 15:00:50 +0800 Subject: [PATCH 23/74] fix: Modify an unimportant typo, Update redis-questions-02.md Modify an unimportant typo --- docs/database/redis/redis-questions-02.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/database/redis/redis-questions-02.md b/docs/database/redis/redis-questions-02.md index 6ec76ff2c8c..9eec2d038fc 100644 --- a/docs/database/redis/redis-questions-02.md +++ b/docs/database/redis/redis-questions-02.md @@ -227,7 +227,7 @@ 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 这部分的内容,感兴趣地可以看看。 From 4071270cf5ac72b085d6fa2331e78fd344777a62 Mon Sep 17 00:00:00 2001 From: xiaodongxu Date: Mon, 24 Feb 2025 15:21:24 +0800 Subject: [PATCH 24/74] =?UTF-8?q?=E4=BF=AE=E5=A4=8D=E5=8D=95=E8=AF=8D?= =?UTF-8?q?=E9=94=99=E8=AF=AF,=E7=97=85=E5=8F=A5?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- docs/java/concurrent/aqs.md | 2 +- docs/java/concurrent/completablefuture-intro.md | 2 +- docs/java/concurrent/java-concurrent-questions-03.md | 4 ++-- 3 files changed, 4 insertions(+), 4 deletions(-) diff --git a/docs/java/concurrent/aqs.md b/docs/java/concurrent/aqs.md index 81d567ccf18..c8e079d1a51 100644 --- a/docs/java/concurrent/aqs.md +++ b/docs/java/concurrent/aqs.md @@ -1324,7 +1324,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/completablefuture-intro.md b/docs/java/concurrent/completablefuture-intro.md index b0a0cf987e6..be21c70e1c7 100644 --- a/docs/java/concurrent/completablefuture-intro.md +++ b/docs/java/concurrent/completablefuture-intro.md @@ -65,7 +65,7 @@ public interface Future { ## CompletableFuture 介绍 -`Future` 在实际使用过程中存在一些局限性比如不支持异步任务的编排组合、获取计算结果的 `get()` 方法为阻塞调用。 +`Future` 在实际使用过程中存在一些局限性,比如不支持异步任务的编排组合、获取计算结果的 `get()` 方法为阻塞调用。 Java 8 才被引入`CompletableFuture` 类可以解决`Future` 的这些缺陷。`CompletableFuture` 除了提供了更为好用和强大的 `Future` 特性之外,还提供了函数式编程、异步任务编排组合(可以将多个异步任务串联起来,组成一个完整的链式调用)等能力。 diff --git a/docs/java/concurrent/java-concurrent-questions-03.md b/docs/java/concurrent/java-concurrent-questions-03.md index 9609d4d9aeb..7a2b5436d4a 100644 --- a/docs/java/concurrent/java-concurrent-questions-03.md +++ b/docs/java/concurrent/java-concurrent-questions-03.md @@ -887,7 +887,7 @@ public FutureTask(Runnable runnable, V result) { ### CompletableFuture 类有什么用? -`Future` 在实际使用过程中存在一些局限性比如不支持异步任务的编排组合、获取计算结果的 `get()` 方法为阻塞调用。 +`Future` 在实际使用过程中存在一些局限性,比如不支持异步任务的编排组合、获取计算结果的 `get()` 方法为阻塞调用。 Java 8 才被引入`CompletableFuture` 类可以解决`Future` 的这些缺陷。`CompletableFuture` 除了提供了更为好用和强大的 `Future` 特性之外,还提供了函数式编程、异步任务编排组合(可以将多个异步任务串联起来,组成一个完整的链式调用)等能力。 @@ -1217,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)。它要做的事情是:让一组线程到达一个屏障(也可以叫同步点)时被阻塞,直到最后一个线程到达屏障时,屏障才会开门,所有被屏障拦截的线程才会继续干活。 From 878ca428c126af480526738fdd6821a06f6242d6 Mon Sep 17 00:00:00 2001 From: XiangdongHe <81752066+XiangdongHe@users.noreply.github.com> Date: Mon, 24 Feb 2025 17:23:42 +0800 Subject: [PATCH 25/74] =?UTF-8?q?fix:=20=E4=BF=AE=E6=94=B9=E4=B8=80?= =?UTF-8?q?=E5=A4=84=E8=A1=A8=E8=BF=B0=E4=B8=8D=E6=B8=85=E6=99=B0=E7=9A=84?= =?UTF-8?q?=E6=96=87=E5=AD=97=E4=BB=8B=E7=BB=8D=EF=BC=8CUpdate=20redis-del?= =?UTF-8?q?ayed-task.md?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 既然是要找到定时过期的任务,肯定需要有定期扫描的,既然消费者没有轮询,那应该就是RDelayedQueue使用zrangebyscore命令定期扫描已经过期的任务。 --- docs/database/redis/redis-delayed-task.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/database/redis/redis-delayed-task.md b/docs/database/redis/redis-delayed-task.md index 52f9c84cf78..35f9304321f 100644 --- a/docs/database/redis/redis-delayed-task.md +++ b/docs/database/redis/redis-delayed-task.md @@ -72,7 +72,7 @@ Redisson 是一个开源的 Java 语言 Redis 客户端,提供了很多开箱 Redisson 的延迟队列 RDelayedQueue 是基于 Redis 的 SortedSet 来实现的。SortedSet 是一个有序集合,其中的每个元素都可以设置一个分数,代表该元素的权重。Redisson 利用这一特性,将需要延迟执行的任务插入到 SortedSet 中,并给它们设置相应的过期时间作为分数。 -Redisson 使用 `zrangebyscore` 命令扫描 SortedSet 中过期的元素,然后将这些过期元素从 SortedSet 中移除,并将它们加入到就绪消息列表中。就绪消息列表是一个阻塞队列,有消息进入就会被监听到。这样做可以避免对整个 SortedSet 进行轮询,提高了执行效率。 +Redisson 定期使用 `zrangebyscore` 命令扫描 SortedSet 中过期的元素,然后将这些过期元素从 SortedSet 中移除,并将它们加入到就绪消息列表中。就绪消息列表是一个阻塞队列,有消息进入就会被消费者监听到。这样做可以避免消费者对整个 SortedSet 进行轮询,提高了执行效率。 相比于 Redis 过期事件监听实现延时任务功能,这种方式具备下面这些优势: From 68a8acfc39297c7bc116229d554215d0275b0ca2 Mon Sep 17 00:00:00 2001 From: houfm <113281428+houfm@users.noreply.github.com> Date: Tue, 25 Feb 2025 00:52:34 +0800 Subject: [PATCH 26/74] =?UTF-8?q?fix:=20=E4=BF=AE=E6=AD=A3=E6=A0=87?= =?UTF-8?q?=E7=82=B9=E7=AC=A6=E5=8F=B7?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- docs/java/basis/why-there-only-value-passing-in-java.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) 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..dc329cd32b6 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 @@ -34,7 +34,7 @@ void sayHello(String str) { - **值传递**:方法接收的是实参值的拷贝,会创建副本。 - **引用传递**:方法接收的直接是实参所引用的对象在堆中的地址,不会创建副本,对形参的修改将影响到实参。 -很多程序设计语言(比如 C++、 Pascal )提供了两种参数传递的方式,不过,在 Java 中只有值传递。 +很多程序设计语言(比如 C++、 Pascal)提供了两种参数传递的方式,不过,在 Java 中只有值传递。 ## 为什么 Java 只有值传递? From a2b71a0ef2ab2be6ea90ed3024a664b16477273d Mon Sep 17 00:00:00 2001 From: hulingfeng <1405559058@qq.com> Date: Thu, 27 Feb 2025 10:44:47 +0800 Subject: [PATCH 27/74] =?UTF-8?q?fix:=20=E4=BF=AE=E6=AD=A3=E4=B8=80?= =?UTF-8?q?=E5=A4=84=E8=A1=A8=E8=BF=B0=E4=B8=8D=E6=B8=85=E7=9A=84JVM?= =?UTF-8?q?=E5=8F=82=E6=95=B0=E5=AE=9E=E4=BE=8B?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- docs/java/jvm/jvm-parameters-intro.md | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/docs/java/jvm/jvm-parameters-intro.md b/docs/java/jvm/jvm-parameters-intro.md index 1de1505ac8a..ec357e6e0d8 100644 --- a/docs/java/jvm/jvm-parameters-intro.md +++ b/docs/java/jvm/jvm-parameters-intro.md @@ -6,7 +6,8 @@ tag: --- > 本文由 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 ## 1.概述 @@ -71,10 +72,10 @@ GC 调优策略中很重要的一条经验总结是这样说的: 另外,你还可以通过 **`-XX:NewRatio=`** 来设置老年代与新生代内存的比值。 -比如下面的参数就是设置老年代与新生代内存的比值为 1。也就是说老年代和新生代所占比值为 1:1,新生代占整个堆栈的 1/2。 +比如下面的参数就是设置新生代与老年代内存的比值为 2(默认值)。也就是说 young/old 所占比值为 2:1,新生代占整个堆栈的 2/3。 ```plain --XX:NewRatio=1 +-XX:NewRatio=2 ``` ### 2.3.显式指定永久代/元空间的大小 From 8f6d7e9d6d833445ac935e0b20018d45aa9d5bdd Mon Sep 17 00:00:00 2001 From: hulingfeng <1405559058@qq.com> Date: Thu, 27 Feb 2025 10:45:58 +0800 Subject: [PATCH 28/74] =?UTF-8?q?fix:=20=E4=BF=AE=E6=AD=A3=E4=B8=80?= =?UTF-8?q?=E5=A4=84=E8=A1=A8=E8=BF=B0=E4=B8=8D=E6=B8=85=E7=9A=84JVM?= =?UTF-8?q?=E5=8F=82=E6=95=B0=E5=AE=9E=E4=BE=8B?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- docs/java/jvm/jvm-parameters-intro.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/java/jvm/jvm-parameters-intro.md b/docs/java/jvm/jvm-parameters-intro.md index ec357e6e0d8..7b460b1277b 100644 --- a/docs/java/jvm/jvm-parameters-intro.md +++ b/docs/java/jvm/jvm-parameters-intro.md @@ -72,7 +72,7 @@ GC 调优策略中很重要的一条经验总结是这样说的: 另外,你还可以通过 **`-XX:NewRatio=`** 来设置老年代与新生代内存的比值。 -比如下面的参数就是设置新生代与老年代内存的比值为 2(默认值)。也就是说 young/old 所占比值为 2:1,新生代占整个堆栈的 2/3。 +比如下面的参数就是设置新生代与老年代内存的比值为 2(默认值)。也就是说 young/old 所占比值为 2,新生代占整个堆栈的 2/3。 ```plain -XX:NewRatio=2 From 374027a9ba3972241e193c96ab4e702ac881bf07 Mon Sep 17 00:00:00 2001 From: xuqi Date: Fri, 28 Feb 2025 22:31:57 +0800 Subject: [PATCH 29/74] =?UTF-8?q?1=E3=80=81=E8=AF=8D=E5=8F=A5=E5=8B=98?= =?UTF-8?q?=E8=AF=AF=E5=92=8C=E8=B0=83=E6=95=B4=EF=BC=9B=202=E3=80=81?= =?UTF-8?q?=E6=A0=87=E7=82=B9=E7=AC=A6=E5=8F=B7=E5=8B=98=E8=AF=AF=E5=92=8C?= =?UTF-8?q?=E8=B0=83=E6=95=B4=E3=80=82?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- ...imization-specification-recommendations.md | 168 +++++++++--------- 1 file changed, 84 insertions(+), 84 deletions(-) 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 1fe8ea3c788..93c2544cd33 100644 --- a/docs/database/mysql/mysql-high-performance-optimization-specification-recommendations.md +++ b/docs/database/mysql/mysql-high-performance-optimization-specification-recommendations.md @@ -11,11 +11,11 @@ tag: ## 数据库命名规范 -- 所有数据库对象名称必须使用小写字母并用下划线分割 -- 所有数据库对象名称禁止使用 MySQL 保留关键字(如果表名中包含关键字查询时,需要将其用单引号括起来) -- 数据库对象的命名要能做到见名识意,并且最后不要超过 32 个字符 -- 临时库表必须以 `tmp_` 为前缀并以日期为后缀,备份表必须以 `bak_` 为前缀并以日期 (时间戳) 为后缀 -- 所有存储相同数据的列名和列类型必须一致(一般作为关联列,如果查询时关联列类型不一致会自动进行数据类型隐式转换,会造成列上的索引失效,导致查询效率降低) +- 所有数据库对象名称必须使用小写字母并用下划线分割。 +- 所有数据库对象名称禁止使用 MySQL 保留关键字(如果表名中包含关键字查询时,需要将其用单引号括起来)。 +- 数据库对象的命名要能做到见名识义,并且最好不要超过 32 个字符。 +- 临时库表必须以 `tmp_` 为前缀并以日期为后缀,备份表必须以 `bak_` 为前缀并以日期 (时间戳) 为后缀。 +- 所有存储相同数据的列名和列类型必须一致(一般作为关联列,如果查询时关联列类型不一致会自动进行数据类型隐式转换,会造成列上的索引失效,导致查询效率降低)。 ## 数据库基本设计规范 @@ -33,19 +33,19 @@ InnoDB 支持事务,支持行级锁,更好的恢复性,高并发下性能 ### 所有表和字段都需要添加注释 -使用 comment 从句添加表和列的备注,从一开始就进行数据字典的维护 +使用 comment 从句添加表和列的备注,从一开始就进行数据字典的维护。 ### 尽量控制单表数据量的大小,建议控制在 500 万以内 500 万并不是 MySQL 数据库的限制,过大会造成修改表结构,备份,恢复都会有很大的问题。 -可以用历史数据归档(应用于日志数据),分库分表(应用于业务数据)等手段来控制数据量大小 +可以用历史数据归档(应用于日志数据),分库分表(应用于业务数据)等手段来控制数据量大小。 ### 谨慎使用 MySQL 分区表 -分区表在物理上表现为多个文件,在逻辑上表现为一个表; +分区表在物理上表现为多个文件,在逻辑上表现为一个表。 -谨慎选择分区键,跨分区查询效率可能更低; +谨慎选择分区键,跨分区查询效率可能更低。 建议采用物理分表的方式管理大数据。 @@ -71,7 +71,7 @@ InnoDB 支持事务,支持行级锁,更好的恢复性,高并发下性能 ### 禁止在线上做数据库压力测试 -### 禁止从开发环境,测试环境直接连接生产环境数据库 +### 禁止从开发环境、测试环境直接连接生产环境数据库 安全隐患极大,要对生产环境抱有敬畏之心! @@ -79,22 +79,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 +103,7 @@ UNSIGNED INT 0~4294967295 **c.小数值类型(比如年龄、状态表示如 0/1)优先使用 TINYINT 类型。** -### 避免使用 TEXT,BLOB 数据类型,最常见的 TEXT 类型可以存储 64k 的数据 +### 避免使用 TEXT、BLOB 数据类型,最常见的 TEXT 类型可以存储 64k 的数据 **a. 建议把 BLOB 或是 TEXT 列分离到单独的扩展表中。** @@ -113,30 +113,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 +148,10 @@ MySQL 时间类型选择的详细介绍请看这篇:[MySQL 时间类型数据 ### 同财务相关的金额类数据必须使用 decimal 类型 -- **非精准浮点**:float,double +- **非精准浮点**:float、double - **精准浮点**:decimal -decimal 类型为精准浮点数,在计算时不会丢失精度。占用空间由定义的宽度决定,每 4 个字节可以存储 9 位数字,并且小数点要占用一个字节。并且,decimal 可用于存储比 bigint 更大的整型数据 +decimal 类型为精准浮点数,在计算时不会丢失精度。占用空间由定义的宽度决定,每 4 个字节可以存储 9 位数字,并且小数点要占用一个字节。并且,decimal 可用于存储比 bigint 更大的整型数据。 不过, 由于 decimal 需要额外的空间和计算开销,应该尽量只在需要对数据进行精确计算时才使用 decimal 。 @@ -161,13 +161,13 @@ decimal 类型为精准浮点数,在计算时不会丢失精度。占用空间 ## 索引设计规范 -### 限制每张表上的索引数量,建议单张表索引不超过 5 个 +### 限制每张表上的索引数量,建议单张表索引不超过 5 个 -索引并不是越多越好!索引可以提高效率同样可以降低效率。 +索引并不是越多越好!索引可以提高效率,同样可以降低效率。 索引可以增加查询效率,但同样也会降低插入和更新的效率,甚至有些情况下会降低查询效率。 -因为 MySQL 优化器在选择如何优化查询时,会根据统一信息,对每一个可以用到的索引来进行评估,以生成出一个最好的执行计划,如果同时有很多个索引都可以用于查询,就会增加 MySQL 优化器生成执行计划的时间,同样会降低查询性能。 +因为 MySQL 优化器在选择如何优化查询时,会根据统一信息,对每一个可以用到的索引来进行评估,以生成出一个最好的执行计划。如果同时有很多个索引都可以用于查询,就会增加 MySQL 优化器生成执行计划的时间,同样会降低查询性能。 ### 禁止使用全文索引 @@ -175,46 +175,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,增加查询性能,索引能过滤出越少的数据,则从磁盘中读入的数据也就越少。 -- **区分度最高的列放在联合索引的最左侧:** 这是最重要的原则。区分度越高,通过索引筛选出的数据就越少,I/O 操作也就越少。计算区分度的方法是 `count(distinct column) / count(*)`。 -- **最频繁使用的列放在联合索引的左侧:** 这符合最左前缀匹配原则。将最常用的查询条件列放在最左侧,可以最大程度地利用索引。 -- **字段长度:** 字段长度对联合索引非叶子节点的影响很小,因为它存储了所有联合索引字段的值。字段长度主要影响主键和包含在其他索引中的字段的存储空间,以及这些索引的叶子节点的大小。因此,在选择联合索引列的顺序时,字段长度的优先级最低。 对于主键和包含在其他索引中的字段,选择较短的字段长度可以节省存储空间和提高 I/O 性能。 +- **区分度最高的列放在联合索引的最左侧:** 这是最重要的原则。区分度越高,通过索引筛选出的数据就越少,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 +222,9 @@ InnoDB 是按照主键索引的顺序来组织表的 **尽量避免使用外键约束** -- 不建议使用外键约束(foreign key),但一定要在表与表之间的关联键上建立索引 -- 外键可用于保证数据的参照完整性,但建议在业务端实现 -- 外键会影响父表和子表的写操作从而降低性能 +- 不建议使用外键约束(foreign key),但一定要在表与表之间的关联键上建立索引。 +- 外键可用于保证数据的参照完整性,但建议在业务端实现。 +- 外键会影响父表和子表的写操作从而降低性能。 ## 数据库 SQL 开发规范 @@ -238,7 +238,7 @@ InnoDB 是按照主键索引的顺序来组织表的 ### 充分利用表上已经存在的索引 -避免使用双%号的查询条件。如:`a like '%123%'`,(如果无前置%,只有后置%,是可以用到列上的索引的) +避免使用双%号的查询条件。如:`a like '%123%'`(如果无前置%,只有后置%,是可以用到列上的索引的)。 一个 SQL 只能利用到复合索引中的一列进行范围查询。如:有 a,b,c 列的联合索引,在查询条件中有 a 列的范围查询,则在 b,c 列上的索引将不会被用到。 @@ -248,18 +248,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 +273,7 @@ insert into t(c1,c2,c3) values ('a','b','c'); ### 避免数据类型的隐式转换 -隐式转换会导致索引失效如: +隐式转换会导致索引失效,如: ```sql select name,phone from customer where id = '111'; @@ -283,7 +283,7 @@ 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 资源,产生大量的慢查询。 @@ -293,7 +293,7 @@ select name,phone from customer where id = '111'; 在 MySQL 中,对于同一个 SQL 多关联(join)一个表,就会多分配一个关联缓存,如果在一个 SQL 中关联的表越多,所占用的内存也就越大。 -如果程序中大量的使用了多表关联的操作,同时 join_buffer_size 设置的也不合理的情况下,就容易造成服务器内存溢出的情况,就会影响到服务器数据库性能的稳定性。 +如果程序中大量地使用了多表关联的操作,同时 join_buffer_size 设置得也不合理,就容易造成服务器内存溢出的情况,就会影响到服务器数据库性能的稳定性。 同时对于关联操作来说,会产生临时表操作,影响查询效率,MySQL 最多允许关联 61 个表,建议不超过 5 个。 @@ -303,17 +303,17 @@ 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 从句中禁止对列进行函数转换和计算 -对列进行函数转换或计算时会导致无法使用索引 +对列进行函数转换或计算时会导致无法使用索引。 **不推荐:** @@ -329,43 +329,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 +373,13 @@ pt-online-schema-change 它会首先建立一个与原表结构相同的新表 ### 禁止为程序使用的账号赋予 super 权限 -- 当达到最大连接数限制时,还运行 1 个有 super 权限的用户连接 -- super 权限只能留给 DBA 处理问题的账号使用 +- 当达到最大连接数限制时,还运行 1 个有 super 权限的用户连接。 +- super 权限只能留给 DBA 处理问题的账号使用。 -### 对于程序连接数据库账号,遵循权限最小原则 +### 对于程序连接数据库账号,遵循权限最小原则 -- 程序使用数据库账号只能在一个 DB 下使用,不准跨库 -- 程序使用的账号原则上不准有 drop 权限 +- 程序使用数据库账号只能在一个 DB 下使用,不准跨库。 +- 程序使用的账号原则上不准有 drop 权限。 ## 推荐阅读 From f9eb32f6234724452f2a0b70f29841f034d873a9 Mon Sep 17 00:00:00 2001 From: xuqi Date: Sat, 1 Mar 2025 00:57:27 +0800 Subject: [PATCH 30/74] =?UTF-8?q?MySQL=E9=AB=98=E6=80=A7=E8=83=BD=E4=BC=98?= =?UTF-8?q?=E5=8C=96=E8=A7=84=E8=8C=83=E5=BB=BA=E8=AE=AE=E6=80=BB=E7=BB=93?= =?UTF-8?q?=201=E3=80=81=E4=B8=93=E4=B8=9A=E6=9C=AF=E8=AF=AD=E5=90=8D?= =?UTF-8?q?=E7=A7=B0=E8=B0=83=E6=95=B4=E3=80=82?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit MySQL索引详解 1、词句勘误和调整; 2、标点符号勘误和调整。 --- ...imization-specification-recommendations.md | 2 +- docs/database/mysql/mysql-index.md | 160 +++++++++--------- 2 files changed, 81 insertions(+), 81 deletions(-) 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 93c2544cd33..af0c387be3e 100644 --- a/docs/database/mysql/mysql-high-performance-optimization-specification-recommendations.md +++ b/docs/database/mysql/mysql-high-performance-optimization-specification-recommendations.md @@ -21,7 +21,7 @@ tag: ### 所有表必须使用 InnoDB 存储引擎 -没有特殊要求(即 InnoDB 无法满足的功能如:列存储,存储空间数据等)的情况下,所有表必须使用 InnoDB 存储引擎(MySQL5.5 之前默认使用 Myisam,5.6 以后默认的为 InnoDB)。 +没有特殊要求(即 InnoDB 无法满足的功能如:列存储,存储空间数据等)的情况下,所有表必须使用 InnoDB 存储引擎(MySQL5.5 之前默认使用 MyISAM,5.6 以后默认的为 InnoDB)。 InnoDB 支持事务,支持行级锁,更好的恢复性,高并发下性能更好。 diff --git a/docs/database/mysql/mysql-index.md b/docs/database/mysql/mysql-index.md index ab0dc7f6760..e5271b6786e 100644 --- a/docs/database/mysql/mysql-index.md +++ b/docs/database/mysql/mysql-index.md @@ -15,20 +15,20 @@ tag: **索引是一种用于快速查询和检索数据的数据结构,其本质可以看成是一种排序好的数据结构。** -索引的作用就相当于书的目录。打个比方: 我们在查字典的时候,如果没有目录,那我们就只能一页一页的去找我们需要查的那个字,速度很慢。如果有目录了,我们只需要先去目录里查找字的位置,然后直接翻到那一页就行了。 +索引的作用就相当于书的目录。打个比方:我们在查字典的时候,如果没有目录,那我们就只能一页一页地去找我们需要查的那个字,速度很慢;如果有目录了,我们只需要先去目录里查找字的位置,然后直接翻到那一页就行了。 -索引底层数据结构存在很多种类型,常见的索引结构有: B 树, B+树 和 Hash、红黑树。在 MySQL 中,无论是 Innodb 还是 MyIsam,都使用了 B+树作为索引结构。 +索引底层数据结构存在很多种类型,常见的索引结构有:B 树、 B+ 树 和 Hash、红黑树。在 MySQL 中,无论是 Innodb 还是 MyISAM,都使用了 B+ 树作为索引结构。 ## 索引的优缺点 -**优点**: +**优点:** -- 使用索引可以大大加快数据的检索速度(大大减少检索的数据量), 减少 IO 次数,这也是创建索引的最主要的原因。 +- 使用索引可以大大加快数据的检索速度(大大减少检索的数据量),减少 IO 次数,这也是创建索引的最主要的原因。 - 通过创建唯一性索引,可以保证数据库表中每一行数据的唯一性。 -**缺点**: +**缺点:** -- 创建索引和维护索引需要耗费许多时间。当对表中的数据进行增删改的时候,如果数据有索引,那么索引也需要动态的修改,会降低 SQL 执行效率。 +- 创建和维护索引需要耗费许多时间。当对表中的数据进行增删改的时候,如果数据有索引,那么索引也需要动态地修改,这会降低 SQL 执行效率。 - 索引需要使用物理文件存储,也会耗费一定空间。 但是,**使用索引一定能提高查询性能吗?** @@ -39,7 +39,7 @@ tag: ### Hash 表 -哈希表是键值对的集合,通过键(key)即可快速取出对应的值(value),因此哈希表可以快速检索数据(接近 O(1))。 +哈希表是键值对的集合,通过键(key)即可快速取出对应的值(value),因此哈希表可以快速检索数据(接近 O(1))。 **为何能够通过 key 快速取出 value 呢?** 原因在于 **哈希算法**(也叫散列算法)。通过哈希算法,我们可以快速找到 key 对应的 index,找到了 index 也就找到了对应的 value。 @@ -50,7 +50,7 @@ index = hash % array_size ![](https://oss.javaguide.cn/github/javaguide/database/mysql20210513092328171.png) -但是!哈希算法有个 **Hash 冲突** 问题,也就是说多个不同的 key 最后得到的 index 相同。通常情况下,我们常用的解决办法是 **链地址法**。链地址法就是将哈希冲突数据存放在链表中。就比如 JDK1.8 之前 `HashMap` 就是通过链地址法来解决哈希冲突的。不过,JDK1.8 以后`HashMap`为了减少链表过长的时候搜索时间过长引入了红黑树。 +但是!哈希算法有个 **Hash 冲突** 问题,也就是说多个不同的 key 最后得到的 index 相同。通常情况下,我们常用的解决办法是 **链地址法**。链地址法就是将哈希冲突数据存放在链表中。就比如 JDK1.8 之前 `HashMap` 就是通过链地址法来解决哈希冲突的。不过,JDK1.8 以后`HashMap`为了提高链表过长时的搜索效率,引入了红黑树。 ![](https://oss.javaguide.cn/github/javaguide/database/mysql20210513092224836.png) @@ -60,15 +60,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 +76,7 @@ SELECT * FROM tb1 WHERE id < 500; 2. 右子树所有节点的值均大于根节点的值。 3. 左右子树也分别为二叉查找树。 -当二叉查找树是平衡的时候,也就是树的每个节点的左右子树深度相差不超过 1 的时候,查询的时间复杂度为 O(log2(N)),具有比较高的效率。然而,当二叉查找树不平衡时,例如在最坏情况下(有序插入节点),树会退化成线性链表(也被称为斜树),导致查询效率急剧下降,时间复杂退化为 O(N)。 +当二叉查找树是平衡的时候,也就是树的每个节点的左右子树深度相差不超过 1 的时候,查询的时间复杂度为 O(log2(N)),具有比较高的效率。然而,当二叉查找树不平衡时,例如在最坏情况下(有序插入节点),树会退化成线性链表(也被称为斜树),导致查询效率急剧下降,时间复杂退化为 O(N)。 ![斜树](https://oss.javaguide.cn/github/javaguide/cs-basics/data-structure/oblique-tree.png) @@ -92,7 +92,7 @@ AVL 树是计算机科学中最早被发明的自平衡二叉查找树,它的 AVL 树采用了旋转操作来保持平衡。主要有四种旋转操作:LL 旋转、RR 旋转、LR 旋转和 RL 旋转。其中 LL 旋转和 RR 旋转分别用于处理左左和右右失衡,而 LR 旋转和 RL 旋转则用于处理左右和右左失衡。 -由于 AVL 树需要频繁地进行旋转操作来保持平衡,因此会有较大的计算开销进而降低了数据库写操作的性能。并且, 在使用 AVL 树时,每个树节点仅存储一个数据,而每次进行磁盘 IO 时只能读取一个节点的数据,如果需要查询的数据分布在多个节点上,那么就需要进行多次磁盘 IO。 **磁盘 IO 是一项耗时的操作,在设计数据库索引时,我们需要优先考虑如何最大限度地减少磁盘 IO 操作的次数。** +由于 AVL 树需要频繁地进行旋转操作来保持平衡,因此会有较大的计算开销进而降低了数据库写操作的性能。并且, 在使用 AVL 树时,每个树节点仅存储一个数据,而每次进行磁盘 IO 时只能读取一个节点的数据,如果需要查询的数据分布在多个节点上,那么就需要进行多次磁盘 IO。**磁盘 IO 是一项耗时的操作,在设计数据库索引时,我们需要优先考虑如何最大限度地减少磁盘 IO 操作的次数。** 实际应用中,AVL 树使用的并不多。 @@ -112,26 +112,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 +140,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 +154,7 @@ B 树也称 B-树,全称为 **多路平衡查找树** ,B+ 树是 B 树的一 - 唯一索引:加速查询 + 列值唯一(可以有 NULL)。 - 覆盖索引:一个索引包含(或者说覆盖)所有需要查询的字段的值。 - 联合索引:多列值组成一个索引,专门用于组合搜索,其效率大于索引合并。 -- 全文索引:对文本的内容进行分词,进行搜索。目前只有 `CHAR`、`VARCHAR` ,`TEXT` 列上可以创建全文索引。一般不会使用,效率较低,通常使用搜索引擎如 ElasticSearch 代替。 +- 全文索引:对文本的内容进行分词,进行搜索。目前只有 `CHAR`、`VARCHAR`、`TEXT` 列上可以创建全文索引。一般不会使用,效率较低,通常使用搜索引擎如 ElasticSearch 代替。 - 前缀索引:对文本的前几个字符创建索引,相比普通索引建立的数据更小,因为只取前几个字符。 MySQL 8.x 中实现的索引新特性: @@ -163,7 +163,7 @@ MySQL 8.x 中实现的索引新特性: - 降序索引:之前的版本就支持通过 desc 来指定索引为降序,但实际上创建的仍然是常规的升序索引。直到 MySQL 8.x 版本才开始真正支持降序索引。另外,在 MySQL 8.x 版本中,不再对 GROUP BY 语句进行隐式排序。 - 函数索引:从 MySQL 8.0.13 版本开始支持在索引中使用函数或者表达式的值,也就是在索引中可以包含函数或者表达式。 -## 主键索引(Primary Key) +## 主键索引(Primary Key) 数据表的主键列使用的就是主键索引。 @@ -177,16 +177,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 也支持了全文索引。 -二级索引: +二级索引: ![二级索引](https://oss.javaguide.cn/github/javaguide/open-source-project/no-cluster-index.png) @@ -198,48 +198,48 @@ 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 引擎,不管主键还是非主键,使用的都是非聚簇索引。 非聚簇索引的叶子节点并不一定存放数据的指针,因为二级索引的叶子节点就存放的是主键,根据主键再回表查数据。 #### 非聚簇索引的优缺点 -**优点**: +**优点:** -更新代价比聚簇索引要小 。非聚簇索引的更新代价就没有聚簇索引那么大了,非聚簇索引的叶子节点是不存放数据的。 +更新代价比聚簇索引要小。非聚簇索引的更新代价就没有聚簇索引那么大了,非聚簇索引的叶子节点是不存放数据的。 -**缺点**: +**缺点:** -- **依赖于有序的数据**:跟聚簇索引一样,非聚簇索引也依赖于有序的数据 -- **可能会二次查询(回表)**:这应该是非聚簇索引最大的缺点了。 当查到索引对应的指针或主键后,可能还需要根据指针或主键再到数据文件或表中查询。 +- **依赖于有序的数据:** 跟聚簇索引一样,非聚簇索引也依赖于有序的数据。 +- **可能会二次查询(回表):** 这应该是非聚簇索引最大的缺点了。当查到索引对应的指针或主键后,可能还需要根据指针或主键再到数据文件或表中查询。 -这是 MySQL 的表的文件截图: +这是 MySQL 的表的文件截图: ![MySQL 表的文件](https://oss.javaguide.cn/github/javaguide/database/mysql20210420165311654.png) -聚簇索引和非聚簇索引: +聚簇索引和非聚簇索引: ![聚簇索引和非聚簇索引](https://oss.javaguide.cn/github/javaguide/database/mysql20210420165326946.png) -#### 非聚簇索引一定回表查询吗(覆盖索引)? +#### 非聚簇索引一定回表查询吗(覆盖索引)? **非聚簇索引不一定回表查询。** @@ -251,7 +251,7 @@ PS: 不懂的同学可以暂存疑,慢慢往下看,后面会有答案的, 那么这个索引的 key 本身就是 name,查到对应的 name 直接返回就行了,无需回表查询。 -即使是 MYISAM 也是这样,虽然 MYISAM 的主键索引确实需要回表,因为它的主键索引的叶子节点存放的是指针。但是!**如果 SQL 查的就是主键呢?** +即使是 MyISAM 也是这样,虽然 MyISAM 的主键索引确实需要回表,因为它的主键索引的叶子节点存放的是指针。但是!**如果 SQL 查的就是主键呢?** ```sql SELECT id FROM table WHERE id=1; @@ -263,7 +263,7 @@ SELECT id FROM table WHERE id=1; ### 覆盖索引 -如果一个索引包含(或者说覆盖)所有需要查询的字段的值,我们就称之为 **覆盖索引(Covering Index)** 。 +如果一个索引包含(或者说覆盖)所有需要查询的字段的值,我们就称之为 **覆盖索引(Covering Index)**。 在 InnoDB 存储引擎中,非主键索引的叶子节点包含的是主键的值。这意味着,当使用非主键索引进行查询时,数据库会先找到对应的主键值,然后再通过主键索引来定位和检索完整的行数据。这个过程被称为“回表”。 @@ -276,7 +276,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` ( @@ -320,7 +320,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`,我们发现是没有用到覆盖索引的。 ![](https://oss.javaguide.cn/github/javaguide/mysql/not-using-covering-index-demo.png) @@ -336,7 +336,7 @@ ALTER TABLE `cus_order` ADD INDEX id_score_name(score, name); ![](https://oss.javaguide.cn/github/javaguide/mysql/using-covering-index-demo.png) -通过 `Extra` 这一列的 `Using index` ,说明这条 SQL 语句成功使用了覆盖索引。 +通过 `Extra` 这一列的 `Using index`,说明这条 SQL 语句成功使用了覆盖索引。 关于 `EXPLAIN` 命令的详细介绍请看:[MySQL 执行计划分析](./mysql-query-execution-plan.md)这篇文章。 @@ -356,13 +356,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` ( @@ -386,21 +386,21 @@ 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` 呢? 先不要往下看答案,给自己 3 分钟时间想一想。 1. 查询 `a=1 AND c=1`:根据最左前缀匹配原则,查询可以使用索引的前缀部分。因此,该查询仅在 `a=1` 上使用索引,然后对结果进行 `c=1` 的过滤。 2. 查询 `c=1` :由于查询中不包含最左列 `a`,根据最左前缀匹配原则,整个索引都无法被使用。 -3. 查询`b=1 AND c=1`:和第二种一样的情况,整个索引都不会使用。 +3. 查询 `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` ( @@ -417,7 +417,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`。这样,只有同时满足条件的记录才会被返回,减少了回表次数。 ![](https://oss.javaguide.cn/github/javaguide/database/mysql/index-condition-pushdown.png) @@ -429,14 +429,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` 这一条件再进一步做筛选。 有了索引下推之后: @@ -449,8 +449,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. 存储过程不能使用索引下推,因为存储引擎无法调用存储函数。 @@ -458,11 +458,11 @@ MySQL 可以简单分为 Server 层和存储引擎层这两层。Server 层处 ### 选择合适的字段创建索引 -- **不为 NULL 的字段**:索引字段的数据应该尽量不为 NULL,因为对于数据为 NULL 的字段,数据库较难优化。如果字段频繁被查询,但又避免不了为 NULL,建议使用 0,1,true,false 这样语义较为清晰的短值或短字符作为替代。 -- **被频繁查询的字段**:我们创建索引的字段应该是查询操作非常频繁的字段。 -- **被作为条件查询的字段**:被作为 WHERE 条件查询的字段,应该被考虑建立索引。 -- **频繁需要排序的字段**:索引已经排序,这样查询可以利用索引的排序,加快排序查询时间。 -- **被经常频繁用于连接的字段**:经常用于连接的字段可能是一些外键列,对于外键列并不一定要建立外键,只是说该列涉及到表与表的关系。对于频繁被连接查询的字段,可以考虑建立索引,提高多表连接查询的效率。 +- **不为 NULL 的字段:** 索引字段的数据应该尽量不为 NULL,因为对于数据为 NULL 的字段,数据库较难优化。如果字段频繁被查询,但又避免不了为 NULL,建议使用 0、1、true、false 这样语义较为清晰的短值或短字符作为替代。 +- **被频繁查询的字段:** 我们创建索引的字段应该是查询操作非常频繁的字段。 +- **被作为条件查询的字段:** 被作为 WHERE 条件查询的字段,应该被考虑建立索引。 +- **频繁需要排序的字段:** 索引已经排序,这样查询可以利用索引的排序,加快排序查询时间。 +- **被经常频繁用于连接的字段:** 经常用于连接的字段可能是一些外键列,对于外键列并不一定要建立外键,只是说该列涉及到表与表的关系。对于频繁被连接查询的字段,可以考虑建立索引,提高多表连接查询的效率。 ### 被频繁更新的字段应该慎重建立索引 @@ -470,7 +470,7 @@ MySQL 可以简单分为 Server 层和存储引擎层这两层。Server 层处 ### 限制每张表上的索引数量 -索引并不是越多越好,建议单张表索引不超过 5 个!索引可以提高效率同样可以降低效率。 +索引并不是越多越好,建议单张表索引不超过 5 个!索引可以提高效率,同样可以降低效率。 索引可以增加查询效率,但同样也会降低插入和更新的效率,甚至有些情况下会降低查询效率。 @@ -478,11 +478,11 @@ MySQL 可以简单分为 Server 层和存储引擎层这两层。Server 层处 ### 尽可能的考虑建立联合索引而不是单列索引 -因为索引是需要占用磁盘空间的,可以简单理解为每个索引都对应着一颗 B+树。如果一个表的字段过多,索引过多,那么当这个表的数据达到一个体量后,索引占用的空间也是很多的,且修改索引时,耗费的时间也是较多的。如果是联合索引,多个字段在一个索引上,那么将会节约很大磁盘空间,且修改数据的操作效率也会提升。 +因为索引是需要占用磁盘空间的,可以简单理解为每个索引都对应着一颗 B+ 树。如果一个表的字段过多,索引过多,那么当这个表的数据达到一个体量后,索引占用的空间也是很多的,且修改索引时,耗费的时间也是较多的。如果是联合索引,多个字段在一个索引上,那么将会节约很大磁盘空间,且修改数据的操作效率也会提升。 ### 注意避免冗余索引 -冗余索引指的是索引的功能相同,能够命中索引(a, b)就肯定能命中索引(a) ,那么索引(a)就是冗余索引。如(name,city )和(name )这两个索引就是冗余索引,能够命中前者的查询肯定是能够命中后者的 在大多数情况下,都应该尽量扩展已有的索引而不是创建新索引。 +冗余索引指的是索引的功能相同,能够命中索引(a, b)就肯定能命中索引(a) ,那么索引(a)就是冗余索引。如(name,city)和(name)这两个索引就是冗余索引,能够命中前者的查询肯定是能够命中后者的。在大多数情况下,都应该尽量扩展已有的索引而不是创建新索引。 ### 字符串类型的字段使用前缀索引代替普通索引 @@ -492,13 +492,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)。 From fffb0f51008cfd4e55deb3eb19572bee5f4f4715 Mon Sep 17 00:00:00 2001 From: xuqi Date: Sun, 2 Mar 2025 00:47:19 +0800 Subject: [PATCH 31/74] =?UTF-8?q?Redis=E5=B8=B8=E8=A7=81=E9=9D=A2=E8=AF=95?= =?UTF-8?q?=E9=A2=98=E6=80=BB=E7=BB=93(=E4=B8=8A)=201=E3=80=81=E8=AF=8D?= =?UTF-8?q?=E5=8F=A5=E5=8B=98=E8=AF=AF=E5=92=8C=E8=B0=83=E6=95=B4=EF=BC=9B?= =?UTF-8?q?=202=E3=80=81=E6=A0=87=E7=82=B9=E7=AC=A6=E5=8F=B7=E5=8B=98?= =?UTF-8?q?=E8=AF=AF=E5=92=8C=E8=B0=83=E6=95=B4=E3=80=82?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit MySQL索引详解 MySQL高性能优化规范建议总结 1、冒号调整。 --- ...imization-specification-recommendations.md | 24 +-- docs/database/mysql/mysql-index.md | 44 ++--- docs/database/redis/redis-questions-01.md | 162 +++++++++--------- 3 files changed, 115 insertions(+), 115 deletions(-) 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 af0c387be3e..38c333b3308 100644 --- a/docs/database/mysql/mysql-high-performance-optimization-specification-recommendations.md +++ b/docs/database/mysql/mysql-high-performance-optimization-specification-recommendations.md @@ -21,7 +21,7 @@ tag: ### 所有表必须使用 InnoDB 存储引擎 -没有特殊要求(即 InnoDB 无法满足的功能如:列存储,存储空间数据等)的情况下,所有表必须使用 InnoDB 存储引擎(MySQL5.5 之前默认使用 MyISAM,5.6 以后默认的为 InnoDB)。 +没有特殊要求(即 InnoDB 无法满足的功能如:列存储、存储空间数据等)的情况下,所有表必须使用 InnoDB 存储引擎(MySQL5.5 之前默认使用 MyISAM,5.6 以后默认的为 InnoDB)。 InnoDB 支持事务,支持行级锁,更好的恢复性,高并发下性能更好。 @@ -198,9 +198,9 @@ InnoDB 是按照主键索引的顺序来组织表的。 建立索引的目的是:希望通过索引进行数据查找,减少随机 IO,增加查询性能,索引能过滤出越少的数据,则从磁盘中读入的数据也就越少。 -- **区分度最高的列放在联合索引的最左侧:** 这是最重要的原则。区分度越高,通过索引筛选出的数据就越少,I/O 操作也就越少。计算区分度的方法是 `count(distinct column) / count(*)`。 -- **最频繁使用的列放在联合索引的左侧:** 这符合最左前缀匹配原则。将最常用的查询条件列放在最左侧,可以最大程度地利用索引。 -- **字段长度:** 字段长度对联合索引非叶子节点的影响很小,因为它存储了所有联合索引字段的值。字段长度主要影响主键和包含在其他索引中的字段的存储空间,以及这些索引的叶子节点的大小。因此,在选择联合索引列的顺序时,字段长度的优先级最低。对于主键和包含在其他索引中的字段,选择较短的字段长度可以节省存储空间和提高 I/O 性能。 +- **区分度最高的列放在联合索引的最左侧**:这是最重要的原则。区分度越高,通过索引筛选出的数据就越少,I/O 操作也就越少。计算区分度的方法是 `count(distinct column) / count(*)`。 +- **最频繁使用的列放在联合索引的左侧**:这符合最左前缀匹配原则。将最常用的查询条件列放在最左侧,可以最大程度地利用索引。 +- **字段长度**:字段长度对联合索引非叶子节点的影响很小,因为它存储了所有联合索引字段的值。字段长度主要影响主键和包含在其他索引中的字段的存储空间,以及这些索引的叶子节点的大小。因此,在选择联合索引列的顺序时,字段长度的优先级最低。对于主键和包含在其他索引中的字段,选择较短的字段长度可以节省存储空间和提高 I/O 性能。 ### 避免建立冗余索引和重复索引(增加了查询优化器生成执行计划的时间) @@ -211,10 +211,10 @@ InnoDB 是按照主键索引的顺序来组织表的。 > 覆盖索引:就是包含了所有查询字段 (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。 --- @@ -253,13 +253,13 @@ InnoDB 是按照主键索引的顺序来组织表的。 ### 禁止使用不含字段列表的 INSERT 语句 -**不推荐:** +**不推荐**: ```sql insert into t values ('a','b','c'); ``` -**推荐:** +**推荐**: ```sql insert into t(c1,c2,c3) values ('a','b','c'); @@ -285,7 +285,7 @@ select name,phone from customer where id = '111'; 通常子查询在 in 子句中,且子查询中为简单 SQL(不包含 union、group by、order by、limit 从句) 时,才可以把子查询转化为关联查询进行优化。 -**子查询性能差的原因:** 子查询的结果集无法使用索引,通常子查询的结果集会被存储到临时表中,不论是内存临时表还是磁盘临时表都不会存在索引,所以查询性能会受到一定的影响。特别是对于返回结果集比较大的子查询,其对查询性能的影响也就越大。由于子查询会产生大量的临时表也没有索引,所以会消耗过多的 CPU 和 IO 资源,产生大量的慢查询。 +**子查询性能差的原因**:子查询的结果集无法使用索引,通常子查询的结果集会被存储到临时表中,不论是内存临时表还是磁盘临时表都不会存在索引,所以查询性能会受到一定的影响。特别是对于返回结果集比较大的子查询,其对查询性能的影响也就越大。由于子查询会产生大量的临时表也没有索引,所以会消耗过多的 CPU 和 IO 资源,产生大量的慢查询。 ### 避免使用 JOIN 关联太多的表 @@ -315,13 +315,13 @@ order by rand() 会把表中所有符合条件的数据装载到内存中,然 对列进行函数转换或计算时会导致无法使用索引。 -**不推荐:** +**不推荐**: ```sql where date(create_time)='20190101' ``` -**推荐:** +**推荐**: ```sql where create_time >= '20190101' and create_time < '20190102' diff --git a/docs/database/mysql/mysql-index.md b/docs/database/mysql/mysql-index.md index e5271b6786e..a21d133feea 100644 --- a/docs/database/mysql/mysql-index.md +++ b/docs/database/mysql/mysql-index.md @@ -21,12 +21,12 @@ tag: ## 索引的优缺点 -**优点:** +**优点**: - 使用索引可以大大加快数据的检索速度(大大减少检索的数据量),减少 IO 次数,这也是创建索引的最主要的原因。 - 通过创建唯一性索引,可以保证数据库表中每一行数据的唯一性。 -**缺点:** +**缺点**: - 创建和维护索引需要耗费许多时间。当对表中的数据进行增删改的时候,如果数据有索引,那么索引也需要动态地修改,这会降低 SQL 执行效率。 - 索引需要使用物理文件存储,也会耗费一定空间。 @@ -181,10 +181,10 @@ MySQL 8.x 中实现的索引新特性: 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 也支持了全文索引。 二级索引: @@ -202,15 +202,15 @@ PS:不懂的同学可以暂存疑,慢慢往下看,后面会有答案的, #### 聚簇索引的优缺点 -**优点:** +**优点**: -- **查询速度非常快:** 聚簇索引的查询速度非常的快,因为整个 B+ 树本身就是一颗多叉平衡树,叶子节点也都是有序的,定位到索引的节点,就相当于定位到了数据。相比于非聚簇索引, 聚簇索引少了一次读取数据的 IO 操作。 -- **对排序查找和范围查找优化:** 聚簇索引对于主键的排序查找和范围查找速度非常快。 +- **查询速度非常快**:聚簇索引的查询速度非常的快,因为整个 B+ 树本身就是一颗多叉平衡树,叶子节点也都是有序的,定位到索引的节点,就相当于定位到了数据。相比于非聚簇索引, 聚簇索引少了一次读取数据的 IO 操作。 +- **对排序查找和范围查找优化**:聚簇索引对于主键的排序查找和范围查找速度非常快。 -**缺点:** +**缺点**: -- **依赖于有序的数据:** 因为 B+ 树是多路平衡树,如果索引的数据不是有序的,那么就需要在插入时排序,如果数据是整型还好,否则类似于字符串或 UUID 这种又长又难比较的数据,插入或查找的速度肯定比较慢。 -- **更新代价大:** 如果对索引列的数据被修改时,那么对应的索引也将会被修改,而且聚簇索引的叶子节点还存放着数据,修改代价肯定是较大的,所以对于主键索引来说,主键一般都是不可被修改的。 +- **依赖于有序的数据**:因为 B+ 树是多路平衡树,如果索引的数据不是有序的,那么就需要在插入时排序,如果数据是整型还好,否则类似于字符串或 UUID 这种又长又难比较的数据,插入或查找的速度肯定比较慢。 +- **更新代价大**:如果对索引列的数据被修改时,那么对应的索引也将会被修改,而且聚簇索引的叶子节点还存放着数据,修改代价肯定是较大的,所以对于主键索引来说,主键一般都是不可被修改的。 ### 非聚簇索引(非聚集索引) @@ -222,14 +222,14 @@ PS:不懂的同学可以暂存疑,慢慢往下看,后面会有答案的, #### 非聚簇索引的优缺点 -**优点:** +**优点**: 更新代价比聚簇索引要小。非聚簇索引的更新代价就没有聚簇索引那么大了,非聚簇索引的叶子节点是不存放数据的。 -**缺点:** +**缺点**: -- **依赖于有序的数据:** 跟聚簇索引一样,非聚簇索引也依赖于有序的数据。 -- **可能会二次查询(回表):** 这应该是非聚簇索引最大的缺点了。当查到索引对应的指针或主键后,可能还需要根据指针或主键再到数据文件或表中查询。 +- **依赖于有序的数据**:跟聚簇索引一样,非聚簇索引也依赖于有序的数据。 +- **可能会二次查询(回表)**:这应该是非聚簇索引最大的缺点了。当查到索引对应的指针或主键后,可能还需要根据指针或主键再到数据文件或表中查询。 这是 MySQL 的表的文件截图: @@ -391,7 +391,7 @@ SELECT * FROM student WHERE class = 'lIrm08RYVk'; 先不要往下看答案,给自己 3 分钟时间想一想。 1. 查询 `a=1 AND c=1`:根据最左前缀匹配原则,查询可以使用索引的前缀部分。因此,该查询仅在 `a=1` 上使用索引,然后对结果进行 `c=1` 的过滤。 -2. 查询 `c=1` :由于查询中不包含最左列 `a`,根据最左前缀匹配原则,整个索引都无法被使用。 +2. 查询 `c=1`:由于查询中不包含最左列 `a`,根据最左前缀匹配原则,整个索引都无法被使用。 3. 查询 `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)(后续版本已经修复)。个人建议知道有这个东西就好,不需要深究,实际项目也不一定能用上。 @@ -458,11 +458,11 @@ MySQL 可以简单分为 Server 层和存储引擎层这两层。Server 层处 ### 选择合适的字段创建索引 -- **不为 NULL 的字段:** 索引字段的数据应该尽量不为 NULL,因为对于数据为 NULL 的字段,数据库较难优化。如果字段频繁被查询,但又避免不了为 NULL,建议使用 0、1、true、false 这样语义较为清晰的短值或短字符作为替代。 -- **被频繁查询的字段:** 我们创建索引的字段应该是查询操作非常频繁的字段。 -- **被作为条件查询的字段:** 被作为 WHERE 条件查询的字段,应该被考虑建立索引。 -- **频繁需要排序的字段:** 索引已经排序,这样查询可以利用索引的排序,加快排序查询时间。 -- **被经常频繁用于连接的字段:** 经常用于连接的字段可能是一些外键列,对于外键列并不一定要建立外键,只是说该列涉及到表与表的关系。对于频繁被连接查询的字段,可以考虑建立索引,提高多表连接查询的效率。 +- **不为 NULL 的字段**:索引字段的数据应该尽量不为 NULL,因为对于数据为 NULL 的字段,数据库较难优化。如果字段频繁被查询,但又避免不了为 NULL,建议使用 0、1、true、false 这样语义较为清晰的短值或短字符作为替代。 +- **被频繁查询的字段**:我们创建索引的字段应该是查询操作非常频繁的字段。 +- **被作为条件查询的字段**:被作为 WHERE 条件查询的字段,应该被考虑建立索引。 +- **频繁需要排序的字段**:索引已经排序,这样查询可以利用索引的排序,加快排序查询时间。 +- **被经常频繁用于连接的字段**:经常用于连接的字段可能是一些外键列,对于外键列并不一定要建立外键,只是说该列涉及到表与表的关系。对于频繁被连接查询的字段,可以考虑建立索引,提高多表连接查询的效率。 ### 被频繁更新的字段应该慎重建立索引 diff --git a/docs/database/redis/redis-questions-01.md b/docs/database/redis/redis-questions-01.md index e553f773c43..dcce4b3799c 100644 --- a/docs/database/redis/redis-questions-01.md +++ b/docs/database/redis/redis-questions-01.md @@ -30,22 +30,22 @@ Redis 没有外部依赖,Linux 和 OS X 是 Redis 开发和测试最多的两 ![try-redis](https://oss.javaguide.cn/github/javaguide/database/redis/try.redis.io.png) -全世界有非常多的网站使用到了 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 内部做了非常多的性能优化,比较重要的有下面 3 点: +Redis 内部做了非常多的性能优化,比较重要的有下面 4 点: 1. Redis 基于内存,内存的访问速度比磁盘快很多; 2. Redis 基于 Reactor 模式设计开发了一套高效的事件处理模型,主要是单线程事件循环和 IO 多路复用(Redis 线程模式后面会详细介绍到); -3. Redis 内置了多种优化过后的数据类型/结构实现,性能非常高。 +3. Redis 内置了多种优化过后的数据类型/结构实现,性能非常高; 4. Redis 通信协议实现简单且解析高效。 -> 下面这张图片总结的挺不错的,分享一下,出自 [Why is Redis so fast?](https://twitter.com/alexxubyte/status/1498703822528544770) 。 +> 下面这张图片总结的挺不错的,分享一下,出自 [Why is Redis so fast?](https://twitter.com/alexxubyte/status/1498703822528544770)。 ![why-redis-so-fast](./images/why-redis-so-fast.png) -那既然都这么快了,为什么不直接用 Redis 当主数据库呢?主要是因为内存成本太高且 Redis 提供的数据持久化仍然有数据丢失的风险。 +那既然都这么快了,为什么不直接用 Redis 当主数据库呢?主要是因为内存成本太高,并且 Redis 提供的数据持久化仍然有数据丢失的风险。 ### 除了 Redis,你还知道其他分布式缓存方案吗? @@ -62,16 +62,16 @@ Redis 内部做了非常多的性能优化,比较重要的有下面 3 点: 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:篇幅问题,我这并没有对上面提到的分布式缓存选型做详细介绍和对比,感兴趣的话,可以自行研究一下。 @@ -87,10 +87,10 @@ PS:篇幅问题,我这并没有对上面提到的分布式缓存选型做详 **区别**: -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 同时使用了惰性删除与定期删除。 @@ -104,7 +104,7 @@ PS:篇幅问题,我这并没有对上面提到的分布式缓存选型做详 **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):服务器每秒可以执行的查询次数; @@ -126,7 +126,7 @@ 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?有什么用? @@ -151,17 +151,17 @@ Redis 从 4.0 版本开始,支持通过 Module 来扩展其功能以满足特 ### 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 维护排行榜、通过 HyperLogLog 统计网站 UV 和 PV。 +- **分布式 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 可以做消息队列么? @@ -171,7 +171,7 @@ Redis 从 4.0 版本开始,支持通过 Module 来扩展其功能以满足特 **Redis 2.0 之前,如果想要使用 Redis 来做消息队列的话,只能通过 List 来实现。** -通过 `RPUSH/LPOP` 或者 `LPUSH/RPOP`即可实现简易版消息队列: +通过 `RPUSH/LPOP` 或者 `LPUSH/RPOP` 即可实现简易版消息队列: ```bash # 生产者生产消息 @@ -184,7 +184,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 时,即可无限等待,直到弹出消息 @@ -216,11 +216,11 @@ pub/sub 既能单播又能广播,还支持 channel 的简单正则匹配。不 为此,Redis 5.0 新增加的一个数据结构 `Stream` 来做消息队列。`Stream` 支持: -- 发布 / 订阅模式 -- 按照消费者组进行消费(借鉴了 Kafka 消费者组的概念) -- 消息持久化( RDB 和 AOF) -- ACK 机制(通过确认机制来告知已经成功处理了消息) -- 阻塞式获取消息 +- 发布 / 订阅模式; +- 按照消费者组进行消费(借鉴了 Kafka 消费者组的概念); +- 消息持久化( RDB 和 AOF); +- ACK 机制(通过确认机制来告知已经成功处理了消息); +- 阻塞式获取消息。 `Stream` 的结构如下: @@ -230,7 +230,7 @@ pub/sub 既能单播又能广播,还支持 channel 的简单正则匹配。不 这里再对图中涉及到的一些概念,进行简单解释: -- `Consumer Group`:消费者组用于组织和管理多个消费者。消费者组本身不处理消息,而是再将消息分发给消费者,由消费者进行真正的消费 +- `Consumer Group`:消费者组用于组织和管理多个消费者。消费者组本身不处理消息,而是再将消息分发给消费者,由消费者进行真正的消费。 - `last_delivered_id`:标识消费者组当前消费位置的游标,消费者组中任意一个消费者读取了消息都会使 last_delivered_id 往前移动。 - `pending_ids`:记录已经被客户端消费但没有 ack 的消息的 ID。 @@ -245,25 +245,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 支持中文分词、聚合统计、停用词、同义词、拼写检查、标签查询、向量相似度查询、多关键词搜索、分页搜索等功能,算是一个功能比较完善的全文搜索引擎了。 @@ -274,7 +274,7 @@ RediSearch 支持中文分词、聚合统计、停用词、同义词、拼写检 对于小型项目的简单搜索场景来说,使用 RediSearch 来作为搜索引擎还是没有问题的(搭配 RedisJSON 使用)。 -对于比较复杂或者数据规模较大的搜索场景还是不太建议使用 RediSearch 来作为搜索引擎,主要是因为下面这些限制和问题: +对于比较复杂或者数据规模较大的搜索场景,还是不太建议使用 RediSearch 来作为搜索引擎,主要是因为下面这些限制和问题: 1. 数据量限制:Elasticsearch 可以支持 PB 级别的数据量,可以轻松扩展到多个节点,利用分片机制提高可用性和性能。RedisSearch 是基于 Redis 实现的,其能存储的数据量受限于 Redis 的内存容量,不太适合存储大规模的数据(内存昂贵,扩展能力较差)。 2. 分布式能力较差:Elasticsearch 是为分布式环境设计的,可以轻松扩展到多个节点。虽然 RedisSearch 支持分布式部署,但在实际应用中可能会面临一些挑战,如数据分片、节点间通信、数据一致性等问题。 @@ -292,10 +292,10 @@ Elasticsearch 适用于全文搜索、复杂查询、实时数据分析和聚合 基于 Redis 实现延时任务的功能无非就下面两种方案: -1. Redis 过期事件监听 -2. Redisson 内置的延时队列 +1. Redis 过期事件监听。 +2. Redisson 内置的延时队列。 -Redis 过期事件监听的存在时效性较差、丢消息、多服务实例下消息重复消费等问题,不被推荐使用。 +Redis 过期事件监听存在时效性较差、丢消息、多服务实例下消息重复消费等问题,不被推荐使用。 Redisson 内置的延时队列具备下面这些优势: @@ -306,7 +306,7 @@ Redisson 内置的延时队列具备下面这些优势: ## 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) @@ -328,7 +328,7 @@ String 的常见应用场景如下: - 常规数据(比如 Session、Token、序列化后的对象、图片的路径)的缓存; - 计数比如用户单位时间的请求数(简单限流可以用到)、页面单位时间的访问数; -- 分布式锁(利用 `SETNX key value` 命令可以实现一个最简易的分布式锁); +- 分布式锁(利用 `SETNX key value` 命令可以实现一个最简易的分布式锁); - …… 关于 String 的详细介绍请看这篇文章:[Redis 5 种基本数据类型详解](https://javaguide.cn/database/redis/redis-data-structures-01.html)。 @@ -349,11 +349,11 @@ String 的常见应用场景如下: ### 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. @@ -388,7 +388,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 会根据初始化的长度决定使用哪种类型,从而减少内存的使用。 | 类型 | 字节 | 位 | | -------- | ---- | --- | @@ -400,10 +400,10 @@ struct __attribute__ ((__packed__)) sdshdr64 { 对于后四种实现都包含了下面这 4 个属性: -- `len`:字符串的长度也就是已经使用的字节数 -- `alloc`:总共可用的字符空间大小,alloc-len 就是 SDS 剩余的空间大小 -- `buf[]`:实际存储字符串的数组 -- `flags`:低三位保存类型标志 +- `len`:字符串的长度也就是已经使用的字节数。 +- `alloc`:总共可用的字符空间大小,alloc-len 就是 SDS 剩余的空间大小。 +- `buf[]`:实际存储字符串的数组。 +- `flags`:低三位保存类型标志。 SDS 相比于 C 语言中的字符串有如下提升: @@ -445,9 +445,9 @@ struct sdshdr { ### 使用 Redis 实现一个排行榜怎么做? -Redis 中有一个叫做 `Sorted Set` (有序集合)的数据类型经常被用在各种排行榜的场景,比如直播间送礼物的排行榜、朋友圈的微信步数排行榜、王者荣耀中的段位排行榜、话题热度排行榜等等。 +Redis 中有一个叫做 `Sorted Set`(有序集合)的数据类型经常被用在各种排行榜的场景,比如直播间送礼物的排行榜、朋友圈的微信步数排行榜、王者荣耀中的段位排行榜、话题热度排行榜等等。 -相关的一些 Redis 命令: `ZRANGE` (从小到大排序)、 `ZREVRANGE` (从大到小排序)、`ZREVRANK` (指定元素排名)。 +相关的一些 Redis 命令:`ZRANGE`(从小到大排序)、`ZREVRANGE`(从大到小排序)、`ZREVRANK`(指定元素排名)。 ![](https://oss.javaguide.cn/github/javaguide/database/redis/2021060714195385.png) @@ -455,15 +455,15 @@ Redis 中有一个叫做 `Sorted Set` (有序集合)的数据类型经常被 ![](https://oss.javaguide.cn/github/javaguide/database/redis/image-20220719071115140.png) -### 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 为什么用跳表实现有序集合](./redis-skiplist.md)。 ### Set 的应用场景是什么? @@ -471,8 +471,8 @@ Redis 中 `Set` 是一种无序集合,集合中的元素没有先后顺序但 `Set` 的常见应用场景如下: -- 存放的数据不能重复的场景:网站 UV 统计(数据量巨大的场景还是 `HyperLogLog`更适合一些)、文章点赞、动态点赞等等。 -- 需要获取多个数据源交集、并集和差集的场景:共同好友(交集)、共同粉丝(交集)、共同关注(交集)、好友推荐(差集)、音乐推荐(差集)、订阅号推荐(差集+交集) 等等。 +- 存放的数据不能重复的场景:网站 UV 统计(数据量巨大的场景还是 `HyperLogLog` 更适合一些)、文章点赞、动态点赞等等。 +- 需要获取多个数据源交集、并集和差集的场景:共同好友(交集)、共同粉丝(交集)、共同关注(交集)、好友推荐(差集)、音乐推荐(差集)、订阅号推荐(差集+交集)等等。 - 需要随机获取数据源中的元素的场景:抽奖系统、随机点名等等。 ### 使用 Set 实现抽奖系统怎么做? @@ -481,11 +481,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(偏移量)。 @@ -504,7 +504,7 @@ Bitmap 存储的是连续的二进制数字(0 和 1),通过 Bitmap, 只需 (integer) 0 ``` -统计 20210308~20210309 总活跃用户数: +统计 20210308~20210309 总活跃用户数: ```bash > BITOP and desk1 20210308 20210309 @@ -513,7 +513,7 @@ Bitmap 存储的是连续的二进制数字(0 和 1),通过 Bitmap, 只需 (integer) 1 ``` -统计 20210308~20210309 在线活跃用户数: +统计 20210308~20210309 在线活跃用户数: ```bash > BITOP or desk2 20210308 20210309 @@ -543,15 +543,15 @@ PFCOUNT PAGE_1:UV ## Redis 持久化机制(重要) -Redis 持久化机制(RDB 持久化、AOF 持久化、RDB 和 AOF 的混合持久化) 相关的问题比较多,也比较重要,于是我单独抽了一篇文章来总结 Redis 持久化机制相关的知识点和问题:[Redis 持久化机制详解](https://javaguide.cn/database/redis/redis-persistence.html) 。 +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 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 设计与实现》有一段话是这样介绍文件事件处理器的,我觉得写得挺不错。 @@ -577,7 +577,7 @@ Redis 通过 **IO 多路复用程序** 来监听来自客户端的大量连接 ![文件事件处理器(file event handler)](https://oss.javaguide.cn/github/javaguide/database/redis/redis-event-handler.png) -相关阅读:[Redis 事件机制详解](http://remcarpediem.net/article/1aa2da89/) 。 +相关阅读:[Redis 事件机制详解](http://remcarpediem.net/article/1aa2da89/)。 ### Redis6.0 之前为什么不使用多线程? @@ -598,10 +598,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 之后为何引入了多线程? @@ -620,13 +620,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 ``` -但是官网描述开启多线程读并不能有太大提升,因此一般情况下并不建议开启 +但是官网描述开启多线程读并不能有太大提升,因此一般情况下并不建议开启。 相关阅读: @@ -638,8 +638,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 版本,源码地址:): @@ -685,7 +685,7 @@ OK (integer) 56 ``` -注意 ⚠️:Redis 中除了字符串类型有自己独有设置过期时间的命令 `setex` 外,其他方法都需要依靠 `expire` 命令来设置过期时间 。另外, `persist` 命令可以移除一个键的过期时间。 +注意 ⚠️:Redis 中除了字符串类型有自己独有设置过期时间的命令 `setex` 外,其他方法都需要依靠 `expire` 命令来设置过期时间 。另外,`persist` 命令可以移除一个键的过期时间。 **过期时间除了有助于缓解内存的消耗,还有什么其他用么?** @@ -695,7 +695,7 @@ OK ### Redis 是如何判断数据是否过期的呢? -Redis 通过一个叫做过期字典(可以看作是 hash 表)来保存数据过期的时间。过期字典的键指向 Redis 数据库中的某个 key(键),过期字典的值是一个 long long 类型的整数,这个整数保存了 key 所指向的数据库键的过期时间(毫秒精度的 UNIX 时间戳)。 +Redis 通过一个叫做过期字典(可以看作是 hash 表)来保存数据过期的时间。过期字典的键指向 Redis 数据库中的某个 key(键),过期字典的值是一个 long long 类型的整数,这个整数保存了 key 所指向的数据库键的过期时间(毫秒精度的 UNIX 时间戳)。 ![Redis 过期字典](https://oss.javaguide.cn/github/javaguide/database/redis/redis-expired-dictionary.png) @@ -724,7 +724,7 @@ typedef struct redisDb { 3. **延迟队列**:把设置过期时间的 key 放到一个延迟队列里,到期之后就删除 key。这种方式可以保证每个过期 key 都能被删除,但维护延迟队列太麻烦,队列本身也要占用资源。 4. **定时删除**:每个设置了过期时间的 key 都会在设置的时间到达时立即被删除。这种方法可以确保内存中不会有过期的键,但是它对 CPU 的压力最大,因为它需要为每个键都设置一个定时器。 -**Redis 采用的那种删除策略呢?** +**Redis 采用的是那种删除策略呢?** Redis 采用的是 **定期删除+惰性/懒汉式删除** 结合的策略,这也是大部分缓存框架的选择。定期删除对内存更加友好,惰性删除对 CPU 更加友好。两者各有千秋,结合起来使用既能兼顾 CPU 友好,又能兼顾内存友好。 @@ -748,7 +748,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. */ @@ -758,7 +758,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 版本)。 @@ -766,7 +766,7 @@ hz 的取值范围为 1~500。增大 hz 参数的值会提升定期删除的频 类似的参数还有一个 **dynamic-hz**,这个参数开启之后 Redis 就会在 hz 的基础上动态计算一个值。Redis 提供并默认启用了使用自适应 hz 值的能力, -这两个参数都在 Redis 配置文件 `redis.conf`中: +这两个参数都在 Redis 配置文件 `redis.conf` 中: ```properties # 默认为 10 @@ -786,27 +786,27 @@ dynamic-hz yes 因为不太好办到,或者说这种删除方式的成本太高了。假如我们使用延迟队列作为删除策略,这样存在下面这些问题: 1. 队列本身的开销可能很大:key 多的情况下,一个延迟队列可能无法容纳。 -2. 维护延迟队列太麻烦:修改 key 的过期时间就需要调整期在延迟队列中的位置,并且,还需要引入并发控制。 +2. 维护延迟队列太麻烦:修改 key 的过期时间就需要调整期在延迟队列中的位置,并且还需要引入并发控制。 ### 大量 key 集中过期怎么办? 当 Redis 中存在大量 key 在同一时间点集中过期时,可能会导致以下问题: -- **请求延迟增加:** Redis 在处理过期 key 时需要消耗 CPU 资源,如果过期 key 数量庞大,会导致 Redis 实例的 CPU 占用率升高,进而影响其他请求的处理速度,造成延迟增加。 -- **内存占用过高:** 过期的 key 虽然已经失效,但在 Redis 真正删除它们之前,仍然会占用内存空间。如果过期 key 没有及时清理,可能会导致内存占用过高,甚至引发内存溢出。 +- **请求延迟增加**:Redis 在处理过期 key 时需要消耗 CPU 资源,如果过期 key 数量庞大,会导致 Redis 实例的 CPU 占用率升高,进而影响其他请求的处理速度,造成延迟增加。 +- **内存占用过高**:过期的 key 虽然已经失效,但在 Redis 真正删除它们之前,仍然会占用内存空间。如果过期 key 没有及时清理,可能会导致内存占用过高,甚至引发内存溢出。 为了避免这些问题,可以采取以下方案: -1. **尽量避免 key 集中过期**: 在设置键的过期时间时尽量随机一点。 -2. **开启 lazy free 机制**: 修改 `redis.conf` 配置文件,将 `lazyfree-lazy-expire` 参数设置为 `yes`,即可开启 lazy free 机制。开启 lazy free 机制后,Redis 会在后台异步删除过期的 key,不会阻塞主线程的运行,从而降低对 Redis 性能的影响。 +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 @@ -830,7 +830,7 @@ Redis 提供了 6 种内存淘汰策略: `allkeys-xxx` 表示从所有的键值中淘汰数据,而 `volatile-xxx` 表示从设置了过期时间的键值中淘汰数据。 -`config.c`中定义了内存淘汰策略的枚举数组: +`config.c` 中定义了内存淘汰策略的枚举数组: ```c configEnum maxmemory_policy_enum[] = { @@ -854,7 +854,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 From 51b3caececcd59122f83f1a6e4072e6b6608cc17 Mon Sep 17 00:00:00 2001 From: xuqi Date: Sun, 2 Mar 2025 15:43:49 +0800 Subject: [PATCH 32/74] =?UTF-8?q?Redis=E5=B8=B8=E8=A7=81=E9=9D=A2=E8=AF=95?= =?UTF-8?q?=E9=A2=98=E6=80=BB=E7=BB=93(=E4=B8=8B)=201=E3=80=81=E8=AF=8D?= =?UTF-8?q?=E5=8F=A5=E5=8B=98=E8=AF=AF=E5=92=8C=E8=B0=83=E6=95=B4=EF=BC=9B?= =?UTF-8?q?=202=E3=80=81=E6=A0=87=E7=82=B9=E7=AC=A6=E5=8F=B7=E5=8B=98?= =?UTF-8?q?=E8=AF=AF=E5=92=8C=E8=B0=83=E6=95=B4=E3=80=82?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- docs/database/redis/redis-questions-02.md | 160 +++++++++++----------- 1 file changed, 80 insertions(+), 80 deletions(-) diff --git a/docs/database/redis/redis-questions-02.md b/docs/database/redis/redis-questions-02.md index 9eec2d038fc..458e0c231cd 100644 --- a/docs/database/redis/redis-questions-02.md +++ b/docs/database/redis/redis-questions-02.md @@ -28,7 +28,7 @@ Redis 事务实际开发中使用的非常少,功能比较鸡肋,不要将 ### 如何使用 Redis 事务? -Redis 可以通过 **`MULTI`,`EXEC`,`DISCARD` 和 `WATCH`** 等命令来实现事务(Transaction)功能。 +Redis 可以通过 **`MULTI`、`EXEC`、`DISCARD` 和 `WATCH`** 等命令来实现事务(Transaction)功能。 ```bash > MULTI @@ -47,8 +47,8 @@ QUEUED 这个过程是这样的: 1. 开始事务(`MULTI`); -2. 命令入队(批量操作 Redis 的命令,先进先出(FIFO)的顺序执行); -3. 执行事务(`EXEC`)。 +2. 命令入队(批量操作 Redis 的命令,先进先出(FIFO)的顺序执行); +3. 执行事务(`EXEC`)。 你也可以通过 [`DISCARD`](https://redis.io/commands/discard) 命令取消一个事务,它会清空事务队列中保存的所有命令。 @@ -138,10 +138,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 +149,28 @@ Redis 官网也解释了自己为啥不支持回滚。简单来说就是 Redis ![Redis 为什么不支持回滚](https://oss.javaguide.cn/github/javaguide/database/redis/redis-rollback.png) -**相关 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,7 +180,7 @@ Redis 从 2.6 版本开始支持执行 Lua 脚本,它的功能和事务非常 一段 Lua 脚本可以视作一条命令执行,一段 Lua 脚本执行过程中不会有其他脚本或 Redis 命令同时执行,保证了操作不会被其他指令插入或打扰。 -不过,如果 Lua 脚本运行时出错并中途结束,出错之后的命令是不会被执行的。并且,出错之前执行的命令是无法被撤销的,无法实现类似关系型数据库执行失败可以回滚的那种原子性效果。因此, **严格来说的话,通过 Lua 脚本来批量执行 Redis 命令实际也是不完全满足原子性的。** +不过,如果 Lua 脚本运行时出错并中途结束,出错之后的命令是不会被执行的。并且,出错之前执行的命令是无法被撤销的,无法实现类似关系型数据库执行失败可以回滚的那种原子性效果。因此,**严格来说的话,通过 Lua 脚本来批量执行 Redis 命令实际也是不完全满足原子性的。** 如果想要让 Lua 脚本中的命令全部执行,必须保证语句语法和命令都是对的。 @@ -190,34 +190,34 @@ Redis 从 2.6 版本开始支持执行 Lua 脚本,它的功能和事务非常 除了下面介绍的内容之外,再推荐两篇不错的文章: -- [你的 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 +227,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 +252,18 @@ Redis 中有一些原生支持批量操作的命令,比如: ![](https://oss.javaguide.cn/github/javaguide/database/redis/redis-pipeline-vs-transaction.png) -另外,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 +274,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 +299,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 +339,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 +363,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 实时分析、发现,文档地址:。 ![阿里云Key分析](https://oss.javaguide.cn/github/javaguide/database/redis/aliyun-key-analysis.png) @@ -381,7 +381,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,7 +432,7 @@ maxmemory-policy allkeys-lfu 需要注意的是,`hotkeys` 参数命令也会增加 Redis 实例的 CPU 和内存消耗(全局扫描),因此需要谨慎使用。 -**2、使用`MONITOR` 命令。** +**2、使用 `MONITOR` 命令。** `MONITOR` 命令是 Redis 提供的一种实时查看 Redis 的所有操作的方式,可以用于临时监控 Redis 实例的操作情况,包括读写、删除等操作。 @@ -473,7 +473,7 @@ OK 如果你用的是公有云的 Redis 服务的话,可以看看其是否提供了 key 分析功能(一般都提供了)。 -这里以阿里云 Redis 为例说明,它支持 hotkey 实时分析、发现,文档地址: 。 +这里以阿里云 Redis 为例说明,它支持 hotkey 实时分析、发现,文档地址:。 ![阿里云Key分析](https://oss.javaguide.cn/github/javaguide/database/redis/aliyun-key-analysis.png) @@ -497,16 +497,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 +517,23 @@ 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.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 @@ -593,7 +593,7 @@ OK **相关问题**: -1. 什么是内存碎片?为什么会有 Redis 内存碎片? +1. 什么是内存碎片?为什么会有 Redis 内存碎片? 2. 如何清理 Redis 内存碎片? **参考答案**:[Redis 内存碎片详解](https://javaguide.cn/database/redis/redis-memory-fragmentation.html)。 @@ -604,7 +604,7 @@ OK #### 什么是缓存穿透? -缓存穿透说简单点就是大量请求的 key 是不合理的,**根本不存在于缓存中,也不存在于数据库中** 。这就导致这些请求直接到了数据库上,根本没有经过缓存这一层,对数据库造成了巨大的压力,可能直接就被这么多请求弄宕机了。 +缓存穿透说简单点就是大量请求的 key 是不合理的,**根本不存在于缓存中,也不存在于数据库中**。这就导致这些请求直接到了数据库上,根本没有经过缓存这一层,对数据库造成了巨大的压力,可能直接就被这么多请求弄宕机了。 ![缓存穿透](https://oss.javaguide.cn/github/javaguide/database/redis/redis-cache-penetration.png) @@ -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://oss.javaguide.cn/github/javaguide/database/redis/redis-cache-penetration-bloom-filter.png) -更多关于布隆过滤器的详细介绍可以看看我的这篇原创:[不了解布隆过滤器?一文给你整的明明白白!](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 对应的是 **热点数据**,该数据 **存在于数据库中,但不存在于缓存中(通常是因为缓存中的那份数据已经过期)**。这就可能会导致瞬时大量的请求直接打到了数据库上,对数据库造成了巨大的压力,可能直接就被这么多请求弄宕机了。 ![缓存击穿](https://oss.javaguide.cn/github/javaguide/database/redis/redis-cache-breakdown.png) @@ -707,15 +707,15 @@ 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. **持久缓存策略**(看情况):虽然一般不推荐设置缓存永不过期,但对于某些关键性和变化不频繁的数据,可以考虑这种策略。 #### 缓存预热如何实现? @@ -735,12 +735,12 @@ Bloom Filter 会使用一个较大的 bit 数组来保存所有的数据,数 下面单独对 **Cache Aside Pattern(旁路缓存模式)** 来聊聊。 -Cache Aside Pattern 中遇到写请求是这样的:更新数据库,然后直接删除缓存 。 +Cache Aside Pattern 中遇到写请求是这样的:更新数据库,然后直接删除缓存。 如果更新数据库成功,而删除缓存这一步失败的情况的话,简单说有两个解决方案: -1. **缓存失效时间变短(不推荐,治标不治本)**:我们让缓存数据的过期时间变短,这样的话缓存就会从数据库中加载数据。另外,这种解决办法对于先操作缓存后操作数据库的场景不适用。 -2. **增加缓存更新重试机制(常用)**:如果缓存服务当前不可用导致缓存删除失败的话,我们就隔一段时间进行重试,重试次数可以自己定。不过,这里更适合引入消息队列实现异步重试,将删除缓存重试的消息投递到消息队列,然后由专门的消费者来重试,直到成功。虽然说多引入了一个消息队列,但其整体带来的收益还是要更高一些。 +1. **缓存失效时间变短**(不推荐,治标不治本):我们让缓存数据的过期时间变短,这样的话缓存就会从数据库中加载数据。另外,这种解决办法对于先操作缓存后操作数据库的场景不适用。 +2. **增加缓存更新重试机制**(常用):如果缓存服务当前不可用导致缓存删除失败的话,我们就隔一段时间进行重试,重试次数可以自己定。不过,这里更适合引入消息队列实现异步重试,将删除缓存重试的消息投递到消息队列,然后由专门的消费者来重试,直到成功。虽然说多引入了一个消息队列,但其整体带来的收益还是要更高一些。 相关文章推荐:[缓存和数据库一致性问题,看这篇就够了 - 水滴与银弹](https://mp.weixin.qq.com/s?__biz=MzIyOTYxNDI5OA==&mid=2247487312&idx=1&sn=fa19566f5729d6598155b5c676eee62d&chksm=e8beb8e5dfc931f3e35655da9da0b61c79f2843101c130cf38996446975014f958a6481aacf1&scene=178&cur_album_id=1699766580538032128#rd)。 @@ -753,18 +753,18 @@ Cache Aside Pattern 中遇到写请求是这样的:更新数据库,然后直 **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 +777,23 @@ 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 脚本。 +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 开发规范](https://developer.aliyun.com/article/531067)。 ## 参考 - 《Redis 开发与运维》 - 《Redis 设计与实现》 -- Redis Transactions : +- Redis Transactions: - What is Redis Pipeline: - 一文详解 Redis 中 BigKey、HotKey 的发现与处理: -- Bigkey 问题的解决思路与方式探索: +- Bigkey 问题的解决思路与方式探索: - Redis 延迟问题全面排障指南: From 25d0dcf455ecd760606d71707d31cda43a91d449 Mon Sep 17 00:00:00 2001 From: DOTime <56373128+DOTime@users.noreply.github.com> Date: Fri, 7 Mar 2025 20:18:12 +0800 Subject: [PATCH 33/74] Update operating-system-basic-questions-02.md MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 原文对段页机制的描述有误 --- .../operating-system/operating-system-basic-questions-02.md | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) 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 98de61c1340..71d0e474d9a 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 @@ -317,12 +317,12 @@ LRU 算法是实际使用中应用的比较多,也被认为是最接近 OPT ### 段页机制 -结合了段式管理和页式管理的一种内存管理机制,把物理内存先分成若干段,每个段又继续分成若干大小相等的页。 +结合了段式管理和页式管理的一种内存管理机制。程序视角中,内存被划分为多个逻辑段,每个逻辑段进一步被划分为固定大小的页。 在段页式机制下,地址翻译的过程分为两个步骤: -1. 段式地址映射。 -2. 页式地址映射。 +1. 段式地址映射:段式转换将虚拟地址(段选择符+段内偏移)转换为线性地址(通过段基址+偏移计算)。 +2. 页式地址映射:页式转换将线性地址拆分为页号+页内偏移,通过页表映射到物理地址。 ### 局部性原理 From 7c6e03dd21d22fb3ea6203141ba08b85954fe52d Mon Sep 17 00:00:00 2001 From: Guide Date: Fri, 7 Mar 2025 23:18:46 +0800 Subject: [PATCH 34/74] [docs update]typo --- .../operating-system-basic-questions-02.md | 8 ++++++-- docs/database/redis/redis-questions-02.md | 2 +- docs/java/collection/arrayblockingqueue-source-code.md | 2 +- 3 files changed, 8 insertions(+), 4 deletions(-) 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 71d0e474d9a..1d3fc0968bd 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 @@ -321,8 +321,12 @@ LRU 算法是实际使用中应用的比较多,也被认为是最接近 OPT 在段页式机制下,地址翻译的过程分为两个步骤: -1. 段式地址映射:段式转换将虚拟地址(段选择符+段内偏移)转换为线性地址(通过段基址+偏移计算)。 -2. 页式地址映射:页式转换将线性地址拆分为页号+页内偏移,通过页表映射到物理地址。 +1. **段式地址映射(虚拟地址 → 线性地址):** + - 虚拟地址 = 段选择符(段号)+ 段内偏移。 + - 根据段号查段表,找到段基址,加上段内偏移得到线性地址。 +2. **页式地址映射(线性地址 → 物理地址):** + - 线性地址 = 页号 + 页内偏移。 + - 根据页号查页表,找到物理页框号,加上页内偏移得到物理地址。 ### 局部性原理 diff --git a/docs/database/redis/redis-questions-02.md b/docs/database/redis/redis-questions-02.md index 458e0c231cd..15153dcaac0 100644 --- a/docs/database/redis/redis-questions-02.md +++ b/docs/database/redis/redis-questions-02.md @@ -779,7 +779,7 @@ Cache Aside Pattern 中遇到写请求是这样的:更新数据库,然后直 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 脚本代替。 +4. 尽量不使用 Redis 事务:Redis 事务实现的功能比较鸡肋,可以使用 Lua 脚本代替。 5. 禁止长时间开启 monitor:对性能影响比较大。 6. 控制 key 的生命周期:避免 Redis 中存放了太多不经常被访问的数据。 7. …… diff --git a/docs/java/collection/arrayblockingqueue-source-code.md b/docs/java/collection/arrayblockingqueue-source-code.md index f52a1a4e3f5..4c923ef0d29 100644 --- a/docs/java/collection/arrayblockingqueue-source-code.md +++ b/docs/java/collection/arrayblockingqueue-source-code.md @@ -11,7 +11,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 后续的几个版本又对阻塞队列进行了不少的更新和完善: From f7abbcc5399b23e842d03208b084a8e82c2f7d79 Mon Sep 17 00:00:00 2001 From: ChaplinLittleJenius <66432787+ChaplinLittleJenius@users.noreply.github.com> Date: Sat, 8 Mar 2025 11:18:37 +0800 Subject: [PATCH 35/74] Update atomic-classes.md MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit fix:修复需求说明错误 经过实测,实际上字段必须满足的是 volatile int 且不为 private,访问修饰符只要不是 private 即可 --- docs/java/concurrent/atomic-classes.md | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/docs/java/concurrent/atomic-classes.md b/docs/java/concurrent/atomic-classes.md index 9c118c11f87..ec47ba6f66f 100644 --- a/docs/java/concurrent/atomic-classes.md +++ b/docs/java/concurrent/atomic-classes.md @@ -341,7 +341,7 @@ Final Reference: Daisy, Final Mark: true - `AtomicLongFieldUpdater`:原子更新长整形字段的更新器 - `AtomicReferenceFieldUpdater`:原子更新引用类型里的字段的更新器 -要想原子地更新对象的属性需要两步。第一步,因为对象的属性修改类型原子类都是抽象类,所以每次使用都必须使用静态方法 newUpdater()创建一个更新器,并且需要设置想要更新的类和属性。第二步,更新的对象属性必须使用 public volatile 修饰符。 +要想原子地更新对象的属性需要两步。第一步,因为对象的属性修改类型原子类都是抽象类,所以每次使用都必须使用静态方法 newUpdater()创建一个更新器,并且需要设置想要更新的类和属性。第二步,更新的对象属性必须使用 volatile int 修饰符。 上面三个类提供的方法几乎相同,所以我们这里以 `AtomicIntegerFieldUpdater`为例子来介绍。 @@ -351,8 +351,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 } From b5cd6d3fe412bd09a0dcc8fe95a42569dc5c6f34 Mon Sep 17 00:00:00 2001 From: 595lzj <126237952+595lzj@users.noreply.github.com> Date: Tue, 11 Mar 2025 16:33:12 +0800 Subject: [PATCH 36/74] =?UTF-8?q?=E7=BC=93=E5=AD=98=E9=9B=AA=E5=B4=A9?= =?UTF-8?q?=E4=B8=AD=E7=AC=94=E8=AF=AF=E6=A0=A1=E6=AD=A3?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 数据库中的大量数据在同一时间过期->缓存中的大量数据在同一时间过期 --- docs/database/redis/redis-questions-02.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/database/redis/redis-questions-02.md b/docs/database/redis/redis-questions-02.md index 15153dcaac0..37ef43ea72a 100644 --- a/docs/database/redis/redis-questions-02.md +++ b/docs/database/redis/redis-questions-02.md @@ -703,7 +703,7 @@ Bloom Filter 会使用一个较大的 bit 数组来保存所有的数据,数 ![缓存雪崩](https://oss.javaguide.cn/github/javaguide/database/redis/redis-cache-avalanche.png) -举个例子:数据库中的大量数据在同一时间过期,这个时候突然有大量的请求需要访问这些过期的数据。这就导致大量的请求直接落到数据库上,对数据库造成了巨大的压力。 +举个例子:缓存中的大量数据在同一时间过期,这个时候突然有大量的请求需要访问这些过期的数据。这就导致大量的请求直接落到数据库上,对数据库造成了巨大的压力。 #### 有哪些解决办法? From 428c0e76df88070b10be74de34c0aaf861c9aa2a Mon Sep 17 00:00:00 2001 From: Guide Date: Tue, 11 Mar 2025 19:43:01 +0800 Subject: [PATCH 37/74] =?UTF-8?q?[docs=20update]=E6=A0=BC=E5=BC=8F?= =?UTF-8?q?=E5=AE=8C=E5=96=84?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- docs/cs-basics/network/network-attack-means.md | 2 +- docs/database/redis/redis-questions-01.md | 2 +- docs/java/concurrent/java-concurrent-questions-03.md | 2 +- docs/java/jvm/jvm-garbage-collection.md | 2 +- 4 files changed, 4 insertions(+), 4 deletions(-) diff --git a/docs/cs-basics/network/network-attack-means.md b/docs/cs-basics/network/network-attack-means.md index c4a4da8f231..748999d6eba 100644 --- a/docs/cs-basics/network/network-attack-means.md +++ b/docs/cs-basics/network/network-attack-means.md @@ -363,7 +363,7 @@ MD5 可以用来生成一个 128 位的消息摘要,它是目前应用比较 **SHA** -安全散列算法。**SHA** 包括**SHA-1**、**SHA-2**和**SHA-3**三个版本。该算法的基本思想是:接收一段明文数据,通过不可逆的方式将其转换为固定长度的密文。简单来说,SHA将输入数据(即预映射或消息)转化为固定长度、较短的输出值,称为散列值(或信息摘要、信息认证码)。SHA-1已被证明不够安全,因此逐渐被SHA-2取代,而SHA-3则作为SHA系列的最新版本,采用不同的结构(Keccak算法)提供更高的安全性和灵活性。 +安全散列算法。**SHA** 包括**SHA-1**、**SHA-2**和**SHA-3**三个版本。该算法的基本思想是:接收一段明文数据,通过不可逆的方式将其转换为固定长度的密文。简单来说,SHA 将输入数据(即预映射或消息)转化为固定长度、较短的输出值,称为散列值(或信息摘要、信息认证码)。SHA-1 已被证明不够安全,因此逐渐被 SHA-2 取代,而 SHA-3 则作为 SHA 系列的最新版本,采用不同的结构(Keccak 算法)提供更高的安全性和灵活性。 **SM3** diff --git a/docs/database/redis/redis-questions-01.md b/docs/database/redis/redis-questions-01.md index dcce4b3799c..6493ebfe168 100644 --- a/docs/database/redis/redis-questions-01.md +++ b/docs/database/redis/redis-questions-01.md @@ -87,7 +87,7 @@ PS:篇幅问题,我这并没有对上面提到的分布式缓存选型做详 **区别**: -1. **数据类型**:Redis 支持更丰富的数据类型(支持更复杂的应用场景)。Redis 不仅仅支持简单的 k/v 类型的数据,同时还提供 list、set、zset、hash 等数据结构的存储;而Memcached 只支持最简单的 k/v 数据类型。 +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 针对网络数据的读写引入了多线程)。 diff --git a/docs/java/concurrent/java-concurrent-questions-03.md b/docs/java/concurrent/java-concurrent-questions-03.md index 7a2b5436d4a..6d6d9ed6dab 100644 --- a/docs/java/concurrent/java-concurrent-questions-03.md +++ b/docs/java/concurrent/java-concurrent-questions-03.md @@ -883,7 +883,7 @@ public FutureTask(Runnable runnable, V result) { `FutureTask`相当于对`Callable` 进行了封装,管理着任务执行的情况,存储了 `Callable` 的 `call` 方法的任务执行结果。 -关于更多 `Future` 的源码细节,可以肝这篇万字解析,写的很清楚:[Java是如何实现Future模式的?万字详解!](https://juejin.cn/post/6844904199625375757)。 +关于更多 `Future` 的源码细节,可以肝这篇万字解析,写的很清楚:[Java 是如何实现 Future 模式的?万字详解!](https://juejin.cn/post/6844904199625375757)。 ### CompletableFuture 类有什么用? diff --git a/docs/java/jvm/jvm-garbage-collection.md b/docs/java/jvm/jvm-garbage-collection.md index 88d182a9b59..970933ee5ce 100644 --- a/docs/java/jvm/jvm-garbage-collection.md +++ b/docs/java/jvm/jvm-garbage-collection.md @@ -253,7 +253,7 @@ public class ReferenceCountingGc { JDK1.2 之前,Java 中引用的定义很传统:如果 reference 类型的数据存储的数值代表的是另一块内存的起始地址,就称这块内存代表一个引用。 -JDK1.2 以后,Java 对引用的概念进行了扩充,将引用分为强引用、软引用、弱引用、虚引用四种(引用强度逐渐减弱),强引用就是 Java 中普通的对象,而软引用、弱引用、虚引用在JDK中定义的类分别是 `SoftReference`、`WeakReference`、`PhantomReference`。 +JDK1.2 以后,Java 对引用的概念进行了扩充,将引用分为强引用、软引用、弱引用、虚引用四种(引用强度逐渐减弱),强引用就是 Java 中普通的对象,而软引用、弱引用、虚引用在 JDK 中定义的类分别是 `SoftReference`、`WeakReference`、`PhantomReference`。 ![Java 引用类型总结](https://oss.javaguide.cn/github/javaguide/java/jvm/java-reference-type.png) From 45ac7d10958f23ea94e260de7817c591ea12d94e Mon Sep 17 00:00:00 2001 From: Guide Date: Tue, 11 Mar 2025 19:44:09 +0800 Subject: [PATCH 38/74] =?UTF-8?q?[docs=20update]Linux=E5=91=BD=E4=BB=A4?= =?UTF-8?q?=E6=96=B0=E5=A2=9E&Redis=E6=8C=81=E4=B9=85=E5=8C=96=E6=A0=A1?= =?UTF-8?q?=E9=AA=8C=E6=9C=BA=E5=88=B6=E4=BF=AE=E6=AD=A3?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../cs-basics/operating-system/linux-intro.md | 2 ++ docs/database/redis/redis-persistence.md | 22 +++++++++++++++++-- 2 files changed, 22 insertions(+), 2 deletions(-) diff --git a/docs/cs-basics/operating-system/linux-intro.md b/docs/cs-basics/operating-system/linux-intro.md index 9255eb1f8a7..a1e2366daab 100644 --- a/docs/cs-basics/operating-system/linux-intro.md +++ b/docs/cs-basics/operating-system/linux-intro.md @@ -355,6 +355,8 @@ Linux 系统是一个多用户多任务的分时操作系统,任何一个要 - `ifconfig` 或 `ip`:用于查看系统的网络接口信息,包括网络接口的 IP 地址、MAC 地址、状态等。 - `netstat [选项]`:用于查看系统的网络连接状态和网络统计信息,可以查看当前的网络连接情况、监听端口、网络协议等。 - `ss [选项]`:比 `netstat` 更好用,提供了更快速、更详细的网络连接信息。 +- `nload`:`sar` 和 `nload` 都可以监控网络流量,但`sar` 的输出是文本形式的数据,不够直观。`nload` 则是一个专门用于实时监控网络流量的工具,提供图形化的终端界面,更加直观。不过,`nload` 不保存历史数据,所以它不适合用于长期趋势分析。并且,系统并没有默认安装它,需要手动安装。 +- `sudo hostnamectl set-hostname 新主机名`:更改主机名,并且重启后依然有效。`sudo hostname 新主机名`也可以更改主机名。不过需要注意的是,使用 `hostname` 命令直接更改主机名只是临时生效,系统重启后会恢复为原来的主机名。 ### 其他 diff --git a/docs/database/redis/redis-persistence.md b/docs/database/redis/redis-persistence.md index 1e51df93448..c17fe7db316 100644 --- a/docs/database/redis/redis-persistence.md +++ b/docs/database/redis/redis-persistence.md @@ -153,9 +153,27 @@ Redis 7.0 版本之后,AOF 重写机制得到了优化改进。下面这段内 ### AOF 校验机制了解吗? -AOF 校验机制是 Redis 在启动时对 AOF 文件进行检查,以判断文件是否完整,是否有损坏或者丢失的数据。这个机制的原理其实非常简单,就是通过使用一种叫做 **校验和(checksum)** 的数字来验证 AOF 文件。这个校验和是通过对整个 AOF 文件内容进行 CRC64 算法计算得出的数字。如果文件内容发生了变化,那么校验和也会随之改变。因此,Redis 在启动时会比较计算出的校验和与文件末尾保存的校验和(计算的时候会把最后一行保存校验和的内容给忽略点),从而判断 AOF 文件是否完整。如果发现文件有问题,Redis 就会拒绝启动并提供相应的错误信息。AOF 校验机制十分简单有效,可以提高 Redis 数据的可靠性。 +纯 AOF 模式下,Redis 不会对整个 AOF 文件使用校验和(如 CRC64),而是通过逐条解析文件中的命令来验证文件的有效性。如果解析过程中发现语法错误(如命令不完整、格式错误),Redis 会终止加载并报错,从而避免错误数据载入内存。 -类似地,RDB 文件也有类似的校验机制来保证 RDB 文件的正确性,这里就不重复进行介绍了。 +在 **混合持久化模式**(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 部分校验通过后,Redis 随后逐条解析 AOF 部分的增量命令。如果解析过程中出现错误(如不完整的命令或格式错误),Redis 会停止继续加载后续命令,并报告错误,但此时 Redis 已经成功加载了 RDB 快照部分的数据。 ## Redis 4.0 对于持久化机制做了什么优化? From 76e4dbaa3b5ed309896fbde0fe8267859e19dd4f Mon Sep 17 00:00:00 2001 From: Guide Date: Thu, 13 Mar 2025 10:14:01 +0800 Subject: [PATCH 39/74] =?UTF-8?q?[docs=20fix]Linux=20=E6=96=87=E4=BB=B6?= =?UTF-8?q?=E6=9D=83=E9=99=90=E8=A7=A3=E9=87=8A=E9=94=99=E8=AF=AF?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- docs/cs-basics/operating-system/linux-intro.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/docs/cs-basics/operating-system/linux-intro.md b/docs/cs-basics/operating-system/linux-intro.md index a1e2366daab..1486fe45c90 100644 --- a/docs/cs-basics/operating-system/linux-intro.md +++ b/docs/cs-basics/operating-system/linux-intro.md @@ -185,8 +185,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`的空父目录,直到遇到非空目录或根目录。 From 3bab51fc416cd7f3a0e22483a3a8319a784f38a0 Mon Sep 17 00:00:00 2001 From: darcy Date: Tue, 18 Mar 2025 11:33:01 +0800 Subject: [PATCH 40/74] [docs fix] code indentation --- docs/java/io/io-design-patterns.md | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/docs/java/io/io-design-patterns.md b/docs/java/io/io-design-patterns.md index 5408c06049b..f005a18ece4 100644 --- a/docs/java/io/io-design-patterns.md +++ b/docs/java/io/io-design-patterns.md @@ -118,8 +118,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 +130,7 @@ public class InputStreamReader extends Reader { } } // 使用 StreamDecoder 对象做具体的读取工作 - public int read() throws IOException { + public int read() throws IOException { return sd.read(); } } From 352b05c1865442964a65150fe4f73dac70490390 Mon Sep 17 00:00:00 2001 From: Guide Date: Tue, 18 Mar 2025 19:23:20 +0800 Subject: [PATCH 41/74] [docs fix]typo --- README.md | 2 +- docs/home.md | 2 +- docs/java/concurrent/java-concurrent-questions-03.md | 2 +- docs/java/new-features/java21.md | 4 ++-- 4 files changed, 5 insertions(+), 5 deletions(-) diff --git a/README.md b/README.md index 56c3dffdfce..5eec54196b6 100755 --- a/README.md +++ b/README.md @@ -88,7 +88,7 @@ **重要知识点详解**: -- [乐观锁和悲观锁详解](./docs/java/concurrent/jmm.md) +- [乐观锁和悲观锁详解](./docs/java/concurrent/optimistic-lock-and-pessimistic-lock.md) - [CAS 详解](./docs/java/concurrent/cas.md) - [JMM(Java 内存模型)详解](./docs/java/concurrent/jmm.md) - **线程池**:[Java 线程池详解](./docs/java/concurrent/java-thread-pool-summary.md)、[Java 线程池最佳实践](./docs/java/concurrent/java-thread-pool-best-practices.md) diff --git a/docs/home.md b/docs/home.md index 2c88abdfdc3..e77cae7c20b 100644 --- a/docs/home.md +++ b/docs/home.md @@ -72,7 +72,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) diff --git a/docs/java/concurrent/java-concurrent-questions-03.md b/docs/java/concurrent/java-concurrent-questions-03.md index 6d6d9ed6dab..ffa9b3fee0c 100644 --- a/docs/java/concurrent/java-concurrent-questions-03.md +++ b/docs/java/concurrent/java-concurrent-questions-03.md @@ -579,7 +579,7 @@ public class ThreadPoolTest { ![将一部分任务保存到MySQL中](https://oss.javaguide.cn/github/javaguide/java/concurrent/threadpool-reject-2-threadpool-reject-02.png) -整个实现逻辑还是比较简单的,核心在于自定义拒绝策略和阻塞队列。如此一来,一旦我们的线程池中线程以达到满载时,我们就可以通过拒绝策略将最新任务持久化到 MySQL 数据库中,等到线程池有了有余力处理所有任务时,让其优先处理数据库中的任务以避免"饥饿"问题。 +整个实现逻辑还是比较简单的,核心在于自定义拒绝策略和阻塞队列。如此一来,一旦我们的线程池中线程达到满载时,我们就可以通过拒绝策略将最新任务持久化到 MySQL 数据库中,等到线程池有了有余力处理所有任务时,让其优先处理数据库中的任务以避免"饥饿"问题。 当然,对于这个问题,我们也可以参考其他主流框架的做法,以 Netty 为例,它的拒绝策略则是直接创建一个线程池以外的线程处理这些任务,为了保证任务的实时处理,这种做法可能需要良好的硬件设备且临时创建的线程无法做到准确的监控: diff --git a/docs/java/new-features/java21.md b/docs/java/new-features/java21.md index 2940cc62bc7..5f145c23cc5 100644 --- a/docs/java/new-features/java21.md +++ b/docs/java/new-features/java21.md @@ -85,7 +85,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 +198,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`以及获取集合的反向视图的方法。 From c7a9f1dc30d4aa8833c382acf2e93c96a1993596 Mon Sep 17 00:00:00 2001 From: Fuqiao Xue Date: Wed, 19 Mar 2025 14:34:35 +0800 Subject: [PATCH 42/74] Update contribution-guideline.md Link to https://www.w3.org/TR/clreq/ --- docs/javaguide/contribution-guideline.md | 1 + 1 file changed, 1 insertion(+) diff --git a/docs/javaguide/contribution-guideline.md b/docs/javaguide/contribution-guideline.md index 55dd44d8f26..0c9e8df0ef1 100644 --- a/docs/javaguide/contribution-guideline.md +++ b/docs/javaguide/contribution-guideline.md @@ -20,6 +20,7 @@ icon: guide - [写给大家看的中文排版指南 - 知乎](https://zhuanlan.zhihu.com/p/20506092) - [中文文案排版细则 - Dawner](https://dawner.top/posts/chinese-copywriting-rules/) - [中文技术文档写作风格指南](https://github.com/yikeke/zh-style-guide/) +- [中文排版需求](https://www.w3.org/TR/clreq/) 如果要提 issue/question 的话,强烈推荐阅读下面这些资料: From e922fecb4fccb01e4cbd73987b7d76163ae3bba0 Mon Sep 17 00:00:00 2001 From: Guide Date: Thu, 20 Mar 2025 15:56:49 +0800 Subject: [PATCH 43/74] =?UTF-8?q?[docs=20add]Java=2024=20=E9=87=8D?= =?UTF-8?q?=E8=A6=81=E6=96=B0=E7=89=B9=E6=80=A7=E8=A7=A3=E8=AF=BB?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- README.md | 1 + docs/home.md | 1 + docs/java/new-features/java22-23.md | 2 + docs/java/new-features/java24.md | 248 ++++++++++++++++++++++++++++ 4 files changed, 252 insertions(+) create mode 100644 docs/java/new-features/java24.md diff --git a/README.md b/README.md index 5eec54196b6..c86501d0598 100755 --- a/README.md +++ b/README.md @@ -126,6 +126,7 @@ JVM 这部分内容主要参考 [JVM 虚拟机规范-Java8](https://docs.oracle. - [Java 20 新特性概览](./docs/java/new-features/java20.md) - [Java 21 新特性概览](./docs/java/new-features/java21.md) - [Java 22 & 23 新特性概览](./docs/java/new-features/java22-23.md) +- [Java 24 新特性概览](./docs/java/new-features/java24.md) ## 计算机基础 diff --git a/docs/home.md b/docs/home.md index e77cae7c20b..015a9105da3 100644 --- a/docs/home.md +++ b/docs/home.md @@ -110,6 +110,7 @@ JVM 这部分内容主要参考 [JVM 虚拟机规范-Java8](https://docs.oracle. - [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) ## 计算机基础 diff --git a/docs/java/new-features/java22-23.md b/docs/java/new-features/java22-23.md index 6bf1868312c..223c2b7a72c 100644 --- a/docs/java/new-features/java22-23.md +++ b/docs/java/new-features/java22-23.md @@ -7,6 +7,8 @@ tag: JDK 23 和 JDK 22 一样,这也是一个非 LTS(长期支持)版本,Oracle 仅提供六个月的支持。下一个长期支持版是 JDK 25,预计明年 9 月份发布。 +下图是从 JDK8 到 JDK 24 每个版本的更新带来的新特性数量和更新时间: + ![](https://oss.javaguide.cn/github/javaguide/java/new-features/jdk8~jdk24.png) 由于 JDK 22 和 JDK 23 重合的新特性较多,这里主要以 JDK 23 为主介绍,会补充 JDK 22 独有的一些特性。 diff --git a/docs/java/new-features/java24.md b/docs/java/new-features/java24.md new file mode 100644 index 00000000000..8c950bbcf7f --- /dev/null +++ b/docs/java/new-features/java24.md @@ -0,0 +1,248 @@ +[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 24 新特性](https://oss.javaguide.cn/github/javaguide/java/new-features/jdk24-features.png) + +下图是从 JDK 8 到 JDK 24 每个版本的更新带来的新特性数量和更新时间: + +![](https://oss.javaguide.cn/github/javaguide/java/new-features/jdk8~jdk24.png) + +## 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 实现的轻量级线程。许多虚拟线程共享同一个操作系统线程,从而允许非常多的虚拟线程。 From 1344041d791cb63c3bed0521c80d4a2652ebe753 Mon Sep 17 00:00:00 2001 From: Guide Date: Mon, 24 Mar 2025 10:47:48 +0800 Subject: [PATCH 44/74] =?UTF-8?q?[docs=20add]=20java=20=E5=AD=A6=E4=B9=A0?= =?UTF-8?q?=E8=B7=AF=E7=BA=BF=E6=9C=80=E6=96=B0=E7=89=88?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- docs/.vuepress/sidebar/index.ts | 6 +++-- docs/interview-preparation/java-roadmap.md | 29 ++++++++++++++++++++++ 2 files changed, 33 insertions(+), 2 deletions(-) create mode 100644 docs/interview-preparation/java-roadmap.md diff --git a/docs/.vuepress/sidebar/index.ts b/docs/.vuepress/sidebar/index.ts index d2a24458834..41e1c98dece 100644 --- a/docs/.vuepress/sidebar/index.ts +++ b/docs/.vuepress/sidebar/index.ts @@ -35,9 +35,10 @@ export default sidebar({ "teach-you-how-to-prepare-for-the-interview-hand-in-hand", "resume-guide", "key-points-of-interview", + "java-roadmap", "project-experience-guide", - "interview-experience", - "self-test-of-common-interview-questions", + "how-to-handle-interview-nerves", + "internship-experience", ], }, { @@ -169,6 +170,7 @@ export default sidebar({ "java20", "java21", "java22-23", + "java24", ], }, ], diff --git a/docs/interview-preparation/java-roadmap.md b/docs/interview-preparation/java-roadmap.md new file mode 100644 index 00000000000..60dd0fa7fc0 --- /dev/null +++ b/docs/interview-preparation/java-roadmap.md @@ -0,0 +1,29 @@ +--- +title: Java 学习路线(最新版,4w+字) +category: 面试准备 +icon: path +--- + +::: tip 重要说明 + +本学习路线保持**年度系统性修订**,严格同步 Java 技术生态与招聘市场的最新动态,**确保内容时效性与前瞻性**。 + +::: + +历时一个月精心打磨,笔者基于当下 Java 后端开发岗位招聘的最新要求,对既有学习路线进行了全面升级。本次升级涵盖技术栈增删、学习路径优化、配套学习资源更新等维度,力争构建出更符合 Java 开发者成长曲线的知识体系。 + +![Java 学习路线 PDF 概览](https://oss.javaguide.cn/github/javaguide/interview-preparation/java-road-map-pdf.png) + +这可能是你见过的最用心、最全面的 Java 后端学习路线。这份学习路线共包含 **4w+** 字,但你完全不用担心内容过多而学不完。我会根据学习难度,划分出适合找小厂工作必学的内容,以及适合逐步提升 Java 后端开发能力的学习路径。 + +![Java 学习路线图](https://oss.javaguide.cn/github/javaguide/interview-preparation/java-road-map.png) + +对于初学者,你可以按照这篇文章推荐的学习路线和资料进行系统性的学习;对于有经验的开发者,你可以根据这篇文章更一步地深入学习 Java 后端开发,提升个人竞争力。 + +在看这份学习路线的过程中,建议搭配 [Java 面试重点总结(重要)](https://javaguide.cn/interview-preparation/key-points-of-interview.html),可以让你在学习过程中更有目的性。 + +由于这份学习路线内容太多,因此我将其整理成了 PDF 版本(共 **61** 页),方便大家阅读。这份 PDF 有黑夜和白天两种阅读版本,满足大家的不同需求。 + +这份学习路线的获取方法很简单:直接在公众号「**JavaGuide**」后台回复“**学习路线**”即可获取。 + +![JavaGuide 官方公众号](https://oss.javaguide.cn/github/javaguide/gongzhonghaoxuanchuan.png) From c70f5093bfd86fc64eceb44880942c1bdb7c4bb2 Mon Sep 17 00:00:00 2001 From: Guide Date: Mon, 24 Mar 2025 10:50:09 +0800 Subject: [PATCH 45/74] =?UTF-8?q?[docs=20update]=E8=A1=A5=E5=85=85?= =?UTF-8?q?=E4=B8=A4=E4=B8=AA=E5=BC=80=E6=BA=90=20Java=20=E5=9F=BA?= =?UTF-8?q?=E7=A1=80=E5=BC=80=E5=8F=91=E6=A1=86=E6=9E=B6=EF=BC=88=E7=B1=BB?= =?UTF-8?q?=E4=BC=BC=E4=BA=8E=20Spring=20Boot=EF=BC=89?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- docs/open-source-project/system-design.md | 2 ++ 1 file changed, 2 insertions(+) diff --git a/docs/open-source-project/system-design.md b/docs/open-source-project/system-design.md index 92623d2363d..5471f2d07b3 100644 --- a/docs/open-source-project/system-design.md +++ b/docs/open-source-project/system-design.md @@ -10,6 +10,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 +19,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 文档 From fe6a2deb3710c7ae13968feac97e25dece535815 Mon Sep 17 00:00:00 2001 From: Guide Date: Mon, 24 Mar 2025 21:01:35 +0800 Subject: [PATCH 46/74] =?UTF-8?q?[docs=20fix]=E5=AE=8C=E5=96=84=E5=92=8C?= =?UTF-8?q?=E8=A1=A5=E5=85=85=20HTTP/1.1=20=E5=92=8C=20HTTP/2.0=20?= =?UTF-8?q?=E7=9A=84=E9=98=9F=E5=A4=B4=E9=98=BB=E5=A1=9E=E7=9A=84=E4=BB=8B?= =?UTF-8?q?=E7=BB=8D?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../network/other-network-questions.md | 27 ++++++++++++++++++- .../network/other-network-questions2.md | 2 +- 2 files changed, 27 insertions(+), 2 deletions(-) diff --git a/docs/cs-basics/network/other-network-questions.md b/docs/cs-basics/network/other-network-questions.md index 24591862b69..c845073f0c7 100644 --- a/docs/cs-basics/network/other-network-questions.md +++ b/docs/cs-basics/network/other-network-questions.md @@ -196,6 +196,7 @@ HTTP 状态码用于描述 HTTP 请求的结果,比如 2xx 就代表请求被 - **多路复用(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 +204,7 @@ HTTP/2.0 多路复用效果图(图源: [HTTP/2 For Web Developers](https://b ![HTTP/2 Multiplexing](https://oss.javaguide.cn/github/javaguide/cs-basics/network/http2.0-multiplexing.png) -可以看到,HTTP/2.0 的多路复用使得不同的请求可以共用一个 TCP 连接,避免建立多个连接带来不必要的额外开销,而 HTTP/1.1 中的每个请求都会建立一个单独的连接 +可以看到,HTTP/2 的多路复用机制允许多个请求和响应共享一个 TCP 连接,从而避免了 HTTP/1.1 在应对并发请求时需要建立多个并行连接的情况,减少了重复连接建立和维护的额外开销。而在 HTTP/1.1 中,尽管支持持久连接,但为了缓解队头阻塞问题,浏览器通常会为同一域名建立多个并行连接。 ### HTTP/2.0 和 HTTP/3.0 有什么区别? @@ -232,6 +233,29 @@ HTTP/1.0、HTTP/2.0 和 HTTP/3.0 的协议栈比较: 关于 HTTP/1.0 -> HTTP/3.0 更详细的演进介绍,推荐阅读[HTTP1 到 HTTP3 的工程优化](https://dbwu.tech/posts/http_evolution/)。 +### 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)协议。也就是说 HTTP 协议自身不对请求和响应之间的通信状态进行保存。那么我们如何保存用户状态呢?Session 机制的存在就是为了解决这个问题,Session 的主要作用就是通过服务端记录用户的状态。典型的场景是购物车,当你要添加商品到购物车的时候,系统不知道是哪个用户操作的,因为 HTTP 协议是无状态的。服务端给特定的用户创建特定的 Session 之后就可以标识这个用户并且跟踪这个用户了(一般情况下,服务器会在一定时间内保存这个 Session,过了时间限制,就会销毁这个 Session)。 @@ -406,6 +430,7 @@ DNS 服务器自底向上可以依次分为以下几个层级(所有 DNS 服务 ### DNS 劫持了解吗?如何应对? DNS 劫持是一种网络攻击,它通过修改 DNS 服务器的解析结果,使用户访问的域名指向错误的 IP 地址,从而导致用户无法访问正常的网站,或者被引导到恶意的网站。DNS 劫持有时也被称为 DNS 重定向、DNS 欺骗或 DNS 污染。 + ## 参考 - 《图解 HTTP》 diff --git a/docs/cs-basics/network/other-network-questions2.md b/docs/cs-basics/network/other-network-questions2.md index 1f2725711d0..fac525d704c 100644 --- a/docs/cs-basics/network/other-network-questions2.md +++ b/docs/cs-basics/network/other-network-questions2.md @@ -45,7 +45,7 @@ tag: HTTP/3.0 之前是基于 TCP 协议的,而 HTTP/3.0 将弃用 TCP,改用 **基于 UDP 的 QUIC 协议** 。 -此变化解决了 HTTP/2 中存在的队头阻塞问题。队头阻塞是指在 HTTP/2.0 中,多个 HTTP 请求和响应共享一个 TCP 连接,如果其中一个请求或响应因为网络拥塞或丢包而被阻塞,那么后续的请求或响应也无法发送,导致整个连接的效率降低。这是由于 HTTP/2.0 在单个 TCP 连接上使用了多路复用,受到 TCP 拥塞控制的影响,少量的丢包就可能导致整个 TCP 连接上的所有流被阻塞。HTTP/3.0 在一定程度上解决了队头阻塞问题,一个连接建立多个不同的数据流,这些数据流之间独立互不影响,某个数据流发生丢包了,其数据流不受影响(本质上是多路复用+轮询)。 +此变化解决了 HTTP/2.0 中存在的队头阻塞问题。队头阻塞是指在 HTTP/2.0 中,多个 HTTP 请求和响应共享一个 TCP 连接,如果其中一个请求或响应因为网络拥塞或丢包而被阻塞,那么后续的请求或响应也无法发送,导致整个连接的效率降低。这是由于 HTTP/2.0 在单个 TCP 连接上使用了多路复用,受到 TCP 拥塞控制的影响,少量的丢包就可能导致整个 TCP 连接上的所有流被阻塞。HTTP/3.0 在一定程度上解决了队头阻塞问题,一个连接建立多个不同的数据流,这些数据流之间独立互不影响,某个数据流发生丢包了,其数据流不受影响(本质上是多路复用+轮询)。 除了解决队头阻塞问题,HTTP/3.0 还可以减少握手过程的延迟。在 HTTP/2.0 中,如果要建立一个安全的 HTTPS 连接,需要经过 TCP 三次握手和 TLS 握手: From a59e69924fef91742a2ac68b95d480b86eb895cc Mon Sep 17 00:00:00 2001 From: Guide Date: Tue, 25 Mar 2025 13:34:17 +0800 Subject: [PATCH 47/74] =?UTF-8?q?[docs=20add]=E5=B7=B2=E7=BB=8F=E6=B7=98?= =?UTF-8?q?=E6=B1=B0=E7=9A=84=20Java=20=E6=8A=80=E6=9C=AF=EF=BC=8C?= =?UTF-8?q?=E4=B8=8D=E8=A6=81=E5=86=8D=E5=AD=A6=E4=BA=86=EF=BC=81?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- docs/.vuepress/sidebar/about-the-author.ts | 1 + .../deprecated-java-technologies.md | 101 ++++++++++++++++++ 2 files changed, 102 insertions(+) create mode 100644 docs/about-the-author/deprecated-java-technologies.md diff --git a/docs/.vuepress/sidebar/about-the-author.ts b/docs/.vuepress/sidebar/about-the-author.ts index 4ed42a239b0..70e7015927e 100644 --- a/docs/.vuepress/sidebar/about-the-author.ts +++ b/docs/.vuepress/sidebar/about-the-author.ts @@ -19,6 +19,7 @@ export const aboutTheAuthor = arraySidebar([ collapsible: false, children: [ "writing-technology-blog-six-years", + "deprecated-java-technologies", "my-article-was-stolen-and-made-into-video-and-it-became-popular", "dog-that-copies-other-people-essay", "zhishixingqiu-two-years", diff --git a/docs/about-the-author/deprecated-java-technologies.md b/docs/about-the-author/deprecated-java-technologies.md new file mode 100644 index 00000000000..fa0ed098707 --- /dev/null +++ b/docs/about-the-author/deprecated-java-technologies.md @@ -0,0 +1,101 @@ +--- +title: 已经淘汰的 Java 技术,不要再学了! +category: 走近作者 +tag: + - 杂谈 +--- + +前几天,我在知乎上随手回答了一个问题:“Java 学到 JSP 就学不下去了,怎么办?”。 + +出于不想让别人走弯路的心态,我回答说:已经淘汰的技术就不要学了,并顺带列举了一些在 Java 开发领域中已经被淘汰的技术。 + +## 已经淘汰的 Java 技术 + +我的回答原内容如下,列举了一些在 Java 开发领域中已经被淘汰的技术: + +**JSP** + +- **原因**:JSP 已经过时,无法满足现代 Web 开发需求;前后端分离成为主流。 +- **替代方案**:模板引擎(如 Thymeleaf、Freemarker)在传统全栈开发中更流行;而在前后端分离架构中,React、Vue、Angular 等现代前端框架已取代 JSP 的角色。 +- **注意**:一些国企和央企的老项目可能仍然在使用 JSP,但这种情况越来越少见。 + +**Struts(尤其是 1.x)** + +- **原因**:配置繁琐、开发效率低,且存在严重的安全漏洞(如世界著名的 Apache Struts 2 漏洞)。此外,社区维护不足,生态逐渐萎缩。 +- **替代方案**:Spring MVC 和 Spring WebFlux 提供了更简洁的开发体验、更强大的功能以及完善的社区支持,完全取代了 Struts。 + +**EJB (Enterprise JavaBeans)** + +- **原因**:EJB 过于复杂,开发成本高,学习曲线陡峭,在实际项目中逐步被更轻量化的框架取代。 +- **替代方案**:Spring/Spring Boot 提供了更加简洁且功能强大的企业级开发解决方案,几乎已经成为 Java 企业开发的事实标准。此外,国产的 Solon 和云原生友好的 Quarkus 等框架也非常不错。 + +**Java Applets** + +- **原因**:现代浏览器(如 Chrome、Firefox、Edge)早已全面移除对 Java Applets 的支持,同时 Applets 存在严重的安全性问题。 +- **替代方案**:HTML5、WebAssembly 以及现代 JavaScript 框架(如 React、Vue)可以实现更加安全、高效的交互体验,无需插件支持。 + +**SOAP / JAX-WS** + +- **原因**:SOAP 和 JAX-WS 过于复杂,数据格式冗长(XML),对开发效率和性能不友好。 +- **替代方案**:RESTful API 和 RPC 更轻量、高效,是现代微服务架构的首选。 + +**RMI(Remote Method Invocation)** + +- **原因**:RMI 是一种早期的 Java 远程调用技术,但兼容性差、配置繁琐,且性能较差。 +- **替代方案**:RESTful API 和 PRC 提供了更简单、高效的远程调用解决方案,完全取代了 RMI。 + +**Swing / JavaFX** + +- **原因**:桌面应用在开发领域的份额大幅减少,Web 和移动端成为主流。Swing 和 JavaFX 的生态不如现代跨平台框架丰富。 +- **替代方案**:跨平台桌面开发框架(如 Flutter Desktop、Electron)更具现代化体验。 +- **注意**:一些国企和央企的老项目可能仍然在使用 Swing / JavaFX,但这种情况越来越少见。 + +**Ant** + +- **原因**:Ant 是一种基于 XML 配置的构建工具,缺乏易用性,配置繁琐。 +- **替代方案**:Maven 和 Gradle 提供了更高效的项目依赖管理和构建功能,成为现代构建工具的首选。 + +## 杠精言论 + +没想到,评论区果然出现了一类很常见的杠精: + +> “学的不是技术,是思想。那爬也是人类不需要的技术吗?为啥你一生下来得先学会爬?如果基础思想都不会就去学各种框架,到最后只能是只会 CV 的废物!” + + + +这句话表面上看似有道理,但实际上却暴露了一个人的**无知和偏执**。 + +**知识越贫乏的人,相信的东西就越绝对**,因为他们从未认真了解过与自己观点相对立的角度,也缺乏对技术发展的全局认识。 + +举个例子,我刚开始学习 Java 后端开发的时候,完全没什么经验,就随便买了一本书开始看。当时看的是**《Java Web 整合开发王者归来》**这本书(梦开始的地方)。 + +在我上大学那会儿,这本书的很多内容其实已经过时了,比如它花了大量篇幅介绍 JSP、Struts、Hibernate、EJB 和 SVN 等技术。不过,直到现在,我依然非常感谢这本书,带我走进了 Java 后端开发的大门。 + +![](https://oss.javaguide.cn/github/javaguide/about-the-author/prattle/java-web-integration-development-king-returns.png) + +这本书一共 **1010** 页,我当时可以说是废寝忘食地学,花了很长时间才把整本书完全“啃”下来。 + +回头来看,我如果能有意识地避免学习这些已经淘汰的技术,真的可以节省大量时间去学习更加主流和实用的内容。 + +那么,这些被淘汰的技术有用吗?说句实话,**屁用没有,纯粹浪费时间**。 + +**既然都要花时间学习,为什么不去学那些更主流、更有实际价值的技术呢?** + +现在本身就很卷,不管是 Java 方向还是其他技术方向,要学习的技术都很多。 + +想要理解所谓的“底层思想”,与其浪费时间在 JSP 这种已经不具备实际应用价值的技术上,不如深入学习一下 Servlet,研究 Spring 的 AOP 和 IoC 原理,从源码角度理解 Spring MVC 的工作机制。 + +这些内容,不仅能帮助你掌握核心的思想,还能在实际开发中真正派上用场,这难道不比花大量时间在 JSP 上更有意义吗? + +## 还有公司在用的技术就要学吗? + +我把这篇文章的相关言论发表在我的[公众号](https://mp.weixin.qq.com/s/lf2dXHcrUSU1pn28Ercj0w)之后,又收到另外一类在我看来非常傻叉的言论: + +- “虽然 JSP 很老了,但还是得学学,会用就行,因为我们很多老项目还在用。” +- “很多央企和国企的老项目还在用,肯定得学学啊!” + +这种观点完全是钻牛角尖!如果按这种逻辑,那你还需要去学 Struts2、SVN、JavaFX 等过时技术,因为它们也还有公司在用。我有一位大学同学毕业后去了武汉的一家国企,写了一年 JavaFX 就受不了跑了。他在之前从来没有接触过 JavaFX,招聘时也没被问过相关问题。 + +一定不要假设自己要面对的是过时技术栈的项目。你要找工作肯定要用主流技术栈去找,还要尽量找能让自己技术有成长,干着也舒服点。真要是找不到合适的工作,去维护老项目,那都是后话,现学现卖就行了。 + +**对于初学者来说别人劝了还非要学习淘汰的技术,多少脑子有点不够用,基本可以告别这一行了!** From 2e068acd54538be0dfa455a57fb3d7ea2dda5547 Mon Sep 17 00:00:00 2001 From: Guide Date: Fri, 28 Mar 2025 10:41:49 +0800 Subject: [PATCH 48/74] =?UTF-8?q?[docs=20update]=E5=AE=8C=E5=96=84?= =?UTF-8?q?=E8=A1=A5=E5=85=85=20Excel=20=E5=B7=A5=E5=85=B7=E7=B1=BB?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- docs/open-source-project/tool-library.md | 17 +++++++++-------- 1 file changed, 9 insertions(+), 8 deletions(-) diff --git a/docs/open-source-project/tool-library.md b/docs/open-source-project/tool-library.md index 93f2d7306bd..d134cc2318f 100644 --- a/docs/open-source-project/tool-library.md +++ b/docs/open-source-project/tool-library.md @@ -6,13 +6,13 @@ 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 +25,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 @@ -65,7 +66,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 支付等常用的支付方式以及各种常用的接口。 From ae269485b72e744b77fdcf8dc7c08f561b75f258 Mon Sep 17 00:00:00 2001 From: Guide Date: Sat, 29 Mar 2025 08:48:27 +0800 Subject: [PATCH 49/74] =?UTF-8?q?[docs=20add]=E5=AE=8C=E5=96=84=E9=9D=A2?= =?UTF-8?q?=E8=AF=95=E5=87=86=E5=A4=87=E9=83=A8=E5=88=86=E5=86=85=E5=AE=B9?= =?UTF-8?q?=EF=BC=8C=E6=B7=BB=E5=8A=A0=E4=B8=A4=E7=AF=87=E6=96=87=E7=AB=A0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- docs/.vuepress/sidebar/index.ts | 4 +- .../how-to-handle-interview-nerves.md | 67 +++++++++++++++++++ .../internship-experience.md | 56 ++++++++++++++++ .../key-points-of-interview.md | 4 +- .../project-experience-guide.md | 2 +- docs/interview-preparation/resume-guide.md | 2 +- ...-prepare-for-the-interview-hand-in-hand.md | 4 +- 7 files changed, 131 insertions(+), 8 deletions(-) create mode 100644 docs/interview-preparation/how-to-handle-interview-nerves.md create mode 100644 docs/interview-preparation/internship-experience.md diff --git a/docs/.vuepress/sidebar/index.ts b/docs/.vuepress/sidebar/index.ts index 41e1c98dece..b277e1a8606 100644 --- a/docs/.vuepress/sidebar/index.ts +++ b/docs/.vuepress/sidebar/index.ts @@ -20,14 +20,14 @@ export default sidebar({ // 必须放在最后面 "/": [ { - text: "必看", + text: "项目介绍", icon: "star", collapsible: true, prefix: "javaguide/", children: ["intro", "use-suggestion", "contribution-guideline", "faq"], }, { - text: "面试准备", + text: "面试准备(必看)", icon: "interview", collapsible: true, prefix: "interview-preparation/", 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..1a1a79409f9 --- /dev/null +++ b/docs/interview-preparation/how-to-handle-interview-nerves.md @@ -0,0 +1,67 @@ +--- +title: 面试太紧张怎么办? +category: 面试准备 +icon: security-fill +--- + +很多小伙伴在第一次技术面试时都会感到紧张甚至害怕,面试结束后还会有种“懵懵的”感觉。我也经历过类似的状况,可以说是深有体会。其实,**紧张是很正常的**——它代表你对面试的重视,也来自于对未知结果的担忧。但如果过度紧张,反而会影响你的临场发挥。 + +下面,我就分享一些自己的心得,帮大家更好地应对面试中的紧张情绪。 + +## 试着接受紧张情绪,调整心态 + +首先要明白,紧张是正常情绪,特别是初次或前几次面试时,多少都会有点忐忑。不要过分排斥这种情绪,可以适当地“拥抱”它: + +- **搞清楚面试的本质**:面试本质上是一场与面试官的深入交流,是一个双向选择的过程。面试失败并不意味着你的价值和努力被否定,而可能只是因为你与目标岗位暂时不匹配,或者仅仅是一次 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) 的 「技术面试题自测篇」概览: + +![技术面试题自测篇](https://oss.javaguide.cn/javamianshizhibei/technical-interview-questions-self-test.png) + +### 多表达 + +平时要多说,多表达出来,不要只是在心里面想,不然真正面试的时候会发现想的和说的不太一样。 + +我前面推荐的模拟面试和自测,有一部分原因就是为了能够多多表达。 + +### 多面试 + +- **先小厂后大厂**:可以先去一些规模较小或者对你来说压力没那么大的公司试试手,积累一些实战经验,增加一些信心;等熟悉了面试流程、能够更从容地回答问题后,再去挑战自己心仪的大厂或热门公司。 +- **积累“失败经验”**:不要怕被拒,有些时候被拒绝却能从中学到更多。多复盘,多思考到底是哪个环节出了问题,再用更好的状态迎接下一次面试。 + +### 保证休息 + +- **留出充裕时间**:面试前尽量不要排太多事情,保证自己能有个好状态去参加面试。 +- **保证休息**:充足睡眠有助于情绪稳定,也能让你在面试时更清晰地思考问题。 + +## 遇到不会的问题不要慌 + +一场面试,不太可能面试官提的每一个问题你都能轻松应对,除非这场面试非常简单。 + +在面试过程中,遇到不会的问题,首先要做的是快速回顾自己过往的知识,看是否能找到突破口。如果实在没有思路的话,可以真诚地向面试要一些提示比如谈谈你对这个问题的理解以及困惑点。一定不要觉得向面试官要提示很可耻,只要沟通没问题,这其实是很正常的。最怕的就是自己不会,还乱回答一通,这样会让面试官觉得你技术态度有问题。 + +## 面试结束后的复盘 + +很多人关注面试前的准备,却忽略了面试后的复盘,这一步真的非常非常非常重要: + +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..4e16fc7e0b5 --- /dev/null +++ b/docs/interview-preparation/internship-experience.md @@ -0,0 +1,56 @@ +--- +title: 校招没有实习经历怎么办? +category: 面试准备 +icon: experience +--- + +由于目前的面试太卷,对于犹豫是否要找实习的同学来说,个人建议不论是本科生还是研究生都应该在参加校招面试之前,争取一下不错的实习机会,尤其是大厂的实习机会,日常实习或者暑期实习都可以。当然,如果大厂实习面不上,中小厂实习也是可以接受的。 + +不过,现在的实习是真难找,今年有非常多的同学没有找到实习,有一部分甚至是 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/key-points-of-interview.md b/docs/interview-preparation/key-points-of-interview.md index 1d710456b86..c2101dc307a 100644 --- a/docs/interview-preparation/key-points-of-interview.md +++ b/docs/interview-preparation/key-points-of-interview.md @@ -1,11 +1,11 @@ --- -title: Java面试重点总结(重要) +title: Java后端面试重点总结 category: 面试准备 icon: star --- ::: tip 友情提示 -本文节选自 **[《Java 面试指北》](../zhuanlan/java-mian-shi-zhi-bei.md)**。这是一份教你如何更高效地准备面试的小册,涵盖常见八股文(系统设计、常见框架、分布式、高并发 ……)、优质面经等内容。 +本文节选自 **[《Java 面试指北》](../zhuanlan/java-mian-shi-zhi-bei.md)**。这是一份教你如何更高效地准备面试的专栏,内容和 JavaGuide 互补,涵盖常见八股文(系统设计、常见框架、分布式、高并发 ……)、优质面经等内容。 ::: ## Java 后端面试哪些知识点是重点? diff --git a/docs/interview-preparation/project-experience-guide.md b/docs/interview-preparation/project-experience-guide.md index f2e93df82b3..0546626f889 100644 --- a/docs/interview-preparation/project-experience-guide.md +++ b/docs/interview-preparation/project-experience-guide.md @@ -5,7 +5,7 @@ icon: project --- ::: tip 友情提示 -本文节选自 **[《Java 面试指北》](../zhuanlan/java-mian-shi-zhi-bei.md)**。这是一份教你如何更高效地准备面试的小册,涵盖常见八股文(系统设计、常见框架、分布式、高并发 ……)、优质面经等内容。 +本文节选自 **[《Java 面试指北》](../zhuanlan/java-mian-shi-zhi-bei.md)**。这是一份教你如何更高效地准备面试的专栏,内容和 JavaGuide 互补,涵盖常见八股文(系统设计、常见框架、分布式、高并发 ……)、优质面经等内容。 ::: ## 没有项目经验怎么办? diff --git a/docs/interview-preparation/resume-guide.md b/docs/interview-preparation/resume-guide.md index 818274601e0..396ef4b47e4 100644 --- a/docs/interview-preparation/resume-guide.md +++ b/docs/interview-preparation/resume-guide.md @@ -1,5 +1,5 @@ --- -title: 程序员简历编写指南(重要) +title: 程序员简历编写指南 category: 面试准备 icon: jianli --- 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..ae8a4bf6f18 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,11 +1,11 @@ --- -title: 手把手教你如何准备Java面试(重要) +title: 如何高效准备Java面试? category: 知识星球 icon: path --- ::: tip 友情提示 -本文节选自 **[《Java 面试指北》](../zhuanlan/java-mian-shi-zhi-bei.md)**。这是一份教你如何更高效地准备面试的小册,涵盖常见八股文(系统设计、常见框架、分布式、高并发 ……)、优质面经等内容。 +本文节选自 **[《Java 面试指北》](../zhuanlan/java-mian-shi-zhi-bei.md)**。这是一份教你如何更高效地准备面试的专栏,内容和 JavaGuide 互补,涵盖常见八股文(系统设计、常见框架、分布式、高并发 ……)、优质面经等内容。 ::: 你的身边一定有很多编程比你厉害但是找的工作并没有你好的朋友!**技术面试不同于编程,编程厉害不代表技术面试就一定能过。** From f63f39f068a61d20790f14b1e8771b26322ea685 Mon Sep 17 00:00:00 2001 From: Guide Date: Wed, 2 Apr 2025 15:46:20 +0800 Subject: [PATCH 50/74] =?UTF-8?q?[docs=20update]MySQL=20=E6=97=B6=E9=97=B4?= =?UTF-8?q?=E7=B1=BB=E5=9E=8B=E5=AE=8C=E5=96=84?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- docs/database/mysql/mysql-questions-01.md | 6 +- .../some-thoughts-on-database-storage-time.md | 97 ++++++++++++------- docs/java/basis/java-basic-questions-01.md | 2 +- docs/java/basis/java-basic-questions-02.md | 2 +- docs/java/basis/java-basic-questions-03.md | 2 +- .../framework/spring/ioc-and-aop.md | 70 ++++++++++++- .../spring/spring-design-patterns-summary.md | 2 +- 7 files changed, 139 insertions(+), 42 deletions(-) diff --git a/docs/database/mysql/mysql-questions-01.md b/docs/database/mysql/mysql-questions-01.md index 1f225d3702e..4878eee56ef 100644 --- a/docs/database/mysql/mysql-questions-01.md +++ b/docs/database/mysql/mysql-questions-01.md @@ -158,10 +158,10 @@ 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)。 +关于两者的详细对比,请参考我写的 [MySQL 时间类型数据存储建议](./some-thoughts-on-database-storage-time.md)。 ### NULL 和 '' 的区别是什么? 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..e22ce2800da 100644 --- a/docs/database/mysql/some-thoughts-on-database-storage-time.md +++ b/docs/database/mysql/some-thoughts-on-database-storage-time.md @@ -3,30 +3,43 @@ title: MySQL日期类型选择建议 category: 数据库 tag: - MySQL +head: + - - meta + - name: keywords + content: MySQL 日期类型选择, MySQL 时间存储最佳实践, MySQL 时间存储效率, MySQL DATETIME 和 TIMESTAMP 区别, MySQL 时间戳存储, MySQL 数据库时间存储类型, MySQL 开发日期推荐, MySQL 字符串存储日期的缺点, MySQL 时区设置方法, MySQL 日期范围对比, 高性能 MySQL 日期存储, MySQL UNIX_TIMESTAMP 用法, 数值型时间戳优缺点, MySQL 时间存储性能优化, MySQL TIMESTAMP 时区转换, MySQL 时间格式转换, MySQL 时间存储空间对比, MySQL 时间类型选择建议, 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 +54,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 +76,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 +93,7 @@ set time_zone='+8:00'; +---------------------+---------------------+ ``` -**扩展:一些关于 MySQL 时区设置的一个常用 sql 命令** +**扩展:MySQL 时区设置常用 SQL 命令** ```sql # 查看当前会话时区 @@ -102,28 +114,26 @@ SET GLOBAL time_zone = 'Europe/Helsinki'; ![](https://oss.javaguide.cn/github/javaguide/FhRGUVHFK0ujRPNA75f6CuOXQHTE.jpeg) -在 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 +142,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 +151,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 +161,26 @@ mysql> select FROM_UNIXTIME(1578707612); 1 row in set (0.01 sec) ``` +## PostgreSQL 中没有 DATETIME + +由于有读者提到 PostgreSQL(PG) 的时间类型,因此这里拓展补充一下。PG 官方文档对时间类型的描述地址:。 + +![PostgreSQL 时间类型总结](https://oss.javaguide.cn/github/javaguide/mysql/pg-datetime-types.png) + +可以看到,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 +192,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/java/basis/java-basic-questions-01.md b/docs/java/basis/java-basic-questions-01.md index f8ac99bc8d7..47ef386848c 100644 --- a/docs/java/basis/java-basic-questions-01.md +++ b/docs/java/basis/java-basic-questions-01.md @@ -6,7 +6,7 @@ tag: head: - - meta - name: keywords - content: JVM,JDK,JRE,字节码详解,Java 基本数据类型,装箱和拆箱 + content: Java特点,Java SE,Java EE,Java ME,Java虚拟机,JVM,JDK,JRE,字节码,Java编译与解释,AOT编译,云原生,AOT与JIT对比,GraalVM,Oracle JDK与OpenJDK区别,OpenJDK,LTS支持,多线程支持,静态变量,成员变量与局部变量区别,包装类型缓存机制,自动装箱与拆箱,浮点数精度丢失,BigDecimal,Java基本数据类型,Java标识符与关键字,移位运算符,Java注释,静态方法与实例方法,方法重载与重写,可变长参数,Java性能优化 - - meta - name: description content: 全网质量最高的Java基础常见知识点和面试题总结,希望对你有帮助! diff --git a/docs/java/basis/java-basic-questions-02.md b/docs/java/basis/java-basic-questions-02.md index 2abc4748ed9..9f8739f291d 100644 --- a/docs/java/basis/java-basic-questions-02.md +++ b/docs/java/basis/java-basic-questions-02.md @@ -6,7 +6,7 @@ tag: head: - - meta - name: keywords - content: 面向对象,构造方法,接口,抽象类,String,Object + content: 面向对象, 面向过程, OOP, POP, Java对象, 构造方法, 封装, 继承, 多态, 接口, 抽象类, 默认方法, 静态方法, 私有方法, 深拷贝, 浅拷贝, 引用拷贝, Object类, equals, hashCode, ==, 字符串, String, StringBuffer, StringBuilder, 不可变性, 字符串常量池, intern, 字符串拼接, Java基础, 面试题 - - meta - name: description content: 全网质量最高的Java基础常见知识点和面试题总结,希望对你有帮助! diff --git a/docs/java/basis/java-basic-questions-03.md b/docs/java/basis/java-basic-questions-03.md index 7bc956f78e8..d98297398a4 100644 --- a/docs/java/basis/java-basic-questions-03.md +++ b/docs/java/basis/java-basic-questions-03.md @@ -6,7 +6,7 @@ tag: head: - - meta - name: keywords - content: Java异常,泛型,反射,IO,注解 + content: Java异常处理, Java泛型, Java反射, Java注解, Java SPI机制, Java序列化, Java反序列化, Java IO流, Java语法糖, Java基础面试题, Checked Exception, Unchecked Exception, try-with-resources, 反射应用场景, 序列化协议, BIO, NIO, AIO, IO模型 - - meta - name: description content: 全网质量最高的Java基础常见知识点和面试题总结,希望对你有帮助! diff --git a/docs/system-design/framework/spring/ioc-and-aop.md b/docs/system-design/framework/spring/ioc-and-aop.md index 749d267cc95..e58f40f81af 100644 --- a/docs/system-design/framework/spring/ioc-and-aop.md +++ b/docs/system-design/framework/spring/ioc-and-aop.md @@ -210,14 +210,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 生成一个被代理对象的子类来作为代理,如下图所示: ![SpringAOPProcess](https://oss.javaguide.cn/github/javaguide/system-design/framework/spring/230ae587a322d6e4d09510161987d346.jpeg) +**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-design-patterns-summary.md b/docs/system-design/framework/spring/spring-design-patterns-summary.md index dfbce7e8a32..e4499b00f2e 100644 --- a/docs/system-design/framework/spring/spring-design-patterns-summary.md +++ b/docs/system-design/framework/spring/spring-design-patterns-summary.md @@ -142,7 +142,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 快很多。 From 6042bc328677ce4d118c80c02aaf2f5f72547e5f Mon Sep 17 00:00:00 2001 From: Slade Date: Sun, 6 Apr 2025 20:13:28 +0800 Subject: [PATCH 51/74] =?UTF-8?q?[docs=20fix]=E4=BF=AE=E6=AD=A3=E9=94=99?= =?UTF-8?q?=E5=88=AB=E5=AD=97?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- docs/java/concurrent/java-concurrent-questions-02.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/docs/java/concurrent/java-concurrent-questions-02.md b/docs/java/concurrent/java-concurrent-questions-02.md index f72935a6201..f3bb411a682 100644 --- a/docs/java/concurrent/java-concurrent-questions-02.md +++ b/docs/java/concurrent/java-concurrent-questions-02.md @@ -475,8 +475,8 @@ synchronized static void method() { 对括号里指定的对象/类加锁: -- `synchronized(object)` 表示进入同步代码库前要获得 **给定对象的锁**。 -- `synchronized(类.class)` 表示进入同步代码前要获得 **给定 Class 的锁** +- `synchronized(object)` 表示进入同步代码块前要获得 **给定对象的锁**。 +- `synchronized(类.class)` 表示进入同步代码块前要获得 **给定 Class 的锁** ```java synchronized(this) { From ff77b0cabf58cba007148a7c804ed27f681a16a3 Mon Sep 17 00:00:00 2001 From: Guide Date: Fri, 11 Apr 2025 07:23:35 +0800 Subject: [PATCH 52/74] =?UTF-8?q?[docs=20update]redis=E3=80=81=E5=A4=9A?= =?UTF-8?q?=E7=BA=BF=E7=A8=8B=E9=83=A8=E5=88=86=E9=97=AE=E9=A2=98=E7=AD=94?= =?UTF-8?q?=E6=A1=88=E8=BF=9B=E4=B8=80=E6=AD=A5=E5=AE=8C=E5=96=84=E4=BC=98?= =?UTF-8?q?=E5=8C=96?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- docs/database/redis/redis-questions-01.md | 8 +++---- ...4\351\200\240\345\207\275\346\225\260.png" | Bin 20107 -> 0 bytes .../java-concurrent-questions-03.md | 20 ++++++++++-------- .../concurrent/java-thread-pool-summary.md | 10 ++++++--- docs/java/new-features/java24.md | 7 ++++++ 5 files changed, 29 insertions(+), 16 deletions(-) delete mode 100644 "docs/java/concurrent/images/java-thread-pool-summary/threadpoolexecutor\346\236\204\351\200\240\345\207\275\346\225\260.png" diff --git a/docs/database/redis/redis-questions-01.md b/docs/database/redis/redis-questions-01.md index 6493ebfe168..7102985b9a5 100644 --- a/docs/database/redis/redis-questions-01.md +++ b/docs/database/redis/redis-questions-01.md @@ -36,10 +36,10 @@ Redis 没有外部依赖,Linux 和 OS X 是 Redis 开发和测试最多的两 Redis 内部做了非常多的性能优化,比较重要的有下面 4 点: -1. Redis 基于内存,内存的访问速度比磁盘快很多; -2. Redis 基于 Reactor 模式设计开发了一套高效的事件处理模型,主要是单线程事件循环和 IO 多路复用(Redis 线程模式后面会详细介绍到); -3. Redis 内置了多种优化过后的数据类型/结构实现,性能非常高; -4. Redis 通信协议实现简单且解析高效。 +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) 协议。这个协议实现简单、解析性能好,并且是二进制安全的。客户端和服务端之间通信的序列化/反序列化开销很小,有助于提升整体的交互速度。 > 下面这张图片总结的挺不错的,分享一下,出自 [Why is Redis so fast?](https://twitter.com/alexxubyte/status/1498703822528544770)。 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 6e3c7082eedf987b3a247caaf3fb81c752c15676..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 20107 zcmd43byQrz(l0tQ4DRj;GPt`0g2Ui2xXS>+-3h@pxI=*8?l8E!1$PUcpaBwykdRAq z&OPUR-&^mW_pNpB>)oqYbye+O*VL}AW)*wp_uB7W0G^V(qC5Zy1OR}K7vT3MARK`4 zw_##oVdCQ9;Ng-G5fT!S(2-F*UUYmk3^b3Ok3)!^g^s)>$x`FWLDzB)ri)oE`=XeiTYoi%}wOU%hNP1y<-7R`uuC zCt|XpV&|i*$6sgYKh0>f6#M}Jvn;pTsuQxUE}3cgwN&tl4k<7Zx2|zJ#39>tDsdTW}gLzIhzQ=^F2@7X1Cw^Uj*qq z6N?rW5i0-^RxkjI7XTQd`OA+Gt((o`=*vtEnC;71Fiz0$)j#Gk(Ent@V#emD`E5(5 z^;6EPn?eJ*kTWrXulkfq8z^`4q8{mr&jJI69av|uArC$LpI@cg27VXkeng~|JF^Mf zoK(Ce@<|u?$sZ5rL@*)vC%6y2qL_?0y?k^!>W|J&;da@%z9t%zi#(TNcfY=Vi0DOp zzgWrlR8CK~5|h6i-S)11bK3tH_k1#OUc)a>D9joz-@K6{m#*?|{==ru!0js|D#jnPLaOxfx17BP z1MBjxt#wE66726kp5XbF&&|fWc~H(??aY+hQJ);Chw$NF*!dr1e9a*30092sa=ZYg z7f9^9{dh-Xsaf}5q@&z<=@sD&W}fqruA}a9DOIf~0KQNm(}F+I;vFEJTDEBgAbX7Q z{&4^jioO*91x22mj2td(1;p^O&bRQq;SI#{>op4RzFz3vqtMOS&{iEr7cU)24A6|X z`J}jtu>MK*WU=4O1)s-cXCY^>^Eh^*rYeSq{eo_8YD}%y`8S|NF}d#L2bB{+qS)6n z2BVb3ohCWxNRo2?=D(6v-4uE=v{_XzkXN-t@h|L?DJQo6f3VLXnEGPSD$Qf zRX%RjO)=S{q(4vbU+u#S9GAyrK?eNW{>6SI9#aMQZ>9j@J%>D6-%+2G{~chqKGI;mCUgo} zlAW;trX?CYTOXA%f?t6QGN6#MEK3U4diDPQ#eO8vKkP^PW54$w3jqLBF}v_>Yo)(5 zW=2O#!CwlP&~+UFAP8>?ZVCnz%nOFem8)z69&jHO#r&K$3|E>Yx zlz6n06Au7-Y(Ovwfby7}A0!C7rHu0>3}tc)We(@%Ed@Z-5@xMeTVOlzvUg=q}*4Jonq! z4m&wu7d_1=yku%`u%$L1RE<#;#SyQwtGAAuDOdJE=H4`q&o>^UzPC?T7Mk8y~eI3x-cl5 zUldC0L3|qS-Dsr`1Ptykr>xX*-MT+)^p4B>+bp`j|7*9nL6Q2T?W|F_b&8}%+Sl%r zM3<9u4(TJ!BO!|AJ~rwZqJ}eat-4%=VvGtgO|T8!Ww&#V_g+jZGIx%>5uTZ-^*JlwR@}M|{6;^=f1sQZ%5@Rm`7<4E{`Zu9-(a{o<^{pOrTnGo3BiY?yr`24MB+yYf|UA){rX@XgqP7|6+WaNleC$Il5%ZxKDf*mB1 z0f%5Q6Fj+m4o1Dn^|2)ZG4|wGF-I!!7=n(C`3Ba?Oj55WwpS$=!QuT-I11UKB$QBO z^DoAo)AD>|^q@CuPt&l(;Bu241r6^9OiA*Ai&3f}V+IiveKHLQi8%)f3J&j@Ryfq` zN7G|$PW(+MPyfW`3-@S-_HLVEp%T`xW;VuE7KNfyij>O?Uc6sP55EFqG3-~Cgvj$o zs-4~@l-%lXZyLER(|fia1FC&pF)H;oVd2NX_|DU6ak5g5z>&CGArYiykr$*gu*1#N zJ9!npwH1tokvGRS_z4Ch!(=!;Lz>06HQjcuPNkP zawvr!21zEp^Uf=~Gc}{7R-ahDrB+Rjsk*A<^i;0VG~Vx72?&$UMP%jI>7{nRI?l5( zSc6o5I-c~Frp^bJ*|&KT=bPL*TzC$B4&nGH0k8`xhCOUWZsPZvL!T{0GI>tx^zzr_ zur~KF=hL2K+oNp+1HPPDp;MVJ-CZV^mTk7Z&57h_R+j4YxU?>?dt-O$Ex(bsKK_us z#M0(`eD-wBlxy+FD;Um2zDOdbjbp*eo>0r(C)&=_#YloLTSSXk{Cx5_PT>cyDzKbj z`g}9fb5mB01P?~9-sJ4_r63bX9-b~^aN@cMXzbLU+Q4*a`GS^7@0RbNt|9Mtdh#Em z3^CIXJ>{{p+xeELkHsb!+Xc2FD@}C_z197;%O1NMr-+u&)WVi*fzkoMu|#8IOJ4<$ zjiSlIIB|3f8j7niy(y+$?2o$Zo*@%TT1nBXS#kYgzIy9qi-*QWguyNCu&}}$ zNG++eQZ13}RuG&`FSD%Us4ghbf}g7C;dk}==z9YKF+wybRgz^Z zu;|74>^fX<#p59PUDoDIaZ?h}M)?9Z?XNkWbdEC#?h>lGt=$5e(a#LiZaWnG7c?63 zs(16O&kw<^(_FfYx%r?~g;Y>ReP*!98_HJ43=b5@cJz^qNbE1abHX9BU`k6rxaAzu zDY>MsJ##Vksb1{a@s@ypYnb>XiQD=T(6l$JxVFlnJ;8=bz`uh6{Uy6+EDfKk3PY<7Rqz} zRwJo|>{uXif3dDR>CzVOqsHOj zhjp$Sy&)uTq-@_qPTid7wjz@{J{KeBc}Qs?!NQP5JH}{Tli-j(siJ?u7_U`*Kew0- zX=}(j>#*f<5YqlI=YF7G|A|p5l%DSND6kxr$41YUTXba(US=7l+L`C1CR;Bl~+R~h#lc6R_{~c^x<6}i9wv-X0v1TShV|96z zEuze0&p}4=B2#6e%-#uyP4>%?$VOgV(L3eV8^QN(Y_BQGQtUll20J#A>v`1Hydu7Rhhe`V`n~lV8%LRcUYoj9)d`4~Sgr+Vn!obpGQ+XEQ4^H!#A!IeW8IuX zoUs@0`2{2D;EBqSj0(w)Yh#UFIhNPJ$x4Cli`Z2|H7_^Mom|EdJ(kgi4O#mzhszoZ z;()?b3|_L!mR}UE@GtN99moeqQytg8?*9VL=#OIRtgov2Rhy~ozgNH6Tx_}b;7qG- zs>>-csERbOoa75_t)V7=zJFiyWdS+^owh8mj`}|G3*m7P**25PSr&kH-}7VfU65)} z+n@BRY`OmS+pj`ir$^w7%Y}k^ z>3|QcYgz1}R(OtaM{b>%c1y;xy6!8V$XTC=8!1VDdJXoTzp-OSpQrF`85FFGS$`3 zmRrJs^uQQj4Z0?`&Mn?Lt7G|g3mcgIgl zZ(dwU&9AXCMw|zD>AjiD=MJnRAO%=(<5NlOW&Y>+b0C@)!XbApAkxLN7~j~tHX-~f z;K{Z3+IKlzD~Ucc%a2t()@*Yr?}=3x9bVUj>N>Hq*aLbA2DTNp-kw&mZHn?w=b6wV zP9GRO69$|%5j$Vns``YotHqV)h7=QWy8-~DDzJ5RjW^nHHMVpc9Q^P=l8xqNgkgZ? zx}yG2p7w%*b4mTmkBDENH4RQN<@IkB?ntkzhVuiKce}ruW472_^J^xc6VM+t?Zl$ zvR-YeKX!;T`$>*!+Isl@n{u2s4{U>Cl*JGfNZz65-^O*lS2gKFv2vQ&^$~_MPR<^F z-Ui*_geFTeohU-sz+J*88W=-M_MC2^IrA|k=wZiA3(K7~n?w@&rvBO*=Jx%bQ?V?` z!$gr6Jqnd70u8TIEp+PA*7fwg^cZy4Osb9Wx~TO6yE;3VGgVBBDU6J1<%cVxyq>xT z>gWlmi@L;DsQ%KESev6Ea>RHYUJROe(llnbfw~+{C=oRixKLbW`b+*KQ%dK|&NRZ( zF?W?0Z>F22BF&}&vBwGj& zj+Vd^9yG*tQGFi%qRdH%<6ivlR3_RT0I5BmJDVz#NbPN(K*VE@OQtCDXK9S6Fa zK-EfJ{r>YLH;IWJX*!d6WlgFHYBhocn(*hx(?+$ezKW~w-u{f0LAuDp{MOf|P~cy{ zfV4Q#X5D2Mlj9vGGJip9#>Z2!^X}ym%7Ij~)eqcC2DBz3m_sr6_}&YZY1N?6}( z^=6c0gUm}uA?yp0jWv~;c**VLRT7Y)2xFC6+Sl4xoP3rJ8|5zIm{(?@rGs$YPuNkR zHSF�$)%B2uVKcm^GuMfRs(#zOH?>ZY#jMNF)`-}uQpt%f99283H8oOoKyG+LQk zUS?JZ7VO-k^vG>XU1JPK#OD#wP_DcNXJyws#ZrgtH97PA+;DIeh}&1@C|aIS>?&r3 z4bIG;i7$w`_c;kxJL(ha&J09_)^DY!0=x#B#MjxR2y#4~)ws9o!Bvq9x3IeQhC{%u$A(H*2tik!&^avvih;AIH-j@c*V-D{t{ob(*|VpxeE2}MW7r66FF|RqCj++ z9jQ8%UjrN1#>Xbpnu~}~qRB!y+mr!U<=1cDw{4oY`pB!{l)hY~t+A>Q*$IiM&LANk zru(o>Qb{q_i5a|>ku|5hT-mZe6vx5-2AaSwp-F6bS}BUFak0lzQg#4Qy?EQ(V`(WI ze`6Tn}NXYaYx|w#g}1;7uaT>t&YX_1fx0f`3!bd46P+nmxvJs2zHniE56zcuy+jCj%Ps}=kN!ppMLkwj-=*Oj!Z8V ztRxf{oS90=vd{g#^d^B$wgLzvCbrcNLAbR;82ALFlZ)W)`~NKRqDuhCmk2e_Lf*do4G4IU zE*||_wezcX|KZzfa>xsN80Aue>BzUms)wu_aVE;nU9{R8;k@QX>@>41-ip!lms{guTb+-M-_vhG9^xpuIAkxLD z4Wp!!pY6-9ivRFgZ;YEQ-at6H)g;Swy+jzaE5ri`9DW1%%9zKGjDuapx&>#o%#G_o z=6y{2g@91Z?*{W>q+XLBu#*VFrKg5M8-a=$#=}pNuHN{hO*)meLY2IRx`)78?DKNw zUu{pEI9jE>kJzcaEmWO-GT_lNoKX>-Rv9}rCSsDluQNOK$)ES^A0^@C)q#T{NX&y`MZkO(lb! z2aNCbr9Ny0zAWSnK7!8Z)%oK?_XWPEDE{KzfkBaYEP-SbH#S?67GtVj5CO3qldkS? z{3O#bXj}WS{5c*Y3P6}f=V*Bt7d=YRz_lWP2t)CgVO^c``i1Fw_=u~eB|4d$VP99? zg*z=llQ{}39IvWnTH{&6CizsO$3t12v+jluud02iZ(hvAjCKRIo}TXUuB98g2Bd$9 zu&EK1x`Ig4?nH+as4IFMaLM@uLKaR$J)l2O>KUnT88=!EAnQX)m|?vxcA^GRNCp|=$n1!ci{)g{>qpmiRfD~L9OgDZ278zXG$XdeW^L$9>j(BUA3@XA5} zR%yJfk)M-79II>w`G~Q2MFAd)uAK*B4a&H`0aR6ejdW!*cqBB|G5tsq*`KpQnV@{t z_>&J4$wzeAcr4ba3^D-(66{Mka%5Aj=wqoBX_cxp8? z$^Aod&jl+fNZ_Ha1y(sC3L29^(!AKu$Jd9<<92tyJXg_CW(^?V9Yca3h5TCo8KllY z+XFp1H;WgoKS_LkaHdc(V%)?|@^MeP5}enZ8@5r#i}r-3MjPY}&OJp2NVh`iX}-{9 zaL4@yd{oaIT>Mq5rzaNwD!D1!NIGRwqf+HePm4UL#-X@9!}<2!_)4 z_#NT0eZ=|9nx)(cGp;k9I!G z!eiTIO={TJ9x!ofjuWZNdh{Nbl*E;vIM9uHyLNb=-$0+0>2i8Ij}YBSgWx)RX*@4x95U^h*VH{N z5&1S~NT?I$?_j*5Rq0m3^cNUCU9zEM)5=t{WYQWbLrQ+KslE!&_7!B=F|?{j?`9?^ zrLZuTK0`EpO}ZOP_{12d2x>->DrxN>ydz3`WavcX^MzxyA+@F|4R0wOJb03_L2ftU z@4E!dx`s$gv1Y!kocuVx6w@a>l`-ptg_V7!!*$O9f z@?p^#YPDk1$DBS^-rHYk04-=;hGha_A zKk=nm`C$;V*n6rpelp452Q(!ChX_-1a_C=>M!P^L(*2~tmW6kf z6jW1|i9#O;o)f?QSya}Z`5S0k+NwfP6@Rm~Bn)8)=&!g`3UW;@tz4GgEsGc=;z6NdOflgMNpbI4MzGzT`j#p># zv+_RA=wZkD)j7GUDv>&Abe8+q6lPh4ei<}XbT@flv1d}*7sUh<4?*t5-&rcn51$Yz z#b~#?^|=m~W*LoVN~LdBreP)RnvZVbkNdCG)1Bvuh*@v;lX#w^0x<&~<=CY`!c0YbqS36#t_l^dmu{GMfj(|ZkvP4@B zsH1HT5Cs6Lg-P~$j`1rz@-K8<1qR^;arVaWW{OZILI49+VOTIX(uC)C*)iu)O*NM#)05t0coN2&I0h}I>*_rQ_wNTfNHquJLV zwoo6&vaG2+mi>tivEK_y5+Jalk$s_=1~!i*v0J2=Q zDJUB4K_KCj%w4N1IM+MUn!uoy!jl`@62asuGg*PuAiky(-=3ZO3`(zY6f&b&4aFWz zNXZ&VZAJ2v@9sS5McKGN%U^~&Uw4=3%Eu@*zxAexBdNAc))@+}0AM}-QO=5%HBJOo z0_k(o~U`-C(e0mX@u;Xltj@N$-ZW4~e-|HRBA&wuJ1WmN4)5~F06#xo60YJ4uA$l+4K*Z0b!zYoQz+bD6M;w=(vrPHTfzFRV2iybKAYSQL8kr+e(c=}|6G+lvq_vjHRD(*KF6M)4tlv<}D>RCa zC_ed454eM1VOWO8Ir1d`hzn&K<=lB@3Mxziw(h2o)mRpE(D}tZFDXL%x)xjlq9|y` zH=Mj`DV4{n*su+zjuEhP!yaa)>fgg`a1Kc;=3OG}zw1P&^V0R0`o=S=PhCZRA?@q9 zNLH%NWZutajfv40@YalsSN@D_$<5gSgH;DHhsf6?(qku$$;##W(2nH~k~7>EukDT$ zsngVpC!-RSG9TtJwmT_`9$wQbn@Tn1Vu~4zkZB@&8to%S%fgVF;0F2#JW7g7k4xdJ zl@c{(-$!zmzRA{iBr;F0HrPu!NYzQzN}xfXkhjoHQPLaMT~IlB$o8tHoY(;C<;vn+ zI|^dBAJ~iy7Up|!bo;Q*N1%_e^jjL&nuxh=`^G8|u(ILMjYx`oUoM@G-FZb`HfS3~ zV|5flS5vYdLdV9a^&8;oTz)|yqkVzN8V9X_u%)0#Hv*Q^w>~*meRgBD&V&&+DjXUqBAP{U}4)nJo6*z zWDR5VK)tdr-%zxK#tw8B2L#$>=#)syzSAQsBk83Yb!$F5=8kg23f_uc-p}Ox>b~*8 zcEA69r6jQyuxjIc2z#O}MI*Q(wH3IwM7TNORCe92>(1~o!gmU(v7)S{D=a)ci&k`e zPX<)F@)liep(dQq1i3+s*}$5dx?7CIP|C&+6PgrNvY<1y2OrI)_kspbz1*4etyP)phpfijtQ zD)kda46yS8X_8nV=w7_CVMw&O>Esnmwj^#yTYzq57SV$vSZ2HRx_LA?gT)rz;eNM8 z_j8hFUYv%|*1v&6(k+iLnT?xXfa@ZvHUblOc z-Ys+x#Wf0i%UXaDd64L%C>h-~I58KcJ@HXam1Bsulc_`>Oc?IRoJF; zO>zKKvMHG7d|buGVcm5owdn+m|dICxhe!cvw4LKe4ts@(hlYW7fm4D-g`&Rq->3 zPA70TSYj*@APeb=g+%k=0ps8oc-T@eoNxAo3^p9uJmHb?c1~q&&7H@H1h=M6Zrb8B zV(}S@nxCQz$C&xnu~gA-ST&+ps}yOLynD;GOQ&sg(O^<>sB^iCn{P6%WqTyaL-A^A zi(wmWHFPircVX)}T6#1e!z3}5y#~X?Y#u%WRmS|NVf;H+>2B7Mw8FAz@vWv#0yTht z0(qb;UICU7^G7HuhG^wRHqtl|XdPf4CdcDN!47Al*JkxDNziM#T6E%dTo=Z0&w}Z% zy!PN|)EkwKvMvPIt+)=byLZX+;kZufm>t37F9^4lc?QS>LiNw|1m~N43p-*;87zlJ z=~zF7m9i7mPTwU(CEC%Ug}hFG$+`QoBEBSM?nM`fB{e|ADeoR1ZJ<2S?g-y;!eZ|ewSCaymtks; z$FsLK^{;VMZ|+6zw_Bi08hJvEr@{mU-3_!3lx^#-iz4Hv9Kw~_GgeOevDp=hcg>yS)9vwly4D0*Vs6;i z%R~Ln^R3hrtOy1VFbqO-h0CM7uU+&9FwGfxVtxBN6PDo zcJdldnUqZ|_cK}xjia(IHNC(iOZ2I(MiC|?Kn}efohSKaf2`Hc{KfkH6NZ5iF>_-| zx@-vk$>Da@;NWLlusZd7NsQb``T5hGL49$u7`im#1XS{=(JTwo96doiBekYyG#j`t zV8mTZjn6iey>HUTv1g1ZmIO2LdK%(kA1Xo~>_hZ<_&JW=p)YlR?5~itGZ}^{@P9Us zxKHg6`Tp!Tz~w=rlrHRN1YKDskQW6raHCcIt=sKy0O@bQ)5Qs>3rr+{^gylp2mDWw z2B1v=pwdF7o*2QCx}!_$KoX6!dI*DHT)kQLjdeetvp~8fU(r$;cq)t?>`&!2AkmQ6 zjtGd0gKN{^>-q@pUwAm**y{Xni7W2Qg|!uF+krwdwycmRh%MZlHq(Z^AB@$00~&rr zrPG;)ALZ`iK68lC;mla;q_JMh<@r?gB<|%~-oc6mEd1c_V+(~jCMcdU-rm0eH!gj) z(}k}76|{;q>G9+<&zRpK#o!yiXe9sjn&pEI*6c7GS&|%b!>bo*N~dxShR7eri06iT z4}PyEf)zxJzM98i(eIh+EaM5*QO^CYz)xd>>BSlDJS_?9S^9d@+_@L&hLtJd4c8SP zy`OWs__FzY%q=;Q!3Nq7VdPsnD1%nmWJ=}HndJ3JiY+KIq<@KJ?&tilbpQc8OL5cv zY%~)~8EfLMcUX!ElCU2<-iM=SMXQ)2qnef}C_%>P4z0}ZXzu9mfGk)p^zh2TxLgey zXzYP_^gA~2%UvOvxq~|N&!Gl!sSxI5hsGDePva^o7(#8y(CIQwGA+_Yr2E4(-f@PB z%62__73|kKO0J?l7sm}qK1^(H{ipX$pL&7SVn5zwHT2(-p`8a=e2EoU-vsXl(P~Vs z&@*xNd2>rHwM|WP@8%T-_9_8|ORW`wG=hA8mp0(DZ6xidmL0>aPvccCN-gnacUvQ1 z8B(BQqsO&;tcwg7HfLGlxH~p8fXfO@VG7=nexpW6f4ARk9vC% zq9;hIdr2E#goqxp4{ycn(0ldWKOdzT*V1@xcy5@^dw7()=9d?l{odX~*;_HPy_u=& z>ga59ghE>koF&p0fjr^8(fBK97Wj5^>7wEI=;xOLiF&NhKO7%E=k*>$eO}0A!81DF zPDnopTH#9DpWpXUOnlInLNhI`+&4eC&BOAYDcEwloM>Mf`wiHuAhB$aD?6b5d*) zHm|hXk5;uA^8`02etcPR%_y*=D(w!E9dggO>jrbv0}2MUlnE-{bD|IOd!tY4YYB(( z8av{h#yee7rZ;nIRmv`L31b+dw&!ZXsYlG#2liR^u`~@q&au@;(t^8L*(@*GuPy-G zM#@7>r$!%s18j)B@9M-DVu^LW$D!b0gIn5yx22eqf5-@m7B_2hVD&9XEIG0Ygn}}h z>7#9{W|T5CdIp#CiN2{UifIOh(GL%nHrke4DMz`%$z`{Eo^kusitDR?TX>uJ`ZZYn z;ib8NvIBLDQT)c~bEu!-J#h3yS^UA;!*Qh}xXhbg;l(`~s$6xNpQ&rKwzK;OjFO@P z_=6^_TdrGa4b_;SoPT!DbSGHdJM=W>67|6&?N#UG1IVY+X$D3Y?#w=`5r=XhwF3Xp;6Z9P*!QY z{tb}ITtwrk^EoL)j7?+u*NM4|70y$68>kU5sOpHH^Xr>l-|@A5Ig1bohpr75b?XOKcP-$2tV@qz8KzfGPku{hmlO z`?h3wy3lbj{ts^Rpk$&?Vr3RQInsih1yw0{%!7&FS?X^1<9{?$rQW}y^>LKmz4|B< zv?1pYYaDIYObJ>#Ox3+=xeg`Jl{5jKzyG}I%mR-2!A*ubV<$@+;;T_%z%2v#4nDZ2 zVH7gaXCoGVu$Lk#)R6?>1OtDietXFO&;MZMN&QmA3mSPl_T@$d-IG|sVmSV>kmztT zBExrG)E(DOgLJM2z0jOYB$uLV|Nf$peqEN1)=}}$O0!%9DLJ1 zyLd82SNpu=WEHl~Sl^dyZH~#%&D|q@imBMIH&~v;4$G!zvIc}oQ&$gLwI}aye51_F zWxYPk#F@tXq)Y4QY$dDc$45hur@1cBuUn1JkY3#>mfM~boj{!|Koe1U1QIUbNpr_I za#uRjFq@{Qnat#Jtu6N?<)_0F?~mM2WJF8#afc3;%2BPFRu~n@aQo&>eoAH<=F+X! zR^$Yav2dPtOX7#>D9+fEttO7Z=D%iAdTCxcqSWIi*dPF^*|DG_nY6@n>Z&<0_jMU> zD?k2a1_&G=Xjs<%8xViZe2Y#i8Plk3>^-JikE`Wm=W3Zg-R=kiTAA3$C6~;LGRePhBYHS*O6^ z_?KwJAa&uUSUXd@IQzmMSV?ut0{VpFLo;!SjG;_n#i%p_24OE+`!zs*gH{nDhOH$;*&@^HX$Sa7=VP zF8+q`3=QtPOkGHUs6!|=!TWC;{XZ9dSOwG@*0iK_u6T*gf#>L;IrQ))w$5D(ZqVE#hvIwhm`wam zfKAC7%08O0 zCL<;m%ULJaAyM?j5n&LcyfNlFYTz3R=?nM7%@h!LNSO`n9Q;hKR*FCk&JdkTj4sA= zsWU|tKC3v!I;vV`c!5Yb>L}$7SM;c0(3yZM;KdQoV#pRCYo-*wT_B{dFx zJHKvxH@#0h_%auzm<~1PgiL{%$A*~ZFeJ(>Uc3!^h&Z~o-%kqAYRdD?eg+?Da2Bb1 zSmb=V6AxHy+otClg<=#!K#YOzW0~%lvZZECLEcD!rdGEVOUZUkqqWT_<}RFV`RH^s zjKb&(O%qBPVS|!Rzp!Ck)oZs5Hbt{>-`5fJ?klNgH9dlolr)h_Jf% zl#*?u0G+1AAglTu$gT$}2v_i8KOJ94&ZpTTB@6BOt*57 zayTBiNGm+bHf#=3i`Z?mU1RhQHdHuW_N3M4lpD!HUjRvAMRf|QnwD3GI@^{(U1MD} z;xP%NQ0j|FGcFHBg0rICDoun?>O=BAt=wAkc$kdw5XP3vGqk^WPe=T04HLJVJCqX0q#L4~Q?RQi4g zP#!p%AD8~Rz<0Y^R$4l>X=!k zo;EJ=;w`>{i)NTa%}MIGwiNH`UsIQu#y}YlB1zUsq1Vi!R^T*m$G*nasdfjl&N7 zi3|<9#tBNs07o9JhYt|kR*9$B#_;_6WvBYKofv`YNEtERJ&cg7{O#=J(-UigI$$U# zHO~?rdG2vNJg-0(A8nrpt6{)q>bYV0uZ?4ow#D}f?}j~4&^2Oi~emJXg{0ycD zyTdSn$E_f;r3z5Urhr!?_YJz}H|dglW-+*ac7YNG9xqjZR<#QT~}`vS=Lp zbt+ZWGgM=WOp6oj4(Z|wc1p(jCGj_tv@fFD@rC(mFd` z*bc(++GD8lmP;9QkS1*@p$@5D%c4YQU$zX3!F$f#@4W2ahHt>NRqT#80# zP5HG>I#Rdw{DM|WO-M$awIhKQ4^OyaZX7j^^pe{EUc2fbYgLT_`!}ME8~Ky`O)TyA z9o-yxK~M2k>iBON=MGfPPTu-=L9KmddC@I-tE&hmUCGfba(dBj3|`xj@8f&}=T;(1 zr{?rPw;hXGle(W(M|(s9c$1br7yO0V9h=Pi#7!ZLMixjHB!|q9&?t+_Grh)=m>0w? zkx-S`Gty@BM_plufGN4qJ&a$iOWasBXrTHPFT_NQE;YJkDH7D%d>G?6-!+Da7h;9% zMN^%E1jba6izBfR-OWrDo_!h2a?_h!z>_{s)8M%L&7jr~M(X>z9w|3E*AaasLlL`b zW^o$MGnTCf)U78`7EY^@x4iccK=xS+C8-Bz*pWjQ6LIjqV9chV8TLh+7z~;#Vv#F^ z2Mc(xcF)0FlMZmF;~x? zEngWpiNGUvcz63B=7rZt*z{uCk|5Dmwyg>V{^`u*YCu42uWYW-)ubu){HQk=FsU8e zL$#s>J)<8<+BXS7)~=tdQ&QvFxg=dI;^T}^_oAy?{?LKX;u&2;H|!16Xx3ia7&S76 z3m?&3XqlI|mi_o3CQH$$5APeuAVPMEQp)_=BKI5cWm#$YM*hqiyQOq*OLQog z8<-YZoyt`zM9yA7`a!_I{sl&QJeIF(?_|wAp7cC#83Bt;zXmm(MZ9$(tz#eaNh6NH2cL;6*PV+iC?n)}#;?MHpmj7=L5iN7+#-t)I|LV@8NJ^d`X{B zy0602KIc9512D-xPBKMAl<-`RT6PlTz@5nt| zzo|@$nbS6;F?TIO7mugrDnR24Yb1+Vv#;A2LOQlIbz++7!l*_|X3|G3#5AMSd5Qv5 zQF&6Z$car2lZCwvgg0p`K&tieaQ>q-GpE%S5rcxl>|1>kHtW(mqoc4Q&)Sh64GQlz z%sNXaVl^EXUeV%>2hkH+$h*XHR8cZfg8}1;_); zgyEGg)n(T;ng_TfPow;!6rg)j-0_%aXU8dmq3ixBV;#Q0kAOY#p_|^NjqR`*((GEd z_<5+z8dAL%2OMS~)P|85G{Ts-S6Y5XJKtOlG3&>>73HFz>=&x43}X+;5wr^g6@w!V zNC?*0)CGd}n#8qV4If zGw@nqpi1Uq%I;f@9)kp>h%5w+Tyjd4%d)sJE0{c%Gob(-9VlGB(pIgG0}E!o##t=P z9NIR8`|x@~j{1>t!?3a& zO`Sb$UB8BWyK;;3oLA)@+wF}RKEuDxs@wT!q#191+Aw>S-MeYZvI3@e(M~UNM&sBu zsh!WrJj&yr_ona8ci*%BtW(h>BH-Qus5SK{BmvHQUaof5FMV>3O6#6#9?Wb*fESQ9 zM*|Y5;~%A>XLpkP)%uEa{K3_=JBFkpAvZ5Ml#dG$4GM6E%wRk(299k_G50LzFV|26b0cbWgGMY;6 z=Tbk7NvWlcf#);7$|ND3B5`p}jQqW*FCz&ET!F9(AS;Uca$~^NiD4(G)b3|8BuYLc z6;??CHpeYXE(-H*`SD?cc0(%8OA9YJ=8L~h&Xc|_B|^59Nf%H#IsR1WmT zvIn`e6h7iKON?^!y}!>SFSeV1(s=&8#xposh0_8F?iSO%Tqs$3FAnYGwQAx9hHB;9 zHrU?LB_#ebeOt&vitL)7>DXZXdbsJRnaNWYQ3aKcsB{6FCV?$~dmN{t_Ajn;=%>qb zR!h$FnCmRcNw#~isoUzHLgMj1!648!yJNOpFqi!1(gx;$iZBYM3(^OyF#@R=BQ`mUv?53O0zXL%ja5&dC7 zfdaH~V}DytrY&TV_HkhHKz~n95P^&R3Q=n%5k8lUOjcu);G7MdoDXav# zHq;Ky^4)4QT@*0h#Vh&rU>*jLxPK!lC;|HxhIt^_w$RPm>G-Bm&x2nA@_4MWGl$7S z)%Ce2|fo!}~xIKD>2Ni}&W4E4B9b{kNv@v7BCjXkUjM`bm!-&~oxb zzV(rXa9vH@JK9#|6g%F{5COA_R9XZ7SbJ)R6Q8)-{0f&B$v;EOF7lyEN Date: Tue, 15 Apr 2025 22:34:26 +0800 Subject: [PATCH 53/74] =?UTF-8?q?=E4=BF=AE=E6=AD=A3=E4=B8=80=E5=A4=84?= =?UTF-8?q?=E6=96=87=E6=A1=A3=E9=94=99=E8=AF=AF?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- docs/java/jvm/jvm-parameters-intro.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/docs/java/jvm/jvm-parameters-intro.md b/docs/java/jvm/jvm-parameters-intro.md index 7b460b1277b..115ef31c734 100644 --- a/docs/java/jvm/jvm-parameters-intro.md +++ b/docs/java/jvm/jvm-parameters-intro.md @@ -70,9 +70,9 @@ GC 调优策略中很重要的一条经验总结是这样说的: > 将新对象预留在新生代,由于 Full GC 的成本远高于 Minor GC,因此尽可能将对象分配在新生代是明智的做法,实际项目中根据 GC 日志分析新生代空间大小分配是否合理,适当通过“-Xmn”命令调节新生代大小,最大限度降低新对象直接进入老年代的情况。 -另外,你还可以通过 **`-XX:NewRatio=`** 来设置老年代与新生代内存的比值。 +另外,你还可以通过 **`-XX:NewRatio=`** 来设置新生代与老年代内存的比例。 -比如下面的参数就是设置新生代与老年代内存的比值为 2(默认值)。也就是说 young/old 所占比值为 2,新生代占整个堆栈的 2/3。 +比如下面的参数就是设置新生代与老年代内存的比例为 1:2(默认值)。也就是说新生代占整个堆栈的 1/3。 ```plain -XX:NewRatio=2 From 9f164247f6d45b9982252cf3fc2060f3dce40354 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E4=B8=80=E5=8F=AA=E5=92=95=E5=92=95=E9=B1=BC?= <50142521+1312255201@users.noreply.github.com> Date: Wed, 16 Apr 2025 00:48:49 +0800 Subject: [PATCH 54/74] =?UTF-8?q?=E5=B0=86=E6=A0=B7=E4=BE=8B=E9=87=8C?= =?UTF-8?q?=E7=9A=842.5->2.4=20-2.5->-2.4?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- docs/java/basis/bigdecimal.md | 16 ++++++++-------- 1 file changed, 8 insertions(+), 8 deletions(-) diff --git a/docs/java/basis/bigdecimal.md b/docs/java/basis/bigdecimal.md index a1e966c9905..7a9b549905a 100644 --- a/docs/java/basis/bigdecimal.md +++ b/docs/java/basis/bigdecimal.md @@ -99,20 +99,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), //...... } From 1eee19f991a5c858295b4b2862cdd15c70895fb6 Mon Sep 17 00:00:00 2001 From: Machisk <499603856@qq.com> Date: Sun, 20 Apr 2025 17:26:55 +0800 Subject: [PATCH 55/74] =?UTF-8?q?=E4=BF=AE=E6=94=B9=E6=96=87=E7=AB=A0?= =?UTF-8?q?=E8=AF=AD=E5=8F=A5=E9=94=99=E8=AF=AF?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- docs/java/concurrent/java-concurrent-questions-01.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/java/concurrent/java-concurrent-questions-01.md b/docs/java/concurrent/java-concurrent-questions-01.md index 50b3922baec..e1768d04d45 100644 --- a/docs/java/concurrent/java-concurrent-questions-01.md +++ b/docs/java/concurrent/java-concurrent-questions-01.md @@ -403,7 +403,7 @@ Process finished with exit code 0 我们分析一下上面的代码为什么避免了死锁的发生? -线程 1 首先获得到 resource1 的监视器锁,这时候线程 2 就获取不到了。然后线程 1 再去获取 resource2 的监视器锁,可以获取到。然后线程 1 释放了对 resource1、resource2 的监视器锁的占用,线程 2 获取到就可以执行了。这样就破坏了破坏循环等待条件,因此避免了死锁。 +线程 1 首先获得到 resource1 的监视器锁,这时候线程 2 就获取不到了。然后线程 1 再去获取 resource2 的监视器锁,可以获取到。然后线程 1 释放了对 resource1、resource2 的监视器锁的占用,线程 2 获取到就可以执行了。这样就破坏了循环等待条件,因此避免了死锁。 ## 虚拟线程 From 3b1767d6e353c3202b92eb4ea69cedf63c213efb Mon Sep 17 00:00:00 2001 From: Guide Date: Fri, 25 Apr 2025 07:00:55 +0800 Subject: [PATCH 56/74] =?UTF-8?q?[docs=20update]=E6=95=B0=E6=8D=AE?= =?UTF-8?q?=E5=BA=93=E5=92=8C=E7=BD=91=E7=BB=9C=E9=83=A8=E5=88=86=E9=97=AE?= =?UTF-8?q?=E9=A2=98=E7=AD=94=E6=A1=88=E4=BC=98=E5=8C=96=E5=AE=8C=E5=96=84?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../network/other-network-questions2.md | 68 +- docs/database/mysql/mysql-questions-01.md | 39 +- .../mysql/transaction-isolation-level.md | 53 +- docs/java/basis/serialization.md | 8 +- .../spring/spring-common-annotations.md | 811 ++++++++++-------- 5 files changed, 537 insertions(+), 442 deletions(-) diff --git a/docs/cs-basics/network/other-network-questions2.md b/docs/cs-basics/network/other-network-questions2.md index fac525d704c..9d193f4913d 100644 --- a/docs/cs-basics/network/other-network-questions2.md +++ b/docs/cs-basics/network/other-network-questions2.md @@ -11,31 +11,61 @@ tag: ### 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 支持一对一、一对多、多对一、多对多; +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 bytes | 8 bytes | -| 是否提供广播或多播服务 | 否 | 是 | +| 特性 | TCP | UDP | +| ------------ | -------------------------- | ----------------------------------- | +| **连接性** | 面向连接 | 无连接 | +| **可靠性** | 可靠 | 不可靠 (尽力而为) | +| **状态维护** | 有状态 | 无状态 | +| **传输效率** | 较低 | 较高 | +| **传输形式** | 面向字节流 | 面向数据报 (报文) | +| **头部开销** | 20 - 60 字节 | 8 字节 | +| **通信模式** | 点对点 (单播) | 单播、多播、广播 | +| **常见应用** | HTTP/HTTPS, FTP, SMTP, SSH | DNS, DHCP, SNMP, TFTP, VoIP, 视频流 | ### 什么时候选择 TCP,什么时候选 UDP? -- **UDP 一般用于即时通信**,比如:语音、 视频、直播等等。这些场景对传输数据的准确性要求不是特别高,比如你看视频即使少个一两帧,实际给人的感觉区别也不大。 -- **TCP 用于对传输准确性要求特别高的场景**,比如文件传输、发送和接收邮件、远程登录等等。 +选择 TCP 还是 UDP,主要取决于你的应用**对数据传输的可靠性要求有多高,以及对实时性和效率的要求有多高**。 + +当**数据准确性和完整性至关重要,一点都不能出错**时,通常选择 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? diff --git a/docs/database/mysql/mysql-questions-01.md b/docs/database/mysql/mysql-questions-01.md index 4878eee56ef..b1493a64662 100644 --- a/docs/database/mysql/mysql-questions-01.md +++ b/docs/database/mysql/mysql-questions-01.md @@ -553,31 +553,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; @@ -590,6 +585,12 @@ mysql> SELECT @@tx_isolation; 关于 MySQL 事务隔离级别的详细介绍,可以看看我写的这篇文章:[MySQL 事务隔离级别详解](./transaction-isolation-level.md)。 +### MySQL 的隔离级别是基于锁实现的吗? + +MySQL 的隔离级别基于锁和 MVCC 机制共同实现的。 + +SERIALIZABLE 隔离级别是通过锁来实现的,READ-COMMITTED 和 REPEATABLE-READ 隔离级别是基于 MVCC 实现的。不过, SERIALIZABLE 之外的其他隔离级别可能也需要用到锁机制,就比如 REPEATABLE-READ 在当前读情况下需要使用加锁读来保证不会出现幻读。 + ## MySQL 锁 锁是一种常见的并发事务的控制方式。 diff --git a/docs/database/mysql/transaction-isolation-level.md b/docs/database/mysql/transaction-isolation-level.md index 52ad40f4a47..8b706640ea6 100644 --- a/docs/database/mysql/transaction-isolation-level.md +++ b/docs/database/mysql/transaction-isolation-level.md @@ -11,43 +11,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/java/basis/serialization.md b/docs/java/basis/serialization.md index fb7d3b69e7f..f6ab9071967 100644 --- a/docs/java/basis/serialization.md +++ b/docs/java/basis/serialization.md @@ -83,7 +83,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,7 +95,7 @@ public class RpcRequest implements Serializable { > > 如果想显式指定 `serialVersionUID` ,则需要在类中使用 `static` 和 `final` 关键字来修饰一个 `long` 类型的变量,变量名字必须为 `"serialVersionUID"` 。 -也就是说,`serialVersionUID` 只是用来被 JVM 识别,实际并没有被序列化。 +也就是说,`serialVersionUID` 本身(作为 static 变量)确实不作为对象状态被序列化。但是,它的值被 Java 序列化机制特殊处理了——作为一个版本标识符被读取并写入序列化流中,用于在反序列化时进行版本兼容性检查。 **如果有些字段不想进行序列化怎么办?** diff --git a/docs/system-design/framework/spring/spring-common-annotations.md b/docs/system-design/framework/spring/spring-common-annotations.md index 57ec89ca256..e51fb4b9603 100644 --- a/docs/system-design/framework/spring/spring-common-annotations.md +++ b/docs/system-design/framework/spring/spring-common-annotations.md @@ -6,21 +6,19 @@ tag: - Spring --- -### 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 +29,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 +62,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; + // ... +} +``` -#### 2.4. `@Scope` +`@Resource(name="beanName")`是 JSR-250 规范定义的注解,也用于依赖注入。它默认按**名称 (by Name)** 查找 Bean 进行注入,而 `@Autowired`默认按**类型 (by Type)** 。如果未指定 `name` 属性,它会尝试根据字段名或方法名查找,如果找不到,则回退到按类型查找(类似 `@Autowired`)。 -声明 Spring Bean 的作用域,使用方法: +`@Resource`只能标注在字段 和 Setter 方法上,不支持构造器注入。 ```java -@Bean -@Scope("singleton") -public Person personSingleton() { - return new Person(); +@Service +public class UserService { + @Resource(name = "userRepositoryA") + private UserRepository userRepository; + // ... } ``` -**四种常见的 Spring Bean 的作用域:** +### 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 +@Component +// 每次获取都会创建新的 PrototypeBean 实例 +@Scope("prototype") +public class PrototypeBean { + // ... +} +``` + +### 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`: + +- `@Controller`:主要用于传统的 Spring MVC 应用,方法返回值通常是逻辑视图名,需要视图解析器配合渲染页面。如果需要返回数据(如 JSON),则需要在方法上额外添加 `@ResponseBody` 注解。 +- `@RestController`:专为构建返回数据的 RESTful API 设计。类上使用此注解后,所有方法的返回值都会默认被视为响应体内容(相当于每个方法都隐式添加了 `@ResponseBody`),通常用于返回 JSON 或 XML 数据。在现代前后端分离的应用中,`@RestController` 是更常用的选择。 -- singleton : 唯一 bean 实例,Spring 中的 bean 默认都是单例的。 -- prototype : 每次请求都会创建一个新的 bean 实例。 -- request : 每一次 HTTP 请求都会产生一个新的 bean,该 bean 仅在当前 HTTP request 内有效。 -- session : 每一个 HTTP Session 会产生一个新的 bean,该 bean 仅在当前 HTTP session 内有效。 +关于`@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)。 + +## 配置 -#### 2.5. `@Configuration` +### 声明配置类 -一般用来声明配置类,可以使用 `@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 方法。 +} +``` + +### 读取配置信息 + +在应用程序开发中,我们经常需要管理一些配置信息,例如数据库连接细节、第三方服务(如阿里云 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 + ...... } ``` -### 3. 处理常见的 HTTP 请求类型 +**注意**:当使用 `@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 +298,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 +309,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 +322,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 +334,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 +345,7 @@ public ResponseEntity deleteUser(@PathVariable(value = "userId") Long userId){ } ``` -#### 3.5. **PATCH 请求** +#### PATCH 请求 一般实际项目中,我们都是 PUT 不够用了之后才用 PATCH 请求去更新数据。 @@ -207,32 +357,40 @@ public ResponseEntity deleteUser(@PathVariable(value = "userId") Long userId){ } ``` -### 4. 前后端传值 - -**掌握前后端传值的正确姿势,是你开始 CRUD 的第一步!** +### 参数绑定 -#### 4.1. `@PathVariable` 和 `@RequestParam` +在处理 HTTP 请求时,Spring MVC 提供了多种注解用于绑定请求参数到方法参数中。以下是常见的参数绑定方式: -`@PathVariable`用于获取路径参数,`@RequestParam`用于获取查询参数。 +#### 从 URL 路径中提取参数 -举个简单的例子: +`@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` 用于绑定查询参数。例如: -那么我们服务获取到的数据就是:`klassId=123456,type=web`。 +```java +@GetMapping("/klasses/{klassId}/teachers") +public List getTeachersByClass(@PathVariable Long klassId, + @RequestParam(value = "type", required = false) String type) { + return teacherService.findTeachersByClassAndType(klassId, type); +} +``` + +若请求 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,91 +430,14 @@ public class UserRegisterRequest { ![](./images/spring-annotations/@RequestBody.png) -👉 需要注意的是:**一个请求方法只可以有一个`@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`(常用) - -使用 `@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; - - @Setter - @Getter - @ToString - static class Book { - String name; - String description; - } - 省略getter/setter - ...... -} -``` - -你可以像使用普通的 Spring bean 一样,将其注入到类中使用。 - -#### 5.3. `@PropertySource`(不常用) - -`@PropertySource`读取指定 properties 文件 - -```java -@Component -@PropertySource("classpath:website.properties") - -class WebSite { - @Value("${url}") - private String url; +**注意**: - 省略getter/setter - ...... -} -``` - -更多内容请查看我的这篇文章:[《10 分钟搞定 SpringBoot 如何优雅读取配置文件?》](https://mp.weixin.qq.com/s?__biz=Mzg2OTA0Njk0OA==&mid=2247486181&idx=2&sn=10db0ae64ef501f96a5b0dbc4bd78786&chksm=cea2452ef9d5cc384678e456427328600971180a77e40c13936b19369672ca3e342c26e92b50&token=816772476&lang=zh_CN#rd) 。 +- 一个方法只能有一个 `@RequestBody` 参数,但可以有多个 `@PathVariable` 和 `@RequestParam`。 +- 如果需要接收多个复杂对象,建议合并成一个单一对象。 -### 6. 参数校验 +## 数据校验 -**数据的校验的重要性就不用说了,即使在前端对数据进行校验的情况下,我们还是要对传入后端的数据再进行一遍校验,避免用户绕过浏览器直接通过一些 HTTP 工具直接向后端请求一些违法数据。** +数据校验是保障系统稳定性和安全性的关键环节。即使在用户界面(前端)已经实施了数据校验,**后端服务仍必须对接收到的数据进行再次校验**。这是因为前端校验可以被轻易绕过(例如,通过开发者工具修改请求或使用 Postman、curl 等 HTTP 工具直接调用 API),恶意或错误的数据可能直接发送到后端。因此,后端校验是防止非法数据、维护数据一致性、确保业务逻辑正确执行的最后一道,也是最重要的一道防线。 Bean Validation 是一套定义 JavaBean 参数校验标准的规范 (JSR 303, 349, 380),它提供了一系列注解,可以直接用于 JavaBean 的属性上,从而实现便捷的参数校验。 @@ -364,46 +445,58 @@ Bean Validation 是一套定义 JavaBean 参数校验标准的规范 (JSR 303, 3 - **JSR 349 (Bean Validation 1.1):** 在 1.0 基础上进行扩展,例如引入了对方法参数和返回值校验的支持、增强了对分组校验(Group Validation)的处理。 - **JSR 380 (Bean Validation 2.0):** 拥抱 Java 8 的新特性,并进行了一些改进,例如支持 `java.time` 包中的日期和时间类型、引入了一些新的校验注解(如 `@NotEmpty`, `@NotBlank`等)。 -校验的时候我们实际用的是 **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)的参考实现。 +Bean Validation 本身只是一套**规范(接口和注解)**,我们需要一个实现了这套规范的**具体框架**来执行校验逻辑。目前,**Hibernate Validator** 是 Bean Validation 规范最权威、使用最广泛的参考实现。 -SpringBoot 项目的 spring-boot-starter-web 依赖中已经有 hibernate-validator 包,不需要引用相关依赖。如下图所示(通过 idea 插件—Maven Helper 生成): +- 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)。 -**注**:更新版本的 spring-boot-starter-web 依赖中不再有 hibernate-validator 包(如 2.3.11.RELEASE),需要自己引入 `spring-boot-starter-validation` 依赖。 +在 Spring Boot 项目中使用 Bean Validation 非常方便,这得益于 Spring Boot 的自动配置能力。关于依赖引入,需要注意: + +- 在较早版本的 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` 依赖: + +```xml + + org.springframework.boot + spring-boot-starter-validation + +``` ![](https://oss.javaguide.cn/2021/03/c7bacd12-1c1a-4e41-aaaf-4cad840fc073.png) -非 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/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` + +### 一些常用的字段验证的注解 -👉 需要注意的是:**所有的注解,推荐使用 JSR 注解,即`javax.validation.constraints`,而不是`org.hibernate.validator.constraints`** +Bean Validation 规范及其实现(如 Hibernate Validator)提供了丰富的注解,用于声明式地定义校验规则。以下是一些常用的注解及其说明: -#### 6.1. 一些常用的字段验证的注解 +- `@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`,但允许等于当前时间。 +- ...... -- `@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` 被注释的元素必须是一个将来的日期 -- …… +### 验证请求体(RequestBody) -#### 6.2. 验证请求体(RequestBody) +当 Controller 方法使用 `@RequestBody` 注解来接收请求体并将其绑定到一个对象时,可以在该参数前添加 `@Valid` 注解来触发对该对象的校验。如果验证失败,它将抛出`MethodArgumentNotValidException`。 ```java @Data @AllArgsConstructor @NoArgsConstructor public class Person { - @NotNull(message = "classId 不能为空") private String classId; @@ -418,17 +511,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); @@ -436,26 +524,45 @@ public class PersonController { } ``` -#### 6.3. 验证请求参数(Path Variables 和 Request Parameters) +### 验证请求参数(Path Variables 和 Request Parameters) + +对于直接映射到方法参数的简单类型数据(如路径变量 `@PathVariable` 或请求参数 `@RequestParam`),校验方式略有不同: + +1. **在 Controller 类上添加 `@Validated` 注解**:这个注解是 Spring 提供的(非 JSR 标准),它使得 Spring 能够处理方法级别的参数校验注解。**这是必需步骤。** +2. **将校验注解直接放在方法参数上**:将 `@Min`, `@Max`, `@Size`, `@Pattern` 等校验注解直接应用于对应的 `@PathVariable` 或 `@RequestParam` 参数。 -**一定一定不要忘记在类上加上 `@Validated` 注解了,这个参数可以告诉 Spring 去校验方法参数。** +一定一定不要忘记在类上加上 `@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 层异常。 @@ -486,34 +593,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`注解即可! + +```java +@Transactional(rollbackFor = Exception.class) +public void save() { + ...... +} + +``` + +我们知道 Exception 分为运行时异常 RuntimeException 和非运行时异常。在`@Transactional`注解中如果不配置`rollbackFor`属性,那么事务只会在遇到`RuntimeException`的时候才会回滚,加上`rollbackFor=Exception.class`,可以让事务在遇到非运行时异常时也回滚。 + +`@Transactional` 注解一般可以作用在`类`或者`方法`上。 + +- **作用于类**:当把`@Transactional` 注解放在类上时,表示所有该类的 public 方法都配置相同的事务属性信息。 +- **作用于方法**:当类配置了`@Transactional`,方法也配置了`@Transactional`,方法的事务会覆盖类的事务配置信息。 + +更多关于 Spring 事务的内容请查看我的这篇文章:[可能是最漂亮的 Spring 事务管理详解](./spring-transaction.md) 。 + +## JPA -#### 8.1. 创建表 +Spring Data JPA 提供了一系列注解和功能,帮助开发者轻松实现 ORM(对象关系映射)。 -`@Entity`声明一个类对应一个数据库实体。 +### 创建表 -`@Table` 设置表名 +`@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 @@ -521,51 +655,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 @@ -582,7 +672,7 @@ private Long id; private Long id; ``` -jpa 提供的主键生成策略有如下几种: +JPA 提供的主键生成策略有如下几种: ```java public class DefaultIdentifierGeneratorFactory @@ -617,148 +707,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 -``` - -一般使用注解的方式比较多。 - -#### 8.5. 声明大字段 +其他不被持久化的字段方式: -`@Lob`:声明某个字段为大字段。 +- **`static`**:静态字段不会被持久化。 +- **`final`**:最终字段不会被持久化。 +- **`transient`**:使用 Java 的 `transient` 关键字声明的字段不会被序列化或持久化。 -```java -@Lob -private String content; -``` +### 大字段存储 -更详细的声明: +`@Lob` 用于声明大字段(如 `CLOB` 或 `BLOB`)。 ```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) @@ -770,101 +824,101 @@ public class AuditSecurityConfiguration { 简单介绍一下上面涉及到的一些注解: 1. `@CreatedDate`: 表示该字段为创建时间字段,在这个实体被 insert 的时候,会设置值 -2. `@CreatedBy` :表示该字段为创建人,在这个实体被 insert 的时候,会设置值 - - `@LastModifiedDate`、`@LastModifiedBy`同理。 - -`@EnableJpaAuditing`:开启 JPA 审计功能。 +2. `@CreatedBy` :表示该字段为创建人,在这个实体被 insert 的时候,会设置值 `@LastModifiedDate`、`@LastModifiedBy`同理。 +3. `@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` 注解一般可以作用在`类`或者`方法`上。 +## JSON 数据处理 -- **作用于类**:当把`@Transactional` 注解放在类上时,表示所有该类的 public 方法都配置相同的事务属性信息。 -- **作用于方法**:当类配置了`@Transactional`,方法也配置了`@Transactional`,方法的事务会覆盖类的事务配置信息。 +在 Web 开发中,经常需要处理 Java 对象与 JSON 格式之间的转换。Spring 通常集成 Jackson 库来完成此任务,以下是一些常用的 Jackson 注解,可以帮助我们定制化 JSON 的序列化(Java 对象转 JSON)和反序列化(JSON 转 Java 对象)过程。 -更多关于 Spring 事务的内容请查看我的这篇文章:[可能是最漂亮的 Spring 事务管理详解](./spring-transaction.md) 。 +### 过滤 JSON 字段 -### 10. json 数据处理 +有时我们不希望 Java 对象的某些字段被包含在最终生成的 JSON 中,或者在将 JSON 转换为 Java 对象时不处理某些 JSON 属性。 -#### 10.1. 过滤 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 @@ -892,7 +946,7 @@ public class Account { ``` -未扁平化之前: +未扁平化之前的 JSON 结构: ```json { @@ -907,7 +961,7 @@ public class Account { } ``` -使用`@JsonUnwrapped` 扁平对象之后: +使用`@JsonUnwrapped` 扁平对象: ```java @Getter @@ -922,6 +976,8 @@ public class Account { } ``` +扁平化后的 JSON 结构: + ```json { "provinceName": "湖北", @@ -931,36 +987,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)。 - From 2e7aa2a8ebf2e7423b08448d95800058721630dc Mon Sep 17 00:00:00 2001 From: Guide Date: Fri, 25 Apr 2025 07:41:01 +0800 Subject: [PATCH 57/74] =?UTF-8?q?[docs=20update]=E5=AE=8C=E5=96=84?= =?UTF-8?q?=E4=BC=98=E5=8C=96=E6=9C=80=E9=87=8D=E8=A6=81=E7=9A=84JVM?= =?UTF-8?q?=E5=8F=82=E6=95=B0=E6=80=BB=E7=BB=93?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- docs/java/jvm/jvm-parameters-intro.md | 222 +++++++++++++------------- 1 file changed, 113 insertions(+), 109 deletions(-) diff --git a/docs/java/jvm/jvm-parameters-intro.md b/docs/java/jvm/jvm-parameters-intro.md index 115ef31c734..b97fc66d923 100644 --- a/docs/java/jvm/jvm-parameters-intro.md +++ b/docs/java/jvm/jvm-parameters-intro.md @@ -7,78 +7,77 @@ tag: > 本文由 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 - -## 1.概述 +> +> JDK 版本:1.8 为主,也会补充新版本常用参数 -在本篇文章中,你将掌握最常用的 JVM 参数配置。 +在本篇文章中,我们将一起掌握 Java 虚拟机(JVM)中最常用的一些参数配置,帮助你更好地理解和调优 Java 应用的运行环境。 -## 2.堆内存相关 +## 堆内存相关 -> Java 虚拟机所管理的内存中最大的一块,Java 堆是所有线程共享的一块内存区域,在虚拟机启动时创建。**此内存区域的唯一目的就是存放对象实例,几乎所有的对象实例以及数组都在这里分配内存。** +> Java 堆(Java Heap)是 JVM 所管理的内存中最大的一块区域,**所有线程共享**,在虚拟机启动时创建。**此内存区域的唯一目的就是存放对象实例,几乎所有的对象实例以及数组都要在堆上分配内存。** ![内存区域常见配置参数](./pictures/内存区域常见配置参数.png) -### 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:2(默认值)。也就是说新生代占整个堆栈的 1/3。 +例如,`-XX:NewRatio=2` (默认值)表示老年代 : 新生代 = 2 : 1。即新生代占整个堆大小的 1/3。 -```plain +```bash -XX:NewRatio=2 ``` -### 2.3.显式指定永久代/元空间的大小 +### 设置永久代/元空间大小 (PermGen/Metaspace) **从 Java 8 开始,如果我们没有指定 Metaspace 的大小,随着更多类的创建,虚拟机会耗尽所有可用的系统内存(永久代并不会出现这种情况)。** @@ -102,7 +101,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) 中提到的: @@ -112,11 +111,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() { @@ -126,111 +121,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)。 From 79f741803cf694b237802de8bf076075295b54d5 Mon Sep 17 00:00:00 2001 From: AhogeK Date: Fri, 25 Apr 2025 16:40:58 +0800 Subject: [PATCH 58/74] =?UTF-8?q?docs(java):=20=E5=88=A0=E9=99=A4=E6=96=87?= =?UTF-8?q?=E6=A1=A3=E4=B8=AD=E5=A4=9A=E4=BD=99=E7=9A=84=E5=8F=A5=E5=8F=B7?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 在 java-basic-questions-01.md 文件中,将多余的句号删除 - 具体修改位置:在"数据校验"条目中,移除了多余的句号 --- docs/java/basis/java-basic-questions-01.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/java/basis/java-basic-questions-01.md b/docs/java/basis/java-basic-questions-01.md index 47ef386848c..49e00a73948 100644 --- a/docs/java/basis/java-basic-questions-01.md +++ b/docs/java/basis/java-basic-questions-01.md @@ -332,7 +332,7 @@ static final int hash(Object key) { - **位字段管理**:例如存储和操作多个布尔值。 - **哈希算法和加密解密**:通过移位和与、或等操作来混淆数据。 - **数据压缩**:例如霍夫曼编码通过移位运算符可以快速处理和操作二进制数据,以生成紧凑的压缩格式。 -- **数据校验**:例如 CRC(循环冗余校验)通过移位和多项式除法生成和校验数据完整性。。 +- **数据校验**:例如 CRC(循环冗余校验)通过移位和多项式除法生成和校验数据完整性。 - **内存对齐**:通过移位操作,可以轻松计算和调整数据的对齐地址。 掌握最基本的移位运算符知识还是很有必要的,这不光可以帮助我们在代码中使用,还可以帮助我们理解源码中涉及到移位运算符的代码。 From 9d11263c6e7e7ab05f84aea206888870227fdf2e Mon Sep 17 00:00:00 2001 From: Guide Date: Tue, 29 Apr 2025 07:07:26 +0800 Subject: [PATCH 59/74] =?UTF-8?q?[docs=20update&fix]=E4=BF=AE=E6=AD=A3?= =?UTF-8?q?=E5=BF=AB=E6=8E=92=E7=AE=97=E6=B3=95&=E8=A1=A5=E5=85=85WebSocke?= =?UTF-8?q?t=E9=9D=A2=E8=AF=95=E9=97=AE=E9=A2=98&=E4=BC=98=E5=8C=96Redis?= =?UTF-8?q?=E6=85=A2=E6=9F=A5=E8=AF=A2?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../10-classical-sorting-algorithms.md | 73 +++++++++++++------ .../network/other-network-questions.md | 67 ++++++++++++++--- docs/database/redis/redis-questions-02.md | 36 ++++++--- docs/java/basis/java-basic-questions-03.md | 62 ++++++++++------ 4 files changed, 172 insertions(+), 66 deletions(-) diff --git a/docs/cs-basics/algorithms/10-classical-sorting-algorithms.md b/docs/cs-basics/algorithms/10-classical-sorting-algorithms.md index 355875f9658..d583b936a12 100644 --- a/docs/cs-basics/algorithms/10-classical-sorting-algorithms.md +++ b/docs/cs-basics/algorithms/10-classical-sorting-algorithms.md @@ -367,31 +367,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/network/other-network-questions.md b/docs/cs-basics/network/other-network-questions.md index c845073f0c7..0b852b063ac 100644 --- a/docs/cs-basics/network/other-network-questions.md +++ b/docs/cs-basics/network/other-network-questions.md @@ -336,23 +336,70 @@ WebSocket 的工作过程可以分为以下几个步骤: 另外,建立 WebSocket 连接之后,通过心跳机制来保持 WebSocket 连接的稳定性和活跃性。 +### WebSocket 与短轮询、长轮询的区别 + +这三种方式,都是为了解决“**客户端如何及时获取服务器最新数据,实现实时更新**”的问题。它们的实现方式和效率、实时性差异较大。 + +**1.短轮询(Short Polling)** + +- **原理**:客户端每隔固定时间(如 5 秒)发起一次 HTTP 请求,询问服务器是否有新数据。服务器收到请求后立即响应。 +- **优点**:实现简单,兼容性好,直接用常规 HTTP 请求即可。 +- **缺点**: + - **实时性一般**:消息可能在两次轮询间到达,用户需等到下次请求才知晓。 + - **资源浪费大**:反复建立/关闭连接,且大多数请求收到的都是“无新消息”,极大增加服务器和网络压力。 + +**2.长轮询(Long Polling)** + +- **原理**:客户端发起请求后,若服务器暂时无新数据,则会保持连接,直到有新数据或超时才响应。客户端收到响应后立即发起下一次请求,实现“伪实时”。 +- **优点**: + - **实时性较好**:一旦有新数据可立即推送,无需等待下次定时请求。 + - **空响应减少**:减少了无效的空响应,提升了效率。 +- **缺点**: + - **服务器资源占用高**:需长时间维护大量连接,消耗服务器线程/连接数。 + - **资源浪费大**:每次响应后仍需重新建立连接,且依然基于 HTTP 单向请求-响应机制。 + +**3. WebSocket** + +- **原理**:客户端与服务器通过一次 HTTP Upgrade 握手后,建立一条持久的 TCP 连接。之后,双方可以随时、主动地发送数据,实现真正的全双工、低延迟通信。 +- **优点**: + - **实时性强**:数据可即时双向收发,延迟极低。 + - **资源效率高**:连接持续,无需反复建立/关闭,减少资源消耗。 + - **功能强大**:支持服务端主动推送消息、客户端主动发起通信。 +- **缺点**: + - **使用限制**:需要服务器和客户端都支持 WebSocket 协议。对连接管理有一定要求(如心跳保活、断线重连等)。 + - **实现麻烦**:实现起来比短轮询和长轮询要更麻烦一些。 + +![Websocket 示意图](https://oss.javaguide.cn/github/javaguide/system-design/web-real-time-message-push/1460000042192394.png) + ### SSE 与 WebSocket 有什么区别? -> 摘自[Web 实时消息推送详解](https://javaguide.cn/system-design/web-real-time-message-push.html)。 +SSE (Server-Sent Events) 和 WebSocket 都是用来实现服务器向浏览器实时推送消息的技术,让网页内容能自动更新,而不需要用户手动刷新。虽然目标相似,但它们在工作方式和适用场景上有几个关键区别: + +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 该如何选择?** +![](https://oss.javaguide.cn/github/javaguide/cs-basics/network/deepseek-sse.png) -SSE 好像一直不被大家所熟知,一部分原因是出现了 WebSocket,这个提供了更丰富的协议来执行双向、全双工通信。对于游戏、即时通信以及需要双向近乎实时更新的场景,拥有双向通道更具吸引力。 +![](https://oss.javaguide.cn/github/javaguide/cs-basics/network/deepseek-sse-eventstream.png) -但是,在某些情况下,不需要从客户端发送数据。而你只需要一些服务器操作的更新。比如:站内信、未读消息数、状态更新、股票行情、监控数量等场景,SSE 不管是从实现的难易和成本上都更加有优势。此外,SSE 具有 WebSocket 在设计上缺乏的多种功能,例如:自动重新连接、事件 ID 和发送任意事件的能力。 +可以看到,响应头应里包含了 `text/event-stream`,说明使用的确实是SSE。并且,响应数据也确实是持续分块传输。 ## PING diff --git a/docs/database/redis/redis-questions-02.md b/docs/database/redis/redis-questions-02.md index 37ef43ea72a..08e5e0a8e43 100644 --- a/docs/database/redis/redis-questions-02.md +++ b/docs/database/redis/redis-questions-02.md @@ -525,11 +525,13 @@ Redis 中的大部分命令都是 O(1) 时间复杂度,但也有少部分 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 会把最早的执行命令依次舍弃。 -⚠️注意:由于慢查询日志会占用一定内存空间,如果设置最大记录条数过大,可能会导致内存占用过高的问题。 +⚠️ 注意:由于慢查询日志会占用一定内存空间,如果设置最大记录条数过大,可能会导致内存占用过高的问题。 `slowlog-log-slower-than` 和 `slowlog-max-len` 的默认配置如下(可以自行修改): @@ -569,12 +571,12 @@ 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`。 @@ -731,15 +733,27 @@ Bloom Filter 会使用一个较大的 bit 数组来保存所有的数据,数 ### 如何保证缓存和数据库数据的一致性? -细说的话可以扯很多,但是我觉得其实没太大必要(小声 BB:很多解决方案我也没太弄明白)。我个人觉得引入缓存之后,如果为了短时间的不一致性问题,选择让系统设计变得更加复杂的话,完全没必要。 +缓存和数据库一致性是个挺常见的技术挑战。引入缓存主要是为了提升性能、减轻数据库压力,但确实会带来数据不一致的风险。绝对的一致性往往意味着更高的系统复杂度和性能开销,所以实践中我们通常会根据业务场景选择合适的策略,在性能和一致性之间找到一个平衡点。 + +下面单独对 **Cache Aside Pattern(旁路缓存模式)** 来聊聊。这是非常常用的一种缓存读写策略,它的读写逻辑是这样的: + +- **读操作**: + 1. 先尝试从缓存读取数据。 + 2. 如果缓存命中,直接返回数据。 + 3. 如果缓存未命中,从数据库查询数据,将查到的数据放入缓存并返回数据。 +- **写操作**: + 1. 先更新数据库。 + 2. 再直接删除缓存中对应的数据。 + +图解如下: -下面单独对 **Cache Aside Pattern(旁路缓存模式)** 来聊聊。 +![](https://oss.javaguide.cn/github/javaguide/database/redis/cache-aside-write.png) -Cache Aside Pattern 中遇到写请求是这样的:更新数据库,然后直接删除缓存。 +![](https://oss.javaguide.cn/github/javaguide/database/redis/cache-aside-read.png) 如果更新数据库成功,而删除缓存这一步失败的情况的话,简单说有两个解决方案: -1. **缓存失效时间变短**(不推荐,治标不治本):我们让缓存数据的过期时间变短,这样的话缓存就会从数据库中加载数据。另外,这种解决办法对于先操作缓存后操作数据库的场景不适用。 +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)。 diff --git a/docs/java/basis/java-basic-questions-03.md b/docs/java/basis/java-basic-questions-03.md index d98297398a4..496e18827da 100644 --- a/docs/java/basis/java-basic-questions-03.md +++ b/docs/java/basis/java-basic-questions-03.md @@ -321,52 +321,68 @@ printArray( stringArray ); 关于反射的详细解读,请看这篇文章 [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 中的一大利器 **注解** 的实现也用到了反射。 - -为什么你使用 Spring 的时候 ,一个`@Component`注解就声明了一个类为 Spring Bean 呢?为什么你通过一个 `@Value`注解就读取到配置文件中的值呢?究竟是怎么起作用的呢? +**4.对象关系映射(ORM)** -这些都是因为你可以基于反射分析类,然后获取到类/属性/方法/方法的参数上的注解。你获取到注解之后,就可以做进一步的处理。 +像 MyBatis、Hibernate 这种框架,能帮你把数据库查出来的一行行数据,自动变成一个个 Java 对象。它是怎么知道数据库字段对应哪个 Java 属性的?还是靠反射。它通过反射获取 Java 类的属性列表,然后把查询结果按名字或配置对应起来,再用反射调用 setter 或直接修改字段值。反过来,保存对象到数据库时,也是用反射读取属性值来拼 SQL。 ## 注解 From dbdd3aaeddbb6f5e8d2ea31e3fb1e418452321cc Mon Sep 17 00:00:00 2001 From: Guide Date: Sun, 4 May 2025 18:24:21 +0800 Subject: [PATCH 60/74] =?UTF-8?q?[docs=20add]=E7=B3=BB=E7=BB=9F=E8=AE=BE?= =?UTF-8?q?=E8=AE=A1-=E6=96=B0=E5=A2=9E=E6=96=87=E7=AB=A0=EF=BC=9A?= =?UTF-8?q?=E4=B8=BA=E4=BB=80=E4=B9=88=E5=89=8D=E5=90=8E=E7=AB=AF=E9=83=BD?= =?UTF-8?q?=E8=A6=81=E5=81=9A=E6=95=B0=E6=8D=AE=E6=A0=A1=E9=AA=8C?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- README.md | 12 +- docs/.vuepress/sidebar/index.ts | 1 + docs/home.md | 12 +- .../system-design/security/data-validation.md | 203 ++++++++++++++++++ 4 files changed, 214 insertions(+), 14 deletions(-) create mode 100644 docs/system-design/security/data-validation.md diff --git a/README.md b/README.md index c86501d0598..e193e6b5b8f 100755 --- a/README.md +++ b/README.md @@ -313,15 +313,13 @@ JVM 这部分内容主要参考 [JVM 虚拟机规范-Java8](https://docs.oracle. - [JWT 优缺点分析以及常见问题解决方案](./docs/system-design/security/advantages-and-disadvantages-of-jwt.md) - [SSO 单点登录详解](./docs/system-design/security/sso-intro.md) - [权限系统设计详解](./docs/system-design/security/design-of-authority-system.md) -- [常见加密算法总结](./docs/system-design/security/encryption-algorithms.md) - -#### 数据脱敏 -数据脱敏说的就是我们根据特定的规则对敏感信息数据进行变形,比如我们把手机号、身份证号某些位数使用 \* 来代替。 +#### 数据安全 -#### 敏感词过滤 - -[敏感词过滤方案总结](./docs/system-design/security/sentive-words-filter.md) +- [常见加密算法总结](./docs/system-design/security/encryption-algorithms.md) +- [敏感词过滤方案总结](./docs/system-design/security/sentive-words-filter.md) +- [数据脱敏方案总结](./docs/system-design/security/data-desensitization.md) +- [为什么前后端都要做数据校验](./docs/system-design/security/data-validation.md) ### 定时任务 diff --git a/docs/.vuepress/sidebar/index.ts b/docs/.vuepress/sidebar/index.ts index b277e1a8606..6a3c73769d4 100644 --- a/docs/.vuepress/sidebar/index.ts +++ b/docs/.vuepress/sidebar/index.ts @@ -469,6 +469,7 @@ export default sidebar({ "encryption-algorithms", "sentive-words-filter", "data-desensitization", + "data-validation", ], }, "system-design-questions", diff --git a/docs/home.md b/docs/home.md index 015a9105da3..047a09fe9ad 100644 --- a/docs/home.md +++ b/docs/home.md @@ -299,15 +299,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) ### 定时任务 diff --git a/docs/system-design/security/data-validation.md b/docs/system-design/security/data-validation.md new file mode 100644 index 00000000000..fdeb6fb95cc --- /dev/null +++ b/docs/system-design/security/data-validation.md @@ -0,0 +1,203 @@ +--- +title: 为什么前后端都要做数据校验 +category: 系统设计 +tag: + - 安全 +--- + +> 相关面试题: +> +> - 前端做了校验,后端还还需要做校验吗? +> - 前端已经做了数据校验,为什么后端还需要再做一遍同样(甚至更严格)的校验呢? +> - 前端/后端需要对哪些内容进行校验? + +咱们平时做 Web 开发,不管是写前端页面还是后端接口,都离不开跟数据打交道。那怎么保证这些传来传去的数据是靠谱的、安全的呢?这就得靠**数据校验**了。而且,这活儿,前端得干,后端**更得干**,还得加上**权限校验**这道重要的“锁”,缺一不可! + +为啥这么说?你想啊,前端校验主要是为了用户体验和挡掉一些明显的“瞎填”数据,但懂点技术的人绕过前端校验简直不要太轻松(比如直接用 Postman 之类的工具发请求)。所以,**后端校验才是咱们系统安全和数据准确性的最后一道,也是最硬核的防线**。它得确保进到系统里的数据不仅格式对,还得符合业务规矩,最重要的是,执行这个操作的人得有**权限**! + +![](https://oss.javaguide.cn/github/javaguide/system-design/security/user-input-validation.png) + +## 前端校验 + +前端校验就像个贴心的门卫,主要目的是在用户填数据的时候,就赶紧告诉他哪儿不对,让他改,省得提交了半天,结果后端说不行,还得重来。这样做的好处显而易见: + +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 权限模型或者其简化版本。用一个图来描述如下: + +![RBAC 权限模型示意图](https://oss.javaguide.cn/github/javaguide/system-design/security/design-of-authority-system/rbac.png) + +关于权限系统设计的详细介绍,可以看这篇文章:[权限系统设计详解](https://javaguide.cn/system-design/security/design-of-authority-system.html)。 + +## 总结 + +总而言之,要想构建一个安全、稳定、用户体验好的 Web 应用,前后端数据校验和后端权限校验这三道关卡,都得设好,而且各有侧重: + +- **前端数据校验:** 提升用户体验,减少无效请求,是第一道“友好”的防线。 +- **后端数据校验:** 保证数据格式正确、符合业务规则,是防止“脏数据”入库的“技术”防线。 Bean Validation 允许我们用注解的方式,直接在 JavaBean(比如我们的 DTO 对象)的属性上声明校验规则,非常方便。 +- **后端权限校验:** 确保“对的人”做“对的事”,是防止越权操作的“安全”防线。Spring Security、Shiro、Sa-Token 等框架可以帮助我们实现权限校验。 + +## 参考 + +- 为什么前后端都需要进行数据校验?: +- 权限系统设计详解: From b1867d3f44e277846d317d39e6cf7b92fe9cb66b Mon Sep 17 00:00:00 2001 From: Guide Date: Thu, 8 May 2025 15:10:25 +0800 Subject: [PATCH 61/74] =?UTF-8?q?fix:=20cas=20=E7=A4=BA=E4=BE=8B=E4=BB=A3?= =?UTF-8?q?=E7=A0=81=E4=BF=AE=E6=AD=A3?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../network/other-network-questions2.md | 16 +++- docs/java/basis/unsafe.md | 87 ++++++++++++++++++- 2 files changed, 99 insertions(+), 4 deletions(-) diff --git a/docs/cs-basics/network/other-network-questions2.md b/docs/cs-basics/network/other-network-questions2.md index 9d193f4913d..dcfd4f46186 100644 --- a/docs/cs-basics/network/other-network-questions2.md +++ b/docs/cs-basics/network/other-network-questions2.md @@ -73,9 +73,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/2.0 中存在的队头阻塞问题。队头阻塞是指在 HTTP/2.0 中,多个 HTTP 请求和响应共享一个 TCP 连接,如果其中一个请求或响应因为网络拥塞或丢包而被阻塞,那么后续的请求或响应也无法发送,导致整个连接的效率降低。这是由于 HTTP/2.0 在单个 TCP 连接上使用了多路复用,受到 TCP 拥塞控制的影响,少量的丢包就可能导致整个 TCP 连接上的所有流被阻塞。HTTP/3.0 在一定程度上解决了队头阻塞问题,一个连接建立多个不同的数据流,这些数据流之间独立互不影响,某个数据流发生丢包了,其数据流不受影响(本质上是多路复用+轮询)。 +- **HTTP/1.x 和 HTTP/2.0**:这两个版本的 HTTP 协议都明确建立在 TCP 之上。TCP 提供了可靠的、面向连接的传输,确保数据按序、无差错地到达,这对于网页内容的正确展示非常重要。发送 HTTP 请求前,需要先通过 TCP 的三次握手建立连接。 +- **HTTP/3.0**:这是一个重大的改变。HTTP/3 弃用了 TCP,转而使用 QUIC 协议,而 QUIC 是构建在 UDP 之上的。 + +![http-3-implementation](https://oss.javaguide.cn/github/javaguide/cs-basics/network/http-3-implementation.png) + +**为什么 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 握手: diff --git a/docs/java/basis/unsafe.md b/docs/java/basis/unsafe.md index efd1337d39c..fff31af808c 100644 --- a/docs/java/basis/unsafe.md +++ b/docs/java/basis/unsafe.md @@ -516,11 +516,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)只会被成功设置并打印一次,并且是按顺序进行的。 ![](https://oss.javaguide.cn/github/javaguide/java/basis/unsafe/image-20220717144939826.png) -需要注意的是,在调用`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()`)来优化。 ### 线程调度 From 8745a28fa1340e5d5d7179575a13122c075184d6 Mon Sep 17 00:00:00 2001 From: Guide Date: Mon, 12 May 2025 16:48:50 +0800 Subject: [PATCH 62/74] =?UTF-8?q?update&fix:=E5=9F=BA=E4=BA=8ETCP/UDP=20?= =?UTF-8?q?=E7=9A=84=E5=8D=8F=E8=AE=AE=E7=9A=84=E5=8D=8F=E8=AE=AE=E5=AE=8C?= =?UTF-8?q?=E5=96=84&Redis=E8=B7=B3=E8=A1=A8=E6=8F=8F=E8=BF=B0=E4=BF=AE?= =?UTF-8?q?=E6=AD=A3?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../network/other-network-questions2.md | 53 ++++++++++++------- docs/database/redis/redis-skiplist.md | 34 +++++++----- 2 files changed, 54 insertions(+), 33 deletions(-) diff --git a/docs/cs-basics/network/other-network-questions2.md b/docs/cs-basics/network/other-network-questions2.md index dcfd4f46186..67c731f44c0 100644 --- a/docs/cs-basics/network/other-network-questions2.md +++ b/docs/cs-basics/network/other-network-questions2.md @@ -101,25 +101,40 @@ HTTP/3.0 之前是基于 TCP 协议的,而 HTTP/3.0 将弃用 TCP,改用 ** - - -### 使用 TCP 的协议有哪些?使用 UDP 的协议有哪些? - -**运行于 TCP 协议之上的协议**: - -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. …… - -**运行于 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/UDP 的协议? + +TCP (传输控制协议) 和 UDP (用户数据报协议) 是互联网传输层的两大核心协议,它们为各种应用层协议提供了基础的通信服务。以下是一些常见的、分别构建在 TCP 和 UDP 之上的应用层协议: + +**运行于 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 | 同步计算机时钟 | 用于在网络中的计算机之间同步时间,确保时间的一致性。 | + +**总结一下:** + +- **TCP** 更适合那些对数据**可靠性、完整性和顺序性**要求高的应用,如网页浏览 (HTTP/HTTPS)、文件传输 (FTP/SFTP)、邮件收发 (SMTP/POP3/IMAP)。 +- **UDP** 则更适用于那些对**实时性要求高、能容忍少量数据丢失**的应用,如域名解析 (DNS)、实时音视频 (RTP)、在线游戏、网络管理 (SNMP) 等。 ### TCP 三次握手和四次挥手(非常重要) diff --git a/docs/database/redis/redis-skiplist.md b/docs/database/redis/redis-skiplist.md index 1194f736374..11f0c32b665 100644 --- a/docs/database/redis/redis-skiplist.md +++ b/docs/database/redis/redis-skiplist.md @@ -283,33 +283,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,查找结束。 - ![](https://oss.javaguide.cn/javaguide/database/redis/skiplist/202401222005323.png) +- **从最高层级开始 (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; // 未找到 } ``` From 237ec3bec38f1903e064e2f468278dc9c1c126bf Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E9=86=89=E9=86=89=E9=86=89=E9=86=89=E9=86=89=E5=B8=85?= =?UTF-8?q?=E7=9A=84=E8=80=81=E8=99=8E12138?= Date: Mon, 12 May 2025 20:59:29 +0800 Subject: [PATCH 63/74] =?UTF-8?q?docs(message-queue):=20=E6=9B=B4=E6=96=B0?= =?UTF-8?q?=E5=BB=B6=E6=97=B6/=E5=AE=9A=E6=97=B6=E6=B6=88=E6=81=AF?= =?UTF-8?q?=E6=94=AF=E6=8C=81=E7=9A=84=E6=B6=88=E6=81=AF=E9=98=9F=E5=88=97?= =?UTF-8?q?=E5=88=97=E8=A1=A8?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 移除了 Kafka,因为 Kafka 不直接支持定时/延时消息 - 保留了 RocketMQ、RabbitMQ 和 Pulsar 作为支持定时/延时消息的消息队列示例 --- docs/high-performance/message-queue/message-queue.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/high-performance/message-queue/message-queue.md b/docs/high-performance/message-queue/message-queue.md index b366836ec2c..5874f290298 100644 --- a/docs/high-performance/message-queue/message-queue.md +++ b/docs/high-performance/message-queue/message-queue.md @@ -101,7 +101,7 @@ RocketMQ、 Kafka、Pulsar、QMQ 都提供了事务相关的功能。事务允 ### 延时/定时处理 -消息发送后不会立即被消费,而是指定一个时间,到时间后再消费。大部分消息队列,例如 RocketMQ、RabbitMQ、Pulsar、Kafka,都支持定时/延时消息。 +消息发送后不会立即被消费,而是指定一个时间,到时间后再消费。大部分消息队列,例如 RocketMQ、RabbitMQ、Pulsar,都支持定时/延时消息。 ![](https://oss.javaguide.cn/github/javaguide/tools/docker/rocketmq-schedule-message.png) From db72d110fffda347e96fb925a63b11390b94cb9a Mon Sep 17 00:00:00 2001 From: Guide Date: Mon, 12 May 2025 21:21:06 +0800 Subject: [PATCH 64/74] =?UTF-8?q?add:=20mysql=E9=9D=A2=E8=AF=95=E9=A2=98?= =?UTF-8?q?=20-=20=E6=89=8B=E6=9C=BA=E5=8F=B7=E5=AD=98=E5=82=A8=E7=94=A8?= =?UTF-8?q?=20INT=20=E8=BF=98=E6=98=AF=20VARCHAR=EF=BC=9F?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- docs/database/mysql/mysql-questions-01.md | 31 +++++++++++++++++++++++ 1 file changed, 31 insertions(+) diff --git a/docs/database/mysql/mysql-questions-01.md b/docs/database/mysql/mysql-questions-01.md index b1493a64662..afff5482eec 100644 --- a/docs/database/mysql/mysql-questions-01.md +++ b/docs/database/mysql/mysql-questions-01.md @@ -187,6 +187,37 @@ TIMESTAMP 只需要使用 4 个字节的存储空间,但是 DATETIME 需要耗 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 等类型可以。 + +**关于 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 基础架构 > 建议配合 [SQL 语句在 MySQL 中的执行过程](./how-sql-executed-in-mysql.md) 这篇文章来理解 MySQL 基础架构。另外,“一个 SQL 语句在 MySQL 中的执行流程”也是面试中比较常问的一个问题。 From c901f230c662c0ed73f4f507b214b07444da3898 Mon Sep 17 00:00:00 2001 From: serendipity <48009043+uenglish@users.noreply.github.com> Date: Tue, 13 May 2025 14:44:28 +0800 Subject: [PATCH 65/74] =?UTF-8?q?fix:=20MySQL=E6=95=B0=E6=8D=AE=E5=BA=93?= =?UTF-8?q?=E6=96=87=E6=A1=A3=E6=8F=8F=E8=BF=B0=E4=BF=AE=E6=AD=A3?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- docs/database/mysql/mysql-questions-01.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/database/mysql/mysql-questions-01.md b/docs/database/mysql/mysql-questions-01.md index afff5482eec..7f93eb605e6 100644 --- a/docs/database/mysql/mysql-questions-01.md +++ b/docs/database/mysql/mysql-questions-01.md @@ -895,7 +895,7 @@ MySQL 性能优化是一个系统性工程,涉及多个方面,在面试中 - **读写分离:** 将读操作和写操作分离到不同的数据库实例,提升数据库的并发处理能力。 - **分库分表:** 将数据分散到多个数据库实例或数据表中,降低单表数据量,提升查询效率。但要权衡其带来的复杂性和维护成本,谨慎使用。 -- **数据冷热分离**:根据数据的访问频率和业务重要性,将数据分为冷数据和热数据,冷数据一般存储在存储在低成本、低性能的介质中,热数据高性能存储介质中。 +- **数据冷热分离**:根据数据的访问频率和业务重要性,将数据分为冷数据和热数据,冷数据一般存储在低成本、低性能的介质中,热数据存储在高性能存储介质中。 - **缓存机制:** 使用 Redis 等缓存中间件,将热点数据缓存到内存中,减轻数据库压力。这个非常常用,提升效果非常明显,性价比极高! **4. 其他优化手段** From 6f3f2c90fe9d8d28f9a21c6cb58a769753d1b716 Mon Sep 17 00:00:00 2001 From: Wenweigood <76194364+Wenweigood@users.noreply.github.com> Date: Tue, 13 May 2025 19:44:03 +0800 Subject: [PATCH 66/74] =?UTF-8?q?=E5=8E=BB=E9=99=A4=E7=A9=BA=E6=A0=BC?= =?UTF-8?q?=EF=BC=8C=E4=BC=98=E5=8C=96=E6=A0=BC=E5=BC=8F?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- docs/java/concurrent/java-concurrent-questions-02.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/java/concurrent/java-concurrent-questions-02.md b/docs/java/concurrent/java-concurrent-questions-02.md index f3bb411a682..40c1b140434 100644 --- a/docs/java/concurrent/java-concurrent-questions-02.md +++ b/docs/java/concurrent/java-concurrent-questions-02.md @@ -60,7 +60,7 @@ public class Singleton { private Singleton() { } - public static Singleton getUniqueInstance() { + public static Singleton getUniqueInstance() { //先判断对象是否已经实例过,没有实例化过才进入加锁代码 if (uniqueInstance == null) { //类对象加锁 From 7980d032650f7edb265eba66308cdde31ba89b8c Mon Sep 17 00:00:00 2001 From: Wenweigood <76194364+Wenweigood@users.noreply.github.com> Date: Tue, 13 May 2025 19:57:42 +0800 Subject: [PATCH 67/74] =?UTF-8?q?=E9=9B=86=E5=90=88=E8=BD=ACMap=E6=8A=9B?= =?UTF-8?q?=E5=87=BA=E5=BC=82=E5=B8=B8=E7=9A=84=E5=B8=B8=E8=A7=81=E6=83=85?= =?UTF-8?q?=E5=BD=A2=E6=8F=90=E9=86=92?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- docs/java/collection/java-collection-precautions-for-use.md | 1 + 1 file changed, 1 insertion(+) diff --git a/docs/java/collection/java-collection-precautions-for-use.md b/docs/java/collection/java-collection-precautions-for-use.md index cb68403f57c..9bd3a4084d5 100644 --- a/docs/java/collection/java-collection-precautions-for-use.md +++ b/docs/java/collection/java-collection-precautions-for-use.md @@ -134,6 +134,7 @@ public static T requireNonNull(T obj) { return obj; } ``` +> `Collectors`也提供了无需mergeFunction的`toMap()`方法,但此时若出现key冲突,则会抛出`duplicateKeyException`异常,因此强烈建议使用`toMap()`方法必填mergeFunction。 ## 集合遍历 From 521b4d409127f7f45751916d56f98359d2a74cdb Mon Sep 17 00:00:00 2001 From: serendipity <48009043+uenglish@users.noreply.github.com> Date: Wed, 14 May 2025 00:19:27 +0800 Subject: [PATCH 68/74] =?UTF-8?q?docs(database-redis):=20=E5=91=BD?= =?UTF-8?q?=E4=BB=A4=E9=A3=8E=E6=A0=BC=E7=BB=9F=E4=B8=80=E4=B8=BA=E5=A4=A7?= =?UTF-8?q?=E5=86=99?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- docs/database/redis/redis-data-structures-01.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/database/redis/redis-data-structures-01.md b/docs/database/redis/redis-data-structures-01.md index 9dfb0c3eaa5..7d993752138 100644 --- a/docs/database/redis/redis-data-structures-01.md +++ b/docs/database/redis/redis-data-structures-01.md @@ -182,7 +182,7 @@ Redis 中的 List 其实就是链表数据结构的实现。我在 [线性数据 "value3" ``` -我专门画了一个图方便大家理解 `RPUSH` , `LPOP` , `lpush` , `RPOP` 命令: +我专门画了一个图方便大家理解 `RPUSH` , `LPOP` , `LPUSH` , `RPOP` 命令: ![](https://oss.javaguide.cn/github/javaguide/database/redis/redis-list.png) From d8d68c7fae6fb4d5e567d589a2edcc776269caba Mon Sep 17 00:00:00 2001 From: flying pig <117554874+flying-pig-z@users.noreply.github.com> Date: Thu, 15 May 2025 09:57:12 +0800 Subject: [PATCH 69/74] =?UTF-8?q?feat:=20=E5=B0=86BigDecimal=E6=B3=A8?= =?UTF-8?q?=E9=87=8A=E4=B8=AD=E5=9B=9B=E8=88=8D=E4=BA=94=E5=85=A5=E6=94=B9?= =?UTF-8?q?=E6=88=90=E5=9B=9B=E8=88=8D=E5=85=AD=E5=85=A5=E4=BA=94=E6=88=90?= =?UTF-8?q?=E5=8F=8C?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- docs/java/basis/bigdecimal.md | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/docs/java/basis/bigdecimal.md b/docs/java/basis/bigdecimal.md index 7a9b549905a..acedfadc32f 100644 --- a/docs/java/basis/bigdecimal.md +++ b/docs/java/basis/bigdecimal.md @@ -230,7 +230,7 @@ public class BigDecimalUtil { /** * 提供(相对)精确的除法运算,当发生除不尽的情况时,精确到 - * 小数点以后10位,以后的数字四舍五入。 + * 小数点以后10位,以后的数字四舍六入五成双。 * * @param v1 被除数 * @param v2 除数 @@ -242,7 +242,7 @@ public class BigDecimalUtil { /** * 提供(相对)精确的除法运算。当发生除不尽的情况时,由scale参数指 - * 定精度,以后的数字四舍五入。 + * 定精度,以后的数字四舍六入五成双。 * * @param v1 被除数 * @param v2 除数 @@ -260,11 +260,11 @@ public class BigDecimalUtil { } /** - * 提供精确的小数位四舍五入处理。 + * 提供精确的小数位四舍六入五成双处理。 * - * @param v 需要四舍五入的数字 + * @param v 需要四舍六入五成双的数字 * @param scale 小数点后保留几位 - * @return 四舍五入后的结果 + * @return 四舍六入五成双后的结果 */ public static double round(double v, int scale) { if (scale < 0) { @@ -288,7 +288,7 @@ public class BigDecimalUtil { } /** - * 提供精确的类型转换(Int)不进行四舍五入 + * 提供精确的类型转换(Int)不进行四舍六入五成双 * * @param v 需要被转换的数字 * @return 返回转换结果 From 466441a4730eb67dc0cabcb66a658912a771d577 Mon Sep 17 00:00:00 2001 From: flying pig <117554874+flying-pig-z@users.noreply.github.com> Date: Thu, 15 May 2025 10:13:26 +0800 Subject: [PATCH 70/74] =?UTF-8?q?feat:=E4=BF=AE=E5=A4=8Djava-keyword-summa?= =?UTF-8?q?ry=E6=96=87=E6=A1=A3=E7=9A=84=E4=B8=80=E4=BA=9B=E9=94=99?= =?UTF-8?q?=E8=AF=AF?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 1.showNumber()方法缺少返回值类型 2.静态导包的例子中,注释表达不太准确(单一导包和通配符导包效果不完全相同) 3.关于静态代码块的描述中重复表达"的时候" --- docs/java/basis/java-keyword-summary.md | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/docs/java/basis/java-keyword-summary.md b/docs/java/basis/java-keyword-summary.md index daf13c9ec14..1d21e2467ed 100644 --- a/docs/java/basis/java-keyword-summary.md +++ b/docs/java/basis/java-keyword-summary.md @@ -54,7 +54,7 @@ super 关键字用于从子类访问父类的变量和方法。 例如: ```java public class Super { protected int number; - protected showNumber() { + protected void showNumber() { System.out.println("number = " + number); } } @@ -199,7 +199,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 +250,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: From 635d5786a0435d2a17d8cc4bf110554bb3cc6dbc Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E5=A6=82=E9=81=87=E5=8F=A4=E5=89=91?= <41989003+L1468999760@users.noreply.github.com> Date: Tue, 20 May 2025 00:37:45 +0800 Subject: [PATCH 71/74] fix: spell mistake --- docs/system-design/web-real-time-message-push.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/system-design/web-real-time-message-push.md b/docs/system-design/web-real-time-message-push.md index f08e1b2e716..ce39f293831 100644 --- a/docs/system-design/web-real-time-message-push.md +++ b/docs/system-design/web-real-time-message-push.md @@ -221,7 +221,7 @@ SSE 与 WebSocket 作用相似,都可以建立服务端与浏览器之间的 SSE 好像一直不被大家所熟知,一部分原因是出现了 WebSocket,这个提供了更丰富的协议来执行双向、全双工通信。对于游戏、即时通信以及需要双向近乎实时更新的场景,拥有双向通道更具吸引力。 -但是,在某些情况下,不需要从客户端发送数据。而你只需要一些服务器操作的更新。比如:站内信、未读消息数、状态更新、股票行情、监控数量等场景,SEE 不管是从实现的难易和成本上都更加有优势。此外,SSE 具有 WebSocket 在设计上缺乏的多种功能,例如:自动重新连接、事件 ID 和发送任意事件的能力。 +但是,在某些情况下,不需要从客户端发送数据。而你只需要一些服务器操作的更新。比如:站内信、未读消息数、状态更新、股票行情、监控数量等场景,SSE 不管是从实现的难易和成本上都更加有优势。此外,SSE 具有 WebSocket 在设计上缺乏的多种功能,例如:自动重新连接、事件 ID 和发送任意事件的能力。 前端只需进行一次 HTTP 请求,带上唯一 ID,打开事件流,监听服务端推送的事件就可以了 From 6b60d671dbf12c2602ef2db2a8702cc1de6e243e Mon Sep 17 00:00:00 2001 From: Guide Date: Tue, 27 May 2025 13:13:04 +0800 Subject: [PATCH 72/74] =?UTF-8?q?update&feat:=20=E7=BD=91=E7=BB=9C?= =?UTF-8?q?=E5=92=8C=E8=AE=A4=E8=AF=81=E7=99=BB=E5=BD=95=E9=83=A8=E5=88=86?= =?UTF-8?q?=E9=9D=A2=E8=AF=95=E9=A2=98=E5=AE=8C=E5=96=84&=E6=96=B0?= =?UTF-8?q?=E5=A2=9E=E4=B8=80=E4=B8=AAJVM=E6=89=8B=E5=86=99=E8=BD=AE?= =?UTF-8?q?=E5=AD=90?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- README.md | 2 + .../network/other-network-questions.md | 59 +++++++++++++++++-- .../java-concurrent-questions-03.md | 2 +- docs/open-source-project/practical-project.md | 2 + docs/open-source-project/system-design.md | 1 + .../basis-of-authority-certification.md | 15 ++--- docs/system-design/security/jwt-intro.md | 9 +-- 7 files changed, 74 insertions(+), 16 deletions(-) diff --git a/README.md b/README.md index e193e6b5b8f..893733ac401 100755 --- a/README.md +++ b/README.md @@ -8,6 +8,8 @@ [GitHub](https://github.com/Snailclimb/JavaGuide) | [Gitee](https://gitee.com/SnailClimb/JavaGuide) +Snailclimb%2FJavaGuide | Trendshift + > - **面试专版**:准备 Java 面试的小伙伴可以考虑面试专版:**[《Java 面试指北 》](./docs/zhuanlan/java-mian-shi-zhi-bei.md)** (质量很高,专为面试打造,配合 JavaGuide 食用)。 diff --git a/docs/cs-basics/network/other-network-questions.md b/docs/cs-basics/network/other-network-questions.md index 0b852b063ac..2f4da42d1a0 100644 --- a/docs/cs-basics/network/other-network-questions.md +++ b/docs/cs-basics/network/other-network-questions.md @@ -258,13 +258,64 @@ HTTP/1.1 队头阻塞的主要原因是无法多路复用: ### HTTP 是不保存状态的协议, 如何保存用户状态? -HTTP 是一种不保存状态,即无状态(stateless)协议。也就是说 HTTP 协议自身不对请求和响应之间的通信状态进行保存。那么我们如何保存用户状态呢?Session 机制的存在就是为了解决这个问题,Session 的主要作用就是通过服务端记录用户的状态。典型的场景是购物车,当你要添加商品到购物车的时候,系统不知道是哪个用户操作的,因为 HTTP 协议是无状态的。服务端给特定的用户创建特定的 Session 之后就可以标识这个用户并且跟踪这个用户了(一般情况下,服务器会在一定时间内保存这个 Session,过了时间限制,就会销毁这个 Session)。 +HTTP 协议本身是 **无状态的 (stateless)** 。这意味着服务器默认情况下无法区分两个连续的请求是否来自同一个用户,或者同一个用户之前的操作是什么。这就像一个“健忘”的服务员,每次你跟他说话,他都不知道你是谁,也不知道你之前点过什么菜。 -在服务端保存 Session 的方法很多,最常用的就是内存和数据库(比如是使用内存数据库 redis 保存)。既然 Session 存放在服务器端,那么我们如何实现 Session 跟踪呢?大部分情况下,我们都是通过在 Cookie 中附加一个 Session ID 来方式来跟踪。 +但在实际的 Web 应用中,比如网上购物、用户登录等场景,我们显然需要记住用户的状态(例如购物车里的商品、用户的登录信息)。为了解决这个问题,主要有以下几种常用机制: -**Cookie 被禁用怎么办?** +**方案一:Session (会话) 配合 Cookie (主流方式):** -最常用的就是利用 URL 重写把 Session ID 直接附加在 URL 路径的后面。 +![](https://oss.javaguide.cn/github/javaguide/system-design/security/session-cookie-authentication-process.png) + +这可以说是最经典也是最常用的方法了。基本流程是这样的: + +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 数据本身存储在服务器端。常见的存储方式有: + +- **服务器内存**:实现简单,访问速度快,但服务器重启数据会丢失,且不利于多服务器间的负载均衡。这种方式适合简单且用户量不大的业务场景。 +- **数据库 (如 MySQL, PostgreSQL)**:数据持久化,但读写性能相对较低,一般不会使用这种方式。 +- **分布式缓存 (如 Redis)**:性能高,支持分布式部署,是目前大规模应用中非常主流的方案。 + +**方案二:当 Cookie 被禁用时:URL 重写 (URL Rewriting)** + +如果用户的浏览器禁用了 Cookie,或者某些情况下不便使用 Cookie,还有一种备选方案是 URL 重写。这种方式会将 `SessionID` 直接附加到 URL 的末尾,作为参数传递。例如:。服务器端会解析 URL 中的 `sessionid` 参数来获取 `SessionID`,进而找到对应的 Session 数据。 + +这种方法一般不会使用,存在以下缺点: + +- URL 会变长且不美观; +- `SessionID` 暴露在 URL 中,安全性较低(容易被复制、分享或记录在日志中); +- 对搜索引擎优化 (SEO) 可能不友好。 + +**方案三:Token-based 认证 (如 JWT - JSON Web Tokens)** + +这是一种越来越流行的无状态认证方式,尤其适用于前后端分离的架构和微服务。 + +![ JWT 身份验证示意图](https://oss.javaguide.cn/github/javaguide/system-design/jwt/jwt-authentication%20process.png) + +以 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 的区别是什么? diff --git a/docs/java/concurrent/java-concurrent-questions-03.md b/docs/java/concurrent/java-concurrent-questions-03.md index 84d58459d09..dc8dc62e979 100644 --- a/docs/java/concurrent/java-concurrent-questions-03.md +++ b/docs/java/concurrent/java-concurrent-questions-03.md @@ -753,7 +753,7 @@ CPU 密集型简单理解就是利用 CPU 计算能力的任务比如你在内 美团技术团队的思路是主要对线程池的核心参数实现自定义可配置。这三个核心参数是: -- **`corePoolSize` :** 核心线程数线程数定义了最小可以同时运行的线程数量。 +- **`corePoolSize` :** 核心线程数定义了最小可以同时运行的线程数量。 - **`maximumPoolSize` :** 当队列中存放的任务达到队列容量的时候,当前可以同时运行的线程数量变为最大线程数。 - **`workQueue`:** 当新任务来的时候会先判断当前运行的线程数量是否达到核心线程数,如果达到的话,新任务就会被存放在队列中。 diff --git a/docs/open-source-project/practical-project.md b/docs/open-source-project/practical-project.md index 1c5e2d70dbb..5c39243fa28 100644 --- a/docs/open-source-project/practical-project.md +++ b/docs/open-source-project/practical-project.md @@ -39,6 +39,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)等文件类型。 @@ -79,6 +80,7 @@ icon: project - [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/system-design.md b/docs/open-source-project/system-design.md index 5471f2d07b3..9d350e6642f 100644 --- a/docs/open-source-project/system-design.md +++ b/docs/open-source-project/system-design.md @@ -29,6 +29,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 映射框架。 ### 其他 diff --git a/docs/system-design/security/basis-of-authority-certification.md b/docs/system-design/security/basis-of-authority-certification.md index c21f80568d7..2dc7e2c6c61 100644 --- a/docs/system-design/security/basis-of-authority-certification.md +++ b/docs/system-design/security/basis-of-authority-certification.md @@ -137,15 +137,16 @@ public String readAllCookies(HttpServletRequest request) { ![](https://oss.javaguide.cn/github/javaguide/system-design/security/session-cookie-authentication-process.png) 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/jwt-intro.md b/docs/system-design/security/jwt-intro.md index f4087fde8e6..6b8213e9e91 100644 --- a/docs/system-design/security/jwt-intro.md +++ b/docs/system-design/security/jwt-intro.md @@ -133,10 +133,11 @@ HMACSHA256( 简化后的步骤如下: -1. 用户向服务器发送用户名、密码以及验证码用于登陆系统。 -2. 如果用户用户名、密码以及验证码校验正确的话,服务端会返回已经签名的 Token,也就是 JWT。 -3. 用户以后每次向后端发请求都在 Header 中带上这个 JWT 。 -4. 服务端检查 JWT 并从中获取用户相关信息。 +1. 用户向服务器发送用户名、密码以及验证码用于登陆系统; +2. 如果用户用户名、密码以及验证码校验正确的话,服务端会返回已经签名的 Token,也就是 JWT; +3. 客户端收到 Token 后自己保存起来(比如浏览器的 `localStorage` ); +4. 用户以后每次向后端发请求都在 Header 中带上这个 JWT ; +5. 服务端检查 JWT 并从中获取用户相关信息。 两点建议: From e953417f8129f28c4fa550a82ea83d835272ea38 Mon Sep 17 00:00:00 2001 From: Guide Date: Tue, 27 May 2025 13:33:18 +0800 Subject: [PATCH 73/74] Update README.md --- README.md | 2 -- 1 file changed, 2 deletions(-) diff --git a/README.md b/README.md index 893733ac401..da7542cae15 100755 --- a/README.md +++ b/README.md @@ -1,7 +1,5 @@ 推荐你通过在线阅读网站进行阅读,体验更好,速度更快!地址:[javaguide.cn](https://javaguide.cn/)。 -[](https://javaguide.cn/about-the-author/zhishixingqiu-two-years.html) -
[![logo](https://oss.javaguide.cn/github/javaguide/csdn/1c00413c65d1995993bf2b0daf7b4f03.png)](https://github.com/Snailclimb/JavaGuide) From 15597f1bf2bfec772406d9d27d311e8fc065f06b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E5=B0=8F=E6=96=B0?= <718949661@qq.com> Date: Tue, 3 Jun 2025 17:33:43 +0800 Subject: [PATCH 74/74] =?UTF-8?q?fix:=20=E8=AF=AD=E6=B3=95=E5=8B=98?= =?UTF-8?q?=E8=AF=AF?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- docs/java/collection/java-collection-questions-01.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/java/collection/java-collection-questions-01.md b/docs/java/collection/java-collection-questions-01.md index 417a2d10f53..d0a54da58b9 100644 --- a/docs/java/collection/java-collection-questions-01.md +++ b/docs/java/collection/java-collection-questions-01.md @@ -324,7 +324,7 @@ final void checkForComodification() { > 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`的实现,通过写时复制的思想保证在进行修改操作时复制出一份快照,基于这份快照完成添加或者删除操作后,将`CopyOnWriteArrayList`底层的数组引用指向这个新的数组空间,由此避免迭代时被并发修改所干扰所导致并发操作安全问题,当然这种做法也存缺点即进行遍历操作时无法获得实时结果: +该思想常运用于并发容器,最经典的实现就是`CopyOnWriteArrayList`的实现,通过写时复制的思想保证在进行修改操作时复制出一份快照,基于这份快照完成添加或者删除操作后,将`CopyOnWriteArrayList`底层的数组引用指向这个新的数组空间,由此避免迭代时被并发修改所干扰所导致并发操作安全问题,当然这种做法也存在缺点,即进行遍历操作时无法获得实时结果: ![](https://oss.javaguide.cn/github/javaguide/java/collection/fail-fast-and-fail-safe-copyonwritearraylist.png)