From 2c4d22bb329b96beff2e138e9875ddc0c32cf5de Mon Sep 17 00:00:00 2001 From: Guide Date: Thu, 12 Feb 2026 23:53:16 +0800 Subject: [PATCH 01/53] =?UTF-8?q?update:=E5=AE=8C=E5=96=84=E5=A6=82?= =?UTF-8?q?=E4=BD=95=E8=B7=A8=E7=BA=BF=E7=A8=8B=E4=BC=A0=E9=80=92=20Thread?= =?UTF-8?q?Local=20=E7=9A=84=E5=80=BC?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../java-concurrent-questions-03.md | 177 +++++++++++++++++- docs/zhuanlan/interview-guide.md | 6 +- 2 files changed, 170 insertions(+), 13 deletions(-) diff --git a/docs/java/concurrent/java-concurrent-questions-03.md b/docs/java/concurrent/java-concurrent-questions-03.md index 430c33f6999..a13da622d83 100644 --- a/docs/java/concurrent/java-concurrent-questions-03.md +++ b/docs/java/concurrent/java-concurrent-questions-03.md @@ -162,12 +162,26 @@ static class Entry extends WeakReference> { ### ⭐️如何跨线程传递 ThreadLocal 的值? -由于 `ThreadLocal` 的变量值存放在 `Thread` 里,而父子线程属于不同的 `Thread` 的。因此在异步场景下,父子线程的 `ThreadLocal` 值无法进行传递。 +**为什么 ThreadLocal 在异步场景下会失效?** -如果想要在异步场景下传递 `ThreadLocal` 值,有两种解决方案: +`ThreadLocal` 的值不在 `ThreadLocal` 对象中,而是存储在 `Thread` 里: -- `InheritableThreadLocal` :`InheritableThreadLocal` 是 JDK1.2 提供的工具,继承自 `ThreadLocal` 。使用 `InheritableThreadLocal` 时,会在创建子线程时,令子线程继承父线程中的 `ThreadLocal` 值,但是无法支持线程池场景下的 `ThreadLocal` 值传递。 -- `TransmittableThreadLocal` : `TransmittableThreadLocal` (简称 TTL) 是阿里巴巴开源的工具类,继承并加强了`InheritableThreadLocal`类,可以在线程池的场景下支持 `ThreadLocal` 值传递。项目地址:。 +```java +Thread → ThreadLocalMap → Entry(ThreadLocal, value) +``` + +`ThreadLocal` 数据结构如下图所示: + +![ThreadLocal 数据结构](https://oss.javaguide.cn/github/javaguide/java/concurrent/threadlocal-data-structure.png) + +异步执行往往意味着任务会从当前线程切换到另一个线程(例如线程池中的工作线程)执行。由于不同线程各自维护独立的 `ThreadLocalMap`,默认情况下 `ThreadLocal` 的上下文无法在异步执行中自动传递。 + +**如何跨线程传递 ThreadLocal 的值?** + +为了解决这个问题,业界有两套主流的解决方案,一套是 JDK 原生的,另一套是阿里巴巴开源的。 + +1. `InheritableThreadLocal` :JDK1.2 提供的一个类,继承自 `ThreadLocal` 。使用 `InheritableThreadLocal` 时,会在创建子线程时,令子线程继承父线程中的 `ThreadLocal` 值,但是无法支持线程池场景下的 `ThreadLocal` 值传递。 +2. `TransmittableThreadLocal` : `TransmittableThreadLocal` (简称 TTL) 是阿里巴巴开源的工具类,继承并加强了`InheritableThreadLocal`类,可以在线程池的场景下支持 `ThreadLocal` 值传递。项目地址:。 #### InheritableThreadLocal 原理 @@ -200,33 +214,176 @@ private void init(/* ... */) { } ``` +**`InheritableThreadLocal` 的方案有什么问题?** + +这个方案的缺陷在于它的**一次性**,也就是它只在线程创建时发生一次复制。然而,现在的开发中,我们会大量使用线程池,但线程池里的线程是被复用的。 + +想象一下,任务A在线程1中执行,把它的 `ThreadLocal` 值传给了线程池里的子线程2。任务A结束后,线程1去休息了。接着,任务B来了,它在线程3中执行,线程池又复用了刚才那个子线程2来执行任务B的一部分。此时,子线程2的`ThreadLocal`里还残留着任务A传给它的脏数据,而任务B(在线程3里)的上下文却完全没有传递过来。这就导致了数据污染和上下文丢失。 + #### TransmittableThreadLocal 原理 JDK 默认没有支持线程池场景下 `ThreadLocal` 值传递的功能,因此阿里巴巴开源了一套工具 `TransmittableThreadLocal` 来实现该功能。 -阿里巴巴无法改动 JDK 的源码,因此他内部通过 **装饰器模式** 在原有的功能上做增强,以此来实现线程池场景下的 `ThreadLocal` 值传递。 +由于阿里巴巴无法改动 JDK 源码,TTL 巧妙地利用了**装饰器模式**对任务(`Runnable`/`Callable`)或线程池(`Executor`)进行增强,将上下文的传递时机从“线程创建时”延迟到了“任务提交与执行时”。 + +TTL 的核心逻辑可以概括为三个阶段(CRR): + +- **Capture(捕获)**:在提交任务(如调用 `execute`)的一瞬间,`TtlRunnable` 会调用 `TransmittableThreadLocal.Transmitter.capture()`。它通过内部维护的 `holder` 集合,抓取当前父线程中所有活跃的 TTL 变量并存入快照。 +- **Replay(回放)**:在线程池的工作线程执行 `run()` 方法前,调用 `replay()`。它将快照中的值 `set` 到当前工作线程中,并备份该线程原有的旧值。 +- **Restore(恢复)**:任务执行结束后,调用 `restore()`。它根据备份将工作线程恢复到执行前的状态,防止上下文污染或内存泄漏。 + +这张图是 TTL 官方提供的 CRR 整个过程的时序图: + +![TTL 官方提供的 CRR 整个过程的时序图](https://oss.javaguide.cn/github/javaguide/java/concurrent/ttl-crr-timing-diagram.png) + +不太好理解吧?可以看下我绘制的这张 CRR 时序图,更清晰直观一些: + +```mermaid +sequenceDiagram + participant P as 父线程(Submitter) + participant W as TTL 包装器(TtlRunnable / Agent) + participant C as 线程池工作线程(Worker) + + Note over P: 1. set context = "A" + P->>W: 2. 提交任务(Capture) + Note right of W: 捕获父线程中所有活跃的 TTL 变量快照 + + W->>C: 3. 执行任务 run() + Note over C: 4. Replay + Note right of C: 备份工作线程原有 TTL 值
并设置 Capture 得到的值 + + Note over C: 5. 业务逻辑执行
get context = "A" + + Note over C: 6. Restore + Note right of C: 恢复工作线程原有 TTL 值
防止上下文污染 + + C-->>P: 7. 任务执行结束 + +``` + +也就是说,TTL 的本质是在任务提交时 Capture 上下文,在任务执行前 Replay 上下文,在任务结束后 Restore 线程状态,从而安全地支持线程池中的 `ThreadLocal` 传递。 + +TTL 提供了两种主要的接入方式,可根据侵入性要求和改造成本进行选择。 + +**1. 显式包装(手动接入)** + +使用 `TtlRunnable.get(Runnable)` 或 `TtlCallable.get(Callable)` 对任务进行包装,使用 `TtlExecutors.getTtlExecutor(Executor)`、`getTtlExecutorService(...)` 对线程池进行包装。这种接入方式清晰可控,但需要业务代码配合,存在一定侵入性。 + +下面这段代码展示了 TTL 通过 CRR,在支持线程池复用和拒绝策略的前提下,安全地传递并隔离 `ThreadLocal` 上下文。 -TTL 改造的地方有两处: +```java +public class TtlContextHolder { + private static final Logger log = LoggerFactory.getLogger(TtlContextHolder.class); + + // 1. 使用 static final 确保 TTL 实例不被重复创建,防止内存泄漏 + // 重写 copy 方法(可选):如果是引用类型,建议实现深拷贝 + private static final TransmittableThreadLocal CONTEXT = new TransmittableThreadLocal() { + @Override + public String copy(String parentValue) { + // 默认是直接返回引用,如果是可变对象(如 Map),请在这里 new 新对象 + return parentValue; + } + }; + + // 2. 线程池初始化:确保只被 TtlExecutors 包装一次 + private static final ExecutorService TTL_EXECUTOR_SERVICE; + + static { + ExecutorService rawExecutor = new ThreadPoolExecutor( + 2, 4, 60L, TimeUnit.SECONDS, + new LinkedBlockingQueue<>(1000), (Runnable r) -> new Thread(r, "ttl-worker-" + r.hashCode()), + new ThreadPoolExecutor.CallerRunsPolicy() // 关键:TTL 完美支持此拒绝策略 + ); + // 包装原始线程池 + TTL_EXECUTOR_SERVICE = TtlExecutors.getTtlExecutorService(rawExecutor); + } + + public static void main(String[] args) throws Exception { + try { + // 3. 在父线程中设置上下文 + CONTEXT.set("value-set-in-parent"); + log.info("父线程上下文: {}", CONTEXT.get()); + + // 4. 使用 Lambda 简化任务提交 + TTL_EXECUTOR_SERVICE.submit(() -> { + log.info("异步任务(Runnable)读取上下文: {}", CONTEXT.get()); + // 模拟业务逻辑 + // 注意:子线程修改是否影响父线程,取决于 copy() 是否做了深拷贝 + CONTEXT.set("value-modified-in-child"); + }); + + Future future = TTL_EXECUTOR_SERVICE.submit(() -> { + log.info("异步任务(Callable)读取上下文: {}", CONTEXT.get()); + return "Success"; + }); -- 实现自定义的 `Thread` ,在 `run()` 方法内部做 `ThreadLocal` 变量的赋值操作。 + future.get(); -- 基于 **线程池** 进行装饰,在 `execute()` 方法中,不提交 JDK 内部的 `Thread` ,而是提交自定义的 `Thread` 。 + // 5. 验证父线程上下文是否被污染 + log.info("父线程最终上下文: {}", CONTEXT.get()); -如果想要查看相关源码,可以引入 Maven 依赖进行下载。 + } finally { + // 6. 清理当前线程(父线程)的上下文,子线程的上下文由 TTL 的 Restore 机制自动恢复 + CONTEXT.remove(); + } + } +} +``` + +输出: + +```ba +09:06:31.438 INFO [main] TtlContextHolder - 父线程上下文: value-set-in-parent +09:06:31.452 INFO [ttl-worker-1663166483] TtlContextHolder - 异步任务(Runnable)读取上下文: value-set-in-parent +09:06:31.453 INFO [ttl-worker-841283083] TtlContextHolder - 异步任务(Callable)读取上下文: value-set-in-parent +09:06:31.453 INFO [main] TtlContextHolder - 父线程最终上下文: value-set-in-parent +``` + +如果你想要测试这段代码,记得引入 TTL 的 Maven 依赖; ```XML com.alibaba transmittable-thread-local - 2.12.0 + 2.14.4 ``` +**2. 无侵入接入(Java Agent)** + +通过 Java Agent 在类加载阶段对线程池相关类进行 字节码增强,自动织入 TTL 的上下文传递逻辑,实现业务代码零改造的上下文透传。这种方式业务代码无需感知 TTL 的存在,但实现复杂度相对较高。 + +TTL Agent 默认修饰了以下 JDK 执行器组件: + +1. **标准线程池**:`java.util.concurrent.ThreadPoolExecutor` 和 `java.util.concurrent.ScheduledThreadPoolExecutor`。 +2. **ForkJoin 体系**:`java.util.concurrent.ForkJoinTask`(从而透明支持了 `CompletableFuture` 和 Java 8 并行流 `Stream`)。 +3. **遗留组件**:`java.util.TimerTask`(自 v2.7.0 起支持,v2.11.2 起默认开启)。 + +在 Java 启动参数中加入 `-javaagent` 配置: + +```bash +# 基础配置 +java -javaagent:path/to/transmittable-thread-local-2.x.y.jar \ + -cp classes \ + com.your.app.Main +``` + #### 应用场景 1. **压测流量标记**: 在压测场景中,使用 `ThreadLocal` 存储压测标记,用于区分压测流量和真实流量。如果标记丢失,可能导致压测流量被错误地当成线上流量处理。 2. **上下文传递**:在分布式系统中,传递链路追踪信息(如 Trace ID)或用户上下文信息。 +#### 总结 + +`ThreadLocal` 的值默认是无法跨线程传递的,因为它的值是存在**每个 `Thread` 对象自己**的 `ThreadLocalMap` 里的,父子线程是两个不同的对象。 + +为了解决这个问题,主要有两种方案: + +1. **JDK的 InheritableThreadLocal**:它会在**创建子线程**的时候,把父线程的值**复制**一份给子线程。但它的问题是,在**线程池**场景下会失效。因为线程池会**复用**线程,这会导致线程拿到的可能是上一个任务传下来的**脏数据**。 +2. **阿里的 TransmittableThreadLocal (TTL)**:这是我们项目里用的方案,它专门解决线程池的问题。它的原理是,在**提交任务**到线程池时,它会把父线程的 `ThreadLocal` 值**捕获**下来,和任务**绑定**在一起。等线程池里的某个线程要执行这个任务时,它再把捕获的值**设置**到这个线程上,任务执行完再**清理**掉。 + +简单说,**InheritableThreadLocal是跟线程绑定的,只在创建时有效;而TTL是跟任务绑定的,完美支持线程池。** + ## 线程池 ### 什么是线程池? diff --git a/docs/zhuanlan/interview-guide.md b/docs/zhuanlan/interview-guide.md index 4f3afbbe538..d715db0855a 100644 --- a/docs/zhuanlan/interview-guide.md +++ b/docs/zhuanlan/interview-guide.md @@ -293,9 +293,9 @@ PostgreSQL 最大的优势,也是它在 AI 时代甩开对手的“王牌” 选择 Redis Stream 的理由: -- 复用现有组件:Redis 已用于会话缓存,无需引入新中间件 -- 功能满足需求:支持消费者组、消息确认(ACK)、持久化 -- 运维简单:对于中小型项目,Redis Stream 完全够用 +- 复用现有组件:Redis 已用于会话缓存,无需引入新中间件。 +- 功能满足需求:支持消费者组、消息确认(ACK)、持久化。 +- 运维简单:对于中小型项目,Redis Stream 完全够用。 ### 构建工具为什么选择 Gradle? From a6e1cbfafb6c35c5c5adaf5006b3e4b6fb20d0a6 Mon Sep 17 00:00:00 2001 From: Guide Date: Sun, 22 Feb 2026 20:21:13 +0800 Subject: [PATCH 02/53] =?UTF-8?q?docs:=E7=BD=91=E7=BB=9C=E9=83=A8=E5=88=86?= =?UTF-8?q?=E7=AC=94=E8=AF=AF=E4=BF=AE=E6=94=B9?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../network/application-layer-protocol.md | 2 +- docs/cs-basics/network/dns.md | 16 ++++++++++++++-- docs/cs-basics/network/http-vs-https.md | 4 ++-- docs/cs-basics/network/http1.0-vs-http1.1.md | 8 ++++---- docs/cs-basics/network/nat.md | 2 +- docs/cs-basics/network/network-attack-means.md | 2 +- docs/cs-basics/network/osi-and-tcp-ip-model.md | 4 ++-- .../cs-basics/network/other-network-questions.md | 2 +- .../network/tcp-reliability-guarantee.md | 2 +- 9 files changed, 27 insertions(+), 15 deletions(-) diff --git a/docs/cs-basics/network/application-layer-protocol.md b/docs/cs-basics/network/application-layer-protocol.md index aacb598a991..b2182c50dce 100644 --- a/docs/cs-basics/network/application-layer-protocol.md +++ b/docs/cs-basics/network/application-layer-protocol.md @@ -138,7 +138,7 @@ RTP 协议分为两种子协议: ## DNS:域名系统 -DNS(Domain Name System,域名管理系统)基于 UDP 协议,用于解决域名和 IP 地址的映射问题。 +DNS(Domain Name System,域名管理系统)通常基于 UDP 协议(端口 53),用于解决域名和 IP 地址的映射问题。当响应数据超过 UDP 长度限制或进行区域传送时会改用 TCP。 ![DNS:域名系统](https://oss.javaguide.cn/github/javaguide/cs-basics/network/dns-overview.png) diff --git a/docs/cs-basics/network/dns.md b/docs/cs-basics/network/dns.md index 1563fb4fcbe..6d51538b932 100644 --- a/docs/cs-basics/network/dns.md +++ b/docs/cs-basics/network/dns.md @@ -16,7 +16,7 @@ DNS(Domain Name System)域名管理系统,是当用户使用浏览器访 在实际使用中,有一种情况下,浏览器是可以不必动用 DNS 就可以获知域名和 IP 地址的映射的。浏览器在本地会维护一个`hosts`列表,一般来说浏览器要先查看要访问的域名是否在`hosts`列表中,如果有的话,直接提取对应的 IP 地址记录,就好了。如果本地`hosts`列表内没有域名-IP 对应记录的话,那么 DNS 就闪亮登场了。 -目前 DNS 的设计采用的是分布式、层次数据库结构,**DNS 是应用层协议,基于 UDP 协议之上,端口为 53** 。 +目前 DNS 的设计采用的是分布式、层次数据库结构,**DNS 是应用层协议,通常基于 UDP 协议,端口为 53**。当响应数据超过 UDP 报文长度限制(512 字节,EDNS0 可扩展至更大)或进行区域传送(Zone Transfer)时,会改用 TCP 协议以保证数据完整性。 ![TCP/IP 各层协议概览](https://oss.javaguide.cn/github/javaguide/cs-basics/network/network-protocol-overview.png) @@ -29,7 +29,19 @@ DNS 服务器自底向上可以依次分为以下几个层级(所有 DNS 服务 - 权威 DNS 服务器。在因特网上具有公共可访问主机的每个组织机构必须提供公共可访问的 DNS 记录,这些记录将这些主机的名字映射为 IP 地址。 - 本地 DNS 服务器。每个 ISP(互联网服务提供商)都有一个自己的本地 DNS 服务器。当主机发出 DNS 请求时,该请求被发往本地 DNS 服务器,它起着代理的作用,并将该请求转发到 DNS 层次结构中。严格说来,不属于 DNS 层级结构。 -世界上并不是只有 13 台根服务器,这是很多人普遍的误解,网上很多文章也是这么写的。实际上,现在根服务器数量远远超过这个数量。最初确实是为 DNS 根服务器分配了 13 个 IP 地址,每个 IP 地址对应一个不同的根 DNS 服务器。然而,由于互联网的快速发展和增长,这个原始的架构变得不太适应当前的需求。为了提高 DNS 的可靠性、安全性和性能,目前这 13 个 IP 地址中的每一个都有多个服务器,截止到 2023 年底,所有根服务器之和达到了 600 多台,未来还会继续增加。 +**世界上真的只有 13 台根服务器吗?** 这是一个流传已久的技术误解。如果你在网上搜索,仍能看到许多陈旧文章宣称“全球仅有 13 台根服务器,且全部由美国控制”。 + +**事实并非如此。** + +最初在设计 DNS(域名系统)架构时,受限于早期 IPv4 数据包的大小限制(UDP 报文需控制在 512 字节以内),预留给根服务器地址的空间确实只够容纳 13 个 IP 地址,每个 IP 地址对应一个不同的根 DNS 服务器。这 13 个地址分别被命名为 `a.root-servers.net` 到 `m.root-servers.net`。 + +虽然**逻辑上**只有 13 个 IP 地址,但随着互联网规模的爆发,物理上的“单一服务器”早已无法承载全球的查询压力。为了提升 DNS 的可靠性、安全性和响应速度,技术人员引入了 **IP 任播(Anycast)** 技术。 + +通过任播技术,每一个逻辑 IP 地址背后都可以对应成百上千台分布在全球各地的物理服务器。当你发起查询请求时,互联网路由协议(BGP)会自动将请求引导至地理位置或网络路径上离你**最近**的那台物理实例。 + +截止到 2023 年底,全球根服务器物理实例总数已超过 1700 台。根据 **[Root-Servers.org](https://root-servers.org/)** 的最新实时监测数据,到 **2026 年,全球根服务器物理实例已突破 1900+ 台**,并正向 2000 台大关迈进。 + +![Root-Servers.org](https://oss.javaguide.cn/github/javaguide/cs-basics/network/root-servers-org.png) ## DNS 工作流程 diff --git a/docs/cs-basics/network/http-vs-https.md b/docs/cs-basics/network/http-vs-https.md index 36691de06b3..74303aba536 100644 --- a/docs/cs-basics/network/http-vs-https.md +++ b/docs/cs-basics/network/http-vs-https.md @@ -38,7 +38,7 @@ HTTP 是应用层协议,它以 TCP(传输层)作为底层协议,默认 HTTPS 协议(Hyper Text Transfer Protocol Secure),是 HTTP 的加强安全版本。HTTPS 是基于 HTTP 的,也是用 TCP 作为底层协议,并额外使用 SSL/TLS 协议用作加密和安全认证。默认端口号是 443. -HTTPS 协议中,SSL 通道通常使用基于密钥的加密算法,密钥长度通常是 40 比特或 128 比特。 +HTTPS 中,TLS 握手完成后,通信数据使用对称加密算法(如 AES-128-GCM 或 AES-256-GCM)保护,密钥通过非对称加密(如 RSA-2048/4096 或 ECDH)在握手阶段协商生成。早期 SSL 使用的 40 比特密钥因强度不足已被废弃,现代 TLS 要求对称密钥至少 128 比特。 ### HTTPS 协议优点 @@ -52,7 +52,7 @@ HTTPS 之所以能达到较高的安全性要求,就是结合了 SSL/TLS 和 T **SSL 和 TLS 没有太大的区别。** -SSL 指安全套接字协议(Secure Sockets Layer),首次发布与 1996 年。SSL 的首次发布其实已经是他的 3.0 版本,SSL 1.0 从未面世,SSL 2.0 则具有较大的缺陷(DROWN 缺陷——Decrypting RSA with Obsolete and Weakened eNcryption)。很快,在 1999 年,SSL 3.0 进一步升级,**新版本被命名为 TLS 1.0**。因此,TLS 是基于 SSL 之上的,但由于习惯叫法,通常把 HTTPS 中的核心加密协议混称为 SSL/TLS。 +SSL 指安全套接字协议(Secure Sockets Layer),首次发布于 1996 年(SSL 3.0)。SSL 1.0 从未面世,SSL 2.0 则具有较大的缺陷(DROWN 缺陷——Decrypting RSA with Obsolete and Weakened eNcryption)。很快,在 1999 年,SSL 3.0 进一步升级,**新版本被命名为 TLS 1.0**。因此,TLS 是基于 SSL 之上的,但由于习惯叫法,通常把 HTTPS 中的核心加密协议混称为 SSL/TLS。目前 SSL 已完全废弃,TLS 1.2 和 TLS 1.3 是现代 HTTPS 的实际标准。 ### SSL/TLS 的工作原理 diff --git a/docs/cs-basics/network/http1.0-vs-http1.1.md b/docs/cs-basics/network/http1.0-vs-http1.1.md index 430437585d3..19210ebb9a0 100644 --- a/docs/cs-basics/network/http1.0-vs-http1.1.md +++ b/docs/cs-basics/network/http1.0-vs-http1.1.md @@ -161,10 +161,10 @@ HTTP/1.0 包含了`Content-Encoding`头部,对消息进行端到端编码。HT ## 总结 1. **连接方式** : HTTP 1.0 为短连接,HTTP 1.1 支持长连接。 -1. **状态响应码** : HTTP/1.1 中新加入了大量的状态码,光是错误响应状态码就新增了 24 种。比如说,`100 (Continue)`——在请求大资源前的预热请求,`206 (Partial Content)`——范围请求的标识码,`409 (Conflict)`——请求与当前资源的规定冲突,`410 (Gone)`——资源已被永久转移,而且没有任何已知的转发地址。 -1. **缓存处理** : 在 HTTP1.0 中主要使用 header 里的 If-Modified-Since,Expires 来做为缓存判断的标准,HTTP1.1 则引入了更多的缓存控制策略例如 Entity tag,If-Unmodified-Since, If-Match, If-None-Match 等更多可供选择的缓存头来控制缓存策略。 -1. **带宽优化及网络连接的使用** :HTTP1.0 中,存在一些浪费带宽的现象,例如客户端只是需要某个对象的一部分,而服务器却将整个对象送过来了,并且不支持断点续传功能,HTTP1.1 则在请求头引入了 range 头域,它允许只请求资源的某个部分,即返回码是 206(Partial Content),这样就方便了开发者自由的选择以便于充分利用带宽和连接。 -1. **Host 头处理** : HTTP/1.1 在请求头中加入了`Host`字段。 +2. **状态响应码** : HTTP/1.1 中新加入了大量的状态码,光是错误响应状态码就新增了 24 种。比如说,`100 (Continue)`——在请求大资源前的预热请求,`206 (Partial Content)`——范围请求的标识码,`409 (Conflict)`——请求与当前资源的规定冲突,`410 (Gone)`——资源已被永久转移,而且没有任何已知的转发地址。 +3. **缓存处理** : 在 HTTP1.0 中主要使用 header 里的 If-Modified-Since,Expires 来做为缓存判断的标准,HTTP1.1 则引入了更多的缓存控制策略例如 Entity tag,If-Unmodified-Since, If-Match, If-None-Match 等更多可供选择的缓存头来控制缓存策略。 +4. **带宽优化及网络连接的使用** :HTTP1.0 中,存在一些浪费带宽的现象,例如客户端只是需要某个对象的一部分,而服务器却将整个对象送过来了,并且不支持断点续传功能,HTTP1.1 则在请求头引入了 range 头域,它允许只请求资源的某个部分,即返回码是 206(Partial Content),这样就方便了开发者自由的选择以便于充分利用带宽和连接。 +5. **Host 头处理** : HTTP/1.1 在请求头中加入了`Host`字段。 ## 参考资料 diff --git a/docs/cs-basics/network/nat.md b/docs/cs-basics/network/nat.md index 51443c259b5..630f4866bef 100644 --- a/docs/cs-basics/network/nat.md +++ b/docs/cs-basics/network/nat.md @@ -26,7 +26,7 @@ SOHO 子网的“代理人”,也就是和外界的窗口,通常由路由器 首先,针对以上信息,我们有如下事实需要说明: -1. 路由器的右侧子网的网络号为`10.0.0/24`,主机号为`10.0.0/8`,三台主机地址,以及路由器的 LAN 侧接口地址,均由 DHCP 协议规定。而且,该 DHCP 运行在路由器内部(路由器自维护一个小 DHCP 服务器),从而为子网内提供 DHCP 服务。 +1. 路由器右侧子网的网络地址为 `10.0.0.0/24`(网络前缀 24 位,主机号占 8 位),三台主机地址以及路由器的 LAN 侧接口地址,均由 DHCP 协议规定。而且,该 DHCP 运行在路由器内部(路由器自维护一个小 DHCP 服务器),从而为子网内提供 DHCP 服务。 2. 路由器的 WAN 侧接口地址同样由 DHCP 协议规定,但该地址是路由器从 ISP(网络服务提供商)处获得,也就是该 DHCP 通常运行在路由器所在区域的 DHCP 服务器上。 现在,路由器内部还运行着 NAT 协议,从而为 LAN-WAN 间通信提供地址转换服务。为此,一个很重要的结构是 **NAT 转换表**。为了说明 NAT 的运行细节,假设有以下请求发生: diff --git a/docs/cs-basics/network/network-attack-means.md b/docs/cs-basics/network/network-attack-means.md index b6026a2c699..876299718a6 100644 --- a/docs/cs-basics/network/network-attack-means.md +++ b/docs/cs-basics/network/network-attack-means.md @@ -349,7 +349,7 @@ DES 使用的密钥表面上是 64 位的,然而只有其中的 56 位被实 常见的非对称加密算法: -- RSA(RSA 加密算法,RSA Algorithm):优势是性能比较快,如果想要较高的加密难度,需要很长的秘钥。 +- RSA(RSA 加密算法,RSA Algorithm):安全性基于大整数分解的计算难度,应用广泛,兼容性好。缺点是性能相对较慢,且密钥越长(如 2048/4096 位)安全性越高,但运算开销也随之增大。 - ECC:基于椭圆曲线提出。是目前加密强度最高的非对称加密算法 - SM2:同样基于椭圆曲线问题设计。最大优势就是国家认可和大力支持。 diff --git a/docs/cs-basics/network/osi-and-tcp-ip-model.md b/docs/cs-basics/network/osi-and-tcp-ip-model.md index 85b842efcf5..49f2c8ccb00 100644 --- a/docs/cs-basics/network/osi-and-tcp-ip-model.md +++ b/docs/cs-basics/network/osi-and-tcp-ip-model.md @@ -24,7 +24,7 @@ head: ![osi七层模型2](https://oss.javaguide.cn/github/javaguide/osi七层模型2.png) -**既然 OSI 七层模型这么厉害,为什么干不过 TCP/IP 四 层模型呢?** +**既然 OSI 七层模型这么厉害,为什么干不过 TCP/IP 四层模型呢?** 的确,OSI 七层模型当时一直被一些大公司甚至一些国家政府支持。这样的背景下,为什么会失败呢?我觉得主要有下面几方面原因: @@ -71,7 +71,7 @@ OSI 七层模型虽然失败了,但是却提供了很多不错的理论基础 - **Telnet(远程登陆协议)**:基于 TCP 协议,用于通过一个终端登陆到其他服务器。Telnet 协议的最大缺点之一是所有数据(包括用户名和密码)均以明文形式发送,这有潜在的安全风险。这就是为什么如今很少使用 Telnet,而是使用一种称为 SSH 的非常安全的网络传输协议的主要原因。 - **SSH(Secure Shell Protocol,安全的网络传输协议)**:基于 TCP 协议,通过加密和认证机制实现安全的访问和文件传输等业务 - **RTP(Real-time Transport Protocol,实时传输协议)**:通常基于 UDP 协议,但也支持 TCP 协议。它提供了端到端的实时传输数据的功能,但不包含资源预留存、不保证实时传输质量,这些功能由 WebRTC 实现。 -- **DNS(Domain Name System,域名管理系统)**: 基于 UDP 协议,用于解决域名和 IP 地址的映射问题。 +- **DNS(Domain Name System,域名管理系统)**: 通常基于 UDP 协议(端口 53),用于解决域名和 IP 地址的映射问题。当响应数据过大或进行区域传送时会改用 TCP。 关于这些协议的详细介绍请看 [应用层常见协议总结(应用层)](./application-layer-protocol.md) 这篇文章。 diff --git a/docs/cs-basics/network/other-network-questions.md b/docs/cs-basics/network/other-network-questions.md index 0af1349e329..df59c7a47b7 100644 --- a/docs/cs-basics/network/other-network-questions.md +++ b/docs/cs-basics/network/other-network-questions.md @@ -80,7 +80,7 @@ head: - **Telnet(远程登陆协议)**:基于 TCP 协议,用于通过一个终端登陆到其他服务器。Telnet 协议的最大缺点之一是所有数据(包括用户名和密码)均以明文形式发送,这有潜在的安全风险。这就是为什么如今很少使用 Telnet,而是使用一种称为 SSH 的非常安全的网络传输协议的主要原因。 - **SSH(Secure Shell Protocol,安全的网络传输协议)**:基于 TCP 协议,通过加密和认证机制实现安全的访问和文件传输等业务 - **RTP(Real-time Transport Protocol,实时传输协议)**:通常基于 UDP 协议,但也支持 TCP 协议。它提供了端到端的实时传输数据的功能,但不包含资源预留存、不保证实时传输质量,这些功能由 WebRTC 实现。 -- **DNS(Domain Name System,域名管理系统)**: 基于 UDP 协议,用于解决域名和 IP 地址的映射问题。 +- **DNS(Domain Name System,域名管理系统)**: 通常基于 UDP 协议(端口 53),用于解决域名和 IP 地址的映射问题。当响应数据过大或进行区域传送时会改用 TCP。 关于这些协议的详细介绍请看 [应用层常见协议总结(应用层)](./application-layer-protocol.md) 这篇文章。 diff --git a/docs/cs-basics/network/tcp-reliability-guarantee.md b/docs/cs-basics/network/tcp-reliability-guarantee.md index 5be11655bf4..e9a43a11d1a 100644 --- a/docs/cs-basics/network/tcp-reliability-guarantee.md +++ b/docs/cs-basics/network/tcp-reliability-guarantee.md @@ -73,7 +73,7 @@ TCP 为全双工(Full-Duplex, FDX)通信,双方可以进行双向通信,客 TCP 的拥塞控制采用了四种算法,即 **慢开始**、 **拥塞避免**、**快重传** 和 **快恢复**。在网络层也可以使路由器采用适当的分组丢弃策略(如主动队列管理 AQM),以减少网络拥塞的发生。 -- **慢开始:** 慢开始算法的思路是当主机开始发送数据时,如果立即把大量数据字节注入到网络,那么可能会引起网络阻塞,因为现在还不知道网络的符合情况。经验表明,较好的方法是先探测一下,即由小到大逐渐增大发送窗口,也就是由小到大逐渐增大拥塞窗口数值。cwnd 初始值为 1,每经过一个传播轮次,cwnd 加倍。 +- **慢开始:** 慢开始算法的思路是当主机开始发送数据时,如果立即把大量数据字节注入到网络,那么可能会引起网络阻塞,因为现在还不知道网络的负荷情况。经验表明,较好的方法是先探测一下,即由小到大逐渐增大发送窗口,也就是由小到大逐渐增大拥塞窗口数值。cwnd 初始值为 1,每经过一个传播轮次,cwnd 加倍。 - **拥塞避免:** 拥塞避免算法的思路是让拥塞窗口 cwnd 缓慢增大,即每经过一个往返时间 RTT 就把发送方的 cwnd 加 1. - **快重传与快恢复:** 在 TCP/IP 中,快速重传和恢复(fast retransmit and recovery,FRR)是一种拥塞控制算法,它能快速恢复丢失的数据包。没有 FRR,如果数据包丢失了,TCP 将会使用定时器来要求传输暂停。在暂停的这段时间内,没有新的或复制的数据包被发送。有了 FRR,如果接收机接收到一个不按顺序的数据段,它会立即给发送机发送一个重复确认。如果发送机接收到三个重复确认,它会假定确认件指出的数据段丢失了,并立即重传这些丢失的数据段。有了 FRR,就不会因为重传时要求的暂停被耽误。  当有单独的数据包丢失时,快速重传和恢复(FRR)能最有效地工作。当有多个数据信息包在某一段很短的时间内丢失时,它则不能很有效地工作。 From 96d1d11b6c5bceddb5efffbdf136071756bb541d Mon Sep 17 00:00:00 2001 From: Guide Date: Sun, 22 Feb 2026 20:53:42 +0800 Subject: [PATCH 03/53] =?UTF-8?q?docs=EF=BC=9A=E6=B3=9B=E5=9E=8B&=E9=80=9A?= =?UTF-8?q?=E9=85=8D=E7=AC=A6=E3=80=81=E5=B8=B8=E8=A7=81SQL=E4=BC=98?= =?UTF-8?q?=E5=8C=96=E6=89=8B=E6=AE=B5=E6=80=BB=E7=BB=93=E5=BC=80=E6=94=BE?= =?UTF-8?q?=E9=98=85=E8=AF=BB?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../deep-pagination-optimization.md | 25 +- docs/high-performance/sql-optimization.md | 383 +++++++++++++++++- docs/java/basis/generics-and-wildcards.md | 351 +++++++++++++++- 3 files changed, 748 insertions(+), 11 deletions(-) diff --git a/docs/high-performance/deep-pagination-optimization.md b/docs/high-performance/deep-pagination-optimization.md index 1a949b59575..c43c057b527 100644 --- a/docs/high-performance/deep-pagination-optimization.md +++ b/docs/high-performance/deep-pagination-optimization.md @@ -127,12 +127,27 @@ LIMIT 1000000, 10; ## 总结 -本文总结了几种常见的深度分页优化方案: +深度分页问题的根本原因在于:当 `LIMIT` 的偏移量过大时,MySQL 需要扫描并跳过大量记录才能获取目标数据,查询优化器可能放弃索引而选择全表扫描。此时即使有索引,也无法避免大量的回表操作,导致查询性能急剧下降。 -1. **范围查询**: 基于 ID 连续性进行分页,通过记录上一页最后一条记录的 ID 来获取下一页数据。适合 ID 连续且按 ID 查询的场景,但在 ID 不连续或需要按其他字段排序时存在局限。 -2. **子查询**: 先通过子查询获取分页的起始主键值,再根据主键进行筛选分页。利用主键索引提高效率,但子查询会生成临时表,复杂场景下性能不佳。 -3. **延迟关联 (INNER JOIN)**: 使用 `INNER JOIN` 将分页操作转移到主键索引上,减少回表次数。相比子查询,延迟关联的性能更优,适合大数据量的分页查询。 -4. **覆盖索引**: 通过索引直接获取所需字段,避免回表操作,减少 IO 开销,适合查询特定字段的场景。但当结果集较大时,MySQL 可能会选择全表扫描。 +本文介绍了四种常见的深度分页优化方案,各方案的特点及适用场景对比如下: + +| 优化方案 | 核心思路 | 适用场景 | 限制 | +| ------------ | ------------------------------------------------------------------- | ----------------------------------- | ------------------------------------------------ | +| **范围查询** | 记录上一页最后一条 ID,通过 `WHERE id > last_id LIMIT n` 获取下一页 | ID 连续、按 ID 排序、允许游标式翻页 | 不支持跳页、ID 不连续时失效、非 ID 排序不适用 | +| **子查询** | 先通过子查询获取起始主键,再根据主键过滤 | 需要支持传统 OFFSET 翻页 | 子查询可能产生临时表、仅适用于 ID 正序 | +| **延迟关联** | 用 `INNER JOIN` 将分页转移到主键索引,减少回表 | 大数据量分页、需要传统翻页逻辑 | SQL 相对复杂 | +| **覆盖索引** | 建立包含查询字段的联合索引,避免回表 | 查询字段固定、可建立合适索引 | 字段较多时索引维护成本高、大结果集可能走全表扫描 | + +**方案选择建议**: + +- **优先使用延迟关联**:对于大多数需要支持传统 `LIMIT offset, size` 翻页逻辑的场景,延迟关联是性能和可维护性较好的选择。 +- **考虑范围查询(游标分页)**:如果业务允许使用"下一页"式的游标翻页(如社交媒体 feed 流、无限滚动),范围查询性能最佳且稳定。 +- **覆盖索引作为补充**:当查询字段固定且数量不多时,可配合其他方案建立覆盖索引进一步优化。 + +**注意事项**: + +- 无论采用哪种方案,都应注意监控实际执行计划(`EXPLAIN`),确保优化器按预期使用索引。 +- 对于超深分页(如百万级偏移量),应从业务层面评估是否真的需要支持,考虑限制最大翻页数或采用其他检索方式(如搜索引擎)。 ## 参考 diff --git a/docs/high-performance/sql-optimization.md b/docs/high-performance/sql-optimization.md index 8ed794fcb38..363169ebe1b 100644 --- a/docs/high-performance/sql-optimization.md +++ b/docs/high-performance/sql-optimization.md @@ -1,5 +1,5 @@ --- -title: 常见SQL优化手段总结(付费) +title: 常见SQL优化手段总结 description: 本文系统总结常见的 SQL 优化手段,涵盖慢 SQL 定位与分析(EXPLAIN、Show Profile)、索引优化策略、查询重写技巧、分页优化等实战方法,帮助你快速提升数据库查询性能。 category: 高性能 head: @@ -8,8 +8,383 @@ head: content: SQL优化,慢SQL,EXPLAIN执行计划,索引优化,MySQL优化,查询优化,分页优化,Show Profile --- -**常见 SQL 优化手段总结** 相关的内容为我的[知识星球](https://javaguide.cn/about-the-author/zhishixingqiu-two-years.html)(点击链接即可查看详细介绍以及加入方法)专属内容,已经整理到了[《Java 面试指北》](https://javaguide.cn/zhuanlan/java-mian-shi-zhi-bei.html)中。 +## 避免使用 SELECT \* -![](https://oss.javaguide.cn/javamianshizhibei/sql-optimization.png) +- `SELECT *` 会消耗更多的 CPU。 +- `SELECT *` 无用字段增加网络带宽资源消耗,增加数据传输时间,尤其是大字段(如 varchar、blob、text)。 +- `SELECT *` 无法使用 MySQL 优化器覆盖索引的优化(基于 MySQL 优化器的“覆盖索引”策略又是速度极快,效率极高,业界极为推荐的查询优化方式) +- `SELECT <字段列表>` 可减少表结构变更带来的影响。 - +## 尽量避免多表做 join + +阿里巴巴《Java 开发手册》中有这样一段描述: + +> 【强制】超过三个表禁止 join。需要 join 的字段,数据类型保持绝对一致;多表关联查询时,保证被关联 的字段需要有索引。 + +![尽量避免多表做 join](https://oss.javaguide.cn/github/javaguide/mysql/alibaba-java-development-handbook-multi-table-join.png) + +join 的效率比较低,主要原因是因为其使用嵌套循环(Nested Loop)来实现关联查询,以前常见的实现效率都不是很高: + +- **Simple Nested-Loop Join** :直接使用笛卡尔积实现 join,逐行遍历/全表扫描,效率最低。 +- **Block Nested-Loop Join (BNL)** :利用 JOIN BUFFER 进行优化。**注意:在 MySQL 8.0.20 及更高版本中,BNL 已被 Hash Join 取代**,Hash Join 通常能将非索引列关联的复杂度从 O(M\*N) 降低到接近 O(M+N)。 +- **Index Nested-Loop Join** :在必要的字段上增加索引,性能得到进一步提升。 + +实际业务场景避免多表 join 常见的做法有两种: + +1. **单表查询后在内存中自己做关联** :对数据库做单表查询,再根据查询结果进行二次查询,以此类推,最后再进行关联。 +2. **数据冗余**,把一些重要的数据在表中做冗余,尽可能地避免关联查询。很笨的一种做法,表结构比较稳定的情况下才会考虑这种做法。进行冗余设计之前,思考一下自己的表结构设计的是否有问题。 + +更加推荐第一种,这种在实际项目中的使用率比较高,除了性能不错之外,还有如下优势: + +1. **拆分后的单表查询代码可复用性更高** :join 联表 SQL 基本不太可能被复用。 +2. **单表查询更利于后续的维护** :不论是后续修改表结构还是进行分库分表,单表查询维护起来都更容易。 + +不过,如果系统要求的并发量不大的话,我觉得多表 join 也是没问题的。很多公司内部复杂的系统,要求的并发量不高,很多数据必须 join 5 张以上的表才能查出来。 + +## 深度分页优化 + +深度分页问题的根本原因在于:当 `LIMIT` 的偏移量过大时,MySQL 需要扫描并跳过大量记录才能获取目标数据,查询优化器可能放弃索引而选择全表扫描。此时即使有索引,也无法避免大量的回表操作,导致查询性能急剧下降。 + +本文介绍了四种常见的深度分页优化方案,各方案的特点及适用场景对比如下: + +| 优化方案 | 核心思路 | 适用场景 | 限制 | +| ------------ | ------------------------------------------------------------------- | ----------------------------------- | ------------------------------------------------ | +| **范围查询** | 记录上一页最后一条 ID,通过 `WHERE id > last_id LIMIT n` 获取下一页 | ID 连续、按 ID 排序、允许游标式翻页 | 不支持跳页、ID 不连续时失效、非 ID 排序不适用 | +| **子查询** | 先通过子查询获取起始主键,再根据主键过滤 | 需要支持传统 OFFSET 翻页 | 子查询可能产生临时表、仅适用于 ID 正序 | +| **延迟关联** | 用 `INNER JOIN` 将分页转移到主键索引,减少回表 | 大数据量分页、需要传统翻页逻辑 | SQL 相对复杂 | +| **覆盖索引** | 建立包含查询字段的联合索引,避免回表 | 查询字段固定、可建立合适索引 | 字段较多时索引维护成本高、大结果集可能走全表扫描 | + +**方案选择建议**: + +- **优先使用延迟关联**:对于大多数需要支持传统 `LIMIT offset, size` 翻页逻辑的场景,延迟关联是性能和可维护性较好的选择。 +- **考虑范围查询(游标分页)**:如果业务允许使用"下一页"式的游标翻页(如社交媒体 feed 流、无限滚动),范围查询性能最佳且稳定。 +- **覆盖索引作为补充**:当查询字段固定且数量不多时,可配合其他方案建立覆盖索引进一步优化。 + +**注意事项**: + +- 无论采用哪种方案,都应注意监控实际执行计划(`EXPLAIN`),确保优化器按预期使用索引。 +- 对于超深分页(如百万级偏移量),应从业务层面评估是否真的需要支持,考虑限制最大翻页数或采用其他检索方式(如搜索引擎)。 + +详细介绍可以阅读这篇文章:[深度分页介绍及优化建议](https://javaguide.cn/high-performance/deep-pagination-optimization.html)。 + +## 建议不要使用外键与级联 + +阿里巴巴《Java 开发手册》中有这样一段描述: + +> 不得使用外键与级联,一切外键概念必须在应用层解决。 + +![](https://oss.javaguide.cn/github/javaguide/mysql/alibaba-java-development-handbook-multi-table-join-foreign-keys-and-cascades.png) + +网络上已经有非常多分析外键与级联缺陷的文章了,个人认为不建议使用外键主要是因为对分库分表不友好,性能方面的影响其实是比较小的。 + +## 选择合适的字段类型 + +存储字节越小,占用也就空间越小,性能也越好。 + +**a.某些字符串可以转换成数字类型存储比如可以将 IP 地址转换成整型数据。** + +数字是连续的,性能更好,占用空间也更小。 + +MySQL 提供了两个方法来处理 ip 地址 + +- `INET_ATON()` : 把 IPv4 转为无符号整型(4 字节,32 位)。对于 IPv6,可使用 `INET6_ATON()` 转为 16 字节(128 位)的二进制字符串。 +- `INET_NTOA()` :把整型的 ip 转为地址 + +插入数据前,先用 `INET_ATON()` 把 ip 地址转为整型,显示数据时,使用 `INET_NTOA()` 把整型的 ip 地址转为地址显示即可。 + +**b.对于非负型的数据 (如自增 ID,整型 IP,年龄) 来说,要优先使用无符号整型来存储。** + +无符号相对于有符号可以多出一倍的存储空间 + +```sql +SIGNED INT -2147483648~2147483647 +UNSIGNED INT 0~4294967295 +``` + +**c.小数值类型(比如年龄、状态表示如 0/1)优先使用 TINYINT 类型。** + +**d.对于日期类型来说, 一定不要用字符串存储日期。可以考虑 DATETIME、TIMESTAMP 和 数值型时间戳。** + +这三种种方式都有各自的优势,根据实际场景选择最合适的才是王道。下面再对这三种方式做一个简单的对比,以供大家实际开发中选择正确的存放时间的数据类型: + +| 类型 | 存储空间 | 日期格式 | 日期范围 | 是否带时区信息 | +| ------------ | -------- | ------------------------------ | ------------------------------------------------------------ | -------------- | +| DATETIME | 5~8 字节 | YYYY-MM-DD hh:mm:ss[.fraction] | 1000-01-01 00:00:00[.000000] ~ 9999-12-31 23:59:59[.999999] | 否 | +| 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 之后的时间 | 否 | + +MySQL 时间类型选择的详细介绍请看这篇:[MySQL 时间类型数据存储建议](https://javaguide.cn/database/mysql/some-thoughts-on-database-storage-time.html)。 + +**e.金额字段用 decimal,避免精度丢失。** + +decimal 用于存储有精度要求的小数比如与金钱相关的数据,可以避免浮点数带来的精度损失。 + +在 Java 中,MySQL 的 decimal 类型对应的是 Java 类 `java.math.BigDecimal` 。 + +`BigDecimal`的详细介绍请参考这篇:[BigDecimal 详解](https://javaguide.cn/java/basis/bigdecimal.html)。 + +**f.尽量使用自增 id 作为主键。** + +如果主键为自增 id 的话,每次都会将数据加在 B+树尾部(本质是双向链表),时间复杂度为 O(1)。在写满一个数据页的时候,直接申请另一个新数据页接着写就可以了。 + +如果主键是非自增 id 的话,为了让新加入数据后 B+树的叶子节点还能保持有序,它就需要往叶子结点的中间找,查找过程的时间复杂度是 O(lgn)。如果这个也被写满的话,就需要进行页分裂。页分裂操作需要加悲观锁,性能非常低。 + +不过, 像分库分表这类场景就不建议使用自增 id 作为主键,应该使用分布式 ID 比如 uuid 。 + +相关阅读:[数据库主键一定要自增吗?有哪些场景不建议自增?](https://mp.weixin.qq.com/s/vNRIFKjbe7itRTxmq-bkAA)。 + +**g.不建议使用 `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` 来判断,而不能使用 =、!=、 <、> 之类的比较运算符。而`''`是可以使用这些比较运算符的。 + +## 尽量用 UNION ALL 代替 UNION + +UNION 会把两个结果集的所有数据放到临时表中后再进行去重操作,更耗时,更消耗 CPU 资源。 + +UNION ALL 不会再对结果集进行去重操作,获取到的数据包含重复的项。 + +不过,如果实际业务场景中不允许产生重复数据的话,还是可以使用 UNION。 + +## 优先使用批量操作 + +对于数据库中的数据更新,如果能使用批量操作就要尽量使用,减少请求数据库的次数,提高性能。 + +```sql +# 反例 +INSERT INTO `cus_order` (`id`, `score`, `name`) VALUES (1, 426547, 'user1'); +INSERT INTO `cus_order` (`id`, `score`, `name`) VALUES (1, 33, 'user2'); +INSERT INTO `cus_order` (`id`, `score`, `name`) VALUES (1, 293854, 'user3'); + +# 正例 +INSERT into `cus_order` (`id`, `score`, `name`) values(1, 426547, 'user1'),(1, 33, 'user2'),(1, 293854, 'user3'); +``` + +## Show Profile 分析 SQL 执行性能 + +为了更精准定位一条 SQL 语句的性能问题,需要清楚地知道这条 SQL 语句运行时消耗了多少系统资源。 [`SHOW PROFILE`](https://dev.mysql.com/doc/refman/5.7/en/show-profile.html) 和 [`SHOW PROFILES`](https://dev.mysql.com/doc/refman/5.7/en/show-profiles.html) 展示 SQL 语句的资源使用情况,展示的消息包括 CPU 的使用,CPU 上下文切换,IO 等待,内存使用等。 + +MySQL 在 5.0.37 版本之后才支持 Profiling,`select @@have_profiling` 命令返回 `YES` 表示该功能可以使用。 + +```sql + mysql> SELECT @@have_profiling; ++------------------+ +| @@have_profiling | ++------------------+ +| YES | ++------------------+ +1 row in set (0.00 sec) +``` + +> **注意** :`SHOW PROFILE` 和 `SHOW PROFILES` 已经被弃用,未来的 MySQL 版本中可能会被删除,取而代之的是使用 [Performance Schema](https://dev.mysql.com/doc/refman/8.0/en/performance-schema.html)。在该功能被删除之前,我们简单介绍一下其基本使用方法。 + +想要使用 Profiling,请确保你的 `profiling` 是开启(on)的状态。 + +你可以通过 `SHOW VARIABLES` 命令查看其状态: + +![](https://oss.javaguide.cn/github/javaguide/mysql/mysql-show-variables-profiling.png) + +也可以通过 `SELECT @@profiling`命令进行查看: + +```sql +mysql> SELECT @@profiling; ++-------------+ +| @@profiling | ++-------------+ +| 0 | ++-------------+ +1 row in set (0.00 sec) +``` + +默认情况下, `Profiling` 是关闭(off)的状态,你直接通过`SET @@profiling=1`命令即可开启。 + +开启成功之后,我们执行几条 SQL 语句。执行完成之后,使用 `SHOW PROFILES` 可以展示当前 Session 下所有 SQL 语句的简要的信息包括 Query_ID(SQL 语句的 ID 编号) 和 Duration(耗时)。 + +具体能收集多少个 SQL,由参数 `profiling_history_size` 决定,默认值为 15,最大值为 100。如果设置为 0,等同于关闭 Profiling。 + +![](https://oss.javaguide.cn/github/javaguide/mysql/mysql-show-profiles-ranking-list-table.png) + +如果想要展示一个 SQL 语句的执行耗时细节,可以使用`SHOW PROFILE` 命令。 + +`SHOW PROFILE` 命令的具体用法如下: + +```sql +SHOW PROFILE [type [, type] ... ] + [FOR QUERY n] + [LIMIT row_count [OFFSET offset]] + +type: { + ALL + | BLOCK IO + | CONTEXT SWITCHES + | CPU + | IPC + | MEMORY + | PAGE FAULTS + | SOURCE + | SWAPS +} +``` + +在执行`SHOW PROFILE` 命令时,可以加上类型子句,比如 CPU、IPC、MEMORY 等,查看具体某类资源的消耗情况: + +```sql +SHOW PROFILE CPU,IPC FOR QUERY 8; +``` + +如果不加 `FOR QUERY {n}`子句,默认展示最新的一次 SQL 的执行情况,加了 `FOR QUERY {n}`,表示展示 Query_ID 为 n 的 SQL 的执行情况。 + +![](https://oss.javaguide.cn/github/javaguide/mysql/mysql-show-profiles-cpu-ipc.png) + +## 优化慢 SQL + +为了优化慢 SQL ,我们首先要找到哪些 SQL 语句执行速度比较慢。 + +MySQL 慢查询日志是用来记录 MySQL 在执行命令中,响应时间超过预设阈值的 SQL 语句。因此,通过分析慢查询日志我们就可以找出执行速度比较慢的 SQL 语句。 + +出于性能层面的考虑,慢查询日志功能默认是关闭的,你可以通过以下命令开启: + +```sql +# 开启慢查询日志功能 +SET GLOBAL slow_query_log = 'ON'; +# 慢查询日志存放位置 +SET GLOBAL slow_query_log_file = '/var/lib/mysql/ranking-list-slow.log'; +# 无论是否超时,未被索引的记录也会记录下来。 +SET GLOBAL log_queries_not_using_indexes = 'ON'; +# 慢查询阈值(秒),SQL 执行超过这个阈值将被记录在日志中。 +SET SESSION long_query_time = 1; +# 慢查询仅记录扫描行数大于此参数的 SQL +SET SESSION min_examined_row_limit = 100; +``` + +设置成功之后,使用 `show variables like 'slow%';` 命令进行查看。 + +```bash +| Variable_name | Value | ++---------------------+--------------------------------------+ +| slow_launch_time | 2 | +| slow_query_log | ON | +| slow_query_log_file | /var/lib/mysql/ranking-list-slow.log | ++---------------------+--------------------------------------+ +3 rows in set (0.01 sec) +``` + +我们故意在百万数据量的表(未使用索引)中执行一条排序的语句: + +```sql +SELECT `score`,`name` FROM `cus_order` ORDER BY `score` DESC; +``` + +确保自己有对应目录的访问权限: + +```bash +chmod 755 /var/lib/mysql/ +``` + +查看对应的慢查询日志: + +```bash + cat /var/lib/mysql/ranking-list-slow.log +``` + +我们刚刚故意执行的 SQL 语句已经被慢查询日志记录了下来: + +```plain +# Time: 2022-10-09T08:55:37.486797Z +# User@Host: root[root] @ [172.17.0.1] Id: 14 +# Query_time: 0.978054 Lock_time: 0.000164 Rows_sent: 999999 Rows_examined: 1999998 +SET timestamp=1665305736; +SELECT `score`,`name` FROM `cus_order` ORDER BY `score` DESC; +``` + +这里对日志中的一些信息进行说明: + +- `Time` :被日志记录的代码在服务器上的运行时间。 +- `User@Host`:谁执行的这段代码。 +- `Query_time`:这段代码运行时长。 +- `Lock_time`:执行这段代码时,锁定了多久。 +- `Rows_sent`:慢查询返回的记录。 +- `Rows_examined`:慢查询扫描过的行数。 + +实际项目中,慢查询日志通常会比较复杂,我们需要借助一些工具对其进行分析。像 MySQL 内置的 `mysqldumpslow` 工具就可以把相同的 SQL 归为一类,并统计出归类项的执行次数和每次执行的耗时等一系列对应的情况。 + +找到了慢 SQL 之后,我们可以通过 `EXPLAIN` 命令分析对应的 `SELECT` 语句: + +```sql +mysql> EXPLAIN SELECT `score`,`name` FROM `cus_order` ORDER BY `score` DESC; ++----+-------------+-----------+------------+------+---------------+------+---------+------+--------+----------+----------------+ +| id | select_type | table | partitions | type | possible_keys | key | key_len | ref | rows | filtered | Extra | ++----+-------------+-----------+------------+------+---------------+------+---------+------+--------+----------+----------------+ +| 1 | SIMPLE | cus_order | NULL | ALL | NULL | NULL | NULL | NULL | 997572 | 100.00 | Using filesort | ++----+-------------+-----------+------------+------+---------------+------+---------+------+--------+----------+----------------+ +1 row in set, 1 warning (0.00 sec) +``` + +比较重要的字段说明: + +- `select_type` :查询的类型,常用的取值有 SIMPLE(普通查询,即没有联合查询、子查询)、PRIMARY(主查询)、UNION(UNION 中后面的查询)、SUBQUERY(子查询)等。 +- `table` :表示查询涉及的表或衍生表。 +- `type` :执行方式,判断查询是否高效的重要参考指标,结果值从差到好依次是:ALL < index < range ~ index_merge < ref < eq_ref < const < system。 +- `rows` : SQL 要查找到结果集需要扫描读取的数据行数,原则上 rows 越少越好。 +- …… + +关于 Explain 的详细介绍,请看这篇文章:[MySQL 执行计划分析](https://javaguide.cn/database/mysql/mysql-query-execution-plan.html)。另外,再推荐一下阿里的这篇文章:[慢 SQL 治理经验总结](https://mp.weixin.qq.com/s/LZRSQJufGRpRw6u4h_Uyww),总结的挺不错。 + +## 正确使用索引 + +正确使用索引可以大大加快数据的检索速度(大大减少检索的数据量)。 + +### 选择合适的字段创建索引 + +- **不为 NULL 的字段** :索引字段的数据应该尽量不为 NULL,因为对于数据为 NULL 的字段,数据库较难优化。如果字段频繁被查询,但又避免不了为 NULL,建议使用 0,1,true,false 这样语义较为清晰的短值或短字符作为替代。 +- **被频繁查询的字段** :我们创建索引的字段应该是查询操作非常频繁的字段。 +- **被作为条件查询的字段** :被作为 WHERE 条件查询的字段,应该被考虑建立索引。 +- **频繁需要排序的字段** :索引已经排序,这样查询可以利用索引的排序,加快排序查询时间。 +- **被经常频繁用于连接的字段** :经常用于连接的字段可能是一些外键列,对于外键列并不一定要建立外键,只是说该列涉及到表与表的关系。对于频繁被连接查询的字段,可以考虑建立索引,提高多表连接查询的效率。 + +### 被频繁更新的字段应该慎重建立索引 + +虽然索引能带来查询上的效率,但是维护索引的成本也是不小的。 如果一个字段不被经常查询,反而被经常修改,那么就更不应该在这种字段上建立索引了。 + +### 尽可能的考虑建立联合索引而不是单列索引 + +因为索引是需要占用磁盘空间的,可以简单理解为每个索引都对应着一颗 B+树。如果一个表的字段过多,索引过多,那么当这个表的数据达到一个体量后,索引占用的空间也是很多的,且修改索引时,耗费的时间也是较多的。如果是联合索引,多个字段在一个索引上,那么将会节约很大磁盘空间,且修改数据的操作效率也会提升。 + +### 注意避免冗余索引 + +冗余索引指的是索引的功能相同,能够命中索引(a, b)就肯定能命中索引(a) ,那么索引(a)就是冗余索引。如(name,city )和(name )这两个索引就是冗余索引,能够命中前者的查询肯定是能够命中后者的 在大多数情况下,都应该尽量扩展已有的索引而不是创建新索引。 + +### 考虑在字符串类型的字段上使用前缀索引代替普通索引 + +前缀索引仅限于字符串类型,较普通索引会占用更小的空间,所以可以考虑使用前缀索引带替普通索引。 + +### 避免索引失效 + +索引失效也是慢查询的主要原因之一,常见的导致索引失效的情况有下面这些: + +- ~~使用 `SELECT *` 进行查询;~~ `SELECT *` 不会直接导致索引失效(如果不走索引大概率是因为 where 查询范围过大导致的),但它可能会带来一些其他的性能问题比如造成网络传输和数据处理的浪费、无法使用索引覆盖; +- 创建了组合索引,但查询条件未准守最左匹配原则; +- 在索引列上进行计算、函数、类型转换等操作; +- 以 % 开头的 LIKE 查询比如 `LIKE '%abc';`; +- 查询条件中使用 OR,且 OR 的前后条件中有一个列没有索引,涉及的索引都不会被使用到; +- IN 的取值范围较大时会导致索引失效(MySQL 参数 eq_range_index_dive_limit 默认为 200,超过该值可能因估算不准而走全表扫描); +- 发生[隐式转换](https://javaguide.cn/database/mysql/index-invalidation-caused-by-implicit-conversion.html); +- …… + +推荐阅读这篇文章:[美团暑期实习一面:MySQl 索引失效的场景有哪些?](https://mp.weixin.qq.com/s/mwME3qukHBFul57WQLkOYg)。 + +### 删除长期未使用的索引 + +删除长期未使用的索引,不用的索引的存在会造成不必要的性能损耗 MySQL 5.7 可以通过查询 sys 库的 schema_unused_indexes 视图来查询哪些索引从未被使用 + +## 参考 + +- MySQL 8.2 Optimizing SQL Statements:https://dev.mysql.com/doc/refman/8.0/en/statement-optimization.html +- 为什么阿里巴巴禁止数据库中做多表 join - Hollis:https://mp.weixin.qq.com/s/GSGVFkDLz1hZ1OjGndUjZg +- MySQL 的 COUNT 语句,竟然都能被面试官虐的这么惨 - Hollis:https://mp.weixin.qq.com/s/IOHvtel2KLNi-Ol4UBivbQ +- MySQL 性能优化神器 Explain 使用分析:https://segmentfault.com/a/1190000008131735 +- 如何使用 MySQL 慢查询日志进行性能优化 :https://kalacloud.com/blog/how-to-use-mysql-slow-query-log-profiling-mysqldumpslow/ diff --git a/docs/java/basis/generics-and-wildcards.md b/docs/java/basis/generics-and-wildcards.md index 3bf523ec0b7..927db5238ab 100644 --- a/docs/java/basis/generics-and-wildcards.md +++ b/docs/java/basis/generics-and-wildcards.md @@ -10,6 +10,353 @@ head: content: Java泛型,通配符,类型擦除,泛型边界,PECS原则,泛型方法,上界下界通配符,泛型接口 --- -**泛型&通配符** 相关的面试题为我的[知识星球](https://javaguide.cn/about-the-author/zhishixingqiu-two-years.html)(点击链接即可查看详细介绍以及加入方法)专属内容,已经整理到了[《Java 面试指北》](https://javaguide.cn/zhuanlan/java-mian-shi-zhi-bei.html)(点击链接即可查看详细介绍以及获取方法)中。 +## 泛型 - +### 什么是泛型?有什么作用? + +**Java 泛型(Generics)** 是 JDK 5 中引入的一个新特性。使用泛型参数,可以增强代码的可读性以及稳定性。**如无特别说明,以下行为以 Java 8 为准。** + +编译器可以对泛型参数进行检测,并且通过泛型参数可以指定传入的对象类型。比如 `ArrayList persons = new ArrayList()` 这行代码指明了该 `ArrayList` 只能传入 `Person` 类型的对象,如果传入其他类型会报错(JDK 7 起可写 `new ArrayList<>()`,由编译器推断类型参数)。 + +```java +ArrayList extends AbstractList +``` + +并且,原生 `List` 返回类型是 `Object` ,需要手动转换类型才能使用,使用泛型后编译器自动转换。 + +### 泛型的使用方式有哪几种? + +泛型一般有三种使用方式:**泛型类**、**泛型接口**、**泛型方法**。 + +**1.泛型类**: + +```java +//此处T可以随便写为任意标识,常见的如T、E、K、V等形式的参数常用于表示泛型 +//在实例化泛型类时,必须指定T的具体类型 +public class Generic{ + + private T key; + + public Generic(T key) { + this.key = key; + } + + public T getKey(){ + return key; + } +} +``` + +如何实例化泛型类: + +```java +Generic genericInteger = new Generic(123456); +// JDK 7 起可写:new Generic<>(123456) +``` + +**2.泛型接口** : + +```java +public interface Generator { + public T method(); +} +``` + +实现泛型接口,不指定类型: + +```java +class GeneratorImpl implements Generator{ + @Override + public T method() { + return null; + } +} +``` + +实现泛型接口,指定类型: + +```java +class GeneratorImpl implements Generator { + @Override + public String method() { + return "hello"; + } +} +``` + +**3.泛型方法** : + +```java + public static < E > void printArray( E[] inputArray ) + { + for ( E element : inputArray ){ + System.out.printf( "%s ", element ); + } + System.out.println(); + } +``` + +使用: + +```java +// 创建不同类型数组: Integer, Double 和 Character +Integer[] intArray = { 1, 2, 3 }; +String[] stringArray = { "Hello", "World" }; +printArray( intArray ); +printArray( stringArray ); +``` + +### 项目中哪里用到了泛型? + +- 自定义接口通用返回结果 `CommonResult` 通过参数 `T` 可根据具体的返回类型动态指定结果的数据类型 +- 定义 `Excel` 处理类 `ExcelUtil` 用于动态指定 `Excel` 导出的数据类型 +- 构建集合工具类(参考 `Collections` 中的 `sort`, `binarySearch` 方法)。 +- …… + +### 什么是泛型擦除机制?为什么要擦除? + +**Java 的泛型是伪泛型,这是因为 Java 在编译期间,所有的泛型信息都会被擦掉,这也就是通常所说类型擦除 。** + +编译器会在编译期间会动态地将泛型 `T` 擦除为 `Object` 或将 `T extends xxx` 擦除为其限定类型 `xxx` 。 + +因此,泛型本质上其实还是编译器的行为,为了保证引入泛型机制但不创建新的类型,减少虚拟机的运行开销,编译器通过擦除将泛型类转化为一般类。 + +这里说的可能有点抽象,我举个例子: + +```java +List list = new ArrayList<>(); + +list.add(12); +//1.编译期间直接添加会报错 +list.add("a"); +Class clazz = list.getClass(); +Method add = clazz.getDeclaredMethod("add", Object.class); +//2.运行期间通过反射添加,是可以的 +add.invoke(list, "kl"); + +System.out.println(list) +``` + +再来举一个例子 : 由于泛型擦除的问题,下面的方法重载会报错。 + +```java +public void print(List list) { } +public void print(List list) { } +``` + +![泛型擦除的问题](https://oss.javaguide.cn/github/javaguide/java/basis/generics-runtime-erasure.png) + +原因也很简单,泛型擦除之后,`List` 与 `List` 在编译以后都变成了 `List` 。 + +**既然编译器要把泛型擦除,那为什么还要用泛型呢?用 Object 代替不行吗?** + +这个问题其实在变相考察泛型的作用: + +- 使用泛型可在编译期间进行类型检测。 + +- 使用 `Object` 类型需要手动添加强制类型转换,降低代码可读性,提高出错概率。 + +- 泛型可以使用自限定类型如 `T extends Comparable` 。 + +### 什么是桥方法? + +桥方法(`Bridge Method`) 用于继承泛型类时保证多态。 + +```java +class Node { + public T data; + public Node(T data) { this.data = data; } + public void setData(T data) { + System.out.println("Node.setData"); + this.data = data; + } +} + +class MyNode extends Node { + public MyNode(Integer data) { super(data); } + + // Node 泛型擦除后为 setData(Object data),而子类 MyNode 中并没有重写该方法,所以编译器会加入该桥方法保证多态 + public void setData(Object data) { + setData((Integer) data); + } + + public void setData(Integer data) { + System.out.println("MyNode.setData"); + super.setData(data); + } +} +``` + +⚠️**注意** :桥方法为编译器自动生成,非手写。 + +### 泛型有哪些限制?为什么? + +泛型的限制一般是由泛型擦除机制导致的。擦除为 `Object` 后无法进行类型判断 + +- 只能声明不能实例化 `T` 类型变量。 +- 泛型参数不能是基本类型。因为基本类型不是 `Object` 子类,应该用基本类型对应的引用类型代替。 +- 不能实例化泛型参数的数组。擦除后为 `Object` 后无法进行类型判断。 +- 不能实例化泛型数组。 +- 泛型无法使用 `instanceof` 对类型参数 T 做运行期判断;`getClass()` 在擦除后也无法区分不同泛型实参(如 `List` 与 `List` 均得到 `List.class`)。 +- 不能实现两个不同泛型参数的同一接口,擦除后多个父类的桥方法将冲突 +- 不能使用 `static` 修饰泛型变量 +- …… + +### 以下代码是否能编译,为什么? + +```java +public final class Algorithm { + public static T max(T x, T y) { + return x > y ? x : y; + } +} +``` + +无法编译,因为 x 和 y 都会被擦除为 `Object` 类型, `Object` 无法使用 `>` 进行比较 + +```java +public class Singleton { + + public static T getInstance() { + if (instance == null) + instance = new Singleton(); + + return instance; + } + + private static T instance = null; +} +``` + +无法编译,因为不能使用 `static` 修饰泛型 `T` 。 + +## 通配符 + +### 什么是通配符?有什么作用? + +泛型类型是固定的,某些场景下使用起来不太灵活,于是,通配符就来了!通配符可以允许类型参数变化,用来解决泛型无法协变的问题。 + +举个例子: + +```java +// 限制类型为 Person 的子类 + +// 限制类型为 Manager 的父类 + +``` + +### 通配符 ?和常用的泛型 T 之间有什么区别? + +- `T` 可以用于声明变量或常量而 `?` 不行。 +- `T` 一般用于声明泛型类或方法,通配符 `?` 一般用于泛型方法的调用代码和形参。 +- `T` 在编译期会被擦除为限定类型或 `Object`。通配符 `?` 在方法内部会被编译器「捕获」为某个具体但未知的类型(capture),因此不能向 `List` 写入除 `null` 外的元素,但可配合泛型方法使用。 + +### 什么是无界通配符? + +无界通配符可以接收任何泛型类型数据,用于实现不依赖于具体类型参数的简单方法,可以捕获参数类型并交由泛型方法进行处理。 + +```java +void testMethod(Person p) { + // 泛型方法自行处理 +} +``` + +**`List` 和 `List` 有区别吗?** 当然有! + +- `List list` 表示 `list` 的元素类型是**某个未知但固定的类型**(即「存在某一类型 T,list 是 List」),因此编译器不允许向其中添加除 `null` 外的任何元素,以避免类型不安全。 +- `List list` 表示 `list` 持有的元素类型是 `Object`,因此可以添加任何类型的对象,但编译器会给出警告。 + +```java +List list = new ArrayList<>(); +list.add("sss");//报错 +List list2 = new ArrayList<>(); +list2.add("sss");//警告信息 +``` + +### 什么是上边界通配符?什么是下边界通配符? + +在使用泛型的时候,我们还可以为传入的泛型类型实参进行上下边界的限制,如:**类型实参只准传入某种类型的父类或某种类型的子类**。 + +**上边界通配符 `extends`** 可以实现泛型的向上转型即传入的类型实参必须是指定类型的子类型。 + +举个例子: + +```java +// 限制必须是 Person 类的子类 + +``` + +类型边界可以设置多个,还可以对 `T` 类型进行限制。 + +```java + + +``` + +**下边界通配符 `super`** 与上边界通配符 `extends`刚好相反,它可以实现泛型的向下转型即传入的类型实参必须是指定类型的父类型。 + +举个例子: + +```java +// 限制必须是 Employee 类的父类 +List +``` + +**`? extends xxx` 和 `? super xxx` 有什么区别?** + +两者接收参数的范围不同。并且,使用 `? extends xxx` 声明的泛型参数只能调用 `get()` 方法返回 `xxx` 类型,调用 `set()` 报错。使用 `? super xxx` 声明的泛型参数只能调用 `set()` 方法接收 xxx 类型,调用 `get()` 报错。 + +**PECS 原则(Producer Extends, Consumer Super)**:从数据结构**取**元素时用 `extends`(生产者,Producer);向数据结构**写**元素时用 `super`(消费者,Consumer)。例如:`List` 只能从中读取 `Number`,不能写入;`List` 可以写入 `Integer` 及其子类,读取时得到的是 `Object`。`Collections.copy(List dest, List src)` 就是典型用法:从 `src` 读、往 `dest` 写。 + +**`T extends xxx` 和 `? extends xxx` 又有什么区别?** + +`T extends xxx` 用于定义泛型类和方法,擦除后为 xxx 类型, `? extends xxx` 用于声明方法形参,接收 xxx 和其子类型。 + +**`Class` 和 `Class` 的区别?** + +直接使用 Class 的话会有一个类型警告,使用 `Class` 则没有,因为 Class 是一个泛型类,接收原生类型会产生警告 + +### 以下代码是否能编译,为什么? + +```java +class Shape { /* ... */ } +class Circle extends Shape { /* ... */ } +class Rectangle extends Shape { /* ... */ } + +class Node { /* ... */ } + +Node nc = new Node<>(); +Node ns = nc; +``` + +不能,因为`Node` 不是 `Node` 的子类 + +```java +class Shape { /* ... */ } +class Circle extends Shape { /* ... */ } +class Rectangle extends Shape { /* ... */ } + +class Node { /* ... */ } +class ChildNode extends Node{ + +} +ChildNode nc = new ChildNode<>(); +Node ns = nc; +``` + +可以编译,`ChildNode` 是 `Node` 的子类 + +```java +public static void print(List list) { + for (Number n : list) + System.out.print(n + " "); + System.out.println(); +} +``` + +可以编译,`List` 可以往外取元素,但是无法调用 `add()` 添加元素。 + +## 参考 + +- Java 官方文档 : https://docs.oracle.com/javase/tutorial/java/generics/index.html +- Java 基础 一文搞懂泛型:https://www.cnblogs.com/XiiX/p/14719568.html From de611b952078e035bdeff8c1edeb514432a6d814 Mon Sep 17 00:00:00 2001 From: Guide Date: Sun, 22 Feb 2026 23:03:40 +0800 Subject: [PATCH 04/53] =?UTF-8?q?feat:=20=E6=96=B0=E5=A2=9E=E9=9A=90?= =?UTF-8?q?=E8=97=8F=E6=8C=87=E5=AE=9A=E6=96=87=E7=AB=A0=E7=BB=84=E4=BB=B6?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- docs/.vuepress/client.ts | 10 +- .../components/unlock/GlobalUnlock.vue | 296 ++++++++++++++++++ .../components/unlock/UnlockContent.vue | 243 ++++++++++++++ docs/.vuepress/features/unlock/config.ts | 32 ++ docs/.vuepress/features/unlock/heights.ts | 9 + .../public/images/qrcode-javaguide.jpg | Bin 0 -> 18375 bytes docs/.vuepress/unlock-config.ts | 2 + docs/java/basis/generics-and-wildcards.md | 2 +- 8 files changed, 592 insertions(+), 2 deletions(-) create mode 100644 docs/.vuepress/components/unlock/GlobalUnlock.vue create mode 100644 docs/.vuepress/components/unlock/UnlockContent.vue create mode 100644 docs/.vuepress/features/unlock/config.ts create mode 100644 docs/.vuepress/features/unlock/heights.ts create mode 100644 docs/.vuepress/public/images/qrcode-javaguide.jpg create mode 100644 docs/.vuepress/unlock-config.ts diff --git a/docs/.vuepress/client.ts b/docs/.vuepress/client.ts index e6fc1f7b6c5..065db30022b 100644 --- a/docs/.vuepress/client.ts +++ b/docs/.vuepress/client.ts @@ -1,10 +1,18 @@ import { defineClientConfig } from "vuepress/client"; import { h } from "vue"; import LayoutToggle from "./components/LayoutToggle.vue"; +import UnlockContent from "./components/unlock/UnlockContent.vue"; +import GlobalUnlock from "./components/unlock/GlobalUnlock.vue"; export default defineClientConfig({ + enhance({ app }) { + // 注册手动解锁组件 + app.component("UnlockContent", UnlockContent); + }, rootComponents: [ - // 将切换按钮添加为根组件,会在所有页面显示 + // 全局切换按钮 () => h(LayoutToggle), + // 全局扫码解锁控制器 + () => h(GlobalUnlock), ], }); diff --git a/docs/.vuepress/components/unlock/GlobalUnlock.vue b/docs/.vuepress/components/unlock/GlobalUnlock.vue new file mode 100644 index 00000000000..7158449ccb2 --- /dev/null +++ b/docs/.vuepress/components/unlock/GlobalUnlock.vue @@ -0,0 +1,296 @@ + + + + + diff --git a/docs/.vuepress/components/unlock/UnlockContent.vue b/docs/.vuepress/components/unlock/UnlockContent.vue new file mode 100644 index 00000000000..3da283d20bf --- /dev/null +++ b/docs/.vuepress/components/unlock/UnlockContent.vue @@ -0,0 +1,243 @@ + + + + + diff --git a/docs/.vuepress/features/unlock/config.ts b/docs/.vuepress/features/unlock/config.ts new file mode 100644 index 00000000000..5b5f97452d7 --- /dev/null +++ b/docs/.vuepress/features/unlock/config.ts @@ -0,0 +1,32 @@ +import { PREVIEW_HEIGHT } from "./heights"; + +const withDefaultHeight = ( + paths: readonly string[], + height: string = PREVIEW_HEIGHT.LONG, +): Record => + Object.fromEntries(paths.map((path) => [path, height])); + +export const unlockConfig = { + // 版本号变更可强制用户重新验证 + unlockVersion: "v3", + code: "8888", + // 使用相对路径,图片放在 docs/.vuepress/public/images 下 + qrCodeUrl: "/images/qrcode-javaguide.jpg", + // 路径 -> 可见高度(建议使用 PREVIEW_HEIGHT 预设) + protectedPaths: { + ...withDefaultHeight([ + "/java/jvm/memory-area.html", + "/java/basis/java-basic-questions-02.html", + "/java/collection/java-collection-questions-02.html", + "/cs-basics/network/tcp-connection-and-disconnection.html", + "/cs-basics/network/http-vs-https.html", + "/cs-basics/network/dns.html", + "/database/mysql/mysql-questions-01.html", + "/high-performance/sql-optimization.html", + ]), + // 如需特殊高度,再单独覆盖 + // "/some/page.html": PREVIEW_HEIGHT.MEDIUM, + }, +} as const; + +export { PREVIEW_HEIGHT }; diff --git a/docs/.vuepress/features/unlock/heights.ts b/docs/.vuepress/features/unlock/heights.ts new file mode 100644 index 00000000000..9dcbec0216a --- /dev/null +++ b/docs/.vuepress/features/unlock/heights.ts @@ -0,0 +1,9 @@ +export const PREVIEW_HEIGHT = { + SHORT: "500px", + MEDIUM: "1000px", + LONG: "1500px", + XL: "1800px", +} as const; + +export type PreviewHeight = + (typeof PREVIEW_HEIGHT)[keyof typeof PREVIEW_HEIGHT]; diff --git a/docs/.vuepress/public/images/qrcode-javaguide.jpg b/docs/.vuepress/public/images/qrcode-javaguide.jpg new file mode 100644 index 0000000000000000000000000000000000000000..731d912ae05cd91d8426046a673c87d6bdcf2910 GIT binary patch literal 18375 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 p) { **`List` 和 `List` 有区别吗?** 当然有! -- `List list` 表示 `list` 的元素类型是**某个未知但固定的类型**(即「存在某一类型 T,list 是 List」),因此编译器不允许向其中添加除 `null` 外的任何元素,以避免类型不安全。 +- `List list` 表示 `list` 的元素类型是**某个未知但固定的类型**(即「存在某一类型 `T`,list 是 `List`」),因此编译器不允许向其中添加除 `null` 外的任何元素,以避免类型不安全。 - `List list` 表示 `list` 持有的元素类型是 `Object`,因此可以添加任何类型的对象,但编译器会给出警告。 ```java From 225aff20c93e809899aae94649421fe9f338b8a0 Mon Sep 17 00:00:00 2001 From: Guide Date: Mon, 23 Feb 2026 15:33:25 +0800 Subject: [PATCH 05/53] =?UTF-8?q?docs:=E7=BC=93=E5=AD=98=E5=9F=BA=E7=A1=80?= =?UTF-8?q?=E5=B8=B8=E8=A7=81=E9=9D=A2=E8=AF=95=E9=A2=98=E6=80=BB=E7=BB=93?= =?UTF-8?q?=E5=BC=80=E6=94=BE=E9=98=85=E8=AF=BB?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- docs/database/redis/cache-basics.md | 186 +++++++++++++++++++++++++++- 1 file changed, 180 insertions(+), 6 deletions(-) diff --git a/docs/database/redis/cache-basics.md b/docs/database/redis/cache-basics.md index c72ec83879f..2d9238ba769 100644 --- a/docs/database/redis/cache-basics.md +++ b/docs/database/redis/cache-basics.md @@ -1,17 +1,191 @@ --- -title: 缓存基础常见面试题总结(付费) -description: 缓存基础常见面试题总结,深入讲解缓存穿透、缓存击穿、缓存雪崩的原因和解决方案,以及缓存一致性、淘汰策略等核心知识点。 +title: 缓存基础常见面试题总结 +description: 深入讲解缓存的核心思想、本地缓存与分布式缓存的区别、多级缓存架构设计。涵盖Caffeine、Redis等主流缓存方案,以及缓存一致性的解决方案。适合Java开发者学习缓存架构设计。 category: 数据库 tag: - Redis head: - - meta - name: keywords - content: 缓存基础,缓存穿透,缓存击穿,缓存雪崩,缓存一致性,缓存淘汰策略,布隆过滤器,分布式缓存 + content: 缓存,本地缓存,分布式缓存,多级缓存,Caffeine,Redis,缓存一致性,系统设计,Java缓存,Guava Cache --- -**缓存基础** 相关的面试题为我的 [知识星球](../../about-the-author/zhishixingqiu-two-years.md)(点击链接即可查看详细介绍以及加入方法)专属内容,已经整理到了[《Java 面试指北》](../../zhuanlan/java-mian-shi-zhi-bei.md)中。 +> **相关面试题** : +> +> - 为什么要用缓存? +> - 本地缓存应该怎么做? +> - 为什么要有分布式缓存?/为什么不直接用本地缓存? +> - 为什么要用多级缓存? +> - 多级缓存适合哪些业务场景? -![](https://oss.javaguide.cn/javamianshizhibei/database-questions.png) +## 缓存的基本思想 - +很多同学只知道缓存可以提高系统性能以及减少请求相应时间,但是,不太清楚缓存的本质思想是什么。 + +缓存的基本思想其实很简单,就是我们非常熟悉的 **空间换时间** 这一经典性能优化策略的运用。所谓空间换时间,也就是用更多的存储空间来存储一些可能重复使用或计算的数据,从而减少数据的重新获取或计算的时间。 + +说到空间换时间,除了缓存之外,你还能想到什么其他的例子吗?这里再列举几个常见的: + +- 索引:索引是一种将数据库表中的某些列或字段按照一定的排序规则组织成一个单独的数据结构,需要额外占用空间,但可以大大提高检索效率,降低数据排序成本。 +- 数据库表字段冗余:将经常联合查询的数据冗余存储在同一张表中,以减少对多张表的关联查询,进而提升查询性能,减轻数据库压力。 +- CDN(内容分发网络):将静态资源分发到多个不同的地方以实现就近访问,进而加快静态资源的访问速度,减轻服务器以及带宽的负担。 + +编程需要要学会归纳总结,将自己学到的东西串联起来!假如你在面试的时候,能聊到这些,面试官一定会对你有一个好印象的。 + +不要把缓存想的太高大上,虽然,它的确对系统的性能提升的性价比非常高。当我们在学习并应用缓存的时候,你会发现缓存的思想实际在 CPU、操作系统或者其他很多地方都被大量用到。 + +比如,CPU Cache 缓存的是内存数据用于解决 CPU 处理速度和内存不匹配的问题,内存缓存的是硬盘数据用于解决硬盘访问速度过慢的问题。 + +![CPU 缓存模型示意图](https://oss.javaguide.cn/github/javaguide/java/concurrent/cpu-cache.png) + +再比如,为了提高虚拟地址到物理地址的转换速度,操作系统在页表方案基础之上引入了转址旁路缓存(Translation Lookasjde Buffer,TLB,也被称为快表) 。 + +![加入 TLB 之后的地址翻译](https://oss.javaguide.cn/github/javaguide/cs-basics/operating-system/physical-virtual-address-translation-mmu.png) + +再拿我们日常使用的软件来说,一般都会对你访问过的一些图片或者文件进行缓存,这样你下次再访问的时候加载速度就很快了。 + +![](https://oss.javaguide.cn/github/javaguide/database/redis/chrome-clear-cache.png) + +我们日常开发过程中用到的缓存,其中的数据通常存储于内存中,因此访问速度非常快。为了避免内存中的数据在重启或者宕机之后丢失,很多缓存中间件会利用磁盘做持久化。也就是说,缓存相比较于我们常用的关系型数据库(比如 MySQL)来说访问速度要快非常多。为了避免用户请求数据库中的数据速度过于缓慢,我们可以在数据库之上增加一层缓存。 + +除了能够提高访问速度之外,缓存支持的并发量也要更大,有了缓存之后,数据库的压力也会随之变小。 + +## 缓存的分类 + +接下来,我们来看看日常开发中用到的缓存通常被分为哪几种。 + +### 本地缓存 + +#### 什么是本地缓存? + +这个实际在很多项目中用的蛮多,特别是单体架构的时候。数据量不大,并且没有分布式要求的话,使用本地缓存还是可以的。 + +本地缓存位于应用内部,其最大的优点是应用存在于同一个进程内部,请求本地缓存的速度非常快,不存在额外的网络开销。 + +常见的单体架构图如下,我们使用 **Nginx** 来做**负载均衡**,部署两个相同的应用到服务器,两个服务使用同一个数据库,并且使用的是本地缓存。 + +![本地缓存示意图](https://oss.javaguide.cn/github/javaguide/database/redis/local-cache.png) + +#### 本地缓存的方案有哪些? + +**1、JDK 自带的 `HashMap` 和 `ConcurrentHashMap` 了。** + +`ConcurrentHashMap` 可以看作是线程安全版本的 `HashMap` ,两者都是存放 key/value 形式的键值对。但是,大部分场景来说不会使用这两者当做缓存,因为只提供了缓存的功能,并没有提供其他诸如过期时间之类的功能。一个稍微完善一点的缓存框架至少要提供:**过期时间**、**淘汰机制**、**命中率统计**这三点。 + +**2、 `Ehcache` 、 `Guava Cache` 、 `Spring Cache` 这三者是使用的比较多的本地缓存框架。** + +- `Ehcache` 的话相比于其他两者更加重量。不过,相比于 `Guava Cache` 、 `Spring Cache` 来说, `Ehcache` 支持可以嵌入到 hibernate 和 mybatis 作为多级缓存,并且可以将缓存的数据持久化到本地磁盘中、同时也提供了集群方案(比较鸡肋,可忽略)。 +- `Guava Cache` 和 `Spring Cache` 两者的话比较像。`Guava` 相比于 `Spring Cache` 的话使用的更多一点,它提供了 API 非常方便我们使用,同时也提供了设置缓存有效时间等功能。它的内部实现也比较干净,很多地方都和 `ConcurrentHashMap` 的思想有异曲同工之妙。 +- 使用 `Spring Cache` 的注解实现缓存的话,代码会看着很干净和优雅,但是很容易出现问题比如缓存穿透、内存溢出。 + +**3、后起之秀 Caffeine。** + +相比于 `Guava` 来说 `Caffeine` 在各个方面比如性能都要更加优秀,一般建议使用其来替代 `Guava` 。并且, `Guava` 和 `Caffeine` 的使用方式很像! + +使用 `Caffeine` 创建本地缓存的代码示例,用到了建造者模式: + +```java +Caffeine.newBuilder() + // 设置最后一次写入或访问后经过固定时间过期 + .expireAfterWrite(60, TimeUnit.DAYS) + // 初始的缓存空间大小 + .initialCapacity(100) + // 缓存的最大条数 + .maximumSize(500) + .build(); +``` + +#### 本地缓存有什么痛点? + +本地的缓存的优势非常明显:**低依赖**、**轻量**、**简单**、**成本低**。 + +但是,本地缓存存在下面这些缺陷: + +- **本地缓存应用耦合,对分布式架构支持不友好**,比如同一个相同的服务部署在多台机器上的时候,各个服务之间的缓存是无法共享的,因为本地缓存只在当前机器上有。 +- **本地缓存容量受服务部署所在的机器限制明显。** 如果当前系统服务所耗费的内存多,那么本地缓存可用的容量就很少。 + +### 分布式缓存 + +#### 什么是分布式缓存? + +我们可以把分布式缓存(Distributed Cache) 看作是一种内存数据库的服务,它的最终作用就是提供缓存数据的服务。 + +分布式缓存脱离于应用独立存在,多个应用可直接的共同使用同一个分布式缓存服务。 + +如下图所示,就是一个简单的使用分布式缓存的架构图。我们使用 Nginx 来做负载均衡,部署两个相同的应用到服务器,两个服务使用同一个数据库和缓存。 + +![分布式缓存](https://oss.javaguide.cn/github/javaguide/database/redis/distributed-cache.png) + +使用分布式缓存之后,缓存服务可以部署在一台单独的服务器上,即使同一个相同的服务部署在多台机器上,也是使用的同一份缓存。 并且,单独的分布式缓存服务的性能、容量和提供的功能都要更加强大。 + +**软件系统设计中没有银弹,往往任何技术的引入都像是把双刃剑。** 你使用的方式得当,就能为系统带来很大的收益。否则,只是费了精力不讨好。 + +简单来说,为系统引入分布式缓存之后往往会带来下面这些问题: + +- **系统复杂性增加** :引入缓存之后,你要维护缓存和数据库的数据一致性、维护热点缓存、保证缓存服务的高可用等等。 +- **系统开发成本往往会增加** :引入缓存意味着系统需要一个单独的缓存服务,这是需要花费相应的成本的,并且这个成本还是很贵的,毕竟耗费的是宝贵的内存。 + +#### 分布式缓存的方案有哪些? + +分布式缓存的话,比较老牌同时也是使用的比较多的还是 **Memcached** 和 **Redis**。不过,现在基本没有看过还有项目使用 **Memcached** 来做缓存,都是直接用 **Redis**。 + +Memcached 是分布式缓存最开始兴起的那会,比较常用的。后来,随着 Redis 的发展,大家慢慢都转而使用更加强大的 Redis 了。 + +有一些大厂也开源了类似于 Redis 的分布式高性能 KV 存储数据库,例如,腾讯开源的 [Tendis](https://github.com/Tencent/Tendis) 。Tendis 基于知名开源项目 [RocksDB](https://github.com/facebook/rocksdb) 作为存储引擎 ,100% 兼容 Redis 协议和 Redis4.0 所有数据模型。关于 Redis 和 Tendis 的对比,腾讯官方曾经发过一篇文章:[Redis vs Tendis:冷热混合存储版架构揭秘](https://mp.weixin.qq.com/s/MeYkfOIdnU6LYlsGb24KjQ) ,可以简单参考一下。 + +不过,从 Tendis 这个项目的 Github 提交记录可以看出,Tendis 开源版几乎已经没有被维护更新了,加上其关注度并不高,使用的公司也比较少。因此,不建议你使用 Tendis 来实现分布式缓存。 + +目前,比较业界认可的 Redis 替代品还是下面这两个开源分布式缓存(都是通过碰瓷 Redis 火的): + +- [Dragonfly](https://github.com/dragonflydb/dragonfly):一种针对现代应用程序负荷需求而构建的内存数据库,完全兼容 Redis 和 Memcached 的 API,迁移时无需修改任何代码,号称全世界最快的内存数据库。 +- [KeyDB](https://github.com/Snapchat/KeyDB): Redis 的一个高性能分支,专注于多线程、内存效率和高吞吐量。 + +不过,个人还是建议分布式缓存首选 Redis ,毕竟经过这么多年的生产考验,生态也这么优秀,资料也很全面。 + +### 多级缓存 + +#### 什么是多级缓存?为什么要用? + +我们这里只来简单聊聊 **本地缓存 + 分布式缓存** 的多级缓存方案,这也是最常用的多级缓存实现方式。 + +这个时候估计有很多小伙伴就会问了:**既然用了分布式缓存,为什么还要用本地缓存呢?** 。 + +本地缓存和分布式缓存虽然都属于缓存,但本地缓存的访问速度要远大于分布式缓存,这是因为访问本地缓存不存在额外的网络开销,我们在上面也提到了。 + +不过,一般情况下,我们也是不建议使用多级缓存的,这会增加维护负担(比如你需要保证一级缓存和二级缓存的数据一致性)。而且,其实际带来的提升效果对于绝大部分业务场景来说其实并不是很大。 + +这里简单总结一下适合多级缓存的两种业务场景: + +- 缓存的数据不会频繁修改,比较稳定; +- 数据访问量特别大比如秒杀场景。 + +多级缓存方案中,第一级缓存(L1)使用本地内存(比如 Caffeine)),第二级缓存(L2)使用分布式缓存(比如 Redis)。 + +![多级缓存](https://oss.javaguide.cn/javaguide/database/redis/multilevel-cache.png) + +读取缓存数据的时候,我们先从 L1 中读取,读取不到的时候再去 L2 读取。这样可以降低 L2 的压力,减少 L2 的读次数。如果 L2 也没有此数据的话,再去数据库查询,数据查询成功后再将数据写入到 L1 和 L2 中。 + +多级缓存开源实现推荐: + +- [J2Cache](https://gitee.com/ld/J2Cache):基于本地内存和 Redis 的两级 Java 缓存框架。 +- [JetCache](https://github.com/alibaba/jetcache):阿里开源的缓存框架,支持多级缓存、分布式缓存自动刷新、 TTL 等功能。 + +如果你想要在自己的项目中实践多级缓存的话,还可以参考借鉴一下 [Redis+Caffeine 两级缓存,让访问速度纵享丝滑](https://mp.weixin.qq.com/s/_ysKYrzyRGebtotGyzQUIw) 这篇文章。 + +#### 多级缓存一致性如何保证? + +在多级缓存系统中,保证强一致性成本太高,业界的几个提供多级缓存功能的缓存框架基本都是最终一致性保证。例如,可以使用 Redis 的发布/订阅机制、Redis Stream 或者消息队列来确保当一个实例的本地缓存发生变化时,其他实例能够及时更新其本地缓存,以保持缓存一致性。 + +政采云技术的方案是 Canal + 广播消息,这里简单介绍一下: + +1. DB 修改数据:首先在数据库中进行数据修改。 +2. 通过监听 Canal 消息,触发缓存的更新:使用 Canal 监听数据库的变更操作,当检测到数据变化时,触发缓存更新。 +3. 同步 Redis 缓存:对于 Redis 缓存,因为集群中只共享一份数据,所以直接同步缓存即可。 +4. 同步本地缓存:由于本地缓存分布在不同的 JVM 实例中,需要借助广播消息队列(MQ)机制,将更新通知广播到各个业务实例,从而同步本地缓存。 + +详细介绍:[分布式多级缓存系统设计与实战](https://juejin.cn/post/7225634879152570405) + +## 参考 + +- 缓存那些事:https://tech.meituan.com/2017/03/17/cache-about.html +- 解析分布式系统的缓存设计:https://segmentfault.com/a/1190000041689802 From 8e8575f18673c632acdf8c0572a4d0964f67d9f7 Mon Sep 17 00:00:00 2001 From: Guide Date: Mon, 23 Feb 2026 15:33:38 +0800 Subject: [PATCH 06/53] =?UTF-8?q?feat:=E4=BC=98=E5=8C=96=E9=98=85=E8=AF=BB?= =?UTF-8?q?=E5=85=A8=E6=96=87=E6=8F=92=E4=BB=B6?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- docs/.vuepress/client.ts | 10 +- .../components/unlock/GlobalUnlock.vue | 352 ++++++++++++------ docs/.vuepress/features/unlock/config.ts | 6 +- docs/.vuepress/features/unlock/heights.ts | 3 +- docs/.vuepress/shims-vue.d.ts | 5 + docs/.vuepress/unlock-config.ts | 2 - 6 files changed, 252 insertions(+), 126 deletions(-) create mode 100644 docs/.vuepress/shims-vue.d.ts delete mode 100644 docs/.vuepress/unlock-config.ts diff --git a/docs/.vuepress/client.ts b/docs/.vuepress/client.ts index 065db30022b..9468f265cd4 100644 --- a/docs/.vuepress/client.ts +++ b/docs/.vuepress/client.ts @@ -1,18 +1,12 @@ import { defineClientConfig } from "vuepress/client"; import { h } from "vue"; import LayoutToggle from "./components/LayoutToggle.vue"; -import UnlockContent from "./components/unlock/UnlockContent.vue"; import GlobalUnlock from "./components/unlock/GlobalUnlock.vue"; +import UnlockContent from "./components/unlock/UnlockContent.vue"; export default defineClientConfig({ enhance({ app }) { - // 注册手动解锁组件 app.component("UnlockContent", UnlockContent); }, - rootComponents: [ - // 全局切换按钮 - () => h(LayoutToggle), - // 全局扫码解锁控制器 - () => h(GlobalUnlock), - ], + rootComponents: [() => h(LayoutToggle), () => h(GlobalUnlock)], }); diff --git a/docs/.vuepress/components/unlock/GlobalUnlock.vue b/docs/.vuepress/components/unlock/GlobalUnlock.vue index 7158449ccb2..4675347aa20 100644 --- a/docs/.vuepress/components/unlock/GlobalUnlock.vue +++ b/docs/.vuepress/components/unlock/GlobalUnlock.vue @@ -1,58 +1,85 @@ -.lock-overlay-container { - position: fixed; +.read-more-anchor { + position: absolute; left: 0; right: 0; - bottom: 32px; - z-index: 9999; + bottom: 0; + height: 190px; display: flex; + align-items: flex-end; justify-content: center; + padding-bottom: 24px; + z-index: 10; + pointer-events: none; +} + +.read-more-mask { + position: absolute; + inset: 0; + background: linear-gradient( + to bottom, + rgba(255, 255, 255, 0), + var(--bg-color, #fff) 72% + ); pointer-events: none; } -.lock-card { - width: min(92vw, 480px); - padding: 1.25rem; +[data-theme="dark"] .read-more-mask { + background: linear-gradient( + to bottom, + rgba(29, 30, 32, 0), + var(--bg-color, #1d1e20) 72% + ); +} + +.read-more-btn { + position: relative; + z-index: 11; + pointer-events: auto; + min-width: 132px; + padding: 0.56rem 1.35rem; + border: 1px solid rgba(62, 175, 124, 0.45); + border-radius: 999px; + background: var(--bg-color, #fff); + color: #3eaf7c; + font-weight: 700; + cursor: pointer; + box-shadow: 0 8px 20px rgba(62, 175, 124, 0.16); + transition: all 0.2s ease; +} + +.read-more-btn:hover { + transform: translateY(-1px); + box-shadow: 0 10px 24px rgba(62, 175, 124, 0.2); +} + +.unlock-modal-mask { + position: fixed; + inset: 0; + z-index: 9999; + background: rgba(15, 23, 42, 0.45); + backdrop-filter: blur(2px); + display: flex; + align-items: center; + justify-content: center; + padding: 16px; +} + +.unlock-modal { + width: min(92vw, 500px); + padding: 1.2rem; border-radius: 14px; border: 1px solid var(--border-color, #e5e7eb); background: var(--bg-color, #fff); - box-shadow: 0 10px 32px rgba(0, 0, 0, 0.12); + box-shadow: 0 10px 36px rgba(0, 0, 0, 0.18); text-align: center; - pointer-events: auto; } -.lock-icon { - font-size: 1.9rem; +.unlock-modal-header { + display: flex; + align-items: center; + justify-content: space-between; + gap: 12px; + margin-bottom: 0.75rem; +} + +.close-btn { + width: 28px; + height: 28px; + border: 0; + border-radius: 999px; + background: #f1f5f9; + color: #334155; + font-size: 18px; + line-height: 28px; + cursor: pointer; + flex-shrink: 0; } .lock-title { - margin: 0.35rem 0 0; - font-size: 1.2rem; + margin: 0; + font-size: 1.16rem; } .lock-reason { - margin: 0.75rem 0 1rem; + margin: 0 0 1rem; color: #64748b; line-height: 1.6; + font-size: 0.9rem; } .qr-container { margin: 0 auto 1rem; - padding: 0.85rem; + padding: 0.8rem; max-width: 300px; border: 1px dashed #3eaf7c; border-radius: 10px; @@ -207,8 +324,8 @@ watch( } .qr-image { - width: 140px; - height: 140px; + width: 136px; + height: 136px; } .qr-tip { @@ -234,6 +351,11 @@ watch( border: 1px solid #d1d5db; font-size: 1rem; text-align: center; + outline: none; +} + +.unlock-input:focus { + border-color: #3eaf7c; } .unlock-btn { @@ -252,14 +374,18 @@ watch( font-size: 0.85rem; } -.lock-footer { - margin: 0.7rem 0 0; - color: #94a3b8; - font-size: 0.8rem; +.unlock-fade-enter-active, +.unlock-fade-leave-active { + transition: opacity 0.2s ease; +} + +.unlock-fade-enter-from, +.unlock-fade-leave-to { + opacity: 0; } .shake-enter-active { - animation: shake 0.5s cubic-bezier(0.36, 0.07, 0.19, 0.97) both; + animation: shake 0.45s cubic-bezier(0.36, 0.07, 0.19, 0.97) both; } @keyframes shake { @@ -274,11 +400,11 @@ watch( 30%, 50%, 70% { - transform: translate3d(-4px, 0, 0); + transform: translate3d(-3px, 0, 0); } 40%, 60% { - transform: translate3d(4px, 0, 0); + transform: translate3d(3px, 0, 0); } } diff --git a/docs/.vuepress/features/unlock/config.ts b/docs/.vuepress/features/unlock/config.ts index 5b5f97452d7..b466495289c 100644 --- a/docs/.vuepress/features/unlock/config.ts +++ b/docs/.vuepress/features/unlock/config.ts @@ -2,13 +2,15 @@ import { PREVIEW_HEIGHT } from "./heights"; const withDefaultHeight = ( paths: readonly string[], - height: string = PREVIEW_HEIGHT.LONG, + height: string = PREVIEW_HEIGHT.XXL, ): Record => Object.fromEntries(paths.map((path) => [path, height])); export const unlockConfig = { // 版本号变更可强制用户重新验证 - unlockVersion: "v3", + unlockVersion: "v5", + // 调试用:设为 true 时无视本地已解锁状态,始终触发限制 + forceLock: false, code: "8888", // 使用相对路径,图片放在 docs/.vuepress/public/images 下 qrCodeUrl: "/images/qrcode-javaguide.jpg", diff --git a/docs/.vuepress/features/unlock/heights.ts b/docs/.vuepress/features/unlock/heights.ts index 9dcbec0216a..34ba390ca45 100644 --- a/docs/.vuepress/features/unlock/heights.ts +++ b/docs/.vuepress/features/unlock/heights.ts @@ -2,7 +2,8 @@ export const PREVIEW_HEIGHT = { SHORT: "500px", MEDIUM: "1000px", LONG: "1500px", - XL: "1800px", + XL: "2000px", + XXL: "2500px", } as const; export type PreviewHeight = diff --git a/docs/.vuepress/shims-vue.d.ts b/docs/.vuepress/shims-vue.d.ts new file mode 100644 index 00000000000..525d5f827b6 --- /dev/null +++ b/docs/.vuepress/shims-vue.d.ts @@ -0,0 +1,5 @@ +declare module "*.vue" { + import type { DefineComponent } from "vue"; + const component: DefineComponent; + export default component; +} diff --git a/docs/.vuepress/unlock-config.ts b/docs/.vuepress/unlock-config.ts deleted file mode 100644 index 06ee4a1e2c3..00000000000 --- a/docs/.vuepress/unlock-config.ts +++ /dev/null @@ -1,2 +0,0 @@ -// 兼容旧导入路径:建议改为 `./features/unlock/config` -export { unlockConfig, PREVIEW_HEIGHT } from "./features/unlock/config"; From 9f2eca9548e79e4e3a8d902377b9325858bc5921 Mon Sep 17 00:00:00 2001 From: Guide Date: Mon, 23 Feb 2026 15:41:13 +0800 Subject: [PATCH 07/53] =?UTF-8?q?feat:=E4=BC=98=E5=8C=96=E9=98=85=E8=AF=BB?= =?UTF-8?q?=E5=85=A8=E6=96=87=E6=8F=92=E4=BB=B6?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../components/unlock/GlobalUnlock.vue | 23 ++++++++++++++++--- docs/.vuepress/features/unlock/config.ts | 13 +++++++---- 2 files changed, 28 insertions(+), 8 deletions(-) diff --git a/docs/.vuepress/components/unlock/GlobalUnlock.vue b/docs/.vuepress/components/unlock/GlobalUnlock.vue index 4675347aa20..681e28b5b3c 100644 --- a/docs/.vuepress/components/unlock/GlobalUnlock.vue +++ b/docs/.vuepress/components/unlock/GlobalUnlock.vue @@ -85,19 +85,36 @@ const globalUnlockKey = `javaguide_site_unlocked_${config.unlockVersion ?? "v1"} const normalizePath = (path: string) => path.replace(/\/$/, "").replace(".html", "").toLowerCase(); +const isPathInPrefix = (currentPath: string, prefix: string) => { + return currentPath === prefix || currentPath.startsWith(`${prefix}/`); +}; + const isLockedPage = computed(() => { const currentPath = normalizePath(pageData.value.path); - return Object.keys(config.protectedPaths) + const byExactPath = Object.keys(config.protectedPaths) .map((p) => normalizePath(p)) .includes(currentPath); + if (byExactPath) return true; + + const prefixes = Object.keys(config.protectedPrefixes ?? {}).map((p) => + normalizePath(p), + ); + return prefixes.some((prefix) => isPathInPrefix(currentPath, prefix)); }); const visibleHeight = computed(() => { const currentPath = normalizePath(pageData.value.path); - const matched = Object.keys(config.protectedPaths).find( + const matchedPath = Object.keys(config.protectedPaths).find( (p) => normalizePath(p) === currentPath, ); - return matched ? config.protectedPaths[matched] : PREVIEW_HEIGHT.LONG; + if (matchedPath) return config.protectedPaths[matchedPath]; + + const matchedPrefix = Object.keys(config.protectedPrefixes ?? {}).find( + (prefix) => isPathInPrefix(currentPath, normalizePath(prefix)), + ); + if (matchedPrefix) return config.protectedPrefixes[matchedPrefix]; + + return PREVIEW_HEIGHT.LONG; }); const toPx = (value: string) => { diff --git a/docs/.vuepress/features/unlock/config.ts b/docs/.vuepress/features/unlock/config.ts index b466495289c..7a5fdd921c1 100644 --- a/docs/.vuepress/features/unlock/config.ts +++ b/docs/.vuepress/features/unlock/config.ts @@ -2,15 +2,15 @@ import { PREVIEW_HEIGHT } from "./heights"; const withDefaultHeight = ( paths: readonly string[], - height: string = PREVIEW_HEIGHT.XXL, + height: string = PREVIEW_HEIGHT.XL, ): Record => Object.fromEntries(paths.map((path) => [path, height])); export const unlockConfig = { // 版本号变更可强制用户重新验证 - unlockVersion: "v5", + unlockVersion: "v1", // 调试用:设为 true 时无视本地已解锁状态,始终触发限制 - forceLock: false, + forceLock: true, code: "8888", // 使用相对路径,图片放在 docs/.vuepress/public/images 下 qrCodeUrl: "/images/qrcode-javaguide.jpg", @@ -23,12 +23,15 @@ export const unlockConfig = { "/cs-basics/network/tcp-connection-and-disconnection.html", "/cs-basics/network/http-vs-https.html", "/cs-basics/network/dns.html", - "/database/mysql/mysql-questions-01.html", - "/high-performance/sql-optimization.html", ]), // 如需特殊高度,再单独覆盖 // "/some/page.html": PREVIEW_HEIGHT.MEDIUM, }, + // 目录前缀 -> 可见高度(该目录下所有文章都触发验证) + // 例如 "/java/collection/" 会匹配 "/java/collection/**" + protectedPrefixes: { + ...withDefaultHeight(["/database/", "/high-performance/"]), + }, } as const; export { PREVIEW_HEIGHT }; From 4baa23dae1e86475b151fc62308d3f3b02891e2a Mon Sep 17 00:00:00 2001 From: Guide Date: Tue, 24 Feb 2026 00:04:32 +0800 Subject: [PATCH 08/53] =?UTF-8?q?fix:=20=E4=BF=AE=E6=94=B9=20GlobalUnlock.?= =?UTF-8?q?vue=20=E8=A7=A3=E5=86=B3=20SSR=20=E6=B0=B4=E5=90=88=E9=98=B6?= =?UTF-8?q?=E6=AE=B5=20+=20=E5=8A=A8=E6=80=81=20DOM=20=E6=93=8D=E4=BD=9C?= =?UTF-8?q?=E5=86=B2=E7=AA=81=E3=80=82?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../components/unlock/GlobalUnlock.vue | 32 ++++++++++++++----- docs/.vuepress/features/unlock/config.ts | 2 +- 2 files changed, 25 insertions(+), 9 deletions(-) diff --git a/docs/.vuepress/components/unlock/GlobalUnlock.vue b/docs/.vuepress/components/unlock/GlobalUnlock.vue index 681e28b5b3c..c5bbf1aa990 100644 --- a/docs/.vuepress/components/unlock/GlobalUnlock.vue +++ b/docs/.vuepress/components/unlock/GlobalUnlock.vue @@ -1,6 +1,6 @@