diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index 144625d3026..efe78bf3f24 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -10,19 +10,20 @@ jobs: runs-on: ubuntu-latest steps: - name: Checkout - uses: actions/checkout@v3 + uses: actions/checkout@v4 - name: Install pnpm - uses: pnpm/action-setup@v2 - with: - run_install: true + uses: pnpm/action-setup@v4 - name: Setup Node.js - uses: actions/setup-node@v3 + uses: actions/setup-node@v4 with: - node-version: 18 + node-version: 22 cache: pnpm + - name: Install deps + run: pnpm install --frozen-lockfile + - name: Build test env: NODE_OPTIONS: --max_old_space_size=4096 diff --git a/.gitignore b/.gitignore index e094687fd0c..242ea3b9602 100644 --- a/.gitignore +++ b/.gitignore @@ -10,8 +10,9 @@ node_modules/ **/.vuepress/.temp/ # VuePress Output dist/ -# Build files -packages/*/lib/ traversal-folder-replace-string.py format-markdown.py + +.npmrc package-lock.json +lintmd-config.json diff --git a/.husky/pre-commit b/.husky/pre-commit index 523f31ae8c8..74821141635 100755 --- a/.husky/pre-commit +++ b/.husky/pre-commit @@ -1,4 +1 @@ -#!/bin/sh -. "$(dirname "$0")/_/husky.sh" - pnpm nano-staged diff --git a/.markdownlint-cli2.mjs b/.markdownlint-cli2.mjs new file mode 100644 index 00000000000..c2bda64a99b --- /dev/null +++ b/.markdownlint-cli2.mjs @@ -0,0 +1,28 @@ +export default { + config: { + default: true, + MD003: { + style: "atx", + }, + MD004: { + style: "dash", + }, + MD010: false, + MD013: false, + MD024: { + allow_different_nesting: true, + }, + MD035: { + style: "---", + }, + MD036: false, + MD040: false, + MD045: false, + MD046: false, + }, + ignores: [ + "**/node_modules/**", + // markdown import demo + "**/*.snippet.md", + ], +}; diff --git a/.markdownlint.json b/.markdownlint.json deleted file mode 100644 index b6f98c5112d..00000000000 --- a/.markdownlint.json +++ /dev/null @@ -1,20 +0,0 @@ -{ - "default": true, - "MD003": { - "style": "atx" - }, - "MD004": { - "style": "dash" - }, - "MD013": false, - "MD024": { - "allow_different_nesting": true - }, - "MD035": { - "style": "---" - }, - "MD040": false, - "MD045": false, - "MD046": false, - "MD049": false -} diff --git a/.markdownlintignore b/.markdownlintignore deleted file mode 100644 index d5d67721c60..00000000000 --- a/.markdownlintignore +++ /dev/null @@ -1,4 +0,0 @@ -**/node_modules/** - -# markdown snippets -*.snippet.md diff --git a/README.md b/README.md index aff6a1d4342..e193e6b5b8f 100755 --- a/README.md +++ b/README.md @@ -1,26 +1,20 @@ -推荐你通过在线阅读网站进行阅读,体验更好,速度更快! +推荐你通过在线阅读网站进行阅读,体验更好,速度更快!地址:[javaguide.cn](https://javaguide.cn/)。 -- **[JavaGuide 在线阅读网站(新版,推荐 👍)](https://javaguide.cn/)** -- [JavaGuide 在线阅读版(老版)](https://snailclimb.gitee.io/javaguide/#/) - -[](https://sourl.cn/e7ee87) +[](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) -[![阅读](https://img.shields.io/badge/阅读-read-brightgreen.svg)](https://javaguide.cn/) -![Stars](https://img.shields.io/github/stars/Snailclimb/JavaGuide) -![forks](https://img.shields.io/github/forks/Snailclimb/JavaGuide) -![issues](https://img.shields.io/github/issues/Snailclimb/JavaGuide) - [GitHub](https://github.com/Snailclimb/JavaGuide) | [Gitee](https://gitee.com/SnailClimb/JavaGuide)
-> 1. **面试专版**:准备面试的小伙伴可以考虑面试专版:[《Java 面试指北 》](https://javaguide.cn/zhuanlan/java-mian-shi-zhi-bei.html) (质量很高,专为面试打造,配合 JavaGuide 食用)。 -> 1. **知识星球**:专属面试小册/一对一交流/简历修改/专属求职指南,欢迎加入 [JavaGuide 知识星球](https://javaguide.cn/about-the-author/zhishixingqiu-two-years.html)(点击链接即可查看星球的详细介绍,一定一定一定确定自己真的需要再加入,一定一定要看完详细介绍之后再加我)。 -> 1. **转载须知**:以下所有文章如非文首说明为转载皆为我(Guide)的原创,转载在文首注明出处,如发现恶意抄袭/搬运,会动用法律武器维护自己的权益。让我们一起维护一个良好的技术创作环境! +> - **面试专版**:准备 Java 面试的小伙伴可以考虑面试专版:**[《Java 面试指北 》](./docs/zhuanlan/java-mian-shi-zhi-bei.md)** (质量很高,专为面试打造,配合 JavaGuide 食用)。 +> - **知识星球**:专属面试小册/一对一交流/简历修改/专属求职指南,欢迎加入 **[JavaGuide 知识星球](./docs/about-the-author/zhishixingqiu-two-years.md)**(点击链接即可查看星球的详细介绍,一定确定自己真的需要再加入)。 +> - **使用建议** :有水平的面试官都是顺着项目经历挖掘技术问题。一定不要死记硬背技术八股文!详细的学习建议请参考:[JavaGuide 使用建议](./docs/javaguide/use-suggestion.md)。 +> - **求个Star**:如果觉得 JavaGuide 的内容对你有帮助的话,还请点个免费的 Star,这是对我最大的鼓励,感谢各位一起同行,共勉!Github 地址:[https://github.com/Snailclimb/JavaGuide](https://github.com/Snailclimb/JavaGuide) 。 +> - **转载须知**:以下所有文章如非文首说明为转载皆为 JavaGuide 原创,转载请在文首注明出处。如发现恶意抄袭/搬运,会动用法律武器维护自己的权益。让我们一起维护一个良好的技术创作环境!
@@ -30,9 +24,10 @@ ## 项目相关 -- [项目介绍](./docs/javaguide/intro.md) -- [贡献指南](./docs/javaguide/contribution-guideline.md) -- [常见问题](./docs/javaguide/faq.md) +- [项目介绍](https://javaguide.cn/javaguide/intro.html) +- [使用建议](https://javaguide.cn/javaguide/use-suggestion.html) +- [贡献指南](https://javaguide.cn/javaguide/contribution-guideline.html) +- [常见问题](https://javaguide.cn/javaguide/faq.html) ## Java @@ -66,15 +61,22 @@ **源码分析**: -- [ArrayList 源码+扩容机制分析](./docs/java/collection/arraylist-source-code.md) -- [HashMap(JDK1.8)源码+底层数据结构分析](./docs/java/collection/hashmap-source-code.md) -- [ConcurrentHashMap 源码+底层数据结构分析](./docs/java/collection/concurrent-hash-map-source-code.md) +- [ArrayList 核心源码+扩容机制分析](./docs/java/collection/arraylist-source-code.md) +- [LinkedList 核心源码分析](./docs/java/collection/linkedlist-source-code.md) +- [HashMap 核心源码+底层数据结构分析](./docs/java/collection/hashmap-source-code.md) +- [ConcurrentHashMap 核心源码+底层数据结构分析](./docs/java/collection/concurrent-hash-map-source-code.md) +- [LinkedHashMap 核心源码分析](./docs/java/collection/linkedhashmap-source-code.md) +- [CopyOnWriteArrayList 核心源码分析](./docs/java/collection/copyonwritearraylist-source-code.md) +- [ArrayBlockingQueue 核心源码分析](./docs/java/collection/arrayblockingqueue-source-code.md) +- [PriorityQueue 核心源码分析](./docs/java/collection/priorityqueue-source-code.md) +- [DelayQueue 核心源码分析](./docs/java/collection/delayqueue-source-code.md) ### IO - [IO 基础知识总结](./docs/java/io/io-basis.md) - [IO 设计模式总结](./docs/java/io/io-design-patterns.md) - [IO 模型详解](./docs/java/io/io-model.md) +- [NIO 核心知识总结](./docs/java/io/nio-basis.md) ### 并发 @@ -86,6 +88,8 @@ **重要知识点详解**: +- [乐观锁和悲观锁详解](./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) - [ThreadLocal 详解](./docs/java/concurrent/threadlocal.md) @@ -96,7 +100,7 @@ ### JVM (必看 :+1:) -JVM 这部分内容主要参考 [JVM 虚拟机规范-Java8 ](https://docs.oracle.com/javase/specs/jvms/se8/html/index.html) 和周志明老师的[《深入理解 Java 虚拟机(第 3 版)》](https://book.douban.com/subject/34907497/) (强烈建议阅读多遍!)。 +JVM 这部分内容主要参考 [JVM 虚拟机规范-Java8](https://docs.oracle.com/javase/specs/jvms/se8/html/index.html) 和周志明老师的[《深入理解 Java 虚拟机(第 3 版)》](https://book.douban.com/subject/34907497/) (强烈建议阅读多遍!)。 - **[Java 内存区域](./docs/java/jvm/memory-area.md)** - **[JVM 垃圾回收](./docs/java/jvm/jvm-garbage-collection.md)** @@ -120,6 +124,9 @@ JVM 这部分内容主要参考 [JVM 虚拟机规范-Java8 ](https://docs.oracle - [Java 18 新特性概览](./docs/java/new-features/java18.md) - [Java 19 新特性概览](./docs/java/new-features/java19.md) - [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) ## 计算机基础 @@ -175,8 +182,8 @@ JVM 这部分内容主要参考 [JVM 虚拟机规范-Java8 ](https://docs.oracle **常见算法问题总结**: -- [几道常见的字符串算法题总结 ](./docs/cs-basics/algorithms/string-algorithm-problems.md) -- [几道常见的链表算法题总结 ](./docs/cs-basics/algorithms/linkedlist-algorithm-problems.md) +- [几道常见的字符串算法题总结](./docs/cs-basics/algorithms/string-algorithm-problems.md) +- [几道常见的链表算法题总结](./docs/cs-basics/algorithms/linkedlist-algorithm-problems.md) - [剑指 offer 部分编程题](./docs/cs-basics/algorithms/the-sword-refers-to-offer.md) - [十大经典排序算法](./docs/cs-basics/algorithms/10-classical-sorting-algorithms.md) @@ -245,7 +252,8 @@ JVM 这部分内容主要参考 [JVM 虚拟机规范-Java8 ](https://docs.oracle ### Maven -[Maven 核心概念总结](./docs/tools/maven/maven-core-concepts.md) +- [Maven 核心概念总结](./docs/tools/maven/maven-core-concepts.md) +- [Maven 最佳实践](./docs/tools/maven/maven-best-practices.md) ### Gradle @@ -287,6 +295,7 @@ JVM 这部分内容主要参考 [JVM 虚拟机规范-Java8 ](https://docs.oracle **重要知识点详解**: +- [IoC & AOP详解(快速搞懂)](./docs/system-design/framework/spring/ioc-and-aop.md) - [Spring 事务详解](./docs/system-design/framework/spring/spring-transaction.md) - [Spring 中的设计模式详解](./docs/system-design/framework/spring/spring-design-patterns-summary.md) - [SpringBoot 自动装配原理详解](./docs/system-design/framework/spring/spring-boot-auto-assembly-principles.md) @@ -305,13 +314,12 @@ JVM 这部分内容主要参考 [JVM 虚拟机规范-Java8 ](https://docs.oracle - [SSO 单点登录详解](./docs/system-design/security/sso-intro.md) - [权限系统设计详解](./docs/system-design/security/design-of-authority-system.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) ### 定时任务 @@ -349,12 +357,13 @@ JVM 这部分内容主要参考 [JVM 虚拟机规范-Java8 ](https://docs.oracle ### 分布式 ID -- [分布式 ID 常见知识点&面试题总结](https://javaguide.cn/distributed-system/distributed-id.html) +- [分布式ID介绍&实现方案总结](https://javaguide.cn/distributed-system/distributed-id.html) - [分布式 ID 设计指南](https://javaguide.cn/distributed-system/distributed-id-design.html) ### 分布式锁 -[分布式锁常见知识点&面试题总结](https://javaguide.cn/distributed-system/distributed-lock.html) +- [分布式锁介绍](https://javaguide.cn/distributed-system/distributed-lock.html) +- [分布式锁常见实现方案总结](https://javaguide.cn/distributed-system/distributed-lock-implementations.html) ### 分布式事务 @@ -366,18 +375,17 @@ JVM 这部分内容主要参考 [JVM 虚拟机规范-Java8 ](https://docs.oracle ## 高性能 -### 数据库读写分离&分库分表 +### 数据库优化 -[数据库读写分离和分库分表常见知识点&面试题总结](./docs/high-performance/read-and-write-separation-and-library-subtable.md) +- [数据库读写分离和分库分表](./docs/high-performance/read-and-write-separation-and-library-subtable.md) +- [数据冷热分离](./docs/high-performance/data-cold-hot-separation.md) +- [常见 SQL 优化手段总结](./docs/high-performance/sql-optimization.md) +- [深度分页介绍及优化建议](./docs/high-performance/deep-pagination-optimization.md) ### 负载均衡 [负载均衡常见知识点&面试题总结](./docs/high-performance/load-balancing.md) -### SQL 优化 - -[常见 SQL 优化手段总结](./docs/high-performance/sql-optimization.md) - ### CDN [CDN(内容分发网络)常见知识点&面试题总结](./docs/high-performance/cdn.md) diff --git a/docs/.vuepress/config.ts b/docs/.vuepress/config.ts index e15c9b107e7..eed17cf0e1d 100644 --- a/docs/.vuepress/config.ts +++ b/docs/.vuepress/config.ts @@ -1,14 +1,14 @@ +import { viteBundler } from "@vuepress/bundler-vite"; import { defineUserConfig } from "vuepress"; -import { searchProPlugin } from "vuepress-plugin-search-pro"; - import theme from "./theme.js"; export default defineUserConfig({ dest: "./dist", - title: "JavaGuide(Java面试 + 学习指南)", + title: "JavaGuide", description: - "「Java学习指北 + Java面试指南」一份涵盖大部分 Java 程序员所需要掌握的核心知识。准备 Java 面试,复习 Java 知识点,首选 JavaGuide! ", + "「Java 学习指北 + Java 面试指南」一份涵盖大部分 Java 程序员所需要掌握的核心知识。准备 Java 面试,复习 Java 知识点,首选 JavaGuide! ", + lang: "zh-CN", head: [ // meta @@ -31,6 +31,14 @@ export default defineUserConfig({ "Java基础, 多线程, JVM, 虚拟机, 数据库, MySQL, Spring, Redis, MyBatis, 系统设计, 分布式, RPC, 高可用, 高并发", }, ], + [ + "meta", + { + name: "description", + content: + "「Java学习 + 面试指南」一份涵盖大部分 Java 程序员所需要掌握的核心知识。准备 Java 面试,首选 JavaGuide!", + }, + ], ["meta", { name: "apple-mobile-web-app-capable", content: "yes" }], // 添加百度统计 [ @@ -44,20 +52,14 @@ export default defineUserConfig({ s.parentNode.insertBefore(hm, s); })();`, ], - ["link", { rel: "icon", href: "/favicon.ico" }], ], - locales: { - "/": { - lang: "zh-CN", - }, - }, + bundler: viteBundler(), theme, - plugins: [searchProPlugin({ indexContent: true })], - pagePatterns: ["**/*.md", "!**/*.snippet.md", "!.vuepress", "!node_modules"], shouldPrefetch: false, + shouldPreload: false, }); diff --git a/docs/.vuepress/navbar.ts b/docs/.vuepress/navbar.ts index e579f5e70fb..88d85c94049 100644 --- a/docs/.vuepress/navbar.ts +++ b/docs/.vuepress/navbar.ts @@ -2,11 +2,6 @@ import { navbar } from "vuepress-theme-hope"; export default navbar([ { text: "面试指南", icon: "java", link: "/home.md" }, - { - text: "知识星球", - icon: "planet", - link: "/about-the-author/zhishixingqiu-two-years.md", - }, { text: "开源项目", icon: "github", link: "/open-source-project/" }, { text: "技术书籍", icon: "book", link: "/books/" }, { @@ -14,6 +9,27 @@ export default navbar([ icon: "article", link: "/high-quality-technical-articles/", }, + { + text: "知识星球", + icon: "planet", + children: [ + { + text: "星球介绍", + icon: "about", + link: "/about-the-author/zhishixingqiu-two-years.md", + }, + { + text: "星球专属优质专栏", + icon: "about", + link: "/zhuanlan/", + }, + { + text: "星球优质主题汇总", + icon: "star", + link: "https://www.yuque.com/snailclimb/rpkqw1/ncxpnfmlng08wlf1", + }, + ], + }, { text: "网站相关", icon: "about", 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/.vuepress/sidebar/high-quality-technical-articles.ts b/docs/.vuepress/sidebar/high-quality-technical-articles.ts index 15b58b8cbc7..8da4200b7e1 100644 --- a/docs/.vuepress/sidebar/high-quality-technical-articles.ts +++ b/docs/.vuepress/sidebar/high-quality-technical-articles.ts @@ -7,10 +7,13 @@ export const highQualityTechnicalArticles = arraySidebar([ prefix: "advanced-programmer/", collapsible: false, children: [ + "programmer-quickly-learn-new-technology", "the-growth-strategy-of-the-technological-giant", "ten-years-of-dachang-growth-road", + "meituan-three-year-summary-lesson-10", "seven-tips-for-becoming-an-advanced-programmer", "20-bad-habits-of-bad-programmers", + "thinking-about-technology-and-business-after-five-years-of-work", ], }, { @@ -31,6 +34,7 @@ export const highQualityTechnicalArticles = arraySidebar([ prefix: "programmer/", collapsible: false, children: [ + "high-value-certifications-for-programmers", "how-do-programmers-publish-a-technical-book", "efficient-book-publishing-and-practice-guide", ], diff --git a/docs/.vuepress/sidebar/index.ts b/docs/.vuepress/sidebar/index.ts index 5cb00567923..6a3c73769d4 100644 --- a/docs/.vuepress/sidebar/index.ts +++ b/docs/.vuepress/sidebar/index.ts @@ -13,20 +13,21 @@ export default sidebar({ "/high-quality-technical-articles/": highQualityTechnicalArticles, "/zhuanlan/": [ "java-mian-shi-zhi-bei", + "back-end-interview-high-frequency-system-design-and-scenario-questions", "handwritten-rpc-framework", "source-code-reading", ], // 必须放在最后面 "/": [ { - text: "必看", + text: "项目介绍", icon: "star", collapsible: true, prefix: "javaguide/", - children: ["intro", "contribution-guideline", "faq"], + children: ["intro", "use-suggestion", "contribution-guideline", "faq"], }, { - text: "面试准备", + text: "面试准备(必看)", icon: "interview", collapsible: true, prefix: "interview-preparation/", @@ -34,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", ], }, { @@ -85,8 +87,14 @@ export default sidebar({ collapsible: true, children: [ "arraylist-source-code", + "linkedlist-source-code", "hashmap-source-code", "concurrent-hash-map-source-code", + "linkedhashmap-source-code", + "copyonwritearraylist-source-code", + "arrayblockingqueue-source-code", + "priorityqueue-source-code", + "delayqueue-source-code", ], }, ], @@ -105,6 +113,7 @@ export default sidebar({ collapsible: true, children: [ "optimistic-lock-and-pessimistic-lock", + "cas", "jmm", "java-thread-pool-summary", "java-thread-pool-best-practices", @@ -113,6 +122,7 @@ export default sidebar({ "atomic-classes", "threadlocal", "completablefuture-intro", + "virtual-thread", ], }, ], @@ -122,7 +132,7 @@ export default sidebar({ prefix: "io/", icon: "code", collapsible: true, - children: ["io-basis", "io-design-patterns", "io-model"], + children: ["io-basis", "io-design-patterns", "io-model", "nio-basis"], }, { text: "JVM", @@ -158,6 +168,9 @@ export default sidebar({ "java18", "java19", "java20", + "java21", + "java22-23", + "java24", ], }, ], @@ -175,13 +188,14 @@ export default sidebar({ children: [ "other-network-questions", "other-network-questions2", - "computer-network-xiexiren-summary", + // "computer-network-xiexiren-summary", { text: "重要知识点", icon: "star", collapsible: true, children: [ "osi-and-tcp-ip-model", + "the-whole-process-of-accessing-web-pages", "application-layer-protocol", "http-vs-https", "http1.0-vs-http1.1", @@ -231,6 +245,8 @@ export default sidebar({ icon: "suanfaku", collapsible: true, children: [ + "classical-algorithm-problems-recommendations", + "common-data-structures-leetcode-recommendations", "string-algorithm-problems", "linkedlist-algorithm-problems", "the-sword-refers-to-offer", @@ -257,7 +273,14 @@ export default sidebar({ icon: "SQL", prefix: "sql/", collapsible: true, - children: ["sql-syntax-summary", "sql-questions-01"], + children: [ + "sql-syntax-summary", + "sql-questions-01", + "sql-questions-02", + "sql-questions-03", + "sql-questions-04", + "sql-questions-05", + ], }, ], }, @@ -303,9 +326,11 @@ export default sidebar({ icon: "star", collapsible: true, children: [ + "redis-delayed-task", "3-commonly-used-cache-read-and-write-strategies", "redis-data-structures-01", "redis-data-structures-02", + "redis-skiplist", "redis-persistence", "redis-memory-fragmentation", "redis-common-blocking-problems-summary", @@ -340,7 +365,7 @@ export default sidebar({ text: "Maven", icon: "configuration", prefix: "maven/", - children: ["maven-core-concepts"], + children: ["maven-core-concepts", "maven-best-practices"], }, { text: "Gradle", @@ -381,14 +406,17 @@ export default sidebar({ "spring-knowledge-and-questions-summary", "springboot-knowledge-and-questions-summary", "spring-common-annotations", + "springboot-source-code", { text: "重要知识点", icon: "star", collapsible: true, children: [ + "ioc-and-aop", "spring-transaction", "spring-design-patterns-summary", "spring-boot-auto-assembly-principles", + "async", ], }, ], @@ -404,9 +432,10 @@ export default sidebar({ collapsible: true, children: [ { - text: "基础", + text: "基础知识", prefix: "basis/", icon: "basic", + collapsible: true, children: [ "RESTfulAPI", "software-engineering", @@ -419,7 +448,7 @@ export default sidebar({ ], }, { - text: "安全", + text: "认证授权", prefix: "security/", icon: "security-fill", collapsible: true, @@ -429,8 +458,18 @@ export default sidebar({ "advantages-and-disadvantages-of-jwt", "sso-intro", "design-of-authority-system", + ], + }, + { + text: "数据安全", + prefix: "security/", + icon: "security-fill", + collapsible: true, + children: [ + "encryption-algorithms", "sentive-words-filter", "data-desensitization", + "data-validation", ], }, "system-design-questions", @@ -470,7 +509,17 @@ export default sidebar({ { text: "分布式锁", icon: "lock", - children: ["distributed-lock"], + children: ["distributed-lock", "distributed-lock-implementations"], + }, + { + text: "分布式事务", + icon: "transanction", + children: ["distributed-transaction"], + }, + { + text: "分布式配置中心", + icon: "configuration", + children: ["distributed-configuration-center"], }, { text: "RPC", @@ -486,18 +535,6 @@ export default sidebar({ collapsible: true, children: ["zookeeper-intro", "zookeeper-plus"], }, - { - text: "分布式事务", - icon: "transanction", - collapsible: true, - children: ["distributed-transaction"], - }, - { - text: "分布式配置中心", - icon: "configuration", - collapsible: true, - children: ["distributed-configuration-center"], - }, ], }, { @@ -521,7 +558,9 @@ export default sidebar({ icon: "mysql", children: [ "read-and-write-separation-and-library-subtable", + "data-cold-hot-separation", "sql-optimization", + "deep-pagination-optimization", ], }, { @@ -546,6 +585,7 @@ export default sidebar({ collapsible: true, children: [ "high-availability-system-design", + "idempotency", "redundancy", "limit-request", "fallback-and-circuit-breaker", diff --git a/docs/.vuepress/styles/config.scss b/docs/.vuepress/styles/config.scss new file mode 100644 index 00000000000..9c8419c3c05 --- /dev/null +++ b/docs/.vuepress/styles/config.scss @@ -0,0 +1 @@ +$theme-color: #2980b9; diff --git a/docs/.vuepress/styles/palette.scss b/docs/.vuepress/styles/palette.scss index fe23e2c0311..de19553fc89 100644 --- a/docs/.vuepress/styles/palette.scss +++ b/docs/.vuepress/styles/palette.scss @@ -1,3 +1,4 @@ -$theme-color: #2980b9; $sidebar-width: 20rem; $sidebar-mobile-width: 16rem; +$vp-font: 'Georgia, -apple-system, "Nimbus Roman No9 L", "PingFang SC", "Hiragino Sans GB", "Noto Serif SC", "Microsoft Yahei", "WenQuanYi Micro Hei", sans-serif'; +$vp-font-heading: 'Georgia, -apple-system, "Nimbus Roman No9 L", "PingFang SC", "Hiragino Sans GB", "Noto Serif SC", "Microsoft Yahei", "WenQuanYi Micro Hei", sans-serif'; diff --git a/docs/.vuepress/theme.ts b/docs/.vuepress/theme.ts index 195dd214ec2..14f04ed6d49 100644 --- a/docs/.vuepress/theme.ts +++ b/docs/.vuepress/theme.ts @@ -1,4 +1,4 @@ -import { getDirname, path } from "@vuepress/utils"; +import { getDirname, path } from "vuepress/utils"; import { hopeTheme } from "vuepress-theme-hope"; import navbar from "./navbar.js"; @@ -7,10 +7,9 @@ import sidebar from "./sidebar/index.js"; const __dirname = getDirname(import.meta.url); export default hopeTheme({ - logo: "/logo.png", hostname: "https://javaguide.cn/", - - iconAssets: "//at.alicdn.com/t/c/font_2922463_kweia6fbo9.css", + logo: "/logo.png", + favicon: "/favicon.ico", author: { name: "Guide", @@ -19,8 +18,8 @@ export default hopeTheme({ repo: "https://github.com/Snailclimb/JavaGuide", docsDir: "docs", - // 纯净模式:https://theme-hope.vuejs.press/zh/guide/interface/pure.html pure: true, + focus: false, breadcrumb: false, navbar, sidebar, @@ -28,15 +27,7 @@ export default hopeTheme({ '鄂ICP备2020015769号-1', displayFooter: true, - pageInfo: [ - "Author", - "Category", - "Tag", - "Date", - "Original", - "Word", - "ReadingTime", - ], + pageInfo: ["Author", "Category", "Tag", "Original", "Word", "ReadingTime"], blog: { intro: "/about-the-author/", @@ -48,32 +39,50 @@ export default hopeTheme({ }, }, + markdown: { + align: true, + codeTabs: true, + gfm: true, + include: { + resolvePath: (file, cwd) => { + if (file.startsWith("@")) + return path.resolve( + __dirname, + "../snippets", + file.replace("@", "./"), + ); + + return path.resolve(cwd, file); + }, + }, + tasklist: true, + }, + plugins: { blog: true, - copyright: true, - mdEnhance: { - align: true, - codetabs: true, - container: true, - figure: true, - include: { - resolvePath: (file, cwd) => { - if (file.startsWith("@")) - return path.resolve( - __dirname, - "../snippets", - file.replace("@", "./") - ); - return path.resolve(cwd, file); - }, - }, - tasklist: true, + copyright: { + author: "JavaGuide(javaguide.cn)", + license: "MIT", + triggerLength: 100, + maxLength: 700, + canonical: "https://javaguide.cn/", + global: true, }, + feed: { atom: true, json: true, rss: true, }, + + icon: { + assets: "//at.alicdn.com/t/c/font_2922463_o9q9dxmps9.css", + }, + + search: { + isSearchable: (page) => page.path !== "/", + maxSuggestions: 10, + }, }, }); diff --git a/docs/readme.md b/docs/README.md similarity index 61% rename from docs/readme.md rename to docs/README.md index a40752c0f3e..ed6cbec001c 100644 --- a/docs/readme.md +++ b/docs/README.md @@ -4,10 +4,10 @@ icon: home title: Java 面试指南 heroImage: /logo.svg heroText: JavaGuide -tagline: 「Java学习 + 面试指南」一份涵盖大部分 Java 程序员所需要掌握的核心知识。准备 Java 面试,首选 JavaGuide! +tagline: 「Java学习 + 面试指南」涵盖 Java 程序员需要掌握的核心知识 actions: - text: 开始阅读 - link: /home/ + link: /home.md type: primary - text: 知识星球 link: /about-the-author/zhishixingqiu-two-years.md @@ -16,10 +16,12 @@ footer: |- 鄂ICP备2020015769号-1 | 主题: VuePress Theme Hope --- -[![Banner](https://oss.javaguide.cn/xingqiu/xingqiu.png)](https://javaguide.cn/about-the-author/zhishixingqiu-two-years.html) - ## 关于网站 +JavaGuide 已经持续维护 6 年多了,累计提交了 **5600+** commit ,共有 **550+** 多位贡献者共同参与维护和完善。真心希望能够把这个项目做好,真正能够帮助到有需要的朋友! + +如果觉得 JavaGuide 的内容对你有帮助的话,还请点个免费的 Star(绝不强制点 Star,觉得内容不错有收获再点赞就好),这是对我最大的鼓励,感谢各位一路同行,共勉!传送门:[GitHub](https://github.com/Snailclimb/JavaGuide) | [Gitee](https://gitee.com/SnailClimb/JavaGuide)。 + - [项目介绍](./javaguide/intro.md) - [贡献指南](./javaguide/contribution-guideline.md) - [常见问题](./javaguide/faq.md) @@ -31,14 +33,6 @@ footer: |- - [我的知识星球快 3 岁了!](./about-the-author/zhishixingqiu-two-years.md) - [坚持写技术博客六年了](./about-the-author/writing-technology-blog-six-years.md) -## 知识星球 - -对于准备面试的同学来说,强烈推荐我创建的一个纯粹的[Java 面试知识星球](./about-the-author/zhishixingqiu-two-years.md),干货非常多,学习氛围也很不错! - -下面是星球提供的部分服务(点击下方图片即可获取知识星球的详细介绍): - -[![星球服务](https://oss.javaguide.cn/xingqiu/xingqiufuwu.png)](./about-the-author/zhishixingqiu-two-years.md) - ## 公众号 最新更新会第一时间同步在公众号,推荐关注!另外,公众号上有很多干货不会同步在线阅读网站。 diff --git a/docs/about-the-author/readme.md b/docs/about-the-author/README.md similarity index 90% rename from docs/about-the-author/readme.md rename to docs/about-the-author/README.md index a16e882c1cf..12f6eab7f3f 100644 --- a/docs/about-the-author/readme.md +++ b/docs/about-the-author/README.md @@ -33,7 +33,7 @@ category: 走近作者 下面这张是我大一下学期办补习班的时候拍的(离开前的最后一顿饭): -![补习班的最后一顿晚餐](https://p3-juejin.byteimg.com/tos-cn-i-k3u1fbpfcp/f36bfd719b9b4463b2f1d3edc51faa97~tplv-k3u1fbpfcp-zoom-1.image) +![补习班的最后一顿晚餐](https://oss.javaguide.cn/p3-juejin/f36bfd719b9b4463b2f1d3edc51faa97~tplv-k3u1fbpfcp-zoom-1.jpeg) 下面这张是我大三去三亚的时候拍的: @@ -45,7 +45,11 @@ category: 走近作者 如果你也想通过接私活变现的话,可以在我的公众号后台回复“**接私活**”来了解一些我的个人经验分享。 -![](https://my-blog-to-use.oss-cn-beijing.aliyuncs.com/2020-8/1d38ea3b-da2a-41df-9ac4-087356e9b5b4-20200802185910087.png) +::: center + +![](https://oss.javaguide.cn/github/javaguide/gongzhonghaoxuanchuan.png) + +::: ## 为什么自称 Guide? @@ -55,7 +59,7 @@ category: 走近作者 我早期写文章用的笔名是 SnailClimb 。很多人不知道这个名字是啥意思,给大家拆解一下就清楚了。SnailClimb=Snail(蜗牛)+Climb(攀登)。我从小就非常喜欢听周杰伦的歌曲,特别是他的《蜗牛》🐌 这首歌曲,另外,当年我高考发挥的算是比较失常,上了大学之后还算是比较“奋青”,所以,我就给自己起的笔名叫做 SnailClimb ,寓意自己要不断向上攀登,嘿嘿 😁 -![](https://p3-juejin.byteimg.com/tos-cn-i-k3u1fbpfcp/37599546f3b34b92a32db579a225aa45~tplv-k3u1fbpfcp-watermark.image) +![](https://oss.javaguide.cn/p3-juejin/37599546f3b34b92a32db579a225aa45~tplv-k3u1fbpfcp-watermark.png) ## 后记 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,招聘时也没被问过相关问题。 + +一定不要假设自己要面对的是过时技术栈的项目。你要找工作肯定要用主流技术栈去找,还要尽量找能让自己技术有成长,干着也舒服点。真要是找不到合适的工作,去维护老项目,那都是后话,现学现卖就行了。 + +**对于初学者来说别人劝了还非要学习淘汰的技术,多少脑子有点不够用,基本可以告别这一行了!** diff --git a/docs/about-the-author/dog-that-copies-other-people-essay.md b/docs/about-the-author/dog-that-copies-other-people-essay.md index cf849132759..653b616eaab 100644 --- a/docs/about-the-author/dog-that-copies-other-people-essay.md +++ b/docs/about-the-author/dog-that-copies-other-people-essay.md @@ -9,23 +9,23 @@ tag: 听朋友说我的文章在知乎又被盗了,原封不动地被别人用来引流。 -![](https://p3-juejin.byteimg.com/tos-cn-i-k3u1fbpfcp/39f223bd8d8240b8b7328f7ab6edbc57~tplv-k3u1fbpfcp-zoom-1.image) +![](https://oss.javaguide.cn/p3-juejin/39f223bd8d8240b8b7328f7ab6edbc57~tplv-k3u1fbpfcp-zoom-1.png) 而且!!!这还不是最气的。 这人还在文末注明的原出处还不是我的。。。 -![](https://p3-juejin.byteimg.com/tos-cn-i-k3u1fbpfcp/fa47e0752f4b4b57af424114bc6bc558~tplv-k3u1fbpfcp-zoom-1.image) +![](https://oss.javaguide.cn/p3-juejin/fa47e0752f4b4b57af424114bc6bc558~tplv-k3u1fbpfcp-zoom-1.png) 也就是说 CSDN 有另外一位抄袭狗盗了我的这篇文章并声明了原创,知乎抄袭狗又原封不动地搬运了这位 CSDN 抄袭狗的文章。 真可谓离谱他妈给离谱开门,离谱到家了。 -![](https://p3-juejin.byteimg.com/tos-cn-i-k3u1fbpfcp/6f8d281579224b13ad235c28e1d7790e~tplv-k3u1fbpfcp-zoom-1.image) +![](https://oss.javaguide.cn/p3-juejin/6f8d281579224b13ad235c28e1d7790e~tplv-k3u1fbpfcp-zoom-1.png) 我打开知乎抄袭狗注明的原出处链接,好家伙,一模一样的内容,还表明了原创。 -![](https://p3-juejin.byteimg.com/tos-cn-i-k3u1fbpfcp/6a6d7b206b6a43ec9b0055a8f47a30be~tplv-k3u1fbpfcp-zoom-1.image) +![](https://oss.javaguide.cn/p3-juejin/6a6d7b206b6a43ec9b0055a8f47a30be~tplv-k3u1fbpfcp-zoom-1.png) 看了一下 CSDN 这位抄袭狗的文章,好家伙,把我高赞回答搬运了一个遍。。。真是很勤奋了。。。 @@ -33,7 +33,7 @@ CSDN 我就不想多说了,就一大型文章垃圾场,都是各种不规范 像我自己平时用 Google 搜索的时候,都是直接屏蔽掉 CSDN 这个站点的。只需要下载一个叫做 Personal Blocklist 的 Chrome 插件,然后将 blog.csdn.net 添加进黑名单就可以了。 -![](https://p3-juejin.byteimg.com/tos-cn-i-k3u1fbpfcp/be151d93cd024c6e911d1a694212d91c~tplv-k3u1fbpfcp-zoom-1.image) +![](https://oss.javaguide.cn/p3-juejin/be151d93cd024c6e911d1a694212d91c~tplv-k3u1fbpfcp-zoom-1.png) 我的文章基本被盗完了,关键是我自己发没有什么流量,反而是盗我文章的那些人比我这个原作者流量还大。 @@ -43,7 +43,7 @@ CSDN 我就不想多说了,就一大型文章垃圾场,都是各种不规范 看看 CSDN 热榜上的文章都是一些什么垃圾,不是各种广告就是一些毫无质量的拼凑文。 -![](https://p3-juejin.byteimg.com/tos-cn-i-k3u1fbpfcp/cd07efe86af74ea0a07d29236718ddc8~tplv-k3u1fbpfcp-zoom-1.image) +![](https://oss.javaguide.cn/p3-juejin/cd07efe86af74ea0a07d29236718ddc8~tplv-k3u1fbpfcp-zoom-1-20230717155426403.png) 当然了,也有极少部分的高质量文章,比如涛哥、二哥、冰河、微观技术等博主的文章。 @@ -51,6 +51,6 @@ CSDN 我就不想多说了,就一大型文章垃圾场,都是各种不规范 今天提到的这篇被盗的文章曾经就被一个培训机构拿去做成了视频用来引流。 -![](https://p3-juejin.byteimg.com/tos-cn-i-k3u1fbpfcp/9dda1e36ceff4cbb9b0bf9501b279be5~tplv-k3u1fbpfcp-zoom-1.image) +![](https://oss.javaguide.cn/p3-juejin/9dda1e36ceff4cbb9b0bf9501b279be5~tplv-k3u1fbpfcp-zoom-1.png) 作为个体,咱也没啥办法,只能遇到一个举报一个。。。 diff --git a/docs/about-the-author/feelings-after-one-month-of-induction-training.md b/docs/about-the-author/feelings-after-one-month-of-induction-training.md index 8ecb6eac455..ed57578a907 100644 --- a/docs/about-the-author/feelings-after-one-month-of-induction-training.md +++ b/docs/about-the-author/feelings-after-one-month-of-induction-training.md @@ -15,7 +15,7 @@ ThoughtWorks 非常提倡分享、提倡帮助他人成长,这一点在公司 另外,ThoughtWorks 也是一家非常提倡 Feedback (反馈) 文化的公司,反馈是告诉人们我们对他们的表现的看法以及他们应该如何更好地做到这一点。刚开始我并没有太在意,慢慢地自己确实感觉到正确的进行反馈对他人会有很大的帮助。因为人在做很多事情的时候,会很难发现别人很容易看到的一些小问题。就比如一个很有趣的现象一样,假如我们在做项目的时候没有测试这个角色,如果你完成了自己的模块,并且自己对这个模块测试了很多遍,你发现已经没啥问题了。但是,到了实际使用的时候会很大概率出现你之前从来没有注意的问题。解释这个问题的说法是:每个人的视野或多或少都是有盲点的,这与我们的关注点息息相关。对于自己做的东西,很多地方自己测试很多遍都不会发现,但是如果让其他人帮你进行测试的话,就很大可能会发现很多显而易见的问题。 -![](https://my-blog-to-use.oss-cn-beijing.aliyuncs.com/2019-7/feedback.png) +![](https://oss.javaguide.cn/github/about-the-author/feedback.png) 工作之后,平时更新公众号、专栏还有维护 Github 的时间变少了。实际上,很多时候下班回来后,都有自己的时间来干自己的事情,但是自己也总是找工作太累或者时间比较零散的接口来推掉了。到了今天,翻看 Github 突然发现 14 天前别人在 Github 上给我提的 PR 我还没有处理。这一点确实是自己没有做好的地方,没有合理安排好自己的时间。实际上自己有很多想写的东西,后面会慢慢将他们提上日程。工作之后,更加发现下班后的几个小时如何度过确实很重要 ,如果你觉得自己没有完成好自己白天该做的工作的话,下班后你可以继续忙白天没有忙完的工作,如果白天的工作对于你游刃有余的话,下班回来之后,你大可去干自己感兴趣的事情,学习自己感兴趣的技术。做任何事情都要基于自身的基础,切不可好高骛远。 diff --git a/docs/about-the-author/feelings-of-half-a-year-from-graduation-to-entry.md b/docs/about-the-author/feelings-of-half-a-year-from-graduation-to-entry.md index 327715f2b73..cc9fe136749 100644 --- a/docs/about-the-author/feelings-of-half-a-year-from-graduation-to-entry.md +++ b/docs/about-the-author/feelings-of-half-a-year-from-graduation-to-entry.md @@ -46,10 +46,8 @@ tag: 还没完成的: 1. Kafka 高级特性比如工作流程、Kafka 为什么快等等的分析; - 2. 源码阅读分析; - -3. ...... +3. …… **所以,我觉得技术的积累和沉淀很大程度在乎工作之外的时间(大佬和一些本身就特别厉害的除外)。** diff --git a/docs/about-the-author/internet-addiction-teenager.md b/docs/about-the-author/internet-addiction-teenager.md index d4a34b8fc27..78f94e2a483 100644 --- a/docs/about-the-author/internet-addiction-teenager.md +++ b/docs/about-the-author/internet-addiction-teenager.md @@ -5,7 +5,7 @@ tag: - 个人经历 --- -> 这篇文章写入 2021 年高考前夕。 +> 这篇文章写于 2021 年高考前夕。 聊到高考,无数人都似乎有很多话说。今天就假借高考的名义,简单来聊聊我的高中求学经历吧! @@ -19,7 +19,11 @@ tag: 最开始接触电脑是在我刚上五年级的时候,那时候家里没电脑,刚开始上网都是在黑网吧玩的。 -在黑网吧上网的经历也是一波三折,经常会遇到警察来检查或者碰到大孩子骚扰。在黑网吧上网的一年多中,我一共两次碰到警察来检查,主要是看有没有未成年人(当时黑网吧里几乎全是未成年人),实际感觉像是要问黑网吧老板要点好处。碰到大孩子骚扰的次数就比较多,大孩子经常抢我电脑,还威胁我把身上所有的钱给他们。我当时一个人也比较怂,被打了几次之后,就尽量避开大孩子来玩的时间去黑网吧,身上也只带很少的钱。小时候的性格就比较独立,在外遇到事情我一般也不会给家里人说。 +黑网吧大概就是下面这样式儿的,一个没有窗户的房间里放了很多台老式电脑,非常拥挤。 + +![黑网吧](https://oss.javaguide.cn/about-the-author/internet-addiction-teenager/heiwangba.png) + +在黑网吧上网的经历也是一波三折,经常会遇到警察来检查或者碰到大孩子骚扰。在黑网吧上网的一年多中,我一共两次碰到警察来检查,主要是看有没有未成年人(当时黑网吧里几乎全是未成年人),实际感觉像是要问黑网吧老板要点好处。碰到大孩子骚扰的次数就比较多,大孩子经常抢我电脑,还威胁我把身上所有的钱给他们。我当时一个人也比较怂,被打了几次之后,就尽量避开大孩子来玩的时间去黑网吧,身上也只带很少的钱。小时候的性格就比较独立,在外遇到事情我一般也不会给家里人说(因为说了也没什么用,家人给我的安全感很少)。 我现在已经记不太清当时是被我哥还是我姐带进网吧的,好像是我姐。 @@ -57,6 +61,10 @@ QQ 飞车这款戏当时还挺火的,很多 90 后的小伙伴应该比较熟 我的最终军衔停留在了两个钻石,玩过的小伙伴应该清楚这在当时要玩多少把(现在升级比较简单)。 +![](https://oss.javaguide.cn/about-the-author/cf.png) + +ps: 回坑 CF 快一年了,目前的军衔是到了两颗星中校 3 了。 + 那时候成绩挺差的。这样说吧!我当时在很普通的一个县级市的高中,全年级有 500 来人,我基本都是在 280 名左右。而且,整个初二我都没有学物理,上物理课就睡觉,考试就交白卷。 为什么对物理这么抵触呢?这是因为开学不久的一次物理课,物理老师误会我在上课吃东西还狡辩,扇了我一巴掌。那时候心里一直记仇到大学,想着以后自己早晚有时间把这个物理老师暴打一顿。 @@ -81,6 +89,8 @@ QQ 飞车这款戏当时还挺火的,很多 90 后的小伙伴应该比较熟 ## 高中从小班掉到平行班 +![出高考成绩后回高中母校拍摄](https://oss.javaguide.cn/about-the-author/internet-addiction-teenager/wodegaozhong.png) + 由于参加了高中提前招生考试,我提前 4 个月就来到了高中,进入了小班,开始学习高中的课程。 上了高中的之后,我上课就偷偷看小说,神印王座、斗罗大陆、斗破苍穹很多小说都是当时看的。中午和晚上回家之后,就在家里玩几把 DNF。当时家里也买了电脑,姥爷给买的,是对自己顺利进入二中的奖励。到我卸载 DNF 的时候,已经练了 4 个满级的号,两个接近满级的号。 @@ -117,9 +127,11 @@ QQ 飞车这款戏当时还挺火的,很多 90 后的小伙伴应该比较熟 ![](https://oss.javaguide.cn/about-the-author/internet-addiction-teenager/image-20220625194714247.png) -高考那几天的失眠,我觉得可能和我喝了老师推荐的安神补脑液有关系,又或者是我自己太过于紧张了。因为那几天睡觉总会感觉有很多蚂蚁在身上爬一样,身上还起了一些小痘痘。 +高考那几天的失眠,我觉得可能和我喝了老师推荐的安神补脑液有关系,又或者是我自己太过于紧张了。因为那几天睡觉总会感觉有很多蚂蚁在身上爬一样,身上还起了一些小痘痘(有点像是过敏)。 + +这里要格外说明一点,避免引起误导:**睡不着本身就是自身的问题,上述言论并没有责怪这个补脑液的意思。** 另外, 这款安神补脑液我去各个平台都查了一下,发现大家对他的评价都挺好,和我们老师当时推荐的理由差不多。如果大家需要改善睡眠的话,可以咨询相关医生之后尝试一下。 -然后,这里要格外说明一点,避免引起误导:**睡不着本身就是自身的问题,上述言论并没有责怪这个补脑液的意思。** 另外, 这款安神补脑液我去各个平台都查了一下,发现大家对他的评价都挺好,和我们老师当时推荐的理由差不多。如果大家需要改善睡眠的话,可以咨询相关医生之后尝试一下。 +高考也确实没发挥好,整个人在考场都是懵的状态。高考成绩出来之后,比我自己预估的还低了几十分,最后只上了一个双非一本。不过,好在专业选的好,吃了一些计算机专业的红利,大学期间也挺努力的。 ## 大学生活 diff --git a/docs/about-the-author/my-article-was-stolen-and-made-into-video-and-it-became-popular.md b/docs/about-the-author/my-article-was-stolen-and-made-into-video-and-it-became-popular.md index 91201ccec76..2fa306d2fe9 100644 --- a/docs/about-the-author/my-article-was-stolen-and-made-into-video-and-it-became-popular.md +++ b/docs/about-the-author/my-article-was-stolen-and-made-into-video-and-it-became-popular.md @@ -15,19 +15,19 @@ tag: 麻烦这个培训机构看到这篇文章之后可以考虑换一个人做类似恶心的事情哈!这人完全没脑子啊! -![](https://oscimg.oschina.net/oscnet/up-db6b9cf323930786fa2bec8b1e1bfaad732.png) +![](https://oss.javaguide.cn/github/javaguide/about-the-author/up-db6b9cf323930786fa2bec8b1e1bfaad732.png) -![](https://oscimg.oschina.net/oscnet/up-6395603ab441b74511c6eda28efee8937d7.png) +![](https://oss.javaguide.cn/github/javaguide/about-the-author/up-6395603ab441b74511c6eda28efee8937d7.png) -![](https://oscimg.oschina.net/oscnet/up-921f60a5c7cee2c5c2eb30f4f7048f648e1.png) +![](https://oss.javaguide.cn/github/javaguide/about-the-author/up-921f60a5c7cee2c5c2eb30f4f7048f648e1.png) -![](https://oscimg.oschina.net/oscnet/up-acc82a797bd01e27f5b7d5d327b32a21d4e.png) +![](https://oss.javaguide.cn/github/javaguide/about-the-author/up-acc82a797bd01e27f5b7d5d327b32a21d4e.png) 我随便找了一个视频看,发现也还是盗用别人的原创。 -![](https://oscimg.oschina.net/oscnet/up-48d0c5ab086265ae19b7396bc59de2c2daf.png) +![](https://oss.javaguide.cn/github/javaguide/about-the-author/up-48d0c5ab086265ae19b7396bc59de2c2daf.png) -![](https://oscimg.oschina.net/oscnet/up-366abf0656007ff96551064104e60740a41.png) +![](https://oss.javaguide.cn/github/javaguide/about-the-author/up-366abf0656007ff96551064104e60740a41.png) 其他的视频就不用多看了,是否还是剽窃别人的原创,原封不动地做成视频,大家心里应该有数。 @@ -49,7 +49,7 @@ tag: 谁能想到,培训机构的人竟然找人来让我删文章了!讲真,这俩人是真的奇葩啊! -![](https://oss.javaguide.cn/javaguide/8f8ccafcf5b764a2289a9c276c30728d.png) +![](https://oss.javaguide.cn/github/javaguide/about-the-author/8f8ccafcf5b764a2289a9c276c30728d.png) ![](https://oss.javaguide.cn/javaguide/a0a4a45d7ec7b1a2622b2a38629e9b09.png) diff --git a/docs/about-the-author/my-college-life.md b/docs/about-the-author/my-college-life.md index a24e899eea4..43d96bd4186 100644 --- a/docs/about-the-author/my-college-life.md +++ b/docs/about-the-author/my-college-life.md @@ -372,7 +372,7 @@ tag: 6. 管理自己的身材,没事去跑跑步,别当油腻男。 7. 别太看重绩点。我觉得绩点对于找工作还有考研实际的作用都可以忽略不计,不过不挂科还是比较重要的。但是,绩点确实在奖学金评选和保研名额选取上占有最大的分量。 8. 别太功利性。做事情以及学习知识都不要奢求它能立马带给你什么,坚持和功利往往是成反比的。 -9. ...... +9. …… ## 后记 diff --git a/docs/about-the-author/zhishixingqiu-two-years.md b/docs/about-the-author/zhishixingqiu-two-years.md index 9ca1fa7ddbd..644478b455a 100644 --- a/docs/about-the-author/zhishixingqiu-two-years.md +++ b/docs/about-the-author/zhishixingqiu-two-years.md @@ -1,18 +1,18 @@ --- -title: 我的知识星球快 3 岁了! +title: 我的知识星球 4 岁了! category: 知识星球 star: 2 --- -时间过的真快,知识星球我已经平稳运行了 3 年有余了! +在 **2019 年 12 月 29 号**,经过了大概一年左右的犹豫期,我正式确定要开始做一个自己的星球,帮助学习 Java 和准备 Java 面试的同学。一转眼,已经四年多了。感谢大家一路陪伴,我会信守承诺,继续认真维护这个纯粹的 Java 知识星球,不让信任我的读者失望。 -在 2019 年 12 月 29 号,经过了大概一年左右的犹豫期,我正式确定要开始做一个自己的星球。 +![](https://oss.javaguide.cn/xingqiu/640-20230727145252757.png) -![](https://oss.javaguide.cn/2021-1/%E7%9F%A5%E8%AF%86%E6%96%B0%E7%90%83%E4%B8%80%E5%91%A8%E5%B9%B4-0293.jpg) +我是比较早一批做星球的技术号主,也是坚持做下来的那一少部人(大部分博主割一波韭菜就不维护星球了)。最开始的一两年,纯粹靠爱发电。当初定价非常低(一顿饭钱),加上刚工作的时候比较忙,提供的服务也没有现在这么多。 -截止到今天,星球已经有 1.3w+ 的同学加入。虽然比不上很多大佬,但这于我来说也算是小有成就了,真的很满足了!我确信自己是一个普通人,能做成这些,也不过是在兴趣和运气的加持下赶上了时代而已。 +慢慢的价格提上来,星球的收入确实慢慢也上来了。不过,考虑到我的受众主要是学生,定价依然比同类星球低很多。另外,我也没有弄训练营的打算,虽然训练营对于我这个流量来说可以赚到更多钱。 **我有自己的原则,不割韭菜,用心做内容,真心希望帮助到他人!** @@ -26,18 +26,22 @@ star: 2 努力做一个最优质的 Java 面试交流星球!加入到我的星球之后,你将获得: -1. 6 个高质量的专栏永久阅读,内容涵盖面试,源码解析,项目实战等内容!价值远超门票! -2. 多本原创 PDF 版本面试手册。 -3. 免费的简历修改服务(已经累计帮助 4000+ 位球友修改简历)。 +1. 6 个高质量的专栏永久阅读,内容涵盖面试,源码解析,项目实战等内容! +2. 多本原创 PDF 版本面试手册免费领取。 +3. 免费的简历修改服务(已经累计帮助 7000+ 位球友修改简历)。 4. 一对一免费提问交流(专属建议,走心回答)。 -5. 专属求职指南和建议,帮助你逆袭大厂! -6. 海量 Java 优质面试资源分享!价值远超门票! -7. 读书交流,学习交流,让我们一起努力创造一个纯粹的学习交流社区。 +5. 专属求职指南和建议,让你少走弯路,效率翻倍! +6. 海量 Java 优质面试资源分享。 +7. 打卡活动,读书交流,学习交流,让学习不再孤单,报团取暖。 8. 不定期福利:节日抽奖、送书送课、球友线下聚会等等。 -9. ...... +9. …… 其中的任何一项服务单独拎出来价值都远超星球门票了。 +这里再送一个 **30** 元的星球专属优惠券吧,数量有限(价格即将上调。老用户续费半价 ,微信扫码即可续费)! + +![知识星球30元优惠卷](https://oss.javaguide.cn/xingqiu/xingqiuyouhuijuan-30.jpg) + ### 专属专栏 星球更新了 **《Java 面试指北》**、**《Java 必读源码系列》**(目前已经整理了 Dubbo 2.6.x、Netty 4.x、SpringBoot2.1 的源码)、 **《从零开始写一个 RPC 框架》**(已更新完)、**《Kafka 常见面试题/知识点总结》** 等多个优质专栏。 @@ -48,11 +52,13 @@ star: 2 ![](https://oss.javaguide.cn/xingqiu/image-20220304102536445.png) +进入星球之后,这些专栏即可免费永久阅读,永久同步更新! + ### PDF 面试手册 -免费赠送多本优质 PDF 面试手册。 +进入星球就免费赠送多本优质 PDF 面试手册。 -![](https://oss.javaguide.cn/xingqiu/image-20220723120918434.png) +![星球 PDF 面试手册](https://oss.javaguide.cn/xingqiu/image-20220723120918434.png) ### 优质精华主题沉淀 @@ -60,9 +66,15 @@ star: 2 ![](https://oss.javaguide.cn/xingqiu/image-20230421154518800.png) -![](https://oss.javaguide.cn/xingqiu/Xnip2023-04-21_15-48-13.jpg) +并且,每个月都会整理出当月优质的主题,方便大家阅读学习,避免错过优质的内容。毫不夸张,单纯这些优质主题就足够门票价值了。 + +![星球每月优质主题整理概览](https://oss.javaguide.cn/xingqiu/image-20230902091117181.png) + +加入星球之后,一定要记得抽时间把星球精华主题看看,相信你一定会有所收货! -加入星球之后,记得抽时间把星球精华主题看看,相信你一定会有所收货! +JavaGuide 知识星球优质主题汇总传送门:(为了避免这里成为知识杂货铺,我会对严格筛选入选的优质主题)。 + +![星球优质主题汇总](https://oss.javaguide.cn/xingqiu/Xnip2023-04-21_15-48-13.png) ### 简历修改 @@ -70,7 +82,7 @@ star: 2 ![](https://oss.javaguide.cn/xingqiu/image-20220304123156348.png) -简单统计了一下,到目前为止,我至少帮助 **6000+** 位球友提供了免费的简历修改服务。 +简单统计了一下,到目前为止,我至少帮助 **7000+** 位球友提供了免费的简历修改服务。 ![](https://oss.javaguide.cn/xingqiu/%E7%AE%80%E5%8E%86%E4%BF%AE%E6%94%B92.jpg) @@ -80,7 +92,7 @@ star: 2 ### 一对一提问 -你可以和我进行一对一免费提问交流,我会很走心地回答你的问题。到目前为止,已经累计回答了 **2000+** 个读者的提问。 +你可以和我进行一对一免费提问交流,我会很走心地回答你的问题。到目前为止,已经累计回答了 **3000+** 个读者的提问。 ![](https://oss.javaguide.cn/xingqiu/wecom-temp-151578-45e66ccd48b3b5d3baa8673d33c7b664.jpg) @@ -125,20 +137,12 @@ star: 2 ## 如何加入? -**方式一(不推荐)**:扫描下面的二维码原价加入(续费半价不到)。 - -![知识星球](https://oss.javaguide.cn/xingqiu/image-20220311203414600.png) - -**方式二(推荐)**:添加我的个人微信(**javaguide1024**)领取一个 **30** 元的星球专属优惠券(续费半价不到)。 +这里赠送一个 **30** 元的星球专属优惠券吧,数量有限(价格即将上调。老用户续费半价 ,微信扫码即可续费)! -**一定要备注“优惠卷”**,不然通过不了。 +![知识星球30元优惠卷](https://oss.javaguide.cn/xingqiu/xingqiuyouhuijuan-30.jpg) -![个人微信](https://oss.javaguide.cn/xingqiu/weixin-guidege666.jpeg) +进入星球之后,记得查看 **[星球使用指南](https://t.zsxq.com/0d18KSarv)** (一定要看!!!) 和 **[星球优质主题汇总](https://t.zsxq.com/12uSKgTIm)** 。 **无任何套路,无任何潜在收费项。用心做内容,不割韭菜!** -进入星球之后,记得查看 **[星球使用指南](https://t.zsxq.com/0d18KSarv)** (一定要看!) 。 - -随着时间推移,星球积累的干货资源越来越多,我花在星球上的时间也越来越多,星球的价格会逐步向上调整,想要加入的同学一定要尽早。 - 不过, **一定要确定需要再进** 。并且, **三天之内觉得内容不满意可以全额退款** 。 diff --git a/docs/books/readme.md b/docs/books/README.md similarity index 75% rename from docs/books/readme.md rename to docs/books/README.md index a268e1a4884..700a7ea0e3e 100644 --- a/docs/books/readme.md +++ b/docs/books/README.md @@ -14,6 +14,8 @@ category: 计算机书籍 如果内容对你有帮助的话,欢迎给本项目点个 Star。我会用我的业余时间持续完善这份书单,感谢! -本项目推荐的大部分书籍的 PDF 版本我已经整理到了云盘里,你可以在公众号“**GitHub 掘金计划**” 后台回复“**书籍**”获取到。 +## 公众号 -![](https://oss.javaguide.cn/github/javaguide/booksimage-20220409153638398.png) +最新更新会第一时间同步在公众号,推荐关注!另外,公众号上有很多干货不会同步在线阅读网站。 + +![JavaGuide 官方公众号](https://oss.javaguide.cn/github/javaguide/gongzhonghaoxuanchuan.png) diff --git a/docs/books/database.md b/docs/books/database.md index d6f1e536879..87f92d24184 100644 --- a/docs/books/database.md +++ b/docs/books/database.md @@ -14,11 +14,11 @@ head: [《数据库系统原理》](https://www.icourse163.org/course/BNU-1002842007)这个课程的老师讲的非常详细,而且每一小节的作业设计的也与所讲知识很贴合,后面还有很多配套实验。 -![](https://oscimg.oschina.net/oscnet/up-e113c726a41874ef5fb19f7ac14e38e16ce.png) +![](https://oss.javaguide.cn/github/javaguide/books/up-e113c726a41874ef5fb19f7ac14e38e16ce.png) 如果你比较喜欢动手,对于理论知识比较抵触的话,推荐你看看[《如何开发一个简单的数据库》](https://cstack.github.io/db_tutorial/) ,这个 project 会手把手教你编写一个简单的数据库。 -![](https://oscimg.oschina.net/oscnet/up-11de8cb239aa7201cc8d78fa28928b9ec7d.png) +![](https://oss.javaguide.cn/github/javaguide/books/up-11de8cb239aa7201cc8d78fa28928b9ec7d.png) GitHub 上也已经有大佬用 Java 实现过一个简易的数据库,介绍的挺详细的,感兴趣的朋友可以去看看。地址:[https://github.com/alchemystar/Freedom](https://github.com/alchemystar/Freedom) 。 @@ -26,7 +26,7 @@ GitHub 上也已经有大佬用 Java 实现过一个简易的数据库,介绍 **只要利用好搜索引擎,你可以找到各种语言实现的数据库玩具。** -![](https://oscimg.oschina.net/oscnet/up-d32d853f847633ac7ed0efdecf56be1f1d2.png) +![](https://oss.javaguide.cn/github/javaguide/books/up-d32d853f847633ac7ed0efdecf56be1f1d2.png) **纸上学来终觉浅 绝知此事要躬行!强烈推荐 CS 专业的小伙伴一定要多多实践!!!** @@ -58,7 +58,7 @@ GitHub 上也已经有大佬用 Java 实现过一个简易的数据库,介绍 - **[《高性能 MySQL》](https://book.douban.com/subject/23008813/)**:MySQL 领域的经典之作!学习 MySQL 必看!属于进阶内容,主要教你如何更好地使用 MySQL 。既有有理论,又有实践!如果你没时间都看一遍的话,我建议第 5 章(创建高性能的索引)、第 6 章(查询性能优化) 你一定要认真看一下。 - **[《MySQL 技术内幕》](https://book.douban.com/subject/24708143/)**:你想深入了解 MySQL 存储引擎的话,看这本书准没错! -![](https://oscimg.oschina.net/oscnet/up-3d31e762933f9e50cc7170b2ebd8433917b.png) +![](https://oss.javaguide.cn/github/javaguide/books/up-3d31e762933f9e50cc7170b2ebd8433917b.png) 视频的话,你可以看看动力节点的 [《MySQL 数据库教程视频》](https://www.bilibili.com/video/BV1fx411X7BD)。这个视频基本上把 MySQL 的相关一些入门知识给介绍完了。 @@ -97,7 +97,7 @@ GitHub 上也已经有大佬用 Java 实现过一个简易的数据库,介绍 如果你要学习 Redis 的话,强烈推荐下面这两本书: - [《Redis 设计与实现》](https://book.douban.com/subject/25900156/) :主要是 Redis 理论知识相关的内容,比较全面。我之前写过一篇文章 [《7 年前,24 岁,出版了一本 Redis 神书》](https://mp.weixin.qq.com/s?__biz=Mzg2OTA0Njk0OA==&mid=2247507030&idx=1&sn=0a5fd669413991b30163ab6f5834a4ad&chksm=cea1939df9d61a8b93925fae92f4cee0838c449534e60731cfaf533369831192e296780b32a6&token=709354671&lang=zh_CN&scene=21#wechat_redirect) 来介绍这本书。 -- [《Redis 核心原理与实践》 ](https://book.douban.com/subject/26612779/):主要是结合源码来分析 Redis 的重要知识点比如各种数据结构和高级特性。 +- [《Redis 核心原理与实践》](https://book.douban.com/subject/26612779/):主要是结合源码来分析 Redis 的重要知识点比如各种数据结构和高级特性。 ![《Redis 设计与实现》和《Redis 设计与实现》](https://oss.javaguide.cn/github/javaguide/books/redis-books.png) diff --git a/docs/books/distributed-system.md b/docs/books/distributed-system.md index d8f6e64b1ce..bb131d6dd65 100644 --- a/docs/books/distributed-system.md +++ b/docs/books/distributed-system.md @@ -8,13 +8,11 @@ icon: "distributed-network" ![](https://oss.javaguide.cn/github/javaguide/books/deep-understanding-of-distributed-system.png) -**[《深入理解分布式系统》](https://book.douban.com/subject/35794814/)** 是今年 3 月份刚出的一本分布式中文原创书籍,主要讲的是分布式领域的基本概念、常见挑战以及共识算法。 +**[《深入理解分布式系统》](https://book.douban.com/subject/35794814/)** 是 2022 年出版的一本分布式中文原创书籍,主要讲的是分布式领域的基本概念、常见挑战以及共识算法。 作者用了大量篇幅来介绍分布式领域中非常重要的共识算法,并且还会基于 Go 语言带着你从零实现了一个共识算法的鼻祖 Paxos 算法。 -实话说,我还没有开始看这本书。但是!这本书的作者的博客上的分布式相关的文章我几乎每一篇都认真看过。 - -作者从 2019 年开始构思《深入理解分布式系统》,2020 年开始动笔,花了接近两年的时间才最终交稿。 +实话说,我还没有开始看这本书。但是!这本书的作者的博客上的分布式相关的文章我几乎每一篇都认真看过。作者从 2019 年开始构思《深入理解分布式系统》,2020 年开始动笔,花了接近两年的时间才最终交稿。 ![](https://oss.javaguide.cn/github/javaguide/books/image-20220706121952258.png) @@ -32,15 +30,13 @@ icon: "distributed-network" 书中介绍的大部分概念你可能之前都听过,但是在看了书中的内容之后,你可能会豁然开朗:“哇塞!原来是这样的啊!这不是某技术的原理么?”。 -这本书我之前专门写过知乎回答介绍和推荐,没看过的朋友可以看看:[有哪些你看了以后大呼过瘾的编程书?](https://www.zhihu.com/question/50408698/answer/2278198495) 。 - -另外,如果你在阅读这本书的时候感觉难度比较大,很多地方读不懂的话,我这里推荐一下《深入理解分布式系统》作者写的[《DDIA 逐章精读》小册](https://ddia.qtmuniao.com)。 +这本书我之前专门写过知乎回答介绍和推荐,没看过的朋友可以看看:[有哪些你看了以后大呼过瘾的编程书?](https://www.zhihu.com/question/50408698/answer/2278198495) 。另外,如果你在阅读这本书的时候感觉难度比较大,很多地方读不懂的话,我这里推荐一下《深入理解分布式系统》作者写的[《DDIA 逐章精读》小册](https://ddia.qtmuniao.com)。 ## 《深入理解分布式事务》 ![](https://oss.javaguide.cn/github/javaguide/books/In-depth-understanding-of-distributed-transactions-xiaoyu.png) -**[《深入理解分布式事务》](https://book.douban.com/subject/35626925/)** 这本书是的其中一位作者是 Apache ShenYu(incubating)网关创始人、Hmily、RainCat、Myth 等分布式事务框架的创始人。 +**[《深入理解分布式事务》](https://book.douban.com/subject/35626925/)** 这本书的其中一位作者是 Apache ShenYu(incubating)网关创始人、Hmily、RainCat、Myth 等分布式事务框架的创始人。 学习分布式事务的时候,可以参考一下这本书。虽有一些小错误以及逻辑不通顺的地方,但对于各种分布式事务解决方案的介绍,总体来说还是不错的。 @@ -50,11 +46,19 @@ icon: "distributed-network" **[《从 Paxos 到 Zookeeper》](https://book.douban.com/subject/26292004/)** 是一本带你入门分布式理论的好书。这本书主要介绍几种典型的分布式一致性协议,以及解决分布式一致性问题的思路,其中重点讲解了 Paxos 和 ZAB 协议。 +PS:Zookeeper 现在用的不多,可以不用重点学习,但 Paxos 和 ZAB 协议还是非常值得深入研究的。 + +## 《深入理解分布式共识算法》 + +![](https://oss.javaguide.cn/github/javaguide/books/deep-dive-into-distributed-consensus-algorithms.png) + +**[《深入理解分布式共识算法》](https://book.douban.com/subject/36335459/)** 详细剖析了 Paxos、Raft、Zab 等主流分布式共识算法的核心原理和实现细节。如果你想要了解分布式共识算法的话,不妨参考一下这本书的总结。 + ## 《微服务架构设计模式》 ![](https://oss.javaguide.cn/github/javaguide/books/microservices-patterns.png) -**[《微服务架构设计模式》](https://book.douban.com/subject/33425123/)** 的作者 Chris Richardson 被评为世界十大软件架构师之一、微服务架构先驱。这本书主要讲的是如何开发和部署生产级别的微服务架构应用,示例代码使用 Java 语言和 Spring 框架。 +**[《微服务架构设计模式》](https://book.douban.com/subject/33425123/)** 的作者 Chris Richardson 被评为世界十大软件架构师之一、微服务架构先驱。这本书汇集了 44 个经过实践验证的架构设计模式,这些模式用来解决诸如服务拆分、事务管理、查询和跨服务通信等难题。书中的内容不仅理论扎实,还通过丰富的 Java 代码示例,引导读者一步步掌握开发和部署生产级别的微服务架构应用。 ## 《凤凰架构》 @@ -75,16 +79,6 @@ icon: "distributed-network" - [周志明老师的又一神书!发现宝藏!](https://mp.weixin.qq.com/s?__biz=Mzg2OTA0Njk0OA==&mid=2247505254&idx=1&sn=04faf3093d6002354f06fffbfc2954e0&chksm=cea19aadf9d613bbba7ed0e02ccc4a9ef3a30f4d83530e7ad319c2cc69cd1770e43d1d470046&scene=178&cur_album_id=1646812382221926401#rd) - [Java 领域的又一神书!周志明老师 YYDS!](https://mp.weixin.qq.com/s/9nbzfZGAWM9_qIMp1r6uUQ) -## 《架构解密》 - -![](https://oss.javaguide.cn/github/javaguide/books/jiagoujiemi.png) - -[《架构解密》](https://book.douban.com/subject/35093373/)这本书和我渊源颇深,在大三的时候,我曾经在图书馆借阅过这本书的第一版,大概了花了不到一周就看完了。 - -这本书的第二版在 2020 年就已经出来了,总共也才 15 个评价,算得上是一本非常小众的技术书籍了。 - -书籍质量怎么说呢,各个知识点介绍的都比较泛,匆忙结束,一共 9 章,总共 331 页。如果你只是想初步了解一些分布式相关的概念的话,可以看看这本书,快速概览一波分布式相关的技术。 - ## 其他 - [《分布式系统 : 概念与设计》](https://book.douban.com/subject/21624776/):偏教材类型,内容全而无趣,可作为参考书籍; diff --git a/docs/books/java.md b/docs/books/java.md index d1fa4af679d..8278ed596e1 100644 --- a/docs/books/java.md +++ b/docs/books/java.md @@ -30,9 +30,11 @@ icon: "java" ![《Java 编程思想》-豆瓣](https://oss.javaguide.cn/github/javaguide/books/image-20220424103124893.png) -这本书被很多人称之为 Java 领域的圣经(_感觉有点过了~~~_)。不太推荐编程初学者阅读,有点劝退的味道,稍微有点基础后阅读更好。 +另外,这本书的作者去年新出版了[《On Java》](https://book.douban.com/subject/35751619/),我更推荐这本,内容更新,介绍了 Java 的 3 个长期支持版(Java 8、11、17)。 -我第一次看的时候还觉得有点枯燥,那时候还在上大二,看了 1/3 就没看下去了。 +![](https://oss.javaguide.cn/github/javaguide/books/on-java/6171657600353_.pic_hd.jpg) + +毕竟,这是市面上目前唯一一本介绍了 Java 的 3 个长期支持版(Java 8、11、17)的技术书籍。 **[《Java 8 实战》](https://book.douban.com/subject/26772632/)** @@ -40,13 +42,19 @@ icon: "java" Java 8 算是一个里程碑式的版本,现在一般企业还是用 Java 8 比较多。掌握 Java 8 的一些新特性比如 Lambda、Stream API 还是挺有必要的。这块的话,我推荐 **[《Java 8 实战》](https://book.douban.com/subject/26772632/)** 这本书。 +**[《Java 编程的逻辑》](https://book.douban.com/subject/30133440/)** + +![《Java编程的逻辑》](https://oss.javaguide.cn/github/javaguide/books/image-20230721153650488.png) + +一本非常低调的好书,相比于入门书来说,内容更有深度。适合初学者,同时也适合大家拿来复习 Java 基础知识。 + ## Java 并发 **[《Java 并发编程之美》](https://book.douban.com/subject/30351286/)** ![《Java 并发编程之美》-豆瓣](https://oss.javaguide.cn/github/javaguide/books/image-20220424112413660.png) -_这本书还是非常适合我们用来学习 Java 多线程的。这本书的讲解非常通俗易懂,作者从并发编程基础到实战都是信手拈来。_ +这本书还是非常适合我们用来学习 Java 多线程的,讲解非常通俗易懂,作者从并发编程基础到实战都是信手拈来。 另外,这本书的作者加多自身也会经常在网上发布各种技术文章。这本书也是加多大佬这么多年在多线程领域的沉淀所得的结果吧!他书中的内容基本都是结合代码讲解,非常有说服力! @@ -64,14 +72,12 @@ _这本书还是非常适合我们用来学习 Java 多线程的。这本书的 这本书的质量也是非常过硬!给作者们点个赞!这本书有统一的排版规则和语言风格、清晰的表达方式和逻辑。并且每篇文章初稿写完后,作者们就会互相审校,合并到主分支时所有成员会再次审校,最后再通篇修订了三遍。 -在线阅读:[https://redspider.gitbook.io/concurrent/](https://redspider.gitbook.io/concurrent/) 。 +在线阅读:。 **[《Java 并发实现原理:JDK 源码剖析》](https://book.douban.com/subject/35013531/)** ![《Java 并发实现原理:JDK 源码剖析》-豆瓣](https://oss.javaguide.cn/github/javaguide/books/0b1b046af81f4c94a03e292e66dd6f7d.png) -这本书是 2020 年新出的,所以,现在知道的人还不是很多。 - 这本书主要是对 Java Concurrent 包中一些比较重要的源码进行了讲解,另外,像 JMM、happen-before、CAS 等等比较重要的并发知识这本书也都会一并介绍到。 不论是你想要深入研究 Java 并发,还是说要准备面试,你都可以看看这本书。 @@ -122,12 +128,12 @@ _这本书还是非常适合我们用来学习 Java 多线程的。这本书的 非常重要!非常重要!特别是 Git 和 Docker。 -- **IDEA**:熟悉基本操作以及常用快捷。你可以通过 GitHub 上的开源教程 [《IntelliJ IDEA 简体中文专题教程》](https://github.com/judasn/IntelliJ-IDEA-Tutorial) 来学习 IDEA 的使用。 -- **Maven**:强烈建议学习常用框架之前可以提前花几天时间学习一下**Maven**的使用。(到处找 Jar 包,下载 Jar 包是真的麻烦费事,使用 Maven 可以为你省很多事情)。 -- **Git**:基本的 Git 技能也是必备的,试着在学习的过程中将自己的代码托管在 Github 上。你可以看看这篇 Github 上开源的 [《Git 极简入门》](https://snailclimb.gitee.io/javaguide/#/docs/tools/Git) 。 -- **Docker**:学着用 Docker 安装学习中需要用到的软件比如 MySQL ,这样方便很多,可以为你节省不少时间。你可以看看这篇 Github 上开源的 [《Docker 基本概念解读》](https://snailclimb.gitee.io/javaguide/#/docs/tools/Docker) 、[《一文搞懂 Docker 镜像的常用操作!》](https://snailclimb.gitee.io/javaguide/#/docs/tools/Docker-Image) +- **IDEA**:熟悉基本操作以及常用快捷。相关资料: [《IntelliJ IDEA 简体中文专题教程》](https://github.com/judasn/IntelliJ-IDEA-Tutorial) 。 +- **Maven**:强烈建议学习常用框架之前可以提前花几天时间学习一下**Maven**的使用。(到处找 Jar 包,下载 Jar 包是真的麻烦费事,使用 Maven 可以为你省很多事情)。相关阅读:[Maven 核心概念总结](https://javaguide.cn/tools/maven/maven-core-concepts.html)。 +- **Git**:基本的 Git 技能也是必备的,试着在学习的过程中将自己的代码托管在 Github 上。相关阅读:[Git 核心概念总结](https://javaguide.cn/tools/git/git-intro.html)。 +- **Docker**:学着用 Docker 安装学习中需要用到的软件比如 MySQL ,这样方便很多,可以为你节省不少时间。相关资料:[《Docker - 从入门到实践》](https://yeasy.gitbook.io/docker_practice/) 。 -除了这些工具之外,我强烈建议你一定要搞懂 GitHub 的使用。一些使用 GitHub 的小技巧,你可以看[《GitHub 小技巧》](https://snailclimb.gitee.io/javaguide/#/docs/tools/Github%E6%8A%80%E5%B7%A7)这篇文章。 +除了这些工具之外,我强烈建议你一定要搞懂 GitHub 的使用。一些使用 GitHub 的小技巧,你可以看[Github 实用小技巧总结](https://javaguide.cn/tools/git/github-tips.html)这篇文章。 ## 常用框架 @@ -171,6 +177,26 @@ SpringBoot 解析,不适合初学者。我是去年入手的,现在就看了 比较一般的一本书,可以简单拿来看一下。 +### MyBatis + +MyBatis 国内用的挺多的,我的建议是不需要花太多时间在上面。当然了,MyBatis 的源码还是非常值得学习的,里面有很多不错的编码实践。这里推荐两本讲解 MyBatis 源码的书籍。 + +**[《手写 MyBatis:渐进式源码实践》](https://book.douban.com/subject/36243250/)** + +![《手写MyBatis:渐进式源码实践》](https://oss.javaguide.cn/github/javaguide/books/image-20230724123402784.png) + +我的好朋友小傅哥出版的一本书。这本书以实践为核心,摒弃 MyBatis 源码中繁杂的内容,聚焦于 MyBaits 中的核心逻辑,简化代码实现过程,以渐进式的开发方式,逐步实现 MyBaits 中的核心功能。 + +这本书的配套项目的仓库地址: 。 + +**[《通用源码阅读指导书――MyBatis 源码详解》](https://book.douban.com/subject/35138963/)** + +![《通用源码阅读指导书――MyBatis源码详解》](https://oss.javaguide.cn/github/javaguide/books/image-20230724123416741.png) + +这本书通过 MyBatis 开源代码讲解源码阅读的流程和方法!一共对 MyBatis 源码中的 300 多个类进行了详细解析,包括其背景知识、组织方式、逻辑结构、实现细节。 + +这本书的配套示例仓库地址: 。 + ### Netty **[《Netty 实战》](https://book.douban.com/subject/27038538/)** @@ -191,7 +217,7 @@ SpringBoot 解析,不适合初学者。我是去年入手的,现在就看了 ![](https://oss.javaguide.cn/github/javaguide/open-source-project/image-20220503085034268.png) -2022 年 3 月刚刚出版的一本书。这本书分为上下两篇,上篇通过一个即时聊天系统的实战案例带你入门 Netty,下篇通过 Netty 源码分析带你搞清 Netty 比较重要的底层原理。 +2022 年 3 月出版的一本书。这本书分为上下两篇,上篇通过一个即时聊天系统的实战案例带你入门 Netty,下篇通过 Netty 源码分析带你搞清 Netty 比较重要的底层原理。 ## 性能调优 @@ -217,7 +243,7 @@ O'Reilly 家族书,性能调优的入门书,我个人觉得性能调优是 ![](https://oss.javaguide.cn/github/javaguide/books/20210412232441459.png) -事务与锁、分布式(CAP、分布式事务......)、高并发、高可用 《软件架构设计:大型网站技术架构与业务架构融合之道》 这本书都有介绍到。 +事务与锁、分布式(CAP、分布式事务……)、高并发、高可用 《软件架构设计:大型网站技术架构与业务架构融合之道》 这本书都有介绍到。 ## 面试 diff --git a/docs/books/search-engine.md b/docs/books/search-engine.md index aa59a1a7a9a..50abbd57056 100644 --- a/docs/books/search-engine.md +++ b/docs/books/search-engine.md @@ -14,14 +14,20 @@ Elasticsearch 在 Apache Lucene 的基础上开发而成,学习 ES 之前, ## Elasticsearch -极客时间的[《Elasticsearch 核心技术与实战》](http://gk.link/a/10bcT "《Elasticsearch 核心技术与实战》")这门课程基于 Elasticsearch 7.1 版本讲解,还算比较新。并且,作者是 eBay 资深技术专家,有 20 年的行业经验,课程质量有保障! +**[《一本书讲透 Elasticsearch:原理、进阶与工程实践》](https://book.douban.com/subject/36716996/)** -![《Elasticsearch 核心技术与实战》-极客时间](https://oss.javaguide.cn/github/javaguide/csdn/20210420231125225.png) +![](https://oss.javaguide.cn/github/javaguide/books/one-book-guide-to-elasticsearch.png) + +基于 8.x 版本编写,目前全网最新的 Elasticsearch 讲解书籍。内容覆盖 Elastic 官方认证的核心知识点,源自真实项目案例和企业级问题解答。 + +**[《Elasticsearch 核心技术与实战》](http://gk.link/a/10bcT "《Elasticsearch 核心技术与实战》")** -如果你想看书的话,可以考虑一下 **[《Elasticsearch 实战》](https://book.douban.com/subject/30380439/)** 这本书。不过,需要说明的是,这本书中的 Elasticsearch 版本比较老,你可以将其作为一个参考书籍来看,有一些原理性的东西可以在上面找找答案。 +极客时间的这门课程基于 Elasticsearch 7.1 版本讲解,还算比较新。并且,作者是 eBay 资深技术专家,有 20 年的行业经验,课程质量有保障! + +![《Elasticsearch 核心技术与实战》-极客时间](https://oss.javaguide.cn/github/javaguide/csdn/20210420231125225.png) -![《Elasticsearch 实战》-豆瓣](https://p3-juejin.byteimg.com/tos-cn-i-k3u1fbpfcp/d8b7fa83490e466aa212382cd323d37f~tplv-k3u1fbpfcp-zoom-1.image) +**[《Elasticsearch 源码解析与优化实战》](https://book.douban.com/subject/30386800/)** -如果你想进一步深入研究 Elasticsearch 原理的话,可以看看张超老师的 **[《Elasticsearch 源码解析与优化实战》](https://book.douban.com/subject/30386800/)** 这本书。这是市面上唯一一本写 Elasticsearch 源码的书。 +![《Elasticsearch 源码解析与优化实战》-豆瓣](https://oss.javaguide.cn/p3-juejin/f856485931a945639d5c23aaed74fb38~tplv-k3u1fbpfcp-zoom-1.png) -![《Elasticsearch 源码解析与优化实战》-豆瓣](https://p3-juejin.byteimg.com/tos-cn-i-k3u1fbpfcp/f856485931a945639d5c23aaed74fb38~tplv-k3u1fbpfcp-zoom-1.image) +如果你想进一步深入研究 Elasticsearch 原理的话,可以看看张超老师的这本书。这是市面上唯一一本写 Elasticsearch 源码的书。 diff --git a/docs/cs-basics/algorithms/10-classical-sorting-algorithms.md b/docs/cs-basics/algorithms/10-classical-sorting-algorithms.md index fa88a078a57..d583b936a12 100644 --- a/docs/cs-basics/algorithms/10-classical-sorting-algorithms.md +++ b/docs/cs-basics/algorithms/10-classical-sorting-algorithms.md @@ -7,40 +7,43 @@ tag: > 本文转自:,JavaGuide 对其做了补充完善。 + + ## 引言 所谓排序,就是使一串记录,按照其中的某个或某些关键字的大小,递增或递减的排列起来的操作。排序算法,就是如何使得记录按照要求排列的方法。排序算法在很多领域得到相当地重视,尤其是在大量数据的处理方面。一个优秀的算法可以节省大量的资源。在各个领域中考虑到数据的各种限制和规范,要得到一个符合实际的优秀算法,得经过大量的推理和分析。 -两年前,我曾在[博客园](https://www.cnblogs.com/guoyaohua/)发布过一篇[《十大经典排序算法最强总结(含 JAVA 代码实现)》](https://www.cnblogs.com/guoyaohua/p/8600214.html)博文,简要介绍了比较经典的十大排序算法,不过在之前的博文中,仅给出了 Java 版本的代码实现,并且有一些细节上的错误。所以,今天重新写一篇文章,深入了解下十大经典排序算法的原理及实现。 - ## 简介 -排序算法可以分为: - -- **内部排序**:数据记录在内存中进行排序。 -- **[外部排序](https://zh.wikipedia.org/wiki/外排序)**:因排序的数据很大,一次不能容纳全部的排序记录,在排序过程中需要访问外存。 - -常见的内部排序算法有:**插入排序**、**希尔排序**、**选择排序**、**冒泡排序**、**归并排序**、**快速排序**、**堆排序**、**基数排序**等,本文只讲解内部排序算法。用一张图概括: - -![十大排序算法](https://oss.javaguide.cn/github/javaguide/cs-basics/sorting-algorithms/sort1.png) - -**图片名词解释:** - -- **n**:数据规模 -- **k**:“桶” 的个数 -- **In-place**:占用常数内存,不占用额外内存 -- **Out-place**:占用额外内存 - -### 术语说明 - -- **稳定**:如果 A 原本在 B 前面,而 A=B,排序之后 A 仍然在 B 的前面。 -- **不稳定**:如果 A 原本在 B 的前面,而 A=B,排序之后 A 可能会出现在 B 的后面。 -- **内排序**:所有排序操作都在内存中完成。 -- **外排序**:由于数据太大,因此把数据放在磁盘中,而排序通过磁盘和内存的数据传输才能进行。 +### 排序算法总结 + +常见的内部排序算法有:**插入排序**、**希尔排序**、**选择排序**、**冒泡排序**、**归并排序**、**快速排序**、**堆排序**、**基数排序**等,本文只讲解内部排序算法。用一张表格概括: + +| 排序算法 | 时间复杂度(平均) | 时间复杂度(最差) | 时间复杂度(最好) | 空间复杂度 | 排序方式 | 稳定性 | +| -------- | ------------------ | ------------------ | ------------------ | ---------- | -------- | ------ | +| 冒泡排序 | O(n^2) | O(n^2) | O(n) | O(1) | 内部排序 | 稳定 | +| 选择排序 | O(n^2) | O(n^2) | O(n^2) | O(1) | 内部排序 | 不稳定 | +| 插入排序 | O(n^2) | O(n^2) | O(n) | O(1) | 内部排序 | 稳定 | +| 希尔排序 | O(nlogn) | O(n^2) | O(nlogn) | O(1) | 内部排序 | 不稳定 | +| 归并排序 | O(nlogn) | O(nlogn) | O(nlogn) | O(n) | 外部排序 | 稳定 | +| 快速排序 | O(nlogn) | O(n^2) | O(nlogn) | O(logn) | 内部排序 | 不稳定 | +| 堆排序 | O(nlogn) | O(nlogn) | O(nlogn) | O(1) | 内部排序 | 不稳定 | +| 计数排序 | O(n+k) | O(n+k) | O(n+k) | O(k) | 外部排序 | 稳定 | +| 桶排序 | O(n+k) | O(n^2) | O(n+k) | O(n+k) | 外部排序 | 稳定 | +| 基数排序 | O(n×k) | O(n×k) | O(n×k) | O(n+k) | 外部排序 | 稳定 | + +**术语解释**: + +- **n**:数据规模,表示待排序的数据量大小。 +- **k**:“桶” 的个数,在某些特定的排序算法中(如基数排序、桶排序等),表示分割成的独立的排序区间或类别的数量。 +- **内部排序**:所有排序操作都在内存中完成,不需要额外的磁盘或其他存储设备的辅助。这适用于数据量小到足以完全加载到内存中的情况。 +- **外部排序**:当数据量过大,不可能全部加载到内存中时使用。外部排序通常涉及到数据的分区处理,部分数据被暂时存储在外部磁盘等存储设备上。 +- **稳定**:如果 A 原本在 B 前面,而 $A=B$,排序之后 A 仍然在 B 的前面。 +- **不稳定**:如果 A 原本在 B 的前面,而 $A=B$,排序之后 A 可能会出现在 B 的后面。 - **时间复杂度**:定性描述一个算法执行所耗费的时间。 - **空间复杂度**:定性描述一个算法执行所需内存的大小。 -### 算法分类 +### 排序算法分类 十种常见排序算法可以分类两大类别:**比较类排序**和**非比较类排序**。 @@ -50,7 +53,7 @@ tag: 比较类排序的优势是,适用于各种规模的数据,也不在乎数据的分布,都能进行排序。可以说,比较排序适用于一切需要排序的情况。 -而**计数排序**、**基数排序**、**桶排序**则属于**非比较类排序算法**。非比较排序不通过比较来决定元素间的相对次序,而是通过确定每个元素之前,应该有多少个元素来排序。由于它可以突破基于比较排序的时间下界,以线性时间运行,因此称为线性时间非比较类排序。 非比较排序只要确定每个元素之前的已有的元素个数即可,所有一次遍历即可解决。算法时间复杂度 `O(n)`。 +而**计数排序**、**基数排序**、**桶排序**则属于**非比较类排序算法**。非比较排序不通过比较来决定元素间的相对次序,而是通过确定每个元素之前,应该有多少个元素来排序。由于它可以突破基于比较排序的时间下界,以线性时间运行,因此称为线性时间非比较类排序。 非比较排序只要确定每个元素之前的已有的元素个数即可,所有一次遍历即可解决。算法时间复杂度 $O(n)$。 非比较排序时间复杂度底,但由于非比较排序需要占用空间来确定唯一位置。所以对数据规模和数据分布有一定的要求。 @@ -104,13 +107,13 @@ public static int[] bubbleSort(int[] arr) { ### 算法分析 - **稳定性**:稳定 -- **时间复杂度**:最佳:O(n) ,最差:O(n2), 平均:O(n2) -- **空间复杂度**:O(1) +- **时间复杂度**:最佳:$O(n)$ ,最差:$O(n^2)$, 平均:$O(n^2)$ +- **空间复杂度**:$O(1)$ - **排序方式**:In-place ## 选择排序 (Selection Sort) -选择排序是一种简单直观的排序算法,无论什么数据进去都是 `O(n²)` 的时间复杂度。所以用到它的时候,数据规模越小越好。唯一的好处可能就是不占用额外的内存空间了吧。它的工作原理:首先在未排序序列中找到最小(大)元素,存放到排序序列的起始位置,然后,再从剩余未排序元素中继续寻找最小(大)元素,然后放到已排序序列的末尾。以此类推,直到所有元素均排序完毕。 +选择排序是一种简单直观的排序算法,无论什么数据进去都是 $O(n^2)$ 的时间复杂度。所以用到它的时候,数据规模越小越好。唯一的好处可能就是不占用额外的内存空间了吧。它的工作原理:首先在未排序序列中找到最小(大)元素,存放到排序序列的起始位置,然后,再从剩余未排序元素中继续寻找最小(大)元素,然后放到已排序序列的末尾。以此类推,直到所有元素均排序完毕。 ### 算法步骤 @@ -151,13 +154,13 @@ public static int[] selectionSort(int[] arr) { ### 算法分析 - **稳定性**:不稳定 -- **时间复杂度**:最佳:O(n2) ,最差:O(n2), 平均:O(n2) -- **空间复杂度**:O(1) +- **时间复杂度**:最佳:$O(n^2)$ ,最差:$O(n^2)$, 平均:$O(n^2)$ +- **空间复杂度**:$O(1)$ - **排序方式**:In-place ## 插入排序 (Insertion Sort) -插入排序是一种简单直观的排序算法。它的工作原理是通过构建有序序列,对于未排序数据,在已排序序列中从后向前扫描,找到相应位置并插入。插入排序在实现上,通常采用 in-place 排序(即只需用到 `O(1)` 的额外空间的排序),因而在从后向前扫描过程中,需要反复把已排序元素逐步向后挪位,为最新元素提供插入空间。 +插入排序是一种简单直观的排序算法。它的工作原理是通过构建有序序列,对于未排序数据,在已排序序列中从后向前扫描,找到相应位置并插入。插入排序在实现上,通常采用 in-place 排序(即只需用到 $O(1)$ 的额外空间的排序),因而在从后向前扫描过程中,需要反复把已排序元素逐步向后挪位,为最新元素提供插入空间。 插入排序的代码实现虽然没有冒泡排序和选择排序那么简单粗暴,但它的原理应该是最容易理解的了,因为只要打过扑克牌的人都应该能够秒懂。插入排序是一种最简单直观的排序算法,它的工作原理是通过构建有序序列,对于未排序数据,在已排序序列中从后向前扫描,找到相应位置并插入。 @@ -201,25 +204,25 @@ public static int[] insertionSort(int[] arr) { ### 算法分析 - **稳定性**:稳定 -- **时间复杂度**:最佳:O(n) ,最差:O(n2), 平均:O(n2) -- **空间复杂度**:O(1) +- **时间复杂度**:最佳:$O(n)$ ,最差:$O(n^2)$, 平均:$O(n2)$ +- **空间复杂度**:O(1)$ - **排序方式**:In-place ## 希尔排序 (Shell Sort) -希尔排序是希尔 (Donald Shell) 于 1959 年提出的一种排序算法。希尔排序也是一种插入排序,它是简单插入排序经过改进之后的一个更高效的版本,也称为递减增量排序算法,同时该算法是冲破 `O(n²)` 的第一批算法之一。 +希尔排序是希尔 (Donald Shell) 于 1959 年提出的一种排序算法。希尔排序也是一种插入排序,它是简单插入排序经过改进之后的一个更高效的版本,也称为递减增量排序算法,同时该算法是冲破 $O(n^2)$ 的第一批算法之一。 希尔排序的基本思想是:先将整个待排序的记录序列分割成为若干子序列分别进行直接插入排序,待整个序列中的记录 “基本有序” 时,再对全体记录进行依次直接插入排序。 ### 算法步骤 -我们来看下希尔排序的基本步骤,在此我们选择增量 `gap=length/2`,缩小增量继续以 `gap = gap/2` 的方式,这种增量选择我们可以用一个序列来表示,`{n/2, (n/2)/2, ..., 1}`,称为**增量序列**。希尔排序的增量序列的选择与证明是个数学难题,我们选择的这个增量序列是比较常用的,也是希尔建议的增量,称为希尔增量,但其实这个增量序列不是最优的。此处我们做示例使用希尔增量。 +我们来看下希尔排序的基本步骤,在此我们选择增量 $gap=length/2$,缩小增量继续以 $gap = gap/2$ 的方式,这种增量选择我们可以用一个序列来表示,$\lbrace \frac{n}{2}, \frac{(n/2)}{2}, \dots, 1 \rbrace$,称为**增量序列**。希尔排序的增量序列的选择与证明是个数学难题,我们选择的这个增量序列是比较常用的,也是希尔建议的增量,称为希尔增量,但其实这个增量序列不是最优的。此处我们做示例使用希尔增量。 先将整个待排序的记录序列分割成为若干子序列分别进行直接插入排序,具体算法描述: -- 选择一个增量序列 `{t1, t2, …, tk}`,其中 `(ti>tj, i 来源:[使用 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; } } ``` @@ -396,8 +428,8 @@ public static void quickSort(int[] array, int low, int high) { ### 算法分析 - **稳定性**:不稳定 -- **时间复杂度**:最佳:O(nlogn), 最差:O(nlogn),平均:O(nlogn) -- **空间复杂度**:O(nlogn) +- **时间复杂度**:最佳:$O(nlogn)$, 最差:$O(n^2)$,平均:$O(nlogn)$ +- **空间复杂度**:$O(logn)$ ## 堆排序 (Heap Sort) @@ -405,9 +437,9 @@ public static void quickSort(int[] array, int low, int high) { ### 算法步骤 -1. 将初始待排序列 `(R1, R2, ……, Rn)` 构建成大顶堆,此堆为初始的无序区; -2. 将堆顶元素 `R[1]` 与最后一个元素 `R[n]` 交换,此时得到新的无序区 `(R1, R2, ……, Rn-1)` 和新的有序区 (Rn), 且满足 `R[1, 2, ……, n-1]<=R[n]`; -3. 由于交换后新的堆顶 `R[1]` 可能违反堆的性质,因此需要对当前无序区 `(R1, R2, ……, Rn-1)` 调整为新堆,然后再次将 R [1] 与无序区最后一个元素交换,得到新的无序区 `(R1, R2, ……, Rn-2)` 和新的有序区 `(Rn-1, Rn)`。不断重复此过程直到有序区的元素个数为 `n-1`,则整个排序过程完成。 +1. 将初始待排序列 $(R_1, R_2, \dots, R_n)$ 构建成大顶堆,此堆为初始的无序区; +2. 将堆顶元素 $R_1$ 与最后一个元素 $R_n$ 交换,此时得到新的无序区 $(R_1, R_2, \dots, R_{n-1})$ 和新的有序区 $R_n$, 且满足 $R_i \leqslant R_n (i \in 1, 2,\dots, n-1)$; +3. 由于交换后新的堆顶 $R_1$ 可能违反堆的性质,因此需要对当前无序区 $(R_1, R_2, \dots, R_{n-1})$ 调整为新堆,然后再次将 $R_1$ 与无序区最后一个元素交换,得到新的无序区 $(R_1, R_2, \dots, R_{n-2})$ 和新的有序区 $(R_{n-1}, R_n)$。不断重复此过程直到有序区的元素个数为 $n-1$,则整个排序过程完成。 ### 图解算法 @@ -485,8 +517,8 @@ public static int[] heapSort(int[] arr) { ### 算法分析 - **稳定性**:不稳定 -- **时间复杂度**:最佳:O(nlogn), 最差:O(nlogn), 平均:O(nlogn) -- **空间复杂度**:O(1) +- **时间复杂度**:最佳:$O(nlogn)$, 最差:$O(nlogn)$, 平均:$O(nlogn)$ +- **空间复杂度**:$O(1)$ ## 计数排序 (Counting Sort) @@ -498,10 +530,10 @@ public static int[] heapSort(int[] arr) { 1. 找出数组中的最大值 `max`、最小值 `min`; 2. 创建一个新数组 `C`,其长度是 `max-min+1`,其元素默认值都为 0; -3. 遍历原数组 `A` 中的元素 `A[i]`,以 `A[i]-min` 作为 `C` 数组的索引,以 `A[i]` 的值在 `A` 中元素出现次数作为 `C[A[i]-min]` 的值; +3. 遍历原数组 `A` 中的元素 `A[i]`,以 `A[i] - min` 作为 `C` 数组的索引,以 `A[i]` 的值在 `A` 中元素出现次数作为 `C[A[i] - min]` 的值; 4. 对 `C` 数组变形,**新元素的值是该元素与前一个元素值的和**,即当 `i>1` 时 `C[i] = C[i] + C[i-1]`; 5. 创建结果数组 `R`,长度和原始数组一样。 -6. **从后向前**遍历原始数组 `A` 中的元素 `A[i]`,使用 `A[i]` 减去最小值 `min` 作为索引,在计数数组 `C` 中找到对应的值 `C[A[i]-min]`,`C[A[i]-min]-1` 就是 `A[i]` 在结果数组 `R` 中的位置,做完上述这些操作,将 `count[A[i]-min]` 减小 1。 +6. **从后向前**遍历原始数组 `A` 中的元素 `A[i]`,使用 `A[i]` 减去最小值 `min` 作为索引,在计数数组 `C` 中找到对应的值 `C[A[i] - min]`,`C[A[i] - min] - 1` 就是 `A[i]` 在结果数组 `R` 中的位置,做完上述这些操作,将 `count[A[i] - min]` 减小 1。 ### 图解算法 @@ -560,13 +592,13 @@ public static int[] countingSort(int[] arr) { } ``` -## 算法分析 +### 算法分析 -当输入的元素是 `n` 个 `0` 到 `k` 之间的整数时,它的运行时间是 `O(n+k)`。计数排序不是比较排序,排序的速度快于任何比较排序算法。由于用来计数的数组 `C` 的长度取决于待排序数组中数据的范围(等于待排序数组的**最大值与最小值的差加上 1**),这使得计数排序对于数据范围很大的数组,需要大量额外内存空间。 +当输入的元素是 `n` 个 `0` 到 `k` 之间的整数时,它的运行时间是 $O(n+k)$。计数排序不是比较排序,排序的速度快于任何比较排序算法。由于用来计数的数组 `C` 的长度取决于待排序数组中数据的范围(等于待排序数组的**最大值与最小值的差加上 1**),这使得计数排序对于数据范围很大的数组,需要大量额外内存空间。 - **稳定性**:稳定 -- **时间复杂度**:最佳:`O(n+k)` 最差:`O(n+k)` 平均:`O(n+k)` -- **空间复杂度**:`O(k)` +- **时间复杂度**:最佳:$O(n+k)$ 最差:$O(n+k)$ 平均:$O(n+k)$ +- **空间复杂度**:$O(k)$ ## 桶排序 (Bucket Sort) @@ -648,22 +680,22 @@ public static List bucketSort(List arr, int bucket_size) { ### 算法分析 - **稳定性**:稳定 -- **时间复杂度**:最佳:`O(n+k)` 最差:`O(n²)` 平均:`O(n+k)` -- **空间复杂度**:`O(k)` +- **时间复杂度**:最佳:$O(n+k)$ 最差:$O(n^2)$ 平均:$O(n+k)$ +- **空间复杂度**:$O(n+k)$ ## 基数排序 (Radix Sort) -基数排序也是非比较的排序算法,对元素中的每一位数字进行排序,从最低位开始排序,复杂度为 `O(n×k)`,`n` 为数组长度,`k` 为数组中元素的最大的位数; +基数排序也是非比较的排序算法,对元素中的每一位数字进行排序,从最低位开始排序,复杂度为 $O(n×k)$,$n$ 为数组长度,$k$ 为数组中元素的最大的位数; 基数排序是按照低位先排序,然后收集;再按照高位排序,然后再收集;依次类推,直到最高位。有时候有些属性是有优先级顺序的,先按低优先级排序,再按高优先级排序。最后的次序就是高优先级高的在前,高优先级相同的低优先级高的在前。基数排序基于分别排序,分别收集,所以是稳定的。 ### 算法步骤 -1. 取得数组中的最大数,并取得位数,即为迭代次数 `N`(例如:数组中最大数值为 1000,则 `N=4`); +1. 取得数组中的最大数,并取得位数,即为迭代次数 $N$(例如:数组中最大数值为 1000,则 $N=4$); 2. `A` 为原始数组,从最低位开始取每个位组成 `radix` 数组; 3. 对 `radix` 进行计数排序(利用计数排序适用于小范围数的特点); 4. 将 `radix` 依次赋值给原数组; -5. 重复 2~4 步骤 `N` 次 +5. 重复 2~4 步骤 $N$ 次 ### 图解算法 @@ -716,8 +748,8 @@ public static int[] radixSort(int[] arr) { ### 算法分析 - **稳定性**:稳定 -- **时间复杂度**:最佳:`O(n×k)` 最差:`O(n×k)` 平均:`O(n×k)` -- **空间复杂度**:`O(n+k)` +- **时间复杂度**:最佳:$O(n×k)$ 最差:$O(n×k)$ 平均:$O(n×k)$ +- **空间复杂度**:$O(n+k)$ **基数排序 vs 计数排序 vs 桶排序** @@ -732,3 +764,5 @@ public static int[] radixSort(int[] arr) { - - - + + diff --git a/docs/cs-basics/algorithms/classical-algorithm-problems-recommendations.md b/docs/cs-basics/algorithms/classical-algorithm-problems-recommendations.md new file mode 100644 index 00000000000..3a6a01a210f --- /dev/null +++ b/docs/cs-basics/algorithms/classical-algorithm-problems-recommendations.md @@ -0,0 +1,113 @@ +--- +title: 经典算法思想总结(含LeetCode题目推荐) +category: 计算机基础 +tag: + - 算法 +--- + +## 贪心算法 + +### 算法思想 + +贪心的本质是选择每一阶段的局部最优,从而达到全局最优。 + +### 一般解题步骤 + +- 将问题分解为若干个子问题 +- 找出适合的贪心策略 +- 求解每一个子问题的最优解 +- 将局部最优解堆叠成全局最优解 + +### LeetCode + +455.分发饼干: + +121.买卖股票的最佳时机: + +122.买卖股票的最佳时机 II: + +55.跳跃游戏: + +45.跳跃游戏 II: + +## 动态规划 + +### 算法思想 + +动态规划中每一个状态一定是由上一个状态推导出来的,这一点就区分于贪心,贪心没有状态推导,而是从局部直接选最优的。 + +经典题目:01 背包、完全背包 + +### 一般解题步骤 + +- 确定 dp 数组(dp table)以及下标的含义 +- 确定递推公式 +- dp 数组如何初始化 +- 确定遍历顺序 +- 举例推导 dp 数组 + +### LeetCode + +509.斐波那契数: + +746.使用最小花费爬楼梯: + +416.分割等和子集: + +518.零钱兑换: + +647.回文子串: + +516.最长回文子序列: + +## 回溯算法 + +### 算法思想 + +回溯算法实际上一个类似枚举的搜索尝试过程,主要是在搜索尝试过程中寻找问题的解,当发现已不满足求解条 + +件时,就“回溯”返回,尝试别的路径。其本质就是穷举。 + +经典题目:8 皇后 + +### 一般解题步骤 + +- 针对所给问题,定义问题的解空间,它至少包含问题的一个(最优)解。 +- 确定易于搜索的解空间结构,使得能用回溯法方便地搜索整个解空间 。 +- 以深度优先的方式搜索解空间,并且在搜索过程中用剪枝函数避免无效搜索。 + +### leetcode + +77.组合: + +39.组合总和: + +40.组合总和 II: + +78.子集: + +90.子集 II: + +51.N 皇后: + +## 分治算法 + +### 算法思想 + +将一个规模为 N 的问题分解为 K 个规模较小的子问题,这些子问题相互独立且与原问题性质相同。求出子问题的解,就可得到原问题的解。 + +经典题目:二分查找、汉诺塔问题 + +### 一般解题步骤 + +- 将原问题分解为若干个规模较小,相互独立,与原问题形式相同的子问题; +- 若子问题规模较小而容易被解决则直接解,否则递归地解各个子问题 +- 将各个子问题的解合并为原问题的解。 + +### LeetCode + +108.将有序数组转换成二叉搜索数: + +148.排序列表: + +23.合并 k 个升序链表: diff --git a/docs/cs-basics/algorithms/common-data-structures-leetcode-recommendations.md b/docs/cs-basics/algorithms/common-data-structures-leetcode-recommendations.md new file mode 100644 index 00000000000..51d9225730f --- /dev/null +++ b/docs/cs-basics/algorithms/common-data-structures-leetcode-recommendations.md @@ -0,0 +1,64 @@ +--- +title: 常见数据结构经典LeetCode题目推荐 +category: 计算机基础 +tag: + - 算法 +--- + +## 数组 + +704.二分查找: + +80.删除有序数组中的重复项 II: + +977.有序数组的平方: + +## 链表 + +707.设计链表: + +206.反转链表: + +92.反转链表 II: + +61.旋转链表: + +## 栈与队列 + +232.用栈实现队列: + +225.用队列实现栈: + +347.前 K 个高频元素: + +239.滑动窗口最大值: + +## 二叉树 + +105.从前序与中序遍历构造二叉树: + +117.填充每个节点的下一个右侧节点指针 II: + +236.二叉树的最近公共祖先: + +129.求根节点到叶节点数字之和: + +102.二叉树的层序遍历: + +530.二叉搜索树的最小绝对差: + +## 图 + +200.岛屿数量: + +207.课程表: + +210.课程表 II: + +## 堆 + +215.数组中的第 K 个最大元素: + +216.数据流的中位数: + +217.前 K 个高频元素: diff --git a/docs/cs-basics/algorithms/linkedlist-algorithm-problems.md b/docs/cs-basics/algorithms/linkedlist-algorithm-problems.md index 678fed36269..1280445409e 100644 --- a/docs/cs-basics/algorithms/linkedlist-algorithm-problems.md +++ b/docs/cs-basics/algorithms/linkedlist-algorithm-problems.md @@ -5,6 +5,8 @@ tag: - 算法 --- + + ## 1. 两数相加 ### 题目描述 @@ -15,7 +17,7 @@ tag: 示例: -``` +```plain 输入:(2 -> 4 -> 3) + (5 -> 6 -> 4) 输出:7 -> 0 -> 8 原因:342 + 465 = 807 @@ -25,14 +27,14 @@ tag: Leetcode 官方详细解答地址: -https://leetcode-cn.com/problems/add-two-numbers/solution/ + > 要对头结点进行操作时,考虑创建哑节点 dummy,使用 dummy->next 表示真正的头节点。这样可以避免处理头节点为空的边界问题。 我们使用变量来跟踪进位,并从包含最低有效位的表头开始模拟逐 位相加的过程。 -![图1,对两数相加方法的可视化: 342 + 465 = 807, 每个结点都包含一个数字,并且数字按位逆序存储。](https://my-blog-to-use.oss-cn-beijing.aliyuncs.com/18-9-20/34910956.jpg) +![图1,对两数相加方法的可视化: 342 + 465 = 807, 每个结点都包含一个数字,并且数字按位逆序存储。](https://oss.javaguide.cn/github/javaguide/cs-basics/algorithms/34910956.jpg) ### Solution @@ -80,7 +82,7 @@ public ListNode addTwoNumbers(ListNode l1, ListNode l2) { > 剑指 offer:输入一个链表,反转链表后,输出链表的所有元素。 -![翻转链表](https://my-blog-to-use.oss-cn-beijing.aliyuncs.com/18-9-20/81431871.jpg) +![翻转链表](https://oss.javaguide.cn/github/javaguide/cs-basics/algorithms/81431871.jpg) ### 问题分析 @@ -153,7 +155,7 @@ public class Solution { 输出: -``` +```plain 5 4 3 @@ -225,7 +227,7 @@ public class Solution { **示例:** -``` +```plain 给定一个链表: 1->2->3->4->5, 和 n = 2. 当删除了倒数第二个节点后,链表变为 1->2->3->5. @@ -246,7 +248,7 @@ public class Solution { 我们注意到这个问题可以容易地简化成另一个问题:删除从列表开头数起的第 (L - n + 1)个结点,其中 L 是列表的长度。只要我们找到列表的长度 L,这个问题就很容易解决。 -![图 1. 删除列表中的第 L - n + 1 个元素](https://my-blog-to-use.oss-cn-beijing.aliyuncs.com/18-9-20/94354387.jpg) +![图 1. 删除列表中的第 L - n + 1 个元素](https://oss.javaguide.cn/github/javaguide/cs-basics/algorithms/94354387.jpg) ### Solution @@ -383,3 +385,5 @@ public class Solution { } } ``` + + diff --git a/docs/cs-basics/algorithms/string-algorithm-problems.md b/docs/cs-basics/algorithms/string-algorithm-problems.md index 518d9b6c508..796fe7bf986 100644 --- a/docs/cs-basics/algorithms/string-algorithm-problems.md +++ b/docs/cs-basics/algorithms/string-algorithm-problems.md @@ -7,7 +7,7 @@ tag: > 作者:wwwxmu > -> 原文地址:https://www.weiweiblog.cn/13string/ +> 原文地址: ## 1. KMP 算法 @@ -25,7 +25,7 @@ tag: **除此之外,再来了解一下 BM 算法!** > BM 算法也是一种精确字符串匹配算法,它采用从右向左比较的方法,同时应用到了两种启发式规则,即坏字符规则 和好后缀规则 ,来决定向右跳跃的距离。基本思路就是从右往左进行字符匹配,遇到不匹配的字符后从坏字符表和好后缀表找一个最大的右移值,将模式串右移继续匹配。 -> 《字符串匹配的 KMP 算法》:http://www.ruanyifeng.com/blog/2013/05/Knuth%E2%80%93Morris%E2%80%93Pratt_algorithm.html +> 《字符串匹配的 KMP 算法》: ## 2. 替换空格 @@ -81,14 +81,14 @@ str.toString().replace(" ","%20"); 示例 1: -``` +```plain 输入: ["flower","flow","flight"] 输出: "fl" ``` 示例 2: -``` +```plain 输入: ["dog","racecar","car"] 输出: "" 解释: 输入不存在公共前缀。 @@ -98,56 +98,56 @@ str.toString().replace(" ","%20"); ```java public class Main { - public static String replaceSpace(String[] strs) { - - // 如果检查值不合法及就返回空串 - if (!checkStrs(strs)) { - return ""; - } - // 数组长度 - int len = strs.length; - // 用于保存结果 - StringBuilder res = new StringBuilder(); - // 给字符串数组的元素按照升序排序(包含数字的话,数字会排在前面) - Arrays.sort(strs); - int m = strs[0].length(); - int n = strs[len - 1].length(); - int num = Math.min(m, n); - for (int i = 0; i < num; i++) { - if (strs[0].charAt(i) == strs[len - 1].charAt(i)) { - res.append(strs[0].charAt(i)); - } else - break; - - } - return res.toString(); - - } - - private static boolean checkStrs(String[] strs) { - boolean flag = false; - if (strs != null) { - // 遍历strs检查元素值 - for (int i = 0; i < strs.length; i++) { - if (strs[i] != null && strs[i].length() != 0) { - flag = true; - } else { - flag = false; - break; - } - } - } - return flag; - } - - // 测试 - public static void main(String[] args) { - String[] strs = { "customer", "car", "cat" }; - // String[] strs = { "customer", "car", null };//空串 - // String[] strs = {};//空串 - // String[] strs = null;//空串 - System.out.println(Main.replaceSpace(strs));// c - } + public static String replaceSpace(String[] strs) { + + // 如果检查值不合法及就返回空串 + if (!checkStrs(strs)) { + return ""; + } + // 数组长度 + int len = strs.length; + // 用于保存结果 + StringBuilder res = new StringBuilder(); + // 给字符串数组的元素按照升序排序(包含数字的话,数字会排在前面) + Arrays.sort(strs); + int m = strs[0].length(); + int n = strs[len - 1].length(); + int num = Math.min(m, n); + for (int i = 0; i < num; i++) { + if (strs[0].charAt(i) == strs[len - 1].charAt(i)) { + res.append(strs[0].charAt(i)); + } else + break; + + } + return res.toString(); + + } + + private static boolean checkStrs(String[] strs) { + boolean flag = false; + if (strs != null) { + // 遍历strs检查元素值 + for (int i = 0; i < strs.length; i++) { + if (strs[i] != null && strs[i].length() != 0) { + flag = true; + } else { + flag = false; + break; + } + } + } + return flag; + } + + // 测试 + public static void main(String[] args) { + String[] strs = { "customer", "car", "cat" }; + // String[] strs = { "customer", "car", null };//空串 + // String[] strs = {};//空串 + // String[] strs = null;//空串 + System.out.println(Main.replaceSpace(strs));// c + } } ``` @@ -158,12 +158,12 @@ public class Main { > LeetCode: 给定一个包含大写字母和小写字母的字符串,找到通过这些字母构造成的最长的回文串。在构造过程中,请注意区分大小写。比如`"Aa"`不能当做一个回文字符串。注 > 意:假设字符串的长度不会超过 1010。 - -> 回文串:“回文串”是一个正读和反读都一样的字符串,比如“level”或者“noon”等等就是回文串。——百度百科 地址:https://baike.baidu.com/item/%E5%9B%9E%E6%96%87%E4%B8%B2/1274921?fr=aladdin +> +> 回文串:“回文串”是一个正读和反读都一样的字符串,比如“level”或者“noon”等等就是回文串。——百度百科 地址: 示例 1: -``` +```plain 输入: "abccccdd" @@ -210,14 +210,14 @@ class Solution { 示例 1: -``` +```plain 输入: "A man, a plan, a canal: Panama" 输出: true ``` 示例 2: -``` +```plain 输入: "race a car" 输出: false ``` @@ -254,7 +254,7 @@ class Solution { 示例 1: -``` +```plain 输入: "babad" 输出: "bab" 注意: "aba"也是一个有效答案。 @@ -262,7 +262,7 @@ class Solution { 示例 2: -``` +```plain 输入: "cbbd" 输出: "bb" ``` @@ -307,7 +307,7 @@ class Solution { 示例 1: -``` +```plain 输入: "bbbab" 输出: @@ -318,7 +318,7 @@ class Solution { 示例 2: -``` +```plain 输入: "cbbd" 输出: @@ -327,7 +327,7 @@ class Solution { 一个可能的最长回文子序列为 "bb"。 -**动态规划:** dp[i][j] = dp[i+1][j-1] + 2 if s.charAt(i) == s.charAt(j) otherwise, dp[i][j] = Math.max(dp[i+1][j], dp[i][j-1]) +**动态规划:** `dp[i][j] = dp[i+1][j-1] + 2 if s.charAt(i) == s.charAt(j) otherwise, dp[i][j] = Math.max(dp[i+1][j], dp[i][j-1])` ```java class Solution { @@ -357,17 +357,17 @@ class Solution { > 2. 如果"X"和"Y"都是合法的括号匹配序列,"XY"也是一个合法的括号匹配序列 > 3. 如果"X"是一个合法的括号匹配序列,那么"(X)"也是一个合法的括号匹配序列 > 4. 每个合法的括号序列都可以由以上规则生成。 - +> > 例如: "","()","()()","((()))"都是合法的括号序列 > 对于一个合法的括号序列我们又有以下定义它的深度: > -> 1. 空串""的深度是 0 -> 2. 如果字符串"X"的深度是 x,字符串"Y"的深度是 y,那么字符串"XY"的深度为 max(x,y) -> 3. 如果"X"的深度是 x,那么字符串"(X)"的深度是 x+1 - +> 1. 空串""的深度是 0 +> 2. 如果字符串"X"的深度是 x,字符串"Y"的深度是 y,那么字符串"XY"的深度为 max(x,y) +> 3. 如果"X"的深度是 x,那么字符串"(X)"的深度是 x+1 +> > 例如: "()()()"的深度是 1,"((()))"的深度是 3。牛牛现在给你一个合法的括号序列,需要你计算出其深度。 -``` +```plain 输入描述: 输入包括一个合法的括号序列s,s长度length(2 ≤ length ≤ 50),序列中只包含'('和')'。 @@ -377,7 +377,7 @@ class Solution { 示例: -``` +```plain 输入: (()) 输出: @@ -459,3 +459,5 @@ public class Main { } ``` + + diff --git a/docs/cs-basics/algorithms/the-sword-refers-to-offer.md b/docs/cs-basics/algorithms/the-sword-refers-to-offer.md index 182bc13863a..73d296d0dc3 100644 --- a/docs/cs-basics/algorithms/the-sword-refers-to-offer.md +++ b/docs/cs-basics/algorithms/the-sword-refers-to-offer.md @@ -74,7 +74,7 @@ public int Fibonacci(int n) { **所以这道题其实就是斐波那契数列的问题。** -代码只需要在上一题的代码稍做修改即可。和上一题唯一不同的就是这一题的初始元素变为 1 2 3 5 8.....而上一题为 1 1 2 3 5 .......。另外这一题也可以用递归做,但是递归效率太低,所以我这里只给出了迭代方式的代码。 +代码只需要在上一题的代码稍做修改即可。和上一题唯一不同的就是这一题的初始元素变为 1 2 3 5 8……而上一题为 1 1 2 3 5 ……。另外这一题也可以用递归做,但是递归效率太低,所以我这里只给出了迭代方式的代码。 **示例代码:** @@ -110,7 +110,7 @@ int jumpFloor(int number) { 假设 n>=2,第一步有 n 种跳法:跳 1 级、跳 2 级、到跳 n 级 跳 1 级,剩下 n-1 级,则剩下跳法是 f(n-1) 跳 2 级,剩下 n-2 级,则剩下跳法是 f(n-2) -...... +…… 跳 n-1 级,剩下 1 级,则剩下跳法是 f(1) 跳 n 级,剩下 0 级,则剩下跳法是 f(0) 所以在 n>=2 的情况下: @@ -229,7 +229,7 @@ public String replaceSpace(StringBuffer str) { 这道题算是比较麻烦和难一点的一个了。我这里采用的是**二分幂**思想,当然也可以采用**快速幂**。 更具剑指 offer 书中细节,该题的解题思路如下:1.当底数为 0 且指数<0 时,会出现对 0 求倒数的情况,需进行错误处理,设置一个全局变量; 2.判断底数是否等于 0,由于 base 为 double 型,所以不能直接用==判断 3.优化求幂函数(二分幂)。 当 n 为偶数,a^n =(a^n/2)_(a^n/2); -当 n 为奇数,a^n = a^[(n-1)/2] _ a^[(n-1)/2] \* a。时间复杂度 O(logn) +当 n 为奇数,a^n = a^[(n-1)/2]_ a^[(n-1)/2] \* a。时间复杂度 O(logn) **时间复杂度**:O(logn) @@ -423,7 +423,7 @@ public class Solution { 思路就是我们根据链表的特点,前一个节点指向下一个节点的特点,把后面的节点移到前面来。 就比如下图:我们把 1 节点和 2 节点互换位置,然后再将 3 节点指向 2 节点,4 节点指向 3 节点,这样以来下面的链表就被反转了。 -![链表](https://p3-juejin.byteimg.com/tos-cn-i-k3u1fbpfcp/844773c7300e4373922bb1a6ae2a55a3~tplv-k3u1fbpfcp-zoom-1.image) +![链表](https://oss.javaguide.cn/p3-juejin/844773c7300e4373922bb1a6ae2a55a3~tplv-k3u1fbpfcp-zoom-1.png) **考察内容:** @@ -573,7 +573,8 @@ public ListNode Merge(ListNode list1,ListNode list2) { **栈:**后进先出(LIFO) **队列:** 先进先出 很明显我们需要根据 JDK 给我们提供的栈的一些基本方法来实现。先来看一下 Stack 类的一些基本方法: -![Stack类的一些常见方法](https://my-blog-to-use.oss-cn-beijing.aliyuncs.com/18-4-4/5985000.jpg) + +![Stack类的一些常见方法](https://oss.javaguide.cn/github/javaguide/cs-basics/algorithms/5985000.jpg) 既然题目给了我们两个栈,我们可以这样考虑当 push 的时候将元素 push 进 stack1,pop 的时候我们先把 stack1 的元素 pop 到 stack2,然后再对 stack2 执行 pop 操作,这样就可以保证是先进先出的。(负[pop]负[pop]得正[先进先出]) @@ -621,10 +622,7 @@ public class Solution { **题目分析:** -这道题想了半天没有思路,参考了 Alias 的答案,他的思路写的也很详细应该很容易看懂。 -作者:Alias -https://www.nowcoder.com/questionTerminal/d77d11405cc7470d82554cb392585106 -来源:牛客网 +这道题想了半天没有思路,参考了 [Alias 的答案](https://www.nowcoder.com/questionTerminal/d77d11405cc7470d82554cb392585106),他的思路写的也很详细应该很容易看懂。 【思路】借用一个辅助的栈,遍历压栈顺序,先讲第一个放入栈中,这里是 1,然后判断栈顶元素是不是出栈顺序的第一个元素,这里是 4,很显然 1≠4,所以我们继续压栈,直到相等以后开始出栈,出栈一个元素,则将出栈顺序向后移动一位,直到不相等,这样循环等压栈顺序遍历完成,如果辅助栈还不为空,说明弹出序列不是该栈的弹出顺序。 @@ -646,7 +644,7 @@ https://www.nowcoder.com/questionTerminal/d77d11405cc7470d82554cb392585106 此时栈顶 5=5,出栈 5,弹出序列向后一位,此时为 3,,辅助栈里面是 1,2,3 -…. +……. 依次执行,最后辅助栈为空。如果不为空说明弹出序列不是该栈的弹出顺序。 **考察内容:** @@ -680,3 +678,5 @@ public class Solution { } } ``` + + diff --git a/docs/cs-basics/data-structure/bloom-filter.md b/docs/cs-basics/data-structure/bloom-filter.md index 1171ed59ffc..be17c1a53aa 100644 --- a/docs/cs-basics/data-structure/bloom-filter.md +++ b/docs/cs-basics/data-structure/bloom-filter.md @@ -5,9 +5,11 @@ tag: - 数据结构 --- -海量数据处理以及缓存穿透这两个场景让我认识了 布隆过滤器 ,我查阅了一些资料来了解它,但是很多现成资料并不满足我的需求,所以就决定自己总结一篇关于布隆过滤器的文章。希望通过这篇文章让更多人了解布隆过滤器,并且会实际去使用它! +布隆过滤器相信大家没用过的话,也已经听过了。 -下面我们将分为几个方面来介绍布隆过滤器: +布隆过滤器主要是为了解决海量数据的存在性问题。对于海量数据中判定某个数据是否存在且容忍轻微误差这一场景(比如缓存穿透、海量数据去重)来说,非常适合。 + +文章内容概览: 1. 什么是布隆过滤器? 2. 布隆过滤器的原理介绍。 @@ -20,11 +22,11 @@ tag: 首先,我们需要了解布隆过滤器的概念。 -布隆过滤器(Bloom Filter)是一个叫做 Bloom 的老哥于 1970 年提出的。我们可以把它看作由二进制向量(或者说位数组)和一系列随机映射函数(哈希函数)两部分组成的数据结构。相比于我们平时常用的的 List、Map、Set 等数据结构,它占用空间更少并且效率更高,但是缺点是其返回的结果是概率性的,而不是非常准确的。理论情况下添加到集合中的元素越多,误报的可能性就越大。并且,存放在布隆过滤器的数据不容易删除。 +布隆过滤器(Bloom Filter,BF)是一个叫做 Bloom 的老哥于 1970 年提出的。我们可以把它看作由二进制向量(或者说位数组)和一系列随机映射函数(哈希函数)两部分组成的数据结构。相比于我们平时常用的 List、Map、Set 等数据结构,它占用空间更少并且效率更高,但是缺点是其返回的结果是概率性的,而不是非常准确的。理论情况下添加到集合中的元素越多,误报的可能性就越大。并且,存放在布隆过滤器的数据不容易删除。 -![布隆过滤器示意图](https://oss.javaguide.cn/github/javaguide/%E5%B8%83%E9%9A%86%E8%BF%87%E6%BB%A4%E5%99%A8-bit%E6%95%B0%E7%BB%84.png) +Bloom Filter 会使用一个较大的 bit 数组来保存所有的数据,数组中的每个元素都只占用 1 bit ,并且每个元素只能是 0 或者 1(代表 false 或者 true),这也是 Bloom Filter 节省内存的核心所在。这样来算的话,申请一个 100w 个元素的位数组只占用 1000000Bit / 8 = 125000 Byte = 125000/1024 KB ≈ 122KB 的空间。 -位数组中的每个元素都只占用 1 bit ,并且每个元素只能是 0 或者 1。这样申请一个 100w 个元素的位数组只占用 1000000Bit / 8 = 125000 Byte = 125000/1024 kb ≈ 122kb 的空间。 +![位数组](https://oss.javaguide.cn/github/javaguide/cs-basics/algorithms/bloom-filter-bit-table.png) 总结:**一个名叫 Bloom 的人提出了一种来检索元素是否在给定大集合中的数据结构,这种数据结构是高效且性能很好的,但缺点是具有一定的错误识别率和删除难度。并且,理论情况下,添加到集合中的元素越多,误报的可能性就越大。** @@ -40,9 +42,9 @@ tag: 1. 对给定元素再次进行相同的哈希计算; 2. 得到值之后判断位数组中的每个元素是否都为 1,如果值都为 1,那么说明这个值在布隆过滤器中,如果存在一个值不为 1,说明该元素不在布隆过滤器中。 -举个简单的例子: +Bloom Filter 的简单原理图如下: -![布隆过滤器hash计算](https://oss.javaguide.cn/github/javaguide/%E5%B8%83%E9%9A%86%E8%BF%87%E6%BB%A4%E5%99%A8-hash%E8%BF%90%E7%AE%97.png) +![Bloom Filter 的简单原理示意图](https://oss.javaguide.cn/github/javaguide/cs-basics/algorithms/bloom-filter-simple-schematic-diagram.png) 如图所示,当字符串存储要加入到布隆过滤器中时,该字符串首先由多个哈希函数生成不同的哈希值,然后将对应的位数组的下标设置为 1(当位数组初始化时,所有位置均为 0)。当第二次存储相同字符串时,因为先前的对应位置已设置为 1,所以很容易知道此值已经存在(去重非常方便)。 @@ -54,8 +56,10 @@ tag: ## 布隆过滤器使用场景 -1. 判断给定数据是否存在:比如判断一个数字是否存在于包含大量数字的数字集中(数字集很大,5 亿以上!)、 防止缓存穿透(判断请求的数据是否有效避免直接绕过缓存请求数据库)等等、邮箱的垃圾邮件过滤、黑名单功能等等。 -2. 去重:比如爬给定网址的时候对已经爬取过的 URL 去重。 +1. 判断给定数据是否存在:比如判断一个数字是否存在于包含大量数字的数字集中(数字集很大,上亿)、 防止缓存穿透(判断请求的数据是否有效避免直接绕过缓存请求数据库)等等、邮箱的垃圾邮件过滤(判断一个邮件地址是否在垃圾邮件列表中)、黑名单功能(判断一个 IP 地址或手机号码是否在黑名单中)等等。 +2. 去重:比如爬给定网址的时候对已经爬取过的 URL 去重、对巨量的 QQ 号/订单号去重。 + +去重场景也需要用到判断给定数据是否存在,因此布隆过滤器主要是为了解决海量数据的存在性问题。 ## 编码实战 @@ -144,7 +148,7 @@ public class MyBloomFilter { */ public int hash(Object value) { int h; - return (value == null) ? 0 : Math.abs(seed * (cap - 1) & ((h = value.hashCode()) ^ (h >>> 16))); + return (value == null) ? 0 : Math.abs((cap - 1) & seed * ((h = value.hashCode()) ^ (h >>> 16))); } } @@ -167,7 +171,7 @@ System.out.println(filter.contains(value2)); Output: -``` +```plain false false true @@ -239,44 +243,46 @@ System.out.println(filter.mightContain(2)); ### 介绍 -Redis v4.0 之后有了 Module(模块/插件) 功能,Redis Modules 让 Redis 可以使用外部模块扩展其功能 。布隆过滤器就是其中的 Module。详情可以查看 Redis 官方对 Redis Modules 的介绍:https://redis.io/modules +Redis v4.0 之后有了 Module(模块/插件) 功能,Redis Modules 让 Redis 可以使用外部模块扩展其功能 。布隆过滤器就是其中的 Module。详情可以查看 Redis 官方对 Redis Modules 的介绍: -另外,官网推荐了一个 RedisBloom 作为 Redis 布隆过滤器的 Module,地址:https://github.com/RedisBloom/RedisBloom +另外,官网推荐了一个 RedisBloom 作为 Redis 布隆过滤器的 Module,地址: 其他还有: -- redis-lua-scaling-bloom-filter(lua 脚本实现):https://github.com/erikdubbelboer/redis-lua-scaling-bloom-filter -- pyreBloom(Python 中的快速 Redis 布隆过滤器):https://github.com/seomoz/pyreBloom -- ...... +- redis-lua-scaling-bloom-filter(lua 脚本实现): +- pyreBloom(Python 中的快速 Redis 布隆过滤器): +- …… RedisBloom 提供了多种语言的客户端支持,包括:Python、Java、JavaScript 和 PHP。 ### 使用 Docker 安装 -如果我们需要体验 Redis 中的布隆过滤器非常简单,通过 Docker 就可以了!我们直接在 Google 搜索 **docker redis bloomfilter** 然后在排除广告的第一条搜素结果就找到了我们想要的答案(这是我平常解决问题的一种方式,分享一下),具体地址:https://hub.docker.com/r/redislabs/rebloom/ (介绍的很详细 )。 +如果我们需要体验 Redis 中的布隆过滤器非常简单,通过 Docker 就可以了!我们直接在 Google 搜索 **docker redis bloomfilter** 然后在排除广告的第一条搜素结果就找到了我们想要的答案(这是我平常解决问题的一种方式,分享一下),具体地址: (介绍的很详细 )。 **具体操作如下:** -``` +```bash ➜ ~ docker run -p 6379:6379 --name redis-redisbloom redislabs/rebloom:latest ➜ ~ docker exec -it redis-redisbloom bash root@21396d02c252:/data# redis-cli 127.0.0.1:6379> ``` +**注意:当前 rebloom 镜像已经被废弃,官方推荐使用[redis-stack](https://hub.docker.com/r/redis/redis-stack)** + ### 常用命令一览 > 注意:key : 布隆过滤器的名称,item : 添加的元素。 -1. **`BF.ADD`**:将元素添加到布隆过滤器中,如果该过滤器尚不存在,则创建该过滤器。格式:`BF.ADD {key} {item}`。 -2. **`BF.MADD`** : 将一个或多个元素添加到“布隆过滤器”中,并创建一个尚不存在的过滤器。该命令的操作方式`BF.ADD`与之相同,只不过它允许多个输入并返回多个值。格式:`BF.MADD {key} {item} [item ...]` 。 -3. **`BF.EXISTS`** : 确定元素是否在布隆过滤器中存在。格式:`BF.EXISTS {key} {item}`。 -4. **`BF.MEXISTS`**:确定一个或者多个元素是否在布隆过滤器中存在格式:`BF.MEXISTS {key} {item} [item ...]`。 +1. `BF.ADD`:将元素添加到布隆过滤器中,如果该过滤器尚不存在,则创建该过滤器。格式:`BF.ADD {key} {item}`。 +2. `BF.MADD` : 将一个或多个元素添加到“布隆过滤器”中,并创建一个尚不存在的过滤器。该命令的操作方式`BF.ADD`与之相同,只不过它允许多个输入并返回多个值。格式:`BF.MADD {key} {item} [item ...]` 。 +3. `BF.EXISTS` : 确定元素是否在布隆过滤器中存在。格式:`BF.EXISTS {key} {item}`。 +4. `BF.MEXISTS`:确定一个或者多个元素是否在布隆过滤器中存在格式:`BF.MEXISTS {key} {item} [item ...]`。 -另外, `BF. RESERVE` 命令需要单独介绍一下: +另外, `BF.RESERVE` 命令需要单独介绍一下: 这个命令的格式如下: -`BF. RESERVE {key} {error_rate} {capacity} [EXPANSION expansion]` 。 +`BF.RESERVE {key} {error_rate} {capacity} [EXPANSION expansion]` 。 下面简单介绍一下每个参数的具体含义: @@ -302,3 +308,5 @@ root@21396d02c252:/data# redis-cli 127.0.0.1:6379> BF.EXISTS myFilter github (integer) 0 ``` + + diff --git a/docs/cs-basics/data-structure/graph.md b/docs/cs-basics/data-structure/graph.md index 7c5db08038a..e9860c240d5 100644 --- a/docs/cs-basics/data-structure/graph.md +++ b/docs/cs-basics/data-structure/graph.md @@ -156,3 +156,5 @@ tag: **第 6 步:** ![深度优先搜索6](https://oss.javaguide.cn/github/javaguide/cs-basics/data-structure/depth-first-search6.png) + + diff --git a/docs/cs-basics/data-structure/heap.md b/docs/cs-basics/data-structure/heap.md index ea77e70d838..5de2e5f2ee2 100644 --- a/docs/cs-basics/data-structure/heap.md +++ b/docs/cs-basics/data-structure/heap.md @@ -33,7 +33,7 @@ tag: 有小伙伴可能会想到用有序数组,初始化一个有序数组时间复杂度是 `O(nlog(n))`,查找最大值或者最小值时间复杂度都是 `O(1)`,但是,涉及到更新(插入或删除)数据时,时间复杂度为 `O(n)`,即使是使用复杂度为 `O(log(n))` 的二分法找到要插入或者删除的数据,在移动数据时也需要 `O(n)` 的时间复杂度。 -**相对于有序数组而言,堆的主要优势在于插入和删除数据效率较高。** 因为堆是基于完全二叉树实现的,所以在插入和删除数据时,只需要在二叉树中上下移动节点,时间复杂度为 `O(log(n))`,相比有序数组的 `O(n)`,效率更高。而最大值或最小值的获取,则是堆的另一个优势,时间复杂度为 `O(1)`,相比有序数组的 `O(log(n))`,更快速。 +**相对于有序数组而言,堆的主要优势在于插入和删除数据效率较高。** 因为堆是基于完全二叉树实现的,所以在插入和删除数据时,只需要在二叉树中上下移动节点,时间复杂度为 `O(log(n))`,相比有序数组的 `O(n)`,效率更高。 不过,需要注意的是:Heap 初始化的时间复杂度为 `O(n)`,而非`O(nlogn)`。 @@ -198,3 +198,5 @@ tag: ![堆排序6](./pictures/堆/堆排序6.png) 堆排序完成! + + diff --git a/docs/cs-basics/data-structure/linear-data-structure.md b/docs/cs-basics/data-structure/linear-data-structure.md index 7d7e8cc1020..e8ae63a19d5 100644 --- a/docs/cs-basics/data-structure/linear-data-structure.md +++ b/docs/cs-basics/data-structure/linear-data-structure.md @@ -97,9 +97,9 @@ tag: 插入删除:O(1)//顶端插入和删除元素 ``` -![栈](https://my-blog-to-use.oss-cn-beijing.aliyuncs.com/2019-6/栈.png) +![栈](https://oss.javaguide.cn/github/javaguide/cs-basics/data-structure/%E6%A0%88.png) -### 3.2. 栈的常见应用常见应用场景 +### 3.2. 栈的常见应用场景 当我们我们要处理的数据只涉及在一端插入和删除数据,并且满足 **后进先出(LIFO, Last In First Out)** 的特性时,我们就可以使用栈这个数据结构。 @@ -107,7 +107,7 @@ tag: 我们只需要使用两个栈(Stack1 和 Stack2)和就能实现这个功能。比如你按顺序查看了 1,2,3,4 这四个页面,我们依次把 1,2,3,4 这四个页面压入 Stack1 中。当你想回头看 2 这个页面的时候,你点击回退按钮,我们依次把 4,3 这两个页面从 Stack1 弹出,然后压入 Stack2 中。假如你又想回到页面 3,你点击前进按钮,我们将 3 页面从 Stack2 弹出,然后压入到 Stack1 中。示例图如下: -![栈实现浏览器倒退和前进](https://my-blog-to-use.oss-cn-beijing.aliyuncs.com/2019-6/栈实现浏览器倒退和前进.png) +![栈实现浏览器倒退和前进](https://oss.javaguide.cn/github/javaguide/cs-basics/data-structure/%E6%A0%88%E5%AE%9E%E7%8E%B0%E6%B5%8F%E8%A7%88%E5%99%A8%E5%80%92%E9%80%80%E5%92%8C%E5%89%8D%E8%BF%9B.png) #### 3.2.2. 检查符号是否成对出现 @@ -115,8 +115,8 @@ tag: > > 有效字符串需满足: > -> 1. 左括号必须用相同类型的右括号闭合。 -> 2. 左括号必须以正确的顺序闭合。 +> 1. 左括号必须用相同类型的右括号闭合。 +> 2. 左括号必须以正确的顺序闭合。 > > 比如 "()"、"()[]{}"、"{[]}" 都是有效字符串,而 "(]"、"([)]" 则不是。 @@ -154,7 +154,12 @@ public boolean isValid(String s){ #### 3.2.4. 维护函数调用 -最后一个被调用的函数必须先完成执行,符合栈的 **后进先出(LIFO, Last In First Out)** 特性。 +最后一个被调用的函数必须先完成执行,符合栈的 **后进先出(LIFO, Last In First Out)** 特性。 +例如递归函数调用可以通过栈来实现,每次递归调用都会将参数和返回地址压栈。 + +#### 3.2.5 深度优先遍历(DFS) + +在深度优先搜索过程中,栈被用来保存搜索路径,以便回溯到上一层。 ### 3.3. 栈的实现 @@ -295,15 +300,38 @@ myStack.pop();//报错:java.lang.IllegalArgumentException: Stack is empty. 顺序队列中,我们说 `front==rear` 的时候队列为空,循环队列中则不一样,也可能为满,如上图所示。解决办法有两种: 1. 可以设置一个标志变量 `flag`,当 `front==rear` 并且 `flag=0` 的时候队列为空,当`front==rear` 并且 `flag=1` 的时候队列为满。 -2. 队列为空的时候就是 `front==rear` ,队列满的时候,我们保证数组还有一个空闲的位置,rear 就指向这个空闲位置,如下图所示,那么现在判断队列是否为满的条件就是:`(rear+1) % QueueSize= front` 。 +2. 队列为空的时候就是 `front==rear` ,队列满的时候,我们保证数组还有一个空闲的位置,rear 就指向这个空闲位置,如下图所示,那么现在判断队列是否为满的条件就是:`(rear+1) % QueueSize==front` 。 + +#### 4.2.3 双端队列 + +**双端队列 (Deque)** 是一种在队列的两端都可以进行插入和删除操作的队列,相比单队列来说更加灵活。 + +一般来说,我们可以对双端队列进行 `addFirst`、`addLast`、`removeFirst` 和 `removeLast` 操作。 + +#### 4.2.4 优先队列 -### 4.3. 常见应用场景 +**优先队列 (Priority Queue)** 从底层结构上来讲并非线性的数据结构,它一般是由堆来实现的。 + +1. 在每个元素入队时,优先队列会将新元素其插入堆中并调整堆。 +2. 在队头出队时,优先队列会返回堆顶元素并调整堆。 + +关于堆的具体实现可以看[堆](https://javaguide.cn/cs-basics/data-structure/heap.html)这一节。 + +总而言之,不论我们进行什么操作,优先队列都能按照**某种排序方式**进行一系列堆的相关操作,从而保证整个集合的**有序性**。 + +虽然优先队列的底层并非严格的线性结构,但是在我们使用的过程中,我们是感知不到**堆**的,从使用者的眼中优先队列可以被认为是一种线性的数据结构:一种会自动排序的线性队列。 + +### 4.3. 队列的常见应用场景 当我们需要按照一定顺序来处理数据的时候可以考虑使用队列这个数据结构。 - **阻塞队列:** 阻塞队列可以看成在队列基础上加了阻塞操作的队列。当队列为空的时候,出队操作阻塞,当队列满的时候,入队操作阻塞。使用阻塞队列我们可以很容易实现“生产者 - 消费者“模型。 -- **线程池中的请求/任务队列:** 线程池中没有空闲线程时,新的任务请求线程资源时,线程池该如何处理呢?答案是将这些请求放在队列中,当有空闲线程的时候,会循环中反复从队列中获取任务来执行。队列分为无界队列(基于链表)和有界队列(基于数组)。无界队列的特点就是可以一直入列,除非系统资源耗尽,比如:`FixedThreadPool` 使用无界队列 `LinkedBlockingQueue`。但是有界队列就不一样了,当队列满的话后面再有任务/请求就会拒绝,在 Java 中的体现就是会抛出`java.util.concurrent.RejectedExecutionException` 异常。 +- **线程池中的请求/任务队列:** 当线程池中没有空闲线程时,新的任务请求线程资源会被如何处理呢?答案是这些任务会被放入任务队列中,等待线程池中的线程空闲后再从队列中取出任务执行。任务队列分为无界队列(基于链表实现)和有界队列(基于数组实现)。无界队列的特点是队列容量理论上没有限制,任务可以持续入队,直到系统资源耗尽。例如:`FixedThreadPool` 使用的阻塞队列 `LinkedBlockingQueue`,其默认容量为 `Integer.MAX_VALUE`,因此可以被视为“无界队列”。而有界队列则不同,当队列已满时,如果再有新任务提交,由于队列无法继续容纳任务,线程池会拒绝这些任务,并抛出 `java.util.concurrent.RejectedExecutionException` 异常。 +- **栈**:双端队列天生便可以实现栈的全部功能(`push`、`pop` 和 `peek`),并且在 Deque 接口中已经实现了相关方法。Stack 类已经和 Vector 一样被遗弃,现在在 Java 中普遍使用双端队列(Deque)来实现栈。 +- **广度优先搜索(BFS)**:在图的广度优先搜索过程中,队列被用于存储待访问的节点,保证按照层次顺序遍历图的节点。 - Linux 内核进程队列(按优先级排队) - 现实生活中的派对,播放器上的播放列表; - 消息队列 -- 等等...... +- 等等…… + + diff --git "a/docs/cs-basics/data-structure/pictures/\347\272\242\351\273\221\346\240\221/\347\272\242\351\273\221\346\240\2211.PNG" "b/docs/cs-basics/data-structure/pictures/\347\272\242\351\273\221\346\240\221/\347\272\242\351\273\221\346\240\2211.PNG" new file mode 100644 index 00000000000..4792fdfddd0 Binary files /dev/null and "b/docs/cs-basics/data-structure/pictures/\347\272\242\351\273\221\346\240\221/\347\272\242\351\273\221\346\240\2211.PNG" differ diff --git "a/docs/cs-basics/data-structure/pictures/\347\272\242\351\273\221\346\240\221/\347\272\242\351\273\221\346\240\2211.png" "b/docs/cs-basics/data-structure/pictures/\347\272\242\351\273\221\346\240\221/\347\272\242\351\273\221\346\240\2211.png" new file mode 100644 index 00000000000..4792fdfddd0 Binary files /dev/null and "b/docs/cs-basics/data-structure/pictures/\347\272\242\351\273\221\346\240\221/\347\272\242\351\273\221\346\240\2211.png" differ diff --git "a/docs/cs-basics/data-structure/pictures/\347\272\242\351\273\221\346\240\221/\347\272\242\351\273\221\346\240\2212.PNG" "b/docs/cs-basics/data-structure/pictures/\347\272\242\351\273\221\346\240\221/\347\272\242\351\273\221\346\240\2212.PNG" new file mode 100644 index 00000000000..a29fbf3c775 Binary files /dev/null and "b/docs/cs-basics/data-structure/pictures/\347\272\242\351\273\221\346\240\221/\347\272\242\351\273\221\346\240\2212.PNG" differ diff --git "a/docs/cs-basics/data-structure/pictures/\347\272\242\351\273\221\346\240\221/\347\272\242\351\273\221\346\240\2212.png" "b/docs/cs-basics/data-structure/pictures/\347\272\242\351\273\221\346\240\221/\347\272\242\351\273\221\346\240\2212.png" new file mode 100644 index 00000000000..a29fbf3c775 Binary files /dev/null and "b/docs/cs-basics/data-structure/pictures/\347\272\242\351\273\221\346\240\221/\347\272\242\351\273\221\346\240\2212.png" differ diff --git "a/docs/cs-basics/data-structure/pictures/\347\272\242\351\273\221\346\240\221/\347\272\242\351\273\221\346\240\2213.PNG" "b/docs/cs-basics/data-structure/pictures/\347\272\242\351\273\221\346\240\221/\347\272\242\351\273\221\346\240\2213.PNG" new file mode 100644 index 00000000000..74c3b7ded69 Binary files /dev/null and "b/docs/cs-basics/data-structure/pictures/\347\272\242\351\273\221\346\240\221/\347\272\242\351\273\221\346\240\2213.PNG" differ diff --git "a/docs/cs-basics/data-structure/pictures/\347\272\242\351\273\221\346\240\221/\347\272\242\351\273\221\346\240\2213.png" "b/docs/cs-basics/data-structure/pictures/\347\272\242\351\273\221\346\240\221/\347\272\242\351\273\221\346\240\2213.png" new file mode 100644 index 00000000000..74c3b7ded69 Binary files /dev/null and "b/docs/cs-basics/data-structure/pictures/\347\272\242\351\273\221\346\240\221/\347\272\242\351\273\221\346\240\2213.png" differ diff --git "a/docs/cs-basics/data-structure/pictures/\347\272\242\351\273\221\346\240\221/\347\272\242\351\273\221\346\240\2214.PNG" "b/docs/cs-basics/data-structure/pictures/\347\272\242\351\273\221\346\240\221/\347\272\242\351\273\221\346\240\2214.PNG" new file mode 100644 index 00000000000..6092109de5d Binary files /dev/null and "b/docs/cs-basics/data-structure/pictures/\347\272\242\351\273\221\346\240\221/\347\272\242\351\273\221\346\240\2214.PNG" differ diff --git "a/docs/cs-basics/data-structure/pictures/\347\272\242\351\273\221\346\240\221/\347\272\242\351\273\221\346\240\2214.png" "b/docs/cs-basics/data-structure/pictures/\347\272\242\351\273\221\346\240\221/\347\272\242\351\273\221\346\240\2214.png" new file mode 100644 index 00000000000..6092109de5d Binary files /dev/null and "b/docs/cs-basics/data-structure/pictures/\347\272\242\351\273\221\346\240\221/\347\272\242\351\273\221\346\240\2214.png" differ diff --git "a/docs/cs-basics/data-structure/pictures/\347\272\242\351\273\221\346\240\221/\347\272\242\351\273\221\346\240\2215.PNG" "b/docs/cs-basics/data-structure/pictures/\347\272\242\351\273\221\346\240\221/\347\272\242\351\273\221\346\240\2215.PNG" new file mode 100644 index 00000000000..15e457f412e Binary files /dev/null and "b/docs/cs-basics/data-structure/pictures/\347\272\242\351\273\221\346\240\221/\347\272\242\351\273\221\346\240\2215.PNG" differ diff --git "a/docs/cs-basics/data-structure/pictures/\347\272\242\351\273\221\346\240\221/\347\272\242\351\273\221\346\240\2215.png" "b/docs/cs-basics/data-structure/pictures/\347\272\242\351\273\221\346\240\221/\347\272\242\351\273\221\346\240\2215.png" new file mode 100644 index 00000000000..15e457f412e Binary files /dev/null and "b/docs/cs-basics/data-structure/pictures/\347\272\242\351\273\221\346\240\221/\347\272\242\351\273\221\346\240\2215.png" differ diff --git "a/docs/cs-basics/data-structure/pictures/\347\272\242\351\273\221\346\240\221/\347\272\242\351\273\221\346\240\2216.PNG" "b/docs/cs-basics/data-structure/pictures/\347\272\242\351\273\221\346\240\221/\347\272\242\351\273\221\346\240\2216.PNG" new file mode 100644 index 00000000000..539579a9da4 Binary files /dev/null and "b/docs/cs-basics/data-structure/pictures/\347\272\242\351\273\221\346\240\221/\347\272\242\351\273\221\346\240\2216.PNG" differ diff --git "a/docs/cs-basics/data-structure/pictures/\347\272\242\351\273\221\346\240\221/\347\272\242\351\273\221\346\240\2216.png" "b/docs/cs-basics/data-structure/pictures/\347\272\242\351\273\221\346\240\221/\347\272\242\351\273\221\346\240\2216.png" new file mode 100644 index 00000000000..539579a9da4 Binary files /dev/null and "b/docs/cs-basics/data-structure/pictures/\347\272\242\351\273\221\346\240\221/\347\272\242\351\273\221\346\240\2216.png" differ diff --git a/docs/cs-basics/data-structure/red-black-tree.md b/docs/cs-basics/data-structure/red-black-tree.md index 11043f45d0c..462010e910e 100644 --- a/docs/cs-basics/data-structure/red-black-tree.md +++ b/docs/cs-basics/data-structure/red-black-tree.md @@ -1,21 +1,93 @@ --- +title: 红黑树 category: 计算机基础 tag: - 数据结构 --- -# 红黑树 +## 红黑树介绍 -**红黑树特点** : +红黑树(Red Black Tree)是一种自平衡二叉查找树。它是在 1972 年由 Rudolf Bayer 发明的,当时被称为平衡二叉 B 树(symmetric binary B-trees)。后来,在 1978 年被 Leo J. Guibas 和 Robert Sedgewick 修改为如今的“红黑树”。 -1. 每个节点非红即黑; -2. 根节点总是黑色的; -3. 每个叶子节点都是黑色的空节点(NIL 节点); -4. 如果节点是红色的,则它的子节点必须是黑色的(反之不一定); -5. 从根节点到叶节点或空子节点的每条路径,必须包含相同数目的黑色节点(即相同的黑色高度)。 +由于其自平衡的特性,保证了最坏情形下在 O(logn) 时间复杂度内完成查找、增加、删除等操作,性能表现稳定。 -**红黑树的应用**:TreeMap、TreeSet 以及 JDK1.8 的 HashMap 底层都用到了红黑树。 +在 JDK 中,`TreeMap`、`TreeSet` 以及 JDK1.8 的 `HashMap` 底层都用到了红黑树。 -**为什么要用红黑树?** 简单来说红黑树就是为了解决二叉查找树的缺陷,因为二叉查找树在某些情况下会退化成一个线性结构。详细了解可以查看 [漫画:什么是红黑树?](https://juejin.im/post/5a27c6946fb9a04509096248#comment)(也介绍到了二叉查找树,非常推荐) +## 为什么需要红黑树? -**相关阅读**:[《红黑树深入剖析及 Java 实现》](https://zhuanlan.zhihu.com/p/24367771)(美团点评技术团队) +红黑树的诞生就是为了解决二叉查找树的缺陷。 + +二叉查找树是一种基于比较的数据结构,它的每个节点都有一个键值,而且左子节点的键值小于父节点的键值,右子节点的键值大于父节点的键值。这样的结构可以方便地进行查找、插入和删除操作,因为只需要比较节点的键值就可以确定目标节点的位置。但是,二叉查找树有一个很大的问题,就是它的形状取决于节点插入的顺序。如果节点是按照升序或降序的方式插入的,那么二叉查找树就会退化成一个线性结构,也就是一个链表。这样的情况下,二叉查找树的性能就会大大降低,时间复杂度就会从 O(logn) 变为 O(n)。 + +红黑树的诞生就是为了解决二叉查找树的缺陷,因为二叉查找树在某些情况下会退化成一个线性结构。 + +## **红黑树特点** + +1. 每个节点非红即黑。黑色决定平衡,红色不决定平衡。这对应了 2-3 树中一个节点内可以存放 1~2 个节点。 +2. 根节点总是黑色的。 +3. 每个叶子节点都是黑色的空节点(NIL 节点)。这里指的是红黑树都会有一个空的叶子节点,是红黑树自己的规则。 +4. 如果节点是红色的,则它的子节点必须是黑色的(反之不一定)。通常这条规则也叫不会有连续的红色节点。一个节点最多临时会有 3 个子节点,中间是黑色节点,左右是红色节点。 +5. 从任意节点到它的叶子节点或空子节点的每条路径,必须包含相同数目的黑色节点(即相同的黑色高度)。每一层都只是有一个节点贡献了树高决定平衡性,也就是对应红黑树中的黑色节点。 + +正是这些特点才保证了红黑树的平衡,让红黑树的高度不会超过 2log(n+1)。 + +## 红黑树数据结构 + +建立在 BST 二叉搜索树的基础上,AVL、2-3 树、红黑树都是自平衡二叉树(统称 B-树)。但相比于 AVL 树,高度平衡所带来的时间复杂度,红黑树对平衡的控制要宽松一些,红黑树只需要保证黑色节点平衡即可。 + +## 红黑树结构实现 + +```java +public class Node { + + public Class clazz; + public Integer value; + public Node parent; + public Node left; + public Node right; + + // AVL 树所需属性 + public int height; + // 红黑树所需属性 + public Color color = Color.RED; + +} +``` + +### 1.左倾染色 + +![幻灯片1](./pictures/红黑树/红黑树1.png) + +- 染色时根据当前节点的爷爷节点,找到当前节点的叔叔节点。 +- 再把父节点染黑、叔叔节点染黑,爷爷节点染红。但爷爷节点染红是临时的,当平衡树高操作后会把根节点染黑。 + +### 2.右倾染色 + +![幻灯片2](./pictures/红黑树/红黑树2.png) + +### 3.左旋调衡 + +#### 3.1 一次左旋 + +![幻灯片3](./pictures/红黑树/红黑树3.png) + +#### 3.2 右旋+左旋 + +![幻灯片4](./pictures/红黑树/红黑树4.png) + +### 4.右旋调衡 + +#### 4.1 一次右旋 + +![幻灯片5](./pictures/红黑树/红黑树5.png) + +#### 4.2 左旋+右旋 + +![幻灯片6](./pictures/红黑树/红黑树6.png) + +## 文章推荐 + +- [《红黑树深入剖析及 Java 实现》 - 美团点评技术团队](https://zhuanlan.zhihu.com/p/24367771) +- [漫画:什么是红黑树? - 程序员小灰](https://juejin.im/post/5a27c6946fb9a04509096248#comment)(也介绍到了二叉查找树,非常推荐) + + diff --git a/docs/cs-basics/data-structure/tree.md b/docs/cs-basics/data-structure/tree.md index eb9d9964857..de9c6eb6a27 100644 --- a/docs/cs-basics/data-structure/tree.md +++ b/docs/cs-basics/data-structure/tree.md @@ -15,7 +15,7 @@ tag: 下图就是一颗树,并且是一颗二叉树。 -![二叉树](https://my-blog-to-use.oss-cn-beijing.aliyuncs.com/2019-6/二叉树-2.png) +![二叉树](https://oss.javaguide.cn/github/javaguide/cs-basics/data-structure/%E4%BA%8C%E5%8F%89%E6%A0%91-2.png) 如上图所示,通过上面这张图说明一下树中的常用概念: @@ -50,7 +50,7 @@ tag: ### 完全二叉树 -除最后一层外,若其余层都是满的,并且最后一层或者是满的,或者是在右边缺少连续若干节点,则这个二叉树就是 **完全二叉树** 。 +除最后一层外,若其余层都是满的,并且最后一层是满的或者是在右边缺少连续若干节点,则这个二叉树就是 **完全二叉树** 。 大家可以想象为一棵树从根结点开始扩展,扩展完左子节点才能开始扩展右子节点,每扩展完一层,才能继续扩展下一层。如下图所示: @@ -176,8 +176,10 @@ public void postOrder(TreeNode root){ if(root == null){ return; } - postOrder(root.left); + postOrder(root.left); postOrder(root.right); system.out.println(root.data); } ``` + + diff --git a/docs/cs-basics/network/application-layer-protocol.md b/docs/cs-basics/network/application-layer-protocol.md index fd8ceec55a1..cb809b9157d 100644 --- a/docs/cs-basics/network/application-layer-protocol.md +++ b/docs/cs-basics/network/application-layer-protocol.md @@ -15,7 +15,35 @@ HTTP 使用客户端-服务器模型,客户端向服务器发送 HTTP Request HTTP 协议基于 TCP 协议,发送 HTTP 请求之前首先要建立 TCP 连接也就是要经历 3 次握手。目前使用的 HTTP 协议大部分都是 1.1。在 1.1 的协议里面,默认是开启了 Keep-Alive 的,这样的话建立的连接就可以在多次请求中被复用了。 -另外, HTTP 协议是”无状态”的协议,它无法记录客户端用户的状态,一般我们都是通过 Session 来记录客户端用户的状态。 +另外, HTTP 协议是“无状态”的协议,它无法记录客户端用户的状态,一般我们都是通过 Session 来记录客户端用户的状态。 + +## Websocket:全双工通信协议 + +WebSocket 是一种基于 TCP 连接的全双工通信协议,即客户端和服务器可以同时发送和接收数据。 + +WebSocket 协议在 2008 年诞生,2011 年成为国际标准,几乎所有主流较新版本的浏览器都支持该协议。不过,WebSocket 不只能在基于浏览器的应用程序中使用,很多编程语言、框架和服务器都提供了 WebSocket 支持。 + +WebSocket 协议本质上是应用层的协议,用于弥补 HTTP 协议在持久通信能力上的不足。客户端和服务器仅需一次握手,两者之间就直接可以创建持久性的连接,并进行双向数据传输。 + +![Websocket 示意图](https://oss.javaguide.cn/github/javaguide/system-design/web-real-time-message-push/1460000042192394.png) + +下面是 WebSocket 的常见应用场景: + +- 视频弹幕 +- 实时消息推送,详见[Web 实时消息推送详解](https://javaguide.cn/system-design/web-real-time-message-push.html)这篇文章 +- 实时游戏对战 +- 多用户协同编辑 +- 社交聊天 +- …… + +WebSocket 的工作过程可以分为以下几个步骤: + +1. 客户端向服务器发送一个 HTTP 请求,请求头中包含 `Upgrade: websocket` 和 `Sec-WebSocket-Key` 等字段,表示要求升级协议为 WebSocket; +2. 服务器收到这个请求后,会进行升级协议的操作,如果支持 WebSocket,它将回复一个 HTTP 101 状态码,响应头中包含 ,`Connection: Upgrade`和 `Sec-WebSocket-Accept: xxx` 等字段、表示成功升级到 WebSocket 协议。 +3. 客户端和服务器之间建立了一个 WebSocket 连接,可以进行双向的数据传输。数据以帧(frames)的形式进行传送,WebSocket 的每条消息可能会被切分成多个数据帧(最小单位)。发送端会将消息切割成多个帧发送给接收端,接收端接收消息帧,并将关联的帧重新组装成完整的消息。 +4. 客户端或服务器可以主动发送一个关闭帧,表示要断开连接。另一方收到后,也会回复一个关闭帧,然后双方关闭 TCP 连接。 + +另外,建立 WebSocket 连接之后,通过心跳机制来保持 WebSocket 连接的稳定性和活跃性。 ## SMTP:简单邮件传输(发送)协议 @@ -32,11 +60,11 @@ SMTP 协议这块涉及的内容比较多,下面这两个问题比较重要: **电子邮件的发送过程?** -比如我的邮箱是“dabai@cszhinan.com”,我要向“xiaoma@qq.com”发送邮件,整个过程可以简单分为下面几步: +比如我的邮箱是“”,我要向“”发送邮件,整个过程可以简单分为下面几步: 1. 通过 **SMTP** 协议,我将我写好的邮件交给 163 邮箱服务器(邮局)。 2. 163 邮箱服务器发现我发送的邮箱是 qq 邮箱,然后它使用 SMTP 协议将我的邮件转发到 qq 邮箱服务器。 -3. qq 邮箱服务器接收邮件之后就通知邮箱为“xiaoma@qq.com”的用户来收邮件,然后用户就通过 **POP3/IMAP** 协议将邮件取出。 +3. qq 邮箱服务器接收邮件之后就通知邮箱为“”的用户来收邮件,然后用户就通过 **POP3/IMAP** 协议将邮件取出。 **如何判断邮箱是真正存在的?** @@ -49,9 +77,9 @@ SMTP 协议这块涉及的内容比较多,下面这两个问题比较重要: 推荐几个在线邮箱是否有效检测工具: -1. https://verify-email.org/ -2. http://tool.chacuo.net/mailverify -3. https://www.emailcamel.com/ +1. +2. +3. ## POP3/IMAP:邮件接收的协议 @@ -74,7 +102,7 @@ FTP 是基于客户—服务器(C/S)模型而设计的,在客户端与 FTP ![FTP工作过程](https://oss.javaguide.cn/github/javaguide/cs-basics/network/ftp.png) -注意 ⚠️:FTP 是一种不安全的协议,因为它在传输过程中不会对数据进行加密。因此,FTP 传输的文件可能会被窃听或篡改。建议在传输敏感数据时使用更安全的协议,如 SFTP(一种基于 SSH 协议的安全文件传输协议,用于在网络上安全地传输文件)。 +注意 ⚠️:FTP 是一种不安全的协议,因为它在传输过程中不会对数据进行加密。因此,FTP 传输的文件可能会被窃听或篡改。建议在传输敏感数据时使用更安全的协议,如 SFTP(SSH File Transfer Protocol,一种基于 SSH 协议的安全文件传输协议,用于在网络上安全地传输文件)。 ## Telnet:远程登陆协议 @@ -86,10 +114,12 @@ FTP 是基于客户—服务器(C/S)模型而设计的,在客户端与 FTP **SSH(Secure Shell)** 基于 TCP 协议,通过加密和认证机制实现安全的访问和文件传输等业务。 -SSH 的经典用途是登录到远程电脑中执行命令。除此之外,SSH 也支持隧道协议、端口映射和 X11 连接。借助 SFTP 或 SCP 协议,SSH 还可以传输文件。 +SSH 的经典用途是登录到远程电脑中执行命令。除此之外,SSH 也支持隧道协议、端口映射和 X11 连接(允许用户在本地运行远程服务器上的图形应用程序)。借助 SFTP(SSH File Transfer Protocol) 或 SCP(Secure Copy Protocol) 协议,SSH 还可以安全传输文件。 SSH 使用客户端-服务器模型,默认端口是 22。SSH 是一个守护进程,负责实时监听客户端请求,并进行处理。大多数现代操作系统都提供了 SSH。 +如下图所示,SSH Client(SSH 客户端)和 SSH Server(SSH 服务器)通过公钥交换生成共享的对称加密密钥,用于后续的加密通信。 + ![SSH:安全的网络传输协议](https://oss.javaguide.cn/github/javaguide/cs-basics/network/ssh-client-server.png) ## RTP:实时传输协议 @@ -110,4 +140,6 @@ DNS(Domain Name System,域名管理系统)基于 UDP 协议,用于解决 ## 参考 - 《计算机网络自顶向下方法》(第七版) -- RTP 协议介绍:https://mthli.xyz/rtp-introduction/ +- RTP 协议介绍: + + diff --git a/docs/cs-basics/network/arp.md b/docs/cs-basics/network/arp.md index f6b38084695..c4ece76011c 100644 --- a/docs/cs-basics/network/arp.md +++ b/docs/cs-basics/network/arp.md @@ -11,9 +11,9 @@ tag: 开始阅读这篇文章之前,你可以先看看下面几个问题: -1. **ARP 协议在协议栈中的位置?** ARP 协议在协议栈中的位置非常重要,在理解了它的工作原理之后,也很难说它到底是网络层协议,还是链路层协议,因为它恰恰串联起了网络层和链路层。国外的大部分教程通常将 ARP 协议放在网络层。 -2. **ARP 协议解决了什么问题,地位如何?** ARP 协议,全称 **地址解析协议(Address Resolution Protocol)**,它解决的是网络层地址和链路层地址之间的转换问题。因为一个 IP 数据报在物理上传输的过程中,总是需要知道下一跳(物理上的下一个目的地)该去往何处,但 IP 地址属于逻辑地址,而 MAC 地址才是物理地址,ARP 协议解决了 IP 地址转 MAC 地址的一些问题。 -3. **ARP 工作原理?** 只希望大家记住几个关键词:**ARP 表、广播问询、单播响应**。 +1. **ARP 协议在协议栈中的位置?** ARP 协议在协议栈中的位置非常重要,在理解了它的工作原理之后,也很难说它到底是网络层协议,还是链路层协议,因为它恰恰串联起了网络层和链路层。国外的大部分教程通常将 ARP 协议放在网络层。 +2. **ARP 协议解决了什么问题,地位如何?** ARP 协议,全称 **地址解析协议(Address Resolution Protocol)**,它解决的是网络层地址和链路层地址之间的转换问题。因为一个 IP 数据报在物理上传输的过程中,总是需要知道下一跳(物理上的下一个目的地)该去往何处,但 IP 地址属于逻辑地址,而 MAC 地址才是物理地址,ARP 协议解决了 IP 地址转 MAC 地址的一些问题。 +3. **ARP 工作原理?** 只希望大家记住几个关键词:**ARP 表、广播问询、单播响应**。 ## MAC 地址 @@ -80,7 +80,7 @@ ARP 的工作原理将分两种场景讨论: 更复杂的情况是,发送主机 A 和接收主机 B 不在同一个子网中,假设一个一般场景,两台主机所在的子网由一台路由器联通。这里需要注意的是,一般情况下,我们说网络设备都有一个 IP 地址和一个 MAC 地址,这里说的网络设备,更严谨的说法应该是一个接口。路由器作为互联设备,具有多个接口,每个接口同样也应该具备不重复的 IP 地址和 MAC 地址。因此,在讨论 ARP 表时,路由器的多个接口都各自维护一个 ARP 表,而非一个路由器只维护一个 ARP 表。 -接下来,回顾同一子网内的 MAC 寻址,如果主机 A 发送一个广播问询分组,那么 A 所在子网内的所有设备(接口)都将不会捕获该分组,因为该分组的目的 IP 地址在另一个子网中,本子网内不会有设备成功接收。那么,主机 A 应该发送怎样的查询分组呢?整个过程按照时间顺序发生的事件如下: +接下来,回顾同一子网内的 MAC 寻址,如果主机 A 发送一个广播问询分组,那么 A 所在的子网内所有设备(接口)都将会捕获该分组,因为该分组的目的 IP 与发送主机 A 的 IP 在同一个子网中。但是当目的 IP 与 A 不在同一子网时,A 所在子网内将不会有设备成功接收该分组。那么,主机 A 应该发送怎样的查询分组呢?整个过程按照时间顺序发生的事件如下: 1. 主机 A 查询 ARP 表,期望寻找到目标路由器的本子网接口的 MAC 地址。 @@ -101,3 +101,5 @@ ARP 的工作原理将分两种场景讨论: 7. 路由器接口将对 IP 数据报重新封装成链路层帧,目标 MAC 地址为主机 B 的 MAC 地址,单播发送,直到目的地。 ![](./images/arp/arp_different_lan.png) + + diff --git a/docs/cs-basics/network/computer-network-xiexiren-summary.md b/docs/cs-basics/network/computer-network-xiexiren-summary.md index 9e87de2242a..fc9f60fd39a 100644 --- a/docs/cs-basics/network/computer-network-xiexiren-summary.md +++ b/docs/cs-basics/network/computer-network-xiexiren-summary.md @@ -5,9 +5,9 @@ tag: - 计算机网络 --- -本文是我在大二学习计算机网络期间整理, 大部分内容都来自于谢希仁老师的[《计算机网络》第七版 ](https://www.elias.ltd/usr/local/etc/%E8%AE%A1%E7%AE%97%E6%9C%BA%E7%BD%91%E7%BB%9C%EF%BC%88%E7%AC%AC7%E7%89%88%EF%BC%89%E8%B0%A2%E5%B8%8C%E4%BB%81.pdf)这本书。为了内容更容易理解,我对之前的整理进行了一波重构,并配上了一些相关的示意图便于理解。 +本文是我在大二学习计算机网络期间整理, 大部分内容都来自于谢希仁老师的[《计算机网络》第七版](https://www.elias.ltd/usr/local/etc/%E8%AE%A1%E7%AE%97%E6%9C%BA%E7%BD%91%E7%BB%9C%EF%BC%88%E7%AC%AC7%E7%89%88%EF%BC%89%E8%B0%A2%E5%B8%8C%E4%BB%81.pdf)这本书。为了内容更容易理解,我对之前的整理进行了一波重构,并配上了一些相关的示意图便于理解。 -![](https://p3-juejin.byteimg.com/tos-cn-i-k3u1fbpfcp/fb5d8645cd55484ab0177f25a13e97db~tplv-k3u1fbpfcp-zoom-1.image) +![](https://oss.javaguide.cn/p3-juejin/fb5d8645cd55484ab0177f25a13e97db~tplv-k3u1fbpfcp-zoom-1.png) 相关问题:[如何评价谢希仁的计算机网络(第七版)? - 知乎](https://www.zhihu.com/question/327872966) 。 @@ -20,42 +20,42 @@ tag: 3. **主机(host)**:连接在因特网上的计算机。 4. **ISP(Internet Service Provider)**:因特网服务提供者(提供商)。 -![ISP (Internet Service Provider) Definition](https://p3-juejin.byteimg.com/tos-cn-i-k3u1fbpfcp/e77e26123d404d438d0c5943e3c65893~tplv-k3u1fbpfcp-zoom-1.image) + ![ISP (Internet Service Provider) Definition](https://oss.javaguide.cn/p3-juejin/e77e26123d404d438d0c5943e3c65893~tplv-k3u1fbpfcp-zoom-1.png) 5. **IXP(Internet eXchange Point)**:互联网交换点 IXP 的主要作用就是允许两个网络直接相连并交换分组,而不需要再通过第三个网络来转发分组。 -![IXP Traffic Levels During the Stratos Skydive — RIPE Labs](https://p3-juejin.byteimg.com/tos-cn-i-k3u1fbpfcp/7f9a6ddaa09441ceac11cb77f7a69d8f~tplv-k3u1fbpfcp-zoom-1.image) + ![IXP Traffic Levels During the Stratos Skydive — RIPE Labs](https://oss.javaguide.cn/p3-juejin/7f9a6ddaa09441ceac11cb77f7a69d8f~tplv-k3u1fbpfcp-zoom-1.png) -

https://labs.ripe.net/Members/fergalc/ixp-traffic-during-stratos-skydive

+

https://labs.ripe.net/Members/fergalc/ixp-traffic-during-stratos-skydive

6. **RFC(Request For Comments)**:意思是“请求评议”,包含了关于 Internet 几乎所有的重要的文字资料。 7. **广域网 WAN(Wide Area Network)**:任务是通过长距离运送主机发送的数据。 8. **城域网 MAN(Metropolitan Area Network)**:用来将多个局域网进行互连。 9. **局域网 LAN(Local Area Network)**:学校或企业大多拥有多个互连的局域网。 -![MAN & WMAN | Red de área metropolitana, Redes informaticas, Par trenzado](https://p3-juejin.byteimg.com/tos-cn-i-k3u1fbpfcp/eb48d21b2e984a63a26250010d7adac4~tplv-k3u1fbpfcp-zoom-1.image) + ![MAN & WMAN | Red de área metropolitana, Redes informaticas, Par trenzado](https://oss.javaguide.cn/p3-juejin/eb48d21b2e984a63a26250010d7adac4~tplv-k3u1fbpfcp-zoom-1.png) -

http://conexionesmanwman.blogspot.com/

+

http://conexionesmanwman.blogspot.com/

10. **个人区域网 PAN(Personal Area Network)**:在个人工作的地方把属于个人使用的电子设备用无线技术连接起来的网络 。 -![Advantages and disadvantages of personal area network (PAN) - IT Release](https://p3-juejin.byteimg.com/tos-cn-i-k3u1fbpfcp/54bd7b420388494fbe917e3c9c13f1a7~tplv-k3u1fbpfcp-zoom-1.image) + ![Advantages and disadvantages of personal area network (PAN) - IT Release](https://oss.javaguide.cn/p3-juejin/54bd7b420388494fbe917e3c9c13f1a7~tplv-k3u1fbpfcp-zoom-1.png) -

https://www.itrelease.com/2018/07/advantages-and-disadvantages-of-personal-area-network-pan/

+

https://www.itrelease.com/2018/07/advantages-and-disadvantages-of-personal-area-network-pan/

-12. **分组(packet )**:因特网中传送的数据单元。由首部 header 和数据段组成。分组又称为包,首部可称为包头。 -13. **存储转发(store and forward )**:路由器收到一个分组,先检查分组是否正确,并过滤掉冲突包错误。确定包正确后,取出目的地址,通过查找表找到想要发送的输出端口地址,然后将该包发送出去。 +11. **分组(packet )**:因特网中传送的数据单元。由首部 header 和数据段组成。分组又称为包,首部可称为包头。 +12. **存储转发(store and forward )**:路由器收到一个分组,先检查分组是否正确,并过滤掉冲突包错误。确定包正确后,取出目的地址,通过查找表找到想要发送的输出端口地址,然后将该包发送出去。 -![](https://p3-juejin.byteimg.com/tos-cn-i-k3u1fbpfcp/addb6b2211444a4da9e0ffc129dd444f~tplv-k3u1fbpfcp-zoom-1.image) + ![](https://oss.javaguide.cn/p3-juejin/addb6b2211444a4da9e0ffc129dd444f~tplv-k3u1fbpfcp-zoom-1.gif) -14. **带宽(bandwidth)**:在计算机网络中,表示在单位时间内从网络中的某一点到另一点所能通过的“最高数据率”。常用来表示网络的通信线路所能传送数据的能力。单位是“比特每秒”,记为 b/s。 -15. **吞吐量(throughput )**:表示在单位时间内通过某个网络(或信道、接口)的数据量。吞吐量更经常地用于对现实世界中的网络的一种测量,以便知道实际上到底有多少数据量能够通过网络。吞吐量受网络的带宽或网络的额定速率的限制。 +13. **带宽(bandwidth)**:在计算机网络中,表示在单位时间内从网络中的某一点到另一点所能通过的“最高数据率”。常用来表示网络的通信线路所能传送数据的能力。单位是“比特每秒”,记为 b/s。 +14. **吞吐量(throughput )**:表示在单位时间内通过某个网络(或信道、接口)的数据量。吞吐量更经常地用于对现实世界中的网络的一种测量,以便知道实际上到底有多少数据量能够通过网络。吞吐量受网络的带宽或网络的额定速率的限制。 ### 1.2. 重要知识点总结 1. **计算机网络(简称网络)把许多计算机连接在一起,而互联网把许多网络连接在一起,是网络的网络。** 2. 小写字母 i 开头的 internet(互联网)是通用名词,它泛指由多个计算机网络相互连接而成的网络。在这些网络之间的通信协议(即通信规则)可以是任意的。大写字母 I 开头的 Internet(互联网)是专用名词,它指全球最大的,开放的,由众多网络相互连接而成的特定的互联网,并采用 TCP/IP 协议作为通信规则,其前身为 ARPANET。Internet 的推荐译名为因特网,现在一般流行称为互联网。 -3. 路由器是实现分组交换的关键构件,其任务是转发收到的分组,这是网络核心部分最重要的功能。分组交换采用存储转发技术,表示把一个报文(要发送的整块数据)分为几个分组后再进行传送。在发送报文之前,先把较长的报文划分成为一个个更小的等长数据段。在每个数据端的前面加上一些由必要的控制信息组成的首部后,就构成了一个分组。分组又称为包。分组是在互联网中传送的数据单元,正是由于分组的头部包含了诸如目的地址和源地址等重要控制信息,每一个分组才能在互联网中独立的选择传输路径,并正确地交付到分组传输的终点。 +3. 路由器是实现分组交换的关键构件,其任务是转发收到的分组,这是网络核心部分最重要的功能。分组交换采用存储转发技术,表示把一个报文(要发送的整块数据)分为几个分组后再进行传送。在发送报文之前,先把较长的报文划分成为一个个更小的等长数据段。在每个数据段的前面加上一些由必要的控制信息组成的首部后,就构成了一个分组。分组又称为包。分组是在互联网中传送的数据单元,正是由于分组的头部包含了诸如目的地址和源地址等重要控制信息,每一个分组才能在互联网中独立的选择传输路径,并正确地交付到分组传输的终点。 4. 互联网按工作方式可划分为边缘部分和核心部分。主机在网络的边缘部分,其作用是进行信息处理。由大量网络和连接这些网络的路由器组成核心部分,其作用是提供连通性和交换。 5. 计算机通信是计算机中进程(即运行着的程序)之间的通信。计算机网络采用的通信方式是客户-服务器方式(C/S 方式)和对等连接方式(P2P 方式)。 6. 客户和服务器都是指通信中所涉及的应用进程。客户是服务请求方,服务器是服务提供方。 @@ -64,44 +64,44 @@ tag: 9. 网络协议即协议,是为进行网络中的数据交换而建立的规则。计算机网络的各层以及其协议集合,称为网络的体系结构。 10. **五层体系结构由应用层,运输层,网络层(网际层),数据链路层,物理层组成。运输层最主要的协议是 TCP 和 UDP 协议,网络层最重要的协议是 IP 协议。** -![](https://p3-juejin.byteimg.com/tos-cn-i-k3u1fbpfcp/acec0fa44041449b8088872dcd7c0b3a~tplv-k3u1fbpfcp-zoom-1.image) +![](https://oss.javaguide.cn/p3-juejin/acec0fa44041449b8088872dcd7c0b3a~tplv-k3u1fbpfcp-zoom-1.gif) 下面的内容会介绍计算机网络的五层体系结构:**物理层+数据链路层+网络层(网际层)+运输层+应用层**。 ## 2. 物理层(Physical Layer) -![物理层](https://p3-juejin.byteimg.com/tos-cn-i-k3u1fbpfcp/cf1bfdd36e5f4bde94aea44bbe7a6f8a~tplv-k3u1fbpfcp-zoom-1.image) +![物理层](https://oss.javaguide.cn/p3-juejin/cf1bfdd36e5f4bde94aea44bbe7a6f8a~tplv-k3u1fbpfcp-zoom-1.png) ### 2.1. 基本术语 -1. **数据(data)** :运送消息的实体。 +1. **数据(data)**:运送消息的实体。 2. **信号(signal)**:数据的电气的或电磁的表现。或者说信号是适合在传输介质上传输的对象。 3. **码元( code)**:在使用时间域(或简称为时域)的波形来表示数字信号时,代表不同离散数值的基本波形。 -4. **单工(simplex )** : 只能有一个方向的通信而没有反方向的交互。 +4. **单工(simplex )**:只能有一个方向的通信而没有反方向的交互。 5. **半双工(half duplex )**:通信的双方都可以发送信息,但不能双方同时发送(当然也就不能同时接收)。 -6. **全双工(full duplex)** : 通信的双方可以同时发送和接收信息。 +6. **全双工(full duplex)**:通信的双方可以同时发送和接收信息。 -![](https://p3-juejin.byteimg.com/tos-cn-i-k3u1fbpfcp/b1f02095b7c34eafb3c255ee81f58c2a~tplv-k3u1fbpfcp-zoom-1.image) + ![](https://oss.javaguide.cn/p3-juejin/b1f02095b7c34eafb3c255ee81f58c2a~tplv-k3u1fbpfcp-zoom-1.png) 7. **失真**:失去真实性,主要是指接受到的信号和发送的信号不同,有磨损和衰减。影响失真程度的因素:1.码元传输速率 2.信号传输距离 3.噪声干扰 4.传输媒体质量 -![](https://p3-juejin.byteimg.com/tos-cn-i-k3u1fbpfcp/f939342f543046459ffabdc476f7bca4~tplv-k3u1fbpfcp-zoom-1.image) + ![](https://oss.javaguide.cn/p3-juejin/f939342f543046459ffabdc476f7bca4~tplv-k3u1fbpfcp-zoom-1.png) -8. **奈氏准则** : 在任何信道中,码元的传输的效率是有上限的,传输速率超过此上限,就会出现严重的码间串扰问题,使接收端对码元的判决(即识别)成为不可能。 +8. **奈氏准则**:在任何信道中,码元的传输的效率是有上限的,传输速率超过此上限,就会出现严重的码间串扰问题,使接收端对码元的判决(即识别)成为不可能。 9. **香农定理**:在带宽受限且有噪声的信道中,为了不产生误差,信息的数据传输速率有上限值。 -10. **基带信号(baseband signal)** : 来自信源的信号。指没有经过调制的数字信号或模拟信号。 +10. **基带信号(baseband signal)**:来自信源的信号。指没有经过调制的数字信号或模拟信号。 11. **带通(频带)信号(bandpass signal)**:把基带信号经过载波调制后,把信号的频率范围搬移到较高的频段以便在信道中传输(即仅在一段频率范围内能够通过信道),这里调制过后的信号就是带通信号。 -12. **调制(modulation )** : 对信号源的信息进行处理后加到载波信号上,使其变为适合在信道传输的形式的过程。 -13. **信噪比(signal-to-noise ratio )** : 指信号的平均功率和噪声的平均功率之比,记为 S/N。信噪比(dB)=10\*log10(S/N)。 +12. **调制(modulation )**:对信号源的信息进行处理后加到载波信号上,使其变为适合在信道传输的形式的过程。 +13. **信噪比(signal-to-noise ratio )**:指信号的平均功率和噪声的平均功率之比,记为 S/N。信噪比(dB)=10\*log10(S/N)。 14. **信道复用(channel multiplexing )**:指多个用户共享同一个信道。(并不一定是同时)。 -![信道复用技术](https://p3-juejin.byteimg.com/tos-cn-i-k3u1fbpfcp/5d9bf7b3db324ae7a88fcedcbace45d8~tplv-k3u1fbpfcp-zoom-1.image) + ![信道复用技术](https://oss.javaguide.cn/p3-juejin/5d9bf7b3db324ae7a88fcedcbace45d8~tplv-k3u1fbpfcp-zoom-1.png) 15. **比特率(bit rate )**:单位时间(每秒)内传送的比特数。 16. **波特率(baud rate)**:单位时间载波调制状态改变的次数。针对数据信号对载波的调制速率。 17. **复用(multiplexing)**:共享信道的方法。 18. **ADSL(Asymmetric Digital Subscriber Line )**:非对称数字用户线。 -19. **光纤同轴混合网(HFC 网)** :在目前覆盖范围很广的有线电视网的基础上开发的一种居民宽带接入网 +19. **光纤同轴混合网(HFC 网)**:在目前覆盖范围很广的有线电视网的基础上开发的一种居民宽带接入网 ### 2.2. 重要知识点总结 @@ -137,7 +137,7 @@ tag: ## 3. 数据链路层(Data Link Layer) -![数据链路层](https://p3-juejin.byteimg.com/tos-cn-i-k3u1fbpfcp/83ec6dafc8c14ca185bafb656d86f0b2~tplv-k3u1fbpfcp-zoom-1.image) +![数据链路层](https://oss.javaguide.cn/p3-juejin/83ec6dafc8c14ca185bafb656d86f0b2~tplv-k3u1fbpfcp-zoom-1.png) ### 3.1. 基本术语 @@ -148,10 +148,10 @@ tag: 5. **MTU(Maximum Transfer Uint )**:最大传送单元。帧的数据部分的的长度上限。 6. **误码率 BER(Bit Error Rate )**:在一段时间内,传输错误的比特占所传输比特总数的比率。 7. **PPP(Point-to-Point Protocol )**:点对点协议。即用户计算机和 ISP 进行通信时所使用的数据链路层协议。以下是 PPP 帧的示意图: - ![PPP](https://p3-juejin.byteimg.com/tos-cn-i-k3u1fbpfcp/6b0310d3103c4149a725a28aaf001899~tplv-k3u1fbpfcp-zoom-1.image) + ![PPP](https://oss.javaguide.cn/p3-juejin/6b0310d3103c4149a725a28aaf001899~tplv-k3u1fbpfcp-zoom-1.jpeg) 8. **MAC 地址(Media Access Control 或者 Medium Access Control)**:意译为媒体访问控制,或称为物理地址、硬件地址,用来定义网络设备的位置。在 OSI 模型中,第三层网络层负责 IP 地址,第二层数据链路层则负责 MAC 地址。因此一个主机会有一个 MAC 地址,而每个网络位置会有一个专属于它的 IP 地址 。地址是识别某个系统的重要标识符,“名字指出我们所要寻找的资源,地址指出资源所在的地方,路由告诉我们如何到达该处。” -![ARP (Address Resolution Protocol) explained](https://p3-juejin.byteimg.com/tos-cn-i-k3u1fbpfcp/057b83e7ec5b4c149e56255a3be89141~tplv-k3u1fbpfcp-zoom-1.image) + ![ARP (Address Resolution Protocol) explained](https://oss.javaguide.cn/p3-juejin/057b83e7ec5b4c149e56255a3be89141~tplv-k3u1fbpfcp-zoom-1.png) 9. **网桥(bridge)**:一种用于数据链路层实现中继,连接两个或多个局域网的网络互连设备。 10. **交换机(switch )**:广义的来说,交换机指的是一种通信系统中完成信息交换的设备。这里工作在数据链路层的交换机指的是交换式集线器,其实质是一个多接口的网桥 @@ -180,7 +180,7 @@ tag: ## 4. 网络层(Network Layer) -![网络层](https://p3-juejin.byteimg.com/tos-cn-i-k3u1fbpfcp/775dc8136bec486aad4f1182c68f24cd~tplv-k3u1fbpfcp-zoom-1.image) +![网络层](https://oss.javaguide.cn/p3-juejin/775dc8136bec486aad4f1182c68f24cd~tplv-k3u1fbpfcp-zoom-1.png) ### 4.1. 基本术语 @@ -195,7 +195,7 @@ tag: ### 4.2. 重要知识点总结 -1. **TCP/IP 协议中的网络层向上只提供简单灵活的,无连接的,尽最大努力交付的数据报服务。网络层不提供服务质量的承诺,不保证分组交付的时限所传送的分组可能出错,丢失,重复和失序。进程之间通信的可靠性由运输层负责** +1. **TCP/IP 协议中的网络层向上只提供简单灵活的,无连接的,尽最大努力交付的数据报服务。网络层不提供服务质量的承诺,不保证分组交付的时限,所传送的分组可能出错、丢失、重复和失序。进程之间通信的可靠性由运输层负责** 2. 在互联网的交付有两种,一是在本网络直接交付不用经过路由器,另一种是和其他网络的间接交付,至少经过一个路由器,但最后一次一定是直接交付 3. 分类的 IP 地址由网络号字段(指明网络)和主机号字段(指明主机)组成。网络号字段最前面的类别指明 IP 地址的类别。IP 地址是一种分等级的地址结构。IP 地址管理机构分配 IP 地址时只分配网络号,主机号由得到该网络号的单位自行分配。路由器根据目的主机所连接的网络号来转发分组。一个路由器至少连接到两个网络,所以一个路由器至少应当有两个不同的 IP 地址 4. IP 数据报分为首部和数据两部分。首部的前一部分是固定长度,共 20 字节,是所有 IP 数据包必须具有的(源地址,目的地址,总长度等重要地段都固定在首部)。一些长度可变的可选字段固定在首部的后面。IP 首部中的生存时间给出了 IP 数据报在互联网中所能经过的最大路由器数。可防止 IP 数据报在互联网中无限制的兜圈子。 @@ -208,7 +208,7 @@ tag: ## 5. 传输层(Transport Layer) -![传输层](https://p3-juejin.byteimg.com/tos-cn-i-k3u1fbpfcp/9fe85e137e7f4f03a580512200a59609~tplv-k3u1fbpfcp-zoom-1.image) +![传输层](https://oss.javaguide.cn/p3-juejin/9fe85e137e7f4f03a580512200a59609~tplv-k3u1fbpfcp-zoom-1.png) ### 5.1. 基本术语 @@ -218,7 +218,7 @@ tag: 4. **TCP(Transmission Control Protocol)**:传输控制协议。 5. **UDP(User Datagram Protocol)**:用户数据报协议。 -![TCP和UDP](https://p3-juejin.byteimg.com/tos-cn-i-k3u1fbpfcp/b136e69e0b9b426782f77623dcf098bd~tplv-k3u1fbpfcp-zoom-1.image) + ![TCP 和 UDP](https://oss.javaguide.cn/p3-juejin/b136e69e0b9b426782f77623dcf098bd~tplv-k3u1fbpfcp-zoom-1.png) 6. **端口(port)**:端口的目的是为了确认对方机器的哪个进程在与自己进行交互,比如 MSN 和 QQ 的端口不同,如果没有端口就可能出现 QQ 进程和 MSN 交互错误。端口又称协议端口号。 7. **停止等待协议(stop-and-wait)**:指发送方每发送完一个分组就停止发送,等待对方确认,在收到确认之后在发送下一个分组。 @@ -261,45 +261,43 @@ tag: ## 6. 应用层(Application Layer) -![应用层](https://p3-juejin.byteimg.com/tos-cn-i-k3u1fbpfcp/0f13f0ee13b24af7bdddf56162eb6602~tplv-k3u1fbpfcp-zoom-1.image) +![应用层](https://oss.javaguide.cn/p3-juejin/0f13f0ee13b24af7bdddf56162eb6602~tplv-k3u1fbpfcp-zoom-1.png) ### 6.1. 基本术语 1. **域名系统(DNS)**:域名系统(DNS,Domain Name System)将人类可读的域名 (例如,www.baidu.com) 转换为机器可读的 IP 地址 (例如,220.181.38.148)。我们可以将其理解为专为互联网设计的电话薄。 -![](https://p3-juejin.byteimg.com/tos-cn-i-k3u1fbpfcp/e7da4b07947f4c0094d46dc96a067df0~tplv-k3u1fbpfcp-zoom-1.image) + ![](https://oss.javaguide.cn/p3-juejin/e7da4b07947f4c0094d46dc96a067df0~tplv-k3u1fbpfcp-zoom-1.png) -

https://www.seobility.net/en/wiki/HTTP_headers

+

https://www.seobility.net/en/wiki/HTTP_headers

2. **文件传输协议(FTP)**:FTP 是 File Transfer Protocol(文件传输协议)的英文简称,而中文简称为“文传协议”。用于 Internet 上的控制文件的双向传输。同时,它也是一个应用程序(Application)。基于不同的操作系统有不同的 FTP 应用程序,而所有这些应用程序都遵守同一种协议以传输文件。在 FTP 的使用当中,用户经常遇到两个概念:"下载"(Download)和"上传"(Upload)。 "下载"文件就是从远程主机拷贝文件至自己的计算机上;"上传"文件就是将文件从自己的计算机中拷贝至远程主机上。用 Internet 语言来说,用户可通过客户机程序向(从)远程主机上传(下载)文件。 -![FTP工作过程](https://p3-juejin.byteimg.com/tos-cn-i-k3u1fbpfcp/f3f2caaa361045a38fb89bb9fee15bd3~tplv-k3u1fbpfcp-zoom-1.image) + ![FTP工作过程](https://oss.javaguide.cn/p3-juejin/f3f2caaa361045a38fb89bb9fee15bd3~tplv-k3u1fbpfcp-zoom-1.png) 3. **简单文件传输协议(TFTP)**:TFTP(Trivial File Transfer Protocol,简单文件传输协议)是 TCP/IP 协议族中的一个用来在客户机与服务器之间进行简单文件传输的协议,提供不复杂、开销不大的文件传输服务。端口号为 69。 4. **远程终端协议(TELNET)**:Telnet 协议是 TCP/IP 协议族中的一员,是 Internet 远程登陆服务的标准协议和主要方式。它为用户提供了在本地计算机上完成远程主机工作的能力。在终端使用者的电脑上使用 telnet 程序,用它连接到服务器。终端使用者可以在 telnet 程序中输入命令,这些命令会在服务器上运行,就像直接在服务器的控制台上输入一样。可以在本地就能控制服务器。要开始一个 telnet 会话,必须输入用户名和密码来登录服务器。Telnet 是常用的远程控制 Web 服务器的方法。 5. **万维网(WWW)**:WWW 是环球信息网的缩写,(亦作“Web”、“WWW”、“'W3'”,英文全称为“World Wide Web”),中文名字为“万维网”,"环球网"等,常简称为 Web。分为 Web 客户端和 Web 服务器程序。WWW 可以让 Web 客户端(常用浏览器)访问浏览 Web 服务器上的页面。是一个由许多互相链接的超文本组成的系统,通过互联网访问。在这个系统中,每个有用的事物,称为一样“资源”;并且由一个全局“统一资源标识符”(URI)标识;这些资源通过超文本传输协议(Hypertext Transfer Protocol)传送给用户,而后者通过点击链接来获得资源。万维网联盟(英语:World Wide Web Consortium,简称 W3C),又称 W3C 理事会。1994 年 10 月在麻省理工学院(MIT)计算机科学实验室成立。万维网联盟的创建者是万维网的发明者蒂姆·伯纳斯-李。万维网并不等同互联网,万维网只是互联网所能提供的服务其中之一,是靠着互联网运行的一项服务。 6. **万维网的大致工作工程:** -![万维网的大致工作工程](https://p3-juejin.byteimg.com/tos-cn-i-k3u1fbpfcp/ba628fd37fdc4ba59c1a74eae32e03b1~tplv-k3u1fbpfcp-zoom-1.image) + ![万维网的大致工作工程](https://oss.javaguide.cn/p3-juejin/ba628fd37fdc4ba59c1a74eae32e03b1~tplv-k3u1fbpfcp-zoom-1.jpeg) 7. **统一资源定位符(URL)**:统一资源定位符是对可以从互联网上得到的资源的位置和访问方法的一种简洁的表示,是互联网上标准资源的地址。互联网上的每个文件都有一个唯一的 URL,它包含的信息指出文件的位置以及浏览器应该怎么处理它。 8. **超文本传输协议(HTTP)**:超文本传输协议(HTTP,HyperText Transfer Protocol)是互联网上应用最为广泛的一种网络协议。所有的 WWW 文件都必须遵守这个标准。设计 HTTP 最初的目的是为了提供一种发布和接收 HTML 页面的方法。1960 年美国人 Ted Nelson 构思了一种通过计算机处理文本信息的方法,并称之为超文本(hypertext),这成为了 HTTP 超文本传输协议标准架构的发展根基。 -HTTP 协议的本质就是一种浏览器与服务器之间约定好的通信格式。HTTP 的原理如下图所示: + HTTP 协议的本质就是一种浏览器与服务器之间约定好的通信格式。HTTP 的原理如下图所示: -![](https://p3-juejin.byteimg.com/tos-cn-i-k3u1fbpfcp/8e3efca026654874bde8be88c96e1783~tplv-k3u1fbpfcp-zoom-1.image) + ![](https://oss.javaguide.cn/p3-juejin/8e3efca026654874bde8be88c96e1783~tplv-k3u1fbpfcp-zoom-1.jpeg) -10. **代理服务器(Proxy Server)**:代理服务器(Proxy Server)是一种网络实体,它又称为万维网高速缓存。 代理服务器把最近的一些请求和响应暂存在本地磁盘中。当新请求到达时,若代理服务器发现这个请求与暂时存放的的请求相同,就返回暂存的响应,而不需要按 URL 的地址再次去互联网访问该资源。代理服务器可在客户端或服务器工作,也可以在中间系统工作。 -11. **简单邮件传输协议(SMTP)** : SMTP(Simple Mail Transfer Protocol)即简单邮件传输协议,它是一组用于由源地址到目的地址传送邮件的规则,由它来控制信件的中转方式。 SMTP 协议属于 TCP/IP 协议簇,它帮助每台计算机在发送或中转信件时找到下一个目的地。 通过 SMTP 协议所指定的服务器,就可以把 E-mail 寄到收信人的服务器上了,整个过程只要几分钟。SMTP 服务器则是遵循 SMTP 协议的发送邮件服务器,用来发送或中转发出的电子邮件。 +9. **代理服务器(Proxy Server)**:代理服务器(Proxy Server)是一种网络实体,它又称为万维网高速缓存。 代理服务器把最近的一些请求和响应暂存在本地磁盘中。当新请求到达时,若代理服务器发现这个请求与暂时存放的的请求相同,就返回暂存的响应,而不需要按 URL 的地址再次去互联网访问该资源。代理服务器可在客户端或服务器工作,也可以在中间系统工作。 +10. **简单邮件传输协议(SMTP)** : SMTP(Simple Mail Transfer Protocol)即简单邮件传输协议,它是一组用于由源地址到目的地址传送邮件的规则,由它来控制信件的中转方式。 SMTP 协议属于 TCP/IP 协议簇,它帮助每台计算机在发送或中转信件时找到下一个目的地。 通过 SMTP 协议所指定的服务器,就可以把 E-mail 寄到收信人的服务器上了,整个过程只要几分钟。SMTP 服务器则是遵循 SMTP 协议的发送邮件服务器,用来发送或中转发出的电子邮件。 -![一个电子邮件被发送的过程](https://p3-juejin.byteimg.com/tos-cn-i-k3u1fbpfcp/2bdccb760474435aae52559f2ef9652f~tplv-k3u1fbpfcp-zoom-1.image) + ![一个电子邮件被发送的过程](https://oss.javaguide.cn/p3-juejin/2bdccb760474435aae52559f2ef9652f~tplv-k3u1fbpfcp-zoom-1.png) -

https://www.campaignmonitor.com/resources/knowledge-base/what-is-the-code-that-makes-bcc-or-cc-operate-in-an-email/

+

https://www.campaignmonitor.com/resources/knowledge-base/what-is-the-code-that-makes-bcc-or-cc-operate-in-an-email/

11. **搜索引擎** :搜索引擎(Search Engine)是指根据一定的策略、运用特定的计算机程序从互联网上搜集信息,在对信息进行组织和处理后,为用户提供检索服务,将用户检索相关的信息展示给用户的系统。搜索引擎包括全文索引、目录索引、元搜索引擎、垂直搜索引擎、集合式搜索引擎、门户搜索引擎与免费链接列表等。 -![搜索引擎](https://p3-juejin.byteimg.com/tos-cn-i-k3u1fbpfcp/b67fde8d49624602959232683a3275e6~tplv-k3u1fbpfcp-zoom-1.image) - 12. **垂直搜索引擎**:垂直搜索引擎是针对某一个行业的专业搜索引擎,是搜索引擎的细分和延伸,是对网页库中的某类专门的信息进行一次整合,定向分字段抽取出需要的数据进行处理后再以某种形式返回给用户。垂直搜索是相对通用搜索引擎的信息量大、查询不准确、深度不够等提出来的新的搜索引擎服务模式,通过针对某一特定领域、某一特定人群或某一特定需求提供的有一定价值的信息和相关服务。其特点就是“专、精、深”,且具有行业色彩,相比较通用搜索引擎的海量信息无序化,垂直搜索引擎则显得更加专注、具体和深入。 13. **全文索引** :全文索引技术是目前搜索引擎的关键技术。试想在 1M 大小的文件中搜索一个词,可能需要几秒,在 100M 的文件中可能需要几十秒,如果在更大的文件中搜索那么就需要更大的系统开销,这样的开销是不现实的。所以在这样的矛盾下出现了全文索引技术,有时候有人叫倒排文档技术。 14. **目录索引**:目录索引( search index/directory),顾名思义就是将网站分门别类地存放在相应的目录中,因此用户在查询信息时,可选择关键词搜索,也可按分类目录逐层查找。 @@ -319,3 +317,5 @@ HTTP 协议的本质就是一种浏览器与服务器之间约定好的通信格 2. 域名系统-从域名解析出 IP 地址 3. 访问一个网站大致的过程 4. 系统调用和应用编程接口概念 + + diff --git a/docs/cs-basics/network/dns.md b/docs/cs-basics/network/dns.md index 21eae6a0ff3..3d3ef0e2254 100644 --- a/docs/cs-basics/network/dns.md +++ b/docs/cs-basics/network/dns.md @@ -15,6 +15,8 @@ DNS(Domain Name System)域名管理系统,是当用户使用浏览器访 ![TCP/IP 各层协议概览](https://oss.javaguide.cn/github/javaguide/cs-basics/network/network-protocol-overview.png) +## DNS 服务器 + DNS 服务器自底向上可以依次分为以下几个层级(所有 DNS 服务器都属于以下四个类别之一): - 根 DNS 服务器。根 DNS 服务器提供 TLD 服务器的 IP 地址。目前世界上只有 13 组根服务器,我国境内目前仍没有根服务器。 @@ -22,6 +24,8 @@ DNS 服务器自底向上可以依次分为以下几个层级(所有 DNS 服务 - 权威 DNS 服务器。在因特网上具有公共可访问主机的每个组织机构必须提供公共可访问的 DNS 记录,这些记录将这些主机的名字映射为 IP 地址。 - 本地 DNS 服务器。每个 ISP(互联网服务提供商)都有一个自己的本地 DNS 服务器。当主机发出 DNS 请求时,该请求被发往本地 DNS 服务器,它起着代理的作用,并将该请求转发到 DNS 层次结构中。严格说来,不属于 DNS 层级结构。 +世界上并不是只有 13 台根服务器,这是很多人普遍的误解,网上很多文章也是这么写的。实际上,现在根服务器数量远远超过这个数量。最初确实是为 DNS 根服务器分配了 13 个 IP 地址,每个 IP 地址对应一个不同的根 DNS 服务器。然而,由于互联网的快速发展和增长,这个原始的架构变得不太适应当前的需求。为了提高 DNS 的可靠性、安全性和性能,目前这 13 个 IP 地址中的每一个都有多个服务器,截止到 2023 年底,所有根服务器之和达到了 600 多台,未来还会继续增加。 + ## DNS 工作流程 以下图为例,介绍 DNS 的查询解析过程。DNS 的查询解析过程分为两种模式: @@ -48,7 +52,7 @@ DNS 服务器自底向上可以依次分为以下几个层级(所有 DNS 服务 ![](https://oss.javaguide.cn/github/javaguide/cs-basics/network/DNS-process2.png) -另外,DNS 的缓存位于本地 DNS 服务器。由于全世界的根服务器甚少,只有 400 多台,分为 13 组,且顶级域的数量也在一个可数的范围内,因此本地 DNS 通常已经缓存了很多 TLD DNS 服务器,所以在实际查找过程中,无需访问根服务器。根服务器通常是被跳过的,不请求的。 +另外,DNS 的缓存位于本地 DNS 服务器。由于全世界的根服务器甚少,只有 600 多台,分为 13 组,且顶级域的数量也在一个可数的范围内,因此本地 DNS 通常已经缓存了很多 TLD DNS 服务器,所以在实际查找过程中,无需访问根服务器。根服务器通常是被跳过的,不请求的。这样可以提高 DNS 查询的效率和速度,减少对根服务器和 TLD 服务器的负担。 ## DNS 报文格式 @@ -68,7 +72,7 @@ DNS 报文分为查询和回答报文,两种形式的报文结构相同。 ## DNS 记录 -DNS 服务器在响应查询时,需要查询自己的数据库,数据库中的条目被称为**资源记录(Resource Record,RR)**。RR 提供了主机名到 IP 地址的映射。RR 是一个包含了`Name`, `Value`, `Type`, `TTL`四个字段的四元组。 +DNS 服务器在响应查询时,需要查询自己的数据库,数据库中的条目被称为 **资源记录(Resource Record,RR)** 。RR 提供了主机名到 IP 地址的映射。RR 是一个包含了`Name`, `Value`, `Type`, `TTL`四个字段的四元组。 ![](https://oss.javaguide.cn/github/javaguide/cs-basics/network/20210506174303797.png) @@ -86,7 +90,7 @@ DNS 服务器在响应查询时,需要查询自己的数据库,数据库中 `CNAME`记录总是指向另一则域名,而非 IP 地址。假设有下述 DNS zone: -``` +```plain NAME TYPE VALUE -------------------------------------------------- bar.example.com. CNAME foo.example.com. @@ -97,6 +101,8 @@ foo.example.com. A 192.0.2.23 ## 参考 -- DNS 服务器类型:https://www.cloudflare.com/zh-cn/learning/dns/dns-server-types/ -- DNS Message Resource Record Field Formats:http://www.tcpipguide.com/free/t_DNSMessageResourceRecordFieldFormats-2.htm -- Understanding Different Types of Record in DNS Server:https://www.mustbegeek.com/understanding-different-types-of-record-in-dns-server/ +- DNS 服务器类型: +- DNS Message Resource Record Field Formats: +- Understanding Different Types of Record in DNS Server: + + diff --git a/docs/cs-basics/network/http-status-codes.md b/docs/cs-basics/network/http-status-codes.md index 4cacb50cd42..5550e06d5b8 100644 --- a/docs/cs-basics/network/http-status-codes.md +++ b/docs/cs-basics/network/http-status-codes.md @@ -15,10 +15,14 @@ HTTP 状态码用于描述 HTTP 请求的结果,比如 2xx 就代表请求被 ### 2xx Success(成功状态码) -- **200 OK**:请求被成功处理。比如我们发送一个查询用户数据的 HTTP 请求到服务端,服务端正确返回了用户数据。这个是我们平时最常见的一个 HTTP 状态码。 -- **201 Created**:请求被成功处理并且在服务端创建了一个新的资源。比如我们通过 POST 请求创建一个新的用户。 -- **202 Accepted**:服务端已经接收到了请求,但是还未处理。 -- **204 No Content**:服务端已经成功处理了请求,但是没有返回任何内容。 +- **200 OK**:请求被成功处理。例如,发送一个查询用户数据的 HTTP 请求到服务端,服务端正确返回了用户数据。这个是我们平时最常见的一个 HTTP 状态码。 +- **201 Created**:请求被成功处理并且在服务端创建了~~一个新的资源~~。例如,通过 POST 请求创建一个新的用户。 +- **202 Accepted**:服务端已经接收到了请求,但是还未处理。例如,发送一个需要服务端花费较长时间处理的请求(如报告生成、Excel 导出),服务端接收了请求但尚未处理完毕。 +- **204 No Content**:服务端已经成功处理了请求,但是没有返回任何内容。例如,发送请求删除一个用户,服务器成功处理了删除操作但没有返回任何内容。 + +🐛 修正(参见:[issue#2458](https://github.com/Snailclimb/JavaGuide/issues/2458)):201 Created 状态码更准确点来说是创建一个或多个新的资源,可以参考:。 + +![](https://oss.javaguide.cn/github/javaguide/cs-basics/network/rfc9110-201-created.png) 这里格外提一下 204 状态码,平时学习/工作中见到的次数并不多。 @@ -64,7 +68,9 @@ HTTP 状态码用于描述 HTTP 请求的结果,比如 2xx 就代表请求被 ### 参考 -- https://www.restapitutorial.com/httpstatuscodes.html -- https://developer.mozilla.org/zh-CN/docs/Web/HTTP/Status -- https://en.wikipedia.org/wiki/List_of_HTTP_status_codes -- https://segmentfault.com/a/1190000018264501 +- +- +- +- + + diff --git a/docs/cs-basics/network/http-vs-https.md b/docs/cs-basics/network/http-vs-https.md index 98742292577..71c224f1be4 100644 --- a/docs/cs-basics/network/http-vs-https.md +++ b/docs/cs-basics/network/http-vs-https.md @@ -138,3 +138,5 @@ SSL/TLS 介绍到这里,了解信息安全的朋友又会想到一个安全隐 - **端口号**:HTTP 默认是 80,HTTPS 默认是 443。 - **URL 前缀**:HTTP 的 URL 前缀是 `http://`,HTTPS 的 URL 前缀是 `https://`。 - **安全性和资源消耗**:HTTP 协议运行在 TCP 之上,所有传输的内容都是明文,客户端和服务器端都无法验证对方的身份。HTTPS 是运行在 SSL/TLS 之上的 HTTP 协议,SSL/TLS 运行在 TCP 之上。所有传输的内容都经过加密,加密采用对称加密,但对称加密的密钥用服务器方的证书进行了非对称加密。所以说,HTTP 安全性没有 HTTPS 高,但是 HTTPS 比 HTTP 耗费更多服务器资源。 + + diff --git a/docs/cs-basics/network/http1.0-vs-http1.1.md b/docs/cs-basics/network/http1.0-vs-http1.1.md index cc27c0e0974..f0bb9850780 100644 --- a/docs/cs-basics/network/http1.0-vs-http1.1.md +++ b/docs/cs-basics/network/http1.0-vs-http1.1.md @@ -53,11 +53,11 @@ HTTP/1.1 的缓存机制在 HTTP/1.0 的基础上,大大增加了灵活性和 ## Host 头处理 -域名系统(DNS)允许多个主机名绑定到同一个 IP 地址上,但是 HTTP/1.0 并没有考虑这个问题,假设我们有一个资源 URL 是http://example1.org/home.html,HTTP/1.0的请求报文中,将会请求的是`GET /home.html HTTP/1.0`.也就是不会加入主机名。这样的报文送到服务器端,服务器是理解不了客户端想请求的真正网址。 +域名系统(DNS)允许多个主机名绑定到同一个 IP 地址上,但是 HTTP/1.0 并没有考虑这个问题,假设我们有一个资源 URL 是 的请求报文中,将会请求的是`GET /home.html HTTP/1.0`.也就是不会加入主机名。这样的报文送到服务器端,服务器是理解不了客户端想请求的真正网址。 因此,HTTP/1.1 在请求头中加入了`Host`字段。加入`Host`字段的报文头部将会是: -``` +```plain GET /home.html HTTP/1.1 Host: example1.org ``` @@ -70,9 +70,70 @@ Host: example1.org HTTP/1.1 引入了范围请求(range request)机制,以避免带宽的浪费。当客户端想请求一个文件的一部分,或者需要继续下载一个已经下载了部分但被终止的文件,HTTP/1.1 可以在请求中加入`Range`头部,以请求(并只能请求字节型数据)数据的一部分。服务器端可以忽略`Range`头部,也可以返回若干`Range`响应。 -如果一个响应包含部分数据的话,那么将带有`206 (Partial Content)`状态码。该状态码的意义在于避免了 HTTP/1.0 代理缓存错误地把该响应认为是一个完整的数据响应,从而把他当作为一个请求的响应缓存。 +`206 (Partial Content)` 状态码的主要作用是确保客户端和代理服务器能正确识别部分内容响应,避免将其误认为完整资源并错误地缓存。这对于正确处理范围请求和缓存管理非常重要。 + +一个典型的 HTTP/1.1 范围请求示例: + +```bash +# 获取一个文件的前 1024 个字节 +GET /z4d4kWk.jpg HTTP/1.1 +Host: i.imgur.com +Range: bytes=0-1023 +``` + +`206 Partial Content` 响应: + +```bash + +HTTP/1.1 206 Partial Content +Content-Range: bytes 0-1023/146515 +Content-Length: 1024 +… +(二进制内容) +``` + +简单解释一下 HTTP 范围响应头部中的字段: + +- **`Content-Range` 头部**:指示返回数据在整个资源中的位置,包括起始和结束字节以及资源的总长度。例如,`Content-Range: bytes 0-1023/146515` 表示服务器端返回了第 0 到 1023 字节的数据(共 1024 字节),而整个资源的总长度是 146,515 字节。 +- **`Content-Length` 头部**:指示此次响应中实际传输的字节数。例如,`Content-Length: 1024` 表示服务器端传输了 1024 字节的数据。 + +`Range` 请求头不仅可以请求单个字节范围,还可以一次性请求多个范围。这种方式被称为“多重范围请求”(multiple range requests)。 + +客户端想要获取资源的第 0 到 499 字节以及第 1000 到 1499 字节: -在范围响应中,`Content-Range`头部标志指示出了该数据块的偏移量和数据块的长度。 +```bash +GET /path/to/resource HTTP/1.1 +Host: example.com +Range: bytes=0-499,1000-1499 +``` + +服务器端返回多个字节范围,每个范围的内容以分隔符分开: + +```bash +HTTP/1.1 206 Partial Content +Content-Type: multipart/byteranges; boundary=3d6b6a416f9b5 +Content-Length: 376 + +--3d6b6a416f9b5 +Content-Type: application/octet-stream +Content-Range: bytes 0-99/2000 + +(第 0 到 99 字节的数据块) + +--3d6b6a416f9b5 +Content-Type: application/octet-stream +Content-Range: bytes 500-599/2000 + +(第 500 到 599 字节的数据块) + +--3d6b6a416f9b5 +Content-Type: application/octet-stream +Content-Range: bytes 1000-1099/2000 + +(第 1000 到 1099 字节的数据块) + +--3d6b6a416f9b5-- +``` ### 状态码 100 @@ -103,3 +164,5 @@ HTTP/1.0 包含了`Content-Encoding`头部,对消息进行端到端编码。HT ## 参考资料 [Key differences between HTTP/1.0 and HTTP/1.1](http://www.ra.ethz.ch/cdstore/www8/data/2136/pdf/pd1.pdf) + + diff --git a/docs/cs-basics/network/images/network-model/nerwork-layer-protocol.png b/docs/cs-basics/network/images/network-model/nerwork-layer-protocol.png new file mode 100644 index 00000000000..a94274cce32 Binary files /dev/null and b/docs/cs-basics/network/images/network-model/nerwork-layer-protocol.png differ diff --git a/docs/cs-basics/network/nat.md b/docs/cs-basics/network/nat.md index 814df5e0139..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 协议的特点,有以下几点: @@ -55,4 +55,6 @@ SOHO 子网的“代理人”,也就是和外界的窗口,通常由路由器 3. WAN 的 ISP 变更接口地址时,无需通告 LAN 内主机。 4. LAN 主机对 WAN 不可见,不可直接寻址,可以保证一定程度的安全性。 -然而,NAT 协议由于其独特性,存在着一些争议。比如,可能你已经注意到了,**NAT 协议在 LAN 以外,标识一个内部主机时,使用的是端口号,因为 IP 地址都是相同的。**这种将端口号作为主机寻址的行为,可能会引发一些误会。此外,路由器作为网络层的设备,修改了传输层的分组内容(修改了源 IP 地址和端口号),同样是不规范的行为。但是,尽管如此,NAT 协议作为 IPv4 时代的产物,极大地方便了一些本来棘手的问题,一直被沿用至今。 +然而,NAT 协议由于其独特性,存在着一些争议。比如,可能你已经注意到了,**NAT 协议在 LAN 以外,标识一个内部主机时,使用的是端口号,因为 IP 地址都是相同的**。这种将端口号作为主机寻址的行为,可能会引发一些误会。此外,路由器作为网络层的设备,修改了传输层的分组内容(修改了源 IP 地址和端口号),同样是不规范的行为。但是,尽管如此,NAT 协议作为 IPv4 时代的产物,极大地方便了一些本来棘手的问题,一直被沿用至今。 + + diff --git a/docs/cs-basics/network/network-attack-means.md b/docs/cs-basics/network/network-attack-means.md index 48892cfee9f..748999d6eba 100644 --- a/docs/cs-basics/network/network-attack-means.md +++ b/docs/cs-basics/network/network-attack-means.md @@ -21,7 +21,7 @@ tag: **IP 头部格式** : -![](https://p3-juejin.byteimg.com/tos-cn-i-k3u1fbpfcp/843fd07074874ee0b695eca659411b42~tplv-k3u1fbpfcp-zoom-1.image) +![](https://oss.javaguide.cn/p3-juejin/843fd07074874ee0b695eca659411b42~tplv-k3u1fbpfcp-zoom-1.png) ### IP 欺骗技术是什么? @@ -29,11 +29,11 @@ tag: IP 欺骗技术就是**伪造**某台主机的 IP 地址的技术。通过 IP 地址的伪装使得某台主机能够**伪装**另外的一台主机,而这台主机往往具有某种特权或者被另外的主机所信任。 -假设现在有一个合法用户 **(1.1.1.1)** 已经同服务器建立正常的连接,攻击者构造攻击的 TCP 数据,伪装自己的 IP 为 **1.1.1.1**,并向服务器发送一个带有 RSI 位的 TCP 数据段。服务器接收到这样的数据后,认为从 **1.1.1.1** 发送的连接有错误,就会清空缓冲区中建立好的连接。 +假设现在有一个合法用户 **(1.1.1.1)** 已经同服务器建立正常的连接,攻击者构造攻击的 TCP 数据,伪装自己的 IP 为 **1.1.1.1**,并向服务器发送一个带有 RST 位的 TCP 数据段。服务器接收到这样的数据后,认为从 **1.1.1.1** 发送的连接有错误,就会清空缓冲区中建立好的连接。 这时,如果合法用户 **1.1.1.1** 再发送合法数据,服务器就已经没有这样的连接了,该用户就必须从新开始建立连接。攻击时,伪造大量的 IP 地址,向目标发送 RST 数据,使服务器不对合法用户服务。虽然 IP 地址欺骗攻击有着相当难度,但我们应该清醒地意识到,这种攻击非常广泛,入侵往往从这种攻击开始。 -![IP 欺骗 DDoS 攻击](https://p3-juejin.byteimg.com/tos-cn-i-k3u1fbpfcp/7547a145adf9404aa3a05f01f5ca2e32~tplv-k3u1fbpfcp-zoom-1.image) +![IP 欺骗 DDoS 攻击](https://oss.javaguide.cn/p3-juejin/7547a145adf9404aa3a05f01f5ca2e32~tplv-k3u1fbpfcp-zoom-1.png) ### 如何缓解 IP 欺骗? @@ -48,13 +48,13 @@ SYN Flood 是互联网上最原始、最经典的 DDoS(Distributed Denial of S SYN Flood 利用了 TCP 协议的三次握手机制,攻击者通常利用工具或者控制僵尸主机向服务器发送海量的变源 IP 地址或变源端口的 TCP SYN 报文,服务器响应了这些报文后就会生成大量的半连接,当系统资源被耗尽后,服务器将无法提供正常的服务。 增加服务器性能,提供更多的连接能力对于 SYN Flood 的海量报文来说杯水车薪,防御 SYN Flood 的关键在于判断哪些连接请求来自于真实源,屏蔽非真实源的请求以保障正常的业务请求能得到服务。 -![](https://p3-juejin.byteimg.com/tos-cn-i-k3u1fbpfcp/2b3d2d4dc8f24890b5957df1c7d6feb8~tplv-k3u1fbpfcp-zoom-1.image) +![](https://oss.javaguide.cn/p3-juejin/2b3d2d4dc8f24890b5957df1c7d6feb8~tplv-k3u1fbpfcp-zoom-1.png) ### TCP SYN Flood 攻击原理是什么? **TCP SYN Flood** 攻击利用的是 **TCP** 的三次握手(**SYN -> SYN/ACK -> ACK**),假设连接发起方是 A,连接接受方是 B,即 B 在某个端口(**Port**)上监听 A 发出的连接请求,过程如下图所示,左边是 A,右边是 B。 -![](https://p3-juejin.byteimg.com/tos-cn-i-k3u1fbpfcp/a39355a1ea404323a11ca6644e009183~tplv-k3u1fbpfcp-zoom-1.image) +![](https://oss.javaguide.cn/p3-juejin/a39355a1ea404323a11ca6644e009183~tplv-k3u1fbpfcp-zoom-1.png) A 首先发送 **SYN**(Synchronization)消息给 B,要求 B 做好接收数据的准备;B 收到后反馈 **SYN-ACK**(Synchronization-Acknowledgement) 消息给 A,这个消息的目的有两个: @@ -71,7 +71,7 @@ A 首先发送 **SYN**(Synchronization)消息给 B,要求 B 做好接收 假设 B 通过某 **TCP** 端口提供服务,B 在收到 A 的 **SYN** 消息时,积极的反馈了 **SYN-ACK** 消息,使连接进入**半开状态**,因为 B 不确定自己发给 A 的 **SYN-ACK** 消息或 A 反馈的 ACK 消息是否会丢在半路,所以会给每个待完成的半开连接都设一个**Timer**,如果超过时间还没有收到 A 的 **ACK** 消息,则重新发送一次 **SYN-ACK** 消息给 A,直到重试超过一定次数时才会放弃。 -![图片](https://p3-juejin.byteimg.com/tos-cn-i-k3u1fbpfcp/7ff1daddcec44d61994f254e664987b4~tplv-k3u1fbpfcp-zoom-1.image) +![图片](https://oss.javaguide.cn/p3-juejin/7ff1daddcec44d61994f254e664987b4~tplv-k3u1fbpfcp-zoom-1.png) B 为帮助 A 能顺利连接,需要**分配内核资源**维护半开连接,那么当 B 面临海量的连接 A 时,如上图所示,**SYN Flood** 攻击就形成了。攻击方 A 可以控制肉鸡向 B 发送大量 SYN 消息但不响应 ACK 消息,或者干脆伪造 SYN 消息中的 **Source IP**,使 B 反馈的 **SYN-ACK** 消息石沉大海,导致 B 被大量注定不能完成的半开连接占据,直到资源耗尽,停止响应正常的连接请求。 @@ -79,9 +79,9 @@ B 为帮助 A 能顺利连接,需要**分配内核资源**维护半开连接 **恶意用户可通过三种不同方式发起 SYN Flood 攻击**: -1. **直接攻击:**不伪造 IP 地址的 SYN 洪水攻击称为直接攻击。在此类攻击中,攻击者完全不屏蔽其 IP 地址。由于攻击者使用具有真实 IP 地址的单一源设备发起攻击,因此很容易发现并清理攻击者。为使目标机器呈现半开状态,黑客将阻止个人机器对服务器的 SYN-ACK 数据包做出响应。为此,通常采用以下两种方式实现:部署防火墙规则,阻止除 SYN 数据包以外的各类传出数据包;或者,对传入的所有 SYN-ACK 数据包进行过滤,防止其到达恶意用户机器。实际上,这种方法很少使用(即便使用过也不多见),因为此类攻击相当容易缓解 – 只需阻止每个恶意系统的 IP 地址。哪怕攻击者使用僵尸网络(如 [Mirai 僵尸网络](https://www.cloudflare.com/learning/ddos/glossary/mirai-botnet/)),通常也不会刻意屏蔽受感染设备的 IP。 -2. **欺骗攻击:**恶意用户还可以伪造其发送的各个 SYN 数据包的 IP 地址,以便阻止缓解措施并加大身份暴露难度。虽然数据包可能经过伪装,但还是可以通过这些数据包追根溯源。此类检测工作很难开展,但并非不可实现;特别是,如果 Internet 服务提供商 (ISP) 愿意提供帮助,则更容易实现。 -3. **分布式攻击(DDoS):**如果使用僵尸网络发起攻击,则追溯攻击源头的可能性很低。随着混淆级别的攀升,攻击者可能还会命令每台分布式设备伪造其发送数据包的 IP 地址。哪怕攻击者使用僵尸网络(如 Mirai 僵尸网络),通常也不会刻意屏蔽受感染设备的 IP。 +1. **直接攻击:** 不伪造 IP 地址的 SYN 洪水攻击称为直接攻击。在此类攻击中,攻击者完全不屏蔽其 IP 地址。由于攻击者使用具有真实 IP 地址的单一源设备发起攻击,因此很容易发现并清理攻击者。为使目标机器呈现半开状态,黑客将阻止个人机器对服务器的 SYN-ACK 数据包做出响应。为此,通常采用以下两种方式实现:部署防火墙规则,阻止除 SYN 数据包以外的各类传出数据包;或者,对传入的所有 SYN-ACK 数据包进行过滤,防止其到达恶意用户机器。实际上,这种方法很少使用(即便使用过也不多见),因为此类攻击相当容易缓解 – 只需阻止每个恶意系统的 IP 地址。哪怕攻击者使用僵尸网络(如 [Mirai 僵尸网络](https://www.cloudflare.com/learning/ddos/glossary/mirai-botnet/)),通常也不会刻意屏蔽受感染设备的 IP。 +2. **欺骗攻击:** 恶意用户还可以伪造其发送的各个 SYN 数据包的 IP 地址,以便阻止缓解措施并加大身份暴露难度。虽然数据包可能经过伪装,但还是可以通过这些数据包追根溯源。此类检测工作很难开展,但并非不可实现;特别是,如果 Internet 服务提供商 (ISP) 愿意提供帮助,则更容易实现。 +3. **分布式攻击(DDoS):** 如果使用僵尸网络发起攻击,则追溯攻击源头的可能性很低。随着混淆级别的攀升,攻击者可能还会命令每台分布式设备伪造其发送数据包的 IP 地址。哪怕攻击者使用僵尸网络(如 Mirai 僵尸网络),通常也不会刻意屏蔽受感染设备的 IP。 ### 如何缓解 SYN Flood? @@ -118,7 +118,7 @@ B 为帮助 A 能顺利连接,需要**分配内核资源**维护半开连接 由于目标服务器利用资源检查并响应每个接收到的 **UDP** 数据包的结果,当接收到大量 **UDP** 数据包时,目标的资源可能会迅速耗尽,导致对正常流量的拒绝服务。 -![](https://p3-juejin.byteimg.com/tos-cn-i-k3u1fbpfcp/23dbbc8243a84ed181e088e38bffb37a~tplv-k3u1fbpfcp-zoom-1.image) +![](https://oss.javaguide.cn/p3-juejin/23dbbc8243a84ed181e088e38bffb37a~tplv-k3u1fbpfcp-zoom-1.png) ### 如何缓解 UDP Flooding? @@ -130,7 +130,7 @@ B 为帮助 A 能顺利连接,需要**分配内核资源**维护半开连接 HTTP Flood 是一种大规模的 DDoS(Distributed Denial of Service,分布式拒绝服务)攻击,旨在利用 HTTP 请求使目标服务器不堪重负。目标因请求而达到饱和,且无法响应正常流量后,将出现拒绝服务,拒绝来自实际用户的其他请求。 -![HTTP 洪水攻击](https://p3-juejin.byteimg.com/tos-cn-i-k3u1fbpfcp/aa64869551d94c8d89fa80eaf4395bfa~tplv-k3u1fbpfcp-zoom-1.image) +![HTTP 洪水攻击](https://oss.javaguide.cn/p3-juejin/aa64869551d94c8d89fa80eaf4395bfa~tplv-k3u1fbpfcp-zoom-1.png) ### HTTP Flood 的攻击原理是什么? @@ -157,7 +157,7 @@ HTTP 洪水攻击有两种: ### DNS Flood 的攻击原理是什么? -![](https://p3-juejin.byteimg.com/tos-cn-i-k3u1fbpfcp/97ea11a212924900b10d159226783887~tplv-k3u1fbpfcp-zoom-1.image) +![](https://oss.javaguide.cn/p3-juejin/97ea11a212924900b10d159226783887~tplv-k3u1fbpfcp-zoom-1.png) 域名系统的功能是将易于记忆的名称(例如 example.com)转换成难以记住的网站服务器地址(例如 192.168.0.1),因此成功攻击 DNS 基础设施将导致大多数人无法使用互联网。DNS Flood 攻击是一种相对较新的基于 DNS 的攻击,这种攻击是在高带宽[物联网(IoT)](https://www.cloudflare.com/learning/ddos/glossary/internet-of-things-iot/)[僵尸网络](https://www.cloudflare.com/learning/ddos/what-is-a-ddos-botnet/)(如 [Mirai](https://www.cloudflare.com/learning/ddos/glossary/mirai-botnet/))兴起后激增的。DNS Flood 攻击使用 IP 摄像头、DVR 盒和其他 IoT 设备的高带宽连接直接淹没主要提供商的 DNS 服务器。来自 IoT 设备的大量请求淹没 DNS 提供商的服务,阻止合法用户访问提供商的 DNS 服务器。 @@ -196,27 +196,27 @@ DNS Flood 对传统上基于放大的攻击方法做出了改变。借助轻易 > 建立 TCP 连接 -可以使用 netcat 工具来建立 TCP 连接,这个工很多操作系统都预装了。打开第一个终端窗口,运行以下命令: +可以使用 netcat 工具来建立 TCP 连接,这个工具很多操作系统都预装了。打开第一个终端窗口,运行以下命令: ```bash -$ nc -nvl 8000 +nc -nvl 8000 ``` 这个命令会启动一个 TCP 服务,监听端口为 `8000`。接着再打开第二个终端窗口,运行以下命令: ```bash -$ nc 127.0.0.1 8000 +nc 127.0.0.1 8000 ``` 该命令会尝试与上面的服务建立连接,在其中一个窗口输入一些字符,就会通过 TCP 连接发送给另一个窗口并打印出来。 -![](https://p3-juejin.byteimg.com/tos-cn-i-k3u1fbpfcp/df0508cbf26446708cf98f8ad514dbea~tplv-k3u1fbpfcp-zoom-1.image) +![](https://oss.javaguide.cn/p3-juejin/df0508cbf26446708cf98f8ad514dbea~tplv-k3u1fbpfcp-zoom-1.gif) > 嗅探流量 编写一个攻击程序,使用 Python 网络库 `scapy` 来读取两个终端窗口之间交换的数据,并将其打印到终端上。代码比较长,下面为一部份,完整代码后台回复 TCP 攻击,代码的核心是调用 `scapy` 的嗅探方法: -![](https://p3-juejin.byteimg.com/tos-cn-i-k3u1fbpfcp/27feb834aa9d4b629fd938611ac9972e~tplv-k3u1fbpfcp-zoom-1.image) +![](https://oss.javaguide.cn/p3-juejin/27feb834aa9d4b629fd938611ac9972e~tplv-k3u1fbpfcp-zoom-1.png) 这段代码告诉 `scapy` 在 `lo0` 网络接口上嗅探数据包,并记录所有 TCP 连接的详细信息。 @@ -252,7 +252,7 @@ $ nc 127.0.0.1 8000 攻击中间人攻击英文名叫 Man-in-the-MiddleAttack,简称「MITM 攻击」。指攻击者与通讯的两端分别创建独立的联系,并交换其所收到的数据,使通讯的两端认为他们正在通过一个私密的连接与对方 直接对话,但事实上整个会话都被攻击者完全控制。我们画一张图: -![图片](https://p3-juejin.byteimg.com/tos-cn-i-k3u1fbpfcp/d69b74e63981472b852797f2fa08976f~tplv-k3u1fbpfcp-zoom-1.image) +![图片](https://oss.javaguide.cn/p3-juejin/d69b74e63981472b852797f2fa08976f~tplv-k3u1fbpfcp-zoom-1.png) 从这张图可以看到,中间人其实就是攻击者。通过这种原理,有很多实现的用途,比如说,你在手机上浏览不健康网站的时候,手机就会提示你,此网站可能含有病毒,是否继续访问还是做其他的操作等等。 @@ -292,7 +292,7 @@ $ nc 127.0.0.1 8000 同样的,举个例子。Sum 和 Mike 两个人签合同。Sum 首先用 **SHA** 算法计算合同的摘要,然后用自己私钥将摘要加密,得到数字签名。Sum 将合同原文、签名,以及公钥三者都交给 Mike -![](https://p3-juejin.byteimg.com/tos-cn-i-k3u1fbpfcp/e4b7d6fca78b45c8840c12411b717f2f~tplv-k3u1fbpfcp-zoom-1.image) +![](https://oss.javaguide.cn/p3-juejin/e4b7d6fca78b45c8840c12411b717f2f~tplv-k3u1fbpfcp-zoom-1.png) 如果 Sum 想要证明合同是 Mike 的,那么就要使用 Mike 的公钥,将这个签名解密得到摘要 x,然后 Mike 计算原文的 sha 摘要 Y,随后对比 x 和 y,如果两者相等,就认为数据没有被篡改 @@ -308,7 +308,7 @@ $ nc 127.0.0.1 8000 对称加密,顾名思义,加密方与解密方使用同一钥匙(秘钥)。具体一些就是,发送方通过使用相应的加密算法和秘钥,对将要发送的信息进行加密;对于接收方而言,使用解密算法和相同的秘钥解锁信息,从而有能力阅读信息。 -![图片](https://p3-juejin.byteimg.com/tos-cn-i-k3u1fbpfcp/ef81cb5e2f0a4d3d9ac5a44ecf97e3cc~tplv-k3u1fbpfcp-zoom-1.image) +![图片](https://oss.javaguide.cn/p3-juejin/ef81cb5e2f0a4d3d9ac5a44ecf97e3cc~tplv-k3u1fbpfcp-zoom-1.png) #### 常见的对称加密算法有哪些? @@ -316,7 +316,7 @@ $ nc 127.0.0.1 8000 DES 使用的密钥表面上是 64 位的,然而只有其中的 56 位被实际用于算法,其余 8 位可以被用于奇偶校验,并在算法中被丢弃。因此,**DES** 的有效密钥长度为 56 位,通常称 **DES** 的密钥长度为 56 位。假设秘钥为 56 位,采用暴力破 Jie 的方式,其秘钥个数为 2 的 56 次方,那么每纳秒执行一次解密所需要的时间差不多 1 年的样子。当然,没人这么干。**DES** 现在已经不是一种安全的加密方法,主要因为它使用的 56 位密钥过短。 -![](https://p3-juejin.byteimg.com/tos-cn-i-k3u1fbpfcp/9eb3a2bf6cf14132a890bc3447480eeb~tplv-k3u1fbpfcp-zoom-1.image) +![](https://oss.javaguide.cn/p3-juejin/9eb3a2bf6cf14132a890bc3447480eeb~tplv-k3u1fbpfcp-zoom-1.jpeg) **IDEA** @@ -328,17 +328,17 @@ DES 使用的密钥表面上是 64 位的,然而只有其中的 56 位被实 **SM1 和 SM4** -之前几种都是国外的,我们国内自行研究了国密 **SM1 **和 **SM4**。其中 S 都属于国家标准,算法公开。优点就是国家的大力支持和认可 +之前几种都是国外的,我们国内自行研究了国密 **SM1**和 **SM4**。其中 S 都属于国家标准,算法公开。优点就是国家的大力支持和认可 **总结**: -![](https://p3-juejin.byteimg.com/tos-cn-i-k3u1fbpfcp/578961e3175540e081e1432c409b075a~tplv-k3u1fbpfcp-zoom-1.image) +![](https://oss.javaguide.cn/p3-juejin/578961e3175540e081e1432c409b075a~tplv-k3u1fbpfcp-zoom-1.png) #### 常见的非对称加密算法有哪些? 在对称加密中,发送方与接收方使用相同的秘钥。那么在非对称加密中则是发送方与接收方使用的不同的秘钥。其主要解决的问题是防止在秘钥协商的过程中发生泄漏。比如在对称加密中,小蓝将需要发送的消息加密,然后告诉你密码是 123balala,ok,对于其他人而言,很容易就能劫持到密码是 123balala。那么在非对称的情况下,小蓝告诉所有人密码是 123balala,对于中间人而言,拿到也没用,因为没有私钥。所以,非对称密钥其实主要解决了密钥分发的难题。如下图 -![](https://p3-juejin.byteimg.com/tos-cn-i-k3u1fbpfcp/153cf04a0ecc43c38003f3a1ab198cc0~tplv-k3u1fbpfcp-zoom-1.image) +![](https://oss.javaguide.cn/p3-juejin/153cf04a0ecc43c38003f3a1ab198cc0~tplv-k3u1fbpfcp-zoom-1.png) 其实我们经常都在使用非对称加密,比如使用多台服务器搭建大数据平台 hadoop,为了方便多台机器设置免密登录,是不是就会涉及到秘钥分发。再比如搭建 docker 集群也会使用相关非对称加密算法。 @@ -351,27 +351,27 @@ DES 使用的密钥表面上是 64 位的,然而只有其中的 56 位被实 总结: -![](https://p3-juejin.byteimg.com/tos-cn-i-k3u1fbpfcp/28b96fb797904d4b818ee237cdc7614c~tplv-k3u1fbpfcp-zoom-1.image) +![](https://oss.javaguide.cn/p3-juejin/28b96fb797904d4b818ee237cdc7614c~tplv-k3u1fbpfcp-zoom-1.png) #### 常见的散列算法有哪些? 这个大家应该更加熟悉了,比如我们平常使用的 MD5 校验,在很多时候,我并不是拿来进行加密,而是用来获得唯一性 ID。在做系统的过程中,存储用户的各种密码信息,通常都会通过散列算法,最终存储其散列值。 -**MD5** +**MD5**(不推荐) MD5 可以用来生成一个 128 位的消息摘要,它是目前应用比较普遍的散列算法,具体的应用场景你可以自行  参阅。虽然,因为算法的缺陷,它的唯一性已经被破解了,但是大部分场景下,这并不会构成安全问题。但是,如果不是长度受限(32 个字符),我还是不推荐你继续使用 **MD5** 的。 **SHA** -安全散列算法。**SHA** 分为 **SHA1** 和 **SH2** 两个版本。该算法的思想是接收一段明文,然后以一种不可逆的方式将它转换成一段(通常更小)密文,也可以简单的理解为取一串输入码(称为预映射或信息),并把它们转化为长度较短、位数固定的输出序列即散列值(也称为信息摘要或信息认证代码)的过程。 +安全散列算法。**SHA** 包括**SHA-1**、**SHA-2**和**SHA-3**三个版本。该算法的基本思想是:接收一段明文数据,通过不可逆的方式将其转换为固定长度的密文。简单来说,SHA 将输入数据(即预映射或消息)转化为固定长度、较短的输出值,称为散列值(或信息摘要、信息认证码)。SHA-1 已被证明不够安全,因此逐渐被 SHA-2 取代,而 SHA-3 则作为 SHA 系列的最新版本,采用不同的结构(Keccak 算法)提供更高的安全性和灵活性。 **SM3** -国密算法**SM3**。加密强度和 SHA-256 想不多。主要是收到国家的支持。 +国密算法**SM3**。加密强度和 SHA-256 算法 相差不多。主要是受到了国家的支持。 **总结**: -![图片](https://p3-juejin.byteimg.com/tos-cn-i-k3u1fbpfcp/79c3c2f72d2f44c7abf2d73a49024495~tplv-k3u1fbpfcp-zoom-1.image) +![图片](https://oss.javaguide.cn/p3-juejin/79c3c2f72d2f44c7abf2d73a49024495~tplv-k3u1fbpfcp-zoom-1.png) **大部分情况下使用对称加密,具有比较不错的安全性。如果需要分布式进行秘钥分发,考虑非对称。如果不需要可逆计算则散列算法。** 因为这段时间有这方面需求,就看了一些这方面的资料,入坑信息安全,就怕以后洗发水都不用买。谢谢大家查看! @@ -383,7 +383,7 @@ MD5 可以用来生成一个 128 位的消息摘要,它是目前应用比较 证书之所以会有信用,是因为证书的签发方拥有信用。所以如果 Sum 想让 Mike 承认自己的公钥,Sum 不会直接将公钥给 Mike ,而是提供由第三方机构,含有公钥的证书。如果 Mike 也信任这个机构,法律都认可,那 ik,信任关系成立 -![](https://p3-juejin.byteimg.com/tos-cn-i-k3u1fbpfcp/b1a3dbf87e3e41ff894f39512a10f66d~tplv-k3u1fbpfcp-zoom-1.image) +![](https://oss.javaguide.cn/p3-juejin/b1a3dbf87e3e41ff894f39512a10f66d~tplv-k3u1fbpfcp-zoom-1.png) 如上图所示,Sum 将自己的申请提交给机构,产生证书的原文。机构用自己的私钥签名 Sum 的申请原文(先根据原文内容计算摘要,再用私钥加密),得到带有签名信息的证书。Mike 拿到带签名信息的证书,通过第三方机构的公钥进行解密,获得 Sum 证书的摘要、证书的原文。有了 Sum 证书的摘要和原文,Mike 就可以进行验签。验签通过,Mike 就可以确认 Sum 的证书的确是第三方机构签发的。 @@ -391,7 +391,7 @@ MD5 可以用来生成一个 128 位的消息摘要,它是目前应用比较 为了让这个信任条更加稳固,就需要环环相扣,打造更长的信任链,避免单点信任风险 -![](https://p3-juejin.byteimg.com/tos-cn-i-k3u1fbpfcp/1481f0409da94ba6bb0fee69bf0996f8~tplv-k3u1fbpfcp-zoom-1.image) +![](https://oss.javaguide.cn/p3-juejin/1481f0409da94ba6bb0fee69bf0996f8~tplv-k3u1fbpfcp-zoom-1.png) 上图中,由信誉最好的根证书机构提供根证书,然后根证书机构去签发二级机构的证书;二级机构去签发三级机构的证书;最后有由三级机构去签发 Sum 证书。 @@ -407,7 +407,7 @@ MD5 可以用来生成一个 128 位的消息摘要,它是目前应用比较 既然知道了中间人攻击的原理也知道了他的危险,现在我们看看如何避免。相信我们都遇到过下面这种状况: -![](https://p3-juejin.byteimg.com/tos-cn-i-k3u1fbpfcp/0dde4b76be6240699312d822a3fe1ed3~tplv-k3u1fbpfcp-zoom-1.image) +![](https://oss.javaguide.cn/p3-juejin/0dde4b76be6240699312d822a3fe1ed3~tplv-k3u1fbpfcp-zoom-1.png) 出现这个界面的很多情况下,都是遇到了中间人攻击的现象,需要对安全证书进行及时地监测。而且大名鼎鼎的 github 网站,也曾遭遇过中间人攻击: @@ -462,7 +462,9 @@ CDN 加速,我们可以这么理解:为了减少流氓骚扰,我干脆将 ## 参考 -- HTTP 洪水攻击 - CloudFlare:https://www.cloudflare.com/zh-cn/learning/ddos/http-flood-ddos-attack/ -- SYN 洪水攻击:https://www.cloudflare.com/zh-cn/learning/ddos/syn-flood-ddos-attack/ -- 什么是 IP 欺骗?:https://www.cloudflare.com/zh-cn/learning/ddos/glossary/ip-spoofing/ -- 什么是 DNS 洪水?| DNS 洪水 DDoS 攻击:https://www.cloudflare.com/zh-cn/learning/ddos/dns-flood-ddos-attack/ +- HTTP 洪水攻击 - CloudFlare: +- SYN 洪水攻击: +- 什么是 IP 欺骗?: +- 什么是 DNS 洪水?| DNS 洪水 DDoS 攻击: + + 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 4fc5d81c6da..34092a336b6 100644 --- a/docs/cs-basics/network/osi-and-tcp-ip-model.md +++ b/docs/cs-basics/network/osi-and-tcp-ip-model.md @@ -78,7 +78,7 @@ OSI 七层模型虽然失败了,但是却提供了很多不错的理论基础 ![传输层常见协议](https://oss.javaguide.cn/github/javaguide/cs-basics/network/transport-layer-protocol.png) -- **TCP(Transmisson Control Protocol,传输控制协议 )**:提供 **面向连接** 的,**可靠** 的数据传输服务。 +- **TCP(Transmission Control Protocol,传输控制协议 )**:提供 **面向连接** 的,**可靠** 的数据传输服务。 - **UDP(User Datagram Protocol,用户数据协议)**:提供 **无连接** 的,**尽最大努力** 的数据传输服务(不保证数据传输的可靠性),简单高效。 ### 网络层(Network layer) @@ -95,7 +95,7 @@ OSI 七层模型虽然失败了,但是却提供了很多不错的理论基础 **网络层常见协议**: -![网络层常见协议](https://oss.javaguide.cn/github/javaguide/cs-basics/network/nerwork-layer-protocol.png) +![网络层常见协议](images/network-model/nerwork-layer-protocol.png) - **IP(Internet Protocol,网际协议)**:TCP/IP 协议中最重要的协议之一,主要作用是定义数据包的格式、对数据包进行路由和寻址,以便它们可以跨网络传播并到达正确的目的地。目前 IP 协议主要分为两种,一种是过去的 IPv4,另一种是较新的 IPv6,目前这两种协议都在使用,但后者已经被提议来取代前者。 - **ARP(Address Resolution Protocol,地址解析协议)**:ARP 协议解决的是网络层地址和链路层地址之间的转换问题。因为一个 IP 数据报在物理上传输的过程中,总是需要知道下一跳(物理上的下一个目的地)该去往何处,但 IP 地址属于逻辑地址,而 MAC 地址才是物理地址,ARP 协议解决了 IP 地址转 MAC 地址的一些问题。 @@ -132,7 +132,7 @@ OSI 七层模型虽然失败了,但是却提供了很多不错的理论基础 - SSH(Secure Shell Protocol,安全的网络传输协议) - RTP(Real-time Transport Protocol,实时传输协议) - DNS(Domain Name System,域名管理系统) -- ...... +- …… **传输层协议** : @@ -154,7 +154,7 @@ OSI 七层模型虽然失败了,但是却提供了很多不错的理论基础 - OSPF(Open Shortest Path First,开放式最短路径优先) - RIP(Routing Information Protocol,路由信息协议) - BGP(Border Gateway Protocol,边界网关协议) -- ...... +- …… **网络接口层** : @@ -163,7 +163,7 @@ OSI 七层模型虽然失败了,但是却提供了很多不错的理论基础 - CSMA/CD 协议 - MAC 协议 - 以太网技术 -- ...... +- …… ## 网络分层的原因 @@ -189,5 +189,7 @@ OSI 七层模型虽然失败了,但是却提供了很多不错的理论基础 ## 参考 -- TCP/IP model vs OSI model:https://fiberbit.com.tw/tcpip-model-vs-osi-model/ -- Data Encapsulation and the TCP/IP Protocol Stack:https://docs.oracle.com/cd/E19683-01/806-4075/ipov-32/index.html +- TCP/IP model vs OSI model: +- Data Encapsulation and the TCP/IP Protocol Stack: + + diff --git a/docs/cs-basics/network/other-network-questions.md b/docs/cs-basics/network/other-network-questions.md index 038b4ef794e..0b852b063ac 100644 --- a/docs/cs-basics/network/other-network-questions.md +++ b/docs/cs-basics/network/other-network-questions.md @@ -15,7 +15,7 @@ tag: #### OSI 七层模型是什么?每一层的作用是什么? -**OSI 七层模型** 是国际标准化组织提出一个网络分层模型,其大体结构以及每一层提供的功能如下图所示: +**OSI 七层模型** 是国际标准化组织提出的一个网络分层模型,其大体结构以及每一层提供的功能如下图所示: ![OSI 七层模型](https://oss.javaguide.cn/github/javaguide/cs-basics/network/osi-7-model.png) @@ -55,7 +55,7 @@ tag: 好了,再来说回:“为什么网络要分层?”。我觉得主要有 3 方面的原因: 1. **各层之间相互独立**:各层之间相互独立,各层之间不需要关心其他层是如何实现的,只需要知道自己如何调用下层提供好的功能就可以了(可以简单理解为接口调用)**。这个和我们对开发时系统进行分层是一个道理。** -2. **提高了整体灵活性**:每一层都可以使用最适合的技术来实现,你只需要保证你提供的功能以及暴露的接口的规则没有改变就行了。**这个和我们平时开发系统的时候要求的高内聚、低耦合的原则也是可以对应上的。** +2. **提高了灵活性和可替换性**:每一层都可以使用最适合的技术来实现,你只需要保证你提供的功能以及暴露的接口的规则没有改变就行了。并且,每一层都可以根据需要进行修改或替换,而不会影响到整个网络的结构。**这个和我们平时开发系统的时候要求的高内聚、低耦合的原则也是可以对应上的。** 3. **大问题化小**:分层可以将复杂的网络问题分解为许多比较小的、界线比较清晰简单的小问题来处理和解决。这样使得复杂的计算机网络系统变得易于设计,实现和标准化。 **这个和我们平时开发的时候,一般会将系统功能分解,然后将复杂的问题分解为容易理解的更小的问题是相对应的,这些较小的问题具有更好的边界(目标和接口)定义。** 我想到了计算机世界非常非常有名的一句话,这里分享一下: @@ -88,13 +88,13 @@ tag: #### 网络层有哪些常见的协议? -![网络层常见协议](https://oss.javaguide.cn/github/javaguide/cs-basics/network/nerwork-layer-protocol.png) +![网络层常见协议](images/network-model/nerwork-layer-protocol.png) - **IP(Internet Protocol,网际协议)**:TCP/IP 协议中最重要的协议之一,属于网络层的协议,主要作用是定义数据包的格式、对数据包进行路由和寻址,以便它们可以跨网络传播并到达正确的目的地。目前 IP 协议主要分为两种,一种是过去的 IPv4,另一种是较新的 IPv6,目前这两种协议都在使用,但后者已经被提议来取代前者。 - **ARP(Address Resolution Protocol,地址解析协议)**:ARP 协议解决的是网络层地址和链路层地址之间的转换问题。因为一个 IP 数据报在物理上传输的过程中,总是需要知道下一跳(物理上的下一个目的地)该去往何处,但 IP 地址属于逻辑地址,而 MAC 地址才是物理地址,ARP 协议解决了 IP 地址转 MAC 地址的一些问题。 - **ICMP(Internet Control Message Protocol,互联网控制报文协议)**:一种用于传输网络状态和错误消息的协议,常用于网络诊断和故障排除。例如,Ping 工具就使用了 ICMP 协议来测试网络连通性。 - **NAT(Network Address Translation,网络地址转换协议)**:NAT 协议的应用场景如同它的名称——网络地址转换,应用于内部网到外部网的地址转换过程中。具体地说,在一个小的子网(局域网,LAN)内,各主机使用的是同一个 LAN 下的 IP 地址,但在该 LAN 以外,在广域网(WAN)中,需要一个统一的 IP 地址来标识该 LAN 在整个 Internet 上的位置。 -- **OSPF(Open Shortest Path First,开放式最短路径优先)** ):一种内部网关协议(Interior Gateway Protocol,IGP),也是广泛使用的一种动态路由协议,基于链路状态算法,考虑了链路的带宽、延迟等因素来选择最佳路径。 +- **OSPF(Open Shortest Path First,开放式最短路径优先)**:一种内部网关协议(Interior Gateway Protocol,IGP),也是广泛使用的一种动态路由协议,基于链路状态算法,考虑了链路的带宽、延迟等因素来选择最佳路径。 - **RIP(Routing Information Protocol,路由信息协议)**:一种内部网关协议(Interior Gateway Protocol,IGP),也是一种动态路由协议,基于距离向量算法,使用固定的跳数作为度量标准,选择跳数最少的路径作为最佳路径。 - **BGP(Border Gateway Protocol,边界网关协议)**:一种用来在路由选择域之间交换网络层可达性信息(Network Layer Reachability Information,NLRI)的路由选择协议,具有高度的灵活性和可扩展性。 @@ -104,25 +104,23 @@ tag: > 类似的问题:打开一个网页,整个过程会使用哪些协议? -图解(图片来源:《图解 HTTP》): +先来看一张图(来源于《图解 HTTP》): - + -> 上图有一个错误,请注意,是 OSPF 不是 OPSF。 OSPF(Open Shortest Path First,ospf)开放最短路径优先协议, 是由 Internet 工程任务组开发的路由选择协议 +上图有一个错误需要注意:是 OSPF 不是 OPSF。 OSPF(Open Shortest Path First,ospf)开放最短路径优先协议, 是由 Internet 工程任务组开发的路由选择协议 -总体来说分为以下几个过程: +总体来说分为以下几个步骤: -1. DNS 解析 -2. TCP 连接 -3. 发送 HTTP 请求 -4. 服务器处理请求并返回 HTTP 报文 -5. 浏览器解析渲染页面 -6. 连接结束 +1. 在浏览器中输入指定网页的 URL。 +2. 浏览器通过 DNS 协议,获取域名对应的 IP 地址。 +3. 浏览器根据 IP 地址和端口号,向目标服务器发起一个 TCP 连接请求。 +4. 浏览器在 TCP 连接上,向服务器发送一个 HTTP 请求报文,请求获取网页的内容。 +5. 服务器收到 HTTP 请求报文后,处理请求,并返回 HTTP 响应报文给浏览器。 +6. 浏览器收到 HTTP 响应报文后,解析响应体中的 HTML 代码,渲染网页的结构和样式,同时根据 HTML 中的其他资源的 URL(如图片、CSS、JS 等),再次发起 HTTP 请求,获取这些资源的内容,直到网页完全加载显示。 +7. 浏览器在不需要和服务器通信时,可以主动关闭 TCP 连接,或者等待服务器的关闭请求。 -具体可以参考下面这两篇文章: - -- [从输入 URL 到页面加载发生了什么?](https://segmentfault.com/a/1190000006879700) -- [浏览器从输入网址到页面展示的过程](https://cloud.tencent.com/developer/article/1879758) +详细介绍可以查看这篇文章:[访问网页的全过程(知识串联)](./the-whole-process-of-accessing-web-pages.md)(强烈推荐)。 ### HTTP 状态码有哪些? @@ -134,44 +132,44 @@ HTTP 状态码用于描述 HTTP 请求的结果,比如 2xx 就代表请求被 ### HTTP Header 中常见的字段有哪些? -| 请求头字段名 | 说明 | 示例 | -| :------------------ | :------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ | :----------------------------------------------------------------------------------------- | -| Accept | 能够接受的回应内容类型(Content-Types)。 | Accept: text/plain | -| Accept-Charset | 能够接受的字符集 | Accept-Charset: utf-8 | -| Accept-Datetime | 能够接受的按照时间来表示的版本 | Accept-Datetime: Thu, 31 May 2007 20:35:00 GMT | -| Accept-Encoding | 能够接受的编码方式列表。参考 HTTP 压缩。 | Accept-Encoding: gzip, deflate | -| Accept-Language | 能够接受的回应内容的自然语言列表。 | Accept-Language: en-US | -| Authorization | 用于超文本传输协议的认证的认证信息 | Authorization: Basic QWxhZGRpbjpvcGVuIHNlc2FtZQ== | -| Cache-Control | 用来指定在这次的请求/响应链中的所有缓存机制 都必须 遵守的指令 | Cache-Control: no-cache | -| Connection | 该浏览器想要优先使用的连接类型 | Connection: keep-alive Connection: Upgrade | -| Content-Length | 以 八位字节数组 (8 位的字节)表示的请求体的长度 | Content-Length: 348 | -| Content-MD5 | 请求体的内容的二进制 MD5 散列值,以 Base64 编码的结果 | Content-MD5: Q2hlY2sgSW50ZWdyaXR5IQ== | -| Content-Type | 请求体的 多媒体类型 (用于 POST 和 PUT 请求中) | Content-Type: application/x-www-form-urlencoded | -| Cookie | 之前由服务器通过 Set- Cookie (下文详述)发送的一个 超文本传输协议 Cookie | Cookie: \$Version=1; Skin=new; | -| Date | 发送该消息的日期和时间(按照 RFC 7231 中定义的"超文本传输协议日期"格式来发送) | Date: Tue, 15 Nov 1994 08:12:31 GMT | -| Expect | 表明客户端要求服务器做出特定的行为 | Expect: 100-continue | -| From | 发起此请求的用户的邮件地址 | From: [user@example.com](mailto:user@example.com) | -| Host | 服务器的域名(用于虚拟主机 ),以及服务器所监听的传输控制协议端口号。如果所请求的端口是对应的服务的标准端口,则端口号可被省略。 | Host: en.wikipedia.org:80 | -| If-Match | 仅当客户端提供的实体与服务器上对应的实体相匹配时,才进行对应的操作。主要作用时,用作像 PUT 这样的方法中,仅当从用户上次更新某个资源以来,该资源未被修改的情况下,才更新该资源。 | If-Match: “737060cd8c284d8af7ad3082f209582d” | -| If-Modified-Since | 允许在对应的内容未被修改的情况下返回 304 未修改( 304 Not Modified ) | If-Modified-Since: Sat, 29 Oct 1994 19:43:31 GMT | -| If-None-Match | 允许在对应的内容未被修改的情况下返回 304 未修改( 304 Not Modified ) | If-None-Match: “737060cd8c284d8af7ad3082f209582d” | -| If-Range | 如果该实体未被修改过,则向我发送我所缺少的那一个或多个部分;否则,发送整个新的实体 | If-Range: “737060cd8c284d8af7ad3082f209582d” | -| If-Unmodified-Since | 仅当该实体自某个特定时间已来未被修改的情况下,才发送回应。 | If-Unmodified-Since: Sat, 29 Oct 1994 19:43:31 GMT | -| Max-Forwards | 限制该消息可被代理及网关转发的次数。 | Max-Forwards: 10 | -| Origin | 发起一个针对 跨来源资源共享 的请求。 | Origin: [http://www.example-social-network.com](http://www.example-social-network.com/) | -| Pragma | 与具体的实现相关,这些字段可能在请求/回应链中的任何时候产生多种效果。 | Pragma: no-cache | -| Proxy-Authorization | 用来向代理进行认证的认证信息。 | Proxy-Authorization: Basic QWxhZGRpbjpvcGVuIHNlc2FtZQ== | -| Range | 仅请求某个实体的一部分。字节偏移以 0 开始。参见字节服务。 | Range: bytes=500-999 | -| Referer | 表示浏览器所访问的前一个页面,正是那个页面上的某个链接将浏览器带到了当前所请求的这个页面。 | Referer: [http://en.wikipedia.org/wiki/Main_Page](https://en.wikipedia.org/wiki/Main_Page) | -| TE | 浏览器预期接受的传输编码方式:可使用回应协议头 Transfer-Encoding 字段中的值; | TE: trailers, deflate | -| Upgrade | 要求服务器升级到另一个协议。 | Upgrade: HTTP/2.0, SHTTP/1.3, IRC/6.9, RTA/x11 | -| User-Agent | 浏览器的浏览器身份标识字符串 | User-Agent: Mozilla/5.0 (X11; Linux x86_64; rv:12.0) Gecko/20100101 Firefox/21.0 | -| Via | 向服务器告知,这个请求是由哪些代理发出的。 | Via: 1.0 fred, 1.1 example.com (Apache/1.1) | -| Warning | 一个一般性的警告,告知,在实体内容体中可能存在错误。 | Warning: 199 Miscellaneous warning | +| 请求头字段名 | 说明 | 示例 | +| :------------------ | :---------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | :------------------------------------------------------------------------------- | +| Accept | 能够接受的回应内容类型(Content-Types)。 | Accept: text/plain | +| Accept-Charset | 能够接受的字符集 | Accept-Charset: utf-8 | +| Accept-Datetime | 能够接受的按照时间来表示的版本 | Accept-Datetime: Thu, 31 May 2007 20:35:00 GMT | +| Accept-Encoding | 能够接受的编码方式列表。参考 HTTP 压缩。 | Accept-Encoding: gzip, deflate | +| Accept-Language | 能够接受的回应内容的自然语言列表。 | Accept-Language: en-US | +| Authorization | 用于超文本传输协议的认证的认证信息 | Authorization: Basic QWxhZGRpbjpvcGVuIHNlc2FtZQ== | +| Cache-Control | 用来指定在这次的请求/响应链中的所有缓存机制 都必须 遵守的指令 | Cache-Control: no-cache | +| Connection | 该浏览器想要优先使用的连接类型 | Connection: keep-alive | +| Content-Length | 以八位字节数组(8 位的字节)表示的请求体的长度 | Content-Length: 348 | +| Content-MD5 | 请求体的内容的二进制 MD5 散列值,以 Base64 编码的结果 | Content-MD5: Q2hlY2sgSW50ZWdyaXR5IQ== | +| Content-Type | 请求体的多媒体类型(用于 POST 和 PUT 请求中) | Content-Type: application/x-www-form-urlencoded | +| Cookie | 之前由服务器通过 Set-Cookie(下文详述)发送的一个超文本传输协议 Cookie | Cookie: $Version=1; Skin=new; | +| Date | 发送该消息的日期和时间(按照 RFC 7231 中定义的"超文本传输协议日期"格式来发送) | Date: Tue, 15 Nov 1994 08:12:31 GMT | +| Expect | 表明客户端要求服务器做出特定的行为 | Expect: 100-continue | +| From | 发起此请求的用户的邮件地址 | From: `user@example.com` | +| Host | 服务器的域名(用于虚拟主机),以及服务器所监听的传输控制协议端口号。如果所请求的端口是对应的服务的标准端口,则端口号可被省略。 | Host: en.wikipedia.org | +| If-Match | 仅当客户端提供的实体与服务器上对应的实体相匹配时,才进行对应的操作。主要作用是用于像 PUT 这样的方法中,仅当从用户上次更新某个资源以来,该资源未被修改的情况下,才更新该资源。 | If-Match: "737060cd8c284d8af7ad3082f209582d" | +| If-Modified-Since | 允许服务器在请求的资源自指定的日期以来未被修改的情况下返回 `304 Not Modified` 状态码 | If-Modified-Since: Sat, 29 Oct 1994 19:43:31 GMT | +| If-None-Match | 允许服务器在请求的资源的 ETag 未发生变化的情况下返回 `304 Not Modified` 状态码 | If-None-Match: "737060cd8c284d8af7ad3082f209582d" | +| If-Range | 如果该实体未被修改过,则向我发送我所缺少的那一个或多个部分;否则,发送整个新的实体 | If-Range: "737060cd8c284d8af7ad3082f209582d" | +| If-Unmodified-Since | 仅当该实体自某个特定时间以来未被修改的情况下,才发送回应。 | If-Unmodified-Since: Sat, 29 Oct 1994 19:43:31 GMT | +| Max-Forwards | 限制该消息可被代理及网关转发的次数。 | Max-Forwards: 10 | +| Origin | 发起一个针对跨来源资源共享的请求。 | `Origin: http://www.example-social-network.com` | +| Pragma | 与具体的实现相关,这些字段可能在请求/回应链中的任何时候产生多种效果。 | Pragma: no-cache | +| Proxy-Authorization | 用来向代理进行认证的认证信息。 | Proxy-Authorization: Basic QWxhZGRpbjpvcGVuIHNlc2FtZQ== | +| Range | 仅请求某个实体的一部分。字节偏移以 0 开始。参见字节服务。 | Range: bytes=500-999 | +| Referer | 表示浏览器所访问的前一个页面,正是那个页面上的某个链接将浏览器带到了当前所请求的这个页面。 | `Referer: http://en.wikipedia.org/wiki/Main_Page` | +| TE | 浏览器预期接受的传输编码方式:可使用回应协议头 Transfer-Encoding 字段中的值; | TE: trailers, deflate | +| Upgrade | 要求服务器升级到另一个协议。 | Upgrade: HTTP/2.0, SHTTP/1.3, IRC/6.9, RTA/x11 | +| User-Agent | 浏览器的浏览器身份标识字符串 | User-Agent: Mozilla/5.0 (X11; Linux x86_64; rv:12.0) Gecko/20100101 Firefox/21.0 | +| Via | 向服务器告知,这个请求是由哪些代理发出的。 | Via: 1.0 fred, 1.1 example.com (Apache/1.1) | +| Warning | 一个一般性的警告,告知,在实体内容体中可能存在错误。 | Warning: 199 Miscellaneous warning | ### HTTP 和 HTTPS 有什么区别?(重要) -![HTTP 和 HTTPS 对比](https://oss.javaguide.cn/github/javaguide/cs-basics/network/http1.0-vs-http1.1.png) +![HTTP 和 HTTPS 对比](https://oss.javaguide.cn/github/javaguide/cs-basics/network/http-vs-https.png) - **端口号**:HTTP 默认是 80,HTTPS 默认是 443。 - **URL 前缀**:HTTP 的 URL 前缀是 `http://`,HTTPS 的 URL 前缀是 `https://`。 @@ -184,7 +182,7 @@ HTTP 状态码用于描述 HTTP 请求的结果,比如 2xx 就代表请求被 ![HTTP/1.0 和 HTTP/1.1 对比](https://oss.javaguide.cn/github/javaguide/cs-basics/network/http1.0-vs-http1.1.png) -- **连接方式** : HTTP/1.0 为短连接,HTTP/1.1 支持长连接。 +- **连接方式** : HTTP/1.0 为短连接,HTTP/1.1 支持长连接。HTTP 协议的长连接和短连接,实质上是 TCP 协议的长连接和短连接。 - **状态响应码** : HTTP/1.1 中新加入了大量的状态码,光是错误响应状态码就新增了 24 种。比如说,`100 (Continue)`——在请求大资源前的预热请求,`206 (Partial Content)`——范围请求的标识码,`409 (Conflict)`——请求与当前资源的规定冲突,`410 (Gone)`——资源已被永久转移,而且没有任何已知的转发地址。 - **缓存机制** : 在 HTTP/1.0 中主要使用 Header 里的 If-Modified-Since,Expires 来做为缓存判断的标准,HTTP/1.1 则引入了更多的缓存控制策略例如 Entity tag,If-Unmodified-Since, If-Match, If-None-Match 等更多可供选择的缓存头来控制缓存策略。 - **带宽**:HTTP/1.0 中,存在一些浪费带宽的现象,例如客户端只是需要某个对象的一部分,而服务器却将整个对象送过来了,并且不支持断点续传功能,HTTP/1.1 则在请求头引入了 range 头域,它允许只请求资源的某个部分,即返回码是 206(Partial Content),这样就方便了开发者自由的选择以便于充分利用带宽和连接。 @@ -196,24 +194,71 @@ HTTP 状态码用于描述 HTTP 请求的结果,比如 2xx 就代表请求被 ![HTTP/1.0 和 HTTP/1.1 对比](https://oss.javaguide.cn/github/javaguide/cs-basics/network/http1.1-vs-http2.0.png) -- **IO 多路复用(Multiplexing)**:HTTP/2.0 在同一连接上可以同时传输多个请求和响应(可以看作是 HTTP/1.1 中长链接的升级版本)。HTTP/1.1 则使用串行方式,每个请求和响应都需要独立的连接。这使得 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`压缩,减少了网络开销。 +- **队头阻塞**: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 需要客户端自己发送请求来获取相关资源。 +HTTP/2.0 多路复用效果图(图源: [HTTP/2 For Web Developers](https://blog.cloudflare.com/http-2-for-web-developers/)): + +![HTTP/2 Multiplexing](https://oss.javaguide.cn/github/javaguide/cs-basics/network/http2.0-multiplexing.png) + +可以看到,HTTP/2 的多路复用机制允许多个请求和响应共享一个 TCP 连接,从而避免了 HTTP/1.1 在应对并发请求时需要建立多个并行连接的情况,减少了重复连接建立和维护的额外开销。而在 HTTP/1.1 中,尽管支持持久连接,但为了缓解队头阻塞问题,浏览器通常会为同一域名建立多个并行连接。 + ### HTTP/2.0 和 HTTP/3.0 有什么区别? ![HTTP/2.0 和 HTTP/3.0 对比](https://oss.javaguide.cn/github/javaguide/cs-basics/network/http2.0-vs-http3.0.png) - **传输协议**:HTTP/2.0 是基于 TCP 协议实现的,HTTP/3.0 新增了 QUIC(Quick UDP Internet Connections) 协议来实现可靠的传输,提供与 TLS/SSL 相当的安全性,具有较低的连接和传输延迟。你可以将 QUIC 看作是 UDP 的升级版本,在其基础上新增了很多功能比如加密、重传等等。HTTP/3.0 之前名为 HTTP-over-QUIC,从这个名字中我们也可以发现,HTTP/3 最大的改造就是使用了 QUIC。 -- **连接建立**:HTTP/2.0 需要经过经典的 TCP 三次握手过程(一般是 3 个 RTT)。由于 QUIC 协议的特性,HTTP/3.0 可以避免 TCP 三次握手的延迟,允许在第一次连接时发送数据(0 个 RTT ,零往返时间)。 +- **连接建立**:HTTP/2.0 需要经过经典的 TCP 三次握手过程(由于安全的 HTTPS 连接建立还需要 TLS 握手,共需要大约 3 个 RTT)。由于 QUIC 协议的特性(TLS 1.3,TLS 1.3 除了支持 1 个 RTT 的握手,还支持 0 个 RTT 的握手)连接建立仅需 0-RTT 或者 1-RTT。这意味着 QUIC 在最佳情况下不需要任何的额外往返时间就可以建立新连接。 +- **头部压缩**:HTTP/2.0 使用 HPACK 算法进行头部压缩,而 HTTP/3.0 使用更高效的 QPACK 头压缩算法。 - **队头阻塞**:HTTP/2.0 多请求复用一个 TCP 连接,一旦发生丢包,就会阻塞住所有的 HTTP 请求。由于 QUIC 协议的特性,HTTP/3.0 在一定程度上解决了队头阻塞(Head-of-Line blocking, 简写:HOL blocking)问题,一个连接建立多个不同的数据流,这些数据流之间独立互不影响,某个数据流发生丢包了,其数据流不受影响(本质上是多路复用+轮询)。 +- **连接迁移**:HTTP/3.0 支持连接迁移,因为 QUIC 使用 64 位 ID 标识连接,只要 ID 不变就不会中断,网络环境改变时(如从 Wi-Fi 切换到移动数据)也能保持连接。而 TCP 连接是由(源 IP,源端口,目的 IP,目的端口)组成,这个四元组中一旦有一项值发生改变,这个连接也就不能用了。 - **错误恢复**:HTTP/3.0 具有更好的错误恢复机制,当出现丢包、延迟等网络问题时,可以更快地进行恢复和重传。而 HTTP/2.0 则需要依赖于 TCP 的错误恢复机制。 -- **安全性**:HTTP/2.0 和 HTTP/3.0 在安全性上都有较高的要求,支持加密通信,但在实现上有所不同。HTTP/2.0 使用 TLS 协议进行加密,而 HTTP/3.0 基于 QUIC 协议,包含了内置的加密和身份验证机制,可以提供更强的安全性。 +- **安全性**:在 HTTP/2.0 中,TLS 用于加密和认证整个 HTTP 会话,包括所有的 HTTP 头部和数据负载。TLS 的工作是在 TCP 层之上,它加密的是在 TCP 连接中传输的应用层的数据,并不会对 TCP 头部以及 TLS 记录层头部进行加密,所以在传输的过程中 TCP 头部可能会被攻击者篡改来干扰通信。而 HTTP/3.0 的 QUIC 对整个数据包(包括报文头和报文体)进行了加密与认证处理,保障安全性。 + +HTTP/1.0、HTTP/2.0 和 HTTP/3.0 的协议栈比较: + +![http-3-implementation](https://oss.javaguide.cn/github/javaguide/cs-basics/network/http-3-implementation.png) + +下图是一个更详细的 HTTP/2.0 和 HTTP/3.0 对比图: + +![HTTP/2.0 和 HTTP/3.0 详细对比图](https://oss.javaguide.cn/github/javaguide/cs-basics/network/http2-and-http3-stacks-comparison.png) + +从上图可以看出: + +- **HTTP/2.0**:使用 TCP 作为传输协议、使用 HPACK 进行头部压缩、依赖 TLS 进行加密。 +- **HTTP/3.0**:使用基于 UDP 的 QUIC 协议、使用更高效的 QPACK 进行头部压缩、在 QUIC 中直接集成了 TLS。QUIC 协议具备连接迁移、拥塞控制与避免、流量控制等特性。 + +关于 HTTP/1.0 -> HTTP/3.0 更详细的演进介绍,推荐阅读[HTTP1 到 HTTP3 的工程优化](https://dbwu.tech/posts/http_evolution/)。 + +### HTTP/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)。 +HTTP 是一种不保存状态,即无状态(stateless)协议。也就是说 HTTP 协议自身不对请求和响应之间的通信状态进行保存。那么我们如何保存用户状态呢?Session 机制的存在就是为了解决这个问题,Session 的主要作用就是通过服务端记录用户的状态。典型的场景是购物车,当你要添加商品到购物车的时候,系统不知道是哪个用户操作的,因为 HTTP 协议是无状态的。服务端给特定的用户创建特定的 Session 之后就可以标识这个用户并且跟踪这个用户了(一般情况下,服务器会在一定时间内保存这个 Session,过了时间限制,就会销毁这个 Session)。 在服务端保存 Session 的方法很多,最常用的就是内存和数据库(比如是使用内存数据库 redis 保存)。既然 Session 存放在服务器端,那么我们如何实现 Session 跟踪呢?大部分情况下,我们都是通过在 Cookie 中附加一个 Session ID 来方式来跟踪。 @@ -232,6 +277,130 @@ URI 的作用像身份证号一样,URL 的作用更像家庭住址一样。URL 准确点来说,这个问题属于认证授权的范畴,你可以在 [认证授权基础概念详解](../../system-design/security/basis-of-authority-certification.md) 这篇文章中找到详细的答案。 +### GET 和 POST 的区别 + +这个问题在知乎上被讨论的挺火热的,地址: 。 + +![](https://static001.geekbang.org/infoq/04/0454a5fff1437c32754f1dfcc3881148.png) + +GET 和 POST 是 HTTP 协议中两种常用的请求方法,它们在不同的场景和目的下有不同的特点和用法。一般来说,可以从以下几个方面来区分二者(重点搞清两者在语义上的区别即可): + +- 语义(主要区别):GET 通常用于获取或查询资源,而 POST 通常用于创建或修改资源。 +- 幂等:GET 请求是幂等的,即多次重复执行不会改变资源的状态,而 POST 请求是不幂等的,即每次执行可能会产生不同的结果或影响资源的状态。 +- 格式:GET 请求的参数通常放在 URL 中,形成查询字符串(querystring),而 POST 请求的参数通常放在请求体(body)中,可以有多种编码格式,如 application/x-www-form-urlencoded、multipart/form-data、application/json 等。GET 请求的 URL 长度受到浏览器和服务器的限制,而 POST 请求的 body 大小则没有明确的限制。不过,实际上 GET 请求也可以用 body 传输数据,只是并不推荐这样做,因为这样可能会导致一些兼容性或者语义上的问题。 +- 缓存:由于 GET 请求是幂等的,它可以被浏览器或其他中间节点(如代理、网关)缓存起来,以提高性能和效率。而 POST 请求则不适合被缓存,因为它可能有副作用,每次执行可能需要实时的响应。 +- 安全性:GET 请求和 POST 请求如果使用 HTTP 协议的话,那都不安全,因为 HTTP 协议本身是明文传输的,必须使用 HTTPS 协议来加密传输数据。另外,GET 请求相比 POST 请求更容易泄露敏感数据,因为 GET 请求的参数通常放在 URL 中。 + +再次提示,重点搞清两者在语义上的区别即可,实际使用过程中,也是通过语义来区分使用 GET 还是 POST。不过,也有一些项目所有的请求都用 POST,这个并不是固定的,项目组达成共识即可。 + +## WebSocket + +### 什么是 WebSocket? + +WebSocket 是一种基于 TCP 连接的全双工通信协议,即客户端和服务器可以同时发送和接收数据。 + +WebSocket 协议在 2008 年诞生,2011 年成为国际标准,几乎所有主流较新版本的浏览器都支持该协议。不过,WebSocket 不只能在基于浏览器的应用程序中使用,很多编程语言、框架和服务器都提供了 WebSocket 支持。 + +WebSocket 协议本质上是应用层的协议,用于弥补 HTTP 协议在持久通信能力上的不足。客户端和服务器仅需一次握手,两者之间就直接可以创建持久性的连接,并进行双向数据传输。 + +![Websocket 示意图](https://oss.javaguide.cn/github/javaguide/system-design/web-real-time-message-push/1460000042192394.png) + +下面是 WebSocket 的常见应用场景: + +- 视频弹幕 +- 实时消息推送,详见[Web 实时消息推送详解](https://javaguide.cn/system-design/web-real-time-message-push.html)这篇文章 +- 实时游戏对战 +- 多用户协同编辑 +- 社交聊天 +- …… + +### WebSocket 和 HTTP 有什么区别? + +WebSocket 和 HTTP 两者都是基于 TCP 的应用层协议,都可以在网络中传输数据。 + +下面是二者的主要区别: + +- WebSocket 是一种双向实时通信协议,而 HTTP 是一种单向通信协议。并且,HTTP 协议下的通信只能由客户端发起,服务器无法主动通知客户端。 +- WebSocket 使用 ws:// 或 wss://(使用 SSL/TLS 加密后的协议,类似于 HTTP 和 HTTPS 的关系) 作为协议前缀,HTTP 使用 http:// 或 https:// 作为协议前缀。 +- WebSocket 可以支持扩展,用户可以扩展协议,实现部分自定义的子协议,如支持压缩、加密等。 +- WebSocket 通信数据格式比较轻量,用于协议控制的数据包头部相对较小,网络开销小,而 HTTP 通信每次都要携带完整的头部,网络开销较大(HTTP/2.0 使用二进制帧进行数据传输,还支持头部压缩,减少了网络开销)。 + +### WebSocket 的工作过程是什么样的? + +WebSocket 的工作过程可以分为以下几个步骤: + +1. 客户端向服务器发送一个 HTTP 请求,请求头中包含 `Upgrade: websocket` 和 `Sec-WebSocket-Key` 等字段,表示要求升级协议为 WebSocket; +2. 服务器收到这个请求后,会进行升级协议的操作,如果支持 WebSocket,它将回复一个 HTTP 101 状态码,响应头中包含 ,`Connection: Upgrade`和 `Sec-WebSocket-Accept: xxx` 等字段、表示成功升级到 WebSocket 协议。 +3. 客户端和服务器之间建立了一个 WebSocket 连接,可以进行双向的数据传输。数据以帧(frames)的形式进行传送,WebSocket 的每条消息可能会被切分成多个数据帧(最小单位)。发送端会将消息切割成多个帧发送给接收端,接收端接收消息帧,并将关联的帧重新组装成完整的消息。 +4. 客户端或服务器可以主动发送一个关闭帧,表示要断开连接。另一方收到后,也会回复一个关闭帧,然后双方关闭 TCP 连接。 + +另外,建立 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 有什么区别? + +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:** **原生支持传输文本和二进制数据**,无需额外编码。 + +为了提供更好的用户体验和利用其简单、高效、基于标准 HTTP 的特性,**Server-Sent Events (SSE) 是目前大型语言模型 API(如 OpenAI、DeepSeek 等)实现流式响应的常用甚至可以说是标准的技木选择**。 + +这里以 DeepSeek 为例,我们发送一个请求并打开浏览器控制台验证一下: + +![](https://oss.javaguide.cn/github/javaguide/cs-basics/network/deepseek-sse.png) + +![](https://oss.javaguide.cn/github/javaguide/cs-basics/network/deepseek-sse-eventstream.png) + +可以看到,响应头应里包含了 `text/event-stream`,说明使用的确实是SSE。并且,响应数据也确实是持续分块传输。 + ## PING ### PING 命令的作用是什么? @@ -262,7 +431,7 @@ PING 命令的输出结果通常包括以下几部分信息: 3. **往返时间(RTT,Round-Trip Time)**:从发送 ICMP Echo Request(请求报文)到接收到 ICMP Echo Reply(响应报文)的总时间,用来衡量网络连接的延迟。 4. **统计结果(Statistics)**:包括发送的 ICMP 请求数据包数量、接收到的 ICMP 响应数据包数量、丢包率、往返时间(RTT)的最小、平均、最大和标准偏差值。 -如果 PING 对应的目标主机无法得到正确的响应,则表明这两个主机之间的连通性存在问题。如果往返时间(RTT)过高,则表明网络延迟过高。 +如果 PING 对应的目标主机无法得到正确的响应,则表明这两个主机之间的连通性存在问题(有些主机或网络管理员可能禁用了对 ICMP 请求的回复,这样也会导致无法得到正确的响应)。如果往返时间(RTT)过高,则表明网络延迟过高。 ### PING 命令的工作原理是什么? @@ -286,11 +455,11 @@ DNS(Domain Name System)域名管理系统,是当用户使用浏览器访 ![DNS:域名系统](https://oss.javaguide.cn/github/javaguide/cs-basics/network/dns-overview.png) -在实际使用中,有一种情况下,浏览器是可以不必动用 DNS 就可以获知域名和 IP 地址的映射的。浏览器在本地会维护一个`hosts`列表,一般来说浏览器要先查看要访问的域名是否在`hosts`列表中,如果有的话,直接提取对应的 IP 地址记录,就好了。如果本地`hosts`列表内没有域名-IP 对应记录的话,那么 DNS 就闪亮登场了。 +在一台电脑上,可能存在浏览器 DNS 缓存,操作系统 DNS 缓存,路由器 DNS 缓存。如果以上缓存都查询不到,那么 DNS 就闪亮登场了。 -目前 DNS 的设计采用的是分布式、层次数据库结构,**DNS 是应用层协议,基于 UDP 协议之上,端口为 53** 。 +目前 DNS 的设计采用的是分布式、层次数据库结构,**DNS 是应用层协议,它可以在 UDP 或 TCP 协议之上运行,端口为 53** 。 -### DNS 服务器有哪些? +### DNS 服务器有哪些?根服务器有多少个? DNS 服务器自底向上可以依次分为以下几个层级(所有 DNS 服务器都属于以下四个类别之一): @@ -299,13 +468,23 @@ DNS 服务器自底向上可以依次分为以下几个层级(所有 DNS 服务 - 权威 DNS 服务器。在因特网上具有公共可访问主机的每个组织机构必须提供公共可访问的 DNS 记录,这些记录将这些主机的名字映射为 IP 地址。 - 本地 DNS 服务器。每个 ISP(互联网服务提供商)都有一个自己的本地 DNS 服务器。当主机发出 DNS 请求时,该请求被发往本地 DNS 服务器,它起着代理的作用,并将该请求转发到 DNS 层次结构中。严格说来,不属于 DNS 层级结构 +世界上并不是只有 13 台根服务器,这是很多人普遍的误解,网上很多文章也是这么写的。实际上,现在根服务器数量远远超过这个数量。最初确实是为 DNS 根服务器分配了 13 个 IP 地址,每个 IP 地址对应一个不同的根 DNS 服务器。然而,由于互联网的快速发展和增长,这个原始的架构变得不太适应当前的需求。为了提高 DNS 的可靠性、安全性和性能,目前这 13 个 IP 地址中的每一个都有多个服务器,截止到 2023 年底,所有根服务器之和达到了 1700 多台,未来还会继续增加。 + ### DNS 解析的过程是什么样的? 整个过程的步骤比较多,我单独写了一篇文章详细介绍:[DNS 域名系统详解(应用层)](./dns.md) 。 +### DNS 劫持了解吗?如何应对? + +DNS 劫持是一种网络攻击,它通过修改 DNS 服务器的解析结果,使用户访问的域名指向错误的 IP 地址,从而导致用户无法访问正常的网站,或者被引导到恶意的网站。DNS 劫持有时也被称为 DNS 重定向、DNS 欺骗或 DNS 污染。 + ## 参考 - 《图解 HTTP》 - 《计算机网络自顶向下方法》(第七版) -- 详解 HTTP/2.0 及 HTTPS 协议:https://juejin.cn/post/7034668672262242318 -- HTTP 请求头字段大全| HTTP Request Headers:https://www.flysnow.org/tools/table/http-request-headers/ +- 详解 HTTP/2.0 及 HTTPS 协议: +- HTTP 请求头字段大全| HTTP Request Headers: +- HTTP1、HTTP2、HTTP3: +- 如何看待 HTTP/3 ? - 车小胖的回答 - 知乎: + + diff --git a/docs/cs-basics/network/other-network-questions2.md b/docs/cs-basics/network/other-network-questions2.md index dd0d7047db9..67c731f44c0 100644 --- a/docs/cs-basics/network/other-network-questions2.md +++ b/docs/cs-basics/network/other-network-questions2.md @@ -11,61 +11,130 @@ 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 支持一对一、一对多、多对一、多对多; -8. ...... - -我把上面总结的内容通过表格形式展示出来了!确定不点个赞嘛? - -| | TCP | UDP | -| ---------------------- | -------------- | ---------- | -| 是否面向连接 | 是 | 否 | -| 是否可靠 | 是 | 否 | -| 是否有状态 | 是 | 否 | -| 传输效率 | 较慢 | 较快 | -| 传输形式 | 字节流 | 数据报文段 | -| 首部开销 | 20 ~ 60 bytes | 8 bytes | -| 是否提供广播或多播服务 | 否 | 是 | +1. **是否面向连接**: + - TCP 是面向连接的。在传输数据之前,必须先通过“三次握手”建立连接;数据传输完成后,还需要通过“四次挥手”来释放连接。这保证了双方都准备好通信。 + - UDP 是无连接的。发送数据前不需要建立任何连接,直接把数据包(数据报)扔出去。 +2. **是否是可靠传输**: + - TCP 提供可靠的数据传输服务。它通过序列号、确认应答 (ACK)、超时重传、流量控制、拥塞控制等一系列机制,来确保数据能够无差错、不丢失、不重复且按顺序地到达目的地。 + - UDP 提供不可靠的传输。它尽最大努力交付 (best-effort delivery),但不保证数据一定能到达,也不保证到达的顺序,更不会自动重传。收到报文后,接收方也不会主动发确认。 +3. **是否有状态**: + - TCP 是有状态的。因为要保证可靠性,TCP 需要在连接的两端维护连接状态信息,比如序列号、窗口大小、哪些数据发出去了、哪些收到了确认等。 + - UDP 是无状态的。它不维护连接状态,发送方发出数据后就不再关心它是否到达以及如何到达,因此开销更小(**这很“渣男”!**)。 +4. **传输效率**: + - TCP 因为需要建立连接、发送确认、处理重传等,其开销较大,传输效率相对较低。 + - UDP 结构简单,没有复杂的控制机制,开销小,传输效率更高,速度更快。 +5. **传输形式**: + - TCP 是面向字节流 (Byte Stream) 的。它将应用程序交付的数据视为一连串无结构的字节流,可能会对数据进行拆分或合并。 + - UDP 是面向报文 (Message Oriented) 的。应用程序交给 UDP 多大的数据块,UDP 就照样发送,既不拆分也不合并,保留了应用程序消息的边界。 +6. **首部开销**: + - TCP 的头部至少需要 20 字节,如果包含选项字段,最多可达 60 字节。 + - UDP 的头部非常简单,固定只有 8 字节。 +7. **是否提供广播或多播服务**: + - TCP 只支持点对点 (Point-to-Point) 的单播通信。 + - UDP 支持一对一 (单播)、一对多 (多播/Multicast) 和一对所有 (广播/Broadcast) 的通信方式。 +8. …… + +为了更直观地对比,可以看下面这个表格: + +| 特性 | TCP | UDP | +| ------------ | -------------------------- | ----------------------------------- | +| **连接性** | 面向连接 | 无连接 | +| **可靠性** | 可靠 | 不可靠 (尽力而为) | +| **状态维护** | 有状态 | 无状态 | +| **传输效率** | 较低 | 较高 | +| **传输形式** | 面向字节流 | 面向数据报 (报文) | +| **头部开销** | 20 - 60 字节 | 8 字节 | +| **通信模式** | 点对点 (单播) | 单播、多播、广播 | +| **常见应用** | HTTP/HTTPS, FTP, SMTP, SSH | DNS, DHCP, SNMP, TFTP, VoIP, 视频流 | ### 什么时候选择 TCP,什么时候选 UDP? -- **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? ~~**HTTP 协议是基于 TCP 协议的**,所以发送 HTTP 请求之前首先要建立 TCP 连接也就是要经历 3 次握手。~~ -🐛 修正(参见 [issue#1915](https://github.com/Snailclimb/JavaGuide/issues/1915)):HTTP/3.0 之前是基于 TCP 协议的,而 HTTP/3.0 将弃用 TCP,改用 **基于 UDP 的 QUIC 协议** 。此变化解决了 HTTP/2 中存在的队头阻塞问题。由于 HTTP/2 在单个 TCP 连接上使用了多路复用,受到 TCP 拥塞控制的影响,少量的丢包就可能导致整个 TCP 连接上的所有流被阻塞。另外,HTTP/2.0 需要经过经典的 TCP 三次握手过程(一般是 3 个 RTT)。由于 QUIC 协议的特性,HTTP/3.0 可以避免 TCP 三次握手的延迟,允许在第一次连接时发送数据(0 个 RTT ,零往返时间)。 +🐛 修正(参见 [issue#1915](https://github.com/Snailclimb/JavaGuide/issues/1915)): + +HTTP/3.0 之前是基于 TCP 协议的,而 HTTP/3.0 将弃用 TCP,改用 **基于 UDP 的 QUIC 协议** : + +- **HTTP/1.x 和 HTTP/2.0**:这两个版本的 HTTP 协议都明确建立在 TCP 之上。TCP 提供了可靠的、面向连接的传输,确保数据按序、无差错地到达,这对于网页内容的正确展示非常重要。发送 HTTP 请求前,需要先通过 TCP 的三次握手建立连接。 +- **HTTP/3.0**:这是一个重大的改变。HTTP/3 弃用了 TCP,转而使用 QUIC 协议,而 QUIC 是构建在 UDP 之上的。 + +![http-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 握手: + +1. TCP 三次握手:客户端和服务器交换 SYN 和 ACK 包,建立一个 TCP 连接。这个过程需要 1.5 个 RTT(round-trip time),即一个数据包从发送到接收的时间。 +2. TLS 握手:客户端和服务器交换密钥和证书,建立一个 TLS 加密层。这个过程需要至少 1 个 RTT(TLS 1.3)或者 2 个 RTT(TLS 1.2)。 + +所以,HTTP/2.0 的连接建立就至少需要 2.5 个 RTT(TLS 1.3)或者 3.5 个 RTT(TLS 1.2)。而在 HTTP/3.0 中,使用的 QUIC 协议(TLS 1.3,TLS 1.3 除了支持 1 个 RTT 的握手,还支持 0 个 RTT 的握手)连接建立仅需 0-RTT 或者 1-RTT。这意味着 QUIC 在最佳情况下不需要任何的额外往返时间就可以建立新连接。 相关证明可以参考下面这两个链接: -- https://zh.wikipedia.org/zh/HTTP/3 -- https://datatracker.ietf.org/doc/rfc9114/ +- +- + +### 你知道哪些基于 TCP/UDP 的协议? + +TCP (传输控制协议) 和 UDP (用户数据报协议) 是互联网传输层的两大核心协议,它们为各种应用层协议提供了基础的通信服务。以下是一些常见的、分别构建在 TCP 和 UDP 之上的应用层协议: + +**运行于 TCP 协议之上的协议 (强调可靠、有序传输):** -### 使用 TCP 的协议有哪些?使用 UDP 的协议有哪些? +| 中文全称 (缩写) | 英文全称 | 主要用途 | 说明与特性 | +| -------------------------- | ---------------------------------- | ---------------------------- | --------------------------------------------------------------------------------------------------------------------------- | +| 超文本传输协议 (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 的安全替代品。 | -**运行于 TCP 协议之上的协议**: +**运行于 UDP 协议之上的协议 (强调快速、低开销传输):** -1. **HTTP 协议**:超文本传输协议(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. ...... +| 中文全称 (缩写) | 英文全称 | 主要用途 | 说明与特性 | +| ----------------------- | ------------------------------------- | -------------------------- | ------------------------------------------------------------------------------------------------------------ | +| 超文本传输协议 (HTTP/3) | HyperText Transfer Protocol version 3 | 新一代网页传输 | 基于 **QUIC** 协议 (QUIC 本身构建于 UDP 之上),旨在减少延迟、解决 TCP 队头阻塞问题,支持 0-RTT 连接建立。 | +| 动态主机配置协议 (DHCP) | Dynamic Host Configuration Protocol | 动态分配 IP 地址及网络配置 | 客户端从服务器自动获取 IP 地址、子网掩码、网关、DNS 服务器等信息。 | +| 域名系统 (DNS) | Domain Name System | 域名到 IP 地址的解析 | **通常使用 UDP** 进行快速查询。当响应数据包过大或进行区域传送 (AXFR) 时,会**切换到 TCP** 以保证数据完整性。 | +| 实时传输协议 (RTP) | Real-time Transport Protocol | 实时音视频数据流传输 | 常用于 VoIP、视频会议、直播等。追求低延迟,允许少量丢包。通常与 RTCP 配合使用。 | +| RTP 控制协议 (RTCP) | RTP Control Protocol | RTP 流的质量监控和控制信息 | 配合 RTP 工作,提供丢包、延迟、抖动等统计信息,辅助流量控制和拥塞管理。 | +| 简单文件传输协议 (TFTP) | Trivial File Transfer Protocol | 简化的文件传输 | 功能简单,常用于局域网内无盘工作站启动、网络设备固件升级等小文件传输场景。 | +| 简单网络管理协议 (SNMP) | Simple Network Management Protocol | 网络设备的监控与管理 | 允许网络管理员查询和修改网络设备的状态信息。 | +| 网络时间协议 (NTP) | Network Time Protocol | 同步计算机时钟 | 用于在网络中的计算机之间同步时间,确保时间的一致性。 | -**运行于 UDP 协议之上的协议**: +**总结一下:** -1. **DHCP 协议**:动态主机配置协议,动态配置 IP 地址 -2. **DNS**:**域名系统(DNS,Domain Name System)将人类可读的域名 (例如,www.baidu.com) 转换为机器可读的 IP 地址 (例如,220.181.38.148)。** 我们可以将其理解为专为互联网设计的电话薄。实际上 DNS 同时支持 UDP 和 TCP 协议。 -3. ...... +- **TCP** 更适合那些对数据**可靠性、完整性和顺序性**要求高的应用,如网页浏览 (HTTP/HTTPS)、文件传输 (FTP/SFTP)、邮件收发 (SMTP/POP3/IMAP)。 +- **UDP** 则更适用于那些对**实时性要求高、能容忍少量数据丢失**的应用,如域名解析 (DNS)、实时音视频 (RTP)、在线游戏、网络管理 (SNMP) 等。 ### TCP 三次握手和四次挥手(非常重要) @@ -116,7 +185,7 @@ IP 地址过滤是一种简单的网络安全措施,实际应用中一般会 ![IPv4](https://oss.javaguide.cn/github/javaguide/cs-basics/network/Figure-1-IPv4Addressformatwithdotteddecimalnotation-29c824f6a451d48d8c27759799f0c995.png) -这么少当然不够用啦!为了解决 IP 地址耗尽的问题,最根本的办法是采用具有更大地址空间的新版本 IP 协议 - **IPv6(Internet Protocol version 6)**。IPv6 地址使用更复杂的格式,该格式使用由单或双冒号分隔的一组数字和字母,例如:2001:0db8:85a3:0000:0000:8a2e:0370:7334 。IPv4 使用 128 位互联网地址,这意味着越有 2^128(3 开头的 39 位数字,恐怖如斯) 个可用 IP 地址。 +这么少当然不够用啦!为了解决 IP 地址耗尽的问题,最根本的办法是采用具有更大地址空间的新版本 IP 协议 - **IPv6(Internet Protocol version 6)**。IPv6 地址使用更复杂的格式,该格式使用由单或双冒号分隔的一组数字和字母,例如:2001:0db8:85a3:0000:0000:8a2e:0370:7334 。IPv6 使用 128 位互联网地址,这意味着越有 2^128(3 开头的 39 位数字,恐怖如斯) 个可用 IP 地址。 ![IPv6](https://oss.javaguide.cn/github/javaguide/cs-basics/network/Figure-2-IPv6Addressformatwithhexadecimalnotation-7da3a419bd81627a9b2cef3b0efb4940.png) @@ -127,7 +196,25 @@ IP 地址过滤是一种简单的网络安全措施,实际应用中一般会 - **对标头结构进行了改进**:IPv6 标头结构相较于 IPv4 更加简化和高效,减少了处理开销,提高了网络性能。 - **可选的扩展头**:允许在 IPv6 标头中添加不同的扩展头(Extension Headers),用于实现不同类型的功能和选项。 - **ICMPv6(Internet Control Message Protocol for IPv6)**:IPv6 中的 ICMPv6 相较于 IPv4 中的 ICMP 有了一些改进,如邻居发现、路径 MTU 发现等功能的改进,从而提升了网络的可靠性和性能。 -- ...... +- …… + +### 如何获取客户端真实 IP? + +获取客户端真实 IP 的方法有多种,主要分为应用层方法、传输层方法和网络层方法。 + +**应用层方法** : + +通过 [X-Forwarded-For](https://en.wikipedia.org/wiki/X-Forwarded-For) 请求头获取,简单方便。不过,这种方法无法保证获取到的是真实 IP,这是因为 X-Forwarded-For 字段可能会被伪造。如果经过多个代理服务器,X-Forwarded-For 字段可能会有多个值(附带了整个请求链中的所有代理服务器 IP 地址)。并且,这种方法只适用于 HTTP 和 SMTP 协议。 + +**传输层方法**: + +利用 TCP Options 字段承载真实源 IP 信息。这种方法适用于任何基于 TCP 的协议,不受应用层的限制。不过,这并非是 TCP 标准所支持的,所以需要通信双方都进行改造。也就是:对于发送方来说,需要有能力把真实源 IP 插入到 TCP Options 里面。对于接收方来说,需要有能力把 TCP Options 里面的 IP 地址读取出来。 + +也可以通过 Proxy Protocol 协议来传递客户端 IP 和 Port 信息。这种方法可以利用 Nginx 或者其他支持该协议的反向代理服务器来获取真实 IP 或者在业务服务器解析真实 IP。 + +**网络层方法**: + +隧道 +DSR 模式。这种方法可以适用于任何协议,就是实施起来会比较麻烦,也存在一定限制,实际应用中一般不会使用这种方法。 ### NAT 的作用是什么? @@ -151,13 +238,13 @@ MAC 地址的全称是 **媒体访问控制地址(Media Access Control Address > 还有一点要知道的是,不仅仅是网络资源才有 IP 地址,网络设备也有 IP 地址,比如路由器。但从结构上说,路由器等网络设备的作用是组成一个网络,而且通常是内网,所以它们使用的 IP 地址通常是内网 IP,内网的设备在与内网以外的设备进行通信时,需要用到 NAT 协议。 -MAC 地址的长度为 6 字节(48 比特),地址空间大小有 280 万亿之多($2^{48}$),MAC 地址由 IEEE 统一管理与分配,理论上,一个网络设备中的网卡上的 MAC 地址是永久的。不同的网卡生产商从 IEEE 那里购买自己的 MAC 地址空间(MAC 的前 24 比特),也就是前 24 比特由 IEEE 统一管理,保证不会重复。而后 24 比特,由各家生产商自己管理,同样保证生产的两块网卡的 MAC 地址不会重复。 +MAC 地址的长度为 6 字节(48 比特),地址空间大小有 280 万亿之多( $2^{48}$ ),MAC 地址由 IEEE 统一管理与分配,理论上,一个网络设备中的网卡上的 MAC 地址是永久的。不同的网卡生产商从 IEEE 那里购买自己的 MAC 地址空间(MAC 的前 24 比特),也就是前 24 比特由 IEEE 统一管理,保证不会重复。而后 24 比特,由各家生产商自己管理,同样保证生产的两块网卡的 MAC 地址不会重复。 MAC 地址具有可携带性、永久性,身份证号永久地标识一个人的身份,不论他到哪里都不会改变。而 IP 地址不具有这些性质,当一台设备更换了网络,它的 IP 地址也就可能发生改变,也就是它在互联网中的定位发生了变化。 最后,记住,MAC 地址有一个特殊地址:FF-FF-FF-FF-FF-FF(全 1 地址),该地址表示广播地址。 -### ARP 协议解决了什么问题地位如何? +### ARP 协议解决了什么问题? ARP 协议,全称 **地址解析协议(Address Resolution Protocol)**,它解决的是网络层地址和链路层地址之间的转换问题。因为一个 IP 数据报在物理上传输的过程中,总是需要知道下一跳(物理上的下一个目的地)该去往何处,但 IP 地址属于逻辑地址,而 MAC 地址才是物理地址,ARP 协议解决了 IP 地址转 MAC 地址的一些问题。 @@ -173,5 +260,8 @@ ARP 协议,全称 **地址解析协议(Address Resolution Protocol)**, - 《图解 HTTP》 - 《计算机网络自顶向下方法》(第七版) -- 什么是 Internet 协议(IP)?:https://www.cloudflare.com/zh-cn/learning/network-layer/internet-protocol/ -- What Is NAT and What Are the Benefits of NAT Firewalls?:https://community.fs.com/blog/what-is-nat-and-what-are-the-benefits-of-nat-firewalls.html +- 什么是 Internet 协议(IP)?: +- 透传真实源 IP 的各种方法 - 极客时间: +- What Is NAT and What Are the Benefits of NAT Firewalls?: + + diff --git a/docs/cs-basics/network/tcp-connection-and-disconnection.md b/docs/cs-basics/network/tcp-connection-and-disconnection.md index 6b3efac77a1..63bc97f82c9 100644 --- a/docs/cs-basics/network/tcp-connection-and-disconnection.md +++ b/docs/cs-basics/network/tcp-connection-and-disconnection.md @@ -13,12 +13,21 @@ tag: 建立一个 TCP 连接需要“三次握手”,缺一不可: -- **一次握手**:客户端发送带有 SYN(SEQ=x) 标志的数据包 -> 服务端,然后客户端进入 **SYN_SEND** 状态,等待服务器的确认; -- **二次握手**:服务端发送带有 SYN+ACK(SEQ=y,ACK=x+1) 标志的数据包 –> 客户端,然后服务端进入 **SYN_RECV** 状态 -- **三次握手**:客户端发送带有 ACK(ACK=y+1) 标志的数据包 –> 服务端,然后客户端和服务器端都进入**ESTABLISHED** 状态,完成 TCP 三次握手。 +- **一次握手**:客户端发送带有 SYN(SEQ=x) 标志的数据包 -> 服务端,然后客户端进入 **SYN_SEND** 状态,等待服务端的确认; +- **二次握手**:服务端发送带有 SYN+ACK(SEQ=y,ACK=x+1) 标志的数据包 –> 客户端,然后服务端进入 **SYN_RECV** 状态; +- **三次握手**:客户端发送带有 ACK(ACK=y+1) 标志的数据包 –> 服务端,然后客户端和服务端都进入**ESTABLISHED** 状态,完成 TCP 三次握手。 当建立了 3 次握手之后,客户端和服务端就可以传输数据啦! +### 什么是半连接队列和全连接队列? + +在 TCP 三次握手过程中,Linux 内核会维护两个队列来管理连接请求: + +1. **半连接队列**(也称 SYN Queue):当服务端收到客户端的 SYN 请求时,此时双方还没有完全建立连接,它会把半连接状态的连接放在半连接队列。 +2. **全连接队列**(也称 Accept Queue):当服务端收到客户端对 ACK 响应时,意味着三次握手成功完成,服务端会将该连接从半连接队列移动到全连接队列。如果未收到客户端的 ACK 响应,会进行重传,重传的等待时间通常是指数增长的。如果重传次数超过系统规定的最大重传次数,系统将从半连接队列中删除该连接信息。 + +这两个队列的存在是为了处理并发连接请求,确保服务端能够有效地管理新的连接请求。另外,新的连接请求被拒绝或忽略除了和每个队列的大小限制有关系之外,还和很多其他因素有关系,这里就不详细介绍了,整体逻辑比较复杂。 + ### 为什么要三次握手? 三次握手的目的是建立可靠的通信信道,说到通讯,简单来说就是数据的发送与接收,而三次握手最主要的目的就是双方确认自己与对方的发送与接收是正常的。 @@ -35,7 +44,13 @@ tag: 服务端传回发送端所发送的 ACK 是为了告诉客户端:“我接收到的信息确实就是你所发送的信号了”,这表明从客户端到服务端的通信是正常的。回传 SYN 则是为了建立并确认从服务端到客户端的通信。 -> SYN 同步序列编号(Synchronize Sequence Numbers) 是 TCP/IP 建立连接时使用的握手信号。在客户机和服务器之间建立正常的 TCP 网络连接时,客户机首先发出一个 SYN 消息,服务器使用 SYN-ACK 应答表示接收到了这个消息,最后客户机再以 ACK(Acknowledgement)消息响应。这样在客户机和服务器之间才能建立起可靠的 TCP 连接,数据才可以在客户机和服务器之间传递。 +> SYN 同步序列编号(Synchronize Sequence Numbers) 是 TCP/IP 建立连接时使用的握手信号。在客户机和服务端之间建立正常的 TCP 网络连接时,客户机首先发出一个 SYN 消息,服务端使用 SYN-ACK 应答表示接收到了这个消息,最后客户机再以 ACK(Acknowledgement)消息响应。这样在客户机和服务端之间才能建立起可靠的 TCP 连接,数据才可以在客户机和服务端之间传递。 + +### 三次握手过程中可以携带数据吗? + +在 TCP 三次握手过程中,第三次握手是可以携带数据的(客户端发送完 ACK 确认包之后就进入 ESTABLISHED 状态了),这一点在 RFC 793 文档中有提到。也就是说,一旦完成了前两次握手,TCP 协议允许数据在第三次握手时开始传输。 + +如果第三次握手的 ACK 确认包丢失,但是客户端已经开始发送携带数据的包,那么服务端在收到这个携带数据的包时,如果该包中包含了 ACK 标记,服务端会将其视为有效的第三次握手确认。这样,连接就被认为是建立的,服务端会处理该数据包,并继续正常的数据传输流程。 ## 断开连接-TCP 四次挥手 @@ -43,10 +58,10 @@ tag: 断开一个 TCP 连接则需要“四次挥手”,缺一不可: -1. **第一次挥手**:客户端发送一个 FIN(SEQ=x) 标志的数据包->服务端,用来关闭客户端到服务器的数据传送。然后,客户端进入 **FIN-WAIT-1** 状态。 -2. **第二次挥手**:服务器收到这个 FIN(SEQ=X) 标志的数据包,它发送一个 ACK (ACK=x+1)标志的数据包->客户端 。然后,此时服务端进入 **CLOSE-WAIT** 状态,客户端进入 **FIN-WAIT-2** 状态。 -3. **第三次挥手**:服务端关闭与客户端的连接并发送一个 FIN (SEQ=y)标志的数据包->客户端请求关闭连接,然后,服务端进入 **LAST-ACK** 状态。 -4. **第四次挥手**:客户端发送 ACK (ACK=y+1)标志的数据包->服务端并且进入**TIME-WAIT**状态,服务端在收到 ACK (ACK=y+1)标志的数据包后进入 CLOSE 状态。此时,如果客户端等待 **2MSL** 后依然没有收到回复,就证明服务端已正常关闭,随后,客户端也可以关闭连接了。 +1. **第一次挥手**:客户端发送一个 FIN(SEQ=x) 标志的数据包->服务端,用来关闭客户端到服务端的数据传送。然后客户端进入 **FIN-WAIT-1** 状态。 +2. **第二次挥手**:服务端收到这个 FIN(SEQ=X) 标志的数据包,它发送一个 ACK (ACK=x+1)标志的数据包->客户端 。然后服务端进入 **CLOSE-WAIT** 状态,客户端进入 **FIN-WAIT-2** 状态。 +3. **第三次挥手**:服务端发送一个 FIN (SEQ=y)标志的数据包->客户端,请求关闭连接,然后服务端进入 **LAST-ACK** 状态。 +4. **第四次挥手**:客户端发送 ACK (ACK=y+1)标志的数据包->服务端,然后客户端进入**TIME-WAIT**状态,服务端在收到 ACK (ACK=y+1)标志的数据包后进入 CLOSE 状态。此时如果客户端等待 **2MSL** 后依然没有收到回复,就证明服务端已正常关闭,随后客户端也可以关闭连接了。 **只要四次挥手没有结束,客户端和服务端就可以继续传输数据!** @@ -61,17 +76,17 @@ TCP 是全双工通信,可以双向传输数据。任何一方都可以在数 3. **第三次挥手**:于是 B 可能又巴拉巴拉说了一通,最后 B 说“我说完了” 4. **第四次挥手**:A 回答“知道了”,这样通话才算结束。 -### 为什么不能把服务器发送的 ACK 和 FIN 合并起来,变成三次挥手? +### 为什么不能把服务端发送的 ACK 和 FIN 合并起来,变成三次挥手? -因为服务器收到客户端断开连接的请求时,可能还有一些数据没有发完,这时先回复 ACK,表示接收到了断开连接的请求。等到数据发完之后再发 FIN,断开服务器到客户端的数据传送。 +因为服务端收到客户端断开连接的请求时,可能还有一些数据没有发完,这时先回复 ACK,表示接收到了断开连接的请求。等到数据发完之后再发 FIN,断开服务端到客户端的数据传送。 -### 如果第二次挥手时服务器的 ACK 没有送达客户端,会怎样? +### 如果第二次挥手时服务端的 ACK 没有送达客户端,会怎样? 客户端没有收到 ACK 确认,会重新发送 FIN 请求。 ### 为什么第四次挥手客户端需要等待 2\*MSL(报文段最长寿命)时间后才进入 CLOSED 状态? -第四次挥手时,客户端发送给服务器的 ACK 有可能丢失,如果服务端因为某些原因而没有收到 ACK 的话,服务端就会重发 FIN,如果客户端在 2\*MSL 的时间内收到了 FIN,就会重新发送 ACK 并再次等待 2MSL,防止 Server 没有收到 ACK 而不断重发 FIN。 +第四次挥手时,客户端发送给服务端的 ACK 有可能丢失,如果服务端因为某些原因而没有收到 ACK 的话,服务端就会重发 FIN,如果客户端在 2\*MSL 的时间内收到了 FIN,就会重新发送 ACK 并再次等待 2MSL,防止 Server 没有收到 ACK 而不断重发 FIN。 > **MSL(Maximum Segment Lifetime)** : 一个片段在网络中最大的存活时间,2MSL 就是一个发送和一个回复所需的最大时间。如果直到 2MSL,Client 都没有再次收到 FIN,那么 Client 推断 ACK 已经被成功接收,则结束 TCP 连接。 @@ -81,4 +96,8 @@ TCP 是全双工通信,可以双向传输数据。任何一方都可以在数 - 《图解 HTTP》 -- TCP and UDP Tutorial:https://www.9tut.com/tcp-and-udp-tutorial +- TCP and UDP Tutorial: + +- 从一次线上问题说起,详解 TCP 半连接队列、全连接队列: + + diff --git a/docs/cs-basics/network/tcp-reliability-guarantee.md b/docs/cs-basics/network/tcp-reliability-guarantee.md index c2f081f2327..d4c9bea80ed 100644 --- a/docs/cs-basics/network/tcp-reliability-guarantee.md +++ b/docs/cs-basics/network/tcp-reliability-guarantee.md @@ -10,9 +10,9 @@ tag: 1. **基于数据块传输**:应用数据被分割成 TCP 认为最适合发送的数据块,再传输给网络层,数据块被称为报文段或段。 2. **对失序数据包重新排序以及去重**:TCP 为了保证不发生丢包,就给每个包一个序列号,有了序列号能够将接收到的数据根据序列号排序,并且去掉重复序列号的数据就可以实现数据包去重。 3. **校验和** : TCP 将保持它首部和数据的检验和。这是一个端到端的检验和,目的是检测数据在传输过程中的任何变化。如果收到段的检验和有差错,TCP 将丢弃这个报文段和不确认收到此报文段。 -4. **超时重传** : 当发送方发送数据之后,它启动一个定时器,等待目的端确认收到这个报文段。接收端实体对已成功收到的包发回一个相应的确认信息(ACK)。如果发送端实体在合理的往返时延(RTT)内未收到确认消息,那么对应的数据包就被假设为[已丢失](https://zh.wikipedia.org/wiki/丢包)并进行重传。 +4. **重传机制** : 在数据包丢失或延迟的情况下,重新发送数据包,直到收到对方的确认应答(ACK)。TCP 重传机制主要有:基于计时器的重传(也就是超时重传)、快速重传(基于接收端的反馈信息来引发重传)、SACK(在快速重传的基础上,返回最近收到的报文段的序列号范围,这样客户端就知道,哪些数据包已经到达服务器了)、D-SACK(重复 SACK,在 SACK 的基础上,额外携带信息,告知发送方有哪些数据包自己重复接收了)。关于重传机制的详细介绍,可以查看[详解 TCP 超时与重传机制](https://zhuanlan.zhihu.com/p/101702312)这篇文章。 5. **流量控制** : TCP 连接的每一方都有固定大小的缓冲空间,TCP 的接收端只允许发送端发送接收端缓冲区能接纳的数据。当接收方来不及处理发送方的数据,能提示发送方降低发送的速率,防止包丢失。TCP 使用的流量控制协议是可变大小的滑动窗口协议(TCP 利用滑动窗口实现流量控制)。 -6. **拥塞控制** : 当网络拥塞时,减少数据的发送。 +6. **拥塞控制** : 当网络拥塞时,减少数据的发送。TCP 在发送数据的时候,需要考虑两个因素:一是接收方的接收能力,二是网络的拥塞程度。接收方的接收能力由滑动窗口表示,表示接收方还有多少缓冲区可以用来接收数据。网络的拥塞程度由拥塞窗口表示,它是发送方根据网络状况自己维护的一个值,表示发送方认为可以在网络中传输的数据量。发送方发送数据的大小是滑动窗口和拥塞窗口的最小值,这样可以保证发送方既不会超过接收方的接收能力,也不会造成网络的过度拥塞。 ## TCP 如何实现流量控制? @@ -101,16 +101,28 @@ ARQ 包括停止等待 ARQ 协议和连续 ARQ 协议。 连续 ARQ 协议可提高信道利用率。发送方维持一个发送窗口,凡位于发送窗口内的分组可以连续发送出去,而不需要等待对方确认。接收方一般采用累计确认,对按序到达的最后一个分组发送确认,表明到这个分组为止的所有分组都已经正确收到了。 -**优点:** 信道利用率高,容易实现,即使确认丢失,也不必重传。 +- **优点:** 信道利用率高,容易实现,即使确认丢失,也不必重传。 +- **缺点:** 不能向发送方反映出接收方已经正确收到的所有分组的信息。 比如:发送方发送了 5 条 消息,中间第三条丢失(3 号),这时接收方只能对前两个发送确认。发送方无法知道后三个分组的下落,而只好把后三个全部重传一次。这也叫 Go-Back-N(回退 N),表示需要退回来重传已经发送过的 N 个消息。 -**缺点:** 不能向发送方反映出接收方已经正确收到的所有分组的信息。 比如:发送方发送了 5 条 消息,中间第三条丢失(3 号),这时接收方只能对前两个发送确认。发送方无法知道后三个分组的下落,而只好把后三个全部重传一次。这也叫 Go-Back-N(回退 N),表示需要退回来重传已经发送过的 N 个消息。 +## 超时重传如何实现?超时重传时间怎么确定? -## Reference +当发送方发送数据之后,它启动一个定时器,等待目的端确认收到这个报文段。接收端实体对已成功收到的包发回一个相应的确认信息(ACK)。如果发送端实体在合理的往返时延(RTT)内未收到确认消息,那么对应的数据包就被假设为[已丢失](https://zh.wikipedia.org/wiki/丢包)并进行重传。 + +- RTT(Round Trip Time):往返时间,也就是数据包从发出去到收到对应 ACK 的时间。 +- RTO(Retransmission Time Out):重传超时时间,即从数据发送时刻算起,超过这个时间便执行重传。 + +RTO 的确定是一个关键问题,因为它直接影响到 TCP 的性能和效率。如果 RTO 设置得太小,会导致不必要的重传,增加网络负担;如果 RTO 设置得太大,会导致数据传输的延迟,降低吞吐量。因此,RTO 应该根据网络的实际状况,动态地进行调整。 + +RTT 的值会随着网络的波动而变化,所以 TCP 不能直接使用 RTT 作为 RTO。为了动态地调整 RTO,TCP 协议采用了一些算法,如加权移动平均(EWMA)算法,Karn 算法,Jacobson 算法等,这些算法都是根据往返时延(RTT)的测量和变化来估计 RTO 的值。 + +## 参考 1. 《计算机网络(第 7 版)》 2. 《图解 HTTP》 3. [https://www.9tut.com/tcp-and-udp-tutorial](https://www.9tut.com/tcp-and-udp-tutorial) 4. [https://github.com/wolverinn/Waking-Up/blob/master/Computer%20Network.md](https://github.com/wolverinn/Waking-Up/blob/master/Computer%20Network.md) 5. TCP Flow Control—[https://www.brianstorti.com/tcp-flow-control/](https://www.brianstorti.com/tcp-flow-control/) -6. TCP 流量控制(Flow Control):https://notfalse.net/24/tcp-flow-control -7. TCP 之滑动窗口原理 : https://cloud.tencent.com/developer/article/1857363 +6. TCP 流量控制(Flow Control): +7. TCP 之滑动窗口原理 : + + diff --git a/docs/cs-basics/network/the-whole-process-of-accessing-web-pages.md b/docs/cs-basics/network/the-whole-process-of-accessing-web-pages.md new file mode 100644 index 00000000000..906d16fae2e --- /dev/null +++ b/docs/cs-basics/network/the-whole-process-of-accessing-web-pages.md @@ -0,0 +1,79 @@ +--- +title: 访问网页的全过程(知识串联) +category: 计算机基础 +tag: + - 计算机网络 +--- + +开发岗中总是会考很多计算机网络的知识点,但如果让面试官只考一道题,便涵盖最多的计网知识点,那可能就是 **网页浏览的全过程** 了。本篇文章将带大家从头到尾过一遍这道被考烂的面试题,必会!!! + +总的来说,网络通信模型可以用下图来表示,也就是大家只要熟记网络结构五层模型,按照这个体系,很多知识点都能顺出来了。访问网页的过程也是如此。 + +![](https://oss.javaguide.cn/github/javaguide/cs-basics/network/five-layers.png) + +开始之前,我们先简单过一遍完整流程: + +1. 在浏览器中输入指定网页的 URL。 +2. 浏览器通过 DNS 协议,获取域名对应的 IP 地址。 +3. 浏览器根据 IP 地址和端口号,向目标服务器发起一个 TCP 连接请求。 +4. 浏览器在 TCP 连接上,向服务器发送一个 HTTP 请求报文,请求获取网页的内容。 +5. 服务器收到 HTTP 请求报文后,处理请求,并返回 HTTP 响应报文给浏览器。 +6. 浏览器收到 HTTP 响应报文后,解析响应体中的 HTML 代码,渲染网页的结构和样式,同时根据 HTML 中的其他资源的 URL(如图片、CSS、JS 等),再次发起 HTTP 请求,获取这些资源的内容,直到网页完全加载显示。 +7. 浏览器在不需要和服务器通信时,可以主动关闭 TCP 连接,或者等待服务器的关闭请求。 + +## 应用层 + +一切的开始——打开浏览器,在地址栏输入 URL,回车确认。那么,什么是 URL?访问 URL 有什么用? + +### URL + +URL(Uniform Resource Locators),即统一资源定位器。网络上的所有资源都靠 URL 来定位,每一个文件就对应着一个 URL,就像是路径地址。理论上,文件资源和 URL 一一对应。实际上也有例外,比如某些 URL 指向的文件已经被重定位到另一个位置,这样就有多个 URL 指向同一个文件。 + +### URL 的组成结构 + +![URL的组成结构](https://oss.javaguide.cn/github/javaguide/cs-basics/network/URL-parts.png) + +1. 协议。URL 的前缀通常表示了该网址采用了何种应用层协议,通常有两种——HTTP 和 HTTPS。当然也有一些不太常见的前缀头,比如文件传输时用到的`ftp:`。 +2. 域名。域名便是访问网址的通用名,这里也有可能是网址的 IP 地址,域名可以理解为 IP 地址的可读版本,毕竟绝大部分人都不会选择记住一个网址的 IP 地址。 +3. 端口。如果指明了访问网址的端口的话,端口会紧跟在域名后面,并用一个冒号隔开。 +4. 资源路径。域名(端口)后紧跟的就是资源路径,从第一个`/`开始,表示从服务器上根目录开始进行索引到的文件路径,上图中要访问的文件就是服务器根目录下`/path/to/myfile.html`。早先的设计是该文件通常物理存储于服务器主机上,但现在随着网络技术的进步,该文件不一定会物理存储在服务器主机上,有可能存放在云上,而文件路径也有可能是虚拟的(遵循某种规则)。 +5. 参数。参数是浏览器在向服务器提交请求时,在 URL 中附带的参数。服务器解析请求时,会提取这些参数。参数采用键值对的形式`key=value`,每一个键值对使用`&`隔开。参数的具体含义和请求操作的具体方法有关。 +6. 锚点。锚点顾名思义,是在要访问的页面上的一个锚。要访问的页面大部分都多于一页,如果指定了锚点,那么在客户端显示该网页是就会定位到锚点处,相当于一个小书签。值得一提的是,在 URL 中,锚点以`#`开头,并且**不会**作为请求的一部分发送给服务端。 + +### DNS + +键入了 URL 之后,第一个重头戏登场——DNS 服务器解析。DNS(Domain Name System)域名系统,要解决的是 **域名和 IP 地址的映射问题** 。毕竟,域名只是一个网址便于记住的名字,而网址真正存在的地址其实是 IP 地址。 + +传送门:[DNS 域名系统详解(应用层)](https://javaguide.cn/cs-basics/network/dns.html) + +### HTTP/HTTPS + +利用 DNS 拿到了目标主机的 IP 地址之后,浏览器便可以向目标 IP 地址发送 HTTP 报文,请求需要的资源了。在这里,根据目标网站的不同,请求报文可能是 HTTP 协议或安全性增强的 HTTPS 协议。 + +传送门: + +- [HTTP vs HTTPS(应用层)](https://javaguide.cn/cs-basics/network/http-vs-https.html) +- [HTTP 1.0 vs HTTP 1.1(应用层)](https://javaguide.cn/cs-basics/network/http1.0-vs-http1.1.html) +- [HTTP 常见状态码总结(应用层)](https://javaguide.cn/cs-basics/network/http-status-codes.html) + +## 传输层 + +由于 HTTP 协议是基于 TCP 协议的,在应用层的数据封装好以后,要交给传输层,经 TCP 协议继续封装。 + +TCP 协议保证了数据传输的可靠性,是数据包传输的主力协议。 + +传送门: + +- [TCP 三次握手和四次挥手(传输层)](https://javaguide.cn/cs-basics/network/tcp-connection-and-disconnection.html) +- [TCP 传输可靠性保障(传输层)](https://javaguide.cn/cs-basics/network/tcp-reliability-guarantee.html) + +## 网络层 + +终于,来到网络层,此时我们的主机不再是和另一台主机进行交互了,而是在和中间系统进行交互。也就是说,应用层和传输层都是端到端的协议,而网络层及以下都是中间件的协议了。 + +**网络层的的核心功能——转发与路由**,必会!!!如果面试官问到了网络层,而你恰好又什么都不会的话,最最起码要说出这五个字——**转发与路由**。 + +- 转发:将分组从路由器的输入端口转移到合适的输出端口。 +- 路由:确定分组从源到目的经过的路径。 + +所以到目前为止,我们的数据包经过了应用层、传输层的封装,来到了网络层,终于开始准备在物理层面传输了,第一个要解决的问题就是——**往哪里传输?或者说,要把数据包发到哪个路由器上?** 这便是 BGP 协议要解决的问题。 diff --git a/docs/cs-basics/operating-system/linux-intro.md b/docs/cs-basics/operating-system/linux-intro.md index 677f22899b6..1486fe45c90 100644 --- a/docs/cs-basics/operating-system/linux-intro.md +++ b/docs/cs-basics/operating-system/linux-intro.md @@ -10,6 +10,8 @@ head: content: 简单介绍一下 Java 程序员必知的 Linux 的一些概念以及常见命令。 --- + + 简单介绍一下 Java 程序员必知的 Linux 的一些概念以及常见命令。 ## 初探 Linux @@ -68,7 +70,7 @@ inode 是 Linux/Unix 文件系统的基础。那 inode 到是什么?有什么作 通过以下五点可以概括 inode 到底是什么: -1. 硬盘的最小存储单位是扇区(Sector),块(block)由多个扇区组成。文件数据存储在块中。块的最常见的大小是 4kb,约为 8 个连续的扇区组成(每个扇区存储 512 字节)。一个文件可能会占用多个 block,但是一个块只能存放一个文件。虽然,我们将文件存储在了块(block)中,但是我们还需要一个空间来存储文件的 **元信息 metadata**:如某个文件被分成几块、每一块在的地址、文件拥有者,创建时间,权限,大小等。这种 **存储文件元信息的区域就叫 inode**,译为索引节点:**i(index)+node**。 **每个文件都有一个唯一的 inode,存储文件的元信息。** +1. 硬盘以扇区 (Sector) 为最小物理存储单位,而操作系统和文件系统以块 (Block) 为单位进行读写,块由多个扇区组成。文件数据存储在这些块中。现代硬盘扇区通常为 4KB,与一些常见块大小相同,但操作系统也支持更大的块大小,以提升大文件读写性能。文件元信息(例如权限、大小、修改时间以及数据块位置)存储在 inode(索引节点)中。每个文件都有唯一的 inode。inode 本身不存储文件数据,而是存储指向数据块的指针,操作系统通过这些指针找到并读取文件数据。 固态硬盘 (SSD) 虽然没有物理扇区,但使用逻辑块,其概念与传统硬盘的块类似。 2. inode 是一种固定大小的数据结构,其大小在文件系统创建时就确定了,并且在文件的生命周期内保持不变。 3. inode 的访问速度非常快,因为系统可以直接通过 inode 号码定位到文件的元数据信息,无需遍历整个文件系统。 4. inode 的数量是有限的,每个文件系统只能包含固定数量的 inode。这意味着当文件系统中的 inode 用完时,无法再创建新的文件或目录,即使磁盘上还有可用空间。因此,在创建文件系统时,需要根据文件和目录的预期数量来合理分配 inode 的数量。 @@ -101,7 +103,7 @@ inode 是 Linux/Unix 文件系统的基础。那 inode 到是什么?有什么作 **2、软链接(Symbolic Link 或 Symlink)** - 软链接和源文件的 inode 节点号不同,而是指向一个文件路径。 -- 源文件删除后,硬链接依然存在,但是指向的是一个无效的文件路径。 +- 源文件删除后,软链接依然存在,但是指向的是一个无效的文件路径。 - 软连接类似于 Windows 系统中的快捷方式。 - 不同于硬链接,可以对目录或者不存在的文件创建软链接,并且,软链接可以跨越文件系统。 - `ln -s` 命令用于创建软链接。 @@ -164,7 +166,7 @@ Linux 使用一种称为目录树的层次结构来组织文件和目录。目 下面只是给出了一些比较常用的命令。 -推荐一个 Linux 命令快查网站,非常不错,大家如果遗忘某些命令或者对某些命令不理解都可以在这里得到解决。Linux 命令在线速查手册:https://wangchujiang.com/linux-command/ 。 +推荐一个 Linux 命令快查网站,非常不错,大家如果遗忘某些命令或者对某些命令不理解都可以在这里得到解决。Linux 命令在线速查手册: 。 ![ Linux 命令快查](https://oss.javaguide.cn/github/javaguide/cs-basics/operating-system/linux/linux-command-search.png) @@ -183,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`的空父目录,直到遇到非空目录或根目录。 @@ -235,7 +237,7 @@ Linux 中的打包文件一般是以 `.tar` 结尾的,压缩的命令一般是 ### 文件权限 -操作系统中每个文件都拥有特定的权限、所属用户和所属组。权限是操作系统用来限制资源访问的机制,在 Linux 中权限一般分为读(readable)、写(writable)和执行(excutable),分为三组。分别对应文件的属主(owner),属组(group)和其他用户(other),通过这样的机制来限制哪些用户、哪些组可以对特定的文件进行什么样的操作。 +操作系统中每个文件都拥有特定的权限、所属用户和所属组。权限是操作系统用来限制资源访问的机制,在 Linux 中权限一般分为读(readable)、写(writable)和执行(executable),分为三组。分别对应文件的属主(owner),属组(group)和其他用户(other),通过这样的机制来限制哪些用户、哪些组可以对特定的文件进行什么样的操作。 通过 **`ls -l`** 命令我们可以 查看某个目录下的文件或目录的权限 @@ -353,6 +355,8 @@ Linux 系统是一个多用户多任务的分时操作系统,任何一个要 - `ifconfig` 或 `ip`:用于查看系统的网络接口信息,包括网络接口的 IP 地址、MAC 地址、状态等。 - `netstat [选项]`:用于查看系统的网络连接状态和网络统计信息,可以查看当前的网络连接情况、监听端口、网络协议等。 - `ss [选项]`:比 `netstat` 更好用,提供了更快速、更详细的网络连接信息。 +- `nload`:`sar` 和 `nload` 都可以监控网络流量,但`sar` 的输出是文本形式的数据,不够直观。`nload` 则是一个专门用于实时监控网络流量的工具,提供图形化的终端界面,更加直观。不过,`nload` 不保存历史数据,所以它不适合用于长期趋势分析。并且,系统并没有默认安装它,需要手动安装。 +- `sudo hostnamectl set-hostname 新主机名`:更改主机名,并且重启后依然有效。`sudo hostname 新主机名`也可以更改主机名。不过需要注意的是,使用 `hostname` 命令直接更改主机名只是临时生效,系统重启后会恢复为原来的主机名。 ### 其他 @@ -373,7 +377,7 @@ Linux 系统是一个多用户多任务的分时操作系统,任何一个要 - 用户级别环境变量 : `~/.bashrc`、`~/.bash_profile`。 - 系统级别环境变量 : `/etc/bashrc`、`/etc/environment`、`/etc/profile`、`/etc/profile.d`。 -上述配置文件执行先后顺序为:`/etc/enviroment` –> `/etc/profile` –> `/etc/profile.d` –> `~/.bash_profile` –> `/etc/bashrc` –> `~/.bashrc` +上述配置文件执行先后顺序为:`/etc/environment` –> `/etc/profile` –> `/etc/profile.d` –> `~/.bash_profile` –> `/etc/bashrc` –> `~/.bashrc` 如果要修改系统级别环境变量文件,需要管理员具备对该文件的写入权限。 @@ -425,3 +429,5 @@ vim ~/.bash_profile ```bash source /etc/profile ``` + + 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 7498b83ebf4..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) @@ -201,10 +201,10 @@ _玩玩电脑游戏还是必须要有 Windows 的,所以我现在是一台 Win 下面是几种常见的线程同步的方式: -1. **互斥锁(Mutex)**:采用互斥对象机制,只有拥有互斥对象的线程才有访问公共资源的权限。因为互斥对象只有一个,所以可以保证公共资源不会被多个线程同时访问。比如 Java 中的 `synchronized` 关键词和各种 `Lock` 都是这种机制。 -2. **读写锁(Read-Write Lock)**:允许多个线程同时读取共享资源,但只有一个线程可以对共享资源进行写操作。 -3. **信号量(Semaphore)**:它允许同一时刻多个线程访问同一资源,但是需要控制同一时刻访问此资源的最大线程数量。 -4. **屏障(Barrier)**:屏障是一种同步原语,用于等待多个线程到达某个点再一起继续执行。当一个线程到达屏障时,它会停止执行并等待其他线程到达屏障,直到所有线程都到达屏障后,它们才会一起继续执行。比如 Java 中的 `CyclicBarrier` 是这种机制。 +1. **互斥锁(Mutex)** :采用互斥对象机制,只有拥有互斥对象的线程才有访问公共资源的权限。因为互斥对象只有一个,所以可以保证公共资源不会被多个线程同时访问。比如 Java 中的 `synchronized` 关键词和各种 `Lock` 都是这种机制。 +2. **读写锁(Read-Write Lock)** :允许多个线程同时读取共享资源,但只有一个线程可以对共享资源进行写操作。 +3. **信号量(Semaphore)** :它允许同一时刻多个线程访问同一资源,但是需要控制同一时刻访问此资源的最大线程数量。 +4. **屏障(Barrier)** :屏障是一种同步原语,用于等待多个线程到达某个点再一起继续执行。当一个线程到达屏障时,它会停止执行并等待其他线程到达屏障,直到所有线程都到达屏障后,它们才会一起继续执行。比如 Java 中的 `CyclicBarrier` 是这种机制。 5. **事件(Event)** :Wait/Notify:通过通知操作的方式来保持多线程同步,还可以方便的实现多线程优先级的比较操作。 ### PCB 是什么?包含哪些信息? @@ -220,7 +220,7 @@ PCB 主要包含下面几部分的内容: - 进程对资源的需求情况,包括 CPU 时间、内存空间、I/O 设备等等。 - 进程打开的文件信息,包括文件描述符、文件类型、打开模式等等。 - 处理机的状态信息(由处理机的各种寄存器中的内容组成的),包括通用寄存器、指令计数器、程序状态字 PSW、用户栈指针。 -- ...... +- …… ### 进程有哪几种状态? @@ -238,12 +238,12 @@ PCB 主要包含下面几部分的内容: > 下面这部分总结参考了:[《进程间通信 IPC (InterProcess Communication)》](https://www.jianshu.com/p/c1015f5ffa74) 这篇文章,推荐阅读,总结的非常不错。 -1. **管道/匿名管道(Pipes)**:用于具有亲缘关系的父子进程间或者兄弟进程之间的通信。 +1. **管道/匿名管道(Pipes)** :用于具有亲缘关系的父子进程间或者兄弟进程之间的通信。 2. **有名管道(Named Pipes)** : 匿名管道由于没有名字,只能用于亲缘关系的进程间通信。为了克服这个缺点,提出了有名管道。有名管道严格遵循 **先进先出(First In First Out)** 。有名管道以磁盘文件的方式存在,可以实现本机任意两个进程通信。 -3. **信号(Signal)**:信号是一种比较复杂的通信方式,用于通知接收进程某个事件已经发生; -4. **消息队列(Message Queuing)**:消息队列是消息的链表,具有特定的格式,存放在内存中并由消息队列标识符标识。管道和消息队列的通信数据都是先进先出的原则。与管道(无名管道:只存在于内存中的文件;命名管道:存在于实际的磁盘介质或者文件系统)不同的是消息队列存放在内核中,只有在内核重启(即,操作系统重启)或者显式地删除一个消息队列时,该消息队列才会被真正的删除。消息队列可以实现消息的随机查询,消息不一定要以先进先出的次序读取,也可以按消息的类型读取.比 FIFO 更有优势。**消息队列克服了信号承载信息量少,管道只能承载无格式字 节流以及缓冲区大小受限等缺点。** -5. **信号量(Semaphores)**:信号量是一个计数器,用于多进程对共享数据的访问,信号量的意图在于进程间同步。这种通信方式主要用于解决与同步相关的问题并避免竞争条件。 -6. **共享内存(Shared memory)**:使得多个进程可以访问同一块内存空间,不同进程可以及时看到对方进程中对共享内存中数据的更新。这种方式需要依靠某种同步操作,如互斥锁和信号量等。可以说这是最有用的进程间通信方式。 +3. **信号(Signal)** :信号是一种比较复杂的通信方式,用于通知接收进程某个事件已经发生; +4. **消息队列(Message Queuing)** :消息队列是消息的链表,具有特定的格式,存放在内存中并由消息队列标识符标识。管道和消息队列的通信数据都是先进先出的原则。与管道(无名管道:只存在于内存中的文件;命名管道:存在于实际的磁盘介质或者文件系统)不同的是消息队列存放在内核中,只有在内核重启(即,操作系统重启)或者显式地删除一个消息队列时,该消息队列才会被真正的删除。消息队列可以实现消息的随机查询,消息不一定要以先进先出的次序读取,也可以按消息的类型读取.比 FIFO 更有优势。消息队列克服了信号承载信息量少,管道只能承载无格式字 节流以及缓冲区大小受限等缺点。 +5. **信号量(Semaphores)** :信号量是一个计数器,用于多进程对共享数据的访问,信号量的意图在于进程间同步。这种通信方式主要用于解决与同步相关的问题并避免竞争条件。 +6. **共享内存(Shared memory)** :使得多个进程可以访问同一块内存空间,不同进程可以及时看到对方进程中对共享内存中数据的更新。这种方式需要依靠某种同步操作,如互斥锁和信号量等。可以说这是最有用的进程间通信方式。 7. **套接字(Sockets)** : 此方法主要用于在客户端和服务器之间通过网络进行通信。套接字是支持 TCP/IP 的网络通信的基本操作单元,可以看做是不同主机之间的进程进行双向通信的端点,简单的说就是通信的两方的一种约定,用套接字中的相关函数来完成通信过程。 ### 进程的调度算法有哪些? @@ -255,7 +255,7 @@ PCB 主要包含下面几部分的内容: - **先到先服务调度算法(FCFS,First Come, First Served)** : 从就绪队列中选择一个最先进入该队列的进程为之分配资源,使它立即执行并一直执行到完成或发生某事件而被阻塞放弃占用 CPU 时再重新调度。 - **短作业优先的调度算法(SJF,Shortest Job First)** : 从就绪队列中选出一个估计运行时间最短的进程为之分配资源,使它立即执行并一直执行到完成或发生某事件而被阻塞放弃占用 CPU 时再重新调度。 - **时间片轮转调度算法(RR,Round-Robin)** : 时间片轮转调度是一种最古老,最简单,最公平且使用最广的算法。每个进程被分配一个时间段,称作它的时间片,即该进程允许运行的时间。 -- **多级反馈队列调度算法(MFQ,Multi-level Feedback Queue)**:前面介绍的几种进程调度的算法都有一定的局限性。如**短进程优先的调度算法,仅照顾了短进程而忽略了长进程** 。多级反馈队列调度算法既能使高优先级的作业得到响应又能使短作业(进程)迅速完成。,因而它是目前**被公认的一种较好的进程调度算法**,UNIX 操作系统采取的便是这种调度算法。 +- **多级反馈队列调度算法(MFQ,Multi-level Feedback Queue)**:前面介绍的几种进程调度的算法都有一定的局限性。如**短进程优先的调度算法,仅照顾了短进程而忽略了长进程** 。多级反馈队列调度算法既能使高优先级的作业得到响应又能使短作业(进程)迅速完成,因而它是目前**被公认的一种较好的进程调度算法**,UNIX 操作系统采取的便是这种调度算法。 - **优先级调度算法(Priority)**:为每个流程分配优先级,首先执行具有最高优先级的进程,依此类推。具有相同优先级的进程以 FCFS 方式执行。可以根据内存要求,时间要求或任何其他资源要求来确定优先级。 ### 什么是僵尸进程和孤儿进程? @@ -303,7 +303,7 @@ ps -A -ostat,ppid,pid,cmd |grep -e '^[Zz]' 1. **互斥**:资源必须处于非共享模式,即一次只有一个进程可以使用。如果另一进程申请该资源,那么必须等待直到该资源被释放为止。 2. **占有并等待**:一个进程至少应该占有一个资源,并等待另一资源,而该资源被其他进程所占有。 3. **非抢占**:资源不能被抢占。只能在持有资源的进程完成任务后,该资源才会被释放。 -4. **循环等待**:有一组等待进程 `{P0, P1,..., Pn}`, `P0` 等待的资源被 `P1` 占有,`P1` 等待的资源被 `P2` 占有,......,`Pn-1` 等待的资源被 `Pn` 占有,`Pn` 等待的资源被 `P0` 占有。 +4. **循环等待**:有一组等待进程 `{P0, P1,..., Pn}`, `P0` 等待的资源被 `P1` 占有,`P1` 等待的资源被 `P2` 占有,……,`Pn-1` 等待的资源被 `Pn` 占有,`Pn` 等待的资源被 `P0` 占有。 **注意 ⚠️**:这四个条件是产生死锁的 **必要条件** ,也就是说只要系统发生死锁,这些条件必然成立,而只要上述条件之一不满足,就不会发生死锁。 @@ -315,7 +315,7 @@ ps -A -ostat,ppid,pid,cmd |grep -e '^[Zz]' 下面通过一个实际的例子来模拟下图展示的线程死锁: -![线程死锁示意图 ](https://my-blog-to-use.oss-cn-beijing.aliyuncs.com/2019-4/2019-4%E6%AD%BB%E9%94%811.png) +![线程死锁示意图 ](https://oss.javaguide.cn/github/javaguide/java/2019-4%E6%AD%BB%E9%94%811-20230814005444749.png) ```java public class DeadLockDemo { @@ -402,7 +402,7 @@ Thread[线程 2,5,main]waiting get resource1 上面提到的 **破坏** 死锁产生的四个必要条件之一就可以成功 **预防系统发生死锁** ,但是会导致 **低效的进程运行** 和 **资源使用率** 。而死锁的避免相反,它的角度是允许系统中**同时存在四个必要条件** ,只要掌握并发进程中与每个进程有关的资源动态申请情况,做出 **明智和合理的选择** ,仍然可以避免死锁,因为四大条件仅仅是产生死锁的必要条件。 -我们将系统的状态分为 **安全状态** 和 **不安全状态** ,每当在未申请者分配资源前先测试系统状态,若把系统资源分配给申请者会产生死锁,则拒绝分配,否则接受申请,并为它分配资源。 +我们将系统的状态分为 **安全状态** 和 **不安全状态** ,每当在为申请者分配资源前先测试系统状态,若把系统资源分配给申请者会产生死锁,则拒绝分配,否则接受申请,并为它分配资源。 > 如果操作系统能够保证所有的进程在有限的时间内得到需要的全部资源,则称系统处于安全状态,否则说系统是不安全的。很显然,系统处于安全状态则不会发生死锁,系统若处于不安全状态则可能发生死锁。 @@ -424,7 +424,7 @@ Thread[线程 2,5,main]waiting get resource1 操作系统中的每一刻时刻的**系统状态**都可以用**进程-资源分配图**来表示,进程-资源分配图是描述进程和资源申请及分配关系的一种有向图,可用于**检测系统是否处于死锁状态**。 -用一个方框表示每一个资源类,方框中的黑点表示该资源类中的各个资源,每个键进程用一个圆圈表示,用 **有向边** 来表示**进程申请资源和资源被分配的情况**。 +用一个方框表示每一个资源类,方框中的黑点表示该资源类中的各个资源,用一个圆圈表示每一个进程,用 **有向边** 来表示**进程申请资源和资源被分配的情况**。 图中 2-21 是**进程-资源分配图**的一个例子,其中共有三个资源类,每个进程的资源占有和申请情况已清楚地表示在图中。在这个例子中,由于存在 **占有和等待资源的环路** ,导致一组进程永远处于等待资源的状态,发生了 **死锁**。 @@ -454,6 +454,8 @@ Thread[线程 2,5,main]waiting get resource1 - 《计算机操作系统—汤小丹》第四版 - 《深入理解计算机系统》 - 《重学操作系统》 -- 操作系统为什么要分用户态和内核态:https://blog.csdn.net/chen134225/article/details/81783980 -- 从根上理解用户态与内核态:https://juejin.cn/post/6923863670132850701 -- 什么是僵尸进程与孤儿进程:https://blog.csdn.net/a745233700/article/details/120715371 +- 操作系统为什么要分用户态和内核态: +- 从根上理解用户态与内核态: +- 什么是僵尸进程与孤儿进程: + + 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 1c9d4f26822..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 @@ -26,14 +26,14 @@ head: - **内存映射**:将一个文件直接映射到进程的进程空间中,这样可以通过内存指针用读写内存的办法直接存取文件内容,速度更快。 - **内存优化**:通过调整内存分配策略和回收算法来优化内存使用效率。 - **内存安全**:保证进程之间使用内存互不干扰,避免一些恶意程序通过修改内存来破坏系统的安全性。 -- ...... +- …… ### 什么是内存碎片? 内存碎片是由内存的申请和释放产生的,通常分为下面两种: - **内部内存碎片(Internal Memory Fragmentation,简称为内存碎片)**:已经分配给进程使用但未被使用的内存。导致内部内存碎片的主要原因是,当采用固定比例比如 2 的幂次方进行内存分配时,进程所分配的内存可能会比其实际所需要的大。举个例子,一个进程只需要 65 字节的内存,但为其分配了 128(2^7) 大小的内存,那 63 字节的内存就成为了内部内存碎片。 -- **外部内存碎片(External Memory Fragmentation,简称为外部碎片)**:由于未分配的连续内存区域太小,以至于不能满足任意进程所需要的内存分配请求,这些小片段且不连续的内存空间被称为外部碎片。也就是说,外部内存碎片指的是那些并为分配给进程但又不能使用的内存。我们后面介绍的分段机制就会导致外部内存碎片。 +- **外部内存碎片(External Memory Fragmentation,简称为外部碎片)**:由于未分配的连续内存区域太小,以至于不能满足任意进程所需要的内存分配请求,这些小片段且不连续的内存空间被称为外部碎片。也就是说,外部内存碎片指的是那些并未分配给进程但又不能使用的内存。我们后面介绍的分段机制就会导致外部内存碎片。 ![内存碎片](https://oss.javaguide.cn/github/javaguide/cs-basics/operating-system/internal-and-external-fragmentation.png) @@ -68,8 +68,8 @@ head: 非连续内存管理存在下面 3 种方式: -- **段式管理**:以段(—段连续的物理内存)的形式管理/分配物理内存。应用程序的虚拟地址空间被分为大小不等的段,段是有实际意义的,每个段定义了一组逻辑信息,例如有主程序段 MAIN、子程序段 X、数据段 D 及栈段 S 等。 -- **页式管理**:把物理内存分为连续等长的物理页,应用程序的虚拟地址空间划也被分为连续等长的虚拟页,现代操作系统广泛使用的一种内存管理方式。 +- **段式管理**:以段(一段连续的物理内存)的形式管理/分配物理内存。应用程序的虚拟地址空间被分为大小不等的段,段是有实际意义的,每个段定义了一组逻辑信息,例如有主程序段 MAIN、子程序段 X、数据段 D 及栈段 S 等。 +- **页式管理**:把物理内存分为连续等长的物理页,应用程序的虚拟地址空间也被划分为连续等长的虚拟页,是现代操作系统广泛使用的一种内存管理方式。 - **段页式管理机制**:结合了段式管理和页式管理的一种内存管理机制,把物理内存先分成若干段,每个段又继续分成若干大小相等的页。 ### 虚拟内存 @@ -98,7 +98,7 @@ head: 1. 用户程序可以访问任意物理内存,可能会不小心操作到系统运行必需的内存,进而造成操作系统崩溃,严重影响系统的安全。 2. 同时运行多个程序容易崩溃。比如你想同时运行一个微信和一个 QQ 音乐,微信在运行的时候给内存地址 1xxx 赋值后,QQ 音乐也同样给内存地址 1xxx 赋值,那么 QQ 音乐对内存的赋值就会覆盖微信之前所赋的值,这就可能会造成微信这个程序会崩溃。 3. 程序运行过程中使用的所有数据或指令都要载入物理内存,根据局部性原理,其中很大一部分可能都不会用到,白白占用了宝贵的物理内存资源。 -4. ...... +4. …… #### 什么是虚拟地址和物理地址? @@ -131,7 +131,7 @@ MMU 将虚拟地址翻译为物理地址的主要机制有 3 种: ### 分段机制 -**分段机制(Segmentation)** 以段(—段 **连续** 的物理内存)的形式管理/分配物理内存。应用程序的虚拟地址空间被分为大小不等的段,段是有实际意义的,每个段定义了一组逻辑信息,例如有主程序段 MAIN、子程序段 X、数据段 D 及栈段 S 等。 +**分段机制(Segmentation)** 以段(一段 **连续** 的物理内存)的形式管理/分配物理内存。应用程序的虚拟地址空间被分为大小不等的段,段是有实际意义的,每个段定义了一组逻辑信息,例如有主程序段 MAIN、子程序段 X、数据段 D 及栈段 S 等。 #### 段表有什么用?地址翻译过程是怎样的? @@ -188,7 +188,7 @@ MMU 将虚拟地址翻译为物理地址的主要机制有 3 种: ![单级页表](https://oss.javaguide.cn/github/javaguide/cs-basics/operating-system/page-table.png) -在分页机制下,每个应用程序都会有一个对应的页表。 +在分页机制下,每个进程都会有一个对应的页表。 分页机制下的虚拟地址由两部分组成: @@ -203,7 +203,7 @@ MMU 将虚拟地址翻译为物理地址的主要机制有 3 种: ![分页机制下的地址翻译过程](https://oss.javaguide.cn/github/javaguide/cs-basics/operating-system/paging-virtual-address-composition.png) -页表中还存有诸如访问标志(标识该页面有没有被访问过)、页类型(该段的类型,例如代码段、数据段等)等信息。 +页表中还存有诸如访问标志(标识该页面有没有被访问过)、脏数据标识位等信息。 **通过虚拟页号一定要找到对应的物理页号吗?找到了物理页号得到最终的物理地址后对应的物理页一定存在吗?** @@ -211,11 +211,11 @@ MMU 将虚拟地址翻译为物理地址的主要机制有 3 种: #### 单级页表有什么问题?为什么需要多级页表? -以 32 位的环境为例,虚拟地址空间范围共有 2^32(4G)。假设 一个页的大小是 2^12(4KB),那页表项共有 4G / 4K = 2^20 个。每个页表项为一个地址,占用 4 字节,2^20 * 2^2/1024*1024= 4MB。也就是说一个程序啥都不干,页表大小就得占用 4M。 +以 32 位的环境为例,虚拟地址空间范围共有 2^32(4G)。假设 一个页的大小是 2^12(4KB),那页表项共有 4G / 4K = 2^20 个。每个页表项为一个地址,占用 4 字节,`2^20 * 2^2 / 1024 * 1024= 4MB`。也就是说一个程序啥都不干,页表大小就得占用 4M。 系统运行的应用程序多起来的话,页表的开销还是非常大的。而且,绝大部分应用程序可能只能用到页表中的几项,其他的白白浪费了。 -为了解决这个问题,操作系统引入了 **多级页表** ,多级页表对应多个页表,每个页表也前一个页表相关联。32 位系统一般为二级页表,64 位系统一般为四级页表。 +为了解决这个问题,操作系统引入了 **多级页表** ,多级页表对应多个页表,每个页表与前一个页表相关联。32 位系统一般为二级页表,64 位系统一般为四级页表。 这里以二级页表为例进行介绍:二级列表分为一级页表和二级页表。一级页表共有 1024 个页表项,一级页表又关联二级页表,二级页表同样共有 1024 个页表项。二级页表中的一级页表项是一对多的关系,二级页表按需加载(只会用到很少一部分二级页表),进而节省空间占用。 @@ -227,7 +227,7 @@ MMU 将虚拟地址翻译为物理地址的主要机制有 3 种: #### TLB 有什么用?使用 TLB 之后的地址翻译流程是怎样的? -为了提高虚拟地址到物理地址的转换速度,操作系统在 **页表方案** 基础之上引入了 **转址旁路缓存(Translation Lookasjde Buffer,TLB,也被称为快表)** 。 +为了提高虚拟地址到物理地址的转换速度,操作系统在 **页表方案** 基础之上引入了 **转址旁路缓存(Translation Lookaside Buffer,TLB,也被称为快表)** 。 ![加入 TLB 之后的地址翻译](https://oss.javaguide.cn/github/javaguide/cs-basics/operating-system/physical-virtual-address-translation-mmu.png) @@ -282,7 +282,7 @@ TLB 的设计思想非常简单,但命中率往往非常高,效果很好。 ![常见的页面置换算法](https://oss.javaguide.cn/github/javaguide/cs-basics/operating-system/image-20230409113009139.png) 1. **最佳页面置换算法(OPT,Optimal)**:优先选择淘汰的页面是以后永不使用的,或者是在最长时间内不再被访问的页面,这样可以保证获得最低的缺页率。但由于人们目前无法预知进程在内存下的若干页面中哪个是未来最长时间内不再被访问的,因而该算法无法实现,只是理论最优的页面置换算法,可以作为衡量其他置换算法优劣的标准。 -2. **先进先出页面置换算法(FIFO,First In First Out)** : 最简单的一种页面置换算法,总是淘汰最先进入内存的页面,即选择在内存中驻留时间最久的页面进行淘汰。该算法易于实现和理解,一般只需要通过一个 FIFO 队列即可需求。不过,它的性能并不是很好。 +2. **先进先出页面置换算法(FIFO,First In First Out)** : 最简单的一种页面置换算法,总是淘汰最先进入内存的页面,即选择在内存中驻留时间最久的页面进行淘汰。该算法易于实现和理解,一般只需要通过一个 FIFO 队列即可满足需求。不过,它的性能并不是很好。 3. **最近最久未使用页面置换算法(LRU ,Least Recently Used)**:LRU 算法赋予每个页面一个访问字段,用来记录一个页面自上次被访问以来所经历的时间 T,当须淘汰一个页面时,选择现有页面中其 T 值最大的,即最近最久未使用的页面予以淘汰。LRU 算法是根据各页之前的访问情况来实现,因此是易于实现的。OPT 算法是根据各页未来的访问情况来实现,因此是不可实现的。 4. **最少使用页面置换算法(LFU,Least Frequently Used)** : 和 LRU 算法比较像,不过该置换算法选择的是之前一段时间内使用最少的页面作为淘汰页。 5. **时钟页面置换算法(Clock)**:可以认为是一种最近未使用算法,即逐出的页面都是最近没有使用的那个。 @@ -317,12 +317,16 @@ LRU 算法是实际使用中应用的比较多,也被认为是最接近 OPT ### 段页机制 -结合了段式管理和页式管理的一种内存管理机制,把物理内存先分成若干段,每个段又继续分成若干大小相等的页。 +结合了段式管理和页式管理的一种内存管理机制。程序视角中,内存被划分为多个逻辑段,每个逻辑段进一步被划分为固定大小的页。 在段页式机制下,地址翻译的过程分为两个步骤: -1. 段式地址映射。 -2. 页式地址映射。 +1. **段式地址映射(虚拟地址 → 线性地址):** + - 虚拟地址 = 段选择符(段号)+ 段内偏移。 + - 根据段号查段表,找到段基址,加上段内偏移得到线性地址。 +2. **页式地址映射(线性地址 → 物理地址):** + - 线性地址 = 页号 + 页内偏移。 + - 根据页号查页表,找到物理页框号,加上页内偏移得到物理地址。 ### 局部性原理 @@ -362,7 +366,7 @@ LRU 算法是实际使用中应用的比较多,也被认为是最接近 OPT **2、软链接(Symbolic Link 或 Symlink)** - 软链接和源文件的 inode 节点号不同,而是指向一个文件路径。 -- 源文件删除后,硬链接依然存在,但是指向的是一个无效的文件路径。 +- 源文件删除后,软链接依然存在,但是指向的是一个无效的文件路径。 - 软连接类似于 Windows 系统中的快捷方式。 - 不同于硬链接,可以对目录或者不存在的文件创建软链接,并且,软链接可以跨越文件系统。 - `ln -s` 命令用于创建软链接。 @@ -404,8 +408,10 @@ LRU 算法是实际使用中应用的比较多,也被认为是最接近 OPT - 《深入理解计算机系统》 - 《重学操作系统》 - 《现代操作系统原理与实现》 -- 王道考研操作系统知识点整理:https://wizardforcel.gitbooks.io/wangdaokaoyan-os/content/13.html -- 内存管理之伙伴系统与 SLAB:https://blog.csdn.net/qq_44272681/article/details/124199068 -- 为什么 Linux 需要虚拟内存:https://draveness.me/whys-the-design-os-virtual-memory/ -- 程序员的自我修养(七):内存缺页错误:https://liam.page/2017/09/01/page-fault/ -- 虚拟内存的那点事儿:https://juejin.cn/post/6844903507594575886 +- 王道考研操作系统知识点整理: +- 内存管理之伙伴系统与 SLAB: +- 为什么 Linux 需要虚拟内存: +- 程序员的自我修养(七):内存缺页错误: +- 虚拟内存的那点事儿: + + diff --git a/docs/cs-basics/operating-system/shell-intro.md b/docs/cs-basics/operating-system/shell-intro.md index a9f666fb7d5..48066214c23 100644 --- a/docs/cs-basics/operating-system/shell-intro.md +++ b/docs/cs-basics/operating-system/shell-intro.md @@ -28,14 +28,14 @@ Shell 编程在我们的日常开发工作中非常实用,目前 Linux 系统 另外,了解 shell 编程也是大部分互联网公司招聘后端开发人员的要求。下图是我截取的一些知名互联网公司对于 Shell 编程的要求。 -![大型互联网公司对于shell编程技能的要求](https://my-blog-to-use.oss-cn-beijing.aliyuncs.com/18-11-16/60190220.jpg) +![大型互联网公司对于shell编程技能的要求](https://oss.javaguide.cn/github/javaguide/cs-basics/shell/60190220.jpg) ### 什么是 Shell? 简单来说“Shell 编程就是对一堆 Linux 命令的逻辑化处理”。 W3Cschool 上的一篇文章是这样介绍 Shell 的,如下图所示。 -![什么是 Shell?](https://my-blog-to-use.oss-cn-beijing.aliyuncs.com/18-11-26/19456505.jpg) +![什么是 Shell?](https://oss.javaguide.cn/github/javaguide/cs-basics/shell/19456505.jpg) ### Shell 编程的 Hello World @@ -59,7 +59,7 @@ shell 中 # 符号表示注释。**shell 的第一行比较特殊,一般都会 (4) 运行脚本:`./helloworld.sh` 。(注意,一定要写成 `./helloworld.sh` ,而不是 `helloworld.sh` ,运行其它二进制的程序也一样,直接写 `helloworld.sh` ,linux 系统会去 PATH 里寻找有没有叫 helloworld.sh 的,而只有 /bin, /sbin, /usr/bin,/usr/sbin 等在 PATH 里,你的当前目录通常不在 PATH 里,所以写成 `helloworld.sh` 是会找不到命令的,要用`./helloworld.sh` 告诉系统说,就在当前目录找。) -![shell 编程Hello World](https://my-blog-to-use.oss-cn-beijing.aliyuncs.com/18-11-16/55296212.jpg) +![shell 编程Hello World](https://oss.javaguide.cn/github/javaguide/cs-basics/shell/55296212.jpg) ## Shell 变量 @@ -97,7 +97,7 @@ echo $hello echo "helloworld!" ``` -![使用自己定义的变量](https://my-blog-to-use.oss-cn-beijing.aliyuncs.com/18-11-17/19835037.jpg) +![使用自己定义的变量](https://oss.javaguide.cn/github/javaguide/cs-basics/shell/19835037.jpg) **Shell 编程中的变量名的命名的注意事项:** @@ -110,21 +110,21 @@ echo "helloworld!" 字符串是 shell 编程中最常用最有用的数据类型(除了数字和字符串,也没啥其它类型好用了),字符串可以用单引号,也可以用双引号。这点和 Java 中有所不同。 -在单引号中所有的特殊符号,如$和反引号都没有特殊含义。在双引号中,除了"$","\"和反引号,其他的字符没有特殊含义。 +在单引号中所有的特殊符号,如$和反引号都没有特殊含义。在双引号中,除了"$"、"\\"、反引号和感叹号(需开启 `history expansion`),其他的字符没有特殊含义。 **单引号字符串:** ```shell #!/bin/bash name='SnailClimb' -hello='Hello, I am '$name'!' +hello='Hello, I am $name!' echo $hello ``` 输出内容: -``` -Hello, I am '$name'! +```plain +Hello, I am $name! ``` **双引号字符串:** @@ -132,13 +132,13 @@ Hello, I am '$name'! ```shell #!/bin/bash name='SnailClimb' -hello="Hello, I am "$name"!" +hello="Hello, I am $name!" echo $hello ``` 输出内容: -``` +```plain Hello, I am SnailClimb! ``` @@ -161,7 +161,7 @@ echo $greeting_2 $greeting_3 输出结果: -![输出结果](https://my-blog-to-use.oss-cn-beijing.aliyuncs.com/18-11-17/51148933.jpg) +![输出结果](https://oss.javaguide.cn/github/javaguide/cs-basics/shell/51148933.jpg) **获取字符串长度:** @@ -177,7 +177,7 @@ expr length "$name"; 输出结果: -``` +```plain 10 10 ``` @@ -220,7 +220,7 @@ var="https://www.runoob.com/linux/linux-shell-variable.html" # 注: *为通配符, 意为匹配任意数量的任意字符 s1=${var%%t*} #h s2=${var%t*} #https://www.runoob.com/linux/linux-shell-variable.h -s3=${var%%.*} #http://www +s3=${var%%.*} #https://www s4=${var#*/} #/www.runoob.com/linux/linux-shell-variable.html s5=${var##*/} #linux-shell-variable.html ``` @@ -261,7 +261,7 @@ Shell 编程支持下面几种运算符 ### 算数运算符 -![算数运算符](https://my-blog-to-use.oss-cn-beijing.aliyuncs.com/18-11-22/4937342.jpg) +![算数运算符](https://oss.javaguide.cn/github/javaguide/cs-basics/shell/4937342.jpg) 我以加法运算符做一个简单的示例(注意:不是单引号,是反引号): @@ -277,7 +277,7 @@ echo "Total value : $val" 关系运算符只支持数字,不支持字符串,除非字符串的值是数字。 -![shell关系运算符](https://my-blog-to-use.oss-cn-beijing.aliyuncs.com/18-11-22/64391380.jpg) +![shell关系运算符](https://oss.javaguide.cn/github/javaguide/cs-basics/shell/64391380.jpg) 通过一个简单的示例演示关系运算符的使用,下面 shell 程序的作用是当 score=100 的时候输出 A 否则输出 B。 @@ -295,13 +295,13 @@ fi 输出结果: -``` +```plain B ``` ### 逻辑运算符 -![逻辑运算符](https://my-blog-to-use.oss-cn-beijing.aliyuncs.com/18-11-22/60545848.jpg) +![逻辑运算符](https://oss.javaguide.cn/github/javaguide/cs-basics/shell/60545848.jpg) 示例: @@ -314,13 +314,13 @@ echo $a; ### 布尔运算符 -![布尔运算符](https://my-blog-to-use.oss-cn-beijing.aliyuncs.com/18-11-22/93961425.jpg) +![布尔运算符](https://oss.javaguide.cn/github/javaguide/cs-basics/shell/93961425.jpg) 这里就不做演示了,应该挺简单的。 ### 字符串运算符 -![ 字符串运算符](https://my-blog-to-use.oss-cn-beijing.aliyuncs.com/18-11-22/309094.jpg) +![ 字符串运算符](https://oss.javaguide.cn/github/javaguide/cs-basics/shell/309094.jpg) 简单示例: @@ -338,17 +338,17 @@ fi 输出: -``` +```plain a 不等于 b ``` ### 文件相关运算符 -![文件相关运算符](https://my-blog-to-use.oss-cn-beijing.aliyuncs.com/18-11-22/60359774.jpg) +![文件相关运算符](https://oss.javaguide.cn/github/javaguide/cs-basics/shell/60359774.jpg) 使用方式很简单,比如我们定义好了一个文件路径`file="/usr/learnshell/test.sh"` 如果我们想判断这个文件是否可读,可以这样`if [ -r $file ]` 如果想判断这个文件是否可写,可以这样`-w $file`,是不是很简单。 -## shell 流程控制 +## Shell 流程控制 ### if 条件语句 @@ -371,7 +371,7 @@ fi 输出结果: -``` +```plain a 小于 b ``` @@ -406,7 +406,8 @@ done ```shell #!/bin/bash -for((i=1;i<=5;i++));do +length=5 +for((i=1;i<=length;i++));do echo $i; done; ``` @@ -438,7 +439,7 @@ done 输出内容: -``` +```plain 按下 退出 输入你最喜欢的电影: 变形金刚 是的!变形金刚 是一个好电影 @@ -453,7 +454,7 @@ do done ``` -## shell 函数 +## Shell 函数 ### 不带参数没有返回值的函数 @@ -469,7 +470,7 @@ echo "-----函数执行完毕-----" 输出结果: -``` +```plain -----函数开始执行----- 这是我的第一个 shell 函数! -----函数执行完毕----- @@ -495,7 +496,7 @@ echo "输入的两个数字之和为 $?" 输出结果: -``` +```plain 输入第一个数字: 1 输入第二个数字: @@ -522,7 +523,7 @@ funWithParam 1 2 3 4 5 6 7 8 9 34 73 输出结果: -``` +```plain 第一个参数为 1 ! 第二个参数为 2 ! 第十个参数为 10 ! @@ -531,3 +532,5 @@ funWithParam 1 2 3 4 5 6 7 8 9 34 73 参数总数有 11 个! 作为一个字符串输出所有参数 1 2 3 4 5 6 7 8 9 34 73 ! ``` + + diff --git a/docs/database/basis.md b/docs/database/basis.md index 8848dcfbad3..1df5d538fb8 100644 --- a/docs/database/basis.md +++ b/docs/database/basis.md @@ -5,6 +5,8 @@ tag: - 数据库基础 --- + + 数据库知识基础,这部分内容一定要理解记忆。虽然这部分内容只是理论知识,但是非常重要,这是后面学习 MySQL 数据库的基础。PS: 这部分内容由于涉及太多概念性内容,所以参考了维基百科和百度百科相应的介绍。 ## 什么是数据库, 数据库管理系统, 数据库系统, 数据库管理员? @@ -34,7 +36,7 @@ ER 图由下面 3 个要素组成: - **实体**:通常是现实世界的业务对象,当然使用一些逻辑对象也可以。比如对于一个校园管理系统,会涉及学生、教师、课程、班级等等实体。在 ER 图中,实体使用矩形框表示。 - **属性**:即某个实体拥有的属性,属性用来描述组成实体的要素,对于产品设计来说可以理解为字段。在 ER 图中,属性使用椭圆形表示。 -- **联系**:即实体与实体之间的关系,这个关系不仅有业务关联关系,还能通过数字表示实体之间的数量对照关系。例如,一个班级会有多个学生就是一种实体间的联系。 +- **联系**:即实体与实体之间的关系,在 ER 图中用菱形表示,这个关系不仅有业务关联关系,还能通过数字表示实体之间的数量对照关系。例如,一个班级会有多个学生就是一种实体间的联系。 下图是一个学生选课的 ER 图,每个学生可以选若干门课程,同一门课程也可以被若干人选择,所以它们之间的关系是多对多(M: N)。另外,还有其他两种实体之间的关系是:1 对 1(1:1)、1 对多(1: N)。 @@ -61,9 +63,9 @@ ER 图由下面 3 个要素组成: 一些重要的概念: - **函数依赖(functional dependency)**:若在一张表中,在属性(或属性组)X 的值确定的情况下,必定能确定属性 Y 的值,那么就可以说 Y 函数依赖于 X,写作 X → Y。 -- **部分函数依赖(partial functional dependency)**:如果 X→Y,并且存在 X 的一个真子集 X0,使得 X0→Y,则称 Y 对 X 部分函数依赖。比如学生基本信息表 R 中(学号,身份证号,姓名)当然学号属性取值是唯一的,在 R 关系中,(学号,身份证号)->(姓名),(学号)->(姓名),(身份证号)->(姓名);所以姓名部分函数依赖与(学号,身份证号); +- **部分函数依赖(partial functional dependency)**:如果 X→Y,并且存在 X 的一个真子集 X0,使得 X0→Y,则称 Y 对 X 部分函数依赖。比如学生基本信息表 R 中(学号,身份证号,姓名)当然学号属性取值是唯一的,在 R 关系中,(学号,身份证号)->(姓名),(学号)->(姓名),(身份证号)->(姓名);所以姓名部分函数依赖于(学号,身份证号); - **完全函数依赖(Full functional dependency)**:在一个关系中,若某个非主属性数据项依赖于全部关键字称之为完全函数依赖。比如学生基本信息表 R(学号,班级,姓名)假设不同的班级学号有相同的,班级内学号不能相同,在 R 关系中,(学号,班级)->(姓名),但是(学号)->(姓名)不成立,(班级)->(姓名)不成立,所以姓名完全函数依赖与(学号,班级); -- **传递函数依赖**:在关系模式 R(U)中,设 X,Y,Z 是 U 的不同的属性子集,如果 X 确定 Y、Y 确定 Z,且有 X 不包含 Y,Y 不确定 X,(X∪Y)∩Z=空集合,则称 Z 传递函数依赖(transitive functional dependency) 于 X。传递函数依赖会导致数据冗余和异常。传递函数依赖的 Y 和 Z 子集往往同属于某一个事物,因此可将其合并放到一个表中。比如在关系 R(学号 , 姓名, 系名,系主任)中,学号 → 系名,系名 → 系主任,所以存在非主属性系主任对于学号的传递函数依赖。。 +- **传递函数依赖**:在关系模式 R(U)中,设 X,Y,Z 是 U 的不同的属性子集,如果 X 确定 Y、Y 确定 Z,且有 X 不包含 Y,Y 不确定 X,(X∪Y)∩Z=空集合,则称 Z 传递函数依赖(transitive functional dependency) 于 X。传递函数依赖会导致数据冗余和异常。传递函数依赖的 Y 和 Z 子集往往同属于某一个事物,因此可将其合并放到一个表中。比如在关系 R(学号 , 姓名, 系名,系主任)中,学号 → 系名,系名 → 系主任,所以存在非主属性系主任对于学号的传递函数依赖。 ### 3NF(第三范式) @@ -80,20 +82,20 @@ ER 图由下面 3 个要素组成: > 【强制】不得使用外键与级联,一切外键概念必须在应用层解决。 > -> 说明: 以学生和成绩的关系为例,学生表中的 student_id 是主键,那么成绩表中的 student_id 则为外键。如果更新学生表中的 student_id,同时触发成绩表中的 student_id 更新,即为级联更新。外键与级联更新适用于单机低并发,不适合分布式、高并发集群; 级联更新是强阻塞,存在数据库更新风暴的风 险; 外键影响数据库的插入速度 +> 说明: 以学生和成绩的关系为例,学生表中的 student_id 是主键,那么成绩表中的 student_id 则为外键。如果更新学生表中的 student_id,同时触发成绩表中的 student_id 更新,即为级联更新。外键与级联更新适用于单机低并发,不适合分布式、高并发集群;级联更新是强阻塞,存在数据库更新风暴的风险;外键影响数据库的插入速度 为什么不要用外键呢?大部分人可能会这样回答: -1. **增加了复杂性:** a. 每次做 DELETE 或者 UPDATE 都必须考虑外键约束,会导致开发的时候很痛苦, 测试数据极为不方便; b. 外键的主从关系是定的,假如那天需求有变化,数据库中的这个字段根本不需要和其他表有关联的话就会增加很多麻烦。 -2. **增加了额外工作**:数据库需要增加维护外键的工作,比如当我们做一些涉及外键字段的增,删,更新操作之后,需要触发相关操作去检查,保证数据的的一致性和正确性,这样会不得不消耗资源;(个人觉得这个不是不用外键的原因,因为即使你不使用外键,你在应用层面也还是要保证的。所以,我觉得这个影响可以忽略不计。) +1. **增加了复杂性:** a. 每次做 DELETE 或者 UPDATE 都必须考虑外键约束,会导致开发的时候很痛苦, 测试数据极为不方便; b. 外键的主从关系是定的,假如哪天需求有变化,数据库中的这个字段根本不需要和其他表有关联的话就会增加很多麻烦。 +2. **增加了额外工作**:数据库需要增加维护外键的工作,比如当我们做一些涉及外键字段的增,删,更新操作之后,需要触发相关操作去检查,保证数据的的一致性和正确性,这样会不得不消耗数据库资源。如果在应用层面去维护的话,可以减小数据库压力; 3. **对分库分表不友好**:因为分库分表下外键是无法生效的。 -4. ...... +4. …… 我个人觉得上面这种回答不是特别的全面,只是说了外键存在的一个常见的问题。实际上,我们知道外键也是有很多好处的,比如: 1. 保证了数据库数据的一致性和完整性; 2. 级联操作方便,减轻了程序代码量; -3. ...... +3. …… 所以说,不要一股脑的就抛弃了外键这个概念,既然它存在就有它存在的道理,如果系统不涉及分库分表,并发量不是很高的情况还是可以考虑使用外键的。 @@ -130,7 +132,7 @@ ER 图由下面 3 个要素组成: ### 执行速度不同 -一般来说:`drop` > `truncate` > `delete`(这个我没有设计测试过)。 +一般来说:`drop` > `truncate` > `delete`(这个我没有实际测试过)。 - `delete`命令执行的时候会产生数据库的`binlog`日志,而日志记录是需要消耗时间的,但是也有个好处方便数据回滚恢复。 - `truncate`命令执行的时候不会产生数据库日志,因此比`delete`要快。除此之外,还会把表的自增值重置和索引恢复到初始大小等。 @@ -152,3 +154,5 @@ Tips:你应该更多地关注在使用场景上,而不是执行效率。 - - - + + diff --git a/docs/database/character-set.md b/docs/database/character-set.md index 2e5f829afc0..e462a5c97e3 100644 --- a/docs/database/character-set.md +++ b/docs/database/character-set.md @@ -11,7 +11,7 @@ MySQL 字符编码集中有两套 UTF-8 编码实现:**`utf8`** 和 **`utf8mb4 为什么会这样呢?这篇文章可以从源头给你解答。 -## 何为字符集? +## 字符集是什么? 字符是各种文字和符号的统称,包括各个国家文字、标点符号、表情、数字等等。 **字符集** 就是一系列字符的集合。字符集的种类较多,每个字符集可以表示的字符范围通常不同,就比如说有些字符集是无法表示汉字的。 @@ -19,9 +19,15 @@ MySQL 字符编码集中有两套 UTF-8 编码实现:**`utf8`** 和 **`utf8mb4 我们要将这些字符和二进制的数据一一对应起来,比如说字符“a”对应“01100001”,反之,“01100001”对应 “a”。我们将字符对应二进制数据的过程称为"**字符编码**",反之,二进制数据解析成字符的过程称为“**字符解码**”。 +## 字符编码是什么? + +字符编码是一种将字符集中的字符与计算机中的二进制数据相互转换的方法,可以看作是一种映射规则。也就是说,字符编码的目的是为了让计算机能够存储和传输各种文字信息。 + +每种字符集都有自己的字符编码规则,常用的字符集编码规则有 ASCII 编码、 GB2312 编码、GBK 编码、GB18030 编码、Big5 编码、UTF-8 编码、UTF-16 编码等。 + ## 有哪些常见的字符集? -常见的字符集有 ASCII、GB2312、GBK、UTF-8......。 +常见的字符集有:ASCII、GB2312、GB18030、GBK、Unicode……。 不同的字符集的主要区别在于: @@ -64,7 +70,7 @@ GB18030 完全兼容 GB2312 和 GBK 字符集,纳入中国国内少数民族 BIG5 主要针对的是繁体中文,收录了 13000 多个汉字。 -### Unicode & UTF-8 编码 +### Unicode & UTF-8 为了更加适合本国语言,诞生了很多种字符集。 @@ -94,18 +100,181 @@ UTF-8 可以根据不同的符号自动选择编码的长短,像英文字符 UTF-32 的规则最简单,不过缺陷也比较明显,对于英文字母这类字符消耗的空间是 UTF-8 的 4 倍之多。 -**UTF-8** 是目前使用最广的一种字符编码,。 +**UTF-8** 是目前使用最广的一种字符编码。 ![](https://oss.javaguide.cn/javaguide/1280px-Utf8webgrowth.svg.png) ## MySQL 字符集 -MySQL 支持很多种字符编码的方式,比如 UTF-8、GB2312、GBK、BIG5。 +MySQL 支持很多种字符集的方式,比如 GB2312、GBK、BIG5、多种 Unicode 字符集(UTF-8 编码、UTF-16 编码、UCS-2 编码、UTF-32 编码等等)。 -你可以通过 `SHOW CHARSET` 命令来查看。 +### 查看支持的字符集 + +你可以通过 `SHOW CHARSET` 命令来查看,支持 like 和 where 子句。 ![](https://oss.javaguide.cn/javaguide/image-20211008164229671.png) +### 默认字符集 + +在 MySQL5.7 中,默认字符集是 `latin1` ;在 MySQL8.0 中,默认字符集是 `utf8mb4` + +### 字符集的层次级别 + +MySQL 中的字符集有以下的层次级别: + +- `server`(MySQL 实例级别) +- `database`(库级别) +- `table`(表级别) +- `column`(字段级别) + +它们的优先级可以简单的认为是从上往下依次增大,也即 `column` 的优先级会大于 `table` 等其余层次的。如指定 MySQL 实例级别字符集是`utf8mb4`,指定某个表字符集是`latin1`,那么这个表的所有字段如果不指定的话,编码就是`latin1`。 + +#### server + +不同版本的 MySQL 其 `server` 级别的字符集默认值不同,在 MySQL5.7 中,其默认值是 `latin1` ;在 MySQL8.0 中,其默认值是 `utf8mb4` 。 + +当然也可以通过在启动 `mysqld` 时指定 `--character-set-server` 来设置 `server` 级别的字符集。 + +```bash +mysqld +mysqld --character-set-server=utf8mb4 +mysqld --character-set-server=utf8mb4 \ + --collation-server=utf8mb4_0900_ai_ci +``` + +或者如果你是通过源码构建的方式启动的 MySQL,你可以在 `cmake` 命令中指定选项: + +```sh +cmake . -DDEFAULT_CHARSET=latin1 +或者 +cmake . -DDEFAULT_CHARSET=latin1 \ + -DDEFAULT_COLLATION=latin1_german1_ci +``` + +此外,你也可以在运行时改变 `character_set_server` 的值,从而达到修改 `server` 级别的字符集的目的。 + +`server` 级别的字符集是 MySQL 服务器的全局设置,它不仅会作为创建或修改数据库时的默认字符集(如果没有指定其他字符集),还会影响到客户端和服务器之间的连接字符集,具体可以查看 [MySQL Connector/J 8.0 - 6.7 Using Character Sets and Unicode](https://dev.mysql.com/doc/connector-j/8.0/en/connector-j-reference-charsets.html)。 + +#### database + +`database` 级别的字符集是我们在创建数据库和修改数据库时指定的: + +```sql +CREATE DATABASE db_name + [[DEFAULT] CHARACTER SET charset_name] + [[DEFAULT] COLLATE collation_name] + +ALTER DATABASE db_name + [[DEFAULT] CHARACTER SET charset_name] + [[DEFAULT] COLLATE collation_name] +``` + +如前面所说,如果在执行上述语句时未指定字符集,那么 MySQL 将会使用 `server` 级别的字符集。 + +可以通过下面的方式查看某个数据库的字符集: + +```sql +USE db_name; +SELECT @@character_set_database, @@collation_database; +``` + +```sql +SELECT DEFAULT_CHARACTER_SET_NAME, DEFAULT_COLLATION_NAME +FROM INFORMATION_SCHEMA.SCHEMATA WHERE SCHEMA_NAME = 'db_name'; +``` + +#### table + +`table` 级别的字符集是在创建表和修改表时指定的: + +```sql +CREATE TABLE tbl_name (column_list) + [[DEFAULT] CHARACTER SET charset_name] + [COLLATE collation_name]] + +ALTER TABLE tbl_name + [[DEFAULT] CHARACTER SET charset_name] + [COLLATE collation_name] +``` + +如果在创建表和修改表时未指定字符集,那么将会使用 `database` 级别的字符集。 + +#### column + +`column` 级别的字符集同样是在创建表和修改表时指定的,只不过它是定义在列中。下面是个例子: + +```sql +CREATE TABLE t1 +( + col1 VARCHAR(5) + CHARACTER SET latin1 + COLLATE latin1_german1_ci +); +``` + +如果未指定列级别的字符集,那么将会使用表级别的字符集。 + +### 连接字符集 + +前面说到了字符集的层次级别,它们是和存储相关的。而连接字符集涉及的是和 MySQL 服务器的通信。 + +连接字符集与下面这几个变量息息相关: + +- `character_set_client` :描述了客户端发送给服务器的 SQL 语句使用的是什么字符集。 +- `character_set_connection` :描述了服务器接收到 SQL 语句时使用什么字符集进行翻译。 +- `character_set_results` :描述了服务器返回给客户端的结果使用的是什么字符集。 + +它们的值可以通过下面的 SQL 语句查询: + +```sql +SELECT * FROM performance_schema.session_variables +WHERE VARIABLE_NAME IN ( +'character_set_client', 'character_set_connection', +'character_set_results', 'collation_connection' +) ORDER BY VARIABLE_NAME; +``` + +```sql +SHOW SESSION VARIABLES LIKE 'character\_set\_%'; +``` + +如果要想修改前面提到的几个变量的值,有以下方式: + +1、修改配置文件 + +```properties +[mysql] +# 只针对MySQL客户端程序 +default-character-set=utf8mb4 +``` + +2、使用 SQL 语句 + +```sql +set names utf8mb4 +# 或者一个个进行修改 +# SET character_set_client = utf8mb4; +# SET character_set_results = utf8mb4; +# SET collation_connection = utf8mb4; +``` + +### JDBC 对连接字符集的影响 + +不知道你们有没有碰到过存储 emoji 表情正常,但是使用类似 Navicat 之类的软件的进行查询的时候,发现 emoji 表情变成了问号的情况。这个问题很有可能就是 JDBC 驱动引起的。 + +根据前面的内容,我们知道连接字符集也是会影响我们存储的数据的,而 JDBC 驱动会影响连接字符集。 + +`mysql-connector-java` (JDBC 驱动)主要通过这几个属性影响连接字符集: + +- `characterEncoding` +- `characterSetResults` + +以 `DataGrip 2023.1.2` 来说,在它配置数据源的高级对话框中,可以看到 `characterSetResults` 的默认值是 `utf8` ,在使用 `mysql-connector-java 8.0.25` 时,连接字符集最后会被设置成 `utf8mb3` 。那么这种情况下 emoji 表情就会被显示为问号,并且当前版本驱动还不支持把 `characterSetResults` 设置为 `utf8mb4` ,不过换成 `mysql-connector-java driver 8.0.29` 却是允许的。 + +具体可以看一下 StackOverflow 的 [DataGrip MySQL stores emojis correctly but displays them as?](https://stackoverflow.com/questions/54815419/datagrip-mysql-stores-emojis-correctly-but-displays-them-as)这个回答。 + +### UTF-8 使用 + 通常情况下,我们建议使用 UTF-8 作为默认的字符编码方式。 不过,这里有一个小坑。 @@ -127,10 +296,10 @@ MySQL 字符编码集中有两套 UTF-8 编码实现: ```sql CREATE TABLE `user` ( - `id` varchar(66) CHARACTER SET utf8mb4 NOT NULL, - `name` varchar(33) CHARACTER SET utf8mb4 NOT NULL, - `phone` varchar(33) CHARACTER SET utf8mb4 DEFAULT NULL, - `password` varchar(100) CHARACTER SET utf8mb4 DEFAULT NULL + `id` varchar(66) CHARACTER SET utf8mb3 NOT NULL, + `name` varchar(33) CHARACTER SET utf8mb3 NOT NULL, + `phone` varchar(33) CHARACTER SET utf8mb3 DEFAULT NULL, + `password` varchar(100) CHARACTER SET utf8mb3 DEFAULT NULL ) ENGINE=InnoDB DEFAULT CHARSET=utf8; ``` @@ -145,7 +314,7 @@ VALUES 报错信息如下: -``` +```plain Incorrect string value: '\xF0\x9F\x98\x98\xF0\x9F...' for column 'name' at row 1 ``` @@ -157,3 +326,8 @@ Incorrect string value: '\xF0\x9F\x98\x98\xF0\x9F...' for column 'name' at row 1 - GB2312-维基百科: - UTF-8-维基百科: - GB18030-维基百科: +- MySQL8 文档: +- MySQL5.7 文档: +- MySQL Connector/J 文档: + + diff --git a/docs/database/elasticsearch/elasticsearch-questions-01.md b/docs/database/elasticsearch/elasticsearch-questions-01.md index be3817a5949..fe6daa6926c 100644 --- a/docs/database/elasticsearch/elasticsearch-questions-01.md +++ b/docs/database/elasticsearch/elasticsearch-questions-01.md @@ -11,3 +11,5 @@ tag: ![](https://oss.javaguide.cn/javamianshizhibei/elasticsearch-questions.png) + + diff --git a/docs/database/mongodb/mongodb-questions-01.md b/docs/database/mongodb/mongodb-questions-01.md index 699d11bd941..81b7db98890 100644 --- a/docs/database/mongodb/mongodb-questions-01.md +++ b/docs/database/mongodb/mongodb-questions-01.md @@ -116,7 +116,7 @@ MongoDB 预留了几个特殊的数据库。 - 随着项目的发展,使用类 JSON 格式(BSON)保存数据是否满足项目需求?MongoDB 中的记录就是一个 BSON 文档,它是由键值对组成的数据结构,类似于 JSON 对象,是 MongoDB 中的基本数据单元。 - 是否需要大数据量的存储?是否需要快速水平扩展?MongoDB 支持分片集群,可以很方便地添加更多的节点(实例),让集群存储更多的数据,具备更强的性能。 - 是否需要更多类型索引来满足更多应用场景?MongoDB 支持多种类型的索引,包括单字段索引、复合索引、多键索引、哈希索引、文本索引、 地理位置索引等,每种类型的索引有不同的使用场合。 -- ...... +- …… ## MongoDB 存储引擎 @@ -143,13 +143,13 @@ MongoDB 预留了几个特殊的数据库。 上面也说了,自 MongoDB 3.2 以后,默认的存储引擎为 WiredTiger 存储引擎。在 WiredTiger 引擎官网上,我们发现 WiredTiger 使用的是 B+ 树作为其存储结构: -``` +```plain WiredTiger maintains a table's data in memory using a data structure called a B-Tree ( B+ Tree to be specific), referring to the nodes of a B-Tree as pages. Internal pages carry only keys. The leaf pages store both keys and values. ``` 此外,WiredTiger 还支持 [LSM(Log Structured Merge)](https://source.wiredtiger.com/3.1.0/lsm.html) 树作为存储结构,MongoDB 在使用 WiredTiger 作为存储引擎时,默认使用的是 B+ 树。 -如果想要了解 MongoDB 使用 B 树的原因,可以看看这篇文章:[为什么 MongoDB 使用 B 树?](https://mp.weixin.qq.com/s/mMWdpbYRiT6LQcdaj4hgXQ)。 +如果想要了解 MongoDB 使用 B+ 树的原因,可以看看这篇文章:[【驳斥八股文系列】别瞎分析了,MongoDB 使用的是 B+ 树,不是你们以为的 B 树](https://zhuanlan.zhihu.com/p/519658576)。 使用 B+ 树时,WiredTiger 以 **page** 为基本单位往磁盘读写数据。B+ 树的每个节点为一个 page,共有三种类型的 page: @@ -274,6 +274,63 @@ MongoDB 单文档原生支持原子性,也具备事务的特性。当谈论 Mo WiredTiger 日志也会被压缩,默认使用的也是 Snappy 压缩算法。如果日志记录小于或等于 128 字节,WiredTiger 不会压缩该记录。 +## Amazon Document 与 MongoDB 的差异 + +Amazon DocumentDB(与 MongoDB 兼容) 是一种快速、可靠、完全托管的数据库服务。Amazon DocumentDB 可在云中轻松设置、操作和扩展与 MongoDB 兼容的数据库。 + +### `$vectorSearch` 运算符 + +Amazon DocumentDB 不支持`$vectorSearch`作为独立运营商。相反,我们在`$search`运营商`vectorSearch`内部支持。有关更多信息,请参阅 [向量搜索 Amazon DocumentDB](https://docs.aws.amazon.com/zh_cn/documentdb/latest/developerguide/vector-search.html)。 + +### `OpCountersCommand` + +Amazon DocumentDB 的`OpCountersCommand`行为偏离于 MongoDB 的`opcounters.command` 如下: + +- MongoDB 的`opcounters.command` 计入除插入、更新和删除之外的所有命令,而 Amazon DocumentDB 的 `OpCountersCommand` 也排除 `find` 命令。 +- Amazon DocumentDB 将内部命令(例如`getCloudWatchMetricsV2`)对 `OpCountersCommand` 计入。 + +### 管理数据库和集合 + +Amazon DocumentDB 不支持管理或本地数据库,MongoDB `system.*` 或 `startup_log` 集合也不支持。 + +### `cursormaxTimeMS` + +在 Amazon DocumentDB 中,`cursor.maxTimeMS` 重置每个请求的计数器。`getMore`因此,如果指定了 3000MS `maxTimeMS`,则该查询耗时 2800MS,而每个后续`getMore`请求耗时 300MS,则游标不会超时。游标仅在单个操作(无论是查询还是单个`getMore`请求)耗时超过指定值时才将超时`maxTimeMS`。此外,检查游标执行时间的扫描器以五 (5) 分钟间隔尺寸运行。 + +### explain() + +Amazon DocumentDB 在利用分布式、容错、自修复的存储系统的专用数据库引擎上模拟 MongoDB 4.0 API。因此,查询计划和`explain()` 的输出在 Amazon DocumentDB 和 MongoDB 之间可能有所不同。希望控制其查询计划的客户可以使用 `$hint` 运算符强制选择首选索引。 + +### 字段名称限制 + +Amazon DocumentDB 不支持点“。” 例如,文档字段名称中 `db.foo.insert({‘x.1’:1})`。 + +Amazon DocumentDB 也不支持字段名称中的 $ 前缀。 + +例如,在 Amazon DocumentDB 或 MongoDB 中尝试以下命令: + +```shell +rs0:PRIMARY< db.foo.insert({"a":{"$a":1}}) +``` + +MongoDB 将返回以下内容: + +```shell +WriteResult({ "nInserted" : 1 }) +``` + +Amazon DocumentDB 将返回一个错误: + +```shell +WriteResult({ + "nInserted" : 0, + "writeError" : { + "code" : 2, + "errmsg" : "Document can't have $ prefix field names: $a" + } +}) +``` + ## 参考 - MongoDB 官方文档(主要参考资料,以官方文档为准): @@ -282,3 +339,5 @@ WiredTiger 日志也会被压缩,默认使用的也是 Snappy 压缩算法。 - Transactions - MongoDB 官方文档: - WiredTiger Storage Engine - MongoDB 官方文档: - WiredTiger 存储引擎之一:基础数据结构分析: + + diff --git a/docs/database/mongodb/mongodb-questions-02.md b/docs/database/mongodb/mongodb-questions-02.md index 851981a8fd0..dcd90d72c4d 100644 --- a/docs/database/mongodb/mongodb-questions-02.md +++ b/docs/database/mongodb/mongodb-questions-02.md @@ -26,7 +26,7 @@ tag: - **地理位置索引:** 基于经纬度的索引,适合 2D 和 3D 的位置查询。 - **唯一索引**:确保索引字段不会存储重复值。如果集合已经存在了违反索引的唯一约束的文档,则后台创建唯一索引会失败。 - **TTL 索引**:TTL 索引提供了一个过期机制,允许为每一个文档设置一个过期时间,当一个文档达到预设的过期时间之后就会被删除。 -- ...... +- …… ### 复合索引中字段的顺序有影响吗? @@ -271,3 +271,5 @@ Rebalance 操作是比较耗费系统资源的,我们可以通过在业务低 - Sharding - MongoDB 官方文档: - MongoDB 分片集群介绍 - 阿里云文档: - 分片集群使用注意事项 - - 腾讯云文档: + + diff --git a/docs/database/mysql/a-thousand-lines-of-mysql-study-notes.md b/docs/database/mysql/a-thousand-lines-of-mysql-study-notes.md index 4dc48353eeb..cb30376687b 100644 --- a/docs/database/mysql/a-thousand-lines-of-mysql-study-notes.md +++ b/docs/database/mysql/a-thousand-lines-of-mysql-study-notes.md @@ -5,7 +5,7 @@ tag: - MySQL --- -> 原文地址:https://shockerli.net/post/1000-line-mysql-note/ ,JavaGuide 对本文进行了简答排版,新增了目录。 +> 原文地址: ,JavaGuide 对本文进行了简答排版,新增了目录。 非常不错的总结,强烈建议保存下来,需要的时候看一看。 @@ -385,7 +385,7 @@ c. WHERE 子句 -- 运算符: =, <=>, <>, !=, <=, <, >=, >, !, &&, ||, in (not) null, (not) like, (not) in, (not) between and, is (not), and, or, not, xor - is/is not 加上ture/false/unknown,检验某个值的真假 + is/is not 加上true/false/unknown,检验某个值的真假 <=>与<>功能相同,<=>可用于null比较 d. GROUP BY 子句, 分组子句 GROUP BY 字段/别名 [排序方式] @@ -621,7 +621,7 @@ CREATE [OR REPLACE] [ALGORITHM = {UNDEFINED | MERGE | TEMPTABLE}] VIEW view_name ### 锁表 -```mysql +```sql /* 锁表 */ 表锁定只用于防止其它客户端进行不正当地读取和写入 MyISAM 支持表锁,InnoDB 支持行锁 @@ -633,7 +633,7 @@ MyISAM 支持表锁,InnoDB 支持行锁 ### 触发器 -```mysql +```sql /* 触发器 */ ------------------ 触发程序是与表有关的命名数据库对象,当该表出现特定事件时,将激活该对象 监听:记录的增加、修改、删除。 @@ -686,7 +686,7 @@ end ### SQL 编程 -```mysql +```sql /* SQL编程 */ ------------------ --// 局部变量 ---------- -- 变量声明 @@ -792,7 +792,7 @@ default(); CREATE FUNCTION function_name (参数列表) RETURNS 返回值类型 函数体 - 函数名,应该合法的标识符,并且不应该与已有的关键字冲突。 - - 一个函数应该属于某个数据库,可以使用db_name.funciton_name的形式执行当前函数所属数据库,否则为当前数据库。 + - 一个函数应该属于某个数据库,可以使用db_name.function_name的形式执行当前函数所属数据库,否则为当前数据库。 - 参数部分,由"参数名"和"参数类型"组成。多个参数用逗号隔开。 - 函数体由多条可用的mysql语句,流程控制,变量声明等语句构成。 - 多条语句应该使用 begin...end 语句块包含。 @@ -821,7 +821,7 @@ INOUT,表示混合型 ### 存储过程 -```mysql +```sql /* 存储过程 */ ------------------ 存储过程是一段可执行性代码的集合。相比函数,更偏向于业务逻辑。 调用:CALL 过程名 @@ -842,7 +842,7 @@ END ### 用户和权限管理 -```mysql +```sql /* 用户和权限管理 */ ------------------ -- root密码重置 1. 停止MySQL服务 @@ -924,7 +924,7 @@ GRANT OPTION -- 允许授予权限 ### 表维护 -```mysql +```sql /* 表维护 */ -- 分析和存储表的关键字分布 ANALYZE [LOCAL | NO_WRITE_TO_BINLOG] TABLE 表名 ... @@ -937,7 +937,7 @@ OPTIMIZE [LOCAL | NO_WRITE_TO_BINLOG] TABLE tbl_name [, tbl_name] ... ### 杂项 -```mysql +```sql /* 杂项 */ ------------------ 1. 可用反引号(`)为标识符(库名、表名、字段名、索引、别名)包裹,以避免与关键字重名!中文也可以作为标识符! 2. 每个库目录存在一个保存当前数据库的选项文件db.opt。 @@ -953,3 +953,5 @@ OPTIMIZE [LOCAL | NO_WRITE_TO_BINLOG] TABLE tbl_name [, tbl_name] ... 6. SQL对大小写不敏感 7. 清除已有语句:\c ``` + + diff --git a/docs/database/mysql/how-sql-executed-in-mysql.md b/docs/database/mysql/how-sql-executed-in-mysql.md index bd7a4a592f5..0b01d9a4da3 100644 --- a/docs/database/mysql/how-sql-executed-in-mysql.md +++ b/docs/database/mysql/how-sql-executed-in-mysql.md @@ -44,7 +44,7 @@ tag: 查询缓存主要用来缓存我们所执行的 SELECT 语句以及该语句的结果集。 -连接建立后,执行查询语句的时候,会先查询缓存,MySQL 会先校验这个 SQL 是否执行过,以 Key-Value 的形式缓存在内存中,Key 是查询预计,Value 是结果集。如果缓存 key 被命中,就会直接返回给客户端,如果没有命中,就会执行后续的操作,完成后也会把结果缓存起来,方便下一次调用。当然在真正执行缓存查询的时候还是会校验用户的权限,是否有该表的查询条件。 +连接建立后,执行查询语句的时候,会先查询缓存,MySQL 会先校验这个 SQL 是否执行过,以 Key-Value 的形式缓存在内存中,Key 是查询语句,Value 是结果集。如果缓存 key 被命中,就会直接返回给客户端,如果没有命中,就会执行后续的操作,完成后也会把结果缓存起来,方便下一次调用。当然在真正执行缓存查询的时候还是会校验用户的权限,是否有该表的查询条件。 MySQL 查询不建议使用缓存,因为查询缓存失效在实际业务场景中可能会非常频繁,假如你对一个表更新的话,这个表上的所有的查询缓存都会被清空。对于不经常更新的数据来说,使用缓存还是可以的。 @@ -86,12 +86,7 @@ select * from tb_student A where A.age='18' and A.name=' 张三 '; - 先检查该语句是否有权限,如果没有权限,直接返回错误信息,如果有权限,在 MySQL8.0 版本以前,会先查询缓存,以这条 SQL 语句为 key 在内存中查询是否有结果,如果有直接缓存,如果没有,执行下一步。 - 通过分析器进行词法分析,提取 SQL 语句的关键元素,比如提取上面这个语句是查询 select,提取需要查询的表名为 tb_student,需要查询所有的列,查询条件是这个表的 id='1'。然后判断这个 SQL 语句是否有语法错误,比如关键词是否正确等等,如果检查没问题就执行下一步。 -- 接下来就是优化器进行确定执行方案,上面的 SQL 语句,可以有两种执行方案: - - a.先查询学生表中姓名为“张三”的学生,然后判断是否年龄是 18。 - b.先找出学生中年龄 18 岁的学生,然后再查询姓名为“张三”的学生。 - - 那么优化器根据自己的优化算法进行选择执行效率最好的一个方案(优化器认为,有时候不一定最好)。那么确认了执行计划后就准备开始执行了。 +- 接下来就是优化器进行确定执行方案,上面的 SQL 语句,可以有两种执行方案:a.先查询学生表中姓名为“张三”的学生,然后判断是否年龄是 18。b.先找出学生中年龄 18 岁的学生,然后再查询姓名为“张三”的学生。那么优化器根据自己的优化算法进行选择执行效率最好的一个方案(优化器认为,有时候不一定最好)。那么确认了执行计划后就准备开始执行了。 - 进行权限校验,如果没有权限就会返回错误信息,如果有权限就会调用数据库引擎接口,返回引擎的执行结果。 @@ -99,13 +94,13 @@ select * from tb_student A where A.age='18' and A.name=' 张三 '; 以上就是一条查询 SQL 的执行流程,那么接下来我们看看一条更新语句如何执行的呢?SQL 语句如下: -``` +```plain update tb_student A set A.age='19' where A.name=' 张三 '; ``` 我们来给张三修改下年龄,在实际数据库肯定不会设置年龄这个字段的,不然要被技术负责人打的。其实这条语句也基本上会沿着上一个查询的流程走,只不过执行更新的时候肯定要记录日志啦,这就会引入日志模块了,MySQL 自带的日志模块是 **binlog(归档日志)** ,所有的存储引擎都可以使用,我们常用的 InnoDB 引擎还自带了一个日志模块 **redo log(重做日志)**,我们就以 InnoDB 模式下来探讨这个语句的执行流程。流程如下: -- 先查询到张三这一条数据,如果有缓存,也是会用到缓存。 +- 先查询到张三这一条数据,不会走查询缓存,因为更新语句会导致与该表相关的查询缓存失效。 - 然后拿到查询的语句,把 age 改为 19,然后调用引擎 API 接口,写入这一行数据,InnoDB 引擎把数据保存在内存中,同时记录 redo log,此时 redo log 进入 prepare 状态,然后告诉执行器,执行完成了,随时可以提交。 - 执行器收到通知后记录 binlog,然后调用引擎接口,提交 redo log 为提交状态。 - 更新完成。 @@ -138,3 +133,5 @@ update tb_student A set A.age='19' where A.name=' 张三 '; - 《MySQL 实战 45 讲》 - MySQL 5.6 参考手册: + + diff --git a/docs/database/mysql/images/redo-log.png b/docs/database/mysql/images/redo-log.png new file mode 100644 index 00000000000..87070397390 Binary files /dev/null and b/docs/database/mysql/images/redo-log.png differ diff --git a/docs/database/mysql/index-invalidation-caused-by-implicit-conversion.md b/docs/database/mysql/index-invalidation-caused-by-implicit-conversion.md index b433a40e571..377460c66a6 100644 --- a/docs/database/mysql/index-invalidation-caused-by-implicit-conversion.md +++ b/docs/database/mysql/index-invalidation-caused-by-implicit-conversion.md @@ -8,7 +8,7 @@ tag: > 本次测试使用的 MySQL 版本是 `5.7.26`,随着 MySQL 版本的更新某些特性可能会发生改变,本文不代表所述观点和结论于 MySQL 所有版本均准确无误,版本差异请自行甄别。 > -> 原文:https://www.guitu18.com/post/2019/11/24/61.html +> 原文: ## 前言 @@ -117,9 +117,9 @@ CALL pre_test1(); 根据官方文档的描述,我们的第 23 两条 SQL 都发生了隐式转换,第 2 条 SQL 的查询条件`num1 = '10000'`,左边是`int`类型右边是字符串,第 3 条 SQL 相反,那么根据官方转换规则第 7 条,左右两边都会转换为浮点数再进行比较。 -先看第 2 条 SQL:`SELECT * FROM`test1`WHERE num1 = '10000';` **左边为 int 类型**`10000`,转换为浮点数还是`10000`,右边字符串类型`'10000'`,转换为浮点数也是`10000`。两边的转换结果都是唯一确定的,所以不影响使用索引。 +先看第 2 条 SQL:``SELECT * FROM `test1` WHERE num1 = '10000';`` **左边为 int 类型**`10000`,转换为浮点数还是`10000`,右边字符串类型`'10000'`,转换为浮点数也是`10000`。两边的转换结果都是唯一确定的,所以不影响使用索引。 -第 3 条 SQL:`SELECT * FROM`test1`WHERE num2 = 10000;` **左边是字符串类型**`'10000'`,转浮点数为 10000 是唯一的,右边`int`类型`10000`转换结果也是唯一的。但是,因为左边是检索条件,`'10000'`转到`10000`虽然是唯一,但是其他字符串也可以转换为`10000`,比如`'10000a'`,`'010000'`,`'10000'`等等都能转为浮点数`10000`,这样的情况下,是不能用到索引的。 +第 3 条 SQL:``SELECT * FROM `test1` WHERE num2 = 10000;`` **左边是字符串类型**`'10000'`,转浮点数为 10000 是唯一的,右边`int`类型`10000`转换结果也是唯一的。但是,因为左边是检索条件,`'10000'`转到`10000`虽然是唯一,但是其他字符串也可以转换为`10000`,比如`'10000a'`,`'010000'`,`'10000'`等等都能转为浮点数`10000`,这样的情况下,是不能用到索引的。 关于这个**隐式转换**我们可以通过查询测试验证一下,先插入几条数据,其中`num2='10000a'`、`'010000'`和`'10000'`: @@ -129,7 +129,7 @@ INSERT INTO `test1` (`id`, `num1`, `num2`, `type1`, `type2`, `str1`, `str2`) VAL INSERT INTO `test1` (`id`, `num1`, `num2`, `type1`, `type2`, `str1`, `str2`) VALUES ('10000003', '10000', ' 10000', '0', '0', '2df3d9465ty2e4hd523', '2df3d9465ty2e4hd523'); ``` -然后使用第三条 SQL 语句`SELECT * FROM`test1`WHERE num2 = 10000;`进行查询: +然后使用第三条 SQL 语句``SELECT * FROM `test1` WHERE num2 = 10000;``进行查询: ![](https://oss.javaguide.cn/github/javaguide/mysqlindex-invalidation-caused-by-implicit-conversion-03.png) @@ -144,7 +144,7 @@ INSERT INTO `test1` (`id`, `num1`, `num2`, `type1`, `type2`, `str1`, `str2`) VAL 如此也就印证了之前的查询结果了。 -再次写一条 SQL 查询 str1 字段:`SELECT * FROM`test1`WHERE str1 = 1234;` +再次写一条 SQL 查询 str1 字段:``SELECT * FROM `test1` WHERE str1 = 1234;`` ![](https://oss.javaguide.cn/github/javaguide/mysqlindex-invalidation-caused-by-implicit-conversion-05.png) @@ -158,3 +158,5 @@ INSERT INTO `test1` (`id`, `num1`, `num2`, `type1`, `type2`, `str1`, `str2`) VAL 4. 字符串转换为数值类型时,非数字开头的字符串会转化为`0`,以数字开头的字符串会截取从第一个字符到第一个非数字内容为止的值为转化结果。 所以,我们在写 SQL 时一定要养成良好的习惯,查询的字段是什么类型,等号右边的条件就写成对应的类型。特别当查询的字段是字符串时,等号右边的条件一定要用引号引起来标明这是一个字符串,否则会造成索引失效触发全表扫描。 + + diff --git a/docs/database/mysql/innodb-implementation-of-mvcc.md b/docs/database/mysql/innodb-implementation-of-mvcc.md index 3712827f0bc..a2e19998d71 100644 --- a/docs/database/mysql/innodb-implementation-of-mvcc.md +++ b/docs/database/mysql/innodb-implementation-of-mvcc.md @@ -5,6 +5,37 @@ tag: - MySQL --- +## 多版本并发控制 (Multi-Version Concurrency Control) + +MVCC 是一种并发控制机制,用于在多个并发事务同时读写数据库时保持数据的一致性和隔离性。它是通过在每个数据行上维护多个版本的数据来实现的。当一个事务要对数据库中的数据进行修改时,MVCC 会为该事务创建一个数据快照,而不是直接修改实际的数据行。 + +1、读操作(SELECT): + +当一个事务执行读操作时,它会使用快照读取。快照读取是基于事务开始时数据库中的状态创建的,因此事务不会读取其他事务尚未提交的修改。具体工作情况如下: + +- 对于读取操作,事务会查找符合条件的数据行,并选择符合其事务开始时间的数据版本进行读取。 +- 如果某个数据行有多个版本,事务会选择不晚于其开始时间的最新版本,确保事务只读取在它开始之前已经存在的数据。 +- 事务读取的是快照数据,因此其他并发事务对数据行的修改不会影响当前事务的读取操作。 + +2、写操作(INSERT、UPDATE、DELETE): + +当一个事务执行写操作时,它会生成一个新的数据版本,并将修改后的数据写入数据库。具体工作情况如下: + +- 对于写操作,事务会为要修改的数据行创建一个新的版本,并将修改后的数据写入新版本。 +- 新版本的数据会带有当前事务的版本号,以便其他事务能够正确读取相应版本的数据。 +- 原始版本的数据仍然存在,供其他事务使用快照读取,这保证了其他事务不受当前事务的写操作影响。 + +3、事务提交和回滚: + +- 当一个事务提交时,它所做的修改将成为数据库的最新版本,并且对其他事务可见。 +- 当一个事务回滚时,它所做的修改将被撤销,对其他事务不可见。 + +4、版本的回收: + +为了防止数据库中的版本无限增长,MVCC 会定期进行版本的回收。回收机制会删除已经不再需要的旧版本数据,从而释放空间。 + +MVCC 通过创建数据的多个版本和使用快照读取来实现并发控制。读操作使用旧版本数据的快照,写操作创建新版本,并确保原始版本仍然可用。这样,不同的事务可以在一定程度上并发执行,而不会相互干扰,从而提高了数据库的并发性能和数据一致性。 + ## 一致性非锁定读和锁定读 ### 一致性非锁定读 @@ -224,3 +255,5 @@ private: - [Innodb 中的事务隔离级别和锁的关系](https://tech.meituan.com/2014/08/20/innodb-lock.html) - [MySQL 事务与 MVCC 如何实现的隔离级别](https://blog.csdn.net/qq_35190492/article/details/109044141) - [InnoDB 事务分析-MVCC](https://leviathan.vip/2019/03/20/InnoDB%E7%9A%84%E4%BA%8B%E5%8A%A1%E5%88%86%E6%9E%90-MVCC/) + + diff --git a/docs/database/mysql/mysql-auto-increment-primary-key-continuous.md b/docs/database/mysql/mysql-auto-increment-primary-key-continuous.md index 92c0ad13d93..ec900188610 100644 --- a/docs/database/mysql/mysql-auto-increment-primary-key-continuous.md +++ b/docs/database/mysql/mysql-auto-increment-primary-key-continuous.md @@ -8,7 +8,7 @@ tag: > 作者:飞天小牛肉 > -> 原文:https://mp.weixin.qq.com/s/qci10h9rJx_COZbHV3aygQ +> 原文: 众所周知,自增主键可以让聚集索引尽量地保持递增顺序插入,避免了随机查询,从而提高了查询效率。 @@ -16,17 +16,17 @@ tag: 下面举个例子来看下,如下所示创建一张表: -![](https://p3-juejin.byteimg.com/tos-cn-i-k3u1fbpfcp/3e6b80ba50cb425386b80924e3da0d23~tplv-k3u1fbpfcp-zoom-1.image) +![](https://oss.javaguide.cn/p3-juejin/3e6b80ba50cb425386b80924e3da0d23~tplv-k3u1fbpfcp-zoom-1.png) ## 自增值保存在哪里? 使用 `insert into test_pk values(null, 1, 1)` 插入一行数据,再执行 `show create table` 命令来看一下表的结构定义: -![](https://p3-juejin.byteimg.com/tos-cn-i-k3u1fbpfcp/c17e46230bd34150966f0d86b2ad5e91~tplv-k3u1fbpfcp-zoom-1.image) +![](https://oss.javaguide.cn/p3-juejin/c17e46230bd34150966f0d86b2ad5e91~tplv-k3u1fbpfcp-zoom-1.png) 上述表的结构定义存放在后缀名为 `.frm` 的本地文件中,在 MySQL 安装目录下的 data 文件夹下可以找到这个 `.frm` 文件: -![](https://p3-juejin.byteimg.com/tos-cn-i-k3u1fbpfcp/3ec0514dd7be423d80b9e7f2d52f5902~tplv-k3u1fbpfcp-zoom-1.image) +![](https://oss.javaguide.cn/p3-juejin/3ec0514dd7be423d80b9e7f2d52f5902~tplv-k3u1fbpfcp-zoom-1.png) 从上述表结构可以看到,表定义里面出现了一个 `AUTO_INCREMENT=2`,表示下一次插入数据时,如果需要自动生成自增值,会生成 id = 2。 @@ -38,13 +38,13 @@ tag: 举个例子:我们现在表里当前数据行里最大的 id 是 1,AUTO_INCREMENT=2,对吧。这时候,我们删除 id=1 的行,AUTO_INCREMENT 还是 2。 -![](https://p3-juejin.byteimg.com/tos-cn-i-k3u1fbpfcp/61b8dc9155624044a86d91c368b20059~tplv-k3u1fbpfcp-zoom-1.image) +![](https://oss.javaguide.cn/p3-juejin/61b8dc9155624044a86d91c368b20059~tplv-k3u1fbpfcp-zoom-1.png) 但如果马上重启 MySQL 实例,重启后这个表的 AUTO_INCREMENT 就会变成 1。 也就是说,MySQL 重启可能会修改一个表的 AUTO_INCREMENT 的值。 -![](https://p3-juejin.byteimg.com/tos-cn-i-k3u1fbpfcp/27fdb15375664249a31f88b64e6e5e66~tplv-k3u1fbpfcp-zoom-1.image) +![](https://oss.javaguide.cn/p3-juejin/27fdb15375664249a31f88b64e6e5e66~tplv-k3u1fbpfcp-zoom-1.png) -![](https://p3-juejin.byteimg.com/tos-cn-i-k3u1fbpfcp/dee15f93e65d44d384345a03404f3481~tplv-k3u1fbpfcp-zoom-1.image) +![](https://oss.javaguide.cn/p3-juejin/dee15f93e65d44d384345a03404f3481~tplv-k3u1fbpfcp-zoom-1.png) 以上,是在我本地 MySQL 5.x 版本的实验,实际上,**到了 MySQL 8.0 版本后,自增值的变更记录被放在了 redo log 中,提供了自增值持久化的能力** ,也就是实现了“如果发生重启,表的自增值可以根据 redo log 恢复为 MySQL 重启前的值” @@ -86,11 +86,11 @@ tag: 举个例子,我们现在往表里插入一条 (null,1,1) 的记录,生成的主键是 1,AUTO_INCREMENT= 2,对吧 -![](https://p3-juejin.byteimg.com/tos-cn-i-k3u1fbpfcp/c22c4f2cea234c7ea496025eb826c3bc~tplv-k3u1fbpfcp-zoom-1.image) +![](https://oss.javaguide.cn/p3-juejin/c22c4f2cea234c7ea496025eb826c3bc~tplv-k3u1fbpfcp-zoom-1.png) 这时我再执行一条插入 `(null,1,1)` 的命令,很显然会报错 `Duplicate entry`,因为我们设置了一个唯一索引字段 `a`: -![](https://p3-juejin.byteimg.com/tos-cn-i-k3u1fbpfcp/c0325e31398d4fa6bb1cbe08ef797b7f~tplv-k3u1fbpfcp-zoom-1.image) +![](https://oss.javaguide.cn/p3-juejin/c0325e31398d4fa6bb1cbe08ef797b7f~tplv-k3u1fbpfcp-zoom-1.png) 但是,你会惊奇的发现,虽然插入失败了,但自增值仍然从 2 增加到了 3! @@ -119,27 +119,27 @@ tag: 我们现在表里有一行 `(1,1,1)` 的记录,AUTO_INCREMENT = 3: -![](https://p3-juejin.byteimg.com/tos-cn-i-k3u1fbpfcp/6220fcf7dac54299863e43b6fb97de3e~tplv-k3u1fbpfcp-zoom-1.image) +![](https://oss.javaguide.cn/p3-juejin/6220fcf7dac54299863e43b6fb97de3e~tplv-k3u1fbpfcp-zoom-1.png) 我们先插入一行数据 `(null, 2, 2)`,也就是 (3, 2, 2) 嘛,并且 AUTO_INCREMENT 变为 4: -![](https://p3-juejin.byteimg.com/tos-cn-i-k3u1fbpfcp/3f02d46437d643c3b3d9f44a004ab269~tplv-k3u1fbpfcp-zoom-1.image) +![](https://oss.javaguide.cn/p3-juejin/3f02d46437d643c3b3d9f44a004ab269~tplv-k3u1fbpfcp-zoom-1.png) 再去执行这样一段 SQL: -![](https://p3-juejin.byteimg.com/tos-cn-i-k3u1fbpfcp/faf5ce4a2920469cae697f845be717f5~tplv-k3u1fbpfcp-zoom-1.image) +![](https://oss.javaguide.cn/p3-juejin/faf5ce4a2920469cae697f845be717f5~tplv-k3u1fbpfcp-zoom-1.png) 虽然我们插入了一条 (null, 3, 3) 记录,但是使用 rollback 进行回滚了,所以数据库中是没有这条记录的: -![](https://p3-juejin.byteimg.com/tos-cn-i-k3u1fbpfcp/6cb4c02722674dd399939d3d03a431c1~tplv-k3u1fbpfcp-zoom-1.image) +![](https://oss.javaguide.cn/p3-juejin/6cb4c02722674dd399939d3d03a431c1~tplv-k3u1fbpfcp-zoom-1.png) 在这种事务回滚的情况下,自增值并没有同样发生回滚!如下图所示,自增值仍然固执地从 4 增加到了 5: -![](https://p3-juejin.byteimg.com/tos-cn-i-k3u1fbpfcp/e6eea1c927424ac7bda34a511ca521ae~tplv-k3u1fbpfcp-zoom-1.image) +![](https://oss.javaguide.cn/p3-juejin/e6eea1c927424ac7bda34a511ca521ae~tplv-k3u1fbpfcp-zoom-1.png) 所以这时候我们再去插入一条数据(null, 3, 3)的时候,主键 id 就会被自动赋为 `5` 了: -![](https://p3-juejin.byteimg.com/tos-cn-i-k3u1fbpfcp/80da69dd13b543c4a32d6ed832a3c568~tplv-k3u1fbpfcp-zoom-1.image) +![](https://oss.javaguide.cn/p3-juejin/80da69dd13b543c4a32d6ed832a3c568~tplv-k3u1fbpfcp-zoom-1.png) 那么,为什么在出现唯一键冲突或者回滚的时候,MySQL 没有把表的自增值改回去呢?回退回去的话不就不会发生自增 id 不连续了吗? @@ -153,7 +153,7 @@ tag: 2. 事务 B 正确提交了,但事务 A 出现了唯一键冲突,也就是 id = 1 的那行记录插入失败了,那如果允许事务 A 把自增 id 回退,也就是把表的当前自增值改回 1,那么就会出现这样的情况:表里面已经有 id = 2 的行,而当前的自增 id 值是 1。 3. 接下来,继续执行的其他事务就会申请到 id=2。这时,就会出现插入语句报错“主键冲突”。 -![](https://p3-juejin.byteimg.com/tos-cn-i-k3u1fbpfcp/5f26f02e60f643c9a7cab88a9f1bdce9~tplv-k3u1fbpfcp-zoom-1.image) +![](https://oss.javaguide.cn/p3-juejin/5f26f02e60f643c9a7cab88a9f1bdce9~tplv-k3u1fbpfcp-zoom-1.png) 而为了解决这个主键冲突,有两种方法: @@ -177,29 +177,29 @@ tag: 注意,这里说的批量插入数据,不是在普通的 insert 语句里面包含多个 value 值!!!,因为这类语句在申请自增 id 的时候,是可以精确计算出需要多少个 id 的,然后一次性申请,申请完成后锁就可以释放了。 -而对于 `insert … select`、replace … select 和 load data 这种类型的语句来说,MySQL 并不知道到底需要申请多少 id,所以就采用了这种批量申请的策略,毕竟一个一个申请的话实在太慢了。 +而对于 `insert … select`、replace …… select 和 load data 这种类型的语句来说,MySQL 并不知道到底需要申请多少 id,所以就采用了这种批量申请的策略,毕竟一个一个申请的话实在太慢了。 举个例子,假设我们现在这个表有下面这些数据: -![](https://p3-juejin.byteimg.com/tos-cn-i-k3u1fbpfcp/6453cfc107f94e3bb86c95072d443472~tplv-k3u1fbpfcp-zoom-1.image) +![](https://oss.javaguide.cn/p3-juejin/6453cfc107f94e3bb86c95072d443472~tplv-k3u1fbpfcp-zoom-1.png) 我们创建一个和当前表 `test_pk` 有相同结构定义的表 `test_pk2`: -![](https://p3-juejin.byteimg.com/tos-cn-i-k3u1fbpfcp/45248a6dc34f431bba14d434bee2c79e~tplv-k3u1fbpfcp-zoom-1.image) +![](https://oss.javaguide.cn/p3-juejin/45248a6dc34f431bba14d434bee2c79e~tplv-k3u1fbpfcp-zoom-1.png) 然后使用 `insert...select` 往 `teset_pk2` 表中批量插入数据: -![](https://p3-juejin.byteimg.com/tos-cn-i-k3u1fbpfcp/c1b061e86bae484694d15ceb703b10ca~tplv-k3u1fbpfcp-zoom-1.image) +![](https://oss.javaguide.cn/p3-juejin/c1b061e86bae484694d15ceb703b10ca~tplv-k3u1fbpfcp-zoom-1.png) 可以看到,成功导入了数据。 再来看下 `test_pk2` 的自增值是多少: -![](https://p3-juejin.byteimg.com/tos-cn-i-k3u1fbpfcp/0ff9039366154c738331d64ebaf88d3b~tplv-k3u1fbpfcp-zoom-1.image) +![](https://oss.javaguide.cn/p3-juejin/0ff9039366154c738331d64ebaf88d3b~tplv-k3u1fbpfcp-zoom-1.png) 如上分析,是 8 而不是 6 -具体来说,insert…select 实际上往表中插入了 5 行数据 (1 1)(2 2)(3 3)(4 4)(5 5)。但是,这五行数据是分三次申请的自增 id,结合批量申请策略,每次申请到的自增 id 个数都是上一次的两倍,所以: +具体来说,insert……select 实际上往表中插入了 5 行数据 (1 1)(2 2)(3 3)(4 4)(5 5)。但是,这五行数据是分三次申请的自增 id,结合批量申请策略,每次申请到的自增 id 个数都是上一次的两倍,所以: - 第一次申请到了一个 id:id=1 - 第二次被分配了两个 id:id=2 和 id=3 @@ -207,7 +207,7 @@ tag: 由于这条语句实际只用上了 5 个 id,所以 id=6 和 id=7 就被浪费掉了。之后,再执行 `insert into test_pk2 values(null,6,6)`,实际上插入的数据就是(8,6,6): -![](https://p3-juejin.byteimg.com/tos-cn-i-k3u1fbpfcp/51612fbac3804cff8c5157df21d6e355~tplv-k3u1fbpfcp-zoom-1.image) +![](https://oss.javaguide.cn/p3-juejin/51612fbac3804cff8c5157df21d6e355~tplv-k3u1fbpfcp-zoom-1.png) ## 小结 @@ -217,3 +217,5 @@ tag: 2. 唯一键冲突 3. 事务回滚 4. 批量插入(如 `insert...select` 语句) + + 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 584f3938217..38c333b3308 100644 --- a/docs/database/mysql/mysql-high-performance-optimization-specification-recommendations.md +++ b/docs/database/mysql/mysql-high-performance-optimization-specification-recommendations.md @@ -7,21 +7,21 @@ tag: > 作者: 听风 原文地址: 。 > -> JavaGuide 已获得作者授权,并对原文内容进行了完善。 +> JavaGuide 已获得作者授权,并对原文内容进行了完善补充。 ## 数据库命名规范 -- 所有数据库对象名称必须使用小写字母并用下划线分割 -- 所有数据库对象名称禁止使用 MySQL 保留关键字(如果表名中包含关键字查询时,需要将其用单引号括起来) -- 数据库对象的命名要能做到见名识意,并且最后不要超过 32 个字符 -- 临时库表必须以 `tmp_` 为前缀并以日期为后缀,备份表必须以 `bak_` 为前缀并以日期 (时间戳) 为后缀 -- 所有存储相同数据的列名和列类型必须一致(一般作为关联列,如果查询时关联列类型不一致会自动进行数据类型隐式转换,会造成列上的索引失效,导致查询效率降低) +- 所有数据库对象名称必须使用小写字母并用下划线分割。 +- 所有数据库对象名称禁止使用 MySQL 保留关键字(如果表名中包含关键字查询时,需要将其用单引号括起来)。 +- 数据库对象的命名要能做到见名识义,并且最好不要超过 32 个字符。 +- 临时库表必须以 `tmp_` 为前缀并以日期为后缀,备份表必须以 `bak_` 为前缀并以日期 (时间戳) 为后缀。 +- 所有存储相同数据的列名和列类型必须一致(一般作为关联列,如果查询时关联列类型不一致会自动进行数据类型隐式转换,会造成列上的索引失效,导致查询效率降低)。 ## 数据库基本设计规范 ### 所有表必须使用 InnoDB 存储引擎 -没有特殊要求(即 InnoDB 无法满足的功能如:列存储,存储空间数据等)的情况下,所有表必须使用 InnoDB 存储引擎(MySQL5.5 之前默认使用 Myisam,5.6 以后默认的为 InnoDB)。 +没有特殊要求(即 InnoDB 无法满足的功能如:列存储、存储空间数据等)的情况下,所有表必须使用 InnoDB 存储引擎(MySQL5.5 之前默认使用 MyISAM,5.6 以后默认的为 InnoDB)。 InnoDB 支持事务,支持行级锁,更好的恢复性,高并发下性能更好。 @@ -29,26 +29,23 @@ InnoDB 支持事务,支持行级锁,更好的恢复性,高并发下性能 兼容性更好,统一字符集可以避免由于字符集转换产生的乱码,不同的字符集进行比较前需要进行转换会造成索引失效,如果数据库中有存储 emoji 表情的需要,字符集需要采用 utf8mb4 字符集。 -参考文章: - -- [MySQL 字符集不一致导致索引失效的一个真实案例](https://blog.csdn.net/horses/article/details/107243447) -- [MySQL 字符集详解](../character-set.md) +推荐阅读一下我写的这篇文章:[MySQL 字符集详解](../character-set.md) 。 ### 所有表和字段都需要添加注释 -使用 comment 从句添加表和列的备注,从一开始就进行数据字典的维护 +使用 comment 从句添加表和列的备注,从一开始就进行数据字典的维护。 ### 尽量控制单表数据量的大小,建议控制在 500 万以内 500 万并不是 MySQL 数据库的限制,过大会造成修改表结构,备份,恢复都会有很大的问题。 -可以用历史数据归档(应用于日志数据),分库分表(应用于业务数据)等手段来控制数据量大小 +可以用历史数据归档(应用于日志数据),分库分表(应用于业务数据)等手段来控制数据量大小。 ### 谨慎使用 MySQL 分区表 -分区表在物理上表现为多个文件,在逻辑上表现为一个表; +分区表在物理上表现为多个文件,在逻辑上表现为一个表。 -谨慎选择分区键,跨分区查询效率可能更低; +谨慎选择分区键,跨分区查询效率可能更低。 建议采用物理分表的方式管理大数据。 @@ -74,7 +71,7 @@ InnoDB 支持事务,支持行级锁,更好的恢复性,高并发下性能 ### 禁止在线上做数据库压力测试 -### 禁止从开发环境,测试环境直接连接生产环境数据库 +### 禁止从开发环境、测试环境直接连接生产环境数据库 安全隐患极大,要对生产环境抱有敬畏之心! @@ -82,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 @@ -106,7 +103,7 @@ UNSIGNED INT 0~4294967295 **c.小数值类型(比如年龄、状态表示如 0/1)优先使用 TINYINT 类型。** -### 避免使用 TEXT,BLOB 数据类型,最常见的 TEXT 类型可以存储 64k 的数据 +### 避免使用 TEXT、BLOB 数据类型,最常见的 TEXT 类型可以存储 64k 的数据 **a. 建议把 BLOB 或是 TEXT 列分离到单独的扩展表中。** @@ -116,44 +113,45 @@ 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/) 。 -### 使用 TIMESTAMP(4 个字节) 或 DATETIME 类型 (8 个字节) 存储时间 - -TIMESTAMP 存储的时间范围 1970-01-01 00:00:01 ~ 2038-01-19-03:14:07 +### 一定不要用字符串存储日期 -TIMESTAMP 占用 4 字节和 INT 相同,但比 INT 可读性高 +对于日期类型来说,一定不要用字符串存储日期。可以考虑 DATETIME、TIMESTAMP 和数值型时间戳。 -超出 TIMESTAMP 取值范围的使用 DATETIME 类型存储 +这三种种方式都有各自的优势,根据实际场景选择最合适的才是王道。下面再对这三种方式做一个简单的对比,以供大家在实际开发中选择正确的存放时间的数据类型: -**经常会有人用字符串存储日期型的数据(不正确的做法)** +| 类型 | 存储空间 | 日期格式 | 日期范围 | 是否带时区信息 | +| ------------ | -------- | ------------------------------ | ------------------------------------------------------------ | -------------- | +| 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 之后的时间 | 否 | -- 缺点 1:无法用日期函数进行计算和比较 -- 缺点 2:用字符串存储日期要占用更多的空间 +MySQL 时间类型选择的详细介绍请看这篇:[MySQL 时间类型数据存储建议](https://javaguide.cn/database/mysql/some-thoughts-on-database-storage-time.html)。 ### 同财务相关的金额类数据必须使用 decimal 类型 -- **非精准浮点**:float,double +- **非精准浮点**:float、double - **精准浮点**:decimal -decimal 类型为精准浮点数,在计算时不会丢失精度。占用空间由定义的宽度决定,每 4 个字节可以存储 9 位数字,并且小数点要占用一个字节。并且,decimal 可用于存储比 bigint 更大的整型数据 +decimal 类型为精准浮点数,在计算时不会丢失精度。占用空间由定义的宽度决定,每 4 个字节可以存储 9 位数字,并且小数点要占用一个字节。并且,decimal 可用于存储比 bigint 更大的整型数据。 不过, 由于 decimal 需要额外的空间和计算开销,应该尽量只在需要对数据进行精确计算时才使用 decimal 。 @@ -163,13 +161,13 @@ decimal 类型为精准浮点数,在计算时不会丢失精度。占用空间 ## 索引设计规范 -### 限制每张表上的索引数量,建议单张表索引不超过 5 个 +### 限制每张表上的索引数量,建议单张表索引不超过 5 个 -索引并不是越多越好!索引可以提高效率同样可以降低效率。 +索引并不是越多越好!索引可以提高效率,同样可以降低效率。 索引可以增加查询效率,但同样也会降低插入和更新的效率,甚至有些情况下会降低查询效率。 -因为 MySQL 优化器在选择如何优化查询时,会根据统一信息,对每一个可以用到的索引来进行评估,以生成出一个最好的执行计划,如果同时有很多个索引都可以用于查询,就会增加 MySQL 优化器生成执行计划的时间,同样会降低查询性能。 +因为 MySQL 优化器在选择如何优化查询时,会根据统一信息,对每一个可以用到的索引来进行评估,以生成出一个最好的执行计划。如果同时有很多个索引都可以用于查询,就会增加 MySQL 优化器生成执行计划的时间,同样会降低查询性能。 ### 禁止使用全文索引 @@ -177,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,增加查询性能,索引能过滤出越少的数据,则从磁盘中读入的数据也就越少。 -- 区分度最高的放在联合索引的最左侧(区分度=列中不同值的数量/列的总行数) -- 尽量把字段长度小的列放在联合索引的最左侧(因为字段长度越小,一页能存储的数据量越大,IO 性能也就越好) -- 使用最频繁的列放到联合索引的左侧(这样可以比较少的建立一些索引) +- **区分度最高的列放在联合索引的最左侧**:这是最重要的原则。区分度越高,通过索引筛选出的数据就越少,I/O 操作也就越少。计算区分度的方法是 `count(distinct column) / count(*)`。 +- **最频繁使用的列放在联合索引的左侧**:这符合最左前缀匹配原则。将最常用的查询条件列放在最左侧,可以最大程度地利用索引。 +- **字段长度**:字段长度对联合索引非叶子节点的影响很小,因为它存储了所有联合索引字段的值。字段长度主要影响主键和包含在其他索引中的字段的存储空间,以及这些索引的叶子节点的大小。因此,在选择联合索引列的顺序时,字段长度的优先级最低。对于主键和包含在其他索引中的字段,选择较短的字段长度可以节省存储空间和提高 I/O 性能。 ### 避免建立冗余索引和重复索引(增加了查询优化器生成执行计划的时间) -- 重复索引示例:primary key(id)、index(id)、unique index(id) -- 冗余索引示例:index(a,b,c)、index(a,b)、index(a) +- 重复索引示例:primary key(id)、index(id)、unique index(id)。 +- 冗余索引示例:index(a,b,c)、index(a,b)、index(a)。 -### 对于频繁的查询优先考虑使用覆盖索引 +### 对于频繁的查询,优先考虑使用覆盖索引 -> 覆盖索引:就是包含了所有查询字段 (where,select,order by,group by 包含的字段) 的索引 +> 覆盖索引:就是包含了所有查询字段 (where、select、order by、group by 包含的字段) 的索引 -**覆盖索引的好处:** +**覆盖索引的好处**: -- **避免 InnoDB 表进行索引的二次查询:** InnoDB 是以聚集索引的顺序来存储的,对于 InnoDB 来说,二级索引在叶子节点中所保存的是行的主键信息,如果是用二级索引查询数据的话,在查找到相应的键值后,还要通过主键进行二次查询才能获取我们真实所需要的数据。而在覆盖索引中,二级索引的键值中可以获取所有的数据,避免了对主键的二次查询 ,减少了 IO 操作,提升了查询效率。 -- **可以把随机 IO 变成顺序 IO 加快查询效率:** 由于覆盖索引是按键值的顺序存储的,对于 IO 密集型的范围查找来说,对比随机从磁盘读取每一行的数据 IO 要少的多,因此利用覆盖索引在访问时也可以把磁盘的随机读取的 IO 转变成索引查找的顺序 IO。 +- **避免 InnoDB 表进行索引的二次查询,也就是回表操作**:InnoDB 是以聚集索引的顺序来存储的,对于 InnoDB 来说,二级索引在叶子节点中所保存的是行的主键信息,如果是用二级索引查询数据的话,在查找到相应的键值后,还要通过主键进行二次查询才能获取我们真实所需要的数据。而在覆盖索引中,二级索引的键值中可以获取所有的数据,避免了对主键的二次查询(回表),减少了 IO 操作,提升了查询效率。 +- **可以把随机 IO 变成顺序 IO 加快查询效率**:由于覆盖索引是按键值的顺序存储的,对于 IO 密集型的范围查找来说,对比随机从磁盘读取每一行的数据 IO 要少的多,因此利用覆盖索引在访问时也可以把磁盘的随机读取的 IO 转变成索引查找的顺序 IO。 --- @@ -224,19 +222,23 @@ InnoDB 是按照主键索引的顺序来组织表的 **尽量避免使用外键约束** -- 不建议使用外键约束(foreign key),但一定要在表与表之间的关联键上建立索引 -- 外键可用于保证数据的参照完整性,但建议在业务端实现 -- 外键会影响父表和子表的写操作从而降低性能 +- 不建议使用外键约束(foreign key),但一定要在表与表之间的关联键上建立索引。 +- 外键可用于保证数据的参照完整性,但建议在业务端实现。 +- 外键会影响父表和子表的写操作从而降低性能。 ## 数据库 SQL 开发规范 +### 尽量不在数据库做运算,复杂运算需移到业务应用里完成 + +尽量不在数据库做运算,复杂运算需移到业务应用里完成。这样可以避免数据库的负担过重,影响数据库的性能和稳定性。数据库的主要作用是存储和管理数据,而不是处理数据。 + ### 优化对性能影响较大的 SQL 语句 -要找到最需要优化的 SQL 语句。要么是使用最频繁的语句,要么是优化后提高最明显的语句,可以通过查询 MySQL 的慢查询日志来发现需要进行优化的 SQL 语句; +要找到最需要优化的 SQL 语句。要么是使用最频繁的语句,要么是优化后提高最明显的语句,可以通过查询 MySQL 的慢查询日志来发现需要进行优化的 SQL 语句。 ### 充分利用表上已经存在的索引 -避免使用双%号的查询条件。如:`a like '%123%'`,(如果无前置%,只有后置%,是可以用到列上的索引的) +避免使用双%号的查询条件。如:`a like '%123%'`(如果无前置%,只有后置%,是可以用到列上的索引的)。 一个 SQL 只能利用到复合索引中的一列进行范围查询。如:有 a,b,c 列的联合索引,在查询条件中有 a 列的范围查询,则在 b,c 列上的索引将不会被用到。 @@ -244,19 +246,20 @@ InnoDB 是按照主键索引的顺序来组织表的 ### 禁止使用 SELECT \* 必须使用 SELECT <字段列表> 查询 -- `SELECT *` 消耗更多的 CPU 和 IO 以网络带宽资源 -- `SELECT *` 无法使用覆盖索引 -- `SELECT <字段列表>` 可减少表结构变更带来的影响 +- `SELECT *` 会消耗更多的 CPU。 +- `SELECT *` 无用字段增加网络带宽资源消耗,增加数据传输时间,尤其是大字段(如 varchar、blob、text)。 +- `SELECT *` 无法使用 MySQL 优化器覆盖索引的优化(基于 MySQL 优化器的“覆盖索引”策略又是速度极快、效率极高、业界极为推荐的查询优化方式)。 +- `SELECT <字段列表>` 可减少表结构变更带来的影响。 ### 禁止使用不含字段列表的 INSERT 语句 -如: +**不推荐**: ```sql insert into t values ('a','b','c'); ``` -应使用: +**推荐**: ```sql insert into t(c1,c2,c3) values ('a','b','c'); @@ -270,7 +273,7 @@ insert into t(c1,c2,c3) values ('a','b','c'); ### 避免数据类型的隐式转换 -隐式转换会导致索引失效如: +隐式转换会导致索引失效,如: ```sql select name,phone from customer where id = '111'; @@ -280,9 +283,9 @@ select name,phone from customer where id = '111'; ### 避免使用子查询,可以把子查询优化为 join 操作 -通常子查询在 in 子句中,且子查询中为简单 SQL(不包含 union、group by、order by、limit 从句) 时,才可以把子查询转化为关联查询进行优化。 +通常子查询在 in 子句中,且子查询中为简单 SQL(不包含 union、group by、order by、limit 从句) 时,才可以把子查询转化为关联查询进行优化。 -**子查询性能差的原因:** 子查询的结果集无法使用索引,通常子查询的结果集会被存储到临时表中,不论是内存临时表还是磁盘临时表都不会存在索引,所以查询性能会受到一定的影响。特别是对于返回结果集比较大的子查询,其对查询性能的影响也就越大。由于子查询会产生大量的临时表也没有索引,所以会消耗过多的 CPU 和 IO 资源,产生大量的慢查询。 +**子查询性能差的原因**:子查询的结果集无法使用索引,通常子查询的结果集会被存储到临时表中,不论是内存临时表还是磁盘临时表都不会存在索引,所以查询性能会受到一定的影响。特别是对于返回结果集比较大的子查询,其对查询性能的影响也就越大。由于子查询会产生大量的临时表也没有索引,所以会消耗过多的 CPU 和 IO 资源,产生大量的慢查询。 ### 避免使用 JOIN 关联太多的表 @@ -290,7 +293,7 @@ select name,phone from customer where id = '111'; 在 MySQL 中,对于同一个 SQL 多关联(join)一个表,就会多分配一个关联缓存,如果在一个 SQL 中关联的表越多,所占用的内存也就越大。 -如果程序中大量的使用了多表关联的操作,同时 join_buffer_size 设置的也不合理的情况下,就容易造成服务器内存溢出的情况,就会影响到服务器数据库性能的稳定性。 +如果程序中大量地使用了多表关联的操作,同时 join_buffer_size 设置得也不合理,就容易造成服务器内存溢出的情况,就会影响到服务器数据库性能的稳定性。 同时对于关联操作来说,会产生临时表操作,影响查询效率,MySQL 最多允许关联 61 个表,建议不超过 5 个。 @@ -300,25 +303,25 @@ select name,phone from customer where id = '111'; ### 对应同一列进行 or 判断时,使用 in 代替 or -in 的值不要超过 500 个,in 操作可以更有效的利用索引,or 大多数情况下很少能利用到索引。 +in 的值不要超过 500 个。in 操作可以更有效的利用索引,or 大多数情况下很少能利用到索引。 ### 禁止使用 order by rand() 进行随机排序 -order by rand() 会把表中所有符合条件的数据装载到内存中,然后在内存中对所有数据根据随机生成的值进行排序,并且可能会对每一行都生成一个随机值,如果满足条件的数据集非常大,就会消耗大量的 CPU 和 IO 及内存资源。 +order by rand() 会把表中所有符合条件的数据装载到内存中,然后在内存中对所有数据根据随机生成的值进行排序,并且可能会对每一行都生成一个随机值。如果满足条件的数据集非常大,就会消耗大量的 CPU 和 IO 及内存资源。 推荐在程序中获取一个随机值,然后从数据库中获取数据的方式。 ### WHERE 从句中禁止对列进行函数转换和计算 -对列进行函数转换或计算时会导致无法使用索引 +对列进行函数转换或计算时会导致无法使用索引。 -**不推荐:** +**不推荐**: ```sql where date(create_time)='20190101' ``` -**推荐:** +**推荐**: ```sql where create_time >= '20190101' and create_time < '20190102' @@ -326,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 修改表结构 -- 避免大表修改产生的主从延迟 -- 避免在对表字段进行修改时进行锁表 +- 避免大表修改产生的主从延迟。 +- 避免在对表字段进行修改时进行锁表。 对大表数据结构的修改一定要谨慎,会造成严重的锁表操作,尤其是生产环境,是不能容忍的。 @@ -370,10 +373,17 @@ pt-online-schema-change 它会首先建立一个与原表结构相同的新表 ### 禁止为程序使用的账号赋予 super 权限 -- 当达到最大连接数限制时,还运行 1 个有 super 权限的用户连接 -- super 权限只能留给 DBA 处理问题的账号使用 +- 当达到最大连接数限制时,还运行 1 个有 super 权限的用户连接。 +- super 权限只能留给 DBA 处理问题的账号使用。 + +### 对于程序连接数据库账号,遵循权限最小原则 + +- 程序使用数据库账号只能在一个 DB 下使用,不准跨库。 +- 程序使用的账号原则上不准有 drop 权限。 + +## 推荐阅读 -### 对于程序连接数据库账号,遵循权限最小原则 +- [技术同学必会的 MySQL 设计规约,都是惨痛的教训 - 阿里开发者](https://mp.weixin.qq.com/s/XC8e5iuQtfsrEOERffEZ-Q) +- [聊聊数据库建表的 15 个小技巧](https://mp.weixin.qq.com/s/NM-aHaW6TXrnO6la6Jfl5A) -- 程序使用数据库账号只能在一个 DB 下使用,不准跨库 -- 程序使用的账号原则上不准有 drop 权限 + diff --git a/docs/database/mysql/mysql-index.md b/docs/database/mysql/mysql-index.md index 38ebdd0f373..a21d133feea 100644 --- a/docs/database/mysql/mysql-index.md +++ b/docs/database/mysql/mysql-index.md @@ -5,7 +5,7 @@ tag: - MySQL --- -> 感谢[WT-AHA](https://github.com/WT-AHA)对本文的完善,相关 PR:https://github.com/Snailclimb/JavaGuide/pull/1648 。 +> 感谢[WT-AHA](https://github.com/WT-AHA)对本文的完善,相关 PR: 。 但凡经历过几场面试的小伙伴,应该都清楚,数据库索引这个知识点在面试中出现的频率高到离谱。 @@ -15,20 +15,20 @@ tag: **索引是一种用于快速查询和检索数据的数据结构,其本质可以看成是一种排序好的数据结构。** -索引的作用就相当于书的目录。打个比方: 我们在查字典的时候,如果没有目录,那我们就只能一页一页的去找我们需要查的那个字,速度很慢。如果有目录了,我们只需要先去目录里查找字的位置,然后直接翻到那一页就行了。 +索引的作用就相当于书的目录。打个比方:我们在查字典的时候,如果没有目录,那我们就只能一页一页地去找我们需要查的那个字,速度很慢;如果有目录了,我们只需要先去目录里查找字的位置,然后直接翻到那一页就行了。 -索引底层数据结构存在很多种类型,常见的索引结构有: B 树, B+树 和 Hash、红黑树。在 MySQL 中,无论是 Innodb 还是 MyIsam,都使用了 B+树作为索引结构。 +索引底层数据结构存在很多种类型,常见的索引结构有:B 树、 B+ 树 和 Hash、红黑树。在 MySQL 中,无论是 Innodb 还是 MyISAM,都使用了 B+ 树作为索引结构。 ## 索引的优缺点 **优点**: -- 使用索引可以大大加快 数据的检索速度(大大减少检索的数据量), 这也是创建索引的最主要的原因。 +- 使用索引可以大大加快数据的检索速度(大大减少检索的数据量),减少 IO 次数,这也是创建索引的最主要的原因。 - 通过创建唯一性索引,可以保证数据库表中每一行数据的唯一性。 **缺点**: -- 创建索引和维护索引需要耗费许多时间。当对表中的数据进行增删改的时候,如果数据有索引,那么索引也需要动态的修改,会降低 SQL 执行效率。 +- 创建和维护索引需要耗费许多时间。当对表中的数据进行增删改的时候,如果数据有索引,那么索引也需要动态地修改,这会降低 SQL 执行效率。 - 索引需要使用物理文件存储,也会耗费一定空间。 但是,**使用索引一定能提高查询性能吗?** @@ -39,7 +39,7 @@ tag: ### Hash 表 -哈希表是键值对的集合,通过键(key)即可快速取出对应的值(value),因此哈希表可以快速检索数据(接近 O(1))。 +哈希表是键值对的集合,通过键(key)即可快速取出对应的值(value),因此哈希表可以快速检索数据(接近 O(1))。 **为何能够通过 key 快速取出 value 呢?** 原因在于 **哈希算法**(也叫散列算法)。通过哈希算法,我们可以快速找到 key 对应的 index,找到了 index 也就找到了对应的 value。 @@ -50,23 +50,25 @@ 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) 为了减少 Hash 冲突的发生,一个好的哈希函数应该“均匀地”将数据分布在整个可能的哈希值集合中。 +MySQL 的 InnoDB 存储引擎不直接支持常规的哈希索引,但是,InnoDB 存储引擎中存在一种特殊的“自适应哈希索引”(Adaptive Hash Index),自适应哈希索引并不是传统意义上的纯哈希索引,而是结合了 B+Tree 和哈希索引的特点,以便更好地适应实际应用中的数据访问模式和性能需求。自适应哈希索引的每个哈希桶实际上是一个小型的 B+Tree 结构。这个 B+Tree 结构可以存储多个键值对,而不仅仅是一个键。这有助于减少哈希冲突链的长度,提高了索引的效率。关于 Adaptive Hash Index 的详细介绍,可以查看 [MySQL 各种“Buffer”之 Adaptive Hash Index](https://mp.weixin.qq.com/s/ra4v1XR5pzSWc-qtGO-dBg) 这篇文章。 + 既然哈希表这么快,**为什么 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)是一种基于二叉树的数据结构,它具有以下特点: @@ -74,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) @@ -86,11 +88,11 @@ SELECT * FROM tb1 WHERE id < 500; AVL 树是计算机科学中最早被发明的自平衡二叉查找树,它的名称来自于发明者 G.M. Adelson-Velsky 和 E.M. Landis 的名字缩写。AVL 树的特点是保证任何节点的左右子树高度之差不超过 1,因此也被称为高度平衡二叉树,它的查找、插入和删除在平均和最坏情况下的时间复杂度都是 O(logn)。 -![AVL 树](https://oss.javaguide.cn/github/javaguide/cs-basics/data-structure/avl-tree.png) +![](https://oss.javaguide.cn/github/javaguide/cs-basics/data-structure/avl-tree.png) AVL 树采用了旋转操作来保持平衡。主要有四种旋转操作:LL 旋转、RR 旋转、LR 旋转和 RL 旋转。其中 LL 旋转和 RR 旋转分别用于处理左左和右右失衡,而 LR 旋转和 RL 旋转则用于处理左右和右左失衡。 -由于 AVL 树需要频繁地进行旋转操作来保持平衡,因此会有较大的计算开销进而降低了查询性能。并且, 在使用 AVL 树时,每个树节点仅存储一个数据,而每次进行磁盘 IO 时只能读取一个节点的数据,如果需要查询的数据分布在多个节点上,那么就需要进行多次磁盘 IO。 **磁盘 IO 是一项耗时的操作,在设计数据库索引时,我们需要优先考虑如何最大限度地减少磁盘 IO 操作的次数。** +由于 AVL 树需要频繁地进行旋转操作来保持平衡,因此会有较大的计算开销进而降低了数据库写操作的性能。并且, 在使用 AVL 树时,每个树节点仅存储一个数据,而每次进行磁盘 IO 时只能读取一个节点的数据,如果需要查询的数据分布在多个节点上,那么就需要进行多次磁盘 IO。**磁盘 IO 是一项耗时的操作,在设计数据库索引时,我们需要优先考虑如何最大限度地减少磁盘 IO 操作的次数。** 实际应用中,AVL 树使用的并不多。 @@ -102,7 +104,7 @@ AVL 树采用了旋转操作来保持平衡。主要有四种旋转操作:LL 2. 根节点总是黑色的; 3. 每个叶子节点都是黑色的空节点(NIL 节点); 4. 如果节点是红色的,则它的子节点必须是黑色的(反之不一定); -5. 从根节点到叶节点或空子节点的每条路径,必须包含相同数目的黑色节点(即相同的黑色高度)。 +5. 从任意节点到它的叶子节点或空子节点的每条路径,必须包含相同数目的黑色节点(即相同的黑色高度)。 ![红黑树](https://oss.javaguide.cn/github/javaguide/cs-basics/data-structure/red-black-tree.png) @@ -110,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 所在的节点即可取出数据;在根据辅助索引查找时,则需要先取出主键的值,再走一遍主索引。 因此,在设计表的时候,不建议使用过长的字段作为主键,也不建议使用非单调的字段作为主键,这样会造成主索引频繁分裂。 ## 索引类型总结 @@ -138,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 引擎,不管主键还是非主键,使用的都是非聚簇索引。 按照应用维度划分: @@ -152,7 +154,8 @@ B 树也称 B-树,全称为 **多路平衡查找树** ,B+ 树是 B 树的一 - 唯一索引:加速查询 + 列值唯一(可以有 NULL)。 - 覆盖索引:一个索引包含(或者说覆盖)所有需要查询的字段的值。 - 联合索引:多列值组成一个索引,专门用于组合搜索,其效率大于索引合并。 -- 全文索引:对文本的内容进行分词,进行搜索。目前只有 `CHAR`、`VARCHAR` ,`TEXT` 列上可以创建全文索引。一般不会使用,效率较低,通常使用搜索引擎如 ElasticSearch 代替。 +- 全文索引:对文本的内容进行分词,进行搜索。目前只有 `CHAR`、`VARCHAR`、`TEXT` 列上可以创建全文索引。一般不会使用,效率较低,通常使用搜索引擎如 ElasticSearch 代替。 +- 前缀索引:对文本的前几个字符创建索引,相比普通索引建立的数据更小,因为只取前几个字符。 MySQL 8.x 中实现的索引新特性: @@ -160,7 +163,7 @@ MySQL 8.x 中实现的索引新特性: - 降序索引:之前的版本就支持通过 desc 来指定索引为降序,但实际上创建的仍然是常规的升序索引。直到 MySQL 8.x 版本才开始真正支持降序索引。另外,在 MySQL 8.x 版本中,不再对 GROUP BY 语句进行隐式排序。 - 函数索引:从 MySQL 8.0.13 版本开始支持在索引中使用函数或者表达式的值,也就是在索引中可以包含函数或者表达式。 -## 主键索引(Primary Key) +## 主键索引(Primary Key) 数据表的主键列使用的就是主键索引。 @@ -172,19 +175,18 @@ MySQL 8.x 中实现的索引新特性: ## 二级索引 -**二级索引(Secondary Index)又称为辅助索引,是因为二级索引的叶子节点存储的数据是主键。也就是说,通过二级索引,可以定位主键的位置。** +二级索引(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) @@ -194,27 +196,27 @@ PS: 不懂的同学可以暂存疑,慢慢往下看,后面会有答案的, #### 聚簇索引介绍 -**聚簇索引(Clustered Index)即索引结构和数据一起存放的索引,并不是一种单独的索引类型。InnoDB 中的主键索引就属于聚簇索引。** +聚簇索引(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 引擎,不管主键还是非主键,使用的都是非聚簇索引。 非聚簇索引的叶子节点并不一定存放数据的指针,因为二级索引的叶子节点就存放的是主键,根据主键再回表查数据。 @@ -222,22 +224,22 @@ PS: 不懂的同学可以暂存疑,慢慢往下看,后面会有答案的, **优点**: -更新代价比聚簇索引要小 。非聚簇索引的更新代价就没有聚簇索引那么大了,非聚簇索引的叶子节点是不存放数据的 +更新代价比聚簇索引要小。非聚簇索引的更新代价就没有聚簇索引那么大了,非聚簇索引的叶子节点是不存放数据的。 **缺点**: -- **依赖于有序的数据**:跟聚簇索引一样,非聚簇索引也依赖于有序的数据 -- **可能会二次查询(回表)**:这应该是非聚簇索引最大的缺点了。 当查到索引对应的指针或主键后,可能还需要根据指针或主键再到数据文件或表中查询。 +- **依赖于有序的数据**:跟聚簇索引一样,非聚簇索引也依赖于有序的数据。 +- **可能会二次查询(回表)**:这应该是非聚簇索引最大的缺点了。当查到索引对应的指针或主键后,可能还需要根据指针或主键再到数据文件或表中查询。 -这是 MySQL 的表的文件截图: +这是 MySQL 的表的文件截图: ![MySQL 表的文件](https://oss.javaguide.cn/github/javaguide/database/mysql20210420165311654.png) -聚簇索引和非聚簇索引: +聚簇索引和非聚簇索引: ![聚簇索引和非聚簇索引](https://oss.javaguide.cn/github/javaguide/database/mysql20210420165326946.png) -#### 非聚簇索引一定回表查询吗(覆盖索引)? +#### 非聚簇索引一定回表查询吗(覆盖索引)? **非聚簇索引不一定回表查询。** @@ -249,7 +251,7 @@ PS: 不懂的同学可以暂存疑,慢慢往下看,后面会有答案的, 那么这个索引的 key 本身就是 name,查到对应的 name 直接返回就行了,无需回表查询。 -即使是 MYISAM 也是这样,虽然 MYISAM 的主键索引确实需要回表,因为它的主键索引的叶子节点存放的是指针。但是!**如果 SQL 查的就是主键呢?** +即使是 MyISAM 也是这样,虽然 MyISAM 的主键索引确实需要回表,因为它的主键索引的叶子节点存放的是指针。但是!**如果 SQL 查的就是主键呢?** ```sql SELECT id FROM table WHERE id=1; @@ -261,7 +263,9 @@ SELECT id FROM table WHERE id=1; ### 覆盖索引 -如果一个索引包含(或者说覆盖)所有需要查询的字段的值,我们就称之为 **覆盖索引(Covering Index)** 。我们知道在 InnoDB 存储引擎中,如果不是主键索引,叶子节点存储的是主键+列值。最终还是要“回表”,也就是要通过主键再查找一次,这样就会比较慢。而覆盖索引就是把要查询出的列和索引是对应的,不做回表操作! +如果一个索引包含(或者说覆盖)所有需要查询的字段的值,我们就称之为 **覆盖索引(Covering Index)**。 + +在 InnoDB 存储引擎中,非主键索引的叶子节点包含的是主键的值。这意味着,当使用非主键索引进行查询时,数据库会先找到对应的主键值,然后再通过主键索引来定位和检索完整的行数据。这个过程被称为“回表”。 **覆盖索引即需要查询的字段正好是索引的字段,那么直接根据该索引,就可以查到数据了,而无需回表查询。** @@ -272,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` ( @@ -312,10 +316,11 @@ CALL BatchinsertDataToCusOder(1, 1000000); # 插入100w+的随机数据 为了能够对这 100w 数据按照 `score` 进行排序,我们需要执行下面的 SQL 语句。 ```sql -SELECT `score`,`name` FROM `cus_order` ORDER BY `score` DESC;#降序排序 +#降序排序 +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) @@ -331,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)这篇文章。 @@ -347,19 +352,113 @@ ALTER TABLE `cus_order` ADD INDEX id_score_name(score, name); ### 最左前缀匹配原则 -最左前缀匹配原则指的是,在使用联合索引时,**MySQL** 会根据联合索引中的字段顺序,从左到右依次到查询条件中去匹配,如果查询条件中存在与联合索引中最左侧字段相匹配的字段,则就会使用该字段过滤一批数据,直至联合索引中全部字段匹配完成,或者在执行过程中遇到范围查询(如 **`>`**、**`<`**)才会停止匹配。对于 **`>=`**、**`<=`**、**`BETWEEN`**、**`like`** 前缀匹配的范围查询,并不会停止匹配。所以,我们在使用联合索引时,可以将区分度高的字段放在最左边,这也可以过滤更多数据。 +最左前缀匹配原则指的是在使用联合索引时,MySQL 会根据索引中的字段顺序,从左到右依次匹配查询条件中的字段。如果查询条件与索引中的最左侧字段相匹配,那么 MySQL 就会使用索引来过滤数据,这样可以提高查询效率。 + +最左匹配原则会一直向右匹配,直到遇到范围查询(如 >、<)为止。对于 >=、<=、BETWEEN 以及前缀匹配 LIKE 的范围查询,不会停止匹配(相关阅读:[联合索引的最左匹配原则全网都在说的一个错误结论](https://mp.weixin.qq.com/s/8qemhRg5MgXs1So5YCv0fQ))。 + +假设有一个联合索引 `(column1, column2, column3)`,其从左到右的所有前缀为 `(column1)`、`(column1, column2)`、`(column1, column2, column3)`(创建 1 个联合索引相当于创建了 3 个索引),包含这些列的所有查询都会走索引而不会全表扫描。 + +我们在使用联合索引时,可以将区分度高的字段放在最左边,这也可以过滤更多数据。 + +我们这里简单演示一下最左前缀匹配的效果。 + +1、创建一个名为 `student` 的表,这张表只有 `id`、`name`、`class` 这 3 个字段。 + +```sql +CREATE TABLE `student` ( + `id` int NOT NULL, + `name` varchar(100) DEFAULT NULL, + `class` varchar(100) DEFAULT NULL, + PRIMARY KEY (`id`), + KEY `name_class_idx` (`name`,`class`) +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4; +``` + +2、下面我们分别测试三条不同的 SQL 语句。 + +![](https://oss.javaguide.cn/github/javaguide/database/mysql/leftmost-prefix-matching-rule.png) + +```sql +# 可以命中索引 +SELECT * FROM student WHERE name = 'Anne Henry'; +EXPLAIN SELECT * FROM student WHERE name = 'Anne Henry' AND class = 'lIrm08RYVk'; +# 无法命中索引 +SELECT * FROM student WHERE class = 'lIrm08RYVk'; +``` + +再来看一个常见的面试题:如果有索引 `联合索引(a,b,c)`,查询 `a=1 AND c=1` 会走索引么?`c=1` 呢?`b=1 AND c=1` 呢? + +先不要往下看答案,给自己 3 分钟时间想一想。 + +1. 查询 `a=1 AND c=1`:根据最左前缀匹配原则,查询可以使用索引的前缀部分。因此,该查询仅在 `a=1` 上使用索引,然后对结果进行 `c=1` 的过滤。 +2. 查询 `c=1`:由于查询中不包含最左列 `a`,根据最左前缀匹配原则,整个索引都无法被使用。 +3. 查询 `b=1 AND c=1`:和第二种一样的情况,整个索引都不会使用。 -相关阅读:[联合索引的最左匹配原则全网都在说的一个错误结论](https://mp.weixin.qq.com/s/8qemhRg5MgXs1So5YCv0fQ)。 +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)** 是 **MySQL 5.6** 版本中提供的一项索引优化功能,可以在非聚簇索引遍历过程中,对索引中包含的字段先做判断,过滤掉不符合条件的记录,减少回表次数。 +**索引下推(Index Condition Pushdown,简称 ICP)** 是 **MySQL 5.6** 版本中提供的一项索引优化功能,它允许存储引擎在索引遍历过程中,执行部分 `WHERE` 字句的判断条件,直接过滤掉不满足条件的记录,从而减少回表次数,提高查询效率。 + +假设我们有一个名为 `user` 的表,其中包含 `id`、`username`、`zipcode` 和 `birthdate` 4 个字段,创建了联合索引 `(zipcode, birthdate)`。 + +```sql +CREATE TABLE `user` ( + `id` int NOT NULL AUTO_INCREMENT, + `username` varchar(20) CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci NOT NULL, + `zipcode` varchar(20) CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci NOT NULL, + `birthdate` date NOT NULL, + PRIMARY KEY (`id`), + KEY `idx_username_birthdate` (`zipcode`,`birthdate`) ) ENGINE=InnoDB AUTO_INCREMENT=1001 DEFAULT CHARSET=utf8mb4; + +# 查询 zipcode 为 431200 且生日在 3 月的用户 +# birthdate 字段使用函数索引失效 +SELECT * FROM user WHERE zipcode = '431200' AND 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) + +![](https://oss.javaguide.cn/github/javaguide/database/mysql/index-condition-pushdown-graphic-illustration.png) + +再来讲讲索引下推的具体原理,先看下面这张 MySQL 简要架构图。 + +![](https://oss.javaguide.cn/javaguide/13526879-3037b144ed09eb88.png) + +MySQL 可以简单分为 Server 层和存储引擎层这两层。Server 层处理查询解析、分析、优化、缓存以及与客户端的交互等操作,而存储引擎层负责数据的存储和读取,MySQL 支持 InnoDB、MyISAM、Memory 等多种存储引擎。 + +索引下推的 **下推** 其实就是指将部分上层(Server 层)负责的事情,交给了下层(存储引擎层)去处理。 + +我们这里结合索引下推原理再对上面提到的例子进行解释。 + +没有索引下推之前: + +- 存储引擎层先根据 `zipcode` 索引字段找到所有 `zipcode = '431200'` 的用户的主键 ID,然后二次回表查询,获取完整的用户数据; +- 存储引擎层把所有 `zipcode = '431200'` 的用户数据全部交给 Server 层,Server 层根据 `MONTH(birthdate) = 3` 这一条件再进一步做筛选。 + +有了索引下推之后: + +- 存储引擎层先根据 `zipcode` 索引字段找到所有 `zipcode = '431200'` 的用户,然后直接判断 `MONTH(birthdate) = 3`,筛选出符合条件的主键 ID; +- 二次回表查询,根据符合条件的主键 ID 去获取完整的用户数据; +- 存储引擎层把符合条件的用户数据全部交给 Server 层。 + +可以看出,**除了可以减少回表次数之外,索引下推还可以减少存储引擎层和 Server 层的数据传输量。** + +最后,总结一下索引下推应用范围: + +1. 适用于 InnoDB 引擎和 MyISAM 引擎的查询。 +2. 适用于执行计划是 range、ref、eq_ref、ref_or_null 的范围查询。 +3. 对于 InnoDB 表,仅用于非聚簇索引。索引下推的目标是减少全行读取次数,从而减少 I/O 操作。对于 InnoDB 聚集索引,完整的记录已经读入 InnoDB 缓冲区。在这种情况下使用索引下推不会减少 I/O。 +4. 子查询不能使用索引下推,因为子查询通常会创建临时表来处理结果,而这些临时表是没有索引的。 +5. 存储过程不能使用索引下推,因为存储引擎无法调用存储函数。 ## 正确使用索引的一些建议 ### 选择合适的字段创建索引 -- **不为 NULL 的字段**:索引字段的数据应该尽量不为 NULL,因为对于数据为 NULL 的字段,数据库较难优化。如果字段频繁被查询,但又避免不了为 NULL,建议使用 0,1,true,false 这样语义较为清晰的短值或短字符作为替代。 +- **不为 NULL 的字段**:索引字段的数据应该尽量不为 NULL,因为对于数据为 NULL 的字段,数据库较难优化。如果字段频繁被查询,但又避免不了为 NULL,建议使用 0、1、true、false 这样语义较为清晰的短值或短字符作为替代。 - **被频繁查询的字段**:我们创建索引的字段应该是查询操作非常频繁的字段。 - **被作为条件查询的字段**:被作为 WHERE 条件查询的字段,应该被考虑建立索引。 - **频繁需要排序的字段**:索引已经排序,这样查询可以利用索引的排序,加快排序查询时间。 @@ -371,7 +470,7 @@ ALTER TABLE `cus_order` ADD INDEX id_score_name(score, name); ### 限制每张表上的索引数量 -索引并不是越多越好,建议单张表索引不超过 5 个!索引可以提高效率同样可以降低效率。 +索引并不是越多越好,建议单张表索引不超过 5 个!索引可以提高效率,同样可以降低效率。 索引可以增加查询效率,但同样也会降低插入和更新的效率,甚至有些情况下会降低查询效率。 @@ -379,11 +478,11 @@ ALTER TABLE `cus_order` ADD INDEX id_score_name(score, name); ### 尽可能的考虑建立联合索引而不是单列索引 -因为索引是需要占用磁盘空间的,可以简单理解为每个索引都对应着一颗 B+树。如果一个表的字段过多,索引过多,那么当这个表的数据达到一个体量后,索引占用的空间也是很多的,且修改索引时,耗费的时间也是较多的。如果是联合索引,多个字段在一个索引上,那么将会节约很大磁盘空间,且修改数据的操作效率也会提升。 +因为索引是需要占用磁盘空间的,可以简单理解为每个索引都对应着一颗 B+ 树。如果一个表的字段过多,索引过多,那么当这个表的数据达到一个体量后,索引占用的空间也是很多的,且修改索引时,耗费的时间也是较多的。如果是联合索引,多个字段在一个索引上,那么将会节约很大磁盘空间,且修改数据的操作效率也会提升。 ### 注意避免冗余索引 -冗余索引指的是索引的功能相同,能够命中索引(a, b)就肯定能命中索引(a) ,那么索引(a)就是冗余索引。如(name,city )和(name )这两个索引就是冗余索引,能够命中前者的查询肯定是能够命中后者的 在大多数情况下,都应该尽量扩展已有的索引而不是创建新索引。 +冗余索引指的是索引的功能相同,能够命中索引(a, b)就肯定能命中索引(a) ,那么索引(a)就是冗余索引。如(name,city)和(name)这两个索引就是冗余索引,能够命中前者的查询肯定是能够命中后者的。在大多数情况下,都应该尽量扩展已有的索引而不是创建新索引。 ### 字符串类型的字段使用前缀索引代替普通索引 @@ -393,13 +492,16 @@ ALTER TABLE `cus_order` ADD INDEX id_score_name(score, name); 索引失效也是慢查询的主要原因之一,常见的导致索引失效的情况有下面这些: -- ~~使用 `SELECT *` 进行查询;~~ `SELECT *` 不会直接导致索引失效(如果不走索引大概率是因为 where 查询范围过大导致的),但它可能会带来一些其他的性能问题比如造成网络传输和数据处理的浪费、无法使用索引覆盖; -- 创建了组合索引,但查询条件未遵守最左匹配原则; -- 在索引列上进行计算、函数、类型转换等操作; -- 以 `%` 开头的 LIKE 查询比如 `like '%abc'`; -- 查询条件中使用 or,且 or 的前后条件中有一个列没有索引,涉及的索引都不会被使用到; -- 发生[隐式转换](./index-invalidation-caused-by-implicit-conversion.md); -- ...... +- ~~使用 `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)。 ### 删除长期未使用的索引 @@ -407,7 +509,7 @@ ALTER TABLE `cus_order` ADD INDEX id_score_name(score, name); MySQL 5.7 可以通过查询 `sys` 库的 `schema_unused_indexes` 视图来查询哪些索引从未被使用。 -### 知道如何分析语句是否走索引查询 +### 知道如何分析 SQL 语句是否走索引查询 我们可以使用 `EXPLAIN` 命令来分析 SQL 的 **执行计划** ,这样就知道语句是否命中索引了。执行计划是指一条 SQL 语句在经过 MySQL 查询优化器的优化会后,具体的执行方式。 @@ -443,3 +545,5 @@ mysql> EXPLAIN SELECT `score`,`name` FROM `cus_order` ORDER BY `score` DESC; | Extra | 附加信息 | 篇幅问题,我这里只是简单介绍了一下 MySQL 执行计划,详细介绍请看:[MySQL 执行计划分析](./mysql-query-execution-plan.md)这篇文章。 + + diff --git a/docs/database/mysql/mysql-logs.md b/docs/database/mysql/mysql-logs.md index f3888285905..ac7e29db2f3 100644 --- a/docs/database/mysql/mysql-logs.md +++ b/docs/database/mysql/mysql-logs.md @@ -9,27 +9,27 @@ tag: ## 前言 -`MySQL` 日志 主要包括错误日志、查询日志、慢查询日志、事务日志、二进制日志几大类。其中,比较重要的还要属二进制日志 `binlog`(归档日志)和事务日志 `redo log`(重做日志)和 `undo log`(回滚日志)。 +MySQL 日志 主要包括错误日志、查询日志、慢查询日志、事务日志、二进制日志几大类。其中,比较重要的还要属二进制日志 binlog(归档日志)和事务日志 redo log(重做日志)和 undo log(回滚日志)。 ![](https://oss.javaguide.cn/github/javaguide/01.png) -今天就来聊聊 `redo log`(重做日志)、`binlog`(归档日志)、两阶段提交、`undo log` (回滚日志)。 +今天就来聊聊 redo log(重做日志)、binlog(归档日志)、两阶段提交、undo log(回滚日志)。 ## redo log -`redo log`(重做日志)是`InnoDB`存储引擎独有的,它让`MySQL`拥有了崩溃恢复能力。 +redo log(重做日志)是 InnoDB 存储引擎独有的,它让 MySQL 拥有了崩溃恢复能力。 -比如 `MySQL` 实例挂了或宕机了,重启时,`InnoDB`存储引擎会使用`redo log`恢复数据,保证数据的持久性与完整性。 +比如 MySQL 实例挂了或宕机了,重启时,InnoDB 存储引擎会使用 redo log 恢复数据,保证数据的持久性与完整性。 ![](https://oss.javaguide.cn/github/javaguide/02.png) -`MySQL` 中数据是以页为单位,你查询一条记录,会从硬盘把一页的数据加载出来,加载出来的数据叫数据页,会放入到 `Buffer Pool` 中。 +MySQL 中数据是以页为单位,你查询一条记录,会从硬盘把一页的数据加载出来,加载出来的数据叫数据页,会放入到 `Buffer Pool` 中。 -后续的查询都是先从 `Buffer Pool` 中找,没有命中再去硬盘加载,减少硬盘 `IO` 开销,提升性能。 +后续的查询都是先从 `Buffer Pool` 中找,没有命中再去硬盘加载,减少硬盘 IO 开销,提升性能。 更新表数据的时候,也是如此,发现 `Buffer Pool` 里存在要更新的数据,就直接在 `Buffer Pool` 里更新。 -然后会把“在某个数据页上做了什么修改”记录到重做日志缓存(`redo log buffer`)里,接着刷盘到 `redo log` 文件里。 +然后会把“在某个数据页上做了什么修改”记录到重做日志缓存(`redo log buffer`)里,接着刷盘到 redo log 文件里。 ![](https://oss.javaguide.cn/github/javaguide/03.png) @@ -41,23 +41,38 @@ tag: ### 刷盘时机 -`InnoDB` 存储引擎为 `redo log` 的刷盘策略提供了 `innodb_flush_log_at_trx_commit` 参数,它支持三种策略: +InnoDB 刷新重做日志的时机有几种情况: -- **0**:设置为 0 的时候,表示每次事务提交时不进行刷盘操作 -- **1**:设置为 1 的时候,表示每次事务提交时都将进行刷盘操作(默认值) -- **2**:设置为 2 的时候,表示每次事务提交时都只把 redo log buffer 内容写入 page cache +InnoDB 将 redo log 刷到磁盘上有几种情况: -`innodb_flush_log_at_trx_commit` 参数默认为 1 ,也就是说当事务提交时会调用 `fsync` 对 redo log 进行刷盘 +1. 事务提交:当事务提交时,log buffer 里的 redo log 会被刷新到磁盘(可以通过`innodb_flush_log_at_trx_commit`参数控制,后文会提到)。 +2. log buffer 空间不足时:log buffer 中缓存的 redo log 已经占满了 log buffer 总容量的大约一半左右,就需要把这些日志刷新到磁盘上。 +3. 事务日志缓冲区满:InnoDB 使用一个事务日志缓冲区(transaction log buffer)来暂时存储事务的重做日志条目。当缓冲区满时,会触发日志的刷新,将日志写入磁盘。 +4. Checkpoint(检查点):InnoDB 定期会执行检查点操作,将内存中的脏数据(已修改但尚未写入磁盘的数据)刷新到磁盘,并且会将相应的重做日志一同刷新,以确保数据的一致性。 +5. 后台刷新线程:InnoDB 启动了一个后台线程,负责周期性(每隔 1 秒)地将脏页(已修改但尚未写入磁盘的数据页)刷新到磁盘,并将相关的重做日志一同刷新。 +6. 正常关闭服务器:MySQL 关闭的时候,redo log 都会刷入到磁盘里去。 -另外,`InnoDB` 存储引擎有一个后台线程,每隔`1` 秒,就会把 `redo log buffer` 中的内容写到文件系统缓存(`page cache`),然后调用 `fsync` 刷盘。 +总之,InnoDB 在多种情况下会刷新重做日志,以保证数据的持久性和一致性。 + +我们要注意设置正确的刷盘策略`innodb_flush_log_at_trx_commit` 。根据 MySQL 配置的刷盘策略的不同,MySQL 宕机之后可能会存在轻微的数据丢失问题。 + +`innodb_flush_log_at_trx_commit` 的值有 3 种,也就是共有 3 种刷盘策略: + +- **0**:设置为 0 的时候,表示每次事务提交时不进行刷盘操作。这种方式性能最高,但是也最不安全,因为如果 MySQL 挂了或宕机了,可能会丢失最近 1 秒内的事务。 +- **1**:设置为 1 的时候,表示每次事务提交时都将进行刷盘操作。这种方式性能最低,但是也最安全,因为只要事务提交成功,redo log 记录就一定在磁盘里,不会有任何数据丢失。 +- **2**:设置为 2 的时候,表示每次事务提交时都只把 log buffer 里的 redo log 内容写入 page cache(文件系统缓存)。page cache 是专门用来缓存文件的,这里被缓存的文件就是 redo log 文件。这种方式的性能和安全性都介于前两者中间。 + +刷盘策略`innodb_flush_log_at_trx_commit` 的默认值为 1,设置为 1 的时候才不会丢失任何数据。为了保证事务的持久性,我们必须将其设置为 1。 + +另外,InnoDB 存储引擎有一个后台线程,每隔`1` 秒,就会把 `redo log buffer` 中的内容写到文件系统缓存(`page cache`),然后调用 `fsync` 刷盘。 ![](https://oss.javaguide.cn/github/javaguide/04.png) -也就是说,一个没有提交事务的 `redo log` 记录,也可能会刷盘。 +也就是说,一个没有提交事务的 redo log 记录,也可能会刷盘。 **为什么呢?** -因为在事务执行过程 `redo log` 记录是会写入`redo log buffer` 中,这些 `redo log` 记录会被后台线程刷盘。 +因为在事务执行过程 redo log 记录是会写入`redo log buffer` 中,这些 redo log 记录会被后台线程刷盘。 ![](https://oss.javaguide.cn/github/javaguide/05.png) @@ -69,15 +84,15 @@ tag: ![](https://oss.javaguide.cn/github/javaguide/06.png) -为`0`时,如果`MySQL`挂了或宕机可能会有`1`秒数据的丢失。 +为`0`时,如果 MySQL 挂了或宕机可能会有`1`秒数据的丢失。 #### innodb_flush_log_at_trx_commit=1 ![](https://oss.javaguide.cn/github/javaguide/07.png) -为`1`时, 只要事务提交成功,`redo log`记录就一定在硬盘里,不会有任何数据丢失。 +为`1`时, 只要事务提交成功,redo log 记录就一定在硬盘里,不会有任何数据丢失。 -如果事务执行期间`MySQL`挂了或宕机,这部分日志丢了,但是事务并没有提交,所以日志丢了也不会有损失。 +如果事务执行期间 MySQL 挂了或宕机,这部分日志丢了,但是事务并没有提交,所以日志丢了也不会有损失。 #### innodb_flush_log_at_trx_commit=2 @@ -85,40 +100,81 @@ tag: 为`2`时, 只要事务提交成功,`redo log buffer`中的内容只写入文件系统缓存(`page cache`)。 -如果仅仅只是`MySQL`挂了不会有任何数据丢失,但是宕机可能会有`1`秒数据的丢失。 +如果仅仅只是 MySQL 挂了不会有任何数据丢失,但是宕机可能会有`1`秒数据的丢失。 ### 日志文件组 -硬盘上存储的 `redo log` 日志文件不只一个,而是以一个**日志文件组**的形式出现的,每个的`redo`日志文件大小都是一样的。 +硬盘上存储的 redo log 日志文件不只一个,而是以一个**日志文件组**的形式出现的,每个的`redo`日志文件大小都是一样的。 -比如可以配置为一组`4`个文件,每个文件的大小是 `1GB`,整个 `redo log` 日志文件组可以记录`4G`的内容。 +比如可以配置为一组`4`个文件,每个文件的大小是 `1GB`,整个 redo log 日志文件组可以记录`4G`的内容。 它采用的是环形数组形式,从头开始写,写到末尾又回到头循环写,如下图所示。 ![](https://oss.javaguide.cn/github/javaguide/10.png) -在个**日志文件组**中还有两个重要的属性,分别是 `write pos、checkpoint` +在这个**日志文件组**中还有两个重要的属性,分别是 `write pos、checkpoint` - **write pos** 是当前记录的位置,一边写一边后移 - **checkpoint** 是当前要擦除的位置,也是往后推移 -每次刷盘 `redo log` 记录到**日志文件组**中,`write pos` 位置就会后移更新。 +每次刷盘 redo log 记录到**日志文件组**中,`write pos` 位置就会后移更新。 -每次 `MySQL` 加载**日志文件组**恢复数据时,会清空加载过的 `redo log` 记录,并把 `checkpoint` 后移更新。 +每次 MySQL 加载**日志文件组**恢复数据时,会清空加载过的 redo log 记录,并把 `checkpoint` 后移更新。 -`write pos` 和 `checkpoint` 之间的还空着的部分可以用来写入新的 `redo log` 记录。 +`write pos` 和 `checkpoint` 之间的还空着的部分可以用来写入新的 redo log 记录。 ![](https://oss.javaguide.cn/github/javaguide/11.png) -如果 `write pos` 追上 `checkpoint` ,表示**日志文件组**满了,这时候不能再写入新的 `redo log` 记录,`MySQL` 得停下来,清空一些记录,把 `checkpoint` 推进一下。 +如果 `write pos` 追上 `checkpoint` ,表示**日志文件组**满了,这时候不能再写入新的 redo log 记录,MySQL 得停下来,清空一些记录,把 `checkpoint` 推进一下。 ![](https://oss.javaguide.cn/github/javaguide/12.png) +注意从 MySQL 8.0.30 开始,日志文件组有了些许变化: + +> The innodb_redo_log_capacity variable supersedes the innodb_log_files_in_group and innodb_log_file_size variables, which are deprecated. When the innodb_redo_log_capacity setting is defined, the innodb_log_files_in_group and innodb_log_file_size settings are ignored; otherwise, these settings are used to compute the innodb_redo_log_capacity setting (innodb_log_files_in_group \* innodb_log_file_size = innodb_redo_log_capacity). If none of those variables are set, redo log capacity is set to the innodb_redo_log_capacity default value, which is 104857600 bytes (100MB). The maximum redo log capacity is 128GB. + +> Redo log files reside in the #innodb_redo directory in the data directory unless a different directory was specified by the innodb_log_group_home_dir variable. If innodb_log_group_home_dir was defined, the redo log files reside in the #innodb_redo directory in that directory. There are two types of redo log files, ordinary and spare. Ordinary redo log files are those being used. Spare redo log files are those waiting to be used. InnoDB tries to maintain 32 redo log files in total, with each file equal in size to 1/32 \* innodb_redo_log_capacity; however, file sizes may differ for a time after modifying the innodb_redo_log_capacity setting. + +意思是在 MySQL 8.0.30 之前可以通过 `innodb_log_files_in_group` 和 `innodb_log_file_size` 配置日志文件组的文件数和文件大小,但在 MySQL 8.0.30 及之后的版本中,这两个变量已被废弃,即使被指定也是用来计算 `innodb_redo_log_capacity` 的值。而日志文件组的文件数则固定为 32,文件大小则为 `innodb_redo_log_capacity / 32` 。 + +关于这一点变化,我们可以验证一下。 + +首先创建一个配置文件,里面配置一下 `innodb_log_files_in_group` 和 `innodb_log_file_size` 的值: + +```properties +[mysqld] +innodb_log_file_size = 10485760 +innodb_log_files_in_group = 64 +``` + +docker 启动一个 MySQL 8.0.32 的容器: + +```bash +docker run -d -p 3312:3309 -e MYSQL_ROOT_PASSWORD=your-password -v /path/to/your/conf:/etc/mysql/conf.d --name +MySQL830 mysql:8.0.32 +``` + +现在我们来看一下启动日志: + +```plain +2023-08-03T02:05:11.720357Z 0 [Warning] [MY-013907] [InnoDB] Deprecated configuration parameters innodb_log_file_size and/or innodb_log_files_in_group have been used to compute innodb_redo_log_capacity=671088640. Please use innodb_redo_log_capacity instead. +``` + +这里也表明了 `innodb_log_files_in_group` 和 `innodb_log_file_size` 这两个变量是用来计算 `innodb_redo_log_capacity` ,且已经被废弃。 + +我们再看下日志文件组的文件数是多少: + +![](images/redo-log.png) + +可以看到刚好是 32 个,并且每个日志文件的大小是 `671088640 / 32 = 20971520` + +所以在使用 MySQL 8.0.30 及之后的版本时,推荐使用 `innodb_redo_log_capacity` 变量配置日志文件组 + ### redo log 小结 -相信大家都知道 `redo log` 的作用和它的刷盘时机、存储形式。 +相信大家都知道 redo log 的作用和它的刷盘时机、存储形式。 -现在我们来思考一个问题:**只要每次把修改后的数据页直接刷盘不就好了,还有 `redo log` 什么事?** +现在我们来思考一个问题:**只要每次把修改后的数据页直接刷盘不就好了,还有 redo log 什么事?** 它们不都是刷盘么?差别在哪里? @@ -134,32 +190,32 @@ tag: 而且数据页刷盘是随机写,因为一个数据页对应的位置可能在硬盘文件的随机位置,所以性能是很差。 -如果是写 `redo log`,一行记录可能就占几十 `Byte`,只包含表空间号、数据页号、磁盘文件偏移 +如果是写 redo log,一行记录可能就占几十 `Byte`,只包含表空间号、数据页号、磁盘文件偏移 量、更新值,再加上是顺序写,所以刷盘速度很快。 -所以用 `redo log` 形式记录修改内容,性能会远远超过刷数据页的方式,这也让数据库的并发能力更强。 +所以用 redo log 形式记录修改内容,性能会远远超过刷数据页的方式,这也让数据库的并发能力更强。 > 其实内存的数据页在一定时机也会刷盘,我们把这称为页合并,讲 `Buffer Pool`的时候会对这块细说 ## binlog -`redo log` 它是物理日志,记录内容是“在某个数据页上做了什么修改”,属于 `InnoDB` 存储引擎。 +redo log 它是物理日志,记录内容是“在某个数据页上做了什么修改”,属于 InnoDB 存储引擎。 -而 `binlog` 是逻辑日志,记录内容是语句的原始逻辑,类似于“给 ID=2 这一行的 c 字段加 1”,属于`MySQL Server` 层。 +而 binlog 是逻辑日志,记录内容是语句的原始逻辑,类似于“给 ID=2 这一行的 c 字段加 1”,属于`MySQL Server` 层。 -不管用什么存储引擎,只要发生了表数据更新,都会产生 `binlog` 日志。 +不管用什么存储引擎,只要发生了表数据更新,都会产生 binlog 日志。 -那 `binlog` 到底是用来干嘛的? +那 binlog 到底是用来干嘛的? -可以说`MySQL`数据库的**数据备份、主备、主主、主从**都离不开`binlog`,需要依靠`binlog`来同步数据,保证数据一致性。 +可以说 MySQL 数据库的**数据备份、主备、主主、主从**都离不开 binlog,需要依靠 binlog 来同步数据,保证数据一致性。 ![](https://oss.javaguide.cn/github/javaguide/01-20220305234724956.png) -`binlog`会记录所有涉及更新数据的逻辑操作,并且是顺序写。 +binlog 会记录所有涉及更新数据的逻辑操作,并且是顺序写。 ### 记录格式 -`binlog` 日志有三种格式,可以通过`binlog_format`参数指定。 +binlog 日志有三种格式,可以通过`binlog_format`参数指定。 - **statement** - **row** @@ -181,28 +237,28 @@ tag: 这样就能保证同步数据的一致性,通常情况下都是指定为`row`,这样可以为数据库的恢复与同步带来更好的可靠性。 -但是这种格式,需要更大的容量来记录,比较占用空间,恢复与同步时会更消耗`IO`资源,影响执行速度。 +但是这种格式,需要更大的容量来记录,比较占用空间,恢复与同步时会更消耗 IO 资源,影响执行速度。 所以就有了一种折中的方案,指定为`mixed`,记录的内容是前两者的混合。 -`MySQL`会判断这条`SQL`语句是否可能引起数据不一致,如果是,就用`row`格式,否则就用`statement`格式。 +MySQL 会判断这条`SQL`语句是否可能引起数据不一致,如果是,就用`row`格式,否则就用`statement`格式。 ### 写入机制 -`binlog`的写入时机也非常简单,事务执行过程中,先把日志写到`binlog cache`,事务提交的时候,再把`binlog cache`写到`binlog`文件中。 +binlog 的写入时机也非常简单,事务执行过程中,先把日志写到`binlog cache`,事务提交的时候,再把`binlog cache`写到 binlog 文件中。 -因为一个事务的`binlog`不能被拆开,无论这个事务多大,也要确保一次性写入,所以系统会给每个线程分配一个块内存作为`binlog cache`。 +因为一个事务的 binlog 不能被拆开,无论这个事务多大,也要确保一次性写入,所以系统会给每个线程分配一个块内存作为`binlog cache`。 我们可以通过`binlog_cache_size`参数控制单个线程 binlog cache 大小,如果存储内容超过了这个参数,就要暂存到磁盘(`Swap`)。 -`binlog`日志刷盘流程如下 +binlog 日志刷盘流程如下 ![](https://oss.javaguide.cn/github/javaguide/04-20220305234747840.png) - **上图的 write,是指把日志写入到文件系统的 page cache,并没有把数据持久化到磁盘,所以速度比较快** - **上图的 fsync,才是将数据持久化到磁盘的操作** -`write`和`fsync`的时机,可以由参数`sync_binlog`控制,默认是`0`。 +`write`和`fsync`的时机,可以由参数`sync_binlog`控制,默认是`1`。 为`0`的时候,表示每次提交事务都只`write`,由系统自行判断什么时候执行`fsync`。 @@ -216,57 +272,63 @@ tag: ![](https://oss.javaguide.cn/github/javaguide/06-20220305234801592.png) -在出现`IO`瓶颈的场景里,将`sync_binlog`设置成一个比较大的值,可以提升性能。 +在出现 IO 瓶颈的场景里,将`sync_binlog`设置成一个比较大的值,可以提升性能。 -同样的,如果机器宕机,会丢失最近`N`个事务的`binlog`日志。 +同样的,如果机器宕机,会丢失最近`N`个事务的 binlog 日志。 ## 两阶段提交 -`redo log`(重做日志)让`InnoDB`存储引擎拥有了崩溃恢复能力。 +redo log(重做日志)让 InnoDB 存储引擎拥有了崩溃恢复能力。 -`binlog`(归档日志)保证了`MySQL`集群架构的数据一致性。 +binlog(归档日志)保证了 MySQL 集群架构的数据一致性。 虽然它们都属于持久化的保证,但是侧重点不同。 -在执行更新语句过程,会记录`redo log`与`binlog`两块日志,以基本的事务为单位,`redo log`在事务执行过程中可以不断写入,而`binlog`只有在提交事务时才写入,所以`redo log`与`binlog`的写入时机不一样。 +在执行更新语句过程,会记录 redo log 与 binlog 两块日志,以基本的事务为单位,redo log 在事务执行过程中可以不断写入,而 binlog 只有在提交事务时才写入,所以 redo log 与 binlog 的写入时机不一样。 ![](https://oss.javaguide.cn/github/javaguide/01-20220305234816065.png) -回到正题,`redo log`与`binlog`两份日志之间的逻辑不一致,会出现什么问题? +回到正题,redo log 与 binlog 两份日志之间的逻辑不一致,会出现什么问题? 我们以`update`语句为例,假设`id=2`的记录,字段`c`值是`0`,把字段`c`值更新成`1`,`SQL`语句为`update T set c=1 where id=2`。 -假设执行过程中写完`redo log`日志后,`binlog`日志写期间发生了异常,会出现什么情况呢? +假设执行过程中写完 redo log 日志后,binlog 日志写期间发生了异常,会出现什么情况呢? ![](https://oss.javaguide.cn/github/javaguide/02-20220305234828662.png) -由于`binlog`没写完就异常,这时候`binlog`里面没有对应的修改记录。因此,之后用`binlog`日志恢复数据时,就会少这一次更新,恢复出来的这一行`c`值是`0`,而原库因为`redo log`日志恢复,这一行`c`值是`1`,最终数据不一致。 +由于 binlog 没写完就异常,这时候 binlog 里面没有对应的修改记录。因此,之后用 binlog 日志恢复数据时,就会少这一次更新,恢复出来的这一行`c`值是`0`,而原库因为 redo log 日志恢复,这一行`c`值是`1`,最终数据不一致。 ![](https://oss.javaguide.cn/github/javaguide/03-20220305235104445.png) -为了解决两份日志之间的逻辑一致问题,`InnoDB`存储引擎使用**两阶段提交**方案。 +为了解决两份日志之间的逻辑一致问题,InnoDB 存储引擎使用**两阶段提交**方案。 -原理很简单,将`redo log`的写入拆成了两个步骤`prepare`和`commit`,这就是**两阶段提交**。 +原理很简单,将 redo log 的写入拆成了两个步骤`prepare`和`commit`,这就是**两阶段提交**。 ![](https://oss.javaguide.cn/github/javaguide/04-20220305234956774.png) -使用**两阶段提交**后,写入`binlog`时发生异常也不会有影响,因为`MySQL`根据`redo log`日志恢复数据时,发现`redo log`还处于`prepare`阶段,并且没有对应`binlog`日志,就会回滚该事务。 +使用**两阶段提交**后,写入 binlog 时发生异常也不会有影响,因为 MySQL 根据 redo log 日志恢复数据时,发现 redo log 还处于`prepare`阶段,并且没有对应 binlog 日志,就会回滚该事务。 ![](https://oss.javaguide.cn/github/javaguide/05-20220305234937243.png) -再看一个场景,`redo log`设置`commit`阶段发生异常,那会不会回滚事务呢? +再看一个场景,redo log 设置`commit`阶段发生异常,那会不会回滚事务呢? ![](https://oss.javaguide.cn/github/javaguide/06-20220305234907651.png) -并不会回滚事务,它会执行上图框住的逻辑,虽然`redo log`是处于`prepare`阶段,但是能通过事务`id`找到对应的`binlog`日志,所以`MySQL`认为是完整的,就会提交事务恢复数据。 +并不会回滚事务,它会执行上图框住的逻辑,虽然 redo log 是处于`prepare`阶段,但是能通过事务`id`找到对应的 binlog 日志,所以 MySQL 认为是完整的,就会提交事务恢复数据。 ## undo log > 这部分内容为 JavaGuide 的补充: -我们知道如果想要保证事务的原子性,就需要在异常发生时,对已经执行的操作进行**回滚**,在 MySQL 中,恢复机制是通过 **回滚日志(undo log)** 实现的,所有事务进行的修改都会先记录到这个回滚日志中,然后再执行相关的操作。如果执行过程中遇到异常的话,我们直接利用 **回滚日志** 中的信息将数据回滚到修改之前的样子即可!并且,回滚日志会先于数据持久化到磁盘上。这样就保证了即使遇到数据库突然宕机等情况,当用户再次启动数据库的时候,数据库还能够通过查询回滚日志来回滚将之前未完成的事务。 +每一个事务对数据的修改都会被记录到 undo log ,当执行事务过程中出现错误或者需要执行回滚操作的话,MySQL 可以利用 undo log 将数据恢复到事务开始之前的状态。 + +undo log 属于逻辑日志,记录的是 SQL 语句,比如说事务执行一条 DELETE 语句,那 undo log 就会记录一条相对应的 INSERT 语句。同时,undo log 的信息也会被记录到 redo log 中,因为 undo log 也要实现持久性保护。并且,undo-log 本身是会被删除清理的,例如 INSERT 操作,在事务提交之后就可以清除掉了;UPDATE/DELETE 操作在事务提交不会立即删除,会加入 history list,由后台线程 purge 进行清理。 -另外,`MVCC` 的实现依赖于:**隐藏字段、Read View、undo log**。在内部实现中,`InnoDB` 通过数据行的 `DB_TRX_ID` 和 `Read View` 来判断数据的可见性,如不可见,则通过数据行的 `DB_ROLL_PTR` 找到 `undo log` 中的历史版本。每个事务读到的数据版本可能是不一样的,在同一个事务中,用户只能看到该事务创建 `Read View` 之前已经提交的修改和该事务本身做的修改 +undo log 是采用 segment(段)的方式来记录的,每个 undo 操作在记录的时候占用一个 **undo log segment**(undo 日志段),undo log segment 包含在 **rollback segment**(回滚段)中。事务开始时,需要为其分配一个 rollback segment。每个 rollback segment 有 1024 个 undo log segment,这有助于管理多个并发事务的回滚需求。 + +通常情况下, **rollback segment header**(通常在回滚段的第一个页)负责管理 rollback segment。rollback segment header 是 rollback segment 的一部分,通常在回滚段的第一个页。**history list** 是 rollback segment header 的一部分,它的主要作用是记录所有已经提交但还没有被清理(purge)的事务的 undo log。这个列表使得 purge 线程能够找到并清理那些不再需要的 undo log 记录。 + +另外,`MVCC` 的实现依赖于:**隐藏字段、Read View、undo log**。在内部实现中,InnoDB 通过数据行的 `DB_TRX_ID` 和 `Read View` 来判断数据的可见性,如不可见,则通过数据行的 `DB_ROLL_PTR` 找到 undo log 中的历史版本。每个事务读到的数据版本可能是不一样的,在同一个事务中,用户只能看到该事务创建 `Read View` 之前已经提交的修改和该事务本身做的修改 ## 总结 @@ -274,16 +336,13 @@ tag: MySQL InnoDB 引擎使用 **redo log(重做日志)** 保证事务的**持久性**,使用 **undo log(回滚日志)** 来保证事务的**原子性**。 -`MySQL`数据库的**数据备份、主备、主主、主从**都离不开`binlog`,需要依靠`binlog`来同步数据,保证数据一致性。 +MySQL 数据库的**数据备份、主备、主主、主从**都离不开 binlog,需要依靠 binlog 来同步数据,保证数据一致性。 -## 站在巨人的肩膀上 +## 参考 - 《MySQL 实战 45 讲》 - 《从零开始带你成为 MySQL 实战优化高手》 - 《MySQL 是怎样运行的:从根儿上理解 MySQL》 - 《MySQL 技术 Innodb 存储引擎》 -## MySQL 好文推荐 - -- [CURD 这么多年,你有了解过 MySQL 的架构设计吗?](https://mp.weixin.qq.com/s/R-1km7r0z3oWfwYQV8iiqA) -- [浅谈 MySQL InnoDB 的内存组件](https://mp.weixin.qq.com/s/7Kab4IQsNcU_bZdbv_MuOg) + diff --git a/docs/database/mysql/mysql-query-cache.md b/docs/database/mysql/mysql-query-cache.md index 760a197ab4a..cdc49b2c59c 100644 --- a/docs/database/mysql/mysql-query-cache.md +++ b/docs/database/mysql/mysql-query-cache.md @@ -128,7 +128,7 @@ set global query_cache_size=600000; - 缓存建立之后,MySQL 的查询缓存系统会跟踪查询中涉及的每张表,如果这些表(数据或结构)发生变化,那么和这张表相关的所有缓存数据都将失效。 - MySQL 缓存在分库分表环境下是不起作用的。 - 不缓存使用 `SQL_NO_CACHE` 的查询。 -- ...... +- …… 查询缓存 `SELECT` 选项示例: @@ -204,3 +204,5 @@ MySQL 中的查询缓存虽然能够提升数据库的查询性能,但是查 - MySQL 缓存机制: - RDS MySQL 查询缓存(Query Cache)的设置和使用 - 阿里元云数据库 RDS 文档: - 8.10.3 The MySQL Query Cache - MySQL 官方文档: + + diff --git a/docs/database/mysql/mysql-query-execution-plan.md b/docs/database/mysql/mysql-query-execution-plan.md index 696fcbb407b..8866737b934 100644 --- a/docs/database/mysql/mysql-query-execution-plan.md +++ b/docs/database/mysql/mysql-query-execution-plan.md @@ -12,13 +12,13 @@ head: content: 执行计划是指一条 SQL 语句在经过MySQL 查询优化器的优化会后,具体的执行方式。优化 SQL 的第一步应该是读懂 SQL 的执行计划。 --- -> 本文来自公号 MySQL 技术,JavaGuide 对其做了补充完善。原文地址:https://mp.weixin.qq.com/s/d5OowNLtXBGEAbT31sSH4g +> 本文来自公号 MySQL 技术,JavaGuide 对其做了补充完善。原文地址: 优化 SQL 的第一步应该是读懂 SQL 的执行计划。本篇文章,我们一起来学习下 MySQL `EXPLAIN` 执行计划相关知识。 ## 什么是执行计划? -**执行计划** 是指一条 SQL 语句在经过 **MySQL 查询优化器** 的优化会后,具体的执行方式。 +**执行计划** 是指一条 SQL 语句在经过 **MySQL 查询优化器** 的优化后,具体的执行方式。 执行计划通常用于 SQL 性能分析、优化等场景。通过 `EXPLAIN` 的结果,可以了解到如数据表的查询顺序、数据查询操作的操作类型、哪些索引可以被命中、哪些索引实际会命中、每个数据表有多少行记录被查询等信息。 @@ -69,7 +69,7 @@ mysql> explain SELECT * FROM dept_emp WHERE emp_no IN (SELECT emp_no FROM dept_e ### id -SELECT 标识符,是查询中 SELECT 的序号,用来标识整个查询中 SELELCT 语句的顺序。 +`SELECT` 标识符,用于标识每个 `SELECT` 语句的执行顺序。 id 如果相同,从上往下依次执行。id 不同,id 值越大,执行优先级越高,如果行引用其他行的并集结果,则该值可以为 NULL。 @@ -89,11 +89,14 @@ id 如果相同,从上往下依次执行。id 不同,id 值越大,执行 查询用到的表名,每行都有对应的表名,表名除了正常的表之外,也可能是以下列出的值: - **``** : 本行引用了 id 为 M 和 N 的行的 UNION 结果; -- **``** : 本行引用了 id 为 N 的表所产生的的派生表结果。派生表有可能产生自 FROM 语句中的子查询。 -**``** : 本行引用了 id 为 N 的表所产生的的物化子查询结果。 +- **``** : 本行引用了 id 为 N 的表所产生的的派生表结果。派生表有可能产生自 FROM 语句中的子查询。 +- **``** : 本行引用了 id 为 N 的表所产生的的物化子查询结果。 ### type(重要) -查询执行的类型,描述了查询是如何执行的。所有值的顺序从最优到最差排序为:system > const > eq_ref > ref > fulltext > ref_or_null > index_merge > unique_subquery > index_subquery > range > index > ALL +查询执行的类型,描述了查询是如何执行的。所有值的顺序从最优到最差排序为: + +system > const > eq_ref > ref > fulltext > ref_or_null > index_merge > unique_subquery > index_subquery > range > index > ALL 常见的几种类型具体含义如下: @@ -137,5 +140,7 @@ rows 列表示根据表统计信息及选用情况,大致估算出找到所需 ## 参考 -- https://dev.mysql.com/doc/refman/5.7/en/explain-output.html -- https://juejin.cn/post/6953444668973514789 +- +- + + diff --git a/docs/database/mysql/mysql-questions-01.md b/docs/database/mysql/mysql-questions-01.md index db72c16eb59..7f93eb605e6 100644 --- a/docs/database/mysql/mysql-questions-01.md +++ b/docs/database/mysql/mysql-questions-01.md @@ -19,7 +19,7 @@ head: ### 什么是关系型数据库? -顾名思义,关系型数据库(RDBMS,Relational Database Management System)就是一种建立在关系模型的基础上的数据库。关系模型表明了数据库中所存储的数据之间的联系(一对一、一对多、多对多)。 +顾名思义,关系型数据库(RDB,Relational Database)就是一种建立在关系模型的基础上的数据库。关系模型表明了数据库中所存储的数据之间的联系(一对一、一对多、多对多)。 关系型数据库中,我们的数据都被存放在了各种表中(比如用户表),表中的每一行就存放着一条数据(比如一个用户的信息)。 @@ -29,7 +29,7 @@ head: **有哪些常见的关系型数据库呢?** -MySQL、PostgreSQL、Oracle、SQL Server、SQLite(微信本地的聊天记录的存储就是用的 SQLite) ......。 +MySQL、PostgreSQL、Oracle、SQL Server、SQLite(微信本地的聊天记录的存储就是用的 SQLite) ……。 ### 什么是 SQL? @@ -45,7 +45,7 @@ SQL 可以帮助我们: - 对数据库中的数据进行简单的数据分析; - 搭配 Hive,Spark SQL 做大数据; - 搭配 SQLFlow 做机器学习; -- ...... +- …… ### 什么是 MySQL? @@ -70,6 +70,154 @@ MySQL 主要具有下面这些优点: 7. 事务支持优秀, InnoDB 存储引擎默认使用 REPEATABLE-READ 并不会有任何性能损失,并且,InnoDB 实现的 REPEATABLE-READ 隔离级别其实是可以解决幻读问题发生的。 8. 支持分库分表、读写分离、高可用。 +## MySQL 字段类型 + +MySQL 字段类型可以简单分为三大类: + +- **数值类型**:整型(TINYINT、SMALLINT、MEDIUMINT、INT 和 BIGINT)、浮点型(FLOAT 和 DOUBLE)、定点型(DECIMAL) +- **字符串类型**:CHAR、VARCHAR、TINYTEXT、TEXT、MEDIUMTEXT、LONGTEXT、TINYBLOB、BLOB、MEDIUMBLOB 和 LONGBLOB 等,最常用的是 CHAR 和 VARCHAR。 +- **日期时间类型**:YEAR、TIME、DATE、DATETIME 和 TIMESTAMP 等。 + +下面这张图不是我画的,忘记是从哪里保存下来的了,总结的还蛮不错的。 + +![MySQL 常见字段类型总结](https://oss.javaguide.cn/github/javaguide/mysql/summary-of-mysql-field-types.png) + +MySQL 字段类型比较多,我这里会挑选一些日常开发使用很频繁且面试常问的字段类型,以面试问题的形式来详细介绍。如无特殊说明,针对的都是 InnoDB 存储引擎。 + +另外,推荐阅读一下《高性能 MySQL(第三版)》的第四章,有详细介绍 MySQL 字段类型优化。 + +### 整数类型的 UNSIGNED 属性有什么用? + +MySQL 中的整数类型可以使用可选的 UNSIGNED 属性来表示不允许负值的无符号整数。使用 UNSIGNED 属性可以将正整数的上限提高一倍,因为它不需要存储负数值。 + +例如, TINYINT UNSIGNED 类型的取值范围是 0 ~ 255,而普通的 TINYINT 类型的值范围是 -128 ~ 127。INT UNSIGNED 类型的取值范围是 0 ~ 4,294,967,295,而普通的 INT 类型的值范围是 -2,147,483,648 ~ 2,147,483,647。 + +对于从 0 开始递增的 ID 列,使用 UNSIGNED 属性可以非常适合,因为不允许负值并且可以拥有更大的上限范围,提供了更多的 ID 值可用。 + +### CHAR 和 VARCHAR 的区别是什么? + +CHAR 和 VARCHAR 是最常用到的字符串类型,两者的主要区别在于:**CHAR 是定长字符串,VARCHAR 是变长字符串。** + +CHAR 在存储时会在右边填充空格以达到指定的长度,检索时会去掉空格;VARCHAR 在存储时需要使用 1 或 2 个额外字节记录字符串的长度,检索时不需要处理。 + +CHAR 更适合存储长度较短或者长度都差不多的字符串,例如 Bcrypt 算法、MD5 算法加密后的密码、身份证号码。VARCHAR 类型适合存储长度不确定或者差异较大的字符串,例如用户昵称、文章标题等。 + +CHAR(M) 和 VARCHAR(M) 的 M 都代表能够保存的字符数的最大值,无论是字母、数字还是中文,每个都只占用一个字符。 + +### VARCHAR(100)和 VARCHAR(10)的区别是什么? + +VARCHAR(100)和 VARCHAR(10)都是变长类型,表示能存储最多 100 个字符和 10 个字符。因此,VARCHAR (100) 可以满足更大范围的字符存储需求,有更好的业务拓展性。而 VARCHAR(10)存储超过 10 个字符时,就需要修改表结构才可以。 + +虽说 VARCHAR(100)和 VARCHAR(10)能存储的字符范围不同,但二者存储相同的字符串,所占用磁盘的存储空间其实是一样的,这也是很多人容易误解的一点。 + +不过,VARCHAR(100) 会消耗更多的内存。这是因为 VARCHAR 类型在内存中操作时,通常会分配固定大小的内存块来保存值,即使用字符类型中定义的长度。例如在进行排序的时候,VARCHAR(100)是按照 100 这个长度来进行的,也就会消耗更多内存。 + +### DECIMAL 和 FLOAT/DOUBLE 的区别是什么? + +DECIMAL 和 FLOAT 的区别是:**DECIMAL 是定点数,FLOAT/DOUBLE 是浮点数。DECIMAL 可以存储精确的小数值,FLOAT/DOUBLE 只能存储近似的小数值。** + +DECIMAL 用于存储具有精度要求的小数,例如与货币相关的数据,可以避免浮点数带来的精度损失。 + +在 Java 中,MySQL 的 DECIMAL 类型对应的是 Java 类 `java.math.BigDecimal`。 + +### 为什么不推荐使用 TEXT 和 BLOB? + +TEXT 类型类似于 CHAR(0-255 字节)和 VARCHAR(0-65,535 字节),但可以存储更长的字符串,即长文本数据,例如博客内容。 + +| 类型 | 可存储大小 | 用途 | +| ---------- | -------------------- | -------------- | +| TINYTEXT | 0-255 字节 | 一般文本字符串 | +| TEXT | 0-65,535 字节 | 长文本字符串 | +| MEDIUMTEXT | 0-16,772,150 字节 | 较大文本数据 | +| LONGTEXT | 0-4,294,967,295 字节 | 极大文本数据 | + +BLOB 类型主要用于存储二进制大对象,例如图片、音视频等文件。 + +| 类型 | 可存储大小 | 用途 | +| ---------- | ---------- | ------------------------ | +| TINYBLOB | 0-255 字节 | 短文本二进制字符串 | +| BLOB | 0-65KB | 二进制字符串 | +| MEDIUMBLOB | 0-16MB | 二进制形式的长文本数据 | +| LONGBLOB | 0-4GB | 二进制形式的极大文本数据 | + +在日常开发中,很少使用 TEXT 类型,但偶尔会用到,而 BLOB 类型则基本不常用。如果预期长度范围可以通过 VARCHAR 来满足,建议避免使用 TEXT。 + +数据库规范通常不推荐使用 BLOB 和 TEXT 类型,这两种类型具有一些缺点和限制,例如: + +- 不能有默认值。 +- 在使用临时表时无法使用内存临时表,只能在磁盘上创建临时表(《高性能 MySQL》书中有提到)。 +- 检索效率较低。 +- 不能直接创建索引,需要指定前缀长度。 +- 可能会消耗大量的网络和 IO 带宽。 +- 可能导致表上的 DML 操作变慢。 +- …… + +### DATETIME 和 TIMESTAMP 的区别是什么? + +DATETIME 类型没有时区信息,TIMESTAMP 和时区有关。 + +TIMESTAMP 只需要使用 4 个字节的存储空间,但是 DATETIME 需要耗费 8 个字节的存储空间。但是,这样同样造成了一个问题,Timestamp 表示的时间范围更小。 + +- 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)。 + +### NULL 和 '' 的区别是什么? + +`NULL` 和 `''` (空字符串) 是两个完全不同的值,它们分别表示不同的含义,并在数据库中有着不同的行为。`NULL` 代表缺失或未知的数据,而 `''` 表示一个已知存在的空字符串。它们的主要区别如下: + +1. **含义**: + - `NULL` 代表一个不确定的值,它不等于任何值,包括它自身。因此,`SELECT NULL = NULL` 的结果是 `NULL`,而不是 `true` 或 `false`。 `NULL` 意味着缺失或未知的信息。虽然 `NULL` 不等于任何值,但在某些操作中,数据库系统会将 `NULL` 值视为相同的类别进行处理,例如:`DISTINCT`,`GROUP BY`,`ORDER BY`。需要注意的是,这些操作将 `NULL` 值视为相同的类别进行处理,并不意味着 `NULL` 值之间是相等的。 它们只是在特定操作中被特殊处理,以保证结果的正确性和一致性。 这种处理方式是为了方便数据操作,而不是改变了 `NULL` 的语义。 + - `''` 表示一个空字符串,它是一个已知的值。 +2. **存储空间**: + - `NULL` 的存储空间占用取决于数据库的实现,通常需要一些空间来标记该值为空。 + - `''` 的存储空间占用通常较小,因为它只存储一个空字符串的标志,不需要存储实际的字符。 +3. **比较运算**: + - 任何值与 `NULL` 进行比较(例如 `=`, `!=`, `>`, `<` 等)的结果都是 `NULL`,表示结果不确定。要判断一个值是否为 `NULL`,必须使用 `IS NULL` 或 `IS NOT NULL`。 + - `''` 可以像其他字符串一样进行比较运算。例如,`'' = ''` 的结果是 `true`。 +4. **聚合函数**: + - 大多数聚合函数(例如 `SUM`, `AVG`, `MIN`, `MAX`)会忽略 `NULL` 值。 + - `COUNT(*)` 会统计所有行数,包括包含 `NULL` 值的行。`COUNT(列名)` 会统计指定列中非 `NULL` 值的行数。 + - 空字符串 `''` 会被聚合函数计算在内。例如,`SUM` 会将其视为 0,`MIN` 和 `MAX` 会将其视为一个空字符串。 + +看了上面的介绍之后,相信你对另外一个高频面试题:“为什么 MySQL 不建议使用 `NULL` 作为列默认值?”也有了答案。 + +### Boolean 类型如何表示? + +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 中的执行流程”也是面试中比较常问的一个问题。 @@ -85,7 +233,7 @@ MySQL 主要具有下面这些优点: - **分析器:** 没有命中缓存的话,SQL 语句就会经过分析器,分析器说白了就是要先看你的 SQL 语句要干嘛,再检查你的 SQL 语句语法是否正确。 - **优化器:** 按照 MySQL 认为最优的方案去执行。 - **执行器:** 执行语句,然后从存储引擎返回数据。 执行语句之前会先判断是否有权限,如果没有权限的话,就会报错。 -- **插件式存储引擎**:主要负责数据的存储和读取,采用的是插件式架构,支持 InnoDB、MyISAM、Memory 等多种存储引擎。 +- **插件式存储引擎**:主要负责数据的存储和读取,采用的是插件式架构,支持 InnoDB、MyISAM、Memory 等多种存储引擎。InnoDB 是 MySQL 的默认存储引擎,绝大部分场景使用 InnoDB 就是最好的选择。 ## MySQL 存储引擎 @@ -115,7 +263,7 @@ mysql> SELECT VERSION(); 1 row in set (0.00 sec) ``` -你也可以通过 `SHOW VARIABLES LIKE '%storage_engine%'` 命令直接查看 MySQL 当前默认的存储引擎。 +你也可以通过 `SHOW VARIABLES LIKE '%storage_engine%'` 命令直接查看 MySQL 当前默认的存储引擎。 ```bash mysql> SHOW VARIABLES LIKE '%storage_engine%'; @@ -141,7 +289,11 @@ mysql> SHOW VARIABLES LIKE '%storage_engine%'; MySQL 存储引擎采用的是 **插件式架构** ,支持多种存储引擎,我们甚至可以为不同的数据库表设置不同的存储引擎以适应不同场景的需要。**存储引擎是基于表的,而不是数据库。** -并且,你还可以根据 MySQL 定义的存储引擎实现标准接口来编写一个属于自己的存储引擎。这些非官方提供的存储引擎可以称为第三方存储引擎,区别于官方存储引擎。像目前最常用的 InnoDB 其实刚开始就是一个第三方存储引擎,后面由于过于优秀,其被 Oracle 直接收购了。 +下图展示了具有可插拔存储引擎的 MySQL 架构(): + +![MySQL architecture diagram showing connectors, interfaces, pluggable storage engines, the file system with files and logs.](https://oss.javaguide.cn/github/javaguide/mysql/mysql-architecture.png) + +你还可以根据 MySQL 定义的存储引擎实现标准接口来编写一个属于自己的存储引擎。这些非官方提供的存储引擎可以称为第三方存储引擎,区别于官方存储引擎。像目前最常用的 InnoDB 其实刚开始就是一个第三方存储引擎,后面由于过于优秀,其被 Oracle 直接收购了。 MySQL 官方文档也有介绍到如何编写一个自定义存储引擎,地址: 。 @@ -155,13 +307,13 @@ MySQL 5.5 版本之后,InnoDB 是 MySQL 的默认存储引擎。 言归正传!咱们下面还是来简单对比一下两者: -**1.是否支持行级锁** +**1、是否支持行级锁** MyISAM 只有表级锁(table-level locking),而 InnoDB 支持行级锁(row-level locking)和表级锁,默认为行级锁。 也就说,MyISAM 一锁就是锁住了整张表,这在并发写的情况下是多么滴憨憨啊!这也是为什么 InnoDB 在并发写的时候,性能更牛皮了! -**2.是否支持事务** +**2、是否支持事务** MyISAM 不提供事务支持。 @@ -169,7 +321,7 @@ InnoDB 提供事务支持,实现了 SQL 标准定义了四个隔离级别, 关于 MySQL 事务的详细介绍,可以看看我写的这篇文章:[MySQL 事务隔离级别详解](./transaction-isolation-level.md)。 -**3.是否支持外键** +**3、是否支持外键** MyISAM 不支持,而 InnoDB 支持。 @@ -183,19 +335,19 @@ MyISAM 不支持,而 InnoDB 支持。 总结:一般我们也是不建议在数据库层面使用外键的,应用层面可以解决。不过,这样会对数据的一致性造成威胁。具体要不要使用外键还是要根据你的项目来决定。 -**4.是否支持数据库异常崩溃后的安全恢复** +**4、是否支持数据库异常崩溃后的安全恢复** MyISAM 不支持,而 InnoDB 支持。 使用 InnoDB 的数据库在异常崩溃后,数据库重新启动的时候会保证数据库恢复到崩溃前的状态。这个恢复的过程依赖于 `redo log` 。 -**5.是否支持 MVCC** +**5、是否支持 MVCC** MyISAM 不支持,而 InnoDB 支持。 讲真,这个对比有点废话,毕竟 MyISAM 连行级锁都不支持。MVCC 可以看作是行级锁的一个升级,可以有效减少加锁操作,提高性能。 -**6.索引实现不一样。** +**6、索引实现不一样。** 虽然 MyISAM 引擎和 InnoDB 引擎都是使用 B+Tree 作为索引结构,但是两者的实现方式不太一样。 @@ -203,12 +355,16 @@ InnoDB 引擎中,其数据文件本身就是索引文件。相比 MyISAM,索 详细区别,推荐你看看我写的这篇文章:[MySQL 索引详解](./mysql-index.md)。 -**7.性能有差别。** +**7、性能有差别。** InnoDB 的性能比 MyISAM 更强大,不管是在读写混合模式下还是只读模式下,随着 CPU 核数的增加,InnoDB 的读写能力呈线性增长。MyISAM 因为读写不能并发,它的处理能力跟核数没关系。 ![InnoDB 和 MyISAM 性能对比](https://oss.javaguide.cn/github/javaguide/mysql/innodb-myisam-performance-comparison.png) +**8、数据缓存策略和机制实现不同。** + +InnoDB 使用缓冲池(Buffer Pool)缓存数据页和索引页,MyISAM 使用键缓存(Key Cache)仅缓存索引页而不缓存数据页。 + **总结**: - InnoDB 支持行级别的锁粒度,MyISAM 不支持,只支持表级别的锁粒度。 @@ -225,15 +381,13 @@ InnoDB 的性能比 MyISAM 更强大,不管是在读写混合模式下还是 ### MyISAM 和 InnoDB 如何选择? -大多数时候我们使用的都是 InnoDB 存储引擎,在某些读密集的情况下,使用 MyISAM 也是合适的。不过,前提是你的项目不介意 MyISAM 不支持事务、崩溃恢复等缺点(可是~我们一般都会介意啊!)。 +大多数时候我们使用的都是 InnoDB 存储引擎,在某些读密集的情况下,使用 MyISAM 也是合适的。不过,前提是你的项目不介意 MyISAM 不支持事务、崩溃恢复等缺点(可是~我们一般都会介意啊)。 《MySQL 高性能》上面有一句话这样写到: > 不要轻易相信“MyISAM 比 InnoDB 快”之类的经验之谈,这个结论往往不是绝对的。在很多我们已知场景中,InnoDB 的速度都可以让 MyISAM 望尘莫及,尤其是用到了聚簇索引,或者需要访问的数据都可以放入内存的应用。 -一般情况下我们选择 InnoDB 都是没有问题的,但是某些情况下你并不在乎可扩展能力和并发能力,也不需要事务支持,也不在乎崩溃后的安全恢复问题的话,选择 MyISAM 也是一个不错的选择。但是一般情况下,我们都是需要考虑到这些问题的。 - -因此,对于咱们日常开发的业务系统来说,你几乎找不到什么理由再使用 MyISAM 作为自己的 MySQL 数据库的存储引擎。 +因此,对于咱们日常开发的业务系统来说,你几乎找不到什么理由使用 MyISAM 了,老老实实用默认的 InnoDB 就可以了! ## MySQL 索引 @@ -241,7 +395,7 @@ MySQL 索引相关的问题比较多,对于面试和工作都比较重要, ## MySQL 查询缓存 -执行查询语句的时候,会先查询缓存。不过,MySQL 8.0 版本后移除,因为这个功能不太实用 +MySQL 查询缓存是查询结果缓存。执行查询语句的时候,会先查询缓存,如果缓存中有对应的查询结果,就会直接返回。 `my.cnf` 加入以下配置,重启 MySQL 开启查询缓存 @@ -257,7 +411,7 @@ set global query_cache_type=1; set global query_cache_size=600000; ``` -如上,**开启查询缓存后在同样的查询条件以及数据情况下,会直接在缓存中返回结果**。这里的查询条件包括查询本身、当前要查询的数据库、客户端协议版本号等一些可能影响结果的信息。 +查询缓存会在同样的查询条件和数据情况下,直接返回缓存中的结果。但需要注意的是,查询缓存的匹配条件非常严格,任何细微的差异都会导致缓存无法命中。这里的查询条件包括查询语句本身、当前使用的数据库、以及其他可能影响结果的因素,如客户端协议版本号等。 **查询缓存不命中的情况:** @@ -265,14 +419,20 @@ set global query_cache_size=600000; 2. 如果查询中包含任何用户自定义函数、存储函数、用户变量、临时表、MySQL 库中的系统表,其查询结果也不会被缓存。 3. 缓存建立之后,MySQL 的查询缓存系统会跟踪查询中涉及的每张表,如果这些表(数据或结构)发生变化,那么和这张表相关的所有缓存数据都将失效。 -**缓存虽然能够提升数据库的查询性能,但是缓存同时也带来了额外的开销,每次查询后都要做一次缓存操作,失效后还要销毁。** 因此,开启查询缓存要谨慎,尤其对于写密集的应用来说更是如此。如果开启,要注意合理控制缓存空间大小,一般来说其大小设置为几十 MB 比较合适。此外,**还可以通过 `sql_cache` 和 `sql_no_cache` 来控制某个查询语句是否需要缓存:** +**缓存虽然能够提升数据库的查询性能,但是缓存同时也带来了额外的开销,每次查询后都要做一次缓存操作,失效后还要销毁。** 因此,开启查询缓存要谨慎,尤其对于写密集的应用来说更是如此。如果开启,要注意合理控制缓存空间大小,一般来说其大小设置为几十 MB 比较合适。此外,还可以通过 `sql_cache` 和 `sql_no_cache` 来控制某个查询语句是否需要缓存: ```sql SELECT sql_no_cache COUNT(*) FROM usr; ``` +MySQL 5.6 开始,查询缓存已默认禁用。MySQL 8.0 开始,已经不再支持查询缓存了(具体可以参考这篇文章:[MySQL 8.0: Retiring Support for the Query Cache](https://dev.mysql.com/blog-archive/mysql-8-0-retiring-support-for-the-query-cache/))。 + +![MySQL 8.0: Retiring Support for the Query Cache](https://oss.javaguide.cn/github/javaguide/mysql/mysql8.0-retiring-support-for-the-query-cache.png) + ## MySQL 日志 +MySQL 日志常见的面试题有: + - MySQL 中常见的日志有哪些? - 慢查询日志有什么用? - binlog 主要记录了什么? @@ -280,7 +440,7 @@ SELECT sql_no_cache COUNT(*) FROM usr; - 页修改之后为什么不直接刷盘呢? - binlog 和 redolog 有什么区别? - undo log 如何保证事务的原子性? -- ...... +- …… 上诉问题的答案可以在[《Java 面试指北》(付费)](../../zhuanlan/java-mian-shi-zhi-bei.md) 的 **「技术面试题篇」** 中找到。 @@ -295,7 +455,7 @@ SELECT sql_no_cache COUNT(*) FROM usr; - 数据库中途突然因为某些原因挂掉了。 - 客户端突然因为网络原因连接不上数据库了。 - 并发访问数据库时,多个线程同时写入数据库,覆盖了彼此的更改。 -- ...... +- …… 上面的任何一个问题都可能会导致数据的不一致性。为了保证数据的一致性,系统必须能够处理这些问题。事务就是我们抽象出来简化这些问题的首选机制。事务的概念起源于数据库,目前,已经成为一个比较广泛的概念。 @@ -365,7 +525,7 @@ COMMIT; 一个事务读取数据并且对数据进行了修改,这个修改对其他事务来说是可见的,即使当前事务没有提交。这时另外一个事务读取了这个还未提交的数据,但第一个事务突然回滚,导致数据并没有被提交到数据库,那第二个事务读取到的就是脏数据,这也就是脏读的由来。 -例如:事务 1 读取某表中的数据 A=20,事务 1 修改 A=A-1,事务 2 读取到 A = 19,事务 1 回滚导致对 A 的修改并为提交到数据库, A 的值还是 20。 +例如:事务 1 读取某表中的数据 A=20,事务 1 修改 A=A-1,事务 2 读取到 A = 19,事务 1 回滚导致对 A 的修改并未提交到数据库, A 的值还是 20。 ![脏读](./images/concurrency-consistency-issues-dirty-reading.png) @@ -398,7 +558,7 @@ COMMIT; - 不可重复读的重点是内容修改或者记录减少比如多次读取一条记录发现其中某些记录的值被修改; - 幻读的重点在于记录新增比如多次执行同一条查询语句(DQL)时,发现查到的记录增加了。 -幻读其实可以看作是不可重复读的一种特殊情况,单独把区分幻读的原因主要是解决幻读和不可重复读的方案不一样。 +幻读其实可以看作是不可重复读的一种特殊情况,单独把幻读区分出来的原因主要是解决幻读和不可重复读的方案不一样。 举个例子:执行 `delete` 和 `update` 操作的时候,可以直接对记录加锁,保证事务安全。而执行 `insert` 操作的时候,由于记录锁(Record Lock)只能锁住已经存在的记录,为了避免插入新记录,需要依赖间隙锁(Gap Lock)。也就是说执行 `insert` 操作的时候需要依赖 Next-Key Lock(Record Lock+Gap Lock) 进行加锁来保证不出现幻读。 @@ -406,7 +566,7 @@ COMMIT; MySQL 中并发事务的控制方式无非就两种:**锁** 和 **MVCC**。锁可以看作是悲观控制的模式,多版本并发控制(MVCC,Multiversion concurrency control)可以看作是乐观控制的模式。 -**锁** 控制方式下会通过锁来显示控制共享资源而不是通过调度手段,MySQL 中主要是通过 **读写锁** 来实现并发控制。 +**锁** 控制方式下会通过锁来显式控制共享资源而不是通过调度手段,MySQL 中主要是通过 **读写锁** 来实现并发控制。 - **共享锁(S 锁)**:又称读锁,事务在读取记录的时候获取共享锁,允许多个事务同时获取(锁兼容)。 - **排他锁(X 锁)**:又称写锁/独占锁,事务在修改记录的时候获取排他锁,不允许多个事务同时获取。如果一个记录已经被加了排他锁,那其他事务不能再对这条记录加任何类型的锁(锁不兼容)。 @@ -424,31 +584,26 @@ MVCC 在 MySQL 中实现所依赖的手段主要是: **隐藏字段、read view ### SQL 标准定义了哪些事务隔离级别? -SQL 标准定义了四个隔离级别: - -- **READ-UNCOMMITTED(读取未提交)**:最低的隔离级别,允许读取尚未提交的数据变更,可能会导致脏读、幻读或不可重复读。 -- **READ-COMMITTED(读取已提交)**:允许读取并发事务已经提交的数据,可以阻止脏读,但是幻读或不可重复读仍有可能发生。 -- **REPEATABLE-READ(可重复读)**:对同一字段的多次读取结果都是一致的,除非数据是被本身事务自己所修改,可以阻止脏读和不可重复读,但幻读仍有可能发生。 -- **SERIALIZABLE(可串行化)**:最高的隔离级别,完全服从 ACID 的隔离级别。所有的事务依次逐个执行,这样事务之间就完全不可能产生干扰,也就是说,该级别可以防止脏读、不可重复读以及幻读。 - ---- - -| 隔离级别 | 脏读 | 不可重复读 | 幻读 | -| :--------------: | :--: | :--------: | :--: | -| READ-UNCOMMITTED | √ | √ | √ | -| READ-COMMITTED | × | √ | √ | -| REPEATABLE-READ | × | × | √ | -| SERIALIZABLE | × | × | × | - -### MySQL 的隔离级别是基于锁实现的吗? +SQL 标准定义了四种事务隔离级别,用来平衡事务的隔离性(Isolation)和并发性能。级别越高,数据一致性越好,但并发性能可能越低。这四个级别是: -MySQL 的隔离级别基于锁和 MVCC 机制共同实现的。 +- **READ-UNCOMMITTED(读取未提交)** :最低的隔离级别,允许读取尚未提交的数据变更,可能会导致脏读、幻读或不可重复读。这种级别在实际应用中很少使用,因为它对数据一致性的保证太弱。 +- **READ-COMMITTED(读取已提交)** :允许读取并发事务已经提交的数据,可以阻止脏读,但是幻读或不可重复读仍有可能发生。这是大多数数据库(如 Oracle, SQL Server)的默认隔离级别。 +- **REPEATABLE-READ(可重复读)** :对同一字段的多次读取结果都是一致的,除非数据是被本身事务自己所修改,可以阻止脏读和不可重复读,但幻读仍有可能发生。MySQL InnoDB 存储引擎的默认隔离级别正是 REPEATABLE READ。并且,InnoDB 在此级别下通过 MVCC(多版本并发控制) 和 Next-Key Locks(间隙锁+行锁) 机制,在很大程度上解决了幻读问题。 +- **SERIALIZABLE(可串行化)** :最高的隔离级别,完全服从 ACID 的隔离级别。所有的事务依次逐个执行,这样事务之间就完全不可能产生干扰,也就是说,该级别可以防止脏读、不可重复读以及幻读。 -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; @@ -461,6 +616,12 @@ mysql> SELECT @@tx_isolation; 关于 MySQL 事务隔离级别的详细介绍,可以看看我写的这篇文章:[MySQL 事务隔离级别详解](./transaction-isolation-level.md)。 +### MySQL 的隔离级别是基于锁实现的吗? + +MySQL 的隔离级别基于锁和 MVCC 机制共同实现的。 + +SERIALIZABLE 隔离级别是通过锁来实现的,READ-COMMITTED 和 REPEATABLE-READ 隔离级别是基于 MVCC 实现的。不过, SERIALIZABLE 之外的其他隔离级别可能也需要用到锁机制,就比如 REPEATABLE-READ 在当前读情况下需要使用加锁读来保证不会出现幻读。 + ## MySQL 锁 锁是一种常见的并发事务的控制方式。 @@ -486,7 +647,7 @@ InnoDB 的行锁是针对索引字段加的锁,表级锁是针对非索引字 InnoDB 行锁是通过对索引数据页上的记录加锁实现的,MySQL InnoDB 支持三种行锁定方式: -- **记录锁(Record Lock)**:也被称为记录锁,属于单个行记录上的锁。 +- **记录锁(Record Lock)**:属于单个行记录上的锁。 - **间隙锁(Gap Lock)**:锁定一个范围,不包括记录本身。 - **临键锁(Next-Key Lock)**:Record Lock+Gap Lock,锁定一个范围,包含记录本身,主要目的是为了解决幻读问题(MySQL 事务部分提到过)。记录锁只能锁住已经存在的记录,为了避免插入新记录,需要依赖间隙锁。 @@ -511,8 +672,10 @@ InnoDB 行锁是通过对索引数据页上的记录加锁实现的,MySQL Inno 由于 MVCC 的存在,对于一般的 `SELECT` 语句,InnoDB 不会加任何锁。不过, 你可以通过以下语句显式加共享锁或排他锁。 ```sql -# 共享锁 +# 共享锁 可以在 MySQL 5.7 和 MySQL 8.0 中使用 SELECT ... LOCK IN SHARE MODE; +# 共享锁 可以在 MySQL 8.0 中使用 +SELECT ... FOR SHARE; # 排他锁 SELECT ... FOR UPDATE; ``` @@ -526,7 +689,7 @@ SELECT ... FOR UPDATE; - **意向共享锁(Intention Shared Lock,IS 锁)**:事务有意向对表中的某些记录加共享锁(S 锁),加共享锁前必须先取得该表的 IS 锁。 - **意向排他锁(Intention Exclusive Lock,IX 锁)**:事务有意向对表中的某些记录加排他锁(X 锁),加排他锁之前必须先取得该表的 IX 锁。 -**意向锁是由数据引擎自己维护的,用户无法手动操作意向锁,在为数据行加共享/排他锁之前,InooDB 会先获取该数据行所在在数据表的对应意向锁。** +**意向锁是由数据引擎自己维护的,用户无法手动操作意向锁,在为数据行加共享/排他锁之前,InnoDB 会先获取该数据行所在在数据表的对应意向锁。** 意向锁之间是互相兼容的。 @@ -552,7 +715,10 @@ SELECT ... FOR UPDATE; ```sql SELECT ... FOR UPDATE -SELECT ... LOCK IN SHARE MODE +# 共享锁 可以在 MySQL 5.7 和 MySQL 8.0 中使用 +SELECT ... LOCK IN SHARE MODE; +# 共享锁 可以在 MySQL 8.0 中使用 +SELECT ... FOR SHARE; ``` 快照即记录的历史版本,每行记录可能存在多个历史版本(多版本技术)。 @@ -575,6 +741,8 @@ SELECT ... LOCK IN SHARE MODE SELECT...FOR UPDATE # 对读的记录加一个S锁 SELECT...LOCK IN SHARE MODE +# 对读的记录加一个S锁 +SELECT...FOR SHARE # 对修改的记录加一个X锁 INSERT... UPDATE... @@ -589,8 +757,8 @@ DELETE... ```sql CREATE TABLE `sequence_id` ( - `id` bigint(20) unsigned NOT NULL AUTO_INCREMENT, - `stub` char(10) NOT NULL DEFAULT '', + `id` BIGINT(20) UNSIGNED NOT NULL AUTO_INCREMENT, + `stub` CHAR(10) NOT NULL DEFAULT '', PRIMARY KEY (`id`), UNIQUE KEY `stub` (`stub`) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4; @@ -694,6 +862,53 @@ mysql> EXPLAIN SELECT `score`,`name` FROM `cus_order` ORDER BY `score` DESC; 读写分离和分库分表相关的问题比较多,于是,我单独写了一篇文章来介绍:[读写分离和分库分表详解](../../high-performance/read-and-write-separation-and-library-subtable.md)。 +### 深度分页如何优化? + +[深度分页介绍及优化建议](../../high-performance/deep-pagination-optimization.md) + +### 数据冷热分离如何做? + +[数据冷热分离详解](../../high-performance/data-cold-hot-separation.md) + +### MySQL 性能怎么优化? + +MySQL 性能优化是一个系统性工程,涉及多个方面,在面试中不可能面面俱到。因此,建议按照“点-线-面”的思路展开,从核心问题入手,再逐步扩展,展示出你对问题的思考深度和解决能力。 + +**1. 抓住核心:慢 SQL 定位与分析** + +性能优化的第一步永远是找到瓶颈。面试时,建议先从 **慢 SQL 定位和分析** 入手,这不仅能展示你解决问题的思路,还能体现你对数据库性能监控的熟练掌握: + +- **监控工具:** 介绍常用的慢 SQL 监控工具,如 **MySQL 慢查询日志**、**Performance Schema** 等,说明你对这些工具的熟悉程度以及如何通过它们定位问题。 +- **EXPLAIN 命令:** 详细说明 `EXPLAIN` 命令的使用,分析查询计划、索引使用情况,可以结合实际案例展示如何解读分析结果,比如执行顺序、索引使用情况、全表扫描等。 + +**2. 由点及面:索引、表结构和 SQL 优化** + +定位到慢 SQL 后,接下来就要针对具体问题进行优化。 这里可以重点介绍索引、表结构和 SQL 编写规范等方面的优化技巧: + +- **索引优化:** 这是 MySQL 性能优化的重点,可以介绍索引的创建原则、覆盖索引、最左前缀匹配原则等。如果能结合你项目的实际应用来说明如何选择合适的索引,会更加分一些。 +- **表结构优化:** 优化表结构设计,包括选择合适的字段类型、避免冗余字段、合理使用范式和反范式设计等等。 +- **SQL 优化:** 避免使用 `SELECT *`、尽量使用具体字段、使用连接查询代替子查询、合理使用分页查询、批量操作等,都是 SQL 编写过程中需要注意的细节。 + +**3. 进阶方案:架构优化** + +当面试官对基础优化知识比较满意时,可能会深入探讨一些架构层面的优化方案。以下是一些常见的架构优化策略: + +- **读写分离:** 将读操作和写操作分离到不同的数据库实例,提升数据库的并发处理能力。 +- **分库分表:** 将数据分散到多个数据库实例或数据表中,降低单表数据量,提升查询效率。但要权衡其带来的复杂性和维护成本,谨慎使用。 +- **数据冷热分离**:根据数据的访问频率和业务重要性,将数据分为冷数据和热数据,冷数据一般存储在低成本、低性能的介质中,热数据存储在高性能存储介质中。 +- **缓存机制:** 使用 Redis 等缓存中间件,将热点数据缓存到内存中,减轻数据库压力。这个非常常用,提升效果非常明显,性价比极高! + +**4. 其他优化手段** + +除了慢 SQL 定位、索引优化和架构优化,还可以提及一些其他优化手段,展示你对 MySQL 性能调优的全面理解: + +- **连接池配置:** 配置合理的数据库连接池(如 **连接池大小**、**超时时间** 等),能够有效提升数据库连接的效率,避免频繁的连接开销。 +- **硬件配置:** 提升硬件性能也是优化的重要手段之一。使用高性能服务器、增加内存、使用 **SSD** 硬盘等硬件升级,都可以有效提升数据库的整体性能。 + +**5.总结** + +在面试中,建议按优先级依次介绍慢 SQL 定位、[索引优化](./mysql-index.md)、表结构设计和 [SQL 优化](../../high-performance/sql-optimization.md)等内容。架构层面的优化,如[读写分离和分库分表](../../high-performance/read-and-write-separation-and-library-subtable.md)、[数据冷热分离](../../high-performance/data-cold-hot-separation.md) 应作为最后的手段,除非在特定场景下有明显的性能瓶颈,否则不应轻易使用,因其引入的复杂性会带来额外的维护成本。 + ## MySQL 学习资料推荐 [**书籍推荐**](../../books/database.md#mysql) 。 @@ -712,6 +927,7 @@ mysql> EXPLAIN SELECT `score`,`name` FROM `cus_order` ORDER BY `score` DESC; - 《高性能 MySQL》第 7 章 MySQL 高级特性 - 《MySQL 技术内幕 InnoDB 存储引擎》第 6 章 锁 - Relational Database: +- 一篇文章看懂 mysql 中 varchar 能存多少汉字、数字,以及 varchar(100)和 varchar(10)的区别: - 技术分享 | 隔离级别:正确理解幻读: - MySQL Server Logs - MySQL 5.7 Reference Manual: - Redo Log - MySQL 5.7 Reference Manual: @@ -720,3 +936,5 @@ mysql> EXPLAIN SELECT `score`,`name` FROM `cus_order` ORDER BY `score` DESC; - 详解 MySQL InnoDB 中意向锁的作用: - 深入剖析 MySQL 自增锁: - 在数据库中不可重复读和幻读到底应该怎么分?: + + diff --git a/docs/database/mysql/some-thoughts-on-database-storage-time.md b/docs/database/mysql/some-thoughts-on-database-storage-time.md index e295fb44f4b..e22ce2800da 100644 --- a/docs/database/mysql/some-thoughts-on-database-storage-time.md +++ b/docs/database/mysql/some-thoughts-on-database-storage-time.md @@ -1,34 +1,45 @@ --- -title: MySQL时间类型数据存储建议 +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+ 支持更高精度的小数秒)的时间值。那么,在实际应用中,我们应该如何在这两者之间做出选择呢? -**通常我们都会首选 Timestamp。** 下面说一下为什么这样做! +下面我们从几个关键维度对它们进行对比: -### DateTime 类型没有时区信息 +### 时区信息 -**DateTime 类型是没有时区信息的(时区无关)** ,DateTime 类型保存的时间都是当前会话所设置的时区对应的时间。这样就会有什么问题呢?当你的时区更换之后,比如你的服务器更换地址或者更换客户端连接时区设置的话,就会导致你从数据库中读出的时间错误。不要小看这个问题,很多系统就是因为这个问题闹出了很多笑话。 +`DATETIME` 类型存储的是**字面量的日期和时间值**,它本身**不包含任何时区信息**。当你插入一个 `DATETIME` 值时,MySQL 存储的就是你提供的那个确切的时间,不会进行任何时区转换。 -**Timestamp 和时区有关**。Timestamp 类型字段的值会随着服务器时区的变化而变化,自动换算成相应的时间,说简单点就是在不同时区,查询到同一个条记录此字段的值会不一样。 +**这样就会有什么问题呢?** 如果你的应用需要支持多个时区,或者服务器、客户端的时区可能发生变化,那么使用 `DATETIME` 时,应用程序需要自行处理时区的转换和解释。如果处理不当(例如,假设所有存储的时间都属于同一个时区,但实际环境变化了),可能会导致时间显示或计算上的混乱。 + +**`TIMESTAMP` 和时区有关**。存储时,MySQL 会将当前会话时区下的时间值转换成 UTC(协调世界时)进行内部存储。当查询 `TIMESTAMP` 字段时,MySQL 又会将存储的 UTC 时间转换回当前会话所设置的时区来显示。 + +这意味着,对于同一条记录的 `TIMESTAMP` 字段,在不同的会话时区设置下查询,可能会看到不同的本地时间表示,但它们都对应着同一个绝对时间点(UTC 时间)。这对于需要全球化、多时区支持的应用来说非常有用。 下面实际演示一下! @@ -43,21 +54,21 @@ 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; ``` 结果: -``` +```plain +---------------------+---------------------+ | date_time | time_stamp | +---------------------+---------------------+ @@ -65,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'; ``` -再次查看数据: +再次查询数据: -``` +```bash +# TIMESTAMP 的值自动转换为 UTC+8 时间 +---------------------+---------------------+ | date_time | time_stamp | +---------------------+---------------------+ @@ -83,7 +93,7 @@ set time_zone='+8:00'; +---------------------+---------------------+ ``` -**扩展:一些关于 MySQL 时区设置的一个常用 sql 命令** +**扩展:MySQL 时区设置常用 SQL 命令** ```sql # 查看当前会话时区 @@ -98,30 +108,32 @@ SET GLOBAL time_zone = '+8:00'; SET GLOBAL time_zone = 'Europe/Helsinki'; ``` -### DateTime 类型耗费空间更大 +### 占用空间 -Timestamp 只需要使用 4 个字节的存储空间,但是 DateTime 需要耗费 8 个字节的存储空间。但是,这样同样造成了一个问题,Timestamp 表示的时间范围更小。 +下图是 MySQL 日期类型所占的存储空间(官方文档传送门:): -- 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 +![](https://oss.javaguide.cn/github/javaguide/FhRGUVHFK0ujRPNA75f6CuOXQHTE.jpeg) -> Timestamp 在不同版本的 MySQL 中有细微差别。 +在 MySQL 5.6.4 之前,DateTime 和 TIMESTAMP 的存储空间是固定的,分别为 8 字节和 4 字节。但是从 MySQL 5.6.4 开始,它们的存储空间会根据毫秒精度的不同而变化,DateTime 的范围是 5~8 字节,TIMESTAMP 的范围是 4~7 字节。 -## 再看 MySQL 日期类型存储空间 +### 表示范围 -下图是 MySQL 5.6 版本中日期类型所占的存储空间: +`TIMESTAMP` 表示的时间范围更小,只能到 2038 年: -![](https://oss.javaguide.cn/github/javaguide/FhRGUVHFK0ujRPNA75f6CuOXQHTE.jpeg) +- `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 -可以看出 5.6.4 之后的 MySQL 多出了一个需要 0 ~ 3 字节的小数位。DateTime 和 Timestamp 会有几种不同的存储空间占用。 +### 性能 -为了方便,本文我们还是默认 Timestamp 只需要使用 4 个字节的存储空间,但是 DateTime 需要耗费 8 个字节的存储空间。 +由于 `TIMESTAMP` 在存储和检索时需要进行 UTC 与当前会话时区的转换,这个过程可能涉及到额外的计算开销,尤其是在需要调用操作系统底层接口获取或处理时区信息时。虽然现代数据库和操作系统对此进行了优化,但在某些极端高并发或对延迟极其敏感的场景下,`DATETIME` 因其不涉及时区转换,处理逻辑相对更简单直接,可能会表现出微弱的性能优势。 -## 数值型时间戳是更好的选择吗? +为了获得可预测的行为并可能减少 `TIMESTAMP` 的转换开销,推荐的做法是在应用程序层面统一管理时区,或者在数据库连接/会话级别显式设置 `time_zone` 参数,而不是依赖服务器的默认或操作系统时区。 -很多时候,我们也会使用 int 或者 bigint 类型的数值也就是时间戳来表示时间。 +## 数值时间戳是更好的选择吗? -这种存储方式的具有 Timestamp 类型的所具有一些优点,并且使用它的进行日期排序以及对比等操作的效率会更高,跨系统也很方便,毕竟只是存放的数值。缺点也很明显,就是数据的可读性太差了,你无法直观的看到具体时间。 +除了上述两种类型,实践中也常用整数类型(`INT` 或 `BIGINT`)来存储所谓的“Unix 时间戳”(即从 1970 年 1 月 1 日 00:00:00 UTC 起至目标时间的总秒数,或毫秒数)。 + +这种存储方式的具有 `TIMESTAMP` 类型的所具有一些优点,并且使用它的进行日期排序以及对比等操作的效率会更高,跨系统也很方便,毕竟只是存放的数值。缺点也很明显,就是数据的可读性太差了,你无法直观的看到具体时间。 时间戳的定义如下: @@ -130,7 +142,8 @@ Timestamp 只需要使用 4 个字节的存储空间,但是 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') | +---------------------------------------+ @@ -138,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) | +---------------------------+ @@ -147,14 +161,41 @@ 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,原因是数值表示时间不够直观。下面是原文: -每种方式都有各自的优势,根据实际场景才是王道。下面再对这三种方式做一个简单的对比,以供大家实际开发中选择正确的存放时间的数据类型: +每种方式都有各自的优势,根据实际场景选择最合适的才是王道。下面再对这三种方式做一个简单的对比,以供大家实际开发中选择正确的存放时间的数据类型: + +| 类型 | 存储空间 | 日期格式 | 日期范围 | 是否带时区信息 | +| ------------ | -------- | ------------------------------ | ------------------------------------------------------------ | -------------- | +| 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 之后的时间 | 否 | + +**选择建议小结:** + +- `TIMESTAMP` 的核心优势在于其内建的时区处理能力。数据库负责 UTC 存储和基于会话时区的自动转换,简化了需要处理多时区应用的开发。如果应用需要处理多时区,或者希望数据库能自动管理时区转换,`TIMESTAMP` 是自然的选择(注意其时间范围限制,也就是 2038 年问题)。 +- 如果应用场景不涉及时区转换,或者希望应用程序完全控制时区逻辑,并且需要表示 2038 年之后的时间,`DATETIME` 是更稳妥的选择。 +- 如果极度关注比较性能,或者需要频繁跨系统传递时间数据,并且可以接受可读性的牺牲(或总是在应用层转换),数值时间戳是一个强大的选项。 -![](https://my-blog-to-use.oss-cn-beijing.aliyuncs.com/2019-11/总结-常用日期存储方式.jpg) + diff --git a/docs/database/mysql/transaction-isolation-level.md b/docs/database/mysql/transaction-isolation-level.md index 647b80ee6eb..8b706640ea6 100644 --- a/docs/database/mysql/transaction-isolation-level.md +++ b/docs/database/mysql/transaction-isolation-level.md @@ -11,43 +11,46 @@ tag: ## 事务隔离级别总结 -SQL 标准定义了四个隔离级别: - -- **READ-UNCOMMITTED(读取未提交)**:最低的隔离级别,允许读取尚未提交的数据变更,可能会导致脏读、幻读或不可重复读。 -- **READ-COMMITTED(读取已提交)**:允许读取并发事务已经提交的数据,可以阻止脏读,但是幻读或不可重复读仍有可能发生。 -- **REPEATABLE-READ(可重复读)**:对同一字段的多次读取结果都是一致的,除非数据是被本身事务自己所修改,可以阻止脏读和不可重复读,但幻读仍有可能发生。 -- **SERIALIZABLE(可串行化)**:最高的隔离级别,完全服从 ACID 的隔离级别。所有的事务依次逐个执行,这样事务之间就完全不可能产生干扰,也就是说,该级别可以防止脏读、不可重复读以及幻读。 - ---- - -| 隔离级别 | 脏读 | 不可重复读 | 幻读 | -| :--------------: | :--: | :--------: | :--: | -| READ-UNCOMMITTED | √ | √ | √ | -| READ-COMMITTED | × | √ | √ | -| REPEATABLE-READ | × | × | √ | -| SERIALIZABLE | × | × | × | - -MySQL InnoDB 存储引擎的默认支持的隔离级别是 **REPEATABLE-READ(可重读)**。我们可以通过`SELECT @@tx_isolation;`命令来查看,MySQL 8.0 该命令改为`SELECT @@transaction_isolation;` - -```sql -MySQL> SELECT @@tx_isolation; -+-----------------+ -| @@tx_isolation | -+-----------------+ -| REPEATABLE-READ | -+-----------------+ +SQL 标准定义了四种事务隔离级别,用来平衡事务的隔离性(Isolation)和并发性能。级别越高,数据一致性越好,但并发性能可能越低。这四个级别是: + +- **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 | × | × | × | + +**默认级别查询:** + +MySQL InnoDB 存储引擎的默认隔离级别是 **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 章这样写到: @@ -111,3 +114,5 @@ SQL 脚本 1 在第一次查询工资为 500 的记录时只有一条,SQL 脚 - - [Mysql 锁:灵魂七拷问](https://tech.youzan.com/seven-questions-about-the-lock-of-MySQL/) - [Innodb 中的事务隔离级别和锁的关系](https://tech.meituan.com/2014/08/20/innodb-lock.html) + + diff --git a/docs/database/nosql.md b/docs/database/nosql.md index fd70056fd2c..d5ca59698bd 100644 --- a/docs/database/nosql.md +++ b/docs/database/nosql.md @@ -57,3 +57,5 @@ NoSQL 数据库主要可以分为下面四种类型: - NoSQL 是什么?- MongoDB 官方文档: - 什么是 NoSQL? - AWS: - NoSQL vs. SQL Databases - MongoDB 官方文档: + + diff --git a/docs/database/redis/3-commonly-used-cache-read-and-write-strategies.md b/docs/database/redis/3-commonly-used-cache-read-and-write-strategies.md index 91427109697..7ad88958704 100644 --- a/docs/database/redis/3-commonly-used-cache-read-and-write-strategies.md +++ b/docs/database/redis/3-commonly-used-cache-read-and-write-strategies.md @@ -7,7 +7,7 @@ tag: 看到很多小伙伴简历上写了“**熟练使用缓存**”,但是被我问到“**缓存常用的 3 种读写策略**”的时候却一脸懵逼。 -在我看来,造成这个问题的原因是我们在学习 Redis 的时候,可能只是简单了写一些 Demo,并没有去关注缓存的读写策略,或者说压根不知道这回事。 +在我看来,造成这个问题的原因是我们在学习 Redis 的时候,可能只是简单写了一些 Demo,并没有去关注缓存的读写策略,或者说压根不知道这回事。 但是,搞懂 3 种常见的缓存读写策略对于实际工作中使用缓存以及面试中被问到缓存都是非常有帮助的! @@ -114,3 +114,5 @@ Write Behind Pattern 和 Read/Write Through Pattern 很相似,两者都是由 这种策略在我们平时开发过程中也非常非常少见,但是不代表它的应用场景少,比如消息队列中消息的异步写入磁盘、MySQL 的 Innodb Buffer Pool 机制都用到了这种策略。 Write Behind Pattern 下 db 的写性能非常高,非常适合一些数据经常变化又对数据一致性要求没那么高的场景,比如浏览量、点赞量。 + + diff --git a/docs/database/redis/cache-basics.md b/docs/database/redis/cache-basics.md index 2a9aee57354..391e5bec82d 100644 --- a/docs/database/redis/cache-basics.md +++ b/docs/database/redis/cache-basics.md @@ -10,3 +10,5 @@ tag: ![](https://oss.javaguide.cn/javamianshizhibei/database-questions.png) + + diff --git a/docs/database/redis/redis-cluster.md b/docs/database/redis/redis-cluster.md index 2db4feda7e3..e3ef2efd04c 100644 --- a/docs/database/redis/redis-cluster.md +++ b/docs/database/redis/redis-cluster.md @@ -7,6 +7,8 @@ tag: **Redis 集群** 相关的面试题为我的 [知识星球](../../about-the-author/zhishixingqiu-two-years.md)(点击链接即可查看详细介绍以及加入方法)专属内容,已经整理到了[《Java 面试指北》](../../zhuanlan/java-mian-shi-zhi-bei.md)中。 -![](https://oss.javaguide.cn/github/javaguide/database/redis/redis-cluster-javamianshizhibei.png) +![](https://oss.javaguide.cn/xingqiu/mianshizhibei-database.png) + + diff --git a/docs/database/redis/redis-common-blocking-problems-summary.md b/docs/database/redis/redis-common-blocking-problems-summary.md index facf3e3ac9b..9aec17fc0cc 100644 --- a/docs/database/redis/redis-common-blocking-problems-summary.md +++ b/docs/database/redis/redis-common-blocking-problems-summary.md @@ -5,7 +5,7 @@ tag: - Redis --- -> 本文整理完善自:https://mp.weixin.qq.com/s/0Nqfq_eQrUb12QH6eBbHXA ,作者:阿 Q 说代码 +> 本文整理完善自: ,作者:阿 Q 说代码 这篇文章会详细总结一下可能导致 Redis 阻塞的情况,这些情况也是影响 Redis 性能的关键因素,使用 Redis 的时候应该格外注意! @@ -18,7 +18,7 @@ Redis 中的大部分命令都是 O(1)时间复杂度,但也有少部分 O(n) - `LRANGE`:会返回 List 中指定范围内的元素。 - `SMEMBERS`:返回 Set 中的所有元素。 - `SINTER`/`SUNION`/`SDIFF`:计算多个 Set 的交集/并集/差集。 -- ...... +- …… 由于这些命令时间复杂度是 O(n),有时候也会全表扫描,随着 n 的增大,执行耗时也会越长,从而导致客户端阻塞。不过, 这些命令并不是一定不能使用,但是需要明确 N 的值。另外,有遍历的需求可以使用 `HSCAN`、`SSCAN`、`ZSCAN` 代替。 @@ -26,7 +26,7 @@ Redis 中的大部分命令都是 O(1)时间复杂度,但也有少部分 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) 的时间复杂度更小。 -- ...... +- …… ## SAVE 创建 RDB 快照 @@ -81,7 +81,10 @@ Redis AOF 持久化机制是在执行完命令之后再记录日志,这和关 ## 大 Key -如果一个 key 对应的 value 所占用的内存比较大,那这个 key 就可以看作是 bigkey。具体多大才算大呢?有一个不是特别精确的参考标准:string 类型的 value 超过 10 kb,复合类型的 value 包含的元素超过 5000 个(对于复合类型的 value 来说,不一定包含的元素越多,占用的内存就越多)。 +如果一个 key 对应的 value 所占用的内存比较大,那这个 key 就可以看作是 bigkey。具体多大才算大呢?有一个不是特别精确的参考标准: + +- string 类型的 value 超过 1MB +- 复合类型(列表、哈希、集合、有序集合等)的 value 包含的元素超过 5000 个(对于复合类型的 value 来说,不一定包含的元素越多,占用的内存就越多)。 大 key 造成的阻塞问题如下: @@ -126,14 +129,14 @@ Redis 集群可以进行节点的动态扩容缩容,这一过程目前还处 **什么是 Swap?** Swap 直译过来是交换的意思,Linux 中的 Swap 常被称为内存交换或者交换分区。类似于 Windows 中的虚拟内存,就是当内存不足的时候,把一部分硬盘空间虚拟成内存使用,从而解决内存容量不足的情况。因此,Swap 分区的作用就是牺牲硬盘,增加内存,解决 VPS 内存不够用或者爆满的问题。 -Swap 对于 Redis 来说是非常致命的,Redis 保证高性能的一个重要前提是所有的数据在内存中。如果操作系统把 Redis 使用的部分内存换出硬盘,由于内存与硬盘读写的速度并几个数量级,会导致发生交换后的 Redis 性能急剧下降。 +Swap 对于 Redis 来说是非常致命的,Redis 保证高性能的一个重要前提是所有的数据在内存中。如果操作系统把 Redis 使用的部分内存换出硬盘,由于内存与硬盘的读写速度差几个数量级,会导致发生交换后的 Redis 性能急剧下降。 识别 Redis 发生 Swap 的检查方法如下: 1、查询 Redis 进程号 ```bash -reids-cli -p 6383 info server | grep process_id +redis-cli -p 6383 info server | grep process_id process_id: 4476 ``` @@ -161,7 +164,7 @@ Swap: 0kB Redis 是典型的 CPU 密集型应用,不建议和其他多核 CPU 密集型服务部署在一起。当其他进程过度消耗 CPU 时,将严重影响 Redis 的吞吐量。 -可以通过`reids-cli --stat`获取当前 Redis 使用情况。通过`top`命令获取进程对 CPU 的利用率等信息 通过`info commandstats`统计信息分析出命令不合理开销时间,查看是否是因为高算法复杂度或者过度的内存优化问题。 +可以通过`redis-cli --stat`获取当前 Redis 使用情况。通过`top`命令获取进程对 CPU 的利用率等信息 通过`info commandstats`统计信息分析出命令不合理开销时间,查看是否是因为高算法复杂度或者过度的内存优化问题。 ## 网络问题 @@ -169,5 +172,7 @@ Redis 是典型的 CPU 密集型应用,不建议和其他多核 CPU 密集型 ## 参考 -- Redis 阻塞的 6 大类场景分析与总结:https://mp.weixin.qq.com/s/eaZCEtTjTuEmXfUubVHjew -- Redis 开发与运维笔记-Redis 的噩梦-阻塞:https://mp.weixin.qq.com/s/TDbpz9oLH6ifVv6ewqgSgA +- Redis 阻塞的 6 大类场景分析与总结: +- Redis 开发与运维笔记-Redis 的噩梦-阻塞: + + diff --git a/docs/database/redis/redis-data-structures-01.md b/docs/database/redis/redis-data-structures-01.md index a8e0e1d742b..7d993752138 100644 --- a/docs/database/redis/redis-data-structures-01.md +++ b/docs/database/redis/redis-data-structures-01.md @@ -1,30 +1,30 @@ --- -title: Redis 5 种基本数据结构详解 +title: Redis 5 种基本数据类型详解 category: 数据库 tag: - Redis head: - - meta - name: keywords - content: Redis常见数据结构 + content: Redis常见数据类型 - - meta - name: description - content: Redis基础数据结构总结:String(字符串)、List(列表)、Set(集合)、Hash(散列)、Zset(有序集合) + content: Redis基础数据类型总结:String(字符串)、List(列表)、Set(集合)、Hash(散列)、Zset(有序集合) --- -Redis 共有 5 种基本数据结构:String(字符串)、List(列表)、Set(集合)、Hash(散列)、Zset(有序集合)。 +Redis 共有 5 种基本数据类型:String(字符串)、List(列表)、Set(集合)、Hash(散列)、Zset(有序集合)。 -这 5 种数据结构是直接提供给用户使用的,是数据的保存形式,其底层实现主要依赖这 8 种数据结构:简单动态字符串(SDS)、LinkedList(双向链表)、Hash Table(哈希表)、SkipList(跳跃表)、Intset(整数集合)、ZipList(压缩列表)、QuickList(快速列表)。 +这 5 种数据类型是直接提供给用户使用的,是数据的保存形式,其底层实现主要依赖这 8 种数据结构:简单动态字符串(SDS)、LinkedList(双向链表)、Dict(哈希表/字典)、SkipList(跳跃表)、Intset(整数集合)、ZipList(压缩列表)、QuickList(快速列表)。 -Redis 基本数据结构的底层数据结构实现如下: +Redis 5 种基本数据类型对应的底层数据结构实现如下表所示: -| String | List | Hash | Set | Zset | -| :----- | :--------------------------- | :------------------ | :-------------- | :---------------- | -| SDS | LinkedList/ZipList/QuickList | Hash Table、ZipList | ZipList、Intset | ZipList、SkipList | +| String | List | Hash | Set | Zset | +| :----- | :--------------------------- | :------------ | :----------- | :---------------- | +| SDS | LinkedList/ZipList/QuickList | Dict、ZipList | Dict、Intset | ZipList、SkipList | -Redis 3.2 之前,List 底层实现是 LinkedList 或者 ZipList。 Redis 3.2 之后,引入了 LinkedList 和 ZipList 的结合 QuickList,List 的底层实现变为 QuickList。 +Redis 3.2 之前,List 底层实现是 LinkedList 或者 ZipList。 Redis 3.2 之后,引入了 LinkedList 和 ZipList 的结合 QuickList,List 的底层实现变为 QuickList。从 Redis 7.0 开始, ZipList 被 ListPack 取代。 -你可以在 Redis 官网上找到 Redis 数据结构非常详细的介绍: +你可以在 Redis 官网上找到 Redis 数据类型/结构非常详细的介绍: - [Redis Data Structures](https://redis.com/redis-enterprise/data-structures/) - [Redis Data types tutorial](https://redis.io/docs/manual/data-types/data-types-tutorial/) @@ -37,9 +37,9 @@ Redis 3.2 之前,List 底层实现是 LinkedList 或者 ZipList。 Redis 3.2 ### 介绍 -String 是 Redis 中最简单同时也是最常用的一个数据结构。 +String 是 Redis 中最简单同时也是最常用的一个数据类型。 -String 是一种二进制安全的数据结构,可以用来存储任何类型的数据比如字符串、整数、浮点数、图片(图片的 base64 编码或者解码或者图片的路径)、序列化后的对象。 +String 是一种二进制安全的数据类型,可以用来存储任何类型的数据比如字符串、整数、浮点数、图片(图片的 base64 编码或者解码或者图片的路径)、序列化后的对象。 ![](https://oss.javaguide.cn/github/javaguide/database/redis/image-20220719124403897.png) @@ -47,19 +47,19 @@ String 是一种二进制安全的数据结构,可以用来存储任何类型 ### 常用命令 -| 命令 | 介绍 | -| ------------------------------ | -------------------------------- | -| SET key value | 设置指定 key 的值 | -| SETNX key value | 只有在 key 不存在时设置 key 的值 | -| GET key | 获取指定 key 的值 | -| MSET key1 value1 key2 value2 … | 设置一个或多个指定 key 的值 | -| MGET key1 key2 ... | 获取一个或多个指定 key 的值 | -| STRLEN key | 返回 key 所储存的字符串值的长度 | -| INCR key | 将 key 中储存的数字值增一 | -| DECR key | 将 key 中储存的数字值减一 | -| EXISTS key | 判断指定 key 是否存在 | -| DEL key(通用) | 删除指定的 key | -| EXPIRE key seconds(通用) | 给指定 key 设置过期时间 | +| 命令 | 介绍 | +| ------------------------------- | -------------------------------- | +| SET key value | 设置指定 key 的值 | +| SETNX key value | 只有在 key 不存在时设置 key 的值 | +| GET key | 获取指定 key 的值 | +| MSET key1 value1 key2 value2 …… | 设置一个或多个指定 key 的值 | +| MGET key1 key2 ... | 获取一个或多个指定 key 的值 | +| STRLEN key | 返回 key 所储存的字符串值的长度 | +| INCR key | 将 key 中储存的数字值增一 | +| DECR key | 将 key 中储存的数字值减一 | +| EXISTS key | 判断指定 key 是否存在 | +| DEL key(通用) | 删除指定的 key | +| EXPIRE key seconds(通用) | 给指定 key 设置过期时间 | 更多 Redis String 命令以及详细使用指南,请查看 Redis 官网对应的介绍: 。 @@ -110,7 +110,7 @@ OK ```bash > EXPIRE key 60 (integer) 1 -> SETNX key 60 value # 设置值并设置过期时间 +> SETEX key 60 value # 设置值并设置过期时间 OK > TTL key (integer) 56 @@ -120,7 +120,7 @@ OK **需要存储常规数据的场景** -- 举例:缓存 session、token、图片地址、序列化后的对象(相比较于 Hash 存储更节省内存)。 +- 举例:缓存 Session、Token、图片地址、序列化后的对象(相比较于 Hash 存储更节省内存)。 - 相关命令:`SET`、`GET`。 **需要计数的场景** @@ -178,11 +178,11 @@ Redis 中的 List 其实就是链表数据结构的实现。我在 [线性数据 ```bash > RPUSH myList2 value1 value2 value3 (integer) 3 -> RPOP myList2 # 将 list的头部(最右边)元素取出 +> RPOP myList2 # 将 list的最右边的元素取出 "value3" ``` -我专门画了一个图方便大家理解 `RPUSH` , `LPOP` , `lpush` , `RPOP` 命令: +我专门画了一个图方便大家理解 `RPUSH` , `LPOP` , `LPUSH` , `RPOP` 命令: ![](https://oss.javaguide.cn/github/javaguide/database/redis/redis-list.png) @@ -218,7 +218,7 @@ Redis 中的 List 其实就是链表数据结构的实现。我在 [线性数据 **消息队列** -Redis List 数据结构可以用来做消息队列,只是功能过于简单且存在很多缺陷,不建议这样做。 +`List` 可以用来做消息队列,只是功能过于简单且存在很多缺陷,不建议这样做。 相对来说,Redis 5.0 新增加的一个数据结构 `Stream` 更适合做消息队列一些,只是功能依然非常简陋。和专业的消息队列相比,还是有很多欠缺的地方比如消息丢失和堆积问题不好解决。 @@ -474,7 +474,7 @@ value1 ![](https://oss.javaguide.cn/github/javaguide/database/redis/2021060714195385.png) -[《Java 面试指北》](https://www.yuque.com/docs/share/f37fc804-bfe6-4b0d-b373-9c462188fec7) 的「技术面试题篇」就有一篇文章详细介绍如何使用 Sorted Set 来设计制作一个排行榜。 +[《Java 面试指北》](https://javaguide.cn/zhuanlan/java-mian-shi-zhi-bei.html) 的「技术面试题篇」就有一篇文章详细介绍如何使用 Sorted Set 来设计制作一个排行榜。 ![](https://oss.javaguide.cn/github/javaguide/database/redis/image-20220719071115140.png) @@ -483,9 +483,21 @@ value1 - 举例:优先级任务队列。 - 相关命令:`ZRANGE` (从小到大排序)、 `ZREVRANGE` (从大到小排序)、`ZREVRANK` (指定元素排名)。 +## 总结 + +| 数据类型 | 说明 | +| -------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | +| String | 一种二进制安全的数据类型,可以用来存储任何类型的数据比如字符串、整数、浮点数、图片(图片的 base64 编码或者解码或者图片的路径)、序列化后的对象。 | +| List | Redis 的 List 的实现为一个双向链表,即可以支持反向查找和遍历,更方便操作,不过带来了部分额外的内存开销。 | +| Hash | 一个 String 类型的 field-value(键值对) 的映射表,特别适合用于存储对象,后续操作的时候,你可以直接修改这个对象中的某些字段的值。 | +| Set | 无序集合,集合中的元素没有先后顺序但都唯一,有点类似于 Java 中的 `HashSet` 。 | +| Zset | 和 Set 相比,Sorted Set 增加了一个权重参数 `score`,使得集合中的元素能够按 `score` 进行有序排列,还可以通过 `score` 的范围来获取元素的列表。有点像是 Java 中 `HashMap` 和 `TreeSet` 的结合体。 | + ## 参考 - Redis Data Structures: 。 - Redis Commands: 。 - Redis Data types tutorial: 。 - Redis 存储对象信息是用 Hash 还是 String : + + diff --git a/docs/database/redis/redis-data-structures-02.md b/docs/database/redis/redis-data-structures-02.md index 4a1c5ad1396..9e5fbcee59b 100644 --- a/docs/database/redis/redis-data-structures-02.md +++ b/docs/database/redis/redis-data-structures-02.md @@ -1,23 +1,29 @@ --- -title: Redis 3 种特殊数据结构详解 +title: Redis 3 种特殊数据类型详解 category: 数据库 tag: - Redis head: - - meta - name: keywords - content: Redis常见数据结构 + content: Redis常见数据类型 - - meta - name: description - content: Redis特殊数据结构总结:HyperLogLogs(基数统计)、Bitmap (位存储)、Geospatial (地理位置)。 + content: Redis特殊数据类型总结:HyperLogLogs(基数统计)、Bitmap (位存储)、Geospatial (地理位置)。 --- -除了 5 种基本的数据结构之外,Redis 还支持 3 种特殊的数据结构:Bitmap、HyperLogLog、GEO。 +除了 5 种基本的数据类型之外,Redis 还支持 3 种特殊的数据类型:Bitmap、HyperLogLog、GEO。 -## Bitmap +## Bitmap (位图) ### 介绍 +根据官网介绍: + +> Bitmaps are not an actual data type, but a set of bit-oriented operations defined on the String type which is treated like a bit vector. Since strings are binary safe blobs and their maximum length is 512 MB, they are suitable to set up to 2^32 different bits. +> +> Bitmap 不是 Redis 中的实际数据类型,而是在 String 类型上定义的一组面向位的操作,将其视为位向量。由于字符串是二进制安全的块,且最大长度为 512 MB,它们适合用于设置最多 2^32 个不同的位。 + Bitmap 存储的是连续的二进制数字(0 和 1),通过 Bitmap, 只需要一个 bit 位来表示某个元素对应的值或者状态,key 就是对应元素本身 。我们知道 8 个 bit 可以组成一个 byte,所以 Bitmap 本身会极大的节省储存空间。 你可以将 Bitmap 看作是一个存储二进制数字(0 和 1)的数组,数组中每个元素的下标叫做 offset(偏移量)。 @@ -30,7 +36,7 @@ Bitmap 存储的是连续的二进制数字(0 和 1),通过 Bitmap, 只需 | ------------------------------------- | ---------------------------------------------------------------- | | SETBIT key offset value | 设置指定 offset 位置的值 | | GETBIT key offset | 获取指定 offset 位置的值 | -| BITCOUNT key start end | 获取 start 和 end 之前值为 1 的元素个数 | +| BITCOUNT key start end | 获取 start 和 end 之间值为 1 的元素个数 | | BITOP operation destkey key1 key2 ... | 对一个或多个 Bitmap 进行运算,可用运算符有 AND, OR, XOR 以及 NOT | **Bitmap 基本操作演示**: @@ -59,7 +65,7 @@ Bitmap 存储的是连续的二进制数字(0 和 1),通过 Bitmap, 只需 - 举例:用户签到情况、活跃用户情况、用户行为统计(比如是否点赞过某个视频)。 - 相关命令:`SETBIT`、`GETBIT`、`BITCOUNT`、`BITOP`。 -## HyperLogLog +## HyperLogLog(基数统计) ### 介绍 @@ -74,7 +80,7 @@ Redis 官方文档中有对应的详细说明: ![](https://oss.javaguide.cn/github/javaguide/database/redis/image-20220721091424563.png) -基数计数概率算法为了节省内存并不会直接存储元数据,而是通过一定的概率统计方法预估基数值(集合中包含元素的个数)。因此, HyperLogLog 的计数结果并不是一个精确值,存在一定的误差(标准误差为 `0.81%` 。)。 +基数计数概率算法为了节省内存并不会直接存储元数据,而是通过一定的概率统计方法预估基数值(集合中包含元素的个数)。因此, HyperLogLog 的计数结果并不是一个精确值,存在一定的误差(标准误差为 `0.81%` )。 ![](https://oss.javaguide.cn/github/javaguide/database/redis/image-20220720194154133.png) @@ -82,6 +88,8 @@ HyperLogLog 的使用非常简单,但原理非常复杂。HyperLogLog 的原 再推荐一个可以帮助理解 HyperLogLog 原理的工具:[Sketch of the Day: HyperLogLog — Cornerstone of a Big Data Infrastructure](http://content.research.neustar.biz/blog/hll.html) 。 +除了 HyperLogLog 之外,Redis 还提供了其他的概率数据结构,对应的官方文档地址: 。 + ### 常用命令 HyperLogLog 相关的命令非常少,最常用的也就 3 个。 @@ -115,12 +123,12 @@ HyperLogLog 相关的命令非常少,最常用的也就 3 个。 ### 应用场景 -**数量量巨大(百万、千万级别以上)的计数场景** +**数量巨大(百万、千万级别以上)的计数场景** -- 举例:热门网站每日/每周/每月访问 ip 数统计、热门帖子 uv 统计、 +- 举例:热门网站每日/每周/每月访问 ip 数统计、热门帖子 uv 统计。 - 相关命令:`PFADD`、`PFCOUNT` 。 -## Geospatial index +## Geospatial (地理位置) ### 介绍 @@ -201,8 +209,18 @@ user2 - 举例:附近的人。 - 相关命令: `GEOADD`、`GEORADIUS`、`GEORADIUSBYMEMBER` 。 +## 总结 + +| 数据类型 | 说明 | +| ---------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | +| Bitmap | 你可以将 Bitmap 看作是一个存储二进制数字(0 和 1)的数组,数组中每个元素的下标叫做 offset(偏移量)。通过 Bitmap, 只需要一个 bit 位来表示某个元素对应的值或者状态,key 就是对应元素本身 。我们知道 8 个 bit 可以组成一个 byte,所以 Bitmap 本身会极大的节省储存空间。 | +| HyperLogLog | Redis 提供的 HyperLogLog 占用空间非常非常小,只需要 12k 的空间就能存储接近`2^64`个不同元素。不过,HyperLogLog 的计数结果并不是一个精确值,存在一定的误差(标准误差为 `0.81%` )。 | +| Geospatial index | Geospatial index(地理空间索引,简称 GEO) 主要用于存储地理位置信息,基于 Sorted Set 实现。 | + ## 参考 -- Redis Data Structures:https://redis.com/redis-enterprise/data-structures/ 。 +- Redis Data Structures: 。 - 《Redis 深度历险:核心原理与应用实践》1.6 四两拨千斤——HyperLogLog -- 布隆过滤器,位图,HyperLogLog:https://hogwartsrico.github.io/2020/06/08/BloomFilter-HyperLogLog-BitMap/index.html +- 布隆过滤器,位图,HyperLogLog: + + diff --git a/docs/database/redis/redis-delayed-task.md b/docs/database/redis/redis-delayed-task.md new file mode 100644 index 00000000000..35f9304321f --- /dev/null +++ b/docs/database/redis/redis-delayed-task.md @@ -0,0 +1,82 @@ +--- +title: 如何基于Redis实现延时任务 +category: 数据库 +tag: + - Redis +--- + +基于 Redis 实现延时任务的功能无非就下面两种方案: + +1. Redis 过期事件监听 +2. Redisson 内置的延时队列 + +面试的时候,你可以先说自己考虑了这两种方案,但最后发现 Redis 过期事件监听这种方案存在很多问题,因此你最终选择了 Redisson 内置的 DelayedQueue 这种方案。 + +这个时候面试官可能会追问你一些相关的问题,我们后面会提到,提前准备就好了。 + +另外,除了下面介绍到的这些问题之外,Redis 相关的常见问题建议你都复习一遍,不排除面试官会顺带问你一些 Redis 的其他问题。 + +### Redis 过期事件监听实现延时任务功能的原理? + +Redis 2.0 引入了发布订阅 (pub/sub) 功能。在 pub/sub 中,引入了一个叫做 **channel(频道)** 的概念,有点类似于消息队列中的 **topic(主题)**。 + +pub/sub 涉及发布者(publisher)和订阅者(subscriber,也叫消费者)两个角色: + +- 发布者通过 `PUBLISH` 投递消息给指定 channel。 +- 订阅者通过`SUBSCRIBE`订阅它关心的 channel。并且,订阅者可以订阅一个或者多个 channel。 + +![Redis 发布订阅 (pub/sub) 功能](https://oss.javaguide.cn/github/javaguide/database/redis/redis-pub-sub.png) + +在 pub/sub 模式下,生产者需要指定消息发送到哪个 channel 中,而消费者则订阅对应的 channel 以获取消息。 + +Redis 中有很多默认的 channel,这些 channel 是由 Redis 本身向它们发送消息的,而不是我们自己编写的代码。其中,`__keyevent@0__:expired` 就是一个默认的 channel,负责监听 key 的过期事件。也就是说,当一个 key 过期之后,Redis 会发布一个 key 过期的事件到`__keyevent@__:expired`这个 channel 中。 + +我们只需要监听这个 channel,就可以拿到过期的 key 的消息,进而实现了延时任务功能。 + +这个功能被 Redis 官方称为 **keyspace notifications** ,作用是实时监控 Redis 键和值的变化。 + +### Redis 过期事件监听实现延时任务功能有什么缺陷? + +**1、时效性差** + +官方文档的一段介绍解释了时效性差的原因,地址: 。 + +![Redis 过期事件](https://oss.javaguide.cn/github/javaguide/database/redis/redis-timing-of-expired-events.png) + +这段话的核心是:过期事件消息是在 Redis 服务器删除 key 时发布的,而不是一个 key 过期之后就会就会直接发布。 + +我们知道常用的过期数据的删除策略就两个: + +1. **惰性删除**:只会在取出 key 的时候才对数据进行过期检查。这样对 CPU 最友好,但是可能会造成太多过期 key 没有被删除。 +2. **定期删除**:每隔一段时间抽取一批 key 执行删除过期 key 操作。并且,Redis 底层会通过限制删除操作执行的时长和频率来减少删除操作对 CPU 时间的影响。 + +定期删除对内存更加友好,惰性删除对 CPU 更加友好。两者各有千秋,所以 Redis 采用的是 **定期删除+惰性/懒汉式删除** 。 + +因此,就会存在我设置了 key 的过期时间,但到了指定时间 key 还未被删除,进而没有发布过期事件的情况。 + +**2、丢消息** + +Redis 的 pub/sub 模式中的消息并不支持持久化,这与消息队列不同。在 Redis 的 pub/sub 模式中,发布者将消息发送给指定的频道,订阅者监听相应的频道以接收消息。当没有订阅者时,消息会被直接丢弃,在 Redis 中不会存储该消息。 + +**3、多服务实例下消息重复消费** + +Redis 的 pub/sub 模式目前只有广播模式,这意味着当生产者向特定频道发布一条消息时,所有订阅相关频道的消费者都能够收到该消息。 + +这个时候,我们需要注意多个服务实例重复处理消息的问题,这会增加代码开发量和维护难度。 + +### Redisson 延迟队列原理是什么?有什么优势? + +Redisson 是一个开源的 Java 语言 Redis 客户端,提供了很多开箱即用的功能,比如多种分布式锁的实现、延时队列。 + +我们可以借助 Redisson 内置的延时队列 RDelayedQueue 来实现延时任务功能。 + +Redisson 的延迟队列 RDelayedQueue 是基于 Redis 的 SortedSet 来实现的。SortedSet 是一个有序集合,其中的每个元素都可以设置一个分数,代表该元素的权重。Redisson 利用这一特性,将需要延迟执行的任务插入到 SortedSet 中,并给它们设置相应的过期时间作为分数。 + +Redisson 定期使用 `zrangebyscore` 命令扫描 SortedSet 中过期的元素,然后将这些过期元素从 SortedSet 中移除,并将它们加入到就绪消息列表中。就绪消息列表是一个阻塞队列,有消息进入就会被消费者监听到。这样做可以避免消费者对整个 SortedSet 进行轮询,提高了执行效率。 + +相比于 Redis 过期事件监听实现延时任务功能,这种方式具备下面这些优势: + +1. **减少了丢消息的可能**:DelayedQueue 中的消息会被持久化,即使 Redis 宕机了,根据持久化机制,也只可能丢失一点消息,影响不大。当然了,你也可以使用扫描数据库的方法作为补偿机制。 +2. **消息不存在重复消费问题**:每个客户端都是从同一个目标队列中获取任务的,不存在重复消费的问题。 + +跟 Redisson 内置的延时队列相比,消息队列可以通过保障消息消费的可靠性、控制消息生产者和消费者的数量等手段来实现更高的吞吐量和更强的可靠性,实际项目中首选使用消息队列的延时消息这种方案。 diff --git a/docs/database/redis/redis-memory-fragmentation.md b/docs/database/redis/redis-memory-fragmentation.md index 799e2131acc..cb2da7476d1 100644 --- a/docs/database/redis/redis-memory-fragmentation.md +++ b/docs/database/redis/redis-memory-fragmentation.md @@ -19,7 +19,7 @@ Redis 内存碎片虽然不会影响 Redis 性能,但是会增加内存消耗 Redis 内存碎片产生比较常见的 2 个原因: -**1、Redis 存储存储数据的时候向操作系统申请的内存空间可能会大于数据实际需要的存储空间。** +**1、Redis 存储数据的时候向操作系统申请的内存空间可能会大于数据实际需要的存储空间。** 以下是这段 Redis 官方的原话: @@ -27,7 +27,7 @@ Redis 内存碎片产生比较常见的 2 个原因: Redis 使用 `zmalloc` 方法(Redis 自己实现的内存分配方法)进行内存分配的时候,除了要分配 `size` 大小的内存之外,还会多分配 `PREFIX_SIZE` 大小的内存。 -`zmalloc` 方法源码如下(源码地址:https://github.com/antirez/redis-tools/blob/master/zmalloc.c): +`zmalloc` 方法源码如下(源码地址: ```java void *zmalloc(size_t size) { @@ -45,7 +45,7 @@ void *zmalloc(size_t size) { } ``` -另外,Redis 可以使用多种内存分配器来分配内存( libc、jemalloc、tcmalloc),默认使用 [jemalloc](https://github.com/jemalloc/jemalloc),而 jemalloc 按照一系列固定的大小(8 字节、16 字节、32 字节......)来分配内存的。jemalloc 划分的内存单元如下图所示: +另外,Redis 可以使用多种内存分配器来分配内存( libc、jemalloc、tcmalloc),默认使用 [jemalloc](https://github.com/jemalloc/jemalloc),而 jemalloc 按照一系列固定的大小(8 字节、16 字节、32 字节……)来分配内存的。jemalloc 划分的内存单元如下图所示: ![jemalloc 内存单元示意图](https://oss.javaguide.cn/github/javaguide/database/redis/6803d3929e3e46c1b1c9d0bb9ee8e717.png) @@ -59,11 +59,11 @@ void *zmalloc(size_t size) { ![](https://oss.javaguide.cn/github/javaguide/redis-docs-memory-optimization.png) -文档地址:https://redis.io/topics/memory-optimization 。 +文档地址: 。 ## 如何查看 Redis 内存碎片的信息? -使用 `info memory` 命令即可查看 Redis 内存相关的信息。下图中每个参数具体的含义,Redis 官方文档有详细的介绍:https://redis.io/commands/INFO 。 +使用 `info memory` 命令即可查看 Redis 内存相关的信息。下图中每个参数具体的含义,Redis 官方文档有详细的介绍: 。 ![](https://oss.javaguide.cn/github/javaguide/redis-info-memory.png) @@ -117,6 +117,8 @@ config set active-defrag-cycle-max 50 ## 参考 -- Redis 官方文档:https://redis.io/topics/memory-optimization -- Redis 核心技术与实战 - 极客时间 - 删除数据后,为什么内存占用率还是很高?:https://time.geekbang.org/column/article/289140 -- Redis 源码解析——内存分配: +- Redis 官方文档: +- Redis 核心技术与实战 - 极客时间 - 删除数据后,为什么内存占用率还是很高?: +- Redis 源码解析——内存分配:< 源码解析——内存管理> + + diff --git a/docs/database/redis/redis-persistence.md b/docs/database/redis/redis-persistence.md index 2eab555430c..c17fe7db316 100644 --- a/docs/database/redis/redis-persistence.md +++ b/docs/database/redis/redis-persistence.md @@ -20,7 +20,7 @@ Redis 不同于 Memcached 的很重要一点就是,Redis 支持持久化,而 - 只追加文件(append-only file, AOF) - RDB 和 AOF 的混合持久化(Redis 4.0 新增) -官方文档地址:https://redis.io/topics/persistence 。 +官方文档地址: 。 ![](https://oss.javaguide.cn/github/javaguide/database/redis/redis4.0-persitence.png) @@ -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 对于持久化机制做了什么优化? @@ -163,7 +181,7 @@ AOF 校验机制是 Redis 在启动时对 AOF 文件进行检查,以判断文 如果把混合持久化打开,AOF 重写的时候就直接把 RDB 的内容写到 AOF 文件开头。这样做的好处是可以结合 RDB 和 AOF 的优点, 快速加载同时避免丢失过多的数据。当然缺点也是有的, AOF 里面的 RDB 部分是压缩格式不再是 AOF 格式,可读性较差。 -官方文档地址:https://redis.io/topics/persistence +官方文档地址: ![](https://oss.javaguide.cn/github/javaguide/database/redis/redis4.0-persitence.png) @@ -173,7 +191,7 @@ AOF 校验机制是 Redis 在启动时对 AOF 文件进行检查,以判断文 **RDB 比 AOF 优秀的地方**: -- RDB 文件存储的内容是经过压缩的二进制数据, 保存着某个时间点的数据集,文件很小,适合做数据的备份,灾难恢复。AOF 文件存储的是每一次写命令,类似于 MySQL 的 binlog 日志,通常会必 RDB 文件大很多。当 AOF 变得太大时,Redis 能够在后台自动重写 AOF。新的 AOF 文件和原有的 AOF 文件所保存的数据库状态一样,但体积更小。不过, Redis 7.0 版本之前,如果在重写期间有写入命令,AOF 可能会使用大量内存,重写期间到达的所有写入命令都会写入磁盘两次。 +- RDB 文件存储的内容是经过压缩的二进制数据, 保存着某个时间点的数据集,文件很小,适合做数据的备份,灾难恢复。AOF 文件存储的是每一次写命令,类似于 MySQL 的 binlog 日志,通常会比 RDB 文件大很多。当 AOF 变得太大时,Redis 能够在后台自动重写 AOF。新的 AOF 文件和原有的 AOF 文件所保存的数据库状态一样,但体积更小。不过, Redis 7.0 版本之前,如果在重写期间有写入命令,AOF 可能会使用大量内存,重写期间到达的所有写入命令都会写入磁盘两次。 - 使用 RDB 文件恢复数据,直接解析还原数据即可,不需要一条一条地执行命令,速度非常快。而 AOF 则需要依次执行每个写命令,速度非常慢。也就是说,与 AOF 相比,恢复大数据集的时候,RDB 速度更快。 **AOF 比 RDB 优秀的地方**: @@ -191,7 +209,9 @@ AOF 校验机制是 Redis 在启动时对 AOF 文件进行检查,以判断文 ## 参考 - 《Redis 设计与实现》 -- Redis persistence - Redis 官方文档:https://redis.io/docs/management/persistence/ -- The difference between AOF and RDB persistence:https://www.sobyte.net/post/2022-04/redis-rdb-and-aof/ -- Redis AOF 持久化详解 - 程序员历小冰:http://remcarpediem.net/article/376c55d8/ -- Redis RDB 与 AOF 持久化 · Analyze:https://wingsxdu.com/posts/database/redis/rdb-and-aof/ +- Redis persistence - Redis 官方文档: +- The difference between AOF and RDB persistence: +- Redis AOF 持久化详解 - 程序员历小冰: +- Redis RDB 与 AOF 持久化 · Analyze: + + diff --git a/docs/database/redis/redis-questions-01.md b/docs/database/redis/redis-questions-01.md index 2bfe0e776d4..7102985b9a5 100644 --- a/docs/database/redis/redis-questions-01.md +++ b/docs/database/redis/redis-questions-01.md @@ -18,9 +18,11 @@ head: ### 什么是 Redis? -[Redis](https://redis.io/) 是一个基于 C 语言开发的开源数据库(BSD 许可),与传统数据库不同的是 Redis 的数据是存在内存中的(内存数据库),读写速度非常快,被广泛应用于缓存方向。并且,Redis 存储的是 KV 键值对数据。 +[Redis](https://redis.io/) (**RE**mote **DI**ctionary **S**erver)是一个基于 C 语言开发的开源 NoSQL 数据库(BSD 许可)。与传统数据库不同的是,Redis 的数据是保存在内存中的(内存数据库,支持持久化),因此读写速度非常快,被广泛应用于分布式缓存方向。并且,Redis 存储的是 KV 键值对数据。 -为了满足不同的业务场景,Redis 内置了多种数据类型实现(比如 String、Hash、Sorted Set、Bitmap、HyperLogLog、GEO)。并且,Redis 还支持事务、持久化、Lua 脚本、多种开箱即用的集群方案(Redis Sentinel、Redis Cluster)。 +为了满足不同的业务场景,Redis 内置了多种数据类型实现(比如 String、Hash、Sorted Set、Bitmap、HyperLogLog、GEO)。并且,Redis 还支持事务、持久化、Lua 脚本、发布订阅模型、多种开箱即用的集群方案(Redis Sentinel、Redis Cluster)。 + +![Redis 数据类型概览](https://oss.javaguide.cn/github/javaguide/database/redis/redis-overview-of-data-types-2023-09-28.jpg) Redis 没有外部依赖,Linux 和 OS X 是 Redis 开发和测试最多的两个操作系统,官方推荐生产环境使用 Linux 部署 Redis。 @@ -28,31 +30,50 @@ 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 内置了多种优化过后的数据结构实现,性能非常高。 +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) 。 +> 下面这张图片总结的挺不错的,分享一下,出自 [Why is Redis so fast?](https://twitter.com/alexxubyte/status/1498703822528544770)。 ![why-redis-so-fast](./images/why-redis-so-fast.png) -### 分布式缓存常见的技术选型方案有哪些? +那既然都这么快了,为什么不直接用 Redis 当主数据库呢?主要是因为内存成本太高,并且 Redis 提供的数据持久化仍然有数据丢失的风险。 + +### 除了 Redis,你还知道其他分布式缓存方案吗? + +如果面试中被问到这个问题的话,面试官主要想看看: + +1. 你在选择 Redis 作为分布式缓存方案时,是否是经过严谨的调研和思考,还是只是因为 Redis 是当前的“热门”技术。 +2. 你在分布式缓存方向的技术广度。 + +如果你了解其他方案,并且能解释为什么最终选择了 Redis(更进一步!),这会对你面试表现加分不少! + +下面简单聊聊常见的分布式缓存技术选型。 分布式缓存的话,比较老牌同时也是使用的比较多的还是 **Memcached** 和 **Redis**。不过,现在基本没有看过还有项目使用 **Memcached** 来做缓存,都是直接用 **Redis**。 Memcached 是分布式缓存最开始兴起的那会,比较常用的。后来,随着 Redis 的发展,大家慢慢都转而使用更加强大的 Redis 了。 -另外,腾讯也开源了一款类似于 Redis 的分布式高性能 KV 存储数据库,基于知名的开源项目 [RocksDB](https://github.com/facebook/rocksdb) 作为存储引擎 ,100% 兼容 Redis 协议和 Redis4.0 所有数据模型,名为 [Tendis](https://github.com/Tencent/Tendis)。 +有一些大厂也开源了类似于 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 和 Tendis 的对比,腾讯官方曾经发过一篇文章:[Redis vs Tendis:冷热混合存储版架构揭秘](https://mp.weixin.qq.com/s/MeYkfOIdnU6LYlsGb24KjQ) ,可以简单参考一下。 +目前,比较业界认可的 Redis 替代品还是下面这两个开源分布式缓存(都是通过碰瓷 Redis 火的): -从这个项目的 GitHub 提交记录可以看出,Tendis 开源版几乎已经没有被维护更新了,加上其关注度并不高,使用的公司也比较少。因此,不建议你使用 Tendis 来实现分布式缓存。 +- [Dragonfly](https://github.com/dragonflydb/dragonfly):一种针对现代应用程序负荷需求而构建的内存数据库,完全兼容 Redis 和 Memcached 的 API,迁移时无需修改任何代码,号称全世界最快的内存数据库。 +- [KeyDB](https://github.com/Snapchat/KeyDB):Redis 的一个高性能分支,专注于多线程、内存效率和高吞吐量。 + +不过,个人还是建议分布式缓存首选 Redis,毕竟经过了这么多年的考验,生态非常优秀,资料也很全面! + +PS:篇幅问题,我这并没有对上面提到的分布式缓存选型做详细介绍和对比,感兴趣的话,可以自行研究一下。 ### 说一下 Redis 和 Memcached 的区别和共同点 @@ -66,62 +87,91 @@ Memcached 是分布式缓存最开始兴起的那会,比较常用的。后来 **区别**: -1. **Redis 支持更丰富的数据类型(支持更复杂的应用场景)**。Redis 不仅仅支持简单的 k/v 类型的数据,同时还提供 list,set,zset,hash 等数据结构的存储。Memcached 只支持最简单的 k/v 数据类型。 -2. **Redis 支持数据的持久化,可以将内存中的数据保持在磁盘中,重启的时候可以再次加载进行使用,而 Memcached 把数据全部存在内存之中。** -3. **Redis 有灾难恢复机制。** 因为可以把缓存中的数据持久化到磁盘上。 -4. **Redis 在服务器内存使用完之后,可以将不用的数据放到磁盘上。但是,Memcached 在服务器内存使用完之后,就会直接报异常。** -5. **Memcached 没有原生的集群模式,需要依靠客户端来实现往集群中分片写入数据;但是 Redis 目前是原生支持 cluster 模式的。** -6. **Memcached 是多线程,非阻塞 IO 复用的网络模型;Redis 使用单线程的多路 IO 复用模型。** (Redis 6.0 针对网络数据的读写引入了多线程) -7. **Redis 支持发布订阅模型、Lua 脚本、事务等功能,而 Memcached 不支持。并且,Redis 支持更多的编程语言。** -8. **Memcached 过期数据的删除策略只用了惰性删除,而 Redis 同时使用了惰性删除与定期删除。** +1. **数据类型**:Redis 支持更丰富的数据类型(支持更复杂的应用场景)。Redis 不仅仅支持简单的 k/v 类型的数据,同时还提供 list、set、zset、hash 等数据结构的存储;而 Memcached 只支持最简单的 k/v 数据类型。 +2. **数据持久化**:Redis 支持数据的持久化,可以将内存中的数据保持在磁盘中,重启的时候可以再次加载进行使用;而 Memcached 把数据全部存在内存之中。也就是说,Redis 有灾难恢复机制,而 Memcached 没有。 +3. **集群模式支持**:Memcached 没有原生的集群模式,需要依靠客户端来实现往集群中分片写入数据;而 Redis 自 3.0 版本起是原生支持集群模式的。 +4. **线程模型**:Memcached 是多线程、非阻塞 IO 复用的网络模型;而 Redis 使用单线程的多路 IO 复用模型(Redis 6.0 针对网络数据的读写引入了多线程)。 +5. **特性支持**:Redis 支持发布订阅模型、Lua 脚本、事务等功能,而 Memcached 不支持。并且,Redis 支持更多的编程语言。 +6. **过期数据删除**:Memcached 过期数据的删除策略只用了惰性删除,而 Redis 同时使用了惰性删除与定期删除。 相信看了上面的对比之后,我们已经没有什么理由可以选择使用 Memcached 来作为自己项目的分布式缓存了。 -### 为什么要用 Redis/为什么要用缓存? - -下面我们主要从“高性能”和“高并发”这两点来回答这个问题。 - -**1、高性能** +### 为什么要用 Redis? -假如用户第一次访问数据库中的某些数据的话,这个过程是比较慢,毕竟是从硬盘中读取的。但是,如果说,用户访问的数据属于高频数据并且不会经常改变的话,那么我们就可以很放心地将该用户访问的数据存在缓存中。 +**1、访问速度更快** -**这样有什么好处呢?** 那就是保证用户下一次再访问这些数据的时候就可以直接从缓存中获取了。操作缓存就是直接操作内存,所以速度相当快。 +传统数据库数据保存在磁盘,而 Redis 基于内存,内存的访问速度比磁盘快很多。引入 Redis 之后,我们可以把一些高频访问的数据放到 Redis 中,这样下次就可以直接从内存中读取,速度可以提升几十倍甚至上百倍。 **2、高并发** -一般像 MySQL 这类的数据库的 QPS 大概都在 1w 左右(4 核 8g) ,但是使用 Redis 缓存之后很容易达到 10w+,甚至最高能达到 30w+(就单机 Redis 的情况,Redis 集群的话会更高)。 +一般像 MySQL 这类的数据库的 QPS 大概都在 4k 左右(4 核 8g),但是使用 Redis 缓存之后很容易达到 5w+,甚至能达到 10w+(就单机 Redis 的情况,Redis 集群的话会更高)。 > QPS(Query Per Second):服务器每秒可以执行的查询次数; 由此可见,直接操作缓存能够承受的数据库请求数量是远远大于直接访问数据库的,所以我们可以考虑把数据库中的部分数据转移到缓存中去,这样用户的一部分请求会直接到缓存这里而不用经过数据库。进而,我们也就提高了系统整体的并发。 +**3、功能全面** + +Redis 除了可以用作缓存之外,还可以用于分布式锁、限流、消息队列、延时队列等场景,功能强大! + +### 为什么用 Redis 而不用本地缓存呢? + +| 特性 | 本地缓存 | Redis | +| ------------ | ------------------------------------ | -------------------------------- | +| 数据一致性 | 多服务器部署时存在数据不一致问题 | 数据一致 | +| 内存限制 | 受限于单台服务器内存 | 独立部署,内存空间更大 | +| 数据丢失风险 | 服务器宕机数据丢失 | 可持久化,数据不易丢失 | +| 管理维护 | 分散,管理不便 | 集中管理,提供丰富的管理工具 | +| 功能丰富性 | 功能有限,通常只提供简单的键值对存储 | 功能丰富,支持多种数据结构和功能 | + ### 常见的缓存读写策略有哪些? -关于常见的缓存读写策略的详细介绍,可以看我写的这篇文章:[3 种常用的缓存读写策略详解](https://javaguide.cn/database/redis/3-commonly-used-cache-read-and-write-strategies.html) 。 +关于常见的缓存读写策略的详细介绍,可以看我写的这篇文章:[3 种常用的缓存读写策略详解](https://javaguide.cn/database/redis/3-commonly-used-cache-read-and-write-strategies.html)。 + +### 什么是 Redis Module?有什么用? + +Redis 从 4.0 版本开始,支持通过 Module 来扩展其功能以满足特殊的需求。这些 Module 以动态链接库(so 文件)的形式被加载到 Redis 中,这是一种非常灵活的动态扩展功能的实现方式,值得借鉴学习! + +我们每个人都可以基于 Redis 去定制化开发自己的 Module,比如实现搜索引擎功能、自定义分布式锁和分布式限流。 + +目前,被 Redis 官方推荐的 Module 有: + +- [RediSearch](https://github.com/RediSearch/RediSearch):用于实现搜索引擎的模块。 +- [RedisJSON](https://github.com/RedisJSON/RedisJSON):用于处理 JSON 数据的模块。 +- [RedisGraph](https://github.com/RedisGraph/RedisGraph):用于实现图形数据库的模块。 +- [RedisTimeSeries](https://github.com/RedisTimeSeries/RedisTimeSeries):用于处理时间序列数据的模块。 +- [RedisBloom](https://github.com/RedisBloom/RedisBloom):用于实现布隆过滤器的模块。 +- [RedisAI](https://github.com/RedisAI/RedisAI):用于执行深度学习/机器学习模型并管理其数据的模块。 +- [RedisCell](https://github.com/brandur/redis-cell):用于实现分布式限流的模块。 +- …… + +关于 Redis 模块的详细介绍,可以查看官方文档:。 ## Redis 应用 ### Redis 除了做缓存,还能做什么? -- **分布式锁**:通过 Redis 来做分布式锁是一种比较常见的方式。通常情况下,我们都是基于 Redisson 来实现分布式锁。关于 Redis 实现分布式锁的详细介绍,可以看我写的这篇文章:[分布式锁详解](https://javaguide.cn/distributed-system/distributed-lock.html) 。 -- **限流**:一般是通过 Redis + Lua 脚本的方式来实现限流。相关阅读:[《我司用了 6 年的 Redis 分布式限流器,可以说是非常厉害了!》](https://mp.weixin.qq.com/s/kyFAWH3mVNJvurQDt4vchA)。 -- **消息队列**:Redis 自带的 list 数据结构可以作为一个简单的队列使用。Redis 5.0 中增加的 Stream 类型的数据结构更加适合用来做消息队列。它比较类似于 Kafka,有主题和消费组的概念,支持消息持久化以及 ACK 机制。 -- **复杂业务场景**:通过 Redis 以及 Redis 扩展(比如 Redisson)提供的数据结构,我们可以很方便地完成很多复杂的业务场景比如通过 bitmap 统计活跃用户、通过 sorted set 维护排行榜。 -- ...... +- **分布式锁**:通过 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。 +- …… ### 如何基于 Redis 实现分布式锁? -关于 Redis 实现分布式锁的详细介绍,可以看我写的这篇文章:[分布式锁详解](https://javaguide.cn/distributed-system/distributed-lock.html) 。 +关于 Redis 实现分布式锁的详细介绍,可以看我写的这篇文章:[分布式锁详解](https://javaguide.cn/distributed-system/distributed-lock-implementations.html)。 ### Redis 可以做消息队列么? -> 实际项目中也没见谁使用 Redis 来做消息队列,对于这部分知识点大家了解就好了。 +> 实际项目中使用 Redis 来做消息队列的非常少,毕竟有更成熟的消息队列中间件可以用。 -先说结论:可以是可以,但不建议使用 Redis 来做消息队列。和专业的消息队列相比,还是有很多欠缺的地方。 +先说结论:**可以是可以,但不建议使用 Redis 来做消息队列。和专业的消息队列相比,还是有很多欠缺的地方。** **Redis 2.0 之前,如果想要使用 Redis 来做消息队列的话,只能通过 List 来实现。** -通过 `RPUSH/LPOP` 或者 `LPUSH/RPOP`即可实现简易版消息队列: +通过 `RPUSH/LPOP` 或者 `LPUSH/RPOP` 即可实现简易版消息队列: ```bash # 生产者生产消息 @@ -134,9 +184,9 @@ Memcached 是分布式缓存最开始兴起的那会,比较常用的。后来 "msg1" ``` -不过,通过 `RPUSH/LPOP` 或者 `LPUSH/RPOP`这样的方式存在性能问题,我们需要不断轮询去调用 `RPOP` 或 `LPOP` 来消费消息。当 List 为空时,大部分的轮询的请求都是无效请求,这种方式大量浪费了系统资源。 +不过,通过 `RPUSH/LPOP` 或者 `LPUSH/RPOP` 这样的方式存在性能问题,我们需要不断轮询去调用 `RPOP` 或 `LPOP` 来消费消息。当 List 为空时,大部分的轮询的请求都是无效请求,这种方式大量浪费了系统资源。 -因此,Redis 还提供了 `BLPOP`、`BRPOP` 这种阻塞式读取的命令(带 B-Bloking 的都是阻塞式),并且还支持一个超时参数。如果 List 为空,Redis 服务端不会立刻返回结果,它会等待 List 中有新数据后在返回或者是等待最多一个超时时间后返回空。如果将超时时间设置为 0 时,即可无限等待,直到弹出消息 +因此,Redis 还提供了 `BLPOP`、`BRPOP` 这种阻塞式读取的命令(带 B-Blocking 的都是阻塞式),并且还支持一个超时参数。如果 List 为空,Redis 服务端不会立刻返回结果,它会等待 List 中有新数据后再返回或者是等待最多一个超时时间后返回空。如果将超时时间设置为 0 时,即可无限等待,直到弹出消息 ```bash # 超时时间为 10s @@ -147,11 +197,13 @@ null **List 实现消息队列功能太简单,像消息确认机制等功能还需要我们自己实现,最要命的是没有广播机制,消息也只能被消费一次。** -**Redis 2.0 引入了 发布订阅 (pub/sub) 解决了 List 实现消息队列没有广播机制的问题。** +**Redis 2.0 引入了发布订阅 (pub/sub) 功能,解决了 List 实现消息队列没有广播机制的问题。** + +![Redis 发布订阅 (pub/sub) 功能](https://oss.javaguide.cn/github/javaguide/database/redis/redis-pub-sub.png) pub/sub 中引入了一个概念叫 **channel(频道)**,发布订阅机制的实现就是基于这个 channel 来做的。 -pub/sub 涉及发布者和订阅者(也叫消费者)两个角色: +pub/sub 涉及发布者(Publisher)和订阅者(Subscriber,也叫消费者)两个角色: - 发布者通过 `PUBLISH` 投递消息给指定 channel。 - 订阅者通过`SUBSCRIBE`订阅它关心的 channel。并且,订阅者可以订阅一个或者多个 channel。 @@ -164,55 +216,144 @@ pub/sub 既能单播又能广播,还支持 channel 的简单正则匹配。不 为此,Redis 5.0 新增加的一个数据结构 `Stream` 来做消息队列。`Stream` 支持: -- 发布 / 订阅模式 -- 按照消费者组进行消费 -- 消息持久化( RDB 和 AOF) +- 发布 / 订阅模式; +- 按照消费者组进行消费(借鉴了 Kafka 消费者组的概念); +- 消息持久化( RDB 和 AOF); +- ACK 机制(通过确认机制来告知已经成功处理了消息); +- 阻塞式获取消息。 + +`Stream` 的结构如下: + +![](https://oss.javaguide.cn/github/javaguide/database/redis/redis-stream-structure.png) + +这是一个有序的消息链表,每个消息都有一个唯一的 ID 和对应的内容。ID 是一个时间戳和序列号的组合,用来保证消息的唯一性和递增性。内容是一个或多个键值对(类似 Hash 基本数据类型),用来存储消息的数据。 + +这里再对图中涉及到的一些概念,进行简单解释: + +- `Consumer Group`:消费者组用于组织和管理多个消费者。消费者组本身不处理消息,而是再将消息分发给消费者,由消费者进行真正的消费。 +- `last_delivered_id`:标识消费者组当前消费位置的游标,消费者组中任意一个消费者读取了消息都会使 last_delivered_id 往前移动。 +- `pending_ids`:记录已经被客户端消费但没有 ack 的消息的 ID。 + +下面是`Stream` 用作消息队列时常用的命令: -`Stream` 使用起来相对要麻烦一些,这里就不演示了。而且,`Stream` 在实际使用中依然会有一些小问题不太好解决比如在 Redis 发生故障恢复后不能保证消息至少被消费一次。 +- `XADD`:向流中添加新的消息。 +- `XREAD`:从流中读取消息。 +- `XREADGROUP`:从消费组中读取消息。 +- `XRANGE`:根据消息 ID 范围读取流中的消息。 +- `XREVRANGE`:与 `XRANGE` 类似,但以相反顺序返回结果。 +- `XDEL`:从流中删除消息。 +- `XTRIM`:修剪流的长度,可以指定修建策略(`MAXLEN`/`MINID`)。 +- `XLEN`:获取流的长度。 +- `XGROUP CREATE`:创建消费者组。 +- `XGROUP DESTROY`:删除消费者组。 +- `XGROUP DELCONSUMER`:从消费者组中删除一个消费者。 +- `XGROUP SETID`:为消费者组设置新的最后递送消息 ID。 +- `XACK`:确认消费组中的消息已被处理。 +- `XPENDING`:查询消费组中挂起(未确认)的消息。 +- `XCLAIM`:将挂起的消息从一个消费者转移到另一个消费者。 +- `XINFO`:获取流(`XINFO STREAM`)、消费组(`XINFO GROUPS`)或消费者(`XINFO CONSUMERS`)的详细信息。 -综上,和专业的消息队列相比,使用 Redis 来实现消息队列还是有很多欠缺的地方比如消息丢失和堆积问题不好解决。因此,我们通常建议不要使用 Redis 来做消息队列,你完全可以选择市面上比较成熟的一些消息队列比如 RocketMQ、Kafka。 +`Stream` 使用起来相对要麻烦一些,这里就不演示了。 + +总的来说,`Stream` 已经可以满足一个消息队列的基本要求了。不过,`Stream` 在实际使用中依然会有一些小问题不太好解决,比如在 Redis 发生故障恢复后不能保证消息至少被消费一次。 + +综上,和专业的消息队列相比,使用 Redis 来实现消息队列还是有很多欠缺的地方,比如消息丢失和堆积问题不好解决。因此,我们通常建议不要使用 Redis 来做消息队列,你完全可以选择市面上比较成熟的一些消息队列,比如 RocketMQ、Kafka。不过,如果你就是想要用 Redis 来做消息队列的话,那我建议你优先考虑 `Stream`,这是目前相对最优的 Redis 消息队列实现。 相关阅读:[Redis 消息队列发展历程 - 阿里开发者 - 2022](https://mp.weixin.qq.com/s/gCUT5TcCQRAxYkTJfTRjJw)。 -## Redis 数据结构 +### Redis 可以做搜索引擎么? + +Redis 是可以实现全文搜索引擎功能的,需要借助 **RediSearch**,这是一个基于 Redis 的搜索引擎模块。 + +RediSearch 支持中文分词、聚合统计、停用词、同义词、拼写检查、标签查询、向量相似度查询、多关键词搜索、分页搜索等功能,算是一个功能比较完善的全文搜索引擎了。 + +相比较于 Elasticsearch 来说,RediSearch 主要在下面两点上表现更优异一些: + +1. 性能更优秀:依赖 Redis 自身的高性能,基于内存操作(Elasticsearch 基于磁盘)。 +2. 较低内存占用实现快速索引:RediSearch 内部使用压缩的倒排索引,所以可以用较低的内存占用来实现索引的快速构建。 + +对于小型项目的简单搜索场景来说,使用 RediSearch 来作为搜索引擎还是没有问题的(搭配 RedisJSON 使用)。 -> 关于 Redis 5 种基础数据结构和 3 种特殊数据结构的详细介绍请看下面这两篇文章: +对于比较复杂或者数据规模较大的搜索场景,还是不太建议使用 RediSearch 来作为搜索引擎,主要是因为下面这些限制和问题: + +1. 数据量限制:Elasticsearch 可以支持 PB 级别的数据量,可以轻松扩展到多个节点,利用分片机制提高可用性和性能。RedisSearch 是基于 Redis 实现的,其能存储的数据量受限于 Redis 的内存容量,不太适合存储大规模的数据(内存昂贵,扩展能力较差)。 +2. 分布式能力较差:Elasticsearch 是为分布式环境设计的,可以轻松扩展到多个节点。虽然 RedisSearch 支持分布式部署,但在实际应用中可能会面临一些挑战,如数据分片、节点间通信、数据一致性等问题。 +3. 聚合功能较弱:Elasticsearch 提供了丰富的聚合功能,而 RediSearch 的聚合功能相对较弱,只支持简单的聚合操作。 +4. 生态较差:Elasticsearch 可以轻松和常见的一些系统/软件集成比如 Hadoop、Spark、Kibana,而 RedisSearch 则不具备该优势。 + +Elasticsearch 适用于全文搜索、复杂查询、实时数据分析和聚合的场景,而 RediSearch 适用于快速数据存储、缓存和简单查询的场景。 + +### 如何基于 Redis 实现延时任务? + +> 类似的问题: > -> - [Redis 5 种基本数据结构详解](https://javaguide.cn/database/redis/redis-data-structures-01.html) -> - [Redis 3 种特殊数据结构详解](https://javaguide.cn/database/redis/redis-data-structures-02.html) +> - 订单在 10 分钟后未支付就失效,如何用 Redis 实现? +> - 红包 24 小时未被查收自动退还,如何用 Redis 实现? + +基于 Redis 实现延时任务的功能无非就下面两种方案: + +1. Redis 过期事件监听。 +2. Redisson 内置的延时队列。 + +Redis 过期事件监听存在时效性较差、丢消息、多服务实例下消息重复消费等问题,不被推荐使用。 -### Redis 常用的数据结构有哪些? +Redisson 内置的延时队列具备下面这些优势: -- **5 种基础数据结构**:String(字符串)、List(列表)、Set(集合)、Hash(散列)、Zset(有序集合)。 -- **3 种特殊数据结构**:HyperLogLogs(基数统计)、Bitmap (位存储)、Geospatial (地理位置)。 +1. **减少了丢消息的可能**:DelayedQueue 中的消息会被持久化,即使 Redis 宕机了,根据持久化机制,也只可能丢失一点消息,影响不大。当然了,你也可以使用扫描数据库的方法作为补偿机制。 +2. **消息不存在重复消费问题**:每个客户端都是从同一个目标队列中获取任务的,不存在重复消费的问题。 + +关于 Redis 实现延时任务的详细介绍,可以看我写的这篇文章:[如何基于 Redis 实现延时任务?](./redis-delayed-task.md)。 + +## Redis 数据类型 + +关于 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) + +### Redis 常用的数据类型有哪些? + +Redis 中比较常见的数据类型有下面这些: + +- **5 种基础数据类型**:String(字符串)、List(列表)、Set(集合)、Hash(散列)、Zset(有序集合)。 +- **3 种特殊数据类型**:HyperLogLog(基数统计)、Bitmap (位图)、Geospatial (地理位置)。 + +除了上面提到的之外,还有一些其他的比如 [Bloom filter(布隆过滤器)](https://javaguide.cn/cs-basics/data-structure/bloom-filter.html)、Bitfield(位域)。 ### String 的应用场景有哪些? -String 是 Redis 中最简单同时也是最常用的一个数据结构。String 是一种二进制安全的数据结构,可以用来存储任何类型的数据比如字符串、整数、浮点数、图片(图片的 base64 编码或者解码或者图片的路径)、序列化后的对象。 +String 是 Redis 中最简单同时也是最常用的一个数据类型。它是一种二进制安全的数据类型,可以用来存储任何类型的数据比如字符串、整数、浮点数、图片(图片的 base64 编码或者解码或者图片的路径)、序列化后的对象。 String 的常见应用场景如下: -- 常规数据(比如 session、token、序列化后的对象、图片的路径)的缓存; +- 常规数据(比如 Session、Token、序列化后的对象、图片的路径)的缓存; - 计数比如用户单位时间的请求数(简单限流可以用到)、页面单位时间的访问数; -- 分布式锁(利用 `SETNX key value` 命令可以实现一个最简易的分布式锁); -- ...... +- 分布式锁(利用 `SETNX key value` 命令可以实现一个最简易的分布式锁); +- …… -关于 String 的详细介绍请看这篇文章:[Redis 5 种基本数据结构详解](https://javaguide.cn/database/redis/redis-data-structures-01.html)。 +关于 String 的详细介绍请看这篇文章:[Redis 5 种基本数据类型详解](https://javaguide.cn/database/redis/redis-data-structures-01.html)。 ### String 还是 Hash 存储对象数据更好呢? -- String 存储的是序列化后的对象数据,存放的是整个对象。Hash 是对对象的每个字段单独存储,可以获取部分字段的信息,也可以修改或者添加部分字段,节省网络流量。如果对象中某些字段需要经常变动或者经常需要单独查询对象中的个别字段信息,Hash 就非常适合。 -- String 存储相对来说更加节省内存,缓存相同数量的对象数据,String 消耗的内存约是 Hash 的一半。并且,存储具有多层嵌套的对象时也方便很多。如果系统对性能和资源消耗非常敏感的话,String 就非常适合。 +简单对比一下二者: + +- **对象存储方式**:String 存储的是序列化后的对象数据,存放的是整个对象,操作简单直接。Hash 是对对象的每个字段单独存储,可以获取部分字段的信息,也可以修改或者添加部分字段,节省网络流量。如果对象中某些字段需要经常变动或者经常需要单独查询对象中的个别字段信息,Hash 就非常适合。 +- **内存消耗**:Hash 通常比 String 更节省内存,特别是在字段较多且字段长度较短时。Redis 对小型 Hash 进行优化(如使用 ziplist 存储),进一步降低内存占用。 +- **复杂对象存储**:String 在处理多层嵌套或复杂结构的对象时更方便,因为无需处理每个字段的独立存储和操作。 +- **性能**:String 的操作通常具有 O(1) 的时间复杂度,因为它存储的是整个对象,操作简单直接,整体读写的性能较好。Hash 由于需要处理多个字段的增删改查操作,在字段较多且经常变动的情况下,可能会带来额外的性能开销。 + +总结: -在绝大部分情况,我们建议使用 String 来存储对象数据即可! +- 在绝大多数情况下,**String** 更适合存储对象数据,尤其是当对象结构简单且整体读写是主要操作时。 +- 如果你需要频繁操作对象的部分字段或节省内存,**Hash** 可能是更好的选择。 ### String 的底层实现是什么? -Redis 是基于 C 语言编写的,但 Redis 的 String 类型的底层实现并不是 C 语言中的字符串(即以空字符 `\0` 结尾的字符数组),而是自己编写了 [SDS](https://github.com/antirez/sds)(Simple Dynamic String,简单动态字符串) 来作为底层实现。 +Redis 是基于 C 语言编写的,但 Redis 的 String 类型的底层实现并不是 C 语言中的字符串(即以空字符 `\0` 结尾的字符数组),而是自己编写了 [SDS](https://github.com/antirez/sds)(Simple Dynamic String,简单动态字符串)来作为底层实现。 SDS 最早是 Redis 作者为日常 C 语言开发而设计的 C 字符串,后来被应用到了 Redis 上,并经过了大量的修改完善以适合高性能操作。 -Redis7.0 的 SDS 的部分源码如下(https://github.com/redis/redis/blob/7.0/src/sds.h): +Redis7.0 的 SDS 的部分源码如下(): ```c /* Note: sdshdr5 is never used, we just access the flags byte directly. @@ -247,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 会根据初始化的长度决定使用哪种类型,从而减少内存的使用。 | 类型 | 字节 | 位 | | -------- | ---- | --- | @@ -259,10 +400,10 @@ struct __attribute__ ((__packed__)) sdshdr64 { 对于后四种实现都包含了下面这 4 个属性: -- `len`:字符串的长度也就是已经使用的字节数 -- `alloc`:总共可用的字符空间大小,alloc-len 就是 SDS 剩余的空间大小 -- `buf[]`:实际存储字符串的数组 -- `flags`:低三位保存类型标志 +- `len`:字符串的长度也就是已经使用的字节数。 +- `alloc`:总共可用的字符空间大小,alloc-len 就是 SDS 剩余的空间大小。 +- `buf[]`:实际存储字符串的数组。 +- `flags`:低三位保存类型标志。 SDS 相比于 C 语言中的字符串有如下提升: @@ -304,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) @@ -314,27 +455,37 @@ Redis 中有一个叫做 `sorted set` 的数据结构经常被用在各种排行 ![](https://oss.javaguide.cn/github/javaguide/database/redis/image-20220719071115140.png) +### Redis 的有序集合底层为什么要用跳表,而不用平衡树、红黑树或者 B+ 树? + +这道面试题很多大厂比较喜欢问,难度还是有点大的。 + +- 平衡树 vs 跳表:平衡树的插入、删除和查询的时间复杂度和跳表一样都是 **O(log n)**。对于范围查询来说,平衡树也可以通过中序遍历的方式达到和跳表一样的效果。但是它的每一次插入或者删除操作都需要保证整颗树左右节点的绝对平衡,只要不平衡就要通过旋转操作来保持平衡,这个过程是比较耗时的。跳表诞生的初衷就是为了克服平衡树的一些缺点。跳表使用概率平衡而不是严格强制的平衡,因此,跳表中的插入和删除算法比平衡树的等效算法简单得多,速度也快得多。 +- 红黑树 vs 跳表:相比较于红黑树来说,跳表的实现也更简单一些,不需要通过旋转和染色(红黑变换)来保证黑平衡。并且,按照区间来查找数据这个操作,红黑树的效率没有跳表高。 +- B+ 树 vs 跳表:B+ 树更适合作为数据库和文件系统中常用的索引结构之一,它的核心思想是通过可能少的 IO 定位到尽可能多的索引来获得查询数据。对于 Redis 这种内存数据库来说,它对这些并不感冒,因为 Redis 作为内存数据库它不可能存储大量的数据,所以对于索引不需要通过 B+ 树这种方式进行维护,只需按照概率进行随机维护即可,节约内存。而且使用跳表实现 zset 时相较前者来说更简单一些,在进行插入时只需通过索引将数据插入到链表中合适的位置再随机维护一定高度的索引即可,也不需要像 B+ 树那样插入时发现失衡时还需要对节点分裂与合并。 + +另外,我还单独写了一篇文章从有序集合的基本使用到跳表的源码分析和实现,让你会对 Redis 的有序集合底层实现的跳表有着更深刻的理解和掌握:[Redis 为什么用跳表实现有序集合](./redis-skiplist.md)。 + ### Set 的应用场景是什么? Redis 中 `Set` 是一种无序集合,集合中的元素没有先后顺序但都唯一,有点类似于 Java 中的 `HashSet` 。 -Set 的常见应用场景如下: +`Set` 的常见应用场景如下: -- 存放的数据不能重复的场景:网站 UV 统计(数据量巨大的场景还是 `HyperLogLog`更适合一些)、文章点赞、动态点赞等等。 -- 需要获取多个数据源交集、并集和差集的场景:共同好友(交集)、共同粉丝(交集)、共同关注(交集)、好友推荐(差集)、音乐推荐(差集)、订阅号推荐(差集+交集) 等等。 +- 存放的数据不能重复的场景:网站 UV 统计(数据量巨大的场景还是 `HyperLogLog` 更适合一些)、文章点赞、动态点赞等等。 +- 需要获取多个数据源交集、并集和差集的场景:共同好友(交集)、共同粉丝(交集)、共同关注(交集)、好友推荐(差集)、音乐推荐(差集)、订阅号推荐(差集+交集)等等。 - 需要随机获取数据源中的元素的场景:抽奖系统、随机点名等等。 ### 使用 Set 实现抽奖系统怎么做? -如果想要使用 Set 实现一个简单的抽奖系统的话,直接使用下面这几个命令就可以了: +如果想要使用 `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(偏移量)。 @@ -353,7 +504,7 @@ Bitmap 存储的是连续的二进制数字(0 和 1),通过 Bitmap, 只需 (integer) 0 ``` -统计 20210308~20210309 总活跃用户数: +统计 20210308~20210309 总活跃用户数: ```bash > BITOP and desk1 20210308 20210309 @@ -362,7 +513,7 @@ Bitmap 存储的是连续的二进制数字(0 和 1),通过 Bitmap, 只需 (integer) 1 ``` -统计 20210308~20210309 在线活跃用户数: +统计 20210308~20210309 在线活跃用户数: ```bash > BITOP or desk2 20210308 20210309 @@ -392,17 +543,17 @@ PFCOUNT PAGE_1:UV ## Redis 持久化机制(重要) -Redis 持久化机制(RDB 持久化、AOF 持久化、RDB 和 AOF 的混合持久化) 相关的问题比较多,也比较重要,于是我单独抽了一篇文章来总结 Redis 持久化机制相关的知识点和问题:[Redis 持久化机制详解](./redis-persistence.md) 。 +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 设计与实现》有一段话是如是介绍文件事件处理器的,我觉得写得挺不错。 +《Redis 设计与实现》有一段话是这样介绍文件事件处理器的,我觉得写得挺不错。 > Redis 基于 Reactor 模式开发了自己的网络事件处理器:这个处理器被称为文件事件处理器(file event handler)。 > @@ -426,27 +577,31 @@ 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 之前为什么不使用多线程? -虽然说 Redis 是单线程模型,但是,实际上,**Redis 在 4.0 之后的版本中就已经加入了对多线程的支持。** +虽然说 Redis 是单线程模型,但实际上,**Redis 在 4.0 之后的版本中就已经加入了对多线程的支持。** + +不过,Redis 4.0 增加的多线程主要是针对一些大键值对的删除操作的命令,使用这些命令就会使用主线程之外的其他线程来“异步处理”,从而减少对主线程的影响。 -不过,Redis 4.0 增加的多线程主要是针对一些大键值对的删除操作的命令,使用这些命令就会使用主线程之外的其他线程来“异步处理”。 +为此,Redis 4.0 之后新增了几个异步命令: -为此,Redis 4.0 之后新增了`UNLINK`(可以看作是 `DEL` 的异步版本)、`FLUSHALL ASYNC`(清空所有数据库的所有 key,不仅仅是当前 `SELECT` 的数据库)、`FLUSHDB ASYNC`(清空当前 `SELECT` 数据库中的所有 key)等异步命令。 +- `UNLINK`:可以看作是 `DEL` 命令的异步版本。 +- `FLUSHALL ASYNC`:用于清空所有数据库的所有键,不限于当前 `SELECT` 的数据库。 +- `FLUSHDB ASYNC`:用于清空当前 `SELECT` 数据库中的所有键。 ![redis4.0 more thread](https://oss.javaguide.cn/github/javaguide/database/redis/redis4.0-more-thread.png) -大体上来说,Redis 6.0 之前主要还是单线程处理。 +总的来说,直到 Redis 6.0 之前,Redis 的主要操作仍然是单线程处理的。 **那 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 之后为何引入了多线程? @@ -465,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 ``` -但是官网描述开启多线程读并不能有太大提升,因此一般情况下并不建议开启 +但是官网描述开启多线程读并不能有太大提升,因此一般情况下并不建议开启。 相关阅读: @@ -483,10 +638,10 @@ 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 版本,源码地址:https://github.com/redis/redis/blob/6.0/src/bio.h): +在`bio.h` 文件中有定义(Redis 6.0 版本,源码地址:): ```java #ifndef __BIO_H @@ -513,11 +668,11 @@ void bioKillThreads(void); ## Redis 内存管理 -### Redis 给缓存数据设置过期时间有啥用? +### Redis 给缓存数据设置过期时间有什么用? 一般情况下,我们设置保存的缓存数据的时候都会设置一个过期时间。为什么呢? -因为内存是有限的,如果缓存中的所有数据都是一直保存的话,分分钟直接 Out of memory。 +内存是有限且珍贵的,如果不对缓存数据设置过期时间,那内存占用就会一直增长,最终可能会导致 OOM 问题。通过设置合理的过期时间,Redis 会自动删除暂时不需要的数据,为新的缓存数据腾出空间。 Redis 自带了给缓存数据设置过期时间的功能,比如: @@ -530,19 +685,19 @@ OK (integer) 56 ``` -注意:**Redis 中除了字符串类型有自己独有设置过期时间的命令 `setex` 外,其他方法都需要依靠 `expire` 命令来设置过期时间 。另外, `persist` 命令可以移除一个键的过期时间。** +注意 ⚠️:Redis 中除了字符串类型有自己独有设置过期时间的命令 `setex` 外,其他方法都需要依靠 `expire` 命令来设置过期时间 。另外,`persist` 命令可以移除一个键的过期时间。 **过期时间除了有助于缓解内存的消耗,还有什么其他用么?** -很多时候,我们的业务场景就是需要某个数据只在某一时间段内存在,比如我们的短信验证码可能只在 1 分钟内有效,用户登录的 token 可能只在 1 天内有效。 +很多时候,我们的业务场景就是需要某个数据只在某一时间段内存在,比如我们的短信验证码可能只在 1 分钟内有效,用户登录的 Token 可能只在 1 天内有效。 如果使用传统的数据库来处理的话,一般都是自己判断过期,这样更麻烦并且性能要差很多。 ### 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) +![Redis 过期字典](https://oss.javaguide.cn/github/javaguide/database/redis/redis-expired-dictionary.png) 过期字典是存储在 redisDb 这个结构里的: @@ -556,42 +711,164 @@ typedef struct redisDb { } redisDb; ``` -### 过期的数据的删除策略了解么? +在查询一个 key 的时候,Redis 首先检查该 key 是否存在于过期字典中(时间复杂度为 O(1)),如果不在就直接返回,在的话需要判断一下这个 key 是否过期,过期直接删除 key 然后返回 null。 + +### Redis 过期 key 删除策略了解么? 如果假设你设置了一批 key 只能存活 1 分钟,那么 1 分钟后,Redis 是怎么对这批 key 进行删除的呢? -常用的过期数据的删除策略就两个(重要!自己造缓存轮子的时候需要格外考虑的东西): +常用的过期数据的删除策略就下面这几种: + +1. **惰性删除**:只会在取出/查询 key 的时候才对数据进行过期检查。这种方式对 CPU 最友好,但是可能会造成太多过期 key 没有被删除。 +2. **定期删除**:周期性地随机从设置了过期时间的 key 中抽查一批,然后逐个检查这些 key 是否过期,过期就删除 key。相比于惰性删除,定期删除对内存更友好,对 CPU 不太友好。 +3. **延迟队列**:把设置过期时间的 key 放到一个延迟队列里,到期之后就删除 key。这种方式可以保证每个过期 key 都能被删除,但维护延迟队列太麻烦,队列本身也要占用资源。 +4. **定时删除**:每个设置了过期时间的 key 都会在设置的时间到达时立即被删除。这种方法可以确保内存中不会有过期的键,但是它对 CPU 的压力最大,因为它需要为每个键都设置一个定时器。 + +**Redis 采用的是那种删除策略呢?** + +Redis 采用的是 **定期删除+惰性/懒汉式删除** 结合的策略,这也是大部分缓存框架的选择。定期删除对内存更加友好,惰性删除对 CPU 更加友好。两者各有千秋,结合起来使用既能兼顾 CPU 友好,又能兼顾内存友好。 + +下面是我们详细介绍一下 Redis 中的定期删除具体是如何做的。 + +Redis 的定期删除过程是随机的(周期性地随机从设置了过期时间的 key 中抽查一批),所以并不保证所有过期键都会被立即删除。这也就解释了为什么有的 key 过期了,并没有被删除。并且,Redis 底层会通过限制删除操作执行的时长和频率来减少删除操作对 CPU 时间的影响。 + +另外,定期删除还会受到执行时间和过期 key 的比例的影响: + +- 执行时间已经超过了阈值,那么就中断这一次定期删除循环,以避免使用过多的 CPU 时间。 +- 如果这一批过期的 key 比例超过一个比例,就会重复执行此删除流程,以更积极地清理过期 key。相应地,如果过期的 key 比例低于这个比例,就会中断这一次定期删除循环,避免做过多的工作而获得很少的内存回收。 + +Redis 7.2 版本的执行时间阈值是 **25ms**,过期 key 比例设定值是 **10%**。 + +```c +#define ACTIVE_EXPIRE_CYCLE_FAST_DURATION 1000 /* Microseconds. */ +#define ACTIVE_EXPIRE_CYCLE_SLOW_TIME_PERC 25 /* Max % of CPU to use. */ +#define ACTIVE_EXPIRE_CYCLE_ACCEPTABLE_STALE 10 /* % of stale keys after which + we do extra efforts. */ +``` + +**每次随机抽查数量是多少?** + +`expire.c` 中定义了每次随机抽查的数量,Redis 7.2 版本为 20,也就是说每次会随机选择 20 个设置了过期时间的 key 判断是否过期。 + +```c +#define ACTIVE_EXPIRE_CYCLE_KEYS_PER_LOOP 20 /* Keys for each DB loop. */ +``` + +**如何控制定期删除的执行频率?** + +在 Redis 中,定期删除的频率是由 **hz** 参数控制的。hz 默认为 10,代表每秒执行 10 次,也就是每秒钟进行 10 次尝试来查找并删除过期的 key。 + +hz 的取值范围为 1~500。增大 hz 参数的值会提升定期删除的频率。如果你想要更频繁地执行定期删除任务,可以适当增加 hz 的值,但这会增加 CPU 的使用率。根据 Redis 官方建议,hz 的值不建议超过 100,对于大部分用户使用默认的 10 就足够了。 + +下面是 hz 参数的官方注释,我翻译了其中的重要信息(Redis 7.2 版本)。 + +![redis.conf 对于 hz 的注释](https://oss.javaguide.cn/github/javaguide/database/redis/redis.conf-hz.png) + +类似的参数还有一个 **dynamic-hz**,这个参数开启之后 Redis 就会在 hz 的基础上动态计算一个值。Redis 提供并默认启用了使用自适应 hz 值的能力, + +这两个参数都在 Redis 配置文件 `redis.conf` 中: + +```properties +# 默认为 10 +hz 10 +# 默认开启 +dynamic-hz yes +``` -1. **惰性删除**:只会在取出 key 的时候才对数据进行过期检查。这样对 CPU 最友好,但是可能会造成太多过期 key 没有被删除。 -2. **定期删除**:每隔一段时间抽取一批 key 执行删除过期 key 操作。并且,Redis 底层会通过限制删除操作执行的时长和频率来减少删除操作对 CPU 时间的影响。 +多提一嘴,除了定期删除过期 key 这个定期任务之外,还有一些其他定期任务例如关闭超时的客户端连接、更新统计信息,这些定期任务的执行频率也是通过 hz 参数决定。 -定期删除对内存更加友好,惰性删除对 CPU 更加友好。两者各有千秋,所以 Redis 采用的是 **定期删除+惰性/懒汉式删除** 。 +**为什么定期删除不是把所有过期 key 都删除呢?** -但是,仅仅通过给 key 设置过期时间还是有问题的。因为还是可能存在定期删除和惰性删除漏掉了很多过期 key 的情况。这样就导致大量过期 key 堆积在内存里,然后就 Out of memory 了。 +这样会对性能造成太大的影响。如果我们 key 数量非常庞大的话,挨个遍历检查是非常耗时的,会严重影响性能。Redis 设计这种策略的目的是为了平衡内存和性能。 -怎么解决这个问题呢?答案就是:**Redis 内存淘汰机制。** +**为什么 key 过期之后不立马把它删掉呢?这样不是会浪费很多内存空间吗?** -### Redis 内存淘汰机制了解么? +因为不太好办到,或者说这种删除方式的成本太高了。假如我们使用延迟队列作为删除策略,这样存在下面这些问题: + +1. 队列本身的开销可能很大:key 多的情况下,一个延迟队列可能无法容纳。 +2. 维护延迟队列太麻烦:修改 key 的过期时间就需要调整期在延迟队列中的位置,并且还需要引入并发控制。 + +### 大量 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 性能的影响。 + +### Redis 内存淘汰策略了解么? > 相关问题:MySQL 里有 2000w 数据,Redis 中只存 20w 的数据,如何保证 Redis 中的数据都是热点数据? -Redis 提供 6 种数据淘汰策略: +Redis 的内存淘汰策略只有在运行内存达到了配置的最大内存阈值时才会触发,这个阈值是通过 `redis.conf` 的 `maxmemory` 参数来定义的。64 位操作系统下,`maxmemory` 默认为 0,表示不限制内存大小。32 位操作系统下,默认的最大内存值是 3GB。 + +你可以使用命令 `config get maxmemory` 来查看 `maxmemory` 的值。 + +```bash +> config get maxmemory +maxmemory +0 +``` + +Redis 提供了 6 种内存淘汰策略: 1. **volatile-lru(least recently used)**:从已设置过期时间的数据集(`server.db[i].expires`)中挑选最近最少使用的数据淘汰。 2. **volatile-ttl**:从已设置过期时间的数据集(`server.db[i].expires`)中挑选将要过期的数据淘汰。 3. **volatile-random**:从已设置过期时间的数据集(`server.db[i].expires`)中任意选择数据淘汰。 -4. **allkeys-lru(least recently used)**:当内存不足以容纳新写入数据时,在键空间中,移除最近最少使用的 key(这个是最常用的)。 +4. **allkeys-lru(least recently used)**:从数据集(`server.db[i].dict`)中移除最近最少使用的数据淘汰。 5. **allkeys-random**:从数据集(`server.db[i].dict`)中任意选择数据淘汰。 -6. **no-eviction**:禁止驱逐数据,也就是说当内存不足以容纳新写入数据时,新写入操作会报错。这个应该没人使用吧! +6. **no-eviction**(默认内存淘汰策略):禁止驱逐数据,当内存不足以容纳新写入数据时,新写入操作会报错。 4.0 版本后增加以下两种: 7. **volatile-lfu(least frequently used)**:从已设置过期时间的数据集(`server.db[i].expires`)中挑选最不经常使用的数据淘汰。 -8. **allkeys-lfu(least frequently used)**:当内存不足以容纳新写入数据时,在键空间中,移除最不经常使用的 key。 +8. **allkeys-lfu(least frequently used)**:从数据集(`server.db[i].dict`)中移除最不经常使用的数据淘汰。 + +`allkeys-xxx` 表示从所有的键值中淘汰数据,而 `volatile-xxx` 表示从设置了过期时间的键值中淘汰数据。 + +`config.c` 中定义了内存淘汰策略的枚举数组: + +```c +configEnum maxmemory_policy_enum[] = { + {"volatile-lru", MAXMEMORY_VOLATILE_LRU}, + {"volatile-lfu", MAXMEMORY_VOLATILE_LFU}, + {"volatile-random",MAXMEMORY_VOLATILE_RANDOM}, + {"volatile-ttl",MAXMEMORY_VOLATILE_TTL}, + {"allkeys-lru",MAXMEMORY_ALLKEYS_LRU}, + {"allkeys-lfu",MAXMEMORY_ALLKEYS_LFU}, + {"allkeys-random",MAXMEMORY_ALLKEYS_RANDOM}, + {"noeviction",MAXMEMORY_NO_EVICTION}, + {NULL, 0} +}; +``` + +你可以使用 `config get maxmemory-policy` 命令来查看当前 Redis 的内存淘汰策略。 + +```bash +> config get maxmemory-policy +maxmemory-policy +noeviction +``` + +可以通过 `config set maxmemory-policy 内存淘汰策略` 命令修改内存淘汰策略,立即生效,但这种方式重启 Redis 之后就失效了。修改 `redis.conf` 中的 `maxmemory-policy` 参数不会因为重启而失效,不过,需要重启之后修改才能生效。 + +```properties +maxmemory-policy noeviction +``` + +关于淘汰策略的详细说明可以参考 Redis 官方文档:。 ## 参考 - 《Redis 开发与运维》 - 《Redis 设计与实现》 -- Redis 命令手册:https://www.redis.com.cn/commands.html +- 《Redis 核心原理与实战》 +- Redis 命令手册: +- RedisSearch 终极使用指南,你值得拥有!: - WHY Redis choose single thread (vs multi threads): [https://medium.com/@jychen7/sharing-redis-single-thread-vs-multi-threads-5870bd44d153](https://medium.com/@jychen7/sharing-redis-single-thread-vs-multi-threads-5870bd44d153) + + diff --git a/docs/database/redis/redis-questions-02.md b/docs/database/redis/redis-questions-02.md index 75adaa15022..08e5e0a8e43 100644 --- a/docs/database/redis/redis-questions-02.md +++ b/docs/database/redis/redis-questions-02.md @@ -12,6 +12,8 @@ head: content: 一篇文章总结Redis常见的知识点和面试题,涵盖Redis基础、Redis常见数据结构、Redis线程模型、Redis内存管理、Redis事务、Redis性能优化等内容。 --- + + ## Redis 事务 ### 什么是 Redis 事务? @@ -26,7 +28,7 @@ Redis 事务实际开发中使用的非常少,功能比较鸡肋,不要将 ### 如何使用 Redis 事务? -Redis 可以通过 **`MULTI`,`EXEC`,`DISCARD` 和 `WATCH`** 等命令来实现事务(Transaction)功能。 +Redis 可以通过 **`MULTI`、`EXEC`、`DISCARD` 和 `WATCH`** 等命令来实现事务(Transaction)功能。 ```bash > MULTI @@ -45,8 +47,8 @@ QUEUED 这个过程是这样的: 1. 开始事务(`MULTI`); -2. 命令入队(批量操作 Redis 的命令,先进先出(FIFO)的顺序执行); -3. 执行事务(`EXEC`)。 +2. 命令入队(批量操作 Redis 的命令,先进先出(FIFO)的顺序执行); +3. 执行事务(`EXEC`)。 你也可以通过 [`DISCARD`](https://redis.io/commands/discard) 命令取消一个事务,它会清空事务队列中保存的所有命令。 @@ -136,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 事务其实是不满足原子性的。 @@ -147,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 事务的持久性也是没办法保证的。 @@ -178,7 +180,7 @@ Redis 从 2.6 版本开始支持执行 Lua 脚本,它的功能和事务非常 一段 Lua 脚本可以视作一条命令执行,一段 Lua 脚本执行过程中不会有其他脚本或 Redis 命令同时执行,保证了操作不会被其他指令插入或打扰。 -不过,如果 Lua 脚本运行时出错并中途结束,出错之后的命令是不会被执行的。并且,出错之前执行的命令是无法被撤销的,无法实现类似关系型数据库执行失败可以回滚的那种原子性效果。因此, **严格来说的话,通过 Lua 脚本来批量执行 Redis 命令实际也是不完全满足原子性的。** +不过,如果 Lua 脚本运行时出错并中途结束,出错之后的命令是不会被执行的。并且,出错之前执行的命令是无法被撤销的,无法实现类似关系型数据库执行失败可以回滚的那种原子性效果。因此,**严格来说的话,通过 Lua 脚本来批量执行 Redis 命令实际也是不完全满足原子性的。** 如果想要让 Lua 脚本中的命令全部执行,必须保证语句语法和命令都是对的。 @@ -188,34 +190,34 @@ Redis 从 2.6 版本开始支持执行 Lua 脚本,它的功能和事务非常 除了下面介绍的内容之外,再推荐两篇不错的文章: -- [你的 Redis 真的变慢了吗?性能优化如何做 - 阿里开发者](https://mp.weixin.qq.com/s/nNEuYw0NlYGhuKKKKoWfcQ) -- [Redis 常见阻塞原因总结 - JavaGuide](./redis-common-blocking-problems-summary.md) +- [你的 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 的介绍中有提到:https://redis.io/docs/manual/pipelining/ 。 +另外,除了能减少 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 客户端实现,无需我们自己再手动实现): @@ -225,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 的是有区别的,使用的时候需要注意: @@ -250,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 集中过期问题 @@ -272,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 设置随机过期时间。 @@ -280,11 +282,32 @@ Lua 脚本同样支持批量操作多条命令。一段 Lua 脚本可以视作 #### 什么是 bigkey? -简单来说,如果一个 key 对应的 value 所占用的内存比较大,那这个 key 就可以看作是 bigkey。具体多大才算大呢?有一个不是特别精确的参考标准:string 类型的 value 超过 10 kb,复合类型的 value 包含的元素超过 5000 个(对于复合类型的 value 来说,不一定包含的元素越多,占用的内存就越多)。 +简单来说,如果一个 key 对应的 value 所占用的内存比较大,那这个 key 就可以看作是 bigkey。具体多大才算大呢?有一个不是特别精确的参考标准: + +- String 类型的 value 超过 1MB +- 复合类型(List、Hash、Set、Sorted Set 等)的 value 包含的元素超过 5000 个(不过,对于复合类型的 value 来说,不一定包含的元素越多,占用的内存就越多)。 + +![bigkey 判定标准](https://oss.javaguide.cn/github/javaguide/database/redis/bigkey-criterion.png) + +#### bigkey 是怎么产生的?有什么危害? + +bigkey 通常是由于下面这些原因产生的: + +- 程序设计不当,比如直接使用 String 类型存储较大的文件对应的二进制数据。 +- 对于业务的数据规模考虑不周到,比如使用集合类型的时候没有考虑到数据量的快速增长。 +- 未及时清理垃圾数据,比如哈希中冗余了大量的无用键值对。 + +bigkey 除了会消耗更多的内存空间和带宽,还会对性能造成比较大的影响。 + +在 [Redis 常见阻塞原因总结](./redis-common-blocking-problems-summary.md) 这篇文章中我们提到:大 key 还会造成阻塞问题。具体来说,主要体现在下面三个方面: -#### bigkey 有什么危害? +1. 客户端超时阻塞:由于 Redis 执行命令是单线程处理,然后在操作大 key 时会比较耗时,那么就会阻塞 Redis,从客户端这一视角看,就是很久很久都没有响应。 +2. 网络阻塞:每次获取大 key 产生的网络流量较大,如果一个 key 的大小是 1 MB,每秒访问量为 1000,那么每秒会产生 1000MB 的流量,这对于普通千兆网卡的服务器来说是灾难性的。 +3. 工作线程阻塞:如果使用 del 删除大 key 时,会阻塞工作线程,这样就没办法处理后续的命令。 -bigkey 除了会消耗更多的内存空间和带宽,还会对性能造成比较大的影响。因此,我们应该尽量避免 Redis 中存在 bigkey。 +大 key 造成的阻塞问题还会进一步影响到主从同步和集群扩容。 + +综上,大 key 带来的潜在问题是非常多的,我们应该尽量避免 Redis 中存在 bigkey。 #### 如何发现 bigkey? @@ -316,22 +339,38 @@ 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 数据类型,包含元素最多的复合数据类型)。 +从这个命令的运行结果,我们可以看出:这个命令会扫描(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` 等命令返回其长度或成员数量。 -**2、借助开源工具分析 RDB 文件。** +| 数据结构 | 命令 | 复杂度 | 结果(对应 key) | +| ---------- | ------ | ------ | ------------------ | +| String | STRLEN | O(1) | 字符串值的长度 | +| Hash | HLEN | O(1) | 哈希表中字段的数量 | +| List | LLEN | O(1) | 列表元素数量 | +| Set | SCARD | O(1) | 集合元素数量 | +| Sorted Set | ZCARD | O(1) | 有序集合的元素数量 | + +对于集合类型还可以使用 `MEMORY USAGE` 命令(Redis 4.0+),这个命令会返回键值对占用的内存空间。 + +**3、借助开源工具分析 RDB 文件。** 通过分析 RDB 文件来找出 big key。这种方案的前提是你的 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 快照文件用的工具,性能更好。 +- [redis-rdb-tools](https://github.com/sripathikrishnan/redis-rdb-tools):Python 语言写的用来分析 Redis 的 RDB 快照文件用的工具。 +- [rdb_bigkeys](https://github.com/weiyanwei412/rdb_bigkeys):Go 语言写的用来分析 Redis 的 RDB 快照文件用的工具,性能更好。 -**3、借助公有云的 Redis 分析服务。** +**4、借助公有云的 Redis 分析服务。** 如果你用的是公有云的 Redis 服务的话,可以看看其是否提供了 key 分析功能(一般都提供了)。 -这里以阿里云 Redis 为例说明,它支持 bigkey 实时分析、发现,文档地址: 。 +这里以阿里云 Redis 为例说明,它支持 bigkey 实时分析、发现,文档地址:。 ![阿里云Key分析](https://oss.javaguide.cn/github/javaguide/database/redis/aliyun-key-analysis.png) @@ -339,15 +378,16 @@ Biggest string found '"ballcat:oauth:refresh_auth:f6cdb384-9a9d-4f2f-af01-dc3f28 bigkey 的常见处理以及优化办法如下(这些方法可以配合起来使用): -- **分割 bigkey**:将一个 bigkey 分割为多个小 key。这种方式需要修改业务层的代码,一般不推荐这样做。 +- **分割 bigkey**:将一个 bigkey 分割为多个小 key。例如,将一个含有上万字段数量的 Hash 按照一定策略(比如二次哈希)拆分为多个 Hash。 - **手动清理**:Redis 4.0+ 可以使用 `UNLINK` 命令来异步删除一个或多个指定的 key。Redis 4.0 以下可以考虑使用 `SCAN` 命令结合 `DEL` 命令来分批次删除。 -- **采用合适的数据结构**:比如使用 HyperLogLog 统计页面 UV。 +- **采用合适的数据结构**:例如,文件二进制数据不使用 String 保存、使用 HyperLogLog 统计页面 UV、Bitmap 保存状态信息(0/1)。 +- **开启 lazy-free(惰性删除/延迟释放)**:lazy-free 特性是 Redis 4.0 开始引入的,指的是让 Redis 采用异步方式延迟释放 key 使用的内存,将该操作交给单独的子线程处理,避免阻塞主线程。 ### Redis hotkey(热 Key) #### 什么是 hotkey? -简单来说,如果一个 key 的访问次数比较多且明显多于其他 key 的话,那这个 key 就可以看作是 hotkey。例如在 Redis 实例的每秒处理请求达到 5000 次,而其中某个 key 的每秒访问量就高达 2000 次,那这个 key 就可以看作是 hotkey。 +如果一个 key 的访问次数比较多且明显多于其他 key 的话,那这个 key 就可以看作是 **hotkey(热 Key)**。例如在 Redis 实例的每秒处理请求达到 5000 次,而其中某个 key 的每秒访问量就高达 2000 次,那这个 key 就可以看作是 hotkey。 hotkey 出现的原因主要是某个热点数据访问量暴增,如重大的热搜事件、参与秒杀的商品。 @@ -377,8 +417,8 @@ Error: ERR An LFU maxmemory policy is not selected, access frequency not tracked Redis 中有两种 LFU 算法: -- **volatile-lru(least recently used)**:从已设置过期时间的数据集(`server.db[i].expires`)中挑选最近最少使用的数据淘汰。 -- **allkeys-lru(least recently used)**:当内存不足以容纳新写入数据时,在键空间中,移除最近最少使用的 key(这个是最常用的)。 +1. **volatile-lfu(least frequently used)**:从已设置过期时间的数据集(`server.db[i].expires`)中挑选最不经常使用的数据淘汰。 +2. **allkeys-lfu(least frequently used)**:当内存不足以容纳新写入数据时,在键空间中,移除最不经常使用的 key。 以下是配置文件 `redis.conf` 中的示例: @@ -392,13 +432,13 @@ maxmemory-policy allkeys-lfu 需要注意的是,`hotkeys` 参数命令也会增加 Redis 实例的 CPU 和内存消耗(全局扫描),因此需要谨慎使用。 -**2、使用`MONITOR` 命令。** +**2、使用 `MONITOR` 命令。** `MONITOR` 命令是 Redis 提供的一种实时查看 Redis 的所有操作的方式,可以用于临时监控 Redis 实例的操作情况,包括读写、删除等操作。 由于该命令对 Redis 性能的影响比较大,因此禁止长时间开启 `MONITOR`(生产环境中建议谨慎使用该命令)。 -```java +```bash # redis-cli 127.0.0.1:6379> MONITOR OK @@ -433,7 +473,7 @@ OK 如果你用的是公有云的 Redis 服务的话,可以看看其是否提供了 key 分析功能(一般都提供了)。 -这里以阿里云 Redis 为例说明,它支持 hotkey 实时分析、发现,文档地址: 。 +这里以阿里云 Redis 为例说明,它支持 hotkey 实时分析、发现,文档地址:。 ![阿里云Key分析](https://oss.javaguide.cn/github/javaguide/database/redis/aliyun-key-analysis.png) @@ -457,43 +497,45 @@ 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 中所有的键值对。 - `LRANGE`:会返回 List 中指定范围内的元素。 - `SMEMBERS`:返回 Set 中的所有元素。 - `SINTER`/`SUNION`/`SDIFF`:计算多个 Set 的交集/并集/差集。 -- ...... +- …… 由于这些命令时间复杂度是 O(n),有时候也会全表扫描,随着 n 的增大,执行耗时也会越长。不过, 这些命令并不是一定不能使用,但是需要明确 N 的值。另外,有遍历的需求可以使用 `HSCAN`、`SSCAN`、`ZSCAN` 代替。 -除了这些 O(n)时间复杂度的命令可能会导致慢查询之外, 还有一些时间复杂度可能在 O(N) 以上的命令,例如: +除了这些 O(n) 时间复杂度的命令可能会导致慢查询之外,还有一些时间复杂度可能在 O(N) 以上的命令,例如: -- `ZRANGE`/`ZREVRANGE`:返回指定 Sorted Set 中指定排名范围内的所有元素。时间复杂度为 O(log(n)+m),n 为所有元素的数量, m 为返回的元素数量,当 m 和 n 相当大时,O(n) 的时间复杂度更小。 -- `ZREMRANGEBYRANK`/`ZREMRANGEBYSCORE`:移除 Sorted Set 中指定排名范围/指定 score 范围内的所有元素。时间复杂度为 O(log(n)+m),n 为所有元素的数量, m 被删除元素的数量,当 m 和 n 相当大时,O(n) 的时间复杂度更小。 -- ...... +- `ZRANGE`/`ZREVRANGE`:返回指定 Sorted Set 中指定排名范围内的所有元素。时间复杂度为 O(log(n)+m),n 为所有元素的数量,m 为返回的元素数量,当 m 和 n 相当大时,O(n) 的时间复杂度更小。 +- `ZREMRANGEBYRANK`/`ZREMRANGEBYSCORE`:移除 Sorted Set 中指定排名范围/指定 score 范围内的所有元素。时间复杂度为 O(log(n)+m),n 为所有元素的数量,m 被删除元素的数量,当 m 和 n 相当大时,O(n) 的时间复杂度更小。 +- …… #### 如何找到慢查询命令? +Redis 提供了一个内置的**慢查询日志 (Slow Log)** 功能,专门用来记录执行时间超过指定阈值的命令。这对于排查性能瓶颈、找出导致 Redis 阻塞的“慢”操作非常有帮助,原理和 MySQL 的慢查询日志类似。 + 在 `redis.conf` 文件中,我们可以使用 `slowlog-log-slower-than` 参数设置耗时命令的阈值,并使用 `slowlog-max-len` 参数设置耗时命令的最大记录条数。 -当 Redis 服务器检测到执行时间超过 `slowlog-log-slower-than`阈值的命令时,就会将该命令记录在慢查询日志(slow log) 中,这点和 MySQL 记录慢查询语句类似。当慢查询日志超过设定的最大记录条数之后,Redis 会把最早的执行命令依次舍弃。 +当 Redis 服务器检测到执行时间超过 `slowlog-log-slower-than` 阈值的命令时,就会将该命令记录在慢查询日志(slow log)中,这点和 MySQL 记录慢查询语句类似。当慢查询日志超过设定的最大记录条数之后,Redis 会把最早的执行命令依次舍弃。 -⚠️注意:由于慢查询日志会占用一定内存空间,如果设置最大记录条数过大,可能会导致内存占用过高的问题。 +⚠️ 注意:由于慢查询日志会占用一定内存空间,如果设置最大记录条数过大,可能会导致内存占用过高的问题。 - `slowlog-log-slower-than`和`slowlog-max-len`的默认配置如下(可以自行修改): +`slowlog-log-slower-than` 和 `slowlog-max-len` 的默认配置如下(可以自行修改): -```nginx +```properties # The following time is expressed in microseconds, so 1000000 is equivalent # to one second. Note that a negative number disables the slow log, while # a value of zero forces the logging of every command. @@ -513,9 +555,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 @@ -529,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`。 @@ -553,7 +595,7 @@ OK **相关问题**: -1. 什么是内存碎片?为什么会有 Redis 内存碎片? +1. 什么是内存碎片?为什么会有 Redis 内存碎片? 2. 如何清理 Redis 内存碎片? **参考答案**:[Redis 内存碎片详解](https://javaguide.cn/database/redis/redis-memory-fragmentation.html)。 @@ -564,7 +606,7 @@ OK #### 什么是缓存穿透? -缓存穿透说简单点就是大量请求的 key 是不合理的,**根本不存在于缓存中,也不存在于数据库中** 。这就导致这些请求直接到了数据库上,根本没有经过缓存这一层,对数据库造成了巨大的压力,可能直接就被这么多请求弄宕机了。 +缓存穿透说简单点就是大量请求的 key 是不合理的,**根本不存在于缓存中,也不存在于数据库中**。这就导致这些请求直接到了数据库上,根本没有经过缓存这一层,对数据库造成了巨大的压力,可能直接就被这么多请求弄宕机了。 ![缓存穿透](https://oss.javaguide.cn/github/javaguide/database/redis/redis-cache-penetration.png) @@ -576,9 +618,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 代码展示的话,差不多是下面这样的: @@ -605,37 +647,35 @@ public Object getObjectInclNullById(Integer id) { **2)布隆过滤器** -布隆过滤器是一个非常神奇的数据结构,通过它我们可以非常方便地判断一个给定数据是否存在于海量数据中。我们需要的就是判断 key 是否合法,有没有感觉布隆过滤器就是我们想要找的那个“人”。 +布隆过滤器是一个非常神奇的数据结构,通过它我们可以非常方便地判断一个给定数据是否存在于海量数据中。我们可以把它看作由二进制向量(或者说位数组)和一系列随机映射函数(哈希函数)两部分组成的数据结构。相比于我们平时常用的 List、Map、Set 等数据结构,它占用空间更少并且效率更高,但是缺点是其返回的结果是概率性的,而不是非常准确的。理论情况下添加到集合中的元素越多,误报的可能性就越大。并且,存放在布隆过滤器的数据不容易删除。 -具体是这样做的:把所有可能存在的请求的值都存放在布隆过滤器中,当用户请求过来,先判断用户发来的请求的值是否存在于布隆过滤器中。不存在的话,直接返回请求参数错误信息给客户端,存在的话才会走下面的流程。 +![Bloom Filter 的简单原理示意图](https://oss.javaguide.cn/github/javaguide/cs-basics/algorithms/bloom-filter-simple-schematic-diagram.png) -加入布隆过滤器之后的缓存处理流程图如下。 +Bloom Filter 会使用一个较大的 bit 数组来保存所有的数据,数组中的每个元素都只占用 1 bit ,并且每个元素只能是 0 或者 1(代表 false 或者 true),这也是 Bloom Filter 节省内存的核心所在。这样来算的话,申请一个 100w 个元素的位数组只占用 1000000Bit / 8 = 125000 Byte = 125000/1024 KB ≈ 122KB 的空间。 -![加入布隆过滤器之后的缓存处理流程图](https://oss.javaguide.cn/github/javaguide/database/redis/redis-cache-penetration-bloom-filter.png) +![位数组](https://oss.javaguide.cn/github/javaguide/cs-basics/algorithms/bloom-filter-bit-table.png) -但是,需要注意的是布隆过滤器可能会存在误判的情况。总结来说就是:**布隆过滤器说某个元素存在,小概率会误判。布隆过滤器说某个元素不在,那么这个元素一定不在。** +具体是这样做的:把所有可能存在的请求的值都存放在布隆过滤器中,当用户请求过来,先判断用户发来的请求的值是否存在于布隆过滤器中。不存在的话,直接返回请求参数错误信息给客户端,存在的话才会走下面的流程。 -_为什么会出现误判的情况呢? 我们还要从布隆过滤器的原理来说!_ +加入布隆过滤器之后的缓存处理流程图如下: -我们先来看一下,**当一个元素加入布隆过滤器中的时候,会进行哪些操作:** +![加入布隆过滤器之后的缓存处理流程图](https://oss.javaguide.cn/github/javaguide/database/redis/redis-cache-penetration-bloom-filter.png) -1. 使用布隆过滤器中的哈希函数对元素值进行计算,得到哈希值(有几个哈希函数得到几个哈希值)。 -2. 根据得到的哈希值,在位数组中把对应下标的值置为 1。 +更多关于布隆过滤器的详细介绍可以看看我的这篇原创:[不了解布隆过滤器?一文给你整的明明白白!](https://javaguide.cn/cs-basics/data-structure/bloom-filter.html),强烈推荐。 -我们再来看一下,**当我们需要判断一个元素是否存在于布隆过滤器的时候,会进行哪些操作:** +**3)接口限流** -1. 对给定元素再次进行相同的哈希计算; -2. 得到值之后判断位数组中的每个元素是否都为 1,如果值都为 1,那么说明这个值在布隆过滤器中,如果存在一个值不为 1,说明该元素不在布隆过滤器中。 +根据用户或者 IP 对接口进行限流,对于异常频繁的访问行为,还可以采取黑名单机制,例如将异常 IP 列入黑名单。 -然后,一定会出现这样一种情况:**不同的字符串可能哈希出来的位置相同。** (可以适当增加位数组大小或者调整我们的哈希函数来降低概率) +后面提到的缓存击穿和雪崩都可以配合接口限流来解决,毕竟这些问题的关键都是有很多请求落到了数据库上造成数据库压力过大。 -更多关于布隆过滤器的内容可以看我的这篇原创:[《不了解布隆过滤器?一文给你整的明明白白!》](https://javaguide.cn/cs-basics/data-structure/bloom-filter/) ,强烈推荐,个人感觉网上应该找不到总结的这么明明白白的文章了。 +限流的具体方案可以参考这篇文章:[服务限流详解](https://javaguide.cn/high-availability/limit-request.html)。 ### 缓存击穿 #### 什么是缓存击穿? -缓存击穿中,请求的 key 对应的是 **热点数据** ,该数据 **存在于数据库中,但不存在于缓存中(通常是因为缓存中的那份数据已经过期)** 。这就可能会导致瞬时大量的请求直接打到了数据库上,对数据库造成了巨大的压力,可能直接就被这么多请求弄宕机了。 +缓存击穿中,请求的 key 对应的是 **热点数据**,该数据 **存在于数据库中,但不存在于缓存中(通常是因为缓存中的那份数据已经过期)**。这就可能会导致瞬时大量的请求直接打到了数据库上,对数据库造成了巨大的压力,可能直接就被这么多请求弄宕机了。 ![缓存击穿](https://oss.javaguide.cn/github/javaguide/database/redis/redis-cache-breakdown.png) @@ -643,9 +683,9 @@ _为什么会出现误判的情况呢? 我们还要从布隆过滤器的原理 #### 有哪些解决办法? -- 设置热点数据永不过期或者过期时间比较长。 -- 针对热点数据提前预热,将其存入缓存中并设置合理的过期时间比如秒杀场景下的数据在秒杀结束之前不过期。 -- 请求数据库写数据到缓存之前,先获取互斥锁,保证只有一个请求会落到数据库上,减少数据库的压力。 +1. **永不过期**(不推荐):设置热点数据永不过期或者过期时间比较长。 +2. **提前预热**(推荐):针对热点数据提前预热,将其存入缓存中并设置合理的过期时间比如秒杀场景下的数据在秒杀结束之前不过期。 +3. **加锁**(看情况):在缓存失效后,通过设置互斥锁确保只有一个请求去查询数据库并更新缓存。 #### 缓存穿透和缓存击穿有什么区别? @@ -665,61 +705,80 @@ _为什么会出现误判的情况呢? 我们还要从布隆过滤器的原理 ![缓存雪崩](https://oss.javaguide.cn/github/javaguide/database/redis/redis-cache-avalanche.png) -举个例子:数据库中的大量数据在同一时间过期,这个时候突然有大量的请求需要访问这些过期的数据。这就导致大量的请求直接落到数据库上,对数据库造成了巨大的压力。 +举个例子:缓存中的大量数据在同一时间过期,这个时候突然有大量的请求需要访问这些过期的数据。这就导致大量的请求直接落到数据库上,对数据库造成了巨大的压力。 #### 有哪些解决办法? -**针对 Redis 服务不可用的情况:** +**针对 Redis 服务不可用的情况**: -1. 采用 Redis 集群,避免单机出现问题整个缓存服务都没办法使用。 -2. 限流,避免同时处理大量的请求。 +1. **Redis 集群**:采用 Redis 集群,避免单机出现问题整个缓存服务都没办法使用。Redis Cluster 和 Redis Sentinel 是两种最常用的 Redis 集群实现方案,详细介绍可以参考:[Redis 集群详解(付费)](https://javaguide.cn/database/redis/redis-cluster.html)。 +2. **多级缓存**:设置多级缓存,例如本地缓存+Redis 缓存的二级缓存组合,当 Redis 缓存出现问题时,还可以从本地缓存中获取到部分数据。 -**针对热点缓存失效的情况:** +**针对大量缓存同时失效的情况**: -1. 设置不同的失效时间比如随机设置缓存的失效时间。 -2. 缓存永不失效(不太推荐,实用性太差)。 -3. 设置二级缓存。 +1. **设置随机失效时间**(可选):为缓存设置随机的失效时间,例如在固定过期时间的基础上加上一个随机值,这样可以避免大量缓存同时到期,从而减少缓存雪崩的风险。 +2. **提前预热**(推荐):针对热点数据提前预热,将其存入缓存中并设置合理的过期时间,比如秒杀场景下的数据在秒杀结束之前不过期。 +3. **持久缓存策略**(看情况):虽然一般不推荐设置缓存永不过期,但对于某些关键性和变化不频繁的数据,可以考虑这种策略。 + +#### 缓存预热如何实现? + +常见的缓存预热方式有两种: + +1. 使用定时任务,比如 xxl-job,来定时触发缓存预热的逻辑,将数据库中的热点数据查询出来并存入缓存中。 +2. 使用消息队列,比如 Kafka,来异步地进行缓存预热,将数据库中的热点数据的主键或者 ID 发送到消息队列中,然后由缓存服务消费消息队列中的数据,根据主键或者 ID 查询数据库并更新缓存。 #### 缓存雪崩和缓存击穿有什么区别? -缓存雪崩和缓存击穿比较像,但缓存雪崩导致的原因是缓存中的大量或者所有数据失效,缓存击穿导致的原因主要是某个热点数据不存在与缓存中(通常是因为缓存中的那份数据已经过期)。 +缓存雪崩和缓存击穿比较像,但缓存雪崩导致的原因是缓存中的大量或者所有数据失效,缓存击穿导致的原因主要是某个热点数据不存在于缓存中(通常是因为缓存中的那份数据已经过期)。 ### 如何保证缓存和数据库数据的一致性? -细说的话可以扯很多,但是我觉得其实没太大必要(小声 BB:很多解决方案我也没太弄明白)。我个人觉得引入缓存之后,如果为了短时间的不一致性问题,选择让系统设计变得更加复杂的话,完全没必要。 +缓存和数据库一致性是个挺常见的技术挑战。引入缓存主要是为了提升性能、减轻数据库压力,但确实会带来数据不一致的风险。绝对的一致性往往意味着更高的系统复杂度和性能开销,所以实践中我们通常会根据业务场景选择合适的策略,在性能和一致性之间找到一个平衡点。 + +下面单独对 **Cache Aside Pattern(旁路缓存模式)** 来聊聊。这是非常常用的一种缓存读写策略,它的读写逻辑是这样的: -下面单独对 **Cache Aside Pattern(旁路缓存模式)** 来聊聊。 +- **读操作**: + 1. 先尝试从缓存读取数据。 + 2. 如果缓存命中,直接返回数据。 + 3. 如果缓存未命中,从数据库查询数据,将查到的数据放入缓存并返回数据。 +- **写操作**: + 1. 先更新数据库。 + 2. 再直接删除缓存中对应的数据。 -Cache Aside Pattern 中遇到写请求是这样的:更新 DB,然后直接删除 cache 。 +图解如下: -如果更新数据库成功,而删除缓存这一步失败的情况的话,简单说两个解决方案: +![](https://oss.javaguide.cn/github/javaguide/database/redis/cache-aside-write.png) -1. **缓存失效时间变短(不推荐,治标不治本)**:我们让缓存数据的过期时间变短,这样的话缓存就会从数据库中加载数据。另外,这种解决办法对于先操作缓存后操作数据库的场景不适用。 -2. **增加 cache 更新重试机制(常用)**:如果 cache 服务当前不可用导致缓存删除失败的话,我们就隔一段时间进行重试,重试次数可以自己定。如果多次重试还是失败的话,我们可以把当前更新失败的 key 存入队列中,等缓存服务可用之后,再将缓存中对应的 key 删除即可。 +![](https://oss.javaguide.cn/github/javaguide/database/redis/cache-aside-read.png) + +如果更新数据库成功,而删除缓存这一步失败的情况的话,简单说有两个解决方案: + +1. **缓存失效时间(TTL - Time To Live)变短**(不推荐,治标不治本):我们让缓存数据的过期时间变短,这样的话缓存就会从数据库中加载数据。另外,这种解决办法对于先操作缓存后操作数据库的场景不适用。 +2. **增加缓存更新重试机制**(常用):如果缓存服务当前不可用导致缓存删除失败的话,我们就隔一段时间进行重试,重试次数可以自己定。不过,这里更适合引入消息队列实现异步重试,将删除缓存重试的消息投递到消息队列,然后由专门的消费者来重试,直到成功。虽然说多引入了一个消息队列,但其整体带来的收益还是要更高一些。 相关文章推荐:[缓存和数据库一致性问题,看这篇就够了 - 水滴与银弹](https://mp.weixin.qq.com/s?__biz=MzIyOTYxNDI5OA==&mid=2247487312&idx=1&sn=fa19566f5729d6598155b5c676eee62d&chksm=e8beb8e5dfc931f3e35655da9da0b61c79f2843101c130cf38996446975014f958a6481aacf1&scene=178&cur_album_id=1699766580538032128#rd)。 ### 哪些情况可能会导致 Redis 阻塞? -单独抽了一篇文章来总结可能会导致 Redis 阻塞的情况:[Redis 常见阻塞原因总结](./redis-memory-fragmentation.md)。 +单独抽了一篇文章来总结可能会导致 Redis 阻塞的情况:[Redis 常见阻塞原因总结](https://javaguide.cn/database/redis/redis-common-blocking-problems-summary.html)。 ## Redis 集群 **Redis Sentinel**: 1. 什么是 Sentinel? 有什么用? -2. Sentinel 如何检测节点是否下线?主观下线与客观下线的区别? +2. Sentinel 如何检测节点是否下线?主观下线与客观下线的区别? 3. Sentinel 是如何实现故障转移的? 4. 为什么建议部署多个 sentinel 节点(哨兵集群)? -5. Sentinel 如何选择出新的 master(选举机制)? -6. 如何从 Sentinel 集群中选择出 Leader ? +5. Sentinel 如何选择出新的 master(选举机制)? +6. 如何从 Sentinel 集群中选择出 Leader? 7. Sentinel 可以防止脑裂吗? **Redis Cluster**: 1. 为什么需要 Redis Cluster?解决了什么问题?有什么优势? 2. Redis Cluster 是如何分片的? -3. 为什么 Redis Cluster 的哈希槽是 16384 个? +3. 为什么 Redis Cluster 的哈希槽是 16384 个? 4. 如何确定给定 key 的应该分布到哪个哈希槽中? 5. Redis Cluster 支持重新分配哈希槽吗? 6. Redis Cluster 扩容缩容期间可以提供服务吗? @@ -732,19 +791,23 @@ Cache Aside Pattern 中遇到写请求是这样的:更新 DB,然后直接删 实际使用 Redis 的过程中,我们尽量要准守一些常见的规范,比如: 1. 使用连接池:避免频繁创建关闭客户端连接。 -2. 尽量不使用 O(n)指令,使用 O(n) 命令时要关注 n 的数量:像 `KEYS *`、`HGETALL`、`LRANGE`、`SMEMBERS`、`SINTER`/`SUNION`/`SDIFF`等 O(n) 命令并非不能使用,但是需要明确 n 的值。另外,有遍历的需求可以使用 `HSCAN`、`SSCAN`、`ZSCAN` 代替。 -3. 使用批量操作减少网络传输:原生批量操作命令(比如 `MGET`、`MSET`等等)、pipeline、Lua 脚本。 -4. 尽量不适用 Redis 事务:Redis 事务实现的功能比较鸡肋,可以使用 Lua 脚本代替。 +2. 尽量不使用 O(n) 指令,使用 O(n) 命令时要关注 n 的数量:像 `KEYS *`、`HGETALL`、`LRANGE`、`SMEMBERS`、`SINTER`/`SUNION`/`SDIFF` 等 O(n) 命令并非不能使用,但是需要明确 n 的值。另外,有遍历的需求可以使用 `HSCAN`、`SSCAN`、`ZSCAN` 代替。 +3. 使用批量操作减少网络传输:原生批量操作命令(比如 `MGET`、`MSET` 等等)、pipeline、Lua 脚本。 +4. 尽量不使用 Redis 事务:Redis 事务实现的功能比较鸡肋,可以使用 Lua 脚本代替。 5. 禁止长时间开启 monitor:对性能影响比较大。 6. 控制 key 的生命周期:避免 Redis 中存放了太多不经常被访问的数据。 -7. ...... +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 问题的解决思路与方式探索: +- Redis 延迟问题全面排障指南: + + diff --git a/docs/database/redis/redis-skiplist.md b/docs/database/redis/redis-skiplist.md new file mode 100644 index 00000000000..11f0c32b665 --- /dev/null +++ b/docs/database/redis/redis-skiplist.md @@ -0,0 +1,779 @@ +--- +title: Redis为什么用跳表实现有序集合 +category: 数据库 +tag: + - Redis +--- + +## 前言 + +近几年针对 Redis 面试时会涉及常见数据结构的底层设计,其中就有这么一道比较有意思的面试题:“Redis 的有序集合底层为什么要用跳表,而不用平衡树、红黑树或者 B+树?”。 + +本文就以这道大厂常问的面试题为切入点,带大家详细了解一下跳表这个数据结构。 + +本文整体脉络如下图所示,笔者会从有序集合的基本使用到跳表的源码分析和实现,让你会对 Redis 的有序集合底层实现的跳表有着更深刻的理解和掌握。 + +![](https://oss.javaguide.cn/javaguide/database/redis/skiplist/202401222005468.png) + +## 跳表在 Redis 中的运用 + +这里我们需要先了解一下 Redis 用到跳表的数据结构有序集合的使用,Redis 有个比较常用的数据结构叫**有序集合(sorted set,简称 zset)**,正如其名它是一个可以保证有序且元素唯一的集合,所以它经常用于排行榜等需要进行统计排列的场景。 + +这里我们通过命令行的形式演示一下排行榜的实现,可以看到笔者分别输入 3 名用户:**xiaoming**、**xiaohong**、**xiaowang**,它们的**score**分别是 60、80、60,最终按照成绩升级降序排列。 + +```bash + +127.0.0.1:6379> zadd rankList 60 xiaoming +(integer) 1 +127.0.0.1:6379> zadd rankList 80 xiaohong +(integer) 1 +127.0.0.1:6379> zadd rankList 60 xiaowang +(integer) 1 + +# 返回有序集中指定区间内的成员,通过索引,分数从高到低 +127.0.0.1:6379> ZREVRANGE rankList 0 100 WITHSCORES +1) "xiaohong" +2) "80" +3) "xiaowang" +4) "60" +5) "xiaoming" +6) "60" +``` + +此时我们通过 `object` 指令查看 zset 的数据结构,可以看到当前有序集合存储的还是**ziplist(压缩列表)**。 + +```bash +127.0.0.1:6379> object encoding rankList +"ziplist" +``` + +因为设计者考虑到 Redis 数据存放于内存,为了节约宝贵的内存空间,在有序集合元素小于 64 字节且个数小于 128 的时候,会使用 ziplist,而这个阈值的默认值的设置就来自下面这两个配置项。 + +```bash +zset-max-ziplist-value 64 +zset-max-ziplist-entries 128 +``` + +一旦有序集合中的某个元素超出这两个其中的一个阈值它就会转为 **skiplist**(实际是 dict+skiplist,还会借用字典来提高获取指定元素的效率)。 + +我们不妨在添加一个大于 64 字节的元素,可以看到有序集合的底层存储转为 skiplist。 + +```bash +127.0.0.1:6379> zadd rankList 90 yigemingzihuichaoguo64zijiedeyonghumingchengyongyuceshitiaobiaodeshijiyunyong +(integer) 1 + +# 超过阈值,转为跳表 +127.0.0.1:6379> object encoding rankList +"skiplist" +``` + +也就是说,ZSet 有两种不同的实现,分别是 ziplist 和 skiplist,具体使用哪种结构进行存储的规则如下: + +- 当有序集合对象同时满足以下两个条件时,使用 ziplist: + 1. ZSet 保存的键值对数量少于 128 个; + 2. 每个元素的长度小于 64 字节。 +- 如果不满足上述两个条件,那么使用 skiplist 。 + +## 手写一个跳表 + +为了更好的回答上述问题以及更好的理解和掌握跳表,这里可以通过手写一个简单的跳表的形式来帮助读者理解跳表这个数据结构。 + +我们都知道有序链表在添加、查询、删除的平均时间复杂都都是 **O(n)** 即线性增长,所以一旦节点数量达到一定体量后其性能表现就会非常差劲。而跳表我们完全可以理解为在原始链表基础上,建立多级索引,通过多级索引检索定位将增删改查的时间复杂度变为 **O(log n)** 。 + +可能这里说的有些抽象,我们举个例子,以下图跳表为例,其原始链表存储按序存储 1-10,有 2 级索引,每级索引的索引个数都是基于下层元素个数的一半。 + +![](https://oss.javaguide.cn/javaguide/database/redis/skiplist/202401222005436.png) + +假如我们需要查询元素 6,其工作流程如下: + +1. 从 2 级索引开始,先来到节点 4。 +2. 查看 4 的后继节点,是 8 的 2 级索引,这个值大于 6,说明 2 级索引后续的索引都是大于 6 的,没有再往后搜寻的必要,我们索引向下查找。 +3. 来到 4 的 1 级索引,比对其后继节点为 6,查找结束。 + +相较于原始有序链表需要 6 次,我们的跳表通过建立多级索引,我们只需两次就直接定位到了目标元素,其查寻的复杂度被直接优化为**O(log n)**。 + +![](https://oss.javaguide.cn/javaguide/database/redis/skiplist/202401222005524.png) + +对应的添加也是一个道理,假如我们需要在这个有序集合中添加一个元素 7,那么我们就需要通过跳表找到**小于元素 7 的最大值**,也就是下图元素 6 的位置,将其插入到元素 6 的后面,让元素 6 的索引指向新插入的节点 7,其工作流程如下: + +1. 从 2 级索引开始定位到了元素 4 的索引。 +2. 查看索引 4 的后继索引为 8,索引向下推进。 +3. 来到 1 级索引,发现索引 4 后继索引为 6,小于插入元素 7,指针推进到索引 6 位置。 +4. 继续比较 6 的后继节点为索引 8,大于元素 7,索引继续向下。 +5. 最终我们来到 6 的原始节点,发现其后继节点为 7,指针没有继续向下的空间,自此我们可知元素 6 就是小于插入元素 7 的最大值,于是便将元素 7 插入。 + +![](https://oss.javaguide.cn/javaguide/database/redis/skiplist/202401222005480.png) + +这里我们又面临一个问题,我们是否需要为元素 7 建立索引,索引多高合适? + +我们上文提到,理想情况是每一层索引是下一层元素个数的二分之一,假设我们的总共有 16 个元素,对应各级索引元素个数应该是: + +```bash +1. 一级索引:16/2=8 +2. 二级索引:8/2 =4 +3. 三级索引:4/2=2 +``` + +由此我们用数学归纳法可知: + +```bash +1. 一级索引:16/2=16/2^1=8 +2. 二级索引:8/2 => 16/2^2 =4 +3. 三级索引:4/2=>16/2^3=2 +``` + +假设元素个数为 n,那么对应 k 层索引的元素个数 r 计算公式为: + +```bash +r=n/2^k +``` + +同理我们再来推断以下索引的最大高度,一般来说最高级索引的元素个数为 2,我们设元素总个数为 n,索引高度为 h,代入上述公式可得: + +```bash +2= n/2^h +=> 2*2^h=n +=> 2^(h+1)=n +=> h+1=log2^n +=> h=log2^n -1 +``` + +而 Redis 又是内存数据库,我们假设元素最大个数是**65536**,我们把**65536**代入上述公式可知最大高度为 16。所以我们建议添加一个元素后为其建立的索引高度不超过 16。 + +因为我们要求尽可能保证每一个上级索引都是下级索引的一半,在实现高度生成算法时,我们可以这样设计: + +1. 跳表的高度计算从原始链表开始,即默认情况下插入的元素的高度为 1,代表没有索引,只有元素节点。 +2. 设计一个为插入元素生成节点索引高度 level 的方法。 +3. 进行一次随机运算,随机数值范围为 0-1 之间。 +4. 如果随机数大于 0.5 则为当前元素添加一级索引,自此我们保证生成一级索引的概率为 **50%** ,这也就保证了 1 级索引理想情况下只有一半的元素会生成索引。 +5. 同理后续每次随机算法得到的值大于 0.5 时,我们的索引高度就加 1,这样就可以保证节点生成的 2 级索引概率为 **25%** ,3 级索引为 **12.5%** …… + +我们回过头,上述插入 7 之后,我们通过随机算法得到 2,即要为其建立 1 级索引: + +![](https://oss.javaguide.cn/javaguide/database/redis/skiplist/202401222005505.png) + +最后我们再来说说删除,假设我们这里要删除元素 10,我们必须定位到当前跳表**各层**元素小于 10 的最大值,索引执行步骤为: + +1. 2 级索引 4 的后继节点为 8,指针推进。 +2. 索引 8 无后继节点,该层无要删除的元素,指针直接向下。 +3. 1 级索引 8 后继节点为 10,说明 1 级索引 8 在进行删除时需要将自己的指针和 1 级索引 10 断开联系,将 10 删除。 +4. 1 级索引完成定位后,指针向下,后继节点为 9,指针推进。 +5. 9 的后继节点为 10,同理需要让其指向 null,将 10 删除。 + +![](https://oss.javaguide.cn/javaguide/database/redis/skiplist/202401222005503.png) + +### 模板定义 + +有了整体的思路之后,我们可以开始实现一个跳表了,首先定义一下跳表中的节点**Node**,从上文的演示中可以看出每一个**Node**它都包含以下几个元素: + +1. 存储的**value**值。 +2. 后继节点的地址。 +3. 多级索引。 + +为了更方便统一管理**Node**后继节点地址和多级索引指向的元素地址,笔者在**Node**中设置了一个**forwards**数组,用于记录原始链表节点的后继节点和多级索引的后继节点指向。 + +以下图为例,我们**forwards**数组长度为 5,其中**索引 0**记录的是原始链表节点的后继节点地址,而其余自底向上表示从 1 级索引到 4 级索引的后继节点指向。 + +![](https://oss.javaguide.cn/javaguide/database/redis/skiplist/202401222005347.png) + +于是我们的就有了这样一个代码定义,可以看出笔者对于数组的长度设置为固定的 16**(上文的推算最大高度建议是 16)**,默认**data**为-1,节点最大高度**maxLevel**初始化为 1,注意这个**maxLevel**的值代表原始链表加上索引的总高度。 + +```java +/** + * 跳表索引最大高度为16 + */ +private static final int MAX_LEVEL = 16; + +class Node { + private int data = -1; + private Node[] forwards = new Node[MAX_LEVEL]; + private int maxLevel = 0; + +} +``` + +### 元素添加 + +定义好节点之后,我们先实现以下元素的添加,添加元素时首先自然是设置**data**这一步我们直接根据将传入的**value**设置到**data**上即可。 + +然后就是高度**maxLevel**的设置 ,我们在上文也已经给出了思路,默认高度为 1,即只有一个原始链表节点,通过随机算法每次大于 0.5 索引高度加 1,由此我们得出高度计算的算法`randomLevel()`: + +```java +/** + * 理论来讲,一级索引中元素个数应该占原始数据的 50%,二级索引中元素个数占 25%,三级索引12.5% ,一直到最顶层。 + * 因为这里每一层的晋升概率是 50%。对于每一个新插入的节点,都需要调用 randomLevel 生成一个合理的层数。 + * 该 randomLevel 方法会随机生成 1~MAX_LEVEL 之间的数,且 : + * 50%的概率返回 1 + * 25%的概率返回 2 + * 12.5%的概率返回 3 ... + * @return + */ +private int randomLevel() { + int level = 1; + while (Math.random() > PROB && level < MAX_LEVEL) { + ++level; + } + return level; +} +``` + +然后再设置当前要插入的**Node**和**Node**索引的后继节点地址,这一步稍微复杂一点,我们假设当前节点的高度为 4,即 1 个节点加 3 个索引,所以我们创建一个长度为 4 的数组**maxOfMinArr** ,遍历各级索引节点中小于当前**value**的最大值。 + +假设我们要插入的**value**为 5,我们的数组查找结果当前节点的前驱节点和 1 级索引、2 级索引的前驱节点都为 4,三级索引为空。 + +![](https://oss.javaguide.cn/javaguide/database/redis/skiplist/202401222005299.png) + +然后我们基于这个数组**maxOfMinArr** 定位到各级的后继节点,让插入的元素 5 指向这些后继节点,而**maxOfMinArr**指向 5,结果如下图: + +![](https://oss.javaguide.cn/javaguide/database/redis/skiplist/202401222005369.png) + +转化成代码就是下面这个形式,是不是很简单呢?我们继续: + +```java +/** + * 默认情况下的高度为1,即只有自己一个节点 + */ +private int levelCount = 1; + +/** + * 跳表最底层的节点,即头节点 + */ +private Node h = new Node(); + +public void add(int value) { + + //随机生成高度 + int level = randomLevel(); + + Node newNode = new Node(); + newNode.data = value; + newNode.maxLevel = level; + + //创建一个node数组,用于记录小于当前value的最大值 + Node[] maxOfMinArr = new Node[level]; + //默认情况下指向头节点 + for (int i = 0; i < level; i++) { + maxOfMinArr[i] = h; + } + + //基于上述结果拿到当前节点的后继节点 + Node p = h; + for (int i = level - 1; i >= 0; i--) { + while (p.forwards[i] != null && p.forwards[i].data < value) { + p = p.forwards[i]; + } + maxOfMinArr[i] = p; + } + + //更新前驱节点的后继节点为当前节点newNode + for (int i = 0; i < level; i++) { + newNode.forwards[i] = maxOfMinArr[i].forwards[i]; + maxOfMinArr[i].forwards[i] = newNode; + } + + //如果当前newNode高度大于跳表最高高度则更新levelCount + if (levelCount < level) { + levelCount = level; + } + +} +``` + +### 元素查询 + +查询逻辑比较简单,从跳表最高级的索引开始定位找到小于要查的 value 的最大值,以下图为例,我们希望查找到节点 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; // 从头节点开始 + + // 从最高层级索引开始,逐层向下 + 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.forwards[i] 为 null,或者 p.forwards[i].data >= value + // 或者 p 是当前层级中小于 value 的最大节点(如果存在这样的节点) + } + + // 经过所有层级的查找,p 现在是原始链表(0级索引)中 + // 小于目标值 value 的最大节点(或者头节点,如果所有元素都大于等于 value) + + // 检查 p 在原始链表中的下一个节点是否是目标值 + if (p.forwards[0] != null && p.forwards[0].data == value) { + return p.forwards[0]; // 找到了,返回该节点 + } + + return null; // 未找到 +} +``` + +### 元素删除 + +最后是删除逻辑,需要查找各层级小于要删除节点的最大值,假设我们要删除 10: + +1. 3 级索引得到小于 10 的最大值为 5,继续向下。 +2. 2 级索引从索引 5 开始查找,发现小于 10 的最大值为 8,继续向下。 +3. 同理 1 级索引得到 8,继续向下。 +4. 原始节点找到 9。 +5. 从最高级索引开始,查看每个小于 10 的节点后继节点是否为 10,如果等于 10,则让这个节点指向 10 的后继节点,将节点 10 及其索引交由 GC 回收。 + +![](https://oss.javaguide.cn/javaguide/database/redis/skiplist/202401222005350.png) + +```java +/** + * 删除 + * + * @param value + */ +public void delete(int value) { + Node p = h; + //找到各级节点小于value的最大值 + Node[] updateArr = new Node[levelCount]; + for (int i = levelCount - 1; i >= 0; i--) { + while (p.forwards[i] != null && p.forwards[i].data < value) { + p = p.forwards[i]; + } + updateArr[i] = p; + } + //查看原始层节点前驱是否等于value,若等于则说明存在要删除的值 + if (p.forwards[0] != null && p.forwards[0].data == value) { + //从最高级索引开始查看其前驱是否等于value,若等于则将当前节点指向value节点的后继节点 + for (int i = levelCount - 1; i >= 0; i--) { + if (updateArr[i].forwards[i] != null && updateArr[i].forwards[i].data == value) { + updateArr[i].forwards[i] = updateArr[i].forwards[i].forwards[i]; + } + } + } + + //从最高级开始查看是否有一级索引为空,若为空则层级减1 + while (levelCount > 1 && h.forwards[levelCount - 1] == null) { + levelCount--; + } + +} +``` + +### 完整代码以及测试 + +完整代码如下,读者可自行参阅: + +```java +public class SkipList { + + /** + * 跳表索引最大高度为16 + */ + private static final int MAX_LEVEL = 16; + + /** + * 每个节点添加一层索引高度的概率为二分之一 + */ + private static final float PROB = 0.5 f; + + /** + * 默认情况下的高度为1,即只有自己一个节点 + */ + private int levelCount = 1; + + /** + * 跳表最底层的节点,即头节点 + */ + private Node h = new Node(); + + public SkipList() {} + + public class Node { + private int data = -1; + /** + * + */ + private Node[] forwards = new Node[MAX_LEVEL]; + private int maxLevel = 0; + + @Override + public String toString() { + return "Node{" + + "data=" + data + + ", maxLevel=" + maxLevel + + '}'; + } + } + + public void add(int value) { + + //随机生成高度 + int level = randomLevel(); + + Node newNode = new Node(); + newNode.data = value; + newNode.maxLevel = level; + + //创建一个node数组,用于记录小于当前value的最大值 + Node[] maxOfMinArr = new Node[level]; + //默认情况下指向头节点 + for (int i = 0; i < level; i++) { + maxOfMinArr[i] = h; + } + + //基于上述结果拿到当前节点的后继节点 + Node p = h; + for (int i = level - 1; i >= 0; i--) { + while (p.forwards[i] != null && p.forwards[i].data < value) { + p = p.forwards[i]; + } + maxOfMinArr[i] = p; + } + + //更新前驱节点的后继节点为当前节点newNode + for (int i = 0; i < level; i++) { + newNode.forwards[i] = maxOfMinArr[i].forwards[i]; + maxOfMinArr[i].forwards[i] = newNode; + } + + //如果当前newNode高度大于跳表最高高度则更新levelCount + if (levelCount < level) { + levelCount = level; + } + + } + + /** + * 理论来讲,一级索引中元素个数应该占原始数据的 50%,二级索引中元素个数占 25%,三级索引12.5% ,一直到最顶层。 + * 因为这里每一层的晋升概率是 50%。对于每一个新插入的节点,都需要调用 randomLevel 生成一个合理的层数。 + * 该 randomLevel 方法会随机生成 1~MAX_LEVEL 之间的数,且 : + * 50%的概率返回 1 + * 25%的概率返回 2 + * 12.5%的概率返回 3 ... + * @return + */ + private int randomLevel() { + int level = 1; + while (Math.random() > PROB && level < MAX_LEVEL) { + ++level; + } + return level; + } + + public Node get(int value) { + Node p = h; + //找到小于value的最大值 + for (int i = levelCount - 1; i >= 0; i--) { + while (p.forwards[i] != null && p.forwards[i].data < value) { + p = p.forwards[i]; + } + } + //如果p的前驱节点等于value则直接返回 + if (p.forwards[0] != null && p.forwards[0].data == value) { + return p.forwards[0]; + } + + return null; + } + + /** + * 删除 + * + * @param value + */ + public void delete(int value) { + Node p = h; + //找到各级节点小于value的最大值 + Node[] updateArr = new Node[levelCount]; + for (int i = levelCount - 1; i >= 0; i--) { + while (p.forwards[i] != null && p.forwards[i].data < value) { + p = p.forwards[i]; + } + updateArr[i] = p; + } + //查看原始层节点前驱是否等于value,若等于则说明存在要删除的值 + if (p.forwards[0] != null && p.forwards[0].data == value) { + //从最高级索引开始查看其前驱是否等于value,若等于则将当前节点指向value节点的后继节点 + for (int i = levelCount - 1; i >= 0; i--) { + if (updateArr[i].forwards[i] != null && updateArr[i].forwards[i].data == value) { + updateArr[i].forwards[i] = updateArr[i].forwards[i].forwards[i]; + } + } + } + + //从最高级开始查看是否有一级索引为空,若为空则层级减1 + while (levelCount > 1 && h.forwards[levelCount - 1] == null) { + levelCount--; + } + + } + + public void printAll() { + Node p = h; + //基于最底层的非索引层进行遍历,只要后继节点不为空,则速速出当前节点,并移动到后继节点 + while (p.forwards[0] != null) { + System.out.println(p.forwards[0]); + p = p.forwards[0]; + } + + } + +} +``` + +对应测试代码和输出结果如下: + +```java +public static void main(String[] args) { + SkipList skipList = new SkipList(); + for (int i = 0; i < 24; i++) { + skipList.add(i); + } + + System.out.println("**********输出添加结果**********"); + skipList.printAll(); + + SkipList.Node node = skipList.get(22); + System.out.println("**********查询结果:" + node+" **********"); + + skipList.delete(22); + System.out.println("**********删除结果**********"); + skipList.printAll(); + + + } +``` + +输出结果: + +```bash +**********输出添加结果********** +Node{data=0, maxLevel=2} +Node{data=1, maxLevel=3} +Node{data=2, maxLevel=1} +Node{data=3, maxLevel=1} +Node{data=4, maxLevel=2} +Node{data=5, maxLevel=2} +Node{data=6, maxLevel=2} +Node{data=7, maxLevel=2} +Node{data=8, maxLevel=4} +Node{data=9, maxLevel=1} +Node{data=10, maxLevel=1} +Node{data=11, maxLevel=1} +Node{data=12, maxLevel=1} +Node{data=13, maxLevel=1} +Node{data=14, maxLevel=1} +Node{data=15, maxLevel=3} +Node{data=16, maxLevel=4} +Node{data=17, maxLevel=2} +Node{data=18, maxLevel=1} +Node{data=19, maxLevel=1} +Node{data=20, maxLevel=1} +Node{data=21, maxLevel=3} +Node{data=22, maxLevel=1} +Node{data=23, maxLevel=1} +**********查询结果:Node{data=22, maxLevel=1} ********** +**********删除结果********** +Node{data=0, maxLevel=2} +Node{data=1, maxLevel=3} +Node{data=2, maxLevel=1} +Node{data=3, maxLevel=1} +Node{data=4, maxLevel=2} +Node{data=5, maxLevel=2} +Node{data=6, maxLevel=2} +Node{data=7, maxLevel=2} +Node{data=8, maxLevel=4} +Node{data=9, maxLevel=1} +Node{data=10, maxLevel=1} +Node{data=11, maxLevel=1} +Node{data=12, maxLevel=1} +Node{data=13, maxLevel=1} +Node{data=14, maxLevel=1} +Node{data=15, maxLevel=3} +Node{data=16, maxLevel=4} +Node{data=17, maxLevel=2} +Node{data=18, maxLevel=1} +Node{data=19, maxLevel=1} +Node{data=20, maxLevel=1} +Node{data=21, maxLevel=3} +Node{data=23, maxLevel=1} +``` + +**Redis 跳表的特点**: + +1. 采用**双向链表**,不同于上面的示例,存在一个回退指针。主要用于简化操作,例如删除某个元素时,还需要找到该元素的前驱节点,使用回退指针会非常方便。 +2. `score` 值可以重复,如果 `score` 值一样,则按照 ele(节点存储的值,为 sds)字典排序 +3. Redis 跳跃表默认允许最大的层数是 32,被源码中 `ZSKIPLIST_MAXLEVEL` 定义。 + +## 和其余三种数据结构的比较 + +最后,我们再来回答一下文章开头的那道面试题: “Redis 的有序集合底层为什么要用跳表,而不用平衡树、红黑树或者 B+树?”。 + +### 平衡树 vs 跳表 + +先来说说它和平衡树的比较,平衡树我们又会称之为 **AVL 树**,是一个严格的平衡二叉树,平衡条件必须满足(所有节点的左右子树高度差不超过 1,即平衡因子为范围为 `[-1,1]`)。平衡树的插入、删除和查询的时间复杂度和跳表一样都是 **O(log n)** 。 + +对于范围查询来说,它也可以通过中序遍历的方式达到和跳表一样的效果。但是它的每一次插入或者删除操作都需要保证整颗树左右节点的绝对平衡,只要不平衡就要通过旋转操作来保持平衡,这个过程是比较耗时的。 + +![](https://oss.javaguide.cn/javaguide/database/redis/skiplist/202401222005312.png) + +跳表诞生的初衷就是为了克服平衡树的一些缺点,跳表的发明者在论文[《Skip lists: a probabilistic alternative to balanced trees》](https://15721.courses.cs.cmu.edu/spring2018/papers/08-oltpindexes1/pugh-skiplists-cacm1990.pdf)中有详细提到: + +![](https://oss.javaguide.cn/github/javaguide/database/redis/skiplist-a-probabilistic-alternative-to-balanced-trees.png) + +> Skip lists are a data structure that can be used in place of balanced trees. Skip lists use probabilistic balancing rather than strictly enforced balancing and as a result the algorithms for insertion and deletion in skip lists are much simpler and significantly faster than equivalent algorithms for balanced trees. +> +> 跳表是一种可以用来代替平衡树的数据结构。跳表使用概率平衡而不是严格强制的平衡,因此,跳表中的插入和删除算法比平衡树的等效算法简单得多,速度也快得多。 + +笔者这里也贴出了 AVL 树插入操作的核心代码,可以看出每一次添加操作都需要进行一次递归定位插入位置,然后还需要根据回溯到根节点检查沿途的各层节点是否失衡,再通过旋转节点的方式进行调整。 + +```java +// 向二分搜索树中添加新的元素(key, value) +public void add(K key, V value) { + root = add(root, key, value); +} + +// 向以node为根的二分搜索树中插入元素(key, value),递归算法 +// 返回插入新节点后二分搜索树的根 +private Node add(Node node, K key, V value) { + + if (node == null) { + size++; + return new Node(key, value); + } + + if (key.compareTo(node.key) < 0) + node.left = add(node.left, key, value); + else if (key.compareTo(node.key) > 0) + node.right = add(node.right, key, value); + else // key.compareTo(node.key) == 0 + node.value = value; + + node.height = 1 + Math.max(getHeight(node.left), getHeight(node.right)); + + int balanceFactor = getBalanceFactor(node); + + // LL型需要右旋 + if (balanceFactor > 1 && getBalanceFactor(node.left) >= 0) { + return rightRotate(node); + } + + //RR型失衡需要左旋 + if (balanceFactor < -1 && getBalanceFactor(node.right) <= 0) { + return leftRotate(node); + } + + //LR需要先左旋成LL型,然后再右旋 + if (balanceFactor > 1 && getBalanceFactor(node.left) < 0) { + node.left = leftRotate(node.left); + return rightRotate(node); + } + + //RL + if (balanceFactor < -1 && getBalanceFactor(node.right) > 0) { + node.right = rightRotate(node.right); + return leftRotate(node); + } + return node; +} +``` + +### 红黑树 vs 跳表 + +红黑树(Red Black Tree)也是一种自平衡二叉查找树,它的查询性能略微逊色于 AVL 树,但插入和删除效率更高。红黑树的插入、删除和查询的时间复杂度和跳表一样都是 **O(log n)** 。 + +红黑树是一个**黑平衡树**,即从任意节点到另外一个叶子叶子节点,它所经过的黑节点是一样的。当对它进行插入操作时,需要通过旋转和染色(红黑变换)来保证黑平衡。不过,相较于 AVL 树为了维持平衡的开销要小一些。关于红黑树的详细介绍,可以查看这篇文章:[红黑树](https://javaguide.cn/cs-basics/data-structure/red-black-tree.html)。 + +相比较于红黑树来说,跳表的实现也更简单一些。并且,按照区间来查找数据这个操作,红黑树的效率没有跳表高。 + +![](https://oss.javaguide.cn/javaguide/database/redis/skiplist/202401222005709.png) + +对应红黑树添加的核心代码如下,读者可自行参阅理解: + +```java +private Node < K, V > add(Node < K, V > node, K key, V val) { + + if (node == null) { + size++; + return new Node(key, val); + + } + + if (key.compareTo(node.key) < 0) { + node.left = add(node.left, key, val); + } else if (key.compareTo(node.key) > 0) { + node.right = add(node.right, key, val); + } else { + node.val = val; + } + + //左节点不为红,右节点为红,左旋 + if (isRed(node.right) && !isRed(node.left)) { + node = leftRotate(node); + } + + //左链右旋 + if (isRed(node.left) && isRed(node.left.left)) { + node = rightRotate(node); + } + + //颜色翻转 + if (isRed(node.left) && isRed(node.right)) { + flipColors(node); + } + + return node; +} +``` + +### B+树 vs 跳表 + +想必使用 MySQL 的读者都知道 B+树这个数据结构,B+树是一种常用的数据结构,具有以下特点: + +1. **多叉树结构**:它是一棵多叉树,每个节点可以包含多个子节点,减小了树的高度,查询效率高。 +2. **存储效率高**:其中非叶子节点存储多个 key,叶子节点存储 value,使得每个节点更够存储更多的键,根据索引进行范围查询时查询效率更高。- +3. **平衡性**:它是绝对的平衡,即树的各个分支高度相差不大,确保查询和插入时间复杂度为 **O(log n)** 。 +4. **顺序访问**:叶子节点间通过链表指针相连,范围查询表现出色。 +5. **数据均匀分布**:B+树插入时可能会导致数据重新分布,使得数据在整棵树分布更加均匀,保证范围查询和删除效率。 + +![](https://oss.javaguide.cn/javaguide/database/redis/skiplist/202401222005649.png) + +所以,B+树更适合作为数据库和文件系统中常用的索引结构之一,它的核心思想是通过可能少的 IO 定位到尽可能多的索引来获得查询数据。对于 Redis 这种内存数据库来说,它对这些并不感冒,因为 Redis 作为内存数据库它不可能存储大量的数据,所以对于索引不需要通过 B+树这种方式进行维护,只需按照概率进行随机维护即可,节约内存。而且使用跳表实现 zset 时相较前者来说更简单一些,在进行插入时只需通过索引将数据插入到链表中合适的位置再随机维护一定高度的索引即可,也不需要像 B+树那样插入时发现失衡时还需要对节点分裂与合并。 + +### Redis 作者给出的理由 + +当然我们也可以通过 Redis 的作者自己给出的理由: + +> There are a few reasons: +> 1、They are not very memory intensive. It's up to you basically. Changing parameters about the probability of a node to have a given number of levels will make then less memory intensive than btrees. +> 2、A sorted set is often target of many ZRANGE or ZREVRANGE operations, that is, traversing the skip list as a linked list. With this operation the cache locality of skip lists is at least as good as with other kind of balanced trees. +> 3、They are simpler to implement, debug, and so forth. For instance thanks to the skip list simplicity I received a patch (already in Redis master) with augmented skip lists implementing ZRANK in O(log(N)). It required little changes to the code. + +翻译过来的意思就是: + +> 有几个原因: +> +> 1、它们不是很占用内存。这主要取决于你。改变节点拥有给定层数的概率的参数,会使它们比 B 树更节省内存。 +> +> 2、有序集合经常是许多 ZRANGE 或 ZREVRANGE 操作的目标,也就是说,以链表的方式遍历跳表。通过这种操作,跳表的缓存局部性至少和其他类型的平衡树一样好。 +> +> 3、它们更容易实现、调试等等。例如,由于跳表的简单性,我收到了一个补丁(已经在 Redis 主分支中),用增强的跳表实现了 O(log(N))的 ZRANK。它只需要对代码做很少的修改。 + +## 小结 + +本文通过大量篇幅介绍跳表的工作原理和实现,帮助读者更进一步的熟悉跳表这一数据结构的优劣,最后再结合各个数据结构操作的特点进行比对,从而帮助读者更好的理解这道面试题,建议读者实现理解跳表时,尽可能配合执笔模拟来了解跳表的增删改查详细过程。 + +## 参考 + +- 为啥 redis 使用跳表(skiplist)而不是使用 red-black?: +- Skip List--跳表(全网最详细的跳表文章没有之一): +- Redis 对象与底层数据结构详解: +- Redis 有序集合(sorted set): +- 红黑树和跳表比较: +- 为什么 redis 的 zset 用跳跃表而不用 b+ tree?: diff --git a/docs/database/sql/sql-questions-01.md b/docs/database/sql/sql-questions-01.md index c8b2cd2ce53..4bf08f0fa0b 100644 --- a/docs/database/sql/sql-questions-01.md +++ b/docs/database/sql/sql-questions-01.md @@ -1,5 +1,5 @@ --- -title: SQL常见面试题总结 +title: SQL常见面试题总结(1) category: 数据库 tag: - 数据库基础 @@ -50,11 +50,11 @@ FROM Customers 答案: ```sql -SELECT distinct prod_id +SELECT DISTINCT prod_id FROM OrderItems ``` -知识点:`distinct` 用于返回列中的唯一不同值。 +知识点:`DISTINCT` 用于返回列中的唯一不同值。 ### 检索所有列 @@ -189,17 +189,17 @@ ORDER BY vend_name DESC 下面的运算符可以在 `WHERE` 子句中使用: -| 运算符 | 描述 | -| :------ | :--------------------------------------------------------- | -| = | 等于 | -| <> | 不等于。**注释:**在 SQL 的一些版本中,该操作符可被写成 != | -| > | 大于 | -| < | 小于 | -| >= | 大于等于 | -| <= | 小于等于 | -| BETWEEN | 在某个范围内 | -| LIKE | 搜索某种模式 | -| IN | 指定针对某个列的多个可能值 | +| 运算符 | 描述 | +| :------ | :----------------------------------------------------------- | +| = | 等于 | +| <> | 不等于。 **注释:** 在 SQL 的一些版本中,该操作符可被写成 != | +| > | 大于 | +| < | 小于 | +| >= | 大于等于 | +| <= | 小于等于 | +| BETWEEN | 在某个范围内 | +| LIKE | 搜索某种模式 | +| IN | 指定针对某个列的多个可能值 | ### 返回固定价格的产品 @@ -287,9 +287,10 @@ ORDER BY prod_price 答案: ```sql -SELECT DISTINCT order_num +SELECT order_num FROM OrderItems -WHERE quantity >= 100 +GROUP BY order_num +HAVING SUM(quantity) >= 100 ``` ## 高级数据过滤 @@ -357,7 +358,7 @@ WHERE prod_id IN ('BR01', 'BR02', 'BR03') AND quantity >= 100 ```sql SELECT prod_name, prod_price FROM Products -WHERE prod_price BETWEEN 3 AND 6 +WHERE prod_price >= 3 and prod_price <= 6 ORDER BY prod_price ``` @@ -952,10 +953,9 @@ WHERE condition; ```sql SELECT cust_id FROM Orders -WHERE order_num IN (SELECT order_num +WHERE order_num IN (SELECT DISTINCT order_num FROM OrderItems - GROUP BY order_num - HAVING Sum(item_price) >= 10) + where item_price >= 10) ``` ### 确定哪些订单购买了 prod_id 为 BR01 的产品(一) @@ -1027,11 +1027,11 @@ ORDER BY order_date `Customers` 表代表顾客信息,`cust_id` 为顾客 id,`cust_email` 为顾客 email -| cust_id | cust_email | -| ------- | --------------- | -| cust10 | cust10@cust.com | -| cust1 | cust1@cust.com | -| cust2 | cust2@cust.com | +| cust_id | cust_email | +| ------- | ----------------- | +| cust10 | | +| cust1 | | +| cust2 | | 【问题】返回购买 `prod_id` 为 `BR01` 的产品的所有顾客的电子邮件(`Customers` 表中的 `cust_email`),结果无需排序。 @@ -1095,13 +1095,14 @@ WHERE b.prod_id = 'BR01' ```sql # 写法 1:子查询 -SELECT o.cust_id AS cust_id, tb.total_ordered AS total_ordered -FROM (SELECT order_num, Sum(item_price * quantity) AS total_ordered +SELECT o.cust_id, SUM(tb.total_ordered) AS `total_ordered` +FROM (SELECT order_num, SUM(item_price * quantity) AS total_ordered FROM OrderItems GROUP BY order_num) AS tb, Orders o WHERE tb.order_num = o.order_num -ORDER BY total_ordered DESC +GROUP BY o.cust_id +ORDER BY total_ordered DESC; # 写法 2:连接表 SELECT b.cust_id, Sum(a.quantity * a.item_price) AS total_ordered @@ -1111,6 +1112,8 @@ GROUP BY cust_id ORDER BY total_ordered DESC ``` +关于写法一详细介绍可以参考: [issue#2402:写法 1 存在的错误以及修改方法](https://github.com/Snailclimb/JavaGuide/issues/2402)。 + ### 从 Products 表中检索所有的产品名称以及对应的销售总数 `Products` 表中检索所有的产品名称:`prod_name`、产品 id:`prod_id` @@ -1336,7 +1339,7 @@ ORDER BY c.cust_name,o.order_num 这是错误的!只对 `cust_name` 进行聚类确实符合题意,但是不符合 `GROUP BY` 的语法。 -select 语句中,如果没有 `GROUP BY` 语句,那么 `cust_name`、`order_num` 会返回若干个值,而 `sum(quantity _ item_price)` 只返回一个值,通过 `group by` `cust_name` 可以让 `cust_name` 和 `sum(quantity _ item_price)` 一一对应起来,或者说**聚类**,所以同样的,也要对 `order_num` 进行聚类。 +select 语句中,如果没有 `GROUP BY` 语句,那么 `cust_name`、`order_num` 会返回若干个值,而 `sum(quantity * item_price)` 只返回一个值,通过 `group by` `cust_name` 可以让 `cust_name` 和 `sum(quantity * item_price)` 一一对应起来,或者说**聚类**,所以同样的,也要对 `order_num` 进行聚类。 > **一句话,select 中的字段要么都聚类,要么都不聚类** @@ -1417,11 +1420,11 @@ ORDER BY order_date `Customers` 表代表顾客信息,`cust_id` 为顾客 id,`cust_email` 为顾客 email -| cust_id | cust_email | -| ------- | --------------- | -| cust10 | cust10@cust.com | -| cust1 | cust1@cust.com | -| cust2 | cust2@cust.com | +| cust_id | cust_email | +| ------- | ----------------- | +| cust10 | | +| cust1 | | +| cust2 | | 【问题】返回购买 `prod_id` 为 BR01 的产品的所有顾客的电子邮件(`Customers` 表中的 `cust_email`),结果无需排序。 @@ -1653,12 +1656,12 @@ ORDER BY prod_name 注意:`vend_id` 列会显示在多个表中,因此在每次引用它时都需要完全限定它。 ```sql -SELECT vend_id, COUNT(prod_id) AS prod_id -FROM Vendors -LEFT JOIN Products +SELECT v.vend_id, COUNT(prod_id) AS prod_id +FROM Vendors v +LEFT JOIN Products p USING(vend_id) -GROUP BY vend_id -ORDER BY vend_id +GROUP BY v.vend_id +ORDER BY v.vend_id ``` ## 组合查询 @@ -1779,11 +1782,11 @@ ORDER BY prod_name 表 `Customers` 含有字段 `cust_name` 顾客名、`cust_contact` 顾客联系方式、`cust_state` 顾客州、`cust_email` 顾客 `email` -| cust_name | cust_contact | cust_state | cust_email | -| --------- | ------------ | ---------- | --------------- | -| cust10 | 8695192 | MI | cust10@cust.com | -| cust1 | 8695193 | MI | cust1@cust.com | -| cust2 | 8695194 | IL | cust2@cust.com | +| cust_name | cust_contact | cust_state | cust_email | +| --------- | ------------ | ---------- | ----------------- | +| cust10 | 8695192 | MI | | +| cust1 | 8695193 | MI | | +| cust2 | 8695194 | IL | | 【问题】修正下面错误的 SQL @@ -1821,3 +1824,5 @@ FROM Customers WHERE cust_state = 'MI' or cust_state = 'IL' ORDER BY cust_name; ``` + + diff --git a/docs/database/sql/sql-questions-02.md b/docs/database/sql/sql-questions-02.md new file mode 100644 index 00000000000..2a4a3e496c6 --- /dev/null +++ b/docs/database/sql/sql-questions-02.md @@ -0,0 +1,450 @@ +--- +title: SQL常见面试题总结(2) +category: 数据库 +tag: + - 数据库基础 + - SQL +--- + +> 题目来源于:[牛客题霸 - SQL 进阶挑战](https://www.nowcoder.com/exam/oj?page=1&tab=SQL%E7%AF%87&topicId=240) + +## 增删改操作 + +SQL 插入记录的方式汇总: + +- **普通插入(全字段)** :`INSERT INTO table_name VALUES (value1, value2, ...)` +- **普通插入(限定字段)** :`INSERT INTO table_name (column1, column2, ...) VALUES (value1, value2, ...)` +- **多条一次性插入** :`INSERT INTO table_name (column1, column2, ...) VALUES (value1_1, value1_2, ...), (value2_1, value2_2, ...), ...` +- **从另一个表导入** :`INSERT INTO table_name SELECT * FROM table_name2 [WHERE key=value]` +- **带更新的插入** :`REPLACE INTO table_name VALUES (value1, value2, ...)`(注意这种原理是检测到主键或唯一性索引键重复就删除原记录后重新插入) + +### 插入记录(一) + +**描述**:牛客后台会记录每个用户的试卷作答记录到 `exam_record` 表,现在有两个用户的作答记录详情如下: + +- 用户 1001 在 2021 年 9 月 1 日晚上 10 点 11 分 12 秒开始作答试卷 9001,并在 50 分钟后提交,得了 90 分; +- 用户 1002 在 2021 年 9 月 4 日上午 7 点 1 分 2 秒开始作答试卷 9002,并在 10 分钟后退出了平台。 + +试卷作答记录表`exam_record`中,表已建好,其结构如下,请用一条语句将这两条记录插入表中。 + +| Filed | Type | Null | Key | Extra | Default | Comment | +| ----------- | ---------- | ---- | --- | -------------- | ------- | -------- | +| id | int(11) | NO | PRI | auto_increment | (NULL) | 自增 ID | +| uid | int(11) | NO | | | (NULL) | 用户 ID | +| exam_id | int(11) | NO | | | (NULL) | 试卷 ID | +| start_time | datetime | NO | | | (NULL) | 开始时间 | +| submit_time | datetime | YES | | | (NULL) | 提交时间 | +| score | tinyint(4) | YES | | | (NULL) | 得分 | + +**答案**: + +```sql +// 存在自增主键,无需手动赋值 +INSERT INTO exam_record (uid, exam_id, start_time, submit_time, score) VALUES +(1001, 9001, '2021-09-01 22:11:12', '2021-09-01 23:01:12', 90), +(1002, 9002, '2021-09-04 07:01:02', NULL, NULL); +``` + +### 插入记录(二) + +**描述**:现有一张试卷作答记录表`exam_record`,结构如下表,其中包含多年来的用户作答试卷记录,由于数据越来越多,维护难度越来越大,需要对数据表内容做精简,历史数据做备份。 + +表`exam_record`: + +| Filed | Type | Null | Key | Extra | Default | Comment | +| ----------- | ---------- | ---- | --- | -------------- | ------- | -------- | +| id | int(11) | NO | PRI | auto_increment | (NULL) | 自增 ID | +| uid | int(11) | NO | | | (NULL) | 用户 ID | +| exam_id | int(11) | NO | | | (NULL) | 试卷 ID | +| start_time | datetime | NO | | | (NULL) | 开始时间 | +| submit_time | datetime | YES | | | (NULL) | 提交时间 | +| score | tinyint(4) | YES | | | (NULL) | 得分 | + +我们已经创建了一张新表`exam_record_before_2021`用来备份 2021 年之前的试题作答记录,结构和`exam_record`表一致,请将 2021 年之前的已完成了的试题作答纪录导入到该表。 + +**答案**: + +```sql +INSERT INTO exam_record_before_2021 (uid, exam_id, start_time, submit_time, score) +SELECT uid,exam_id,start_time,submit_time,score +FROM exam_record +WHERE YEAR(submit_time) < 2021; +``` + +### 插入记录(三) + +**描述**:现在有一套 ID 为 9003 的高难度 SQL 试卷,时长为一个半小时,请你将 2021-01-01 00:00:00 作为发布时间插入到试题信息表`examination_info`,不管该 ID 试卷是否存在,都要插入成功,请尝试插入它。 + +试题信息表`examination_info`: + +| Filed | Type | Null | Key | Extra | Default | Comment | +| ------------ | ----------- | ---- | --- | -------------- | ------- | ------------ | +| id | int(11) | NO | PRI | auto_increment | (NULL) | 自增 ID | +| exam_id | int(11) | NO | UNI | | (NULL) | 试卷 ID | +| tag | varchar(32) | YES | | | (NULL) | 类别标签 | +| difficulty | varchar(8) | YES | | | (NULL) | 难度 | +| duration | int(11) | NO | | | (NULL) | 时长(分钟数) | +| release_time | datetime | YES | | | (NULL) | 发布时间 | + +**答案**: + +```sql +REPLACE INTO examination_info VALUES + (NULL, 9003, "SQL", "hard", 90, "2021-01-01 00:00:00"); +``` + +### 更新记录(一) + +**描述**:现在有一张试卷信息表 `examination_info`, 表结构如下图所示: + +| Filed | Type | Null | Key | Extra | Default | Comment | +| ------------ | -------- | ---- | --- | -------------- | ------- | -------- | +| id | int(11) | NO | PRI | auto_increment | (NULL) | 自增 ID | +| exam_id | int(11) | NO | UNI | | (NULL) | 试卷 ID | +| tag | char(32) | YES | | | (NULL) | 类别标签 | +| difficulty | char(8) | YES | | | (NULL) | 难度 | +| duration | int(11) | NO | | | (NULL) | 时长 | +| release_time | datetime | YES | | | (NULL) | 发布时间 | + +请把**examination_info**表中`tag`为`PYTHON`的`tag`字段全部修改为`Python`。 + +**思路**:这题有两种解题思路,最容易想到的是直接`update + where`来指定条件更新,第二种就是根据要修改的字段进行查找替换 + +**答案一**: + +```sql +UPDATE examination_info SET tag = 'Python' WHERE tag='PYTHON' +``` + +**答案二**: + +```sql +UPDATE examination_info +SET tag = REPLACE(tag,'PYTHON','Python') + +# REPLACE (目标字段,"查找内容","替换内容") +``` + +### 更新记录(二) + +**描述**:现有一张试卷作答记录表 exam_record,其中包含多年来的用户作答试卷记录,结构如下表:作答记录表 `exam_record`: **`submit_time`** 为 完成时间 (注意这句话) + +| Filed | Type | Null | Key | Extra | Default | Comment | +| ----------- | ---------- | ---- | --- | -------------- | ------- | -------- | +| id | int(11) | NO | PRI | auto_increment | (NULL) | 自增 ID | +| uid | int(11) | NO | | | (NULL) | 用户 ID | +| exam_id | int(11) | NO | | | (NULL) | 试卷 ID | +| start_time | datetime | NO | | | (NULL) | 开始时间 | +| submit_time | datetime | YES | | | (NULL) | 提交时间 | +| score | tinyint(4) | YES | | | (NULL) | 得分 | + +**题目要求**:请把 `exam_record` 表中 2021 年 9 月 1 日==之前==开始作答的==未完成==记录全部改为被动完成,即:将完成时间改为'2099-01-01 00:00:00',分数改为 0。 + +**思路**:注意题干中的关键字(已经高亮) `" xxx 时间 "`之前这个条件, 那么这里马上就要想到要进行时间的比较 可以直接 `xxx_time < "2021-09-01 00:00:00",` 也可以采用`date()`函数来进行比较;第二个条件就是 `"未完成"`, 即完成时间为 NULL,也就是题目中的提交时间 ----- `submit_time 为 NULL`。 + +**答案**: + +```sql +UPDATE exam_record SET submit_time = '2099-01-01 00:00:00', score = 0 WHERE DATE(start_time) < "2021-09-01" AND submit_time IS null +``` + +### 删除记录(一) + +**描述**:现有一张试卷作答记录表 `exam_record`,其中包含多年来的用户作答试卷记录,结构如下表: + +作答记录表`exam_record:` **`start_time`** 是试卷开始时间`submit_time` 是交卷,即结束时间。 + +| Filed | Type | Null | Key | Extra | Default | Comment | +| ----------- | ---------- | ---- | --- | -------------- | ------- | -------- | +| id | int(11) | NO | PRI | auto_increment | (NULL) | 自增 ID | +| uid | int(11) | NO | | | (NULL) | 用户 ID | +| exam_id | int(11) | NO | | | (NULL) | 试卷 ID | +| start_time | datetime | NO | | | (NULL) | 开始时间 | +| submit_time | datetime | YES | | | (NULL) | 提交时间 | +| score | tinyint(4) | YES | | | (NULL) | 得分 | + +**要求**:请删除`exam_record`表中作答时间小于 5 分钟整且分数不及格(及格线为 60 分)的记录; + +**思路**:这一题虽然是练习删除,仔细看确是考察对时间函数的用法,这里提及的分钟数比较,常用的函数有 **`TIMEDIFF`**和**`TIMESTAMPDIFF`** ,两者用法稍有区别,后者更为灵活,这都是看个人习惯。 + +1.  `TIMEDIFF`:两个时间之间的差值 + +```sql +TIMEDIFF(time1, time2) +``` + +两者参数都是必须的,都是一个时间或者日期时间表达式。如果指定的参数不合法或者是 NULL,那么函数将返回 NULL。 + +对于这题而言,可以用在 minute 函数里面,因为 TIMEDIFF 计算出来的是时间的差值,在外面套一个 MINUTE 函数,计算出来的就是分钟数。 + +2. `TIMESTAMPDIFF`:用于计算两个日期的时间差 + +```sql +TIMESTAMPDIFF(unit,datetime_expr1,datetime_expr2) +# 参数说明 +#unit: 日期比较返回的时间差单位,常用可选值如下: +SECOND:秒 +MINUTE:分钟 +HOUR:小时 +DAY:天 +WEEK:星期 +MONTH:月 +QUARTER:季度 +YEAR:年 +# TIMESTAMPDIFF函数返回datetime_expr2 - datetime_expr1的结果(人话: 后面的 - 前面的 即2-1),其中datetime_expr1和datetime_expr2可以是DATE或DATETIME类型值(人话:可以是“2023-01-01”, 也可以是“2023-01-01- 00:00:00”) +``` + +这题需要进行分钟的比较,那么就是 TIMESTAMPDIFF(MINUTE, 开始时间, 结束时间) < 5 + +**答案**: + +```sql +DELETE FROM exam_record WHERE MINUTE (TIMEDIFF(submit_time , start_time)) < 5 AND score < 60 +``` + +```sql +DELETE FROM exam_record WHERE TIMESTAMPDIFF(MINUTE, start_time, submit_time) < 5 AND score < 60 +``` + +### 删除记录(二) + +**描述**:现有一张试卷作答记录表`exam_record`,其中包含多年来的用户作答试卷记录,结构如下表: + +作答记录表`exam_record`:`start_time` 是试卷开始时间,`submit_time` 是交卷时间,即结束时间,如果未完成的话,则为空。 + +| Filed | Type | Null | Key | Extra | Default | Comment | +| ----------- | ---------- | :--: | --- | -------------- | ------- | -------- | +| id | int(11) | NO | PRI | auto_increment | (NULL) | 自增 ID | +| uid | int(11) | NO | | | (NULL) | 用户 ID | +| exam_id | int(11) | NO | | | (NULL) | 试卷 ID | +| start_time | datetime | NO | | | (NULL) | 开始时间 | +| submit_time | datetime | YES | | | (NULL) | 提交时间 | +| score | tinyint(4) | YES | | | (NULL) | 分数 | + +**要求**:请删除`exam_record`表中未完成作答==或==作答时间小于 5 分钟整的记录中,开始作答时间最早的 3 条记录。 + +**思路**:这题比较简单,但是要注意题干中给出的信息,结束时间,如果未完成的话,则为空,这个其实就是一个条件 + +还有一个条件就是小于 5 分钟,跟上题类似,但是这里是**或**,即两个条件满足一个就行;另外就是稍微考察到了排序和 limit 的用法。 + +**答案**: + +```sql +DELETE FROM exam_record WHERE submit_time IS null OR TIMESTAMPDIFF(MINUTE, start_time, submit_time) < 5 +ORDER BY start_time +LIMIT 3 +# 默认就是asc, desc是降序排列 +``` + +### 删除记录(三) + +**描述**:现有一张试卷作答记录表 exam_record,其中包含多年来的用户作答试卷记录,结构如下表: + +| Filed | Type | Null | Key | Extra | Default | Comment | +| ----------- | ---------- | :--: | --- | -------------- | ------- | -------- | +| id | int(11) | NO | PRI | auto_increment | (NULL) | 自增 ID | +| uid | int(11) | NO | | | (NULL) | 用户 ID | +| exam_id | int(11) | NO | | | (NULL) | 试卷 ID | +| start_time | datetime | NO | | | (NULL) | 开始时间 | +| submit_time | datetime | YES | | | (NULL) | 提交时间 | +| score | tinyint(4) | YES | | | (NULL) | 分数 | + +**要求**:请删除`exam_record`表中所有记录,==并重置自增主键== + +**思路**:这题考察对三种删除语句的区别,注意高亮部分,要求重置主键; + +- `DROP`: 清空表,删除表结构,不可逆 +- `TRUNCATE`: 格式化表,不删除表结构,不可逆 +- `DELETE`:删除数据,可逆 + +这里选用`TRUNCATE`的原因是:TRUNCATE 只能作用于表;`TRUNCATE`会清空表中的所有行,但表结构及其约束、索引等保持不变;`TRUNCATE`会重置表的自增值;使用`TRUNCATE`后会使表和索引所占用的空间会恢复到初始大小。 + +这题也可以采用`DELETE`来做,但是在删除后,还需要手动`ALTER`表结构来设置主键初始值; + +同理也可以采用`DROP`来做,直接删除整张表,包括表结构,然后再新建表即可。 + +**答案**: + +```sql +TRUNCATE exam_record; +``` + +## 表与索引操作 + +### 创建一张新表 + +**描述**:现有一张用户信息表,其中包含多年来在平台注册过的用户信息,随着牛客平台的不断壮大,用户量飞速增长,为了高效地为高活跃用户提供服务,现需要将部分用户拆分出一张新表。 + +原来的用户信息表: + +| Filed | Type | Null | Key | Default | Extra | Comment | +| ------------- | ----------- | ---- | --- | ----------------- | -------------- | -------- | +| id | int(11) | NO | PRI | (NULL) | auto_increment | 自增 ID | +| uid | int(11) | NO | UNI | (NULL) | | 用户 ID | +| nick_name | varchar(64) | YES | | (NULL) | | 昵称 | +| achievement | int(11) | YES | | 0 | | 成就值 | +| level | int(11) | YES | | (NULL) | | 用户等级 | +| job | varchar(32) | YES | | (NULL) | | 职业方向 | +| register_time | datetime | YES | | CURRENT_TIMESTAMP | | 注册时间 | + +作为数据分析师,请**创建一张优质用户信息表 user_info_vip**,表结构和用户信息表一致。 + +你应该返回的输出如下表格所示,请写出建表语句将表格中所有限制和说明记录到表里。 + +| Filed | Type | Null | Key | Default | Extra | Comment | +| ------------- | ----------- | ---- | --- | ----------------- | -------------- | -------- | +| id | int(11) | NO | PRI | (NULL) | auto_increment | 自增 ID | +| uid | int(11) | NO | UNI | (NULL) | | 用户 ID | +| nick_name | varchar(64) | YES | | (NULL) | | 昵称 | +| achievement | int(11) | YES | | 0 | | 成就值 | +| level | int(11) | YES | | (NULL) | | 用户等级 | +| job | varchar(32) | YES | | (NULL) | | 职业方向 | +| register_time | datetime | YES | | CURRENT_TIMESTAMP | | 注册时间 | + +**思路**:如果这题给出了旧表的名称,可直接`create table 新表 as select * from 旧表;` 但是这题并没有给出旧表名称,所以需要自己创建,注意默认值和键的创建即可,比较简单。(注意:如果是在牛客网上面执行,请注意 comment 中要和题目中的 comment 保持一致,包括大小写,否则不通过,还有字符也要设置) + +答案: + +```sql +CREATE TABLE IF NOT EXISTS user_info_vip( + id INT(11) PRIMARY KEY AUTO_INCREMENT COMMENT'自增ID', + uid INT(11) UNIQUE NOT NULL COMMENT '用户ID', + nick_name VARCHAR(64) COMMENT'昵称', + achievement INT(11) DEFAULT 0 COMMENT '成就值', + `level` INT(11) COMMENT '用户等级', + job VARCHAR(32) COMMENT '职业方向', + register_time DATETIME DEFAULT CURRENT_TIMESTAMP COMMENT '注册时间' +)CHARACTER SET UTF8 +``` + +### 修改表 + +**描述**: 现有一张用户信息表`user_info`,其中包含多年来在平台注册过的用户信息。 + +**用户信息表 `user_info`:** + +| Filed | Type | Null | Key | Default | Extra | Comment | +| ------------- | ----------- | ---- | --- | ----------------- | -------------- | -------- | +| id | int(11) | NO | PRI | (NULL) | auto_increment | 自增 ID | +| uid | int(11) | NO | UNI | (NULL) | | 用户 ID | +| nick_name | varchar(64) | YES | | (NULL) | | 昵称 | +| achievement | int(11) | YES | | 0 | | 成就值 | +| level | int(11) | YES | | (NULL) | | 用户等级 | +| job | varchar(32) | YES | | (NULL) | | 职业方向 | +| register_time | datetime | YES | | CURRENT_TIMESTAMP | | 注册时间 | + +**要求:**请在用户信息表,字段 `level` 的后面增加一列最多可保存 15 个汉字的字段 `school`;并将表中 `job` 列名改为 `profession`,同时 `varchar` 字段长度变为 10;`achievement` 的默认值设置为 0。 + +**思路**:首先做这题之前,需要了解 ALTER 语句的基本用法: + +- 添加一列:`ALTER TABLE 表名 ADD COLUMN 列名 类型 【first | after 字段名】;`(first : 在某列之前添加,after 反之) +- 修改列的类型或约束:`ALTER TABLE 表名 MODIFY COLUMN 列名 新类型 【新约束】;` +- 修改列名:`ALTER TABLE 表名 change COLUMN 旧列名 新列名 类型;` +- 删除列:`ALTER TABLE 表名 drop COLUMN 列名;` +- 修改表名:`ALTER TABLE 表名 rename 【to】 新表名;` +- 将某一列放到第一列:`ALTER TABLE 表名 MODIFY COLUMN 列名 类型 first;` + +`COLUMN` 关键字其实可以省略不写,这里基于规范还是罗列出来了。 + +在修改时,如果有多个修改项,可以写到一起,但要注意格式 + +**答案**: + +```sql +ALTER TABLE user_info + ADD school VARCHAR(15) AFTER level, + CHANGE job profession VARCHAR(10), + MODIFY achievement INT(11) DEFAULT 0; +``` + +### 删除表 + +**描述**:现有一张试卷作答记录表 `exam_record`,其中包含多年来的用户作答试卷记录。一般每年都会为 `exam_record` 表建立一张备份表 `exam_record_{YEAR},{YEAR}` 为对应年份。 + +现在随着数据越来越多,存储告急,请你把很久前的(2011 到 2014 年)备份表都删掉(如果存在的话)。 + +**思路**:这题很简单,直接删就行,如果嫌麻烦,可以将要删除的表用逗号隔开,写到一行;这里肯定会有小伙伴问:如果要删除很多张表呢?放心,如果要删除很多张表,可以写脚本来进行删除。 + +**答案**: + +```sql +DROP TABLE IF EXISTS exam_record_2011; +DROP TABLE IF EXISTS exam_record_2012; +DROP TABLE IF EXISTS exam_record_2013; +DROP TABLE IF EXISTS exam_record_2014; +``` + +### 创建索引 + +**描述**:现有一张试卷信息表 `examination_info`,其中包含各种类型试卷的信息。为了对表更方便快捷地查询,需要在 `examination_info` 表创建以下索引, + +规则如下:在 `duration` 列创建普通索引 `idx_duration`、在 `exam_id` 列创建唯一性索引 `uniq_idx_exam_id`、在 `tag` 列创建全文索引 `full_idx_tag`。 + +根据题意,将返回如下结果: + +| examination_info | 0 | PRIMARY | 1 | id | A | 0 | | | | BTREE | +| ---------------- | --- | ---------------- | --- | -------- | --- | --- | --- | --- | --- | -------- | +| examination_info | 0 | uniq_idx_exam_id | 1 | exam_id | A | 0 | | | YES | BTREE | +| examination_info | 1 | idx_duration | 1 | duration | A | 0 | | | | BTREE | +| examination_info | 1 | full_idx_tag | 1 | tag | | 0 | | | YES | FULLTEXT | + +备注:后台会通过 `SHOW INDEX FROM examination_info` 语句来对比输出结果 + +**思路**:做这题首先需要了解常见的索引类型: + +- B-Tree 索引:B-Tree(或称为平衡树)索引是最常见和默认的索引类型。它适用于各种查询条件,可以快速定位到符合条件的数据。B-Tree 索引适用于普通的查找操作,支持等值查询、范围查询和排序。 +- 唯一索引:唯一索引与普通的 B-Tree 索引类似,不同之处在于它要求被索引的列的值是唯一的。这意味着在插入或更新数据时,MySQL 会验证索引列的唯一性。 +- 主键索引:主键索引是一种特殊的唯一索引,它用于唯一标识表中的每一行数据。每个表只能有一个主键索引,它可以帮助提高数据的访问速度和数据完整性。 +- 全文索引:全文索引用于在文本数据中进行全文搜索。它支持在文本字段中进行关键字搜索,而不仅仅是简单的等值或范围查找。全文索引适用于需要进行全文搜索的应用场景。 + +```sql +-- 示例: +-- 添加B-Tree索引: + CREATE INDEX idx_name(索引名) ON 表名 (字段名); -- idx_name为索引名,以下都是 +-- 创建唯一索引: + CREATE UNIQUE INDEX idx_name ON 表名 (字段名); +-- 创建一个主键索引: + ALTER TABLE 表名 ADD PRIMARY KEY (字段名); +-- 创建一个全文索引 + ALTER TABLE 表名 ADD FULLTEXT INDEX idx_name (字段名); + +-- 通过以上示例,可以看出create 和 alter 都可以添加索引 +``` + +有了以上的基础知识之后,该题答案也就浮出水面了。 + +**答案**: + +```sql +ALTER TABLE examination_info + ADD INDEX idx_duration(duration), + ADD UNIQUE INDEX uniq_idx_exam_id(exam_id), + ADD FULLTEXT INDEX full_idx_tag(tag); +``` + +### 删除索引 + +**描述**:请删除`examination_info`表上的唯一索引 uniq_idx_exam_id 和全文索引 full_idx_tag。 + +**思路**:该题考察删除索引的基本语法: + +```sql +-- 使用 DROP INDEX 删除索引 +DROP INDEX idx_name ON 表名; + +-- 使用 ALTER TABLE 删除索引 +ALTER TABLE employees DROP INDEX idx_email; +``` + +这里需要注意的是:在 MySQL 中,一次删除多个索引的操作是不支持的。每次删除索引时,只能指定一个索引名称进行删除。 + +而且 **DROP** 命令需要慎用!!! + +**答案**: + +```sql +DROP INDEX uniq_idx_exam_id ON examination_info; +DROP INDEX full_idx_tag ON examination_info; +``` + + diff --git a/docs/database/sql/sql-questions-03.md b/docs/database/sql/sql-questions-03.md new file mode 100644 index 00000000000..f5acd8fc5c8 --- /dev/null +++ b/docs/database/sql/sql-questions-03.md @@ -0,0 +1,1301 @@ +--- +title: SQL常见面试题总结(3) +category: 数据库 +tag: + - 数据库基础 + - SQL +--- + +> 题目来源于:[牛客题霸 - SQL 进阶挑战](https://www.nowcoder.com/exam/oj?page=1&tab=SQL%E7%AF%87&topicId=240) + +较难或者困难的题目可以根据自身实际情况和面试需要来决定是否要跳过。 + +## 聚合函数 + +### SQL 类别高难度试卷得分的截断平均值(较难) + +**描述**: 牛客的运营同学想要查看大家在 SQL 类别中高难度试卷的得分情况。 + +请你帮她从`exam_record`数据表中计算所有用户完成 SQL 类别高难度试卷得分的截断平均值(去掉一个最大值和一个最小值后的平均值)。 + +示例数据:`examination_info`(`exam_id` 试卷 ID, tag 试卷类别, `difficulty` 试卷难度, `duration` 考试时长, `release_time` 发布时间) + +| id | exam_id | tag | difficulty | duration | release_time | +| --- | ------- | ---- | ---------- | -------- | ------------------- | +| 1 | 9001 | SQL | hard | 60 | 2020-01-01 10:00:00 | +| 2 | 9002 | 算法 | medium | 80 | 2020-08-02 10:00:00 | + +示例数据:`exam_record`(uid 用户 ID, exam_id 试卷 ID, start_time 开始作答时间, submit_time 交卷时间, score 得分) + +| id | uid | exam_id | start_time | submit_time | score | +| --- | ---- | ------- | ------------------- | ------------------- | ------ | +| 1 | 1001 | 9001 | 2020-01-02 09:01:01 | 2020-01-02 09:21:01 | 80 | +| 2 | 1001 | 9001 | 2021-05-02 10:01:01 | 2021-05-02 10:30:01 | 81 | +| 3 | 1001 | 9001 | 2021-06-02 19:01:01 | 2021-06-02 19:31:01 | 84 | +| 4 | 1001 | 9002 | 2021-09-05 19:01:01 | 2021-09-05 19:40:01 | 89 | +| 5 | 1001 | 9001 | 2021-09-02 12:01:01 | (NULL) | (NULL) | +| 6 | 1001 | 9002 | 2021-09-01 12:01:01 | (NULL) | (NULL) | +| 7 | 1002 | 9002 | 2021-02-02 19:01:01 | 2021-02-02 19:30:01 | 87 | +| 8 | 1002 | 9001 | 2021-05-05 18:01:01 | 2021-05-05 18:59:02 | 90 | +| 9 | 1003 | 9001 | 2021-09-07 12:01:01 | 2021-09-07 10:31:01 | 50 | +| 10 | 1004 | 9001 | 2021-09-06 10:01:01 | (NULL) | (NULL) | + +根据输入你的查询结果如下: + +| tag | difficulty | clip_avg_score | +| --- | ---------- | -------------- | +| SQL | hard | 81.7 | + +从`examination_info`表可知,试卷 9001 为高难度 SQL 试卷,该试卷被作答的得分有[80,81,84,90,50],去除最高分和最低分后为[80,81,84],平均分为 81.6666667,保留一位小数后为 81.7 + +**输入描述:** + +输入数据中至少有 3 个有效分数 + +**思路一:** 要找出高难度 sql 试卷,肯定需要联 examination_info 这张表,然后找出高难度的课程,由 examination_info 得知,高难度 sql 的 exam_id 为 9001,那么等下就以 exam_id = 9001 作为条件去查询; + +先找出 9001 号考试 `select * from exam_record where exam_id = 9001` + +然后,找出最高分 `select max(score) 最高分 from exam_record where exam_id = 9001` + +接着,找出最低分 `select min(score) 最低分 from exam_record where exam_id = 9001` + +在查询出来的分数结果集当中,去掉最高分和最低分,最直观能想到的就是 NOT IN 或者 用 NOT EXISTS 也行,这里以 NOT IN 来做 + +首先将主体写出来`select tag, difficulty, round(avg(score), 1) clip_avg_score from examination_info info INNER JOIN exam_record record` + +**小 tips** : MYSQL 的 `ROUND()` 函数 ,`ROUND(X)`返回参数 X 最近似的整数 `ROUND(X,D)`返回 X ,其值保留到小数点后 D 位,第 D 位的保留方式为四舍五入。 + +再将上面的 "碎片" 语句拼凑起来即可, 注意在 NOT IN 中两个子查询用 UNION ALL 来关联,用 union 把 max 和 min 的结果集中在一行当中,这样形成一列多行的效果。 + +**答案一:** + +```sql +SELECT tag, difficulty, ROUND(AVG(score), 1) clip_avg_score + FROM examination_info info INNER JOIN exam_record record + WHERE info.exam_id = record.exam_id + AND record.exam_id = 9001 + AND record.score NOT IN( + SELECT MAX(score) + FROM exam_record + WHERE exam_id = 9001 + UNION ALL + SELECT MIN(score) + FROM exam_record + WHERE exam_id = 9001 + ) +``` + +这是最直观,也是最容易想到的解法,但是还有待改进,这算是投机取巧过关,其实严格按照题目要求应该这么写: + +```sql +SELECT tag, + difficulty, + ROUND(AVG(score), 1) clip_avg_score +FROM examination_info info +INNER JOIN exam_record record +WHERE info.exam_id = record.exam_id + AND record.exam_id = + (SELECT examination_info.exam_id + FROM examination_info + WHERE tag = 'SQL' + AND difficulty = 'hard' ) + AND record.score NOT IN + (SELECT MAX(score) + FROM exam_record + WHERE exam_id = + (SELECT examination_info.exam_id + FROM examination_info + WHERE tag = 'SQL' + AND difficulty = 'hard' ) + UNION ALL SELECT MIN(score) + FROM exam_record + WHERE exam_id = + (SELECT examination_info.exam_id + FROM examination_info + WHERE tag = 'SQL' + AND difficulty = 'hard' ) ) +``` + +然而你会发现,重复的语句非常多,所以可以利用`WITH`来抽取公共部分 + +**`WITH` 子句介绍**: + +`WITH` 子句,也称为公共表表达式(Common Table Expression,CTE),是在 SQL 查询中定义临时表的方式。它可以让我们在查询中创建一个临时命名的结果集,并且可以在同一查询中引用该结果集。 + +基本用法: + +```sql +WITH cte_name (column1, column2, ..., columnN) AS ( + -- 查询体 + SELECT ... + FROM ... + WHERE ... +) +-- 主查询 +SELECT ... +FROM cte_name +WHERE ... +``` + +`WITH` 子句由以下几个部分组成: + +- `cte_name`: 给临时表起一个名称,可以在主查询中引用。 +- `(column1, column2, ..., columnN)`: 可选,指定临时表的列名。 +- `AS`: 必需,表示开始定义临时表。 +- `CTE 查询体`: 实际的查询语句,用于定义临时表中的数据。 + +`WITH` 子句的主要用途之一是增强查询的可读性和可维护性,尤其在涉及多个嵌套子查询或需要重复使用相同的查询逻辑时。通过将这些逻辑放在一个命名的临时表中,我们可以更清晰地组织查询,并消除重复代码。 + +此外,`WITH` 子句还可以在复杂的查询中实现递归查询。递归查询允许我们在单个查询中执行对同一表的多次迭代,逐步构建结果集。这在处理层次结构数据、组织结构和树状结构等场景中非常有用。 + +**小细节**:MySQL 5.7 版本以及之前的版本不支持在 `WITH` 子句中直接使用别名。 + +下面是改进后的答案: + +```sql +WITH t1 AS + (SELECT record.*, + info.tag, + info.difficulty + FROM exam_record record + INNER JOIN examination_info info ON record.exam_id = info.exam_id + WHERE info.tag = "SQL" + AND info.difficulty = "hard" ) +SELECT tag, + difficulty, + ROUND(AVG(score), 1) +FROM t1 +WHERE score NOT IN + (SELECT max(score) + FROM t1 + UNION SELECT min(score) + FROM t1) +``` + +**思路二:** + +- 筛选 SQL 高难度试卷:`where tag="SQL" and difficulty="hard"` +- 计算截断平均值:`(和-最大值-最小值) / (总个数-2)`: + - `(sum(score) - max(score) - min(score)) / (count(score) - 2)` + - 有一个缺点就是,如果最大值和最小值有多个,这个方法就很难筛选出来, 但是题目中说了----->**`去掉一个最大值和一个最小值后的平均值`**, 所以这里可以用这个公式。 + +**答案二:** + +```sql +SELECT info.tag, + info.difficulty, + ROUND((SUM(record.score)- MIN(record.score)- MAX(record.score)) / (COUNT(record.score)- 2), 1) AS clip_avg_score +FROM examination_info info, + exam_record record +WHERE info.exam_id = record.exam_id + AND info.tag = "SQL" + AND info.difficulty = "hard"; +``` + +### 统计作答次数 + +有一个试卷作答记录表 `exam_record`,请从中统计出总作答次数 `total_pv`、试卷已完成作答数 `complete_pv`、已完成的试卷数 `complete_exam_cnt`。 + +示例数据 `exam_record` 表(`uid` 用户 ID, `exam_id` 试卷 ID, `start_time` 开始作答时间, `submit_time` 交卷时间, `score` 得分): + +| id | uid | exam_id | start_time | submit_time | score | +| --- | ---- | ------- | ------------------- | ------------------- | ------ | +| 1 | 1001 | 9001 | 2020-01-02 09:01:01 | 2020-01-02 09:21:01 | 80 | +| 2 | 1001 | 9001 | 2021-05-02 10:01:01 | 2021-05-02 10:30:01 | 81 | +| 3 | 1001 | 9001 | 2021-06-02 19:01:01 | 2021-06-02 19:31:01 | 84 | +| 4 | 1001 | 9002 | 2021-09-05 19:01:01 | 2021-09-05 19:40:01 | 89 | +| 5 | 1001 | 9001 | 2021-09-02 12:01:01 | (NULL) | (NULL) | +| 6 | 1001 | 9002 | 2021-09-01 12:01:01 | (NULL) | (NULL) | +| 7 | 1002 | 9002 | 2021-02-02 19:01:01 | 2021-02-02 19:30:01 | 87 | +| 8 | 1002 | 9001 | 2021-05-05 18:01:01 | 2021-05-05 18:59:02 | 90 | +| 9 | 1003 | 9001 | 2021-09-07 12:01:01 | 2021-09-07 10:31:01 | 50 | +| 10 | 1004 | 9001 | 2021-09-06 10:01:01 | (NULL) | (NULL) | + +示例输出: + +| total_pv | complete_pv | complete_exam_cnt | +| -------- | ----------- | ----------------- | +| 10 | 7 | 2 | + +解释:表示截止当前,有 10 次试卷作答记录,已完成的作答次数为 7 次(中途退出的为未完成状态,其交卷时间和份数为 NULL),已完成的试卷有 9001 和 9002 两份。 + +**思路**: 这题一看到统计次数,肯定第一时间就要想到用`COUNT`这个函数来解决,问题是要统计不同的记录,该怎么来写?使用子查询就能解决这个题目(这题用 case when 也能写出来,解法类似,逻辑不同而已);首先在做这个题之前,让我们先来了解一下`COUNT`的基本用法; + +`COUNT()` 函数的基本语法如下所示: + +```sql +COUNT(expression) +``` + +其中,`expression` 可以是列名、表达式、常量或通配符。下面是一些常见的用法示例: + +1. 计算表中所有行的数量: + +```sql +SELECT COUNT(*) FROM table_name; +``` + +2. 计算特定列非空(不为 NULL)值的数量: + +```sql +SELECT COUNT(column_name) FROM table_name; +``` + +3. 计算满足条件的行数: + +```sql +SELECT COUNT(*) FROM table_name WHERE condition; +``` + +4. 结合 `GROUP BY` 使用,计算分组后每个组的行数: + +```sql +SELECT column_name, COUNT(*) FROM table_name GROUP BY column_name; +``` + +5. 计算不同列组合的唯一组合数: + +```sql +SELECT COUNT(DISTINCT column_name1, column_name2) FROM table_name; +``` + +在使用 `COUNT()` 函数时,如果不指定任何参数或者使用 `COUNT(*)`,将会计算所有行的数量。而如果使用列名,则只会计算该列非空值的数量。 + +另外,`COUNT()` 函数的结果是一个整数值。即使结果是零,也不会返回 NULL,这点需要谨记。 + +**答案**: + +```sql +SELECT + count(*) total_pv, + ( SELECT count(*) FROM exam_record WHERE submit_time IS NOT NULL ) complete_pv, + ( SELECT COUNT( DISTINCT exam_id, score IS NOT NULL OR NULL ) FROM exam_record ) complete_exam_cnt +FROM + exam_record +``` + +这里着重说一下`COUNT( DISTINCT exam_id, score IS NOT NULL OR NULL )`这一句,判断 score 是否为 null ,如果是即为真,如果不是返回 null;注意这里如果不加 `or null` 在不是 null 的情况下只会返回 false 也就是返回 0; + +`COUNT`本身是不可以对多列求行数的,`distinct`的加入是的多列成为一个整体,可以求出现的行数了;`count distinct`在计算时只返回非 null 的行, 这个也要注意; + +另外通过本题 get 到了------>count 加条件常用句式`count( 列判断 or null)` + +### 得分不小于平均分的最低分 + +**描述**: 请从试卷作答记录表中找到 SQL 试卷得分不小于该类试卷平均得分的用户最低得分。 + +示例数据 exam_record 表(uid 用户 ID, exam_id 试卷 ID, start_time 开始作答时间, submit_time 交卷时间, score 得分): + +| id | uid | exam_id | start_time | submit_time | score | +| --- | ---- | ------- | ------------------- | ------------------- | ------ | +| 1 | 1001 | 9001 | 2020-01-02 09:01:01 | 2020-01-02 09:21:01 | 80 | +| 2 | 1002 | 9001 | 2021-09-05 19:01:01 | 2021-09-05 19:40:01 | 89 | +| 3 | 1002 | 9002 | 2021-09-02 12:01:01 | (NULL) | (NULL) | +| 4 | 1002 | 9003 | 2021-09-01 12:01:01 | (NULL) | (NULL) | +| 5 | 1002 | 9001 | 2021-02-02 19:01:01 | 2021-02-02 19:30:01 | 87 | +| 6 | 1002 | 9002 | 2021-05-05 18:01:01 | 2021-05-05 18:59:02 | 90 | +| 7 | 1003 | 9002 | 2021-02-06 12:01:01 | (NULL) | (NULL) | +| 8 | 1003 | 9003 | 2021-09-07 10:01:01 | 2021-09-07 10:31:01 | 86 | +| 9 | 1004 | 9003 | 2021-09-06 12:01:01 | (NULL) | (NULL) | + +`examination_info` 表(`exam_id` 试卷 ID, `tag` 试卷类别, `difficulty` 试卷难度, `duration` 考试时长, `release_time` 发布时间) + +| id | exam_id | tag | difficulty | duration | release_time | +| --- | ------- | ---- | ---------- | -------- | ------------------- | +| 1 | 9001 | SQL | hard | 60 | 2020-01-01 10:00:00 | +| 2 | 9002 | SQL | easy | 60 | 2020-02-01 10:00:00 | +| 3 | 9003 | 算法 | medium | 80 | 2020-08-02 10:00:00 | + +示例输出数据: + +| min_score_over_avg | +| ------------------ | +| 87 | + +**解释**:试卷 9001 和 9002 为 SQL 类别,作答这两份试卷的得分有[80,89,87,90],平均分为 86.5,不小于平均分的最小分数为 87 + +**思路**:这类题目第一眼看确实很复杂, 因为不知道从哪入手,但是当我们仔细读题审题后,要学会抓住题干中的关键信息。以本题为例:`请从试卷作答记录表中找到SQL试卷得分不小于该类试卷平均得分的用户最低得分。`你能一眼从中提取哪些有效信息来作为解题思路? + +第一条:找到==SQL==试卷得分 + +第二条:该类试卷==平均得分== + +第三条:该类试卷的==用户最低得分== + +然后中间的 “桥梁” 就是==不小于== + +将条件拆分后,先逐步完成 + +```sql +-- 找出tag为‘SQL’的得分 【80, 89,87,90】 +-- 再算出这一组的平均得分 +select ROUND(AVG(score), 1) from examination_info info INNER JOIN exam_record record + where info.exam_id = record.exam_id + and tag= 'SQL' +``` + +然后再找出该类试卷的最低得分,接着将结果集`【80, 89,87,90】` 去和平均分数作比较,方可得出最终答案。 + +**答案**: + +```sql +SELECT MIN(score) AS min_score_over_avg +FROM examination_info info +INNER JOIN exam_record record +WHERE info.exam_id = record.exam_id + AND tag= 'SQL' + AND score >= + (SELECT ROUND(AVG(score), 1) + FROM examination_info info + INNER JOIN exam_record record + WHERE info.exam_id = record.exam_id + AND tag= 'SQL' ) +``` + +其实这类题目给出的要求看似很 “绕”,但其实仔细梳理一遍,将大条件拆分成小条件,逐个拆分完以后,最后将所有条件拼凑起来。反正只要记住:**抓主干,理分支**,问题便迎刃而解。 + +## 分组查询 + +### 平均活跃天数和月活人数 + +**描述**:用户在牛客试卷作答区作答记录存储在表 `exam_record` 中,内容如下: + +`exam_record` 表(`uid` 用户 ID, `exam_id` 试卷 ID, `start_time` 开始作答时间, `submit_time` 交卷时间, `score` 得分) + +| id | uid | exam_id | start_time | submit_time | score | +| --- | ---- | ------- | ------------------- | ------------------- | ------ | +| 1 | 1001 | 9001 | 2021-07-02 09:01:01 | 2021-07-02 09:21:01 | 80 | +| 2 | 1002 | 9001 | 2021-09-05 19:01:01 | 2021-09-05 19:40:01 | 81 | +| 3 | 1002 | 9002 | 2021-09-02 12:01:01 | (NULL) | (NULL) | +| 4 | 1002 | 9003 | 2021-09-01 12:01:01 | (NULL) | (NULL) | +| 5 | 1002 | 9001 | 2021-07-02 19:01:01 | 2021-07-02 19:30:01 | 82 | +| 6 | 1002 | 9002 | 2021-07-05 18:01:01 | 2021-07-05 18:59:02 | 90 | +| 7 | 1003 | 9002 | 2021-07-06 12:01:01 | (NULL) | (NULL) | +| 8 | 1003 | 9003 | 2021-09-07 10:01:01 | 2021-09-07 10:31:01 | 86 | +| 9 | 1004 | 9003 | 2021-09-06 12:01:01 | (NULL) | (NULL) | +| 10 | 1002 | 9003 | 2021-09-01 12:01:01 | 2021-09-01 12:31:01 | 81 | +| 11 | 1005 | 9001 | 2021-09-01 12:01:01 | 2021-09-01 12:31:01 | 88 | +| 12 | 1006 | 9002 | 2021-09-02 12:11:01 | 2021-09-02 12:31:01 | 89 | +| 13 | 1007 | 9002 | 2020-09-02 12:11:01 | 2020-09-02 12:31:01 | 89 | + +请计算 2021 年每个月里试卷作答区用户平均月活跃天数 `avg_active_days` 和月度活跃人数 `mau`,上面数据的示例输出如下: + +| month | avg_active_days | mau | +| ------ | --------------- | --- | +| 202107 | 1.50 | 2 | +| 202109 | 1.25 | 4 | + +**解释**:2021 年 7 月有 2 人活跃,共活跃了 3 天(1001 活跃 1 天,1002 活跃 2 天),平均活跃天数 1.5;2021 年 9 月有 4 人活跃,共活跃了 5 天,平均活跃天数 1.25,结果保留 2 位小数。 + +注:此处活跃指有==交卷==行为。 + +**思路**:读完题先注意高亮部分;一般求天数和月活跃人数马上就要想到相关的日期函数;这一题我们同样来进行拆分,把问题细化再解决;首先求活跃人数,肯定要用到`COUNT()`,那这里首先就有一个坑,不知道大家注意了没有?用户 1002 在 9 月份做了两种不同的试卷,所以这里要注意去重,不然在统计的时候,活跃人数是错的;第二个就是要知道日期的格式化,如上表,题目要求以`202107`这种日期格式展现,要用到`DATE_FORMAT`来进行格式化。 + +基本用法: + +`DATE_FORMAT(date_value, format)` + +- `date_value` 参数是待格式化的日期或时间值。 +- `format` 参数是指定的日期或时间格式(这个和 Java 里面的日期格式一样)。 + +**答案**: + +```sql +SELECT DATE_FORMAT(submit_time, '%Y%m') MONTH, + round(count(DISTINCT UID, DATE_FORMAT(submit_time, '%Y%m%d')) / count(DISTINCT UID), 2) avg_active_days, + COUNT(DISTINCT UID) mau +FROM exam_record +WHERE YEAR (submit_time) = 2021 +GROUP BY MONTH +``` + +这里多说一句, 使用`COUNT(DISTINCT uid, DATE_FORMAT(submit_time, '%Y%m%d'))` 可以统计在 `uid` 列和 `submit_time` 列按照年份、月份和日期进行格式化后的组合值的数量。 + +### 月总刷题数和日均刷题数 + +**描述**:现有一张题目练习记录表 `practice_record`,示例内容如下: + +| id | uid | question_id | submit_time | score | +| --- | ---- | ----------- | ------------------- | ----- | +| 1 | 1001 | 8001 | 2021-08-02 11:41:01 | 60 | +| 2 | 1002 | 8001 | 2021-09-02 19:30:01 | 50 | +| 3 | 1002 | 8001 | 2021-09-02 19:20:01 | 70 | +| 4 | 1002 | 8002 | 2021-09-02 19:38:01 | 70 | +| 5 | 1003 | 8002 | 2021-08-01 19:38:01 | 80 | + +请从中统计出 2021 年每个月里用户的月总刷题数 `month_q_cnt` 和日均刷题数 `avg_day_q_cnt`(按月份升序排序)以及该年的总体情况,示例数据输出如下: + +| submit_month | month_q_cnt | avg_day_q_cnt | +| ------------ | ----------- | ------------- | +| 202108 | 2 | 0.065 | +| 202109 | 3 | 0.100 | +| 2021 汇总 | 5 | 0.161 | + +**解释**:2021 年 8 月共有 2 次刷题记录,日均刷题数为 2/31=0.065(保留 3 位小数);2021 年 9 月共有 3 次刷题记录,日均刷题数为 3/30=0.100;2021 年共有 5 次刷题记录(年度汇总平均无实际意义,这里我们按照 31 天来算 5/31=0.161) + +> 牛客已经采用最新的 Mysql 版本,如果您运行结果出现错误:ONLY_FULL_GROUP_BY,意思是:对于 GROUP BY 聚合操作,如果在 SELECT 中的列,没有在 GROUP BY 中出现,那么这个 SQL 是不合法的,因为列不在 GROUP BY 从句中,也就是说查出来的列必须在 group by 后面出现否则就会报错,或者这个字段出现在聚合函数里面。 + +**思路:** + +看到实例数据就要马上联想到相关的函数,比如`submit_month`就要用到`DATE_FORMAT`来格式化日期。然后查出每月的刷题数量。 + +每月的刷题数量 + +```sql +SELECT MONTH ( submit_time ), COUNT( question_id ) +FROM + practice_record +GROUP BY + MONTH (submit_time) +``` + +接着第三列这里要用到`DAY(LAST_DAY(date_value))`函数来查找给定日期的月份中的天数。 + +示例代码如下: + +```sql +SELECT DAY(LAST_DAY('2023-07-08')) AS days_in_month; +-- 输出:31 + +SELECT DAY(LAST_DAY('2023-02-01')) AS days_in_month; +-- 输出:28 (闰年中的二月份) + +SELECT DAY(LAST_DAY(NOW())) AS days_in_current_month; +-- 输出:31 (当前月份的天数) +``` + +使用 `LAST_DAY()` 函数获取给定日期的当月最后一天,然后使用 `DAY()` 函数提取该日期的天数。这样就能获得指定月份的天数。 + +需要注意的是,`LAST_DAY()` 函数返回的是日期值,而 `DAY()` 函数用于提取日期值中的天数部分。 + +有了上述的分析之后,即可马上写出答案,这题复杂就复杂在处理日期上,其中的逻辑并不难。 + +**答案**: + +```sql +SELECT DATE_FORMAT(submit_time, '%Y%m') submit_month, + count(question_id) month_q_cnt, + ROUND(COUNT(question_id) / DAY (LAST_DAY(submit_time)), 3) avg_day_q_cnt +FROM practice_record +WHERE DATE_FORMAT(submit_time, '%Y') = '2021' +GROUP BY submit_month +UNION ALL +SELECT '2021汇总' AS submit_month, + count(question_id) month_q_cnt, + ROUND(COUNT(question_id) / 31, 3) avg_day_q_cnt +FROM practice_record +WHERE DATE_FORMAT(submit_time, '%Y') = '2021' +ORDER BY submit_month +``` + +在实例数据输出中因为最后一行需要得出汇总数据,所以这里要 `UNION ALL`加到结果集中;别忘了最后要排序! + +### 未完成试卷数大于 1 的有效用户(较难) + +**描述**:现有试卷作答记录表 `exam_record`(`uid` 用户 ID, `exam_id` 试卷 ID, `start_time` 开始作答时间, `submit_time` 交卷时间, `score` 得分),示例数据如下: + +| id | uid | exam_id | start_time | submit_time | score | +| --- | ---- | ------- | ------------------- | ------------------- | ------ | +| 1 | 1001 | 9001 | 2021-07-02 09:01:01 | 2021-07-02 09:21:01 | 80 | +| 2 | 1002 | 9001 | 2021-09-05 19:01:01 | 2021-09-05 19:40:01 | 81 | +| 3 | 1002 | 9002 | 2021-09-02 12:01:01 | (NULL) | (NULL) | +| 4 | 1002 | 9003 | 2021-09-01 12:01:01 | (NULL) | (NULL) | +| 5 | 1002 | 9001 | 2021-07-02 19:01:01 | 2021-07-02 19:30:01 | 82 | +| 6 | 1002 | 9002 | 2021-07-05 18:01:01 | 2021-07-05 18:59:02 | 90 | +| 7 | 1003 | 9002 | 2021-07-06 12:01:01 | (NULL) | (NULL) | +| 8 | 1003 | 9003 | 2021-09-07 10:01:01 | 2021-09-07 10:31:01 | 86 | +| 9 | 1004 | 9003 | 2021-09-06 12:01:01 | (NULL) | (NULL) | +| 10 | 1002 | 9003 | 2021-09-01 12:01:01 | 2021-09-01 12:31:01 | 81 | +| 11 | 1005 | 9001 | 2021-09-01 12:01:01 | 2021-09-01 12:31:01 | 88 | +| 12 | 1006 | 9002 | 2021-09-02 12:11:01 | 2021-09-02 12:31:01 | 89 | +| 13 | 1007 | 9002 | 2020-09-02 12:11:01 | 2020-09-02 12:31:01 | 89 | + +还有一张试卷信息表 `examination_info`(`exam_id` 试卷 ID, `tag` 试卷类别, `difficulty` 试卷难度, `duration` 考试时长, `release_time` 发布时间),示例数据如下: + +| id | exam_id | tag | difficulty | duration | release_time | +| --- | ------- | ---- | ---------- | -------- | ------------------- | +| 1 | 9001 | SQL | hard | 60 | 2020-01-01 10:00:00 | +| 2 | 9002 | SQL | easy | 60 | 2020-02-01 10:00:00 | +| 3 | 9003 | 算法 | medium | 80 | 2020-08-02 10:00:00 | + +请统计 2021 年每个未完成试卷作答数大于 1 的有效用户的数据(有效用户指完成试卷作答数至少为 1 且未完成数小于 5),输出用户 ID、未完成试卷作答数、完成试卷作答数、作答过的试卷 tag 集合,按未完成试卷数量由多到少排序。示例数据的输出结果如下: + +| uid | incomplete_cnt | complete_cnt | detail | +| ---- | -------------- | ------------ | --------------------------------------------------------------------------- | +| 1002 | 2 | 4 | 2021-09-01:算法;2021-07-02:SQL;2021-09-02:SQL;2021-09-05:SQL;2021-07-05:SQL | + +**解释**:2021 年的作答记录中,除了 1004,其他用户均满足有效用户定义,但只有 1002 未完成试卷数大于 1,因此只输出 1002,detail 中是 1002 作答过的试卷{日期:tag}集合,日期和 tag 间用 **:** 连接,多元素间用 **;** 连接。 + +**思路:** + +仔细读题后,分析出:首先要联表,因为后面要输出`tag`; + +筛选出 2021 年的数据 + +```sql +SELECT * +FROM exam_record er +LEFT JOIN examination_info ei ON er.exam_id = ei.exam_id +WHERE YEAR (er.start_time)= 2021 +``` + +根据 uid 进行分组,然后对每个用户进行条件进行判断,题目中要求`完成试卷数至少为1,未完成试卷数要大于1,小于5` + +那么等会儿写 sql 的时候条件应该是:`未完成 > 1 and 已完成 >=1 and 未完成 < 5` + +因为最后要用到字符串的拼接,而且还要组合拼接,这个可以用`GROUP_CONCAT`函数,下面简单介绍一下该函数的用法: + +基本格式: + +```sql +GROUP_CONCAT([DISTINCT] expr [ORDER BY {unsigned_integer | col_name | expr} [ASC | DESC] [, ...]] [SEPARATOR sep]) +``` + +- `expr`:要连接的列或表达式。 +- `DISTINCT`:可选参数,用于去重。当指定了 `DISTINCT`,相同的值只会出现一次。 +- `ORDER BY`:可选参数,用于排序连接后的值。可以选择升序 (`ASC`) 或降序 (`DESC`) 排序。 +- `SEPARATOR sep`:可选参数,用于设置连接后的值的分隔符。(本题要用这个参数设置 ; 号 ) + +`GROUP_CONCAT()` 函数常用于 `GROUP BY` 子句中,将一组行的值连接为一个字符串,并在结果集中以聚合的形式返回。 + +**答案**: + +```sql +SELECT a.uid, + SUM(CASE + WHEN a.submit_time IS NULL THEN 1 + END) AS incomplete_cnt, + SUM(CASE + WHEN a.submit_time IS NOT NULL THEN 1 + END) AS complete_cnt, + GROUP_CONCAT(DISTINCT CONCAT(DATE_FORMAT(a.start_time, '%Y-%m-%d'), ':', b.tag) + ORDER BY start_time SEPARATOR ";") AS detail +FROM exam_record a +LEFT JOIN examination_info b ON a.exam_id = b.exam_id +WHERE YEAR (a.start_time)= 2021 +GROUP BY a.uid +HAVING incomplete_cnt > 1 +AND complete_cnt >= 1 +AND incomplete_cnt < 5 +ORDER BY incomplete_cnt DESC +``` + +- `SUM(CASE WHEN a.submit_time IS NULL THEN 1 END)` 统计了每个用户未完成的记录数量。 +- `SUM(CASE WHEN a.submit_time IS NOT NULL THEN 1 END)` 统计了每个用户已完成的记录数量。 +- `GROUP_CONCAT(DISTINCT CONCAT(DATE_FORMAT(a.start_time, '%Y-%m-%d'), ':', b.tag) ORDER BY a.start_time SEPARATOR ';')` 将每个用户的考试日期和标签以逗号分隔的形式连接成一个字符串,并按考试开始时间进行排序。 + +## 嵌套子查询 + +### 月均完成试卷数不小于 3 的用户爱作答的类别(较难) + +**描述**:现有试卷作答记录表 `exam_record`(`uid`:用户 ID, `exam_id`:试卷 ID, `start_time`:开始作答时间, `submit_time`:交卷时间,没提交的话为 NULL, `score`:得分),示例数据如下: + +| id | uid | exam_id | start_time | submit_time | score | +| --- | ---- | ------- | ------------------- | ------------------- | ------ | +| 1 | 1001 | 9001 | 2021-07-02 09:01:01 | (NULL) | (NULL) | +| 2 | 1002 | 9003 | 2021-09-01 12:01:01 | 2021-09-01 12:21:01 | 60 | +| 3 | 1002 | 9002 | 2021-09-02 12:01:01 | 2021-09-02 12:31:01 | 70 | +| 4 | 1002 | 9001 | 2021-09-05 19:01:01 | 2021-09-05 19:40:01 | 81 | +| 5 | 1002 | 9002 | 2021-07-06 12:01:01 | (NULL) | (NULL) | +| 6 | 1003 | 9003 | 2021-09-07 10:01:01 | 2021-09-07 10:31:01 | 86 | +| 7 | 1003 | 9003 | 2021-09-08 12:01:01 | 2021-09-08 12:11:01 | 40 | +| 8 | 1003 | 9001 | 2021-09-08 13:01:01 | (NULL) | (NULL) | +| 9 | 1003 | 9002 | 2021-09-08 14:01:01 | (NULL) | (NULL) | +| 10 | 1003 | 9003 | 2021-09-08 15:01:01 | (NULL) | (NULL) | +| 11 | 1005 | 9001 | 2021-09-01 12:01:01 | 2021-09-01 12:31:01 | 88 | +| 12 | 1005 | 9002 | 2021-09-01 12:01:01 | 2021-09-01 12:31:01 | 88 | +| 13 | 1005 | 9002 | 2021-09-02 12:11:01 | 2021-09-02 12:31:01 | 89 | + +试卷信息表 `examination_info`(`exam_id`:试卷 ID, `tag`:试卷类别, `difficulty`:试卷难度, `duration`:考试时长, `release_time`:发布时间),示例数据如下: + +| id | exam_id | tag | difficulty | duration | release_time | +| --- | ------- | ---- | ---------- | -------- | ------------------- | +| 1 | 9001 | SQL | hard | 60 | 2020-01-01 10:00:00 | +| 2 | 9002 | C++ | easy | 60 | 2020-02-01 10:00:00 | +| 3 | 9003 | 算法 | medium | 80 | 2020-08-02 10:00:00 | + +请从表中统计出 “当月均完成试卷数”不小于 3 的用户们爱作答的类别及作答次数,按次数降序输出,示例输出如下: + +| tag | tag_cnt | +| ---- | ------- | +| C++ | 4 | +| SQL | 2 | +| 算法 | 1 | + +**解释**:用户 1002 和 1005 在 2021 年 09 月的完成试卷数目均为 3,其他用户均小于 3;然后用户 1002 和 1005 作答过的试卷 tag 分布结果按作答次数降序排序依次为 C++、SQL、算法。 + +**思路**:这题考察联合子查询,重点在于`月均回答>=3`, 但是个人认为这里没有表述清楚,应该直接说查 9 月的就容易理解多了;这里不是每个月都要>=3 或者是所有答题次数/答题月份。不要理解错误了。 + +先查询出哪些用户月均答题大于三次 + +```sql +SELECT UID +FROM exam_record record +GROUP BY UID, + MONTH (start_time) +HAVING count(submit_time) >= 3 +``` + +有了这一步之后再进行深入,只要能理解上一步(我的意思是不被题目中的月均所困扰),然后再套一个子查询,查哪些用户包含其中,然后查出题目中所需的列即可。记得排序!! + +```sql +SELECT tag, + count(start_time) AS tag_cnt +FROM exam_record record +INNER JOIN examination_info info ON record.exam_id = info.exam_id +WHERE UID IN + (SELECT UID + FROM exam_record record + GROUP BY UID, + MONTH (start_time) + HAVING count(submit_time) >= 3) +GROUP BY tag +ORDER BY tag_cnt DESC +``` + +### 试卷发布当天作答人数和平均分 + +**描述**:现有用户信息表 `user_info`(`uid` 用户 ID,`nick_name` 昵称, `achievement` 成就值, `level` 等级, `job` 职业方向, `register_time` 注册时间),示例数据如下: + +| id | uid | nick_name | achievement | level | job | register_time | +| --- | ---- | --------- | ----------- | ----- | ---- | ------------------- | +| 1 | 1001 | 牛客 1 号 | 3100 | 7 | 算法 | 2020-01-01 10:00:00 | +| 2 | 1002 | 牛客 2 号 | 2100 | 6 | 算法 | 2020-01-01 10:00:00 | +| 3 | 1003 | 牛客 3 号 | 1500 | 5 | 算法 | 2020-01-01 10:00:00 | +| 4 | 1004 | 牛客 4 号 | 1100 | 4 | 算法 | 2020-01-01 10:00:00 | +| 5 | 1005 | 牛客 5 号 | 1600 | 6 | C++ | 2020-01-01 10:00:00 | +| 6 | 1006 | 牛客 6 号 | 3000 | 6 | C++ | 2020-01-01 10:00:00 | + +**释义**:用户 1001 昵称为牛客 1 号,成就值为 3100,用户等级是 7 级,职业方向为算法,注册时间 2020-01-01 10:00:00 + +试卷信息表 `examination_info`(`exam_id` 试卷 ID, `tag` 试卷类别, `difficulty` 试卷难度, `duration` 考试时长, `release_time` 发布时间) 示例数据如下: + +| id | exam_id | tag | difficulty | duration | release_time | +| --- | ------- | ---- | ---------- | -------- | ------------------- | +| 1 | 9001 | SQL | hard | 60 | 2021-09-01 06:00:00 | +| 2 | 9002 | C++ | easy | 60 | 2020-02-01 10:00:00 | +| 3 | 9003 | 算法 | medium | 80 | 2020-08-02 10:00:00 | + +试卷作答记录表 `exam_record`(`uid` 用户 ID, `exam_id` 试卷 ID, `start_time` 开始作答时间, `submit_time` 交卷时间, `score` 得分) 示例数据如下: + +| id | uid | exam_id | start_time | submit_time | score | +| --- | ---- | ------- | ------------------- | ------------------- | ------ | +| 1 | 1001 | 9001 | 2021-07-02 09:01:01 | 2021-09-01 09:41:01 | 70 | +| 2 | 1002 | 9003 | 2021-09-01 12:01:01 | 2021-09-01 12:21:01 | 60 | +| 3 | 1002 | 9002 | 2021-09-02 12:01:01 | 2021-09-02 12:31:01 | 70 | +| 4 | 1002 | 9001 | 2021-09-01 19:01:01 | 2021-09-01 19:40:01 | 80 | +| 5 | 1002 | 9003 | 2021-08-01 12:01:01 | 2021-08-01 12:21:01 | 60 | +| 6 | 1002 | 9002 | 2021-08-02 12:01:01 | 2021-08-02 12:31:01 | 70 | +| 7 | 1002 | 9001 | 2021-09-01 19:01:01 | 2021-09-01 19:40:01 | 85 | +| 8 | 1002 | 9002 | 2021-07-06 12:01:01 | (NULL) | (NULL) | +| 9 | 1003 | 9002 | 2021-09-07 10:01:01 | 2021-09-07 10:31:01 | 86 | +| 10 | 1003 | 9003 | 2021-09-08 12:01:01 | 2021-09-08 12:11:01 | 40 | +| 11 | 1003 | 9003 | 2021-09-01 13:01:01 | 2021-09-01 13:41:01 | 70 | +| 12 | 1003 | 9001 | 2021-09-08 14:01:01 | (NULL) | (NULL) | +| 13 | 1003 | 9002 | 2021-09-08 15:01:01 | (NULL) | (NULL) | +| 14 | 1005 | 9001 | 2021-09-01 12:01:01 | 2021-09-01 12:31:01 | 90 | +| 15 | 1005 | 9002 | 2021-09-01 12:01:01 | 2021-09-01 12:31:01 | 88 | +| 16 | 1005 | 9002 | 2021-09-02 12:11:01 | 2021-09-02 12:31:01 | 89 | + +请计算每张 SQL 类别试卷发布后,当天 5 级以上的用户作答的人数 `uv` 和平均分 `avg_score`,按人数降序,相同人数的按平均分升序,示例数据结果输出如下: + +| exam_id | uv | avg_score | +| ------- | --- | --------- | +| 9001 | 3 | 81.3 | + +解释:只有一张 SQL 类别的试卷,试卷 ID 为 9001,发布当天(2021-09-01)有 1001、1002、1003、1005 作答过,但是 1003 是 5 级用户,其他 3 位为 5 级以上,他们三的得分有[70,80,85,90],平均分为 81.3(保留 1 位小数)。 + +**思路**:这题看似很复杂,但是先逐步将“外边”条件拆分,然后合拢到一起,答案就出来,多表查询反正记住:由外向里,抽丝剥茧。 + +先把三种表连起来,同时给定一些条件,比如题目中要求`等级> 5`的用户,那么可以先查出来 + +```sql +SELECT DISTINCT u_info.uid +FROM examination_info e_info +INNER JOIN exam_record record +INNER JOIN user_info u_info +WHERE e_info.exam_id = record.exam_id + AND u_info.uid = record.uid + AND u_info.LEVEL > 5 +``` + +接着注意题目中要求:`每张sql类别试卷发布后,当天作答用户`,注意其中的==当天==,那我们马上就要想到要用到时间的比较。 + +对试卷发布日期和开始考试日期进行比较:`DATE(e_info.release_time) = DATE(record.start_time)`;不用担心`submit_time` 为 null 的问题,后续在 where 中会给过滤掉。 + +**答案**: + +```sql +SELECT record.exam_id AS exam_id, + COUNT(DISTINCT u_info.uid) AS uv, + ROUND(SUM(record.score) / COUNT(u_info.uid), 1) AS avg_score +FROM examination_info e_info +INNER JOIN exam_record record +INNER JOIN user_info u_info +WHERE e_info.exam_id = record.exam_id + AND u_info.uid = record.uid + AND DATE (e_info.release_time) = DATE (record.start_time) + AND submit_time IS NOT NULL + AND tag = 'SQL' + AND u_info.LEVEL > 5 +GROUP BY record.exam_id +ORDER BY uv DESC, + avg_score ASC +``` + +注意最后的分组排序!先按人数排,若一致,按平均分排。 + +### 作答试卷得分大于过 80 的人的用户等级分布 + +**描述**: + +现有用户信息表 `user_info`(`uid` 用户 ID,`nick_name` 昵称, `achievement` 成就值, `level` 等级, `job` 职业方向, `register_time` 注册时间): + +| id | uid | nick_name | achievement | level | job | register_time | +| --- | ---- | --------- | ----------- | ----- | ---- | ------------------- | +| 1 | 1001 | 牛客 1 号 | 3100 | 7 | 算法 | 2020-01-01 10:00:00 | +| 2 | 1002 | 牛客 2 号 | 2100 | 6 | 算法 | 2020-01-01 10:00:00 | +| 3 | 1003 | 牛客 3 号 | 1500 | 5 | 算法 | 2020-01-01 10:00:00 | +| 4 | 1004 | 牛客 4 号 | 1100 | 4 | 算法 | 2020-01-01 10:00:00 | +| 5 | 1005 | 牛客 5 号 | 1600 | 6 | C++ | 2020-01-01 10:00:00 | +| 6 | 1006 | 牛客 6 号 | 3000 | 6 | C++ | 2020-01-01 10:00:00 | + +试卷信息表 `examination_info`(`exam_id` 试卷 ID, `tag` 试卷类别, `difficulty` 试卷难度, `duration` 考试时长, `release_time` 发布时间): + +| id | exam_id | tag | difficulty | duration | release_time | +| --- | ------- | ---- | ---------- | -------- | ------------------- | +| 1 | 9001 | SQL | hard | 60 | 2021-09-01 06:00:00 | +| 2 | 9002 | C++ | easy | 60 | 2021-09-01 06:00:00 | +| 3 | 9003 | 算法 | medium | 80 | 2021-09-01 10:00:00 | + +试卷作答信息表 `exam_record`(`uid` 用户 ID, `exam_id` 试卷 ID, `start_time` 开始作答时间, `submit_time` 交卷时间, `score` 得分): + +| id | uid | exam_id | start_time | submit_time | score | +| --- | ---- | ------- | ------------------- | ------------------- | ------ | +| 1 | 1001 | 9001 | 2021-09-01 09:01:01 | 2021-09-01 09:41:01 | 79 | +| 2 | 1002 | 9003 | 2021-09-01 12:01:01 | 2021-09-01 12:21:01 | 60 | +| 3 | 1002 | 9002 | 2021-09-01 12:01:01 | 2021-09-01 12:31:01 | 70 | +| 4 | 1002 | 9001 | 2021-09-01 19:01:01 | 2021-09-01 19:40:01 | 80 | +| 5 | 1002 | 9003 | 2021-08-01 12:01:01 | 2021-08-01 12:21:01 | 60 | +| 6 | 1002 | 9002 | 2021-09-01 12:01:01 | 2021-09-01 12:31:01 | 70 | +| 7 | 1002 | 9001 | 2021-09-01 19:01:01 | 2021-09-01 19:40:01 | 85 | +| 8 | 1002 | 9002 | 2021-09-01 12:01:01 | (NULL) | (NULL) | +| 9 | 1003 | 9002 | 2021-09-07 10:01:01 | 2021-09-07 10:31:01 | 86 | +| 10 | 1003 | 9003 | 2021-09-08 12:01:01 | 2021-09-08 12:11:01 | 40 | +| 11 | 1003 | 9003 | 2021-09-01 13:01:01 | 2021-09-01 13:41:01 | 81 | +| 12 | 1003 | 9001 | 2021-09-01 14:01:01 | (NULL) | (NULL) | +| 13 | 1003 | 9002 | 2021-09-08 15:01:01 | (NULL) | (NULL) | +| 14 | 1005 | 9001 | 2021-09-01 12:01:01 | 2021-09-01 12:31:01 | 90 | +| 15 | 1005 | 9002 | 2021-09-01 12:01:01 | 2021-09-01 12:31:01 | 88 | +| 16 | 1005 | 9002 | 2021-09-02 12:11:01 | 2021-09-02 12:31:01 | 89 | + +统计作答 SQL 类别的试卷得分大于过 80 的人的用户等级分布,按数量降序排序(保证数量都不同)。示例数据结果输出如下: + +| level | level_cnt | +| ----- | --------- | +| 6 | 2 | +| 5 | 1 | + +解释:9001 为 SQL 类试卷,作答该试卷大于 80 分的人有 1002、1003、1005 共 3 人,6 级两人,5 级一人。 + +**思路:**这题和上一题都是一样的数据,只是查询条件改变了而已,上一题理解了,这题分分钟做出来。 + +**答案**: + +```sql +SELECT u_info.LEVEL AS LEVEL, + count(u_info.uid) AS level_cnt +FROM examination_info e_info +INNER JOIN exam_record record +INNER JOIN user_info u_info +WHERE e_info.exam_id = record.exam_id + AND u_info.uid = record.uid + AND record.score > 80 + AND submit_time IS NOT NULL + AND tag = 'SQL' +GROUP BY LEVEL +ORDER BY level_cnt DESC +``` + +## 合并查询 + +### 每个题目和每份试卷被作答的人数和次数 + +**描述**: + +现有试卷作答记录表 exam_record(uid 用户 ID, exam_id 试卷 ID, start_time 开始作答时间, submit_time 交卷时间, score 得分): + +| id | uid | exam_id | start_time | submit_time | score | +| --- | ---- | ------- | ------------------- | ------------------- | ------ | +| 1 | 1001 | 9001 | 2021-09-01 09:01:01 | 2021-09-01 09:41:01 | 81 | +| 2 | 1002 | 9002 | 2021-09-01 12:01:01 | 2021-09-01 12:31:01 | 70 | +| 3 | 1002 | 9001 | 2021-09-01 19:01:01 | 2021-09-01 19:40:01 | 80 | +| 4 | 1002 | 9002 | 2021-09-01 12:01:01 | 2021-09-01 12:31:01 | 70 | +| 5 | 1004 | 9001 | 2021-09-01 19:01:01 | 2021-09-01 19:40:01 | 85 | +| 6 | 1002 | 9002 | 2021-09-01 12:01:01 | (NULL) | (NULL) | + +题目练习表 practice_record(uid 用户 ID, question_id 题目 ID, submit_time 提交时间, score 得分): + +| id | uid | question_id | submit_time | score | +| --- | ---- | ----------- | ------------------- | ----- | +| 1 | 1001 | 8001 | 2021-08-02 11:41:01 | 60 | +| 2 | 1002 | 8001 | 2021-09-02 19:30:01 | 50 | +| 3 | 1002 | 8001 | 2021-09-02 19:20:01 | 70 | +| 4 | 1002 | 8002 | 2021-09-02 19:38:01 | 70 | +| 5 | 1003 | 8001 | 2021-08-02 19:38:01 | 70 | +| 6 | 1003 | 8001 | 2021-08-02 19:48:01 | 90 | +| 7 | 1003 | 8002 | 2021-08-01 19:38:01 | 80 | + +请统计每个题目和每份试卷被作答的人数和次数,分别按照"试卷"和"题目"的 uv & pv 降序显示,示例数据结果输出如下: + +| tid | uv | pv | +| ---- | --- | --- | +| 9001 | 3 | 3 | +| 9002 | 1 | 3 | +| 8001 | 3 | 5 | +| 8002 | 2 | 2 | + +**解释**:“试卷”有 3 人共练习 3 次试卷 9001,1 人作答 3 次 9002;“刷题”有 3 人刷 5 次 8001,有 2 人刷 2 次 8002 + +**思路**:这题的难点和易错点在于`UNION`和`ORDER BY` 同时使用的问题 + +有以下几种情况:使用`union`和多个`order by`不加括号,报错! + +`order by`在`union`连接的子句中不起作用; + +比如不加括号: + +```sql +SELECT exam_id AS tid, + COUNT(DISTINCT UID) AS uv, + COUNT(UID) AS pv +FROM exam_record +GROUP BY exam_id +ORDER BY uv DESC, + pv DESC +UNION +SELECT question_id AS tid, + COUNT(DISTINCT UID) AS uv, + COUNT(UID) AS pv +FROM practice_record +GROUP BY question_id +ORDER BY uv DESC, + pv DESC +``` + +直接报语法错误,如果没有括号,只能有一个`order by` + +还有一种`order by`不起作用的情况,但是能在子句的子句中起作用,这里的解决方案就是在外面再套一层查询。 + +**答案**: + +```sql +SELECT * +FROM + (SELECT exam_id AS tid, + COUNT(DISTINCT exam_record.uid) uv, + COUNT(*) pv + FROM exam_record + GROUP BY exam_id + ORDER BY uv DESC, pv DESC) t1 +UNION +SELECT * +FROM + (SELECT question_id AS tid, + COUNT(DISTINCT practice_record.uid) uv, + COUNT(*) pv + FROM practice_record + GROUP BY question_id + ORDER BY uv DESC, pv DESC) t2; +``` + +### 分别满足两个活动的人 + +**描述**: 为了促进更多用户在牛客平台学习和刷题进步,我们会经常给一些既活跃又表现不错的用户发放福利。假使以前我们有两拨运营活动,分别给每次试卷得分都能到 85 分的人(activity1)、至少有一次用了一半时间就完成高难度试卷且分数大于 80 的人(activity2)发了福利券。 + +现在,需要你一次性将这两个活动满足的人筛选出来,交给运营同学。请写出一个 SQL 实现:输出 2021 年里,所有每次试卷得分都能到 85 分的人以及至少有一次用了一半时间就完成高难度试卷且分数大于 80 的人的 id 和活动号,按用户 ID 排序输出。 + +现有试卷信息表 `examination_info`(`exam_id` 试卷 ID, `tag` 试卷类别, `difficulty` 试卷难度, `duration` 考试时长, `release_time` 发布时间): + +| id | exam_id | tag | difficulty | duration | release_time | +| --- | ------- | ---- | ---------- | -------- | ------------------- | +| 1 | 9001 | SQL | hard | 60 | 2021-09-01 06:00:00 | +| 2 | 9002 | C++ | easy | 60 | 2021-09-01 06:00:00 | +| 3 | 9003 | 算法 | medium | 80 | 2021-09-01 10:00:00 | + +试卷作答记录表 `exam_record`(`uid` 用户 ID, `exam_id` 试卷 ID, `start_time` 开始作答时间, `submit_time` 交卷时间, `score` 得分): + +| id | uid | exam_id | start_time | submit_time | score | +| --- | ---- | ------- | ------------------- | ------------------- | ------ | +| 1 | 1001 | 9001 | 2021-09-01 09:01:01 | 2021-09-01 09:31:00 | 81 | +| 2 | 1002 | 9002 | 2021-09-01 12:01:01 | 2021-09-01 12:31:01 | 70 | +| 3 | 1003 | 9001 | 2021-09-01 19:01:01 | 2021-09-01 19:40:01 | **86** | +| 4 | 1003 | 9002 | 2021-09-01 12:01:01 | 2021-09-01 12:31:01 | 89 | +| 5 | 1004 | 9001 | 2021-09-01 19:01:01 | 2021-09-01 19:30:01 | 85 | + +示例数据输出结果: + +| uid | activity | +| ---- | --------- | +| 1001 | activity2 | +| 1003 | activity1 | +| 1004 | activity1 | +| 1004 | activity2 | + +**解释**:用户 1001 最小分数 81 不满足活动 1,但 29 分 59 秒完成了 60 分钟长的试卷得分 81,满足活动 2;1003 最小分数 86 满足活动 1,完成时长都大于试卷时长的一半,不满足活动 2;用户 1004 刚好用了一半时间(30 分钟整)完成了试卷得分 85,满足活动 1 和活动 2。 + +**思路**: 这一题需要涉及到时间的减法,需要用到 `TIMESTAMPDIFF()` 函数计算两个时间戳之间的分钟差值。 + +下面我们来看一下基本用法 + +示例: + +```sql +TIMESTAMPDIFF(MINUTE, start_time, end_time) +``` + +`TIMESTAMPDIFF()` 函数的第一个参数是时间单位,这里我们选择 `MINUTE` 表示返回分钟差值。第二个参数是较早的时间戳,第三个参数是较晚的时间戳。函数会返回它们之间的分钟差值 + +了解了这个函数的用法之后,我们再回过头来看`activity1`的要求,求分数大于 85 即可,那我们还是先把这个写出来,后续思路就会清晰很多 + +```sql +SELECT DISTINCT UID +FROM exam_record +WHERE score >= 85 + AND YEAR (start_time) = '2021' +``` + +根据条件 2,接着写出`在一半时间内完成高难度试卷且分数大于80的人` + +```sql +SELECT UID +FROM examination_info info +INNER JOIN exam_record record +WHERE info.exam_id = record.exam_id + AND (TIMESTAMPDIFF(MINUTE, start_time, submit_time)) < (info.duration / 2) + AND difficulty = 'hard' + AND score >= 80 +``` + +然后再把两者`UNION` 起来即可。(这里特别要注意括号问题和`order by`位置,具体用法在上一篇中已提及) + +**答案**: + +```sql +SELECT DISTINCT UID UID, + 'activity1' activity +FROM exam_record +WHERE UID not in + (SELECT UID + FROM exam_record + WHERE score<85 + AND YEAR(submit_time) = 2021 ) +UNION +SELECT DISTINCT UID UID, + 'activity2' activity +FROM exam_record e_r +LEFT JOIN examination_info e_i ON e_r.exam_id = e_i.exam_id +WHERE YEAR(submit_time) = 2021 + AND difficulty = 'hard' + AND TIMESTAMPDIFF(SECOND, start_time, submit_time) <= duration *30 + AND score>80 +ORDER BY UID +``` + +## 连接查询 + +### 满足条件的用户的试卷完成数和题目练习数(困难) + +**描述**: + +现有用户信息表 user_info(uid 用户 ID,nick_name 昵称, achievement 成就值, level 等级, job 职业方向, register_time 注册时间): + +| id | uid | nick_name | achievement | level | job | register_time | +| --- | ---- | --------- | ----------- | ----- | ---- | ------------------- | +| 1 | 1001 | 牛客 1 号 | 3100 | 7 | 算法 | 2020-01-01 10:00:00 | +| 2 | 1002 | 牛客 2 号 | 2300 | 7 | 算法 | 2020-01-01 10:00:00 | +| 3 | 1003 | 牛客 3 号 | 2500 | 7 | 算法 | 2020-01-01 10:00:00 | +| 4 | 1004 | 牛客 4 号 | 1200 | 5 | 算法 | 2020-01-01 10:00:00 | +| 5 | 1005 | 牛客 5 号 | 1600 | 6 | C++ | 2020-01-01 10:00:00 | +| 6 | 1006 | 牛客 6 号 | 2000 | 6 | C++ | 2020-01-01 10:00:00 | + +试卷信息表 examination_info(exam_id 试卷 ID, tag 试卷类别, difficulty 试卷难度, duration 考试时长, release_time 发布时间): + +| id | exam_id | tag | difficulty | duration | release_time | +| --- | ------- | ---- | ---------- | -------- | ------------------- | +| 1 | 9001 | SQL | hard | 60 | 2021-09-01 06:00:00 | +| 2 | 9002 | C++ | hard | 60 | 2021-09-01 06:00:00 | +| 3 | 9003 | 算法 | medium | 80 | 2021-09-01 10:00:00 | + +试卷作答记录表 exam_record(uid 用户 ID, exam_id 试卷 ID, start_time 开始作答时间, submit_time 交卷时间, score 得分): + +| id | uid | exam_id | start_time | submit_time | score | +| --- | ---- | ------- | ------------------- | ------------------- | ----- | +| 1 | 1001 | 9001 | 2021-09-01 09:01:01 | 2021-09-01 09:31:00 | 81 | +| 2 | 1002 | 9002 | 2021-09-01 12:01:01 | 2021-09-01 12:31:01 | 81 | +| 3 | 1003 | 9001 | 2021-09-01 19:01:01 | 2021-09-01 19:40:01 | 86 | +| 4 | 1003 | 9002 | 2021-09-01 12:01:01 | 2021-09-01 12:31:51 | 89 | +| 5 | 1004 | 9001 | 2021-09-01 19:01:01 | 2021-09-01 19:30:01 | 85 | +| 6 | 1005 | 9002 | 2021-09-01 12:01:01 | 2021-09-01 12:31:02 | 85 | +| 7 | 1006 | 9003 | 2021-09-07 10:01:01 | 2021-09-07 10:21:01 | 84 | +| 8 | 1006 | 9001 | 2021-09-07 10:01:01 | 2021-09-07 10:21:01 | 80 | + +题目练习记录表 practice_record(uid 用户 ID, question_id 题目 ID, submit_time 提交时间, score 得分): + +| id | uid | question_id | submit_time | score | +| --- | ---- | ----------- | ------------------- | ----- | +| 1 | 1001 | 8001 | 2021-08-02 11:41:01 | 60 | +| 2 | 1002 | 8001 | 2021-09-02 19:30:01 | 50 | +| 3 | 1002 | 8001 | 2021-09-02 19:20:01 | 70 | +| 4 | 1002 | 8002 | 2021-09-02 19:38:01 | 70 | +| 5 | 1004 | 8001 | 2021-08-02 19:38:01 | 70 | +| 6 | 1004 | 8002 | 2021-08-02 19:48:01 | 90 | +| 7 | 1001 | 8002 | 2021-08-02 19:38:01 | 70 | +| 8 | 1004 | 8002 | 2021-08-02 19:48:01 | 90 | +| 9 | 1004 | 8002 | 2021-08-02 19:58:01 | 94 | +| 10 | 1004 | 8003 | 2021-08-02 19:38:01 | 70 | +| 11 | 1004 | 8003 | 2021-08-02 19:48:01 | 90 | +| 12 | 1004 | 8003 | 2021-08-01 19:38:01 | 80 | + +请你找到高难度 SQL 试卷得分平均值大于 80 并且是 7 级的红名大佬,统计他们的 2021 年试卷总完成次数和题目总练习次数,只保留 2021 年有试卷完成记录的用户。结果按试卷完成数升序,按题目练习数降序。 + +示例数据输出如下: + +| uid | exam_cnt | question_cnt | +| ---- | -------- | ------------ | +| 1001 | 1 | 2 | +| 1003 | 2 | 0 | + +解释:用户 1001、1003、1004、1006 满足高难度 SQL 试卷得分平均值大于 80,但只有 1001、1003 是 7 级红名大佬;1001 完成了 1 次试卷 1001,练习了 2 次题目;1003 完成了 2 次试卷 9001、9002,未练习题目(因此计数为 0) + +**思路:** + +先将条件进行初步筛选,比如先查出做过高难度 sql 试卷的用户 + +```sql +SELECT + record.uid +FROM + exam_record record + INNER JOIN examination_info e_info ON record.exam_id = e_info.exam_id + JOIN user_info u_info ON record.uid = u_info.uid +WHERE + e_info.tag = 'SQL' + AND e_info.difficulty = 'hard' +``` + +然后根据题目要求,接着再往里叠条件即可; + +但是这里又要注意: + +第一:不能`YEAR(submit_time)= 2021`这个条件放到最后,要在`ON`条件里,因为左连接存在返回左表全部行,右表为 null 的情形,放在 `JOIN`条件的 `ON` 子句中的目的是为了确保在连接两个表时,只有满足年份条件的记录会进行连接。这样可以避免其他年份的记录被包含在结果中。即 1001 做过 2021 年的试卷,但没有练习过,如果把条件放到最后,就会排除掉这种情况。 + +第二,必须是`COUNT(distinct er.exam_id) exam_cnt, COUNT(distinct pr.id) question_cnt,`要加 distinct,因为有左连接产生很多重复值。 + +**答案**: + +```sql +SELECT er.uid AS UID, + count(DISTINCT er.exam_id) AS exam_cnt, + count(DISTINCT pr.id) AS question_cnt +FROM exam_record er +LEFT JOIN practice_record pr ON er.uid = pr.uid +AND YEAR (er.submit_time)= 2021 +AND YEAR (pr.submit_time)= 2021 +WHERE er.uid IN + (SELECT er.uid + FROM exam_record er + LEFT JOIN examination_info ei ON er.exam_id = ei.exam_id + LEFT JOIN user_info ui ON er.uid = ui.uid + WHERE tag = 'SQL' + AND difficulty = 'hard' + AND LEVEL = 7 + GROUP BY er.uid + HAVING avg(score) > 80) +GROUP BY er.uid +ORDER BY exam_cnt, + question_cnt DESC +``` + +可能细心的小伙伴会发现,为什么明明将条件限制了`tag = 'SQL' AND difficulty = 'hard'`,但是用户 1003 仍然能查出两条考试记录,其中一条的考试`tag`为 `C++`; 这是由于`LEFT JOIN`的特性,即使没有与右表匹配的行,左表的所有记录仍然会被保留。 + +### 每个 6/7 级用户活跃情况(困难) + +**描述**: + +现有用户信息表 `user_info`(`uid` 用户 ID,`nick_name` 昵称, `achievement` 成就值, `level` 等级, `job` 职业方向, `register_time` 注册时间): + +| id | uid | nick_name | achievement | level | job | register_time | +| --- | ---- | --------- | ----------- | ----- | ---- | ------------------- | +| 1 | 1001 | 牛客 1 号 | 3100 | 7 | 算法 | 2020-01-01 10:00:00 | +| 2 | 1002 | 牛客 2 号 | 2300 | 7 | 算法 | 2020-01-01 10:00:00 | +| 3 | 1003 | 牛客 3 号 | 2500 | 7 | 算法 | 2020-01-01 10:00:00 | +| 4 | 1004 | 牛客 4 号 | 1200 | 5 | 算法 | 2020-01-01 10:00:00 | +| 5 | 1005 | 牛客 5 号 | 1600 | 6 | C++ | 2020-01-01 10:00:00 | +| 6 | 1006 | 牛客 6 号 | 2600 | 7 | C++ | 2020-01-01 10:00:00 | + +试卷信息表 `examination_info`(`exam_id` 试卷 ID, `tag` 试卷类别, `difficulty` 试卷难度, `duration` 考试时长, `release_time` 发布时间): + +| id | exam_id | tag | difficulty | duration | release_time | +| --- | ------- | ---- | ---------- | -------- | ------------------- | +| 1 | 9001 | SQL | hard | 60 | 2021-09-01 06:00:00 | +| 2 | 9002 | C++ | easy | 60 | 2021-09-01 06:00:00 | +| 3 | 9003 | 算法 | medium | 80 | 2021-09-01 10:00:00 | + +试卷作答记录表 `exam_record`(`uid` 用户 ID, `exam_id` 试卷 ID, `start_time` 开始作答时间, `submit_time` 交卷时间, `score` 得分): + +| uid | exam_id | start_time | submit_time | score | +| ---- | ------- | ------------------- | ------------------- | ------ | +| 1001 | 9001 | 2021-09-01 09:01:01 | 2021-09-01 09:31:00 | 78 | +| 1001 | 9001 | 2021-09-01 09:01:01 | 2021-09-01 09:31:00 | 81 | +| 1005 | 9001 | 2021-09-01 19:01:01 | 2021-09-01 19:30:01 | 85 | +| 1005 | 9002 | 2021-09-01 12:01:01 | 2021-09-01 12:31:02 | 85 | +| 1006 | 9003 | 2021-09-07 10:01:01 | 2021-09-07 10:21:59 | 84 | +| 1006 | 9001 | 2021-09-07 10:01:01 | 2021-09-07 10:21:01 | 81 | +| 1002 | 9001 | 2020-09-01 13:01:01 | 2020-09-01 13:41:01 | 81 | +| 1005 | 9001 | 2021-09-01 14:01:01 | (NULL) | (NULL) | + +题目练习记录表 `practice_record`(`uid` 用户 ID, `question_id` 题目 ID, `submit_time` 提交时间, `score` 得分): + +| uid | question_id | submit_time | score | +| ---- | ----------- | ------------------- | ----- | +| 1001 | 8001 | 2021-08-02 11:41:01 | 60 | +| 1004 | 8001 | 2021-08-02 19:38:01 | 70 | +| 1004 | 8002 | 2021-08-02 19:48:01 | 90 | +| 1001 | 8002 | 2021-08-02 19:38:01 | 70 | +| 1004 | 8002 | 2021-08-02 19:48:01 | 90 | +| 1006 | 8002 | 2021-08-04 19:58:01 | 94 | +| 1006 | 8003 | 2021-08-03 19:38:01 | 70 | +| 1006 | 8003 | 2021-08-02 19:48:01 | 90 | +| 1006 | 8003 | 2020-08-01 19:38:01 | 80 | + +请统计每个 6/7 级用户总活跃月份数、2021 年活跃天数、2021 年试卷作答活跃天数、2021 年答题活跃天数,按照总活跃月份数、2021 年活跃天数降序排序。由示例数据结果输出如下: + +| uid | act_month_total | act_days_2021 | act_days_2021_exam | +| ---- | --------------- | ------------- | ------------------ | +| 1006 | 3 | 4 | 1 | +| 1001 | 2 | 2 | 1 | +| 1005 | 1 | 1 | 1 | +| 1002 | 1 | 0 | 0 | +| 1003 | 0 | 0 | 0 | + +**解释**:6/7 级用户共有 5 个,其中 1006 在 202109、202108、202008 共 3 个月活跃过,2021 年活跃的日期有 20210907、20210804、20210803、20210802 共 4 天,2021 年在试卷作答区 20210907 活跃 1 天,在题目练习区活跃了 3 天。 + +**思路:** + +这题的关键在于`CASE WHEN THEN`的使用,不然要写很多的`left join` 因为会产生很多的结果集。 + +`CASE WHEN THEN`语句是一种条件表达式,用于在 SQL 中根据条件执行不同的操作或返回不同的结果。 + +语法结构如下: + +```sql +CASE + WHEN condition1 THEN result1 + WHEN condition2 THEN result2 + ... + ELSE result +END +``` + +在这个结构中,可以根据需要添加多个`WHEN`子句,每个`WHEN`子句后面跟着一个条件(condition)和一个结果(result)。条件可以是任何逻辑表达式,如果满足条件,将返回对应的结果。 + +最后的`ELSE`子句是可选的,用于指定当所有前面的条件都不满足时的默认返回结果。如果没有提供`ELSE`子句,则默认返回`NULL`。 + +例如: + +```sql +SELECT score, + CASE + WHEN score >= 90 THEN '优秀' + WHEN score >= 80 THEN '良好' + WHEN score >= 60 THEN '及格' + ELSE '不及格' + END AS grade +FROM student_scores; +``` + +在上述示例中,根据学生成绩(score)的不同范围,使用 CASE WHEN THEN 语句返回相应的等级(grade)。如果成绩大于等于 90,则返回"优秀";如果成绩大于等于 80,则返回"良好";如果成绩大于等于 60,则返回"及格";否则返回"不及格"。 + +那了解到了上述的用法之后,回过头看看该题,要求列出不同的活跃天数。 + +```sql +count(distinct act_month) as act_month_total, +count(distinct case when year(act_time)='2021'then act_day end) as act_days_2021, +count(distinct case when year(act_time)='2021' and tag='exam' then act_day end) as act_days_2021_exam, +count(distinct case when year(act_time)='2021' and tag='question'then act_day end) as act_days_2021_question +``` + +这里的 tag 是先给标记,方便对查询进行区分,将考试和答题分开。 + +找出试卷作答区的用户 + +```sql +SELECT + uid, + exam_id AS ans_id, + start_time AS act_time, + date_format( start_time, '%Y%m' ) AS act_month, + date_format( start_time, '%Y%m%d' ) AS act_day, + 'exam' AS tag + FROM + exam_record +``` + +紧接着就是答题作答区的用户 + +```sql +SELECT + uid, + question_id AS ans_id, + submit_time AS act_time, + date_format( submit_time, '%Y%m' ) AS act_month, + date_format( submit_time, '%Y%m%d' ) AS act_day, + 'question' AS tag + FROM + practice_record +``` + +最后将两个结果进行`UNION` 最后别忘了将结果进行排序 (这题有点类似于分治法的思想) + +**答案**: + +```sql +SELECT user_info.uid, + count(DISTINCT act_month) AS act_month_total, + count(DISTINCT CASE + WHEN YEAR (act_time)= '2021' THEN act_day + END) AS act_days_2021, + count(DISTINCT CASE + WHEN YEAR (act_time)= '2021' + AND tag = 'exam' THEN act_day + END) AS act_days_2021_exam, + count(DISTINCT CASE + WHEN YEAR (act_time)= '2021' + AND tag = 'question' THEN act_day + END) AS act_days_2021_question +FROM + (SELECT UID, + exam_id AS ans_id, + start_time AS act_time, + date_format(start_time, '%Y%m') AS act_month, + date_format(start_time, '%Y%m%d') AS act_day, + 'exam' AS tag + FROM exam_record + UNION ALL SELECT UID, + question_id AS ans_id, + submit_time AS act_time, + date_format(submit_time, '%Y%m') AS act_month, + date_format(submit_time, '%Y%m%d') AS act_day, + 'question' AS tag + FROM practice_record) total +RIGHT JOIN user_info ON total.uid = user_info.uid +WHERE user_info.LEVEL IN (6, + 7) +GROUP BY user_info.uid +ORDER BY act_month_total DESC, + act_days_2021 DESC +``` + + diff --git a/docs/database/sql/sql-questions-04.md b/docs/database/sql/sql-questions-04.md new file mode 100644 index 00000000000..84f1a2b3c8c --- /dev/null +++ b/docs/database/sql/sql-questions-04.md @@ -0,0 +1,832 @@ +--- +title: SQL常见面试题总结(4) +category: 数据库 +tag: + - 数据库基础 + - SQL +--- + +> 题目来源于:[牛客题霸 - SQL 进阶挑战](https://www.nowcoder.com/exam/oj?page=1&tab=SQL%E7%AF%87&topicId=240) + +较难或者困难的题目可以根据自身实际情况和面试需要来决定是否要跳过。 + +## 专用窗口函数 + +MySQL 8.0 版本引入了窗口函数的支持,下面是 MySQL 中常见的窗口函数及其用法: + +1. `ROW_NUMBER()`: 为查询结果集中的每一行分配一个唯一的整数值。 + +```sql +SELECT col1, col2, ROW_NUMBER() OVER (ORDER BY col1) AS row_num +FROM table; +``` + +2. `RANK()`: 计算每一行在排序结果中的排名。 + +```sql +SELECT col1, col2, RANK() OVER (ORDER BY col1 DESC) AS ranking +FROM table; +``` + +3. `DENSE_RANK()`: 计算每一行在排序结果中的排名,保留相同的排名。 + +```sql +SELECT col1, col2, DENSE_RANK() OVER (ORDER BY col1 DESC) AS ranking +FROM table; +``` + +4. `NTILE(n)`: 将结果分成 n 个基本均匀的桶,并为每个桶分配一个标识号。 + +```sql +SELECT col1, col2, NTILE(4) OVER (ORDER BY col1) AS bucket +FROM table; +``` + +5. `SUM()`, `AVG()`,`COUNT()`, `MIN()`, `MAX()`: 这些聚合函数也可以与窗口函数结合使用,计算窗口内指定列的汇总、平均值、计数、最小值和最大值。 + +```sql +SELECT col1, col2, SUM(col1) OVER () AS sum_col +FROM table; +``` + +6. `LEAD()` 和 `LAG()`: LEAD 函数用于获取当前行之后的某个偏移量的行的值,而 LAG 函数用于获取当前行之前的某个偏移量的行的值。 + +```sql +SELECT col1, col2, LEAD(col1, 1) OVER (ORDER BY col1) AS next_col1, + LAG(col1, 1) OVER (ORDER BY col1) AS prev_col1 +FROM table; +``` + +7. `FIRST_VALUE()` 和 `LAST_VALUE()`: FIRST_VALUE 函数用于获取窗口内指定列的第一个值,LAST_VALUE 函数用于获取窗口内指定列的最后一个值。 + +```sql +SELECT col1, col2, FIRST_VALUE(col2) OVER (PARTITION BY col1 ORDER BY col2) AS first_val, + LAST_VALUE(col2) OVER (PARTITION BY col1 ORDER BY col2) AS last_val +FROM table; +``` + +窗口函数通常需要配合 OVER 子句一起使用,用于定义窗口的大小、排序规则和分组方式。 + +### 每类试卷得分前三名 + +**描述**: + +现有试卷信息表 `examination_info`(`exam_id` 试卷 ID, `tag` 试卷类别, `difficulty` 试卷难度, `duration` 考试时长, `release_time` 发布时间): + +| id | exam_id | tag | difficulty | duration | release_time | +| --- | ------- | ---- | ---------- | -------- | ------------------- | +| 1 | 9001 | SQL | hard | 60 | 2021-09-01 06:00:00 | +| 2 | 9002 | SQL | hard | 60 | 2021-09-01 06:00:00 | +| 3 | 9003 | 算法 | medium | 80 | 2021-09-01 10:00:00 | + +试卷作答记录表 `exam_record`(`uid` 用户 ID, `exam_id` 试卷 ID, `start_time` 开始作答时间, `submit_time` 交卷时间, score 得分): + +| id | uid | exam_id | start_time | submit_time | score | +| --- | ---- | ------- | ------------------- | ------------------- | ------ | +| 1 | 1001 | 9001 | 2021-09-01 09:01:01 | 2021-09-01 09:31:00 | 78 | +| 2 | 1002 | 9001 | 2021-09-01 09:01:01 | 2021-09-01 09:31:00 | 81 | +| 3 | 1002 | 9002 | 2021-09-01 12:01:01 | 2021-09-01 12:31:01 | 81 | +| 4 | 1003 | 9001 | 2021-09-01 19:01:01 | 2021-09-01 19:40:01 | 86 | +| 5 | 1003 | 9002 | 2021-09-01 12:01:01 | 2021-09-01 12:31:51 | 89 | +| 6 | 1004 | 9001 | 2021-09-01 19:01:01 | 2021-09-01 19:30:01 | 85 | +| 7 | 1005 | 9003 | 2021-09-01 12:01:01 | 2021-09-01 12:31:02 | 85 | +| 8 | 1006 | 9003 | 2021-09-07 10:01:01 | 2021-09-07 10:21:01 | 84 | +| 9 | 1003 | 9003 | 2021-09-08 12:01:01 | 2021-09-08 12:11:01 | 40 | +| 10 | 1003 | 9002 | 2021-09-01 14:01:01 | (NULL) | (NULL) | + +找到每类试卷得分的前 3 名,如果两人最大分数相同,选择最小分数大者,如果还相同,选择 uid 大者。由示例数据结果输出如下: + +| tid | uid | ranking | +| ---- | ---- | ------- | +| SQL | 1003 | 1 | +| SQL | 1004 | 2 | +| SQL | 1002 | 3 | +| 算法 | 1005 | 1 | +| 算法 | 1006 | 2 | +| 算法 | 1003 | 3 | + +**解释**:有作答得分记录的试卷 tag 有 SQL 和算法,SQL 试卷用户 1001、1002、1003、1004 有作答得分,最高得分分别为 81、81、89、85,最低得分分别为 78、81、86、40,因此先按最高得分排名再按最低得分排名取前三为 1003、1004、1002。 + +**答案**: + +```sql +SELECT tag, + UID, + ranking +FROM + (SELECT b.tag AS tag, + a.uid AS UID, + ROW_NUMBER() OVER (PARTITION BY b.tag + ORDER BY b.tag, + max(a.score) DESC, + min(a.score) DESC, + a.uid DESC) AS ranking + FROM exam_record a + LEFT JOIN examination_info b ON a.exam_id = b.exam_id + GROUP BY b.tag, + a.uid) t +WHERE ranking <= 3 +``` + +### 第二快/慢用时之差大于试卷时长一半的试卷(较难) + +**描述**: + +现有试卷信息表 `examination_info`(`exam_id` 试卷 ID, `tag` 试卷类别, `difficulty` 试卷难度, `duration` 考试时长, `release_time` 发布时间): + +| id | exam_id | tag | difficulty | duration | release_time | +| --- | ------- | ---- | ---------- | -------- | ------------------- | +| 1 | 9001 | SQL | hard | 60 | 2021-09-01 06:00:00 | +| 2 | 9002 | C++ | hard | 60 | 2021-09-01 06:00:00 | +| 3 | 9003 | 算法 | medium | 80 | 2021-09-01 10:00:00 | + +试卷作答记录表 `exam_record`(`uid` 用户 ID, `exam_id` 试卷 ID, `start_time` 开始作答时间, `submit_time` 交卷时间, `score` 得分): + +| id | uid | exam_id | start_time | submit_time | score | +| ---- | ---- | ------- | ------------------- | ------------------- | ------ | +| 1 | 1001 | 9001 | 2021-09-01 09:01:01 | 2021-09-01 09:51:01 | 78 | +| 2 | 1001 | 9002 | 2021-09-01 09:01:01 | 2021-09-01 09:31:00 | 81 | +| 3 | 1002 | 9002 | 2021-09-01 12:01:01 | 2021-09-01 12:31:01 | 81 | +| 4 | 1003 | 9001 | 2021-09-01 19:01:01 | 2021-09-01 19:59:01 | 86 | +| 5 | 1003 | 9002 | 2021-09-01 12:01:01 | 2021-09-01 12:31:51 | 89 | +| 6 | 1004 | 9002 | 2021-09-01 19:01:01 | 2021-09-01 19:30:01 | 85 | +| 7 | 1005 | 9001 | 2021-09-01 12:01:01 | 2021-09-01 12:31:02 | 85 | +| 8 | 1006 | 9001 | 2021-09-07 10:02:01 | 2021-09-07 10:21:01 | 84 | +| 9 | 1003 | 9001 | 2021-09-08 12:01:01 | 2021-09-08 12:11:01 | 40 | +| 10 | 1003 | 9002 | 2021-09-01 14:01:01 | (NULL) | (NULL) | +| 11 | 1005 | 9001 | 2021-09-01 14:01:01 | (NULL) | (NULL) | +| 12 | 1003 | 9003 | 2021-09-08 15:01:01 | (NULL) | (NULL) | + +找到第二快和第二慢用时之差大于试卷时长的一半的试卷信息,按试卷 ID 降序排序。由示例数据结果输出如下: + +| exam_id | duration | release_time | +| ------- | -------- | ------------------- | +| 9001 | 60 | 2021-09-01 06:00:00 | + +**解释**:试卷 9001 被作答用时有 50 分钟、58 分钟、30 分 1 秒、19 分钟、10 分钟,第二快和第二慢用时之差为 50 分钟-19 分钟=31 分钟,试卷时长为 60 分钟,因此满足大于试卷时长一半的条件,输出试卷 ID、时长、发布时间。 + +**思路:** + +第一步,找到每张试卷完成时间的顺序排名和倒序排名 也就是表 a; + +第二步,与通过试卷信息表 b 建立内连接,并根据试卷 id 分组,利用`having`筛选排名为第二个数据,将秒转化为分钟并进行比较,最后再根据试卷 id 倒序排序就行 + +**答案**: + +```sql +SELECT a.exam_id, + b.duration, + b.release_time +FROM + (SELECT exam_id, + row_number() OVER (PARTITION BY exam_id + ORDER BY timestampdiff(SECOND, start_time, submit_time) DESC) rn1, + row_number() OVER (PARTITION BY exam_id + ORDER BY timestampdiff(SECOND, start_time, submit_time) ASC) rn2, + timestampdiff(SECOND, start_time, submit_time) timex + FROM exam_record + WHERE score IS NOT NULL ) a +INNER JOIN examination_info b ON a.exam_id = b.exam_id +GROUP BY a.exam_id +HAVING (max(IF (rn1 = 2, a.timex, 0))- max(IF (rn2 = 2, a.timex, 0)))/ 60 > b.duration / 2 +ORDER BY a.exam_id DESC +``` + +### 连续两次作答试卷的最大时间窗(较难) + +**描述** + +现有试卷作答记录表 `exam_record`(`uid` 用户 ID, `exam_id` 试卷 ID, `start_time` 开始作答时间, `submit_time` 交卷时间, `score` 得分): + +| id | uid | exam_id | start_time | submit_time | score | +| --- | ---- | ------- | ------------------- | ------------------- | ----- | +| 1 | 1006 | 9003 | 2021-09-07 10:01:01 | 2021-09-07 10:21:02 | 84 | +| 2 | 1006 | 9001 | 2021-09-01 12:11:01 | 2021-09-01 12:31:01 | 89 | +| 3 | 1006 | 9002 | 2021-09-06 10:01:01 | 2021-09-06 10:21:01 | 81 | +| 4 | 1005 | 9002 | 2021-09-05 10:01:01 | 2021-09-05 10:21:01 | 81 | +| 5 | 1005 | 9001 | 2021-09-05 10:31:01 | 2021-09-05 10:51:01 | 81 | + +请计算在 2021 年至少有两天作答过试卷的人中,计算该年连续两次作答试卷的最大时间窗 `days_window`,那么根据该年的历史规律他在 `days_window` 天里平均会做多少套试卷,按最大时间窗和平均做答试卷套数倒序排序。由示例数据结果输出如下: + +| uid | days_window | avg_exam_cnt | +| ---- | ----------- | ------------ | +| 1006 | 6 | 2.57 | + +**解释**:用户 1006 分别在 20210901、20210906、20210907 作答过 3 次试卷,连续两次作答最大时间窗为 6 天(1 号到 6 号),他 1 号到 7 号这 7 天里共做了 3 张试卷,平均每天 3/7=0.428571 张,那么 6 天里平均会做 0.428571\*6=2.57 张试卷(保留两位小数);用户 1005 在 20210905 做了两张试卷,但是只有一天的作答记录,过滤掉。 + +**思路:** + +上面这个解释中提示要对作答记录去重,千万别被骗了,不要去重!去重就通不过测试用例。注意限制时间是 2021 年; + +而且要注意时间差要+1 天;还要注意==没交卷也算在内==!!!! (反正感觉这题描述不清,出的不是很好) + +**答案**: + +```sql +SELECT UID, + max(datediff(next_time, start_time)) + 1 AS days_window, + round(count(start_time)/(datediff(max(start_time), min(start_time))+ 1) * (max(datediff(next_time, start_time))+ 1), 2) AS avg_exam_cnt +FROM + (SELECT UID, + start_time, + lead(start_time, 1) OVER (PARTITION BY UID + ORDER BY start_time) AS next_time + FROM exam_record + WHERE YEAR (start_time) = '2021' ) a +GROUP BY UID +HAVING count(DISTINCT date(start_time)) > 1 +ORDER BY days_window DESC, + avg_exam_cnt DESC +``` + +### 近三个月未完成为 0 的用户完成情况 + +**描述**: + +现有试卷作答记录表 `exam_record`(`uid`:用户 ID, `exam_id`:试卷 ID, `start_time`:开始作答时间, `submit_time`:交卷时间,为空的话则代表未完成, `score`:得分): + +| id | uid | exam_id | start_time | submit_time | score | +| --- | ---- | ------- | ------------------- | ------------------- | ------ | +| 1 | 1006 | 9003 | 2021-09-06 10:01:01 | 2021-09-06 10:21:02 | 84 | +| 2 | 1006 | 9001 | 2021-08-02 12:11:01 | 2021-08-02 12:31:01 | 89 | +| 3 | 1006 | 9002 | 2021-06-06 10:01:01 | 2021-06-06 10:21:01 | 81 | +| 4 | 1006 | 9002 | 2021-05-06 10:01:01 | 2021-05-06 10:21:01 | 81 | +| 5 | 1006 | 9001 | 2021-05-01 12:01:01 | (NULL) | (NULL) | +| 6 | 1001 | 9001 | 2021-09-05 10:31:01 | 2021-09-05 10:51:01 | 81 | +| 7 | 1001 | 9003 | 2021-08-01 09:01:01 | 2021-08-01 09:51:11 | 78 | +| 8 | 1001 | 9002 | 2021-07-01 09:01:01 | 2021-07-01 09:31:00 | 81 | +| 9 | 1001 | 9002 | 2021-07-01 12:01:01 | 2021-07-01 12:31:01 | 81 | +| 10 | 1001 | 9002 | 2021-07-01 12:01:01 | (NULL) | (NULL) | + +找到每个人近三个有试卷作答记录的月份中没有试卷是未完成状态的用户的试卷作答完成数,按试卷完成数和用户 ID 降序排名。由示例数据结果输出如下: + +| uid | exam_complete_cnt | +| ---- | ----------------- | +| 1006 | 3 | + +**解释**:用户 1006 近三个有作答试卷的月份为 202109、202108、202106,作答试卷数为 3,全部完成;用户 1001 近三个有作答试卷的月份为 202109、202108、202107,作答试卷数为 5,完成试卷数为 4,因为有未完成试卷,故过滤掉。 + +**思路:** + +1. `找到每个人近三个有试卷作答记录的月份中没有试卷是未完成状态的用户的试卷作答完成数`首先看这句话,肯定要先根据人进行分组 +2. 最近三个月,可以采用连续重复排名,倒序排列,排名<=3 +3. 统计作答数 +4. 拼装剩余条件 +5. 排序 + +**答案**: + +```sql +SELECT UID, + count(score) exam_complete_cnt +FROM + (SELECT *, DENSE_RANK() OVER (PARTITION BY UID + ORDER BY date_format(start_time, '%Y%m') DESC) dr + FROM exam_record) t1 +WHERE dr <= 3 +GROUP BY UID +HAVING count(dr)= count(score) +ORDER BY exam_complete_cnt DESC, + UID DESC +``` + +### 未完成率较高的 50%用户近三个月答卷情况(困难) + +**描述**: + +现有用户信息表 `user_info`(`uid` 用户 ID,`nick_name` 昵称, `achievement` 成就值, `level` 等级, `job` 职业方向, `register_time` 注册时间): + +| id | uid | nick_name | achievement | level | job | register_time | +| --- | ---- | ------------ | ----------- | ----- | ---- | ------------------- | +| 1 | 1001 | 牛客 1 号 | 3200 | 7 | 算法 | 2020-01-01 10:00:00 | +| 2 | 1002 | 牛客 2 号 | 2500 | 6 | 算法 | 2020-01-01 10:00:00 | +| 3 | 1003 | 牛客 3 号 ♂ | 2200 | 5 | 算法 | 2020-01-01 10:00:00 | + +试卷信息表 `examination_info`(`exam_id` 试卷 ID, `tag` 试卷类别, `difficulty` 试卷难度, `duration` 考试时长, `release_time` 发布时间): + +| id | exam_id | tag | difficulty | duration | release_time | +| --- | ------- | ------ | ---------- | -------- | ------------------- | +| 1 | 9001 | SQL | hard | 60 | 2020-01-01 10:00:00 | +| 2 | 9002 | SQL | hard | 80 | 2020-01-01 10:00:00 | +| 3 | 9003 | 算法 | hard | 80 | 2020-01-01 10:00:00 | +| 4 | 9004 | PYTHON | medium | 70 | 2020-01-01 10:00:00 | + +试卷作答记录表 `exam_record`(`uid` 用户 ID, `exam_id` 试卷 ID, `start_time` 开始作答时间, `submit_time` 交卷时间, `score` 得分): + +| id | uid | exam_id | start_time | submit_time | score | +| --- | ---- | ------- | ------------------- | ------------------- | ----- | +| 1 | 1001 | 9001 | 2020-01-01 09:01:01 | 2020-01-01 09:21:59 | 90 | +| 15 | 1002 | 9001 | 2020-01-01 18:01:01 | 2020-01-01 18:59:02 | 90 | +| 13 | 1001 | 9001 | 2020-01-02 10:01:01 | 2020-01-02 10:31:01 | 89 | +| 2 | 1002 | 9001 | 2020-01-20 10:01:01 | | | +| 3 | 1002 | 9001 | 2020-02-01 12:11:01 | | | +| 5 | 1001 | 9001 | 2020-03-01 12:01:01 | | | +| 6 | 1002 | 9001 | 2020-03-01 12:01:01 | 2020-03-01 12:41:01 | 90 | +| 4 | 1003 | 9001 | 2020-03-01 19:01:01 | | | +| 7 | 1002 | 9001 | 2020-05-02 19:01:01 | 2020-05-02 19:32:00 | 90 | +| 14 | 1001 | 9002 | 2020-01-01 12:11:01 | | | +| 8 | 1001 | 9002 | 2020-01-02 19:01:01 | 2020-01-02 19:59:01 | 69 | +| 9 | 1001 | 9002 | 2020-02-02 12:01:01 | 2020-02-02 12:20:01 | 99 | +| 10 | 1002 | 9002 | 2020-02-02 12:01:01 | | | +| 11 | 1002 | 9002 | 2020-02-02 12:01:01 | 2020-02-02 12:43:01 | 81 | +| 12 | 1002 | 9002 | 2020-03-02 12:11:01 | | | +| 17 | 1001 | 9002 | 2020-05-05 18:01:01 | | | +| 16 | 1002 | 9003 | 2020-05-06 12:01:01 | | | + +请统计 SQL 试卷上未完成率较高的 50%用户中,6 级和 7 级用户在有试卷作答记录的近三个月中,每个月的答卷数目和完成数目。按用户 ID、月份升序排序。 + +由示例数据结果输出如下: + +| uid | start_month | total_cnt | complete_cnt | +| ---- | ----------- | --------- | ------------ | +| 1002 | 202002 | 3 | 1 | +| 1002 | 202003 | 2 | 1 | +| 1002 | 202005 | 2 | 1 | + +解释:各个用户对 SQL 试卷的未完成数、作答总数、未完成率如下: + +| uid | incomplete_cnt | total_cnt | incomplete_rate | +| ---- | -------------- | --------- | --------------- | +| 1001 | 3 | 7 | 0.4286 | +| 1002 | 4 | 8 | 0.5000 | +| 1003 | 1 | 1 | 1.0000 | + +1001、1002、1003 分别排在 1.0、0.5、0.0 的位置,因此较高的 50%用户(排位<=0.5)为 1002、1003; + +1003 不是 6 级或 7 级; + +有试卷作答记录的近三个月为 202005、202003、202002; + +这三个月里 1002 的作答题数分别为 3、2、2,完成数目分别为 1、1、1。 + +**思路:** + +注意点:这题注意求的是所有的答题次数和完成次数,而 sql 类别的试卷是限制未完成率排名,6, 7 级用户限制的是做题记录。 + +先求出未完成率的排名 + +```sql +SELECT UID, + count(submit_time IS NULL + OR NULL)/ count(start_time) AS num, + PERCENT_RANK() OVER ( + ORDER BY count(submit_time IS NULL + OR NULL)/ count(start_time)) AS ranking +FROM exam_record +LEFT JOIN examination_info USING (exam_id) +WHERE tag = 'SQL' +GROUP BY UID +``` + +再求出最近三个月的练习记录 + +```sql +SELECT UID, + date_format(start_time, '%Y%m') AS month_d, + submit_time, + exam_id, + dense_rank() OVER (PARTITION BY UID + ORDER BY date_format(start_time, '%Y%m') DESC) AS ranking +FROM exam_record +LEFT JOIN user_info USING (UID) +WHERE LEVEL IN (6,7) +``` + +**答案**: + +```sql +SELECT t1.uid, + t1.month_d, + count(*) AS total_cnt, + count(t1.submit_time) AS complete_cnt +FROM-- 先求出未完成率的排名 + + (SELECT UID, + count(submit_time IS NULL OR NULL)/ count(start_time) AS num, + PERCENT_RANK() OVER ( + ORDER BY count(submit_time IS NULL OR NULL)/ count(start_time)) AS ranking + FROM exam_record + LEFT JOIN examination_info USING (exam_id) + WHERE tag = 'SQL' + GROUP BY UID) t +INNER JOIN + (-- 再求出近三个月的练习记录 + SELECT UID, + date_format(start_time, '%Y%m') AS month_d, + submit_time, + exam_id, + dense_rank() OVER (PARTITION BY UID + ORDER BY date_format(start_time, '%Y%m') DESC) AS ranking + FROM exam_record + LEFT JOIN user_info USING (UID) + WHERE LEVEL IN (6,7) ) t1 USING (UID) +WHERE t1.ranking <= 3 AND t.ranking >= 0.5 -- 使用限制找到符合条件的记录 + +GROUP BY t1.uid, + t1.month_d +ORDER BY t1.uid, + t1.month_d +``` + +### 试卷完成数同比 2020 年的增长率及排名变化(困难) + +**描述**: + +现有试卷信息表 `examination_info`(`exam_id` 试卷 ID, `tag` 试卷类别, `difficulty` 试卷难度, `duration` 考试时长, `release_time` 发布时间): + +| id | exam_id | tag | difficulty | duration | release_time | +| --- | ------- | ------ | ---------- | -------- | ------------------- | +| 1 | 9001 | SQL | hard | 60 | 2021-01-01 10:00:00 | +| 2 | 9002 | C++ | hard | 80 | 2021-01-01 10:00:00 | +| 3 | 9003 | 算法 | hard | 80 | 2021-01-01 10:00:00 | +| 4 | 9004 | PYTHON | medium | 70 | 2021-01-01 10:00:00 | + +试卷作答记录表 `exam_record`(`uid` 用户 ID, `exam_id` 试卷 ID, `start_time` 开始作答时间, `submit_time` 交卷时间, `score` 得分): + +| id | uid | exam_id | start_time | submit_time | score | +| --- | ---- | ------- | ------------------- | ------------------- | ----- | +| 1 | 1001 | 9001 | 2020-08-02 10:01:01 | 2020-08-02 10:31:01 | 89 | +| 2 | 1002 | 9001 | 2020-04-01 18:01:01 | 2020-04-01 18:59:02 | 90 | +| 3 | 1001 | 9001 | 2020-04-01 09:01:01 | 2020-04-01 09:21:59 | 80 | +| 5 | 1002 | 9001 | 2021-03-02 19:01:01 | 2021-03-02 19:32:00 | 20 | +| 8 | 1003 | 9001 | 2021-05-02 12:01:01 | 2021-05-02 12:31:01 | 98 | +| 13 | 1003 | 9001 | 2020-01-02 10:01:01 | 2020-01-02 10:31:01 | 89 | +| 9 | 1001 | 9002 | 2020-02-02 12:01:01 | 2020-02-02 12:20:01 | 99 | +| 10 | 1002 | 9002 | 2021-02-02 12:01:01 | 2020-02-02 12:43:01 | 81 | +| 11 | 1001 | 9002 | 2020-01-02 19:01:01 | 2020-01-02 19:59:01 | 69 | +| 16 | 1002 | 9002 | 2020-02-02 12:01:01 | | | +| 17 | 1002 | 9002 | 2020-03-02 12:11:01 | | | +| 18 | 1001 | 9002 | 2021-05-05 18:01:01 | | | +| 4 | 1002 | 9003 | 2021-01-20 10:01:01 | 2021-01-20 10:10:01 | 81 | +| 6 | 1001 | 9003 | 2021-04-02 19:01:01 | 2021-04-02 19:40:01 | 89 | +| 15 | 1002 | 9003 | 2021-01-01 18:01:01 | 2021-01-01 18:59:02 | 90 | +| 7 | 1004 | 9004 | 2020-05-02 12:01:01 | 2020-05-02 12:20:01 | 99 | +| 12 | 1001 | 9004 | 2021-09-02 12:11:01 | | | +| 14 | 1002 | 9004 | 2020-01-01 12:11:01 | 2020-01-01 12:31:01 | 83 | + +请计算 2021 年上半年各类试卷的做完次数相比 2020 年上半年同期的增长率(百分比格式,保留 1 位小数),以及做完次数排名变化,按增长率和 21 年排名降序输出。 + +由示例数据结果输出如下: + +| tag | exam_cnt_20 | exam_cnt_21 | growth_rate | exam_cnt_rank_20 | exam_cnt_rank_21 | rank_delta | +| --- | ----------- | ----------- | ----------- | ---------------- | ---------------- | ---------- | +| SQL | 3 | 2 | -33.3% | 1 | 2 | 1 | + +解释:2020 年上半年有 3 个 tag 有作答完成的记录,分别是 C++、SQL、PYTHON,它们被做完的次数分别是 3、3、2,做完次数排名为 1、1(并列)、3; + +2021 年上半年有 2 个 tag 有作答完成的记录,分别是算法、SQL,它们被做完的次数分别是 3、2,做完次数排名为 1、2;具体如下: + +| tag | start_year | exam_cnt | exam_cnt_rank | +| ------ | ---------- | -------- | ------------- | +| C++ | 2020 | 3 | 1 | +| SQL | 2020 | 3 | 1 | +| PYTHON | 2020 | 2 | 3 | +| 算法 | 2021 | 3 | 1 | +| SQL | 2021 | 2 | 2 | + +因此能输出同比结果的 tag 只有 SQL,从 2020 到 2021 年,做完次数 3=>2,减少 33.3%(保留 1 位小数);排名 1=>2,后退 1 名。 + +**思路:** + +本题难点在于长整型的数据类型要求不能有负号产生,用 cast 函数转换数据类型为 signed。 + +以及用到的`增长率计算公式:(exam_cnt_21-exam_cnt_20)/exam_cnt_20` + +做完次数排名变化(2021 年和 2020 年比排名升了或者降了多少) + +计算公式:`exam_cnt_rank_21 - exam_cnt_rank_20` + +在 MySQL 中,`CAST()` 函数用于将一个表达式的数据类型转换为另一个数据类型。它的基本语法如下: + +```sql +CAST(expression AS data_type) + +-- 将一个字符串转换成整数 +SELECT CAST('123' AS INT); +``` + +示例就不一一举例了,这个函数很简单 + +**答案**: + +```sql +SELECT + tag, + exam_cnt_20, + exam_cnt_21, + concat( + round( + 100 * (exam_cnt_21 - exam_cnt_20) / exam_cnt_20, + 1 + ), + '%' + ) AS growth_rate, + exam_cnt_rank_20, + exam_cnt_rank_21, + cast(exam_cnt_rank_21 AS signed) - cast(exam_cnt_rank_20 AS signed) AS rank_delta +FROM + ( + #2020年、2021年上半年各类试卷的做完次数和做完次数排名 + SELECT + tag, + count( + IF ( + date_format(start_time, '%Y%m%d') BETWEEN '20200101' + AND '20200630', + start_time, + NULL + ) + ) AS exam_cnt_20, + count( + IF ( + substring(start_time, 1, 10) BETWEEN '2021-01-01' + AND '2021-06-30', + start_time, + NULL + ) + ) AS exam_cnt_21, + rank() over ( + ORDER BY + count( + IF ( + date_format(start_time, '%Y%m%d') BETWEEN '20200101' + AND '20200630', + start_time, + NULL + ) + ) DESC + ) AS exam_cnt_rank_20, + rank() over ( + ORDER BY + count( + IF ( + substring(start_time, 1, 10) BETWEEN '2021-01-01' + AND '2021-06-30', + start_time, + NULL + ) + ) DESC + ) AS exam_cnt_rank_21 + FROM + examination_info + JOIN exam_record USING (exam_id) + WHERE + submit_time IS NOT NULL + GROUP BY + tag + ) main +WHERE + exam_cnt_21 * exam_cnt_20 <> 0 +ORDER BY + growth_rate DESC, + exam_cnt_rank_21 DESC +``` + +## 聚合窗口函数 + +### 对试卷得分做 min-max 归一化 + +**描述**: + +现有试卷信息表 `examination_info`(`exam_id` 试卷 ID, `tag` 试卷类别, `difficulty` 试卷难度, `duration` 考试时长, `release_time` 发布时间): + +| id | exam_id | tag | difficulty | duration | release_time | +| --- | ------- | ------ | ---------- | -------- | ------------------- | +| 1 | 9001 | SQL | hard | 60 | 2020-01-01 10:00:00 | +| 2 | 9002 | C++ | hard | 80 | 2020-01-01 10:00:00 | +| 3 | 9003 | 算法 | hard | 80 | 2020-01-01 10:00:00 | +| 4 | 9004 | PYTHON | medium | 70 | 2020-01-01 10:00:00 | + +试卷作答记录表 `exam_record`(`uid` 用户 ID, `exam_id` 试卷 ID, `start_time` 开始作答时间, `submit_time` 交卷时间, `score` 得分): + +| id | uid | exam_id | start_time | submit_time | score | +| --- | ---- | ------- | ------------------- | ------------------- | ------ | +| 6 | 1003 | 9001 | 2020-01-02 12:01:01 | 2020-01-02 12:31:01 | 68 | +| 9 | 1001 | 9001 | 2020-01-02 10:01:01 | 2020-01-02 10:31:01 | 89 | +| 1 | 1001 | 9001 | 2020-01-01 09:01:01 | 2020-01-01 09:21:59 | 90 | +| 12 | 1002 | 9002 | 2021-05-05 18:01:01 | (NULL) | (NULL) | +| 3 | 1004 | 9002 | 2020-01-01 12:01:01 | 2020-01-01 12:11:01 | 60 | +| 2 | 1003 | 9002 | 2020-01-01 19:01:01 | 2020-01-01 19:30:01 | 75 | +| 7 | 1001 | 9002 | 2020-01-02 12:01:01 | 2020-01-02 12:43:01 | 81 | +| 10 | 1002 | 9002 | 2020-01-01 12:11:01 | 2020-01-01 12:31:01 | 83 | +| 4 | 1003 | 9002 | 2020-01-01 12:01:01 | 2020-01-01 12:41:01 | 90 | +| 5 | 1002 | 9002 | 2020-01-02 19:01:01 | 2020-01-02 19:32:00 | 90 | +| 11 | 1002 | 9004 | 2021-09-06 12:01:01 | (NULL) | (NULL) | +| 8 | 1001 | 9005 | 2020-01-02 12:11:01 | (NULL) | (NULL) | + +在物理学及统计学数据计算时,有个概念叫 min-max 标准化,也被称为离差标准化,是对原始数据的线性变换,使结果值映射到[0 - 1]之间。 + +转换函数为: + +![](https://oss.javaguide.cn/github/javaguide/database/sql/29A377601170AB822322431FCDF7EDFE.png) + +请你将用户作答高难度试卷的得分在每份试卷作答记录内执行 min-max 归一化后缩放到[0,100]区间,并输出用户 ID、试卷 ID、归一化后分数平均值;最后按照试卷 ID 升序、归一化分数降序输出。(注:得分区间默认为[0,100],如果某个试卷作答记录中只有一个得分,那么无需使用公式,归一化并缩放后分数仍为原分数)。 + +由示例数据结果输出如下: + +| uid | exam_id | avg_new_score | +| ---- | ------- | ------------- | +| 1001 | 9001 | 98 | +| 1003 | 9001 | 0 | +| 1002 | 9002 | 88 | +| 1003 | 9002 | 75 | +| 1001 | 9002 | 70 | +| 1004 | 9002 | 0 | + +解释:高难度试卷有 9001、9002、9003; + +作答了 9001 的记录有 3 条,分数分别为 68、89、90,按给定公式归一化后分数为:0、95、100,而后两个得分都是用户 1001 作答的,因此用户 1001 对试卷 9001 的新得分为(95+100)/2≈98(只保留整数部分),用户 1003 对于试卷 9001 的新得分为 0。最后结果按照试卷 ID 升序、归一化分数降序输出。 + +**思路:** + +注意点: + +1. 将高难度的试卷,按每类试卷的得分,利用 max/min (col) over()窗口函数求得各组内最大最小值,然后进行归一化公式计算,缩放区间为[0,100],即 min_max\*100 +2. 若某类试卷只有一个得分,则无需使用归一化公式,因只有一个分 max_score=min_score,score,公式后结果可能会变成 0。 +3. 最后结果按 uid、exam_id 分组求归一化后均值,score 为 NULL 的要过滤掉。 + +最后就是仔细看上面公式 (说实话,这题看起来就很绕) + +**答案**: + +```sql +SELECT + uid, + exam_id, + round(sum(min_max) / count(score), 0) AS avg_new_score +FROM + ( + SELECT + *, + IF ( + max_score = min_score, + score, + (score - min_score) / (max_score - min_score) * 100 + ) AS min_max + FROM + ( + SELECT + uid, + a.exam_id, + score, + max(score) over (PARTITION BY a.exam_id) AS max_score, + min(score) over (PARTITION BY a.exam_id) AS min_score + FROM + exam_record a + LEFT JOIN examination_info b USING (exam_id) + WHERE + difficulty = 'hard' + ) t + WHERE + score IS NOT NULL + ) t1 +GROUP BY + uid, + exam_id +ORDER BY + exam_id ASC, + avg_new_score DESC; +``` + +### 每份试卷每月作答数和截止当月的作答总数 + +**描述:** + +现有试卷作答记录表 exam_record(uid 用户 ID, exam_id 试卷 ID, start_time 开始作答时间, submit_time 交卷时间, score 得分): + +| id | uid | exam_id | start_time | submit_time | score | +| --- | ---- | ------- | ------------------- | ------------------- | ------ | +| 1 | 1001 | 9001 | 2020-01-01 09:01:01 | 2020-01-01 09:21:59 | 90 | +| 2 | 1002 | 9001 | 2020-01-20 10:01:01 | 2020-01-20 10:10:01 | 89 | +| 3 | 1002 | 9001 | 2020-02-01 12:11:01 | 2020-02-01 12:31:01 | 83 | +| 4 | 1003 | 9001 | 2020-03-01 19:01:01 | 2020-03-01 19:30:01 | 75 | +| 5 | 1004 | 9001 | 2020-03-01 12:01:01 | 2020-03-01 12:11:01 | 60 | +| 6 | 1003 | 9001 | 2020-03-01 12:01:01 | 2020-03-01 12:41:01 | 90 | +| 7 | 1002 | 9001 | 2020-05-02 19:01:01 | 2020-05-02 19:32:00 | 90 | +| 8 | 1001 | 9002 | 2020-01-02 19:01:01 | 2020-01-02 19:59:01 | 69 | +| 9 | 1004 | 9002 | 2020-02-02 12:01:01 | 2020-02-02 12:20:01 | 99 | +| 10 | 1003 | 9002 | 2020-02-02 12:01:01 | 2020-02-02 12:31:01 | 68 | +| 11 | 1001 | 9002 | 2020-02-02 12:01:01 | 2020-02-02 12:43:01 | 81 | +| 12 | 1001 | 9002 | 2020-03-02 12:11:01 | (NULL) | (NULL) | + +请输出每份试卷每月作答数和截止当月的作答总数。 +由示例数据结果输出如下: + +| exam_id | start_month | month_cnt | cum_exam_cnt | +| ------- | ----------- | --------- | ------------ | +| 9001 | 202001 | 2 | 2 | +| 9001 | 202002 | 1 | 3 | +| 9001 | 202003 | 3 | 6 | +| 9001 | 202005 | 1 | 7 | +| 9002 | 202001 | 1 | 1 | +| 9002 | 202002 | 3 | 4 | +| 9002 | 202003 | 1 | 5 | + +解释:试卷 9001 在 202001、202002、202003、202005 共 4 个月有被作答记录,每个月被作答数分别为 2、1、3、1,截止当月累积作答总数为 2、3、6、7。 + +**思路:** + +这题就两个关键点:统计截止当月的作答总数、输出每份试卷每月作答数和截止当月的作答总数 + +这个是关键`**sum(count(*)) over(partition by exam_id order by date_format(start_time,'%Y%m'))**` + +**答案**: + +```sql +SELECT exam_id, + date_format(start_time, '%Y%m') AS start_month, + count(*) AS month_cnt, + sum(count(*)) OVER (PARTITION BY exam_id + ORDER BY date_format(start_time, '%Y%m')) AS cum_exam_cnt +FROM exam_record +GROUP BY exam_id, + start_month +``` + +### 每月及截止当月的答题情况(较难) + +**描述**:现有试卷作答记录表 `exam_record`(`uid` 用户 ID, `exam_id` 试卷 ID, `start_time` 开始作答时间, `submit_time` 交卷时间, `score` 得分): + +| id | uid | exam_id | start_time | submit_time | score | +| --- | ---- | ------- | ------------------- | ------------------- | ------ | +| 1 | 1001 | 9001 | 2020-01-01 09:01:01 | 2020-01-01 09:21:59 | 90 | +| 2 | 1002 | 9001 | 2020-01-20 10:01:01 | 2020-01-20 10:10:01 | 89 | +| 3 | 1002 | 9001 | 2020-02-01 12:11:01 | 2020-02-01 12:31:01 | 83 | +| 4 | 1003 | 9001 | 2020-03-01 19:01:01 | 2020-03-01 19:30:01 | 75 | +| 5 | 1004 | 9001 | 2020-03-01 12:01:01 | 2020-03-01 12:11:01 | 60 | +| 6 | 1003 | 9001 | 2020-03-01 12:01:01 | 2020-03-01 12:41:01 | 90 | +| 7 | 1002 | 9001 | 2020-05-02 19:01:01 | 2020-05-02 19:32:00 | 90 | +| 8 | 1001 | 9002 | 2020-01-02 19:01:01 | 2020-01-02 19:59:01 | 69 | +| 9 | 1004 | 9002 | 2020-02-02 12:01:01 | 2020-02-02 12:20:01 | 99 | +| 10 | 1003 | 9002 | 2020-02-02 12:01:01 | 2020-02-02 12:31:01 | 68 | +| 11 | 1001 | 9002 | 2020-01-02 19:01:01 | 2020-02-02 12:43:01 | 81 | +| 12 | 1001 | 9002 | 2020-03-02 12:11:01 | (NULL) | (NULL) | + +请输出自从有用户作答记录以来,每月的试卷作答记录中月活用户数、新增用户数、截止当月的单月最大新增用户数、截止当月的累积用户数。结果按月份升序输出。 + +由示例数据结果输出如下: + +| start_month | mau | month_add_uv | max_month_add_uv | cum_sum_uv | +| ----------- | --- | ------------ | ---------------- | ---------- | +| 202001 | 2 | 2 | 2 | 2 | +| 202002 | 4 | 2 | 2 | 4 | +| 202003 | 3 | 0 | 2 | 4 | +| 202005 | 1 | 0 | 2 | 4 | + +| month | 1001 | 1002 | 1003 | 1004 | +| ------ | ---- | ---- | ---- | ---- | +| 202001 | 1 | 1 | | | +| 202002 | 1 | 1 | 1 | 1 | +| 202003 | 1 | | 1 | 1 | +| 202005 | | 1 | | | + +由上述矩阵可以看出,2020 年 1 月有 2 个用户活跃(mau=2),当月新增用户数为 2; + +2020 年 2 月有 4 个用户活跃,当月新增用户数为 2,最大单月新增用户数为 2,当前累积用户数为 4。 + +**思路:** + +难点: + +1.如何求每月新增用户 + +2.截至当月的答题情况 + +大致流程: + +(1)统计每个人的首次登陆月份 `min()` + +(2)统计每月的月活和新增用户数:先得到每个人的首次登陆月份,再对首次登陆月份分组求和是该月份的新增人数 + +(3)统计截止当月的单月最大新增用户数、截止当月的累积用户数 ,最终按照按月份升序输出 + +**答案**: + +```sql +-- 截止当月的单月最大新增用户数、截止当月的累积用户数,按月份升序输出 +SELECT + start_month, + mau, + month_add_uv, + max( month_add_uv ) over ( ORDER BY start_month ), + sum( month_add_uv ) over ( ORDER BY start_month ) +FROM + ( + -- 统计每月的月活和新增用户数 + SELECT + date_format( a.start_time, '%Y%m' ) AS start_month, + count( DISTINCT a.uid ) AS mau, + count( DISTINCT b.uid ) AS month_add_uv + FROM + exam_record a + LEFT JOIN ( + -- 统计每个人的首次登陆月份 + SELECT uid, min( date_format( start_time, '%Y%m' )) AS first_month FROM exam_record GROUP BY uid ) b ON date_format( a.start_time, '%Y%m' ) = b.first_month + GROUP BY + start_month + ) main +ORDER BY + start_month +``` + + diff --git a/docs/database/sql/sql-questions-05.md b/docs/database/sql/sql-questions-05.md new file mode 100644 index 00000000000..c20af2cad39 --- /dev/null +++ b/docs/database/sql/sql-questions-05.md @@ -0,0 +1,1013 @@ +--- +title: SQL常见面试题总结(5) +category: 数据库 +tag: + - 数据库基础 + - SQL +--- + +> 题目来源于:[牛客题霸 - SQL 进阶挑战](https://www.nowcoder.com/exam/oj?page=1&tab=SQL%E7%AF%87&topicId=240) + +较难或者困难的题目可以根据自身实际情况和面试需要来决定是否要跳过。 + +## 空值处理 + +### 统计有未完成状态的试卷的未完成数和未完成率 + +**描述**: + +现有试卷作答记录表 `exam_record`(`uid` 用户 ID, `exam_id` 试卷 ID, `start_time` 开始作答时间, `submit_time` 交卷时间, `score` 得分),数据如下: + +| id | uid | exam_id | start_time | submit_time | score | +| --- | ---- | ------- | ------------------- | ------------------- | ------ | +| 1 | 1001 | 9001 | 2020-01-02 09:01:01 | 2020-01-02 09:21:01 | 80 | +| 2 | 1001 | 9001 | 2021-05-02 10:01:01 | 2021-05-02 10:30:01 | 81 | +| 3 | 1001 | 9001 | 2021-09-02 12:01:01 | (NULL) | (NULL) | + +请统计有未完成状态的试卷的未完成数 incomplete_cnt 和未完成率 incomplete_rate。由示例数据结果输出如下: + +| exam_id | incomplete_cnt | complete_rate | +| ------- | -------------- | ------------- | +| 9001 | 1 | 0.333 | + +解释:试卷 9001 有 3 次被作答的记录,其中两次完成,1 次未完成,因此未完成数为 1,未完成率为 0.333(保留 3 位小数) + +**思路**: + +这题只需要注意一个是有条件限制,一个是没条件限制的;要么分别查询条件,然后合并;要么直接在 select 里面进行条件判断。 + +**答案**: + +写法 1: + +```sql +SELECT exam_id, + count(submit_time IS NULL OR NULL) incomplete_cnt, + ROUND(count(submit_time IS NULL OR NULL) / count(*), 3) complete_rate +FROM exam_record +GROUP BY exam_id +HAVING incomplete_cnt <> 0 +``` + +写法 2: + +```sql +SELECT exam_id, + count(submit_time IS NULL OR NULL) incomplete_cnt, + ROUND(count(submit_time IS NULL OR NULL) / count(*), 3) complete_rate +FROM exam_record +GROUP BY exam_id +HAVING incomplete_cnt <> 0 +``` + +两种写法都可以,只有中间的写法不一样,一个是对符合条件的才`COUNT`,一个是直接上`IF`,后者更为直观,最后这个`having`解释一下, 无论是 `complete_rate` 还是 `incomplete_cnt`,只要不为 0 即可,不为 0 就意味着有未完成的。 + +### 0 级用户高难度试卷的平均用时和平均得分 + +**描述**: + +现有用户信息表 `user_info`(`uid` 用户 ID,`nick_name` 昵称, `achievement` 成就值, `level` 等级, `job` 职业方向, `register_time` 注册时间),数据如下: + +| id | uid | nick_name | achievement | level | job | register_time | +| --- | ---- | --------- | ----------- | ----- | ---- | ------------------- | +| 1 | 1001 | 牛客 1 号 | 10 | 0 | 算法 | 2020-01-01 10:00:00 | +| 2 | 1002 | 牛客 2 号 | 2100 | 6 | 算法 | 2020-01-01 10:00:00 | + +试卷信息表 `examination_info`(`exam_id` 试卷 ID, `tag` 试卷类别, `difficulty` 试卷难度, `duration` 考试时长, `release_time` 发布时间),数据如下: + +| id | exam_id | tag | difficulty | duration | release_time | +| --- | ------- | ---- | ---------- | -------- | ------------------- | +| 1 | 9001 | SQL | hard | 60 | 2020-01-01 10:00:00 | +| 2 | 9002 | SQL | easy | 60 | 2020-01-01 10:00:00 | +| 3 | 9004 | 算法 | medium | 80 | 2020-01-01 10:00:00 | + +试卷作答记录表 `exam_record`(`uid` 用户 ID, `exam_id` 试卷 ID, `start_time` 开始作答时间, `submit_time` 交卷时间, `score` 得分),数据如下: + +| id | uid | exam_id | start_time | submit_time | score | +| --- | ---- | ------- | ------------------- | ------------------- | ------ | +| 1 | 1001 | 9001 | 2020-01-02 09:01:01 | 2020-01-02 09:21:59 | 80 | +| 2 | 1001 | 9001 | 2021-05-02 10:01:01 | (NULL) | (NULL) | +| 3 | 1001 | 9002 | 2021-02-02 19:01:01 | 2021-02-02 19:30:01 | 87 | +| 4 | 1001 | 9001 | 2021-06-02 19:01:01 | 2021-06-02 19:32:00 | 20 | +| 5 | 1001 | 9002 | 2021-09-05 19:01:01 | 2021-09-05 19:40:01 | 89 | +| 6 | 1001 | 9002 | 2021-09-01 12:01:01 | (NULL) | (NULL) | +| 7 | 1002 | 9002 | 2021-05-05 18:01:01 | 2021-05-05 18:59:02 | 90 | + +请输出每个 0 级用户所有的高难度试卷考试平均用时和平均得分,未完成的默认试卷最大考试时长和 0 分处理。由示例数据结果输出如下: + +| uid | avg_score | avg_time_took | +| ---- | --------- | ------------- | +| 1001 | 33 | 36.7 | + +解释:0 级用户有 1001,高难度试卷有 9001,1001 作答 9001 的记录有 3 条,分别用时 20 分钟、未完成(试卷时长 60 分钟)、30 分钟(未满 31 分钟),分别得分为 80 分、未完成(0 分处理)、20 分。因此他的平均用时为 110/3=36.7(保留一位小数),平均得分为 33 分(取整) + +**思路**:这题用`IF`是判断的最方便的,因为涉及到 NULL 值的判断。当然 `case when`也可以,大同小异。这题的难点就在于空值的处理,其他的这些查询条件什么的,我相信难不倒大家。 + +**答案**: + +```sql +SELECT UID, + round(avg(new_socre)) AS avg_score, + round(avg(time_diff), 1) AS avg_time_took +FROM + (SELECT er.uid, + IF (er.submit_time IS NOT NULL, TIMESTAMPDIFF(MINUTE, start_time, submit_time), ef.duration) AS time_diff, + IF (er.submit_time IS NOT NULL,er.score,0) AS new_socre + FROM exam_record er + LEFT JOIN user_info uf ON er.uid = uf.uid + LEFT JOIN examination_info ef ON er.exam_id = ef.exam_id + WHERE uf.LEVEL = 0 AND ef.difficulty = 'hard' ) t +GROUP BY UID +ORDER BY UID +``` + +## 高级条件语句 + +### 筛选限定昵称成就值活跃日期的用户(较难) + +**描述**: + +现有用户信息表 `user_info`(`uid` 用户 ID,`nick_name` 昵称, `achievement` 成就值, `level` 等级, `job` 职业方向, `register_time` 注册时间): + +| id | uid | nick_name | achievement | level | job | register_time | +| --- | ---- | ----------- | ----------- | ----- | ---- | ------------------- | +| 1 | 1001 | 牛客 1 号 | 1000 | 2 | 算法 | 2020-01-01 10:00:00 | +| 2 | 1002 | 牛客 2 号 | 1200 | 3 | 算法 | 2020-01-01 10:00:00 | +| 3 | 1003 | 进击的 3 号 | 2200 | 5 | 算法 | 2020-01-01 10:00:00 | +| 4 | 1004 | 牛客 4 号 | 2500 | 6 | 算法 | 2020-01-01 10:00:00 | +| 5 | 1005 | 牛客 5 号 | 3000 | 7 | C++ | 2020-01-01 10:00:00 | + +试卷作答记录表 `exam_record`(`uid` 用户 ID, `exam_id` 试卷 ID, `start_time` 开始作答时间, `submit_time` 交卷时间, `score` 得分): + +| id | uid | exam_id | start_time | submit_time | score | +| --- | ---- | ------- | ------------------- | ------------------- | ------ | +| 1 | 1001 | 9001 | 2020-01-02 09:01:01 | 2020-01-02 09:21:59 | 80 | +| 3 | 1001 | 9002 | 2021-02-02 19:01:01 | 2021-02-02 19:30:01 | 87 | +| 2 | 1001 | 9001 | 2021-05-02 10:01:01 | (NULL) | (NULL) | +| 4 | 1001 | 9001 | 2021-06-02 19:01:01 | 2021-06-02 19:32:00 | 20 | +| 6 | 1001 | 9002 | 2021-09-01 12:01:01 | (NULL) | (NULL) | +| 5 | 1001 | 9002 | 2021-09-05 19:01:01 | 2021-09-05 19:40:01 | 89 | +| 11 | 1002 | 9001 | 2020-01-01 12:01:01 | 2020-01-01 12:31:01 | 81 | +| 12 | 1002 | 9002 | 2020-02-01 12:01:01 | 2020-02-01 12:31:01 | 82 | +| 13 | 1002 | 9002 | 2020-02-02 12:11:01 | 2020-02-02 12:31:01 | 83 | +| 7 | 1002 | 9002 | 2021-05-05 18:01:01 | 2021-05-05 18:59:02 | 90 | +| 16 | 1002 | 9001 | 2021-09-06 12:01:01 | 2021-09-06 12:21:01 | 80 | +| 17 | 1002 | 9001 | 2021-09-06 12:01:01 | (NULL) | (NULL) | +| 18 | 1002 | 9001 | 2021-09-07 12:01:01 | (NULL) | (NULL) | +| 8 | 1003 | 9003 | 2021-02-06 12:01:01 | (NULL) | (NULL) | +| 9 | 1003 | 9001 | 2021-09-07 10:01:01 | 2021-09-07 10:31:01 | 89 | +| 10 | 1004 | 9002 | 2021-08-06 12:01:01 | (NULL) | (NULL) | +| 14 | 1005 | 9001 | 2021-02-01 11:01:01 | 2021-02-01 11:31:01 | 84 | +| 15 | 1006 | 9001 | 2021-02-01 11:01:01 | 2021-02-01 11:31:01 | 84 | + +题目练习记录表 `practice_record`(`uid` 用户 ID, `question_id` 题目 ID, `submit_time` 提交时间, `score` 得分): + +| id | uid | question_id | submit_time | score | +| --- | ---- | ----------- | ------------------- | ----- | +| 1 | 1001 | 8001 | 2021-08-02 11:41:01 | 60 | +| 2 | 1002 | 8001 | 2021-09-02 19:30:01 | 50 | +| 3 | 1002 | 8001 | 2021-09-02 19:20:01 | 70 | +| 4 | 1002 | 8002 | 2021-09-02 19:38:01 | 70 | +| 5 | 1003 | 8002 | 2021-09-01 19:38:01 | 80 | + +请找到昵称以『牛客』开头『号』结尾、成就值在 1200~2500 之间,且最近一次活跃(答题或作答试卷)在 2021 年 9 月的用户信息。 + +由示例数据结果输出如下: + +| uid | nick_name | achievement | +| ---- | --------- | ----------- | +| 1002 | 牛客 2 号 | 1200 | + +**解释**:昵称以『牛客』开头『号』结尾且成就值在 1200~2500 之间的有 1002、1004; + +1002 最近一次试卷区活跃为 2021 年 9 月,最近一次题目区活跃为 2021 年 9 月;1004 最近一次试卷区活跃为 2021 年 8 月,题目区未活跃。 + +因此最终满足条件的只有 1002。 + +**思路**: + +先根据条件列出主要查询语句 + +昵称以『牛客』开头『号』结尾: `nick_name LIKE "牛客%号"` + +成就值在 1200~2500 之间:`achievement BETWEEN 1200 AND 2500` + +第三个条件因为限定了为 9 月,所以直接写就行:`( date_format( record.submit_time, '%Y%m' )= 202109 OR date_format( pr.submit_time, '%Y%m' )= 202109 )` + +**答案**: + +```sql +SELECT DISTINCT u_info.uid, + u_info.nick_name, + u_info.achievement +FROM user_info u_info +LEFT JOIN exam_record record ON record.uid = u_info.uid +LEFT JOIN practice_record pr ON u_info.uid = pr.uid +WHERE u_info.nick_name LIKE "牛客%号" + AND u_info.achievement BETWEEN 1200 + AND 2500 + AND (date_format(record.submit_time, '%Y%m')= 202109 + OR date_format(pr.submit_time, '%Y%m')= 202109) +GROUP BY u_info.uid +``` + +### 筛选昵称规则和试卷规则的作答记录(较难) + +**描述**: + +现有用户信息表 `user_info`(`uid` 用户 ID,`nick_name` 昵称, `achievement` 成就值, `level` 等级, `job` 职业方向, `register_time` 注册时间): + +| id | uid | nick_name | achievement | level | job | register_time | +| --- | ---- | ------------ | ----------- | ----- | ---- | ------------------- | +| 1 | 1001 | 牛客 1 号 | 1900 | 2 | 算法 | 2020-01-01 10:00:00 | +| 2 | 1002 | 牛客 2 号 | 1200 | 3 | 算法 | 2020-01-01 10:00:00 | +| 3 | 1003 | 牛客 3 号 ♂ | 2200 | 5 | 算法 | 2020-01-01 10:00:00 | +| 4 | 1004 | 牛客 4 号 | 2500 | 6 | 算法 | 2020-01-01 10:00:00 | +| 5 | 1005 | 牛客 555 号 | 2000 | 7 | C++ | 2020-01-01 10:00:00 | +| 6 | 1006 | 666666 | 3000 | 6 | C++ | 2020-01-01 10:00:00 | + +试卷信息表 `examination_info`(`exam_id` 试卷 ID, `tag` 试卷类别, `difficulty` 试卷难度, `duration` 考试时长, `release_time` 发布时间): + +| id | exam_id | tag | difficulty | duration | release_time | +| --- | ------- | --- | ---------- | -------- | ------------------- | +| 1 | 9001 | C++ | hard | 60 | 2020-01-01 10:00:00 | +| 2 | 9002 | c# | hard | 80 | 2020-01-01 10:00:00 | +| 3 | 9003 | SQL | medium | 70 | 2020-01-01 10:00:00 | + +试卷作答记录表 `exam_record`(`uid` 用户 ID, `exam_id` 试卷 ID, `start_time` 开始作答时间, `submit_time` 交卷时间, `score` 得分): + +| id | uid | exam_id | start_time | submit_time | score | +| --- | ---- | ------- | ------------------- | ------------------- | ------ | +| 1 | 1001 | 9001 | 2020-01-02 09:01:01 | 2020-01-02 09:21:59 | 80 | +| 2 | 1001 | 9001 | 2021-05-02 10:01:01 | (NULL) | (NULL) | +| 4 | 1001 | 9001 | 2021-06-02 19:01:01 | 2021-06-02 19:32:00 | 20 | +| 3 | 1001 | 9002 | 2021-02-02 19:01:01 | 2021-02-02 19:30:01 | 87 | +| 5 | 1001 | 9002 | 2021-09-05 19:01:01 | 2021-09-05 19:40:01 | 89 | +| 6 | 1001 | 9002 | 2021-09-01 12:01:01 | (NULL) | (NULL) | +| 11 | 1002 | 9001 | 2020-01-01 12:01:01 | 2020-01-01 12:31:01 | 81 | +| 16 | 1002 | 9001 | 2021-09-06 12:01:01 | 2021-09-06 12:21:01 | 80 | +| 17 | 1002 | 9001 | 2021-09-06 12:01:01 | (NULL) | (NULL) | +| 18 | 1002 | 9001 | 2021-09-07 12:01:01 | (NULL) | (NULL) | +| 7 | 1002 | 9002 | 2021-05-05 18:01:01 | 2021-05-05 18:59:02 | 90 | +| 12 | 1002 | 9002 | 2020-02-01 12:01:01 | 2020-02-01 12:31:01 | 82 | +| 13 | 1002 | 9002 | 2020-02-02 12:11:01 | 2020-02-02 12:31:01 | 83 | +| 9 | 1003 | 9001 | 2021-09-07 10:01:01 | 2021-09-07 10:31:01 | 89 | +| 8 | 1003 | 9003 | 2021-02-06 12:01:01 | (NULL) | (NULL) | +| 10 | 1004 | 9002 | 2021-08-06 12:01:01 | (NULL) | (NULL) | +| 14 | 1005 | 9001 | 2021-02-01 11:01:01 | 2021-02-01 11:31:01 | 84 | +| 15 | 1006 | 9001 | 2021-02-01 11:01:01 | 2021-09-01 11:31:01 | 84 | + +找到昵称以"牛客"+纯数字+"号"或者纯数字组成的用户对于字母 c 开头的试卷类别(如 C,C++,c#等)的已完成的试卷 ID 和平均得分,按用户 ID、平均分升序排序。由示例数据结果输出如下: + +| uid | exam_id | avg_score | +| ---- | ------- | --------- | +| 1002 | 9001 | 81 | +| 1002 | 9002 | 85 | +| 1005 | 9001 | 84 | +| 1006 | 9001 | 84 | + +解释:昵称满足条件的用户有 1002、1004、1005、1006; + +c 开头的试卷有 9001、9002; + +满足上述条件的作答记录中,1002 完成 9001 的得分有 81、80,平均分为 81(80.5 取整四舍五入得 81); + +1002 完成 9002 的得分有 90、82、83,平均分为 85; + +**思路**: + +还是老样子,既然给出了条件,就先把各个条件先写出来 + +找到昵称以"牛客"+纯数字+"号"或者纯数字组成的用户: 我最开始是这么写的:`nick_name LIKE '牛客%号' OR nick_name REGEXP '^[0-9]+$'`,如果表中有个 “牛客 H 号” ,那也能通过。 + +所以这里还得用正则: `nick_name LIKE '^牛客[0-9]+号'` + +对于字母 c 开头的试卷类别: `e_info.tag LIKE 'c%'` 或者 `tag regexp '^c|^C'` 第一个也能匹配到大写 C + +**答案**: + +```sql +SELECT UID, + exam_id, + ROUND(AVG(score), 0) avg_score +FROM exam_record +WHERE UID IN + (SELECT UID + FROM user_info + WHERE nick_name RLIKE "^牛客[0-9]+号 $" + OR nick_name RLIKE "^[0-9]+$") + AND exam_id IN + (SELECT exam_id + FROM examination_info + WHERE tag RLIKE "^[cC]") + AND score IS NOT NULL +GROUP BY UID,exam_id +ORDER BY UID,avg_score; +``` + +### 根据指定记录是否存在输出不同情况(困难) + +**描述**: + +现有用户信息表 `user_info`(`uid` 用户 ID,`nick_name` 昵称, `achievement` 成就值, `level` 等级, `job` 职业方向, `register_time` 注册时间): + +| id | uid | nick_name | achievement | level | job | register_time | +| --- | ---- | ----------- | ----------- | ----- | ---- | ------------------- | +| 1 | 1001 | 牛客 1 号 | 19 | 0 | 算法 | 2020-01-01 10:00:00 | +| 2 | 1002 | 牛客 2 号 | 1200 | 3 | 算法 | 2020-01-01 10:00:00 | +| 3 | 1003 | 进击的 3 号 | 22 | 0 | 算法 | 2020-01-01 10:00:00 | +| 4 | 1004 | 牛客 4 号 | 25 | 0 | 算法 | 2020-01-01 10:00:00 | +| 5 | 1005 | 牛客 555 号 | 2000 | 7 | C++ | 2020-01-01 10:00:00 | +| 6 | 1006 | 666666 | 3000 | 6 | C++ | 2020-01-01 10:00:00 | + +试卷作答记录表 `exam_record`(`uid` 用户 ID, `exam_id` 试卷 ID, `start_time` 开始作答时间, `submit_time` 交卷时间, `score` 得分): + +| id | uid | exam_id | start_time | submit_time | score | +| --- | ---- | ------- | ------------------- | ------------------- | ------ | +| 1 | 1001 | 9001 | 2020-01-02 09:01:01 | 2020-01-02 09:21:59 | 80 | +| 2 | 1001 | 9001 | 2021-05-02 10:01:01 | (NULL) | (NULL) | +| 3 | 1001 | 9002 | 2021-02-02 19:01:01 | 2021-02-02 19:30:01 | 87 | +| 4 | 1001 | 9002 | 2021-09-01 12:01:01 | (NULL) | (NULL) | +| 5 | 1001 | 9003 | 2021-09-02 12:01:01 | (NULL) | (NULL) | +| 6 | 1001 | 9004 | 2021-09-03 12:01:01 | (NULL) | (NULL) | +| 7 | 1002 | 9001 | 2020-01-01 12:01:01 | 2020-01-01 12:31:01 | 99 | +| 8 | 1002 | 9003 | 2020-02-01 12:01:01 | 2020-02-01 12:31:01 | 82 | +| 9 | 1002 | 9003 | 2020-02-02 12:11:01 | (NULL) | (NULL) | +| 10 | 1002 | 9002 | 2021-05-05 18:01:01 | (NULL) | (NULL) | +| 11 | 1002 | 9001 | 2021-09-06 12:01:01 | (NULL) | (NULL) | +| 12 | 1003 | 9003 | 2021-02-06 12:01:01 | (NULL) | (NULL) | +| 13 | 1003 | 9001 | 2021-09-07 10:01:01 | 2021-09-07 10:31:01 | 89 | + +请你筛选表中的数据,当有任意一个 0 级用户未完成试卷数大于 2 时,输出每个 0 级用户的试卷未完成数和未完成率(保留 3 位小数);若不存在这样的用户,则输出所有有作答记录的用户的这两个指标。结果按未完成率升序排序。 + +由示例数据结果输出如下: + +| uid | incomplete_cnt | incomplete_rate | +| ---- | -------------- | --------------- | +| 1004 | 0 | 0.000 | +| 1003 | 1 | 0.500 | +| 1001 | 4 | 0.667 | + +**解释**:0 级用户有 1001、1003、1004;他们作答试卷数和未完成数分别为:6:4、2:1、0:0; + +存在 1001 这个 0 级用户未完成试卷数大于 2,因此输出这三个用户的未完成数和未完成率(1004 未作答过试卷,未完成率默认填 0,保留 3 位小数后是 0.000); + +结果按照未完成率升序排序。 + +附:如果 1001 不满足『未完成试卷数大于 2』,则需要输出 1001、1002、1003 的这两个指标,因为试卷作答记录表里只有这三个用户的作答记录。 + +**思路**: + +先把可能满足条件**“0 级用户未完成试卷数大于 2”**的 SQL 写出来 + +```sql +SELECT ui.uid UID +FROM user_info ui +LEFT JOIN exam_record er ON ui.uid = er.uid +WHERE ui.uid IN + (SELECT ui.uid + FROM user_info ui + LEFT JOIN exam_record er ON ui.uid = er.uid + WHERE er.submit_time IS NULL + AND ui.LEVEL = 0 ) +GROUP BY ui.uid +HAVING sum(IF(er.submit_time IS NULL, 1, 0)) > 2 +``` + +然后再分别写出两种情况的 SQL 查询语句: + +情况 1. 查询存在条件要求的 0 级用户的试卷未完成率 + +```sql +SELECT + tmp1.uid uid, + sum( + IF + ( er.submit_time IS NULL AND er.start_time IS NOT NULL, 1, 0 )) incomplete_cnt, + round( + sum( + IF + ( er.submit_time IS NULL AND er.start_time IS NOT NULL, 1, 0 ))/ count( tmp1.uid ), + 3 + ) incomplete_rate +FROM + ( + SELECT DISTINCT + ui.uid + FROM + user_info ui + LEFT JOIN exam_record er ON ui.uid = er.uid + WHERE + er.submit_time IS NULL + AND ui.LEVEL = 0 + ) tmp1 + LEFT JOIN exam_record er ON tmp1.uid = er.uid +GROUP BY + tmp1.uid +ORDER BY + incomplete_rate +``` + +情况 2. 查询不存在条件要求时所有有作答记录的 yong 用户的试卷未完成率 + +```sql +SELECT + ui.uid uid, + sum( CASE WHEN er.submit_time IS NULL AND er.start_time IS NOT NULL THEN 1 ELSE 0 END ) incomplete_cnt, + round( + sum( + IF + ( er.submit_time IS NULL AND er.start_time IS NOT NULL, 1, 0 ))/ count( ui.uid ), + 3 + ) incomplete_rate +FROM + user_info ui + JOIN exam_record er ON ui.uid = er.uid +GROUP BY + ui.uid +ORDER BY + incomplete_rate +``` + +拼在一起,就是答案 + +```sql +WITH host_user AS + (SELECT ui.uid UID + FROM user_info ui + LEFT JOIN exam_record er ON ui.uid = er.uid + WHERE ui.uid IN + (SELECT ui.uid + FROM user_info ui + LEFT JOIN exam_record er ON ui.uid = er.uid + WHERE er.submit_time IS NULL + AND ui.LEVEL = 0 ) + GROUP BY ui.uid + HAVING sum(IF (er.submit_time IS NULL, 1, 0))> 2), + tt1 AS + (SELECT tmp1.uid UID, + sum(IF (er.submit_time IS NULL + AND er.start_time IS NOT NULL, 1, 0)) incomplete_cnt, + round(sum(IF (er.submit_time IS NULL + AND er.start_time IS NOT NULL, 1, 0))/ count(tmp1.uid), 3) incomplete_rate + FROM + (SELECT DISTINCT ui.uid + FROM user_info ui + LEFT JOIN exam_record er ON ui.uid = er.uid + WHERE er.submit_time IS NULL + AND ui.LEVEL = 0 ) tmp1 + LEFT JOIN exam_record er ON tmp1.uid = er.uid + GROUP BY tmp1.uid + ORDER BY incomplete_rate), + tt2 AS + (SELECT ui.uid UID, + sum(CASE + WHEN er.submit_time IS NULL + AND er.start_time IS NOT NULL THEN 1 + ELSE 0 + END) incomplete_cnt, + round(sum(IF (er.submit_time IS NULL + AND er.start_time IS NOT NULL, 1, 0))/ count(ui.uid), 3) incomplete_rate + FROM user_info ui + JOIN exam_record er ON ui.uid = er.uid + GROUP BY ui.uid + ORDER BY incomplete_rate) + (SELECT tt1.* + FROM tt1 + LEFT JOIN + (SELECT UID + FROM host_user) t1 ON 1 = 1 + WHERE t1.uid IS NOT NULL ) +UNION ALL + (SELECT tt2.* + FROM tt2 + LEFT JOIN + (SELECT UID + FROM host_user) t2 ON 1 = 1 + WHERE t2.uid IS NULL) +``` + +V2 版本(根据上面做出的改进,答案缩短了,逻辑更强): + +```sql +SELECT + ui.uid, + SUM( + IF + ( start_time IS NOT NULL AND score IS NULL, 1, 0 )) AS incomplete_cnt,#3.试卷未完成数 + ROUND( AVG( IF ( start_time IS NOT NULL AND score IS NULL, 1, 0 )), 3 ) AS incomplete_rate #4.未完成率 + +FROM + user_info ui + LEFT JOIN exam_record USING ( uid ) +WHERE +CASE + + WHEN (#1.当有任意一个0级用户未完成试卷数大于2时 + SELECT + MAX( lv0_incom_cnt ) + FROM + ( + SELECT + SUM( + IF + ( score IS NULL, 1, 0 )) AS lv0_incom_cnt + FROM + user_info + JOIN exam_record USING ( uid ) + WHERE + LEVEL = 0 + GROUP BY + uid + ) table1 + )> 2 THEN + uid IN ( #1.1找出每个0级用户 + SELECT uid FROM user_info WHERE LEVEL = 0 ) ELSE uid IN ( #2.若不存在这样的用户,找出有作答记录的用户 + SELECT DISTINCT uid FROM exam_record ) + END + GROUP BY + ui.uid + ORDER BY + incomplete_rate #5.结果按未完成率升序排序 +``` + +### 各用户等级的不同得分表现占比(较难) + +**描述**: + +现有用户信息表 `user_info`(`uid` 用户 ID,`nick_name` 昵称, `achievement` 成就值, `level` 等级, `job` 职业方向, `register_time` 注册时间): + +| id | uid | nick_name | achievement | level | job | register_time | +| --- | ---- | ------------ | ----------- | ----- | ---- | ------------------- | +| 1 | 1001 | 牛客 1 号 | 19 | 0 | 算法 | 2020-01-01 10:00:00 | +| 2 | 1002 | 牛客 2 号 | 1200 | 3 | 算法 | 2020-01-01 10:00:00 | +| 3 | 1003 | 牛客 3 号 ♂ | 22 | 0 | 算法 | 2020-01-01 10:00:00 | +| 4 | 1004 | 牛客 4 号 | 25 | 0 | 算法 | 2020-01-01 10:00:00 | +| 5 | 1005 | 牛客 555 号 | 2000 | 7 | C++ | 2020-01-01 10:00:00 | +| 6 | 1006 | 666666 | 3000 | 6 | C++ | 2020-01-01 10:00:00 | + +试卷作答记录表 exam_record(uid 用户 ID, exam_id 试卷 ID, start_time 开始作答时间, submit_time 交卷时间, score 得分): + +| id | uid | exam_id | start_time | submit_time | score | +| --- | ---- | ------- | ------------------- | ------------------- | ------ | +| 1 | 1001 | 9001 | 2020-01-02 09:01:01 | 2020-01-02 09:21:59 | 80 | +| 2 | 1001 | 9001 | 2021-05-02 10:01:01 | (NULL) | (NULL) | +| 3 | 1001 | 9002 | 2021-02-02 19:01:01 | 2021-02-02 19:30:01 | 75 | +| 4 | 1001 | 9002 | 2021-09-01 12:01:01 | 2021-09-01 12:11:01 | 60 | +| 5 | 1001 | 9003 | 2021-09-02 12:01:01 | 2021-09-02 12:41:01 | 90 | +| 6 | 1001 | 9001 | 2021-06-02 19:01:01 | 2021-06-02 19:32:00 | 20 | +| 7 | 1001 | 9002 | 2021-09-05 19:01:01 | 2021-09-05 19:40:01 | 89 | +| 8 | 1001 | 9004 | 2021-09-03 12:01:01 | (NULL) | (NULL) | +| 9 | 1002 | 9001 | 2020-01-01 12:01:01 | 2020-01-01 12:31:01 | 99 | +| 10 | 1002 | 9003 | 2020-02-01 12:01:01 | 2020-02-01 12:31:01 | 82 | +| 11 | 1002 | 9003 | 2020-02-02 12:11:01 | 2020-02-02 12:41:01 | 76 | + +为了得到用户试卷作答的定性表现,我们将试卷得分按分界点[90,75,60]分为优良中差四个得分等级(分界点划分到左区间),请统计不同用户等级的人在完成过的试卷中各得分等级占比(结果保留 3 位小数),未完成过试卷的用户无需输出,结果按用户等级降序、占比降序排序。 + +由示例数据结果输出如下: + +| level | score_grade | ratio | +| ----- | ----------- | ----- | +| 3 | 良 | 0.667 | +| 3 | 优 | 0.333 | +| 0 | 良 | 0.500 | +| 0 | 中 | 0.167 | +| 0 | 优 | 0.167 | +| 0 | 差 | 0.167 | + +解释:完成过试卷的用户有 1001、1002;完成了的试卷对应的用户等级和分数等级如下: + +| uid | exam_id | score | level | score_grade | +| ---- | ------- | ----- | ----- | ----------- | +| 1001 | 9001 | 80 | 0 | 良 | +| 1001 | 9002 | 75 | 0 | 良 | +| 1001 | 9002 | 60 | 0 | 中 | +| 1001 | 9003 | 90 | 0 | 优 | +| 1001 | 9001 | 20 | 0 | 差 | +| 1001 | 9002 | 89 | 0 | 良 | +| 1002 | 9001 | 99 | 3 | 优 | +| 1002 | 9003 | 82 | 3 | 良 | +| 1002 | 9003 | 76 | 3 | 良 | + +因此 0 级用户(只有 1001)的各分数等级比例为:优 1/6,良 1/6,中 1/6,差 3/6;3 级用户(只有 1002)各分数等级比例为:优 1/3,良 2/3。结果保留 3 位小数。 + +**思路**: + +先把 **“将试卷得分按分界点[90,75,60]分为优良中差四个得分等级”**这个条件写出来,这里可以用到`case when` + +```sql +CASE + WHEN a.score >= 90 THEN + '优' + WHEN a.score < 90 AND a.score >= 75 THEN + '良' + WHEN a.score < 75 AND a.score >= 60 THEN + '中' ELSE '差' +END +``` + +这题的关键点就在于这,其他剩下的就是条件拼接了 + +**答案**: + +```sql +SELECT a.LEVEL, + a.score_grade, + ROUND(a.cur_count / b.total_num, 3) AS ratio +FROM + (SELECT b.LEVEL AS LEVEL, + (CASE + WHEN a.score >= 90 THEN '优' + WHEN a.score < 90 + AND a.score >= 75 THEN '良' + WHEN a.score < 75 + AND a.score >= 60 THEN '中' + ELSE '差' + END) AS score_grade, + count(1) AS cur_count + FROM exam_record a + LEFT JOIN user_info b ON a.uid = b.uid + WHERE a.submit_time IS NOT NULL + GROUP BY b.LEVEL, + score_grade) a +LEFT JOIN + (SELECT b.LEVEL AS LEVEL, + count(b.LEVEL) AS total_num + FROM exam_record a + LEFT JOIN user_info b ON a.uid = b.uid + WHERE a.submit_time IS NOT NULL + GROUP BY b.LEVEL) b ON a.LEVEL = b.LEVEL +ORDER BY a.LEVEL DESC, + ratio DESC +``` + +## 限量查询 + +### 注册时间最早的三个人 + +**描述**: + +现有用户信息表 `user_info`(`uid` 用户 ID,`nick_name` 昵称, `achievement` 成就值, `level` 等级, `job` 职业方向, `register_time` 注册时间): + +| id | uid | nick_name | achievement | level | job | register_time | +| --- | ---- | ------------ | ----------- | ----- | ---- | ------------------- | +| 1 | 1001 | 牛客 1 号 | 19 | 0 | 算法 | 2020-01-01 10:00:00 | +| 2 | 1002 | 牛客 2 号 | 1200 | 3 | 算法 | 2020-02-01 10:00:00 | +| 3 | 1003 | 牛客 3 号 ♂ | 22 | 0 | 算法 | 2020-01-02 10:00:00 | +| 4 | 1004 | 牛客 4 号 | 25 | 0 | 算法 | 2020-01-02 11:00:00 | +| 5 | 1005 | 牛客 555 号 | 4000 | 7 | C++ | 2020-01-11 10:00:00 | +| 6 | 1006 | 666666 | 3000 | 6 | C++ | 2020-11-01 10:00:00 | + +请从中找到注册时间最早的 3 个人。由示例数据结果输出如下: + +| uid | nick_name | register_time | +| ---- | ------------ | ------------------- | +| 1001 | 牛客 1 | 2020-01-01 10:00:00 | +| 1003 | 牛客 3 号 ♂ | 2020-01-02 10:00:00 | +| 1004 | 牛客 4 号 | 2020-01-02 11:00:00 | + +解释:按注册时间排序后选取前三名,输出其用户 ID、昵称、注册时间。 + +**答案**: + +```sql +SELECT uid, nick_name, register_time + FROM user_info + ORDER BY register_time + LIMIT 3 +``` + +### 注册当天就完成了试卷的名单第三页(较难) + +**描述**:现有用户信息表 `user_info`(`uid` 用户 ID,`nick_name` 昵称, `achievement` 成就值, `level` 等级, `job` 职业方向, `register_time` 注册时间): + +| id | uid | nick_name | achievement | level | job | register_time | +| --- | ---- | ------------ | ----------- | ----- | ---- | ------------------- | +| 1 | 1001 | 牛客 1 | 19 | 0 | 算法 | 2020-01-01 10:00:00 | +| 2 | 1002 | 牛客 2 号 | 1200 | 3 | 算法 | 2020-01-01 10:00:00 | +| 3 | 1003 | 牛客 3 号 ♂ | 22 | 0 | 算法 | 2020-01-01 10:00:00 | +| 4 | 1004 | 牛客 4 号 | 25 | 0 | 算法 | 2020-01-01 10:00:00 | +| 5 | 1005 | 牛客 555 号 | 4000 | 7 | 算法 | 2020-01-11 10:00:00 | +| 6 | 1006 | 牛客 6 号 | 25 | 0 | 算法 | 2020-01-02 11:00:00 | +| 7 | 1007 | 牛客 7 号 | 25 | 0 | 算法 | 2020-01-02 11:00:00 | +| 8 | 1008 | 牛客 8 号 | 25 | 0 | 算法 | 2020-01-02 11:00:00 | +| 9 | 1009 | 牛客 9 号 | 25 | 0 | 算法 | 2020-01-02 11:00:00 | +| 10 | 1010 | 牛客 10 号 | 25 | 0 | 算法 | 2020-01-02 11:00:00 | +| 11 | 1011 | 666666 | 3000 | 6 | C++ | 2020-01-02 10:00:00 | + +试卷信息表 examination_info(exam_id 试卷 ID, tag 试卷类别, difficulty 试卷难度, duration 考试时长, release_time 发布时间): + +| id | exam_id | tag | difficulty | duration | release_time | +| --- | ------- | ---- | ---------- | -------- | ------------------- | +| 1 | 9001 | 算法 | hard | 60 | 2020-01-01 10:00:00 | +| 2 | 9002 | 算法 | hard | 80 | 2020-01-01 10:00:00 | +| 3 | 9003 | SQL | medium | 70 | 2020-01-01 10:00:00 | + +试卷作答记录表 `exam_record`(`uid` 用户 ID, `exam_id` 试卷 ID, `start_time` 开始作答时间, `submit_time` 交卷时间, `score` 得分): + +| id | uid | exam_id | start_time | submit_time | score | +| --- | ---- | ------- | ------------------- | ------------------- | ----- | +| 1 | 1001 | 9001 | 2020-01-02 09:01:01 | 2020-01-02 09:21:59 | 80 | +| 2 | 1002 | 9003 | 2020-01-20 10:01:01 | 2020-01-20 10:10:01 | 81 | +| 3 | 1002 | 9002 | 2020-01-01 12:11:01 | 2020-01-01 12:31:01 | 83 | +| 4 | 1003 | 9002 | 2020-01-01 19:01:01 | 2020-01-01 19:30:01 | 75 | +| 5 | 1004 | 9002 | 2020-01-01 12:01:01 | 2020-01-01 12:11:01 | 60 | +| 6 | 1005 | 9002 | 2020-01-01 12:01:01 | 2020-01-01 12:41:01 | 90 | +| 7 | 1006 | 9001 | 2020-01-02 19:01:01 | 2020-01-02 19:32:00 | 20 | +| 8 | 1007 | 9002 | 2020-01-02 19:01:01 | 2020-01-02 19:40:01 | 89 | +| 9 | 1008 | 9003 | 2020-01-02 12:01:01 | 2020-01-02 12:20:01 | 99 | +| 10 | 1008 | 9001 | 2020-01-02 12:01:01 | 2020-01-02 12:31:01 | 98 | +| 11 | 1009 | 9002 | 2020-01-02 12:01:01 | 2020-01-02 12:31:01 | 82 | +| 12 | 1010 | 9002 | 2020-01-02 12:11:01 | 2020-01-02 12:41:01 | 76 | +| 13 | 1011 | 9001 | 2020-01-02 10:01:01 | 2020-01-02 10:31:01 | 89 | + +![](https://oss.javaguide.cn/github/javaguide/database/sql/D2B491866B85826119EE3474F10D3636.png) + +找到求职方向为算法工程师,且注册当天就完成了算法类试卷的人,按参加过的所有考试最高得分排名。排名榜很长,我们将采用分页展示,每页 3 条,现在需要你取出第 3 页(页码从 1 开始)的人的信息。 + +由示例数据结果输出如下: + +| uid | level | register_time | max_score | +| ---- | ----- | ------------------- | --------- | +| 1010 | 0 | 2020-01-02 11:00:00 | 76 | +| 1003 | 0 | 2020-01-01 10:00:00 | 75 | +| 1004 | 0 | 2020-01-01 11:00:00 | 60 | + +解释:除了 1011 其他用户的求职方向都为算法工程师;算法类试卷有 9001 和 9002,11 个用户注册当天都完成了算法类试卷;计算他们的所有考试最大分时,只有 1002 和 1008 完成了两次考试,其他人只完成了一场考试,1002 两场考试最高分为 81,1008 最高分为 99。 + +按最高分排名如下: + +| uid | level | register_time | max_score | +| ---- | ----- | ------------------- | --------- | +| 1008 | 0 | 2020-01-02 11:00:00 | 99 | +| 1005 | 7 | 2020-01-01 10:00:00 | 90 | +| 1007 | 0 | 2020-01-02 11:00:00 | 89 | +| 1002 | 3 | 2020-01-01 10:00:00 | 83 | +| 1009 | 0 | 2020-01-02 11:00:00 | 82 | +| 1001 | 0 | 2020-01-01 10:00:00 | 80 | +| 1010 | 0 | 2020-01-02 11:00:00 | 76 | +| 1003 | 0 | 2020-01-01 10:00:00 | 75 | +| 1004 | 0 | 2020-01-01 11:00:00 | 60 | +| 1006 | 0 | 2020-01-02 11:00:00 | 20 | + +每页 3 条,第三页也就是第 7~9 条,返回 1010、1003、1004 的行记录即可。 + +**思路**: + +1. 每页三条,即需要取出第三页的人的信息,要用到`limit` + +2. 统计求职方向为算法工程师且注册当天就完成了算法类试卷的人的**信息和每次记录的得分**,先求满足条件的用户,后用 left join 做连接查找信息和每次记录的得分 + +**答案**: + +```sql +SELECT t1.uid, + LEVEL, + register_time, + max(score) AS max_score +FROM exam_record t +JOIN examination_info USING (exam_id) +JOIN user_info t1 ON t.uid = t1.uid +AND date(t.submit_time) = date(t1.register_time) +WHERE job = '算法' + AND tag = '算法' +GROUP BY t1.uid, + LEVEL, + register_time +ORDER BY max_score DESC +LIMIT 6,3 +``` + +## 文本转换函数 + +### 修复串列了的记录 + +**描述**:现有试卷信息表 `examination_info`(`exam_id` 试卷 ID, `tag` 试卷类别, `difficulty` 试卷难度, `duration` 考试时长, `release_time` 发布时间): + +| id | exam_id | tag | difficulty | duration | release_time | +| --- | ------- | -------------- | ---------- | -------- | ------------------- | +| 1 | 9001 | 算法 | hard | 60 | 2021-01-01 10:00:00 | +| 2 | 9002 | 算法 | hard | 80 | 2021-01-01 10:00:00 | +| 3 | 9003 | SQL | medium | 70 | 2021-01-01 10:00:00 | +| 4 | 9004 | 算法,medium,80 | | 0 | 2021-01-01 10:00:00 | + +录题同学有一次手误将部分记录的试题类别 tag、难度、时长同时录入到了 tag 字段,请帮忙找出这些录错了的记录,并拆分后按正确的列类型输出。 + +由示例数据结果输出如下: + +| exam_id | tag | difficulty | duration | +| ------- | ---- | ---------- | -------- | +| 9004 | 算法 | medium | 80 | + +**思路**: + +先来学习下本题要用到的函数 + +`SUBSTRING_INDEX` 函数用于提取字符串中指定分隔符的部分。它接受三个参数:原始字符串、分隔符和指定要返回的部分的数量。 + +以下是 `SUBSTRING_INDEX` 函数的语法: + +```sql +SUBSTRING_INDEX(str, delimiter, count) +``` + +- `str`:要进行分割的原始字符串。 +- `delimiter`:用作分割的字符串或字符。 +- `count`:指定要返回的部分的数量。 + - 如果 `count` 大于 0,则返回从左边开始的前 `count` 个部分(以分隔符为界)。 + - 如果 `count` 小于 0,则返回从右边开始的前 `count` 个部分(以分隔符为界),即从右侧向左计数。 + +下面是一些示例,演示了 `SUBSTRING_INDEX` 函数的使用: + +1. 提取字符串中的第一个部分: + + ```sql + SELECT SUBSTRING_INDEX('apple,banana,cherry', ',', 1); + -- 输出结果:'apple' + ``` + +2. 提取字符串中的最后一个部分: + + ```sql + SELECT SUBSTRING_INDEX('apple,banana,cherry', ',', -1); + -- 输出结果:'cherry' + ``` + +3. 提取字符串中的前两个部分: + + ```sql + SELECT SUBSTRING_INDEX('apple,banana,cherry', ',', 2); + -- 输出结果:'apple,banana' + ``` + +4. 提取字符串中的最后两个部分: + + ```sql + SELECT SUBSTRING_INDEX('apple,banana,cherry', ',', -2); + -- 输出结果:'banana,cherry' + ``` + +**答案**: + +```sql +SELECT + exam_id, + substring_index( tag, ',', 1 ) tag, + substring_index( substring_index( tag, ',', 2 ), ',',- 1 ) difficulty, + substring_index( tag, ',',- 1 ) duration +FROM + examination_info +WHERE + difficulty = '' +``` + +### 对过长的昵称截取处理 + +**描述**:现有用户信息表 `user_info`(`uid` 用户 ID,`nick_name` 昵称, `achievement` 成就值, `level` 等级, `job` 职业方向, `register_time` 注册时间): + +| id | uid | nick_name | achievement | level | job | register_time | +| --- | ---- | ---------------------- | ----------- | ----- | ---- | ------------------- | +| 1 | 1001 | 牛客 1 | 19 | 0 | 算法 | 2020-01-01 10:00:00 | +| 2 | 1002 | 牛客 2 号 | 1200 | 3 | 算法 | 2020-01-01 10:00:00 | +| 3 | 1003 | 牛客 3 号 ♂ | 22 | 0 | 算法 | 2020-01-01 10:00:00 | +| 4 | 1004 | 牛客 4 号 | 25 | 0 | 算法 | 2020-01-01 11:00:00 | +| 5 | 1005 | 牛客 5678901234 号 | 4000 | 7 | 算法 | 2020-01-11 10:00:00 | +| 6 | 1006 | 牛客 67890123456789 号 | 25 | 0 | 算法 | 2020-01-02 11:00:00 | + +有的用户的昵称特别长,在一些展示场景会导致样式混乱,因此需要将特别长的昵称转换一下再输出,请输出字符数大于 10 的用户信息,对于字符数大于 13 的用户输出前 10 个字符然后加上三个点号:『...』。 + +由示例数据结果输出如下: + +| uid | nick_name | +| ---- | ------------------ | +| 1005 | 牛客 5678901234 号 | +| 1006 | 牛客 67890123... | + +解释:字符数大于 10 的用户有 1005 和 1006,长度分别为 13、17;因此需要对 1006 的昵称截断输出。 + +**思路**: + +这题涉及到字符的计算,要计算字符串的字符数(即字符串的长度),可以使用 `LENGTH` 函数或 `CHAR_LENGTH` 函数。这两个函数的区别在于对待多字节字符的方式。 + +1. `LENGTH` 函数:它返回给定字符串的字节数。对于包含多字节字符的字符串,每个字符都会被当作一个字节来计算。 + +示例: + +```sql +SELECT LENGTH('你好'); -- 输出结果:6,因为 '你好' 中的每个汉字每个占3个字节 +``` + +1. `CHAR_LENGTH` 函数:它返回给定字符串的字符数。对于包含多字节字符的字符串,每个字符会被当作一个字符来计算。 + +示例: + +```sql +SELECT CHAR_LENGTH('你好'); -- 输出结果:2,因为 '你好' 中有两个字符,即两个汉字 +``` + +**答案**: + +```sql +SELECT + uid, +CASE + + WHEN CHAR_LENGTH( nick_name ) > 13 THEN + CONCAT( SUBSTR( nick_name, 1, 10 ), '...' ) ELSE nick_name + END AS nick_name +FROM + user_info +WHERE + CHAR_LENGTH( nick_name ) > 10 +GROUP BY + uid; +``` + +### 大小写混乱时的筛选统计(较难) + +**描述**: + +现有试卷信息表 `examination_info`(`exam_id` 试卷 ID, `tag` 试卷类别, `difficulty` 试卷难度, `duration` 考试时长, `release_time` 发布时间): + +| id | exam_id | tag | difficulty | duration | release_time | +| --- | ------- | ---- | ---------- | -------- | ------------------- | +| 1 | 9001 | 算法 | hard | 60 | 2021-01-01 10:00:00 | +| 2 | 9002 | C++ | hard | 80 | 2021-01-01 10:00:00 | +| 3 | 9003 | C++ | hard | 80 | 2021-01-01 10:00:00 | +| 4 | 9004 | sql | medium | 70 | 2021-01-01 10:00:00 | +| 5 | 9005 | C++ | hard | 80 | 2021-01-01 10:00:00 | +| 6 | 9006 | C++ | hard | 80 | 2021-01-01 10:00:00 | +| 7 | 9007 | C++ | hard | 80 | 2021-01-01 10:00:00 | +| 8 | 9008 | SQL | medium | 70 | 2021-01-01 10:00:00 | +| 9 | 9009 | SQL | medium | 70 | 2021-01-01 10:00:00 | +| 10 | 9010 | SQL | medium | 70 | 2021-01-01 10:00:00 | + +试卷作答信息表 `exam_record`(`uid` 用户 ID, `exam_id` 试卷 ID, `start_time` 开始作答时间, `submit_time` 交卷时间, `score` 得分): + +| id | uid | exam_id | start_time | submit_time | score | +| --- | ---- | ------- | ------------------- | ------------------- | ------ | +| 1 | 1001 | 9001 | 2020-01-01 09:01:01 | 2020-01-01 09:21:59 | 80 | +| 2 | 1002 | 9003 | 2020-01-20 10:01:01 | 2020-01-20 10:10:01 | 81 | +| 3 | 1002 | 9002 | 2020-02-01 12:11:01 | 2020-02-01 12:31:01 | 83 | +| 4 | 1003 | 9002 | 2020-03-01 19:01:01 | 2020-03-01 19:30:01 | 75 | +| 5 | 1004 | 9002 | 2020-03-01 12:01:01 | 2020-03-01 12:11:01 | 60 | +| 6 | 1005 | 9002 | 2020-03-01 12:01:01 | 2020-03-01 12:41:01 | 90 | +| 7 | 1006 | 9001 | 2020-05-02 19:01:01 | 2020-05-02 19:32:00 | 20 | +| 8 | 1007 | 9003 | 2020-01-02 19:01:01 | 2020-01-02 19:40:01 | 89 | +| 9 | 1008 | 9004 | 2020-02-02 12:01:01 | 2020-02-02 12:20:01 | 99 | +| 10 | 1008 | 9001 | 2020-02-02 12:01:01 | 2020-02-02 12:31:01 | 98 | +| 11 | 1009 | 9002 | 2020-02-02 12:01:01 | 2020-01-02 12:43:01 | 81 | +| 12 | 1010 | 9001 | 2020-01-02 12:11:01 | (NULL) | (NULL) | +| 13 | 1010 | 9001 | 2020-02-02 12:01:01 | 2020-01-02 10:31:01 | 89 | + +试卷的类别 tag 可能出现大小写混乱的情况,请先筛选出试卷作答数小于 3 的类别 tag,统计将其转换为大写后对应的原本试卷作答数。 + +如果转换后 tag 并没有发生变化,不输出该条结果。 + +由示例数据结果输出如下: + +| tag | answer_cnt | +| --- | ---------- | +| C++ | 6 | + +解释:被作答过的试卷有 9001、9002、9003、9004,他们的 tag 和被作答次数如下: + +| exam_id | tag | answer_cnt | +| ------- | ---- | ---------- | +| 9001 | 算法 | 4 | +| 9002 | C++ | 6 | +| 9003 | c++ | 2 | +| 9004 | sql | 2 | + +作答次数小于 3 的 tag 有 c++和 sql,而转为大写后只有 C++本来就有作答数,于是输出 c++转化大写后的作答次数为 6。 + +**思路**: + +首先,这题有点混乱,9004 根据示例数据查出来只有 1 次,这里显示有 2 次。 + +先看一下大小写转换函数: + +1.`UPPER(s)`或`UCASE(s)`函数可以将字符串 s 中的字母字符全部转换成大写字母; + +2.`LOWER(s)`或者`LCASE(s)`函数可以将字符串 s 中的字母字符全部转换成小写字母。 + +难点在于相同表做连接要查询不同的值 + +**答案**: + +```sql +WITH a AS + (SELECT tag, + COUNT(start_time) AS answer_cnt + FROM exam_record er + JOIN examination_info ei ON er.exam_id = ei.exam_id + GROUP BY tag) +SELECT a.tag, + b.answer_cnt +FROM a +INNER JOIN a AS b ON UPPER(a.tag)= b.tag #a小写 b大写 +AND a.tag != b.tag +WHERE a.answer_cnt < 3; +``` + + diff --git a/docs/database/sql/sql-syntax-summary.md b/docs/database/sql/sql-syntax-summary.md index cc51b15f2a8..cff0b931495 100644 --- a/docs/database/sql/sql-syntax-summary.md +++ b/docs/database/sql/sql-syntax-summary.md @@ -28,7 +28,7 @@ SQL(Structured Query Language),标准 SQL 由 ANSI 标准委员会管理, #### SQL 语法结构 -![](https://p3-juejin.byteimg.com/tos-cn-i-k3u1fbpfcp/cb684d4c75fc430e92aaee226069c7da~tplv-k3u1fbpfcp-zoom-1.image) +![](https://oss.javaguide.cn/p3-juejin/cb684d4c75fc430e92aaee226069c7da~tplv-k3u1fbpfcp-zoom-1.png) SQL 语法结构包括: @@ -148,7 +148,7 @@ WHERE username = 'root'; ### 删除数据 - `DELETE` 语句用于删除表中的记录。 -- `TRUNCATE TABLE` 可以清空表,也就是删除所有行。 +- `TRUNCATE TABLE` 可以清空表,也就是删除所有行。说明:`TRUNCATE` 语句不属于 DML 语法而是 DDL 语法。 **删除表中的指定数据** @@ -257,11 +257,11 @@ ORDER BY cust_name DESC; **使用 WHERE 和 HAVING 过滤数据** ```sql -SELECT cust_name, COUNT(*) AS num +SELECT cust_name, COUNT(*) AS NumberOfOrders FROM Customers WHERE cust_email IS NOT NULL GROUP BY cust_name -HAVING COUNT(*) >= 1; +HAVING COUNT(*) > 1; ``` **`having` vs `where`**: @@ -322,7 +322,7 @@ WHERE cust_id IN (SELECT cust_id 内部查询首先在其父查询之前执行,以便可以将内部查询的结果传递给外部查询。执行过程可以参考下图: -![](https://p3-juejin.byteimg.com/tos-cn-i-k3u1fbpfcp/c439da1f5d4e4b00bdfa4316b933d764~tplv-k3u1fbpfcp-zoom-1.image) +![](https://oss.javaguide.cn/p3-juejin/c439da1f5d4e4b00bdfa4316b933d764~tplv-k3u1fbpfcp-zoom-1.png) ### WHERE @@ -396,7 +396,7 @@ WHERE prod_price BETWEEN 3 AND 5; **AND 示例** -```ini +```sql SELECT prod_id, prod_name, prod_price FROM products WHERE vend_id = 'DLL01' AND prod_price <= 4; @@ -500,11 +500,11 @@ SQL 允许在 `JOIN` 左边加上一些修饰性的关键词,从而形成不 下图展示了 LEFT JOIN、RIGHT JOIN、INNER JOIN、OUTER JOIN 相关的 7 种用法。 -![](https://p3-juejin.byteimg.com/tos-cn-i-k3u1fbpfcp/701670942f0f45d3a3a2187cd04a12ad~tplv-k3u1fbpfcp-zoom-1.image) +![](https://oss.javaguide.cn/p3-juejin/701670942f0f45d3a3a2187cd04a12ad~tplv-k3u1fbpfcp-zoom-1.png) -如果不加任何修饰词,只写 `JOIN`,那么默认为 `INNER JOIIN` +如果不加任何修饰词,只写 `JOIN`,那么默认为 `INNER JOIN` -对于 `INNER JOIIN` 来说,还有一种隐式的写法,称为 “**隐式内连接**”,也就是没有 `INNER JOIIN` 关键字,使用 `WHERE` 语句实现内连接的功能 +对于 `INNER JOIN` 来说,还有一种隐式的写法,称为 “**隐式内连接**”,也就是没有 `INNER JOIN` 关键字,使用 `WHERE` 语句实现内连接的功能 ```sql # 隐式内连接 @@ -556,7 +556,7 @@ SELECT column_name(s) FROM table2; | `LEFT()`、`RIGHT()` | 左边或者右边的字符 | | `LOWER()`、`UPPER()` | 转换为小写或者大写 | | `LTRIM()`、`RTRIM()` | 去除左边或者右边的空格 | -| `LENGTH()` | 长度 | +| `LENGTH()` | 长度,以字节为单位 | | `SOUNDEX()` | 转换为语音值 | 其中, **`SOUNDEX()`** 可以将一个字符串转换为描述其语音表示的字母数字模式。 @@ -728,7 +728,7 @@ DROP PRIMARY KEY; - 通过只给用户访问视图的权限,保证数据的安全性; - 更改数据格式和表示。 -![mysql视图](https://p3-juejin.byteimg.com/tos-cn-i-k3u1fbpfcp/ec4c975296ea4a7097879dac7c353878~tplv-k3u1fbpfcp-zoom-1.image) +![mysql视图](https://oss.javaguide.cn/p3-juejin/ec4c975296ea4a7097879dac7c353878~tplv-k3u1fbpfcp-zoom-1.jpeg) #### 创建视图 @@ -867,7 +867,7 @@ COMMIT; ## 权限控制 -要授予用户帐户权限,可以用`GRANT`命令。有撤销用户的权限,可以用`REVOKE`命令。这里以 MySQl 为例,介绍权限控制实际应用。 +要授予用户帐户权限,可以用`GRANT`命令。要撤销用户的权限,可以用`REVOKE`命令。这里以 MySQL 为例,介绍权限控制实际应用。 `GRANT`授予权限语法: @@ -1001,7 +1001,7 @@ SET PASSWORD FOR myuser = 'mypass'; 存储过程可以看成是对一系列 SQL 操作的批处理。存储过程可以由触发器,其他存储过程以及 Java, Python,PHP 等应用程序调用。 -![mysql存储过程](https://p3-juejin.byteimg.com/tos-cn-i-k3u1fbpfcp/60afdc9c9a594f079727ec64a2e698a3~tplv-k3u1fbpfcp-zoom-1.image) +![mysql存储过程](https://oss.javaguide.cn/p3-juejin/60afdc9c9a594f079727ec64a2e698a3~tplv-k3u1fbpfcp-zoom-1.jpeg) 使用存储过程的好处: @@ -1018,7 +1018,7 @@ SET PASSWORD FOR myuser = 'mypass'; 需要注意的是:**阿里巴巴《Java 开发手册》强制禁止使用存储过程。因为存储过程难以调试和扩展,更没有移植性。** -![](https://p3-juejin.byteimg.com/tos-cn-i-k3u1fbpfcp/93a5e011ade4450ebfa5d82057532a49~tplv-k3u1fbpfcp-zoom-1.image) +![](https://oss.javaguide.cn/p3-juejin/93a5e011ade4450ebfa5d82057532a49~tplv-k3u1fbpfcp-zoom-1.png) 至于到底要不要在项目中使用,还是要看项目实际需求,权衡好利弊即可! @@ -1127,7 +1127,7 @@ MySQL 不允许在触发器中使用 CALL 语句 ,也就是不能调用存储 > 注意:在 MySQL 中,分号 `;` 是语句结束的标识符,遇到分号表示该段语句已经结束,MySQL 可以开始执行了。因此,解释器遇到触发器执行动作中的分号后就开始执行,然后会报错,因为没有找到和 BEGIN 匹配的 END。 > -> 这时就会用到 `DELIMITER` 命令(DELIMITER 是定界符,分隔符的意思)。它是一条命令,不需要语句结束标识,语法为:`DELIMITER new_delemiter`。`new_delemiter` 可以设为 1 个或多个长度的符号,默认的是分号 `;`,我们可以把它修改为其他符号,如 `$` - `DELIMITER $` 。在这之后的语句,以分号结束,解释器不会有什么反应,只有遇到了 `$`,才认为是语句结束。注意,使用完之后,我们还应该记得把它给修改回来。 +> 这时就会用到 `DELIMITER` 命令(DELIMITER 是定界符,分隔符的意思)。它是一条命令,不需要语句结束标识,语法为:`DELIMITER new_delimiter`。`new_delimiter` 可以设为 1 个或多个长度的符号,默认的是分号 `;`,我们可以把它修改为其他符号,如 `$` - `DELIMITER $` 。在这之后的语句,以分号结束,解释器不会有什么反应,只有遇到了 `$`,才认为是语句结束。注意,使用完之后,我们还应该记得把它给修改回来。 在 MySQL 5.7.2 版之前,可以为每个表定义最多六个触发器。 @@ -1208,3 +1208,5 @@ DROP TRIGGER IF EXISTS trigger_insert_user; - [后端程序员必备:SQL 高性能优化指南!35+条优化建议立马 GET!](https://mp.weixin.qq.com/s/I-ZT3zGTNBZ6egS7T09jyQ) - [后端程序员必备:书写高质量 SQL 的 30 条建议](https://mp.weixin.qq.com/s?__biz=Mzg2OTA0Njk0OA==&mid=2247486461&idx=1&sn=60a22279196d084cc398936fe3b37772&chksm=cea24436f9d5cd20a4fa0e907590f3e700d7378b3f608d7b33bb52cfb96f503b7ccb65a1deed&token=1987003517&lang=zh_CN#rd) + + diff --git a/docs/distributed-system/api-gateway.md b/docs/distributed-system/api-gateway.md index 8ced73cafe3..e9b9f27e6dd 100644 --- a/docs/distributed-system/api-gateway.md +++ b/docs/distributed-system/api-gateway.md @@ -21,7 +21,7 @@ category: 分布式 ## 网关能提供哪些功能? -绝大部分网关可以提供下面这些功能: +绝大部分网关可以提供下面这些功能(有一些功能需要借助其他框架或者中间件): - **请求转发**:将请求转发到目标微服务。 - **负载均衡**:根据各个微服务实例的负载情况或者具体的负载均衡策略配置对请求实现动态的负载均衡。 @@ -37,10 +37,11 @@ category: 分布式 - **异常处理**:对于业务服务返回的异常响应,可以在网关层在返回给用户之前做转换处理。这样可以把一些业务侧返回的异常细节隐藏,转换成用户友好的错误提示返回。 - **API 文档:** 如果计划将 API 暴露给组织以外的开发人员,那么必须考虑使用 API 文档,例如 Swagger 或 OpenAPI。 - **协议转换**:通过协议转换整合后台基于 REST、AMQP、Dubbo 等不同风格和实现技术的微服务,面向 Web Mobile、开放平台等特定客户端提供统一服务。 +- **证书管理**:将 SSL 证书部署到 API 网关,由一个统一的入口管理接口,降低了证书更换时的复杂度。 下图来源于[百亿规模 API 网关服务 Shepherd 的设计与实现 - 美团技术团队 - 2021](https://mp.weixin.qq.com/s/iITqdIiHi3XGKq6u6FRVdg)这篇文章。 -![](https://oscimg.oschina.net/oscnet/up-35e102c633bbe8e0dea1e075ea3fee5dcfb.png) +![](https://oss.javaguide.cn/github/javaguide/distributed-system/api-gateway/up-35e102c633bbe8e0dea1e075ea3fee5dcfb.png) ## 有哪些常见的网关系统? @@ -50,7 +51,7 @@ Zuul 是 Netflix 开发的一款提供动态路由、监控、弹性、安全的 Zuul 核心架构如下: -![Zuul 核心架构](https://oss.javaguide.cn/github/javaguide/system-design/distributed-system/api-gateway/zuul-core-architecture.webp) +![Zuul 核心架构](https://oss.javaguide.cn/github/javaguide/distributed-system/api-gateway/zuul-core-architecture.webp) Zuul 主要通过过滤器(类似于 AOP)来过滤请求,从而实现网关必备的各种功能。 @@ -72,7 +73,7 @@ Zuul 主要通过过滤器(类似于 AOP)来过滤请求,从而实现网 [Zuul 1.x](https://netflixtechblog.com/announcing-zuul-edge-service-in-the-cloud-ab3af5be08ee) 基于同步 IO,性能较差。[Zuul 2.x](https://netflixtechblog.com/open-sourcing-zuul-2-82ea476cb2b3) 基于 Netty 实现了异步 IO,性能得到了大幅改进。 -![Zuul2 架构](https://oscimg.oschina.net/oscnet/up-4f9047dc9109e27f9fced1b365e2b976e9d.png) +![Zuul2 架构](https://oss.javaguide.cn/github/javaguide/distributed-system/api-gateway/zuul2-core-architecture.png) - GitHub 地址: - 官方 Wiki: @@ -92,18 +93,37 @@ Spring Cloud Gateway 和 Zuul 2.x 的差别不大,也是通过过滤器来处 - Github 地址: - 官网: +### OpenResty + +根据官方介绍: + +> OpenResty 是一个基于 Nginx 与 Lua 的高性能 Web 平台,其内部集成了大量精良的 Lua 库、第三方模块以及大多数的依赖项。用于方便地搭建能够处理超高并发、扩展性极高的动态 Web 应用、Web 服务和动态网关。 + +![OpenResty 和 Nginx 以及 Lua 的关系](https://oss.javaguide.cn/github/javaguide/system-design/distributed-system/api-gatewaynginx-lua-openresty.png) + +OpenResty 基于 Nginx,主要还是看中了其优秀的高并发能力。不过,由于 Nginx 采用 C 语言开发,二次开发门槛较高。如果想在 Nginx 上实现一些自定义的逻辑或功能,就需要编写 C 语言的模块,并重新编译 Nginx。 + +为了解决这个问题,OpenResty 通过实现 `ngx_lua` 和 `stream_lua` 等 Nginx 模块,把 Lua/LuaJIT 完美地整合进了 Nginx,从而让我们能够在 Nginx 内部里嵌入 Lua 脚本,使得可以通过简单的 Lua 语言来扩展网关的功能,比如实现自定义的路由规则、过滤器、缓存策略等。 + +> Lua 是一种非常快速的动态脚本语言,它的运行速度接近于 C 语言。LuaJIT 是 Lua 的一个即时编译器,它可以显著提高 Lua 代码的执行效率。LuaJIT 将一些常用的 Lua 函数和工具库预编译并缓存,这样在下次调用时就可以直接使用缓存的字节码,从而大大加快了执行速度。 + +关于 OpenResty 的入门以及网关安全实战推荐阅读这篇文章:[每个后端都应该了解的 OpenResty 入门以及网关安全实战](https://mp.weixin.qq.com/s/3HglZs06W95vF3tSa3KrXw)。 + +- Github 地址: +- 官网地址: + ### Kong -Kong 是一款基于 [OpenResty](https://github.com/openresty/) (Nginx + Lua)的高性能、云原生、可扩展的网关系统,主要由 3 个组件组成: +Kong 是一款基于 [OpenResty](https://github.com/openresty/) (Nginx + Lua)的高性能、云原生、可扩展、生态丰富的网关系统,主要由 3 个组件组成: - Kong Server:基于 Nginx 的服务器,用来接收 API 请求。 - Apache Cassandra/PostgreSQL:用来存储操作数据。 - Kong Dashboard:官方推荐 UI 管理工具,当然,也可以使用 RESTful 方式 管理 Admin api。 -> OpenResty 是一个基于 Nginx 与 Lua 的高性能 Web 平台,其内部集成了大量精良的 Lua 库、第三方模块以及大多数的依赖项。用于方便地搭建能够处理超高并发、扩展性极高的动态 Web 应用、Web 服务和动态网关。 - ![](https://oss.javaguide.cn/github/javaguide/system-design/distributed-system/api-gateway/kong-way.webp) +由于默认使用 Apache Cassandra/PostgreSQL 存储数据,Kong 的整个架构比较臃肿,并且会带来高可用的问题。 + Kong 提供了插件机制来扩展其功能,插件在 API 请求响应循环的生命周期中被执行。比如在服务上启用 Zipkin 插件: ```shell @@ -113,24 +133,28 @@ $ curl -X POST http://kong:8001/services/{service}/plugins \ --data "config.sample_ratio=0.001" ``` -> Kong 本身就是一个 Lua 应用程序,并且是在 Openresty 的基础之上做了一层封装的应用。归根结底就是利用 Lua 嵌入 Nginx 的方式,赋予了 Nginx 可编程的能力,这样以插件的形式在 Nginx 这一层能够做到无限想象的事情。例如限流、安全访问策略、路由、负载均衡等等。编写一个 Kong 插件,就是按照 Kong 插件编写规范,写一个自己自定义的 Lua 脚本,然后加载到 Kong 中,最后引用即可。 +Kong 本身就是一个 Lua 应用程序,并且是在 Openresty 的基础之上做了一层封装的应用。归根结底就是利用 Lua 嵌入 Nginx 的方式,赋予了 Nginx 可编程的能力,这样以插件的形式在 Nginx 这一层能够做到无限想象的事情。例如限流、安全访问策略、路由、负载均衡等等。编写一个 Kong 插件,就是按照 Kong 插件编写规范,写一个自己自定义的 Lua 脚本,然后加载到 Kong 中,最后引用即可。 ![](https://oss.javaguide.cn/github/javaguide/system-design/distributed-system/api-gateway/kong-gateway-overview.png) +除了 Lua,Kong 还可以基于 Go 、JavaScript、Python 等语言开发插件,得益于对应的 PDK(插件开发工具包)。 + +关于 Kong 插件的详细介绍,推荐阅读官方文档:,写的比较详细。 + - Github 地址: - 官网地址: ### APISIX -APISIX 是一款基于 Nginx 和 etcd 的高性能、云原生、可扩展的网关系统。 +APISIX 是一款基于 OpenResty 和 etcd 的高性能、云原生、可扩展的网关系统。 > etcd 是使用 Go 语言开发的一个开源的、高可用的分布式 key-value 存储系统,使用 Raft 协议做分布式共识。 与传统 API 网关相比,APISIX 具有动态路由和插件热加载,特别适合微服务系统下的 API 管理。并且,APISIX 与 SkyWalking(分布式链路追踪系统)、Zipkin(分布式链路追踪系统)、Prometheus(监控系统) 等 DevOps 生态工具对接都十分方便。 -![APISIX 架构图](https://oscimg.oschina.net/oscnet/up-cc6717d095705a584dd8daaaadb13c5c75b.png) +![APISIX 架构图](https://oss.javaguide.cn/github/javaguide/distributed-system/api-gateway/apisix-architecture.png) -作为 NGINX 和 Kong 的替代项目,APISIX 目前已经是 Apache 顶级开源项目,并且是最快毕业的国产开源项目。国内目前已经有很多知名企业(比如金山、有赞、爱奇艺、腾讯、贝壳)使用 APISIX 处理核心的业务流量。 +作为 Nginx 和 Kong 的替代项目,APISIX 目前已经是 Apache 顶级开源项目,并且是最快毕业的国产开源项目。国内目前已经有很多知名企业(比如金山、有赞、爱奇艺、腾讯、贝壳)使用 APISIX 处理核心的业务流量。 根据官网介绍:“APISIX 已经生产可用,功能、性能、架构全面优于 Kong”。 @@ -141,7 +165,7 @@ APISIX 同样支持定制化的插件开发。开发者除了能够使用 Lua > Wasm 是基于堆栈的虚拟机的二进制指令格式,一种低级汇编语言,旨在非常接近已编译的机器代码,并且非常接近本机性能。Wasm 最初是为浏览器构建的,但是随着技术的成熟,在服务器端看到了越来越多的用例。 -![](https://oscimg.oschina.net/oscnet/up-a240d3b113cde647f5850f4c7cc55d4ff5c.png) +![](https://oss.javaguide.cn/github/javaguide/distributed-system/api-gateway/up-a240d3b113cde647f5850f4c7cc55d4ff5c.png) - Github 地址: - 官网地址: @@ -151,21 +175,37 @@ APISIX 同样支持定制化的插件开发。开发者除了能够使用 Lua - [为什么说 Apache APISIX 是最好的 API 网关?](https://mp.weixin.qq.com/s/j8ggPGEHFu3x5ekJZyeZnA) - [有了 NGINX 和 Kong,为什么还需要 Apache APISIX](https://www.apiseven.com/zh/blog/why-we-need-Apache-APISIX) - [APISIX 技术博客](https://www.apiseven.com/zh/blog) -- [APISIX 用户案例](https://www.apiseven.com/zh/usercases) +- [APISIX 用户案例](https://www.apiseven.com/zh/usercases)(推荐) ### Shenyu Shenyu 是一款基于 WebFlux 的可扩展、高性能、响应式网关,Apache 顶级开源项目。 -![Shenyu 架构](https://oscimg.oschina.net/oscnet/up-1c2b39f22e5a0bb1730531429c4147bfbf8.png) +![Shenyu 架构](https://oss.javaguide.cn/github/javaguide/distributed-system/api-gateway/shenyu-architecture.png) Shenyu 通过插件扩展功能,插件是 ShenYu 的灵魂,并且插件也是可扩展和热插拔的。不同的插件实现不同的功能。Shenyu 自带了诸如限流、熔断、转发、重写、重定向、和路由监控等插件。 - Github 地址: - 官网地址: +## 如何选择? + +上面介绍的几个常见的网关系统,最常用的是 Spring Cloud Gateway、Kong、APISIX 这三个。 + +对于公司业务以 Java 为主要开发语言的情况下,Spring Cloud Gateway 通常是个不错的选择,其优点有:简单易用、成熟稳定、与 Spring Cloud 生态系统兼容、Spring 社区成熟等等。不过,Spring Cloud Gateway 也有一些局限性和不足之处, 一般还需要结合其他网关一起使用比如 OpenResty。并且,其性能相比较于 Kong 和 APISIX,还是差一些。如果对性能要求比较高的话,Spring Cloud Gateway 不是一个好的选择。 + +Kong 和 APISIX 功能更丰富,性能更强大,技术架构更贴合云原生。Kong 是开源 API 网关的鼻祖,生态丰富,用户群体庞大。APISIX 属于后来者,更优秀一些,根据 APISIX 官网介绍:“APISIX 已经生产可用,功能、性能、架构全面优于 Kong”。下面简单对比一下二者: + +- APISIX 基于 etcd 来做配置中心,不存在单点问题,云原生友好;而 Kong 基于 Apache Cassandra/PostgreSQL ,存在单点风险,需要额外的基础设施保障做高可用。 +- APISIX 支持热更新,并且实现了毫秒级别的热更新响应;而 Kong 不支持热更新。 +- APISIX 的性能要优于 Kong 。 +- APISIX 支持的插件更多,功能更丰富。 + ## 参考 - Kong 插件开发教程[通俗易懂]: - API 网关 Kong 实战: - Spring Cloud Gateway 原理介绍和应用: +- 微服务为什么要用到 API 网关?: + + diff --git a/docs/distributed-system/distributed-configuration-center.md b/docs/distributed-system/distributed-configuration-center.md index e10ba19d9eb..2e00aec70a3 100644 --- a/docs/distributed-system/distributed-configuration-center.md +++ b/docs/distributed-system/distributed-configuration-center.md @@ -8,3 +8,5 @@ category: 分布式 ![](https://oss.javaguide.cn/javamianshizhibei/distributed-system.png) + + diff --git a/docs/distributed-system/distributed-id-design.md b/docs/distributed-system/distributed-id-design.md index c3172737e66..5b737f34593 100644 --- a/docs/distributed-system/distributed-id-design.md +++ b/docs/distributed-system/distributed-id-design.md @@ -91,9 +91,7 @@ UA 是一个特殊字符串头,服务器依次可以识别出客户使用的 4.支持用后核销; -5.优惠券、兑换券属于广撒网的策略,所以利用率低,也就不适合使用数据。 - -**库进行存储(占空间,有效的数据有少)** +5.优惠券、兑换券属于广撒网的策略,所以利用率低,也就不适合使用数据库进行存储 **(占空间,有效的数据又少)**。 设计思路上,需要设计一种有效的兑换码生成策略,支持预先生成,支持校验,内容简洁,生成的兑换码都具有唯一性,那么这种策略就是一种特殊的编解码策略,按照约定的编解码规则支撑上述需求。 @@ -101,7 +99,7 @@ UA 是一个特殊字符串头,服务器依次可以识别出客户使用的 abcdefghijklmnopqrstuvwxyzABCDEFGHJKLMNPQRSTUVWXZY0123456789 -之前说过,兑换码要求近可能简洁,那么设计时就需要考虑兑换码的字符数,假设上限为 12 位,而字符空间有 60 位,那么可以表示的空间范围为 60^12=130606940160000000000000(也就是可以 12 位的兑换码可以生成天量,应该够运营同学挥霍了),转换成 2 进制: +之前说过,兑换码要求尽可能简洁,那么设计时就需要考虑兑换码的字符数,假设上限为 12 位,而字符空间有 60 位,那么可以表示的空间范围为 60^12=130606940160000000000000(也就是可以 12 位的兑换码可以生成天量,应该够运营同学挥霍了),转换成 2 进制: 1001000100000000101110011001101101110011000000000000000000000(61 位) @@ -171,3 +169,5 @@ span 是层的意思,比如在第一个实例算是第一层, 请求代理 - 客户的长网址: - ID 映射的短网址: (演示使用,可能无法正确打开) - 转进制后的短网址: (演示使用,可能无法正确打开) + + diff --git a/docs/distributed-system/distributed-id.md b/docs/distributed-system/distributed-id.md index c0a3de56c59..9920f8f7753 100644 --- a/docs/distributed-system/distributed-id.md +++ b/docs/distributed-system/distributed-id.md @@ -1,5 +1,5 @@ --- -title: 分布式ID常见问题总结 +title: 分布式ID介绍&实现方案总结 category: 分布式 --- @@ -9,7 +9,7 @@ category: 分布式 日常开发中,我们需要对系统中的各种数据使用 ID 唯一表示,比如用户 ID 对应且仅对应一个人,商品 ID 对应且仅对应一件商品,订单 ID 对应且仅对应一个订单。 -我们现实生活中也有各种 ID,比如身份证 ID 对应且仅对应一个人、地址 ID 对应且仅对应 +我们现实生活中也有各种 ID,比如身份证 ID 对应且仅对应一个人、地址 ID 对应且仅对应一个地址。 简单来说,**ID 就是数据的唯一标识**。 @@ -98,7 +98,7 @@ COMMIT; 如果我们可以批量获取,然后存在在内存里面,需要用到的时候,直接从内存里面拿就舒服了!这也就是我们说的 **基于数据库的号段模式来生成分布式 ID。** -数据库的号段模式也是目前比较主流的一种分布式 ID 生成方式。像滴滴开源的[Tinyid](https://github.com/didi/tinyid/wiki/tinyid%E5%8E%9F%E7%90%86%E4%BB%8B%E7%BB%8D) 就是基于这种方式来做的。不过,TinyId 使用了双号段缓存、增加多 db 支持等方式来进一步优化。 +数据库的号段模式也是目前比较主流的一种分布式 ID 生成方式。像滴滴开源的[Tinyid](https://github.com/didi/tinyid/wiki/tinyid原理介绍) 就是基于这种方式来做的。不过,TinyId 使用了双号段缓存、增加多 db 支持等方式来进一步优化。 以 MySQL 举例,我们通过下面的方式即可。 @@ -137,7 +137,7 @@ SELECT `current_max_id`, `step`,`version` FROM `sequence_id_generator` where `bi 结果: -``` +```plain id current_max_id step version biz_type 1 0 100 0 101 ``` @@ -151,7 +151,7 @@ SELECT `current_max_id`, `step`,`version` FROM `sequence_id_generator` where `bi 结果: -``` +```plain id current_max_id step version biz_type 1 100 100 1 101 ``` @@ -186,7 +186,7 @@ OK 除了高可用和并发之外,我们知道 Redis 基于内存,我们需要持久化数据,避免重启机器或者机器故障后数据丢失。Redis 支持两种不同的持久化方式:**快照(snapshotting,RDB)**、**只追加文件(append-only file, AOF)**。 并且,Redis 4.0 开始支持 **RDB 和 AOF 的混合持久化**(默认关闭,可以通过配置项 `aof-use-rdb-preamble` 开启)。 -关于 Redis 持久化,我这里就不过多介绍。不了解这部分内容的小伙伴,可以看看 [JavaGuide 对于 Redis 知识点的总结](https://snailclimb.gitee.io/javaguide/#/docs/database/Redis/redis-all)。 +关于 Redis 持久化,我这里就不过多介绍。不了解这部分内容的小伙伴,可以看看 [Redis 持久化机制详解](https://javaguide.cn/database/redis/redis-persistence.html)这篇文章。 **Redis 方案的优缺点:** @@ -228,12 +228,16 @@ UUID.randomUUID() 我们这里重点关注一下这个 Version(版本),不同的版本对应的 UUID 的生成规则是不同的。 -5 种不同的 Version(版本)值分别对应的含义(参考[维基百科对于 UUID 的介绍](https://zh.wikipedia.org/wiki/%E9%80%9A%E7%94%A8%E5%94%AF%E4%B8%80%E8%AF%86%E5%88%AB%E7%A0%81)): +8 种不同的 Version(版本)值分别对应的含义(参考[维基百科对于 UUID 的介绍](https://zh.wikipedia.org/wiki/通用唯一识别码)): -- **版本 1** : UUID 是根据时间和节点 ID(通常是 MAC 地址)生成; -- **版本 2** : UUID 是根据标识符(通常是组或用户 ID)、时间和节点 ID 生成; -- **版本 3、版本 5** : 版本 5 - 确定性 UUID 通过散列(hashing)名字空间(namespace)标识符和名称生成; -- **版本 4** : UUID 使用[随机性](https://zh.wikipedia.org/wiki/随机性)或[伪随机性](https://zh.wikipedia.org/wiki/伪随机性)生成。 +- **版本 1 (基于时间和节点 ID)** : 基于时间戳(通常是当前时间)和节点 ID(通常为设备的 MAC 地址)生成。当包含 MAC 地址时,可以保证全球唯一性,但也因此存在隐私泄露的风险。 +- **版本 2 (基于标识符、时间和节点 ID)** : 与版本 1 类似,也基于时间和节点 ID,但额外包含了本地标识符(例如用户 ID 或组 ID)。 +- **版本 3 (基于命名空间和名称的 MD5 哈希)**:使用 MD5 哈希算法,将命名空间标识符(一个 UUID)和名称字符串组合计算得到。相同的命名空间和名称总是生成相同的 UUID(**确定性生成**)。 +- **版本 4 (基于随机数)**:几乎完全基于随机数生成,通常使用伪随机数生成器(PRNG)或加密安全随机数生成器(CSPRNG)来生成。 虽然理论上存在碰撞的可能性,但理论上碰撞概率极低(2^122 的可能性),可以认为在实际应用中是唯一的。 +- **版本 5 (基于命名空间和名称的 SHA-1 哈希)**:类似于版本 3,但使用 SHA-1 哈希算法。 +- **版本 6 (基于时间戳、计数器和节点 ID)**:改进了版本 1,将时间戳放在最高有效位(Most Significant Bit,MSB),使得 UUID 可以直接按时间排序。 +- **版本 7 (基于时间戳和随机数据)**:基于 Unix 时间戳和随机数据生成。 由于时间戳位于最高有效位,因此支持按时间排序。并且,不依赖 MAC 地址或节点 ID,避免了隐私问题。 +- **版本 8 (自定义)**:允许用户根据自己的需求定义 UUID 的生成方式。其结构和内容由用户决定,提供更大的灵活性。 下面是 Version 1 版本下生成的 UUID 的示例: @@ -261,28 +265,33 @@ int version = uuid.version();// 4 最后,我们再简单分析一下 **UUID 的优缺点** (面试的时候可能会被问到的哦!) : -- **优点**:生成速度比较快、简单易用 +- **优点**:生成速度通常比较快、简单易用 - **缺点**:存储消耗空间大(32 个字符串,128 位)、 不安全(基于 MAC 地址生成 UUID 的算法会造成 MAC 地址泄露)、无序(非自增)、没有具体业务含义、需要解决重复 ID 问题(当机器时间不对的情况下,可能导致会产生重复 ID) #### Snowflake(雪花算法) Snowflake 是 Twitter 开源的分布式 ID 生成算法。Snowflake 由 64 bit 的二进制数字组成,这 64bit 的二进制被分成了几部分,每一部分存储的数据都有特定的含义: -- **第 0 位**:符号位(标识正负),始终为 0,没有用,不用管。 -- **第 1~41 位**:一共 41 位,用来表示时间戳,单位是毫秒,可以支撑 2 ^41 毫秒(约 69 年) -- **第 42~52 位**:一共 10 位,一般来说,前 5 位表示机房 ID,后 5 位表示机器 ID(实际项目中可以根据实际情况调整)。这样就可以区分不同集群/机房的节点。 -- **第 53~64 位**:一共 12 位,用来表示序列号。 序列号为自增值,代表单台机器每毫秒能够产生的最大 ID 数(2^12 = 4096),也就是说单台机器每毫秒最多可以生成 4096 个 唯一 ID。 +![Snowflake 组成](https://oss.javaguide.cn/github/javaguide/system-design/distributed-system/snowflake-distributed-id-schematic-diagram.png) -![Snowflake 示意图](https://oss.javaguide.cn/github/javaguide/system-design/distributed-system/snowflake-distributed-id-schematic-diagram.png) +- **sign(1bit)**:符号位(标识正负),始终为 0,代表生成的 ID 为正数。 +- **timestamp (41 bits)**:一共 41 位,用来表示时间戳,单位是毫秒,可以支撑 2 ^41 毫秒(约 69 年) +- **datacenter id + worker id (10 bits)**:一般来说,前 5 位表示机房 ID,后 5 位表示机器 ID(实际项目中可以根据实际情况调整)。这样就可以区分不同集群/机房的节点。 +- **sequence (12 bits)**:一共 12 位,用来表示序列号。 序列号为自增值,代表单台机器每毫秒能够产生的最大 ID 数(2^12 = 4096),也就是说单台机器每毫秒最多可以生成 4096 个 唯一 ID。 -如果你想要使用 Snowflake 算法的话,一般不需要你自己再造轮子。有很多基于 Snowflake 算法的开源实现比如美团 的 Leaf、百度的 UidGenerator,并且这些开源实现对原有的 Snowflake 算法进行了优化。 - -另外,在实际项目中,我们一般也会对 Snowflake 算法进行改造,最常见的就是在 Snowflake 算法生成的 ID 中加入业务类型信息。 +在实际项目中,我们一般也会对 Snowflake 算法进行改造,最常见的就是在 Snowflake 算法生成的 ID 中加入业务类型信息。 我们再来看看 Snowflake 算法的优缺点: - **优点**:生成速度比较快、生成的 ID 有序递增、比较灵活(可以对 Snowflake 算法进行简单的改造比如加入业务 ID) -- **缺点**:需要解决重复 ID 问题(依赖时间,当机器时间不对的情况下,可能导致会产生重复 ID)。 +- **缺点**:需要解决重复 ID 问题(ID 生成依赖时间,在获取时间的时候,可能会出现时间回拨的问题,也就是服务器上的时间突然倒退到之前的时间,进而导致会产生重复 ID)、依赖机器 ID 对分布式环境不友好(当需要自动启停或增减机器时,固定的机器 ID 可能不够灵活)。 + +如果你想要使用 Snowflake 算法的话,一般不需要你自己再造轮子。有很多基于 Snowflake 算法的开源实现比如美团 的 Leaf、百度的 UidGenerator(后面会提到),并且这些开源实现对原有的 Snowflake 算法进行了优化,性能更优秀,还解决了 Snowflake 算法的时间回拨问题和依赖机器 ID 的问题。 + +并且,Seata 还提出了“改良版雪花算法”,针对原版雪花算法进行了一定的优化改良,解决了时间回拨问题,大幅提高的 QPS。具体介绍和改进原理,可以参考下面这两篇文章: + +- [Seata 基于改良版雪花算法的分布式 UUID 生成器分析](https://seata.io/zh-cn/blog/seata-analysis-UUID-generator.html) +- [在开源项目中看到一个改良版的雪花算法,现在它是你的了。](https://www.cnblogs.com/thisiswhy/p/17611163.html) ### 开源框架 @@ -290,9 +299,14 @@ Snowflake 是 Twitter 开源的分布式 ID 生成算法。Snowflake 由 64 bit [UidGenerator](https://github.com/baidu/uid-generator) 是百度开源的一款基于 Snowflake(雪花算法)的唯一 ID 生成器。 -不过,UidGenerator 对 Snowflake(雪花算法)进行了改进,生成的唯一 ID 组成如下。 +不过,UidGenerator 对 Snowflake(雪花算法)进行了改进,生成的唯一 ID 组成如下: -![](https://oss.javaguide.cn/github/javaguide/system-design/distributed-system/uidgenerator-distributed-id-schematic-diagram.png) +![UidGenerator 生成的 ID 组成](https://oss.javaguide.cn/github/javaguide/system-design/distributed-system/uidgenerator-distributed-id-schematic-diagram.png) + +- **sign(1bit)**:符号位(标识正负),始终为 0,代表生成的 ID 为正数。 +- **delta seconds (28 bits)**:当前时间,相对于时间基点"2016-05-20"的增量值,单位:秒,最多可支持约 8.7 年 +- **worker id (22 bits)**:机器 id,最多可支持约 420w 次机器启动。内置实现为在启动时由数据库分配,默认分配策略为用后即弃,后续可提供复用策略。 +- **sequence (13 bits)**:每秒下的并发序列,13 bits 可支持每秒 8192 个并发。 可以看出,和原始 Snowflake(雪花算法)生成的唯一 ID 的组成不太一样。并且,上面这些参数我们都可以自定义。 @@ -304,15 +318,15 @@ UidGenerator 官方文档中的介绍如下: #### Leaf(美团) -**[Leaf](https://github.com/Meituan-Dianping/Leaf)** 是美团开源的一个分布式 ID 解决方案 。这个项目的名字 Leaf(树叶) 起源于德国哲学家、数学家莱布尼茨的一句话:“There are no two identical leaves in the world”(世界上没有两片相同的树叶) 。这名字起得真心挺不错的,有点文艺青年那味了! +[Leaf](https://github.com/Meituan-Dianping/Leaf) 是美团开源的一个分布式 ID 解决方案 。这个项目的名字 Leaf(树叶) 起源于德国哲学家、数学家莱布尼茨的一句话:“There are no two identical leaves in the world”(世界上没有两片相同的树叶) 。这名字起得真心挺不错的,有点文艺青年那味了! -Leaf 提供了 **号段模式** 和 **Snowflake(雪花算法)** 这两种模式来生成分布式 ID。并且,它支持双号段,还解决了雪花 ID 系统时钟回拨问题。不过,时钟问题的解决需要弱依赖于 Zookeeper 。 +Leaf 提供了 **号段模式** 和 **Snowflake(雪花算法)** 这两种模式来生成分布式 ID。并且,它支持双号段,还解决了雪花 ID 系统时钟回拨问题。不过,时钟问题的解决需要弱依赖于 Zookeeper(使用 Zookeeper 作为注册中心,通过在特定路径下读取和创建子节点来管理 workId) 。 Leaf 的诞生主要是为了解决美团各个业务线生成分布式 ID 的方法多种多样以及不可靠的问题。 Leaf 对原有的号段模式进行改进,比如它这里增加了双号段避免获取 DB 在获取号段的时候阻塞请求获取 ID 的线程。简单来说,就是我一个号段还没用完之前,我自己就主动提前去获取下一个号段(图片来自于美团官方文章:[《Leaf——美团点评分布式 ID 生成系统》](https://tech.meituan.com/2017/04/21/mt-leaf.html))。 -![](https://oscimg.oschina.net/oscnet/up-5c152efed042a8fe7e13692e0339d577f5c.png) +![](https://oss.javaguide.cn/github/javaguide/distributed-system/distributed-id/leaf-principle.png) 根据项目 README 介绍,在 4C8G VM 基础上,通过公司 RPC 方式调用,QPS 压测结果近 5w/s,TP999 1ms。 @@ -324,7 +338,7 @@ Leaf 对原有的号段模式进行改进,比如它这里增加了双号段避 为了搞清楚这个问题,我们先来看看基于数据库号段模式的简单架构方案。(图片来自于 Tinyid 的官方 wiki:[《Tinyid 原理介绍》](https://github.com/didi/tinyid/wiki/tinyid%E5%8E%9F%E7%90%86%E4%BB%8B%E7%BB%8D)) -![](https://oscimg.oschina.net/oscnet/up-4afc0e45c0c86ba5ad645d023dce11e53c2.png) +![](https://oss.javaguide.cn/github/javaguide/distributed-system/distributed-id/tinyid-principle.png) 在这种架构模式下,我们通过 HTTP 请求向发号器服务申请唯一 ID。负载均衡 router 会把我们的请求送往其中的一台 tinyid-server。 @@ -337,7 +351,7 @@ Leaf 对原有的号段模式进行改进,比如它这里增加了双号段避 Tinyid 的原理比较简单,其架构如下图所示: -![](https://oscimg.oschina.net/oscnet/up-53f74cd615178046d6c04fe50513fee74ce.png) +![](https://oss.javaguide.cn/github/javaguide/distributed-system/distributed-id/tinyid-architecture-design.png) 相比于基于数据库号段模式的简单架构方案,Tinyid 方案主要做了下面这些优化: @@ -347,10 +361,35 @@ Tinyid 的原理比较简单,其架构如下图所示: Tinyid 的优缺点这里就不分析了,结合数据库号段模式的优缺点和 Tinyid 的原理就能知道。 +#### IdGenerator(个人) + +和 UidGenerator、Leaf 一样,[IdGenerator](https://github.com/yitter/IdGenerator) 也是一款基于 Snowflake(雪花算法)的唯一 ID 生成器。 + +IdGenerator 有如下特点: + +- 生成的唯一 ID 更短; +- 兼容所有雪花算法(号段模式或经典模式,大厂或小厂); +- 原生支持 C#/Java/Go/C/Rust/Python/Node.js/PHP(C 扩展)/SQL/ 等语言,并提供多线程安全调用动态库(FFI); +- 解决了时间回拨问题,支持手工插入新 ID(当业务需要在历史时间生成新 ID 时,用本算法的预留位能生成 5000 个每秒); +- 不依赖外部存储系统; +- 默认配置下,ID 可用 71000 年不重复。 + +IdGenerator 生成的唯一 ID 组成如下: + +![IdGenerator 生成的 ID 组成](https://oss.javaguide.cn/github/javaguide/system-design/distributed-system/idgenerator-distributed-id-schematic-diagram.png) + +- **timestamp (位数不固定)**:时间差,是生成 ID 时的系统时间减去 BaseTime(基础时间,也称基点时间、原点时间、纪元时间,默认值为 2020 年) 的总时间差(毫秒单位)。初始为 5bits,随着运行时间而增加。如果觉得默认值太老,你可以重新设置,不过要注意,这个值以后最好不变。 +- **worker id (默认 6 bits)**:机器 id,机器码,最重要参数,是区分不同机器或不同应用的唯一 ID,最大值由 `WorkerIdBitLength`(默认 6)限定。如果一台服务器部署多个独立服务,需要为每个服务指定不同的 WorkerId。 +- **sequence (默认 6 bits)**:序列数,是每毫秒下的序列数,由参数中的 `SeqBitLength`(默认 6)限定。增加 `SeqBitLength` 会让性能更高,但生成的 ID 也会更长。 + +Java 语言使用示例:。 + ## 总结 通过这篇文章,我基本上已经把最常见的分布式 ID 生成方案都总结了一波。 除了上面介绍的方式之外,像 ZooKeeper 这类中间件也可以帮助我们生成唯一 ID。**没有银弹,一定要结合实际项目来选择最适合自己的方案。** -不过,本文主要介绍的是分布式 ID 的理论知识。在实际的面试中,面试官可能会结合具体的业务场景来考察你对分布式 ID 的设计,你可以参考这篇文章:[分布式 ID 设计指南](https://chat.yqcloud.top/distributed-id-design.md)(对于实际工作中分布式 ID 的设计也非常有帮助)。 +不过,本文主要介绍的是分布式 ID 的理论知识。在实际的面试中,面试官可能会结合具体的业务场景来考察你对分布式 ID 的设计,你可以参考这篇文章:[分布式 ID 设计指南](./distributed-id-design)(对于实际工作中分布式 ID 的设计也非常有帮助)。 + + diff --git a/docs/distributed-system/distributed-lock-implementations.md b/docs/distributed-system/distributed-lock-implementations.md new file mode 100644 index 00000000000..cb4504c4a7a --- /dev/null +++ b/docs/distributed-system/distributed-lock-implementations.md @@ -0,0 +1,381 @@ +--- +title: 分布式锁常见实现方案总结 +category: 分布式 +--- + + + +通常情况下,我们一般会选择基于 Redis 或者 ZooKeeper 实现分布式锁,Redis 用的要更多一点,我这里也先以 Redis 为例介绍分布式锁的实现。 + +## 基于 Redis 实现分布式锁 + +### 如何基于 Redis 实现一个最简易的分布式锁? + +不论是本地锁还是分布式锁,核心都在于“互斥”。 + +在 Redis 中, `SETNX` 命令是可以帮助我们实现互斥。`SETNX` 即 **SET** if **N**ot e**X**ists (对应 Java 中的 `setIfAbsent` 方法),如果 key 不存在的话,才会设置 key 的值。如果 key 已经存在, `SETNX` 啥也不做。 + +```bash +> SETNX lockKey uniqueValue +(integer) 1 +> SETNX lockKey uniqueValue +(integer) 0 +``` + +释放锁的话,直接通过 `DEL` 命令删除对应的 key 即可。 + +```bash +> DEL lockKey +(integer) 1 +``` + +为了防止误删到其他的锁,这里我们建议使用 Lua 脚本通过 key 对应的 value(唯一值)来判断。 + +选用 Lua 脚本是为了保证解锁操作的原子性。因为 Redis 在执行 Lua 脚本时,可以以原子性的方式执行,从而保证了锁释放操作的原子性。 + +```lua +// 释放锁时,先比较锁对应的 value 值是否相等,避免锁的误释放 +if redis.call("get",KEYS[1]) == ARGV[1] then + return redis.call("del",KEYS[1]) +else + return 0 +end +``` + +![Redis 实现简易分布式锁](https://oss.javaguide.cn/github/javaguide/distributed-system/distributed-lock/distributed-lock-setnx.png) + +这是一种最简易的 Redis 分布式锁实现,实现方式比较简单,性能也很高效。不过,这种方式实现分布式锁存在一些问题。就比如应用程序遇到一些问题比如释放锁的逻辑突然挂掉,可能会导致锁无法被释放,进而造成共享资源无法再被其他线程/进程访问。 + +### 为什么要给锁设置一个过期时间? + +为了避免锁无法被释放,我们可以想到的一个解决办法就是:**给这个 key(也就是锁) 设置一个过期时间** 。 + +```bash +127.0.0.1:6379> SET lockKey uniqueValue EX 3 NX +OK +``` + +- **lockKey**:加锁的锁名; +- **uniqueValue**:能够唯一标识锁的随机字符串; +- **NX**:只有当 lockKey 对应的 key 值不存在的时候才能 SET 成功; +- **EX**:过期时间设置(秒为单位)EX 3 标示这个锁有一个 3 秒的自动过期时间。与 EX 对应的是 PX(毫秒为单位),这两个都是过期时间设置。 + +**一定要保证设置指定 key 的值和过期时间是一个原子操作!!!** 不然的话,依然可能会出现锁无法被释放的问题。 + +这样确实可以解决问题,不过,这种解决办法同样存在漏洞:**如果操作共享资源的时间大于过期时间,就会出现锁提前过期的问题,进而导致分布式锁直接失效。如果锁的超时时间设置过长,又会影响到性能。** + +你或许在想:**如果操作共享资源的操作还未完成,锁过期时间能够自己续期就好了!** + +### 如何实现锁的优雅续期? + +对于 Java 开发的小伙伴来说,已经有了现成的解决方案:**[Redisson](https://github.com/redisson/redisson)** 。其他语言的解决方案,可以在 Redis 官方文档中找到,地址: 。 + +![Distributed locks with Redis](https://oss.javaguide.cn/github/javaguide/redis-distributed-lock.png) + +Redisson 是一个开源的 Java 语言 Redis 客户端,提供了很多开箱即用的功能,不仅仅包括多种分布式锁的实现。并且,Redisson 还支持 Redis 单机、Redis Sentinel、Redis Cluster 等多种部署架构。 + +Redisson 中的分布式锁自带自动续期机制,使用起来非常简单,原理也比较简单,其提供了一个专门用来监控和续期锁的 **Watch Dog( 看门狗)**,如果操作共享资源的线程还未执行完成的话,Watch Dog 会不断地延长锁的过期时间,进而保证锁不会因为超时而被释放。 + +![Redisson 看门狗自动续期](https://oss.javaguide.cn/github/javaguide/distributed-system/distributed-lock/distributed-lock-redisson-renew-expiration.png) + +看门狗名字的由来于 `getLockWatchdogTimeout()` 方法,这个方法返回的是看门狗给锁续期的过期时间,默认为 30 秒([redisson-3.17.6](https://github.com/redisson/redisson/releases/tag/redisson-3.17.6))。 + +```java +//默认 30秒,支持修改 +private long lockWatchdogTimeout = 30 * 1000; + +public Config setLockWatchdogTimeout(long lockWatchdogTimeout) { + this.lockWatchdogTimeout = lockWatchdogTimeout; + return this; +} +public long getLockWatchdogTimeout() { + return lockWatchdogTimeout; +} +``` + +`renewExpiration()` 方法包含了看门狗的主要逻辑: + +```java +private void renewExpiration() { + //...... + Timeout task = commandExecutor.getConnectionManager().newTimeout(new TimerTask() { + @Override + public void run(Timeout timeout) throws Exception { + //...... + // 异步续期,基于 Lua 脚本 + CompletionStage future = renewExpirationAsync(threadId); + future.whenComplete((res, e) -> { + if (e != null) { + // 无法续期 + log.error("Can't update lock " + getRawName() + " expiration", e); + EXPIRATION_RENEWAL_MAP.remove(getEntryName()); + return; + } + + if (res) { + // 递归调用实现续期 + renewExpiration(); + } else { + // 取消续期 + cancelExpirationRenewal(null); + } + }); + } + // 延迟 internalLockLeaseTime/3(默认 10s,也就是 30/3) 再调用 + }, internalLockLeaseTime / 3, TimeUnit.MILLISECONDS); + + ee.setTimeout(task); + } +``` + +默认情况下,每过 10 秒,看门狗就会执行续期操作,将锁的超时时间设置为 30 秒。看门狗续期前也会先判断是否需要执行续期操作,需要才会执行续期,否则取消续期操作。 + +Watch Dog 通过调用 `renewExpirationAsync()` 方法实现锁的异步续期: + +```java +protected CompletionStage renewExpirationAsync(long threadId) { + return evalWriteAsync(getRawName(), LongCodec.INSTANCE, RedisCommands.EVAL_BOOLEAN, + // 判断是否为持锁线程,如果是就执行续期操作,就锁的过期时间设置为 30s(默认) + "if (redis.call('hexists', KEYS[1], ARGV[2]) == 1) then " + + "redis.call('pexpire', KEYS[1], ARGV[1]); " + + "return 1; " + + "end; " + + "return 0;", + Collections.singletonList(getRawName()), + internalLockLeaseTime, getLockName(threadId)); +} +``` + +可以看出, `renewExpirationAsync` 方法其实是调用 Lua 脚本实现的续期,这样做主要是为了保证续期操作的原子性。 + +我这里以 Redisson 的分布式可重入锁 `RLock` 为例来说明如何使用 Redisson 实现分布式锁: + +```java +// 1.获取指定的分布式锁对象 +RLock lock = redisson.getLock("lock"); +// 2.拿锁且不设置锁超时时间,具备 Watch Dog 自动续期机制 +lock.lock(); +// 3.执行业务 +... +// 4.释放锁 +lock.unlock(); +``` + +只有未指定锁超时时间,才会使用到 Watch Dog 自动续期机制。 + +```java +// 手动给锁设置过期时间,不具备 Watch Dog 自动续期机制 +lock.lock(10, TimeUnit.SECONDS); +``` + +如果使用 Redis 来实现分布式锁的话,还是比较推荐直接基于 Redisson 来做的。 + +### 如何实现可重入锁? + +所谓可重入锁指的是在一个线程中可以多次获取同一把锁,比如一个线程在执行一个带锁的方法,该方法中又调用了另一个需要相同锁的方法,则该线程可以直接执行调用的方法即可重入 ,而无需重新获得锁。像 Java 中的 `synchronized` 和 `ReentrantLock` 都属于可重入锁。 + +**不可重入的分布式锁基本可以满足绝大部分业务场景了,一些特殊的场景可能会需要使用可重入的分布式锁。** + +可重入分布式锁的实现核心思路是线程在获取锁的时候判断是否为自己的锁,如果是的话,就不用再重新获取了。为此,我们可以为每个锁关联一个可重入计数器和一个占有它的线程。当可重入计数器大于 0 时,则锁被占有,需要判断占有该锁的线程和请求获取锁的线程是否为同一个。 + +实际项目中,我们不需要自己手动实现,推荐使用我们上面提到的 **Redisson** ,其内置了多种类型的锁比如可重入锁(Reentrant Lock)、自旋锁(Spin Lock)、公平锁(Fair Lock)、多重锁(MultiLock)、 红锁(RedLock)、 读写锁(ReadWriteLock)。 + +![](https://oss.javaguide.cn/github/javaguide/distributed-system/distributed-lock/redisson-readme-locks.png) + +### Redis 如何解决集群情况下分布式锁的可靠性? + +为了避免单点故障,生产环境下的 Redis 服务通常是集群化部署的。 + +Redis 集群下,上面介绍到的分布式锁的实现会存在一些问题。由于 Redis 集群数据同步到各个节点时是异步的,如果在 Redis 主节点获取到锁后,在没有同步到其他节点时,Redis 主节点宕机了,此时新的 Redis 主节点依然可以获取锁,所以多个应用服务就可以同时获取到锁。 + +![](https://oss.javaguide.cn/github/javaguide/distributed-system/distributed-lock/redis-master-slave-distributed-lock.png) + +针对这个问题,Redis 之父 antirez 设计了 [Redlock 算法](https://redis.io/topics/distlock) 来解决。 + +![](https://oss.javaguide.cn/github/javaguide/distributed-system/distributed-lock/distributed-lock-redis.io-realock.png) + +Redlock 算法的思想是让客户端向 Redis 集群中的多个独立的 Redis 实例依次请求申请加锁,如果客户端能够和半数以上的实例成功地完成加锁操作,那么我们就认为,客户端成功地获得分布式锁,否则加锁失败。 + +即使部分 Redis 节点出现问题,只要保证 Redis 集群中有半数以上的 Redis 节点可用,分布式锁服务就是正常的。 + +Redlock 是直接操作 Redis 节点的,并不是通过 Redis 集群操作的,这样才可以避免 Redis 集群主从切换导致的锁丢失问题。 + +Redlock 实现比较复杂,性能比较差,发生时钟变迁的情况下还存在安全性隐患。《数据密集型应用系统设计》一书的作者 Martin Kleppmann 曾经专门发文([How to do distributed locking - Martin Kleppmann - 2016](https://martin.kleppmann.com/2016/02/08/how-to-do-distributed-locking.html))怼过 Redlock,他认为这是一个很差的分布式锁实现。感兴趣的朋友可以看看[Redis 锁从面试连环炮聊到神仙打架](https://mp.weixin.qq.com/s?__biz=Mzg3NjU3NTkwMQ==&mid=2247505097&idx=1&sn=5c03cb769c4458350f4d4a321ad51f5a&source=41#wechat_redirect)这篇文章,有详细介绍到 antirez 和 Martin Kleppmann 关于 Redlock 的激烈辩论。 + +实际项目中不建议使用 Redlock 算法,成本和收益不成正比,可以考虑基于 Redis 主从复制+哨兵模式实现分布式锁。 + +## 基于 ZooKeeper 实现分布式锁 + +ZooKeeper 相比于 Redis 实现分布式锁,除了提供相对更高的可靠性之外,在功能层面还有一个非常有用的特性:**Watch 机制**。这个机制可以用来实现公平的分布式锁。不过,使用 ZooKeeper 实现的分布式锁在性能方面相对较差,因此如果对性能要求比较高的话,ZooKeeper 可能就不太适合了。 + +### 如何基于 ZooKeeper 实现分布式锁? + +ZooKeeper 分布式锁是基于 **临时顺序节点** 和 **Watcher(事件监听器)** 实现的。 + +获取锁: + +1. 首先我们要有一个持久节点`/locks`,客户端获取锁就是在`locks`下创建临时顺序节点。 +2. 假设客户端 1 创建了`/locks/lock1`节点,创建成功之后,会判断 `lock1`是否是 `/locks` 下最小的子节点。 +3. 如果 `lock1`是最小的子节点,则获取锁成功。否则,获取锁失败。 +4. 如果获取锁失败,则说明有其他的客户端已经成功获取锁。客户端 1 并不会不停地循环去尝试加锁,而是在前一个节点比如`/locks/lock0`上注册一个事件监听器。这个监听器的作用是当前一个节点释放锁之后通知客户端 1(避免无效自旋),这样客户端 1 就加锁成功了。 + +释放锁: + +1. 成功获取锁的客户端在执行完业务流程之后,会将对应的子节点删除。 +2. 成功获取锁的客户端在出现故障之后,对应的子节点由于是临时顺序节点,也会被自动删除,避免了锁无法被释放。 +3. 我们前面说的事件监听器其实监听的就是这个子节点删除事件,子节点删除就意味着锁被释放。 + +![](https://oss.javaguide.cn/github/javaguide/distributed-system/distributed-lock/distributed-lock-zookeeper.png) + +实际项目中,推荐使用 Curator 来实现 ZooKeeper 分布式锁。Curator 是 Netflix 公司开源的一套 ZooKeeper Java 客户端框架,相比于 ZooKeeper 自带的客户端 zookeeper 来说,Curator 的封装更加完善,各种 API 都可以比较方便地使用。 + +`Curator`主要实现了下面四种锁: + +- `InterProcessMutex`:分布式可重入排它锁 +- `InterProcessSemaphoreMutex`:分布式不可重入排它锁 +- `InterProcessReadWriteLock`:分布式读写锁 +- `InterProcessMultiLock`:将多个锁作为单个实体管理的容器,获取锁的时候获取所有锁,释放锁也会释放所有锁资源(忽略释放失败的锁)。 + +```java +CuratorFramework client = ZKUtils.getClient(); +client.start(); +// 分布式可重入排它锁 +InterProcessLock lock1 = new InterProcessMutex(client, lockPath1); +// 分布式不可重入排它锁 +InterProcessLock lock2 = new InterProcessSemaphoreMutex(client, lockPath2); +// 将多个锁作为一个整体 +InterProcessMultiLock lock = new InterProcessMultiLock(Arrays.asList(lock1, lock2)); + +if (!lock.acquire(10, TimeUnit.SECONDS)) { + throw new IllegalStateException("不能获取多锁"); +} +System.out.println("已获取多锁"); +System.out.println("是否有第一个锁: " + lock1.isAcquiredInThisProcess()); +System.out.println("是否有第二个锁: " + lock2.isAcquiredInThisProcess()); +try { + // 资源操作 + resource.use(); +} finally { + System.out.println("释放多个锁"); + lock.release(); +} +System.out.println("是否有第一个锁: " + lock1.isAcquiredInThisProcess()); +System.out.println("是否有第二个锁: " + lock2.isAcquiredInThisProcess()); +client.close(); +``` + +### 为什么要用临时顺序节点? + +每个数据节点在 ZooKeeper 中被称为 **znode**,它是 ZooKeeper 中数据的最小单元。 + +我们通常是将 znode 分为 4 大类: + +- **持久(PERSISTENT)节点**:一旦创建就一直存在即使 ZooKeeper 集群宕机,直到将其删除。 +- **临时(EPHEMERAL)节点**:临时节点的生命周期是与 **客户端会话(session)** 绑定的,**会话消失则节点消失** 。并且,**临时节点只能做叶子节点** ,不能创建子节点。 +- **持久顺序(PERSISTENT_SEQUENTIAL)节点**:除了具有持久(PERSISTENT)节点的特性之外, 子节点的名称还具有顺序性。比如 `/node1/app0000000001`、`/node1/app0000000002` 。 +- **临时顺序(EPHEMERAL_SEQUENTIAL)节点**:除了具备临时(EPHEMERAL)节点的特性之外,子节点的名称还具有顺序性。 + +可以看出,临时节点相比持久节点,最主要的是对会话失效的情况处理不一样,临时节点会话消失则对应的节点消失。这样的话,如果客户端发生异常导致没来得及释放锁也没关系,会话失效节点自动被删除,不会发生死锁的问题。 + +使用 Redis 实现分布式锁的时候,我们是通过过期时间来避免锁无法被释放导致死锁问题的,而 ZooKeeper 直接利用临时节点的特性即可。 + +假设不使用顺序节点的话,所有尝试获取锁的客户端都会对持有锁的子节点加监听器。当该锁被释放之后,势必会造成所有尝试获取锁的客户端来争夺锁,这样对性能不友好。使用顺序节点之后,只需要监听前一个节点就好了,对性能更友好。 + +### 为什么要设置对前一个节点的监听? + +> Watcher(事件监听器),是 ZooKeeper 中的一个很重要的特性。ZooKeeper 允许用户在指定节点上注册一些 Watcher,并且在一些特定事件触发的时候,ZooKeeper 服务端会将事件通知到感兴趣的客户端上去,该机制是 ZooKeeper 实现分布式协调服务的重要特性。 + +同一时间段内,可能会有很多客户端同时获取锁,但只有一个可以获取成功。如果获取锁失败,则说明有其他的客户端已经成功获取锁。获取锁失败的客户端并不会不停地循环去尝试加锁,而是在前一个节点注册一个事件监听器。 + +这个事件监听器的作用是:**当前一个节点对应的客户端释放锁之后(也就是前一个节点被删除之后,监听的是删除事件),通知获取锁失败的客户端(唤醒等待的线程,Java 中的 `wait/notifyAll` ),让它尝试去获取锁,然后就成功获取锁了。** + +### 如何实现可重入锁? + +这里以 Curator 的 `InterProcessMutex` 对可重入锁的实现来介绍(源码地址:[InterProcessMutex.java](https://github.com/apache/curator/blob/master/curator-recipes/src/main/java/org/apache/curator/framework/recipes/locks/InterProcessMutex.java))。 + +当我们调用 `InterProcessMutex#acquire`方法获取锁的时候,会调用`InterProcessMutex#internalLock`方法。 + +```java +// 获取可重入互斥锁,直到获取成功为止 +@Override +public void acquire() throws Exception { + if (!internalLock(-1, null)) { + throw new IOException("Lost connection while trying to acquire lock: " + basePath); + } +} +``` + +`internalLock` 方法会先获取当前请求锁的线程,然后从 `threadData`( `ConcurrentMap` 类型)中获取当前线程对应的 `lockData` 。 `lockData` 包含锁的信息和加锁的次数,是实现可重入锁的关键。 + +第一次获取锁的时候,`lockData`为 `null`。获取锁成功之后,会将当前线程和对应的 `lockData` 放到 `threadData` 中 + +```java +private boolean internalLock(long time, TimeUnit unit) throws Exception { + // 获取当前请求锁的线程 + Thread currentThread = Thread.currentThread(); + // 拿对应的 lockData + LockData lockData = threadData.get(currentThread); + // 第一次获取锁的话,lockData 为 null + if (lockData != null) { + // 当前线程获取过一次锁之后 + // 因为当前线程的锁存在, lockCount 自增后返回,实现锁重入. + lockData.lockCount.incrementAndGet(); + return true; + } + // 尝试获取锁 + String lockPath = internals.attemptLock(time, unit, getLockNodeBytes()); + if (lockPath != null) { + LockData newLockData = new LockData(currentThread, lockPath); + // 获取锁成功之后,将当前线程和对应的 lockData 放到 threadData 中 + threadData.put(currentThread, newLockData); + return true; + } + + return false; +} +``` + +`LockData`是 `InterProcessMutex`中的一个静态内部类。 + +```java +private final ConcurrentMap threadData = Maps.newConcurrentMap(); + +private static class LockData +{ + // 当前持有锁的线程 + final Thread owningThread; + // 锁对应的子节点 + final String lockPath; + // 加锁的次数 + final AtomicInteger lockCount = new AtomicInteger(1); + + private LockData(Thread owningThread, String lockPath) + { + this.owningThread = owningThread; + this.lockPath = lockPath; + } +} +``` + +如果已经获取过一次锁,后面再来获取锁的话,直接就会在 `if (lockData != null)` 这里被拦下了,然后就会执行`lockData.lockCount.incrementAndGet();` 将加锁次数加 1。 + +整个可重入锁的实现逻辑非常简单,直接在客户端判断当前线程有没有获取锁,有的话直接将加锁次数加 1 就可以了。 + +## 总结 + +在这篇文章中,我介绍了实现分布式锁的两种常见方式:**Redis** 和 **ZooKeeper**。至于具体选择 Redis 还是 ZooKeeper 来实现分布式锁,还是要根据业务的具体需求来决定。 + +- 如果对性能要求比较高的话,建议使用 Redis 实现分布式锁。推荐优先选择 **Redisson** 提供的现成分布式锁,而不是自己实现。实际项目中不建议使用 Redlock 算法,成本和收益不成正比,可以考虑基于 Redis 主从复制+哨兵模式实现分布式锁。 +- 如果对可靠性要求比较高,建议使用 ZooKeeper 实现分布式锁,推荐基于 **Curator** 框架来实现。不过,现在很多项目都不会用到 ZooKeeper,如果单纯是因为分布式锁而引入 ZooKeeper 的话,那是不太可取的,不建议这样做,为了一个小小的功能增加了系统的复杂度。 + +需要注意的是,无论选择哪种方式实现分布式锁,包括 Redis、ZooKeeper 或 Etcd(本文没介绍,但也经常用来实现分布式锁),都无法保证 100% 的安全性,特别是在遇到进程垃圾回收(GC)、网络延迟等异常情况下。 + +为了进一步提高系统的可靠性,建议引入一个兜底机制。例如,可以通过 **版本号(Fencing Token)机制** 来避免并发冲突。 + +最后,再分享几篇我觉得写的还不错的文章: + +- [分布式锁实现原理与最佳实践 - 阿里云开发者](https://mp.weixin.qq.com/s/JzCHpIOiFVmBoAko58ZuGw) +- [聊聊分布式锁 - 字节跳动技术团队](https://mp.weixin.qq.com/s/-N4x6EkxwAYDGdJhwvmZLw) +- [Redis、ZooKeeper、Etcd,谁有最好用的分布式锁? - 腾讯云开发者](https://mp.weixin.qq.com/s/yZC6VJGxt1ANZkn0SljZBg) + + diff --git a/docs/distributed-system/distributed-lock.md b/docs/distributed-system/distributed-lock.md index c70b4264bb3..ba53f443d03 100644 --- a/docs/distributed-system/distributed-lock.md +++ b/docs/distributed-system/distributed-lock.md @@ -1,13 +1,36 @@ --- -title: 分布式锁常见问题总结 +title: 分布式锁介绍 category: 分布式 --- + + 网上有很多分布式锁相关的文章,写了一个相对简洁易懂的版本,针对面试和工作应该够用了。 -## 分布式锁介绍 +这篇文章我们先介绍一下分布式锁的基本概念。 + +## 为什么需要分布式锁? + +在多线程环境中,如果多个线程同时访问共享资源(例如商品库存、外卖订单),会发生数据竞争,可能会导致出现脏数据或者系统问题,威胁到程序的正常运行。 + +举个例子,假设现在有 100 个用户参与某个限时秒杀活动,每位用户限购 1 件商品,且商品的数量只有 3 个。如果不对共享资源进行互斥访问,就可能出现以下情况: + +- 线程 1、2、3 等多个线程同时进入抢购方法,每一个线程对应一个用户。 +- 线程 1 查询用户已经抢购的数量,发现当前用户尚未抢购且商品库存还有 1 个,因此认为可以继续执行抢购流程。 +- 线程 2 也执行查询用户已经抢购的数量,发现当前用户尚未抢购且商品库存还有 1 个,因此认为可以继续执行抢购流程。 +- 线程 1 继续执行,将库存数量减少 1 个,然后返回成功。 +- 线程 2 继续执行,将库存数量减少 1 个,然后返回成功。 +- 此时就发生了超卖问题,导致商品被多卖了一份。 + +![共享资源未互斥访问导致出现问题](https://oss.javaguide.cn/github/javaguide/distributed-system/distributed-lock/oversold-without-locking.png) + +为了保证共享资源被安全地访问,我们需要使用互斥操作对共享资源进行保护,即同一时刻只允许一个线程访问共享资源,其他线程需要等待当前线程释放后才能访问。这样可以避免数据竞争和脏数据问题,保证程序的正确性和稳定性。 -对于单机多线程来说,在 Java 中,我们通常使用 `ReetrantLock` 类、`synchronized` 关键字这类 JDK 自带的 **本地锁** 来控制一个 JVM 进程内的多个线程对本地共享资源的访问。 +**如何才能实现共享资源的互斥访问呢?** 锁是一个比较通用的解决方案,更准确点来说是悲观锁。 + +悲观锁总是假设最坏的情况,认为共享资源每次被访问的时候就会出现问题(比如共享数据被修改),所以每次在获取资源操作的时候都会上锁,这样其他线程想拿到这个资源就会阻塞直到锁被上一个持有者释放。也就是说,**共享资源每次只给一个线程使用,其它线程阻塞,用完后再把资源转让给其它线程**。 + +对于单机多线程来说,在 Java 中,我们通常使用 `ReentrantLock` 类、`synchronized` 关键字这类 JDK 自带的 **本地锁** 来控制一个 JVM 进程内的多个线程对本地共享资源的访问。 下面是我对本地锁画的一张示意图。 @@ -25,371 +48,37 @@ category: 分布式 从图中可以看出,这些独立的进程中的线程访问共享资源是互斥的,同一时刻只有一个线程可以获取到分布式锁访问共享资源。 +## 分布式锁应该具备哪些条件? + 一个最基本的分布式锁需要满足: -- **互斥**:任意一个时刻,锁只能被一个线程持有; -- **高可用**:锁服务是高可用的。并且,即使客户端的释放锁的代码逻辑出现问题,锁最终一定还是会被释放,不会影响其他线程对共享资源的访问。 +- **互斥**:任意一个时刻,锁只能被一个线程持有。 +- **高可用**:锁服务是高可用的,当一个锁服务出现问题,能够自动切换到另外一个锁服务。并且,即使客户端的释放锁的代码逻辑出现问题,锁最终一定还是会被释放,不会影响其他线程对共享资源的访问。这一般是通过超时机制实现的。 - **可重入**:一个节点获取了锁之后,还可以再次获取锁。 -通常情况下,我们一般会选择基于 Redis 或者 ZooKeeper 实现分布式锁,Redis 用的要更多一点,我这里也以 Redis 为例介绍分布式锁的实现。 - -## 基于 Redis 实现分布式锁 - -### 如何基于 Redis 实现一个最简易的分布式锁? - -不论是本地锁还是分布式锁,核心都在于“互斥”。 - -在 Redis 中, `SETNX` 命令是可以帮助我们实现互斥。`SETNX` 即 **SET** if **N**ot e**X**ists (对应 Java 中的 `setIfAbsent` 方法),如果 key 不存在的话,才会设置 key 的值。如果 key 已经存在, `SETNX` 啥也不做。 - -```bash -> SETNX lockKey uniqueValue -(integer) 1 -> SETNX lockKey uniqueValue -(integer) 0 -``` - -释放锁的话,直接通过 `DEL` 命令删除对应的 key 即可。 - -```bash -> DEL lockKey -(integer) 1 -``` - -为了防止误删到其他的锁,这里我们建议使用 Lua 脚本通过 key 对应的 value(唯一值)来判断。 - -选用 Lua 脚本是为了保证解锁操作的原子性。因为 Redis 在执行 Lua 脚本时,可以以原子性的方式执行,从而保证了锁释放操作的原子性。 - -```lua -// 释放锁时,先比较锁对应的 value 值是否相等,避免锁的误释放 -if redis.call("get",KEYS[1]) == ARGV[1] then - return redis.call("del",KEYS[1]) -else - return 0 -end -``` - -![Redis 实现简易分布式锁](https://oss.javaguide.cn/github/javaguide/distributed-system/distributed-lock/distributed-lock-setnx.png) - -这是一种最简易的 Redis 分布式锁实现,实现方式比较简单,性能也很高效。不过,这种方式实现分布式锁存在一些问题。就比如应用程序遇到一些问题比如释放锁的逻辑突然挂掉,可能会导致锁无法被释放,进而造成共享资源无法再被其他线程/进程访问。 - -### 为什么要给锁设置一个过期时间? - -为了避免锁无法被释放,我们可以想到的一个解决办法就是:**给这个 key(也就是锁) 设置一个过期时间** 。 - -```bash -127.0.0.1:6379> SET lockKey uniqueValue EX 3 NX -OK -``` - -- **lockKey**:加锁的锁名; -- **uniqueValue**:能够唯一标示锁的随机字符串; -- **NX**:只有当 lockKey 对应的 key 值不存在的时候才能 SET 成功; -- **EX**:过期时间设置(秒为单位)EX 3 标示这个锁有一个 3 秒的自动过期时间。与 EX 对应的是 PX(毫秒为单位),这两个都是过期时间设置。 - -**一定要保证设置指定 key 的值和过期时间是一个原子操作!!!** 不然的话,依然可能会出现锁无法被释放的问题。 - -这样确实可以解决问题,不过,这种解决办法同样存在漏洞:**如果操作共享资源的时间大于过期时间,就会出现锁提前过期的问题,进而导致分布式锁直接失效。如果锁的超时时间设置过长,又会影响到性能。** - -你或许在想:**如果操作共享资源的操作还未完成,锁过期时间能够自己续期就好了!** - -### 如何实现锁的优雅续期? - -对于 Java 开发的小伙伴来说,已经有了现成的解决方案:**[Redisson](https://github.com/redisson/redisson)** 。其他语言的解决方案,可以在 Redis 官方文档中找到,地址: 。 - -![Distributed locks with Redis](https://oss.javaguide.cn/github/javaguide/redis-distributed-lock.png) - -Redisson 是一个开源的 Java 语言 Redis 客户端,提供了很多开箱即用的功能,不仅仅包括多种分布式锁的实现。并且,Redisson 还支持 Redis 单机、Redis Sentinel、Redis Cluster 等多种部署架构。 - -Redisson 中的分布式锁自带自动续期机制,使用起来非常简单,原理也比较简单,其提供了一个专门用来监控和续期锁的 **Watch Dog( 看门狗)**,如果操作共享资源的线程还未执行完成的话,Watch Dog 会不断地延长锁的过期时间,进而保证锁不会因为超时而被释放。 - -![Redisson 看门狗自动续期](https://oss.javaguide.cn/github/javaguide/distributed-system/distributed-lock/distributed-lock-redisson-renew-expiration.png) - -看门狗名字的由来于 `getLockWatchdogTimeout()` 方法,这个方法返回的是看门狗给锁续期的过期时间,默认为 30 秒([redisson-3.17.6](https://github.com/redisson/redisson/releases/tag/redisson-3.17.6))。 - -```java -//默认 30秒,支持修改 -private long lockWatchdogTimeout = 30 * 1000; - -public Config setLockWatchdogTimeout(long lockWatchdogTimeout) { - this.lockWatchdogTimeout = lockWatchdogTimeout; - return this; -} -public long getLockWatchdogTimeout() { - return lockWatchdogTimeout; -} -``` - -`renewExpiration()` 方法包含了看门狗的主要逻辑: - -```java -private void renewExpiration() { - //...... - Timeout task = commandExecutor.getConnectionManager().newTimeout(new TimerTask() { - @Override - public void run(Timeout timeout) throws Exception { - //...... - // 异步续期,基于 Lua 脚本 - CompletionStage future = renewExpirationAsync(threadId); - future.whenComplete((res, e) -> { - if (e != null) { - // 无法续期 - log.error("Can't update lock " + getRawName() + " expiration", e); - EXPIRATION_RENEWAL_MAP.remove(getEntryName()); - return; - } - - if (res) { - // 递归调用实现续期 - renewExpiration(); - } else { - // 取消续期 - cancelExpirationRenewal(null); - } - }); - } - // 延迟 internalLockLeaseTime/3(默认 10s,也就是 30/3) 再调用 - }, internalLockLeaseTime / 3, TimeUnit.MILLISECONDS); - - ee.setTimeout(task); - } -``` - -默认情况下,每过 10 秒,看门狗就会执行续期操作,将锁的超时时间设置为 30 秒。看门狗续期前也会先判断是否需要执行续期操作,需要才会执行续期,否则取消续期操作。 - -Watch Dog 通过调用 `renewExpirationAsync()` 方法实现锁的异步续期: - -```java -protected CompletionStage renewExpirationAsync(long threadId) { - return evalWriteAsync(getRawName(), LongCodec.INSTANCE, RedisCommands.EVAL_BOOLEAN, - // 判断是否为持锁线程,如果是就执行续期操作,就锁的过期时间设置为 30s(默认) - "if (redis.call('hexists', KEYS[1], ARGV[2]) == 1) then " + - "redis.call('pexpire', KEYS[1], ARGV[1]); " + - "return 1; " + - "end; " + - "return 0;", - Collections.singletonList(getRawName()), - internalLockLeaseTime, getLockName(threadId)); -} -``` - -可以看出, `renewExpirationAsync` 方法其实是调用 Lua 脚本实现的续期,这样做主要是为了保证续期操作的原子性。 - -我这里以 Redisson 的分布式可重入锁 `RLock` 为例来说明如何使用 Redisson 实现分布式锁: - -```java -// 1.获取指定的分布式锁对象 -RLock lock = redisson.getLock("lock"); -// 2.拿锁且不设置锁超时时间,具备 Watch Dog 自动续期机制 -lock.lock(); -// 3.执行业务 -... -// 4.释放锁 -lock.unlock(); -``` - -只有未指定锁超时时间,才会使用到 Watch Dog 自动续期机制。 +除了上面这三个基本条件之外,一个好的分布式锁还需要满足下面这些条件: -```java -// 手动给锁设置过期时间,不具备 Watch Dog 自动续期机制 -lock.lock(10, TimeUnit.SECONDS); -``` +- **高性能**:获取和释放锁的操作应该快速完成,并且不应该对整个系统的性能造成过大影响。 +- **非阻塞**:如果获取不到锁,不能无限期等待,避免对系统正常运行造成影响。 -如果使用 Redis 来实现分布式锁的话,还是比较推荐直接基于 Redisson 来做的。 +## 分布式锁的常见实现方式有哪些? -### 如何实现可重入锁? +常见分布式锁实现方案如下: -所谓可重入锁指的是在一个线程中可以多次获取同一把锁,比如一个线程在执行一个带锁的方法,该方法中又调用了另一个需要相同锁的方法,则该线程可以直接执行调用的方法即可重入 ,而无需重新获得锁。像 Java 中的 `synchronized` 和 `ReentrantLock` 都属于可重入锁。 +- 基于关系型数据库比如 MySQL 实现分布式锁。 +- 基于分布式协调服务 ZooKeeper 实现分布式锁。 +- 基于分布式键值存储系统比如 Redis 、Etcd 实现分布式锁。 -**不可重入的分布式锁基本可以满足绝大部分业务场景了,一些特殊的场景可能会需要使用可重入的分布式锁。** +关系型数据库的方式一般是通过唯一索引或者排他锁实现。不过,一般不会使用这种方式,问题太多比如性能太差、不具备锁失效机制。 -可重入分布式锁的实现核心思路是线程在获取锁的时候判断是否为自己的锁,如果是的话,就不用再重新获取了。为此,我们可以为每个锁关联一个可重入计数器和一个占有它的线程。当可重入计数器大于 0 时,则锁被占有,需要判断占有该锁的线程和请求获取锁的线程是否为同一个。 +基于 ZooKeeper 或者 Redis 实现分布式锁这两种实现方式要用的更多一些,我专门写了一篇文章来详细介绍这两种方案:[分布式锁常见实现方案总结](./distributed-lock-implementations.md)。 -实际项目中,我们不需要自己手动实现,推荐使用我们上面提到的 **Redisson** ,其内置了多种类型的锁比如可重入锁(Reentrant Lock)、自旋锁(Spin Lock)、公平锁(Fair Lock)、多重锁(MultiLock)、 红锁(RedLock)、 读写锁(ReadWriteLock)。 - -![](https://oss.javaguide.cn/github/javaguide/distributed-system/distributed-lock/redisson-readme-locks.png) - -### Redis 如何解决集群情况下分布式锁的可靠性? - -为了避免单点故障,生产环境下的 Redis 服务通常是集群化部署的。 - -Redis 集群下,上面介绍到的分布式锁的实现会存在一些问题。由于 Redis 集群数据同步到各个节点时是异步的,如果在 Redis 主节点获取到锁后,在没有同步到其他节点时,Redis 主节点宕机了,此时新的 Redis 主节点依然可以获取锁,所以多个应用服务就可以同时获取到锁。 - -![](https://oss.javaguide.cn/github/javaguide/distributed-system/distributed-lock/redis-master-slave-distributed-lock.png) - -针对这个问题,Redis 之父 antirez 设计了 [Redlock 算法](https://redis.io/topics/distlock) 来解决。 - -![](https://oss.javaguide.cn/github/javaguide/distributed-system/distributed-lock/distributed-lock-redis.io-realock.png) - -Redlock 算法的思想是让客户端向 Redis 集群中的多个独立的 Redis 实例依次请求申请加锁,如果客户端能够和半数以上的实例成功地完成加锁操作,那么我们就认为,客户端成功地获得分布式锁,否则加锁失败。 - -即使部分 Redis 节点出现问题,只要保证 Redis 集群中有半数以上的 Redis 节点可用,分布式锁服务就是正常的。 - -Redlock 是直接操作 Redis 节点的,并不是通过 Redis 集群操作的,这样才可以避免 Redis 集群主从切换导致的锁丢失问题。 - -Redlock 实现比较复杂,性能比较差,发生时钟变迁的情况下还存在安全性隐患。《数据密集型应用系统设计》一书的作者 Martin Kleppmann 曾经专门发文([How to do distributed locking - Martin Kleppmann - 2016](https://martin.kleppmann.com/2016/02/08/how-to-do-distributed-locking.html))怼过 Redlock,他认为这是一个很差的分布式锁实现。感兴趣的朋友可以看看[Redis 锁从面试连环炮聊到神仙打架](https://mp.weixin.qq.com/s?__biz=Mzg3NjU3NTkwMQ==&mid=2247505097&idx=1&sn=5c03cb769c4458350f4d4a321ad51f5a&source=41#wechat_redirect)这篇文章,有详细介绍到 antirez 和 Martin Kleppmann 关于 Redlock 的激烈辩论。 - -实际项目中不建议使用 Redlock 算法,成本和收益不成正比。 - -如果不是非要实现绝对可靠的分布式锁的话,其实单机版 Redis 就完全够了,实现简单,性能也非常高。如果你必须要实现一个绝对可靠的分布式锁的话,可以基于 ZooKeeper 来做,只是性能会差一些。 - -## 基于 ZooKeeper 实现分布式锁 - -Redis 实现分布式锁性能较高,ZooKeeper 实现分布式锁可靠性更高。实际项目中,我们应该根据业务的具体需求来选择。 - -### 如何基于 ZooKeeper 实现分布式锁? - -ZooKeeper 分布式锁是基于 **临时顺序节点** 和 **Watcher(事件监听器)** 实现的。 - -获取锁: - -1. 首先我们要有一个持久节点`/locks`,客户端获取锁就是在`locks`下创建临时顺序节点。 -2. 假设客户端 1 创建了`/locks/lock1`节点,创建成功之后,会判断 `lock1`是否是 `/locks` 下最小的子节点。 -3. 如果 `lock1`是最小的子节点,则获取锁成功。否则,获取锁失败。 -4. 如果获取锁失败,则说明有其他的客户端已经成功获取锁。客户端 1 并不会不停地循环去尝试加锁,而是在前一个节点比如`/locks/lock0`上注册一个事件监听器。这个监听器的作用是当前一个节点释放锁之后通知客户端 1(避免无效自旋),这样客户端 1 就加锁成功了。 - -释放锁: - -1. 成功获取锁的客户端在执行完业务流程之后,会将对应的子节点删除。 -2. 成功获取锁的客户端在出现故障之后,对应的子节点由于是临时顺序节点,也会被自动删除,避免了锁无法被释放。 -3. 我们前面说的事件监听器其实监听的就是这个子节点删除事件,子节点删除就意味着锁被释放。 - -![](https://oss.javaguide.cn/github/javaguide/distributed-system/distributed-lock/distributed-lock-zookeeper.png) - -实际项目中,推荐使用 Curator 来实现 ZooKeeper 分布式锁。Curator 是 Netflix 公司开源的一套 ZooKeeper Java 客户端框架,相比于 ZooKeeper 自带的客户端 zookeeper 来说,Curator 的封装更加完善,各种 API 都可以比较方便地使用。 - -`Curator`主要实现了下面四种锁: - -- `InterProcessMutex`:分布式可重入排它锁 -- `InterProcessSemaphoreMutex`:分布式不可重入排它锁 -- `InterProcessReadWriteLock`:分布式读写锁 -- `InterProcessMultiLock`:将多个锁作为单个实体管理的容器,获取锁的时候获取所有锁,释放锁也会释放所有锁资源(忽略释放失败的锁)。 - -```java -CuratorFramework client = ZKUtils.getClient(); -client.start(); -// 分布式可重入排它锁 -InterProcessLock lock1 = new InterProcessMutex(client, lockPath1); -// 分布式不可重入排它锁 -InterProcessLock lock2 = new InterProcessSemaphoreMutex(client, lockPath2); -// 将多个锁作为一个整体 -InterProcessMultiLock lock = new InterProcessMultiLock(Arrays.asList(lock1, lock2)); - -if (!lock.acquire(10, TimeUnit.SECONDS)) { - throw new IllegalStateException("不能获取多锁"); -} -System.out.println("已获取多锁"); -System.out.println("是否有第一个锁: " + lock1.isAcquiredInThisProcess()); -System.out.println("是否有第二个锁: " + lock2.isAcquiredInThisProcess()); -try { - // 资源操作 - resource.use(); -} finally { - System.out.println("释放多个锁"); - lock.release(); -} -System.out.println("是否有第一个锁: " + lock1.isAcquiredInThisProcess()); -System.out.println("是否有第二个锁: " + lock2.isAcquiredInThisProcess()); -client.close(); -``` - -### 为什么要用临时顺序节点? - -每个数据节点在 ZooKeeper 中被称为 **znode**,它是 ZooKeeper 中数据的最小单元。 - -我们通常是将 znode 分为 4 大类: - -- **持久(PERSISTENT)节点**:一旦创建就一直存在即使 ZooKeeper 集群宕机,直到将其删除。 -- **临时(EPHEMERAL)节点**:临时节点的生命周期是与 **客户端会话(session)** 绑定的,**会话消失则节点消失** 。并且,**临时节点只能做叶子节点** ,不能创建子节点。 -- **持久顺序(PERSISTENT_SEQUENTIAL)节点**:除了具有持久(PERSISTENT)节点的特性之外, 子节点的名称还具有顺序性。比如 `/node1/app0000000001`、`/node1/app0000000002` 。 -- **临时顺序(EPHEMERAL_SEQUENTIAL)节点**:除了具备临时(EPHEMERAL)节点的特性之外,子节点的名称还具有顺序性。 - -可以看出,临时节点相比持久节点,最主要的是对会话失效的情况处理不一样,临时节点会话消失则对应的节点消失。这样的话,如果客户端发生异常导致没来得及释放锁也没关系,会话失效节点自动被删除,不会发生死锁的问题。 - -使用 Redis 实现分布式锁的时候,我们是通过过期时间来避免锁无法被释放导致死锁问题的,而 ZooKeeper 直接利用临时节点的特性即可。 - -假设不适用顺序节点的话,所有尝试获取锁的客户端都会对持有锁的子节点加监听器。当该锁被释放之后,势必会造成所有尝试获取锁的客户端来争夺锁,这样对性能不友好。使用顺序节点之后,只需要监听前一个节点就好了,对性能更友好。 - -### 为什么要设置对前一个节点的监听? - -> Watcher(事件监听器),是 ZooKeeper 中的一个很重要的特性。ZooKeeper 允许用户在指定节点上注册一些 Watcher,并且在一些特定事件触发的时候,ZooKeeper 服务端会将事件通知到感兴趣的客户端上去,该机制是 ZooKeeper 实现分布式协调服务的重要特性。 - -同一时间段内,可能会有很多客户端同时获取锁,但只有一个可以获取成功。如果获取锁失败,则说明有其他的客户端已经成功获取锁。获取锁失败的客户端并不会不停地循环去尝试加锁,而是在前一个节点注册一个事件监听器。 - -这个事件监听器的作用是:**当前一个节点对应的客户端释放锁之后(也就是前一个节点被删除之后,监听的是删除事件),通知获取锁失败的客户端(唤醒等待的线程,Java 中的 `wait/notifyAll` ),让它尝试去获取锁,然后就成功获取锁了。** - -### 如何实现可重入锁? - -这里以 Curator 的 `InterProcessMutex` 对可重入锁的实现来介绍(源码地址:[InterProcessMutex.java](https://github.com/apache/curator/blob/master/curator-recipes/src/main/java/org/apache/curator/framework/recipes/locks/InterProcessMutex.java))。 - -当我们调用 `InterProcessMutex#acquire`方法获取锁的时候,会调用`InterProcessMutex#internalLock`方法。 - -```java -// 获取可重入互斥锁,直到获取成功为止 -@Override -public void acquire() throws Exception { - if (!internalLock(-1, null)) { - throw new IOException("Lost connection while trying to acquire lock: " + basePath); - } -} -``` - -`internalLock` 方法会先获取当前请求锁的线程,然后从 `threadData`( `ConcurrentMap` 类型)中获取当前线程对应的 `lockData` 。 `lockData` 包含锁的信息和加锁的次数,是实现可重入锁的关键。 - -第一次获取锁的时候,`lockData`为 `null`。获取锁成功之后,会将当前线程和对应的 `lockData` 放到 `threadData` 中 - -```java -private boolean internalLock(long time, TimeUnit unit) throws Exception { - // 获取当前请求锁的线程 - Thread currentThread = Thread.currentThread(); - // 拿对应的 lockData - LockData lockData = threadData.get(currentThread); - // 第一次获取锁的话,lockData 为 null - if (lockData != null) { - // 当前线程获取过一次锁之后 - // 因为当前线程的锁存在, lockCount 自增后返回,实现锁重入. - lockData.lockCount.incrementAndGet(); - return true; - } - // 尝试获取锁 - String lockPath = internals.attemptLock(time, unit, getLockNodeBytes()); - if (lockPath != null) { - LockData newLockData = new LockData(currentThread, lockPath); - // 获取锁成功之后,将当前线程和对应的 lockData 放到 threadData 中 - threadData.put(currentThread, newLockData); - return true; - } - - return false; -} -``` - -`LockData`是 `InterProcessMutex`中的一个静态内部类。 - -```java - -private final ConcurrentMap threadData = Maps.newConcurrentMap(); - -private static class LockData -{ - // 当前持有锁的线程 - final Thread owningThread; - // 锁对应的子节点 - final String lockPath; - // 加锁的次数 - final AtomicInteger lockCount = new AtomicInteger(1); - - private LockData(Thread owningThread, String lockPath) - { - this.owningThread = owningThread; - this.lockPath = lockPath; - } -} -``` - -如果已经获取过一次锁,后面再来获取锁的话,直接就会在 `if (lockData != null)` 这里被拦下了,然后就会执行`lockData.lockCount.incrementAndGet();` 将加锁次数加 1。 +## 总结 -整个可重入锁的实现逻辑非常简单,直接在客户端判断当前线程有没有获取锁,有的话直接将加锁次数加 1 就可以了。 +这篇文章我们主要介绍了: -## 总结 +- 分布式锁的用途:分布式系统下,不同的服务/客户端通常运行在独立的 JVM 进程上。如果多个 JVM 进程共享同一份资源的话,使用本地锁就没办法实现资源的互斥访问了。 +- 分布式锁的应该具备的条件:互斥、高可用、可重入、高性能、非阻塞。 +- 分布式锁的常见实现方式:关系型数据库比如 MySQL、分布式协调服务 ZooKeeper、分布式键值存储系统比如 Redis 、Etcd 。 -这篇文章我们介绍了分布式锁的基本概念以及实现分布式锁的两种常见方式。至于具体选择 Redis 还是 ZooKeeper 来实现分布式锁,还是要看业务的具体需求。如果对性能要求比较高的话,建议使用 Redis 实现分布式锁。如果对可靠性要求比较高的话,建议使用 ZooKeeper 实现分布式锁。 + diff --git a/docs/distributed-system/distributed-process-coordination/zookeeper/zookeeper-in-action.md b/docs/distributed-system/distributed-process-coordination/zookeeper/zookeeper-in-action.md index 04e49a7d218..af6f3de5a21 100644 --- a/docs/distributed-system/distributed-process-coordination/zookeeper/zookeeper-in-action.md +++ b/docs/distributed-system/distributed-process-coordination/zookeeper/zookeeper-in-action.md @@ -293,3 +293,5 @@ zkClient.setData().forPath("/node1/00001","c++".getBytes());//更新节点数据 ```java List childrenPaths = zkClient.getChildren().forPath("/node1"); ``` + + diff --git a/docs/distributed-system/distributed-process-coordination/zookeeper/zookeeper-intro.md b/docs/distributed-system/distributed-process-coordination/zookeeper/zookeeper-intro.md index f4102f61e6d..955c5d2813a 100644 --- a/docs/distributed-system/distributed-process-coordination/zookeeper/zookeeper-intro.md +++ b/docs/distributed-system/distributed-process-coordination/zookeeper/zookeeper-intro.md @@ -39,7 +39,7 @@ _如果文章有任何需要改善和完善的地方,欢迎在评论区指出 ZooKeeper 是一个开源的**分布式协调服务**,它的设计目标是将那些复杂且容易出错的分布式一致性服务封装起来,构成一个高效可靠的原语集,并以一系列简单易用的接口提供给用户使用。 -> **原语:** 操作系统或计算机网络用语范畴。是由若干条指令组成的,用于完成一定功能的一个过程。具有不可分割性·即原语的执行必须是连续的,在执行过程中不允许被中断。 +> **原语:** 操作系统或计算机网络用语范畴。是由若干条指令组成的,用于完成一定功能的一个过程。具有不可分割性,即原语的执行必须是连续的,在执行过程中不允许被中断。 ZooKeeper 为我们提供了高可用、高性能、稳定的分布式数据一致性解决方案,通常被用于实现诸如数据发布/订阅、负载均衡、命名服务、分布式协调/通知、集群管理、Master 选举、分布式锁和分布式队列等功能。这些功能的实现主要依赖于 ZooKeeper 提供的 **数据存储+事件监听** 功能(后文会详细介绍到) 。 @@ -57,6 +57,9 @@ ZooKeeper 将数据保存在内存中,性能是不错的。 在“读”多于 - **原子性:** 所有事务请求的处理结果在整个集群中所有机器上的应用情况是一致的,也就是说,要么整个集群中所有的机器都成功应用了某一个事务,要么都没有应用。 - **单一系统映像:** 无论客户端连到哪一个 ZooKeeper 服务器上,其看到的服务端数据模型都是一致的。 - **可靠性:** 一旦一次更改请求被应用,更改的结果就会被持久化,直到被下一次更改覆盖。 +- **实时性:** 一旦数据发生变更,其他节点会实时感知到。每个客户端的系统视图都是最新的。 +- **集群部署**:3~5 台(最好奇数台)机器就可以组成一个集群,每台机器都在内存保存了 ZooKeeper 的全部数据,机器之间互相通信同步数据,客户端连接任何一台机器都可以。 +- **高可用:**如果某台机器宕机,会保证数据不丢失。集群中挂掉不超过一半的机器,都能保证集群可用。比如 3 台机器可以挂 1 台,5 台机器可以挂 2 台。 ### ZooKeeper 应用场景 @@ -76,9 +79,9 @@ _破音:拿出小本本,下面的内容非常重要哦!_ ### Data model(数据模型) -ZooKeeper 数据模型采用层次化的多叉树形结构,每个节点上都可以存储数据,这些数据可以是数字、字符串或者是二级制序列。并且。每个节点还可以拥有 N 个子节点,最上层是根节点以“/”来代表。每个数据节点在 ZooKeeper 中被称为 **znode**,它是 ZooKeeper 中数据的最小单元。并且,每个 znode 都一个唯一的路径标识。 +ZooKeeper 数据模型采用层次化的多叉树形结构,每个节点上都可以存储数据,这些数据可以是数字、字符串或者是二进制序列。并且。每个节点还可以拥有 N 个子节点,最上层是根节点以“/”来代表。每个数据节点在 ZooKeeper 中被称为 **znode**,它是 ZooKeeper 中数据的最小单元。并且,每个 znode 都有一个唯一的路径标识。 -强调一句:**ZooKeeper 主要是用来协调服务的,而不是用来存储业务数据的,所以不要放比较大的数据在 znode 上,ZooKeeper 给出的上限是每个结点的数据大小最大是 1M。** +强调一句:**ZooKeeper 主要是用来协调服务的,而不是用来存储业务数据的,所以不要放比较大的数据在 znode 上,ZooKeeper 给出的每个节点的数据大小上限是 1M 。** 从下图可以更直观地看出:ZooKeeper 节点路径标识方式和 Unix 文件系统路径非常相似,都是由一系列使用斜杠"/"进行分割的路径表示,开发人员可以向这个节点中写入数据,也可以在节点下面创建子节点。这些操作我们后面都会介绍到。 @@ -91,7 +94,7 @@ ZooKeeper 数据模型采用层次化的多叉树形结构,每个节点上都 我们通常是将 znode 分为 4 大类: - **持久(PERSISTENT)节点**:一旦创建就一直存在即使 ZooKeeper 集群宕机,直到将其删除。 -- **临时(EPHEMERAL)节点**:临时节点的生命周期是与 **客户端会话(session)** 绑定的,**会话消失则节点消失** 。并且,**临时节点只能做叶子节点** ,不能创建子节点。 +- **临时(EPHEMERAL)节点**:临时节点的生命周期是与 **客户端会话(session)** 绑定的,**会话消失则节点消失**。并且,**临时节点只能做叶子节点** ,不能创建子节点。 - **持久顺序(PERSISTENT_SEQUENTIAL)节点**:除了具有持久(PERSISTENT)节点的特性之外, 子节点的名称还具有顺序性。比如 `/node1/app0000000001`、`/node1/app0000000002` 。 - **临时顺序(EPHEMERAL_SEQUENTIAL)节点**:除了具备临时(EPHEMERAL)节点的特性之外,子节点的名称还具有顺序性 @@ -215,8 +218,8 @@ ZooKeeper 集群中的所有机器通过一个 **Leader 选举过程** 来选定 1. **Leader election(选举阶段)**:节点在一开始都处于选举阶段,只要有一个节点得到超半数节点的票数,它就可以当选准 leader。 2. **Discovery(发现阶段)**:在这个阶段,followers 跟准 leader 进行通信,同步 followers 最近接收的事务提议。 -3. **Synchronization(同步阶段)** :同步阶段主要是利用 leader 前一阶段获得的最新提议历史,同步集群中所有的副本。同步完成之后准 leader 才会成为真正的 leader。 -4. **Broadcast(广播阶段)** :到了这个阶段,ZooKeeper 集群才能正式对外提供事务服务,并且 leader 可以进行消息广播。同时如果有新的节点加入,还需要对新节点进行同步。 +3. **Synchronization(同步阶段)**:同步阶段主要是利用 leader 前一阶段获得的最新提议历史,同步集群中所有的副本。同步完成之后准 leader 才会成为真正的 leader。 +4. **Broadcast(广播阶段)**:到了这个阶段,ZooKeeper 集群才能正式对外提供事务服务,并且 leader 可以进行消息广播。同时如果有新的节点加入,还需要对新节点进行同步。 ZooKeeper 集群中的服务器状态有下面几种: @@ -252,7 +255,7 @@ Paxos 算法应该可以说是 ZooKeeper 的灵魂了。但是,ZooKeeper 并 ### ZAB 协议介绍 -ZAB(ZooKeeper Atomic Broadcast 原子广播) 协议是为分布式协调服务 ZooKeeper 专门设计的一种支持崩溃恢复的原子广播协议。 在 ZooKeeper 中,主要依赖 ZAB 协议来实现分布式数据一致性,基于该协议,ZooKeeper 实现了一种主备模式的系统架构来保持集群中各个副本之间的数据一致性。 +ZAB(ZooKeeper Atomic Broadcast,原子广播) 协议是为分布式协调服务 ZooKeeper 专门设计的一种支持崩溃恢复的原子广播协议。 在 ZooKeeper 中,主要依赖 ZAB 协议来实现分布式数据一致性,基于该协议,ZooKeeper 实现了一种主备模式的系统架构来保持集群中各个副本之间的数据一致性。 ### ZAB 协议两种基本的模式:崩溃恢复和消息广播 @@ -261,12 +264,41 @@ ZAB 协议包括两种基本的模式,分别是 - **崩溃恢复**:当整个服务框架在启动过程中,或是当 Leader 服务器出现网络中断、崩溃退出与重启等异常情况时,ZAB 协议就会进入恢复模式并选举产生新的 Leader 服务器。当选举产生了新的 Leader 服务器,同时集群中已经有过半的机器与该 Leader 服务器完成了状态同步之后,ZAB 协议就会退出恢复模式。其中,**所谓的状态同步是指数据同步,用来保证集群中存在过半的机器能够和 Leader 服务器的数据状态保持一致**。 - **消息广播**:**当集群中已经有过半的 Follower 服务器完成了和 Leader 服务器的状态同步,那么整个服务框架就可以进入消息广播模式了。** 当一台同样遵守 ZAB 协议的服务器启动后加入到集群中时,如果此时集群中已经存在一个 Leader 服务器在负责进行消息广播,那么新加入的服务器就会自觉地进入数据恢复模式:找到 Leader 所在的服务器,并与其进行数据同步,然后一起参与到消息广播流程中去。 +### ZAB 协议&Paxos 算法文章推荐 + 关于 **ZAB 协议&Paxos 算法** 需要讲和理解的东西太多了,具体可以看下面这几篇文章: - [Paxos 算法详解](https://javaguide.cn/distributed-system/protocol/paxos-algorithm.html) -- [Zookeeper ZAB 协议分析](https://dbaplus.cn/news-141-1875-1.html) +- [ZooKeeper 与 Zab 协议 · Analyze](https://wingsxdu.com/posts/database/zookeeper/) - [Raft 算法详解](https://javaguide.cn/distributed-system/protocol/raft-algorithm.html) +## ZooKeeper VS ETCD + +[ETCD](https://etcd.io/) 是一种强一致性的分布式键值存储,它提供了一种可靠的方式来存储需要由分布式系统或机器集群访问的数据。ETCD 内部采用 [Raft 算法](https://javaguide.cn/distributed-system/protocol/raft-algorithm.html)作为一致性算法,基于 Go 语言实现。 + +与 ZooKeeper 类似,ETCD 也可用于数据发布/订阅、负载均衡、命名服务、分布式协调/通知、分布式锁等场景。那二者如何选择呢? + +得物技术的[浅析如何基于 ZooKeeper 实现高可用架构](https://mp.weixin.qq.com/s/pBI3rjv5NdS1124Z7HQ-JA)这篇文章给出了如下的对比表格(我进一步做了优化),可以作为参考: + +| | ZooKeeper | ETCD | +| ---------------- | --------------------------------------------------------------------- | ------------------------------------------------------ | +| **语言** | Java | Go | +| **协议** | TCP | Grpc | +| **接口调用** | 必须要使用自己的 client 进行调用 | 可通过 HTTP 传输,即可通过 CURL 等命令实现调用 | +| **一致性算法** | Zab 协议 | Raft 算法 | +| **Watcher 机制** | 较局限,一次性触发器 | 一次 Watch 可以监听所有的事件 | +| **数据模型** | 基于目录的层次模式 | 参考了 zk 的数据模型,是个扁平的 kv 模型 | +| **存储** | kv 存储,使用的是 ConcurrentHashMap,内存存储,一般不建议存储较多数据 | kv 存储,使用 bbolt 存储引擎,可以处理几个 GB 的数据。 | +| **MVCC** | 不支持 | 支持,通过两个 B+ Tree 进行版本控制 | +| **全局 Session** | 存在缺陷 | 实现更灵活,避免了安全性问题 | +| **权限校验** | ACL | RBAC | +| **事务能力** | 提供了简易的事务能力 | 只提供了版本号的检查能力 | +| **部署维护** | 复杂 | 简单 | + +ZooKeeper 在存储性能、全局 Session、Watcher 机制等方面存在一定局限性,越来越多的开源项目在替换 ZooKeeper 为 Raft 实现或其它分布式协调服务,例如:[Kafka Needs No Keeper - Removing ZooKeeper Dependency (confluent.io)](https://www.confluent.io/blog/removing-zookeeper-dependency-in-kafka/)、[Moving Toward a ZooKeeper-Less Apache Pulsar (streamnative.io)](https://streamnative.io/blog/moving-toward-zookeeper-less-apache-pulsar)。 + +ETCD 相对来说更优秀一些,提供了更稳定的高负载读写能力,对 ZooKeeper 暴露的许多问题进行了改进优化。并且,ETCD 基本能够覆盖 ZooKeeper 的所有应用场景,实现对其的替代。 + ## 总结 1. ZooKeeper 本身就是一个分布式程序(只要半数以上节点存活,ZooKeeper 就能正常服务)。 @@ -279,3 +311,6 @@ ZAB 协议包括两种基本的模式,分别是 ## 参考 - 《从 Paxos 到 ZooKeeper 分布式一致性原理与实践》 +- 谈谈 ZooKeeper 的局限性: + + diff --git a/docs/distributed-system/distributed-process-coordination/zookeeper/zookeeper-plus.md b/docs/distributed-system/distributed-process-coordination/zookeeper/zookeeper-plus.md index f046e189788..856378a0cd5 100644 --- a/docs/distributed-system/distributed-process-coordination/zookeeper/zookeeper-plus.md +++ b/docs/distributed-system/distributed-process-coordination/zookeeper/zookeeper-plus.md @@ -9,9 +9,7 @@ tag: ## 什么是 ZooKeeper -`ZooKeeper` 由 `Yahoo` 开发,后来捐赠给了 `Apache` ,现已成为 `Apache` 顶级项目。`ZooKeeper` 是一个开源的分布式应用程序协调服务器,其为分布式系统提供一致性服务。其一致性是通过基于 `Paxos` 算法的 `ZAB` 协议完成的。其主要功能包括:配置维护、分布式同步、集群管理、分布式事务等。 - -![zookeeper](https://p3-juejin.byteimg.com/tos-cn-i-k3u1fbpfcp/9ec99b7ae7a14e4d8faf675e5791d80c~tplv-k3u1fbpfcp-zoom-1.image) +`ZooKeeper` 由 `Yahoo` 开发,后来捐赠给了 `Apache` ,现已成为 `Apache` 顶级项目。`ZooKeeper` 是一个开源的分布式应用程序协调服务器,其为分布式系统提供一致性服务。其一致性是通过基于 `Paxos` 算法的 `ZAB` 协议完成的。其主要功能包括:配置维护、分布式同步、集群管理等。 简单来说, `ZooKeeper` 是一个 **分布式协调服务框架** 。分布式?协调服务?这啥玩意?🤔🤔 @@ -19,15 +17,15 @@ tag: 比如,我现在有一个秒杀服务,并发量太大单机系统承受不住,那我加几台服务器也 **一样** 提供秒杀服务,这个时候就是 **`Cluster` 集群** 。 -![cluster](https://p3-juejin.byteimg.com/tos-cn-i-k3u1fbpfcp/60263e969b9e4a0f81724b1f4d5b3d58~tplv-k3u1fbpfcp-zoom-1.image) +![cluster](https://oss.javaguide.cn/p3-juejin/60263e969b9e4a0f81724b1f4d5b3d58~tplv-k3u1fbpfcp-zoom-1.jpeg) 但是,我现在换一种方式,我将一个秒杀服务 **拆分成多个子服务** ,比如创建订单服务,增加积分服务,扣优惠券服务等等,**然后我将这些子服务都部署在不同的服务器上** ,这个时候就是 **`Distributed` 分布式** 。 -![distributed](https://p3-juejin.byteimg.com/tos-cn-i-k3u1fbpfcp/0d42e7b4249144b3a77a0c519216ae3d~tplv-k3u1fbpfcp-zoom-1.image) +![distributed](https://oss.javaguide.cn/p3-juejin/0d42e7b4249144b3a77a0c519216ae3d~tplv-k3u1fbpfcp-zoom-1.jpeg) 而我为什么反驳同学所说的分布式就是加机器呢?因为我认为加机器更加适用于构建集群,因为它真是只有加机器。而对于分布式来说,你首先需要将业务进行拆分,然后再加机器(不仅仅是加机器那么简单),同时你还要去解决分布式带来的一系列问题。 -![](https://p3-juejin.byteimg.com/tos-cn-i-k3u1fbpfcp/e3662ca1a09c4444b07f15dbf85c6ba8~tplv-k3u1fbpfcp-zoom-1.image) +![](https://oss.javaguide.cn/p3-juejin/e3662ca1a09c4444b07f15dbf85c6ba8~tplv-k3u1fbpfcp-zoom-1.jpeg) 比如各个分布式组件如何协调起来,如何减少各个系统之间的耦合度,分布式事务的处理,如何去配置整个分布式系统等等。`ZooKeeper` 主要就是解决这些问题的。 @@ -37,7 +35,7 @@ tag: 理解起来其实很简单,比如说把一个班级作为整个系统,而学生是系统中的一个个独立的子系统。这个时候班里的小红小明偷偷谈恋爱被班里的大嘴巴小花发现了,小花欣喜若狂告诉了周围的人,然后小红小明谈恋爱的消息在班级里传播起来了。当在消息的传播(散布)过程中,你抓到一个同学问他们的情况,如果回答你不知道,那么说明整个班级系统出现了数据不一致的问题(因为小花已经知道这个消息了)。而如果他直接不回答你,因为整个班级有消息在进行传播(为了保证一致性,需要所有人都知道才可提供服务),这个时候就出现了系统的可用性问题。 -![](https://p3-juejin.byteimg.com/tos-cn-i-k3u1fbpfcp/38b9ff4b193e4487afe32c9710c6d644~tplv-k3u1fbpfcp-zoom-1.image) +![](https://oss.javaguide.cn/p3-juejin/38b9ff4b193e4487afe32c9710c6d644~tplv-k3u1fbpfcp-zoom-1-20230717160254318-20230717160259975.jpeg) 而上述前者就是 `Eureka` 的处理方式,它保证了 AP(可用性),后者就是我们今天所要讲的 `ZooKeeper` 的处理方式,它保证了 CP(数据一致性)。 @@ -47,7 +45,7 @@ tag: 这时候请你思考一个问题,同学之间如果采用传纸条的方式去传播消息,那么就会出现一个问题——我咋知道我的小纸条有没有传到我想要传递的那个人手中呢?万一被哪个小家伙给劫持篡改了呢,对吧? -![](https://p3-juejin.byteimg.com/tos-cn-i-k3u1fbpfcp/8c73e264d28b4a93878f4252e4e3e43c~tplv-k3u1fbpfcp-zoom-1.image) +![](https://oss.javaguide.cn/p3-juejin/8c73e264d28b4a93878f4252e4e3e43c~tplv-k3u1fbpfcp-zoom-1.jpeg) 这个时候就引申出一个概念—— **拜占庭将军问题** 。它意指 **在不可靠信道上试图通过消息传递的方式达到一致性是不可能的**, 所以所有的一致性算法的 **必要前提** 就是安全可靠的消息通道。 @@ -73,11 +71,11 @@ tag: 而如果在第一阶段并不是所有参与者都返回了准备好了的消息,那么此时协调者将会给所有参与者发送 **回滚事务的 `rollback` 请求**,参与者收到之后将会 **回滚它在第一阶段所做的事务处理** ,然后再将处理情况返回给协调者,最终协调者收到响应后便给事务发起者返回处理失败的结果。 -![2PC流程](https://p3-juejin.byteimg.com/tos-cn-i-k3u1fbpfcp/1a7210167f1d4d4fb97afcec19902a59~tplv-k3u1fbpfcp-zoom-1.image) +![2PC流程](https://oss.javaguide.cn/p3-juejin/1a7210167f1d4d4fb97afcec19902a59~tplv-k3u1fbpfcp-zoom-1.jpeg) 个人觉得 2PC 实现得还是比较鸡肋的,因为事实上它只解决了各个事务的原子性问题,随之也带来了很多的问题。 -![](https://p3-juejin.byteimg.com/tos-cn-i-k3u1fbpfcp/cc534022c7184770b9b82b2d0008432a~tplv-k3u1fbpfcp-zoom-1.image) +![](https://oss.javaguide.cn/p3-juejin/cc534022c7184770b9b82b2d0008432a~tplv-k3u1fbpfcp-zoom-1.jpeg) - **单点故障问题**,如果协调者挂了那么整个系统都处于不可用的状态了。 - **阻塞问题**,即当协调者发送 `prepare` 请求,参与者收到之后如果能处理那么它将会进行事务的处理但并不提交,这个时候会一直占用着资源不释放,如果此时协调者挂了,那么这些资源都不会再释放了,这会极大影响性能。 @@ -93,11 +91,11 @@ tag: 2. **PreCommit 阶段**:协调者根据参与者返回的响应来决定是否可以进行下面的 `PreCommit` 操作。如果上面参与者返回的都是 YES,那么协调者将向所有参与者发送 `PreCommit` 预提交请求,**参与者收到预提交请求后,会进行事务的执行操作,并将 `Undo` 和 `Redo` 信息写入事务日志中** ,最后如果参与者顺利执行了事务则给协调者返回成功的响应。如果在第一阶段协调者收到了 **任何一个 NO** 的信息,或者 **在一定时间内** 并没有收到全部的参与者的响应,那么就会中断事务,它会向所有参与者发送中断请求(abort),参与者收到中断请求之后会立即中断事务,或者在一定时间内没有收到协调者的请求,它也会中断事务。 3. **DoCommit 阶段**:这个阶段其实和 `2PC` 的第二阶段差不多,如果协调者收到了所有参与者在 `PreCommit` 阶段的 YES 响应,那么协调者将会给所有参与者发送 `DoCommit` 请求,**参与者收到 `DoCommit` 请求后则会进行事务的提交工作**,完成后则会给协调者返回响应,协调者收到所有参与者返回的事务提交成功的响应之后则完成事务。若协调者在 `PreCommit` 阶段 **收到了任何一个 NO 或者在一定时间内没有收到所有参与者的响应** ,那么就会进行中断请求的发送,参与者收到中断请求后则会 **通过上面记录的回滚日志** 来进行事务的回滚操作,并向协调者反馈回滚状况,协调者收到参与者返回的消息后,中断事务。 -![3PC流程](https://p3-juejin.byteimg.com/tos-cn-i-k3u1fbpfcp/80854635d48c42d896dbaa066abf5c26~tplv-k3u1fbpfcp-zoom-1.image) +![3PC流程](https://oss.javaguide.cn/p3-juejin/80854635d48c42d896dbaa066abf5c26~tplv-k3u1fbpfcp-zoom-1.jpeg) > 这里是 `3PC` 在成功的环境下的流程图,你可以看到 `3PC` 在很多地方进行了超时中断的处理,比如协调者在指定时间内未收到全部的确认消息则进行事务中断的处理,这样能 **减少同步阻塞的时间** 。还有需要注意的是,**`3PC` 在 `DoCommit` 阶段参与者如未收到协调者发送的提交事务的请求,它会在一定时间内进行事务的提交**。为什么这么做呢?是因为这个时候我们肯定**保证了在第一阶段所有的协调者全部返回了可以执行事务的响应**,这个时候我们有理由**相信其他系统都能进行事务的执行和提交**,所以**不管**协调者有没有发消息给参与者,进入第三阶段参与者都会进行事务的提交操作。 -总之,`3PC` 通过一系列的超时机制很好的缓解了阻塞问题,但是最重要的一致性并没有得到根本的解决,比如在 `PreCommit` 阶段,当一个参与者收到了请求之后其他参与者和协调者挂了或者出现了网络分区,这个时候收到消息的参与者都会进行事务提交,这就会出现数据不一致性问题。 +总之,`3PC` 通过一系列的超时机制很好的缓解了阻塞问题,但是最重要的一致性并没有得到根本的解决,比如在 `DoCommit` 阶段,当一个参与者收到了请求之后其他参与者和协调者挂了或者出现了网络分区,这个时候收到消息的参与者都会进行事务提交,这就会出现数据不一致性问题。 所以,要解决一致性问题还需要靠 `Paxos` 算法 ⭐️ ⭐️ ⭐️ 。 @@ -114,7 +112,7 @@ tag: > 下面是 `prepare` 阶段的流程图,你可以对照着参考一下。 -![paxos第一阶段](https://p3-juejin.byteimg.com/tos-cn-i-k3u1fbpfcp/cd1e5f78875b4ad6b54013738f570943~tplv-k3u1fbpfcp-zoom-1.image) +![paxos第一阶段](https://oss.javaguide.cn/p3-juejin/cd1e5f78875b4ad6b54013738f570943~tplv-k3u1fbpfcp-zoom-1.jpeg) #### accept 阶段 @@ -122,11 +120,11 @@ tag: 表决者收到提案请求后会再次比较本身已经批准过的最大提案编号和该提案编号,如果该提案编号 **大于等于** 已经批准过的最大提案编号,那么就 `accept` 该提案(此时执行提案内容但不提交),随后将情况返回给 `Proposer` 。如果不满足则不回应或者返回 NO 。 -![paxos第二阶段1](https://p3-juejin.byteimg.com/tos-cn-i-k3u1fbpfcp/dad7f51d58b24a72b249278502ec04bd~tplv-k3u1fbpfcp-zoom-1.image) +![paxos第二阶段1](https://oss.javaguide.cn/p3-juejin/dad7f51d58b24a72b249278502ec04bd~tplv-k3u1fbpfcp-zoom-1.jpeg) 当 `Proposer` 收到超过半数的 `accept` ,那么它这个时候会向所有的 `acceptor` 发送提案的提交请求。需要注意的是,因为上述仅仅是超过半数的 `acceptor` 批准执行了该提案内容,其他没有批准的并没有执行该提案内容,所以这个时候需要**向未批准的 `acceptor` 发送提案内容和提案编号并让它无条件执行和提交**,而对于前面已经批准过该提案的 `acceptor` 来说 **仅仅需要发送该提案的编号** ,让 `acceptor` 执行提交就行了。 -![paxos第二阶段2](https://p3-juejin.byteimg.com/tos-cn-i-k3u1fbpfcp/9359bbabb511472e8de04d0826967996~tplv-k3u1fbpfcp-zoom-1.image) +![paxos第二阶段2](https://oss.javaguide.cn/p3-juejin/9359bbabb511472e8de04d0826967996~tplv-k3u1fbpfcp-zoom-1.jpeg) 而如果 `Proposer` 如果没有收到超过半数的 `accept` 那么它将会将 **递增** 该 `Proposal` 的编号,然后 **重新进入 `Prepare` 阶段** 。 @@ -140,7 +138,7 @@ tag: 就这样无休无止的永远提案下去,这就是 `paxos` 算法的死循环问题。 -![](https://p3-juejin.byteimg.com/tos-cn-i-k3u1fbpfcp/bc3d45941abf4fca903f7f4b69405abf~tplv-k3u1fbpfcp-zoom-1.image) +![](https://oss.javaguide.cn/p3-juejin/bc3d45941abf4fca903f7f4b69405abf~tplv-k3u1fbpfcp-zoom-1.jpeg) 那么如何解决呢?很简单,人多了容易吵架,我现在 **就允许一个能提案** 就行了。 @@ -150,7 +148,7 @@ tag: 作为一个优秀高效且可靠的分布式协调框架,`ZooKeeper` 在解决分布式数据一致性问题时并没有直接使用 `Paxos` ,而是专门定制了一致性协议叫做 `ZAB(ZooKeeper Atomic Broadcast)` 原子广播协议,该协议能够很好地支持 **崩溃恢复** 。 -![Zookeeper架构](https://p3-juejin.byteimg.com/tos-cn-i-k3u1fbpfcp/07bf6c1e10f84fc58a2453766ca6bd18~tplv-k3u1fbpfcp-zoom-1.image) +![Zookeeper架构](https://oss.javaguide.cn/p3-juejin/07bf6c1e10f84fc58a2453766ca6bd18~tplv-k3u1fbpfcp-zoom-1.png) ### ZAB 中的三个角色 @@ -168,11 +166,9 @@ tag: 不就是 **在整个集群中保持数据的一致性** 嘛?如果是你,你会怎么做呢? -![](https://p3-juejin.byteimg.com/tos-cn-i-k3u1fbpfcp/43b1a324d45b4e6a8a1228666d563f92~tplv-k3u1fbpfcp-zoom-1.image) - 废话,第一步肯定需要 `Leader` 将写请求 **广播** 出去呀,让 `Leader` 问问 `Followers` 是否同意更新,如果超过半数以上的同意那么就进行 `Follower` 和 `Observer` 的更新(和 `Paxos` 一样)。当然这么说有点虚,画张图理解一下。 -![消息广播](https://p3-juejin.byteimg.com/tos-cn-i-k3u1fbpfcp/b64c7f25a5d24766889da14260005e31~tplv-k3u1fbpfcp-zoom-1.image) +![消息广播](https://oss.javaguide.cn/p3-juejin/b64c7f25a5d24766889da14260005e31~tplv-k3u1fbpfcp-zoom-1.jpeg) 嗯。。。看起来很简单,貌似懂了 🤥🤥🤥。这两个 `Queue` 哪冒出来的?答案是 **`ZAB` 需要让 `Follower` 和 `Observer` 保证顺序性** 。何为顺序性,比如我现在有一个写请求 A,此时 `Leader` 将请求 A 广播出去,因为只需要半数同意就行,所以可能这个时候有一个 `Follower` F1 因为网络原因没有收到,而 `Leader` 又广播了一个请求 B,因为网络原因,F1 竟然先收到了请求 B 然后才收到了请求 A,这个时候请求处理的顺序不同就会导致数据的不同,从而 **产生数据不一致问题** 。 @@ -214,7 +210,7 @@ tag: 假设 `Leader (server2)` 发送 `commit` 请求(忘了请看上面的消息广播模式),他发送给了 `server3`,然后要发给 `server1` 的时候突然挂了。这个时候重新选举的时候我们如果把 `server1` 作为 `Leader` 的话,那么肯定会产生数据不一致性,因为 `server3` 肯定会提交刚刚 `server2` 发送的 `commit` 请求的提案,而 `server1` 根本没收到所以会丢弃。 -![崩溃恢复](https://p3-juejin.byteimg.com/tos-cn-i-k3u1fbpfcp/4b8365e80bdf441ea237847fb91236b7~tplv-k3u1fbpfcp-zoom-1.image) +![崩溃恢复](https://oss.javaguide.cn/p3-juejin/4b8365e80bdf441ea237847fb91236b7~tplv-k3u1fbpfcp-zoom-1.jpeg) 那怎么解决呢? @@ -224,7 +220,7 @@ tag: 假设 `Leader (server2)` 此时同意了提案 N1,自身提交了这个事务并且要发送给所有 `Follower` 要 `commit` 的请求,却在这个时候挂了,此时肯定要重新进行 `Leader` 的选举,比如说此时选 `server1` 为 `Leader` (这无所谓)。但是过了一会,这个 **挂掉的 `Leader` 又重新恢复了** ,此时它肯定会作为 `Follower` 的身份进入集群中,需要注意的是刚刚 `server2` 已经同意提交了提案 N1,但其他 `server` 并没有收到它的 `commit` 信息,所以其他 `server` 不可能再提交这个提案 N1 了,这样就会出现数据不一致性问题了,所以 **该提案 N1 最终需要被抛弃掉** 。 -![崩溃恢复](https://p3-juejin.byteimg.com/tos-cn-i-k3u1fbpfcp/99cdca39ad6340ae8b77e8befe94e36e~tplv-k3u1fbpfcp-zoom-1.image) +![崩溃恢复](https://oss.javaguide.cn/p3-juejin/99cdca39ad6340ae8b77e8befe94e36e~tplv-k3u1fbpfcp-zoom-1.jpeg) ## Zookeeper 的几个理论知识 @@ -236,7 +232,7 @@ tag: `zookeeper` 数据存储结构与标准的 `Unix` 文件系统非常相似,都是在根节点下挂很多子节点(树型)。但是 `zookeeper` 中没有文件系统中目录与文件的概念,而是 **使用了 `znode` 作为数据节点** 。`znode` 是 `zookeeper` 中的最小数据单元,每个 `znode` 上都可以保存数据,同时还可以挂载子节点,形成一个树形化命名空间。 -![zk数据模型](https://p3-juejin.byteimg.com/tos-cn-i-k3u1fbpfcp/663240470d524dd4ac6e68bde0b666eb~tplv-k3u1fbpfcp-zoom-1.image) +![zk数据模型](https://oss.javaguide.cn/p3-juejin/663240470d524dd4ac6e68bde0b666eb~tplv-k3u1fbpfcp-zoom-1.jpeg) 每个 `znode` 都有自己所属的 **节点类型** 和 **节点状态**。 @@ -281,13 +277,13 @@ tag: `Watcher` 为事件监听器,是 `zk` 非常重要的一个特性,很多功能都依赖于它,它有点类似于订阅的方式,即客户端向服务端 **注册** 指定的 `watcher` ,当服务端符合了 `watcher` 的某些事件或要求则会 **向客户端发送事件通知** ,客户端收到通知后找到自己定义的 `Watcher` 然后 **执行相应的回调方法** 。 -![watcher机制](https://p3-juejin.byteimg.com/tos-cn-i-k3u1fbpfcp/ac87b7cff7b44c63997ff0f6a7b6d2eb~tplv-k3u1fbpfcp-zoom-1.image) +![watcher机制](https://oss.javaguide.cn/p3-juejin/ac87b7cff7b44c63997ff0f6a7b6d2eb~tplv-k3u1fbpfcp-zoom-1.jpeg) ## Zookeeper 的几个典型应用场景 前面说了这么多的理论知识,你可能听得一头雾水,这些玩意有啥用?能干啥事?别急,听我慢慢道来。 -![](https://p3-juejin.byteimg.com/tos-cn-i-k3u1fbpfcp/dbc1a52b0c304bb093ef08fb1d4c704c~tplv-k3u1fbpfcp-zoom-1.image) +![](https://oss.javaguide.cn/p3-juejin/dbc1a52b0c304bb093ef08fb1d4c704c~tplv-k3u1fbpfcp-zoom-1.jpeg) ### 选主 @@ -299,10 +295,28 @@ tag: 你想想为什么我们要创建临时节点?还记得临时节点的生命周期吗?`master` 挂了是不是代表会话断了?会话断了是不是意味着这个节点没了?还记得 `watcher` 吗?我们是不是可以 **让其他不是 `master` 的节点监听节点的状态** ,比如说我们监听这个临时节点的父节点,如果子节点个数变了就代表 `master` 挂了,这个时候我们 **触发回调函数进行重新选举** ,或者我们直接监听节点的状态,我们可以通过节点是否已经失去连接来判断 `master` 是否挂了等等。 -![选主](https://p3-juejin.byteimg.com/tos-cn-i-k3u1fbpfcp/00468757fb8f4f51875f645fbb7b25a2~tplv-k3u1fbpfcp-zoom-1.image) +![选主](https://oss.javaguide.cn/p3-juejin/00468757fb8f4f51875f645fbb7b25a2~tplv-k3u1fbpfcp-zoom-1.jpeg) 总的来说,我们可以完全 **利用 临时节点、节点状态 和 `watcher` 来实现选主的功能**,临时节点主要用来选举,节点状态和`watcher` 可以用来判断 `master` 的活性和进行重新选举。 +### 数据发布/订阅 + +还记得 Zookeeper 的 `Watcher` 机制吗? Zookeeper 通过这种推拉相结合的方式实现客户端与服务端的交互:客户端向服务端注册节点,一旦相应节点的数据变更,服务端就会向“监听”该节点的客户端发送 `Watcher` 事件通知,客户端接收到通知后需要 **主动** 到服务端获取最新的数据。基于这种方式,Zookeeper 实现了 **数据发布/订阅** 功能。 + +一个典型的应用场景为 **全局配置信息的集中管理**。 客户端在启动时会主动到 Zookeeper 服务端获取配置信息,同时 **在指定节点注册一个** `Watcher` **监听**。当配置信息发生变更,服务端通知所有订阅的客户端重新获取配置信息,实现配置信息的实时更新。 + +上面所提到的全局配置信息通常包括机器列表信息、运行时的开关配置、数据库配置信息等。需要注意的是,这类全局配置信息通常具备以下特性: + +- 数据量较小 +- 数据内容在运行时动态变化 +- 集群中机器共享一致配置 + +### 负载均衡 + +可以通过 Zookeeper 的 **临时节点** 实现负载均衡。回顾一下临时节点的特性:当创建节点的客户端与服务端之间断开连接,即客户端会话(session)消失时,对应节点也会自动消失。因此,我们可以使用临时节点来维护 Server 的地址列表,从而保证请求不会被分配到已停机的服务上。 + +具体地,我们需要在集群的每一个 Server 中都使用 Zookeeper 客户端连接 Zookeeper 服务端,同时用 Server **自身的地址信息**在服务端指定目录下创建临时节点。当客户端请求调用集群服务时,首先通过 Zookeeper 获取该目录下的节点列表 (即所有可用的 Server),随后根据不同的负载均衡策略将请求转发到某一具体的 Server。 + ### 分布式锁 分布式锁的实现方式有很多种,比如 `Redis`、数据库、`zookeeper` 等。个人认为 `zookeeper` 在实现分布式锁这方面是非常非常简单的。 @@ -341,19 +355,19 @@ tag: 而 `zookeeper` 天然支持的 `watcher` 和 临时节点能很好的实现这些需求。我们可以为每条机器创建临时节点,并监控其父节点,如果子节点列表有变动(我们可能创建删除了临时节点),那么我们可以使用在其父节点绑定的 `watcher` 进行状态监控和回调。 -![集群管理](https://p3-juejin.byteimg.com/tos-cn-i-k3u1fbpfcp/f3d70709f10f4fa6b09125a56a976fda~tplv-k3u1fbpfcp-zoom-1.image) +![集群管理](https://oss.javaguide.cn/p3-juejin/f3d70709f10f4fa6b09125a56a976fda~tplv-k3u1fbpfcp-zoom-1.jpeg) 至于注册中心也很简单,我们同样也是让 **服务提供者** 在 `zookeeper` 中创建一个临时节点并且将自己的 `ip、port、调用方式` 写入节点,当 **服务消费者** 需要进行调用的时候会 **通过注册中心找到相应的服务的地址列表(IP 端口什么的)** ,并缓存到本地(方便以后调用),当消费者调用服务时,不会再去请求注册中心,而是直接通过负载均衡算法从地址列表中取一个服务提供者的服务器调用服务。 当服务提供者的某台服务器宕机或下线时,相应的地址会从服务提供者地址列表中移除。同时,注册中心会将新的服务地址列表发送给服务消费者的机器并缓存在消费者本机(当然你可以让消费者进行节点监听,我记得 `Eureka` 会先试错,然后再更新)。 -![注册中心](https://p3-juejin.byteimg.com/tos-cn-i-k3u1fbpfcp/469cebf9670740d1a6711fe54db70e05~tplv-k3u1fbpfcp-zoom-1.image) +![注册中心](https://oss.javaguide.cn/p3-juejin/469cebf9670740d1a6711fe54db70e05~tplv-k3u1fbpfcp-zoom-1.jpeg) ## 总结 看到这里的同学实在是太有耐心了 👍👍👍 不知道大家是否还记得我讲了什么 😒。 -![](https://p3-juejin.byteimg.com/tos-cn-i-k3u1fbpfcp/912c1aa6b7794d4aac8ebe6a14832cae~tplv-k3u1fbpfcp-zoom-1.image) +![](https://oss.javaguide.cn/p3-juejin/912c1aa6b7794d4aac8ebe6a14832cae~tplv-k3u1fbpfcp-zoom-1.jpeg) 这篇文章中我带大家入门了 `zookeeper` 这个强大的分布式协调框架。现在我们来简单梳理一下整篇文章的内容。 @@ -368,3 +382,5 @@ tag: - `zookeeper` 的典型应用场景,比如选主,注册中心等等。 如果忘了可以回去看看再次理解一下,如果有疑问和建议欢迎提出 🤝🤝🤝。 + + diff --git a/docs/distributed-system/distributed-transaction.md b/docs/distributed-system/distributed-transaction.md index 2e3d3cf57a4..fa4c83c743c 100644 --- a/docs/distributed-system/distributed-transaction.md +++ b/docs/distributed-system/distributed-transaction.md @@ -1,5 +1,5 @@ --- -title: 分布式事务常见问题总结(付费) +title: 分布式事务常见解决方案总结(付费) category: 分布式 --- @@ -8,3 +8,5 @@ category: 分布式 ![](https://oss.javaguide.cn/javamianshizhibei/distributed-system.png) + + diff --git a/docs/distributed-system/images/distributed-lock/distributed-lock-redisson-renew-expiration.drawio b/docs/distributed-system/images/distributed-lock/distributed-lock-redisson-renew-expiration.drawio deleted file mode 100644 index 8493d73f0cb..00000000000 --- a/docs/distributed-system/images/distributed-lock/distributed-lock-redisson-renew-expiration.drawio +++ /dev/null @@ -1 +0,0 @@ -7VxZd6LKFv41rHXOQ7IKKBAeGTSxj5DJdNq8ISKCAx7FAX793VUMyqCxO5qk+57OWh1qoIY9f7uKMLw23d4srPnICAbOhOHQYMvwOsNxLItE+EVqoqSmIclJhbvwBmmnXcWTFztpJUprV97AWRY6hkEwCb15sdIOZjPHDgt11mIRbIrdhsGkOOvccp1KxZNtTaq1L94gHGX7EuVdw63juaNsalHASUvfssfuIljN0gkZjm+JrVZLSpqnVjZYutPlyBoEm70qvsnw2iIIwuRputWcCSFuRrfkvdaB1nzhC2cWnvJCxM6d5+Wt1n/d3Pz7Q9Xix/DqKhtmGUYZRZwBECgtBotwFLjBzJo0d7WqvVqsHTIqC4Vdl04QzNNK3wnDKGW2tQoDqBqF00naugwXwTgnNw81w2AWpt1ZAcrVvWXrDFYL2zmyIXiddiS72HszJcmNE0ydcBFBh4UzsUJvXRQDK5UmN++Xv3ofeLAWDqWSz7PidTpVKvkSQsVBQmvhOmH63o4x8LC3kF0VZddPsC7b6dqarJxMBEu8pOKZs2oz8kLnaW5RAm5An99kizeZaMEkWNDR+KFkO7ad98xaZsHMOcaztbMIne1RZqStua6kJBVwIyPyZk85ZT6pG+3rJYcOs7BA+p+m84eTeTjkPpTMnIw+m8hchcjfvhtQwTQlRm0xsso0G4wiMZJaIT5sOqyjcIlu+0ROq6yJ586gaAMpHahXCQk98A1K2jD1BgNq8epYumM6Klkw6UxsEhsFNvGoyiZZqHLpYkzi/zBf8WGuoshHQfhYR4ErujUJ7PE1+e+vv2tZ2rH6EOYV+HC6riycpRdbfToe0Yw52RfdqaAygl7DwQmZTs3DqTcN3jHhrKhXHjqmSypEX3Vqd4WuITot+vbUPL1TEDBbChkyF54NEQyHS+ciMlAXK4iTMGVGQQLEf1dB1nC1pGxSoAMrzLe7Rnhyye9v1toipllViJk+y5B7lj4ZDjacjJi0swfqL+yVJce2HPGSXhnjknjwuMYvY1y1+GWDcr7Yp2o7KmQGXDMnj96UQq3cMFAjch8svdALiIHoB2EYTA+rexL5wL8a4xISb6Fay3kCAYfelvBVpVMqWS3KauB5YIUWCFlS5FrLtctw6ha4zWn3tyb3Gqm4/7Jd2THyrNtHZOvBusMP+EEk8EYkrO2pvTZ8ZWNocjyY2l77dhT2b4T4bjZaWi/C4v7pWzC4fdzcedIa3uI7MzvuTOXoNZK2d92x0OGTfm1PRfdP7a3p90h9fKePt6bei6He79+0Yjr/i7nue213MJ1MBujb2tGRZ2jKpq03N4b/4BpdJTI10v49srnJuu9D+xPewhibzstkDONHRuwiKPPWyyOy4H1Td732zWhivQyCQVq+97eb3o/HoH3zILfHaGt2jZXpKcj0xysjbkYD/4G7e8KxGb8GZtzkOv7oBdYqdHxja4zdpP9sbppkTTCfpbuCOetxpq62Tf/xhdQ5nhqauhJ3/Db0acO746gXocj0MG9226HRfViZcRv6PKzuNIQMD8eG/pCVgfZYMDxl0+k+8B2/uTE1BRka3hq+G97pz3SdbZi34/f2yg9QfuDJe2kbWd8K+Iez8q6vAm3NlenD/rpN3PEVdKcpLOx7a8S9dB1Kso5Zb2t0n1d33R7udR9Qx3fdtH1Dx3lCGN7j2rpKxtqaT1gw/ZHY7Rox7AvmNrB548Zm112ZrV5kdl99Q0OCqeGI0OBOV2CNz+RdHvbI3eku0MeG9RI62RtYNwaaCHcajKsDzTXgWQy88hQe6ImMbi/r7+b9b2HNdA3PW0KPXtyE/WGUrLnNwTOsC9aqYRZ4H5m+S/ZD1sYZ3TZ+0tsI9h5999usOTWAD00EsoaADzAurBfWTdacjdXpQr9ZALL2Ou/fbOS2Z057vssaugI6o4TWC8ia3wa5f/Y6MZY6POhejIm2n8dmysXQSuSqFpOvCZF54VIGs1E1mE2BkRRGASwjMzJmJJZpiowkMYC6kia59RuAGvFcoEYuBsPSJ4Ma7oQ0ijMbKCTXSMg6sZZLzz4bSHkTfOzRJXPzda7/3aEpX1Qk1CgOkYCpCkapDCTIbwx0YbDDybWI5suC1IQcx3DNqYLyTgmQEfolxlUGaki/Jkpng7vyQayznFuzWmCyy+Vf2Yk5Jfhk4fb/4rBE0lAcrAVxZOjsWUB/12EXsOwyozRI3kphGYl07aysNJMlYUZWSB9ZYxSN4iaVUfS0Rm7tIZtkqQeQzQdj9HrTf1T9zgLDEeaK7p19n627PMzmatz/W7JXBsViPSgWiKyoYpYQbbFoyTRbjKqRUAIiC2iVJSp2TUbBWZOcTd9fHJHRqiCSB56RoAYzqk4qTxTN3ywVLsilE4cGe82deOTQ4K4vFUbyNTHIF/BiQOdF9IPYjOu82EtNCC3o20IpSksHGfVmipY/2U3KH+QmpWKOpiF/bDKXz/Kc+wBDJKqvVFHEJ+RyT/QTmYCfxU+wXIEl2WHS13UTwmeo968q73GzcBQWnk+9hbNHwe+zzzWHwrkWgqsGo6CI1HdKJDVOPK5GgX+eAchS5gePNRejYNpfLd92omUNLHrgikN1xIs6VFku4XuRqzvCb9RA2bIlPR+3xM9VNyyyBYW7lkEIjisdLd07Cw8oQEz0CZpIlXrrhT+yIeC5t1N3KO3mIoUzOGf5RO3NwuJLO2dJksqXcsqZvguDT/4w+Dz5VOwAAKiL0UtZxDTo107DHENr6k2iZFJosqZzOh/PY2pF+gGIdLk6x79La7a8WoKADg+AFcRIYsHeAfIF/EvgC6xboNtoUECTP8j0AacgRm3SPevkObekHOIRBdMNAmBk7mcgUAW6fMgxYn3etuRBS1baGQzFoXwmgyxx142SUmT6uGeROZTb6X2bLF7KJuOvd5nkfaEM/qA8XfXioVxm0qXvk1z+Rtybhx00lOk3EH+mU4nSjTgJcdWYpS4HcLGTd8zVEHnfqh9O5lBbSfKMybkTe2rq5s89c8Il7rJV+8cKNdy92KETPuFexW9+dze3Sp+nQjUnexAlSA2iFiTqwDTGyEKU/1t1kBs1AO1j1UE8T9RMg7qaYBTCTSUxjBoJMUkNxIaNNEKUFXKL7YZ8iLKfLX/XnDRwlWncq0BUKudT+TAV/ebl2p6dGoF+DUk8fkuzEtFCec/8qPSHjhla6YUwCmnHTmiPskL2hcxRWPoTyX2hJOg1F6hZqSYTwV5K0IW6vFEV2AkEvqgsBUuYUYSP8g1lJl7KV7A5hXOP/NnOQqjJq7+FoktHxcLudFjcPTakv8kzhc7nhd37g+4ZJenQ+aECoWKSpmwSQase8SWWksgdJlgbDBcRQIHCdpHUyGgPyFcymOnJpJQdcCt1walAwLmk7SUv9jIDCryu51mMF4uYBg7pgUshf2K+lTTAlRKFEanmCGRCFe2dkoJXF+jqajMQLRIFFA47SxmILJGy+/wEZkRZeJ0sk663kNTNF5YvVSDvKmgvFVzNf+Qjw4AcpXo1SOFpH+7XQ/lzeQL2pzwBd4r5OWe2A+Pi5RMs1H1Bll+sPnf6+WH1z7j5TxD0Dd0dPD6N7IZo/okfkPFF8133ZdLlPiCrJXIVLv/3ARnXKH1AVuNlL3XXspZJh33sf1+hfJWvUITKVyhCjXKf6SsUKO4+gE/yk7s/M8A3/wc= \ No newline at end of file diff --git a/docs/distributed-system/images/distributed-lock/distributed-lock-setnx.drawio b/docs/distributed-system/images/distributed-lock/distributed-lock-setnx.drawio deleted file mode 100644 index c8c345c0df1..00000000000 --- a/docs/distributed-system/images/distributed-lock/distributed-lock-setnx.drawio +++ /dev/null @@ -1 +0,0 @@ -7VtZd6JME/41nPN9F8lpoEG4ZNGEOYIxOpPX3CESBBccxAV+/VvNpiwmM6MmM3ln5iJQvVXXU9W1NFKsstjfBeZqqvsTe04xaLKnWJViGJpGPPwhlCiliGIrJTiBO8k6HQgDN7YzIsqoG3dir0sdQ9+fh+6qTLT85dK2whLNDAJ/V+724s/Lq65Mx64RBpY5r1Of3Ek4zffFi4eGe9t1pvnSPIfTlrFpzZzA3yyzBSmG7fCdTkdImxdmPlm20/XUnPi7IxLbplgl8P0wfVrsFXtOhJvLLR3XOdFaMB7Yy/BHBojfo29Tun/3vHRX40f9WeBexjdMts2tOd/Y+T4SbsMoF1GyR5vMQlOsvJu6oT1YmRZp3YFSAG0aLuZZ84u/DDOUaR7eJ+Z6Wox9cedzxZ/7Abwv/SV0ktdh4M8K0bNAyRiyg9Den9wqXQgQNNP2F3YYRNAlG8Bx2bYypcStDIPdAWKOzWjTI3Qxat1ymXJliuUUsx9kCw+ZeJtF/f1usxhHgTYbc4Pgu2OYq5vtDXNhSdcldyxe0MaXlxfGsoqeFcGfkGmD5E+KuVDtTMyMWBczLbJ1MdP5wIsLma0J+cs3HQhUW6DkDiXKVLtFSQIlyDXhw6bDJglX5Nagw+bcdZbwaoEobaDLRIQunDFS1rBwJxOyTCOkB9BRxX6EC8HEt0owsagOk8jVUboESI2HDl+TvT2BQzl79YNw6jv+0py3D1TZ2gTbwjIOXbq+v8qInh2GUSY6cxP6b1rLkaS5QtKEk9dPHWDc3wSW/YoW8s14BPbcDN1tef4m6WZDH3wXVi5wZGk+P5wyJDmhAlFoBo4dZuMqKBWM/DpwrZp1DdpD4x+K4c0FUeXleL0qJFmBt2uOIXQoYfLjdhPYazc2x8l8xEpWZIfJnjmZ4tSTaDZ4kSyQyCY7uOdjnE9r7Ukju0G3ENOU4ckO+DNhzx1HNild8U/+y8vaPhft01pccVj8PMyEXUKY/77x84abdQKDBB1obrU/NMKTQ/5+MbcmOYZliRzJF5ny6FRPp4MNpzOm7fQJ+pU9sGBbps1f0wNjXDkSWNzggzGun+7ctVwwjd8OdCAWXpFHd5GE54XhJ4fEg792Q9cnB8DYD0N/AR3mpEEuIu5ylAP/Gg6PkHgG2Vyv0rThxd0TXOVkSSmnopwCzxMzNEHJ0lems946FCPvAW1Gebg3mOdIxuOn/caKkWvePyJL9bdddsJOIo7VI25rLayt7kk7XRHjycJytftpOL7j4t5yujafuOBh8MWf3D/ueq6whVFsd2nF3YUYPUfCvjeccV027ae5MnoYaHvDGxF63FNne0MdxUD3xnedOFn/ydiOXc2ZLObzCfqytVXk6oq009T2Tvf6jj6UIkMh7d8ii5lvxx60D/Ae5th1n+YzmD/SYwfBO2s+PSITxhuq42p307n5NPEn2fuDt9+N/nn0tbu+qM3Q3hjqG8OVkOHNNnrcjiZen+kNcGzEz74Rt5muN30CXrmup+/1mZP2X64Mg/AE65mqwxnLEWOosmZ4j0+EZrtyaKhS3PU06KPB2Fk0ilBkuJg1hlqoD/sbI9agT3/TUxDSXRzraj9/B9ljTnelXXfYZ7tee2coEtIVvNc9J+ypXxM+NVi3642O3vvw3mfJuKyN8LcB/HD+fugrQVt7Y3iwv2Ebdz0J9RSJhn3v9XiU8SGlfCxHe334ddMbjvBo2Eddz3Gy9l0yzwBhGMdoqkzm2hsDzBnelB8O9Rj2BWvr2LhzYmPobIzOKDKGz56uIM5QcERk0FMl4PErGcvCHpme6oB8LOCXyMnaAd8YZML1FJhXBZkrgFkMWLkSC/JE+nCU93eK/vfAc8LD1z2Rxyhuw/4wSnnWGHgGvoBXBdOAfWR4DtkP4Y3RhxoeqBqCvUffPI02Fjrg0EagawhwgHmBX+Cb8JzP1R1Cv6UPuva8Gt/tRM01FiPPoXVVApuRQvMJdM3TQO+/ut0YC10WbC/GxNovcmbSiKmcma2G9JBtCIjZa6WGdD2w+mm/yINfTBxizTVylMBRkkK1MSUrlNhKKB1KhodOQpGIR74jhZh81XFw5prg3VVKpBO/LFOiWCzlwVJJzefWWv6oU/49MrMTLihfvFL3IO9HXkpO/idzhmbm3Mik65kdWtP8Ja8QoctoOubK+TluSPxooaEMQl8r86un54kyCpSEqTZPiTwlARHN7ChTGGhqi5SIKYHOKSIZIsFz8gBBJLQeqe1BA0WJEnA1N0mXJgUBWE4g88AkIqIkPhkiU7KQKK1KFqpFh1N/Md6sf64KxpY0qVoGSyIYm3+rTnNWOazFlsthnMDW9aDVoAfitfSgLto/twLQuEHhKhUAulJwa/Hvm//T+RF0bL+5HX3KpF98M+kXmUrSj8/L+i+a1zejiBqx+kjrY/8E68PiB1tfww1C4bw+pfXRqBnIg/lhhqVLoNyca39l1qPG4dezTaER4kq0c4ht3oqUaoXZepxUD4M4Ev0AkVAUUlb7JGFQvVjWGAjhpkCodaVAKK/mlgAXKKGdpGdJngZB8QFYjkTKMp1cZ2FK4mrY/B65UjUbugB6GFdO34aknUYN2F3tGqsxa79qPflHbnTPyxSYVsVE3vdOt1nO9UPxU1zqnodUq3oZ+OHXuvSnz+pOYXJmrCHkMcsHXeviphubNFJYr8xlYynw8PXTjZUaDqkIBs74fwwWiG0ywAuCmPnwzKH/N5UKRQq2C/FMaswCHKqouzEz84YghzTxSSSikAeIaqQWaZJoSlCOSogpqydKiO8cDr/q8349HH49G224gabPi4WvH+8yDRWFv5fMF79kPs/VcEzZ0bznFXOz1rB/tKe5uAPhq3VBjr9tVS6rTviQ2lytWpbEM8ItU5ktdZVX80hMg0f63IXGVKU/WaWRqeenH22nP1NpvHz9HiGuevd8hqmKqGz2H2GoDR+Jfe6aZCrg/1JNkmkqSookUhfS0BxTcvtvjYqID1fLJx9fpcJNtwb1NAqp7W6Sc3Hksw1ZTb4WkZN7+dNQ/2eA5VimAmsB9BGwBYjvA2xD6fitrL2a54gkz0m/NkGvZPQcd5TFl54bMvrsk/AmVTqRrJM6t0RJ6OgMyW84iqLAb17hK//M6UgBxQvdXSCxCBPy8I+rHy0Mzd6+Z8kPn/5G/FcqRz+lZ4l+dUiCT/RLIncmr1WOaqfYKWX8q4On6gD5bz1fU0CWv4j6wevhl5FpIHP4/Snb/hc= \ No newline at end of file diff --git a/docs/distributed-system/images/distributed-lock/distributed-lock-zookeeper.drawio b/docs/distributed-system/images/distributed-lock/distributed-lock-zookeeper.drawio deleted file mode 100644 index 83f96de539e..00000000000 --- a/docs/distributed-system/images/distributed-lock/distributed-lock-zookeeper.drawio +++ /dev/null @@ -1 +0,0 @@ -7RxZe6LK8tfwmHwNNAiPCJowRzCOzmScl/shIIILDqICv/4Um8piYuKS5J6ZeQi9VVfXXtWdELQ4Dx48bTlRXMOcERQyAoKWCIoiScTCj7gnTHsaHJ92WJ5tZJP2HX07MrNOlPWubcNcFSb6rjvz7WWxU3cXC1P3C32a57nb4rSxOyvuutQss9LR17VZtffZNvxJfi6W3w88mrY1ybdmGZyOjDR9annuepFtSFB0m22321w6PNdyYNlJVxPNcLcHXXSLoEXPdf30ax6I5iwmbk63dF37yOgOcc9c+Kcs+POwno9CT56OmL73x1K15d3mLoOy0WZrMz9Ggqwf5hRKjmjGQEiCbm4ntm/2l5oej25BJqBv4s9n2fDYns1Ed+Z6yVra1MZ4jKF/5Xvu1MxHFu4CljerJ8jRMT3fDA66shM9mO7c9L0QpuSjXEbdTPxIvpG2t3tmUo1szuSAj0yDzoQoEyBrB3tPQ/jIyPgGklIXJmlKulw46SqRx+MxpetXJPJOhDMiUyyqEJnk6RoioyvRmK7SuMUQQosQ4IMlOI5oNohWgxCahNBGFfrDuf06IpdId0jnrEub2dYCmjpQ04T+ZkxFG8yJkA3MbcOIt6nl6p7vKAbvLvzMIJLchTiVm62cUw1c4RTPVBlFXYtRuEJ70wD7mzVdz5+4lrvQZq19b3Pf23HdZcYgx/T9MKOWtvbdktkpkRIo6IW/MjonjWHcuGfyphQcDkph1qrqmr72NjtNNQPb/3XwfQAUWnuYcSMHeZSrK3ft6ebrhsTXPMv0X7LhmTbElH1RSDxzpvn2puj56lieLX1ybcB5J1wsWTQDmC5JTYppturQE5UA0Rx3zxRA0SxTBJUSpwIqkcHdic6w0WyNkWZnsVFYLbVFQWLZP+vYRScydrdKhEyACSS9DPaD8GUlP1scwbUS68MQXJvgWaLFEzwmODIzTDxKhgSCb+dbwhHSXTMYdSrT0UYQeRWE/nRb5JmAtzZK4MUyuYzpmlCaaRKMVCulL6lzxSDt4rNsk0KIU2eo7kBtaLYgA3c4E6czxZTGBbAsVwTgjscr8ypCRdZ4pWt7/lED0Vf0/GzJ89PUTnMPPArD1QRYNE3eM8d5d5ZXIfHrlIZwexl/2vMkA9gpR6JIT+7K9m03VpKR6/vuHCbM4oHmLqgvBljwr0bB/Ng9NbXVMs1MxnYQs7aZbCnkvSjvgW9D8zUwH2kTtH5jEVQzAIZT4tOjSv0Om3j0HKz1CNna43ekS+6mQxu0ETK0EjIbfa5vFEfYKiIfGXPdlh8n/uiBibqLyUp7Zryn/jfXePy+7drcBlbRnYUedeZ8+Dvkgu5gynTodJ5sN9FTXw5UZxj3R11pGqjSMIJ+Z/TQjpL9n9XNyJYtYz6bGejbxpSQrYjCVpZaW8XpWcpACFUxHv8Z6tRsM3JgvI8DgLHtPM+mAD9UIgtBm9aevyMN1quSZcsPk5n2bLhG1n5ygu3w13dXfujx8hQF6kBZq7aAVGe6VqJWaDg9qtvHkRr9dtWoRXWcyTPgynQcJVCmVjp/sVTVGCfYT5MsRl0MKVVqyqrz/TnuM+2mr0pC1HFkmCPD2mk4DFGo2phWB7KvDHprNZJhTm/dFRFSbBwpUi9vA+0xo9jCtjPo0R2ntVVFASkiDhTH8rvSjwRPGfbtOMODdg/aPTpel43F+K2Bfzhv7+cKMNZaqw6cb9DCHUdAXVEg4dyBEg0zPIQUj8UwUAY/1t3BEA8HPdRxLCsb3yZw+gjDOkqWmjGsQO1jRnUm7GCgRHAu2FvB6oMVqQNrrbaHoTr47SgiYlQRhzENupIAOP6I19JwRqorWUAfHfCN6aRvAW8MNGG6IsCVgOYi8CwCXtkCDfREymCYz7d28x8B5wSHH0FMj2HUgvNhlOIsU/ANeAGuIiaB96HqWPF5YtwoZSDjviQjOHv405FJda4AH1oIZA0BHwAu4At4xzjnsDoDmLdwQdZ+L0cPW1621fnQsUhFEkBnBF97BllzZJD7H3YnwlyHBt2LcKztFzGbXKPo2hieqhhNzFVtJuauZTAbR+OdOKw5Kd5hId5JAp1KyAPhDCRfItHCRFMk+Dz2iYOgdtIjfNM22kNc6cl3HXln7tkgmhLBk1mSx/O7rRzYKikq3euLgwArPeaRAOtz5INHHFC++UGKw2btAx/VTP4nMH0tc21JRjM1fX2SN/IS1IuJyelyznDFMH5XaTksDNQFB+S18k2Ke0fCWcjx3pF91sRppXzU0FaT3Q5HCf9pMr1qgsYwpXLZtRM0/gP4eLmqwfv4f1gRQGjd6//z8396v487Is2pjX/Wd41PJic43ygv/jfQbaWEeT0PMBeGEN8T7C1pgRkH7I/7nzQf7Pgi6aEOUqucudRb6navkv8wh6qrUV8oISZxSZtZdJo2nwCK4UqgjhSB3sHyWhUgySqHXzUMxaz7hpahLCpVS1EQxoIBq5Wh99kN+k124wwzcarvfgnLz3VxUlPqqnDgeDmerBQ6uRtendRSuaZ0cvzqpKpsnyNUrjfB5/GKbZR5xd/u8qSWVSd4t6+mEKV6IoPq6om3VYiaq4DjClHlwP+xQhTvEhmKquHVTRWirorxxRWi4iFY/NEegnuLQiz+SwpR9hC7t0AfpRDk2eWOItXOvHpna/Ml4nKBLJkp/GEkWz+RqWflbSJZ8uzqxdls2Wcs+yRlSBzmKK9nLJ+InexHspOqviD6y86z2HlunpksFTxPCw8mZK8ajtZBOL70fqX8YLI0v4FenA8fKQYXrW9Q76lv/CcseuMryAxJkvzthSYnYiEwbs9cfbo6L0CuXDoVw2NDM7nxNcNjDpfen9QEXCSqKZ1i9lquva5KlZM6+Vl1FpckOUIczbWvSHKWKUk8i+pS9JsSnXrPM9LbOGDyjQ44f0K6fza6g3L0CWnVANfcJ13GGjOf24OXZDN/eHaqBy/Nv5IHr8ufGwTfTJ4rMPEDUEGslecrv/Esi+LxB2/tdjN9V3jUntQ/9yz9OspxVT5qfdA9amCywLS8Yn3mLVhpwUWegb6oQ8c9RDXEu6SHEDFmMHNNp1yOiz+Dh2D/eojbeAj2VA9xbgXmXR6C59/mIV7xKFfyEPxfD/GaKh+1PnfgInBe98xdBH0RF3FXeq9+RR9R9/snBR9BfW0fweNP6CMaH+4jClb55ZdF7IXt9kXKJ9WoCrElA3riL4W91bCTJP02y17F7M0L6Bv4gvxUf33BcZV9IVsgGap4JfflsoW6O+2CJ6jesH4pT0Ai7hO6ghPemt3q4ejr5r1K29s8HCVRuRZYfux56rtREjVegfTuZ6PQ3P8NjXT6/i+V0K1/AQ== \ No newline at end of file diff --git a/docs/distributed-system/images/distributed-lock/distributed-lock.drawio b/docs/distributed-system/images/distributed-lock/distributed-lock.drawio deleted file mode 100644 index 397d121d1ae..00000000000 --- a/docs/distributed-system/images/distributed-lock/distributed-lock.drawio +++ /dev/null @@ -1 +0,0 @@ -5Vpbd6JIEP41PGYOlwbhkYsmzBGM0Yxr3hAQQQQHUYFfv9UNeINkshNNZnfNQ+jqW/VX1XUDilNX2X1irRdG7LghxdJORnEaxbIMQwvwD1PykiJJnZLgJb5TDToSRn7hVkS6om59x92cDUzjOEz99TnRjqPItdMzmpUk8f582DwOz3ddW57bIIxsK2xSJ76TLupzCdKx48H1vUW9tcCjsmdm2UsvibdRtSHFcj2h1+uJZffKqherTrpZWE68PyFxXYpTkzhOy6dVprohBrfGrZzXe6X3wHjiRul7Jvy8365meaIvZ/wo+emZ1vpud1etsrPCrVsfgzCb5jVC5IguXoShOGW/8FN3tLZs3LsHnQDaIl2FVffcD0M1DuOEzOVca47mCOibNImXbt0TxRFMV6q93SR1s1cPxRygAh1045WbJjkMqSeIFbqV+jG1+u2PwmQ71ZjFiRyFDlcpUaVA3mHtI4bwUMH4DyBlrwxpCV2tnFwT5Pl8ztr2DUE+qHAFMivQDZAZiWuCzNQDrw4y1wD5+w8DCFRXpJQeJSlUt0PJIiUqDfABh7QN4QvcTkGuSFboexE0bcDNBbqCUfXBlshVx8p3HLxNq0iPQqfx8nGUVtaQEa8kptpm1WLqoIaYJL4pJfZWQkIN7F0HjG/VjJN0EXtxZIXdI1U5UvtxvK4EFLhpmldoWds0vrA5F1ACXEn+V4UzaUxx4xtfN7XstFPLq1bzotnbZHe4puVh8Anelg8cON4mtvsGMJXPTK3Ec9O3rDPXLvDEDa3U350z0ia+aupj7AOLB0URmPP7jLgLDSgZq2ZdKMGBjQ9YSKHFRAohvpWbtRWdqYzwc4sdJBHy3YZIWYYBDLfOjp3w5JH/cPvFLqV0qC5PiWAGBKorURKiRIbqCpQoUhJNumRK6tVbwhHKXas12nS2b80g7jnTuvcbg8QFvq0ZWQ/r2RrjSpDmFYrX3rr8VdRTTT7GEqdK9/rVe9VS3MFl4IQzHbhD1dK/q1v1kHg+37g3UZum1ny3dhY29IqMjX5t8ZmGCK/rb0XXtlzhlv4WoW/8mXQ41OJxEWracv5WtpxpCWtakG/eMriScpcMgpvYwc+f4hSu5wRO3UvDKYTYMCiHTOBES3rkd12/wXDvdRzCBx3Hx3SlGZ3dPASedWjuhldSuAiBUUuewaCWPIO5WXTFoF+jDDnnGj/6K5IGH3wU8WeP8cZP/Rj7qlmcpvHqTX2ek1+Ln0vxjVSszbpMz+d+hsWqkC3lmkrXFHh2rNQCL142wfnuPIpVMhA2qz4+mOxLrqDZJNvaBe1bD0+0rcW7PudwTs5zRs7v7JW9MwJ5b6hS4axsX39YpLN7vhhEi4014ZPH0ffYeXjaD3xxB7O4fmQX/ZWUv+RiNhgv+T5XjtN9hX4c6ZkZTDG9GGjLzNSmBdCD2X2vIPtPzN3M1z1nFYYO/X3narRvqPJe17p7Ixh6xljOTRX3/8htNtzNAugfoQzW2Pcn4RLWz43Co6HNWZMn2oL5pub5+v0itCZO7FTtxyDbT/96ivX7oaQv6cwcG1vTl2kzWG6Nops7wZAdjFBhFi+xWXTZfrCYAK98PzAyY+mV46O1aWKeYD9L83gzmrKmpuhm8DTBNNdXUlOTi36gwxgd5i7zaU7npo84c6ynxni4NQsdxgy3A5WmDR8Vhjas24A94g1f3vfHQ64fdPemKtOGijIj8NKB9kz41GHffjA9aQ+hPeTwvKoP87cF+aG6fRwrQ193awZwvnEX9QOZHqgyA+fOjGJa8SGXfETTzBg/bwfjKZqOh3Q/8Lyqf0/WGdEI5rG6puC1MnOEeDNYCOOxUcC5YG8DmfdeYY69rdmb5ub4JTBUmjdVlGMMBpoMPD7juRyckR1oHuBjA78YJ3sPfCPAhB+osK4GmKsgswJk5csc4Ekb42k93juMfwCeCQ/PGcZjWnThfIguedZZeAa+gFcVMSD73Aw8fB7MG2uMdTTSdBrOnv8IdMZcGSCHLg26RoMcYF3gF/jGPNdr9ccwLopB117Ws/u9pPvmahp4jKHJcGfk1JqArgU66P2z3y+Q2Ofg7hUI3/brmEzhPMLkeb5hMpHYtJg17foGk28JYXgcr4gCflDggSMPKk4eDpnD/6aAIKDORdzJs824U/zMEgLTkipimYGQINhEOP6USdqnABHh8BMoEt2Q2XXDDcdyxfmrFbdTyQjXkYwknIcfPMd+dT7QaYk+yoQan/9dObwAOTxJ3htpPJGwrBIJqzhtKPN5nNj3CEXGqd89fndQ7zpLPrgnJDMaJTEkmVEoSTpsFcBW5DXFNzs6KRqUx3ylaPBn2IhXorlX9BS3T5RcIX9kzdSq4kSSIS3d1F7UjfqlBn0lE0RflqakpqKLbXH2zRRd+vxk5pPr+ah2uV9Xz685ekdBvwn/n3HZPqGij4Sm2f/Uij4rNsD/dfXmrGDyG6Wct0syGGrH2ixuUac/vJG+eaH+Y1JpvsVtr4ze2nJ9QWWUZ1ts16dGQqz0BXfiNuXNm94l9t9xl1rq3P8xdy9IX/76nn3/+/vo/+vuO/QXv8BnmyXmdsfSFNK/3rF0Ol/9yo1tqVc1PE3kyPgruCMYZ/b7BGNMf7RS0PuIUNiT9yU18uyrev1rF9CE9gQ4viVj4z/4wrmSHMeclxmFS3mUnqnxMcOvF/rkryK4ZgqEyx4yoiSZFEJESpZbvm9qKYeRwZAl4wcFlzWaBbKyuCGqpMypksGwvkC+lCDrSCweLAmUzL/jHe6fZ5P5K1XeUOdcK+iWeLPtA8PfMMrQPH4OWqrV8aNbrvs3 \ No newline at end of file diff --git a/docs/distributed-system/images/distributed-lock/jvm-local-lock.drawio b/docs/distributed-system/images/distributed-lock/jvm-local-lock.drawio deleted file mode 100644 index 35ec176608f..00000000000 --- a/docs/distributed-system/images/distributed-lock/jvm-local-lock.drawio +++ /dev/null @@ -1 +0,0 @@ -7VrZdpvKEv0aHpPF0CB4ZJKNLyDLkqMjvyFADEKgi5AEfP2pZtAEdnwSKck96yYP7q6eqnftqu6iRTDyOn9IrY1vJI4bETTp5ASjEDRNUSQHf7CkqCWCMKgFXho4TaeTYBKUbiMkG+kucNztRccsSaIs2FwK7SSOXTu7kFlpmhwuuy2T6HLVjeW5HcHEtqKudBY4md/uixNODY9u4Pnt0hyL6paFZa+8NNnFzYIEzSy55XLJ181rq52s2enWt5zkcCZiVIKR0yTJ6tI6l90Ig9viVo8bvtN6VDx14+wzAyRtpsx87u3pP+X60VELXnx6/tLsZW9FO7fdRqVsVrQIVVt08SQUwUgHP8jcycaycesBOAEyP1tHTfM2S5PVEUkGJMsgiuQkStJqNgYAom372LNtiZMYJpS6e2q2uXfTzM3PRM0eH9xk7WZpAV2a1iPeDSFprqkfzswrMLXMP7MswzVgWA2lvOPcJ1Sh0AD7D0BmOyA/fTNAQKg8IQ0JQSLUASHyBC91wIdNZ30IX+F2DnIjsqLAi6FqA5QuyCUMYQDEF5uGdeA4eJlek56MTuLpkzhrXJfib2Sm1sFaMw1Qx0wC27VSa92bG4nqMt91IFQ01STN/MRLYitST1LpJNWTZNNYKHSzrGjgsnZZcmm/aywBwrT4qwG6qsxx5SvbVpX8vFEpmlrX0+xduj/66bsm2ia71HY/AqKJwlbqudlH/ZqOGKUPLZ66kZUF+8uA22e/ZuhzEoDOR6Zw1KVDI+aKArWmzagrFhzV+HFitIw7j5HguLxKSANCZQkePJgjVIEQEMFThMoRPE8IZNUkEsKwl1a6tYCD9IIYn3fY1N0GpbWo5sNU2OCtV2CwEsEqVyTD1IjwctLxwDqLx6o4REPUS5gP3aTj6ccjulHs4pTriwBfgOMU3USBn+TIF/4rezkmWS637l34QHXo8GTtLRzCJRGH8zaWUx3D3/Yk5V3bcrl7nqQItag2rscgpnuWItSN0uzdojT3a6L0j0bl6+juWFv/aPG7hWzut4XsnzIm90lPuved9Dd4EqK7151f7EmD/3tSF5XB/6YnDT7pSfG/z5NYpie/+6WexHfvhx3Pih0Rf6w4YXFB5zOIsfzZyuAGGFcSmmQ6wNP/JCX7LkPPcGsh6oPtJ29oTHtyt2ajrrLt2jU7t/jvToSEq4nunA5QPebuuft10wDIGUS16gSpwgCX/+j42+PrV0nFRcj9oQzj0ynpp3NS5o8Ky1Q3V7hzAF4uF4OziHH7AMy132nalEDoC789cYS625cb4fsgb31rg4vBuvocfEytqzT8OdkGWZDgeLtIsixZf0jnZfWvJz3PsENK1nZTf6ZeBjm2qlQtKbZSspVA2bEyi2DEukoPt3uPoKUcbE3Lz48m/VZIaDHLd3ZJBtbjC2kryV5nHMYpWMYo2L29tvdGKB4MWSidtR1oj362eGDLUexvrRmbPk+eEufx5TAK+D2MYvTYLvW1ULwVfD6arlidqftpgUQ+T7TcDOdYXo6UVW4q8xLk4eJhWFbrz8z9ItA8Zx1FDvm0dxUyMGTxoCnqwQjHnjEVC1PG7d8Km472ixDaJyiHOQ76LFrB/IVReiTUGWv2Qlow3lS8QHvwI2vmJE5Tfw7zw/yvl0R7GAvaiszNqbEzA5E0w9XOKNXCCcf0aIJKs3xLzFKl9dCfga6sHhq5sfLq/vHGNLFOsJ6leKwZz2lTkTQzfJlhmRtImamIpR5q0EeDsatiXpCFGSDGnGqZMR3vzFKDPuPdSCZJI0CloYzbOmCPWCMQD/p0zOihejBlkTRklBuhl42U10pPDdbVw/lZfQz1MYPHNW1Yvx3YD7X1U18R2tSdGcL+pirSQ5EcySIF+86Nct7oIdZ6xPPcmL7uRtM5mk/HpB56XtN+qOaZkAjG0Zoi4blyc4JYM/S56dQoYV+wtoHMB680p97OHM4Lc/oWGjLJmjIqMAYjRQQdX/FYBvZIjxQP8LFBX4yTfQC9EWDCjmSYVwHMZbBZCbYKRAbwJI3pvO3vHfs/gs6VDq85xmNeqrA/RNY6azSUQS/QVUYU2L4wQw/vB+tGG1MNTRSNhL0X30KNMtcG2EElgWsk2AHmBX1Bb6xzO5c+hX5xAlx72yweDoIWmOt56FGGIoLPiJk1A66FGvD+NdBLxOsM+F6JsLffJGIOrh4kEOI7EbMVnQfMVnb7U6nvgyZHCDIhyvi6AgWJPH3QvI6l/9oniQF5+SSBqO7RNug52e73JMH0GAqukyyBL5gI3zlFqXpEAiHCNgRJex7f7YrhWC6/fPcF79wu3G3sIgyuHOi3fzuh+55NuShr9n+BPvffXdI2fNlWyIjQgeI2eQVP2w4lr/pbWxi7IlhYxqlC/ciAXxuGlUTEyf0DfjhvV12kP7kmJDAKIVBVAiMRgnBcKoSlqjf6r3bcrgaY1dtsRv+ZEeKdK9w7PMX1M5JL1f9qzsxqLodVVrRyM9tvK+2LPnmjuzV3SXSm79MG33e3vgHRfZ/N0TxJNdeSRK+kiw1K+34e0H2zxvTkMGdBIohVJAL+IlzGFIa0V+xmyH2BrOoM+8MFCROyG9pqWvJydTzJVWeYn6se3qp5hPos4wiR/UTG/Wcw9ZyL7JFKPS9sHXa9HzPZwWXMFLgulW50mEH19CuW+jPL6bdCjPo3 \ No newline at end of file diff --git a/docs/distributed-system/images/distributed-lock/redis-master-slave-distributed-lock.drawio b/docs/distributed-system/images/distributed-lock/redis-master-slave-distributed-lock.drawio deleted file mode 100644 index ad79bccfb77..00000000000 --- a/docs/distributed-system/images/distributed-lock/redis-master-slave-distributed-lock.drawio +++ /dev/null @@ -1 +0,0 @@ -7Vxbd6LYEv41rDXnIVmbm8FHFEzoJRg7Oj3mjSBBFCVHMQq//lTtC4KXdDKdpDNzXN0rwr7Url311YXiIqnt+fZ66T9N3HQcJpJCxltJtSRFUXXjCn6wJWctV0aTNUTLeMya5F3DXVyEvJHw1nU8Dle1gVmaJln8VG8M0sUiDLJam79cppv6sMc0qa/65EfhQcNd4CeHrT/icTZhrYZytWu/CeNoIlaWG3x/D34wi5bpesHXkxS10+h0OgbrnvuCFt/oauKP002lSbUltb1M04wdzbftMEHZCrGxeZ0TvSXfy3CRvWZCPAh6iuMOLh4f19+19o31lzW74FRWWS7kEY5BPPw0XWaTNEoXfmLvWlt0zyFSJXC2G9NN0ydolKFxGmZZznXtr7MUmibZPOG9j+ki450KkljNwiyYcHqrbJnOSlUofHjHn8cJIuxbmLWWfrxYAdduukgFuXS9DJDgJMsAOIqumvAHZIF/cMDqMkrTKAn9p3h1GaRz2hGs6NDOI6MOh1X6utLiK4CEl/lfyOClLk5HnF96Ym1rZzk/Y8JFiZ7UmYC8v4zC7AVFGSViwBLDdB7COjBvGSZ+Fj/X6fvcJKJyHJ9qLpd+XhnwlMYgmQrlW2yAAdy6L+Smxu2AW7cu74HsrTPggHHx2vmasB/B9Iqqms97iRWFNPdY0fZIMakfkIKDipB3TdSg3mBcClvt2U/WofARjSRDkD/5i5rZNf67Rj9AsXyxorYBACaAlS3FkeiHo4gSyBeBIAa8MXqs66hFd/0H8Ns1K/STOFrAcQBYDJfQ8Bwusxgco8k75vF4zAw+BI78B0oPYc1hA8T1lqRbdYuWG3Ce4HKt0kG20yRdUmaEi/wKVs21A7sOt8dCDd/xzoNX7fa0Nz00Uk6dXJKrK6OGSI6Qt1nxEaTXiIpTQSB9fFyFHwPwxhGE/9Z4QtFXumu55qx3vvuEu2aoTZfjcCkQu0gX4T8Lz6+JNivBxynFNl4ZleQTYUkAk1wqTf04On8Z87K+592NPdyf8O47Up9gIFdvDwEBAxe6/2X04P8BzgcWJ3s//6EKJ3vxQoQLAi4tu+Ae3qTL+EsWHCp560V9qT8UzWDUFa3JD3S+0H78QRTuXRPoIBdspRlteSbkpFNJIXbxGD2qjrJg4H95rFyOFVj+W2SUHRmmjtoCtgp/DKRn0FMdj9A6TWyzQR56Uyt7G3hkYBupDW52doswzZaL7KIwNDHvhX0yPWWawnMejbGxHo+xk0dk7NzFZOwgtE2kc2xJHZWkW7Sn9JB0yQZtq4iDhcBjAq+GwiNSP1BIObXUxc4r6Gi85cgLoNO81Hek8l2felVZwl/tOqIKsX2109NS99XGOiL5uAPonkimztZ6ttaztX4xa1Ul+A/6R/1+2Yugr5IRvucVDsv4X7jEkbVGPS270N4n37vSP+uqRlXfnrS95KYbwjvrwl8bx910BdW2JrVsqWlJ9pVkwHFDArdldCSzQ7takqlLtiEZttS64l1NNsaQWgRnNWGMLYHvA5dowJimBB7xbC9fzl6IrNbtRX4Xe5E/zVxeUQPYXeEjhjaTOAvvnnyqxw049perw49xklQvuDsdpd0uC8V7F+r1UrKouONJlPirFT/+wkg7uFo/fXENCeR+fZOLfrO7kSELqE4qNzEMchpONXi8FQvHLnf3sABKecLDeE5vzJQOhDqb23QVZ3GKjuQhzTIQ6snayzh89NfUle27oAzLRi1/9cRuFz3GW4Reiy5oilYiWpCUn/ngsNkp+MXnSFJaW0z52rc3nnKft7SHH9t1UDxpD9fJ2i9I7N98J4GVPnfVsTrOddXN9edgHjy7g5neu2uycbkThdfy6mHhNp35hIxvzEY3b8KMYD0u3PWD+m3RLZyNa5nPgXq/cOJWev8jWfg3/aYz7Wtwrvo/vhPfIrFnRbFzc58EC+/pARJ+Z2qv3TsnGs+TZEy+PYcwxm2bG8eyN+60H7kDM/faTjRStpNAxfWTKcyLb6fb2f2P+wJ+i/FNsrq/a5Hwr1Zye/eN3P81IV3FK4Lceb6dbp7vr/+cB4VmBNcd4rdbc//HdnV758TONfA3I1vXcoruNNC86UjxclNxB/a6Z32fdgfm2rMcBdpVx5qtgeIGj7sDG8a7smfR8bnb1pTewC3gX+YWM5g7w2PiWC7pTodKdzCE39GWjoDxvbZGvEEEdAMY76qwNqHzc3PrWX2cD3Nc3cs1vTfo4/ogh0DpTh29R9ckCqyZ96wocgsbeAQZUb5muI+CjTEL906DvfUzd9DfVvjV4DjvDvoq8uTFJHdjTfUsm/Pfh/6IuFQWhPID83TkHeZs3BxoUp6A90GwAVpKr01y4HVTymDgrHsDIas+jJltPbp3WCvXNDEO1ly7BcphhPsFGTlbyrsKsrLctTcw6bqjwkQcwFhHxnPABo6VXSH/O5DFwESeoipNJn8H9OkWXtuUPZCZVziROx0Cf/0towVyudNkvn9Yh8ol51ggyAfggTAM2CrIvfCmEawTrL2pDWs4OcVO29SAjw2dB3y4U5hnRTmfR6Cv8LjcepSGw/idwr6mfZnJ1wU9gYzZ2oWHaxeuxjFHQMYqyJXxWcyAz5lOdRqDnooR0JlpbD0HMaXSdsQa2BFgBPZhKoyWI0M/4BD210aMjDgWUS5MxuOY2gXsYUhpPrTJBvXDMIzHpuahHtU07v7ctgjIh9D9MNsoRpRGX8hZR36ZPmFOXsoK5Ag2OB1yLLkwLhJ6B51qskvl0c9gPOqjYDp1AMN9mdsKtICuir4usAPzAIOjrC5DsypDwOMI96uBjNCOAAcO090AcMl1AtgBfdkbr412grgbcpxEa9whx/872Vl/y+VzxNZgv2+yM1v1FjUbI6+xMfDdx+yL2Wz8on2Bf4uYPj/JvtzYpD7Ss4Zc3js/iXwjxtg+UL7IU4T+uupzs5rPjU2UuwoYwfEoNx4XUO424wt1c4djhC+q2pwLNjdTejc434Q9mQyfyiajfCIe27A/irNxw4kNiN23N63J+DqK7ufJ6gEiI0TSGUQ4Dym4yI0FEXI6AiSZKkWx4gI3w62L1guWz1bHHVLLE54ErGhIXBpxCHoA0hPjpjOUOre2IViuQ8C7b3s7DWLEwmhWeMJaqWQFbRcsBqySejfc6XBLUcRQKVNUWhG0YzS1IQoFIJVoyy11C3N1V1gItXYWAUD7BWqoX7g6WFEO1sPRhJE42AgvAn0K7IF5yKlbWdssYA9C44jQvCIz2PdozZBF0YTWpoPF4hyIbjTCA4/CcmfgJYYiwqO82frJCCPv2mVW96qotWdRas2i8jdZlMIziGMWJXCwb1UcA8csCzOvStYQE8waFFdkSL9kWUHdsvJ3sqwOehXwvFZA9+7TiOXSPXo3Tx5oRMGoB7zAfsCDXm9yjAywJ+51XejvF9yDqi7TPXjJIXoa8LC2wjIFipMM1xKRKYxbEIEc0F0EEQIjkUt41NxSXgt3w/Y0BF6pd2Fe/w6x7ETwq9N1Ke5x3w7xmH1SXXNsgk5wPtoAZIDY3jY5/YBHvgr9RRo93IF9YzY7iPgeIauGfXnUfu5XkAkADmY8M4SxMeiOYhsyBtTrYEZ2GcWMRhRYE8YK/IHOpg7qYkv9wsDWRMZFsQh6ZdYA9FBeRSAyFLmM+IAtsHsVMddDeaAnHGAWLbw7Ykhk5xDNUXccex76telMZOUb6AMe0D4R44ixPo28wK/MMq77hnNNOP4C1K8Ma3IevSngq8x4HAvt14Fx0Ybpgtqs0NN+H+nRbBt9jwdyHio06+FRDvsAH9mxOZV29LWYJWzKq4NdOx/7beqybBvkPYogaoBMhW774C9tGSLfFv1Zz+prZduCZWldFa4CCw0vYP8txQO5eSXXiwdX2kHxQDMOawei7d1rB8bbq65799PkxqmnpRL/OcQBr61+4g25elXqaLWpWpriTa8vkB4rg9UfhamWrMb+alJW0GpVrn9QGfVN+Dx4eO8QnzI5UtxSP6q4pTSPVsk/+eGmr6Led3nOqHkcBL96r0guH9/m4FH3K54f/ORn83Nr4nazKesnvdT/dU1cFc/8/raauHjR4VwUPxfFz0Xxc1H8XBQ/F8XPRfFzUfxcFD8Xxc9F8XNR/FwUPxfFz0XxL1E9OCiKq1eHT9R9alFclj+uKj73V/TlhXNJ/J+CzuZ+bevI856fWxL/Ld+P2H2OQXrT+71fBQbvUjoXj43/9B3dX62x/xpAlAP/JVfecjj9msKRlxsa+HJDk9AWU8KXt37/Owv/ilfG3/WFh584MXIpN+S6G+MQ+cWbPkc/rPDxbz8IM3y3Wz3179+ohzd7Hh8flSA4GX+/LqbechtH27+Bpxy5jdNUD0NdeW/n/VMx7UDT3/50oQF9VauDngz8mQl+66smT1Xn9cX9z1uwUt7cFVhRD7HS1A+h8nFI0U8m7SixVyXtOiTtBxn7N//ZR5C1TATcu5CsYLa8DmAUWb98ov2DnZwRBn7Y+Lc7OaJc6nvXm0fcnHjwpYpd/cOwe+wxrHM8e+94pgux/sZ4dviQyjmefQWs7MczXT3EyqfGM+XYEyzneHaOZz+NZ/qR+unnxjPlMGtXagUI0+QH5WcWTFsC87IbUrONyLQ7UqstGW36ELFM/SMrSRj0UwwGjsHPvVaLqQ/LKh4ZvAUwWdnDxPV21Y4GLokfbmhIhkUrIswHG5wP4BJWbQLTbVossfADEOf6x2+tf/ysXHu1/yjiS48YHi9q1K3pYq8o8neqGhL/wE5l+O7TOqr9Pw== \ No newline at end of file diff --git a/docs/distributed-system/protocol/cap-and-base-theorem.md b/docs/distributed-system/protocol/cap-and-base-theorem.md index 3fca93f54e9..36a2fa54d4a 100644 --- a/docs/distributed-system/protocol/cap-and-base-theorem.md +++ b/docs/distributed-system/protocol/cap-and-base-theorem.md @@ -35,7 +35,7 @@ CAP 理论的提出者布鲁尔在提出 CAP 猜想的时候,并没有详细 **什么是网络分区?** -分布式系统中,多个节点之前的网络本来是连通的,但是因为某些故障(比如部分节点网络出了问题)某些节点之间不连通了,整个网络就分成了几块区域,这就叫 **网络分区**。 +分布式系统中,多个节点之间的网络本来是连通的,但是因为某些故障(比如部分节点网络出了问题)某些节点之间不连通了,整个网络就分成了几块区域,这就叫 **网络分区**。 ![partition-tolerance](https://oss.javaguide.cn/2020-11/partition-tolerance.png) @@ -71,6 +71,12 @@ CAP 理论的提出者布鲁尔在提出 CAP 猜想的时候,并没有详细 2. **Eureka 保证的则是 AP。** Eureka 在设计的时候就是优先保证 A (可用性)。在 Eureka 中不存在什么 Leader 节点,每个节点都是一样的、平等的。因此 Eureka 不会像 ZooKeeper 那样出现选举过程中或者半数以上的机器不可用的时候服务就是不可用的情况。 Eureka 保证即使大部分节点挂掉也不会影响正常提供服务,只要有一个节点是可用的就行了。只不过这个节点上的数据可能并不是最新的。 3. **Nacos 不仅支持 CP 也支持 AP。** +**🐛 修正(参见:[issue#1906](https://github.com/Snailclimb/JavaGuide/issues/1906))**: + +ZooKeeper 通过可线性化(Linearizable)写入、全局 FIFO 顺序访问等机制来保障数据一致性。多节点部署的情况下, ZooKeeper 集群处于 Quorum 模式。Quorum 模式下的 ZooKeeper 集群, 是一组 ZooKeeper 服务器节点组成的集合,其中大多数节点必须同意任何变更才能被视为有效。 + +由于 Quorum 模式下的读请求不会触发各个 ZooKeeper 节点之间的数据同步,因此在某些情况下还是可能会存在读取到旧数据的情况,导致不同的客户端视图上看到的结果不同,这可能是由于网络延迟、丢包、重传等原因造成的。ZooKeeper 为了解决这个问题,提供了 Watcher 机制和版本号机制来帮助客户端检测数据的变化和版本号的变更,以保证数据的一致性。 + ### 总结 在进行分布式系统设计和开发时,我们不应该仅仅局限在 CAP 问题上,还要关注系统的扩展性、可用性等等 @@ -85,7 +91,7 @@ CAP 理论的提出者布鲁尔在提出 CAP 猜想的时候,并没有详细 1. [CAP 定理简化](https://medium.com/@ravindraprasad/cap-theorem-simplified-28499a67eab4) (英文,有趣的案例) 2. [神一样的 CAP 理论被应用在何方](https://juejin.im/post/6844903936718012430) (中文,列举了很多实际的例子) -3. [请停止呼叫数据库 CP 或 AP ](https://martin.kleppmann.com/2015/05/11/please-stop-calling-databases-cp-or-ap.html) (英文,带给你不一样的思考) +3. [请停止呼叫数据库 CP 或 AP](https://martin.kleppmann.com/2015/05/11/please-stop-calling-databases-cp-or-ap.html) (英文,带给你不一样的思考) ## BASE 理论 @@ -135,9 +141,7 @@ CAP 理论这节我们也说过了: > 分布式一致性的 3 种级别: > > 1. **强一致性**:系统写入了什么,读出来的就是什么。 -> > 2. **弱一致性**:不一定可以读取到最新写入的值,也不保证多少时间之后读取到的数据是最新的,只是会尽量保证某个时刻达到数据一致的状态。 -> > 3. **最终一致性**:弱一致性的升级版,系统会保证在一定时间内达到数据一致的状态。 > > **业界比较推崇是最终一致性级别,但是某些对数据一致要求十分严格的场景比如银行转账还是要保证强一致性。** @@ -153,3 +157,5 @@ CAP 理论这节我们也说过了: ### 总结 **ACID 是数据库事务完整性的理论,CAP 是分布式系统设计理论,BASE 是 CAP 理论中 AP 方案的延伸。** + + diff --git a/docs/distributed-system/protocol/gossip-protocl.md b/docs/distributed-system/protocol/gossip-protocl.md index 69b588cc75c..5590401a9b6 100644 --- a/docs/distributed-system/protocol/gossip-protocl.md +++ b/docs/distributed-system/protocol/gossip-protocl.md @@ -38,7 +38,7 @@ NoSQL 数据库 Redis 和 Apache Cassandra、服务网格解决方案 Consul 等 我们经常使用的分布式缓存 Redis 的官方集群解决方案(3.0 版本引入) Redis Cluster 就是基于 Gossip 协议来实现集群中各个节点数据的最终一致性。 -![](https://oscimg.oschina.net/oscnet/up-fcacc1eefca6e51354a5f1fc9f2919f51ec.png) +![Redis 的官方集群解决方案](https://oss.javaguide.cn/github/javaguide/distributed-system/protocol/up-fcacc1eefca6e51354a5f1fc9f2919f51ec.png) Redis Cluster 是一个典型的分布式系统,分布式系统中的各个节点需要互相通信。既然要相互通信就要遵循一致的通信协议,Redis Cluster 中的各个节点基于 **Gossip 协议** 来进行通信共享信息,每个 Redis 节点都维护了一份集群的状态信息。 @@ -47,13 +47,13 @@ Redis Cluster 的节点之间会相互发送多种 Gossip 消息: - **MEET**:在 Redis Cluster 中的某个 Redis 节点上执行 `CLUSTER MEET ip port` 命令,可以向指定的 Redis 节点发送一条 MEET 信息,用于将其添加进 Redis Cluster 成为新的 Redis 节点。 - **PING/PONG**:Redis Cluster 中的节点都会定时地向其他节点发送 PING 消息,来交换各个节点状态信息,检查各个节点状态,包括在线状态、疑似下线状态 PFAIL 和已下线状态 FAIL。 - **FAIL**:Redis Cluster 中的节点 A 发现 B 节点 PFAIL ,并且在下线报告的有效期限内集群中半数以上的节点将 B 节点标记为 PFAIL,节点 A 就会向集群广播一条 FAIL 消息,通知其他节点将故障节点 B 标记为 FAIL 。 -- ...... +- …… 下图就是主从架构的 Redis Cluster 的示意图,图中的虚线代表的就是各个节点之间使用 Gossip 进行通信 ,实线表示主从复制。 ![](./images/gossip/redis-cluster-gossip.png) -有了 Redis Cluster 之后,不需要专门部署 Sentinel 集群服务了。Redis Cluster 相当于是内置了 Sentinel 机制,Redis Cluster 内部的各个 Redis 节点通过 Gossip 协议互相探测健康状态,在故障时可以自动切换。 +有了 Redis Cluster 之后,不需要专门部署 Sentinel 集群服务了。Redis Cluster 相当于是内置了 Sentinel 机制,Redis Cluster 内部的各个 Redis 节点通过 Gossip 协议共享集群内信息。 关于 Redis Cluster 的详细介绍,可以查看这篇文章 [Redis 集群详解(付费)](https://javaguide.cn/database/redis/redis-cluster.html) 。 @@ -67,7 +67,7 @@ Gossip 设计了两种可能的消息传播模式:**反熵(Anti-Entropy)** > 熵的概念最早起源于[物理学](https://zh.wikipedia.org/wiki/物理学),用于度量一个热力学系统的混乱程度。熵最好理解为不确定性的量度而不是确定性的量度,因为越随机的信源的熵越大。 -在这里,你可以把反熵中的熵了解为节点之间数据的混乱程度/差异性,反熵就是指消除不同节点中数据的差异,提升节点间数据的相似度,从而降低熵值。 +在这里,你可以把反熵中的熵理解为节点之间数据的混乱程度/差异性,反熵就是指消除不同节点中数据的差异,提升节点间数据的相似度,从而降低熵值。 具体是如何反熵的呢?集群中的节点,每隔段时间就随机选择某个其他节点,然后通过互相交换自己的所有数据来消除两者之间的差异,实现数据的最终一致性。 @@ -79,9 +79,9 @@ Gossip 设计了两种可能的消息传播模式:**反熵(Anti-Entropy)** 伪代码如下: -![](https://oscimg.oschina.net/oscnet/up-df16e98bf71e872a7e1f01ca31cee93d77b.png) +![反熵伪代码](https://oss.javaguide.cn/github/javaguide/distributed-system/protocol/up-df16e98bf71e872a7e1f01ca31cee93d77b.png) -在我们实际应用场景中,一般不会采用随机的节点进行反熵,而是需要可以的设计一个闭环。这样的话,我们能够在一个确定的时间范围内实现各个节点数据的最终一致性,而不是基于随机的概率。像 InfluxDB 就是这样来实现反熵的。 +在我们实际应用场景中,一般不会采用随机的节点进行反熵,而是可以设计成一个闭环。这样的话,我们能够在一个确定的时间范围内实现各个节点数据的最终一致性,而不是基于随机的概率。像 InfluxDB 就是这样来实现反熵的。 ![](./images/gossip/反熵-闭环.png) @@ -98,7 +98,7 @@ Gossip 设计了两种可能的消息传播模式:**反熵(Anti-Entropy)** 如下图所示(下图来自于[INTRODUCTION TO GOSSIP](https://managementfromscratch.wordpress.com/2016/04/01/introduction-to-gossip/) 这篇文章): -![Gossip 传播示意图](./images/gossip/gossip-rumor- mongering.gif) +![Gossip 传播示意图](./images/gossip/gossip-rumor-mongering.gif) 伪代码如下: @@ -138,6 +138,8 @@ Gossip 设计了两种可能的消息传播模式:**反熵(Anti-Entropy)** ## 参考 -- 一万字详解 Redis Cluster Gossip 协议:https://segmentfault.com/a/1190000038373546 +- 一万字详解 Redis Cluster Gossip 协议: - 《分布式协议与算法实战》 - 《Redis 设计与实现》 + + diff --git a/docs/distributed-system/protocol/images/gossip/gossip-rumor- mongering.gif b/docs/distributed-system/protocol/images/gossip/gossip-rumor-mongering.gif similarity index 100% rename from docs/distributed-system/protocol/images/gossip/gossip-rumor- mongering.gif rename to docs/distributed-system/protocol/images/gossip/gossip-rumor-mongering.gif diff --git a/docs/distributed-system/protocol/paxos-algorithm.md b/docs/distributed-system/protocol/paxos-algorithm.md index fb62f923212..c820209f4a8 100644 --- a/docs/distributed-system/protocol/paxos-algorithm.md +++ b/docs/distributed-system/protocol/paxos-algorithm.md @@ -61,13 +61,13 @@ Basic Paxos 中存在 3 个重要的角色: 2. **接受者(Acceptor)**:也可以叫做投票员(voter),负责对提议者的提案进行投票,同时需要记住自己的投票历史; 3. **学习者(Learner)**:如果有超过半数接受者就某个提议达成了共识,那么学习者就需要接受这个提议,并就该提议作出运算,然后将运算结果返回给客户端。 -![](https://oscimg.oschina.net/oscnet/up-890fa3212e8bf72886a595a34654918486c.png) +![](https://oss.javaguide.cn/github/javaguide/distributed-system/protocol/up-890fa3212e8bf72886a595a34654918486c.png) 为了减少实现该算法所需的节点数,一个节点可以身兼多个角色。并且,一个提案被选定需要被半数以上的 Acceptor 接受。这样的话,Basic Paxos 算法还具备容错性,在少于一半的节点出现故障时,集群仍能正常工作。 ## Multi Paxos 思想 -Basic Paxos 算法的仅能就单个值达成共识,为了能够对一系列的值达成共识,我们需要用到 Basic Paxos 思想。 +Basic Paxos 算法的仅能就单个值达成共识,为了能够对一系列的值达成共识,我们需要用到 Multi Paxos 思想。 ⚠️**注意**:Multi-Paxos 只是一种思想,这种思想的核心就是通过多个 Basic Paxos 实例就一系列值达成共识。也就是说,Basic Paxos 是 Multi-Paxos 思想的核心,Multi-Paxos 就是多执行几次 Basic Paxos。 @@ -77,5 +77,7 @@ Basic Paxos 算法的仅能就单个值达成共识,为了能够对一系列 ## 参考 -- https://zh.wikipedia.org/wiki/Paxos -- 分布式系统中的一致性与共识算法:http://www.xuyasong.com/?p=1970 +- +- 分布式系统中的一致性与共识算法: + + diff --git a/docs/distributed-system/protocol/raft-algorithm.md b/docs/distributed-system/protocol/raft-algorithm.md index 61462eb0a5d..18d2c2eb0cb 100644 --- a/docs/distributed-system/protocol/raft-algorithm.md +++ b/docs/distributed-system/protocol/raft-algorithm.md @@ -102,24 +102,24 @@ Leader 会向所有的 Follower 周期性发送心跳来保证自己的 Leader 由于可能同一时刻出现多个 Candidate,导致没有 Candidate 获得大多数选票,如果没有其他手段来重新分配选票的话,那么可能会无限重复下去。 -raft 使用了随机的选举超时时间来避免上述情况。每一个 Candidate 在发起选举后,都会随机化一个新的枚举超时时间,这种机制使得各个服务器能够分散开来,在大多数情况下只有一个服务器会率先超时;它会在其他服务器超时之前赢得选举。 +raft 使用了随机的选举超时时间来避免上述情况。每一个 Candidate 在发起选举后,都会随机化一个新的选举超时时间,这种机制使得各个服务器能够分散开来,在大多数情况下只有一个服务器会率先超时;它会在其他服务器超时之前赢得选举。 ## 4 日志复制 -一旦选出了 Leader,它就开始接受客户端的请求。每一个客户端的请求都包含一条需要被复制状态机(`Replicated State Mechine`)执行的命令。 +一旦选出了 Leader,它就开始接受客户端的请求。每一个客户端的请求都包含一条需要被复制状态机(`Replicated State Machine`)执行的命令。 Leader 收到客户端请求后,会生成一个 entry,包含``,再将这个 entry 添加到自己的日志末尾后,向所有的节点广播该 entry,要求其他服务器复制这条 entry。 如果 Follower 接受该 entry,则会将 entry 添加到自己的日志后面,同时返回给 Leader 同意。 -如果 Leader 收到了多数的成功响应,Leader 会将这个 entry 应用到自己的状态机中,之后可以成为这个 entry 是 committed 的,并且向客户端返回执行结果。 +如果 Leader 收到了多数的成功响应,Leader 会将这个 entry 应用到自己的状态机中,之后可以称这个 entry 是 committed 的,并且向客户端返回执行结果。 raft 保证以下两个性质: - 在两个日志里,有两个 entry 拥有相同的 index 和 term,那么它们一定有相同的 cmd - 在两个日志里,有两个 entry 拥有相同的 index 和 term,那么它们前面的 entry 也一定相同 -通过“仅有 Leader 可以生存 entry”来保证第一个性质,第二个性质需要一致性检查来进行保证。 +通过“仅有 Leader 可以生成 entry”来保证第一个性质,第二个性质需要一致性检查来进行保证。 一般情况下,Leader 和 Follower 的日志保持一致,然后,Leader 的崩溃会导致日志不一样,这样一致性检查会产生失败。Leader 通过强制 Follower 复制自己的日志来处理日志的不一致。这就意味着,在 Follower 上的冲突日志会被领导者的日志覆盖。 @@ -163,7 +163,9 @@ raft 的要求之一就是安全性不依赖于时间:系统不能仅仅因为 ## 6 参考 -- https://tanxinyu.work/raft/ -- https://github.com/OneSizeFitsQuorum/raft-thesis-zh_cn/blob/master/raft-thesis-zh_cn.md -- https://github.com/ongardie/dissertation/blob/master/stanford.pdf -- https://knowledge-sharing.gitbooks.io/raft/content/chapter5.html +- +- +- +- + + diff --git a/docs/distributed-system/rpc/dubbo.md b/docs/distributed-system/rpc/dubbo.md index ee6c6a39a35..3eaee38b50c 100644 --- a/docs/distributed-system/rpc/dubbo.md +++ b/docs/distributed-system/rpc/dubbo.md @@ -56,7 +56,7 @@ Dubbo 是由阿里开源,后来加入了 Apache 。正是由于 Dubbo 的出 1. **负载均衡**:同一个服务部署在不同的机器时该调用哪一台机器上的服务。 2. **服务调用链路生成**:随着系统的发展,服务越来越多,服务间依赖关系变得错踪复杂,甚至分不清哪个应用要在哪个应用之前启动,架构师都不能完整的描述应用的架构关系。Dubbo 可以为我们解决服务之间互相是如何调用的。 3. **服务访问压力以及时长统计、资源调度和治理**:基于访问压力实时管理集群容量,提高集群利用率。 -4. ...... +4. …… ![Dubbo 能力概览](https://oss.javaguide.cn/github/javaguide/system-design/distributed-system/rpc/dubbo-features-overview.jpg) @@ -171,7 +171,7 @@ src `org.apache.dubbo.rpc.cluster.LoadBalance` -``` +```plain xxx=com.xxx.XxxLoadBalance ``` @@ -262,7 +262,7 @@ public abstract class AbstractLoadBalance implements LoadBalance { 根据权重随机选择(对加权随机算法的实现)。这是 Dubbo 默认采用的一种负载均衡策略。 -` RandomLoadBalance` 具体的实现原理非常简单,假如有两个提供相同服务的服务器 S1,S2,S1 的权重为 7,S2 的权重为 3。 +`RandomLoadBalance` 具体的实现原理非常简单,假如有两个提供相同服务的服务器 S1,S2,S1 的权重为 7,S2 的权重为 3。 我们把这些权重值分布在坐标区间会得到:S1->[0, 7) ,S2->[7, 10)。我们生成[0, 10) 之间的随机数,随机数落到对应的区间,我们就选择对应的服务器来处理请求。 @@ -456,4 +456,6 @@ Kryo 和 FST 这两种序列化方式是 Dubbo 后来才引入的,性能非常 Dubbo 官方文档中还有一个关于这些[序列化协议的性能对比图](https://dubbo.apache.org/zh/docs/v2.7/user/serialization/#m-zhdocsv27userserialization)可供参考。 -![序列化协议的性能对比](https://oscimg.oschina.net/oscnet/up-00c3ce1e5d222e477ed84310239daa2f6b0.png) +![序列化协议的性能对比](https://oss.javaguide.cn/github/javaguide/distributed-system/rpc/dubbo-serialization-protocol-performance-comparison.png) + + diff --git a/docs/distributed-system/rpc/http&rpc.md b/docs/distributed-system/rpc/http&rpc.md index 88ac10b03fe..35301d0bceb 100644 --- a/docs/distributed-system/rpc/http&rpc.md +++ b/docs/distributed-system/rpc/http&rpc.md @@ -5,7 +5,7 @@ tag: - rpc --- -> 本文来自[小白 debug](https://juejin.cn/user/4001878057422087)投稿,原文:https://juejin.cn/post/7121882245605883934 。 +> 本文来自[小白 debug](https://juejin.cn/user/4001878057422087)投稿,原文: 。 我想起了我刚工作的时候,第一次接触 RPC 协议,当时就很懵,我 HTTP 协议用的好好的,为什么还要用 RPC 协议? @@ -192,3 +192,5 @@ res = remoteFunc(req) - 从发展历史来说,**HTTP 主要用于 B/S 架构,而 RPC 更多用于 C/S 架构。但现在其实已经没分那么清了,B/S 和 C/S 在慢慢融合。** 很多软件同时支持多端,所以对外一般用 HTTP 协议,而内部集群的微服务之间则采用 RPC 协议进行通讯。 - RPC 其实比 HTTP 出现的要早,且比目前主流的 HTTP1.1 性能要更好,所以大部分公司内部都还在使用 RPC。 - **HTTP2.0** 在 **HTTP1.1** 的基础上做了优化,性能可能比很多 RPC 协议都要好,但由于是这几年才出来的,所以也不太可能取代掉 RPC。 + + diff --git a/docs/distributed-system/rpc/rpc-intro.md b/docs/distributed-system/rpc/rpc-intro.md index b65ed37e4ab..d2c5fb5e9c7 100644 --- a/docs/distributed-system/rpc/rpc-intro.md +++ b/docs/distributed-system/rpc/rpc-intro.md @@ -25,13 +25,13 @@ tag: 1. **客户端(服务消费端)**:调用远程方法的一端。 1. **客户端 Stub(桩)**:这其实就是一代理类。代理类主要做的事情很简单,就是把你调用方法、类、方法参数等信息传递到服务端。 -1. **网络传输**:网络传输就是你要把你调用的方法的信息比如说参数啊这些东西传输到服务端,然后服务端执行完之后再把返回结果通过网络传输给你传输回来。网络传输的实现方式有很多种比如最近基本的 Socket 或者性能以及封装更加优秀的 Netty(推荐)。 +1. **网络传输**:网络传输就是你要把你调用的方法的信息比如说参数啊这些东西传输到服务端,然后服务端执行完之后再把返回结果通过网络传输给你传输回来。网络传输的实现方式有很多种比如最基本的 Socket 或者性能以及封装更加优秀的 Netty(推荐)。 1. **服务端 Stub(桩)**:这个桩就不是代理类了。我觉得理解为桩实际不太好,大家注意一下就好。这里的服务端 Stub 实际指的就是接收到客户端执行方法的请求后,去执行对应的方法然后返回结果给客户端的类。 1. **服务端(服务提供端)**:提供远程方法的一端。 具体原理图如下,后面我会串起来将整个 RPC 的过程给大家说一下。 -![RPC原理图](https://my-blog-to-use.oss-cn-beijing.aliyuncs.com/18-12-6/37345851.jpg) +![RPC原理图](https://oss.javaguide.cn/github/javaguide/distributed-system/rpc/37345851.jpg) 1. 服务消费端(client)以本地调用的方式调用远程服务; 1. 客户端 Stub(client stub) 接收到调用后负责将方法、参数等组装成能够进行网络传输的消息体(序列化):`RpcRequest`; @@ -67,7 +67,7 @@ Dubbo 是由阿里开源,后来加入了 Apache 。正是由于 Dubbo 的出 Dubbo 算的是比较优秀的国产开源项目了,它的源码也是非常值得学习和阅读的! - GitHub:[https://github.com/apache/incubator-dubbo](https://github.com/apache/incubator-dubbo "https://github.com/apache/incubator-dubbo") -- 官网:https://dubbo.apache.org/zh/ +- 官网: ### Motan @@ -82,7 +82,7 @@ Motan 是新浪微博开源的一款 RPC 框架,据说在新浪微博正支撑 ### gRPC -![](https://my-blog-to-use.oss-cn-beijing.aliyuncs.com/2020-8/2843b10d-0c2f-4b7e-9c3e-ea4466792a8b.png) +![](https://oss.javaguide.cn/github/javaguide/distributed-system/rpc/2843b10d-0c2f-4b7e-9c3e-ea4466792a8b.png) gRPC 是 Google 开源的一个高性能、通用的开源 RPC 框架。其由主要面向移动应用开发并基于 HTTP/2 协议标准而设计(支持双向流、消息头压缩等功能,更加节省带宽),基于 ProtoBuf 序列化协议开发,并且支持众多开发语言。 @@ -114,11 +114,11 @@ Dubbo 不论是从功能完善程度、生态系统还是社区活跃度来说 下图展示了 Dubbo 的生态系统。 -![](https://my-blog-to-use.oss-cn-beijing.aliyuncs.com/2020-8/eee98ff2-8e06-4628-a42b-d30ffcd2831e.png) +![](https://oss.javaguide.cn/github/javaguide/distributed-system/rpc/eee98ff2-8e06-4628-a42b-d30ffcd2831e.png) Dubbo 也是 Spring Cloud Alibaba 里面的一个组件。 -![](https://my-blog-to-use.oss-cn-beijing.aliyuncs.com/2020-8/0d195dae-72bc-4956-8451-3eaf6dd11cbd.png) +![](https://oss.javaguide.cn/github/javaguide/distributed-system/rpc/0d195dae-72bc-4956-8451-3eaf6dd11cbd.png) 但是,Dubbo 和 Motan 主要是给 Java 语言使用。虽然,Dubbo 和 Motan 目前也能兼容部分语言,但是不太推荐。如果需要跨多种语言调用的话,可以考虑使用 gRPC。 @@ -137,3 +137,5 @@ Dubbo 也是 Spring Cloud Alibaba 里面的一个组件。 ## 既然有了 HTTP 协议,为什么还要有 RPC ? 关于这个问题的详细答案,请看这篇文章:[有了 HTTP 协议,为什么还要有 RPC ?](http&rpc.md) 。 + + diff --git a/docs/distributed-system/spring-cloud-gateway-questions.md b/docs/distributed-system/spring-cloud-gateway-questions.md index a85c8ee189e..1e6e86845af 100644 --- a/docs/distributed-system/spring-cloud-gateway-questions.md +++ b/docs/distributed-system/spring-cloud-gateway-questions.md @@ -68,7 +68,7 @@ Route 路由和 Predicate 断言的对应关系如下:: Spring Cloud Gateway 作为微服务的入口,需要尽量避免重启,而现在配置更改需要重启服务不能满足实际生产过程中的动态刷新、实时变更的业务需求,所以我们需要在 Spring Cloud Gateway 运行时动态配置网关。 -实现动态路由的方式有很多种,其中一种推荐的方式是基于 Nacos 配置中心来做。简单来说,我们将将路由配置放在 Nacos 中存储,然后写个监听器监听 Nacos 上配置的变化,将变化后的配置更新到 GateWay 应用的进程内。 +实现动态路由的方式有很多种,其中一种推荐的方式是基于 Nacos 注册中心来做。 Spring Cloud Gateway 可以从注册中心获取服务的元数据(例如服务名称、路径等),然后根据这些信息自动生成路由规则。这样,当你添加、移除或更新服务实例时,网关会自动感知并相应地调整路由规则,无需手动维护路由配置。 其实这些复杂的步骤并不需要我们手动实现,通过 Nacos Server 和 Spring Cloud Alibaba Nacos Config 即可实现配置的动态变更,官方文档地址: 。 @@ -153,3 +153,5 @@ public class GlobalErrorWebExceptionHandler implements ErrorWebExceptionHandler - Spring Cloud Gateway 官方文档: - Creating a custom Spring Cloud Gateway Filter: - 全局异常处理: + + diff --git a/docs/high-availability/fallback-and-circuit-breaker.md b/docs/high-availability/fallback-and-circuit-breaker.md index bc2e477b753..59725fa0521 100644 --- a/docs/high-availability/fallback-and-circuit-breaker.md +++ b/docs/high-availability/fallback-and-circuit-breaker.md @@ -1,8 +1,13 @@ --- title: 降级&熔断详解(付费) category: 高可用 +icon: circuit --- **降级&熔断** 相关的面试题为我的[知识星球](https://javaguide.cn/about-the-author/zhishixingqiu-two-years.html)(点击链接即可查看详细介绍以及加入方法)专属内容,已经整理到了[《Java 面试指北》](https://javaguide.cn/zhuanlan/java-mian-shi-zhi-bei.html)中。 +![](https://oss.javaguide.cn/xingqiu/mianshizhibei-gaobingfa.png) + + + diff --git a/docs/high-availability/high-availability-system-design.md b/docs/high-availability/high-availability-system-design.md index 67547074c01..f461f93e99b 100644 --- a/docs/high-availability/high-availability-system-design.md +++ b/docs/high-availability/high-availability-system-design.md @@ -1,6 +1,7 @@ --- title: 高可用系统设计指南 category: 高可用 +icon: design --- ## 什么是高可用?可用性的判断标准是啥? @@ -19,7 +20,7 @@ category: 高可用 4. 代码中的坏味道导致内存泄漏或者其他问题导致程序挂掉。 5. 网站架构某个重要的角色比如 Nginx 或者数据库突然不可用。 6. 自然灾害或者人为破坏。 -7. ...... +7. …… ## 有哪些提高系统可用性的方法? @@ -65,4 +66,6 @@ category: 高可用 - **注意备份,必要时候回滚。** - **灰度发布:** 将服务器集群分成若干部分,每天只发布一部分机器,观察运行稳定没有故障,第二天继续发布一部分机器,持续几天才把整个集群全部发布完毕,期间如果发现问题,只需要回滚已发布的一部分服务器即可 - **定期检查/更换硬件:** 如果不是购买的云服务的话,定期还是需要对硬件进行一波检查的,对于一些需要更换或者升级的硬件,要及时更换或者升级。 -- ..... +- …… + + diff --git a/docs/high-availability/idempotency.md b/docs/high-availability/idempotency.md new file mode 100644 index 00000000000..41384457ccb --- /dev/null +++ b/docs/high-availability/idempotency.md @@ -0,0 +1,13 @@ +--- +title: 接口幂等方案总结(付费) +category: 高可用 +icon: security-fill +--- + +**接口幂等** 相关的面试题为我的[知识星球](https://javaguide.cn/about-the-author/zhishixingqiu-two-years.html)(点击链接即可查看详细介绍以及加入方法)专属内容,已经整理到了[《Java 面试指北》](https://javaguide.cn/zhuanlan/java-mian-shi-zhi-bei.html)中。 + +![](https://oss.javaguide.cn/xingqiu/mianshizhibei-gaobingfa.png) + + + + diff --git a/docs/high-availability/limit-request.md b/docs/high-availability/limit-request.md index 72593d3c713..22db662eedf 100644 --- a/docs/high-availability/limit-request.md +++ b/docs/high-availability/limit-request.md @@ -1,11 +1,12 @@ --- title: 服务限流详解 category: 高可用 +icon: limit_rate --- 针对软件系统来说,限流就是对请求的速率进行限制,避免瞬时的大量请求击垮软件系统。毕竟,软件系统的处理能力是有限的。如果说超过了其处理能力的范围,软件系统可能直接就挂掉了。 -限流可能会导致用户的请求无法被正确处理,不过,这往往也是权衡了软件系统的稳定性之后得到的最优解。 +限流可能会导致用户的请求无法被正确处理或者无法立即被处理,不过,这往往也是权衡了软件系统的稳定性之后得到的最优解。 现实生活中,处处都有限流的实际应用,就比如排队买票是为了避免大量用户涌入购票而导致售票员无法处理。 @@ -17,32 +18,46 @@ category: 高可用 ### 固定窗口计数器算法 -固定窗口其实就是时间窗口。**固定窗口计数器算法** 规定了我们单位时间处理的请求数量。 +固定窗口其实就是时间窗口,其原理是将时间划分为固定大小的窗口,在每个窗口内限制请求的数量或速率,即固定窗口计数器算法规定了系统单位时间处理的请求数量。 -假如我们规定系统中某个接口 1 分钟只能访问 33 次的话,使用固定窗口计数器算法的实现思路如下: +假如我们规定系统中某个接口 1 分钟只能被访问 33 次的话,使用固定窗口计数器算法的实现思路如下: +- 将时间划分固定大小窗口,这里是 1 分钟一个窗口。 - 给定一个变量 `counter` 来记录当前接口处理的请求数量,初始值为 0(代表接口当前 1 分钟内还未处理请求)。 - 1 分钟之内每处理一个请求之后就将 `counter+1` ,当 `counter=33` 之后(也就是说在这 1 分钟内接口已经被访问 33 次的话),后续的请求就会被全部拒绝。 - 等到 1 分钟结束后,将 `counter` 重置 0,重新开始计数。 -**这种限流算法无法保证限流速率,因而无法保证突然激增的流量。** +![固定窗口计数器算法](https://static001.infoq.cn/resource/image/8d/15/8ded7a2b90e1482093f92fff555b3615.png) -就比如说我们限制某个接口 1 分钟只能访问 1000 次,该接口的 QPS 为 500,前 55s 这个接口 1 个请求没有接收,后 1s 突然接收了 1000 个请求。然后,在当前场景下,这 1000 个请求在 1s 内是没办法被处理的,系统直接就被瞬时的大量请求给击垮了。 +优点:实现简单,易于理解。 -![固定窗口计数器算法](https://static001.infoq.cn/resource/image/8d/15/8ded7a2b90e1482093f92fff555b3615.png) +缺点: + +- 限流不够平滑。例如,我们限制某个接口每分钟只能访问 30 次,假设前 30 秒就有 30 个请求到达的话,那后续 30 秒将无法处理请求,这是不可取的,用户体验极差! +- 无法保证限流速率,因而无法应对突然激增的流量。例如,我们限制某个接口 1 分钟只能访问 1000 次,该接口的 QPS 为 500,前 55s 这个接口 1 个请求没有接收,后 1s 突然接收了 1000 个请求。然后,在当前场景下,这 1000 个请求在 1s 内是没办法被处理的,系统直接就被瞬时的大量请求给击垮了。 ### 滑动窗口计数器算法 -**滑动窗口计数器算法** 算的上是固定窗口计数器算法的升级版。 +**滑动窗口计数器算法** 算的上是固定窗口计数器算法的升级版,限流的颗粒度更小。 滑动窗口计数器算法相比于固定窗口计数器算法的优化在于:**它把时间以一定比例分片** 。 -例如我们的接口限流每分钟处理 60 个请求,我们可以把 1 分钟分为 60 个窗口。每隔 1 秒移动一次,每个窗口一秒只能处理 不大于 `60(请求数)/60(窗口数)` 的请求, 如果当前窗口的请求计数总和超过了限制的数量的话就不再处理其他请求。 +例如我们的接口限流每分钟处理 60 个请求,我们可以把 1 分钟分为 60 个窗口。每隔 1 秒移动一次,每个窗口一秒只能处理不大于 `60(请求数)/60(窗口数)` 的请求, 如果当前窗口的请求计数总和超过了限制的数量的话就不再处理其他请求。 很显然, **当滑动窗口的格子划分的越多,滑动窗口的滚动就越平滑,限流的统计就会越精确。** ![滑动窗口计数器算法](https://static001.infoq.cn/resource/image/ae/15/ae4d3cd14efb8dc7046d691c90264715.png) +优点: + +- 相比于固定窗口算法,滑动窗口计数器算法可以应对突然激增的流量。 +- 相比于固定窗口算法,滑动窗口计数器算法的颗粒度更小,可以提供更精确的限流控制。 + +缺点: + +- 与固定窗口计数器算法类似,滑动窗口计数器算法依然存在限流不够平滑的问题。 +- 相比较于固定窗口计数器算法,滑动窗口计数器算法实现和理解起来更复杂一些。 + ### 漏桶算法 我们可以把发请求的动作比作成注水到桶中,我们处理请求的过程可以比喻为漏桶漏水。我们往桶中以任意速率流入水,以一定速率流出水。当水超过桶流量则丢弃,因为桶容量是不变的,保证了整体的速率。 @@ -51,12 +66,48 @@ category: 高可用 ![漏桶算法](https://static001.infoq.cn/resource/image/75/03/75938d1010138ce66e38c6ed0392f103.png) +优点: + +- 实现简单,易于理解。 +- 可以控制限流速率,避免网络拥塞和系统过载。 + +缺点: + +- 无法应对突然激增的流量,因为只能以固定的速率处理请求,对系统资源利用不够友好。 +- 桶流入水(发请求)的速率如果一直大于桶流出水(处理请求)的速率的话,那么桶会一直是满的,一部分新的请求会被丢弃,导致服务质量下降。 + +实际业务场景中,基本不会使用漏桶算法。 + ### 令牌桶算法 令牌桶算法也比较简单。和漏桶算法算法一样,我们的主角还是桶(这限流算法和桶过不去啊)。不过现在桶里装的是令牌了,请求在被处理之前需要拿到一个令牌,请求处理完毕之后将这个令牌丢弃(删除)。我们根据限流大小,按照一定的速率往桶里添加令牌。如果桶装满了,就不能继续往里面继续添加令牌了。 ![令牌桶算法](https://static001.infoq.cn/resource/image/ec/93/eca0e5eaa35dac938c673fecf2ec9a93.png) +优点: + +- 可以限制平均速率和应对突然激增的流量。 +- 可以动态调整生成令牌的速率。 + +缺点: + +- 如果令牌产生速率和桶的容量设置不合理,可能会出现问题比如大量的请求被丢弃、系统过载。 +- 相比于其他限流算法,实现和理解起来更复杂一些。 + +## 针对什么来进行限流? + +实际项目中,还需要确定限流对象,也就是针对什么来进行限流。常见的限流对象如下: + +- IP :针对 IP 进行限流,适用面较广,简单粗暴。 +- 业务 ID:挑选唯一的业务 ID 以实现更针对性地限流。例如,基于用户 ID 进行限流。 +- 个性化:根据用户的属性或行为,进行不同的限流策略。例如, VIP 用户不限流,而普通用户限流。根据系统的运行指标(如 QPS、并发调用数、系统负载等),动态调整限流策略。例如,当系统负载较高的时候,控制每秒通过的请求减少。 + +针对 IP 进行限流是目前比较常用的一个方案。不过,实际应用中需要注意用户真实 IP 地址的正确获取。常用的真实 IP 获取方法有 X-Forwarded-For 和 TCP Options 字段承载真实源 IP 信息。虽然 X-Forwarded-For 字段可能会被伪造,但因为其实现简单方便,很多项目还是直接用的这种方法。 + +除了我上面介绍到的限流对象之外,还有一些其他较为复杂的限流对象策略,比如阿里的 Sentinel 还支持 [基于调用关系的限流](https://github.com/alibaba/Sentinel/wiki/流量控制#基于调用关系的流量控制)(包括基于调用方限流、基于调用链入口限流、关联流量限流等)以及更细维度的 [热点参数限流](https://github.com/alibaba/Sentinel/wiki/热点参数限流)(实时的统计热点参数并针对热点参数的资源调用进行流量控制)。 + +另外,一个项目可以根据具体的业务需求选择多种不同的限流对象搭配使用。 + ## 单机限流怎么做? 单机限流针对的是单体架构应用。 @@ -188,7 +239,7 @@ Resilience4j 不仅提供限流,还提供了熔断、负载保护、自动重 分布式限流常见的方案: -- **借助中间件架限流**:可以借助 Sentinel 或者使用 Redis 来自己实现对应的限流逻辑。 +- **借助中间件限流**:可以借助 Sentinel 或者使用 Redis 来自己实现对应的限流逻辑。 - **网关层限流**:比较常用的一种方案,直接在网关层把限流给安排上了。不过,通常网关层限流通常也需要借助到中间件/框架。就比如 Spring Cloud Gateway 的分布式限流实现`RedisRateLimiter`就是基于 Redis+Lua 来实现的,再比如 Spring Cloud Gateway 还可以整合 Sentinel 来做限流。 如果你要基于 Redis 来手动实现限流逻辑的话,建议配合 Lua 脚本来做。 @@ -202,10 +253,46 @@ Resilience4j 不仅提供限流,还提供了熔断、负载保护、自动重 > ShenYu 地址: -![ShenYu 限流脚本](https://oss.javaguide.cn/github/javaguide/csdn/e1e2a75f489e4854990dabe3b6cec522.jpg) +![ShenYu 限流脚本](https://oss.javaguide.cn/github/javaguide/high-availability/limit-request/shenyu-ratelimit-lua-scripts.png) + +另外,如果不想自己写 Lua 脚本的话,也可以直接利用 Redisson 中的 `RRateLimiter` 来实现分布式限流,其底层实现就是基于 Lua 代码+令牌桶算法。 -## 相关阅读 +Redisson 是一个开源的 Java 语言 Redis 客户端,提供了很多开箱即用的功能,比如 Java 中常用的数据结构实现、分布式锁、延迟队列等等。并且,Redisson 还支持 Redis 单机、Redis Sentinel、Redis Cluster 等多种部署架构。 + +`RRateLimiter` 的使用方式非常简单。我们首先需要获取一个`RRateLimiter`对象,直接通过 Redisson 客户端获取即可。然后,设置限流规则就好。 + +```java +// 创建一个 Redisson 客户端实例 +RedissonClient redissonClient = Redisson.create(); +// 获取一个名为 "javaguide.limiter" 的限流器对象 +RRateLimiter rateLimiter = redissonClient.getRateLimiter("javaguide.limiter"); +// 尝试设置限流器的速率为每小时 100 次 +// RateType 有两种,OVERALL是全局限流,ER_CLIENT是单Client限流(可以认为就是单机限流) +rateLimiter.trySetRate(RateType.OVERALL, 100, 1, RateIntervalUnit.HOURS); +``` + +接下来我们调用`acquire()`方法或`tryAcquire()`方法即可获取许可。 + +```java +// 获取一个许可,如果超过限流器的速率则会等待 +// acquire()是同步方法,对应的异步方法:acquireAsync() +rateLimiter.acquire(1); +// 尝试在 5 秒内获取一个许可,如果成功则返回 true,否则返回 false +// tryAcquire()是同步方法,对应的异步方法:tryAcquireAsync() +boolean res = rateLimiter.tryAcquire(1, 5, TimeUnit.SECONDS); +``` + +## 总结 + +这篇文章主要介绍了常见的限流算法、限流对象的选择以及单机限流和分布式限流分别应该怎么做。 + +## 参考 - 服务治理之轻量级熔断框架 Resilience4j: - 超详细的 Guava RateLimiter 限流原理解析: - 实战 Spring Cloud Gateway 之限流篇 👍: +- 详解 Redisson 分布式限流的实现原理: +- 一文详解 Java 限流接口实现 - 阿里云开发者: +- 分布式限流方案的探索与实践 - 腾讯云开发者: + + diff --git a/docs/high-availability/performance-test.md b/docs/high-availability/performance-test.md index c033e9aaa0c..47201441d7e 100644 --- a/docs/high-availability/performance-test.md +++ b/docs/high-availability/performance-test.md @@ -1,21 +1,22 @@ --- title: 性能测试入门 category: 高可用 +icon: et-performance --- 性能测试一般情况下都是由测试这个职位去做的,那还需要我们开发学这个干嘛呢?了解性能测试的指标、分类以及工具等知识有助于我们更好地去写出性能更好的程序,另外作为开发这个角色,如果你会性能测试的话,相信也会为你的履历加分不少。 这篇文章是我会结合自己的实际经历以及在测试这里取的经所得,除此之外,我还借鉴了一些优秀书籍,希望对你有帮助。 -## 一 不同角色看网站性能 +## 不同角色看网站性能 -### 1.1 用户 +### 用户 当用户打开一个网站的时候,最关注的是什么?当然是网站响应速度的快慢。比如我们点击了淘宝的主页,淘宝需要多久将首页的内容呈现在我的面前,我点击了提交订单按钮需要多久返回结果等等。 所以,用户在体验我们系统的时候往往根据你的响应速度的快慢来评判你的网站的性能。 -### 1.2 开发人员 +### 开发人员 用户与开发人员都关注速度,这个速度实际上就是我们的系统**处理用户请求的速度**。 @@ -28,74 +29,98 @@ category: 高可用 5. 系统用到的算法是否还需要优化? 6. 系统是否存在内存泄露的问题? 7. 项目使用的 Redis 缓存多大?服务器性能如何?用的是机械硬盘还是固态硬盘? -8. ...... +8. …… -### 1.3 测试人员 +### 测试人员 测试人员一般会根据性能测试工具来测试,然后一般会做出一个表格。这个表格可能会涵盖下面这些重要的内容: 1. 响应时间; 2. 请求成功率; 3. 吞吐量; -4. ...... +4. …… -### 1.4 运维人员 +### 运维人员 -运维人员会倾向于根据基础设施和资源的利用率来判断网站的性能,比如我们的服务器资源使用是否合理、数据库资源是否存在滥用的情况、当然,这是传统的运维人员,现在 Devpos 火起来后,单纯干运维的很少了。我们这里暂且还保留有这个角色。 +运维人员会倾向于根据基础设施和资源的利用率来判断网站的性能,比如我们的服务器资源使用是否合理、数据库资源是否存在滥用的情况、当然,这是传统的运维人员,现在 Devops 火起来后,单纯干运维的很少了。我们这里暂且还保留有这个角色。 -## 二 性能测试需要注意的点 +## 性能测试需要注意的点 几乎没有文章在讲性能测试的时候提到这个问题,大家都会讲如何去性能测试,有哪些性能测试指标这些东西。 -### 2.1 了解系统的业务场景 +### 了解系统的业务场景 **性能测试之前更需要你了解当前的系统的业务场景。** 对系统业务了解的不够深刻,我们很容易犯测试方向偏执的错误,从而导致我们忽略了对系统某些更需要性能测试的地方进行测试。比如我们的系统可以为用户提供发送邮件的功能,用户配置成功邮箱后只需输入相应的邮箱之后就能发送,系统每天大概能处理上万次发邮件的请求。很多人看到这个可能就直接开始使用相关工具测试邮箱发送接口,但是,发送邮件这个场景可能不是当前系统的性能瓶颈,这么多人用我们的系统发邮件, 还可能有很多人一起发邮件,单单这个场景就这么人用,那用户管理可能才是性能瓶颈吧! -### 2.2 历史数据非常有用 +### 历史数据非常有用 -当前系统所留下的历史数据非常重要,一般情况下,我们可以通过相应的些历史数据初步判定这个系统哪些接口调用的比较多、哪些 service 承受的压力最大,这样的话,我们就可以针对这些地方进行更细致的性能测试与分析。 +当前系统所留下的历史数据非常重要,一般情况下,我们可以通过相应的些历史数据初步判定这个系统哪些接口调用的比较多、哪些服务承受的压力最大,这样的话,我们就可以针对这些地方进行更细致的性能测试与分析。 另外,这些地方也就像这个系统的一个短板一样,优化好了这些地方会为我们的系统带来质的提升。 -### 三 性能测试的指标 +## 常见性能指标 -### 3.1 响应时间 +### 响应时间 -**响应时间就是用户发出请求到用户收到系统处理结果所需要的时间。** 重要吗?实在太重要! +**响应时间 RT(Response-time)就是用户发出请求到用户收到系统处理结果所需要的时间。** -比较出名的 2-5-8 原则是这样描述的:通常来说,2 到 5 秒,页面体验会比较好,5 到 8 秒还可以接受,8 秒以上基本就很难接受了。另外,据统计当网站慢一秒就会流失十分之一的客户。 +RT 是一个非常重要且直观的指标,RT 数值大小直接反应了系统处理用户请求速度的快慢。 -但是,在某些场景下我们也并不需要太看重 2-5-8 原则 ,比如我觉得系统导出导入大数据量这种就不需要,系统生成系统报告这种也不需要。 +### 并发数 -### 3.2 并发数 +**并发数可以简单理解为系统能够同时供多少人访问使用也就是说系统同时能处理的请求数量。** -**并发数是系统能同时处理请求的数目即同时提交请求的用户数目。** +并发数反应了系统的负载能力。 -不得不说,高并发是现在后端架构中非常非常火热的一个词了,这个与当前的互联网环境以及中国整体的互联网用户量都有很大关系。一般情况下,你的系统并发量越大,说明你的产品做的就越大。但是,并不是每个系统都需要达到像淘宝、12306 这种亿级并发量的。 +### QPS 和 TPS -### 3.3 吞吐量 +- **QPS(Query Per Second)** :服务器每秒可以执行的查询次数; +- **TPS(Transaction Per Second)** :服务器每秒处理的事务数(这里的一个事务可以理解为客户发出请求到收到服务器的过程); -吞吐量指的是系统单位时间内系统处理的请求数量。衡量吞吐量有几个重要的参数:QPS(TPS)、并发数、响应时间。 +书中是这样描述 QPS 和 TPS 的区别的。 -1. QPS(Query Per Second):服务器每秒可以执行的查询次数; -2. TPS(Transaction Per Second):服务器每秒处理的事务数(这里的一个事务可以理解为客户发出请求到收到服务器的过程); -3. 并发数;系统能同时处理请求的数目即同时提交请求的用户数目。 -4. 响应时间:一般取多次请求的平均响应时间 +> QPS vs TPS:QPS 基本类似于 TPS,但是不同的是,对于一个页面的一次访问,形成一个 TPS;但一次页面请求,可能产生多次对服务器的请求,服务器对这些请求,就可计入“QPS”之中。如,访问一个页面会请求服务器 2 次,一次访问,产生一个“T”,产生 2 个“Q”。 -理清他们的概念,就很容易搞清楚他们之间的关系了。 +### 吞吐量 -- **QPS(TPS)** = 并发数/平均响应时间 -- **并发数** = QPS\*平均响应时间 +**吞吐量指的是系统单位时间内系统处理的请求数量。** -书中是这样描述 QPS 和 TPS 的区别的。 +一个系统的吞吐量与请求对系统的资源消耗等紧密关联。请求对系统资源消耗越多,系统吞吐能力越低,反之则越高。 -> QPS vs TPS:QPS 基本类似于 TPS,但是不同的是,对于一个页面的一次访问,形成一个 TPS;但一次页面请求,可能产生多次对服务器的请求,服务器对这些请求,就可计入“QPS”之中。如,访问一个页面会请求服务器 2 次,一次访问,产生一个“T”,产生 2 个“Q”。 +TPS、QPS 都是吞吐量的常用量化指标。 + +- **QPS(TPS)** = 并发数/平均响应时间(RT) +- **并发数** = QPS \* 平均响应时间(RT) + +## 系统活跃度指标 + +### PV(Page View) + +访问量, 即页面浏览量或点击量,衡量网站用户访问的网页数量;在一定统计周期内用户每打开或刷新一个页面就记录 1 次,多次打开或刷新同一页面则浏览量累计。UV 从网页打开的数量/刷新的次数的角度来统计的。 + +### UV(Unique Visitor) + +独立访客,统计 1 天内访问某站点的用户数。1 天内相同访客多次访问网站,只计算为 1 个独立访客。UV 是从用户个体的角度来统计的。 -### 3.4 性能计数器 +### DAU(Daily Active User) -**性能计数器是描述服务器或者操作系统的一些数据指标如内存使用、CPU 使用、磁盘与网络 I/O 等情况。** +日活跃用户数量。 -### 四 几种常见的性能测试 +### MAU(monthly active users) + +月活跃用户人数。 + +举例:某网站 DAU 为 1200w, 用户日均使用时长 1 小时,RT 为 0.5s,求并发量和 QPS。 + +平均并发量 = DAU(1200w)\* 日均使用时长(1 小时,3600 秒) /一天的秒数(86400)=1200w/24 = 50w + +真实并发量(考虑到某些时间段使用人数比较少) = DAU(1200w)\* 日均使用时长(1 小时,3600 秒) /一天的秒数-访问量比较小的时间段假设为 8 小时(57600)=1200w/16 = 75w + +峰值并发量 = 平均并发量 \* 6 = 300w + +QPS = 真实并发量/RT = 75W/0.5=150w/s + +## 性能测试分类 ### 性能测试 @@ -117,25 +142,27 @@ category: 高可用 模拟真实场景,给系统一定压力,看看业务是否能稳定运行。 -## 五 常用性能测试工具 +## 常用性能测试工具 -这里就不多扩展了,有时间的话会单独拎一个熟悉的说一下。 +### 后端常用 -### 5.1 后端常用 +既然系统设计涉及到系统性能方面的问题,那在面试的时候,面试官就很可能会问:**你是如何进行性能测试的?** -没记错的话,除了 LoadRunner 其他几款性能测试工具都是开源免费的。 +推荐 4 个比较常用的性能测试工具: -1. Jmeter:Apache JMeter 是 JAVA 开发的性能测试工具。 -2. LoadRunner:一款商业的性能测试工具。 -3. Galtling:一款基于 Scala 开发的高性能服务器性能测试工具。 -4. ab:全称为 Apache Bench 。Apache 旗下的一款测试工具,非常实用。 +1. **Jmeter** :Apache JMeter 是 JAVA 开发的性能测试工具。 +2. **LoadRunner**:一款商业的性能测试工具。 +3. **Galtling** :一款基于 Scala 开发的高性能服务器性能测试工具。 +4. **ab** :全称为 Apache Bench 。Apache 旗下的一款测试工具,非常实用。 -### 5.2 前端常用 +没记错的话,除了 **LoadRunner** 其他几款性能测试工具都是开源免费的。 -1. Fiddler:抓包工具,它可以修改请求的数据,甚至可以修改服务器返回的数据,功能非常强大,是 Web 调试的利器。 -2. HttpWatch: 可用于录制 HTTP 请求信息的工具。 +### 前端常用 -## 六 常见的性能优化策略 +1. **Fiddler**:抓包工具,它可以修改请求的数据,甚至可以修改服务器返回的数据,功能非常强大,是 Web 调试的利器。 +2. **HttpWatch**: 可用于录制 HTTP 请求信息的工具。 + +## 常见的性能优化策略 性能优化之前我们需要对请求经历的各个环节进行分析,排查出可能出现性能瓶颈的地方,定位问题。 @@ -146,4 +173,6 @@ category: 高可用 3. 系统是否存在死锁的地方? 4. 系统是否存在内存泄漏?(Java 的自动回收内存虽然很方便,但是,有时候代码写的不好真的会造成内存泄漏) 5. 数据库索引使用是否合理? -6. ...... +6. …… + + diff --git a/docs/high-availability/redundancy.md b/docs/high-availability/redundancy.md index 71b86b46f72..9d14d726675 100644 --- a/docs/high-availability/redundancy.md +++ b/docs/high-availability/redundancy.md @@ -1,6 +1,7 @@ --- title: 冗余设计详解 category: 高可用 +icon: cluster --- 冗余设计是保证系统和数据高可用的最常的手段。 @@ -42,3 +43,5 @@ category: 高可用 - [《从零开始学架构》— 28 | 业务高可用的保障:异地多活架构](http://gk.link/a/10pKZ) 不过,这些文章大多也都是在介绍概念知识。目前,网上还缺少真正介绍具体要如何去实践落地异地多活架构的资料。 + + diff --git a/docs/high-availability/timeout-and-retry.md b/docs/high-availability/timeout-and-retry.md index 2dc78454ac1..3c7ba1ac9cd 100644 --- a/docs/high-availability/timeout-and-retry.md +++ b/docs/high-availability/timeout-and-retry.md @@ -1,6 +1,7 @@ --- title: 超时&重试详解 category: 高可用 +icon: retry --- 由于网络问题、系统或者服务内部的 Bug、服务器宕机、操作系统崩溃等问题的不确定性,我们的系统或者服务永远不可能保证时刻都是可用的状态。 @@ -50,13 +51,22 @@ category: 高可用 重试的核心思想是通过消耗服务器的资源来尽可能获得请求更大概率被成功处理。由于瞬态故障和偶然性故障是很少发生的,因此,重试对于服务器的资源消耗几乎是可以被忽略的。 +### 常见的重试策略有哪些? + +常见的重试策略有两种: + +1. **固定间隔时间重试**:每次重试之间都使用相同的时间间隔,比如每隔 1.5 秒进行一次重试。这种重试策略的优点是实现起来比较简单,不需要考虑重试次数和时间的关系,也不需要维护额外的状态信息。但是这种重试策略的缺点是可能会导致重试过于频繁或过于稀疏,从而影响系统的性能和效率。如果重试间隔太短,可能会对目标系统造成过大的压力,导致雪崩效应;如果重试间隔太长,可能会导致用户等待时间过长,影响用户体验。 +2. **梯度间隔重试**:根据重试次数的增加去延长下次重试时间,比如第一次重试间隔为 1 秒,第二次为 2 秒,第三次为 4 秒,以此类推。这种重试策略的优点是能够有效提高重试成功的几率(随着重试次数增加,但是重试依然不成功,说明目标系统恢复时间比较长,因此可以根据重试次数延长下次重试时间),也能通过柔性化的重试避免对下游系统造成更大压力。但是这种重试策略的缺点是实现起来比较复杂,需要考虑重试次数和时间的关系,以及设置合理的上限和下限值。另外,这种重试策略也可能会导致用户等待时间过长,影响用户体验。 + +这两种适合的场景各不相同。固定间隔时间重试适用于目标系统恢复时间比较稳定和可预测的场景,比如网络波动或服务重启。梯度间隔重试适用于目标系统恢复时间比较长或不可预测的场景,比如网络故障和服务故障。 + ### 重试的次数如何设置? 重试的次数不宜过多,否则依然会对系统负载造成比较大的压力。 -重试的次数通常建议设为 3 次。并且,我们通常还会设置重试的间隔,比如说我们要重试 3 次的话,第 1 次请求失败后,等待 1 秒再进行重试,第 2 次请求失败后,等待 2 秒再进行重试,第 3 次请求失败后,等待 3 秒再进行重试。 +重试的次数通常建议设为 3 次。大部分情况下,我们还是更建议使用梯度间隔重试策略,比如说我们要重试 3 次的话,第 1 次请求失败后,等待 1 秒再进行重试,第 2 次请求失败后,等待 2 秒再进行重试,第 3 次请求失败后,等待 3 秒再进行重试。 -### 重试幂等 +### 什么是重试幂等? 超时和重试机制在实际项目中使用的话,需要注意保证同一个请求没有被多次执行。 @@ -64,7 +74,13 @@ category: 高可用 举个例子:用户支付购买某个课程,结果用户支付的请求由于重试的问题导致用户购买同一门课程支付了两次。对于这种情况,我们在执行用户购买课程的请求的时候需要判断一下用户是否已经购买过。这样的话,就不会因为重试的问题导致重复购买了。 +### Java 中如何实现重试? + +如果要手动编写代码实现重试逻辑的话,可以通过循环(例如 while 或 for 循环)或者递归实现。不过,一般不建议自己动手实现,有很多第三方开源库提供了更完善的重试机制实现,例如 Spring Retry、Resilience4j、Guava Retrying。 + ## 参考 - 微服务之间调用超时的设置治理: - 超时、重试和抖动回退: + + diff --git a/docs/high-performance/cdn.md b/docs/high-performance/cdn.md index 3049b979930..f4ca0eab5f2 100644 --- a/docs/high-performance/cdn.md +++ b/docs/high-performance/cdn.md @@ -1,5 +1,5 @@ --- -title: CDN常见问题总结 +title: CDN工作原理详解 category: 高性能 head: - - meta @@ -33,7 +33,7 @@ head: ![阿里云文档:https://help.aliyun.com/document_detail/64836.html](https://oss.javaguide.cn/github/javaguide/high-performance/cdn/cdn-aliyun-dcdn.png) -绝大部分公司都会在项目开发中交使用 CDN 服务,但很少会有自建 CDN 服务的公司。基于成本、稳定性和易用性考虑,建议直接选择专业的云厂商(比如阿里云、腾讯云、华为云、青云)或者 CDN 厂商(比如网宿、蓝汛)提供的开箱即用的 CDN 服务。 +绝大部分公司都会在项目开发中使用 CDN 服务,但很少会有自建 CDN 服务的公司。基于成本、稳定性和易用性考虑,建议直接选择专业的云厂商(比如阿里云、腾讯云、华为云、青云)或者 CDN 厂商(比如网宿、蓝汛)提供的开箱即用的 CDN 服务。 很多朋友可能要问了:**既然是就近访问,为什么不直接将服务部署在多个不同的地方呢?** @@ -52,13 +52,22 @@ head: ### 静态资源是如何被缓存到 CDN 节点中的? -你可以通过预热的方式将源站的资源同步到 CDN 的节点中。这样的话,用户首次请求资源可以直接从 CDN 节点中取,无需回源。这样可以降低源站压力,提升用户体验。 +你可以通过 **预热** 的方式将源站的资源同步到 CDN 的节点中。这样的话,用户首次请求资源可以直接从 CDN 节点中取,无需回源。这样可以降低源站压力,提升用户体验。 -如果不预热的话,你访问的资源可能不再 CDN 节点中,这个时候 CDN 节点将请求源站获取资源,这个过程是大家经常说的 **回源**。 +如果不预热的话,你访问的资源可能不在 CDN 节点中,这个时候 CDN 节点将请求源站获取资源,这个过程是大家经常说的 **回源**。 -**命中率** 和 **回源率** 是衡量 CDN 服务质量两个重要指标。命中率越高越好,回源率越低越好。 +> - 回源:当 CDN 节点上没有用户请求的资源或该资源的缓存已经过期时,CDN 节点需要从原始服务器获取最新的资源内容,这个过程就是回源。当用户请求发生回源的话,会导致该请求的响应速度比未使用 CDN 还慢,因为相比于未使用 CDN 还多了一层 CDN 的调用流程。 +> - 预热:预热是指在 CDN 上提前将内容缓存到 CDN 节点上。这样当用户在请求这些资源时,能够快速地从最近的 CDN 节点获取到而不需要回源,进而减少了对源站的访问压力,提高了访问速度。 + +![CDN 回源](https://oss.javaguide.cn/github/javaguide/high-performance/cdn/cdn-back-to-source.png) + +如果资源有更新的话,你也可以对其 **刷新** ,删除 CDN 节点上缓存的旧资源,并强制 CDN 节点回源站获取最新资源。 + +几乎所有云厂商提供的 CDN 服务都具备缓存的刷新和预热功能(下图是阿里云 CDN 服务提供的相应功能): -如果资源有更新的话,你也可以对其 **刷新** ,删除 CDN 节点上缓存的资源,当用户访问对应的资源时直接回源获取最新的资源,并重新缓存。 +![CDN 缓存的刷新和预热](https://oss.javaguide.cn/github/javaguide/high-performance/cdn/cdn-refresh-warm-up.png) + +**命中率** 和 **回源率** 是衡量 CDN 服务质量两个重要指标。命中率越高越好,回源率越低越好。 ### 如何找到最合适的 CDN 节点? @@ -95,7 +104,7 @@ CDN 服务提供商几乎都提供了这种比较基础的防盗链机制。 时间戳防盗链 URL 示例: -``` +```plain http://cdn.wangsu.com/4/123.mp3? wsSecret=79aead3bd7b5db4adeffb93a010298b5&wsTime=1601026312 ``` @@ -122,3 +131,5 @@ http://cdn.wangsu.com/4/123.mp3? wsSecret=79aead3bd7b5db4adeffb93a010298b5&wsTim - 时间戳防盗链 - 七牛云 CDN: - CDN 是个啥玩意?一文说个明白: - 《透视 HTTP 协议》- 37 | CDN:加速我们的网络服务: + + diff --git a/docs/high-performance/data-cold-hot-separation.md b/docs/high-performance/data-cold-hot-separation.md new file mode 100644 index 00000000000..d7ae70c2bfd --- /dev/null +++ b/docs/high-performance/data-cold-hot-separation.md @@ -0,0 +1,68 @@ +--- +title: 数据冷热分离详解 +category: 高性能 +head: + - - meta + - name: keywords + content: 数据冷热分离,冷数据迁移,冷数据存储 + - - meta + - name: description + content: 数据冷热分离是指根据数据的访问频率和业务重要性,将数据分为冷数据和热数据,冷数据一般存储在存储在低成本、低性能的介质中,热数据高性能存储介质中。 +--- + +## 什么是数据冷热分离? + +数据冷热分离是指根据数据的访问频率和业务重要性,将数据分为冷数据和热数据,冷数据一般存储在存储在低成本、低性能的介质中,热数据高性能存储介质中。 + +### 冷数据和热数据 + +热数据是指经常被访问和修改且需要快速访问的数据,冷数据是指不经常访问,对当前项目价值较低,但需要长期保存的数据。 + +冷热数据到底如何区分呢?有两个常见的区分方法: + +1. **时间维度区分**:按照数据的创建时间、更新时间、过期时间等,将一定时间段内的数据视为热数据,超过该时间段的数据视为冷数据。例如,订单系统可以将 1 年前的订单数据作为冷数据,1 年内的订单数据作为热数据。这种方法适用于数据的访问频率和时间有较强的相关性的场景。 +2. **访问频率区分**:将高频访问的数据视为热数据,低频访问的数据视为冷数据。例如,内容系统可以将浏览量非常低的文章作为冷数据,浏览量较高的文章作为热数据。这种方法需要记录数据的访问频率,成本较高,适合访问频率和数据本身有较强的相关性的场景。 + +几年前的数据并不一定都是冷数据,例如一些优质文章发表几年后依然有很多人访问,大部分普通用户新发表的文章却基本没什么人访问。 + +这两种区分冷热数据的方法各有优劣,实际项目中,可以将两者结合使用。 + +### 冷热分离的思想 + +冷热分离的思想非常简单,就是对数据进行分类,然后分开存储。冷热分离的思想可以应用到很多领域和场景中,而不仅仅是数据存储,例如: + +- 邮件系统中,可以将近期的比较重要的邮件放在收件箱,将比较久远的不太重要的邮件存入归档。 +- 日常生活中,可以将常用的物品放在显眼的位置,不常用的物品放入储藏室或者阁楼。 +- 图书馆中,可以将最受欢迎和最常借阅的图书单独放在一个显眼的区域,将较少借阅的书籍放在不起眼的位置。 +- …… + +### 数据冷热分离的优缺点 + +- 优点:热数据的查询性能得到优化(用户的绝大部分操作体验会更好)、节约成本(可以冷热数据的不同存储需求,选择对应的数据库类型和硬件配置,比如将热数据放在 SSD 上,将冷数据放在 HDD 上) +- 缺点:系统复杂性和风险增加(需要分离冷热数据,数据错误的风险增加)、统计效率低(统计的时候可能需要用到冷库的数据)。 + +## 冷数据如何迁移? + +冷数据迁移方案: + +1. 业务层代码实现:当有对数据进行写操作时,触发冷热分离的逻辑,判断数据是冷数据还是热数据,冷数据就入冷库,热数据就入热库。这种方案会影响性能且冷热数据的判断逻辑不太好确定,还需要修改业务层代码,因此一般不会使用。 +2. 任务调度:可以利用 xxl-job 或者其他分布式任务调度平台定时去扫描数据库,找出满足冷数据条件的数据,然后批量地将其复制到冷库中,并从热库中删除。这种方法修改的代码非常少,非常适合按照时间区分冷热数据的场景。 +3. 监听数据库的变更日志 binlog :将满足冷数据条件的数据从 binlog 中提取出来,然后复制到冷库中,并从热库中删除。这种方法可以不用修改代码,但不适合按照时间维度区分冷热数据的场景。 + +如果你的公司有 DBA 的话,也可以让 DBA 进行冷数据的人工迁移,一次迁移完成冷数据到冷库。然后,再搭配上面介绍的方案实现后续冷数据的迁移工作。 + +## 冷数据如何存储? + +冷数据的存储要求主要是容量大,成本低,可靠性高,访问速度可以适当牺牲。 + +冷数据存储方案: + +- 中小厂:直接使用 MySQL/PostgreSQL 即可(不改变数据库选型和项目当前使用的数据库保持一致),比如新增一张表来存储某个业务的冷数据或者使用单独的冷库来存放冷数据(涉及跨库查询,增加了系统复杂性和维护难度) +- 大厂:Hbase(常用)、RocksDB、Doris、Cassandra + +如果公司成本预算足的话,也可以直接上 TiDB 这种分布式关系型数据库,直接一步到位。TiDB 6.0 正式支持数据冷热存储分离,可以降低 SSD 使用成本。使用 TiDB 6.0 的数据放置功能,可以在同一个集群实现海量数据的冷热存储,将新的热数据存入 SSD,历史冷数据存入 HDD。 + +## 案例分享 + +- [如何快速优化几千万数据量的订单表 - 程序员济癫 - 2023](https://www.cnblogs.com/fulongyuanjushi/p/17910420.html) +- [海量数据冷热分离方案与实践 - 字节跳动技术团队 - 2022](https://mp.weixin.qq.com/s/ZKRkZP6rLHuTE1wvnqmAPQ) diff --git a/docs/high-performance/deep-pagination-optimization.md b/docs/high-performance/deep-pagination-optimization.md new file mode 100644 index 00000000000..0d39e627cef --- /dev/null +++ b/docs/high-performance/deep-pagination-optimization.md @@ -0,0 +1,140 @@ +--- +title: 深度分页介绍及优化建议 +category: 高性能 +head: + - - meta + - name: keywords + content: 深度分页 + - - meta + - name: description + content: 查询偏移量过大的场景我们称为深度分页,这会导致查询性能较低。深度分页可以采用范围查询、子查询、INNER JOIN 延迟关联、覆盖索引等方法进行优化。 +--- + +## 深度分页介绍 + +查询偏移量过大的场景我们称为深度分页,这会导致查询性能较低,例如: + +```sql +# MySQL 在无法利用索引的情况下跳过1000000条记录后,再获取10条记录 +SELECT * FROM t_order ORDER BY id LIMIT 1000000, 10 +``` + +## 深度分页问题的原因 + +当查询偏移量过大时,MySQL 的查询优化器可能会选择全表扫描而不是利用索引来优化查询。这是因为扫描索引和跳过大量记录可能比直接全表扫描更耗费资源。 + +![深度分页问题](https://oss.javaguide.cn/github/javaguide/mysql/deep-pagination-phenomenon.png) + +不同机器上这个查询偏移量过大的临界点可能不同,取决于多个因素,包括硬件配置(如 CPU 性能、磁盘速度)、表的大小、索引的类型和统计信息等。 + +![转全表扫描的临界点](https://oss.javaguide.cn/github/javaguide/mysql/deep-pagination-phenomenon-critical-point.png) + +MySQL 的查询优化器采用基于成本的策略来选择最优的查询执行计划。它会根据 CPU 和 I/O 的成本来决定是否使用索引扫描或全表扫描。如果优化器认为全表扫描的成本更低,它就会放弃使用索引。不过,即使偏移量很大,如果查询中使用了覆盖索引(covering index),MySQL 仍然可能会使用索引,避免回表操作。 + +## 深度分页优化建议 + +这里以 MySQL 数据库为例介绍一下如何优化深度分页。 + +### 范围查询 + +当可以保证 ID 的连续性时,根据 ID 范围进行分页是比较好的解决方案: + +```sql +# 查询指定 ID 范围的数据 +SELECT * FROM t_order WHERE id > 100000 AND id <= 100010 ORDER BY id +# 也可以通过记录上次查询结果的最后一条记录的ID进行下一页的查询: +SELECT * FROM t_order WHERE id > 100000 LIMIT 10 +``` + +这种基于 ID 范围的深度分页优化方式存在很大限制: + +1. **ID 连续性要求高**: 实际项目中,数据库自增 ID 往往因为各种原因(例如删除数据、事务回滚等)导致 ID 不连续,难以保证连续性。 +2. **排序问题**: 如果查询需要按照其他字段(例如创建时间、更新时间等)排序,而不是按照 ID 排序,那么这种方法就不再适用。 +3. **并发场景**: 在高并发场景下,单纯依赖记录上次查询的最后一条记录的 ID 进行分页,容易出现数据重复或遗漏的问题。 + +### 子查询 + +我们先查询出 limit 第一个参数对应的主键值,再根据这个主键值再去过滤并 limit,这样效率会更快一些。 + +阿里巴巴《Java 开发手册》中也有对应的描述: + +> 利用延迟关联或者子查询优化超多分页场景。 +> +> ![](https://oss.javaguide.cn/github/javaguide/mysql/alibaba-java-development-handbook-paging.png) + +```sql +# 通过子查询来获取 id 的起始值,把 limit 1000000 的条件转移到子查询 +SELECT * FROM t_order WHERE id >= (SELECT id FROM t_order where id > 1000000 limit 1) LIMIT 10; +``` + +**工作原理**: + +1. 子查询 `(SELECT id FROM t_order where id > 1000000 limit 1)` 会利用主键索引快速定位到第 1000001 条记录,并返回其 ID 值。 +2. 主查询 `SELECT * FROM t_order WHERE id >= ... LIMIT 10` 将子查询返回的起始 ID 作为过滤条件,使用 `id >=` 获取从该 ID 开始的后续 10 条记录。 + +不过,子查询的结果会产生一张新表,会影响性能,应该尽量避免大量使用子查询。并且,这种方法只适用于 ID 是正序的。在复杂分页场景,往往需要通过过滤条件,筛选到符合条件的 ID,此时的 ID 是离散且不连续的。 + +当然,我们也可以利用子查询先去获取目标分页的 ID 集合,然后再根据 ID 集合获取内容,但这种写法非常繁琐,不如使用 INNER JOIN 延迟关联。 + +### 延迟关联 + +延迟关联与子查询的优化思路类似,都是通过将 `LIMIT` 操作转移到主键索引树上,减少回表次数。相比直接使用子查询,延迟关联通过 `INNER JOIN` 将子查询结果集成到主查询中,避免了子查询可能产生的临时表。在执行 `INNER JOIN` 时,MySQL 优化器能够利用索引进行高效的连接操作(如索引扫描或其他优化策略),因此在深度分页场景下,性能通常优于直接使用子查询。 + +```sql +-- 使用 INNER JOIN 进行延迟关联 +SELECT t1.* +FROM t_order t1 +INNER JOIN (SELECT id FROM t_order where id > 1000000 LIMIT 10) t2 ON t1.id = t2.id; +``` + +**工作原理**: + +1. 子查询 `(SELECT id FROM t_order where id > 1000000 LIMIT 10)` 利用主键索引快速定位目标分页的 10 条记录的 ID。 +2. 通过 `INNER JOIN` 将子查询结果与主表 `t_order` 关联,获取完整的记录数据。 + +除了使用 INNER JOIN 之外,还可以使用逗号连接子查询。 + +```sql +-- 使用逗号进行延迟关联 +SELECT t1.* FROM t_order t1, +(SELECT id FROM t_order where id > 1000000 LIMIT 10) t2 +WHERE t1.id = t2.id; +``` + +**注意**: 虽然逗号连接子查询也能实现类似的效果,但为了代码可读性和可维护性,建议使用更规范的 `INNER JOIN` 语法。 + +### 覆盖索引 + +索引中已经包含了所有需要获取的字段的查询方式称为覆盖索引。 + +**覆盖索引的好处:** + +- **避免 InnoDB 表进行索引的二次查询,也就是回表操作:** InnoDB 是以聚集索引的顺序来存储的,对于 InnoDB 来说,二级索引在叶子节点中所保存的是行的主键信息,如果是用二级索引查询数据的话,在查找到相应的键值后,还要通过主键进行二次查询才能获取我们真实所需要的数据。而在覆盖索引中,二级索引的键值中可以获取所有的数据,避免了对主键的二次查询(回表),减少了 IO 操作,提升了查询效率。 +- **可以把随机 IO 变成顺序 IO 加快查询效率:** 由于覆盖索引是按键值的顺序存储的,对于 IO 密集型的范围查找来说,对比随机从磁盘读取每一行的数据 IO 要少的多,因此利用覆盖索引在访问时也可以把磁盘的随机读取的 IO 转变成索引查找的顺序 IO。 + +```sql +# 如果只需要查询 id, code, type 这三列,可建立 code 和 type 的覆盖索引 +SELECT id, code, type FROM t_order +ORDER BY code +LIMIT 1000000, 10; +``` + +**⚠️注意**: + +- 当查询的结果集占表的总行数的很大一部分时,MySQL 查询优化器可能选择放弃使用索引,自动转换为全表扫描。 +- 虽然可以使用 `FORCE INDEX` 强制查询优化器走索引,但这种方式可能会导致查询优化器无法选择更优的执行计划,效果并不总是理想。 + +## 总结 + +本文总结了几种常见的深度分页优化方案: + +1. **范围查询**: 基于 ID 连续性进行分页,通过记录上一页最后一条记录的 ID 来获取下一页数据。适合 ID 连续且按 ID 查询的场景,但在 ID 不连续或需要按其他字段排序时存在局限。 +2. **子查询**: 先通过子查询获取分页的起始主键值,再根据主键进行筛选分页。利用主键索引提高效率,但子查询会生成临时表,复杂场景下性能不佳。 +3. **延迟关联 (INNER JOIN)**: 使用 `INNER JOIN` 将分页操作转移到主键索引上,减少回表次数。相比子查询,延迟关联的性能更优,适合大数据量的分页查询。 +4. **覆盖索引**: 通过索引直接获取所需字段,避免回表操作,减少 IO 开销,适合查询特定字段的场景。但当结果集较大时,MySQL 可能会选择全表扫描。 + +## 参考 + +- 聊聊如何解决 MySQL 深分页问题 - 捡田螺的小男孩: +- 数据库深分页介绍及优化方案 - 京东零售技术: +- MySQL 深分页优化 - 得物技术: diff --git a/docs/high-performance/images/cdn/cdn-101.drawio b/docs/high-performance/images/cdn/cdn-101.drawio deleted file mode 100644 index 87e4159be9f..00000000000 --- a/docs/high-performance/images/cdn/cdn-101.drawio +++ /dev/null @@ -1 +0,0 @@ -7Vtdc9rIEv01PO4tCUmO/SgQtpXyCGMgXvwmBJElsPE1wkj69fec1ggExrvZ3KQ2VbvOA8NoPrrP6e7pHpGW1X3Kr17Dl0e1ms2XrbYxy1uW12rj75ODD/YUVc/FxaeqI35NZlWXue8YJuVcdxq6d5PM5uuDgdlqtcySl8POaPX8PI+yg77w9XW1PRz2dbU83PUljOfvOoZRuHzfe5/Mskfda55d7B9cz5P4sd76zLGrJ09hPVqrsn4MZ6tto8vqtazu62qVVa2nvDtfEr0amGre5QdPd5K9zp+zb5lghPMwv/O+WFfhrT/69LI07ozfLC3cW7jcaJVbPad17rTcbqtntzrdFhhjz2Wrg8al9Lifw7fwitS02mdL7N6ZvlLDrNC4nf13Q7U6X1fP2W9rYdXFAPPsJd8/RCuWz96nVsdrXZhsuJ3WxcVunxT7iAn8J3rWKO72aGfznNMfs6clOkw019nrajHvrparV/Q8r57nlCFZLo+6wmUSP+NrBOjm6O+8zV+zBKy7+sFTMptxm872Mcnmw5cw4p5b2Dj6Xleb59mcqBr4tgyn82UnjBax9B9vDgC0VZtn+ns9pNW2OvJP1szCLFk960XXi3kWPdZfasMxBLRj1rUhUIV53ujSVnA1Xz3Ns9cCQ+qnhiZd+6R1rr9vGxb+Sfc9No1b94XaqeLd0nuzQ0Nb3l+xQvO9FR6zTXhfjugEgl8d/nvHPZ6cyd97zKu/Y5D3pNKQ4mW4Xv9lxHdhKJzWUht/yMTO/WomjLP3TFgnmDj/aUy0v52JA/xORKM/ZPr7sfv7sLH+HBv46gubyZMcLLvAcsMocbtaJ9rJp6ssWz19HD5o2fJ3IjhlK6Ifrl+qA+9rktNuO7KlW/cadQ/aszALEYCrr+3L9VvcandyRM129/Y6aD8UHXt6n2+i0kjC6zsj8lZvN9bMmhWOpQrnLXqK3lTqblX3opw9RYl//ZhNr5yy//y4Du+d19vh59Xs+m7bT87fMMu6eY7Km6eL4qE4z/ujhXNjVeP8pGPcDv08SCdb5bl24Llo90z0p9Ory1L2vw/epokfz56Wy5nx+W3uGYnqulvf621VOojVyC2CLp9/KaL28m2a4vnQzrHG9uZ+ucD6RZAODHy3wvs7I8T8wIsT/+pxGd7PVjP9/TbNFw/3DyU+y9n1cv0w7Bjz3ztL6GI8/P5o3LSDMir8t9t0+/Zw9eUpKu3z6OrSCLsd7PE56HvQMY2s/siP9+3PHmahPS76o0ncH7G9oDxxkI7RdrfYW+vgmgpzpX2lMjUa5LvnXQNzeptg5DtBYpiqa2+DUrVvRqoNXa2gXBQ3ox77C/BiD8rxFnMtVU7i5v7ArR2M1AZYG33BcJyrJ7XFXANzHeX5BvrquZlKJ/lu7oj9fqG8OMOzvNZPpewfWMqDTiMXa0dlkLhlULobcG2qRO+Dz0mJfYY2uB7YvufGzfUP5lLXMnIwJ5f+1N/2e8qBzljTbwOrgv2BF23Rziu5lcW1gXG7to1g1Ct27QbeBzw0+TnN4dlo5Bda9xjzHd2/xjpoD8xAdHTxbACOBsbIa4wfKeMmVYUqwW3qV7p0gb+3qHR8foHtDLBXr60E5xh4xmXfu4PtjLlOSWz7go8L/sGh19twDvAxgq5tBl6t6wB7jY3KfnzIFhuyL+3HI46KeBnB0DaAB+2npK8Ab2IG7G3gMDAG5YI2sBXe04hct4UjrR+wtlWCeaOJvdtT7PdzqshHutjLp/fywRFsrIQ8dnMvrLWTYVD6lthzusD4RakK24HeR/strD7XFJuhzvX3h/RQZ7ehs9GQY78f7J5ylNANPtPcL4Ld1rI29kvpk9X6De7FbxvfwR14Ho3b4NlRYrOLrdhsSbuekEPEOvjhaJKjnfchZx86BtcvgYJPg3OHnMKXzcpO79Z72zHKPn218j3Io+gnBewG+yj6Gfxnwf0LYg77sFUZG9oHC3Ioe12pPICc/b0vgvMJ1y+ACXyUHCgTmFAf42YUWe9lcXeyBGVsaf0bsQq2uh9b0OYhyz7WpfQlcMs4vospR/0p8RD7aq6bYzzWXUi/8sZVXLimL03Mem7fG0Mm10bsQIz0NwHavhdvA/Jf0nYmJvFhTKAv1j5Y2VVE3sVOJoXRxCrbY+XusIIv7bDi3Bqrm33sQNwkR70Cdk7cMDbiPm3YmoF4STtvY4028OIaJWSDPIxnwh1iMvwv7VnBk9rHRYnjvhVoG4W/Qk7Esy5j8Bj25pITG+vSR9RJfIAbxuWMNUEV+yrMewqxhpz1bOCM+DvQGIxt+u8+9op80h6XwBpy9z3GlAXbJc8h+EzBOCLxF/4FrDH+DjYcMd7xPLIob38UWwd2KmdWve+RPddnV1JxCF50jOR5Cb/z9Lrwazwz5Lws7BxnLGJcU84BuJ/wXIFvR/RRxo/KRq/hU+jTGBiwiXYgeQdttMILZ7UNHez+6IExm7HcrHSI4YfS5hld+3vDP/c2VnPcGNfQO6Jt5dwLdrqLDyc5I5fpBLzGJvyIcpa0VVXCRzEW3G/7cga6dbwwGZcUcojDWO7v4v24HCDXA1ZVvAVfiJXUu8QZkvZgu/7BPsAqr9r0Zcol3NnQVfsHeB2SM1Uc9lec4nygX+l4vz9LEQtgN5OiOs8n5In8ltCzigU9ZYB/+MiCeknshf/kcg6Nav//8PyATpRb8MkCj/FF7AdyjLVPGbAFzc8C+QbWxPe22JgHu4GcwG7vp+CQPq/PgmZ/hW/J3ABriX/EZn22MDYxrgfkb4cf7LMAZiM5P22JE8BvvjD2uQTPgaTKAad7LiU32Z1Pi21zPHyQNjSxuNc+t2nkKtcr5PduFt4PLvwUMdYbJzfIfW8s1AmlzcrkOwu/9geF34fFsW2+q4Xt8/flXt3348s9+8TV2FmrI3dhvfOW+6l1fs6rsYuLlnv+i95ONe+fzuvvWkjz51Op7zXOTlwwnSrd2z+tdHe+/4Lp69d2FJ26YJqdTc+cs1/iIqn96Ze7SDp7h3jXC+hAF93WuSdXym7LNf9JfuN8QPcv7Def/r3y+oFXXiidYltK6h975YX1I/vnX3mx/GRZzNLtC0odpFtDo1RInYK2ypAy5jejCT+dG0mtUAKhrEYb6SlLI5fjkTrP0n17kQXVeIzp8RpjExRGAf3MaddA34TPS5RkSG98tA2MReo1RIkm+/WQnt4pYor0J66u0R6x/nijUBZwfcWro8QwOR+p3EYVBlKsGeSJN7wqqORBGSXtAcZEHMNrHJR+WBP7T5kKdlmeLzEWaV1X0qgMZZ2F9Fbkga6O7z0oJWmn7+CZWcndi1nyyR5DV8vR4xp2hQuxGsdIgW2Mh85RzGse9EN+8u9vJBUFlliX+q159Qf7gB5fBEusVcq6SNt98tE1WLLGLF+mvHoYujVPW8rL6znoxz1Nnyl04SIVfKjXJV8p+UT5TJmhwyIWXIYu5bAqOb6sFXEeUo8vKfp5raR1VDFSVovzlXwP1krKlJ6+WpxxPFPbDdPjG6a+BdcR3Yup1yMXMTDMJ7xSQekN/eJ+padggvHSxnhDUl6UisBsI2kuZKR9BvfbnLYEu9z0r1TF75ByxsS0rGwG+A4N4MSrRrEpEyW3IfaYcG2UB0NiLzyRc2OKlBpzxB6QpltiExU2RsVbrGUUW7anwkcvIx8TXgt1OXdMu8gr/oFXApsu7HKedDLYL3ghBkvYdo9XFSj5BlgjSMXvCvIH3xAMyT/tk2uhPd7y+pG4oty9S7Wfii/w2oBYBOIDA/GNPsdz/3Js0neU4Ch7sCzJKhuhfvC99BI88rqDcycl9Yd+pfhzwvERbRCYDfSnS1wYy2gHW833tlqHJSRx4vXKJfeFvcA+hpzni62J3RW0B5EHpd+YPGZ6LPXIK9uhDL621cezo9jo+AkS9M7tdedxdhXHD4iIIxSbvHyEpCw+C3hexZBEowHZJRt5xSa04mXUeGvIxRIvt4BQv7p4FRawuyURI6G1ifXQ+8yoIPJjuahR4mVoozBUsMy+J9G9QiGht/m01mJSupWniQdp60gH9NICaOI0sK2Ql51txTF2FU2lX0fegR5vYDzQ61IGRIfUB8OzI3Ti7Sl0xm3MdHhVwtLzG9EpUXJv5Coqhe89KcZuQ/ywqO1zXM2Vc2FcxbWismHaKDWoNIb97DQm/+LvBflnDBN0ZSxlcUUWXiMwfmhZTFVKLECpfqljmRvrT7FFiSPyXXENHV+q7zqGlLotcYlX3dinEF9n6T2UeFPtWVS6gZ2yOnN6Irtgw/PqaqvPQr9gjK58vOdAN8QOxsWAfs5roS3PjanYNl+P6bPOE6x4Lc5XP4KnPPu9Wa73eL31g8r1b89Vj8p1nbraf3Pxfv7dBd8snJ9/PVnwnUXn8+nXX6Lgc5yjgs/8xlLh5xV8F6evS9zWhbH/Yc8/qNo7/4DrX7fas0/8GOzfau/7f+CgGOFLZD0/uNrro4r82dVeIJfpg4IvjeQlGl8884V+ulwFI15esx2bgYUsdD/WHsqL9N0zS15keD6qXmRlV8giRr7DHOnkWubEkJdm2BPrlR+vNeNadvXCetyORnxJ5+/m4YTkCzq+pKn2sQZZc913z3nSMvtO5AUn1o+wj4rlBX1jHl9kHM45GJ9VL/QnB3vNF0ZDbslOeDEvuKr72MTnDucvfLnCF+Uy985vcjBLm1is4ukoduTl6+gh5wt/eSm2l6MpQzGrXiJ/9LyUSlKyj4Z+yTv94uM5h3gIfsSxMSc4wfnjn8lbfpe87/n4Xnmrl8R6jUBeXmq+fp8UarTYaC6LodeBHPBHVCzCS/l5Jf57TX5okwvm/sJB//pgbo6596rky9moso2nWF6oMjMkPn3ehIgMvjNLD/bw1eWBL9R7NjNBp+8tflAm+O3nWJ0Jtv/e3M/+hl+Tzp9nLn8ezgSCGVtSJXzha/a+u5GczML14y7VO/pdqSF/f46ylm4+O/jl+R/+ztE5kQXUfa/zZZglb4e/Vz+Fqd7hdpVAko9/DOwcZRfr1eY1mutZzZ+XHy307hWEffEf53ApwBvPs3dLCck7xf8P3r/ht6v/8l476Y/i/V0lcrzQd7OOr/v/HVEN3/8nE6v3Pw== \ No newline at end of file diff --git a/docs/high-performance/images/cdn/cdn-overview.drawio b/docs/high-performance/images/cdn/cdn-overview.drawio deleted file mode 100644 index 4cf6f69882f..00000000000 --- a/docs/high-performance/images/cdn/cdn-overview.drawio +++ /dev/null @@ -1 +0,0 @@ -7V1bV9tIs/01Xuuch5mlq2MeBRJEWW45xlKIeTOyIywbzAEbWfr1Z+9q2ZYNSchM8g2Zj2Qm6NLq7rrtquoupJZ9crM+ux/dXavFeDJvWcZ43bL9loU/71z84JVSXzk6eqcvZPfTsb5k7i4MptWkvmjUV1fT8eRhr+FysZgvp3f7F9PF7e0kXe5dG93fL4r9Zl8W8/1R70bZ5MmFQTqaP716MR0vr+urZvtod+P9ZJpdb4Zuu46+czPatK5JebgejRdF45IdtOyT+8ViqY9u1ieTObm3YYx+7vQrd7czu5/cLl/ywNB+79+uFiNn9bF7uz7KZx+8sz/senKPo/mqJrkVuK2O2/JOWoHTOj5pQWK8cto6xsGpXPE+jB5HZxRNy2rPMfrx1T0pXJY139r/tyJZx18Wt8s/HkSqHhqY7bv17iaOMvkZvGsd+60jkwfecevoaDtOjnFEBf5Mb2subsewlpM1H79e3sxxwcThw/J+MZucLOaLe1y5XdxOOIfpfH5waTSfZrc4TcG6Ca4fP07ul1NI3atv3EzHYw5zXFxPl5PB3SjlmAV0HNfuF6vb8YRcNXA2H11N5sejdJbJ9cPBwYBaq812fb5p0rLsY/krfS5Hy+nitu70YTZZptcbqjaKYwjTDqVeKwJJmKwbl2otOJssbibL+xJN6rtbodc26b6rz4uGhm+uXTeVu742qo0q23a9Uzsc1Jr3I1poPtXCQ2lPbsceDZpim48eHqbpvuwn6+nyc+N4SIb9abn1qb+uGSgn5SGfv8nayXgPJJ4ytsE49xm+ba7dT+YQ8uM+tDzHzHqEj4spZrKV21FnT2y22dnv4WGxuk8n9UNNIDjoxzGNg47e7Xe0HN1nk+WTjkS0W6r/hrStZ6StYeThbnT7IiDpEEi00IyrrfH9kWrTYpP77Op/LKfDVhbmaVjO0e7YNf73KQ4Bf+xWx2h1TALe0Sn/4wGu+K2g0/I0BLZbaNnZThks0LOuweyJ3kJ5usSIfXV9OQTdT0D36Er6I8V3lItIyj1uuf6zivttKztEiq37rEfZc1DPIojxp+HY7t/T6k2TxZcvD5Nfo2ju92GFinN34CUAzF++fLHS9IlLwZ1x+6rttg+xY+cTKNyMAPXDgP3DYnBM58CQnwFy+xlA6vwyIG8/4bgfDWh3MJujEzEkxBJey9M2dtTyOq/Urzc9d2dzXk/S/L7r+LZCft9X1yJtv1Ci1i+T6Lvv2xAClDseTm8kmt5yXWDv4+JhWkc2V4vlcnHz9ZhJ2x3+PCO55YIiGj3c6Sj/y3RNSR3LkN7mqrG5guPxaDmCJ9CnwOjHrGUdr6FS1snH95F1WR47VxfrVVoZ09H7cyP1F49de2yPS9dWpfuY3qSPKvcKdXJUjW/Safj+enl15la92+uH0YV7/3HwYTF+f170pp1HPGV3b9Oqe3NUXpaddS+euV1btwunx8bHQbiO8mGhfM9ReWpEebjG9fzq7LSS8S+ix6tpmI1v5vOx8eFx4htTdeIVoR8UKu9nKvbK6IT3P5WpNX+8ynF/4LCPonsxn6H/MvJnFs7t0cW5McLzkZ9Nw7Pr+ehivBjX5x/z9ezy4rLCz2r8fv5wOTg2Jp+P56DFuPx8bXStqErL8PFjXjxenn26SSunk56dGqOTY4zxIVLxbKXyYdmLlRX6yu6dOGXPTwwVny9UnBXdfOZEcWb1zgIzOnGqqMpclWdZFPdXqkqdXhw6oe9loG2N59E+cZQfmKSzm4dVlHtVdKacbq7Ar9SOSs/u5v1SxQHo99boswQfjagKwZN0peK+qaoEcwnQDverfhFNvRLzAt+CdeSHWTTAM37ioj88M1xFfh99h27opyvMy1V+ivFV1c1TtJ9V0cCzcOyoWNlq6pnqxHF7foY+PlgY0+zmGbiUmeoiMDAfjgG6kgz9cj7gx8zc0NiNgzXau+CX0xt4VW/AefUdPI/2HF9V4Bd40cf8kwq0OGpAueP5gbFHh4rDVVTNTOX3y2FprKOps8Y9G31kMo84qzD2UlVol4c2+rJDf8g5kd4laKPsih7mFPrJqgfao3xmDPy+0c2HoM1zo7NgDb0qyHvwHGMGK+UP7ajiM1uawKvEEj0eGC5kZShfQa59zN3BuPhLmfsZaE95BjqeoylYwr7wvFdElbceVrs5dWOFOUEX0AdksIYMSlXBfqr+MsqzVQ+6Bzopd8gqKCkXNTXW4A15alKOk+nxshcPyX88pwqMbaEfzg1z/xD2fN5TReRDn0j3bgzaG3ilLNBlPC9LAzqaFD0/LVQJuUK/e3EGu+ijvXIj0ZnQhU5k2o490Ip5xwF0h/SR54GBay70DXbCeQfsz6Zd4XkTc4CepLvnY+p4aEV5YmJ8t8c2eQA8iR4i3wNPAsgwq8AT6ix4B93wowXmXVBXMD8n8q9zdQLdGeB+DFuBrnXjEDJIjR50EDzEGJQ38GZgQL6Ow2OVJxwfY8xcBZvG/KmvblQlZjQ1YJuOpSrPiOLhsie2PsT4inJEO/AxTkrKB3NiHzZtfij89Ig40DlvX+9qmoVGP0N7tcRcXGKctrWgpN7gOmytDx1xiK20Xa1noBF2hrFS6BN0LN7pFvgL2YUmdNom74lhUQzbr5KGrDzgRFpS90M/g2woCw+87RMbLNgasA/0Th0b8hB7bshpJXOpiJEBxhoC41JLXSjcS6BXs0JjTgIdAIb6M9iE2AWwM3RE32GrGMONYuEhbQT4I2PClkkXaI/Pr4mB0KlKAd80Bs7WkAP0Y2Nv/Ikx44A4aw+AVZgz5gubqcY5+QssJZ4tMdaK/ITOoq8Q/A2o69Az2pX0AZ4Oca8PrIa95WEBPQTuJCvB1+ryWlUz6k8RxaSJdgGMyzGfHe4X1EfgoEmcpK1B70BDQhx2iWuqhC5BB8kHzFV4oCpiBfEePIG9aryeQV9gywODuAo/A6yuAvga6hjnHrhXJwbwOliDX2vtS2rdvlGF4H8uvBP7hv45wEz4sn5D14gFCrIYlrBRYyiySDFW4tDOd8dbjIBM+6CNfPOKgehaSoy2owR26YfgMc79PuVfUG+h/0bkb3SPmLvRi0TL20+1XwG+gPeu4txy+Kkcvs2fwYY4T9jYdCtz+kc+AxskHtFvAKvAx2GcFLBV2JMH7JmBVx7GAobBpzR0BpgNHa5m9CnQ4RnknWGOKWkyIEvwd2hqv435UJ+AhZh3g4ZU+7M4LMD3krYITIAeXbbjeEj60B7+ovSAG9CfivFR4gwgN0U7wLyBVReR6BrkD/p6Afwu/VylIOcImDKDbBX0OnSJ9YgL6AsK4hz6XZMG9Iv5Jhb0CnJgn6noJcYwRScGB34Udg+/AnrCCrhIvwj96APjYP8n1EXoXDWTOAK6SNtxqM/b2IV9Dxzwn3EN7D0P6Edc4nhEn0t/AeyBTZfieyr6TIlrDPHfcXqAH02sMtC/Q3rhG/qQHecA/gGHgW/oDzKBHSH2atqNzBU8IB6vd7EXYgHESsByHB8jdlPg5QzzViV0pBIshr5RpuiHOA1+QUa72ASyJPaEjImIPRX0wMScKE/qFWM76JhniJ4TZ3zoQjMOEL2GXlUBcRL+gb4Z9Pi0Ya9inIU2JvgDXVPUOeBW0ohD+o74B/qiOGS8SX8JXLsMtW7MwJu+FV3QB7GvmanxMMC9EHjjCf7TXoEpwBHoKeVQhfRDWY8+EnLoUZ+BPfTnkCdlAr1AvI3YJqL+0kfm4CVjnpwYCp8TM/YKSDewJ1nTPzRwzrkS7IGvgyyb+Aw6oMPAYMTK8Ksu5gDdBz5UlwvkFCvt01PzqhE7Y97EBFyn/z3NgZ/09XZEPwKZiB2Tp6VjcC7AA9o85CmxMtvYPeIC6awQHyMe6Ak+BfQtxDfSTDwQvA2FDsgd2CPxJfEsZ/tsJXZbEeub9PUlXmQMCdtrxBce5QeMQawNm8L8GAuAjwkxl7EjcxoTPC6GEvtksGVPbBttgREzxEtq43eber4UzGasVH3Im7Gc9qUz+HXquEedWwvm54gvqmxF3MLcahlv4k/oOnMN4Avk6o6nhsiffUZnWeO4YNwHnIYdxYjDaasV471rYB5wCjgJuYA+xHQV8ZG5C3TIAo7AXnGM/8fw6X36U/KEOrT1J+RDjc+72BP5Ce0WcS5i94I4BvklzEFokw5zH8RPsAnwgXmQzh8ybauMQxC3lLS7ocRdjOMYi8MXMM9xGBfp43qM0yHsgXJOqIeI8iRGA64idswzHXdNmQMEhc63mjFjI36+yBijQvZDM8rPgRmMk5ijJUYUEFdnlAXwkPzY9QGamvnUBkuaMasleVwFfhmFoTGXcZ9i7AYeOsQ00Dun/3AkhqxCG3KE/6Bfg90g9/wE/0t7od+GTNrhmcHYGrxNGefCDx8/NPMx2EEp/AJH1EW2ZBzOOAj9mep2SL8Ffnuwq0tg7YyxK/NWu/cZcQXt1YfO5OeIzUmDI+sDyFmo26bEJTHonxrNvJTzg34iX8nniLsy5szgUd+VuAsygv7g/qwZm0D3E9oq/A2en5I++pkk29lLuH+s/flSSUyPOSIu3M/VM/o45jKIoTF3e7gkBsKWmGcZI+pgyRgZtvx+cw+YUnnuCLpCHwCeQXfOLw5sAjrLeUAOwObJNMzSE/PukusiOeWo1yhS+/L2Y8aFI/73q1fktouuT1bknM7TBTmn86sW5Dp/eVF7PJp0vjy7qN1OO5OrL69iUftwd7Jt/+OL2kdPOH7iR9wR6njcCAre8d/jo/+mhezOV8T9eheynbeF7J+5kO0qLihxwevnLmRz4df51QvZDIThoBGwcxHw00OXix9IsBQCWqSWDN7gMIf8uQlclkxQuhKInqO9x/ZwuuN8dzxbRro92tBpIkAojRL0mVcnEqTwfoVklEEjjg20hQNkEirj0bGeq0gWB+mU+xj3OufiFZx/yf4ZzCIxYAKzlCS0ZFAwxnwQnJywX84HyYoc95cSuMp4TEaZoDvVFYO0EwZ1c7RFsMngAHNDIMfFR5kPaEWwdamgNwggQlls1PMOGDwaMoYEhqEkrOjD0Xwhr5AAIthFe9CcMqhEwssFu74sYvd0kIiALCR9D1x41Qu5n4SXTKKlXwSjTDBAV0FZIRmyrpiIcIFCy6mQxGTKZFNxTC68g+dcVL7c9Et55ZQnExTMGTTM9ObBQCc/eh6fHmRxToJrhH9Tjh3UNCoGybIQqOQ8emDALLxA4kSeM7hn+0gCcgbV7EdoL690EJ8x2UF4CPo98CLMeppO4QkXkXnMhQAGbFwM7HIhoE7QqJ/RRcFFKvASic6Z0vIdcJ4ZeVppnQF/EQRf+aFV65QZ3bBtQP1F3xkXtyodSCcSvF/5Mz4j+sBgTXRC88bQcsvqOYouO1cij2BJeQy5yCKLbAn1ol4kB7+42IsAlQsz0F9TJzJz6DaD0IDJMPqIcrG7kvKDbQgPKX/qJ/vCcVKUsqEx9ZDAn+e1nYotMFAmLyKxgb7YRo/tOX6VmLQdJXyUMbjgstQ6Qvq4cXEKOUrQSVuqSD/oq8Sep2yfUgfBs37901vpBZBgVS8kcF6F7ocBP/k05DnHtWUhbODIAhx1TfSupD7IfBAgJ5Tjsm5LOtZadziHsNbV6/YBNrrhFL73+OP74+vxWZYxJI7joWy9YKYlKOT2kJaQoFF/Jcu0pUhHU8U0MSlkmRDWxu08PZM4FClgdFsQY0ptE+2h9ZlpSc4nshyixMpwXCE1gGb2fEF3zYUprS2ktpZIEbWliQXV2pH3aaVcAuDWgT0CZ5Ggs42j0VSu18jbr9sbTI1KaF8p6MBltHx8wJ2seI47icUFXVmsLB3rhdxhgkNbrSRpuVHEbkPssNzoZ6KfFb+QaFwrtQ5TR0mBphj6s6WY8hd756aKYJhwV9pyLrK4sQ5lgwMJtJ6LKQuyTPr80xrLmNTJT9FFwRE5V+yjxhd9XmNIVR8LLjEhZDIstg5dp13X9FP/hDa9ODbT+F7W9kB/dVbUvjDkgpmlbTxwQRuwg7gY0c4tLvzRb1yJbnPjovZ1vvCKCyJlN9Z6L/c+LxCbeMvRRf8ozAPYezDtwm93bcQ4lfM3UryXx6oHKV4dulr/bMLnvKA48mtVTC7/PpvwyZ+nBan6z6tMBG2j/Q8ngs5zhYtfkcRf0VTn7/Pun+ON/ZZe/cw6oYj7U3lg/vQ6oVxStl+cXqWsp7F7MdzQ9viDj6cYpiLgQDgf83jG+SANSyqpA+F6tNDgmQhG9LEEJkyF6vsIACJZ5w5dpkJwQQX3yrrcA+U+SDWjWzGlngPpS79iiO7ZqkK61xgffLP0nrdn6PqUZK1uFPenDaZ9yg9l76p+Fi6cqWL9rIT9IdfWl9zX3NAHl47rfZtr7BEDGdAO91pFCIIga1Pv0SasaSkQOhvcZ0NbvUbb6H/vWdLKvQHuh8teRlj0Au4pKtmHYQDH65Gfcn9treetbPYNHlsb3YBLLrfHDX7vyaEpn+dl2I5j1gYI7azxcevrD+jHlb20eh86klqcvhH7jfYSTCqG9KA31LRwT5n1DqTx9g66w/0i1rKQz6wpY13TOXSHe5wh92MxZ/LHc5kW6JqSfgX+GLLO7m9olTTa0PrDGpfMkHGpP77sCZFfhl5vT6g/3PekzZFnFQNp1vP0K+5/e4XIPU8pa0tkVNPHPVoGvBGC8O2Yor8fcqkryGe7+dVjcQ9OapFkr2w3FusaNnPoV6Et+pzPuAdfISxzQffBeDObe4SR6Axp3pxf5vs0ew2ajcY8duOxrouhLGiDzTTHY23VZq6N8XLapO6/IXux28Y5ZMdahMSCnF0lOjsrRGdlf37oyt46Q9N4uOZ+ZE9qmZQTvb+LuO/PvVqpo8plHwp6ev6w0x2D9Tuu0ra3lj0QhrMxa60U7Qz2w72kpCTPZf+8yozaBkvKUMY6U6yxo5w2tljI/g11RWqLKANlMjXrcf9U6hsO5+Jt5xJVmV3T38Aq6OquLffoVpjLDuty2hJkSxzfYsrB9Zz8EP1q9rvmvq3i/j2uK6ntAy68py0Nzc2zrCeBHjvADlsSP1nayYqI8q+oO0O9h047rSg/bYNar1LKXfREag93vFrueOVteQVb2vKKz2541d1hB3BT6uFK6Dn5hrYpx7G47AC8pJ5buhZI+qikxkzwTGRnM/GMWLdxo3a4KDge2lGto3rfPpGaj0jqTDyp5VCyt3epnuUP+MY9UNnj0tineR4oS2ooq8ABn13W7GkeJKypKnfYm+iaRRwnldTyMZWhDfO4oh+CzZRSR0b89VOpUVLAOMxXam24Tx5JPWRm7+mp+KzNuAf6vPFdUy1DyKXGSPpL1k/U/ZaslRoa4i+R6sHHAuOa8+y7rAclNqpK9uf0PiR18T1sCtdqHhhcAook7qCOan6xrod7ob34kphNLDc1Ddxfl2Nb7++KvTfsc6djGxk32jXoTqlba44FPd3iw7MyoyxlqS4zZX+63pdVFWyUNT7Aw574QG+DF6y14V5nuY/l4RbvE9bNkFcab1nzXAlvqoT1R3Vt8m4c8Gqtj2nLnJfIzmF9ibYP5Uidpa/K/etaplwKiLZ4v/OlwALozbDU/nxIORlS25RnGgsC1uUiLmBN4FRjL+xnLX4o3tj/V/1HpfRSHvmzjFjnofVS6v20TRnQhVo+M8QbXCKsZpbomNR/hLSJnZ1OWeuVbXxB87rmb8XYAH2JfWTmxrcQm4jrEeW35R/0s9T1kqxzEJwA/yYzYxdLTKW+Q2LAq50sJTbZ+qdZ0WwPG5SlcDvKm7FNI1Z531yiAMb6yc9Zotgmlr/NLrTjPE33+Jtx8ovC/F25d61O57/xV3x+QJSvZmf06f72SxeYfouKAtN9bRUFmwm9VRQ8VcLfx27cFyzMvi15vVUUvFUUvFUUvFUUvFUUvFUUvFUUvFUU/AsqCn4gVn2VFQXuD+xj/44Jn2O8uoTv6e74f3vCt1XC3yjhe2bR6y3he0v43hK+t4TvLeF7S/jeEr63hO8t4fsXJnw/EKu+zoTvBS/CbLxft84gxqOH623q1shAeP3jaIms4lauWIa9zUs2L8m2npaWd9qOcXr6fQm8rpfu2p2DXLJ99Ke738lL37trm9/t6he/edd9+nrOlyb+v+ULUZ2jfzzxf/pbx2eD7tO3BP+L8/z2V6T7ivP8FxREvOX5P5Lnp3zHkvPT83x/Vvz6PD/QtWMDvgNL3i3njJFLMw6PqiiM5N1Zoa1svvuQv08wXyAm0vWHt0PGi0UvyUp5p5+fmOM84PvqLFWNQ75b6RPfl8da/YtM5ylVaI/zGevz3ZG/7WfTdxhVQ+fJ/U/DzdzcycywpDaV7/OR+ta+GSVZJbWr/mz9aTv+5QLjMddc994PdS6hn6+PWUfJ9xspKwoyqfVDHGtivszlEa1/4DtfWfPoqNshzuWdaCVjOUT3rvBkynfg9ddSF+3PL6Kc79xB+5ts2x78cOT9lP4HvmenlJrPPEWseRxu28+HVi/OpL28U0a3t3bj30WYH+sh+TsThq7/Jn/CSt5HlF9fbNvO+J61hLRY43xzf9yO402/pFveGYaxNJ8hby3H+ANkW1+rZaA+L7IrZDs9eb8SMpOzTLf9vJHZrBydGJtrdT+nTVr5PqFF3Xb9nbGg35d3V2fFUTiVN63ybzPmNSB3tDlH7hIsUxtx9cXpamhdP6ZT/f6cIetc7Yw1jw7fH8d30nZv5N3BW9tMrU8PlxeRcXnR/0nx88t9wCupb2w/rdD6VsT87Bcpvhb/glf3JT9VYfzpbk6HNUPlZPetCjnbfKxi84WL+rH6Gxeb71185wMX5rciLx3BfoMdm/e66/j0Ba7zlUTv7sFOkOMcxAwvjd3ffa+jXxy5t5/bsvvONzMOPoaBCdefv2gc/W9r8xWNZz/VU9+bT5jt/YGR0ultphtACx2n0YYR7B91MOrJM1+WT7+xYW2/sXESeSp41Z/V+Jr9vhD0thDyUz634dr2ngLaP8c+nuvz13+Yo/2CPPQvo+sWJa0mTBovg8lvJqjfhclXhn5O+wC0DtPcl6KfeYB+dvs/jH7PvbvrN0Q/u/GFoU7r+LR15MjvSBy3joJn6wHCj/8iPPxaEPjDeAg47FhHexr5h/n3LOY/gHkvWFT4HSLKvVXhvw6Vm18g+n5E+brWg98dQOq7vwqp1kFHh6tZvxhS3z33ywsaaqhlz0Lqi77m6OxhnBcQ5oIjfkUSx4jyOietjnxREqdHHpEO/3agDQYRkF+KeloUtYVAPbVX/6W1ra3/jNDP7GyC/w3WWf8c1uF098VS3Xz34Vc7+H8= \ No newline at end of file diff --git a/docs/high-performance/images/message-queue/message-queue-pub-sub-model.drawio b/docs/high-performance/images/message-queue/message-queue-pub-sub-model.drawio deleted file mode 100644 index 7a28c60d6db..00000000000 --- a/docs/high-performance/images/message-queue/message-queue-pub-sub-model.drawio +++ /dev/null @@ -1 +0,0 @@ -7Zttc6M2EIB/DTN3H+IRCPHy0SSmnfZuejPpTNtPHQwy5oIRB3Li3K+vBBLmRY59iR27PpwZB62EAO2j3dUKa/B2tfmlCPLlZxLhVDNAtNHgnWYYOjBc9o9LnmuJC2AtiIskEo22gvvkO5ZnCuk6iXDZaUgJSWmSd4UhyTIc0o4sKAry1G22IGn3qnkQ44HgPgzSofSvJKLLWuoY9lb+K07ipbyybokHngfhQ1yQdSaupxnQt3zfd+rqVSD7Eg9aLoOIPLVEcKbB24IQWh+tNrc45WMrh60+z99R29x3gTN6yAnfpnfu796j+zA34Hcr/Pz17rt9o4vHfAzSNZbPUd0tfZYjVD0j5r3oGvSelgnF93kQ8tonxgSTLekqFdWLJE1vSUqK6lyI9Qhhm8lLWpAH3KpxLRsGFj+DZNQPVknKAfoNU68Ikqxkt/CZZETU35N1UV1wSSnjwkBwyr7Yo/Iv3qCcxITEKQ7ypJyEZFVVhGXV1F/UvbPDdv/I8MQVxBDgguLNzsHVG5WxqYDJCtOCdQnECQgYE1SfJOaBaQq9P22pMpAtWy3bTIFGHAia4+YKW42yA6HUH1GwQr9WSsW4dhRtfVsTWXFTVhOVjTLQUb7ZVrKjmP//UpBoHeJC9qbVepDVA4jYrbIZjfcDFJR5Pc0XyYZD1yfKvJ2arpIoK3TwfMFrHjAN+ZADXpDTDlwLa4YDuqRZQ9JkkzZmUnZ0xoz9NgRn0ZQba1YK06Ask7BnNvjA175BR6LcUi2zqgBwBeJNQv8W5/Djf7hiJ0iU7jZCz1XhWRKwE4caIWn2jQsCBEcDr9XDgw2vvI8X5/4QoxYmSIGJlBU4DWjy2L0NFTviCl9IUhkUQSm0u5QivYdfffvirLbf6nWEQK8j0OuIBkWM6aCjCuXmsV9PN9xPdwtkPrMTFl18CuY4/ULKhCYkY3VzQikjgdm3NIm5IGTqZOYTeilv6TXBRIv6RfVpdToV51KSK+kdmEQAHOAPsI+Ccln5c1GT88dYbWIe4E0SUtqThEVb5QRnjzglOf73wwqXJQuEPv7ATO3MukuZVm+yu9Dt2V1b4eEVE8o4ld0195MptRs+pwkL4or9/ndeR3uf5o2gQfOPNWW9YCEvCA0E3C4Y+ukF4n9KP119hujUn+v338jqcQSb2LFFElQFivBUYSIajdxo5GoYL8zIWSOZI5mV2UQ9Mt0zk3lA6mQk86cg07gwMh0FmXWapsyD7PVJn1um/PVqm/SZF9t8T5MGqi9x6jTQwglxGKrQnjvIRC+v+6+COre3LoZIFUa+ayLI3W8RW4mgjPC1xFuyQPL42FmgkgYFHearKrGfpPJ22bOI0mVAxcApnpvsGC+0BoYXtyNTleTQaMdLOQnLU6dkXminO2q23yc3Zbu9XL31ytyU3U8J9Ds6cW7KPIKlh8zScwZaG1o3YT3peH0Rzz8Y/DrsDgFzbeIAgY+12e+5CG1maZ6lOY42czTP1Dy3JWHfujb1D3YWjMgqXOq5hn6g1I+CVkkU8T68ArPnDOZVfxz0nOuh0gzyNHR3wKTdaafFdqToXGt8eXvS7LaRO436DZiYyHWOQrmcjILNm14HZLEo8Umo1BWbTjOkOb7m6trM1Zj/YRwosOjpn4057aq+6+yFA2lHBkJ0OCKqMGS74bk/jJDuCl6EBzhCWGH20+36MJi13jOY1RXbS0N2GFZs2rh+BRo7sP/XNMng517cvn4tdKF+eunAvcvT0XVAEn1cxf8Mq3inZ/hM48yreOgqIyJpE4Rx6poXUtAliUnGCCWcograr5jSZ6HUYE1J1wo2q4ZmYXDYquESN89f83rAEVc/+qHLH8NUo/jWdU0vRTrYKt+xrjnacsQaiT3j6x4H0wffSN/bPO5wzfonyZNwgM5b3jtcLLClTg9GtjsHV+O0LKeXyXAOi9bhCd84VGQBZ6bmOZrn8TB9amhygTtG5xdGk70jL3au6Fx2rM4kIA6VA3mmaTrjXNWLQDYl2LJwyoRVG3emOcPc0pC3loYNWe7E2gD4/rVouv8OKVTYDVtlN06madVrduOe1ZXvWZnImdhn3rQyVBmAkbwrJw9Z9rl3S02FcxtTT2PqiefcVXC+77vF49tNI5zqvOi53wg1Va+ZjJveF7LpvQcmBCYAmBA40NWhZXa3qE2XhYMmggjquuPw31CeCCGkcr0jQleAEI/rTLeF0A8TxIrbH2XXmfHtL9/h7D8= \ No newline at end of file diff --git a/docs/high-performance/images/message-queue/message-queue-pub-sub-model.png b/docs/high-performance/images/message-queue/message-queue-pub-sub-model.png deleted file mode 100644 index a5a77736493..00000000000 Binary files a/docs/high-performance/images/message-queue/message-queue-pub-sub-model.png and /dev/null differ diff --git a/docs/high-performance/images/message-queue/message-queue-queue-model.drawio b/docs/high-performance/images/message-queue/message-queue-queue-model.drawio deleted file mode 100644 index ea1c6caca4b..00000000000 --- a/docs/high-performance/images/message-queue/message-queue-queue-model.drawio +++ /dev/null @@ -1 +0,0 @@ -7Zrbcts2EEC/hjPtQzS8iLdHSRGTdpLUidpp89SBSIjCGCRoELKkfH0XJCDxZstJ7Vp2Jc9YwAJcAtyDXWBFw5llu3ccFeuPLMHUsM1kZzhvDdu2TDuELynZ15LQdGpBykmiOh0FC/IN6yuVdEMSXLY6CsaoIEVbGLM8x7FoyRDnbNvutmK0fdcCpbgnWMSI9qV/kkSsa2lg+0f5e0zStb6z5akJL1F8nXK2ydX9DNuJvCiKgro5Q1qXmmi5RgnbNkTO3HBmnDFRl7LdDFP5bPVjq6+L7mg9jJvjXDzkgpvpl6Vd7vPZ+yj8vP32x++/5J/eKC23iG6wnoZHQd90xUAtjFrs1ZPybjZMN7wpKztOoIPlFrtjI5RS+X3FWbKJMdfaYFi1wrpZPZGDbhuGCgaHynS7JgIvChTLli0wB7K1yCjULCiisqgpWJEdTuRwCKUzRhmvFDnj2WQc+iAvBWfXuNHixQFermTLNRaxtI0pK9oqpppahDJCJcu/YjHliOQljPQjy5lqX7ANr8a2FgIQtV35FFx46vKf7FCOUsZSilFBylHMsqohLquu0arWDsWmfteeqjv0jaothLnAu4ZIGfkdZhkWHFSaqnWsgVML0nFUfdvAW4nWDbK1DKkFlR40H6GCguLqOxizBxjrmj9PJnItQy2mqCxJ3LZ69eBr12G5qt4wLSw605QGxDsi/lLXyPJXadiRq2pvd8rOVWWvCbgThxoh7RXs8wak1Lc9sdRx0vKHfYwamLgDmGgZxxQJctv2okPsqDtcMVI5FEWpa3Uo7eJXz0dd1XRrHUXeuKPI7ygSiKdY9BRVKB+m/eN0O6fpboAslzGB4PMBLTG9YiURhOXQtmRCAAng3yhJpSAG+4L7dKZU9pweYk2D+lX1aSidqGsFKwbp7blE0wzMqId9gsq1dK16QRRyGtkulfF/RFjpjwgE43KE81tMWYH//inDZQlx8ufvWKmtVXfGy+rhftd1OiCO+37XHlhQ9lP53fFpMrV14z0leVLxdiL+LiWEOPmwPAgOaP62EaAFKzlnAim4Q7Mfp1eu/BuM09Wnj079ef3x2zc7HJk2RLAuSY7bJ8nSwkdHyb04uYuTq5ycf2ZOzruQeSGz2geGHTK9ZybTv5B5IbMi0zszMoMBMus0TVmg/MeTPjMw/iY7Jn2W/JjvOaSB6ls8dRpoFcQ4jofQXgbu2L3/3P8qqAu7B+znTgOFp/1hIw2UM3mS+Dc5IF1+7BxQKRAX/WxVJY4I1cOFuajamSN1MnGk3EWdR7mnnzpvnkmCye/uB7p5oYcmmAL3eRNMeh1fNhL/941Ej8TnPnxZAz8fzV0jiIzQMuahAbEkgIJnTD0jCGQBqpOohy/MX7SdfRskFQyaMV6JejB3Sc1IktC7NhS8TmadDAJN5JzXQpPbTZwHfZq8/5SmgVR6nx3AKgyMMKpAg4L/omnSDmyhhm+9Frq8bqLogdvPp6NrIB3eZwkwmwTGxJKScG4E/SNKn66GPW1db4VA04yi13/g6P7ufNjINSzuD1jceQSLz/fO18+EF5vC/fTF5zdsnHiD7zZc9k0vc9/Uw3AA1rv3Tab/bPumQTLt+wJdYEzHxjS8bJteCExPt22C6vH1sPo4eXwHz5n/Aw== \ No newline at end of file diff --git a/docs/high-performance/images/message-queue/message-queue-queue-model.png b/docs/high-performance/images/message-queue/message-queue-queue-model.png deleted file mode 100644 index 8fb2cd6a7ba..00000000000 Binary files a/docs/high-performance/images/message-queue/message-queue-queue-model.png and /dev/null differ diff --git a/docs/high-performance/load-balancing.md b/docs/high-performance/load-balancing.md index a41af365247..619df980574 100644 --- a/docs/high-performance/load-balancing.md +++ b/docs/high-performance/load-balancing.md @@ -1,5 +1,5 @@ --- -title: 负载均衡常见问题总结 +title: 负载均衡原理及算法详解 category: 高性能 head: - - meta @@ -24,7 +24,7 @@ head: 负载均衡可以简单分为 **服务端负载均衡** 和 **客户端负载均衡** 这两种。 -服务端负载均衡涉及到的知识点更多,工作中遇到的也比较多,因为,我会花更多时间来介绍。 +服务端负载均衡涉及到的知识点更多,工作中遇到的也比较多,因此,我会花更多时间来介绍。 ### 服务端负载均衡 @@ -61,7 +61,7 @@ head: 七层负载均衡比四层负载均衡会消耗更多的性能,不过,也相对更加灵活,能够更加智能地路由网络请求,比如说你可以根据请求的内容进行优化如缓存、压缩、加密。 -简单来说,**四层负载均衡性能更强,七层负载均衡功能更强!** 不过,对于绝大部分业务场景来说,四层负载均衡和七层负载均衡的性能差异基本可以忽略不计的。 +简单来说,**四层负载均衡性能很强,七层负载均衡功能更强!** 不过,对于绝大部分业务场景来说,四层负载均衡和七层负载均衡的性能差异基本可以忽略不计的。 下面这段话摘自 Nginx 官网的 [What Is Layer 4 Load Balancing?](https://www.nginx.com/resources/glossary/layer-4-load-balancing/) 这篇文章。 @@ -85,7 +85,7 @@ head: 客户端负载均衡器和服务运行在同一个进程或者说 Java 程序里,不存在额外的网络开销。不过,客户端负载均衡的实现会受到编程语言的限制,比如说 Spring Cloud Load Balancer 就只能用于 Java 语言。 -Java 领域主流的微服务框架 Dubbo、Spring Cloud 等都内置了开箱即用的客户端负载均衡实现。Dubbo 属于是默认自带了负载均衡功能,Spring Cloud 是通过组件的形式实现的负载均衡,属于可选项,比较常用的是 Spring Cloud Load Balancer(官方,推荐) 和 Ribbon(Netflix,已被启用)。 +Java 领域主流的微服务框架 Dubbo、Spring Cloud 等都内置了开箱即用的客户端负载均衡实现。Dubbo 属于是默认自带了负载均衡功能,Spring Cloud 是通过组件的形式实现的负载均衡,属于可选项,比较常用的是 Spring Cloud Load Balancer(官方,推荐) 和 Ribbon(Netflix,已被弃用)。 下图是我画的一个简单的基于 Spring Cloud Load Balancer(Ribbon 也类似) 的客户端负载均衡示意图: @@ -113,15 +113,45 @@ Java 领域主流的微服务框架 Dubbo、Spring Cloud 等都内置了开箱 未加权重的轮询算法适合于服务器性能相近的集群,其中每个服务器承载相同的负载。加权轮询算法适合于服务器性能不等的集群,权重的存在可以使请求分配更加合理化。 +在加权轮询的基础上,还有进一步改进得到的负载均衡算法,比如平滑的加权轮训算法。 + +平滑的加权轮训算法最早是在 Nginx 中被实现,可以参考这个 commit:。如果你认真学习过 Dubbo 负载均衡策略的话,就会发现 Dubbo 的加权轮询就借鉴了该算法实现并进一步做了优化。 + +![Dubbo 加权轮询负载均衡算法](https://oss.javaguide.cn/github/javaguide/high-performance/load-balancing/dubbo-round-robin-load-balance.png) + +### 两次随机法 + +两次随机法在随机法的基础上多增加了一次随机,多选出一个服务器。随后再根据两台服务器的负载等情况,从其中选择出一个最合适的服务器。 + +两次随机法的好处是可以动态地调节后端节点的负载,使其更加均衡。如果只使用一次随机法,可能会导致某些服务器过载,而某些服务器空闲。 + +### 哈希法 + +将请求的参数信息通过哈希函数转换成一个哈希值,然后根据哈希值来决定请求被哪一台服务器处理。 + +在服务器数量不变的情况下,相同参数的请求总是发到同一台服务器处理,比如同个 IP 的请求、同一个用户的请求。 + ### 一致性 Hash 法 -相同参数的请求总是发到同一台服务器处理,比如同个 IP 的请求。 +和哈希法类似,一致性 Hash 法也可以让相同参数的请求总是发到同一台服务器处理。不过,它解决了哈希法存在的一些问题。 + +常规哈希法在服务器数量变化时,哈希值会重新落在不同的服务器上,这明显违背了使用哈希法的本意。而一致性哈希法的核心思想是将数据和节点都映射到一个哈希环上,然后根据哈希值的顺序来确定数据属于哪个节点。当服务器增加或删除时,只影响该服务器的哈希,而不会导致整个服务集群的哈希键值重新分布。 ### 最小连接法 -当有新的请求出现时,遍历服务器节点列表并选取其中活动连接数最小的一台服务器来响应当前请求。活动连接数可以理解为当前正在处理的请求数。 +当有新的请求出现时,遍历服务器节点列表并选取其中连接数最小的一台服务器来响应当前请求。相同连接的情况下,可以进行加权随机。 + +最少连接数基于一个服务器连接数越多,负载就越高这一理想假设。然而, 实际情况是连接数并不能代表服务器的实际负载,有些连接耗费系统资源更多,有些连接不怎么耗费系统资源。 + +### 最少活跃法 -最小连接法可以尽可能最大地使请求分配更加合理化,提高服务器的利用率。不过,这种方法实现起来也最复杂,需要监控每一台服务器处理的请求连接数。 +最少活跃法和最小连接法类似,但要更科学一些。最少活跃法以活动连接数为标准,活动连接数可以理解为当前正在处理的请求数。活跃数越低,说明处理能力越强,这样就可以使处理能力强的服务器处理更多请求。相同活跃数的情况下,可以进行加权随机。 + +### 最快响应时间法 + +不同于最小连接法和最少活跃法,最快响应时间法以响应时间为标准来选择具体是哪一台服务器处理。客户端会维持每个服务器的响应时间,每次请求挑选响应时间最短的。相同响应时间的情况下,可以进行加权随机。 + +这种算法可以使得请求被更快处理,但可能会造成流量过于集中于高性能服务器的问题。 ## 七层负载均衡可以怎么做? @@ -233,3 +263,5 @@ Spring Cloud 2020.0.0 版本移除了 Netflix 除 Eureka 外的所有组件。Sp - 干货 | eBay 的 4 层软件负载均衡实现: - HTTP Load Balancing(Nginx 官方文档): - 深入浅出负载均衡 - vivo 互联网技术: + + diff --git a/docs/high-performance/message-queue/disruptor-questions.md b/docs/high-performance/message-queue/disruptor-questions.md index b58e7edef10..1881f6c2c79 100644 --- a/docs/high-performance/message-queue/disruptor-questions.md +++ b/docs/high-performance/message-queue/disruptor-questions.md @@ -25,7 +25,7 @@ LMAX 公司 2010 年在 QCon 演讲后,Disruptor 获得了业界关注,并 > “Duke 选择大奖”旨在表彰过去一年里全球个人或公司开发的、最具影响力的 Java 技术应用,由甲骨文公司主办。含金量非常高! -我专门找到了 Oracle 官方当年颁布获得 Duke's Choice Awards 项目的那篇文章(文章地址:https://blogs.oracle.com/java/post/and-the-winners-arethe-dukes-choice-award) 。从文中可以看出,同年获得此大奖荣誉的还有大名鼎鼎的 Netty、JRebel 等项目。 +我专门找到了 Oracle 官方当年颁布获得 Duke's Choice Awards 项目的那篇文章(文章地址: 。从文中可以看出,同年获得此大奖荣誉的还有大名鼎鼎的 Netty、JRebel 等项目。 ![2011 年的 Oracle 官方的 Duke's Choice Awards](https://oss.javaguide.cn/javaguide/image-20211015152323898.png) @@ -76,7 +76,7 @@ Disruptor 真的很快,关于它为什么这么快这个问题,会在后文 - **SOFATracer**:SOFATracer 是蚂蚁金服开源的分布式应用链路追踪工具,它基于 Disruptor 来实现异步日志。 - **Storm** : Storm 是一个开源的分布式实时计算系统,它基于 Disruptor 来实现工作进程内发生的消息传递(同一 Storm 节点上的线程间,无需网络通信)。 - **HBase**:HBase 是一个分布式列存储数据库系统,它基于 Disruptor 来提高写并发性能。 -- ...... +- …… ## Disruptor 核心概念有哪些? @@ -115,7 +115,7 @@ Disruptor 真的很快,关于它为什么这么快这个问题,会在后文 ## Disruptor 为什么这么快? - **RingBuffer(环形数组)** : Disruptor 内部的 RingBuffer 是通过数组实现的。由于这个数组中的所有元素在初始化时一次性全部创建,因此这些元素的内存地址一般来说是连续的。这样做的好处是,当生产者不断往 RingBuffer 中插入新的事件对象时,这些事件对象的内存地址就能够保持连续,从而利用 CPU 缓存的局部性原理,将相邻的事件对象一起加载到缓存中,提高程序的性能。这类似于 MySQL 的预读机制,将连续的几个页预读到内存里。除此之外,RingBuffer 基于数组还支持批量操作(一次处理多个元素)、还可以避免频繁的内存分配和垃圾回收(RingBuffer 是一个固定大小的数组,当向数组中添加新元素时,如果数组已满,则新元素将覆盖掉最旧的元素)。 -- **避免了伪共享问题**:CPU 缓存内部是按照 Cache Line(缓存行)管理的,一般的 Cache Line 大小在 64 字节左右。Disruptor 为了确保目标字段独占一个 Cache Line,会在目标字段前后增加了 64 个字节的填充(前 56 个字节和后 8 个字节),这样可以避免 Cache Line 的伪共享(False Sharing)问题。 +- **避免了伪共享问题**:CPU 缓存内部是按照 Cache Line(缓存行)管理的,一般的 Cache Line 大小在 64 字节左右。Disruptor 为了确保目标字段独占一个 Cache Line,会在目标字段前后增加字节填充(前 56 个字节和后 56 个字节),这样可以避免 Cache Line 的伪共享(False Sharing)问题。同时,为了让 RingBuffer 存放数据的数组独占缓存行,数组的设计为 无效填充(128 字节)+ 有效数据。 - **无锁设计**:Disruptor 采用无锁设计,避免了传统锁机制带来的竞争和延迟。Disruptor 的无锁实现起来比较复杂,主要是基于 CAS、内存屏障(Memory Barrier)、RingBuffer 等技术实现的。 综上所述,Disruptor 之所以能够如此快,是基于一系列优化策略的综合作用,既充分利用了现代 CPU 缓存结构的特点,又避免了常见的并发问题和性能瓶颈。 @@ -134,5 +134,7 @@ CPU 缓存是通过将最近使用的数据存储在高速缓存中来实现更 ## 参考 -- Disruptor 高性能之道-等待策略: -- 《Java 并发编程实战》- 40 | 案例分析(三):高性能队列 Disruptor:https://time.geekbang.org/column/article/98134 +- Disruptor 高性能之道-等待策略:< 高性能之道-等待策略/> +- 《Java 并发编程实战》- 40 | 案例分析(三):高性能队列 Disruptor: + + diff --git a/docs/high-performance/message-queue/kafka-questions-01.md b/docs/high-performance/message-queue/kafka-questions-01.md index 7f124abbca5..070858cc1f8 100644 --- a/docs/high-performance/message-queue/kafka-questions-01.md +++ b/docs/high-performance/message-queue/kafka-questions-01.md @@ -5,6 +5,8 @@ tag: - 消息队列 --- +## Kafka 基础 + ### Kafka 是什么?主要应用场景有哪些? Kafka 是一个分布式流式处理平台。这到底是什么意思呢? @@ -37,7 +39,7 @@ Kafka 主要有两大应用场景: #### 队列模型:早期的消息模型 -![队列模型](https://my-blog-to-use.oss-cn-beijing.aliyuncs.com/2019-11/队列模型23.png) +![队列模型](https://oss.javaguide.cn/github/javaguide/high-performance/message-queue/%E9%98%9F%E5%88%97%E6%A8%A1%E5%9E%8B23.png) **使用队列(Queue)作为消息通信载体,满足生产者与消费者模式,一条消息只能被一个消费者使用,未被消费的消息在队列中保留直到被消费或超时。** 比如:我们生产者发送 100 条消息的话,两个消费者来消费一般情况下两个消费者会按照消息发送的顺序各自消费一半(也就是你一个我一个的消费。) @@ -61,6 +63,8 @@ Kafka 主要有两大应用场景: > **RocketMQ 的消息模型和 Kafka 基本是完全一样的。唯一的区别是 Kafka 中没有队列这个概念,与之对应的是 Partition(分区)。** +## Kafka 核心概念 + ### 什么是 Producer、Consumer、Broker、Topic、Partition? Kafka 将生产者发布的消息发送到 **Topic(主题)** 中,需要这些消息的消费者可以订阅这些 **Topic(主题)**,如下图所示: @@ -91,13 +95,15 @@ Kafka 将生产者发布的消息发送到 **Topic(主题)** 中,需要这 1. Kafka 通过给特定 Topic 指定多个 Partition, 而各个 Partition 可以分布在不同的 Broker 上, 这样便能提供比较好的并发能力(负载均衡)。 2. Partition 可以指定对应的 Replica 数, 这也极大地提高了消息存储的安全性, 提高了容灾能力,不过也相应的增加了所需要的存储空间。 -### Zookeeper 在 Kafka 中的作用知道吗? +## Zookeeper 和 Kafka + +### Zookeeper 在 Kafka 中的作用是什么? -> **要想搞懂 zookeeper 在 Kafka 中的作用 一定要自己搭建一个 Kafka 环境然后自己进 zookeeper 去看一下有哪些文件夹和 Kafka 有关,每个节点又保存了什么信息。** 一定不要光看不实践,这样学来的也终会忘记!这部分内容参考和借鉴了这篇文章:https://www.jianshu.com/p/a036405f989c 。 +> 要想搞懂 zookeeper 在 Kafka 中的作用 一定要自己搭建一个 Kafka 环境然后自己进 zookeeper 去看一下有哪些文件夹和 Kafka 有关,每个节点又保存了什么信息。 一定不要光看不实践,这样学来的也终会忘记!这部分内容参考和借鉴了这篇文章: 。 下图就是我的本地 Zookeeper ,它成功和我本地的 Kafka 关联上(以下文件夹结构借助 idea 插件 Zookeeper tool 实现)。 - + ZooKeeper 主要为 Kafka 提供元数据的管理的功能。 @@ -106,7 +112,17 @@ ZooKeeper 主要为 Kafka 提供元数据的管理的功能。 1. **Broker 注册**:在 Zookeeper 上会有一个专门**用来进行 Broker 服务器列表记录**的节点。每个 Broker 在启动时,都会到 Zookeeper 上进行注册,即到 `/brokers/ids` 下创建属于自己的节点。每个 Broker 就会将自己的 IP 地址和端口等信息记录到该节点中去 2. **Topic 注册**:在 Kafka 中,同一个**Topic 的消息会被分成多个分区**并将其分布在多个 Broker 上,**这些分区信息及与 Broker 的对应关系**也都是由 Zookeeper 在维护。比如我创建了一个名字为 my-topic 的主题并且它有两个分区,对应到 zookeeper 中会创建这些文件夹:`/brokers/topics/my-topic/Partitions/0`、`/brokers/topics/my-topic/Partitions/1` 3. **负载均衡**:上面也说过了 Kafka 通过给特定 Topic 指定多个 Partition, 而各个 Partition 可以分布在不同的 Broker 上, 这样便能提供比较好的并发能力。 对于同一个 Topic 的不同 Partition,Kafka 会尽力将这些 Partition 分布到不同的 Broker 服务器上。当生产者产生消息后也会尽量投递到不同 Broker 的 Partition 里面。当 Consumer 消费的时候,Zookeeper 可以根据当前的 Partition 数量以及 Consumer 数量来实现动态负载均衡。 -4. ...... +4. …… + +### 使用 Kafka 能否不引入 Zookeeper? + +在 Kafka 2.8 之前,Kafka 最被大家诟病的就是其重度依赖于 Zookeeper。在 Kafka 2.8 之后,引入了基于 Raft 协议的 KRaft 模式,不再依赖 Zookeeper,大大简化了 Kafka 的架构,让你可以以一种轻量级的方式来使用 Kafka。 + +不过,要提示一下:**如果要使用 KRaft 模式的话,建议选择较高版本的 Kafka,因为这个功能还在持续完善优化中。Kafka 3.3.1 版本是第一个将 KRaft(Kafka Raft)共识协议标记为生产就绪的版本。** + +![](https://oss.javaguide.cn/github/javaguide/high-performance/message-queue/kafka3.3.1-kraft-production-ready.png) + +## Kafka 消费顺序、消息丢失和重复消费 ### Kafka 如何保证消息的消费顺序? @@ -119,7 +135,7 @@ ZooKeeper 主要为 Kafka 提供元数据的管理的功能。 我们知道 Kafka 中 Partition(分区)是真正保存消息的地方,我们发送的消息都被放在了这里。而我们的 Partition(分区) 又存在于 Topic(主题) 这个概念中,并且我们可以给特定 Topic 指定多个 Partition。 -![](https://my-blog-to-use.oss-cn-beijing.aliyuncs.com/2019-11/KafkaTopicPartionsLayout.png) +![](https://oss.javaguide.cn/github/javaguide/high-performance/message-queue/KafkaTopicPartionsLayout.png) 每次添加消息到 Partition(分区) 的时候都会采用尾加法,如上图所示。 **Kafka 只能为我们保证 Partition(分区) 中的消息有序。** @@ -136,7 +152,7 @@ Kafka 中发送 1 条消息的时候,可以指定 topic, partition, key,data 当然不仅仅只有上面两种方法,上面两种方法是我觉得比较好理解的, -### Kafka 如何保证消息不丢失 +### Kafka 如何保证消息不丢失? #### 生产者丢失消息的情况 @@ -164,13 +180,13 @@ if (sendResult.getRecordMetadata() != null) { 如果消息发送失败的话,我们检查失败的原因之后重新发送即可! -**另外这里推荐为 Producer 的`retries `(重试次数)设置一个比较合理的值,一般是 3 ,但是为了保证消息不丢失的话一般会设置比较大一点。设置完成之后,当出现网络问题之后能够自动重试消息发送,避免消息丢失。另外,建议还要设置重试间隔,因为间隔太小的话重试的效果就不明显了,网络波动一次你 3 次一下子就重试完了** +另外,这里推荐为 Producer 的`retries`(重试次数)设置一个比较合理的值,一般是 3 ,但是为了保证消息不丢失的话一般会设置比较大一点。设置完成之后,当出现网络问题之后能够自动重试消息发送,避免消息丢失。另外,建议还要设置重试间隔,因为间隔太小的话重试的效果就不明显了,网络波动一次你 3 次一下子就重试完了。 #### 消费者丢失消息的情况 我们知道消息在被追加到 Partition(分区)的时候都会分配一个特定的偏移量(offset)。偏移量(offset)表示 Consumer 当前消费到的 Partition(分区)的所在的位置。Kafka 通过偏移量(offset)可以保证消息在分区内的顺序性。 -![kafka offset](https://my-blog-to-use.oss-cn-beijing.aliyuncs.com/2019-11/kafka-offset.jpg) +![kafka offset](https://oss.javaguide.cn/github/javaguide/high-performance/message-queue/kafka-offset.jpg) 当消费者拉取到了分区的某个消息之后,消费者会自动提交了 offset。自动提交的话会有一个问题,试想一下,当消费者刚拿到这个消息准备进行真正消费的时候,突然挂掉了,消息实际上并没有被消费,但是 offset 却被自动提交了。 @@ -204,7 +220,7 @@ acks 的默认值即为 1,代表我们的消息被 leader 副本接收之后 我们最开始也说了我们发送的消息会被发送到 leader 副本,然后 follower 副本才能从 leader 副本中拉取消息进行同步。多个 follower 副本之间的消息同步情况不一样,当我们配置了 **unclean.leader.election.enable = false** 的话,当 leader 副本发生故障时就不会从 follower 副本中和 leader 同步程度达不到要求的副本中选择出 leader ,这样降低了消息丢失的可能性。 -### Kafka 如何保证消息不重复消费 +### Kafka 如何保证消息不重复消费? **kafka 出现消息重复消费的原因:** @@ -218,7 +234,208 @@ acks 的默认值即为 1,代表我们的消息被 leader 副本接收之后 - 处理完消息再提交:依旧有消息重复消费的风险,和自动提交一样 - 拉取到消息即提交:会有消息丢失的风险。允许消息延时的场景,一般会采用这种方式。然后,通过定时任务在业务不繁忙(比如凌晨)的时候做数据兜底。 -### Reference +## Kafka 重试机制 + +在 Kafka 如何保证消息不丢失这里,我们提到了 Kafka 的重试机制。由于这部分内容较为重要,我们这里再来详细介绍一下。 + +网上关于 Spring Kafka 的默认重试机制文章很多,但大多都是过时的,和实际运行结果完全不一样。以下是根据 [spring-kafka-2.9.3](https://mvnrepository.com/artifact/org.springframework.kafka/spring-kafka/2.9.3) 源码重新梳理一下。 + +### 消费失败会怎么样? + +在消费过程中,当其中一个消息消费异常时,会不会卡住后续队列消息的消费?这样业务岂不是卡住了? + +生产者代码: + +```Java + for (int i = 0; i < 10; i++) { + kafkaTemplate.send(KafkaConst.TEST_TOPIC, String.valueOf(i)) + } +``` + +消费者消代码: + +```Java + @KafkaListener(topics = {KafkaConst.TEST_TOPIC},groupId = "apple") + private void customer(String message) throws InterruptedException { + log.info("kafka customer:{}",message); + Integer n = Integer.parseInt(message); + if (n%5==0){ + throw new RuntimeException(); + } + } +``` + +在默认配置下,当消费异常会进行重试,重试多次后会跳过当前消息,继续进行后续消息的消费,不会一直卡在当前消息。下面是一段消费的日志,可以看出当 `test-0@95` 重试多次后会被跳过。 -- Kafka 官方文档:https://kafka.apache.org/documentation/ +```Java +2023-08-10 12:03:32.918 DEBUG 9700 --- [ntainer#0-0-C-1] o.s.kafka.listener.DefaultErrorHandler : Skipping seek of: test-0@95 +2023-08-10 12:03:32.918 TRACE 9700 --- [ntainer#0-0-C-1] o.s.kafka.listener.DefaultErrorHandler : Seeking: test-0 to: 96 +2023-08-10 12:03:32.918 INFO 9700 --- [ntainer#0-0-C-1] o.a.k.clients.consumer.KafkaConsumer : [Consumer clientId=consumer-apple-1, groupId=apple] Seeking to offset 96 for partition test-0 + +``` + +因此,即使某个消息消费异常,Kafka 消费者仍然能够继续消费后续的消息,不会一直卡在当前消息,保证了业务的正常进行。 + +### 默认会重试多少次? + +默认配置下,消费异常会进行重试,重试次数是多少, 重试是否有时间间隔? + +看源码 `FailedRecordTracker` 类有个 `recovered` 函数,返回 Boolean 值判断是否要进行重试,下面是这个函数中判断是否重试的逻辑: + +```java + @Override + public boolean recovered(ConsumerRecord << ? , ? > record, Exception exception, + @Nullable MessageListenerContainer container, + @Nullable Consumer << ? , ? > consumer) throws InterruptedException { + + if (this.noRetries) { + // 不支持重试 + attemptRecovery(record, exception, null, consumer); + return true; + } + // 取已经失败的消费记录集合 + Map < TopicPartition, FailedRecord > map = this.failures.get(); + if (map == null) { + this.failures.set(new HashMap < > ()); + map = this.failures.get(); + } + // 获取消费记录所在的Topic和Partition + TopicPartition topicPartition = new TopicPartition(record.topic(), record.partition()); + FailedRecord failedRecord = getFailedRecordInstance(record, exception, map, topicPartition); + // 通知注册的重试监听器,消息投递失败 + this.retryListeners.forEach(rl - > + rl.failedDelivery(record, exception, failedRecord.getDeliveryAttempts().get())); + // 获取下一次重试的时间间隔 + long nextBackOff = failedRecord.getBackOffExecution().nextBackOff(); + if (nextBackOff != BackOffExecution.STOP) { + this.backOffHandler.onNextBackOff(container, exception, nextBackOff); + return false; + } else { + attemptRecovery(record, exception, topicPartition, consumer); + map.remove(topicPartition); + if (map.isEmpty()) { + this.failures.remove(); + } + return true; + } + } +``` + +其中, `BackOffExecution.STOP` 的值为 -1。 + +```java +@FunctionalInterface +public interface BackOffExecution { + + long STOP = -1; + long nextBackOff(); + +} +``` + +`nextBackOff` 的值调用 `BackOff` 类的 `nextBackOff()` 函数。如果当前执行次数大于最大执行次数则返回 `STOP`,既超过这个最大执行次数后才会停止重试。 + +```Java +public long nextBackOff() { + this.currentAttempts++; + if (this.currentAttempts <= getMaxAttempts()) { + return getInterval(); + } + else { + return STOP; + } +} +``` + +那么这个 `getMaxAttempts` 的值又是多少呢?回到最开始,当执行出错会进入 `DefaultErrorHandler` 。`DefaultErrorHandler` 默认的构造函数是: + +```Java +public DefaultErrorHandler() { + this(null, SeekUtils.DEFAULT_BACK_OFF); +} +``` + +`SeekUtils.DEFAULT_BACK_OFF` 定义的是: + +```Java +public static final int DEFAULT_MAX_FAILURES = 10; + +public static final FixedBackOff DEFAULT_BACK_OFF = new FixedBackOff(0, DEFAULT_MAX_FAILURES - 1); +``` + +`DEFAULT_MAX_FAILURES` 的值是 10,`currentAttempts` 从 0 到 9,所以总共会执行 10 次,每次重试的时间间隔为 0。 + +最后,简单总结一下:Kafka 消费者在默认配置下会进行最多 10 次 的重试,每次重试的时间间隔为 0,即立即进行重试。如果在 10 次重试后仍然无法成功消费消息,则不再进行重试,消息将被视为消费失败。 + +### 如何自定义重试次数以及时间间隔? + +从上面的代码可以知道,默认错误处理器的重试次数以及时间间隔是由 `FixedBackOff` 控制的,`FixedBackOff` 是 `DefaultErrorHandler` 初始化时默认的。所以自定义重试次数以及时间间隔,只需要在 `DefaultErrorHandler` 初始化的时候传入自定义的 `FixedBackOff` 即可。重新实现一个 `KafkaListenerContainerFactory` ,调用 `setCommonErrorHandler` 设置新的自定义的错误处理器就可以实现。 + +```Java +@Bean +public KafkaListenerContainerFactory kafkaListenerContainerFactory(ConsumerFactory consumerFactory) { + ConcurrentKafkaListenerContainerFactory factory = new ConcurrentKafkaListenerContainerFactory(); + // 自定义重试时间间隔以及次数 + FixedBackOff fixedBackOff = new FixedBackOff(1000, 5); + factory.setCommonErrorHandler(new DefaultErrorHandler(fixedBackOff)); + factory.setConsumerFactory(consumerFactory); + return factory; +} +``` + +### 如何在重试失败后进行告警? + +自定义重试失败后逻辑,需要手动实现,以下是一个简单的例子,重写 `DefaultErrorHandler` 的 `handleRemaining` 函数,加上自定义的告警等操作。 + +```Java +@Slf4j +public class DelErrorHandler extends DefaultErrorHandler { + + public DelErrorHandler(FixedBackOff backOff) { + super(null,backOff); + } + + @Override + public void handleRemaining(Exception thrownException, List> records, Consumer consumer, MessageListenerContainer container) { + super.handleRemaining(thrownException, records, consumer, container); + log.info("重试多次失败"); + // 自定义操作 + } +} +``` + +`DefaultErrorHandler` 只是默认的一个错误处理器,Spring Kafka 还提供了 `CommonErrorHandler` 接口。手动实现 `CommonErrorHandler` 就可以实现更多的自定义操作,有很高的灵活性。例如根据不同的错误类型,实现不同的重试逻辑以及业务逻辑等。 + +### 重试失败后的数据如何再次处理? + +当达到最大重试次数后,数据会直接被跳过,继续向后进行。当代码修复后,如何重新消费这些重试失败的数据呢? + +**死信队列(Dead Letter Queue,简称 DLQ)** 是消息中间件中的一种特殊队列。它主要用于处理无法被消费者正确处理的消息,通常是因为消息格式错误、处理失败、消费超时等情况导致的消息被"丢弃"或"死亡"的情况。当消息进入队列后,消费者会尝试处理它。如果处理失败,或者超过一定的重试次数仍无法被成功处理,消息可以发送到死信队列中,而不是被永久性地丢弃。在死信队列中,可以进一步分析、处理这些无法正常消费的消息,以便定位问题、修复错误,并采取适当的措施。 + +`@RetryableTopic` 是 Spring Kafka 中的一个注解,它用于配置某个 Topic 支持消息重试,更推荐使用这个注解来完成重试。 + +```Java +// 重试 5 次,重试间隔 100 毫秒,最大间隔 1 秒 +@RetryableTopic( + attempts = "5", + backoff = @Backoff(delay = 100, maxDelay = 1000) +) +@KafkaListener(topics = {KafkaConst.TEST_TOPIC}, groupId = "apple") +private void customer(String message) { + log.info("kafka customer:{}", message); + Integer n = Integer.parseInt(message); + if (n % 5 == 0) { + throw new RuntimeException(); + } + System.out.println(n); +} +``` + +当达到最大重试次数后,如果仍然无法成功处理消息,消息会被发送到对应的死信队列中。对于死信队列的处理,既可以用 `@DltHandler` 处理,也可以使用 `@KafkaListener` 重新消费。 + +## 参考 + +- Kafka 官方文档: - 极客时间—《Kafka 核心技术与实战》第 11 节:无消息丢失配置怎么实现? + + diff --git a/docs/high-performance/message-queue/message-queue.md b/docs/high-performance/message-queue/message-queue.md index 4d7b6717ce9..5874f290298 100644 --- a/docs/high-performance/message-queue/message-queue.md +++ b/docs/high-performance/message-queue/message-queue.md @@ -19,13 +19,13 @@ tag: 我们可以把消息队列看作是一个存放消息的容器,当我们需要使用消息的时候,直接从容器中取出消息供自己使用即可。由于队列 Queue 是一种先进先出的数据结构,所以消费消息时也是按照顺序来消费的。 -![Message queue](https://oss.javaguide.cn/%E6%B6%88%E6%81%AF%E9%98%9F%E5%88%97/message-queue-small.png) +![](https://oss.javaguide.cn/github/javaguide/high-performance/message-queue/message-queue-small.png) 参与消息传递的双方称为 **生产者** 和 **消费者** ,生产者负责发送消息,消费者负责处理消息。 -![发布/订阅(Pub/Sub)模型](../images/message-queue/message-queue-pub-sub-model.png) +![发布/订阅(Pub/Sub)模型](https://oss.javaguide.cn/github/javaguide/high-performance/message-queue/message-queue-pub-sub-model.png) -我们知道操作系统中的进程通信的一种很重要的方式就是消息队列。我们这里提到的消息队列稍微有点区别,更多指的是各个服务以及系统内部各个组件/模块之前的通信,属于一种 **中间件** 。 +操作系统中的进程通信的一种很重要的方式就是消息队列。我们这里提到的消息队列稍微有点区别,更多指的是各个服务以及系统内部各个组件/模块之前的通信,属于一种 **中间件** 。 维基百科是这样介绍中间件的: @@ -35,25 +35,27 @@ tag: 除了消息队列之外,常见的中间件还有 RPC 框架、分布式组件、HTTP 服务器、任务调度框架、配置中心、数据库层的分库分表工具和数据迁移工具等等。 -关于中间件比较详细的介绍可以参考阿里巴巴淘系技术的一篇回答:https://www.zhihu.com/question/19730582/answer/1663627873 。 +关于中间件比较详细的介绍可以参考阿里巴巴淘系技术的一篇回答: 。 随着分布式和微服务系统的发展,消息队列在系统设计中有了更大的发挥空间,使用消息队列可以降低系统耦合性、实现任务异步、有效地进行流量削峰,是分布式和微服务系统中重要的组件之一。 ## 消息队列有什么用? -通常来说,使用消息队列能为我们的系统带来下面三点好处: +通常来说,使用消息队列主要能为我们的系统带来下面三点好处: -1. **通过异步处理提高系统性能(减少响应所需时间)** -2. **削峰/限流** -3. **降低系统耦合性。** +1. 异步处理 +2. 削峰/限流 +3. 降低系统耦合性 + +除了这三点之外,消息队列还有其他的一些应用场景,例如实现分布式事务、顺序保证和数据流处理。 如果在面试的时候你被面试官问到这个问题的话,一般情况是你在你的简历上涉及到消息队列这方面的内容,这个时候推荐你结合你自己的项目来回答。 -### 通过异步处理提高系统性能(减少响应所需时间) +### 异步处理 ![通过异步处理提高系统性能](https://oss.javaguide.cn/github/javaguide/Asynchronous-message-queue.png) -将用户的请求数据存储到消息队列之后就立即返回结果。随后,系统再对消息进行消费。 +将用户请求中包含的耗时操作,通过消息队列实现异步处理,将对应的消息发送到消息队列之后就立即返回结果,减少响应时间,提高用户体验。随后,系统再对消息进行消费。 因为用户请求数据写入消息队列之后就立即返回给用户了,但是请求数据在后续的业务校验、写数据库等操作中可能失败。因此,**使用消息队列进行异步处理之后,需要适当修改业务流程进行配合**,比如用户在提交订单之后,订单数据写入消息队列,不能立即返回用户订单提交成功,需要在消息队列的订单消费者进程真正处理完该订单之后,甚至出库后,再通过电子邮件或短信通知用户订单成功,以免交易纠纷。这就类似我们平时手机订火车票和电影票。 @@ -67,15 +69,17 @@ tag: ### 降低系统耦合性 -使用消息队列还可以降低系统耦合性。我们知道如果模块之间不存在直接调用,那么新增模块或者修改模块就对其他模块影响较小,这样系统的可扩展性无疑更好一些。还是直接上图吧: +使用消息队列还可以降低系统耦合性。如果模块之间不存在直接调用,那么新增模块或者修改模块就对其他模块影响较小,这样系统的可扩展性无疑更好一些。 -![解耦](https://my-blog-to-use.oss-cn-beijing.aliyuncs.com/2019-11/消息队列-解耦.png) +生产者(客户端)发送消息到消息队列中去,消费者(服务端)处理消息,需要消费的系统直接去消息队列取消息进行消费即可而不需要和其他系统有耦合,这显然也提高了系统的扩展性。 -生产者(客户端)发送消息到消息队列中去,接受者(服务端)处理消息,需要消费的系统直接去消息队列取消息进行消费即可而不需要和其他系统有耦合,这显然也提高了系统的扩展性。 +![发布/订阅(Pub/Sub)模型](https://oss.javaguide.cn/github/javaguide/high-performance/message-queue/message-queue-pub-sub-model.png) **消息队列使用发布-订阅模式工作,消息发送者(生产者)发布消息,一个或多个消息接受者(消费者)订阅消息。** 从上图可以看到**消息发送者(生产者)和消息接受者(消费者)之间没有直接耦合**,消息发送者将消息发送至分布式消息队列即结束对消息的处理,消息接受者从分布式消息队列获取该消息后进行后续处理,并不需要知道该消息从何而来。**对新增业务,只要对该类消息感兴趣,即可订阅该消息,对原有系统和业务没有任何影响,从而实现网站业务的可扩展性设计**。 -消息接受者对消息进行过滤、处理、包装后,构造成一个新的消息类型,将消息继续发送出去,等待其他消息接受者订阅该消息。因此基于事件(消息对象)驱动的业务架构可以是一系列流程。 +例如,我们商城系统分为用户、订单、财务、仓储、消息通知、物流、风控等多个服务。用户在完成下单后,需要调用财务(扣款)、仓储(库存管理)、物流(发货)、消息通知(通知用户发货)、风控(风险评估)等服务。使用消息队列后,下单操作和后续的扣款、发货、通知等操作就解耦了,下单完成发送一个消息到消息队列,需要用到的地方去订阅这个消息进行消息即可。 + +![](https://oss.javaguide.cn/github/javaguide/high-performance/message-queue/message-queue-decouple-mall-example.png) 另外,为了避免消息队列服务器宕机造成消息丢失,会将成功发送到消息队列的消息存储在消息生产者服务器上,等消息真正被消费者服务器处理后才删除消息。在消息队列服务器宕机后,生产者服务器会选择分布式消息队列服务器集群中的其他服务器发布消息。 @@ -83,7 +87,7 @@ tag: ### 实现分布式事务 -我们知道分布式事务的解决方案之一就是 MQ 事务。 +分布式事务的解决方案之一就是 MQ 事务。 RocketMQ、 Kafka、Pulsar、QMQ 都提供了事务相关的功能。事务允许事件流应用将消费,处理,生产消息整个过程定义为一个原子操作。 @@ -91,6 +95,26 @@ RocketMQ、 Kafka、Pulsar、QMQ 都提供了事务相关的功能。事务允 ![分布式事务详解 - MQ事务](https://oss.javaguide.cn/github/javaguide/csdn/07b338324a7d8894b8aef4b659b76d92.png) +### 顺序保证 + +在很多应用场景中,处理数据的顺序至关重要。消息队列保证数据按照特定的顺序被处理,适用于那些对数据顺序有严格要求的场景。大部分消息队列,例如 RocketMQ、RabbitMQ、Pulsar、Kafka,都支持顺序消息。 + +### 延时/定时处理 + +消息发送后不会立即被消费,而是指定一个时间,到时间后再消费。大部分消息队列,例如 RocketMQ、RabbitMQ、Pulsar,都支持定时/延时消息。 + +![](https://oss.javaguide.cn/github/javaguide/tools/docker/rocketmq-schedule-message.png) + +### 即时通讯 + +MQTT(消息队列遥测传输协议)是一种轻量级的通讯协议,采用发布/订阅模式,非常适合于物联网(IoT)等需要在低带宽、高延迟或不可靠网络环境下工作的应用。它支持即时消息传递,即使在网络条件较差的情况下也能保持通信的稳定性。 + +RabbitMQ 内置了 MQTT 插件用于实现 MQTT 功能(默认不启用,需要手动开启)。 + +### 数据流处理 + +针对分布式系统产生的海量数据流,如业务日志、监控数据、用户行为等,消息队列可以实时或批量收集这些数据,并将其导入到大数据处理引擎中,实现高效的数据流管理和处理。 + ## 使用消息队列会带来哪些问题? - **系统可用性降低:** 系统可用性在某种程度上降低,为什么这样说呢?在加入 MQ 之前,你不用考虑消息丢失或者说 MQ 挂掉等等的情况,但是,引入 MQ 之后你就需要去考虑了! @@ -117,15 +141,15 @@ JMS 定义了五种不同的消息正文格式以及调用的消息类型,允 #### 点到点(P2P)模型 -![队列模型](../images/message-queue/message-queue-queue-model.png) +![队列模型](https://oss.javaguide.cn/github/javaguide/high-performance/message-queue/message-queue-queue-model.png) 使用**队列(Queue)**作为消息通信载体;满足**生产者与消费者模式**,一条消息只能被一个消费者使用,未被消费的消息在队列中保留直到被消费或超时。比如:我们生产者发送 100 条消息的话,两个消费者来消费一般情况下两个消费者会按照消息发送的顺序各自消费一半(也就是你一个我一个的消费。) #### 发布/订阅(Pub/Sub)模型 -![发布/订阅(Pub/Sub)模型](../images/message-queue/message-queue-pub-sub-model.png) +![发布/订阅(Pub/Sub)模型](https://oss.javaguide.cn/github/javaguide/high-performance/message-queue/message-queue-pub-sub-model.png) -发布订阅模型(Pub/Sub) 使用**主题(Topic)**作为消息通信载体,类似于**广播模式**;发布者发布一条消息,该消息通过主题传递给所有的订阅者,**在一条消息广播之后才订阅的用户则是收不到该条消息的**。 +发布订阅模型(Pub/Sub) 使用**主题(Topic)**作为消息通信载体,类似于**广播模式**;发布者发布一条消息,该消息通过主题传递给所有的订阅者。 ### AMQP 是什么? @@ -182,11 +206,11 @@ Kafka 是一个分布式系统,由通过高性能 TCP 网络协议进行通信 不过,要提示一下:**如果要使用 KRaft 模式的话,建议选择较高版本的 Kafka,因为这个功能还在持续完善优化中。Kafka 3.3.1 版本是第一个将 KRaft(Kafka Raft)共识协议标记为生产就绪的版本。** -![](https://oss.javaguide.cn/github/javaguide/high-performance/message-queue/kafka3.3.1-kraft-%20production-ready.png) +![](https://oss.javaguide.cn/github/javaguide/high-performance/message-queue/kafka3.3.1-kraft-production-ready.png) -Kafka 官网:http://kafka.apache.org/ +Kafka 官网: -Kafka 更新记录(可以直观看到项目是否还在维护):https://kafka.apache.org/downloads +Kafka 更新记录(可以直观看到项目是否还在维护): #### RocketMQ @@ -207,9 +231,9 @@ RocketMQ 的核心特性(摘自 RocketMQ 官网): > Apache RocketMQ 自诞生以来,因其架构简单、业务功能丰富、具备极强可扩展性等特点被众多企业开发者以及云厂商广泛采用。历经十余年的大规模场景打磨,RocketMQ 已经成为业内共识的金融级可靠业务消息首选方案,被广泛应用于互联网、大数据、移动互联网、物联网等领域的业务场景。 -RocketMQ 官网:https://rocketmq.apache.org/ (文档很详细,推荐阅读) +RocketMQ 官网: (文档很详细,推荐阅读) -RocketMQ 更新记录(可以直观看到项目是否还在维护):https://github.com/apache/rocketmq/releases +RocketMQ 更新记录(可以直观看到项目是否还在维护): #### RabbitMQ @@ -228,9 +252,9 @@ RabbitMQ 发展到今天,被越来越多的人认可,这和它在易用性 - **易用的管理界面:** RabbitMQ 提供了一个易用的用户界面,使得用户可以监控和管理消息、集群中的节点等。在安装 RabbitMQ 的时候会介绍到,安装好 RabbitMQ 就自带管理界面。 - **插件机制:** RabbitMQ 提供了许多插件,以实现从多方面进行扩展,当然也可以编写自己的插件。感觉这个有点类似 Dubbo 的 SPI 机制 -RabbitMQ 官网:https://www.rabbitmq.com/ 。 +RabbitMQ 官网: 。 -RabbitMQ 更新记录(可以直观看到项目是否还在维护):https://www.rabbitmq.com/news.html +RabbitMQ 更新记录(可以直观看到项目是否还在维护): #### Pulsar @@ -253,9 +277,9 @@ Pulsar 的关键特性如下(摘自官网): - 基于 Pulsar Functions 的 serverless connector 框架 Pulsar IO 使得数据更易移入、移出 Apache Pulsar。 - 分层式存储可在数据陈旧时,将数据从热存储卸载到冷/长期存储(如 S3、GCS)中。 -Pulsar 官网:https://pulsar.apache.org/ +Pulsar 官网: -Pulsar 更新记录(可以直观看到项目是否还在维护):https://github.com/apache/pulsar/releases +Pulsar 更新记录(可以直观看到项目是否还在维护): #### ActiveMQ @@ -284,5 +308,7 @@ Pulsar 更新记录(可以直观看到项目是否还在维护):https://gi ## 参考 - 《大型网站技术架构 》 -- KRaft: Apache Kafka Without ZooKeeper:https://developer.confluent.io/learn/kraft/ -- 消息队列的使用场景是什么样的?:https://mp.weixin.qq.com/s/4V1jI6RylJr7Jr9JsQe73A +- KRaft: Apache Kafka Without ZooKeeper: +- 消息队列的使用场景是什么样的?: + + diff --git a/docs/high-performance/message-queue/rabbitmq-questions.md b/docs/high-performance/message-queue/rabbitmq-questions.md index 0f0d9f8e519..e971eaf3604 100644 --- a/docs/high-performance/message-queue/rabbitmq-questions.md +++ b/docs/high-performance/message-queue/rabbitmq-questions.md @@ -25,7 +25,7 @@ PS:也可能直接问什么是消息队列?消息队列就是一个使用队 ## RabbitMQ 特点? - **可靠性**: RabbitMQ 使用一些机制来保证可靠性, 如持久化、传输确认及发布确认等。 -- **灵活的路由** : 在消息进入队列之前,通过交换器来路由消息。对于典型的路由功能, RabbitMQ 己经提供了一些内置的交换器来实现。针对更复杂的路由功能,可以将多个 交换器绑定在一起, 也可以通过插件机制来实现自己的交换器。 +- **灵活的路由** : 在消息进入队列之前,通过交换器来路由消息。对于典型的路由功能, RabbitMQ 己经提供了一些内置的交换器来实现。针对更复杂的路由功能,可以将多个交换器绑定在一起, 也可以通过插件机制来实现自己的交换器。 - **扩展性**: 多个 RabbitMQ 节点可以组成一个集群,也可以根据实际业务情况动态地扩展 集群中节点。 - **高可用性** : 队列可以在集群中的机器上设置镜像,使得在部分节点出现问题的情况下队 列仍然可用。 - **多种协议**: RabbitMQ 除了原生支持 AMQP 协议,还支持 STOMP, MQTT 等多种消息 中间件协议。 @@ -142,7 +142,7 @@ RabbitMQ 中的交换器、交换器类型、队列、绑定、路由键等都 - **Module Layer**:协议最高层,主要定义了一些客户端调用的命令,客户端可以用这些命令实现自己的业务逻辑。 - **Session Layer**:中间层,主要负责客户端命令发送给服务器,再将服务端应答返回客户端,提供可靠性同步机制和错误处理。 -- **TransportLayer**:最底层,主要传输二进制数据流,提供帧的处理、信道服用、错误检测和数据表示等。 +- **TransportLayer**:最底层,主要传输二进制数据流,提供帧的处理、信道复用、错误检测和数据表示等。 **AMQP 模型的三大组件**: @@ -164,13 +164,13 @@ RabbitMQ 中的交换器、交换器类型、队列、绑定、路由键等都 ## 说说 Broker 服务节点、Queue 队列、Exchange 交换器? -- **Broker**:可以看做 RabbitMQ 的服务节点。一般请下一个 Broker 可以看做一个 RabbitMQ 服务器。 -- **Queue** :RabbitMQ 的内部对象,用于存储消息。多个消费者可以订阅同一队列,这时队列中的消息会被平摊(轮询)给多个消费者进行处理。 -- **Exchange** : 生产者将消息发送到交换器,由交换器将消息路由到一个或者多个队列中。当路由不到时,或返回给生产者或直接丢弃。 +- **Broker**:可以看做 RabbitMQ 的服务节点。一般情况下一个 Broker 可以看做一个 RabbitMQ 服务器。 +- **Queue**:RabbitMQ 的内部对象,用于存储消息。多个消费者可以订阅同一队列,这时队列中的消息会被平摊(轮询)给多个消费者进行处理。 +- **Exchange**:生产者将消息发送到交换器,由交换器将消息路由到一个或者多个队列中。当路由不到时,或返回给生产者或直接丢弃。 ## 什么是死信队列?如何导致的? -DLX,全称为 `Dead-Letter-Exchange`,死信交换器,死信邮箱。当消息在一个队列中变成死信 (`dead message`) 之后,它能被重新被发送到另一个交换器中,这个交换器就是 DLX,绑定 DLX 的队列就称之为死信队列。 +DLX,全称为 `Dead-Letter-Exchange`,死信交换器,死信邮箱。当消息在一个队列中变成死信 (`dead message`) 之后,它能被重新发送到另一个交换器中,这个交换器就是 DLX,绑定 DLX 的队列就称之为死信队列。 **导致的死信的几种原因**: @@ -243,3 +243,5 @@ Demo 级别的,一般就是你本地启动了玩玩儿的?,没人生产用 ## 如何解决消息队列的延时以及过期失效问题? RabbtiMQ 是可以设置过期时间的,也就是 TTL。如果消息在 queue 中积压超过一定的时间就会被 RabbitMQ 给清理掉,这个数据就没了。那这就是第二个坑了。这就不是说数据会大量积压在 mq 里,而是大量的数据会直接搞丢。我们可以采取一个方案,就是批量重导,这个我们之前线上也有类似的场景干过。就是大量积压的时候,我们当时就直接丢弃数据了,然后等过了高峰期以后,比如大家一起喝咖啡熬夜到晚上 12 点以后,用户都睡觉了。这个时候我们就开始写程序,将丢失的那批数据,写个临时程序,一点一点的查出来,然后重新灌入 mq 里面去,把白天丢的数据给他补回来。也只能是这样了。假设 1 万个订单积压在 mq 里面,没有处理,其中 1000 个订单都丢了,你只能手动写程序把那 1000 个订单给查出来,手动发到 mq 里去再补一次。 + + diff --git a/docs/high-performance/message-queue/rocketmq-questions.md b/docs/high-performance/message-queue/rocketmq-questions.md index 81c467201c5..9591e5d2612 100644 --- a/docs/high-performance/message-queue/rocketmq-questions.md +++ b/docs/high-performance/message-queue/rocketmq-questions.md @@ -6,7 +6,10 @@ tag: - 消息队列 --- -> [本文由 FrancisQ 投稿!](https://mp.weixin.qq.com/s?__biz=Mzg2OTA0Njk0OA==&mid=2247485969&idx=1&sn=6bd53abde30d42a778d5a35ec104428c&chksm=cea245daf9d5cccce631f93115f0c2c4a7634e55f5bef9009fd03f5a0ffa55b745b5ef4f0530&token=294077121&lang=zh_CN#rd) +> [本文由 FrancisQ 投稿!](https://mp.weixin.qq.com/s?__biz=Mzg2OTA0Njk0OA==&mid=2247485969&idx=1&sn=6bd53abde30d42a778d5a35ec104428c&chksm=cea245daf9d5cccce631f93115f0c2c4a7634e55f5bef9009fd03f5a0ffa55b745b5ef4f0530&token=294077121&lang=zh_CN#rd) 相比原文主要进行了下面这些完善: +> +> - [分析了 RocketMQ 高性能读写的原因和顺序消费的具体实现](https://github.com/Snailclimb/JavaGuide/pull/2133) +> - [增加了消息类型、消费者类型、消费者组和生产者组的介绍](https://github.com/Snailclimb/JavaGuide/pull/2134) ## 消息队列扫盲 @@ -28,13 +31,13 @@ tag: 我来举个 🌰 吧,比如我们有一个购票系统,需求是用户在购买完之后能接收到购买完成的短信。 -![](https://my-blog-to-use.oss-cn-beijing.aliyuncs.com/2019-11/16ef37fee7e09230.jpg) +![](https://oss.javaguide.cn/github/javaguide/high-performance/message-queue/16ef37fee7e09230.jpg) 我们省略中间的网络通信时间消耗,假如购票系统处理需要 150ms ,短信系统处理需要 200ms ,那么整个处理流程的时间消耗就是 150ms + 200ms = 350ms。 当然,乍看没什么问题。可是仔细一想你就感觉有点问题,我用户购票在购票系统的时候其实就已经完成了购买,而我现在通过同步调用非要让整个请求拉长时间,而短信系统这玩意又不是很有必要,它仅仅是一个辅助功能增强用户体验感而已。我现在整个调用流程就有点 **头重脚轻** 的感觉了,购票是一个不太耗时的流程,而我现在因为同步调用,非要等待发送短信这个比较耗时的操作才返回结果。那我如果再加一个发送邮件呢? -![](https://my-blog-to-use.oss-cn-beijing.aliyuncs.com/2019-11/16ef380429cf373e.jpg) +![](https://oss.javaguide.cn/github/javaguide/high-performance/message-queue/16ef380429cf373e.jpg) 这样整个系统的调用链又变长了,整个时间就变成了 550ms。 @@ -48,13 +51,11 @@ tag: 回想一下,我们在给大妈发送需要的信息之后我们是 **同步等待大妈给我配好饭菜** 的,上面我们只是加了鸡腿和土豆丝,万一我再加一个番茄牛腩,韭菜鸡蛋,这样是不是大妈打饭配菜的流程就会变长,我们等待的时间也会相应的变长。 -![](https://my-blog-to-use.oss-cn-beijing.aliyuncs.com/2019-11/006APoFYly1fvd9cwjlfrj30as0b03ym.jpg) - 那后来,我们工作赚钱了有钱去饭店吃饭了,我们告诉服务员来一碗牛肉面加个荷包蛋 **(传达一个消息)** ,然后我们就可以在饭桌上安心的玩手机了 **(干自己其他事情)** ,等到我们的牛肉面上了我们就可以吃了。这其中我们也就传达了一个消息,然后我们又转过头干其他事情了。这其中虽然做面的时间没有变短,但是我们只需要传达一个消息就可以干其他事情了,这是一个 **异步** 的概念。 所以,为了解决这一个问题,聪明的程序员在中间也加了个类似于服务员的中间件——消息队列。这个时候我们就可以把模型给改造了。 -![](https://my-blog-to-use.oss-cn-beijing.aliyuncs.com/2019-11/16ef38124f55eaea.jpg) +![](https://oss.javaguide.cn/github/javaguide/high-performance/message-queue/16ef38124f55eaea.jpg) 这样,我们在将消息存入消息队列之后我们就可以直接返回了(我们告诉服务员我们要吃什么然后玩手机),所以整个耗时只是 150ms + 10ms = 160ms。 @@ -64,21 +65,21 @@ tag: 回到最初同步调用的过程,我们写个伪代码简单概括一下。 -![](https://my-blog-to-use.oss-cn-beijing.aliyuncs.com/2019-11/16ef381a505d3e1f.jpg) +![](https://oss.javaguide.cn/github/javaguide/high-performance/message-queue/16ef381a505d3e1f.jpg) 那么第二步,我们又添加了一个发送邮件,我们就得重新去修改代码,如果我们又加一个需求:用户购买完还需要给他加积分,这个时候我们是不是又得改代码? -![](https://my-blog-to-use.oss-cn-beijing.aliyuncs.com/2019-11/16ef381c4e1b1ac7.jpg) +![](https://oss.javaguide.cn/github/javaguide/high-performance/message-queue/16ef381c4e1b1ac7.jpg) 如果你觉得还行,那么我这个时候不要发邮件这个服务了呢,我是不是又得改代码,又得重启应用? -![](https://my-blog-to-use.oss-cn-beijing.aliyuncs.com/2019-11/16ef381f273a66bd.jpg) +![](https://oss.javaguide.cn/github/javaguide/high-performance/message-queue/16ef381f273a66bd.jpg) 这样改来改去是不是很麻烦,那么 **此时我们就用一个消息队列在中间进行解耦** 。你需要注意的是,我们后面的发送短信、发送邮件、添加积分等一些操作都依赖于上面的 `result` ,这东西抽象出来就是购票的处理结果呀,比如订单号,用户账号等等,也就是说我们后面的一系列服务都是需要同样的消息来进行处理。既然这样,我们是不是可以通过 **“广播消息”** 来实现。 我上面所讲的“广播”并不是真正的广播,而是接下来的系统作为消费者去 **订阅** 特定的主题。比如我们这里的主题就可以叫做 `订票` ,我们购买系统作为一个生产者去生产这条消息放入消息队列,然后消费者订阅了这个主题,会从消息队列中拉取消息并消费。就比如我们刚刚画的那张图,你会发现,在生产者这边我们只需要关注 **生产消息到指定主题中** ,而 **消费者只需要关注从指定主题中拉取消息** 就行了。 -![](https://my-blog-to-use.oss-cn-beijing.aliyuncs.com/2019-11/16ef382674b66892.jpg) +![](https://oss.javaguide.cn/github/javaguide/high-performance/message-queue/16ef382674b66892.jpg) > 如果没有消息队列,每当一个新的业务接入,我们都要在主系统调用新接口、或者当我们取消某些业务,我们也得在主系统删除某些接口调用。有了消息队列,我们只需要关心消息是否送达了队列,至于谁希望订阅,接下来收到消息如何处理,是下游的事情,无疑极大地减少了开发和联调的工作量。 @@ -86,7 +87,7 @@ tag: 我们再次回到一开始我们使用同步调用系统的情况,并且思考一下,如果此时有大量用户请求购票整个系统会变成什么样? -![](https://my-blog-to-use.oss-cn-beijing.aliyuncs.com/2019-11/16ef382a9756bb1c.jpg) +![](https://oss.javaguide.cn/github/javaguide/high-performance/message-queue/16ef382a9756bb1c.jpg) 如果,此时有一万的请求进入购票系统,我们知道运行我们主业务的服务器配置一般会比较好,所以这里我们假设购票系统能承受这一万的用户请求,那么也就意味着我们同时也会出现一万调用发短信服务的请求。而对于短信系统来说并不是我们的主要业务,所以我们配备的硬件资源并不会太高,那么你觉得现在这个短信系统能承受这一万的峰值么,且不说能不能承受,系统会不会 **直接崩溃** 了? @@ -128,13 +129,13 @@ tag: 可用性降低,复杂度上升,又带来一系列的重复消费,顺序消费,分布式事务,消息堆积的问题,这消息队列还怎么用啊 😵? -![](https://my-blog-to-use.oss-cn-beijing.aliyuncs.com/2019-11/16ef382d709abc9d.png) +![](https://oss.javaguide.cn/github/javaguide/high-performance/message-queue/16ef382d709abc9d.png) 别急,办法总是有的。 ## RocketMQ 是什么? -![](https://my-blog-to-use.oss-cn-beijing.aliyuncs.com/2019-11/16ef383014430799.jpg) +![](https://oss.javaguide.cn/github/javaguide/high-performance/message-queue/16ef383014430799.jpg) 哇,你个混蛋!上面给我抛出那么多问题,你现在又讲 `RocketMQ` ,还让不让人活了?!🤬 @@ -160,7 +161,7 @@ tag: 就像我们理解队列一样,消息中间件的队列模型就真的只是一个队列。。。我画一张图给大家理解。 -![](https://my-blog-to-use.oss-cn-beijing.aliyuncs.com/2019-11/16ef3834ae653469.jpg) +![](https://oss.javaguide.cn/github/javaguide/high-performance/message-queue/16ef3834ae653469.jpg) 在一开始我跟你提到了一个 **“广播”** 的概念,也就是说如果我们此时我们需要将一个消息发送给多个消费者(比如此时我需要将信息发送给短信系统和邮件系统),这个时候单个队列即不能满足需求了。 @@ -176,7 +177,7 @@ tag: 其中,发布者将消息发送到指定主题中,订阅者需要 **提前订阅主题** 才能接受特定主题的消息。 -![](https://my-blog-to-use.oss-cn-beijing.aliyuncs.com/2019-11/16ef3837887d9a54sds.jpg) +![](https://oss.javaguide.cn/github/javaguide/high-performance/message-queue/16ef3837887d9a54sds.jpg) ### RocketMQ 中的消息模型 @@ -186,7 +187,7 @@ tag: 所以,`RocketMQ` 中的 **主题模型** 到底是如何实现的呢?首先我画一张图,大家尝试着去理解一下。 -![](https://my-blog-to-use.oss-cn-beijing.aliyuncs.com/2019-11/16ef383d3e8c9788.jpg) +![](https://oss.javaguide.cn/github/javaguide/high-performance/message-queue/16ef383d3e8c9788.jpg) 我们可以看到在整个图中有 `Producer Group`、`Topic`、`Consumer Group` 三个角色,我来分别介绍一下他们。 @@ -200,19 +201,19 @@ tag: 当然也可以消费者个数小于队列个数,只不过不太建议。如下图。 -![](https://my-blog-to-use.oss-cn-beijing.aliyuncs.com/2019-11/16ef3850c808d707.jpg) +![](https://oss.javaguide.cn/github/javaguide/high-performance/message-queue/16ef3850c808d707.jpg) **每个消费组在每个队列上维护一个消费位置** ,为什么呢? 因为我们刚刚画的仅仅是一个消费者组,我们知道在发布订阅模式中一般会涉及到多个消费者组,而每个消费者组在每个队列中的消费位置都是不同的。如果此时有多个消费者组,那么消息被一个消费者组消费完之后是不会删除的(因为其它消费者组也需要呀),它仅仅是为每个消费者组维护一个 **消费位移(offset)** ,每次消费者组消费完会返回一个成功的响应,然后队列再把维护的消费位移加一,这样就不会出现刚刚消费过的消息再一次被消费了。 -![](https://my-blog-to-use.oss-cn-beijing.aliyuncs.com/2019-11/16ef3857fefaa079.jpg) +![](https://oss.javaguide.cn/github/javaguide/high-performance/message-queue/16ef3857fefaa079.jpg) 可能你还有一个问题,**为什么一个主题中需要维护多个队列** ? 答案是 **提高并发能力** 。的确,每个主题中只存在一个队列也是可行的。你想一下,如果每个主题中只存在一个队列,这个队列中也维护着每个消费者组的消费位置,这样也可以做到 **发布订阅模式** 。如下图。 -![](https://my-blog-to-use.oss-cn-beijing.aliyuncs.com/2019-11/16ef38600cdb6d4b.jpg) +![](https://oss.javaguide.cn/github/javaguide/high-performance/message-queue/16ef38600cdb6d4b.jpg) 但是,这样我生产者是不是只能向一个队列发送消息?又因为需要维护消费位置所以一个队列只能对应一个消费者组中的消费者,这样是不是其他的 `Consumer` 就没有用武之地了?从这两个角度来讲,并发度一下子就小了很多。 @@ -234,7 +235,7 @@ tag: `Topic` 消息量都比较均匀的情况下,如果某个 `broker` 上的队列越多,则该 `broker` 压力越大。 - ![](https://my-blog-to-use.oss-cn-beijing.aliyuncs.com/2019-11/16ef38687488a5a4.jpg) + ![](https://oss.javaguide.cn/github/javaguide/high-performance/message-queue/16ef38687488a5a4.jpg) > 所以说我们需要配置多个 Broker。 @@ -246,7 +247,7 @@ tag: 听完了上面的解释你可能会觉得,这玩意好简单。不就是这样的么? -![](https://my-blog-to-use.oss-cn-beijing.aliyuncs.com/2019-11/16ef386c6d1e8bdb.jpg) +![](https://oss.javaguide.cn/github/javaguide/high-performance/message-queue/16ef386c6d1e8bdb.jpg) 嗯?你可能会发现一个问题,这老家伙 `NameServer` 干啥用的,这不多余吗?直接 `Producer`、`Consumer` 和 `Broker` 直接进行生产消息,消费消息不就好了么? @@ -258,11 +259,11 @@ tag: 当然,`RocketMQ` 中的技术架构肯定不止前面那么简单,因为上面图中的四个角色都是需要做集群的。我给出一张官网的架构图,大家尝试理解一下。 -![](https://my-blog-to-use.oss-cn-beijing.aliyuncs.com/2019-11/16ef386fa3be1e53.jpg) +![](https://oss.javaguide.cn/github/javaguide/high-performance/message-queue/16ef386fa3be1e53.jpg) 其实和我们最开始画的那张乞丐版的架构图也没什么区别,主要是一些细节上的差别。听我细细道来 🤨。 -第一、我们的 `Broker` **做了集群并且还进行了主从部署** ,由于消息分布在各个 `Broker` 上,一旦某个 `Broker` 宕机,则该`Broker` 上的消息读写都会受到影响。所以 `Rocketmq` 提供了 `master/slave` 的结构,` salve` 定时从 `master` 同步数据(同步刷盘或者异步刷盘),如果 `master` 宕机,**则 `slave` 提供消费服务,但是不能写入消息** (后面我还会提到哦)。 +第一、我们的 `Broker` **做了集群并且还进行了主从部署** ,由于消息分布在各个 `Broker` 上,一旦某个 `Broker` 宕机,则该`Broker` 上的消息读写都会受到影响。所以 `Rocketmq` 提供了 `master/slave` 的结构,`salve` 定时从 `master` 同步数据(同步刷盘或者异步刷盘),如果 `master` 宕机,**则 `slave` 提供消费服务,但是不能写入消息** (后面我还会提到哦)。 第二、为了保证 `HA` ,我们的 `NameServer` 也做了集群部署,但是请注意它是 **去中心化** 的。也就意味着它没有主节点,你可以很明显地看出 `NameServer` 的所有节点是没有进行 `Info Replicate` 的,在 `RocketMQ` 中是通过 **单个 Broker 和所有 NameServer 保持长连接** ,并且在每隔 30 秒 `Broker` 会向所有 `Nameserver` 发送心跳,心跳包含了自身的 `Topic` 配置信息,这个步骤就对应这上面的 `Routing Info` 。 @@ -270,6 +271,168 @@ tag: 第四、消费者通过 `NameServer` 获取所有 `Broker` 的路由信息后,向 `Broker` 发送 `Pull` 请求来获取消息数据。`Consumer` 可以以两种模式启动—— **广播(Broadcast)和集群(Cluster)**。广播模式下,一条消息会发送给 **同一个消费组中的所有消费者** ,集群模式下消息只会发送给一个消费者。 +## RocketMQ 功能特性 + +### 消息 + +#### 普通消息 + +普通消息一般应用于微服务解耦、事件驱动、数据集成等场景,这些场景大多数要求数据传输通道具有可靠传输的能力,且对消息的处理时机、处理顺序没有特别要求。以在线的电商交易场景为例,上游订单系统将用户下单支付这一业务事件封装成独立的普通消息并发送至 RocketMQ 服务端,下游按需从服务端订阅消息并按照本地消费逻辑处理下游任务。每个消息之间都是相互独立的,且不需要产生关联。另外还有日志系统,以离线的日志收集场景为例,通过埋点组件收集前端应用的相关操作日志,并转发到 RocketMQ 。 + +![](https://rocketmq.apache.org/zh/assets/images/lifecyclefornormal-e8a2a7e42a0722f681eb129b51e1bd66.png) + +**普通消息生命周期** + +- 初始化:消息被生产者构建并完成初始化,待发送到服务端的状态。 +- 待消费:消息被发送到服务端,对消费者可见,等待消费者消费的状态。 +- 消费中:消息被消费者获取,并按照消费者本地的业务逻辑进行处理的过程。 此时服务端会等待消费者完成消费并提交消费结果,如果一定时间后没有收到消费者的响应,RocketMQ 会对消息进行重试处理。 +- 消费提交:消费者完成消费处理,并向服务端提交消费结果,服务端标记当前消息已经被处理(包括消费成功和失败)。RocketMQ 默认支持保留所有消息,此时消息数据并不会立即被删除,只是逻辑标记已消费。消息在保存时间到期或存储空间不足被删除前,消费者仍然可以回溯消息重新消费。 +- 消息删除:RocketMQ 按照消息保存机制滚动清理最早的消息数据,将消息从物理文件中删除。 + +#### 定时消息 + +在分布式定时调度触发、任务超时处理等场景,需要实现精准、可靠的定时事件触发。使用 RocketMQ 的定时消息可以简化定时调度任务的开发逻辑,实现高性能、可扩展、高可靠的定时触发能力。定时消息仅支持在 MessageType 为 Delay 的主题内使用,即定时消息只能发送至类型为定时消息的主题中,发送的消息的类型必须和主题的类型一致。在 4.x 版本中,只支持延时消息,默认分为 18 个等级分别为:1s 5s 10s 30s 1m 2m 3m 4m 5m 6m 7m 8m 9m 10m 20m 30m 1h 2h,也可以在配置文件中增加自定义的延时等级和时长。在 5.x 版本中,开始支持定时消息,在构造消息时提供了 3 个 API 来指定延迟时间或定时时间。 + +基于定时消息的超时任务处理具备如下优势: + +- **精度高、开发门槛低**:基于消息通知方式不存在定时阶梯间隔。可以轻松实现任意精度事件触发,无需业务去重。 +- **高性能可扩展**:传统的数据库扫描方式较为复杂,需要频繁调用接口扫描,容易产生性能瓶颈。RocketMQ 的定时消息具有高并发和水平扩展的能力。 + +![](https://rocketmq.apache.org/zh/assets/images/lifecyclefordelay-2ce8278df69cd026dd11ffd27ab09a17.png) + +**定时消息生命周期** + +- 初始化:消息被生产者构建并完成初始化,待发送到服务端的状态。 +- 定时中:消息被发送到服务端,和普通消息不同的是,服务端不会直接构建消息索引,而是会将定时消息**单独存储在定时存储系统中**,等待定时时刻到达。 +- 待消费:定时时刻到达后,服务端将消息重新写入普通存储引擎,对下游消费者可见,等待消费者消费的状态。 +- 消费中:消息被消费者获取,并按照消费者本地的业务逻辑进行处理的过程。 此时服务端会等待消费者完成消费并提交消费结果,如果一定时间后没有收到消费者的响应,RocketMQ 会对消息进行重试处理。 +- 消费提交:消费者完成消费处理,并向服务端提交消费结果,服务端标记当前消息已经被处理(包括消费成功和失败)。RocketMQ 默认支持保留所有消息,此时消息数据并不会立即被删除,只是逻辑标记已消费。消息在保存时间到期或存储空间不足被删除前,消费者仍然可以回溯消息重新消费。 +- 消息删除:Apache RocketMQ 按照消息保存机制滚动清理最早的消息数据,将消息从物理文件中删除。 + +定时消息的实现逻辑需要先经过定时存储等待触发,定时时间到达后才会被投递给消费者。因此,如果将大量定时消息的定时时间设置为同一时刻,则到达该时刻后会有大量消息同时需要被处理,会造成系统压力过大,导致消息分发延迟,影响定时精度。 + +#### 顺序消息 + +顺序消息仅支持使用 MessageType 为 FIFO 的主题,即顺序消息只能发送至类型为顺序消息的主题中,发送的消息的类型必须和主题的类型一致。和普通消息发送相比,顺序消息发送必须要设置消息组。(推荐实现 MessageQueueSelector 的方式,见下文)。要保证消息的顺序性需要单一生产者串行发送。 + +单线程使用 MessageListenerConcurrently 可以顺序消费,多线程环境下使用 MessageListenerOrderly 才能顺序消费。 + +#### 事务消息 + +事务消息是 Apache RocketMQ 提供的一种高级消息类型,支持在分布式场景下保障消息生产和本地事务的最终一致性。简单来讲,就是将本地事务(数据库的 DML 操作)与发送消息合并在同一个事务中。例如,新增一个订单。在事务未提交之前,不发送订阅的消息。发送消息的动作随着事务的成功提交而发送,随着事务的回滚而取消。当然真正地处理过程不止这么简单,包含了半消息、事务监听和事务回查等概念,下面有更详细的说明。 + +## 关于发送消息 + +### **不建议单一进程创建大量生产者** + +Apache RocketMQ 的生产者和主题是多对多的关系,支持同一个生产者向多个主题发送消息。对于生产者的创建和初始化,建议遵循够用即可、最大化复用原则,如果有需要发送消息到多个主题的场景,无需为每个主题都创建一个生产者。 + +### **不建议频繁创建和销毁生产者** + +Apache RocketMQ 的生产者是可以重复利用的底层资源,类似数据库的连接池。因此不需要在每次发送消息时动态创建生产者,且在发送结束后销毁生产者。这样频繁的创建销毁会在服务端产生大量短连接请求,严重影响系统性能。 + +正确示例: + +```java +Producer p = ProducerBuilder.build(); +for (int i =0;i messageViewList = simpleConsumer.receive(10, Duration.ofSeconds(30)); + messageViewList.forEach(messageView -> { + System.out.println(messageView); + // 消费处理完成后,需要主动调用 ACK 提交消费结果。 + try { + simpleConsumer.ack(messageView); + } catch (ClientException e) { + logger.error("Failed to ack message, messageId={}", messageView.getMessageId(), e); + } + }); +} catch (ClientException e) { + // 如果遇到系统流控等原因造成拉取失败,需要重新发起获取消息请求。 + logger.error("Failed to receive message", e); +} +``` + +SimpleConsumer 适用于以下场景: + +- 消息处理时长不可控:如果消息处理时长无法预估,经常有长时间耗时的消息处理情况。建议使用 SimpleConsumer 消费类型,可以在消费时自定义消息的预估处理时长,若实际业务中预估的消息处理时长不符合预期,也可以通过接口提前修改。 +- 需要异步化、批量消费等高级定制场景:SimpleConsumer 在 SDK 内部没有复杂的线程封装,完全由业务逻辑自由定制,可以实现异步分发、批量消费等高级定制场景。 +- 需要自定义消费速率:SimpleConsumer 是由业务逻辑主动调用接口获取消息,因此可以自由调整获取消息的频率,自定义控制消费速率。 + +### PullConsumer + +施工中。。。 + +## 消费者分组和生产者分组 + +### 生产者分组 + +RocketMQ 服务端 5.x 版本开始,**生产者是匿名的**,无需管理生产者分组(ProducerGroup);对于历史版本服务端 3.x 和 4.x 版本,已经使用的生产者分组可以废弃无需再设置,且不会对当前业务产生影响。 + +### 消费者分组 + +消费者分组是多个消费行为一致的消费者的负载均衡分组。消费者分组不是具体实体而是一个逻辑资源。通过消费者分组实现消费性能的水平扩展以及高可用容灾。 + +消费者分组中的订阅关系、投递顺序性、消费重试策略是一致的。 + +- 订阅关系:Apache RocketMQ 以消费者分组的粒度管理订阅关系,实现订阅关系的管理和追溯。 +- 投递顺序性:Apache RocketMQ 的服务端将消息投递给消费者消费时,支持顺序投递和并发投递,投递方式在消费者分组中统一配置。 +- 消费重试策略: 消费者消费消息失败时的重试策略,包括重试次数、死信队列设置等。 + +RocketMQ 服务端 5.x 版本:上述消费者的消费行为从关联的消费者分组中统一获取,因此,同一分组内所有消费者的消费行为必然是一致的,客户端无需关注。 + +RocketMQ 服务端 3.x/4.x 历史版本:上述消费逻辑由消费者客户端接口定义,因此,您需要自己在消费者客户端设置时保证同一分组下的消费者的消费行为一致。(来自官方网站) + ## 如何解决顺序消费和重复消费? 其实,这些东西都是我在介绍消息队列带来的一些副作用的时候提到的,也就是说,这些问题不仅仅挂钩于 `RocketMQ` ,而是应该每个消息中间件都需要去解决的。 @@ -294,12 +457,55 @@ tag: 那么,我们现在使用了 **普通顺序模式** ,我们从上面学习知道了在 `Producer` 生产消息的时候会进行轮询(取决你的负载均衡策略)来向同一主题的不同消息队列发送消息。那么如果此时我有几个消息分别是同一个订单的创建、支付、发货,在轮询的策略下这 **三个消息会被发送到不同队列** ,因为在不同的队列此时就无法使用 `RocketMQ` 带来的队列有序特性来保证消息有序性了。 -![](https://my-blog-to-use.oss-cn-beijing.aliyuncs.com/2019-11/16ef3874585e096e.jpg) +![](https://oss.javaguide.cn/github/javaguide/high-performance/message-queue/16ef3874585e096e.jpg) 那么,怎么解决呢? 其实很简单,我们需要处理的仅仅是将同一语义下的消息放入同一个队列(比如这里是同一个订单),那我们就可以使用 **Hash 取模法** 来保证同一个订单在同一个队列中就行了。 +RocketMQ 实现了两种队列选择算法,也可以自己实现 + +- 轮询算法 + + - 轮询算法就是向消息指定的 topic 所在队列中依次发送消息,保证消息均匀分布 + - 是 RocketMQ 默认队列选择算法 + +- 最小投递延迟算法 + + - 每次消息投递的时候统计消息投递的延迟,选择队列时优先选择消息延时小的队列,导致消息分布不均匀,按照如下设置即可。 + + - ```java + producer.setSendLatencyFaultEnable(true); + ``` + +- 继承 MessageQueueSelector 实现 + + - ```java + SendResult sendResult = producer.send(msg, new MessageQueueSelector() { + @Override + public MessageQueue select(List mqs, Message msg, Object arg) { + //从mqs中选择一个队列,可以根据msg特点选择 + return null; + } + }, new Object()); + ``` + +### 特殊情况处理 + +#### 发送异常 + +选择队列后会与 Broker 建立连接,通过网络请求将消息发送到 Broker 上,如果 Broker 挂了或者网络波动发送消息超时此时 RocketMQ 会进行重试。 + +重新选择其他 Broker 中的消息队列进行发送,默认重试两次,可以手动设置。 + +```java +producer.setRetryTimesWhenSendFailed(5); +``` + +#### 消息过大 + +消息超过 4k 时 RocketMQ 会将消息压缩后在发送到 Broker 上,减少网络资源的占用。 + ### 重复消费 emmm,就两个字—— **幂等** 。在编程中一个*幂等* 操作的特点是其任意多次执行所产生的影响均与一次执行的影响相同。比如说,这个时候我们有一个订单的处理积分的系统,每当来一个消息的时候它就负责为创建这个订单的用户的积分加上相应的数值。可是有一次,消息队列发送给订单系统 FrancisQ 的订单信息,其要求是给 FrancisQ 的积分加上 500。但是积分系统在收到 FrancisQ 的订单信息处理完成之后返回给消息队列处理成功的信息的时候出现了网络波动(当然还有很多种情况,比如 Broker 意外重启等等),这条回应没有发送成功。 @@ -324,7 +530,7 @@ emmm,就两个字—— **幂等** 。在编程中一个*幂等* 操作的特 在 `RocketMQ` 中使用的是 **事务消息加上事务反查机制** 来解决分布式事务问题的。我画了张图,大家可以对照着图进行理解。 -![](https://my-blog-to-use.oss-cn-beijing.aliyuncs.com/2019-11/16ef38798d7a987f.png) +![](https://oss.javaguide.cn/github/javaguide/high-performance/message-queue/16ef38798d7a987f.png) 在第一步发送的 half 消息 ,它的意思是 **在事务提交之前,对于消费者来说,这个消息是不可见的** 。 @@ -334,6 +540,216 @@ emmm,就两个字—— **幂等** 。在编程中一个*幂等* 操作的特 你还需要注意的是,在 `MQ Server` 指向系统 B 的操作已经和系统 A 不相关了,也就是说在消息队列中的分布式事务是——**本地事务和存储消息到消息队列才是同一个事务**。这样也就产生了事务的**最终一致性**,因为整个过程是异步的,**每个系统只要保证它自己那一部分的事务就行了**。 +实践中会遇到的问题:事务消息需要一个事务监听器来监听本地事务是否成功,并且事务监听器接口只允许被实现一次。那就意味着需要把各种事务消息的本地事务都写在一个接口方法里面,必将会产生大量的耦合和类型判断。采用函数 Function 接口来包装整个业务过程,作为一个参数传递到监听器的接口方法中。再调用 Function 的 apply() 方法来执行业务,事务也会在 apply() 方法中执行。让监听器与业务之间实现解耦,使之具备了真实生产环境中的可行性。 + +1.模拟一个添加用户浏览记录的需求 + +```java +@PostMapping("/add") +@ApiOperation("添加用户浏览记录") +public Result add(Long userId, Long forecastLogId) { + + // 函数式编程:浏览记录入库 + Function function = transactionId -> viewHistoryHandler.addViewHistory(transactionId, userId, forecastLogId); + + Map hashMap = new HashMap<>(); + hashMap.put("userId", userId); + hashMap.put("forecastLogId", forecastLogId); + String jsonString = JSON.toJSONString(hashMap); + + // 发送事务消息;将本地的事务操作,用函数Function接口接收,作为一个参数传入到方法中 + TransactionSendResult transactionSendResult = mqProducerService.sendTransactionMessage(jsonString, MQDestination.TAG_ADD_VIEW_HISTORY, function); + return Result.success(transactionSendResult); +} +``` + +2.发送事务消息的方法 + +```java +/** + * 发送事务消息 + * + * @param msgBody + * @param tag + * @param function + * @return + */ +public TransactionSendResult sendTransactionMessage(String msgBody, String tag, Function function) { + // 构建消息体 + Message message = buildMessage(msgBody); + + // 构建消息投递信息 + String destination = buildDestination(tag); + + TransactionSendResult result = rocketMQTemplate.sendMessageInTransaction(destination, message, function); + return result; +} +``` + +3.生产者消息监听器,只允许一个类去实现该监听器 + +```java +@Slf4j +@RocketMQTransactionListener +public class TransactionMsgListener implements RocketMQLocalTransactionListener { + + @Autowired + private RedisService redisService; + + /** + * 执行本地事务(在发送消息成功时执行) + * + * @param message + * @param o + * @return commit or rollback or unknown + */ + @Override + public RocketMQLocalTransactionState executeLocalTransaction(Message message, Object o) { + + // 1、获取事务ID + String transactionId = null; + try { + transactionId = message.getHeaders().get("rocketmq_TRANSACTION_ID").toString(); + // 2、判断传入函数对象是否为空,如果为空代表没有要执行的业务直接抛弃消息 + if (o == null) { + //返回ROLLBACK状态的消息会被丢弃 + log.info("事务消息回滚,没有需要处理的业务 transactionId={}", transactionId); + return RocketMQLocalTransactionState.ROLLBACK; + } + // 将Object o转换成Function对象 + Function function = (Function) o; + // 执行业务 事务也会在function.apply中执行 + Boolean apply = function.apply(transactionId); + if (apply) { + log.info("事务提交,消息正常处理 transactionId={}", transactionId); + //返回COMMIT状态的消息会立即被消费者消费到 + return RocketMQLocalTransactionState.COMMIT; + } + } catch (Exception e) { + log.info("出现异常 返回ROLLBACK transactionId={}", transactionId); + return RocketMQLocalTransactionState.ROLLBACK; + } + return RocketMQLocalTransactionState.ROLLBACK; + } + + /** + * 事务回查机制,检查本地事务的状态 + * + * @param message + * @return + */ + @Override + public RocketMQLocalTransactionState checkLocalTransaction(Message message) { + + String transactionId = message.getHeaders().get("rocketmq_TRANSACTION_ID").toString(); + + // 查redis + MqTransaction mqTransaction = redisService.getCacheObject("mqTransaction:" + transactionId); + if (Objects.isNull(mqTransaction)) { + return RocketMQLocalTransactionState.ROLLBACK; + } + return RocketMQLocalTransactionState.COMMIT; + } +} +``` + +4.模拟的业务场景,这里的方法必须提取出来,放在别的类里面.如果调用方与被调用方在同一个类中,会发生事务失效的问题. + +```java +@Component +public class ViewHistoryHandler { + + @Autowired + private IViewHistoryService viewHistoryService; + + @Autowired + private IMqTransactionService mqTransactionService; + + @Autowired + private RedisService redisService; + + /** + * 浏览记录入库 + * + * @param transactionId + * @param userId + * @param forecastLogId + * @return + */ + @Transactional + public Boolean addViewHistory(String transactionId, Long userId, Long forecastLogId) { + // 构建浏览记录 + ViewHistory viewHistory = new ViewHistory(); + viewHistory.setUserId(userId); + viewHistory.setForecastLogId(forecastLogId); + viewHistory.setCreateTime(LocalDateTime.now()); + boolean save = viewHistoryService.save(viewHistory); + + // 本地事务信息 + MqTransaction mqTransaction = new MqTransaction(); + mqTransaction.setTransactionId(transactionId); + mqTransaction.setCreateTime(new Date()); + mqTransaction.setStatus(MqTransaction.StatusEnum.VALID.getStatus()); + + // 1.可以把事务信息存数据库 + mqTransactionService.save(mqTransaction); + + // 2.也可以选择存redis,4个小时有效期,'4个小时'是RocketMQ内置的最大回查超时时长,过期未确认将强制回滚 + redisService.setCacheObject("mqTransaction:" + transactionId, mqTransaction, 4L, TimeUnit.HOURS); + + // 放开注释,模拟异常,事务回滚 + // int i = 10 / 0; + + return save; + } +} +``` + +5.消费消息,以及幂等处理 + +```java +@Service +@RocketMQMessageListener(topic = MQDestination.TOPIC, selectorExpression = MQDestination.TAG_ADD_VIEW_HISTORY, consumerGroup = MQDestination.TAG_ADD_VIEW_HISTORY) +public class ConsumerAddViewHistory implements RocketMQListener { + // 监听到消息就会执行此方法 + @Override + public void onMessage(Message message) { + // 幂等校验 + String transactionId = message.getTransactionId(); + + // 查redis + MqTransaction mqTransaction = redisService.getCacheObject("mqTransaction:" + transactionId); + + // 不存在事务记录 + if (Objects.isNull(mqTransaction)) { + return; + } + + // 已消费 + if (Objects.equals(mqTransaction.getStatus(), MqTransaction.StatusEnum.CONSUMED.getStatus())) { + return; + } + + String msg = new String(message.getBody()); + Map map = JSON.parseObject(msg, new TypeReference>() { + }); + Long userId = map.get("userId"); + Long forecastLogId = map.get("forecastLogId"); + + // 下游的业务处理 + // TODO 记录用户喜好,更新用户画像 + + // TODO 更新'证券预测文章'的浏览量,重新计算文章的曝光排序 + + // 更新状态为已消费 + mqTransaction.setUpdateTime(new Date()); + mqTransaction.setStatus(MqTransaction.StatusEnum.CONSUMED.getStatus()); + redisService.setCacheObject("mqTransaction:" + transactionId, mqTransaction, 4L, TimeUnit.HOURS); + log.info("监听到消息:msg={}", JSON.toJSONString(map)); + } +} +``` + ## 如何解决消息堆积问题? 在上面我们提到了消息队列一个很重要的功能——**削峰** 。那么如果这个峰值太大了导致消息堆积在队列中怎么办呢? @@ -346,14 +762,74 @@ emmm,就两个字—— **幂等** 。在编程中一个*幂等* 操作的特 > > 别忘了在 `RocketMQ` 中,**一个队列只会被一个消费者消费** ,如果你仅仅是增加消费者实例就会出现我一开始给你画架构图的那种情况。 -![](https://my-blog-to-use.oss-cn-beijing.aliyuncs.com/2019-11/16ef387d939ab66d.jpg) +![](https://oss.javaguide.cn/github/javaguide/high-performance/message-queue/16ef387d939ab66d.jpg) -## 什么事回溯消费? +## 什么是回溯消费? 回溯消费是指 `Consumer` 已经消费成功的消息,由于业务上需求需要重新消费,在`RocketMQ` 中, `Broker` 在向`Consumer` 投递成功消息后,**消息仍然需要保留** 。并且重新消费一般是按照时间维度,例如由于 `Consumer` 系统故障,恢复后需要重新消费 1 小时前的数据,那么 `Broker` 要提供一种机制,可以按照时间维度来回退消费进度。`RocketMQ` 支持按照时间回溯消费,时间维度精确到毫秒。 这是官方文档的解释,我直接照搬过来就当科普了 😁😁😁。 +## RocketMQ 如何保证高性能读写 + +### 传统 IO 方式 + +![](https://oss.javaguide.cn/github/javaguide/high-performance/message-queue/31699457085_.pic.jpg) + +传统的 IO 读写其实就是 read + write 的操作,整个过程会分为如下几步 + +- 用户调用 read()方法,开始读取数据,此时发生一次上下文从用户态到内核态的切换,也就是图示的切换 1 +- 将磁盘数据通过 DMA 拷贝到内核缓存区 +- 将内核缓存区的数据拷贝到用户缓冲区,这样用户,也就是我们写的代码就能拿到文件的数据 +- read()方法返回,此时就会从内核态切换到用户态,也就是图示的切换 2 +- 当我们拿到数据之后,就可以调用 write()方法,此时上下文会从用户态切换到内核态,即图示切换 3 +- CPU 将用户缓冲区的数据拷贝到 Socket 缓冲区 +- 将 Socket 缓冲区数据拷贝至网卡 +- write()方法返回,上下文重新从内核态切换到用户态,即图示切换 4 + +整个过程发生了 4 次上下文切换和 4 次数据的拷贝,这在高并发场景下肯定会严重影响读写性能故引入了零拷贝技术 + +### 零拷贝技术 + +#### mmap + +mmap(memory map)是一种内存映射文件的方法,即将一个文件或者其它对象映射到进程的地址空间,实现文件磁盘地址和进程虚拟地址空间中一段虚拟地址的一一对映关系。 + +简单地说就是内核缓冲区和应用缓冲区共享,从而减少了从读缓冲区到用户缓冲区的一次 CPU 拷贝。基于此上述架构图可变为: + +![](https://oss.javaguide.cn/github/javaguide/high-performance/message-queue/41699457086_.pic.jpg) + +基于 mmap IO 读写其实就变成 mmap + write 的操作,也就是用 mmap 替代传统 IO 中的 read 操作。 + +当用户发起 mmap 调用的时候会发生上下文切换 1,进行内存映射,然后数据被拷贝到内核缓冲区,mmap 返回,发生上下文切换 2;随后用户调用 write,发生上下文切换 3,将内核缓冲区的数据拷贝到 Socket 缓冲区,write 返回,发生上下文切换 4。 + +发生 4 次上下文切换和 3 次 IO 拷贝操作,在 Java 中的实现: + +```java +FileChannel fileChannel = new RandomAccessFile("test.txt", "rw").getChannel(); +MappedByteBuffer mappedByteBuffer = fileChannel.map(FileChannel.MapMode.READ_WRITE, 0, fileChannel.size()); +``` + +#### sendfile + +sendfile()跟 mmap()一样,也会减少一次 CPU 拷贝,但是它同时也会减少两次上下文切换。 + +![](https://oss.javaguide.cn/github/javaguide/high-performance/message-queue/51699457087_.pic.jpg) + +如图,用户在发起 sendfile()调用时会发生切换 1,之后数据通过 DMA 拷贝到内核缓冲区,之后再将内核缓冲区的数据 CPU 拷贝到 Socket 缓冲区,最后拷贝到网卡,sendfile()返回,发生切换 2。发生了 3 次拷贝和两次切换。Java 也提供了相应 api: + +```java +FileChannel channel = FileChannel.open(Paths.get("./test.txt"), StandardOpenOption.WRITE, StandardOpenOption.CREATE); +//调用transferTo方法向目标数据传输 +channel.transferTo(position, len, target); +``` + +在如上代码中,并没有文件的读写操作,而是直接将文件的数据传输到 target 目标缓冲区,也就是说,sendfile 是无法知道文件的具体的数据的;但是 mmap 不一样,他是可以修改内核缓冲区的数据的。假设如果需要对文件的内容进行修改之后再传输,只有 mmap 可以满足。 + +通过上面的一些介绍,结论是基于零拷贝技术,可以减少 CPU 的拷贝次数和上下文切换次数,从而可以实现文件高效的读写操作。 + +RocketMQ 内部主要是使用基于 mmap 实现的零拷贝(其实就是调用上述提到的 api),用来读写文件,这也是 RocketMQ 为什么快的一个很重要原因。 + ## RocketMQ 的刷盘机制 上面我讲了那么多的 `RocketMQ` 的架构和设计原理,你有没有好奇 @@ -368,7 +844,7 @@ emmm,就两个字—— **幂等** 。在编程中一个*幂等* 操作的特 ### 同步刷盘和异步刷盘 -![](https://my-blog-to-use.oss-cn-beijing.aliyuncs.com/2019-11/16ef387fba311cda.jpg) +![](https://oss.javaguide.cn/github/javaguide/high-performance/message-queue/16ef387fba311cda-20230814005009889.jpg) 如上图所示,在同步刷盘中需要等待一个刷盘成功的 `ACK` ,同步刷盘对 `MQ` 消息可靠性来说是一种不错的保障,但是 **性能上会有较大影响** ,一般地适用于金融等特定业务场景。 @@ -393,7 +869,7 @@ emmm,就两个字—— **幂等** 。在编程中一个*幂等* 操作的特 在单主从架构中,如果一个主节点挂掉了,那么也就意味着整个系统不能再生产了。那么这个可用性的问题能否解决呢?**一个主从不行那就多个主从的呗**,别忘了在我们最初的架构图中,每个 `Topic` 是分布在不同 `Broker` 中的。 -![](https://my-blog-to-use.oss-cn-beijing.aliyuncs.com/2019-11/16ef38687488a5a4.jpg) +![](https://oss.javaguide.cn/github/javaguide/high-performance/message-queue/16ef38687488a5asadasfg4.jpg) 但是这种复制方式同样也会带来一个问题,那就是无法保证 **严格顺序** 。在上文中我们提到了如何保证的消息顺序性是通过将一个语义的消息发送在同一个队列中,使用 `Topic` 下的队列来保证顺序性的。如果此时我们主节点 A 负责的是订单 A 的一系列语义消息,然后它挂了,这样其他节点是无法代替主节点 A 的,如果我们任意节点都可以存入任何消息,那就没有顺序性可言了。 @@ -413,17 +889,17 @@ emmm,就两个字—— **幂等** 。在编程中一个*幂等* 操作的特 总结来说,整个消息存储的结构,最主要的就是 `CommitLoq` 和 `ConsumeQueue` 。而 `ConsumeQueue` 你可以大概理解为 `Topic` 中的队列。 -![](https://my-blog-to-use.oss-cn-beijing.aliyuncs.com/2019-11/16ef3884c02acc72.png) +![](https://oss.javaguide.cn/github/javaguide/high-performance/message-queue/16ef3884c02acc72.png) -`RocketMQ` 采用的是 **混合型的存储结构** ,即为 `Broker` 单个实例下所有的队列共用一个日志数据文件来存储消息。有意思的是在同样高并发的 `Kafka` 中会为每个 `Topic` 分配一个存储文件。这就有点类似于我们有一大堆书需要装上书架,`RockeMQ` 是不分书的种类直接成批的塞上去的,而 `Kafka` 是将书本放入指定的分类区域的。 +`RocketMQ` 采用的是 **混合型的存储结构** ,即为 `Broker` 单个实例下所有的队列共用一个日志数据文件来存储消息。有意思的是在同样高并发的 `Kafka` 中会为每个 `Topic` 分配一个存储文件。这就有点类似于我们有一大堆书需要装上书架,`RocketMQ` 是不分书的种类直接成批的塞上去的,而 `Kafka` 是将书本放入指定的分类区域的。 而 `RocketMQ` 为什么要这么做呢?原因是 **提高数据的写入效率** ,不分 `Topic` 意味着我们有更大的几率获取 **成批** 的消息进行数据写入,但也会带来一个麻烦就是读取消息的时候需要遍历整个大文件,这是非常耗时的。 所以,在 `RocketMQ` 中又使用了 `ConsumeQueue` 作为每个队列的索引文件来 **提升读取消息的效率**。我们可以直接根据队列的消息序号,计算出索引的全局位置(索引序号\*索引固定⻓度 20),然后直接读取这条索引,再根据索引中记录的消息的全局位置,找到消息。 -讲到这里,你可能对 `RockeMQ` 的存储架构还有些模糊,没事,我们结合着图来理解一下。 +讲到这里,你可能对 `RocketMQ` 的存储架构还有些模糊,没事,我们结合着图来理解一下。 -![](https://my-blog-to-use.oss-cn-beijing.aliyuncs.com/2019-11/16ef388763c25c62.jpg) +![](https://oss.javaguide.cn/github/javaguide/high-performance/message-queue/16ef388763c25c62.jpg) emmm,是不是有一点复杂 🤣,看英文图片和英文文档的时候就不要怂,硬着头皮往下看就行。 @@ -437,8 +913,6 @@ emmm,是不是有一点复杂 🤣,看英文图片和英文文档的时候 因为有一个知识点因为写嗨了忘讲了,想想在哪里加也不好,所以我留给大家去思考 🤔🤔 一下吧。 -![](https://my-blog-to-use.oss-cn-beijing.aliyuncs.com/2019-11/e314ee45gy1g05zgr67bbj20gp0b3aba.jpg) - 为什么 `CommitLog` 文件要设计成固定大小的长度呢?提醒:**内存映射机制**。 ## 总结 @@ -451,8 +925,10 @@ emmm,是不是有一点复杂 🤣,看英文图片和英文文档的时候 2. 消息队列的作用(异步,解耦,削峰) 3. 消息队列带来的一系列问题(消息堆积、重复消费、顺序消费、分布式事务等等) 4. 消息队列的两种消息模型——队列和主题模式 -5. 分析了 `RocketMQ` 的技术架构(`NameServer`、`Broker`、`Producer`、`Comsumer`) +5. 分析了 `RocketMQ` 的技术架构(`NameServer`、`Broker`、`Producer`、`Consumer`) 6. 结合 `RocketMQ` 回答了消息队列副作用的解决方案 7. 介绍了 `RocketMQ` 的存储机制和刷盘策略。 等等。。。 + + diff --git a/docs/high-performance/read-and-write-separation-and-library-subtable.md b/docs/high-performance/read-and-write-separation-and-library-subtable.md index 6847b6ad5c8..da25f066e9e 100644 --- a/docs/high-performance/read-and-write-separation-and-library-subtable.md +++ b/docs/high-performance/read-and-write-separation-and-library-subtable.md @@ -1,5 +1,5 @@ --- -title: 读写分离和分库分表常见问题总结 +title: 读写分离和分库分表详解 category: 高性能 head: - - meta @@ -22,34 +22,6 @@ head: 一般情况下,我们都会选择一主多从,也就是一台主数据库负责写,其他的从数据库负责读。主库和从库之间会进行数据同步,以保证从库中数据的准确性。这样的架构实现起来比较简单,并且也符合系统的写少读多的特点。 -### 读写分离会带来什么问题?如何解决? - -读写分离对于提升数据库的并发非常有效,但是,同时也会引来一个问题:主库和从库的数据存在延迟,比如你写完主库之后,主库的数据同步到从库是需要时间的,这个时间差就导致了主库和从库的数据不一致性问题。这也就是我们经常说的 **主从同步延迟** 。 - -主从同步延迟问题的解决,没有特别好的一种方案(可能是我太菜了,欢迎评论区补充)。你可以根据自己的业务场景,参考一下下面几种解决办法。 - -**1.强制将读请求路由到主库处理。** - -既然你从库的数据过期了,那我就直接从主库读取嘛!这种方案虽然会增加主库的压力,但是,实现起来比较简单,也是我了解到的使用最多的一种方式。 - -比如 `Sharding-JDBC` 就是采用的这种方案。通过使用 Sharding-JDBC 的 `HintManager` 分片键值管理器,我们可以强制使用主库。 - -```java -HintManager hintManager = HintManager.getInstance(); -hintManager.setMasterRouteOnly(); -// 继续JDBC操作 -``` - -对于这种方案,你可以将那些必须获取最新数据的读请求都交给主库处理。 - -**2.延迟读取。** - -还有一些朋友肯定会想既然主从同步存在延迟,那我就在延迟之后读取啊,比如主从同步延迟 0.5s,那我就 1s 之后再读取数据。这样多方便啊!方便是方便,但是也很扯淡。 - -不过,如果你是这样设计业务流程就会好很多:对于一些对数据比较敏感的场景,你可以在完成写请求之后,避免立即进行请求操作。比如你支付成功之后,跳转到一个支付成功的页面,当你点击返回之后才返回自己的账户。 - -另外,[《MySQL 实战 45 讲》](https://time.geekbang.org/column/intro/100020801?code=ieY8HeRSlDsFbuRtggbBQGxdTh-1jMASqEIeqzHAKrI%3D)这个专栏中的[《读写分离有哪些坑?》](https://time.geekbang.org/column/article/77636)这篇文章还介绍了很多其他比较实际的解决办法,感兴趣的小伙伴可以自行研究一下。 - ### 如何实现读写分离? 不论是使用哪一种读写分离具体的实现方案,想要实现读写分离一般包含如下几步: @@ -66,7 +38,9 @@ hintManager.setMasterRouteOnly(); 我们可以在应用和数据中间加了一个代理层。应用程序所有的数据请求都交给代理层处理,代理层负责分离读写请求,将它们路由到对应的数据库中。 -提供类似功能的中间件有 **MySQL Router**(官方)、**Atlas**(基于 MySQL Proxy)、**MaxScale**、**MyCat**。 +提供类似功能的中间件有 **MySQL Router**(官方, MySQL Proxy 的替代方案)、**Atlas**(基于 MySQL Proxy)、**MaxScale**、**MyCat**。 + +关于 MySQL Router 多提一点:在 MySQL 8.2 的版本中,MySQL Router 能自动分辨对数据库读写/操作并把这些操作路由到正确的实例上。这是一项有价值的功能,可以优化数据库性能和可扩展性,而无需在应用程序中进行任何更改。具体介绍可以参考官方博客:[MySQL 8.2 – transparent read/write splitting](https://blogs.oracle.com/mysql/post/mysql-82-transparent-readwrite-splitting)。 **2. 组件方式** @@ -89,7 +63,7 @@ MySQL binlog(binary log 即二进制日志文件) 主要记录了 MySQL 数据 3. 从库会创建一个 I/O 线程向主库请求更新的 binlog 4. 主库会创建一个 binlog dump 线程来发送 binlog ,从库中的 I/O 线程负责接收 5. 从库的 I/O 线程将接收的 binlog 写入到 relay log 中。 -6. 从库的 SQL 线程读取 relay log 同步数据本地(也就是再执行一遍 SQL )。 +6. 从库的 SQL 线程读取 relay log 同步数据到本地(也就是再执行一遍 SQL )。 怎么样?看了我对主从复制这个过程的讲解,你应该搞明白了吧! @@ -105,6 +79,75 @@ MySQL binlog(binary log 即二进制日志文件) 主要记录了 MySQL 数据 **MySQL 主从复制是依赖于 binlog 。另外,常见的一些同步 MySQL 数据到其他数据源的工具(比如 canal)的底层一般也是依赖 binlog 。** +### 如何避免主从延迟? + +读写分离对于提升数据库的并发非常有效,但是,同时也会引来一个问题:主库和从库的数据存在延迟,比如你写完主库之后,主库的数据同步到从库是需要时间的,这个时间差就导致了主库和从库的数据不一致性问题。这也就是我们经常说的 **主从同步延迟** 。 + +如果我们的业务场景无法容忍主从同步延迟的话,应该如何避免呢(注意:我这里说的是避免而不是减少延迟)? + +这里提供两种我知道的方案(能力有限,欢迎补充),你可以根据自己的业务场景参考一下。 + +#### 强制将读请求路由到主库处理 + +既然你从库的数据过期了,那我就直接从主库读取嘛!这种方案虽然会增加主库的压力,但是,实现起来比较简单,也是我了解到的使用最多的一种方式。 + +比如 `Sharding-JDBC` 就是采用的这种方案。通过使用 Sharding-JDBC 的 `HintManager` 分片键值管理器,我们可以强制使用主库。 + +```java +HintManager hintManager = HintManager.getInstance(); +hintManager.setMasterRouteOnly(); +// 继续JDBC操作 +``` + +对于这种方案,你可以将那些必须获取最新数据的读请求都交给主库处理。 + +#### 延迟读取 + +还有一些朋友肯定会想既然主从同步存在延迟,那我就在延迟之后读取啊,比如主从同步延迟 0.5s,那我就 1s 之后再读取数据。这样多方便啊!方便是方便,但是也很扯淡。 + +不过,如果你是这样设计业务流程就会好很多:对于一些对数据比较敏感的场景,你可以在完成写请求之后,避免立即进行请求操作。比如你支付成功之后,跳转到一个支付成功的页面,当你点击返回之后才返回自己的账户。 + +#### 总结 + +关于如何避免主从延迟,我们这里介绍了两种方案。实际上,延迟读取这种方案没办法完全避免主从延迟,只能说可以减少出现延迟的概率而已,实际项目中一般不会使用。 + +总的来说,要想不出现延迟问题,一般还是要强制将那些必须获取最新数据的读请求都交给主库处理。如果你的项目的大部分业务场景对数据准确性要求不是那么高的话,这种方案还是可以选择的。 + +### 什么情况下会出现主从延迟?如何尽量减少延迟? + +我们在上面的内容中也提到了主从延迟以及避免主从延迟的方法,这里我们再来详细分析一下主从延迟出现的原因以及应该如何尽量减少主从延迟。 + +要搞懂什么情况下会出现主从延迟,我们需要先搞懂什么是主从延迟。 + +MySQL 主从同步延时是指从库的数据落后于主库的数据,这种情况可能由以下两个原因造成: + +1. 从库 I/O 线程接收 binlog 的速度跟不上主库写入 binlog 的速度,导致从库 relay log 的数据滞后于主库 binlog 的数据; +2. 从库 SQL 线程执行 relay log 的速度跟不上从库 I/O 线程接收 binlog 的速度,导致从库的数据滞后于从库 relay log 的数据。 + +与主从同步有关的时间点主要有 3 个: + +1. 主库执行完一个事务,写入 binlog,将这个时刻记为 T1; +2. 从库 I/O 线程接收到 binlog 并写入 relay log 的时刻记为 T2; +3. 从库 SQL 线程读取 relay log 同步数据本地的时刻记为 T3。 + +结合我们上面讲到的主从复制原理,可以得出: + +- T2 和 T1 的差值反映了从库 I/O 线程的性能和网络传输的效率,这个差值越小说明从库 I/O 线程的性能和网络传输效率越高。 +- T3 和 T2 的差值反映了从库 SQL 线程执行的速度,这个差值越小,说明从库 SQL 线程执行速度越快。 + +那什么情况下会出现出从延迟呢?这里列举几种常见的情况: + +1. **从库机器性能比主库差**:从库接收 binlog 并写入 relay log 以及执行 SQL 语句的速度会比较慢(也就是 T2-T1 和 T3-T2 的值会较大),进而导致延迟。解决方法是选择与主库一样规格或更高规格的机器作为从库,或者对从库进行性能优化,比如调整参数、增加缓存、使用 SSD 等。 +2. **从库处理的读请求过多**:从库需要执行主库的所有写操作,同时还要响应读请求,如果读请求过多,会占用从库的 CPU、内存、网络等资源,影响从库的复制效率(也就是 T2-T1 和 T3-T2 的值会较大,和前一种情况类似)。解决方法是引入缓存(推荐)、使用一主多从的架构,将读请求分散到不同的从库,或者使用其他系统来提供查询的能力,比如将 binlog 接入到 Hadoop、Elasticsearch 等系统中。 +3. **大事务**:运行时间比较长,长时间未提交的事务就可以称为大事务。由于大事务执行时间长,并且从库上的大事务会比主库上的大事务花费更多的时间和资源,因此非常容易造成主从延迟。解决办法是避免大批量修改数据,尽量分批进行。类似的情况还有执行时间较长的慢 SQL ,实际项目遇到慢 SQL 应该进行优化。 +4. **从库太多**:主库需要将 binlog 同步到所有的从库,如果从库数量太多,会增加同步的时间和开销(也就是 T2-T1 的值会比较大,但这里是因为主库同步压力大导致的)。解决方案是减少从库的数量,或者将从库分为不同的层级,让上层的从库再同步给下层的从库,减少主库的压力。 +5. **网络延迟**:如果主从之间的网络传输速度慢,或者出现丢包、抖动等问题,那么就会影响 binlog 的传输效率,导致从库延迟。解决方法是优化网络环境,比如提升带宽、降低延迟、增加稳定性等。 +6. **单线程复制**:MySQL5.5 及之前,只支持单线程复制。为了优化复制性能,MySQL 5.6 引入了 **多线程复制**,MySQL 5.7 还进一步完善了多线程复制。 +7. **复制模式**:MySQL 默认的复制是异步的,必然会存在延迟问题。全同步复制不存在延迟问题,但性能太差了。半同步复制是一种折中方案,相对于异步复制,半同步复制提高了数据的安全性,减少了主从延迟(还是有一定程度的延迟)。MySQL 5.5 开始,MySQL 以插件的形式支持 **semi-sync 半同步复制**。并且,MySQL 5.7 引入了 **增强半同步复制** 。 +8. …… + +[《MySQL 实战 45 讲》](https://time.geekbang.org/column/intro/100020801?code=ieY8HeRSlDsFbuRtggbBQGxdTh-1jMASqEIeqzHAKrI%3D)这个专栏中的[读写分离有哪些坑?](https://time.geekbang.org/column/article/77636)这篇文章也有对主从延迟解决方案这一话题进行探讨,感兴趣的可以阅读学习一下。 + ## 分库分表 读写分离主要应对的是数据库读并发,没有解决数据库存储问题。试想一下:**如果 MySQL 一张表的数据量过大怎么办?** @@ -151,17 +194,36 @@ MySQL binlog(binary log 即二进制日志文件) 主要记录了 MySQL 数据 - 单表的数据达到千万级别以上,数据库读写速度比较缓慢。 - 数据库中的数据占用的空间越来越大,备份时间越来越长。 -- 应用的并发量太大。 +- 应用的并发量太大(应该优先考虑其他性能优化方法,而非分库分表)。 + +不过,分库分表的成本太高,如非必要尽量不要采用。而且,并不一定是单表千万级数据量就要分表,毕竟每张表包含的字段不同,它们在不错的性能下能够存放的数据量也不同,还是要具体情况具体分析。 + +之前看过一篇文章分析 “[InnoDB 中高度为 3 的 B+ 树最多可以存多少数据](https://juejin.cn/post/7165689453124517896)”,写的挺不错,感兴趣的可以看看。 ### 常见的分片算法有哪些? 分片算法主要解决了数据被水平分片之后,数据究竟该存放在哪个表的问题。 -- **哈希分片**:求指定 key(比如 id) 的哈希,然后根据哈希值确定数据应被放置在哪个表中。哈希分片比较适合随机读写的场景,不太适合经常需要范围查询的场景。 -- **范围分片**:按照特性的范围区间(比如时间区间、ID 区间)来分配数据,比如 将 `id` 为 `1~299999` 的记录分到第一个库, `300000~599999` 的分到第二个库。范围分片适合需要经常进行范围查找的场景,不太适合随机读写的场景(数据未被分散,容易出现热点数据的问题)。 +常见的分片算法有: + +- **哈希分片**:求指定分片键的哈希,然后根据哈希值确定数据应被放置在哪个表中。哈希分片比较适合随机读写的场景,不太适合经常需要范围查询的场景。哈希分片可以使每个表的数据分布相对均匀,但对动态伸缩(例如新增一个表或者库)不友好。 +- **范围分片**:按照特定的范围区间(比如时间区间、ID 区间)来分配数据,比如 将 `id` 为 `1~299999` 的记录分到第一个表, `300000~599999` 的分到第二个表。范围分片适合需要经常进行范围查找且数据分布均匀的场景,不太适合随机读写的场景(数据未被分散,容易出现热点数据的问题)。 +- **映射表分片**:使用一个单独的表(称为映射表)来存储分片键和分片位置的对应关系。映射表分片策略可以支持任何类型的分片算法,如哈希分片、范围分片等。映射表分片策略是可以灵活地调整分片规则,不需要修改应用程序代码或重新分布数据。不过,这种方式需要维护额外的表,还增加了查询的开销和复杂度。 +- **一致性哈希分片**:将哈希空间组织成一个环形结构,将分片键和节点(数据库或表)都映射到这个环上,然后根据顺时针的规则确定数据或请求应该分配到哪个节点上,解决了传统哈希对动态伸缩不友好的问题。 - **地理位置分片**:很多 NewSQL 数据库都支持地理位置分片算法,也就是根据地理位置(如城市、地域)来分配数据。 -- **融合算法**:灵活组合多种分片算法,比如将哈希分片和范围分片组合。 -- ...... +- **融合算法分片**:灵活组合多种分片算法,比如将哈希分片和范围分片组合。 +- …… + +### 分片键如何选择? + +分片键(Sharding Key)是数据分片的关键字段。分片键的选择非常重要,它关系着数据的分布和查询效率。一般来说,分片键应该具备以下特点: + +- 具有共性,即能够覆盖绝大多数的查询场景,尽量减少单次查询所涉及的分片数量,降低数据库压力; +- 具有离散性,即能够将数据均匀地分散到各个分片上,避免数据倾斜和热点问题; +- 具有稳定性,即分片键的值不会发生变化,避免数据迁移和一致性问题; +- 具有扩展性,即能够支持分片的动态增加和减少,避免数据重新分片的开销。 + +实际项目中,分片键很难满足上面提到的所有特点,需要权衡一下。并且,分片键可以是表中多个字段的组合,例如取用户 ID 后四位作为订单 ID 后缀。 ### 分库分表会带来什么问题呢? @@ -169,24 +231,39 @@ MySQL binlog(binary log 即二进制日志文件) 主要记录了 MySQL 数据 引入分库分表之后,会给系统带来什么挑战呢? -- **join 操作**:同一个数据库中的表分布在了不同的数据库中,导致无法使用 join 操作。这样就导致我们需要手动进行数据的封装,比如你在一个数据库中查询到一个数据之后,再根据这个数据去另外一个数据库中找对应的数据。 -- **事务问题**:同一个数据库中的表分布在了不同的数据库中,如果单个操作涉及到多个数据库,那么数据库自带的事务就无法满足我们的要求了。 -- **分布式 id**:分库之后, 数据遍布在不同服务器上的数据库,数据库的自增主键已经没办法满足生成的主键唯一了。我们如何为不同的数据节点生成全局唯一主键呢?这个时候,我们就需要为我们的系统引入分布式 id 了。 -- ...... +- **join 操作**:同一个数据库中的表分布在了不同的数据库中,导致无法使用 join 操作。这样就导致我们需要手动进行数据的封装,比如你在一个数据库中查询到一个数据之后,再根据这个数据去另外一个数据库中找对应的数据。不过,很多大厂的资深 DBA 都是建议尽量不要使用 join 操作。因为 join 的效率低,并且会对分库分表造成影响。对于需要用到 join 操作的地方,可以采用多次查询业务层进行数据组装的方法。不过,这种方法需要考虑业务上多次查询的事务性的容忍度。 +- **事务问题**:同一个数据库中的表分布在了不同的数据库中,如果单个操作涉及到多个数据库,那么数据库自带的事务就无法满足我们的要求了。这个时候,我们就需要引入分布式事务了。关于分布式事务常见解决方案总结,网站上也有对应的总结: 。 +- **分布式 ID**:分库之后, 数据遍布在不同服务器上的数据库,数据库的自增主键已经没办法满足生成的主键唯一了。我们如何为不同的数据节点生成全局唯一主键呢?这个时候,我们就需要为我们的系统引入分布式 ID 了。关于分布式 ID 的详细介绍&实现方案总结,可以看我写的这篇文章:[分布式 ID 介绍&实现方案总结](https://javaguide.cn/distributed-system/distributed-id.html)。 +- **跨库聚合查询问题**:分库分表会导致常规聚合查询操作,如 group by,order by 等变得异常复杂。这是因为这些操作需要在多个分片上进行数据汇总和排序,而不是在单个数据库上进行。为了实现这些操作,需要编写复杂的业务代码,或者使用中间件来协调分片间的通信和数据传输。这样会增加开发和维护的成本,以及影响查询的性能和可扩展性。 +- …… 另外,引入分库分表之后,一般需要 DBA 的参与,同时还需要更多的数据库服务器,这些都属于成本。 ### 分库分表有没有什么比较推荐的方案? +Apache ShardingSphere 是一款分布式的数据库生态系统, 可以将任意数据库转换为分布式数据库,并通过数据分片、弹性伸缩、加密等能力对原有数据库进行增强。 + ShardingSphere 项目(包括 Sharding-JDBC、Sharding-Proxy 和 Sharding-Sidecar)是当当捐入 Apache 的,目前主要由京东数科的一些巨佬维护。 -![](https://oscimg.oschina.net/oscnet/up-0aa05fa5f54e41a44b09619fc0ee597933c.png) +ShardingSphere 绝对可以说是当前分库分表的首选!ShardingSphere 的功能完善,除了支持读写分离和分库分表,还提供分布式事务、数据库治理、影子库、数据加密和脱敏等功能。 -ShardingSphere 绝对可以说是当前分库分表的首选!ShardingSphere 的功能完善,除了支持读写分离和分库分表,还提供分布式事务、数据库治理等功能。 +ShardingSphere 提供的功能如下: + +![ShardingSphere 提供的功能](https://oss.javaguide.cn/github/javaguide/high-performance/shardingsphere-features.png) + +ShardingSphere 的优势如下(摘自 ShardingSphere 官方文档:): + +- 极致性能:驱动程序端历经长年打磨,效率接近原生 JDBC,性能极致。 +- 生态兼容:代理端支持任何通过 MySQL/PostgreSQL 协议的应用访问,驱动程序端可对接任意实现 JDBC 规范的数据库。 +- 业务零侵入:面对数据库替换场景,ShardingSphere 可满足业务无需改造,实现平滑业务迁移。 +- 运维低成本:在保留原技术栈不变前提下,对 DBA 学习、管理成本低,交互友好。 +- 安全稳定:基于成熟数据库底座之上提供增量能力,兼顾安全性及稳定性。 +- 弹性扩展:具备计算、存储平滑在线扩展能力,可满足业务多变的需求。 +- 开放生态:通过多层次(内核、功能、生态)插件化能力,为用户提供可定制满足自身特殊需求的独有系统。 另外,ShardingSphere 的生态体系完善,社区活跃,文档完善,更新和发布比较频繁。 -艿艿之前写了一篇分库分表的实战文章,各位朋友可以看看:[《芋道 Spring Boot 分库分表入门》](https://mp.weixin.qq.com/s/A2MYOFT7SP-7kGOon8qJaw) 。 +不过,还是要多提一句:**现在很多公司都是用的类似于 TiDB 这种分布式关系型数据库,不需要我们手动进行分库分表(数据库层面已经帮我们做了),也不需要解决手动分库分表引入的各种问题,直接一步到位,内置很多实用的功能(如无感扩容和缩容、冷热存储分离)!如果公司条件允许的话,个人也是比较推荐这种方式!** ### 分库分表后,数据怎么迁移呢? @@ -208,4 +285,7 @@ ShardingSphere 绝对可以说是当前分库分表的首选!ShardingSphere - 读写分离基于主从复制,MySQL 主从复制是依赖于 binlog 。 - **分库** 就是将数据库中的数据分散到不同的数据库上。**分表** 就是对单表的数据进行拆分,可以是垂直拆分,也可以是水平拆分。 - 引入分库分表之后,需要系统解决事务、分布式 id、无法 join 操作问题。 -- ShardingSphere 绝对可以说是当前分库分表的首选!ShardingSphere 的功能完善,除了支持读写分离和分库分表,还提供分布式事务、数据库治理等功能。另外,ShardingSphere 的生态体系完善,社区活跃,文档完善,更新和发布比较频繁。 +- 现在很多公司都是用的类似于 TiDB 这种分布式关系型数据库,不需要我们手动进行分库分表(数据库层面已经帮我们做了),也不需要解决手动分库分表引入的各种问题,直接一步到位,内置很多实用的功能(如无感扩容和缩容、冷热存储分离)!如果公司条件允许的话,个人也是比较推荐这种方式! +- 如果必须要手动分库分表的话,ShardingSphere 是首选!ShardingSphere 的功能完善,除了支持读写分离和分库分表,还提供分布式事务、数据库治理等功能。另外,ShardingSphere 的生态体系完善,社区活跃,文档完善,更新和发布比较频繁。 + + diff --git a/docs/high-performance/sql-optimization.md b/docs/high-performance/sql-optimization.md index ffd444fc3dc..9aa94dfd528 100644 --- a/docs/high-performance/sql-optimization.md +++ b/docs/high-performance/sql-optimization.md @@ -15,3 +15,5 @@ head: ![](https://oss.javaguide.cn/javamianshizhibei/sql-optimization.png) + + diff --git a/docs/high-quality-technical-articles/readme.md b/docs/high-quality-technical-articles/README.md similarity index 76% rename from docs/high-quality-technical-articles/readme.md rename to docs/high-quality-technical-articles/README.md index 4d0917caef7..149ddba7a4f 100644 --- a/docs/high-quality-technical-articles/readme.md +++ b/docs/high-quality-technical-articles/README.md @@ -1,19 +1,18 @@ # 程序人生 -::: tip 这是一则或许对你有用的小广告 -👉 欢迎准备 Java 面试以及学习 Java 的同学加入我的[知识星球](./../about-the-author/zhishixingqiu-two-years.md),干货很多!收费虽然是白菜价,但星球里的内容或许比你参加上万的培训班质量还要高。 + -👉 [《Java 面试指北》](./../zhuanlan/java-mian-shi-zhi-bei.md)持续更新完善中!这是一份教你如何更高效地准备面试的小册,涵盖常见八股文(系统设计、常见框架、分布式、高并发 ......)、优质面经等内容。 -::: - -这里主要会收录一些我看到的和程序员密切相关的非技术类的优质文章,每一篇都值得你阅读 3 遍以上!常看常新! +这里主要会收录一些我看到的或者我自己写的和程序员密切相关的非技术类的优质文章,每一篇都值得你阅读 3 遍以上!常看常新! ## 练级攻略 +- [程序员如何快速学习新技术](./advanced-programmer/programmer-quickly-learn-new-technology.md) - [程序员的技术成长战略](./advanced-programmer/the-growth-strategy-of-the-technological-giant.md) - [十年大厂成长之路](./advanced-programmer/ten-years-of-dachang-growth-road.md) +- [美团三年,总结的 10 条血泪教训](./advanced-programmer/meituan-three-year-summary-lesson-10.md) - [给想成长为高级别开发同学的七条建议](./advanced-programmer/seven-tips-for-becoming-an-advanced-programmer.md) - [糟糕程序员的 20 个坏习惯](./advanced-programmer/20-bad-habits-of-bad-programmers.md) +- [工作五年之后,对技术和业务的思考](./advanced-programmer/thinking-about-technology-and-business-after-five-years-of-work.md) ## 个人经历 @@ -24,6 +23,7 @@ ## 程序员 +- [程序员最该拿的几种高含金量证书](./programmer/high-value-certifications-for-programmers.md) - [程序员怎样出版一本技术书](./programmer/how-do-programmers-publish-a-technical-book.md) - [程序员高效出书避坑和实践指南](./programmer/efficient-book-publishing-and-practice-guide.md) @@ -43,3 +43,5 @@ - [新入职一家公司如何快速进入工作状态](./work/get-into-work-mode-quickly-when-you-join-a-company.md) - [32 条总结教你提升职场经验](./work/32-tips-improving-career.md) - [聊聊大厂的绩效考核](./work/employee-performance.md) + + diff --git a/docs/high-quality-technical-articles/advanced-programmer/20-bad-habits-of-bad-programmers.md b/docs/high-quality-technical-articles/advanced-programmer/20-bad-habits-of-bad-programmers.md index ea47e2965d1..59191347757 100644 --- a/docs/high-quality-technical-articles/advanced-programmer/20-bad-habits-of-bad-programmers.md +++ b/docs/high-quality-technical-articles/advanced-programmer/20-bad-habits-of-bad-programmers.md @@ -8,11 +8,9 @@ tag: > **推荐语**:Kaito 大佬的一篇文章,很实用的建议! > ->
-> -> **原文地址:** https://mp.weixin.qq.com/s/6hUU6SZsxGPWAIIByq93Rw +> **原文地址:** -我想你肯定遇到过这样一类程序员:**他们无论是写代码,还是写文档,又或是和别\*\***人沟通,都显得特别专业\*\*。每次遇到这类人,我都在想,他们到底是怎么做到的? +我想你肯定遇到过这样一类程序员:**他们无论是写代码,还是写文档,又或是和别人沟通,都显得特别专业**。每次遇到这类人,我都在想,他们到底是怎么做到的? 随着工作时间的增长,渐渐地我也总结出一些经验,他们身上都保持着一些看似很微小的优秀习惯,但正是因为这些习惯,体现出了一个优秀程序员的基本素养。 @@ -144,3 +142,5 @@ tag: 优秀程序员的专业技能,我们可能很难在短时间内学会,但这些基本的职业素养,是可以在短期内做到的。 希望你我可以有则改之,无则加勉。 + + diff --git a/docs/high-quality-technical-articles/advanced-programmer/meituan-three-year-summary-lesson-10.md b/docs/high-quality-technical-articles/advanced-programmer/meituan-three-year-summary-lesson-10.md new file mode 100644 index 00000000000..3ae2a5eac42 --- /dev/null +++ b/docs/high-quality-technical-articles/advanced-programmer/meituan-three-year-summary-lesson-10.md @@ -0,0 +1,172 @@ +--- +title: 美团三年,总结的10条血泪教训 +category: 技术文章精选集 +author: CityDreamer部落 +tag: + - 练级攻略 +--- + +> **推荐语**:作者用了很多生动的例子和故事展示了自己在美团的成长和感悟,看了之后受益颇多! +> +> **内容概览**: +> +> 本文的作者提出了以下十条建议,希望能对其他职场人有所启发和帮助: +> +> 1. 结构化思考与表达,提高个人影响力 +> 2. 忘掉职级,该怼就怼,推动事情往前走 +> 3. 用好平台资源,结识优秀的人,学习通识课 +> 4. 一切都是争取来的,不要等待机会,要主动寻求 +> 5. 关注商业,升维到老板思维,看清趋势,及时止损 +> 6. 培养数据思维,利用数据了解世界,指导决策 +> 7. 做一个好"销售",无论是自己还是产品,都要学会展示和说服 +> 8. 少加班多运动,保持身心健康,提高工作效率 +> 9. 有随时可以离开的底气,不要被职场所困,借假修真,提升自己 +> 10. 只是一份工作,不要过分纠结,相信自己,走出去看看 +> +> **原文地址**: + +在美团的三年多时光,如同一部悠长的交响曲,高高低低,而今离开已有一段时间。闲暇之余,梳理了三年多的收获与感慨,总结成 10 条,既是对过去一段时光的的一个深情回眸,也是对未来之路的一份期许。 + +倘若一些感悟能为刚步入职场的年轻人,或是刚在职业生涯中崭露头角的后起之秀,带来一点点启示与帮助,也是莫大的荣幸。 + +## 01 结构化思考与表达 + +美团是一家特别讲究方法论的公司,人人都要熟读四大名著《高效能人士的七个习惯》、《金字塔原理》、《用图表说话》和《学会提问》。 + +与结构化思考和表达相关的,是《金字塔原理》,作者是麦肯锡公司第一位女性咨询顾问。这本书告诉我们,思考和表达的过程,就像构建金字塔(或者构建一棵树),先有整体结论,再寻找证据,证据之间要讲究相互独立、而且能穷尽(MECE 原则),论证的过程也要按特定的顺序进行,比如时间顺序、空间顺序、重要性顺序…… + +作为大厂社畜,日常很大一部分工作就是写文档、看别人文档。大家做的事,但最后呈现的结果却有很大差异。一篇逻辑清晰、详略得当的文档,给人一种如沐春风的感受,能提炼出重要信息,是好的参考指南。 + +结构化思考与表达算是职场最通用的能力,也是打造个人影响力最重要的途径之一。 + +## 02 忘掉职级,该怼就怼 + +在阿里工作时,能看到每个人的 Title,看到江湖地位高(职级高+入职时间早)的同学,即便跟自己没有汇报关系,不自然的会多一层敬畏。推进工作时,会多一层压力,对方未读或已读未回时,不知如何应对。 + +美团只能看到每个人的坑位信息,还有 Ta 的上级。工作相关的问题,可以向任何人提问,如果协同方没有及时响应,隔段时间@一次,甚至"怼一怼",都没啥问题,事情一直往前推进才最重要。除了大象消息直接提问外,还有个大杀器--TT(公司级问题流转系统),在上面提问时,加上对方主管,如果对方未及时回应,问题会自动升级,每天定时 Push,直到解决为止。 + +我见到一些很年轻的同事,他们在推动 OKR、要资源的事上,很有一套,只要能达到自己的目标,不会考虑别人的感受,最终,他们还真能把事办成。 + +当然了,段位越高的人,越能用自己的人格魅力、影响力、资源等,去影响和推动事情的进程,而不是靠对他人的 Push。只是在拿结果的事上,不要把自己太当回事,把别人太当回事,大家在一起,也只是为了完成各自的任务,忘掉职级,该怼时还得怼。 + +## 03 用好平台资源 + +没有人能在一家公司待一辈子,公司再牛,跟自己关系不大,重要的是,在有限的时间内,最大化用好平台资源。 + +在美团除了认识自己节点的同事外,有幸认识一群特别棒的协作方,还有其他 BU 的同学。 + +这些优秀的人身上,有很多共同的特质:谦虚、利他、乐于分享、双赢思维。 + +有两位做运营的同学。 + +一位是无意中关注他公众号结识上的。他公众号记录了很多职场成长、家庭建造上的思考和收获,还有定期个人复盘。他和太太都是大厂中层管理者,从文章中看到的不是他多厉害,而是非常接地气的故事。我们约饭了两次,有很多共同话题,现在还时不时有一些互动。 + +一位职级更高的同学,他在内网发起了一个"请我喝一杯咖啡,和我一起聊聊个人困惑"的活动,我报名参与了一期。和他聊天的过程,特别像是一场教练对话(最近学习教练课程时才感受到的),帮我排除干扰、聚焦目标的同时,也从他分享个人成长蜕变的过程,收获很多动力。(刚好自己最近也学习了教练技术,后面也准备采用类似的方式,去帮助曾经像我一样迷茫的人) + +还有一些协作方同学。他们工作做得超级到位,能感受到,他们在乎他人时间;稍微有点出彩的事儿,不忘记拉上更多人。利他和双赢思维,在他们身上是最好的阐释。 + +除了结识优秀的人,向他们学习外,还可以关注各个通道/工种的课程资源。 + +在大厂,多数人的角色都是螺丝钉,但千万不要局限于做一颗螺丝钉。多去学习一些通识课,了解商业交付的各个环节,看清商业世界,明白自己的定位,超越自己的定位。 + +## 04 一切都是争取来的 + +工作很多年了,很晚才明白这个道理。 + +之前一直认为,只要做好自己该做的,一定会被看见,被赏识,也会得到更多机会。但很多时候,这只是个人的一厢情愿。除了自己,不会有人关心你的权益。 + +社会主义初级阶段,我国国内的主要矛盾是人民日益增长的物质文化需要同落后的社会生产之间的矛盾。无论在哪里,资源都是稀缺的,自己在乎的,就得去争取。 + +想成长某个技能、想参与哪个模块、想做哪个项目,升职加薪……自己不提,不去争取,不会有人主动给你。 + +争不争取是一回事,能不能得到是一回事,只有争取,才有可能得到。争取了,即便没有得到,最终也没失去什么。 + +## 05 关注商业 + +大公司,极度关注效率,大部分岗位,拆解的粒度越细,效率会越高,这些对组织是有利的。但对个人来说,则很容易螺丝钉化。 + +做技术的同学,更是这样。 + +做前端的同学,不会关注数据是如何落库的;做后端的同学,不会思考页面是否存在兼容性问题;做业务开发的,不用考虑微服务诸多中间件是如何搭建起来的…… + +大部分人都想着怎么把自己这摊子事搞好,不会去思考上下游同学在做些什么,更少有人真正关注商业,关心公司的盈利模式,关心每一次产品迭代到底带来哪些业务价值。 + +把手头的事做好是应该的,但绝不能停留在此。所有的产品,只有在商业社会产生交付,让客户真正获益,才是有价值的。 + +关注商业,能帮我们升维到老板思维,明白投入产出比,抓大放小;也帮助我们,在碰到不好的业务时,及时止损;更重要的是,它帮助我们真正看清趋势,提前做好准备。 + +《五分钟商学院》系列,是很好的商业入门级书籍。尽管作者刘润最近存在争议,但不可否认,他比我们大多数人段位还是高很多,他的书值得一读。 + +## 06 培养数据思维 + +当今数字化时代,数据思维显得尤为重要。数据不仅可以帮助我们更好地了解世界,还可以指导我们的决策和行动。 + +非常幸运的是,在阿里和美团的两份经历,都是做商业化广告业务,在离钱💰最近的地方,也培养了数据的敏感性。见过商业数据指标的定义、加工、生产和应用全流程,也在不断熏陶下,能看懂大部分指标背后的价值。 + +除了直接面向业务的数据,还有研发协作全流程产生的数据。数据被记录和汇总统计后,能直观地看到每个环节的效率和质量。螺丝钉们的工作,也彻彻底底被数字量化,除了积极面对虚拟化、线上化、数字化外,我们别无他法。 + +受工作数据化的影响,生活中,我也渐渐变成了一个数据记录狂,日常运动(骑行、跑步、健走等)必须通过智能手表记录下来,没带 Apple Watch,感觉这次白运动了。每天也在很努力地完成三个圆环。 + +数据时代,我们沦为了透明人。也得益于数据被记录和分析,我们做任何事,都能快速得到反馈,这也是自我提升的一个重要环节。 + +## 07 做一个好"销售" + +就某种程度来说,所有的工作,本质都是销售。 + +这是很多大咖的观点,我也是很晚才明白这个道理。 + +我们去一家公司应聘,本质上是在讲一个「我很牛」的故事,销售的是自己;日常工作汇报、季度/年度述职、晋升答辩,是在销售自己;在任何一个场合曝光,也是在销售自己。 + +如果我们所服务的组织,对外提供的是一件产品或一项服务,所有上下游协作的同学,唯一在做的事就是,齐心协力把产品/服务卖出去, 我们本质做的还是销售。 + +所以, 千万不要看不起任何销售,也不要认为认为销售是一件很丢面子的事。 + +真正的大佬,随时随地都在销售。 + +## 08 少加班多运动 + +在职场,大家都认同一个观点,工作是做不完的。 + +我们要做的是,用好时间管理四象限法,识别重要程度和优先级,有限时间,聚焦在固定几件事上。 + +这要求我们不断提高自己的问题识别能力、拆解能力,还有专注力。 + +我们会因为部分项目的需要而加班,但不会长期加班。 + +加班时间短一点,就能腾出更多时间运动。 + +最近一次线下培训课,认识一位老师 Hubert,Hubert 是一位超级有魅力的中年大叔(可以通过「有意思教练」的课程链接到他),从外企高管的位置离开后,和太太一起创办了一家培训机构。作为公司高层,日常工作非常忙,头发也有些花白了,但一身腱子肉胜过很多健身教练,给人的状态也是很年轻。聊天得知,Hubert 经常 5 点多起来泡健身房~ + +我身边还有一些同事,跟我年龄差不多,因为长期加班,发福严重,比实际年龄看起来苍老 10+岁; + +还有同事曾经加班进 ICU,幸好后面身体慢慢恢复过来。 + +某某厂员工长期加班猝死的例子,更是屡见不鲜。 + +减少加班,增加运动,绝对是一件性价比极高的事。 + +## 09 有随时可以离开的底气 + +当今职场,跟父辈时候完全不一样,职业的多样性和变化性越来越快,很少有人能够在同一份工作或同一个公司待一辈子。除了某些特定的岗位,如公务员、事业单位等,大多数人都会在职业生涯中经历多次的职业变化和调整。 + +在商业组织里,个体是弱势群体,但不要做弱者。每一段职场,每一项工作,都是上天给我们的修炼。 + +我很喜欢"借假修真"这个词。我们参与的大大小小的项目, 重要吗?对公司来说可能重要,对个人来说,则未必。我们去做,一方面是迫于生计; + +另外一方面,参与每个项目的感悟、心得、体会,是真实存在的,很多的能力,都是在这个过程得到提升。 + +明白这一点,就不会被职场所困,会刻意在各样事上提升自己,积累的越多,对事务的本质理解的越深、越广,也越发相信很多底层知识是通用的,内心越平静,也会建立起随时都可以离开的底气。 + +## 10 只是一份工作 + +工作中,我们时常会遇到各种挑战和困难,如发展瓶颈、难以处理的人和事,甚至职场 PUA 等。这些经历可能会让我们感到疲惫、沮丧,甚至怀疑自己的能力和价值。然而,重要的是要明白,困难只是成长道路上的暂时阻碍,而不是我们的定义。 + +写总结和复盘是很好的方式,可以帮我们理清思路,找到问题的根源,并学习如何应对类似的情况。但也要注意不要陷入自我怀疑和内耗的陷阱。遇到困难时,应该学会相信自己,积极寻找解决问题的方法,而不是过分纠结于自己的不足和错误。 + +内网常有同学匿名分享工作压力过大,常常失眠甚至中度抑郁,每次看到这些话题,非常难过。大环境不好,是不争的事实,但并不代表个体就没有出路。 + +我们容易预设困难,容易加很多"可是",当窗户布满灰尘时,不要试图努力把窗户擦干净,走出去吧,你将看到一片蔚蓝的天空。 + +## 最后 + +写到最后,特别感恩美团三年多的经历。感谢我的 Leader 们,感谢曾经并肩作战过的小伙伴,感谢遇到的每一位和我一样在平凡的岗位,努力想带给身边一片微光的同学。所有的相遇,都是缘分。 diff --git a/docs/high-quality-technical-articles/advanced-programmer/programmer-quickly-learn-new-technology.md b/docs/high-quality-technical-articles/advanced-programmer/programmer-quickly-learn-new-technology.md new file mode 100644 index 00000000000..3cd553a182e --- /dev/null +++ b/docs/high-quality-technical-articles/advanced-programmer/programmer-quickly-learn-new-technology.md @@ -0,0 +1,50 @@ +--- +title: 程序员如何快速学习新技术 +category: 技术文章精选集 +tag: + - 练级攻略 +--- + +> **推荐语**:这是[《Java 面试指北》](https://javaguide.cn/zhuanlan/java-mian-shi-zhi-bei.html)练级攻略篇中的一篇文章,分享了我对于如何快速学习一门新技术的看法。 +> +> ![《Java 面试指北》练级攻略篇](https://oss.javaguide.cn/javamianshizhibei/training-strategy-articles.png) + +很多时候,我们因为工作原因需要快速学习某项技术,进而在项目中应用。或者说,我们想要去面试的公司要求的某项技术我们之前没有接触过,为了应对面试需要,我们需要快速掌握这项技术。 + +作为一个人纯自学出生的程序员,这篇文章简单聊聊自己对于如何快速学习某项技术的看法。 + +学习任何一门技术的时候,一定要先搞清楚这个技术是为了解决什么问题的。深入学习这个技术的之前,一定先从全局的角度来了解这个技术,思考一下它是由哪些模块构成的,提供了哪些功能,和同类的技术想必它有什么优势。 + +比如说我们在学习 Spring 的时候,通过 Spring 官方文档你就可以知道 Spring 最新的技术动态,Spring 包含哪些模块 以及 Spring 可以帮你解决什么问题。 + +![](https://oss.javaguide.cn/github/javaguide/system-design/web-real-time-message-push/20210506110341207.png) + +再比如说我在学习消息队列的时候,我会先去了解这个消息队列一般在系统中有什么作用,帮助我们解决了什么问题。消息队列的种类很多,具体学习研究某个消息队列的时候,我会将其和自己已经学习过的消息队列作比较。像我自己在学习 RocketMQ 的时候,就会先将其和自己曾经学习过的第 1 个消息队列 ActiveMQ 进行比较,思考 RocketMQ 相对于 ActiveMQ 有了哪些提升,解决了 ActiveMQ 的哪些痛点,两者有哪些相似的地方,又有哪些不同的地方。 + +**学习一个技术最有效最快的办法就是将这个技术和自己之前学到的技术建立连接,形成一个网络。** + +然后,我建议你先去看看官方文档的教程,运行一下相关的 Demo ,做一些小项目。 + +不过,官方文档通常是英文的,通常只有国产项目以及少部分国外的项目提供了中文文档。并且,官方文档介绍的往往也比较粗糙,不太适合初学者作为学习资料。 + +如果你看不太懂官网的文档,你也可以搜索相关的关键词找一些高质量的博客或者视频来看。 **一定不要一上来就想着要搞懂这个技术的原理。** + +就比如说我们在学习 Spring 框架的时候,我建议你在搞懂 Spring 框架所解决的问题之后,不是直接去开始研究 Spring 框架的原理或者源码,而是先实际去体验一下 Spring 框架提供的核心功能 IoC(Inverse of Control:控制反转) 和 AOP(Aspect-Oriented Programming:面向切面编程),使用 Spring 框架写一些 Demo,甚至是使用 Spring 框架做一些小项目。 + +一言以蔽之, **在研究这个技术的原理之前,先要搞懂这个技术是怎么使用的。** + +这样的循序渐进的学习过程,可以逐渐帮你建立学习的快感,获得即时的成就感,避免直接研究原理性的知识而被劝退。 + +**研究某个技术原理的时候,为了避免内容过于抽象,我们同样可以动手实践。** + +比如说我们学习 Tomcat 原理的时候,我们发现 Tomcat 的自定义线程池挺有意思,那我们自己也可以手写一个定制版的线程池。再比如我们学习 Dubbo 原理的时候,可以自己动手造一个简易版的 RPC 框架。 + +另外,学习项目中需要用到的技术和面试中需要用到的技术其实还是有一些差别的。 + +如果你学习某一项技术是为了在实际项目中使用的话,那你的侧重点就是学习这项技术的使用以及最佳实践,了解这项技术在使用过程中可能会遇到的问题。你的最终目标就是这项技术为项目带来了实际的效果,并且,这个效果是正面的。 + +如果你学习某一项技术仅仅是为了面试的话,那你的侧重点就应该放在这项技术在面试中最常见的一些问题上,也就是我们常说的八股文。 + +很多人一提到八股文,就是一脸不屑。在我看来,如果你不是死记硬背八股文,而是去所思考这些面试题的本质。那你在准备八股文的过程中,同样也能让你加深对这项技术的了解。 + +最后,最重要同时也是最难的还是 **知行合一!知行合一!知行合一!** 不论是编程还是其他领域,最重要不是你知道的有多少,而是要尽量做到知行合一。 diff --git a/docs/high-quality-technical-articles/advanced-programmer/seven-tips-for-becoming-an-advanced-programmer.md b/docs/high-quality-technical-articles/advanced-programmer/seven-tips-for-becoming-an-advanced-programmer.md index 0dbe554eafb..ef273f47b5c 100644 --- a/docs/high-quality-technical-articles/advanced-programmer/seven-tips-for-becoming-an-advanced-programmer.md +++ b/docs/high-quality-technical-articles/advanced-programmer/seven-tips-for-becoming-an-advanced-programmer.md @@ -18,7 +18,7 @@ tag: > 6. 关注全局 > 7. 归纳总结能力 > -> **原文地址**:https://mp.weixin.qq.com/s/8lMGzBzXine-NAsqEaIE4g +> **原文地址**: ### 建议 1:刻意加强需求评审能力 @@ -103,3 +103,5 @@ tag: 普通程序员往往是工作的事情做完就拉到,很少回头去对自己的技术,对业务进行归纳和总结。 而高级的程序员往往都会在一件比较大的事情做完之后总结一下,做个 ppt,写个博客啥的记录下来。这样既对自己的工作是一个归纳,也可以分享给其它同学,促进团队的共同成长。 + + diff --git a/docs/high-quality-technical-articles/advanced-programmer/ten-years-of-dachang-growth-road.md b/docs/high-quality-technical-articles/advanced-programmer/ten-years-of-dachang-growth-road.md index 5c3d3f9c9d0..045c2bfed66 100644 --- a/docs/high-quality-technical-articles/advanced-programmer/ten-years-of-dachang-growth-road.md +++ b/docs/high-quality-technical-articles/advanced-programmer/ten-years-of-dachang-growth-road.md @@ -6,7 +6,7 @@ tag: - 练级攻略 --- -> **推荐语**:这篇文章的作者有着丰富的工作经验,曾在大厂工作了 12 年。结合自己走过的弯路和接触过的优秀技术人,他总结出了一些对于个人成长具有普遍指导意义的经验和特质 +> **推荐语**:这篇文章的作者有着丰富的工作经验,曾在大厂工作了 12 年。结合自己走过的弯路和接触过的优秀技术人,他总结出了一些对于个人成长具有普遍指导意义的经验和特质。 > > **原文地址:** @@ -132,3 +132,5 @@ tag: ## 结语 以上就是我对互联网从业技术人员十年成长之路的心得,希望在你困惑和关键选择的时候可以帮助到你。如果我的只言片语能够在未来的某个时间帮助到你哪怕一点,那将是我莫大的荣幸。 + + diff --git a/docs/high-quality-technical-articles/advanced-programmer/the-growth-strategy-of-the-technological-giant.md b/docs/high-quality-technical-articles/advanced-programmer/the-growth-strategy-of-the-technological-giant.md index c3acee32889..20aed477d3a 100644 --- a/docs/high-quality-technical-articles/advanced-programmer/the-growth-strategy-of-the-technological-giant.md +++ b/docs/high-quality-technical-articles/advanced-programmer/the-growth-strategy-of-the-technological-giant.md @@ -8,9 +8,7 @@ tag: > **推荐语**:波波老师的一篇文章,写的非常好,不光是对技术成长有帮助,其他领域也是同样适用的!建议反复阅读,形成一套自己的技术成长策略。 > ->
-> -> **原文地址:** https://mp.weixin.qq.com/s/YrN8T67s801-MRo01lCHXA +> **原文地址:** ## 1. 前言 @@ -148,7 +146,7 @@ Brendan Gregg,Jay Kreps 和 Brad Traversy 三个人走的技术路线各不相 ## 四、战略思维的诞生 -![思考周期和机会点](https://p3-juejin.byteimg.com/tos-cn-i-k3u1fbpfcp/dc87167f53b243d49f9f4e8c7fe530a1~tplv-k3u1fbpfcp-zoom-1.image) +![思考周期和机会点](https://oss.javaguide.cn/p3-juejin/dc87167f53b243d49f9f4e8c7fe530a1~tplv-k3u1fbpfcp-zoom-1.png) 一般毕业生刚进入企业工作的时候,思考大都是以天/星期/月为单位的,基本上都是今天学个什么技术,明天学个什么语言,很少会去思考一年甚至更长的目标。这是个眼前漆黑看不到的懵懂时期,捕捉到机会点的能力和概率都非常小。 @@ -204,4 +202,4 @@ Brendan Gregg,Jay Kreps 和 Brad Traversy 三个人走的技术路线各不相 > > 实现战略目标,就像种树一样。刚开始只是一个小根芽,树干还没有长出来;树干长出来了,枝叶才能慢慢长出来;树枝长出来,然后才能开花和结果。刚开始种树的时候,只管栽培灌溉,别老是纠结枝什么时候长出来,花什么时候开,果实什么时候结出来。纠结有什么好处呢?只要你坚持投入栽培,还怕没有枝叶花实吗? -![悬想何益](https://p3-juejin.byteimg.com/tos-cn-i-k3u1fbpfcp/863dbfa7d8f64123a41cbc1406aa0c46~tplv-k3u1fbpfcp-zoom-1.image) + diff --git a/docs/high-quality-technical-articles/advanced-programmer/thinking-about-technology-and-business-after-five-years-of-work.md b/docs/high-quality-technical-articles/advanced-programmer/thinking-about-technology-and-business-after-five-years-of-work.md new file mode 100644 index 00000000000..d32f586b449 --- /dev/null +++ b/docs/high-quality-technical-articles/advanced-programmer/thinking-about-technology-and-business-after-five-years-of-work.md @@ -0,0 +1,109 @@ +--- +title: 工作五年之后,对技术和业务的思考 +category: 技术文章精选集 +author: 知了一笑 +tag: + - 练级攻略 +--- + +> **推荐语**:这是我在两年前看到的一篇对我触动比较深的文章。确实要学会适应变化,并积累能力。积累解决问题的能力,优化思考方式,拓宽自己的认知。 +> +> **原文地址:** + +苦海无边,回头无岸。 + +## 01 前言 + +晃晃悠悠的,在互联网行业工作了五年,默然回首,你看哪里像灯火阑珊处? + +初入职场,大部分程序员会觉得苦学技术,以后会顺风顺水升职加薪,这样的想法没有错,但是不算全面,五年后你会不会继续做技术写代码这是核心问题。 + +初入职场,会觉得努力加班可以不断提升能力,可以学到技术的公司就算薪水低点也可以接受,但是五年之后会认为加班都是在不断挤压自己的上升空间,薪水低是人生的天花板。 + +这里想说的关键问题就是:初入职场的认知和想法大部分不会再适用于五年后的认知。 + +工作五年之后面临的最大压力就是选择:职场天花板,技术能力天花板,薪水天花板,三十岁天花板。 + +如何面对这些问题,是大部分程序员都在思考和纠结的。做选择的唯一参考点就是:利益最大化,这里可以理解为职场更好的升职加薪,顺风顺水。 + +五年,变化最大不是工作经验,能力积累,而是心态,清楚的知道现实和理想之间是存在巨大的差距。 + +## 02 学会适应变化,并积累能力 + +回首自己的职场五年,最认可的一句话就是:学会适应变化,并积累能力。 + +变化的就是,五年的时间技术框架更新迭代,开发工具的变迁,公司环境队友的更换,甚至是不同城市的流浪,想着能把肉体和灵魂安放在一处,有句很经典的话就是:唯一不变的就是变化本身。 + +要积累的是:解决问题的能力,思考方式,拓宽认知。 + +这种很难直白的描述,属于个人认知的范畴,不同的人有不一样的看法,所以只能站在大众化的角度去思考。 + +首先聊聊技术,大部分小白级别的,都希望自己的技术能力不断提高,争取做到架构师级别,但是站在当前的互联网环境中,这种想法实现难度还是偏高,这里既不是打击也不是为了抬杠。 + +可以观察一下现状,技术团队大的 20-30 人,小的 10-15 人,能有一个架构师去专门管理底层框架都是少有现象。 + +这个问题的原因很多,首先架构师的成本过高,环境架构也不是需要经常升级,说的难听点可能框架比项目生命周期更高。 + +所以大部分公司的大部分业务,基于现有大部分成熟的开源框架都可以解决,这也就导致架构师这个角色通常由项目主管代替或者级别较高的开发直接负责,这就是现实情况。 + +这就导致技术框架的选择思路就是:只选对的。即这方面的人才多,开源解决方案多,以此降低技术方面对公司业务发展的影响。 + +那为什么还要不断学习和积累技术能力?如果没有这个能力,程序员岗位可能根本走不了五年之久,需要用技术深度积累不断解决工作中的各种问题,用技术的广度提升自己实现业务需求的认知边界,这是安放肉体的根本保障。 + +这就是导致很多五年以后的程序员压力陡然升高的原因,走向管理岗的另一个壁垒就是业务思维和认知。 + +## 03 提高业务能力的积累 + +程序员该不该用心研究业务,这个问题真的没有纠结的必要,只要不是纯技术型的公司,都需要面对业务。 + +不管技术、运营、产品、管理层,都是在面向业务工作。 + +从自己职场轨迹来看,五年变化最大就是解决业务问题的能力,职场之初面对很多业务场景都不知道如何下手,到几年之后设计业务的解决方案。 + +这是大部分程序员在职场前五年跳槽就能涨薪的根本原因,面对业务场景,基于积累的经验和现有的开源工具,能快速给出合理的解决思路和实现过程。 + +工作五年可能对技术底层的清晰程度都没有初入职场的小白清楚,但是写的程序却可以避开很多坑坑洼洼,对于业务的审视也是很细节全面。 + +解决业务能力的积累,对于技术视野的宽度需求更甚,比如职场初期对于海量数据的处理束手无策,但是在工作几年之后见识数据行业的技术栈,真的就是技术选型的视野问题。 + +什么是衡量技术能力的标准?站在一个共识的角度上看:系统的架构与代码设计能适应业务的不断变化和各种需求。 + +相对比与技术,业务的变化更加快速频繁,高级工程师或者架构师之所以薪资高,这些角色一方面能适应业务的迭代,并且在工作中具有一定前瞻性,会考虑业务变化的情况下代码复用逻辑,这样的能力是需要一定的技术视野和业务思维的沉淀。 + +所以职场中:业务能说的井井有条,代码能写的明明白白,得到机会的可能性更大。 + +## 04 不同的阶段技术和业务的平衡和选择 + +从理性的角度看技术和业务两个方面,能让大部分人职场走的平稳顺利,但是不同的阶段对两者的平衡和选择是不一样的。 + +在思考如何选择的时候,可以参考二八原则的逻辑,即在任何一组东西中,最重要的只占其中一小部分,约 20%,其余 80%尽管是多数,却是次要的,因此又称二八定律。 + +个人真的非常喜欢这个原则,大部分人都不是天才,所以很难三心二意同时做好几件事情,在同一时间段内应该集中精力做好一件事件。 + +但是单纯的二八原则模式可能不适应大部分职场初期的人,因为初期要学习很多内容,如何在职场生存:专业能力,职场关系,为人处世,产品设计等等。 + +当然这些东西不是都要用心刻意学习,但是合理安排二二六原则或其他组合是更明智的,首先是专业能力要重点练习,其次可以根据自己的兴趣合理选择一到两个方面去慢慢了解,例如产品,运营,运维,数据等,毕竟三五年以后会不会继续写代码很难说,多给自己留个机会总是有备无患。 + +在职场初期,基本都是从技术角度去思考问题,如何快速提升自己的编码能力,在公司能稳定是首要目标,因此大部分时间都是在做基础编码和学习规范,这时可能 90%的心思都是放在基础编码上,另外 10%会学习环境架构。 + +最多一到两年,就会开始独立负责模块需求开发,需要自己设计整个代码思路,这里业务就会进入视野,要懂得业务上下游关联关系,学会思考如何设计代码结构,才能在需求变动的情况下代码改动较少,这个时候可能就会放 20%的心思在业务方面,30%学习架构方式。 + +三到五年这个时间段,是解决问题能力提升最快的时候,因为这个阶段的程序员基本都是在开发核心业务链路,例如交易、支付、结算、智能商业等模块,需要对业务整体有较清晰的把握能力,不然就是给自己挖坑,这个阶段要对业务流付出大量心血思考。 + +越是核心的业务线,越是容易爆发各种问题,如果在日常工作中不花心思处理各种细节问题,半夜异常自动的消息和邮件总是容易让人憔悴。 + +所以努力学习技术是提升自己,培养自己的业务认知也同样重要,个人认为这二者的分量平分秋色,只是需要在合适的阶段做出合理的权重划分。 + +## 05 学会在职场做选择和生存 + +基于技术能力和业务思维,学会在职场做选择和生存,这些是职场前五年一路走来的最大体会。 + +不管是技术还是业务,这两个概念依旧是个很大的命题,不容易把握,所以学会理清这两个方面能力中的公共模块是关键。 + +不管技术还是业务,都不可能从一家公司完全复制到另一家公司,但是可以把一家公司的技术框架,业务解决方案学会,并且带到另一家公司,例如技术领域内的架构、设计、流程、数据管理,业务领域内的思考方式、产品逻辑、分析等,这些是核心能力并且是大部分公司人才招聘的要求,所以这些才是工作中需要重点积累的。 + +人的精力是有限的,而且面对三十这个天花板,各种事件也会接连而至,在职场中学会合理安排时间并不断提升核心能力,这样才能保证自己的竞争力。 + +职场就像苦海无边,回首望去可能也没有岸边停泊,但是要具有换船的能力或者有个小木筏也就大差不差了。 + + diff --git a/docs/high-quality-technical-articles/interview/how-to-examine-the-technical-ability-of-programmers-in-the-first-test-of-technology.md b/docs/high-quality-technical-articles/interview/how-to-examine-the-technical-ability-of-programmers-in-the-first-test-of-technology.md index cef6e51c6e5..f96a20fec16 100644 --- a/docs/high-quality-technical-articles/interview/how-to-examine-the-technical-ability-of-programmers-in-the-first-test-of-technology.md +++ b/docs/high-quality-technical-articles/interview/how-to-examine-the-technical-ability-of-programmers-in-the-first-test-of-technology.md @@ -8,17 +8,13 @@ tag: > **推荐语**:从面试官和面试者两个角度探讨了技术面试!非常不错! > ->
-> > **内容概览:** > > - 实战与理论结合。比如,候选人叙述 JVM 内存模型布局之后,可以接着问:有哪些原因可能会导致 OOM , 有哪些预防措施? 你是否遇到过内存泄露的问题? 如何排查和解决这类问题? > - 项目经历考察不宜超过两个。因为要深入考察一个项目的详情,所占用的时间还是比较大的。一般来说,会让候选人挑选一个他或她觉得最有收获的/最有挑战的/印象最深刻的/自己觉得特有意思的项目。然后围绕这个项目进行发问。通常是从项目背景出发,考察项目的技术栈、项目模块及交互的整体理解、项目中遇到的有挑战性的技术问题及解决方案、排查和解决问题、代码可维护性问题、工程质量保障等。 > - 多问少说,让候选者多表现。根据候选者的回答适当地引导或递进或横向移动。 > ->
-> -> **原文地址**:https://www.cnblogs.com/lovesqcc/p/15169365.html +> **原文地址**: ## 灵魂三连问 @@ -342,3 +338,5 @@ tag: - [技术面试官的 9 大误区](https://zhuanlan.zhihu.com/p/51404304) - [如何当一个好的面试官?](https://www.zhihu.com/question/26240321) + + diff --git a/docs/high-quality-technical-articles/interview/my-personal-experience-in-2021.md b/docs/high-quality-technical-articles/interview/my-personal-experience-in-2021.md index dba4a243ab6..f7d62085553 100644 --- a/docs/high-quality-technical-articles/interview/my-personal-experience-in-2021.md +++ b/docs/high-quality-technical-articles/interview/my-personal-experience-in-2021.md @@ -8,9 +8,7 @@ tag: > **推荐语**:这篇文章的作者校招最终去了飞书做开发。在这篇文章中,他分享了自己的校招经历以及个人经验。 > ->
-> -> **原文地址**:https://www.ihewro.com/archives/1217/ +> **原文地址**: ## 基本情况 @@ -60,9 +58,9 @@ tag: 第一次投了钉钉,没想到因为行测做的不好,在简历筛选给拒绝了。 -第二次阿里妈妈的后端面试,一面电话面试,我感觉面的还可以,最后题目也做出来了。最后反问阶段问对我的面试有什么建议,面试官说投阿里最好还是 Java 的… 然后电话结束后就给我拒了… +第二次阿里妈妈的后端面试,一面电话面试,我感觉面的还可以,最后题目也做出来了。最后反问阶段问对我的面试有什么建议,面试官说投阿里最好还是 Java 的…… 然后电话结束后就给我拒了…… -当时真的心态有点崩,问了这个晚上 7 点半的面试,一直看书晚上都没吃… +当时真的心态有点崩,问了这个晚上 7 点半的面试,一直看书晚上都没吃…… 所以春招和阿里就无缘了。 @@ -82,7 +80,7 @@ tag: #### 字节飞书 -第一次一面就凉了,原因应该是笔试题目结果不对… +第一次一面就凉了,原因应该是笔试题目结果不对…… 第二次一面在 4 月底了,很顺利。二面在五一劳动节后,面试官还让学姐告诉我让我多看看智能指针,面试的时候让我手写 shared_ptr,我之前看了一些实现,但是没有自己写过,导致代码考虑的不够完善,leader 就一直提醒我要怎么改怎么改。 @@ -96,7 +94,7 @@ tag: ## 入职字节实习 -入职字节之前我本来觉得这个岗位可能是我面试的最适合我的了,因为我主 c++,而且飞书用 c++应该挺深的。来之后就觉得我可能不太喜欢做客户端相关,感觉好复杂…也许服务端好一些,现在我仍然不能确定。 +入职字节之前我本来觉得这个岗位可能是我面试的最适合我的了,因为我主 c++,而且飞书用 c++应该挺深的。来之后就觉得我可能不太喜欢做客户端相关,感觉好复杂……也许服务端好一些,现在我仍然不能确定。 字节的实习福利在这些公司中应该算是比较好的,小问题是工位比较窄,还是工作强度比其他的互联网公司大一些。字节食堂免费而且挺不错的。字节办公大厦很多,我所在的办公地点比较小。 @@ -199,3 +197,5 @@ tag: - **对于自己不会的,尽量多的说!!!!** 实在不行,就往别的地方说!!!总之是引导面试官往自己会的地方上说。 - 面试中的笔试和前面的笔试风格不同,面试笔试题目不太难,但是考察是冷静思考,代码优雅,没有 bug,先思考清楚!!!在写!!! - 在描述项目的难点的时候,不要去聊文档调研是难点,回答这部分问题更应该是技术上的难点,最后通过了什么技术解决了这个问题,这部分技术可以让面试官来更多提问以便知道自己的技术能力。 + + diff --git a/docs/high-quality-technical-articles/interview/screen-candidates-for-packaging.md b/docs/high-quality-technical-articles/interview/screen-candidates-for-packaging.md index b2ed4c772b3..5b0ff739b34 100644 --- a/docs/high-quality-technical-articles/interview/screen-candidates-for-packaging.md +++ b/docs/high-quality-technical-articles/interview/screen-candidates-for-packaging.md @@ -8,9 +8,7 @@ tag: > **推荐语**:经常听到培训班待过的朋友给我说他们的老师是怎么教他们“包装”自己的,不光是培训班,我认识的很多朋友也都会在面试之前“包装”一下自己,所以这个现象是普遍存在的。但是面试官也不都是傻子,通过下面这篇文章来看看面试官是如何甄别应聘者的包装程度。 > ->
-> -> **原文地址**:https://my.oschina.net/hooker/blog/3014656 +> **原文地址**: ## 前言 @@ -116,3 +114,5 @@ tag: 然而笔者碰到的问题是:使用 Git 两年却不知道 GitHub、使用 Redis 一年却不知道数据结构也不知道序列化、专业做爬虫却不懂 `content-type` 含义、使用搜索引擎技术却说不出两个分词插件、使用数据库读写分离却不知道同步延时等等。 写在最后,笔者认为在招聘途中,并不是不允许求职者包装,但是尽可能满足能筹平衡。虽然这篇文章没有完美的结尾,但是笔者提供了面试失败的各种经验。笔者最终招到了如意的小伙伴。也希望所有技术面试官早日找到符合自己产品发展的 IT 伙伴。 + + diff --git a/docs/high-quality-technical-articles/interview/some-secrets-about-alibaba-interview.md b/docs/high-quality-technical-articles/interview/some-secrets-about-alibaba-interview.md index 32a33086c4e..175efc3da14 100644 --- a/docs/high-quality-technical-articles/interview/some-secrets-about-alibaba-interview.md +++ b/docs/high-quality-technical-articles/interview/some-secrets-about-alibaba-interview.md @@ -8,9 +8,7 @@ tag: > **推荐语**:详细介绍了求职者在面试中应该具备哪些能力才会有更大概率脱颖而出。 > ->
-> -> **原文地址:** https://mp.weixin.qq.com/s/M2M808PwQ2JcMqfLQfXQMw +> **原文地址:** 最近我的工作稍微轻松些,就被安排去校招面试了 @@ -117,3 +115,5 @@ action,action,action ,重要的事情说三遍,做技术的不可能光 但是,面试时间有限,同学们一定要在有限的时间里展现出自己的**能力**和**无限的潜力** 。 最后,祝愿优秀的你能找到自己理想的工作! + + diff --git a/docs/high-quality-technical-articles/interview/summary-of-spring-recruitment.md b/docs/high-quality-technical-articles/interview/summary-of-spring-recruitment.md index 17644017cb3..474434645a9 100644 --- a/docs/high-quality-technical-articles/interview/summary-of-spring-recruitment.md +++ b/docs/high-quality-technical-articles/interview/summary-of-spring-recruitment.md @@ -8,11 +8,7 @@ tag: > **推荐语**:牛客网热帖,写的很全面!暑期实习,投了阿里、腾讯、字节,拿到了阿里和腾讯的 offer。 > ->
-> -> **原文地址:** https://www.nowcoder.com/discuss/640519 -> ->
+> **原文地址:** > > **下篇**:[十年饮冰,难凉热血——秋招总结](https://www.nowcoder.com/discuss/804679) @@ -57,7 +53,7 @@ tag: 更多书籍推荐建议大家看 [JavaGuide](https://javaguide.cn/books/) 这个网站上的书籍推荐,比较全面。 -![](https://p3-juejin.byteimg.com/tos-cn-i-k3u1fbpfcp/62099c9b2fd24d3cb6511e49756f486b~tplv-k3u1fbpfcp-zoom-1.image) +![](https://oss.javaguide.cn/p3-juejin/62099c9b2fd24d3cb6511e49756f486b~tplv-k3u1fbpfcp-zoom-1.png) ### 教程推荐 @@ -69,7 +65,7 @@ tag: - [MySQL 实战 45 讲](https://time.geekbang.org/column/intro/100020801):前 27 讲多看几遍基本可以秒杀面试中遇到的 MySQL 问题了。 - [Redis 核心技术与实战](https://time.geekbang.org/column/intro/100056701):讲解了大量的 Redis 在生产上的使用场景,和《Redis 设计与实现》配合着看,也可以秒杀面试中遇到的 Redis 问题了。 - [JavaGuide](https://javaguide.cn/books/):「Java 学习+面试指南」一份涵盖大部分 Java 程序员所需要掌握的核心知识。 -- [《Java 面试指北》](https://mp.weixin.qq.com/s?__biz=Mzg2OTA0Njk0OA==&mid=2247519384&idx=1&sn=bc7e71af75350b755f04ca4178395b1a&chksm=cea1c353f9d64a458f797696d4144b4d6e58639371a4612b8e4d106d83a66d2289e7b2cd7431&token=660789642&lang=zh_CN&scene=21#wechat_redirect):这是一份教你如何更高效地准备面试的小册,涵盖常见八股文(系统设计、常见框架、分布式、高并发 ......)、优质面经等内容。 +- [《Java 面试指北》](https://mp.weixin.qq.com/s?__biz=Mzg2OTA0Njk0OA==&mid=2247519384&idx=1&sn=bc7e71af75350b755f04ca4178395b1a&chksm=cea1c353f9d64a458f797696d4144b4d6e58639371a4612b8e4d106d83a66d2289e7b2cd7431&token=660789642&lang=zh_CN&scene=21#wechat_redirect):这是一份教你如何更高效地准备面试的小册,涵盖常见八股文(系统设计、常见框架、分布式、高并发 ……)、优质面经等内容。 ## 找工作 @@ -161,3 +157,5 @@ Java 卷吗?毫无疑问,很卷,我个人认为开发属于没有什么门 ## 祝福 惟愿诸君,前程似锦! + + diff --git a/docs/high-quality-technical-articles/interview/technical-preliminary-preparation.md b/docs/high-quality-technical-articles/interview/technical-preliminary-preparation.md index 4a7a3de9239..ae4e95b1818 100644 --- a/docs/high-quality-technical-articles/interview/technical-preliminary-preparation.md +++ b/docs/high-quality-technical-articles/interview/technical-preliminary-preparation.md @@ -15,7 +15,7 @@ tag: > - 项目经历考察不宜超过两个。因为要深入考察一个项目的详情,所占用的时间还是比较大的。一般来说,会让候选人挑选一个他或她觉得最有收获的/最有挑战的/印象最深刻的/自己觉得特有意思的项目。然后围绕这个项目进行发问。通常是从项目背景出发,考察项目的技术栈、项目模块及交互的整体理解、项目中遇到的有挑战性的技术问题及解决方案、排查和解决问题、代码可维护性问题、工程质量保障等。 > - 多问少说,让候选者多表现。根据候选者的回答适当地引导或递进或横向移动。 > -> **原文地址:** https://www.cnblogs.com/lovesqcc/p/15169365.html +> **原文地址:** ## 考察目标和思路 @@ -212,3 +212,5 @@ tag: 重点是:有些问题你答得很有深度,也体现了你的深度思考能力。 这一点是我当了技术面试官才领会到的。当然,并不是每位技术面试官都是这么想的,但我觉得这应该是个更合适的方式。 + + diff --git a/docs/high-quality-technical-articles/interview/the-experience-and-thinking-of-an-interview-experienced-by-an-older-programmer.md b/docs/high-quality-technical-articles/interview/the-experience-and-thinking-of-an-interview-experienced-by-an-older-programmer.md index 02ccd59a266..8aaa1d65aca 100644 --- a/docs/high-quality-technical-articles/interview/the-experience-and-thinking-of-an-interview-experienced-by-an-older-programmer.md +++ b/docs/high-quality-technical-articles/interview/the-experience-and-thinking-of-an-interview-experienced-by-an-older-programmer.md @@ -17,7 +17,7 @@ tag: > 5. 要善于从失败中学习。正是在杭州四个月空档期的持续学习、思考、积累和提炼,以及面试失败的反思、不断调整对策、完善准备、改善原有的短板,采取更为合理的方式,才在回武汉的短短两个周内拿到比较满意的 offer 。 > 6. 面试是通过沟通来理解双方的过程。面试中的问题,千变万化,但有一些问题是需要提前准备好的。 > -> **原文地址**:https://www.cnblogs.com/lovesqcc/p/14354921.html +> **原文地址**: 从每一段经历中学习,在每一件事情中修行。善于从挫折中学习。 @@ -358,3 +358,5 @@ ZOOM 的一位面试官或许是我见过的所有面试官中最差劲的。共 经过这一段面试的历炼,我觉得现在相比离职时的自己,又有了不少进步的。不说脱胎换骨,至少也是蜕了一层皮吧。差距,差距还是有的。起码面试那些知名大厂企业的技术专家和架构师还有差距。这与我平时工作的挑战性、认知视野的局限性及总结不足有关。下一次,我希望积蓄足够实力做到更好,和内心热爱的有价值有意义的事情再近一些些。 面试,其实也是一段工作经历。 + + diff --git a/docs/high-quality-technical-articles/interview/the-experience-of-get-offer-from-over-20-big-companies.md b/docs/high-quality-technical-articles/interview/the-experience-of-get-offer-from-over-20-big-companies.md index 16d8f9e4856..4cc38409fb7 100644 --- a/docs/high-quality-technical-articles/interview/the-experience-of-get-offer-from-over-20-big-companies.md +++ b/docs/high-quality-technical-articles/interview/the-experience-of-get-offer-from-over-20-big-companies.md @@ -8,9 +8,7 @@ tag: > **推荐语**:很实用的面试经验分享! > ->
-> -> **原文地址**:https://mp.weixin.qq.com/s/HXKg6-H0kGUU2OA1DS43Bw +> **原文地址**: 突然回想起当年,我也在秋招时也斩获了 20+的互联网各大厂 offer。现在想起来也是有点唏嘘,毕竟拿得再多也只能选择一家。不过许多朋友想让我分享下互联网面试方法,今天就来给大家仔细讲讲打法! @@ -76,7 +74,7 @@ tag: - 明确的知道业务架构或技术方案选型以及决策逻辑 - 深入掌握项目中涉及的组件以及框架 - 熟悉项目中的疑难杂症或长期遗留 bug 的解决方案 -- ...... +- …… ## 专业知识考查 @@ -195,3 +193,5 @@ tag: 这篇文章其实算讲的是方法论,很多我们一看就明白的「道理」实施起来可能会很难。可能会遇到一个不按常理出牌的面试官,也可能也会遇到一个沟通困难的面试官,当然也可能会撞上一个不怎么匹配的岗位。 总而言之,为了自己想要争取的东西,做好足够的准备总是没有坏处的。祝愿大家能成为`π`型人才,获得想要的`offer`! + + diff --git a/docs/high-quality-technical-articles/personal-experience/8-years-programmer-work-summary.md b/docs/high-quality-technical-articles/personal-experience/8-years-programmer-work-summary.md index f6362c7fa05..0af5480b58e 100644 --- a/docs/high-quality-technical-articles/personal-experience/8-years-programmer-work-summary.md +++ b/docs/high-quality-technical-articles/personal-experience/8-years-programmer-work-summary.md @@ -8,9 +8,7 @@ tag: > **推荐语**:这篇文章讲述了一位中科大的朋友 8 年的经历:从 2013 年毕业之后加入上海航天 x 院某卫星研究所,再到入职华为,从华为离职。除了丰富的经历之外,作者在文章还给出了很多自己对于工作/生活的思考。我觉得非常受用!我在这里,向这位作者表达一下衷心的感谢。 > ->
-> -> **原文地址**:https://www.cnblogs.com/scada/p/14259332.html +> **原文地址**: --- @@ -232,3 +230,5 @@ _PS:有几个问题先在这里解释一下,评论就不一一回复了_ ## 总结 好了 7 年多,近 8 年的职场讲完了,不管过去如何,未来还是要继续努力,希望看到这篇文章觉得有帮助的朋友,可以帮忙点个推荐,这样可能更多的人看到,也许可以避免更多的人犯我犯的错误。另外欢迎私信或者其他方式交流(某 Xin 号,jingyewandeng),可以讨论职场经验,方向,我也可以帮忙改简历(免费啊),不用怕打扰,能帮助别人是一项很有成绩感的事,并且过程中也会有收获,程序员也不要太腼腆呵呵 + + diff --git a/docs/high-quality-technical-articles/personal-experience/four-year-work-in-tencent-summary.md b/docs/high-quality-technical-articles/personal-experience/four-year-work-in-tencent-summary.md index c0d82be8417..4630bff560e 100644 --- a/docs/high-quality-technical-articles/personal-experience/four-year-work-in-tencent-summary.md +++ b/docs/high-quality-technical-articles/personal-experience/four-year-work-in-tencent-summary.md @@ -20,7 +20,7 @@ tag: **下文中的“我”,指这位作者本人。** -> 原文地址:https://zhuanlan.zhihu.com/p/602517682 +> 原文地址: 研究生毕业后, 一直在腾讯工作,不知不觉就过了四年。个人本身没有刻意总结的习惯,以前只顾着往前奔跑了,忘了停下来思考总结。记得看过一个职业规划文档,说的三年一个阶段,五年一个阶段的说法,现在恰巧是四年,同时又从腾讯离开,该做一个总结了。 @@ -105,3 +105,5 @@ PS:还好以前有奖杯,不然一点念想都没了。(现在腾讯似乎 - 深入一个技术方向,不断钻研底层技术知识,这样就有希望成为此技术专家。坦白来说,虽然我深入研究并实践过领域驱动设计,也用来建模和解决了一些复杂业务问题,但是发自内心的,我其实更喜欢钻研技术,同时,我又对大数据很感兴趣。因此,我决定了,以后的方向,就做数据相关的工作。 腾讯的四年,是我的第一份工作经历,认识了很多厉害的人,学到了很多。最后自己主动离开,也算走的体面(即使损失了大礼包),还是感谢腾讯。 + + diff --git a/docs/high-quality-technical-articles/personal-experience/huawei-od-275-days.md b/docs/high-quality-technical-articles/personal-experience/huawei-od-275-days.md index 56b1c1299a1..419f364adcf 100644 --- a/docs/high-quality-technical-articles/personal-experience/huawei-od-275-days.md +++ b/docs/high-quality-technical-articles/personal-experience/huawei-od-275-days.md @@ -7,9 +7,7 @@ tag: > **推荐语**:一位朋友的华为 OD 工作经历以及腾讯面试经历分享,内容很不错。 > ->
-> -> **原文地址**:https://www.cnblogs.com/shoufeng/p/14322931.html +> **原文地址**: ## 时间线 @@ -64,7 +62,7 @@ tag: 还听到一些内部的说法: - 没股票,没 TUP,年终奖少,只有工资可能比我司高一点点而已; -- 不能借针对 HW 的消费贷,也不能买公司提供的优惠保险… +- 不能借针对 HW 的消费贷,也不能买公司提供的优惠保险…… ### 那,到底要不要去华为 OD? @@ -94,7 +92,7 @@ d) 你的加班一定要提加班申请电子流换 Double 薪资,不然只能 **答案是:真的。** -据各类非官方渠道(比如知乎上的一些分享),转华为自有是有条件的(https://www.zhihu.com/question/356592219/answer/1562692667): +据各类非官方渠道(比如知乎上的一些分享),转华为自有是有条件的(): 1)入职时间:一年以上 2)绩效要求:连续两次绩效 A @@ -335,3 +333,5 @@ blabla 有少量的基础问题和一面有重复,还有几个和大数据相 **入职鹅厂已经 1 月有余。不同的岗位,不同的工作内容,也是不同的挑战。** 感受比较深的是,作为程序员,还是要自我驱动,努力提升个人技术能力,横向纵向都要扩充,这样才能走得长远。 + + diff --git a/docs/high-quality-technical-articles/personal-experience/two-years-of-back-end-develop--experience-in-didi-and-toutiao.md b/docs/high-quality-technical-articles/personal-experience/two-years-of-back-end-develop--experience-in-didi-and-toutiao.md index b3609658d43..5b6c47be7c7 100644 --- a/docs/high-quality-technical-articles/personal-experience/two-years-of-back-end-develop--experience-in-didi-and-toutiao.md +++ b/docs/high-quality-technical-articles/personal-experience/two-years-of-back-end-develop--experience-in-didi-and-toutiao.md @@ -16,9 +16,9 @@ tag: > - 想舔就舔,不想舔也没必要酸别人,Respect Greatness。 > - 时刻准备着,技术在手就没什么可怕的,哪天干得不爽了直接跳槽。 > - 平时积极总结沉淀,多跟别人交流,形成方法论。 -> - ...... +> - …… > -> **原文地址**:https://www.nowcoder.com/discuss/351805 +> **原文地址**: 先简单交代一下背景吧,某不知名 985 的本硕,17 年毕业加入滴滴,当时找工作时候也是在牛客这里跟大家一起奋战的。今年下半年跳槽到了头条,一直从事后端研发相关的工作。之前没有实习经历,算是两年半的工作经验吧。这两年半之间完成了一次晋升,换了一家公司,有过开心满足的时光,也有过迷茫挣扎的日子,不过还算顺利地从一只职场小菜鸟转变为了一名资深划水员。在这个过程中,总结出了一些还算实用的划水经验,有些是自己领悟到的,有些是跟别人交流学到的,在这里跟大家分享一下。 @@ -148,3 +148,5 @@ tag: 本来还想分享一些生活方面的故事,发现已经这么长了,那就先这样叭。上面写的一些总结和建议我自己做的也不是很好,还需要继续加油,和大家共勉。另外,其中某些观点,由于个人视角的局限性也不保证是普适和正确的,可能再工作几年这些观点也会发生改变,欢迎大家跟我交流~(甩锅成功) 最后祝大家都能找到心仪的工作,快乐工作,幸福生活,广阔天地,大有作为。 + + diff --git a/docs/high-quality-technical-articles/programmer/efficient-book-publishing-and-practice-guide.md b/docs/high-quality-technical-articles/programmer/efficient-book-publishing-and-practice-guide.md index e7077a592a5..fad7bede853 100644 --- a/docs/high-quality-technical-articles/programmer/efficient-book-publishing-and-practice-guide.md +++ b/docs/high-quality-technical-articles/programmer/efficient-book-publishing-and-practice-guide.md @@ -8,9 +8,7 @@ tag: > **推荐语**:详细介绍了程序员出书的一些常见问题,强烈建议有出书想法的朋友看看这篇文章。 > ->
-> -> **原文地址**:https://www.cnblogs.com/JavaArchitect/p/14128202.html +> **原文地址**: 古有三不朽, 所谓立德、立功、立言。程序员出一本属于自己的书,如果说是立言,可能过于高大上,但终究也算一件雅事。 @@ -138,3 +136,5 @@ tag: 可能当下,写公众号和录视频等的方式,挣钱收益要高于出书,不过话可以这样说,经营公众号和录制视频也是个长期的事情,在短时间里可能未必有收益,如果不是系统地发表内容的话,可能甚至不会有收益。所以出书可能是个非常好的前期准备工作,你靠出书系统积累了素材,靠出书整合了你的知识体系,那么在此基础上,靠公众号或者录视频挣钱可能就会事半功倍。 从上文里大家可以看到,在出书前期,联系出版社编辑和定选题并不难,如果要写案例书,那么在参考别人内容的基础上,要写完一般书可能也不是高不可攀的事情。甚至可以这样说,出书是个体力活,只要坚持,要出本书并不难,只是你愿不愿意坚持下去的问题。但一旦你有了属于自己的技术书,那么在找工作时,你就能自信地和面试官说你是这方面的专家,在你的视频、公众号和文字里,你也能正大光明地说,你是计算机图书的作者。更为重要的是,和名校、大厂经历一样,属于你的技术书同样是证明程序员能力的重要证据,当你通过出书有效整合了相关方面的知识体系后,那么在这方面,不管是找工作,或者是干私活,或者是接项目做,你都能理直气壮地和别人说:我能行! + + diff --git a/docs/high-quality-technical-articles/programmer/high-value-certifications-for-programmers.md b/docs/high-quality-technical-articles/programmer/high-value-certifications-for-programmers.md new file mode 100644 index 00000000000..7ea9f7932e0 --- /dev/null +++ b/docs/high-quality-technical-articles/programmer/high-value-certifications-for-programmers.md @@ -0,0 +1,114 @@ +--- +title: 程序员最该拿的几种高含金量证书 +category: 技术文章精选集 +tag: + - 程序员 +--- + +证书是能有效证明自己能力的好东西,它就是你实力的象征。在短短的面试时间内,证书可以为你加不少分。通过考证来提升自己,是一种性价比很高的办法。不过,相比金融、建筑、医疗等行业,IT 行业的职业资格证书并没有那么多。 + +下面我总结了一下程序员可以考的一些常见证书。 + +## 软考 + +全国计算机技术与软件专业技术资格(水平)考试,简称“软考”,是国内认可度较高的一项计算机技术资格认证。尽管一些人吐槽其实际价值,但在特定领域和情况下,它还是非常有用的,例如软考证书在国企和事业单位中具有较高的认可度、在某些城市软考证书可以用于积分落户、可用于个税补贴。 + +软考有初、中、高三个级别,建议直接考高级。相比于 PMP(项目管理专业人士认证),软考高项的难度更大,特别是论文部分,绝大部分人都挂在了论文部分。过了软考高项,在一些单位可以内部挂证,每个月多拿几百。 + +![软考高级证书](https://oss.javaguide.cn/github/javaguide/programmer-life/programmer-certification/ruankao-advanced-certification%20.jpg) + +官网地址:。 + +备考建议:[2024 年上半年,一次通过软考高级架构师考试的备考秘诀 - 阿里云开发者](https://mp.weixin.qq.com/s/9aUXHJ7dXgrHuT19jRhCnw) + +## PAT + +攀拓计算机能力测评(PAT)是一个专注于考察算法能力的测评体系,由浙江大学主办。该测评分为四个级别:基础级、乙级、甲级和顶级。 + +通过 PAT 测评并达到联盟企业规定的相应评级和分数,可以跳过学历门槛,免除筛选简历和笔试环节,直接获得面试机会。具体有哪些公司可以去官网看看: 。 + +对于考研浙江大学的同学来说,PAT(甲级)成绩在一年内可以作为硕士研究生招生考试上机复试成绩。 + +![PAT(甲级)成绩作用](https://oss.javaguide.cn/github/javaguide/programmer-life/programmer-certification/pat-enterprise-alliance.png) + +## PMP + +PMP(Project Management Professional)认证由美国项目管理协会(PMI)提供,是全球范围内认可度最高的项目管理专业人士资格认证。PMP 认证旨在提升项目管理专业人士的知识和技能,确保项目顺利完成。 + +![PMP 证书](https://oss.javaguide.cn/github/javaguide/programmer-life/programmer-certification/pmp-certification.png) + +PMP 是“一证在手,全球通用”的资格认证,对项目管理人士来说,PMP 证书含金量还是比较高的。放眼全球,很多成功企业都会将 PMP 认证作为项目经理的入职标准。 + +但是!真正有价值的不是 PMP 证书,而是《PMBOK》 那套项目管理体系,在《PMBOK》(PMP 考试指定用书)中也包含了非常多商业活动、实业项目、组织规划、建筑行业等各个领域的项目案例。 + +另外,PMP 证书不是一个高大上的证书,而是一个基础的证书。 + +## ACP + +ACP(Agile Certified Practitioner)认证同样由美国项目管理协会(PMI)提供,是项目管理领域的另一个重要认证。与 PMP(Project Management Professional)注重传统的瀑布方法论不同,ACP 专注于敏捷项目管理方法论,如 Scrum、Kanban、Lean、Extreme Programming(XP)等。 + +## OCP + +Oracle Certified Professional(OCP)是 Oracle 公司提供的一项专业认证,专注于 Oracle 数据库及相关技术。这个认证旨在验证和认证个人在使用和管理 Oracle 数据库方面的专业知识和技能。 + +下图展示了 Oracle 认证的不同路径和相应的认证级别,分别是核心路径(Core Track)和专业路径(Speciality Track)。 + +![OCP 认证路径](https://oss.javaguide.cn/github/javaguide/programmer-life/programmer-certification/oracle-certified-professional.jpg) + +## 阿里云认证 + +阿里云(Alibaba Cloud)提供的专业认证,认证方向包括云计算、大数据、人工智能、Devops 等。职业认证分为 ACA、ACP、ACE 三个等级,除了职业认证之外,还有一个开发者 Clouder 认证,这是专门为开发者设立的专项技能认证。 + +![](https://oss.javaguide.cn/github/javaguide/programmer-life/programmer-certification/aliyun-professional-certification.png) + +官网地址:。 + +## 华为认证 + +华为认证是由华为技术有限公司提供的面向 ICT(信息与通信技术)领域的专业认证,认证方向包括网络、存储、云计算、大数据、人工智能等,非常庞大的认证体系。 + +![](https://oss.javaguide.cn/github/javaguide/programmer-life/programmer-certification/huawei-professional-certification.png) + +## AWS 认证 + +AWS 云认证考试是 AWS 云计算服务的官方认证考试,旨在验证 IT 专业人士在设计、部署和管理 AWS 基础架构方面的技能。 + +AWS 认证分为多个级别,包括基础级、从业者级、助理级、专业级和专家级(Specialty),涵盖多个角色和技能: + +- **基础级别**:AWS Certified Cloud Practitioner,适合初学者,验证对 AWS 基础知识的理解,是最简单的入门认证。 +- **助理级别**:包括 AWS Certified Solutions Architect – Associate、AWS Certified Developer – Associate 和 AWS Certified SysOps Administrator – Associate,适合中级专业人士,验证其设计、开发和管理 AWS 应用的能力。 +- **专业级别**:包括 AWS Certified Solutions Architect – Professional 和 AWS Certified DevOps Engineer – Professional,适合高级专业人士,验证其在复杂和大规模 AWS 环境中的能力。 +- **专家级别**:包括 AWS Certified Advanced Networking – Specialty、AWS Certified Big Data – Specialty 等,专注于特定技术领域的深度知识和技能。 + +备考建议:[小白入门云计算的最佳方式,是去考一张 AWS 的证书(附备考经验)](https://mp.weixin.qq.com/s/xAqNOnfZ05GDRuUbAiMHIA) + +## Google Cloud 认证 + +与 AWS 认证不同,Google Cloud 认证只有一门助理级认证(Associate Cloud Engineer),其他大部分为专业级(专家级)认证。 + +备考建议:[如何备考谷歌云认证](https://mp.weixin.qq.com/s/Vw5LGPI_akA7TQl1FMygWw) + +官网地址: + +## 微软认证 + +微软的认证体系主要针对其 Azure 云平台,分为基础级别、助理级别和专家级别,认证方向包括云计算、数据管理、开发、生产力工具等。 + +![](https://oss.javaguide.cn/github/javaguide/programmer-life/programmer-certification/microsoft-certification.png) + +## Elastic 认证 + +Elastic 认证是由 Elastic 公司提供的一系列专业认证,旨在验证个人在使用 Elastic Stack(包括 Elasticsearch、Logstash、Kibana 、Beats 等)方面的技能和知识。 + +如果你在日常开发核心工作是负责 ElasticSearch 相关业务的话,还是比较建议考的,含金量挺高。 + +目前 Elastic 认证证书分为四类:Elastic Certified Engineer、Elastic Certified Analyst、Elastic Certified Observability Engineer、Elastic Certified SIEM Specialist。 + +比较建议考 **Elastic Certified Engineer**,这个是 Elastic Stack 的基础认证,考察安装、配置、管理和维护 Elasticsearch 集群等核心技能。 + +![](https://oss.javaguide.cn/github/javaguide/programmer-life/programmer-certification/elastic-certified-engineer-certification.png) + +## 其他 + +- PostgreSQL 认证:国内的 PostgreSQL 认证分为专员级(PCA)、专家级(PCP)和大师级(PCM),主要考查 PostgreSQL 数据库管理和优化,价格略贵,不是很推荐。 +- Kubernetes 认证:Cloud Native Computing Foundation (CNCF) 提供了几个官方认证,例如 Certified Kubernetes Administrator (CKA)、Certified Kubernetes Application Developer (CKAD),主要考察 Kubernetes 方面的技能和知识。 diff --git a/docs/high-quality-technical-articles/programmer/how-do-programmers-publish-a-technical-book.md b/docs/high-quality-technical-articles/programmer/how-do-programmers-publish-a-technical-book.md index 1927c927127..99dae8f9d72 100644 --- a/docs/high-quality-technical-articles/programmer/how-do-programmers-publish-a-technical-book.md +++ b/docs/high-quality-technical-articles/programmer/how-do-programmers-publish-a-technical-book.md @@ -8,9 +8,7 @@ tag: > **推荐语**:详细介绍了程序员应该如何从头开始出一本自己的书籍。 > ->
-> -> **原文地址**:https://www.cnblogs.com/JavaArchitect/p/12195219.html +> **原文地址**: 在面试或联系副业的时候,如果能令人信服地证明自己的实力,那么很有可能事半功倍。如何证明自己的实力?最有信服力的是大公司职位背景背书,没有之一,比如在 BAT 担任资深架构,那么其它话甚至都不用讲了。 @@ -91,3 +89,5 @@ tag: 其实出书收益并不高,算下来月入大概能在 3k 左右,如果是和图书出版公司合作,估计更少,但这好歹能证明自己的实力。不过在出书后不能止步于此,因为在大厂里有太多的牛人,甚至不用靠出书来证明自己的实力。 那么如何让出书带来的利益最大化呢?第一可以靠这进大厂,面试时有自己的书绝对是加分项。第二可以用这个去各大网站开专栏,录视频,或者开公众号,毕竟有出版社的背书,能更让别人信服你的能力。第三更得用写书时积累的学习方法和上进的态势继续专研更高深技术,技术有了,不仅能到大厂挣更多的钱,还能通过企业培训等方式更高效地挣钱。 + + diff --git a/docs/high-quality-technical-articles/work/32-tips-improving-career.md b/docs/high-quality-technical-articles/work/32-tips-improving-career.md index 00da645167b..78b0e66b122 100644 --- a/docs/high-quality-technical-articles/work/32-tips-improving-career.md +++ b/docs/high-quality-technical-articles/work/32-tips-improving-career.md @@ -68,3 +68,5 @@ tag: - Leader 的天然职责是让团队活下去,唯一的途径是实现上级、老板、公司经营者的目标,越是艰难的时候越明显; - Leader 的重要职责是识别团队需要被做的事情,并坚定信念,使众人行,越是艰难的时候越要坚定; - Leader 应该让自己遇到的每个人都感觉自己很重要、被需要。 + + diff --git a/docs/high-quality-technical-articles/work/employee-performance.md b/docs/high-quality-technical-articles/work/employee-performance.md index 11e0d4b0bbc..af22114fb04 100644 --- a/docs/high-quality-technical-articles/work/employee-performance.md +++ b/docs/high-quality-technical-articles/work/employee-performance.md @@ -13,9 +13,7 @@ tag: > - 短期打法:找出 1-2 件事,体现出你的独特价值(抓关键事件)。 > - 长期打法:通过一步步信任的建立,成为团队的核心人员或者是老板的心腹,具备不可替代性。 > ->
-> -> **原文地址**:https://mp.weixin.qq.com/s/D1s8p7z8Sp60c-ndGyh2yQ +> **原文地址**: 在新公司度过了一个完整的 Q3 季度,被打了绩效,也给下属打了绩效,感慨颇深。 @@ -130,3 +128,5 @@ A 和 C 属于绩效的两个极端,背后的逻辑类似,反着理解即可 当大家攻山头的能力都很强时,**到底做成什么样才算做好了?**当你弄清楚了这个玄机,职场也就看透了。 如果这篇文章让你有一点启发,来个点赞和在看呀!我是武哥,我们下期见! + + diff --git a/docs/high-quality-technical-articles/work/get-into-work-mode-quickly-when-you-join-a-company.md b/docs/high-quality-technical-articles/work/get-into-work-mode-quickly-when-you-join-a-company.md index 30b4e1cc0f3..72c672a5f92 100644 --- a/docs/high-quality-technical-articles/work/get-into-work-mode-quickly-when-you-join-a-company.md +++ b/docs/high-quality-technical-articles/work/get-into-work-mode-quickly-when-you-join-a-company.md @@ -7,15 +7,13 @@ tag: > **推荐语**:强烈建议每一位即将入职/在职的小伙伴看看这篇文章,看完之后可以帮助你少踩很多坑。整篇文章逻辑清晰,内容全面! > ->
-> -> **原文地址**:https://www.cnblogs.com/hunternet/p/14675348.html +> **原文地址**: ![新入职一家公司如何快速进入状态](https://oss.javaguide.cn/github/javaguide/high-quality-technical-articles/work/%E6%96%B0%E5%85%A5%E8%81%8C%E4%B8%80%E5%AE%B6%E5%85%AC%E5%8F%B8%E5%A6%82%E4%BD%95%E5%BF%AB%E9%80%9F%E8%BF%9B%E5%85%A5%E7%8A%B6%E6%80%81.png) 一年一度的金三银四跳槽大戏即将落幕,相信很多跳槽的小伙伴们已经找到了心仪的工作,即将或已经有了新的开始。 -相信有过跳槽经验的小伙伴们都知道,每到一个新的公司面临的可能都是新的业务、新的技术、新的团队......这些可能会打破你原来工作思维、编码习惯、合作方式...... +相信有过跳槽经验的小伙伴们都知道,每到一个新的公司面临的可能都是新的业务、新的技术、新的团队……这些可能会打破你原来工作思维、编码习惯、合作方式…… 而于公司而言,又不能给你几个月的时间去慢慢的熟悉。这个时候,如何快速进入工作状态,尽快发挥自己的价值是非常重要的。 @@ -47,7 +45,7 @@ tag: 我认为测试绝对是一个人快速了解团队业务的方式。通过测试我们可以走一走自己团队所负责项目的整体流程,如果遇到自己走不下去或想不通的地方及时去问,在这个过程中我们自然而然的就可以快速的了解到核心的业务流程。 -在了解业务的过程中,我们应该注意的是不要让自己过多的去追求细节,我们的目的是先能够整体了解业务流程,我们面向哪些用户,提供了哪些服务...... +在了解业务的过程中,我们应该注意的是不要让自己过多的去追求细节,我们的目的是先能够整体了解业务流程,我们面向哪些用户,提供了哪些服务…… ## 技术 @@ -57,13 +55,13 @@ tag: 接下来,我们就是要了解技术了,但也不是一上来就去翻源代码。 **应该按照从宏观到细节,由外而内逐步地对系统进行分析。** -首先,我们应该简单的了解一下 **自己团队/项目的所用到的技术栈** ,Java 还是.NET、亦或是多种语言并存,项目是前后端分离还是服务端全包,使用的数据库是 MySQL 还是 PostgreSQL......,这样我们可能会对所用到的技术和框架,以及自己所负责的内容有一定的预期,这一点有的人可能在面试的时候就会简单了解过。 +首先,我们应该简单的了解一下 **自己团队/项目的所用到的技术栈** ,Java 还是.NET、亦或是多种语言并存,项目是前后端分离还是服务端全包,使用的数据库是 MySQL 还是 PostgreSQL……,这样我们可能会对所用到的技术和框架,以及自己所负责的内容有一定的预期,这一点有的人可能在面试的时候就会简单了解过。 -下一步,我们应该了解的是 **系统的宏观业务架构** 。自己的团队主要负责哪些系统,每个系统又主要包含哪些模块,又与哪些外部系统进行交互......对于这些,最好可以通过流程图或者思维导图等方式整理出来。 +下一步,我们应该了解的是 **系统的宏观业务架构** 。自己的团队主要负责哪些系统,每个系统又主要包含哪些模块,又与哪些外部系统进行交互……对于这些,最好可以通过流程图或者思维导图等方式整理出来。 -然后,我们要做的是看一下 **自己的团队提供了哪些对外的接口或者服务** 。每个接口和服务所提供功能是什么。这一点我们可以继续去测试自己的系统,这个时候我们要看一看主要流程中主要包含了哪些页面,每个页面又调用了后端的哪些接口,每个后端接口又对应着哪个代码仓库。(如果是单纯做后端服务的,可以看一下我们提供了哪些服务,又有哪些上游服务,每个上游服务调用自己团队的哪些服务......),同样我们应该用画图的形式整理出来。 +然后,我们要做的是看一下 **自己的团队提供了哪些对外的接口或者服务** 。每个接口和服务所提供功能是什么。这一点我们可以继续去测试自己的系统,这个时候我们要看一看主要流程中主要包含了哪些页面,每个页面又调用了后端的哪些接口,每个后端接口又对应着哪个代码仓库。(如果是单纯做后端服务的,可以看一下我们提供了哪些服务,又有哪些上游服务,每个上游服务调用自己团队的哪些服务……),同样我们应该用画图的形式整理出来。 -接着,我们要了解一下 **自己的系统或服务又依赖了哪些外部服务** ,也就是说需要哪些外部系统的支持,这些服务也许是团队之外、公司之外,也可能是其他公司提供的。这个时候我们可以简单的进入代码看一下与外部系统的交互是怎么做的,包括通讯框架(REST、RPC)、通讯协议...... +接着,我们要了解一下 **自己的系统或服务又依赖了哪些外部服务** ,也就是说需要哪些外部系统的支持,这些服务也许是团队之外、公司之外,也可能是其他公司提供的。这个时候我们可以简单的进入代码看一下与外部系统的交互是怎么做的,包括通讯框架(REST、RPC)、通讯协议…… 到了代码层面,我们首先应该了解每个模块代码的层次结构,一个模块分了多少层,每个层次的职责是什么,了解了这个就对系统的整个设计有了初步的概念,紧接着就是代码的目录结构、配置文件的位置。 @@ -75,13 +73,13 @@ tag: 上面我们提到,新入职一家公司,第一阶段的目标是有跟着团队做项目的能力,接下来我们要了解的就是项目是如何运作的。 -我们应该把握从需求设计到代码编写入库最终到发布上线的整个过程中的一些关键点。例如项目采用敏捷还是瀑布的模式,一个迭代周期是多长,需求的来源以及展现形式,有没有需求评审,代码的编写规范是什么,编写完成后如何构建,如何入库,有没有提交规范,如何交付测试,发布前的准备是什么,发布工具如何使用...... +我们应该把握从需求设计到代码编写入库最终到发布上线的整个过程中的一些关键点。例如项目采用敏捷还是瀑布的模式,一个迭代周期是多长,需求的来源以及展现形式,有没有需求评审,代码的编写规范是什么,编写完成后如何构建,如何入库,有没有提交规范,如何交付测试,发布前的准备是什么,发布工具如何使用…… 关于项目我们只需要观察同事,或者自己亲身经历一个迭代的开发,就能够大概了解清楚。 -在了解项目运作的同时,我们还应该去了解团队,同样我们应该先从外部开始,我们对接了哪些外部团队,比如需求从哪里来,是否对接公司外部的团队,提供服务的上游团队有哪些,依赖的下游团队有哪些,团队之间如何沟通,常用的沟通方式是什么....... +在了解项目运作的同时,我们还应该去了解团队,同样我们应该先从外部开始,我们对接了哪些外部团队,比如需求从哪里来,是否对接公司外部的团队,提供服务的上游团队有哪些,依赖的下游团队有哪些,团队之间如何沟通,常用的沟通方式是什么…… -接下来则是团队内部,团队中有哪些角色,每个人的职责是什么,这样遇到问题我们也可以清楚的找到对应的同事寻求帮助。是否有一些定期的活动与会议,例如每日站会、周例会,是否有一些约定俗成的规矩,是否有一些内部评审,分享机制...... +接下来则是团队内部,团队中有哪些角色,每个人的职责是什么,这样遇到问题我们也可以清楚的找到对应的同事寻求帮助。是否有一些定期的活动与会议,例如每日站会、周例会,是否有一些约定俗成的规矩,是否有一些内部评审,分享机制…… ## 总结 @@ -92,3 +90,5 @@ tag: 关于如何快速进入工作状态,如果你有好的方法与建议,欢迎在评论区留言。 最后我们用一张思维导图来回顾一下这篇文章的内容。如果你觉得这篇文章对你有所帮助,可以关注文末公众号,我会经常分享一些自己成长过程中的经验与心得,与大家一起学习与进步。 + + diff --git a/docs/home.md b/docs/home.md index 7c2407e9602..047a09fe9ad 100644 --- a/docs/home.md +++ b/docs/home.md @@ -1,31 +1,18 @@ --- icon: creative -title: JavaGuide(Java学习&&面试指南) +title: JavaGuide(Java学习&面试指南) --- ::: tip 友情提示 - **面试专版**:准备 Java 面试的小伙伴可以考虑面试专版:**[《Java 面试指北 》](./zhuanlan/java-mian-shi-zhi-bei.md)** (质量很高,专为面试打造,配合 JavaGuide 食用)。 - **知识星球**:专属面试小册/一对一交流/简历修改/专属求职指南,欢迎加入 **[JavaGuide 知识星球](./about-the-author/zhishixingqiu-two-years.md)**(点击链接即可查看星球的详细介绍,一定确定自己真的需要再加入)。 -- **转载须知**:以下所有文章如非文首说明为转载皆为 JavaGuide 原创,转载在文首注明出处,如发现恶意抄袭/搬运,会动用法律武器维护自己的权益。让我们一起维护一个良好的技术创作环境! +- **使用建议** :有水平的面试官都是顺着项目经历挖掘技术问题。一定不要死记硬背技术八股文!详细的学习建议请参考:[JavaGuide 使用建议](./javaguide/use-suggestion.md)。 +- **求个 Star**:如果觉得 JavaGuide 的内容对你有帮助的话,还请点个免费的 Star,这是对我最大的鼓励,感谢各位一起同行,共勉!传送门:[GitHub](https://github.com/Snailclimb/JavaGuide) | [Gitee](https://gitee.com/SnailClimb/JavaGuide)。 +- **转载须知**:以下所有文章如非文首说明为转载皆为 JavaGuide 原创,转载请在文首注明出处。如发现恶意抄袭/搬运,会动用法律武器维护自己的权益。让我们一起维护一个良好的技术创作环境! ::: -
- -[![logo](https://oss.javaguide.cn/github/javaguide/csdn/1c00413c65d1995993bf2b0daf7b4f03.png)](https://github.com/Snailclimb/JavaGuide) - -[![阅读](https://img.shields.io/badge/阅读-read-brightgreen.svg)](https://javaguide.cn/) -![Stars](https://img.shields.io/github/stars/Snailclimb/JavaGuide) -![forks](https://img.shields.io/github/forks/Snailclimb/JavaGuide) -![issues](https://img.shields.io/github/issues/Snailclimb/JavaGuide) - -[GitHub](https://github.com/Snailclimb/JavaGuide) | [Gitee](https://gitee.com/SnailClimb/JavaGuide) - -
- -[![Banner](https://oss.javaguide.cn/xingqiu/xingqiu.png)](https://javaguide.cn/about-the-author/zhishixingqiu-two-years.html) - ## Java ### 基础 @@ -54,19 +41,26 @@ title: JavaGuide(Java学习&&面试指南) - [Java 集合常见知识点&面试题总结(上)](./java/collection/java-collection-questions-01.md) (必看 :+1:) - [Java 集合常见知识点&面试题总结(下)](./java/collection/java-collection-questions-02.md) (必看 :+1:) -- [Java 容器使用注意事项总结](./java/collection/java-collection-precautions-for-use.md) +- [Java 集合使用注意事项总结](./java/collection/java-collection-precautions-for-use.md) **源码分析**: -- [ArrayList 源码+扩容机制分析](./java/collection/arraylist-source-code.md) -- [HashMap(JDK1.8)源码+底层数据结构分析](./java/collection/hashmap-source-code.md) -- [ConcurrentHashMap 源码+底层数据结构分析](./java/collection/concurrent-hash-map-source-code.md) +- [ArrayList 核心源码+扩容机制分析](./java/collection/arraylist-source-code.md) +- [LinkedList 核心源码分析](./java/collection/linkedlist-source-code.md) +- [HashMap 核心源码+底层数据结构分析](./java/collection/hashmap-source-code.md) +- [ConcurrentHashMap 核心源码+底层数据结构分析](./java/collection/concurrent-hash-map-source-code.md) +- [LinkedHashMap 核心源码分析](./java/collection/linkedhashmap-source-code.md) +- [CopyOnWriteArrayList 核心源码分析](./java/collection/copyonwritearraylist-source-code.md) +- [ArrayBlockingQueue 核心源码分析](./java/collection/arrayblockingqueue-source-code.md) +- [PriorityQueue 核心源码分析](./java/collection/priorityqueue-source-code.md) +- [DelayQueue 核心源码分析](./java/collection/priorityqueue-source-code.md) ### IO - [IO 基础知识总结](./java/io/io-basis.md) - [IO 设计模式总结](./java/io/io-design-patterns.md) - [IO 模型详解](./java/io/io-model.md) +- [NIO 核心知识总结](./java/io/nio-basis.md) ### 并发 @@ -78,6 +72,8 @@ title: JavaGuide(Java学习&&面试指南) **重要知识点详解**: +- [乐观锁和悲观锁详解](./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) - [ThreadLocal 详解](./java/concurrent/threadlocal.md) @@ -112,6 +108,9 @@ JVM 这部分内容主要参考 [JVM 虚拟机规范-Java8](https://docs.oracle. - [Java 18 新特性概览](./java/new-features/java18.md) - [Java 19 新特性概览](./java/new-features/java19.md) - [Java 20 新特性概览](./java/new-features/java20.md) +- [Java 21 新特性概览](./java/new-features/java21.md) +- [Java 22 & 23 新特性概览](./java/new-features/java22-23.md) +- [Java 24 新特性概览](./java/new-features/java24.md) ## 计算机基础 @@ -174,6 +173,8 @@ JVM 这部分内容主要参考 [JVM 虚拟机规范-Java8](https://docs.oracle. 另外,[GeeksforGeeks](https://www.geeksforgeeks.org/fundamentals-of-algorithms/) 这个网站总结了常见的算法 ,比较全面系统。 +[![Banner](https://oss.javaguide.cn/xingqiu/xingqiu.png)](./about-the-author/zhishixingqiu-two-years.md) + ## 数据库 ### 基础 @@ -237,7 +238,8 @@ JVM 这部分内容主要参考 [JVM 虚拟机规范-Java8](https://docs.oracle. ### Maven -[Maven 核心概念总结](./tools/maven/maven-core-concepts.md) +- [Maven 核心概念总结](./tools/maven/maven-core-concepts.md) +- [Maven 最佳实践](./tools/maven/maven-best-practices.md) ### Gradle @@ -279,6 +281,7 @@ JVM 这部分内容主要参考 [JVM 虚拟机规范-Java8](https://docs.oracle. **重要知识点详解**: +- [IoC & AOP 详解(快速搞懂)](./system-design/framework/spring/ioc-and-aop.md) - [Spring 事务详解](./system-design/framework/spring/spring-transaction.md) - [Spring 中的设计模式详解](./system-design/framework/spring/spring-design-patterns-summary.md) - [SpringBoot 自动装配原理详解](./system-design/framework/spring/spring-boot-auto-assembly-principles.md) @@ -297,13 +300,12 @@ JVM 这部分内容主要参考 [JVM 虚拟机规范-Java8](https://docs.oracle. - [SSO 单点登录详解](./system-design/security/sso-intro.md) - [权限系统设计详解](./system-design/security/design-of-authority-system.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) ### 定时任务 @@ -346,7 +348,8 @@ JVM 这部分内容主要参考 [JVM 虚拟机规范-Java8](https://docs.oracle. ### 分布式锁 -[分布式锁常见知识点&面试题总结](./distributed-system/distributed-lock.md) +- [分布式锁介绍](https://javaguide.cn/distributed-system/distributed-lock.html) +- [分布式锁常见实现方案总结](https://javaguide.cn/distributed-system/distributed-lock-implementations.html) ### 分布式事务 @@ -358,18 +361,17 @@ JVM 这部分内容主要参考 [JVM 虚拟机规范-Java8](https://docs.oracle. ## 高性能 -### 数据库读写分离&分库分表 +### 数据库优化 -[数据库读写分离和分库分表常见知识点&面试题总结](./high-performance/read-and-write-separation-and-library-subtable.md) +- [数据库读写分离和分库分表](./high-performance/read-and-write-separation-and-library-subtable.md) +- [数据冷热分离](./high-performance/data-cold-hot-separation.md) +- [常见 SQL 优化手段总结](./high-performance/sql-optimization.md) +- [深度分页介绍及优化建议](./high-performance/deep-pagination-optimization.md) ### 负载均衡 [负载均衡常见知识点&面试题总结](./high-performance/load-balancing.md) -### SQL 优化 - -[常见 SQL 优化手段总结](./high-performance/sql-optimization.md) - ### CDN [CDN(内容分发网络)常见知识点&面试题总结](./high-performance/cdn.md) 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/interview-experience.md b/docs/interview-preparation/interview-experience.md index 623aa555e73..2b58e95df10 100644 --- a/docs/interview-preparation/interview-experience.md +++ b/docs/interview-preparation/interview-experience.md @@ -25,6 +25,6 @@ icon: experience 有很多同学要说了:“为什么不直接给出具体答案呢?”。主要原因有如下两点: 1. 参考资料解释的要更详细一些,还可以顺便让你把相关的知识点复习一下。 -2. 给出的参考资料基本都是我的原创,假如后续我想对面试问题的答案进行完善,就不需要挨个把之前的面经写的答案给修改了(面试中的很多问题都是比较类似的)。当然了,我的原创文章也不太可能覆盖到面试的每个点,部面试问题的答案,我是精选的其他技术博主写的优质文章,文章质量都很高。 +2. 给出的参考资料基本都是我的原创,假如后续我想对面试问题的答案进行完善,就不需要挨个把之前的面经写的答案给修改了(面试中的很多问题都是比较类似的)。当然了,我的原创文章也不太可能覆盖到面试的每个点,部分面试问题的答案,我是精选的其他技术博主写的优质文章,文章质量都很高。 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) diff --git a/docs/interview-preparation/key-points-of-interview.md b/docs/interview-preparation/key-points-of-interview.md index 6375c878100..c2101dc307a 100644 --- a/docs/interview-preparation/key-points-of-interview.md +++ b/docs/interview-preparation/key-points-of-interview.md @@ -1,25 +1,26 @@ --- -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 后端面试哪些知识点是重点? -**准备面试的时候,具体哪些知识点是重点呢?** +**准备面试的时候,具体哪些知识点是重点呢?如何把握重点?** 给你几点靠谱的建议: -1. Java 基础、集合、并发、MySQL、Redis、Spring、Spring Boot 这些 Java 后端开发必备的知识点。大厂以及中小厂的面试问的比较多的就是这些知识点(不信的话,你可以去多找一些面经看看)。我这里没有提到计算机基础相关的内容,这个会在下面提到。 -2. 你的项目经历涉及到的知识点,有水平的面试官都是会根据你的项目经历来问的。举个例子,你的项目经历使用了 Redis 来做限流,那 Redis 相关的八股文(比如 Redis 常见数据结构)以及限流相关的八股文(比如常见的限流算法)你就应该多花更多心思来搞懂!吃透!你把项目经历上的知识点吃透之后,再把你简历上哪些写熟练掌握的技术给吃透。最后,再去花时间准备其他知识点。 -3. 针对自身找工作的需求,你又可以适当地调整复习的重点。像中小厂一般问计算机基础比较少一些,有些大厂比如字节比较重视计算机基础尤其是算法。这样的话,如果你的目标是中小厂的话,计算机基础就准备面试来说不是那么重要了。如果复习时间不够的话,可以暂时先放放。 +1. Java 基础、集合、并发、MySQL、Redis 、Spring、Spring Boot 这些 Java 后端开发必备的知识点(MySQL + Redis >= Java > Spring + Spring Boot)。大厂以及中小厂的面试问的比较多的就是这些知识点。Spring 和 Spring Boot 这俩框架类的知识点相对前面的知识点来说重要性要稍低一些,但一般面试也会问一些,尤其是中小厂。并发知识一般中大厂提问更多也更难,尤其是大厂喜欢深挖底层,很容易把人问倒。计算机基础相关的内容会在下面提到。 +2. 你的项目经历涉及到的知识点是重中之重,有水平的面试官都是会根据你的项目经历来问的。举个例子,你的项目经历使用了 Redis 来做限流,那 Redis 相关的八股文(比如 Redis 常见数据结构)以及限流相关的八股文(比如常见的限流算法)你就应该多花更多心思来搞懂吃透!你把项目经历上的知识点吃透之后,再把你简历上哪些写熟练掌握的技术给吃透,最后再去花时间准备其他知识点。 +3. 针对自身找工作的需求,你又可以适当地调整复习的重点。像中小厂一般问计算机基础比较少一些,有些大厂比如字节比较重视计算机基础尤其是算法。这样的话,如果你的目标是中小厂的话,计算机基础就准备面试来说不是那么重要了。如果复习时间不够的话,可以暂时先放放,腾出时间给其他重要的知识点。 4. 一般校招的面试不会强制要求你会分布式/微服务、高并发的知识(不排除个别岗位有这方面的硬性要求),所以到底要不要掌握还是要看你个人当前的实际情况。如果你会这方面的知识的话,对面试相对来说还是会更有利一些(想要让项目经历有亮点,还是得会一些性能优化的知识。性能优化的知识这也算是高并发知识的一个小分支了)。如果你的技能介绍或者项目经历涉及到分布式/微服务、高并发的知识,那建议你尽量也要抽时间去认真准备一下,面试中很可能会被问到,尤其是项目经历用到的时候。不过,也还是主要准备写在简历上的那些知识点就好。 -5. JVM 相关的知识点,一般是大厂才会问到,面试中小厂就没必要准备了。JVM 面试中比较常问的是 [Java 内存区域](https://javaguide.cn/java/jvm/memory-area.html)、[JVM 垃圾回收](https://javaguide.cn/java/jvm/jvm-garbage-collection.html)、[类加载器和双亲委派模型](https://javaguide.cn/java/jvm/classloader.html) 以及 JVM 调优和问题排查(我之前分享过一些[常见的线上问题案例](https://t.zsxq.com/0bsAac47U),里面就有 JVM 相关的)。 +5. JVM 相关的知识点,一般是大厂(例如美团、阿里)和一些不错的中厂(例如携程、顺丰、招银网络)才会问到,面试国企、差一点的中厂和小厂就没必要准备了。JVM 面试中比较常问的是 [Java 内存区域](https://javaguide.cn/java/jvm/memory-area.html)、[JVM 垃圾回收](https://javaguide.cn/java/jvm/jvm-garbage-collection.html)、[类加载器和双亲委派模型](https://javaguide.cn/java/jvm/classloader.html) 以及 JVM 调优和问题排查(我之前分享过一些[常见的线上问题案例](https://t.zsxq.com/0bsAac47U),里面就有 JVM 相关的)。 6. 不同的大厂面试侧重点也会不同。比如说你要去阿里这种公司的话,项目和八股文就是重点,阿里笔试一般会有代码题,进入面试后就很少问代码题了,但是对原理性的问题问的比较深,经常会问一些你对技术的思考。再比如说你要面试字节这种公司,那计算机基础,尤其是算法是重点,字节的面试十分注重代码功底,有时候开始面试就会直接甩给你一道代码题,写出来再谈别的。也会问面试八股文,以及项目,不过,相对来说要少很多。建议你看一下这篇文章 [为了解开互联网大厂秋招内幕,我把他们全面了一遍](https://mp.weixin.qq.com/s/pBsGQNxvRupZeWt4qZReIA),了解一下常见大厂的面试题侧重点。 +7. 多去找一些面经看看,尤其你目标公司或者类似公司对应岗位的面经。这样可以实现针对性的复习,还能顺便自测一波,检查一下自己的掌握情况。 看似 Java 后端八股文很多,实际把复习范围一缩小,重要的东西就是那些。考虑到时间问题,你不可能连一些比较冷门的知识点也给准备了。这没必要,主要精力先放在那些重要的知识点即可。 @@ -31,8 +32,12 @@ icon: star 举个例子:你的项目中需要用到 Redis 来做缓存,你对照着官网简单了解并实践了简单使用 Redis 之后,你去看了 Redis 对应的八股文。你发现 Redis 可以用来做限流、分布式锁,于是你去在项目中实践了一下并掌握了对应的八股文。紧接着,你又发现 Redis 内存不够用的情况下,还能使用 Redis Cluster 来解决,于是你就又去实践了一下并掌握了对应的八股文。 -**一定要记住你的主要目标是理解和记关键词,而不是像背课文一样一字一句地记下来!** +**一定要记住你的主要目标是理解和记关键词,而不是像背课文一样一字一句地记下来,这样毫无意义!效率最低,对自身帮助也最小!** -另外,记录博客或者用自己的理解把对应的知识点讲给别人听也是一个不错的选择。 +还要注意适当“投机取巧”,不要单纯死记八股,有些技术方案的实现有很多种,例如分布式 ID、分布式锁、幂等设计,想要完全记住所有方案不太现实,你就重点记忆你项目的实现方案以及选择该种实现方案的原因就好了。当然,其他方案还是建议你简单了解一下,不然也没办法和你选择的方案进行对比。 + +想要检测自己是否搞懂或者加深印象,记录博客或者用自己的理解把对应的知识点讲给别人听也是一个不错的选择。 + +另外,准备八股文的过程中,强烈建议你花个几个小时去根据你的简历(主要是项目经历部分)思考一下哪些地方可能被深挖,然后把你自己的思考以面试问题的形式体现出来。面试之后,你还要根据当下的面试情况复盘一波,对之前自己整理的面试问题进行完善补充。这个过程对于个人进一步熟悉自己的简历(尤其是项目经历)部分,非常非常有用。这些问题你也一定要多花一些时间搞懂吃透,能够流畅地表达出来。面试问题可以参考 [Java 面试常见问题总结(2024 最新版)](https://t.zsxq.com/0eRq7EJPy),记得根据自己项目经历去深入拓展即可! 最后,准备技术面试的同学一定要定期复习(自测的方式非常好),不然确实会遗忘的。 diff --git a/docs/interview-preparation/project-experience-guide.md b/docs/interview-preparation/project-experience-guide.md index b5f964e823f..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 互补,涵盖常见八股文(系统设计、常见框架、分布式、高并发 ……)、优质面经等内容。 ::: ## 没有项目经验怎么办? @@ -54,7 +54,7 @@ GitHub 或者码云上面有很多实战类别项目,你可以选择一个来 如果参加这种赛事能获奖的话,项目含金量非常高。即使没获奖也没啥,也可以写简历上。 -![阿里云天池大赛](https://oscimg.oschina.net/oscnet/up-673f598477242691900a1e72c5d8b26df2c.png) +![阿里云天池大赛](https://oss.javaguide.cn/xingqiu/up-673f598477242691900a1e72c5d8b26df2c.png) ### 参与实际项目 @@ -95,7 +95,7 @@ GitHub 或者码云上面有很多实战类别项目,你可以选择一个来 3. **数据库方面**:数据库设计可否优化?索引是否使用使用正确?SQL 语句是否可以优化?是否需要进行读写分离? 4. **缓存**:项目有没有哪些数据是经常被访问的?是否引入缓存来提高响应速度? 5. **安全**:项目是否存在安全问题? -6. ...... +6. …… 另外,我在星球分享过常见的性能优化方向实践案例,涉及到多线程、异步、索引、缓存等方向,强烈推荐你看看: 。 @@ -103,12 +103,12 @@ GitHub 或者码云上面有很多实战类别项目,你可以选择一个来 分析你的代码:右键项目-> Analyze->Inspect Code -![](https://oscimg.oschina.net/oscnet/up-651672bce128025a135c1536cd5dc00532e.png) +![](https://oss.javaguide.cn/xingqiu/up-651672bce128025a135c1536cd5dc00532e.png) 扫描完成之后,IDEA 会给出一些可能存在的代码坏味道比如命名问题。 -![](https://oscimg.oschina.net/oscnet/up-05c83b319941995b07c8020fddc57f26037.png) +![](https://oss.javaguide.cn/xingqiu/up-05c83b319941995b07c8020fddc57f26037.png) 并且,你还可以自定义检查规则。 -![](https://oscimg.oschina.net/oscnet/up-6b618ad3bad0bc3f76e6066d90c8cd2f255.png) +![](https://oss.javaguide.cn/xingqiu/up-6b618ad3bad0bc3f76e6066d90c8cd2f255.png) diff --git a/docs/interview-preparation/resume-guide.md b/docs/interview-preparation/resume-guide.md index f5fec4fb7ab..396ef4b47e4 100644 --- a/docs/interview-preparation/resume-guide.md +++ b/docs/interview-preparation/resume-guide.md @@ -1,11 +1,11 @@ --- -title: 程序员简历编写指南(重要) +title: 程序员简历编写指南 category: 面试准备 icon: jianli --- ::: tip 友情提示 -本文节选自 **[《Java 面试指北》](../zhuanlan/java-mian-shi-zhi-bei.md)**。这是一份教你如何更高效地准备面试的小册,涵盖常见八股文(系统设计、常见框架、分布式、高并发 ......)、优质面经等内容。 +本文节选自 **[《Java 面试指北》](../zhuanlan/java-mian-shi-zhi-bei.md)**。这是一份教你如何更高效地准备面试的小册,涵盖常见八股文(系统设计、常见框架、分布式、高并发 ……)、优质面经等内容。 ::: ## 前言 @@ -26,7 +26,7 @@ icon: jianli - 一般情况下你的简历上注明你会的东西才会被问到(Java 基础、集合、并发、MySQL、Redis 、Spring、Spring Boot 这些算是每个人必问的),比如写了你熟练使用 Redis,那面试官就很大概率会问你 Redis 的一些问题,再比如你写了你在项目中使用了消息队列,那面试官大概率问很多消息队列相关的问题。 - 技能熟练度在很大程度上也决定了面试官提问的深度。 -在不夸大自己能力的情况下,写出一份好的简历也是一项很棒的能力。 +在不夸大自己能力的情况下,写出一份好的简历也是一项很棒的能力。一般情况下,技术能力和学习能力比较厉害的,写出来的简历也比较棒! ## 简历模板 @@ -36,13 +36,13 @@ icon: jianli 下面是我收集的一些还不错的简历模板: -- 适合中文的简历模板收集(推荐,免费): -- 木及简历(部分收费) : -- 简单简历(付费): -- 站长简历: +- 适合中文的简历模板收集(推荐,开源免费): +- 木及简历(推荐,部分免费) : +- 简单简历(推荐,部分免费): +- 极简简历(免费): +- Markdown 简历排版工具(开源免费): +- 站长简历(收费,支持 AI 生成): - typora+markdown+css 自定义简历模板 : -- 极简简历 : -- Markdown 简历排版工具: - 超级简历(部分收费) : 上面这些简历模板大多是只有 1 页内容,很难展现足够的信息量。如果你不是顶级大牛(比如 ACM 大赛获奖)的话,我建议还是尽可能多写一点可以突出你自己能力的内容(校招生 2 页之内,社招生 3 页之内,记得精炼语言,不要过多废话)。 @@ -53,6 +53,10 @@ icon: jianli - 技术名词最好规范大小写比较好,比如 java->Java ,spring boot -> Spring Boot 。这个虽然有些面试官不会介意,但是很多面试官都会在意这个细节的。 - 中文和数字英文之间加上空格的话看起来会舒服一点。 +另外,知识星球里还有真实的简历模板可供参考,地址: (需加入[知识星球](https://javaguide.cn/about-the-author/zhishixingqiu-two-years.html)获取)。 + +![](https://oss.javaguide.cn/javamianshizhibei/image-20230918073550606.png) + ## 简历内容 ### 个人信息 @@ -105,7 +109,7 @@ icon: jianli ### 实习经历/工作经历(重要) -工作经历针对社招,实际经历针对校招。 +工作经历针对社招,实习经历针对校招。 工作经历建议采用时间倒序的方式来介绍。实习经历和工作经历都需要简单突出介绍自己在职期间主要做了什么。 @@ -138,6 +142,8 @@ icon: jianli 项目介绍尽量压缩在两行之内,不需要介绍太多,但也不要随便几个字就介绍完了。 +另外,个人收获和项目成果都是可选的,如果选择写的话,也不要花费太多篇幅,记住你的重点是介绍工作内容/个人职责。 + **2、技术架构直接写技术名词就行,不要再介绍技术是干嘛的了,没意义,属于无效介绍。** ![](https://oss.javaguide.cn/github/javaguide/interview-preparation/46c92fbc5160e65dd85c451143177144.png) @@ -154,17 +160,29 @@ icon: jianli - 使用 xxx 技术了优化了 xxx 接口,系统 QPS 从 xxx 提高到了 xxx。 - 使用 xxx 技术解决了 xxx 问题,查询速度优化了 xxx,系统 QPS 达到 10w+。 - 使用 xxx 技术优化了 xxx 模块,响应时间从 2s 降低到 0.2s。 -- ...... - -示例: - -- 使用 Sharding-JDBC 对 MySQL 数据库进行分库分表,优化千万级大表,单表数据量保持在 500w 以下。 +- …… + +个人职责介绍示例(这里只是举例,不要照搬,结合自己项目经历自己去写,不然面试的时候容易被问倒) : + +- 基于 Spring Cloud Gateway + Spring Security OAuth2 + JWT 实现微服务统一认证授权和鉴权,使用 RBAC 权限模型实现动态权限控制。 +- 参与项目订单模块的开发,负责订单创建、删除、查询等功能,基于 Spring 状态机实现订单状态流转。 +- 商品和订单搜索场景引入 Elasticsearch,并且实现了相关商品推荐以及搜索提示功能。 +- 整合 Canal + RabbitMQ 将 MySQL 增量数据(如商品、订单数据)同步到 Elasticsearch。 +- 利用 RabbitMQ 官方提供的延迟队列插件实现延时任务场景比如订单超时自动取消、优惠券过期提醒、退款处理。 +- 消息推送系统引入 RabbitMQ 实现异步处理、削峰填谷和服务解耦,最高推送速度 10w/s,单日最大消息量 2000 万。 +- 使用 MAT 工具分析 dump 文件解决了广告服务新版本上线后导致大量的服务超时告警的问题。 +- 排查并解决扣费模块由于扣费父任务和反作弊子任务使用同一个线程池导致的死锁问题。 +- 基于 EasyExcel 实现广告投放数据的导入导出,通过 MyBatis 批处理插入数据,基于任务表实现异步。 +- 负责用户统计模块的开发,使用 CompletableFuture 并行加载后台用户统计模块的数据信息,平均相应时间从 3.5s 降低到 1s。 +- 基于 Sentinel 对核心场景(如用户登入注册、收货地址查询等)进行限流、降级,保护系统,提升用户体验。 - 热门数据(如首页、热门博客)使用 Redis+Caffeine 两级缓存,解决了缓存击穿和穿透问题,查询速度毫秒级,QPS 30w+。 -- 使用 CompletableFuture 优化购物车查询模块,对获取用户信息、商品详情、优惠券信息等异步 RPC 调用进行编排,响应时间从 2s 降低 0.2s。 +- 使用 CompletableFuture 优化购物车查询模块,对获取用户信息、商品详情、优惠券信息等异步 RPC 调用进行编排,响应时间从 2s 降低为 0.2s。 +- 搭建 EasyMock 服务,用于模拟第三方平台接口,方便了在网络隔离情况下的接口对接工作。 +- 基于 SkyWalking + Elasticsearch 搭建分布式链路追踪系统实现全链路监控。 **4、如果你觉得你的项目技术比较落后的话,可以自己私下进行改进。重要的是让项目比较有亮点,通过什么方式就无所谓了。** -项目经历这部分对于简历来说非常重要,《Java 面试指北》的面试准备篇有好几篇关于优化项目经历的文章,建议你仔细阅读一下,应该会对你有帮助。 +项目经历这部分对于简历来说非常重要,[《Java 面试指北》](https://javaguide.cn/zhuanlan/java-mian-shi-zhi-bei.html)的面试准备篇有好几篇关于优化项目经历的文章,建议你仔细阅读一下,应该会对你有帮助。 ![](https://oss.javaguide.cn/zhishixingqiu/4e11dbc842054e53ad6c5f0445023eb5~tplv-k3u1fbpfcp-zoom-1.png) @@ -172,6 +190,10 @@ icon: jianli ![](https://oss.javaguide.cn/zhishixingqiu/image-20230424222513028.png) +**6、避免模糊性描述,介绍要具体(技术+场景+效果),也要注意精简语言(避免堆砌技术词,省略不必要的描述)。** + +![](https://oss.javaguide.cn/github/javaguide/interview-preparation/project-experience-avoiding-ambiguity-descriptio.png) + ### 荣誉奖项(可选) 如果你有含金量比较高的竞赛(比如 ACM、阿里的天池大赛)的获奖经历的话,荣誉奖项这块内容一定要写一下!并且,你还可以将荣誉奖项这块内容适当往前放,放在一个更加显眼的位置。 @@ -207,7 +229,7 @@ icon: jianli STAR 法则由下面 4 个单词组成(STAR 法则的名字就是由它们的首字母组成): - **Situation:** 情景。 事情是在什么情况下发生的? -- **Task::** 任务。你的任务是什么? +- **Task:** 任务。你的任务是什么? - **Action:** 行动。你做了什么? - **Result:** 结果。最终的结果怎样? @@ -255,3 +277,19 @@ FAB 法则由下面 3 个单词组成(FAB 法则的名字就是由它们的首 - 项目经历建议以时间倒序排序,另外项目经历不在于多(精选 2~3 即可),而在于有亮点。 - 准备面试的过程中应该将你写在简历上的东西作为重点,尤其是项目经历上和技能介绍上的。 - 面试和工作是两回事,聪明的人会把面试官往自己擅长的领域领,其他人则被面试官牵着鼻子走。虽说面试和工作是两回事,但是你要想要获得自己满意的 offer ,你自身的实力必须要强。 + +## 简历修改 + +到目前为止,我至少帮助 **6000+** 位球友提供了免费的简历修改服务。由于个人精力有限,修改简历仅限加入星球的读者,需要帮看简历的话,可以加入 [**JavaGuide 官方知识星球**](https://javaguide.cn/about-the-author/zhishixingqiu-two-years.html#%E7%AE%80%E5%8E%86%E4%BF%AE%E6%94%B9)(点击链接查看详细介绍)。 + +![img](https://oss.javaguide.cn/xingqiu/%E7%AE%80%E5%8E%86%E4%BF%AE%E6%94%B92.jpg) + +虽然收费只有培训班/训练营的百分之一,但是知识星球里的内容质量更高,提供的服务也更全面,非常适合准备 Java 面试和学习 Java 的同学。 + +下面是星球提供的部分服务(点击下方图片即可获取知识星球的详细介绍): + +[![星球服务](https://oss.javaguide.cn/xingqiu/xingqiufuwu.png)](../about-the-author/zhishixingqiu-two-years.md) + +这里再提供一份限时专属优惠卷: + +![知识星球30元优惠卷](https://oss.javaguide.cn/xingqiu/xingqiuyouhuijuan-30.jpg) 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 0fb6e5be442..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 互补,涵盖常见八股文(系统设计、常见框架、分布式、高并发 ……)、优质面经等内容。 ::: 你的身边一定有很多编程比你厉害但是找的工作并没有你好的朋友!**技术面试不同于编程,编程厉害不代表技术面试就一定能过。** @@ -29,14 +29,14 @@ icon: path 你会发现大厂面试你会用到,以后工作之后你也会用到。我分别列举 2 个例子吧! - **面试中**:像字节、腾讯这些大厂的技术面试以及几乎所有公司的笔试都会考操作系统相关的问题。 -- **工作中**:在实际使用缓存的时候,你会发现在操作系统中可以找到很多缓存思想的影子。比如 CPU Cache 缓存的是内存数据用于解决 CPU 处理速度和内存不匹配的问题,内存缓存的是硬盘数据用于解决硬盘访问速度过慢的问题。再比如操作系统在页表方案基础之上引入了快表来加速虚拟地址到物理地址的转换。我们可以把快表理解为一种特殊的高速缓冲存储器(Cache)。 +- **工作中**:在实际使用缓存的时候,软件层次而言的缓存思想,则是源自数据库速度、Redis(内存中间件)速度、本地内存速度之间的不匹配;而在计算机存储层次结构设计中,我们也能发现同样的问题及缓存思想的使用:内存用于解决磁盘访问速度过慢的问题,CPU 用三级缓存缓解寄存器和内存之间的速度差异。它们面临的都是同一个问题(速度不匹配)和同一个思想,那么计算机先驱者在存储层次结构设计上对缓存性能的优化措施,同样也适用于软件层次缓存的性能优化。 **如何求职为导向学习呢?** 简答来说就是:根据招聘要求整理一份目标岗位的技能清单,然后按照技能清单去学习和提升。 1. 你首先搞清楚自己要找什么工作 2. 然后根据招聘岗位的要求梳理一份技能清单 3. 根据技能清单写好最终的简历 -4. 最后再按照建立的要求去学习和提升。 +4. 最后再按照简历的要求去学习和提升。 这其实也是 **以终为始** 思想的运用。 @@ -61,14 +61,16 @@ icon: path 下面是常见的获取招聘信息的渠道: - **目标企业的官网/公众号**:最及时最权威的获取招聘信息的途径。 -- **招聘网站**:[BOSS 直聘](https://www.zhipin.com/)、[智联招聘](https://www.zhaopin.com/)、[拉勾招聘](https://www.lagou.com/)......。 +- **招聘网站**:[BOSS 直聘](https://www.zhipin.com/)、[智联招聘](https://www.zhaopin.com/)、[拉勾招聘](https://www.lagou.com/)……。 - **牛客网**:每年秋招/春招,都会有大批量的公司会到牛客网发布招聘信息,并且还会有大量的公司员工来到这里发内推的帖子。地址: 。 - **超级简历**:超级简历目前整合了各大企业的校园招聘入口,地址: - **认识的朋友**:如果你有认识的朋友在目标企业工作的话,你也可以找他们了解招聘信息,并且可以让他们帮你内推。 - **宣讲会**:宣讲会也是一个不错的途径,不过,好的企业通常只会去比较好的学校,可以留意一下意向公司的宣讲会安排或者直接去到一所比较好的学校参加宣讲会。像我当时校招就去参加了几场宣讲会。不过,我是在荆州上学,那边没什么比较好的学校,一般没有公司去开宣讲会。所以,我当时是直接跑到武汉来了,参加了武汉理工大学以及华中科技大学的几场宣讲会。总体感觉还是很不错的! - **其他**:校园就业信息网、学校论坛、班级 or 年级 QQ 群。 -校招的话,建议以官网为准,有宣讲会、靠谱一点的内推的话更好。社招的话,可以多留意一下各大招聘网站比如 BOSS 直聘、拉勾上的职位信息,也可以找被熟人内推,获得面试机会的概率更大一些,进度一般也更快一些。 +校招的话,建议以官网为准,有宣讲会的话更好。社招的话,可以多留意一下各大招聘网站比如 BOSS 直聘、拉勾上的职位信息。 + +不论校招和社招,如果能找到比较靠谱的内推机会的话,获得面试的机会的概率还是非常大的。而且,你可以让内推你的人定向地给你一些建议。找内推的方式有很多,首选比较熟悉的朋友、同学,还可以留意技术交流社区和公众号上的内推信息。 一般是只能投递一个岗位,不过,也有极少数投递不同部门两个岗位的情况,这个应该不会有影响,但你的前一次面试情况可能会被记录,也就是说就算你投递成功两个岗位,第一个岗位面试失败的话,对第二个岗位也会有影响,很可能直接就被 pass。 @@ -113,6 +115,8 @@ icon: path - 技能介绍太杂,没有亮点。不需要全才,某个领域做得好就行了! - 对 Java 后台开发的部分技能比如 Spring Boot 的熟悉度仅仅为了解,无法满足企业的要求。 +详细的程序员简历编写指南请参考:[程序员简历到底该怎么写?](https://javaguide.cn/interview-preparation/resume-guide.html)。 + ## 岗位匹配度很重要 校招通常会对你的项目经历的研究方向比较宽容,即使你的项目经历和对应公司的具体业务没有关系,影响其实也并不大。 @@ -121,67 +125,34 @@ icon: path 不过,这个也并不绝对,也有一些公司在招聘的时候更看重的是你的过往经历,较少地关注岗位匹配度,优秀公司的工作经历以及有亮点的项目经验都是加分项。这类公司相信你既然在某个领域(比如电商、支付)已经做的不错了,那应该也可以在另外一个领域(比如流媒体平台、社交软件)很快成为专家。这个领域指的不是技术领域,更多的是业务方向。横跨技术领域(比如后端转算法、后端转大数据)找工作,你又没有相关的经验,几乎是没办法找到的。即使找到了,也大概率会面临 HR 压薪资的问题。 -## 提前准备技术面试和手撕算法 +## 提前准备技术面试 -面试之前一定要提前准备一下常见的面试题: +面试之前一定要提前准备一下常见的面试题也就是八股文: - 自己面试中可能涉及哪些知识点、那些知识点是重点。 -- 面试中哪些问题会被经常问到、面试中自己改如何回答。(强烈不推荐死记硬背,第一:通过背这种方式你能记住多少?能记住多久?第二:背题的方式的学习很难坚持下去!) - -这块内容只会介绍面试大概会涉及到哪方面的知识点,具体这些知识点涵盖哪些问题,后面的文章有介绍到。 - -**Java** : - -- Java 基础 -- Java 集合 -- Java 并发 -- JVM - -**计算机基础**: +- 面试中哪些问题会被经常问到、面试中自己该如何回答。(强烈不推荐死记硬背,第一:通过背这种方式你能记住多少?能记住多久?第二:背题的方式的学习很难坚持下去!) -- 算法 -- 数据结构 -- 计算机网络 -- 操作系统 +Java 后端面试复习的重点请看这篇文章:[Java 面试重点总结(重要)](https://javaguide.cn/interview-preparation/key-points-of-interview.html)。 -**数据库**: - -- MySQL -- Redis - -**常用框架**: - -- Spring -- SpringBoot -- MyBatis -- Netty -- Zookeeper -- Dubbo +不同类型的公司对于技能的要求侧重点是不同的比如腾讯、字节可能更重视计算机基础比如网络、操作系统这方面的内容。阿里、美团这种可能更重视你的项目经历、实战能力。 -**分布式** : +一定不要抱着一种思想,觉得八股文或者基础问题的考查意义不大。如果你抱着这种思想复习的话,那效果可能不会太好。实际上,个人认为还是很有意义的,八股文或者基础性的知识在日常开发中也会需要经常用到。例如,线程池这块的拒绝策略、核心参数配置什么的,如果你不了解,实际项目中使用线程池可能就用的不是很明白,容易出现问题。而且,其实这种基础性的问题是最容易准备的,像各种底层原理、系统设计、场景题以及深挖你的项目这类才是最难的! -- CAP 理论 和 BASE 理论、Paxos 算法和 Raft 算法 -- RPC -- 分布式事务 -- 分布式 ID +八股文资料首推我的 [《Java 面试指北》](https://t.zsxq.com/11rZ6D7Wk) (配合 JavaGuide 使用,会根据每一年的面试情况对内容进行更新完善)和 [JavaGuide](https://javaguide.cn/) 。里面不仅仅是原创八股文,还有很多对实际开发有帮助的干货。除了我的资料之外,你还可以去网上找一些其他的优质的文章、视频来看。 -**高并发**: +![《Java 面试指北》内容概览](https://oss.javaguide.cn/javamianshizhibei/javamianshizhibei-content-overview.png) -- 消息队列 -- 读写分离&分库分表 -- 负载均衡 +## 提前准备手撕算法 -**高可用**: +很明显,国内现在的校招面试开始越来越重视算法了,尤其是像字节跳动、腾讯这类大公司。绝大部分公司的校招笔试是有算法题的,如果 AC 率比较低的话,基本就挂掉了。 -- 限流 -- 降级 -- 熔断 +社招的话,算法面试同样会有。不过,面试官可能会更看重你的工程能力,你的项目经历。如果你的其他方面都很优秀,但是算法很菜的话,不一定会挂掉。不过,还是建议刷下算法题,避免让其成为自己在面试中的短板。 -![](https://oss.javaguide.cn/github/javaguide/interview-preparation/20210414112925296.png) +社招往往是在技术面试的最后,面试官给你一个算法题目让你做。 -不同类型的公司对于技能的要求侧重点是不同的比如腾讯、字节可能更重视计算机基础比如网络、操作系统这方面的内容。阿里、美团这种可能更重视你的项目经历、实战能力。 +关于如何准备算法面试[《Java 面试指北》](https://javaguide.cn/zhuanlan/java-mian-shi-zhi-bei.html) 的面试准备篇有详细介绍到。 -关于如何准备算法面试请看《Java 面试指北》的「面试准备篇」中对应的文章。 +![《Java 面试指北》面试准备篇](https://oss.javaguide.cn/javamianshizhibei/preparation-for-interview.png) ## 提前准备自我介绍 @@ -198,7 +169,7 @@ icon: path - 如果你去过大公司实习,那对应的实习经历就是你的亮点。 - 如果你参加过技术竞赛,那竞赛经历就是你的亮点。 - 如果你大学就接触过企业级项目的开发,实战经验比较多,那这些项目经历就是你的亮点。 -- ...... +- …… 从社招和校招两个角度来举例子吧!我下面的两个例子仅供参考,自我介绍并不需要死记硬背,记住要说的要点,面试的时候根据公司的情况临场发挥也是没问题的。另外,网上一般建议的是准备好两份自我介绍:一份对 hr 说的,主要讲能突出自己的经历,会的编程技术一语带过;另一份对技术面试官说的,主要讲自己会的技术细节和项目经验。 @@ -226,9 +197,13 @@ icon: path ## 总结 -这篇文章内容有点多,如果这篇文章只能让你记住 4 句话,那请记住下面这 4 句: +这篇文章内容有点多,如果这篇文章只能让你记住 7 句话,那请记住下面这 7 句: 1. 一定要提前准备面试!技术面试不同于编程,编程厉害不代表技术面试就一定能过。 -2. 一定不要对面试抱有侥幸心理。打铁还需自身硬!千万不要觉得自己看几篇面经,看几篇面试题解析就能通过面试了。一定要静下心来深入学习! +2. 一定不要对面试抱有侥幸心理。打铁还需自身硬!千万不要觉得自己看几篇面经,看几篇面试题解析就能通过面试了。一定要静下心来深入学习!尤其是目标是大厂的同学,那更要深挖原理! 3. 建议大学生尽可能早一点以求职为导向来学习的。这样更有针对性,并且可以大概率减少自己处在迷茫的时间,很大程度上还可以让自己少走很多弯路。 但是,不要把“以求职为导向学习”理解为“我就不用学课堂上那些计算机基础课程了”! -4. 手撕算法是当下技术面试的标配,尽早准备! +4. 一定不要抱着一种思想,觉得八股文或者基础问题的考查意义不大。如果你抱着这种思想复习的话,那效果可能不会太好。实际上,个人认为还是很有意义的,八股文或者基础性的知识在日常开发中也会需要经常用到。例如,线程池这块的拒绝策略、核心参数配置什么的,如果你不了解,实际项目中使用线程池可能就用的不是很明白,容易出现问题。 +5. 手撕算法是当下技术面试的标配,尽早准备! +6. 岗位匹配度很重要。校招通常会对你的项目经历的研究方向比较宽容,即使你的项目经历和对应公司的具体业务没有关系,影响其实也并不大。社招的话就不一样了,毕竟公司是要招聘可以直接来干活的人,你有相关的经验,公司会比较省事。 + +7. 面试之后及时复盘。面试就像是一场全新的征程,失败和胜利都是平常之事。所以,劝各位不要因为面试失败而灰心、丧失斗志。也不要因为面试通过而沾沾自喜,等待你的将是更美好的未来,继续加油! diff --git a/docs/java/basis/bigdecimal.md b/docs/java/basis/bigdecimal.md index 8363fd7890f..acedfadc32f 100644 --- a/docs/java/basis/bigdecimal.md +++ b/docs/java/basis/bigdecimal.md @@ -21,7 +21,7 @@ System.out.println(a == b);// false **为什么浮点数 `float` 或 `double` 运算的时候会有精度丢失的风险呢?** -这个和计算机保存浮点数的机制有很大关系。我们知道计算机是二进制的,而且计算机在表示一个数字时,宽度是有限的,无限循环的小数存储在计算机时,只能被截断,所以就会导致小数精度发生损失的情况。这也就是解释了为什么浮点数没有办法用二进制精确表示。 +这个和计算机保存小数的机制有很大关系。我们知道计算机是二进制的,而且计算机在表示一个数字时,宽度是有限的,无限循环的小数存储在计算机时,只能被截断,所以就会导致小数精度发生损失的情况。这也就是解释了为什么十进制小数没有办法用二进制精确表示。 就比如说十进制下的 0.2 就没办法精确转换成二进制小数: @@ -40,9 +40,9 @@ System.out.println(a == b);// false ## BigDecimal 介绍 -`BigDecimal` 可以实现对浮点数的运算,不会造成精度丢失。 +`BigDecimal` 可以实现对小数的运算,不会造成精度丢失。 -通常情况下,大部分需要浮点数精确运算结果的业务场景(比如涉及到钱的场景)都是通过 `BigDecimal` 来做的。 +通常情况下,大部分需要小数精确运算结果的业务场景(比如涉及到钱的场景)都是通过 `BigDecimal` 来做的。 《阿里巴巴 Java 开发手册》中提到:**浮点数之间的等值判断,基本数据类型不能用 == 来比较,包装数据类型不能用 equals 来判断。** @@ -50,7 +50,7 @@ System.out.println(a == b);// false 具体原因我们在上面已经详细介绍了,这里就不多提了。 -想要解决浮点数运算精度丢失这个问题,可以直接使用 `BigDecimal` 来定义浮点数的值,然后再进行浮点数的运算操作即可。 +想要解决浮点数运算精度丢失这个问题,可以直接使用 `BigDecimal` 来定义小数的值,然后再进行小数的运算操作即可。 ```java BigDecimal a = new BigDecimal("1.0"); @@ -99,21 +99,21 @@ 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 - UP(BigDecimal.ROUND_UP), + // 2.4 -> 3 , 1.6 -> 2 + // -1.6 -> -2 , -2.4 -> -3 + UP(BigDecimal.ROUND_UP), + // 2.4 -> 2 , 1.6 -> 1 + // -1.6 -> -1 , -2.4 -> -2 + DOWN(BigDecimal.ROUND_DOWN), + // 2.4 -> 3 , 1.6 -> 2 + // -1.6 -> -1 , -2.4 -> -2 + CEILING(BigDecimal.ROUND_CEILING), // 2.5 -> 2 , 1.6 -> 1 - // -1.6 -> -1 , -2.5 -> -2 - DOWN(BigDecimal.ROUND_DOWN), - // 2.5 -> 3 , 1.6 -> 2 - // -1.6 -> -1 , -2.5 -> -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 - HALF_UP(BigDecimal.ROUND_HALF_UP), + FLOOR(BigDecimal.ROUND_FLOOR), + // 2.4 -> 2 , 1.6 -> 2 + // -1.6 -> -2 , -2.4 -> -2 + HALF_UP(BigDecimal.ROUND_HALF_UP), //...... } ``` @@ -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 除数 @@ -256,15 +256,15 @@ public class BigDecimalUtil { } BigDecimal b1 = BigDecimal.valueOf(v1); BigDecimal b2 = BigDecimal.valueOf(v2); - return b1.divide(b2, scale, RoundingMode.HALF_UP).doubleValue(); + return b1.divide(b2, scale, RoundingMode.HALF_EVEN).doubleValue(); } /** - * 提供精确的小数位四舍五入处理。 + * 提供精确的小数位四舍六入五成双处理。 * - * @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 返回转换结果 @@ -351,8 +351,14 @@ public class BigDecimalUtil { } ``` +相关 issue:[建议对保留规则设置为 RoundingMode.HALF_EVEN,即四舍六入五成双,#2129](https://github.com/Snailclimb/JavaGuide/issues/2129) 。 + +![RoundingMode.HALF_EVEN](https://oss.javaguide.cn/github/javaguide/java/basis/RoundingMode.HALF_EVEN.png) + ## 总结 浮点数没有办法用二进制精确表示,因此存在精度丢失的风险。 不过,Java 提供了`BigDecimal` 来操作浮点数。`BigDecimal` 的实现利用到了 `BigInteger` (用来操作大整数), 所不同的是 `BigDecimal` 加入了小数位的概念。 + + diff --git a/docs/java/basis/generics-and-wildcards.md b/docs/java/basis/generics-and-wildcards.md index 6f9be1fbc9a..bd9cd4b00bf 100644 --- a/docs/java/basis/generics-and-wildcards.md +++ b/docs/java/basis/generics-and-wildcards.md @@ -5,7 +5,7 @@ tag: - Java基础 --- -**泛型&通配符** 相关的面试题为我的[知识星球](https://javaguide.cn/about-the-author/zhishixingqiu-two-years.html)(点击链接即可查看详细介绍以及加入方法)专属内容,已经整理到了[《Java 面试指北》](hhttps://javaguide.cn/zhuanlan/java-mian-shi-zhi-bei.html)(点击链接即可查看详细介绍以及获取方法)中。 +**泛型&通配符** 相关的面试题为我的[知识星球](https://javaguide.cn/about-the-author/zhishixingqiu-two-years.html)(点击链接即可查看详细介绍以及加入方法)专属内容,已经整理到了[《Java 面试指北》](https://javaguide.cn/zhuanlan/java-mian-shi-zhi-bei.html)(点击链接即可查看详细介绍以及获取方法)中。 [《Java 面试指北》](hhttps://javaguide.cn/zhuanlan/java-mian-shi-zhi-bei.html) 的部分内容展示如下,你可以将其看作是 [JavaGuide](https://javaguide.cn/#/) 的补充完善,两者可以配合使用。 @@ -16,3 +16,5 @@ tag: ![](https://oss.javaguide.cn/xingqiu/image-20220211231206733.png) + + diff --git a/docs/java/basis/java-basic-questions-01.md b/docs/java/basis/java-basic-questions-01.md index 8f542b60565..49e00a73948 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基础常见知识点和面试题总结,希望对你有帮助! @@ -18,26 +18,39 @@ head: ### Java 语言有哪些特点? -1. 简单易学; +1. 简单易学(语法简单,上手容易); 2. 面向对象(封装,继承,多态); 3. 平台无关性( Java 虚拟机实现平台无关性); 4. 支持多线程( C++ 语言没有内置的多线程机制,因此必须调用操作系统的多线程功能来进行多线程程序设计,而 Java 语言却提供了多线程支持); -5. 可靠性; -6. 安全性; -7. 支持网络编程并且很方便( Java 语言诞生本身就是为简化网络编程设计的,因此 Java 语言不仅支持网络编程而且很方便); -8. 编译与解释并存; +5. 可靠性(具备异常处理和自动内存管理机制); +6. 安全性(Java 语言本身的设计就提供了多重安全防护机制如访问权限修饰符、限制程序直接访问操作系统资源); +7. 高效性(通过 Just In Time 编译器等技术的优化,Java 语言的运行效率还是非常不错的); +8. 支持网络编程并且很方便; +9. 编译与解释并存; +10. …… -> **🐛 修正(参见:[issue#544](https://github.com/Snailclimb/JavaGuide/issues/544))**:C++11 开始(2011 年的时候),C++就引入了多线程库,在 windows、linux、macos 都可以使用`std::thread`和`std::async`来创建线程。参考链接:http://www.cplusplus.com/reference/thread/thread/?kw=thread +> **🐛 修正(参见:[issue#544](https://github.com/Snailclimb/JavaGuide/issues/544))**:C++11 开始(2011 年的时候),C++就引入了多线程库,在 windows、linux、macos 都可以使用`std::thread`和`std::async`来创建线程。参考链接: 🌈 拓展一下: “Write Once, Run Anywhere(一次编写,随处运行)”这句宣传口号,真心经典,流传了好多年!以至于,直到今天,依然有很多人觉得跨平台是 Java 语言最大的优势。实际上,跨平台已经不是 Java 最大的卖点了,各种 JDK 新特性也不是。目前市面上虚拟化技术已经非常成熟,比如你通过 Docker 就很容易实现跨平台了。在我看来,Java 强大的生态才是! +### Java SE vs Java EE + +- Java SE(Java Platform,Standard Edition): Java 平台标准版,Java 编程语言的基础,它包含了支持 Java 应用程序开发和运行的核心类库以及虚拟机等核心组件。Java SE 可以用于构建桌面应用程序或简单的服务器应用程序。 +- Java EE(Java Platform, Enterprise Edition ):Java 平台企业版,建立在 Java SE 的基础上,包含了支持企业级应用程序开发和部署的标准和规范(比如 Servlet、JSP、EJB、JDBC、JPA、JTA、JavaMail、JMS)。 Java EE 可以用于构建分布式、可移植、健壮、可伸缩和安全的服务端 Java 应用程序,例如 Web 应用程序。 + +简单来说,Java SE 是 Java 的基础版本,Java EE 是 Java 的高级版本。Java SE 更适合开发桌面应用程序或简单的服务器应用程序,Java EE 更适合开发复杂的企业级应用程序或 Web 应用程序。 + +除了 Java SE 和 Java EE,还有一个 Java ME(Java Platform,Micro Edition)。Java ME 是 Java 的微型版本,主要用于开发嵌入式消费电子设备的应用程序,例如手机、PDA、机顶盒、冰箱、空调等。Java ME 无需重点关注,知道有这个东西就好了,现在已经用不上了。 + ### JVM vs JDK vs JRE #### JVM -Java 虚拟机(JVM)是运行 Java 字节码的虚拟机。JVM 有针对不同系统的特定实现(Windows,Linux,macOS),目的是使用相同的字节码,它们都会给出相同的结果。字节码和不同系统的 JVM 实现是 Java 语言“一次编译,随处可以运行”的关键所在。 +Java 虚拟机(Java Virtual Machine, JVM)是运行 Java 字节码的虚拟机。JVM 有针对不同系统的特定实现(Windows,Linux,macOS),目的是使用相同的字节码,它们都会给出相同的结果。字节码和不同系统的 JVM 实现是 Java 语言“一次编译,随处可以运行”的关键所在。 + +如下图所示,不同编程语言(Java、Groovy、Kotlin、JRuby、Clojure ...)通过各自的编译器编译成 `.class` 文件,并最终通过 JVM 在不同平台(Windows、Mac、Linux)上运行。 ![运行在 Java 虚拟机之上的编程语言](https://oss.javaguide.cn/github/javaguide/java/basis/java-virtual-machine-program-language-os.png) @@ -49,27 +62,49 @@ Java 虚拟机(JVM)是运行 Java 字节码的虚拟机。JVM 有针对不 #### JDK 和 JRE -JDK(Java Development Kit),它是功能齐全的 Java SDK,是提供给开发者使用的,能够创建和编译 Java 程序。他包含了 JRE,同时还包含了编译 java 源码的编译器 javac 以及一些其他工具比如 javadoc(文档注释工具)、jdb(调试器)、jconsole(基于 JMX 的可视化监控⼯具)、javap(反编译工具)等等。 +JDK(Java Development Kit)是一个功能齐全的 Java 开发工具包,供开发者使用,用于创建和编译 Java 程序。它包含了 JRE(Java Runtime Environment),以及编译器 javac 和其他工具,如 javadoc(文档生成器)、jdb(调试器)、jconsole(监控工具)、javap(反编译工具)等。 + +JRE 是运行已编译 Java 程序所需的环境,主要包含以下两个部分: + +1. **JVM** : 也就是我们上面提到的 Java 虚拟机。 +2. **Java 基础类库(Class Library)**:一组标准的类库,提供常用的功能和 API(如 I/O 操作、网络通信、数据结构等)。 + +简单来说,JRE 只包含运行 Java 程序所需的环境和类库,而 JDK 不仅包含 JRE,还包括用于开发和调试 Java 程序的工具。 + +如果需要编写、编译 Java 程序或使用 Java API 文档,就需要安装 JDK。某些需要 Java 特性的应用程序(如 JSP 转换为 Servlet 或使用反射)也可能需要 JDK 来编译和运行 Java 代码。因此,即使不进行 Java 开发工作,有时也可能需要安装 JDK。 -JRE(Java Runtime Environment) 是 Java 运行时环境。它是运行已编译 Java 程序所需的所有内容的集合,主要包括 Java 虚拟机(JVM)、Java 基础类库(Class Library)。 +下图清晰展示了 JDK、JRE 和 JVM 的关系。 -也就是说,JRE 是 Java 运行时环境,仅包含 Java 应用程序的运行时环境和必要的类库。而 JDK 则包含了 JRE,同时还包括了 javac、javadoc、jdb、jconsole、javap 等工具,可以用于 Java 应用程序的开发和调试。如果需要进行 Java 编程工作,比如编写和编译 Java 程序、使用 Java API 文档等,就需要安装 JDK。而对于某些需要使用 Java 特性的应用程序,如 JSP 转换为 Java Servlet、使用反射等,也需要 JDK 来编译和运行 Java 代码。因此,即使不打算进行 Java 应用程序的开发工作,也有可能需要安装 JDK。 +![jdk-include-jre](https://oss.javaguide.cn/github/javaguide/java/basis/jdk-include-jre.png) -![JDK 包含 JRE](https://oss.javaguide.cn/github/javaguide/java/basis/jdk-include-jre.png) +不过,从 JDK 9 开始,就不需要区分 JDK 和 JRE 的关系了,取而代之的是模块系统(JDK 被重新组织成 94 个模块)+ [jlink](http://openjdk.java.net/jeps/282) 工具 (随 Java 9 一起发布的新命令行工具,用于生成自定义 Java 运行时映像,该映像仅包含给定应用程序所需的模块) 。并且,从 JDK 11 开始,Oracle 不再提供单独的 JRE 下载。 + +在 [Java 9 新特性概览](https://javaguide.cn/java/new-features/java9.html)这篇文章中,我在介绍模块化系统的时候提到: + +> 在引入了模块系统之后,JDK 被重新组织成 94 个模块。Java 应用可以通过新增的 jlink 工具,创建出只包含所依赖的 JDK 模块的自定义运行时镜像。这样可以极大的减少 Java 运行时环境的大小。 + +也就是说,可以用 jlink 根据自己的需求,创建一个更小的 runtime(运行时),而不是不管什么应用,都是同样的 JRE。 + +定制的、模块化的 Java 运行时映像有助于简化 Java 应用的部署和节省内存并增强安全性和可维护性。这对于满足现代应用程序架构的需求,如虚拟化、容器化、微服务和云原生开发,是非常重要的。 ### 什么是字节码?采用字节码的好处是什么? -在 Java 中,JVM 可以理解的代码就叫做字节码(即扩展名为 `.class` 的文件),它不面向任何特定的处理器,只面向虚拟机。Java 语言通过字节码的方式,在一定程度上解决了传统解释型语言执行效率低的问题,同时又保留了解释型语言可移植的特点。所以, Java 程序运行时相对来说还是高效的(不过,和 C++,Rust,Go 等语言还是有一定差距的),而且,由于字节码并不针对一种特定的机器,因此,Java 程序无须重新编译便可在多种不同操作系统的计算机上运行。 +在 Java 中,JVM 可以理解的代码就叫做字节码(即扩展名为 `.class` 的文件),它不面向任何特定的处理器,只面向虚拟机。Java 语言通过字节码的方式,在一定程度上解决了传统解释型语言执行效率低的问题,同时又保留了解释型语言可移植的特点。所以, Java 程序运行时相对来说还是高效的(不过,和 C、 C++,Rust,Go 等语言还是有一定差距的),而且,由于字节码并不针对一种特定的机器,因此,Java 程序无须重新编译便可在多种不同操作系统的计算机上运行。 **Java 程序从源代码到运行的过程如下图所示**: ![Java程序转变为机器代码的过程](https://oss.javaguide.cn/github/javaguide/java/basis/java-code-to-machine-code.png) -我们需要格外注意的是 `.class->机器码` 这一步。在这一步 JVM 类加载器首先加载字节码文件,然后通过解释器逐行解释执行,这种方式的执行速度会相对比较慢。而且,有些方法和代码块是经常需要被调用的(也就是所谓的热点代码),所以后面引进了 JIT(just-in-time compilation) 编译器,而 JIT 属于运行时编译。当 JIT 编译器完成第一次编译后,其会将字节码对应的机器码保存下来,下次可以直接使用。而我们知道,机器码的运行效率肯定是高于 Java 解释器的。这也解释了我们为什么经常会说 **Java 是编译与解释共存的语言** 。 +我们需要格外注意的是 `.class->机器码` 这一步。在这一步 JVM 类加载器首先加载字节码文件,然后通过解释器逐行解释执行,这种方式的执行速度会相对比较慢。而且,有些方法和代码块是经常需要被调用的(也就是所谓的热点代码),所以后面引进了 **JIT(Just in Time Compilation)** 编译器,而 JIT 属于运行时编译。当 JIT 编译器完成第一次编译后,其会将字节码对应的机器码保存下来,下次可以直接使用。而我们知道,机器码的运行效率肯定是高于 Java 解释器的。这也解释了我们为什么经常会说 **Java 是编译与解释共存的语言** 。 + +> 🌈 拓展阅读: +> +> - [基本功 | Java 即时编译器原理解析及实践 - 美团技术团队](https://tech.meituan.com/2020/10/22/java-jit-practice-in-meituan.html) +> - [基于静态编译构建微服务应用 - 阿里巴巴中间件](https://mp.weixin.qq.com/s/4haTyXUmh8m-dBQaEzwDJw) ![Java程序转变为机器代码的过程](https://oss.javaguide.cn/github/javaguide/java/basis/java-code-to-machine-code-with-jit.png) -> HotSpot 采用了惰性评估(Lazy Evaluation)的做法,根据二八定律,消耗大部分系统资源的只有那一小部分的代码(热点代码),而这也就是 JIT 所需要编译的部分。JVM 会根据代码每次被执行的情况收集信息并相应地做出一些优化,因此执行的次数越多,它的速度就越快。JDK 9 引入了一种新的编译模式 AOT(Ahead of Time Compilation),它是直接将字节码编译成机器码,这样就避免了 JIT 预热等各方面的开销。JDK 支持分层编译和 AOT 协作使用。 +> HotSpot 采用了惰性评估(Lazy Evaluation)的做法,根据二八定律,消耗大部分系统资源的只有那一小部分的代码(热点代码),而这也就是 JIT 所需要编译的部分。JVM 会根据代码每次被执行的情况收集信息并相应地做出一些优化,因此执行的次数越多,它的速度就越快。 JDK、JRE、JVM、JIT 这四者的关系如下图所示。 @@ -79,12 +114,6 @@ JDK、JRE、JVM、JIT 这四者的关系如下图所示。 ![JVM 的大致结构模型](https://oss.javaguide.cn/github/javaguide/java/basis/jvm-rough-structure-model.png) -### 为什么不全部使用 AOT 呢? - -AOT 可以提前编译节省启动时间,那为什么不全部使用这种编译方式呢? - -长话短说,这和 Java 语言的动态特性有千丝万缕的联系了。举个例子,CGLIB 动态代理使用的是 ASM 技术,而这种技术大致原理是运行时直接在内存中生成并加载修改后的字节码文件也就是 `.class` 文件,如果全部使用 AOT 提前编译,也就不能使用 ASM 技术了。为了支持类似的动态特性,所以选择使用 JIT 即时编译器。 - ### 为什么说 Java 语言“编译与解释并存”? 其实这个问题我们讲字节码的时候已经提到过,因为比较重要,所以我们这里再提一下。 @@ -98,7 +127,7 @@ AOT 可以提前编译节省启动时间,那为什么不全部使用这种编 根据维基百科介绍: -> 为了改善编译语言的效率而发展出的[即时编译](https://zh.wikipedia.org/wiki/即時編譯)技术,已经缩小了这两种语言间的差距。这种技术混合了编译语言与解释型语言的优点,它像编译语言一样,先把程序源代码编译成[字节码](https://zh.wikipedia.org/wiki/字节码)。到执行期时,再将字节码直译,之后执行。[Java](https://zh.wikipedia.org/wiki/Java)与[LLVM](https://zh.wikipedia.org/wiki/LLVM)是这种技术的代表产物。 +> 为了改善解释语言的效率而发展出的[即时编译](https://zh.wikipedia.org/wiki/即時編譯)技术,已经缩小了这两种语言间的差距。这种技术混合了编译语言与解释型语言的优点,它像编译语言一样,先把程序源代码编译成[字节码](https://zh.wikipedia.org/wiki/字节码)。到执行期时,再将字节码直译,之后执行。[Java](https://zh.wikipedia.org/wiki/Java)与[LLVM](https://zh.wikipedia.org/wiki/LLVM)是这种技术的代表产物。 > > 相关阅读:[基本功 | Java 即时编译器原理解析及实践](https://tech.meituan.com/2020/10/22/java-jit-practice-in-meituan.html) @@ -106,6 +135,25 @@ AOT 可以提前编译节省启动时间,那为什么不全部使用这种编 这是因为 Java 语言既具有编译型语言的特征,也具有解释型语言的特征。因为 Java 程序要经过先编译,后解释两个步骤,由 Java 编写的程序需要先经过编译步骤,生成字节码(`.class` 文件),这种字节码必须由 Java 解释器来解释执行。 +### AOT 有什么优点?为什么不全部使用 AOT 呢? + +JDK 9 引入了一种新的编译模式 **AOT(Ahead of Time Compilation)** 。和 JIT 不同的是,这种编译模式会在程序被执行前就将其编译成机器码,属于静态编译(C、 C++,Rust,Go 等语言就是静态编译)。AOT 避免了 JIT 预热等各方面的开销,可以提高 Java 程序的启动速度,避免预热时间长。并且,AOT 还能减少内存占用和增强 Java 程序的安全性(AOT 编译后的代码不容易被反编译和修改),特别适合云原生场景。 + +**JIT 与 AOT 两者的关键指标对比**: + +JIT vs AOT + +可以看出,AOT 的主要优势在于启动时间、内存占用和打包体积。JIT 的主要优势在于具备更高的极限处理能力,可以降低请求的最大延迟。 + +提到 AOT 就不得不提 [GraalVM](https://www.graalvm.org/) 了!GraalVM 是一种高性能的 JDK(完整的 JDK 发行版本),它可以运行 Java 和其他 JVM 语言,以及 JavaScript、Python 等非 JVM 语言。 GraalVM 不仅能提供 AOT 编译,还能提供 JIT 编译。感兴趣的同学,可以去看看 GraalVM 的官方文档:。如果觉得官方文档看着比较难理解的话,也可以找一些文章来看看,比如: + +- [基于静态编译构建微服务应用](https://mp.weixin.qq.com/s/4haTyXUmh8m-dBQaEzwDJw) +- [走向 Native 化:Spring&Dubbo AOT 技术示例与原理讲解](https://cn.dubbo.apache.org/zh-cn/blog/2023/06/28/%e8%b5%b0%e5%90%91-native-%e5%8c%96springdubbo-aot-%e6%8a%80%e6%9c%af%e7%a4%ba%e4%be%8b%e4%b8%8e%e5%8e%9f%e7%90%86%e8%ae%b2%e8%a7%a3/) + +**既然 AOT 这么多优点,那为什么不全部使用这种编译方式呢?** + +我们前面也对比过 JIT 与 AOT,两者各有优点,只能说 AOT 更适合当下的云原生场景,对微服务架构的支持也比较友好。除此之外,AOT 编译无法支持 Java 的一些动态特性,如反射、动态代理、动态加载、JNI(Java Native Interface)等。然而,很多框架和库(如 Spring、CGLIB)都用到了这些特性。如果只使用 AOT 编译,那就没办法使用这些框架和库了,或者说需要针对性地去做适配和优化。举个例子,CGLIB 动态代理使用的是 ASM 技术,而这种技术大致原理是运行时直接在内存中生成并加载修改后的字节码文件也就是 `.class` 文件,如果全部使用 AOT 提前编译,也就不能使用 ASM 技术了。为了支持类似的动态特性,所以选择使用 JIT 即时编译器。 + ### Oracle JDK vs OpenJDK 可能在看这个问题之前很多人和我一样并没有接触和使用过 OpenJDK 。那么 Oracle JDK 和 OpenJDK 之间是否存在重大差异?下面我通过收集到的一些资料,为你解答这个被很多人忽视的问题。 @@ -132,11 +180,9 @@ AOT 可以提前编译节省启动时间,那为什么不全部使用这种编 > > 答: > -> 1. OpenJDK 是开源的,开源意味着你可以对它根据你自己的需要进行修改、优化,比如 Alibaba 基于 OpenJDK 开发了 Dragonwell8:[https://github.com/alibaba/dragonwell8](https://github.com/alibaba/dragonwell8) -> -> 2. OpenJDK 是商业免费的(这也是为什么通过 yum 包管理器上默认安装的 JDK 是 OpenJDK 而不是 Oracle JDK)。虽然 Oracle JDK 也是商业免费(比如 JDK 8),但并不是所有版本都是免费的。 -> -> 3. OpenJDK 更新频率更快。Oracle JDK 一般是每 6 个月发布一个新版本,而 OpenJDK 一般是每 3 个月发布一个新版本。(现在你知道为啥 Oracle JDK 更稳定了吧,先在 OpenJDK 试试水,把大部分问题都解决掉了才在 Oracle JDK 上发布) +> 1. OpenJDK 是开源的,开源意味着你可以对它根据你自己的需要进行修改、优化,比如 Alibaba 基于 OpenJDK 开发了 Dragonwell8:[https://github.com/alibaba/dragonwell8](https://github.com/alibaba/dragonwell8) +> 2. OpenJDK 是商业免费的(这也是为什么通过 yum 包管理器上默认安装的 JDK 是 OpenJDK 而不是 Oracle JDK)。虽然 Oracle JDK 也是商业免费(比如 JDK 8),但并不是所有版本都是免费的。 +> 3. OpenJDK 更新频率更快。Oracle JDK 一般是每 6 个月发布一个新版本,而 OpenJDK 一般是每 3 个月发布一个新版本。(现在你知道为啥 Oracle JDK 更稳定了吧,先在 OpenJDK 试试水,把大部分问题都解决掉了才在 Oracle JDK 上发布) > > 基于以上这些原因,OpenJDK 还是有存在的必要的! @@ -151,8 +197,6 @@ AOT 可以提前编译节省启动时间,那为什么不全部使用这种编 - BCL 协议(Oracle Binary Code License Agreement):可以使用 JDK(支持商用),但是不能进行修改。 - OTN 协议(Oracle Technology Network License Agreement):11 及之后新发布的 JDK 用的都是这个协议,可以自己私下用,但是商用需要付费。 -![](https://oscimg.oschina.net/oscnet/up-5babce06ef8fad5c4df5d7a6cf53d4a7901.png) - ### Java 和 C++ 的区别? 我知道很多人没学过 C++,但是面试官就是没事喜欢拿咱们 Java 和 C++ 比呀!没办法!!!就算没学过 C++,也要记下来。 @@ -163,7 +207,7 @@ AOT 可以提前编译节省启动时间,那为什么不全部使用这种编 - Java 的类是单继承的,C++ 支持多重继承;虽然 Java 的类不可以多继承,但是接口可以多继承。 - Java 有自动内存管理垃圾回收机制(GC),不需要程序员手动释放无用内存。 - C ++同时支持方法重载和操作符重载,但是 Java 只支持方法重载(操作符重载增加了复杂性,这与 Java 最初的设计思想不符)。 -- ...... +- …… ## 基本语法 @@ -171,8 +215,6 @@ AOT 可以提前编译节省启动时间,那为什么不全部使用这种编 Java 中的注释有三种: -![Java 注释类型总结](https://oss.javaguide.cn/github/javaguide/java/basis/java-annotation-types.png) - 1. **单行注释**:通常用于解释方法内某单行代码的作用。 2. **多行注释**:通常用于解释一段代码的作用。 @@ -210,7 +252,7 @@ Java 中的注释有三种: 在我们编写程序的时候,需要大量地为程序、类、变量、方法等取名字,于是就有了 **标识符** 。简单来说, **标识符就是一个名字** 。 -有一些标识符,Java 语言已经赋予了其特殊的含义,只能用于特定的地方,这些特殊的标识符就是 **关键字** 。简单来说,**关键字是被赋予特殊含义的标识**符 。比如,在我们的日常生活中,如果我们想要开一家店,则要给这个店起一个名字,起的这个“名字”就叫标识符。但是我们店的名字不能叫“警察局”,因为“警察局”这个名字已经被赋予了特殊的含义,而“警察局”就是我们日常生活中的关键字。 +有一些标识符,Java 语言已经赋予了其特殊的含义,只能用于特定的地方,这些特殊的标识符就是 **关键字** 。简单来说,**关键字是被赋予特殊含义的标识符** 。比如,在我们的日常生活中,如果我们想要开一家店,则要给这个店起一个名字,起的这个“名字”就叫标识符。但是我们店的名字不能叫“警察局”,因为“警察局”这个名字已经被赋予了特殊的含义,而“警察局”就是我们日常生活中的关键字。 ### Java 语言关键字有哪些? @@ -242,9 +284,26 @@ Java 中的注释有三种: ### 自增自减运算符 -在写代码的过程中,常见的一种情况是需要某个整数类型变量增加 1 或减少 1,Java 提供了一种特殊的运算符,用于这种表达式,叫做自增运算符(++)和自减运算符(--)。 +在写代码的过程中,常见的一种情况是需要某个整数类型变量增加 1 或减少 1。Java 提供了自增运算符 (`++`) 和自减运算符 (`--`) 来简化这种操作。 + +`++` 和 `--` 运算符可以放在变量之前,也可以放在变量之后: + +- **前缀形式**(例如 `++a` 或 `--a`):先自增/自减变量的值,然后再使用该变量,例如,`b = ++a` 先将 `a` 增加 1,然后把增加后的值赋给 `b`。 +- **后缀形式**(例如 `a++` 或 `a--`):先使用变量的当前值,然后再自增/自减变量的值。例如,`b = a++` 先将 `a` 的当前值赋给 `b`,然后再将 `a` 增加 1。 -++ 和 -- 运算符可以放在变量之前,也可以放在变量之后,当运算符放在变量之前时(前缀),先自增/减,再赋值;当运算符放在变量之后时(后缀),先赋值,再自增/减。例如,当 `b = ++a` 时,先自增(自己增加 1),再赋值(赋值给 b);当 `b = a++` 时,先赋值(赋值给 b),再自增(自己增加 1)。也就是,++a 输出的是 a+1 的值,a++输出的是 a 值。用一句口诀就是:“符号在前就先加/减,符号在后就后加/减”。 +为了方便记忆,可以使用下面的口诀:**符号在前就先加/减,符号在后就后加/减**。 + +下面来看一个考察自增自减运算符的高频笔试题:执行下面的代码后,`a` 、`b` 、 `c` 、`d`和`e`的值是? + +```java +int a = 9; +int b = a++; +int c = ++a; +int d = c--; +int e = --d; +``` + +答案:`a = 11` 、`b = 9` 、 `c = 10` 、 `d = 10` 、 `e = 10`。 ### 移位运算符 @@ -263,18 +322,29 @@ static final int hash(Object key) { ``` -在 Java 代码里使用 `<<`、 `>>` 和`>>>`转换成的指令码运行起来会更高效些。 +**使用移位运算符的主要原因**: + +1. **高效**:移位运算符直接对应于处理器的移位指令。现代处理器具有专门的硬件指令来执行这些移位操作,这些指令通常在一个时钟周期内完成。相比之下,乘法和除法等算术运算在硬件层面上需要更多的时钟周期来完成。 +2. **节省内存**:通过移位操作,可以使用一个整数(如 `int` 或 `long`)来存储多个布尔值或标志位,从而节省内存。 + +移位运算符最常用于快速乘以或除以 2 的幂次方。除此之外,它还在以下方面发挥着重要作用: + +- **位字段管理**:例如存储和操作多个布尔值。 +- **哈希算法和加密解密**:通过移位和与、或等操作来混淆数据。 +- **数据压缩**:例如霍夫曼编码通过移位运算符可以快速处理和操作二进制数据,以生成紧凑的压缩格式。 +- **数据校验**:例如 CRC(循环冗余校验)通过移位和多项式除法生成和校验数据完整性。 +- **内存对齐**:通过移位操作,可以轻松计算和调整数据的对齐地址。 掌握最基本的移位运算符知识还是很有必要的,这不光可以帮助我们在代码中使用,还可以帮助我们理解源码中涉及到移位运算符的代码。 Java 中有三种移位运算符: -![Java 移位运算符总结](https://oss.javaguide.cn/github/javaguide/java/basis/shift-operator.png) - -- `<<` :左移运算符,向左移若干位,高位丢弃,低位补零。`x << 1`,相当于 x 乘以 2(不溢出的情况下)。 -- `>>` :带符号右移,向右移若干位,高位补符号位,低位丢弃。正数高位补 0,负数高位补 1。`x >> 1`,相当于 x 除以 2。 +- `<<` :左移运算符,向左移若干位,高位丢弃,低位补零。`x << n`,相当于 x 乘以 2 的 n 次方(不溢出的情况下)。 +- `>>` :带符号右移,向右移若干位,高位补符号位,低位丢弃。正数高位补 0,负数高位补 1。`x >> n`,相当于 x 除以 2 的 n 次方。 - `>>>` :无符号右移,忽略符号位,空位都以 0 补齐。 +虽然移位运算本质上可以分为左移和右移,但在实际应用中,右移操作需要考虑符号位的处理方式。 + 由于 `double`,`float` 在二进制中的表现比较特殊,因此不能来进行移位操作。 移位操作符实际上支持的类型只有`int`和`long`,编译器在对`short`、`byte`、`char`类型进行移位前,都会将其转换为`int`类型再操作。 @@ -298,7 +368,7 @@ System.out.println("左移 10 位后的数据对应的二进制字符 " + Intege 输出: -``` +```plain 初始数据:-1 初始数据对应的二进制字符串:11111111111111111111111111111111 左移 10 位后的数据 -1024 @@ -333,36 +403,36 @@ System.out.println("左移 10 位后的数据对应的二进制字符 " + Intege 思考一下:下列语句的运行结果是什么? ```java - public static void main(String[] args) { - boolean flag = false; - for (int i = 0; i <= 3; i++) { - if (i == 0) { - System.out.println("0"); - } else if (i == 1) { - System.out.println("1"); - continue; - } else if (i == 2) { - System.out.println("2"); - flag = true; - } else if (i == 3) { - System.out.println("3"); - break; - } else if (i == 4) { - System.out.println("4"); - } - System.out.println("xixi"); - } - if (flag) { - System.out.println("haha"); - return; +public static void main(String[] args) { + boolean flag = false; + for (int i = 0; i <= 3; i++) { + if (i == 0) { + System.out.println("0"); + } else if (i == 1) { + System.out.println("1"); + continue; + } else if (i == 2) { + System.out.println("2"); + flag = true; + } else if (i == 3) { + System.out.println("3"); + break; + } else if (i == 4) { + System.out.println("4"); } - System.out.println("heihei"); + System.out.println("xixi"); } + if (flag) { + System.out.println("haha"); + return; + } + System.out.println("heihei"); +} ``` 运行结果: -``` +```plain 0 xixi 1 @@ -386,16 +456,18 @@ Java 中有 8 种基本数据类型,分别为: 这 8 种基本数据类型的默认值以及所占空间的大小如下: -| 基本类型 | 位数 | 字节 | 默认值 | 取值范围 | -| :-------- | :--- | :--- | :------ | ------------------------------------------ | -| `byte` | 8 | 1 | 0 | -128 ~ 127 | -| `short` | 16 | 2 | 0 | -32768 ~ 32767 | -| `int` | 32 | 4 | 0 | -2147483648 ~ 2147483647 | -| `long` | 64 | 8 | 0L | -9223372036854775808 ~ 9223372036854775807 | -| `char` | 16 | 2 | 'u0000' | 0 ~ 65535 | -| `float` | 32 | 4 | 0f | 1.4E-45 ~ 3.4028235E38 | -| `double` | 64 | 8 | 0d | 4.9E-324 ~ 1.7976931348623157E308 | -| `boolean` | 1 | | false | true、false | +| 基本类型 | 位数 | 字节 | 默认值 | 取值范围 | +| :-------- | :--- | :--- | :------ | -------------------------------------------------------------- | +| `byte` | 8 | 1 | 0 | -128 ~ 127 | +| `short` | 16 | 2 | 0 | -32768(-2^15) ~ 32767(2^15 - 1) | +| `int` | 32 | 4 | 0 | -2147483648 ~ 2147483647 | +| `long` | 64 | 8 | 0L | -9223372036854775808(-2^63) ~ 9223372036854775807(2^63 -1) | +| `char` | 16 | 2 | 'u0000' | 0 ~ 65535(2^16 - 1) | +| `float` | 32 | 4 | 0f | 1.4E-45 ~ 3.4028235E38 | +| `double` | 64 | 8 | 0d | 4.9E-324 ~ 1.7976931348623157E308 | +| `boolean` | 1 | | false | true、false | + +可以看到,像 `byte`、`short`、`int`、`long`能表示的最大正数都减 1 了。这是为什么呢?这是因为在二进制补码表示法中,最高位是用来表示符号的(0 表示正数,1 表示负数),其余位表示数值部分。所以,如果我们要表示最大的正数,我们需要把除了最高位之外的所有位都设为 1。如果我们再加 1,就会导致溢出,变成一个负数。 对于 `boolean`,官方文档未明确定义,它依赖于 JVM 厂商的具体实现。逻辑上理解是占用 1 位,但是实际中会考虑计算机高效存储因素。 @@ -404,14 +476,13 @@ Java 中有 8 种基本数据类型,分别为: **注意:** 1. Java 里使用 `long` 类型的数据一定要在数值后面加上 **L**,否则将作为整型解析。 -2. `char a = 'h'`char :单引号,`String a = "hello"` :双引号。 +2. Java 里使用 `float` 类型的数据一定要在数值后面加上 **f 或 F**,否则将无法通过编译。 +3. `char a = 'h'`char :单引号,`String a = "hello"` :双引号。 这八种基本类型都有对应的包装类分别为:`Byte`、`Short`、`Integer`、`Long`、`Float`、`Double`、`Character`、`Boolean` 。 ### 基本类型和包装类型的区别? -![基本类型 vs 包装类型](https://oss.javaguide.cn/github/javaguide/java/basis/primitive-type-vs-packaging-type.png) - - **用途**:除了定义一些常量和局部变量之外,我们在其他地方比如方法参数、对象属性中很少会使用基本类型来定义变量。并且,包装类型可用于泛型,而基本类型不可以。 - **存储方式**:基本数据类型的局部变量存放在 Java 虚拟机栈中的局部变量表中,基本数据类型的成员变量(未被 `static` 修饰 )存放在 Java 虚拟机的堆中。包装类型属于对象类型,我们知道几乎所有对象实例都存在于堆中。 - **占用空间**:相比于包装类型(对象类型), 基本数据类型占用的空间往往非常小。 @@ -420,11 +491,21 @@ Java 中有 8 种基本数据类型,分别为: **为什么说是几乎所有对象实例都存在于堆中呢?** 这是因为 HotSpot 虚拟机引入了 JIT 优化之后,会对对象进行逃逸分析,如果发现某一个对象并没有逃逸到方法外部,那么就可能通过标量替换来实现栈上分配,而避免堆上分配内存 -⚠️ 注意:**基本数据类型存放在栈中是一个常见的误区!** 基本数据类型的成员变量如果没有被 `static` 修饰的话(不建议这么使用,应该要使用基本数据类型对应的包装类型),就存放在堆中。 +⚠️ 注意:**基本数据类型存放在栈中是一个常见的误区!** 基本数据类型的存储位置取决于它们的作用域和声明方式。如果它们是局部变量,那么它们会存放在栈中;如果它们是成员变量,那么它们会存放在堆/方法区/元空间中。 ```java -class BasicTypeVar{ - private int x; +public class Test { + // 成员变量,存放在堆中 + int a = 10; + // 被 static 修饰的成员变量,JDK 1.7 及之前位于方法区,1.8 后存放于元空间,均不存放于堆中。 + // 变量属于类,不属于对象。 + static int b = 20; + + public void method() { + // 局部变量,存放在栈中 + int c = 30; + static int d = 40; // 编译错误,不能在方法中使用 static 修饰局部变量 + } } ``` @@ -432,7 +513,11 @@ class BasicTypeVar{ Java 基本数据类型的包装类型的大部分都用到了缓存机制来提升性能。 -`Byte`,`Short`,`Integer`,`Long` 这 4 种包装类默认创建了数值 **[-128,127]** 的相应类型的缓存数据,`Character` 创建了数值在 **[0,127]** 范围的缓存数据,`Boolean` 直接返回 `True` or `False`。 +`Byte`,`Short`,`Integer`,`Long` 这 4 种包装类默认创建了数值 **[-128,127]** 的相应类型的缓存数据,`Character` 创建了数值在 **[0,127]** 范围的缓存数据,`Boolean` 直接返回 `TRUE` or `FALSE`。 + +对于 `Integer`,可以通过 JVM 参数 `-XX:AutoBoxCacheMax=` 修改缓存上限,但不能修改下限 -128。实际使用时,并不建议设置过大的值,避免浪费内存,甚至是 OOM。 + +对于`Byte`,`Short`,`Long` ,`Character` 没有类似 `-XX:AutoBoxCacheMax` 参数可以修改,因此缓存范围是固定的,无法通过 JVM 参数调整。`Boolean` 则直接返回预定义的 `TRUE` 和 `FALSE` 实例,没有缓存范围的概念。 **Integer 缓存源码:** @@ -513,7 +598,7 @@ System.out.println(i1==i2); 记住:**所有整型包装类对象之间值的比较,全部使用 equals 方法比较**。 -![](https://oscimg.oschina.net/oscnet/up-1ae0425ce8646adfb768b5374951eeb820d.png) +![](https://oss.javaguide.cn/github/javaguide/up-1ae0425ce8646adfb768b5374951eeb820d.png) ### 自动装箱与拆箱了解吗?原理是什么? @@ -587,7 +672,7 @@ private static long sum() { ```java float a = 2.0f - 1.9f; float b = 1.8f - 1.7f; -System.out.println(a);// 0.100000024 +System.out.printf("%.9f",a);// 0.100000024 System.out.println(b);// 0.099999905 System.out.println(a == b);// false ``` @@ -617,15 +702,18 @@ System.out.println(a == b);// false ```java BigDecimal a = new BigDecimal("1.0"); -BigDecimal b = new BigDecimal("0.9"); +BigDecimal b = new BigDecimal("1.00"); BigDecimal c = new BigDecimal("0.8"); -BigDecimal x = a.subtract(b); +BigDecimal x = a.subtract(c); BigDecimal y = b.subtract(c); -System.out.println(x); /* 0.1 */ -System.out.println(y); /* 0.1 */ -System.out.println(Objects.equals(x, y)); /* true */ +System.out.println(x); /* 0.2 */ +System.out.println(y); /* 0.20 */ +// 比较内容,不是比较值 +System.out.println(Objects.equals(x, y)); /* false */ +// 比较值相等用相等compareTo,相等返回0 +System.out.println(0 == x.compareTo(y)); /* true */ ``` 关于 `BigDecimal` 的详细介绍,可以看看我写的这篇文章:[BigDecimal 详解](https://javaguide.cn/java/basis/bigdecimal.html)。 @@ -650,13 +738,19 @@ System.out.println(l + 1 == Long.MIN_VALUE); // true ### 成员变量与局部变量的区别? -![成员变量 vs 局部变量](https://oss.javaguide.cn/github/javaguide/java/basis/member-var-vs-local-var.png) - - **语法形式**:从语法形式上看,成员变量是属于类的,而局部变量是在代码块或方法中定义的变量或是方法的参数;成员变量可以被 `public`,`private`,`static` 等修饰符所修饰,而局部变量不能被访问控制修饰符及 `static` 所修饰;但是,成员变量和局部变量都能被 `final` 所修饰。 - **存储方式**:从变量在内存中的存储方式来看,如果成员变量是使用 `static` 修饰的,那么这个成员变量是属于类的,如果没有使用 `static` 修饰,这个成员变量是属于实例的。而对象存在于堆内存,局部变量则存在于栈内存。 - **生存时间**:从变量在内存中的生存时间上看,成员变量是对象的一部分,它随着对象的创建而存在,而局部变量随着方法的调用而自动生成,随着方法的调用结束而消亡。 - **默认值**:从变量是否有默认值来看,成员变量如果没有被赋初始值,则会自动以类型的默认值而赋值(一种情况例外:被 `final` 修饰的成员变量也必须显式地赋值),而局部变量则不会自动赋值。 +**为什么成员变量有默认值?** + +1. 先不考虑变量类型,如果没有默认值会怎样?变量存储的是内存地址对应的任意随机值,程序读取该值运行会出现意外。 + +2. 默认值有两种设置方式:手动和自动,根据第一点,没有手动赋值一定要自动赋值。成员变量在运行时可借助反射等方法手动赋值,而局部变量不行。 + +3. 对于编译器(javac)来说,局部变量没赋值很好判断,可以直接报错。而成员变量可能是运行时赋值,无法判断,误报“没默认值”又会影响用户体验,所以采用自动赋默认值。 + 成员变量与局部变量代码示例: ```java @@ -741,7 +835,7 @@ public class StringExample { 输出: -``` +```plain 字符型常量占用的字节数为:2 字符串常量占用的字节数为:13 ``` @@ -924,6 +1018,7 @@ public class SuperMan extends Hero{ } public class SuperSuperMan extends SuperMan { + @Override public String name() { return "超级超级英雄"; } @@ -937,7 +1032,7 @@ public class SuperSuperMan extends SuperMan { ### 什么是可变长参数? -从 Java5 开始,Java 支持定义可变长参数,所谓可变长参数就是允许在调用方法时传入不定长度的参数。就比如下面的这个 `printVariable` 方法就可以接受 0 个或者多个参数。 +从 Java5 开始,Java 支持定义可变长参数,所谓可变长参数就是允许在调用方法时传入不定长度的参数。就比如下面这个方法就可以接受 0 个或者多个参数。 ```java public static void method1(String... args) { @@ -987,7 +1082,7 @@ public class VariableLengthArgument { 输出: -``` +```plain ab a b @@ -1016,7 +1111,9 @@ public class VariableLengthArgument { ## 参考 -- What is the difference between JDK and JRE?:https://stackoverflow.com/questions/1906445/what-is-the-difference-between-jdk-and-jre -- Oracle vs OpenJDK:https://www.educba.com/oracle-vs-openjdk/ -- Differences between Oracle JDK and OpenJDK:https://stackoverflow.com/questions/22358071/differences-between-oracle-jdk-and-openjdk -- 彻底弄懂 Java 的移位操作符:https://juejin.cn/post/6844904025880526861 +- What is the difference between JDK and JRE?: +- Oracle vs OpenJDK: +- Differences between Oracle JDK and OpenJDK: +- 彻底弄懂 Java 的移位操作符: + + diff --git a/docs/java/basis/java-basic-questions-02.md b/docs/java/basis/java-basic-questions-02.md index 71c6016e361..9f8739f291d 100644 --- a/docs/java/basis/java-basic-questions-02.md +++ b/docs/java/basis/java-basic-questions-02.md @@ -6,24 +6,38 @@ 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基础常见知识点和面试题总结,希望对你有帮助! --- + + ## 面向对象基础 ### 面向对象和面向过程的区别 -两者的主要区别在于解决问题的方式不同: +面向过程编程(Procedural-Oriented Programming,POP)和面向对象编程(Object-Oriented Programming,OOP)是两种常见的编程范式,两者的主要区别在于解决问题的方式不同: + +- **面向过程编程(POP)**:面向过程把解决问题的过程拆成一个个方法,通过一个个方法的执行解决问题。 +- **面向对象编程(OOP)**:面向对象会先抽象出对象,然后用对象执行方法的方式解决问题。 + +相比较于 POP,OOP 开发的程序一般具有下面这些优点: + +- **易维护**:由于良好的结构和封装性,OOP 程序通常更容易维护。 +- **易复用**:通过继承和多态,OOP 设计使得代码更具复用性,方便扩展功能。 +- **易扩展**:模块化设计使得系统扩展变得更加容易和灵活。 + +POP 的编程方式通常更为简单和直接,适合处理一些较简单的任务。 + +POP 和 OOP 的性能差异主要取决于它们的运行机制,而不仅仅是编程范式本身。因此,简单地比较两者的性能是一个常见的误区(相关 issue : [面向过程:面向过程性能比面向对象高??](https://github.com/Snailclimb/JavaGuide/issues/431) )。 -- 面向过程把解决问题的过程拆成一个个方法,通过一个个方法的执行解决问题。 -- 面向对象会先抽象出对象,然后用对象执行方法的方式解决问题。 +![ POP 和 OOP 性能比较不合适](https://oss.javaguide.cn/github/javaguide/java/basis/pop-vs-oop-performance.png) -另外,面向对象开发的程序一般更易维护、易复用、易扩展。 +在选择编程范式时,性能并不是唯一的考虑因素。代码的可维护性、可扩展性和开发效率同样重要。 -相关 issue : [面向过程:面向过程性能比面向对象高??](https://github.com/Snailclimb/JavaGuide/issues/431) 。 +现代编程语言基本都支持多种编程范式,既可以用来进行面向过程编程,也可以进行面向对象编程。 下面是一个求圆的面积和周长的示例,简单分别展示了面向对象和面向过程两种不同的解决方案。 @@ -112,7 +126,7 @@ System.out.println(str1.equals(str3)); 输出结果: -``` +```plain false true true @@ -134,13 +148,13 @@ true ### 构造方法有哪些特点?是否可被 override? -构造方法特点如下: +构造方法具有以下特点: -- 名字与类名相同。 -- 没有返回值,但不能用 void 声明构造函数。 -- 生成类的对象时自动执行,无需调用。 +- **名称与类名相同**:构造方法的名称必须与类名完全一致。 +- **没有返回值**:构造方法没有返回类型,且不能使用 `void` 声明。 +- **自动执行**:在生成类的对象时,构造方法会自动执行,无需显式调用。 -构造方法不能被 override(重写),但是可以 overload(重载),所以你可以看到一个类中有多个构造函数的情况。 +构造方法**不能被重写(override)**,但**可以被重载(overload)**。因此,一个类中可以有多个构造方法,这些构造方法可以具有不同的参数列表,以提供不同的对象初始化方式。 ### 面向对象三大特征 @@ -194,21 +208,71 @@ public class Student { - 对象类型和引用类型之间具有继承(类)/实现(接口)的关系; - 引用类型变量发出的方法调用的到底是哪个类中的方法,必须在程序运行期间才能确定; - 多态不能调用“只在子类存在但在父类不存在”的方法; -- 如果子类重写了父类的方法,真正执行的是子类覆盖的方法,如果子类没有覆盖父类的方法,执行的是父类的方法。 +- 如果子类重写了父类的方法,真正执行的是子类重写的方法,如果子类没有重写父类的方法,执行的是父类的方法。 ### 接口和抽象类有什么共同点和区别? -**共同点**: +#### 接口和抽象类的共同点 + +- **实例化**:接口和抽象类都不能直接实例化,只能被实现(接口)或继承(抽象类)后才能创建具体的对象。 +- **抽象方法**:接口和抽象类都可以包含抽象方法。抽象方法没有方法体,必须在子类或实现类中实现。 + +#### 接口和抽象类的区别 + +- **设计目的**:接口主要用于对类的行为进行约束,你实现了某个接口就具有了对应的行为。抽象类主要用于代码复用,强调的是所属关系。 +- **继承和实现**:一个类只能继承一个类(包括抽象类),因为 Java 不支持多继承。但一个类可以实现多个接口,一个接口也可以继承多个其他接口。 +- **成员变量**:接口中的成员变量只能是 `public static final` 类型的,不能被修改且必须有初始值。抽象类的成员变量可以有任何修饰符(`private`, `protected`, `public`),可以在子类中被重新定义或赋值。 +- **方法**: + - Java 8 之前,接口中的方法默认是 `public abstract` ,也就是只能有方法声明。自 Java 8 起,可以在接口中定义 `default`(默认) 方法和 `static` (静态)方法。 自 Java 9 起,接口可以包含 `private` 方法。 + - 抽象类可以包含抽象方法和非抽象方法。抽象方法没有方法体,必须在子类中实现。非抽象方法有具体实现,可以直接在抽象类中使用或在子类中重写。 + +在 Java 8 及以上版本中,接口引入了新的方法类型:`default` 方法、`static` 方法和 `private` 方法。这些方法让接口的使用更加灵活。 + +Java 8 引入的`default` 方法用于提供接口方法的默认实现,可以在实现类中被覆盖。这样就可以在不修改实现类的情况下向现有接口添加新功能,从而增强接口的扩展性和向后兼容性。 + +```java +public interface MyInterface { + default void defaultMethod() { + System.out.println("This is a default method."); + } +} +``` + +Java 8 引入的`static` 方法无法在实现类中被覆盖,只能通过接口名直接调用( `MyInterface.staticMethod()`),类似于类中的静态方法。`static` 方法通常用于定义一些通用的、与接口相关的工具方法,一般很少用。 + +```java +public interface MyInterface { + static void staticMethod() { + System.out.println("This is a static method in the interface."); + } +} +``` + +Java 9 允许在接口中使用 `private` 方法。`private`方法可以用于在接口内部共享代码,不对外暴露。 -- 都不能被实例化。 -- 都可以包含抽象方法。 -- 都可以有默认实现的方法(Java 8 可以用 `default` 关键字在接口中定义默认方法)。 +```java +public interface MyInterface { + // default 方法 + default void defaultMethod() { + commonMethod(); + } -**区别**: + // static 方法 + static void staticMethod() { + commonMethod(); + } -- 接口主要用于对类的行为进行约束,你实现了某个接口就具有了对应的行为。抽象类主要用于代码复用,强调的是所属关系。 -- 一个类只能继承一个类,但是可以实现多个接口。 -- 接口中的成员变量只能是 `public static final` 类型的,不能被修改且必须有初始值,而抽象类的成员变量默认 default,可在子类中被重新定义,也可被重新赋值。 + // 私有静态方法,可以被 static 和 default 方法调用 + private static void commonMethod() { + System.out.println("This is a private method used internally."); + } + + // 实例私有方法,只能被 default 方法调用。 + private void instanceCommonMethod() { + System.out.println("This is a private instance method used internally."); + } +} +``` ### 深拷贝和浅拷贝区别了解吗?什么是引用拷贝? @@ -291,19 +355,19 @@ Person person1Copy = person1.clone(); System.out.println(person1.getAddress() == person1Copy.getAddress()); ``` -从输出结构就可以看出,虽然 `person1` 的克隆对象和 `person1` 包含的 `Address` 对象已经是不同的了。 +从输出结构就可以看出,显然 `person1` 的克隆对象和 `person1` 包含的 `Address` 对象已经是不同的了。 **那什么是引用拷贝呢?** 简单来说,引用拷贝就是两个不同的引用指向同一个对象。 我专门画了一张图来描述浅拷贝、深拷贝、引用拷贝: -![浅拷贝、深拷贝、引用拷贝示意图](https://oss.javaguide.cn/github/javaguide/java/basis/shallow&deep-copy.png) +![shallow&deep-copy](https://oss.javaguide.cn/github/javaguide/java/basis/shallow&deep-copy.png) ## Object ### Object 类的常见方法有哪些? -Object 类是一个特殊的类,是所有类的父类。它主要提供了以下 11 个方法: +Object 类是一个特殊的类,是所有类的父类,主要提供了以下 11 个方法: ```java /** @@ -319,7 +383,7 @@ public native int hashCode() */ public boolean equals(Object obj) /** - * naitive 方法,用于创建并返回当前对象的一份拷贝。 + * native 方法,用于创建并返回当前对象的一份拷贝。 */ protected native Object clone() throws CloneNotSupportedException /** @@ -339,7 +403,7 @@ public final native void notifyAll() */ public final native void wait(long timeout) throws InterruptedException /** - * 多了 nanos 参数,这个参数表示额外时间(以毫微秒为单位,范围是 0-999999)。 所以超时的时间还需要加上 nanos 毫秒。。 + * 多了 nanos 参数,这个参数表示额外时间(以纳秒为单位,范围是 0-999999)。 所以超时的时间还需要加上 nanos 纳秒。。 */ public final void wait(long timeout, int nanos) throws InterruptedException /** @@ -427,10 +491,10 @@ public boolean equals(Object anObject) { `hashCode()` 定义在 JDK 的 `Object` 类中,这就意味着 Java 中的任何类都包含有 `hashCode()` 函数。另外需要注意的是:`Object` 的 `hashCode()` 方法是本地方法,也就是用 C 语言或 C++ 实现的。 -> ⚠️ 注意:该方法在 **Oracle OpenJDK8** 中默认是 "使用线程局部状态来实现 Marsaglia's xor-shift 随机数生成", 并不是 "地址" 或者 "地址转换而来", 不同 JDK/VM 可能不同在 **Oracle OpenJDK8** 中有六种生成方式 (其中第五种是返回地址), 通过添加 VM 参数: -XX:hashCode=4 启用第五种。参考源码: +> ⚠️ 注意:该方法在 **Oracle OpenJDK8** 中默认是 "使用线程局部状态来实现 Marsaglia's xor-shift 随机数生成", 并不是 "地址" 或者 "地址转换而来", 不同 JDK/VM 可能不同。在 **Oracle OpenJDK8** 中有六种生成方式 (其中第五种是返回地址), 通过添加 VM 参数: -XX:hashCode=4 启用第五种。参考源码: > -> - https://hg.openjdk.org/jdk8u/jdk8u/hotspot/file/87ee5ee27509/src/share/vm/runtime/globals.hpp(1127行) -> - https://hg.openjdk.org/jdk8u/jdk8u/hotspot/file/87ee5ee27509/src/share/vm/runtime/synchronizer.cpp(537行开始) +> - (1127 行) +> - (537 行开始) ```java public native int hashCode(); @@ -507,7 +571,7 @@ abstract class AbstractStringBuilder implements Appendable, CharSequence { count += len; return this; } - //... + //... } ``` @@ -521,9 +585,9 @@ abstract class AbstractStringBuilder implements Appendable, CharSequence { **对于三者使用的总结:** -1. 操作少量的数据: 适用 `String` -2. 单线程操作字符串缓冲区下操作大量数据: 适用 `StringBuilder` -3. 多线程操作字符串缓冲区下操作大量数据: 适用 `StringBuffer` +- 操作少量的数据: 适用 `String` +- 单线程操作字符串缓冲区下操作大量数据: 适用 `StringBuilder` +- 多线程操作字符串缓冲区下操作大量数据: 适用 `StringBuffer` ### String 为什么是不可变的? @@ -532,7 +596,7 @@ abstract class AbstractStringBuilder implements Appendable, CharSequence { ```java public final class String implements java.io.Serializable, Comparable, CharSequence { private final char value[]; - //... + //... } ``` @@ -570,7 +634,7 @@ public final class String implements java.io.Serializable, Comparable, C > > 如果字符串中包含的汉字超过 Latin-1 可表示范围内的字符,`byte` 和 `char` 所占用的空间是一样的。 > -> 这是官方的介绍:https://openjdk.java.net/jeps/254 。 +> 这是官方的介绍: 。 ### 字符串拼接用“+” 还是 StringBuilder? @@ -619,7 +683,7 @@ System.out.println(s); 如果你使用 IDEA 的话,IDEA 自带的代码检查机制也会提示你修改代码。 -不过,使用 “+” 进行字符串拼接会产生大量的临时对象的问题在 JDK9 中得到了解决。在 JDK9 当中,字符串相加 “+” 改为了用动态方法 `makeConcatWithConstants()` 来实现,而不是大量的 `StringBuilder` 了。这个改进是 JDK9 的 [JEP 280](https://openjdk.org/jeps/280) 提出的,这也意味着 JDK 9 之后,你可以放心使用“+” 进行字符串拼接了。关于这部分改进的详细介绍,推荐阅读这篇文章:还在无脑用 [StringBuilder?来重温一下字符串拼接吧](https://juejin.cn/post/7182872058743750715) 。 +在 JDK 9 中,字符串相加“+”改为用动态方法 `makeConcatWithConstants()` 来实现,通过提前分配空间从而减少了部分临时对象的创建。然而这种优化主要针对简单的字符串拼接,如: `a+b+c` 。对于循环中的大量拼接操作,仍然会逐个动态分配内存(类似于两个两个 append 的概念),并不如手动使用 StringBuilder 来进行拼接效率高。这个改进是 JDK9 的 [JEP 280](https://openjdk.org/jeps/280) 提出的,关于这部分改进的详细介绍,推荐阅读这篇文章:还在无脑用 [StringBuilder?来重温一下字符串拼接吧](https://juejin.cn/post/7182872058743750715) 以及参考 [issue#2442](https://github.com/Snailclimb/JavaGuide/issues/2442)。 ### String#equals() 和 Object#equals() 有何区别? @@ -630,21 +694,26 @@ System.out.println(s); **字符串常量池** 是 JVM 为了提升性能和减少内存消耗针对字符串(String 类)专门开辟的一块区域,主要目的是为了避免字符串的重复创建。 ```java -// 在堆中创建字符串对象”ab“ -// 将字符串对象”ab“的引用保存在字符串常量池中 +// 在字符串常量池中创建字符串对象 ”ab“ +// 将字符串对象 ”ab“ 的引用赋值给 aa String aa = "ab"; -// 直接返回字符串常量池中字符串对象”ab“的引用 +// 直接返回字符串常量池中字符串对象 ”ab“,赋值给引用 bb String bb = "ab"; -System.out.println(aa==bb);// true +System.out.println(aa==bb); // true ``` 更多关于字符串常量池的介绍可以看一下 [Java 内存区域详解](https://javaguide.cn/java/jvm/memory-area.html) 这篇文章。 ### String s1 = new String("abc");这句话创建了几个字符串对象? -会创建 1 或 2 个字符串对象。 +先说答案:会创建 1 或 2 个字符串对象。 -1、如果字符串常量池中不存在字符串对象“abc”的引用,那么会在堆中创建 2 个字符串对象“abc”。 +1. 字符串常量池中不存在 "abc":会创建 2 个 字符串对象。一个在字符串常量池中,由 `ldc` 指令触发创建。一个在堆中,由 `new String()` 创建,并使用常量池中的 "abc" 进行初始化。 +2. 字符串常量池中已存在 "abc":会创建 1 个 字符串对象。该对象在堆中,由 `new String()` 创建,并使用常量池中的 "abc" 进行初始化。 + +下面开始详细分析。 + +1、如果字符串常量池中不存在字符串对象 “abc”,那么它首先会在字符串常量池中创建字符串对象 "abc",然后在堆内存中再创建其中一个字符串对象 "abc"。 示例代码(JDK 1.8): @@ -654,16 +723,40 @@ String s1 = new String("abc"); 对应的字节码: -![](https://oss.javaguide.cn/github/javaguide/open-source-project/image-20220413175809959.png) +```java +// 在堆内存中分配一个尚未初始化的 String 对象。 +// #2 是常量池中的一个符号引用,指向 java/lang/String 类。 +// 在类加载的解析阶段,这个符号引用会被解析成直接引用,即指向实际的 java/lang/String 类。 +0 new #2 +// 复制栈顶的 String 对象引用,为后续的构造函数调用做准备。 +// 此时操作数栈中有两个相同的对象引用:一个用于传递给构造函数,另一个用于保持对新对象的引用,后续将其存储到局部变量表。 +3 dup +// JVM 先检查字符串常量池中是否存在 "abc"。 +// 如果常量池中已存在 "abc",则直接返回该字符串的引用; +// 如果常量池中不存在 "abc",则 JVM 会在常量池中创建该字符串字面量并返回它的引用。 +// 这个引用被压入操作数栈,用作构造函数的参数。 +4 ldc #3 +// 调用构造方法,使用从常量池中加载的 "abc" 初始化堆中的 String 对象 +// 新的 String 对象将包含与常量池中的 "abc" 相同的内容,但它是一个独立的对象,存储于堆中。 +6 invokespecial #4 : (Ljava/lang/String;)V> +// 将堆中的 String 对象引用存储到局部变量表 +9 astore_1 +// 返回,结束方法 +10 return +``` + +`ldc (load constant)` 指令的确是从常量池中加载各种类型的常量,包括字符串常量、整数常量、浮点数常量,甚至类引用等。对于字符串常量,`ldc` 指令的行为如下: -`ldc` 命令用于判断字符串常量池中是否保存了对应的字符串对象的引用,如果保存了的话直接返回,如果没有保存的话,会在堆中创建对应的字符串对象并将该字符串对象的引用保存到字符串常量池中。 +1. **从常量池加载字符串**:`ldc` 首先检查字符串常量池中是否已经有内容相同的字符串对象。 +2. **复用已有字符串对象**:如果字符串常量池中已经存在内容相同的字符串对象,`ldc` 会将该对象的引用加载到操作数栈上。 +3. **没有则创建新对象并加入常量池**:如果字符串常量池中没有相同内容的字符串对象,JVM 会在常量池中创建一个新的字符串对象,并将其引用加载到操作数栈中。 -2、如果字符串常量池中已存在字符串对象“abc”的引用,则只会在堆中创建 1 个字符串对象“abc”。 +2、如果字符串常量池中已存在字符串对象“abc”,则只会在堆中创建 1 个字符串对象“abc”。 示例代码(JDK 1.8): ```java -// 字符串常量池中已存在字符串对象“abc”的引用 +// 字符串常量池中已存在字符串对象“abc” String s1 = "abc"; // 下面这段代码只会在堆中创建 1 个字符串对象“abc” String s2 = new String("abc"); @@ -671,35 +764,48 @@ String s2 = new String("abc"); 对应的字节码: -![](https://oss.javaguide.cn/github/javaguide/open-source-project/image-20220413180021072.png) +```java +0 ldc #2 +2 astore_1 +3 new #3 +6 dup +7 ldc #2 +9 invokespecial #4 : (Ljava/lang/String;)V> +12 astore_2 +13 return +``` 这里就不对上面的字节码进行详细注释了,7 这个位置的 `ldc` 命令不会在堆中创建新的字符串对象“abc”,这是因为 0 这个位置已经执行了一次 `ldc` 命令,已经在堆中创建过一次字符串对象“abc”了。7 这个位置执行 `ldc` 命令会直接返回字符串常量池中字符串对象“abc”对应的引用。 ### String#intern 方法有什么作用? -`String.intern()` 是一个 native(本地)方法,其作用是将指定的字符串对象的引用保存在字符串常量池中,可以简单分为两种情况: +`String.intern()` 是一个 `native` (本地) 方法,用来处理字符串常量池中的字符串对象引用。它的工作流程可以概括为以下两种情况: + +1. **常量池中已有相同内容的字符串对象**:如果字符串常量池中已经有一个与调用 `intern()` 方法的字符串内容相同的 `String` 对象,`intern()` 方法会直接返回常量池中该对象的引用。 +2. **常量池中没有相同内容的字符串对象**:如果字符串常量池中还没有一个与调用 `intern()` 方法的字符串内容相同的对象,`intern()` 方法会将当前字符串对象的引用添加到字符串常量池中,并返回该引用。 -- 如果字符串常量池中保存了对应的字符串对象的引用,就直接返回该引用。 -- 如果字符串常量池中没有保存了对应的字符串对象的引用,那就在常量池中创建一个指向该字符串对象的引用并返回。 +总结: + +- `intern()` 方法的主要作用是确保字符串引用在常量池中的唯一性。 +- 当调用 `intern()` 时,如果常量池中已经存在相同内容的字符串,则返回常量池中已有对象的引用;否则,将该字符串添加到常量池并返回其引用。 示例代码(JDK 1.8) : ```java -// 在堆中创建字符串对象”Java“ -// 将字符串对象”Java“的引用保存在字符串常量池中 +// s1 指向字符串常量池中的 "Java" 对象 String s1 = "Java"; -// 直接返回字符串常量池中字符串对象”Java“对应的引用 +// s2 也指向字符串常量池中的 "Java" 对象,和 s1 是同一个对象 String s2 = s1.intern(); -// 会在堆中在单独创建一个字符串对象 +// 在堆中创建一个新的 "Java" 对象,s3 指向它 String s3 = new String("Java"); -// 直接返回字符串常量池中字符串对象”Java“对应的引用 +// s4 指向字符串常量池中的 "Java" 对象,和 s1 是同一个对象 String s4 = s3.intern(); -// s1 和 s2 指向的是堆中的同一个对象 +// s1 和 s2 指向的是同一个常量池中的对象 System.out.println(s1 == s2); // true -// s3 和 s4 指向的是堆中不同的对象 +// s3 指向堆中的对象,s4 指向常量池中的对象,所以不同 System.out.println(s3 == s4); // false -// s1 和 s4 指向的是堆中的同一个对象 -System.out.println(s1 == s4); //true +// s1 和 s4 都指向常量池中的同一个对象 +System.out.println(s1 == s4); // true ``` ### String 类型的变量和常量做“+”运算时发生了什么? @@ -760,7 +866,7 @@ String d = str1 + str2; // 常量池中的对象 System.out.println(c == d);// true ``` -被 `final` 关键字修改之后的 `String` 会被编译器当做常量来处理,编译器在程序编译期就可以确定它的值,其效果就相当于访问常量。 +被 `final` 关键字修饰之后的 `String` 会被编译器当做常量来处理,编译器在程序编译期就可以确定它的值,其效果就相当于访问常量。 如果 ,编译器在运行时才能知道其确切值的话,就无法对其优化。 @@ -780,4 +886,7 @@ public static String getStr() { ## 参考 - 深入解析 String#intern: -- R 大(RednaxelaFX)关于常量折叠的回答:https://www.zhihu.com/question/55976094/answer/147302764 +- Java String 源码解读: +- R 大(RednaxelaFX)关于常量折叠的回答: + + diff --git a/docs/java/basis/java-basic-questions-03.md b/docs/java/basis/java-basic-questions-03.md index f21aa72d589..496e18827da 100644 --- a/docs/java/basis/java-basic-questions-03.md +++ b/docs/java/basis/java-basic-questions-03.md @@ -6,12 +6,14 @@ 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基础常见知识点和面试题总结,希望对你有帮助! --- + + ## 异常 **Java 异常类层次结构图概览**: @@ -47,14 +49,14 @@ head: - `ArithmeticException`(算术错误) - `SecurityException` (安全错误比如权限不够) - `UnsupportedOperationException`(不支持的操作错误比如重复创建同一用户) -- ...... +- …… ![](https://oss.javaguide.cn/github/javaguide/java/basis/unchecked-exception.png) ### Throwable 类常用方法有哪些? -- `String getMessage()`: 返回异常发生时的简要描述 -- `String toString()`: 返回异常发生时的详细信息 +- `String getMessage()`: 返回异常发生时的详细信息 +- `String toString()`: 返回异常发生时的简要描述 - `String getLocalizedMessage()`: 返回异常对象的本地化信息。使用 `Throwable` 的子类覆盖这个方法,可以生成本地化信息。如果子类没有覆盖该方法,则该方法返回的信息与 `getMessage()`返回的结果相同 - `void printStackTrace()`: 在控制台上打印 `Throwable` 对象封装的异常信息 @@ -79,7 +81,7 @@ try { 输出: -``` +```plain Try to do something Catch Exception -> RuntimeException Finally @@ -87,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 @@ -115,7 +109,7 @@ public static int f(int value) { 输出: -``` +```plain 0 ``` @@ -140,7 +134,7 @@ try { 输出: -``` +```plain Try to do something Catch Exception -> RuntimeException ``` @@ -215,9 +209,9 @@ catch (IOException e) { - 不要把异常定义为静态变量,因为这样会导致异常栈信息错乱。每次手动抛出异常,我们都需要手动 new 一个异常对象抛出。 - 抛出的异常信息一定要有意义。 -- 建议抛出更加具体的异常比如字符串转换为数字格式错误的时候应该抛出`NumberFormatException`而不是其父类`IllegalArgumentException`。 -- 使用日志打印异常之后就不要再抛出异常了(两者不要同时存在一段代码逻辑中)。 -- ...... +- 建议抛出更加具体的异常,比如字符串转换为数字格式错误的时候应该抛出`NumberFormatException`而不是其父类`IllegalArgumentException`。 +- 避免重复记录日志:如果在捕获异常的地方已经记录了足够的信息(包括异常类型、错误信息和堆栈跟踪等),那么在业务代码中再次抛出这个异常时,就不应该再次记录相同的错误信息。重复记录日志会使得日志文件膨胀,并且可能会掩盖问题的实际原因,使得问题更难以追踪和解决。 +- …… ## 泛型 @@ -284,7 +278,7 @@ class GeneratorImpl implements Generator{ 实现泛型接口,指定类型: ```java -class GeneratorImpl implements Generator{ +class GeneratorImpl implements Generator { @Override public String method() { return "hello"; @@ -321,58 +315,74 @@ printArray( stringArray ); - 自定义接口通用返回结果 `CommonResult` 通过参数 `T` 可根据具体的返回类型动态指定结果的数据类型 - 定义 `Excel` 处理类 `ExcelUtil` 用于动态指定 `Excel` 导出的数据类型 - 构建集合工具类(参考 `Collections` 中的 `sort`, `binarySearch` 方法)。 -- ...... +- …… ## 反射 关于反射的详细解读,请看这篇文章 [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 等,底层都大量运用了反射机制**,这才让它们能够那么灵活和强大。 -**这些框架中也大量使用了动态代理,而动态代理的实现也依赖反射。** +下面简单列举几个最场景的场景帮助大家理解。 -比如下面是通过 JDK 实现动态代理的示例代码,其中就使用了反射类 `Method` 来调用指定的方法。 +**1.依赖注入与控制反转(IoC)** + +以 Spring/Spring Boot 为代表的 IoC 框架,会在启动时扫描带有特定注解(如 `@Component`, `@Service`, `@Repository`, `@Controller`)的类,利用反射实例化对象(Bean),并通过反射注入依赖(如 `@Autowired`、构造器注入等)。 + +**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。 ## 注解 @@ -415,21 +425,20 @@ SPI 将服务接口和具体的服务实现分离开来,将服务调用方和 很多框架都使用了 Java 的 SPI 机制,比如:Spring 框架、数据库加载驱动、日志接口、以及 Dubbo 的扩展实现等等。 -![](https://oss.javaguide.cn/github/javaguide/java/basis/spi/22e1830e0b0e4115a882751f6c417857tplv-k3u1fbpfcp-zoom-1.jpeg) + ### SPI 和 API 有什么区别? **那 SPI 和 API 有啥区别?** -说到 SPI 就不得不说一下 API 了,从广义上来说它们都属于接口,而且很容易混淆。下面先用一张图说明一下: +说到 SPI 就不得不说一下 API(Application Programming Interface) 了,从广义上来说它们都属于接口,而且很容易混淆。下面先用一张图说明一下: -![](https://oss.javaguide.cn/github/javaguide/java/basis/spi/1ebd1df862c34880bc26b9d494535b3dtplv-k3u1fbpfcp-watermark.png) +![SPI VS API](https://oss.javaguide.cn/github/javaguide/java/basis/spi-vs-api.png) -一般模块之间都是通过接口进行通讯,那我们在服务调用方和服务实现方(也称服务提供者)之间引入一个“接口”。 +一般模块之间都是通过接口进行通讯,因此我们在服务调用方和服务实现方(也称服务提供者)之间引入一个“接口”。 -当实现方提供了接口和实现,我们可以通过调用实现方的接口从而拥有实现方给我们提供的能力,这就是 API ,这种接口和实现都是放在实现方的。 - -当接口存在于调用方这边时,就是 SPI ,由接口调用方确定接口规则,然后由不同的厂商去根据这个规则对这个接口进行实现,从而提供服务。 +- 当实现方提供了接口和实现,我们可以通过调用实现方的接口从而拥有实现方给我们提供的能力,这就是 **API**。这种情况下,接口和实现都是放在实现方的包中。调用方通过接口调用实现方的功能,而不需要关心具体的实现细节。 +- 当接口存在于调用方这边时,这就是 **SPI** 。由接口调用方确定接口规则,然后由不同的厂商根据这个规则对这个接口进行实现,从而提供服务。 举个通俗易懂的例子:公司 H 是一家科技公司,新设计了一款芯片,然后现在需要量产了,而市面上有好几家芯片制造业公司,这个时候,只要 H 公司指定好了这芯片生产的标准(定义好了接口标准),那么这些合作的芯片公司(服务提供者)就按照标准交付自家特色的芯片(提供不同方案的实现,但是给出来的结果是一样的)。 @@ -450,8 +459,8 @@ SPI 将服务接口和具体的服务实现分离开来,将服务调用方和 简单来说: -- **序列化**:将数据结构或对象转换成二进制字节流的过程 -- **反序列化**:将在序列化过程中所生成的二进制字节流转换成数据结构或者对象的过程 +- **序列化**:将数据结构或对象转换成可以存储或传输的形式,通常是二进制字节流,也可以是 JSON, XML 等文本格式 +- **反序列化**:将在序列化过程中所生成的数据转换为原始数据结构或者对象的过程 对于 Java 这种面向对象编程语言来说,我们序列化的都是对象(Object)也就是实例化后的类(Class),但是在 C++这种半面向对象的语言中,struct(结构体)定义的是数据结构类型,而 class 对应的是对象类型。 @@ -558,7 +567,7 @@ Java IO 流的 40 多个类都是从如下 4 个抽象类基类中派生出来 ```java String[] strs = {"JavaGuide", "公众号:JavaGuide", "博客:https://javaguide.cn/"}; for (String s : strs) { - System.out.println(s); + System.out.println(s); } ``` @@ -569,3 +578,5 @@ for (String s : strs) { Java 中最常用的语法糖主要有泛型、自动拆装箱、变长参数、枚举、内部类、增强 for 循环、try-with-resources 语法、lambda 表达式等。 关于这些语法糖的详细解读,请看这篇文章 [Java 语法糖详解](./syntactic-sugar.md) 。 + + diff --git a/docs/java/basis/java-keyword-summary.md b/docs/java/basis/java-keyword-summary.md index 34a05f66197..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); } } @@ -79,8 +79,8 @@ public class Sub extends Super { ## 参考 -- https://www.codejava.net/java-core/the-java-language/java-keywords -- https://blog.csdn.net/u013393958/article/details/79881037 +- +- # static 关键字详解 @@ -150,7 +150,7 @@ public class StaticDemo { 静态代码块的格式是 -``` +```plain static { 语句体; } @@ -166,8 +166,8 @@ static { 静态内部类与非静态内部类之间存在一个最大的区别,我们知道非静态内部类在编译完成之后会隐含地保存着一个引用,该引用是指向创建它的外围类,但是静态内部类却没有。没有这个引用就意味着: -1. 它的创建是不需要依赖外围类的创建。 -2. 它不能使用任何外围类的非 static 成员变量和方法。 +1. 它的创建是不需要依赖外围类的创建。 +2. 它不能使用任何外围类的非 static 成员变量和方法。 Example(静态内部类实现单例模式) @@ -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: @@ -282,19 +282,19 @@ public class Test { 上述代码输出: -``` +```plain 静态代码块!--非静态代码块!--默认构造方法!--静态方法中的内容! --静态方法中的代码块!-- ``` 当只执行 `Test.test();` 时输出: -``` +```plain 静态代码块!--静态方法中的内容! --静态方法中的代码块!-- ``` 当只执行 `Test test = new Test();` 时输出: -``` +```plain 静态代码块!--非静态代码块!--默认构造方法!-- ``` @@ -302,6 +302,8 @@ public class Test { ### 参考 -- https://blog.csdn.net/chen13579867831/article/details/78995480 -- https://www.cnblogs.com/chenssy/p/3388487.html -- https://www.cnblogs.com/Qian123/p/5713440.html +- +- +- + + diff --git a/docs/java/basis/proxy.md b/docs/java/basis/proxy.md index 7ca450cfcdf..615b0f00e42 100644 --- a/docs/java/basis/proxy.md +++ b/docs/java/basis/proxy.md @@ -208,7 +208,7 @@ public class DebugInvocationHandler implements InvocationHandler { this.target = target; } - + @Override public Object invoke(Object proxy, Method method, Object[] args) throws InvocationTargetException, IllegalAccessException { //调用方法之前,我们可以添加自己的操作 System.out.println("before method " + method.getName()); @@ -229,7 +229,7 @@ public class DebugInvocationHandler implements InvocationHandler { public class JdkProxyFactory { public static Object getProxy(Object target) { return Proxy.newProxyInstance( - target.getClass().getClassLoader(), // 目标类的类加载 + target.getClass().getClassLoader(), // 目标类的类加载器 target.getClass().getInterfaces(), // 代理需要实现的接口,可指定多个 new DebugInvocationHandler(target) // 代理对象对应的自定义 InvocationHandler ); @@ -248,7 +248,7 @@ smsService.send("java"); 运行上述代码之后,控制台打印出: -``` +```plain before method send send message:java after method send @@ -400,3 +400,5 @@ after method send 这篇文章中主要介绍了代理模式的两种实现:静态代理以及动态代理。涵盖了静态代理和动态代理实战、静态代理和动态代理的区别、JDK 动态代理和 Cglib 动态代理区别等内容。 文中涉及到的所有源码,你可以在这里找到:[https://github.com/Snailclimb/guide-rpc-framework-learning/tree/master/src/main/java/github/javaguide/proxy](https://github.com/Snailclimb/guide-rpc-framework-learning/tree/master/src/main/java/github/javaguide/proxy) 。 + + diff --git a/docs/java/basis/reflection.md b/docs/java/basis/reflection.md index 61161c59e6b..3ce8ccab9a9 100644 --- a/docs/java/basis/reflection.md +++ b/docs/java/basis/reflection.md @@ -116,7 +116,7 @@ public class TargetObject { } ``` -2. 使用反射操作这个类的方法以及参数 +2. 使用反射操作这个类的方法以及属性 ```java package cn.javaguide; @@ -170,15 +170,18 @@ public class Main { 输出内容: -``` +```plain publicMethod privateMethod I love JavaGuide value is JavaGuide ``` -**注意** : 有读者提到上面代码运行会抛出 `ClassNotFoundException` 异常,具体原因是你没有下面把这段代码的包名替换成自己创建的 `TargetObject` 所在的包 。 +**注意** : 有读者提到上面代码运行会抛出 `ClassNotFoundException` 异常,具体原因是你没有下面把这段代码的包名替换成自己创建的 `TargetObject` 所在的包 。 +可以参考: 这篇文章。 ```java Class targetClass = Class.forName("cn.javaguide.TargetObject"); ``` + + diff --git a/docs/java/basis/serialization.md b/docs/java/basis/serialization.md index f8a8a491ffc..f6ab9071967 100644 --- a/docs/java/basis/serialization.md +++ b/docs/java/basis/serialization.md @@ -11,8 +11,8 @@ tag: 简单来说: -- **序列化**:将数据结构或对象转换成二进制字节流的过程 -- **反序列化**:将在序列化过程中所生成的二进制字节流转换成数据结构或者对象的过程 +- **序列化**:将数据结构或对象转换成可以存储或传输的形式,通常是二进制字节流,也可以是 JSON, XML 等文本格式 +- **反序列化**:将在序列化过程中所生成的数据转换为原始数据结构或者对象的过程 对于 Java 这种面向对象编程语言来说,我们序列化的都是对象(Object)也就是实例化后的类(Class),但是在 C++这种半面向对象的语言中,struct(结构体)定义的是数据结构类型,而 class 对应的是对象类型。 @@ -81,7 +81,13 @@ public class RpcRequest implements Serializable { **serialVersionUID 不是被 static 变量修饰了吗?为什么还会被“序列化”?** -`static` 修饰的变量是静态变量,位于方法区,本身是不会被序列化的。 `static` 变量是属于类的而不是对象。你反序列之后,`static` 变量的值就像是默认赋予给了对象一样,看着就像是 `static` 变量被序列化,实际只是假象罢了。 +~~`static` 修饰的变量是静态变量,位于方法区,本身是不会被序列化的。 `static` 变量是属于类的而不是对象。你反序列之后,`static` 变量的值就像是默认赋予给了对象一样,看着就像是 `static` 变量被序列化,实际只是假象罢了。~~ + +**🐛 修正(参见:[issue#2174](https://github.com/Snailclimb/JavaGuide/issues/2174))**: + +通常情况下,`static` 变量是属于类的,不属于任何单个对象实例,所以它们本身不会被包含在对象序列化的数据流里。序列化保存的是对象的状态(也就是实例变量的值)。然而,`serialVersionUID` 是一个特例,`serialVersionUID` 的序列化做了特殊处理。关键在于,`serialVersionUID` 不是作为对象状态的一部分被序列化的,而是被序列化机制本身用作一个特殊的“指纹”或“版本号”。 + +当一个对象被序列化时,`serialVersionUID` 会被写入到序列化的二进制流中(像是在保存一个版本号,而不是保存 `static` 变量本身的状态);在反序列化时,也会解析它并做一致性判断,以此来验证序列化对象的版本一致性。如果两者不匹配,反序列化过程将抛出 `InvalidClassException`,因为这通常意味着序列化的类的定义已经发生了更改,可能不再兼容。 官方说明如下: @@ -89,7 +95,7 @@ public class RpcRequest implements Serializable { > > 如果想显式指定 `serialVersionUID` ,则需要在类中使用 `static` 和 `final` 关键字来修饰一个 `long` 类型的变量,变量名字必须为 `"serialVersionUID"` 。 -也就是说,`serialVersionUID` 只是用来被 JVM 识别,实际并没有被序列化。 +也就是说,`serialVersionUID` 本身(作为 static 变量)确实不作为对象状态被序列化。但是,它的值被 Java 序列化机制特殊处理了——作为一个版本标识符被读取并写入序列化流中,用于在反序列化时进行版本兼容性检查。 **如果有些字段不想进行序列化怎么办?** @@ -196,7 +202,7 @@ GitHub 地址:[https://github.com/protocolbuffers/protobuf](https://github.com ### ProtoStuff -由于 Protobuf 的易用性,它的哥哥 Protostuff 诞生了。 +由于 Protobuf 的易用性较差,它的哥哥 Protostuff 诞生了。 protostuff 基于 Google protobuf,但是提供了更多的功能和更简易的用法。虽然更加易用,但是不代表 ProtoStuff 性能更差。 @@ -212,10 +218,12 @@ Dubbo2.x 默认启用的序列化方式是 Hessian2 ,但是,Dubbo 对 Hessian2 ### 总结 -Kryo 是专门针对 Java 语言序列化方式并且性能非常好,如果你的应用是专门针对 Java 语言的话可以考虑使用,并且 Dubbo 官网的一篇文章中提到说推荐使用 Kryo 作为生产环境的序列化方式。(文章地址:[https://dubbo.apache.org/zh/docs/v2.7/user/references/protocol/rest/](https://dubbo.apache.org/zh/docs/v2.7/user/references/protocol/rest/)) +Kryo 是专门针对 Java 语言序列化方式并且性能非常好,如果你的应用是专门针对 Java 语言的话可以考虑使用,并且 Dubbo 官网的一篇文章中提到说推荐使用 Kryo 作为生产环境的序列化方式。(文章地址:)。 -![](https://my-blog-to-use.oss-cn-beijing.aliyuncs.com/2020-8/569e541a-22b2-4846-aa07-0ad479f07440.png) +![](https://oss.javaguide.cn/github/javaguide/java/569e541a-22b2-4846-aa07-0ad479f07440-20230814090158124.png) 像 Protobuf、 ProtoStuff、hessian 这类都是跨语言的序列化方式,如果有跨语言需求的话可以考虑使用。 除了我上面介绍到的序列化方式的话,还有像 Thrift,Avro 这些。 + + diff --git a/docs/java/basis/spi.md b/docs/java/basis/spi.md index 474db4bbcef..a2a7bccb7d3 100644 --- a/docs/java/basis/spi.md +++ b/docs/java/basis/spi.md @@ -14,9 +14,9 @@ head: > 本文来自 [Kingshion](https://github.com/jjx0708) 投稿。欢迎更多朋友参与到 JavaGuide 的维护工作,这是一件非常有意义的事情。详细信息请看:[JavaGuide 贡献指南](https://javaguide.cn/javaguide/contribution-guideline.html) 。 -在面向对象的设计原则中,一般推荐模块之间基于接口编程,通常情况下调用方模块是不会感知到被调用方模块的内部具体实现。一旦代码里面涉及具体实现类,就违反了开闭原则。如果需要替换一种实现,就需要修改代码。 +面向对象设计鼓励模块间基于接口而非具体实现编程,以降低模块间的耦合,遵循依赖倒置原则,并支持开闭原则(对扩展开放,对修改封闭)。然而,直接依赖具体实现会导致在替换实现时需要修改代码,违背了开闭原则。为了解决这个问题,SPI 应运而生,它提供了一种服务发现机制,允许在程序外部动态指定具体实现。这与控制反转(IoC)的思想相似,将组件装配的控制权移交给了程序之外。 -为了实现在模块装配的时候不用在程序里面动态指明,这就需要一种服务发现机制。Java SPI 就是提供了这样一个机制:**为某个接口寻找服务实现的机制。这有点类似 IoC 的思想,将装配的控制权移交到了程序之外。** +SPI 机制也解决了 Java 类加载体系中双亲委派模型带来的限制。[双亲委派模型](https://javaguide.cn/java/jvm/classloader.html)虽然保证了核心库的安全性和一致性,但也限制了核心库或扩展库加载应用程序类路径上的类(通常由第三方实现)。SPI 允许核心或扩展库定义服务接口,第三方开发者提供并部署实现,SPI 服务加载机制则在运行时动态发现并加载这些实现。例如,JDBC 4.0 及之后版本利用 SPI 自动发现和加载数据库驱动,开发者只需将驱动 JAR 包放置在类路径下即可,无需使用`Class.forName()`显式加载驱动类。 ## SPI 介绍 @@ -28,21 +28,20 @@ SPI 将服务接口和具体的服务实现分离开来,将服务调用方和 很多框架都使用了 Java 的 SPI 机制,比如:Spring 框架、数据库加载驱动、日志接口、以及 Dubbo 的扩展实现等等。 -![](https://oss.javaguide.cn/github/javaguide/java/basis/spi/22e1830e0b0e4115a882751f6c417857tplv-k3u1fbpfcp-zoom-1.jpeg) + ### SPI 和 API 有什么区别? **那 SPI 和 API 有啥区别?** -说到 SPI 就不得不说一下 API 了,从广义上来说它们都属于接口,而且很容易混淆。下面先用一张图说明一下: +说到 SPI 就不得不说一下 API(Application Programming Interface) 了,从广义上来说它们都属于接口,而且很容易混淆。下面先用一张图说明一下: -![](https://oss.javaguide.cn/github/javaguide/java/basis/spi/1ebd1df862c34880bc26b9d494535b3dtplv-k3u1fbpfcp-watermark.png) +![SPI VS API](https://oss.javaguide.cn/github/javaguide/java/basis/spi-vs-api.png) -一般模块之间都是通过通过接口进行通讯,那我们在服务调用方和服务实现方(也称服务提供者)之间引入一个“接口”。 +一般模块之间都是通过接口进行通讯,因此我们在服务调用方和服务实现方(也称服务提供者)之间引入一个“接口”。 -当实现方提供了接口和实现,我们可以通过调用实现方的接口从而拥有实现方给我们提供的能力,这就是 API ,这种接口和实现都是放在实现方的。 - -当接口存在于调用方这边时,就是 SPI ,由接口调用方确定接口规则,然后由不同的厂商去根据这个规则对这个接口进行实现,从而提供服务。 +- 当实现方提供了接口和实现,我们可以通过调用实现方的接口从而拥有实现方给我们提供的能力,这就是 **API**。这种情况下,接口和实现都是放在实现方的包中。调用方通过接口调用实现方的功能,而不需要关心具体的实现细节。 +- 当接口存在于调用方这边时,这就是 **SPI** 。由接口调用方确定接口规则,然后由不同的厂商根据这个规则对这个接口进行实现,从而提供服务。 举个通俗易懂的例子:公司 H 是一家科技公司,新设计了一款芯片,然后现在需要量产了,而市面上有好几家芯片制造业公司,这个时候,只要 H 公司指定好了这芯片生产的标准(定义好了接口标准),那么这些合作的芯片公司(服务提供者)就按照标准交付自家特色的芯片(提供不同方案的实现,但是给出来的结果是一样的)。 @@ -58,7 +57,7 @@ SLF4J (Simple Logging Facade for Java)是 Java 的一个日志门面(接 新建一个 Java 项目 `service-provider-interface` 目录结构如下:(注意直接新建 Java 项目就好了,不用新建 Maven 项目,Maven 项目会涉及到一些编译配置,如果有私服的话,直接 deploy 会比较方便,但是没有的话,在过程中可能会遇到一些奇怪的问题。) -``` +```plain │ service-provider-interface.iml │ ├─.idea @@ -171,7 +170,7 @@ public class Main { 新建项目 `service-provider` 目录结构如下: -``` +```plain │ service-provider.iml │ ├─.idea @@ -290,7 +289,7 @@ public class TestJavaSPI { `ServiceLoader` 是 JDK 提供的一个工具类, 位于`package java.util;`包下。 -``` +```plain A facility to load implementations of a service. ``` @@ -332,6 +331,10 @@ public void reload() { } ``` +其解决第三方类加载的机制其实就蕴含在 `ClassLoader cl = Thread.currentThread().getContextClassLoader();` 中,`cl` 就是**线程上下文类加载器**(Thread Context ClassLoader)。这是每个线程持有的类加载器,JDK 的设计允许应用程序或容器(如 Web 应用服务器)设置这个类加载器,以便核心类库能够通过它来加载应用程序类。 + +线程上下文类加载器默认情况下是应用程序类加载器(Application ClassLoader),它负责加载 classpath 上的类。当核心库需要加载应用程序提供的类时,它可以使用线程上下文类加载器来完成。这样,即使是由引导类加载器加载的核心库代码,也能够加载并使用由应用程序类加载器加载的类。 + 根据代码的调用顺序,在 `reload()` 方法中是通过一个内部类 `LazyIterator` 实现的。先继续往下面看。 `ServiceLoader` 实现了 `Iterable` 接口的方法后,具有了迭代的能力,在这个 `iterator` 方法被调用时,首先会在 `ServiceLoader` 的 `Provider` 缓存中进行查找,如果缓存中没有命中那么则在 `LazyIterator` 中进行查找。 @@ -552,9 +555,11 @@ public class MyServiceLoader { 其实不难发现,SPI 机制的具体实现本质上还是通过反射完成的。即:**我们按照规定将要暴露对外使用的具体实现类在 `META-INF/services/` 文件下声明。** -另外,SPI 机制在很多框架中都有应用:Spring 框架的基本原理也是类似的反射。还有 Dubbo 框架提供同样的 SPI 扩展机制,只不过 Dubbo 和 spring 框架中的 SPI 机制具体实现方式跟咱们今天学得这个有些细微的区别,不过整体的原理都是一致的,相信大家通过对 JDK 中 SPI 机制的学习,能够一通百通,加深对其他高深框的理解。 +另外,SPI 机制在很多框架中都有应用:Spring 框架的基本原理也是类似的方式。还有 Dubbo 框架提供同样的 SPI 扩展机制,只不过 Dubbo 和 spring 框架中的 SPI 机制具体实现方式跟咱们今天学得这个有些细微的区别,不过整体的原理都是一致的,相信大家通过对 JDK 中 SPI 机制的学习,能够一通百通,加深对其他高深框架的理解。 通过 SPI 机制能够大大地提高接口设计的灵活性,但是 SPI 机制也存在一些缺点,比如: 1. 遍历加载所有的实现类,这样效率还是相对较低的; 2. 当多个 `ServiceLoader` 同时 `load` 时,会有并发问题。 + + diff --git a/docs/java/basis/syntactic-sugar.md b/docs/java/basis/syntactic-sugar.md index ca2d38c38b1..3ce9bfc1099 100644 --- a/docs/java/basis/syntactic-sugar.md +++ b/docs/java/basis/syntactic-sugar.md @@ -14,7 +14,7 @@ head: > 作者:Hollis > -> 原文:https://mp.weixin.qq.com/s/o4XdEMq1DL-nBS-f8Za5Aw +> 原文: 语法糖是大厂 Java 面试常问的一个知识点。 @@ -46,7 +46,7 @@ Java 中最常用的语法糖主要有泛型、变长参数、条件编译、自 在开始之前先科普下,Java 中的`switch`自身原本就支持基本类型。比如`int`、`char`等。对于`int`类型,直接进行数值的比较。对于`char`类型则是比较其 ascii 码。所以,对于编译器来说,`switch`中其实只能使用整型,任何类型的比较都要转换成整型。比如`byte`。`short`,`char`(ascii 码是整型)以及`int`。 -那么接下来看下`switch`对`String`得支持,有以下代码: +那么接下来看下`switch`对`String`的支持,有以下代码: ```java public class switchDemoString { @@ -246,7 +246,7 @@ public static transient void print(String strs[]) } ``` -从反编译后代码可以看出,可变参数在被使用的时候,他首先会创建一个数组,数组的长度就是调用该方法是传递的实参的个数,然后再把参数值全部放到这个数组当中,然后再把这个数组作为参数传递到被调用的方法中。 +从反编译后代码可以看出,可变参数在被使用的时候,他首先会创建一个数组,数组的长度就是调用该方法是传递的实参的个数,然后再把参数值全部放到这个数组当中,然后再把这个数组作为参数传递到被调用的方法中。(注:`trasient` 仅在修饰成员变量时有意义,此处 “修饰方法” 是由于在 javassist 中使用相同数值分别表示 `trasient` 以及 `vararg`,见 [此处](https://github.com/jboss-javassist/javassist/blob/7302b8b0a09f04d344a26ebe57f29f3db43f2a3e/src/main/javassist/bytecode/AccessFlag.java#L32)。) ### 枚举 @@ -379,6 +379,83 @@ public class OutterClass } ``` +**为什么内部类可以使用外部类的 private 属性**: + +我们在 InnerClass 中增加一个方法,打印外部类的 userName 属性 + +```java +//省略其他属性 +public class OutterClass { + private String userName; + ...... + class InnerClass{ + ...... + public void printOut(){ + System.out.println("Username from OutterClass:"+userName); + } + } +} + +// 此时,使用javap -p命令对OutterClass反编译结果: +public classOutterClass { + private String userName; + ...... + static String access$000(OutterClass); +} +// 此时,InnerClass的反编译结果: +class OutterClass$InnerClass { + final OutterClass this$0; + ...... + public void printOut(); +} + +``` + +实际上,在编译完成之后,inner 实例内部会有指向 outer 实例的引用`this$0`,但是简单的`outer.name`是无法访问 private 属性的。从反编译的结果可以看到,outer 中会有一个桥方法`static String access$000(OutterClass)`,恰好返回 String 类型,即 userName 属性。正是通过这个方法实现内部类访问外部类私有属性。所以反编译后的`printOut()`方法大致如下: + +```java +public void printOut() { + System.out.println("Username from OutterClass:" + OutterClass.access$000(this.this$0)); +} +``` + +补充: + +1. 匿名内部类、局部内部类、静态内部类也是通过桥方法来获取 private 属性。 +2. 静态内部类没有`this$0`的引用 +3. 匿名内部类、局部内部类通过复制使用局部变量,该变量初始化之后就不能被修改。以下是一个案例: + +```java +public class OutterClass { + private String userName; + + public void test(){ + //这里i初始化为1后就不能再被修改 + int i=1; + class Inner{ + public void printName(){ + System.out.println(userName); + System.out.println(i); + } + } + } +} +``` + +反编译后: + +```java +//javap命令反编译Inner的结果 +//i被复制进内部类,且为final +class OutterClass$1Inner { + final int val$i; + final OutterClass this$0; + OutterClass$1Inner(); + public void printName(); +} + +``` + ### 条件编译 —般情况下,程序中的每一行代码都要参加编译。但有时候出于对程序代码优化的考虑,希望只对其中一部分内容进行编译,此时就需要在程序中加上条件,让编译器只对满足条件的代码进行编译,将不满足条件的代码舍弃,这就是条件编译。 @@ -423,7 +500,7 @@ public class ConditionalCompilation 首先,我们发现,在反编译后的代码中没有`System.out.println("Hello, ONLINE!");`,这其实就是条件编译。当`if(ONLINE)`为 false 的时候,编译器就没有对其内的代码进行编译。 -所以,**Java 语法的条件编译,是通过判断条件为常量的 if 语句实现的。其原理也是 Java 语言的语法糖。根据 if 判断条件的真假,编译器直接把分支为 false 的代码块消除。通过该方式实现的条件编译,必须在方法体内实现,而无法在正整个 Java 类的结构或者类的属性上进行条件编译,这与 C/C++的条件编译相比,确实更有局限性。在 Java 语言设计之初并没有引入条件编译的功能,虽有局限,但是总比没有更强。** +所以,**Java 语法的条件编译,是通过判断条件为常量的 if 语句实现的。其原理也是 Java 语言的语法糖。根据 if 判断条件的真假,编译器直接把分支为 false 的代码块消除。通过该方式实现的条件编译,必须在方法体内实现,而无法在整个 Java 类的结构或者类的属性上进行条件编译,这与 C/C++的条件编译相比,确实更有局限性。在 Java 语言设计之初并没有引入条件编译的功能,虽有局限,但是总比没有更强。** ### 断言 @@ -758,7 +835,8 @@ class GT{ 以上代码输出结果为:2! -由于经过类型擦除,所有的泛型类实例都关联到同一份字节码上,泛型类的所有静态变量是共享的。 +有些同学可能会误认为泛型类是不同的类,对应不同的字节码,其实 +由于经过类型擦除,所有的泛型类实例都关联到同一份字节码上,泛型类的静态变量是共享的。上面例子里的`GT.var`和`GT.var`其实是一个变量。 ### 自动装箱与拆箱 @@ -777,7 +855,7 @@ public static void main(String[] args) { 输出结果: -``` +```plain a == b is false c == d is true ``` @@ -808,3 +886,5 @@ Iterator 是工作在一个独立的线程中,并且拥有一个 mutex 锁。 前面介绍了 12 种 Java 中常用的语法糖。所谓语法糖就是提供给开发人员便于开发的一种语法而已。但是这种语法只有开发人员认识。要想被执行,需要进行解糖,即转成 JVM 认识的语法。当我们把语法糖解糖之后,你就会发现其实我们日常使用的这些方便的语法,其实都是一些其他更简单的语法构成的。 有了这些语法糖,我们在日常开发的时候可以大大提升效率,但是同时也要避过度使用。使用之前最好了解下原理,避免掉坑。 + + diff --git a/docs/java/basis/unsafe.md b/docs/java/basis/unsafe.md index e99ddfd7a78..fff31af808c 100644 --- a/docs/java/basis/unsafe.md +++ b/docs/java/basis/unsafe.md @@ -10,6 +10,8 @@ tag: > - [Java 魔法类:Unsafe 应用解析 - 美团技术团队 -2019](https://tech.meituan.com/2019/02/14/talk-about-java-magic-class-unsafe.html) > - [Java 双刃剑之 Unsafe 类详解 - 码农参上 - 2021](https://xie.infoq.cn/article/8b6ed4195e475bfb32dacc5cb) + + 阅读过 JUC 源码的同学,一定会发现很多并发工具类都调用了一个叫做 `Unsafe` 的类。 那这个类主要是用来干什么的呢?有什么使用场景呢?这篇文章就带你搞清楚! @@ -152,7 +154,7 @@ private void memoryTest() { 先看结果输出: -``` +```plain addr: 2433733895744 addr3: 2433733894944 16843009 @@ -275,7 +277,7 @@ public static void main(String[] args){ 运行结果: -``` +```plain subThread change flag to:false detected flag changed main thread end @@ -304,6 +306,49 @@ public boolean validate(long stamp) { #### 介绍 +**例子** + +```java +import sun.misc.Unsafe; +import java.lang.reflect.Field; + +public class Main { + + private int value; + + public static void main(String[] args) throws Exception{ + Unsafe unsafe = reflectGetUnsafe(); + assert unsafe != null; + long offset = unsafe.objectFieldOffset(Main.class.getDeclaredField("value")); + Main main = new Main(); + System.out.println("value before putInt: " + main.value); + unsafe.putInt(main, offset, 42); + System.out.println("value after putInt: " + main.value); + System.out.println("value after putInt: " + unsafe.getInt(main, offset)); + } + + private static Unsafe reflectGetUnsafe() { + try { + Field field = Unsafe.class.getDeclaredField("theUnsafe"); + field.setAccessible(true); + return (Unsafe) field.get(null); + } catch (Exception e) { + e.printStackTrace(); + return null; + } + } + +} +``` + +输出结果: + +```plain +value before putInt: 0 +value after putInt: 42 +value after putInt: 42 +``` + **对象属性** 对象成员属性的内存偏移量获取,以及字段属性值的修改,在上面的例子中我们已经测试过了。除了前面的`putInt`、`getInt`方法外,Unsafe 提供了全部 8 种基础数据类型以及`Object`的`put`和`get`方法,并且所有的`put`方法都可以越过访问权限,直接修改内存中的数据。阅读 openJDK 源码中的注释发现,基础数据类型和`Object`的读写稍有不同,基础数据类型是直接操作的属性值(`value`),而`Object`的操作则是基于引用值(`reference value`)。下面是`Object`的读写方法: @@ -374,11 +419,11 @@ public void objTest() throws Exception{ } ``` -打印结果分别为 1、1、0,说明通过`allocateInstance`方法创建对象过程中,不会调用类的构造方法。使用这种方式创建对象时,只用到了`Class`对象,所以说如果想要跳过对象的初始化阶段或者跳过构造器的安全检查,就可以使用这种方法。在上面的例子中,如果将 A 类的构造函数改为`private`类型,将无法通过构造函数和反射创建对象,但`allocateInstance`方法仍然有效。 +打印结果分别为 1、1、0,说明通过`allocateInstance`方法创建对象过程中,不会调用类的构造方法。使用这种方式创建对象时,只用到了`Class`对象,所以说如果想要跳过对象的初始化阶段或者跳过构造器的安全检查,就可以使用这种方法。在上面的例子中,如果将 A 类的构造函数改为`private`类型,将无法通过构造函数和反射创建对象(可以通过构造函数对象 setAccessible 后创建对象),但`allocateInstance`方法仍然有效。 #### 典型应用 -- **常规对象实例化方式**:我们通常所用到的创建对象的方式,从本质上来讲,都是通过 new 机制来实现对象的创建。但是,new 机制有个特点就是当类只提供有参的构造函数且无显示声明无参构造函数时,则必须使用有参构造函数进行对象构造,而使用有参构造函数时,必须传递相应个数的参数才能完成对象实例化。 +- **常规对象实例化方式**:我们通常所用到的创建对象的方式,从本质上来讲,都是通过 new 机制来实现对象的创建。但是,new 机制有个特点就是当类只提供有参的构造函数且无显式声明无参构造函数时,则必须使用有参构造函数进行对象构造,而使用有参构造函数时,必须传递相应个数的参数才能完成对象实例化。 - **非常规的实例化方式**:而 Unsafe 中提供 allocateInstance 方法,仅通过 Class 对象就可以创建此类的实例对象,而且不需要调用其构造函数、初始化代码、JVM 安全检查等。它抑制修饰符检测,也就是即使构造器是 private 修饰的也能通过此方法实例化,只需提类对象即可创建相应的对象。由于这种特性,allocateInstance 在 java.lang.invoke、Objenesis(提供绕过类构造器的对象生成方式)、Gson(反序列化时用到)中都有相应的应用。 ### 数组操作 @@ -408,7 +453,7 @@ public native int arrayIndexScale(Class arrayClass); ```java /** - * CAS + * CAS * @param o 包含要修改field的对象 * @param offset 对象中某field的偏移量 * @param expected 期望值 @@ -467,15 +512,98 @@ private void increment(int x){ 运行代码会依次输出: -``` +```plain 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()`)来优化。 ### 线程调度 @@ -557,7 +685,7 @@ public static void main(String[] args) { 程序输出为: -``` +```plain park main mainThread subThread try to unpark mainThread unpark mainThread success @@ -580,7 +708,7 @@ unpark mainThread success public native long staticFieldOffset(Field f); //获取静态属性的对象指针 public native Object staticFieldBase(Field f); -//判断类是否需要实例化(用于获取类的静态属性前进行检测) +//判断类是否需要初始化(用于获取类的静态属性前进行检测) public native boolean shouldBeInitialized(Class c); ``` @@ -594,6 +722,11 @@ public class User { } private void staticTest() throws Exception { User user=new User(); + // 也可以用下面的语句触发类初始化 + // 1. + // unsafe.ensureClassInitialized(User.class); + // 2. + // System.out.println(User.name); System.out.println(unsafe.shouldBeInitialized(User.class)); Field sexField = User.class.getDeclaredField("name"); long fieldOffset = unsafe.staticFieldOffset(sexField); @@ -605,16 +738,18 @@ private void staticTest() throws Exception { 运行结果: -``` -falseHydra +```plain +false +Hydra ``` 在 `Unsafe` 的对象操作中,我们学习了通过`objectFieldOffset`方法获取对象属性偏移量并基于它对变量的值进行存取,但是它不适用于类中的静态属性,这时候就需要使用`staticFieldOffset`方法。在上面的代码中,只有在获取`Field`对象的过程中依赖到了`Class`,而获取静态变量的属性时不再依赖于`Class`。 -在上面的代码中首先创建一个`User`对象,这是因为如果一个类没有被实例化,那么它的静态属性也不会被初始化,最后获取的字段属性将是`null`。所以在获取静态属性前,需要调用`shouldBeInitialized`方法,判断在获取前是否需要初始化这个类。如果删除创建 User 对象的语句,运行结果会变为: +在上面的代码中首先创建一个`User`对象,这是因为如果一个类没有被初始化,那么它的静态属性也不会被初始化,最后获取的字段属性将是`null`。所以在获取静态属性前,需要调用`shouldBeInitialized`方法,判断在获取前是否需要初始化这个类。如果删除创建 User 对象的语句,运行结果会变为: -``` -truenull +```plain +true +null ``` **使用`defineClass`方法允许程序在运行时动态地创建一个类** @@ -678,3 +813,5 @@ public native int pageSize(); ## 总结 在本文中,我们首先介绍了 `Unsafe` 的基本概念、工作原理,并在此基础上,对它的 API 进行了说明与实践。相信大家通过这一过程,能够发现 `Unsafe` 在某些场景下,确实能够为我们提供编程中的便利。但是回到开头的话题,在使用这些便利时,确实存在着一些安全上的隐患,在我看来,一项技术具有不安全因素并不可怕,可怕的是它在使用过程中被滥用。尽管之前有传言说会在 Java9 中移除 `Unsafe` 类,不过它还是照样已经存活到了 Java16。按照存在即合理的逻辑,只要使用得当,它还是能给我们带来不少的帮助,因此最后还是建议大家,在使用 `Unsafe` 的过程中一定要做到使用谨慎使用、避免滥用。 + + 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 ad44d43aea3..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 只有值传递? @@ -64,7 +64,7 @@ public static void swap(int a, int b) { 输出: -``` +```plain a = 20 b = 10 num1 = 10 @@ -84,22 +84,22 @@ num2 = 20 代码: ```java - public static void main(String[] args) { + public static void main(String[] args) { int[] arr = { 1, 2, 3, 4, 5 }; System.out.println(arr[0]); change(arr); System.out.println(arr[0]); - } + } - public static void change(int[] array) { + public static void change(int[] array) { // 将数组的第一个元素变为0 array[0] = 0; - } + } ``` 输出: -``` +```plain 1 0 ``` @@ -143,7 +143,7 @@ public static void swap(Person person1, Person person2) { 输出: -``` +```plain person1:小李 person2:小张 xiaoZhang:小张 @@ -184,7 +184,7 @@ int main() 输出结果: -``` +```plain invoke before: 10 incr before: 10 incr after: 11 @@ -215,3 +215,5 @@ Java 中将实参传递给方法(或函数)的方式是 **值传递**: - [Java 到底是值传递还是引用传递? - Hollis 的回答 - 知乎](https://www.zhihu.com/question/31203609/answer/576030121) - [Oracle Java Tutorials - Passing Information to a Method or a Constructor](https://docs.oracle.com/javase/tutorial/java/javaOO/arguments.html) - [Interview with James Gosling, Father of Java](https://mappingthejourney.com/single-post/2017/06/29/episode-3-interview-with-james-gosling-father-of-java/) + + diff --git a/docs/java/collection/arrayblockingqueue-source-code.md b/docs/java/collection/arrayblockingqueue-source-code.md new file mode 100644 index 00000000000..4c923ef0d29 --- /dev/null +++ b/docs/java/collection/arrayblockingqueue-source-code.md @@ -0,0 +1,773 @@ +--- +title: ArrayBlockingQueue 源码分析 +category: Java +tag: + - Java集合 +--- + +## 阻塞队列简介 + +### 阻塞队列的历史 + +Java 阻塞队列的历史可以追溯到 JDK1.5 版本,当时 Java 平台增加了 `java.util.concurrent`,即我们常说的 JUC 包,其中包含了各种并发流程控制工具、并发容器、原子类等。这其中自然也包含了我们这篇文章所讨论的阻塞队列。 + +为了解决高并发场景下多线程之间数据共享的问题,JDK1.5 版本中出现了 `ArrayBlockingQueue` 和 `LinkedBlockingQueue`,它们是带有生产者-消费者模式实现的并发容器。其中,`ArrayBlockingQueue` 是有界队列,即添加的元素达到上限之后,再次添加就会被阻塞或者抛出异常。而 `LinkedBlockingQueue` 则由链表构成的队列,正是因为链表的特性,所以 `LinkedBlockingQueue` 在添加元素上并不会向 `ArrayBlockingQueue` 那样有着较多的约束,所以 `LinkedBlockingQueue` 设置队列是否有界是可选的(注意这里的无界并不是指可以添加任意数量的元素,而是说队列的大小默认为 `Integer.MAX_VALUE`,近乎于无限大)。 + +随着 Java 的不断发展,JDK 后续的几个版本又对阻塞队列进行了不少的更新和完善: + +1. JDK1.6 版本:增加 `SynchronousQueue`,一个不存储元素的阻塞队列。 +2. JDK1.7 版本:增加 `TransferQueue`,一个支持更多操作的阻塞队列。 +3. JDK1.8 版本:增加 `DelayQueue`,一个支持延迟获取元素的阻塞队列。 + +### 阻塞队列的思想 + +阻塞队列就是典型的生产者-消费者模型,它可以做到以下几点: + +1. 当阻塞队列数据为空时,所有的消费者线程都会被阻塞,等待队列非空。 +2. 当生产者往队列里填充数据后,队列就会通知消费者队列非空,消费者此时就可以进来消费。 +3. 当阻塞队列因为消费者消费过慢或者生产者存放元素过快导致队列填满时无法容纳新元素时,生产者就会被阻塞,等待队列非满时继续存放元素。 +4. 当消费者从队列中消费一个元素之后,队列就会通知生产者队列非满,生产者可以继续填充数据了。 + +总结一下:阻塞队列就说基于非空和非满两个条件实现生产者和消费者之间的交互,尽管这些交互流程和等待通知的机制实现非常复杂,好在 Doug Lea 的操刀之下已将阻塞队列的细节屏蔽,我们只需调用 `put`、`take`、`offer`、`poll` 等 API 即可实现多线程之间的生产和消费。 + +这也使得阻塞队列在多线程开发中有着广泛的运用,最常见的例子无非是我们的线程池,从源码中我们就能看出当核心线程无法及时处理任务时,这些任务都会扔到 `workQueue` 中。 + +```java +public ThreadPoolExecutor(int corePoolSize, + int maximumPoolSize, + long keepAliveTime, + TimeUnit unit, + BlockingQueue workQueue, + ThreadFactory threadFactory, + RejectedExecutionHandler handler) {// ...} +``` + +## ArrayBlockingQueue 常见方法及测试 + +简单了解了阻塞队列的历史之后,我们就开始重点讨论本篇文章所要介绍的并发容器——`ArrayBlockingQueue`。为了后续更加深入的了解 `ArrayBlockingQueue`,我们不妨基于下面几个实例了解以下 `ArrayBlockingQueue` 的使用。 + +先看看第一个例子,我们这里会用两个线程分别模拟生产者和消费者,生产者生产完会使用 `put` 方法生产 10 个元素给消费者进行消费,当队列元素达到我们设置的上限 5 时,`put` 方法就会阻塞。 +同理消费者也会通过 `take` 方法消费元素,当队列为空时,`take` 方法就会阻塞消费者线程。这里笔者为了保证消费者能够在消费完 10 个元素后及时退出。便通过倒计时门闩,来控制消费者结束,生产者在这里只会生产 10 个元素。当消费者将 10 个元素消费完成之后,按下倒计时门闩,所有线程都会停止。 + +```java +public class ProducerConsumerExample { + + public static void main(String[] args) throws InterruptedException { + + // 创建一个大小为 5 的 ArrayBlockingQueue + ArrayBlockingQueue queue = new ArrayBlockingQueue<>(5); + + // 创建生产者线程 + Thread producer = new Thread(() -> { + try { + for (int i = 1; i <= 10; i++) { + // 向队列中添加元素,如果队列已满则阻塞等待 + queue.put(i); + System.out.println("生产者添加元素:" + i); + } + } catch (InterruptedException e) { + e.printStackTrace(); + } + + }); + + CountDownLatch countDownLatch = new CountDownLatch(1); + + // 创建消费者线程 + Thread consumer = new Thread(() -> { + try { + int count = 0; + while (true) { + + // 从队列中取出元素,如果队列为空则阻塞等待 + int element = queue.take(); + System.out.println("消费者取出元素:" + element); + ++count; + if (count == 10) { + break; + } + } + + countDownLatch.countDown(); + } catch (InterruptedException e) { + e.printStackTrace(); + } + + }); + + // 启动线程 + producer.start(); + consumer.start(); + + // 等待线程结束 + producer.join(); + consumer.join(); + + countDownLatch.await(); + + producer.interrupt(); + consumer.interrupt(); + } + +} +``` + +代码输出结果如下,可以看到只有生产者往队列中投放元素之后消费者才能消费,这也就意味着当队列中没有数据的时消费者就会阻塞,等待队列非空再继续消费。 + +```cpp +生产者添加元素:1 +生产者添加元素:2 +消费者取出元素:1 +消费者取出元素:2 +生产者添加元素:3 +消费者取出元素:3 +生产者添加元素:4 +生产者添加元素:5 +消费者取出元素:4 +生产者添加元素:6 +消费者取出元素:5 +生产者添加元素:7 +生产者添加元素:8 +生产者添加元素:9 +生产者添加元素:10 +消费者取出元素:6 +消费者取出元素:7 +消费者取出元素:8 +消费者取出元素:9 +消费者取出元素:10 +``` + +了解了 `put`、`take` 这两个会阻塞的存和取方法之后,我我们再来看看阻塞队列中非阻塞的入队和出队方法 `offer` 和 `poll`。 + +如下所示,我们设置了一个大小为 3 的阻塞队列,我们会尝试在队列用 offer 方法存放 4 个元素,然后再从队列中用 `poll` 尝试取 4 次。 + +```cpp +public class OfferPollExample { + + public static void main(String[] args) { + // 创建一个大小为 3 的 ArrayBlockingQueue + ArrayBlockingQueue queue = new ArrayBlockingQueue<>(3); + + // 向队列中添加元素 + System.out.println(queue.offer("A")); + System.out.println(queue.offer("B")); + System.out.println(queue.offer("C")); + + // 尝试向队列中添加元素,但队列已满,返回 false + System.out.println(queue.offer("D")); + + // 从队列中取出元素 + System.out.println(queue.poll()); + System.out.println(queue.poll()); + System.out.println(queue.poll()); + + // 尝试从队列中取出元素,但队列已空,返回 null + System.out.println(queue.poll()); + } + +} +``` + +最终代码的输出结果如下,可以看到因为队列的大小为 3 的缘故,我们前 3 次存放到队列的结果为 true,第 4 次存放时,由于队列已满,所以存放结果返回 false。这也是为什么我们后续的 `poll` 方法只得到了 3 个元素的值。 + +```cpp +true +true +true +false +A +B +C +null +``` + +了解了阻塞存取和非阻塞存取,我们再来看看阻塞队列的一个比较特殊的操作,某些场景下,我们希望能够一次性将阻塞队列的结果存到列表中再进行批量操作,我们就可以使用阻塞队列的 `drainTo` 方法,这个方法会一次性将队列中所有元素存放到列表,如果队列中有元素,且成功存到 list 中则 `drainTo` 会返回本次转移到 list 中的元素数,反之若队列为空,`drainTo` 则直接返回 0。 + +```java +public class DrainToExample { + + public static void main(String[] args) { + // 创建一个大小为 5 的 ArrayBlockingQueue + ArrayBlockingQueue queue = new ArrayBlockingQueue<>(5); + + // 向队列中添加元素 + queue.add(1); + queue.add(2); + queue.add(3); + queue.add(4); + queue.add(5); + + // 创建一个 List,用于存储从队列中取出的元素 + List list = new ArrayList<>(); + + // 从队列中取出所有元素,并添加到 List 中 + queue.drainTo(list); + + // 输出 List 中的元素 + System.out.println(list); + } + +} +``` + +代码输出结果如下 + +```cpp +[1, 2, 3, 4, 5] +``` + +## ArrayBlockingQueue 源码分析 + +自此我们对阻塞队列的使用有了基本的印象,接下来我们就可以进一步了解一下 `ArrayBlockingQueue` 的工作机制了。 + +### 整体设计 + +在了解 `ArrayBlockingQueue` 的具体细节之前,我们先来看看 `ArrayBlockingQueue` 的类图。 + +![ArrayBlockingQueue 类图](https://oss.javaguide.cn/github/javaguide/java/collection/arrayblockingqueue-class-diagram.png) + +从图中我们可以看出,`ArrayBlockingQueue` 继承了阻塞队列 `BlockingQueue` 这个接口,不难猜出通过继承 `BlockingQueue` 这个接口之后,`ArrayBlockingQueue` 就拥有了阻塞队列那些常见的操作行为。 + +同时, `ArrayBlockingQueue` 还继承了 `AbstractQueue` 这个抽象类,这个继承了 `AbstractCollection` 和 `Queue` 的抽象类,从抽象类的特定和语义我们也可以猜出,这个继承关系使得 `ArrayBlockingQueue` 拥有了队列的常见操作。 + +所以我们是否可以得出这样一个结论,通过继承 `AbstractQueue` 获得队列所有的操作模板,其实现的入队和出队操作的整体框架。然后 `ArrayBlockingQueue` 通过继承 `BlockingQueue` 获取到阻塞队列的常见操作并将这些操作实现,填充到 `AbstractQueue` 模板方法的细节中,由此 `ArrayBlockingQueue` 成为一个完整的阻塞队列。 + +为了印证这一点,我们到源码中一探究竟。首先我们先来看看 `AbstractQueue`,从类的继承关系我们可以大致得出,它通过 `AbstractCollection` 获得了集合的常见操作方法,然后通过 `Queue` 接口获得了队列的特性。 + +```java +public abstract class AbstractQueue + extends AbstractCollection + implements Queue { + //... +} +``` + +对于集合的操作无非是增删改查,所以我们不妨从添加方法入手,从源码中我们可以看到,它实现了 `AbstractCollection` 的 `add` 方法,其内部逻辑如下: + +1. 调用继承 `Queue` 接口的来的 `offer` 方法,如果 `offer` 成功则返回 `true`。 +2. 如果 `offer` 失败,即代表当前元素入队失败直接抛异常。 + +```java +public boolean add(E e) { + if (offer(e)) + return true; + else + throw new IllegalStateException("Queue full"); +} +``` + +而 `AbstractQueue` 中并没有对 `Queue` 的 `offer` 的实现,很明显这样做的目的是定义好了 `add` 的核心逻辑,将 `offer` 的细节交由其子类即我们的 `ArrayBlockingQueue` 实现。 + +到此,我们对于抽象类 `AbstractQueue` 的分析就结束了,我们继续看看 `ArrayBlockingQueue` 中另一个重要的继承接口 `BlockingQueue`。 + +点开 `BlockingQueue` 之后,我们可以看到这个接口同样继承了 `Queue` 接口,这就意味着它也具备了队列所拥有的所有行为。同时,它还定义了自己所需要实现的方法。 + +```java +public interface BlockingQueue extends Queue { + + //元素入队成功返回true,反之则会抛出异常IllegalStateException + boolean add(E e); + + //元素入队成功返回true,反之返回false + boolean offer(E e); + + //元素入队成功则直接返回,如果队列已满元素不可入队则将线程阻塞,因为阻塞期间可能会被打断,所以这里方法签名抛出了InterruptedException + void put(E e) throws InterruptedException; + + //和上一个方法一样,只不过队列满时只会阻塞单位为unit,时间为timeout的时长,如果在等待时长内没有入队成功则直接返回false。 + boolean offer(E e, long timeout, TimeUnit unit) + throws InterruptedException; + + //从队头取出一个元素,如果队列为空则阻塞等待,因为会阻塞线程的缘故,所以该方法可能会被打断,所以签名定义了InterruptedException + E take() throws InterruptedException; + + //取出队头的元素并返回,如果当前队列为空则阻塞等待timeout且单位为unit的时长,如果这个时间段没有元素则直接返回null。 + E poll(long timeout, TimeUnit unit) + throws InterruptedException; + + //获取队列剩余元素个数 + int remainingCapacity(); + + //删除我们指定的对象,如果成功返回true,反之返回false。 + boolean remove(Object o); + + //判断队列中是否包含指定元素 + public boolean contains(Object o); + + //将队列中的元素全部存到指定的集合中 + int drainTo(Collection c); + + //转移maxElements个元素到集合中 + int drainTo(Collection c, int maxElements); +} +``` + +了解了 `BlockingQueue` 的常见操作后,我们就知道了 `ArrayBlockingQueue` 通过继承 `BlockingQueue` 的方法并实现后,填充到 `AbstractQueue` 的方法上,由此我们便知道了上文中 `AbstractQueue` 的 `add` 方法的 `offer` 方法是哪里是实现的了。 + +```java +public boolean add(E e) { + //AbstractQueue的offer来自下层的ArrayBlockingQueue从BlockingQueue继承并实现的offer方法 + if (offer(e)) + return true; + else + throw new IllegalStateException("Queue full"); +} +``` + +### 初始化 + +了解 `ArrayBlockingQueue` 的细节前,我们不妨先看看其构造函数,了解一下其初始化过程。从源码中我们可以看出 `ArrayBlockingQueue` 有 3 个构造方法,而最核心的构造方法就是下方这一个。 + +```java +// capacity 表示队列初始容量,fair 表示 锁的公平性 +public ArrayBlockingQueue(int capacity, boolean fair) { + //如果设置的队列大小小于0,则直接抛出IllegalArgumentException + if (capacity <= 0) + throw new IllegalArgumentException(); + //初始化一个数组用于存放队列的元素 + this.items = new Object[capacity]; + //创建阻塞队列流程控制的锁 + lock = new ReentrantLock(fair); + //用lock锁创建两个条件控制队列生产和消费 + notEmpty = lock.newCondition(); + notFull = lock.newCondition(); +} +``` + +这个构造方法里面有两个比较核心的成员变量 `notEmpty`(非空) 和 `notFull` (非满) ,需要我们格外留意,它们是实现生产者和消费者有序工作的关键所在,这一点笔者会在后续的源码解析中详细说明,这里我们只需初步了解一下阻塞队列的构造即可。 + +另外两个构造方法都是基于上述的构造方法,默认情况下,我们会使用下面这个构造方法,该构造方法就意味着 `ArrayBlockingQueue` 用的是非公平锁,即各个生产者或者消费者线程收到通知后,对于锁的争抢是随机的。 + +```java + public ArrayBlockingQueue(int capacity) { + this(capacity, false); + } +``` + +还有一个不怎么常用的构造方法,在初始化容量和锁的非公平性之后,它还提供了一个 `Collection` 参数,从源码中不难看出这个构造方法是将外部传入的集合的元素在初始化时直接存放到阻塞队列中。 + +```java +public ArrayBlockingQueue(int capacity, boolean fair, + Collection c) { + //初始化容量和锁的公平性 + this(capacity, fair); + + final ReentrantLock lock = this.lock; + //上锁并将c中的元素存放到ArrayBlockingQueue底层的数组中 + lock.lock(); + try { + int i = 0; + try { + //遍历并添加元素到数组中 + for (E e : c) { + checkNotNull(e); + items[i++] = e; + } + } catch (ArrayIndexOutOfBoundsException ex) { + throw new IllegalArgumentException(); + } + //记录当前队列容量 + count = i; + //更新下一次put或者offer或用add方法添加到队列底层数组的位置 + putIndex = (i == capacity) ? 0 : i; + } finally { + //完成遍历后释放锁 + lock.unlock(); + } +} +``` + +### 阻塞式获取和新增元素 + +`ArrayBlockingQueue` 阻塞式获取和新增元素对应的就是生产者-消费者模型,虽然它也支持非阻塞式获取和新增元素(例如 `poll()` 和 `offer(E e)` 方法,后文会介绍到),但一般不会使用。 + +`ArrayBlockingQueue` 阻塞式获取和新增元素的方法为: + +- `put(E e)`:将元素插入队列中,如果队列已满,则该方法会一直阻塞,直到队列有空间可用或者线程被中断。 +- `take()` :获取并移除队列头部的元素,如果队列为空,则该方法会一直阻塞,直到队列非空或者线程被中断。 + +这两个方法实现的关键就是在于两个条件对象 `notEmpty`(非空) 和 `notFull` (非满),这个我们在上文的构造方法中有提到。 + +接下来笔者就通过两张图让大家了解一下这两个条件是如何在阻塞队列中运用的。 + +![ArrayBlockingQueue 非空条件](https://oss.javaguide.cn/github/javaguide/java/collection/ArrayBlockingQueue-notEmpty-take.png) + +假设我们的代码消费者先启动,当它发现队列中没有数据,那么非空条件就会将这个线程挂起,即等待条件非空时挂起。然后 CPU 执行权到达生产者,生产者发现队列中可以存放数据,于是将数据存放进去,通知此时条件非空,此时消费者就会被唤醒到队列中使用 `take` 等方法获取值了。 + +![ArrayBlockingQueue 非满条件](https://oss.javaguide.cn/github/javaguide/java/collection/ArrayBlockingQueue-notFull-put.png) + +随后的执行中,生产者生产速度远远大于消费者消费速度,于是生产者将队列塞满后再次尝试将数据存入队列,发现队列已满,于是阻塞队列就将当前线程挂起,等待非满。然后消费者拿着 CPU 执行权进行消费,于是队列可以存放新数据了,发出一个非满的通知,此时挂起的生产者就会等待 CPU 执行权到来时再次尝试将数据存到队列中。 + +简单了解阻塞队列的基于两个条件的交互流程之后,我们不妨看看 `put` 和 `take` 方法的源码。 + +```java +public void put(E e) throws InterruptedException { + //确保插入的元素不为null + checkNotNull(e); + //加锁 + final ReentrantLock lock = this.lock; + //这里使用lockInterruptibly()方法而不是lock()方法是为了能够响应中断操作,如果在等待获取锁的过程中被打断则该方法会抛出InterruptedException异常。 + lock.lockInterruptibly(); + try { + //如果count等数组长度则说明队列已满,当前线程将被挂起放到AQS队列中,等待队列非满时插入(非满条件)。 + //在等待期间,锁会被释放,其他线程可以继续对队列进行操作。 + while (count == items.length) + notFull.await(); + //如果队列可以存放元素,则调用enqueue将元素入队 + enqueue(e); + } finally { + //释放锁 + lock.unlock(); + } +} +``` + +`put`方法内部调用了 `enqueue` 方法来实现元素入队,我们继续深入查看一下 `enqueue` 方法的实现细节: + +```java +private void enqueue(E x) { + //获取队列底层的数组 + final Object[] items = this.items; + //将putindex位置的值设置为我们传入的x + items[putIndex] = x; + //更新putindex,如果putindex等于数组长度,则更新为0 + if (++putIndex == items.length) + putIndex = 0; + //队列长度+1 + count++; + //通知队列非空,那些因为获取元素而阻塞的线程可以继续工作了 + notEmpty.signal(); +} +``` + +从源码中可以看到入队操作的逻辑就是在数组中追加一个新元素,整体执行步骤为: + +1. 获取 `ArrayBlockingQueue` 底层的数组 `items`。 +2. 将元素存到 `putIndex` 位置。 +3. 更新 `putIndex` 到下一个位置,如果 `putIndex` 等于队列长度,则说明 `putIndex` 已经到达数组末尾了,下一次插入则需要 0 开始。(`ArrayBlockingQueue` 用到了循环队列的思想,即从头到尾循环复用一个数组) +4. 更新 `count` 的值,表示当前队列长度+1。 +5. 调用 `notEmpty.signal()` 通知队列非空,消费者可以从队列中获取值了。 + +自此我们了解了 `put` 方法的流程,为了更加完整的了解 `ArrayBlockingQueue` 关于生产者-消费者模型的设计,我们继续看看阻塞获取队列元素的 `take` 方法。 + +```java +public E take() throws InterruptedException { + //获取锁 + final ReentrantLock lock = this.lock; + lock.lockInterruptibly(); + try { + //如果队列中元素个数为0,则将当前线程打断并存入AQS队列中,等待队列非空时获取并移除元素(非空条件) + while (count == 0) + notEmpty.await(); + //如果队列不为空则调用dequeue获取元素 + return dequeue(); + } finally { + //释放锁 + lock.unlock(); + } +} +``` + +理解了 `put` 方法再看`take` 方法就很简单了,其核心逻辑和`put` 方法正好是相反的,比如`put` 方法在队列满的时候等待队列非满时插入元素(非满条件),而`take` 方法等待队列非空时获取并移除元素(非空条件)。 + +`take`方法内部调用了 `dequeue` 方法来实现元素出队,其核心逻辑和 `enqueue` 方法也是相反的。 + +```java +private E dequeue() { + //获取阻塞队列底层的数组 + final Object[] items = this.items; + @SuppressWarnings("unchecked") + //从队列中获取takeIndex位置的元素 + E x = (E) items[takeIndex]; + //将takeIndex置空 + items[takeIndex] = null; + //takeIndex向后挪动,如果等于数组长度则更新为0 + if (++takeIndex == items.length) + takeIndex = 0; + //队列长度减1 + count--; + if (itrs != null) + itrs.elementDequeued(); + //通知那些被打断的线程当前队列状态非满,可以继续存放元素 + notFull.signal(); + return x; +} +``` + +由于`dequeue` 方法(出队)和上面介绍的 `enqueue` 方法(入队)的步骤大致类似,这里就不重复介绍了。 + +为了帮助理解,我专门画了一张图来展示 `notEmpty`(非空) 和 `notFull` (非满)这两个条件对象是如何控制 `ArrayBlockingQueue` 的存和取的。 + +![ArrayBlockingQueue 非空非满](https://oss.javaguide.cn/github/javaguide/java/collection/ArrayBlockingQueue-notEmpty-notFull.png) + +- **消费者**:当消费者从队列中 `take` 或者 `poll` 等操作取出一个元素之后,就会通知队列非满,此时那些等待非满的生产者就会被唤醒等待获取 CPU 时间片进行入队操作。 +- **生产者**:当生产者将元素存到队列中后,就会触发通知队列非空,此时消费者就会被唤醒等待 CPU 时间片尝试获取元素。如此往复,两个条件对象就构成一个环路,控制着多线程之间的存和取。 + +### 非阻塞式获取和新增元素 + +`ArrayBlockingQueue` 非阻塞式获取和新增元素的方法为: + +- `offer(E e)`:将元素插入队列尾部。如果队列已满,则该方法会直接返回 false,不会等待并阻塞线程。 +- `poll()`:获取并移除队列头部的元素,如果队列为空,则该方法会直接返回 null,不会等待并阻塞线程。 +- `add(E e)`:将元素插入队列尾部。如果队列已满则会抛出 `IllegalStateException` 异常,底层基于 `offer(E e)` 方法。 +- `remove()`:移除队列头部的元素,如果队列为空则会抛出 `NoSuchElementException` 异常,底层基于 `poll()`。 +- `peek()`:获取但不移除队列头部的元素,如果队列为空,则该方法会直接返回 null,不会等待并阻塞线程。 + +先来看看 `offer` 方法,逻辑和 `put` 差不多,唯一的区别就是入队失败时不会阻塞当前线程,而是直接返回 `false`。 + +```java +public boolean offer(E e) { + //确保插入的元素不为null + checkNotNull(e); + //获取锁 + final ReentrantLock lock = this.lock; + lock.lock(); + try { + //队列已满直接返回false + if (count == items.length) + return false; + else { + //反之将元素入队并直接返回true + enqueue(e); + return true; + } + } finally { + //释放锁 + lock.unlock(); + } + } +``` + +`poll` 方法同理,获取元素失败也是直接返回空,并不会阻塞获取元素的线程。 + +```java +public E poll() { + final ReentrantLock lock = this.lock; + //上锁 + lock.lock(); + try { + //如果队列为空直接返回null,反之出队返回元素值 + return (count == 0) ? null : dequeue(); + } finally { + lock.unlock(); + } + } +``` + +`add` 方法其实就是对于 `offer` 做了一层封装,如下代码所示,可以看到 `add` 会调用没有规定时间的 `offer`,如果入队失败则直接抛异常。 + +```java +public boolean add(E e) { + return super.add(e); + } + + +public boolean add(E e) { + //调用offer方法如果失败直接抛出异常 + if (offer(e)) + return true; + else + throw new IllegalStateException("Queue full"); + } +``` + +`remove` 方法同理,调用 `poll`,如果返回 `null` 则说明队列没有元素,直接抛出异常。 + +```java +public E remove() { + E x = poll(); + if (x != null) + return x; + else + throw new NoSuchElementException(); + } +``` + +`peek()` 方法的逻辑也很简单,内部调用了 `itemAt` 方法。 + +```java +public E peek() { + //加锁 + final ReentrantLock lock = this.lock; + lock.lock(); + try { + //当队列为空时返回 null + return itemAt(takeIndex); + } finally { + //释放锁 + lock.unlock(); + } + } + +//返回队列中指定位置的元素 +@SuppressWarnings("unchecked") +final E itemAt(int i) { + return (E) items[i]; +} +``` + +### 指定超时时间内阻塞式获取和新增元素 + +在 `offer(E e)` 和 `poll()` 非阻塞获取和新增元素的基础上,设计者提供了带有等待时间的 `offer(E e, long timeout, TimeUnit unit)` 和 `poll(long timeout, TimeUnit unit)` ,用于在指定的超时时间内阻塞式地添加和获取元素。 + +```java + public boolean offer(E e, long timeout, TimeUnit unit) + throws InterruptedException { + + checkNotNull(e); + long nanos = unit.toNanos(timeout); + final ReentrantLock lock = this.lock; + lock.lockInterruptibly(); + try { + //队列已满,进入循环 + while (count == items.length) { + //时间到了队列还是满的,则直接返回false + if (nanos <= 0) + return false; + //阻塞nanos时间,等待非满 + nanos = notFull.awaitNanos(nanos); + } + enqueue(e); + return true; + } finally { + lock.unlock(); + } + } +``` + +可以看到,带有超时时间的 `offer` 方法在队列已满的情况下,会等待用户所传的时间段,如果规定时间内还不能存放元素则直接返回 `false`。 + +```java +public E poll(long timeout, TimeUnit unit) throws InterruptedException { + long nanos = unit.toNanos(timeout); + final ReentrantLock lock = this.lock; + lock.lockInterruptibly(); + try { + //队列为空,循环等待,若时间到还是空的,则直接返回null + while (count == 0) { + if (nanos <= 0) + return null; + nanos = notEmpty.awaitNanos(nanos); + } + return dequeue(); + } finally { + lock.unlock(); + } + } +``` + +同理,带有超时时间的 `poll` 也一样,队列为空则在规定时间内等待,若时间到了还是空的,则直接返回 null。 + +### 判断元素是否存在 + +`ArrayBlockingQueue` 提供了 `contains(Object o)` 来判断指定元素是否存在于队列中。 + +```java +public boolean contains(Object o) { + //若目标元素为空,则直接返回 false + if (o == null) return false; + //获取当前队列的元素数组 + final Object[] items = this.items; + //加锁 + final ReentrantLock lock = this.lock; + lock.lock(); + try { + // 如果队列非空 + if (count > 0) { + final int putIndex = this.putIndex; + //从队列头部开始遍历 + int i = takeIndex; + do { + if (o.equals(items[i])) + return true; + if (++i == items.length) + i = 0; + } while (i != putIndex); + } + return false; + } finally { + //释放锁 + lock.unlock(); + } +} +``` + +## ArrayBlockingQueue 获取和新增元素的方法对比 + +为了帮助理解 `ArrayBlockingQueue` ,我们再来对比一下上面提到的这些获取和新增元素的方法。 + +新增元素: + +| 方法 | 队列满时处理方式 | 方法返回值 | +| ----------------------------------------- | -------------------------------------------------------- | ---------- | +| `put(E e)` | 线程阻塞,直到中断或被唤醒 | void | +| `offer(E e)` | 直接返回 false | boolean | +| `offer(E e, long timeout, TimeUnit unit)` | 指定超时时间内阻塞,超过规定时间还未添加成功则返回 false | boolean | +| `add(E e)` | 直接抛出 `IllegalStateException` 异常 | boolean | + +获取/移除元素: + +| 方法 | 队列空时处理方式 | 方法返回值 | +| ----------------------------------- | --------------------------------------------------- | ---------- | +| `take()` | 线程阻塞,直到中断或被唤醒 | E | +| `poll()` | 返回 null | E | +| `poll(long timeout, TimeUnit unit)` | 指定超时时间内阻塞,超过规定时间还是空的则返回 null | E | +| `peek()` | 返回 null | E | +| `remove()` | 直接抛出 `NoSuchElementException` 异常 | boolean | + +![](https://oss.javaguide.cn/github/javaguide/java/collection/ArrayBlockingQueue-get-add-element-methods.png) + +## ArrayBlockingQueue 相关面试题 + +### ArrayBlockingQueue 是什么?它的特点是什么? + +`ArrayBlockingQueue` 是 `BlockingQueue` 接口的有界队列实现类,常用于多线程之间的数据共享,底层采用数组实现,从其名字就能看出来了。 + +`ArrayBlockingQueue` 的容量有限,一旦创建,容量不能改变。 + +为了保证线程安全,`ArrayBlockingQueue` 的并发控制采用可重入锁 `ReentrantLock` ,不管是插入操作还是读取操作,都需要获取到锁才能进行操作。并且,它还支持公平和非公平两种方式的锁访问机制,默认是非公平锁。 + +`ArrayBlockingQueue` 虽名为阻塞队列,但也支持非阻塞获取和新增元素(例如 `poll()` 和 `offer(E e)` 方法),只是队列满时添加元素会抛出异常,队列为空时获取的元素为 null,一般不会使用。 + +### ArrayBlockingQueue 和 LinkedBlockingQueue 有什么区别? + +`ArrayBlockingQueue` 和 `LinkedBlockingQueue` 是 Java 并发包中常用的两种阻塞队列实现,它们都是线程安全的。不过,不过它们之间也存在下面这些区别: + +- 底层实现:`ArrayBlockingQueue` 基于数组实现,而 `LinkedBlockingQueue` 基于链表实现。 +- 是否有界:`ArrayBlockingQueue` 是有界队列,必须在创建时指定容量大小。`LinkedBlockingQueue` 创建时可以不指定容量大小,默认是`Integer.MAX_VALUE`,也就是无界的。但也可以指定队列大小,从而成为有界的。 +- 锁是否分离: `ArrayBlockingQueue`中的锁是没有分离的,即生产和消费用的是同一个锁;`LinkedBlockingQueue`中的锁是分离的,即生产用的是`putLock`,消费是`takeLock`,这样可以防止生产者和消费者线程之间的锁争夺。 +- 内存占用:`ArrayBlockingQueue` 需要提前分配数组内存,而 `LinkedBlockingQueue` 则是动态分配链表节点内存。这意味着,`ArrayBlockingQueue` 在创建时就会占用一定的内存空间,且往往申请的内存比实际所用的内存更大,而`LinkedBlockingQueue` 则是根据元素的增加而逐渐占用内存空间。 + +### ArrayBlockingQueue 和 ConcurrentLinkedQueue 有什么区别? + +`ArrayBlockingQueue` 和 `ConcurrentLinkedQueue` 是 Java 并发包中常用的两种队列实现,它们都是线程安全的。不过,不过它们之间也存在下面这些区别: + +- 底层实现:`ArrayBlockingQueue` 基于数组实现,而 `ConcurrentLinkedQueue` 基于链表实现。 +- 是否有界:`ArrayBlockingQueue` 是有界队列,必须在创建时指定容量大小,而 `ConcurrentLinkedQueue` 是无界队列,可以动态地增加容量。 +- 是否阻塞:`ArrayBlockingQueue` 支持阻塞和非阻塞两种获取和新增元素的方式(一般只会使用前者), `ConcurrentLinkedQueue` 是无界的,仅支持非阻塞式获取和新增元素。 + +### ArrayBlockingQueue 的实现原理是什么? + +`ArrayBlockingQueue` 的实现原理主要分为以下几点(这里以阻塞式获取和新增元素为例介绍): + +- `ArrayBlockingQueue` 内部维护一个定长的数组用于存储元素。 +- 通过使用 `ReentrantLock` 锁对象对读写操作进行同步,即通过锁机制来实现线程安全。 +- 通过 `Condition` 实现线程间的等待和唤醒操作。 + +这里再详细介绍一下线程间的等待和唤醒具体的实现(不需要记具体的方法,面试中回答要点即可): + +- 当队列已满时,生产者线程会调用 `notFull.await()` 方法让生产者进行等待,等待队列非满时插入(非满条件)。 +- 当队列为空时,消费者线程会调用 `notEmpty.await()`方法让消费者进行等待,等待队列非空时消费(非空条件)。 +- 当有新的元素被添加时,生产者线程会调用 `notEmpty.signal()`方法唤醒正在等待消费的消费者线程。 +- 当队列中有元素被取出时,消费者线程会调用 `notFull.signal()`方法唤醒正在等待插入元素的生产者线程。 + +关于 `Condition`接口的补充: + +> `Condition`是 JDK1.5 之后才有的,它具有很好的灵活性,比如可以实现多路通知功能也就是在一个`Lock`对象中可以创建多个`Condition`实例(即对象监视器),**线程对象可以注册在指定的`Condition`中,从而可以有选择性的进行线程通知,在调度线程上更加灵活。 在使用`notify()/notifyAll()`方法进行通知时,被通知的线程是由 JVM 选择的,用`ReentrantLock`类结合`Condition`实例可以实现“选择性通知”** ,这个功能非常重要,而且是 `Condition` 接口默认提供的。而`synchronized`关键字就相当于整个 `Lock` 对象中只有一个`Condition`实例,所有的线程都注册在它一个身上。如果执行`notifyAll()`方法的话就会通知所有处于等待状态的线程,这样会造成很大的效率问题。而`Condition`实例的`signalAll()`方法,只会唤醒注册在该`Condition`实例中的所有等待线程。 + +## 参考文献 + +- 深入理解 Java 系列 | BlockingQueue 用法详解: +- 深入浅出阻塞队列 BlockingQueue 及其典型实现 ArrayBlockingQueue: +- 并发编程大扫盲:ArrayBlockingQueue 底层原理和实战: + diff --git a/docs/java/collection/arraylist-source-code.md b/docs/java/collection/arraylist-source-code.md index bd5d2d294f0..5c71801b699 100644 --- a/docs/java/collection/arraylist-source-code.md +++ b/docs/java/collection/arraylist-source-code.md @@ -5,11 +5,13 @@ tag: - Java集合 --- + + ## ArrayList 简介 `ArrayList` 的底层是数组队列,相当于动态数组。与 Java 中的数组相比,它的容量能动态增长。在添加大量元素前,应用程序可以使用`ensureCapacity`操作来增加 `ArrayList` 实例的容量。这可以减少递增式再分配的数量。 -`ArrayList`继承于 **`AbstractList`** ,实现了 **`List`**, **`RandomAccess`**, **`Cloneable`**, **`java.io.Serializable`** 这些接口。 +`ArrayList` 继承于 `AbstractList` ,实现了 `List`, `RandomAccess`, `Cloneable`, `java.io.Serializable` 这些接口。 ```java @@ -19,36 +21,54 @@ public class ArrayList extends AbstractList } ``` -- `RandomAccess` 是一个标志接口,表明实现这个接口的 List 集合是支持**快速随机访问**的。在 `ArrayList` 中,我们即可以通过元素的序号快速获取元素对象,这就是快速随机访问。 -- `ArrayList` 实现了 **`Cloneable` 接口** ,即覆盖了函数`clone()`,能被克隆。 -- `ArrayList` 实现了 `java.io.Serializable`接口,这意味着`ArrayList`支持序列化,能通过序列化去传输。 +- `List` : 表明它是一个列表,支持添加、删除、查找等操作,并且可以通过下标进行访问。 +- `RandomAccess` :这是一个标志接口,表明实现这个接口的 `List` 集合是支持 **快速随机访问** 的。在 `ArrayList` 中,我们即可以通过元素的序号快速获取元素对象,这就是快速随机访问。 +- `Cloneable` :表明它具有拷贝能力,可以进行深拷贝或浅拷贝操作。 +- `Serializable` : 表明它可以进行序列化操作,也就是可以将对象转换为字节流进行持久化存储或网络传输,非常方便。 -### Arraylist 和 Vector 的区别? +![ArrayList 类图](https://oss.javaguide.cn/github/javaguide/java/collection/arraylist-class-diagram.png) -1. `ArrayList` 是 `List` 的主要实现类,底层使用 `Object[ ]`存储,适用于频繁的查找工作,线程不安全 ; -2. `Vector` 是 `List` 的古老实现类,底层使用 `Object[ ]`存储,线程安全的。 +### ArrayList 和 Vector 的区别?(了解即可) -### Arraylist 与 LinkedList 区别? +- `ArrayList` 是 `List` 的主要实现类,底层使用 `Object[]`存储,适用于频繁的查找工作,线程不安全 。 +- `Vector` 是 `List` 的古老实现类,底层使用`Object[]` 存储,线程安全。 -1. **是否保证线程安全:** `ArrayList` 和 `LinkedList` 都是不同步的,也就是不保证线程安全; -2. **底层数据结构:** `Arraylist` 底层使用的是 **`Object` 数组**;`LinkedList` 底层使用的是 **双向链表** 数据结构(JDK1.6 之前为循环链表,JDK1.7 取消了循环。注意双向链表和双向循环链表的区别,下面有介绍到!) -3. **插入和删除是否受元素位置的影响:** ① **`ArrayList` 采用数组存储,所以插入和删除元素的时间复杂度受元素位置的影响。** 比如:执行`add(E e)`方法的时候, `ArrayList` 会默认在将指定的元素追加到此列表的末尾,这种情况时间复杂度就是 O(1)。但是如果要在指定位置 i 插入和删除元素的话(`add(int index, E element)`)时间复杂度就为 O(n-i)。因为在进行上述操作的时候集合中第 i 和第 i 个元素之后的(n-i)个元素都要执行向后位/向前移一位的操作。 ② **`LinkedList` 采用链表存储,所以对于`add(E e)`方法的插入,删除元素时间复杂度不受元素位置的影响,近似 O(1),如果是要在指定位置`i`插入和删除元素的话(`(add(int index, E element)`) 时间复杂度近似为`o(n))`因为需要先移动到指定位置再插入。** -4. **是否支持快速随机访问:** `LinkedList` 不支持高效的随机元素访问,而 `ArrayList` 支持。快速随机访问就是通过元素的序号快速获取元素对象(对应于`get(int index)`方法)。 -5. **内存空间占用:** `ArrayList` 的空 间浪费主要体现在在 list 列表的结尾会预留一定的容量空间,而 `LinkedList` 的空间花费则体现在它的每一个元素都需要消耗比 `ArrayList` 更多的空间(因为要存放直接后继和直接前驱以及数据)。 +### ArrayList 可以添加 null 值吗? -## ArrayList 核心源码解读 +`ArrayList` 中可以存储任何类型的对象,包括 `null` 值。不过,不建议向`ArrayList` 中添加 `null` 值, `null` 值无意义,会让代码难以维护比如忘记做判空处理就会导致空指针异常。 + +示例代码: ```java -package java.util; +ArrayList listOfStrings = new ArrayList<>(); +listOfStrings.add(null); +listOfStrings.add("java"); +System.out.println(listOfStrings); +``` + +输出: + +```plain +[null, java] +``` + +### Arraylist 与 LinkedList 区别? + +- **是否保证线程安全:** `ArrayList` 和 `LinkedList` 都是不同步的,也就是不保证线程安全; +- **底层数据结构:** `ArrayList` 底层使用的是 **`Object` 数组**;`LinkedList` 底层使用的是 **双向链表** 数据结构(JDK1.6 之前为循环链表,JDK1.7 取消了循环。注意双向链表和双向循环链表的区别,下面有介绍到!) +- **插入和删除是否受元素位置的影响:** + - `ArrayList` 采用数组存储,所以插入和删除元素的时间复杂度受元素位置的影响。 比如:执行`add(E e)`方法的时候, `ArrayList` 会默认在将指定的元素追加到此列表的末尾,这种情况时间复杂度就是 O(1)。但是如果要在指定位置 i 插入和删除元素的话(`add(int index, E element)`),时间复杂度就为 O(n)。因为在进行上述操作的时候集合中第 i 和第 i 个元素之后的(n-i)个元素都要执行向后位/向前移一位的操作。 + - `LinkedList` 采用链表存储,所以在头尾插入或者删除元素不受元素位置的影响(`add(E e)`、`addFirst(E e)`、`addLast(E e)`、`removeFirst()`、 `removeLast()`),时间复杂度为 O(1),如果是要在指定位置 `i` 插入和删除元素的话(`add(int index, E element)`,`remove(Object o)`,`remove(int index)`), 时间复杂度为 O(n) ,因为需要先移动到指定位置再插入和删除。 +- **是否支持快速随机访问:** `LinkedList` 不支持高效的随机元素访问,而 `ArrayList`(实现了 `RandomAccess` 接口) 支持。快速随机访问就是通过元素的序号快速获取元素对象(对应于`get(int index)`方法)。 +- **内存空间占用:** `ArrayList` 的空间浪费主要体现在在 list 列表的结尾会预留一定的容量空间,而 LinkedList 的空间花费则体现在它的每一个元素都需要消耗比 ArrayList 更多的空间(因为要存放直接后继和直接前驱以及数据)。 -import java.util.function.Consumer; -import java.util.function.Predicate; -import java.util.function.UnaryOperator; +## ArrayList 核心源码解读 +这里以 JDK1.8 为例,分析一下 `ArrayList` 的底层源码。 +```java public class ArrayList extends AbstractList - implements List, RandomAccess, Cloneable, java.io.Serializable -{ + implements List, RandomAccess, Cloneable, java.io.Serializable { private static final long serialVersionUID = 8683452581122892189L; /** @@ -61,8 +81,8 @@ public class ArrayList extends AbstractList */ private static final Object[] EMPTY_ELEMENTDATA = {}; - //用于默认大小空实例的共享空数组实例。 - //我们把它从EMPTY_ELEMENTDATA数组中区分出来,以知道在添加第一个元素时容量需要增加多少。 + //用于默认大小空实例的共享空数组实例。 + //我们把它从EMPTY_ELEMENTDATA数组中区分出来,以知道在添加第一个元素时容量需要增加多少。 private static final Object[] DEFAULTCAPACITY_EMPTY_ELEMENTDATA = {}; /** @@ -87,14 +107,14 @@ public class ArrayList extends AbstractList this.elementData = EMPTY_ELEMENTDATA; } else { //其他情况,抛出异常 - throw new IllegalArgumentException("Illegal Capacity: "+ - initialCapacity); + throw new IllegalArgumentException("Illegal Capacity: " + + initialCapacity); } } /** - *默认无参构造函数 - *DEFAULTCAPACITY_EMPTY_ELEMENTDATA 为0.初始化为10,也就是说初始其实是空数组 当添加第一个元素的时候数组容量才变成10 + * 默认无参构造函数 + * DEFAULTCAPACITY_EMPTY_ELEMENTDATA 为0.初始化为10,也就是说初始其实是空数组 当添加第一个元素的时候数组容量才变成10 */ public ArrayList() { this.elementData = DEFAULTCAPACITY_EMPTY_ELEMENTDATA; @@ -125,44 +145,54 @@ public class ArrayList extends AbstractList modCount++; if (size < elementData.length) { elementData = (size == 0) - ? EMPTY_ELEMENTDATA - : Arrays.copyOf(elementData, size); + ? EMPTY_ELEMENTDATA + : Arrays.copyOf(elementData, size); } } //下面是ArrayList的扩容机制 //ArrayList的扩容机制提高了性能,如果每次只扩充一个, //那么频繁的插入会导致频繁的拷贝,降低性能,而ArrayList的扩容机制避免了这种情况。 + /** * 如有必要,增加此ArrayList实例的容量,以确保它至少能容纳元素的数量 - * @param minCapacity 所需的最小容量 + * + * @param minCapacity 所需的最小容量 */ public void ensureCapacity(int minCapacity) { - //如果是true,minExpand的值为0,如果是false,minExpand的值为10 + // 如果不是默认空数组,则minExpand的值为0; + // 如果是默认空数组,则minExpand的值为10 int minExpand = (elementData != DEFAULTCAPACITY_EMPTY_ELEMENTDATA) - // any size if not default element table - ? 0 - // larger than default for default empty table. It's already - // supposed to be at default size. - : DEFAULT_CAPACITY; - //如果最小容量大于已有的最大容量 + // 如果不是默认元素表,则可以使用任意大小 + ? 0 + // 如果是默认空数组,它应该已经是默认大小 + : DEFAULT_CAPACITY; + + // 如果最小容量大于已有的最大容量 if (minCapacity > minExpand) { + // 根据需要的最小容量,确保容量足够 ensureExplicitCapacity(minCapacity); } } - //1.得到最小扩容量 - //2.通过最小容量扩容 - private void ensureCapacityInternal(int minCapacity) { + + + // 根据给定的最小容量和当前数组元素来计算所需容量。 + private static int calculateCapacity(Object[] elementData, int minCapacity) { + // 如果当前数组元素为空数组(初始情况),返回默认容量和最小容量中的较大值作为所需容量 if (elementData == DEFAULTCAPACITY_EMPTY_ELEMENTDATA) { - // 获取“默认的容量”和“传入参数”两者之间的最大值 - minCapacity = Math.max(DEFAULT_CAPACITY, minCapacity); + return Math.max(DEFAULT_CAPACITY, minCapacity); } + // 否则直接返回最小容量 + return minCapacity; + } - ensureExplicitCapacity(minCapacity); + // 确保内部容量达到指定的最小容量。 + private void ensureCapacityInternal(int minCapacity) { + ensureExplicitCapacity(calculateCapacity(elementData, minCapacity)); } - //判断是否需要扩容 + + //判断是否需要扩容 private void ensureExplicitCapacity(int minCapacity) { modCount++; - // overflow-conscious code if (minCapacity - elementData.length > 0) //调用grow方法进行扩容,调用此方法代表已经开始扩容了 @@ -188,23 +218,24 @@ public class ArrayList extends AbstractList newCapacity = minCapacity; //再检查新容量是否超出了ArrayList所定义的最大容量, //若超出了,则调用hugeCapacity()来比较minCapacity和 MAX_ARRAY_SIZE, - //如果minCapacity大于MAX_ARRAY_SIZE,则新容量则为Interger.MAX_VALUE,否则,新容量大小则为 MAX_ARRAY_SIZE。 + //如果minCapacity大于MAX_ARRAY_SIZE,则新容量则为Integer.MAX_VALUE,否则,新容量大小则为 MAX_ARRAY_SIZE。 if (newCapacity - MAX_ARRAY_SIZE > 0) newCapacity = hugeCapacity(minCapacity); // minCapacity is usually close to size, so this is a win: elementData = Arrays.copyOf(elementData, newCapacity); } + //比较minCapacity和 MAX_ARRAY_SIZE private static int hugeCapacity(int minCapacity) { if (minCapacity < 0) // overflow throw new OutOfMemoryError(); return (minCapacity > MAX_ARRAY_SIZE) ? - Integer.MAX_VALUE : - MAX_ARRAY_SIZE; + Integer.MAX_VALUE : + MAX_ARRAY_SIZE; } /** - *返回此列表中的元素数。 + * 返回此列表中的元素数。 */ public int size() { return size; @@ -227,12 +258,12 @@ public class ArrayList extends AbstractList } /** - *返回此列表中指定元素的首次出现的索引,如果此列表不包含此元素,则为-1 + * 返回此列表中指定元素的首次出现的索引,如果此列表不包含此元素,则为-1 */ public int indexOf(Object o) { if (o == null) { for (int i = 0; i < size; i++) - if (elementData[i]==null) + if (elementData[i] == null) return i; } else { for (int i = 0; i < size; i++) @@ -248,11 +279,11 @@ public class ArrayList extends AbstractList */ public int lastIndexOf(Object o) { if (o == null) { - for (int i = size-1; i >= 0; i--) - if (elementData[i]==null) + for (int i = size - 1; i >= 0; i--) + if (elementData[i] == null) return i; } else { - for (int i = size-1; i >= 0; i--) + for (int i = size - 1; i >= 0; i--) if (o.equals(elementData[i])) return i; } @@ -276,9 +307,12 @@ public class ArrayList extends AbstractList } /** - *以正确的顺序(从第一个到最后一个元素)返回一个包含此列表中所有元素的数组。 - *返回的数组将是“安全的”,因为该列表不保留对它的引用。 (换句话说,这个方法必须分配一个新的数组)。 - *因此,调用者可以自由地修改返回的数组。 此方法充当基于阵列和基于集合的API之间的桥梁。 + * 以正确的顺序(从第一个到最后一个元素)返回一个包含此列表中所有元素的数组。 + * 返回的数组将是“安全的”,因为该列表不保留对它的引用。 + * (换句话说,这个方法必须分配一个新的数组)。 + * 因此,调用者可以自由地修改返回的数组结构。 + * 注意:如果元素是引用类型,修改元素的内容会影响到原列表中的对象。 + * 此方法充当基于数组和基于集合的API之间的桥梁。 */ public Object[] toArray() { return Arrays.copyOf(elementData, size); @@ -286,17 +320,17 @@ public class ArrayList extends AbstractList /** * 以正确的顺序返回一个包含此列表中所有元素的数组(从第一个到最后一个元素); - *返回的数组的运行时类型是指定数组的运行时类型。 如果列表适合指定的数组,则返回其中。 - *否则,将为指定数组的运行时类型和此列表的大小分配一个新数组。 - *如果列表适用于指定的数组,其余空间(即数组的列表数量多于此元素),则紧跟在集合结束后的数组中的元素设置为null 。 - *(这仅在调用者知道列表不包含任何空元素的情况下才能确定列表的长度。) + * 返回的数组的运行时类型是指定数组的运行时类型。 如果列表适合指定的数组,则返回其中。 + * 否则,将为指定数组的运行时类型和此列表的大小分配一个新数组。 + * 如果列表适用于指定的数组,其余空间(即数组的列表数量多于此元素),则紧跟在集合结束后的数组中的元素设置为null 。 + * (这仅在调用者知道列表不包含任何空元素的情况下才能确定列表的长度。) */ @SuppressWarnings("unchecked") public T[] toArray(T[] a) { if (a.length < size) // 新建一个运行时类型的数组,但是ArrayList数组的内容 return (T[]) Arrays.copyOf(elementData, size, a.getClass()); - //调用System提供的arraycopy()方法实现数组之间的复制 + //调用System提供的arraycopy()方法实现数组之间的复制 System.arraycopy(elementData, 0, a, 0, size); if (a.length > size) a[size] = null; @@ -344,8 +378,8 @@ public class ArrayList extends AbstractList /** * 在此列表中的指定位置插入指定的元素。 - *先调用 rangeCheckForAdd 对index进行界限检查;然后调用 ensureCapacityInternal 方法保证capacity足够大; - *再将从index开始之后的所有成员后移一个位置;将element插入index位置;最后size加1。 + * 先调用 rangeCheckForAdd 对index进行界限检查;然后调用 ensureCapacityInternal 方法保证capacity足够大; + * 再将从index开始之后的所有成员后移一个位置;将element插入index位置;最后size加1。 */ public void add(int index, E element) { rangeCheckForAdd(index); @@ -353,7 +387,7 @@ public class ArrayList extends AbstractList ensureCapacityInternal(size + 1); // Increments modCount!! //arraycopy()这个实现数组之间复制的方法一定要看一下,下面就用到了arraycopy()方法实现数组自己复制自己 System.arraycopy(elementData, index, elementData, index + 1, - size - index); + size - index); elementData[index] = element; size++; } @@ -369,16 +403,16 @@ public class ArrayList extends AbstractList int numMoved = size - index - 1; if (numMoved > 0) - System.arraycopy(elementData, index+1, elementData, index, - numMoved); + System.arraycopy(elementData, index + 1, elementData, index, + numMoved); elementData[--size] = null; // clear to let GC do its work - //从列表中删除的元素 + //从列表中删除的元素 return oldValue; } /** * 从列表中删除指定元素的第一个出现(如果存在)。 如果列表不包含该元素,则它不会更改。 - *返回true,如果此列表包含指定的元素 + * 返回true,如果此列表包含指定的元素 */ public boolean remove(Object o) { if (o == null) { @@ -398,16 +432,15 @@ public class ArrayList extends AbstractList } /* - * Private remove method that skips bounds checking and does not - * return the value removed. + * 该方法为私有的移除方法,跳过了边界检查,并且不返回被移除的值。 */ private void fastRemove(int index) { modCount++; int numMoved = size - index - 1; if (numMoved > 0) - System.arraycopy(elementData, index+1, elementData, index, - numMoved); - elementData[--size] = null; // clear to let GC do its work + System.arraycopy(elementData, index + 1, elementData, index, + numMoved); + elementData[--size] = null; // 在移除元素后,将该位置的元素设为 null,以便垃圾回收器(GC)能够回收该元素。 } /** @@ -448,7 +481,7 @@ public class ArrayList extends AbstractList int numMoved = size - index; if (numMoved > 0) System.arraycopy(elementData, index, elementData, index + numNew, - numMoved); + numMoved); System.arraycopy(a, 0, elementData, index, numNew); size += numNew; @@ -457,16 +490,16 @@ public class ArrayList extends AbstractList /** * 从此列表中删除所有索引为fromIndex (含)和toIndex之间的元素。 - *将任何后续元素移动到左侧(减少其索引)。 + * 将任何后续元素移动到左侧(减少其索引)。 */ protected void removeRange(int fromIndex, int toIndex) { modCount++; int numMoved = size - toIndex; System.arraycopy(elementData, toIndex, elementData, fromIndex, - numMoved); + numMoved); // clear to let GC do its work - int newSize = size - (toIndex-fromIndex); + int newSize = size - (toIndex - fromIndex); for (int i = newSize; i < size; i++) { elementData[i] = null; } @@ -493,7 +526,7 @@ public class ArrayList extends AbstractList * 返回IndexOutOfBoundsException细节信息 */ private String outOfBoundsMsg(int index) { - return "Index: "+index+", Size: "+size; + return "Index: " + index + ", Size: " + size; } /** @@ -507,7 +540,7 @@ public class ArrayList extends AbstractList /** * 仅保留此列表中包含在指定集合中的元素。 - *换句话说,从此列表中删除其中不包含在指定集合中的所有元素。 + * 换句话说,从此列表中删除其中不包含在指定集合中的所有元素。 */ public boolean retainAll(Collection c) { Objects.requireNonNull(c); @@ -517,227 +550,220 @@ public class ArrayList extends AbstractList /** * 从列表中的指定位置开始,返回列表中的元素(按正确顺序)的列表迭代器。 - *指定的索引表示初始调用将返回的第一个元素为next 。 初始调用previous将返回指定索引减1的元素。 - *返回的列表迭代器是fail-fast 。 + * 指定的索引表示初始调用将返回的第一个元素为next 。 初始调用previous将返回指定索引减1的元素。 + * 返回的列表迭代器是fail-fast 。 */ public ListIterator listIterator(int index) { if (index < 0 || index > size) - throw new IndexOutOfBoundsException("Index: "+index); + throw new IndexOutOfBoundsException("Index: " + index); return new ListItr(index); } /** - *返回列表中的列表迭代器(按适当的顺序)。 - *返回的列表迭代器是fail-fast 。 + * 返回列表中的列表迭代器(按适当的顺序)。 + * 返回的列表迭代器是fail-fast 。 */ public ListIterator listIterator() { return new ListItr(0); } /** - *以正确的顺序返回该列表中的元素的迭代器。 - *返回的迭代器是fail-fast 。 + * 以正确的顺序返回该列表中的元素的迭代器。 + * 返回的迭代器是fail-fast 。 */ public Iterator iterator() { return new Itr(); } - - ``` ## ArrayList 扩容机制分析 ### 先从 ArrayList 的构造函数说起 -**(JDK8)ArrayList 有三种方式来初始化,构造方法源码如下:** +ArrayList 有三种方式来初始化,构造方法源码如下(JDK8): ```java - /** - * 默认初始容量大小 - */ - private static final int DEFAULT_CAPACITY = 10; - - - private static final Object[] DEFAULTCAPACITY_EMPTY_ELEMENTDATA = {}; - - /** - *默认构造函数,使用初始容量10构造一个空列表(无参数构造) - */ - public ArrayList() { - this.elementData = DEFAULTCAPACITY_EMPTY_ELEMENTDATA; - } +/** + * 默认初始容量大小 + */ +private static final int DEFAULT_CAPACITY = 10; + +private static final Object[] DEFAULTCAPACITY_EMPTY_ELEMENTDATA = {}; + +/** + * 默认构造函数,使用初始容量10构造一个空列表(无参数构造) + */ +public ArrayList() { + this.elementData = DEFAULTCAPACITY_EMPTY_ELEMENTDATA; +} - /** - * 带初始容量参数的构造函数。(用户自己指定容量) - */ - public ArrayList(int initialCapacity) { - if (initialCapacity > 0) {//初始容量大于0 - //创建initialCapacity大小的数组 - this.elementData = new Object[initialCapacity]; - } else if (initialCapacity == 0) {//初始容量等于0 - //创建空数组 - this.elementData = EMPTY_ELEMENTDATA; - } else {//初始容量小于0,抛出异常 - throw new IllegalArgumentException("Illegal Capacity: "+ - initialCapacity); - } +/** + * 带初始容量参数的构造函数。(用户自己指定容量) + */ +public ArrayList(int initialCapacity) { + if (initialCapacity > 0) {//初始容量大于0 + //创建initialCapacity大小的数组 + this.elementData = new Object[initialCapacity]; + } else if (initialCapacity == 0) {//初始容量等于0 + //创建空数组 + this.elementData = EMPTY_ELEMENTDATA; + } else {//初始容量小于0,抛出异常 + throw new IllegalArgumentException("Illegal Capacity: " + initialCapacity); } +} - /** - *构造包含指定collection元素的列表,这些元素利用该集合的迭代器按顺序返回 - *如果指定的集合为null,throws NullPointerException。 - */ - public ArrayList(Collection c) { - elementData = c.toArray(); - if ((size = elementData.length) != 0) { - // c.toArray might (incorrectly) not return Object[] (see 6260652) - if (elementData.getClass() != Object[].class) - elementData = Arrays.copyOf(elementData, size, Object[].class); - } else { - // replace with empty array. - this.elementData = EMPTY_ELEMENTDATA; - } +/** + *构造包含指定collection元素的列表,这些元素利用该集合的迭代器按顺序返回 + *如果指定的集合为null,throws NullPointerException。 + */ +public ArrayList(Collection c) { + elementData = c.toArray(); + if ((size = elementData.length) != 0) { + // c.toArray might (incorrectly) not return Object[] (see 6260652) + if (elementData.getClass() != Object[].class) + elementData = Arrays.copyOf(elementData, size, Object[].class); + } else { + // replace with empty array. + this.elementData = EMPTY_ELEMENTDATA; } - +} ``` -细心的同学一定会发现:**以无参数构造方法创建 `ArrayList` 时,实际上初始化赋值的是一个空数组。当真正对数组进行添加元素操作时,才真正分配容量。即向数组中添加第一个元素时,数组容量扩为 10。** 下面在我们分析 ArrayList 扩容时会讲到这一点内容! +细心的同学一定会发现:**以无参数构造方法创建 `ArrayList` 时,实际上初始化赋值的是一个空数组。当真正对数组进行添加元素操作时,才真正分配容量。即向数组中添加第一个元素时,数组容量扩为 10。** 下面在我们分析 `ArrayList` 扩容时会讲到这一点内容! -> 补充:JDK6 new 无参构造的 `ArrayList` 对象时,直接创建了长度是 10 的 `Object[]` 数组 elementData 。 +> 补充:JDK6 new 无参构造的 `ArrayList` 对象时,直接创建了长度是 10 的 `Object[]` 数组 `elementData` 。 ### 一步一步分析 ArrayList 扩容机制 -这里以无参构造函数创建的 ArrayList 为例分析 +这里以无参构造函数创建的 `ArrayList` 为例分析。 -#### 先来看 `add` 方法 +#### add 方法 ```java - /** - * 将指定的元素追加到此列表的末尾。 - */ - public boolean add(E e) { - //添加元素之前,先调用ensureCapacityInternal方法 - ensureCapacityInternal(size + 1); // Increments modCount!! - //这里看到ArrayList添加元素的实质就相当于为数组赋值 - elementData[size++] = e; - return true; - } +/** +* 将指定的元素追加到此列表的末尾。 +*/ +public boolean add(E e) { + // 加元素之前,先调用ensureCapacityInternal方法 + ensureCapacityInternal(size + 1); // Increments modCount!! + // 这里看到ArrayList添加元素的实质就相当于为数组赋值 + elementData[size++] = e; + return true; +} ``` -> **注意**:JDK11 移除了 `ensureCapacityInternal()` 和 `ensureExplicitCapacity()` 方法 - -#### 再来看看 `ensureCapacityInternal()` 方法 +**注意**:JDK11 移除了 `ensureCapacityInternal()` 和 `ensureExplicitCapacity()` 方法 -(JDK7)可以看到 `add` 方法 首先调用了`ensureCapacityInternal(size + 1)` +`ensureCapacityInternal` 方法的源码如下: ```java - //得到最小扩容量 - private void ensureCapacityInternal(int minCapacity) { - if (elementData == DEFAULTCAPACITY_EMPTY_ELEMENTDATA) { - // 获取默认的容量和传入参数的较大值 - minCapacity = Math.max(DEFAULT_CAPACITY, minCapacity); - } +// 根据给定的最小容量和当前数组元素来计算所需容量。 +private static int calculateCapacity(Object[] elementData, int minCapacity) { + // 如果当前数组元素为空数组(初始情况),返回默认容量和最小容量中的较大值作为所需容量 + if (elementData == DEFAULTCAPACITY_EMPTY_ELEMENTDATA) { + return Math.max(DEFAULT_CAPACITY, minCapacity); + } + // 否则直接返回最小容量 + return minCapacity; +} - ensureExplicitCapacity(minCapacity); - } +// 确保内部容量达到指定的最小容量。 +private void ensureCapacityInternal(int minCapacity) { + ensureExplicitCapacity(calculateCapacity(elementData, minCapacity)); +} ``` -**当 要 add 进第 1 个元素时,minCapacity 为 1,在 Math.max()方法比较后,minCapacity 为 10。** - -> 此处和后续 JDK8 代码格式化略有不同,核心代码基本一样。 - -#### `ensureExplicitCapacity()` 方法 - -如果调用 `ensureCapacityInternal()` 方法就一定会进入(执行)这个方法,下面我们来研究一下这个方法的源码! +`ensureCapacityInternal` 方法非常简单,内部直接调用了 `ensureExplicitCapacity` 方法: ```java - //判断是否需要扩容 - private void ensureExplicitCapacity(int minCapacity) { - modCount++; - - // overflow-conscious code - if (minCapacity - elementData.length > 0) - //调用grow方法进行扩容,调用此方法代表已经开始扩容了 - grow(minCapacity); - } - +//判断是否需要扩容 +private void ensureExplicitCapacity(int minCapacity) { + modCount++; + //判断当前数组容量是否足以存储minCapacity个元素 + if (minCapacity - elementData.length > 0) + //调用grow方法进行扩容 + grow(minCapacity); +} ``` 我们来仔细分析一下: -- 当我们要 add 进第 1 个元素到 ArrayList 时,elementData.length 为 0 (因为还是一个空的 list),因为执行了 `ensureCapacityInternal()` 方法 ,所以 minCapacity 此时为 10。此时,`minCapacity - elementData.length > 0`成立,所以会进入 `grow(minCapacity)` 方法。 -- 当 add 第 2 个元素时,minCapacity 为 2,此时 elementData.length(容量)在添加第一个元素后扩容成 10 了。此时,`minCapacity - elementData.length > 0` 不成立,所以不会进入 (执行)`grow(minCapacity)` 方法。 +- 当我们要 `add` 进第 1 个元素到 `ArrayList` 时,`elementData.length` 为 0 (因为还是一个空的 list),因为执行了 `ensureCapacityInternal()` 方法 ,所以 `minCapacity` 此时为 10。此时,`minCapacity - elementData.length > 0`成立,所以会进入 `grow(minCapacity)` 方法。 +- 当 `add` 第 2 个元素时,`minCapacity` 为 2,此时 `elementData.length`(容量)在添加第一个元素后扩容成 `10` 了。此时,`minCapacity - elementData.length > 0` 不成立,所以不会进入 (执行)`grow(minCapacity)` 方法。 - 添加第 3、4···到第 10 个元素时,依然不会执行 grow 方法,数组容量都为 10。 -直到添加第 11 个元素,minCapacity(为 11)比 elementData.length(为 10)要大。进入 grow 方法进行扩容。 +直到添加第 11 个元素,`minCapacity`(为 11)比 `elementData.length`(为 10)要大。进入 `grow` 方法进行扩容。 -#### `grow()` 方法 +#### grow 方法 ```java - /** - * 要分配的最大数组大小 - */ - private static final int MAX_ARRAY_SIZE = Integer.MAX_VALUE - 8; - - /** - * ArrayList扩容的核心方法。 - */ - private void grow(int minCapacity) { - // oldCapacity为旧容量,newCapacity为新容量 - int oldCapacity = elementData.length; - //将oldCapacity 右移一位,其效果相当于oldCapacity /2, - //我们知道位运算的速度远远快于整除运算,整句运算式的结果就是将新容量更新为旧容量的1.5倍, - int newCapacity = oldCapacity + (oldCapacity >> 1); - //然后检查新容量是否大于最小需要容量,若还是小于最小需要容量,那么就把最小需要容量当作数组的新容量, - if (newCapacity - minCapacity < 0) - newCapacity = minCapacity; - // 如果新容量大于 MAX_ARRAY_SIZE,进入(执行) `hugeCapacity()` 方法来比较 minCapacity 和 MAX_ARRAY_SIZE, - //如果minCapacity大于最大容量,则新容量则为`Integer.MAX_VALUE`,否则,新容量大小则为 MAX_ARRAY_SIZE 即为 `Integer.MAX_VALUE - 8`。 - if (newCapacity - MAX_ARRAY_SIZE > 0) - newCapacity = hugeCapacity(minCapacity); - // minCapacity is usually close to size, so this is a win: - elementData = Arrays.copyOf(elementData, newCapacity); - } +/** + * 要分配的最大数组大小 + */ +private static final int MAX_ARRAY_SIZE = Integer.MAX_VALUE - 8; + +/** + * ArrayList扩容的核心方法。 + */ +private void grow(int minCapacity) { + // oldCapacity为旧容量,newCapacity为新容量 + int oldCapacity = elementData.length; + // 将oldCapacity 右移一位,其效果相当于oldCapacity /2, + // 我们知道位运算的速度远远快于整除运算,整句运算式的结果就是将新容量更新为旧容量的1.5倍, + int newCapacity = oldCapacity + (oldCapacity >> 1); + + // 然后检查新容量是否大于最小需要容量,若还是小于最小需要容量,那么就把最小需要容量当作数组的新容量, + if (newCapacity - minCapacity < 0) + newCapacity = minCapacity; + + // 如果新容量大于 MAX_ARRAY_SIZE,进入(执行) `hugeCapacity()` 方法来比较 minCapacity 和 MAX_ARRAY_SIZE, + // 如果minCapacity大于最大容量,则新容量则为`Integer.MAX_VALUE`,否则,新容量大小则为 MAX_ARRAY_SIZE 即为 `Integer.MAX_VALUE - 8`。 + if (newCapacity - MAX_ARRAY_SIZE > 0) + newCapacity = hugeCapacity(minCapacity); + + // minCapacity is usually close to size, so this is a win: + elementData = Arrays.copyOf(elementData, newCapacity); +} ``` -**int newCapacity = oldCapacity + (oldCapacity >> 1),所以 ArrayList 每次扩容之后容量都会变为原来的 1.5 倍左右(oldCapacity 为偶数就是 1.5 倍,否则是 1.5 倍左右)!** 奇偶不同,比如:10+10/2 = 15, 33+33/2=49。如果是奇数的话会丢掉小数. +**`int newCapacity = oldCapacity + (oldCapacity >> 1)`,所以 ArrayList 每次扩容之后容量都会变为原来的 1.5 倍左右(oldCapacity 为偶数就是 1.5 倍,否则是 1.5 倍左右)!** 奇偶不同,比如:10+10/2 = 15, 33+33/2=49。如果是奇数的话会丢掉小数. > ">>"(移位运算符):>>1 右移一位相当于除 2,右移 n 位相当于除以 2 的 n 次方。这里 oldCapacity 明显右移了 1 位所以相当于 oldCapacity /2。对于大数据的 2 进制运算,位移运算符比那些普通运算符的运算要快很多,因为程序仅仅移动一下而已,不去计算,这样提高了效率,节省了资源 **我们再来通过例子探究一下`grow()` 方法:** -- 当 add 第 1 个元素时,oldCapacity 为 0,经比较后第一个 if 判断成立,newCapacity = minCapacity(为 10)。但是第二个 if 判断不会成立,即 newCapacity 不比 MAX_ARRAY_SIZE 大,则不会进入 `hugeCapacity` 方法。数组容量为 10,add 方法中 return true,size 增为 1。 -- 当 add 第 11 个元素进入 grow 方法时,newCapacity 为 15,比 minCapacity(为 11)大,第一个 if 判断不成立。新容量没有大于数组最大 size,不会进入 hugeCapacity 方法。数组容量扩为 15,add 方法中 return true,size 增为 11。 +- 当 `add` 第 1 个元素时,`oldCapacity` 为 0,经比较后第一个 if 判断成立,`newCapacity = minCapacity`(为 10)。但是第二个 if 判断不会成立,即 `newCapacity` 不比 `MAX_ARRAY_SIZE` 大,则不会进入 `hugeCapacity` 方法。数组容量为 10,`add` 方法中 return true,size 增为 1。 +- 当 `add` 第 11 个元素进入 `grow` 方法时,`newCapacity` 为 15,比 `minCapacity`(为 11)大,第一个 if 判断不成立。新容量没有大于数组最大 size,不会进入 `hugeCapacity` 方法。数组容量扩为 15,add 方法中 return true,size 增为 11。 - 以此类推······ **这里补充一点比较重要,但是容易被忽视掉的知识点:** -- java 中的 `length`属性是针对数组说的,比如说你声明了一个数组,想知道这个数组的长度则用到了 length 这个属性. -- java 中的 `length()` 方法是针对字符串说的,如果想看这个字符串的长度则用到 `length()` 这个方法. -- java 中的 `size()` 方法是针对泛型集合说的,如果想看这个泛型有多少个元素,就调用此方法来查看! +- Java 中的 `length`属性是针对数组说的,比如说你声明了一个数组,想知道这个数组的长度则用到了 length 这个属性. +- Java 中的 `length()` 方法是针对字符串说的,如果想看这个字符串的长度则用到 `length()` 这个方法. +- Java 中的 `size()` 方法是针对泛型集合说的,如果想看这个泛型有多少个元素,就调用此方法来查看! -#### `hugeCapacity()` 方法。 +#### hugeCapacity() 方法 -从上面 `grow()` 方法源码我们知道:如果新容量大于 MAX_ARRAY_SIZE,进入(执行) `hugeCapacity()` 方法来比较 minCapacity 和 MAX_ARRAY_SIZE,如果 minCapacity 大于最大容量,则新容量则为`Integer.MAX_VALUE`,否则,新容量大小则为 MAX_ARRAY_SIZE 即为 `Integer.MAX_VALUE - 8`。 +从上面 `grow()` 方法源码我们知道:如果新容量大于 `MAX_ARRAY_SIZE`,进入(执行) `hugeCapacity()` 方法来比较 `minCapacity` 和 `MAX_ARRAY_SIZE`,如果 `minCapacity` 大于最大容量,则新容量则为`Integer.MAX_VALUE`,否则,新容量大小则为 `MAX_ARRAY_SIZE` 即为 `Integer.MAX_VALUE - 8`。 ```java - private static int hugeCapacity(int minCapacity) { - if (minCapacity < 0) // overflow - throw new OutOfMemoryError(); - //对minCapacity和MAX_ARRAY_SIZE进行比较 - //若minCapacity大,将Integer.MAX_VALUE作为新数组的大小 - //若MAX_ARRAY_SIZE大,将MAX_ARRAY_SIZE作为新数组的大小 - //MAX_ARRAY_SIZE = Integer.MAX_VALUE - 8; - return (minCapacity > MAX_ARRAY_SIZE) ? - Integer.MAX_VALUE : - MAX_ARRAY_SIZE; - } +private static int hugeCapacity(int minCapacity) { + if (minCapacity < 0) // overflow + throw new OutOfMemoryError(); + // 对minCapacity和MAX_ARRAY_SIZE进行比较 + // 若minCapacity大,将Integer.MAX_VALUE作为新数组的大小 + // 若MAX_ARRAY_SIZE大,将MAX_ARRAY_SIZE作为新数组的大小 + // MAX_ARRAY_SIZE = Integer.MAX_VALUE - 8; + return (minCapacity > MAX_ARRAY_SIZE) ? + Integer.MAX_VALUE : + MAX_ARRAY_SIZE; +} ``` ### `System.arraycopy()` 和 `Arrays.copyOf()`方法 -阅读源码的话,我们就会发现 ArrayList 中大量调用了这两个方法。比如:我们上面讲的扩容操作以及`add(int index, E element)`、`toArray()` 等方法中都用到了该方法! +阅读源码的话,我们就会发现 `ArrayList` 中大量调用了这两个方法。比如:我们上面讲的扩容操作以及`add(int index, E element)`、`toArray()` 等方法中都用到了该方法! #### `System.arraycopy()` 方法 @@ -783,26 +809,26 @@ public class ArrayList extends AbstractList ```java public class ArraycopyTest { - public static void main(String[] args) { - // TODO Auto-generated method stub - int[] a = new int[10]; - a[0] = 0; - a[1] = 1; - a[2] = 2; - a[3] = 3; - System.arraycopy(a, 2, a, 3, 3); - a[2]=99; - for (int i = 0; i < a.length; i++) { - System.out.print(a[i] + " "); - } - } + public static void main(String[] args) { + // TODO Auto-generated method stub + int[] a = new int[10]; + a[0] = 0; + a[1] = 1; + a[2] = 2; + a[3] = 3; + System.arraycopy(a, 2, a, 3, 3); + a[2]=99; + for (int i = 0; i < a.length; i++) { + System.out.print(a[i] + " "); + } + } } ``` 结果: -``` +```plain 0 1 99 2 3 0 0 0 0 0 ``` @@ -812,9 +838,9 @@ public class ArraycopyTest { ```java public static int[] copyOf(int[] original, int newLength) { - // 申请一个新的数组 + // 申请一个新的数组 int[] copy = new int[newLength]; - // 调用System.arraycopy,将源数组中的数据进行拷贝,并返回新的数组 + // 调用System.arraycopy,将源数组中的数据进行拷贝,并返回新的数组 System.arraycopy(original, 0, copy, 0, Math.min(original.length, newLength)); return copy; @@ -838,20 +864,20 @@ public class ArraycopyTest { ```java public class ArrayscopyOfTest { - public static void main(String[] args) { - int[] a = new int[3]; - a[0] = 0; - a[1] = 1; - a[2] = 2; - int[] b = Arrays.copyOf(a, 10); - System.out.println("b.length"+b.length); - } + public static void main(String[] args) { + int[] a = new int[3]; + a[0] = 0; + a[1] = 1; + a[2] = 2; + int[] b = Arrays.copyOf(a, 10); + System.out.println("b.length"+b.length); + } } ``` 结果: -``` +```plain 10 ``` @@ -896,23 +922,23 @@ public class ArrayscopyOfTest { ```java public class EnsureCapacityTest { - public static void main(String[] args) { - ArrayList list = new ArrayList(); - final int N = 10000000; - long startTime = System.currentTimeMillis(); - for (int i = 0; i < N; i++) { - list.add(i); - } - long endTime = System.currentTimeMillis(); - System.out.println("使用ensureCapacity方法前:"+(endTime - startTime)); - - } + public static void main(String[] args) { + ArrayList list = new ArrayList(); + final int N = 10000000; + long startTime = System.currentTimeMillis(); + for (int i = 0; i < N; i++) { + list.add(i); + } + long endTime = System.currentTimeMillis(); + System.out.println("使用ensureCapacity方法前:"+(endTime - startTime)); + + } } ``` 运行结果: -``` +```plain 使用ensureCapacity方法前:2158 ``` @@ -934,8 +960,10 @@ public class EnsureCapacityTest { 运行结果: -``` +```plain 使用ensureCapacity方法后:1773 ``` 通过运行结果,我们可以看出向 `ArrayList` 添加大量元素之前使用`ensureCapacity` 方法可以提升性能。不过,这个性能差距几乎可以忽略不计。而且,实际项目根本也不可能往 `ArrayList` 里面添加这么多元素。 + + diff --git a/docs/java/collection/concurrent-hash-map-source-code.md b/docs/java/collection/concurrent-hash-map-source-code.md index ca72fe0c66f..d0d210aacdf 100644 --- a/docs/java/collection/concurrent-hash-map-source-code.md +++ b/docs/java/collection/concurrent-hash-map-source-code.md @@ -5,9 +5,9 @@ tag: - Java集合 --- -> 本文来自公众号:末读代码的投稿,原文地址:https://mp.weixin.qq.com/s/AHWzboztt53ZfFZmsSnMSw 。 +> 本文来自公众号:末读代码的投稿,原文地址: 。 -上一篇文章介绍了 HashMap 源码,反响不错,也有很多同学发表了自己的观点,这次又来了,这次是 `ConcurrentHashMap ` 了,作为线程安全的 HashMap ,它的使用频率也是很高。那么它的存储结构和实现原理是怎么样的呢? +上一篇文章介绍了 HashMap 源码,反响不错,也有很多同学发表了自己的观点,这次又来了,这次是 `ConcurrentHashMap` 了,作为线程安全的 HashMap ,它的使用频率也是很高。那么它的存储结构和实现原理是怎么样的呢? ## 1. ConcurrentHashMap 1.7 @@ -328,7 +328,7 @@ private void rehash(HashEntry node) { HashEntry e = oldTable[i]; if (e != null) { HashEntry next = e.next; - // 计算新的位置,新的位置只可能是不便或者是老的位置+老的容量。 + // 计算新的位置,新的位置只可能是不变或者是老的位置+老的容量。 int idx = e.hash & sizeMask; if (next == null) // Single node on list // 如果当前位置还不是链表,只是一个元素,直接赋值 @@ -337,7 +337,7 @@ private void rehash(HashEntry node) { // 如果是链表了 HashEntry lastRun = e; int lastIdx = idx; - // 新的位置只可能是不便或者是老的位置+老的容量。 + // 新的位置只可能是不变或者是老的位置+老的容量。 // 遍历结束后,lastRun 后面的元素位置都是相同的 for (HashEntry last = next; last != null; last = last.next) { int k = last.hash & sizeMask; @@ -368,7 +368,19 @@ private void rehash(HashEntry node) { } ``` -有些同学可能会对最后的两个 for 循环有疑惑,这里第一个 for 是为了寻找这样一个节点,这个节点后面的所有 next 节点的新位置都是相同的。然后把这个作为一个链表赋值到新位置。第二个 for 循环是为了把剩余的元素通过头插法插入到指定位置链表。这样实现的原因可能是基于概率统计,有深入研究的同学可以发表下意见。 +有些同学可能会对最后的两个 for 循环有疑惑,这里第一个 for 是为了寻找这样一个节点,这个节点后面的所有 next 节点的新位置都是相同的。然后把这个作为一个链表赋值到新位置。第二个 for 循环是为了把剩余的元素通过头插法插入到指定位置链表。~~这样实现的原因可能是基于概率统计,有深入研究的同学可以发表下意见。~~ + +内部第二个 `for` 循环中使用了 `new HashEntry(h, p.key, v, n)` 创建了一个新的 `HashEntry`,而不是复用之前的,是因为如果复用之前的,那么会导致正在遍历(如正在执行 `get` 方法)的线程由于指针的修改无法遍历下去。正如注释中所说的: + +> 当它们不再被可能正在并发遍历表的任何读取线程引用时,被替换的节点将被垃圾回收。 +> +> The nodes they replace will be garbage collectable as soon as they are no longer referenced by any reader thread that may be in the midst of concurrently traversing table + +为什么需要再使用一个 `for` 循环找到 `lastRun` ,其实是为了减少对象创建的次数,正如注解中所说的: + +> 从统计上看,在默认的阈值下,当表容量加倍时,只有大约六分之一的节点需要被克隆。 +> +> Statistically, at the default threshold, only about one-sixth of them need cloning when a table doubles. ### 5. get @@ -439,10 +451,10 @@ private final Node[] initTable() { } ``` -从源码中可以发现 `ConcurrentHashMap` 的初始化是通过**自旋和 CAS** 操作完成的。里面需要注意的是变量 `sizeCtl` ,它的值决定着当前的初始化状态。 +从源码中可以发现 `ConcurrentHashMap` 的初始化是通过**自旋和 CAS** 操作完成的。里面需要注意的是变量 `sizeCtl` (sizeControl 的缩写),它的值决定着当前的初始化状态。 -1. -1 说明正在初始化 -2. -N 说明有 N-1 个线程正在进行扩容 +1. -1 说明正在初始化,其他线程需要自旋等待 +2. -N 说明 table 正在进行扩容,高 16 位表示扩容的标识戳,低 16 位减 1 为正在进行扩容的线程数 3. 0 表示 table 初始化大小,如果 table 没有初始化 4. \>0 表示 table 扩容的阈值,如果 table 已经初始化。 @@ -589,3 +601,5 @@ Java7 中 `ConcurrentHashMap` 使用的分段锁,也就是每一个 Segment Java8 中的 `ConcurrentHashMap` 使用的 `Synchronized` 锁加 CAS 的机制。结构也由 Java7 中的 **`Segment` 数组 + `HashEntry` 数组 + 链表** 进化成了 **Node 数组 + 链表 / 红黑树**,Node 是类似于一个 HashEntry 的结构。它的冲突再达到一定大小时会转化成红黑树,在冲突小于一定数量时又退回链表。 有些同学可能对 `Synchronized` 的性能存在疑问,其实 `Synchronized` 锁自从引入锁升级策略后,性能不再是问题,有兴趣的同学可以自己了解下 `Synchronized` 的**锁升级**。 + + diff --git a/docs/java/collection/copyonwritearraylist-source-code.md b/docs/java/collection/copyonwritearraylist-source-code.md new file mode 100644 index 00000000000..9aceb83bc4e --- /dev/null +++ b/docs/java/collection/copyonwritearraylist-source-code.md @@ -0,0 +1,317 @@ +--- +title: CopyOnWriteArrayList 源码分析 +category: Java +tag: + - Java集合 +--- + +## CopyOnWriteArrayList 简介 + +在 JDK1.5 之前,如果想要使用并发安全的 `List` 只能选择 `Vector`。而 `Vector` 是一种老旧的集合,已经被淘汰。`Vector` 对于增删改查等方法基本都加了 `synchronized`,这种方式虽然能够保证同步,但这相当于对整个 `Vector` 加上了一把大锁,使得每个方法执行的时候都要去获得锁,导致性能非常低下。 + +JDK1.5 引入了 `Java.util.concurrent`(JUC)包,其中提供了很多线程安全且并发性能良好的容器,其中唯一的线程安全 `List` 实现就是 `CopyOnWriteArrayList` 。关于`java.util.concurrent` 包下常见并发容器的总结,可以看我写的这篇文章:[Java 常见并发容器总结](https://javaguide.cn/java/concurrent/java-concurrent-collections.html) 。 + +### CopyOnWriteArrayList 到底有什么厉害之处? + +对于大部分业务场景来说,读取操作往往是远大于写入操作的。由于读取操作不会对原有数据进行修改,因此,对于每次读取都进行加锁其实是一种资源浪费。相比之下,我们应该允许多个线程同时访问 `List` 的内部数据,毕竟对于读取操作来说是安全的。 + +这种思路与 `ReentrantReadWriteLock` 读写锁的设计思想非常类似,即读读不互斥、读写互斥、写写互斥(只有读读不互斥)。`CopyOnWriteArrayList` 更进一步地实现了这一思想。为了将读操作性能发挥到极致,`CopyOnWriteArrayList` 中的读取操作是完全无需加锁的。更加厉害的是,写入操作也不会阻塞读取操作,只有写写才会互斥。这样一来,读操作的性能就可以大幅度提升。 + +`CopyOnWriteArrayList` 线程安全的核心在于其采用了 **写时复制(Copy-On-Write)** 的策略,从 `CopyOnWriteArrayList` 的名字就能看出了。 + +### Copy-On-Write 的思想是什么? + +`CopyOnWriteArrayList`名字中的“Copy-On-Write”即写时复制,简称 COW。 + +下面是维基百科对 Copy-On-Write 的介绍,介绍的挺不错: + +> 写入时复制(英语:Copy-on-write,简称 COW)是一种计算机程序设计领域的优化策略。其核心思想是,如果有多个调用者(callers)同时请求相同资源(如内存或磁盘上的数据存储),他们会共同获取相同的指针指向相同的资源,直到某个调用者试图修改资源的内容时,系统才会真正复制一份专用副本(private copy)给该调用者,而其他调用者所见到的最初的资源仍然保持不变。这过程对其他的调用者都是透明的。此作法主要的优点是如果调用者没有修改该资源,就不会有副本(private copy)被创建,因此多个调用者只是读取操作时可以共享同一份资源。 + +这里再以 `CopyOnWriteArrayList`为例介绍:当需要修改( `add`,`set`、`remove` 等操作) `CopyOnWriteArrayList` 的内容时,不会直接修改原数组,而是会先创建底层数组的副本,对副本数组进行修改,修改完之后再将修改后的数组赋值回去,这样就可以保证写操作不会影响读操作了。 + +可以看出,写时复制机制非常适合读多写少的并发场景,能够极大地提高系统的并发性能。 + +不过,写时复制机制并不是银弹,其依然存在一些缺点,下面列举几点: + +1. 内存占用:每次写操作都需要复制一份原始数据,会占用额外的内存空间,在数据量比较大的情况下,可能会导致内存资源不足。 +2. 写操作开销:每一次写操作都需要复制一份原始数据,然后再进行修改和替换,所以写操作的开销相对较大,在写入比较频繁的场景下,性能可能会受到影响。 +3. 数据一致性问题:修改操作不会立即反映到最终结果中,还需要等待复制完成,这可能会导致一定的数据一致性问题。 +4. …… + +## CopyOnWriteArrayList 源码分析 + +这里以 JDK1.8 为例,分析一下 `CopyOnWriteArrayList` 的底层核心源码。 + +`CopyOnWriteArrayList` 的类定义如下: + +```java +public class CopyOnWriteArrayList +extends Object +implements List, RandomAccess, Cloneable, Serializable +{ + //... +} +``` + +`CopyOnWriteArrayList` 实现了以下接口: + +- `List` : 表明它是一个列表,支持添加、删除、查找等操作,并且可以通过下标进行访问。 +- `RandomAccess` :这是一个标志接口,表明实现这个接口的 `List` 集合是支持 **快速随机访问** 的。 +- `Cloneable` :表明它具有拷贝能力,可以进行深拷贝或浅拷贝操作。 +- `Serializable` : 表明它可以进行序列化操作,也就是可以将对象转换为字节流进行持久化存储或网络传输,非常方便。 + +![CopyOnWriteArrayList 类图](https://oss.javaguide.cn/github/javaguide/java/collection/copyonwritearraylist-class-diagram.png) + +### 初始化 + +`CopyOnWriteArrayList` 中有一个无参构造函数和两个有参构造函数。 + +```java +// 创建一个空的 CopyOnWriteArrayList +public CopyOnWriteArrayList() { + setArray(new Object[0]); +} + +// 按照集合的迭代器返回的顺序创建一个包含指定集合元素的 CopyOnWriteArrayList +public CopyOnWriteArrayList(Collection c) { + Object[] elements; + if (c.getClass() == CopyOnWriteArrayList.class) + elements = ((CopyOnWriteArrayList)c).getArray(); + else { + elements = c.toArray(); + // c.toArray might (incorrectly) not return Object[] (see 6260652) + if (elements.getClass() != Object[].class) + elements = Arrays.copyOf(elements, elements.length, Object[].class); + } + setArray(elements); +} + +// 创建一个包含指定数组的副本的列表 +public CopyOnWriteArrayList(E[] toCopyIn) { + setArray(Arrays.copyOf(toCopyIn, toCopyIn.length, Object[].class)); +} +``` + +### 插入元素 + +`CopyOnWriteArrayList` 的 `add()`方法有三个版本: + +- `add(E e)`:在 `CopyOnWriteArrayList` 的尾部插入元素。 +- `add(int index, E element)`:在 `CopyOnWriteArrayList` 的指定位置插入元素。 +- `addIfAbsent(E e)`:如果指定元素不存在,那么添加该元素。如果成功添加元素则返回 true。 + +这里以`add(E e)`为例进行介绍: + +```java +// 插入元素到 CopyOnWriteArrayList 的尾部 +public boolean add(E e) { + final ReentrantLock lock = this.lock; + // 加锁 + lock.lock(); + try { + // 获取原来的数组 + Object[] elements = getArray(); + // 原来数组的长度 + int len = elements.length; + // 创建一个长度+1的新数组,并将原来数组的元素复制给新数组 + Object[] newElements = Arrays.copyOf(elements, len + 1); + // 元素放在新数组末尾 + newElements[len] = e; + // array指向新数组 + setArray(newElements); + return true; + } finally { + // 解锁 + lock.unlock(); + } +} +``` + +从上面的源码可以看出: + +- `add`方法内部用到了 `ReentrantLock` 加锁,保证了同步,避免了多线程写的时候会复制出多个副本出来。锁被修饰保证了锁的内存地址肯定不会被修改,并且,释放锁的逻辑放在 `finally` 中,可以保证锁能被释放。 +- `CopyOnWriteArrayList` 通过复制底层数组的方式实现写操作,即先创建一个新的数组来容纳新添加的元素,然后在新数组中进行写操作,最后将新数组赋值给底层数组的引用,替换掉旧的数组。这也就证明了我们前面说的:`CopyOnWriteArrayList` 线程安全的核心在于其采用了 **写时复制(Copy-On-Write)** 的策略。 +- 每次写操作都需要通过 `Arrays.copyOf` 复制底层数组,时间复杂度是 O(n) 的,且会占用额外的内存空间。因此,`CopyOnWriteArrayList` 适用于读多写少的场景,在写操作不频繁且内存资源充足的情况下,可以提升系统的性能表现。 +- `CopyOnWriteArrayList` 中并没有类似于 `ArrayList` 的 `grow()` 方法扩容的操作。 + +> `Arrays.copyOf` 方法的时间复杂度是 O(n),其中 n 表示需要复制的数组长度。因为这个方法的实现原理是先创建一个新的数组,然后将源数组中的数据复制到新数组中,最后返回新数组。这个方法会复制整个数组,因此其时间复杂度与数组长度成正比,即 O(n)。值得注意的是,由于底层调用了系统级别的拷贝指令,因此在实际应用中这个方法的性能表现比较优秀,但是也需要注意控制复制的数据量,避免出现内存占用过高的情况。 + +### 读取元素 + +`CopyOnWriteArrayList` 的读取操作是基于内部数组 `array` 并没有发生实际的修改,因此在读取操作时不需要进行同步控制和锁操作,可以保证数据的安全性。这种机制下,多个线程可以同时读取列表中的元素。 + +```java +// 底层数组,只能通过getArray和setArray方法访问 +private transient volatile Object[] array; + +public E get(int index) { + return get(getArray(), index); +} + +final Object[] getArray() { + return array; +} + +private E get(Object[] a, int index) { + return (E) a[index]; +} +``` + +不过,`get`方法是弱一致性的,在某些情况下可能读到旧的元素值。 + +`get(int index)`方法是分两步进行的: + +1. 通过`getArray()`获取当前数组的引用; +2. 直接从数组中获取下标为 index 的元素。 + +这个过程并没有加锁,所以在并发环境下可能出现如下情况: + +1. 线程 1 调用`get(int index)`方法获取值,内部通过`getArray()`方法获取到了 array 属性值; +2. 线程 2 调用`CopyOnWriteArrayList`的`add`、`set`、`remove` 等修改方法时,内部通过`setArray`方法修改了`array`属性的值; +3. 线程 1 还是从旧的 `array` 数组中取值。 + +### 获取列表中元素的个数 + +```java +public int size() { + return getArray().length; +} +``` + +`CopyOnWriteArrayList`中的`array`数组每次复制都刚好能够容纳下所有元素,并不像`ArrayList`那样会预留一定的空间。因此,`CopyOnWriteArrayList`中并没有`size`属性`CopyOnWriteArrayList`的底层数组的长度就是元素个数,因此`size()`方法只要返回数组长度就可以了。 + +### 删除元素 + +`CopyOnWriteArrayList`删除元素相关的方法一共有 4 个: + +1. `remove(int index)`:移除此列表中指定位置上的元素。将任何后续元素向左移动(从它们的索引中减去 1)。 +2. `boolean remove(Object o)`:删除此列表中首次出现的指定元素,如果不存在该元素则返回 false。 +3. `boolean removeAll(Collection c)`:从此列表中删除指定集合中包含的所有元素。 +4. `void clear()`:移除此列表中的所有元素。 + +这里以`remove(int index)`为例进行介绍: + +```java +public E remove(int index) { + // 获取可重入锁 + final ReentrantLock lock = this.lock; + // 加锁 + lock.lock(); + try { + //获取当前array数组 + Object[] elements = getArray(); + // 获取当前array长度 + int len = elements.length; + //获取指定索引的元素(旧值) + E oldValue = get(elements, index); + int numMoved = len - index - 1; + // 判断删除的是否是最后一个元素 + if (numMoved == 0) + // 如果删除的是最后一个元素,直接复制该元素前的所有元素到新的数组 + setArray(Arrays.copyOf(elements, len - 1)); + else { + // 分段复制,将index前的元素和index+1后的元素复制到新数组 + // 新数组长度为旧数组长度-1 + Object[] newElements = new Object[len - 1]; + System.arraycopy(elements, 0, newElements, 0, index); + System.arraycopy(elements, index + 1, newElements, index, + numMoved); + //将新数组赋值给array引用 + setArray(newElements); + } + return oldValue; + } finally { + // 解锁 + lock.unlock(); + } +} +``` + +### 判断元素是否存在 + +`CopyOnWriteArrayList`提供了两个用于判断指定元素是否在列表中的方法: + +- `contains(Object o)`:判断是否包含指定元素。 +- `containsAll(Collection c)`:判断是否保证指定集合的全部元素。 + +```java +// 判断是否包含指定元素 +public boolean contains(Object o) { + //获取当前array数组 + Object[] elements = getArray(); + //调用index尝试查找指定元素,如果返回值大于等于0,则返回true,否则返回false + return indexOf(o, elements, 0, elements.length) >= 0; +} + +// 判断是否保证指定集合的全部元素 +public boolean containsAll(Collection c) { + //获取当前array数组 + Object[] elements = getArray(); + //获取数组长度 + int len = elements.length; + //遍历指定集合 + for (Object e : c) { + //循环调用indexOf方法判断,只要有一个没有包含就直接返回false + if (indexOf(e, elements, 0, len) < 0) + return false; + } + //最后表示全部包含或者制定集合为空集合,那么返回true + return true; +} +``` + +## CopyOnWriteArrayList 常用方法测试 + +代码: + +```java +// 创建一个 CopyOnWriteArrayList 对象 +CopyOnWriteArrayList list = new CopyOnWriteArrayList<>(); + +// 向列表中添加元素 +list.add("Java"); +list.add("Python"); +list.add("C++"); +System.out.println("初始列表:" + list); + +// 使用 get 方法获取指定位置的元素 +System.out.println("列表第二个元素为:" + list.get(1)); + +// 使用 remove 方法删除指定元素 +boolean result = list.remove("C++"); +System.out.println("删除结果:" + result); +System.out.println("列表删除元素后为:" + list); + +// 使用 set 方法更新指定位置的元素 +list.set(1, "Golang"); +System.out.println("列表更新后为:" + list); + +// 使用 add 方法在指定位置插入元素 +list.add(0, "PHP"); +System.out.println("列表插入元素后为:" + list); + +// 使用 size 方法获取列表大小 +System.out.println("列表大小为:" + list.size()); + +// 使用 removeAll 方法删除指定集合中所有出现的元素 +result = list.removeAll(List.of("Java", "Golang")); +System.out.println("批量删除结果:" + result); +System.out.println("列表批量删除元素后为:" + list); + +// 使用 clear 方法清空列表中所有元素 +list.clear(); +System.out.println("列表清空后为:" + list); +``` + +输出: + +```plain +列表更新后为:[Java, Golang] +列表插入元素后为:[PHP, Java, Golang] +列表大小为:3 +批量删除结果:true +列表批量删除元素后为:[PHP] +列表清空后为:[] +``` + + diff --git a/docs/java/collection/delayqueue-source-code.md b/docs/java/collection/delayqueue-source-code.md new file mode 100644 index 00000000000..5fb6f4affad --- /dev/null +++ b/docs/java/collection/delayqueue-source-code.md @@ -0,0 +1,359 @@ +--- +title: DelayQueue 源码分析 +category: Java +tag: + - Java集合 +--- + +## DelayQueue 简介 + +`DelayQueue` 是 JUC 包(`java.util.concurrent)`为我们提供的延迟队列,用于实现延时任务比如订单下单 15 分钟未支付直接取消。它是 `BlockingQueue` 的一种,底层是一个基于 `PriorityQueue` 实现的一个无界队列,是线程安全的。关于`PriorityQueue`可以参考笔者编写的这篇文章:[PriorityQueue 源码分析](./priorityqueue-source-code.md) 。 + +![BlockingQueue 的实现类](https://oss.javaguide.cn/github/javaguide/java/collection/blocking-queue-hierarchy.png) + +`DelayQueue` 中存放的元素必须实现 `Delayed` 接口,并且需要重写 `getDelay()`方法(计算是否到期)。 + +```java +public interface Delayed extends Comparable { + long getDelay(TimeUnit unit); +} +``` + +默认情况下, `DelayQueue` 会按照到期时间升序编排任务。只有当元素过期时(`getDelay()`方法返回值小于等于 0),才能从队列中取出。 + +## DelayQueue 发展史 + +- `DelayQueue` 最早是在 Java 5 中引入的,作为 `java.util.concurrent` 包中的一部分,用于支持基于时间的任务调度和缓存过期删除等场景,该版本仅仅支持延迟功能的实现,还未解决线程安全问题。 +- 在 Java 6 中,`DelayQueue` 的实现进行了优化,通过使用 `ReentrantLock` 和 `Condition` 解决线程安全及线程间交互的效率,提高了其性能和可靠性。 +- 在 Java 7 中,`DelayQueue` 的实现进行了进一步的优化,通过使用 CAS 操作实现元素的添加和移除操作,提高了其并发操作性能。 +- 在 Java 8 中,`DelayQueue` 的实现没有进行重大变化,但是在 `java.time` 包中引入了新的时间类,如 `Duration` 和 `Instant`,使得使用 `DelayQueue` 进行基于时间的调度更加方便和灵活。 +- 在 Java 9 中,`DelayQueue` 的实现进行了一些微小的改进,主要是对代码进行了一些优化和精简。 + +总的来说,`DelayQueue` 的发展史主要是通过优化其实现方式和提高其性能和可靠性,使其更加适用于基于时间的调度和缓存过期删除等场景。 + +## DelayQueue 常见使用场景示例 + +我们这里希望任务可以按照我们预期的时间执行,例如提交 3 个任务,分别要求 1s、2s、3s 后执行,即使是乱序添加,1s 后要求 1s 执行的任务会准时执行。 + +![延迟任务](https://oss.javaguide.cn/github/javaguide/java/collection/delayed-task.png) + +对此我们可以使用 `DelayQueue` 来实现,所以我们首先需要继承 `Delayed` 实现 `DelayedTask`,实现 `getDelay` 方法以及优先级比较 `compareTo`。 + +```java +/** + * 延迟任务 + */ +public class DelayedTask implements Delayed { + /** + * 任务到期时间 + */ + private long executeTime; + /** + * 任务 + */ + private Runnable task; + + public DelayedTask(long delay, Runnable task) { + this.executeTime = System.currentTimeMillis() + delay; + this.task = task; + } + + /** + * 查看当前任务还有多久到期 + * @param unit + * @return + */ + @Override + public long getDelay(TimeUnit unit) { + return unit.convert(executeTime - System.currentTimeMillis(), TimeUnit.MILLISECONDS); + } + + /** + * 延迟队列需要到期时间升序入队,所以我们需要实现compareTo进行到期时间比较 + * @param o + * @return + */ + @Override + public int compareTo(Delayed o) { + return Long.compare(this.executeTime, ((DelayedTask) o).executeTime); + } + + public void execute() { + task.run(); + } +} +``` + +完成任务的封装之后,使用就很简单了,设置好多久到期然后将任务提交到延迟队列中即可。 + +```java +// 创建延迟队列,并添加任务 +DelayQueue < DelayedTask > delayQueue = new DelayQueue < > (); + +//分别添加1s、2s、3s到期的任务 +delayQueue.add(new DelayedTask(2000, () -> System.out.println("Task 2"))); +delayQueue.add(new DelayedTask(1000, () -> System.out.println("Task 1"))); +delayQueue.add(new DelayedTask(3000, () -> System.out.println("Task 3"))); + +// 取出任务并执行 +while (!delayQueue.isEmpty()) { + //阻塞获取最先到期的任务 + DelayedTask task = delayQueue.take(); + if (task != null) { + task.execute(); + } +} +``` + +从输出结果可以看出,即使笔者先提到 2s 到期的任务,1s 到期的任务 Task1 还是优先执行的。 + +```java +Task 1 +Task 2 +Task 3 +``` + +## DelayQueue 源码解析 + +这里以 JDK1.8 为例,分析一下 `DelayQueue` 的底层核心源码。 + +`DelayQueue` 的类定义如下: + +```java +public class DelayQueue extends AbstractQueue implements BlockingQueue +{ + //... +} +``` + +`DelayQueue` 继承了 `AbstractQueue` 类,实现了 `BlockingQueue` 接口。 + +![DelayQueue类图](https://oss.javaguide.cn/github/javaguide/java/collection/delayqueue-class-diagram.png) + +### 核心成员变量 + +`DelayQueue` 的 4 个核心成员变量如下: + +```java +//可重入锁,实现线程安全的关键 +private final transient ReentrantLock lock = new ReentrantLock(); +//延迟队列底层存储数据的集合,确保元素按照到期时间升序排列 +private final PriorityQueue q = new PriorityQueue(); + +//指向准备执行优先级最高的线程 +private Thread leader = null; +//实现多线程之间等待唤醒的交互 +private final Condition available = lock.newCondition(); +``` + +- `lock` : 我们都知道 `DelayQueue` 存取是线程安全的,所以为了保证存取元素时线程安全,我们就需要在存取时上锁,而 `DelayQueue` 就是基于 `ReentrantLock` 独占锁确保存取操作的线程安全。 +- `q` : 延迟队列要求元素按照到期时间进行升序排列,所以元素添加时势必需要进行优先级排序,所以 `DelayQueue` 底层元素的存取都是通过这个优先队列 `PriorityQueue` 的成员变量 `q` 来管理的。 +- `leader` : 延迟队列的任务只有到期之后才会执行,对于没有到期的任务只有等待,为了确保优先级最高的任务到期后可以即刻被执行,设计者就用 `leader` 来管理延迟任务,只有 `leader` 所指向的线程才具备定时等待任务到期执行的权限,而其他那些优先级低的任务只能无限期等待,直到 `leader` 线程执行完手头的延迟任务后唤醒它。 +- `available` : 上文讲述 `leader` 线程时提到的等待唤醒操作的交互就是通过 `available` 实现的,假如线程 1 尝试在空的 `DelayQueue` 获取任务时,`available` 就会将其放入等待队列中。直到有一个线程添加一个延迟任务后通过 `available` 的 `signal` 方法将其唤醒。 + +### 构造方法 + +相较于其他的并发容器,延迟队列的构造方法比较简单,它只有两个构造方法,因为所有成员变量在类加载时都已经初始完成了,所以默认构造方法什么也没做。还有一个传入 `Collection` 对象的构造方法,它会将调用 `addAll()`方法将集合元素存到优先队列 `q` 中。 + +```java +public DelayQueue() {} + +public DelayQueue(Collection c) { + this.addAll(c); +} +``` + +### 添加元素 + +`DelayQueue` 添加元素的方法无论是 `add`、`put` 还是 `offer`,本质上就是调用一下 `offer` ,所以了解延迟队列的添加逻辑我们只需阅读 offer 方法即可。 + +`offer` 方法的整体逻辑为: + +1. 尝试获取 `lock` 。 +2. 如果上锁成功,则调 `q` 的 `offer` 方法将元素存放到优先队列中。 +3. 调用 `peek` 方法看看当前队首元素是否就是本次入队的元素,如果是则说明当前这个元素是即将到期的任务(即优先级最高的元素),于是将 `leader` 设置为空,通知因为队列为空时调用 `take` 等方法导致阻塞的线程来争抢元素。 +4. 上述步骤执行完成,释放 `lock`。 +5. 返回 true。 + +源码如下,笔者已详细注释,读者可自行参阅: + +```java +public boolean offer(E e) { + //尝试获取lock + final ReentrantLock lock = this.lock; + lock.lock(); + try { + //如果上锁成功,则调q的offer方法将元素存放到优先队列中 + q.offer(e); + //调用peek方法看看当前队首元素是否就是本次入队的元素,如果是则说明当前这个元素是即将到期的任务(即优先级最高的元素) + if (q.peek() == e) { + //将leader设置为空,通知调用取元素方法而阻塞的线程来争抢这个任务 + leader = null; + available.signal(); + } + return true; + } finally { + //上述步骤执行完成,释放lock + lock.unlock(); + } +} +``` + +### 获取元素 + +`DelayQueue` 中获取元素的方式分为阻塞式和非阻塞式,先来看看逻辑比较复杂的阻塞式获取元素方法 `take`,为了让读者可以更直观的了解阻塞式获取元素的全流程,笔者将以 3 个线程并发获取元素为例讲述 `take` 的工作流程。 + +> 想要理解下面的内容,需要用到 AQS 相关的知识,推荐阅读下面这两篇文章: +> +> - [图文讲解 AQS ,一起看看 AQS 的源码……(图文较长)](https://xie.infoq.cn/article/5a3cc0b709012d40cb9f41986) +> - [AQS 都看完了,Condition 原理可不能少!](https://xie.infoq.cn/article/0223d5e5f19726b36b084b10d) + +1、首先, 3 个线程会尝试获取可重入锁 `lock`,假设我们现在有 3 个线程分别是 t1、t2、t3,随后 t1 得到了锁,而 t2、t3 没有抢到锁,故将这两个线程存入等待队列中。 + +![](https://oss.javaguide.cn/github/javaguide/java/collection/delayqueue-take-0.png) + +2、紧接着 t1 开始进行元素获取的逻辑。 + +3、线程 t1 首先会查看 `DelayQueue` 队列首元素是否为空。 + +4、如果元素为空,则说明当前队列没有任何元素,故 t1 就会被阻塞存到 `conditionWaiter` 这个队列中。 + +![](https://oss.javaguide.cn/github/javaguide/java/collection/delayqueue-take-1.png) + +注意,调用 `await` 之后 t1 就会释放 `lcok` 锁,假如 `DelayQueue` 持续为空,那么 t2、t3 也会像 t1 一样执行相同的逻辑并进入 `conditionWaiter` 队列中。 + +![](https://oss.javaguide.cn/github/javaguide/java/collection/delayqueue-take-2.png) + +如果元素不为空,则判断当前任务是否到期,如果元素到期,则直接返回出去。如果元素未到期,则判断当前 `leader` 线程(`DelayQueue` 中唯一一个可以等待并获取元素的线程引用)是否为空,若不为空,则说明当前 `leader` 正在等待执行一个优先级比当前元素还高的元素到期,故当前线程 t1 只能调用 `await` 进入无限期等待,等到 `leader` 取得元素后唤醒。反之,若 `leader` 线程为空,则将当前线程设置为 leader 并进入有限期等待,到期后取出元素并返回。 + +自此我们阻塞式获取元素的逻辑都已完成后,源码如下,读者可自行参阅: + +```java +public E take() throws InterruptedException { + // 尝试获取可重入锁,将底层AQS的state设置为1,并设置为独占锁 + final ReentrantLock lock = this.lock; + lock.lockInterruptibly(); + try { + for (;;) { + //查看队列第一个元素 + E first = q.peek(); + //若为空,则将当前线程放入ConditionObject的等待队列中,并将底层AQS的state设置为0,表示释放锁并进入无限期等待 + if (first == null) + available.await(); + else { + //若元素不为空,则查看当前元素多久到期 + long delay = first.getDelay(NANOSECONDS); + //如果小于0则说明已到期直接返回出去 + if (delay <= 0) + return q.poll(); + //如果大于0则说明任务还没到期,首先需要释放对这个元素的引用 + first = null; // don't retain ref while waiting + //判断leader是否为空,如果不为空,则说明正有线程作为leader并等待一个任务到期,则当前线程进入无限期等待 + if (leader != null) + available.await(); + else { + //反之将我们的线程成为leader + Thread thisThread = Thread.currentThread(); + leader = thisThread; + try { + //并进入有限期等待 + available.awaitNanos(delay); + } finally { + //等待任务到期时,释放leader引用,进入下一次循环将任务return出去 + if (leader == thisThread) + leader = null; + } + } + } + } + } finally { + // 收尾逻辑:当leader为null,并且队列中有任务时,唤醒等待的获取元素的线程。 + if (leader == null && q.peek() != null) + available.signal(); + //释放锁 + lock.unlock(); + } +} +``` + +我们再来看看非阻塞的获取元素方法 `poll` ,逻辑比较简单,整体步骤如下: + +1. 尝试获取可重入锁。 +2. 查看队列第一个元素,判断元素是否为空。 +3. 若元素为空,或者元素未到期,则直接返回空。 +4. 若元素不为空且到期了,直接调用 `poll` 返回出去。 +5. 释放可重入锁 `lock` 。 + +源码如下,读者可自行参阅源码及注释: + +```java +public E poll() { + //尝试获取可重入锁 + final ReentrantLock lock = this.lock; + lock.lock(); + try { + //查看队列第一个元素,判断元素是否为空 + E first = q.peek(); + + //若元素为空,或者元素未到期,则直接返回空 + if (first == null || first.getDelay(NANOSECONDS) > 0) + return null; + else + //若元素不为空且到期了,直接调用poll返回出去 + return q.poll(); + } finally { + //释放可重入锁lock + lock.unlock(); + } +} +``` + +### 查看元素 + +上文获取元素时都会调用到 `peek` 方法,peek 顾名思义仅仅窥探一下队列中的元素,它的步骤就 4 步: + +1. 上锁。 +2. 调用优先队列 q 的 peek 方法查看索引 0 位置的元素。 +3. 释放锁。 +4. 将元素返回出去。 + +```java +public E peek() { + final ReentrantLock lock = this.lock; + lock.lock(); + try { + return q.peek(); + } finally { + lock.unlock(); + } +} +``` + +## DelayQueue 常见面试题 + +### DelayQueue 的实现原理是什么? + +`DelayQueue` 底层是使用优先队列 `PriorityQueue` 来存储元素,而 `PriorityQueue` 采用二叉小顶堆的思想确保值小的元素排在最前面,这就使得 `DelayQueue` 对于延迟任务优先级的管理就变得十分方便了。同时 `DelayQueue` 为了保证线程安全还用到了可重入锁 `ReentrantLock`,确保单位时间内只有一个线程可以操作延迟队列。最后,为了实现多线程之间等待和唤醒的交互效率,`DelayQueue` 还用到了 `Condition`,通过 `Condition` 的 `await` 和 `signal` 方法完成多线程之间的等待唤醒。 + +### DelayQueue 的实现是否线程安全? + +`DelayQueue` 的实现是线程安全的,它通过 `ReentrantLock` 实现了互斥访问和 `Condition` 实现了线程间的等待和唤醒操作,可以保证多线程环境下的安全性和可靠性。 + +### DelayQueue 的使用场景有哪些? + +`DelayQueue` 通常用于实现定时任务调度和缓存过期删除等场景。在定时任务调度中,需要将需要执行的任务封装成延迟任务对象,并将其添加到 `DelayQueue` 中,`DelayQueue` 会自动按照剩余延迟时间进行升序排序(默认情况),以保证任务能够按照时间先后顺序执行。对于缓存过期这个场景而言,在数据被缓存到内存之后,我们可以将缓存的 key 封装成一个延迟的删除任务,并将其添加到 `DelayQueue` 中,当数据过期时,拿到这个任务的 key,将这个 key 从内存中移除。 + +### DelayQueue 中 Delayed 接口的作用是什么? + +`Delayed` 接口定义了元素的剩余延迟时间(`getDelay`)和元素之间的比较规则(该接口继承了 `Comparable` 接口)。若希望元素能够存放到 `DelayQueue` 中,就必须实现 `Delayed` 接口的 `getDelay()` 方法和 `compareTo()` 方法,否则 `DelayQueue` 无法得知当前任务剩余时长和任务优先级的比较。 + +### DelayQueue 和 Timer/TimerTask 的区别是什么? + +`DelayQueue` 和 `Timer/TimerTask` 都可以用于实现定时任务调度,但是它们的实现方式不同。`DelayQueue` 是基于优先级队列和堆排序算法实现的,可以实现多个任务按照时间先后顺序执行;而 `Timer/TimerTask` 是基于单线程实现的,只能按照任务的执行顺序依次执行,如果某个任务执行时间过长,会影响其他任务的执行。另外,`DelayQueue` 还支持动态添加和移除任务,而 `Timer/TimerTask` 只能在创建时指定任务。 + +## 参考文献 + +- 《深入理解高并发编程:JDK 核心技术》: +- 一口气说出 Java 6 种延时队列的实现方法(面试官也得服): +- 图解 DelayQueue 源码(java 8)——延时队列的小九九: + diff --git a/docs/java/collection/hashmap-source-code.md b/docs/java/collection/hashmap-source-code.md index 211058378a3..0e9342f0edf 100644 --- a/docs/java/collection/hashmap-source-code.md +++ b/docs/java/collection/hashmap-source-code.md @@ -5,6 +5,8 @@ tag: - Java集合 --- + + > 感谢 [changfubai](https://github.com/changfubai) 对本文的改进做出的贡献! ## HashMap 简介 @@ -13,7 +15,7 @@ HashMap 主要用来存放键值对,它基于哈希表的 Map 接口实现, `HashMap` 可以存储 null 的 key 和 value,但 null 作为键只能有一个,null 作为值可以有多个 -JDK1.8 之前 HashMap 由 数组+链表 组成的,数组是 HashMap 的主体,链表则是主要为了解决哈希冲突而存在的(“拉链法”解决冲突)。 JDK1.8 以后的 `HashMap` 在解决哈希冲突时有了较大的变化,当链表长度大于阈值(默认为 8)(将链表转换成红黑树前会判断,如果当前数组的长度小于 64,那么会选择先进行数组扩容,而不是转换为红黑树)时,将链表转化为红黑树,以减少搜索时间。 +JDK1.8 之前 HashMap 由 数组+链表 组成的,数组是 HashMap 的主体,链表则是主要为了解决哈希冲突而存在的(“拉链法”解决冲突)。 JDK1.8 以后的 `HashMap` 在解决哈希冲突时有了较大的变化,当链表长度大于等于阈值(默认为 8)(将链表转换成红黑树前会判断,如果当前数组的长度小于 64,那么会选择先进行数组扩容,而不是转换为红黑树)时,将链表转化为红黑树,以减少搜索时间。 `HashMap` 默认的初始化大小为 16。之后每次扩充,容量变为原来的 2 倍。并且, `HashMap` 总是使用 2 的幂作为哈希表的大小。 @@ -78,40 +80,40 @@ public class HashMap extends AbstractMap implements Map, Cloneabl static final int DEFAULT_INITIAL_CAPACITY = 1 << 4; // 最大容量 static final int MAXIMUM_CAPACITY = 1 << 30; - // 默认的填充因子 + // 默认的负载因子 static final float DEFAULT_LOAD_FACTOR = 0.75f; - // 当桶(bucket)上的结点数大于这个值时会转成红黑树 + // 当桶(bucket)上的结点数大于等于这个值时会转成红黑树 static final int TREEIFY_THRESHOLD = 8; - // 当桶(bucket)上的结点数小于这个值时树转链表 + // 当桶(bucket)上的结点数小于等于这个值时树转链表 static final int UNTREEIFY_THRESHOLD = 6; // 桶中结构转化为红黑树对应的table的最小容量 static final int MIN_TREEIFY_CAPACITY = 64; // 存储元素的数组,总是2的幂次倍 transient Node[] table; - // 存放具体元素的集 + // 一个包含了映射中所有键值对的集合视图 transient Set> entrySet; // 存放元素的个数,注意这个不等于数组的长度。 transient int size; // 每次扩容和更改map结构的计数器 transient int modCount; - // 临界值(容量*填充因子) 当实际大小超过临界值时,会进行扩容 + // 阈值(容量*负载因子) 当实际大小超过阈值时,会进行扩容 int threshold; - // 加载因子 + // 负载因子 final float loadFactor; } ``` -- **loadFactor 加载因子** +- **loadFactor 负载因子** - loadFactor 加载因子是控制数组存放数据的疏密程度,loadFactor 越趋近于 1,那么 数组中存放的数据(entry)也就越多,也就越密,也就是会让链表的长度增加,loadFactor 越小,也就是趋近于 0,数组中存放的数据(entry)也就越少,也就越稀疏。 + loadFactor 负载因子是控制数组存放数据的疏密程度,loadFactor 越趋近于 1,那么 数组中存放的数据(entry)也就越多,也就越密,也就是会让链表的长度增加,loadFactor 越小,也就是趋近于 0,数组中存放的数据(entry)也就越少,也就越稀疏。 **loadFactor 太大导致查找元素效率低,太小导致数组的利用率低,存放的数据会很分散。loadFactor 的默认值为 0.75f 是官方给出的一个比较好的临界值**。 - 给定的默认容量为 16,负载因子为 0.75。Map 在使用过程中不断的往里面存放数据,当数量达到了 16 \* 0.75 = 12 就需要将当前 16 的容量进行扩容,而扩容这个过程涉及到 rehash、复制数据等操作,所以非常消耗性能。 + 给定的默认容量为 16,负载因子为 0.75。Map 在使用过程中不断的往里面存放数据,当数量超过了 16 \* 0.75 = 12 就需要将当前 16 的容量进行扩容,而扩容这个过程涉及到 rehash、复制数据等操作,所以非常消耗性能。 - **threshold** - **threshold = capacity \* loadFactor**,**当 Size>=threshold**的时候,那么就要考虑对数组的扩增了,也就是说,这个的意思就是 **衡量数组是否需要扩增的一个标准**。 + **threshold = capacity \* loadFactor**,**当 Size>threshold**的时候,那么就要考虑对数组的扩增了,也就是说,这个的意思就是 **衡量数组是否需要扩增的一个标准**。 **Node 节点类源码:** @@ -201,7 +203,7 @@ HashMap 中有四个构造方法,它们分别如下: this(initialCapacity, DEFAULT_LOAD_FACTOR); } - // 指定“容量大小”和“加载因子”的构造函数 + // 指定“容量大小”和“负载因子”的构造函数 public HashMap(int initialCapacity, float loadFactor) { if (initialCapacity < 0) throw new IllegalArgumentException("Illegal initial capacity: " + initialCapacity); @@ -210,10 +212,13 @@ HashMap 中有四个构造方法,它们分别如下: if (loadFactor <= 0 || Float.isNaN(loadFactor)) throw new IllegalArgumentException("Illegal load factor: " + loadFactor); this.loadFactor = loadFactor; + // 初始容量暂时存放到 threshold ,在resize中再赋值给 newCap 进行table初始化 this.threshold = tableSizeFor(initialCapacity); } ``` +> 值得注意的是上述四个构造方法中,都初始化了负载因子 loadFactor,由于 HashMap 中没有 capacity 这样的字段,即使指定了初始化容量 initialCapacity ,也只是通过 tableSizeFor 将其扩容到与 initialCapacity 最接近的 2 的幂次方大小,然后暂时赋值给 threshold ,后续通过 resize 方法将 threshold 赋值给 newCap 进行 table 的初始化。 + **putMapEntries 方法:** ```java @@ -222,18 +227,25 @@ final void putMapEntries(Map m, boolean evict) { if (s > 0) { // 判断table是否已经初始化 if (table == null) { // pre-size - // 未初始化,s为m的实际元素个数 + /* + * 未初始化,s为m的实际元素个数,ft=s/loadFactor => s=ft*loadFactor, 跟我们前面提到的 + * 阈值=容量*负载因子 是不是很像,是的,ft指的是要添加s个元素所需的最小的容量 + */ float ft = ((float)s / loadFactor) + 1.0F; int t = ((ft < (float)MAXIMUM_CAPACITY) ? (int)ft : MAXIMUM_CAPACITY); - // 计算得到的t大于阈值,则初始化阈值 + /* + * 根据构造函数可知,table未初始化,threshold实际上是存放的初始化容量,如果添加s个元素所 + * 需的最小容量大于初始化容量,则将最小容量扩容为最接近的2的幂次方大小作为初始化。 + * 注意这里不是初始化阈值 + */ if (t > threshold) threshold = tableSizeFor(t); } // 已初始化,并且m元素个数大于阈值,进行扩容处理 else if (s > threshold) resize(); - // 将m中的所有元素添加至HashMap中 + // 将m中的所有元素添加至HashMap中,如果table未初始化,putVal中会调用resize初始化或扩容 for (Map.Entry e : m.entrySet()) { K key = e.getKey(); V value = e.getValue(); @@ -252,12 +264,7 @@ HashMap 只提供了 put 用于添加元素,putVal 方法只是给 put 方法 1. 如果定位到的数组位置没有元素 就直接插入。 2. 如果定位到的数组位置有元素就和要插入的 key 比较,如果 key 相同就直接覆盖,如果 key 不相同,就判断 p 是否是一个树节点,如果是就调用`e = ((TreeNode)p).putTreeVal(this, tab, hash, key, value)`将元素添加进入。如果不是就遍历链表插入(插入的是链表尾部)。 -![ ](https://my-blog-to-use.oss-cn-beijing.aliyuncs.com/2019-7/put方法.png) - -说明:上图有两个小问题: - -- 直接覆盖之后应该就会 return,不会有后续操作。参考 JDK8 HashMap.java 658 行([issue#608](https://github.com/Snailclimb/JavaGuide/issues/608))。 -- 当链表长度大于阈值(默认为 8)并且 HashMap 数组长度超过 64 的时候才会执行链表转红黑树的操作,否则就只是对数组扩容。参考 HashMap 的 `treeifyBin()` 方法([issue#1087](https://github.com/Snailclimb/JavaGuide/issues/1087))。 +![ ](https://oss.javaguide.cn/github/javaguide/database/sql/put.png) ```java public V put(K key, V value) { @@ -276,7 +283,7 @@ final V putVal(int hash, K key, V value, boolean onlyIfAbsent, // 桶中已经存在元素(处理hash冲突) else { Node e; K k; - // 判断table[i]中的元素是否与插入的key一样,若相同那就直接使用插入的值p替换掉旧的值e。 + //快速判断第一个节点table[i]的key是否与插入的key一样,若相同就直接使用插入的值p替换掉旧的值e。 if (p.hash == hash && ((k = p.key) == key || (key != null && key.equals(k)))) e = p; @@ -401,7 +408,7 @@ final Node getNode(int hash, Object key) { ### resize 方法 -进行扩容,会伴随着一次重新 hash 分配,并且会遍历 hash 表中所有的元素,是非常耗时的。在编写程序中,要尽量避免 resize。 +进行扩容,会伴随着一次重新 hash 分配,并且会遍历 hash 表中所有的元素,是非常耗时的。在编写程序中,要尽量避免 resize。resize 方法实际上是将 table 初始化和 table 扩容 进行了整合,底层的行为都是给 table 赋值一个新的数组。 ```java final Node[] resize() { @@ -420,14 +427,16 @@ final Node[] resize() { newThr = oldThr << 1; // double threshold } else if (oldThr > 0) // initial capacity was placed in threshold + // 创建对象时初始化容量大小放在threshold中,此时只需要将其作为新的数组容量 newCap = oldThr; else { - // signifies using defaults + // signifies using defaults 无参构造函数创建的对象在这里计算容量和阈值 newCap = DEFAULT_INITIAL_CAPACITY; newThr = (int)(DEFAULT_LOAD_FACTOR * DEFAULT_INITIAL_CAPACITY); } - // 计算新的resize上限 if (newThr == 0) { + // 创建时指定了初始化容量或者负载因子,在这里进行阈值初始化, + // 或者扩容前的旧容量小于16,在这里计算新的resize上限 float ft = (float)newCap * loadFactor; newThr = (newCap < MAXIMUM_CAPACITY && ft < (float)MAXIMUM_CAPACITY ? (int)ft : Integer.MAX_VALUE); } @@ -442,8 +451,11 @@ final Node[] resize() { if ((e = oldTab[j]) != null) { oldTab[j] = null; if (e.next == null) + // 只有一个节点,直接计算元素新的位置即可 newTab[e.hash & (newCap - 1)] = e; else if (e instanceof TreeNode) + // 将红黑树拆分成2棵子树,如果子树节点数小于等于 UNTREEIFY_THRESHOLD(默认为 6),则将子树转换为链表。 + // 如果子树节点数大于 UNTREEIFY_THRESHOLD,则保持子树的树结构。 ((TreeNode)e).split(this, newTab, j, oldCap); else { Node loHead = null, loTail = null; @@ -561,3 +573,5 @@ public class HashMapDemo { } ``` + + diff --git a/docs/java/collection/java-collection-precautions-for-use.md b/docs/java/collection/java-collection-precautions-for-use.md index 380f63ce2bb..9bd3a4084d5 100644 --- a/docs/java/collection/java-collection-precautions-for-use.md +++ b/docs/java/collection/java-collection-precautions-for-use.md @@ -15,35 +15,58 @@ tag: > **判断所有集合内部的元素是否为空,使用 `isEmpty()` 方法,而不是 `size()==0` 的方式。** -这是因为 `isEmpty()` 方法的可读性更好,并且时间复杂度为 O(1)。 +这是因为 `isEmpty()` 方法的可读性更好,并且时间复杂度为 `O(1)`。 -绝大部分我们使用的集合的 `size()` 方法的时间复杂度也是 O(1),不过,也有很多复杂度不是 O(1) 的,比如 `java.util.concurrent` 包下的某些集合(`ConcurrentLinkedQueue`、`ConcurrentHashMap`...)。 +绝大部分我们使用的集合的 `size()` 方法的时间复杂度也是 `O(1)`,不过,也有很多复杂度不是 `O(1)` 的,比如 `java.util.concurrent` 包下的 `ConcurrentLinkedQueue`。`ConcurrentLinkedQueue` 的 `isEmpty()` 方法通过 `first()` 方法进行判断,其中 `first()` 方法返回的是队列中第一个值不为 `null` 的节点(节点值为`null`的原因是在迭代器中使用的逻辑删除) -下面是 `ConcurrentHashMap` 的 `size()` 方法和 `isEmpty()` 方法的源码。 +```java +public boolean isEmpty() { return first() == null; } + +Node first() { + restartFromHead: + for (;;) { + for (Node h = head, p = h, q;;) { + boolean hasItem = (p.item != null); + if (hasItem || (q = p.next) == null) { // 当前节点值不为空 或 到达队尾 + updateHead(h, p); // 将head设置为p + return hasItem ? p : null; + } + else if (p == q) continue restartFromHead; + else p = q; // p = p.next + } + } +} +``` + +由于在插入与删除元素时,都会执行`updateHead(h, p)`方法,所以该方法的执行的时间复杂度可以近似为`O(1)`。而 `size()` 方法需要遍历整个链表,时间复杂度为`O(n)` ```java public int size() { - long n = sumCount(); - return ((n < 0L) ? 0 : - (n > (long)Integer.MAX_VALUE) ? Integer.MAX_VALUE : - (int)n); + int count = 0; + for (Node p = first(); p != null; p = succ(p)) + if (p.item != null) + if (++count == Integer.MAX_VALUE) + break; + return count; } +``` + +此外,在`ConcurrentHashMap` 1.7 中 `size()` 方法和 `isEmpty()` 方法的时间复杂度也不太一样。`ConcurrentHashMap` 1.7 将元素数量存储在每个`Segment` 中,`size()` 方法需要统计每个 `Segment` 的数量,而 `isEmpty()` 只需要找到第一个不为空的 `Segment` 即可。但是在`ConcurrentHashMap` 1.8 中的 `size()` 方法和 `isEmpty()` 都需要调用 `sumCount()` 方法,其时间复杂度与 `Node` 数组的大小有关。下面是 `sumCount()` 方法的源码: + +```java final long sumCount() { CounterCell[] as = counterCells; CounterCell a; long sum = baseCount; - if (as != null) { - for (int i = 0; i < as.length; ++i) { + if (as != null) + for (int i = 0; i < as.length; ++i) if ((a = as[i]) != null) sum += a.value; - } - } return sum; } -public boolean isEmpty() { - return sumCount() <= 0L; // ignore transient negative values -} ``` +这是因为在并发的环境下,`ConcurrentHashMap` 将每个 `Node` 中节点的数量存储在 `CounterCell[]` 数组中。在 `ConcurrentHashMap` 1.7 中,将元素数量存储在每个`Segment` 中,`size()` 方法需要统计每个 `Segment` 的数量,而 `isEmpty()` 只需要找到第一个不为空的 `Segment` 即可。 + ## 集合转 Map 《阿里巴巴 Java 开发手册》的描述如下: @@ -111,6 +134,7 @@ public static T requireNonNull(T obj) { return obj; } ``` +> `Collectors`也提供了无需mergeFunction的`toMap()`方法,但此时若出现key冲突,则会抛出`duplicateKeyException`异常,因此强烈建议使用`toMap()`方法必填mergeFunction。 ## 集合遍历 @@ -141,7 +165,7 @@ System.out.println(list); /* [1, 3, 5, 7, 9] */ - 使用普通的 for 循环 - 使用 fail-safe 的集合类。`java.util`包下面的所有的集合类都是 fail-fast 的,而`java.util.concurrent`包下面的所有的类都是 fail-safe 的。 -- ...... +- …… ## 集合去重 @@ -213,8 +237,6 @@ public int indexOf(Object o) { ``` -我们的 `List` 有 N 个元素,那时间复杂度就接近是 O (n^2)。 - ## 集合转数组 《阿里巴巴 Java 开发手册》的描述如下: @@ -439,3 +461,5 @@ CollectionUtils.addAll(list, str); Integer[] array = {1, 2, 3}; List list = List.of(array); ``` + + diff --git a/docs/java/collection/java-collection-questions-01.md b/docs/java/collection/java-collection-questions-01.md index 532d486d446..417a2d10f53 100644 --- a/docs/java/collection/java-collection-questions-01.md +++ b/docs/java/collection/java-collection-questions-01.md @@ -12,11 +12,15 @@ head: content: Java集合常见知识点和面试题总结,希望对你有帮助! --- + + + + ## 集合概述 ### Java 集合概览 -Java 集合, 也叫作容器,主要是由两大接口派生而来:一个是 `Collection`接口,主要用于存放单一元素;另一个是 `Map` 接口,主要用于存放键值对。对于`Collection` 接口,下面又有三个主要的子接口:`List`、`Set` 和 `Queue`。 +Java 集合,也叫作容器,主要是由两大接口派生而来:一个是 `Collection`接口,主要用于存放单一元素;另一个是 `Map` 接口,主要用于存放键值对。对于`Collection` 接口,下面又有三个主要的子接口:`List`、`Set` 、 `Queue`。 Java 集合框架如下图所示: @@ -27,7 +31,7 @@ Java 集合框架如下图所示: ### 说说 List, Set, Queue, Map 四者的区别? - `List`(对付顺序的好帮手): 存储的元素是有序的、可重复的。 -- `Set`(注重独一无二的性质): 存储的元素是无序的、不可重复的。 +- `Set`(注重独一无二的性质): 存储的元素不可重复的。 - `Queue`(实现排队功能的叫号机): 按特定的排队规则来确定先后顺序,存储的元素是有序的、可重复的。 - `Map`(用 key 来搜索的专家): 使用键值对(key-value)存储,类似于数学上的函数 y=f(x),"x" 代表 key,"y" 代表 value,key 是无序的、不可重复的,value 是无序的、可重复的,每个键最多映射到一个值。 @@ -37,29 +41,30 @@ Java 集合框架如下图所示: #### List -- `ArrayList`:`Object[]` 数组 -- `Vector`:`Object[]` 数组 -- `LinkedList`:双向链表(JDK1.6 之前为循环链表,JDK1.7 取消了循环) +- `ArrayList`:`Object[]` 数组。详细可以查看:[ArrayList 源码分析](./arraylist-source-code.md)。 +- `Vector`:`Object[]` 数组。 +- `LinkedList`:双向链表(JDK1.6 之前为循环链表,JDK1.7 取消了循环)。详细可以查看:[LinkedList 源码分析](./linkedlist-source-code.md)。 #### Set -- `HashSet`(无序,唯一): 基于 `HashMap` 实现的,底层采用 `HashMap` 来保存元素 -- `LinkedHashSet`: `LinkedHashSet` 是 `HashSet` 的子类,并且其内部是通过 `LinkedHashMap` 来实现的。有点类似于我们之前说的 `LinkedHashMap` 其内部是基于 `HashMap` 实现一样,不过还是有一点点区别的 -- `TreeSet`(有序,唯一): 红黑树(自平衡的排序二叉树) +- `HashSet`(无序,唯一): 基于 `HashMap` 实现的,底层采用 `HashMap` 来保存元素。 +- `LinkedHashSet`: `LinkedHashSet` 是 `HashSet` 的子类,并且其内部是通过 `LinkedHashMap` 来实现的。 +- `TreeSet`(有序,唯一): 红黑树(自平衡的排序二叉树)。 #### Queue -- `PriorityQueue`: `Object[]` 数组来实现二叉堆 -- `ArrayQueue`: `Object[]` 数组 + 双指针 +- `PriorityQueue`: `Object[]` 数组来实现小顶堆。详细可以查看:[PriorityQueue 源码分析](./priorityqueue-source-code.md)。 +- `DelayQueue`:`PriorityQueue`。详细可以查看:[DelayQueue 源码分析](./delayqueue-source-code.md)。 +- `ArrayDeque`: 可扩容动态双向数组。 再来看看 `Map` 接口下面的集合。 #### Map -- `HashMap`:JDK1.8 之前 `HashMap` 由数组+链表组成的,数组是 `HashMap` 的主体,链表则是主要为了解决哈希冲突而存在的(“拉链法”解决冲突)。JDK1.8 以后在解决哈希冲突时有了较大的变化,当链表长度大于阈值(默认为 8)(将链表转换成红黑树前会判断,如果当前数组的长度小于 64,那么会选择先进行数组扩容,而不是转换为红黑树)时,将链表转化为红黑树,以减少搜索时间 -- `LinkedHashMap`:`LinkedHashMap` 继承自 `HashMap`,所以它的底层仍然是基于拉链式散列结构即由数组和链表或红黑树组成。另外,`LinkedHashMap` 在上面结构的基础上,增加了一条双向链表,使得上面的结构可以保持键值对的插入顺序。同时通过对链表进行相应的操作,实现了访问顺序相关逻辑。详细可以查看:[《LinkedHashMap 源码详细分析(JDK1.8)》](https://www.imooc.com/article/22931) -- `Hashtable`:数组+链表组成的,数组是 `Hashtable` 的主体,链表则是主要为了解决哈希冲突而存在的 -- `TreeMap`:红黑树(自平衡的排序二叉树) +- `HashMap`:JDK1.8 之前 `HashMap` 由数组+链表组成的,数组是 `HashMap` 的主体,链表则是主要为了解决哈希冲突而存在的(“拉链法”解决冲突)。JDK1.8 以后在解决哈希冲突时有了较大的变化,当链表长度大于阈值(默认为 8)(将链表转换成红黑树前会判断,如果当前数组的长度小于 64,那么会选择先进行数组扩容,而不是转换为红黑树)时,将链表转化为红黑树,以减少搜索时间。详细可以查看:[HashMap 源码分析](./hashmap-source-code.md)。 +- `LinkedHashMap`:`LinkedHashMap` 继承自 `HashMap`,所以它的底层仍然是基于拉链式散列结构即由数组和链表或红黑树组成。另外,`LinkedHashMap` 在上面结构的基础上,增加了一条双向链表,使得上面的结构可以保持键值对的插入顺序。同时通过对链表进行相应的操作,实现了访问顺序相关逻辑。详细可以查看:[LinkedHashMap 源码分析](./linkedhashmap-source-code.md) +- `Hashtable`:数组+链表组成的,数组是 `Hashtable` 的主体,链表则是主要为了解决哈希冲突而存在的。 +- `TreeMap`:红黑树(自平衡的排序二叉树)。 ### 如何选用集合? @@ -145,7 +150,7 @@ System.out.println(listOfStrings); 输出: -``` +```plain [null, java] ``` @@ -187,26 +192,11 @@ System.out.println(listOfStrings); - 头部插入/删除:只需要修改头结点的指针即可完成插入/删除操作,因此时间复杂度为 O(1)。 - 尾部插入/删除:只需要修改尾结点的指针即可完成插入/删除操作,因此时间复杂度为 O(1)。 -- 指定位置插入/删除:需要先移动到指定位置,再修改指定节点的指针完成插入/删除,因此需要移动平均 n/2 个元素,时间复杂度为 O(n)。 +- 指定位置插入/删除:需要先移动到指定位置,再修改指定节点的指针完成插入/删除,不过由于有头尾指针,可以从较近的指针出发,因此需要遍历平均 n/4 个元素,时间复杂度为 O(n)。 -这里简单列举一个例子: +这里简单列举一个例子:假如我们要删除节点 9 的话,需要先遍历链表找到该节点。然后,再执行相应节点指针指向的更改,具体的源码可以参考:[LinkedList 源码分析](./linkedlist-source-code.md) 。 -```java -// LinkedList中有5个元素 -+---+ +---+ +---+ +---+ +---+ -| 1 |--->| 2 |--->| 3 |--->| 4 |--->| 5 | -+---+ +---+ +---+ +---+ +---+ -// 在节点2和3之间插入一个新节点6 -// 我们需要先移动到节点2这里,再修改节点2和节点6的指针(节点2指向节点6,节点6指向节点3) -+---+ +---+ +---+ +---+ +---+ +---+ -| 1 |--->| 2 |--->| 6 |--->| 3 |--->| 4 |--->| 5 | -+---+ +---+ +---+ +---+ +---+ +---+ -// 删除节点6 -// 我们需要先移动到节点3这里,找到它的前一个节点和后一个节点,然后再修改节点2的指针(指向节点3) -+---+ +---+ +---+ +---+ +---+ -| 1 |--->| 2 |--->| 3 |--->| 4 |--->| 5 | -+---+ +---+ +---+ +---+ +---+ -``` +![unlink 方法逻辑](https://oss.javaguide.cn/github/javaguide/java/collection/linkedlist-unlink.jpg) ### LinkedList 为什么不能实现 RandomAccess 接口? @@ -247,7 +237,7 @@ public interface RandomAccess { 查看源码我们发现实际上 `RandomAccess` 接口中什么都没有定义。所以,在我看来 `RandomAccess` 接口不过是一个标识罢了。标识什么? 标识实现这个接口的类具有随机访问功能。 -在 `binarySearch()` 方法中,它要判断传入的 list 是否 `RandomAccess` 的实例,如果是,调用`indexedBinarySearch()`方法,如果不是,那么调用`iteratorBinarySearch()`方法 +在 `binarySearch()` 方法中,它要判断传入的 list 是否 `RandomAccess` 的实例,如果是,调用`indexedBinarySearch()`方法,如果不是,那么调用`iteratorBinarySearch()`方法 ```java public static @@ -259,12 +249,108 @@ public interface RandomAccess { } ``` -`ArrayList` 实现了 `RandomAccess` 接口, 而 `LinkedList` 没有实现。为什么呢?我觉得还是和底层数据结构有关!`ArrayList` 底层是数组,而 `LinkedList` 底层是链表。数组天然支持随机访问,时间复杂度为 O(1),所以称为快速随机访问。链表需要遍历到特定位置才能访问特定位置的元素,时间复杂度为 O(n),所以不支持快速随机访问。,`ArrayList` 实现了 `RandomAccess` 接口,就表明了他具有快速随机访问功能。 `RandomAccess` 接口只是标识,并不是说 `ArrayList` 实现 `RandomAccess` 接口才具有快速随机访问功能的! +`ArrayList` 实现了 `RandomAccess` 接口, 而 `LinkedList` 没有实现。为什么呢?我觉得还是和底层数据结构有关!`ArrayList` 底层是数组,而 `LinkedList` 底层是链表。数组天然支持随机访问,时间复杂度为 O(1),所以称为快速随机访问。链表需要遍历到特定位置才能访问特定位置的元素,时间复杂度为 O(n),所以不支持快速随机访问。`ArrayList` 实现了 `RandomAccess` 接口,就表明了他具有快速随机访问功能。 `RandomAccess` 接口只是标识,并不是说 `ArrayList` 实现 `RandomAccess` 接口才具有快速随机访问功能的! ### 说一说 ArrayList 的扩容机制吧 详见笔主的这篇文章: [ArrayList 扩容机制分析](https://javaguide.cn/java/collection/arraylist-source-code.html#_3-1-%E5%85%88%E4%BB%8E-arraylist-%E7%9A%84%E6%9E%84%E9%80%A0%E5%87%BD%E6%95%B0%E8%AF%B4%E8%B5%B7)。 +### 说说集合中的 fail-fast 和 fail-safe 是什么 + +关于`fail-fast`引用`medium`中一篇文章关于`fail-fast`和`fail-safe`的说法: + +> Fail-fast systems are designed to immediately stop functioning upon encountering an unexpected condition. This immediate failure helps to catch errors early, making debugging more straightforward. + +快速失败的思想即针对可能发生的异常进行提前表明故障并停止运行,通过尽早的发现和停止错误,降低故障系统级联的风险。 + +在`java.util`包下的大部分集合是不支持线程安全的,为了能够提前发现并发操作导致线程安全风险,提出通过维护一个`modCount`记录修改的次数,迭代期间通过比对预期修改次数`expectedModCount`和`modCount`是否一致来判断是否存在并发操作,从而实现快速失败,由此保证在避免在异常时执行非必要的复杂代码。 + +对应的我们给出下面这样一段在示例,我们首先插入`100`个操作元素,一个线程迭代元素,一个线程删除元素,最终输出结果如愿抛出`ConcurrentModificationException`: + +```java +// 使用线程安全的 CopyOnWriteArrayList 避免 ConcurrentModificationException +List list = new CopyOnWriteArrayList<>(); +CountDownLatch countDownLatch = new CountDownLatch(2); + +// 添加元素 +for (int i = 0; i < 100; i++) { + list.add(i); +} + +Thread t1 = new Thread(() -> { + // 迭代元素 (注意:Integer 是不可变的,这里的 i++ 不会修改 list 中的值) + for (Integer i : list) { + i++; // 这行代码实际上没有修改list中的元素 + } + countDownLatch.countDown(); +}); + +Thread t2 = new Thread(() -> { + System.out.println("删除元素1"); + list.remove(Integer.valueOf(1)); // 使用 Integer.valueOf(1) 删除指定值的对象 + countDownLatch.countDown(); +}); + +t1.start(); +t2.start(); +countDownLatch.await(); +``` + +我们在初始化时插入了`100`个元素,此时对应的修改`modCount`次数为`100`,随后线程 2 在线程 1 迭代期间进行元素删除操作,此时对应的`modCount`就变为`101`。 +线程 1 在随后`foreach`第 2 轮循环发现`modCount` 为`101`,与预期的`expectedModCount(值为100因为初始化插入了元素100个)`不等,判定为并发操作异常,于是便快速失败,抛出`ConcurrentModificationException`: + +![](https://oss.javaguide.cn/github/javaguide/java/collection/fail-fast-and-fail-safe-insert-100-values.png) + +对此我们也给出`for`循环底层迭代器获取下一个元素时的`next`方法,可以看到其内部的`checkForComodification`具有针对修改次数比对的逻辑: + +```java + public E next() { + //检查是否存在并发修改 + checkForComodification(); + //...... + //返回下一个元素 + return (E) elementData[lastRet = i]; + } + +final void checkForComodification() { + //当前循环遍历次数和预期修改次数不一致时,就会抛出ConcurrentModificationException + if (modCount != expectedModCount) + throw new ConcurrentModificationException(); + } + +``` + +而`fail-safe`也就是安全失败的含义,它旨在即使面对意外情况也能恢复并继续运行,这使得它特别适用于不确定或者不稳定的环境: + +> Fail-safe systems take a different approach, aiming to recover and continue even in the face of unexpected conditions. This makes them particularly suited for uncertain or volatile environments. + +该思想常运用于并发容器,最经典的实现就是`CopyOnWriteArrayList`的实现,通过写时复制的思想保证在进行修改操作时复制出一份快照,基于这份快照完成添加或者删除操作后,将`CopyOnWriteArrayList`底层的数组引用指向这个新的数组空间,由此避免迭代时被并发修改所干扰所导致并发操作安全问题,当然这种做法也存缺点即进行遍历操作时无法获得实时结果: + +![](https://oss.javaguide.cn/github/javaguide/java/collection/fail-fast-and-fail-safe-copyonwritearraylist.png) + +对应我们也给出`CopyOnWriteArrayList`实现`fail-safe`的核心代码,可以看到它的实现就是通过`getArray`获取数组引用然后通过`Arrays.copyOf`得到一个数组的快照,基于这个快照完成添加操作后,修改底层`array`变量指向的引用地址由此完成写时复制: + +```java +public boolean add(E e) { + final ReentrantLock lock = this.lock; + lock.lock(); + try { + //获取原有数组 + Object[] elements = getArray(); + int len = elements.length; + //基于原有数组复制出一份内存快照 + Object[] newElements = Arrays.copyOf(elements, len + 1); + //进行添加操作 + newElements[len] = e; + //array指向新的数组 + setArray(newElements); + return true; + } finally { + lock.unlock(); + } + } +``` + ## Set ### Comparable 和 Comparator 的区别 @@ -312,7 +398,7 @@ System.out.println(arrayList); Output: -``` +```plain 原始数组: [-1, 3, 3, -5, 7, 4, -9, -7] Collections.reverse(arrayList): @@ -390,7 +476,7 @@ public class Person implements Comparable { Output: -``` +```plain 5-小红 10-王五 20-李四 @@ -466,7 +552,7 @@ Output: ### 什么是 BlockingQueue? -`BlockingQueue` (阻塞队列)是一个接口,继承自 `Queue`。`BlockingQueue`阻塞的原因是其支持当队列没有元素时一直阻塞,直到有有元素;还支持如果队列已满,一直等到队列可以放入新元素时再放入。 +`BlockingQueue` (阻塞队列)是一个接口,继承自 `Queue`。`BlockingQueue`阻塞的原因是其支持当队列没有元素时一直阻塞,直到有元素;还支持如果队列已满,一直等到队列可以放入新元素时再放入。 ```java public interface BlockingQueue extends Queue { @@ -485,10 +571,21 @@ public interface BlockingQueue extends Queue { Java 中常用的阻塞队列实现类有以下几种: 1. `ArrayBlockingQueue`:使用数组实现的有界阻塞队列。在创建时需要指定容量大小,并支持公平和非公平两种方式的锁访问机制。 -2. `LinkedBlockingQueue`:使用单向链表实现的可选有界阻塞队列。在创建时可以指定容量大小,如果不指定则默认为`Integer.MAX_VALUE`。和`ArrayBlockingQueue`类似, 它也支持公平和非公平的锁访问机制。 +2. `LinkedBlockingQueue`:使用单向链表实现的可选有界阻塞队列。在创建时可以指定容量大小,如果不指定则默认为`Integer.MAX_VALUE`。和`ArrayBlockingQueue`不同的是, 它仅支持非公平的锁访问机制。 3. `PriorityBlockingQueue`:支持优先级排序的无界阻塞队列。元素必须实现`Comparable`接口或者在构造函数中传入`Comparator`对象,并且不能插入 null 元素。 4. `SynchronousQueue`:同步队列,是一种不存储元素的阻塞队列。每个插入操作都必须等待对应的删除操作,反之删除操作也必须等待插入操作。因此,`SynchronousQueue`通常用于线程之间的直接传递数据。 5. `DelayQueue`:延迟队列,其中的元素只有到了其指定的延迟时间,才能够从队列中出队。 -6. ...... +6. …… 日常开发中,这些队列使用的其实都不多,了解即可。 + +### ArrayBlockingQueue 和 LinkedBlockingQueue 有什么区别? + +`ArrayBlockingQueue` 和 `LinkedBlockingQueue` 是 Java 并发包中常用的两种阻塞队列实现,它们都是线程安全的。不过,不过它们之间也存在下面这些区别: + +- 底层实现:`ArrayBlockingQueue` 基于数组实现,而 `LinkedBlockingQueue` 基于链表实现。 +- 是否有界:`ArrayBlockingQueue` 是有界队列,必须在创建时指定容量大小。`LinkedBlockingQueue` 创建时可以不指定容量大小,默认是`Integer.MAX_VALUE`,也就是无界的。但也可以指定队列大小,从而成为有界的。 +- 锁是否分离: `ArrayBlockingQueue`中的锁是没有分离的,即生产和消费用的是同一个锁;`LinkedBlockingQueue`中的锁是分离的,即生产用的是`putLock`,消费是`takeLock`,这样可以防止生产者和消费者线程之间的锁争夺。 +- 内存占用:`ArrayBlockingQueue` 需要提前分配数组内存,而 `LinkedBlockingQueue` 则是动态分配链表节点内存。这意味着,`ArrayBlockingQueue` 在创建时就会占用一定的内存空间,且往往申请的内存比实际所用的内存更大,而`LinkedBlockingQueue` 则是根据元素的增加而逐渐占用内存空间。 + + diff --git a/docs/java/collection/java-collection-questions-02.md b/docs/java/collection/java-collection-questions-02.md index 6e07e414a3e..94eafcf9825 100644 --- a/docs/java/collection/java-collection-questions-02.md +++ b/docs/java/collection/java-collection-questions-02.md @@ -12,6 +12,8 @@ head: content: Java集合常见知识点和面试题总结,希望对你有帮助! --- + + ## Map(重要) ### HashMap 和 Hashtable 的区别 @@ -21,6 +23,7 @@ head: - **对 Null key 和 Null value 的支持:** `HashMap` 可以存储 null 的 key 和 value,但 null 作为键只能有一个,null 作为值可以有多个;Hashtable 不允许有 null 键和 null 值,否则会抛出 `NullPointerException`。 - **初始容量大小和每次扩充容量大小的不同:** ① 创建时如果不指定容量初始值,`Hashtable` 默认的初始大小为 11,之后每次扩充,容量变为原来的 2n+1。`HashMap` 默认的初始化大小为 16。之后每次扩充,容量变为原来的 2 倍。② 创建时如果给定了容量初始值,那么 `Hashtable` 会直接使用你给定的大小,而 `HashMap` 会将其扩充为 2 的幂次方大小(`HashMap` 中的`tableSizeFor()`方法保证,下面给出了源代码)。也就是说 `HashMap` 总是使用 2 的幂作为哈希表的大小,后面会介绍到为什么是 2 的幂次方。 - **底层数据结构:** JDK1.8 以后的 `HashMap` 在解决哈希冲突时有了较大的变化,当链表长度大于阈值(默认为 8)时,将链表转化为红黑树(将链表转换成红黑树前会判断,如果当前数组的长度小于 64,那么会选择先进行数组扩容,而不是转换为红黑树),以减少搜索时间(后文中我会结合源码对这一过程进行分析)。`Hashtable` 没有这样的机制。 +- **哈希函数的实现**:`HashMap` 对哈希值进行了高位和低位的混合扰动处理以减少冲突,而 `Hashtable` 直接使用键的 `hashCode()` 值。 **`HashMap` 中带有初始容量的构造函数:** @@ -45,18 +48,18 @@ head: 下面这个方法保证了 `HashMap` 总是使用 2 的幂作为哈希表的大小。 ```java - /** - * Returns a power of two size for the given target capacity. - */ - static final int tableSizeFor(int cap) { - int n = cap - 1; - n |= n >>> 1; - n |= n >>> 2; - n |= n >>> 4; - n |= n >>> 8; - n |= n >>> 16; - return (n < 0) ? 1 : (n >= MAXIMUM_CAPACITY) ? MAXIMUM_CAPACITY : n + 1; - } +/** + * Returns a power of two size for the given target capacity. + */ +static final int tableSizeFor(int cap) { + int n = cap - 1; + n |= n >>> 1; + n |= n >>> 2; + n |= n >>> 4; + n |= n >>> 8; + n |= n >>> 16; + return (n < 0) ? 1 : (n >= MAXIMUM_CAPACITY) ? MAXIMUM_CAPACITY : n + 1; +} ``` ### HashMap 和 HashSet 区别 @@ -78,6 +81,15 @@ head: 实现 `NavigableMap` 接口让 `TreeMap` 有了对集合内元素的搜索的能力。 +`NavigableMap` 接口提供了丰富的方法来探索和操作键值对: + +1. **定向搜索**: `ceilingEntry()`, `floorEntry()`, `higherEntry()`和 `lowerEntry()` 等方法可以用于定位大于等于、小于等于、严格大于、严格小于给定键的最接近的键值对。 +2. **子集操作**: `subMap()`, `headMap()`和 `tailMap()` 方法可以高效地创建原集合的子集视图,而无需复制整个集合。 +3. **逆序视图**:`descendingMap()` 方法返回一个逆序的 `NavigableMap` 视图,使得可以反向迭代整个 `TreeMap`。 +4. **边界操作**: `firstEntry()`, `lastEntry()`, `pollFirstEntry()`和 `pollLastEntry()` 等方法可以方便地访问和移除元素。 + +这些方法都是基于红黑树数据结构的属性实现的,红黑树保持平衡状态,从而保证了搜索操作的时间复杂度为 O(log n),这让 `TreeMap` 成为了处理有序集合搜索问题的强大工具。 + 实现`SortedMap`接口让 `TreeMap` 有了对集合中的元素根据键排序的能力。默认是按 key 的升序排序,不过我们也可以指定排序的比较器。示例代码如下: ```java @@ -118,7 +130,7 @@ public class Person { 输出: -``` +```plain person1 person4 person2 @@ -136,7 +148,7 @@ TreeMap treeMap = new TreeMap<>((person1, person2) -> { }); ``` -**综上,相比于`HashMap`来说 `TreeMap` 主要多了对集合中的元素根据键排序的能力以及对集合内元素的搜索的能力。** +**综上,相比于`HashMap`来说, `TreeMap` 主要多了对集合中的元素根据键排序的能力以及对集合内元素的搜索的能力。** ### HashSet 如何检查重复? @@ -173,7 +185,7 @@ final V putVal(int hash, K key, V value, boolean onlyIfAbsent, JDK1.8 之前 `HashMap` 底层是 **数组和链表** 结合在一起使用也就是 **链表散列**。HashMap 通过 key 的 `hashcode` 经过扰动函数处理过后得到 hash 值,然后通过 `(n - 1) & hash` 判断当前元素存放的位置(这里的 n 指的是数组的长度),如果当前位置存在元素的话,就判断该元素与要存入的元素的 hash 值以及 key 是否相同,如果相同的话,直接覆盖,不相同就通过拉链法解决冲突。 -所谓扰动函数指的就是 HashMap 的 `hash` 方法。使用 `hash` 方法也就是扰动函数是为了防止一些实现比较差的 `hashCode()` 方法 换句话说使用扰动函数之后可以减少碰撞。 +`HashMap` 中的扰动函数(`hash` 方法)是用来优化哈希值的分布。通过对原始的 `hashCode()` 进行额外处理,扰动函数可以减小由于糟糕的 `hashCode()` 实现导致的碰撞,从而提高数据的分布均匀性。 **JDK 1.8 HashMap 的 hash 方法源码:** @@ -210,10 +222,23 @@ static int hash(int h) { #### JDK1.8 之后 -相比于之前的版本, JDK1.8 之后在解决哈希冲突时有了较大的变化,当链表长度大于阈值(默认为 8)(将链表转换成红黑树前会判断,如果当前数组的长度小于 64,那么会选择先进行数组扩容,而不是转换为红黑树)时,将链表转化为红黑树,以减少搜索时间。 +相比于之前的版本, JDK1.8 之后在解决哈希冲突时有了较大的变化,当链表长度大于阈值(默认为 8)(将链表转换成红黑树前会判断,如果当前数组的长度小于 64,那么会选择先进行数组扩容,而不是转换为红黑树)时,将链表转化为红黑树。 + +这样做的目的是减少搜索时间:链表的查询效率为 O(n)(n 是链表的长度),红黑树是一种自平衡二叉搜索树,其查询效率为 O(log n)。当链表较短时,O(n) 和 O(log n) 的性能差异不明显。但当链表变长时,查询性能会显著下降。 ![jdk1.8之后的内部结构-HashMap](https://oss.javaguide.cn/github/javaguide/java/collection/jdk1.8_hashmap.png) +**为什么优先扩容而非直接转为红黑树?** + +数组扩容能减少哈希冲突的发生概率(即将元素重新分散到新的、更大的数组中),这在多数情况下比直接转换为红黑树更高效。 + +红黑树需要保持自平衡,维护成本较高。并且,过早引入红黑树反而会增加复杂度。 + +**为什么选择阈值 8 和 64?** + +1. 泊松分布表明,链表长度达到 8 的概率极低(小于千万分之一)。在绝大多数情况下,链表长度都不会超过 8。阈值设置为 8,可以保证性能和空间效率的平衡。 +2. 数组长度阈值 64 同样是经过实践验证的经验值。在小数组中扩容成本低,优先扩容可以避免过早引入红黑树。数组大小达到 64 时,冲突概率较高,此时红黑树的性能优势开始显现。 + > TreeMap、TreeSet 以及 JDK1.8 之后的 HashMap 底层都用到了红黑树。红黑树就是为了解决二叉查找树的缺陷,因为二叉查找树在某些情况下会退化成一个线性结构。 我们来结合源码分析一下 `HashMap` 链表到红黑树的转换。 @@ -228,7 +253,7 @@ for (int binCount = 0; ; ++binCount) { // 遍历到链表最后一个节点 if ((e = p.next) == null) { p.next = newNode(hash, key, value, null); - // 如果链表元素个数大于等于TREEIFY_THRESHOLD(8) + // 如果链表元素个数大于TREEIFY_THRESHOLD(8) if (binCount >= TREEIFY_THRESHOLD - 1) // -1 for 1st // 红黑树转换(并不会直接转换成红黑树) treeifyBin(tab, hash); @@ -274,11 +299,55 @@ final void treeifyBin(Node[] tab, int hash) { ### HashMap 的长度为什么是 2 的幂次方 -为了能让 HashMap 存取高效,尽量较少碰撞,也就是要尽量把数据分配均匀。我们上面也讲到了过了,Hash 值的范围值-2147483648 到 2147483647,前后加起来大概 40 亿的映射空间,只要哈希函数映射得比较均匀松散,一般应用是很难出现碰撞的。但问题是一个 40 亿长度的数组,内存是放不下的。所以这个散列值是不能直接拿来用的。用之前还要先做对数组的长度取模运算,得到的余数才能用来要存放的位置也就是对应的数组下标。这个数组下标的计算方法是“ `(n - 1) & hash`”。(n 代表数组长度)。这也就解释了 HashMap 的长度为什么是 2 的幂次方。 +为了让 `HashMap` 存取高效并减少碰撞,我们需要确保数据尽量均匀分布。哈希值在 Java 中通常使用 `int` 表示,其范围是 `-2147483648 ~ 2147483647`前后加起来大概 40 亿的映射空间,只要哈希函数映射得比较均匀松散,一般应用是很难出现碰撞的。但是,问题是一个 40 亿长度的数组,内存是放不下的。所以,这个散列值是不能直接拿来用的。用之前还要先做对数组的长度取模运算,得到的余数才能用来要存放的位置也就是对应的数组下标。 **这个算法应该如何设计呢?** -我们首先可能会想到采用%取余的操作来实现。但是,重点来了:**“取余(%)操作中如果除数是 2 的幂次则等价于与其除数减一的与(&)操作(也就是说 hash%length==hash&(length-1)的前提是 length 是 2 的 n 次方;)。”** 并且 **采用二进制位操作 &,相对于%能够提高运算效率,这就解释了 HashMap 的长度为什么是 2 的幂次方。** +我们首先可能会想到采用 % 取余的操作来实现。但是,重点来了:“**取余(%)操作中如果除数是 2 的幂次则等价于与其除数减一的与(&)操作**(也就是说 `hash%length==hash&(length-1)` 的前提是 length 是 2 的 n 次方)。” 并且,**采用二进制位操作 & 相对于 % 能够提高运算效率**。 + +除了上面所说的位运算比取余效率高之外,我觉得更重要的一个原因是:**长度是 2 的幂次方,可以让 `HashMap` 在扩容的时候更均匀**。例如: + +- length = 8 时,length - 1 = 7 的二进制位`0111` +- length = 16 时,length - 1 = 15 的二进制位`1111` + +这时候原本存在 `HashMap` 中的元素计算新的数组位置时 `hash&(length-1)`,取决 hash 的第四个二进制位(从右数),会出现两种情况: + +1. 第四个二进制位为 0,数组位置不变,也就是说当前元素在新数组和旧数组的位置相同。 +2. 第四个二进制位为 1,数组位置在新数组扩容之后的那一部分。 + +这里列举一个例子: + +```plain +假设有一个元素的哈希值为 10101100 + +旧数组元素位置计算: +hash = 10101100 +length - 1 = 00000111 +& ----------------- +index = 00000100 (4) + +新数组元素位置计算: +hash = 10101100 +length - 1 = 00001111 +& ----------------- +index = 00001100 (12) + +看第四位(从右数): +1.高位为 0:位置不变。 +2.高位为 1:移动到新位置(原索引位置+原容量)。 +``` + +⚠️注意:这里列举的场景看的是第四个二进制位,更准确点来说看的是高位(从右数),例如 `length = 32` 时,`length - 1 = 31`,二进制为 `11111`,这里看的就是第五个二进制位。 + +也就是说扩容之后,在旧数组元素 hash 值比较均匀(至于 hash 值均不均匀,取决于前面讲的对象的 `hashcode()` 方法和扰动函数)的情况下,新数组元素也会被分配的比较均匀,最好的情况是会有一半在新数组的前半部分,一半在新数组后半部分。 + +这样也使得扩容机制变得简单和高效,扩容后只需检查哈希值高位的变化来决定元素的新位置,要么位置不变(高位为 0),要么就是移动到新位置(高位为 1,原索引位置+原容量)。 + +最后,简单总结一下 `HashMap` 的长度是 2 的幂次方的原因: + +1. 位运算效率更高:位运算(&)比取余运算(%)更高效。当长度为 2 的幂次方时,`hash % length` 等价于 `hash & (length - 1)`。 +2. 可以更好地保证哈希值的均匀分布:扩容之后,在旧数组元素 hash 值比较均匀的情况下,新数组元素也会被分配的比较均匀,最好的情况是会有一半在新数组的前半部分,一半在新数组后半部分。 +3. 扩容机制变得简单和高效:扩容后只需检查哈希值高位的变化来决定元素的新位置,要么位置不变(高位为 0),要么就是移动到新位置(高位为 1,原索引位置+原容量)。 ### HashMap 多线程操作导致死循环问题 @@ -354,7 +423,7 @@ final V putVal(int hash, K key, V value, boolean onlyIfAbsent, 当遍历不存在阻塞时, parallelStream 的性能是最低的: -``` +```plain Benchmark Mode Cnt Score Error Units Test.entrySet avgt 5 288.651 ± 10.536 ns/op Test.keySet avgt 5 584.594 ± 21.431 ns/op @@ -364,7 +433,7 @@ Test.parallelStream avgt 5 6919.163 ± 1116.139 ns/op 加入阻塞代码`Thread.sleep(10)`后, parallelStream 的性能才是最高的: -``` +```plain Benchmark Mode Cnt Score Error Units Test.entrySet avgt 5 1554828440.000 ± 23657748.653 ns/op Test.keySet avgt 5 1550612500.000 ± 6474562.858 ns/op @@ -457,6 +526,89 @@ Java 8 中,锁粒度更细,`synchronized` 只锁定当前链表或红黑二 - **Hash 碰撞解决方法** : JDK 1.7 采用拉链法,JDK1.8 采用拉链法结合红黑树(链表长度超过一定阈值时,将链表转换为红黑树)。 - **并发度**:JDK 1.7 最大并发度是 Segment 的个数,默认是 16。JDK 1.8 最大并发度是 Node 数组的大小,并发度更大。 +### ConcurrentHashMap 为什么 key 和 value 不能为 null? + +`ConcurrentHashMap` 的 key 和 value 不能为 null 主要是为了避免二义性。null 是一个特殊的值,表示没有对象或没有引用。如果你用 null 作为键,那么你就无法区分这个键是否存在于 `ConcurrentHashMap` 中,还是根本没有这个键。同样,如果你用 null 作为值,那么你就无法区分这个值是否是真正存储在 `ConcurrentHashMap` 中的,还是因为找不到对应的键而返回的。 + +拿 get 方法取值来说,返回的结果为 null 存在两种情况: + +- 值没有在集合中 ; +- 值本身就是 null。 + +这也就是二义性的由来。 + +具体可以参考 [ConcurrentHashMap 源码分析](https://javaguide.cn/java/collection/concurrent-hash-map-source-code.html) 。 + +多线程环境下,存在一个线程操作该 `ConcurrentHashMap` 时,其他的线程将该 `ConcurrentHashMap` 修改的情况,所以无法通过 `containsKey(key)` 来判断否存在这个键值对,也就没办法解决二义性问题了。 + +与此形成对比的是,`HashMap` 可以存储 null 的 key 和 value,但 null 作为键只能有一个,null 作为值可以有多个。如果传入 null 作为参数,就会返回 hash 值为 0 的位置的值。单线程环境下,不存在一个线程操作该 HashMap 时,其他的线程将该 `HashMap` 修改的情况,所以可以通过 `contains(key)`来做判断是否存在这个键值对,从而做相应的处理,也就不存在二义性问题。 + +也就是说,多线程下无法正确判定键值对是否存在(存在其他线程修改的情况),单线程是可以的(不存在其他线程修改的情况)。 + +如果你确实需要在 ConcurrentHashMap 中使用 null 的话,可以使用一个特殊的静态空对象来代替 null。 + +```java +public static final Object NULL = new Object(); +``` + +最后,再分享一下 `ConcurrentHashMap` 作者本人 (Doug Lea)对于这个问题的回答: + +> The main reason that nulls aren't allowed in ConcurrentMaps (ConcurrentHashMaps, ConcurrentSkipListMaps) is that ambiguities that may be just barely tolerable in non-concurrent maps can't be accommodated. The main one is that if `map.get(key)` returns `null`, you can't detect whether the key explicitly maps to `null` vs the key isn't mapped. In a non-concurrent map, you can check this via `map.contains(key)`, but in a concurrent one, the map might have changed between calls. + +翻译过来之后的,大致意思还是单线程下可以容忍歧义,而多线程下无法容忍。 + +### ConcurrentHashMap 能保证复合操作的原子性吗? + +`ConcurrentHashMap` 是线程安全的,意味着它可以保证多个线程同时对它进行读写操作时,不会出现数据不一致的情况,也不会导致 JDK1.7 及之前版本的 `HashMap` 多线程操作导致死循环问题。但是,这并不意味着它可以保证所有的复合操作都是原子性的,一定不要搞混了! + +复合操作是指由多个基本操作(如`put`、`get`、`remove`、`containsKey`等)组成的操作,例如先判断某个键是否存在`containsKey(key)`,然后根据结果进行插入或更新`put(key, value)`。这种操作在执行过程中可能会被其他线程打断,导致结果不符合预期。 + +例如,有两个线程 A 和 B 同时对 `ConcurrentHashMap` 进行复合操作,如下: + +```java +// 线程 A +if (!map.containsKey(key)) { +map.put(key, value); +} +// 线程 B +if (!map.containsKey(key)) { +map.put(key, anotherValue); +} +``` + +如果线程 A 和 B 的执行顺序是这样: + +1. 线程 A 判断 map 中不存在 key +2. 线程 B 判断 map 中不存在 key +3. 线程 B 将 (key, anotherValue) 插入 map +4. 线程 A 将 (key, value) 插入 map + +那么最终的结果是 (key, value),而不是预期的 (key, anotherValue)。这就是复合操作的非原子性导致的问题。 + +**那如何保证 `ConcurrentHashMap` 复合操作的原子性呢?** + +`ConcurrentHashMap` 提供了一些原子性的复合操作,如 `putIfAbsent`、`compute`、`computeIfAbsent` 、`computeIfPresent`、`merge`等。这些方法都可以接受一个函数作为参数,根据给定的 key 和 value 来计算一个新的 value,并且将其更新到 map 中。 + +上面的代码可以改写为: + +```java +// 线程 A +map.putIfAbsent(key, value); +// 线程 B +map.putIfAbsent(key, anotherValue); +``` + +或者: + +```java +// 线程 A +map.computeIfAbsent(key, k -> value); +// 线程 B +map.computeIfAbsent(key, k -> anotherValue); +``` + +很多同学可能会说了,这种情况也能加锁同步呀!确实可以,但不建议使用加锁的同步机制,违背了使用 `ConcurrentHashMap` 的初衷。在使用 `ConcurrentHashMap` 的时候,尽量使用这些原子性的复合操作方法来保证原子性。 + ## Collections 工具类(不重要) **`Collections` 工具类常用方法**: @@ -504,3 +656,5 @@ synchronizedList(List list)//返回指定列表支持的同步(线程安全 synchronizedMap(Map m) //返回由指定映射支持的同步(线程安全的)Map。 synchronizedSet(Set s) //返回指定 set 支持的同步(线程安全的)set。 ``` + + diff --git a/docs/java/collection/linkedhashmap-source-code.md b/docs/java/collection/linkedhashmap-source-code.md new file mode 100644 index 00000000000..08c9a2bcb28 --- /dev/null +++ b/docs/java/collection/linkedhashmap-source-code.md @@ -0,0 +1,590 @@ +--- +title: LinkedHashMap 源码分析 +category: Java +tag: + - Java集合 +--- + +## LinkedHashMap 简介 + +`LinkedHashMap` 是 Java 提供的一个集合类,它继承自 `HashMap`,并在 `HashMap` 基础上维护一条双向链表,使得具备如下特性: + +1. 支持遍历时会按照插入顺序有序进行迭代。 +2. 支持按照元素访问顺序排序,适用于封装 LRU 缓存工具。 +3. 因为内部使用双向链表维护各个节点,所以遍历时的效率和元素个数成正比,相较于和容量成正比的 HashMap 来说,迭代效率会高很多。 + +`LinkedHashMap` 逻辑结构如下图所示,它是在 `HashMap` 基础上在各个节点之间维护一条双向链表,使得原本散列在不同 bucket 上的节点、链表、红黑树有序关联起来。 + +![LinkedHashMap 逻辑结构](https://oss.javaguide.cn/github/javaguide/java/collection/linkhashmap-structure-overview.png) + +## LinkedHashMap 使用示例 + +### 插入顺序遍历 + +如下所示,我们按照顺序往 `LinkedHashMap` 添加元素然后进行遍历。 + +```java +HashMap < String, String > map = new LinkedHashMap < > (); +map.put("a", "2"); +map.put("g", "3"); +map.put("r", "1"); +map.put("e", "23"); + +for (Map.Entry < String, String > entry: map.entrySet()) { + System.out.println(entry.getKey() + ":" + entry.getValue()); +} +``` + +输出: + +```java +a:2 +g:3 +r:1 +e:23 +``` + +可以看出,`LinkedHashMap` 的迭代顺序是和插入顺序一致的,这一点是 `HashMap` 所不具备的。 + +### 访问顺序遍历 + +`LinkedHashMap` 定义了排序模式 `accessOrder`(boolean 类型,默认为 false),访问顺序则为 true,插入顺序则为 false。 + +为了实现访问顺序遍历,我们可以使用传入 `accessOrder` 属性的 `LinkedHashMap` 构造方法,并将 `accessOrder` 设置为 true,表示其具备访问有序性。 + +```java +LinkedHashMap map = new LinkedHashMap<>(16, 0.75f, true); +map.put(1, "one"); +map.put(2, "two"); +map.put(3, "three"); +map.put(4, "four"); +map.put(5, "five"); +//访问元素2,该元素会被移动至链表末端 +map.get(2); +//访问元素3,该元素会被移动至链表末端 +map.get(3); +for (Map.Entry entry : map.entrySet()) { + System.out.println(entry.getKey() + " : " + entry.getValue()); +} +``` + +输出: + +```java +1 : one +4 : four +5 : five +2 : two +3 : three +``` + +可以看出,`LinkedHashMap` 的迭代顺序是和访问顺序一致的。 + +### LRU 缓存 + +从上一个我们可以了解到通过 `LinkedHashMap` 我们可以封装一个简易版的 LRU(**L**east **R**ecently **U**sed,最近最少使用) 缓存,确保当存放的元素超过容器容量时,将最近最少访问的元素移除。 + +![](https://oss.javaguide.cn/github/javaguide/java/collection/lru-cache.png) + +具体实现思路如下: + +- 继承 `LinkedHashMap`; +- 构造方法中指定 `accessOrder` 为 true ,这样在访问元素时就会把该元素移动到链表尾部,链表首元素就是最近最少被访问的元素; +- 重写`removeEldestEntry` 方法,该方法会返回一个 boolean 值,告知 `LinkedHashMap` 是否需要移除链表首元素(缓存容量有限)。 + +```java +public class LRUCache extends LinkedHashMap { + private final int capacity; + + public LRUCache(int capacity) { + super(capacity, 0.75f, true); + this.capacity = capacity; + } + + /** + * 判断size超过容量时返回true,告知LinkedHashMap移除最老的缓存项(即链表的第一个元素) + */ + @Override + protected boolean removeEldestEntry(Map.Entry eldest) { + return size() > capacity; + } +} +``` + +测试代码如下,笔者初始化缓存容量为 3,然后按照次序先后添加 4 个元素。 + +```java +LRUCache cache = new LRUCache<>(3); +cache.put(1, "one"); +cache.put(2, "two"); +cache.put(3, "three"); +cache.put(4, "four"); +cache.put(5, "five"); +for (int i = 1; i <= 5; i++) { + System.out.println(cache.get(i)); +} +``` + +输出: + +```java +null +null +three +four +five +``` + +从输出结果来看,由于缓存容量为 3 ,因此,添加第 4 个元素时,第 1 个元素会被删除。添加第 5 个元素时,第 2 个元素会被删除。 + +## LinkedHashMap 源码解析 + +### Node 的设计 + +在正式讨论 `LinkedHashMap` 前,我们先来聊聊 `LinkedHashMap` 节点 `Entry` 的设计,我们都知道 `HashMap` 的 bucket 上的因为冲突转为链表的节点会在符合以下两个条件时会将链表转为红黑树: + +1. ~~链表上的节点个数达到树化的阈值 7,即`TREEIFY_THRESHOLD - 1`。~~ +2. bucket 的容量达到最小的树化容量即`MIN_TREEIFY_CAPACITY`。 + +> **🐛 修正(参见:[issue#2147](https://github.com/Snailclimb/JavaGuide/issues/2147))**: +> +> 链表上的节点个数达到树化的阈值是 8 而非 7。因为源码的判断是从链表初始元素开始遍历,下标是从 0 开始的,所以判断条件设置为 8-1=7,其实是迭代到尾部元素时再判断整个链表长度大于等于 8 才进行树化操作。 +> +> ![](https://oss.javaguide.cn/github/javaguide/java/jvm/LinkedHashMap-putval-TREEIFY.png) + +而 `LinkedHashMap` 是在 `HashMap` 的基础上为 bucket 上的每一个节点建立一条双向链表,这就使得转为红黑树的树节点也需要具备双向链表节点的特性,即每一个树节点都需要拥有两个引用存储前驱节点和后继节点的地址,所以对于树节点类 `TreeNode` 的设计就是一个比较棘手的问题。 + +对此我们不妨来看看两者之间节点类的类图,可以看到: + +1. `LinkedHashMap` 的节点内部类 `Entry` 基于 `HashMap` 的基础上,增加 `before` 和 `after` 指针使节点具备双向链表的特性。 +2. `HashMap` 的树节点 `TreeNode` 继承了具备双向链表特性的 `LinkedHashMap` 的 `Entry`。 + +![LinkedHashMap 和 HashMap 之间的关系](https://oss.javaguide.cn/github/javaguide/java/collection/map-hashmap-linkedhashmap.png) + +很多读者此时就会有这样一个疑问,为什么 `HashMap` 的树节点 `TreeNode` 要通过 `LinkedHashMap` 获取双向链表的特性呢?为什么不直接在 `Node` 上实现前驱和后继指针呢? + +先来回答第一个问题,我们都知道 `LinkedHashMap` 是在 `HashMap` 基础上对节点增加双向指针实现双向链表的特性,所以 `LinkedHashMap` 内部链表转红黑树时,对应的节点会转为树节点 `TreeNode`,为了保证使用 `LinkedHashMap` 时树节点具备双向链表的特性,所以树节点 `TreeNode` 需要继承 `LinkedHashMap` 的 `Entry`。 + +再来说说第二个问题,我们直接在 `HashMap` 的节点 `Node` 上直接实现前驱和后继指针,然后 `TreeNode` 直接继承 `Node` 获取双向链表的特性为什么不行呢?其实这样做也是可以的。只不过这种做法会使得使用 `HashMap` 时存储键值对的节点类 `Node` 多了两个没有必要的引用,占用没必要的内存空间。 + +所以,为了保证 `HashMap` 底层的节点类 `Node` 没有多余的引用,又要保证 `LinkedHashMap` 的节点类 `Entry` 拥有存储链表的引用,设计者就让 `LinkedHashMap` 的节点 `Entry` 去继承 Node 并增加存储前驱后继节点的引用 `before`、`after`,让需要用到链表特性的节点去实现需要的逻辑。然后树节点 `TreeNode` 再通过继承 `Entry` 获取 `before`、`after` 两个指针。 + +```java +static class Entry extends HashMap.Node { + Entry before, after; + Entry(int hash, K key, V value, Node next) { + super(hash, key, value, next); + } + } +``` + +但是这样做,不也使得使用 `HashMap` 时的 `TreeNode` 多了两个没有必要的引用吗?这不也是一种空间的浪费吗? + +```java +static final class TreeNode extends LinkedHashMap.Entry { + //略 + +} +``` + +对于这个问题,引用作者的一段注释,作者们认为在良好的 `hashCode` 算法时,`HashMap` 转红黑树的概率不大。就算转为红黑树变为树节点,也可能会因为移除或者扩容将 `TreeNode` 变为 `Node`,所以 `TreeNode` 的使用概率不算很大,对于这一点资源空间的浪费是可以接受的。 + +```bash +Because TreeNodes are about twice the size of regular nodes, we +use them only when bins contain enough nodes to warrant use +(see TREEIFY_THRESHOLD). And when they become too small (due to +removal or resizing) they are converted back to plain bins. In +usages with well-distributed user hashCodes, tree bins are +rarely used. Ideally, under random hashCodes, the frequency of +nodes in bins follows a Poisson distribution +``` + +### 构造方法 + +`LinkedHashMap` 构造方法有 4 个实现也比较简单,直接调用父类即 `HashMap` 的构造方法完成初始化。 + +```java +public LinkedHashMap() { + super(); + accessOrder = false; +} + +public LinkedHashMap(int initialCapacity) { + super(initialCapacity); + accessOrder = false; +} + +public LinkedHashMap(int initialCapacity, float loadFactor) { + super(initialCapacity, loadFactor); + accessOrder = false; +} + +public LinkedHashMap(int initialCapacity, + float loadFactor, + boolean accessOrder) { + super(initialCapacity, loadFactor); + this.accessOrder = accessOrder; +} +``` + +我们上面也提到了,默认情况下 `accessOrder` 为 false,如果我们要让 `LinkedHashMap` 实现键值对按照访问顺序排序(即将最近未访问的元素排在链表首部、最近访问的元素移动到链表尾部),需要调用第 4 个构造方法将 `accessOrder` 设置为 true。 + +### get 方法 + +`get` 方法是 `LinkedHashMap` 增删改查操作中唯一一个重写的方法, `accessOrder` 为 true 的情况下, 它会在元素查询完成之后,将当前访问的元素移到链表的末尾。 + +```java +public V get(Object key) { + Node < K, V > e; + //获取key的键值对,若为空直接返回 + if ((e = getNode(hash(key), key)) == null) + return null; + //若accessOrder为true,则调用afterNodeAccess将当前元素移到链表末尾 + if (accessOrder) + afterNodeAccess(e); + //返回键值对的值 + return e.value; + } +``` + +从源码可以看出,`get` 的执行步骤非常简单: + +1. 调用父类即 `HashMap` 的 `getNode` 获取键值对,若为空则直接返回。 +2. 判断 `accessOrder` 是否为 true,若为 true 则说明需要保证 `LinkedHashMap` 的链表访问有序性,执行步骤 3。 +3. 调用 `LinkedHashMap` 重写的 `afterNodeAccess` 将当前元素添加到链表末尾。 + +关键点在于 `afterNodeAccess` 方法的实现,这个方法负责将元素移动到链表末尾。 + +```java +void afterNodeAccess(Node < K, V > e) { // move node to last + LinkedHashMap.Entry < K, V > last; + //如果accessOrder 且当前节点不为链表尾节点 + if (accessOrder && (last = tail) != e) { + + //获取当前节点、以及前驱节点和后继节点 + LinkedHashMap.Entry < K, V > p = + (LinkedHashMap.Entry < K, V > ) e, b = p.before, a = p.after; + + //将当前节点的后继节点指针指向空,使其和后继节点断开联系 + p.after = null; + + //如果前驱节点为空,则说明当前节点是链表的首节点,故将后继节点设置为首节点 + if (b == null) + head = a; + else + //如果前驱节点不为空,则让前驱节点指向后继节点 + b.after = a; + + //如果后继节点不为空,则让后继节点指向前驱节点 + if (a != null) + a.before = b; + else + //如果后继节点为空,则说明当前节点在链表最末尾,直接让last 指向前驱节点,这个 else其实 没有意义,因为最开头if已经确保了p不是尾结点了,自然after不会是null + last = b; + + //如果last为空,则说明当前链表只有一个节点p,则将head指向p + if (last == null) + head = p; + else { + //反之让p的前驱指针指向尾节点,再让尾节点的前驱指针指向p + p.before = last; + last.after = p; + } + //tail指向p,自此将节点p移动到链表末尾 + tail = p; + + ++modCount; + } +} +``` + +从源码可以看出, `afterNodeAccess` 方法完成了下面这些操作: + +1. 如果 `accessOrder` 为 true 且链表尾部不为当前节点 p,我们则需要将当前节点移到链表尾部。 +2. 获取当前节点 p、以及它的前驱节点 b 和后继节点 a。 +3. 将当前节点 p 的后继指针设置为 null,使其和后继节点 p 断开联系。 +4. 尝试将前驱节点指向后继节点,若前驱节点为空,则说明当前节点 p 就是链表首节点,故直接将后继节点 a 设置为首节点,随后我们再将 p 追加到 a 的末尾。 +5. 再尝试让后继节点 a 指向前驱节点 b。 +6. 上述操作让前驱节点和后继节点完成关联,并将当前节点 p 独立出来,这一步则是将当前节点 p 追加到链表末端,如果链表末端为空,则说明当前链表只有一个节点 p,所以直接让 head 指向 p 即可。 +7. 上述操作已经将 p 成功到达链表末端,最后我们将 tail 指针即指向链表末端的指针指向 p 即可。 + +可以结合这张图理解,展示了 key 为 13 的元素被移动到了链表尾部。 + +![LinkedHashMap 移动元素 13 到链表尾部](https://oss.javaguide.cn/github/javaguide/java/collection/linkedhashmap-get.png) + +看不太懂也没关系,知道这个方法的作用就够了,后续有时间再慢慢消化。 + +### remove 方法后置操作——afterNodeRemoval + +`LinkedHashMap` 并没有对 `remove` 方法进行重写,而是直接继承 `HashMap` 的 `remove` 方法,为了保证键值对移除后双向链表中的节点也会同步被移除,`LinkedHashMap` 重写了 `HashMap` 的空实现方法 `afterNodeRemoval`。 + +```java +final Node removeNode(int hash, Object key, Object value, + boolean matchValue, boolean movable) { + //略 + if (node != null && (!matchValue || (v = node.value) == value || + (value != null && value.equals(v)))) { + if (node instanceof TreeNode) + ((TreeNode)node).removeTreeNode(this, tab, movable); + else if (node == p) + tab[index] = node.next; + else + p.next = node.next; + ++modCount; + --size; + //HashMap的removeNode完成元素移除后会调用afterNodeRemoval进行移除后置操作 + afterNodeRemoval(node); + return node; + } + } + return null; + } +//空实现 +void afterNodeRemoval(Node p) { } +``` + +我们可以看到从 `HashMap` 继承来的 `remove` 方法内部调用的 `removeNode` 方法将节点从 bucket 删除后,调用了 `afterNodeRemoval`。 + +```java +void afterNodeRemoval(Node e) { // unlink + + //获取当前节点p、以及e的前驱节点b和后继节点a + LinkedHashMap.Entry p = + (LinkedHashMap.Entry)e, b = p.before, a = p.after; + //将p的前驱和后继指针都设置为null,使其和前驱、后继节点断开联系 + p.before = p.after = null; + + //如果前驱节点为空,则说明当前节点p是链表首节点,让head指针指向后继节点a即可 + if (b == null) + head = a; + else + //如果前驱节点b不为空,则让b直接指向后继节点a + b.after = a; + + //如果后继节点为空,则说明当前节点p在链表末端,所以直接让tail指针指向前驱节点a即可 + if (a == null) + tail = b; + else + //反之后继节点的前驱指针直接指向前驱节点 + a.before = b; + } +``` + +从源码可以看出, `afterNodeRemoval` 方法的整体操作就是让当前节点 p 和前驱节点、后继节点断开联系,等待 gc 回收,整体步骤为: + +1. 获取当前节点 p、以及 p 的前驱节点 b 和后继节点 a。 +2. 让当前节点 p 和其前驱、后继节点断开联系。 +3. 尝试让前驱节点 b 指向后继节点 a,若 b 为空则说明当前节点 p 在链表首部,我们直接将 head 指向后继节点 a 即可。 +4. 尝试让后继节点 a 指向前驱节点 b,若 a 为空则说明当前节点 p 在链表末端,所以直接让 tail 指针指向前驱节点 b 即可。 + +可以结合这张图理解,展示了 key 为 13 的元素被删除,也就是从链表中移除了这个元素。 + +![LinkedHashMap 删除元素 13](https://oss.javaguide.cn/github/javaguide/java/collection/linkedhashmap-remove.png) + +看不太懂也没关系,知道这个方法的作用就够了,后续有时间再慢慢消化。 + +### put 方法后置操作——afterNodeInsertion + +同样的 `LinkedHashMap` 并没有实现插入方法,而是直接继承 `HashMap` 的所有插入方法交由用户使用,但为了维护双向链表访问的有序性,它做了这样两件事: + +1. 重写 `afterNodeAccess`(上文提到过),如果当前被插入的 key 已存在与 `map` 中,因为 `LinkedHashMap` 的插入操作会将新节点追加至链表末尾,所以对于存在的 key 则调用 `afterNodeAccess` 将其放到链表末端。 +2. 重写了 `HashMap` 的 `afterNodeInsertion` 方法,当 `removeEldestEntry` 返回 true 时,会将链表首节点移除。 + +这一点我们可以在 `HashMap` 的插入操作核心方法 `putVal` 中看到。 + +```java +final V putVal(int hash, K key, V value, boolean onlyIfAbsent, + boolean evict) { + //略 + if (e != null) { // existing mapping for key + V oldValue = e.value; + if (!onlyIfAbsent || oldValue == null) + e.value = value; + //如果当前的key在map中存在,则调用afterNodeAccess + afterNodeAccess(e); + return oldValue; + } + } + ++modCount; + if (++size > threshold) + resize(); + //调用插入后置方法,该方法被LinkedHashMap重写 + afterNodeInsertion(evict); + return null; + } +``` + +上述步骤的源码上文已经解释过了,所以这里我们着重了解一下 `afterNodeInsertion` 的工作流程,假设我们的重写了 `removeEldestEntry`,当链表 `size` 超过 `capacity` 时,就返回 true。 + +```java +/** + * 判断size超过容量时返回true,告知LinkedHashMap移除最老的缓存项(即链表的第一个元素) + */ +protected boolean removeEldestEntry(Map.Entry < K, V > eldest) { + return size() > capacity; +} +``` + +以下图为例,假设笔者最后新插入了一个不存在的节点 19,假设 `capacity` 为 4,所以 `removeEldestEntry` 返回 true,我们要将链表首节点移除。 + +![LinkedHashMap 中插入新元素 19](https://oss.javaguide.cn/github/javaguide/java/collection/linkedhashmap-after-insert-1.png) + +移除的步骤很简单,查看链表首节点是否存在,若存在则断开首节点和后继节点的关系,并让首节点指针指向下一节点,所以 head 指针指向了 12,节点 10 成为没有任何引用指向的空对象,等待 GC。 + +![LinkedHashMap 中插入新元素 19](https://oss.javaguide.cn/github/javaguide/java/collection/linkedhashmap-after-insert-2.png) + +```java +void afterNodeInsertion(boolean evict) { // possibly remove eldest + LinkedHashMap.Entry first; + //如果evict为true且队首元素不为空以及removeEldestEntry返回true,则说明我们需要最老的元素(即在链表首部的元素)移除。 + if (evict && (first = head) != null && removeEldestEntry(first)) { + //获取链表首部的键值对的key + K key = first.key; + //调用removeNode将元素从HashMap的bucket中移除,并和LinkedHashMap的双向链表断开,等待gc回收 + removeNode(hash(key), key, null, false, true); + } + } +``` + +从源码可以看出, `afterNodeInsertion` 方法完成了下面这些操作: + +1. 判断 `eldest` 是否为 true,只有为 true 才能说明可能需要将最年长的键值对(即链表首部的元素)进行移除,具体是否具体要进行移除,还得确定链表是否为空`((first = head) != null)`,以及 `removeEldestEntry` 方法是否返回 true,只有这两个方法返回 true 才能确定当前链表不为空,且链表需要进行移除操作了。 +2. 获取链表第一个元素的 key。 +3. 调用 `HashMap` 的 `removeNode` 方法,该方法我们上文提到过,它会将节点从 `HashMap` 的 bucket 中移除,并且 `LinkedHashMap` 还重写了 `removeNode` 中的 `afterNodeRemoval` 方法,所以这一步将通过调用 `removeNode` 将元素从 `HashMap` 的 bucket 中移除,并和 `LinkedHashMap` 的双向链表断开,等待 gc 回收。 + +## LinkedHashMap 和 HashMap 遍历性能比较 + +`LinkedHashMap` 维护了一个双向链表来记录数据插入的顺序,因此在迭代遍历生成的迭代器的时候,是按照双向链表的路径进行遍历的。这一点相比于 `HashMap` 那种遍历整个 bucket 的方式来说,高效许多。 + +这一点我们可以从两者的迭代器中得以印证,先来看看 `HashMap` 的迭代器,可以看到 `HashMap` 迭代键值对时会用到一个 `nextNode` 方法,该方法会返回 next 指向的下一个元素,并会从 next 开始遍历 bucket 找到下一个 bucket 中不为空的元素 Node。 + +```java + final class EntryIterator extends HashIterator + implements Iterator < Map.Entry < K, V >> { + public final Map.Entry < K, + V > next() { + return nextNode(); + } + } + + //获取下一个Node + final Node < K, V > nextNode() { + Node < K, V > [] t; + //获取下一个元素next + Node < K, V > e = next; + if (modCount != expectedModCount) + throw new ConcurrentModificationException(); + if (e == null) + throw new NoSuchElementException(); + //将next指向bucket中下一个不为空的Node + if ((next = (current = e).next) == null && (t = table) != null) { + do {} while (index < t.length && (next = t[index++]) == null); + } + return e; + } +``` + +相比之下 `LinkedHashMap` 的迭代器则是直接使用通过 `after` 指针快速定位到当前节点的后继节点,简洁高效许多。 + +```java + final class LinkedEntryIterator extends LinkedHashIterator + implements Iterator < Map.Entry < K, V >> { + public final Map.Entry < K, + V > next() { + return nextNode(); + } + } + //获取下一个Node + final LinkedHashMap.Entry < K, V > nextNode() { + //获取下一个节点next + LinkedHashMap.Entry < K, V > e = next; + if (modCount != expectedModCount) + throw new ConcurrentModificationException(); + if (e == null) + throw new NoSuchElementException(); + //current 指针指向当前节点 + current = e; + //next直接当前节点的after指针快速定位到下一个节点 + next = e.after; + return e; + } +``` + +为了验证笔者所说的观点,笔者对这两个容器进行了压测,测试插入 1000w 和迭代 1000w 条数据的耗时,代码如下: + +```java +int count = 1000_0000; +Map hashMap = new HashMap<>(); +Map linkedHashMap = new LinkedHashMap<>(); + +long start, end; + +start = System.currentTimeMillis(); +for (int i = 0; i < count; i++) { + hashMap.put(ThreadLocalRandom.current().nextInt(1, count), ThreadLocalRandom.current().nextInt(0, count)); +} +end = System.currentTimeMillis(); +System.out.println("map time putVal: " + (end - start)); + +start = System.currentTimeMillis(); +for (int i = 0; i < count; i++) { + linkedHashMap.put(ThreadLocalRandom.current().nextInt(1, count), ThreadLocalRandom.current().nextInt(0, count)); +} +end = System.currentTimeMillis(); +System.out.println("linkedHashMap putVal time: " + (end - start)); + +start = System.currentTimeMillis(); +long num = 0; +for (Integer v : hashMap.values()) { + num = num + v; +} +end = System.currentTimeMillis(); +System.out.println("map get time: " + (end - start)); + +start = System.currentTimeMillis(); +for (Integer v : linkedHashMap.values()) { + num = num + v; +} +end = System.currentTimeMillis(); +System.out.println("linkedHashMap get time: " + (end - start)); +System.out.println(num); +``` + +从输出结果来看,因为 `LinkedHashMap` 需要维护双向链表的缘故,插入元素相较于 `HashMap` 会更耗时,但是有了双向链表明确的前后节点关系,迭代效率相对于前者高效了许多。不过,总体来说却别不大,毕竟数据量这么庞大。 + +```bash +map time putVal: 5880 +linkedHashMap putVal time: 7567 +map get time: 143 +linkedHashMap get time: 67 +63208969074998 +``` + +## LinkedHashMap 常见面试题 + +### 什么是 LinkedHashMap? + +`LinkedHashMap` 是 Java 集合框架中 `HashMap` 的一个子类,它继承了 `HashMap` 的所有属性和方法,并且在 `HashMap` 的基础重写了 `afterNodeRemoval`、`afterNodeInsertion`、`afterNodeAccess` 方法。使之拥有顺序插入和访问有序的特性。 + +### LinkedHashMap 如何按照插入顺序迭代元素? + +`LinkedHashMap` 按照插入顺序迭代元素是它的默认行为。`LinkedHashMap` 内部维护了一个双向链表,用于记录元素的插入顺序。因此,当使用迭代器迭代元素时,元素的顺序与它们最初插入的顺序相同。 + +### LinkedHashMap 如何按照访问顺序迭代元素? + +`LinkedHashMap` 可以通过构造函数中的 `accessOrder` 参数指定按照访问顺序迭代元素。当 `accessOrder` 为 true 时,每次访问一个元素时,该元素会被移动到链表的末尾,因此下次访问该元素时,它就会成为链表中的最后一个元素,从而实现按照访问顺序迭代元素。 + +### LinkedHashMap 如何实现 LRU 缓存? + +将 `accessOrder` 设置为 true 并重写 `removeEldestEntry` 方法当链表大小超过容量时返回 true,使得每次访问一个元素时,该元素会被移动到链表的末尾。一旦插入操作让 `removeEldestEntry` 返回 true 时,视为缓存已满,`LinkedHashMap` 就会将链表首元素移除,由此我们就能实现一个 LRU 缓存。 + +### LinkedHashMap 和 HashMap 有什么区别? + +`LinkedHashMap` 和 `HashMap` 都是 Java 集合框架中的 Map 接口的实现类。它们的最大区别在于迭代元素的顺序。`HashMap` 迭代元素的顺序是不确定的,而 `LinkedHashMap` 提供了按照插入顺序或访问顺序迭代元素的功能。此外,`LinkedHashMap` 内部维护了一个双向链表,用于记录元素的插入顺序或访问顺序,而 `HashMap` 则没有这个链表。因此,`LinkedHashMap` 的插入性能可能会比 `HashMap` 略低,但它提供了更多的功能并且迭代效率相较于 `HashMap` 更加高效。 + +## 参考文献 + +- LinkedHashMap 源码详细分析(JDK1.8): +- HashMap 与 LinkedHashMap: +- 源于 LinkedHashMap 源码: + diff --git a/docs/java/collection/linkedlist-source-code.md b/docs/java/collection/linkedlist-source-code.md new file mode 100644 index 00000000000..810ee25cd70 --- /dev/null +++ b/docs/java/collection/linkedlist-source-code.md @@ -0,0 +1,518 @@ +--- +title: LinkedList 源码分析 +category: Java +tag: + - Java集合 +--- + + + +## LinkedList 简介 + +`LinkedList` 是一个基于双向链表实现的集合类,经常被拿来和 `ArrayList` 做比较。关于 `LinkedList` 和`ArrayList`的详细对比,我们 [Java 集合常见面试题总结(上)](./java-collection-questions-01.md)有详细介绍到。 + +![双向链表](https://oss.javaguide.cn/github/javaguide/cs-basics/data-structure/bidirectional-linkedlist.png) + +不过,我们在项目中一般是不会使用到 `LinkedList` 的,需要用到 `LinkedList` 的场景几乎都可以使用 `ArrayList` 来代替,并且,性能通常会更好!就连 `LinkedList` 的作者约书亚 · 布洛克(Josh Bloch)自己都说从来不会使用 `LinkedList` 。 + +![](https://oss.javaguide.cn/github/javaguide/redisimage-20220412110853807.png) + +另外,不要下意识地认为 `LinkedList` 作为链表就最适合元素增删的场景。我在上面也说了,`LinkedList` 仅仅在头尾插入或者删除元素的时候时间复杂度近似 O(1),其他情况增删元素的平均时间复杂度都是 O(n) 。 + +### LinkedList 插入和删除元素的时间复杂度? + +- 头部插入/删除:只需要修改头结点的指针即可完成插入/删除操作,因此时间复杂度为 O(1)。 +- 尾部插入/删除:只需要修改尾结点的指针即可完成插入/删除操作,因此时间复杂度为 O(1)。 +- 指定位置插入/删除:需要先移动到指定位置,再修改指定节点的指针完成插入/删除,不过由于有头尾指针,可以从较近的指针出发,因此需要遍历平均 n/4 个元素,时间复杂度为 O(n)。 + +### LinkedList 为什么不能实现 RandomAccess 接口? + +`RandomAccess` 是一个标记接口,用来表明实现该接口的类支持随机访问(即可以通过索引快速访问元素)。由于 `LinkedList` 底层数据结构是链表,内存地址不连续,只能通过指针来定位,不支持随机快速访问,所以不能实现 `RandomAccess` 接口。 + +## LinkedList 源码分析 + +这里以 JDK1.8 为例,分析一下 `LinkedList` 的底层核心源码。 + +`LinkedList` 的类定义如下: + +```java +public class LinkedList + extends AbstractSequentialList + implements List, Deque, Cloneable, java.io.Serializable +{ + //... +} +``` + +`LinkedList` 继承了 `AbstractSequentialList` ,而 `AbstractSequentialList` 又继承于 `AbstractList` 。 + +阅读过 `ArrayList` 的源码我们就知道,`ArrayList` 同样继承了 `AbstractList` , 所以 `LinkedList` 会有大部分方法和 `ArrayList` 相似。 + +`LinkedList` 实现了以下接口: + +- `List` : 表明它是一个列表,支持添加、删除、查找等操作,并且可以通过下标进行访问。 +- `Deque` :继承自 `Queue` 接口,具有双端队列的特性,支持从两端插入和删除元素,方便实现栈和队列等数据结构。需要注意,`Deque` 的发音为 "deck" [dɛk],这个大部分人都会读错。 +- `Cloneable` :表明它具有拷贝能力,可以进行深拷贝或浅拷贝操作。 +- `Serializable` : 表明它可以进行序列化操作,也就是可以将对象转换为字节流进行持久化存储或网络传输,非常方便。 + +![LinkedList 类图](https://oss.javaguide.cn/github/javaguide/java/collection/linkedlist--class-diagram.png) + +`LinkedList` 中的元素是通过 `Node` 定义的: + +```java +private static class Node { + E item;// 节点值 + Node next; // 指向的下一个节点(后继节点) + Node prev; // 指向的前一个节点(前驱结点) + + // 初始化参数顺序分别是:前驱结点、本身节点值、后继节点 + Node(Node prev, E element, Node next) { + this.item = element; + this.next = next; + this.prev = prev; + } +} +``` + +### 初始化 + +`LinkedList` 中有一个无参构造函数和一个有参构造函数。 + +```java +// 创建一个空的链表对象 +public LinkedList() { +} + +// 接收一个集合类型作为参数,会创建一个与传入集合相同元素的链表对象 +public LinkedList(Collection c) { + this(); + addAll(c); +} +``` + +### 插入元素 + +`LinkedList` 除了实现了 `List` 接口相关方法,还实现了 `Deque` 接口的很多方法,所以我们有很多种方式插入元素。 + +我们这里以 `List` 接口中相关的插入方法为例进行源码讲解,对应的是`add()` 方法。 + +`add()` 方法有两个版本: + +- `add(E e)`:用于在 `LinkedList` 的尾部插入元素,即将新元素作为链表的最后一个元素,时间复杂度为 O(1)。 +- `add(int index, E element)`:用于在指定位置插入元素。这种插入方式需要先移动到指定位置,再修改指定节点的指针完成插入/删除,因此需要移动平均 n/4 个元素,时间复杂度为 O(n)。 + +```java +// 在链表尾部插入元素 +public boolean add(E e) { + linkLast(e); + return true; +} + +// 在链表指定位置插入元素 +public void add(int index, E element) { + // 下标越界检查 + checkPositionIndex(index); + + // 判断 index 是不是链表尾部位置 + if (index == size) + // 如果是就直接调用 linkLast 方法将元素节点插入链表尾部即可 + linkLast(element); + else + // 如果不是则调用 linkBefore 方法将其插入指定元素之前 + linkBefore(element, node(index)); +} + +// 将元素节点插入到链表尾部 +void linkLast(E e) { + // 将最后一个元素赋值(引用传递)给节点 l + final Node l = last; + // 创建节点,并指定节点前驱为链表尾节点 last,后继引用为空 + final Node newNode = new Node<>(l, e, null); + // 将 last 引用指向新节点 + last = newNode; + // 判断尾节点是否为空 + // 如果 l 是null 意味着这是第一次添加元素 + if (l == null) + // 如果是第一次添加,将first赋值为新节点,此时链表只有一个元素 + first = newNode; + else + // 如果不是第一次添加,将新节点赋值给l(添加前的最后一个元素)的next + l.next = newNode; + size++; + modCount++; +} + +// 在指定元素之前插入元素 +void linkBefore(E e, Node succ) { + // assert succ != null;断言 succ不为 null + // 定义一个节点元素保存 succ 的 prev 引用,也就是它的前一节点信息 + final Node pred = succ.prev; + // 初始化节点,并指明前驱和后继节点 + final Node newNode = new Node<>(pred, e, succ); + // 将 succ 节点前驱引用 prev 指向新节点 + succ.prev = newNode; + // 判断前驱节点是否为空,为空表示 succ 是第一个节点 + if (pred == null) + // 新节点成为第一个节点 + first = newNode; + else + // succ 节点前驱的后继引用指向新节点 + pred.next = newNode; + size++; + modCount++; +} +``` + +### 获取元素 + +`LinkedList`获取元素相关的方法一共有 3 个: + +1. `getFirst()`:获取链表的第一个元素。 +2. `getLast()`:获取链表的最后一个元素。 +3. `get(int index)`:获取链表指定位置的元素。 + +```java +// 获取链表的第一个元素 +public E getFirst() { + final Node f = first; + if (f == null) + throw new NoSuchElementException(); + return f.item; +} + +// 获取链表的最后一个元素 +public E getLast() { + final Node l = last; + if (l == null) + throw new NoSuchElementException(); + return l.item; +} + +// 获取链表指定位置的元素 +public E get(int index) { + // 下标越界检查,如果越界就抛异常 + checkElementIndex(index); + // 返回链表中对应下标的元素 + return node(index).item; +} +``` + +这里的核心在于 `node(int index)` 这个方法: + +```java +// 返回指定下标的非空节点 +Node node(int index) { + // 断言下标未越界 + // assert isElementIndex(index); + // 如果index小于size的二分之一 从前开始查找(向后查找) 反之向前查找 + if (index < (size >> 1)) { + Node x = first; + // 遍历,循环向后查找,直至 i == index + for (int i = 0; i < index; i++) + x = x.next; + return x; + } else { + Node x = last; + for (int i = size - 1; i > index; i--) + x = x.prev; + return x; + } +} +``` + +`get(int index)` 或 `remove(int index)` 等方法内部都调用了该方法来获取对应的节点。 + +从这个方法的源码可以看出,该方法通过比较索引值与链表 size 的一半大小来确定从链表头还是尾开始遍历。如果索引值小于 size 的一半,就从链表头开始遍历,反之从链表尾开始遍历。这样可以在较短的时间内找到目标节点,充分利用了双向链表的特性来提高效率。 + +### 删除元素 + +`LinkedList`删除元素相关的方法一共有 5 个: + +1. `removeFirst()`:删除并返回链表的第一个元素。 +2. `removeLast()`:删除并返回链表的最后一个元素。 +3. `remove(E e)`:删除链表中首次出现的指定元素,如果不存在该元素则返回 false。 +4. `remove(int index)`:删除指定索引处的元素,并返回该元素的值。 +5. `void clear()`:移除此链表中的所有元素。 + +```java +// 删除并返回链表的第一个元素 +public E removeFirst() { + final Node f = first; + if (f == null) + throw new NoSuchElementException(); + return unlinkFirst(f); +} + +// 删除并返回链表的最后一个元素 +public E removeLast() { + final Node l = last; + if (l == null) + throw new NoSuchElementException(); + return unlinkLast(l); +} + +// 删除链表中首次出现的指定元素,如果不存在该元素则返回 false +public boolean remove(Object o) { + // 如果指定元素为 null,遍历链表找到第一个为 null 的元素进行删除 + if (o == null) { + for (Node x = first; x != null; x = x.next) { + if (x.item == null) { + unlink(x); + return true; + } + } + } else { + // 如果不为 null ,遍历链表找到要删除的节点 + for (Node x = first; x != null; x = x.next) { + if (o.equals(x.item)) { + unlink(x); + return true; + } + } + } + return false; +} + +// 删除链表指定位置的元素 +public E remove(int index) { + // 下标越界检查,如果越界就抛异常 + checkElementIndex(index); + return unlink(node(index)); +} +``` + +这里的核心在于 `unlink(Node x)` 这个方法: + +```java +E unlink(Node x) { + // 断言 x 不为 null + // assert x != null; + // 获取当前节点(也就是待删除节点)的元素 + final E element = x.item; + // 获取当前节点的下一个节点 + final Node next = x.next; + // 获取当前节点的前一个节点 + final Node prev = x.prev; + + // 如果前一个节点为空,则说明当前节点是头节点 + if (prev == null) { + // 直接让链表头指向当前节点的下一个节点 + first = next; + } else { // 如果前一个节点不为空 + // 将前一个节点的 next 指针指向当前节点的下一个节点 + prev.next = next; + // 将当前节点的 prev 指针置为 null,,方便 GC 回收 + x.prev = null; + } + + // 如果下一个节点为空,则说明当前节点是尾节点 + if (next == null) { + // 直接让链表尾指向当前节点的前一个节点 + last = prev; + } else { // 如果下一个节点不为空 + // 将下一个节点的 prev 指针指向当前节点的前一个节点 + next.prev = prev; + // 将当前节点的 next 指针置为 null,方便 GC 回收 + x.next = null; + } + + // 将当前节点元素置为 null,方便 GC 回收 + x.item = null; + size--; + modCount++; + return element; +} +``` + +`unlink()` 方法的逻辑如下: + +1. 首先获取待删除节点 x 的前驱和后继节点; +2. 判断待删除节点是否为头节点或尾节点: + - 如果 x 是头节点,则将 first 指向 x 的后继节点 next + - 如果 x 是尾节点,则将 last 指向 x 的前驱节点 prev + - 如果 x 不是头节点也不是尾节点,执行下一步操作 +3. 将待删除节点 x 的前驱的后继指向待删除节点的后继 next,断开 x 和 x.prev 之间的链接; +4. 将待删除节点 x 的后继的前驱指向待删除节点的前驱 prev,断开 x 和 x.next 之间的链接; +5. 将待删除节点 x 的元素置空,修改链表长度。 + +可以参考下图理解(图源:[LinkedList 源码分析(JDK 1.8)](https://www.tianxiaobo.com/2018/01/31/LinkedList-%E6%BA%90%E7%A0%81%E5%88%86%E6%9E%90-JDK-1-8/)): + +![unlink 方法逻辑](https://oss.javaguide.cn/github/javaguide/java/collection/linkedlist-unlink.jpg) + +### 遍历链表 + +推荐使用`for-each` 循环来遍历 `LinkedList` 中的元素, `for-each` 循环最终会转换成迭代器形式。 + +```java +LinkedList list = new LinkedList<>(); +list.add("apple"); +list.add("banana"); +list.add("pear"); + +for (String fruit : list) { + System.out.println(fruit); +} +``` + +`LinkedList` 的遍历的核心就是它的迭代器的实现。 + +```java +// 双向迭代器 +private class ListItr implements ListIterator { + // 表示上一次调用 next() 或 previous() 方法时经过的节点; + private Node lastReturned; + // 表示下一个要遍历的节点; + private Node next; + // 表示下一个要遍历的节点的下标,也就是当前节点的后继节点的下标; + private int nextIndex; + // 表示当前遍历期望的修改计数值,用于和 LinkedList 的 modCount 比较,判断链表是否被其他线程修改过。 + private int expectedModCount = modCount; + ………… +} +``` + +下面我们对迭代器 `ListItr` 中的核心方法进行详细介绍。 + +我们先来看下从头到尾方向的迭代: + +```java +// 判断还有没有下一个节点 +public boolean hasNext() { + // 判断下一个节点的下标是否小于链表的大小,如果是则表示还有下一个元素可以遍历 + return nextIndex < size; +} +// 获取下一个节点 +public E next() { + // 检查在迭代过程中链表是否被修改过 + checkForComodification(); + // 判断是否还有下一个节点可以遍历,如果没有则抛出 NoSuchElementException 异常 + if (!hasNext()) + throw new NoSuchElementException(); + // 将 lastReturned 指向当前节点 + lastReturned = next; + // 将 next 指向下一个节点 + next = next.next; + nextIndex++; + return lastReturned.item; +} +``` + +再来看一下从尾到头方向的迭代: + +```java +// 判断是否还有前一个节点 +public boolean hasPrevious() { + return nextIndex > 0; +} + +// 获取前一个节点 +public E previous() { + // 检查是否在迭代过程中链表被修改 + checkForComodification(); + // 如果没有前一个节点,则抛出异常 + if (!hasPrevious()) + throw new NoSuchElementException(); + // 将 lastReturned 和 next 指针指向上一个节点 + lastReturned = next = (next == null) ? last : next.prev; + nextIndex--; + return lastReturned.item; +} +``` + +如果需要删除或插入元素,也可以使用迭代器进行操作。 + +```java +LinkedList list = new LinkedList<>(); +list.add("apple"); +list.add(null); +list.add("banana"); + +// Collection 接口的 removeIf 方法底层依然是基于迭代器 +list.removeIf(Objects::isNull); + +for (String fruit : list) { + System.out.println(fruit); +} +``` + +迭代器对应的移除元素的方法如下: + +```java +// 从列表中删除上次被返回的元素 +public void remove() { + // 检查是否在迭代过程中链表被修改 + checkForComodification(); + // 如果上次返回的节点为空,则抛出异常 + if (lastReturned == null) + throw new IllegalStateException(); + + // 获取当前节点的下一个节点 + Node lastNext = lastReturned.next; + // 从链表中删除上次返回的节点 + unlink(lastReturned); + // 修改指针 + if (next == lastReturned) + next = lastNext; + else + nextIndex--; + // 将上次返回的节点引用置为 null,方便 GC 回收 + lastReturned = null; + expectedModCount++; +} +``` + +## LinkedList 常用方法测试 + +代码: + +```java +// 创建 LinkedList 对象 +LinkedList list = new LinkedList<>(); + +// 添加元素到链表末尾 +list.add("apple"); +list.add("banana"); +list.add("pear"); +System.out.println("链表内容:" + list); + +// 在指定位置插入元素 +list.add(1, "orange"); +System.out.println("链表内容:" + list); + +// 获取指定位置的元素 +String fruit = list.get(2); +System.out.println("索引为 2 的元素:" + fruit); + +// 修改指定位置的元素 +list.set(3, "grape"); +System.out.println("链表内容:" + list); + +// 删除指定位置的元素 +list.remove(0); +System.out.println("链表内容:" + list); + +// 删除第一个出现的指定元素 +list.remove("banana"); +System.out.println("链表内容:" + list); + +// 获取链表的长度 +int size = list.size(); +System.out.println("链表长度:" + size); + +// 清空链表 +list.clear(); +System.out.println("清空后的链表:" + list); +``` + +输出: + +```plain +索引为 2 的元素:banana +链表内容:[apple, orange, banana, grape] +链表内容:[orange, banana, grape] +链表内容:[orange, grape] +链表长度:2 +清空后的链表:[] +``` + + diff --git a/docs/java/collection/priorityqueue-source-code.md b/docs/java/collection/priorityqueue-source-code.md new file mode 100644 index 00000000000..b38cae9bcb9 --- /dev/null +++ b/docs/java/collection/priorityqueue-source-code.md @@ -0,0 +1,14 @@ +--- +title: PriorityQueue 源码分析(付费) +category: Java +tag: + - Java集合 +--- + +**PriorityQueue 源码分析** 为我的[知识星球](https://javaguide.cn/about-the-author/zhishixingqiu-two-years.html)(点击链接即可查看详细介绍以及加入方法)专属内容,已经整理到了[《Java 必读源码系列》](https://javaguide.cn/zhuanlan/source-code-reading.html)中。 + +![PriorityQueue 源码分析](https://oss.javaguide.cn/xingqiu/image-20230727084055593.png) + + + + diff --git a/docs/java/concurrent/aqs.md b/docs/java/concurrent/aqs.md index e004b872c94..c8e079d1a51 100644 --- a/docs/java/concurrent/aqs.md +++ b/docs/java/concurrent/aqs.md @@ -5,6 +5,8 @@ tag: - Java并发 --- + + ## AQS 介绍 AQS 的全称为 `AbstractQueuedSynchronizer` ,翻译过来的意思就是抽象队列同步器。这个类在 `java.util.concurrent.locks` 包下面。 @@ -18,31 +20,88 @@ public abstract class AbstractQueuedSynchronizer extends AbstractOwnableSynchron } ``` -AQS 为构建锁和同步器提供了一些通用功能的是实现,因此,使用 AQS 能简单且高效地构造出应用广泛的大量的同步器,比如我们提到的 `ReentrantLock`,`Semaphore`,其他的诸如 `ReentrantReadWriteLock`,`SynchronousQueue`等等皆是基于 AQS 的。 +AQS 为构建锁和同步器提供了一些通用功能的实现。因此,使用 AQS 能简单且高效地构造出应用广泛的大量的同步器,比如我们提到的 `ReentrantLock`,`Semaphore`,其他的诸如 `ReentrantReadWriteLock`,`SynchronousQueue`等等皆是基于 AQS 的。 ## AQS 原理 在面试中被问到并发知识的时候,大多都会被问到“请你说一下自己对于 AQS 原理的理解”。下面给大家一个示例供大家参考,面试不是背题,大家一定要加入自己的思想,即使加入不了自己的思想也要保证自己能够通俗的讲出来而不是背出来。 +### AQS 快速了解 + +在真正讲解 AQS 源码之前,需要对 AQS 有一个整体层面的认识。这里会先通过几个问题,从整体层面上认识 AQS,了解 AQS 在整个 Java 并发中所位于的层面,之后在学习 AQS 源码的过程中,才能更加了解同步器和 AQS 之间的关系。 + +#### AQS 的作用是什么? + +AQS 解决了开发者在实现同步器时的复杂性问题。它提供了一个通用框架,用于实现各种同步器,例如 **可重入锁**(`ReentrantLock`)、**信号量**(`Semaphore`)和 **倒计时器**(`CountDownLatch`)。通过封装底层的线程同步机制,AQS 将复杂的线程管理逻辑隐藏起来,使开发者只需专注于具体的同步逻辑。 + +简单来说,AQS 是一个抽象类,为同步器提供了通用的 **执行框架**。它定义了 **资源获取和释放的通用流程**,而具体的资源获取逻辑则由具体同步器通过重写模板方法来实现。 因此,可以将 AQS 看作是同步器的 **基础“底座”**,而同步器则是基于 AQS 实现的 **具体“应用”**。 + +#### AQS 为什么使用 CLH 锁队列的变体? + +CLH 锁是一种基于 **自旋锁** 的优化实现。 + +先说一下自旋锁存在的问题:自旋锁通过线程不断对一个原子变量执行 `compareAndSet`(简称 `CAS`)操作来尝试获取锁。在高并发场景下,多个线程会同时竞争同一个原子变量,容易造成某个线程的 `CAS` 操作长时间失败,从而导致 **“饥饿”问题**(某些线程可能永远无法获取锁)。 + +CLH 锁通过引入一个队列来组织并发竞争的线程,对自旋锁进行了改进: + +- 每个线程会作为一个节点加入到队列中,并通过自旋监控前一个线程节点的状态,而不是直接竞争共享变量。 +- 线程按顺序排队,确保公平性,从而避免了 “饥饿” 问题。 + +AQS(AbstractQueuedSynchronizer)在 CLH 锁的基础上进一步优化,形成了其内部的 **CLH 队列变体**。主要改进点有以下两方面: + +1. **自旋 + 阻塞**: CLH 锁使用纯自旋方式等待锁的释放,但大量的自旋操作会占用过多的 CPU 资源。AQS 引入了 **自旋 + 阻塞** 的混合机制: + - 如果线程获取锁失败,会先短暂自旋尝试获取锁; + - 如果仍然失败,则线程会进入阻塞状态,等待被唤醒,从而减少 CPU 的浪费。 +2. **单向队列改为双向队列**:CLH 锁使用单向队列,节点只知道前驱节点的状态,而当某个节点释放锁时,需要通过队列唤醒后续节点。AQS 将队列改为 **双向队列**,新增了 `next` 指针,使得节点不仅知道前驱节点,也可以直接唤醒后继节点,从而简化了队列操作,提高了唤醒效率。 + +#### AQS 的性能比较好,原因是什么? + +因为 AQS 内部大量使用了 `CAS` 操作。 + +AQS 内部通过队列来存储等待的线程节点。由于队列是共享资源,在多线程场景下,需要保证队列的同步访问。 + +AQS 内部通过 `CAS` 操作来控制队列的同步访问,`CAS` 操作主要用于控制 `队列初始化` 、 `线程节点入队` 两个操作的并发安全。虽然利用 `CAS` 控制并发安全可以保证比较好的性能,但同时会带来比较高的 **编码复杂度** 。 + +#### AQS 中为什么 Node 节点需要不同的状态? + +AQS 中的 `waitStatus` 状态类似于 **状态机** ,通过不同状态来表明 Node 节点的不同含义,并且根据不同操作,来控制状态之间的流转。 + +- 状态 `0` :新节点加入队列之后,初始状态为 `0` 。 + +- 状态 `SIGNAL` :当有新的节点加入队列,此时新节点的前继节点状态就会由 `0` 更新为 `SIGNAL` ,表示前继节点释放锁之后,需要对新节点进行唤醒操作。如果唤醒 `SIGNAL` 状态节点的后续节点,就会将 `SIGNAL` 状态更新为 `0` 。即通过清除 `SIGNAL` 状态,表示已经执行了唤醒操作。 + +- 状态 `CANCELLED` :如果一个节点在队列中等待获取锁锁时,因为某种原因失败了,该节点的状态就会变为 `CANCELLED` ,表明取消获取锁,这种状态的节点是异常的,无法被唤醒,也无法唤醒后继节点。 + ### AQS 核心思想 -AQS 核心思想是,如果被请求的共享资源空闲,则将当前请求资源的线程设置为有效的工作线程,并且将共享资源设置为锁定状态。如果被请求的共享资源被占用,那么就需要一套线程阻塞等待以及被唤醒时锁分配的机制,这个机制 AQS 是基于 **CLH 锁** (Craig, Landin, and Hagersten locks) 实现的。 +AQS 核心思想是,如果被请求的共享资源空闲,则将当前请求资源的线程设置为有效的工作线程,并且将共享资源设置为锁定状态。如果被请求的共享资源被占用,那么就需要一套线程阻塞等待以及被唤醒时锁分配的机制,这个机制 AQS 是基于 **CLH 锁** (Craig, Landin, and Hagersten locks) 进一步优化实现的。 + +**CLH 锁** 对自旋锁进行了改进,是基于单链表的自旋锁。在多线程场景下,会将请求获取锁的线程组织成一个单向队列,每个等待的线程会通过自旋访问前一个线程节点的状态,前一个节点释放锁之后,当前节点才可以获取锁。**CLH 锁** 的队列结构如下图所示。 + +![CLH 锁的队列结构](https://oss.javaguide.cn/github/javaguide/open-source-project/clh-lock-queue-structure.png) + +AQS 中使用的 **等待队列** 是 CLH 锁队列的变体(接下来简称为 CLH 变体队列)。 + +AQS 的 CLH 变体队列是一个双向队列,会暂时获取不到锁的线程将被加入到该队列中,CLH 变体队列和原本的 CLH 锁队列的区别主要有两点: -CLH 锁是对自旋锁的一种改进,是一个虚拟的双向队列(虚拟的双向队列即不存在队列实例,仅存在结点之间的关联关系),暂时获取不到锁的线程将被加入到该队列中。AQS 将每条请求共享资源的线程封装成一个 CLH 队列锁的一个结点(Node)来实现锁的分配。在 CLH 队列锁中,一个节点表示一个线程,它保存着线程的引用(thread)、 当前节点在队列中的状态(waitStatus)、前驱节点(prev)、后继节点(next)。 +- 由 **自旋** 优化为 **自旋 + 阻塞** :自旋操作的性能很高,但大量的自旋操作比较占用 CPU 资源,因此在 CLH 变体队列中会先通过自旋尝试获取锁,如果失败再进行阻塞等待。 +- 由 **单向队列** 优化为 **双向队列** :在 CLH 变体队列中,会对等待的线程进行阻塞操作,当队列前边的线程释放锁之后,需要对后边的线程进行唤醒,因此增加了 `next` 指针,成为了双向队列。 -CLH 队列锁结构如下图所示: +AQS 将每条请求共享资源的线程封装成一个 CLH 变体队列的一个结点(Node)来实现锁的分配。在 CLH 变体队列中,一个节点表示一个线程,它保存着线程的引用(thread)、 当前节点在队列中的状态(waitStatus)、前驱节点(prev)、后继节点(next)。 -![](https://p3-juejin.byteimg.com/tos-cn-i-k3u1fbpfcp/40cb932a64694262993907ebda6a0bfe~tplv-k3u1fbpfcp-zoom-1.image) +AQS 中的 CLH 变体队列结构如下图所示: + +![CLH 变体队列结构](https://oss.javaguide.cn/github/javaguide/java/concurrent/clh-queue-structure-bianti.png) 关于 AQS 核心数据结构-CLH 锁的详细解读,强烈推荐阅读 [Java AQS 核心数据结构-CLH 锁 - Qunar 技术沙龙](https://mp.weixin.qq.com/s/jEx-4XhNGOFdCo4Nou5tqg) 这篇文章。 -AQS(`AbstractQueuedSynchronizer`)的核心原理图(图源[Java 并发之 AQS 详解](https://www.cnblogs.com/waterystone/p/4920797.html))如下: +AQS(`AbstractQueuedSynchronizer`)的核心原理图: -![](https://oss.javaguide.cn/github/javaguide/CLH.png) +![CLH 变体队列](https://oss.javaguide.cn/github/javaguide/java/concurrent/clh-queue-state.png) -AQS 使用 **int 成员变量 `state` 表示同步状态**,通过内置的 **线程等待队列** 来完成获取资源线程的排队工作。 +AQS 使用 **int 成员变量 `state` 表示同步状态**,通过内置的 **FIFO 线程等待/等待队列** 来完成获取资源线程的排队工作。 -`state` 变量由 `volatile` 修饰,用于展示当前临界资源的获锁情况。 +`state` 变量由 `volatile` 修饰,用于展示当前临界资源的获取情况。 ```java // 共享变量,使用volatile修饰保证线程可见性 @@ -66,24 +125,47 @@ protected final boolean compareAndSetState(int expect, int update) { } ``` -以 `ReentrantLock` 为例,`state` 初始值为 0,表示未锁定状态。A 线程 `lock()` 时,会调用 `tryAcquire()` 独占该锁并将 `state+1` 。此后,其他线程再 `tryAcquire()` 时就会失败,直到 A 线程 `unlock()` 到 `state=`0(即释放锁)为止,其它线程才有机会获取该锁。当然,释放锁之前,A 线程自己是可以重复获取此锁的(`state` 会累加),这就是可重入的概念。但要注意,获取多少次就要释放多少次,这样才能保证 state 是能回到零态的。相关阅读:[从 ReentrantLock 的实现看 AQS 的原理及应用 - 美团技术团队](./reentrantlock.md)。 +以可重入的互斥锁 `ReentrantLock` 为例,它的内部维护了一个 `state` 变量,用来表示锁的占用状态。`state` 的初始值为 0,表示锁处于未锁定状态。当线程 A 调用 `lock()` 方法时,会尝试通过 `tryAcquire()` 方法独占该锁,并让 `state` 的值加 1。如果成功了,那么线程 A 就获取到了锁。如果失败了,那么线程 A 就会被加入到一个等待队列(CLH 变体队列)中,直到其他线程释放该锁。假设线程 A 获取锁成功了,释放锁之前,A 线程自己是可以重复获取此锁的(`state` 会累加)。这就是可重入性的体现:一个线程可以多次获取同一个锁而不会被阻塞。但是,这也意味着,一个线程必须释放与获取的次数相同的锁,才能让 `state` 的值回到 0,也就是让锁恢复到未锁定状态。只有这样,其他等待的线程才能有机会获取该锁。 -再以 `CountDownLatch` 以例,任务分为 N 个子线程去执行,`state` 也初始化为 N(注意 N 要与线程个数一致)。这 N 个子线程是并行执行的,每个子线程执行完后`countDown()` 一次,state 会 CAS(Compare and Swap) 减 1。等到所有子线程都执行完后(即 `state=0` ),会 `unpark()` 主调用线程,然后主调用线程就会从 `await()` 函数返回,继续后余动作。 +线程 A 尝试获取锁的过程如下图所示(图源[从 ReentrantLock 的实现看 AQS 的原理及应用 - 美团技术团队](./reentrantlock.md)): -### AQS 资源共享方式 +![AQS 独占模式获取锁](https://oss.javaguide.cn/github/javaguide/java/concurrent/aqs-exclusive-mode-acquire-lock.png) -AQS 定义两种资源共享方式:`Exclusive`(独占,只有一个线程能执行,如`ReentrantLock`)和`Share`(共享,多个线程可同时执行,如`Semaphore`/`CountDownLatch`)。 +再以倒计时器 `CountDownLatch` 以例,任务分为 N 个子线程去执行,`state` 也初始化为 N(注意 N 要与线程个数一致)。这 N 个子线程开始执行任务,每执行完一个子线程,就调用一次 `countDown()` 方法。该方法会尝试使用 CAS(Compare and Swap) 操作,让 `state` 的值减少 1。当所有的子线程都执行完毕后(即 `state` 的值变为 0),`CountDownLatch` 会调用 `unpark()` 方法,唤醒主线程。这时,主线程就可以从 `await()` 方法(`CountDownLatch` 中的`await()` 方法而非 AQS 中的)返回,继续执行后续的操作。 -一般来说,自定义同步器的共享方式要么是独占,要么是共享,他们也只需实现`tryAcquire-tryRelease`、`tryAcquireShared-tryReleaseShared`中的一种即可。但 AQS 也支持自定义同步器同时实现独占和共享两种方式,如`ReentrantReadWriteLock`。 +### Node 节点 waitStatus 状态含义 -### 自定义同步器 +AQS 中的 `waitStatus` 状态类似于 **状态机** ,通过不同状态来表明 Node 节点的不同含义,并且根据不同操作,来控制状态之间的流转。 + +| Node 节点状态 | 值 | 含义 | +| ------------- | --- | ------------------------------------------------------------------------------------------------------------------------- | +| `CANCELLED` | 1 | 表示线程已经取消获取锁。线程在等待获取资源时被中断、等待资源超时会更新为该状态。 | +| `SIGNAL` | -1 | 表示后继节点需要当前节点唤醒。在当前线程节点释放锁之后,需要对后继节点进行唤醒。 | +| `CONDITION` | -2 | 表示节点在等待 Condition。当其他线程调用了 Condition 的 `signal()` 方法后,节点会从等待队列转移到同步队列中等待获取资源。 | +| `PROPAGATE` | -3 | 用于共享模式。在共享模式下,可能会出现线程在队列中无法被唤醒的情况,因此引入了 `PROPAGATE` 状态来解决这个问题。 | +| | 0 | 加入队列的新节点的初始状态。 | + +在 AQS 的源码中,经常使用 `> 0` 、 `< 0` 来对 `waitStatus` 进行判断。 -同步器的设计是基于模板方法模式的,如果需要自定义同步器一般的方式是这样(模板方法模式很经典的一个应用): +如果 `waitStatus > 0` ,表明节点的状态已经取消等待获取资源。 -1. 使用者继承 `AbstractQueuedSynchronizer` 并重写指定的方法。 -2. 将 AQS 组合在自定义同步组件的实现中,并调用其模板方法,而这些模板方法会调用使用者重写的方法。 +如果 `waitStatus < 0` ,表明节点的状态处于正常的状态,即没有取消等待。 -这和我们以往通过实现接口的方式有很大区别,这是模板方法模式很经典的一个运用。 +其中 `SIGNAL` 状态是最重要的,节点状态流转以及对应操作如下: + +| 状态流转 | 对应操作 | +| ---------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------- | +| `0` | 新节点入队时,初始状态为 `0` 。 | +| `0 -> SIGNAL` | 新节点入队时,它的前继节点状态会由 `0` 更新为 `SIGNAL` 。`SIGNAL` 状态表明该节点的后续节点需要被唤醒。 | +| `SIGNAL -> 0` | 在唤醒后继节点时,需要清除当前节点的状态。通常发生在 `head` 节点,比如 `head` 节点的状态由 `SIGNAL` 更新为 `0` ,表示已经对 `head` 节点的后继节点唤醒了。 | +| `0 -> PROPAGATE` | AQS 内部引入了 `PROPAGATE` 状态,为了解决并发场景下,可能造成的线程节点无法唤醒的情况。(在 AQS 共享模式获取资源的源码分析会讲到) | + +### 自定义同步器 + +基于 AQS 可以实现自定义的同步器, AQS 提供了 5 个模板方法(模板方法模式)。如果需要自定义同步器一般的方式是这样(模板方法模式很经典的一个应用): + +1. 自定义的同步器继承 `AbstractQueuedSynchronizer` 。 +2. 重写 AQS 暴露的模板方法。 **AQS 使用了模板方法模式,自定义同步器时需要重写下面几个 AQS 提供的钩子方法:** @@ -106,6 +188,742 @@ protected boolean isHeldExclusively() 除了上面提到的钩子方法之外,AQS 类中的其他方法都是 `final` ,所以无法被其他类重写。 +### AQS 资源共享方式 + +AQS 定义两种资源共享方式:`Exclusive`(独占,只有一个线程能执行,如`ReentrantLock`)和`Share`(共享,多个线程可同时执行,如`Semaphore`/`CountDownLatch`)。 + +一般来说,自定义同步器的共享方式要么是独占,要么是共享,他们也只需实现`tryAcquire-tryRelease`、`tryAcquireShared-tryReleaseShared`中的一种即可。但 AQS 也支持自定义同步器同时实现独占和共享两种方式,如`ReentrantReadWriteLock`。 + +### AQS 资源获取源码分析(独占模式) + +AQS 中以独占模式获取资源的入口方法是 `acquire()` ,如下: + +```JAVA +// AQS +public final void acquire(int arg) { + if (!tryAcquire(arg) && + acquireQueued(addWaiter(Node.EXCLUSIVE), arg)) + selfInterrupt(); +} +``` + +在 `acquire()` 中,线程会先尝试获取共享资源;如果获取失败,会将线程封装为 Node 节点加入到 AQS 的等待队列中;加入队列之后,会让等待队列中的线程尝试获取资源,并且会对线程进行阻塞操作。分别对应以下三个方法: + +- `tryAcquire()` :尝试获取锁(模板方法),`AQS` 不提供具体实现,由子类实现。 +- `addWaiter()` :如果获取锁失败,会将当前线程封装为 Node 节点加入到 AQS 的 CLH 变体队列中等待获取锁。 +- `acquireQueued()` :对线程进行阻塞,并调用 `tryAcquire()` 方法让队列中的线程尝试获取锁。 + +#### `tryAcquire()` 分析 + +AQS 中对应的 `tryAcquire()` 模板方法如下: + +```JAVA +// AQS +protected boolean tryAcquire(int arg) { + throw new UnsupportedOperationException(); +} +``` + +`tryAcquire()` 方法是 AQS 提供的模板方法,不提供默认实现。 + +因此,这里分析 `tryAcquire()` 方法时,以 `ReentrantLock` 的非公平锁(独占锁)为例进行分析,`ReentrantLock` 内部实现的 `tryAcquire()` 会调用到下边的 `nonfairTryAcquire()` : + +```JAVA +// ReentrantLock +final boolean nonfairTryAcquire(int acquires) { + final Thread current = Thread.currentThread(); + // 1、获取 AQS 中的 state 状态 + int c = getState(); + // 2、如果 state 为 0,证明锁没有被其他线程占用 + if (c == 0) { + // 2.1、通过 CAS 对 state 进行更新 + if (compareAndSetState(0, acquires)) { + // 2.2、如果 CAS 更新成功,就将锁的持有者设置为当前线程 + setExclusiveOwnerThread(current); + return true; + } + } + // 3、如果当前线程和锁的持有线程相同,说明发生了「锁的重入」 + else if (current == getExclusiveOwnerThread()) { + int nextc = c + acquires; + if (nextc < 0) // overflow + throw new Error("Maximum lock count exceeded"); + // 3.1、将锁的重入次数加 1 + setState(nextc); + return true; + } + // 4、如果锁被其他线程占用,就返回 false,表示获取锁失败 + return false; +} +``` + +在 `nonfairTryAcquire()` 方法内部,主要通过两个核心操作去完成资源的获取: + +- 通过 `CAS` 更新 `state` 变量。`state == 0` 表示资源没有被占用。`state > 0` 表示资源被占用,此时 `state` 表示重入次数。 +- 通过 `setExclusiveOwnerThread()` 设置持有资源的线程。 + +如果线程更新 `state` 变量成功,就表明获取到了资源, 因此将持有资源的线程设置为当前线程即可。 + +#### `addWaiter()` 分析 + +在通过 `tryAcquire()` 方法尝试获取资源失败之后,会调用 `addWaiter()` 方法将当前线程封装为 Node 节点加入 `AQS` 内部的队列中。`addWaite()` 代码如下: + +```JAVA +// AQS +private Node addWaiter(Node mode) { + // 1、将当前线程封装为 Node 节点。 + Node node = new Node(Thread.currentThread(), mode); + Node pred = tail; + // 2、如果 pred != null,则证明 tail 节点已经被初始化,直接将 Node 节点加入队列即可。 + if (pred != null) { + node.prev = pred; + // 2.1、通过 CAS 控制并发安全。 + if (compareAndSetTail(pred, node)) { + pred.next = node; + return node; + } + } + // 3、初始化队列,并将新创建的 Node 节点加入队列。 + enq(node); + return node; +} +``` + +**节点入队的并发安全:** + +在 `addWaiter()` 方法中,需要执行 Node 节点 **入队** 的操作。由于是在多线程环境下,因此需要通过 `CAS` 操作保证并发安全。 + +通过 `CAS` 操作去更新 `tail` 指针指向新入队的 Node 节点,`CAS` 可以保证只有一个线程会成功修改 `tail` 指针,以此来保证 Node 节点入队时的并发安全。 + +**AQS 内部队列的初始化:** + +在执行 `addWaiter()` 时,如果发现 `pred == null` ,即 `tail` 指针为 null,则证明队列没有初始化,需要调用 `enq()` 方法初始化队列,并将 `Node` 节点加入到初始化后的队列中,代码如下: + +```JAVA +// AQS +private Node enq(final Node node) { + for (;;) { + Node t = tail; + if (t == null) { + // 1、通过 CAS 操作保证队列初始化的并发安全 + if (compareAndSetHead(new Node())) + tail = head; + } else { + // 2、与 addWaiter() 方法中节点入队的操作相同 + node.prev = t; + if (compareAndSetTail(t, node)) { + t.next = node; + return t; + } + } + } +} +``` + +在 `enq()` 方法中初始化队列,在初始化过程中,也需要通过 `CAS` 来保证并发安全。 + +初始化队列总共包含两个步骤:初始化 `head` 节点、`tail` 指向 `head` 节点。 + +**初始化后的队列如下图所示:** + +![](https://oss.javaguide.cn/github/javaguide/java/concurrent/clh-queue-structure-init.png) + +#### `acquireQueued()` 分析 + +为了方便阅读,这里再贴一下 `AQS` 中 `acquire()` 获取资源的代码: + +```JAVA +// AQS +public final void acquire(int arg) { + if (!tryAcquire(arg) && + acquireQueued(addWaiter(Node.EXCLUSIVE), arg)) + selfInterrupt(); +} +``` + +在 `acquire()` 方法中,通过 `addWaiter()` 方法将 `Node` 节点加入队列之后,就会调用 `acquireQueued()` 方法。代码如下: + +```JAVA +// AQS:令队列中的节点尝试获取锁,并且对线程进行阻塞。 +final boolean acquireQueued(final Node node, int arg) { + boolean failed = true; + try { + boolean interrupted = false; + for (;;) { + // 1、尝试获取锁。 + final Node p = node.predecessor(); + if (p == head && tryAcquire(arg)) { + setHead(node); + p.next = null; // help GC + failed = false; + return interrupted; + } + // 2、判断线程是否可以阻塞,如果可以,则阻塞当前线程。 + if (shouldParkAfterFailedAcquire(p, node) && + parkAndCheckInterrupt()) + interrupted = true; + } + } finally { + // 3、如果获取锁失败,就会取消获取锁,将节点状态更新为 CANCELLED。 + if (failed) + cancelAcquire(node); + } +} +``` + +在 `acquireQueued()` 方法中,主要做两件事情: + +- **尝试获取资源:** 当前线程加入队列之后,如果发现前继节点是 `head` 节点,说明当前线程是队列中第一个等待的节点,于是调用 `tryAcquire()` 尝试获取资源。 + +- **阻塞当前线程** :如果尝试获取资源失败,就需要阻塞当前线程,等待被唤醒之后获取资源。 + +**1、尝试获取资源** + +在 `acquireQueued()` 方法中,尝试获取资源总共有 2 个步骤: + +- `p == head` :表明当前节点的前继节点为 `head` 节点。此时当前节点为 AQS 队列中的第一个等待节点。 +- `tryAcquire(arg) == true` :表明当前线程尝试获取资源成功。 + +在成功获取资源之后,就需要将当前线程的节点 **从等待队列中移除** 。移除操作为:将当前等待的线程节点设置为 `head` 节点(`head` 节点是虚拟节点,并不参与排队获取资源)。 + +**2、阻塞当前线程** + +在 `AQS` 中,当前节点的唤醒需要依赖于上一个节点。如果上一个节点取消获取锁,它的状态就会变为 `CANCELLED` ,`CANCELLED` 状态的节点没有获取到锁,也就无法执行解锁操作对当前节点进行唤醒。因此在阻塞当前线程之前,需要跳过 `CANCELLED` 状态的节点。 + +通过 `shouldParkAfterFailedAcquire()` 方法来判断当前线程节点是否可以阻塞,如下: + +```JAVA +// AQS:判断当前线程节点是否可以阻塞。 +private static boolean shouldParkAfterFailedAcquire(Node pred, Node node) { + int ws = pred.waitStatus; + // 1、前继节点状态正常,直接返回 true 即可。 + if (ws == Node.SIGNAL) + return true; + // 2、ws > 0 表示前继节点的状态异常,即为 CANCELLED 状态,需要跳过异常状态的节点。 + if (ws > 0) { + do { + node.prev = pred = pred.prev; + } while (pred.waitStatus > 0); + pred.next = node; + } else { + // 3、如果前继节点的状态不是 SIGNAL,也不是 CANCELLED,就将状态设置为 SIGNAL。 + compareAndSetWaitStatus(pred, ws, Node.SIGNAL); + } + return false; +} +``` + +`shouldParkAfterFailedAcquire()` 方法中的判断逻辑: + +- 如果发现前继节点的状态是 `SIGNAL` ,则可以阻塞当前线程。 +- 如果发现前继节点的状态是 `CANCELLED` ,则需要跳过 `CANCELLED` 状态的节点。 +- 如果发现前继节点的状态不是 `SIGNAL` 和 `CANCELLED` ,表明前继节点的状态处于正常等待资源的状态,因此将前继节点的状态设置为 `SIGNAL` ,表明该前继节点需要对后续节点进行唤醒。 + +当判断当前线程可以阻塞之后,通过调用 `parkAndCheckInterrupt()` 方法来阻塞当前线程。内部使用了 `LockSupport` 来实现阻塞。`LockSupoprt` 底层是基于 `Unsafe` 类来阻塞线程,代码如下: + +```JAVA +// AQS +private final boolean parkAndCheckInterrupt() { + // 1、线程阻塞到这里 + LockSupport.park(this); + // 2、线程被唤醒之后,返回线程中断状态 + return Thread.interrupted(); +} +``` + +**为什么在线程被唤醒之后,要返回线程的中断状态呢?** + +在 `parkAndCheckInterrupt()` 方法中,当执行完 `LockSupport.park(this)` ,线程会被阻塞,代码如下: + +```JAVA +// AQS +private final boolean parkAndCheckInterrupt() { + LockSupport.park(this); + // 线程被唤醒之后,需要返回线程中断状态 + return Thread.interrupted(); +} +``` + +当线程被唤醒之后,需要执行 `Thread.interrupted()` 来返回线程的中断状态,这是为什么呢? + +这个和线程的中断协作机制有关系,线程被唤醒之后,并不确定是被中断唤醒,还是被 `LockSupport.unpark()` 唤醒,因此需要通过线程的中断状态来判断。 + +**在 `acquire()` 方法中,为什么需要调用 `selfInterrupt()` ?** + +`acquire()` 方法代码如下: + +```JAVA +// AQS +public final void acquire(int arg) { + if (!tryAcquire(arg) && + acquireQueued(addWaiter(Node.EXCLUSIVE), arg)) + selfInterrupt(); +} +``` + +在 `acquire()` 方法中,当 `if` 语句的条件返回 `true` 后,就会调用 `selfInterrupt()` ,该方法会中断当前线程,为什么需要中断当前线程呢? + +当 `if` 判断为 `true` 时,需要 `tryAcquire()` 返回 `false` ,并且 `acquireQueued()` 返回 `true` 。 + +其中 `acquireQueued()` 方法返回的是线程被唤醒之后的 **中断状态** ,通过执行 `Thread.interrupted()` 来返回。该方法在返回中断状态的同时,会清除线程的中断状态。 + +因此如果 `if` 判断为 `true` ,表明线程的中断状态为 `true` ,但是调用 `Thread.interrupted()` 之后,线程的中断状态被清除为 `false` ,因此需要重新执行 `selfInterrupt()` 来重新设置线程的中断状态。 + +### AQS 资源释放源码分析(独占模式) + +AQS 中以独占模式释放资源的入口方法是 `release()` ,代码如下: + +```JAVA +// AQS +public final boolean release(int arg) { + // 1、尝试释放锁 + if (tryRelease(arg)) { + Node h = head; + // 2、唤醒后继节点 + if (h != null && h.waitStatus != 0) + unparkSuccessor(h); + return true; + } + return false; +} +``` + +在 `release()` 方法中,主要做两件事:尝试释放锁和唤醒后继节点。对应方法如下: + +**1、尝试释放锁** + +通过 `tryRelease()` 方法尝试释放锁,该方法为模板方法,由自定义同步器实现,因此这里仍然以 `ReentrantLock` 为例来讲解。 + +`ReentrantLock` 中实现的 `tryRelease()` 方法如下: + +```JAVA +// ReentrantLock +protected final boolean tryRelease(int releases) { + int c = getState() - releases; + // 1、判断持有锁的线程是否为当前线程 + if (Thread.currentThread() != getExclusiveOwnerThread()) + throw new IllegalMonitorStateException(); + boolean free = false; + // 2、如果 state 为 0,则表明当前线程已经没有重入次数。因此将 free 更新为 true,表明该线程会释放锁。 + if (c == 0) { + free = true; + // 3、更新持有资源的线程为 null + setExclusiveOwnerThread(null); + } + // 4、更新 state 值 + setState(c); + return free; +} +``` + +在 `tryRelease()` 方法中,会先计算释放锁之后的 `state` 值,判断 `state` 值是否为 0。 + +- 如果 `state == 0` ,表明该线程没有重入次数了,更新 `free = true` ,并修改持有资源的线程为 null,表明该线程完全释放这把锁。 +- 如果 `state != 0` ,表明该线程还存在重入次数,因此不更新 `free` 值,`free` 值为 `false` 表明该线程没有完全释放这把锁。 + +之后更新 `state` 值,并返回 `free` 值,`free` 值表明线程是否完全释放锁。 + +**2、唤醒后继节点** + +如果 `tryRelease()` 返回 `true` ,表明线程已经没有重入次数了,锁已经被完全释放,因此需要唤醒后继节点。 + +在唤醒后继节点之前,需要判断是否可以唤醒后继节点,判断条件为: `h != null && h.waitStatus != 0` 。这里解释一下为什么要这样判断: + +- `h == null` :表明 `head` 节点还没有被初始化,也就是 AQS 中的队列没有被初始化,因此无法唤醒队列中的线程节点。 +- `h != null && h.waitStatus == 0` :表明头节点刚刚初始化完毕(节点的初始化状态为 0),后继节点线程还没有成功入队,因此不需要对后续节点进行唤醒。(当后继节点入队之后,会将前继节点的状态修改为 `SIGNAL` ,表明需要对后继节点进行唤醒) +- `h != null && h.waitStatus != 0` :其中 `waitStatus` 有可能大于 0,也有可能小于 0。其中 `> 0` 表明节点已经取消等待获取资源,`< 0` 表明节点处于正常等待状态。 + +接下来进入 `unparkSuccessor()` 方法查看如何唤醒后继节点: + +```JAVA +// AQS:这里的入参 node 为队列的头节点(虚拟头节点) +private void unparkSuccessor(Node node) { + int ws = node.waitStatus; + // 1、将头节点的状态进行清除,为后续的唤醒做准备。 + if (ws < 0) + compareAndSetWaitStatus(node, ws, 0); + + Node s = node.next; + // 2、如果后继节点异常,则需要从 tail 向前遍历,找到正常状态的节点进行唤醒。 + if (s == null || s.waitStatus > 0) { + s = null; + for (Node t = tail; t != null && t != node; t = t.prev) + if (t.waitStatus <= 0) + s = t; + } + if (s != null) + // 3、唤醒后继节点 + LockSupport.unpark(s.thread); +} +``` + +在 `unparkSuccessor()` 中,如果头节点的状态 `< 0` (在正常情况下,只要有后继节点,头节点的状态应该为 `SIGNAL` ,即 -1),表示需要对后继节点进行唤醒,因此这里提前清除头节点的状态标识,将状态修改为 0,表示已经执行了对后续节点唤醒的操作。 + +如果 `s == null` 或者 `s.waitStatus > 0` ,表明后继节点异常,此时不能唤醒异常节点,而是要找到正常状态的节点进行唤醒。 + +因此需要从 `tail` 指针向前遍历,来找到第一个状态正常(`waitStatus <= 0`)的节点进行唤醒。 + +**为什么要从 `tail` 指针向前遍历,而不是从 `head` 指针向后遍历,寻找正常状态的节点呢?** + +遍历的方向和 **节点的入队操作** 有关。入队方法如下: + +```JAVA +// AQS:节点入队方法 +private Node addWaiter(Node mode) { + Node node = new Node(Thread.currentThread(), mode); + Node pred = tail; + if (pred != null) { + // 1、先修改 prev 指针。 + node.prev = pred; + if (compareAndSetTail(pred, node)) { + // 2、再修改 next 指针。 + pred.next = node; + return node; + } + } + enq(node); + return node; +} +``` + +在 `addWaiter()` 方法中,`node` 节点入队需要修改 `node.prev` 和 `pred.next` 两个指针,但是这两个操作并不是 **原子操作** ,先修改了 `node.prev` 指针,之后才修改 `pred.next` 指针。 + +在极端情况下,可能会出现 `head` 节点的下一个节点状态为 `CANCELLED` ,此时新入队的节点仅更新了 `node.prev` 指针,还未更新 `pred.next` 指针,如下图: + +![](https://oss.javaguide.cn/github/javaguide/java/concurrent/aqs-addWaiter.png) + +这样如果从 `head` 指针向后遍历,无法找到新入队的节点,因此需要从 `tail` 指针向前遍历找到新入队的节点。 + +### 图解 AQS 工作原理(独占模式) + +至此,AQS 中以独占模式获取资源、释放资源的源码就讲完了。为了对 AQS 的工作原理、节点状态变化有一个更加清晰的认识,接下来会通过画图的方式来了解整个 AQS 的工作原理。 + +由于 AQS 是底层同步工具,获取和释放资源的方法并没有提供具体实现,因此这里基于 `ReentrantLock` 来画图进行讲解。 + +假设总共有 3 个线程尝试获取锁,线程分别为 `T1` 、 `T2` 和 `T3` 。 + +此时,假设线程 `T1` 先获取到锁,线程 `T2` 排队等待获取锁。在线程 `T2` 进入队列之前,需要对 AQS 内部队列进行初始化。`head` 节点在初始化后状态为 `0` 。AQS 内部初始化后的队列如下图: + +![](https://oss.javaguide.cn/github/javaguide/java/concurrent/aqs-acquire-and-release-process.png) + +此时,线程 `T2` 尝试获取锁。由于线程 `T1` 持有锁,因此线程 `T2` 会进入队列中等待获取锁。同时会将前继节点( `head` 节点)的状态由 `0` 更新为 `SIGNAL` ,表示需要对 `head` 节点的后继节点进行唤醒。此时,AQS 内部队列如下图所示: + +![](https://oss.javaguide.cn/github/javaguide/java/concurrent/aqs-acquire-and-release-process-2.png) + +此时,线程 `T3` 尝试获取锁。由于线程 `T1` 持有锁,因此线程 `T3` 会进入队列中等待获取锁。同时会将前继节点(线程 `T2` 节点)的状态由 `0` 更新为 `SIGNAL` ,表示线程 `T2` 节点需要对后继节点进行唤醒。此时,AQS 内部队列如下图所示: + +![](https://oss.javaguide.cn/github/javaguide/java/concurrent/aqs-acquire-and-release-process-3.png) + +此时,假设线程 `T1` 释放锁,会唤醒后继节点 `T2` 。线程 `T2` 被唤醒后获取到锁,并且会从等待队列中退出。 + +这里线程 `T2` 节点退出等待队列并不是直接从队列移除,而是令线程 `T2` 节点成为新的 `head` 节点,以此来退出资源获取的等待。此时 AQS 内部队列如下所示: + +![](https://oss.javaguide.cn/github/javaguide/java/concurrent/aqs-acquire-and-release-process-4.png) + +此时,假设线程 `T2` 释放锁,会唤醒后继节点 `T3` 。线程 `T3` 获取到锁之后,同样也退出等待队列,即将线程 `T3` 节点变为 `head` 节点来退出资源获取的等待。此时 AQS 内部队列如下所示: + +![](https://oss.javaguide.cn/github/javaguide/java/concurrent/aqs-acquire-and-release-process-5.png) + +### AQS 资源获取源码分析(共享模式) + +AQS 中以独占模式获取资源的入口方法是 `acquireShared()` ,如下: + +```JAVA +// AQS +public final void acquireShared(int arg) { + if (tryAcquireShared(arg) < 0) + doAcquireShared(arg); +} +``` + +在 `acquireShared()` 方法中,会先尝试获取共享锁,如果获取失败,则将当前线程加入到队列中阻塞,等待唤醒后尝试获取共享锁,分别对应一下两个方法:`tryAcquireShared()` 和 `doAcquireShared()` 。 + +其中 `tryAcquireShared()` 方法是 AQS 提供的模板方法,由同步器来实现具体逻辑。因此这里以 `Semaphore` 为例,来分析共享模式下,如何获取资源。 + +#### `tryAcquireShared()` 分析 + +`Semaphore` 中实现了公平锁和非公平锁,接下来以非公平锁为例来分析 `tryAcquireShared()` 源码。 + +`Semaphore` 中重写的 `tryAcquireShared()` 方法会调用下边的 `nonfairTryAcquireShared()` 方法: + +```JAVA +// Semaphore 重写 AQS 的模板方法 +protected int tryAcquireShared(int acquires) { + return nonfairTryAcquireShared(acquires); +} + +// Semaphore +final int nonfairTryAcquireShared(int acquires) { + for (;;) { + // 1、获取可用资源数量。 + int available = getState(); + // 2、计算剩余资源数量。 + int remaining = available - acquires; + // 3、如果剩余资源数量 < 0,则说明资源不足,直接返回;如果 CAS 更新 state 成功,则说明当前线程获取到了共享资源,直接返回。 + if (remaining < 0 || + compareAndSetState(available, remaining)) + return remaining; + } +} +``` + +在共享模式下,AQS 中的 `state` 值表示共享资源的数量。 + +在 `nonfairTryAcquireShared()` 方法中,会在死循环中不断尝试获取资源,如果 「剩余资源数不足」 或者 「当前线程成功获取资源」 ,就退出死循环。方法返回 **剩余的资源数量** ,根据返回值的不同,分为 3 种情况: + +- **剩余资源数量 > 0** :表示成功获取资源,并且后续的线程也可以成功获取资源。 +- **剩余资源数量 = 0** :表示成功获取资源,但是后续的线程无法成功获取资源。 +- **剩余资源数量 < 0** :表示获取资源失败。 + +#### `doAcquireShared()` 分析 + +为了方便阅读,这里再贴一下获取资源的入口方法 `acquireShared()` : + +```JAVA +// AQS +public final void acquireShared(int arg) { + if (tryAcquireShared(arg) < 0) + doAcquireShared(arg); +} +``` + +在 `acquireShared()` 方法中,会先通过 `tryAcquireShared()` 尝试获取资源。 + +如果发现方法的返回值 `< 0` ,即剩余的资源数小于 0,则表明当前线程获取资源失败。因此会进入 `doAcquireShared()` 方法,将当前线程加入到 AQS 队列进行等待。如下: + +```JAVA +// AQS +private void doAcquireShared(int arg) { + // 1、将当前线程加入到队列中等待。 + final Node node = addWaiter(Node.SHARED); + boolean failed = true; + try { + boolean interrupted = false; + for (;;) { + final Node p = node.predecessor(); + if (p == head) { + // 2、如果当前线程是等待队列的第一个节点,则尝试获取资源。 + int r = tryAcquireShared(arg); + if (r >= 0) { + // 3、将当前线程节点移出等待队列,并唤醒后续线程节点。 + setHeadAndPropagate(node, r); + p.next = null; // help GC + if (interrupted) + selfInterrupt(); + failed = false; + return; + } + } + if (shouldParkAfterFailedAcquire(p, node) && + parkAndCheckInterrupt()) + interrupted = true; + } + } finally { + // 3、如果获取资源失败,就会取消获取资源,将节点状态更新为 CANCELLED。 + if (failed) + cancelAcquire(node); + } +} +``` + +由于当前线程已经尝试获取资源失败了,因此在 `doAcquireShared()` 方法中,需要将当前线程封装为 Node 节点,加入到队列中进行等待。 + +以 **共享模式** 获取资源和 **独占模式** 获取资源最大的不同之处在于:共享模式下,资源的数量可能会大于 1,即可以多个线程同时持有资源。 + +因此在共享模式下,当线程线程被唤醒之后,获取到了资源,如果发现还存在剩余资源,就会尝试唤醒后边的线程去尝试获取资源。对应的 `setHeadAndPropagate()` 方法如下: + +```JAVA +// AQS +private void setHeadAndPropagate(Node node, int propagate) { + Node h = head; + // 1、将当前线程节点移出等待队列。 + setHead(node); + // 2、唤醒后续等待节点。 + if (propagate > 0 || h == null || h.waitStatus < 0 || + (h = head) == null || h.waitStatus < 0) { + Node s = node.next; + if (s == null || s.isShared()) + doReleaseShared(); + } +} +``` + +在 `setHeadAndPropagate()` 方法中,唤醒后续节点需要满足一定的条件,主要需要满足 2 个条件: + +- `propagate > 0` :`propagate` 代表获取资源之后剩余的资源数量,如果 `> 0` ,则可以唤醒后续线程去获取资源。 +- `h.waitStatus < 0` :这里的 `h` 节点是执行 `setHead()` 之前的 `head` 节点。判断 `head.waitStatus` 时使用 `< 0` ,主要为了确定 `head` 节点的状态为 `SIGNAL` 或 `PROPAGATE` 。如果 `head` 节点为 `SIGNAL` ,则可以唤醒后续节点;如果 `head` 节点状态为 `PROPAGATE` ,也可以唤醒后续节点(这是为了解决并发场景下出现的问题,后续会细讲)。 + +代码中关于 **唤醒后续等待节点** 的 `if` 判断稍微复杂一些,这里来讲一下为什么这样写: + +```JAVA +if (propagate > 0 || h == null || h.waitStatus < 0 || + (h = head) == null || h.waitStatus < 0) +``` + +- `h == null || h.waitStatus < 0` : `h == null` 用于防止空指针异常。正常情况下 h 不会为 `null` ,因为执行到这里之前,当前节点已经加入到队列中了,队列不可能还没有初始化。 + + `h.waitStatus < 0` 主要判断 `head` 节点的状态是否为 `SIGNAL` 或者 `PROPAGATE` ,直接使用 `< 0` 来判断比较方便。 + +- `(h = head) == null || h.waitStatus < 0` :如果到这里说明之前判断的 `h.waitStatus < 0` ,说明存在并发。 + + 同时存在其他线程在唤醒后续节点,已经将 `head` 节点的值由 `SIGNAL` 修改为 `0` 了。因此,这里重新获取新的 `head` 节点,这次获取的 `head` 节点为通过 `setHead()` 设置的当前线程节点,之后再次判断 `waitStatus` 状态。 + +如果 `if` 条件判断通过,就会走到 `doReleaseShared()` 方法唤醒后续等待节点,如下: + +```JAVA +private void doReleaseShared() { + for (;;) { + Node h = head; + // 1、队列中至少需要一个等待的线程节点。 + if (h != null && h != tail) { + int ws = h.waitStatus; + // 2、如果 head 节点的状态为 SIGNAL,则可以唤醒后继节点。 + if (ws == Node.SIGNAL) { + // 2.1 清除 head 节点的 SIGNAL 状态,更新为 0。表示已经唤醒该节点的后继节点了。 + if (!compareAndSetWaitStatus(h, Node.SIGNAL, 0)) + continue; + // 2.2 唤醒后继节点 + unparkSuccessor(h); + } + // 3、如果 head 节点的状态为 0,则更新为 PROPAGATE。这是为了解决并发场景下存在的问题,接下来会细讲。 + else if (ws == 0 && + !compareAndSetWaitStatus(h, 0, Node.PROPAGATE)) + continue; + } + if (h == head) + break; + } +} +``` + +在 `doReleaseShared()` 方法中,会判断 `head` 节点的 `waitStatus` 状态来决定接下来的操作,有两种情况: + +- `head` 节点的状态为 `SIGNAL` :表明 `head` 节点存在后继节点需要唤醒,因此通过 `CAS` 操作将 `head` 节点的 `SIGNAL` 状态更新为 `0` 。通过清除 `SIGNAL` 状态来表示已经对 `head` 节点的后继节点进行唤醒操作了。 +- `head` 节点的状态为 `0` :表明存在并发情况,需要将 `0` 修改为 `PROPAGATE` 来保证在并发场景下可以正常唤醒线程。 + +#### 为什么需要 `PROPAGATE` 状态? + +在 `doReleaseShared()` 释放资源时,第 3 步不太容易理解,即如果发现 `head` 节点的状态是 `0` ,就将 `head` 节点的状态由 `0` 更新为 `PROPAGATE` 。 + +AQS 中,Node 节点的 `PROPAGATE` 就是为了处理并发场景下可能出现的无法唤醒线程节点的问题。`PROPAGATE` 只在 `doReleaseShared()` 方法中用到一次。 + +**接下来通过案例分析,为什么需要 `PROPAGATE` 状态?** + +在共享模式下,线程获取和释放资源的方法调用链如下: + +- 线程获取资源的方法调用链为: `acquireShared() -> tryAcquireShared() -> 线程阻塞等待唤醒 -> tryAcquireShared() -> setHeadAndPropagate() -> if (剩余资源数 > 0) || (head.waitStatus < 0) 则唤醒后续节点` 。 + +- 线程释放资源的方法调用链为: `releaseShared() -> tryReleaseShared() -> doReleaseShared()` 。 + +**如果在释放资源时,没有将 `head` 节点的状态由 `0` 改为 `PROPAGATE` :** + +假设总共有 4 个线程尝试以共享模式获取资源,总共有 2 个资源。初始 `T3` 和 `T4` 线程获取到了资源,`T1` 和 `T2` 线程没有获取到,因此在队列中排队等候。 + +- 在时刻 1 时,线程 `T1` 和 `T2` 在等待队列中,`T3` 和 `T4` 持有资源。此时等待队列内节点以及对应状态为(括号内为节点的 `waitStatus` 状态): + + `head(-1) -> T1(-1) -> T2(0)` 。 + +- 在时刻 2 时,线程 `T3` 释放资源,通过 `doReleaseShared()` 方法将 `head` 节点的状态由 `SIGNAL` 更新为 `0` ,并唤醒线程 `T1` ,之后线程 `T3` 退出。 + + 线程 `T1` 被唤醒之后,通过 `tryAcquireShared()` 获取到资源,但是此时还未来得及执行 `setHeadAndPropagate()` 将自己设置为 `head` 节点。此时等待队列内节点状态为: + + `head(0) -> T1(-1) -> T2(0)` 。 + +- 在时刻 3 时,线程 `T4` 释放资源, 由于此时 `head` 节点的状态为 `0` ,因此在 `doReleaseShared()` 方法中无法唤醒 `head` 的后继节点, 之后线程 `T4` 退出。 + +- 在时刻 4 时,线程 `T1` 继续执行 `setHeadAndPropagate()` 方法将自己设置为 `head` 节点。 + + 但是此时由于线程 `T1` 执行 `tryAcquireShared()` 方法返回的剩余资源数为 `0` ,并且 `head` 节点的状态为 `0` ,因此线程 `T1` 并不会在 `setHeadAndPropagate()` 方法中唤醒后续节点。此时等待队列内节点状态为: + + `head(-1,线程 T1 节点) -> T2(0)` 。 + +此时,就导致线程 `T2` 节点在等待队列中,无法被唤醒。对应时刻表如下: + +| 时刻 | 线程 T1 | 线程 T2 | 线程 T3 | 线程 T4 | 等待队列 | +| ------ | -------------------------------------------------------------- | -------- | ---------------- | ------------------------------------------------------------- | --------------------------------- | +| 时刻 1 | 等待队列 | 等待队列 | 持有资源 | 持有资源 | `head(-1) -> T1(-1) -> T2(0)` | +| 时刻 2 | (执行)被唤醒后,获取资源,但未来得及将自己设置为 `head` 节点 | 等待队列 | (执行)释放资源 | 持有资源 | `head(0) -> T1(-1) -> T2(0)` | +| 时刻 3 | | 等待队列 | 已退出 | (执行)释放资源。但 `head` 节点状态为 `0` ,无法唤醒后继节点 | `head(0) -> T1(-1) -> T2(0)` | +| 时刻 4 | (执行)将自己设置为 `head` 节点 | 等待队列 | 已退出 | 已退出 | `head(-1,线程 T1 节点) -> T2(0)` | + +**如果在线程释放资源时,将 `head` 节点的状态由 `0` 改为 `PROPAGATE` ,则可以解决上边出现的并发问题,如下:** + +- 在时刻 1 时,线程 `T1` 和 `T2` 在等待队列中,`T3` 和 `T4` 持有资源。此时等待队列内节点以及对应状态为: + + `head(-1) -> T1(-1) -> T2(0)` 。 + +- 在时刻 2 时,线程 `T3` 释放资源,通过 `doReleaseShared()` 方法将 `head` 节点的状态由 `SIGNAL` 更新为 `0` ,并唤醒线程 `T1` ,之后线程 `T3` 退出。 + + 线程 `T1` 被唤醒之后,通过 `tryAcquireShared()` 获取到资源,但是此时还未来得及执行 `setHeadAndPropagate()` 将自己设置为 `head` 节点。此时等待队列内节点状态为: + + `head(0) -> T1(-1) -> T2(0)` 。 + +- 在时刻 3 时,线程 `T4` 释放资源, 由于此时 `head` 节点的状态为 `0` ,因此在 `doReleaseShared()` 方法中会将 `head` 节点的状态由 `0` 更新为 `PROPAGATE` , 之后线程 `T4` 退出。此时等待队列内节点状态为: + + `head(PROPAGATE) -> T1(-1) -> T2(0)` 。 + +- 在时刻 4 时,线程 `T1` 继续执行 `setHeadAndPropagate()` 方法将自己设置为 `head` 节点。此时等待队列内节点状态为: + + `head(-1,线程 T1 节点) -> T2(0)` 。 + +- 在时刻 5 时,虽然此时由于线程 `T1` 执行 `tryAcquireShared()` 方法返回的剩余资源数为 `0` ,但是 `head` 节点状态为 `PROPAGATE < 0` (这里的 `head` 节点是老的 `head` 节点,而不是刚成为 `head` 节点的线程 `T1` 节点)。 + + 因此线程 `T1` 会在 `setHeadAndPropagate()` 方法中唤醒后续 `T2` 节点,并将 `head` 节点的状态由 `SIGNAL` 更新为 `0`。此时等待队列内节点状态为: + + `head(0,线程 T1 节点) -> T2(0)` 。 + +- 在时刻 6 时,线程 `T2` 被唤醒后,获取到资源,并将自己设置为 `head` 节点。此时等待队列内节点状态为: + + `head(0,线程 T2 节点)` 。 + +有了 `PROPAGATE` 状态,就可以避免线程 `T2` 无法被唤醒的情况。对应时刻表如下: + +| 时刻 | 线程 T1 | 线程 T2 | 线程 T3 | 线程 T4 | 等待队列 | +| ------ | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ------------------------------------------------------------------ | ---------------- | ------------------------------------------------------------------- | ------------------------------------ | +| 时刻 1 | 等待队列 | 等待队列 | 持有资源 | 持有资源 | `head(-1) -> T1(-1) -> T2(0)` | +| 时刻 2 | (执行)被唤醒后,获取资源,但未来得及将自己设置为 `head` 节点 | 等待队列 | (执行)释放资源 | 持有资源 | `head(0) -> T1(-1) -> T2(0)` | +| 时刻 3 | 未继续向下执行 | 等待队列 | 已退出 | (执行)释放资源。此时会将 `head` 节点状态由 `0` 更新为 `PROPAGATE` | `head(PROPAGATE) -> T1(-1) -> T2(0)` | +| 时刻 4 | (执行)将自己设置为 `head` 节点 | 等待队列 | 已退出 | 已退出 | `head(-1,线程 T1 节点) -> T2(0)` | +| 时刻 5 | (执行)由于 `head` 节点状态为 `PROPAGATE < 0` ,因此会在 `setHeadAndPropagate()` 方法中唤醒后续节点,此时将新的 `head` 节点的状态由 `SIGNAL` 更新为 `0` ,并唤醒线程 `T2` | 等待队列 | 已退出 | 已退出 | `head(0,线程 T1 节点) -> T2(0)` | +| 时刻 6 | 已退出 | (执行)线程 `T2` 被唤醒后,获取到资源,并将自己设置为 `head` 节点 | 已退出 | 已退出 | `head(0,线程 T2 节点)` | + +### AQS 资源释放源码分析(共享模式) + +AQS 中以共享模式释放资源的入口方法是 `releaseShared()` ,代码如下: + +```JAVA +// AQS +public final boolean releaseShared(int arg) { + if (tryReleaseShared(arg)) { + doReleaseShared(); + return true; + } + return false; +} +``` + +其中 `tryReleaseShared()` 方法是 AQS 提供的模板方法,这里同样以 `Semaphore` 来讲解,如下: + +```JAVA +// Semaphore +protected final boolean tryReleaseShared(int releases) { + for (;;) { + int current = getState(); + int next = current + releases; + if (next < current) // overflow + throw new Error("Maximum permit count exceeded"); + if (compareAndSetState(current, next)) + return true; + } +} +``` + +在 `Semaphore` 实现的 `tryReleaseShared()` 方法中,会在死循环内不断尝试释放资源,即通过 `CAS` 操作来更新 `state` 值。 + +如果更新成功,则证明资源释放成功,会进入到 `doReleaseShared()` 方法。 + +`doReleaseShared()` 方法在前文获取资源(共享模式)的部分已进行了详细的源码分析,此处不再重复。 + ## 常见同步工具类 下面介绍几个基于 AQS 的常见同步工具类。 @@ -116,9 +934,9 @@ protected boolean isHeldExclusively() `synchronized` 和 `ReentrantLock` 都是一次只允许一个线程访问某个资源,而`Semaphore`(信号量)可以用来控制同时访问特定资源的线程数量。 -Semaphore 的使用简单,我们这里假设有 N(N>5) 个线程来获取 `Semaphore` 中的共享资源,下面的代码表示同一时刻 N 个线程中只有 5 个线程能获取到共享资源,其他线程都会阻塞,只有获取到共享资源的线程才能执行。等到有线程释放了共享资源,其他阻塞的线程才能获取到。 +`Semaphore` 的使用简单,我们这里假设有 `N(N>5)` 个线程来获取 `Semaphore` 中的共享资源,下面的代码表示同一时刻 N 个线程中只有 5 个线程能获取到共享资源,其他线程都会阻塞,只有获取到共享资源的线程才能执行。等到有线程释放了共享资源,其他阻塞的线程才能获取到。 -```java  +```java // 初始共享资源数量 final Semaphore semaphore = new Semaphore(5); // 获取1个许可 @@ -138,11 +956,11 @@ semaphore.release(); ```java public Semaphore(int permits) { - sync = new NonfairSync(permits); + sync = new NonfairSync(permits); } public Semaphore(int permits, boolean fair) { - sync = fair ? new FairSync(permits) : new NonfairSync(permits); + sync = fair ? new FairSync(permits) : new NonfairSync(permits); } ``` @@ -154,41 +972,86 @@ public Semaphore(int permits, boolean fair) { `Semaphore` 是共享锁的一种实现,它默认构造 AQS 的 `state` 值为 `permits`,你可以将 `permits` 的值理解为许可证的数量,只有拿到许可证的线程才能执行。 -调用`semaphore.acquire()` ,线程尝试获取许可证,如果 `state >= 0` 的话,则表示可以获取成功。如果获取成功的话,使用 CAS 操作去修改 `state` 的值 `state=state-1`。如果 `state<0` 的话,则表示许可证数量不足。此时会创建一个 Node 节点加入阻塞队列,挂起当前线程。 +以无参 `acquire` 方法为例,调用`semaphore.acquire()` ,线程尝试获取许可证,如果 `state > 0` 的话,则表示可以获取成功,如果 `state <= 0` 的话,则表示许可证数量不足,获取失败。 + +如果可以获取成功的话(`state > 0` ),会尝试使用 CAS 操作去修改 `state` 的值 `state=state-1`。如果获取失败则会创建一个 Node 节点加入等待队列,挂起当前线程。 ```java -/** - * 获取1个许可证 - */ +// 获取1个许可证 public void acquire() throws InterruptedException { - sync.acquireSharedInterruptibly(1); + sync.acquireSharedInterruptibly(1); } -/** - * 共享模式下获取许可证,获取成功则返回,失败则加入阻塞队列,挂起线程 - */ + +// 获取一个或者多个许可证 +public void acquire(int permits) throws InterruptedException { + if (permits < 0) throw new IllegalArgumentException(); + sync.acquireSharedInterruptibly(permits); +} +``` + +`acquireSharedInterruptibly`方法是 `AbstractQueuedSynchronizer` 中的默认实现。 + +```java +// 共享模式下获取许可证,获取成功则返回,失败则加入等待队列,挂起线程 public final void acquireSharedInterruptibly(int arg) throws InterruptedException { if (Thread.interrupted()) throw new InterruptedException(); - // 尝试获取许可证,arg为获取许可证个数,当可用许可证数减当前获取的许可证数结果小于0,则创建一个节点加入阻塞队列,挂起当前线程。 + // 尝试获取许可证,arg为获取许可证个数,当获取失败时,则创建一个节点加入等待队列,挂起当前线程。 if (tryAcquireShared(arg) < 0) doAcquireSharedInterruptibly(arg); } ``` -调用`semaphore.release();` ,线程尝试释放许可证,并使用 CAS 操作去修改 `state` 的值 `state=state+1`。释放许可证成功之后,同时会唤醒同步队列中的一个线程。被唤醒的线程会重新尝试去修改 `state` 的值 `state=state-1` ,如果 `state>=0` 则获取令牌成功,否则重新进入阻塞队列,挂起线程。 +这里再以非公平模式(`NonfairSync`)的为例,看看 `tryAcquireShared` 方法的实现。 + +```java +// 共享模式下尝试获取资源(在Semaphore中的资源即许可证): +protected int tryAcquireShared(int acquires) { + return nonfairTryAcquireShared(acquires); +} + +// 非公平的共享模式获取许可证 +final int nonfairTryAcquireShared(int acquires) { + for (;;) { + // 当前可用许可证数量 + int available = getState(); + /* + * 尝试获取许可证,当前可用许可证数量小于等于0时,返回负值,表示获取失败, + * 当前可用许可证大于0时才可能获取成功,CAS失败了会循环重新获取最新的值尝试获取 + */ + int remaining = available - acquires; + if (remaining < 0 || + compareAndSetState(available, remaining)) + return remaining; + } +} +``` + +以无参 `release` 方法为例,调用`semaphore.release();` ,线程尝试释放许可证,并使用 CAS 操作去修改 `state` 的值 `state=state+1`。释放许可证成功之后,同时会唤醒等待队列中的一个线程。被唤醒的线程会重新尝试去修改 `state` 的值 `state=state-1` ,如果 `state > 0` 则获取令牌成功,否则重新进入等待队列,挂起线程。 ```java // 释放一个许可证 public void release() { - sync.releaseShared(1); + sync.releaseShared(1); +} + +// 释放一个或者多个许可证 +public void release(int permits) { + if (permits < 0) throw new IllegalArgumentException(); + sync.releaseShared(permits); } +``` -// 释放共享锁,同时会唤醒同步队列中的一个线程。 +`releaseShared`方法是 `AbstractQueuedSynchronizer` 中的默认实现。 + +```java +// 释放共享锁 +// 如果 tryReleaseShared 返回 true,就唤醒等待队列中的一个或多个线程。 public final boolean releaseShared(int arg) { //释放共享锁 if (tryReleaseShared(arg)) { - //唤醒同步队列中的一个线程 + //释放当前节点的后置等待节点 doReleaseShared(); return true; } @@ -196,16 +1059,43 @@ public final boolean releaseShared(int arg) { } ``` +`tryReleaseShared` 方法是`Semaphore` 的内部类 `Sync` 重写的一个方法, `AbstractQueuedSynchronizer`中的默认实现仅仅抛出 `UnsupportedOperationException` 异常。 + +```java +// 内部类 Sync 中重写的一个方法 +// 尝试释放资源 +protected final boolean tryReleaseShared(int releases) { + for (;;) { + int current = getState(); + // 可用许可证+1 + int next = current + releases; + if (next < current) // overflow + throw new Error("Maximum permit count exceeded"); + // CAS修改state的值 + if (compareAndSetState(current, next)) + return true; + } +} +``` + +可以看到,上面提到的几个方法底层基本都是通过同步器 `sync` 实现的。`Sync` 是 `CountDownLatch` 的内部类 , 继承了 `AbstractQueuedSynchronizer` ,重写了其中的某些方法。并且,Sync 对应的还有两个子类 `NonfairSync`(对应非公平模式) 和 `FairSync`(对应公平模式)。 + +```java +private static final class Sync extends AbstractQueuedSynchronizer { + // ... +} +static final class NonfairSync extends Sync { + // ... +} +static final class FairSync extends Sync { + // ... +} +``` + #### 实战 ```java -/** - * - * @author Snailclimb - * @date 2018年9月30日 - * @Description: 需要一次性拿一个许可的情况 - */ -public class SemaphoreExample1 { +public class SemaphoreExample { // 请求的数量 private static final int threadCount = 550; @@ -255,8 +1145,7 @@ semaphore.release(5);// 释放5个许可 [issue645 补充内容](https://github.com/Snailclimb/JavaGuide/issues/645): -> `Semaphore` 与 `CountDownLatch` 一样,也是共享锁的一种实现。它默认构造 AQS 的 `state` 为 `permits`。当执行任务的线程数量超出 `permits`,那么多余的线程将会被放入阻塞队列 `Park`,并自旋判断 `state` 是否大于 0。只有当 `state` 大于 0 的时候,阻塞的线程才能继续执行,此时先前执行任务的线程继续执行 `release()` 方法,`release()` 方法使得 state 的变量会加 1,那么自旋的线程便会判断成功。 -> 如此,每次只有最多不超过 `permits` 数量的线程能自旋成功,便限制了执行任务线程的数量。 +> `Semaphore` 基于 AQS 实现,用于控制并发访问的线程数量,但它与共享锁的概念有所不同。`Semaphore` 的构造函数使用 `permits` 参数初始化 AQS 的 `state` 变量,该变量表示可用的许可数量。当线程调用 `acquire()` 方法尝试获取许可时,`state` 会原子性地减 1。如果 `state` 减 1 后大于等于 0,则 `acquire()` 成功返回,线程可以继续执行。如果 `state` 减 1 后小于 0,表示当前并发访问的线程数量已达到 `permits` 的限制,该线程会被放入 AQS 的等待队列并阻塞,**而不是自旋等待**。当其他线程完成任务并调用 `release()` 方法时,`state` 会原子性地加 1。`release()` 操作会唤醒 AQS 等待队列中的一个或多个阻塞线程。这些被唤醒的线程将再次尝试 `acquire()` 操作,竞争获取可用的许可。因此,`Semaphore` 通过控制许可数量来限制并发访问的线程数量,而不是通过自旋和共享锁机制。 ### CountDownLatch (倒计时器) @@ -268,7 +1157,104 @@ semaphore.release(5);// 释放5个许可 #### 原理 -`CountDownLatch` 是共享锁的一种实现,它默认构造 AQS 的 `state` 值为 `count`。当线程使用 `countDown()` 方法时,其实使用了`tryReleaseShared`方法以 CAS 的操作来减少 `state`,直至 `state` 为 0 。当调用 `await()` 方法的时候,如果 `state` 不为 0,那就证明任务还没有执行完毕,`await()` 方法就会一直阻塞,也就是说 `await()` 方法之后的语句不会被执行。然后,`CountDownLatch` 会自旋 CAS 判断 `state == 0`,如果 `state == 0` 的话,就会释放所有等待的线程,`await()` 方法之后的语句得到执行。 +`CountDownLatch` 是共享锁的一种实现,它默认构造 AQS 的 `state` 值为 `count`。这个我们通过 `CountDownLatch` 的构造方法即可看出。 + +```java +public CountDownLatch(int count) { + if (count < 0) throw new IllegalArgumentException("count < 0"); + this.sync = new Sync(count); +} + +private static final class Sync extends AbstractQueuedSynchronizer { + Sync(int count) { + setState(count); + } + //... +} +``` + +当线程调用 `countDown()` 时,其实使用了`tryReleaseShared`方法以 CAS 的操作来减少 `state`,直至 `state` 为 0 。当 `state` 为 0 时,表示所有的线程都调用了 `countDown` 方法,那么在 `CountDownLatch` 上等待的线程就会被唤醒并继续执行。 + +```java +public void countDown() { + // Sync 是 CountDownLatch 的内部类 , 继承了 AbstractQueuedSynchronizer + sync.releaseShared(1); +} +``` + +`releaseShared`方法是 `AbstractQueuedSynchronizer` 中的默认实现。 + +```java +// 释放共享锁 +// 如果 tryReleaseShared 返回 true,就唤醒等待队列中的一个或多个线程。 +public final boolean releaseShared(int arg) { + //释放共享锁 + if (tryReleaseShared(arg)) { + //释放当前节点的后置等待节点 + doReleaseShared(); + return true; + } + return false; +} +``` + +`tryReleaseShared` 方法是`CountDownLatch` 的内部类 `Sync` 重写的一个方法, `AbstractQueuedSynchronizer`中的默认实现仅仅抛出 `UnsupportedOperationException` 异常。 + +```java +// 对 state 进行递减,直到 state 变成 0; +// 只有 count 递减到 0 时,countDown 才会返回 true +protected boolean tryReleaseShared(int releases) { + // 自选检查 state 是否为 0 + for (;;) { + int c = getState(); + // 如果 state 已经是 0 了,直接返回 false + if (c == 0) + return false; + // 对 state 进行递减 + int nextc = c-1; + // CAS 操作更新 state 的值 + if (compareAndSetState(c, nextc)) + return nextc == 0; + } +} +``` + +以无参 `await`方法为例,当调用 `await()` 的时候,如果 `state` 不为 0,那就证明任务还没有执行完毕,`await()` 就会一直阻塞,也就是说 `await()` 之后的语句不会被执行(`main` 线程被加入到等待队列也就是 变体 CLH 队列中了)。然后,`CountDownLatch` 会自旋 CAS 判断 `state == 0`,如果 `state == 0` 的话,就会释放所有等待的线程,`await()` 方法之后的语句得到执行。 + +```java +// 等待(也可以叫做加锁) +public void await() throws InterruptedException { + sync.acquireSharedInterruptibly(1); +} +// 带有超时时间的等待 +public boolean await(long timeout, TimeUnit unit) + throws InterruptedException { + return sync.tryAcquireSharedNanos(1, unit.toNanos(timeout)); +} +``` + +`acquireSharedInterruptibly`方法是 `AbstractQueuedSynchronizer` 中的默认实现。 + +```java +// 尝试获取锁,获取成功则返回,失败则加入等待队列,挂起线程 +public final void acquireSharedInterruptibly(int arg) + throws InterruptedException { + if (Thread.interrupted()) + throw new InterruptedException(); + // 尝试获得锁,获取成功则返回 + if (tryAcquireShared(arg) < 0) + // 获取失败加入等待队列,挂起线程 + doAcquireSharedInterruptibly(arg); +} +``` + +`tryAcquireShared` 方法是`CountDownLatch` 的内部类 `Sync` 重写的一个方法,其作用就是判断 `state` 的值是否为 0,是的话就返回 1,否则返回 -1。 + +```java +protected int tryAcquireShared(int acquires) { + return (getState() == 0) ? 1 : -1; +} +``` #### 实战 @@ -280,30 +1266,25 @@ semaphore.release(5);// 释放5个许可 **CountDownLatch 代码示例**: ```java -/** - * - * @author SnailClimb - * @date 2018年10月1日 - * @Description: CountDownLatch 使用方法示例 - */ -public class CountDownLatchExample1 { +public class CountDownLatchExample { // 请求的数量 - private static final int threadCount = 550; + private static final int THREAD_COUNT = 550; public static void main(String[] args) throws InterruptedException { // 创建一个具有固定线程数量的线程池对象(如果这里线程池的线程数量给太少的话你会发现执行的很慢) + // 只是测试使用,实际场景请手动赋值线程池参数 ExecutorService threadPool = Executors.newFixedThreadPool(300); - final CountDownLatch countDownLatch = new CountDownLatch(threadCount); - for (int i = 0; i < threadCount; i++) { - final int threadnum = i; - threadPool.execute(() -> {// Lambda 表达式的运用 + final CountDownLatch countDownLatch = new CountDownLatch(THREAD_COUNT); + for (int i = 0; i < THREAD_COUNT; i++) { + final int threadNum = i; + threadPool.execute(() -> { try { - test(threadnum); + test(threadNum); } catch (InterruptedException e) { - // TODO Auto-generated catch block e.printStackTrace(); } finally { - countDownLatch.countDown();// 表示一个请求已经被完成 + // 表示一个请求已经被完成 + countDownLatch.countDown(); } }); @@ -314,12 +1295,11 @@ public class CountDownLatchExample1 { } public static void test(int threadnum) throws InterruptedException { - Thread.sleep(1000);// 模拟请求的耗时操作 - System.out.println("threadnum:" + threadnum); - Thread.sleep(1000);// 模拟请求的耗时操作 + Thread.sleep(1000); + System.out.println("threadNum:" + threadnum); + Thread.sleep(1000); } } - ``` 上面的代码中,我们定义了请求的数量为 550,当这 550 个请求被处理完成之后,才会执行`System.out.println("finish");`。 @@ -344,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)。它要做的事情是:让一组线程到达一个屏障(也可以叫同步点)时被阻塞,直到最后一个线程到达屏障时,屏障才会开门,所有被屏障拦截的线程才会继续干活。 @@ -383,9 +1363,9 @@ public CyclicBarrier(int parties, Runnable barrierAction) { ```java public int await() throws InterruptedException, BrokenBarrierException { try { - return dowait(false, 0L); + return dowait(false, 0L); } catch (TimeoutException toe) { - throw new Error(toe); // cannot happen + throw new Error(toe); // cannot happen } } ``` @@ -415,7 +1395,7 @@ public int await() throws InterruptedException, BrokenBarrierException { breakBarrier(); throw new InterruptedException(); } - // cout减1 + // count 减1 int index = --count; // 当 count 数量减为 0 之后说明最后一个线程已经到达栅栏了,也就是达到了可以执行await 方法之后的条件 if (index == 0) { // tripped @@ -477,12 +1457,6 @@ public int await() throws InterruptedException, BrokenBarrierException { 示例 1: ```java -/** - * - * @author Snailclimb - * @date 2018年10月1日 - * @Description: 测试 CyclicBarrier 类中带参数的 await() 方法 - */ public class CyclicBarrierExample1 { // 请求的数量 private static final int threadCount = 550; @@ -527,7 +1501,7 @@ public class CyclicBarrierExample1 { 运行结果,如下: -``` +```plain threadnum:0is ready threadnum:1is ready threadnum:2is ready @@ -558,12 +1532,6 @@ threadnum:6is finish 示例 2: ```java -/** - * - * @author SnailClimb - * @date 2018年10月1日 - * @Description: 新建 CyclicBarrier 的时候指定一个 Runnable - */ public class CyclicBarrierExample2 { // 请求的数量 private static final int threadCount = 550; @@ -605,7 +1573,7 @@ public class CyclicBarrierExample2 { 运行结果,如下: -``` +```plain threadnum:0is ready threadnum:1is ready threadnum:2is ready @@ -630,3 +1598,10 @@ threadnum:8is finish threadnum:7is finish ...... ``` + +## 参考 + +- Java 并发之 AQS 详解: +- 从 ReentrantLock 的实现看 AQS 的原理及应用: + + diff --git a/docs/java/concurrent/atomic-classes.md b/docs/java/concurrent/atomic-classes.md index bcc32153888..ec47ba6f66f 100644 --- a/docs/java/concurrent/atomic-classes.md +++ b/docs/java/concurrent/atomic-classes.md @@ -7,17 +7,21 @@ tag: ## Atomic 原子类介绍 -Atomic 翻译成中文是原子的意思。在化学上,我们知道原子是构成一般物质的最小单位,在化学反应中是不可分割的。在我们这里 Atomic 是指一个操作是不可中断的。即使是在多个线程一起执行的时候,一个操作一旦开始,就不会被其他线程干扰。 +`Atomic` 翻译成中文是“原子”的意思。在化学上,原子是构成物质的最小单位,在化学反应中不可分割。在编程中,`Atomic` 指的是一个操作具有原子性,即该操作不可分割、不可中断。即使在多个线程同时执行时,该操作要么全部执行完成,要么不执行,不会被其他线程看到部分完成的状态。 -所以,所谓原子类说简单点就是具有原子/原子操作特征的类。 +原子类简单来说就是具有原子性操作特征的类。 -并发包 `java.util.concurrent` 的原子类都存放在`java.util.concurrent.atomic`下,如下图所示。 +`java.util.concurrent.atomic` 包中的 `Atomic` 原子类提供了一种线程安全的方式来操作单个变量。 -![JUC原子类概览](https://my-blog-to-use.oss-cn-beijing.aliyuncs.com/2019-6/JUC原子类概览.png) +`Atomic` 类依赖于 CAS(Compare-And-Swap,比较并交换)乐观锁来保证其方法的原子性,而不需要使用传统的锁机制(如 `synchronized` 块或 `ReentrantLock`)。 -根据操作的数据类型,可以将 JUC 包中的原子类分为 4 类 +这篇文章我们只介绍 Atomic 原子类的概念,具体实现原理可以阅读笔者写的这篇文章:[CAS 详解](./cas.md)。 -**基本类型** +![JUC原子类概览](https://oss.javaguide.cn/github/javaguide/java/JUC%E5%8E%9F%E5%AD%90%E7%B1%BB%E6%A6%82%E8%A7%88.png) + +根据操作的数据类型,可以将 JUC 包中的原子类分为 4 类: + +**1、基本类型** 使用原子的方式更新基本类型 @@ -25,7 +29,7 @@ Atomic 翻译成中文是原子的意思。在化学上,我们知道原子是 - `AtomicLong`:长整型原子类 - `AtomicBoolean`:布尔型原子类 -**数组类型** +**2、数组类型** 使用原子的方式更新数组里的某个元素 @@ -33,7 +37,7 @@ Atomic 翻译成中文是原子的意思。在化学上,我们知道原子是 - `AtomicLongArray`:长整型数组原子类 - `AtomicReferenceArray`:引用类型数组原子类 -**引用类型** +**3、引用类型** - `AtomicReference`:引用类型原子类 - `AtomicMarkableReference`:原子更新带有标记的引用类型。该类将 boolean 标记与引用关联起来,~~也可以解决使用 CAS 进行原子更新时可能出现的 ABA 问题~~。 @@ -41,7 +45,7 @@ Atomic 翻译成中文是原子的意思。在化学上,我们知道原子是 **🐛 修正(参见:[issue#626](https://github.com/Snailclimb/JavaGuide/issues/626))** : `AtomicMarkableReference` 不能解决 ABA 问题。 -**对象的属性修改类型** +**4、对象的属性修改类型** - `AtomicIntegerFieldUpdater`:原子更新整型字段的更新器 - `AtomicLongFieldUpdater`:原子更新长整型字段的更新器 @@ -57,7 +61,7 @@ Atomic 翻译成中文是原子的意思。在化学上,我们知道原子是 上面三个类提供的方法几乎相同,所以我们这里以 `AtomicInteger` 为例子来介绍。 -**AtomicInteger 类常用方法** +**`AtomicInteger` 类常用方法** : ```java public final int get() //获取当前的值 @@ -66,90 +70,51 @@ public final int getAndIncrement()//获取当前的值,并自增 public final int getAndDecrement() //获取当前的值,并自减 public final int getAndAdd(int delta) //获取当前的值,并加上预期的值 boolean compareAndSet(int expect, int update) //如果输入的数值等于预期值,则以原子方式将该值设置为输入值(update) -public final void lazySet(int newValue)//最终设置为newValue,使用 lazySet 设置之后可能导致其他线程在之后的一小段时间内还是可以读到旧的值。 +public final void lazySet(int newValue)//最终设置为newValue, lazySet 提供了一种比 set 方法更弱的语义,可能导致其他线程在之后的一小段时间内还是可以读到旧的值,但可能更高效。 ``` **`AtomicInteger` 类使用示例** : ```java -import java.util.concurrent.atomic.AtomicInteger; - -public class AtomicIntegerTest { +// 初始化 AtomicInteger 对象,初始值为 0 +AtomicInteger atomicInt = new AtomicInteger(0); - public static void main(String[] args) { - int temvalue = 0; - AtomicInteger i = new AtomicInteger(0); - temvalue = i.getAndSet(3); - System.out.println("temvalue:" + temvalue + "; i:" + i); //temvalue:0; i:3 - temvalue = i.getAndIncrement(); - System.out.println("temvalue:" + temvalue + "; i:" + i); //temvalue:3; i:4 - temvalue = i.getAndAdd(5); - System.out.println("temvalue:" + temvalue + "; i:" + i); //temvalue:4; i:9 - } - -} -``` +// 使用 getAndSet 方法获取当前值,并设置新值为 3 +int tempValue = atomicInt.getAndSet(3); +System.out.println("tempValue: " + tempValue + "; atomicInt: " + atomicInt); -### 基本数据类型原子类的优势 +// 使用 getAndIncrement 方法获取当前值,并自增 1 +tempValue = atomicInt.getAndIncrement(); +System.out.println("tempValue: " + tempValue + "; atomicInt: " + atomicInt); -通过一个简单例子带大家看一下基本数据类型原子类的优势 - -**1、多线程环境不使用原子类保证线程安全(基本数据类型)** - -```java -class Test { - private volatile int count = 0; - //若要线程安全执行执行count++,需要加锁 - public synchronized void increment() { - count++; - } - - public int getCount() { - return count; - } -} -``` +// 使用 getAndAdd 方法获取当前值,并增加指定值 5 +tempValue = atomicInt.getAndAdd(5); +System.out.println("tempValue: " + tempValue + "; atomicInt: " + atomicInt); -**2、多线程环境使用原子类保证线程安全(基本数据类型)** +// 使用 compareAndSet 方法进行原子性条件更新,期望值为 9,更新值为 10 +boolean updateSuccess = atomicInt.compareAndSet(9, 10); +System.out.println("Update Success: " + updateSuccess + "; atomicInt: " + atomicInt); -```java -class Test2 { - private AtomicInteger count = new AtomicInteger(); - - public void increment() { - count.incrementAndGet(); - } - //使用AtomicInteger之后,不需要加锁,也可以实现线程安全。 - public int getCount() { - return count.get(); - } -} +// 获取当前值 +int currentValue = atomicInt.get(); +System.out.println("Current value: " + currentValue); +// 使用 lazySet 方法设置新值为 15 +atomicInt.lazySet(15); +System.out.println("After lazySet, atomicInt: " + atomicInt); ``` -### AtomicInteger 线程安全原理简单分析 - -`AtomicInteger` 类的部分源码: +输出: ```java - // setup to use Unsafe.compareAndSwapInt for updates(更新操作时提供“比较并替换”的作用) - private static final Unsafe unsafe = Unsafe.getUnsafe(); - private static final long valueOffset; - - static { - try { - valueOffset = unsafe.objectFieldOffset - (AtomicInteger.class.getDeclaredField("value")); - } catch (Exception ex) { throw new Error(ex); } - } - - private volatile int value; +tempValue: 0; atomicInt: 3 +tempValue: 3; atomicInt: 4 +tempValue: 4; atomicInt: 9 +Update Success: true; atomicInt: 10 +Current value: 10 +After lazySet, atomicInt: 15 ``` -`AtomicInteger` 类主要利用 CAS (compare and swap) + volatile 和 native 方法来保证原子操作,从而避免 synchronized 的高开销,执行效率大为提升。 - -CAS 的原理是拿期望的值和原本的一个值作比较,如果相同则更新成新的值。UnSafe 类的 `objectFieldOffset()` 方法是一个本地方法,这个方法是用来拿到“原来的值”的内存地址。另外 value 是一个 volatile 变量,在内存中可见,因此 JVM 可以保证任何时刻任何线程总能拿到该变量的最新值。 - ## 数组类型原子类 使用原子的方式更新数组里的某个元素 @@ -175,28 +140,57 @@ public final void lazySet(int i, int newValue)//最终 将index=i 位置的元 **`AtomicIntegerArray` 类使用示例** : ```java -import java.util.concurrent.atomic.AtomicIntegerArray; - -public class AtomicIntegerArrayTest { - - public static void main(String[] args) { - int temvalue = 0; - int[] nums = { 1, 2, 3, 4, 5, 6 }; - AtomicIntegerArray i = new AtomicIntegerArray(nums); - for (int j = 0; j < nums.length; j++) { - System.out.println(i.get(j)); - } - temvalue = i.getAndSet(0, 2); - System.out.println("temvalue:" + temvalue + "; i:" + i); - temvalue = i.getAndIncrement(0); - System.out.println("temvalue:" + temvalue + "; i:" + i); - temvalue = i.getAndAdd(0, 5); - System.out.println("temvalue:" + temvalue + "; i:" + i); - } +int[] nums = {1, 2, 3, 4, 5, 6}; +// 创建 AtomicIntegerArray +AtomicIntegerArray atomicArray = new AtomicIntegerArray(nums); + +// 打印 AtomicIntegerArray 中的初始值 +System.out.println("Initial values in AtomicIntegerArray:"); +for (int j = 0; j < nums.length; j++) { + System.out.print("Index " + j + ": " + atomicArray.get(j) + " "); +} +// 使用 getAndSet 方法将索引 0 处的值设置为 2,并返回旧值 +int tempValue = atomicArray.getAndSet(0, 2); +System.out.println("\nAfter getAndSet(0, 2):"); +System.out.println("Returned value: " + tempValue); +for (int j = 0; j < atomicArray.length(); j++) { + System.out.print("Index " + j + ": " + atomicArray.get(j) + " "); +} + +// 使用 getAndIncrement 方法将索引 0 处的值加 1,并返回旧值 +tempValue = atomicArray.getAndIncrement(0); +System.out.println("\nAfter getAndIncrement(0):"); +System.out.println("Returned value: " + tempValue); +for (int j = 0; j < atomicArray.length(); j++) { + System.out.print("Index " + j + ": " + atomicArray.get(j) + " "); +} + +// 使用 getAndAdd 方法将索引 0 处的值增加 5,并返回旧值 +tempValue = atomicArray.getAndAdd(0, 5); +System.out.println("\nAfter getAndAdd(0, 5):"); +System.out.println("Returned value: " + tempValue); +for (int j = 0; j < atomicArray.length(); j++) { + System.out.print("Index " + j + ": " + atomicArray.get(j) + " "); } ``` +输出: + +```plain +Initial values in AtomicIntegerArray: +Index 0: 1 Index 1: 2 Index 2: 3 Index 3: 4 Index 4: 5 Index 5: 6 +After getAndSet(0, 2): +Returned value: 1 +Index 0: 2 Index 1: 2 Index 2: 3 Index 3: 4 Index 4: 5 Index 5: 6 +After getAndIncrement(0): +Returned value: 2 +Index 0: 3 Index 1: 2 Index 2: 3 Index 3: 4 Index 4: 5 Index 5: 6 +After getAndAdd(0, 5): +Returned value: 3 +Index 0: 8 Index 1: 2 Index 2: 3 Index 3: 4 Index 4: 5 Index 5: 6 +``` + ## 引用类型原子类 基本类型原子类只能更新一个变量,如果需要原子更新多个变量,需要使用 引用类型原子类。 @@ -210,174 +204,133 @@ public class AtomicIntegerArrayTest { **`AtomicReference` 类使用示例** : ```java -import java.util.concurrent.atomic.AtomicReference; - -public class AtomicReferenceTest { - - public static void main(String[] args) { - AtomicReference < Person > ar = new AtomicReference < Person > (); - Person person = new Person("SnailClimb", 22); - ar.set(person); - Person updatePerson = new Person("Daisy", 20); - ar.compareAndSet(person, updatePerson); - - System.out.println(ar.get().getName()); - System.out.println(ar.get().getAge()); - } -} - +// Person 类 class Person { private String name; private int age; + //省略getter/setter和toString +} - public Person(String name, int age) { - super(); - this.name = name; - this.age = age; - } - public String getName() { - return name; - } +// 创建 AtomicReference 对象并设置初始值 +AtomicReference ar = new AtomicReference<>(new Person("SnailClimb", 22)); - public void setName(String name) { - this.name = name; - } +// 打印初始值 +System.out.println("Initial Person: " + ar.get().toString()); - public int getAge() { - return age; - } +// 更新值 +Person updatePerson = new Person("Daisy", 20); +ar.compareAndSet(ar.get(), updatePerson); - public void setAge(int age) { - this.age = age; - } +// 打印更新后的值 +System.out.println("Updated Person: " + ar.get().toString()); -} +// 尝试再次更新 +Person anotherUpdatePerson = new Person("John", 30); +boolean isUpdated = ar.compareAndSet(updatePerson, anotherUpdatePerson); + +// 打印是否更新成功及最终值 +System.out.println("Second Update Success: " + isUpdated); +System.out.println("Final Person: " + ar.get().toString()); ``` -上述代码首先创建了一个 `Person` 对象,然后把 `Person` 对象设置进 `AtomicReference` 对象中,然后调用 `compareAndSet` 方法,该方法就是通过 CAS 操作设置 ar。如果 ar 的值为 `person` 的话,则将其设置为 `updatePerson`。实现原理与 `AtomicInteger` 类中的 `compareAndSet` 方法相同。运行上面的代码后的输出结果如下: +输出: -``` -Daisy -20 +```plain +Initial Person: Person{name='SnailClimb', age=22} +Updated Person: Person{name='Daisy', age=20} +Second Update Success: true +Final Person: Person{name='John', age=30} ``` **`AtomicStampedReference` 类使用示例** : ```java -import java.util.concurrent.atomic.AtomicStampedReference; - -public class AtomicStampedReferenceDemo { - public static void main(String[] args) { - // 实例化、取当前值和 stamp 值 - final Integer initialRef = 0, initialStamp = 0; - final AtomicStampedReference asr = new AtomicStampedReference<>(initialRef, initialStamp); - System.out.println("currentValue=" + asr.getReference() + ", currentStamp=" + asr.getStamp()); - - // compare and set - final Integer newReference = 666, newStamp = 999; - final boolean casResult = asr.compareAndSet(initialRef, newReference, initialStamp, newStamp); - System.out.println("currentValue=" + asr.getReference() - + ", currentStamp=" + asr.getStamp() - + ", casResult=" + casResult); - - // 获取当前的值和当前的 stamp 值 - int[] arr = new int[1]; - final Integer currentValue = asr.get(arr); - final int currentStamp = arr[0]; - System.out.println("currentValue=" + currentValue + ", currentStamp=" + currentStamp); - - // 单独设置 stamp 值 - final boolean attemptStampResult = asr.attemptStamp(newReference, 88); - System.out.println("currentValue=" + asr.getReference() - + ", currentStamp=" + asr.getStamp() - + ", attemptStampResult=" + attemptStampResult); - - // 重新设置当前值和 stamp 值 - asr.set(initialRef, initialStamp); - System.out.println("currentValue=" + asr.getReference() + ", currentStamp=" + asr.getStamp()); - - // [不推荐使用,除非搞清楚注释的意思了] weak compare and set - // 困惑!weakCompareAndSet 这个方法最终还是调用 compareAndSet 方法。[版本: jdk-8u191] - // 但是注释上写着 "May fail spuriously and does not provide ordering guarantees, - // so is only rarely an appropriate alternative to compareAndSet." - // todo 感觉有可能是 jvm 通过方法名在 native 方法里面做了转发 - final boolean wCasResult = asr.weakCompareAndSet(initialRef, newReference, initialStamp, newStamp); - System.out.println("currentValue=" + asr.getReference() - + ", currentStamp=" + asr.getStamp() - + ", wCasResult=" + wCasResult); - } -} +// 创建一个 AtomicStampedReference 对象,初始值为 "SnailClimb",初始版本号为 1 +AtomicStampedReference asr = new AtomicStampedReference<>("SnailClimb", 1); + +// 打印初始值和版本号 +int[] initialStamp = new int[1]; +String initialRef = asr.get(initialStamp); +System.out.println("Initial Reference: " + initialRef + ", Initial Stamp: " + initialStamp[0]); + +// 更新值和版本号 +int oldStamp = initialStamp[0]; +String oldRef = initialRef; +String newRef = "Daisy"; +int newStamp = oldStamp + 1; + +boolean isUpdated = asr.compareAndSet(oldRef, newRef, oldStamp, newStamp); +System.out.println("Update Success: " + isUpdated); + +// 打印更新后的值和版本号 +int[] updatedStamp = new int[1]; +String updatedRef = asr.get(updatedStamp); +System.out.println("Updated Reference: " + updatedRef + ", Updated Stamp: " + updatedStamp[0]); + +// 尝试用错误的版本号更新 +boolean isUpdatedWithWrongStamp = asr.compareAndSet(newRef, "John", oldStamp, newStamp + 1); +System.out.println("Update with Wrong Stamp Success: " + isUpdatedWithWrongStamp); + +// 打印最终的值和版本号 +int[] finalStamp = new int[1]; +String finalRef = asr.get(finalStamp); +System.out.println("Final Reference: " + finalRef + ", Final Stamp: " + finalStamp[0]); ``` 输出结果如下: -``` -currentValue=0, currentStamp=0 -currentValue=666, currentStamp=999, casResult=true -currentValue=666, currentStamp=999 -currentValue=666, currentStamp=88, attemptStampResult=true -currentValue=0, currentStamp=0 -currentValue=666, currentStamp=999, wCasResult=true +```plain +Initial Reference: SnailClimb, Initial Stamp: 1 +Update Success: true +Updated Reference: Daisy, Updated Stamp: 2 +Update with Wrong Stamp Success: false +Final Reference: Daisy, Final Stamp: 2 ``` **`AtomicMarkableReference` 类使用示例** : ```java -import java.util.concurrent.atomic.AtomicMarkableReference; - -public class AtomicMarkableReferenceDemo { - public static void main(String[] args) { - // 实例化、取当前值和 mark 值 - final Boolean initialRef = null, initialMark = false; - final AtomicMarkableReference amr = new AtomicMarkableReference<>(initialRef, initialMark); - System.out.println("currentValue=" + amr.getReference() + ", currentMark=" + amr.isMarked()); - - // compare and set - final Boolean newReference1 = true, newMark1 = true; - final boolean casResult = amr.compareAndSet(initialRef, newReference1, initialMark, newMark1); - System.out.println("currentValue=" + amr.getReference() - + ", currentMark=" + amr.isMarked() - + ", casResult=" + casResult); - - // 获取当前的值和当前的 mark 值 - boolean[] arr = new boolean[1]; - final Boolean currentValue = amr.get(arr); - final boolean currentMark = arr[0]; - System.out.println("currentValue=" + currentValue + ", currentMark=" + currentMark); - - // 单独设置 mark 值 - final boolean attemptMarkResult = amr.attemptMark(newReference1, false); - System.out.println("currentValue=" + amr.getReference() - + ", currentMark=" + amr.isMarked() - + ", attemptMarkResult=" + attemptMarkResult); - - // 重新设置当前值和 mark 值 - amr.set(initialRef, initialMark); - System.out.println("currentValue=" + amr.getReference() + ", currentMark=" + amr.isMarked()); - - // [不推荐使用,除非搞清楚注释的意思了] weak compare and set - // 困惑!weakCompareAndSet 这个方法最终还是调用 compareAndSet 方法。[版本: jdk-8u191] - // 但是注释上写着 "May fail spuriously and does not provide ordering guarantees, - // so is only rarely an appropriate alternative to compareAndSet." - // todo 感觉有可能是 jvm 通过方法名在 native 方法里面做了转发 - final boolean wCasResult = amr.weakCompareAndSet(initialRef, newReference1, initialMark, newMark1); - System.out.println("currentValue=" + amr.getReference() - + ", currentMark=" + amr.isMarked() - + ", wCasResult=" + wCasResult); - } -} +// 创建一个 AtomicMarkableReference 对象,初始值为 "SnailClimb",初始标记为 false +AtomicMarkableReference amr = new AtomicMarkableReference<>("SnailClimb", false); + +// 打印初始值和标记 +boolean[] initialMark = new boolean[1]; +String initialRef = amr.get(initialMark); +System.out.println("Initial Reference: " + initialRef + ", Initial Mark: " + initialMark[0]); + +// 更新值和标记 +String oldRef = initialRef; +String newRef = "Daisy"; +boolean oldMark = initialMark[0]; +boolean newMark = true; + +boolean isUpdated = amr.compareAndSet(oldRef, newRef, oldMark, newMark); +System.out.println("Update Success: " + isUpdated); + +// 打印更新后的值和标记 +boolean[] updatedMark = new boolean[1]; +String updatedRef = amr.get(updatedMark); +System.out.println("Updated Reference: " + updatedRef + ", Updated Mark: " + updatedMark[0]); + +// 尝试用错误的标记更新 +boolean isUpdatedWithWrongMark = amr.compareAndSet(newRef, "John", oldMark, !newMark); +System.out.println("Update with Wrong Mark Success: " + isUpdatedWithWrongMark); + +// 打印最终的值和标记 +boolean[] finalMark = new boolean[1]; +String finalRef = amr.get(finalMark); +System.out.println("Final Reference: " + finalRef + ", Final Mark: " + finalMark[0]); ``` 输出结果如下: -``` -currentValue=null, currentMark=false -currentValue=true, currentMark=true, casResult=true -currentValue=true, currentMark=true -currentValue=true, currentMark=false, attemptMarkResult=true -currentValue=null, currentMark=false -currentValue=true, currentMark=true, wCasResult=true +```plain +Initial Reference: SnailClimb, Initial Mark: false +Update Success: true +Updated Reference: Daisy, Updated Mark: true +Update with Wrong Mark Success: false +Final Reference: Daisy, Final Mark: true ``` ## 对象的属性修改类型原子类 @@ -388,61 +341,59 @@ currentValue=true, currentMark=true, wCasResult=true - `AtomicLongFieldUpdater`:原子更新长整形字段的更新器 - `AtomicReferenceFieldUpdater`:原子更新引用类型里的字段的更新器 -要想原子地更新对象的属性需要两步。第一步,因为对象的属性修改类型原子类都是抽象类,所以每次使用都必须使用静态方法 newUpdater()创建一个更新器,并且需要设置想要更新的类和属性。第二步,更新的对象属性必须使用 public volatile 修饰符。 +要想原子地更新对象的属性需要两步。第一步,因为对象的属性修改类型原子类都是抽象类,所以每次使用都必须使用静态方法 newUpdater()创建一个更新器,并且需要设置想要更新的类和属性。第二步,更新的对象属性必须使用 volatile int 修饰符。 上面三个类提供的方法几乎相同,所以我们这里以 `AtomicIntegerFieldUpdater`为例子来介绍。 **`AtomicIntegerFieldUpdater` 类使用示例** : ```java -import java.util.concurrent.atomic.AtomicIntegerFieldUpdater; - -public class AtomicIntegerFieldUpdaterTest { - public static void main(String[] args) { - AtomicIntegerFieldUpdater a = AtomicIntegerFieldUpdater.newUpdater(User.class, "age"); - - User user = new User("Java", 22); - System.out.println(a.getAndIncrement(user));// 22 - System.out.println(a.get(user));// 23 - } +// Person 类 +class Person { + private String name; + // 要使用 AtomicIntegerFieldUpdater,字段必须是 volatile int + volatile int age; + //省略getter/setter和toString } -class User { - private String name; - public volatile int age; +// 创建 AtomicIntegerFieldUpdater 对象 +AtomicIntegerFieldUpdater ageUpdater = AtomicIntegerFieldUpdater.newUpdater(Person.class, "age"); - public User(String name, int age) { - super(); - this.name = name; - this.age = age; - } +// 创建 Person 对象 +Person person = new Person("SnailClimb", 22); - public String getName() { - return name; - } +// 打印初始值 +System.out.println("Initial Person: " + person); - public void setName(String name) { - this.name = name; - } +// 更新 age 字段 +ageUpdater.incrementAndGet(person); // 自增 +System.out.println("After Increment: " + person); - public int getAge() { - return age; - } +ageUpdater.addAndGet(person, 5); // 增加 5 +System.out.println("After Adding 5: " + person); - public void setAge(int age) { - this.age = age; - } +ageUpdater.compareAndSet(person, 28, 30); // 如果当前值是 28,则设置为 30 +System.out.println("After Compare and Set (28 to 30): " + person); -} +// 尝试使用错误的比较值进行更新 +boolean isUpdated = ageUpdater.compareAndSet(person, 28, 35); // 这次应该失败 +System.out.println("Compare and Set (28 to 35) Success: " + isUpdated); +System.out.println("Final Person: " + person); ``` 输出结果: -``` -22 -23 +```plain +Initial Person: Name: SnailClimb, Age: 22 +After Increment: Name: SnailClimb, Age: 23 +After Adding 5: Name: SnailClimb, Age: 28 +After Compare and Set (28 to 30): Name: SnailClimb, Age: 30 +Compare and Set (28 to 35) Success: false +Final Person: Name: SnailClimb, Age: 30 ``` ## 参考 - 《Java 并发编程的艺术》 + + diff --git a/docs/java/concurrent/cas.md b/docs/java/concurrent/cas.md new file mode 100644 index 00000000000..af97f28d0c8 --- /dev/null +++ b/docs/java/concurrent/cas.md @@ -0,0 +1,162 @@ +--- +title: CAS 详解 +category: Java +tag: + - Java并发 +--- + +乐观锁和悲观锁的介绍以及乐观锁常见实现方式可以阅读笔者写的这篇文章:[乐观锁和悲观锁详解](https://javaguide.cn/java/concurrent/optimistic-lock-and-pessimistic-lock.html)。 + +这篇文章主要介绍 :Java 中 CAS 的实现以及 CAS 存在的一些问题。 + +## Java 中 CAS 是如何实现的? + +在 Java 中,实现 CAS(Compare-And-Swap, 比较并交换)操作的一个关键类是`Unsafe`。 + +`Unsafe`类位于`sun.misc`包下,是一个提供低级别、不安全操作的类。由于其强大的功能和潜在的危险性,它通常用于 JVM 内部或一些需要极高性能和底层访问的库中,而不推荐普通开发者在应用程序中使用。关于 `Unsafe`类的详细介绍,可以阅读这篇文章:📌[Java 魔法类 Unsafe 详解](https://javaguide.cn/java/basis/unsafe.html)。 + +`sun.misc`包下的`Unsafe`类提供了`compareAndSwapObject`、`compareAndSwapInt`、`compareAndSwapLong`方法来实现的对`Object`、`int`、`long`类型的 CAS 操作: + +```java +/** + * 以原子方式更新对象字段的值。 + * + * @param o 要操作的对象 + * @param offset 对象字段的内存偏移量 + * @param expected 期望的旧值 + * @param x 要设置的新值 + * @return 如果值被成功更新,则返回 true;否则返回 false + */ +boolean compareAndSwapObject(Object o, long offset, Object expected, Object x); + +/** + * 以原子方式更新 int 类型的对象字段的值。 + */ +boolean compareAndSwapInt(Object o, long offset, int expected, int x); + +/** + * 以原子方式更新 long 类型的对象字段的值。 + */ +boolean compareAndSwapLong(Object o, long offset, long expected, long x); +``` + +`Unsafe`类中的 CAS 方法是`native`方法。`native`关键字表明这些方法是用本地代码(通常是 C 或 C++)实现的,而不是用 Java 实现的。这些方法直接调用底层的硬件指令来实现原子操作。也就是说,Java 语言并没有直接用 Java 实现 CAS。 + +更准确点来说,Java 中 CAS 是 C++ 内联汇编的形式实现的,通过 JNI(Java Native Interface) 调用。因此,CAS 的具体实现与操作系统以及 CPU 密切相关。 + +`java.util.concurrent.atomic` 包提供了一些用于原子操作的类。 + +![JUC原子类概览](https://oss.javaguide.cn/github/javaguide/java/JUC%E5%8E%9F%E5%AD%90%E7%B1%BB%E6%A6%82%E8%A7%88.png) + +关于这些 Atomic 原子类的介绍和使用,可以阅读这篇文章:[Atomic 原子类总结](https://javaguide.cn/java/concurrent/atomic-classes.html)。 + +Atomic 类依赖于 CAS 乐观锁来保证其方法的原子性,而不需要使用传统的锁机制(如 `synchronized` 块或 `ReentrantLock`)。 + +`AtomicInteger`是 Java 的原子类之一,主要用于对 `int` 类型的变量进行原子操作,它利用`Unsafe`类提供的低级别原子操作方法实现无锁的线程安全性。 + +下面,我们通过解读`AtomicInteger`的核心源码(JDK1.8),来说明 Java 如何使用`Unsafe`类的方法来实现原子操作。 + +`AtomicInteger`核心源码如下: + +```java +// 获取 Unsafe 实例 +private static final Unsafe unsafe = Unsafe.getUnsafe(); +private static final long valueOffset; + +static { + try { + // 获取“value”字段在AtomicInteger类中的内存偏移量 + valueOffset = unsafe.objectFieldOffset + (AtomicInteger.class.getDeclaredField("value")); + } catch (Exception ex) { throw new Error(ex); } +} +// 确保“value”字段的可见性 +private volatile int value; + +// 如果当前值等于预期值,则原子地将值设置为newValue +// 使用 Unsafe#compareAndSwapInt 方法进行CAS操作 +public final boolean compareAndSet(int expect, int update) { + return unsafe.compareAndSwapInt(this, valueOffset, expect, update); +} + +// 原子地将当前值加 delta 并返回旧值 +public final int getAndAdd(int delta) { + return unsafe.getAndAddInt(this, valueOffset, delta); +} + +// 原子地将当前值加 1 并返回加之前的值(旧值) +// 使用 Unsafe#getAndAddInt 方法进行CAS操作。 +public final int getAndIncrement() { + return unsafe.getAndAddInt(this, valueOffset, 1); +} + +// 原子地将当前值减 1 并返回减之前的值(旧值) +public final int getAndDecrement() { + return unsafe.getAndAddInt(this, valueOffset, -1); +} +``` + +`Unsafe#getAndAddInt`源码: + +```java +// 原子地获取并增加整数值 +public final int getAndAddInt(Object o, long offset, int delta) { + int v; + do { + // 以 volatile 方式获取对象 o 在内存偏移量 offset 处的整数值 + v = getIntVolatile(o, offset); + } while (!compareAndSwapInt(o, offset, v, v + delta)); + // 返回旧值 + return v; +} +``` + +可以看到,`getAndAddInt` 使用了 `do-while` 循环:在`compareAndSwapInt`操作失败时,会不断重试直到成功。也就是说,`getAndAddInt`方法会通过 `compareAndSwapInt` 方法来尝试更新 `value` 的值,如果更新失败(当前值在此期间被其他线程修改),它会重新获取当前值并再次尝试更新,直到操作成功。 + +由于 CAS 操作可能会因为并发冲突而失败,因此通常会与`while`循环搭配使用,在失败后不断重试,直到操作成功。这就是 **自旋锁机制** 。 + +## CAS 算法存在哪些问题? + +ABA 问题是 CAS 算法最常见的问题。 + +### ABA 问题 + +如果一个变量 V 初次读取的时候是 A 值,并且在准备赋值的时候检查到它仍然是 A 值,那我们就能说明它的值没有被其他线程修改过了吗?很明显是不能的,因为在这段时间它的值可能被改为其他值,然后又改回 A,那 CAS 操作就会误认为它从来没有被修改过。这个问题被称为 CAS 操作的 **"ABA"问题。** + +ABA 问题的解决思路是在变量前面追加上**版本号或者时间戳**。JDK 1.5 以后的 `AtomicStampedReference` 类就是用来解决 ABA 问题的,其中的 `compareAndSet()` 方法就是首先检查当前引用是否等于预期引用,并且当前标志是否等于预期标志,如果全部相等,则以原子方式将该引用和该标志的值设置为给定的更新值。 + +```java +public boolean compareAndSet(V expectedReference, + V newReference, + int expectedStamp, + int newStamp) { + Pair current = pair; + return + expectedReference == current.reference && + expectedStamp == current.stamp && + ((newReference == current.reference && + newStamp == current.stamp) || + casPair(current, Pair.of(newReference, newStamp))); +} +``` + +### 循环时间长开销大 + +CAS 经常会用到自旋操作来进行重试,也就是不成功就一直循环执行直到成功。如果长时间不成功,会给 CPU 带来非常大的执行开销。 + +如果 JVM 能够支持处理器提供的`pause`指令,那么自旋操作的效率将有所提升。`pause`指令有两个重要作用: + +1. **延迟流水线执行指令**:`pause`指令可以延迟指令的执行,从而减少 CPU 的资源消耗。具体的延迟时间取决于处理器的实现版本,在某些处理器上,延迟时间可能为零。 +2. **避免内存顺序冲突**:在退出循环时,`pause`指令可以避免由于内存顺序冲突而导致的 CPU 流水线被清空,从而提高 CPU 的执行效率。 + +### 只能保证一个共享变量的原子操作 + +CAS 操作仅能对单个共享变量有效。当需要操作多个共享变量时,CAS 就显得无能为力。不过,从 JDK 1.5 开始,Java 提供了`AtomicReference`类,这使得我们能够保证引用对象之间的原子性。通过将多个变量封装在一个对象中,我们可以使用`AtomicReference`来执行 CAS 操作。 + +除了 `AtomicReference` 这种方式之外,还可以利用加锁来保证。 + +## 总结 + +在 Java 中,CAS 通过 `Unsafe` 类中的 `native` 方法实现,这些方法调用底层的硬件指令来完成原子操作。由于其实现依赖于 C++ 内联汇编和 JNI 调用,因此 CAS 的具体实现与操作系统以及 CPU 密切相关。 + +CAS 虽然具有高效的无锁特性,但也需要注意 ABA 、循环时间长开销大等问题。 diff --git a/docs/java/concurrent/completablefuture-intro.md b/docs/java/concurrent/completablefuture-intro.md index c85e3948f6a..be21c70e1c7 100644 --- a/docs/java/concurrent/completablefuture-intro.md +++ b/docs/java/concurrent/completablefuture-intro.md @@ -5,21 +5,83 @@ tag: - Java并发 --- -自己在项目中使用 `CompletableFuture` 比较多,看到很多开源框架中也大量使用到了 `CompletableFuture` 。 +实际项目中,一个接口可能需要同时获取多种不同的数据,然后再汇总返回,这种场景还是挺常见的。举个例子:用户请求获取订单信息,可能需要同时获取用户信息、商品详情、物流信息、商品推荐等数据。 -因此,专门写一篇文章来介绍这个 Java 8 才被引入的一个非常有用的用于异步编程的类。 +如果是串行(按顺序依次执行每个任务)执行的话,接口的响应速度会非常慢。考虑到这些任务之间有大部分都是 **无前后顺序关联** 的,可以 **并行执行** ,就比如说调用获取商品详情的时候,可以同时调用获取物流信息。通过并行执行多个任务的方式,接口的响应速度会得到大幅优化。 -## 简单介绍 +![](https://oss.javaguide.cn/github/javaguide/high-performance/serial-to-parallel.png) -`CompletableFuture` 同时实现了 `Future` 和 `CompletionStage` 接口。 +对于存在前后调用顺序关系的任务,可以进行任务编排。 + +![](https://oss.javaguide.cn/github/javaguide/high-performance/serial-to-parallel2.png) + +1. 获取用户信息之后,才能调用商品详情和物流信息接口。 +2. 成功获取商品详情和物流信息之后,才能调用商品推荐接口。 + +可能会用到多线程异步任务编排的场景(这里只是举例,数据不一定是一次返回,可能会对接口进行拆分): + +1. 首页:例如技术社区的首页可能需要同时获取文章推荐列表、广告栏、文章排行榜、热门话题等信息。 +2. 详情页:例如技术社区的文章详情页可能需要同时获取作者信息、文章详情、文章评论等信息。 +3. 统计模块:例如技术社区的后台统计模块可能需要同时获取粉丝数汇总、文章数据(阅读量、评论量、收藏量)汇总等信息。 + +对于 Java 程序来说,Java 8 才被引入的 `CompletableFuture` 可以帮助我们来做多个任务的编排,功能非常强大。 + +这篇文章是 `CompletableFuture` 的简单入门,带大家看看 `CompletableFuture` 常用的 API。 + +## Future 介绍 + +`Future` 类是异步思想的典型运用,主要用在一些需要执行耗时任务的场景,避免程序一直原地等待耗时任务执行完成,执行效率太低。具体来说是这样的:当我们执行某一耗时的任务时,可以将这个耗时任务交给一个子线程去异步执行,同时我们可以干点其他事情,不用傻傻等待耗时任务执行完成。等我们的事情干完后,我们再通过 `Future` 类获取到耗时任务的执行结果。这样一来,程序的执行效率就明显提高了。 + +这其实就是多线程中经典的 **Future 模式**,你可以将其看作是一种设计模式,核心思想是异步调用,主要用在多线程领域,并非 Java 语言独有。 + +在 Java 中,`Future` 类只是一个泛型接口,位于 `java.util.concurrent` 包下,其中定义了 5 个方法,主要包括下面这 4 个功能: + +- 取消任务; +- 判断任务是否被取消; +- 判断任务是否已经执行完成; +- 获取任务执行结果。 + +```java +// V 代表了Future执行的任务返回值的类型 +public interface Future { + // 取消任务执行 + // 成功取消返回 true,否则返回 false + boolean cancel(boolean mayInterruptIfRunning); + // 判断任务是否被取消 + boolean isCancelled(); + // 判断任务是否已经执行完成 + boolean isDone(); + // 获取任务执行结果 + V get() throws InterruptedException, ExecutionException; + // 指定时间内没有返回计算结果就抛出 TimeOutException 异常 + V get(long timeout, TimeUnit unit) + + throws InterruptedException, ExecutionException, TimeoutExceptio + +} +``` + +简单理解就是:我有一个任务,提交给了 `Future` 来处理。任务执行期间我自己可以去做任何想做的事情。并且,在这期间我还可以取消任务以及获取任务的执行状态。一段时间之后,我就可以 `Future` 那里直接取出任务执行结果。 + +## CompletableFuture 介绍 + +`Future` 在实际使用过程中存在一些局限性,比如不支持异步任务的编排组合、获取计算结果的 `get()` 方法为阻塞调用。 + +Java 8 才被引入`CompletableFuture` 类可以解决`Future` 的这些缺陷。`CompletableFuture` 除了提供了更为好用和强大的 `Future` 特性之外,还提供了函数式编程、异步任务编排组合(可以将多个异步任务串联起来,组成一个完整的链式调用)等能力。 + +下面我们来简单看看 `CompletableFuture` 类的定义。 ```java public class CompletableFuture implements Future, CompletionStage { } ``` +可以看到,`CompletableFuture` 同时实现了 `Future` 和 `CompletionStage` 接口。 + ![](https://oss.javaguide.cn/github/javaguide/java/concurrent/completablefuture-class-diagram.jpg) +`CompletionStage` 接口描述了一个异步计算的阶段。很多计算可以分成多个阶段或步骤,此时可以通过它将所有步骤组合起来,形成异步计算的流水线。 + `CompletableFuture` 除了提供了更为好用和强大的 `Future` 特性之外,还提供了函数式编程的能力。 ![](https://oss.javaguide.cn/javaguide/image-20210902092441434.png) @@ -40,7 +102,7 @@ public class CompletableFuture implements Future, CompletionStage { 由于方法众多,所以这里不能一一讲解,下文中我会介绍大部分常见方法的使用。 -## 常见操作 +## CompletableFuture 常见操作 ### 创建 CompletableFuture @@ -374,7 +436,7 @@ completableFuture.get(); // ExecutionException ### 组合 CompletableFuture -你可以使用 `thenCompose()` 按顺序链接两个 `CompletableFuture` 对象。 +你可以使用 `thenCompose()` 按顺序链接两个 `CompletableFuture` 对象,实现异步的任务链。它的作用是将前一个任务的返回结果作为下一个任务的输入参数,从而形成一个依赖关系。 ```java public CompletableFuture thenCompose( @@ -403,9 +465,9 @@ CompletableFuture future assertEquals("hello!world!", future.get()); ``` -在实际开发中,这个方法还是非常有用的。比如说,我们先要获取用户信息然后再用用户信息去做其他事情。 +在实际开发中,这个方法还是非常有用的。比如说,task1 和 task2 都是异步执行的,但 task1 必须执行完成后才能开始执行 task2(task2 依赖 task1 的执行结果)。 -和 `thenCompose()` 方法类似的还有 `thenCombine()` 方法, `thenCombine()` 同样可以组合两个 `CompletableFuture` 对象。 +和 `thenCompose()` 方法类似的还有 `thenCombine()` 方法, 它同样可以组合两个 `CompletableFuture` 对象。 ```java CompletableFuture completableFuture @@ -418,9 +480,76 @@ assertEquals("hello!world!nice!", completableFuture.get()); **那 `thenCompose()` 和 `thenCombine()` 有什么区别呢?** -- `thenCompose()` 可以两个 `CompletableFuture` 对象,并将前一个任务的返回结果作为下一个任务的参数,它们之间存在着先后顺序。 +- `thenCompose()` 可以链接两个 `CompletableFuture` 对象,并将前一个任务的返回结果作为下一个任务的参数,它们之间存在着先后顺序。 - `thenCombine()` 会在两个任务都执行完成后,把两个任务的结果合并。两个任务是并行执行的,它们之间并没有先后依赖顺序。 +除了 `thenCompose()` 和 `thenCombine()` 之外, 还有一些其他的组合 `CompletableFuture` 的方法用于实现不同的效果,满足不同的业务需求。 + +例如,如果我们想要实现 task1 和 task2 中的任意一个任务执行完后就执行 task3 的话,可以使用 `acceptEither()`。 + +```java +public CompletableFuture acceptEither( + CompletionStage other, Consumer action) { + return orAcceptStage(null, other, action); +} + +public CompletableFuture acceptEitherAsync( + CompletionStage other, Consumer action) { + return orAcceptStage(asyncPool, other, action); +} +``` + +简单举一个例子: + +```java +CompletableFuture task = CompletableFuture.supplyAsync(() -> { + System.out.println("任务1开始执行,当前时间:" + System.currentTimeMillis()); + try { + Thread.sleep(500); + } catch (InterruptedException e) { + e.printStackTrace(); + } + System.out.println("任务1执行完毕,当前时间:" + System.currentTimeMillis()); + return "task1"; +}); + +CompletableFuture task2 = CompletableFuture.supplyAsync(() -> { + System.out.println("任务2开始执行,当前时间:" + System.currentTimeMillis()); + try { + Thread.sleep(1000); + } catch (InterruptedException e) { + e.printStackTrace(); + } + System.out.println("任务2执行完毕,当前时间:" + System.currentTimeMillis()); + return "task2"; +}); + +task.acceptEitherAsync(task2, (res) -> { + System.out.println("任务3开始执行,当前时间:" + System.currentTimeMillis()); + System.out.println("上一个任务的结果为:" + res); +}); + +// 增加一些延迟时间,确保异步任务有足够的时间完成 +try { + Thread.sleep(2000); +} catch (InterruptedException e) { + e.printStackTrace(); +} +``` + +输出: + +```plain +任务1开始执行,当前时间:1695088058520 +任务2开始执行,当前时间:1695088058521 +任务1执行完毕,当前时间:1695088059023 +任务3开始执行,当前时间:1695088059023 +上一个任务的结果为:task1 +任务2执行完毕,当前时间:1695088059523 +``` + +任务组合操作`acceptEitherAsync()`会在异步任务 1 和异步任务 2 中的任意一个完成时触发执行任务 3,但是需要注意,这个触发时机是不确定的。如果任务 1 和任务 2 都还未完成,那么任务 3 就不能被执行。 + ### 并行运行多个 CompletableFuture 你可以通过 `CompletableFuture` 的 `allOf()`这个静态方法来并行运行多个 `CompletableFuture` 。 @@ -491,7 +620,7 @@ System.out.println("all futures done..."); 输出: -```java +```plain future1 done... future2 done... all futures done... @@ -506,22 +635,98 @@ System.out.println(f.get()); 输出结果可能是: -```java +```plain future2 done... efg ``` 也可能是: -``` +```plain future1 done... abc ``` +## CompletableFuture 使用建议 + +### 使用自定义线程池 + +我们上面的代码示例中,为了方便,都没有选择自定义线程池。实际项目中,这是不可取的。 + +`CompletableFuture` 默认使用全局共享的 `ForkJoinPool.commonPool()` 作为执行器,所有未指定执行器的异步任务都会使用该线程池。这意味着应用程序、多个库或框架(如 Spring、第三方库)若都依赖 `CompletableFuture`,默认情况下它们都会共享同一个线程池。 + +虽然 `ForkJoinPool` 效率很高,但当同时提交大量任务时,可能会导致资源竞争和线程饥饿,进而影响系统性能。 + +为避免这些问题,建议为 `CompletableFuture` 提供自定义线程池,带来以下优势: + +- **隔离性**:为不同任务分配独立的线程池,避免全局线程池资源争夺。 +- **资源控制**:根据任务特性调整线程池大小和队列类型,优化性能表现。 +- **异常处理**:通过自定义 `ThreadFactory` 更好地处理线程中的异常情况。 + +```java +private ThreadPoolExecutor executor = new ThreadPoolExecutor(10, 10, + 0L, TimeUnit.MILLISECONDS, + new LinkedBlockingQueue()); + +CompletableFuture.runAsync(() -> { + //... +}, executor); +``` + +### 尽量避免使用 get() + +`CompletableFuture`的`get()`方法是阻塞的,尽量避免使用。如果必须要使用的话,需要添加超时时间,否则可能会导致主线程一直等待,无法执行其他任务。 + +```java + CompletableFuture future = CompletableFuture.supplyAsync(() -> { + try { + Thread.sleep(10_000); + } catch (InterruptedException e) { + e.printStackTrace(); + } + return "Hello, world!"; + }); + + // 获取异步任务的返回值,设置超时时间为 5 秒 + try { + String result = future.get(5, TimeUnit.SECONDS); + System.out.println(result); + } catch (InterruptedException | ExecutionException | TimeoutException e) { + // 处理异常 + e.printStackTrace(); + } +} +``` + +上面这段代码在调用 `get()` 时抛出了 `TimeoutException` 异常。这样我们就可以在异常处理中进行相应的操作,比如取消任务、重试任务、记录日志等。 + +### 正确进行异常处理 + +使用 `CompletableFuture`的时候一定要以正确的方式进行异常处理,避免异常丢失或者出现不可控问题。 + +下面是一些建议: + +- 使用 `whenComplete` 方法可以在任务完成时触发回调函数,并正确地处理异常,而不是让异常被吞噬或丢失。 +- 使用 `exceptionally` 方法可以处理异常并重新抛出,以便异常能够传播到后续阶段,而不是让异常被忽略或终止。 +- 使用 `handle` 方法可以处理正常的返回结果和异常,并返回一个新的结果,而不是让异常影响正常的业务逻辑。 +- 使用 `CompletableFuture.allOf` 方法可以组合多个 `CompletableFuture`,并统一处理所有任务的异常,而不是让异常处理过于冗长或重复。 +- …… + +### 合理组合多个异步任务 + +正确使用 `thenCompose()` 、 `thenCombine()` 、`acceptEither()`、`allOf()`、`anyOf()`等方法来组合多个异步任务,以满足实际业务的需求,提高程序执行效率。 + +实际使用中,我们还可以利用或者参考现成的异步任务编排框架,比如京东的 [asyncTool](https://gitee.com/jd-platform-opensource/asyncTool) 。 + +![asyncTool README 文档](https://oss.javaguide.cn/github/javaguide/java/concurrent/asyncTool-readme.png) + ## 后记 -这篇文章只是简单介绍了 `CompletableFuture` 比较常用的一些 API 。 +这篇文章只是简单介绍了 `CompletableFuture` 的核心概念和比较常用的一些 API 。如果想要深入学习的话,还可以多找一些书籍和博客看,比如下面几篇文章就挺不错: -如果想要深入学习的话,可以多找一些书籍和博客看。 +- [CompletableFuture 原理与实践-外卖商家端 API 的异步化 - 美团技术团队](https://tech.meituan.com/2022/05/12/principles-and-practices-of-completablefuture.html):这篇文章详细介绍了 `CompletableFuture` 在实际项目中的运用。参考这篇文章,可以对项目中类似的场景进行优化,也算是一个小亮点了。这种性能优化方式比较简单且效果还不错! +- [读 RocketMQ 源码,学习并发编程三大神器 - 勇哥 java 实战分享](https://mp.weixin.qq.com/s/32Ak-WFLynQfpn0Cg0N-0A):这篇文章介绍了 RocketMQ 对`CompletableFuture`的应用。具体来说,从 RocketMQ 4.7 开始,RocketMQ 引入了 `CompletableFuture`来实现异步消息处理 。 另外,建议 G 友们可以看看京东的 [asyncTool](https://gitee.com/jd-platform-opensource/asyncTool) 这个并发框架,里面大量使用到了 `CompletableFuture` 。 + + diff --git "a/docs/java/concurrent/images/java-thread-pool-summary/\347\272\277\347\250\213\346\261\240\345\220\204\344\270\252\345\217\202\346\225\260\344\271\213\351\227\264\347\232\204\345\205\263\347\263\273.png" b/docs/java/concurrent/images/java-thread-pool-summary/relationship-between-thread-pool-parameters.png similarity index 100% rename from "docs/java/concurrent/images/java-thread-pool-summary/\347\272\277\347\250\213\346\261\240\345\220\204\344\270\252\345\217\202\346\225\260\344\271\213\351\227\264\347\232\204\345\205\263\347\263\273.png" rename to docs/java/concurrent/images/java-thread-pool-summary/relationship-between-thread-pool-parameters.png diff --git "a/docs/java/concurrent/images/java-thread-pool-summary/threadpoolexecutor\346\236\204\351\200\240\345\207\275\346\225\260.png" "b/docs/java/concurrent/images/java-thread-pool-summary/threadpoolexecutor\346\236\204\351\200\240\345\207\275\346\225\260.png" deleted file mode 100644 index 6e3c7082eed..00000000000 Binary files "a/docs/java/concurrent/images/java-thread-pool-summary/threadpoolexecutor\346\236\204\351\200\240\345\207\275\346\225\260.png" and /dev/null differ diff --git a/docs/java/concurrent/java-concurrent-collections.md b/docs/java/concurrent/java-concurrent-collections.md index 2acc2932770..45aa258818a 100644 --- a/docs/java/concurrent/java-concurrent-collections.md +++ b/docs/java/concurrent/java-concurrent-collections.md @@ -15,82 +15,37 @@ JDK 提供的这些容器大部分在 `java.util.concurrent` 包中。 ## ConcurrentHashMap -我们知道 `HashMap` 不是线程安全的,在并发场景下如果要保证一种可行的方式是使用 `Collections.synchronizedMap()` 方法来包装我们的 `HashMap`。但这是通过使用一个全局的锁来同步不同线程间的并发访问,因此会带来不可忽视的性能问题。 +我们知道,`HashMap` 是线程不安全的,如果在并发场景下使用,一种常见的解决方式是通过 `Collections.synchronizedMap()` 方法对 `HashMap` 进行包装,使其变为线程安全。不过,这种方式是通过一个全局锁来同步不同线程间的并发访问,会导致严重的性能瓶颈,尤其是在高并发场景下。 -所以就有了 `HashMap` 的线程安全版本—— `ConcurrentHashMap` 的诞生。 +为了解决这一问题,`ConcurrentHashMap` 应运而生,作为 `HashMap` 的线程安全版本,它提供了更高效的并发处理能力。 在 JDK1.7 的时候,`ConcurrentHashMap` 对整个桶数组进行了分割分段(`Segment`,分段锁),每一把锁只锁容器其中一部分数据(下面有示意图),多线程访问容器里不同数据段的数据,就不会存在锁竞争,提高并发访问率。 -到了 JDK1.8 的时候,`ConcurrentHashMap` 已经摒弃了 `Segment` 的概念,而是直接用 `Node` 数组+链表+红黑树的数据结构来实现,并发控制使用 `synchronized` 和 CAS 来操作。(JDK1.6 以后 `synchronized` 锁做了很多优化) 整个看起来就像是优化过且线程安全的 `HashMap`,虽然在 JDK1.8 中还能看到 `Segment` 的数据结构,但是已经简化了属性,只是为了兼容旧版本。 +![Java7 ConcurrentHashMap 存储结构](https://oss.javaguide.cn/github/javaguide/java/collection/java7_concurrenthashmap.png) -## CopyOnWriteArrayList - -### CopyOnWriteArrayList 简介 - -```java -public class CopyOnWriteArrayList -extends Object -implements List, RandomAccess, Cloneable, Serializable -``` +到了 JDK1.8 的时候,`ConcurrentHashMap` 取消了 `Segment` 分段锁,采用 `Node + CAS + synchronized` 来保证并发安全。数据结构跟 `HashMap` 1.8 的结构类似,数组+链表/红黑二叉树。Java 8 在链表长度超过一定阈值(8)时将链表(寻址时间复杂度为 O(N))转换为红黑树(寻址时间复杂度为 O(log(N)))。 -在很多应用场景中,读操作可能会远远大于写操作。由于读操作根本不会修改原有的数据,因此对于每次读取都进行加锁其实是一种资源浪费。我们应该允许多个线程同时访问 `List` 的内部数据,毕竟读取操作是安全的。 +Java 8 中,锁粒度更细,`synchronized` 只锁定当前链表或红黑二叉树的首节点,这样只要 hash 不冲突,就不会产生并发,就不会影响其他 Node 的读写,效率大幅提升。 -这和我们之前提到过的 `ReentrantReadWriteLock` 读写锁的思想非常类似,也就是读读共享、写写互斥、读写互斥、写读互斥。JDK 中提供了 `CopyOnWriteArrayList` 类比相比于在读写锁的思想又更进一步。为了将读取的性能发挥到极致,`CopyOnWriteArrayList` 读取是完全不用加锁的,并且更厉害的是:写入也不会阻塞读取操作。只有写入和写入之间需要进行同步等待。这样一来,读操作的性能就会大幅度提升。**那它是怎么做的呢?** +![Java8 ConcurrentHashMap 存储结构](https://oss.javaguide.cn/github/javaguide/java/collection/java8_concurrenthashmap.png) -### CopyOnWriteArrayList 是如何做到的? +关于 `ConcurrentHashMap` 的详细介绍,请看我写的这篇文章:[`ConcurrentHashMap` 源码分析](./../collection/concurrent-hash-map-source-code.md)。 -`CopyOnWriteArrayList` 类的所有可变操作(add,set 等等)都是通过创建底层数组的新副本来实现的。当 List 需要被修改的时候,我并不修改原有内容,而是对原有数据进行一次复制,将修改的内容写入副本。写完之后,再将修改完的副本替换原来的数据,这样就可以保证写操作不会影响读操作了。 - -从 `CopyOnWriteArrayList` 的名字就能看出 `CopyOnWriteArrayList` 是满足 `CopyOnWrite` 的。所谓 `CopyOnWrite` 也就是说:在计算机,如果你想要对一块内存进行修改时,我们不在原有内存块中进行写操作,而是将内存拷贝一份,在新的内存中进行写操作,写完之后呢,就将指向原来内存指针指向新的内存,原来的内存就可以被回收掉了。 - -### CopyOnWriteArrayList 读取和写入源码简单分析 +## CopyOnWriteArrayList -#### CopyOnWriteArrayList 读取操作的实现 +在 JDK1.5 之前,如果想要使用并发安全的 `List` 只能选择 `Vector`。而 `Vector` 是一种老旧的集合,已经被淘汰。`Vector` 对于增删改查等方法基本都加了 `synchronized`,这种方式虽然能够保证同步,但这相当于对整个 `Vector` 加上了一把大锁,使得每个方法执行的时候都要去获得锁,导致性能非常低下。 -读取操作没有任何同步控制和锁操作,理由就是内部数组 `array` 不会发生修改,只会被另外一个 `array` 替换,因此可以保证数据安全。 +JDK1.5 引入了 `Java.util.concurrent`(JUC)包,其中提供了很多线程安全且并发性能良好的容器,其中唯一的线程安全 `List` 实现就是 `CopyOnWriteArrayList` 。 -```java - /** The array, accessed only via getArray/setArray. */ - private transient volatile Object[] array; - public E get(int index) { - return get(getArray(), index); - } - @SuppressWarnings("unchecked") - private E get(Object[] a, int index) { - return (E) a[index]; - } - final Object[] getArray() { - return array; - } +对于大部分业务场景来说,读取操作往往是远大于写入操作的。由于读取操作不会对原有数据进行修改,因此,对于每次读取都进行加锁其实是一种资源浪费。相比之下,我们应该允许多个线程同时访问 `List` 的内部数据,毕竟对于读取操作来说是安全的。 -``` +这种思路与 `ReentrantReadWriteLock` 读写锁的设计思想非常类似,即读读不互斥、读写互斥、写写互斥(只有读读不互斥)。`CopyOnWriteArrayList` 更进一步地实现了这一思想。为了将读操作性能发挥到极致,`CopyOnWriteArrayList` 中的读取操作是完全无需加锁的。更加厉害的是,写入操作也不会阻塞读取操作,只有写写才会互斥。这样一来,读操作的性能就可以大幅度提升。 -#### CopyOnWriteArrayList 写入操作的实现 +`CopyOnWriteArrayList` 线程安全的核心在于其采用了 **写时复制(Copy-On-Write)** 的策略,从 `CopyOnWriteArrayList` 的名字就能看出了。 -`CopyOnWriteArrayList` 写入操作 `add()`方法在添加集合的时候加了锁,保证了同步,避免了多线程写的时候会 copy 出多个副本出来。 +当需要修改( `add`,`set`、`remove` 等操作) `CopyOnWriteArrayList` 的内容时,不会直接修改原数组,而是会先创建底层数组的副本,对副本数组进行修改,修改完之后再将修改后的数组赋值回去,这样就可以保证写操作不会影响读操作了。 -```java - /** - * Appends the specified element to the end of this list. - * - * @param e element to be appended to this list - * @return {@code true} (as specified by {@link Collection#add}) - */ - public boolean add(E e) { - final ReentrantLock lock = this.lock; - lock.lock();//加锁 - try { - Object[] elements = getArray(); - int len = elements.length; - Object[] newElements = Arrays.copyOf(elements, len + 1);//拷贝新数组 - newElements[len] = e; - setArray(newElements); - return true; - } finally { - lock.unlock();//释放锁 - } - } -``` +关于 `CopyOnWriteArrayList` 的详细介绍,请看我写的这篇文章:[`CopyOnWriteArrayList` 源码分析](./../collection/copyonwritearraylist-source-code.md)。 ## ConcurrentLinkedQueue @@ -110,7 +65,7 @@ Java 提供的线程安全的 `Queue` 可以分为**阻塞队列**和**非阻塞 `BlockingQueue` 是一个接口,继承自 `Queue`,所以其实现类也可以作为 `Queue` 的实现来使用,而 `Queue` 又继承自 `Collection` 接口。下面是 `BlockingQueue` 的相关实现类: -![BlockingQueue 的实现类](https://my-blog-to-use.oss-cn-beijing.aliyuncs.com/18-12-9/51622268.jpg) +![BlockingQueue 的实现类](https://oss.javaguide.cn/github/javaguide/java/51622268.jpg) 下面主要介绍一下 3 个常见的 `BlockingQueue` 的实现类:`ArrayBlockingQueue`、`LinkedBlockingQueue`、`PriorityBlockingQueue` 。 @@ -183,13 +138,13 @@ private static ArrayBlockingQueue blockingQueue = new ArrayBlockingQueu 跳表的本质是同时维护了多个链表,并且链表是分层的, -![2级索引跳表](https://my-blog-to-use.oss-cn-beijing.aliyuncs.com/18-12-9/93666217.jpg) +![2级索引跳表](https://oss.javaguide.cn/github/javaguide/java/93666217.jpg) 最低层的链表维护了跳表内所有的元素,每上面一层链表都是下面一层的子集。 跳表内的所有链表的元素都是排序的。查找时,可以从顶级链表开始找。一旦发现被查找的元素大于当前链表中的取值,就会转入下一层链表继续找。这也就是说在查找过程中,搜索是跳跃式的。如上图所示,在跳表中查找元素 18。 -![在跳表中查找元素18](https://my-blog-to-use.oss-cn-beijing.aliyuncs.com/18-12-9/32005738.jpg) +![在跳表中查找元素18](https://oss.javaguide.cn/github/javaguide/java/32005738.jpg) 查找 18 的时候原来需要遍历 18 次,现在只需要 7 次即可。针对链表长度比较大的时候,构建索引查找效率的提升就会非常明显。 @@ -200,5 +155,7 @@ private static ArrayBlockingQueue blockingQueue = new ArrayBlockingQueu ## 参考 - 《实战 Java 高并发程序设计》 -- https://javadoop.com/post/java-concurrent-queue -- https://juejin.im/post/5aeebd02518825672f19c546 +- +- + + diff --git a/docs/java/concurrent/java-concurrent-questions-01.md b/docs/java/concurrent/java-concurrent-questions-01.md index e7b137c164a..e1768d04d45 100644 --- a/docs/java/concurrent/java-concurrent-questions-01.md +++ b/docs/java/concurrent/java-concurrent-questions-01.md @@ -14,9 +14,11 @@ head: -## 什么是线程和进程? +## 线程 -### 何为进程? +### ⭐️什么是线程和进程? + +#### 何为进程? 进程是程序的一次执行过程,是系统运行程序的基本单位,因此进程是动态的。系统运行一个程序即是一个进程从创建,运行到消亡的过程。 @@ -24,11 +26,11 @@ head: 如下图所示,在 Windows 中通过查看任务管理器的方式,我们就可以清楚看到 Windows 当前运行的进程(`.exe` 文件的运行)。 -![进程示例图片-Windows](https://my-blog-to-use.oss-cn-beijing.aliyuncs.com/2019-6/进程示例图片-Windows.png) +![进程示例图片-Windows](https://oss.javaguide.cn/github/javaguide/java/%E8%BF%9B%E7%A8%8B%E7%A4%BA%E4%BE%8B%E5%9B%BE%E7%89%87-Windows.png) -### 何为线程? +#### 何为线程? -线程与进程相似,但线程是一个比进程更小的执行单位。一个进程在其执行的过程中可以产生多个线程。与进程不同的是同类的多个线程共享进程的**堆**和**方法区**资源,但每个线程有自己的**程序计数器**、**虚拟机栈**和**本地方法栈**,所以系统在产生一个线程,或是在各个线程之间作切换工作时,负担要比进程小得多,也正因为如此,线程也被称为轻量级进程。 +线程与进程相似,但线程是一个比进程更小的执行单位。一个进程在其执行的过程中可以产生多个线程。与进程不同的是同类的多个线程共享进程的**堆**和**方法区**资源,但每个线程有自己的**程序计数器**、**虚拟机栈**和**本地方法栈**,所以系统在产生一个线程,或是在各个线程之间做切换工作时,负担要比进程小得多,也正因为如此,线程也被称为轻量级进程。 Java 程序天生就是多线程程序,我们可以通过 JMX 来看看一个普通的 Java 程序有哪些线程,代码如下。 @@ -49,7 +51,7 @@ public class MultiThread { 上述程序输出如下(输出内容可能不同,不用太纠结下面每个线程的作用,只用知道 main 线程执行 main 方法即可): -``` +```plain [5] Attach Listener //添加事件 [4] Signal Dispatcher // 分发处理给 JVM 信号的线程 [3] Finalizer //调用对象 finalize 方法的线程 @@ -59,11 +61,30 @@ public class MultiThread { 从上面的输出内容可以看出:**一个 Java 程序的运行是 main 线程和多个其他线程同时运行**。 -## 请简要描述线程与进程的关系,区别及优缺点? +### Java 线程和操作系统的线程有啥区别? + +JDK 1.2 之前,Java 线程是基于绿色线程(Green Threads)实现的,这是一种用户级线程(用户线程),也就是说 JVM 自己模拟了多线程的运行,而不依赖于操作系统。由于绿色线程和原生线程比起来在使用时有一些限制(比如绿色线程不能直接使用操作系统提供的功能如异步 I/O、只能在一个内核线程上运行无法利用多核),在 JDK 1.2 及以后,Java 线程改为基于原生线程(Native Threads)实现,也就是说 JVM 直接使用操作系统原生的内核级线程(内核线程)来实现 Java 线程,由操作系统内核进行线程的调度和管理。 + +我们上面提到了用户线程和内核线程,考虑到很多读者不太了解二者的区别,这里简单介绍一下: + +- 用户线程:由用户空间程序管理和调度的线程,运行在用户空间(专门给应用程序使用)。 +- 内核线程:由操作系统内核管理和调度的线程,运行在内核空间(只有内核程序可以访问)。 + +顺便简单总结一下用户线程和内核线程的区别和特点:用户线程创建和切换成本低,但不可以利用多核。内核态线程,创建和切换成本高,可以利用多核。 + +一句话概括 Java 线程和操作系统线程的关系:**现在的 Java 线程的本质其实就是操作系统的线程**。 + +线程模型是用户线程和内核线程之间的关联方式,常见的线程模型有这三种: -从 JVM 角度说进程和线程之间的关系。 +1. 一对一(一个用户线程对应一个内核线程) +2. 多对一(多个用户线程映射到一个内核线程) +3. 多对多(多个用户线程映射到多个内核线程) -### 图解进程和线程的关系 +![常见的三种线程模型](https://oss.javaguide.cn/github/javaguide/java/concurrent/three-types-of-thread-models.png) + +在 Windows 和 Linux 等主流操作系统中,Java 线程采用的是一对一的线程模型,也就是一个 Java 线程对应一个系统内核线程。Solaris 系统是一个特例(Solaris 系统本身就支持多对多的线程模型),HotSpot VM 在 Solaris 上支持多对多和一对一。具体可以参考 R 大的回答: [JVM 中的线程模型是用户级的么?](https://www.zhihu.com/question/23096638/answer/29617153)。 + +### ⭐️请简要描述线程与进程的关系,区别及优缺点? 下图是 Java 内存区域,通过下图我们从 JVM 的角度来说一下线程和进程之间的关系。 @@ -77,7 +98,7 @@ public class MultiThread { 下面来思考这样一个问题:为什么**程序计数器**、**虚拟机栈**和**本地方法栈**是线程私有的呢?为什么堆和方法区是线程共享的呢? -### 程序计数器为什么是私有的? +#### 程序计数器为什么是私有的? 程序计数器主要有下面两个作用: @@ -88,53 +109,28 @@ public class MultiThread { 所以,程序计数器私有主要是为了**线程切换后能恢复到正确的执行位置**。 -### 虚拟机栈和本地方法栈为什么是私有的? +#### 虚拟机栈和本地方法栈为什么是私有的? - **虚拟机栈:** 每个 Java 方法在执行之前会创建一个栈帧用于存储局部变量表、操作数栈、常量池引用等信息。从方法调用直至执行完成的过程,就对应着一个栈帧在 Java 虚拟机栈中入栈和出栈的过程。 - **本地方法栈:** 和虚拟机栈所发挥的作用非常相似,区别是:**虚拟机栈为虚拟机执行 Java 方法 (也就是字节码)服务,而本地方法栈则为虚拟机使用到的 Native 方法服务。** 在 HotSpot 虚拟机中和 Java 虚拟机栈合二为一。 所以,为了**保证线程中的局部变量不被别的线程访问到**,虚拟机栈和本地方法栈是线程私有的。 -### 一句话简单了解堆和方法区 +#### 一句话简单了解堆和方法区 堆和方法区是所有线程共享的资源,其中堆是进程中最大的一块内存,主要用于存放新创建的对象 (几乎所有对象都在这里分配内存),方法区主要用于存放已被加载的类信息、常量、静态变量、即时编译器编译后的代码等数据。 -## 并发与并行的区别 - -- **并发**:两个及两个以上的作业在同一 **时间段** 内执行。 -- **并行**:两个及两个以上的作业在同一 **时刻** 执行。 - -最关键的点是:是否是 **同时** 执行。 - -## 同步和异步的区别 +### 如何创建线程? -- **同步**:发出一个调用之后,在没有得到结果之前, 该调用就不可以返回,一直等待。 -- **异步**:调用在发出之后,不用等待返回结果,该调用直接返回。 - -## 为什么要使用多线程? - -先从总体上来说: - -- **从计算机底层来说:** 线程可以比作是轻量级的进程,是程序执行的最小单位,线程间的切换和调度的成本远远小于进程。另外,多核 CPU 时代意味着多个线程可以同时运行,这减少了线程上下文切换的开销。 -- **从当代互联网发展趋势来说:** 现在的系统动不动就要求百万级甚至千万级的并发量,而多线程并发编程正是开发高并发系统的基础,利用好多线程机制可以大大提高系统整体的并发能力以及性能。 - -再深入到计算机底层来探讨: - -- **单核时代**:在单核时代多线程主要是为了提高单进程利用 CPU 和 IO 系统的效率。 假设只运行了一个 Java 进程的情况,当我们请求 IO 的时候,如果 Java 进程中只有一个线程,此线程被 IO 阻塞则整个进程被阻塞。CPU 和 IO 设备只有一个在运行,那么可以简单地说系统整体效率只有 50%。当使用多线程的时候,一个线程被 IO 阻塞,其他线程还可以继续使用 CPU。从而提高了 Java 进程利用系统资源的整体效率。 -- **多核时代**: 多核时代多线程主要是为了提高进程利用多核 CPU 的能力。举个例子:假如我们要计算一个复杂的任务,我们只用一个线程的话,不论系统有几个 CPU 核心,都只会有一个 CPU 核心被利用到。而创建多个线程,这些线程可以被映射到底层多个 CPU 上执行,在任务中的多个线程没有资源竞争的情况下,任务执行的效率会有显著性的提高,约等于(单核时执行时间/CPU 核心数)。 - -## 使用多线程可能带来什么问题? +一般来说,创建线程有很多种方式,例如继承`Thread`类、实现`Runnable`接口、实现`Callable`接口、使用线程池、使用`CompletableFuture`类等等。 -并发编程的目的就是为了能提高程序的执行效率提高程序运行速度,但是并发编程并不总是能提高程序运行速度的,而且并发编程可能会遇到很多问题,比如:内存泄漏、死锁、线程不安全等等。 +不过,这些方式其实并没有真正创建出线程。准确点来说,这些都属于是在 Java 代码中使用多线程的方法。 -## 如何理解线程安全和不安全? +严格来说,Java 就只有一种方式可以创建线程,那就是通过`new Thread().start()`创建。不管是哪种方式,最终还是依赖于`new Thread().start()`。 -线程安全和不安全是在多线程环境下对于同一份数据的访问是否能够保证其正确性和一致性的描述。 - -- 线程安全指的是在多线程环境下,对于同一份数据,不管有多少个线程同时访问,都能保证这份数据的正确性和一致性。 -- 线程不安全则表示在多线程环境下,对于同一份数据,多个线程同时访问时可能会导致数据混乱、错误或者丢失。 +关于这个问题的详细分析可以查看这篇文章:[大家都说 Java 有三种创建线程的方式!并发编程中的惊天骗局!](https://mp.weixin.qq.com/s/NspUsyhEmKnJ-4OprRFp9g)。 -## 说说线程的生命周期和状态? +### ⭐️说说线程的生命周期和状态? Java 线程在运行的生命周期中的指定时刻只可能处于下面 6 种不同状态的其中一个状态: @@ -157,7 +153,7 @@ Java 线程状态变迁图(图源:[挑错 |《Java 并发编程的艺术》中 > > **为什么 JVM 没有区分这两种状态呢?** (摘自:[Java 线程运行怎么有第六种状态? - Dawell 的回答](https://www.zhihu.com/question/56494969/answer/154053599) ) 现在的时分(time-sharing)多任务(multi-task)操作系统架构通常都是用所谓的“时间分片(time quantum or time slice)”方式进行抢占式(preemptive)轮转调度(round-robin 式)。这个时间分片通常是很小的,一个线程一次最多只能在 CPU 上运行比如 10-20ms 的时间(此时处于 running 状态),也即大概只有 0.01 秒这一量级,时间片用后就要被切换下来放入调度队列的末尾等待再次调度。(也即回到 ready 状态)。线程切换的如此之快,区分这两种状态就没什么意义了。 -![RUNNABLE-VS-RUNNING](https://my-blog-to-use.oss-cn-beijing.aliyuncs.com/2019-3/RUNNABLE-VS-RUNNING.png) +![RUNNABLE-VS-RUNNING](https://oss.javaguide.cn/github/javaguide/java/RUNNABLE-VS-RUNNING.png) - 当线程执行 `wait()`方法之后,线程进入 **WAITING(等待)** 状态。进入等待状态的线程需要依靠其他线程的通知才能够返回到运行状态。 - **TIMED_WAITING(超时等待)** 状态相当于在等待状态的基础上增加了超时限制,比如通过 `sleep(long millis)`方法或 `wait(long millis)`方法可以将线程置于 TIMED_WAITING 状态。当超时时间结束后,线程将会返回到 RUNNABLE 状态。 @@ -166,7 +162,7 @@ Java 线程状态变迁图(图源:[挑错 |《Java 并发编程的艺术》中 相关阅读:[线程的几种状态你真的了解么?](https://mp.weixin.qq.com/s/R5MrTsWvk9McFSQ7bS0W2w) 。 -## 什么是线程上下文切换? +### 什么是线程上下文切换? 线程在执行过程中会有自己的运行条件和状态(也称上下文),比如上文所说到过的程序计数器,栈信息等。当出现如下情况的时候,线程会从占用 CPU 状态中退出。 @@ -179,15 +175,103 @@ Java 线程状态变迁图(图源:[挑错 |《Java 并发编程的艺术》中 上下文切换是现代操作系统的基本功能,因其每次需要保存信息恢复信息,这将会占用 CPU,内存等系统资源进行处理,也就意味着效率会有一定损耗,如果频繁切换就会造成整体效率低下。 -## 什么是线程死锁?如何避免死锁? +### Thread#sleep() 方法和 Object#wait() 方法对比 + +**共同点**:两者都可以暂停线程的执行。 + +**区别**: + +- **`sleep()` 方法没有释放锁,而 `wait()` 方法释放了锁** 。 +- `wait()` 通常被用于线程间交互/通信,`sleep()`通常被用于暂停执行。 +- `wait()` 方法被调用后,线程不会自动苏醒,需要别的线程调用同一个对象上的 `notify()`或者 `notifyAll()` 方法。`sleep()`方法执行完成后,线程会自动苏醒,或者也可以使用 `wait(long timeout)` 超时后线程会自动苏醒。 +- `sleep()` 是 `Thread` 类的静态本地方法,`wait()` 则是 `Object` 类的本地方法。为什么这样设计呢?下一个问题就会聊到。 + +### 为什么 wait() 方法不定义在 Thread 中? + +`wait()` 是让获得对象锁的线程实现等待,会自动释放当前线程占有的对象锁。每个对象(`Object`)都拥有对象锁,既然要释放当前线程占有的对象锁并让其进入 WAITING 状态,自然是要操作对应的对象(`Object`)而非当前的线程(`Thread`)。 + +类似的问题:**为什么 `sleep()` 方法定义在 `Thread` 中?** + +因为 `sleep()` 是让当前线程暂停执行,不涉及到对象类,也不需要获得对象锁。 + +### 可以直接调用 Thread 类的 run 方法吗? + +这是另一个非常经典的 Java 多线程面试问题,而且在面试中会经常被问到。很简单,但是很多人都会答不上来! + +new 一个 `Thread`,线程进入了新建状态。调用 `start()`方法,会启动一个线程并使线程进入了就绪状态,当分配到时间片后就可以开始运行了。 `start()` 会执行线程的相应准备工作,然后自动执行 `run()` 方法的内容,这是真正的多线程工作。 但是,直接执行 `run()` 方法,会把 `run()` 方法当成一个 main 线程下的普通方法去执行,并不会在某个线程中执行它,所以这并不是多线程工作。 + +**总结:调用 `start()` 方法方可启动线程并使线程进入就绪状态,直接执行 `run()` 方法的话不会以多线程的方式执行。** + +## 多线程 + +### 并发与并行的区别 + +- **并发**:两个及两个以上的作业在同一 **时间段** 内执行。 +- **并行**:两个及两个以上的作业在同一 **时刻** 执行。 + +最关键的点是:是否是 **同时** 执行。 + +### 同步和异步的区别 + +- **同步**:发出一个调用之后,在没有得到结果之前, 该调用就不可以返回,一直等待。 +- **异步**:调用在发出之后,不用等待返回结果,该调用直接返回。 + +### ⭐️为什么要使用多线程? + +先从总体上来说: + +- **从计算机底层来说:** 线程可以比作是轻量级的进程,是程序执行的最小单位,线程间的切换和调度的成本远远小于进程。另外,多核 CPU 时代意味着多个线程可以同时运行,这减少了线程上下文切换的开销。 +- **从当代互联网发展趋势来说:** 现在的系统动不动就要求百万级甚至千万级的并发量,而多线程并发编程正是开发高并发系统的基础,利用好多线程机制可以大大提高系统整体的并发能力以及性能。 + +再深入到计算机底层来探讨: + +- **单核时代**:在单核时代多线程主要是为了提高单进程利用 CPU 和 IO 系统的效率。 假设只运行了一个 Java 进程的情况,当我们请求 IO 的时候,如果 Java 进程中只有一个线程,此线程被 IO 阻塞则整个进程被阻塞。CPU 和 IO 设备只有一个在运行,那么可以简单地说系统整体效率只有 50%。当使用多线程的时候,一个线程被 IO 阻塞,其他线程还可以继续使用 CPU。从而提高了 Java 进程利用系统资源的整体效率。 +- **多核时代**: 多核时代多线程主要是为了提高进程利用多核 CPU 的能力。举个例子:假如我们要计算一个复杂的任务,我们只用一个线程的话,不论系统有几个 CPU 核心,都只会有一个 CPU 核心被利用到。而创建多个线程,这些线程可以被映射到底层多个 CPU 核心上执行,在任务中的多个线程没有资源竞争的情况下,任务执行的效率会有显著性的提高,约等于(单核时执行时间/CPU 核心数)。 + +### ⭐️单核 CPU 支持 Java 多线程吗? + +单核 CPU 是支持 Java 多线程的。操作系统通过时间片轮转的方式,将 CPU 的时间分配给不同的线程。尽管单核 CPU 一次只能执行一个任务,但通过快速在多个线程之间切换,可以让用户感觉多个任务是同时进行的。 + +这里顺带提一下 Java 使用的线程调度方式。 + +操作系统主要通过两种线程调度方式来管理多线程的执行: + +- **抢占式调度(Preemptive Scheduling)**:操作系统决定何时暂停当前正在运行的线程,并切换到另一个线程执行。这种切换通常是由系统时钟中断(时间片轮转)或其他高优先级事件(如 I/O 操作完成)触发的。这种方式存在上下文切换开销,但公平性和 CPU 资源利用率较好,不易阻塞。 +- **协同式调度(Cooperative Scheduling)**:线程执行完毕后,主动通知系统切换到另一个线程。这种方式可以减少上下文切换带来的性能开销,但公平性较差,容易阻塞。 -### 认识线程死锁 +Java 使用的线程调度是抢占式的。也就是说,JVM 本身不负责线程的调度,而是将线程的调度委托给操作系统。操作系统通常会基于线程优先级和时间片来调度线程的执行,高优先级的线程通常获得 CPU 时间片的机会更多。 + +### ⭐️单核 CPU 上运行多个线程效率一定会高吗? + +单核 CPU 同时运行多个线程的效率是否会高,取决于线程的类型和任务的性质。一般来说,有两种类型的线程: + +1. **CPU 密集型**:CPU 密集型的线程主要进行计算和逻辑处理,需要占用大量的 CPU 资源。 +2. **IO 密集型**:IO 密集型的线程主要进行输入输出操作,如读写文件、网络通信等,需要等待 IO 设备的响应,而不占用太多的 CPU 资源。 + +在单核 CPU 上,同一时刻只能有一个线程在运行,其他线程需要等待 CPU 的时间片分配。如果线程是 CPU 密集型的,那么多个线程同时运行会导致频繁的线程切换,增加了系统的开销,降低了效率。如果线程是 IO 密集型的,那么多个线程同时运行可以利用 CPU 在等待 IO 时的空闲时间,提高了效率。 + +因此,对于单核 CPU 来说,如果任务是 CPU 密集型的,那么开很多线程会影响效率;如果任务是 IO 密集型的,那么开很多线程会提高效率。当然,这里的“很多”也要适度,不能超过系统能够承受的上限。 + +### 使用多线程可能带来什么问题? + +并发编程的目的就是为了能提高程序的执行效率进而提高程序的运行速度,但是并发编程并不总是能提高程序运行速度的,而且并发编程可能会遇到很多问题,比如:内存泄漏、死锁、线程不安全等等。 + +### 如何理解线程安全和不安全? + +线程安全和不安全是在多线程环境下对于同一份数据的访问是否能够保证其正确性和一致性的描述。 + +- 线程安全指的是在多线程环境下,对于同一份数据,不管有多少个线程同时访问,都能保证这份数据的正确性和一致性。 +- 线程不安全则表示在多线程环境下,对于同一份数据,多个线程同时访问时可能会导致数据混乱、错误或者丢失。 + +## ⭐️死锁 + +### 什么是线程死锁? 线程死锁描述的是这样一种情况:多个线程同时被阻塞,它们中的一个或者全部都在等待某个资源被释放。由于线程被无限期地阻塞,因此程序不可能正常终止。 如下图所示,线程 A 持有资源 2,线程 B 持有资源 1,他们同时都想申请对方的资源,所以这两个线程就会互相等待而进入死锁状态。 -![线程死锁示意图 ](https://my-blog-to-use.oss-cn-beijing.aliyuncs.com/2019-4/2019-4%E6%AD%BB%E9%94%811.png) +![线程死锁示意图 ](https://oss.javaguide.cn/github/javaguide/java/2019-4%E6%AD%BB%E9%94%811.png) 下面通过一个例子来说明线程死锁,代码模拟了上图的死锁的情况 (代码来源于《并发编程之美》): @@ -232,21 +316,44 @@ public class DeadLockDemo { Output -``` +```plain Thread[线程 1,5,main]get resource1 Thread[线程 2,5,main]get resource2 Thread[线程 1,5,main]waiting get resource2 Thread[线程 2,5,main]waiting get resource1 ``` -线程 A 通过 `synchronized (resource1)` 获得 `resource1` 的监视器锁,然后通过`Thread.sleep(1000);`让线程 A 休眠 1s 为的是让线程 B 得到执行然后获取到 resource2 的监视器锁。线程 A 和线程 B 休眠结束了都开始企图请求获取对方的资源,然后这两个线程就会陷入互相等待的状态,这也就产生了死锁。 +线程 A 通过 `synchronized (resource1)` 获得 `resource1` 的监视器锁,然后通过 `Thread.sleep(1000);` 让线程 A 休眠 1s,为的是让线程 B 得到执行然后获取到 resource2 的监视器锁。线程 A 和线程 B 休眠结束了都开始企图请求获取对方的资源,然后这两个线程就会陷入互相等待的状态,这也就产生了死锁。 上面的例子符合产生死锁的四个必要条件: -1. 互斥条件:该资源任意一个时刻只由一个线程占用。 -2. 请求与保持条件:一个线程因请求资源而阻塞时,对已获得的资源保持不放。 -3. 不剥夺条件:线程已获得的资源在未使用完之前不能被其他线程强行剥夺,只有自己使用完毕后才释放资源。 -4. 循环等待条件:若干线程之间形成一种头尾相接的循环等待资源关系。 +1. **互斥条件**:该资源任意一个时刻只由一个线程占用。 +2. **请求与保持条件**:一个线程因请求资源而阻塞时,对已获得的资源保持不放。 +3. **不剥夺条件**:线程已获得的资源在未使用完之前不能被其他线程强行剥夺,只有自己使用完毕后才释放资源。 +4. **循环等待条件**:若干线程之间形成一种头尾相接的循环等待资源关系。 + +### 如何检测死锁? + +- 使用`jmap`、`jstack`等命令查看 JVM 线程栈和堆内存的情况。如果有死锁,`jstack` 的输出中通常会有 `Found one Java-level deadlock:`的字样,后面会跟着死锁相关的线程信息。另外,实际项目中还可以搭配使用`top`、`df`、`free`等命令查看操作系统的基本情况,出现死锁可能会导致 CPU、内存等资源消耗过高。 +- 采用 VisualVM、JConsole 等工具进行排查。 + +这里以 JConsole 工具为例进行演示。 + +首先,我们要找到 JDK 的 bin 目录,找到 jconsole 并双击打开。 + +![jconsole](https://oss.javaguide.cn/github/javaguide/java/concurrent/jdk-home-bin-jconsole.png) + +对于 MAC 用户来说,可以通过 `/usr/libexec/java_home -V`查看 JDK 安装目录,找到后通过 `open . + 文件夹地址`打开即可。例如,我本地的某个 JDK 的路径是: + +```bash + open . /Users/guide/Library/Java/JavaVirtualMachines/corretto-1.8.0_252/Contents/Home +``` + +打开 jconsole 后,连接对应的程序,然后进入线程界面选择检测死锁即可! + +![jconsole 检测死锁](https://oss.javaguide.cn/github/javaguide/java/concurrent/jconsole-check-deadlock.png) + +![jconsole 检测到死锁](https://oss.javaguide.cn/github/javaguide/java/concurrent/jconsole-check-deadlock-done.png) ### 如何预防和避免线程死锁? @@ -260,7 +367,7 @@ Thread[线程 2,5,main]waiting get resource1 避免死锁就是在资源分配时,借助于算法(比如银行家算法)对资源分配进行计算评估,使其进入安全状态。 -> **安全状态** 指的是系统能够按照某种线程推进顺序(P1、P2、P3.....Pn)来为每个线程分配所需资源,直到满足每个线程对资源的最大需求,使每个线程都可顺利完成。称 `` 序列为安全序列。 +> **安全状态** 指的是系统能够按照某种线程推进顺序(P1、P2、P3……Pn)来为每个线程分配所需资源,直到满足每个线程对资源的最大需求,使每个线程都可顺利完成。称 `` 序列为安全序列。 我们对线程 2 的代码修改成下面这样就不会产生死锁了。 @@ -283,7 +390,7 @@ new Thread(() -> { 输出: -``` +```plain Thread[线程 1,5,main]get resource1 Thread[线程 1,5,main]waiting get resource2 Thread[线程 1,5,main]get resource2 @@ -296,31 +403,16 @@ Process finished with exit code 0 我们分析一下上面的代码为什么避免了死锁的发生? -线程 1 首先获得到 resource1 的监视器锁,这时候线程 2 就获取不到了。然后线程 1 再去获取 resource2 的监视器锁,可以获取到。然后线程 1 释放了对 resource1、resource2 的监视器锁的占用,线程 2 获取到就可以执行了。这样就破坏了破坏循环等待条件,因此避免了死锁。 +线程 1 首先获得到 resource1 的监视器锁,这时候线程 2 就获取不到了。然后线程 1 再去获取 resource2 的监视器锁,可以获取到。然后线程 1 释放了对 resource1、resource2 的监视器锁的占用,线程 2 获取到就可以执行了。这样就破坏了循环等待条件,因此避免了死锁。 -## sleep() 方法和 wait() 方法对比 +## 虚拟线程 -**共同点**:两者都可以暂停线程的执行。 +虚拟线程在 Java 21 正式发布,这是一项重量级的更新。我写了一篇文章来总结虚拟线程常见的问题:[虚拟线程常见问题总结](./virtual-thread.md),包含下面这些问题: -**区别**: +1. 什么是虚拟线程? +2. 虚拟线程和平台线程有什么关系? +3. 虚拟线程有什么优点和缺点? +4. 如何创建虚拟线程? +5. 虚拟线程的底层原理是什么? -- **`sleep()` 方法没有释放锁,而 `wait()` 方法释放了锁** 。 -- `wait()` 通常被用于线程间交互/通信,`sleep()`通常被用于暂停执行。 -- `wait()` 方法被调用后,线程不会自动苏醒,需要别的线程调用同一个对象上的 `notify()`或者 `notifyAll()` 方法。`sleep()`方法执行完成后,线程会自动苏醒,或者也可以使用 `wait(long timeout)` 超时后线程会自动苏醒。 -- `sleep()` 是 `Thread` 类的静态本地方法,`wait()` 则是 `Object` 类的本地方法。为什么这样设计呢? - -## 为什么 wait() 方法不定义在 Thread 中? - -`wait()` 是让获得对象锁的线程实现等待,会自动释放当前线程占有的对象锁。每个对象(`Object`)都拥有对象锁,既然要释放当前线程占有的对象锁并让其进入 WAITING 状态,自然是要操作对应的对象(`Object`)而非当前的线程(`Thread`)。 - -类似的问题:**为什么 `sleep()` 方法定义在 `Thread` 中?** - -因为 `sleep()` 是让当前线程暂停执行,不涉及到对象类,也不需要获得对象锁。 - -## 可以直接调用 Thread 类的 run 方法吗? - -这是另一个非常经典的 Java 多线程面试问题,而且在面试中会经常被问到。很简单,但是很多人都会答不上来! - -new 一个 `Thread`,线程进入了新建状态。调用 `start()`方法,会启动一个线程并使线程进入了就绪状态,当分配到时间片后就可以开始运行了。 `start()` 会执行线程的相应准备工作,然后自动执行 `run()` 方法的内容,这是真正的多线程工作。 但是,直接执行 `run()` 方法,会把 `run()` 方法当成一个 main 线程下的普通方法去执行,并不会在某个线程中执行它,所以这并不是多线程工作。 - -**总结:调用 `start()` 方法方可启动线程并使线程进入就绪状态,直接执行 `run()` 方法的话不会以多线程的方式执行。** + diff --git a/docs/java/concurrent/java-concurrent-questions-02.md b/docs/java/concurrent/java-concurrent-questions-02.md index 2477362b5c1..40c1b140434 100644 --- a/docs/java/concurrent/java-concurrent-questions-02.md +++ b/docs/java/concurrent/java-concurrent-questions-02.md @@ -12,17 +12,19 @@ head: content: Java并发常见知识点和面试题总结(含详细解答)。 --- -## JMM(Java 内存模型) + + +## ⭐️JMM(Java 内存模型) JMM(Java 内存模型)相关的问题比较多,也比较重要,于是我单独抽了一篇文章来总结 JMM 相关的知识点和问题:[JMM(Java 内存模型)详解](./jmm.md) 。 -## volatile 关键字 +## ⭐️volatile 关键字 ### 如何保证变量的可见性? 在 Java 中,`volatile` 关键字可以保证变量的可见性,如果我们将变量声明为 **`volatile`** ,这就指示 JVM,这个变量是共享且不稳定的,每次使用它都到主存中进行读取。 -![](https://oss.javaguide.cn/github/javaguide/java/concurrent/jmm.png) +![JMM(Java 内存模型)](https://oss.javaguide.cn/github/javaguide/java/concurrent/jmm.png) ![JMM(Java 内存模型)强制在主存中进行读取](https://oss.javaguide.cn/github/javaguide/java/concurrent/jmm2.png) @@ -58,7 +60,7 @@ public class Singleton { private Singleton() { } - public static Singleton getUniqueInstance() { + public static Singleton getUniqueInstance() { //先判断对象是否已经实例过,没有实例化过才进入加锁代码 if (uniqueInstance == null) { //类对象加锁 @@ -94,7 +96,7 @@ public class Singleton { * @author Guide哥 * @date 2022/08/03 13:40 **/ -public class VolatoleAtomicityDemo { +public class VolatileAtomicityDemo { public volatile static int inc = 0; public void increase() { @@ -103,11 +105,11 @@ public class VolatoleAtomicityDemo { public static void main(String[] args) throws InterruptedException { ExecutorService threadPool = Executors.newFixedThreadPool(5); - VolatoleAtomicityDemo volatoleAtomicityDemo = new VolatoleAtomicityDemo(); + VolatileAtomicityDemo volatileAtomicityDemo = new VolatileAtomicityDemo(); for (int i = 0; i < 5; i++) { threadPool.execute(() -> { for (int j = 0; j < 500; j++) { - volatoleAtomicityDemo.increase(); + volatileAtomicityDemo.increase(); } }); } @@ -172,7 +174,7 @@ public void increase() { } ``` -## 乐观锁和悲观锁 +## ⭐️乐观锁和悲观锁 ### 什么是悲观锁? @@ -203,8 +205,7 @@ try { 乐观锁总是假设最好的情况,认为共享资源每次被访问的时候不会出现问题,线程可以不停地执行,无需加锁也无需等待,只是在提交修改的时候去验证对应的资源(也就是数据)是否被其它线程修改了(具体方法可以使用版本号机制或 CAS 算法)。 在 Java 中`java.util.concurrent.atomic`包下面的原子变量类(比如`AtomicInteger`、`LongAdder`)就是使用了乐观锁的一种实现方式 **CAS** 实现的。 - -![JUC原子类概览](https://my-blog-to-use.oss-cn-beijing.aliyuncs.com/2019-6/JUC原子类概览.png) +![JUC原子类概览](https://oss.javaguide.cn/github/javaguide/java/JUC%E5%8E%9F%E5%AD%90%E7%B1%BB%E6%A6%82%E8%A7%88-20230814005211968.png) ```java // LongAdder 在高并发场景下会比 AtomicInteger 和 AtomicLong 的性能更好 @@ -219,8 +220,8 @@ sum.increment(); 理论上来说: -- 悲观锁通常多用于写比较多的情况下(多写场景,竞争激烈),这样可以避免频繁失败和重试影响性能,悲观锁的开销是固定的。不过,如果乐观锁解决了频繁失败和重试这个问题的话(比如`LongAdder`),也是可以考虑使用乐观锁的,要视实际情况而定。 -- 乐观锁通常多于写比较少的情况下(多读场景,竞争较少),这样可以避免频繁加锁影响性能。不过,乐观锁主要针对的对象是单个共享变量(参考`java.util.concurrent.atomic`包下面的原子变量类)。 +- 悲观锁通常多用于写比较多的情况(多写场景,竞争激烈),这样可以避免频繁失败和重试影响性能,悲观锁的开销是固定的。不过,如果乐观锁解决了频繁失败和重试这个问题的话(比如`LongAdder`),也是可以考虑使用乐观锁的,要视实际情况而定。 +- 乐观锁通常多用于写比较少的情况(多读场景,竞争较少),这样可以避免频繁加锁影响性能。不过,乐观锁主要针对的对象是单个共享变量(参考`java.util.concurrent.atomic`包下面的原子变量类)。 ### 如何实现乐观锁? @@ -268,7 +269,7 @@ Java 语言并没有直接实现 CAS,CAS 相关的实现是通过 C++ 内联 ```java /** - * CAS + * CAS * @param o 包含要修改field的对象 * @param offset 对象中某field的偏移量 * @param expected 期望值 @@ -284,9 +285,111 @@ public final native boolean compareAndSwapLong(Object o, long offset, long expec 关于 `Unsafe` 类的详细介绍可以看这篇文章:[Java 魔法类 Unsafe 详解 - JavaGuide - 2022](https://javaguide.cn/java/basis/unsafe.html) 。 -### 乐观锁存在哪些问题? +### Java 中 CAS 是如何实现的? + +在 Java 中,实现 CAS(Compare-And-Swap, 比较并交换)操作的一个关键类是`Unsafe`。 + +`Unsafe`类位于`sun.misc`包下,是一个提供低级别、不安全操作的类。由于其强大的功能和潜在的危险性,它通常用于 JVM 内部或一些需要极高性能和底层访问的库中,而不推荐普通开发者在应用程序中使用。关于 `Unsafe`类的详细介绍,可以阅读这篇文章:📌[Java 魔法类 Unsafe 详解](https://javaguide.cn/java/basis/unsafe.html)。 + +`sun.misc`包下的`Unsafe`类提供了`compareAndSwapObject`、`compareAndSwapInt`、`compareAndSwapLong`方法来实现的对`Object`、`int`、`long`类型的 CAS 操作: + +```java +/** + * 以原子方式更新对象字段的值。 + * + * @param o 要操作的对象 + * @param offset 对象字段的内存偏移量 + * @param expected 期望的旧值 + * @param x 要设置的新值 + * @return 如果值被成功更新,则返回 true;否则返回 false + */ +boolean compareAndSwapObject(Object o, long offset, Object expected, Object x); + +/** + * 以原子方式更新 int 类型的对象字段的值。 + */ +boolean compareAndSwapInt(Object o, long offset, int expected, int x); + +/** + * 以原子方式更新 long 类型的对象字段的值。 + */ +boolean compareAndSwapLong(Object o, long offset, long expected, long x); +``` + +`Unsafe`类中的 CAS 方法是`native`方法。`native`关键字表明这些方法是用本地代码(通常是 C 或 C++)实现的,而不是用 Java 实现的。这些方法直接调用底层的硬件指令来实现原子操作。也就是说,Java 语言并没有直接用 Java 实现 CAS,而是通过 C++ 内联汇编的形式实现的(通过 JNI 调用)。因此,CAS 的具体实现与操作系统以及 CPU 密切相关。 + +`java.util.concurrent.atomic` 包提供了一些用于原子操作的类。这些类利用底层的原子指令,确保在多线程环境下的操作是线程安全的。 + +![JUC原子类概览](https://oss.javaguide.cn/github/javaguide/java/JUC%E5%8E%9F%E5%AD%90%E7%B1%BB%E6%A6%82%E8%A7%88.png) + +关于这些 Atomic 原子类的介绍和使用,可以阅读这篇文章:[Atomic 原子类总结](https://javaguide.cn/java/concurrent/atomic-classes.html)。 + +`AtomicInteger`是 Java 的原子类之一,主要用于对 `int` 类型的变量进行原子操作,它利用`Unsafe`类提供的低级别原子操作方法实现无锁的线程安全性。 + +下面,我们通过解读`AtomicInteger`的核心源码(JDK1.8),来说明 Java 如何使用`Unsafe`类的方法来实现原子操作。 + +`AtomicInteger`核心源码如下: + +```java +// 获取 Unsafe 实例 +private static final Unsafe unsafe = Unsafe.getUnsafe(); +private static final long valueOffset; + +static { + try { + // 获取“value”字段在AtomicInteger类中的内存偏移量 + valueOffset = unsafe.objectFieldOffset + (AtomicInteger.class.getDeclaredField("value")); + } catch (Exception ex) { throw new Error(ex); } +} +// 确保“value”字段的可见性 +private volatile int value; + +// 如果当前值等于预期值,则原子地将值设置为newValue +// 使用 Unsafe#compareAndSwapInt 方法进行CAS操作 +public final boolean compareAndSet(int expect, int update) { + return unsafe.compareAndSwapInt(this, valueOffset, expect, update); +} + +// 原子地将当前值加 delta 并返回旧值 +public final int getAndAdd(int delta) { + return unsafe.getAndAddInt(this, valueOffset, delta); +} + +// 原子地将当前值加 1 并返回加之前的值(旧值) +// 使用 Unsafe#getAndAddInt 方法进行CAS操作。 +public final int getAndIncrement() { + return unsafe.getAndAddInt(this, valueOffset, 1); +} + +// 原子地将当前值减 1 并返回减之前的值(旧值) +public final int getAndDecrement() { + return unsafe.getAndAddInt(this, valueOffset, -1); +} +``` + +`Unsafe#getAndAddInt`源码: -ABA 问题是乐观锁最常见的问题。 +```java +// 原子地获取并增加整数值 +public final int getAndAddInt(Object o, long offset, int delta) { + int v; + do { + // 以 volatile 方式获取对象 o 在内存偏移量 offset 处的整数值 + v = getIntVolatile(o, offset); + } while (!compareAndSwapInt(o, offset, v, v + delta)); + // 返回旧值 + return v; +} +``` + +可以看到,`getAndAddInt` 使用了 `do-while` 循环:在`compareAndSwapInt`操作失败时,会不断重试直到成功。也就是说,`getAndAddInt`方法会通过 `compareAndSwapInt` 方法来尝试更新 `value` 的值,如果更新失败(当前值在此期间被其他线程修改),它会重新获取当前值并再次尝试更新,直到操作成功。 + +由于 CAS 操作可能会因为并发冲突而失败,因此通常会与`while`循环搭配使用,在失败后不断重试,直到操作成功。这就是 **自旋锁机制** 。 + +### CAS 算法存在哪些问题? + +ABA 问题是 CAS 算法最常见的问题。 #### ABA 问题 @@ -313,14 +416,16 @@ public boolean compareAndSet(V expectedReference, CAS 经常会用到自旋操作来进行重试,也就是不成功就一直循环执行直到成功。如果长时间不成功,会给 CPU 带来非常大的执行开销。 -如果 JVM 能支持处理器提供的 pause 指令那么效率会有一定的提升,pause 指令有两个作用: +如果 JVM 能够支持处理器提供的`pause`指令,那么自旋操作的效率将有所提升。`pause`指令有两个重要作用: -1. 可以延迟流水线执行指令,使 CPU 不会消耗过多的执行资源,延迟的时间取决于具体实现的版本,在一些处理器上延迟时间是零。 -2. 可以避免在退出循环的时候因内存顺序冲而引起 CPU 流水线被清空,从而提高 CPU 的执行效率。 +1. **延迟流水线执行指令**:`pause`指令可以延迟指令的执行,从而减少 CPU 的资源消耗。具体的延迟时间取决于处理器的实现版本,在某些处理器上,延迟时间可能为零。 +2. **避免内存顺序冲突**:在退出循环时,`pause`指令可以避免由于内存顺序冲突而导致的 CPU 流水线被清空,从而提高 CPU 的执行效率。 #### 只能保证一个共享变量的原子操作 -CAS 只对单个共享变量有效,当操作涉及跨多个共享变量时 CAS 无效。但是从 JDK 1.5 开始,提供了`AtomicReference`类来保证引用对象之间的原子性,你可以把多个变量放在一个对象里来进行 CAS 操作.所以我们可以使用锁或者利用`AtomicReference`类把多个共享变量合并成一个共享变量来操作。 +CAS 操作仅能对单个共享变量有效。当需要操作多个共享变量时,CAS 就显得无能为力。不过,从 JDK 1.5 开始,Java 提供了`AtomicReference`类,这使得我们能够保证引用对象之间的原子性。通过将多个变量封装在一个对象中,我们可以使用`AtomicReference`来执行 CAS 操作。 + +除了 `AtomicReference` 这种方式之外,还可以利用加锁来保证。 ## synchronized 关键字 @@ -332,6 +437,8 @@ CAS 只对单个共享变量有效,当操作涉及跨多个共享变量时 CAS 不过,在 Java 6 之后, `synchronized` 引入了大量的优化如自旋锁、适应性自旋锁、锁消除、锁粗化、偏向锁、轻量级锁等技术来减少锁操作的开销,这些优化让 `synchronized` 锁的效率提升了很多。因此, `synchronized` 还是可以在实际项目中使用的,像 JDK 源码、很多开源框架都大量使用了 `synchronized` 。 +关于偏向锁多补充一点:由于偏向锁增加了 JVM 的复杂性,同时也并没有为所有应用都带来性能提升。因此,在 JDK15 中,偏向锁被默认关闭(仍然可以使用 `-XX:+UseBiasedLocking` 启用偏向锁),在 JDK18 中,偏向锁已经被彻底废弃(无法通过命令行打开)。 + ### 如何使用 synchronized? `synchronized` 关键字的使用方式主要有下面 3 种: @@ -368,8 +475,8 @@ synchronized static void method() { 对括号里指定的对象/类加锁: -- `synchronized(object)` 表示进入同步代码库前要获得 **给定对象的锁**。 -- `synchronized(类.class)` 表示进入同步代码前要获得 **给定 Class 的锁** +- `synchronized(object)` 表示进入同步代码块前要获得 **给定对象的锁**。 +- `synchronized(类.class)` 表示进入同步代码块前要获得 **给定 Class 的锁** ```java synchronized(this) { @@ -385,11 +492,11 @@ synchronized(this) { ### 构造方法可以用 synchronized 修饰么? -先说结论:**构造方法不能使用 synchronized 关键字修饰。** +构造方法不能使用 synchronized 关键字修饰。不过,可以在构造方法内部使用 synchronized 代码块。 -构造方法本身就属于线程安全的,不存在同步的构造方法一说。 +另外,构造方法本身是线程安全的,但如果在构造方法中涉及到共享资源的操作,就需要采取适当的同步措施来保证整个构造过程的线程安全。 -### synchronized 底层原理了解吗? +### ⭐️synchronized 底层原理了解吗? synchronized 关键字底层原理属于 JVM 层面的东西。 @@ -423,13 +530,13 @@ public class SynchronizedDemo { ![执行 monitorenter 获取锁](https://oss.javaguide.cn/github/javaguide/java/concurrent/synchronized-get-lock-code-block.png) -对象锁的的拥有者线程才可以执行 `monitorexit` 指令来释放锁。在执行 `monitorexit` 指令后,将锁计数器设为 0,表明锁被释放,其他线程可以尝试获取锁。 +对象锁的拥有者线程才可以执行 `monitorexit` 指令来释放锁。在执行 `monitorexit` 指令后,将锁计数器设为 0,表明锁被释放,其他线程可以尝试获取锁。 ![执行 monitorexit 释放锁](https://oss.javaguide.cn/github/javaguide/java/concurrent/synchronized-release-lock-block.png) 如果获取对象锁失败,那当前线程就要阻塞等待,直到锁被另外一个线程释放为止。 -#### synchronized 修饰方法的的情况 +#### synchronized 修饰方法的情况 ```java public class SynchronizedDemo2 { @@ -442,7 +549,7 @@ public class SynchronizedDemo2 { ![synchronized关键字原理](https://oss.javaguide.cn/github/javaguide/synchronized%E5%85%B3%E9%94%AE%E5%AD%97%E5%8E%9F%E7%90%862.png) -`synchronized` 修饰的方法并没有 `monitorenter` 指令和 `monitorexit` 指令,取得代之的确实是 `ACC_SYNCHRONIZED` 标识,该标识指明了该方法是一个同步方法。JVM 通过该 `ACC_SYNCHRONIZED` 访问标志来辨别一个方法是否声明为同步方法,从而执行相应的同步调用。 +`synchronized` 修饰的方法并没有 `monitorenter` 指令和 `monitorexit` 指令,取而代之的是 `ACC_SYNCHRONIZED` 标识,该标识指明了该方法是一个同步方法。JVM 通过该 `ACC_SYNCHRONIZED` 访问标志来辨别一个方法是否声明为同步方法,从而执行相应的同步调用。 如果是实例方法,JVM 会尝试获取实例对象的锁。如果是静态方法,JVM 会尝试获取当前 class 的锁。 @@ -450,23 +557,47 @@ public class SynchronizedDemo2 { `synchronized` 同步语句块的实现使用的是 `monitorenter` 和 `monitorexit` 指令,其中 `monitorenter` 指令指向同步代码块的开始位置,`monitorexit` 指令则指明同步代码块的结束位置。 -`synchronized` 修饰的方法并没有 `monitorenter` 指令和 `monitorexit` 指令,取得代之的确实是 `ACC_SYNCHRONIZED` 标识,该标识指明了该方法是一个同步方法。 +`synchronized` 修饰的方法并没有 `monitorenter` 指令和 `monitorexit` 指令,取而代之的是 `ACC_SYNCHRONIZED` 标识,该标识指明了该方法是一个同步方法。 -**不过两者的本质都是对对象监视器 monitor 的获取。** +**不过,两者的本质都是对对象监视器 monitor 的获取。** 相关推荐:[Java 锁与线程的那些事 - 有赞技术团队](https://tech.youzan.com/javasuo-yu-xian-cheng-de-na-xie-shi/) 。 🧗🏻 进阶一下:学有余力的小伙伴可以抽时间详细研究一下对象监视器 `monitor`。 -### JDK1.6 之后的 synchronized 底层做了哪些优化? +### JDK1.6 之后的 synchronized 底层做了哪些优化?锁升级原理了解吗? -JDK1.6 对锁的实现引入了大量的优化,如偏向锁、轻量级锁、自旋锁、适应性自旋锁、锁消除、锁粗化等技术来减少锁操作的开销。 +在 Java 6 之后, `synchronized` 引入了大量的优化如自旋锁、适应性自旋锁、锁消除、锁粗化、偏向锁、轻量级锁等技术来减少锁操作的开销,这些优化让 `synchronized` 锁的效率提升了很多(JDK18 中,偏向锁已经被彻底废弃,前面已经提到过了)。 锁主要存在四种状态,依次是:无锁状态、偏向锁状态、轻量级锁状态、重量级锁状态,他们会随着竞争的激烈而逐渐升级。注意锁可以升级不可降级,这种策略是为了提高获得锁和释放锁的效率。 -关于这几种优化的详细信息可以查看下面这篇文章:[Java6 及以上版本对 synchronized 的优化](https://www.cnblogs.com/wuqinglong/p/9945618.html) 。 +`synchronized` 锁升级是一个比较复杂的过程,面试也很少问到,如果你想要详细了解的话,可以看看这篇文章:[浅析 synchronized 锁升级的原理与实现](https://www.cnblogs.com/star95/p/17542850.html)。 + +### synchronized 的偏向锁为什么被废弃了? + +Open JDK 官方声明:[JEP 374: Deprecate and Disable Biased Locking](https://openjdk.org/jeps/374) + +在 JDK15 中,偏向锁被默认关闭(仍然可以使用 `-XX:+UseBiasedLocking` 启用偏向锁),在 JDK18 中,偏向锁已经被彻底废弃(无法通过命令行打开)。 + +在官方声明中,主要原因有两个方面: + +- **性能收益不明显:** + +偏向锁是 HotSpot 虚拟机的一项优化技术,可以提升单线程对同步代码块的访问性能。 + +受益于偏向锁的应用程序通常使用了早期的 Java 集合 API,例如 HashTable、Vector,在这些集合类中通过 synchronized 来控制同步,这样在单线程频繁访问时,通过偏向锁会减少同步开销。 + +随着 JDK 的发展,出现了 ConcurrentHashMap 高性能的集合类,在集合类内部进行了许多性能优化,此时偏向锁带来的性能收益就不明显了。 + +偏向锁仅仅在单线程访问同步代码块的场景中可以获得性能收益。 -### synchronized 和 volatile 有什么区别? +如果存在多线程竞争,就需要 **撤销偏向锁** ,这个操作的性能开销是比较昂贵的。偏向锁的撤销需要等待进入到全局安全点(safe point),该状态下所有线程都是暂停的,此时去检查线程状态并进行偏向锁的撤销。 + +- **JVM 内部代码维护成本太高:** + +偏向锁将许多复杂代码引入到同步子系统,并且对其他的 HotSpot 组件也具有侵入性。这种复杂性为理解代码、系统重构带来了困难,因此, OpenJDK 官方希望禁用、废弃并删除偏向锁。 + +### ⭐️synchronized 和 volatile 有什么区别? `synchronized` 关键字和 `volatile` 关键字是两个互补的存在,而不是对立的存在! @@ -504,7 +635,7 @@ public ReentrantLock(boolean fair) { - **公平锁** : 锁被释放之后,先申请的线程先得到锁。性能较差一些,因为公平锁为了保证时间上的绝对顺序,上下文切换更频繁。 - **非公平锁**:锁被释放之后,后申请的线程可能会先获取到锁,是随机或者按照其他优先级排序的。性能更好,但可能会导致某些线程永远无法获取到锁。 -### synchronized 和 ReentrantLock 有什么区别? +### ⭐️synchronized 和 ReentrantLock 有什么区别? #### 两者都是可重入锁 @@ -515,7 +646,7 @@ JDK 提供的所有现成的 `Lock` 实现类,包括 `synchronized` 关键字 在下面的代码中,`method1()` 和 `method2()`都被 `synchronized` 关键字修饰,`method1()`调用了`method2()`。 ```java -public class ReentrantLockDemo { +public class SynchronizedDemo { public synchronized void method1() { System.out.println("方法1"); method2(); @@ -539,9 +670,10 @@ public class ReentrantLockDemo { 相比`synchronized`,`ReentrantLock`增加了一些高级功能。主要来说主要有三点: -- **等待可中断** : `ReentrantLock`提供了一种能够中断等待锁的线程的机制,通过 `lock.lockInterruptibly()` 来实现这个机制。也就是说正在等待的线程可以选择放弃等待,改为处理其他事情。 -- **可实现公平锁** : `ReentrantLock`可以指定是公平锁还是非公平锁。而`synchronized`只能是非公平锁。所谓的公平锁就是先等待的线程先获得锁。`ReentrantLock`默认情况是非公平的,可以通过 `ReentrantLock`类的`ReentrantLock(boolean fair)`构造方法来制定是否是公平的。 +- **等待可中断** : `ReentrantLock`提供了一种能够中断等待锁的线程的机制,通过 `lock.lockInterruptibly()` 来实现这个机制。也就是说当前线程在等待获取锁的过程中,如果其他线程中断当前线程「 `interrupt()` 」,当前线程就会抛出 `InterruptedException` 异常,可以捕捉该异常进行相应处理。 +- **可实现公平锁** : `ReentrantLock`可以指定是公平锁还是非公平锁。而`synchronized`只能是非公平锁。所谓的公平锁就是先等待的线程先获得锁。`ReentrantLock`默认情况是非公平的,可以通过 `ReentrantLock`类的`ReentrantLock(boolean fair)`构造方法来指定是否是公平的。 - **可实现选择性通知(锁可以绑定多个条件)**: `synchronized`关键字与`wait()`和`notify()`/`notifyAll()`方法相结合可以实现等待/通知机制。`ReentrantLock`类当然也可以实现,但是需要借助于`Condition`接口与`newCondition()`方法。 +- **支持超时** :`ReentrantLock` 提供了 `tryLock(timeout)` 的方法,可以指定等待获取锁的最长等待时间,如果超过了等待时间,就会获取锁失败,不会一直等待。 如果你想使用上述功能,那么选择 `ReentrantLock` 是一个不错的选择。 @@ -549,6 +681,85 @@ public class ReentrantLockDemo { > `Condition`是 JDK1.5 之后才有的,它具有很好的灵活性,比如可以实现多路通知功能也就是在一个`Lock`对象中可以创建多个`Condition`实例(即对象监视器),**线程对象可以注册在指定的`Condition`中,从而可以有选择性的进行线程通知,在调度线程上更加灵活。 在使用`notify()/notifyAll()`方法进行通知时,被通知的线程是由 JVM 选择的,用`ReentrantLock`类结合`Condition`实例可以实现“选择性通知”** ,这个功能非常重要,而且是 `Condition` 接口默认提供的。而`synchronized`关键字就相当于整个 `Lock` 对象中只有一个`Condition`实例,所有的线程都注册在它一个身上。如果执行`notifyAll()`方法的话就会通知所有处于等待状态的线程,这样会造成很大的效率问题。而`Condition`实例的`signalAll()`方法,只会唤醒注册在该`Condition`实例中的所有等待线程。 +关于 **等待可中断** 的补充: + +> `lockInterruptibly()` 会让获取锁的线程在阻塞等待的过程中可以响应中断,即当前线程在获取锁的时候,发现锁被其他线程持有,就会阻塞等待。 +> +> 在阻塞等待的过程中,如果其他线程中断当前线程 `interrupt()` ,就会抛出 `InterruptedException` 异常,可以捕获该异常,做一些处理操作。 +> +> 为了更好理解这个方法,借用 Stack Overflow 上的一个案例,可以更好地理解 `lockInterruptibly()` 可以响应中断: +> +> ```JAVA +> public class MyRentrantlock { +> Thread t = new Thread() { +> @Override +> public void run() { +> ReentrantLock r = new ReentrantLock(); +> // 1.1、第一次尝试获取锁,可以获取成功 +> r.lock(); +> +> // 1.2、此时锁的重入次数为 1 +> System.out.println("lock() : lock count :" + r.getHoldCount()); +> +> // 2、中断当前线程,通过 Thread.currentThread().isInterrupted() 可以看到当前线程的中断状态为 true +> interrupt(); +> System.out.println("Current thread is intrupted"); +> +> // 3.1、尝试获取锁,可以成功获取 +> r.tryLock(); +> // 3.2、此时锁的重入次数为 2 +> System.out.println("tryLock() on intrupted thread lock count :" + r.getHoldCount()); +> try { +> // 4、打印线程的中断状态为 true,那么调用 lockInterruptibly() 方法就会抛出 InterruptedException 异常 +> System.out.println("Current Thread isInterrupted:" + Thread.currentThread().isInterrupted()); +> r.lockInterruptibly(); +> System.out.println("lockInterruptibly() --NOt executable statement" + r.getHoldCount()); +> } catch (InterruptedException e) { +> r.lock(); +> System.out.println("Error"); +> } finally { +> r.unlock(); +> } +> +> // 5、打印锁的重入次数,可以发现 lockInterruptibly() 方法并没有成功获取到锁 +> System.out.println("lockInterruptibly() not able to Acqurie lock: lock count :" + r.getHoldCount()); +> +> r.unlock(); +> System.out.println("lock count :" + r.getHoldCount()); +> r.unlock(); +> System.out.println("lock count :" + r.getHoldCount()); +> } +> }; +> public static void main(String str[]) { +> MyRentrantlock m = new MyRentrantlock(); +> m.t.start(); +> } +> } +> ``` +> +> 输出: +> +> ```BASH +> lock() : lock count :1 +> Current thread is intrupted +> tryLock() on intrupted thread lock count :2 +> Current Thread isInterrupted:true +> Error +> lockInterruptibly() not able to Acqurie lock: lock count :2 +> lock count :1 +> lock count :0 +> ``` + +关于 **支持超时** 的补充: + +> **为什么需要 `tryLock(timeout)` 这个功能呢?** +> +> `tryLock(timeout)` 方法尝试在指定的超时时间内获取锁。如果成功获取锁,则返回 `true`;如果在锁可用之前超时,则返回 `false`。此功能在以下几种场景中非常有用: +> +> - **防止死锁:** 在复杂的锁场景中,`tryLock(timeout)` 可以通过允许线程在合理的时间内放弃并重试来帮助防止死锁。 +> - **提高响应速度:** 防止线程无限期阻塞。 +> - **处理时间敏感的操作:** 对于具有严格时间限制的操作,`tryLock(timeout)` 允许线程在无法及时获取锁时继续执行替代操作。 + ### 可中断锁和不可中断锁有什么区别? - **可中断锁**:获取锁的过程中可以被中断,不需要一直等到获取锁之后 才能进行其他逻辑处理。`ReentrantLock` 就属于是可中断锁。 @@ -606,7 +817,7 @@ public ReentrantReadWriteLock(boolean fair) { - 在线程持有读锁的情况下,该线程不能取得写锁(因为获取写锁的时候,如果发现当前的读锁被占用,就马上获取失败,不管读锁是不是被当前线程持有)。 - 在线程持有写锁的情况下,该线程可以继续获取读锁(获取读锁时如果发现写锁被占用,只有写锁没有被当前线程占用的情况才会获取失败)。 -读写锁的源码分析,推荐阅读 [聊聊 Java 的几把 JVM 级锁 - 阿里巴巴中间件 ](https://mp.weixin.qq.com/s/h3VIUyH9L0v14MrQJiiDbw) 这篇文章,写的很不错。 +读写锁的源码分析,推荐阅读 [聊聊 Java 的几把 JVM 级锁 - 阿里巴巴中间件](https://mp.weixin.qq.com/s/h3VIUyH9L0v14MrQJiiDbw) 这篇文章,写的很不错。 ### 读锁为什么不能升级为写锁? @@ -620,7 +831,7 @@ public ReentrantReadWriteLock(boolean fair) { ### StampedLock 是什么? -`StampedLock` 是 JDK 1.8 引入的性能更好的读写锁,不可重入且不支持条件变量 `Conditon`。 +`StampedLock` 是 JDK 1.8 引入的性能更好的读写锁,不可重入且不支持条件变量 `Condition`。 不同于一般的 `Lock` 类,`StampedLock` 并不是直接实现 `Lock`或 `ReadWriteLock`接口,而是基于 **CLH 锁** 独立实现的(AQS 也是基于这玩意)。 @@ -675,7 +886,7 @@ public long tryOptimisticRead() { 和 `ReentrantReadWriteLock` 一样,`StampedLock` 同样适合读多写少的业务场景,可以作为 `ReentrantReadWriteLock`的替代品,性能更好。 -不过,需要注意的是`StampedLock`不可重入,不支持条件变量 `Conditon`,对中断操作支持也不友好(使用不当容易导致 CPU 飙升)。如果你需要用到 `ReentrantLock` 的一些高级性能,就不太建议使用 `StampedLock` 了。 +不过,需要注意的是`StampedLock`不可重入,不支持条件变量 `Condition`,对中断操作支持也不友好(使用不当容易导致 CPU 飙升)。如果你需要用到 `ReentrantLock` 的一些高级性能,就不太建议使用 `StampedLock` 了。 另外,`StampedLock` 性能虽好,但使用起来相对比较麻烦,一旦使用不当,就会出现生产问题。强烈建议你在使用`StampedLock` 之前,看看 [StampedLock 官方文档中的案例](https://docs.oracle.com/javase/8/docs/api/java/util/concurrent/locks/StampedLock.html)。 @@ -698,9 +909,11 @@ Atomic 原子类部分的内容我单独写了一篇文章来总结:[Atomic - 《深入理解 Java 虚拟机》 - 《实战 Java 高并发程序设计》 -- Guide to the Volatile Keyword in Java - Baeldung:https://www.baeldung.com/java-volatile -- 不可不说的 Java“锁”事 - 美团技术团队:https://tech.meituan.com/2018/11/15/java-lock.html -- 在 ReadWriteLock 类中读锁为什么不能升级为写锁?:https://cloud.tencent.com/developer/article/1176230 -- 高性能解决线程饥饿的利器 StampedLock:https://mp.weixin.qq.com/s/2Acujjr4BHIhlFsCLGwYSg -- 理解 Java 中的 ThreadLocal - 技术小黑屋:https://droidyue.com/blog/2016/03/13/learning-threadlocal-in-java/ -- ThreadLocal (Java Platform SE 8 ) - Oracle Help Center:https://docs.oracle.com/javase/8/docs/api/java/lang/ThreadLocal.html +- Guide to the Volatile Keyword in Java - Baeldung: +- 不可不说的 Java“锁”事 - 美团技术团队: +- 在 ReadWriteLock 类中读锁为什么不能升级为写锁?: +- 高性能解决线程饥饿的利器 StampedLock: +- 理解 Java 中的 ThreadLocal - 技术小黑屋: +- ThreadLocal (Java Platform SE 8 ) - Oracle Help Center: + + diff --git a/docs/java/concurrent/java-concurrent-questions-03.md b/docs/java/concurrent/java-concurrent-questions-03.md index ba202bf2e1a..84d58459d09 100644 --- a/docs/java/concurrent/java-concurrent-questions-03.md +++ b/docs/java/concurrent/java-concurrent-questions-03.md @@ -12,97 +12,42 @@ head: content: Java并发常见知识点和面试题总结(含详细解答),希望对你有帮助! --- + + ## ThreadLocal ### ThreadLocal 有什么用? -通常情况下,我们创建的变量是可以被任何一个线程访问并修改的。**如果想实现每一个线程都有自己的专属本地变量该如何解决呢?** - -JDK 中自带的`ThreadLocal`类正是为了解决这样的问题。 **`ThreadLocal`类主要解决的就是让每个线程绑定自己的值,可以将`ThreadLocal`类形象的比喻成存放数据的盒子,盒子中可以存储每个线程的私有数据。** +通常情况下,我们创建的变量可以被任何一个线程访问和修改。这在多线程环境中可能导致数据竞争和线程安全问题。那么,**如果想让每个线程都有自己的专属本地变量,该如何实现呢?** -如果你创建了一个`ThreadLocal`变量,那么访问这个变量的每个线程都会有这个变量的本地副本,这也是`ThreadLocal`变量名的由来。他们可以使用 `get()` 和 `set()` 方法来获取默认值或将其值更改为当前线程所存的副本的值,从而避免了线程安全问题。 +JDK 中提供的 `ThreadLocal` 类正是为了解决这个问题。**`ThreadLocal` 类允许每个线程绑定自己的值**,可以将其形象地比喻为一个“存放数据的盒子”。每个线程都有自己独立的盒子,用于存储私有数据,确保不同线程之间的数据互不干扰。 -再举个简单的例子:两个人去宝屋收集宝物,这两个共用一个袋子的话肯定会产生争执,但是给他们两个人每个人分配一个袋子的话就不会出现这样的问题。如果把这两个人比作线程的话,那么 ThreadLocal 就是用来避免这两个线程竞争的。 +当你创建一个 `ThreadLocal` 变量时,每个访问该变量的线程都会拥有一个独立的副本。这也是 `ThreadLocal` 名称的由来。线程可以通过 `get()` 方法获取自己线程的本地副本,或通过 `set()` 方法修改该副本的值,从而避免了线程安全问题。 -### 如何使用 ThreadLocal? - -相信看了上面的解释,大家已经搞懂 `ThreadLocal` 类是个什么东西了。下面简单演示一下如何在项目中实际使用 `ThreadLocal` 。 +举个简单的例子:假设有两个人去宝屋收集宝物。如果他们共用一个袋子,必然会产生争执;但如果每个人都有一个独立的袋子,就不会有这个问题。如果将这两个人比作线程,那么 `ThreadLocal` 就是用来避免这两个线程竞争同一个资源的方法。 ```java -import java.text.SimpleDateFormat; -import java.util.Random; - -public class ThreadLocalExample implements Runnable{ - - // SimpleDateFormat 不是线程安全的,所以每个线程都要有自己独立的副本 - private static final ThreadLocal formatter = ThreadLocal.withInitial(() -> new SimpleDateFormat("yyyyMMdd HHmm")); - - public static void main(String[] args) throws InterruptedException { - ThreadLocalExample obj = new ThreadLocalExample(); - for(int i=0 ; i<10; i++){ - Thread t = new Thread(obj, ""+i); - Thread.sleep(new Random().nextInt(1000)); - t.start(); - } - } - - @Override - public void run() { - System.out.println("Thread Name= "+Thread.currentThread().getName()+" default Formatter = "+formatter.get().toPattern()); - try { - Thread.sleep(new Random().nextInt(1000)); - } catch (InterruptedException e) { - e.printStackTrace(); - } - //formatter pattern is changed here by thread, but it won't reflect to other threads - formatter.set(new SimpleDateFormat()); - - System.out.println("Thread Name= "+Thread.currentThread().getName()+" formatter = "+formatter.get().toPattern()); +public class ThreadLocalExample { + private static ThreadLocal threadLocal = ThreadLocal.withInitial(() -> 0); + + public static void main(String[] args) { + Runnable task = () -> { + int value = threadLocal.get(); + value += 1; + threadLocal.set(value); + System.out.println(Thread.currentThread().getName() + " Value: " + threadLocal.get()); + }; + + Thread thread1 = new Thread(task, "Thread-1"); + Thread thread2 = new Thread(task, "Thread-2"); + + thread1.start(); // 输出: Thread-1 Value: 1 + thread2.start(); // 输出: Thread-2 Value: 1 } - } - -``` - -输出结果 : - -``` -Thread Name= 0 default Formatter = yyyyMMdd HHmm -Thread Name= 0 formatter = yy-M-d ah:mm -Thread Name= 1 default Formatter = yyyyMMdd HHmm -Thread Name= 2 default Formatter = yyyyMMdd HHmm -Thread Name= 1 formatter = yy-M-d ah:mm -Thread Name= 3 default Formatter = yyyyMMdd HHmm -Thread Name= 2 formatter = yy-M-d ah:mm -Thread Name= 4 default Formatter = yyyyMMdd HHmm -Thread Name= 3 formatter = yy-M-d ah:mm -Thread Name= 4 formatter = yy-M-d ah:mm -Thread Name= 5 default Formatter = yyyyMMdd HHmm -Thread Name= 5 formatter = yy-M-d ah:mm -Thread Name= 6 default Formatter = yyyyMMdd HHmm -Thread Name= 6 formatter = yy-M-d ah:mm -Thread Name= 7 default Formatter = yyyyMMdd HHmm -Thread Name= 7 formatter = yy-M-d ah:mm -Thread Name= 8 default Formatter = yyyyMMdd HHmm -Thread Name= 9 default Formatter = yyyyMMdd HHmm -Thread Name= 8 formatter = yy-M-d ah:mm -Thread Name= 9 formatter = yy-M-d ah:mm -``` - -从输出中可以看出,虽然 `Thread-0` 已经改变了 `formatter` 的值,但 `Thread-1` 默认格式化值与初始化值相同,其他线程也一样。 - -上面有一段代码用到了创建 `ThreadLocal` 变量的那段代码用到了 Java8 的知识,它等于下面这段代码,如果你写了下面这段代码的话,IDEA 会提示你转换为 Java8 的格式(IDEA 真的不错!)。因为 ThreadLocal 类在 Java 8 中扩展,使用一个新的方法`withInitial()`,将 Supplier 功能接口作为参数。 - -```java -private static final ThreadLocal formatter = new ThreadLocal(){ - @Override - protected SimpleDateFormat initialValue(){ - return new SimpleDateFormat("yyyyMMdd HHmm"); - } -}; ``` -### ThreadLocal 原理了解吗? +### ⭐️ThreadLocal 原理了解吗? 从 `Thread`类源代码入手。 @@ -159,15 +104,36 @@ ThreadLocalMap(ThreadLocal firstKey, Object firstValue) { ![ThreadLocal内部类](https://oss.javaguide.cn/github/javaguide/java/concurrent/thread-local-inner-class.png) -### ThreadLocal 内存泄露问题是怎么导致的? +### ⭐️ThreadLocal 内存泄露问题是怎么导致的? + +`ThreadLocal` 内存泄漏的根本原因在于其内部实现机制。 -`ThreadLocalMap` 中使用的 key 为 `ThreadLocal` 的弱引用,而 value 是强引用。所以,如果 `ThreadLocal` 没有被外部强引用的情况下,在垃圾回收的时候,key 会被清理掉,而 value 不会被清理掉。 +通过上面的内容我们已经知道:每个线程维护一个名为 `ThreadLocalMap` 的 map。 当你使用 `ThreadLocal` 存储值时,实际上是将值存储在当前线程的 `ThreadLocalMap` 中,其中 `ThreadLocal` 实例本身作为 key,而你要存储的值作为 value。 -这样一来,`ThreadLocalMap` 中就会出现 key 为 null 的 Entry。假如我们不做任何措施的话,value 永远无法被 GC 回收,这个时候就可能会产生内存泄露。`ThreadLocalMap` 实现中已经考虑了这种情况,在调用 `set()`、`get()`、`remove()` 方法的时候,会清理掉 key 为 null 的记录。使用完 `ThreadLocal`方法后最好手动调用`remove()`方法 +`ThreadLocal` 的 `set()` 方法源码如下: + +```java +public void set(T value) { + Thread t = Thread.currentThread(); // 获取当前线程 + ThreadLocalMap map = getMap(t); // 获取当前线程的 ThreadLocalMap + if (map != null) { + map.set(this, value); // 设置值 + } else { + createMap(t, value); // 创建新的 ThreadLocalMap + } +} +``` + +`ThreadLocalMap` 的 `set()` 和 `createMap()` 方法中,并没有直接存储 `ThreadLocal` 对象本身,而是使用 `ThreadLocal` 的哈希值计算数组索引,最终存储于类型为`static class Entry extends WeakReference>`的数组中。 + +```java +int i = key.threadLocalHashCode & (len-1); +``` + +`ThreadLocalMap` 的 `Entry` 定义如下: ```java static class Entry extends WeakReference> { - /** The value associated with this ThreadLocal. */ Object value; Entry(ThreadLocal k, Object v) { @@ -177,11 +143,91 @@ static class Entry extends WeakReference> { } ``` -**弱引用介绍:** +`ThreadLocalMap` 的 `key` 和 `value` 引用机制: -> 如果一个对象只具有弱引用,那就类似于**可有可无的生活用品**。弱引用与软引用的区别在于:只具有弱引用的对象拥有更短暂的生命周期。在垃圾回收器线程扫描它 所管辖的内存区域的过程中,一旦发现了只具有弱引用的对象,不管当前内存空间足够与否,都会回收它的内存。不过,由于垃圾回收器是一个优先级很低的线程, 因此不一定会很快发现那些只具有弱引用的对象。 -> -> 弱引用可以和一个引用队列(ReferenceQueue)联合使用,如果弱引用所引用的对象被垃圾回收,Java 虚拟机就会把这个弱引用加入到与之关联的引用队列中。 +- **key 是弱引用**:`ThreadLocalMap` 中的 key 是 `ThreadLocal` 的弱引用 (`WeakReference>`)。 这意味着,如果 `ThreadLocal` 实例不再被任何强引用指向,垃圾回收器会在下次 GC 时回收该实例,导致 `ThreadLocalMap` 中对应的 key 变为 `null`。 +- **value 是强引用**:即使 `key` 被 GC 回收,`value` 仍然被 `ThreadLocalMap.Entry` 强引用存在,无法被 GC 回收。 + +当 `ThreadLocal` 实例失去强引用后,其对应的 value 仍然存在于 `ThreadLocalMap` 中,因为 `Entry` 对象强引用了它。如果线程持续存活(例如线程池中的线程),`ThreadLocalMap` 也会一直存在,导致 key 为 `null` 的 entry 无法被垃圾回收,即会造成内存泄漏。 + +也就是说,内存泄漏的发生需要同时满足两个条件: + +1. `ThreadLocal` 实例不再被强引用; +2. 线程持续存活,导致 `ThreadLocalMap` 长期存在。 + +虽然 `ThreadLocalMap` 在 `get()`, `set()` 和 `remove()` 操作时会尝试清理 key 为 null 的 entry,但这种清理机制是被动的,并不完全可靠。 + +**如何避免内存泄漏的发生?** + +1. 在使用完 `ThreadLocal` 后,务必调用 `remove()` 方法。 这是最安全和最推荐的做法。 `remove()` 方法会从 `ThreadLocalMap` 中显式地移除对应的 entry,彻底解决内存泄漏的风险。 即使将 `ThreadLocal` 定义为 `static final`,也强烈建议在每次使用后调用 `remove()`。 +2. 在线程池等线程复用的场景下,使用 `try-finally` 块可以确保即使发生异常,`remove()` 方法也一定会被执行。 + +### ⭐️如何跨线程传递 ThreadLocal 的值? + +由于 `ThreadLocal` 的变量值存放在 `Thread` 里,而父子线程属于不同的 `Thread` 的。因此在异步场景下,父子线程的 `ThreadLocal` 值无法进行传递。 + +如果想要在异步场景下传递 `ThreadLocal` 值,有两种解决方案: + +- `InheritableThreadLocal` :`InheritableThreadLocal` 是 JDK1.2 提供的工具,继承自 `ThreadLocal` 。使用 `InheritableThreadLocal` 时,会在创建子线程时,令子线程继承父线程中的 `ThreadLocal` 值,但是无法支持线程池场景下的 `ThreadLocal` 值传递。 +- `TransmittableThreadLocal` : `TransmittableThreadLocal` (简称 TTL) 是阿里巴巴开源的工具类,继承并加强了`InheritableThreadLocal`类,可以在线程池的场景下支持 `ThreadLocal` 值传递。项目地址:。 + +#### InheritableThreadLocal 原理 + +`InheritableThreadLocal` 实现了创建异步线程时,继承父线程 `ThreadLocal` 值的功能。该类是 JDK 团队提供的,通过改造 JDK 源码包中的 `Thread` 类来实现创建线程时,`ThreadLocal` 值的传递。 + +**`InheritableThreadLocal` 的值存储在哪里?** + +在 `Thread` 类中添加了一个新的 `ThreadLocalMap` ,命名为 `inheritableThreadLocals` ,该变量用于存储需要跨线程传递的 `ThreadLocal` 值。如下: + +```JAVA +class Thread implements Runnable { + ThreadLocal.ThreadLocalMap threadLocals = null; + ThreadLocal.ThreadLocalMap inheritableThreadLocals = null; +} +``` + +**如何完成 `ThreadLocal` 值的传递?** + +通过改造 `Thread` 类的构造方法来实现,在创建 `Thread` 线程时,拿到父线程的 `inheritableThreadLocals` 变量赋值给子线程即可。相关代码如下: + +```JAVA +// Thread 的构造方法会调用 init() 方法 +private void init(/* ... */) { + // 1、获取父线程 + Thread parent = currentThread(); + // 2、将父线程的 inheritableThreadLocals 赋值给子线程 + if (inheritThreadLocals && parent.inheritableThreadLocals != null) + this.inheritableThreadLocals = + ThreadLocal.createInheritedMap(parent.inheritableThreadLocals); +} +``` + +#### TransmittableThreadLocal 原理 + +JDK 默认没有支持线程池场景下 `ThreadLocal` 值传递的功能,因此阿里巴巴开源了一套工具 `TransmittableThreadLocal` 来实现该功能。 + +阿里巴巴无法改动 JDK 的源码,因此他内部通过 **装饰器模式** 在原有的功能上做增强,以此来实现线程池场景下的 `ThreadLocal` 值传递。 + +TTL 改造的地方有两处: + +- 实现自定义的 `Thread` ,在 `run()` 方法内部做 `ThreadLocal` 变量的赋值操作。 + +- 基于 **线程池** 进行装饰,在 `execute()` 方法中,不提交 JDK 内部的 `Thread` ,而是提交自定义的 `Thread` 。 + +如果想要查看相关源码,可以引入 Maven 依赖进行下载。 + +```XML + + com.alibaba + transmittable-thread-local + 2.12.0 + +``` + +#### 应用场景 + +1. **压测流量标记**: 在压测场景中,使用 `ThreadLocal` 存储压测标记,用于区分压测流量和真实流量。如果标记丢失,可能导致压测流量被错误地当成线上流量处理。 +2. **上下文传递**:在分布式系统中,传递链路追踪信息(如 Trace ID)或用户上下文信息。 ## 线程池 @@ -189,38 +235,40 @@ static class Entry extends WeakReference> { 顾名思义,线程池就是管理一系列线程的资源池。当有任务要处理时,直接从线程池中获取线程来处理,处理完之后线程并不会立即被销毁,而是等待下一个任务。 -### 为什么要用线程池? - -池化技术想必大家已经屡见不鲜了,线程池、数据库连接池、Http 连接池等等都是对这个思想的应用。池化技术的思想主要是为了减少每次获取资源的消耗,提高对资源的利用率。 +### ⭐️为什么要用线程池? -**线程池**提供了一种限制和管理资源(包括执行一个任务)的方式。 每个**线程池**还维护一些基本统计信息,例如已完成任务的数量。 +池化技术想必大家已经屡见不鲜了,线程池、数据库连接池、HTTP 连接池等等都是对这个思想的应用。池化技术的思想主要是为了减少每次获取资源的消耗,提高对资源的利用率。 -这里借用《Java 并发编程的艺术》提到的来说一下**使用线程池的好处**: +线程池提供了一种限制和管理资源(包括执行一个任务)的方式。 每个线程池还维护一些基本统计信息,例如已完成任务的数量。使用线程池主要带来以下几个好处: -- **降低资源消耗**。通过重复利用已创建的线程降低线程创建和销毁造成的消耗。 -- **提高响应速度**。当任务到达时,任务可以不需要等到线程创建就能立即执行。 -- **提高线程的可管理性**。线程是稀缺资源,如果无限制的创建,不仅会消耗系统资源,还会降低系统的稳定性,使用线程池可以进行统一的分配,调优和监控。 +1. **降低资源消耗**:线程池里的线程是可以重复利用的。一旦线程完成了某个任务,它不会立即销毁,而是回到池子里等待下一个任务。这就避免了频繁创建和销毁线程带来的开销。 +2. **提高响应速度**:因为线程池里通常会维护一定数量的核心线程(或者说“常驻工人”),任务来了之后,可以直接交给这些已经存在的、空闲的线程去执行,省去了创建线程的时间,任务能够更快地得到处理。 +3. **提高线程的可管理性**:线程池允许我们统一管理池中的线程。我们可以配置线程池的大小(核心线程数、最大线程数)、任务队列的类型和大小、拒绝策略等。这样就能控制并发线程的总量,防止资源耗尽,保证系统的稳定性。同时,线程池通常也提供了监控接口,方便我们了解线程池的运行状态(比如有多少活跃线程、多少任务在排队等),便于调优。 ### 如何创建线程池? -**方式一:通过`ThreadPoolExecutor`构造函数来创建(推荐)。** +在 Java 中,创建线程池主要有两种方式: + +**方式一:通过 `ThreadPoolExecutor` 构造函数直接创建 (推荐)** -![通过构造方法实现](./images/java-thread-pool-summary/threadpoolexecutor构造函数.png) +![](https://oss.javaguide.cn/github/javaguide/java/concurrent/threadpoolexecutor-construtors.png) -**方式二:通过 `Executor` 框架的工具类 `Executors` 来创建。** +这是最推荐的方式,因为它允许开发者明确指定线程池的核心参数,对线程池的运行行为有更精细的控制,从而避免资源耗尽的风险。 -我们可以创建多种类型的 `ThreadPoolExecutor`: +**方式二:通过 `Executors` 工具类创建 (不推荐用于生产环境)** -- **`FixedThreadPool`**:该方法返回一个固定线程数量的线程池。该线程池中的线程数量始终不变。当有一个新的任务提交时,线程池中若有空闲线程,则立即执行。若没有,则新的任务会被暂存在一个任务队列中,待有线程空闲时,便处理在任务队列中的任务。 -- **`SingleThreadExecutor`:** 该方法返回一个只有一个线程的线程池。若多余一个任务被提交到该线程池,任务会被保存在一个任务队列中,待线程空闲,按先入先出的顺序执行队列中的任务。 -- **`CachedThreadPool`:** 该方法返回一个可根据实际情况调整线程数量的线程池。线程池的线程数量不确定,但若有空闲线程可以复用,则会优先使用可复用的线程。若所有线程均在工作,又有新的任务提交,则会创建新的线程处理任务。所有线程在当前任务执行完毕后,将返回线程池进行复用。 -- **`ScheduledThreadPool`**:该返回一个用来在给定的延迟后运行任务或者定期执行任务的线程池。 +`Executors`工具类提供的创建线程池的方法如下图所示: -对应 `Executors` 工具类中的方法如图所示: +![](https://oss.javaguide.cn/github/javaguide/java/concurrent/executors-new-thread-pool-methods.png) -![](https://oss.javaguide.cn/github/javaguide/java/concurrent/executors-inner-threadpool.png) +可以看出,通过`Executors`工具类可以创建多种类型的线程池,包括: -### 为什么不推荐使用内置线程池? +- `FixedThreadPool`:固定线程数量的线程池。该线程池中的线程数量始终不变。当有一个新的任务提交时,线程池中若有空闲线程,则立即执行。若没有,则新的任务会被暂存在一个任务队列中,待有线程空闲时,便处理在任务队列中的任务。 +- `SingleThreadExecutor`: 只有一个线程的线程池。若多余一个任务被提交到该线程池,任务会被保存在一个任务队列中,待线程空闲,按先入先出的顺序执行队列中的任务。 +- `CachedThreadPool`: 可根据实际情况调整线程数量的线程池。线程池的线程数量不确定,但若有空闲线程可以复用,则会优先使用可复用的线程。若所有线程均在工作,又有新的任务提交,则会创建新的线程处理任务。所有线程在当前任务执行完毕后,将返回线程池进行复用。 +- `ScheduledThreadPool`:给定的延迟后运行任务或者定期执行任务的线程池。 + +### ⭐️为什么不推荐使用内置线程池? 在《阿里巴巴 Java 开发手册》“并发处理”这一章节,明确指出线程资源必须通过线程池提供,不允许在应用中自行显式创建线程。 @@ -232,21 +280,19 @@ static class Entry extends WeakReference> { `Executors` 返回线程池对象的弊端如下(后文会详细介绍到): -- **`FixedThreadPool` 和 `SingleThreadExecutor`**:使用的是无界的 `LinkedBlockingQueue`,任务队列最大长度为 `Integer.MAX_VALUE`,可能堆积大量的请求,从而导致 OOM。 -- **`CachedThreadPool`**:使用的是同步队列 `SynchronousQueue`, 允许创建的线程数量为 `Integer.MAX_VALUE` ,可能会创建大量线程,从而导致 OOM。 -- **`ScheduledThreadPool` 和 `SingleThreadScheduledExecutor`** : 使用的无界的延迟阻塞队列`DelayedWorkQueue`,任务队列最大长度为 `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())); } @@ -268,7 +314,7 @@ public ScheduledThreadPoolExecutor(int corePoolSize) { } ``` -### 线程池常见参数有哪些?如何解释? +### ⭐️线程池常见参数有哪些?如何解释? ```java /** @@ -298,33 +344,111 @@ public ScheduledThreadPoolExecutor(int corePoolSize) { } ``` -**`ThreadPoolExecutor` 3 个最重要的参数:** +`ThreadPoolExecutor` 3 个最重要的参数: -- **`corePoolSize` :** 任务队列未达到队列容量时,最大可以同时运行的线程数量。 -- **`maximumPoolSize` :** 任务队列中存放的任务达到队列容量的时候,当前可以同时运行的线程数量变为最大线程数。 -- **`workQueue`:** 新任务来的时候会先判断当前运行的线程数量是否达到核心线程数,如果达到的话,新任务就会被存放在队列中。 +- `corePoolSize` : 任务队列未达到队列容量时,最大可以同时运行的线程数量。 +- `maximumPoolSize` : 任务队列中存放的任务达到队列容量的时候,当前可以同时运行的线程数量变为最大线程数。 +- `workQueue`: 新任务来的时候会先判断当前运行的线程数量是否达到核心线程数,如果达到的话,新任务就会被存放在队列中。 `ThreadPoolExecutor`其他常见参数 : -- **`keepAliveTime`**:线程池中的线程数量大于 `corePoolSize` 的时候,如果这时没有新的任务提交,核心线程外的线程不会立即销毁,而是会等待,直到等待的时间超过了 `keepAliveTime`才会被回收销毁; -- **`unit`** : `keepAliveTime` 参数的时间单位。 -- **`threadFactory`** :executor 创建新线程的时候会用到。 -- **`handler`** :饱和策略。关于饱和策略下面单独介绍一下。 +- `keepAliveTime`:当线程池中的线程数量大于 `corePoolSize` ,即有非核心线程(线程池中核心线程以外的线程)时,这些非核心线程空闲后不会立即销毁,而是会等待,直到等待的时间超过了 `keepAliveTime`才会被回收销毁。 +- `unit` : `keepAliveTime` 参数的时间单位。 +- `threadFactory` :executor 创建新线程的时候会用到。 +- `handler` :拒绝策略(后面会单独详细介绍一下)。 下面这张图可以加深你对线程池中各个参数的相互关系的理解(图片来源:《Java 性能调优实战》): -![线程池各个参数的关系](./images/java-thread-pool-summary/线程池各个参数之间的关系.png) +![线程池各个参数的关系](https://oss.javaguide.cn/github/javaguide/java/concurrent/relationship-between-thread-pool-parameters.png) + +### 线程池的核心线程会被回收吗? + +`ThreadPoolExecutor` 默认不会回收核心线程,即使它们已经空闲了。这是为了减少创建线程的开销,因为核心线程通常是要长期保持活跃的。但是,如果线程池是被用于周期性使用的场景,且频率不高(周期之间有明显的空闲时间),可以考虑将 `allowCoreThreadTimeOut(boolean value)` 方法的参数设置为 `true`,这样就会回收空闲(时间间隔由 `keepAliveTime` 指定)的核心线程了。 + +```java +public void allowCoreThreadTimeOut(boolean value) { + // 核心线程的 keepAliveTime 必须大于 0 才能启用超时机制 + if (value && keepAliveTime <= 0) { + throw new IllegalArgumentException("Core threads must have nonzero keep alive times"); + } + // 设置 allowCoreThreadTimeOut 的值 + if (value != allowCoreThreadTimeOut) { + allowCoreThreadTimeOut = value; + // 如果启用了超时机制,清理所有空闲的线程,包括核心线程 + if (value) { + interruptIdleWorkers(); + } + } +} +``` + +### 核心线程空闲时处于什么状态? + +核心线程空闲时,其状态分为以下两种情况: + +- **设置了核心线程的存活时间** :核心线程在空闲时,会处于 `WAITING` 状态,等待获取任务。如果阻塞等待的时间超过了核心线程存活时间,则该线程会退出工作,将该线程从线程池的工作线程集合中移除,线程状态变为 `TERMINATED` 状态。 +- **没有设置核心线程的存活时间** :核心线程在空闲时,会一直处于 `WAITING` 状态,等待获取任务,核心线程会一直存活在线程池中。 + +当队列中有可用任务时,会唤醒被阻塞的线程,线程的状态会由 `WAITING` 状态变为 `RUNNABLE` 状态,之后去执行对应任务。 + +接下来通过相关源码,了解一下线程池内部是如何做的。 + +线程在线程池内部被抽象为了 `Worker` ,当 `Worker` 被启动之后,会不断去任务队列中获取任务。 + +在获取任务的时候,会根据 `timed` 值来决定从任务队列( `BlockingQueue` )获取任务的行为。 + +如果「设置了核心线程的存活时间」或者「线程数量超过了核心线程数量」,则将 `timed` 标记为 `true` ,表明获取任务时需要使用 `poll()` 指定超时时间。 + +- `timed == true` :使用 `poll()` 来获取任务。使用 `poll()` 方法获取任务超时的话,则当前线程会退出执行( `TERMINATED` ),该线程从线程池中被移除。 +- `timed == false` :使用 `take()` 来获取任务。使用 `take()` 方法获取任务会让当前线程一直阻塞等待(`WAITING`)。 -### 线程池的饱和策略有哪些? +源码如下: -如果当前同时运行的线程数量达到最大线程数量并且队列也已经被放满了任务时,`ThreadPoolTaskExecutor` 定义一些策略: +```JAVA +// ThreadPoolExecutor +private Runnable getTask() { + boolean timedOut = false; + for (;;) { + // ... -- **`ThreadPoolExecutor.AbortPolicy`:** 抛出 `RejectedExecutionException`来拒绝新任务的处理。 -- **`ThreadPoolExecutor.CallerRunsPolicy`:** 调用执行自己的线程运行任务,也就是直接在调用`execute`方法的线程中运行(`run`)被拒绝的任务,如果执行程序已关闭,则会丢弃该任务。因此这种策略会降低对于新任务提交速度,影响程序的整体性能。如果您的应用程序可以承受此延迟并且你要求任何一个任务请求都要被执行的话,你可以选择这个策略。 -- **`ThreadPoolExecutor.DiscardPolicy`:** 不处理新任务,直接丢弃掉。 -- **`ThreadPoolExecutor.DiscardOldestPolicy`:** 此策略将丢弃最早的未处理的任务请求。 + // 1、如果「设置了核心线程的存活时间」或者是「线程数量超过了核心线程数量」,则 timed 为 true。 + boolean timed = allowCoreThreadTimeOut || wc > corePoolSize; + // 2、扣减线程数量。 + // wc > maximuimPoolSize:线程池中的线程数量超过最大线程数量。其中 wc 为线程池中的线程数量。 + // timed && timeOut:timeOut 表示获取任务超时。 + // 分为两种情况:核心线程设置了存活时间 && 获取任务超时,则扣减线程数量;线程数量超过了核心线程数量 && 获取任务超时,则扣减线程数量。 + if ((wc > maximumPoolSize || (timed && timedOut)) + && (wc > 1 || workQueue.isEmpty())) { + if (compareAndDecrementWorkerCount(c)) + return null; + continue; + } + try { + // 3、如果 timed 为 true,则使用 poll() 获取任务;否则,使用 take() 获取任务。 + Runnable r = timed ? + workQueue.poll(keepAliveTime, TimeUnit.NANOSECONDS) : + workQueue.take(); + // 4、获取任务之后返回。 + if (r != null) + return r; + timedOut = true; + } catch (InterruptedException retry) { + timedOut = false; + } + } +} +``` + +### ⭐️线程池的拒绝策略有哪些? -举个例子:Spring 通过 `ThreadPoolTaskExecutor` 或者我们直接通过 `ThreadPoolExecutor` 的构造函数创建线程池的时候,当我们不指定 `RejectedExecutionHandler` 饱和策略来配置线程池的时候,默认使用的是 `AbortPolicy`。在这种饱和策略下,如果队列满了,`ThreadPoolExecutor` 将抛出 `RejectedExecutionException` 异常来拒绝新来的任务 ,这代表你将丢失对这个任务的处理。如果不想丢弃任务的话,可以使用`CallerRunsPolicy`。`CallerRunsPolicy` 和其他的几个策略不同,它既不会抛弃任务,也不会抛出异常,而是将任务回退给调用者,使用调用者的线程来执行任务 +如果当前同时运行的线程数量达到最大线程数量并且队列也已经被放满了任务时,`ThreadPoolExecutor` 定义一些策略: + +- `ThreadPoolExecutor.AbortPolicy`:抛出 `RejectedExecutionException`来拒绝新任务的处理。 +- `ThreadPoolExecutor.CallerRunsPolicy`:调用执行者自己的线程运行任务,也就是直接在调用`execute`方法的线程中运行(`run`)被拒绝的任务,如果执行程序已关闭,则会丢弃该任务。因此这种策略会降低对于新任务提交速度,影响程序的整体性能。如果你的应用程序可以承受此延迟并且你要求任何一个任务请求都要被执行的话,你可以选择这个策略。 +- `ThreadPoolExecutor.DiscardPolicy`:不处理新任务,直接丢弃掉。 +- `ThreadPoolExecutor.DiscardOldestPolicy`:此策略将丢弃最早的未处理的任务请求。 + +举个例子:Spring 通过 `ThreadPoolTaskExecutor` 或者我们直接通过 `ThreadPoolExecutor` 的构造函数创建线程池的时候,当我们不指定 `RejectedExecutionHandler` 拒绝策略来配置线程池的时候,默认使用的是 `AbortPolicy`。在这种拒绝策略下,如果队列满了,`ThreadPoolExecutor` 将抛出 `RejectedExecutionException` 异常来拒绝新来的任务 ,这代表你将丢失对这个任务的处理。如果不想丢弃任务的话,可以使用`CallerRunsPolicy`。`CallerRunsPolicy` 和其他的几个策略不同,它既不会抛弃任务,也不会抛出异常,而是将任务回退给调用者,使用调用者的线程来执行任务。 ```java public static class CallerRunsPolicy implements RejectedExecutionHandler { @@ -340,26 +464,203 @@ public static class CallerRunsPolicy implements RejectedExecutionHandler { } ``` +### 如果不允许丢弃任务,应该选择哪个拒绝策略? + +根据上面对线程池拒绝策略的介绍,相信大家很容易能够得出答案是:`CallerRunsPolicy` 。 + +这里我们再来结合`CallerRunsPolicy` 的源码来看看: + +```java +public static class CallerRunsPolicy implements RejectedExecutionHandler { + + public CallerRunsPolicy() { } + + + public void rejectedExecution(Runnable r, ThreadPoolExecutor e) { + //只要当前程序没有关闭,就用执行execute方法的线程执行该任务 + if (!e.isShutdown()) { + + r.run(); + } + } + } +``` + +从源码可以看出,只要当前程序不关闭就会使用执行`execute`方法的线程执行该任务。 + +### CallerRunsPolicy 拒绝策略有什么风险?如何解决? + +我们上面也提到了:如果想要保证任何一个任务请求都要被执行的话,那选择 `CallerRunsPolicy` 拒绝策略更合适一些。 + +不过,如果走到`CallerRunsPolicy`的任务是个非常耗时的任务,且处理提交任务的线程是主线程,可能会导致主线程阻塞,影响程序的正常运行。 + +这里简单举一个例子,该线程池限定了最大线程数为 2,阻塞队列大小为 1(这意味着第 4 个任务就会走到拒绝策略),`ThreadUtil`为 Hutool 提供的工具类: + +```java +public class ThreadPoolTest { + + private static final Logger log = LoggerFactory.getLogger(ThreadPoolTest.class); + + public static void main(String[] args) { + // 创建一个线程池,核心线程数为1,最大线程数为2 + // 当线程数大于核心线程数时,多余的空闲线程存活的最长时间为60秒, + // 任务队列为容量为1的ArrayBlockingQueue,饱和策略为CallerRunsPolicy。 + ThreadPoolExecutor threadPoolExecutor = new ThreadPoolExecutor(1, + 2, + 60, + TimeUnit.SECONDS, + new ArrayBlockingQueue<>(1), + new ThreadPoolExecutor.CallerRunsPolicy()); + + // 提交第一个任务,由核心线程执行 + threadPoolExecutor.execute(() -> { + log.info("核心线程执行第一个任务"); + ThreadUtil.sleep(1, TimeUnit.MINUTES); + }); + + // 提交第二个任务,由于核心线程被占用,任务将进入队列等待 + threadPoolExecutor.execute(() -> { + log.info("非核心线程处理入队的第二个任务"); + ThreadUtil.sleep(1, TimeUnit.MINUTES); + }); + + // 提交第三个任务,由于核心线程被占用且队列已满,创建非核心线程处理 + threadPoolExecutor.execute(() -> { + log.info("非核心线程处理第三个任务"); + ThreadUtil.sleep(1, TimeUnit.MINUTES); + }); + + // 提交第四个任务,由于核心线程和非核心线程都被占用,队列也满了,根据CallerRunsPolicy策略,任务将由提交任务的线程(即主线程)来执行 + threadPoolExecutor.execute(() -> { + log.info("主线程处理第四个任务"); + ThreadUtil.sleep(2, TimeUnit.MINUTES); + }); + + // 提交第五个任务,主线程被第四个任务卡住,该任务必须等到主线程执行完才能提交 + threadPoolExecutor.execute(() -> { + log.info("核心线程执行第五个任务"); + }); + + // 关闭线程池 + threadPoolExecutor.shutdown(); + } +} + +``` + +输出: + +```bash +18:19:48.203 INFO [pool-1-thread-1] c.j.concurrent.ThreadPoolTest - 核心线程执行第一个任务 +18:19:48.203 INFO [pool-1-thread-2] c.j.concurrent.ThreadPoolTest - 非核心线程处理第三个任务 +18:19:48.203 INFO [main] c.j.concurrent.ThreadPoolTest - 主线程处理第四个任务 +18:20:48.212 INFO [pool-1-thread-2] c.j.concurrent.ThreadPoolTest - 非核心线程处理入队的第二个任务 +18:21:48.219 INFO [pool-1-thread-2] c.j.concurrent.ThreadPoolTest - 核心线程执行第五个任务 +``` + +从输出结果可以看出,因为`CallerRunsPolicy`这个拒绝策略,导致耗时的任务用了主线程执行,导致线程池阻塞,进而导致后续任务无法及时执行,严重的情况下很可能导致 OOM。 + +我们从问题的本质入手,调用者采用`CallerRunsPolicy`是希望所有的任务都能够被执行,暂时无法处理的任务又被保存在阻塞队列`BlockingQueue`中。这样的话,在内存允许的情况下,我们可以增加阻塞队列`BlockingQueue`的大小并调整堆内存以容纳更多的任务,确保任务能够被准确执行。 + +为了充分利用 CPU,我们还可以调整线程池的`maximumPoolSize` (最大线程数)参数,这样可以提高任务处理速度,避免累计在 `BlockingQueue`的任务过多导致内存用完。 + +![调整阻塞队列大小和最大线程数](https://oss.javaguide.cn/github/javaguide/java/concurrent/threadpool-reject-2-threadpool-reject-01.png) + +如果服务器资源以达到可利用的极限,这就意味我们要在设计策略上改变线程池的调度了,我们都知道,导致主线程卡死的本质就是因为我们不希望任何一个任务被丢弃。换个思路,有没有办法既能保证任务不被丢弃且在服务器有余力时及时处理呢? + +这里提供的一种**任务持久化**的思路,这里所谓的任务持久化,包括但不限于: + +1. 设计一张任务表将任务存储到 MySQL 数据库中。 +2. Redis 缓存任务。 +3. 将任务提交到消息队列中。 + +这里以方案一为例,简单介绍一下实现逻辑: + +1. 实现`RejectedExecutionHandler`接口自定义拒绝策略,自定义拒绝策略负责将线程池暂时无法处理(此时阻塞队列已满)的任务入库(保存到 MySQL 中)。注意:线程池暂时无法处理的任务会先被放在阻塞队列中,阻塞队列满了才会触发拒绝策略。 +2. 继承`BlockingQueue`实现一个混合式阻塞队列,该队列包含 JDK 自带的`ArrayBlockingQueue`。另外,该混合式阻塞队列需要修改取任务处理的逻辑,也就是重写`take()`方法,取任务时优先从数据库中读取最早的任务,数据库中无任务时再从 `ArrayBlockingQueue`中去取任务。 + +![将一部分任务保存到MySQL中](https://oss.javaguide.cn/github/javaguide/java/concurrent/threadpool-reject-2-threadpool-reject-02.png) + +整个实现逻辑还是比较简单的,核心在于自定义拒绝策略和阻塞队列。如此一来,一旦我们的线程池中线程达到满载时,我们就可以通过拒绝策略将最新任务持久化到 MySQL 数据库中,等到线程池有了有余力处理所有任务时,让其优先处理数据库中的任务以避免"饥饿"问题。 + +当然,对于这个问题,我们也可以参考其他主流框架的做法,以 Netty 为例,它的拒绝策略则是直接创建一个线程池以外的线程处理这些任务,为了保证任务的实时处理,这种做法可能需要良好的硬件设备且临时创建的线程无法做到准确的监控: + +```java +private static final class NewThreadRunsPolicy implements RejectedExecutionHandler { + NewThreadRunsPolicy() { + super(); + } + public void rejectedExecution(Runnable r, ThreadPoolExecutor executor) { + try { + //创建一个临时线程处理任务 + final Thread t = new Thread(r, "Temporary task executor"); + t.start(); + } catch (Throwable e) { + throw new RejectedExecutionException( + "Failed to start a new thread", e); + } + } +} +``` + +ActiveMQ 则是尝试在指定的时效内尽可能的争取将任务入队,以保证最大交付: + +```java +new RejectedExecutionHandler() { + @Override + public void rejectedExecution(final Runnable r, final ThreadPoolExecutor executor) { + try { + //限时阻塞等待,实现尽可能交付 + executor.getQueue().offer(r, 60, TimeUnit.SECONDS); + } catch (InterruptedException e) { + throw new RejectedExecutionException("Interrupted waiting for BrokerService.worker"); + } + throw new RejectedExecutionException("Timed Out while attempting to enqueue Task."); + } + }); +``` + ### 线程池常用的阻塞队列有哪些? 新任务来的时候会先判断当前运行的线程数量是否达到核心线程数,如果达到的话,新任务就会被存放在队列中。 不同的线程池会选用不同的阻塞队列,我们可以结合内置线程池来分析。 -- 容量为 `Integer.MAX_VALUE` 的 `LinkedBlockingQueue`(无界队列):`FixedThreadPool` 和 `SingleThreadExector` 。由于队列永远不会被放满,因此`FixedThreadPool`最多只能创建核心线程数的线程。 +- 容量为 `Integer.MAX_VALUE` 的 `LinkedBlockingQueue`(有界阻塞队列):`FixedThreadPool` 和 `SingleThreadExecutor` 。`FixedThreadPool`最多只能创建核心线程数的线程(核心线程数和最大线程数相等),`SingleThreadExecutor`只能创建一个线程(核心线程数和最大线程数都是 1),二者的任务队列永远不会被放满。 - `SynchronousQueue`(同步队列):`CachedThreadPool` 。`SynchronousQueue` 没有容量,不存储元素,目的是保证对于提交的任务,如果有空闲线程,则使用空闲线程来处理;否则新建一个线程来处理任务。也就是说,`CachedThreadPool` 的最大线程数是 `Integer.MAX_VALUE` ,可以理解为线程数是可以无限扩展的,可能会创建大量线程,从而导致 OOM。 -- `DelayedWorkQueue`(延迟阻塞队列):`ScheduledThreadPool` 和 `SingleThreadScheduledExecutor` 。`DelayedWorkQueue` 的内部元素并不是按照放入的时间排序,而是会按照延迟的时间长短对任务进行排序,内部采用的是“堆”的数据结构,可以保证每次出队的任务都是当前队列中执行时间最靠前的。`DelayedWorkQueue` 添加元素满了之后会自动扩容原来容量的 1/2,即永远不会阻塞,最大扩容可达 `Integer.MAX_VALUE`,所以最多只能创建核心线程数的线程。 +- `DelayedWorkQueue`(延迟队列):`ScheduledThreadPool` 和 `SingleThreadScheduledExecutor` 。`DelayedWorkQueue` 的内部元素并不是按照放入的时间排序,而是会按照延迟的时间长短对任务进行排序,内部采用的是“堆”的数据结构,可以保证每次出队的任务都是当前队列中执行时间最靠前的。`DelayedWorkQueue` 添加元素满了之后会自动扩容,增加原来容量的 50%,即永远不会阻塞,最大扩容可达 `Integer.MAX_VALUE`,所以最多只能创建核心线程数的线程。 +- `ArrayBlockingQueue`(有界阻塞队列):底层由数组实现,容量一旦创建,就不能修改。 -### 线程池处理任务的流程了解吗? +### ⭐️线程池处理任务的流程了解吗? -![图解线程池实现原理](https://oss.javaguide.cn/javaguide/%E5%9B%BE%E8%A7%A3%E7%BA%BF%E7%A8%8B%E6%B1%A0%E5%AE%9E%E7%8E%B0%E5%8E%9F%E7%90%86.png) +![图解线程池实现原理](https://oss.javaguide.cn/github/javaguide/java/concurrent/thread-pool-principle.png) 1. 如果当前运行的线程数小于核心线程数,那么就会新建一个线程来执行任务。 2. 如果当前运行的线程数等于或大于核心线程数,但是小于最大线程数,那么就把该任务放入到任务队列里等待执行。 3. 如果向任务队列投放任务失败(任务队列已经满了),但是当前运行的线程数是小于最大线程数的,就新建一个线程来执行任务。 -4. 如果当前运行的线程数已经等同于最大线程数了,新建线程将会使当前运行的线程超出最大线程数,那么当前任务会被拒绝,饱和策略会调用`RejectedExecutionHandler.rejectedExecution()`方法。 +4. 如果当前运行的线程数已经等同于最大线程数了,新建线程将会使当前运行的线程超出最大线程数,那么当前任务会被拒绝,拒绝策略会调用`RejectedExecutionHandler.rejectedExecution()`方法。 + +再提一个有意思的小问题:**线程池在提交任务前,可以提前创建线程吗?** + +答案是可以的!`ThreadPoolExecutor` 提供了两个方法帮助我们在提交任务之前,完成核心线程的创建,从而实现线程池预热的效果: -### 如何给线程池命名? +- `prestartCoreThread()`:启动一个线程,等待任务,如果已达到核心线程数,这个方法返回 false,否则返回 true; +- `prestartAllCoreThreads()`:启动所有的核心线程,并返回启动成功的核心线程数。 + +### ⭐️线程池中线程异常后,销毁还是复用? + +直接说结论,需要分两种情况: + +- **使用`execute()`提交任务**:当任务通过`execute()`提交到线程池并在执行过程中抛出异常时,如果这个异常没有在任务内被捕获,那么该异常会导致当前线程终止,并且异常会被打印到控制台或日志文件中。线程池会检测到这种线程终止,并创建一个新线程来替换它,从而保持配置的线程数不变。 +- **使用`submit()`提交任务**:对于通过`submit()`提交的任务,如果在任务执行中发生异常,这个异常不会直接打印出来。相反,异常会被封装在由`submit()`返回的`Future`对象中。当调用`Future.get()`方法时,可以捕获到一个`ExecutionException`。在这种情况下,线程不会因为异常而终止,它会继续存在于线程池中,准备执行后续的任务。 + +简单来说:使用`execute()`时,未捕获异常导致线程终止,线程池创建新线程替代;使用`submit()`时,异常被封装在`Future`中,线程继续复用。 + +这种设计允许`submit()`提供更灵活的错误处理机制,因为它允许调用者决定如何处理异常,而`execute()`则适用于那些不需要关注执行结果的场景。 + +具体的源码分析可以参考这篇:[线程池中线程异常后:销毁还是复用? - 京东技术](https://mp.weixin.qq.com/s/9ODjdUU-EwQFF5PrnzOGfw)。 + +### ⭐️如何给线程池命名? 初始化线程池的时候需要显示命名(设置线程池名称前缀),有利于定位问题。 @@ -367,45 +668,42 @@ public static class CallerRunsPolicy implements RejectedExecutionHandler { 给线程池里的线程命名通常有下面两种方式: -**1、利用 guava 的 `ThreadFactoryBuilder` ** +**1、利用 guava 的 `ThreadFactoryBuilder`** ```java ThreadFactory threadFactory = new ThreadFactoryBuilder() .setNameFormat(threadNamePrefix + "-%d") .setDaemon(true).build(); -ExecutorService threadPool = new ThreadPoolExecutor(corePoolSize, maximumPoolSize, keepAliveTime, TimeUnit.MINUTES, workQueue, threadFactory) +ExecutorService threadPool = new ThreadPoolExecutor(corePoolSize, maximumPoolSize, keepAliveTime, TimeUnit.MINUTES, workQueue, threadFactory); ``` -**2、自己实现 `ThreadFactor`。** +**2、自己实现 `ThreadFactory`。** ```java -import java.util.concurrent.Executors; import java.util.concurrent.ThreadFactory; import java.util.concurrent.atomic.AtomicInteger; + /** * 线程工厂,它设置线程名称,有利于我们定位问题。 */ public final class NamingThreadFactory implements ThreadFactory { private final AtomicInteger threadNum = new AtomicInteger(); - private final ThreadFactory delegate; private final String name; /** * 创建一个带名字的线程池生产工厂 */ - public NamingThreadFactory(ThreadFactory delegate, String name) { - this.delegate = delegate; - this.name = name; // TODO consider uniquifying this + public NamingThreadFactory(String name) { + this.name = name; } @Override public Thread newThread(Runnable r) { - Thread t = delegate.newThread(r); + Thread t = new Thread(r); t.setName(name + " [#" + threadNum.incrementAndGet() + "]"); return t; } - } ``` @@ -421,7 +719,7 @@ public final class NamingThreadFactory implements ThreadFactory { > > Linux 相比与其他操作系统(包括其他类 Unix 系统)有很多的优点,其中有一项就是,其上下文切换和模式切换的时间消耗非常少。 -类比于实现世界中的人类通过合作做某件事情,我们可以肯定的一点是线程池大小设置过大或者过小都会有问题,合适的才是最好。 +类比于现实世界中的人类通过合作做某件事情,我们可以肯定的一点是线程池大小设置过大或者过小都会有问题,合适的才是最好。 - 如果我们设置的线程池数量太小的话,如果同一时间有大量任务/请求需要处理,可能会导致大量的请求/任务在任务队列中排队等待执行,甚至会出现任务队列满了之后任务/请求无法处理的情况,或者大量任务堆积在任务队列导致 OOM。这样很明显是有问题的,CPU 根本没有得到充分利用。 - 如果我们设置线程数量太大,大量线程可能会同时在争取 CPU 资源,这样会导致大量的上下文切换,从而增加线程的执行时间,影响了整体执行效率。 @@ -447,9 +745,9 @@ CPU 密集型简单理解就是利用 CPU 计算能力的任务比如你在内 > > IO 密集型任务下,几乎全是线程等待时间,从理论上来说,你就可以将线程数设置为 2N(按道理来说,WT/ST 的结果应该比较大,这里选择 2N 的原因应该是为了避免创建过多线程吧)。 -公示也只是参考,具体还是要根据项目实际线上运行情况来动态调整。我在后面介绍的美团的线程池参数动态配置这种方案就非常不错,很实用! +公式也只是参考,具体还是要根据项目实际线上运行情况来动态调整。我在后面介绍的美团的线程池参数动态配置这种方案就非常不错,很实用! -### 如何动态修改线程池的参数? +### ⭐️如何动态修改线程池的参数? 美团技术团队在[《Java 线程池实现原理及其在美团业务中的实践》](https://tech.meituan.com/2020/04/02/java-pooling-pratice-in-meituan.html)这篇文章中介绍到对线程池参数实现可自定义配置的思路和方法。 @@ -467,7 +765,7 @@ CPU 密集型简单理解就是利用 CPU 计算能力的任务比如你在内 ![](https://oss.javaguide.cn/github/javaguide/java/concurrent/threadpoolexecutor-methods.png) -格外需要注意的是`corePoolSize`, 程序运行期间的时候,我们调用 `setCorePoolSize()`这个方法的话,线程池会首先判断当前工作线程数是否大于`corePoolSize`,如果大于的话就会回收工作线程。 +格外需要注意的是`corePoolSize`, 程序运行期间的时候,我们调用 `setCorePoolSize()`这个方法的话,线程池会首先判断当前工作线程数是否大于`corePoolSize`,如果大于的话就会回收工作线程。 另外,你也看到了上面并没有动态指定队列长度的方法,美团的方式是自定义了一个叫做 `ResizableCapacityLinkedBlockIngQueue` 的队列(主要就是把`LinkedBlockingQueue`的 capacity 字段的 final 关键字修饰给去掉了,让它变为可变的)。 @@ -475,15 +773,50 @@ CPU 密集型简单理解就是利用 CPU 计算能力的任务比如你在内 ![动态配置线程池参数最终效果](https://oss.javaguide.cn/github/javaguide/java/concurrent/meituan-dynamically-configuring-thread-pool-parameters.png) -还没看够?推荐 why 神的[如何设置线程池参数?美团给出了一个让面试官虎躯一震的回答。](https://mp.weixin.qq.com/s/9HLuPcoWmTqAeFKa1kj-_A)这篇文章,深度剖析,很不错哦! +还没看够?我在[《后端面试高频系统设计&场景题》](https://javaguide.cn/zhuanlan/back-end-interview-high-frequency-system-design-and-scenario-questions.html#%E4%BB%8B%E7%BB%8D)中详细介绍了如何设计一个动态线程池,这也是面试中常问的一道系统设计题。 + +![《后端面试高频系统设计&场景题》](https://oss.javaguide.cn/xingqiu/back-end-interview-high-frequency-system-design-and-scenario-questions-fengmian.png) 如果我们的项目也想要实现这种效果的话,可以借助现成的开源项目: -- **[Hippo-4](https://github.com/opengoofy/hippo4j)**:一款强大的动态线程池框架,解决了传统线程池使用存在的一些痛点比如线程池参数没办法动态修改、不支持运行时变量的传递、无法执行优雅关闭。除了支持动态修改线程池参数、线程池任务传递上下文,还支持通知报警、运行监控等开箱即用的功能。 +- **[Hippo4j](https://github.com/opengoofy/hippo4j)**:异步线程池框架,支持线程池动态变更&监控&报警,无需修改代码轻松引入。支持多种使用模式,轻松引入,致力于提高系统运行保障能力。 - **[Dynamic TP](https://github.com/dromara/dynamic-tp)**:轻量级动态线程池,内置监控告警功能,集成三方中间件线程池管理,基于主流配置中心(已支持 Nacos、Apollo,Zookeeper、Consul、Etcd,可通过 SPI 自定义实现)。 +### ⭐️如何设计一个能够根据任务的优先级来执行的线程池? + +这是一个常见的面试问题,本质其实还是在考察求职者对于线程池以及阻塞队列的掌握。 + +我们上面也提到了,不同的线程池会选用不同的阻塞队列作为任务队列,比如`FixedThreadPool` 使用的是`LinkedBlockingQueue`(有界队列),默认构造器初始的队列长度为 `Integer.MAX_VALUE` ,由于队列永远不会被放满,因此`FixedThreadPool`最多只能创建核心线程数的线程。 + +假如我们需要实现一个优先级任务线程池的话,那可以考虑使用 `PriorityBlockingQueue` (优先级阻塞队列)作为任务队列(`ThreadPoolExecutor` 的构造函数有一个 `workQueue` 参数可以传入任务队列)。 + +![ThreadPoolExecutor构造函数](https://oss.javaguide.cn/github/javaguide/java/concurrent/common-parameters-of-threadpool-workqueue.jpg) + +`PriorityBlockingQueue` 是一个支持优先级的无界阻塞队列,可以看作是线程安全的 `PriorityQueue`,两者底层都是使用小顶堆形式的二叉堆,即值最小的元素优先出队。不过,`PriorityQueue` 不支持阻塞操作。 + +要想让 `PriorityBlockingQueue` 实现对任务的排序,传入其中的任务必须是具备排序能力的,方式有两种: + +1. 提交到线程池的任务实现 `Comparable` 接口,并重写 `compareTo` 方法来指定任务之间的优先级比较规则。 +2. 创建 `PriorityBlockingQueue` 时传入一个 `Comparator` 对象来指定任务之间的排序规则(推荐)。 + +不过,这存在一些风险和问题,比如: + +- `PriorityBlockingQueue` 是无界的,可能堆积大量的请求,从而导致 OOM。 +- 可能会导致饥饿问题,即低优先级的任务长时间得不到执行。 +- 由于需要对队列中的元素进行排序操作以及保证线程安全(并发控制采用的是可重入锁 `ReentrantLock`),因此会降低性能。 + +对于 OOM 这个问题的解决比较简单粗暴,就是继承`PriorityBlockingQueue` 并重写一下 `offer` 方法(入队)的逻辑,当插入的元素数量超过指定值就返回 false 。 + +饥饿问题这个可以通过优化设计来解决(比较麻烦),比如等待时间过长的任务会被移除并重新添加到队列中,但是优先级会被提升。 + +对于性能方面的影响,是没办法避免的,毕竟需要对任务进行排序操作。并且,对于大部分业务场景来说,这点性能影响是可以接受的。 + ## Future +重点是要掌握 `CompletableFuture` 的使用以及常见面试题。 + +除了下面的面试题之外,还推荐你看看我写的这篇文章: [CompletableFuture 详解](./completablefuture-intro.md)。 + ### Future 类有什么用? `Future` 类是异步思想的典型运用,主要用在一些需要执行耗时任务的场景,避免程序一直原地等待耗时任务执行完成,执行效率太低。具体来说是这样的:当我们执行某一耗时的任务时,可以将这个耗时任务交给一个子线程去异步执行,同时我们可以干点其他事情,不用傻傻等待耗时任务执行完成。等我们的事情干完后,我们再通过 `Future` 类获取到耗时任务的执行结果。这样一来,程序的执行效率就明显提高了。 @@ -552,9 +885,11 @@ public FutureTask(Runnable runnable, V result) { `FutureTask`相当于对`Callable` 进行了封装,管理着任务执行的情况,存储了 `Callable` 的 `call` 方法的任务执行结果。 +关于更多 `Future` 的源码细节,可以肝这篇万字解析,写的很清楚:[Java 是如何实现 Future 模式的?万字详解!](https://juejin.cn/post/6844904199625375757)。 + ### CompletableFuture 类有什么用? -`Future` 在实际使用过程中存在一些局限性比如不支持异步任务的编排组合、获取计算结果的 `get()` 方法为阻塞调用。 +`Future` 在实际使用过程中存在一些局限性,比如不支持异步任务的编排组合、获取计算结果的 `get()` 方法为阻塞调用。 Java 8 才被引入`CompletableFuture` 类可以解决`Future` 的这些缺陷。`CompletableFuture` 除了提供了更为好用和强大的 `Future` 特性之外,还提供了函数式编程、异步任务编排组合(可以将多个异步任务串联起来,组成一个完整的链式调用)等能力。 @@ -575,36 +910,105 @@ public class CompletableFuture implements Future, CompletionStage { ![](https://oss.javaguide.cn/javaguide/image-20210902093026059.png) -## AQS +### ⭐️一个任务需要依赖另外两个任务执行完之后再执行,怎么设计? -### AQS 是什么? +这种任务编排场景非常适合通过`CompletableFuture`实现。这里假设要实现 T3 在 T2 和 T1 执行完后执行。 -AQS 的全称为 `AbstractQueuedSynchronizer` ,翻译过来的意思就是抽象队列同步器。这个类在 `java.util.concurrent.locks` 包下面。 +代码如下(这里为了简化代码,用到了 Hutool 的线程工具类 `ThreadUtil` 和日期时间工具类 `DateUtil`): -![](https://my-blog-to-use.oss-cn-beijing.aliyuncs.com/Java%20%E7%A8%8B%E5%BA%8F%E5%91%98%E5%BF%85%E5%A4%87%EF%BC%9A%E5%B9%B6%E5%8F%91%E7%9F%A5%E8%AF%86%E7%B3%BB%E7%BB%9F%E6%80%BB%E7%BB%93/AQS.png) +```java +// T1 +CompletableFuture futureT1 = CompletableFuture.runAsync(() -> { + System.out.println("T1 is executing. Current time:" + DateUtil.now()); + // 模拟耗时操作 + ThreadUtil.sleep(1000); +}); +// T2 +CompletableFuture futureT2 = CompletableFuture.runAsync(() -> { + System.out.println("T2 is executing. Current time:" + DateUtil.now()); + ThreadUtil.sleep(1000); +}); + +// 使用allOf()方法合并T1和T2的CompletableFuture,等待它们都完成 +CompletableFuture bothCompleted = CompletableFuture.allOf(futureT1, futureT2); +// 当T1和T2都完成后,执行T3 +bothCompleted.thenRunAsync(() -> System.out.println("T3 is executing after T1 and T2 have completed.Current time:" + DateUtil.now())); +// 等待所有任务完成,验证效果 +ThreadUtil.sleep(3000); +``` + +通过 `CompletableFuture` 的 `allOf()` 这个静态方法来并行运行 T1 和 T2,当 T1 和 T2 都完成后,再执行 T3。 + +### ⭐️使用 CompletableFuture,有一个任务失败,如何处理异常? + +使用 `CompletableFuture`的时候一定要以正确的方式进行异常处理,避免异常丢失或者出现不可控问题。 + +下面是一些建议: + +- 使用 `whenComplete` 方法可以在任务完成时触发回调函数,并正确地处理异常,而不是让异常被吞噬或丢失。 +- 使用 `exceptionally` 方法可以处理异常并重新抛出,以便异常能够传播到后续阶段,而不是让异常被忽略或终止。 +- 使用 `handle` 方法可以处理正常的返回结果和异常,并返回一个新的结果,而不是让异常影响正常的业务逻辑。 +- 使用 `CompletableFuture.allOf` 方法可以组合多个 `CompletableFuture`,并统一处理所有任务的异常,而不是让异常处理过于冗长或重复。 +- …… -AQS 就是一个抽象类,主要用来构建锁和同步器。 +### ⭐️在使用 CompletableFuture 的时候为什么要自定义线程池? + +`CompletableFuture` 默认使用全局共享的 `ForkJoinPool.commonPool()` 作为执行器,所有未指定执行器的异步任务都会使用该线程池。这意味着应用程序、多个库或框架(如 Spring、第三方库)若都依赖 `CompletableFuture`,默认情况下它们都会共享同一个线程池。 + +虽然 `ForkJoinPool` 效率很高,但当同时提交大量任务时,可能会导致资源竞争和线程饥饿,进而影响系统性能。 + +为避免这些问题,建议为 `CompletableFuture` 提供自定义线程池,带来以下优势: + +- 隔离性:为不同任务分配独立的线程池,避免全局线程池资源争夺。 +- 资源控制:根据任务特性调整线程池大小和队列类型,优化性能表现。 +- 异常处理:通过自定义 `ThreadFactory` 更好地处理线程中的异常情况。 ```java -public abstract class AbstractQueuedSynchronizer extends AbstractOwnableSynchronizer implements java.io.Serializable { -} +private ThreadPoolExecutor executor = new ThreadPoolExecutor(10, 10, + 0L, TimeUnit.MILLISECONDS, + new LinkedBlockingQueue()); + +CompletableFuture.runAsync(() -> { + //... +}, executor); ``` -AQS 为构建锁和同步器提供了一些通用功能的实现,因此,使用 AQS 能简单且高效地构造出应用广泛的大量的同步器,比如我们提到的 `ReentrantLock`,`Semaphore`,其他的诸如 `ReentrantReadWriteLock`,`SynchronousQueue`等等皆是基于 AQS 的。 +## AQS + +关于 AQS 源码的详细分析,可以看看这一篇文章:[AQS 详解](./aqs.md)。 + +### AQS 是什么? + +AQS (`AbstractQueuedSynchronizer` ,抽象队列同步器)是从 JDK1.5 开始提供的 Java 并发核心组件。 + +AQS 解决了开发者在实现同步器时的复杂性问题。它提供了一个通用框架,用于实现各种同步器,例如 **可重入锁**(`ReentrantLock`)、**信号量**(`Semaphore`)和 **倒计时器**(`CountDownLatch`)。通过封装底层的线程同步机制,AQS 将复杂的线程管理逻辑隐藏起来,使开发者只需专注于具体的同步逻辑。 + +简单来说,AQS 是一个抽象类,为同步器提供了通用的 **执行框架**。它定义了 **资源获取和释放的通用流程**,而具体的资源获取逻辑则由具体同步器通过重写模板方法来实现。 因此,可以将 AQS 看作是同步器的 **基础“底座”**,而同步器则是基于 AQS 实现的 **具体“应用”**。 + +### ⭐️AQS 的原理是什么? -### AQS 的原理是什么? +AQS 核心思想是,如果被请求的共享资源空闲,则将当前请求资源的线程设置为有效的工作线程,并且将共享资源设置为锁定状态。如果被请求的共享资源被占用,那么就需要一套线程阻塞等待以及被唤醒时锁分配的机制,这个机制 AQS 是基于 **CLH 锁** (Craig, Landin, and Hagersten locks) 进一步优化实现的。 -AQS 核心思想是,如果被请求的共享资源空闲,则将当前请求资源的线程设置为有效的工作线程,并且将共享资源设置为锁定状态。如果被请求的共享资源被占用,那么就需要一套线程阻塞等待以及被唤醒时锁分配的机制,这个机制 AQS 是用 **CLH 队列锁** 实现的,即将暂时获取不到锁的线程加入到队列中。 +**CLH 锁** 对自旋锁进行了改进,是基于单链表的自旋锁。在多线程场景下,会将请求获取锁的线程组织成一个单向队列,每个等待的线程会通过自旋访问前一个线程节点的状态,前一个节点释放锁之后,当前节点才可以获取锁。**CLH 锁** 的队列结构如下图所示。 -CLH(Craig,Landin,and Hagersten) 队列是一个虚拟的双向队列(虚拟的双向队列即不存在队列实例,仅存在结点之间的关联关系)。AQS 是将每条请求共享资源的线程封装成一个 CLH 锁队列的一个结点(Node)来实现锁的分配。在 CLH 同步队列中,一个节点表示一个线程,它保存着线程的引用(thread)、 当前节点在队列中的状态(waitStatus)、前驱节点(prev)、后继节点(next)。 +![CLH 锁的队列结构](https://oss.javaguide.cn/github/javaguide/open-source-project/clh-lock-queue-structure.png) -CLH 队列结构如下图所示: +AQS 中使用的 **等待队列** 是 CLH 锁队列的变体(接下来简称为 CLH 变体队列)。 -![](https://p3-juejin.byteimg.com/tos-cn-i-k3u1fbpfcp/40cb932a64694262993907ebda6a0bfe~tplv-k3u1fbpfcp-zoom-1.image) +AQS 的 CLH 变体队列是一个双向队列,会暂时获取不到锁的线程将被加入到该队列中,CLH 变体队列和原本的 CLH 锁队列的区别主要有两点: -AQS(`AbstractQueuedSynchronizer`)的核心原理图(图源[Java 并发之 AQS 详解](https://www.cnblogs.com/waterystone/p/4920797.html))如下: +- 由 **自旋** 优化为 **自旋 + 阻塞** :自旋操作的性能很高,但大量的自旋操作比较占用 CPU 资源,因此在 CLH 变体队列中会先通过自旋尝试获取锁,如果失败再进行阻塞等待。 +- 由 **单向队列** 优化为 **双向队列** :在 CLH 变体队列中,会对等待的线程进行阻塞操作,当队列前边的线程释放锁之后,需要对后边的线程进行唤醒,因此增加了 `next` 指针,成为了双向队列。 -![](https://my-blog-to-use.oss-cn-beijing.aliyuncs.com/Java%20%E7%A8%8B%E5%BA%8F%E5%91%98%E5%BF%85%E5%A4%87%EF%BC%9A%E5%B9%B6%E5%8F%91%E7%9F%A5%E8%AF%86%E7%B3%BB%E7%BB%9F%E6%80%BB%E7%BB%93/CLH.png) +AQS 将每条请求共享资源的线程封装成一个 CLH 变体队列的一个结点(Node)来实现锁的分配。在 CLH 变体队列中,一个节点表示一个线程,它保存着线程的引用(thread)、 当前节点在队列中的状态(waitStatus)、前驱节点(prev)、后继节点(next)。 + +AQS 中的 CLH 变体队列结构如下图所示: + +![CLH 变体队列结构](https://oss.javaguide.cn/github/javaguide/java/concurrent/clh-queue-structure-bianti.png) + +AQS(`AbstractQueuedSynchronizer`)的核心原理图: + +![CLH 变体队列](https://oss.javaguide.cn/github/javaguide/java/concurrent/clh-queue-state.png) AQS 使用 **int 成员变量 `state` 表示同步状态**,通过内置的 **线程等待队列** 来完成获取资源线程的排队工作。 @@ -634,7 +1038,7 @@ protected final boolean compareAndSetState(int expect, int update) { 以 `ReentrantLock` 为例,`state` 初始值为 0,表示未锁定状态。A 线程 `lock()` 时,会调用 `tryAcquire()` 独占该锁并将 `state+1` 。此后,其他线程再 `tryAcquire()` 时就会失败,直到 A 线程 `unlock()` 到 `state=`0(即释放锁)为止,其它线程才有机会获取该锁。当然,释放锁之前,A 线程自己是可以重复获取此锁的(`state` 会累加),这就是可重入的概念。但要注意,获取多少次就要释放多少次,这样才能保证 state 是能回到零态的。 -再以 `CountDownLatch` 以例,任务分为 N 个子线程去执行,`state` 也初始化为 N(注意 N 要与线程个数一致)。这 N 个子线程是并行执行的,每个子线程执行完后`countDown()` 一次,state 会 CAS(Compare and Swap) 减 1。等到所有子线程都执行完后(即 `state=0` ),会 `unpark()` 主调用线程,然后主调用线程就会从 `await()` 函数返回,继续后余动作。 +再以 `CountDownLatch` 以例,任务分为 N 个子线程去执行,`state` 也初始化为 N(注意 N 要与线程个数一致)。这 N 个子线程是并行执行的,每个子线程执行完后`countDown()` 一次,state 会 CAS(Compare and Swap) 减 1。等到所有子线程都执行完后(即 `state=0` ),会 `unpark()` 主调用线程,然后主调用线程就会从 `await()` 函数返回,继续后续动作。 ### Semaphore 有什么用? @@ -662,11 +1066,11 @@ semaphore.release(); ```java public Semaphore(int permits) { - sync = new NonfairSync(permits); + sync = new NonfairSync(permits); } public Semaphore(int permits, boolean fair) { - sync = fair ? new FairSync(permits) : new NonfairSync(permits); + sync = fair ? new FairSync(permits) : new NonfairSync(permits); } ``` @@ -685,7 +1089,7 @@ public Semaphore(int permits, boolean fair) { * 获取1个许可证 */ public void acquire() throws InterruptedException { - sync.acquireSharedInterruptibly(1); + sync.acquireSharedInterruptibly(1); } /** * 共享模式下获取许可证,获取成功则返回,失败则加入阻塞队列,挂起线程 @@ -705,7 +1109,7 @@ public final void acquireSharedInterruptibly(int arg) ```java // 释放一个许可证 public void release() { - sync.releaseShared(1); + sync.releaseShared(1); } // 释放共享锁,同时会唤醒同步队列中的一个线程。 @@ -728,7 +1132,7 @@ public final boolean releaseShared(int arg) { ### CountDownLatch 的原理是什么? -`CountDownLatch` 是共享锁的一种实现,它默认构造 AQS 的 `state` 值为 `count`。当线程使用 `countDown()` 方法时,其实使用了`tryReleaseShared`方法以 CAS 的操作来减少 `state`,直至 `state` 为 0 。当调用 `await()` 方法的时候,如果 `state` 不为 0,那就证明任务还没有执行完毕,`await()` 方法就会一直阻塞,也就是说 `await()` 方法之后的语句不会被执行。然后,`CountDownLatch` 会自旋 CAS 判断 `state == 0`,如果 `state == 0` 的话,就会释放所有等待的线程,`await()` 方法之后的语句得到执行。 +`CountDownLatch` 是共享锁的一种实现,它默认构造 AQS 的 `state` 值为 `count`。当线程使用 `countDown()` 方法时,其实使用了`tryReleaseShared`方法以 CAS 的操作来减少 `state`,直至 `state` 为 0 。当调用 `await()` 方法的时候,如果 `state` 不为 0,那就证明任务还没有执行完毕,`await()` 方法就会一直阻塞,也就是说 `await()` 方法之后的语句不会被执行。直到`count` 个线程调用了`countDown()`使 state 值被减为 0,或者调用`await()`的线程被中断,该线程才会从阻塞中被唤醒,`await()` 方法之后的语句得到执行。 ### 用过 CountDownLatch 么?什么场景下用的? @@ -815,7 +1219,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)。它要做的事情是:让一组线程到达一个屏障(也可以叫同步点)时被阻塞,直到最后一个线程到达屏障时,屏障才会开门,所有被屏障拦截的线程才会继续干活。 @@ -854,9 +1258,9 @@ public CyclicBarrier(int parties, Runnable barrierAction) { ```java public int await() throws InterruptedException, BrokenBarrierException { try { - return dowait(false, 0L); + return dowait(false, 0L); } catch (TimeoutException toe) { - throw new Error(toe); // cannot happen + throw new Error(toe); // cannot happen } } ``` @@ -943,12 +1347,21 @@ public int await() throws InterruptedException, BrokenBarrierException { } ``` +## 虚拟线程 + +虚拟线程在 Java 21 正式发布,这是一项重量级的更新。 + +虽然目前面试中问的不多,但还是建议大家去简单了解一下,具体可以阅读这篇文章:[虚拟线程极简入门](./virtual-thread.md) 。重点搞清楚虚拟线程和平台线程的关系以及虚拟线程的优势即可。 + ## 参考 - 《深入理解 Java 虚拟机》 - 《实战 Java 高并发程序设计》 -- 带你了解下 SynchronousQueue(并发队列专题):https://juejin.cn/post/7031196740128768037 -- 阻塞队列 — DelayedWorkQueue 源码分析:https://zhuanlan.zhihu.com/p/310621485 -- Java 多线程(三)——FutureTask/CompletableFuture:https://www.cnblogs.com/iwehdio/p/14285282.html -- Java 并发之 AQS 详解:https://www.cnblogs.com/waterystone/p/4920797.html -- Java 并发包基石-AQS 详解:https://www.cnblogs.com/chengxiao/archive/2017/07/24/7141160.html +- Java 线程池的实现原理及其在业务中的最佳实践:阿里云开发者: +- 带你了解下 SynchronousQueue(并发队列专题): +- 阻塞队列 — DelayedWorkQueue 源码分析: +- Java 多线程(三)——FutureTask/CompletableFuture: +- Java 并发之 AQS 详解: +- Java 并发包基石-AQS 详解: + + diff --git a/docs/java/concurrent/java-thread-pool-best-practices.md b/docs/java/concurrent/java-thread-pool-best-practices.md index 8d8c2c91d0e..04154bfa378 100644 --- a/docs/java/concurrent/java-thread-pool-best-practices.md +++ b/docs/java/concurrent/java-thread-pool-best-practices.md @@ -13,9 +13,9 @@ tag: `Executors` 返回线程池对象的弊端如下(后文会详细介绍到): -- **`FixedThreadPool` 和 `SingleThreadExecutor`**:使用的是无界的 `LinkedBlockingQueue`,任务队列最大长度为 `Integer.MAX_VALUE`,可能堆积大量的请求,从而导致 OOM。 -- **`CachedThreadPool`**:使用的是同步队列 `SynchronousQueue`, 允许创建的线程数量为 `Integer.MAX_VALUE` ,可能会创建大量线程,从而导致 OOM。 -- **`ScheduledThreadPool` 和 `SingleThreadScheduledExecutor`** : 使用的无界的延迟阻塞队列`DelayedWorkQueue`,任务队列最大长度为 `Integer.MAX_VALUE`,可能堆积大量的请求,从而导致 OOM。 +- **`FixedThreadPool` 和 `SingleThreadExecutor`**:使用的是阻塞队列 `LinkedBlockingQueue`,任务队列的默认长度和最大长度为 `Integer.MAX_VALUE`,可以看作是无界队列,可能堆积大量的请求,从而导致 OOM。 +- **`CachedThreadPool`**:使用的是同步队列 `SynchronousQueue`,允许创建的线程数量为 `Integer.MAX_VALUE` ,可能会创建大量线程,从而导致 OOM。 +- **`ScheduledThreadPool` 和 `SingleThreadScheduledExecutor`** : 使用的无界的延迟阻塞队列`DelayedWorkQueue`,任务队列最大长度为 `Integer.MAX_VALUE`,可能堆积大量的请求,从而导致 OOM。 说白了就是:**使用有界队列,控制线程创建数量。** @@ -59,7 +59,7 @@ public static void printThreadPoolStatus(ThreadPoolExecutor threadPool) { 一般建议是不同的业务使用不同的线程池,配置线程池的时候根据当前业务的情况对当前线程池进行配置,因为不同的业务的并发以及对资源的使用情况都不同,重心优化系统性能瓶颈相关的业务。 -**我们再来看一个真实的事故案例!** (本案例来源自:[《线程池运用不当的一次线上事故》](https://club.perfma.com/article/646639) ,很精彩的一个案例) +**我们再来看一个真实的事故案例!** (本案例来源自:[《线程池运用不当的一次线上事故》](https://heapdump.cn/article/646639) ,很精彩的一个案例) ![案例代码概览](https://oss.javaguide.cn/github/javaguide/java/concurrent/production-accident-threadpool-sharing-example.png) @@ -88,36 +88,33 @@ ThreadFactory threadFactory = new ThreadFactoryBuilder() ExecutorService threadPool = new ThreadPoolExecutor(corePoolSize, maximumPoolSize, keepAliveTime, TimeUnit.MINUTES, workQueue, threadFactory) ``` -**2、自己实现 `ThreadFactor`。** +**2、自己实现 `ThreadFactory`。** ```java -import java.util.concurrent.Executors; import java.util.concurrent.ThreadFactory; import java.util.concurrent.atomic.AtomicInteger; + /** * 线程工厂,它设置线程名称,有利于我们定位问题。 */ public final class NamingThreadFactory implements ThreadFactory { private final AtomicInteger threadNum = new AtomicInteger(); - private final ThreadFactory delegate; private final String name; /** * 创建一个带名字的线程池生产工厂 */ - public NamingThreadFactory(ThreadFactory delegate, String name) { - this.delegate = delegate; - this.name = name; // TODO consider uniquifying this + public NamingThreadFactory(String name) { + this.name = name; } @Override public Thread newThread(Runnable r) { - Thread t = delegate.newThread(r); + Thread t = new Thread(r); t.setName(name + " [#" + threadNum.incrementAndGet() + "]"); return t; } - } ``` @@ -125,11 +122,11 @@ public final class NamingThreadFactory implements ThreadFactory { 说到如何给线程池配置参数,美团的骚操作至今让我难忘(后面会提到)! -我们先来看一下各种书籍和博客上一般推荐的配置线程池参数的方式,可以作为参考! +我们先来看一下各种书籍和博客上一般推荐的配置线程池参数的方式,可以作为参考。 ### 常规操作 -很多人甚至可能都会觉得把线程池配置过大一点比较好!我觉得这明显是有问题的。就拿我们生活中非常常见的一例子来说:**并不是人多就能把事情做好,增加了沟通交流成本。你本来一件事情只需要 3 个人做,你硬是拉来了 6 个人,会提升做事效率嘛?我想并不会。** 线程数量过多的影响也是和我们分配多少人做事情一样,对于多线程这个场景来说主要是增加了**上下文切换**成本。不清楚什么是上下文切换的话,可以看我下面的介绍。 +很多人甚至可能都会觉得把线程池配置过大一点比较好!我觉得这明显是有问题的。就拿我们生活中非常常见的一例子来说:**并不是人多就能把事情做好,增加了沟通交流成本。你本来一件事情只需要 3 个人做,你硬是拉来了 6 个人,会提升做事效率嘛?我想并不会。** 线程数量过多的影响也是和我们分配多少人做事情一样,对于多线程这个场景来说主要是增加了**上下文切换** 成本。不清楚什么是上下文切换的话,可以看我下面的介绍。 > 上下文切换: > @@ -139,33 +136,38 @@ public final class NamingThreadFactory implements ThreadFactory { > > Linux 相比与其他操作系统(包括其他类 Unix 系统)有很多的优点,其中有一项就是,其上下文切换和模式切换的时间消耗非常少。 -类比于实现世界中的人类通过合作做某件事情,我们可以肯定的一点是线程池大小设置过大或者过小都会有问题,合适的才是最好。 +类比于现实世界中的人类通过合作做某件事情,我们可以肯定的一点是线程池大小设置过大或者过小都会有问题,合适的才是最好。 - 如果我们设置的线程池数量太小的话,如果同一时间有大量任务/请求需要处理,可能会导致大量的请求/任务在任务队列中排队等待执行,甚至会出现任务队列满了之后任务/请求无法处理的情况,或者大量任务堆积在任务队列导致 OOM。这样很明显是有问题的,CPU 根本没有得到充分利用。 - 如果我们设置线程数量太大,大量线程可能会同时在争取 CPU 资源,这样会导致大量的上下文切换,从而增加线程的执行时间,影响了整体执行效率。 有一个简单并且适用面比较广的公式: -- **CPU 密集型任务(N+1):** 这种任务消耗的主要是 CPU 资源,可以将线程数设置为 N(CPU 核心数)+1。比 CPU 核心数多出来的一个线程是为了防止线程偶发的缺页中断,或者其它原因导致的任务暂停而带来的影响。一旦任务暂停,CPU 就会处于空闲状态,而在这种情况下多出来的一个线程就可以充分利用 CPU 的空闲时间。 -- **I/O 密集型任务(2N):** 这种任务应用起来,系统会用大部分的时间来处理 I/O 交互,而线程在处理 I/O 的时间段内不会占用 CPU 来处理,这时就可以将 CPU 交出给其它线程使用。因此在 I/O 密集型任务的应用中,我们可以多配置一些线程,具体的计算方法是 2N。 +- **CPU 密集型任务 (N):** 这种任务消耗的主要是 CPU 资源,线程数应设置为 N(CPU 核心数)。由于任务主要瓶颈在于 CPU 计算能力,与核心数相等的线程数能够最大化 CPU 利用率,过多线程反而会导致竞争和上下文切换开销。 +- **I/O 密集型任务(M \* N):** 这类任务大部分时间处理 I/O 交互,线程在等待 I/O 时不占用 CPU。 为了充分利用 CPU 资源,线程数可以设置为 M \* N,其中 N 是 CPU 核心数,M 是一个大于 1 的倍数,建议默认设置为 2 ,具体取值取决于 I/O 等待时间和任务特点,需要通过测试和监控找到最佳平衡点。 + +CPU 密集型任务不再推荐 N+1,原因如下: + +- "N+1" 的初衷是希望预留线程处理突发暂停,但实际上,处理缺页中断等情况仍然需要占用 CPU 核心。 +- CPU 密集场景下,CPU 始终是瓶颈,预留线程并不能凭空增加 CPU 处理能力,反而可能加剧竞争。 **如何判断是 CPU 密集任务还是 IO 密集任务?** CPU 密集型简单理解就是利用 CPU 计算能力的任务比如你在内存中对大量数据进行排序。但凡涉及到网络读取,文件读取这类都是 IO 密集型,这类任务的特点是 CPU 计算耗费时间相比于等待 IO 操作完成的时间来说很少,大部分时间都花在了等待 IO 操作完成上。 -> 🌈 拓展一下(参见:[issue#1737](https://github.com/Snailclimb/JavaGuide/issues/1737)): -> -> 线程数更严谨的计算的方法应该是:`最佳线程数 = N(CPU 核心数)∗(1+WT(线程等待时间)/ST(线程计算时间))`,其中 `WT(线程等待时间)=线程运行总时间 - ST(线程计算时间)`。 -> -> 线程等待时间所占比例越高,需要越多线程。线程计算时间所占比例越高,需要越少线程。 -> -> 我们可以通过 JDK 自带的工具 VisualVM 来查看 `WT/ST` 比例。 -> -> CPU 密集型任务的 `WT/ST` 接近或者等于 0,因此, 线程数可以设置为 N(CPU 核心数)∗(1+0)= N,和我们上面说的 N(CPU 核心数)+1 差不多。 -> -> IO 密集型任务下,几乎全是线程等待时间,从理论上来说,你就可以将线程数设置为 2N(按道理来说,WT/ST 的结果应该比较大,这里选择 2N 的原因应该是为了避免创建过多线程吧)。 +🌈 拓展一下(参见:[issue#1737](https://github.com/Snailclimb/JavaGuide/issues/1737)): + +线程数更严谨的计算的方法应该是:`最佳线程数 = N(CPU 核心数)∗(1+WT(线程等待时间)/ST(线程计算时间))`,其中 `WT(线程等待时间)=线程运行总时间 - ST(线程计算时间)`。 + +线程等待时间所占比例越高,需要越多线程。线程计算时间所占比例越高,需要越少线程。 + +我们可以通过 JDK 自带的工具 VisualVM 来查看 `WT/ST` 比例。 -**公示也只是参考,具体还是要根据项目实际线上运行情况来动态调整。我在后面介绍的美团的线程池参数动态配置这种方案就非常不错,很实用!** +CPU 密集型任务的 `WT/ST` 接近或者等于 0,因此, 线程数可以设置为 N(CPU 核心数)∗(1+0)= N,和我们上面说的 N(CPU 核心数)+1 差不多。 + +IO 密集型任务下,几乎全是线程等待时间,从理论上来说,你就可以将线程数设置为 2N(按道理来说,WT/ST 的结果应该比较大,这里选择 2N 的原因应该是为了避免创建过多线程吧)。 + +**注意**:上面提到的公示也只是参考,实际项目不太可能直接按照公式来设置线程池参数,毕竟不同的业务场景对应的需求不同,具体还是要根据项目实际线上运行情况来动态调整。接下来介绍的美团的线程池参数动态配置这种方案就非常不错,很实用! ### 美团的骚操作 @@ -173,7 +175,7 @@ CPU 密集型简单理解就是利用 CPU 计算能力的任务比如你在内 美团技术团队的思路是主要对线程池的核心参数实现自定义可配置。这三个核心参数是: -- **`corePoolSize` :** 核心线程数线程数定义了最小可以同时运行的线程数量。 +- **`corePoolSize` :** 核心线程数定义了最小可以同时运行的线程数量。 - **`maximumPoolSize` :** 当队列中存放的任务达到队列容量的时候,当前可以同时运行的线程数量变为最大线程数。 - **`workQueue`:** 当新任务来的时候会先判断当前运行的线程数量是否达到核心线程数,如果达到的话,新任务就会被存放在队列中。 @@ -195,7 +197,7 @@ CPU 密集型简单理解就是利用 CPU 计算能力的任务比如你在内 如果我们的项目也想要实现这种效果的话,可以借助现成的开源项目: -- **[Hippo-4](https://github.com/opengoofy/hippo4j)**:一款强大的动态线程池框架,解决了传统线程池使用存在的一些痛点比如线程池参数没办法动态修改、不支持运行时变量的传递、无法执行优雅关闭。除了支持动态修改线程池参数、线程池任务传递上下文,还支持通知报警、运行监控等开箱即用的功能。 +- **[Hippo4j](https://github.com/opengoofy/hippo4j)**:异步线程池框架,支持线程池动态变更&监控&报警,无需修改代码轻松引入。支持多种使用模式,轻松引入,致力于提高系统运行保障能力。 - **[Dynamic TP](https://github.com/dromara/dynamic-tp)**:轻量级动态线程池,内置监控告警功能,集成三方中间件线程池管理,基于主流配置中心(已支持 Nacos、Apollo,Zookeeper、Consul、Etcd,可通过 SPI 自定义实现)。 ## 6、别忘记关闭线程池 @@ -205,7 +207,7 @@ CPU 密集型简单理解就是利用 CPU 计算能力的任务比如你在内 线程池提供了两个关闭方法: - **`shutdown()`** :关闭线程池,线程池的状态变为 `SHUTDOWN`。线程池不再接受新任务了,但是队列里的任务得执行完毕。 -- **`shutdownNow()`** :关闭线程池,线程的状态变为 `STOP`。线程池会终止当前正在运行的任务,并停止处理排队的任务并返回正在等待执行的 List。 +- **`shutdownNow()`** :关闭线程池,线程池的状态变为 `STOP`。线程池会终止当前正在运行的任务,停止处理排队的任务并返回正在等待执行的 List。 调用完 `shutdownNow` 和 `shuwdown` 方法后,并不代表线程池已经完成关闭操作,它只是异步的通知线程池进行关闭处理。如果要同步等待线程池彻底关闭后才继续往下执行,需要调用`awaitTermination`方法进行同步等待。 @@ -230,7 +232,7 @@ try { 线程池本身的目的是为了提高任务执行效率,避免因频繁创建和销毁线程而带来的性能开销。如果将耗时任务提交到线程池中执行,可能会导致线程池中的线程被长时间占用,无法及时响应其他任务,甚至会导致线程池崩溃或者程序假死。 -因此,在使用线程池时,我们应该尽量避免将耗时任务提交到线程池中执行。对于一些比较耗时的操作,如网络请求、文件读写等,可以采用异步操作的方式来处理,以避免阻塞线程池中的线程。 +因此,在使用线程池时,我们应该尽量避免将耗时任务提交到线程池中执行。对于一些比较耗时的操作,如网络请求、文件读写等,可以采用 `CompletableFuture` 等其他异步操作的方式来处理,以避免阻塞线程池中的线程。 ## 8、线程池使用的一些小坑 @@ -295,4 +297,6 @@ server.tomcat.max-threads=1 解决上述问题比较建议的办法是使用阿里巴巴开源的 `TransmittableThreadLocal`(`TTL`)。`TransmittableThreadLocal`类继承并加强了 JDK 内置的`InheritableThreadLocal`类,在使用线程池等会池化复用线程的执行组件情况下,提供`ThreadLocal`值的传递功能,解决异步执行时上下文传递的问题。 -`TransmittableThreadLocal` 项目地址:https://github.com/alibaba/transmittable-thread-local 。 +`TransmittableThreadLocal` 项目地址: 。 + + diff --git a/docs/java/concurrent/java-thread-pool-summary.md b/docs/java/concurrent/java-thread-pool-summary.md index fd7b04a150f..47a1f916de2 100644 --- a/docs/java/concurrent/java-thread-pool-summary.md +++ b/docs/java/concurrent/java-thread-pool-summary.md @@ -5,6 +5,8 @@ tag: - Java并发 --- + + 池化技术想必大家已经屡见不鲜了,线程池、数据库连接池、HTTP 连接池等等都是对这个思想的应用。池化技术的思想主要是为了减少每次获取资源的消耗,提高对资源的利用率。 这篇文章我会详细介绍一下线程池的基本概念以及核心原理。 @@ -80,7 +82,7 @@ public class ScheduledThreadPoolExecutor 线程池实现类 `ThreadPoolExecutor` 是 `Executor` 框架最核心的类。 -### 构造方法介绍 +### 线程池参数分析 `ThreadPoolExecutor` 类中提供的四个构造方法。我们来看最长的那个,其余三个都是在这个构造方法的基础上产生(其他几个构造方法说白点都是给定某些默认参数的构造方法比如默认制定拒绝策略是什么)。 @@ -112,76 +114,92 @@ public class ScheduledThreadPoolExecutor } ``` -下面这些对创建非常重要,在后面使用线程池的过程中你一定会用到!所以,务必拿着小本本记清楚。 +下面这些参数非常重要,在后面使用线程池的过程中你一定会用到!所以,务必拿着小本本记清楚。 -**`ThreadPoolExecutor` 3 个最重要的参数:** +`ThreadPoolExecutor` 3 个最重要的参数: -- **`corePoolSize` :** 任务队列未达到队列容量时,最大可以同时运行的线程数量。 -- **`maximumPoolSize` :** 任务队列中存放的任务达到队列容量的时候,当前可以同时运行的线程数量变为最大线程数。 -- **`workQueue`:** 新任务来的时候会先判断当前运行的线程数量是否达到核心线程数,如果达到的话,新任务就会被存放在队列中。 +- `corePoolSize` : 任务队列未达到队列容量时,最大可以同时运行的线程数量。 +- `maximumPoolSize` : 任务队列中存放的任务达到队列容量的时候,当前可以同时运行的线程数量变为最大线程数。 +- `workQueue`: 新任务来的时候会先判断当前运行的线程数量是否达到核心线程数,如果达到的话,新任务就会被存放在队列中。 `ThreadPoolExecutor`其他常见参数 : -- **`keepAliveTime`**:线程池中的线程数量大于 `corePoolSize` 的时候,如果这时没有新的任务提交,核心线程外的线程不会立即销毁,而是会等待,直到等待的时间超过了 `keepAliveTime`才会被回收销毁。 -- **`unit`** : `keepAliveTime` 参数的时间单位。 -- **`threadFactory`** :executor 创建新线程的时候会用到。 -- **`handler`** :饱和策略。关于饱和策略下面单独介绍一下。 +- `keepAliveTime`:线程池中的线程数量大于 `corePoolSize` 的时候,如果这时没有新的任务提交,核心线程外的线程不会立即销毁,而是会等待,直到等待的时间超过了 `keepAliveTime`才会被回收销毁。 +- `unit` : `keepAliveTime` 参数的时间单位。 +- `threadFactory` :executor 创建新线程的时候会用到。 +- `handler` :拒绝策略(后面会单独详细介绍一下)。 下面这张图可以加深你对线程池中各个参数的相互关系的理解(图片来源:《Java 性能调优实战》): -![线程池各个参数的关系](./images/java-thread-pool-summary/线程池各个参数之间的关系.png) +![线程池各个参数的关系](https://oss.javaguide.cn/github/javaguide/java/concurrent/relationship-between-thread-pool-parameters.png) -**`ThreadPoolExecutor` 饱和策略定义:** +**`ThreadPoolExecutor` 拒绝策略定义:** -如果当前同时运行的线程数量达到最大线程数量并且队列也已经被放满了任务时,`ThreadPoolTaskExecutor` 定义一些策略: +如果当前同时运行的线程数量达到最大线程数量并且队列也已经被放满了任务时,`ThreadPoolExecutor` 定义一些策略: -- **`ThreadPoolExecutor.AbortPolicy`**:抛出 `RejectedExecutionException`来拒绝新任务的处理。 -- **`ThreadPoolExecutor.CallerRunsPolicy`**:调用执行自己的线程运行任务,也就是直接在调用`execute`方法的线程中运行(`run`)被拒绝的任务,如果执行程序已关闭,则会丢弃该任务。因此这种策略会降低对于新任务提交速度,影响程序的整体性能。如果您的应用程序可以承受此延迟并且你要求任何一个任务请求都要被执行的话,你可以选择这个策略。 -- **`ThreadPoolExecutor.DiscardPolicy`**:不处理新任务,直接丢弃掉。 -- **`ThreadPoolExecutor.DiscardOldestPolicy`**:此策略将丢弃最早的未处理的任务请求。 +- `ThreadPoolExecutor.AbortPolicy`:抛出 `RejectedExecutionException`来拒绝新任务的处理。 +- `ThreadPoolExecutor.CallerRunsPolicy`:调用执行自己的线程运行任务,也就是直接在调用`execute`方法的线程中运行(`run`)被拒绝的任务,如果执行程序已关闭,则会丢弃该任务。因此这种策略会降低对于新任务提交速度,影响程序的整体性能。如果您的应用程序可以承受此延迟并且你要求任何一个任务请求都要被执行的话,你可以选择这个策略。 +- `ThreadPoolExecutor.DiscardPolicy`:不处理新任务,直接丢弃掉。 +- `ThreadPoolExecutor.DiscardOldestPolicy`:此策略将丢弃最早的未处理的任务请求。 举个例子: -Spring 通过 `ThreadPoolTaskExecutor` 或者我们直接通过 `ThreadPoolExecutor` 的构造函数创建线程池的时候,当我们不指定 `RejectedExecutionHandler` 饱和策略的话来配置线程池的时候默认使用的是 `ThreadPoolExecutor.AbortPolicy`。在默认情况下,`ThreadPoolExecutor` 将抛出 `RejectedExecutionException` 来拒绝新来的任务 ,这代表你将丢失对这个任务的处理。 对于可伸缩的应用程序,建议使用 `ThreadPoolExecutor.CallerRunsPolicy`。当最大池被填满时,此策略为我们提供可伸缩队列(这个直接查看 `ThreadPoolExecutor` 的构造函数源码就可以看出,比较简单的原因,这里就不贴代码了)。 +举个例子:Spring 通过 `ThreadPoolTaskExecutor` 或者我们直接通过 `ThreadPoolExecutor` 的构造函数创建线程池的时候,当我们不指定 `RejectedExecutionHandler` 拒绝策略来配置线程池的时候,默认使用的是 `AbortPolicy`。在这种拒绝策略下,如果队列满了,`ThreadPoolExecutor` 将抛出 `RejectedExecutionException` 异常来拒绝新来的任务 ,这代表你将丢失对这个任务的处理。如果不想丢弃任务的话,可以使用`CallerRunsPolicy`。`CallerRunsPolicy` 和其他的几个策略不同,它既不会抛弃任务,也不会抛出异常,而是将任务回退给调用者,使用调用者的线程来执行任务 + +```java +public static class CallerRunsPolicy implements RejectedExecutionHandler { + + public CallerRunsPolicy() { } + + public void rejectedExecution(Runnable r, ThreadPoolExecutor e) { + if (!e.isShutdown()) { + // 直接主线程执行,而不是线程池中的线程执行 + r.run(); + } + } + } +``` -### 线程池创建两种方式 +### 线程池创建的两种方式 -**方式一:通过`ThreadPoolExecutor`构造函数来创建(推荐)。** +在 Java 中,创建线程池主要有两种方式: -![通过构造方法实现](./images/java-thread-pool-summary/threadpoolexecutor构造函数.png) +**方式一:通过 `ThreadPoolExecutor` 构造函数直接创建 (推荐)** -**方式二:通过 `Executor` 框架的工具类 `Executors` 来创建。** +![](https://oss.javaguide.cn/github/javaguide/java/concurrent/threadpoolexecutor-construtors.png) -我们可以创建多种类型的 `ThreadPoolExecutor`: +这是最推荐的方式,因为它允许开发者明确指定线程池的核心参数,对线程池的运行行为有更精细的控制,从而避免资源耗尽的风险。 -- **`FixedThreadPool`**:该方法返回一个固定线程数量的线程池。该线程池中的线程数量始终不变。当有一个新的任务提交时,线程池中若有空闲线程,则立即执行。若没有,则新的任务会被暂存在一个任务队列中,待有线程空闲时,便处理在任务队列中的任务。 -- **`SingleThreadExecutor`:** 该方法返回一个只有一个线程的线程池。若多余一个任务被提交到该线程池,任务会被保存在一个任务队列中,待线程空闲,按先入先出的顺序执行队列中的任务。 -- **`CachedThreadPool`:** 该方法返回一个可根据实际情况调整线程数量的线程池。线程池的线程数量不确定,但若有空闲线程可以复用,则会优先使用可复用的线程。若所有线程均在工作,又有新的任务提交,则会创建新的线程处理任务。所有线程在当前任务执行完毕后,将返回线程池进行复用。 -- **`ScheduledThreadPool`**:该返回一个用来在给定的延迟后运行任务或者定期执行任务的线程池。 +**方式二:通过 `Executors` 工具类创建 (不推荐用于生产环境)** -对应 `Executors` 工具类中的方法如图所示: +`Executors`工具类提供的创建线程池的方法如下图所示: -![](https://oss.javaguide.cn/github/javaguide/java/concurrent/executors-inner-threadpool.png) +![](https://oss.javaguide.cn/github/javaguide/java/concurrent/executors-new-thread-pool-methods.png) + +可以看出,通过`Executors`工具类可以创建多种类型的线程池,包括: + +- `FixedThreadPool`:固定线程数量的线程池。该线程池中的线程数量始终不变。当有一个新的任务提交时,线程池中若有空闲线程,则立即执行。若没有,则新的任务会被暂存在一个任务队列中,待有线程空闲时,便处理在任务队列中的任务。 +- `SingleThreadExecutor`: 只有一个线程的线程池。若多余一个任务被提交到该线程池,任务会被保存在一个任务队列中,待线程空闲,按先入先出的顺序执行队列中的任务。 +- `CachedThreadPool`: 可根据实际情况调整线程数量的线程池。线程池的线程数量不确定,但若有空闲线程可以复用,则会优先使用可复用的线程。若所有线程均在工作,又有新的任务提交,则会创建新的线程处理任务。所有线程在当前任务执行完毕后,将返回线程池进行复用。 +- `ScheduledThreadPool`:给定的延迟后运行任务或者定期执行任务的线程池。 《阿里巴巴 Java 开发手册》强制线程池不允许使用 `Executors` 去创建,而是通过 `ThreadPoolExecutor` 构造函数的方式,这样的处理方式让写的同学更加明确线程池的运行规则,规避资源耗尽的风险 `Executors` 返回线程池对象的弊端如下(后文会详细介绍到): -- **`FixedThreadPool` 和 `SingleThreadExecutor`**:使用的是无界的 `LinkedBlockingQueue`,任务队列最大长度为 `Integer.MAX_VALUE`,可能堆积大量的请求,从而导致 OOM。 -- **`CachedThreadPool`**:使用的是同步队列 `SynchronousQueue`, 允许创建的线程数量为 `Integer.MAX_VALUE` ,可能会创建大量线程,从而导致 OOM。 -- **`ScheduledThreadPool` 和 `SingleThreadScheduledExecutor`** : 使用的无界的延迟阻塞队列`DelayedWorkQueue`,任务队列最大长度为 `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())); } @@ -209,7 +227,7 @@ public ScheduledThreadPoolExecutor(int corePoolSize) { 不同的线程池会选用不同的阻塞队列,我们可以结合内置线程池来分析。 -- 容量为 `Integer.MAX_VALUE` 的 `LinkedBlockingQueue`(无界队列):`FixedThreadPool` 和 `SingleThreadExector` 。由于队列永远不会被放满,因此`FixedThreadPool`最多只能创建核心线程数的线程。 +- 容量为 `Integer.MAX_VALUE` 的 `LinkedBlockingQueue`(无界队列):`FixedThreadPool` 和 `SingleThreadExector` 。`FixedThreadPool`最多只能创建核心线程数的线程(核心线程数和最大线程数相等),`SingleThreadExector`只能创建一个线程(核心线程数和最大线程数都是 1),二者的任务队列永远不会被放满。 - `SynchronousQueue`(同步队列):`CachedThreadPool` 。`SynchronousQueue` 没有容量,不存储元素,目的是保证对于提交的任务,如果有空闲线程,则使用空闲线程来处理;否则新建一个线程来处理任务。也就是说,`CachedThreadPool` 的最大线程数是 `Integer.MAX_VALUE` ,可以理解为线程数是可以无限扩展的,可能会创建大量线程,从而导致 OOM。 - `DelayedWorkQueue`(延迟阻塞队列):`ScheduledThreadPool` 和 `SingleThreadScheduledExecutor` 。`DelayedWorkQueue` 的内部元素并不是按照放入的时间排序,而是会按照延迟的时间长短对任务进行排序,内部采用的是“堆”的数据结构,可以保证每次出队的任务都是当前队列中执行时间最靠前的。`DelayedWorkQueue` 添加元素满了之后会自动扩容原来容量的 1/2,即永远不会阻塞,最大扩容可达 `Integer.MAX_VALUE`,所以最多只能创建核心线程数的线程。 @@ -217,9 +235,9 @@ public ScheduledThreadPoolExecutor(int corePoolSize) { 我们上面讲解了 `Executor`框架以及 `ThreadPoolExecutor` 类,下面让我们实战一下,来通过写一个 `ThreadPoolExecutor` 的小 Demo 来回顾上面的内容。 -### ThreadPoolExecutor 示例代码 +### 线程池示例代码 -首先创建一个 `Runnable` 接口的实现类(当然也可以是 `Callable` 接口,我们上面也说了两者的区别。) +首先创建一个 `Runnable` 接口的实现类(当然也可以是 `Callable` 接口,我们后面会介绍两者的区别。) `MyRunnable.java` @@ -311,11 +329,11 @@ public class ThreadPoolExecutorDemo { - `keepAliveTime` : 等待时间为 1L。 - `unit`: 等待时间的单位为 TimeUnit.SECONDS。 - `workQueue`:任务队列为 `ArrayBlockingQueue`,并且容量为 100; -- `handler`:饱和策略为 `CallerRunsPolicy`。 +- `handler`:拒绝策略为 `CallerRunsPolicy`。 **输出结构**: -``` +```plain pool-1-thread-3 Start. Time = Sun Apr 12 11:14:37 CST 2020 pool-1-thread-5 Start. Time = Sun Apr 12 11:14:37 CST 2020 pool-1-thread-2 Start. Time = Sun Apr 12 11:14:37 CST 2020 @@ -336,6 +354,7 @@ pool-1-thread-4 End. Time = Sun Apr 12 11:14:47 CST 2020 pool-1-thread-5 End. Time = Sun Apr 12 11:14:47 CST 2020 pool-1-thread-3 End. Time = Sun Apr 12 11:14:47 CST 2020 pool-1-thread-2 End. Time = Sun Apr 12 11:14:47 CST 2020 +Finished all threads // 任务全部执行完了才会跳出来,因为executor.isTerminated()判断为true了才会跳出while循环,当且仅当调用 shutdown() 方法后,并且所有提交的任务完成后返回为 true ``` @@ -398,7 +417,7 @@ pool-1-thread-2 End. Time = Sun Apr 12 11:14:47 CST 2020 1. 如果当前运行的线程数小于核心线程数,那么就会新建一个线程来执行任务。 2. 如果当前运行的线程数等于或大于核心线程数,但是小于最大线程数,那么就把该任务放入到任务队列里等待执行。 3. 如果向任务队列投放任务失败(任务队列已经满了),但是当前运行的线程数是小于最大线程数的,就新建一个线程来执行任务。 -4. 如果当前运行的线程数已经等同于最大线程数了,新建线程将会使当前运行的线程超出最大线程数,那么当前任务会被拒绝,饱和策略会调用`RejectedExecutionHandler.rejectedExecution()`方法。 +4. 如果当前运行的线程数已经等同于最大线程数了,新建线程将会使当前运行的线程超出最大线程数,那么当前任务会被拒绝,拒绝策略会调用`RejectedExecutionHandler.rejectedExecution()`方法。 ![图解线程池实现原理](https://oss.javaguide.cn/github/javaguide/java/concurrent/thread-pool-principle.png) @@ -519,7 +538,7 @@ pool-1-thread-2 End. Time = Sun Apr 12 11:14:47 CST 2020 #### `Runnable` vs `Callable` -`Runnable`自 Java 1.0 以来一直存在,但`Callable`仅在 Java 1.5 中引入,目的就是为了来处理`Runnable`不支持的用例。**`Runnable` 接口**不会返回结果或抛出检查异常,但是 **`Callable` 接口**可以。所以,如果任务不需要返回结果或抛出异常推荐使用 **`Runnable` 接口**,这样代码看起来会更加简洁。 +`Runnable`自 Java 1.0 以来一直存在,但`Callable`仅在 Java 1.5 中引入,目的就是为了来处理`Runnable`不支持的用例。`Runnable` 接口不会返回结果或抛出检查异常,但是 `Callable` 接口可以。所以,如果任务不需要返回结果或抛出异常推荐使用 `Runnable` 接口,这样代码看起来会更加简洁。 工具类 `Executors` 可以实现将 `Runnable` 对象转换成 `Callable` 对象。(`Executors.callable(Runnable task)` 或 `Executors.callable(Runnable task, Object result)`)。 @@ -552,14 +571,15 @@ public interface Callable { #### `execute()` vs `submit()` -- `execute()`方法用于提交不需要返回值的任务,所以无法判断任务是否被线程池执行成功与否; -- `submit()`方法用于提交需要返回值的任务。线程池会返回一个 `Future` 类型的对象,通过这个 `Future` 对象可以判断任务是否执行成功,并且可以通过 `Future` 的 `get()`方法来获取返回值,`get()`方法会阻塞当前线程直到任务完成,而使用 `get(long timeout,TimeUnit unit)`方法的话,如果在 `timeout` 时间内任务还没有执行完,就会抛出 `java.util.concurrent.TimeoutException`。 +`execute()` 和 `submit()`是两种提交任务到线程池的方法,有一些区别: -这里只是为了演示使用,推荐使用 `ThreadPoolExecutor` 构造方法来创建线程池。 +- **返回值**:`execute()` 方法用于提交不需要返回值的任务。通常用于执行 `Runnable` 任务,无法判断任务是否被线程池成功执行。`submit()` 方法用于提交需要返回值的任务。可以提交 `Runnable` 或 `Callable` 任务。`submit()` 方法返回一个 `Future` 对象,通过这个 `Future` 对象可以判断任务是否执行成功,并获取任务的返回值(`get()`方法会阻塞当前线程直到任务完成, `get(long timeout,TimeUnit unit)`多了一个超时时间,如果在 `timeout` 时间内任务还没有执行完,就会抛出 `java.util.concurrent.TimeoutException`)。 +- **异常处理**:在使用 `submit()` 方法时,可以通过 `Future` 对象处理任务执行过程中抛出的异常;而在使用 `execute()` 方法时,异常处理需要通过自定义的 `ThreadFactory` (在线程工厂创建线程的时候设置`UncaughtExceptionHandler`对象来 处理异常)或 `ThreadPoolExecutor` 的 `afterExecute()` 方法来处理 示例 1:使用 `get()`方法获取返回值。 ```java +// 这里只是为了演示使用,推荐使用 `ThreadPoolExecutor` 构造方法来创建线程池。 ExecutorService executorService = Executors.newFixedThreadPool(3); Future submit = executorService.submit(() -> { @@ -578,7 +598,7 @@ executorService.shutdown(); 输出: -``` +```plain abc ``` @@ -603,15 +623,15 @@ executorService.shutdown(); 输出: -``` +```plain Exception in thread "main" java.util.concurrent.TimeoutException - at java.util.concurrent.FutureTask.get(FutureTask.java:205) + at java.util.concurrent.FutureTask.get(FutureTask.java:205) ``` #### `shutdown()`VS`shutdownNow()` - **`shutdown()`** :关闭线程池,线程池的状态变为 `SHUTDOWN`。线程池不再接受新任务了,但是队列里的任务得执行完毕。 -- **`shutdownNow()`** :关闭线程池,线程的状态变为 `STOP`。线程池会终止当前正在运行的任务,并停止处理排队的任务并返回正在等待执行的 List。 +- **`shutdownNow()`** :关闭线程池,线程池的状态变为 `STOP`。线程池会终止当前正在运行的任务,并停止处理排队的任务并返回正在等待执行的 List。 #### `isTerminated()` VS `isShutdown()` @@ -660,9 +680,9 @@ Exception in thread "main" java.util.concurrent.TimeoutException **上图说明:** -1. 如果当前运行的线程数小于 `corePoolSize`, 如果再来新任务的话,就创建新的线程来执行任务; -2. 当前运行的线程数等于 `corePoolSize` 后, 如果再来新任务的话,会将任务加入 `LinkedBlockingQueue`; -3. 线程池中的线程执行完 手头的任务后,会在循环中反复从 `LinkedBlockingQueue` 中获取任务来执行; +1. 如果当前运行的线程数小于 `corePoolSize`, 如果再来新任务的话,就创建新的线程来执行任务; +2. 当前运行的线程数等于 `corePoolSize` 后, 如果再来新任务的话,会将任务加入 `LinkedBlockingQueue`; +3. 线程池中的线程执行完 手头的任务后,会在循环中反复从 `LinkedBlockingQueue` 中获取任务来执行; #### 为什么不推荐使用`FixedThreadPool`? @@ -809,3 +829,5 @@ public class ScheduledThreadPoolExecutor - [Java Scheduler ScheduledExecutorService ScheduledThreadPoolExecutor Example](https://www.journaldev.com/2340/java-scheduler-scheduledexecutorservice-scheduledthreadpoolexecutor-example "Java Scheduler ScheduledExecutorService ScheduledThreadPoolExecutor Example") - [java.util.concurrent.ScheduledThreadPoolExecutor Example](https://examples.javacodegeeks.com/core-java/util/concurrent/scheduledthreadpoolexecutor/java-util-concurrent-scheduledthreadpoolexecutor-example/ "java.util.concurrent.ScheduledThreadPoolExecutor Example") - [ThreadPoolExecutor – Java Thread Pool Example](https://www.journaldev.com/1069/threadpoolexecutor-java-thread-pool-example-executorservice "ThreadPoolExecutor – Java Thread Pool Example") + + diff --git a/docs/java/concurrent/jmm.md b/docs/java/concurrent/jmm.md index 18954c19737..dbc36a351b9 100644 --- a/docs/java/concurrent/jmm.md +++ b/docs/java/concurrent/jmm.md @@ -32,7 +32,7 @@ JMM(Java 内存模型)主要定义了对于一个共享变量,当另一个线 现代的 CPU Cache 通常分为三层,分别叫 L1,L2,L3 Cache。有些 CPU 可能还有 L4 Cache,这里不做讨论,并不常见 -**CPU Cache 的工作方式:** 先复制一份数据到 CPU Cache 中,当 CPU 需要用到的时候就可以直接从 CPU Cache 中读取数据,当运算完成后,再将运算得到的数据写回 Main Memory 中。但是,这样存在 **内存缓存不一致性的问题** !比如我执行一个 i++ 操作的话,如果两个线程同时执行的话,假设两个线程从 CPU Cache 中读取的 i=1,两个线程做了 1++ 运算完之后再写回 Main Memory 之后 i=2,而正确结果应该是 i=3。 +**CPU Cache 的工作方式:** 先复制一份数据到 CPU Cache 中,当 CPU 需要用到的时候就可以直接从 CPU Cache 中读取数据,当运算完成后,再将运算得到的数据写回 Main Memory 中。但是,这样存在 **内存缓存不一致性的问题** !比如我执行一个 i++ 操作的话,如果两个线程同时执行的话,假设两个线程从 CPU Cache 中读取的 i=1,两个线程做了 i++ 运算完之后再写回 Main Memory 之后 i=2,而正确结果应该是 i=3。 **CPU 为了解决内存缓存不一致性问题可以通过制定缓存一致协议(比如 [MESI 协议](https://zh.wikipedia.org/wiki/MESI%E5%8D%8F%E8%AE%AE))或者其他手段来解决。** 这个缓存一致性协议指的是在 CPU 高速缓存与主内存交互的时候需要遵守的原则和规范。不同的 CPU 中,使用的缓存一致性协议通常也会有所不同。 @@ -61,9 +61,13 @@ Java 源代码会经历 **编译器优化重排 —> 指令并行重排 —> 内 **指令重排序可以保证串行语义一致,但是没有义务保证多线程间的语义也一致** ,所以在多线程下,指令重排序可能会导致一些问题。 -编译器和处理器的指令重排序的处理方式不一样。对于编译器,通过禁止特定类型的编译器重排序的方式来禁止重排序。对于处理器,通过插入内存屏障(Memory Barrier,或有时叫做内存栅栏,Memory Fence)的方式来禁止特定类型的处理器重排序。指令并行重排和内存系统重排都属于是处理器级别的指令重排序。 +对于编译器优化重排和处理器的指令重排序(指令并行重排和内存系统重排都属于是处理器级别的指令重排序),处理该问题的方式不一样。 -> 内存屏障(Memory Barrier,或有时叫做内存栅栏,Memory Fence)是一种 CPU 指令,用来禁止处理器指令发生重排序(像屏障一样),从而保障指令执行的有序性。另外,为了达到屏障的效果,它也会使处理器写入、读取值之前,将主内存的值写入高速缓存,清空无效队列,从而保障变量的可见性。 +- 对于编译器,通过禁止特定类型的编译器重排序的方式来禁止重排序。 + +- 对于处理器,通过插入内存屏障(Memory Barrier,或有时叫做内存栅栏,Memory Fence)的方式来禁止特定类型的处理器重排序。 + +> 内存屏障(Memory Barrier,或有时叫做内存栅栏,Memory Fence)是一种 CPU 指令,用来禁止处理器指令发生重排序(像屏障一样),从而保障指令执行的有序性。另外,为了达到屏障的效果,它会在处理器写入值时,强制将写缓冲区中的数据刷新到主内存;在读取值之前,使处理器本地缓存中的相关数据失效,强制从主内存中加载最新值,从而保障变量的可见性。 ## JMM(Java Memory Model) @@ -89,8 +93,8 @@ JMM 说白了就是定义了一些规范来解决这些问题,开发者可以 **什么是主内存?什么是本地内存?** -- **主内存**:所有线程创建的实例对象都存放在主内存中,不管该实例对象是成员变量还是方法中的本地变量(也称局部变量) -- **本地内存**:每个线程都有一个私有的本地内存来存储共享变量的副本,并且,每个线程只能访问自己的本地内存,无法访问其他线程的本地内存。本地内存是 JMM 抽象出来的一个概念,存储了主内存中的共享变量副本。 +- **主内存**:所有线程创建的实例对象都存放在主内存中,不管该实例对象是成员变量,还是局部变量,类信息、常量、静态变量都是放在主内存中。为了获取更好的运行速度,虚拟机及硬件系统可能会让工作内存优先存储于寄存器和高速缓存中。 +- **本地内存**:每个线程都有一个私有的本地内存,本地内存存储了该线程以读 / 写共享变量的副本。每个线程只能操作自己本地内存中的变量,无法直接访问其他线程的本地内存。如果线程间需要通信,必须通过主内存来进行。本地内存是 JMM 抽象出来的一个概念,并不真实存在,它涵盖了缓存、写缓冲区、寄存器以及其他的硬件和编译器优化。 Java 内存模型的抽象示意图如下: @@ -126,7 +130,7 @@ Java 内存模型的抽象示意图如下: - 一个变量在同一个时刻只允许一条线程对其进行 lock 操作,但 lock 操作可以被同一条线程重复执行多次,多次执行 lock 后,只有执行相同次数的 unlock 操作,变量才会被解锁。 - 如果对一个变量执行 lock 操作,将会清空工作内存中此变量的值,在执行引擎使用这个变量前,需要重新执行 load 或 assign 操作初始化变量的值。 - 如果一个变量事先没有被 lock 操作锁定,则不允许对它执行 unlock 操作,也不允许去 unlock 一个被其他线程锁定住的变量。 -- ...... +- …… ### Java 内存区域和 JMM 有何区别? @@ -160,9 +164,9 @@ JSR 133 引入了 happens-before 这个概念来描述两个操作之间的内 我们看下面这段代码: ```java -int userNum = getUserNum(); // 1 -int teacherNum = getTeacherNum(); // 2 -int totalNum = userNum + teacherNum; // 3 +int userNum = getUserNum(); // 1 +int teacherNum = getTeacherNum(); // 2 +int totalNum = userNum + teacherNum; // 3 ``` - 1 happens-before 2 @@ -232,7 +236,9 @@ happens-before 与 JMM 的关系用《Java 并发编程的艺术》这本书中 ## 参考 - 《Java 并发编程的艺术》第三章 Java 内存模型 -- 《深入浅出 Java 多线程》:http://concurrent.redspider.group/RedSpider.html -- Java 内存访问重排序的研究:https://tech.meituan.com/2014/09/23/java-memory-reordering.html -- 嘿,同学,你要的 Java 内存模型 (JMM) 来了:https://xie.infoq.cn/article/739920a92d0d27e2053174ef2 -- JSR 133 (Java Memory Model) FAQ:https://www.cs.umd.edu/~pugh/java/memoryModel/jsr-133-faq.html +- 《深入浅出 Java 多线程》: +- Java 内存访问重排序的研究: +- 嘿,同学,你要的 Java 内存模型 (JMM) 来了: +- JSR 133 (Java Memory Model) FAQ: + + diff --git a/docs/java/concurrent/optimistic-lock-and-pessimistic-lock.md b/docs/java/concurrent/optimistic-lock-and-pessimistic-lock.md index 77564054a10..ba370690a11 100644 --- a/docs/java/concurrent/optimistic-lock-and-pessimistic-lock.md +++ b/docs/java/concurrent/optimistic-lock-and-pessimistic-lock.md @@ -5,9 +5,7 @@ tag: - Java并发 --- -如果将悲观锁(Pessimistic Lock)和乐观锁(PessimisticLock 或 OptimisticLock)对应到现实生活中来。悲观锁有点像是一位比较悲观(也可以说是未雨绸缪)的人,总是会假设最坏的情况,避免出现问题。乐观锁有点像是一位比较乐观的人,总是会假设最好的情况,在要出现问题之前快速解决问题。 - -在程序世界中,乐观锁和悲观锁的最终目的都是为了保证线程安全,避免在并发场景下的资源竞争问题。但是,相比于乐观锁,悲观锁对性能的影响更大! +如果将悲观锁(Pessimistic Lock)和乐观锁(Optimistic Lock)对应到现实生活中来。悲观锁有点像是一位比较悲观(也可以说是未雨绸缪)的人,总是会假设最坏的情况,避免出现问题。乐观锁有点像是一位比较乐观的人,总是会假设最好的情况,在要出现问题之前快速解决问题。 ## 什么是悲观锁? @@ -31,34 +29,30 @@ try { } ``` -高并发的场景下,激烈的锁竞争会造成线程阻塞,大量阻塞线程会导致系统的上下文切换,增加系统的性能开销。并且,悲观锁还可能会存在死锁问题,影响代码的正常运行。 +高并发的场景下,激烈的锁竞争会造成线程阻塞,大量阻塞线程会导致系统的上下文切换,增加系统的性能开销。并且,悲观锁还可能会存在死锁问题(线程获得锁的顺序不当时),影响代码的正常运行。 ## 什么是乐观锁? 乐观锁总是假设最好的情况,认为共享资源每次被访问的时候不会出现问题,线程可以不停地执行,无需加锁也无需等待,只是在提交修改的时候去验证对应的资源(也就是数据)是否被其它线程修改了(具体方法可以使用版本号机制或 CAS 算法)。 -像 Java 中`java.util.concurrent.atomic`包下面的原子变量类(比如`AtomicInteger`、`LongAdder`)就是使用了乐观锁的一种实现方式 **CAS** 实现的。 - -![JUC原子类概览](https://my-blog-to-use.oss-cn-beijing.aliyuncs.com/2019-6/JUC原子类概览.png) +在 Java 中`java.util.concurrent.atomic`包下面的原子变量类(比如`AtomicInteger`、`LongAdder`)就是使用了乐观锁的一种实现方式 **CAS** 实现的。 +![JUC原子类概览](https://oss.javaguide.cn/github/javaguide/java/JUC%E5%8E%9F%E5%AD%90%E7%B1%BB%E6%A6%82%E8%A7%88-20230814005211968.png) ```java // LongAdder 在高并发场景下会比 AtomicInteger 和 AtomicLong 的性能更好 // 代价就是会消耗更多的内存空间(空间换时间) -LongAdder longAdder = new LongAdder(); -// 自增 -longAdder.increment(); -// 获取结果 -longAdder.sum(); +LongAdder sum = new LongAdder(); +sum.increment(); ``` -高并发的场景下,乐观锁相比悲观锁来说,不存在锁竞争造成线程阻塞,也不会有死锁的问题,在性能上往往会更胜一筹。但是,如果冲突频繁发生(写占比非常多的情况),会频繁失败和重试(悲观锁的开销是固定的),这样同样会非常影响性能,导致 CPU 飙升。 +高并发的场景下,乐观锁相比悲观锁来说,不存在锁竞争造成线程阻塞,也不会有死锁问题,在性能上往往会更胜一筹。但是,如果冲突频繁发生(写占比非常多的情况),会频繁失败并重试,这样同样会非常影响性能,导致 CPU 飙升。 不过,大量失败重试的问题也是可以解决的,像我们前面提到的 `LongAdder`以空间换时间的方式就解决了这个问题。 理论上来说: -- 悲观锁通常多用于写比较多的情况下(多写场景,竞争激烈),这样可以避免频繁失败和重试影响性能,悲观锁的开销是固定的。不过,如果乐观锁解决了频繁失败和重试这个问题的话(比如`LongAdder`),也是可以考虑使用乐观锁的,要视实际情况而定。 -- 乐观锁通常多于写比较少的情况下(多读场景,竞争较少),这样可以避免频繁加锁影响性能。不过,乐观锁主要针对的对象是单个共享变量(参考`java.util.concurrent.atomic`包下面的原子变量类)。 +- 悲观锁通常多用于写比较多的情况(多写场景,竞争激烈),这样可以避免频繁失败和重试影响性能,悲观锁的开销是固定的。不过,如果乐观锁解决了频繁失败和重试这个问题的话(比如`LongAdder`),也是可以考虑使用乐观锁的,要视实际情况而定。 +- 乐观锁通常多用于写比较少的情况(多读场景,竞争较少),这样可以避免频繁加锁影响性能。不过,乐观锁主要针对的对象是单个共享变量(参考`java.util.concurrent.atomic`包下面的原子变量类)。 ## 如何实现乐观锁? @@ -100,75 +94,21 @@ CAS 涉及到三个操作数: 当多个线程同时使用 CAS 操作一个变量时,只有一个会胜出,并成功更新,其余均会失败,但失败的线程并不会被挂起,仅是被告知失败,并且允许再次尝试,当然也允许失败的线程放弃操作。 -Java 语言并没有直接实现 CAS,CAS 相关的实现是通过 C++ 内联汇编的形式实现的(JNI 调用)。因此, CAS 的具体实现和操作系统以及 CPU 都有关系。 - -`sun.misc`包下的`Unsafe`类提供了`compareAndSwapObject`、`compareAndSwapInt`、`compareAndSwapLong`方法来实现的对`Object`、`int`、`long`类型的 CAS 操作 - -```java -/** - * CAS - * @param o 包含要修改field的对象 - * @param offset 对象中某field的偏移量 - * @param expected 期望值 - * @param update 更新值 - * @return true | false - */ -public final native boolean compareAndSwapObject(Object o, long offset, Object expected, Object update); - -public final native boolean compareAndSwapInt(Object o, long offset, int expected,int update); - -public final native boolean compareAndSwapLong(Object o, long offset, long expected, long update); -``` - -关于 `Unsafe` 类的详细介绍可以看这篇文章:[Java 魔法类 Unsafe 详解 - JavaGuide - 2022](https://javaguide.cn/java/basis/unsafe.html) 。 - -## 乐观锁存在哪些问题? - -ABA 问题是乐观锁最常见的问题。 - -### ABA 问题 - -如果一个变量 V 初次读取的时候是 A 值,并且在准备赋值的时候检查到它仍然是 A 值,那我们就能说明它的值没有被其他线程修改过了吗?很明显是不能的,因为在这段时间它的值可能被改为其他值,然后又改回 A,那 CAS 操作就会误认为它从来没有被修改过。这个问题被称为 CAS 操作的 **"ABA"问题。** - -ABA 问题的解决思路是在变量前面追加上**版本号或者时间戳**。JDK 1.5 以后的 `AtomicStampedReference` 类就是用来解决 ABA 问题的,其中的 `compareAndSet()` 方法就是首先检查当前引用是否等于预期引用,并且当前标志是否等于预期标志,如果全部相等,则以原子方式将该引用和该标志的值设置为给定的更新值。 +关于 CAS 的进一步介绍,可以阅读读者写的这篇文章:[CAS 详解](./cas.md),其中详细提到了 Java 中 CAS 的实现以及 CAS 存在的一些问题。 -```java -public boolean compareAndSet(V expectedReference, - V newReference, - int expectedStamp, - int newStamp) { - Pair current = pair; - return - expectedReference == current.reference && - expectedStamp == current.stamp && - ((newReference == current.reference && - newStamp == current.stamp) || - casPair(current, Pair.of(newReference, newStamp))); -} -``` - -### 循环时间长开销大 - -CAS 经常会用到自旋操作来进行重试,也就是不成功就一直循环执行直到成功。如果长时间不成功,会给 CPU 带来非常大的执行开销。 - -如果 JVM 能支持处理器提供的 pause 指令那么效率会有一定的提升,pause 指令有两个作用: - -1. 可以延迟流水线执行指令,使 CPU 不会消耗过多的执行资源,延迟的时间取决于具体实现的版本,在一些处理器上延迟时间是零。 -2. 可以避免在退出循环的时候因内存顺序冲而引起 CPU 流水线被清空,从而提高 CPU 的执行效率。 +## 总结 -### 只能保证一个共享变量的原子操作 +本文详细介绍了乐观锁和悲观锁的概念以及乐观锁常见实现方式: -CAS 只对单个共享变量有效,当操作涉及跨多个共享变量时 CAS 无效。但是从 JDK 1.5 开始,提供了`AtomicReference`类来保证引用对象之间的原子性,你可以把多个变量放在一个对象里来进行 CAS 操作.所以我们可以使用锁或者利用`AtomicReference`类把多个共享变量合并成一个共享变量来操作。 +- 悲观锁基于悲观的假设,认为共享资源在每次访问时都会发生冲突,因此在每次操作时都会加锁。这种锁机制会导致其他线程阻塞,直到锁被释放。Java 中的 `synchronized` 和 `ReentrantLock` 是悲观锁的典型实现方式。虽然悲观锁能有效避免数据竞争,但在高并发场景下会导致线程阻塞、上下文切换频繁,从而影响系统性能,并且还可能引发死锁问题。 +- 乐观锁基于乐观的假设,认为共享资源在每次访问时不会发生冲突,因此无须加锁,只需在提交修改时验证数据是否被其他线程修改。Java 中的 `AtomicInteger` 和 `LongAdder` 等类通过 CAS(Compare-And-Swap)算法实现了乐观锁。乐观锁避免了线程阻塞和死锁问题,在读多写少的场景中性能优越。但在写操作频繁的情况下,可能会导致大量重试和失败,从而影响性能。 +- 乐观锁主要通过版本号机制或 CAS 算法实现。版本号机制通过比较版本号确保数据一致性,而 CAS 通过硬件指令实现原子操作,直接比较和交换变量值。 -## 总结 - -- 高并发的场景下,激烈的锁竞争会造成线程阻塞,大量阻塞线程会导致系统的上下文切换,增加系统的性能开销。并且,悲观锁还可能会存在死锁问题,影响代码的正常运行。乐观锁相比悲观锁来说,不存在锁竞争造成线程阻塞,也不会有死锁的问题,在性能上往往会更胜一筹。不过,如果冲突频繁发生(写占比非常多的情况),会频繁失败和重试,这样同样会非常影响性能,导致 CPU 飙升。 -- 乐观锁一般会使用版本号机制或 CAS 算法实现,CAS 算法相对来说更多一些,这里需要格外注意。 -- CAS 的全称是 **Compare And Swap(比较与交换)** ,用于实现乐观锁,被广泛应用于各大框架中。CAS 的思想很简单,就是用一个预期值和要更新的变量值进行比较,两值相等才会进行更新。 -- 乐观锁的问题:ABA 问题、循环时间长开销大、只能保证一个共享变量的原子操作。 +悲观锁和乐观锁各有优缺点,适用于不同的应用场景。在实际开发中,选择合适的锁机制能够有效提升系统的并发性能和稳定性。 ## 参考 - 《Java 并发编程核心 78 讲》 -- 通俗易懂 悲观锁、乐观锁、可重入锁、自旋锁、偏向锁、轻量/重量级锁、读写锁、各种锁及其 Java 实现!:https://zhuanlan.zhihu.com/p/71156910 -- 一文彻底搞懂 CAS 实现原理 & 深入到 CPU 指令:https://zhuanlan.zhihu.com/p/94976168 +- 通俗易懂 悲观锁、乐观锁、可重入锁、自旋锁、偏向锁、轻量/重量级锁、读写锁、各种锁及其 Java 实现!: + + diff --git a/docs/java/concurrent/reentrantlock.md b/docs/java/concurrent/reentrantlock.md index 517ed44db14..ef1cd38625c 100644 --- a/docs/java/concurrent/reentrantlock.md +++ b/docs/java/concurrent/reentrantlock.md @@ -5,7 +5,7 @@ tag: - Java并发 --- -> 本文转载自:https://tech.meituan.com/2019/12/05/aqs-theory-and-apply.html +> 本文转载自: > > 作者:美团技术团队 @@ -33,25 +33,25 @@ synchronized (object) {} public synchronized void test () {} // 4.可重入 for (int i = 0; i < 100; i++) { - synchronized (this) {} + synchronized (this) {} } // **************************ReentrantLock的使用方式************************** public void test () throw Exception { - // 1.初始化选择公平锁、非公平锁 - ReentrantLock lock = new ReentrantLock(true); - // 2.可用于代码块 - lock.lock(); - try { - try { - // 3.支持多种加锁方式,比较灵活; 具有可重入特性 - if(lock.tryLock(100, TimeUnit.MILLISECONDS)){ } - } finally { - // 4.手动释放锁 - lock.unlock() - } - } finally { - lock.unlock(); - } + // 1.初始化选择公平锁、非公平锁 + ReentrantLock lock = new ReentrantLock(true); + // 2.可用于代码块 + lock.lock(); + try { + try { + // 3.支持多种加锁方式,比较灵活; 具有可重入特性 + if(lock.tryLock(100, TimeUnit.MILLISECONDS)){ } + } finally { + // 4.手动释放锁 + lock.unlock() + } + } finally { + lock.unlock(); + } } ``` @@ -66,13 +66,13 @@ public void test () throw Exception { // 非公平锁 static final class NonfairSync extends Sync { - ... - final void lock() { - if (compareAndSetState(0, 1)) - setExclusiveOwnerThread(Thread.currentThread()); - else - acquire(1); - } + ... + final void lock() { + if (compareAndSetState(0, 1)) + setExclusiveOwnerThread(Thread.currentThread()); + else + acquire(1); + } ... } ``` @@ -101,9 +101,9 @@ static final class NonfairSync extends Sync { static final class FairSync extends Sync { ... - final void lock() { - acquire(1); - } + final void lock() { + acquire(1); + } ... } ``` @@ -275,13 +275,13 @@ ReentrantLock 中公平锁和非公平锁在底层是相同的,这里以非公 // java.util.concurrent.locks.ReentrantLock static final class NonfairSync extends Sync { - ... - final void lock() { - if (compareAndSetState(0, 1)) - setExclusiveOwnerThread(Thread.currentThread()); - else - acquire(1); - } + ... + final void lock() { + if (compareAndSetState(0, 1)) + setExclusiveOwnerThread(Thread.currentThread()); + else + acquire(1); + } ... } ``` @@ -292,8 +292,8 @@ static final class NonfairSync extends Sync { // java.util.concurrent.locks.AbstractQueuedSynchronizer public final void acquire(int arg) { - if (!tryAcquire(arg) && acquireQueued(addWaiter(Node.EXCLUSIVE), arg)) - selfInterrupt(); + if (!tryAcquire(arg) && acquireQueued(addWaiter(Node.EXCLUSIVE), arg)) + selfInterrupt(); } ``` @@ -303,7 +303,7 @@ public final void acquire(int arg) { // java.util.concurrent.locks.AbstractQueuedSynchronizer protected boolean tryAcquire(int arg) { - throw new UnsupportedOperationException(); + throw new UnsupportedOperationException(); } ``` @@ -323,21 +323,21 @@ protected boolean tryAcquire(int arg) { // java.util.concurrent.locks.AbstractQueuedSynchronizer private Node addWaiter(Node mode) { - Node node = new Node(Thread.currentThread(), mode); - // Try the fast path of enq; backup to full enq on failure - Node pred = tail; - if (pred != null) { - node.prev = pred; - if (compareAndSetTail(pred, node)) { - pred.next = node; - return node; - } - } - enq(node); - return node; + Node node = new Node(Thread.currentThread(), mode); + // Try the fast path of enq; backup to full enq on failure + Node pred = tail; + if (pred != null) { + node.prev = pred; + if (compareAndSetTail(pred, node)) { + pred.next = node; + return node; + } + } + enq(node); + return node; } private final boolean compareAndSetTail(Node expect, Node update) { - return unsafe.compareAndSwapObject(this, tailOffset, expect, update); + return unsafe.compareAndSwapObject(this, tailOffset, expect, update); } ``` @@ -352,13 +352,13 @@ private final boolean compareAndSetTail(Node expect, Node update) { // java.util.concurrent.locks.AbstractQueuedSynchronizer static { - try { - stateOffset = unsafe.objectFieldOffset(AbstractQueuedSynchronizer.class.getDeclaredField("state")); - headOffset = unsafe.objectFieldOffset(AbstractQueuedSynchronizer.class.getDeclaredField("head")); - tailOffset = unsafe.objectFieldOffset(AbstractQueuedSynchronizer.class.getDeclaredField("tail")); - waitStatusOffset = unsafe.objectFieldOffset(Node.class.getDeclaredField("waitStatus")); - nextOffset = unsafe.objectFieldOffset(Node.class.getDeclaredField("next")); - } catch (Exception ex) { + try { + stateOffset = unsafe.objectFieldOffset(AbstractQueuedSynchronizer.class.getDeclaredField("state")); + headOffset = unsafe.objectFieldOffset(AbstractQueuedSynchronizer.class.getDeclaredField("head")); + tailOffset = unsafe.objectFieldOffset(AbstractQueuedSynchronizer.class.getDeclaredField("tail")); + waitStatusOffset = unsafe.objectFieldOffset(Node.class.getDeclaredField("waitStatus")); + nextOffset = unsafe.objectFieldOffset(Node.class.getDeclaredField("next")); + } catch (Exception ex) { throw new Error(ex); } } @@ -372,19 +372,19 @@ static { // java.util.concurrent.locks.AbstractQueuedSynchronizer private Node enq(final Node node) { - for (;;) { - Node t = tail; - if (t == null) { // Must initialize - if (compareAndSetHead(new Node())) - tail = head; - } else { - node.prev = t; - if (compareAndSetTail(t, node)) { - t.next = node; - return t; - } - } - } + for (;;) { + Node t = tail; + if (t == null) { // Must initialize + if (compareAndSetHead(new Node())) + tail = head; + } else { + node.prev = t; + if (compareAndSetTail(t, node)) { + t.next = node; + return t; + } + } + } } ``` @@ -406,13 +406,13 @@ private Node enq(final Node node) { // java.util.concurrent.locks.ReentrantLock public final boolean hasQueuedPredecessors() { - // The correctness of this depends on head being initialized - // before tail and on head.next being accurate if the current - // thread is first in queue. - Node t = tail; // Read fields in reverse initialization order - Node h = head; - Node s; - return h != t && ((s = h.next) == null || s.thread != Thread.currentThread()); + // The correctness of this depends on head being initialized + // before tail and on head.next being accurate if the current + // thread is first in queue. + Node t = tail; // Read fields in reverse initialization order + Node h = head; + Node s; + return h != t && ((s = h.next) == null || s.thread != Thread.currentThread()); } ``` @@ -424,14 +424,14 @@ public final boolean hasQueuedPredecessors() { // java.util.concurrent.locks.AbstractQueuedSynchronizer#enq if (t == null) { // Must initialize - if (compareAndSetHead(new Node())) - tail = head; + if (compareAndSetHead(new Node())) + tail = head; } else { - node.prev = t; - if (compareAndSetTail(t, node)) { - t.next = node; - return t; - } + node.prev = t; + if (compareAndSetTail(t, node)) { + t.next = node; + return t; + } } ``` @@ -445,8 +445,8 @@ if (t == null) { // Must initialize // java.util.concurrent.locks.AbstractQueuedSynchronizer public final void acquire(int arg) { - if (!tryAcquire(arg) && acquireQueued(addWaiter(Node.EXCLUSIVE), arg)) - selfInterrupt(); + if (!tryAcquire(arg) && acquireQueued(addWaiter(Node.EXCLUSIVE), arg)) + selfInterrupt(); } ``` @@ -460,31 +460,31 @@ public final void acquire(int arg) { // java.util.concurrent.locks.AbstractQueuedSynchronizer final boolean acquireQueued(final Node node, int arg) { - // 标记是否成功拿到资源 - boolean failed = true; - try { - // 标记等待过程中是否中断过 - boolean interrupted = false; - // 开始自旋,要么获取锁,要么中断 - for (;;) { - // 获取当前节点的前驱节点 - final Node p = node.predecessor(); - // 如果p是头结点,说明当前节点在真实数据队列的首部,就尝试获取锁(别忘了头结点是虚节点) - if (p == head && tryAcquire(arg)) { - // 获取锁成功,头指针移动到当前node - setHead(node); - p.next = null; // help GC - failed = false; - return interrupted; - } - // 说明p为头节点且当前没有获取到锁(可能是非公平锁被抢占了)或者是p不为头结点,这个时候就要判断当前node是否要被阻塞(被阻塞条件:前驱节点的waitStatus为-1),防止无限循环浪费资源。具体两个方法下面细细分析 - if (shouldParkAfterFailedAcquire(p, node) && parkAndCheckInterrupt()) - interrupted = true; - } - } finally { - if (failed) - cancelAcquire(node); - } + // 标记是否成功拿到资源 + boolean failed = true; + try { + // 标记等待过程中是否中断过 + boolean interrupted = false; + // 开始自旋,要么获取锁,要么中断 + for (;;) { + // 获取当前节点的前驱节点 + final Node p = node.predecessor(); + // 如果p是头结点,说明当前节点在真实数据队列的首部,就尝试获取锁(别忘了头结点是虚节点) + if (p == head && tryAcquire(arg)) { + // 获取锁成功,头指针移动到当前node + setHead(node); + p.next = null; // help GC + failed = false; + return interrupted; + } + // 说明p为头节点且当前没有获取到锁(可能是非公平锁被抢占了)或者是p不为头结点,这个时候就要判断当前node是否要被阻塞(被阻塞条件:前驱节点的waitStatus为-1),防止无限循环浪费资源。具体两个方法下面细细分析 + if (shouldParkAfterFailedAcquire(p, node) && parkAndCheckInterrupt()) + interrupted = true; + } + } finally { + if (failed) + cancelAcquire(node); + } } ``` @@ -494,32 +494,32 @@ final boolean acquireQueued(final Node node, int arg) { // java.util.concurrent.locks.AbstractQueuedSynchronizer private void setHead(Node node) { - head = node; - node.thread = null; - node.prev = null; + head = node; + node.thread = null; + node.prev = null; } // java.util.concurrent.locks.AbstractQueuedSynchronizer // 靠前驱节点判断当前线程是否应该被阻塞 private static boolean shouldParkAfterFailedAcquire(Node pred, Node node) { - // 获取头结点的节点状态 - int ws = pred.waitStatus; - // 说明头结点处于唤醒状态 - if (ws == Node.SIGNAL) - return true; - // 通过枚举值我们知道waitStatus>0是取消状态 - if (ws > 0) { - do { - // 循环向前查找取消节点,把取消节点从队列中剔除 - node.prev = pred = pred.prev; - } while (pred.waitStatus > 0); - pred.next = node; - } else { - // 设置前任节点等待状态为SIGNAL - compareAndSetWaitStatus(pred, ws, Node.SIGNAL); - } - return false; + // 获取头结点的节点状态 + int ws = pred.waitStatus; + // 说明头结点处于唤醒状态 + if (ws == Node.SIGNAL) + return true; + // 通过枚举值我们知道waitStatus>0是取消状态 + if (ws > 0) { + do { + // 循环向前查找取消节点,把取消节点从队列中剔除 + node.prev = pred = pred.prev; + } while (pred.waitStatus > 0); + pred.next = node; + } else { + // 设置前任节点等待状态为SIGNAL + compareAndSetWaitStatus(pred, ws, Node.SIGNAL); + } + return false; } ``` @@ -555,21 +555,21 @@ acquireQueued 方法中的 Finally 代码: // java.util.concurrent.locks.AbstractQueuedSynchronizer final boolean acquireQueued(final Node node, int arg) { - boolean failed = true; - try { + boolean failed = true; + try { ... - for (;;) { - final Node p = node.predecessor(); - if (p == head && tryAcquire(arg)) { - ... - failed = false; + for (;;) { + final Node p = node.predecessor(); + if (p == head && tryAcquire(arg)) { ... - } - ... - } finally { - if (failed) - cancelAcquire(node); - } + failed = false; + ... + } + ... + } finally { + if (failed) + cancelAcquire(node); + } } ``` @@ -580,37 +580,37 @@ final boolean acquireQueued(final Node node, int arg) { private void cancelAcquire(Node node) { // 将无效节点过滤 - if (node == null) - return; + if (node == null) + return; // 设置该节点不关联任何线程,也就是虚节点 - node.thread = null; - Node pred = node.prev; + node.thread = null; + Node pred = node.prev; // 通过前驱节点,跳过取消状态的node - while (pred.waitStatus > 0) - node.prev = pred = pred.prev; + while (pred.waitStatus > 0) + node.prev = pred = pred.prev; // 获取过滤后的前驱节点的后继节点 - Node predNext = pred.next; + Node predNext = pred.next; // 把当前node的状态设置为CANCELLED - node.waitStatus = Node.CANCELLED; + node.waitStatus = Node.CANCELLED; // 如果当前节点是尾节点,将从后往前的第一个非取消状态的节点设置为尾节点 // 更新失败的话,则进入else,如果更新成功,将tail的后继节点设置为null - if (node == tail && compareAndSetTail(node, pred)) { - compareAndSetNext(pred, predNext, null); - } else { - int ws; - // 如果当前节点不是head的后继节点,1:判断当前节点前驱节点的是否为SIGNAL,2:如果不是,则把前驱节点设置为SINGAL看是否成功 + if (node == tail && compareAndSetTail(node, pred)) { + compareAndSetNext(pred, predNext, null); + } else { + int ws; + // 如果当前节点不是head的后继节点,1:判断当前节点前驱节点的是否为SIGNAL,2:如果不是,则把前驱节点设置为SIGNAL看是否成功 // 如果1和2中有一个为true,再判断当前节点的线程是否为null // 如果上述条件都满足,把当前节点的前驱节点的后继指针指向当前节点的后继节点 - if (pred != head && ((ws = pred.waitStatus) == Node.SIGNAL || (ws <= 0 && compareAndSetWaitStatus(pred, ws, Node.SIGNAL))) && pred.thread != null) { - Node next = node.next; - if (next != null && next.waitStatus <= 0) - compareAndSetNext(pred, predNext, next); - } else { + if (pred != head && ((ws = pred.waitStatus) == Node.SIGNAL || (ws <= 0 && compareAndSetWaitStatus(pred, ws, Node.SIGNAL))) && pred.thread != null) { + Node next = node.next; + if (next != null && next.waitStatus <= 0) + compareAndSetNext(pred, predNext, next); + } else { // 如果当前节点是head的后继节点,或者上述条件不满足,那就唤醒当前节点的后继节点 - unparkSuccessor(node); - } - node.next = node; // help GC - } + unparkSuccessor(node); + } + node.next = node; // help GC + } } ``` @@ -645,7 +645,7 @@ private void cancelAcquire(Node node) { > > ```java > do { -> node.prev = pred = pred.prev; +> node.prev = pred = pred.prev; > } while (pred.waitStatus > 0); > ``` @@ -657,7 +657,7 @@ private void cancelAcquire(Node node) { // java.util.concurrent.locks.ReentrantLock public void unlock() { - sync.release(1); + sync.release(1); } ``` @@ -667,13 +667,13 @@ public void unlock() { // java.util.concurrent.locks.AbstractQueuedSynchronizer public final boolean release(int arg) { - if (tryRelease(arg)) { - Node h = head; - if (h != null && h.waitStatus != 0) - unparkSuccessor(h); - return true; - } - return false; + if (tryRelease(arg)) { + Node h = head; + if (h != null && h.waitStatus != 0) + unparkSuccessor(h); + return true; + } + return false; } ``` @@ -684,19 +684,19 @@ public final boolean release(int arg) { // 方法返回当前锁是不是没有被线程持有 protected final boolean tryRelease(int releases) { - // 减少可重入次数 - int c = getState() - releases; - // 当前线程不是持有锁的线程,抛出异常 - if (Thread.currentThread() != getExclusiveOwnerThread()) - throw new IllegalMonitorStateException(); - boolean free = false; - // 如果持有线程全部释放,将当前独占锁所有线程设置为null,并更新state - if (c == 0) { - free = true; - setExclusiveOwnerThread(null); - } - setState(c); - return free; + // 减少可重入次数 + int c = getState() - releases; + // 当前线程不是持有锁的线程,抛出异常 + if (Thread.currentThread() != getExclusiveOwnerThread()) + throw new IllegalMonitorStateException(); + boolean free = false; + // 如果持有线程全部释放,将当前独占锁所有线程设置为null,并更新state + if (c == 0) { + free = true; + setExclusiveOwnerThread(null); + } + setState(c); + return free; } ``` @@ -706,16 +706,16 @@ protected final boolean tryRelease(int releases) { // java.util.concurrent.locks.AbstractQueuedSynchronizer public final boolean release(int arg) { - // 上边自定义的tryRelease如果返回true,说明该锁没有被任何线程持有 - if (tryRelease(arg)) { - // 获取头结点 - Node h = head; - // 头结点不为空并且头结点的waitStatus不是初始化节点情况,解除线程挂起状态 - if (h != null && h.waitStatus != 0) - unparkSuccessor(h); - return true; - } - return false; + // 上边自定义的tryRelease如果返回true,说明该锁没有被任何线程持有 + if (tryRelease(arg)) { + // 获取头结点 + Node h = head; + // 头结点不为空并且头结点的waitStatus不是初始化节点情况,解除线程挂起状态 + if (h != null && h.waitStatus != 0) + unparkSuccessor(h); + return true; + } + return false; } ``` @@ -733,23 +733,23 @@ public final boolean release(int arg) { // java.util.concurrent.locks.AbstractQueuedSynchronizer private void unparkSuccessor(Node node) { - // 获取头结点waitStatus - int ws = node.waitStatus; - if (ws < 0) - compareAndSetWaitStatus(node, ws, 0); - // 获取当前节点的下一个节点 - Node s = node.next; - // 如果下个节点是null或者下个节点被cancelled,就找到队列最开始的非cancelled的节点 - if (s == null || s.waitStatus > 0) { - s = null; - // 就从尾部节点开始找,到队首,找到队列第一个waitStatus<0的节点。 - for (Node t = tail; t != null && t != node; t = t.prev) - if (t.waitStatus <= 0) - s = t; - } - // 如果当前节点的下个节点不为空,而且状态<=0,就把当前节点unpark - if (s != null) - LockSupport.unpark(s.thread); + // 获取头结点waitStatus + int ws = node.waitStatus; + if (ws < 0) + compareAndSetWaitStatus(node, ws, 0); + // 获取当前节点的下一个节点 + Node s = node.next; + // 如果下个节点是null或者下个节点被cancelled,就找到队列最开始的非cancelled的节点 + if (s == null || s.waitStatus > 0) { + s = null; + // 就从尾部节点开始找,到队首,找到队列第一个waitStatus<0的节点。 + for (Node t = tail; t != null && t != node; t = t.prev) + if (t.waitStatus <= 0) + s = t; + } + // 如果当前节点的下个节点不为空,而且状态<=0,就把当前节点unpark + if (s != null) + LockSupport.unpark(s.thread); } ``` @@ -761,18 +761,18 @@ private void unparkSuccessor(Node node) { // java.util.concurrent.locks.AbstractQueuedSynchronizer private Node addWaiter(Node mode) { - Node node = new Node(Thread.currentThread(), mode); - // Try the fast path of enq; backup to full enq on failure - Node pred = tail; - if (pred != null) { - node.prev = pred; - if (compareAndSetTail(pred, node)) { - pred.next = node; - return node; - } - } - enq(node); - return node; + Node node = new Node(Thread.currentThread(), mode); + // Try the fast path of enq; backup to full enq on failure + Node pred = tail; + if (pred != null) { + node.prev = pred; + if (compareAndSetTail(pred, node)) { + pred.next = node; + return node; + } + } + enq(node); + return node; } ``` @@ -788,8 +788,8 @@ private Node addWaiter(Node mode) { // java.util.concurrent.locks.AbstractQueuedSynchronizer private final boolean parkAndCheckInterrupt() { - LockSupport.park(this); - return Thread.interrupted(); + LockSupport.park(this); + return Thread.interrupted(); } ``` @@ -799,24 +799,24 @@ private final boolean parkAndCheckInterrupt() { // java.util.concurrent.locks.AbstractQueuedSynchronizer final boolean acquireQueued(final Node node, int arg) { - boolean failed = true; - try { - boolean interrupted = false; - for (;;) { - final Node p = node.predecessor(); - if (p == head && tryAcquire(arg)) { - setHead(node); - p.next = null; // help GC - failed = false; - return interrupted; - } - if (shouldParkAfterFailedAcquire(p, node) && parkAndCheckInterrupt()) - interrupted = true; - } - } finally { - if (failed) - cancelAcquire(node); - } + boolean failed = true; + try { + boolean interrupted = false; + for (;;) { + final Node p = node.predecessor(); + if (p == head && tryAcquire(arg)) { + setHead(node); + p.next = null; // help GC + failed = false; + return interrupted; + } + if (shouldParkAfterFailedAcquire(p, node) && parkAndCheckInterrupt()) + interrupted = true; + } + } finally { + if (failed) + cancelAcquire(node); + } } ``` @@ -826,7 +826,7 @@ final boolean acquireQueued(final Node node, int arg) { // java.util.concurrent.locks.AbstractQueuedSynchronizer static void selfInterrupt() { - Thread.currentThread().interrupt(); + Thread.currentThread().interrupt(); } ``` @@ -873,17 +873,17 @@ ReentrantLock 的可重入性是 AQS 很好的应用之一,在了解完上述 // java.util.concurrent.locks.ReentrantLock.FairSync#tryAcquire if (c == 0) { - if (!hasQueuedPredecessors() && compareAndSetState(0, acquires)) { - setExclusiveOwnerThread(current); - return true; - } + if (!hasQueuedPredecessors() && compareAndSetState(0, acquires)) { + setExclusiveOwnerThread(current); + return true; + } } else if (current == getExclusiveOwnerThread()) { - int nextc = c + acquires; - if (nextc < 0) - throw new Error("Maximum lock count exceeded"); - setState(nextc); - return true; + int nextc = c + acquires; + if (nextc < 0) + throw new Error("Maximum lock count exceeded"); + setState(nextc); + return true; } ``` @@ -893,17 +893,17 @@ else if (current == getExclusiveOwnerThread()) { // java.util.concurrent.locks.ReentrantLock.Sync#nonfairTryAcquire if (c == 0) { - if (compareAndSetState(0, acquires)){ - setExclusiveOwnerThread(current); - return true; - } + if (compareAndSetState(0, acquires)){ + setExclusiveOwnerThread(current); + return true; + } } else if (current == getExclusiveOwnerThread()) { - int nextc = c + acquires; - if (nextc < 0) // overflow - throw new Error("Maximum lock count exceeded"); - setState(nextc); - return true; + int nextc = c + acquires; + if (nextc < 0) // overflow + throw new Error("Maximum lock count exceeded"); + setState(nextc); + return true; } ``` @@ -1015,6 +1015,8 @@ public class LeeMain { ## 参考资料 -- Lea D. The java. util. concurrent synchronizer framework[J]. Science of Computer Programming, 2005, 58(3): 293-309. +- Lea D. The java. util. concurrent synchronizer framework\[J]. Science of Computer Programming, 2005, 58(3): 293-309. - 《Java 并发编程实战》 - [不可不说的 Java“锁”事](https://tech.meituan.com/2018/11/15/java-lock.html) + + diff --git a/docs/java/concurrent/threadlocal.md b/docs/java/concurrent/threadlocal.md index 8bf21a4faab..0cdaf0adfd6 100644 --- a/docs/java/concurrent/threadlocal.md +++ b/docs/java/concurrent/threadlocal.md @@ -24,7 +24,7 @@ tag: - `ThreadLocalMap.set()`方法实现原理? - `ThreadLocalMap.get()`方法实现原理? - 项目中`ThreadLocal`使用情况?遇到的坑? -- ...... +- …… 上述的一些问题你是否都已经掌握的很清楚了呢?本文将围绕这些问题使用图文方式来剖析`ThreadLocal`的**点点滴滴**。 @@ -96,7 +96,7 @@ size: 0 - **弱引用**:使用 WeakReference 修饰的对象被称为弱引用,只要发生垃圾回收,若这个对象只被弱引用指向,那么就会被回收 - **虚引用**:虚引用是最弱的引用,在 Java 中使用 PhantomReference 进行定义。虚引用中唯一的作用就是用队列接收对象即将死亡的通知 -接着再来看下代码,我们使用反射的方式来看看`GC`后`ThreadLocal`中的数据情况:(下面代码来源自:https://blog.csdn.net/thewindkee/article/details/103726942 本地运行演示 GC 回收场景) +接着再来看下代码,我们使用反射的方式来看看`GC`后`ThreadLocal`中的数据情况:(下面代码来源自: 本地运行演示 GC 回收场景) ```java public class ThreadLocalDemo { @@ -692,7 +692,7 @@ private void resize() { 我们以`get(ThreadLocal1)`为例,通过`hash`计算后,正确的`slot`位置应该是 4,而`index=4`的槽位已经有了数据,且`key`值不等于`ThreadLocal1`,所以需要继续往后迭代查找。 -迭代到`index=5`的数据时,此时`Entry.key=null`,触发一次探测式数据回收操作,执行`expungeStaleEntry()`方法,执行完后,`index 5,8`的数据都会被回收,而`index 6,7`的数据都会前移。`index 6,7`前移之后,继续从 `index=5` 往后迭代,于是就在 `index=5` 找到了`key`值相等的`Entry`数据,如下图所示: +迭代到`index=5`的数据时,此时`Entry.key=null`,触发一次探测式数据回收操作,执行`expungeStaleEntry()`方法,执行完后,`index 5,8`的数据都会被回收,而`index 6,7`的数据都会前移。`index 6,7`前移之后,继续从 `index=5` 往后迭代,于是就在 `index=6` 找到了`key`值相等的`Entry`数据,如下图所示: ![](./images/thread-local/28.png) @@ -910,3 +910,5 @@ public class MyThreadPoolTaskExecutor extends ThreadPoolTaskExecutor { #### 使用 MQ 发送消息给第三方系统 在 MQ 发送的消息体中自定义属性`requestId`,接收方消费消息后,自己解析`requestId`使用即可。 + + diff --git a/docs/java/concurrent/virtual-thread.md b/docs/java/concurrent/virtual-thread.md new file mode 100644 index 00000000000..f7f889fb81f --- /dev/null +++ b/docs/java/concurrent/virtual-thread.md @@ -0,0 +1,236 @@ +--- +title: 虚拟线程常见问题总结 +category: Java +tag: + - Java并发 +--- + +> 本文部分内容来自 [Lorin](https://github.com/Lorin-github) 的[PR](https://github.com/Snailclimb/JavaGuide/pull/2190)。 + +虚拟线程在 Java 21 正式发布,这是一项重量级的更新。 + +## 什么是虚拟线程? + +虚拟线程(Virtual Thread)是 JDK 而不是 OS 实现的轻量级线程(Lightweight Process,LWP),由 JVM 调度。许多虚拟线程共享同一个操作系统线程,虚拟线程的数量可以远大于操作系统线程的数量。 + +## 虚拟线程和平台线程有什么关系? + +在引入虚拟线程之前,`java.lang.Thread` 包已经支持所谓的平台线程(Platform Thread),也就是没有虚拟线程之前,我们一直使用的线程。JVM 调度程序通过平台线程(载体线程)来管理虚拟线程,一个平台线程可以在不同的时间执行不同的虚拟线程(多个虚拟线程挂载在一个平台线程上),当虚拟线程被阻塞或等待时,平台线程可以切换到执行另一个虚拟线程。 + +虚拟线程、平台线程和系统内核线程的关系图如下所示(图源:[How to Use Java 19 Virtual Threads](https://medium.com/javarevisited/how-to-use-java-19-virtual-threads-c16a32bad5f7)): + +![虚拟线程、平台线程和系统内核线程的关系](https://oss.javaguide.cn/github/javaguide/java/new-features/virtual-threads-platform-threads-kernel-threads-relationship.png) + +关于平台线程和系统内核线程的对应关系多提一点:在 Windows 和 Linux 等主流操作系统中,Java 线程采用的是一对一的线程模型,也就是一个平台线程对应一个系统内核线程。Solaris 系统是一个特例,HotSpot VM 在 Solaris 上支持多对多和一对一。具体可以参考 R 大的回答: [JVM 中的线程模型是用户级的么?](https://www.zhihu.com/question/23096638/answer/29617153)。 + +## 虚拟线程有什么优点和缺点? + +### 优点 + +- **非常轻量级**:可以在单个线程中创建成百上千个虚拟线程而不会导致过多的线程创建和上下文切换。 +- **简化异步编程**: 虚拟线程可以简化异步编程,使代码更易于理解和维护。它可以将异步代码编写得更像同步代码,避免了回调地狱(Callback Hell)。 +- **减少资源开销**: 由于虚拟线程是由 JVM 实现的,它能够更高效地利用底层资源,例如 CPU 和内存。虚拟线程的上下文切换比平台线程更轻量,因此能够更好地支持高并发场景。 + +### 缺点 + +- **不适用于计算密集型任务**: 虚拟线程适用于 I/O 密集型任务,但不适用于计算密集型任务,因为密集型计算始终需要 CPU 资源作为支持。 +- **与某些第三方库不兼容**: 虽然虚拟线程设计时考虑了与现有代码的兼容性,但某些依赖平台线程特性的第三方库可能不完全兼容虚拟线程。 + +## 如何创建虚拟线程? + +官方提供了以下四种方式创建虚拟线程: + +1. 使用 `Thread.startVirtualThread()` 创建 +2. 使用 `Thread.ofVirtual()` 创建 +3. 使用 `ThreadFactory` 创建 +4. 使用 `Executors.newVirtualThreadPerTaskExecutor()`创建 + +**1、使用 `Thread.startVirtualThread()` 创建** + +```java +public class VirtualThreadTest { + public static void main(String[] args) { + CustomThread customThread = new CustomThread(); + Thread.startVirtualThread(customThread); + } +} + +static class CustomThread implements Runnable { + @Override + public void run() { + System.out.println("CustomThread run"); + } +} +``` + +**2、使用 `Thread.ofVirtual()` 创建** + +```java +public class VirtualThreadTest { + public static void main(String[] args) { + CustomThread customThread = new CustomThread(); + // 创建不启动 + Thread unStarted = Thread.ofVirtual().unstarted(customThread); + unStarted.start(); + // 创建直接启动 + Thread.ofVirtual().start(customThread); + } +} +static class CustomThread implements Runnable { + @Override + public void run() { + System.out.println("CustomThread run"); + } +} +``` + +**3、使用 `ThreadFactory` 创建** + +```java +public class VirtualThreadTest { + public static void main(String[] args) { + CustomThread customThread = new CustomThread(); + ThreadFactory factory = Thread.ofVirtual().factory(); + Thread thread = factory.newThread(customThread); + thread.start(); + } +} + +static class CustomThread implements Runnable { + @Override + public void run() { + System.out.println("CustomThread run"); + } +} +``` + +**4、使用`Executors.newVirtualThreadPerTaskExecutor()`创建** + +```java +public class VirtualThreadTest { + public static void main(String[] args) { + CustomThread customThread = new CustomThread(); + ExecutorService executor = Executors.newVirtualThreadPerTaskExecutor(); + executor.submit(customThread); + } +} +static class CustomThread implements Runnable { + @Override + public void run() { + System.out.println("CustomThread run"); + } +} +``` + +## 虚拟线程和平台线程性能对比 + +通过多线程和虚拟线程的方式处理相同的任务,对比创建的系统线程数和处理耗时。 + +**说明**:统计创建的系统线程中部分为后台线程(比如 GC 线程),两种场景下都一样,所以并不影响对比。 + +**测试代码**: + +```java +public class VirtualThreadTest { + static List list = new ArrayList<>(); + public static void main(String[] args) { + // 开启线程 统计平台线程数 + ScheduledExecutorService scheduledExecutorService = Executors.newScheduledThreadPool(1); + scheduledExecutorService.scheduleAtFixedRate(() -> { + ThreadMXBean threadBean = ManagementFactory.getThreadMXBean(); + ThreadInfo[] threadInfo = threadBean.dumpAllThreads(false, false); + updateMaxThreadNum(threadInfo.length); + }, 10, 10, TimeUnit.MILLISECONDS); + + long start = System.currentTimeMillis(); + // 虚拟线程 + ExecutorService executor = Executors.newVirtualThreadPerTaskExecutor(); + // 使用平台线程 + // ExecutorService executor = Executors.newFixedThreadPool(200); + for (int i = 0; i < 10000; i++) { + executor.submit(() -> { + try { + // 线程睡眠 0.5 s,模拟业务处理 + TimeUnit.MILLISECONDS.sleep(500); + } catch (InterruptedException ignored) { + } + }); + } + executor.close(); + System.out.println("max:" + list.get(0) + " platform thread/os thread"); + System.out.printf("totalMillis:%dms\n", System.currentTimeMillis() - start); + + + } + // 更新创建的平台最大线程数 + private static void updateMaxThreadNum(int num) { + if (list.isEmpty()) { + list.add(num); + } else { + Integer integer = list.get(0); + if (num > integer) { + list.add(0, num); + } + } + } +} +``` + +**请求数 10000 单请求耗时 1s**: + +```plain +// Virtual Thread +max:22 platform thread/os thread +totalMillis:1806ms + +// Platform Thread 线程数200 +max:209 platform thread/os thread +totalMillis:50578ms + +// Platform Thread 线程数500 +max:509 platform thread/os thread +totalMillis:20254ms + +// Platform Thread 线程数1000 +max:1009 platform thread/os thread +totalMillis:10214ms + +// Platform Thread 线程数2000 +max:2009 platform thread/os thread +totalMillis:5358ms +``` + +**请求数 10000 单请求耗时 0.5s**: + +```plain +// Virtual Thread +max:22 platform thread/os thread +totalMillis:1316ms + +// Platform Thread 线程数200 +max:209 platform thread/os thread +totalMillis:25619ms + +// Platform Thread 线程数500 +max:509 platform thread/os thread +totalMillis:10277ms + +// Platform Thread 线程数1000 +max:1009 platform thread/os thread +totalMillis:5197ms + +// Platform Thread 线程数2000 +max:2009 platform thread/os thread +totalMillis:2865ms +``` + +- 可以看到在密集 IO 的场景下,需要创建大量的平台线程异步处理才能达到虚拟线程的处理速度。 +- 因此,在密集 IO 的场景,虚拟线程可以大幅提高线程的执行效率,减少线程资源的创建以及上下文切换。 + +**注意**:有段时间 JDK 一直致力于 Reactor 响应式编程来提高 Java 性能,但响应式编程难以理解、调试、使用,最终又回到了同步编程,最终虚拟线程诞生。 + +## 虚拟线程的底层原理是什么? + +如果你想要详细了解虚拟线程实现原理,推荐一篇文章:[虚拟线程 - VirtualThread 源码透视](https://www.cnblogs.com/throwable/p/16758997.html)。 + +面试一般是不会问到这个问题的,仅供学有余力的同学进一步研究学习。 diff --git a/docs/java/io/io-basis.md b/docs/java/io/io-basis.md index 68c1104d9a5..1ea1bcd3f86 100755 --- a/docs/java/io/io-basis.md +++ b/docs/java/io/io-basis.md @@ -6,6 +6,8 @@ tag: - Java基础 --- + + ## IO 流简介 IO 即 `Input/Output`,输入和输出。数据输入到计算机内存的过程即输入,反之输出到外部存储(比如数据库,文件,远程主机)的过程即输出。数据传输过程类似于水流,因此称为 IO 流。IO 流在 Java 中分为输入流和输出流,而根据数据的处理方式又分为字节流和字符流。 @@ -18,7 +20,7 @@ Java IO 流的 40 多个类都是从如下 4 个抽象类基类中派生出来 ## 字节流 ### InputStream(字节输入流) - + `InputStream`用于从源头(通常是文件)读取数据(字节信息)到内存中,`java.io.InputStream`抽象类是所有字节输入流的父类。 `InputStream` 常用方法: @@ -62,7 +64,7 @@ try (InputStream fis = new FileInputStream("input.txt")) { 输出: -``` +```plain Number of remaining bytes:11 The actual number of bytes skipped:2 The content read from file:JavaGuide @@ -80,7 +82,7 @@ String result = new String(bufferedInputStream.readAllBytes()); System.out.println(result); ``` -`DataInputStream` 用于读取指定类型数据,不能单独使用,必须结合 `FileInputStream` 。 +`DataInputStream` 用于读取指定类型数据,不能单独使用,必须结合其它流,比如 `FileInputStream` 。 ```java FileInputStream fileInputStream = new FileInputStream("input.txt"); @@ -138,7 +140,7 @@ FileOutputStream fileOutputStream = new FileOutputStream("output.txt"); BufferedOutputStream bos = new BufferedOutputStream(fileOutputStream) ``` -**`DataOutputStream`** 用于写入指定类型数据,不能单独使用,必须结合 `FileOutputStream` +**`DataOutputStream`** 用于写入指定类型数据,不能单独使用,必须结合其它流,比如 `FileOutputStream` 。 ```java // 输出流 @@ -182,7 +184,9 @@ The content read from file:§å®¶å¥½ 因此,I/O 流就干脆提供了一个直接操作字符的接口,方便我们平时对字符进行流操作。如果音频文件、图片等媒体文件用字节流比较好,如果涉及到字符的话使用字符流比较好。 -字符流默认采用的是 `Unicode` 编码,我们可以通过构造方法自定义编码。顺便分享一下之前遇到的笔试题:常用字符编码所占字节数?`utf8` :英文占 1 字节,中文占 3 字节,`unicode`:任何字符都占 2 个字节,`gbk`:英文占 1 字节,中文占 2 字节。 +字符流默认采用的是 `Unicode` 编码,我们可以通过构造方法自定义编码。 + +Unicode 本身只是一种字符集,它为每个字符分配一个唯一的数字编号,并没有规定具体的存储方式。UTF-8、UTF-16、UTF-32 都是 Unicode 的编码方式,它们使用不同的字节数来表示 Unicode 字符。例如,UTF-8 :英文占 1 字节,中文占 3 字节。 ### Reader(字符输入流) @@ -231,7 +235,7 @@ try (FileReader fileReader = new FileReader("input.txt");) { 输出: -``` +```plain The actual number of bytes skipped:3 The content read from file:我是Guide。 ``` @@ -294,7 +298,7 @@ BufferedInputStream bufferedInputStream = new BufferedInputStream(new FileInputS 我使用 `write(int b)` 和 `read()` 方法,分别通过字节流和字节缓冲流复制一个 `524.9 mb` 的 PDF 文件耗时对比如下: -``` +```plain 使用缓冲流复制PDF文件总耗时:15428 毫秒 使用普通字节流复制PDF文件总耗时:2555062 毫秒 ``` @@ -345,7 +349,7 @@ void copy_pdf_to_another_pdf_stream() { 这次我们使用 `read(byte b[])` 和 `write(byte b[], int off, int len)` 方法,分别通过字节流和字节缓冲流复制一个 524.9 mb 的 PDF 文件耗时对比如下: -``` +```plain 使用缓冲流复制PDF文件总耗时:695 毫秒 使用普通字节流复制PDF文件总耗时:989 毫秒 ``` @@ -426,7 +430,7 @@ class BufferedInputStream extends FilterInputStream { ### BufferedOutputStream(字节缓冲输出流) -`BufferedOutputStream` 将数据(字节信息)写入到目的地(通常是文件)的过程中不会一个字节一个字节的写入,而是会先将要写入的字节存放在缓存区,并从内部缓冲区中单独写入字节。这样大幅减少了 IO 次数,提高了读取效率 +`BufferedOutputStream` 将数据(字节信息)写入到目的地(通常是文件)的过程中不会一个字节一个字节的写入,而是会先将要写入的字节存放在缓存区,并从内部缓冲区中单独写入字节。这样大幅减少了 IO 次数,提高了效率 ```java try (BufferedOutputStream bos = new BufferedOutputStream(new FileOutputStream("output.txt"))) { @@ -514,7 +518,7 @@ System.out.println("读取之前的偏移量:" + randomAccessFile.getFilePoint 输出: -``` +```plain 读取之前的偏移量:0,当前读取到的字符A,读取之后的偏移量:1 读取之前的偏移量:6,当前读取到的字符G,读取之后的偏移量:7 读取之前的偏移量:0,当前读取到的字符A,读取之后的偏移量:1 @@ -542,3 +546,5 @@ randomAccessFile.write(new byte[]{'H', 'I', 'J', 'K'}); ![](https://oss.javaguide.cn/github/javaguide/java/image-20220428104115362.png) `RandomAccessFile` 的实现依赖于 `FileDescriptor` (文件描述符) 和 `FileChannel` (内存映射文件)。 + + diff --git a/docs/java/io/io-design-patterns.md b/docs/java/io/io-design-patterns.md index f7d09053cbb..f005a18ece4 100644 --- a/docs/java/io/io-design-patterns.md +++ b/docs/java/io/io-design-patterns.md @@ -99,7 +99,7 @@ IO 流中的装饰器模式应用的例子实在是太多了,不需要特意 **适配器(Adapter Pattern)模式** 主要用于接口互不兼容的类的协调工作,你可以将其联想到我们日常经常使用的电源适配器。 -适配器模式中存在被适配的对象或者类称为 **适配者(Adapter)** ,作用于适配者的对象或者类称为**适配器(Adapter)** 。适配器分为对象适配器和类适配器。类适配器使用继承关系来实现,对象适配器使用组合关系来实现。 +适配器模式中存在被适配的对象或者类称为 **适配者(Adaptee)** ,作用于适配者的对象或者类称为**适配器(Adapter)** 。适配器分为对象适配器和类适配器。类适配器使用继承关系来实现,对象适配器使用组合关系来实现。 IO 流中的字符流和字节流的接口不同,它们之间可以协调工作就是基于适配器模式来做的,更准确点来说是对象适配器。通过适配器,我们可以将字节流对象适配成一个字符流对象,这样我们可以直接通过字节流对象来读取或者写入字符数据。 @@ -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(); } } @@ -215,7 +215,7 @@ static final class RunnableAdapter implements Callable { 工厂模式用于创建对象,NIO 中大量用到了工厂模式,比如 `Files` 类的 `newInputStream` 方法用于创建 `InputStream` 对象(静态工厂)、 `Paths` 类的 `get` 方法创建 `Path` 对象(静态工厂)、`ZipFileSystem` 类(`sun.nio`包下的类,属于 `java.nio` 相关的一些内部实现)的 `getPath` 的方法创建 `Path` 对象(简单工厂)。 ```java -InputStream is Files.newInputStream(Paths.get(generatorLogoPath)) +InputStream is = Files.newInputStream(Paths.get(generatorLogoPath)) ``` ## 观察者模式 @@ -314,6 +314,8 @@ class PollingWatchService ## 参考 -- Patterns in Java APIs:http://cecs.wright.edu/~tkprasad/courses/ceg860/paper/node26.html -- 装饰器模式:通过剖析 Java IO 类库源码学习装饰器模式:https://time.geekbang.org/column/article/204845 -- sun.nio 包是什么,是 java 代码么? - RednaxelaFX https://www.zhihu.com/question/29237781/answer/43653953 +- Patterns in Java APIs: +- 装饰器模式:通过剖析 Java IO 类库源码学习装饰器模式: +- sun.nio 包是什么,是 java 代码么? - RednaxelaFX + + diff --git a/docs/java/io/io-model.md b/docs/java/io/io-model.md index 8e0932700ad..e6d48bc0439 100644 --- a/docs/java/io/io-model.md +++ b/docs/java/io/io-model.md @@ -18,7 +18,7 @@ I/O 一直是很多小伙伴难以理解的一个知识点,这篇文章我会 ### 何为 I/O? -I/O(**I**nput/**O**utpu) 即**输入/输出** 。 +I/O(**I**nput/**O**utput) 即**输入/输出** 。 **我们先从计算机结构的角度来解读一下 I/O。** @@ -67,7 +67,7 @@ UNIX 系统下, IO 模型一共有 5 种:**同步阻塞 I/O**、**同步非 同步阻塞 IO 模型中,应用程序发起 read 调用后,会一直阻塞,直到内核把数据拷贝到用户空间。 -![图源:《深入拆解Tomcat & Jetty》](https://p3-juejin.byteimg.com/tos-cn-i-k3u1fbpfcp/6a9e704af49b4380bb686f0c96d33b81~tplv-k3u1fbpfcp-watermark.image) +![图源:《深入拆解Tomcat & Jetty》](https://oss.javaguide.cn/p3-juejin/6a9e704af49b4380bb686f0c96d33b81~tplv-k3u1fbpfcp-watermark.png) 在客户端连接数量不高的情况下,是没问题的。但是,当面对十万甚至百万级连接的时候,传统的 BIO 模型是无能为力的。因此,我们需要一种更高效的 I/O 处理模型来应对更高的并发量。 @@ -81,7 +81,7 @@ Java 中的 NIO 可以看作是 **I/O 多路复用模型**。也有很多人认 我们先来看看 **同步非阻塞 IO 模型**。 -![图源:《深入拆解Tomcat & Jetty》](https://p3-juejin.byteimg.com/tos-cn-i-k3u1fbpfcp/bb174e22dbe04bb79fe3fc126aed0c61~tplv-k3u1fbpfcp-watermark.image) +![图源:《深入拆解Tomcat & Jetty》](https://oss.javaguide.cn/p3-juejin/bb174e22dbe04bb79fe3fc126aed0c61~tplv-k3u1fbpfcp-watermark.png) 同步非阻塞 IO 模型中,应用程序会一直发起 read 调用,等待数据从内核空间拷贝到用户空间的这段时间里,线程依然是阻塞的,直到在内核把数据拷贝到用户空间。 @@ -91,7 +91,7 @@ Java 中的 NIO 可以看作是 **I/O 多路复用模型**。也有很多人认 这个时候,**I/O 多路复用模型** 就上场了。 -![](https://p6-juejin.byteimg.com/tos-cn-i-k3u1fbpfcp/88ff862764024c3b8567367df11df6ab~tplv-k3u1fbpfcp-watermark.image) +![](https://oss.javaguide.cn/github/javaguide/java/io/88ff862764024c3b8567367df11df6ab~tplv-k3u1fbpfcp-watermark.png) IO 多路复用模型中,线程首先发起 select 调用,询问内核数据是否准备就绪,等内核把数据准备好了,用户线程再发起 read 调用。read 调用的过程(数据从内核空间 -> 用户空间)还是阻塞的。 @@ -104,7 +104,7 @@ IO 多路复用模型中,线程首先发起 select 调用,询问内核数据 Java 中的 NIO ,有一个非常重要的**选择器 ( Selector )** 的概念,也可以被称为 **多路复用器**。通过它,只需要一个线程便可以管理多个客户端连接。当客户端数据到了之后,才会为其服务。 -![](https://p3-juejin.byteimg.com/tos-cn-i-k3u1fbpfcp/0f483f2437ce4ecdb180134270a00144~tplv-k3u1fbpfcp-watermark.image) +![Buffer、Channel和Selector三者之间的关系](https://oss.javaguide.cn/github/javaguide/java/nio/channel-buffer-selector.png) ### AIO (Asynchronous I/O) @@ -112,19 +112,21 @@ AIO 也就是 NIO 2。Java 7 中引入了 NIO 的改进版 NIO 2,它是异步 IO 异步 IO 是基于事件和回调机制实现的,也就是应用操作之后会直接返回,不会堵塞在那里,当后台处理完成,操作系统会通知相应的线程进行后续的操作。 -![](https://p1-juejin.byteimg.com/tos-cn-i-k3u1fbpfcp/3077e72a1af049559e81d18205b56fd7~tplv-k3u1fbpfcp-watermark.image) +![](https://oss.javaguide.cn/github/javaguide/java/io/3077e72a1af049559e81d18205b56fd7~tplv-k3u1fbpfcp-watermark.png) 目前来说 AIO 的应用还不是很广泛。Netty 之前也尝试使用过 AIO,不过又放弃了。这是因为,Netty 使用了 AIO 之后,在 Linux 系统上的性能并没有多少提升。 最后,来一张图,简单总结一下 Java 中的 BIO、NIO、AIO。 -![](https://images.xiaozhuanlan.com/photo/2020/33b193457c928ae02217480f994814b6.png) +![BIO、NIO 和 AIO 对比](https://oss.javaguide.cn/github/javaguide/java/nio/bio-aio-nio.png) ## 参考 - 《深入拆解 Tomcat & Jetty》 -- 如何完成一次 IO:https://llc687.top/126.html +- 如何完成一次 IO: - 程序员应该这样理解 IO:[https://www.jianshu.com/p/fa7bdc4f3de7](https://www.jianshu.com/p/fa7bdc4f3de7) -- 10 分钟看懂, Java NIO 底层原理:https://www.cnblogs.com/crazymakercircle/p/10225159.html -- IO 模型知多少 | 理论篇:https://www.cnblogs.com/sheng-jie/p/how-much-you-know-about-io-models.html +- 10 分钟看懂, Java NIO 底层原理: +- IO 模型知多少 | 理论篇: - 《UNIX 网络编程 卷 1;套接字联网 API 》6.2 节 IO 模型 + + diff --git a/docs/java/io/nio-basis.md b/docs/java/io/nio-basis.md new file mode 100644 index 00000000000..4cf9723ba37 --- /dev/null +++ b/docs/java/io/nio-basis.md @@ -0,0 +1,392 @@ +--- +title: Java NIO 核心知识总结 +category: Java +tag: + - Java IO + - Java基础 +--- + +在学习 NIO 之前,需要先了解一下计算机 I/O 模型的基础理论知识。还不了解的话,可以参考我写的这篇文章:[Java IO 模型详解](https://javaguide.cn/java/io/io-model.html)。 + +## NIO 简介 + +在传统的 Java I/O 模型(BIO)中,I/O 操作是以阻塞的方式进行的。也就是说,当一个线程执行一个 I/O 操作时,它会被阻塞直到操作完成。这种阻塞模型在处理多个并发连接时可能会导致性能瓶颈,因为需要为每个连接创建一个线程,而线程的创建和切换都是有开销的。 + +为了解决这个问题,在 Java1.4 版本引入了一种新的 I/O 模型 — **NIO** (New IO,也称为 Non-blocking IO) 。NIO 弥补了同步阻塞 I/O 的不足,它在标准 Java 代码中提供了非阻塞、面向缓冲、基于通道的 I/O,可以使用少量的线程来处理多个连接,大大提高了 I/O 效率和并发。 + +下图是 BIO、NIO 和 AIO 处理客户端请求的简单对比图(关于 AIO 的介绍,可以看我写的这篇文章:[Java IO 模型详解](https://javaguide.cn/java/io/io-model.html),不是重点,了解即可)。 + +![BIO、NIO 和 AIO 对比](https://oss.javaguide.cn/github/javaguide/java/nio/bio-aio-nio.png) + +⚠️需要注意:使用 NIO 并不一定意味着高性能,它的性能优势主要体现在高并发和高延迟的网络环境下。当连接数较少、并发程度较低或者网络传输速度较快时,NIO 的性能并不一定优于传统的 BIO 。 + +## NIO 核心组件 + +NIO 主要包括以下三个核心组件: + +- **Buffer(缓冲区)**:NIO 读写数据都是通过缓冲区进行操作的。读操作的时候将 Channel 中的数据填充到 Buffer 中,而写操作时将 Buffer 中的数据写入到 Channel 中。 +- **Channel(通道)**:Channel 是一个双向的、可读可写的数据传输通道,NIO 通过 Channel 来实现数据的输入输出。通道是一个抽象的概念,它可以代表文件、套接字或者其他数据源之间的连接。 +- **Selector(选择器)**:允许一个线程处理多个 Channel,基于事件驱动的 I/O 多路复用模型。所有的 Channel 都可以注册到 Selector 上,由 Selector 来分配线程来处理事件。 + +三者的关系如下图所示(暂时不理解没关系,后文会详细介绍): + +![Buffer、Channel和Selector三者之间的关系](https://oss.javaguide.cn/github/javaguide/java/nio/channel-buffer-selector.png) + +下面详细介绍一下这三个组件。 + +### Buffer(缓冲区) + +在传统的 BIO 中,数据的读写是面向流的, 分为字节流和字符流。 + +在 Java 1.4 的 NIO 库中,所有数据都是用缓冲区处理的,这是新库和之前的 BIO 的一个重要区别,有点类似于 BIO 中的缓冲流。NIO 在读取数据时,它是直接读到缓冲区中的。在写入数据时,写入到缓冲区中。 使用 NIO 在读写数据时,都是通过缓冲区进行操作。 + +`Buffer` 的子类如下图所示。其中,最常用的是 `ByteBuffer`,它可以用来存储和操作字节数据。 + +![Buffer 的子类](https://oss.javaguide.cn/github/javaguide/java/nio/buffer-subclasses.png) + +你可以将 Buffer 理解为一个数组,`IntBuffer`、`FloatBuffer`、`CharBuffer` 等分别对应 `int[]`、`float[]`、`char[]` 等。 + +为了更清晰地认识缓冲区,我们来简单看看`Buffer` 类中定义的四个成员变量: + +```java +public abstract class Buffer { + // Invariants: mark <= position <= limit <= capacity + private int mark = -1; + private int position = 0; + private int limit; + private int capacity; +} +``` + +这四个成员变量的具体含义如下: + +1. 容量(`capacity`):`Buffer`可以存储的最大数据量,`Buffer`创建时设置且不可改变; +2. 界限(`limit`):`Buffer` 中可以读/写数据的边界。写模式下,`limit` 代表最多能写入的数据,一般等于 `capacity`(可以通过`limit(int newLimit)`方法设置);读模式下,`limit` 等于 Buffer 中实际写入的数据大小。 +3. 位置(`position`):下一个可以被读写的数据的位置(索引)。从写操作模式到读操作模式切换的时候(flip),`position` 都会归零,这样就可以从头开始读写了。 +4. 标记(`mark`):`Buffer`允许将位置直接定位到该标记处,这是一个可选属性; + +并且,上述变量满足如下的关系:**0 <= mark <= position <= limit <= capacity** 。 + +另外,Buffer 有读模式和写模式这两种模式,分别用于从 Buffer 中读取数据或者向 Buffer 中写入数据。Buffer 被创建之后默认是写模式,调用 `flip()` 可以切换到读模式。如果要再次切换回写模式,可以调用 `clear()` 或者 `compact()` 方法。 + +![position 、limit 和 capacity 之前的关系](https://oss.javaguide.cn/github/javaguide/java/nio/JavaNIOBuffer.png) + +![position 、limit 和 capacity 之前的关系](https://oss.javaguide.cn/github/javaguide/java/nio/NIOBufferClassAttributes.png) + +`Buffer` 对象不能通过 `new` 调用构造方法创建对象 ,只能通过静态方法实例化 `Buffer`。 + +这里以 `ByteBuffer`为例进行介绍: + +```java +// 分配堆内存 +public static ByteBuffer allocate(int capacity); +// 分配直接内存 +public static ByteBuffer allocateDirect(int capacity); +``` + +Buffer 最核心的两个方法: + +1. `get` : 读取缓冲区的数据 +2. `put` :向缓冲区写入数据 + +除上述两个方法之外,其他的重要方法: + +- `flip` :将缓冲区从写模式切换到读模式,它会将 `limit` 的值设置为当前 `position` 的值,将 `position` 的值设置为 0。 +- `clear`: 清空缓冲区,将缓冲区从读模式切换到写模式,并将 `position` 的值设置为 0,将 `limit` 的值设置为 `capacity` 的值。 +- …… + +Buffer 中数据变化的过程: + +```java +import java.nio.*; + +public class CharBufferDemo { + public static void main(String[] args) { + // 分配一个容量为8的CharBuffer + CharBuffer buffer = CharBuffer.allocate(8); + System.out.println("初始状态:"); + printState(buffer); + + // 向buffer写入3个字符 + buffer.put('a').put('b').put('c'); + System.out.println("写入3个字符后的状态:"); + printState(buffer); + + // 调用flip()方法,准备读取buffer中的数据,将 position 置 0,limit 的置 3 + buffer.flip(); + System.out.println("调用flip()方法后的状态:"); + printState(buffer); + + // 读取字符 + while (buffer.hasRemaining()) { + System.out.print(buffer.get()); + } + + // 调用clear()方法,清空缓冲区,将 position 的值置为 0,将 limit 的值置为 capacity 的值 + buffer.clear(); + System.out.println("调用clear()方法后的状态:"); + printState(buffer); + + } + + // 打印buffer的capacity、limit、position、mark的位置 + private static void printState(CharBuffer buffer) { + System.out.print("capacity: " + buffer.capacity()); + System.out.print(", limit: " + buffer.limit()); + System.out.print(", position: " + buffer.position()); + System.out.print(", mark 开始读取的字符: " + buffer.mark()); + System.out.println("\n"); + } +} +``` + +输出: + +```bash +初始状态: +capacity: 8, limit: 8, position: 0 + +写入3个字符后的状态: +capacity: 8, limit: 8, position: 3 + +准备读取buffer中的数据! + +调用flip()方法后的状态: +capacity: 8, limit: 3, position: 0 + +读取到的数据:abc + +调用clear()方法后的状态: +capacity: 8, limit: 8, position: 0 +``` + +为了帮助理解,我绘制了一张图片展示 `capacity`、`limit`和`position`每一阶段的变化。 + +![capacity、limit和position每一阶段的变化](https://oss.javaguide.cn/github/javaguide/java/nio/NIOBufferClassAttributesDataChanges.png) + +### Channel(通道) + +Channel 是一个通道,它建立了与数据源(如文件、网络套接字等)之间的连接。我们可以利用它来读取和写入数据,就像打开了一条自来水管,让数据在 Channel 中自由流动。 + +BIO 中的流是单向的,分为各种 `InputStream`(输入流)和 `OutputStream`(输出流),数据只是在一个方向上传输。通道与流的不同之处在于通道是双向的,它可以用于读、写或者同时用于读写。 + +Channel 与前面介绍的 Buffer 打交道,读操作的时候将 Channel 中的数据填充到 Buffer 中,而写操作时将 Buffer 中的数据写入到 Channel 中。 + +![Channel 和 Buffer之间的关系](https://oss.javaguide.cn/github/javaguide/java/nio/channel-buffer.png) + +另外,因为 Channel 是全双工的,所以它可以比流更好地映射底层操作系统的 API。特别是在 UNIX 网络编程模型中,底层操作系统的通道都是全双工的,同时支持读写操作。 + +`Channel` 的子类如下图所示。 + +![Channel 的子类](https://oss.javaguide.cn/github/javaguide/java/nio/channel-subclasses.png) + +其中,最常用的是以下几种类型的通道: + +- `FileChannel`:文件访问通道; +- `SocketChannel`、`ServerSocketChannel`:TCP 通信通道; +- `DatagramChannel`:UDP 通信通道; + +![Channel继承关系图](https://oss.javaguide.cn/github/javaguide/java/nio/channel-inheritance-relationship.png) + +Channel 最核心的两个方法: + +1. `read` :读取数据并写入到 Buffer 中。 +2. `write` :将 Buffer 中的数据写入到 Channel 中。 + +这里我们以 `FileChannel` 为例演示一下是读取文件数据的。 + +```java +RandomAccessFile reader = new RandomAccessFile("/Users/guide/Documents/test_read.in", "r")) +FileChannel channel = reader.getChannel(); +ByteBuffer buffer = ByteBuffer.allocate(1024); +channel.read(buffer); +``` + +### Selector(选择器) + +Selector(选择器) 是 NIO 中的一个关键组件,它允许一个线程处理多个 Channel。Selector 是基于事件驱动的 I/O 多路复用模型,主要运作原理是:通过 Selector 注册通道的事件,Selector 会不断地轮询注册在其上的 Channel。当事件发生时,比如:某个 Channel 上面有新的 TCP 连接接入、读和写事件,这个 Channel 就处于就绪状态,会被 Selector 轮询出来。Selector 会将相关的 Channel 加入到就绪集合中。通过 SelectionKey 可以获取就绪 Channel 的集合,然后对这些就绪的 Channel 进行相应的 I/O 操作。 + +![Selector 选择器工作示意图](https://oss.javaguide.cn/github/javaguide/java/nio/selector-channel-selectionkey.png) + +一个多路复用器 Selector 可以同时轮询多个 Channel,由于 JDK 使用了 `epoll()` 代替传统的 `select` 实现,所以它并没有最大连接句柄 `1024/2048` 的限制。这也就意味着只需要一个线程负责 Selector 的轮询,就可以接入成千上万的客户端。 + +Selector 可以监听以下四种事件类型: + +1. `SelectionKey.OP_ACCEPT`:表示通道接受连接的事件,这通常用于 `ServerSocketChannel`。 +2. `SelectionKey.OP_CONNECT`:表示通道完成连接的事件,这通常用于 `SocketChannel`。 +3. `SelectionKey.OP_READ`:表示通道准备好进行读取的事件,即有数据可读。 +4. `SelectionKey.OP_WRITE`:表示通道准备好进行写入的事件,即可以写入数据。 + +`Selector`是抽象类,可以通过调用此类的 `open()` 静态方法来创建 Selector 实例。Selector 可以同时监控多个 `SelectableChannel` 的 `IO` 状况,是非阻塞 `IO` 的核心。 + +一个 Selector 实例有三个 `SelectionKey` 集合: + +1. 所有的 `SelectionKey` 集合:代表了注册在该 Selector 上的 `Channel`,这个集合可以通过 `keys()` 方法返回。 +2. 被选择的 `SelectionKey` 集合:代表了所有可通过 `select()` 方法获取的、需要进行 `IO` 处理的 Channel,这个集合可以通过 `selectedKeys()` 返回。 +3. 被取消的 `SelectionKey` 集合:代表了所有被取消注册关系的 `Channel`,在下一次执行 `select()` 方法时,这些 `Channel` 对应的 `SelectionKey` 会被彻底删除,程序通常无须直接访问该集合,也没有暴露访问的方法。 + +简单演示一下如何遍历被选择的 `SelectionKey` 集合并进行处理: + +```java +Set selectedKeys = selector.selectedKeys(); +Iterator keyIterator = selectedKeys.iterator(); +while (keyIterator.hasNext()) { + SelectionKey key = keyIterator.next(); + if (key != null) { + if (key.isAcceptable()) { + // ServerSocketChannel 接收了一个新连接 + } else if (key.isConnectable()) { + // 表示一个新连接建立 + } else if (key.isReadable()) { + // Channel 有准备好的数据,可以读取 + } else if (key.isWritable()) { + // Channel 有空闲的 Buffer,可以写入数据 + } + } + keyIterator.remove(); +} +``` + +Selector 还提供了一系列和 `select()` 相关的方法: + +- `int select()`:监控所有注册的 `Channel`,当它们中间有需要处理的 `IO` 操作时,该方法返回,并将对应的 `SelectionKey` 加入被选择的 `SelectionKey` 集合中,该方法返回这些 `Channel` 的数量。 +- `int select(long timeout)`:可以设置超时时长的 `select()` 操作。 +- `int selectNow()`:执行一个立即返回的 `select()` 操作,相对于无参数的 `select()` 方法而言,该方法不会阻塞线程。 +- `Selector wakeup()`:使一个还未返回的 `select()` 方法立刻返回。 +- …… + +使用 Selector 实现网络读写的简单示例: + +```java +import java.io.IOException; +import java.net.InetSocketAddress; +import java.nio.ByteBuffer; +import java.nio.channels.SelectionKey; +import java.nio.channels.Selector; +import java.nio.channels.ServerSocketChannel; +import java.nio.channels.SocketChannel; +import java.util.Iterator; +import java.util.Set; + +public class NioSelectorExample { + + public static void main(String[] args) { + try { + ServerSocketChannel serverSocketChannel = ServerSocketChannel.open(); + serverSocketChannel.configureBlocking(false); + serverSocketChannel.socket().bind(new InetSocketAddress(8080)); + + Selector selector = Selector.open(); + // 将 ServerSocketChannel 注册到 Selector 并监听 OP_ACCEPT 事件 + serverSocketChannel.register(selector, SelectionKey.OP_ACCEPT); + + while (true) { + int readyChannels = selector.select(); + + if (readyChannels == 0) { + continue; + } + + Set selectedKeys = selector.selectedKeys(); + Iterator keyIterator = selectedKeys.iterator(); + + while (keyIterator.hasNext()) { + SelectionKey key = keyIterator.next(); + + if (key.isAcceptable()) { + // 处理连接事件 + ServerSocketChannel server = (ServerSocketChannel) key.channel(); + SocketChannel client = server.accept(); + client.configureBlocking(false); + + // 将客户端通道注册到 Selector 并监听 OP_READ 事件 + client.register(selector, SelectionKey.OP_READ); + } else if (key.isReadable()) { + // 处理读事件 + SocketChannel client = (SocketChannel) key.channel(); + ByteBuffer buffer = ByteBuffer.allocate(1024); + int bytesRead = client.read(buffer); + + if (bytesRead > 0) { + buffer.flip(); + System.out.println("收到数据:" +new String(buffer.array(), 0, bytesRead)); + // 将客户端通道注册到 Selector 并监听 OP_WRITE 事件 + client.register(selector, SelectionKey.OP_WRITE); + } else if (bytesRead < 0) { + // 客户端断开连接 + client.close(); + } + } else if (key.isWritable()) { + // 处理写事件 + SocketChannel client = (SocketChannel) key.channel(); + ByteBuffer buffer = ByteBuffer.wrap("Hello, Client!".getBytes()); + client.write(buffer); + + // 将客户端通道注册到 Selector 并监听 OP_READ 事件 + client.register(selector, SelectionKey.OP_READ); + } + + keyIterator.remove(); + } + } + } catch (IOException e) { + e.printStackTrace(); + } + } +} +``` + +在示例中,我们创建了一个简单的服务器,监听 8080 端口,使用 Selector 处理连接、读取和写入事件。当接收到客户端的数据时,服务器将读取数据并将其打印到控制台,然后向客户端回复 "Hello, Client!"。 + +## NIO 零拷贝 + +零拷贝是提升 IO 操作性能的一个常用手段,像 ActiveMQ、Kafka 、RocketMQ、QMQ、Netty 等顶级开源项目都用到了零拷贝。 + +零拷贝是指计算机执行 IO 操作时,CPU 不需要将数据从一个存储区域复制到另一个存储区域,从而可以减少上下文切换以及 CPU 的拷贝时间。也就是说,零拷贝主要解决操作系统在处理 I/O 操作时频繁复制数据的问题。零拷贝的常见实现技术有: `mmap+write`、`sendfile`和 `sendfile + DMA gather copy` 。 + +下图展示了各种零拷贝技术的对比图: + +| | CPU 拷贝 | DMA 拷贝 | 系统调用 | 上下文切换 | +| -------------------------- | -------- | -------- | ---------- | ---------- | +| 传统方法 | 2 | 2 | read+write | 4 | +| mmap+write | 1 | 2 | mmap+write | 4 | +| sendfile | 1 | 2 | sendfile | 2 | +| sendfile + DMA gather copy | 0 | 2 | sendfile | 2 | + +可以看出,无论是传统的 I/O 方式,还是引入了零拷贝之后,2 次 DMA(Direct Memory Access) 拷贝是都少不了的。因为两次 DMA 都是依赖硬件完成的。零拷贝主要是减少了 CPU 拷贝及上下文的切换。 + +Java 对零拷贝的支持: + +- `MappedByteBuffer` 是 NIO 基于内存映射(`mmap`)这种零拷⻉⽅式的提供的⼀种实现,底层实际是调用了 Linux 内核的 `mmap` 系统调用。它可以将一个文件或者文件的一部分映射到内存中,形成一个虚拟内存文件,这样就可以直接操作内存中的数据,而不需要通过系统调用来读写文件。 +- `FileChannel` 的`transferTo()/transferFrom()`是 NIO 基于发送文件(`sendfile`)这种零拷贝方式的提供的一种实现,底层实际是调用了 Linux 内核的 `sendfile`系统调用。它可以直接将文件数据从磁盘发送到网络,而不需要经过用户空间的缓冲区。关于`FileChannel`的用法可以看看这篇文章:[Java NIO 文件通道 FileChannel 用法](https://www.cnblogs.com/robothy/p/14235598.html)。 + +代码示例: + +```java +private void loadFileIntoMemory(File xmlFile) throws IOException { + FileInputStream fis = new FileInputStream(xmlFile); + // 创建 FileChannel 对象 + FileChannel fc = fis.getChannel(); + // FileChannel.map() 将文件映射到直接内存并返回 MappedByteBuffer 对象 + MappedByteBuffer mmb = fc.map(FileChannel.MapMode.READ_ONLY, 0, fc.size()); + xmlFileBuffer = new byte[(int)fc.size()]; + mmb.get(xmlFileBuffer); + fis.close(); +} +``` + +## 总结 + +这篇文章我们主要介绍了 NIO 的核心知识点,包括 NIO 的核心组件和零拷贝。 + +如果我们需要使用 NIO 构建网络程序的话,不建议直接使用原生 NIO,编程复杂且功能性太弱,推荐使用一些成熟的基于 NIO 的网络编程框架比如 Netty。Netty 在 NIO 的基础上进行了一些优化和扩展比如支持多种协议、支持 SSL/TLS 等等。 + +## 参考 + +- Java NIO 浅析: + +- 面试官:Java NIO 了解? + +- Java NIO:Buffer、Channel 和 Selector: + + diff --git a/docs/java/jvm/class-file-structure.md b/docs/java/jvm/class-file-structure.md index f7fdfbf3e87..31cc64e30fb 100644 --- a/docs/java/jvm/class-file-structure.md +++ b/docs/java/jvm/class-file-structure.md @@ -31,11 +31,11 @@ ClassFile { u2 access_flags;//Class 的访问标记 u2 this_class;//当前类 u2 super_class;//父类 - u2 interfaces_count;//接口 + u2 interfaces_count;//接口数量 u2 interfaces[interfaces_count];//一个类可以实现多个接口 - u2 fields_count;//Class 文件的字段属性 + u2 fields_count;//字段数量 field_info fields[fields_count];//一个类可以有多个字段 - u2 methods_count;//Class 文件的方法数量 + u2 methods_count;//方法数量 method_info methods[methods_count];//一个类可以有个多个方法 u2 attributes_count;//此类的属性表中的属性数 attribute_info attributes[attributes_count];//属性表集合 @@ -60,9 +60,7 @@ ClassFile { u4 magic; //Class 文件的标志 ``` -每个 Class 文件的头 4 个字节称为魔数(Magic Number),它的唯一作用是**确定这个文件是否为一个能被虚拟机接收的 Class 文件**。 - -程序设计者很多时候都喜欢用一些特殊的数字表示固定的文件类型或者其它特殊的含义。 +每个 Class 文件的头 4 个字节称为魔数(Magic Number),它的唯一作用是**确定这个文件是否为一个能被虚拟机接收的 Class 文件**。Java 规范规定魔数为固定值:0xCAFEBABE。如果读取的文件不是以这个魔数开头,Java 虚拟机将拒绝加载它。 ### Class 文件版本号(Minor&Major Version) @@ -71,7 +69,7 @@ ClassFile { u2 major_version;//Class 的大版本号 ``` -紧接着魔数的四个字节存储的是 Class 文件的版本号:第 5 和第 6 位是**次版本号**,第 7 和第 8 位是**主版本号**。 +紧接着魔数的四个字节存储的是 Class 文件的版本号:第 5 和第 6 个字节是**次版本号**,第 7 和第 8 个字节是**主版本号**。 每当 Java 发布大版本(比如 Java 8,Java9)的时候,主版本号都会加 1。你可以使用 `javap -v` 命令来快速查看 Class 文件的版本号信息。 @@ -99,11 +97,11 @@ ClassFile { | CONSTANT_utf8_info | 1 | UTF-8 编码的字符串 | | CONSTANT_Integer_info | 3 | 整形字面量 | | CONSTANT_Float_info | 4 | 浮点型字面量 | -| CONSTANT_Long_info | 5 | 长整型字面量 | -| CONSTANT_Double_info | 6 | 双精度浮点型字面量 | -| CONSTANT_Class_info | 7 | 类或接口的符号引用 | -| CONSTANT_String_info | 8 | 字符串类型字面量 | -| CONSTANT_FieldRef_info | 9 | 字段的符号引用 | +| CONSTANT_Long_info | 5 | 长整型字面量 | +| CONSTANT_Double_info | 6 | 双精度浮点型字面量 | +| CONSTANT_Class_info | 7 | 类或接口的符号引用 | +| CONSTANT_String_info | 8 | 字符串类型字面量 | +| CONSTANT_FieldRef_info | 9 | 字段的符号引用 | | CONSTANT_MethodRef_info | 10 | 类中方法的符号引用 | | CONSTANT_InterfaceMethodRef_info | 11 | 接口中方法的符号引用 | | CONSTANT_NameAndType_info | 12 | 字段或方法的符号引用 | @@ -123,7 +121,7 @@ ClassFile { 类访问和属性修饰符: -![类访问和属性修饰符](https://my-blog-to-use.oss-cn-beijing.aliyuncs.com/2019-6/访问标志.png) +![类访问和属性修饰符](https://oss.javaguide.cn/github/javaguide/java/%E8%AE%BF%E9%97%AE%E6%A0%87%E5%BF%97.png) 我们定义了一个 `Employee` 类 @@ -136,14 +134,14 @@ public class Employee { 通过`javap -v class类名` 指令来看一下类的访问标志。 -![查看类的访问标志](https://my-blog-to-use.oss-cn-beijing.aliyuncs.com/2019-6/查看类的访问标志.png) +![查看类的访问标志](https://oss.javaguide.cn/github/javaguide/java/%E6%9F%A5%E7%9C%8B%E7%B1%BB%E7%9A%84%E8%AE%BF%E9%97%AE%E6%A0%87%E5%BF%97.png) ### 当前类(This Class)、父类(Super Class)、接口(Interfaces)索引集合 ```java u2 this_class;//当前类 u2 super_class;//父类 - u2 interfaces_count;//接口 + u2 interfaces_count;//接口数量 u2 interfaces[interfaces_count];//一个类可以实现多个接口 ``` @@ -151,12 +149,12 @@ Java 类的继承关系由类索引、父类索引和接口索引集合三项确 类索引用于确定这个类的全限定名,父类索引用于确定这个类的父类的全限定名,由于 Java 语言的单继承,所以父类索引只有一个,除了 `java.lang.Object` 之外,所有的 Java 类都有父类,因此除了 `java.lang.Object` 外,所有 Java 类的父类索引都不为 0。 -接口索引集合用来描述这个类实现了那些接口,这些被实现的接口将按 `implements` (如果这个类本身是接口的话则是`extends`) 后的接口顺序从左到右排列在接口索引集合中。 +接口索引集合用来描述这个类实现了哪些接口,这些被实现的接口将按 `implements` (如果这个类本身是接口的话则是`extends`) 后的接口顺序从左到右排列在接口索引集合中。 ### 字段表集合(Fields) ```java - u2 fields_count;//Class 文件的字段的个数 + u2 fields_count;//字段数量 field_info fields[fields_count];//一个类会可以有个字段 ``` @@ -164,7 +162,7 @@ Java 类的继承关系由类索引、父类索引和接口索引集合三项确 **field info(字段表) 的结构:** -![字段表的结构 ](https://my-blog-to-use.oss-cn-beijing.aliyuncs.com/2019-6/字段表的结构.png) +![字段表的结构 ](https://oss.javaguide.cn/github/javaguide/java/%E5%AD%97%E6%AE%B5%E8%A1%A8%E7%9A%84%E7%BB%93%E6%9E%84.png) - **access_flags:** 字段的作用域(`public` ,`private`,`protected`修饰符),是实例变量还是类变量(`static`修饰符),可否被序列化(transient 修饰符),可变性(final),可见性(volatile 修饰符,是否强制从主内存读写)。 - **name_index:** 对常量池的引用,表示的字段的名称; @@ -181,7 +179,7 @@ Java 类的继承关系由类索引、父类索引和接口索引集合三项确 ### 方法表集合(Methods) ```java - u2 methods_count;//Class 文件的方法的数量 + u2 methods_count;//方法数量 method_info methods[methods_count];//一个类可以有个多个方法 ``` @@ -191,7 +189,7 @@ Class 文件存储格式中对方法的描述与对字段的描述几乎采用 **method_info(方法表的) 结构:** -![方法表的结构](https://my-blog-to-use.oss-cn-beijing.aliyuncs.com/2019-6/方法表的结构.png) +![方法表的结构](https://oss.javaguide.cn/github/javaguide/java/%E6%96%B9%E6%B3%95%E8%A1%A8%E7%9A%84%E7%BB%93%E6%9E%84.png) **方法表的 access_flag 取值:** @@ -211,6 +209,8 @@ Class 文件存储格式中对方法的描述与对字段的描述几乎采用 ## 参考 - 《实战 Java 虚拟机》 -- Chapter 4. The class File Format - Java Virtual Machine Specification:https://docs.oracle.com/javase/specs/jvms/se8/html/jvms-4.html +- Chapter 4. The class File Format - Java Virtual Machine Specification: - 实例分析 JAVA CLASS 的文件结构: - 《Java 虚拟机原理图解》 1.2.2、Class 文件中的常量池详解(上): + + diff --git a/docs/java/jvm/class-loading-process.md b/docs/java/jvm/class-loading-process.md index 295385ef3d2..6d6bcd2ea54 100644 --- a/docs/java/jvm/class-loading-process.md +++ b/docs/java/jvm/class-loading-process.md @@ -7,7 +7,7 @@ tag: ## 类的生命周期 -类从被加载到虚拟机内存中开始到卸载出内存为止,它的整个生命周期可以简单概括为 7 个阶段::加载(Loading)、验证(Verification)、准备(Preparation)、解析(Resolution)、初始化(Initialization)、使用(Using)和卸载(Unloading)。其中,前三个阶段可以统称为连接(Linking)。 +类从被加载到虚拟机内存中开始到卸载出内存为止,它的整个生命周期可以简单概括为 7 个阶段:加载(Loading)、验证(Verification)、准备(Preparation)、解析(Resolution)、初始化(Initialization)、使用(Using)和卸载(Unloading)。其中,验证、准备和解析这三个阶段可以统称为连接(Linking)。 这 7 个阶段的顺序如下图所示: @@ -49,7 +49,7 @@ tag: 验证阶段这一步在整个类加载过程中耗费的资源还是相对较多的,但很有必要,可以有效防止恶意代码的执行。任何时候,程序安全都是第一位。 -不过,验证阶段也不是必须要执行的阶段。如果程序运行的全部代码(包括自己编写的、第三方包中的、从外部加载的、动态生成的等所有代码)都已经被反复使用和验证过,在生产环境的实施阶段就可以考虑使用 `-Xverify:none` 参数来关闭大部分的类验证措施,以缩短虚拟机类加载的时间。 +不过,验证阶段也不是必须要执行的阶段。如果程序运行的全部代码(包括自己编写的、第三方包中的、从外部加载的、动态生成的等所有代码)都已经被反复使用和验证过,在生产环境的实施阶段就可以考虑使用 `-Xverify:none` 参数来关闭大部分的类验证措施,以缩短虚拟机类加载的时间。但是需要注意的是 `-Xverify:none` 和 `-noverify` 在 JDK 13 中被标记为 deprecated ,在未来版本的 JDK 中可能会被移除。 验证阶段主要由四个检验阶段组成: @@ -73,7 +73,7 @@ tag: - `java.lang.IllegalAccessError`:当类试图访问或修改它没有权限访问的字段,或调用它没有权限访问的方法时,抛出该异常。 - `java.lang.NoSuchFieldError`:当类试图访问或修改一个指定的对象字段,而该对象不再包含该字段时,抛出该异常。 - `java.lang.NoSuchMethodError`:当类试图访问一个指定的方法,而该方法不存在时,抛出该异常。 -- ...... +- …… ### 准备 @@ -83,15 +83,15 @@ tag: 2. 从概念上讲,类变量所使用的内存都应当在 **方法区** 中进行分配。不过有一点需要注意的是:JDK 7 之前,HotSpot 使用永久代来实现方法区的时候,实现是完全符合这种逻辑概念的。 而在 JDK 7 及之后,HotSpot 已经把原本放在永久代的字符串常量池、静态变量等移动到堆中,这个时候类变量则会随着 Class 对象一起存放在 Java 堆中。相关阅读:[《深入理解 Java 虚拟机(第 3 版)》勘误#75](https://github.com/fenixsoft/jvm_book/issues/75 "《深入理解Java虚拟机(第3版)》勘误#75") 3. 这里所设置的初始值"通常情况"下是数据类型默认的零值(如 0、0L、null、false 等),比如我们定义了`public static int value=111` ,那么 value 变量在准备阶段的初始值就是 0 而不是 111(初始化阶段才会赋值)。特殊情况:比如给 value 变量加上了 final 关键字`public static final int value=111` ,那么准备阶段 value 的值就被赋值为 111。 -**基本数据类型的零值**:(图片来自《深入理解 Java 虚拟机》第 3 版 7.33 ) +**基本数据类型的零值**:(图片来自《深入理解 Java 虚拟机》第 3 版 7.3.3 ) -![基本数据类型的零值](https://my-blog-to-use.oss-cn-beijing.aliyuncs.com/2019-6/基本数据类型的零值.png) +![基本数据类型的零值](https://oss.javaguide.cn/github/javaguide/java/%E5%9F%BA%E6%9C%AC%E6%95%B0%E6%8D%AE%E7%B1%BB%E5%9E%8B%E7%9A%84%E9%9B%B6%E5%80%BC.png) ### 解析 **解析阶段是虚拟机将常量池内的符号引用替换为直接引用的过程。** 解析动作主要针对类或接口、字段、类方法、接口方法、方法类型、方法句柄和调用限定符 7 类符号引用进行。 -《深入理解 Java 虚拟机》7.34 节第三版对符号引用和直接引用的解释如下: +《深入理解 Java 虚拟机》7.3.4 节第三版对符号引用和直接引用的解释如下: ![符号引用和直接引用](https://oss.javaguide.cn/github/javaguide/java/jvm/symbol-reference-and-direct-reference.png) @@ -107,18 +107,16 @@ tag: 对于` ()` 方法的调用,虚拟机会自己确保其在多线程环境中的安全性。因为 ` ()` 方法是带锁线程安全,所以在多线程环境下进行类初始化的话可能会引起多个线程阻塞,并且这种阻塞很难被发现。 -对于初始化阶段,虚拟机严格规范了有且只有 5 种情况下,必须对类进行初始化(只有主动去使用类才会初始化类): +对于初始化阶段,虚拟机严格规范了有且只有 6 种情况下,必须对类进行初始化(只有主动去使用类才会初始化类): -1. 当遇到 `new`、 `getstatic`、`putstatic` 或 `invokestatic` 这 4 条字节码指令时,比如 `new` 一个类,读取一个静态字段(未被 final 修饰)、或调用一个类的静态方法时。 - - 当 jvm 执行 `new` 指令时会初始化类。即当程序创建一个类的实例对象。 - - 当 jvm 执行 `getstatic` 指令时会初始化类。即程序访问类的静态变量(不是静态常量,常量会被加载到运行时常量池)。 - - 当 jvm 执行 `putstatic` 指令时会初始化类。即程序给类的静态变量赋值。 - - 当 jvm 执行 `invokestatic` 指令时会初始化类。即程序调用类的静态方法。 -2. 使用 `java.lang.reflect` 包的方法对类进行反射调用时如 `Class.forname("...")`, `newInstance()` 等等。如果类没初始化,需要触发其初始化。 +1. 遇到 `new`、`getstatic`、`putstatic` 或 `invokestatic` 这 4 条字节码指令时: + - `new`: 创建一个类的实例对象。 + - `getstatic`、`putstatic`: 读取或设置一个类型的静态字段(被 `final` 修饰、已在编译期把结果放入常量池的静态字段除外)。 + - `invokestatic`: 调用类的静态方法。 +2. 使用 `java.lang.reflect` 包的方法对类进行反射调用时如 `Class.forName("...")`, `newInstance()` 等等。如果类没初始化,需要触发其初始化。 3. 初始化一个类,如果其父类还未初始化,则先触发该父类的初始化。 4. 当虚拟机启动时,用户需要定义一个要执行的主类 (包含 `main` 方法的那个类),虚拟机会先初始化这个类。 -5. `MethodHandle` 和 `VarHandle` 可以看作是轻量级的反射调用机制,而要想使用这 2 个调用, - 就必须先使用 `findStaticVarHandle` 来初始化要调用的类。 +5. `MethodHandle` 和 `VarHandle` 可以看作是轻量级的反射调用机制,而要想使用这 2 个调用,就必须先使用 `findStaticVarHandle` 来初始化要调用的类。 6. **「补充,来自[issue745](https://github.com/Snailclimb/JavaGuide/issues/745 "issue745")」** 当一个接口中定义了 JDK8 新加入的默认方法(被 default 关键字修饰的接口方法)时,如果有这个接口的实现类发生了初始化,那该接口要在其之前被初始化。 ## 类卸载 @@ -141,4 +139,6 @@ tag: - 《深入理解 Java 虚拟机》 - 《实战 Java 虚拟机》 -- Chapter 5. Loading, Linking, and Initializing - Java Virtual Machine Specification:https://docs.oracle.com/javase/specs/jvms/se8/html/jvms-5.html#jvms-5.4 +- Chapter 5. Loading, Linking, and Initializing - Java Virtual Machine Specification: + + diff --git a/docs/java/jvm/classloader.md b/docs/java/jvm/classloader.md index cf5fc3f00e7..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 应用所需的资源如文本、图像、配置文件、视频等等文件资源。本文只讨论其核心功能:加载类。 @@ -86,13 +86,13 @@ public abstract class ClassLoader { JVM 中内置了三个重要的 `ClassLoader`: -1. **`BootstrapClassLoader`(启动类加载器)**:最顶层的加载类,由 C++实现,通常表示为 null,并且没有父级,主要用来加载 JDK 内部的核心类库( `%JAVA_HOME%/lib`目录下的 `rt.jar`、`resources.jar`、`charsets.jar`等 jar 包和类)以及被 `-Xbootclasspath`参数指定的路径下的所有类。 -2. **`ExtensionClassLoader`(扩展类加载器)**:主要负责加载 `%JRE_HOME%/lib/ext` 目录下的 jar 包和类以及被 `java.ext.dirs` 系统变量所指定的路径下的所有类。 -3. **`AppClassLoader`(应用程序类加载器)**:面向我们用户的加载器,负责加载当前应用 classpath 下的所有 jar 包和类。 +1. **`BootstrapClassLoader`(启动类加载器)**:最顶层的加载类,由 C++实现,通常表示为 null,并且没有父级,主要用来加载 JDK 内部的核心类库( `%JAVA_HOME%/lib`目录下的 `rt.jar`、`resources.jar`、`charsets.jar`等 jar 包和类)以及被 `-Xbootclasspath`参数指定的路径下的所有类。 +2. **`ExtensionClassLoader`(扩展类加载器)**:主要负责加载 `%JRE_HOME%/lib/ext` 目录下的 jar 包和类以及被 `java.ext.dirs` 系统变量所指定的路径下的所有类。 +3. **`AppClassLoader`(应用程序类加载器)**:面向我们用户的加载器,负责加载当前应用 classpath 下的所有 jar 包和类。 > 🌈 拓展一下: > -> - **`rt.jar`**:rt 代表“RunTime”,`rt.jar`是 Java 基础类库,包含 Java doc 里面看到的所有的类的类文件。也就是说,我们常用内置库 `java.xxx.* `都在里面,比如`java.util.*`、`java.io.*`、`java.nio.*`、`java.lang.*`、`java.sql.*`、`java.math.*`。 +> - **`rt.jar`**:rt 代表“RunTime”,`rt.jar`是 Java 基础类库,包含 Java doc 里面看到的所有的类的类文件。也就是说,我们常用内置库 `java.xxx.*`都在里面,比如`java.util.*`、`java.io.*`、`java.nio.*`、`java.lang.*`、`java.sql.*`、`java.math.*`。 > - Java 9 引入了模块系统,并且略微更改了上述的类加载器。扩展类加载器被改名为平台类加载器(platform class loader)。Java SE 中除了少数几个关键模块,比如说 `java.base` 是由启动类加载器加载之外,其他的模块均由平台类加载器所加载。 除了这三种类加载器之外,用户还可以加入自定义的类加载器来进行拓展,以满足自己的特殊需求。就比如说,我们可以对 Java 类的字节码( `.class` 文件)进行加密,加载时再利用自定义的类加载器对其解密。 @@ -145,7 +145,7 @@ public class PrintClassLoaderTree { 输出结果(JDK 8 ): -``` +```plain |--sun.misc.Launcher$AppClassLoader@18b4aac2 |--sun.misc.Launcher$ExtClassLoader@53bd815b |--null @@ -163,7 +163,7 @@ public class PrintClassLoaderTree { `ClassLoader` 类有两个关键的方法: -- `protected Class loadClass(String name, boolean resolve)`:加载指定二进制名称的类,实现了双亲委派机制 。`name` 为类的二进制名称,`resove` 如果为 true,在加载时调用 `resolveClass(Class c)` 方法解析该类。 +- `protected Class loadClass(String name, boolean resolve)`:加载指定二进制名称的类,实现了双亲委派机制 。`name` 为类的二进制名称,`resolve` 如果为 true,在加载时调用 `resolveClass(Class c)` 方法解析该类。 - `protected Class findClass(String name)`:根据类的二进制名称来查找类,默认实现是空方法。 官方 API 文档中写到: @@ -273,6 +273,7 @@ protected Class loadClass(String name, boolean resolve) - 在类加载的时候,系统会首先判断当前类是否被加载过。已经被加载的类会直接返回,否则才会尝试加载(每个父类加载器都会走一遍这个流程)。 - 类加载器在进行类加载的时候,它首先不会自己去尝试加载这个类,而是把这个请求委派给父类加载器去完成(调用父加载器 `loadClass()`方法来加载类)。这样的话,所有的请求最终都会传送到顶层的启动类加载器 `BootstrapClassLoader` 中。 - 只有当父加载器反馈自己无法完成这个加载请求(它的搜索范围中没有找到所需的类)时,子加载器才会尝试自己去加载(调用自己的 `findClass()` 方法来加载类)。 +- 如果子类加载器也无法加载这个类,那么它会抛出一个 `ClassNotFoundException` 异常。 🌈 拓展一下: @@ -280,9 +281,50 @@ protected Class loadClass(String name, boolean resolve) ### 双亲委派模型的好处 -双亲委派模型保证了 Java 程序的稳定运行,可以避免类的重复加载(JVM 区分不同类的方式不仅仅根据类名,相同的类文件被不同的类加载器加载产生的是两个不同的类),也保证了 Java 的核心 API 不被篡改。 +双亲委派模型是 Java 类加载机制的重要组成部分,它通过委派父加载器优先加载类的方式,实现了两个关键的安全目标:避免类的重复加载和防止核心 API 被篡改。 + +JVM 区分不同类的依据是类名加上加载该类的类加载器,即使类名相同,如果由不同的类加载器加载,也会被视为不同的类。 双亲委派模型确保核心类总是由 `BootstrapClassLoader` 加载,保证了核心类的唯一性。 + +例如,当应用程序尝试加载 `java.lang.Object` 时,`AppClassLoader` 会首先将请求委派给 `ExtClassLoader`,`ExtClassLoader` 再委派给 `BootstrapClassLoader`。`BootstrapClassLoader` 会在 JRE 核心类库中找到并加载 `java.lang.Object`,从而保证应用程序使用的是 JRE 提供的标准版本。 + +有很多小伙伴就要说了:“那我绕过双亲委派模型不就可以了么?”。 + +然而,即使攻击者绕过了双亲委派模型,Java 仍然具备更底层的安全机制来保护核心类库。`ClassLoader` 的 `preDefineClass` 方法会在定义类之前进行类名校验。任何以 `"java."` 开头的类名都会触发 `SecurityException`,阻止恶意代码定义或加载伪造的核心类。 + +JDK 8 中`ClassLoader#preDefineClass` 方法源码如下: + +```java +private ProtectionDomain preDefineClass(String name, + ProtectionDomain pd) + { + // 检查类名是否合法 + if (!checkName(name)) { + throw new NoClassDefFoundError("IllegalName: " + name); + } + + // 防止在 "java.*" 包中定义类。 + // 此检查对于安全性至关重要,因为它可以防止恶意代码替换核心 Java 类。 + // JDK 9 利用平台类加载器增强了 preDefineClass 方法的安全性 + if ((name != null) && name.startsWith("java.")) { + throw new SecurityException + ("禁止的包名: " + + name.substring(0, name.lastIndexOf('.'))); + } + + // 如果未指定 ProtectionDomain,则使用默认域(defaultDomain)。 + if (pd == null) { + pd = defaultDomain; + } + + if (name != null) { + checkCerts(name, pd.getCodeSource()); + } + + return pd; + } +``` -如果没有使用双亲委派模型,而是每个类加载器加载自己的话就会出现一些问题,比如我们编写一个称为 `java.lang.Object` 类的话,那么程序运行的时候,系统就会出现两个不同的 `Object` 类。双亲委派模型可以保证加载的是 JRE 里的那个 `Object` 类,而不是你写的 `Object` 类。这是因为 `AppClassLoader` 在加载你的 `Object` 类时,会委托给 `ExtClassLoader` 去加载,而 `ExtClassLoader` 又会委托给 `BootstrapClassLoader`,`BootstrapClassLoader` 发现自己已经加载过了 `Object` 类,会直接返回,不会去加载你写的 `Object` 类。 +JDK 9 中这部分逻辑有所改变,多了平台类加载器(`getPlatformClassLoader()` 方法获取),增强了 `preDefineClass` 方法的安全性。这里就不贴源码了,感兴趣的话,可以自己去看看。 ### 打破双亲委派模型方法 @@ -294,19 +336,56 @@ protected Class loadClass(String name, boolean resolve) > 类加载器在进行类加载的时候,它首先不会自己去尝试加载这个类,而是把这个请求委派给父类加载器去完成(调用父加载器 `loadClass()`方法来加载类)。 +重写 `loadClass()`方法之后,我们就可以改变传统双亲委派模型的执行流程。例如,子类加载器可以在委派给父类加载器之前,先自己尝试加载这个类,或者在父类加载器返回之后,再尝试从其他地方加载这个类。具体的规则由我们自己实现,根据项目需求定制化。 + 我们比较熟悉的 Tomcat 服务器为了能够优先加载 Web 应用目录下的类,然后再加载其他目录下的类,就自定义了类加载器 `WebAppClassLoader` 来打破双亲委托机制。这也是 Tomcat 下 Web 应用之间的类实现隔离的具体原理。 Tomcat 的类加载器的层次结构如下: ![Tomcat 的类加载器的层次结构](https://oss.javaguide.cn/github/javaguide/java/jvm/tomcat-class-loader-parents-delegation-model.png) -感兴趣的小伙伴可以自行研究一下 Tomcat 类加载器的层次结构,这有助于我们搞懂 Tomcat 隔离 Web 应用的原理,推荐资料是[《深入拆解 Tomcat & Jetty》](http://gk.link/a/10Egr)。 +Tomcat 这四个自定义的类加载器对应的目录如下: + +- `CommonClassLoader`对应`/common/*` +- `CatalinaClassLoader`对应`/server/*` +- `SharedClassLoader`对应 `/shared/*` +- `WebAppClassloader`对应 `/webapps//WEB-INF/*` + +从图中的委派关系中可以看出: + +- `CommonClassLoader`作为 `CatalinaClassLoader` 和 `SharedClassLoader` 的父加载器。`CommonClassLoader` 能加载的类都可以被 `CatalinaClassLoader` 和 `SharedClassLoader` 使用。因此,`CommonClassLoader` 是为了实现公共类库(可以被所有 Web 应用和 Tomcat 内部组件使用的类库)的共享和隔离。 +- `CatalinaClassLoader` 和 `SharedClassLoader` 能加载的类则与对方相互隔离。`CatalinaClassLoader` 用于加载 Tomcat 自身的类,为了隔离 Tomcat 本身的类和 Web 应用的类。`SharedClassLoader` 作为 `WebAppClassLoader` 的父加载器,专门来加载 Web 应用之间共享的类比如 Spring、Mybatis。 +- 每个 Web 应用都会创建一个单独的 `WebAppClassLoader`,并在启动 Web 应用的线程里设置线程线程上下文类加载器为 `WebAppClassLoader`。各个 `WebAppClassLoader` 实例之间相互隔离,进而实现 Web 应用之间的类隔。 + +单纯依靠自定义类加载器没办法满足某些场景的要求,例如,有些情况下,高层的类加载器需要加载低层的加载器才能加载的类。 + +比如,SPI 中,SPI 的接口(如 `java.sql.Driver`)是由 Java 核心库提供的,由`BootstrapClassLoader` 加载。而 SPI 的实现(如`com.mysql.cj.jdbc.Driver`)是由第三方供应商提供的,它们是由应用程序类加载器或者自定义类加载器来加载的。默认情况下,一个类及其依赖类由同一个类加载器加载。所以,加载 SPI 的接口的类加载器(`BootstrapClassLoader`)也会用来加载 SPI 的实现。按照双亲委派模型,`BootstrapClassLoader` 是无法找到 SPI 的实现类的,因为它无法委托给子类加载器去尝试加载。 + +再比如,假设我们的项目中有 Spring 的 jar 包,由于其是 Web 应用之间共享的,因此会由 `SharedClassLoader` 加载(Web 服务器是 Tomcat)。我们项目中有一些用到了 Spring 的业务类,比如实现了 Spring 提供的接口、用到了 Spring 提供的注解。所以,加载 Spring 的类加载器(也就是 `SharedClassLoader`)也会用来加载这些业务类。但是业务类在 Web 应用目录下,不在 `SharedClassLoader` 的加载路径下,所以 `SharedClassLoader` 无法找到业务类,也就无法加载它们。 + +如何解决这个问题呢? 这个时候就需要用到 **线程上下文类加载器(`ThreadContextClassLoader`)** 了。 + +拿 Spring 这个例子来说,当 Spring 需要加载业务类的时候,它不是用自己的类加载器,而是用当前线程的上下文类加载器。还记得我上面说的吗?每个 Web 应用都会创建一个单独的 `WebAppClassLoader`,并在启动 Web 应用的线程里设置线程线程上下文类加载器为 `WebAppClassLoader`。这样就可以让高层的类加载器(`SharedClassLoader`)借助子类加载器( `WebAppClassLoader`)来加载业务类,破坏了 Java 的类加载委托机制,让应用逆向使用类加载器。 + +线程上下文类加载器的原理是将一个类加载器保存在线程私有数据里,跟线程绑定,然后在需要的时候取出来使用。这个类加载器通常是由应用程序或者容器(如 Tomcat)设置的。 + +`Java.lang.Thread` 中的`getContextClassLoader()`和 `setContextClassLoader(ClassLoader cl)`分别用来获取和设置线程的上下文类加载器。如果没有通过`setContextClassLoader(ClassLoader cl)`进行设置的话,线程将继承其父线程的上下文类加载器。 + +Spring 获取线程线程上下文类加载器的代码如下: + +```java +cl = Thread.currentThread().getContextClassLoader(); +``` + +感兴趣的小伙伴可以自行深入研究一下 Tomcat 打破双亲委派模型的原理,推荐资料:[《深入拆解 Tomcat & Jetty》](http://gk.link/a/10Egr)。 ## 推荐阅读 - 《深入拆解 Java 虚拟机》 -- 深入分析 Java ClassLoader 原理:https://blog.csdn.net/xyang81/article/details/7292380 -- Java 类加载器(ClassLoader):http://gityuan.com/2016/01/24/java-classloader/ -- Class Loaders in Java:https://www.baeldung.com/java-classloaders -- Class ClassLoader - Oracle 官方文档:https://docs.oracle.com/javase/8/docs/api/java/lang/ClassLoader.html -- 老大难的 Java ClassLoader 再不理解就老了:https://zhuanlan.zhihu.com/p/51374915 +- 深入分析 Java ClassLoader 原理: +- Java 类加载器(ClassLoader): +- Class Loaders in Java: +- Class ClassLoader - Oracle 官方文档: +- 老大难的 Java ClassLoader 再不理解就老了: + + diff --git a/docs/java/jvm/jdk-monitoring-and-troubleshooting-tools.md b/docs/java/jvm/jdk-monitoring-and-troubleshooting-tools.md index 3a773636b76..33fc2d8767b 100644 --- a/docs/java/jvm/jdk-monitoring-and-troubleshooting-tools.md +++ b/docs/java/jvm/jdk-monitoring-and-troubleshooting-tools.md @@ -13,7 +13,7 @@ tag: - **`jstat`**(JVM Statistics Monitoring Tool): 用于收集 HotSpot 虚拟机各方面的运行数据; - **`jinfo`** (Configuration Info for Java) : Configuration Info for Java,显示虚拟机配置信息; - **`jmap`** (Memory Map for Java) : 生成堆转储快照; -- **`jhat`** (JVM Heap Dump Browser) : 用于分析 heapdump 文件,它会建立一个 HTTP/HTML 服务器,让用户可以在浏览器上查看分析结果; +- **`jhat`** (JVM Heap Dump Browser) : 用于分析 heapdump 文件,它会建立一个 HTTP/HTML 服务器,让用户可以在浏览器上查看分析结果。JDK9 移除了 jhat; - **`jstack`** (Stack Trace for Java) : 生成虚拟机当前时刻的线程快照,线程快照就是当前虚拟机内每一条线程正在执行的方法堆栈的集合。 ### `jps`:查看所有 Java 进程 @@ -133,6 +133,8 @@ Server is ready. 访问 +注意⚠️:JDK9 移除了 jhat([JEP 241: Remove the jhat Tool](https://openjdk.org/jeps/241)),你可以使用其替代品 Eclipse Memory Analyzer Tool (MAT) 和 VisualVM,这也是官方所推荐的。 + ### **`jstack`** :生成虚拟机当前时刻的线程快照 `jstack`(Stack Trace for Java)命令用于生成虚拟机当前时刻的线程快照。线程快照就是当前虚拟机内每一条线程正在执行的方法堆栈的集合. @@ -182,14 +184,14 @@ public class DeadLockDemo { Output -``` +```plain Thread[线程 1,5,main]get resource1 Thread[线程 2,5,main]get resource2 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 休眠结束了都开始企图请求获取对方的资源,然后这两个线程就会陷入互相等待的状态,这也就产生了死锁。 **通过 `jstack` 命令分析:** @@ -243,7 +245,7 @@ Found 1 deadlock. ### JConsole:Java 监视与管理控制台 -JConsole 是基于 JMX 的可视化监视、管理工具。可以很方便的监视本地及远程服务器的 java 进程的内存使用情况。你可以在控制台输出`console`命令启动或者在 JDK 目录下的 bin 目录找到`jconsole.exe`然后双击启动。 +JConsole 是基于 JMX 的可视化监视、管理工具。可以很方便的监视本地及远程服务器的 java 进程的内存使用情况。你可以在控制台输入`jconsole`命令启动或者在 JDK 目录下的 bin 目录找到`jconsole.exe`然后双击启动。 #### 连接 Jconsole @@ -260,7 +262,7 @@ JConsole 是基于 JMX 的可视化监视、管理工具。可以很方便的监 在使用 JConsole 连接时,远程进程地址如下: -``` +```plain 外网访问 ip 地址:60001 ``` @@ -297,14 +299,27 @@ VisualVM 提供在 Java 虚拟机 (Java Virtual Machine, JVM) 上运行的 Java VisualVM 基于 NetBeans 平台开发,因此他一开始就具备了插件扩展功能的特性,通过插件扩展支持,VisualVM 可以做到: -- **显示虚拟机进程以及进程的配置、环境信息(jps、jinfo)。** -- **监视应用程序的 CPU、GC、堆、方法区以及线程的信息(jstat、jstack)。** -- **dump 以及分析堆转储快照(jmap、jhat)。** -- **方法级的程序运行性能分析,找到被调用最多、运行时间最长的方法。** -- **离线程序快照:收集程序的运行时配置、线程 dump、内存 dump 等信息建立一个快照,可以将快照发送开发者处进行 Bug 反馈。** -- **其他 plugins 的无限的可能性......** +- 显示虚拟机进程以及进程的配置、环境信息(jps、jinfo)。 +- 监视应用程序的 CPU、GC、堆、方法区以及线程的信息(jstat、jstack)。 +- dump 以及分析堆转储快照(jmap、jhat)。 +- 方法级的程序运行性能分析,找到被调用最多、运行时间最长的方法。 +- 离线程序快照:收集程序的运行时配置、线程 dump、内存 dump 等信息建立一个快照,可以将快照发送开发者处进行 Bug 反馈。 +- 其他 plugins 的无限的可能性…… 这里就不具体介绍 VisualVM 的使用,如果想了解的话可以看: - - + +### MAT:内存分析器工具 + +MAT(Memory Analyzer Tool)是一款快速便捷且功能强大丰富的 JVM 堆内存离线分析工具。其通过展现 JVM 异常时所记录的运行时堆转储快照(Heap dump)状态(正常运行时也可以做堆转储分析),帮助定位内存泄漏问题或优化大内存消耗逻辑。 + +在遇到 OOM 和 GC 问题的时候,我一般会首选使用 MAT 分析 dump 文件在,这也是该工具应用最多的一个场景。 + +关于 MAT 的详细介绍推荐下面这两篇文章,写的很不错: + +- [JVM 内存分析工具 MAT 的深度讲解与实践—入门篇](https://juejin.cn/post/6908665391136899079) +- [JVM 内存分析工具 MAT 的深度讲解与实践—进阶篇](https://juejin.cn/post/6911624328472133646) + + diff --git a/docs/java/jvm/jvm-garbage-collection.md b/docs/java/jvm/jvm-garbage-collection.md index c161229a8f9..970933ee5ce 100644 --- a/docs/java/jvm/jvm-garbage-collection.md +++ b/docs/java/jvm/jvm-garbage-collection.md @@ -51,16 +51,16 @@ Java 堆是垃圾收集器管理的主要区域,因此也被称作 **GC 堆( ### 对象优先在 Eden 区分配 -大多数情况下,对象在新生代中 Eden 区分配。当 Eden 区没有足够空间进行分配时,虚拟机将发起一次 Minor GC。下面我们来进行实际测试以下。 +大多数情况下,对象在新生代中 Eden 区分配。当 Eden 区没有足够空间进行分配时,虚拟机将发起一次 Minor GC。下面我们来进行实际测试一下。 测试代码: ```java public class GCTest { - public static void main(String[] args) { - byte[] allocation1, allocation2; - allocation1 = new byte[30900*1024]; - } + public static void main(String[] args) { + byte[] allocation1, allocation2; + allocation1 = new byte[30900*1024]; + } } ``` @@ -91,14 +91,14 @@ allocation2 = new byte[900*1024]; ```java public class GCTest { - public static void main(String[] args) { - byte[] allocation1, allocation2,allocation3,allocation4,allocation5; - allocation1 = new byte[32000*1024]; - allocation2 = new byte[1000*1024]; - allocation3 = new byte[1000*1024]; - allocation4 = new byte[1000*1024]; - allocation5 = new byte[1000*1024]; - } + public static void main(String[] args) { + byte[] allocation1, allocation2,allocation3,allocation4,allocation5; + allocation1 = new byte[32000*1024]; + allocation2 = new byte[1000*1024]; + allocation3 = new byte[1000*1024]; + allocation4 = new byte[1000*1024]; + allocation5 = new byte[1000*1024]; + } } ``` @@ -107,7 +107,10 @@ public class GCTest { 大对象就是需要大量连续内存空间的对象(比如:字符串、数组)。 -大对象直接进入老年代主要是为了避免为大对象分配内存时由于分配担保机制带来的复制而降低效率。 +大对象直接进入老年代的行为是由虚拟机动态决定的,它与具体使用的垃圾回收器和相关参数有关。大对象直接进入老年代是一种优化策略,旨在避免将大对象放入新生代,从而减少新生代的垃圾回收频率和成本。 + +- G1 垃圾回收器会根据 `-XX:G1HeapRegionSize` 参数设置的堆区域大小和 `-XX:G1MixedGCLiveThresholdPercent` 参数设置的阈值,来决定哪些对象会直接进入老年代。 +- Parallel Scavenge 垃圾回收器中,默认情况下,并没有一个固定的阈值(`XX:ThresholdTolerance`是动态调整的)来决定何时直接在老年代分配大对象。而是由虚拟机根据当前的堆内存情况和历史数据动态决定。 ### 长期存活的对象将进入老年代 @@ -119,7 +122,7 @@ public class GCTest { > 修正([issue552](https://github.com/Snailclimb/JavaGuide/issues/552)):“Hotspot 遍历所有对象时,按照年龄从小到大对其所占用的大小进行累积,当累积的某个年龄大小超过了 survivor 区的 50% 时(默认值是 50%,可以通过 `-XX:TargetSurvivorRatio=percent` 来设置,参见 [issue1199](https://github.com/Snailclimb/JavaGuide/issues/1199) ),取这个年龄和 MaxTenuringThreshold 中更小的一个值,作为新的晋升年龄阈值”。 > -> jdk8 官方文档引用:https://docs.oracle.com/javase/8/docs/technotes/tools/unix/java.html 。 +> jdk8 官方文档引用:。 > > ![](https://oss.javaguide.cn/java-guide-blog/image-20210523201742303.png) > @@ -224,11 +227,12 @@ public class ReferenceCountingGc { **哪些对象可以作为 GC Roots 呢?** -- 虚拟机栈(栈帧中的本地变量表)中引用的对象 +- 虚拟机栈(栈帧中的局部变量表)中引用的对象 - 本地方法栈(Native 方法)中引用的对象 - 方法区中类静态属性引用的对象 - 方法区中常量引用的对象 - 所有被同步锁持有的对象 +- JNI(Java Native Interface)引用的对象 **对象可以被回收,就代表一定会被回收吗?** @@ -249,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); +``` **虚引用主要用来跟踪对象被垃圾回收的活动**。 @@ -318,7 +351,7 @@ JDK1.2 以后,Java 对引用的概念进行了扩充,将引用分为强引 ![标记-清除算法](https://oss.javaguide.cn/github/javaguide/java/jvm/mark-and-sweep-garbage-collection-algorithm.png) -关于具体是标记可回收对象还是不可回收对象,众说纷纭,两种说法其实都没问题,我个人更倾向于是前者。 +关于具体是标记可回收对象(不可达对象)还是不可回收对象(可达对象),众说纷纭,两种说法其实都没问题,我个人更倾向于是后者。 如果按照前者的理解,整个标记-清除过程大致是这样的: @@ -349,7 +382,7 @@ JDK1.2 以后,Java 对引用的概念进行了扩充,将引用分为强引 当前虚拟机的垃圾收集都采用分代收集算法,这种算法没有什么新的思想,只是根据对象存活周期的不同将内存分为几块。一般将 Java 堆分为新生代和老年代,这样我们就可以根据各个年代的特点选择合适的垃圾收集算法。 -比如在新生代中,每次收集都会有大量对象死去,所以可以选择”标记-复制“算法,只需要付出少量对象的复制成本就可以完成每次垃圾收集。而老年代的对象存活几率是比较高的,而且没有额外的空间对它进行分配担保,所以我们必须选择“标记-清除”或“标记-整理”算法进行垃圾收集。 +比如在新生代中,每次收集都会有大量对象死去,所以可以选择“复制”算法,只需要付出少量对象的复制成本就可以完成每次垃圾收集。而老年代的对象存活几率是比较高的,而且没有额外的空间对它进行分配担保,所以我们必须选择“标记-清除”或“标记-整理”算法进行垃圾收集。 **延伸面试问题:** HotSpot 为什么要分为新生代和老年代? @@ -363,8 +396,8 @@ JDK1.2 以后,Java 对引用的概念进行了扩充,将引用分为强引 JDK 默认垃圾收集器(使用 `java -XX:+PrintCommandLineFlags -version` 命令查看): -- JDK 8:Parallel Scavenge(新生代)+ Parallel Old(老年代) -- JDK 9 ~ JDK20: G1 +- JDK 8: Parallel Scavenge(新生代)+ Parallel Old(老年代) +- JDK 9 ~ JDK22: G1 ### Serial 收集器 @@ -447,7 +480,7 @@ JDK1.8 默认使用的是 Parallel Scavenge + Parallel Old,如果指定了-XX: 从名字中的**Mark Sweep**这两个词可以看出,CMS 收集器是一种 **“标记-清除”算法**实现的,它的运作过程相比于前面几种垃圾收集器来说更加复杂一些。整个过程分为四个步骤: -- **初始标记:** 暂停所有的其他线程,并记录下直接与 root 相连的对象,速度很快 ; +- **初始标记:** 短暂停顿,标记直接与 root 相连的对象(根对象); - **并发标记:** 同时开启 GC 和用户线程,用一个闭包结构去记录可达对象。但在这个阶段结束,这个闭包结构并不能保证包含当前所有的可达对象。因为用户线程可能会不断的更新引用域,所以 GC 线程无法保证可达性分析的实时性。所以这个算法里会跟踪记录这些发生引用更新的地方。 - **重新标记:** 重新标记阶段就是为了修正并发标记期间因为用户程序继续运行而导致标记产生变动的那一部分对象的标记记录,这个阶段的停顿时间一般会比初始标记阶段的时间稍长,远远比并发标记阶段时间短 - **并发清除:** 开启用户线程,同时 GC 线程开始对未标记的区域做清扫。 @@ -460,9 +493,11 @@ JDK1.8 默认使用的是 Parallel Scavenge + Parallel Old,如果指定了-XX: - **无法处理浮动垃圾;** - **它使用的回收算法-“标记-清除”算法会导致收集结束时会有大量空间碎片产生。** +**CMS 垃圾回收器在 Java 9 中已经被标记为过时(deprecated),并在 Java 14 中被移除。** + ### G1 收集器 -**G1 (Garbage-First) 是一款面向服务器的垃圾收集器,主要针对配备多颗处理器及大容量内存的机器. 以极高概率满足 GC 停顿时间要求的同时,还具备高吞吐量性能特征.** +**G1 (Garbage-First) 是一款面向服务器的垃圾收集器,主要针对配备多颗处理器及大容量内存的机器. 以极高概率满足 GC 停顿时间要求的同时,还具备高吞吐量性能特征。** 被视为 JDK1.7 中 HotSpot 虚拟机的一个重要进化特征。它具备以下特点: @@ -473,10 +508,10 @@ JDK1.8 默认使用的是 Parallel Scavenge + Parallel Old,如果指定了-XX: G1 收集器的运作大致分为以下几个步骤: -- **初始标记** -- **并发标记** -- **最终标记** -- **筛选回收** +- **初始标记**: 短暂停顿(Stop-The-World,STW),标记从 GC Roots 可直接引用的对象,即标记所有直接可达的活跃对象 +- **并发标记**:与应用并发运行,标记所有可达对象。 这一阶段可能持续较长时间,取决于堆的大小和对象的数量。 +- **最终标记**: 短暂停顿(STW),处理并发标记阶段结束后残留的少量未处理的引用变更。 +- **筛选回收**:根据标记结果,选择回收价值高的区域,复制存活对象到新区域,回收旧区域内存。这一阶段包含一个或多个停顿(STW),具体取决于回收的复杂度。 ![G1 收集器](https://oss.javaguide.cn/github/javaguide/java/jvm/g1-garbage-collector.png) @@ -488,20 +523,33 @@ G1 收集器的运作大致分为以下几个步骤: 与 CMS 中的 ParNew 和 G1 类似,ZGC 也采用标记-复制算法,不过 ZGC 对该算法做了重大改进。 -在 ZGC 中出现 Stop The World 的情况会更少! +ZGC 可以将暂停时间控制在几毫秒以内,且暂停时间不受堆内存大小的影响,出现 Stop The World 的情况会更少,但代价是牺牲了一些吞吐量。ZGC 最大支持 16TB 的堆内存。 + +ZGC 在 Java11 中引入,处于试验阶段。经过多个版本的迭代,不断的完善和修复问题,ZGC 在 Java15 已经可以正式使用了。 -Java11 的时候 ,ZGC 还在试验阶段。经过多个版本的迭代,不断的完善和修复问题,ZGC 在 Java 15 已经可以正式使用了! +不过,默认的垃圾回收器依然是 G1。你可以通过下面的参数启用 ZGC: -不过,默认的垃圾回收器依然是 G1。你可以通过下面的参数启动 ZGC: +```bash +java -XX:+UseZGC className +``` + +在 Java21 中,引入了分代 ZGC,暂停时间可以缩短到 1 毫秒以内。 + +你可以通过下面的参数启用分代 ZGC: ```bash -$ java -XX:+UseZGC className +java -XX:+UseZGC -XX:+ZGenerational className ``` -关于 ZGC 收集器的详细介绍推荐阅读美团技术团队的 [新一代垃圾回收器 ZGC 的探索与实践](https://tech.meituan.com/2020/08/06/new-zgc-practice-in-meituan.html) 这篇文章。 +关于 ZGC 收集器的详细介绍推荐看看这几篇文章: + +- [从历代 GC 算法角度剖析 ZGC - 京东技术](https://mp.weixin.qq.com/s/ExkB40cq1_Z0ooDzXn7CVw) +- [新一代垃圾回收器 ZGC 的探索与实践 - 美团技术团队](https://tech.meituan.com/2020/08/06/new-zgc-practice-in-meituan.html) +- [极致八股文之 JVM 垃圾回收器 G1&ZGC 详解 - 阿里云开发者](https://mp.weixin.qq.com/s/Ywj3XMws0IIK-kiUllN87Q) ## 参考 - 《深入理解 Java 虚拟机:JVM 高级特性与最佳实践(第二版》 -- https://my.oschina.net/hosee/blog/644618 -- +- The Java® Virtual Machine Specification - Java SE 8 Edition: + + diff --git a/docs/java/jvm/jvm-in-action.md b/docs/java/jvm/jvm-in-action.md index 7d38c7d65b2..99b6fc6041d 100644 --- a/docs/java/jvm/jvm-in-action.md +++ b/docs/java/jvm/jvm-in-action.md @@ -51,3 +51,9 @@ JVM 线上问题排查和性能调优也是面试常问的一个问题,尤其 [Java 中 9 种常见的 CMS GC 问题分析与解决 - 美团技术团 - 2020](https://tech.meituan.com/2020/11/12/java-9-cms-gc.html) 这篇文章共 2w+ 字,详细介绍了 GC 基础,总结了 CMS GC 的一些常见问题分析与解决办法。 + +[给祖传系统做了点 GC 调优,暂停时间降低了 90% - 京东云技术团队 - 2023](https://juejin.cn/post/7311623433817571365) + +这篇文章提到了一个在规则引擎系统中遇到的 GC(垃圾回收)问题,主要表现为系统在启动后发生了一次较长的 Young GC(年轻代垃圾回收)导致性能下降。经过分析,问题的核心在于动态对象年龄判定机制,它导致了过早的对象晋升,引起了长时间的垃圾回收。 + + diff --git a/docs/java/jvm/jvm-intro.md b/docs/java/jvm/jvm-intro.md index 3fad8066cad..2fdb9b3e055 100644 --- a/docs/java/jvm/jvm-intro.md +++ b/docs/java/jvm/jvm-intro.md @@ -5,7 +5,7 @@ tag: - JVM --- -> 来自[说出你的愿望吧丷](https://juejin.im/user/5c2400afe51d45451758aa96)投稿,原文地址:https://juejin.im/post/5e1505d0f265da5d5d744050 。 +> 来自[说出你的愿望吧丷](https://juejin.im/user/5c2400afe51d45451758aa96)投稿,原文地址:。 ## 前言 @@ -17,7 +17,7 @@ JVM 是 Java Virtual Machine 的缩写,它是一个虚构出来的计算机, 好,其实抛开这么专业的句子不说,就知道 JVM 其实就类似于一台小电脑运行在 windows 或者 linux 这些操作系统环境下即可。它直接和操作系统进行交互,与硬件不直接交互,而操作系统可以帮我们完成和硬件进行交互的工作。 -![](https://my-blog-to-use.oss-cn-beijing.aliyuncs.com/2019-11/d947f91e44c44c6c80222b49c2dee859-new-image19a36451-d673-486e-9c8e-3c7d8ab66929.png) +![](https://static001.geekbang.org/infoq/da/da0380a04d9c04facd2add5f6dba06fa.png) ### 1.1 Java 文件是如何被运行的 @@ -29,7 +29,7 @@ JVM 是 Java Virtual Machine 的缩写,它是一个虚构出来的计算机, 如果 **JVM** 想要执行这个 **.class** 文件,我们需要将其装进一个 **类加载器** 中,它就像一个搬运工一样,会把所有的 **.class** 文件全部搬进 JVM 里面来。 -![](https://my-blog-to-use.oss-cn-beijing.aliyuncs.com/2019-11/81f1813f371c40ffa1c1f6d78bc49ed9-new-image28314ec8-066f-451e-8373-4517917d6bf7.png) +![](https://static001.geekbang.org/infoq/2f/2f012fde94376f43a25dbe1dd07e0dd8.png) #### ② 方法区 @@ -51,7 +51,7 @@ JVM 是 Java Virtual Machine 的缩写,它是一个虚构出来的计算机, 主要就是完成一个加载工作,类似于一个指针一样的,指向下一行我们需要执行的代码。和栈一样,都是 **线程独享** 的,就是说每一个线程都会有自己对应的一块区域而不会存在并发和多线程的问题。 -![](https://my-blog-to-use.oss-cn-beijing.aliyuncs.com/2019-11/897863ee5ecb4d92b9119d065f468262-new-imagef7287f0b-c9f0-4f22-9eb4-6968bbaa5a82.png) +![](https://static001.geekbang.org/infoq/c6/c602f57ea9297f50bbc265f1821d6263.png) #### 小总结 @@ -63,20 +63,20 @@ JVM 是 Java Virtual Machine 的缩写,它是一个虚构出来的计算机, 一个简单的学生类 -![](https://my-blog-to-use.oss-cn-beijing.aliyuncs.com/2019-11/29046a721c2548e0a0680ec5baf4ea95-new-imageb0b42e5e-8e25-409e-b7b9-6586a39a0b8d.png) +![](https://static001.geekbang.org/infoq/12/12f0b239db65b8a95f0ce90e9a580e4d.png) 一个 main 方法 -![](https://my-blog-to-use.oss-cn-beijing.aliyuncs.com/2019-11/a3d34d33eab74f6f8743ecf62807445c-new-image08506a9e-5101-4f30-b0bc-3abbcb8f1894.png) +![](https://static001.geekbang.org/infoq/0c/0c6d94ab88a9f2b923f5fea3f95bc2eb.png) 执行 main 方法的步骤如下: -1. 编译好 App.java 后得到 App.class 后,执行 App.class,系统会启动一个 JVM 进程,从 classpath 路径中找到一个名为 App.class 的二进制文件,将 App 的类信息加载到运行时数据区的方法区内,这个过程叫做 App 类的加载 -2. JVM 找到 App 的主程序入口,执行 main 方法 -3. 这个 main 中的第一条语句为 Student student = new Student("tellUrDream") ,就是让 JVM 创建一个 Student 对象,但是这个时候方法区中是没有 Student 类的信息的,所以 JVM 马上加载 Student 类,把 Student 类的信息放到方法区中 -4. 加载完 Student 类后,JVM 在堆中为一个新的 Student 实例分配内存,然后调用构造函数初始化 Student 实例,这个 Student 实例持有 **指向方法区中的 Student 类的类型信息** 的引用 -5. 执行 student.sayName();时,JVM 根据 student 的引用找到 student 对象,然后根据 student 对象持有的引用定位到方法区中 student 类的类型信息的方法表,获得 sayName() 的字节码地址。 -6. 执行 sayName() +1. 编译好 App.java 后得到 App.class 后,执行 App.class,系统会启动一个 JVM 进程,从 classpath 路径中找到一个名为 App.class 的二进制文件,将 App 的类信息加载到运行时数据区的方法区内,这个过程叫做 App 类的加载 +2. JVM 找到 App 的主程序入口,执行 main 方法 +3. 这个 main 中的第一条语句为 Student student = new Student("tellUrDream") ,就是让 JVM 创建一个 Student 对象,但是这个时候方法区中是没有 Student 类的信息的,所以 JVM 马上加载 Student 类,把 Student 类的信息放到方法区中 +4. 加载完 Student 类后,JVM 在堆中为一个新的 Student 实例分配内存,然后调用构造函数初始化 Student 实例,这个 Student 实例持有 **指向方法区中的 Student 类的类型信息** 的引用 +5. 执行 student.sayName();时,JVM 根据 student 的引用找到 student 对象,然后根据 student 对象持有的引用定位到方法区中 student 类的类型信息的方法表,获得 sayName() 的字节码地址。 +6. 执行 sayName() 其实也不用管太多,只需要知道对象实例初始化时会去方法区中找类信息,完成后再到栈那里去运行方法。找方法就在方法表中找。 @@ -90,15 +90,15 @@ JVM 是 Java Virtual Machine 的缩写,它是一个虚构出来的计算机, #### 2.1.1 加载 -1. 将 class 文件加载到内存 -2. 将静态数据结构转化成方法区中运行时的数据结构 -3. 在堆中生成一个代表这个类的 java.lang.Class 对象作为数据访问的入口 +1. 将 class 文件加载到内存 +2. 将静态数据结构转化成方法区中运行时的数据结构 +3. 在堆中生成一个代表这个类的 java.lang.Class 对象作为数据访问的入口 #### 2.1.2 链接 -1. 验证:确保加载的类符合 JVM 规范和安全,保证被校验类的方法在运行时不会做出危害虚拟机的事件,其实就是一个安全检查 -2. 准备:为 static 变量在方法区中分配内存空间,设置变量的初始值,例如 static int a = 3 (注意:准备阶段只设置类中的静态变量(方法区中),不包括实例变量(堆内存中),实例变量是对象初始化时赋值的) -3. 解析:虚拟机将常量池内的符号引用替换为直接引用的过程(符号引用比如我现在 import java.util.ArrayList 这就算符号引用,直接引用就是指针或者对象地址,注意引用对象一定是在内存进行) +1. 验证:确保加载的类符合 JVM 规范和安全,保证被校验类的方法在运行时不会做出危害虚拟机的事件,其实就是一个安全检查 +2. 准备:为 static 变量在方法区中分配内存空间,设置变量的初始值,例如 static int a = 3 (注意:准备阶段只设置类中的静态变量(方法区中),不包括实例变量(堆内存中),实例变量是对象初始化时赋值的) +3. 解析:虚拟机将常量池内的符号引用替换为直接引用的过程(符号引用比如我现在 import java.util.ArrayList 这就算符号引用,直接引用就是指针或者对象地址,注意引用对象一定是在内存进行) #### 2.1.3 初始化 @@ -114,10 +114,10 @@ GC 将无用对象从内存中卸载 加载一个 Class 类的顺序也是有优先级的,类加载器从最底层开始往上的顺序是这样的 -1. BootStrap ClassLoader:rt.jar -2. Extension ClassLoader: 加载扩展的 jar 包 -3. App ClassLoader:指定的 classpath 下面的 jar 包 -4. Custom ClassLoader:自定义的类加载器 +1. BootStrap ClassLoader:rt.jar +2. Extension ClassLoader: 加载扩展的 jar 包 +3. App ClassLoader:指定的 classpath 下面的 jar 包 +4. Custom ClassLoader:自定义的类加载器 ### 2.3 双亲委派机制 @@ -190,7 +190,7 @@ public class Person{ 局部变量表用于存放方法参数和方法内部所定义的局部变量。它的容量是以 Slot 为最小单位,一个 slot 可以存放 32 位以内的数据类型。 -虚拟机通过索引定位的方式使用局部变量表,范围为[0,局部变量表的 slot 的数量]。方法中的参数就会按一定顺序排列在这个局部变量表中,至于怎么排的我们可以先不关心。而为了节省栈帧空间,这些 slot 是可以复用的,当方法执行位置超过了某个变量,那么这个变量的 slot 可以被其它变量复用。当然如果需要复用,那我们的垃圾回收自然就不会去动这些内存。 +虚拟机通过索引定位的方式使用局部变量表,范围为 `[0,局部变量表的 slot 的数量]`。方法中的参数就会按一定顺序排列在这个局部变量表中,至于怎么排的我们可以先不关心。而为了节省栈帧空间,这些 slot 是可以复用的,当方法执行位置超过了某个变量,那么这个变量的 slot 可以被其它变量复用。当然如果需要复用,那我们的垃圾回收自然就不会去动这些内存。 #### 3.3.6 虚拟机堆的概念 @@ -198,8 +198,10 @@ JVM 内存会划分为堆内存和非堆内存,堆内存中也会划分为** 堆内存中存放的是对象,垃圾收集就是收集这些对象然后交给 GC 算法进行回收。非堆内存其实我们已经说过了,就是方法区。在 1.8 中已经移除永久代,替代品是一个元空间(MetaSpace),最大区别是 metaSpace 是不存在于 JVM 中的,它使用的是本地内存。并有两个参数 - MetaspaceSize:初始化元空间大小,控制发生GC - MaxMetaspaceSize:限制元空间大小上限,防止占用过多物理内存。 +```plain +MetaspaceSize:初始化元空间大小,控制发生GC +MaxMetaspaceSize:限制元空间大小上限,防止占用过多物理内存。 +``` 移除的原因可以大致了解一下:融合 HotSpot JVM 和 JRockit VM 而做出的改变,因为 JRockit 是没有永久代的,不过这也间接性地解决了永久代的 OOM 问题。 @@ -219,13 +221,13 @@ JVM 内存会划分为堆内存和非堆内存,堆内存中也会划分为** 而且当老年区执行了 full gc 之后仍然无法进行对象保存的操作,就会产生 OOM,这时候就是虚拟机中的堆内存不足,原因可能会是堆内存设置的大小过小,这个可以通过参数-Xms、-Xmx 来调整。也可能是代码中创建的对象大且多,而且它们一直在被引用从而长时间垃圾收集无法收集它们。 -![](https://my-blog-to-use.oss-cn-beijing.aliyuncs.com/2019-11/c02ecba3c33f43429a765987b928e423-new-image93b46f3d-33f9-46f9-a825-ec7129b004f6.png) +![](https://static001.geekbang.org/infoq/39/398255141fde8ba208f6c99f4edaa9fe.png) 补充说明:关于-XX:TargetSurvivorRatio 参数的问题。其实也不一定是要满足-XX:MaxTenuringThreshold 才移动到老年代。可以举个例子:如对象年龄 5 的占 30%,年龄 6 的占 36%,年龄 7 的占 34%,加入某个年龄段(如例子中的年龄 6)后,总占用超过 Survivor 空间\*TargetSurvivorRatio 的时候,从该年龄段开始及大于的年龄对象就要进入老年代(即例子中的年龄 6 对象,就是年龄 6 和年龄 7 晋升到老年代),这时候无需等到 MaxTenuringThreshold 中要求的 15 #### 3.3.8 如何判断一个对象需要被干掉 -![](https://my-blog-to-use.oss-cn-beijing.aliyuncs.com/2019-11/1c1d85b5fb8b47239af2a5c0436eb2d7-new-image0cd10827-2f96-433c-9b16-93d4fe491d88.png) +![](https://static001.geekbang.org/infoq/1b/1ba7f3cff6e07c6e9c6765cc4ef74997.png) 图中程序计数器、虚拟机栈、本地方法栈,3 个区域随着线程的生存而生存的。内存分配和回收都是确定的。随着线程的结束内存自然就被回收了,因此不需要考虑垃圾回收的问题。而 Java 堆和方法区则不一样,各线程共享,内存的分配和回收都是动态的。因此垃圾收集器所关注的都是堆和方法这部分内存。 @@ -237,11 +239,11 @@ JVM 内存会划分为堆内存和非堆内存,堆内存中也会划分为** (了解一下即可)在 Java 语言汇总能作为 GC Roots 的对象分为以下几种: -1. 虚拟机栈(栈帧中的本地方法表)中引用的对象(局部变量) -2. 方法区中静态变量所引用的对象(静态变量) -3. 方法区中常量引用的对象 -4. 本地方法栈(即 native 修饰的方法)中 JNI 引用的对象(JNI 是 Java 虚拟机调用对应的 C 函数的方式,通过 JNI 函数也可以创建新的 Java 对象。且 JNI 对于对象的局部引用或者全局引用都会把它们指向的对象都标记为不可回收) -5. 已启动的且未终止的 Java 线程 +1. 虚拟机栈(栈帧中的本地方法表)中引用的对象(局部变量) +2. 方法区中静态变量所引用的对象(静态变量) +3. 方法区中常量引用的对象 +4. 本地方法栈(即 native 修饰的方法)中 JNI 引用的对象(JNI 是 Java 虚拟机调用对应的 C 函数的方式,通过 JNI 函数也可以创建新的 Java 对象。且 JNI 对于对象的局部引用或者全局引用都会把它们指向的对象都标记为不可回收) +5. 已启动的且未终止的 Java 线程 这种方法的优点是能够解决循环引用的问题,可它的实现需要耗费大量资源和时间,也需要 GC(它的分析过程引用关系不能发生变化,所以需要停止所有进程) @@ -253,58 +255,24 @@ finalize()是 Object 类的一个方法、一个对象的 finalize()方法只会 补充一句:并不提倡在程序中调用 finalize()来进行自救。建议忘掉 Java 程序中该方法的存在。因为它执行的时间不确定,甚至是否被执行也不确定(Java 程序的不正常退出),而且运行代价高昂,无法保证各个对象的调用顺序(甚至有不同线程中调用)。在 Java9 中已经被标记为 **deprecated** ,且 `java.lang.ref.Cleaner`(也就是强、软、弱、幻象引用的那一套)中已经逐步替换掉它,会比 `finalize` 来的更加的轻量及可靠。 -![](https://my-blog-to-use.oss-cn-beijing.aliyuncs.com/2019-11/c807dab33f8b42329c1910d609e7ed21-new-image565aeab2-6d3e-4c2c-80f6-7a7b0f629fda.png) +![](https://static001.geekbang.org/infoq/8d/8d7f0381c7d857c7ceb8ae5a5fef0f4a.png) 判断一个对象的死亡至少需要两次标记 -1. 如果对象进行可达性分析之后没发现与 GC Roots 相连的引用链,那它将会第一次标记并且进行一次筛选。判断的条件是决定这个对象是否有必要执行 finalize()方法。如果对象有必要执行 finalize()方法,则被放入 F-Queue 队列中。 -2. GC 对 F-Queue 队列中的对象进行二次标记。如果对象在 finalize()方法中重新与引用链上的任何一个对象建立了关联,那么二次标记时则会将它移出“即将回收”集合。如果此时对象还没成功逃脱,那么只能被回收了。 +1. 如果对象进行可达性分析之后没发现与 GC Roots 相连的引用链,那它将会第一次标记并且进行一次筛选。判断的条件是决定这个对象是否有必要执行 finalize()方法。如果对象有必要执行 finalize()方法,则被放入 F-Queue 队列中。 +2. GC 对 F-Queue 队列中的对象进行二次标记。如果对象在 finalize()方法中重新与引用链上的任何一个对象建立了关联,那么二次标记时则会将它移出“即将回收”集合。如果此时对象还没成功逃脱,那么只能被回收了。 如果确定对象已经死亡,我们又该如何回收这些垃圾呢 ### 3.4 垃圾回收算法 -不会非常详细的展开,常用的有标记清除,复制,标记整理和分代收集算法 - -#### 3.4.1 标记清除算法 - -标记清除算法就是分为“标记”和“清除”两个阶段。标记出所有需要回收的对象,标记结束后统一回收。这个套路很简单,也存在不足,后续的算法都是根据这个基础来加以改进的。 - -其实它就是把已死亡的对象标记为空闲内存,然后记录在一个空闲列表中,当我们需要 new 一个对象时,内存管理模块会从空闲列表中寻找空闲的内存来分给新的对象。 - -不足的方面就是标记和清除的效率比较低下。且这种做法会让内存中的碎片非常多。这个导致了如果我们需要使用到较大的内存块时,无法分配到足够的连续内存。比如下图 - -![](https://my-blog-to-use.oss-cn-beijing.aliyuncs.com/2019-11/01605d96d85f4daab9bfa5e7000f0d31-new-image78e03b85-fbef-4df9-b41e-2b63d78d119f.png) - -此时可使用的内存块都是零零散散的,导致了刚刚提到的大内存对象问题 - -#### 3.4.2 复制算法 - -为了解决效率问题,复制算法就出现了。它将可用内存按容量划分成两等分,每次只使用其中的一块。和 survivor 一样也是用 from 和 to 两个指针这样的玩法。fromPlace 存满了,就把存活的对象 copy 到另一块 toPlace 上,然后交换指针的内容。这样就解决了碎片的问题。 - -这个算法的代价就是把内存缩水了,这样堆内存的使用效率就会变得十分低下了 - -![](https://my-blog-to-use.oss-cn-beijing.aliyuncs.com/2019-11/fc349fbb9b204495a5321febe27818d4-new-image45920a9a-552c-4656-94d6-e3ca45ff9b76.png) - -不过它们分配的时候也不是按照 1:1 这样进行分配的,就类似于 Eden 和 Survivor 也不是等价分配是一个道理。 - -#### 3.4.3 标记整理算法 - -复制算法在对象存活率高的时候会有一定的效率问题,标记过程仍然与“标记-清除”算法一样,但后续步骤不是直接对可回收对象进行清理,而是让所有存活的对象都向一端移动,然后直接清理掉边界以外的内存 - -![](https://my-blog-to-use.oss-cn-beijing.aliyuncs.com/2019-11/2599e9f722074d34a3f7fd9f0076f121-new-imagec76192ec-b63a-43e3-a6d6-cf01f749953f.png) - -#### 3.4.4 分代收集算法 - -这种算法并没有什么新的思想,只是根据对象存活周期的不同将内存划分为几块。一般是把 Java 堆分为新生代和老年代,这样就可以根据各个年代的特点采用最适当的收集算法。在新生代中,每次垃圾收集时都发现有大批对象死去,只有少量存活,那就选用复制算法,只需要付出少量存活对象的复制成本就可以完成收集。而老年代中因为对象存活率高、没有额外空间对它进行分配担保,就必须使用“标记-清除”或者“标记-整理”算法来进行回收。 - -说白了就是八仙过海各显神通,具体问题具体分析了而已。 +关于常见垃圾回收算法的详细介绍,建议阅读这篇:[JVM 垃圾回收详解(重点)](https://javaguide.cn/java/jvm/jvm-garbage-collection.html)。 ### 3.5 (了解)各种各样的垃圾回收器 HotSpot VM 中的垃圾回收器,以及适用场景 -![](https://my-blog-to-use.oss-cn-beijing.aliyuncs.com/2019-11/11e9dcd0f1ee4f25836e6f1c47104c51-new-image69e1c56a-1d40-493a-9901-6efc647a01f3.png) +![](https://static001.geekbang.org/infoq/9f/9ff72176ab0bf58bc43e142f69427379.png) 到 jdk8 为止,默认的垃圾收集器是 Parallel Scavenge 和 Parallel Old @@ -358,17 +326,19 @@ System.out.println("total mem=" + Runtime.getRuntime().totalMemory() / 1024.0 / 注意:此处设置的是 Java 堆大小,也就是新生代大小 + 老年代大小 -![](https://my-blog-to-use.oss-cn-beijing.aliyuncs.com/2019-11/5e7b352c16d74c789c665af46d3a2509-new-imagedd645dae-307d-4572-b6e2-b5a9925a46cd.png) +![](https://static001.geekbang.org/infoq/11/114f32ddd295b2e30444f42f6180538c.png) 设置一个 VM options 的参数 - -Xmx20m -Xms5m -XX:+PrintGCDetails +```plain +-Xmx20m -Xms5m -XX:+PrintGCDetails +``` -![](https://my-blog-to-use.oss-cn-beijing.aliyuncs.com/2019-11/fe99e355f4754fa4be7427cb65261f3d-new-imagebb5cf485-99f8-43eb-8809-2a89e6a1768e.png) +![](https://static001.geekbang.org/infoq/7e/7ea0bf0dec20e44bf95128c571d6ef0e.png) 再次启动 main 方法 -![](https://my-blog-to-use.oss-cn-beijing.aliyuncs.com/2019-11/300539f6560043dd8a3fe085d28420e6-new-image3c581a2e-196f-4b01-90f1-c27731b4610b.png) +![](https://static001.geekbang.org/infoq/c8/c89edbd0a147a791cfabdc37923c6836.png) 这里 GC 弹出了一个 Allocation Failure 分配失败,这个事情发生在 PSYoungGen,也就是年轻代中 @@ -384,7 +354,7 @@ System.out.println("free mem=" + Runtime.getRuntime().freeMemory() / 1024.0 / 10 System.out.println("total mem=" + Runtime.getRuntime().totalMemory() / 1024.0 / 1024 + "M"); ``` -![](https://my-blog-to-use.oss-cn-beijing.aliyuncs.com/2019-11/bdd717d0a3394be7a733760052773374-new-image371b5d59-0020-4091-9874-603c0ab0073d.png) +![](https://static001.geekbang.org/infoq/db/dbeb6aea0a90949f7d7fe4746ddb11a3.png) 此时 free memory 就又缩水了,不过 total memory 是没有变化的。Java 会尽可能将 total mem 的值维持在最小堆内存大小 @@ -396,7 +366,7 @@ System.out.println("free mem=" + Runtime.getRuntime().freeMemory() / 1024.0 / 10 System.out.println("total mem=" + Runtime.getRuntime().totalMemory() / 1024.0 / 1024 + "M"); //当前可用的总空间 ``` -![](https://my-blog-to-use.oss-cn-beijing.aliyuncs.com/2019-11/0fd7550ae2144adca8ed2ede12d5fb96-new-image0c31ff20-289d-4088-8c67-a846d0c5d1e0.png) +![](https://static001.geekbang.org/infoq/b6/b6a7c522166dbd425dbb06eb56c9b071.png) 这时候我们创建了一个 10M 的字节数据,这时候最小堆内存是顶不住的。我们会发现现在的 total memory 已经变成了 15M,这就是已经申请了一次内存的结果。 @@ -409,27 +379,32 @@ System.out.println("free mem=" + Runtime.getRuntime().freeMemory() / 1024.0 / 10 System.out.println("total mem=" + Runtime.getRuntime().totalMemory() / 1024.0 / 1024 + "M"); //当前可用的总空间 ``` -![](https://my-blog-to-use.oss-cn-beijing.aliyuncs.com/2019-11/4cc44b5d5d1c40c48640ece6a296b1ac-new-image4b57baf6-085b-4150-9c60-ac51b0f815d7.png) +![](https://static001.geekbang.org/infoq/8d/8dd6e8fccfd1394b83251c136ee44ceb.png) 此时我们手动执行了一次 fullgc,此时 total memory 的内存空间又变回 5.5M 了,此时又是把申请的内存释放掉的结果。 ### 4.2 调整新生代和老年代的比值 +```plain -XX:NewRatio --- 新生代(eden+2\*Survivor)和老年代(不包含永久区)的比值 例如:-XX:NewRatio=4,表示新生代:老年代=1:4,即新生代占整个堆的 1/5。在 Xms=Xmx 并且设置了 Xmn 的情况下,该参数不需要进行设置。 +``` ### 4.3 调整 Survivor 区和 Eden 区的比值 +```plain -XX:SurvivorRatio(幸存代)--- 设置两个 Survivor 区和 eden 的比值 例如:8,表示两个 Survivor:eden=2:8,即一个 Survivor 占年轻代的 1/10 +``` ### 4.4 设置年轻代和老年代的大小 +```plain -XX:NewSize --- 设置年轻代大小 - -XX:MaxNewSize --- 设置年轻代最大值 +``` 可以通过设置不同参数来测试不同的情况,反正最优解当然就是官方的 Eden 和 Survivor 的占比为 8:1:1,然后在刚刚介绍这些参数的时候都已经附带了一些说明,感兴趣的也可以看看。反正最大堆内存和最小堆内存如果数值不同会导致多次的 gc,需要注意。 @@ -439,13 +414,17 @@ System.out.println("total mem=" + Runtime.getRuntime().totalMemory() / 1024.0 / 在 OOM 时,记得 Dump 出堆,确保可以排查现场问题,通过下面命令你可以输出一个.dump 文件,这个文件可以使用 VisualVM 或者 Java 自带的 Java VisualVM 工具。 - -Xmx20m -Xms5m -XX:+HeapDumpOnOutOfMemoryError -XX:HeapDumpPath=你要输出的日志路径 +```plain +-Xmx20m -Xms5m -XX:+HeapDumpOnOutOfMemoryError -XX:HeapDumpPath=你要输出的日志路径 +``` 一般我们也可以通过编写脚本的方式来让 OOM 出现时给我们报个信,可以通过发送邮件或者重启程序等来解决。 ### 4.6 永久区的设置 - -XX:PermSize -XX:MaxPermSize +```plain +-XX:PermSize -XX:MaxPermSize +``` 初始空间(默认为物理内存的 1/64)和最大空间(默认为物理内存的 1/4)。也就是说,jvm 启动时,永久区一开始就占用了 PermSize 大小的空间,如果空间还不够,可以继续扩展,但是不能超过 MaxPermSize,否则会 OOM。 @@ -461,8 +440,10 @@ JDK5.0 以后每个线程堆栈大小为 1M,以前每个线程堆栈大小为 #### 4.7.2 设置线程栈的大小 - -XXThreadStackSize: - 设置线程栈的大小(0 means use default stack size) +```plain +-XXThreadStackSize: +设置线程栈的大小(0 means use default stack size) +``` 这些参数都是可以通过自己编写程序去简单测试的,这里碍于篇幅问题就不再提供 demo 了 @@ -472,61 +453,81 @@ JDK5.0 以后每个线程堆栈大小为 1M,以前每个线程堆栈大小为 #### 4.8.1 设置内存页的大小 - -XXThreadStackSize: - 设置内存页的大小,不可设置过大,会影响Perm的大小 +```plain +-XXThreadStackSize: +设置内存页的大小,不可设置过大,会影响Perm的大小 +``` #### 4.8.2 设置原始类型的快速优化 - -XX:+UseFastAccessorMethods: - 设置原始类型的快速优化 +```plain +-XX:+UseFastAccessorMethods: +设置原始类型的快速优化 +``` #### 4.8.3 设置关闭手动 GC - -XX:+DisableExplicitGC: - 设置关闭System.gc()(这个参数需要严格的测试) +```plain +-XX:+DisableExplicitGC: +设置关闭System.gc()(这个参数需要严格的测试) +``` #### 4.8.4 设置垃圾最大年龄 - -XX:MaxTenuringThreshold - 设置垃圾最大年龄。如果设置为0的话,则年轻代对象不经过Survivor区,直接进入年老代. - 对于年老代比较多的应用,可以提高效率。如果将此值设置为一个较大值, - 则年轻代对象会在Survivor区进行多次复制,这样可以增加对象再年轻代的存活时间, - 增加在年轻代即被回收的概率。该参数只有在串行GC时才有效. +```plain +-XX:MaxTenuringThreshold +设置垃圾最大年龄。如果设置为0的话,则年轻代对象不经过Survivor区,直接进入年老代.对于年老代比较多的应用,可以提高效率。如果将此值设置为一个较大值,则年轻代对象会在Survivor区进行多次复制,这样可以增加对象再年轻代的存活时间,加在年轻代即被回收的概率。该参数只有在串行GC时才有效. +``` #### 4.8.5 加快编译速度 - -XX:+AggressiveOpts - +```plain +-XX:+AggressiveOpts 加快编译速度 +``` #### 4.8.6 改善锁机制性能 - -XX:+UseBiasedLocking +```plain +-XX:+UseBiasedLocking +``` #### 4.8.7 禁用垃圾回收 - -Xnoclassgc +```plain +-Xnoclassgc +``` #### 4.8.8 设置堆空间存活时间 - -XX:SoftRefLRUPolicyMSPerMB - 设置每兆堆空闲空间中SoftReference的存活时间,默认值是1s。 +```plain +-XX:SoftRefLRUPolicyMSPerMB +设置每兆堆空闲空间中SoftReference的存活时间,默认值是1s。 +``` #### 4.8.9 设置对象直接分配在老年代 - -XX:PretenureSizeThreshold - 设置对象超过多大时直接在老年代分配,默认值是0。 +```plain +-XX:PretenureSizeThreshold +设置对象超过多大时直接在老年代分配,默认值是0。 +``` #### 4.8.10 设置 TLAB 占 eden 区的比例 - -XX:TLABWasteTargetPercent - 设置TLAB占eden区的百分比,默认值是1% 。 +```plain +-XX:TLABWasteTargetPercent +设置TLAB占eden区的百分比,默认值是1% 。 +``` #### 4.8.11 设置是否优先 YGC - -XX:+CollectGen0First - 设置FullGC时是否先YGC,默认值是false。 +```plain +-XX:+CollectGen0First +设置FullGC时是否先YGC,默认值是false。 +``` ## finally 真的扯了很久这东西,参考了多方的资料,有极客时间的《深入拆解虚拟机》和《Java 核心技术面试精讲》,也有百度,也有自己在学习的一些线上课程的总结。希望对你有所帮助,谢谢。 + + diff --git a/docs/java/jvm/jvm-parameters-intro.md b/docs/java/jvm/jvm-parameters-intro.md index 4bdea9f41c9..b97fc66d923 100644 --- a/docs/java/jvm/jvm-parameters-intro.md +++ b/docs/java/jvm/jvm-parameters-intro.md @@ -6,78 +6,78 @@ 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 +> JDK 版本:1.8 为主,也会补充新版本常用参数 -## 1.概述 +在本篇文章中,我们将一起掌握 Java 虚拟机(JVM)中最常用的一些参数配置,帮助你更好地理解和调优 Java 应用的运行环境。 -在本篇文章中,你将掌握最常用的 JVM 参数配置。 +## 堆内存相关 -## 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。也就是说老年代和新生代所占比值为 1:1,新生代占整个堆栈的 1/2。 +例如,`-XX:NewRatio=2` (默认值)表示老年代 : 新生代 = 2 : 1。即新生代占整个堆大小的 1/3。 -``` --XX:NewRatio=1 +```bash +-XX:NewRatio=2 ``` -### 2.3.显式指定永久代/元空间的大小 +### 设置永久代/元空间大小 (PermGen/Metaspace) **从 Java 8 开始,如果我们没有指定 Metaspace 的大小,随着更多类的创建,虚拟机会耗尽所有可用的系统内存(永久代并不会出现这种情况)。** @@ -101,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) 中提到的: @@ -111,123 +111,130 @@ 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() { - // Set the high-water mark to MaxMetapaceSize during VM initializaton since + // Set the high-water mark to MaxMetapaceSize during VM initialization since // we can't do a GC during initialization. _capacity_until_GC = MaxMetaspaceSize; } ``` -相关阅读:[issue 更正:MaxMetaspaceSize 如果不指定大小的话,不会耗尽内存 #1204 ](https://github.com/Snailclimb/JavaGuide/issues/1204) 。 +**3、`-XX:MaxMetaspaceSize` 的重要性:**如果不显式设置 -`XX:MaxMetaspaceSize`,元空间的最大大小理论上受限于可用的本地内存。在极端情况下(如类加载器泄漏导致不断加载类),这确实**可能耗尽大量本地内存**。因此,**强烈建议设置一个合理的 `-XX:MaxMetaspaceSize` 上限**,以防止对系统造成影响。 -## 3.垃圾收集相关 +相关阅读:[issue 更正:MaxMetaspaceSize 如果不指定大小的话,不会耗尽内存 #1204](https://github.com/Snailclimb/JavaGuide/issues/1204) 。 -### 3.1.垃圾回收器 +## 垃圾收集相关 -为了提高应用程序的稳定性,选择正确的[垃圾收集](http://www.oracle.com/webfolder/technetwork/tutorials/obe/java/gc01/index.html)算法至关重要。 +### 选择垃圾回收器 -JVM 具有四种类型的 GC 实现: +选择合适的垃圾收集器(Garbage Collector, GC)对于应用的吞吐量和响应延迟至关重要。关于垃圾收集算法和收集器的详细介绍,可以看笔者写的这篇:[JVM 垃圾回收详解(重点)](https://javaguide.cn/java/jvm/jvm-garbage-collection.html)。 -- 串行垃圾收集器 -- 并行垃圾收集器 -- CMS 垃圾收集器 -- G1 垃圾收集器 +JVM 提供了多种 GC 实现,适用于不同的场景: -可以使用以下参数声明这些实现: +- **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`。 -```bash --XX:+UseSerialGC --XX:+UseParallelGC --XX:+UseParNewGC --XX:+UseG1GC -``` - -有关*垃圾回收*实施的更多详细信息,请参见[此处](https://github.com/Snailclimb/JavaGuide/blob/master/docs/java/jvm/JVM%E5%9E%83%E5%9C%BE%E5%9B%9E%E6%94%B6.md)。 +### GC 日志记录 -### 3.2.GC 日志记录 +在生产环境或进行 GC 问题排查时,**务必开启 GC 日志记录**。详细的 GC 日志是分析和解决 GC 问题的关键依据。 -生产环境上,或者其他要测试 GC 问题的环境上,一定会配置上打印 GC 日志的参数,便于分析 GC 相关的问题。 +以下是一些推荐配置的 GC 日志参数(适用于 JDK 8/11 等常见版本): ```bash -# 必选 -# 打印基本 GC 信息 +# --- 推荐的基础配置 --- +# 打印详细 GC 信息 -XX:+PrintGCDetails +# 打印 GC 发生的时间戳 (相对于 JVM 启动时间) +# -XX:+PrintGCTimeStamps +# 打印 GC 发生的日期和时间 (更常用) -XX:+PrintGCDateStamps -# 打印对象分布 +# 指定 GC 日志文件的输出路径,%t 可以输出日期时间戳 +-Xloggc:/path/to/gc-%t.log + +# --- 推荐的进阶配置 --- +# 打印对象年龄分布 (有助于判断对象晋升老年代的情况) -XX:+PrintTenuringDistribution -# 打印堆数据 +# 在 GC 前后打印堆信息 -XX:+PrintHeapAtGC -# 打印Reference处理信息 -# 强引用/弱引用/软引用/虚引用/finalize 相关的方法 +# 打印各种类型引用 (强/软/弱/虚) 的处理信息 -XX:+PrintReferenceGC -# 打印STW时间 +# 打印应用暂停时间 (Stop-The-World, STW) -XX:+PrintGCApplicationStoppedTime -# 可选 -# 打印safepoint信息,进入 STW 阶段之前,需要要找到一个合适的 safepoint --XX:+PrintSafepointStatistics --XX:PrintSafepointStatisticsCount=1 - -# GC日志输出的文件路径 --Xloggc:/path/to/gc-%t.log -# 开启日志文件分割 +# --- GC 日志文件滚动配置 --- +# 启用 GC 日志文件滚动 -XX:+UseGCLogFileRotation -# 最多分割几个文件,超过之后从头文件开始写 +# 设置滚动日志文件的数量 (例如,保留最近 14 个) -XX:NumberOfGCLogFiles=14 -# 每个文件上限大小,超过就触发分割 +# 设置每个日志文件的最大大小 (例如,50MB) -XX:GCLogFileSize=50M + +# --- 可选的辅助诊断配置 --- +# 打印安全点 (Safepoint) 统计信息 (有助于分析 STW 原因) +# -XX:+PrintSafepointStatistics +# -XX:PrintSafepointStatisticsCount=1 ``` -## 4.处理 OOM +**注意:** JDK 9 及之后版本引入了统一的 JVM 日志框架 (`-Xlog`),配置方式有所不同,但上述 `-Xloggc` 和滚动参数通常仍然兼容或有对应的新参数。 + +## 处理 OOM 对于大型应用程序来说,面对内存不足错误是非常常见的,这反过来会导致应用程序崩溃。这是一个非常关键的场景,很难通过复制来解决这个问题。 这就是为什么 JVM 提供了一些参数,这些参数将堆内存转储到一个物理文件中,以后可以用来查找泄漏: ```bash +# 在发生 OOM 时生成堆转储文件 -XX:+HeapDumpOnOutOfMemoryError --XX:HeapDumpPath=./java_pid.hprof --XX:OnOutOfMemoryError="< cmd args >;< cmd args >" + +# 指定堆转储文件的输出路径。 会被替换为进程 ID +-XX:HeapDumpPath=/path/to/heapdump/java_pid.hprof +# 示例:-XX:HeapDumpPath=/data/dumps/ + +# (可选) 在发生 OOM 时执行指定的命令或脚本 +# 例如,发送告警通知或尝试重启服务(需谨慎使用) +# -XX:OnOutOfMemoryError=" " +# 示例:-XX:OnOutOfMemoryError="sh /path/to/notify.sh" + +# (可选) 启用 GC 开销限制检查 +# 如果 GC 时间占总时间比例过高(默认 98%)且回收效果甚微(默认小于 2% 堆内存), +# 会提前抛出 OOM,防止应用长时间卡死在 GC 中。 -XX:+UseGCOverheadLimit ``` -这里有几点需要注意: - -- **HeapDumpOnOutOfMemoryError** 指示 JVM 在遇到 **OutOfMemoryError** 错误时将 heap 转储到物理文件中。 -- **HeapDumpPath** 表示要写入文件的路径; 可以给出任何文件名; 但是,如果 JVM 在名称中找到一个 `` 标记,则当前进程的进程 id 将附加到文件名中,并使用`.hprof`格式 -- **OnOutOfMemoryError** 用于发出紧急命令,以便在内存不足的情况下执行; 应该在 `cmd args` 空间中使用适当的命令。例如,如果我们想在内存不足时重启服务器,我们可以设置参数: `-XX:OnOutOfMemoryError="shutdown -r"` 。 -- **UseGCOverheadLimit** 是一种策略,它限制在抛出 OutOfMemory 错误之前在 GC 中花费的 VM 时间的比例 - -## 5.其他 - -- `-server` : 启用“ Server Hotspot VM”; 此参数默认用于 64 位 JVM -- `-XX:+UseStringDeduplication` : _Java 8u20_ 引入了这个 JVM 参数,通过创建太多相同 String 的实例来减少不必要的内存使用; 这通过将重复 String 值减少为单个全局 `char []` 数组来优化堆内存。 -- `-XX:+UseLWPSynchronization`: 设置基于 LWP (轻量级进程)的同步策略,而不是基于线程的同步。 -- ``-XX:LargePageSizeInBytes `: 设置用于 Java 堆的较大页面大小; 它采用 GB/MB/KB 的参数; 页面大小越大,我们可以更好地利用虚拟内存硬件资源; 然而,这可能会导致 PermGen 的空间大小更大,这反过来又会迫使 Java 堆空间的大小减小。 -- `-XX:MaxHeapFreeRatio` : 设置 GC 后, 堆空闲的最大百分比,以避免收缩。 -- `-XX:SurvivorRatio` : eden/survivor 空间的比例, 例如`-XX:SurvivorRatio=6` 设置每个 survivor 和 eden 之间的比例为 1:6。 -- `-XX:+UseLargePages` : 如果系统支持,则使用大页面内存; 请注意,如果使用这个 JVM 参数,OpenJDK 7 可能会崩溃。 -- `-XX:+UseStringCache` : 启用 String 池中可用的常用分配字符串的缓存。 -- `-XX:+UseCompressedStrings` : 对 String 对象使用 `byte []` 类型,该类型可以用纯 ASCII 格式表示。 -- `-XX:+OptimizeStringConcat` : 它尽可能优化字符串串联操作。 - -## 文章推荐 - -这里推荐了非常多优质的 JVM 实践相关的文章,推荐阅读,尤其是 JVM 性能优化和问题排查相关的文章。 - -- [JVM 参数配置说明 - 阿里云官方文档 - 2022](https://help.aliyun.com/document_detail/148851.html) -- [JVM 内存配置最佳实践 - 阿里云官方文档 - 2022](https://help.aliyun.com/document_detail/383255.html) -- [求你了,GC 日志打印别再瞎配置了 - 思否 - 2022](https://segmentfault.com/a/1190000039806436) -- [一次大量 JVM Native 内存泄露的排查分析(64M 问题) - 掘金 - 2022](https://juejin.cn/post/7078624931826794503) -- [一次线上 JVM 调优实践,FullGC40 次/天到 10 天一次的优化过程 - HeapDump - 2021](https://heapdump.cn/article/1859160) -- [听说 JVM 性能优化很难?今天我小试了一把! - 陈树义 - 2021](https://shuyi.tech/archives/have-a-try-in-jvm-combat) -- [你们要的线上 GC 问题案例来啦 - 编了个程 - 2021](https://mp.weixin.qq.com/s/df1uxHWUXzhErxW1sZ6OvQ) -- [Java 中 9 种常见的 CMS GC 问题分析与解决 - 美团技术团队 - 2020](https://tech.meituan.com/2020/11/12/java-9-cms-gc.html) -- [从实际案例聊聊 Java 应用的 GC 优化-美团技术团队 - 美团技术团队 - 2017](https://tech.meituan.com/2017/12/29/jvm-optimize.html) +## 其他常用参数 + +- `-server`: 明确启用 Server 模式的 HotSpot VM。(在 64 位 JVM 上通常是默认值)。 +- `-XX:+UseStringDeduplication`: (JDK 8u20+) 尝试识别并共享底层 `char[]` 数组相同的 String 对象,以减少内存占用。适用于存在大量重复字符串的场景。 +- `-XX:SurvivorRatio=`: 设置 Eden 区与单个 Survivor 区的大小比例。例如 `-XX:SurvivorRatio=8` 表示 Eden:Survivor = 8:1。 +- `-XX:MaxTenuringThreshold=`: 设置对象从新生代晋升到老年代的最大年龄阈值(对象每经历一次 Minor GC 且存活,年龄加 1)。默认值通常是 15。 +- `-XX:+DisableExplicitGC`: 禁止代码中显式调用 `System.gc()`。推荐开启,避免人为触发不必要的 Full GC。 +- `-XX:+UseLargePages`: (需要操作系统支持) 尝试使用大内存页(如 2MB 而非 4KB),可能提升内存密集型应用的性能,但需谨慎测试。 +- -`XX:MinHeapFreeRatio= / -XX:MaxHeapFreeRatio=`: 控制 GC 后堆内存保持空闲的最小/最大百分比,用于动态调整堆大小(如果 `-Xms` 和 `-Xmx` 不相等)。通常建议将 `-Xms` 和 `-Xmx` 设为一致,避免调整开销。 + +**注意:** 以下参数在现代 JVM 版本中可能已**弃用、移除或默认开启且无需手动设置**: + +- `-XX:+UseLWPSynchronization`: 较旧的同步策略选项,现代 JVM 通常有更优化的实现。 +- `-XX:LargePageSizeInBytes`: 通常由 `-XX:+UseLargePages` 自动确定或通过 OS 配置。 +- `-XX:+UseStringCache`: 已被移除。 +- `-XX:+UseCompressedStrings`: 已被 Java 9 及之后默认开启的 Compact Strings 特性取代。 +- `-XX:+OptimizeStringConcat`: 字符串连接优化(invokedynamic)在 Java 9 及之后是默认行为。 + +## 总结 + +本文为 Java 开发者提供了一份实用的 JVM 常用参数配置指南,旨在帮助读者理解和优化 Java 应用的性能与稳定性。文章重点强调了以下几个方面: + +1. **堆内存配置:** 建议显式设置初始与最大堆内存 (`-Xms`, -`Xmx`,通常设为一致) 和新生代大小 (`-Xmn` 或 `-XX:NewSize/-XX:MaxNewSize`),这对 GC 性能至关重要。 +2. **元空间管理 (Java 8+):** 澄清了 `-XX:MetaspaceSize` 的实际作用(首次触发 Full GC 的阈值,而非初始容量),并强烈建议设置 `-XX:MaxMetaspaceSize` 以防止潜在的本地内存耗尽。 +3. **垃圾收集器选择与日志:**介绍了不同 GC 算法的适用场景,并强调在生产和测试环境中开启详细 GC 日志 (`-Xloggc`, `-XX:+PrintGCDetails` 等) 对于问题排查的必要性。 +4. **OOM 故障排查:** 说明了如何通过 `-XX:+HeapDumpOnOutOfMemoryError` 等参数在发生 OOM 时自动生成堆转储文件,以便进行后续的内存泄漏分析。 +5. **其他参数:** 简要介绍了如字符串去重等其他有用参数,并指出了部分旧参数的现状。 + +具体的问题排查和调优案例,可以参考笔者整理的这篇文章:[JVM 线上问题排查和性能调优案例](https://javaguide.cn/java/jvm/jvm-in-action.html)。 + + diff --git a/docs/java/jvm/memory-area.md b/docs/java/jvm/memory-area.md index 286f302613c..c841024a452 100644 --- a/docs/java/jvm/memory-area.md +++ b/docs/java/jvm/memory-area.md @@ -88,12 +88,12 @@ Java 虚拟机规范对于运行时数据区域的规定是相当宽松的。以 Java 方法有两种返回方式,一种是 return 语句正常返回,一种是抛出异常。不管哪种返回方式,都会导致栈帧被弹出。也就是说, **栈帧随着方法调用而创建,随着方法结束而销毁。无论方法正常完成还是异常完成都算作方法结束。** -除了 `StackOverFlowError` 错误之外,栈还可能会出现`OutOfMemoryError`错误,这是因为如果栈的内存大小可以动态扩展, 如果虚拟机在动态扩展栈时无法申请到足够的内存空间,则抛出`OutOfMemoryError`异常。 +除了 `StackOverFlowError` 错误之外,栈还可能会出现`OutOfMemoryError`错误,这是因为如果栈的内存大小可以动态扩展, 那么当虚拟机在动态扩展栈时无法申请到足够的内存空间,则抛出`OutOfMemoryError`异常。 简单总结一下程序运行中栈可能会出现两种错误: -- **`StackOverFlowError`:** 若栈的内存大小不允许动态扩展,那么当线程请求栈的深度超过当前 Java 虚拟机栈的最大深度的时候,就抛出 `StackOverFlowError` 错误。 -- **`OutOfMemoryError`:** 如果栈的内存大小可以动态扩展, 如果虚拟机在动态扩展栈时无法申请到足够的内存空间,则抛出`OutOfMemoryError`异常。 +- **`StackOverFlowError`:** 如果栈的内存大小不允许动态扩展,那么当线程请求栈的深度超过当前 Java 虚拟机栈的最大深度的时候,就抛出 `StackOverFlowError` 错误。 +- **`OutOfMemoryError`:** 如果栈的内存大小可以动态扩展, 那么当虚拟机在动态扩展栈时无法申请到足够的内存空间,则抛出`OutOfMemoryError`异常。 ![](https://oss.javaguide.cn/github/javaguide/java/jvm/%E3%80%8A%E6%B7%B1%E5%85%A5%E7%90%86%E8%A7%A3%E8%99%9A%E6%8B%9F%E6%9C%BA%E3%80%8B%E7%AC%AC%E4%B8%89%E7%89%88%E7%9A%84%E7%AC%AC2%E7%AB%A0-%E8%99%9A%E6%8B%9F%E6%9C%BA%E6%A0%88.png) @@ -125,16 +125,34 @@ Java 堆是垃圾收集器管理的主要区域,因此也被称作 **GC 堆( **JDK 8 版本之后 PermGen(永久代) 已被 Metaspace(元空间) 取代,元空间使用的是本地内存。** (我会在方法区这部分内容详细介绍到)。 -大部分情况,对象都会首先在 Eden 区域分配,在一次新生代垃圾回收后,如果对象还存活,则会进入 S0 或者 S1,并且对象的年龄还会加 1(Eden 区->Survivor 区后对象的初始年龄变为 1),当它的年龄增加到一定程度(默认为 15 岁),就会被晋升到老年代中。对象晋升到老年代的年龄阈值,可以通过参数 `-XX:MaxTenuringThreshold` 来设置。 +大部分情况,对象都会首先在 Eden 区域分配,在一次新生代垃圾回收后,如果对象还存活,则会进入 S0 或者 S1,并且对象的年龄还会加 1(Eden 区->Survivor 区后对象的初始年龄变为 1),当它的年龄增加到一定程度(默认为 15 岁),就会被晋升到老年代中。对象晋升到老年代的年龄阈值,可以通过参数 `-XX:MaxTenuringThreshold` 来设置。不过,设置的值应该在 0-15,否则会爆出以下错误: -> **🐛 修正(参见:[issue552](https://github.com/Snailclimb/JavaGuide/issues/552))**:“Hotspot 遍历所有对象时,按照年龄从小到大对其所占用的大小进行累积,当累积的某个年龄大小超过了 survivor 区的一半时,取这个年龄和 MaxTenuringThreshold 中更小的一个值,作为新的晋升年龄阈值”。 +```bash +MaxTenuringThreshold of 20 is invalid; must be between 0 and 15 +``` + +**为什么年龄只能是 0-15?** + +因为记录年龄的区域在对象头中,这个区域的大小通常是 4 位。这 4 位可以表示的最大二进制数字是 1111,即十进制的 15。因此,对象的年龄被限制为 0 到 15。 + +这里我们简单结合对象布局来详细介绍一下。 + +在 HotSpot 虚拟机中,对象在内存中存储的布局可以分为 3 块区域:对象头(Header)、实例数据(Instance Data)和对齐填充(Padding)。其中,对象头包括两部分:标记字段(Mark Word)和类型指针(Klass Word)。关于对象内存布局的详细介绍,后文会介绍到,这里就不重复提了。 + +这个年龄信息就是在标记字段中存放的(标记字段还存放了对象自身的其他信息比如哈希码、锁状态信息等等)。`markOop.hpp`定义了标记字(mark word)的结构: + +![标记字段结构](https://oss.javaguide.cn/github/javaguide/java/jvm/hotspot-markOop.hpp..png) + +可以看到对象年龄占用的大小确实是 4 位。 + +> **🐛 修正(参见:[issue552](https://github.com/Snailclimb/JavaGuide/issues/552))**:“Hotspot 遍历所有对象时,按照年龄从小到大对其所占用的大小进行累加,当累加到某个年龄时,所累加的大小超过了 Survivor 区的一半,则取这个年龄和 `MaxTenuringThreshold` 中更小的一个值,作为新的晋升年龄阈值”。 > > **动态年龄计算的代码如下** > > ```c++ > uint ageTable::compute_tenuring_threshold(size_t survivor_capacity) { -> //survivor_capacity是survivor空间的大小 -> size_t desired_survivor_size = (size_t)((((double) survivor_capacity)*TargetSurvivorRatio)/100); +> //survivor_capacity是survivor空间的大小 +> size_t desired_survivor_size = (size_t)((((double) survivor_capacity)*TargetSurvivorRatio)/100);//TargetSurvivorRatio 为50 > size_t total = 0; > uint age = 1; > while (age < table_size) { @@ -143,7 +161,7 @@ Java 堆是垃圾收集器管理的主要区域,因此也被称作 **GC 堆( > age++; > } > uint result = age < MaxTenuringThreshold ? age : MaxTenuringThreshold; -> ... +> ... > } > ``` @@ -151,7 +169,7 @@ Java 堆是垃圾收集器管理的主要区域,因此也被称作 **GC 堆( 1. **`java.lang.OutOfMemoryError: GC Overhead Limit Exceeded`**:当 JVM 花太多时间执行垃圾回收并且只能回收很少的堆空间时,就会发生此错误。 2. **`java.lang.OutOfMemoryError: Java heap space`** :假如在创建新的对象时, 堆内存中的空间不足以存放新创建的对象, 就会引发此错误。(和配置的最大堆内存有关,且受制于物理内存大小。最大堆内存可通过`-Xmx`参数配置,若没有特别配置,将会使用默认值,详见:[Default Java 8 max heap size](https://stackoverflow.com/questions/28272923/default-xmxsize-in-java-8-max-heap-size)) -3. ...... +3. …… ### 方法区 @@ -171,7 +189,7 @@ Java 堆是垃圾收集器管理的主要区域,因此也被称作 **GC 堆( ![](https://oss.javaguide.cn/github/javaguide/java/jvm/20210425134508117.png) -1、整个永久代有一个 JVM 本身设置的固定大小上限,无法进行调整,而元空间使用的是本地内存,受本机可用内存的限制,虽然元空间仍旧可能溢出,但是比原来出现的几率会更小。 +1、整个永久代有一个 JVM 本身设置的固定大小上限,无法进行调整(也就是受到 JVM 内存的限制),而元空间使用的是本地内存,受本机可用内存的限制,虽然元空间仍旧可能溢出,但是比原来出现的几率会更小。 > 当元空间溢出时会得到如下错误:`java.lang.OutOfMemoryError: MetaSpace` @@ -181,6 +199,8 @@ Java 堆是垃圾收集器管理的主要区域,因此也被称作 **GC 堆( 3、在 JDK8,合并 HotSpot 和 JRockit 的代码时, JRockit 从来没有一个叫永久代的东西, 合并之后就没有必要额外的设置这么一个永久代的地方了。 +4、永久代会为 GC 带来不必要的复杂度,并且回收效率偏低。 + **方法区常用参数有哪些?** JDK 1.8 之前永久代还没被彻底移除的时候通常通过下面这些参数来调节方法区大小。 @@ -222,17 +242,17 @@ Class 文件中除了有类的版本、字段、方法、接口等描述信息 **字符串常量池** 是 JVM 为了提升性能和减少内存消耗针对字符串(String 类)专门开辟的一块区域,主要目的是为了避免字符串的重复创建。 ```java -// 在堆中创建字符串对象”ab“ -// 将字符串对象”ab“的引用保存在字符串常量池中 +// 在字符串常量池中创建字符串对象 ”ab“ +// 将字符串对象 ”ab“ 的引用赋值给给 aa String aa = "ab"; -// 直接返回字符串常量池中字符串对象”ab“的引用 +// 直接返回字符串常量池中字符串对象 ”ab“,赋值给引用 bb String bb = "ab"; -System.out.println(aa==bb);// true +System.out.println(aa==bb); // true ``` HotSpot 虚拟机中字符串常量池的实现是 `src/hotspot/share/classfile/stringTable.cpp` ,`StringTable` 可以简单理解为一个固定大小的`HashTable` ,容量为 `StringTableSize`(可以通过 `-XX:StringTableSize` 参数来设置),保存的是字符串(key)和 字符串对象的引用(value)的映射关系,字符串对象的引用指向堆中的字符串对象。 -JDK1.7 之前,字符串常量池存放在永久代。JDK1.7 字符串常量池和静态变量从永久代移动了 Java 堆中。 +JDK1.7 之前,字符串常量池存放在永久代。JDK1.7 字符串常量池和静态变量从永久代移动到了 Java 堆中。 ![method-area-jdk1.6](https://oss.javaguide.cn/github/javaguide/java/jvm/method-area-jdk1.6.png) @@ -258,9 +278,9 @@ JDK1.4 中新加入的 **NIO(Non-Blocking I/O,也被称为 New I/O)**, 直接内存的分配不会受到 Java 堆的限制,但是,既然是内存就会受到本机总内存大小以及处理器寻址空间的限制。 -类似的概念还有 **堆外内存** 。在一些文章中将直接内存等价于堆外内,个人觉得不是特别准确。 +类似的概念还有 **堆外内存** 。在一些文章中将直接内存等价于堆外内存,个人觉得不是特别准确。 -堆外内存就是把内存对象分配在堆(新生代+老年代+永久代)以外的内存,这些内存直接受操作系统管理(而不是虚拟机),这样做的结果就是能够在一定程度上减少垃圾回收对应用程序造成的影响。 +堆外内存就是把内存对象分配在堆外的内存,这些内存直接受操作系统管理(而不是虚拟机),这样做的结果就是能够在一定程度上减少垃圾回收对应用程序造成的影响。 ## HotSpot 虚拟机对象探秘 @@ -312,9 +332,12 @@ Java 对象的创建过程我建议最好是能默写出来,并且要掌握每 ### 对象的内存布局 -在 Hotspot 虚拟机中,对象在内存中的布局可以分为 3 块区域:**对象头**、**实例数据**和**对齐填充**。 +在 Hotspot 虚拟机中,对象在内存中的布局可以分为 3 块区域:**对象头(Header)**、**实例数据(Instance Data)**和**对齐填充(Padding)**。 + +对象头包括两部分信息: -**Hotspot 虚拟机的对象头包括两部分信息**,**第一部分用于存储对象自身的运行时数据**(哈希码、GC 分代年龄、锁状态标志等等),**另一部分是类型指针**,即对象指向它的类元数据的指针,虚拟机通过这个指针来确定这个对象是哪个类的实例。 +1. 标记字段(Mark Word):用于存储对象自身的运行时数据, 如哈希码(HashCode)、GC 分代年龄、锁状态标志、线程持有的锁、偏向线程 ID、偏向时间戳等等。 +2. 类型指针(Klass pointer):对象指向它的类元数据的指针,虚拟机通过这个指针来确定这个对象是哪个类的实例。 **实例数据部分是对象真正存储的有效信息**,也是在程序中所定义的各种类型的字段内容。 @@ -344,10 +367,12 @@ HotSpot 虚拟机主要使用的就是这种方式来进行对象访问。 - 《深入理解 Java 虚拟机:JVM 高级特性与最佳实践(第二版》 - 《自己动手写 Java 虚拟机》 -- Chapter 2. The Structure of the Java Virtual Machine:https://docs.oracle.com/javase/specs/jvms/se8/html/jvms-2.html -- JVM 栈帧内部结构-动态链接:https://chenxitag.com/archives/368 -- Java 中 new String("字面量") 中 "字面量" 是何时进入字符串常量池的? - 木女孩的回答 - 知乎:https://www.zhihu.com/question/55994121/answer/147296098 -- JVM 常量池中存储的是对象还是引用呢? - RednaxelaFX 的回答 - 知乎:https://www.zhihu.com/question/57109429/answer/151717241 +- Chapter 2. The Structure of the Java Virtual Machine: +- JVM 栈帧内部结构-动态链接: +- Java 中 new String("字面量") 中 "字面量" 是何时进入字符串常量池的? - 木女孩的回答 - 知乎: +- JVM 常量池中存储的是对象还是引用呢? - RednaxelaFX 的回答 - 知乎: - - - + + diff --git a/docs/java/new-features/java10.md b/docs/java/new-features/java10.md index 41118979b2c..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 的发生,以带来更好的性能提升、更大的吞吐量。 @@ -106,12 +106,14 @@ Oracle 的 HotSpot VM 便附带两个用 C++ 实现的 JIT compiler:C1 及 C2 - **线程-局部管控**:Java 10 中线程管控引入 JVM 安全点的概念,将允许在不运行全局 JVM 安全点的情况下实现线程回调,由线程本身或者 JVM 线程来执行,同时保持线程处于阻塞状态,这种方式使得停止单个线程变成可能,而不是只能启用或停止所有线程 - **备用存储装置上的堆分配**:Java 10 中将使得 JVM 能够使用适用于不同类型的存储机制的堆,在可选内存设备上进行堆内存分配 -- ...... +- …… ## 参考 -- Java 10 Features and Enhancements : https://howtodoinjava.com/java10/java10-features/ +- Java 10 Features and Enhancements : - Guide to Java10 : -- 4 Class Data Sharing : https://docs.oracle.com/javase/10/vm/class-data-sharing.htm#JSJVM-GUID-7EAA3411-8CF0-4D19-BD05-DF5E1780AA91 +- 4 Class Data Sharing : + + diff --git a/docs/java/new-features/java11.md b/docs/java/new-features/java11.md index 8057b136b1e..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 位平台 @@ -121,9 +121,11 @@ Consumer consumer = (String i) -> System.out.println(i); - **低开销的 Heap Profiling**:Java 11 中提供一种低开销的 Java 堆分配采样方法,能够得到堆分配的 Java 对象信息,并且能够通过 JVMTI 访问堆信息 - **TLS1.3 协议**:Java 11 中包含了传输层安全性(TLS)1.3 规范(RFC 8446)的实现,替换了之前版本中包含的 TLS,包括 TLS 1.2,同时还改进了其他 TLS 功能,例如 OCSP 装订扩展(RFC 6066,RFC 6961),以及会话散列和扩展主密钥扩展(RFC 7627),在安全性和性能方面也做了很多提升 - **飞行记录器(Java Flight Recorder)**:飞行记录器之前是商业版 JDK 的一项分析工具,但在 Java 11 中,其代码被包含到公开代码库中,这样所有人都能使用该功能了。 -- ...... +- …… ## 参考 -- JDK 11 Release Notes:https://www.oracle.com/java/technologies/javase/11-relnote-issues.html +- JDK 11 Release Notes: - Java 11 – Features and Comparison: + + diff --git a/docs/java/new-features/java12-13.md b/docs/java/new-features/java12-13.md index 2ae8f9b1f01..8b3207ab4d5 100644 --- a/docs/java/new-features/java12-13.md +++ b/docs/java/new-features/java12-13.md @@ -9,7 +9,7 @@ tag: ### String 增强 -Java 11 增加了两个的字符串处理方法,如以下所示。 +Java 12 增加了两个的字符串处理方法,如以下所示。 `indent()` 方法可以实现字符串缩进。 @@ -24,7 +24,7 @@ System.out.println(text); 输出: -``` +```plain Java Java ``` @@ -82,7 +82,7 @@ System.out.println(result); 输出: -``` +```plain 1K ``` @@ -128,7 +128,7 @@ switch (day) { Object obj = "我是字符串"; if(obj instanceof String){ String str = (String) obj; - System.out.println(str); + System.out.println(str); } ``` @@ -137,7 +137,7 @@ if(obj instanceof String){ ```java Object obj = "我是字符串"; if(obj instanceof String str){ - System.out.println(str); + System.out.println(str); } ``` @@ -145,7 +145,7 @@ if(obj instanceof String str){ ### 增强 ZGC(释放未使用内存) -在 Java 11 中是实验性的引入的 ZGC 在实际的使用中存在未能主动将未使用的内存释放给操作系统的问题。 +在 Java 11 中实验性引入的 ZGC 在实际的使用中存在未能主动将未使用的内存释放给操作系统的问题。 ZGC 堆由一组称为 ZPages 的堆区域组成。在 GC 周期中清空 ZPages 区域时,它们将被释放并返回到页面缓存 **ZPageCache** 中,此缓存中的 ZPages 按最近最少使用(LRU)的顺序,并按照大小进行组织。 @@ -179,8 +179,8 @@ Java 13 中对 Java 10 中引入的应用程序类数据共享(AppCDS)进行了 这提高了应用程序类数据共享([AppCDS](https://openjdk.java.net/jeps/310))的可用性。无需用户进行试运行来为每个应用程序创建类列表。 ```bash -$ java -XX:ArchiveClassesAtExit=my_app_cds.jsa -cp my_app.jar -$ java -XX:SharedArchiveFile=my_app_cds.jsa -cp my_app.jar +java -XX:ArchiveClassesAtExit=my_app_cds.jsa -cp my_app.jar +java -XX:SharedArchiveFile=my_app_cds.jsa -cp my_app.jar ``` ### 预览新特性 @@ -288,9 +288,11 @@ public String translateEscapes() { ## 参考 -- JDK Project Overview: -- Oracle Java12 ReleaseNote:https://www.oracle.com/java/technologies/javase/12all-relnotes.htm -- What is new in Java 12:https://mkyong.com/java/what-is-new-in-java-12/ +- JDK Project Overview: +- Oracle Java12 ReleaseNote: +- What is new in Java 12: - Oracle Java13 ReleaseNote - New Java13 Features - Java13 新特性概述 + + diff --git a/docs/java/new-features/java14-15.md b/docs/java/new-features/java14-15.md index 1cf72221467..415ca543e4b 100644 --- a/docs/java/new-features/java14-15.md +++ b/docs/java/new-features/java14-15.md @@ -56,9 +56,9 @@ System.out.println(result); #### record 关键字 -`record` 关键字可以简化 **数据类**(一个 Java 类一旦实例化就不能再修改)的定义方式,使用 `record` 代替 `class` 定义的类,只需要声明属性,就可以在获得属性的访问方法,以及 `toString()`,`hashCode()`, `equals()`方法 +`record` 关键字可以简化 **数据类**(一个 Java 类一旦实例化就不能再修改)的定义方式,使用 `record` 代替 `class` 定义的类,只需要声明属性,就可以在获得属性的访问方法,以及 `toString()`,`hashCode()`, `equals()`方法。 -类似于使用 `class` 定义类,同时使用了 lombok 插件,并打上了`@Getter,@ToString,@EqualsAndHashCode`注解 +类似于使用 `class` 定义类,同时使用了 lombok 插件,并打上了`@Getter,@ToString,@EqualsAndHashCode`注解。 ```java /** @@ -117,7 +117,7 @@ c++ php ### 其他 -- 从 Java11 引入的 ZGC 作为继 G1 过后的下一代 GC 算法,从支持 Linux 平台到 Java14 开始支持 MacOS 和 Window(个人感觉是终于可以在日常开发工具中先体验下 ZGC 的效果了,虽然其实 G1 也够用) +- 从 Java11 引入的 ZGC 作为继 G1 过后的下一代 GC 算法,从支持 Linux 平台到 Java14 开始支持 MacOS 和 Windows(个人感觉是终于可以在日常开发工具中先体验下 ZGC 的效果了,虽然其实 G1 也够用) - 移除了 CMS(Concurrent Mark Sweep) 垃圾收集器(功成而退) - 新增了 jpackage 工具,标配将应用打成 jar 包外,还支持不同平台的特性包,比如 linux 下的`deb`和`rpm`,window 平台下的`msi`和`exe` @@ -156,7 +156,7 @@ Java11 的时候 ,ZGC 还在试验阶段。 不过,默认的垃圾回收器依然是 G1。你可以通过下面的参数启动 ZGC: ```bash -$ java -XX:+UseZGC className +java -XX:+UseZGC className ``` ### EdDSA(数字签名算法) @@ -182,7 +182,7 @@ System.out.println(encodedString); 输出: -``` +```plain 0Hc0lxxASZNvS52WsvnncJOH/mlFhnA8Tc6D/k5DtAX5BSsNVjtPF4R4+yMWXVjrvB2mxVXmChIbki6goFBgAg== ``` @@ -237,5 +237,7 @@ Java 15 并没有对此特性进行调整,继续预览特性,主要用于接 - **Nashorn JavaScript 引擎彻底移除**:Nashorn 从 Java8 开始引入的 JavaScript 引擎,Java9 对 Nashorn 做了些增强,实现了一些 ES6 的新特性。在 Java 11 中就已经被弃用,到了 Java 15 就彻底被删除了。 - **DatagramSocket API 重构** -- **禁用和废弃偏向锁(Biased Locking)**:偏向锁的引入增加了 JVM 的复杂性大于其带来的性能提升。不过,你仍然可以使用 `-XX:+UseBiasedLocking` 启用偏向锁定,但它会提示 这是一个已弃用的 API。 -- ...... +- **禁用和废弃偏向锁(Biased Locking)**:偏向锁的引入增加了 JVM 的复杂性大于其带来的性能提升。不过,你仍然可以使用 `-XX:+UseBiasedLocking` 启用偏向锁定,但它会提示这是一个已弃用的 API。 +- …… + + diff --git a/docs/java/new-features/java16.md b/docs/java/new-features/java16.md index 2f1729d8645..60906c40020 100644 --- a/docs/java/new-features/java16.md +++ b/docs/java/new-features/java16.md @@ -9,7 +9,7 @@ Java 16 在 2021 年 3 月 16 日正式发布,非长期支持(LTS)版本 相关阅读:[OpenJDK Java 16 文档](https://openjdk.java.net/projects/jdk/16/) 。 -## JEP 338:向量 API(第二次孵化) +## JEP 338:向量 API(第一次孵化) 向量(Vector) API 最初由 [JEP 338](https://openjdk.java.net/jeps/338) 提出,并作为[孵化 API](http://openjdk.java.net/jeps/11)集成到 Java 16 中。第二轮孵化由 [JEP 414](https://openjdk.java.net/jeps/414) 提出并集成到 Java 17 中,第三轮孵化由 [JEP 417](https://openjdk.java.net/jeps/417) 提出并集成到 Java 18 中,第四轮由 [JEP 426](https://openjdk.java.net/jeps/426) 提出并集成到了 Java 19 中。 @@ -69,14 +69,14 @@ public void inc(Integer count) { 引入外部内存访问 API 以允许 Java 程序安全有效地访问 Java 堆之外的外部内存。 -Java 14([ JEP 370](https://openjdk.org/jeps/370)) 的时候,第一次孵化外部内存访问 API,Java 15 中进行了第二次复活([JEP 383](https://openjdk.org/jeps/383)),在 Java 16 中进行了第三次孵化。 +Java 14([JEP 370](https://openjdk.org/jeps/370)) 的时候,第一次孵化外部内存访问 API,Java 15 中进行了第二次复活([JEP 383](https://openjdk.org/jeps/383)),在 Java 16 中进行了第三次孵化。 引入外部内存访问 API 的目的如下: - 通用:单个 API 应该能够对各种外部内存(如本机内存、持久内存、堆内存等)进行操作。 - 安全:无论操作何种内存,API 都不应该破坏 JVM 的安全性。 - 控制:可以自由的选择如何释放内存(显式、隐式等)。 -- 可用:如果需要访问外部内存,API 应该是 `sun.misc.Unsafa`. +- 可用:如果需要访问外部内存,API 应该是 `sun.misc.Unsafe`. ## JEP 394:instanceof 模式匹配(转正) @@ -84,7 +84,7 @@ Java 14([ JEP 370](https://openjdk.org/jeps/370)) 的时候,第一次孵化外 | ---------- | ----------------- | --------------------------------------- | ---------------------------------------- | | Java SE 14 | preview | [JEP 305](https://openjdk.org/jeps/305) | 首次引入 instanceof 模式匹配。 | | Java SE 15 | Second Preview | [JEP 375](https://openjdk.org/jeps/375) | 相比较上个版本无变化,继续收集更多反馈。 | -| Java SE 16 | Permanent Release | [JEP 394](https://openjdk.org/jeps/394) | 模式变量不在隐式为 final。 | +| Java SE 16 | Permanent Release | [JEP 394](https://openjdk.org/jeps/394) | 模式变量不再隐式为 final。 | 从 Java 16 开始,你可以对 `instanceof` 中的变量值进行修改。 @@ -115,9 +115,9 @@ if (o instanceof String s) { ```java public class Outer { - class Inner { - static int age; - } + class Inner { + static int age; + } } ``` @@ -148,3 +148,5 @@ public class Outer { - [Consolidated JDK 16 Release Notes](https://www.oracle.com/java/technologies/javase/16all-relnotes.html) - [Java 16 正式发布,新特性一一解析](https://www.infoq.cn/article/IAkwhx7i9V7G8zLVEd4L) - [实操 | 剖析 Java16 新语法特性](https://xie.infoq.cn/article/8304c894c4e38318d38ceb116)(写的很赞) + + diff --git a/docs/java/new-features/java17.md b/docs/java/new-features/java17.md index a2c0e0fc6f3..e478f1f5c43 100644 --- a/docs/java/new-features/java17.md +++ b/docs/java/new-features/java17.md @@ -29,7 +29,7 @@ Java 17 将是继 Java 8 以来最重要的长期支持(LTS)版本,是 Jav - [JEP 410:Remove the Experimental AOT and JIT Compiler(删除实验性的 AOT 和 JIT 编译器)](https://openjdk.java.net/jeps/410) - [JEP 411:Deprecate the Security Manager for Removal(弃用安全管理器以进行删除)](https://openjdk.java.net/jeps/411) - [JEP 412:Foreign Function & Memory API (外部函数和内存 API)](https://openjdk.java.net/jeps/412)(孵化) -- [JEP 414:Vector(向量) API ](https://openjdk.java.net/jeps/417)(第二次孵化) +- [JEP 414:Vector(向量) API](https://openjdk.java.net/jeps/417)(第二次孵化) - [JEP 415:Context-Specific Deserialization Filters](https://openjdk.java.net/jeps/415) 这里只对 356、398、413、406、407、409、410、411、412、414 这几个我觉得比较重要的新特性进行详细介绍。 @@ -40,9 +40,9 @@ Java 17 将是继 Java 8 以来最重要的长期支持(LTS)版本,是 Jav JDK 17 之前,我们可以借助 `Random`、`ThreadLocalRandom`和`SplittableRandom`来生成随机数。不过,这 3 个类都各有缺陷,且缺少常见的伪随机算法支持。 -Java 17 为伪随机数生成器 (pseudorandom number generator,RPNG,又称为确定性随机位生成器)增加了新的接口类型和实现,使得开发者更容易在应用程序中互换使用各种 PRNG 算法。 +Java 17 为伪随机数生成器 (pseudorandom number generator,PRNG,又称为确定性随机位生成器)增加了新的接口类型和实现,使得开发者更容易在应用程序中互换使用各种 PRNG 算法。 -> [RPNG](https://ctf-wiki.org/crypto/streamcipher/prng/intro/) 用来生成接近于绝对随机数序列的数字序列。一般来说,PRNG 会依赖于一个初始值,也称为种子,来生成对应的伪随机数序列。只要种子确定了,PRNG 所生成的随机数就是完全确定的,因此其生成的随机数序列并不是真正随机的。 +> [PRNG](https://ctf-wiki.org/crypto/streamcipher/prng/intro/) 用来生成接近于绝对随机数序列的数字序列。一般来说,PRNG 会依赖于一个初始值,也称为种子,来生成对应的伪随机数序列。只要种子确定了,PRNG 所生成的随机数就是完全确定的,因此其生成的随机数序列并不是真正随机的。 使用示例: @@ -161,7 +161,7 @@ Java 17,删除实验性的提前 (AOT) 和即时 (JIT) 编译器,因为该 Java 程序可以通过该 API 与 Java 运行时之外的代码和数据进行互操作。通过高效地调用外部函数(即 JVM 之外的代码)和安全地访问外部内存(即不受 JVM 管理的内存),该 API 使 Java 程序能够调用本机库并处理本机数据,而不会像 JNI 那样危险和脆弱。 -外部函数和内存 API 在 Java 17 中进行了第一轮孵化,由 [JEP 412](https://openjdk.java.net/jeps/412) 提出。第二轮孵化由[ JEP 419](https://openjdk.org/jeps/419) 提出并集成到了 Java 18 中,预览由 [JEP 424](https://openjdk.org/jeps/424) 提出并集成到了 Java 19 中。 +外部函数和内存 API 在 Java 17 中进行了第一轮孵化,由 [JEP 412](https://openjdk.java.net/jeps/412) 提出。第二轮孵化由[JEP 419](https://openjdk.org/jeps/419) 提出并集成到了 Java 18 中,预览由 [JEP 424](https://openjdk.org/jeps/424) 提出并集成到了 Java 19 中。 在 [Java 19 新特性概览](./java19.md) 中,我有详细介绍到外部函数和内存 API,这里就不再做额外的介绍了。 @@ -172,3 +172,5 @@ Java 程序可以通过该 API 与 Java 运行时之外的代码和数据进行 该孵化器 API 提供了一个 API 的初始迭代以表达一些向量计算,这些计算在运行时可靠地编译为支持的 CPU 架构上的最佳向量硬件指令,从而获得优于同等标量计算的性能,充分利用单指令多数据(SIMD)技术(大多数现代 CPU 上都可以使用的一种指令)。尽管 HotSpot 支持自动向量化,但是可转换的标量操作集有限且易受代码更改的影响。该 API 将使开发人员能够轻松地用 Java 编写可移植的高性能向量算法。 在 [Java 18 新特性概览](./java18.md) 中,我有详细介绍到向量 API,这里就不再做额外的介绍了。 + + diff --git a/docs/java/new-features/java18.md b/docs/java/new-features/java18.md index 3f9da396f61..40fa7bb61df 100644 --- a/docs/java/new-features/java18.md +++ b/docs/java/new-features/java18.md @@ -13,7 +13,7 @@ Java 18 带来了 9 个新特性: - [JEP 408:Simple Web Server(简易的 Web 服务器)](https://openjdk.java.net/jeps/408) - [JEP 413:Code Snippets in Java API Documentation(Java API 文档中的代码片段)](https://openjdk.java.net/jeps/413) - [JEP 416:Reimplement Core Reflection with Method Handles(使用方法句柄重新实现反射核心)](https://openjdk.java.net/jeps/416) -- [JEP 417:Vector(向量) API ](https://openjdk.java.net/jeps/417)(第三次孵化) +- [JEP 417:Vector(向量) API](https://openjdk.java.net/jeps/417)(第三次孵化) - [JEP 418:Internet-Address Resolution(互联网地址解析)SPI](https://openjdk.java.net/jeps/418) - [JEP 419:Foreign Function & Memory API(外部函数和内存 API)](https://openjdk.java.net/jeps/419)(第二次孵化) - [JEP 420:Pattern Matching for switch(switch 模式匹配)](https://openjdk.java.net/jeps/420)(第二次预览) @@ -134,6 +134,8 @@ Java 18 定义了一个全新的 SPI(service-provider interface),用于主 Java 程序可以通过该 API 与 Java 运行时之外的代码和数据进行互操作。通过高效地调用外部函数(即 JVM 之外的代码)和安全地访问外部内存(即不受 JVM 管理的内存),该 API 使 Java 程序能够调用本机库并处理本机数据,而不会像 JNI 那样危险和脆弱。 -外部函数和内存 API 在 Java 17 中进行了第一轮孵化,由 [JEP 412](https://openjdk.java.net/jeps/412) 提出。第二轮孵化由[ JEP 419](https://openjdk.org/jeps/419) 提出并集成到了 Java 18 中,预览由 [JEP 424](https://openjdk.org/jeps/424) 提出并集成到了 Java 19 中。 +外部函数和内存 API 在 Java 17 中进行了第一轮孵化,由 [JEP 412](https://openjdk.java.net/jeps/412) 提出。第二轮孵化由[JEP 419](https://openjdk.org/jeps/419) 提出并集成到了 Java 18 中,预览由 [JEP 424](https://openjdk.org/jeps/424) 提出并集成到了 Java 19 中。 在 [Java 19 新特性概览](./java19.md) 中,我有详细介绍到外部函数和内存 API,这里就不再做额外的介绍了。 + + diff --git a/docs/java/new-features/java19.md b/docs/java/new-features/java19.md index 4729cca4294..a207bc6830a 100644 --- a/docs/java/new-features/java19.md +++ b/docs/java/new-features/java19.md @@ -5,7 +5,7 @@ tag: - Java新特性 --- -JDK 19 定于 9 月 20 日正式发布以供生产使用,非长期支持版本。不过,JDK 19 中有一些比较重要的新特性值得关注。 +JDK 19 定于 2022 年 9 月 20 日正式发布以供生产使用,非长期支持版本。不过,JDK 19 中有一些比较重要的新特性值得关注。 JDK 19 只有 7 个新特性: @@ -25,7 +25,7 @@ JDK 19 只有 7 个新特性: Java 程序可以通过该 API 与 Java 运行时之外的代码和数据进行互操作。通过高效地调用外部函数(即 JVM 之外的代码)和安全地访问外部内存(即不受 JVM 管理的内存),该 API 使 Java 程序能够调用本机库并处理本机数据,而不会像 JNI 那样危险和脆弱。 -外部函数和内存 API 在 Java 17 中进行了第一轮孵化,由 [JEP 412](https://openjdk.java.net/jeps/412) 提出。第二轮孵化由[ JEP 419](https://openjdk.org/jeps/419) 提出并集成到了 Java 18 中,预览由 [JEP 424](https://openjdk.org/jeps/424) 提出并集成到了 Java 19 中。 +外部函数和内存 API 在 Java 17 中进行了第一轮孵化,由 [JEP 412](https://openjdk.java.net/jeps/412) 提出。第二轮孵化由[JEP 419](https://openjdk.org/jeps/419) 提出并集成到了 Java 18 中,预览由 [JEP 424](https://openjdk.org/jeps/424) 提出并集成到了 Java 19 中。 在没有外部函数和内存 API 之前: @@ -36,7 +36,7 @@ Java 程序可以通过该 API 与 Java 运行时之外的代码和数据进行 Foreign Function & Memory API (FFM API) 定义了类和接口: -- 分配外部内存:`MemorySegment`、、`MemoryAddress`和`SegmentAllocator`); +- 分配外部内存:`MemorySegment`、`MemoryAddress`和`SegmentAllocator`; - 操作和访问结构化的外部内存:`MemoryLayout`, `VarHandle`; - 控制外部内存的分配和释放:`MemorySession`; - 调用外部函数:`Linker`、`FunctionDescriptor`和`SymbolLookup`。 @@ -78,10 +78,11 @@ assert Arrays.equals(javaStrings, new String[] {"car", "cat", "dog", "mouse"}); 虚拟线程避免了上下文切换的额外耗费,兼顾了多线程的优点,简化了高并发程序的复杂,可以有效减少编写、维护和观察高吞吐量并发应用程序的工作量。 -知乎有一个关于 Java 19 虚拟线程的讨论,感兴趣的可以去看看:https://www.zhihu.com/question/536743167 。 +知乎有一个关于 Java 19 虚拟线程的讨论,感兴趣的可以去看看: 。 Java 虚拟线程的详细解读和原理可以看下面这两篇文章: +- [虚拟线程原理及性能分析|得物技术](https://mp.weixin.qq.com/s/vdLXhZdWyxc6K-D3Aj03LA) - [Java19 正式 GA!看虚拟线程如何大幅提高系统吞吐量](https://mp.weixin.qq.com/s/yyApBXxpXxVwttr01Hld6Q) - [虚拟线程 - VirtualThread 源码透视](https://www.cnblogs.com/throwable/p/16758997.html) @@ -114,3 +115,5 @@ JDK 19 引入了结构化并发,一种多线程编程方法,目的是为了 ``` 结构化并发非常适合虚拟线程,虚拟线程是 JDK 实现的轻量级线程。许多虚拟线程共享同一个操作系统线程,从而允许非常多的虚拟线程。 + + diff --git a/docs/java/new-features/java20.md b/docs/java/new-features/java20.md index e2f07b06d13..9dd86a71c70 100644 --- a/docs/java/new-features/java20.md +++ b/docs/java/new-features/java20.md @@ -18,8 +18,8 @@ JDK 20 只有 7 个新特性: - [JEP 433:switch 模式匹配](https://openjdk.org/jeps/433)(第四次预览) - [JEP 434: Foreign Function & Memory API(外部函数和内存 API)](https://openjdk.org/jeps/434)(第二次预览) - [JEP 436: Virtual Threads(虚拟线程)](https://openjdk.org/jeps/436)(第二次预览) -- [JEP 437: Structured Concurrency(结构化并发)](https://openjdk.org/jeps/437)(第二次孵化) -- [JEP 432:向量 API(](https://openjdk.org/jeps/438)第五次孵化) +- [JEP 437:Structured Concurrency(结构化并发)](https://openjdk.org/jeps/437)(第二次孵化) +- [JEP 432:向量 API(](https://openjdk.org/jeps/438)第五次孵化) ## JEP 429:作用域值(第一次孵化) @@ -38,20 +38,109 @@ ScopedValue.where(V, ) 作用域值允许在大型程序中的组件之间安全有效地共享数据,而无需求助于方法参数。 -关于作用域值的详细介绍,推荐阅读[作用域值常见问题解答](https://www.happycoders.eu/java/scoped-values/)。 +关于作用域值的详细介绍,推荐阅读[作用域值常见问题解答](https://www.happycoders.eu/java/scoped-values/)这篇文章。 ## JEP 432:记录模式(第二次预览) -记录模式(Record Patterns) 可对 record 的值进行解构,可以嵌套记录模式和类型模式,实现强大的、声明性的和可组合的数据导航和处理形式。 +记录模式(Record Patterns) 可对 record 的值进行解构,也就是更方便地从记录类(Record Class)中提取数据。并且,还可以嵌套记录模式和类型模式结合使用,以实现强大的、声明性的和可组合的数据导航和处理形式。 记录模式不能单独使用,而是要与 instanceof 或 switch 模式匹配一同使用。 -记录模式在 Java 19 进行了第一次预览, 由[JEP 405](https://openjdk.org/jeps/405)提出。JDK 20 中是第二次预览,由 [JEP 432](https://openjdk.org/jeps/432) 提出。这次的改进包括: +先以 instanceof 为例简单演示一下。 + +简单定义一个记录类: + +```java +record Shape(String type, long unit){} +``` + +没有记录模式之前: + +```java +Shape circle = new Shape("Circle", 10); +if (circle instanceof Shape shape) { + + System.out.println("Area of " + shape.type() + " is : " + Math.PI * Math.pow(shape.unit(), 2)); +} +``` + +有了记录模式之后: + +```java +Shape circle = new Shape("Circle", 10); +if (circle instanceof Shape(String type, long unit)) { + System.out.println("Area of " + type + " is : " + Math.PI * Math.pow(unit, 2)); +} +``` + +再看看记录模式与 switch 的配合使用。 + +定义一些类: + +```java +interface Shape {} +record Circle(double radius) implements Shape { } +record Square(double side) implements Shape { } +record Rectangle(double length, double width) implements Shape { } +``` + +没有记录模式之前: + +```java +Shape shape = new Circle(10); +switch (shape) { + case Circle c: + System.out.println("The shape is Circle with area: " + Math.PI * c.radius() * c.radius()); + break; + + case Square s: + System.out.println("The shape is Square with area: " + s.side() * s.side()); + break; + + case Rectangle r: + System.out.println("The shape is Rectangle with area: + " + r.length() * r.width()); + break; + + default: + System.out.println("Unknown Shape"); + break; +} +``` + +有了记录模式之后: + +```java +Shape shape = new Circle(10); +switch(shape) { + + case Circle(double radius): + System.out.println("The shape is Circle with area: " + Math.PI * radius * radius); + break; + + case Square(double side): + System.out.println("The shape is Square with area: " + side * side); + break; + + case Rectangle(double length, double width): + System.out.println("The shape is Rectangle with area: + " + length * width); + break; + + default: + System.out.println("Unknown Shape"); + break; +} +``` + +记录模式可以避免不必要的转换,使得代码更建简洁易读。而且,用了记录模式后不必再担心 `null` 或者 `NullPointerException`,代码更安全可靠。 + +记录模式在 Java 19 进行了第一次预览, 由 [JEP 405](https://openjdk.org/jeps/405) 提出。JDK 20 中是第二次预览,由 [JEP 432](https://openjdk.org/jeps/432) 提出。这次的改进包括: - 添加对通用记录模式类型参数推断的支持, - 添加对记录模式的支持以出现在增强语句的标题中`for` - 删除对命名记录模式的支持。 +**注意**:不要把记录模式和 [JDK16](./java16.md) 正式引入的记录类搞混了。 + ## JEP 433:switch 模式匹配(第四次预览) 正如 `instanceof` 一样, `switch` 也紧跟着增加了类型匹配自动转换功能。 @@ -107,7 +196,7 @@ static String formatterPatternSwitch(Object o) { Java 程序可以通过该 API 与 Java 运行时之外的代码和数据进行互操作。通过高效地调用外部函数(即 JVM 之外的代码)和安全地访问外部内存(即不受 JVM 管理的内存),该 API 使 Java 程序能够调用本机库并处理本机数据,而不会像 JNI 那样危险和脆弱。 -外部函数和内存 API 在 Java 17 中进行了第一轮孵化,由 [JEP 412](https://openjdk.java.net/jeps/412) 提出。Java 18 中进行了第二次孵化,由[ JEP 419](https://openjdk.org/jeps/419) 提出。Java 19 中是第一次预览,由 [JEP 424](https://openjdk.org/jeps/424) 提出。 +外部函数和内存 API 在 Java 17 中进行了第一轮孵化,由 [JEP 412](https://openjdk.java.net/jeps/412) 提出。Java 18 中进行了第二次孵化,由[JEP 419](https://openjdk.org/jeps/419) 提出。Java 19 中是第一次预览,由 [JEP 424](https://openjdk.org/jeps/424) 提出。 JDK 20 中是第二次预览,由 [JEP 434](https://openjdk.org/jeps/434) 提出,这次的改进包括: @@ -119,21 +208,72 @@ JDK 20 中是第二次预览,由 [JEP 434](https://openjdk.org/jeps/434) 提 ## JEP 436: 虚拟线程(第二次预览) -虚拟线程(Virtual Thread-)是 JDK 而不是 OS 实现的轻量级线程(Lightweight Process,LWP),许多虚拟线程共享同一个操作系统线程,虚拟线程的数量可以远大于操作系统线程的数量。 +虚拟线程(Virtual Thread)是 JDK 而不是 OS 实现的轻量级线程(Lightweight Process,LWP),由 JVM 调度。许多虚拟线程共享同一个操作系统线程,虚拟线程的数量可以远大于操作系统线程的数量。 -虚拟线程在其他多线程语言中已经被证实是十分有用的,比如 Go 中的 Goroutine、Erlang 中的进程。 +在引入虚拟线程之前,`java.lang.Thread` 包已经支持所谓的平台线程,也就是没有虚拟线程之前,我们一直使用的线程。JVM 调度程序通过平台线程(载体线程)来管理虚拟线程,一个平台线程可以在不同的时间执行不同的虚拟线程(多个虚拟线程挂载在一个平台线程上),当虚拟线程被阻塞或等待时,平台线程可以切换到执行另一个虚拟线程。 + +虚拟线程、平台线程和系统内核线程的关系图如下所示(图源:[How to Use Java 19 Virtual Threads](https://medium.com/javarevisited/how-to-use-java-19-virtual-threads-c16a32bad5f7)): -虚拟线程避免了上下文切换的额外耗费,兼顾了多线程的优点,简化了高并发程序的复杂,可以有效减少编写、维护和观察高吞吐量并发应用程序的工作量。 +![虚拟线程、平台线程和系统内核线程的关系](https://oss.javaguide.cn/github/javaguide/java/new-features/virtual-threads-platform-threads-kernel-threads-relationship.png) -知乎有一个关于 Java 19 虚拟线程的讨论,感兴趣的可以去看看:https://www.zhihu.com/question/536743167 。 +关于平台线程和系统内核线程的对应关系多提一点:在 Windows 和 Linux 等主流操作系统中,Java 线程采用的是一对一的线程模型,也就是一个平台线程对应一个系统内核线程。Solaris 系统是一个特例,HotSpot VM 在 Solaris 上支持多对多和一对一。具体可以参考 R 大的回答: [JVM 中的线程模型是用户级的么?](https://www.zhihu.com/question/23096638/answer/29617153)。 -Java 虚拟线程的详细解读和原理可以看下面这两篇文章: +相比较于平台线程来说,虚拟线程是廉价且轻量级的,使用完后立即被销毁,因此它们不需要被重用或池化,每个任务可以有自己专属的虚拟线程来运行。虚拟线程暂停和恢复来实现线程之间的切换,避免了上下文切换的额外耗费,兼顾了多线程的优点,简化了高并发程序的复杂,可以有效减少编写、维护和观察高吞吐量并发应用程序的工作量。 +虚拟线程在其他多线程语言中已经被证实是十分有用的,比如 Go 中的 Goroutine、Erlang 中的进程。 + +知乎有一个关于 Java 19 虚拟线程的讨论,感兴趣的可以去看看: 。 + +Java 虚拟线程的详细解读和原理可以看下面这几篇文章: + +- [虚拟线程极简入门](https://javaguide.cn/java/concurrent/virtual-thread.html) - [Java19 正式 GA!看虚拟线程如何大幅提高系统吞吐量](https://mp.weixin.qq.com/s/yyApBXxpXxVwttr01Hld6Q) - [虚拟线程 - VirtualThread 源码透视](https://www.cnblogs.com/throwable/p/16758997.html) 虚拟线程在 Java 19 中进行了第一次预览,由[JEP 425](https://openjdk.org/jeps/425)提出。JDK 20 中是第二次预览,做了一些细微变化,这里就不细提了。 +最后,我们来看一下四种创建虚拟线程的方法: + +```java +// 1、通过 Thread.ofVirtual() 创建 +Runnable fn = () -> { + // your code here +}; + +Thread thread = Thread.ofVirtual(fn) + .start(); + +// 2、通过 Thread.startVirtualThread() 、创建 +Thread thread = Thread.startVirtualThread(() -> { + // your code here +}); + +// 3、通过 Executors.newVirtualThreadPerTaskExecutor() 创建 +var executorService = Executors.newVirtualThreadPerTaskExecutor(); + +executorService.submit(() -> { + // your code here +}); + +class CustomThread implements Runnable { + @Override + public void run() { + System.out.println("CustomThread run"); + } +} + +//4、通过 ThreadFactory 创建 +CustomThread customThread = new CustomThread(); +// 获取线程工厂类 +ThreadFactory factory = Thread.ofVirtual().factory(); +// 创建虚拟线程 +Thread thread = factory.newThread(customThread); +// 启动线程 +thread.start(); +``` + +通过上述列举的 4 种创建虚拟线程的方式可以看出,官方为了降低虚拟线程的门槛,尽力复用原有的 `Thread` 线程类,这样可以平滑的过渡到虚拟线程的使用。 + ## JEP 437: 结构化并发(第二次孵化) Java 19 引入了结构化并发,一种多线程编程方法,目的是为了通过结构化并发 API 来简化多线程编程,并不是为了取代`java.util.concurrent`,目前处于孵化器阶段。 @@ -158,7 +298,7 @@ Java 19 引入了结构化并发,一种多线程编程方法,目的是为了 结构化并发非常适合虚拟线程,虚拟线程是 JDK 实现的轻量级线程。许多虚拟线程共享同一个操作系统线程,从而允许非常多的虚拟线程。 -JDK 20 中对结构化并发唯一变化是更新为支持在任务范围内创建的线程`StructuredTaskScope`继承范围值 这简化了跨线程共享不可变数据,详见[JEP 429 ](https://openjdk.org/jeps/429)。 +JDK 20 中对结构化并发唯一变化是更新为支持在任务范围内创建的线程`StructuredTaskScope`继承范围值 这简化了跨线程共享不可变数据,详见[JEP 429](https://openjdk.org/jeps/429)。 ## JEP 432:向量 API(第五次孵化) @@ -169,3 +309,5 @@ JDK 20 中对结构化并发唯一变化是更新为支持在任务范围内创 向量(Vector) API 最初由 [JEP 338](https://openjdk.java.net/jeps/338) 提出,并作为[孵化 API](http://openjdk.java.net/jeps/11)集成到 Java 16 中。第二轮孵化由 [JEP 414](https://openjdk.java.net/jeps/414) 提出并集成到 Java 17 中,第三轮孵化由 [JEP 417](https://openjdk.java.net/jeps/417) 提出并集成到 Java 18 中,第四轮由 [JEP 426](https://openjdk.java.net/jeps/426) 提出并集成到了 Java 19 中。 Java20 的这次孵化基本没有改变向量 API ,只是进行了一些错误修复和性能增强,详见 [JEP 438](https://openjdk.org/jeps/438)。 + + diff --git a/docs/java/new-features/java21.md b/docs/java/new-features/java21.md new file mode 100644 index 00000000000..5f145c23cc5 --- /dev/null +++ b/docs/java/new-features/java21.md @@ -0,0 +1,382 @@ +--- +title: Java 21 新特性概览(重要) +category: Java +tag: + - Java新特性 +--- + +JDK 21 于 2023 年 9 月 19 日 发布,这是一个非常重要的版本,里程碑式。 + +JDK21 是 LTS(长期支持版),至此为止,目前有 JDK8、JDK11、JDK17 和 JDK21 这四个长期支持版了。 + +JDK 21 共有 15 个新特性,这篇文章会挑选其中较为重要的一些新特性进行详细介绍: + +- [JEP 430:String Templates(字符串模板)](https://openjdk.org/jeps/430)(预览) +- [JEP 431:Sequenced Collections(序列化集合)](https://openjdk.org/jeps/431) + +- [JEP 439:Generational ZGC(分代 ZGC)](https://openjdk.org/jeps/439) + +- [JEP 440:Record Patterns(记录模式)](https://openjdk.org/jeps/440) + +- [JEP 441:Pattern Matching for switch(switch 的模式匹配)](https://openjdk.org/jeps/442) + +- [JEP 442:Foreign Function & Memory API(外部函数和内存 API)](https://openjdk.org/jeps/442)(第三次预览) + +- [JEP 443:Unnamed Patterns and Variables(未命名模式和变量](https://openjdk.org/jeps/443)(预览) + +- [JEP 444:Virtual Threads(虚拟线程)](https://openjdk.org/jeps/444) + +- [JEP 445:Unnamed Classes and Instance Main Methods(未命名类和实例 main 方法 )](https://openjdk.org/jeps/445)(预览) + +## JEP 430:字符串模板(预览) + +String Templates(字符串模板) 目前仍然是 JDK 21 中的一个预览功能。 + +String Templates 提供了一种更简洁、更直观的方式来动态构建字符串。通过使用占位符`${}`,我们可以将变量的值直接嵌入到字符串中,而不需要手动处理。在运行时,Java 编译器会将这些占位符替换为实际的变量值。并且,表达式支持局部变量、静态/非静态字段甚至方法、计算结果等特性。 + +实际上,String Templates(字符串模板)再大多数编程语言中都存在: + +```typescript +"Greetings {{ name }}!"; //Angular +`Greetings ${ name }!`; //Typescript +$"Greetings { name }!" //Visual basic +f"Greetings { name }!" //Python +``` + +Java 在没有 String Templates 之前,我们通常使用字符串拼接或格式化方法来构建字符串: + +```java +//concatenation +message = "Greetings " + name + "!"; + +//String.format() +message = String.format("Greetings %s!", name); //concatenation + +//MessageFormat +message = new MessageFormat("Greetings {0}!").format(name); + +//StringBuilder +message = new StringBuilder().append("Greetings ").append(name).append("!").toString(); +``` + +这些方法或多或少都存在一些缺点,比如难以阅读、冗长、复杂。 + +Java 使用 String Templates 进行字符串拼接,可以直接在字符串中嵌入表达式,而无需进行额外的处理: + +```java +String message = STR."Greetings \{name}!"; +``` + +在上面的模板表达式中: + +- STR 是模板处理器。 +- `\{name}`为表达式,运行时,这些表达式将被相应的变量值替换。 + +Java 目前支持三种模板处理器: + +- STR:自动执行字符串插值,即将模板中的每个嵌入式表达式替换为其值(转换为字符串)。 +- FMT:和 STR 类似,但是它还可以接受格式说明符,这些格式说明符出现在嵌入式表达式的左边,用来控制输出的样式。 +- RAW:不会像 STR 和 FMT 模板处理器那样自动处理字符串模板,而是返回一个 `StringTemplate` 对象,这个对象包含了模板中的文本和表达式的信息。 + +```java +String name = "Lokesh"; + +//STR +String message = STR."Greetings \{name}."; + +//FMT +String message = FMT."Greetings %-12s\{name}."; + +//RAW +StringTemplate st = RAW."Greetings \{name}."; +String message = STR.process(st); +``` + +除了 JDK 自带的三种模板处理器外,你还可以实现 `StringTemplate.Processor` 接口来创建自己的模板处理器,只需要继承 `StringTemplate.Processor`接口,然后实现 `process` 方法即可。 + +我们可以使用局部变量、静态/非静态字段甚至方法作为嵌入表达式: + +```java +//variable +message = STR."Greetings \{name}!"; + +//method +message = STR."Greetings \{getName()}!"; + +//field +message = STR."Greetings \{this.name}!"; +``` + +还可以在表达式中执行计算并打印结果: + +```java +int x = 10, y = 20; +String s = STR."\{x} + \{y} = \{x + y}"; //"10 + 20 = 30" +``` + +为了提高可读性,我们可以将嵌入的表达式分成多行: + +```java +String time = STR."The current time is \{ + //sample comment - current time in HH:mm:ss + DateTimeFormatter + .ofPattern("HH:mm:ss") + .format(LocalTime.now()) + }."; +``` + +## JEP431:序列化集合 + +JDK 21 引入了一种新的集合类型:**Sequenced Collections(序列化集合,也叫有序集合)**,这是一种具有确定出现顺序(encounter order)的集合(无论我们遍历这样的集合多少次,元素的出现顺序始终是固定的)。序列化集合提供了处理集合的第一个和最后一个元素以及反向视图(与原始集合相反的顺序)的简单方法。 + +Sequenced Collections 包括以下三个接口: + +- [`SequencedCollection`](https://docs.oracle.com/en/java/javase/21/docs/api/java.base/java/util/SequencedCollection.html) +- [`SequencedSet`](https://docs.oracle.com/en/java/javase/21/docs/api/java.base/java/util/SequencedSet.html) +- [`SequencedMap`](https://docs.oracle.com/en/java/javase/21/docs/api/java.base/java/util/SequencedMap.html) + +`SequencedCollection` 接口继承了 `Collection`接口, 提供了在集合两端访问、添加或删除元素以及获取集合的反向视图的方法。 + +```java +interface SequencedCollection extends Collection { + + // New Method + + SequencedCollection reversed(); + + // Promoted methods from Deque + + void addFirst(E); + void addLast(E); + + E getFirst(); + E getLast(); + + E removeFirst(); + E removeLast(); +} +``` + +`List` 和 `Deque` 接口实现了`SequencedCollection` 接口。 + +这里以 `ArrayList` 为例,演示一下实际使用效果: + +```java +ArrayList arrayList = new ArrayList<>(); + +arrayList.add(1); // List contains: [1] + +arrayList.addFirst(0); // List contains: [0, 1] +arrayList.addLast(2); // List contains: [0, 1, 2] + +Integer firstElement = arrayList.getFirst(); // 0 +Integer lastElement = arrayList.getLast(); // 2 + +List reversed = arrayList.reversed(); +System.out.println(reversed); // Prints [2, 1, 0] +``` + +`SequencedSet`接口直接继承了 `SequencedCollection` 接口并重写了 `reversed()` 方法。 + +```java +interface SequencedSet extends SequencedCollection, Set { + + SequencedSet reversed(); +} +``` + +`SortedSet` 和 `LinkedHashSet` 实现了`SequencedSet`接口。 + +这里以 `LinkedHashSet` 为例,演示一下实际使用效果: + +```java +LinkedHashSet linkedHashSet = new LinkedHashSet<>(List.of(1, 2, 3)); + +Integer firstElement = linkedHashSet.getFirst(); // 1 +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 [4, 3, 2, 1, 0] +``` + +`SequencedMap` 接口继承了 `Map`接口, 提供了在集合两端访问、添加或删除键值对、获取包含 key 的 `SequencedSet`、包含 value 的 `SequencedCollection`、包含 entry(键值对) 的 `SequencedSet`以及获取集合的反向视图的方法。 + +```java +interface SequencedMap extends Map { + + // New Methods + + SequencedMap reversed(); + + SequencedSet sequencedKeySet(); + SequencedCollection sequencedValues(); + SequencedSet> sequencedEntrySet(); + + V putFirst(K, V); + V putLast(K, V); + + + // Promoted Methods from NavigableMap + + Entry firstEntry(); + Entry lastEntry(); + + Entry pollFirstEntry(); + Entry pollLastEntry(); +} +``` + +`SortedMap` 和`LinkedHashMap` 实现了`SequencedMap` 接口。 + +这里以 `LinkedHashMap` 为例,演示一下实际使用效果: + +```java +LinkedHashMap map = new LinkedHashMap<>(); + +map.put(1, "One"); +map.put(2, "Two"); +map.put(3, "Three"); + +map.firstEntry(); //1=One +map.lastEntry(); //3=Three + +System.out.println(map); //{1=One, 2=Two, 3=Three} + +Map.Entry first = map.pollFirstEntry(); //1=One +Map.Entry last = map.pollLastEntry(); //3=Three + +System.out.println(map); //{2=Two} + +map.putFirst(1, "One"); //{1=One, 2=Two} +map.putLast(3, "Three"); //{1=One, 2=Two, 3=Three} + +System.out.println(map); //{1=One, 2=Two, 3=Three} +System.out.println(map.reversed()); //{3=Three, 2=Two, 1=One} +``` + +## JEP 439:分代 ZGC + +JDK21 中对 ZGC 进行了功能扩展,增加了分代 GC 功能。不过,默认是关闭的,需要通过配置打开: + +```bash +// 启用分代ZGC +java -XX:+UseZGC -XX:+ZGenerational ... +``` + +在未来的版本中,官方会把 ZGenerational 设为默认值,即默认打开 ZGC 的分代 GC。在更晚的版本中,非分代 ZGC 就被移除。 + +> In a future release we intend to make Generational ZGC the default, at which point -XX:-ZGenerational will select non-generational ZGC. In an even later release we intend to remove non-generational ZGC, at which point the ZGenerational option will become obsolete. +> +> 在将来的版本中,我们打算将 Generational ZGC 作为默认选项,此时-XX:-ZGenerational 将选择非分代 ZGC。在更晚的版本中,我们打算移除非分代 ZGC,此时 ZGenerational 选项将变得过时。 + +分代 ZGC 可以显著减少垃圾回收过程中的停顿时间,并提高应用程序的响应性能。这对于大型 Java 应用程序和高并发场景下的性能优化非常有价值。 + +## JEP 440:记录模式 + +记录模式在 Java 19 进行了第一次预览, 由 [JEP 405](https://openjdk.org/jeps/405) 提出。JDK 20 中是第二次预览,由 [JEP 432](https://openjdk.org/jeps/432) 提出。最终,记录模式在 JDK21 顺利转正。 + +[Java 20 新特性概览](./java20.md)已经详细介绍过记录模式,这里就不重复了。 + +## JEP 441:switch 的模式匹配 + +增强 Java 中的 switch 表达式和语句,允许在 case 标签中使用模式。当模式匹配时,执行 case 标签对应的代码。 + +在下面的代码中,switch 表达式使用了类型模式来进行匹配。 + +```java +static String formatterPatternSwitch(Object obj) { + return switch (obj) { + case Integer i -> String.format("int %d", i); + case Long l -> String.format("long %d", l); + case Double d -> String.format("double %f", d); + case String s -> String.format("String %s", s); + default -> obj.toString(); + }; +} +``` + +## JEP 442:外部函数和内存 API(第三次预览) + +Java 程序可以通过该 API 与 Java 运行时之外的代码和数据进行互操作。通过高效地调用外部函数(即 JVM 之外的代码)和安全地访问外部内存(即不受 JVM 管理的内存),该 API 使 Java 程序能够调用本机库并处理本机数据,而不会像 JNI 那样危险和脆弱。 + +外部函数和内存 API 在 Java 17 中进行了第一轮孵化,由 [JEP 412](https://openjdk.java.net/jeps/412) 提出。Java 18 中进行了第二次孵化,由[JEP 419](https://openjdk.org/jeps/419) 提出。Java 19 中是第一次预览,由 [JEP 424](https://openjdk.org/jeps/424) 提出。JDK 20 中是第二次预览,由 [JEP 434](https://openjdk.org/jeps/434) 提出。JDK 21 中是第三次预览,由 [JEP 442](https://openjdk.org/jeps/442) 提出。 + +在 [Java 19 新特性概览](./java19.md) 中,我有详细介绍到外部函数和内存 API,这里就不再做额外的介绍了。 + +## JEP 443:未命名模式和变量(预览) + +未命名模式和变量使得我们可以使用下划线 `_` 表示未命名的变量以及模式匹配时不使用的组件,旨在提高代码的可读性和可维护性。 + +未命名变量的典型场景是 `try-with-resources` 语句、 `catch` 子句中的异常变量和`for`循环。当变量不需要使用的时候就可以使用下划线 `_`代替,这样清晰标识未被使用的变量。 + +```java +try (var _ = ScopedContext.acquire()) { + // No use of acquired resource +} +try { ... } +catch (Exception _) { ... } +catch (Throwable _) { ... } + +for (int i = 0, _ = runOnce(); i < arr.length; i++) { + ... +} +``` + +未命名模式是一个无条件的模式,并不绑定任何值。未命名模式变量出现在类型模式中。 + +```java +if (r instanceof ColoredPoint(_, Color c)) { ... c ... } + +switch (b) { + case Box(RedBall _), Box(BlueBall _) -> processBox(b); + case Box(GreenBall _) -> stopProcessing(); + case Box(_) -> pickAnotherBox(); +} +``` + +## JEP 444:虚拟线程 + +虚拟线程是一项重量级的更新,一定一定要重视! + +虚拟线程在 Java 19 中进行了第一次预览,由[JEP 425](https://openjdk.org/jeps/425)提出。JDK 20 中是第二次预览。最终,虚拟线程在 JDK21 顺利转正。 + +[Java 20 新特性概览](./java20.md)已经详细介绍过虚拟线程,这里就不重复了。 + +## JEP 445:未命名类和实例 main 方法 (预览) + +这个特性主要简化了 `main` 方法的的声明。对于 Java 初学者来说,这个 `main` 方法的声明引入了太多的 Java 语法概念,不利于初学者快速上手。 + +没有使用该特性之前定义一个 `main` 方法: + +```java +public class HelloWorld { + public static void main(String[] args) { + System.out.println("Hello, World!"); + } +} +``` + +使用该新特性之后定义一个 `main` 方法: + +```java +class HelloWorld { + void main() { + System.out.println("Hello, World!"); + } +} +``` + +进一步精简(未命名的类允许我们不定义类名): + +```java +void main() { + System.out.println("Hello, World!"); +} +``` + +## 参考 + +- Java 21 String Templates: +- Java 21 Sequenced Collections: diff --git a/docs/java/new-features/java22-23.md b/docs/java/new-features/java22-23.md new file mode 100644 index 00000000000..223c2b7a72c --- /dev/null +++ b/docs/java/new-features/java22-23.md @@ -0,0 +1,429 @@ +--- +title: Java 22 & 23 新特性概览 +category: Java +tag: + - Java新特性 +--- + +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 独有的一些特性。 + +JDK 23 一共有 12 个新特性: + +- [JEP 455: 模式中的原始类型、instanceof 和 switch(预览)](https://openjdk.org/jeps/455) +- [JEP 456: 类文件 API(第二次预览)](https://openjdk.org/jeps/466) +- [JEP 467:Markdown 文档注释](https://openjdk.org/jeps/467) +- [JEP 469:向量 API(第八次孵化)](https://openjdk.org/jeps/469) +- [JEP 473:流收集器(第二次预览)](https://openjdk.org/jeps/473) +- [JEP 471:弃用 sun.misc.Unsafe 中的内存访问方法](https://openjdk.org/jeps/471) +- [JEP 474:ZGC:默认的分代模式](https://openjdk.org/jeps/474) +- [JEP 476:模块导入声明 (预览)](https://openjdk.org/jeps/476) +- [JEP 477:未命名类和实例 main 方法 (第三次预览)](https://openjdk.org/jeps/477) +- [JEP 480:结构化并发 (第三次预览)](https://openjdk.org/jeps/480) +- [JEP 481: 作用域值 (第三次预览)](https://openjdk.org/jeps/481) +- [JEP 482:灵活的构造函数体(第二次预览)](https://openjdk.org/jeps/482) + +JDK 22 的新特性如下: + +![](https://oss.javaguide.cn/github/javaguide/java/new-features/jdk22-new-features.png) + +其中,下面这 3 条新特性我会单独拎出来详细介绍一下: + +- [JEP 423:G1 垃圾收集器区域固定](https://openjdk.org/jeps/423) +- [JEP 454:外部函数与内存 API](https://openjdk.org/jeps/454) +- [JEP 456:未命名模式和变量](https://openjdk.org/jeps/456) +- [JEP 458:启动多文件源代码程序](https://openjdk.org/jeps/458) + +## JDK 23 + +### JEP 455: 模式中的原始类型、instanceof 和 switch(预览) + +在 JEP 455 之前, `instanceof` 只支持引用类型,`switch` 表达式和语句的 `case` 标签只能使用整数字面量、枚举常量和字符串字面量。 + +JEP 455 的预览特性中,`instanceof` 和 `switch` 全面支持所有原始类型,包括 `byte`, `short`, `char`, `int`, `long`, `float`, `double`, `boolean`。 + +```java +// 传统写法 +if (i >= -128 && i <= 127) { + byte b = (byte)i; + ... b ... +} + +// 使用 instanceof 改进 +if (i instanceof byte b) { + ... b ... +} + +long v = ...; +// 传统写法 +if (v == 1L) { + // ... +} else if (v == 2L) { + // ... +} else if (v == 10_000_000_000L) { + // ... +} + +// 使用 long 类型的 case 标签 +switch (v) { + case 1L: + // ... + break; + case 2L: + // ... + break; + case 10_000_000_000L: + // ... + break; + default: + // ... +} +``` + +### JEP 456: 类文件 API(第二次预览) + +类文件 API 在 JDK 22 进行了第一次预览,由 [JEP 457](https://openjdk.org/jeps/457) 提出。 + +类文件 API 的目标是提供一套标准化的 API,用于解析、生成和转换 Java 类文件,取代过去对第三方库(如 ASM)在类文件处理上的依赖。 + +```java +// 创建一个 ClassFile 对象,这是操作类文件的入口。 +ClassFile cf = ClassFile.of(); +// 解析字节数组为 ClassModel +ClassModel classModel = cf.parse(bytes); + +// 构建新的类文件,移除以 "debug" 开头的所有方法 +byte[] newBytes = cf.build(classModel.thisClass().asSymbol(), + classBuilder -> { + // 遍历所有类元素 + for (ClassElement ce : classModel) { + // 判断是否为方法 且 方法名以 "debug" 开头 + if (!(ce instanceof MethodModel mm + && mm.methodName().stringValue().startsWith("debug"))) { + // 添加到新的类文件中 + classBuilder.with(ce); + } + } + }); +``` + +### JEP 467:Markdown 文档注释 + +在 JavaDoc 文档注释中可以使用 Markdown 语法,取代原本只能使用 HTML 和 JavaDoc 标签的方式。 + +Markdown 更简洁易读,减少了手动编写 HTML 的繁琐,同时保留了对 HTML 元素和 JavaDoc 标签的支持。这个增强旨在让 API 文档注释的编写和阅读变得更加轻松,同时不会影响现有注释的解释。Markdown 提供了对常见文档元素(如段落、列表、链接等)的简化表达方式,提升了文档注释的可维护性和开发者体验。 + +![Markdown 文档注释](https://oss.javaguide.cn/github/javaguide/java/new-features/jep467-markdown-documentation-comments.png) + +### JEP 469:向量 API(第八次孵化) + +向量计算由对向量的一系列操作组成。向量 API 用来表达向量计算,该计算可以在运行时可靠地编译为支持的 CPU 架构上的最佳向量指令,从而实现优于等效标量计算的性能。 + +向量 API 的目标是为用户提供简洁易用且与平台无关的表达范围广泛的向量计算。 + +这是对数组元素的简单标量计算: + +```java +void scalarComputation(float[] a, float[] b, float[] c) { + for (int i = 0; i < a.length; i++) { + c[i] = (a[i] * a[i] + b[i] * b[i]) * -1.0f; + } +} +``` + +这是使用 Vector API 进行的等效向量计算: + +```java +static final VectorSpecies SPECIES = FloatVector.SPECIES_PREFERRED; + +void vectorComputation(float[] a, float[] b, float[] c) { + int i = 0; + int upperBound = SPECIES.loopBound(a.length); + for (; i < upperBound; i += SPECIES.length()) { + // FloatVector va, vb, vc; + var va = FloatVector.fromArray(SPECIES, a, i); + var vb = FloatVector.fromArray(SPECIES, b, i); + var vc = va.mul(va) + .add(vb.mul(vb)) + .neg(); + vc.intoArray(c, i); + } + for (; i < a.length; i++) { + c[i] = (a[i] * a[i] + b[i] * b[i]) * -1.0f; + } +} +``` + +### JEP 473:流收集器(第二次预览) + +流收集器在 JDK 22 进行了第一次预览,由 [JEP 461](https://openjdk.org/jeps/457) 提出。 + +这个改进使得 Stream API 可以支持自定义中间操作。 + +```java +source.gather(a).gather(b).gather(c).collect(...) +``` + +### JEP 471:弃用 sun.misc.Unsafe 中的内存访问方法 + +JEP 471 提议弃用 `sun.misc.Unsafe` 中的内存访问方法,这些方法将来的版本中会被移除。 + +这些不安全的方法已有安全高效的替代方案: + +- `java.lang.invoke.VarHandle` :JDK 9 (JEP 193) 中引入,提供了一种安全有效地操作堆内存的方法,包括对象的字段、类的静态字段以及数组元素。 +- `java.lang.foreign.MemorySegment` :JDK 22 (JEP 454) 中引入,提供了一种安全有效地访问堆外内存的方法,有时会与 `VarHandle` 协同工作。 + +这两个类是 Foreign Function & Memory API(外部函数和内存 API) 的核心组件,分别用于管理和操作堆外内存。Foreign Function & Memory API 在 JDK 22 中正式转正,成为标准特性。 + +```java +import jdk.incubator.foreign.*; +import java.lang.invoke.VarHandle; + +// 管理堆外整数数组的类 +class OffHeapIntBuffer { + + // 用于访问整数元素的VarHandle + private static final VarHandle ELEM_VH = ValueLayout.JAVA_INT.arrayElementVarHandle(); + + // 内存管理器 + private final Arena arena; + + // 堆外内存段 + private final MemorySegment buffer; + + // 构造函数,分配指定数量的整数空间 + public OffHeapIntBuffer(long size) { + this.arena = Arena.ofShared(); + this.buffer = arena.allocate(ValueLayout.JAVA_INT, size); + } + + // 释放内存 + public void deallocate() { + arena.close(); + } + + // 以volatile方式设置指定索引的值 + public void setVolatile(long index, int value) { + ELEM_VH.setVolatile(buffer, 0L, index, value); + } + + // 初始化指定范围的元素为0 + public void initialize(long start, long n) { + buffer.asSlice(ValueLayout.JAVA_INT.byteSize() * start, + ValueLayout.JAVA_INT.byteSize() * n) + .fill((byte) 0); + } + + // 将指定范围的元素复制到新数组 + public int[] copyToNewArray(long start, int n) { + return buffer.asSlice(ValueLayout.JAVA_INT.byteSize() * start, + ValueLayout.JAVA_INT.byteSize() * n) + .toArray(ValueLayout.JAVA_INT); + } +} +``` + +### JEP 474:ZGC:默认的分代模式 + +Z 垃圾回收器 (ZGC) 的默认模式切换为分代模式,并弃用非分代模式,计划在未来版本中移除。这是因为分代 ZGC 是大多数场景下的更优选择。 + +### JEP 476:模块导入声明 (预览) + +模块导入声明允许在 Java 代码中简洁地导入整个模块的所有导出包,而无需逐个声明包的导入。这一特性简化了模块化库的重用,特别是在使用多个模块时,避免了大量的包导入声明,使得开发者可以更方便地访问第三方库和 Java 基本类。 + +此特性对初学者和原型开发尤为有用,因为它无需开发者将自己的代码模块化,同时保留了对传统导入方式的兼容性,提升了开发效率和代码可读性。 + +```java +// 导入整个 java.base 模块,开发者可以直接访问 List、Map、Stream 等类,而无需每次手动导入相关包 +import module java.base; + +public class Example { + public static void main(String[] args) { + String[] fruits = { "apple", "berry", "citrus" }; + Map fruitMap = Stream.of(fruits) + .collect(Collectors.toMap( + s -> s.toUpperCase().substring(0, 1), + Function.identity())); + + System.out.println(fruitMap); + } +} +``` + +### JEP 477:未命名类和实例 main 方法 (第三次预览) + +这个特性主要简化了 `main` 方法的的声明。对于 Java 初学者来说,这个 `main` 方法的声明引入了太多的 Java 语法概念,不利于初学者快速上手。 + +没有使用该特性之前定义一个 `main` 方法: + +```java +public class HelloWorld { + public static void main(String[] args) { + System.out.println("Hello, World!"); + } +} +``` + +使用该新特性之后定义一个 `main` 方法: + +```java +class HelloWorld { + void main() { + System.out.println("Hello, World!"); + } +} +``` + +进一步简化(未命名的类允许我们省略类名) + +```java +void main() { + System.out.println("Hello, World!"); +} +``` + +### JEP 480:结构化并发 (第三次预览) + +Java 19 引入了结构化并发,一种多线程编程方法,目的是为了通过结构化并发 API 来简化多线程编程,并不是为了取代`java.util.concurrent`,目前处于孵化器阶段。 + +结构化并发将不同线程中运行的多个任务视为单个工作单元,从而简化错误处理、提高可靠性并增强可观察性。也就是说,结构化并发保留了单线程代码的可读性、可维护性和可观察性。 + +结构化并发的基本 API 是[`StructuredTaskScope`](https://download.java.net/java/early_access/loom/docs/api/jdk.incubator.concurrent/jdk/incubator/concurrent/StructuredTaskScope.html)。`StructuredTaskScope` 支持将任务拆分为多个并发子任务,在它们自己的线程中执行,并且子任务必须在主任务继续之前完成。 + +`StructuredTaskScope` 的基本用法如下: + +```java + try (var scope = new StructuredTaskScope()) { + // 使用fork方法派生线程来执行子任务 + Future future1 = scope.fork(task1); + Future future2 = scope.fork(task2); + // 等待线程完成 + scope.join(); + // 结果的处理可能包括处理或重新抛出异常 + ... process results/exceptions ... + } // close +``` + +结构化并发非常适合虚拟线程,虚拟线程是 JDK 实现的轻量级线程。许多虚拟线程共享同一个操作系统线程,从而允许非常多的虚拟线程。 + +### JEP 481:作用域值 (第三次预览) + +作用域值(Scoped Values)可以在线程内和线程间共享不可变的数据,优于线程局部变量,尤其是在使用大量虚拟线程时。 + +```java +final static ScopedValue<...> V = new ScopedValue<>(); + +// In some method +ScopedValue.where(V, ) + .run(() -> { ... V.get() ... call methods ... }); + +// In a method called directly or indirectly from the lambda expression +... V.get() ... +``` + +作用域值允许在大型程序中的组件之间安全有效地共享数据,而无需求助于方法参数。 + +### JEP 482:灵活的构造函数体(第二次预览) + +这个特性最初在 JDK 22 由 [JEP 447: Statements before super(...) (Preview)](https://openjdk.org/jeps/447)提出。 + +Java 要求在构造函数中,`super(...)` 或 `this(...)` 调用必须作为第一条语句出现。这意味着我们无法在调用父类构造函数之前在子类构造函数中直接初始化字段。 + +灵活的构造函数体解决了这一问题,它允许在构造函数体内,在调用 `super(..)` 或 `this(..)` 之前编写语句,这些语句可以初始化字段,但不能引用正在构造的实例。这样可以防止在父类构造函数中调用子类方法时,子类的字段未被正确初始化,增强了类构造的可靠性。 + +这一特性解决了之前 Java 语法限制了构造函数代码组织的问题,让开发者能够更自由、更自然地表达构造函数的行为,例如在构造函数中直接进行参数验证、准备和共享,而无需依赖辅助方法或构造函数,提高了代码的可读性和可维护性。 + +```java +class Person { + private final String name; + private int age; + + public Person(String name, int age) { + if (age < 0) { + throw new IllegalArgumentException("Age cannot be negative."); + } + this.name = name; // 在调用父类构造函数之前初始化字段 + this.age = age; + // ... 其他初始化代码 + } +} + +class Employee extends Person { + private final int employeeId; + + public Employee(String name, int age, int employeeId) { + this.employeeId = employeeId; // 在调用父类构造函数之前初始化字段 + super(name, age); // 调用父类构造函数 + // ... 其他初始化代码 + } +} +``` + +## JDK 22 + +### JEP 423:G1 垃圾收集器区域固定 + +JEP 423 提出在 G1 垃圾收集器中实现区域固定(Region Pinning)功能,旨在减少由于 Java Native Interface (JNI) 关键区域导致的延迟问题。 + +JNI 关键区域内的对象不能在垃圾收集时被移动,因此 G1 以往通过禁用垃圾收集解决该问题,导致线程阻塞及严重的延迟。通过在 G1 的老年代和年轻代中引入区域固定机制,允许在关键区域内固定对象所在的内存区域,同时继续回收未固定的区域,避免了禁用垃圾回收的需求。这种改进有助于显著降低延迟,提升系统在与 JNI 交互时的吞吐量和稳定性。 + +### JEP 454:外部函数和内存 API + +Java 程序可以通过该 API 与 Java 运行时之外的代码和数据进行互操作。通过高效地调用外部函数(即 JVM 之外的代码)和安全地访问外部内存(即不受 JVM 管理的内存),该 API 使 Java 程序能够调用本机库并处理本机数据,而不会像 JNI 那样危险和脆弱。 + +外部函数和内存 API 在 Java 17 中进行了第一轮孵化,由 [JEP 412](https://openjdk.java.net/jeps/412) 提出。Java 18 中进行了第二次孵化,由[JEP 419](https://openjdk.org/jeps/419) 提出。Java 19 中是第一次预览,由 [JEP 424](https://openjdk.org/jeps/424) 提出。JDK 20 中是第二次预览,由 [JEP 434](https://openjdk.org/jeps/434) 提出。JDK 21 中是第三次预览,由 [JEP 442](https://openjdk.org/jeps/442) 提出。 + +最终,该特性在 JDK 22 中顺利转正。 + +在 [Java 19 新特性概览](./java19.md) 中,我有详细介绍到外部函数和内存 API,这里就不再做额外的介绍了。 + +### JEP 456:未命名模式和变量 + +未命名模式和变量在 JDK 21 中由 [JEP 443](https://openjdk.org/jeps/443)提出预览,JDK 22 中就已经转正。 + +关于这个新特性的详细介绍,可以看看[Java 21 新特性概览(重要)](./java21.md)这篇文章中的介绍。 + +### JEP 458:启动多文件源代码程序 + +Java 11 引入了 [JEP 330:启动单文件源代码程序](https://openjdk.org/jeps/330),增强了 `java` 启动器的功能,使其能够直接运行单个 Java 源文件。通过命令 `java HelloWorld.java`,Java 可以在内存中隐式编译源代码并立即执行,而不需要在磁盘上生成 `.class` 文件。这简化了开发者在编写小型工具程序或学习 Java 时的工作流程,避免了手动编译的额外步骤。 + +假设文件`Prog.java`声明了两个类: + +```java +class Prog { + public static void main(String[] args) { Helper.run(); } +} + +class Helper { + static void run() { System.out.println("Hello!"); } +} +``` + +`java Prog.java`命令会在内存中编译两个类并执行`main`该文件中声明的第一个类的方法。 + +这种方式有一个限制,程序的所有源代码必须放在一个`.java`文件中。 + +[JEP 458:启动多文件源代码程序](https://openjdk.org/jeps/458) 是对 JEP 330 功能的扩展,允许直接运行由多个 Java 源文件组成的程序,而无需显式的编译步骤。 + +假设一个目录中有两个 Java 源文件 `Prog.java` 和 `Helper.java`,每个文件各自声明了一个类: + +```java +// Prog.java +class Prog { + public static void main(String[] args) { Helper.run(); } +} + +// Helper.java +class Helper { + static void run() { System.out.println("Hello!"); } +} +``` + +当你运行命令 `java Prog.java` 时,Java 启动器会在内存中编译并执行 `Prog` 类的 `main` 方法。由于 `Prog` 类中的代码引用了 `Helper` 类,启动器会自动在文件系统中找到 `Helper.java` 文件,编译其中的 `Helper` 类,并在内存中执行它。这个过程是自动的,开发者无需显式调用 `javac` 来编译所有源文件。 + +这一特性使得从小型项目到大型项目的过渡更加平滑,开发者可以自由选择何时引入构建工具,避免在快速迭代时被迫设置复杂的项目结构。该特性消除了单文件的限制,进一步简化了从单一文件到多文件程序的开发过程,特别适合原型开发、快速实验以及早期项目的探索阶段。 diff --git a/docs/java/new-features/java24.md b/docs/java/new-features/java24.md new file mode 100644 index 00000000000..4b8df7e2317 --- /dev/null +++ b/docs/java/new-features/java24.md @@ -0,0 +1,255 @@ +--- +title: Java 24 新特性概览 +category: Java +tag: + - Java新特性 +--- + +[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 实现的轻量级线程。许多虚拟线程共享同一个操作系统线程,从而允许非常多的虚拟线程。 diff --git a/docs/java/new-features/java8-common-new-features.md b/docs/java/new-features/java8-common-new-features.md index 301573edcb3..e402ba5a882 100644 --- a/docs/java/new-features/java8-common-new-features.md +++ b/docs/java/new-features/java8-common-new-features.md @@ -1,7 +1,14 @@ -# Java8 新特性实战 +--- +title: Java8 新特性实战 +category: Java +tag: + - Java新特性 +--- > 本文来自[cowbi](https://github.com/cowbi)的投稿~ + + Oracle 于 2014 发布了 Java8(jdk1.8),诸多原因使它成为目前市场上使用最多的 jdk 版本。虽然发布距今已将近 7 年,但很多程序员对其新特性还是不够了解,尤其是用惯了 Java8 之前版本的老程序员,比如我。 为了不脱离队伍太远,还是有必要对这些新特性做一些总结梳理。它较 jdk.7 有很多变化或者说是优化,比如 interface 里可以有静态方法,并且可以有方法体,这一点就颠覆了之前的认知;`java.util.HashMap` 数据结构里增加了红黑树;还有众所周知的 Lambda 表达式等等。本文不能把所有的新特性都给大家一一分享,只列出比较常用的新特性给大家做详细讲解。更多相关内容请看[官网关于 Java8 的新特性的介绍](https://www.oracle.com/java/technologies/javase/8-whats-new.html)。 @@ -88,9 +95,7 @@ public class InterfaceNewImpl implements InterfaceNew , InterfaceNew1{ 在 java 8 中专门有一个包放函数式接口`java.util.function`,该包下的所有接口都有 `@FunctionalInterface` 注解,提供函数式编程。 -在其他包中也有函数式接口,其中一些没有`@FunctionalInterface` 注解,但是只要符合函数式接口的定义就是函数式接口,与是否有 - -`@FunctionalInterface`注解无关,注解只是在编译时起到强制规范定义的作用。其在 Lambda 表达式中有广泛的应用。 +在其他包中也有函数式接口,其中一些没有`@FunctionalInterface` 注解,但是只要符合函数式接口的定义就是函数式接口,与是否有`@FunctionalInterface`注解无关,注解只是在编译时起到强制规范定义的作用。其在 Lambda 表达式中有广泛的应用。 ## Lambda 表达式 @@ -204,7 +209,7 @@ void lamndaFor() { strings.forEach((s) -> System.out.println(s)); //or strings.forEach(System.out::println); - //map + //map Map map = new HashMap<>(); map.forEach((k,v)->System.out.println(v)); } @@ -602,60 +607,89 @@ public static T requireNonNull(T obj) { `ofNullable` 方法和`of`方法唯一区别就是当 value 为 null 时,`ofNullable` 返回的是`EMPTY`,of 会抛出 `NullPointerException` 异常。如果需要把 `NullPointerException` 暴漏出来就用 `of`,否则就用 `ofNullable`。 -### `map()`相关方法。 +**`map()` 和 `flatMap()` 有什么区别的?** + +`map` 和 `flatMap` 都是将一个函数应用于集合中的每个元素,但不同的是`map`返回一个新的集合,`flatMap`是将每个元素都映射为一个集合,最后再将这个集合展平。 + +在实际应用场景中,如果`map`返回的是数组,那么最后得到的是一个二维数组,使用`flatMap`就是为了将这个二维数组展平变成一个一维数组。 ```java -/** -* 如果value为null,返回EMPTY,否则返回Optional封装的参数值 -*/ -public Optional map(Function mapper) { - Objects.requireNonNull(mapper); - if (!isPresent()) - return empty(); - else { - return Optional.ofNullable(mapper.apply(value)); - } -} -/** -* 如果value为null,返回EMPTY,否则返回Optional封装的参数值,如果参数值返回null会抛 NullPointerException -*/ -public Optional flatMap(Function> mapper) { - Objects.requireNonNull(mapper); - if (!isPresent()) - return empty(); - else { - return Objects.requireNonNull(mapper.apply(value)); - } +public class MapAndFlatMapExample { + public static void main(String[] args) { + List listOfArrays = Arrays.asList( + new String[]{"apple", "banana", "cherry"}, + new String[]{"orange", "grape", "pear"}, + new String[]{"kiwi", "melon", "pineapple"} + ); + + List mapResult = listOfArrays.stream() + .map(array -> Arrays.stream(array).map(String::toUpperCase).toArray(String[]::new)) + .collect(Collectors.toList()); + + System.out.println("Using map:"); + mapResult.forEach(arrays-> System.out.println(Arrays.toString(arrays))); + + List flatMapResult = listOfArrays.stream() + .flatMap(array -> Arrays.stream(array).map(String::toUpperCase)) + .collect(Collectors.toList()); + + System.out.println("Using flatMap:"); + System.out.println(flatMapResult); + } } + ``` -**`map()` 和 `flatMap()` 有什么区别的?** +运行结果: -**1.参数不一样,`map` 的参数上面看到过,`flatMap` 的参数是这样** +```plain +Using map: +[[APPLE, BANANA, CHERRY], [ORANGE, GRAPE, PEAR], [KIWI, MELON, PINEAPPLE]] -```java -class ZooFlat { - private DogFlat dog = new DogFlat(); +Using flatMap: +[APPLE, BANANA, CHERRY, ORANGE, GRAPE, PEAR, KIWI, MELON, PINEAPPLE] +``` - public DogFlat getDog() { - return dog; - } - } +最简单的理解就是`flatMap()`可以将`map()`的结果展开。 + +在`Optional`里面,当使用`map()`时,如果映射函数返回的是一个普通值,它会将这个值包装在一个新的`Optional`中。而使用`flatMap`时,如果映射函数返回的是一个`Optional`,它会将这个返回的`Optional`展平,不再包装成嵌套的`Optional`。 -class DogFlat { - private int age = 1; - public Optional getAge() { - return Optional.ofNullable(age); +下面是一个对比的示例代码: + +```java +public static void main(String[] args) { + int userId = 1; + + // 使用flatMap的代码 + String cityUsingFlatMap = getUserById(userId) + .flatMap(OptionalExample::getAddressByUser) + .map(Address::getCity) + .orElse("Unknown"); + + System.out.println("User's city using flatMap: " + cityUsingFlatMap); + + // 不使用flatMap的代码 + Optional> optionalAddress = getUserById(userId) + .map(OptionalExample::getAddressByUser); + + String cityWithoutFlatMap; + if (optionalAddress.isPresent()) { + Optional
addressOptional = optionalAddress.get(); + if (addressOptional.isPresent()) { + Address address = addressOptional.get(); + cityWithoutFlatMap = address.getCity(); + } else { + cityWithoutFlatMap = "Unknown"; + } + } else { + cityWithoutFlatMap = "Unknown"; } -} -ZooFlat zooFlat = new ZooFlat(); -Optional.ofNullable(zooFlat).map(o -> o.getDog()).flatMap(d -> d.getAge()).ifPresent(age -> - System.out.println(age) -); + System.out.println("User's city without flatMap: " + cityWithoutFlatMap); + } ``` -**2.`flatMap()` 参数返回值如果是 null 会抛 `NullPointerException`,而 `map()` 返回`EMPTY`。** +在`Stream`和`Optional`中正确使用`flatMap`可以减少很多不必要的代码。 ### 判断 value 是否为 null @@ -733,7 +767,7 @@ public Optional filter(Predicate predicate) { ### 小结 -看完 `Optional` 源码,`Optional` 的方法真的非常简单,值得注意的是如果坚决不想看见 `NPE`,就不要用 `of() `、 `get()`、`flatMap(..)`。最后再综合用一下 `Optional` 的高频方法。 +看完 `Optional` 源码,`Optional` 的方法真的非常简单,值得注意的是如果坚决不想看见 `NPE`,就不要用 `of()`、 `get()`、`flatMap(..)`。最后再综合用一下 `Optional` 的高频方法。 ```java Optional.ofNullable(zoo).map(o -> o.getDog()).map(d -> d.getAge()).filter(v->v==1).orElse(3); @@ -879,7 +913,7 @@ public void pushWeek(){ + period.getYears() + "年" + period.getMonths() + "月" + period.getDays() + "天"); - //打印结果是 “date1 到 date2 相隔:0年9月27天” + //打印结果是 “date1 到 date2 相隔:0年9月27天” //这里period.getDays()得到的天是抛去年月以外的天数,并不是总天数 //如果要获取纯粹的总天数应该用下面的方法 long day = date2.toEpochDay() - date1.toEpochDay(); @@ -976,7 +1010,7 @@ System.out.println(date); //Wed Jan 27 14:05:29 CST 2021 ``` -在新特性中引入了 `java.time.ZonedDateTime ` 来表示带时区的时间。它可以看成是 `LocalDateTime + ZoneId`。 +在新特性中引入了 `java.time.ZonedDateTime` 来表示带时区的时间。它可以看成是 `LocalDateTime + ZoneId`。 ```java //当前时区时间 @@ -1018,3 +1052,5 @@ System.out.println("本地时区时间: " + localZoned); - Date time-api 这些都是开发当中比较常用的特性。梳理下来发现它们真香,而我却没有更早的应用。总觉得学习 java 8 新特性比较麻烦,一直使用老的实现方式。其实这些新特性几天就可以掌握,一但掌握,效率会有很大的提高。其实我们涨工资也是涨的学习的钱,不学习终究会被淘汰,35 岁危机会提前来临。 + + diff --git a/docs/java/new-features/java8-tutorial-translate.md b/docs/java/new-features/java8-tutorial-translate.md index 54197f4ff2f..9e0fd04ec70 100644 --- a/docs/java/new-features/java8-tutorial-translate.md +++ b/docs/java/new-features/java8-tutorial-translate.md @@ -7,7 +7,7 @@ 欢迎阅读我对 Java 8 的介绍。本教程将逐步指导您完成所有新语言功能。 在简短的代码示例的基础上,您将学习如何使用默认接口方法,lambda 表达式,方法引用和可重复注释。 在本文的最后,您将熟悉最新的 API 更改,如流,函数式接口(Functional Interfaces),Map 类的扩展和新的 Date API。 没有大段枯燥的文字,只有一堆注释的代码片段。 -### 接口的默认方法(Default Methods for Interfaces) +## 接口的默认方法(Default Methods for Interfaces) Java 8 使我们能够通过使用 `default` 关键字向接口添加非抽象方法实现。 此功能也称为[虚拟扩展方法](http://stackoverflow.com/a/24102730)。 @@ -51,7 +51,7 @@ formula 是作为匿名对象实现的。该代码非常容易理解,6 行代 **译者注:** 不管是抽象类还是接口,都可以通过匿名内部类的方式访问。不能通过抽象类或者接口直接创建对象。对于上面通过匿名内部类方式访问接口,我们可以这样理解:一个内部类实现了接口里的抽象方法并且返回一个内部类对象,之后我们让接口的引用来指向这个对象。 -### Lambda 表达式(Lambda expressions) +## Lambda 表达式(Lambda expressions) 首先看看在老版本的 Java 中是如何排列字符串的: @@ -66,7 +66,7 @@ Collections.sort(names, new Comparator() { }); ``` -只需要给静态方法` Collections.sort` 传入一个 List 对象以及一个比较器来按指定顺序排列。通常做法都是创建一个匿名的比较器对象然后将其传递给 `sort` 方法。 +只需要给静态方法`Collections.sort` 传入一个 List 对象以及一个比较器来按指定顺序排列。通常做法都是创建一个匿名的比较器对象然后将其传递给 `sort` 方法。 在 Java 8 中你就没必要使用这种传统的匿名对象的方式了,Java 8 提供了更简洁的语法,lambda 表达式: @@ -90,13 +90,13 @@ names.sort((a, b) -> b.compareTo(a)); List 类本身就有一个 `sort` 方法。并且 Java 编译器可以自动推导出参数类型,所以你可以不用再写一次类型。接下来我们看看 lambda 表达式还有什么其他用法。 -### 函数式接口(Functional Interfaces) +## 函数式接口(Functional Interfaces) **译者注:** 原文对这部分解释不太清楚,故做了修改! Java 语言设计者们投入了大量精力来思考如何使现有的函数友好地支持 Lambda。最终采取的方法是:增加函数式接口的概念。**“函数式接口”是指仅仅只包含一个抽象方法,但是可以有多个非抽象方法(也就是上面提到的默认方法)的接口。** 像这样的接口,可以被隐式转换为 lambda 表达式。`java.lang.Runnable` 与 `java.util.concurrent.Callable` 是函数式接口最典型的两个例子。Java 8 增加了一种特殊的注解`@FunctionalInterface`,但是这个注解通常不是必须的(某些情况建议使用),只要接口只包含一个抽象方法,虚拟机会自动判断该接口为函数式接口。一般建议在接口上使用`@FunctionalInterface` 注解进行声明,这样的话,编译器如果发现你标注了这个注解的接口有多于一个抽象方法的时候会报错的,如下图所示 -![@FunctionalInterface 注解](https://my-blog-to-use.oss-cn-beijing.aliyuncs.com/2019-2/@FunctionalInterface.png) +![@FunctionalInterface 注解](https://oss.javaguide.cn/github/javaguide/java/@FunctionalInterface.png) 示例: @@ -116,7 +116,7 @@ public interface Converter { **译者注:** 大部分函数式接口都不用我们自己写,Java8 都给我们实现好了,这些接口都在 java.util.function 包里。 -### 方法和构造函数引用(Method and Constructor References) +## 方法和构造函数引用(Method and Constructor References) 前一节中的代码还可以通过静态方法引用来表示: @@ -176,9 +176,9 @@ Person person = personFactory.create("Peter", "Parker"); 我们只需要使用 `Person::new` 来获取 Person 类构造函数的引用,Java 编译器会自动根据`PersonFactory.create`方法的参数类型来选择合适的构造函数。 -### Lambda 表达式作用域(Lambda Scopes) +## Lambda 表达式作用域(Lambda Scopes) -#### 访问局部变量 +### 访问局部变量 我们可以直接在 lambda 表达式中访问外部的局部变量: @@ -209,9 +209,9 @@ Converter stringConverter = num = 3;//在lambda表达式中试图修改num同样是不允许的。 ``` -#### 访问字段和静态变量 +### 访问字段和静态变量 -与局部变量相比,我们对 lambda 表达式中的实例字段和静态变量都有读写访问权限。 该行为和匿名对象是一致的。 +与局部变量相比,我们在 lambda 表达式中对实例字段和静态变量都有读写访问权限。 该行为和匿名对象是一致的。 ```java class Lambda4 { @@ -232,7 +232,7 @@ class Lambda4 { } ``` -#### 访问默认接口方法 +### 访问默认接口方法 还记得第一节中的 formula 示例吗? `Formula` 接口定义了一个默认方法`sqrt`,可以从包含匿名对象的每个 formula 实例访问该方法。 这不适用于 lambda 表达式。 @@ -242,13 +242,13 @@ class Lambda4 { Formula formula = (a) -> sqrt(a * 100); ``` -### 内置函数式接口(Built-in Functional Interfaces) +## 内置函数式接口(Built-in Functional Interfaces) JDK 1.8 API 包含许多内置函数式接口。 其中一些接口在老版本的 Java 中是比较常见的比如:`Comparator` 或`Runnable`,这些接口都增加了`@FunctionalInterface`注解以便能用在 lambda 表达式上。 但是 Java 8 API 同样还提供了很多全新的函数式接口来让你的编程工作更加方便,有一些接口是来自 [Google Guava](https://code.google.com/p/guava-libraries/) 库里的,即便你对这些很熟悉了,还是有必要看看这些是如何扩展到 lambda 上使用的。 -#### Predicate +### Predicate Predicate 接口是只有一个参数的返回布尔类型值的 **断言型** 接口。该接口包含多种默认方法来将 Predicate 组合成其他复杂的逻辑(比如:与,或,非): @@ -301,7 +301,7 @@ Predicate isEmpty = String::isEmpty; Predicate isNotEmpty = isEmpty.negate(); ``` -#### Function +### Function Function 接口接受一个参数并生成结果。默认方法可用于将多个函数链接在一起(compose, andThen): @@ -341,7 +341,7 @@ Function backToString = toInteger.andThen(String::valueOf); backToString.apply("123"); // "123" ``` -#### Supplier +### Supplier Supplier 接口产生给定泛型类型的结果。 与 Function 接口不同,Supplier 接口不接受参数。 @@ -350,7 +350,7 @@ Supplier personSupplier = Person::new; personSupplier.get(); // new Person ``` -#### Consumer +### Consumer Consumer 接口表示要对单个输入参数执行的操作。 @@ -359,7 +359,7 @@ Consumer greeter = (p) -> System.out.println("Hello, " + p.firstName); greeter.accept(new Person("Luke", "Skywalker")); ``` -#### Comparator +### Comparator Comparator 是老 Java 中的经典接口, Java 8 在此之上添加了多种默认方法: @@ -398,7 +398,7 @@ optional.ifPresent((s) -> System.out.println(s.charAt(0))); // "b" ## Streams(流) -`java.util.Stream` 表示能应用在一组元素上一次执行的操作序列。Stream 操作分为中间操作或者最终操作两种,最终操作返回一特定类型的计算结果,而中间操作返回 Stream 本身,这样你就可以将多个操作依次串起来。Stream 的创建需要指定一个数据源,比如` java.util.Collection` 的子类,List 或者 Set, Map 不支持。Stream 的操作可以串行执行或者并行执行。 +`java.util.Stream` 表示能应用在一组元素上一次执行的操作序列。Stream 操作分为中间操作或者最终操作两种,最终操作返回一特定类型的计算结果,而中间操作返回 Stream 本身,这样你就可以将多个操作依次串起来。Stream 的创建需要指定一个数据源,比如`java.util.Collection` 的子类,List 或者 Set, Map 不支持。Stream 的操作可以串行执行或者并行执行。 首先看看 Stream 是怎么用,首先创建实例代码需要用到的数据 List: @@ -572,7 +572,7 @@ long millis = TimeUnit.NANOSECONDS.toMillis(t1 - t0); System.out.println(String.format("sequential sort took: %d ms", millis)); ``` -``` +```plain 1000000 sequential sort took: 709 ms//串行排序所用的时间 ``` @@ -595,7 +595,7 @@ System.out.println(String.format("parallel sort took: %d ms", millis)); ```java 1000000 -parallel sort took: 475 ms//串行排序所用的时间 +parallel sort took: 475 ms//并行排序所用的时间 ``` 上面两个代码几乎是一样的,但是并行版的快了 50% 左右,唯一需要做的改动就是将 `stream()` 改为`parallelStream()`。 @@ -884,7 +884,7 @@ Hint[] hints2 = Person.class.getAnnotationsByType(Hint.class); System.out.println(hints2.length); // 2 ``` -即便我们没有在 `Person`类上定义 `@Hints`注解,我们还是可以通过 `getAnnotation(Hints.class) `来获取 `@Hints`注解,更加方便的方法是使用 `getAnnotationsByType` 可以直接获取到所有的`@Hint`注解。 +即便我们没有在 `Person`类上定义 `@Hints`注解,我们还是可以通过 `getAnnotation(Hints.class)`来获取 `@Hints`注解,更加方便的方法是使用 `getAnnotationsByType` 可以直接获取到所有的`@Hint`注解。 另外 Java 8 的注解还增加到两种新的 target 上了: ```java @@ -895,3 +895,5 @@ System.out.println(hints2.length); // 2 ## Where to go from here? 关于 Java 8 的新特性就写到这了,肯定还有更多的特性等待发掘。JDK 1.8 里还有很多很有用的东西,比如`Arrays.parallelSort`, `StampedLock`和`CompletableFuture`等等。 + + diff --git a/docs/java/new-features/java9.md b/docs/java/new-features/java9.md index 85c488f887d..8fbce002f9d 100644 --- a/docs/java/new-features/java9.md +++ b/docs/java/new-features/java9.md @@ -5,9 +5,9 @@ tag: - Java新特性 --- -**Java 9** 发布于 2017 年 9 月 21 日 。作为 Java 8 之后 3 年半才发布的新版本,Java 9 带来了很多重大的变化其中最重要的改动是 Java 平台模块系统的引入,其他还有诸如集合、`Stream` 流......。 +**Java 9** 发布于 2017 年 9 月 21 日 。作为 Java 8 之后 3 年半才发布的新版本,Java 9 带来了很多重大的变化其中最重要的改动是 Java 平台模块系统的引入,其他还有诸如集合、`Stream` 流……。 -你可以在 [Archived OpenJDK General-Availability Releases](http://jdk.java.net/archive/) 上下载自己需要的 JDK 版本!官方的新特性说明文档地址:https://openjdk.java.net/projects/jdk/ 。 +你可以在 [Archived OpenJDK General-Availability Releases](http://jdk.java.net/archive/) 上下载自己需要的 JDK 版本!官方的新特性说明文档地址: 。 **概览(精选了一部分)**: @@ -29,14 +29,14 @@ JShell 是 Java 9 新增的一个实用工具。为 Java 提供了类似于 Pyth 1. 降低了输出第一行 Java 版"Hello World!"的门槛,能够提高新手的学习热情。 2. 在处理简单的小逻辑,验证简单的小问题时,比 IDE 更有效率(并不是为了取代 IDE,对于复杂逻辑的验证,IDE 更合适,两者互补)。 -3. ...... +3. …… **JShell 的代码和普通的可编译代码,有什么不一样?** 1. 一旦语句输入完成,JShell 立即就能返回执行的结果,而不再需要编辑器、编译器、解释器。 2. JShell 支持变量的重复声明,后面声明的会覆盖前面声明的。 3. JShell 支持独立的表达式比如普通的加法运算 `1 + 1`。 -4. ...... +4. …… ## 模块化系统 @@ -138,7 +138,7 @@ try (scanner;writer) { **什么是 effectively-final 变量?** 简单来说就是没有被 `final` 修饰但是值在初始化后从未更改的变量。 -正如上面的代码所演示的那样,即使 `writer` 变量没有被显示声明为 `final`,但它在第一次被复制后就不会改变了,因此,它就是 effectively-final 变量。 +正如上面的代码所演示的那样,即使 `writer` 变量没有被显示声明为 `final`,但它在第一次被赋值后就不会改变了,因此,它就是 effectively-final 变量。 ## Stream & Optional 增强 @@ -230,7 +230,7 @@ System.out.println(currentProcess.info()); `Flow` 中包含了 `Flow.Publisher`、`Flow.Subscriber`、`Flow.Subscription` 和 `Flow.Processor` 等 4 个核心接口。Java 9 还提供了`SubmissionPublisher` 作为`Flow.Publisher` 的一个实现。 -关于 Java 9 响应式流更详细的解读,推荐你看 [Java 9 揭秘(17. Reactive Streams )- 林本托 ](https://www.cnblogs.com/IcanFixIt/p/7245377.html) 这篇文章。 +关于 Java 9 响应式流更详细的解读,推荐你看 [Java 9 揭秘(17. Reactive Streams )- 林本托](https://www.cnblogs.com/IcanFixIt/p/7245377.html) 这篇文章。 ## 变量句柄 @@ -244,16 +244,18 @@ System.out.println(currentProcess.info()); - **平台日志 API 改进**:Java 9 允许为 JDK 和应用配置同样的日志实现。新增了 `System.LoggerFinder` 用来管理 JDK 使 用的日志记录器实现。JVM 在运行时只有一个系统范围的 `LoggerFinder` 实例。我们可以通过添加自己的 `System.LoggerFinder` 实现来让 JDK 和应用使用 SLF4J 等其他日志记录框架。 - **`CompletableFuture`类增强**:新增了几个新的方法(`completeAsync` ,`orTimeout` 等)。 -- **Nashorn 引擎的增强**:Nashorn 从 Java8 开始引入的 JavaScript 引擎,Java9 对 Nashorn 做了些增强,实现了一些 ES6 的新特性(Java 11 中已经被弃用)。 +- **Nashorn 引擎的增强**:Nashorn 是从 Java8 开始引入的 JavaScript 引擎,Java9 对 Nashorn 做了些增强,实现了一些 ES6 的新特性(Java 11 中已经被弃用)。 - **I/O 流的新特性**:增加了新的方法来读取和复制 `InputStream` 中包含的数据。 - **改进应用的安全性能**:Java 9 新增了 4 个 SHA- 3 哈希算法,SHA3-224、SHA3-256、SHA3-384 和 SHA3-512。 - **改进方法句柄(Method Handle)**:方法句柄从 Java7 开始引入,Java9 在类`java.lang.invoke.MethodHandles` 中新增了更多的静态方法来创建不同类型的方法句柄。 -- ...... +- …… ## 参考 -- Java version history:https://en.wikipedia.org/wiki/Java_version_history -- Release Notes for JDK 9 and JDK 9 Update Releases : https://www.oracle.com/java/technologies/javase/9-all-relnotes.html +- Java version history: +- Release Notes for JDK 9 and JDK 9 Update Releases : - 《深入剖析 Java 新特性》-极客时间 - JShell:怎么快速验证简单的小问题? -- New Features in Java 9:https://www.baeldung.com/new-java-9 -- Java – Try with Resources:https://www.baeldung.com/java-try-with-resources +- New Features in Java 9: +- Java – Try with Resources: + + diff --git a/docs/javaguide/contribution-guideline.md b/docs/javaguide/contribution-guideline.md index ea0079b53d0..0c9e8df0ef1 100644 --- a/docs/javaguide/contribution-guideline.md +++ b/docs/javaguide/contribution-guideline.md @@ -8,9 +8,11 @@ icon: guide 你可以从下面几个方向来做贡献: -- 笔记内容大多是手敲,所以难免会有笔误,你可以帮我找错别字。 -- 很多知识点我可能没有涉及到,所以你可以对其他知识点进行补充。 -- 现有的知识点难免存在不完善或者错误,所以你可以对已有知识点进行修改/补充。 +- 修改错别字,毕竟内容基本都是手敲,难免会有笔误。 +- 对原有内容进行修改完善,例如对某个面试问题的答案进行完善、对某篇文章的内容进行完善。 +- 新增内容,例如新增面试常问的问题、添加重要知识点的详解。 + +目前的贡献奖励也比较丰富和完善,对于多次贡献的用户,有耳机、键盘等实物奖励以及现金奖励! 一定一定一定要注意 **排版规范**: @@ -18,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 的话,强烈推荐阅读下面这些资料: @@ -25,3 +28,5 @@ icon: guide - [《如何向开源社区提问题》](https://github.com/seajs/seajs/issues/545) - [《如何有效地报告 Bug》](http://www.chiark.greenend.org.uk/~sgtatham/bugs-cn.html) - [《如何向开源项目提交无法解答的问题》](https://zhuanlan.zhihu.com/p/25795393)。 + +另外,你可以参考学习别人的文章,但一定一定一定不能复制粘贴别人的内容,努力比别人写的更容易理解,用自己的话讲出来,适当简化表达,突出重点! diff --git a/docs/javaguide/faq.md b/docs/javaguide/faq.md index 25a6a720d53..37f9bc3a94d 100644 --- a/docs/javaguide/faq.md +++ b/docs/javaguide/faq.md @@ -40,7 +40,7 @@ JavaGuide 这个项目诞生一年左右就有出版社的老师联系我了, - 开源版本更容易维护和修改,也能让更多人更方便地参与到项目的建设中,这也是我最初做这个项目的初衷。 - 我觉得出书是一件神圣的事情,自认能力还不够。 - 个人精力有限,不光有本职工作,还弄了一个[知识星球](https://javaguide.cn/about-the-author/zhishixingqiu-two-years.html)赚点外快,还要维护完善 JavaGuide。 -- ...... +- …… 这几年一直在默默完善,真心希望 JavaGuide 越来越好,帮助到更多朋友!也欢迎大家参与进来! diff --git a/docs/javaguide/intro.md b/docs/javaguide/intro.md index 58e95ea4f67..9eadc8a8949 100644 --- a/docs/javaguide/intro.md +++ b/docs/javaguide/intro.md @@ -21,11 +21,7 @@ icon: about 加油!奥利给! -## 学习建议 - -JavaGuide 整体目录规划已经非常清晰了,你可以从头开始学习,也可以根据自身情况来选择性地学习。 - -## 知识星球 +## 官方知识星球 对于准备面试的同学来说,强烈推荐我创建的一个纯粹的[Java 面试知识星球](../about-the-author/zhishixingqiu-two-years.md),干货非常多,学习氛围也很不错! diff --git a/docs/javaguide/use-suggestion.md b/docs/javaguide/use-suggestion.md new file mode 100644 index 00000000000..e7ce843aaee --- /dev/null +++ b/docs/javaguide/use-suggestion.md @@ -0,0 +1,27 @@ +--- +title: 使用建议 +category: 走近项目 +icon: star +--- + +**对于不准备面试的同学来说** ,本文档倾向于给你提供一个比较详细的学习路径,目录清晰,让你对于 Java 整体的知识体系有一个清晰认识。你可以跟着视频、书籍或者官方文档学习完某个知识点之后,然后来这里找对应的总结,帮助你更好地掌握对应的知识点。甚至说,你在有编程基础的情况下,想要学习某个知识点的话,可以直接看我的总结,这样学习效率会非常高。 + +**对于准备面试的同学来说** ,本文档涵盖 Java 程序员所需要掌握的核心知识的常见面试问题总结。 + +大部分人看 JavaGuide 应该都是为了准备技术八股文。**那如何才能更高效地准备技术八股文?** + +对于技术八股文来说,尽量不要死记硬背,这种方式非常枯燥且对自身能力提升有限!但是!想要一点不背是不太现实的,只是说要结合实际应用场景和实战来理解记忆。 + +我一直觉得面试八股文最好是和实际应用场景和实战相结合。很多同学现在的方向都错了,上来就是直接背八股文,硬生生学成了文科,那当然无趣了。 + +举个例子:你的项目中需要用到 Redis 来做缓存,你对照着官网简单了解并实践了简单使用 Redis 之后,你去看了 Redis 对应的八股文。你发现 Redis 可以用来做限流、分布式锁,于是你去在项目中实践了一下并掌握了对应的八股文。紧接着,你又发现 Redis 内存不够用的情况下,还能使用 Redis Cluster 来解决,于是你就又去实践了一下并掌握了对应的八股文。 + +而且, **面试中有水平的面试官都是根据你的项目经历来顺带着问一些技术八股文** 。 + +举个例子:你的项目用到了消息队列,那面试官可能就会问你:为什么使用消息队列?项目中什么模块用到了消息队列?如何保证消息不丢失?如何保证消息的顺序性?(结合你使用的具体的消息队列来准备)……。 + +**一定要记住你的主要目标是理解和记关键词,而不是像背课文一样一字一句地记下来!** + +另外,记录博客或者用自己的理解把对应的知识点讲给别人听也是一个不错的选择。 + +最后,准备技术面试的同学一定要定期复习(自测的方式非常好),不然确实会遗忘的。 diff --git a/docs/open-source-project/readme.md b/docs/open-source-project/README.md similarity index 88% rename from docs/open-source-project/readme.md rename to docs/open-source-project/README.md index 36a4fc7e721..bb8a79f43a9 100644 --- a/docs/open-source-project/readme.md +++ b/docs/open-source-project/README.md @@ -7,6 +7,8 @@ category: 开源项目 精选 GitHub 和 Gitee 上优质的 Java 开源项目。 +灵感来源于[awesome-java](https://github.com/akullpp/awesome-java) 这个项目,可以看作是这个项目的中文本土版本,项目类型更全面且加入了更多中文开源项目。 + 欢迎大家在项目 [issues 区](https://github.com/CodingDocs/awesome-java/issues)推荐自己认可的 Java 开源项目,让我们共同维护一个优质的 Java 开源项目精选集! - GitHub 地址:[https://github.com/CodingDocs/awesome-java](https://github.com/CodingDocs/awesome-java) @@ -16,7 +18,7 @@ category: 开源项目 另外,我的公众号还会定期分享优质开源项目,每月一期,每一期我都会精选 5 个高质量的 Java 开源项目。 -目前已经更新到了第 19 期: +目前已经更新到了第 24 期: 1. [一款基于 Spring Boot + Vue 的一站式开源持续测试平台](http://mp.weixin.qq.com/s?__biz=Mzg2OTA0Njk0OA==&mid=2247515383&idx=1&sn=ba7244020c05d966b483d8c302d54e85&chksm=cea1f33cf9d67a2a111bcf6cadc3cc1c44828ba2302cd3e13bbd88349e43d4254808e6434133&scene=21#wechat_redirect)。 2. [用 Java 写个沙盒塔防游戏!已上架 Steam,Apple Store](https://mp.weixin.qq.com/s?__biz=Mzg2OTA0Njk0OA==&mid=2247515981&idx=1&sn=e4b9c06af65f739bdcdf76bdc35d59f6&chksm=cea1f086f9d679908bd6604b1c42d67580160d9789951f3707ad2f5de4d97aa72121d8fe777e&token=435278690&lang=zh_CN&scene=21#wechat_redirect) @@ -37,6 +39,10 @@ category: 开源项目 17. [3.2k!这是我见过最强的消息推送平台!!](https://mp.weixin.qq.com/s/heag76H4UwZmr8oBY_2gcw) 18. [好家伙,又一本技术书籍开源了!!](https://mp.weixin.qq.com/s/w-JuBlcqCeAZR0xUFWzvHQ) 19. [开箱即用的 ChatGPT Java SDK!支持 GPT3.5、 GPT4 API](https://mp.weixin.qq.com/s/WhI2K1VF0h_57TEVGCwuCA) +20. [这是我见过最强大的技术社区实战项目!!](https://mp.weixin.qq.com/s/tdBQ0Td_Gsev4AaIlq5ltg) +21. [颜值吊打 Postman,这款开源 API 调试工具我超爱!!](https://mp.weixin.qq.com/s/_KXBGckyS--P97G48zXCrw) +22. [轻量级 Spring,够优雅!!](https://mp.weixin.qq.com/s/tl2539hsYsvEm8wjmQwDEg) +23. [这是我见过最强的 Java 版内网穿透神器!](https://mp.weixin.qq.com/s/4hyQsTICIUf9EvAVrC6wEg) 推荐你在我的公众号“**JavaGuide**”回复“**开源**”在线阅读[「优质开源项目推荐」](https://mp.weixin.qq.com/mp/appmsgalbum?__biz=Mzg2OTA0Njk0OA==&action=getalbum&album_id=1345382825083895808&scene=173&from_msgid=2247516459&from_itemidx=1&count=3&nolastread=1#wechat_redirect)系列。 diff --git a/docs/open-source-project/machine-learning.md b/docs/open-source-project/machine-learning.md index 9dd7664a343..6ace75ac622 100644 --- a/docs/open-source-project/machine-learning.md +++ b/docs/open-source-project/machine-learning.md @@ -1,10 +1,21 @@ --- -title: Java 优质开源机器学习项目 +title: Java 优质开源 AI 项目 category: 开源项目 icon: a-MachineLearning --- -- **[Deeplearning4j](https://github.com/eclipse/deeplearning4j)**:Deeplearning4j 是第一个为 Java 和 Scala 编写的商业级,开源,分布式深度学习库。 -- **[Smile](https://github.com/haifengl/smile)**:基于 Java 和 Scala 的机器学习库。 +由于 Java 在 AI 领域目前的应用较少,因此相关的开源项目也非常少。 -相关阅读:[Java 能用于机器学习和数据科学吗?-InfoQ](https://www.infoq.cn/article/GA9UeYlv8ohBzBso9eph) +## 基础框架 + +- [Spring AI](https://github.com/spring-projects/spring-ai):人工智能工程应用框架,为开发 AI 应用程序提供了 Spring 友好的 API 和抽象。 +- [Spring AI Alibaba](https://github.com/alibaba/spring-ai-alibaba):一款 Java 语言实现的 AI 应用开发框架,旨在简化 Java AI 应用程序开发,让 Java 开发者像使用 Spring 开发普通应用一样开发 AI 应用。 +- [LangChain4j](https://github.com/langchain4j/langchain4j):LangChiain 的 Java 版本,用于简化将 LLM(Large Language Model,大语言模型) 集成到 Java 应用程序的过程。 +- [Deeplearning4j](https://github.com/eclipse/deeplearning4j):Deeplearning4j 是第一个为 Java 和 Scala 编写的商业级,开源,分布式深度学习库。 +- [Smile](https://github.com/haifengl/smile):基于 Java 和 Scala 的机器学习库。 +- [GdxAI](https://github.com/libgdx/gdx-ai):完全用 Java 编写的人工智能框架,用于使用 libGDX 进行游戏开发。 + +## 实战 + +- [springboot-openai-chatgpt](https://github.com/274056675/springboot-openai-chatgpt):一个基于 SpringCloud 微服务架构,已对接 GPT-3.5、GPT-4.0、百度文心一言、Midjourney 绘图等等。 +- [ai-beehive](https://github.com/hncboy/ai-beehive):AI 蜂巢,基于 Java 使用 Spring Boot 3 和 JDK 17,支持的功能有 ChatGPT、OpenAi Image、Midjourney、NewBing、文心一言等等。 diff --git a/docs/open-source-project/practical-project.md b/docs/open-source-project/practical-project.md index e57f191d5b6..1c5e2d70dbb 100644 --- a/docs/open-source-project/practical-project.md +++ b/docs/open-source-project/practical-project.md @@ -4,60 +4,75 @@ category: 开源项目 icon: project --- -## 快速开发脚手架 +## 快速开发平台 -- [Snowy](https://gitee.com/xiaonuobase/snowy):国内首个国密前后端分离快速开发平台,定位不是深度封装的框架,也不是无代码平台,更不是某个领域的产品。详细介绍:[5.1k!这是我见过最强的前后端分离快速开发脚手架!!](https://mp.weixin.qq.com/s?__biz=Mzg2OTA0Njk0OA==&mid=2247534316&idx=1&sn=69938397674fc33ecda43c8c9d0a4039&chksm=cea10927f9d68031bc862485c6be984ade5af233d4d871d498c38f22164a84314678c0c67cd7&token=1464380539&lang=zh_CN#rd)。 +- [Snowy](https://gitee.com/xiaonuobase/snowy):国内首个国密前后端分离快速开发平台。详细介绍:[5.1k!这是我见过最强的前后端分离快速开发脚手架!!](https://mp.weixin.qq.com/s?__biz=Mzg2OTA0Njk0OA==&mid=2247534316&idx=1&sn=69938397674fc33ecda43c8c9d0a4039&chksm=cea10927f9d68031bc862485c6be984ade5af233d4d871d498c38f22164a84314678c0c67cd7&token=1464380539&lang=zh_CN#rd)。 +- [eladmin](https://github.com/elunez/eladmin) : 前后端分离的后台管理系统,项目采用分模块开发方式, 权限控制采用 RBAC,支持数据字典与数据权限管理,支持一键生成前后端代码,支持动态路由。 - [RuoYi](https://gitee.com/y_project/RuoYi):RuoYi 一款基于基于 SpringBoot 的权限管理系统 易读易懂、界面简洁美观,直接运行即可用 。 - [AgileBoot-Back-End](https://github.com/valarchie/AgileBoot-Back-End):基于 Ruoyi 做了大量重构优化的基础快速开发框架。 -- [RuoYi-Vue-Pro](https://github.com/YunaiV/ruoyi-vue-pro):RuoYi-Vue 全新 Pro 版本,优化重构所有功能。 +- [SmartAdmin](https://gitee.com/lab1024/smart-admin) : 一套简洁、易用的低代码中后台解决方案。 +- [EuBackend](https://gitee.com/zhaoeryu/eu-backend):基于 SpringBoot 开发的轻量级快速开发平台。 +- [RuoYi-Vue-Pro](https://github.com/YunaiV/ruoyi-vue-pro):RuoYi-Vue 全新 Pro 版本,优化重构所有功能,支持数据权限、SaaS 多租户、Flowable 工作流、三方登录、支付等功能。 +- [RuoYi-Vue-Plus](https://gitee.com/dromara/RuoYi-Vue-Plus):RuoYi-Vue 全新 Plus 版本,重写了 RuoYi-Vue 所有功能,集成了 Sa-Token、Mybatis-Plus、Jackson、SpringDoc、Hutool、OSS 定期同步等。 +- [pig](https://gitee.com/log4j/pig "pig"):基于 Spring Boot + Spring Cloud + OAuth2 的 RBAC 权限管理系统。 - [Guns](https://gitee.com/stylefeng/guns):现代化的 Java 应用开发基础框架。 - [JeecgBoot](https://github.com/zhangdaiscott/jeecg-boot):一款基于代码生成器的 J2EE 低代码快速开发平台,支持生成前后端分离架构的项目。 - [Erupt](https://gitee.com/erupt/erupt) : 低代码全栈类框架,它使用 Java 注解 动态生成页面以及增、删、改、查、权限控制等后台功能。 -- [SmartAdmin](https://gitee.com/lab1024/smart-admin) : 一套简洁、易用的低代码中后台解决方案。 - [BallCat](https://github.com/ballcat-projects/ballcat):一个功能完善的快速开发脚手架!除了最基本的权限管理,定时任务功能之外,还额外支持 XSS 过滤,SQL 防注入、数据脱敏等多种功能 -- [JHipster](https://github.com/jhipster/generator-jhipster) :开源应用程序平台,可在几秒钟内创建 Spring Boot + Angular / React 项目! - -相关阅读: - -- [听说你要接私活?Guide 连夜整理了 5 个开源免费的 Java 项目快速开发脚手架。](https://sourl.cn/cFyLTR) -- [解放双手,再来推荐 5 个 Java 项目开发快速开发脚手架!项目经验和私活都不愁了!](https://sourl.cn/StkiAv) +- [JHipster](https://github.com/jhipster/generator-jhipster) :开源应用程序平台,可在几秒钟内创建 Spring Boot + Angular / React 项目。 ## 博客/论坛系统 下面这几个项目都是非常适合 Spring Boot 初学者学习的,下面的大部分项目的总体代码架构我都看过,个人觉得还算不错,不会误导没有实际做过项目的朋友。 -- [forest](https://github.com/rymcu):下一代的知识社区系统,可以自定义专题和作品集。后端基于 SpringBoot + Shrio + MyBatis + JWT + Redis,前端基于 Vue + NuxtJS + Element-UI。 -- [vhr](https://github.com/lenve/vhr "vhr"):微人事是一个前后端分离的人力资源管理系统,项目采用 SpringBoot+Vue 开发。 -- [favorites-web](https://github.com/cloudfavorites/favorites-web) :云收藏 Spring Boot 2.X 开源项目。云收藏是一个使用 Spring Boot 构建的开源网站,可以让用户在线随时随地收藏的一个网站,在网站上分类整理收藏的网站或者文章。 +- [paicoding](https://github.com/itwanger/paicoding):一款好用又强大的开源社区,基于 Spring Boot 系列主流技术栈,附详细的教程。 +- [forest](https://github.com/rymcu/forest):下一代的知识社区系统,可以自定义专题和作品集。后端基于 SpringBoot + Shrio + MyBatis + JWT + Redis,前端基于 Vue + NuxtJS + Element-UI。 - [community](https://github.com/codedrinker/community):开源论坛、问答系统,现有功能提问、回复、通知、最新、最热、消除零回复功能。功能持续更新中…… 技术栈 Spring、Spring Boot、MyBatis、MySQL/H2、Bootstrap。 +- [OneBlog](https://gitee.com/yadong.zhang/DBlog):简洁美观、功能强大并且自适应的博客系统,支持广告位、SEO、实时通讯等功能。 - [VBlog](https://github.com/lenve/VBlog):V 部落,Vue+SpringBoot 实现的多用户博客管理平台! -- [My-Blog](https://github.com/ZHENFENG13/My-Blog):My Blog 是由 SpringBoot + Mybatis + Thymeleaf 等技术实现的 Java 博客系统,页面美观、功能齐全、部署简单及完善的代码,一定会给使用者无与伦比的体验。 +- [My-Blog](https://github.com/ZHENFENG13/My-Blog): SpringBoot + Mybatis + Thymeleaf 等技术实现的 Java 博客系统,页面美观、功能齐全、部署简单及完善的代码,一定会给使用者无与伦比的体验。 + +## Wiki/文档系统 + +- [zyplayer-doc](https://gitee.com/dromara/zyplayer-doc):适合团队和个人私有化部署使用的知识库、笔记、WIKI 文档管理工具,同时还包含数据库管理、Api 接口管理等模块。 +- [kkFileView](https://gitee.com/kekingcn/file-online-preview):文档在线预览解决方案,支持几乎所有主流文档格式预览,例如 doc、docx、ppt、pptx、wps、xls、xlsx、zip、rar、ofd、xmind、bpmn 、eml 、epub、3ds、dwg、psd 、mp4、mp3 等等。 + +## 文件管理系统/网盘 + +- [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)等文件类型。 ## 考试/刷题系统 -- [uexam](https://gitee.com/mindskip/uexam):一个非常不错的考试系统!考试系统应用场景还挺多的,不论是对于在校大学生还是已经工作的小伙伴,并且,类似的私活也有很多。相关阅读:[《好一个 Spring Boot 开源在线考试系统!解决了我的燃眉之急》](http://link.zhihu.com/?target=https%3A//mp.weixin.qq.com/s%3F__biz%3DMzg2OTA0Njk0OA%3D%3D%26mid%3D2247491585%26idx%3D1%26sn%3D8d3c6768c22e72d6bfcbeee9624886a7%26chksm%3Dcea1afcaf9d626dc918760289c37025ad526f6255786bc198d2402203df64c873ad7934f58df%26scene%3D178%26cur_album_id%3D1345382825083895808%23rd) 。 -- [PassJava-Platform](https://github.com/Jackson0714/PassJava-Platform):一个基于微服务(SpringBoot、Spring Cloud)的面试刷题系统!相关阅读:[《一个基于 Spring Cloud 的面试刷题系统。面试、毕设、项目经验一网打尽》](http://link.zhihu.com/?target=https%3A//mp.weixin.qq.com/s%3F__biz%3DMzg2OTA0Njk0OA%3D%3D%26mid%3D2247497045%26idx%3D1%26sn%3D577175bfd6c040a0df5a494fce6f9758%26chksm%3Dcea1ba9ef9d633883a2e213c0fb9a88bdc87051347d4b3fad2c2befb65d8b16e1ea81d8146dd%26scene%3D178%26cur_album_id%3D1345382825083895808%23rd)。 +- [PlayEdu](https://github.com/PlayEdu/PlayEdu):一款适用于搭建内部培训平台的开源系统,旨在为企业/机构打造自己品牌的内部培训平台。 +- [HOJ](https://gitee.com/himitzh0730/hoj):分布式架构的在线测评平台 OJ ,功能非常全面,支持刷题、训练、比赛、评测等功能。 +- [VOJ](https://github.com/simplefanC/voj):基于微服务架构的高性能在线评测系统。拥有本地判题服务,同时支持其它知名 OJ (HDU、POJ...) 的远程判题。采用现阶段流行技术实现,采用 Docker 容器化部署。 +- [OnlineJudge](https://github.com/SDUOJ/OnlineJudge):基于微服务架构的在线评测系统,支持多种国际赛制支持(ICPC/OI/IOI),采用 Docker 容器化部署。 +- [sg-exam](https://gitee.com/wells2333/sg-exam):方便易用、高颜值的教学管理平台,提供多租户、权限管理、考试、练习、在线学习等功能。 +- [uexam](https://gitee.com/mindskip/uexam):功能全面的在线考试系统,开发部署简单快捷、界面设计友好、代码结构清晰。相关阅读:[好一个 Spring Boot 开源在线考试系统!解决了我的燃眉之急](http://link.zhihu.com/?target=https%3A//mp.weixin.qq.com/s%3F__biz%3DMzg2OTA0Njk0OA%3D%3D%26mid%3D2247491585%26idx%3D1%26sn%3D8d3c6768c22e72d6bfcbeee9624886a7%26chksm%3Dcea1afcaf9d626dc918760289c37025ad526f6255786bc198d2402203df64c873ad7934f58df%26scene%3D178%26cur_album_id%3D1345382825083895808%23rd) 。 +- [PassJava-Platform](https://github.com/Jackson0714/PassJava-Platform):基于微服务架构的面试刷题小程序!相关阅读:[一个基于 Spring Cloud 的面试刷题系统。面试、毕设、项目经验一网打尽](http://link.zhihu.com/?target=https%3A//mp.weixin.qq.com/s%3F__biz%3DMzg2OTA0Njk0OA%3D%3D%26mid%3D2247497045%26idx%3D1%26sn%3D577175bfd6c040a0df5a494fce6f9758%26chksm%3Dcea1ba9ef9d633883a2e213c0fb9a88bdc87051347d4b3fad2c2befb65d8b16e1ea81d8146dd%26scene%3D178%26cur_album_id%3D1345382825083895808%23rd)。 ## 商城系统 下面的商城系统大多比较复杂比如 mall ,如果没有 Java 基础和 Spring Boot 都还没有摸熟的话不推荐过度研究下面几个项目或者使用这些项目当作毕业设计。 +- [congomall](https://gitee.com/nageoffer/congomall):不一样的 TOC 商城系统,SpringCloud-Alibaba 微服务架构设计,基于 DDD 领域驱动模型开发,代码设计优雅,涵盖商城核心业务。 - [mall](https://github.com/macrozheng/mall "mall"):mall 项目是一套电商系统,包括前台商城系统及后台管理系统,基于 SpringBoot+MyBatis 实现。 - [mall-swarm](https://github.com/macrozheng/mall-swarm "mall-swarm") : mall-swarm 是一套微服务商城系统,采用了 Spring Cloud Greenwich、Spring Boot 2、MyBatis、Docker、Elasticsearch 等核心技术,同时提供了基于 Vue 的管理后台方便快速搭建系统。 -- [onemall](https://github.com/YunaiV/onemall):mall 商城,基于微服务的思想,构建在 B2C 电商场景下的项目实战。核心技术栈,是 Spring Boot + Dubbo 。未来,会重构成 Spring Cloud Alibaba 。 - [litemall](https://github.com/linlinjava/litemall "litemall"):又一个小商城。litemall = Spring Boot 后端 + Vue 管理员前端 + 微信小程序用户前端 + Vue 用户移动端。 -- [xmall](https://github.com/Exrick/xmall) :基于 SOA 架构的分布式电商购物商城 前后端分离 前台商城:Vue 全家桶 后台管理系统:Spring/Dubbo/SSM/Elasticsearch/Redis/MySQL/ActiveMQ/Shiro/Zookeeper 等 - [newbee-mall](https://github.com/newbee-ltd/newbee-mall) :newbee-mall 项目(新蜂商城)是一套电商系统,包括 newbee-mall 商城系统及 newbee-mall-admin 商城后台管理系统,基于 Spring Boot 2.X 及相关技术栈开发。 +## 售票系统 + +- [12306](https://gitee.com/nageoffer/12306) :基于 JDK17 + SpringBoot3 + SpringCloud 微服务架构的高并发 12306 购票服务。 + ## 权限管理系统 权限管理系统在企业级的项目中一般都是非常重要的,如果你需求去实际了解一个不错的权限系统是如何设计的话,推荐你可以参考下面这些开源项目。 -- [Spring-Cloud-Admin](https://github.com/wxiaoqi/Spring-Cloud-Admin "Spring-Cloud-Admin"):Cloud-Admin 是国内首个基于 Spring Cloud 微服务化开发平台,具有统一授权、认证后台管理系统,其中包含具备用户管理、资源权限管理、网关 API 管理等多个模块,支持多业务系统并行开发,可以作为后端服务的开发脚手架。代码简洁,架构清晰,适合学习和直接项目中使用。核心技术采用 Spring Boot2 以及 Spring Cloud Gateway 相关核心组件,前端采用 vue-element-admin 组件。 -- [pig](https://gitee.com/log4j/pig "pig"):(gitee)基于 Spring Boot 2.2、 Spring Cloud Hoxton & Alibaba、 OAuth2 的 RBAC 权限管理系统。 -- [FEBS-Shiro](https://github.com/wuyouzhuguli/FEBS-Shiro "FEBS-Shiro"):Spring Boot 2.1.3,Shiro1.4.0 & Layui 2.5.4 权限管理系统。 -- [eladmin](https://github.com/elunez/eladmin) : 项目基于 Spring Boot 2.1.0、 Jpa、 Spring Security、redis、Vue 的前后端分离的后台管理系统,项目采用分模块开发方式, 权限控制采用 RBAC,支持数据字典与数据权限管理,支持一键生成前后端代码,支持动态路由。 -- [SpringBoot-Shiro-Vue](https://github.com/Heeexy/SpringBoot-Shiro-Vue):提供一套基于 Spring Boot-Shiro-Vue 的权限管理思路.前后端都加以控制,做到按钮/接口级别的权限。 +- [SpringBoot-Shiro-Vue](https://github.com/Heeexy/SpringBoot-Shiro-Vue):基于 Spring Boot-Shiro-Vue 的权限管理思路,前后端都加以控制,可以做到按钮/接口级别的权限。 +- [renren-security](https://gitee.com/renrenio/renren-security):一套灵活的权限控制系统,可控制到页面或按钮,满足绝大部分的权限需求 ## 造轮子 diff --git a/docs/open-source-project/system-design.md b/docs/open-source-project/system-design.md index 2e8c1a6a354..5471f2d07b3 100644 --- a/docs/open-source-project/system-design.md +++ b/docs/open-source-project/system-design.md @@ -6,12 +6,61 @@ icon: "xitongsheji" ## 基础框架 -- [Spring Boot](https://github.com/spring-projects/spring-boot "spring-boot"):Spring Boot 可以轻松创建独立的生产级基于 Spring 的应用程序,内置 web 服务器让你可以像运行普通 Java 程序一样运行项 目。另外,大部分 Spring Boot 项目只需要少量的配置即可,这有别于 Spring 的重配置。 +### Web 框架 + +- [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。 + +### 微服务/云原生 + +- [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 文档 + +- [Swagger](https://swagger.io/) :较主流的 RESTful 风格的 API 文档工具,提供了一套工具和规范,让开发人员能够更轻松地创建和维护可读性强、易于使用和交互的 API 文档。 +- [Knife4j](https://doc.xiaominfo.com/):集 Swagger2 和 OpenAPI3 为一体的增强解决方案。 + +### Bean 映射 + +- [MapStruct](https://github.com/mapstruct/mapstruct)(推荐):满足 JSR269 规范的一个 Java 注解处理器,用于为 Java Bean 生成类型安全且高性能的映射。它基于编译阶段生成 get/set 代码,此实现过程中没有反射,不会造成额外的性能损失。 +- [JMapper](https://github.com/jmapper-framework/jmapper-core) : 一个高性能且易于使用的 Bean 映射框架。 + +### 其他 + - [Guice](https://github.com/google/guice):Google 开源的一个轻量级依赖注入框架,相当于一个功能极简化的轻量级 Spring Boot。在某些情况下非常实用,就比如说我们的项目只需要使用依赖注入,不需要 AOP 等功能特性。 -- [SOFABoot](https://github.com/sofastack/sofa-boot):SOFABoot 基于 Spring Boot ,不过在其基础上增加了 Readiness Check,类隔离,日志空间隔离等等能力。 配套提供的还有:SOFARPC(RPC 框架)、SOFABolt(基于 Netty 的远程通信框架)、SOFARegistry(注册中心)...详情请参考:[SOFAStack](https://github.com/sofastack) 。 -- [Spring Batch](https://github.com/spring-projects/spring-batch) : Spring Batch 是一个轻量级但功能又十分全面的批处理框架,主要用于批处理场景比如从数据库、文件或队列中读取大量记录。不过,需要注意的是:Spring Batch 不是调度框架。商业和开源领域都有许多优秀的企业调度框架比如 Quartz、XXL-JOB、Elastic-Job。它旨在与调度程序一起工作,而不是取代调度程序。更多介绍请参考 [Spring Batch 官方文档](https://docs.spring.io/spring-batch/docs/4.3.x/reference/html/spring-batch-intro.html#spring-batch-intro),入门教程可以参考 [Spring Batch 从入门到实战](https://mrbird.cc/Spring-Batch入门.html)。 +- [Spring Batch](https://github.com/spring-projects/spring-batch) : Spring Batch 是一个轻量级但功能又十分全面的批处理框架,主要用于批处理场景比如从数据库、文件或队列中读取大量记录。不过,需要注意的是:Spring Batch 不是调度框架。商业和开源领域都有许多优秀的企业调度框架比如 Quartz、XXL-JOB、Elastic-Job。它旨在与调度程序一起工作,而不是取代调度程序。 + +## 认证授权 + +### 权限认证 + +- [Sa-Token](https://github.com/dromara/sa-token):轻量级 Java 权限认证框架。支持认证授权、单点登录、踢人下线、自动续签等功能。相比于 Spring Security 和 Shiro 来说,Sa-Token 内置的开箱即用的功能更多,使用也更简单。 +- [Spring Security](https://github.com/spring-projects/spring-security):Spring 官方安全框架,能够用于身份验证、授权、加密和会话管理,是目前使用最广泛的 Java 安全框架。 +- [Shiro](https://github.com/apache/shiro):Java 安全框架,功能和 Spring Security 类似,但使用起来更简单。 + +### 第三方登录 + +- [WxJava](https://github.com/Wechat-Group/WxJava) : WxJava (微信开发 Java SDK),支持包括微信支付、开放平台、小程序、企业微信/企业号和公众号等的后端开发。 +- [JustAuth](https://github.com/justauth/JustAuth):小而全而美的第三方登录开源组件。目前已经集成了诸如:GitHub、Gitee、支付宝、新浪微博、微信、Google、Facebook、Twitter、StackOverflow 等国内外数十家第三方平台。 + +### 单点登录(SSO) + +- [CAS](https://github.com/apereo/cas):企业多语言网络单点登录解决方案。 +- [MaxKey](https://gitee.com/dromara/MaxKey):单点登录认证系统,提供安全、标准和开放的用户身份管理(IDM)、身份认证(AM)、单点登录(SSO)、RBAC 权限管理和资源管理等。 +- [Keycloak](https://github.com/keycloak/keycloak):免费、开源身份认证和访问管理系统,支持高度可配置的单点登录功能。 + +## 网络通讯 + +- [Netty](https://github.com/netty/netty) : 一个基于 NIO 的 client-server(客户端服务器)框架,使用它可以快速简单地开发网络应用程序。 +- [Retrofit](https://github.com/square/retrofit):适用于 Android 和 Java 的类型安全的 HTTP 客户端。Retrofit 的 HTTP 请求使用的是 [OkHttp](https://square.github.io/okhttp/) 库(一款被广泛使用网络框架)。 +- [Forest](https://gitee.com/dromara/forest):轻量级 HTTP 客户端 API 框架,让 Java 发送 HTTP/HTTPS 请求不再难。它比 OkHttp 和 HttpClient 更高层,是封装调用第三方 restful api client 接口的好帮手,是 retrofit 和 feign 之外另一个选择。 +- [netty-websocket-spring-boot-starter](https://github.com/YeautyYE/netty-websocket-spring-boot-starter) :帮助你在 Spring Boot 中使用 Netty 来开发 WebSocket 服务器,并像 spring-websocket 的注解开发一样简单。 ## 数据库 @@ -22,8 +71,10 @@ icon: "xitongsheji" ### 数据库框架 -- [MyBatis-Plus](https://github.com/baomidou/mybatis-plus) : [MyBatis-Plus](https://github.com/baomidou/mybatis-plus)(简称 MP)是一个 [MyBatis](http://www.mybatis.org/mybatis-3/) 的增强工具,在 MyBatis 的基础上只做增强不做改变,为简化开发、提高效率而生。 -- [Redisson](https://github.com/redisson/redisson "redisson"):Redis 基础上的一个 Java 驻内存数据网格(In-Memory Data Grid),支持超过 30 个对象和服务:`Set`,`SortedSet`, `Map`, `List`, `Queue`, `Deque` ......,并且提供了多种分布式锁的实现。更多介绍请看:[《Redisson 项目介绍》](https://github.com/redisson/redisson/wiki/Redisson%E9%A1%B9%E7%9B%AE%E4%BB%8B%E7%BB%8D "Redisson项目介绍")。 +- [MyBatis-Plus](https://github.com/baomidou/mybatis-plus) : [MyBatis](http://www.mybatis.org/mybatis-3/) 增强工具,在 MyBatis 的基础上只做增强不做改变,为简化开发、提高效率而生。 +- [MyBatis-Flex](https://gitee.com/mybatis-flex/mybatis-flex):一个优雅的 MyBatis 增强框架,无其他任何第三方依赖,支持 CRUD、分页查询、多表查询、批量操作。 +- [jOOQ](https://github.com/jOOQ/jOOQ):用 Java 编写 SQL 的最佳方式。 +- [Redisson](https://github.com/redisson/redisson "redisson"):Redisson 是一款架设在 Redis 基础之上的 Java 驻内存数据网格 (In-Memory Data Grid),它充分利用了 Redis 键值数据库的优势,为 Java 开发者提供了一系列具有分布式特性的常用工具类。例如,分布式 Java 对象(`Set`,`SortedSet`,`Map`,`List`,`Queue`,`Deque` 等)、分布式锁等。详细介绍请看:[Redisson 项目介绍](https://github.com/redisson/redisson/wiki/Redisson%E9%A1%B9%E7%9B%AE%E4%BB%8B%E7%BB%8D "Redisson项目介绍")。 ### 数据同步 @@ -40,25 +91,12 @@ icon: "xitongsheji" ## 搜索引擎 - [Elasticsearch](https://github.com/elastic/elasticsearch "elasticsearch") (推荐):开源,分布式,RESTful 搜索引擎。 +- [Meilisearch](https://github.com/meilisearch/meilisearch):一个功能强大、快速、开源、易于使用和部署的搜索引擎,支持中文搜索(不需要添加额外的配置)。 - [Solr](https://lucene.apache.org/solr/) : Solr(读作“solar”)是 Apache Lucene 项目的开源企业搜索平台。 - -## 认证授权 - -- [WxJava](https://github.com/Wechat-Group/WxJava) : WxJava (微信开发 Java SDK),支持包括微信支付、开放平台、小程序、企业微信/企业号和公众号等的后端开发。 -- [Sa-Token](https://github.com/dromara/sa-token):轻量级 Java 权限认证框架。支持认证授权、单点登录、踢人下线、自动续签等功能。 -- [JustAuth](https://github.com/justauth/JustAuth):小而全而美的第三方登录开源组件。目前已经集成了诸如:GitHub、Gitee、支付宝、新浪微博、微信、Google、Facebook、Twitter、StackOverflow 等国内外数十家第三方平台。 - -## 网络通讯 - -- [Netty](https://github.com/netty/netty) : 一个基于 NIO 的 client-server(客户端服务器)框架,使用它可以快速简单地开发网络应用程序。 -- [Retrofit](https://github.com/square/retrofit):适用于 Android 和 Java 的类型安全的 HTTP 客户端。Retrofit 的 HTTP 请求使用的是 [OkHttp](https://square.github.io/okhttp/) 库(一款被广泛使用网络框架)。 -- [Forest](https://gitee.com/dromara/forest):轻量级 HTTP 客户端 API 框架,让 Java 发送 HTTP/HTTPS 请求不再难。它比 OkHttp 和 HttpClient 更高层,是封装调用第三方 restful api client 接口的好帮手,是 retrofit 和 feign 之外另一个选择。 -- [netty-websocket-spring-boot-starter](https://github.com/YeautyYE/netty-websocket-spring-boot-starter) :帮助你在 Spring Boot 中使用 Netty 来开发 WebSocket 服务器,并像 spring-websocket 的注解开发一样简单。 +- [Easy-ES](https://gitee.com/dromara/easy-es):傻瓜级 ElasticSearch 搜索引擎 ORM 框架。 ## 测试 -为了能让我们编写的系统更加健壮,必要的测试(UI 测试、单元测试...)是必须的。 - ### 测试框架 - [JUnit](http://junit.org/) : Java 测试框架。 @@ -79,38 +117,37 @@ icon: "xitongsheji" ### API 调试 -- [Insomnia](https://insomnia.rest/) :像人类而不是机器人一样调试 API。我平时经常用的,界面美观且轻量,总之很喜欢。 -- [Postman](https://www.getpostman.com/):API 请求生成器。 -- [Postwoman](https://github.com/liyasthomas/postwoman "postwoman"):API 请求生成器-一个免费、快速、漂亮的 Postma 替代品。 +- [Reqable](https://reqable.com/zh-CN/):新一代开源 API 开发工具。Reqable = Fiddler + Charles + Postman, 让 API 调试更快。 +- [Insomnia](https://insomnia.rest/) :像人类而不是机器人一样调试 API。我平时经常用的一款 API 开发工具,界面美观且轻量,总之很喜欢。 +- [RapidAPI](https://paw.cloud/):一款功能齐全的 HTTP 客户端,但仅支持 Mac。 +- [Postcat](https://github.com/Postcatlab/postcat):一个可扩展的开源 API 工具平台。 +- [Postman](https://www.getpostman.com/):开发者最常用的 API 测试工具之一。 +- [Hoppscotch](https://github.com/liyasthomas/postwoman "postwoman")(原 Postwoman):开源 API 测试工具。官方定位是 Postman、Insomnia 等产品的开源替代品。 +- [Restful Fast Request](https://gitee.com/dromara/fast-request):IDEA 版 Postman,API 调试工具 + API 管理工具 + API 搜索工具。 ## 任务调度 - [Quartz](https://github.com/quartz-scheduler/quartz):一个很火的开源任务调度框架,Java 定时任务领域的老大哥或者说参考标准, 很多其他任务调度框架都是基于 `quartz` 开发的,比如当当网的`elastic-job`就是基于`quartz`二次开发之后的分布式调度解决方案 - [XXL-JOB](https://github.com/xuxueli/xxl-job) :XXL-JOB 是一个分布式任务调度平台,其核心设计目标是开发迅速、学习简单、轻量级、易扩展。现已开放源代码并接入多家公司线上产品线,开箱即用。 - [Elastic-Job](http://elasticjob.io/index_zh.html):Elastic-Job 是当当网开源的一个基于 Quartz 和 Zookeeper 的分布式调度解决方案,由两个相互独立的子项目 Elastic-Job-Lite 和 Elastic-Job-Cloud 组成,一般我们只要使用 Elastic-Job-Lite 就好。 -- [EasyScheduler](https://github.com/analysys/EasyScheduler "EasyScheduler") (已经更名为 DolphinScheduler,已经成为 Apache 孵化器项目):Easy Scheduler 是一个分布式工作流任务调度系统,主要解决“复杂任务依赖但无法直接监控任务健康状态”的问题。Easy Scheduler 以 DAG 方式组装任务,可以实时监控任务的运行状态。同时,它支持重试,重新运行等操作... 。 +- [EasyScheduler](https://github.com/analysys/EasyScheduler "EasyScheduler") (已经更名为 DolphinScheduler,已经成为 Apache 孵化器项目):分布式易扩展的可视化工作流任务调度平台,主要解决“复杂任务依赖但无法直接监控任务健康状态”的问题。 - [PowerJob](https://gitee.com/KFCFans/PowerJob):新一代分布式任务调度与计算框架,支持 CRON、API、固定频率、固定延迟等调度策略,提供工作流来编排任务解决依赖关系,使用简单,功能强大,文档齐全,欢迎各位接入使用! 。 -- [DolphinScheduler](https://github.com/apache/dolphinscheduler):分布式易扩展的可视化工作流任务调度平台。 -相关阅读: +## 工作流 -- [Spring Job、Quartz、XXL-Job 对比+全解析](https://mp.weixin.qq.com/s/jqN4noo5NazckPCehWFgpA) -- [推荐 5 个 YYDS 的 Java 项目](https://mp.weixin.qq.com/s?__biz=Mzg2OTA0Njk0OA==&mid=2247518215&idx=1&sn=91e467f39322d2e7979b85fe235822d2&chksm=cea1c7ccf9d64edaf966c95923d72d337bf5e655a773a3d295d65fc92e4535ae5d8b0e6d9d86&token=2063686030&lang=zh_CN#rd) +1. [Flowable](https://github.com/flowable/flowable-engine) :Activiti5 的一个分支发展而来,功能丰富,在 Activiti 的基础上,引入了更多高级功能,如更强大的 CMMN(案例管理模型与符号)、DMN(决策模型与符号)支持,以及更灵活的集成选项。 +2. [Activiti](https://github.com/Activiti/Activiti):功能扩展相对保守,适合需要稳定 BPMN 2.0 工作流引擎的传统企业应用。 +3. [Warm-Flow](https://gitee.com/dromara/warm-flow):国产开源工作流引擎,其特点简洁轻量但又不简单,五脏俱全,组件独立,可扩展。 +4. [FlowLong](https://gitee.com/aizuda/flowlong):国产开源工作流引擎,专门中国特色流程审批打造。 ## 分布式 ### API 网关 -微服务下一个系统被拆分为多个服务,但是像 安全认证,流量控制,日志,监控等功能是每个服务都需要的,没有网关的话,我们就需要在每个服务中单独实现,这使得我们做了很多重复的事情并且没有一个全局的视图来统一管理这些功能。 - -综上:一般情况下,网关一般都会提供请求转发、安全认证(身份/权限认证)、流量控制、负载均衡、容灾、日志、监控这些功能。 - -上面介绍了这么多功能实际上网关主要做了一件事情:请求过滤 。权限校验、流量控制这些都可以通过过滤器实现,请求转也是通过过滤器实现的。 - -1. [Kong](https://github.com/Kong/kong "kong"):Kong 是一个云原生、快速的、可伸缩的分布式微服务抽象层(也称为 API 网关、API 中间件或在某些情况下称为服务网格)。2015 年作为开源项目发布,其核心价值是高性能和可扩展性。 -2. [Soul](https://github.com/Dromara/soul "soul"):高性能、基于 webflux 的反应式 Java API 网关 -3. [Spring Cloud Gateway](https://github.com/spring-cloud/spring-cloud-gateway) : 基于 Spring Framework 5.x 和 Spring Boot 2.x 构建的高性能网关。 -4. [Zuul](https://github.com/Netflix/zuul) : Zuul 是一个 L7 应用程序网关,它提供了动态路由,监视,弹性,安全性等功能。 +- [Kong](https://github.com/Kong/kong "kong"):Kong 是一个云原生、快速的、可伸缩的分布式微服务抽象层(也称为 API 网关、API 中间件或在某些情况下称为服务网格)。2015 年作为开源项目发布,其核心价值是高性能和可扩展性。 +- [ShenYu](https://github.com/Dromara/soul "soul"):适用于所有微服务的可伸缩、高性能、响应性 API 网关解决方案。 +- [Spring Cloud Gateway](https://github.com/spring-cloud/spring-cloud-gateway) : 基于 Spring Framework 5.x 和 Spring Boot 2.x 构建的高性能网关。 +- [Zuul](https://github.com/Netflix/zuul) : Zuul 是一个 L7 应用程序网关,它提供了动态路由,监视,弹性,安全性等功能。 ### 配置中心 @@ -121,51 +158,61 @@ icon: "xitongsheji" ### 链路追踪 -目前分布式链路追踪系统基本都是根据谷歌的《Dapper 大规模分布式系统的跟踪系统》这篇论文发展而来,主流的有 Pinpoint,Skywalking ,CAT(当然也有其他的例如 Zipkin,Jaeger 等产品,不过总体来说不如前面选取的 3 个完成度高)等。 - -1. [Skywalking](https://github.com/apache/skywalking "skywalking") : 针对分布式系统的应用性能监控,尤其是针对微服务、云原生和面向容器的分布式系统架构。 -2. [Zipkin](https://github.com/openzipkin/zipkin "zipkin"):Zipkin 是一个分布式跟踪系统。它有助于收集解决服务体系结构中的延迟问题所需的时序数据。功能包括该数据的收集和查找。 -3. [CAT](https://github.com/dianping/cat "cat"):CAT 作为服务端项目基础组件,提供了 Java, C/C++, Node.js, Python, Go 等多语言客户端,已经在美团点评的基础架构中间件框架(MVC 框架,RPC 框架,数据库框架,缓存框架等,消息队列,配置系统等)深度集成,为美团点评各业务线提供系统丰富的性能指标、健康状况、实时告警等。 +- [Skywalking](https://github.com/apache/skywalking "skywalking") : 针对分布式系统的应用性能监控,尤其是针对微服务、云原生和面向容器的分布式系统架构。 +- [Zipkin](https://github.com/openzipkin/zipkin "zipkin"):Zipkin 是一个分布式跟踪系统。它有助于收集解决服务体系结构中的延迟问题所需的时序数据。功能包括该数据的收集和查找。 +- [CAT](https://github.com/dianping/cat "cat"):CAT 作为服务端项目基础组件,提供了 Java, C/C++, Node.js, Python, Go 等多语言客户端,已经在美团点评的基础架构中间件框架(MVC 框架,RPC 框架,数据库框架,缓存框架等,消息队列,配置系统等)深度集成,为美团点评各业务线提供系统丰富的性能指标、健康状况、实时告警等。 相关阅读:[Skywalking 官网对于主流开源链路追踪系统的对比](https://skywalking.apache.org/zh/blog/2019-03-29-introduction-of-skywalking-and-simple-practice.html) +### 分布式锁 + +- [Lock4j](https://gitee.com/baomidou/lock4j):支持 Redisson、ZooKeeper 等不同方案的高性能分布式锁。 +- [Redisson](https://github.com/redisson/redisson "redisson"):Redisson 在分布式锁方面提供全面且强大的支持,超越了简单的 Redis 锁实现。 + ## 高性能 ### 多线程 -- [Hippo-4J](https://github.com/opengoofy/hippo4j):一款强大的动态线程池框架,解决了传统线程池使用存在的一些痛点比如线程池参数没办法动态修改、不支持运行时变量的传递、无法执行优雅关闭。除了支持动态修改线程池参数、线程池任务传递上下文,还支持通知报警、运行监控等开箱即用的功能。 +- [Hippo4j](https://github.com/opengoofy/hippo4j):异步线程池框架,支持线程池动态变更&监控&报警,无需修改代码轻松引入。支持多种使用模式,轻松引入,致力于提高系统运行保障能力。 - [Dynamic Tp](https://github.com/dromara/dynamic-tp):轻量级动态线程池,内置监控告警功能,集成三方中间件线程池管理,基于主流配置中心(已支持 Nacos、Apollo,Zookeeper、Consul、Etcd,可通过 SPI 自定义实现)。 - [asyncTool](https://gitee.com/jd-platform-opensource/asyncTool) : 京东的一位大佬开源的多线程工具库,里面大量使用到了 `CompletableFuture` ,可以解决任意的多线程并行、串行、阻塞、依赖、回调的并行框架,可以任意组合各线程的执行顺序,带全链路执行结果回调。 ### 缓存 -- [Caffeine](https://github.com/ben-manes/caffeine) : 一款强大的本地缓存解决方案,性能非常 🐂。 +#### 本地缓存 + +- [Caffeine](https://github.com/ben-manes/caffeine) : 一款强大的本地缓存解决方案,性能非常强大。 +- [Guava](https://github.com/google/guava):Google Java 核心库,内置了比较完善的本地缓存实现。 +- [OHC](https://github.com/snazy/ohc) :Java 堆外缓存解决方案(项目从 2021 年开始就不再进行维护了)。 + +#### 分布式缓存 + - [Redis](https://github.com/redis/redis):一个使用 C 语言开发的内存数据库,分布式缓存首选。 +- [Dragonfly](https://github.com/dragonflydb/dragonfly):一种针对现代应用程序负荷需求而构建的内存数据库,完全兼容 Redis 和 Memcached 的 API,迁移时无需修改任何代码,号称全世界最快的内存数据库。 +- [KeyDB](https://github.com/Snapchat/KeyDB): Redis 的一个高性能分支,专注于多线程、内存效率和高吞吐量。 + +#### 多级缓存 + +- [J2Cache](https://gitee.com/ld/J2Cache):基于本地内存和 Redis 的两级 Java 缓存框架。 +- [JetCache](https://github.com/alibaba/jetcache):阿里开源的缓存框架,支持多级缓存、分布式缓存自动刷新、 TTL 等功能。 ### 消息队列 **分布式队列**: - [RocketMQ](https://github.com/apache/rocketmq "RocketMQ"):阿里巴巴开源的一款高性能、高吞吐量的分布式消息中间件。 -- [Kafaka](https://github.com/apache/kafka "Kafaka"): Kafka 是一种分布式的,基于发布 / 订阅的消息系统。关于它的入门可以查看:[Kafka 入门看这一篇就够了](https://github.com/Snailclimb/JavaGuide/blob/master/docs/system-design/data-communication/Kafka入门看这一篇就够了.md "Kafka入门看这一篇就够了") +- [Kafka](https://github.com/apache/kafka "Kafka"): Kafka 是一种分布式的,基于发布 / 订阅的消息系统。 - [RabbitMQ](https://github.com/rabbitmq "RabbitMQ") :由 erlang 开发的基于 AMQP(Advanced Message Queue 高级消息队列协议)协议实现的消息队列。 **内存队列**: -- [Disruptor](https://github.com/LMAX-Exchange/disruptor):Disruptor 是英国外汇交易公司 LMAX 开发的一个高性能队列,研发的初衷是解决内存队列的延迟问题(在性能测试中发现竟然与 I/O 操作处于同样的数量级)。相关阅读:[《高性能内存队列——Disruptor》](https://tech.meituan.com/2016/11/18/disruptor.html) 。 - -**可视化管理工具**: - -- [Kafdrop](https://github.com/obsidiandynamics/kafdrop) : 一个用于查看 Kafka 主题和浏览消费者组的 Web UI。 -- [EFAK](https://github.com/smartloli/EFAK) (Eagle For Apache Kafka,以前叫做 Kafka Eagle):一个简单的高性能监控系统,用于对 Kafka 集群进行全面的监控和管理。 +- [Disruptor](https://github.com/LMAX-Exchange/disruptor):Disruptor 是英国外汇交易公司 LMAX 开发的一个高性能队列,研发的初衷是解决内存队列的延迟问题(在性能测试中发现竟然与 I/O 操作处于同样的数量级)。 -### 数据库中间件 +### 读写分离和分库分表 - [ShardingSphere](https://github.com/apache/shardingsphere):ShardingSphere 是一套开源的分布式数据库中间件解决方案组成的生态圈,它由 Sharding-JDBC、Sharding-Proxy 和 Sharding-Sidecar(计划中)这 3 款相互独立的产品组成。 - [MyCat](https://github.com/MyCatApache/MyCat2) : MyCat 是数据库分库分表的中间件,MyCat 使用最多的两个功能是:读写分离和分库分表。MyCat 是一些社区爱好者在阿里 Cobar 的基础上进行二次开发,解决了 Cobar 当时存 在的一些问题,并且加入了许多新的功能在其中。 -- [dynamic-datasource-spring-boot-starter](https://github.com/baomidou/dynamic-datasource-spring-boot-starter):dynamic-datasource-spring-boot-starter 是一个基于 springboot 的快速集成多数据源的启动器。如果说你有配置多数据源、读写分离等需求的话,可以了解一下这个项目。 - -相关阅读:[数据库中间件详解(精品长文)](https://zhuanlan.zhihu.com/p/87144535) +- [dynamic-datasource-spring-boot-starter](https://github.com/baomidou/dynamic-datasource-spring-boot-starter):一个基于 Spring Boot 的快速集成多数据源的启动器,支持多数据源、动态数据源、主从分离、读写分离和分布式事务。 ## 高可用 @@ -176,7 +223,7 @@ icon: "xitongsheji" - [Sentinel](https://github.com/alibaba/Sentinel)(推荐):面向分布式服务架构的高可用防护组件,主要以流量为切入点,从流量控制、熔断降级、系统自适应保护等多个维度来帮助用户保障微服务的稳定性。 - [Hystrix](https://github.com/Netflix/Hystrix):类似于 Sentinel 。 -相关阅读:[Sentinel 与 Hystrix 的对比](https://sentinelguard.io/zh-cn/blog/sentinel-vs-hystrix.html) +相关阅读:[Sentinel 与 Hystrix 的对比](https://sentinelguard.io/zh-cn/blog/sentinel-vs-hystrix.html)。 单机限流: @@ -194,3 +241,10 @@ icon: "xitongsheji" - 新一代 ELK 架构 : Elasticsearch+Logstash+Kibana+Beats。 - EFK : EFK 中的 F 代表的是 [Fluentd](https://github.com/fluent/fluentd)。 - [TLog](https://gitee.com/dromara/TLog):一个轻量级的分布式日志标记追踪神器,10 分钟即可接入,自动对日志打标签完成微服务的链路追踪。 + +## 字节码操作 + +- [ASM](https://asm.ow2.io/):通用 Java 字节码操作和分析框架。它可用于直接以二进制形式修改现有类或动态生成类。 +- [Byte Buddy](https://github.com/raphw/byte-buddy):Java 字节码生成和操作库,用于在 Java 应用程序运行时创建和修改 Java 类,无需使用编译器 +- [Javassist](https://github.com/jboss-javassist/javassist):动态编辑 Java 字节码的类库。 +- [Recaf](https://github.com/Col-E/Recaf):现代 Java 字节码编辑器,基于 ASM(Java 字节码操作框架) 来修改字节码,可简化编辑已编译 Java 应用程序的过程。 diff --git a/docs/open-source-project/tool-library.md b/docs/open-source-project/tool-library.md index 51a449c2c76..d134cc2318f 100644 --- a/docs/open-source-project/tool-library.md +++ b/docs/open-source-project/tool-library.md @@ -6,23 +6,33 @@ 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 语言也可以“甜甜的”。 -- [p3c](https://github.com/alibaba/p3c "p3c"):Alibaba Java Coding Guidelines pmd implements and IDE plugin。Eclipse 和 IDEA 上都有该插件,推荐使用! -- [arthas](https://github.com/alibaba/arthas "arthas"):Arthas 是 Alibaba 开源的 Java 诊断工具。 -- [sonarqube](https://github.com/SonarSource/sonarqube "sonarqube"):SonarQube 支持所有开发人员编写更干净,更安全的代码。 -- [checkstyle](https://github.com/checkstyle/checkstyle "checkstyle") :Checkstyle 是一种开发工具,可帮助程序员编写符合编码标准的 Java 代码。它使检查 Java 代码的过程自动化,从而使人们不必执行这项无聊(但很重要)的任务。这使其成为想要实施编码标准的项目的理想选择。 -- [pmd](https://github.com/pmd/pmd "pmd") : 可扩展的多语言静态代码分析器。 -- [spotbugs](https://github.com/spotbugs/spotbugs "spotbugs") :SpotBugs 是 FindBugs 的继任者。静态分析工具,用于查找 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 应用程序。它提供了丰富的命令和功能,用于分析应用程序的性能问题,包括启动过程中的资源消耗和加载时间。 +- [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)实现。 ## 文档处理 +### 文档解析 + +- [Tika](https://github.com/apache/tika):Apache Tika 工具包能够检测并提取来自超过一千种不同文件类型(如 PPT、XLS 和 PDF)的元数据和文本内容。 + ### 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 + +- [poi-tl](https://github.com/Sayi/poi-tl):基于 Apache POI 的 Word 模板引擎,可以根据 Word 模板和数据生成 Word 文档,所见即所得! ### JSON @@ -30,10 +40,13 @@ icon: codelibrary-fill ### PDF -- [pdfbox](https://github.com/apache/pdfbox) :用于处理 PDF 文档的开放源码 Java 工具。该项目允许创建新的 PDF 文档、对现有文档进行操作以及从文档中提取内容。PDFBox 还包括几个命令行实用程序。PDFBox 是在 Apache 2.0 版许可下发布的。 -- [OpenPDF](https://github.com/LibrePDF/OpenPDF):OpenPDF 是一个免费的 Java 库,用于使用 LGPL 和 MPL 开源许可创建和编辑 PDF 文件。OpenPDF 基于 iText 的一个分支。 -- [itext7](https://github.com/itext/itext7):iText 7 代表了想要利用利用好 PDF 的开发人员的更高级别的 sdk。iText 7 配备了更好的文档引擎、高级和低级编程功能以及创建、编辑和增强 PDF 文档的能力,几乎对每个工作流都有好处。 -- [FOP](https://xmlgraphics.apache.org/fop/) :Apache FOP 项目的主要的输出目标是 PDF。 +对于简单的 PDF 创建需求,OpenPDF 是一个不错的选择,它开源免费,API 简单易用。对于需要解析、转换和提取文本等操作的复杂场景,可以选择 Apache PDFBox。当然了,复杂场景如果不介意 LGPL 许可也可以选择 iText。 + +- [x-easypdf](https://gitee.com/dromara/x-easypdf):一个用搭积木的方式构建 PDF 的框架(基于 pdfbox/fop),支持 PDF 导出和编辑,适合简单的 PDF 文档生成场景。 +- [iText](https://github.com/itext/itext7):一个用于创建、编辑和增强 PDF 文档的 Java 库。iText 7 社区版采用 AGPL 许可证,如果你的项目是闭源商业项目,需要购买商业许可证。 iText 5 仍然是 LGPL 许可,可以免费用于商业用途,但已经停止维护。 +- [OpenPDF](https://github.com/LibrePDF/OpenPDF):完全开源免费 (LGPL/MPL 双重许可),基于 iText 的一个分支,可以作为 iText 的替代品,简单易用,但功能相比于 iText 更少一些(对于大多数场景已经足够)。 +- [Apache PDFBox](https://github.com/apache/pdfbox) :完全开源免费 (Apache 许可证),功能强大,支持 PDF 的创建、解析、转换和提取文本等。不过,由于其功能过于丰富,因此 API 设计相对复杂,学习难度会大一些。 +- [FOP](https://xmlgraphics.apache.org/fop/) : Apache FOP 用于将 XSL-FO(Extensible Stylesheet Language Formatting Objects)格式化对象转换为多种输出格式,最常见的是 PDF。 ## 图片处理 @@ -46,9 +59,14 @@ icon: codelibrary-fill - [AJ-Captcha](https://gitee.com/anji-plus/captcha):行为验证码(滑动拼图、点选文字),前后端(java)交互。 - [tianai-captcha](https://gitee.com/tianai/tianai-captcha):好看又好用的滑块验证码。 +## 短信&邮件 + +- [SMS4J](https://github.com/dromara/SMS4J):短信聚合框架,解决接入多个短信 SDK 的繁琐流程。 +- [Simple Java Mail](https://github.com/bbottema/simple-java-mail):最简单的 Java 轻量级邮件库,同时能够发送复杂的电子邮件。 + ## 在线支付 -- [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 支付等常用的支付方式以及各种常用的接口。 @@ -56,4 +74,4 @@ icon: codelibrary-fill - [oshi](https://github.com/oshi/oshi "oshi"):一款为 Java 语言提供的基于 JNA 的(本机)操作系统和硬件信息库。 - [ip2region](https://github.com/lionsoul2014/ip2region) :最自由的 ip 地址查询库,ip 到地区的映射库,提供 Binary,B 树和纯内存三种查询算法,妈妈再也不用担心我的 ip 地址定位。 -- [agrona](https://github.com/real-logic/agrona):Java 高性能数据结构(`Buffers`、`Lists`、`Maps`、`Scalable Timer Wheel`......)和实用方法。 +- [agrona](https://github.com/real-logic/agrona):Java 高性能数据结构(`Buffers`、`Lists`、`Maps`、`Scalable Timer Wheel`……)和实用方法。 diff --git a/docs/open-source-project/tools.md b/docs/open-source-project/tools.md index a3d48701ca2..56c6185bfcc 100644 --- a/docs/open-source-project/tools.md +++ b/docs/open-source-project/tools.md @@ -4,10 +4,24 @@ category: 开源项目 icon: tool --- -## Java +## 代码质量 -- [JADX](https://github.com/skylot/jadx):一款功能强大的反编译工具。 -- [Recaf](https://github.com/Col-E/Recaf):Java 字节码编辑器,基于 ASM(Java 字节码操作框架) 来修改字节码,可简化编辑已编译 Java 应用程序的过程。 +- [SonarQube](https://github.com/SonarSource/sonarqube "sonarqube"):静态代码检查工具,,帮助检查代码缺陷,可以快速的定位代码中潜在的或者明显的错误,改善代码质量,提高开发速度。 +- [Spotless](https://github.com/diffplug/spotless):Spotless 是支持多种语言的代码格式化工具,支持 Maven 和 Gradle 以 Plugin 的形式构建。 +- [CheckStyle](https://github.com/checkstyle/checkstyle "checkstyle") : 类似于 Spotless,可帮助程序员编写符合编码标准的 Java 代码。 +- [PMD](https://github.com/pmd/pmd "pmd") : 可扩展的多语言静态代码分析器。 +- [SpotBugs](https://github.com/spotbugs/spotbugs "spotbugs") : FindBugs 的继任者。静态分析工具,用于查找 Java 代码中的错误。 +- [P3C](https://github.com/alibaba/p3c "p3c"):Alibaba Java Coding Guidelines pmd implements and IDE plugin。Eclipse 和 IDEA 上都有该插件。 + +## 项目构建 + +- [Maven](https://maven.apache.org/):一个软件项目管理和理解工具。基于项目对象模型 (Project Object Model,POM) 的概念,Maven 可以从一条中心信息管理项目的构建、报告和文档。详细介绍:[Maven 核心概念总结](https://javaguide.cn/tools/maven/maven-core-concepts.html)。 +- [Gradle](https://gradle.org/) :一个开源的构建自动化工具,它足够灵活,可以构建几乎任何类型的软件。Gradle 对你要构建什么或者如何构建它做了很少的假设,这使得 Gradle 特别灵活。详细介绍:[Gradle 核心概念总结](https://javaguide.cn/tools/gradle/gradle-core-concepts.html)。 + +## 反编译 + +- [JADX](https://github.com/skylot/jadx):用于从 Android Dex 和 Apk 文件生成 Java 源代码的命令行和 GUI 工具。 +- [JD-GUI](https://github.com/java-decompiler/jd-gui):一个独立的 GUI 工具,可显示 CLASS 文件中的 Java 源代码。 ## 数据库 @@ -24,6 +38,7 @@ icon: tool ### 数据库管理 +- [Chat2DB](https://github.com/alibaba/Chat2DB):阿里巴巴开源的一款智能的通用数据库工具和 SQL 客户端,支持 Windows、Mac 本地安装,也支持服务器端部署,Web 网页访问。和传统的数据库客户端软件 Navicat、DBeaver 相比 Chat2DB 集成了 AIGC 的能力,支持自然语言生成 SQL、SQL 性能优化等功能。 - [Beekeeper Studio](https://github.com/beekeeper-studio/beekeeper-studio):跨平台数据库管理工具,颜值高,支持 SQLite、MySQL、MariaDB、Postgres、CockroachDB、SQL Server、Amazon Redshift。 - [Sequel Pro](https://github.com/sequelpro/sequelpro):适用于 macOS 的 MySQL/MariaDB 数据库管理工具。 - [DBeaver](https://github.com/dbeaver/dbeaver):一个基于 Java 开发 ,并且支持几乎所有的数据库产品的开源数据库管理工具。DBeaver 社区版不光支持关系型数据库比如 MySQL、PostgreSQL、MariaDB、SQLite、Oracle、Db2、SQL Server,还比如 SQLite、H2 这些内嵌数据库。还支持常见的全文搜索引擎比如 Elasticsearch 和 Solr、大数据相关的工具比如 Hive 和 Spark。 @@ -33,20 +48,24 @@ icon: tool ### Redis -- [Another Redis Desktop Manager](https://github.com/qishibo/AnotherRedisDesktopManager/blob/master/README.zh-CN.md):更快、更好、更稳定的 Redis 桌面(GUI)管理客户端。 +- [Another Redis Desktop Manager](https://github.com/qishibo/AnotherRedisDesktopManager/blob/master/README.zh-CN.md):更快、更好、更稳定的 Redis 桌面(GUI)管理客户端,兼容 Windows、Mac、Linux。 +- [Tiny RDM](https://github.com/tiny-craft/tiny-rdm):一个更现代化的 Redis 桌面(GUI)管理客户端,基于 Webview2,兼容 Windows、Mac、Linux。 - [Redis Manager](https://github.com/ngbdf/redis-manager):Redis 一站式管理平台,支持集群(cluster、master-replica、sentinel)的监控、安装(除 sentinel)、管理、告警以及基本的数据操作功能。 +- [CacheCloud](https://github.com/sohutv/cachecloud):一个 Redis 云管理平台,支持 Redis 多种架构(Standalone、Sentinel、Cluster)高效管理、有效降低大规模 Redis 运维成本,提升资源管控能力和利用率。 +- [RedisShake](https://github.com/tair-opensource/RedisShake):一个用于处理和迁移 Redis 数据的工具。 -## Devops +## Docker -- [Portainer](https://github.com/portainer/portainer):可视化管理 Docker 和 Kubernetes。相关阅读:[《吊炸天的 Docker 图形化工具 Portainer,必须推荐给你!》](https://mp.weixin.qq.com/s?__biz=Mzg2OTA0Njk0OA==&mid=2247504221&idx=1&sn=85a3c69d64fba1b0d6d8485ab28ab4c4&chksm=cea19e96f9d617802920d5769bafc824b3b80afdfb6166a00532f0caa3b6f5bdac930e4e89de&token=693114125&lang=zh_CN#rd)。 +- [Portainer](https://github.com/portainer/portainer):可视化管理 Docker,Web 应用的形式。 +- [lazydocker](https://github.com/jesseduffield/lazydocker):适用于 docker 和 docker-compose 的简单终端 UI。 ## ZooKeeper - [PrettyZoo](https://github.com/vran-dev/PrettyZoo):一个基于 Apache Curator 和 JavaFX 实现的 ZooKeeper 图形化管理客户端,颜值非常高,支持 Mac / Windows / Linux 。你可以使用 PrettyZoo 来实现对 ZooKeeper 的可视化增删改查。 - [zktools](https://zktools.readthedocs.io/en/latest/#installing):一个低延迟的 ZooKeeper 图形化管理客户端,颜值非常高,支持 Mac / Windows / Linux 。你可以使用 zktools 来实现对 ZooKeeper 的可视化增删改查。 -## Markdown +## Kafka -- [MarkText](https://github.com/marktext/marktext):一个简单而优雅的开源 Markdown 编辑器,专注于速度和可用性。Linux、macOS 和 Windows 均适用。 -- [Typora](https://www.typora.io/) :我一直用的一款 Markdown 工具,直接文件夹视图和目录视图,支持 Markdown 格式直接导出成 PDF、HTML 等格式。 -- [Markdown Here](https://github.com/adam-p/markdown-here):使用 Markdown 语法发邮件,并且提供多种主题,快来拯救你的邮件格式吧! +- [Kafka UI](https://github.com/provectus/kafka-ui):免费的开源 Web UI,用于监控和管理 Apache Kafka 集群。 +- [Kafdrop](https://github.com/obsidiandynamics/kafdrop) : 一个用于查看 Kafka 主题和浏览消费者组的 Web UI。 +- [EFAK](https://github.com/smartloli/EFAK) (Eagle For Apache Kafka,以前叫做 Kafka Eagle):一个简单的高性能监控系统,用于对 Kafka 集群进行全面的监控和管理。 diff --git a/docs/open-source-project/tutorial.md b/docs/open-source-project/tutorial.md index e78b1ecd66a..0cab3269eab 100644 --- a/docs/open-source-project/tutorial.md +++ b/docs/open-source-project/tutorial.md @@ -6,56 +6,56 @@ icon: "book" ## Java -- **[JavaGuide](https://github.com/Snailclimb/JavaGuide "JavaGuide")** :【Java 学习+面试指南】 一份涵盖大部分 Java 程序员所需要掌握的核心知识。 -- **[toBeBetterJavaer](https://github.com/itwanger/toBeBetterJavaer)**:一份通俗易懂、风趣幽默的 Java 学习指南,内容涵盖 Java 基础、Java 集合框架、Java 并发编程、JVM、Java 企业级开发(Git、SSM、Spring Boot)等知识点。 -- **[interview-guide](https://github.com/csguide-dabai/interview-guide)**:总结了后端面试八股文中的重点,希望能帮助各位准备互联网开发岗校招面试的同学。 -- **[advanced-java](https://github.com/doocs/advanced-java "advanced-java")** :互联网 Java 工程师进阶知识完全扫盲:涵盖高并发、分布式、高可用、微服务、海量数据处理等领域知识。 -- **[toBeTopJavaer](https://github.com/hollischuang/toBeTopJavaer "toBeTopJavaer")**:Java 工程师成神之路 。 -- **[technology-talk](https://github.com/aalansehaiyang/technology-talk)** : 汇总 java 生态圈常用技术框架、开源中间件,系统架构、数据库、大公司架构案例、常用三方类库、项目管理、线上问题排查、个人成长、思考等知识 -- **[JCSprout](https://github.com/crossoverJie/JCSprout "JCSprout")** :处于萌芽阶段的 Java 核心知识库。 -- **[bestJavaer](https://github.com/crisxuan/bestJavaer)** : 这是一个成为更好的 Java 程序员的系列教程。 -- **[java-design-patterns](https://github.com/iluwatar/java-design-patterns "java-design-patterns")**:用 Java 实现的设计模式。 +- [JavaGuide](https://github.com/Snailclimb/JavaGuide "JavaGuide") :【Java 学习+面试指南】 一份涵盖大部分 Java 程序员所需要掌握的核心知识。 +- [toBeBetterJavaer](https://github.com/itwanger/toBeBetterJavaer):一份通俗易懂、风趣幽默的 Java 学习指南,内容涵盖 Java 基础、Java 集合框架、Java 并发编程、JVM、Java 企业级开发(Git、SSM、Spring Boot)等知识点。 +- [interview-guide](https://github.com/csguide-dabai/interview-guide):总结了后端面试八股文中的重点,希望能帮助各位准备互联网开发岗校招面试的同学。 +- [advanced-java](https://github.com/doocs/advanced-java "advanced-java") :互联网 Java 工程师进阶知识完全扫盲:涵盖高并发、分布式、高可用、微服务、海量数据处理等领域知识。 +- [toBeTopJavaer](https://github.com/hollischuang/toBeTopJavaer "toBeTopJavaer"):Java 工程师成神之路 。 +- [technology-talk](https://github.com/aalansehaiyang/technology-talk) : 汇总 java 生态圈常用技术框架、开源中间件,系统架构、数据库、大公司架构案例、常用三方类库、项目管理、线上问题排查、个人成长、思考等知识 +- [JCSprout](https://github.com/crossoverJie/JCSprout) :处于萌芽阶段的 Java 核心知识库。 +- [bestJavaer](https://github.com/crisxuan/bestJavaer) : 这是一个成为更好的 Java 程序员的系列教程。 +- [java-design-patterns](https://github.com/iluwatar/java-design-patterns "java-design-patterns"):用 Java 实现的设计模式。 ## 计算机基础 -- **[cs-self-learning](https://github.com/PKUFlyingPig/cs-self-learning)**:计算机自学指南,汇总欧美众多名校高质量计算机课程。 -- **[CS-Notes](https://github.com/CyC2018/CS-Notes "CS-Notes")**:技术面试必备基础知识、Leetcode 题解、后端面试、Java 面试、春招、秋招、操作系统、计算机网络、系统设计。 -- **[Waking-Up](https://github.com/wolverinn/Waking-Up)**:计算机基础(计算机网络/操作系统/数据库/Git...)面试问题全面总结。 +- [cs-self-learning](https://github.com/PKUFlyingPig/cs-self-learning):计算机自学指南,汇总欧美众多名校高质量计算机课程。 +- [CS-Notes](https://github.com/CyC2018/CS-Notes "CS-Notes"):技术面试必备基础知识、Leetcode 题解、后端面试、Java 面试、春招、秋招、操作系统、计算机网络、系统设计。 +- [Waking-Up](https://github.com/wolverinn/Waking-Up):计算机基础(计算机网络/操作系统/数据库/Git...)面试问题全面总结。 ## 系统设计 ### SpringBoot -- **[springboot-guide](https://github.com/Snailclimb/springboot-guide)**:SpringBoot 核心知识点总结。 基于 Spring Boot 2.19+。 -- **[SpringAll](https://github.com/wuyouzhuguli/SpringAll "SpringAll")**:循序渐进,学习 Spring Boot、Spring Boot & Shiro、Spring Cloud、Spring Security & Spring Security OAuth2,博客 Spring 系列源码。 -- **[Springboot-Notebook](https://github.com/chengxy-nds/Springboot-Notebook)** :一系列以 Spring Boot 为基础开发框架,整合 Redis、 Rabbitmq、ES、MongoDB、Spring Cloud、Kafka、Skywalking 等互联网主流技术,实现各种常见功能点的综合性案例。 -- **[springboot-learning-example](https://github.com/JeffLi1993/springboot-learning-example "springboot-learning-example")**:Spring Boot 实践学习案例,是 Spring Boot 初学者及核心技术巩固的最佳实践。 -- **[spring-boot-demo](https://github.com/xkcoding/spring-boot-demo "spring-boot-demo")**:spring boot demo 是一个用来深度学习并实战 spring boot 的项目,目前总共包含 63 个集成 demo,已经完成 52 个。 -- **[SpringBoot-Labs](https://github.com/YunaiV/SpringBoot-Labs)**:Spring Boot 系列教程。 +- [springboot-guide](https://github.com/Snailclimb/springboot-guide):SpringBoot 核心知识点总结。 基于 Spring Boot 2.19+。 +- [SpringAll](https://github.com/wuyouzhuguli/SpringAll "SpringAll"):循序渐进,学习 Spring Boot、Spring Boot & Shiro、Spring Cloud、Spring Security & Spring Security OAuth2,博客 Spring 系列源码。 +- [Springboot-Notebook](https://github.com/chengxy-nds/Springboot-Notebook) :一系列以 Spring Boot 为基础开发框架,整合 Redis、 Rabbitmq、ES、MongoDB、Spring Cloud、Kafka、Skywalking 等互联网主流技术,实现各种常见功能点的综合性案例。 +- [springboot-learning-example](https://github.com/JeffLi1993/springboot-learning-example "springboot-learning-example"):Spring Boot 实践学习案例,是 Spring Boot 初学者及核心技术巩固的最佳实践。 +- [spring-boot-demo](https://github.com/xkcoding/spring-boot-demo "spring-boot-demo"):spring boot demo 是一个用来深度学习并实战 spring boot 的项目,目前总共包含 63 个集成 demo,已经完成 52 个。 +- [SpringBoot-Labs](https://github.com/YunaiV/SpringBoot-Labs):Spring Boot 系列教程。 相关文章:[GitHub 点赞接近 100k 的 SpringBoot 学习教程+实战推荐!牛批!](https://mp.weixin.qq.com/s?__biz=Mzg2OTA0Njk0OA==&mid=2247488298&idx=3&sn=0a8fd88ec5a050de131c2a3305482ac4&chksm=cea25ce1f9d5d5f7f53a0237d27489326bce4546353b038085c03b086d91ef396bf824d3a155&token=496868067&lang=zh_CN#rd) ### SpringCloud -- **[SpringCloudLearning](https://github.com/forezp/SpringCloudLearning "SpringCloudLearning")** : 方志朋的《史上最简单的 Spring Cloud 教程源码》。 -- **[springcloud-learning](https://github.com/macrozheng/springcloud-learning)** : 一套涵盖大部分核心组件使用的 Spring Cloud 教程。 -- **[SpringCloud](https://github.com/zhoutaoo/SpringCloud "SpringCloud")**:基于 SpringCloud2.1 的微服务开发脚手架,整合了 spring-security-oauth2、nacos、feign、sentinel、springcloud-gateway 等。 +- [SpringCloudLearning](https://github.com/forezp/SpringCloudLearning "SpringCloudLearning") : 方志朋的《史上最简单的 Spring Cloud 教程源码》。 +- [springcloud-learning](https://github.com/macrozheng/springcloud-learning) : 一套涵盖大部分核心组件使用的 Spring Cloud 教程。 +- [SpringCloud](https://github.com/zhoutaoo/SpringCloud "SpringCloud"):基于 SpringCloud2.1 的微服务开发脚手架,整合了 spring-security-oauth2、nacos、feign、sentinel、springcloud-gateway 等。 相关文章:[GitHub 点赞接近 70k 的 Spring Cloud 学习教程+实战项目推荐!牛批!](https://mp.weixin.qq.com/s?__biz=Mzg2OTA0Njk0OA==&mid=2247488377&idx=1&sn=0fb33ef330159db5a9c8bc0f029cd739&chksm=cea25cb2f9d5d5a4c7bacc9dcfc90ed86e89f4262e32b40c7aa47af84c747cb6c0429f753e1d&token=496868067&lang=zh_CN#rd) ### Nginx -- **[nginx-tutorial](https://github.com/dunwu/nginx-tutorial)**:一系列 Nginx 极简教程,包含 HTTP 反向代理、HTTPS 反向代理、负载均衡、静态站点、文件服务器搭建等实战内容。 +- [nginx-tutorial](https://github.com/dunwu/nginx-tutorial):一系列 Nginx 极简教程,包含 HTTP 反向代理、HTTPS 反向代理、负载均衡、静态站点、文件服务器搭建等实战内容。 ## 大数据 -- **[BigData-Notes](https://github.com/heibaiying/BigData-Notes "BigData-Notes")** :大数据入门指南 ⭐️。 -- **[flink-learning](https://github.com/zhisheng17/flink-learning "flink-learning")**:含 Flink 入门、概念、原理、实战、性能调优、源码解析等内容。 +- [juicy-bigdata](https://github.com/datawhalechina/juicy-bigdata):妙趣横生大数据,大数据技术相关内容的导论课程。 +- [flink-learning](https://github.com/zhisheng17/flink-learning "flink-learning"):含 Flink 入门、概念、原理、实战、性能调优、源码解析等内容。 ## 开源书籍 -- **[《高并发的哲学原理》](https://github.com/johnlui/PPHC)**:本书的目标是在作者有限的认知范围内,讨论一下高并发问题背后隐藏的一个哲学原理——找出单点,进行拆分。 -- **[《Effective Java(第 3 版)》中英对照版](https://github.com/clxering/Effective-Java-3rd-edition-Chinese-English-bilingual)**:《Effective Java(第 3 版)各章节的中英文学习参考。 -- **[《DDIA(设计数据密集型应用)》中文版](https://github.com/Vonng/ddia)**:《Designing Data-Intensive Application》DDIA 中文翻译。 -- **[《凤凰架构》](https://github.com/fenixsoft/awesome-fenix)**:讨论如何构建一套可靠的大型分布式系统。 -- **[《分布式系统模式》中文版](https://github.com/dreamhead/patterns-of-distributed-systems)**:《Patterns of Distributed Systems》中文翻译。 +- [《高并发的哲学原理》](https://github.com/johnlui/PPHC):本书的目标是在作者有限的认知范围内,讨论一下高并发问题背后隐藏的一个哲学原理——找出单点,进行拆分。 +- [《Effective Java(第 3 版)》中英对照版](https://github.com/clxering/Effective-Java-3rd-edition-Chinese-English-bilingual):《Effective Java(第 3 版)各章节的中英文学习参考。 +- [《DDIA(设计数据密集型应用)》中文版](https://github.com/Vonng/ddia):《Designing Data-Intensive Application》DDIA 中文翻译。 +- [《凤凰架构》](https://github.com/fenixsoft/awesome-fenix):讨论如何构建一套可靠的大型分布式系统。 +- [《分布式系统模式》中文版](https://github.com/dreamhead/patterns-of-distributed-systems):《Patterns of Distributed Systems》中文翻译。 diff --git a/docs/snippets/article-footer.snippet.md b/docs/snippets/article-footer.snippet.md new file mode 100644 index 00000000000..5ec368caefb --- /dev/null +++ b/docs/snippets/article-footer.snippet.md @@ -0,0 +1 @@ +![JavaGuide 官方公众号](https://oss.javaguide.cn/github/javaguide/gongzhonghaoxuanchuan.png) diff --git a/docs/snippets/article-header.snippet.md b/docs/snippets/article-header.snippet.md new file mode 100644 index 00000000000..1bac94f1cb5 --- /dev/null +++ b/docs/snippets/article-header.snippet.md @@ -0,0 +1 @@ +[![JavaGuide官方知识星球](https://oss.javaguide.cn/xingqiu/xingqiu.png)](../about-the-author/zhishixingqiu-two-years.md) diff --git a/docs/snippets/planet.snippet.md b/docs/snippets/planet.snippet.md index 32f691af337..b9f08320cde 100644 --- a/docs/snippets/planet.snippet.md +++ b/docs/snippets/planet.snippet.md @@ -1,12 +1,10 @@ -[《Java 面试指北》](https://javaguide.cn/zhuanlan/java-mian-shi-zhi-bei.html)(点击链接即可查看详细介绍)的部分内容展示如下,你可以将其看作是 [JavaGuide](https://javaguide.cn/#/) 的补充完善,两者可以配合使用。 +[《Java 面试指北》](../zhuanlan/java-mian-shi-zhi-bei.md)(点击链接即可查看详细介绍)的部分内容展示如下,你可以将其看作是 [JavaGuide](https://javaguide.cn/#/) 的补充完善,两者可以配合使用。 ![《Java 面试指北》内容概览](https://oss.javaguide.cn/xingqiu/image-20220304102536445.png) -## 星球介绍 +为了帮助更多同学准备 Java 面试以及学习 Java ,我创建了一个纯粹的[Java 面试知识星球](../about-the-author/zhishixingqiu-two-years.md)。虽然收费只有培训班/训练营的百分之一,但是知识星球里的内容质量更高,提供的服务也更全面,非常适合准备 Java 面试和学习 Java 的同学。 -为了帮助更多同学准备 Java 面试以及学习 Java ,我创建了一个纯粹的[ Java 面试知识星球](https://javaguide.cn/about-the-author/zhishixingqiu-two-years.html)。虽然收费只有培训班/训练营的百分之一,但是知识星球里的内容质量更高,提供的服务也更全面,非常适合准备 Java 面试和学习 Java 的同学。 - -**欢迎准备 Java 面试以及学习 Java 的同学加入我的 [知识星球](https://javaguide.cn/about-the-author/zhishixingqiu-two-years.html),干货非常多,学习氛围也很不错!收费虽然是白菜价,但星球里的内容或许比你参加上万的培训班质量还要高。** +**欢迎准备 Java 面试以及学习 Java 的同学加入我的 [知识星球](../about-the-author/zhishixingqiu-two-years.md),干货非常多,学习氛围也很不错!收费虽然是白菜价,但星球里的内容或许比你参加上万的培训班质量还要高。** 下面是星球提供的部分服务(点击下方图片即可获取知识星球的详细介绍): @@ -14,24 +12,14 @@ **我有自己的原则,不割韭菜,用心做内容,真心希望帮助到你!** -如果你感兴趣的话,不妨花 3 分钟左右看看星球的详细介绍:[JavaGuide 知识星球详细介绍](https://javaguide.cn/about-the-author/zhishixingqiu-two-years.html)。 - -## 如何加入? - -**方式一(不推荐)**:扫描下面的二维码原价加入(续费半价不到)。 +如果你感兴趣的话,不妨花 3 分钟左右看看星球的详细介绍:[JavaGuide 知识星球详细介绍](../about-the-author/zhishixingqiu-two-years.md) 。 -![知识星球](https://oss.javaguide.cn/xingqiu/image-20220311203414600.png) +这里再送一张 **30** 元的星球专属优惠券,数量有限(价格即将上调。老用户续费半价 ,微信扫码即可续费)! -**方式二(推荐)**:添加我的个人微信(**javaguide1024**)领取一个 **30** 元的星球专属优惠券(续费半价不到)。 +![知识星球30元优惠卷](https://oss.javaguide.cn/xingqiu/xingqiuyouhuijuan-30.jpg) -**一定要备注“优惠卷”**,不然通过不了。 - -![个人微信](https://oss.javaguide.cn/xingqiu/weixin-guidege666.jpeg) +进入星球之后,记得查看 **[星球使用指南](https://t.zsxq.com/0d18KSarv)** (一定要看!!!) 和 **[星球优质主题汇总](https://t.zsxq.com/12uSKgTIm)** ,干货多多! **无任何套路,无任何潜在收费项。用心做内容,不割韭菜!** -进入星球之后,记得查看 **[星球使用指南](https://t.zsxq.com/0d18KSarv)** (一定要看!) 。 - -随着时间推移,星球积累的干货资源越来越多,我花在星球上的时间也越来越多,星球的价格会逐步向上调整,想要加入的同学一定要尽早。 - 不过, **一定要确定需要再进** 。并且, **三天之内觉得内容不满意可以全额退款** 。 diff --git a/docs/snippets/planet2.snippet.md b/docs/snippets/planet2.snippet.md new file mode 100644 index 00000000000..891d58c8923 --- /dev/null +++ b/docs/snippets/planet2.snippet.md @@ -0,0 +1,27 @@ +## 星球其他资源 + +[知识星球](../about-the-author/zhishixingqiu-two-years.md)除了提供了 **《Java 面试指北》** 、 **《Java 必读源码系列》**(目前已经整理了 Dubbo 2.6.x 、Netty 4.x、SpringBoot2.1 的源码)、 **《手写 RPC 框架》** 、**《Kafka 常见面试题/知识点总结》** 等多个专属小册,还有读书活动、学习打卡、简历修改、免费提问、海量 Java 优质面试资源以及各种不定时的福利。 + +![知识星球专栏概览](https://oss.javaguide.cn/xingqiu/image-20220211231206733.png) + +![星球 PDF 面试手册](https://oss.javaguide.cn/xingqiu/image-20220723120918434.png) + +下面是星球提供的部分服务(点击下方图片即可获取知识星球的详细介绍): + +[![星球服务](https://oss.javaguide.cn/xingqiu/xingqiufuwu.png)](../about-the-author/zhishixingqiu-two-years.md) + +**我有自己的原则,不割韭菜,用心做内容,真心希望帮助到你!** + +如果你感兴趣的话,不妨花 3 分钟左右看看星球的详细介绍:[JavaGuide 知识星球详细介绍](../about-the-author/zhishixingqiu-two-years.md)。 + +## 星球限时优惠 + +这里再送一张 **30** 元的星球专属优惠券,数量有限(价格即将上调。老用户续费半价 ,微信扫码即可续费)! + +![知识星球30元优惠卷](https://oss.javaguide.cn/xingqiu/xingqiuyouhuijuan-30.jpg) + +进入星球之后,记得查看 **[星球使用指南](https://t.zsxq.com/0d18KSarv)** (一定要看!!!) 和 **[星球优质主题汇总](https://www.yuque.com/snailclimb/rpkqw1/ncxpnfmlng08wlf1)** 。 + +**无任何套路,无任何潜在收费项。用心做内容,不割韭菜!** + +不过, **一定要确定需要再进** 。并且, **三天之内觉得内容不满意可以全额退款** 。 diff --git a/docs/snippets/small-advertisement.snippet.md b/docs/snippets/small-advertisement.snippet.md index 51709c61012..03b14a8738c 100644 --- a/docs/snippets/small-advertisement.snippet.md +++ b/docs/snippets/small-advertisement.snippet.md @@ -1,6 +1,6 @@ ::: tip 这是一则或许对你有用的小广告 -- **面试专版**:准备 Java 面试的小伙伴可以考虑面试专版:**[《Java 面试指北 》](https://javaguide.cn/zhuanlan/java-mian-shi-zhi-bei.html)** (质量很高,专为面试打造,配合 JavaGuide 食用)。 -- **知识星球**:专属面试小册/一对一交流/简历修改/专属求职指南,欢迎加入 **[JavaGuide 知识星球](https://javaguide.cn/about-the-author/zhishixingqiu-two-years.html)**(点击链接即可查看星球的详细介绍,一定一定一定确定自己真的需要再加入,一定一定要看完详细介绍之后再加我)。 +- **面试专版**:准备 Java 面试的小伙伴可以考虑面试专版:**[《Java 面试指北 》](../zhuanlan/java-mian-shi-zhi-bei.md)** (质量非常高,专为面试打造,配合 JavaGuide 食用效果最佳)。 +- **知识星球**:技术专栏/一对一提问/简历修改/求职指南/面试打卡/不定时福利,欢迎加入 **[JavaGuide 官方知识星球](../about-the-author/zhishixingqiu-two-years.md)**。 ::: diff --git a/docs/snippets/the-way-join-planet.snippet.md b/docs/snippets/the-way-join-planet.snippet.md deleted file mode 100644 index 6b5b3e9875a..00000000000 --- a/docs/snippets/the-way-join-planet.snippet.md +++ /dev/null @@ -1,17 +0,0 @@ -**方式一(不推荐)**:扫描下面的二维码原价加入(续费半价不到)。 - -![知识星球](https://oss.javaguide.cn/xingqiu/image-20220311203414600.png) - -**方式二(推荐)**:添加我的个人微信(**javaguide1024**)领取一个 **30** 元的星球专属优惠券(续费半价不到)。 - -**一定要备注“优惠卷”**,不然通过不了。 - -![个人微信](https://oss.javaguide.cn/xingqiu/weixin-guidege666.jpeg) - -**无任何套路,无任何潜在收费项。用心做内容,不割韭菜!** - -进入星球之后,记得查看 **[星球使用指南](https://t.zsxq.com/0d18KSarv)** (一定要看!) 。 - -随着时间推移,星球积累的干货资源越来越多,我花在星球上的时间也越来越多,星球的价格会逐步向上调整,想要加入的同学一定要尽早。 - -不过, **一定要确定需要再进** 。并且, **三天之内觉得内容不满意可以全额退款** 。 diff --git a/docs/snippets/yuanma.snippet.md b/docs/snippets/yuanma.snippet.md new file mode 100644 index 00000000000..643e3ede41f --- /dev/null +++ b/docs/snippets/yuanma.snippet.md @@ -0,0 +1,25 @@ +[《Java 必读源码系列》](../zhuanlan/source-code-reading.md)(点击链接即可查看详细介绍)的部分内容展示如下。 + +![《Java 必读源码系列》](https://oss.javaguide.cn/xingqiu/image-20220621091832348.png) + +为了帮助更多同学准备 Java 面试以及学习 Java ,我创建了一个纯粹的[Java 面试知识星球](../about-the-author/zhishixingqiu-two-years.md)。虽然收费只有培训班/训练营的百分之一,但是知识星球里的内容质量更高,提供的服务也更全面,非常适合准备 Java 面试和学习 Java 的同学。 + +**欢迎准备 Java 面试以及学习 Java 的同学加入我的 [知识星球](../about-the-author/zhishixingqiu-two-years.md),干货非常多,学习氛围也很不错!收费虽然是白菜价,但星球里的内容或许比你参加上万的培训班质量还要高。** + +下面是星球提供的部分服务(点击下方图片即可获取知识星球的详细介绍): + +[![星球服务](https://oss.javaguide.cn/xingqiu/xingqiufuwu.png)](../about-the-author/zhishixingqiu-two-years.md) + +**我有自己的原则,不割韭菜,用心做内容,真心希望帮助到你!** + +如果你感兴趣的话,不妨花 3 分钟左右看看星球的详细介绍:[JavaGuide 知识星球详细介绍](../about-the-author/zhishixingqiu-two-years.md) 。 + +这里再送一个 **30** 元的星球专属优惠券,数量有限(价格即将上调。老用户续费半价 ,微信扫码即可续费)! + +![知识星球30元优惠卷](https://oss.javaguide.cn/xingqiu/xingqiuyouhuijuan-30.jpg) + +进入星球之后,记得查看 **[星球使用指南](https://t.zsxq.com/0d18KSarv)** (一定要看!!!) 和 **[星球优质主题汇总](https://t.zsxq.com/12uSKgTIm)** 。 + +**无任何套路,无任何潜在收费项。用心做内容,不割韭菜!** + +不过, **一定要确定需要再进** 。并且, **三天之内觉得内容不满意可以全额退款** 。 diff --git "a/docs/system-design/J2EE\345\237\272\347\241\200\347\237\245\350\257\206.md" "b/docs/system-design/J2EE\345\237\272\347\241\200\347\237\245\350\257\206.md" index 5e347f8094c..5e8e735af3a 100644 --- "a/docs/system-design/J2EE\345\237\272\347\241\200\347\237\245\350\257\206.md" +++ "b/docs/system-design/J2EE\345\237\272\347\241\200\347\237\245\350\257\206.md" @@ -1,4 +1,4 @@ -## Servlet 总结 +# Servlet 总结 在 Java Web 程序中,**Servlet**主要负责接收用户请求 `HttpServletRequest`,在`doGet()`,`doPost()`中做相应的处理,并将回应`HttpServletResponse`反馈给用户。**Servlet** 可以设置初始化参数,供 Servlet 内部使用。一个 Servlet 类只会有一个实例,在它初始化时调用`init()`方法,销毁时调用`destroy()`方法**。**Servlet 需要在 web.xml 中配置(MyEclipse 中创建 Servlet 会自动配置),**一个 Servlet 可以设置多个 URL 访问**。**Servlet 不是线程安全**,因此要谨慎使用类变量。 @@ -40,16 +40,20 @@ Servlet 接口定义了 5 个方法,其中**前三个方法与 Servlet 生命 参考:《javaweb 整合开发王者归来》P81 -## get 和 post 请求的区别 +## GET 和 POST 的区别 -get 和 post 请求实际上是没有区别,大家可以自行查询相关文章(参考文章:[https://www.cnblogs.com/logsharing/p/8448446.html](https://www.cnblogs.com/logsharing/p/8448446.html),知乎对应的问题链接:[get 和 post 区别?](https://www.zhihu.com/question/28586791))! +这个问题在知乎上被讨论的挺火热的,地址: 。 -可以把 get 和 post 当作两个不同的行为,两者并没有什么本质区别,底层都是 TCP 连接。 get 请求用来从服务器上获得资源,而 post 是用来向服务器提交数据。比如你要获取人员列表可以用 get 请求,你需要创建一个人员可以用 post 。这也是 Restful API 最基本的一个要求。 +![](https://static001.geekbang.org/infoq/04/0454a5fff1437c32754f1dfcc3881148.png) -推荐阅读: +GET 和 POST 是 HTTP 协议中两种常用的请求方法,它们在不同的场景和目的下有不同的特点和用法。一般来说,可以从以下几个方面来区分它们: -- -- +- 语义上的区别:GET 通常用于获取或查询资源,而 POST 通常用于创建或修改资源。GET 请求应该是幂等的,即多次重复执行不会改变资源的状态,而 POST 请求则可能有副作用,即每次执行可能会产生不同的结果或影响资源的状态。 +- 格式上的区别:GET 请求的参数通常放在 URL 中,形成查询字符串(querystring),而 POST 请求的参数通常放在请求体(body)中,可以有多种编码格式,如 application/x-www-form-urlencoded、multipart/form-data、application/json 等。GET 请求的 URL 长度受到浏览器和服务器的限制,而 POST 请求的 body 大小则没有明确的限制。 +- 缓存上的区别:由于 GET 请求是幂等的,它可以被浏览器或其他中间节点(如代理、网关)缓存起来,以提高性能和效率。而 POST 请求则不适合被缓存,因为它可能有副作用,每次执行可能需要实时的响应。 +- 安全性上的区别:GET 请求和 POST 请求都不是绝对安全的,因为 HTTP 协议本身是明文传输的,无论是 URL、header 还是 body 都可能被窃取或篡改。为了保证安全性,必须使用 HTTPS 协议来加密传输数据。不过,在一些场景下,GET 请求相比 POST 请求更容易泄露敏感数据,因为 GET 请求的参数会出现在 URL 中,而 URL 可能会被记录在浏览器历史、服务器日志、代理日志等地方。因此,一般情况下,私密数据传输应该使用 POST + body。 + +重点搞清了,两者在语义上的区别即可。不过,也有一些项目所有的请求都用 POST,这个并不是固定的,项目组达成共识即可。 ## 什么情况下调用 doGet()和 doPost() @@ -141,29 +145,29 @@ JSP 有 9 个内置对象: ## Request 对象的主要方法有哪些 -- setAttribute(String name,Object):设置名字为 name 的 request 的参数值 -- getAttribute(String name):返回由 name 指定的属性值 -- getAttributeNames():返回 request 对象所有属性的名字集合,结果是一个枚举的实例 -- getCookies():返回客户端的所有 Cookie 对象,结果是一个 Cookie 数组 -- getCharacterEncoding():返回请求中的字符编码方式 = getContentLength():返回请求的 Body 的长度 -- getHeader(String name):获得 HTTP 协议定义的文件头信息 -- getHeaders(String name):返回指定名字的 request Header 的所有值,结果是一个枚举的实例 -- getHeaderNames():返回所以 request Header 的名字,结果是一个枚举的实例 -- getInputStream():返回请求的输入流,用于获得请求中的数据 -- getMethod():获得客户端向服务器端传送数据的方法 -- getParameter(String name):获得客户端传送给服务器端的有 name 指定的参数值 -- getParameterNames():获得客户端传送给服务器端的所有参数的名字,结果是一个枚举的实例 -- getParameterValues(String name):获得有 name 指定的参数的所有值 -- getProtocol():获取客户端向服务器端传送数据所依据的协议名称 -- getQueryString():获得查询字符串 -- getRequestURI():获取发出请求字符串的客户端地址 -- getRemoteAddr():获取客户端的 IP 地址 -- getRemoteHost():获取客户端的名字 -- getSession([Boolean create]):返回和请求相关 Session -- getServerName():获取服务器的名字 -- getServletPath():获取客户端所请求的脚本文件的路径 -- getServerPort():获取服务器的端口号 -- removeAttribute(String name):删除请求中的一个属性 +- `setAttribute(String name,Object)`:设置名字为 name 的 request 的参数值 +- `getAttribute(String name)`:返回由 name 指定的属性值 +- `getAttributeNames()`:返回 request 对象所有属性的名字集合,结果是一个枚举的实例 +- `getCookies()`:返回客户端的所有 Cookie 对象,结果是一个 Cookie 数组 +- `getCharacterEncoding()`:返回请求中的字符编码方式 = getContentLength()`:返回请求的 Body 的长度 +- `getHeader(String name)`:获得 HTTP 协议定义的文件头信息 +- `getHeaders(String name)`:返回指定名字的 request Header 的所有值,结果是一个枚举的实例 +- `getHeaderNames()`:返回所以 request Header 的名字,结果是一个枚举的实例 +- `getInputStream()`:返回请求的输入流,用于获得请求中的数据 +- `getMethod()`:获得客户端向服务器端传送数据的方法 +- `getParameter(String name)`:获得客户端传送给服务器端的有 name 指定的参数值 +- `getParameterNames()`:获得客户端传送给服务器端的所有参数的名字,结果是一个枚举的实例 +- `getParameterValues(String name)`:获得有 name 指定的参数的所有值 +- `getProtocol()`:获取客户端向服务器端传送数据所依据的协议名称 +- `getQueryString()`:获得查询字符串 +- `getRequestURI()`:获取发出请求字符串的客户端地址 +- `getRemoteAddr()`:获取客户端的 IP 地址 +- `getRemoteHost()`:获取客户端的名字 +- `getSession([Boolean create])`:返回和请求相关 Session +- `getServerName()`:获取服务器的名字 +- `getServletPath()`:获取客户端所请求的脚本文件的路径 +- `getServerPort()`:获取服务器的端口号 +- `removeAttribute(String name)`:删除请求中的一个属性 ## request.getAttribute()和 request.getParameter()有何区别 @@ -284,3 +288,5 @@ Cookie 和 Session 都是用来跟踪浏览器用户身份的会话方式,但 Cookie 数据保存在客户端(浏览器端),Session 数据保存在服务器端。 Cookie 存储在客户端中,而 Session 存储在服务器上,相对来说 Session 安全性更高。如果使用 Cookie 的一些敏感信息不要写入 Cookie 中,最好能将 Cookie 信息加密然后使用到的时候再去服务器端解密。 + + diff --git a/docs/system-design/basis/RESTfulAPI.md b/docs/system-design/basis/RESTfulAPI.md index 944cff397d1..15671201961 100644 --- a/docs/system-design/basis/RESTfulAPI.md +++ b/docs/system-design/basis/RESTfulAPI.md @@ -25,7 +25,7 @@ category: 代码质量 1. 你通过某电商网站搜索某某商品,电商网站的前端就调用了后端提供了搜索商品相关的 API。 2. 你使用 JDK 开发 Java 程序,想要读取用户的输入的话,你就需要使用 JDK 提供的 IO 相关的 API。 -3. ...... +3. …… 你可以把 API 理解为程序与程序之间通信的桥梁,其本质就是一个函数而已。另外,API 的使用也不是没有章法的,它的规则由(比如数据输入和输出的格式)API 提供方制定。 @@ -37,7 +37,7 @@ category: 代码质量 举个例子,如果我给你下面两个 API 你是不是立马能知道它们是干什么用的!这就是 RESTful API 的强大之处! -``` +```plain GET /classes:列出所有班级 POST /classes:新建一个班级 ``` @@ -87,7 +87,7 @@ POST /classes:新建一个班级 Talk is cheap!来举个实际的例子来说明一下吧!现在有这样一个 API 提供班级(class)的信息,还包括班级中的学生和教师的信息,则它的路径应该设计成下面这样。 -``` +```plain GET /classes:列出所有班级 POST /classes:新建一个班级 GET /classes/{classId}:获取某个指定班级的信息 @@ -101,7 +101,7 @@ DELETE /classes/{classId}/teachers/{ID}:删除某个指定班级下的指定 反例: -``` +```plain /getAllclasses /createNewclass /deleteAllActiveclasses @@ -113,13 +113,13 @@ DELETE /classes/{classId}/teachers/{ID}:删除某个指定班级下的指定 如果我们在查询的时候需要添加特定条件的话,建议使用 url 参数的形式。比如我们要查询 state 状态为 active 并且 name 为 guidegege 的班级: -``` +```plain GET /classes?state=active&name=guidegege ``` 比如我们要实现分页查询: -``` +```plain GET /classes?page=1&size=10 //指定第1页,每页10个数据 ``` @@ -175,3 +175,5 @@ GET /classes?page=1&size=10 //指定第1页,每页10个数据 - - + + diff --git a/docs/system-design/basis/naming.md b/docs/system-design/basis/naming.md index 9b27c6f85fe..4be3d038848 100644 --- a/docs/system-design/basis/naming.md +++ b/docs/system-design/basis/naming.md @@ -242,8 +242,10 @@ Codelf 提供了在线网站版本,网址:[https://unbug.github.io/codelf/]( 好的命名对于其他人(包括你自己)理解你的代码有着很大的帮助!你的代码越容易被理解,可维护性就越强,侧面也就说明你的代码设计的也就越好! -在日常编码过程中,我们需要谨记常见命名规范比如类名需要使用大驼峰命名法、不要使用拼音,更不要使用中文......。 +在日常编码过程中,我们需要谨记常见命名规范比如类名需要使用大驼峰命名法、不要使用拼音,更不要使用中文……。 另外,国人开发的一个叫做 Codelf 的网站被很多人称为“变量命名神器”,当你为命名而头疼的时候,你可以去参考一下上面提供的一些命名示例。 最后,祝愿大家都不用再为命名而困扰! + + diff --git a/docs/system-design/basis/pictures/common-design-patterns.drawio b/docs/system-design/basis/pictures/common-design-patterns.drawio index 31dfbb3ef65..80bea68175f 100644 --- a/docs/system-design/basis/pictures/common-design-patterns.drawio +++ b/docs/system-design/basis/pictures/common-design-patterns.drawio @@ -1 +1,106 @@ -7Zzrc5s4EMD/Gn2MBxAvfQQbcteZ3mNyN+3clw6xsU2DjYtJnNxff1oh8ZRzdmMMaak7DqwwD+1Pq92VBMLTzfNtGuzWH5NFGCNNWTwjPEOaptrYpn9A8pJLbAXnglUaLfhBpeAu+jfkQoVLH6NFuK8dmCVJnEW7unCebLfhPKvJgjRNDvXDlklcv+ouWIUtwd08iNvST9EiW/On0KxS/ksYrdbiyqpJ8pL7YP6wSpPHLb8e0rBv+r7P62MTiHPxB92vg0VyqIiwh/A0TZIs39o8T8MY6lZUW/47/0hpcd9puM1O+cGN9yWZfTtYL59mu6d/fPz052/fbvhZnoL4MRSPwW42exEVxB4xhJOoCLuHdZSFd7tgDqUHigSVrbNNzIv3WZo8FBXJJOKxFdh5CLP5mu8sozieJnGSsutgferoxAJ5ss38YBPFQNSHMHPTINru6U19TLYJL79LHlN2C+sso6BoBnboF312+IID9pNVkqziMNhF+8k82bCC+Z4d6i/zs9PN6vkNzeVXyJ9C3Ns22dIrue36FpUXpln4XBHx+r8Nk02YpfQ6Ci+9UQX1vLHQ1pLvH0r0iABmXcHOULgw4LivipOXKqcbXOvnEID/HwHAfNfSrva6dt+7FguTE9yLelBe165WV65htpVrqRLlqqQz5epnKPf9q/J1wL9fwT0q0Li0gT7VHC+C0F7Oj9V9/0icqn39iPYHpWVTomUzznhF1tRtfntMRMHNnrlTtFoVzdg9l4V0a8X+ejZyVGRPkacj10augzwDEQ/ZrrgAynVR/KJJF621TNbHN4Go0MNFQRyttnR3TjUUUrkLOoio5+Xwgk20WMBlpMyWVAsTxH1HTRm4sbo8mbxUs4wJUSqfWncjTEUFY9WwJ6pdfrQ21FpXTFsSv8IzkcOApBtkhly/RRs9MXX8jzHxqqPZRKRhz5bLpTY/as9+Tp7q/orEXSFSV7QrYiTOCtgvC1FwqdlyfER8kFCn2TZGdPpER9UHxo7ET6KkuDpyMOv7XPjvEUQwcr2RnV7NjjEwdiTel2chZ4aICRuEmp7R3PSKDDbJhDo85T88LIIsCUHUz1aRO+PWx9FHgvokSBc6GgoytrzD8sHucGQwg4iAz8yDtf0u2J4SDSo0GGRRYDMepBXfSOUb9KZByhLRxZ54CIM9BqgLtsFhNOC2c32/fqxaHCv0+V2n0crT5HVVlNAvGkdoBlSP4ahil0Cp65c/y2ur+JlodlRQbXhQzpoeyFnjA4nKdqsNsJQWjRBE7JlYBYubzdE8pTqKoqImSn4NILisDLusOuWllJtqRX4o7pPJSKWIo12UGZWyYF/KV5XrN/XEdgtlVYV1hPhxLdaKtEMO85G0wwWN45Dt3ZsSm28N+FTctxEk8ojPAzsIzroF29eI+EZGjkV2vUMihsgazhV1qGwLNigqxGYbM3DVR0iuYUiMgUFCZNlGljsCL4oAKrZTcadGSLqHBFtDg0SWYGRguCakpKGXUUc2rsFGMxzrnw1ZCM/GzIp4zHZHV+SqkBikbkBIz4xgCSNNEN44W+qsQfghk9KG4uzZUs3+A+tt/euyyVLdDdTjE6ZjyCdLgXLrQ9qrONjvhfIGrMlrT6NqqR23B7ax6KfrQ9mdqV02P+OY2t+/Kl9H/yLTqK6tQEm29UITbCxwDWj4AVMaPGTr4wSboQwSFK329NxHM2JpWx6Zw9HZHBosy48VYwMWojdjm136oj/buNL5yAxsDo1wkZqzrlQYwoYQ1+t4LHvIELyp+zvfmAwNDclaD8iMKWx6FYGo1p5dJUU2WpX3NlCjaxJ2bOiFKDWUHWpTXGVMr/ZqX/qHRJaDzwfxbLYxQ047bhrZ6MJ+DK3vkWfeIamaB0y0Exq7nH67nKGZE1PGjA5LWBwXmAF/BY/m5LIrVM1Gyr3Ir/Y2LiPL3HSac1/a8/BHsQ3nr1C2yMSoETCArDuRpUEM6DEIswQwub+6sG3wCTbjxweJmHWKjJ5Taaoiy4ywkJcYrF+RsjO6IN2YGTEYLPAgp+HRWTejKrLsCDUtFkucGZA4s9s9z4hHJ3iotjE0PGQJkKLnYVk0RxnXJvZsRnSzd05kORBmPWDp6jEPZQxaLmouromB8dff9/4tXj/8+uWP2/3vXz+bh0z2VqU8YHXyFzFMISkGEh86GM9nEudD8BTcwtuwxMjxfYpOGJhW8bHFPpBhmcEqNFjN6EJuVlzqK70Ue/HWZL4dqL8cB/dh7Bbv2JK8f4Rjr2K+X7GKLvuwc2ZBFiXbZoNQB9EgjoAuaQ7H0zZmPWDHajtcU21ZuHY+/Igv8SnLKot7sPcf \ No newline at end of file + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/docs/system-design/basis/pictures/programming-principles .drawio b/docs/system-design/basis/pictures/programming-principles .drawio deleted file mode 100644 index 50c31e07abf..00000000000 --- a/docs/system-design/basis/pictures/programming-principles .drawio +++ /dev/null @@ -1 +0,0 @@ -7ZlRb5swEIB/jR9bYQwUHoGErt26SY20qnuZXHAAleCUOE3aX7+zMUmI2bQ9JK26KFJi3xmfOX++uwAi8Wx92dB5ccMzViHbytaIjJBtYwtj+JGSl1biW6QV5E2Z6UFbwaR8Zd2VWrosM7boDRScV6Kc94Upr2uWip6MNg1f9YdNedW3Oqc5MwSTlFam9K7MRKHvwr7Yyj+xMi86y9gLWs0DTR/zhi9rbQ/ZJPGSJPFb9Yx2c+kbXRQ046sdERkjEjeci7Y1W8eskr7t3NZel/xGu1l3w2rxNxfcfZ/+eLJe4ofX7/Gt81NE/Cc+07M802rJuttQixUvnYPULTI5CUYkWhWlYJM5TaV2BUiArBCzSqsXouGPG0dKybSsqphXvFGzEScOneBiM7LT1LxmUtj5yJKdRybSQnemvBYJnZWVxOyaiaihZb2Ald7wmmv9hC8bta5CCKDHdkkIX+AQ+SUHLM5zzvOK0Xm5OE/5TCnShRqaTNvZobk7v2tH2oLp7855rBFsvSPS/r9kfMZEA1Nam9OiWdBnxfZ0f7UlL+h4KXaoI74WUk17vpl7u+PQ0Jv+DwDY/rEJYDhz2X9LgBP0CCDEJID42CTAdg9GQGAQMPn25WpkYAC3KIb2em8Hd7dbi2hV5jV0U3AcA3kkHVZCDA61YlZmmTQzCNcWvw4DnUVs9+MDQyyvD8xQyBiIGPahcCGOGTDGLvJdFMZo7KAoRhDfpSRBETQSJQmv6TO9lKke2V4lMXpoenR5T0uZCNWmnC3U/sJOWNibr5UjOz20cvUrTY1QADZ9BNER7HemWjOwgoCg0H2nGFf0gVXRpoTYN76FHHu6vxNCI/VRcwoqSl4P4d87GB/iLDjOXvoMzLOA/YHDgA92GNzhwwBguopQxaYkFCNfoRo5KCQGk2AK6t3fkfPn7LoTDy0z206nzEvT/zXb+n1gHPx3wfNguZZ4g7xA2JKYBDJyhmbiPdFxoNT63ugYqMUBCv9CxY4WkwSNPRREKFINCDShWa+feDlM+sHOO+PFrNwVFGNZ9rQVGOQaGVZCFDhoDMHFQ1F04uU4vNhkL74E7tvy0pVPPV6gSBmjYKRqEyiZPQ2O30pGsqo/8XKcfOS9N17Mp4OfryYTg4c3ez5kPAv4cEx4uM/E0PMifDEARXAwKGwDivvw8uvViYq3o8IZ+iN8XCqIQcXo9v7ExPGY2LyxOkKkgO72vZXS7bwcJONf \ No newline at end of file diff --git a/docs/system-design/basis/pictures/programming-principles.drawio b/docs/system-design/basis/pictures/programming-principles.drawio new file mode 100644 index 00000000000..3fba02cccae --- /dev/null +++ b/docs/system-design/basis/pictures/programming-principles.drawio @@ -0,0 +1,49 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/docs/system-design/basis/refactoring.md b/docs/system-design/basis/refactoring.md index 3fff7ff29d3..c6042837743 100644 --- a/docs/system-design/basis/refactoring.md +++ b/docs/system-design/basis/refactoring.md @@ -28,7 +28,7 @@ category: 代码质量 **常见的软件设计原则如下**: -![常见的软件设计原则](https://oss.javaguide.cn/github/javaguide/system-design/basis/programming-principles%20.png) +![常见的软件设计原则](https://oss.javaguide.cn/github/javaguide/system-design/basis/programming-principles.png) 更全面的设计原则总结,可以看 **[java-design-patterns](https://github.com/iluwatar/java-design-patterns)** 和 **[hacker-laws-zh](https://github.com/nusr/hacker-laws-zh)** 这两个开源项目。 @@ -42,7 +42,7 @@ category: 代码质量 2. **避免代码腐化**:通过重构干掉坏味道代码; 3. **加深对代码的理解**:重构代码的过程会加深你对某部分代码的理解; 4. **发现潜在 bug**:是这样的,很多潜在的 bug ,都是我们在重构的过程中发现的; -5. ...... +5. …… 看了上面介绍的关于重构带来的好处之后,你会发现重构的最终目标是 **提高软件开发速度和质量** 。 @@ -54,6 +54,12 @@ category: 代码质量 > 重构的唯一目的就是让我们开发更快,用更少的工作量创造更大的价值。 +## 性能优化就是重构吗? + +重构的目的是提高代码的可读性、可维护性和灵活性,它关注的是代码的内部结构——如何让开发者更容易理解代码,如何让后续的功能开发和维护更加高效。而性能优化则是为了让代码运行得更快、占用更少的资源,它关注的是程序的外部表现——如何减少响应时间、降低资源消耗、提升系统吞吐量。这两者看似对立,但实际上它们的目标是统一的,都是为了提高软件的整体质量。 + +在实际开发中,理想的做法是首先**确保代码的可读性和可维护性**,然后根据实际需求选择合适的性能优化手段。优秀的软件设计不是一味追求性能最大化,而是要在可维护性和性能之间找到平衡。通过这种方式,我们可以打造既**易于管理**又具有**良好性能**的软件系统。 + ## 何时进行重构? 重构在是开发过程中随时可以进行的,见机行事即可,并不需要单独分配一两天的时间专门用来重构。 @@ -122,7 +128,7 @@ Code Review 可以非常有效提高代码的整体质量,它会帮助我们 - 学习了某个设计模式/工程实践之后,不顾项目实际情况,刻意使用在项目上(避免货物崇拜编程); - 项目进展比较急的时候,重构项目调用的某个 API 的底层代码(重构之后对项目调用这个 API 并没有带来什么价值); - 重写比重构更容易更省事; -- ...... +- …… ### 遵循方法 @@ -132,6 +138,7 @@ Code Review 可以非常有效提高代码的整体质量,它会帮助我们 除了可以在重构项目代码的过程中练习精进重构之外,你还可以有下面这些手段: +- [当我重构时,我在想些什么](https://mp.weixin.qq.com/s/pFaFKMXzNCOuW2SD9Co40g):转转技术的这篇文章总结了常见的重构场景和重构方式。 - [重构实战练习](https://linesh.gitbook.io/refactoring/):通过几个小案例一步一步带你学习重构! - [设计模式+重构学习网站](https://refactoringguru.cn/):免费在线学习代码重构、 设计模式、 SOLID 原则 (单一职责、 开闭原则、 里氏替换、 接口隔离以及依赖反转) 。 - [IDEA 官方文档的代码重构教程](https://www.jetbrains.com/help/idea/refactoring-source-code.html#popular-refactorings):教你如何使用 IDEA 进行重构。 @@ -140,3 +147,5 @@ Code Review 可以非常有效提高代码的整体质量,它会帮助我们 - [再读《重构》- ThoughtWorks 洞见 - 2020](https://insights.thoughtworks.cn/reread-refactoring/):详细介绍了重构的要点比如小步重构、捡垃圾式的重构,主要是重构概念相关的介绍。 - [常见代码重构技巧 - VectorJin - 2021](https://juejin.cn/post/6954378167947624484):从软件设计原则、设计模式、代码分层、命名规范等角度介绍了如何进行重构,比较偏实战。 + + diff --git a/docs/system-design/basis/software-engineering.md b/docs/system-design/basis/software-engineering.md index eb860a825aa..c6cd4fa3188 100644 --- a/docs/system-design/basis/software-engineering.md +++ b/docs/system-design/basis/software-engineering.md @@ -48,7 +48,7 @@ Dijkstra(Dijkstra 算法的作者) 在 1972 年图灵奖获奖感言中也 **瀑布模型** 定义了一套完成的软件开发周期,完整地展示了一个软件的的生命周期。 -![](https://oscimg.oschina.net/oscnet/up-264f2750a3d30366e36c375ec3a30ec2775.png) +![](https://oss.javaguide.cn/github/javaguide/system-design/schedule-task/up-264f2750a3d30366e36c375ec3a30ec2775.png) **敏捷开发模型** 是目前使用的最多的一种软件开发模型。[MBA 智库百科对敏捷开发的描述](https://wiki.mbalib.com/wiki/%E6%95%8F%E6%8D%B7%E5%BC%80%E5%8F%91)是这样的: @@ -82,7 +82,7 @@ Dijkstra(Dijkstra 算法的作者) 在 1972 年图灵奖获奖感言中也 这个最小可行产品,可以理解为刚好能够满足客户需求的产品。下面这张图片把这个思想展示的非常精髓。 -![](https://oscimg.oschina.net/oscnet/up-a99961ff7725106c0592abca845d555568a.png) +![](https://oss.javaguide.cn/github/javaguide/system-design/schedule-task/up-a99961ff7725106c0592abca845d555568a.png) 利用最小可行产品,我们可以也可以提早进行市场分析,这对于我们在探索产品不确定性的道路上非常有帮助。可以非常有效地指导我们下一步该往哪里走。 @@ -96,3 +96,5 @@ Dijkstra(Dijkstra 算法的作者) 在 1972 年图灵奖获奖感言中也 - 软件工程的基本概念-清华大学软件学院 刘强: - 软件开发过程-维基百科:[https://zh.wikipedia.org/wiki/软件开发过程](https://zh.wikipedia.org/wiki/软件开发过程) + + diff --git a/docs/system-design/basis/unit-test.md b/docs/system-design/basis/unit-test.md index d3e73515db4..3331eb2791c 100644 --- a/docs/system-design/basis/unit-test.md +++ b/docs/system-design/basis/unit-test.md @@ -27,7 +27,7 @@ category: 代码质量 每个开发者都会经历重构,重构后把代码改坏了的情况并不少见,很可能你只是修改了一个很简单的方法就导致系统出现了一个比较严重的错误。 -如果有了单元测试的话,就不会存在这个隐患了。写完一个类,把单元测试写了,确保这个类逻辑正确;写第二个类,单元测试.....写 100 个类,道理一样,每个类做到第一点“保证逻辑正确性”,100 个类拼在一起肯定不出问题。你大可以放心一边重构,一边运行 APP;而不是整体重构完,提心吊胆地 run。 +如果有了单元测试的话,就不会存在这个隐患了。写完一个类,把单元测试写了,确保这个类逻辑正确;写第二个类,单元测试……写 100 个类,道理一样,每个类做到第一点“保证逻辑正确性”,100 个类拼在一起肯定不出问题。你大可以放心一边重构,一边运行 APP;而不是整体重构完,提心吊胆地 run。 ### 提高代码质量 @@ -41,7 +41,7 @@ category: 代码质量 ### 快速定位 bug -如果程序有 bug,我们运行一次全部单元测试,找到不通过的测试,可以很快地定位对应的执行代码。修复代码后,运行对应的单元测试;如还不通过,继续修改,运行测试.....直到**测试通过**。 +如果程序有 bug,我们运行一次全部单元测试,找到不通过的测试,可以很快地定位对应的执行代码。修复代码后,运行对应的单元测试;如还不通过,继续修改,运行测试……直到**测试通过**。 ### 持续集成依赖单元测试 @@ -57,7 +57,7 @@ category: 代码质量 ### 大牛都写单元测试 -国外很多家喻户晓的开源项目,都有大量单元测试。例如,[retrofit](https://link.jianshu.com?t=https://github.com/square/retrofit/tree/master/retrofit/src/test/java/retrofit2)、[okhttp](https://link.jianshu.com?t=https://github.com/square/okhttp/tree/master/okhttp-tests/src/test/java/okhttp3)、[butterknife](https://link.jianshu.com?t=https://github.com/JakeWharton/butterknife/tree/master/butterknife-compiler/src/test/java/butterknife).... 国外大牛都写单元测试,我们也写吧! +国外很多家喻户晓的开源项目,都有大量单元测试。例如,[retrofit](https://link.jianshu.com?t=https://github.com/square/retrofit/tree/master/retrofit/src/test/java/retrofit2)、[okhttp](https://link.jianshu.com?t=https://github.com/square/okhttp/tree/master/okhttp-tests/src/test/java/okhttp3)、[butterknife](https://link.jianshu.com?t=https://github.com/JakeWharton/butterknife/tree/master/butterknife-compiler/src/test/java/butterknife)…… 国外大牛都写单元测试,我们也写吧! 很多读者都有这种想法,一开始满腔热血。当真要对自己项目单元测试时,便困难重重,很大原因是项目对单元测试不友好。最后只能对一些不痛不痒的工具类做单元测试,久而久之,当初美好愿望也不了了之。 @@ -67,7 +67,7 @@ category: 代码质量 ### 心虚 -笔者也是个不太相信自己代码的人,总觉得哪里会突然冒出莫名其妙的 bug,也怕别人不小心改了自己的代码(被害妄想症),新版本上线提心吊胆......花点时间写单元测试,有事没事跑一下测试,确保原逻辑没问题,至少能睡安稳一点。 +笔者也是个不太相信自己代码的人,总觉得哪里会突然冒出莫名其妙的 bug,也怕别人不小心改了自己的代码(被害妄想症),新版本上线提心吊胆……花点时间写单元测试,有事没事跑一下测试,确保原逻辑没问题,至少能睡安稳一点。 ## TDD 测试驱动开发 @@ -113,7 +113,7 @@ JUnit 几乎是默认选择,但是其不支持 Mock,因此我们还需要选 究竟是选择 Mockito 还是 Spock 呢?我这里做了一些简单的对比分析: -- Spock 没办法 Mock 静态方法和私有方法 ,Mockito 3.4.0 以后,支持静态方法的 Mock,具体可以看这个 issue: +- Spock 没办法 Mock 静态方法和私有方法 ,Mockito 3.4.0 以后,支持静态方法的 Mock,具体可以看这个 issue:,具体教程可以看这篇文章: - Spock 基于 Groovy,写出来的测试代码更清晰易读,比较规范(自带 given-when-then 的常用测试结构规范)。Mockito 没有具体的结构规范,需要项目组自己约定一个或者遵守比较好的测试代码实践。通常来说,同样的测试用例,Spock 的代码要更简洁。 - Mockito 使用的人群更广泛,稳定可靠。并且,Mockito 是 SpringBoot Test 默认集成的 Mock 工具。 @@ -134,3 +134,5 @@ Mockito 和 Spock 都是非常不错的 Mock 工具,相对来说,Mockito 的 作为一名经验丰富的程序员,写单元测试更多的是**对自己的代码负责**。有测试用例的代码,别人更容易看懂,以后别人接手你的代码时,也可能放心做改动。 **多敲代码实践,多跟有单元测试经验的工程师交流**,你会发现写单元测试获得的收益会更多。 + + diff --git a/docs/system-design/design-pattern.md b/docs/system-design/design-pattern.md index 2c07d175e2c..2b9541f8678 100644 --- a/docs/system-design/design-pattern.md +++ b/docs/system-design/design-pattern.md @@ -18,3 +18,5 @@ head: **《设计模式》PDF 电子书内容概览**: ![《设计模式》PDF文档概览](https://oss.javaguide.cn/github/javaguide/system-design/design-pattern-pdf.png) + + diff --git a/docs/system-design/framework/mybatis/mybatis-interview.md b/docs/system-design/framework/mybatis/mybatis-interview.md index 6acb2c120cc..988111d777a 100644 --- a/docs/system-design/framework/mybatis/mybatis-interview.md +++ b/docs/system-design/framework/mybatis/mybatis-interview.md @@ -13,7 +13,11 @@ head: content: 几道常见的 MyBatis 常见 --- + + > 本篇文章由 JavaGuide 收集自网络,原出处不明。 +> +> 比起这些枯燥的面试题,我更建议你看看文末推荐的 MyBatis 优质好文。 ### #{} 和 \${} 的区别是什么? @@ -21,7 +25,16 @@ head: 答: -- `${}`是 Properties 文件中的变量占位符,它可以用于标签属性值和 sql 内部,属于静态文本替换,比如\${driver}会被静态替换为`com.mysql.jdbc. Driver`。 +- `${}`是 Properties 文件中的变量占位符,它可以用于标签属性值和 sql 内部,属于原样文本替换,可以替换任意内容,比如\${driver}会被原样替换为`com.mysql.jdbc. Driver`。 + +一个示例:根据参数按任意字段排序: + +```sql +select * from users order by ${orderCols} +``` + +`orderCols`可以是 `name`、`name desc`、`name,sex asc`等,实现灵活的排序。 + - `#{}`是 sql 的参数占位符,MyBatis 会将 sql 中的`#{}`替换为? 号,在 sql 执行前会使用 PreparedStatement 的参数设置方法,按序给 sql 的? 号占位符设置参数值,比如 ps.setInt(0, parameterValue),`#{item.name}` 的取值方式为使用反射从参数对象中获取 item 对象的 name 属性值,相当于 `param.getItem().getName()`。 ### xml 映射文件中,除了常见的 select、insert、update、delete 标签之外,还有哪些标签? @@ -295,3 +308,13 @@ MyBatis 提供了 9 种动态 sql 标签: 答:Hibernate 属于全自动 ORM 映射工具,使用 Hibernate 查询关联对象或者关联集合对象时,可以根据对象关系模型直接获取,所以它是全自动的。而 MyBatis 在查询关联对象或关联集合对象时,需要手动编写 sql 来完成,所以,称之为半自动 ORM 映射工具。 面试题看似都很简单,但是想要能正确回答上来,必定是研究过源码且深入的人,而不是仅会使用的人或者用的很熟的人,以上所有面试题及其答案所涉及的内容,在我的 MyBatis 系列博客中都有详细讲解和原理分析。 + + + +### 文章推荐 + +- [2W 字全面剖析 Mybatis 中的 9 种设计模式](https://juejin.cn/post/7273516671574687759) +- [从零开始实现一个 MyBatis 加解密插件](https://mp.weixin.qq.com/s/WUEAdFDwZsZ4EKO8ix0ijg) +- [MyBatis 最全使用指南](https://juejin.cn/post/7051910683264286750) +- [脑洞打开!第一次看到这样使用 MyBatis 的,看得我一愣一愣的。](https://juejin.cn/post/7269390456530190376) +- [MyBatis 居然也有并发问题](https://juejin.cn/post/7264921613551730722) diff --git a/docs/system-design/framework/netty.md b/docs/system-design/framework/netty.md index 504919a41a2..1a0833f86e1 100644 --- a/docs/system-design/framework/netty.md +++ b/docs/system-design/framework/netty.md @@ -7,3 +7,5 @@ icon: "network" **Netty** 相关的面试题为我的[知识星球](https://javaguide.cn/about-the-author/zhishixingqiu-two-years.html)(点击链接即可查看详细介绍以及加入方法)专属内容,已经整理到了[《Java 面试指北》](https://javaguide.cn/zhuanlan/java-mian-shi-zhi-bei.html)中。 + + diff --git a/docs/system-design/framework/spring/Async.md b/docs/system-design/framework/spring/Async.md new file mode 100644 index 00000000000..a27eb61c970 --- /dev/null +++ b/docs/system-design/framework/spring/Async.md @@ -0,0 +1,721 @@ +--- +title: Async 注解原理分析 +category: 框架 +tag: + - Spring +--- + +`@Async` 注解由 Spring 框架提供,被该注解标注的类或方法会在 **异步线程** 中执行。这意味着当方法被调用时,调用者将不会等待该方法执行完成,而是可以继续执行后续的代码。 + +`@Async` 注解的使用非常简单,需要两个步骤: + +1. 在启动类上添加注解 `@EnableAsync` ,开启异步任务。 +2. 在需要异步执行的方法或类上添加注解 `@Async` 。 + +```java +@SpringBootApplication +// 开启异步任务 +@EnableAsync +public class YourApplication { + + public static void main(String[] args) { + SpringApplication.run(YourApplication.class, args); + } +} + +// 异步服务类 +@Service +public class MyService { + + // 推荐使用自定义线程池,这里只是演示基本用法 + @Async + public CompletableFuture doSomethingAsync() { + + // 这里会有一些业务耗时操作 + // ... + // 使用 CompletableFuture 可以更方便地处理异步任务的结果,避免阻塞主线程 + return CompletableFuture.completedFuture("Async Task Completed"); + } + +} +``` + +接下来,我们一起来看看 `@Async` 的底层原理。 + +## @Async 原理分析 + +`@Async` 可以异步执行任务,本质上是使用 **动态代理** 来实现的。通过 Spring 中的后置处理器 `BeanPostProcessor` 为使用 `@Async` 注解的类创建动态代理,之后 `@Async` 注解方法的调用会被动态代理拦截,在拦截器中将方法的执行封装为异步任务提交给线程池处理。 + +接下来,我们来详细分析一下。 + +### 开启异步 + +使用 `@Async` 之前,需要在启动类上添加 `@EnableAsync` 来开启异步,`@EnableAsync` 注解如下: + +```JAVA +// 省略其他注解 ... +@Import(AsyncConfigurationSelector.class) +public @interface EnableAsync { /* ... */ } +``` + +在 `@EnableAsync` 注解上通过 `@Import` 注解引入了 `AsyncConfigurationSelector` ,因此 Spring 会去加载通过 `@Import` 注解引入的类。 + +`AsyncConfigurationSelector` 类实现了 `ImportSelector` 接口,因此在该类中会重写 `selectImports()` 方法来自定义加载 Bean 的逻辑,如下: + +```JAVA +public class AsyncConfigurationSelector extends AdviceModeImportSelector { + @Override + @Nullable + public String[] selectImports(AdviceMode adviceMode) { + switch (adviceMode) { + // 基于 JDK 代理织入的通知 + case PROXY: + return new String[] {ProxyAsyncConfiguration.class.getName()}; + // 基于 AspectJ 织入的通知 + case ASPECTJ: + return new String[] {ASYNC_EXECUTION_ASPECT_CONFIGURATION_CLASS_NAME}; + default: + return null; + } + } +} +``` + +在 `selectImports()` 方法中,会根据通知的不同类型来选择加载不同的类,其中 `adviceMode` 默认值为 `PROXY` 。 + +这里以基于 JDK 代理的通知为例,此时会加载 `ProxyAsyncConfiguration` 类,如下: + +```JAVA +@Configuration +@Role(BeanDefinition.ROLE_INFRASTRUCTURE) +public class ProxyAsyncConfiguration extends AbstractAsyncConfiguration { + @Bean(name = TaskManagementConfigUtils.ASYNC_ANNOTATION_PROCESSOR_BEAN_NAME) + @Role(BeanDefinition.ROLE_INFRASTRUCTURE) + public AsyncAnnotationBeanPostProcessor asyncAdvisor() { + // ... + // 加载后置处理器 + AsyncAnnotationBeanPostProcessor bpp = new AsyncAnnotationBeanPostProcessor(); + + // ... + return bpp; + } +} +``` + +### 后置处理器 + +在 `ProxyAsyncConfiguration` 类中,会通过 `@Bean` 注解加载一个后置处理器 `AsyncAnnotationBeanPostProcessor` ,这个后置处理器是使 `@Async` 注解起作用的关键。 + +如果某一个类或者方法上使用了 `@Async` 注解,`AsyncAnnotationBeanPostProcessor` 处理器就会为该类创建一个动态代理。 + +该类的方法在执行时,会被代理对象的拦截器所拦截,其中被 `@Async` 注解标记的方法会异步执行。 + +`AsyncAnnotationBeanPostProcessor` 代码如下: + +```JAVA +public class AsyncAnnotationBeanPostProcessor extends AbstractBeanFactoryAwareAdvisingPostProcessor { + @Override + public void setBeanFactory(BeanFactory beanFactory) { + super.setBeanFactory(beanFactory); + // 创建 AsyncAnnotationAdvisor,它是一个 Advisor + // 用于拦截带有 @Async 注解的方法并将这些方法异步执行。 + AsyncAnnotationAdvisor advisor = new AsyncAnnotationAdvisor(this.executor, this.exceptionHandler); + // 如果设置了自定义的 asyncAnnotationType,则将其设置到 advisor 中。 + // asyncAnnotationType 用于指定自定义的异步注解,例如 @MyAsync。 + if (this.asyncAnnotationType != null) { + advisor.setAsyncAnnotationType(this.asyncAnnotationType); + } + advisor.setBeanFactory(beanFactory); + this.advisor = advisor; + } +} +``` + +`AsyncAnnotationBeanPostProcessor` 的父类实现了 `BeanFactoryAware` 接口,因此在该类中重写了 `setBeanFactory()` 方法作为扩展点,来加载 `AsyncAnnotationAdvisor` 。 + +#### 创建 Advisor + +`Advisor` 是 `Spring AOP` 对 `Advice` 和 `Pointcut` 的抽象。`Advice` 为执行的通知逻辑,`Pointcut` 为通知执行的切入点。 + +在后置处理器 `AsyncAnnotationBeanPostProcessor` 中会去创建 `AsyncAnnotationAdvisor` , 在它的构造方法中,会构建对应的 `Advice` 和 `Pointcut` ,如下: + +```JAVA +public class AsyncAnnotationAdvisor extends AbstractPointcutAdvisor implements BeanFactoryAware { + + private Advice advice; // 异步执行的 Advice + private Pointcut pointcut; // 匹配 @Async 注解方法的切点 + + // 构造函数 + public AsyncAnnotationAdvisor(/* 参数省略 */) { + // 1. 创建 Advice,负责异步执行逻辑 + this.advice = buildAdvice(executor, exceptionHandler); + // 2. 创建 Pointcut,选择要被增强的目标方法 + this.pointcut = buildPointcut(asyncAnnotationTypes); + } + + // 创建 Advice + protected Advice buildAdvice(/* 参数省略 */) { + // 创建处理异步执行的拦截器 + AnnotationAsyncExecutionInterceptor interceptor = new AnnotationAsyncExecutionInterceptor(null); + // 使用执行器和异常处理器配置拦截器 + interceptor.configure(executor, exceptionHandler); + return interceptor; + } + + // 创建 Pointcut + protected Pointcut buildPointcut(Set> asyncAnnotationTypes) { + ComposablePointcut result = null; + for (Class asyncAnnotationType : asyncAnnotationTypes) { + // 1. 类级别切点:如果类上有注解则匹配 + Pointcut cpc = new AnnotationMatchingPointcut(asyncAnnotationType, true); + // 2. 方法级别切点:如果方法上有注解则匹配 + Pointcut mpc = new AnnotationMatchingPointcut(null, asyncAnnotationType, true); + + if (result == null) { + result = new ComposablePointcut(cpc); + } else { + // 使用 union 合并之前的切点 + result.union(cpc); + } + // 将方法级别切点添加到组合切点 + result = result.union(mpc); + } + // 返回组合切点,如果没有提供注解类型则返回 Pointcut.TRUE + return (result != null ? result : Pointcut.TRUE); + } +} +``` + +`AsyncAnnotationAdvisor` 的核心在于构建 `Advice` 和 `Pointcut` : + +- 构建 `Advice` :会创建 `AnnotationAsyncExecutionInterceptor` 拦截器,在拦截器的 `invoke()` 方法中会执行通知的逻辑。 +- 构建 `Pointcut` :由 `ClassFilter` 和 `MethodMatcher` 组成,用于匹配哪些方法需要执行通知( `Advice` )的逻辑。 + +#### 后置处理逻辑 + +`AsyncAnnotationBeanPostProcessor` 后置处理器中实现的 `postProcessAfterInitialization()` 方法在其父类 `AbstractAdvisingBeanPostProcessor` 中,在 `Bean` 初始化之后,会进入到 `postProcessAfterInitialization()` 方法进行后置处理。 + +在后置处理方法中,会判断 `Bean` 是否符合后置处理器中 `Advisor` 通知的条件,如果符合,则创建代理对象。如下: + +```JAVA +// AbstractAdvisingBeanPostProcessor +public Object postProcessAfterInitialization(Object bean, String beanName) { + if (this.advisor == null || bean instanceof AopInfrastructureBean) { + return bean; + } + if (bean instanceof Advised) { + Advised advised = (Advised) bean; + if (!advised.isFrozen() && isEligible(AopUtils.getTargetClass(bean))) { + if (this.beforeExistingAdvisors) { + advised.addAdvisor(0, this.advisor); + } + else { + advised.addAdvisor(this.advisor); + } + return bean; + } + } + // 判断给定的 Bean 是否符合后置处理器中 Advisor 通知的条件,符合的话,就创建代理对象。 + if (isEligible(bean, beanName)) { + ProxyFactory proxyFactory = prepareProxyFactory(bean, beanName); + if (!proxyFactory.isProxyTargetClass()) { + evaluateProxyInterfaces(bean.getClass(), proxyFactory); + } + // 添加 Advisor。 + proxyFactory.addAdvisor(this.advisor); + customizeProxyFactory(proxyFactory); + // 返回代理对象。 + return proxyFactory.getProxy(getProxyClassLoader()); + } + return bean; +} +``` + +### @Async 注解方法的拦截 + +`@Async` 注解方法的执行会在 `AnnotationAsyncExecutionInterceptor` 中被拦截,在 `invoke()` 方法中执行拦截器的逻辑。此时会将 `@Async` 注解标注的方法封装为异步任务,交给执行器来执行。 + +`invoke()` 方法在 `AnnotationAsyncExecutionInterceptor` 的父类 `AsyncExecutionInterceptor` 中定义,如下: + +```JAVA +public class AsyncExecutionInterceptor extends AsyncExecutionAspectSupport implements MethodInterceptor, Ordered { + @Override + @Nullable + public Object invoke(final MethodInvocation invocation) throws Throwable { + Class targetClass = (invocation.getThis() != null ? AopUtils.getTargetClass(invocation.getThis()) : null); + Method specificMethod = ClassUtils.getMostSpecificMethod(invocation.getMethod(), targetClass); + final Method userDeclaredMethod = BridgeMethodResolver.findBridgedMethod(specificMethod); + + // 1、确定异步任务执行器 + AsyncTaskExecutor executor = determineAsyncExecutor(userDeclaredMethod); + + // 2、将要执行的方法封装为 Callable 异步任务 + Callable task = () -> { + try { + // 2.1、执行方法 + Object result = invocation.proceed(); + // 2.2、如果方法返回值是 Future 类型,阻塞等待结果 + if (result instanceof Future) { + return ((Future) result).get(); + } + } + catch (ExecutionException ex) { + handleError(ex.getCause(), userDeclaredMethod, invocation.getArguments()); + } + catch (Throwable ex) { + handleError(ex, userDeclaredMethod, invocation.getArguments()); + } + return null; + }; + // 3、提交任务 + return doSubmit(task, executor, invocation.getMethod().getReturnType()); + } +} +``` + +在 `invoke()` 方法中,主要有 3 个步骤: + +1. 确定执行异步任务的执行器。 +2. 将 `@Async` 注解标注的方法封装为 `Callable` 异步任务。 +3. 将任务提交给执行器执行。 + +#### 1、获取异步任务执行器 + +在 `determineAsyncExecutor()` 方法中,会获取异步任务的执行器(即执行异步任务的 **线程池** )。代码如下: + +```JAVA +// 确定异步任务的执行器 +protected AsyncTaskExecutor determineAsyncExecutor(Method method) { + // 1、先从缓存中获取。 + AsyncTaskExecutor executor = this.executors.get(method); + if (executor == null) { + Executor targetExecutor; + // 2、获取执行器的限定符。 + String qualifier = getExecutorQualifier(method); + if (StringUtils.hasLength(qualifier)) { + // 3、根据限定符获取对应的执行器。 + targetExecutor = findQualifiedExecutor(this.beanFactory, qualifier); + } + else { + // 4、如果没有限定符,则使用默认的执行器。即 Spring 提供的默认线程池:SimpleAsyncTaskExecutor。 + targetExecutor = this.defaultExecutor.get(); + } + if (targetExecutor == null) { + return null; + } + // 5、将执行器包装为 TaskExecutorAdapter 适配器。 + // TaskExecutorAdapter 是 Spring 对于 JDK 线程池做的一层抽象,还是继承自 JDK 的线程池 Executor。这里可以不用管太多,只要知道它是线程池就可以了。 + executor = (targetExecutor instanceof AsyncListenableTaskExecutor ? + (AsyncListenableTaskExecutor) targetExecutor : new TaskExecutorAdapter(targetExecutor)); + this.executors.put(method, executor); + } + return executor; +} +``` + +在 `determineAsyncExecutor()` 方法中确定了异步任务的执行器(线程池),主要是通过 `@Async` 注解的 `value` 值来获取执行器的限定符,根据限定符再去 `BeanFactory` 中查找对应的执行器就可以了。 + +如果在 `@Async` 注解中没有指定线程池,则会通过 `this.defaultExecutor.get()` 来获取默认的线程池,其中 `defaultExecutor` 在下边方法中进行赋值: + +```JAVA +// AsyncExecutionInterceptor +protected Executor getDefaultExecutor(@Nullable BeanFactory beanFactory) { + // 1、尝试从 beanFactory 中获取线程池。 + Executor defaultExecutor = super.getDefaultExecutor(beanFactory); + // 2、如果 beanFactory 中没有,则创建 SimpleAsyncTaskExecutor 线程池。 + return (defaultExecutor != null ? defaultExecutor : new SimpleAsyncTaskExecutor()); +} +``` + +其中 `super.getDefaultExecutor()` 会在 `beanFactory` 中尝试获取 `Executor` 类型的线程池。代码如下: + +```JAVA +protected Executor getDefaultExecutor(@Nullable BeanFactory beanFactory) { + if (beanFactory != null) { + try { + // 1、从 beanFactory 中获取 TaskExecutor 类型的线程池。 + return beanFactory.getBean(TaskExecutor.class); + } + catch (NoUniqueBeanDefinitionException ex) { + try { + // 2、如果有多个,则尝试从 beanFactory 中获取执行名称的 Executor 线程池。 + return beanFactory.getBean(DEFAULT_TASK_EXECUTOR_BEAN_NAME, Executor.class); + } + catch (NoSuchBeanDefinitionException ex2) { + if (logger.isInfoEnabled()) { + // ... + } + } + } + catch (NoSuchBeanDefinitionException ex) { + try { + // 3、如果没有,则尝试从 beanFactory 中获取执行名称的 Executor 线程池。 + return beanFactory.getBean(DEFAULT_TASK_EXECUTOR_BEAN_NAME, Executor.class); + } + catch (NoSuchBeanDefinitionException ex2) { + // ... + } + } + } + return null; +} +``` + +在 `getDefaultExecutor()` 中,如果从 `beanFactory` 获取线程池失败的话,则会创建 `SimpleAsyncTaskExecutor` 线程池。 + +该线程池的在每次执行异步任务时,都会创建一个新的线程去执行任务,并不会对线程进行复用,从而导致异步任务执行的开销很大。一旦在 `@Async` 注解标注的方法某一瞬间并发量剧增,应用就会大量创建线程,从而影响服务质量甚至出现服务不可用。 + +同一时刻如果向 `SimpleAsyncTaskExecutor` 线程池提交 10000 个任务,那么该线程池就会创建 10000 个线程,其的 `execute()` 方法如下: + +```JAVA +// SimpleAsyncTaskExecutor:execute() 内部会调用 doExecute() +protected void doExecute(Runnable task) { + // 创建新线程 + Thread thread = (this.threadFactory != null ? this.threadFactory.newThread(task) : createThread(task)); + thread.start(); +} +``` + +**建议:在使用 `@Async` 时需要自己指定线程池,避免 Spring 默认线程池带来的风险。** + +在 `@Async` 注解中的 `value` 指定了线程池的限定符,根据限定符可以获取 **自定义的线程池** 。获取限定符的代码如下: + +```JAVA +// AnnotationAsyncExecutionInterceptor +protected String getExecutorQualifier(Method method) { + // 1.从方法上获取 Async 注解。 + Async async = AnnotatedElementUtils.findMergedAnnotation(method, Async.class); + // 2. 如果方法上没有找到 @Async 注解,则尝试从方法所在的类上获取 @Async 注解。 + if (async == null) { + async = AnnotatedElementUtils.findMergedAnnotation(method.getDeclaringClass(), Async.class); + } + // 3. 如果找到了 @Async 注解,则获取注解的 value 值并返回,作为线程池的限定符。 + // 如果 "value" 属性值为空字符串,则使用默认的线程池。 + // 如果没有找到 @Async 注解,则返回 null,同样使用默认的线程池。 + return (async != null ? async.value() : null); +} +``` + +#### 2、将方法封装为异步任务 + +在 `invoke()` 方法获取执行器之后,会将方法封装为异步任务,代码如下: + +```JAVA +// 将要执行的方法封装为 Callable 异步任务 +Callable task = () -> { + try { + // 2.1、执行被拦截的方法 (proceed() 方法是 AOP 中的核心方法,用于执行目标方法) + Object result = invocation.proceed(); + + // 2.2、如果被拦截方法的返回值是 Future 类型,则需要阻塞等待结果, + // 并将 Future 的结果作为异步任务的结果返回。 这是为了处理异步方法嵌套调用的情况。 + // 例如,一个异步方法内部调用了另一个异步方法,则需要等待内部异步方法执行完成, + // 才能返回最终的结果。 + if (result instanceof Future) { + return ((Future) result).get(); // 阻塞等待 Future 的结果 + } + } + catch (ExecutionException ex) { + // 2.3、处理 ExecutionException 异常。 ExecutionException 是 Future.get() 方法抛出的异常, + handleError(ex.getCause(), userDeclaredMethod, invocation.getArguments()); // 处理原始异常 + } + catch (Throwable ex) { + // 2.4、处理其他类型的异常。 将异常、被拦截的方法和方法参数作为参数调用 handleError() 方法进行处理。 + handleError(ex, userDeclaredMethod, invocation.getArguments()); + } + // 2.5、如果方法返回值不是 Future 类型,或者发生异常,则返回 null。 + return null; +}; +``` + +相比于 `Runnable` ,`Callable` 可以返回结果,并且抛出异常。 + +将 `invocation.proceed()` 的执行(原方法的执行)封装为 `Callable` 异步任务。这里仅仅当 `result` (方法返回值)类型为 `Future` 才返回,如果是其他类型则直接返回 `null` 。 + +因此使用 `@Async` 注解标注的方法如果使用 `Future` 类型之外的返回值,则无法获取方法的执行结果。 + +#### 3、提交异步任务 + +在 `AsyncExecutionInterceptor # invoke()` 中将要执行的方法封装为 Callable 任务之后,就会将任务交给执行器来执行。提交相关的代码如下: + +```JAVA +protected Object doSubmit(Callable task, AsyncTaskExecutor executor, Class returnType) { + // 根据方法的返回值类型,选择不同的异步执行方式并返回结果。 + // 1. 如果方法返回值是 CompletableFuture 类型 + if (CompletableFuture.class.isAssignableFrom(returnType)) { + // 使用 CompletableFuture.supplyAsync() 方法异步执行任务。 + return CompletableFuture.supplyAsync(() -> { + try { + return task.call(); + } + catch (Throwable ex) { + throw new CompletionException(ex); // 将异常包装为 CompletionException,以便在 future.get() 时抛出 + } + }, executor); + } + // 2. 如果方法返回值是 ListenableFuture 类型 + else if (ListenableFuture.class.isAssignableFrom(returnType)) { + // 将 AsyncTaskExecutor 强制转换为 AsyncListenableTaskExecutor, + // 并调用 submitListenable() 方法提交任务。 + // AsyncListenableTaskExecutor 是 ListenableFuture 的专用异步执行器, + // 它可以返回一个 ListenableFuture 对象,允许添加回调函数来监听任务的完成。 + return ((AsyncListenableTaskExecutor) executor).submitListenable(task); + } + // 3. 如果方法返回值是 Future 类型 + else if (Future.class.isAssignableFrom(returnType)) { + // 直接调用 AsyncTaskExecutor 的 submit() 方法提交任务,并返回一个 Future 对象。 + return executor.submit(task); + } + // 4. 如果方法返回值是 void 或其他类型 + else { + // 直接调用 AsyncTaskExecutor 的 submit() 方法提交任务。 + // 由于方法返回值是 void,因此不需要返回任何结果,直接返回 null。 + executor.submit(task); + return null; + } +} +``` + +在 `doSubmit()` 方法中,会根据 `@Async` 注解标注方法的返回值不同,来选择不同的任务提交方式,最后任务会由执行器(线程池)执行。 + +### 总结 + +![Async原理总结](./images/async/async.png) + +理解 `@Async` 原理的核心在于理解 `@EnableAsync` 注解,该注解开启了异步任务的功能。 + +主要流程如上图,会通过后置处理器来创建代理对象,之后代理对象中 `@Async` 方法的执行会走到 `Advice` 内部的拦截器中,之后将方法封装为异步任务,并提交线程池进行处理。 + +## @Async 使用建议 + +### 自定义线程池 + +如果没有显式地配置线程池,在 `@Async` 底层会先在 `BeanFactory` 中尝试获取线程池,如果获取不到,则会创建一个 `SimpleAsyncTaskExecutor` 实现。`SimpleAsyncTaskExecutor` 本质上不算是一个真正的线程池,因为它对于每个请求都会启动一个新线程而不重用现有线程,这会带来一些潜在的问题,例如资源消耗过大。 + +具体线程池获取可以参考这篇文章:[浅析 Spring 中 Async 注解底层异步线程池原理|得物技术](https://mp.weixin.qq.com/s/FySv5L0bCdrlb5MoSfQtAA)。 + +一定要显式配置一个线程池,推荐`ThreadPoolTaskExecutor`。并且,还可以根据任务的性质和需求,为不同的异步方法指定不同的线程池。 + +```java +@Configuration +@EnableAsync +public class AsyncConfig { + + @Bean(name = "executor1") + public Executor executor1() { + ThreadPoolTaskExecutor executor = new ThreadPoolTaskExecutor(); + executor.setCorePoolSize(3); + executor.setMaxPoolSize(5); + executor.setQueueCapacity(50); + executor.setThreadNamePrefix("AsyncExecutor1-"); + executor.initialize(); + return executor; + } + + @Bean(name = "executor2") + public Executor executor2() { + ThreadPoolTaskExecutor executor = new ThreadPoolTaskExecutor(); + executor.setCorePoolSize(2); + executor.setMaxPoolSize(4); + executor.setQueueCapacity(100); + executor.setThreadNamePrefix("AsyncExecutor2-"); + executor.initialize(); + return executor; + } +} +``` + +`@Async` 注解中指定线程池的 Bean 名称: + +```java +@Service +public class AsyncService { + + @Async("executor1") + public void performTask1() { + // 任务1的逻辑 + System.out.println("Executing Task1 with Executor1"); + } + + @Async("executor2") + public void performTask2() { + // 任务2的逻辑 + System.out.println("Executing Task2 with Executor2"); + } +} +``` + +### 避免 @Async 注解失效 + +`@Async` 注解会在以下几个场景失效,需要注意: + +**1、同一类中调用异步方法** + +如果你在同一个类内部调用一个`@Async`注解的方法,那这个方法将不会异步执行。 + +```java +@Service +public class MyService { + + public void myMethod() { + // 直接通过 this 引用调用,绕过了 Spring 的代理机制,异步执行失效 + asyncMethod(); + } + + @Async + public void asyncMethod() { + // 异步执行的逻辑 + } +} +``` + +这是因为 Spring 的异步机制是通过 **代理** 实现的,而在同一个类内部的方法调用会绕过 Spring 的代理机制,也就是绕过了代理对象,直接通过 this 引用调用的。由于没有经过代理,所有的代理相关的处理(即将任务提交线程池异步执行)都不会发生。 + +为了避免这个问题,比较推荐的做法是将异步方法移至另一个 Spring Bean 中。 + +```java +@Service +public class AsyncService { + @Async + public void asyncMethod() { + // 异步执行的逻辑 + } +} + +@Service +public class MyService { + @Autowired + private AsyncService asyncService; + + public void myMethod() { + asyncService.asyncMethod(); + } +} +``` + +**2、使用 static 关键字修饰异步方法** + +如果`@Async`注解的方法被 `static` 关键字修饰,那这个方法将不会异步执行。 + +这是因为 Spring 的异步机制是通过代理实现的,由于静态方法不属于实例而是属于类且不参与继承,Spring 的代理机制(无论是基于 JDK 还是 CGLIB)无法拦截静态方法来提供如异步执行这样的增强功能。 + +篇幅问题,这里没有进一步详细介绍,不了解的代理机制的朋友,可以看看我写的 [Java 代理模式详解](https://javaguide.cn/java/basis/proxy.html)这篇文章。 + +如果你需要异步执行一个静态方法的逻辑,可以考虑设计一个非静态的包装方法,这个包装方法使用 `@Async` 注解,并在其内部调用静态方法 + +```java +@Service +public class AsyncService { + + @Async + public void asyncWrapper() { + // 调用静态方法 + SClass.staticMethod(); + } +} + +public class SClass { + public static void staticMethod() { + // 执行一些操作 + } +} +``` + +**3、忘记开启异步支持** + +Spring Boot 默认情况下不启用异步支持,确保在主配置类 `Application` 上添加`@EnableAsync`注解以启用异步功能。 + +```java +@SpringBootApplication +@EnableAsync +public class Application { + public static void main(String[] args) { + SpringApplication.run(Application.class, args); + } +} +``` + +**4、`@Async` 注解的方法所在的类必须是 Spring Bean** + +`@Async` 注解的方法必须位于 Spring 管理的 Bean 中,只有这样,Spring 才能在创建 Bean 时应用代理,代理能够拦截方法调用并实现异步执行的逻辑。如果该方法不在 Spring 管理的 bean 中,Spring 就无法创建必要的代理,`@Async` 注解就不会产生任何效果。 + +### 返回值类型 + +建议将 `@Async` 注解方法的返回值类型定义为 `void` 和 `Future` 。 + +- 如果不需要获取异步方法返回的结果,将返回值类型定义为 `void` 。 +- 如果需要获取异步方法返回的结果,将返回值类型定义为 `Future`(例如`CompletableFuture` 、 `ListenableFuture` )。 + +如果将 `@Async` 注解方法的返回值定义为其他类型(如 `Object` 、 `String` 等等),则无法获取方法返回值。 + +这种设计符合异步编程的基本原则,即调用者不应立即期待一个结果,而是应该能够在未来某个时间点获取结果。如果返回类型是 `Future`,调用者可以使用这个返回的 `Future` 对象来查询任务的状态,取消任务,或者在任务完成时获取结果。 + +### 处理异步方法中的异常 + +异步方法中抛出的异常默认不会被调用者捕获。为了管理这些异常,建议使用`CompletableFuture`的异常处理功能,或者配置一个全局的`AsyncUncaughtExceptionHandler`来处理没有正确捕获的异常。 + +```java +@Configuration +@EnableAsync +public class AsyncConfig implements AsyncConfigurer{ + + @Override + public AsyncUncaughtExceptionHandler getAsyncUncaughtExceptionHandler() { + return new CustomAsyncExceptionHandler(); + } + +} + +// 自定义异常处理器 +class CustomAsyncExceptionHandler implements AsyncUncaughtExceptionHandler { + + @Override + public void handleUncaughtException(Throwable ex, Method method, Object... params) { + // 日志记录或其他处理逻辑 + } +} +``` + +### 未考虑事务管理 + +`@Async`注解的方法需要事务支持时,务必在该异步方法上独立使用。 + +```java +@Service +public class AsyncTransactionalService { + + @Async + // Propagation.REQUIRES_NEW 表示 Spring 在执行异步方法时开启一个新的、与当前事务无关的事务 + @Transactional(propagation = Propagation.REQUIRES_NEW) + public void asyncTransactionalMethod() { + // 这里的操作会在新的事务中执行 + // 执行一些数据库操作 + } +} +``` + +### 未指定异步方法执行顺序 + +`@Async`注解的方法执行是非阻塞的,它们可能以任意顺序完成。如果需要按照特定的顺序处理结果,你可以将方法的返回值设定为 `Future` 或 `CompletableFuture` ,通过返回值对象来实现一个方法在另一个方法完成后再执行。 + +```java +@Async +public CompletableFuture fetchDataAsync() { + return CompletableFuture.completedFuture("Data"); +} + +@Async +public CompletableFuture processDataAsync(String data) { + return CompletableFuture.supplyAsync(() -> "Processed " + data); +} +``` + +`processDataAsync` 方法在 `fetchDataAsync`后执行: + +```java +CompletableFuture dataFuture = asyncService.fetchDataAsync(); +dataFuture.thenCompose(data -> asyncService.processDataAsync(data)) + .thenAccept(result -> System.out.println(result)); +``` + +## diff --git a/docs/system-design/framework/spring/async.md b/docs/system-design/framework/spring/async.md new file mode 100644 index 00000000000..a27eb61c970 --- /dev/null +++ b/docs/system-design/framework/spring/async.md @@ -0,0 +1,721 @@ +--- +title: Async 注解原理分析 +category: 框架 +tag: + - Spring +--- + +`@Async` 注解由 Spring 框架提供,被该注解标注的类或方法会在 **异步线程** 中执行。这意味着当方法被调用时,调用者将不会等待该方法执行完成,而是可以继续执行后续的代码。 + +`@Async` 注解的使用非常简单,需要两个步骤: + +1. 在启动类上添加注解 `@EnableAsync` ,开启异步任务。 +2. 在需要异步执行的方法或类上添加注解 `@Async` 。 + +```java +@SpringBootApplication +// 开启异步任务 +@EnableAsync +public class YourApplication { + + public static void main(String[] args) { + SpringApplication.run(YourApplication.class, args); + } +} + +// 异步服务类 +@Service +public class MyService { + + // 推荐使用自定义线程池,这里只是演示基本用法 + @Async + public CompletableFuture doSomethingAsync() { + + // 这里会有一些业务耗时操作 + // ... + // 使用 CompletableFuture 可以更方便地处理异步任务的结果,避免阻塞主线程 + return CompletableFuture.completedFuture("Async Task Completed"); + } + +} +``` + +接下来,我们一起来看看 `@Async` 的底层原理。 + +## @Async 原理分析 + +`@Async` 可以异步执行任务,本质上是使用 **动态代理** 来实现的。通过 Spring 中的后置处理器 `BeanPostProcessor` 为使用 `@Async` 注解的类创建动态代理,之后 `@Async` 注解方法的调用会被动态代理拦截,在拦截器中将方法的执行封装为异步任务提交给线程池处理。 + +接下来,我们来详细分析一下。 + +### 开启异步 + +使用 `@Async` 之前,需要在启动类上添加 `@EnableAsync` 来开启异步,`@EnableAsync` 注解如下: + +```JAVA +// 省略其他注解 ... +@Import(AsyncConfigurationSelector.class) +public @interface EnableAsync { /* ... */ } +``` + +在 `@EnableAsync` 注解上通过 `@Import` 注解引入了 `AsyncConfigurationSelector` ,因此 Spring 会去加载通过 `@Import` 注解引入的类。 + +`AsyncConfigurationSelector` 类实现了 `ImportSelector` 接口,因此在该类中会重写 `selectImports()` 方法来自定义加载 Bean 的逻辑,如下: + +```JAVA +public class AsyncConfigurationSelector extends AdviceModeImportSelector { + @Override + @Nullable + public String[] selectImports(AdviceMode adviceMode) { + switch (adviceMode) { + // 基于 JDK 代理织入的通知 + case PROXY: + return new String[] {ProxyAsyncConfiguration.class.getName()}; + // 基于 AspectJ 织入的通知 + case ASPECTJ: + return new String[] {ASYNC_EXECUTION_ASPECT_CONFIGURATION_CLASS_NAME}; + default: + return null; + } + } +} +``` + +在 `selectImports()` 方法中,会根据通知的不同类型来选择加载不同的类,其中 `adviceMode` 默认值为 `PROXY` 。 + +这里以基于 JDK 代理的通知为例,此时会加载 `ProxyAsyncConfiguration` 类,如下: + +```JAVA +@Configuration +@Role(BeanDefinition.ROLE_INFRASTRUCTURE) +public class ProxyAsyncConfiguration extends AbstractAsyncConfiguration { + @Bean(name = TaskManagementConfigUtils.ASYNC_ANNOTATION_PROCESSOR_BEAN_NAME) + @Role(BeanDefinition.ROLE_INFRASTRUCTURE) + public AsyncAnnotationBeanPostProcessor asyncAdvisor() { + // ... + // 加载后置处理器 + AsyncAnnotationBeanPostProcessor bpp = new AsyncAnnotationBeanPostProcessor(); + + // ... + return bpp; + } +} +``` + +### 后置处理器 + +在 `ProxyAsyncConfiguration` 类中,会通过 `@Bean` 注解加载一个后置处理器 `AsyncAnnotationBeanPostProcessor` ,这个后置处理器是使 `@Async` 注解起作用的关键。 + +如果某一个类或者方法上使用了 `@Async` 注解,`AsyncAnnotationBeanPostProcessor` 处理器就会为该类创建一个动态代理。 + +该类的方法在执行时,会被代理对象的拦截器所拦截,其中被 `@Async` 注解标记的方法会异步执行。 + +`AsyncAnnotationBeanPostProcessor` 代码如下: + +```JAVA +public class AsyncAnnotationBeanPostProcessor extends AbstractBeanFactoryAwareAdvisingPostProcessor { + @Override + public void setBeanFactory(BeanFactory beanFactory) { + super.setBeanFactory(beanFactory); + // 创建 AsyncAnnotationAdvisor,它是一个 Advisor + // 用于拦截带有 @Async 注解的方法并将这些方法异步执行。 + AsyncAnnotationAdvisor advisor = new AsyncAnnotationAdvisor(this.executor, this.exceptionHandler); + // 如果设置了自定义的 asyncAnnotationType,则将其设置到 advisor 中。 + // asyncAnnotationType 用于指定自定义的异步注解,例如 @MyAsync。 + if (this.asyncAnnotationType != null) { + advisor.setAsyncAnnotationType(this.asyncAnnotationType); + } + advisor.setBeanFactory(beanFactory); + this.advisor = advisor; + } +} +``` + +`AsyncAnnotationBeanPostProcessor` 的父类实现了 `BeanFactoryAware` 接口,因此在该类中重写了 `setBeanFactory()` 方法作为扩展点,来加载 `AsyncAnnotationAdvisor` 。 + +#### 创建 Advisor + +`Advisor` 是 `Spring AOP` 对 `Advice` 和 `Pointcut` 的抽象。`Advice` 为执行的通知逻辑,`Pointcut` 为通知执行的切入点。 + +在后置处理器 `AsyncAnnotationBeanPostProcessor` 中会去创建 `AsyncAnnotationAdvisor` , 在它的构造方法中,会构建对应的 `Advice` 和 `Pointcut` ,如下: + +```JAVA +public class AsyncAnnotationAdvisor extends AbstractPointcutAdvisor implements BeanFactoryAware { + + private Advice advice; // 异步执行的 Advice + private Pointcut pointcut; // 匹配 @Async 注解方法的切点 + + // 构造函数 + public AsyncAnnotationAdvisor(/* 参数省略 */) { + // 1. 创建 Advice,负责异步执行逻辑 + this.advice = buildAdvice(executor, exceptionHandler); + // 2. 创建 Pointcut,选择要被增强的目标方法 + this.pointcut = buildPointcut(asyncAnnotationTypes); + } + + // 创建 Advice + protected Advice buildAdvice(/* 参数省略 */) { + // 创建处理异步执行的拦截器 + AnnotationAsyncExecutionInterceptor interceptor = new AnnotationAsyncExecutionInterceptor(null); + // 使用执行器和异常处理器配置拦截器 + interceptor.configure(executor, exceptionHandler); + return interceptor; + } + + // 创建 Pointcut + protected Pointcut buildPointcut(Set> asyncAnnotationTypes) { + ComposablePointcut result = null; + for (Class asyncAnnotationType : asyncAnnotationTypes) { + // 1. 类级别切点:如果类上有注解则匹配 + Pointcut cpc = new AnnotationMatchingPointcut(asyncAnnotationType, true); + // 2. 方法级别切点:如果方法上有注解则匹配 + Pointcut mpc = new AnnotationMatchingPointcut(null, asyncAnnotationType, true); + + if (result == null) { + result = new ComposablePointcut(cpc); + } else { + // 使用 union 合并之前的切点 + result.union(cpc); + } + // 将方法级别切点添加到组合切点 + result = result.union(mpc); + } + // 返回组合切点,如果没有提供注解类型则返回 Pointcut.TRUE + return (result != null ? result : Pointcut.TRUE); + } +} +``` + +`AsyncAnnotationAdvisor` 的核心在于构建 `Advice` 和 `Pointcut` : + +- 构建 `Advice` :会创建 `AnnotationAsyncExecutionInterceptor` 拦截器,在拦截器的 `invoke()` 方法中会执行通知的逻辑。 +- 构建 `Pointcut` :由 `ClassFilter` 和 `MethodMatcher` 组成,用于匹配哪些方法需要执行通知( `Advice` )的逻辑。 + +#### 后置处理逻辑 + +`AsyncAnnotationBeanPostProcessor` 后置处理器中实现的 `postProcessAfterInitialization()` 方法在其父类 `AbstractAdvisingBeanPostProcessor` 中,在 `Bean` 初始化之后,会进入到 `postProcessAfterInitialization()` 方法进行后置处理。 + +在后置处理方法中,会判断 `Bean` 是否符合后置处理器中 `Advisor` 通知的条件,如果符合,则创建代理对象。如下: + +```JAVA +// AbstractAdvisingBeanPostProcessor +public Object postProcessAfterInitialization(Object bean, String beanName) { + if (this.advisor == null || bean instanceof AopInfrastructureBean) { + return bean; + } + if (bean instanceof Advised) { + Advised advised = (Advised) bean; + if (!advised.isFrozen() && isEligible(AopUtils.getTargetClass(bean))) { + if (this.beforeExistingAdvisors) { + advised.addAdvisor(0, this.advisor); + } + else { + advised.addAdvisor(this.advisor); + } + return bean; + } + } + // 判断给定的 Bean 是否符合后置处理器中 Advisor 通知的条件,符合的话,就创建代理对象。 + if (isEligible(bean, beanName)) { + ProxyFactory proxyFactory = prepareProxyFactory(bean, beanName); + if (!proxyFactory.isProxyTargetClass()) { + evaluateProxyInterfaces(bean.getClass(), proxyFactory); + } + // 添加 Advisor。 + proxyFactory.addAdvisor(this.advisor); + customizeProxyFactory(proxyFactory); + // 返回代理对象。 + return proxyFactory.getProxy(getProxyClassLoader()); + } + return bean; +} +``` + +### @Async 注解方法的拦截 + +`@Async` 注解方法的执行会在 `AnnotationAsyncExecutionInterceptor` 中被拦截,在 `invoke()` 方法中执行拦截器的逻辑。此时会将 `@Async` 注解标注的方法封装为异步任务,交给执行器来执行。 + +`invoke()` 方法在 `AnnotationAsyncExecutionInterceptor` 的父类 `AsyncExecutionInterceptor` 中定义,如下: + +```JAVA +public class AsyncExecutionInterceptor extends AsyncExecutionAspectSupport implements MethodInterceptor, Ordered { + @Override + @Nullable + public Object invoke(final MethodInvocation invocation) throws Throwable { + Class targetClass = (invocation.getThis() != null ? AopUtils.getTargetClass(invocation.getThis()) : null); + Method specificMethod = ClassUtils.getMostSpecificMethod(invocation.getMethod(), targetClass); + final Method userDeclaredMethod = BridgeMethodResolver.findBridgedMethod(specificMethod); + + // 1、确定异步任务执行器 + AsyncTaskExecutor executor = determineAsyncExecutor(userDeclaredMethod); + + // 2、将要执行的方法封装为 Callable 异步任务 + Callable task = () -> { + try { + // 2.1、执行方法 + Object result = invocation.proceed(); + // 2.2、如果方法返回值是 Future 类型,阻塞等待结果 + if (result instanceof Future) { + return ((Future) result).get(); + } + } + catch (ExecutionException ex) { + handleError(ex.getCause(), userDeclaredMethod, invocation.getArguments()); + } + catch (Throwable ex) { + handleError(ex, userDeclaredMethod, invocation.getArguments()); + } + return null; + }; + // 3、提交任务 + return doSubmit(task, executor, invocation.getMethod().getReturnType()); + } +} +``` + +在 `invoke()` 方法中,主要有 3 个步骤: + +1. 确定执行异步任务的执行器。 +2. 将 `@Async` 注解标注的方法封装为 `Callable` 异步任务。 +3. 将任务提交给执行器执行。 + +#### 1、获取异步任务执行器 + +在 `determineAsyncExecutor()` 方法中,会获取异步任务的执行器(即执行异步任务的 **线程池** )。代码如下: + +```JAVA +// 确定异步任务的执行器 +protected AsyncTaskExecutor determineAsyncExecutor(Method method) { + // 1、先从缓存中获取。 + AsyncTaskExecutor executor = this.executors.get(method); + if (executor == null) { + Executor targetExecutor; + // 2、获取执行器的限定符。 + String qualifier = getExecutorQualifier(method); + if (StringUtils.hasLength(qualifier)) { + // 3、根据限定符获取对应的执行器。 + targetExecutor = findQualifiedExecutor(this.beanFactory, qualifier); + } + else { + // 4、如果没有限定符,则使用默认的执行器。即 Spring 提供的默认线程池:SimpleAsyncTaskExecutor。 + targetExecutor = this.defaultExecutor.get(); + } + if (targetExecutor == null) { + return null; + } + // 5、将执行器包装为 TaskExecutorAdapter 适配器。 + // TaskExecutorAdapter 是 Spring 对于 JDK 线程池做的一层抽象,还是继承自 JDK 的线程池 Executor。这里可以不用管太多,只要知道它是线程池就可以了。 + executor = (targetExecutor instanceof AsyncListenableTaskExecutor ? + (AsyncListenableTaskExecutor) targetExecutor : new TaskExecutorAdapter(targetExecutor)); + this.executors.put(method, executor); + } + return executor; +} +``` + +在 `determineAsyncExecutor()` 方法中确定了异步任务的执行器(线程池),主要是通过 `@Async` 注解的 `value` 值来获取执行器的限定符,根据限定符再去 `BeanFactory` 中查找对应的执行器就可以了。 + +如果在 `@Async` 注解中没有指定线程池,则会通过 `this.defaultExecutor.get()` 来获取默认的线程池,其中 `defaultExecutor` 在下边方法中进行赋值: + +```JAVA +// AsyncExecutionInterceptor +protected Executor getDefaultExecutor(@Nullable BeanFactory beanFactory) { + // 1、尝试从 beanFactory 中获取线程池。 + Executor defaultExecutor = super.getDefaultExecutor(beanFactory); + // 2、如果 beanFactory 中没有,则创建 SimpleAsyncTaskExecutor 线程池。 + return (defaultExecutor != null ? defaultExecutor : new SimpleAsyncTaskExecutor()); +} +``` + +其中 `super.getDefaultExecutor()` 会在 `beanFactory` 中尝试获取 `Executor` 类型的线程池。代码如下: + +```JAVA +protected Executor getDefaultExecutor(@Nullable BeanFactory beanFactory) { + if (beanFactory != null) { + try { + // 1、从 beanFactory 中获取 TaskExecutor 类型的线程池。 + return beanFactory.getBean(TaskExecutor.class); + } + catch (NoUniqueBeanDefinitionException ex) { + try { + // 2、如果有多个,则尝试从 beanFactory 中获取执行名称的 Executor 线程池。 + return beanFactory.getBean(DEFAULT_TASK_EXECUTOR_BEAN_NAME, Executor.class); + } + catch (NoSuchBeanDefinitionException ex2) { + if (logger.isInfoEnabled()) { + // ... + } + } + } + catch (NoSuchBeanDefinitionException ex) { + try { + // 3、如果没有,则尝试从 beanFactory 中获取执行名称的 Executor 线程池。 + return beanFactory.getBean(DEFAULT_TASK_EXECUTOR_BEAN_NAME, Executor.class); + } + catch (NoSuchBeanDefinitionException ex2) { + // ... + } + } + } + return null; +} +``` + +在 `getDefaultExecutor()` 中,如果从 `beanFactory` 获取线程池失败的话,则会创建 `SimpleAsyncTaskExecutor` 线程池。 + +该线程池的在每次执行异步任务时,都会创建一个新的线程去执行任务,并不会对线程进行复用,从而导致异步任务执行的开销很大。一旦在 `@Async` 注解标注的方法某一瞬间并发量剧增,应用就会大量创建线程,从而影响服务质量甚至出现服务不可用。 + +同一时刻如果向 `SimpleAsyncTaskExecutor` 线程池提交 10000 个任务,那么该线程池就会创建 10000 个线程,其的 `execute()` 方法如下: + +```JAVA +// SimpleAsyncTaskExecutor:execute() 内部会调用 doExecute() +protected void doExecute(Runnable task) { + // 创建新线程 + Thread thread = (this.threadFactory != null ? this.threadFactory.newThread(task) : createThread(task)); + thread.start(); +} +``` + +**建议:在使用 `@Async` 时需要自己指定线程池,避免 Spring 默认线程池带来的风险。** + +在 `@Async` 注解中的 `value` 指定了线程池的限定符,根据限定符可以获取 **自定义的线程池** 。获取限定符的代码如下: + +```JAVA +// AnnotationAsyncExecutionInterceptor +protected String getExecutorQualifier(Method method) { + // 1.从方法上获取 Async 注解。 + Async async = AnnotatedElementUtils.findMergedAnnotation(method, Async.class); + // 2. 如果方法上没有找到 @Async 注解,则尝试从方法所在的类上获取 @Async 注解。 + if (async == null) { + async = AnnotatedElementUtils.findMergedAnnotation(method.getDeclaringClass(), Async.class); + } + // 3. 如果找到了 @Async 注解,则获取注解的 value 值并返回,作为线程池的限定符。 + // 如果 "value" 属性值为空字符串,则使用默认的线程池。 + // 如果没有找到 @Async 注解,则返回 null,同样使用默认的线程池。 + return (async != null ? async.value() : null); +} +``` + +#### 2、将方法封装为异步任务 + +在 `invoke()` 方法获取执行器之后,会将方法封装为异步任务,代码如下: + +```JAVA +// 将要执行的方法封装为 Callable 异步任务 +Callable task = () -> { + try { + // 2.1、执行被拦截的方法 (proceed() 方法是 AOP 中的核心方法,用于执行目标方法) + Object result = invocation.proceed(); + + // 2.2、如果被拦截方法的返回值是 Future 类型,则需要阻塞等待结果, + // 并将 Future 的结果作为异步任务的结果返回。 这是为了处理异步方法嵌套调用的情况。 + // 例如,一个异步方法内部调用了另一个异步方法,则需要等待内部异步方法执行完成, + // 才能返回最终的结果。 + if (result instanceof Future) { + return ((Future) result).get(); // 阻塞等待 Future 的结果 + } + } + catch (ExecutionException ex) { + // 2.3、处理 ExecutionException 异常。 ExecutionException 是 Future.get() 方法抛出的异常, + handleError(ex.getCause(), userDeclaredMethod, invocation.getArguments()); // 处理原始异常 + } + catch (Throwable ex) { + // 2.4、处理其他类型的异常。 将异常、被拦截的方法和方法参数作为参数调用 handleError() 方法进行处理。 + handleError(ex, userDeclaredMethod, invocation.getArguments()); + } + // 2.5、如果方法返回值不是 Future 类型,或者发生异常,则返回 null。 + return null; +}; +``` + +相比于 `Runnable` ,`Callable` 可以返回结果,并且抛出异常。 + +将 `invocation.proceed()` 的执行(原方法的执行)封装为 `Callable` 异步任务。这里仅仅当 `result` (方法返回值)类型为 `Future` 才返回,如果是其他类型则直接返回 `null` 。 + +因此使用 `@Async` 注解标注的方法如果使用 `Future` 类型之外的返回值,则无法获取方法的执行结果。 + +#### 3、提交异步任务 + +在 `AsyncExecutionInterceptor # invoke()` 中将要执行的方法封装为 Callable 任务之后,就会将任务交给执行器来执行。提交相关的代码如下: + +```JAVA +protected Object doSubmit(Callable task, AsyncTaskExecutor executor, Class returnType) { + // 根据方法的返回值类型,选择不同的异步执行方式并返回结果。 + // 1. 如果方法返回值是 CompletableFuture 类型 + if (CompletableFuture.class.isAssignableFrom(returnType)) { + // 使用 CompletableFuture.supplyAsync() 方法异步执行任务。 + return CompletableFuture.supplyAsync(() -> { + try { + return task.call(); + } + catch (Throwable ex) { + throw new CompletionException(ex); // 将异常包装为 CompletionException,以便在 future.get() 时抛出 + } + }, executor); + } + // 2. 如果方法返回值是 ListenableFuture 类型 + else if (ListenableFuture.class.isAssignableFrom(returnType)) { + // 将 AsyncTaskExecutor 强制转换为 AsyncListenableTaskExecutor, + // 并调用 submitListenable() 方法提交任务。 + // AsyncListenableTaskExecutor 是 ListenableFuture 的专用异步执行器, + // 它可以返回一个 ListenableFuture 对象,允许添加回调函数来监听任务的完成。 + return ((AsyncListenableTaskExecutor) executor).submitListenable(task); + } + // 3. 如果方法返回值是 Future 类型 + else if (Future.class.isAssignableFrom(returnType)) { + // 直接调用 AsyncTaskExecutor 的 submit() 方法提交任务,并返回一个 Future 对象。 + return executor.submit(task); + } + // 4. 如果方法返回值是 void 或其他类型 + else { + // 直接调用 AsyncTaskExecutor 的 submit() 方法提交任务。 + // 由于方法返回值是 void,因此不需要返回任何结果,直接返回 null。 + executor.submit(task); + return null; + } +} +``` + +在 `doSubmit()` 方法中,会根据 `@Async` 注解标注方法的返回值不同,来选择不同的任务提交方式,最后任务会由执行器(线程池)执行。 + +### 总结 + +![Async原理总结](./images/async/async.png) + +理解 `@Async` 原理的核心在于理解 `@EnableAsync` 注解,该注解开启了异步任务的功能。 + +主要流程如上图,会通过后置处理器来创建代理对象,之后代理对象中 `@Async` 方法的执行会走到 `Advice` 内部的拦截器中,之后将方法封装为异步任务,并提交线程池进行处理。 + +## @Async 使用建议 + +### 自定义线程池 + +如果没有显式地配置线程池,在 `@Async` 底层会先在 `BeanFactory` 中尝试获取线程池,如果获取不到,则会创建一个 `SimpleAsyncTaskExecutor` 实现。`SimpleAsyncTaskExecutor` 本质上不算是一个真正的线程池,因为它对于每个请求都会启动一个新线程而不重用现有线程,这会带来一些潜在的问题,例如资源消耗过大。 + +具体线程池获取可以参考这篇文章:[浅析 Spring 中 Async 注解底层异步线程池原理|得物技术](https://mp.weixin.qq.com/s/FySv5L0bCdrlb5MoSfQtAA)。 + +一定要显式配置一个线程池,推荐`ThreadPoolTaskExecutor`。并且,还可以根据任务的性质和需求,为不同的异步方法指定不同的线程池。 + +```java +@Configuration +@EnableAsync +public class AsyncConfig { + + @Bean(name = "executor1") + public Executor executor1() { + ThreadPoolTaskExecutor executor = new ThreadPoolTaskExecutor(); + executor.setCorePoolSize(3); + executor.setMaxPoolSize(5); + executor.setQueueCapacity(50); + executor.setThreadNamePrefix("AsyncExecutor1-"); + executor.initialize(); + return executor; + } + + @Bean(name = "executor2") + public Executor executor2() { + ThreadPoolTaskExecutor executor = new ThreadPoolTaskExecutor(); + executor.setCorePoolSize(2); + executor.setMaxPoolSize(4); + executor.setQueueCapacity(100); + executor.setThreadNamePrefix("AsyncExecutor2-"); + executor.initialize(); + return executor; + } +} +``` + +`@Async` 注解中指定线程池的 Bean 名称: + +```java +@Service +public class AsyncService { + + @Async("executor1") + public void performTask1() { + // 任务1的逻辑 + System.out.println("Executing Task1 with Executor1"); + } + + @Async("executor2") + public void performTask2() { + // 任务2的逻辑 + System.out.println("Executing Task2 with Executor2"); + } +} +``` + +### 避免 @Async 注解失效 + +`@Async` 注解会在以下几个场景失效,需要注意: + +**1、同一类中调用异步方法** + +如果你在同一个类内部调用一个`@Async`注解的方法,那这个方法将不会异步执行。 + +```java +@Service +public class MyService { + + public void myMethod() { + // 直接通过 this 引用调用,绕过了 Spring 的代理机制,异步执行失效 + asyncMethod(); + } + + @Async + public void asyncMethod() { + // 异步执行的逻辑 + } +} +``` + +这是因为 Spring 的异步机制是通过 **代理** 实现的,而在同一个类内部的方法调用会绕过 Spring 的代理机制,也就是绕过了代理对象,直接通过 this 引用调用的。由于没有经过代理,所有的代理相关的处理(即将任务提交线程池异步执行)都不会发生。 + +为了避免这个问题,比较推荐的做法是将异步方法移至另一个 Spring Bean 中。 + +```java +@Service +public class AsyncService { + @Async + public void asyncMethod() { + // 异步执行的逻辑 + } +} + +@Service +public class MyService { + @Autowired + private AsyncService asyncService; + + public void myMethod() { + asyncService.asyncMethod(); + } +} +``` + +**2、使用 static 关键字修饰异步方法** + +如果`@Async`注解的方法被 `static` 关键字修饰,那这个方法将不会异步执行。 + +这是因为 Spring 的异步机制是通过代理实现的,由于静态方法不属于实例而是属于类且不参与继承,Spring 的代理机制(无论是基于 JDK 还是 CGLIB)无法拦截静态方法来提供如异步执行这样的增强功能。 + +篇幅问题,这里没有进一步详细介绍,不了解的代理机制的朋友,可以看看我写的 [Java 代理模式详解](https://javaguide.cn/java/basis/proxy.html)这篇文章。 + +如果你需要异步执行一个静态方法的逻辑,可以考虑设计一个非静态的包装方法,这个包装方法使用 `@Async` 注解,并在其内部调用静态方法 + +```java +@Service +public class AsyncService { + + @Async + public void asyncWrapper() { + // 调用静态方法 + SClass.staticMethod(); + } +} + +public class SClass { + public static void staticMethod() { + // 执行一些操作 + } +} +``` + +**3、忘记开启异步支持** + +Spring Boot 默认情况下不启用异步支持,确保在主配置类 `Application` 上添加`@EnableAsync`注解以启用异步功能。 + +```java +@SpringBootApplication +@EnableAsync +public class Application { + public static void main(String[] args) { + SpringApplication.run(Application.class, args); + } +} +``` + +**4、`@Async` 注解的方法所在的类必须是 Spring Bean** + +`@Async` 注解的方法必须位于 Spring 管理的 Bean 中,只有这样,Spring 才能在创建 Bean 时应用代理,代理能够拦截方法调用并实现异步执行的逻辑。如果该方法不在 Spring 管理的 bean 中,Spring 就无法创建必要的代理,`@Async` 注解就不会产生任何效果。 + +### 返回值类型 + +建议将 `@Async` 注解方法的返回值类型定义为 `void` 和 `Future` 。 + +- 如果不需要获取异步方法返回的结果,将返回值类型定义为 `void` 。 +- 如果需要获取异步方法返回的结果,将返回值类型定义为 `Future`(例如`CompletableFuture` 、 `ListenableFuture` )。 + +如果将 `@Async` 注解方法的返回值定义为其他类型(如 `Object` 、 `String` 等等),则无法获取方法返回值。 + +这种设计符合异步编程的基本原则,即调用者不应立即期待一个结果,而是应该能够在未来某个时间点获取结果。如果返回类型是 `Future`,调用者可以使用这个返回的 `Future` 对象来查询任务的状态,取消任务,或者在任务完成时获取结果。 + +### 处理异步方法中的异常 + +异步方法中抛出的异常默认不会被调用者捕获。为了管理这些异常,建议使用`CompletableFuture`的异常处理功能,或者配置一个全局的`AsyncUncaughtExceptionHandler`来处理没有正确捕获的异常。 + +```java +@Configuration +@EnableAsync +public class AsyncConfig implements AsyncConfigurer{ + + @Override + public AsyncUncaughtExceptionHandler getAsyncUncaughtExceptionHandler() { + return new CustomAsyncExceptionHandler(); + } + +} + +// 自定义异常处理器 +class CustomAsyncExceptionHandler implements AsyncUncaughtExceptionHandler { + + @Override + public void handleUncaughtException(Throwable ex, Method method, Object... params) { + // 日志记录或其他处理逻辑 + } +} +``` + +### 未考虑事务管理 + +`@Async`注解的方法需要事务支持时,务必在该异步方法上独立使用。 + +```java +@Service +public class AsyncTransactionalService { + + @Async + // Propagation.REQUIRES_NEW 表示 Spring 在执行异步方法时开启一个新的、与当前事务无关的事务 + @Transactional(propagation = Propagation.REQUIRES_NEW) + public void asyncTransactionalMethod() { + // 这里的操作会在新的事务中执行 + // 执行一些数据库操作 + } +} +``` + +### 未指定异步方法执行顺序 + +`@Async`注解的方法执行是非阻塞的,它们可能以任意顺序完成。如果需要按照特定的顺序处理结果,你可以将方法的返回值设定为 `Future` 或 `CompletableFuture` ,通过返回值对象来实现一个方法在另一个方法完成后再执行。 + +```java +@Async +public CompletableFuture fetchDataAsync() { + return CompletableFuture.completedFuture("Data"); +} + +@Async +public CompletableFuture processDataAsync(String data) { + return CompletableFuture.supplyAsync(() -> "Processed " + data); +} +``` + +`processDataAsync` 方法在 `fetchDataAsync`后执行: + +```java +CompletableFuture dataFuture = asyncService.fetchDataAsync(); +dataFuture.thenCompose(data -> asyncService.processDataAsync(data)) + .thenAccept(result -> System.out.println(result)); +``` + +## diff --git a/docs/system-design/framework/spring/images/async/async.png b/docs/system-design/framework/spring/images/async/async.png new file mode 100644 index 00000000000..6b68fd35084 Binary files /dev/null and b/docs/system-design/framework/spring/images/async/async.png differ diff --git a/docs/system-design/framework/spring/ioc-and-aop.md b/docs/system-design/framework/spring/ioc-and-aop.md new file mode 100644 index 00000000000..e58f40f81af --- /dev/null +++ b/docs/system-design/framework/spring/ioc-and-aop.md @@ -0,0 +1,289 @@ +--- +title: IoC & AOP详解(快速搞懂) +category: 框架 +tag: + - Spring +--- + +这篇文章会从下面从以下几个问题展开对 IoC & AOP 的解释 + +- 什么是 IoC? +- IoC 解决了什么问题? +- IoC 和 DI 的区别? +- 什么是 AOP? +- AOP 解决了什么问题? +- AOP 的应用场景有哪些? +- AOP 为什么叫做切面编程? +- AOP 实现方式有哪些? + +首先声明:IoC & AOP 不是 Spring 提出来的,它们在 Spring 之前其实已经存在了,只不过当时更加偏向于理论。Spring 在技术层次将这两个思想进行了很好的实现。 + +## IoC (Inversion of control ) + +### 什么是 IoC? + +IoC (Inversion of Control )即控制反转/反转控制。它是一种思想不是一个技术实现。描述的是:Java 开发领域对象的创建以及管理的问题。 + +例如:现有类 A 依赖于类 B + +- **传统的开发方式** :往往是在类 A 中手动通过 new 关键字来 new 一个 B 的对象出来 +- **使用 IoC 思想的开发方式** :不通过 new 关键字来创建对象,而是通过 IoC 容器(Spring 框架) 来帮助我们实例化对象。我们需要哪个对象,直接从 IoC 容器里面去取即可。 + +从以上两种开发方式的对比来看:我们 “丧失了一个权力” (创建、管理对象的权力),从而也得到了一个好处(不用再考虑对象的创建、管理等一系列的事情) + +**为什么叫控制反转?** + +- **控制** :指的是对象创建(实例化、管理)的权力 +- **反转** :控制权交给外部环境(IoC 容器) + +![IoC 图解](https://oss.javaguide.cn/github/javaguide/system-design/framework/spring/IoC&Aop-ioc-illustration.png) + +### IoC 解决了什么问题? + +IoC 的思想就是两方之间不互相依赖,由第三方容器来管理相关资源。这样有什么好处呢? + +1. 对象之间的耦合度或者说依赖程度降低; +2. 资源变的容易管理;比如你用 Spring 容器提供的话很容易就可以实现一个单例。 + +例如:现有一个针对 User 的操作,利用 Service 和 Dao 两层结构进行开发 + +在没有使用 IoC 思想的情况下,Service 层想要使用 Dao 层的具体实现的话,需要通过 new 关键字在`UserServiceImpl` 中手动 new 出 `IUserDao` 的具体实现类 `UserDaoImpl`(不能直接 new 接口类)。 + +很完美,这种方式也是可以实现的,但是我们想象一下如下场景: + +开发过程中突然接到一个新的需求,针对`IUserDao` 接口开发出另一个具体实现类。因为 Server 层依赖了`IUserDao`的具体实现,所以我们需要修改`UserServiceImpl`中 new 的对象。如果只有一个类引用了`IUserDao`的具体实现,可能觉得还好,修改起来也不是很费力气,但是如果有许许多多的地方都引用了`IUserDao`的具体实现的话,一旦需要更换`IUserDao` 的实现方式,那修改起来将会非常的头疼。 + +![IoC&Aop-ioc-illustration-dao-service](https://oss.javaguide.cn/github/javaguide/system-design/framework/spring/IoC&Aop-ioc-illustration-dao-service.png) + +使用 IoC 的思想,我们将对象的控制权(创建、管理)交由 IoC 容器去管理,我们在使用的时候直接向 IoC 容器 “要” 就可以了 + +![](https://oss.javaguide.cn/github/javaguide/system-design/framework/spring/IoC&Aop-ioc-illustration-dao.png) + +### IoC 和 DI 有区别吗? + +IoC(Inverse of Control:控制反转)是一种设计思想或者说是某种模式。这个设计思想就是 **将原本在程序中手动创建对象的控制权交给第三方比如 IoC 容器。** 对于我们常用的 Spring 框架来说, IoC 容器实际上就是个 Map(key,value),Map 中存放的是各种对象。不过,IoC 在其他语言中也有应用,并非 Spring 特有。 + +IoC 最常见以及最合理的实现方式叫做依赖注入(Dependency Injection,简称 DI)。 + +老马(Martin Fowler)在一篇文章中提到将 IoC 改名为 DI,原文如下,原文地址: 。 + +![](https://oss.javaguide.cn/github/javaguide/system-design/framework/spring/martin-fowler-injection.png) + +老马的大概意思是 IoC 太普遍并且不表意,很多人会因此而迷惑,所以,使用 DI 来精确指名这个模式比较好。 + +## AOP(Aspect oriented programming) + +这里不会涉及太多专业的术语,核心目的是将 AOP 的思想说清楚。 + +### 什么是 AOP? + +AOP(Aspect Oriented Programming)即面向切面编程,AOP 是 OOP(面向对象编程)的一种延续,二者互补,并不对立。 + +AOP 的目的是将横切关注点(如日志记录、事务管理、权限控制、接口限流、接口幂等等)从核心业务逻辑中分离出来,通过动态代理、字节码操作等技术,实现代码的复用和解耦,提高代码的可维护性和可扩展性。OOP 的目的是将业务逻辑按照对象的属性和行为进行封装,通过类、对象、继承、多态等概念,实现代码的模块化和层次化(也能实现代码的复用),提高代码的可读性和可维护性。 + +### AOP 为什么叫面向切面编程? + +AOP 之所以叫面向切面编程,是因为它的核心思想就是将横切关注点从核心业务逻辑中分离出来,形成一个个的**切面(Aspect)**。 + +![面向切面编程图解](https://oss.javaguide.cn/github/javaguide/system-design/framework/spring/aop-program-execution.jpg) + +这里顺带总结一下 AOP 关键术语(不理解也没关系,可以继续往下看): + +- **横切关注点(cross-cutting concerns)** :多个类或对象中的公共行为(如日志记录、事务管理、权限控制、接口限流、接口幂等等)。 +- **切面(Aspect)**:对横切关注点进行封装的类,一个切面是一个类。切面可以定义多个通知,用来实现具体的功能。 +- **连接点(JoinPoint)**:连接点是方法调用或者方法执行时的某个特定时刻(如方法调用、异常抛出等)。 +- **通知(Advice)**:通知就是切面在某个连接点要执行的操作。通知有五种类型,分别是前置通知(Before)、后置通知(After)、返回通知(AfterReturning)、异常通知(AfterThrowing)和环绕通知(Around)。前四种通知都是在目标方法的前后执行,而环绕通知可以控制目标方法的执行过程。 +- **切点(Pointcut)**:一个切点是一个表达式,它用来匹配哪些连接点需要被切面所增强。切点可以通过注解、正则表达式、逻辑运算等方式来定义。比如 `execution(* com.xyz.service..*(..))`匹配 `com.xyz.service` 包及其子包下的类或接口。 +- **织入(Weaving)**:织入是将切面和目标对象连接起来的过程,也就是将通知应用到切点匹配的连接点上。常见的织入时机有两种,分别是编译期织入(Compile-Time Weaving 如:AspectJ)和运行期织入(Runtime Weaving 如:AspectJ、Spring AOP)。 + +### AOP 常见的通知类型有哪些? + +![](https://oss.javaguide.cn/github/javaguide/system-design/framework/spring/aspectj-advice-types.jpg) + +- **Before**(前置通知):目标对象的方法调用之前触发 +- **After** (后置通知):目标对象的方法调用之后触发 +- **AfterReturning**(返回通知):目标对象的方法调用完成,在返回结果值之后触发 +- **AfterThrowing**(异常通知):目标对象的方法运行中抛出 / 触发异常后触发。AfterReturning 和 AfterThrowing 两者互斥。如果方法调用成功无异常,则会有返回值;如果方法抛出了异常,则不会有返回值。 +- **Around** (环绕通知):编程式控制目标对象的方法调用。环绕通知是所有通知类型中可操作范围最大的一种,因为它可以直接拿到目标对象,以及要执行的方法,所以环绕通知可以任意的在目标对象的方法调用前后搞事,甚至不调用目标对象的方法 + +### AOP 解决了什么问题? + +OOP 不能很好地处理一些分散在多个类或对象中的公共行为(如日志记录、事务管理、权限控制、接口限流、接口幂等等),这些行为通常被称为 **横切关注点(cross-cutting concerns)** 。如果我们在每个类或对象中都重复实现这些行为,那么会导致代码的冗余、复杂和难以维护。 + +AOP 可以将横切关注点(如日志记录、事务管理、权限控制、接口限流、接口幂等等)从 **核心业务逻辑(core concerns,核心关注点)** 中分离出来,实现关注点的分离。 + +![](https://oss.javaguide.cn/github/javaguide/system-design/framework/spring/crosscut-logic-and-businesslogic-separation%20%20%20%20%20%20.png) + +以日志记录为例进行介绍,假如我们需要对某些方法进行统一格式的日志记录,没有使用 AOP 技术之前,我们需要挨个写日志记录的逻辑代码,全是重复的的逻辑。 + +```java +public CommonResponse method1() { + // 业务逻辑 + xxService.method1(); + // 省略具体的业务处理逻辑 + // 日志记录 + ServletRequestAttributes attributes = (ServletRequestAttributes) RequestContextHolder.getRequestAttributes(); + HttpServletRequest request = attributes.getRequest(); + // 省略记录日志的具体逻辑 如:获取各种信息,写入数据库等操作... + return CommonResponse.success(); +} + +public CommonResponse method2() { + // 业务逻辑 + xxService.method2(); + // 省略具体的业务处理逻辑 + // 日志记录 + ServletRequestAttributes attributes = (ServletRequestAttributes) RequestContextHolder.getRequestAttributes(); + HttpServletRequest request = attributes.getRequest(); + // 省略记录日志的具体逻辑 如:获取各种信息,写入数据库等操作... + return CommonResponse.success(); +} + +// ... +``` + +使用 AOP 技术之后,我们可以将日志记录的逻辑封装成一个切面,然后通过切入点和通知来指定在哪些方法需要执行日志记录的操作。 + +```java + +// 日志注解 +@Target({ElementType.PARAMETER,ElementType.METHOD}) +@Retention(RetentionPolicy.RUNTIME) +@Documented +public @interface Log { + + /** + * 描述 + */ + String description() default ""; + + /** + * 方法类型 INSERT DELETE UPDATE OTHER + */ + MethodType methodType() default MethodType.OTHER; +} + +// 日志切面 +@Component +@Aspect +public class LogAspect { + // 切入点,所有被 Log 注解标注的方法 + @Pointcut("@annotation(cn.javaguide.annotation.Log)") + public void webLog() { + } + + /** + * 环绕通知 + */ + @Around("webLog()") + public Object doAround(ProceedingJoinPoint joinPoint) throws Throwable { + // 省略具体的处理逻辑 + } + + // 省略其他代码 +} +``` + +这样的话,我们一行注解即可实现日志记录: + +```java +@Log(description = "method1",methodType = MethodType.INSERT) +public CommonResponse method1() { + // 业务逻辑 + xxService.method1(); + // 省略具体的业务处理逻辑 + return CommonResponse.success(); +} +``` + +### AOP 的应用场景有哪些? + +- 日志记录:自定义日志记录注解,利用 AOP,一行代码即可实现日志记录。 +- 性能统计:利用 AOP 在目标方法的执行前后统计方法的执行时间,方便优化和分析。 +- 事务管理:`@Transactional` 注解可以让 Spring 为我们进行事务管理比如回滚异常操作,免去了重复的事务管理逻辑。`@Transactional`注解就是基于 AOP 实现的。 +- 权限控制:利用 AOP 在目标方法执行前判断用户是否具备所需要的权限,如果具备,就执行目标方法,否则就不执行。例如,SpringSecurity 利用`@PreAuthorize` 注解一行代码即可自定义权限校验。 +- 接口限流:利用 AOP 在目标方法执行前通过具体的限流算法和实现对请求进行限流处理。 +- 缓存管理:利用 AOP 在目标方法执行前后进行缓存的读取和更新。 +- …… + +### AOP 实现方式有哪些? + +AOP 的常见实现方式有动态代理、字节码操作等方式。 + +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 相对来说更简单。 + +如果我们的切面比较少,那么两者性能差异不大。但是,当切面太多的话,最好选择 AspectJ ,它比 Spring AOP 快很多。 + +## 参考 + +- AOP in Spring Boot, is it a JDK dynamic proxy or a Cglib dynamic proxy?: +- Spring Proxying Mechanisms: diff --git a/docs/system-design/framework/spring/spring-boot-auto-assembly-principles.md b/docs/system-design/framework/spring/spring-boot-auto-assembly-principles.md index 08dc8625fc2..15da09e634a 100644 --- a/docs/system-design/framework/spring/spring-boot-auto-assembly-principles.md +++ b/docs/system-design/framework/spring/spring-boot-auto-assembly-principles.md @@ -81,6 +81,7 @@ public class DemoApplication { 我们现在提到自动装配的时候,一般会和 Spring Boot 联系在一起。但是,实际上 Spring Framework 早就实现了这个功能。Spring Boot 只是在其基础上,通过 SPI 的方式,做了进一步优化。 > SpringBoot 定义了一套接口规范,这套规范规定:SpringBoot 在启动时会扫描外部引用 jar 包中的`META-INF/spring.factories`文件,将文件中配置的类型信息加载到 Spring 容器(此处涉及到 JVM 类加载机制与 Spring 的容器知识),并执行类中定义的各种操作。对于外部 jar 来说,只需要按照 SpringBoot 定义的标准,就能将自己的功能装置进 SpringBoot。 +> 自 Spring Boot 3.0 开始,自动配置包的路径从`META-INF/spring.factories` 修改为 `META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports`。 没有 Spring Boot 的情况下,如果我们需要引入第三方依赖,需要手动配置,非常麻烦。但是,Spring Boot 中,我们直接引入一个 starter 即可。比如你想要在项目中使用 redis 的话,直接在项目中引入对应的 starter 即可。 @@ -125,7 +126,7 @@ public @interface SpringBootConfiguration { - `@Configuration`:允许在上下文中注册额外的 bean 或导入其他配置类 - `@ComponentScan`:扫描被`@Component` (`@Service`,`@Controller`)注解的 bean,注解默认会扫描启动类所在的包下所有的类 ,可以自定义不扫描某些 bean。如下图所示,容器中将排除`TypeExcludeFilter`和`AutoConfigurationExcludeFilter`。 -![](https://p3-juejin.byteimg.com/tos-cn-i-k3u1fbpfcp/bcc73490afbe4c6ba62acde6a94ffdfd~tplv-k3u1fbpfcp-watermark.image) +![](https://oss.javaguide.cn/p3-juejin/bcc73490afbe4c6ba62acde6a94ffdfd~tplv-k3u1fbpfcp-watermark.png) `@EnableAutoConfiguration` 是实现自动装配的重要注解,我们以这个注解入手。 @@ -191,7 +192,7 @@ public String[] selectImports(AnnotationMetadata annotationMetadata) { 该方法调用链如下: -![](https://p6-juejin.byteimg.com/tos-cn-i-k3u1fbpfcp/3c1200712655443ca4b38500d615bb70~tplv-k3u1fbpfcp-watermark.image) +![](https://oss.javaguide.cn/github/javaguide/system-design/framework/spring/3c1200712655443ca4b38500d615bb70~tplv-k3u1fbpfcp-watermark.png) 现在我们结合`getAutoConfigurationEntry()`的源码来详细分析一下: @@ -223,27 +224,27 @@ AutoConfigurationEntry getAutoConfigurationEntry(AutoConfigurationMetadata autoC 判断自动装配开关是否打开。默认`spring.boot.enableautoconfiguration=true`,可在 `application.properties` 或 `application.yml` 中设置 -![](https://p3-juejin.byteimg.com/tos-cn-i-k3u1fbpfcp/77aa6a3727ea4392870f5cccd09844ab~tplv-k3u1fbpfcp-watermark.image) +![](https://oss.javaguide.cn/p3-juejin/77aa6a3727ea4392870f5cccd09844ab~tplv-k3u1fbpfcp-watermark.png) **第 2 步**: 用于获取`EnableAutoConfiguration`注解中的 `exclude` 和 `excludeName`。 -![](https://p3-juejin.byteimg.com/tos-cn-i-k3u1fbpfcp/3d6ec93bbda1453aa08c52b49516c05a~tplv-k3u1fbpfcp-zoom-1.image) +![](https://oss.javaguide.cn/p3-juejin/3d6ec93bbda1453aa08c52b49516c05a~tplv-k3u1fbpfcp-zoom-1.png) **第 3 步** 获取需要自动装配的所有配置类,读取`META-INF/spring.factories` -``` +```plain spring-boot/spring-boot-project/spring-boot-autoconfigure/src/main/resources/META-INF/spring.factories ``` -![](https://p6-juejin.byteimg.com/tos-cn-i-k3u1fbpfcp/58c51920efea4757aa1ec29c6d5f9e36~tplv-k3u1fbpfcp-watermark.image) +![](https://oss.javaguide.cn/github/javaguide/system-design/framework/spring/58c51920efea4757aa1ec29c6d5f9e36~tplv-k3u1fbpfcp-watermark.png) 从下图可以看到这个文件的配置内容都被我们读取到了。`XXXAutoConfiguration`的作用就是按需加载组件。 -![](https://p3-juejin.byteimg.com/tos-cn-i-k3u1fbpfcp/94d6e1a060ac41db97043e1758789026~tplv-k3u1fbpfcp-watermark.image) +![](https://oss.javaguide.cn/github/javaguide/system-design/framework/spring/94d6e1a060ac41db97043e1758789026~tplv-k3u1fbpfcp-watermark.png) 不光是这个依赖下的`META-INF/spring.factories`被读取到,所有 Spring Boot Starter 下的`META-INF/spring.factories`都会被读取到。 @@ -251,7 +252,7 @@ spring-boot/spring-boot-project/spring-boot-autoconfigure/src/main/resources/MET 如果,我们自己要创建一个 Spring Boot Starter,这一步是必不可少的。 -![](https://p1-juejin.byteimg.com/tos-cn-i-k3u1fbpfcp/68fa66aeee474b0385f94d23bcfe1745~tplv-k3u1fbpfcp-watermark.image) +![](https://oss.javaguide.cn/github/javaguide/system-design/framework/spring/68fa66aeee474b0385f94d23bcfe1745~tplv-k3u1fbpfcp-watermark.png) **第 4 步**: @@ -259,7 +260,7 @@ spring-boot/spring-boot-project/spring-boot-autoconfigure/src/main/resources/MET 很明显,这是不现实的。我们 debug 到后面你会发现,`configurations` 的值变小了。 -![](https://p6-juejin.byteimg.com/tos-cn-i-k3u1fbpfcp/267f8231ae2e48d982154140af6437b0~tplv-k3u1fbpfcp-watermark.image) +![](https://oss.javaguide.cn/github/javaguide/system-design/framework/spring/267f8231ae2e48d982154140af6437b0~tplv-k3u1fbpfcp-watermark.png) 因为,这一步有经历了一遍筛选,`@ConditionalOnXXX` 中的所有条件都满足,该类才会生效。 @@ -295,28 +296,30 @@ public class RabbitAutoConfiguration { 第一步,创建`threadpool-spring-boot-starter`工程 -![](https://p9-juejin.byteimg.com/tos-cn-i-k3u1fbpfcp/1ff0ebe7844f40289eb60213af72c5a6~tplv-k3u1fbpfcp-watermark.image) +![](https://oss.javaguide.cn/github/javaguide/system-design/framework/spring/1ff0ebe7844f40289eb60213af72c5a6~tplv-k3u1fbpfcp-watermark.png) 第二步,引入 Spring Boot 相关依赖 -![](https://p9-juejin.byteimg.com/tos-cn-i-k3u1fbpfcp/5e14254276604f87b261e5a80a354cc0~tplv-k3u1fbpfcp-watermark.image) +![](https://oss.javaguide.cn/github/javaguide/system-design/framework/spring/5e14254276604f87b261e5a80a354cc0~tplv-k3u1fbpfcp-watermark.png) 第三步,创建`ThreadPoolAutoConfiguration` -![](https://p9-juejin.byteimg.com/tos-cn-i-k3u1fbpfcp/1843f1d12c5649fba85fd7b4e4a59e39~tplv-k3u1fbpfcp-watermark.image) +![](https://oss.javaguide.cn/github/javaguide/system-design/framework/spring/1843f1d12c5649fba85fd7b4e4a59e39~tplv-k3u1fbpfcp-watermark.png) 第四步,在`threadpool-spring-boot-starter`工程的 resources 包下创建`META-INF/spring.factories`文件 -![](https://p9-juejin.byteimg.com/tos-cn-i-k3u1fbpfcp/97b738321f1542ea8140484d6aaf0728~tplv-k3u1fbpfcp-watermark.image) +![](https://oss.javaguide.cn/github/javaguide/system-design/framework/spring/97b738321f1542ea8140484d6aaf0728~tplv-k3u1fbpfcp-watermark.png) 最后新建工程引入`threadpool-spring-boot-starter` -![](https://p3-juejin.byteimg.com/tos-cn-i-k3u1fbpfcp/edcdd8595a024aba85b6bb20d0e3fed4~tplv-k3u1fbpfcp-watermark.image) +![](https://oss.javaguide.cn/github/javaguide/system-design/framework/spring/edcdd8595a024aba85b6bb20d0e3fed4~tplv-k3u1fbpfcp-watermark.png) 测试通过!!! -![](https://p1-juejin.byteimg.com/tos-cn-i-k3u1fbpfcp/9a265eea4de742a6bbdbbaa75f437307~tplv-k3u1fbpfcp-watermark.image) +![](https://oss.javaguide.cn/github/javaguide/system-design/framework/spring/9a265eea4de742a6bbdbbaa75f437307~tplv-k3u1fbpfcp-watermark.png) ## 总结 Spring Boot 通过`@EnableAutoConfiguration`开启自动装配,通过 SpringFactoriesLoader 最终加载`META-INF/spring.factories`中的自动配置类实现自动装配,自动配置类其实就是通过`@Conditional`按需加载的配置类,想要其生效必须引入`spring-boot-starter-xxx`包实现起步依赖 + + diff --git a/docs/system-design/framework/spring/spring-common-annotations.md b/docs/system-design/framework/spring/spring-common-annotations.md index 3baa7859a18..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; @@ -42,8 +46,8 @@ package org.springframework.boot.autoconfigure; @SpringBootConfiguration @EnableAutoConfiguration @ComponentScan(excludeFilters = { - @Filter(type = FilterType.CUSTOM, classes = TypeExcludeFilter.class), - @Filter(type = FilterType.CUSTOM, classes = AutoConfigurationExcludeFilter.class) }) + @Filter(type = FilterType.CUSTOM, classes = TypeExcludeFilter.class), + @Filter(type = FilterType.CUSTOM, classes = AutoConfigurationExcludeFilter.class) }) public @interface SpringBootApplication { ...... } @@ -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 { + // ... +} +``` -- singleton : 唯一 bean 实例,Spring 中的 bean 默认都是单例的。 -- prototype : 每次请求都会创建一个新的 bean 实例。 -- request : 每一次 HTTP 请求都会产生一个新的 bean,该 bean 仅在当前 HTTP request 内有效。 -- session : 每一个 HTTP Session 会产生一个新的 bean,该 bean 仅在当前 HTTP session 内有效。 +### Bean 注册 -#### 2.5. `@Configuration` +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` 是更常用的选择。 + +关于`@RestController` 和 `@Controller`的对比,请看这篇文章:[@RestController vs @Controller](https://mp.weixin.qq.com/s?__biz=Mzg2OTA0Njk0OA==&mid=2247485544&idx=1&sn=3cc95b88979e28fe3bfe539eb421c6d8&chksm=cea247a3f9d5ceb5e324ff4b8697adc3e828ecf71a3468445e70221cce768d1e722085359907&token=1725092312&lang=zh_CN#rd)。 -一般用来声明配置类,可以使用 `@Component`注解替代,不过使用`@Configuration`注解声明配置类更加语义化。 +## 配置 + +### 声明配置类 + +`@Configuration` 主要用于声明一个类是 Spring 的配置类。虽然也可以用 `@Component` 注解替代,但 `@Configuration` 能够更明确地表达该类的用途(定义 Bean),语义更清晰,也便于 Spring 进行特定的处理(例如,通过 CGLIB 代理确保 `@Bean` 方法的单例行为)。 ```java @Configuration public class AppConfig { + + // @Bean 注解用于在配置类中声明一个 Bean @Bean public TransferService transferService() { return new TransferServiceImpl(); } + // 配置类中可以包含一个或多个 @Bean 方法。 } ``` -### 3. 处理常见的 HTTP 请求类型 +### 读取配置信息 + +在应用程序开发中,我们经常需要管理一些配置信息,例如数据库连接细节、第三方服务(如阿里云 OSS、短信服务、微信认证)的密钥或地址等。通常,这些信息会**集中存放在配置文件**(如 `application.yml` 或 `application.properties`)中,方便管理和修改。 + +Spring 提供了多种便捷的方式来读取这些配置信息。假设我们有如下 `application.yml` 文件: + +```yaml +wuhan2020: 2020年初武汉爆发了新型冠状病毒,疫情严重,但是,我相信一切都会过去!武汉加油!中国加油! + +my-profile: + name: Guide哥 + email: koushuangbwcx@163.com + +library: + location: 湖北武汉加油中国加油 + books: + - name: 天才基本法 + description: 二十二岁的林朝夕在父亲确诊阿尔茨海默病这天,得知自己暗恋多年的校园男神裴之即将出国深造的消息——对方考取的学校,恰是父亲当年为她放弃的那所。 + - name: 时间的秩序 + description: 为什么我们记得过去,而非未来?时间“流逝”意味着什么?是我们存在于时间之内,还是时间存在于我们之中?卡洛·罗韦利用诗意的文字,邀请我们思考这一亘古难题——时间的本质。 + - name: 了不起的我 + description: 如何养成一个新习惯?如何让心智变得更成熟?如何拥有高质量的关系? 如何走出人生的艰难时刻? +``` + +下面介绍几种常用的读取配置的方式: + +1、`@Value("${property.key}")` 注入配置文件(如 `application.properties` 或 `application.yml`)中的单个属性值。它还支持 Spring 表达式语言 (SpEL),可以实现更复杂的注入逻辑。 + +```java +@Value("${wuhan2020}") +String wuhan2020; +``` + +2、`@ConfigurationProperties`可以读取配置信息并与 Bean 绑定,用的更多一些。 + +```java +@Component +@ConfigurationProperties(prefix = "library") +class LibraryProperties { + @NotEmpty + private String location; + private List books; + + @Setter + @Getter + @ToString + static class Book { + String name; + String description; + } + 省略getter/setter + ...... +} +``` + +你可以像使用普通的 Spring Bean 一样,将其注入到类中使用。 + +```java +@Service +public class LibraryService { + + private final LibraryProperties libraryProperties; + + @Autowired + public LibraryService(LibraryProperties libraryProperties) { + this.libraryProperties = libraryProperties; + } + + public void printLibraryInfo() { + System.out.println(libraryProperties); + } +} +``` + +### 加载指定的配置文件 + +`@PropertySource` 注解允许加载自定义的配置文件。适用于需要将部分配置信息独立存储的场景。 + +```java +@Component +@PropertySource("classpath:website.properties") + +class WebSite { + @Value("${url}") + private String url; + + 省略getter/setter + ...... +} +``` + +**注意**:当使用 `@PropertySource` 时,确保外部文件路径正确,且文件在类路径(classpath)中。 + +更多内容请查看我的这篇文章:[10 分钟搞定 SpringBoot 如何优雅读取配置文件?](https://mp.weixin.qq.com/s?__biz=Mzg2OTA0Njk0OA==&mid=2247486181&idx=2&sn=10db0ae64ef501f96a5b0dbc4bd78786&chksm=cea2452ef9d5cc384678e456427328600971180a77e40c13936b19369672ca3e342c26e92b50&token=816772476&lang=zh_CN#rd) 。 + +## MVC + +### HTTP 请求 **5 种常见的请求类型:** @@ -148,9 +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,22 +309,22 @@ 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") public ResponseEntity createUser(@Valid @RequestBody UserCreateRequest userCreateRequest) { - return userRespository.save(userCreateRequest); + return userRepository.save(userCreateRequest); } ``` -#### 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`。 -那么我们服务获取到的数据就是:`klassId=123456,type=web`。 +#### 绑定查询参数 -#### 4.2. `@RequestBody` +`@RequestParam` 用于绑定查询参数。例如: -用于读取 Request 请求(可能是 POST,PUT,DELETE,GET 请求)的 body 部分并且**Content-Type 为 application/json** 格式的数据,接收到数据之后会自动将数据绑定到 Java 对象上去。系统会使用`HttpMessageConverter`或者自定义的`HttpMessageConverter`将请求的 body 中的 json 字符串转换为 java 对象。 +```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`。 + +#### 绑定请求体中的 JSON 数据 + +`@RequestBody` 用于读取 Request 请求(可能是 POST,PUT,DELETE,GET 请求)的 body 部分并且**Content-Type 为 application/json** 格式的数据,接收到数据之后会自动将数据绑定到 Java 对象上去。系统会使用`HttpMessageConverter`或者自定义的`HttpMessageConverter`将请求的 body 中的 json 字符串转换为 java 对象。 我用一个简单的例子来给演示一下基本使用! @@ -272,134 +430,73 @@ public class UserRegisterRequest { ![](./images/spring-annotations/@RequestBody.png) -👉 需要注意的是:**一个请求方法只可以有一个`@RequestBody`,但是可以有多个`@RequestParam`和`@PathVariable`**。 如果你的方法必须要用两个 `@RequestBody`来接受数据的话,大概率是你的数据库设计或者系统设计出问题了! +**注意**: -### 5. 读取配置信息 +- 一个方法只能有一个 `@RequestBody` 参数,但可以有多个 `@PathVariable` 和 `@RequestParam`。 +- 如果需要接收多个复杂对象,建议合并成一个单一对象。 -**很多时候我们需要将一些常用的配置信息比如阿里云 oss、发送短信、微信认证的相关配置信息等等放到配置文件中。** +## 数据校验 -**下面我们来看一下 Spring 为我们提供了哪些方式帮助我们从配置文件中读取这些配置信息。** +数据校验是保障系统稳定性和安全性的关键环节。即使在用户界面(前端)已经实施了数据校验,**后端服务仍必须对接收到的数据进行再次校验**。这是因为前端校验可以被轻易绕过(例如,通过开发者工具修改请求或使用 Postman、curl 等 HTTP 工具直接调用 API),恶意或错误的数据可能直接发送到后端。因此,后端校验是防止非法数据、维护数据一致性、确保业务逻辑正确执行的最后一道,也是最重要的一道防线。 -我们的数据源`application.yml`内容如下: +Bean Validation 是一套定义 JavaBean 参数校验标准的规范 (JSR 303, 349, 380),它提供了一系列注解,可以直接用于 JavaBean 的属性上,从而实现便捷的参数校验。 -```yaml -wuhan2020: 2020年初武汉爆发了新型冠状病毒,疫情严重,但是,我相信一切都会过去!武汉加油!中国加油! +- **JSR 303 (Bean Validation 1.0):** 奠定了基础,引入了核心校验注解(如 `@NotNull`、`@Size`、`@Min`、`@Max` 等),定义了如何通过注解的方式对 JavaBean 的属性进行校验,并支持嵌套对象校验和自定义校验器。 +- **JSR 349 (Bean Validation 1.1):** 在 1.0 基础上进行扩展,例如引入了对方法参数和返回值校验的支持、增强了对分组校验(Group Validation)的处理。 +- **JSR 380 (Bean Validation 2.0):** 拥抱 Java 8 的新特性,并进行了一些改进,例如支持 `java.time` 包中的日期和时间类型、引入了一些新的校验注解(如 `@NotEmpty`, `@NotBlank`等)。 -my-profile: - name: Guide哥 - email: koushuangbwcx@163.com +Bean Validation 本身只是一套**规范(接口和注解)**,我们需要一个实现了这套规范的**具体框架**来执行校验逻辑。目前,**Hibernate Validator** 是 Bean Validation 规范最权威、使用最广泛的参考实现。 -library: - location: 湖北武汉加油中国加油 - books: - - name: 天才基本法 - description: 二十二岁的林朝夕在父亲确诊阿尔茨海默病这天,得知自己暗恋多年的校园男神裴之即将出国深造的消息——对方考取的学校,恰是父亲当年为她放弃的那所。 - - name: 时间的秩序 - description: 为什么我们记得过去,而非未来?时间“流逝”意味着什么?是我们存在于时间之内,还是时间存在于我们之中?卡洛·罗韦利用诗意的文字,邀请我们思考这一亘古难题——时间的本质。 - - name: 了不起的我 - description: 如何养成一个新习惯?如何让心智变得更成熟?如何拥有高质量的关系? 如何走出人生的艰难时刻? -``` +- 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)。 -#### 5.1. `@Value`(常用) +在 Spring Boot 项目中使用 Bean Validation 非常方便,这得益于 Spring Boot 的自动配置能力。关于依赖引入,需要注意: -使用 `@Value("${property}")` 读取比较简单的配置信息: +- 在较早版本的 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` 依赖: -```java -@Value("${wuhan2020}") -String wuhan2020; +```xml + + org.springframework.boot + spring-boot-starter-validation + ``` -#### 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) 。 - -### 6. 参数校验 - -**数据的校验的重要性就不用说了,即使在前端对数据进行校验的情况下,我们还是要对传入后端的数据再进行一遍校验,避免用户绕过浏览器直接通过一些 HTTP 工具直接向后端请求一些违法数据。** - -**JSR(Java Specification Requests)** 是一套 JavaBean 参数校验的标准,它定义了很多常用的校验注解,我们可以直接将这些注解加在我们 JavaBean 的属性上面,这样就可以在需要校验的时候进行校验了,非常方便! +![](https://oss.javaguide.cn/2021/03/c7bacd12-1c1a-4e41-aaaf-4cad840fc073.png) -校验的时候我们实际用的是 **Hibernate Validator** 框架。Hibernate Validator 是 Hibernate 团队最初的数据校验框架,Hibernate Validator 4.x 是 Bean Validation 1.0(JSR 303)的参考实现,Hibernate Validator 5.x 是 Bean Validation 1.1(JSR 349)的参考实现,目前最新版的 Hibernate Validator 6.x 是 Bean Validation 2.0(JSR 380)的参考实现。 +非 SpringBoot 项目需要自行引入相关依赖包,这里不多做讲解,具体可以查看我的这篇文章:[如何在 Spring/Spring Boot 中做参数校验?你需要了解的都在这里!](https://mp.weixin.qq.com/s?__biz=Mzg2OTA0Njk0OA==&mid=2247485783&idx=1&sn=a407f3b75efa17c643407daa7fb2acd6&chksm=cea2469cf9d5cf8afbcd0a8a1c9cc4294d6805b8e01bee6f76bb2884c5bc15478e91459def49&token=292197051&lang=zh_CN#rd)。 -SpringBoot 项目的 spring-boot-starter-web 依赖中已经有 hibernate-validator 包,不需要引用相关依赖。如下图所示(通过 idea 插件—Maven Helper 生成): +👉 需要注意的是:所有的注解,推荐使用 JSR 注解,即`javax.validation.constraints`,而不是`org.hibernate.validator.constraints` -**注**:更新版本的 spring-boot-starter-web 依赖中不再有 hibernate-validator 包(如 2.3.11.RELEASE),需要自己引入 `spring-boot-starter-validation` 依赖。 +### 一些常用的字段验证的注解 -![](https://oss.javaguide.cn/2021/03/c7bacd12-1c1a-4e41-aaaf-4cad840fc073.png) +Bean Validation 规范及其实现(如 Hibernate Validator)提供了丰富的注解,用于声明式地定义校验规则。以下是一些常用的注解及其说明: -非 SpringBoot 项目需要自行引入相关依赖包,这里不多做讲解,具体可以查看我的这篇文章:《[如何在 Spring/Spring Boot 中做参数校验?你需要了解的都在这里!](https://mp.weixin.qq.com/s?__biz=Mzg2OTA0Njk0OA==&mid=2247485783&idx=1&sn=a407f3b75efa17c643407daa7fb2acd6&chksm=cea2469cf9d5cf8afbcd0a8a1c9cc4294d6805b8e01bee6f76bb2884c5bc15478e91459def49&token=292197051&lang=zh_CN#rd)》。 - -👉 需要注意的是:**所有的注解,推荐使用 JSR 注解,即`javax.validation.constraints`,而不是`org.hibernate.validator.constraints`** - -#### 6.1. 一些常用的字段验证的注解 - -- `@NotEmpty` 被注释的字符串的不能为 null 也不能为空 -- `@NotBlank` 被注释的字符串非 null,并且必须包含一个非空白字符 -- `@Null` 被注释的元素必须为 null -- `@NotNull` 被注释的元素必须不为 null -- `@AssertTrue` 被注释的元素必须为 true -- `@AssertFalse` 被注释的元素必须为 false -- `@Pattern(regex=,flag=)`被注释的元素必须符合指定的正则表达式 -- `@Email` 被注释的元素必须是 Email 格式。 -- `@Min(value)`被注释的元素必须是一个数字,其值必须大于等于指定的最小值 -- `@Max(value)`被注释的元素必须是一个数字,其值必须小于等于指定的最大值 -- `@DecimalMin(value)`被注释的元素必须是一个数字,其值必须大于等于指定的最小值 -- `@DecimalMax(value)` 被注释的元素必须是一个数字,其值必须小于等于指定的最大值 -- `@Size(max=, min=)`被注释的元素的大小必须在指定的范围内 -- `@Digits(integer, fraction)`被注释的元素必须是一个数字,其值必须在可接受的范围内 -- `@Past`被注释的元素必须是一个过去的日期 -- `@Future` 被注释的元素必须是一个将来的日期 +- `@NotNull`: 检查被注解的元素(任意类型)不能为 `null`。 +- `@NotEmpty`: 检查被注解的元素(如 `CharSequence`、`Collection`、`Map`、`Array`)不能为 `null` 且其大小/长度不能为 0。注意:对于字符串,`@NotEmpty` 允许包含空白字符的字符串,如 `" "`。 +- `@NotBlank`: 检查被注解的 `CharSequence`(如 `String`)不能为 `null`,并且去除首尾空格后的长度必须大于 0。(即,不能为空白字符串)。 +- `@Null`: 检查被注解的元素必须为 `null`。 +- `@AssertTrue` / `@AssertFalse`: 检查被注解的 `boolean` 或 `Boolean` 类型元素必须为 `true` / `false`。 +- `@Min(value)` / `@Max(value)`: 检查被注解的数字类型(或其字符串表示)的值必须大于等于 / 小于等于指定的 `value`。适用于整数类型(`byte`、`short`、`int`、`long`、`BigInteger` 等)。 +- `@DecimalMin(value)` / `@DecimalMax(value)`: 功能类似 `@Min` / `@Max`,但适用于包含小数的数字类型(`BigDecimal`、`BigInteger`、`CharSequence`、`byte`、`short`、`int`、`long`及其包装类)。 `value` 必须是数字的字符串表示。 +- `@Size(min=, max=)`: 检查被注解的元素(如 `CharSequence`、`Collection`、`Map`、`Array`)的大小/长度必须在指定的 `min` 和 `max` 范围之内(包含边界)。 +- `@Digits(integer=, fraction=)`: 检查被注解的数字类型(或其字符串表示)的值,其整数部分的位数必须 ≤ `integer`,小数部分的位数必须 ≤ `fraction`。 +- `@Pattern(regexp=, flags=)`: 检查被注解的 `CharSequence`(如 `String`)是否匹配指定的正则表达式 (`regexp`)。`flags` 可以指定匹配模式(如不区分大小写)。 +- `@Email`: 检查被注解的 `CharSequence`(如 `String`)是否符合 Email 格式(内置了一个相对宽松的正则表达式)。 +- `@Past` / `@Future`: 检查被注解的日期或时间类型(`java.util.Date`、`java.util.Calendar`、JSR 310 `java.time` 包下的类型)是否在当前时间之前 / 之后。 +- `@PastOrPresent` / `@FutureOrPresent`: 类似 `@Past` / `@Future`,但允许等于当前时间。 - ...... -#### 6.2. 验证请求体(RequestBody) +### 验证请求体(RequestBody) + +当 Controller 方法使用 `@RequestBody` 注解来接收请求体并将其绑定到一个对象时,可以在该参数前添加 `@Valid` 注解来触发对该对象的校验。如果验证失败,它将抛出`MethodArgumentNotValidException`。 ```java @Data @AllArgsConstructor @NoArgsConstructor public class Person { - @NotNull(message = "classId 不能为空") private String classId; @@ -414,17 +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); @@ -432,26 +524,45 @@ public class PersonController { } ``` -#### 6.3. 验证请求参数(Path Variables 和 Request Parameters) +### 验证请求参数(Path Variables 和 Request Parameters) + +对于直接映射到方法参数的简单类型数据(如路径变量 `@PathVariable` 或请求参数 `@RequestParam`),校验方式略有不同: -**一定一定不要忘记在类上加上 `@Validated` 注解了,这个参数可以告诉 Spring 去校验方法参数。** +1. **在 Controller 类上添加 `@Validated` 注解**:这个注解是 Spring 提供的(非 JSR 标准),它使得 Spring 能够处理方法级别的参数校验注解。**这是必需步骤。** +2. **将校验注解直接放在方法参数上**:将 `@Min`, `@Max`, `@Size`, `@Pattern` 等校验注解直接应用于对应的 `@PathVariable` 或 `@RequestParam` 参数。 + +一定一定不要忘记在类上加上 `@Validated` 注解了,这个参数可以告诉 Spring 去校验方法参数。 ```java @RestController @RequestMapping("/api") -@Validated +@Validated // 关键步骤 1: 必须在类上添加 @Validated public class PersonController { @GetMapping("/person/{id}") - public ResponseEntity getPersonByID(@Valid @PathVariable("id") @Max(value = 5,message = "超过 id 的范围了") Integer id) { + public ResponseEntity getPersonByID( + @PathVariable("id") + @Max(value = 5, message = "ID 不能超过 5") // 关键步骤 2: 校验注解直接放在参数上 + Integer id + ) { + // 如果传入的 id > 5,Spring 会在进入方法体前抛出 ConstraintViolationException 异常。 + // 全局异常处理器同样需要处理此异常。 return ResponseEntity.ok().body(id); } + + @GetMapping("/person") + public ResponseEntity findPersonByName( + @RequestParam("name") + @NotBlank(message = "姓名不能为空") // 同样适用于 @RequestParam + @Size(max = 10, message = "姓名长度不能超过 10") + String name + ) { + return ResponseEntity.ok().body("Found person: " + name); + } } ``` -更多关于如何在 Spring 项目中进行参数校验的内容,请看《[如何在 Spring/Spring Boot 中做参数校验?你需要了解的都在这里!](https://mp.weixin.qq.com/s?__biz=Mzg2OTA0Njk0OA==&mid=2247485783&idx=1&sn=a407f3b75efa17c643407daa7fb2acd6&chksm=cea2469cf9d5cf8afbcd0a8a1c9cc4294d6805b8e01bee6f76bb2884c5bc15478e91459def49&token=292197051&lang=zh_CN#rd)》这篇文章。 - -### 7. 全局处理 Controller 层异常 +## 全局异常处理 介绍一下我们 Spring 项目必备的全局处理 Controller 层异常。 @@ -482,34 +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() { + ...... +} -#### 8.1. 创建表 +``` -`@Entity`声明一个类对应一个数据库实体。 +我们知道 Exception 分为运行时异常 RuntimeException 和非运行时异常。在`@Transactional`注解中如果不配置`rollbackFor`属性,那么事务只会在遇到`RuntimeException`的时候才会回滚,加上`rollbackFor=Exception.class`,可以让事务在遇到非运行时异常时也回滚。 + +`@Transactional` 注解一般可以作用在`类`或者`方法`上。 -`@Table` 设置表名 +- **作用于类**:当把`@Transactional` 注解放在类上时,表示所有该类的 public 方法都配置相同的事务属性信息。 +- **作用于方法**:当类配置了`@Transactional`,方法也配置了`@Transactional`,方法的事务会覆盖类的事务配置信息。 + +更多关于 Spring 事务的内容请查看我的这篇文章:[可能是最漂亮的 Spring 事务管理详解](./spring-transaction.md) 。 + +## JPA + +Spring Data JPA 提供了一系列注解和功能,帮助开发者轻松实现 ORM(对象关系映射)。 + +### 创建表 + +`@Entity` 用于声明一个类为 JPA 实体类,与数据库中的表映射。`@Table` 指定实体对应的表名。 ```java @Entity @Table(name = "role") public class Role { + @Id @GeneratedValue(strategy = GenerationType.IDENTITY) private Long id; + private String name; private String description; - 省略getter/setter...... + + // 省略 getter/setter } ``` -#### 8.2. 创建主键 +### 主键生成策略 -`@Id`:声明一个字段为主键。 +`@Id`声明字段为主键。`@GeneratedValue` 指定主键的生成策略。 -使用`@Id`声明之后,我们还需要定义主键的生成策略。我们可以使用 `@GeneratedValue` 指定主键生成策略。 +JPA 提供了 4 种主键生成策略: -**1.通过 `@GeneratedValue`直接使用 JPA 内置提供的四种主键生成策略来指定主键生成策略。** +- **`GenerationType.TABLE`**:通过数据库表生成主键。 +- **`GenerationType.SEQUENCE`**:通过数据库序列生成主键(适用于 Oracle 等数据库)。 +- **`GenerationType.IDENTITY`**:主键自增长(适用于 MySQL 等数据库)。 +- **`GenerationType.AUTO`**:由 JPA 自动选择合适的生成策略(默认策略)。 ```java @Id @@ -517,51 +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 @@ -578,183 +672,147 @@ private Long id; private Long id; ``` -jpa 提供的主键生成策略有如下几种: +JPA 提供的主键生成策略有如下几种: ```java public class DefaultIdentifierGeneratorFactory - implements MutableIdentifierGeneratorFactory, Serializable, ServiceRegistryAwareService { - - @SuppressWarnings("deprecation") - public DefaultIdentifierGeneratorFactory() { - register( "uuid2", UUIDGenerator.class ); - register( "guid", GUIDGenerator.class ); // can be done with UUIDGenerator + strategy - register( "uuid", UUIDHexGenerator.class ); // "deprecated" for new use - register( "uuid.hex", UUIDHexGenerator.class ); // uuid.hex is deprecated - register( "assigned", Assigned.class ); - register( "identity", IdentityGenerator.class ); - register( "select", SelectGenerator.class ); - register( "sequence", SequenceStyleGenerator.class ); - register( "seqhilo", SequenceHiLoGenerator.class ); - register( "increment", IncrementGenerator.class ); - register( "foreign", ForeignGenerator.class ); - register( "sequence-identity", SequenceIdentityGenerator.class ); - register( "enhanced-sequence", SequenceStyleGenerator.class ); - register( "enhanced-table", TableGenerator.class ); - } + implements MutableIdentifierGeneratorFactory, Serializable, ServiceRegistryAwareService { + + @SuppressWarnings("deprecation") + public DefaultIdentifierGeneratorFactory() { + register( "uuid2", UUIDGenerator.class ); + register( "guid", GUIDGenerator.class ); // can be done with UUIDGenerator + strategy + register( "uuid", UUIDHexGenerator.class ); // "deprecated" for new use + register( "uuid.hex", UUIDHexGenerator.class ); // uuid.hex is deprecated + register( "assigned", Assigned.class ); + register( "identity", IdentityGenerator.class ); + register( "select", SelectGenerator.class ); + register( "sequence", SequenceStyleGenerator.class ); + register( "seqhilo", SequenceHiLoGenerator.class ); + register( "increment", IncrementGenerator.class ); + register( "foreign", ForeignGenerator.class ); + register( "sequence-identity", SequenceIdentityGenerator.class ); + register( "enhanced-sequence", SequenceStyleGenerator.class ); + register( "enhanced-table", TableGenerator.class ); + } - public void register(String strategy, Class generatorClass) { - LOG.debugf( "Registering IdentifierGenerator strategy [%s] -> [%s]", strategy, generatorClass.getName() ); - final Class previous = generatorStrategyToClassNameMap.put( strategy, generatorClass ); - if ( previous != null ) { - LOG.debugf( " - overriding [%s]", previous.getName() ); - } - } + public void register(String strategy, Class generatorClass) { + LOG.debugf( "Registering IdentifierGenerator strategy [%s] -> [%s]", strategy, generatorClass.getName() ); + final Class previous = generatorStrategyToClassNameMap.put( strategy, generatorClass ); + if ( previous != null ) { + LOG.debugf( " - overriding [%s]", previous.getName() ); + } + } } ``` -#### 8.3. 设置字段类型 +### 字段映射 -`@Column` 声明字段。 +`@Column` 用于指定实体字段与数据库列的映射关系。 -**示例:** - -设置属性 userName 对应的数据库字段名为 user_name,长度为 32,非空 +- **`name`**:指定数据库列名。 +- **`nullable`**:指定是否允许为 `null`。 +- **`length`**:设置字段的长度(仅适用于 `String` 类型)。 +- **`columnDefinition`**:指定字段的数据库类型和默认值。 ```java -@Column(name = "user_name", nullable = false, length=32) +@Column(name = "user_name", nullable = false, length = 32) private String userName; -``` -设置字段类型并且加默认值,这个还是挺常用的。 - -```java @Column(columnDefinition = "tinyint(1) default 1") private Boolean enabled; ``` -#### 8.4. 指定不持久化特定字段 - -`@Transient`:声明不需要与数据库映射的字段,在保存的时候不需要保存进数据库 。 +### 忽略字段 -如果我们想让`secrect` 这个字段不被持久化,可以使用 `@Transient`关键字声明。 +`@Transient` 用于声明不需要持久化的字段。 ```java -@Entity(name="USER") +@Entity public class User { - ...... @Transient - private String secrect; // not persistent because of @Transient - + private String temporaryField; // 不会映射到数据库表中 } ``` -除了 `@Transient`关键字声明, 还可以采用下面几种方法: +其他不被持久化的字段方式: -```java -static String secrect; // not persistent because of static -final String secrect = "Satish"; // not persistent because of final -transient String secrect; // not persistent because of transient -``` +- **`static`**:静态字段不会被持久化。 +- **`final`**:最终字段不会被持久化。 +- **`transient`**:使用 Java 的 `transient` 关键字声明的字段不会被序列化或持久化。 -一般使用注解的方式比较多。 +### 大字段存储 -#### 8.5. 声明大字段 - -`@Lob`:声明某个字段为大字段。 +`@Lob` 用于声明大字段(如 `CLOB` 或 `BLOB`)。 ```java @Lob -private String content; -``` - -更详细的声明: - -```java -@Lob -//指定 Lob 类型数据的获取策略, FetchType.EAGER 表示非延迟加载,而 FetchType.LAZY 表示延迟加载 ; -@Basic(fetch = FetchType.EAGER) -//columnDefinition 属性指定数据表对应的 Lob 字段类型 @Column(name = "content", columnDefinition = "LONGTEXT NOT NULL") private String content; ``` -#### 8.6. 创建枚举类型的字段 +### 枚举类型映射 -可以使用枚举类型的字段,不过枚举字段要用`@Enumerated`注解修饰。 +`@Enumerated` 用于将枚举类型映射为数据库字段。 + +- **`EnumType.ORDINAL`**:存储枚举的序号(默认)。 +- **`EnumType.STRING`**:存储枚举的名称(推荐)。 ```java public enum Gender { - MALE("男性"), - FEMALE("女性"); - - private String value; - Gender(String str){ - value=str; - } + MALE, + FEMALE } -``` -```java @Entity -@Table(name = "role") -public class Role { - @Id - @GeneratedValue(strategy = GenerationType.IDENTITY) - private Long id; - private String name; - private String description; +public class User { + @Enumerated(EnumType.STRING) private Gender gender; - 省略getter/setter...... } ``` -数据库里面对应存储的是 MALE/FEMALE。 +数据库中存储的值为 `MALE` 或 `FEMALE`。 + +### 审计功能 -#### 8.7. 增加审计功能 +通过 JPA 的审计功能,可以在实体中自动记录创建时间、更新时间、创建人和更新人等信息。 -只要继承了 `AbstractAuditBase`的类都会默认加上下面四个字段。 +审计基类: ```java @Data -@AllArgsConstructor -@NoArgsConstructor @MappedSuperclass -@EntityListeners(value = AuditingEntityListener.class) +@EntityListeners(AuditingEntityListener.class) public abstract class AbstractAuditBase { @CreatedDate @Column(updatable = false) - @JsonIgnore private Instant createdAt; @LastModifiedDate - @JsonIgnore private Instant updatedAt; @CreatedBy @Column(updatable = false) - @JsonIgnore private String createdBy; @LastModifiedBy - @JsonIgnore private String updatedBy; } - ``` -我们对应的审计功能对应地配置类可能是下面这样的(Spring Security 项目): +配置审计功能: ```java - @Configuration @EnableJpaAuditing -public class AuditSecurityConfiguration { +public class AuditConfig { + @Bean - AuditorAware auditorAware() { + public AuditorAware auditorProvider() { return () -> Optional.ofNullable(SecurityContextHolder.getContext()) .map(SecurityContext::getAuthentication) .filter(Authentication::isAuthenticated) @@ -766,101 +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` 注解一般可以作用在`类`或者`方法`上。 - -- **作用于类**:当把`@Transactional` 注解放在类上时,表示所有该类的 public 方法都配置相同的事务属性信息。 -- **作用于方法**:当类配置了`@Transactional`,方法也配置了`@Transactional`,方法的事务会覆盖类的事务配置信息。 +## JSON 数据处理 -更多关于 Spring 事务的内容请查看我的这篇文章:[可能是最漂亮的 Spring 事务管理详解](./spring-transaction.md) 。 +在 Web 开发中,经常需要处理 Java 对象与 JSON 格式之间的转换。Spring 通常集成 Jackson 库来完成此任务,以下是一些常用的 Jackson 注解,可以帮助我们定制化 JSON 的序列化(Java 对象转 JSON)和反序列化(JSON 转 Java 对象)过程。 -### 10. json 数据处理 +### 过滤 JSON 字段 -#### 10.1. 过滤 json 数据 +有时我们不希望 Java 对象的某些字段被包含在最终生成的 JSON 中,或者在将 JSON 转换为 Java 对象时不处理某些 JSON 属性。 -**`@JsonIgnoreProperties` 作用在类上用于过滤掉特定字段不返回或者不解析。** +`@JsonIgnoreProperties` 作用在类上用于过滤掉特定字段不返回或者不解析。 ```java -//生成json时将userRoles属性过滤 +// 在生成 JSON 时忽略 userRoles 属性 +// 如果允许未知属性(即 JSON 中有而类中没有的属性),可以添加 ignoreUnknown = true @JsonIgnoreProperties({"userRoles"}) public class User { - private String userName; private String fullName; private String password; private List userRoles = new ArrayList<>(); + // getters and setters... } ``` -**`@JsonIgnore`一般用于类的属性上,作用和上面的`@JsonIgnoreProperties` 一样。** +`@JsonIgnore`作用于字段或` getter/setter` 方法级别,用于指定在序列化或反序列化时忽略该特定属性。 ```java - public class User { - private String userName; private String fullName; private String password; - //生成json时将userRoles属性过滤 + + // 在生成 JSON 时忽略 userRoles 属性 @JsonIgnore private List userRoles = new ArrayList<>(); + // getters and setters... } ``` -#### 10.2. 格式化 json 数据 +`@JsonIgnoreProperties` 更适用于在类定义时明确排除多个字段,或继承场景下的字段排除;`@JsonIgnore` 则更直接地用于标记单个具体字段。 + +### 格式化 JSON 数据 -`@JsonFormat`一般用来格式化 json 数据。 +`@JsonFormat` 用于指定属性在序列化和反序列化时的格式。常用于日期时间类型的格式化。 比如: ```java -@JsonFormat(shape=JsonFormat.Shape.STRING, pattern="yyyy-MM-dd'T'HH:mm:ss.SSS'Z'", timezone="GMT") +// 指定 Date 类型序列化为 ISO 8601 格式字符串,并设置时区为 GMT +@JsonFormat(shape = JsonFormat.Shape.STRING, pattern = "yyyy-MM-dd'T'HH:mm:ss.SSS'Z'", timezone = "GMT") private Date date; ``` -#### 10.3. 扁平化对象 +### 扁平化 JSON 对象 + +`@JsonUnwrapped` 注解作用于字段上,用于在序列化时将其嵌套对象的属性“提升”到当前对象的层级,反序列化时执行相反操作。这可以使 JSON 结构更扁平。 + +假设有 `Account` 类,包含 `Location` 和 `PersonInfo` 两个嵌套对象。 ```java @Getter @@ -888,7 +946,7 @@ public class Account { ``` -未扁平化之前: +未扁平化之前的 JSON 结构: ```json { @@ -903,7 +961,7 @@ public class Account { } ``` -使用`@JsonUnwrapped` 扁平对象之后: +使用`@JsonUnwrapped` 扁平对象: ```java @Getter @@ -918,6 +976,8 @@ public class Account { } ``` +扁平化后的 JSON 结构: + ```json { "provinceName": "湖北", @@ -927,34 +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)。 + 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 a1a9e3420b4..e4499b00f2e 100644 --- a/docs/system-design/framework/spring/spring-design-patterns-summary.md +++ b/docs/system-design/framework/spring/spring-design-patterns-summary.md @@ -25,7 +25,7 @@ tag: > 关于 Spring IOC 的理解,推荐看这一下知乎的一个回答: ,非常不错。 -**控制反转怎么理解呢?** 举个例子:"对象 a 依赖了对象 b,当对象 a 需要使用 对象 b 的时候必须自己去创建。但是当系统引入了 IOC 容器后, 对象 a 和对象 b 之前就失去了直接的联系。这个时候,当对象 a 需要使用 对象 b 的时候, 我们可以指定 IOC 容器去创建一个对象 b 注入到对象 a 中"。 对象 a 获得依赖对象 b 的过程,由主动行为变为了被动行为,控制权反转,这就是控制反转名字的由来。 +**控制反转怎么理解呢?** 举个例子:"对象 a 依赖了对象 b,当对象 a 需要使用 对象 b 的时候必须自己去创建。但是当系统引入了 IOC 容器后, 对象 a 和对象 b 之间就失去了直接的联系。这个时候,当对象 a 需要使用 对象 b 的时候, 我们可以指定 IOC 容器去创建一个对象 b 注入到对象 a 中"。 对象 a 获得依赖对象 b 的过程,由主动行为变为了被动行为,控制权反转,这就是控制反转名字的由来。 **DI(Dependency Inject,依赖注入)是实现控制反转的一种设计模式,依赖注入就是将实例变量传入到一个对象中去。** @@ -51,13 +51,13 @@ import org.springframework.context.ApplicationContext; import org.springframework.context.support.FileSystemXmlApplicationContext; public class App { - public static void main(String[] args) { - ApplicationContext context = new FileSystemXmlApplicationContext( - "C:/work/IOC Containers/springframework.applicationcontext/src/main/resources/bean-factory-config.xml"); + public static void main(String[] args) { + ApplicationContext context = new FileSystemXmlApplicationContext( + "C:/work/IOC Containers/springframework.applicationcontext/src/main/resources/bean-factory-config.xml"); - HelloApplicationContext obj = (HelloApplicationContext) context.getBean("helloApplicationContext"); - obj.getMsg(); - } + HelloApplicationContext obj = (HelloApplicationContext) context.getBean("helloApplicationContext"); + obj.getMsg(); + } } ``` @@ -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 快很多。 @@ -206,7 +206,7 @@ Spring 中默认存在以下事件,他们都是对 `ApplicationContextEvent` #### 事件监听者角色 -`ApplicationListener` 充当了事件监听者角色,它是一个接口,里面只定义了一个 `onApplicationEvent()`方法来处理`ApplicationEvent`。`ApplicationListener`接口类源码如下,可以看出接口定义看出接口中的事件只要实现了 `ApplicationEvent`就可以了。所以,在 Spring 中我们只要实现 `ApplicationListener` 接口的 `onApplicationEvent()` 方法即可完成监听事件 +`ApplicationListener` 充当了事件监听者角色,它是一个接口,里面只定义了一个 `onApplicationEvent()`方法来处理`ApplicationEvent`。`ApplicationListener`接口类源码如下,可以看出接口定义看出接口中的事件只要实现了 `ApplicationEvent`就可以了。所以,在 Spring 中我们只要实现 `ApplicationListener` 接口的 `onApplicationEvent()` 方法即可完成监听事件 ```java package org.springframework.context; @@ -233,7 +233,7 @@ public interface ApplicationEventPublisher { ``` -`ApplicationEventPublisher` 接口的`publishEvent()`这个方法在`AbstractApplicationContext`类中被实现,阅读这个方法的实现,你会发现实际上事件真正是通过`ApplicationEventMulticaster`来广播出去的。具体内容过多,就不在这里分析了,后面可能会单独写一篇文章提到。 +`ApplicationEventPublisher` 接口的`publishEvent()`这个方法在`AbstractApplicationContext`类中被实现,阅读这个方法的实现,你会发现实际上事件真正是通过`ApplicationEventMulticaster`来广播出去的。具体内容过多,就不在这里分析了,后面可能会单独写一篇文章提到。 ### Spring 的事件流程总结 @@ -340,14 +340,15 @@ Spring 框架中用到了哪些设计模式? - **包装器设计模式** : 我们的项目需要连接多个数据库,而且不同的客户在每次访问中根据需要会去访问不同的数据库。这种模式让我们可以根据客户的需求能够动态切换不同的数据源。 - **观察者模式:** Spring 事件驱动模型就是观察者模式很经典的一个应用。 - **适配器模式** :Spring AOP 的增强或通知(Advice)使用到了适配器模式、spring MVC 中也是用到了适配器模式适配`Controller`。 -- ...... +- …… ## 参考 - 《Spring 技术内幕》 - -- - - - - + + diff --git a/docs/system-design/framework/spring/spring-knowledge-and-questions-summary.md b/docs/system-design/framework/spring/spring-knowledge-and-questions-summary.md index 91b3e715509..eab9117ad90 100644 --- a/docs/system-design/framework/spring/spring-knowledge-and-questions-summary.md +++ b/docs/system-design/framework/spring/spring-knowledge-and-questions-summary.md @@ -30,7 +30,7 @@ Spring 翻译过来就是春天的意思,可见其目标和使命就是为 Jav Spring 提供的核心功能主要是 IoC 和 AOP。学习 Spring ,一定要把 IoC 和 AOP 的核心思想搞懂! - Spring 官网: -- GitHub 地址: https://github.com/spring-projects/spring-framework +- GitHub 地址: ### Spring 包含的模块有哪些? @@ -132,7 +132,7 @@ Spring 时代我们一般通过 XML 文件来配置 Bean,后来开发人员觉 相关阅读: - [IoC 源码阅读](https://javadoop.com/post/spring-ioc) -- [面试被问了几百遍的 IoC 和 AOP ,还在傻傻搞不清楚?](https://mp.weixin.qq.com/s?__biz=Mzg2OTA0Njk0OA==&mid=2247486938&idx=1&sn=c99ef0233f39a5ffc1b98c81e02dfcd4&chksm=cea24211f9d5cb07fa901183ba4d96187820713a72387788408040822ffb2ed575d28e953ce7&token=1736772241&lang=zh_CN#rd) +- [IoC & AOP 详解(快速搞懂)](./ioc-and-aop.md) ### 什么是 Spring Bean? @@ -207,7 +207,7 @@ public OneService getService(status) { Spring 内置的 `@Autowired` 以及 JDK 内置的 `@Resource` 和 `@Inject` 都可以用于注入 Bean。 -| Annotaion | Package | Source | +| Annotation | Package | Source | | ------------ | ---------------------------------- | ------------ | | `@Autowired` | `org.springframework.bean.factory` | Spring 2.5+ | | `@Resource` | `javax.annotation` | Java JSR-250 | @@ -277,6 +277,79 @@ private SmsService smsService; - `@Autowired` 是 Spring 提供的注解,`@Resource` 是 JDK 提供的注解。 - `Autowired` 默认的注入方式为`byType`(根据类型进行匹配),`@Resource`默认注入方式为 `byName`(根据名称进行匹配)。 - 当一个接口存在多个实现类的情况下,`@Autowired` 和`@Resource`都需要通过名称才能正确匹配到对应的 Bean。`Autowired` 可以通过 `@Qualifier` 注解来显式指定名称,`@Resource`可以通过 `name` 属性来显式指定名称。 +- `@Autowired` 支持在构造函数、方法、字段和参数上使用。`@Resource` 主要用于字段和方法上的注入,不支持在构造函数或参数上使用。 + +### 注入 Bean 的方式有哪些? + +依赖注入 (Dependency Injection, DI) 的常见方式: + +1. 构造函数注入:通过类的构造函数来注入依赖项。 +1. Setter 注入:通过类的 Setter 方法来注入依赖项。 +1. Field(字段) 注入:直接在类的字段上使用注解(如 `@Autowired` 或 `@Resource`)来注入依赖项。 + +构造函数注入示例: + +```java +@Service +public class UserService { + + private final UserRepository userRepository; + + public UserService(UserRepository userRepository) { + this.userRepository = userRepository; + } + + //... +} +``` + +Setter 注入示例: + +```java +@Service +public class UserService { + + private UserRepository userRepository; + + // 在 Spring 4.3 及以后的版本,特定情况下 @Autowired 可以省略不写 + @Autowired + public void setUserRepository(UserRepository userRepository) { + this.userRepository = userRepository; + } + + //... +} +``` + +Field 注入示例: + +```java +@Service +public class UserService { + + @Autowired + private UserRepository userRepository; + + //... +} +``` + +### 构造函数注入还是 Setter 注入? + +Spring 官方有对这个问题的回答:。 + +我这里主要提取总结完善一下 Spring 官方的建议。 + +**Spring 官方推荐构造函数注入**,这种注入方式的优势如下: + +1. 依赖完整性:确保所有必需依赖在对象创建时就被注入,避免了空指针异常的风险。 +2. 不可变性:有助于创建不可变对象,提高了线程安全性。 +3. 初始化保证:组件在使用前已完全初始化,减少了潜在的错误。 +4. 测试便利性:在单元测试中,可以直接通过构造函数传入模拟的依赖项,而不必依赖 Spring 容器进行注入。 + +构造函数注入适合处理**必需的依赖项**,而 **Setter 注入** 则更适合**可选的依赖项**,这些依赖项可以有默认值或在对象生命周期中动态设置。虽然 `@Autowired` 可以用于 Setter 方法来处理必需的依赖项,但构造函数注入仍然是更好的选择。 + +在某些情况下(例如第三方类不提供 Setter 方法),构造函数注入可能是**唯一的选择**。 ### Bean 的作用域有哪些? @@ -307,44 +380,185 @@ public Person personPrototype() { } ``` -### 单例 Bean 的线程安全问题了解吗? +### Bean 是线程安全的吗? + +Spring 框架中的 Bean 是否线程安全,取决于其作用域和状态。 + +我们这里以最常用的两种作用域 prototype 和 singleton 为例介绍。几乎所有场景的 Bean 作用域都是使用默认的 singleton ,重点关注 singleton 作用域即可。 + +prototype 作用域下,每次获取都会创建一个新的 bean 实例,不存在资源竞争问题,所以不存在线程安全问题。singleton 作用域下,IoC 容器中只有唯一的 bean 实例,可能会存在资源竞争问题(取决于 Bean 是否有状态)。如果这个 bean 是有状态的话,那就存在线程安全问题(有状态 Bean 是指包含可变的成员变量的对象)。 + +有状态 Bean 示例: + +```java +// 定义了一个购物车类,其中包含一个保存用户的购物车里商品的 List +@Component +public class ShoppingCart { + private List items = new ArrayList<>(); + + public void addItem(String item) { + items.add(item); + } + + public List getItems() { + return items; + } +} +``` + +不过,大部分 Bean 实际都是无状态(没有定义可变的成员变量)的(比如 Dao、Service),这种情况下, Bean 是线程安全的。 + +无状态 Bean 示例: + +```java +// 定义了一个用户服务,它仅包含业务逻辑而不保存任何状态。 +@Component +public class UserService { + + public User findUserById(Long id) { + //... + } + //... +} +``` + +对于有状态单例 Bean 的线程安全问题,常见的三种解决办法是: -大部分时候我们并没有在项目中使用多线程,所以很少有人会关注这个问题。单例 Bean 存在线程问题,主要是因为当多个线程操作同一个对象的时候是存在资源竞争的。 +1. **避免可变成员变量**: 尽量设计 Bean 为无状态。 +2. **使用`ThreadLocal`**: 将可变成员变量保存在 `ThreadLocal` 中,确保线程独立。 +3. **使用同步机制**: 利用 `synchronized` 或 `ReentrantLock` 来进行同步控制,确保线程安全。 -常见的有两种解决办法: +这里以 `ThreadLocal`为例,演示一下`ThreadLocal` 保存用户登录信息的场景: + +```java +public class UserThreadLocal { + + private UserThreadLocal() {} + + private static final ThreadLocal LOCAL = ThreadLocal.withInitial(() -> null); + + public static void put(SysUser sysUser) { + LOCAL.set(sysUser); + } -1. 在 Bean 中尽量避免定义可变的成员变量。 -2. 在类中定义一个 `ThreadLocal` 成员变量,将需要的可变成员变量保存在 `ThreadLocal` 中(推荐的一种方式)。 + public static SysUser get() { + return LOCAL.get(); + } -不过,大部分 Bean 实际都是无状态(没有实例变量)的(比如 Dao、Service),这种情况下, Bean 是线程安全的。 + public static void remove() { + LOCAL.remove(); + } +} +``` ### Bean 的生命周期了解么? -> 下面的内容整理自: ,除了这篇文章,再推荐一篇很不错的文章: 。 +1. **创建 Bean 的实例**:Bean 容器首先会找到配置文件中的 Bean 定义,然后使用 Java 反射 API 来创建 Bean 的实例。 +2. **Bean 属性赋值/填充**:为 Bean 设置相关属性和依赖,例如`@Autowired` 等注解注入的对象、`@Value` 注入的值、`setter`方法或构造函数注入依赖和值、`@Resource`注入的各种资源。 +3. **Bean 初始化**: + - 如果 Bean 实现了 `BeanNameAware` 接口,调用 `setBeanName()`方法,传入 Bean 的名字。 + - 如果 Bean 实现了 `BeanClassLoaderAware` 接口,调用 `setBeanClassLoader()`方法,传入 `ClassLoader`对象的实例。 + - 如果 Bean 实现了 `BeanFactoryAware` 接口,调用 `setBeanFactory()`方法,传入 `BeanFactory`对象的实例。 + - 与上面的类似,如果实现了其他 `*.Aware`接口,就调用相应的方法。 + - 如果有和加载这个 Bean 的 Spring 容器相关的 `BeanPostProcessor` 对象,执行`postProcessBeforeInitialization()` 方法 + - 如果 Bean 实现了`InitializingBean`接口,执行`afterPropertiesSet()`方法。 + - 如果 Bean 在配置文件中的定义包含 `init-method` 属性,执行指定的方法。 + - 如果有和加载这个 Bean 的 Spring 容器相关的 `BeanPostProcessor` 对象,执行`postProcessAfterInitialization()` 方法。 +4. **销毁 Bean**:销毁并不是说要立马把 Bean 给销毁掉,而是把 Bean 的销毁方法先记录下来,将来需要销毁 Bean 或者销毁容器的时候,就调用这些方法去释放 Bean 所持有的资源。 + - 如果 Bean 实现了 `DisposableBean` 接口,执行 `destroy()` 方法。 + - 如果 Bean 在配置文件中的定义包含 `destroy-method` 属性,执行指定的 Bean 销毁方法。或者,也可以直接通过`@PreDestroy` 注解标记 Bean 销毁之前执行的方法。 + +`AbstractAutowireCapableBeanFactory` 的 `doCreateBean()` 方法中能看到依次执行了这 4 个阶段: -- Bean 容器找到配置文件中 Spring Bean 的定义。 -- Bean 容器利用 Java Reflection API 创建一个 Bean 的实例。 -- 如果涉及到一些属性值 利用 `set()`方法设置一些属性值。 -- 如果 Bean 实现了 `BeanNameAware` 接口,调用 `setBeanName()`方法,传入 Bean 的名字。 -- 如果 Bean 实现了 `BeanClassLoaderAware` 接口,调用 `setBeanClassLoader()`方法,传入 `ClassLoader`对象的实例。 -- 如果 Bean 实现了 `BeanFactoryAware` 接口,调用 `setBeanFactory()`方法,传入 `BeanFactory`对象的实例。 -- 与上面的类似,如果实现了其他 `*.Aware`接口,就调用相应的方法。 -- 如果有和加载这个 Bean 的 Spring 容器相关的 `BeanPostProcessor` 对象,执行`postProcessBeforeInitialization()` 方法 -- 如果 Bean 实现了`InitializingBean`接口,执行`afterPropertiesSet()`方法。 -- 如果 Bean 在配置文件中的定义包含 init-method 属性,执行指定的方法。 -- 如果有和加载这个 Bean 的 Spring 容器相关的 `BeanPostProcessor` 对象,执行`postProcessAfterInitialization()` 方法 -- 当要销毁 Bean 的时候,如果 Bean 实现了 `DisposableBean` 接口,执行 `destroy()` 方法。 -- 当要销毁 Bean 的时候,如果 Bean 在配置文件中的定义包含 destroy-method 属性,执行指定的方法。 +```java +protected Object doCreateBean(final String beanName, final RootBeanDefinition mbd, final @Nullable Object[] args) + throws BeanCreationException { + + // 1. 创建 Bean 的实例 + BeanWrapper instanceWrapper = null; + if (instanceWrapper == null) { + instanceWrapper = createBeanInstance(beanName, mbd, args); + } -图示: + Object exposedObject = bean; + try { + // 2. Bean 属性赋值/填充 + populateBean(beanName, mbd, instanceWrapper); + // 3. Bean 初始化 + exposedObject = initializeBean(beanName, exposedObject, mbd); + } + + // 4. 销毁 Bean-注册回调接口 + try { + registerDisposableBeanIfNecessary(beanName, bean, mbd); + } + + return exposedObject; +} +``` -![Spring Bean 生命周期](https://images.xiaozhuanlan.com/photo/2019/24bc2bad3ce28144d60d9e0a2edf6c7f.jpg) +`Aware` 接口能让 Bean 能拿到 Spring 容器资源。 + +Spring 中提供的 `Aware` 接口主要有: + +1. `BeanNameAware`:注入当前 bean 对应 beanName; +2. `BeanClassLoaderAware`:注入加载当前 bean 的 ClassLoader; +3. `BeanFactoryAware`:注入当前 `BeanFactory` 容器的引用。 + +`BeanPostProcessor` 接口是 Spring 为修改 Bean 提供的强大扩展点。 + +```java +public interface BeanPostProcessor { + + // 初始化前置处理 + default Object postProcessBeforeInitialization(Object bean, String beanName) throws BeansException { + return bean; + } + + // 初始化后置处理 + default Object postProcessAfterInitialization(Object bean, String beanName) throws BeansException { + return bean; + } + +} +``` + +- `postProcessBeforeInitialization`:Bean 实例化、属性注入完成后,`InitializingBean#afterPropertiesSet`方法以及自定义的 `init-method` 方法之前执行; +- `postProcessAfterInitialization`:类似于上面,不过是在 `InitializingBean#afterPropertiesSet`方法以及自定义的 `init-method` 方法之后执行。 + +`InitializingBean` 和 `init-method` 是 Spring 为 Bean 初始化提供的扩展点。 + +```java +public interface InitializingBean { + // 初始化逻辑 + void afterPropertiesSet() throws Exception; +} +``` + +指定 `init-method` 方法,指定初始化方法: + +```xml + + + + + + +``` -与之比较类似的中文版本: +**如何记忆呢?** -![Spring Bean 生命周期](https://images.xiaozhuanlan.com/photo/2019/b5d264565657a5395c2781081a7483e1.jpg) +1. 整体上可以简单分为四步:实例化 —> 属性赋值 —> 初始化 —> 销毁。 +2. 初始化这一步涉及到的步骤比较多,包含 `Aware` 接口的依赖注入、`BeanPostProcessor` 在初始化前后的处理以及 `InitializingBean` 和 `init-method` 的初始化操作。 +3. 销毁这一步会注册相关销毁回调接口,最后通过`DisposableBean` 和 `destory-method` 进行销毁。 -## Spring AoP +最后,再分享一张清晰的图解(图源:[如何记忆 Spring Bean 的生命周期](https://chaycao.github.io/2020/02/15/如何记忆Spring-Bean的生命周期.html))。 + +![](https://oss.javaguide.cn/github/javaguide/system-design/framework/spring/spring-bean-lifestyle.png) + +## Spring AOP ### 谈谈自己对于 AOP 的了解 @@ -356,7 +570,7 @@ Spring AOP 就是基于动态代理的,如果要代理的对象,实现了某 当然你也可以使用 **AspectJ** !Spring AOP 已经集成了 AspectJ ,AspectJ 应该算的上是 Java 生态系统中最完整的 AOP 框架了。 -AOP 切面编程设计到的一些专业术语: +AOP 切面编程涉及到的一些专业术语: | 术语 | 含义 | | :---------------- | :-------------------------------------------------------------------: | @@ -370,13 +584,24 @@ AOP 切面编程设计到的一些专业术语: ### Spring AOP 和 AspectJ AOP 有什么区别? -**Spring AOP 属于运行时增强,而 AspectJ 是编译时增强。** Spring AOP 基于代理(Proxying),而 AspectJ 基于字节码操作(Bytecode Manipulation)。 +| 特性 | Spring AOP | AspectJ | +| -------------- | -------------------------------------------------------- | ------------------------------------------ | +| **增强方式** | 运行时增强(基于动态代理) | 编译时增强、类加载时增强(直接操作字节码) | +| **切入点支持** | 方法级(Spring Bean 范围内,不支持 final 和 staic 方法) | 方法级、字段、构造器、静态方法等 | +| **性能** | 运行时依赖代理,有一定开销,切面多时性能较低 | 运行时无代理开销,性能更高 | +| **复杂性** | 简单,易用,适合大多数场景 | 功能强大,但相对复杂 | +| **使用场景** | Spring 应用下比较简单的 AOP 需求 | 高性能、高复杂度的 AOP 需求 | + +**如何选择?** + +- **功能考量**:AspectJ 支持更复杂的 AOP 场景,Spring AOP 更简单易用。如果你需要增强 `final` 方法、静态方法、字段访问、构造器调用等,或者需要在非 Spring 管理的对象上应用增强逻辑,AspectJ 是唯一的选择。 +- **性能考量**:切面数量较少时两者性能差异不大,但切面较多时 AspectJ 性能更优。 -Spring AOP 已经集成了 AspectJ ,AspectJ 应该算的上是 Java 生态系统中最完整的 AOP 框架了。AspectJ 相比于 Spring AOP 功能更加强大,但是 Spring AOP 相对来说更简单, +**一句话总结**:简单场景优先使用 Spring AOP;复杂场景或高性能需求时,选择 AspectJ。 -如果我们的切面比较少,那么两者性能差异不大。但是,当切面太多的话,最好选择 AspectJ ,它比 Spring AOP 快很多。 +### AOP 常见的通知类型有哪些? -### AspectJ 定义的通知类型有哪些? +![](https://oss.javaguide.cn/github/javaguide/system-design/framework/spring/aspectj-advice-types.jpg) - **Before**(前置通知):目标对象的方法调用之前触发 - **After** (后置通知):目标对象的方法调用之后触发 @@ -441,7 +666,7 @@ MVC 是模型(Model)、视图(View)、控制器(Controller)的简写,其核心 - Model:系统涉及的数据,也就是 dao 和 bean。 - View:展示模型中的数据,只是用来展示。 -- Controller:处理用户请求都发送给 ,返回数据给 JSP 并展示给用户。 +- Controller:接受用户请求,并将请求发送至 Model,最后返回数据给 JSP 并展示给用户 ![](https://oss.javaguide.cn/java-guide-blog/mvc-model2.png) @@ -460,7 +685,7 @@ MVC 是一种设计模式,Spring MVC 是一款很优秀的 MVC 框架。Spring 记住了下面这些组件,也就记住了 SpringMVC 的工作原理。 - **`DispatcherServlet`**:**核心的中央处理器**,负责接收请求、分发,并给予客户端响应。 -- **`HandlerMapping`**:**处理器映射器**,根据 uri 去匹配查找能处理的 `Handler` ,并会将请求涉及到的拦截器和 `Handler` 一起封装。 +- **`HandlerMapping`**:**处理器映射器**,根据 URL 去匹配查找能处理的 `Handler` ,并会将请求涉及到的拦截器和 `Handler` 一起封装。 - **`HandlerAdapter`**:**处理器适配器**,根据 `HandlerMapping` 找到的 `Handler` ,适配执行对应的 `Handler`; - **`Handler`**:**请求处理器**,处理实际请求的处理器。 - **`ViewResolver`**:**视图解析器**,根据 `Handler` 返回的逻辑视图 / 视图,解析并渲染真正的视图,并传递给 `DispatcherServlet` 响应客户端 @@ -476,13 +701,23 @@ MVC 是一种设计模式,Spring MVC 是一款很优秀的 MVC 框架。Spring **流程说明(重要):** 1. 客户端(浏览器)发送请求, `DispatcherServlet`拦截请求。 -2. `DispatcherServlet` 根据请求信息调用 `HandlerMapping` 。`HandlerMapping` 根据 uri 去匹配查找能处理的 `Handler`(也就是我们平常说的 `Controller` 控制器) ,并会将请求涉及到的拦截器和 `Handler` 一起封装。 -3. `DispatcherServlet` 调用 `HandlerAdapter`适配执行 `Handler` 。 +2. `DispatcherServlet` 根据请求信息调用 `HandlerMapping` 。`HandlerMapping` 根据 URL 去匹配查找能处理的 `Handler`(也就是我们平常说的 `Controller` 控制器) ,并会将请求涉及到的拦截器和 `Handler` 一起封装。 +3. `DispatcherServlet` 调用 `HandlerAdapter`适配器执行 `Handler` 。 4. `Handler` 完成对用户请求的处理后,会返回一个 `ModelAndView` 对象给`DispatcherServlet`,`ModelAndView` 顾名思义,包含了数据模型以及相应的视图的信息。`Model` 是返回的数据对象,`View` 是个逻辑上的 `View`。 5. `ViewResolver` 会根据逻辑 `View` 查找实际的 `View`。 6. `DispaterServlet` 把返回的 `Model` 传给 `View`(视图渲染)。 7. 把 `View` 返回给请求者(浏览器) +上述流程是传统开发模式(JSP,Thymeleaf 等)的工作原理。然而现在主流的开发方式是前后端分离,这种情况下 Spring MVC 的 `View` 概念发生了一些变化。由于 `View` 通常由前端框架(Vue, React 等)来处理,后端不再负责渲染页面,而是只负责提供数据,因此: + +- 前后端分离时,后端通常不再返回具体的视图,而是返回**纯数据**(通常是 JSON 格式),由前端负责渲染和展示。 +- `View` 的部分在前后端分离的场景下往往不需要设置,Spring MVC 的控制器方法只需要返回数据,不再返回 `ModelAndView`,而是直接返回数据,Spring 会自动将其转换为 JSON 格式。相应的,`ViewResolver` 也将不再被使用。 + +怎么做到呢? + +- 使用 `@RestController` 注解代替传统的 `@Controller` 注解,这样所有方法默认会返回 JSON 格式的数据,而不是试图解析视图。 +- 如果你使用的是 `@Controller`,可以结合 `@ResponseBody` 注解来返回 JSON。 + ### 统一异常处理怎么做? 推荐使用注解的方式统一异常处理,具体会使用到 `@ControllerAdvice` + `@ExceptionHandler` 这两个注解 。 @@ -510,25 +745,25 @@ public class GlobalExceptionHandler { ```java @Nullable - private Method getMappedMethod(Class exceptionType) { - List> matches = new ArrayList<>(); + private Method getMappedMethod(Class exceptionType) { + List> matches = new ArrayList<>(); //找到可以处理的所有异常信息。mappedMethods 中存放了异常和处理异常的方法的对应关系 - for (Class mappedException : this.mappedMethods.keySet()) { - if (mappedException.isAssignableFrom(exceptionType)) { - matches.add(mappedException); - } - } + for (Class mappedException : this.mappedMethods.keySet()) { + if (mappedException.isAssignableFrom(exceptionType)) { + matches.add(mappedException); + } + } // 不为空说明有方法处理异常 - if (!matches.isEmpty()) { + if (!matches.isEmpty()) { // 按照匹配程度从小到大排序 - matches.sort(new ExceptionDepthComparator(exceptionType)); + matches.sort(new ExceptionDepthComparator(exceptionType)); // 返回处理异常的方法 - return this.mappedMethods.get(matches.get(0)); - } - else { - return null; - } - } + return this.mappedMethods.get(matches.get(0)); + } + else { + return null; + } + } ``` 从源代码看出:**`getMappedMethod()`会首先找到可以匹配处理异常的所有方法信息,然后对其进行从小到大的排序,最后取最小的那一个匹配的方法(即匹配度最高的那个)。** @@ -544,7 +779,165 @@ public class GlobalExceptionHandler { - **包装器设计模式** : 我们的项目需要连接多个数据库,而且不同的客户在每次访问中根据需要会去访问不同的数据库。这种模式让我们可以根据客户的需求能够动态切换不同的数据源。 - **观察者模式:** Spring 事件驱动模型就是观察者模式很经典的一个应用。 - **适配器模式** : Spring AOP 的增强或通知(Advice)使用到了适配器模式、spring MVC 中也是用到了适配器模式适配`Controller`。 -- ...... +- …… + +## Spring 的循环依赖 + +### Spring 循环依赖了解吗,怎么解决? + +循环依赖是指 Bean 对象循环引用,是两个或多个 Bean 之间相互持有对方的引用,例如 CircularDependencyA → CircularDependencyB → CircularDependencyA。 + +```java +@Component +public class CircularDependencyA { + @Autowired + private CircularDependencyB circB; +} + +@Component +public class CircularDependencyB { + @Autowired + private CircularDependencyA circA; +} +``` + +单个对象的自我依赖也会出现循环依赖,但这种概率极低,属于是代码编写错误。 + +```java +@Component +public class CircularDependencyA { + @Autowired + private CircularDependencyA circA; +} +``` + +Spring 框架通过使用三级缓存来解决这个问题,确保即使在循环依赖的情况下也能正确创建 Bean。 + +Spring 中的三级缓存其实就是三个 Map,如下: + +```java +// 一级缓存 +/** Cache of singleton objects: bean name to bean instance. */ +private final Map singletonObjects = new ConcurrentHashMap<>(256); + +// 二级缓存 +/** Cache of early singleton objects: bean name to bean instance. */ +private final Map earlySingletonObjects = new HashMap<>(16); + +// 三级缓存 +/** Cache of singleton factories: bean name to ObjectFactory. */ +private final Map> singletonFactories = new HashMap<>(16); +``` + +简单来说,Spring 的三级缓存包括: + +1. **一级缓存(singletonObjects)**:存放最终形态的 Bean(已经实例化、属性填充、初始化),单例池,为“Spring 的单例属性”⽽⽣。一般情况我们获取 Bean 都是从这里获取的,但是并不是所有的 Bean 都在单例池里面,例如原型 Bean 就不在里面。 +2. **二级缓存(earlySingletonObjects)**:存放过渡 Bean(半成品,尚未属性填充),也就是三级缓存中`ObjectFactory`产生的对象,与三级缓存配合使用的,可以防止 AOP 的情况下,每次调用`ObjectFactory#getObject()`都是会产生新的代理对象的。 +3. **三级缓存(singletonFactories)**:存放`ObjectFactory`,`ObjectFactory`的`getObject()`方法(最终调用的是`getEarlyBeanReference()`方法)可以生成原始 Bean 对象或者代理对象(如果 Bean 被 AOP 切面代理)。三级缓存只会对单例 Bean 生效。 + +接下来说一下 Spring 创建 Bean 的流程: + +1. 先去 **一级缓存 `singletonObjects`** 中获取,存在就返回; +2. 如果不存在或者对象正在创建中,于是去 **二级缓存 `earlySingletonObjects`** 中获取; +3. 如果还没有获取到,就去 **三级缓存 `singletonFactories`** 中获取,通过执行 `ObjectFacotry` 的 `getObject()` 就可以获取该对象,获取成功之后,从三级缓存移除,并将该对象加入到二级缓存中。 + +在三级缓存中存储的是 `ObjectFacoty` : + +```java +public interface ObjectFactory { + T getObject() throws BeansException; +} +``` + +Spring 在创建 Bean 的时候,如果允许循环依赖的话,Spring 就会将刚刚实例化完成,但是属性还没有初始化完的 Bean 对象给提前暴露出去,这里通过 `addSingletonFactory` 方法,向三级缓存中添加一个 `ObjectFactory` 对象: + +```java +// AbstractAutowireCapableBeanFactory # doCreateBean # +public abstract class AbstractAutowireCapableBeanFactory ... { + protected Object doCreateBean(...) { + //... + + // 支撑循环依赖:将 ()->getEarlyBeanReference 作为一个 ObjectFactory 对象的 getObject() 方法加入到三级缓存中 + addSingletonFactory(beanName, () -> getEarlyBeanReference(beanName, mbd, bean)); + } +} +``` + +那么上边在说 Spring 创建 Bean 的流程时说了,如果一级缓存、二级缓存都取不到对象时,会去三级缓存中通过 `ObjectFactory` 的 `getObject` 方法获取对象。 + +```java +class A { + // 使用了 B + private B b; +} +class B { + // 使用了 A + private A a; +} +``` + +以上面的循环依赖代码为例,整个解决循环依赖的流程如下: + +- 当 Spring 创建 A 之后,发现 A 依赖了 B ,又去创建 B,B 依赖了 A ,又去创建 A; +- 在 B 创建 A 的时候,那么此时 A 就发生了循环依赖,由于 A 此时还没有初始化完成,因此在 **一二级缓存** 中肯定没有 A; +- 那么此时就去三级缓存中调用 `getObject()` 方法去获取 A 的 **前期暴露的对象** ,也就是调用上边加入的 `getEarlyBeanReference()` 方法,生成一个 A 的 **前期暴露对象**; +- 然后就将这个 `ObjectFactory` 从三级缓存中移除,并且将前期暴露对象放入到二级缓存中,那么 B 就将这个前期暴露对象注入到依赖,来支持循环依赖。 + +**只用两级缓存够吗?** 在没有 AOP 的情况下,确实可以只使用一级和二级缓存来解决循环依赖问题。但是,当涉及到 AOP 时,三级缓存就显得非常重要了,因为它确保了即使在 Bean 的创建过程中有多次对早期引用的请求,也始终只返回同一个代理对象,从而避免了同一个 Bean 有多个代理对象的问题。 + +**最后总结一下 Spring 如何解决三级缓存**: + +在三级缓存这一块,主要记一下 Spring 是如何支持循环依赖的即可,也就是如果发生循环依赖的话,就去 **三级缓存 `singletonFactories`** 中拿到三级缓存中存储的 `ObjectFactory` 并调用它的 `getObject()` 方法来获取这个循环依赖对象的前期暴露对象(虽然还没初始化完成,但是可以拿到该对象在堆中的存储地址了),并且将这个前期暴露对象放到二级缓存中,这样在循环依赖时,就不会重复初始化了! + +不过,这种机制也有一些缺点,比如增加了内存开销(需要维护三级缓存,也就是三个 Map),降低了性能(需要进行多次检查和转换)。并且,还有少部分情况是不支持循环依赖的,比如非单例的 bean 和`@Async`注解的 bean 无法支持循环依赖。 + +### @Lazy 能解决循环依赖吗? + +`@Lazy` 用来标识类是否需要懒加载/延迟加载,可以作用在类上、方法上、构造器上、方法参数上、成员变量中。 + +Spring Boot 2.2 新增了**全局懒加载属性**,开启后全局 bean 被设置为懒加载,需要时再去创建。 + +配置文件配置全局懒加载: + +```properties +#默认false +spring.main.lazy-initialization=true +``` + +编码的方式设置全局懒加载: + +```java +SpringApplication springApplication=new SpringApplication(Start.class); +springApplication.setLazyInitialization(false); +springApplication.run(args); +``` + +如非必要,尽量不要用全局懒加载。全局懒加载会让 Bean 第一次使用的时候加载会变慢,并且它会延迟应用程序问题的发现(当 Bean 被初始化时,问题才会出现)。 + +如果一个 Bean 没有被标记为懒加载,那么它会在 Spring IoC 容器启动的过程中被创建和初始化。如果一个 Bean 被标记为懒加载,那么它不会在 Spring IoC 容器启动时立即实例化,而是在第一次被请求时才创建。这可以帮助减少应用启动时的初始化时间,也可以用来解决循环依赖问题。 + +循环依赖问题是如何通过`@Lazy` 解决的呢?这里举一个例子,比如说有两个 Bean,A 和 B,他们之间发生了循环依赖,那么 A 的构造器上添加 `@Lazy` 注解之后(延迟 Bean B 的实例化),加载的流程如下: + +- 首先 Spring 会去创建 A 的 Bean,创建时需要注入 B 的属性; +- 由于在 A 上标注了 `@Lazy` 注解,因此 Spring 会去创建一个 B 的代理对象,将这个代理对象注入到 A 中的 B 属性; +- 之后开始执行 B 的实例化、初始化,在注入 B 中的 A 属性时,此时 A 已经创建完毕了,就可以将 A 给注入进去。 + +从上面的加载流程可以看出: `@Lazy` 解决循环依赖的关键点在于代理对象的使用。 + +- **没有 `@Lazy` 的情况下**:在 Spring 容器初始化 `A` 时会立即尝试创建 `B`,而在创建 `B` 的过程中又会尝试创建 `A`,最终导致循环依赖(即无限递归,最终抛出异常)。 +- **使用 `@Lazy` 的情况下**:Spring 不会立即创建 `B`,而是会注入一个 `B` 的代理对象。由于此时 `B` 仍未被真正初始化,`A` 的初始化可以顺利完成。等到 `A` 实例实际调用 `B` 的方法时,代理对象才会触发 `B` 的真正初始化。 + +`@Lazy` 能够在一定程度上打破循环依赖链,允许 Spring 容器顺利地完成 Bean 的创建和注入。但这并不是一个根本性的解决方案,尤其是在构造函数注入、复杂的多级依赖等场景中,`@Lazy` 无法有效地解决问题。因此,最佳实践仍然是尽量避免设计上的循环依赖。 + +### SpringBoot 允许循环依赖发生么? + +SpringBoot 2.6.x 以前是默认允许循环依赖的,也就是说你的代码出现了循环依赖问题,一般情况下也不会报错。SpringBoot 2.6.x 以后官方不再推荐编写存在循环依赖的代码,建议开发者自己写代码的时候去减少不必要的互相依赖。这其实也是我们最应该去做的,循环依赖本身就是一种设计缺陷,我们不应该过度依赖 Spring 而忽视了编码的规范和质量,说不定未来某个 SpringBoot 版本就彻底禁止循环依赖的代码了。 + +SpringBoot 2.6.x 以后,如果你不想重构循环依赖的代码的话,也可以采用下面这些方法: + +- 在全局配置文件中设置允许循环依赖存在:`spring.main.allow-circular-references=true`。最简单粗暴的方式,不太推荐。 +- 在导致循环依赖的 Bean 上添加 `@Lazy` 注解,这是一种比较推荐的方式。`@Lazy` 用来标识类是否需要懒加载/延迟加载,可以作用在类上、方法上、构造器上、方法参数上、成员变量中。 +- …… ## Spring 事务 @@ -552,8 +945,8 @@ public class GlobalExceptionHandler { ### Spring 管理事务的方式有几种? -- **编程式事务**:在代码中硬编码(不推荐使用) : 通过 `TransactionTemplate`或者 `TransactionManager` 手动管理事务,实际应用中很少使用,但是对于你理解 Spring 事务管理原理有帮助。 -- **声明式事务**:在 XML 配置文件中配置或者直接基于注解(推荐使用) : 实际是通过 AOP 实现(基于`@Transactional` 的全注解方式使用最多) +- **编程式事务**:在代码中硬编码(在分布式系统中推荐使用) : 通过 `TransactionTemplate`或者 `TransactionManager` 手动管理事务,事务范围过大会出现事务未提交导致超时,因此事务要比锁的粒度更小。 +- **声明式事务**:在 XML 配置文件中配置或者直接基于注解(单体应用或者简单业务系统推荐使用) : 实际是通过 AOP 实现(基于`@Transactional` 的全注解方式使用最多) ### Spring 事务中哪几种事务传播行为? @@ -595,13 +988,9 @@ public class GlobalExceptionHandler { public enum Isolation { DEFAULT(TransactionDefinition.ISOLATION_DEFAULT), - READ_UNCOMMITTED(TransactionDefinition.ISOLATION_READ_UNCOMMITTED), - READ_COMMITTED(TransactionDefinition.ISOLATION_READ_COMMITTED), - REPEATABLE_READ(TransactionDefinition.ISOLATION_REPEATABLE_READ), - SERIALIZABLE(TransactionDefinition.ISOLATION_SERIALIZABLE); private final int value; @@ -629,9 +1018,29 @@ public enum Isolation { `Exception` 分为运行时异常 `RuntimeException` 和非运行时异常。事务管理对于企业应用来说是至关重要的,即使出现异常情况,它也可以保证数据的一致性。 -当 `@Transactional` 注解作用于类上时,该类的所有 public 方法将都具有该类型的事务属性,同时,我们也可以在方法级别使用该标注来覆盖类级别的定义。如果类或者方法加了这个注解,那么这个类里面的方法抛出异常,就会回滚,数据库里面的数据也会回滚。 +当 `@Transactional` 注解作用于类上时,该类的所有 public 方法将都具有该类型的事务属性,同时,我们也可以在方法级别使用该标注来覆盖类级别的定义。 -在 `@Transactional` 注解中如果不配置`rollbackFor`属性,那么事务只会在遇到`RuntimeException`的时候才会回滚,加上 `rollbackFor=Exception.class`,可以让事务在遇到非运行时异常时也回滚。 +`@Transactional` 注解默认回滚策略是只有在遇到`RuntimeException`(运行时异常) 或者 `Error` 时才会回滚事务,而不会回滚 `Checked Exception`(受检查异常)。这是因为 Spring 认为`RuntimeException`和 Error 是不可预期的错误,而受检异常是可预期的错误,可以通过业务逻辑来处理。 + +![](https://oss.javaguide.cn/github/javaguide/system-design/framework/spring/spring-transactional-rollbackfor.png) + +如果想要修改默认的回滚策略,可以使用 `@Transactional` 注解的 `rollbackFor` 和 `noRollbackFor` 属性来指定哪些异常需要回滚,哪些异常不需要回滚。例如,如果想要让所有的异常都回滚事务,可以使用如下的注解: + +```java +@Transactional(rollbackFor = Exception.class) +public void someMethod() { +// some business logic +} +``` + +如果想要让某些特定的异常不回滚事务,可以使用如下的注解: + +```java +@Transactional(noRollbackFor = CustomException.class) +public void someMethod() { +// some business logic +} +``` ## Spring Data JPA @@ -713,7 +1122,7 @@ public abstract class AbstractAuditBase { ### 实体之间的关联关系注解有哪些? -- `@OneToOne ` : 一对一。 +- `@OneToOne` : 一对一。 - `@ManyToMany`:多对多。 - `@OneToMany` : 一对多。 - `@ManyToOne`:多对一。 @@ -734,7 +1143,7 @@ Spring Security 重要的是实战,这里仅对小部分知识点进行总结 - `authenticated()`:只允许已认证的用户访问。 - `fullyAuthenticated()`:只允许已经登录或者通过 remember-me 登录的用户访问。 - `hasRole(String)` : 只允许指定的角色访问。 -- `hasAnyRole(String) ` : 指定一个或者多个角色,满足其一的用户即可访问。 +- `hasAnyRole(String)` : 指定一个或者多个角色,满足其一的用户即可访问。 - `hasAuthority(String)`:只允许具有指定权限的用户访问 - `hasAnyAuthority(String)`:指定一个或者多个权限,满足其一的用户即可访问。 - `hasIpAddress(String)` : 只允许指定 ip 的用户访问。 @@ -747,7 +1156,7 @@ Spring Security 重要的是实战,这里仅对小部分知识点进行总结 如果我们需要保存密码这类敏感数据到数据库的话,需要先加密再保存。 -Spring Security 提供了多种加密算法的实现,开箱即用,非常方便。这些加密算法实现类的父类是 `PasswordEncoder` ,如果你想要自己实现一个加密算法的话,也需要继承 `PasswordEncoder`。 +Spring Security 提供了多种加密算法的实现,开箱即用,非常方便。这些加密算法实现类的接口是 `PasswordEncoder` ,如果你想要自己实现一个加密算法的话,也需要实现 `PasswordEncoder` 接口。 `PasswordEncoder` 接口一共也就 3 个必须实现的方法。 @@ -779,11 +1188,13 @@ public interface PasswordEncoder { ## 参考 - 《Spring 技术内幕》 -- 《从零开始深入学习 Spring》:https://juejin.cn/book/6857911863016390663 +- 《从零开始深入学习 Spring》: - - - -- https://www.cnblogs.com/clwydjgs/p/9317849.html +- - - - + + diff --git a/docs/system-design/framework/spring/spring-transaction.md b/docs/system-design/framework/spring/spring-transaction.md index 2b1a8e33c60..c9358ab2f9f 100644 --- a/docs/system-design/framework/spring/spring-transaction.md +++ b/docs/system-design/framework/spring/spring-transaction.md @@ -16,10 +16,10 @@ tag: 我们系统的每个业务方法可能包括了多个原子性的数据库操作,比如下面的 `savePerson()` 方法中就有两个原子性的数据库操作。这些原子性的数据库操作是有依赖的,它们要么都执行,要不就都不执行。 ```java - public void savePerson() { - personDao.save(person); - personDetailDao.save(personDetail); - } + public void savePerson() { + personDao.save(person); + personDetailDao.save(personDetail); + } ``` 另外,需要格外注意的是:**事务能否生效数据库引擎是否支持事务是关键。比如常用的 MySQL 数据库默认使用支持事务的 `innodb`引擎。但是,如果把数据库引擎变为 `myisam`,那么程序也就不再支持事务了!** @@ -35,23 +35,23 @@ tag: ```java public class OrdersService { - private AccountDao accountDao; + private AccountDao accountDao; - public void setOrdersDao(AccountDao accountDao) { - this.accountDao = accountDao; - } + public void setOrdersDao(AccountDao accountDao) { + this.accountDao = accountDao; + } @Transactional(propagation = Propagation.REQUIRED, isolation = Isolation.DEFAULT, readOnly = false, timeout = -1) - public void accountMoney() { + public void accountMoney() { //小红账户多1000 - accountDao.addMoney(1000,xiaohong); - //模拟突然出现的异常,比如银行中可能为突然停电等等 + accountDao.addMoney(1000,xiaohong); + //模拟突然出现的异常,比如银行中可能为突然停电等等 //如果没有配置事务管理的话会造成,小红账户多了1000而小明账户没有少钱 - int i = 10 / 0; - //小王账户少1000 - accountDao.reduceMoney(1000,xiaoming); - } + int i = 10 / 0; + //小王账户少1000 + accountDao.reduceMoney(1000,xiaoming); + } } ``` @@ -74,7 +74,7 @@ public class OrdersService { > > 翻译过来的意思是:原子性,隔离性和持久性是数据库的属性,而一致性(在 ACID 意义上)是应用程序的属性。应用可能依赖数据库的原子性和隔离属性来实现一致性,但这并不仅取决于数据库。因此,字母 C 不属于 ACID 。 -《Designing Data-Intensive Application(数据密集型应用系统设计)》这本书强推一波,值得读很多遍!豆瓣有接近 90% 的人看了这本书之后给了五星好评。另外,中文翻译版本已经在 GitHub 开源,地址:[https://github.com/Vonng/ddiaopen in new window](https://github.com/Vonng/ddia) 。 +《Designing Data-Intensive Application(数据密集型应用系统设计)》这本书强推一波,值得读很多遍!豆瓣有接近 90% 的人看了这本书之后给了五星好评。另外,中文翻译版本已经在 GitHub 开源,地址:[https://github.com/Vonng/ddia](https://github.com/Vonng/ddia) 。 ## 详谈 Spring 对事务的支持 @@ -276,7 +276,7 @@ public interface TransactionStatus{ ### 事务属性详解 -实际业务开发中,大家一般都是使用 `@Transactional` 注解来开启事务,但很多人并不清楚这个注解里面的参数是什么意思,有什么用。为了更好的在项目中使用事务管理,强烈推荐好好阅读一下下面的内容。 +实际业务开发中,大家一般都是使用 `@Transactional` 注解来开启事务,很多人并不清楚这个注解里面的参数是什么意思,有什么用。为了更好的在项目中使用事务管理,强烈推荐好好阅读一下下面的内容。 #### 事务传播行为 @@ -419,33 +419,66 @@ Class B { **3.`TransactionDefinition.PROPAGATION_NESTED`**: -如果当前存在事务,就在嵌套事务内执行;如果当前没有事务,就执行与`TransactionDefinition.PROPAGATION_REQUIRED`类似的操作。也就是说: +如果当前存在事务,则创建一个事务作为当前事务的嵌套事务执行; 如果当前没有事务,就执行与`TransactionDefinition.PROPAGATION_REQUIRED`类似的操作。也就是说: - 在外部方法开启事务的情况下,在内部开启一个新的事务,作为嵌套事务存在。 - 如果外部方法无事务,则单独开启一个事务,与 `PROPAGATION_REQUIRED` 类似。 -这里还是简单举个例子:如果 `bMethod()` 回滚的话,`aMethod()`不会回滚。如果 `aMethod()` 回滚的话,`bMethod()`会回滚。 - -```java -@Service -Class A { - @Autowired - B b; - @Transactional(propagation = Propagation.REQUIRED) - public void aMethod { - //do something - b.bMethod(); +`TransactionDefinition.PROPAGATION_NESTED`代表的嵌套事务以父子关系呈现,其核心理念是子事务不会独立提交,依赖于父事务,在父事务中运行;当父事务提交时,子事务也会随着提交,理所当然的,当父事务回滚时,子事务也会回滚; +> 与`TransactionDefinition.PROPAGATION_REQUIRES_NEW`区别于:`PROPAGATION_REQUIRES_NEW`是独立事务,不依赖于外部事务,以平级关系呈现,执行完就会立即提交,与外部事务无关; + +子事务也有自己的特性,可以独立进行回滚,不会引发父事务的回滚,但是前提是需要处理子事务的异常,避免异常被父事务感知导致外部事务回滚; + +举个例子: +- 如果 `aMethod()` 回滚的话,作为嵌套事务的`bMethod()`会回滚。 +- 如果 `bMethod()` 回滚的话,`aMethod()`是否回滚,要看`bMethod()`的异常是否被处理: + - `bMethod()`的异常没有被处理,即`bMethod()`内部没有处理异常,且`aMethod()`也没有处理异常,那么`aMethod()`将感知异常致使整体回滚。 + ```java + @Service + Class A { + @Autowired + B b; + @Transactional(propagation = Propagation.REQUIRED) + public void aMethod (){ + //do something + b.bMethod(); + } } -} - -@Service -Class B { - @Transactional(propagation = Propagation.NESTED) - public void bMethod { - //do something + + @Service + Class B { + @Transactional(propagation = Propagation.NESTED) + public void bMethod (){ + //do something and throw an exception + } } -} -``` + ``` + - `bMethod()`处理异常或`aMethod()`处理异常,`aMethod()`不会回滚。 + + ```java + @Service + Class A { + @Autowired + B b; + @Transactional(propagation = Propagation.REQUIRED) + public void aMethod (){ + //do something + try { + b.bMethod(); + } catch (Exception e) { + System.out.println("方法回滚"); + } + } + } + + @Service + Class B { + @Transactional(propagation = Propagation.NESTED) + public void bMethod { + //do something and throw an exception + } + } + ``` **4.`TransactionDefinition.PROPAGATION_MANDATORY`** @@ -582,27 +615,27 @@ public interface TransactionDefinition { @Documented public @interface Transactional { - @AliasFor("transactionManager") - String value() default ""; + @AliasFor("transactionManager") + String value() default ""; - @AliasFor("value") - String transactionManager() default ""; + @AliasFor("value") + String transactionManager() default ""; - Propagation propagation() default Propagation.REQUIRED; + Propagation propagation() default Propagation.REQUIRED; - Isolation isolation() default Isolation.DEFAULT; + Isolation isolation() default Isolation.DEFAULT; - int timeout() default TransactionDefinition.TIMEOUT_DEFAULT; + int timeout() default TransactionDefinition.TIMEOUT_DEFAULT; - boolean readOnly() default false; + boolean readOnly() default false; - Class[] rollbackFor() default {}; + Class[] rollbackFor() default {}; - String[] rollbackForClassName() default {}; + String[] rollbackForClassName() default {}; - Class[] noRollbackFor() default {}; + Class[] noRollbackFor() default {}; - String[] noRollbackForClassName() default {}; + String[] noRollbackForClassName() default {}; } ``` @@ -628,23 +661,23 @@ public @interface Transactional { ```java public class DefaultAopProxyFactory implements AopProxyFactory, Serializable { - @Override - public AopProxy createAopProxy(AdvisedSupport config) throws AopConfigException { - if (config.isOptimize() || config.isProxyTargetClass() || hasNoUserSuppliedProxyInterfaces(config)) { - Class targetClass = config.getTargetClass(); - if (targetClass == null) { - throw new AopConfigException("TargetSource cannot determine target class: " + - "Either an interface or a target is required for proxy creation."); - } - if (targetClass.isInterface() || Proxy.isProxyClass(targetClass)) { - return new JdkDynamicAopProxy(config); - } - return new ObjenesisCglibAopProxy(config); - } - else { - return new JdkDynamicAopProxy(config); - } - } + @Override + public AopProxy createAopProxy(AdvisedSupport config) throws AopConfigException { + if (config.isOptimize() || config.isProxyTargetClass() || hasNoUserSuppliedProxyInterfaces(config)) { + Class targetClass = config.getTargetClass(); + if (targetClass == null) { + throw new AopConfigException("TargetSource cannot determine target class: " + + "Either an interface or a target is required for proxy creation."); + } + if (targetClass.isInterface() || Proxy.isProxyClass(targetClass)) { + return new JdkDynamicAopProxy(config); + } + return new ObjenesisCglibAopProxy(config); + } + else { + return new JdkDynamicAopProxy(config); + } + } ....... } ``` @@ -655,9 +688,9 @@ public class DefaultAopProxyFactory implements AopProxyFactory, Serializable { #### Spring AOP 自调用问题 -若同一类中的其他没有 `@Transactional` 注解的方法内部调用有 `@Transactional` 注解的方法,有`@Transactional` 注解的方法的事务会失效。 +当一个方法被标记了`@Transactional` 注解的时候,Spring 事务管理器只会在被其他类方法调用的时候生效,而不会在一个类中方法调用生效。 -这是由于`Spring AOP`代理的原因造成的,因为只有当 `@Transactional` 注解的方法在类以外被调用的时候,Spring 事务管理才生效。 +这是因为 Spring AOP 工作原理决定的。因为 Spring AOP 使用动态代理来实现事务的管理,它会在运行的时候为带有 `@Transactional` 注解的方法生成代理对象,并在方法调用的前后应用事物逻辑。如果该方法被其他类调用我们的代理对象就会拦截方法调用并处理事务。但是在一个类中的其他方法内部调用的时候,我们代理对象就无法拦截到这个内部调用,因此事务也就失效了。 `MyService` 类中的`method1()`调用`method2()`就会导致`method2()`的事务失效。 @@ -678,6 +711,25 @@ private void method1() { 解决办法就是避免同一类中自调用或者使用 AspectJ 取代 Spring AOP 代理。 +[issue #2091](https://github.com/Snailclimb/JavaGuide/issues/2091)补充了一个例子: + +```java +@Service +public class MyService { + +private void method1() { + ((MyService)AopContext.currentProxy()).method2(); // 先获取该类的代理对象,然后通过代理对象调用method2。 + //...... +} +@Transactional + public void method2() { + //...... + } +} +``` + +上面的代码确实可以在自调用的时候开启事务,但是这是因为使用了 `AopContext.currentProxy()` 方法来获取当前类的代理对象,然后通过代理对象调用 `method2()`。这样就相当于从外部调用了 `method2()`,所以事务注解才会生效。我们一般也不会在代码中这么写,所以可以忽略这个特殊的例子。 + #### `@Transactional` 的使用注意事项总结 - `@Transactional` 注解只有作用到 public 方法上事务才生效,不推荐在接口上使用; @@ -685,7 +737,7 @@ private void method1() { - 正确的设置 `@Transactional` 的 `rollbackFor` 和 `propagation` 属性,否则事务可能会回滚失败; - 被 `@Transactional` 注解的方法所在的类必须被 Spring 管理,否则不生效; - 底层使用的数据库必须支持事务机制,否则不生效; -- ...... +- …… ## 参考 @@ -696,3 +748,5 @@ private void method1() { - Spring 事务的传播特性:[https://github.com/love-somnus/Spring/wiki/Spring 事务的传播特性](https://github.com/love-somnus/Spring/wiki/Spring事务的传播特性) - [Spring 事务传播行为详解](https://segmentfault.com/a/1190000013341344):[https://segmentfault.com/a/1190000013341344](https://segmentfault.com/a/1190000013341344) - 全面分析 Spring 的编程式事务管理及声明式事务管理:[https://www.ibm.com/developerworks/cn/education/opensource/os-cn-spring-trans/index.html](https://www.ibm.com/developerworks/cn/education/opensource/os-cn-spring-trans/index.html) + + diff --git a/docs/system-design/framework/spring/springboot-knowledge-and-questions-summary.md b/docs/system-design/framework/spring/springboot-knowledge-and-questions-summary.md index f348b08ab25..7ced5db9da2 100644 --- a/docs/system-design/framework/spring/springboot-knowledge-and-questions-summary.md +++ b/docs/system-design/framework/spring/springboot-knowledge-and-questions-summary.md @@ -8,3 +8,5 @@ tag: **Spring Boot** 相关的面试题为我的[知识星球](https://javaguide.cn/about-the-author/zhishixingqiu-two-years.html)(点击链接即可查看详细介绍以及加入方法)专属内容,已经整理到了[《Java 面试指北》](https://javaguide.cn/zhuanlan/java-mian-shi-zhi-bei.html)中。 + + diff --git a/docs/system-design/framework/spring/springboot-source-code.md b/docs/system-design/framework/spring/springboot-source-code.md new file mode 100644 index 00000000000..d39f92c804a --- /dev/null +++ b/docs/system-design/framework/spring/springboot-source-code.md @@ -0,0 +1,14 @@ +--- +title: Spring Boot核心源码解读(付费) +category: 框架 +tag: + - Spring +--- + +**Spring Boot 核心源码解读** 为我的[知识星球](https://javaguide.cn/about-the-author/zhishixingqiu-two-years.html)(点击链接即可查看详细介绍以及加入方法)专属内容,已经整理到了[《Java 必读源码系列》](https://javaguide.cn/zhuanlan/source-code-reading.html)中。 + +![Spring Boot核心源码解读](https://oss.javaguide.cn/xingqiu/springboot-source-code.png) + + + + diff --git a/docs/system-design/schedule-task.md b/docs/system-design/schedule-task.md index 3ec51615482..99b3d574056 100644 --- a/docs/system-design/schedule-task.md +++ b/docs/system-design/schedule-task.md @@ -15,16 +15,21 @@ head: 我们来看一下几个非常常见的业务场景: -1. 某系统凌晨要进行数据备份。 +1. 某系统凌晨 1 点要进行数据备份。 2. 某电商平台,用户下单半个小时未支付的情况下需要自动取消订单。 3. 某媒体聚合平台,每 10 分钟动态抓取某某网站的数据为自己所用。 4. 某博客平台,支持定时发送文章。 5. 某基金平台,每晚定时计算用户当日收益情况并推送给用户最新的数据。 -6. ...... +6. …… -这些场景往往都要求我们在某个特定的时间去做某个事情。 +这些场景往往都要求我们在某个特定的时间去做某个事情,也就是定时或者延时去做某个事情。 -## 单机定时任务技术选型 +- 定时任务:在指定时间点执行特定的任务,例如每天早上 8 点,每周一下午 3 点等。定时任务可以用来做一些周期性的工作,如数据备份,日志清理,报表生成等。 +- 延时任务:一定的延迟时间后执行特定的任务,例如 10 分钟后,3 小时后等。延时任务可以用来做一些异步的工作,如订单取消,推送通知,红包撤回等。 + +尽管二者的适用场景有所区别,但它们的核心思想都是将任务的执行时间安排在未来的某个点上,以达到预期的调度效果。 + +## 单机定时任务 ### Timer @@ -113,6 +118,16 @@ executor.shutdown(); 不论是使用 `Timer` 还是 `ScheduledExecutorService` 都无法使用 Cron 表达式指定任务执行的具体时间。 +### DelayQueue + +`DelayQueue` 是 JUC 包(`java.util.concurrent)`为我们提供的延迟队列,用于实现延时任务比如订单下单 15 分钟未支付直接取消。它是 `BlockingQueue` 的一种,底层是一个基于 `PriorityQueue` 实现的一个无界队列,是线程安全的。关于`PriorityQueue`可以参考笔者编写的这篇文章:[PriorityQueue 源码分析](https://javaguide.cn/java/collection/priorityqueue-source-code.html) 。 + +![BlockingQueue 的实现类](https://oss.javaguide.cn/github/javaguide/java/collection/blocking-queue-hierarchy.png) + +`DelayQueue` 和 `Timer/TimerTask` 都可以用于实现定时任务调度,但是它们的实现方式不同。`DelayQueue` 是基于优先级队列和堆排序算法实现的,可以实现多个任务按照时间先后顺序执行;而 `Timer/TimerTask` 是基于单线程实现的,只能按照任务的执行顺序依次执行,如果某个任务执行时间过长,会影响其他任务的执行。另外,`DelayQueue` 还支持动态添加和移除任务,而 `Timer/TimerTask` 只能在创建时指定任务。 + +关于 `DelayQueue` 的详细介绍,请参考我写的这篇文章:[`DelayQueue` 源码分析](https://javaguide.cn/java/collection/delayqueue-source-code.html)。 + ### Spring Task 我们直接通过 Spring 提供的 `@Scheduled` 注解即可定义定时任务,非常方便! @@ -151,17 +166,17 @@ Kafka、Dubbo、ZooKeeper、Netty、Caffeine、Akka 中都有对时间轮的实 下图是一个有 12 个时间格的时间轮,转完一圈需要 12 s。当我们需要新建一个 3s 后执行的定时任务,只需要将定时任务放在下标为 3 的时间格中即可。当我们需要新建一个 9s 后执行的定时任务,只需要将定时任务放在下标为 9 的时间格中即可。 -![](https://oss.javaguide.cn/javaguide/20210607171334861.png) +![](https://oss.javaguide.cn/github/javaguide/system-design/schedule-task/one-layers-of-time-wheel.png) -那当我们需要创建一个 13s 后执行的定时任务怎么办呢?这个时候可以引入一叫做 **圈数/轮数** 的概念,也就是说这个任务还是放在下标为 3 的时间格中, 不过它的圈数为 2 。 +那当我们需要创建一个 13s 后执行的定时任务怎么办呢?这个时候可以引入一叫做 **圈数/轮数** 的概念,也就是说这个任务还是放在下标为 1 的时间格中, 不过它的圈数为 2 。 除了增加圈数这种方法之外,还有一种 **多层次时间轮** (类似手表),Kafka 采用的就是这种方案。 针对下图的时间轮,我来举一个例子便于大家理解。 -![](https://oss.javaguide.cn/javaguide/20210607193042151.png) +![](https://oss.javaguide.cn/github/javaguide/system-design/schedule-task/three-layers-of-time-wheel.png) -上图的时间轮,第 1 层的时间精度为 1 ,第 2 层的时间精度为 20 ,第 3 层的时间精度为 400。假如我们需要添加一个 350s 后执行的任务 A 的话(当前时间是 0s),这个任务会被放在第 2 层(因为第二层的时间跨度为 20\*20=400>350)的第 350/20=17 个时间格子。 +上图的时间轮(ms -> s),第 1 层的时间精度为 1 ,第 2 层的时间精度为 20 ,第 3 层的时间精度为 400。假如我们需要添加一个 350s 后执行的任务 A 的话(当前时间是 0s),这个任务会被放在第 2 层(因为第二层的时间跨度为 20\*20=400>350)的第 350/20=17 个时间格子。 当第一层转了 17 圈之后,时间过去了 340s ,第 2 层的指针此时来到第 17 个时间格子。此时,第 2 层第 17 个格子的任务会被移动到第 1 层。 @@ -169,17 +184,37 @@ Kafka、Dubbo、ZooKeeper、Netty、Caffeine、Akka 中都有对时间轮的实 这里在层与层之间的移动也叫做时间轮的升降级。参考手表来理解就好! -![](https://oscimg.oschina.net/oscnet/up-c9e9da713a7c05ae3187364deeded318fa9.png) - **时间轮比较适合任务数量比较多的定时任务场景,它的任务写入和执行的时间复杂度都是 0(1)。** -## 分布式定时任务技术选型 +## 分布式定时任务 + +### Redis + +Redis 是可以用来做延时任务的,基于 Redis 实现延时任务的功能无非就下面两种方案: + +1. Redis 过期事件监听 +2. Redisson 内置的延时队列 + +这部分内容的详细介绍我放在了[《后端面试高频系统设计&场景题》](https://javaguide.cn/zhuanlan/back-end-interview-high-frequency-system-design-and-scenario-questions.html)中,有需要的同学可以进入星球后阅读学习。篇幅太多,这里就不重复分享了。 -上面提到的一些定时任务的解决方案都是在单机下执行的,适用于比较简单的定时任务场景比如每天凌晨备份一次数据。 +![《后端面试高频系统设计&场景题》](https://oss.javaguide.cn/xingqiu/back-end-interview-high-frequency-system-design-and-scenario-questions-fengmian.png) + +### MQ + +大部分消息队列,例如 RocketMQ、RabbitMQ,都支持定时/延时消息。定时消息和延时消息本质其实是相同的,都是服务端根据消息设置的定时时间在某一固定时刻将消息投递给消费者消费。 + +不过,在使用 MQ 定时消息之前一定要看清楚其使用限制,以免不适合项目需求,例如 RocketMQ 定时时长最大值默认为 24 小时且不支持自定义修改、只支持 18 个 Level 的延时并不支持任意时间。 + +**优缺点总结:** + +- **优点**:可以与 Spring 集成、支持分布式、支持集群、性能不错 +- **缺点**:功能性较差、不灵活、需要保障消息可靠性 + +## 分布式任务调度框架 如果我们需要一些高级特性比如支持任务在分布式场景下的分片和高可用的话,我们就需要用到分布式任务调度框架了。 -通常情况下,一个定时任务的执行往往涉及到下面这些角色: +通常情况下,一个分布式定时任务的执行往往涉及到下面这些角色: - **任务**:首先肯定是要执行的任务,这个任务就是具体的业务逻辑比如定时发送文章。 - **调度器**:其次是调度中心,调度中心主要负责任务管理,会分配任务给执行器。 @@ -187,32 +222,41 @@ Kafka、Dubbo、ZooKeeper、Netty、Caffeine、Akka 中都有对时间轮的实 ### Quartz -一个很火的开源任务调度框架,完全由`Java`写成。`Quartz` 可以说是 Java 定时任务领域的老大哥或者说参考标准,其他的任务调度框架基本都是基于 `Quartz` 开发的,比如当当网的`elastic-job`就是基于`quartz`二次开发之后的分布式调度解决方案。 +一个很火的开源任务调度框架,完全由 Java 写成。Quartz 可以说是 Java 定时任务领域的老大哥或者说参考标准,其他的任务调度框架基本都是基于 Quartz 开发的,比如当当网的`elastic-job`就是基于 Quartz 二次开发之后的分布式调度解决方案。 -使用 `Quartz` 可以很方便地与 `Spring` 集成,并且支持动态添加任务和集群。但是,`Quartz` 使用起来也比较麻烦,API 繁琐。 +使用 Quartz 可以很方便地与 Spring 集成,并且支持动态添加任务和集群。但是,Quartz 使用起来也比较麻烦,API 繁琐。 -并且,`Quzrtz` 并没有内置 UI 管理控制台,不过你可以使用 [quartzui](https://github.com/zhaopeiym/quartzui) 这个开源项目来解决这个问题。 +并且,Quartz 并没有内置 UI 管理控制台,不过你可以使用 [quartzui](https://github.com/zhaopeiym/quartzui) 这个开源项目来解决这个问题。 -另外,`Quartz` 虽然也支持分布式任务。但是,它是在数据库层面,通过数据库的锁机制做的,有非常多的弊端比如系统侵入性严重、节点负载不均衡。有点伪分布式的味道。 +另外,Quartz 虽然也支持分布式任务。但是,它是在数据库层面,通过数据库的锁机制做的,有非常多的弊端比如系统侵入性严重、节点负载不均衡。有点伪分布式的味道。 **优缺点总结:** -- 优点:可以与 `Spring` 集成,并且支持动态添加任务和集群。 -- 缺点:分布式支持不友好,没有内置 UI 管理控制台、使用麻烦(相比于其他同类型框架来说) +- 优点:可以与 Spring 集成,并且支持动态添加任务和集群。 +- 缺点:分布式支持不友好,不支持任务可视化管理、使用麻烦(相比于其他同类型框架来说) ### Elastic-Job -`Elastic-Job` 是当当网开源的一个基于`Quartz`和`ZooKeeper`的分布式调度解决方案,由两个相互独立的子项目 `Elastic-Job-Lite` 和 `Elastic-Job-Cloud` 组成,一般我们只要使用 `Elastic-Job-Lite` 就好。 +ElasticJob 当当网开源的一个面向互联网生态和海量任务的分布式调度解决方案,由两个相互独立的子项目 ElasticJob-Lite 和 ElasticJob-Cloud 组成。 + +ElasticJob-Lite 和 ElasticJob-Cloud 两者的对比如下: + +| | ElasticJob-Lite | ElasticJob-Cloud | +| :------- | :-------------- | ----------------- | +| 无中心化 | 是 | 否 | +| 资源分配 | 不支持 | 支持 | +| 作业模式 | 常驻 | 常驻 + 瞬时 | +| 部署依赖 | ZooKeeper | ZooKeeper + Mesos | `ElasticJob` 支持任务在分布式场景下的分片和高可用、任务可视化管理等功能。 -![](https://oscimg.oschina.net/oscnet/up-0042738eb83d32773fd3bf391baaa6951d1.png) +![](https://oss.javaguide.cn/github/javaguide/system-design/schedule-task/elasticjob-feature-list.png) ElasticJob-Lite 的架构设计如下图所示: -![](https://oscimg.oschina.net/oscnet/up-a8f63f828666d43009d5d3497bcbd2cfb61.png) +![ElasticJob-Lite 的架构设计](https://oss.javaguide.cn/github/javaguide/system-design/schedule-task/elasticjob-lite-architecture-design.png) -从上图可以看出,`Elastic-Job` 没有调度中心这一概念,而是使用 `ZooKeeper` 作为注册中心,注册中心负责协调分配任务到不同的节点上。 +从上图可以看出,Elastic-Job 没有调度中心这一概念,而是使用 ZooKeeper 作为注册中心,注册中心负责协调分配任务到不同的节点上。 Elastic-Job 中的定时调度都是由执行器自行触发,这种设计也被称为去中心化设计(调度和处理都是执行器单独完成)。 @@ -236,26 +280,33 @@ public class TestJob implements SimpleJob { **优缺点总结:** -- 优点:可以与 `Spring` 集成、支持分布式、支持集群、性能不错 +- 优点:可以与 Spring 集成、支持分布式、支持集群、性能不错、支持任务可视化管理 - 缺点:依赖了额外的中间件比如 Zookeeper(复杂度增加,可靠性降低、维护成本变高) ### XXL-JOB `XXL-JOB` 于 2015 年开源,是一款优秀的轻量级分布式任务调度框架,支持任务可视化管理、弹性扩容缩容、任务失败重试和告警、任务分片等功能, -![](https://oscimg.oschina.net/oscnet/up-111a63288ee9057f754ca08e3c3ac86a295.png) +![](https://oss.javaguide.cn/github/javaguide/system-design/schedule-task/xxljob-feature-list.png) -根据 `XXL-JOB` 官网介绍,其解决了很多 `Quartz` 的不足。 +根据 `XXL-JOB` 官网介绍,其解决了很多 Quartz 的不足。 -![](https://oscimg.oschina.net/oscnet/up-57071c34a4c57c3ea6084a363c85d645c23.png) +> Quartz 作为开源作业调度中的佼佼者,是作业调度的首选。但是集群环境中 Quartz 采用 API 的方式对任务进行管理,从而可以避免上述问题,但是同样存在以下问题: +> +> - 问题一:调用 API 的的方式操作任务,不人性化; +> - 问题二:需要持久化业务 QuartzJobBean 到底层数据表中,系统侵入性相当严重。 +> - 问题三:调度逻辑和 QuartzJobBean 耦合在同一个项目中,这将导致一个问题,在调度任务数量逐渐增多,同时调度任务逻辑逐渐加重的情况下,此时调度系统的性能将大大受限于业务; +> - 问题四:quartz 底层以“抢占式”获取 DB 锁并由抢占成功节点负责运行任务,会导致节点负载悬殊非常大;而 XXL-JOB 通过执行器实现“协同分配式”运行任务,充分发挥集群优势,负载各节点均衡。 +> +> XXL-JOB 弥补了 quartz 的上述不足之处。 `XXL-JOB` 的架构设计如下图所示: -![](https://oscimg.oschina.net/oscnet/up-b8ecc6acf651f112c4dfae98243d72adea3.png) +![](https://oss.javaguide.cn/github/javaguide/system-design/schedule-task/xxljob-architecture-design-v2.1.0.png) 从上图可以看出,`XXL-JOB` 由 **调度中心** 和 **执行器** 两大部分组成。调度中心主要负责任务管理、执行器管理以及日志管理。执行器主要是接收调度信号并处理。另外,调度中心进行任务调度时,是通过自研 RPC 来实现的。 -不同于 `Elastic-Job` 的去中心化设计, `XXL-JOB` 的这种设计也被称为中心化设计(调度中心调度多个执行器执行任务)。 +不同于 Elastic-Job 的去中心化设计, `XXL-JOB` 的这种设计也被称为中心化设计(调度中心调度多个执行器执行任务)。 和 `Quzrtz` 类似 `XXL-JOB` 也是基于数据库锁调度任务,存在性能瓶颈。不过,一般在任务量不是特别大的情况下,没有什么影响的,可以满足绝大部分公司的要求。 @@ -284,7 +335,7 @@ public ReturnT myAnnotationJobHandler(String param) throws Exception { } ``` -![](https://oscimg.oschina.net/oscnet/up-07715bc445ef1db927bc6ec101dace5028e.png) +![](https://oss.javaguide.cn/github/javaguide/system-design/schedule-task/xxljob-admin-task-management.png) **相关地址:** @@ -293,7 +344,7 @@ public ReturnT myAnnotationJobHandler(String param) throws Exception { **优缺点总结:** -- 优点:开箱即用(学习成本比较低)、与 Spring 集成、支持分布式、支持集群、内置了 UI 管理控制台。 +- 优点:开箱即用(学习成本比较低)、与 Spring 集成、支持分布式、支持集群、支持任务可视化管理。 - 缺点:不支持动态添加任务(如果一定想要动态创建任务也是支持的,参见:[xxl-job issue277](https://github.com/xuxueli/xxl-job/issues/277))。 ### PowerJob @@ -306,15 +357,28 @@ public ReturnT myAnnotationJobHandler(String param) throws Exception { 由于 SchedulerX 属于人民币产品,我这里就不过多介绍。PowerJob 官方也对比过其和 QuartZ、XXL-JOB 以及 SchedulerX。 -![](https://oscimg.oschina.net/oscnet/up-795f5e9b0d875063717b1ee6a08f2ff1c01.png) +| | QuartZ | xxl-job | SchedulerX 2.0 | PowerJob | +| -------------- | ------------------------------------------- | ------------------------------------------ | ---------------------------------------------------- | --------------------------------------------------------------- | +| 定时类型 | CRON | CRON | CRON、固定频率、固定延迟、OpenAPI | **CRON、固定频率、固定延迟、OpenAPI** | +| 任务类型 | 内置 Java | 内置 Java、GLUE Java、Shell、Python 等脚本 | 内置 Java、外置 Java(FatJar)、Shell、Python 等脚本 | **内置 Java、外置 Java(容器)、Shell、Python 等脚本** | +| 分布式计算 | 无 | 静态分片 | MapReduce 动态分片 | **MapReduce 动态分片** | +| 在线任务治理 | 不支持 | 支持 | 支持 | **支持** | +| 日志白屏化 | 不支持 | 支持 | 不支持 | **支持** | +| 调度方式及性能 | 基于数据库锁,有性能瓶颈 | 基于数据库锁,有性能瓶颈 | 不详 | **无锁化设计,性能强劲无上限** | +| 报警监控 | 无 | 邮件 | 短信 | **WebHook、邮件、钉钉与自定义扩展** | +| 系统依赖 | JDBC 支持的关系型数据库(MySQL、Oracle...) | MySQL | 人民币 | **任意 Spring Data Jpa 支持的关系型数据库(MySQL、Oracle...)** | +| DAG 工作流 | 不支持 | 不支持 | 支持 | **支持** | + +## 定时任务方案总结 + +单机定时任务的常见解决方案有 `Timer`、`ScheduledExecutorService`、`DelayQueue`、Spring Task 和时间轮,其中最常用也是比较推荐使用的是时间轮。另外,这几种单机定时任务解决方案同样可以实现延时任务。 -## 总结 +Redis 和 MQ 虽然可以实现分布式定时任务,但这两者本身不是专门用来做分布式定时任务的,它们并不提供较为完整和强大的分布式定时任务的功能。而且,两者不太适合执行周期性的定时任务,因为它们只能保证消息被消费一次,而不能保证消息被消费多次。因此,它们更适合执行一次性的延时任务,例如订单取消、红包撤回。实际项目中,MQ 延时任务用的更多一些,可以降低业务之间的耦合度。 -这篇文章中,我主要介绍了: +Quartz、Elastic-Job、XXL-JOB 和 PowerJob 这几个是专门用来做分布式调度的框架,提供的分布式定时任务的功能更为完善和强大,更加适合执行周期性的定时任务。除了 Quartz 之外,另外三者都是支持任务可视化管理的。 -- **定时任务的相关概念**:为什么需要定时任务、定时任务中的核心角色、分布式定时任务。 -- **定时任务的技术选型**:XXL-JOB 2015 年推出,已经经过了很多年的考验。XXL-JOB 轻量级,并且使用起来非常简单。虽然存在性能瓶颈,但是,在绝大多数情况下,对于企业的基本需求来说是没有影响的。PowerJob 属于分布式任务调度领域里的新星,其稳定性还有待继续考察。ElasticJob 由于在架构设计上是基于 Zookeeper ,而 XXL-JOB 是基于数据库,性能方面的话,ElasticJob 略胜一筹。 +XXL-JOB 2015 年推出,已经经过了很多年的考验。XXL-JOB 轻量级,并且使用起来非常简单。虽然存在性能瓶颈,但是,在绝大多数情况下,对于企业的基本需求来说是没有影响的。PowerJob 属于分布式任务调度领域里的新星,其稳定性还有待继续考察。ElasticJob 由于在架构设计上是基于 Zookeeper ,而 XXL-JOB 是基于数据库,性能方面的话,ElasticJob 略胜一筹。 这篇文章并没有介绍到实际使用,但是,并不代表实际使用不重要。我在写这篇文章之前,已经动手写过相应的 Demo。像 Quartz,我在大学那会就用过。不过,当时用的是 Spring 。为了能够更好地体验,我自己又在 Spring Boot 上实际体验了一下。如果你并没有实际使用某个框架,就直接说它并不好用的话,是站不住脚的。 -最后,这篇文章要感谢艿艿的帮助,写这篇文章的时候向艿艿询问过一些问题。推荐一篇艿艿写的偏实战类型的硬核文章:[《Spring Job?Quartz?XXL-Job?年轻人才做选择,艿艿全莽~》](https://mp.weixin.qq.com/s?__biz=MzUzMTA2NTU2Ng==&mid=2247490679&idx=1&sn=25374dbdcca95311d41be5d7b7db454d&chksm=fa4963c6cd3eead055bb9cd10cca13224bb35d0f7373a27aa22a55495f71e24b8273a7603314&scene=27#wechat_redirect) 。 + diff --git a/docs/system-design/security/advantages-and-disadvantages-of-jwt.md b/docs/system-design/security/advantages-and-disadvantages-of-jwt.md index 13fbcce6dd9..bbb6d19778f 100644 --- a/docs/system-design/security/advantages-and-disadvantages-of-jwt.md +++ b/docs/system-design/security/advantages-and-disadvantages-of-jwt.md @@ -5,15 +5,11 @@ tag: - 安全 --- -在 [JWT 基本概念详解](https://javaguide.cn/system-design/security/jwt-intro.html)这篇文章中,我介绍了: +校招面试中,遇到大部分的候选者认证登录这块用的都是 JWT。提问 JWT 的概念性问题以及使用 JWT 的原因,基本都能回答一些,但当问到 JWT 存在的一些问题和解决方案时,只有一小部分候选者回答的还可以。 -- 什么是 JWT? -- JWT 由哪些部分组成? -- 如何基于 JWT 进行身份验证? -- JWT 如何防止 Token 被篡改? -- 如何加强 JWT 的安全性? +JWT 不是银弹,也有很多缺陷,很多时候并不是最优的选择。这篇文章,我们一起探讨一下 JWT 身份认证的优缺点以及常见问题的解决办法,来看看为什么很多人不再推荐使用 JWT 了。 -这篇文章,我们一起探讨一下 JWT 身份认证的优缺点以及常见问题的解决办法。 +关于 JWT 的基本概念介绍请看我写的这篇文章: [JWT 基本概念详解](https://javaguide.cn/system-design/security/jwt-intro.html)。 ## JWT 的优势 @@ -21,7 +17,7 @@ tag: ### 无状态 -JWT 自身包含了身份验证所需要的所有信息,因此,我们的服务器不需要存储 Session 信息。这显然增加了系统的可用性和伸缩性,大大减轻了服务端的压力。 +JWT 自身包含了身份验证所需要的所有信息,因此,我们的服务器不需要存储 JWT 信息。这显然增加了系统的可用性和伸缩性,大大减轻了服务端的压力。 不过,也正是由于 JWT 的无状态,也导致了它最大的缺点:**不可控!** @@ -84,6 +80,12 @@ public class XSSFilter implements Filter { 但是,使用 JWT 进行身份认证就不会存在这种问题,因为只要 JWT 可以被客户端存储就能够使用,而且 JWT 还可以跨语言使用。 +> 为什么使用 Session 进行身份认证的话不适合移动端 ? +> +> 1. 状态管理: Session 基于服务器端的状态管理,而移动端应用通常是无状态的。移动设备的连接可能不稳定或中断,因此难以维护长期的会话状态。如果使用 Session 进行身份认证,移动应用需要频繁地与服务器进行会话维护,增加了网络开销和复杂性; +> 2. 兼容性: 移动端应用通常会面向多个平台,如 iOS、Android 和 Web。每个平台对于 Session 的管理和存储方式可能不同,可能导致跨平台兼容性的问题; +> 3. 安全性: 移动设备通常处于不受信任的网络环境,存在数据泄露和攻击的风险。将敏感的会话信息存储在移动设备上增加了被攻击的潜在风险。 + ### 单点登录友好 使用 Session 进行身份认证的话,实现单点登录,需要我们把用户的 Session 信息保存在一台电脑上,并且还会遇到常见的 Cookie 跨域的问题。但是,使用 JWT 进行认证的话, JWT 被保存在客户端,不会存在这些问题。 @@ -100,15 +102,15 @@ public class XSSFilter implements Filter { - 用户的帐户被封禁/删除; - 用户被服务端强制注销; - 用户被踢下线; -- ...... +- …… 这个问题不存在于 Session 认证方式中,因为在 Session 认证方式中,遇到这种情况的话服务端删除对应的 Session 记录即可。但是,使用 JWT 认证的方式就不好解决了。我们也说过了,JWT 一旦派发出去,如果后端不增加其他逻辑的话,它在失效之前都是有效的。 那我们如何解决这个问题呢?查阅了很多资料,我简单总结了下面 4 种方案: -**1、将 JWT 存入内存数据库** +**1、将 JWT 存入数据库** -将 JWT 存入 DB 中,Redis 内存数据库在这里是不错的选择。如果需要让某个 JWT 失效就直接从 Redis 中删除这个 JWT 即可。但是,这样会导致每次使用 JWT 发送请求都要先从 DB 中查询 JWT 是否存在的步骤,而且违背了 JWT 的无状态原则。 +将有效的 JWT 存入数据库中,更建议使用内存数据库比如 Redis。如果需要让某个 JWT 失效就直接从 Redis 中删除这个 JWT 即可。但是,这样会导致每次使用 JWT 都要先从 Redis 中查询 JWT 是否存在的步骤,而且违背了 JWT 的无状态原则。 **2、黑名单机制** @@ -139,38 +141,60 @@ JWT 有效期一般都建议设置的不太长,那么 JWT 过期后如何认 JWT 认证的话,我们应该如何解决续签问题呢?查阅了很多资料,我简单总结了下面 4 种方案: -**1、类似于 Session 认证中的做法** +**1、类似于 Session 认证中的做法(不推荐)** -这种方案满足于大部分场景。假设服务端给的 JWT 有效期设置为 30 分钟,服务端每次进行校验时,如果发现 JWT 的有效期马上快过期了,服务端就重新生成 JWT 给客户端。客户端每次请求都检查新旧 JWT,如果不一致,则更新本地的 JWT。这种做法的问题是仅仅在快过期的时候请求才会更新 JWT ,对客户端不是很友好。 +这种方案满足于大部分场景。假设服务端给的 JWT 有效期设置为 30 分钟,服务端每次进行校验时,如果发现 JWT 的有效期马上快过期了,服务端就重新生成 JWT 给客户端。客户端每次请求都检查新旧 JWT,如果不一致,则更新本地的 JWT。这种做法的问题是仅仅在快过期的时候请求才会更新 JWT ,对客户端不是很友好。 -**2、每次请求都返回新 JWT** +**2、每次请求都返回新 JWT(不推荐)** 这种方案的的思路很简单,但是,开销会比较大,尤其是在服务端要存储维护 JWT 的情况下。 -**3、JWT 有效期设置到半夜** +**3、JWT 有效期设置到半夜(不推荐)** 这种方案是一种折衷的方案,保证了大部分用户白天可以正常登录,适用于对安全性要求不高的系统。 -**4、用户登录返回两个 JWT** +**4、用户登录返回两个 JWT(推荐)** -第一个是 accessJWT ,它的过期时间 JWT 本身的过期时间比如半个小时,另外一个是 refreshJWT 它的过期时间更长一点比如为 1 天。客户端登录后,将 accessJWT 和 refreshJWT 保存在本地,每次访问将 accessJWT 传给服务端。服务端校验 accessJWT 的有效性,如果过期的话,就将 refreshJWT 传给服务端。如果有效,服务端就生成新的 accessJWT 给客户端。否则,客户端就重新登录即可。 +第一个是 accessJWT ,它的过期时间 JWT 本身的过期时间比如半个小时,另外一个是 refreshJWT 它的过期时间更长一点比如为 1 天。refreshJWT 只用来获取 accessJWT,不容易被泄露。 + +客户端登录后,将 accessJWT 和 refreshJWT 保存在本地,每次访问将 accessJWT 传给服务端。服务端校验 accessJWT 的有效性,如果过期的话,就将 refreshJWT 传给服务端。如果有效,服务端就生成新的 accessJWT 给客户端。否则,客户端就重新登录即可。 这种方案的不足是: - 需要客户端来配合; - 用户注销的时候需要同时保证两个 JWT 都无效; - 重新请求获取 JWT 的过程中会有短暂 JWT 不可用的情况(可以通过在客户端设置定时器,当 accessJWT 快过期的时候,提前去通过 refreshJWT 获取新的 accessJWT); -- 存在安全问题,只要拿到了未过期的 refreshJWT 就一直可以获取到 accessJWT。 +- 存在安全问题,只要拿到了未过期的 refreshJWT 就一直可以获取到 accessJWT。不过,由于 refreshJWT 只用来获取 accessJWT,不容易被泄露。 + +### JWT 体积太大 + +JWT 结构复杂(Header、Payload 和 Signature),包含了更多额外的信息,还需要进行 Base64Url 编码,这会使得 JWT 体积较大,增加了网络传输的开销。 + +JWT 组成: + +![JWT 组成](https://oss.javaguide.cn/javaguide/system-design/jwt/jwt-composition.png) + +JWT 示例: + +```plain +eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9. +eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkpvaG4gRG9lIiwiaWF0IjoxNTE2MjM5MDIyfQ. +SflKxwRJSMeKKF2QT4fwpMeJf36POk6yJV_adQssw5c +``` + +解决办法: + +- 尽量减少 JWT Payload(载荷)中的信息,只保留必要的用户和权限信息。 +- 在传输 JWT 之前,使用压缩算法(如 GZIP)对 JWT 进行压缩以减少体积。 +- 在某些情况下,使用传统的 Token 可能更合适。传统的 Token 通常只是一个唯一标识符,对应的信息(例如用户 ID、Token 过期时间、权限信息)存储在服务端,通常会通过 Redis 保存。 ## 总结 -JWT 其中一个很重要的优势是无状态,但实际上,我们想要在实际项目中合理使用 JWT 的话,也还是需要保存 JWT 信息。 +JWT 其中一个很重要的优势是无状态,但实际上,我们想要在实际项目中合理使用 JWT 做认证登录的话,也还是需要保存 JWT 信息。 JWT 也不是银弹,也有很多缺陷,具体是选择 JWT 还是 Session 方案还是要看项目的具体需求。万万不可尬吹 JWT,而看不起其他身份认证方案。 -另外,不用 JWT 直接使用普通的 Token(随机生成,不包含具体的信息) 结合 Redis 来做身份认证也是可以的。我在 [「优质开源项目推荐」](https://javaguide.cn/open-source-project/)的第 8 期推荐过的 [Sa-Token](https://github.com/dromara/sa-JWT) 这个项目是一个比较完善的 基于 JWT 的身份认证解决方案,支持自动续签、踢人下线、账号封禁、同端互斥登录等功能,感兴趣的朋友可以看看。 - -![](https://oss.javaguide.cn/javaguide/system-design/jwt/image-20220609170714725.png) +另外,不用 JWT 直接使用普通的 Token(随机生成的 ID,不包含具体的信息) 结合 Redis 来做身份认证也是可以的。 ## 参考 @@ -178,3 +202,5 @@ JWT 也不是银弹,也有很多缺陷,具体是选择 JWT 还是 Session - How to log out when using JWT: - CSRF protection with JSON Web JWTs: - Invalidating JSON Web JWTs: + + diff --git a/docs/system-design/security/basis-of-authority-certification.md b/docs/system-design/security/basis-of-authority-certification.md index cb520675b65..c21f80568d7 100644 --- a/docs/system-design/security/basis-of-authority-certification.md +++ b/docs/system-design/security/basis-of-authority-certification.md @@ -33,17 +33,15 @@ tag: 系统权限控制最常采用的访问控制模型就是 **RBAC 模型** 。 -**什么是 RBAC 呢?** +**什么是 RBAC 呢?** RBAC 即基于角色的权限访问控制(Role-Based Access Control)。这是一种通过角色关联权限,角色同时又关联用户的授权的方式。 -RBAC 即基于角色的权限访问控制(Role-Based Access Control)。这是一种通过角色关联权限,角色同时又关联用户的授权的方式。 - -简单地说:一个用户可以拥有若干角色,每一个角色又可以被分配若干权限,这样就构造成“用户-角色-权限” 的授权模型。在这种模型中,用户与角色、角色与权限之间构成了多对多的关系,如下图 +简单地说:一个用户可以拥有若干角色,每一个角色又可以被分配若干权限,这样就构造成“用户-角色-权限” 的授权模型。在这种模型中,用户与角色、角色与权限之间构成了多对多的关系。 ![RBAC 权限模型示意图](https://oss.javaguide.cn/github/javaguide/system-design/security/design-of-authority-system/rbac.png) -**在 RBAC 中,权限与角色相关联,用户通过成为适当角色的成员而得到这些角色的权限。这就极大地简化了权限的管理。** +在 RBAC 权限模型中,权限与角色相关联,用户通过成为包含特定角色的成员而得到这些角色的权限,这就极大地简化了权限的管理。 -本系统的权限设计相关的表如下(一共 5 张表,2 张用户建立表之间的联系): +为了实现 RBAC 权限模型,数据库表的常见设计如下(一共 5 张表,2 张用户建立表之间的联系): ![](https://oss.javaguide.cn/2020-11/%E6%95%B0%E6%8D%AE%E5%BA%93%E8%AE%BE%E8%AE%A1-%E6%9D%83%E9%99%90.png) @@ -70,7 +68,7 @@ RBAC 即基于角色的权限访问控制(Role-Based Access Control)。这 1. 我们在 `Cookie` 中保存已经登录过的用户信息,下次访问网站的时候页面可以自动帮你登录的一些基本信息给填了。除此之外,`Cookie` 还能保存用户首选项,主题和其他设置信息。 2. 使用 `Cookie` 保存 `SessionId` 或者 `Token` ,向后端发送请求的时候带上 `Cookie`,这样后端就能取到 `Session` 或者 `Token` 了。这样就能记录用户当前的状态了,因为 HTTP 协议是无状态的。 3. `Cookie` 还可以用来记录和分析用户行为。举个简单的例子你在网上购物的时候,因为 HTTP 协议是没有状态的,如果服务器想要获取你在某个页面的停留状态或者看了哪些商品,一种常用的实现方式就是将这些信息存放在 `Cookie` -4. ...... +4. …… ## 如何在项目中使用 Cookie 呢? @@ -168,6 +166,7 @@ Session-Cookie 方案在单体环境是一个非常好的身份认证方案。 1. 某个用户的所有请求都通过特性的哈希策略分配给同一个服务器处理。这样的话,每个服务器都保存了一部分用户的 Session 信息。服务器宕机,其保存的所有 Session 信息就完全丢失了。 2. 每一个服务器保存的 Session 信息都是互相同步的,也就是说每一个服务器都保存了全量的 Session 信息。每当一个服务器的 Session 信息发生变化,我们就将其同步到其他服务器。这种方案成本太大,并且,节点越多时,同步成本也越高。 3. 单独使用一个所有服务器都能访问到的数据节点(比如缓存)来存放 Session 信息。为了保证高可用,数据节点尽量要避免是单点。 +4. Spring Session 是一个用于在多个服务器之间管理会话的项目。它可以与多种后端存储(如 Redis、MongoDB 等)集成,从而实现分布式会话管理。通过 Spring Session,可以将会话数据存储在共享的外部存储中,以实现跨服务器的会话同步和共享。 ## 如果没有 Cookie 的话 Session 还能用吗? @@ -175,11 +174,11 @@ Session-Cookie 方案在单体环境是一个非常好的身份认证方案。 一般是通过 `Cookie` 来保存 `SessionID` ,假如你使用了 `Cookie` 保存 `SessionID` 的方案的话, 如果客户端禁用了 `Cookie`,那么 `Session` 就无法正常工作。 -但是,并不是没有 `Cookie` 之后就不能用 `Session` 了,比如你可以将 `SessionID` 放在请求的 `url` 里面`https://javaguide.cn/?Session_id=xxx` 。这种方案的话可行,但是安全性和用户体验感降低。当然,为了你也可以对 `SessionID` 进行一次加密之后再传入后端。 +但是,并不是没有 `Cookie` 之后就不能用 `Session` 了,比如你可以将 `SessionID` 放在请求的 `url` 里面`https://javaguide.cn/?Session_id=xxx` 。这种方案的话可行,但是安全性和用户体验感降低。当然,为了安全你也可以对 `SessionID` 进行一次加密之后再传入后端。 ## 为什么 Cookie 无法防止 CSRF 攻击,而 Token 可以? -**CSRF(Cross Site Request Forgery)** 一般被翻译为 **跨站请求伪造** 。那么什么是 **跨站请求伪造** 呢?说简单用你的身份去发送一些对你不友好的请求。举个简单的例子: +**CSRF(Cross Site Request Forgery)** 一般被翻译为 **跨站请求伪造** 。那么什么是 **跨站请求伪造** 呢?说简单点,就是用你的身份去发送一些对你不友好的请求。举个简单的例子: 小壮登录了某网上银行,他来到了网上银行的帖子区,看到一个帖子下面有一个链接写着“科学理财,年盈利率过万”,小壮好奇的点开了这个链接,结果发现自己的账户少了 10000 元。这是这么回事呢?原来黑客在链接中藏了一个请求,这个请求直接利用小壮的身份给银行发送了一个转账请求,也就是通过你的 Cookie 向银行发出请求。 @@ -191,7 +190,7 @@ Session-Cookie 方案在单体环境是一个非常好的身份认证方案。 `Session` 认证中 `Cookie` 中的 `SessionId` 是由浏览器发送到服务端的,借助这个特性,攻击者就可以通过让用户误点攻击链接,达到攻击效果。 -但是,我们使用 `Token` 的话就不会存在这个问题,在我们登录成功获得 `Token` 之后,一般会选择存放在 `localStorage` (浏览器本地存储)中。然后我们在前端通过某些方式会给每个发到后端的请求加上这个 `Token`,这样就不会出现 CSRF 漏洞的问题。因为,即使有个你点击了非法链接发送了请求到服务端,这个非法请求是不会携带 `Token` 的,所以这个请求将是非法的。 +但是,我们使用 `Token` 的话就不会存在这个问题,在我们登录成功获得 `Token` 之后,一般会选择存放在 `localStorage` (浏览器本地存储)中。然后我们在前端通过某些方式会给每个发到后端的请求加上这个 `Token`,这样就不会出现 CSRF 漏洞的问题。因为,即使你点击了非法链接发送了请求到服务端,这个非法请求是不会携带 `Token` 的,所以这个请求将是非法的。 ![](https://oss.javaguide.cn/github/javaguide/system-design/security/20210615161108272.png) @@ -250,6 +249,8 @@ OAuth 2.0 比较常用的场景就是第三方登录,当你的网站接入了 ## 参考 -- 不要用 JWT 替代 session 管理(上):全面了解 Token,JWT,OAuth,SAML,SSO:https://zhuanlan.zhihu.com/p/38942172 -- Introduction to JSON Web Tokens:https://jwt.io/introduction -- JSON Web Token Claims:https://auth0.com/docs/secure/tokens/json-web-tokens/json-web-token-claims +- 不要用 JWT 替代 session 管理(上):全面了解 Token,JWT,OAuth,SAML,SSO: +- Introduction to JSON Web Tokens: +- JSON Web Token Claims: + + diff --git a/docs/system-design/security/data-desensitization.md b/docs/system-design/security/data-desensitization.md index 082aab97202..08b5052f268 100644 --- a/docs/system-design/security/data-desensitization.md +++ b/docs/system-design/security/data-desensitization.md @@ -1,8 +1,531 @@ --- -title: 数据脱敏 +title: 数据脱敏方案总结 category: 系统设计 tag: - 安全 --- -数据脱敏说的就是我们根据特定的规则对敏感信息数据进行变形,比如我们把手机号、身份证号某些位数使用 \* 来代替。 + + +> 本文转载完善自[Hutool:一行代码搞定数据脱敏 - 京东云开发者](https://mp.weixin.qq.com/s/1qFWczesU50ndPPLtABHFg)。 + +## 什么是数据脱敏 + +### 数据脱敏的定义 + +数据脱敏百度百科中是这样定义的: + +> 数据脱敏,指对某些敏感信息通过脱敏规则进行数据的变形,实现敏感隐私数据的可靠保护。这样就可以在开发、测试和其它非生产环境以及外包环境中安全地使用脱敏后的真实数据集。在涉及客户安全数据或者一些商业性敏感数据的情况下,在不违反系统规则条件下,对真实数据进行改造并提供测试使用,如身份证号、手机号、卡号、客户号等个人信息都需要进行数据脱敏。是数据库安全技术之一。 + +总的来说,数据脱敏是指对某些敏感信息通过脱敏规则进行数据的变形,实现敏感隐私数据的可靠保护。 + +在数据脱敏过程中,通常会采用不同的算法和技术,以根据不同的需求和场景对数据进行处理。例如,对于身份证号码,可以使用掩码算法(masking)将前几位数字保留,其他位用 “X” 或 "\*" 代替;对于姓名,可以使用伪造(pseudonymization)算法,将真实姓名替换成随机生成的假名。 + +### 常用脱敏规则 + +常用脱敏规则是为了保护敏感数据的安全性,在处理和存储敏感数据时对其进行变换或修改。 + +下面是几种常见的脱敏规则: + +- 替换(常用):将敏感数据中的特定字符或字符序列替换为其他字符。例如,将信用卡号中的中间几位数字替换为星号(\*)或其他字符。 +- 删除:将敏感数据中的部分内容随机删除。比如,将电话号码的随机 3 位数字进行删除。 +- 重排:将原始数据中的某些字符或字段的顺序打乱。例如,将身份证号码的随机位交错互换。 +- 加噪:在数据中注入一些误差或者噪音,达到对数据脱敏的效果。例如,在敏感数据中添加一些随机生成的字符。 +- 加密(常用):使用加密算法将敏感数据转换为密文。例如,将银行卡号用 MD5 或 SHA-256 等哈希函数进行散列。常见加密算法总结可以参考这篇文章: 。 +- …… + +## 常用脱敏工具 + +### Hutool + +Hutool 一个 Java 基础工具类,对文件、流、加密解密、转码、正则、线程、XML 等 JDK 方法进行封装,组成各种 Util 工具类,同时提供以下组件: + +| 模块 | 介绍 | +| :----------------: | :---------------------------------------------------------------------------: | +| hutool-aop | JDK 动态代理封装,提供非 IOC 下的切面支持 | +| hutool-bloomFilter | 布隆过滤,提供一些 Hash 算法的布隆过滤 | +| hutool-cache | 简单缓存实现 | +| hutool-core | 核心,包括 Bean 操作、日期、各种 Util 等 | +| hutool-cron | 定时任务模块,提供类 Crontab 表达式的定时任务 | +| hutool-crypto | 加密解密模块,提供对称、非对称和摘要算法封装 | +| hutool-db | JDBC 封装后的数据操作,基于 ActiveRecord 思想 | +| hutool-dfa | 基于 DFA 模型的多关键字查找 | +| hutool-extra | 扩展模块,对第三方封装(模板引擎、邮件、Servlet、二维码、Emoji、FTP、分词等) | +| hutool-http | 基于 HttpUrlConnection 的 Http 客户端封装 | +| hutool-log | 自动识别日志实现的日志门面 | +| hutool-script | 脚本执行封装,例如 Javascript | +| hutool-setting | 功能更强大的 Setting 配置文件和 Properties 封装 | +| hutool-system | 系统参数调用封装(JVM 信息等) | +| hutool-json | JSON 实现 | +| hutool-captcha | 图片验证码实现 | +| hutool-poi | 针对 POI 中 Excel 和 Word 的封装 | +| hutool-socket | 基于 Java 的 NIO 和 AIO 的 Socket 封装 | +| hutool-jwt | JSON Web Token (JWT) 封装实现 | + +可以根据需求对每个模块单独引入,也可以通过引入`hutool-all`方式引入所有模块,本文所使用的数据脱敏工具就是在 `hutool.core` 模块。 + +现阶段最新版本的 Hutool 支持的脱敏数据类型如下,基本覆盖了常见的敏感信息。 + +1. 用户 id +2. 中文姓名 +3. 身份证号 +4. 座机号 +5. 手机号 +6. 地址 +7. 电子邮件 +8. 密码 +9. 中国大陆车牌,包含普通车辆、新能源车辆 +10. 银行卡 + +#### 一行代码实现脱敏 + +Hutool 提供的脱敏方法如下图所示: + +![](https://oss.javaguide.cn/github/javaguide/system-design/security/2023-08-01-10-2119fnVCIDozqHgRGx.png) + +注意:Hutool 脱敏是通过 \* 来代替敏感信息的,具体实现是在 StrUtil.hide 方法中,如果我们想要自定义隐藏符号,则可以把 Hutool 的源码拷出来,重新实现即可。 + +这里以手机号、银行卡号、身份证号、密码信息的脱敏为例,下面是对应的测试代码。 + +```java +import cn.hutool.core.util.DesensitizedUtil; +import org.junit.Test; +import org.springframework.boot.test.context.Spring BootTest; + +/** + * + * @description: Hutool实现数据脱敏 + */ +@Spring BootTest +public class HuToolDesensitizationTest { + + @Test + public void testPhoneDesensitization(){ + String phone="13723231234"; + System.out.println(DesensitizedUtil.mobilePhone(phone)); //输出:137****1234 + } + @Test + public void testBankCardDesensitization(){ + String bankCard="6217000130008255666"; + System.out.println(DesensitizedUtil.bankCard(bankCard)); //输出:6217 **** **** *** 5666 + } + + @Test + public void testIdCardNumDesensitization(){ + String idCardNum="411021199901102321"; + //只显示前4位和后2位 + System.out.println(DesensitizedUtil.idCardNum(idCardNum,4,2)); //输出:4110************21 + } + @Test + public void testPasswordDesensitization(){ + String password="www.jd.com_35711"; + System.out.println(DesensitizedUtil.password(password)); //输出:**************** + } +} +``` + +以上就是使用 Hutool 封装好的工具类实现数据脱敏。 + +#### 配合 JackSon 通过注解方式实现脱敏 + +现在有了数据脱敏工具类,如果前端需要显示数据数据的地方比较多,我们不可能在每个地方都调用一个工具类,这样就显得代码太冗余了,那我们如何通过注解的方式优雅的完成数据脱敏呢? + +如果项目是基于 Spring Boot 的 web 项目,则可以利用 Spring Boot 自带的 jackson 自定义序列化实现。它的实现原理其实就是在 json 进行序列化渲染给前端时,进行脱敏。 + +**第一步:脱敏策略的枚举。** + +```java +/** + * @author + * @description:脱敏策略枚举 + */ +public enum DesensitizationTypeEnum { + //自定义 + MY_RULE, + //用户id + USER_ID, + //中文名 + CHINESE_NAME, + //身份证号 + ID_CARD, + //座机号 + FIXED_PHONE, + //手机号 + MOBILE_PHONE, + //地址 + ADDRESS, + //电子邮件 + EMAIL, + //密码 + PASSWORD, + //中国大陆车牌,包含普通车辆、新能源车辆 + CAR_LICENSE, + //银行卡 + BANK_CARD +} +``` + +上面表示支持的脱敏类型。 + +**第二步:定义一个用于脱敏的 Desensitization 注解。** + +- `@Retention (RetentionPolicy.RUNTIME)`:运行时生效。 +- `@Target (ElementType.FIELD)`:可用在字段上。 +- `@JacksonAnnotationsInside`:此注解可以点进去看一下是一个元注解,主要是用户打包其他注解一起使用。 +- `@JsonSerialize`:上面说到过,该注解的作用就是可自定义序列化,可以用在注解上,方法上,字段上,类上,运行时生效等等,根据提供的序列化类里面的重写方法实现自定义序列化。 + +```java +/** + * @author + */ +@Target(ElementType.FIELD) +@Retention(RetentionPolicy.RUNTIME) +@JacksonAnnotationsInside +@JsonSerialize(using = DesensitizationSerialize.class) +public @interface Desensitization { + /** + * 脱敏数据类型,在MY_RULE的时候,startInclude和endExclude生效 + */ + DesensitizationTypeEnum type() default DesensitizationTypeEnum.MY_RULE; + + /** + * 脱敏开始位置(包含) + */ + int startInclude() default 0; + + /** + * 脱敏结束位置(不包含) + */ + int endExclude() default 0; +} +``` + +注:只有使用了自定义的脱敏枚举 `MY_RULE` 的时候,开始位置和结束位置才生效。 + +**第三步:创建自定的序列化类** + +这一步是我们实现数据脱敏的关键。自定义序列化类继承 `JsonSerializer`,实现 `ContextualSerializer` 接口,并重写两个方法。 + +```java +/** + * @author + * @description: 自定义序列化类 + */ +@AllArgsConstructor +@NoArgsConstructor +public class DesensitizationSerialize extends JsonSerializer implements ContextualSerializer { + private DesensitizationTypeEnum type; + + private Integer startInclude; + + private Integer endExclude; + + @Override + public void serialize(String str, JsonGenerator jsonGenerator, SerializerProvider serializerProvider) throws IOException { + switch (type) { + // 自定义类型脱敏 + case MY_RULE: + jsonGenerator.writeString(CharSequenceUtil.hide(str, startInclude, endExclude)); + break; + // userId脱敏 + case USER_ID: + jsonGenerator.writeString(String.valueOf(DesensitizedUtil.userId())); + break; + // 中文姓名脱敏 + case CHINESE_NAME: + jsonGenerator.writeString(DesensitizedUtil.chineseName(String.valueOf(str))); + break; + // 身份证脱敏 + case ID_CARD: + jsonGenerator.writeString(DesensitizedUtil.idCardNum(String.valueOf(str), 1, 2)); + break; + // 固定电话脱敏 + case FIXED_PHONE: + jsonGenerator.writeString(DesensitizedUtil.fixedPhone(String.valueOf(str))); + break; + // 手机号脱敏 + case MOBILE_PHONE: + jsonGenerator.writeString(DesensitizedUtil.mobilePhone(String.valueOf(str))); + break; + // 地址脱敏 + case ADDRESS: + jsonGenerator.writeString(DesensitizedUtil.address(String.valueOf(str), 8)); + break; + // 邮箱脱敏 + case EMAIL: + jsonGenerator.writeString(DesensitizedUtil.email(String.valueOf(str))); + break; + // 密码脱敏 + case PASSWORD: + jsonGenerator.writeString(DesensitizedUtil.password(String.valueOf(str))); + break; + // 中国车牌脱敏 + case CAR_LICENSE: + jsonGenerator.writeString(DesensitizedUtil.carLicense(String.valueOf(str))); + break; + // 银行卡脱敏 + case BANK_CARD: + jsonGenerator.writeString(DesensitizedUtil.bankCard(String.valueOf(str))); + break; + default: + } + + } + + @Override + public JsonSerializer createContextual(SerializerProvider serializerProvider, BeanProperty beanProperty) throws JsonMappingException { + if (beanProperty != null) { + // 判断数据类型是否为String类型 + if (Objects.equals(beanProperty.getType().getRawClass(), String.class)) { + // 获取定义的注解 + Desensitization desensitization = beanProperty.getAnnotation(Desensitization.class); + // 为null + if (desensitization == null) { + desensitization = beanProperty.getContextAnnotation(Desensitization.class); + } + // 不为null + if (desensitization != null) { + // 创建定义的序列化类的实例并且返回,入参为注解定义的type,开始位置,结束位置。 + return new DesensitizationSerialize(desensitization.type(), desensitization.startInclude(), + desensitization.endExclude()); + } + } + + return serializerProvider.findValueSerializer(beanProperty.getType(), beanProperty); + } + return serializerProvider.findNullValueSerializer(null); + } +} +``` + +经过上述三步,已经完成了通过注解实现数据脱敏了,下面我们来测试一下。 + +首先定义一个要测试的 pojo,对应的字段加入要脱敏的策略。 + +```java +/** + * + * @description: + */ +@Data +@NoArgsConstructor +@AllArgsConstructor +public class TestPojo { + + private String userName; + + @Desensitization(type = DesensitizationTypeEnum.MOBILE_PHONE) + private String phone; + + @Desensitization(type = DesensitizationTypeEnum.PASSWORD) + private String password; + + @Desensitization(type = DesensitizationTypeEnum.MY_RULE, startInclude = 0, endExclude = 2) + private String address; +} +``` + +接下来写一个测试的 controller + +```java +@RestController +public class TestController { + + @RequestMapping("/test") + public TestPojo testDesensitization(){ + TestPojo testPojo = new TestPojo(); + testPojo.setUserName("我是用户名"); + testPojo.setAddress("地球中国-北京市通州区京东总部2号楼"); + testPojo.setPhone("13782946666"); + testPojo.setPassword("sunyangwei123123123."); + System.out.println(testPojo); + return testPojo; + } + +} +``` + +![](https://oss.javaguide.cn/github/javaguide/system-design/security/2023-08-02-16-497DdCBy8vbf2D69g.png) + +可以看到我们成功实现了数据脱敏。 + +### Apache ShardingSphere + +ShardingSphere 是一套开源的分布式数据库中间件解决方案组成的生态圈,它由 Sharding-JDBC、Sharding-Proxy 和 Sharding-Sidecar(计划中)这 3 款相互独立的产品组成。 他们均提供标准化的数据分片、分布式事务和数据库治理功能 。 + +Apache ShardingSphere 下面存在一个数据脱敏模块,此模块集成的常用的数据脱敏的功能。其基本原理是对用户输入的 SQL 进行解析拦截,并依靠用户的脱敏配置进行 SQL 的改写,从而实现对原文字段的加密及加密字段的解密。最终实现对用户无感的加解密存储、查询。 + +通过 Apache ShardingSphere 可以自动化&透明化数据脱敏过程,用户无需关注脱敏中间实现细节。并且,提供了多种内置、第三方(AKS)的脱敏策略,用户仅需简单配置即可使用。 + +官方文档地址: 。 + +### FastJSON + +平时开发 Web 项目的时候,除了默认的 Spring 自带的序列化工具,FastJson 也是一个很常用的 Spring Web Restful 接口序列化的工具。 + +FastJSON 实现数据脱敏的方式主要有两种: + +- 基于注解 `@JSONField` 实现:需要自定义一个用于脱敏的序列化的类,然后在需要脱敏的字段上通过 `@JSONField` 中的 `serializeUsing` 指定为我们自定义的序列化类型即可。 +- 基于序列化过滤器:需要实现 `ValueFilter` 接口,重写 `process` 方法完成自定义脱敏,然后在 JSON 转换时使用自定义的转换策略。具体实现可参考这篇文章: 。 + +### Mybatis-Mate + +先介绍一下 MyBatis、MyBatis-Plus 和 Mybatis-Mate 这三者的关系: + +- MyBatis 是一款优秀的持久层框架,它支持定制化 SQL、存储过程以及高级映射。 +- MyBatis-Plus 是一个 MyBatis 的增强工具,能够极大地简化持久层的开发工作。 +- Mybatis-Mate 是为 MyBatis-Plus 提供的企业级模块,旨在更敏捷优雅处理数据。不过,使用之前需要配置授权码(付费)。 + +Mybatis-Mate 支持敏感词脱敏,内置手机号、邮箱、银行卡号等 9 种常用脱敏规则。 + +```java +@FieldSensitive("testStrategy") +private String username; + +@Configuration +public class SensitiveStrategyConfig { + + /** + * 注入脱敏策略 + */ + @Bean + public ISensitiveStrategy sensitiveStrategy() { + // 自定义 testStrategy 类型脱敏处理 + return new SensitiveStrategy().addStrategy("testStrategy", t -> t + "***test***"); + } +} + +// 跳过脱密处理,用于编辑场景 +RequestDataTransfer.skipSensitive(); +``` + +### MyBatis-Flex + +类似于 MybatisPlus,MyBatis-Flex 也是一个 MyBatis 增强框架。MyBatis-Flex 同样提供了数据脱敏功能,并且是可以免费使用的。 + +MyBatis-Flex 提供了 `@ColumnMask()` 注解,以及内置的 9 种脱敏规则,开箱即用: + +```java +/** + * 内置的数据脱敏方式 + */ +public class Masks { + /** + * 手机号脱敏 + */ + public static final String MOBILE = "mobile"; + /** + * 固定电话脱敏 + */ + public static final String FIXED_PHONE = "fixed_phone"; + /** + * 身份证号脱敏 + */ + public static final String ID_CARD_NUMBER = "id_card_number"; + /** + * 中文名脱敏 + */ + public static final String CHINESE_NAME = "chinese_name"; + /** + * 地址脱敏 + */ + public static final String ADDRESS = "address"; + /** + * 邮件脱敏 + */ + public static final String EMAIL = "email"; + /** + * 密码脱敏 + */ + public static final String PASSWORD = "password"; + /** + * 车牌号脱敏 + */ + public static final String CAR_LICENSE = "car_license"; + /** + * 银行卡号脱敏 + */ + public static final String BANK_CARD_NUMBER = "bank_card_number"; + //... +} +``` + +使用示例: + +```java +@Table("tb_account") +public class Account { + + @Id(keyType = KeyType.Auto) + private Long id; + + @ColumnMask(Masks.CHINESE_NAME) + private String userName; + + @ColumnMask(Masks.EMAIL) + private String email; + +} +``` + +如果这些内置的脱敏规则不满足你的要求的话,你还可以自定义脱敏规则。 + +1、通过 `MaskManager` 注册新的脱敏规则: + +```java +MaskManager.registerMaskProcessor("自定义规则名称" + , data -> { + return data; + }) +``` + +2、使用自定义的脱敏规则 + +```java +@Table("tb_account") +public class Account { + + @Id(keyType = KeyType.Auto) + private Long id; + + @ColumnMask("自定义规则名称") + private String userName; +} +``` + +并且,对于需要跳过脱密处理的场景,例如进入编辑页面编辑用户数据,MyBatis-Flex 也提供了对应的支持: + +1. **`MaskManager#execWithoutMask`**(推荐):该方法使用了模版方法设计模式,保障跳过脱敏处理并执行相关逻辑后自动恢复脱敏处理。 +2. **`MaskManager#skipMask`**:跳过脱敏处理。 +3. **`MaskManager#restoreMask`**:恢复脱敏处理,确保后续的操作继续使用脱敏逻辑。 + +`MaskManager#execWithoutMask`方法实现如下: + +```java +public static T execWithoutMask(Supplier supplier) { + try { + skipMask(); + return supplier.get(); + } finally { + restoreMask(); + } +} +``` + +`MaskManager` 的`skipMask`和`restoreMask`方法一般配套使用,推荐`try{...}finally{...}`模式。 + +## 总结 + +这篇文章主要介绍了: + +- 数据脱敏的定义:数据脱敏是指对某些敏感信息通过脱敏规则进行数据的变形,实现敏感隐私数据的可靠保护。 +- 常用的脱敏规则:替换、删除、重排、加噪和加密。 +- 常用的脱敏工具:Hutool、Apache ShardingSphere、FastJSON、Mybatis-Mate 和 MyBatis-Flex。 + +## 参考 + +- Hutool 工具官网: +- 聊聊如何自定义数据脱敏: +- FastJSON 实现数据脱敏: + + 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 等框架可以帮助我们实现权限校验。 + +## 参考 + +- 为什么前后端都需要进行数据校验?: +- 权限系统设计详解: diff --git a/docs/system-design/security/design-of-authority-system.md b/docs/system-design/security/design-of-authority-system.md index 1be9a09546d..ef619abf66c 100644 --- a/docs/system-design/security/design-of-authority-system.md +++ b/docs/system-design/security/design-of-authority-system.md @@ -12,9 +12,11 @@ head: content: 基于角色的访问控制(Role-Based Access Control,简称 RBAC)指的是通过用户的角色(Role)授权其相关权限,实现了灵活的访问控制,相比直接授予用户权限,要更加简单、高效、可扩展。 --- + + > 作者:转转技术团队 > -> 原文:https://mp.weixin.qq.com/s/ONMuELjdHYa0yQceTj01Iw +> 原文: ## 老权限系统的问题与现状 @@ -206,4 +208,6 @@ head: ## 参考 -- 选择合适的权限模型:https://docs.authing.cn/v2/guides/access-control/choose-the-right-access-control-model.html +- 选择合适的权限模型: + + diff --git a/docs/system-design/security/encryption-algorithms.md b/docs/system-design/security/encryption-algorithms.md new file mode 100644 index 00000000000..cb75b10a415 --- /dev/null +++ b/docs/system-design/security/encryption-algorithms.md @@ -0,0 +1,375 @@ +--- +title: 常见加密算法总结 +category: 系统设计 +tag: + - 安全 +--- + +加密算法是一种用数学方法对数据进行变换的技术,目的是保护数据的安全,防止被未经授权的人读取或修改。加密算法可以分为三大类:对称加密算法、非对称加密算法和哈希算法(也叫摘要算法)。 + +日常开发中常见的需要用到加密算法的场景: + +1. 保存在数据库中的密码需要加盐之后使用哈希算法(比如 BCrypt)进行加密。 +2. 保存在数据库中的银行卡号、身份号这类敏感数据需要使用对称加密算法(比如 AES)保存。 +3. 网络传输的敏感数据比如银行卡号、身份号需要用 HTTPS + 非对称加密算法(如 RSA)来保证传输数据的安全性。 +4. …… + +ps: 严格上来说,哈希算法其实不属于加密算法,只是可以用到某些加密场景中(例如密码加密),两者可以看作是并列关系。加密算法通常指的是可以将明文转换为密文,并且能够通过某种方式(如密钥)再将密文还原为明文的算法。而哈希算法是一种单向过程,它将输入信息转换成一个固定长度的、看似随机的哈希值,但这个过程是不可逆的,也就是说,不能从哈希值还原出原始信息。 + +## 哈希算法 + +哈希算法也叫散列函数或摘要算法,它的作用是对任意长度的数据生成一个固定长度的唯一标识,也叫哈希值、散列值或消息摘要(后文统称为哈希值)。 + +![哈希算法效果演示](https://oss.javaguide.cn/github/javaguide/system-design/security/encryption-algorithms/hash-function-effect-demonstration.png) + +哈希算法的是不可逆的,你无法通过哈希之后的值再得到原值。 + +哈希值的作用是可以用来验证数据的完整性和一致性。 + +举两个实际的例子: + +- 保存密码到数据库时使用哈希算法进行加密,可以通过比较用户输入密码的哈希值和数据库保存的哈希值是否一致,来判断密码是否正确。 +- 我们下载一个文件时,可以通过比较文件的哈希值和官方提供的哈希值是否一致,来判断文件是否被篡改或损坏; + +这种算法的特点是不可逆: + +- 不能从哈希值还原出原始数据。 +- 原始数据的任何改变都会导致哈希值的巨大变化。 + +哈希算法可以简单分为两类: + +1. **加密哈希算法**:安全性较高的哈希算法,它可以提供一定的数据完整性保护和数据防篡改能力,能够抵御一定的攻击手段,安全性相对较高,但性能较差,适用于对安全性要求较高的场景。例如 SHA2、SHA3、SM3、RIPEMD-160、BLAKE2、SipHash 等等。 +2. **非加密哈希算法**:安全性相对较低的哈希算法,易受到暴力破解、冲突攻击等攻击手段的影响,但性能较高,适用于对安全性没有要求的业务场景。例如 CRC32、MurMurHash3、SipHash 等等。 + +除了这两种之外,还有一些特殊的哈希算法,例如安全性更高的**慢哈希算法**。 + +常见的哈希算法有: + +- MD(Message Digest,消息摘要算法):MD2、MD4、MD5 等,已经不被推荐使用。 +- SHA(Secure Hash Algorithm,安全哈希算法):SHA-1 系列安全性低,SHA2,SHA3 系列安全性较高。 +- 国密算法:例如 SM2、SM3、SM4,其中 SM2 为非对称加密算法,SM4 为对称加密算法,SM3 为哈希算法(安全性及效率和 SHA-256 相当,但更适合国内的应用环境)。 +- Bcrypt(密码哈希算法):基于 Blowfish 加密算法的密码哈希算法,专门为密码加密而设计,安全性高,属于慢哈希算法。 +- MAC(Message Authentication Code,消息认证码算法):HMAC 是一种基于哈希的 MAC,可以与任何安全的哈希算法结合使用,例如 SHA-256。 +- CRC:(Cyclic Redundancy Check,循环冗余校验):CRC32 是一种 CRC 算法,它的特点是生成 32 位的校验值,通常用于数据完整性校验、文件校验等场景。 +- SipHash:加密哈希算法,它的设计目的是在速度和安全性之间达到一个平衡,用于防御[哈希泛洪 DoS 攻击](https://aumasson.jp/siphash/siphashdos_29c3_slides.pdf)。Rust 默认使用 SipHash 作为哈希算法,从 Redis4.0 开始,哈希算法被替换为 SipHash。 +- MurMurHash:经典快速的非加密哈希算法,目前最新的版本是 MurMurHash3,可以生成 32 位或者 128 位哈希值; +- …… + +哈希算法一般是不需要密钥的,但也存在部分特殊哈希算法需要密钥。例如,MAC 和 SipHash 就是一种基于密钥的哈希算法,它在哈希算法的基础上增加了一个密钥,使得只有知道密钥的人才能验证数据的完整性和来源。 + +### MD + +MD 算法有多个版本,包括 MD2、MD4、MD5 等,其中 MD5 是最常用的版本,它可以生成一个 128 位(16 字节)的哈希值。从安全性上说:MD5 > MD4 > MD2。除了这些版本,还有一些基于 MD4 或 MD5 改进的算法,如 RIPEMD、HAVAL 等。 + +即使是最安全 MD 算法 MD5 也存在被破解的风险,攻击者可以通过暴力破解或彩虹表攻击等方式,找到与原始数据相同的哈希值,从而破解数据。 + +为了增加破解难度,通常可以选择加盐。盐(Salt)在密码学中,是指通过在密码任意固定位置插入特定的字符串,让哈希后的结果和使用原始密码的哈希结果不相符,这种过程称之为“加盐”。 + +加盐之后就安全了吗?并不一定,这只是增加了破解难度,不代表无法破解。而且,MD5 算法本身就存在弱碰撞(Collision)问题,即多个不同的输入产生相同的 MD5 值。 + +因此,MD 算法已经不被推荐使用,建议使用更安全的哈希算法比如 SHA-2、Bcrypt。 + +Java 提供了对 MD 算法系列的支持,包括 MD2、MD5。 + +MD5 代码示例(未加盐): + +```java +String originalString = "Java学习 + 面试指南:javaguide.cn"; +// 创建MD5摘要对象 +MessageDigest messageDigest = MessageDigest.getInstance("MD5"); +messageDigest.update(originalString.getBytes(StandardCharsets.UTF_8)); +// 计算哈希值 +byte[] result = messageDigest.digest(); +// 将哈希值转换为十六进制字符串 +String hexString = new HexBinaryAdapter().marshal(result); +System.out.println("Original String: " + originalString); +System.out.println("MD5 Hash: " + hexString.toLowerCase()); +``` + +输出: + +```bash +Original String: Java学习 + 面试指南:javaguide.cn +MD5 Hash: fb246796f5b1b60d4d0268c817c608fa +``` + +### SHA + +SHA(Secure Hash Algorithm)系列算法是一组密码哈希算法,用于将任意长度的数据映射为固定长度的哈希值。SHA 系列算法由美国国家安全局(NSA)于 1993 年设计,目前共有 SHA-1、SHA-2、SHA-3 三种版本。 + +SHA-1 算法将任意长度的数据映射为 160 位的哈希值。然而,SHA-1 算法存在一些严重的缺陷,比如安全性低,容易受到碰撞攻击和长度扩展攻击。因此,SHA-1 算法已经不再被推荐使用。 SHA-2 家族(如 SHA-256、SHA-384、SHA-512 等)和 SHA-3 系列是 SHA-1 算法的替代方案,它们都提供了更高的安全性和更长的哈希值长度。 + +SHA-2 家族是在 SHA-1 算法的基础上改进而来的,它们采用了更复杂的运算过程和更多的轮次,使得攻击者更难以通过预计算或巧合找到碰撞。 + +为了寻找一种更安全和更先进的密码哈希算法,美国国家标准与技术研究院(National Institute of Standards and Technology,简称 NIST)在 2007 年公开征集 SHA-3 的候选算法。NIST 一共收到了 64 个算法方案,经过多轮的评估和筛选,最终在 2012 年宣布 Keccak 算法胜出,成为 SHA-3 的标准算法(SHA-3 与 SHA-2 算法没有直接的关系)。 Keccak 算法具有与 MD 和 SHA-1/2 完全不同的设计思路,即海绵结构(Sponge Construction),使得传统攻击方法无法直接应用于 SHA-3 的攻击中(能够抵抗目前已知的所有攻击方式包括碰撞攻击、长度扩展攻击、差分攻击等)。 + +由于 SHA-2 算法还没有出现重大的安全漏洞,而且在软件中的效率更高,所以大多数人还是倾向于使用 SHA-2 算法。 + +相比 MD5 算法,SHA-2 算法之所以更强,主要有两个原因: + +- 哈希值长度更长:例如 SHA-256 算法的哈希值长度为 256 位,而 MD5 算法的哈希值长度为 128 位,这就提高了攻击者暴力破解或者彩虹表攻击的难度。 +- 更强的碰撞抗性:SHA 算法采用了更复杂的运算过程和更多的轮次,使得攻击者更难以通过预计算或巧合找到碰撞。目前还没有找到任何两个不同的数据,它们的 SHA-256 哈希值相同。 + +当然,SHA-2 也不是绝对安全的,也有被暴力破解或者彩虹表攻击的风险,所以,在实际的应用中,加盐还是必不可少的。 + +Java 提供了对 SHA 算法系列的支持,包括 SHA-1、SHA-256、SHA-384 和 SHA-512。 + +SHA-256 代码示例(未加盐): + +```java +String originalString = "Java学习 + 面试指南:javaguide.cn"; +// 创建SHA-256摘要对象 +MessageDigest messageDigest = MessageDigest.getInstance("SHA-256"); +messageDigest.update(originalString.getBytes()); +// 计算哈希值 +byte[] result = messageDigest.digest(); +// 将哈希值转换为十六进制字符串 +String hexString = new HexBinaryAdapter().marshal(result); +System.out.println("Original String: " + originalString); +System.out.println("SHA-256 Hash: " + hexString.toLowerCase()); +``` + +输出: + +```bash +Original String: Java学习 + 面试指南:javaguide.cn +SHA-256 Hash: 184eb7e1d7fb002444098c9bde3403c6f6722c93ecfac242c0e35cd9ed3b41cd +``` + +### Bcrypt + +Bcrypt 算法是一种基于 Blowfish 加密算法的密码哈希算法,专门为密码加密而设计,安全性高。 + +由于 Bcrypt 采用了 salt(盐) 和 cost(成本) 两种机制,它可以有效地防止彩虹表攻击和暴力破解攻击,从而保证密码的安全性。salt 是一个随机生成的字符串,用于和密码混合,增加密码的复杂度和唯一性。cost 是一个数值参数,用于控制 Bcrypt 算法的迭代次数,增加密码哈希的计算时间和资源消耗。 + +Bcrypt 算法可以根据实际情况进行调整加密的复杂度,可以设置不同的 cost 值和 salt 值,从而满足不同的安全需求,灵活性很高。 + +Java 应用程序的安全框架 Spring Security 支持多种密码编码器,其中 `BCryptPasswordEncoder` 是官方推荐的一种,它使用 BCrypt 算法对用户的密码进行加密存储。 + +```java +@Bean +public PasswordEncoder passwordEncoder(){ + return new BCryptPasswordEncoder(); +} +``` + +## 对称加密 + +对称加密算法是指加密和解密使用同一个密钥的算法,也叫共享密钥加密算法。 + +![对称加密](https://oss.javaguide.cn/github/javaguide/system-design/security/encryption-algorithms/symmetric-encryption.png) + +常见的对称加密算法有 DES、3DES、AES 等。 + +### DES 和 3DES + +DES(Data Encryption Standard)使用 64 位的密钥(有效秘钥长度为 56 位,8 位奇偶校验位)和 64 位的明文进行加密。 + +虽然 DES 一次只能加密 64 位,但我们只需要把明文划分成 64 位一组的块,就可以实现任意长度明文的加密。如果明文长度不是 64 位的倍数,必须进行填充,常用的模式有 PKCS5Padding, PKCS7Padding, NOPADDING。 + +DES 加密算法的基本思想是将 64 位的明文分成两半,然后对每一半进行多轮的变换,最后再合并成 64 位的密文。这些变换包括置换、异或、选择、移位等操作,每一轮都使用了一个子密钥,而这些子密钥都是由同一个 56 位的主密钥生成的。DES 加密算法总共进行了 16 轮变换,最后再进行一次逆置换,得到最终的密文。 + +![DES(Data Encryption Standard)](https://oss.javaguide.cn/github/javaguide/system-design/security/des-steps.jpg) + +这是一个经典的对称加密算法,但也有明显的缺陷,即 56 位的密钥安全性不足,已被证实可以在短时间内破解。 + +为了提高 DES 算法的安全性,人们提出了一些变种或者替代方案,例如 3DES(Triple DES)。 + +3DES(Triple DES)是 DES 向 AES 过渡的加密算法,它使用 2 个或者 3 个 56 位的密钥对数据进行三次加密。3DES 相当于是对每个数据块应用三次 DES 的对称加密算法。 + +为了兼容普通的 DES,3DES 并没有直接使用 加密->加密->加密 的方式,而是采用了加密->解密->加密 的方式。当三种密钥均相同时,前两步相互抵消,相当于仅实现了一次加密,因此可实现对普通 DES 加密算法的兼容。3DES 比 DES 更为安全,但其处理速度不高。 + +### AES + +AES(Advanced Encryption Standard)算法是一种更先进的对称密钥加密算法,它使用 128 位、192 位或 256 位的密钥对数据进行加密或解密,密钥越长,安全性越高。 + +AES 也是一种分组(或者叫块)密码,分组长度只能是 128 位,也就是说,每个分组为 16 个字节。AES 加密算法有多种工作模式(mode of operation),如:ECB、CBC、OFB、CFB、CTR、XTS、OCB、GCM(目前使用最广泛的模式)。不同的模式参数和加密流程不同,但是核心仍然是 AES 算法。 + +和 DES 类似,对于不是 128 位倍数的明文需要进行填充,常用的填充模式有 PKCS5Padding, PKCS7Padding, NOPADDING。不过,AES-GCM 是流加密算法,可以对任意长度的明文进行加密,所以对应的填充模式为 NoPadding,即无需填充。 + +AES 的速度比 3DES 快,而且更安全。 + +![AES(Advanced Encryption Standard)](https://oss.javaguide.cn/github/javaguide/system-design/security/aes-steps.jpg) + +DES 算法和 AES 算法简单对比(图片来自于:[RSA vs. AES Encryption: Key Differences Explained](https://cheapsslweb.com/blog/rsa-vs-aes-encryption)): + +![DES 和 AES 对比](https://oss.javaguide.cn/github/javaguide/system-design/security/des-vs-aes.png) + +基于 Java 实现 AES 算法代码示例: + +```java +private static final String AES_ALGORITHM = "AES"; +// AES密钥 +private static final String AES_SECRET_KEY = "4128D9CDAC7E2F82951CBAF7FDFE675B"; +// AES加密模式为GCM,填充方式为NoPadding +// AES-GCM 是流加密(Stream cipher)算法,所以对应的填充模式为 NoPadding,即无需填充。 +private static final String AES_TRANSFORMATION = "AES/GCM/NoPadding"; +// 加密器 +private static Cipher encryptionCipher; +// 解密器 +private static Cipher decryptionCipher; + +/** + * 完成一些初始化工作 + */ +public static void init() throws Exception { + // 将AES密钥转换为SecretKeySpec对象 + SecretKeySpec secretKeySpec = new SecretKeySpec(AES_SECRET_KEY.getBytes(), AES_ALGORITHM); + // 使用指定的AES加密模式和填充方式获取对应的加密器并初始化 + encryptionCipher = Cipher.getInstance(AES_TRANSFORMATION); + encryptionCipher.init(Cipher.ENCRYPT_MODE, secretKeySpec); + // 使用指定的AES加密模式和填充方式获取对应的解密器并初始化 + decryptionCipher = Cipher.getInstance(AES_TRANSFORMATION); + decryptionCipher.init(Cipher.DECRYPT_MODE, secretKeySpec, new GCMParameterSpec(128, encryptionCipher.getIV())); +} + +/** + * 加密 + */ +public static String encrypt(String data) throws Exception { + byte[] dataInBytes = data.getBytes(); + // 加密数据 + byte[] encryptedBytes = encryptionCipher.doFinal(dataInBytes); + return Base64.getEncoder().encodeToString(encryptedBytes); +} + +/** + * 解密 + */ +public static String decrypt(String encryptedData) throws Exception { + byte[] dataInBytes = Base64.getDecoder().decode(encryptedData); + // 解密数据 + byte[] decryptedBytes = decryptionCipher.doFinal(dataInBytes); + return new String(decryptedBytes, StandardCharsets.UTF_8); +} + +public static void main(String[] args) throws Exception { + String originalString = "Java学习 + 面试指南:javaguide.cn"; + init(); + String encryptedData = encrypt(originalString); + String decryptedData = decrypt(encryptedData); + System.out.println("Original String: " + originalString); + System.out.println("AES Encrypted Data : " + encryptedData); + System.out.println("AES Decrypted Data : " + decryptedData); +} +``` + +输出: + +```bash +Original String: Java学习 + 面试指南:javaguide.cn +AES Encrypted Data : E1qTkK91suBqToag7WCyoFP9uK5hR1nSfM6p+oBlYj71bFiIVnk5TsQRT+zpjv8stha7oyKi3jQ= +AES Decrypted Data : Java学习 + 面试指南:javaguide.cn +``` + +## 非对称加密 + +非对称加密算法是指加密和解密使用不同的密钥的算法,也叫公开密钥加密算法。这两个密钥互不相同,一个称为公钥,另一个称为私钥。公钥可以公开给任何人使用,私钥则要保密。 + +如果用公钥加密数据,只能用对应的私钥解密(加密);如果用私钥加密数据,只能用对应的公钥解密(签名)。这样就可以实现数据的安全传输和身份认证。 + +![非对称加密](https://oss.javaguide.cn/github/javaguide/system-design/security/encryption-algorithms/asymmetric-encryption.png) + +常见的非对称加密算法有 RSA、DSA、ECC 等。 + +### RSA + +RSA(Rivest–Shamir–Adleman algorithm)算法是一种基于大数分解的困难性的非对称加密算法,它需要选择两个大素数作为私钥的一部分,然后计算出它们的乘积作为公钥的一部分(寻求两个大素数比较简单,而将它们的乘积进行因式分解却极其困难)。RSA 算法原理的详细介绍,可以参考这篇文章:[你真的了解 RSA 加密算法吗? - 小傅哥](https://www.cnblogs.com/xiaofuge/p/16954187.html)。 + +RSA 算法的安全性依赖于大数分解的难度,目前已经有 512 位和 768 位的 RSA 公钥被成功分解,因此建议使用 2048 位或以上的密钥长度。 + +RSA 算法的优点是简单易用,可以用于数据加密和数字签名;缺点是运算速度慢,不适合大量数据的加密。 + +RSA 算法是是目前应用最广泛的非对称加密算法,像 SSL/TLS、SSH 等协议中就用到了 RSA 算法。 + +![HTTPS 证书签名算法中带RSA 加密的SHA-256 ](https://oss.javaguide.cn/github/javaguide/system-design/security/encryption-algorithms/https-rsa-sha-256.png) + +基于 Java 实现 RSA 算法代码示例: + +```java +private static final String RSA_ALGORITHM = "RSA"; + +/** + * 生成RSA密钥对 + */ +public static KeyPair generateKeyPair() throws NoSuchAlgorithmException { + KeyPairGenerator keyPairGenerator = KeyPairGenerator.getInstance(RSA_ALGORITHM); + // 密钥大小为2048位 + keyPairGenerator.initialize(2048); + return keyPairGenerator.generateKeyPair(); +} + +/** + * 使用公钥加密数据 + */ +public static String encrypt(String data, PublicKey publicKey) throws Exception { + Cipher cipher = Cipher.getInstance(RSA_ALGORITHM); + cipher.init(Cipher.ENCRYPT_MODE, publicKey); + byte[] encryptedData = cipher.doFinal(data.getBytes(StandardCharsets.UTF_8)); + return Base64.getEncoder().encodeToString(encryptedData); +} + +/** + * 使用私钥解密数据 + */ +public static String decrypt(String encryptedData, PrivateKey privateKey) throws Exception { + byte[] decodedData = Base64.getDecoder().decode(encryptedData); + Cipher cipher = Cipher.getInstance(RSA_ALGORITHM); + cipher.init(Cipher.DECRYPT_MODE, privateKey); + byte[] decryptedData = cipher.doFinal(decodedData); + return new String(decryptedData, StandardCharsets.UTF_8); +} + +public static void main(String[] args) throws Exception { + KeyPair keyPair = generateKeyPair(); + PublicKey publicKey = keyPair.getPublic(); + PrivateKey privateKey = keyPair.getPrivate(); + String originalString = "Java学习 + 面试指南:javaguide.cn"; + String encryptedData = encrypt(originalString, publicKey); + String decryptedData = decrypt(encryptedData, privateKey); + System.out.println("Original String: " + originalString); + System.out.println("RSA Encrypted Data : " + encryptedData); + System.out.println("RSA Decrypted Data : " + decryptedData); +} +``` + +输出: + +```bash +Original String: Java学习 + 面试指南:javaguide.cn +RSA Encrypted Data : T9ey/CEPUAhZm4UJjuVNIg8RPd1fQ32S9w6+rvOKxmuMumkJY2daFfWuCn8A73Mk5bL6TigOJI0GHfKOt/W2x968qLM3pBGCcPX17n4pR43f32IIIz9iPdgF/INOqDxP5ZAtCDvTiuzcSgDHXqiBSK5TDjtj7xoGjfudYAXICa8pWitnqDgJYoo2J0F8mKzxoi8D8eLE455MEx8ZT1s7FUD/z7/H8CfShLRbO9zq/zFI06TXn123ufg+F4lDaq/5jaIxGVEUB/NFeX4N6OZCFHtAV32mw71BYUadzI9TgvkkUr1rSKmQ0icNhnRdKedJokGUh8g9QQ768KERu92Ibg== +RSA Decrypted Data : Java学习 + 面试指南:javaguide.cn +``` + +### DSA + +DSA(Digital Signature Algorithm)算法是一种基于离散对数的困难性的非对称加密算法,它需要选择一个素数 q 和一个 q 的倍数 p 作为私钥的一部分,然后计算出一个模 p 的原根 g 和一个模 q 的整数 y 作为公钥的一部分。DSA 算法的安全性依赖于离散对数的难度,目前已经有 1024 位的 DSA 公钥被成功破解,因此建议使用 2048 位或以上的密钥长度。 + +DSA 算法的优点是数字签名速度快,适合生成数字证书;缺点是不能用于数据加密,且签名过程需要随机数。 + +DSA 算法签名过程: + +1. 使用消息摘要算法对要发送的数据进行加密,生成一个信息摘要,也就是一个短的、唯一的、不可逆的数据表示。 +2. 发送方用自己的 DSA 私钥对信息摘要再进行加密,形成一个数字签名,也就是一个可以证明数据来源和完整性的数据附加。 +3. 将原始数据和数字签名一起通过互联网传送给接收方。 +4. 接收方用发送方的公钥对数字签名进行解密,得到信息摘要。同时,接收方也用消息摘要算法对收到的原始数据进行加密,得到另一个信息摘要。接收方将两个信息摘要进行比较,如果两者一致,则说明在传送过程中数据没有被篡改或损坏;否则,则说明数据已经失去了安全性和保密性。 + +![DSA 算法签名过程](https://oss.javaguide.cn/github/javaguide/system-design/security/encryption-algorithms/dsa-algorithm-signing-process.png) + +## 总结 + +这篇文章介绍了三种加密算法:哈希算法、对称加密算法和非对称加密算法。 + +- 哈希算法是一种用数学方法对数据生成一个固定长度的唯一标识的技术,可以用来验证数据的完整性和一致性,常见的哈希算法有 MD、SHA、MAC 等。 +- 对称加密算法是一种加密和解密使用同一个密钥的算法,可以用来保护数据的安全性和保密性,常见的对称加密算法有 DES、3DES、AES 等。 +- 非对称加密算法是一种加密和解密使用不同的密钥的算法,可以用来实现数据的安全传输和身份认证,常见的非对称加密算法有 RSA、DSA、ECC 等。 + +## 参考 + +- 深入理解完美哈希 - 腾讯技术工程: +- 写给开发人员的实用密码学(二)—— 哈希函数: +- 奇妙的安全旅行之 DSA 算法: +- AES-GCM 加密简介: +- Java AES 256 GCM Encryption and Decryption Example | JCE Unlimited Strength: + + diff --git a/docs/system-design/security/jwt-intro.md b/docs/system-design/security/jwt-intro.md index 9b50bbb1cde..f4087fde8e6 100644 --- a/docs/system-design/security/jwt-intro.md +++ b/docs/system-design/security/jwt-intro.md @@ -5,6 +5,8 @@ tag: - 安全 --- + + ## 什么是 JWT? JWT (JSON Web Token) 是目前最流行的跨域认证解决方案,是一种基于 Token 的认证授权机制。 从 JWT 的全称可以看出,JWT 本身也是 Token,一种规范化之后的 JSON 结构的 Token。 @@ -23,19 +25,19 @@ JWT 自身包含了身份验证所需要的所有信息,因此,我们的服 ## JWT 由哪些部分组成? -![此图片来源于:https://supertokens.com/blog/oauth-vs-jwt](https://oss.javaguide.cn/javaguide/system-design/jwt/jwt-composition.png) +![JWT 组成](https://oss.javaguide.cn/javaguide/system-design/jwt/jwt-composition.png) JWT 本质上就是一组字串,通过(`.`)切分成三个为 Base64 编码的部分: -- **Header** : 描述 JWT 的元数据,定义了生成签名的算法以及 `Token` 的类型。 -- **Payload** : 用来存放实际需要传递的数据 -- **Signature(签名)**:服务器通过 Payload、Header 和一个密钥(Secret)使用 Header 里面指定的签名算法(默认是 HMAC SHA256)生成。 +- **Header(头部)** : 描述 JWT 的元数据,定义了生成签名的算法以及 `Token` 的类型。Header 被 Base64Url 编码后成为 JWT 的第一部分。 +- **Payload(载荷)** : 用来存放实际需要传递的数据,包含声明(Claims),如`sub`(subject,主题)、`jti`(JWT ID)。Payload 被 Base64Url 编码后成为 JWT 的第二部分。 +- **Signature(签名)**:服务器通过 Payload、Header 和一个密钥(Secret)使用 Header 里面指定的签名算法(默认是 HMAC SHA256)生成。生成的签名会成为 JWT 的第三部分。 JWT 通常是这样的:`xxxxx.yyyyy.zzzzz`。 示例: -``` +```plain eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9. eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkpvaG4gRG9lIiwiaWF0IjoxNTE2MjM5MDIyfQ. SflKxwRJSMeKKF2QT4fwpMeJf36POk6yJV_adQssw5c @@ -114,7 +116,7 @@ Signature 部分是对前两部分的签名,作用是防止 JWT(主要是 pa 签名的计算公式如下: -``` +```plain HMACSHA256( base64UrlEncode(header) + "." + base64UrlEncode(payload), @@ -160,5 +162,7 @@ HMACSHA256( 3. JWT 存放在 localStorage 中而不是 Cookie 中,避免 CSRF 风险。 4. 一定不要将隐私信息存放在 Payload 当中。 5. 密钥一定保管好,一定不要泄露出去。JWT 安全的核心在于签名,签名安全的核心在密钥。 -6. Payload 要加入 `exp` (JWT 的过期时间),永久有效的 JWT 不合理。并且,JWT 的过期时间不易过长。 -7. ...... +6. Payload 要加入 `exp` (JWT 的过期时间),永久有效的 JWT 不合理。并且,JWT 的过期时间不宜过长。 +7. …… + + diff --git a/docs/system-design/security/sentive-words-filter.md b/docs/system-design/security/sentive-words-filter.md index 4d015708865..d4e8a53a26c 100644 --- a/docs/system-design/security/sentive-words-filter.md +++ b/docs/system-design/security/sentive-words-filter.md @@ -13,7 +13,7 @@ tag: ### Trie 树 -**Trie 树** 也称为字典树、单词查找树,哈系树的一种变种,通常被用于字符串匹配,用来解决在一组字符串集合中快速查找某个字符串的问题。像浏览器搜索的关键词提示一般就是基于 Trie 树来做的。 +**Trie 树** 也称为字典树、单词查找树,哈希树的一种变种,通常被用于字符串匹配,用来解决在一组字符串集合中快速查找某个字符串的问题。像浏览器搜索的关键词提示就可以基于 Trie 树来做的。 ![浏览器 Trie 树效果展示](https://oss.javaguide.cn/github/javaguide/system-design/security/brower-trie.png) @@ -32,9 +32,9 @@ tag: 可以看出, **Trie 树的核心原理其实很简单,就是通过公共前缀来提高字符串匹配效率。** -[Apache Commons Collecions](https://mvnrepository.com/artifact/org.apache.commons/commons-collections4) 这个库中就有 Trie 树实现: +[Apache Commons Collections](https://mvnrepository.com/artifact/org.apache.commons/commons-collections4) 这个库中就有 Trie 树实现: -![Apache Commons Collecions 中的 Trie 树实现](https://oss.javaguide.cn/github/javaguide/system-design/security/common-collections-trie.png) +![Apache Commons Collections 中的 Trie 树实现](https://oss.javaguide.cn/github/javaguide/system-design/security/common-collections-trie.png) ```java Trie trie = new PatriciaTrie<>(); @@ -48,11 +48,17 @@ assertEquals("{Abi=doctor, Abigail=student}", trie.prefixMap("Abi").toString()); assertEquals("{Chris=doctor, Christina=student}", trie.prefixMap("Chr").toString()); ``` +Trie 树是一种利用空间换时间的数据结构,占用的内存会比较大。也正是因为这个原因,实际工程项目中都是使用的改进版 Trie 树例如双数组 Trie 树(Double-Array Trie,DAT)。 + +DAT 的设计者是日本的 Aoe Jun-ichi,Mori Akira 和 Sato Takuya,他们在 1989 年发表了一篇论文[《An Efficient Implementation of Trie Structures》](https://www.co-ding.com/assets/pdf/dat.pdf),详细介绍了 DAT 的构造和应用,原作者写的示例代码地址:。相比较于 Trie 树,DAT 的内存占用极低,可以达到 Trie 树内存的 1%左右。DAT 在中文分词、自然语言处理、信息检索等领域有广泛的应用,是一种非常优秀的数据结构。 + +### AC 自动机 + Aho-Corasick(AC)自动机是一种建立在 Trie 树上的一种改进算法,是一种多模式匹配算法,由贝尔实验室的研究人员 Alfred V. Aho 和 Margaret J.Corasick 发明。 -AC 自动机算法使用 Trie 树来存放模式串的前缀,通过失败匹配指针(失配指针)来处理匹配失败的跳转。 +AC 自动机算法使用 Trie 树来存放模式串的前缀,通过失败匹配指针(失配指针)来处理匹配失败的跳转。关于 AC 自动机的详细介绍,可以查看这篇文章:[地铁十分钟 | AC 自动机](https://zhuanlan.zhihu.com/p/146369212)。 -相关阅读:[地铁十分钟 | AC 自动机](https://zhuanlan.zhihu.com/p/146369212) +如果使用上面提到的 DAT 来表示 AC 自动机 ,就可以兼顾两者的优点,得到一种高效的多模式匹配算法。Github 上已经有了开源 Java 实现版本: 。 ### DFA @@ -83,7 +89,7 @@ System.out.println(matchStrList2); 输出: -``` +```plain 大 [大, 憨憨] [大, 大憨憨] @@ -98,3 +104,5 @@ System.out.println(matchStrList2); - [一种敏感词自动过滤管理系统](https://patents.google.com/patent/CN101964000B) - [一种网络游戏中敏感词过滤方法及系统](https://patents.google.com/patent/CN103714160A/zh) + + diff --git a/docs/system-design/security/sso-intro.md b/docs/system-design/security/sso-intro.md index 17ace7dbd73..b0b00552045 100644 --- a/docs/system-design/security/sso-intro.md +++ b/docs/system-design/security/sso-intro.md @@ -5,7 +5,7 @@ tag: - 安全 --- -> 本文授权转载自:https://ken.io/note/sso-design-implement 作者:ken.io +> 本文授权转载自: 作者:ken.io ## SSO 介绍 @@ -13,7 +13,7 @@ tag: SSO 英文全称 Single Sign On,单点登录。SSO 是在多个应用系统中,用户只需要登录一次就可以访问所有相互信任的应用系统。 -例如你登录网易账号中心(https://reg.163.com/ )之后访问以下站点都是登录状态。 +例如你登录网易账号中心( )之后访问以下站点都是登录状态。 - 网易直播 [https://v.163.com](https://v.163.com/) - 网易博客 [https://blog.163.com](https://blog.163.com/) @@ -120,3 +120,5 @@ SSO 英文全称 Single Sign On,单点登录。SSO 是在多个应用系统中 - 关于方案:这次设计方案更多是提供实现思路。如果涉及到 APP 用户登录等情况,在访问 SSO 服务时,增加对 APP 的签名验证就好了。当然,如果有无线网关,验证签名不是问题。 - 关于时序图:时序图中并没有包含所有场景,只列举了核心/主要场景,另外对于一些不影响理解思路的消息能省就省了。 + + diff --git a/docs/system-design/system-design-questions.md b/docs/system-design/system-design-questions.md index 462a98f7771..e34d5cc479c 100644 --- a/docs/system-design/system-design-questions.md +++ b/docs/system-design/system-design-questions.md @@ -9,3 +9,5 @@ icon: "design" ![](https://oss.javaguide.cn/javamianshizhibei/system-design-questions.png) + + diff --git a/docs/system-design/web-real-time-message-push.md b/docs/system-design/web-real-time-message-push.md index 5bde2f12cd6..f08e1b2e716 100644 --- a/docs/system-design/web-real-time-message-push.md +++ b/docs/system-design/web-real-time-message-push.md @@ -189,10 +189,14 @@ iframe 流的服务器开销很大,而且 IE、Chrome 等浏览器一直会处 iframe 流非常不友好,强烈不推荐。 -### SSE (我的方式) +### SSE (推荐) 很多人可能不知道,服务端向客户端推送消息,其实除了可以用`WebSocket`这种耳熟能详的机制外,还有一种服务器发送事件(Server-Sent Events),简称 SSE。这是一种服务器端到客户端(浏览器)的单向消息推送。 +大名鼎鼎的 ChatGPT 就是采用的 SSE。对于需要长时间等待响应的对话场景,ChatGPT 采用了一种巧妙的策略:它会将已经计算出的数据“推送”给用户,并利用 SSE 技术在计算过程中持续返回数据。这样做的好处是可以避免用户因等待时间过长而选择关闭页面。 + +![ChatGPT 使用 SSE 实现对话](https://oss.javaguide.cn/github/javaguide/system-design/web-real-time-message-push/chatgpt-sse.png) + SSE 基于 HTTP 协议的,我们知道一般意义上的 HTTP 协议是无法做到服务端主动向客户端推送消息的,但 SSE 是个例外,它变换了一种思路。 ![](https://oss.javaguide.cn/github/javaguide/system-design/web-real-time-message-push/1460000042192390.png) @@ -298,11 +302,20 @@ public static void sendMessage(String userId, String message) { Websocket 应该是大家都比较熟悉的一种实现消息推送的方式,上边我们在讲 SSE 的时候也和 Websocket 进行过比较。 -是一种在 TCP 连接上进行全双工通信的协议,建立客户端和服务器之间的通信渠道。浏览器和服务器仅需一次握手,两者之间就直接可以创建持久性的连接,并进行双向数据传输。 +这是一种在 TCP 连接上进行全双工通信的协议,建立客户端和服务器之间的通信渠道。浏览器和服务器仅需一次握手,两者之间就直接可以创建持久性的连接,并进行双向数据传输。 ![Websocket 示意图](https://oss.javaguide.cn/github/javaguide/system-design/web-real-time-message-push/1460000042192394.png) -SpringBoot 整合 Websocket,先引入 Websocket 相关的工具包,和 SSE 相比额外的开发成本。 +WebSocket 的工作过程可以分为以下几个步骤: + +1. 客户端向服务器发送一个 HTTP 请求,请求头中包含 `Upgrade: websocket` 和 `Sec-WebSocket-Key` 等字段,表示要求升级协议为 WebSocket; +2. 服务器收到这个请求后,会进行升级协议的操作,如果支持 WebSocket,它将回复一个 HTTP 101 状态码,响应头中包含 ,`Connection: Upgrade`和 `Sec-WebSocket-Accept: xxx` 等字段、表示成功升级到 WebSocket 协议。 +3. 客户端和服务器之间建立了一个 WebSocket 连接,可以进行双向的数据传输。数据以帧(frames)的形式进行传送,而不是传统的 HTTP 请求和响应。WebSocket 的每条消息可能会被切分成多个数据帧(最小单位)。发送端会将消息切割成多个帧发送给接收端,接收端接收消息帧,并将关联的帧重新组装成完整的消息。 +4. 客户端或服务器可以主动发送一个关闭帧,表示要断开连接。另一方收到后,也会回复一个关闭帧,然后双方关闭 TCP 连接。 + +另外,建立 WebSocket 连接之后,通过心跳机制来保持 WebSocket 连接的稳定性和活跃性。 + +SpringBoot 整合 WebSocket,先引入 WebSocket 相关的工具包,和 SSE 相比有额外的开发成本。 ```xml @@ -361,6 +374,22 @@ public class WebSocketServer { } ``` +服务端还需要注入`ServerEndpointerExporter`,这个 Bean 就会自动注册使用了`@ServerEndpoint`注解的 WebSocket 服务器。 + +```java +@Configuration +public class WebSocketConfiguration { + + /** + * 用于注册使用了 @ServerEndpoint 注解的 WebSocket 服务器 + */ + @Bean + public ServerEndpointExporter serverEndpointExporter() { + return new ServerEndpointExporter(); + } +} +``` + 前端初始化打开 WebSocket 连接,并监听连接状态,接收服务端数据或向服务端发送数据。 ```javascript @@ -445,3 +474,5 @@ MQTT 协议为什么在物联网(IOT)中如此受偏爱?而不是其它协 | SSE | 一种服务器端到客户端(浏览器)的单向消息推送。 | 简单、易实现,功能丰富 | 不支持双向通信 | | WebSocket | 除了最初建立连接时用 HTTP 协议,其他时候都是直接基于 TCP 协议进行通信的,可以实现客户端和服务端的全双工通信。 | 性能高、开销小 | 对开发人员要求更高,实现相对复杂一些 | | MQTT | 基于发布/订阅(publish/subscribe)模式的轻量级通讯协议,通过订阅相应的主题来获取消息。 | 成熟稳定,轻量级 | 对开发人员要求更高,实现相对复杂一些 | + + diff --git a/docs/tools/docker/docker-in-action.md b/docs/tools/docker/docker-in-action.md index 4844d541394..3c7198cf96b 100644 --- a/docs/tools/docker/docker-in-action.md +++ b/docs/tools/docker/docker-in-action.md @@ -20,7 +20,7 @@ tag: 官网地址: 。 -![认识容器](https://my-blog-to-use.oss-cn-beijing.aliyuncs.com/2019-7/container.png) +![认识容器](https://oss.javaguide.cn/github/javaguide/tools/docker/container.png) ### 为什么要用 Docker? @@ -46,35 +46,35 @@ Docker 的出现完美地解决了这一问题,我们可以在容器中安装 接下来对 Docker 进行安装,以 Windows 系统为例,访问 Docker 的官网: -![](https://oscimg.oschina.net/oscnet/up-4e3146984adaee0067bdc5e9b1d757bb479.png) +![安装 Docker](https://oss.javaguide.cn/github/javaguide/tools/docker/docker-install-windows.png) 然后点击`Get Started`: -![](https://oscimg.oschina.net/oscnet/up-96adfbfebe3e59097c8ba25e55f68ba7908.png) +![安装 Docker](https://oss.javaguide.cn/github/javaguide/tools/docker/docker-install-windows-download.png) 在此处点击`Download for Windows`即可进行下载。 如果你的电脑是`Windows 10 64位专业版`的操作系统,则在安装 Docker 之前需要开启一下`Hyper-V`,开启方式如下。打开控制面板,选择程序: -![](https://oscimg.oschina.net/oscnet/up-73ce678240826de0f49225250a970b4d205.png) +![开启 Hyper-V](https://oss.javaguide.cn/github/javaguide/tools/docker/docker-windows-hyperv.png) 点击`启用或关闭Windows功能`: -![](https://oscimg.oschina.net/oscnet/up-9c7a96c332e56b9506325a1f1fdb608a659.png) +![开启 Hyper-V](https://oss.javaguide.cn/github/javaguide/tools/docker/docker-windows-hyperv-enable.png) 勾选上`Hyper-V`,点击确定即可: -![](https://oscimg.oschina.net/oscnet/up-aad4a58c5e917f7185908d6320d7fb06861.png) +![开启 Hyper-V](https://oss.javaguide.cn/github/javaguide/tools/docker/docker-windows-hyperv-check.png) 完成更改后需要重启一下计算机。 开启了`Hyper-V`后,我们就可以对 Docker 进行安装了,打开安装程序后,等待片刻点击`Ok`即可: -![](https://oscimg.oschina.net/oscnet/up-62ac3c9184bdc21387755294613ff5054c6.png) +![安装 Docker](https://oss.javaguide.cn/github/javaguide/tools/docker/docker-windows-hyperv-install.png) 安装完成后,我们仍然需要重启计算机,重启后,若提示如下内容: -![](https://oscimg.oschina.net/oscnet/up-3585c7d6a4632134ed925493a7d43e14a43.png) +![安装 Docker](https://oss.javaguide.cn/github/javaguide/tools/docker/docker-windows-hyperv-wsl2.png) 它的意思是询问我们是否使用 WSL2,这是基于 Windows 的一个 Linux 子系统,这里我们取消即可,它就会使用我们之前勾选的`Hyper-V`虚拟机。 @@ -142,21 +142,21 @@ systemctl enable docker 和 GitHub 一样,Docker 也提供了一个 DockerHub 用于查询各种镜像的地址和安装教程,为此,我们先访问 DockerHub:[https://hub.docker.com/](https://hub.docker.com/) -![](https://oscimg.oschina.net/oscnet/up-37d083cc92fe36aad829e975646b9d27fa0.png) +![DockerHub](https://oss.javaguide.cn/github/javaguide/tools/docker/dockerhub-com.png) 在左上角的搜索框中输入`MySQL`并回车: -![](https://oscimg.oschina.net/oscnet/up-ced37002391a059754def9b3a6c2aa4e342.png) +![DockerHub 搜索 MySQL](https://oss.javaguide.cn/github/javaguide/tools/docker/dockerhub-mysql.png) 可以看到相关 MySQL 的镜像非常多,若右上角有`OFFICIAL IMAGE`标识,则说明是官方镜像,所以我们点击第一个 MySQL 镜像: -![](https://oscimg.oschina.net/oscnet/up-48ba3fdc99c93a96e18b929195ca8e93c6c.png) +![MySQL 官方镜像](https://oss.javaguide.cn/github/javaguide/tools/docker/dockerhub-mysql-official-image.png) 右边提供了下载 MySQL 镜像的指令为`docker pull MySQL`,但该指令始终会下载 MySQL 镜像的最新版本。 若是想下载指定版本的镜像,则点击下面的`View Available Tags`: -![](https://oscimg.oschina.net/oscnet/up-ed601649275c6cfe65bbe422b463c263a64.png) +![查看其他版本的 MySQL](https://oss.javaguide.cn/github/javaguide/tools/docker/dockerhub-mysql-view-available-tags.png) 这里就可以看到各种版本的镜像,右边有下载的指令,所以若是想下载 5.7.32 版本的 MySQL 镜像,则执行: @@ -164,17 +164,13 @@ systemctl enable docker docker pull MySQL:5.7.32 ``` -然而下载镜像的过程是非常慢的,所以我们需要配置一下镜像源加速下载,访问`阿里云`官网: +然而下载镜像的过程是非常慢的,所以我们需要配置一下镜像源加速下载,访问`阿里云`官网,点击控制台: -![](https://oscimg.oschina.net/oscnet/up-0a46effd262d3db1b613a0db597efa31f34.png) - -点击控制台: - -![](https://oscimg.oschina.net/oscnet/up-60f198e0106be6b43044969d2900272504f.png) +![阿里云镜像加速](https://oss.javaguide.cn/github/javaguide/tools/docker/docker-aliyun-mirror-admin.png) 然后点击左上角的菜单,在弹窗的窗口中,将鼠标悬停在产品与服务上,并在右侧搜索容器镜像服务,最后点击容器镜像服务: -![](https://oscimg.oschina.net/oscnet/up-2f6706a979b405dab01bc44a29bb6b26fc4.png) +![阿里云镜像加速](https://oss.javaguide.cn/github/javaguide/tools/docker/docker-aliyun-mirror-admin-accelerator.png) 点击左侧的镜像加速器,并依次执行右侧的配置指令即可。 @@ -246,7 +242,7 @@ docker pull MySQL:5.7 docker search MySQL ``` -![](https://oscimg.oschina.net/oscnet/up-559083ae80e7501e86e95fbbad25b6d571a.png) +![](https://oss.javaguide.cn/github/javaguide/tools/docker/docker-search-mysql-terminal.png) 不过该指令只能查看 MySQL 相关的镜像信息,而不能知道有哪些版本,若想知道版本,则只能这样查询: @@ -254,11 +250,9 @@ docker search MySQL docker search MySQL:5.5 ``` -![](https://oscimg.oschina.net/oscnet/up-68394e25f652964bb042571151c5e0fd2e9.png) - 若是查询的版本不存在,则结果为空: -![](https://oscimg.oschina.net/oscnet/up-abfdd51b9ad2ced3711268369f52b077b12.png) +![](https://oss.javaguide.cn/github/javaguide/tools/docker/docker-search-mysql-404-terminal.png) 删除镜像使用指令: @@ -317,7 +311,7 @@ docker pull tomcat:8.0-jre8 下载完成后就可以运行了,运行后查看一下当前运行的容器:`docker ps` 。 -![](https://oscimg.oschina.net/oscnet/up-bd48e20ef07b7c91ad16f92821a3dbca5b5.png) +![](https://oss.javaguide.cn/github/javaguide/tools/docker/docker-ps-terminal.png) 其中`CONTAINER_ID`为容器的 id,`IMAGE`为镜像名,`COMMAND`为容器内执行的命令,`CREATED`为容器的创建时间,`STATUS`为容器的状态,`PORTS`为容器内服务监听的端口,`NAMES`为容器的名称。 @@ -331,7 +325,7 @@ docker run -p 8080:8080 tomcat:8.0-jre8 此时外部就可以访问 Tomcat 了: -![](https://oscimg.oschina.net/oscnet/up-16d9ff4d29094681f51424ea8d0ee4fd73e.png) +![](https://oss.javaguide.cn/github/javaguide/tools/docker/docker-run-tomact-8080.png) 若是这样进行映射: @@ -361,9 +355,7 @@ docker run -d -p 8080:8080 --name tomcat01 tomcat:8.0-jre8 docker ps -a ``` -该参数会将运行和非运行的容器全部列举出来: - -![](https://oscimg.oschina.net/oscnet/up-16d9ff4d29094681f51424ea8d0ee4fd73e.png) +该参数会将运行和非运行的容器全部列举出来。 `-q`参数将只查询正在运行的容器 id:`docker ps -q` 。 @@ -471,16 +463,12 @@ docker logs -ft 289cc00dc5ed docker top 289cc00dc5ed ``` -![](https://oscimg.oschina.net/oscnet/up-7ec71a682712e56e90490f55c32cf660fd3.png) - 若是想与容器进行交互,则使用指令: ```shell docker exec -it 289cc00dc5ed bash ``` -![](https://oscimg.oschina.net/oscnet/up-fd17796322f833685ca8ead592d38581898.png) - 此时终端将会进入容器内部,执行的指令都将在容器中生效,在容器内只能执行一些比较简单的指令,如:ls、cd 等,若是想退出容器终端,重新回到 CentOS 中,则执行`exit`即可。 现在我们已经能够进入容器终端执行相关操作了,那么该如何向 tomcat 容器中部署一个项目呢? @@ -515,7 +503,7 @@ docker cp 289cc00dc5ed:/usr/local/tomcat/webapps/test.html ./ docker inspect 923c969b0d91 ``` -![](https://oscimg.oschina.net/oscnet/up-fca74d4350cdfebfc2b06101e1cab411619.png) +![](https://oss.javaguide.cn/github/javaguide/tools/docker/docker-inspect-terminal.png) ## Docker 数据卷 @@ -529,7 +517,7 @@ docker run -d -p 8080:8080 --name tomcat01 -v /opt/apps:/usr/local/tomcat/webapp 然而此时访问 tomcat 会发现无法访问: -![](https://oscimg.oschina.net/oscnet/up-8fa1b23f6ea2567b5938370e7d7f636533f.png) +![](https://oss.javaguide.cn/github/javaguide/tools/docker/docker-data-volume-webapp-8080.png) 这就说明我们的数据卷设置成功了,Docker 会将容器内的`webapps`目录与`/opt/apps`目录进行同步,而此时`/opt/apps`目录是空的,导致`webapps`目录也会变成空目录,所以就访问不到了。 @@ -572,7 +560,7 @@ public class HelloServlet extends HttpServlet { 这是一个非常简单的 Servlet,我们将其打包上传到`/opt/apps`中,那么容器内肯定就会同步到该文件,此时进行访问: -![](https://oscimg.oschina.net/oscnet/up-712716a8c8c444ba3a77ade8ff27e7c6cf5.png) +![](https://oss.javaguide.cn/github/javaguide/tools/docker/docker-data-volume-webapp-8080-hello-world.png) 这种方式设置的数据卷称为自定义数据卷,因为数据卷的目录是由我们自己设置的,Docker 还为我们提供了另外一种设置数据卷的方式: @@ -642,3 +630,5 @@ Loaded image: my_tomcat:1.0 REPOSITORY TAG IMAGE ID CREATED SIZE my_tomcat 1.0 79ab047fade5 7 minutes ago 463MB ``` + + diff --git a/docs/tools/docker/docker-intro.md b/docs/tools/docker/docker-intro.md index a4d89121762..5db4f557784 100644 --- a/docs/tools/docker/docker-intro.md +++ b/docs/tools/docker/docker-intro.md @@ -5,13 +5,13 @@ tag: - Docker --- -**本文只是对 Docker 的概念做了较为详细的介绍,并不涉及一些像 Docker 环境的安装以及 Docker 的一些常见操作和命令。** +本文只是对 Docker 的概念做了较为详细的介绍,并不涉及一些像 Docker 环境的安装以及 Docker 的一些常见操作和命令。 -## 一 认识容器 +## 容器介绍 **Docker 是世界领先的软件容器平台**,所以想要搞懂 Docker 的概念我们必须先从容器开始说起。 -### 1.1 什么是容器? +### 什么是容器? #### 先来看看容器较为官方的解释 @@ -23,21 +23,21 @@ tag: #### 再来看看容器较为通俗的解释 -**如果需要通俗地描述容器的话,我觉得容器就是一个存放东西的地方,就像书包可以装各种文具、衣柜可以放各种衣服、鞋架可以放各种鞋子一样。我们现在所说的容器存放的东西可能更偏向于应用比如网站、程序甚至是系统环境。** +如果需要通俗地描述容器的话,我觉得容器就是一个存放东西的地方,就像书包可以装各种文具、衣柜可以放各种衣服、鞋架可以放各种鞋子一样。我们现在所说的容器存放的东西可能更偏向于应用比如网站、程序甚至是系统环境。 -![认识容器](https://my-blog-to-use.oss-cn-beijing.aliyuncs.com/2019-7/container.png) +![认识容器](https://oss.javaguide.cn/github/javaguide/tools/docker/container.png) -### 1.2 图解物理机,虚拟机与容器 +### 图解物理机,虚拟机与容器 关于虚拟机与容器的对比在后面会详细介绍到,这里只是通过网上的图片加深大家对于物理机、虚拟机与容器这三者的理解(下面的图片来源于网络)。 **物理机:** -![物理机](https://my-blog-to-use.oss-cn-beijing.aliyuncs.com/2019-7/物理机图解.png) +![物理机](https://oss.javaguide.cn/github/javaguide/tools/docker/%E7%89%A9%E7%90%86%E6%9C%BA%E5%9B%BE%E8%A7%A3.jpeg) **虚拟机:** -![虚拟机](https://my-blog-to-use.oss-cn-beijing.aliyuncs.com/2019-7/虚拟机图解.png) +![虚拟机](https://oss.javaguide.cn/github/javaguide/tools/docker/%E8%99%9A%E6%8B%9F%E6%9C%BA%E5%9B%BE%E8%A7%A3.jpeg) **容器:** @@ -45,87 +45,73 @@ tag: 通过上面这三张抽象图,我们可以大概通过类比概括出:**容器虚拟化的是操作系统而不是硬件,容器之间是共享同一套操作系统资源的。虚拟机技术是虚拟出一套硬件后,在其上运行一个完整操作系统。因此容器的隔离级别会稍低一些。** ---- - -**相信通过上面的解释大家对于容器这个既陌生又熟悉的概念有了一个初步的认识,下面我们就来谈谈 Docker 的一些概念。** - -## 二 再来谈谈 Docker 的一些概念 - -### 2.1 什么是 Docker? - -说实话关于 Docker 是什么并太好说,下面我通过四点向你说明 Docker 到底是个什么东西。 - -- **Docker 是世界领先的软件容器平台。** -- **Docker** 使用 Google 公司推出的 **Go 语言** 进行开发实现,基于 **Linux 内核** 提供的 CGroup 功能和 namespace 来实现的,以及 AUFS 类的 **UnionFS** 等技术,**对进程进行封装隔离,属于操作系统层面的虚拟化技术。** 由于隔离的进程独立于宿主和其它的隔离的进程,因此也称其为容器。 -- **Docker 能够自动执行重复性任务,例如搭建和配置开发环境,从而解放了开发人员以便他们专注在真正重要的事情上:构建杰出的软件。** -- **用户可以方便地创建和使用容器,把自己的应用放入容器。容器还可以进行版本管理、复制、分享、修改,就像管理普通的代码一样。** +### 容器 VS 虚拟机 -### 2.2 Docker 思想 +每当说起容器,我们不得不将其与虚拟机做一个比较。就我而言,对于两者无所谓谁会取代谁,而是两者可以和谐共存。 -- **集装箱** -- **标准化:** ① 运输方式 ② 存储方式 ③ API 接口 -- **隔离** +简单来说:**容器和虚拟机具有相似的资源隔离和分配优势,但功能有所不同,因为容器虚拟化的是操作系统,而不是硬件,因此容器更容易移植,效率也更高。** -### 2.3 Docker 容器的特点 +传统虚拟机技术是虚拟出一套硬件后,在其上运行一个完整操作系统,在该系统上再运行所需应用进程;而容器内的应用进程直接运行于宿主的内核,容器内没有自己的内核,而且也没有进行硬件虚拟。因此容器要比传统虚拟机更为轻便。 -- **轻量** : 在一台机器上运行的多个 Docker 容器可以共享这台机器的操作系统内核;它们能够迅速启动,只需占用很少的计算和内存资源。镜像是通过文件系统层进行构造的,并共享一些公共文件。这样就能尽量降低磁盘用量,并能更快地下载镜像。 -- **标准** : Docker 容器基于开放式标准,能够在所有主流 Linux 版本、Microsoft Windows 以及包括 VM、裸机服务器和云在内的任何基础设施上运行。 -- **安全** : Docker 赋予应用的隔离性不仅限于彼此隔离,还独立于底层的基础设施。Docker 默认提供最强的隔离,因此应用出现问题,也只是单个容器的问题,而不会波及到整台机器。 +![](https://oss.javaguide.cn/javaguide/2e2b95eebf60b6d03f6c1476f4d7c697.png) -### 2.4 为什么要用 Docker ? +**容器和虚拟机的对比**: -- **Docker 的镜像提供了除内核外完整的运行时环境,确保了应用运行环境一致性,从而不会再出现 “这段代码在我机器上没问题啊” 这类问题;——一致的运行环境** -- **可以做到秒级、甚至毫秒级的启动时间。大大的节约了开发、测试、部署的时间。——更快速的启动时间** -- **避免公用的服务器,资源会容易受到其他用户的影响。——隔离性** -- **善于处理集中爆发的服务器使用压力;——弹性伸缩,快速扩展** -- **可以很轻易的将在一个平台上运行的应用,迁移到另一个平台上,而不用担心运行环境的变化导致应用无法正常运行的情况。——迁移方便** -- **使用 Docker 可以通过定制应用镜像来实现持续集成、持续交付、部署。——持续交付和部署** +![](https://oss.javaguide.cn/javaguide/4ef8691d67eb1eb53217099d0a691eb5.png) ---- +- 容器是一个应用层抽象,用于将代码和依赖资源打包在一起。 多个容器可以在同一台机器上运行,共享操作系统内核,但各自作为独立的进程在用户空间中运行 。与虚拟机相比, **容器占用的空间较少**(容器镜像大小通常只有几十兆),**瞬间就能完成启动** 。 -## 三 容器 VS 虚拟机 +- 虚拟机 (VM) 是一个物理硬件层抽象,用于将一台服务器变成多台服务器。管理程序允许多个 VM 在一台机器上运行。每个 VM 都包含一整套操作系统、一个或多个应用、必要的二进制文件和库资源,因此 **占用大量空间** 。而且 VM **启动也十分缓慢** 。 -**每当说起容器,我们不得不将其与虚拟机做一个比较。就我而言,对于两者无所谓谁会取代谁,而是两者可以和谐共存。** +通过 Docker 官网,我们知道了这么多 Docker 的优势,但是大家也没有必要完全否定虚拟机技术,因为两者有不同的使用场景。**虚拟机更擅长于彻底隔离整个运行环境**。例如,云服务提供商通常采用虚拟机技术隔离不同的用户。而 **Docker 通常用于隔离不同的应用** ,例如前端,后端以及数据库。 -简单来说:**容器和虚拟机具有相似的资源隔离和分配优势,但功能有所不同,因为容器虚拟化的是操作系统,而不是硬件,因此容器更容易移植,效率也更高。** +就我而言,对于两者无所谓谁会取代谁,而是两者可以和谐共存。 -### 3.1 两者对比图 +![](https://oss.javaguide.cn/javaguide/056c87751b9dd7b56f4264240fe96d00.png) -传统虚拟机技术是虚拟出一套硬件后,在其上运行一个完整操作系统,在该系统上再运行所需应用进程;而容器内的应用进程直接运行于宿主的内核,容器内没有自己的内核,而且也没有进行硬件虚拟。因此容器要比传统虚拟机更为轻便。 +## Docker 介绍 -![](https://oss.javaguide.cn/javaguide/2e2b95eebf60b6d03f6c1476f4d7c697.png) +### 什么是 Docker? -### 3.2 容器与虚拟机总结 +说实话关于 Docker 是什么并不太好说,下面我通过四点向你说明 Docker 到底是个什么东西。 -![](https://oss.javaguide.cn/javaguide/4ef8691d67eb1eb53217099d0a691eb5.png) +- **Docker 是世界领先的软件容器平台。** +- **Docker** 使用 Google 公司推出的 **Go 语言** 进行开发实现,基于 **Linux 内核** 提供的 CGroup 功能和 namespace 来实现的,以及 AUFS 类的 **UnionFS** 等技术,**对进程进行封装隔离,属于操作系统层面的虚拟化技术。** 由于隔离的进程独立于宿主和其它的隔离的进程,因此也称其为容器。 +- Docker 能够自动执行重复性任务,例如搭建和配置开发环境,从而解放了开发人员以便他们专注在真正重要的事情上:构建杰出的软件。 +- 用户可以方便地创建和使用容器,把自己的应用放入容器。容器还可以进行版本管理、复制、分享、修改,就像管理普通的代码一样。 -- **容器是一个应用层抽象,用于将代码和依赖资源打包在一起。** **多个容器可以在同一台机器上运行,共享操作系统内核,但各自作为独立的进程在用户空间中运行** 。与虚拟机相比, **容器占用的空间较少**(容器镜像大小通常只有几十兆),**瞬间就能完成启动** 。 +**Docker 思想**: -- **虚拟机 (VM) 是一个物理硬件层抽象,用于将一台服务器变成多台服务器。** 管理程序允许多个 VM 在一台机器上运行。每个 VM 都包含一整套操作系统、一个或多个应用、必要的二进制文件和库资源,因此 **占用大量空间** 。而且 VM **启动也十分缓慢** 。 +- **集装箱**:就像海运中的集装箱一样,Docker 容器包含了应用程序及其所有依赖项,确保在任何环境中都能以相同的方式运行。 +- **标准化**:运输方式、存储方式、API 接口。 +- **隔离**:每个 Docker 容器都在自己的隔离环境中运行,与宿主机和其他容器隔离。 -通过 Docker 官网,我们知道了这么多 Docker 的优势,但是大家也没有必要完全否定虚拟机技术,因为两者有不同的使用场景。**虚拟机更擅长于彻底隔离整个运行环境**。例如,云服务提供商通常采用虚拟机技术隔离不同的用户。而 **Docker 通常用于隔离不同的应用** ,例如前端,后端以及数据库。 +### Docker 容器的特点 -### 3.3 容器与虚拟机两者是可以共存的 +- **轻量** : 在一台机器上运行的多个 Docker 容器可以共享这台机器的操作系统内核;它们能够迅速启动,只需占用很少的计算和内存资源。镜像是通过文件系统层进行构造的,并共享一些公共文件。这样就能尽量降低磁盘用量,并能更快地下载镜像。 +- **标准** : Docker 容器基于开放式标准,能够在所有主流 Linux 版本、Microsoft Windows 以及包括 VM、裸机服务器和云在内的任何基础设施上运行。 +- **安全** : Docker 赋予应用的隔离性不仅限于彼此隔离,还独立于底层的基础设施。Docker 默认提供最强的隔离,因此应用出现问题,也只是单个容器的问题,而不会波及到整台机器。 -就我而言,对于两者无所谓谁会取代谁,而是两者可以和谐共存。 +### 为什么要用 Docker ? -![](https://oss.javaguide.cn/javaguide/056c87751b9dd7b56f4264240fe96d00.png) +- Docker 的镜像提供了除内核外完整的运行时环境,确保了应用运行环境一致性,从而不会再出现 “这段代码在我机器上没问题啊” 这类问题;——一致的运行环境 +- 可以做到秒级、甚至毫秒级的启动时间。大大的节约了开发、测试、部署的时间。——更快速的启动时间 +- 避免公用的服务器,资源会容易受到其他用户的影响。——隔离性 +- 善于处理集中爆发的服务器使用压力;——弹性伸缩,快速扩展 +- 可以很轻易的将在一个平台上运行的应用,迁移到另一个平台上,而不用担心运行环境的变化导致应用无法正常运行的情况。——迁移方便 +- 使用 Docker 可以通过定制应用镜像来实现持续集成、持续交付、部署。——持续交付和部署 --- -## 四 Docker 基本概念 - -**Docker 中有非常重要的三个基本概念,理解了这三个概念,就理解了 Docker 的整个生命周期。** +## Docker 基本概念 -- **镜像(Image)** -- **容器(Container)** -- **仓库(Repository)** +Docker 中有非常重要的三个基本概念:镜像(Image)、容器(Container)和仓库(Repository)。 -理解了这三个概念,就理解了 Docker 的整个生命周期 +理解了这三个概念,就理解了 Docker 的整个生命周期。 -![docker基本概念](https://my-blog-to-use.oss-cn-beijing.aliyuncs.com/2019-7/docker基本概念.png) +![](https://oss.javaguide.cn/github/javaguide/tools/docker/docker-build-run.jpeg) -### 4.1 镜像(Image):一个特殊的文件系统 +### 镜像(Image):一个特殊的文件系统 **操作系统分为内核和用户空间**。对于 Linux 而言,内核启动后,会挂载 root 文件系统为其提供用户空间支持。而 Docker 镜像(Image),就相当于是一个 root 文件系统。 @@ -137,7 +123,7 @@ Docker 设计时,就充分利用 **Union FS** 的技术,将其设计为**分 分层存储的特征还使得镜像的复用、定制变的更为容易。甚至可以用之前构建好的镜像作为基础层,然后进一步添加新的层,以定制自己所需的内容,构建新的镜像。 -### 4.2 容器(Container):镜像运行时的实体 +### 容器(Container):镜像运行时的实体 镜像(Image)和容器(Container)的关系,就像是面向对象程序设计中的 类 和 实例 一样,镜像是静态的定义,**容器是镜像运行时的实体。容器可以被创建、启动、停止、删除、暂停等** 。 @@ -147,7 +133,7 @@ Docker 设计时,就充分利用 **Union FS** 的技术,将其设计为**分 按照 Docker 最佳实践的要求,**容器不应该向其存储层内写入任何数据** ,容器存储层要保持无状态化。**所有的文件写入操作,都应该使用数据卷(Volume)、或者绑定宿主目录**,在这些位置的读写会跳过容器存储层,直接对宿主(或网络存储)发生读写,其性能和稳定性更高。数据卷的生存周期独立于容器,容器消亡,数据卷不会消亡。因此, **使用数据卷后,容器可以随意删除、重新 run ,数据却不会丢失。** -### 4.3 仓库(Repository):集中存放镜像文件的地方 +### 仓库(Repository):集中存放镜像文件的地方 镜像构建完成后,可以很容易的在当前宿主上运行,但是, **如果需要在其它服务器上使用这个镜像,我们就需要一个集中的存储、分发镜像的服务,Docker Registry 就是这样的服务。** @@ -165,7 +151,7 @@ Docker 设计时,就充分利用 **Union FS** 的技术,将其设计为**分 比如我们想要搜索自己想要的镜像: -![利用Docker Hub 搜索镜像](https://my-blog-to-use.oss-cn-beijing.aliyuncs.com/2019-7/Screen%20Shot%202019-11-04%20at%208.21.39%20PM.png) +![利用Docker Hub 搜索镜像](https://oss.javaguide.cn/github/javaguide/tools/docker/Screen%20Shot%202019-11-04%20at%208.21.39%20PM.png) 在 Docker Hub 的搜索结果中,有几项关键的信息有助于我们选择合适的镜像: @@ -183,15 +169,40 @@ mariadb MariaDB is a community-developed fork of MyS mysql/mysql-server Optimized MySQL Server Docker images. Create… 650 [OK] ``` -在国内访问**Docker Hub** 可能会比较慢国内也有一些云服务商提供类似于 Docker Hub 的公开服务。比如 [时速云镜像库](https://www.tenxcloud.com/ "时速云镜像库")、[网易云镜像服务](https://www.163yun.com/product/repo "网易云镜像服务")、[DaoCloud 镜像市场](https://www.daocloud.io/ "DaoCloud 镜像市场")、[阿里云镜像库](https://www.aliyun.com/product/containerservice?utm_content=se_1292836 "阿里云镜像库")等。 +在国内访问 **Docker Hub** 可能会比较慢国内也有一些云服务商提供类似于 Docker Hub 的公开服务。比如 [时速云镜像库](https://www.tenxcloud.com/ "时速云镜像库")、[网易云镜像服务](https://www.163yun.com/product/repo "网易云镜像服务")、[DaoCloud 镜像市场](https://www.daocloud.io/ "DaoCloud 镜像市场")、[阿里云镜像库](https://www.aliyun.com/product/containerservice?utm_content=se_1292836 "阿里云镜像库")等。 -除了使用公开服务外,用户还可以在 **本地搭建私有 Docker Registry** 。Docker 官方提供了 Docker Registry 镜像,可以直接使用做为私有 Registry 服务。开源的 Docker Registry 镜像只提供了 Docker Registry API 的服务端实现,足以支持 docker 命令,不影响使用。但不包含图形界面,以及镜像维护、用户管理、访问控制等高级功能。 +除了使用公开服务外,用户还可以在 **本地搭建私有 Docker Registry** 。Docker 官方提供了 Docker Registry 镜像,可以直接使用做为私有 Registry 服务。开源的 Docker Registry 镜像只提供了 Docker Registry API 的服务端实现,足以支持 Docker 命令,不影响使用。但不包含图形界面,以及镜像维护、用户管理、访问控制等高级功能。 ---- +### Image、Container 和 Repository 的关系 + +下面这一张图很形象地展示了 Image、Container、Repository 和 Registry/Hub 这四者的关系: + +![Docker 架构](https://oss.javaguide.cn/github/javaguide/tools/docker/docker-regitstry.png) + +- Dockerfile 是一个文本文件,包含了一系列的指令和参数,用于定义如何构建一个 Docker 镜像。运行 `docker build`命令并指定一个 Dockerfile 时,Docker 会读取 Dockerfile 中的指令,逐步构建一个新的镜像,并将其保存在本地。 +- `docker pull` 命令可以从指定的 Registry/Hub 下载一个镜像到本地,默认使用 Docker Hub。 +- `docker run` 命令可以从本地镜像创建一个新的容器并启动它。如果本地没有镜像,Docker 会先尝试从 Registry/Hub 拉取镜像。 +- `docker push` 命令可以将本地的 Docker 镜像上传到指定的 Registry/Hub。 -## 五 常见命令 +上面涉及到了一些 Docker 的基本命令,后面会详细介绍大。 -### 5.1 基本命令 +### Build Ship and Run + +Docker 的概念基本上已经讲完,我们再来谈谈:Build, Ship, and Run。 + +如果你搜索 Docker 官网,会发现如下的字样:**“Docker - Build, Ship, and Run Any App, Anywhere”**。那么 Build, Ship, and Run 到底是在干什么呢? + +![](https://oss.javaguide.cn/github/javaguide/tools/docker/docker-build-ship-run.jpg) + +- **Build(构建镜像)**:镜像就像是集装箱包括文件以及运行环境等等资源。 +- **Ship(运输镜像)**:主机和仓库间运输,这里的仓库就像是超级码头一样。 +- **Run (运行镜像)**:运行的镜像就是一个容器,容器就是运行程序的地方。 + +Docker 运行过程也就是去仓库把镜像拉到本地,然后用一条命令把镜像运行起来变成容器。所以,我们也常常将 Docker 称为码头工人或码头装卸工,这和 Docker 的中文翻译搬运工人如出一辙。 + +## Docker 常见命令 + +### 基本命令 ```bash docker version # 查看docker版本 @@ -201,7 +212,9 @@ docker ps #查看正在运行的容器 docker image prune # 清理临时的、没有被使用的镜像文件。-a, --all: 删除所有没有用的镜像,而不仅仅是临时文件; ``` -### 5.2 拉取镜像 +### 拉取镜像 + +`docker pull` 命令默认使用的 Registry/Hub 是 Docker Hub。当你执行 docker pull 命令而没有指定任何 Registry/Hub 的地址时,Docker 会从 Docker Hub 拉取镜像。 ```bash docker search mysql # 查看mysql相关镜像 @@ -209,7 +222,19 @@ docker pull mysql:5.7 # 拉取mysql镜像 docker image ls # 查看所有已下载镜像 ``` -### 5.3 删除镜像 +### 构建镜像 + +运行 `docker build`命令并指定一个 Dockerfile 时,Docker 会读取 Dockerfile 中的指令,逐步构建一个新的镜像,并将其保存在本地。 + +```bash +# +# imageName 是镜像名称,1.0.0 是镜像的版本号或标签 +docker build -t imageName:1.0.0 . +``` + +需要注意:Dockerfile 的文件名不必须为 Dockerfile,也不一定要放在构建上下文的根目录中。使用 `-f` 或 `--file` 选项,可以指定任何位置的任何文件作为 Dockerfile。当然,一般大家习惯性的会使用默认的文件名 `Dockerfile`,以及会将其置于镜像构建上下文目录中。 + +### 删除镜像 比如我们要删除我们下载的 mysql 镜像。 @@ -237,31 +262,194 @@ mysql 5.7 f6509bac4980 3 months ago docker rmi f6509bac4980 # 或者 docker rmi mysql ``` -## 六 Build Ship and Run +### 镜像推送 -**Docker 的概念以及常见命令基本上已经讲完,我们再来谈谈:Build, Ship, and Run。** +`docker push` 命令用于将本地的 Docker 镜像上传到指定的 Registry/Hub。 -如果你搜索 Docker 官网,会发现如下的字样:**“Docker - Build, Ship, and Run Any App, Anywhere”**。那么 Build, Ship, and Run 到底是在干什么呢? +```bash +# 将镜像推送到私有镜像仓库 Harbor +# harbor.example.com是私有镜像仓库的地址,ubuntu是镜像的名称,18.04是镜像的版本标签 +docker push harbor.example.com/ubuntu:18.04 +``` -![](https://oscimg.oschina.net/oscnet/up-4123a5154118e1aaaf6e5a01286f463a1e2.png) +镜像推送之前,要确保本地已经构建好需要推送的 Docker 镜像。另外,务必先登录到对应的镜像仓库。 -- **Build(构建镜像)**:镜像就像是集装箱包括文件以及运行环境等等资源。 -- **Ship(运输镜像)**:主机和仓库间运输,这里的仓库就像是超级码头一样。 -- **Run (运行镜像)**:运行的镜像就是一个容器,容器就是运行程序的地方。 +## Docker 数据管理 -**Docker 运行过程也就是去仓库把镜像拉到本地,然后用一条命令把镜像运行起来变成容器。所以,我们也常常将 Docker 称为码头工人或码头装卸工,这和 Docker 的中文翻译搬运工人如出一辙。** +在容器中管理数据主要有两种方式: -## 七 简单了解一下 Docker 底层原理 +1. 数据卷(Volumes) +2. 挂载主机目录 (Bind mounts) -### 7.1 虚拟化技术 +![Docker 数据管理](https://oss.javaguide.cn/github/javaguide/tools/docker/docker-data-management.png) -首先,Docker **容器虚拟化**技术为基础的软件,那么什么是虚拟化技术呢? +数据卷是由 Docker 管理的数据存储区域,有如下这些特点: -简单点来说,虚拟化技术可以这样定义: +- 可以在容器之间共享和重用。 +- 即使容器被删除,数据卷中的数据也不会被自动删除,从而确保数据的持久性。 +- 对数据卷的修改会立马生效。 +- 对数据卷的更新,不会影响镜像。 + +```bash +# 创建一个数据卷 +docker volume create my-vol +# 查看所有的数据卷 +docker volume ls +# 查看数据卷的具体信息 +docker inspect web +# 删除指定的数据卷 +docker volume rm my-vol +``` + +在用 `docker run` 命令的时候,使用 `--mount` 标记来将一个或多个数据卷挂载到容器里。 + +还可以通过 `--mount` 标记将宿主机上的文件或目录挂载到容器中,这使得容器可以直接访问宿主机的文件系统。Docker 挂载主机目录的默认权限是读写,用户也可以通过增加 `readonly` 指定为只读。 + +## Docker Compose + +### 什么是 Docker Compose?有什么用? + +Docker Compose 是 Docker 官方编排(Orchestration)项目之一,基于 Python 编写,负责实现对 Docker 容器集群的快速编排。通过 Docker Compose,开发者可以使用 YAML 文件来配置应用的所有服务,然后只需一个简单的命令即可创建和启动所有服务。 + +Docker Compose 是开源项目,地址:。 + +Docker Compose 的核心功能: + +- **多容器管理**:允许用户在一个 YAML 文件中定义和管理多个容器。 +- **服务编排**:配置容器间的网络和依赖关系。 +- **一键部署**:通过简单的命令,如`docker-compose up`和`docker-compose down`,可以轻松地启动和停止整个应用程序。 + +Docker Compose 简化了多容器应用程序的开发、测试和部署过程,提高了开发团队的生产力,同时降低了应用程序的部署复杂度和管理成本。 + +### Docker Compose 文件基本结构 + +Docker Compose 文件是 Docker Compose 工具的核心,用于定义和配置多容器 Docker 应用。这个文件通常命名为 `docker-compose.yml`,采用 YAML(YAML Ain't Markup Language)格式编写。 + +Docker Compose 文件基本结构如下: + +- **版本(version):** 指定 Compose 文件格式的版本。版本决定了可用的配置选项。 +- **服务(services):** 定义了应用中的每个容器(服务)。每个服务可以使用不同的镜像、环境设置和依赖关系。 + - **镜像(image):** 从指定的镜像中启动容器,可以是存储仓库、标签以及镜像 ID。 + - **命令(command):** 可选,覆盖容器启动后默认执行的命令。在启动服务时运行特定的命令或脚本,常用于启动应用程序、执行初始化脚本等。 + - **端口(ports):** 可选,映射容器和宿主机的端口。 + - **依赖(depends_on):** 依赖配置的选项,意思是如果服务启动是如果有依赖于其他服务的,先启动被依赖的服务,启动完成后在启动该服务。 + - **环境变量(environment):** 可选,设置服务运行所需的环境变量。 + - **重启(restart):** 可选,控制容器的重启策略。在容器退出时,根据指定的策略自动重启容器。 + - **服务卷(volumes):** 可选,定义服务使用的卷,用于数据持久化或在容器之间共享数据。 + - **构建(build):** 指定构建镜像的 dockerfile 的上下文路径,或者详细配置对象。 +- **网络(networks):** 定义了容器间的网络连接。 +- **卷(volumes):** 用于数据持久化和共享的数据卷定义。常用于数据库存储、配置文件、日志等数据的持久化。 + +```yaml +version: "3.8" # 定义版本, 表示当前使用的 docker-compose 语法的版本 +services: # 服务,可以存在多个 + servicename1: # 服务名字,它也是内部 bridge 网络可以使用的 DNS name,如果不是集群模式相当于 docker run 的时候指定的一个名称, + #集群(Swarm)模式是多个容器的逻辑抽象 + image: # 镜像的名字 + command: # 可选,如果设置,则会覆盖默认镜像里的 CMD 命令 + environment: # 可选,等价于 docker container run 里的 --env 选项设置环境变量 + volumes: # 可选,等价于 docker container run 里的 -v 选项 绑定数据卷 + networks: # 可选,等价于 docker container run 里的 --network 选项指定网络 + ports: # 可选,等价于 docker container run 里的 -p 选项指定端口映射 + restart: # 可选,控制容器的重启策略 + build: #构建目录 + depends_on: #服务依赖配置 + servicename2: + image: + command: + networks: + ports: + servicename3: + #... +volumes: # 可选,需要创建的数据卷,类似 docker volume create + db_data: +networks: # 可选,等价于 docker network create +``` + +### Docker Compose 常见命令 + +#### 启动 + +`docker-compose up`会根据 `docker-compose.yml` 文件中定义的服务来创建和启动容器,并将它们连接到默认的网络中。 + +```bash +# 在当前目录下寻找 docker-compose.yml 文件,并根据其中定义的服务启动应用程序 +docker-compose up +# 后台启动 +docker-compose up -d +# 强制重新创建所有容器,即使它们已经存在 +docker-compose up --force-recreate +# 重新构建镜像 +docker-compose up --build +# 指定要启动的服务名称,而不是启动所有服务 +# 可以同时指定多个服务,用空格分隔。 +docker-compose up service_name +``` -> 虚拟化技术是一种资源管理技术,是将计算机的各种[实体资源](https://zh.wikipedia.org/wiki/資源_(計算機科學 "实体资源"))([CPU](https://zh.wikipedia.org/wiki/CPU "CPU")、[内存](https://zh.wikipedia.org/wiki/内存 "内存")、[磁盘空间](https://zh.wikipedia.org/wiki/磁盘空间 "磁盘空间")、[网络适配器](https://zh.wikipedia.org/wiki/網路適配器 "网络适配器")等),予以抽象、转换后呈现出来并可供分割、组合为一个或多个电脑配置环境。由此,打破实体结构间的不可切割的障碍,使用户可以比原本的配置更好的方式来应用这些电脑硬件资源。这些资源的新虚拟部分是不受现有资源的架设方式,地域或物理配置所限制。一般所指的虚拟化资源包括计算能力和数据存储。 +另外,如果 Compose 文件名称不是 `docker-compose.yml` 也没问题,可以通过 `-f` 参数指定。 -### 7.2 Docker 基于 LXC 虚拟容器技术 +```bash +docker-compose -f docker-compose.prod.yml up +``` + +#### 暂停 + +`docker-compose down`用于停止并移除通过 `docker-compose up` 启动的容器和网络。 + +```bash +# 在当前目录下寻找 docker-compose.yml 文件 +# 根据其中定义移除启动的所有容器,网络和卷。 +docker-compose down +# 停止容器但不移除 +docker-compose down --stop +# 指定要停止和移除的特定服务,而不是停止和移除所有服务 +# 可以同时指定多个服务,用空格分隔。 +docker-compose down service_name +``` + +同样地,如果 Compose 文件名称不是 `docker-compose.yml` 也没问题,可以通过 `-f` 参数指定。 + +```bash +docker-compose -f docker-compose.prod.yml down +``` + +#### 查看 + +`docker-compose ps`用于查看通过 `docker-compose up` 启动的所有容器的状态信息。 + +```bash +# 查看所有容器的状态信息 +docker-compose ps +# 只显示服务名称 +docker-compose ps --services +# 查看指定服务的容器 +docker-compose ps service_name +``` + +#### 其他 + +| 命令 | 介绍 | +| ------------------------ | ---------------------- | +| `docker-compose version` | 查看版本 | +| `docker-compose images` | 列出所有容器使用的镜像 | +| `docker-compose kill` | 强制停止服务的容器 | +| `docker-compose exec` | 在容器中执行命令 | +| `docker-compose logs` | 查看日志 | +| `docker-compose pause` | 暂停服务 | +| `docker-compose unpause` | 恢复服务 | +| `docker-compose push` | 推送服务镜像 | +| `docker-compose start` | 启动当前停止的某个容器 | +| `docker-compose stop` | 停止当前运行的某个容器 | +| `docker-compose rm` | 删除服务停止的容器 | +| `docker-compose top` | 查看进程 | + +## Docker 底层原理 + +首先,Docker 是基于轻量级虚拟化技术的软件,那什么是虚拟化技术呢? + +简单点来说,虚拟化技术可以这样定义: + +> 虚拟化技术是一种资源管理技术,是将计算机的各种[实体资源](https://zh.wikipedia.org/wiki/計算機科學 "实体资源"))([CPU](https://zh.wikipedia.org/wiki/CPU "CPU")、[内存](https://zh.wikipedia.org/wiki/内存 "内存")、[磁盘空间](https://zh.wikipedia.org/wiki/磁盘空间 "磁盘空间")、[网络适配器](https://zh.wikipedia.org/wiki/網路適配器 "网络适配器")等),予以抽象、转换后呈现出来并可供分割、组合为一个或多个电脑配置环境。由此,打破实体结构间的不可切割的障碍,使用户可以比原本的配置更好的方式来应用这些电脑硬件资源。这些资源的新虚拟部分是不受现有资源的架设方式,地域或物理配置所限制。一般所指的虚拟化资源包括计算能力和数据存储。 Docker 技术是基于 LXC(Linux container- Linux 容器)虚拟容器技术的。 @@ -273,9 +461,9 @@ LXC 技术主要是借助 Linux 内核中提供的 CGroup 功能和 namespace - **namespace 是 Linux 内核用来隔离内核资源的方式。** 通过 namespace 可以让一些进程只能看到与自己相关的一部分资源,而另外一些进程也只能看到与它们自己相关的资源,这两拨进程根本就感觉不到对方的存在。具体的实现方式是把一个或多个进程的相关资源指定在同一个 namespace 中。Linux namespaces 是对全局系统资源的一种封装隔离,使得处于不同 namespace 的进程拥有独立的全局系统资源,改变一个 namespace 中的系统资源只会影响当前 namespace 里的进程,对其他 namespace 中的进程没有影响。 - (以上关于 namespace 介绍内容来自 ,更多关于 namespace 的呢内容可以查看这篇文章 )。 + (以上关于 namespace 介绍内容来自 ,更多关于 namespace 的内容可以查看这篇文章 )。 -- **CGroup 是 Control Groups 的缩写,是 Linux 内核提供的一种可以限制、记录、隔离进程组 (process groups) 所使用的物力资源 (如 cpu memory i/o 等等) 的机制。** +- **CGroup 是 Control Groups 的缩写,是 Linux 内核提供的一种可以限制、记录、隔离进程组 (process groups) 所使用的物理资源 (如 cpu memory i/o 等等) 的机制。** (以上关于 CGroup 介绍内容来自 ,更多关于 CGroup 的内容可以查看这篇文章 )。 @@ -283,17 +471,19 @@ LXC 技术主要是借助 Linux 内核中提供的 CGroup 功能和 namespace 两者都是将进程进行分组,但是两者的作用还是有本质区别。namespace 是为了隔离进程组之间的资源,而 cgroup 是为了对一组进程进行统一的资源监控和限制。 -## 八 总结 +## 总结 -本文主要把 Docker 中的一些常见概念做了详细的阐述,但是并不涉及 Docker 的安装、镜像的使用、容器的操作等内容。这部分东西,希望读者自己可以通过阅读书籍与官方文档的形式掌握。如果觉得官方文档阅读起来很费力的话,这里推荐一本书籍《Docker 技术入门与实战第二版》。 +本文主要把 Docker 中的一些常见概念和命令做了详细的阐述。从零到上手实战可以看[Docker 从入门到上手干事](https://javaguide.cn/tools/docker/docker-in-action.html)这篇文章,内容非常详细! -## 九 推荐阅读 +另外,再给大家推荐一本质量非常高的开源书籍[《Docker 从入门到实践》](https://yeasy.gitbook.io/docker_practice/introduction/why "《Docker 从入门到实践》") ,这本书的内容非常新,毕竟书籍的内容是开源的,可以随时改进。 -- [10 分钟看懂 Docker 和 K8S](https://zhuanlan.zhihu.com/p/53260098 "10分钟看懂Docker和K8S") -- [从零开始入门 K8s:详解 K8s 容器基本概念](https://www.infoq.cn/article/te70FlSyxhltL1Cr7gzM "从零开始入门 K8s:详解 K8s 容器基本概念") +![《Docker 从入门到实践》网站首页](https://oss.javaguide.cn/github/javaguide/tools/docker/docker-getting-started-practice-website-homepage.png) -## 十 参考 +## 参考 +- [Docker Compose:从零基础到实战应用的全面指南](https://juejin.cn/post/7306756690727747610) - [Linux Namespace 和 Cgroup](https://segmentfault.com/a/1190000009732550 "Linux Namespace和Cgroup") - [LXC vs Docker: Why Docker is Better](https://www.upguard.com/articles/docker-vs-lxc "LXC vs Docker: Why Docker is Better") - [CGroup 介绍、应用实例及原理描述](https://www.ibm.com/developerworks/cn/linux/1506_cgroup/index.html "CGroup 介绍、应用实例及原理描述") + + diff --git a/docs/tools/git/git-intro.md b/docs/tools/git/git-intro.md index 391ee2bcf37..c2cf8000570 100644 --- a/docs/tools/git/git-intro.md +++ b/docs/tools/git/git-intro.md @@ -21,7 +21,7 @@ tag: 为了解决这个问题,人们很久以前就开发了许多种本地版本控制系统,大多都是采用某种简单的数据库来记录文件的历次更新差异。 -![本地版本控制系统](https://my-blog-to-use.oss-cn-beijing.aliyuncs.com/2019-3/本地版本控制系统.png) +![本地版本控制系统](https://oss.javaguide.cn/github/javaguide/tools/git/%E6%9C%AC%E5%9C%B0%E7%89%88%E6%9C%AC%E6%8E%A7%E5%88%B6%E7%B3%BB%E7%BB%9F.png) ### 集中化的版本控制系统 @@ -29,7 +29,7 @@ tag: 集中化的版本控制系统都有一个单一的集中管理的服务器,保存所有文件的修订版本,而协同工作的人们都通过客户端连到这台服务器,取出最新的文件或者提交更新。 -![集中化的版本控制系统](https://my-blog-to-use.oss-cn-beijing.aliyuncs.com/2019-3/集中化的版本控制系统.png) +![集中化的版本控制系统](https://oss.javaguide.cn/github/javaguide/tools/git/%E9%9B%86%E4%B8%AD%E5%8C%96%E7%9A%84%E7%89%88%E6%9C%AC%E6%8E%A7%E5%88%B6%E7%B3%BB%E7%BB%9F.png) 这么做虽然解决了本地版本控制系统无法让在不同系统上的开发者协同工作的诟病,但也还是存在下面的问题: @@ -42,7 +42,7 @@ tag: 这类系统,客户端并不只提取最新版本的文件快照,而是把代码仓库完整地镜像下来。 这么一来,任何一处协同工作用的服务器发生故障,事后都可以用任何一个镜像出来的本地仓库恢复。 因为每一次的克隆操作,实际上都是一次对代码仓库的完整备份。 -![分布式版本控制系统](https://my-blog-to-use.oss-cn-beijing.aliyuncs.com/2019-3/分布式版本控制系统.png) +![分布式版本控制系统](https://oss.javaguide.cn/github/javaguide/tools/git/%E5%88%86%E5%B8%83%E5%BC%8F%E7%89%88%E6%9C%AC%E6%8E%A7%E5%88%B6%E7%B3%BB%E7%BB%9F.png) 分布式版本控制系统可以不用联网就可以工作,因为每个人的电脑上都是完整的版本库,当你修改了某个文件后,你只需要将自己的修改推送给别人就可以了。但是,在实际使用分布式版本控制系统的时候,很少会直接进行推送修改,而是使用一台充当“中央服务器”的东西。这个服务器的作用仅仅是用来方便“交换”大家的修改,没有它大家也一样干活,只是交换修改不方便而已。 @@ -66,7 +66,7 @@ Git 在保存和对待各种信息的时候与其它版本控制系统有很大 具体原理如下图所示,理解起来其实很简单,每当我们提交更新一个文件之后,系统都会记录这个文件做了哪些更新,以增量符号 Δ(Delta)表示。 -![](https://my-blog-to-use.oss-cn-beijing.aliyuncs.com/2019-3deltas.png) +![](https://oss.javaguide.cn/github/javaguide/tools/git/2019-3deltas.png) **我们怎样才能得到一个文件的最终版本呢?** @@ -78,7 +78,7 @@ Git 在保存和对待各种信息的时候与其它版本控制系统有很大 Git 不按照以上方式对待或保存数据。 反之,Git 更像是把数据看作是对小型文件系统的一组快照。 每次你提交更新,或在 Git 中保存项目状态时,它主要对当时的全部文件制作一个快照并保存这个快照的索引。 为了高效,如果文件没有修改,Git 不再重新存储该文件,而是只保留一个链接指向之前存储的文件。 Git 对待数据更像是一个 **快照流**。 -![](https://my-blog-to-use.oss-cn-beijing.aliyuncs.com/2019-3snapshots.png) +![](https://oss.javaguide.cn/github/javaguide/tools/git/2019-3snapshots.png) ### Git 的三种状态 @@ -90,7 +90,7 @@ Git 有三种状态,你的文件可能处于其中之一: 由此引入 Git 项目的三个工作区域的概念:**Git 仓库(.git directory)**、**工作目录(Working Directory)** 以及 **暂存区域(Staging Area)** 。 -![](https://my-blog-to-use.oss-cn-beijing.aliyuncs.com/2019-3areas.png) +![](https://oss.javaguide.cn/github/javaguide/tools/git/2019-3areas.png) **基本的 Git 工作流程如下:** @@ -121,7 +121,7 @@ Git 有三种状态,你的文件可能处于其中之一: 一个好的 Git 提交消息如下: -``` +```plain 标题行:用这一行来描述和解释你的这次提交 主体部分可以是很少的几行,来加入更多的细节来解释提交,最好是能给出一些相关的背景或者解释这个提交能修复和解决什么问题。 @@ -200,7 +200,7 @@ git branch test git checkout test ``` -![](https://my-blog-to-use.oss-cn-beijing.aliyuncs.com/2019-3切换分支.png) +![](https://oss.javaguide.cn/github/javaguide/tools/git/2019-3%E5%88%87%E6%8D%A2%E5%88%86%E6%94%AF.png) 你也可以直接这样创建分支并切换过去(上面两条命令的合写) @@ -245,3 +245,5 @@ git push origin - [图解 Git](https://marklodato.github.io/visual-git-guide/index-zh-cn.html):图解 Git 中的最常用命令。如果你稍微理解 git 的工作原理,这篇文章能够让你理解的更透彻。 - [猴子都能懂得 Git 入门](https://backlog.com/git-tutorial/cn/intro/intro1_1.html):有趣的讲解。 - [Pro Git book](https://git-scm.com/book/zh/v2):国外的一本 Git 书籍,被翻译成多国语言,质量很高。 + + diff --git a/docs/tools/git/github-tips.md b/docs/tools/git/github-tips.md index 285288156f6..25df8592c71 100644 --- a/docs/tools/git/github-tips.md +++ b/docs/tools/git/github-tips.md @@ -127,7 +127,7 @@ Github 前段时间推出的 Codespaces 可以提供类似 VS Code 的在线 IDE 2. Githunb Topics 按照类别/话题将一些项目进行了分类汇总。比如 [Data visualization](https://github.com/topics/data-visualization) 汇总了数据可视化相关的一些开源项目,[Awesome Lists](https://github.com/topics/awesome) 汇总了 Awesome 系列的仓库; 3. 通过 Github Trending 我们可以看到最近比较热门的一些开源项目,我们可以按照语言类型以及时间维度对项目进行筛选; 4. Github Collections 类似一个收藏夹集合。比如 [Teaching materials for computational social science](https://github.com/collections/teaching-computational-social-science) 这个收藏夹就汇总了计算机课程相关的开源资源,[Learn to Code](https://github.com/collections/learn-to-code) 这个收藏夹就汇总了对你学习编程有帮助的一些仓库; -5. ...... +5. …… ![](https://oss.javaguide.cn/github/javaguide/github-explore.png) @@ -146,3 +146,5 @@ GitHub Actions 有一个官方市场,上面有非常多别人提交的 Actions 这一篇文章,我毫无保留地把自己这些年总结的 Github 小技巧分享了出来,真心希望对大家有帮助,真心希望大家一定要利用好 Github 这个专属程序员的宝藏。 另外,这篇文章中,我并没有提到 Github 搜索技巧。在我看来,Github 搜索技巧不必要记网上那些文章说的各种命令啥的,真没啥卵用。你会发现你用的最多的还是关键字搜索以及 Github 自带的筛选功能。 + + diff --git a/docs/tools/gradle/gradle-core-concepts.md b/docs/tools/gradle/gradle-core-concepts.md index 62eeb3ad459..7f0763c0fec 100644 --- a/docs/tools/gradle/gradle-core-concepts.md +++ b/docs/tools/gradle/gradle-core-concepts.md @@ -78,7 +78,7 @@ Gradle Wrapper 会给我们带来下面这些好处: ### 生成 Gradle Wrapper -如果想要生成 Gradle Wrapper 的话,需要本地配置好 Gradle 环境变量。Gradle 中已经内置了内置了 Wrapper Task,在项目根目录执行执行`gradle wrapper`命令即可帮助我们生成 Gradle Wrapper。 +如果想要生成 Gradle Wrapper 的话,需要本地配置好 Gradle 环境变量。Gradle 中已经内置了 Wrapper Task,在项目根目录执行执行`gradle wrapper`命令即可帮助我们生成 Gradle Wrapper。 执行命令 `gradle wrapper` 命令时可以指定一些参数来控制 wrapper 的生成。具体有如下两个配置参数: @@ -87,7 +87,7 @@ Gradle Wrapper 会给我们带来下面这些好处: 执行`gradle wrapper`命令之后,Gradle Wrapper 就生成完成了,项目根目录中生成如下文件: -``` +```plain ├── gradle │ └── wrapper │ ├── gradle-wrapper.jar @@ -302,3 +302,5 @@ Gradle 支持单项目和多项目构建。在初始化阶段,Gradle 确定哪 - 【Gradle】Gradle 的生命周期详解: - 手把手带你自定义 Gradle 插件 —— Gradle 系列(2): - Gradle 爬坑指南 -- 理解 Plugin、Task、构建流程: + + diff --git a/docs/tools/maven/maven-best-practices.md b/docs/tools/maven/maven-best-practices.md new file mode 100644 index 00000000000..0f682f46d1d --- /dev/null +++ b/docs/tools/maven/maven-best-practices.md @@ -0,0 +1,233 @@ +--- +title: Maven最佳实践 +category: 开发工具 +head: + - - meta + - name: keywords + content: Maven坐标,Maven仓库,Maven生命周期,Maven多模块管理 + - - meta + - name: description + content: Maven 是一种广泛使用的 Java 项目构建自动化工具。它简化了构建过程并帮助管理依赖关系,使开发人员的工作更轻松。在这篇博文中,我们将讨论一些最佳实践、提示和技巧,以优化我们在项目中对 Maven 的使用并改善我们的开发体验。 +--- + +> 本文由 JavaGuide 翻译并完善,原文地址: 。 + +Maven 是一种广泛使用的 Java 项目构建自动化工具。它简化了构建过程并帮助管理依赖关系,使开发人员的工作更轻松。Maven 详细介绍可以参考我写的这篇 [Maven 核心概念总结](./maven-core-concepts.md) 。 + +这篇文章不会涉及到 Maven 概念的介绍,主要讨论一些最佳实践、建议和技巧,以优化我们在项目中对 Maven 的使用并改善我们的开发体验。 + +## Maven 标准目录结构 + +Maven 遵循标准目录结构来保持项目之间的一致性。遵循这种结构可以让其他开发人员更轻松地理解我们的项目。 + +Maven 项目的标准目录结构如下: + +```groovy +src/ + main/ + java/ + resources/ + test/ + java/ + resources/ +pom.xml +``` + +- `src/main/java`:源代码目录 +- `src/main/resources`:资源文件目录 +- `src/test/java`:测试代码目录 +- `src/test/resources`:测试资源文件目录 + +这只是一个最简单的 Maven 项目目录示例。实际项目中,我们还会根据项目规范去做进一步的细分。 + +## 指定 Maven 编译器插件 + +默认情况下,Maven 使用 Java5 编译我们的项目。要使用不同的 JDK 版本,请在 `pom.xml` 文件中配置 Maven 编译器插件。 + +例如,如果你想要使用 Java8 来编译你的项目,你可以在``标签下添加以下的代码片段: + +```xml + + + + org.apache.maven.plugins + maven-compiler-plugin + 3.8.1 + + 1.8 + 1.8 + + + + +``` + +这样,Maven 就会使用 Java8 的编译器来编译你的项目。如果你想要使用其他版本的 JDK,你只需要修改``和``标签的值即可。例如,如果你想要使用 Java11,你可以将它们的值改为 11。 + +## 有效管理依赖关系 + +Maven 的依赖管理系统是其最强大的功能之一。在顶层 pom 文件中,通过标签 `dependencyManagement` 定义公共的依赖关系,这有助于避免冲突并确保所有模块使用相同版本的依赖项。 + +例如,假设我们有一个父模块和两个子模块 A 和 B,我们想要在所有模块中使用 JUnit 5.7.2 作为测试框架。我们可以在父模块的`pom.xml`文件中使用``标签来定义 JUnit 的版本: + +```xml + + + + org.junit.jupiter + junit-jupiter + 5.7.2 + test + + + +``` + +在子模块 A 和 B 的 `pom.xml` 文件中,我们只需要引用 JUnit 的 `groupId` 和 `artifactId` 即可: + +```xml + + + org.junit.jupiter + junit-jupiter + + +``` + +## 针对不同环境使用配置文件 + +Maven 配置文件允许我们配置不同环境的构建设置,例如开发、测试和生产。在 `pom.xml` 文件中定义配置文件并使用命令行参数激活它们: + +```xml + + + development + + true + + + dev + + + + production + + prod + + + +``` + +使用命令行激活配置文件: + +```bash +mvn clean install -P production +``` + +## 保持 pom.xml 干净且井然有序 + +组织良好的 `pom.xml` 文件更易于维护和理解。以下是维护干净的 `pom.xml` 的一些技巧: + +- 将相似的依赖项和插件组合在一起。 +- 使用注释来描述特定依赖项或插件的用途。 +- 将插件和依赖项的版本号保留在 `` 标签内以便于管理。 + +```xml + + 5.7.0 + 3.9.0 + +``` + +## 使用 Maven Wrapper + +Maven Wrapper 是一个用于管理和使用 Maven 的工具,它允许在没有预先安装 Maven 的情况下运行和构建 Maven 项目。 + +Maven 官方文档是这样介绍 Maven Wrapper 的: + +> The Maven Wrapper is an easy way to ensure a user of your Maven build has everything necessary to run your Maven build. +> +> Maven Wrapper 是一种简单的方法,可以确保 Maven 构建的用户拥有运行 Maven 构建所需的一切。 + +Maven Wrapper 可以确保构建过程使用正确的 Maven 版本,非常方便。要使用 Maven Wrapper,请在项目目录中运行以下命令: + +```bash +mvn wrapper:wrapper +``` + +此命令会在我们的项目中生成 Maven Wrapper 文件。现在我们可以使用 `./mvnw` (或 Windows 上的 `./mvnw.cmd`)而不是 `mvn` 来执行 Maven 命令。 + +## 通过持续集成实现构建自动化 + +将 Maven 项目与持续集成 (CI) 系统(例如 Jenkins 或 GitHub Actions)集成,可确保自动构建、测试和部署我们的代码。CI 有助于及早发现问题并在整个团队中提供一致的构建流程。以下是 Maven 项目的简单 GitHub Actions 工作流程示例: + +```groovy +name: Java CI with Maven + +on: [push] + +jobs: + build: + runs-on: ubuntu-latest + + steps: + - name: Checkout code + uses: actions/checkout@v2 + + - name: Set up JDK 11 + uses: actions/setup-java@v2 + with: + java-version: '11' + distribution: 'adopt' + + - name: Build with Maven + run: ./mvnw clean install +``` + +## 利用 Maven 插件获得附加功能 + +有许多 Maven 插件可用于扩展 Maven 的功能。一些流行的插件包括(前三个是 Maven 自带的插件,后三个是第三方提供的插件): + +- maven-surefire-plugin:配置并执行单元测试。 +- maven-failsafe-plugin:配置并执行集成测试。 +- maven-javadoc-plugin:生成 Javadoc 格式的项目文档。 +- maven-checkstyle-plugin:强制执行编码标准和最佳实践。 +- jacoco-maven-plugin: 单测覆盖率。 +- sonar-maven-plugin:分析代码质量。 +- …… + +jacoco-maven-plugin 使用示例: + +```xml + + + + org.jacoco + jacoco-maven-plugin + 0.8.8 + + + + prepare-agent + + + + generate-code-coverage-report + test + + report + + + + + + +``` + +如果这些已有的插件无法满足我们的需求,我们还可以自定义插件。 + +探索可用的插件并在 `pom.xml` 文件中配置它们以增强我们的开发过程。 + +## 总结 + +Maven 是一个强大的工具,可以简化 Java 项目的构建过程和依赖关系管理。通过遵循这些最佳实践和技巧,我们可以优化 Maven 的使用并改善我们的 Java 开发体验。请记住使用标准目录结构,有效管理依赖关系,利用不同环境的配置文件,并将项目与持续集成系统集成,以确保构建一致。 diff --git a/docs/tools/maven/maven-core-concepts.md b/docs/tools/maven/maven-core-concepts.md index be3a4de46ab..14b344d7524 100644 --- a/docs/tools/maven/maven-core-concepts.md +++ b/docs/tools/maven/maven-core-concepts.md @@ -139,7 +139,7 @@ Maven 的依赖范围如下: 举个例子,项目存在下面这样的依赖关系: -``` +```plain 依赖链路一:A -> B -> C -> X(1.0) 依赖链路二:A -> D -> X(2.0) ``` @@ -152,7 +152,7 @@ Maven 在遇到这种问题的时候,会遵循 **路径最短优先** 和 ** **路径最短优先** -``` +```plain 依赖链路一:A -> B -> C -> X(1.0) // dist = 3 依赖链路二:A -> D -> X(2.0) // dist = 2 ``` @@ -161,8 +161,8 @@ Maven 在遇到这种问题的时候,会遵循 **路径最短优先** 和 ** 不过,你也可以发现。路径最短优先原则并不是通用的,像下面这种路径长度相等的情况就不能单单通过其解决了: -``` -依赖链路一:A -> B -> X(1.0) // dist = 3 +```plain +依赖链路一:A -> B -> X(1.0) // dist = 2 依赖链路二:A -> D -> X(2.0) // dist = 2 ``` @@ -190,21 +190,21 @@ Maven 在遇到这种问题的时候,会遵循 **路径最短优先** 和 ** 举个例子,当前项目存在下面这样的依赖关系: -``` +```plain 依赖链路一:A -> B -> C -> X(1.5) // dist = 3 依赖链路二:A -> D -> X(1.0) // dist = 2 ``` 根据路径最短优先原则,X(1.0) 会被解析使用,也就是说实际用的是 1.0 版本的 X。 -但是!!!这会一些问题:如果 D 依赖用到了 1.5 版本的 X 中才有的一个类,运行项目就会报`NoClassDefFoundError`错误。如果 D 依赖用到了 1.5 版本的 X 中才有的一个方法,运行项目就会报`NoSuchMethodError`错误。 +但是!!!这会一些问题:如果 C 依赖用到了 1.5 版本的 X 中才有的一个类,运行项目就会报`NoClassDefFoundError`错误。如果 C 依赖用到了 1.5 版本的 X 中才有的一个方法,运行项目就会报`NoSuchMethodError`错误。 现在知道为什么你的 Maven 项目总是会报`NoClassDefFoundError`和`NoSuchMethodError`错误了吧? -**如何解决呢?** 我们可以通过`exclusive`标签手动将 X(1.0) 给排除。 +**如何解决呢?** 我们可以通过`exclusion`标签手动将 X(1.0) 给排除。 ```xml - + ...... @@ -217,11 +217,11 @@ Maven 在遇到这种问题的时候,会遵循 **路径最短优先** 和 ** 一般我们在解决依赖冲突的时候,都会优先保留版本较高的。这是因为大部分 jar 在升级的时候都会做到向下兼容。 -如果高版本修改了低版本的一些类或者方法的话,这个时候就能直接保留高版本了,而是应该考虑优化上层依赖,比如升级上层依赖的版本。 +如果高版本修改了低版本的一些类或者方法的话,这个时候就不能直接保留高版本了,而是应该考虑优化上层依赖,比如升级上层依赖的版本。 还是上面的例子: -``` +```plain 依赖链路一:A -> B -> C -> X(1.5) // dist = 3 依赖链路二:A -> D -> X(1.0) // dist = 2 ``` @@ -460,3 +460,5 @@ Maven 插件被分为下面两种类型: - Maven 依赖范围: - 解决 maven 依赖冲突,这篇就够了!: - Multi-Module Project with Maven: + + diff --git a/docs/zhuanlan/readme.md b/docs/zhuanlan/README.md similarity index 61% rename from docs/zhuanlan/readme.md rename to docs/zhuanlan/README.md index 0a08ea65186..9175b71ff3c 100644 --- a/docs/zhuanlan/readme.md +++ b/docs/zhuanlan/README.md @@ -6,24 +6,13 @@ category: 知识星球 这部分的内容为我的[知识星球](../about-the-author/zhishixingqiu-two-years.md)专属,目前已经更新了下面这些专栏: - **[《Java 面试指北》](./java-mian-shi-zhi-bei.md)** : 与 JavaGuide 开源版的内容互补! +- **[《后端面试高频系统设计&场景题》](./back-end-interview-high-frequency-system-design-and-scenario-questions.md)** : 包含了常见的系统设计案例比如短链系统、秒杀系统以及高频的场景题比如海量数据去重、第三方授权登录。 - **[《手写 RPC 框架》](./java-mian-shi-zhi-bei.md)** : 从零开始基于 Netty+Kyro+Zookeeper 实现一个简易的 RPC 框架。 - **[《Java 必读源码系列》](./source-code-reading.md)**:目前已经整理了 Dubbo 2.6.x、Netty 4.x、SpringBoot 2.1 等框架/中间件的源码 -- ...... +- …… 欢迎准备 Java 面试以及学习 Java 的同学加入我的[知识星球](../about-the-author/zhishixingqiu-two-years.md),干货非常多!收费虽然是白菜价,但星球里的内容比你参加几万的培训班质量还要高。 我有自己的原则,不割韭菜,用心做内容,真心希望帮助到你! -## 更多专栏 - -除了上面介绍的之外,我的[知识星球](../about-the-author/zhishixingqiu-two-years.md)还有 **《Kafka 常见面试题/知识点总结》**、**《程序员副业赚钱之路》**等多个专栏。 - -![](https://oss.javaguide.cn/xingqiu/image-20220211231206733.png) - -另外,星球还会有读书活动、学习打卡、简历修改、免费提问、海量 Java 优质面试资源以及各种不定时的福利。 - -![](https://oss.javaguide.cn/xingqiu/image-20220304124333119.png) - -## 星球限时优惠 - - + diff --git a/docs/zhuanlan/back-end-interview-high-frequency-system-design-and-scenario-questions.md b/docs/zhuanlan/back-end-interview-high-frequency-system-design-and-scenario-questions.md new file mode 100644 index 00000000000..ee848d003b8 --- /dev/null +++ b/docs/zhuanlan/back-end-interview-high-frequency-system-design-and-scenario-questions.md @@ -0,0 +1,22 @@ +--- +title: 《后端面试高频系统设计&场景题》 +category: 知识星球 +--- + +## 介绍 + +**《后端面试高频系统设计&场景题》** 是我的[知识星球](../about-the-author/zhishixingqiu-two-years.md)的一个内部小册,包含了常见的系统设计案例比如短链系统、秒杀系统以及高频的场景题比如海量数据去重、第三方授权登录。 + +近年来,随着国内的技术面试越来越卷,越来越多的公司开始在面试中考察系统设计和场景问题,以此来更全面的考察求职者,不论是校招还是社招。不过,正常面试全是场景题的情况还是极少的,面试官一般会在面试中穿插一两个系统设计和场景题来考察你。 + +于是,我总结了这份《后端面试高频系统设计&场景题》,包含了常见的系统设计案例比如短链系统、秒杀系统以及高频的场景题比如海量数据去重、第三方授权登录。 + +即使不是准备面试,我也强烈推荐你认真阅读这一系列文章,这对于提升自己系统设计思维和解决实际问题的能力还是非常有帮助的。并且,涉及到的很多案例都可以用到自己的项目上比如抽奖系统设计、第三方授权登录、Redis 实现延时任务的正确方式。 + +《后端面试高频系统设计&场景题》本身是属于《Java 面试指北》的一部分,后面由于内容篇幅较多,因此被单独提了出来。 + +## 内容概览 + +![《后端面试高频系统设计&场景题》](https://oss.javaguide.cn/xingqiu/back-end-interview-high-frequency-system-design-and-scenario-questions-fengmian.png) + + diff --git a/docs/zhuanlan/handwritten-rpc-framework.md b/docs/zhuanlan/handwritten-rpc-framework.md index 5c4b3b90341..5a055d56a31 100644 --- a/docs/zhuanlan/handwritten-rpc-framework.md +++ b/docs/zhuanlan/handwritten-rpc-framework.md @@ -1,5 +1,5 @@ --- -title: 《手写 RPC 框架》(付费) +title: 《手写 RPC 框架》 category: 知识星球 --- @@ -18,16 +18,4 @@ category: 知识星球 - GitHub 地址:[https://github.com/Snailclimb/guide-rpc-framework](https://github.com/Snailclimb/guide-rpc-framework) 。 - Gitee 地址:[https://gitee.com/SnailClimb/guide-rpc-framework](https://gitee.com/SnailClimb/guide-rpc-framework) 。 -## 星球其他资源 - -除了 **《手写 RPC 框架》** 之外,星球还有 **《Java 必读源码系列》**(目前已经整理了 Dubbo 2.6.x、Netty 4.x、SpringBoot2.1 的源码)、 **《Java 面试指北》**、**《Kafka 常见面试题/知识点总结》** 等多个专属小册。 - -![](https://oss.javaguide.cn/xingqiu/image-20220316200015412.png) - -另外,星球还会有读书活动、学习打卡、简历修改、免费提问、海量 Java 优质面试资源以及各种不定时的福利。 - -![](https://oss.javaguide.cn/xingqiu/image-20220304124333119.png) - -## 星球限时优惠 - - + diff --git a/docs/zhuanlan/java-mian-shi-zhi-bei.md b/docs/zhuanlan/java-mian-shi-zhi-bei.md index 00529688358..752a6bcea71 100644 --- a/docs/zhuanlan/java-mian-shi-zhi-bei.md +++ b/docs/zhuanlan/java-mian-shi-zhi-bei.md @@ -1,18 +1,26 @@ --- -title: 《Java 面试指北》(付费) +title: 《Java 面试指北》 category: 知识星球 star: 5 --- +我花费了三年的时间,写了一本针对 Java 面试的《Java 面试指北》,内容质量非常高,非常适合准备 Java 面试的朋友使用! + +目前的成绩:累计阅读 **270w+** ,点赞 **3550+** ,评论 **1130+** (几乎每一条提问类型的评论我看到后都会用心回复)。 + +![《Java 面试指北》统计](https://oss.javaguide.cn/xingqiu/java-interview-guide-statistics.png) + ## 介绍 **《Java 面试指北》** 是我的[知识星球](../about-the-author/zhishixingqiu-two-years.md)的一个内部小册,和 [JavaGuide 开源版](https://javaguide.cn/)的内容互补。相比于开源版本来说,《Java 面试指北》添加了下面这些内容(不仅仅是这些内容): -- 10+ 篇文章手把手教你如何准备面试。 -- 更全面的八股文面试题(系统设计、常见框架、分布式、高并发 ......)。 -- 优质面经精选。 -- 技术面试题自测。 -- 练级攻略(有助于个人成长的经验)。 +- 17+ 篇文章手把手教你如何准备面试,50+ 准备面试过程中的常见问题详细解读,让你更高效地准备 Java 面试。 +- 更全面的八股文面试题(系统设计、场景题、常见框架、分布式&微服务、高并发 ……)。 +- 优质面经精选(相比于牛客网或者其他网站的面经,《Java 面试指北》中整理的面经质量更高,并且,我会提供优质的参考资料)。 +- 技术面试题自测(高效准备技术八股文的技巧之一在于多多自测,查漏补缺)。 +- 练级攻略(有助于个人成长的经验分享)。 + +《Java 面试指北》 会根据每一年的面试情况对内容进行更新完善,保证内容质量的时效性。并且,只需要加入[知识星球](../about-the-author/zhishixingqiu-two-years.md)一次,即可永久获取《Java 面试指北》的访问权限,持续同步更新完善。 ## 内容概览 @@ -20,13 +28,13 @@ star: 5 ### 面试准备篇 -在 **「面试准备篇」** ,我写了 10+ 篇文章手把手教你如何准备面试,涵盖项目经验、简历编写、源码学习、算法准备、面试资源等内容。 +在 **「面试准备篇」** ,我写了 17+ 篇文章手把手教你如何准备面试,50+ 准备面试过程中的常见问题详细解读。准备面试过程中常见的疑问这里都有解答,内容涵盖项目经验、简历编写、源码学习、算法准备、面试资源等等。 ![《Java 面试指北》面试准备篇](https://oss.javaguide.cn/javamianshizhibei/preparation-for-interview.png) 另外,考虑到很多小伙伴缺少项目经历,我还推荐了很多小众但优质的实战项目,有视频也有开源项目,有业务系统,也有各种含金量比较高的轮子类项目。 -![实战项目推荐](https://oss.javaguide.cn/javamianshizhibei/practical-project-recommendation.png) +![《Java面试指北》-实战项目推荐](https://oss.javaguide.cn/javamianshizhibei/practical-project-recommendation.png) ### 技术面试题篇 @@ -42,21 +50,21 @@ star: 5 如果你是非科班的同学,也能在这些文章中找到对应的非科班的同学写的面经。 -![《Java 面试指北》面经篇](https://oss.javaguide.cn/githubjuejinjihua/thinkimage-20220612185810480.png) - -并且,[知识星球](https://javaguide.cn/about-the-author/zhishixingqiu-two-years.html)还有专门分享面经和面试题的专题,里面会分享很多优质的面经和面试题。 +![《Java 面试指北》面经篇](https://oss.javaguide.cn/javamianshizhibei/thinkimage-20220612185810480.png) -![](https://oss.javaguide.cn/xingqiu/image-20220304120018731.png) +相比于牛客网或者其他网站的面经,《Java 面试指北》中整理的面经质量更高,并且,我会提供优质的参考资料。 -![](https://oss.javaguide.cn/xingqiu/image-20220628101743381.png) +另外,[知识星球](https://javaguide.cn/about-the-author/zhishixingqiu-two-years.html)还有专门分享面经和面试题的专题,里面会分享很多优质的面经和面试题。 -![](https://oss.javaguide.cn/xingqiu/image-20220628101805897.png) +![星球面经专题](https://oss.javaguide.cn/javamianshizhibei/image-20220304120018731.png) ### 技术面试题自测篇 为了让小伙伴们自测以检查自己的掌握情况,我还推出了 **「技术面试题自测」** 系列。不过,目前只更新了 Java 和数据库的自测,正在持续更新中。 -![《Java 面试指北》技术面试题自测篇](https://oss.javaguide.cn/xingqiu/image-20220621095641897.png) +![《Java 面试指北》技术面试题自测篇](https://oss.javaguide.cn/javamianshizhibei/image-20220621095641897.png) + +高效准备技术八股文的技巧之一在于多多自测,查漏补缺。 ### 练级攻略篇 @@ -66,16 +74,10 @@ star: 5 每一篇内容都非常干货,不少球友看了之后表示收获满满。不过,最重要的还是知行合一。 -## 星球其他资源 - -除了 **《Java 面试指北》** 之外,星球还有 **《Java 必读源码系列》**(目前已经整理了 Dubbo 2.6.x、Netty 4.x、SpringBoot2.1 的源码)、 **《从零开始写一个 RPC 框架》**(已更新完)、**《Kafka 常见面试题/知识点总结》** 等多个专属小册。 - -![](https://oss.javaguide.cn/xingqiu/image-20220316200015412.png) - -另外,星球还会有读书活动、学习打卡、简历修改、免费提问、海量 Java 优质面试资源以及各种不定时的福利。 +### 工作篇 -![](https://oss.javaguide.cn/xingqiu/image-20220304124333119.png) +**「工作篇」** 这个系列主要内容是分享有助于个人以及职场发展的内容以及在工作中经常会遇到的问题。 -## 星球限时优惠 +![《Java 面试指北》工作篇](https://oss.javaguide.cn/javamianshizhibei/gongzuopian.png) - + diff --git a/docs/zhuanlan/source-code-reading.md b/docs/zhuanlan/source-code-reading.md index 16207d9c6e7..2441f2e7adc 100644 --- a/docs/zhuanlan/source-code-reading.md +++ b/docs/zhuanlan/source-code-reading.md @@ -1,12 +1,12 @@ --- -title: 《Java 必读源码系列》(付费) +title: 《Java 必读源码系列》 category: 知识星球 star: true --- ## 介绍 -**《Java 必读源码系列》** 是我的[知识星球](../about-the-author/zhishixingqiu-two-years.md)的一个内部小册,目前已经整理了 Dubbo 2.6.x、Netty 4.x、SpringBoot 2.1 等框架/中间件的源码。 +**《Java 必读源码系列》** 是我的[知识星球](../about-the-author/zhishixingqiu-two-years.md)的一个内部小册,目前已经整理了 Dubbo 2.6.x、Netty 4.x、SpringBoot 2.1 等框架/中间件的源码。后续还会整理更多值得阅读的优质源码,持续完善中。 结构清晰,内容详细,非常适合想要深入学习框架/中间件源码的同学阅读。 @@ -14,16 +14,10 @@ star: true ![](https://oss.javaguide.cn/xingqiu/image-20220621091832348.png) -## 星球其他资源 + -除了 **《Java 必读源码系列》** 之外,星球还有 **《从零开始写一个 RPC 框架》**、 **《Java 面试指北》**、 **《Java 必读源码系列》**(目前已经整理了 Dubbo 2.6.x、Netty 4.x、SpringBoot2.1 的源码)、**《Kafka 常见面试题/知识点总结》** 等多个专栏。 +## 更多专栏 -![](https://oss.javaguide.cn/xingqiu/image-20220211231206733.png) +除了《Java 必读源码系列》之外,我的知识星球还有 [《Java 面试指北》](https://mp.weixin.qq.com/s?__biz=Mzg2OTA0Njk0OA==&mid=2247536358&idx=2&sn=a6098093107d596d3c426c9e71e871b8&chksm=cea1012df9d6883b95aab61fd815a238c703b2d4b36d78901553097a4939504e3e6d73f2b14b&token=710779655&lang=zh_CN#rd)**、**[《后端面试高频系统设计&场景题》](https://mp.weixin.qq.com/s?__biz=Mzg2OTA0Njk0OA==&mid=2247536451&idx=1&sn=5eae2525ac3d79591dd86c6051522c0b&chksm=cea10088f9d6899e0aee4146de162a6de6ece71ba4c80c23f04d12b1fd48c087a31bc7d413f4&token=710779655&lang=zh_CN#rd)、《手写 RPC 框架》等多个专栏。进入星球之后,统统都可以免费阅读。 -另外,星球还会有读书活动、学习打卡、简历修改、免费提问、海量 Java 优质面试资源以及各种不定时的福利。 - -![](https://oss.javaguide.cn/xingqiu/image-20220304124333119.png) - -## 星球限时优惠 - - +![](https://mmbiz.qpic.cn/mmbiz_png/iaIdQfEric9TyC1icms4objsyiaJe2Iic7RZUq6nzsOOTX27x6Vfm5SibGic952kp3JM0RfRpLZXrneOCEOOogicj69yKw/640?wx_fmt=png&wxfrom=5&wx_lazy=1&wx_co=1) diff --git a/package.json b/package.json index f5c4299149e..fa13424e049 100644 --- a/package.json +++ b/package.json @@ -5,31 +5,33 @@ "description": "javaguide", "license": "MIT", "author": "Guide", - "packageManager": "pnpm@8.5.1", "scripts": { "docs:build": "vuepress build docs", "docs:dev": "vuepress dev docs", "docs:clean-dev": "vuepress dev docs --clean-cache", - "lint": "prettier --check --write . && markdownlint docs/**/*.md", - "prepare": "husky install" + "lint": "pnpm lint:prettier && pnpm lint:md", + "lint:md": "markdownlint-cli2 '**/*.md'", + "lint:prettier": "prettier --check --write .", + "prepare": "husky", + "update": "pnpm dlx vp-update" }, "nano-staged": { - ".ts,.scss": "prettier --write", - ".md": [ - "prettier --write", - "markdownlint" - ] + "**/*": "prettier --write --ignore-unknown", + ".md": "markdownlint-cli2" }, "dependencies": { - "@vuepress/client": "2.0.0-beta.62", - "@vuepress/utils": "2.0.0-beta.62", - "husky": "8.0.3", - "markdownlint-cli": "0.34.0", + "@vuepress/bundler-vite": "2.0.0-rc.19", + "@vuepress/plugin-feed": "2.0.0-rc.70", + "@vuepress/plugin-search": "2.0.0-rc.70", + "husky": "9.1.7", + "markdownlint-cli2": "0.17.1", + "mathjax-full": "3.2.2", "nano-staged": "0.8.0", - "prettier": "2.8.8", - "vue": "3.3.2", - "vuepress": "2.0.0-beta.62", - "vuepress-plugin-search-pro": "2.0.0-beta.211", - "vuepress-theme-hope": "2.0.0-beta.211" - } + "prettier": "3.4.2", + "sass-embedded": "1.83.1", + "vue": "^3.5.13", + "vuepress": "2.0.0-rc.19", + "vuepress-theme-hope": "2.0.0-rc.68" + }, + "packageManager": "pnpm@10.0.0" } diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index f6eabb18965..d174a9236f9 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -1,6419 +1,5944 @@ -lockfileVersion: '6.0' - -dependencies: - '@vuepress/client': - specifier: 2.0.0-beta.62 - version: 2.0.0-beta.62 - '@vuepress/utils': - specifier: 2.0.0-beta.62 - version: 2.0.0-beta.62 - husky: - specifier: 8.0.3 - version: 8.0.3 - markdownlint-cli: - specifier: 0.34.0 - version: 0.34.0 - nano-staged: - specifier: 0.8.0 - version: 0.8.0 - prettier: - specifier: 2.8.8 - version: 2.8.8 - vue: - specifier: 3.3.2 - version: 3.3.2 - vuepress: - specifier: 2.0.0-beta.62 - version: 2.0.0-beta.62(@vuepress/client@2.0.0-beta.62)(vue@3.3.2) - vuepress-plugin-search-pro: - specifier: 2.0.0-beta.211 - version: 2.0.0-beta.211(vuepress@2.0.0-beta.62) - vuepress-theme-hope: - specifier: 2.0.0-beta.211 - version: 2.0.0-beta.211(react-dom@16.14.0)(react@16.14.0)(vuepress@2.0.0-beta.62) +lockfileVersion: '9.0' + +settings: + autoInstallPeers: true + excludeLinksFromLockfile: false + +importers: + + .: + dependencies: + '@vuepress/bundler-vite': + specifier: 2.0.0-rc.19 + version: 2.0.0-rc.19(@types/node@22.10.5)(sass-embedded@1.83.1) + '@vuepress/plugin-feed': + specifier: 2.0.0-rc.70 + version: 2.0.0-rc.70(vuepress@2.0.0-rc.19(@vuepress/bundler-vite@2.0.0-rc.19(@types/node@22.10.5)(sass-embedded@1.83.1))(vue@3.5.13)) + '@vuepress/plugin-search': + specifier: 2.0.0-rc.70 + version: 2.0.0-rc.70(vuepress@2.0.0-rc.19(@vuepress/bundler-vite@2.0.0-rc.19(@types/node@22.10.5)(sass-embedded@1.83.1))(vue@3.5.13)) + husky: + specifier: 9.1.7 + version: 9.1.7 + markdownlint-cli2: + specifier: 0.17.1 + version: 0.17.1 + mathjax-full: + specifier: 3.2.2 + version: 3.2.2 + nano-staged: + specifier: 0.8.0 + version: 0.8.0 + prettier: + specifier: 3.4.2 + version: 3.4.2 + sass-embedded: + specifier: 1.83.1 + version: 1.83.1 + vue: + specifier: ^3.5.13 + version: 3.5.13 + vuepress: + specifier: 2.0.0-rc.19 + version: 2.0.0-rc.19(@vuepress/bundler-vite@2.0.0-rc.19(@types/node@22.10.5)(sass-embedded@1.83.1))(vue@3.5.13) + vuepress-theme-hope: + specifier: 2.0.0-rc.68 + version: 2.0.0-rc.68(@vuepress/plugin-feed@2.0.0-rc.70(vuepress@2.0.0-rc.19(@vuepress/bundler-vite@2.0.0-rc.19(@types/node@22.10.5)(sass-embedded@1.83.1))(vue@3.5.13)))(@vuepress/plugin-search@2.0.0-rc.70(vuepress@2.0.0-rc.19(@vuepress/bundler-vite@2.0.0-rc.19(@types/node@22.10.5)(sass-embedded@1.83.1))(vue@3.5.13)))(katex@0.16.20)(markdown-it@14.1.0)(mathjax-full@3.2.2)(nodejs-jieba@0.2.1(encoding@0.1.13))(sass-embedded@1.83.1)(vuepress@2.0.0-rc.19(@vuepress/bundler-vite@2.0.0-rc.19(@types/node@22.10.5)(sass-embedded@1.83.1))(vue@3.5.13)) + +packages: + + '@babel/helper-string-parser@7.25.9': + resolution: {integrity: sha512-4A/SCr/2KLd5jrtOMFzaKjVtAei3+2r/NChoBNoZ3EyP/+GlhoaEGoWOZUmFmoITP7zOJyHIMm+DYRd8o3PvHA==} + engines: {node: '>=6.9.0'} + + '@babel/helper-validator-identifier@7.25.9': + resolution: {integrity: sha512-Ed61U6XJc3CVRfkERJWDz4dJwKe7iLmmJsbOGu9wSloNSFttHV0I8g6UAgb7qnK5ly5bGLPd4oXZlxCdANBOWQ==} + engines: {node: '>=6.9.0'} + + '@babel/parser@7.26.5': + resolution: {integrity: sha512-SRJ4jYmXRqV1/Xc+TIVG84WjHBXKlxO9sHQnA2Pf12QQEAp1LOh6kDzNHXcUnbH1QI0FDoPPVOt+vyUDucxpaw==} + engines: {node: '>=6.0.0'} + hasBin: true + + '@babel/types@7.26.5': + resolution: {integrity: sha512-L6mZmwFDK6Cjh1nRCLXpa6no13ZIioJDz7mdkzHv399pThrTa/k0nUlNaenOeh2kWu/iaOQYElEpKPUswUa9Vg==} + engines: {node: '>=6.9.0'} + + '@bufbuild/protobuf@2.2.3': + resolution: {integrity: sha512-tFQoXHJdkEOSwj5tRIZSPNUuXK3RaR7T1nUrPgbYX1pUbvqqaaZAsfo+NXBPsz5rZMSKVFrgK1WL8Q/MSLvprg==} + + '@esbuild/aix-ppc64@0.21.5': + resolution: {integrity: sha512-1SDgH6ZSPTlggy1yI6+Dbkiz8xzpHJEVAlF/AM1tHPLsf5STom9rwtjE4hKAF20FfXXNTFqEYXyJNWh1GiZedQ==} + engines: {node: '>=12'} + cpu: [ppc64] + os: [aix] + + '@esbuild/aix-ppc64@0.24.2': + resolution: {integrity: sha512-thpVCb/rhxE/BnMLQ7GReQLLN8q9qbHmI55F4489/ByVg2aQaQ6kbcLb6FHkocZzQhxc4gx0sCk0tJkKBFzDhA==} + engines: {node: '>=18'} + cpu: [ppc64] + os: [aix] + + '@esbuild/android-arm64@0.21.5': + resolution: {integrity: sha512-c0uX9VAUBQ7dTDCjq+wdyGLowMdtR/GoC2U5IYk/7D1H1JYC0qseD7+11iMP2mRLN9RcCMRcjC4YMclCzGwS/A==} + engines: {node: '>=12'} + cpu: [arm64] + os: [android] + + '@esbuild/android-arm64@0.24.2': + resolution: {integrity: sha512-cNLgeqCqV8WxfcTIOeL4OAtSmL8JjcN6m09XIgro1Wi7cF4t/THaWEa7eL5CMoMBdjoHOTh/vwTO/o2TRXIyzg==} + engines: {node: '>=18'} + cpu: [arm64] + os: [android] + + '@esbuild/android-arm@0.21.5': + resolution: {integrity: sha512-vCPvzSjpPHEi1siZdlvAlsPxXl7WbOVUBBAowWug4rJHb68Ox8KualB+1ocNvT5fjv6wpkX6o/iEpbDrf68zcg==} + engines: {node: '>=12'} + cpu: [arm] + os: [android] + + '@esbuild/android-arm@0.24.2': + resolution: {integrity: sha512-tmwl4hJkCfNHwFB3nBa8z1Uy3ypZpxqxfTQOcHX+xRByyYgunVbZ9MzUUfb0RxaHIMnbHagwAxuTL+tnNM+1/Q==} + engines: {node: '>=18'} + cpu: [arm] + os: [android] + + '@esbuild/android-x64@0.21.5': + resolution: {integrity: sha512-D7aPRUUNHRBwHxzxRvp856rjUHRFW1SdQATKXH2hqA0kAZb1hKmi02OpYRacl0TxIGz/ZmXWlbZgjwWYaCakTA==} + engines: {node: '>=12'} + cpu: [x64] + os: [android] + + '@esbuild/android-x64@0.24.2': + resolution: {integrity: sha512-B6Q0YQDqMx9D7rvIcsXfmJfvUYLoP722bgfBlO5cGvNVb5V/+Y7nhBE3mHV9OpxBf4eAS2S68KZztiPaWq4XYw==} + engines: {node: '>=18'} + cpu: [x64] + os: [android] + + '@esbuild/darwin-arm64@0.21.5': + resolution: {integrity: sha512-DwqXqZyuk5AiWWf3UfLiRDJ5EDd49zg6O9wclZ7kUMv2WRFr4HKjXp/5t8JZ11QbQfUS6/cRCKGwYhtNAY88kQ==} + engines: {node: '>=12'} + cpu: [arm64] + os: [darwin] + + '@esbuild/darwin-arm64@0.24.2': + resolution: {integrity: sha512-kj3AnYWc+CekmZnS5IPu9D+HWtUI49hbnyqk0FLEJDbzCIQt7hg7ucF1SQAilhtYpIujfaHr6O0UHlzzSPdOeA==} + engines: {node: '>=18'} + cpu: [arm64] + os: [darwin] + + '@esbuild/darwin-x64@0.21.5': + resolution: {integrity: sha512-se/JjF8NlmKVG4kNIuyWMV/22ZaerB+qaSi5MdrXtd6R08kvs2qCN4C09miupktDitvh8jRFflwGFBQcxZRjbw==} + engines: {node: '>=12'} + cpu: [x64] + os: [darwin] + + '@esbuild/darwin-x64@0.24.2': + resolution: {integrity: sha512-WeSrmwwHaPkNR5H3yYfowhZcbriGqooyu3zI/3GGpF8AyUdsrrP0X6KumITGA9WOyiJavnGZUwPGvxvwfWPHIA==} + engines: {node: '>=18'} + cpu: [x64] + os: [darwin] + + '@esbuild/freebsd-arm64@0.21.5': + resolution: {integrity: sha512-5JcRxxRDUJLX8JXp/wcBCy3pENnCgBR9bN6JsY4OmhfUtIHe3ZW0mawA7+RDAcMLrMIZaf03NlQiX9DGyB8h4g==} + engines: {node: '>=12'} + cpu: [arm64] + os: [freebsd] + + '@esbuild/freebsd-arm64@0.24.2': + resolution: {integrity: sha512-UN8HXjtJ0k/Mj6a9+5u6+2eZ2ERD7Edt1Q9IZiB5UZAIdPnVKDoG7mdTVGhHJIeEml60JteamR3qhsr1r8gXvg==} + engines: {node: '>=18'} + cpu: [arm64] + os: [freebsd] + + '@esbuild/freebsd-x64@0.21.5': + resolution: {integrity: sha512-J95kNBj1zkbMXtHVH29bBriQygMXqoVQOQYA+ISs0/2l3T9/kj42ow2mpqerRBxDJnmkUDCaQT/dfNXWX/ZZCQ==} + engines: {node: '>=12'} + cpu: [x64] + os: [freebsd] + + '@esbuild/freebsd-x64@0.24.2': + resolution: {integrity: sha512-TvW7wE/89PYW+IevEJXZ5sF6gJRDY/14hyIGFXdIucxCsbRmLUcjseQu1SyTko+2idmCw94TgyaEZi9HUSOe3Q==} + engines: {node: '>=18'} + cpu: [x64] + os: [freebsd] + + '@esbuild/linux-arm64@0.21.5': + resolution: {integrity: sha512-ibKvmyYzKsBeX8d8I7MH/TMfWDXBF3db4qM6sy+7re0YXya+K1cem3on9XgdT2EQGMu4hQyZhan7TeQ8XkGp4Q==} + engines: {node: '>=12'} + cpu: [arm64] + os: [linux] + + '@esbuild/linux-arm64@0.24.2': + resolution: {integrity: sha512-7HnAD6074BW43YvvUmE/35Id9/NB7BeX5EoNkK9obndmZBUk8xmJJeU7DwmUeN7tkysslb2eSl6CTrYz6oEMQg==} + engines: {node: '>=18'} + cpu: [arm64] + os: [linux] + + '@esbuild/linux-arm@0.21.5': + resolution: {integrity: sha512-bPb5AHZtbeNGjCKVZ9UGqGwo8EUu4cLq68E95A53KlxAPRmUyYv2D6F0uUI65XisGOL1hBP5mTronbgo+0bFcA==} + engines: {node: '>=12'} + cpu: [arm] + os: [linux] + + '@esbuild/linux-arm@0.24.2': + resolution: {integrity: sha512-n0WRM/gWIdU29J57hJyUdIsk0WarGd6To0s+Y+LwvlC55wt+GT/OgkwoXCXvIue1i1sSNWblHEig00GBWiJgfA==} + engines: {node: '>=18'} + cpu: [arm] + os: [linux] + + '@esbuild/linux-ia32@0.21.5': + resolution: {integrity: sha512-YvjXDqLRqPDl2dvRODYmmhz4rPeVKYvppfGYKSNGdyZkA01046pLWyRKKI3ax8fbJoK5QbxblURkwK/MWY18Tg==} + engines: {node: '>=12'} + cpu: [ia32] + os: [linux] + + '@esbuild/linux-ia32@0.24.2': + resolution: {integrity: sha512-sfv0tGPQhcZOgTKO3oBE9xpHuUqguHvSo4jl+wjnKwFpapx+vUDcawbwPNuBIAYdRAvIDBfZVvXprIj3HA+Ugw==} + engines: {node: '>=18'} + cpu: [ia32] + os: [linux] + + '@esbuild/linux-loong64@0.21.5': + resolution: {integrity: sha512-uHf1BmMG8qEvzdrzAqg2SIG/02+4/DHB6a9Kbya0XDvwDEKCoC8ZRWI5JJvNdUjtciBGFQ5PuBlpEOXQj+JQSg==} + engines: {node: '>=12'} + cpu: [loong64] + os: [linux] + + '@esbuild/linux-loong64@0.24.2': + resolution: {integrity: sha512-CN9AZr8kEndGooS35ntToZLTQLHEjtVB5n7dl8ZcTZMonJ7CCfStrYhrzF97eAecqVbVJ7APOEe18RPI4KLhwQ==} + engines: {node: '>=18'} + cpu: [loong64] + os: [linux] + + '@esbuild/linux-mips64el@0.21.5': + resolution: {integrity: sha512-IajOmO+KJK23bj52dFSNCMsz1QP1DqM6cwLUv3W1QwyxkyIWecfafnI555fvSGqEKwjMXVLokcV5ygHW5b3Jbg==} + engines: {node: '>=12'} + cpu: [mips64el] + os: [linux] + + '@esbuild/linux-mips64el@0.24.2': + resolution: {integrity: sha512-iMkk7qr/wl3exJATwkISxI7kTcmHKE+BlymIAbHO8xanq/TjHaaVThFF6ipWzPHryoFsesNQJPE/3wFJw4+huw==} + engines: {node: '>=18'} + cpu: [mips64el] + os: [linux] + + '@esbuild/linux-ppc64@0.21.5': + resolution: {integrity: sha512-1hHV/Z4OEfMwpLO8rp7CvlhBDnjsC3CttJXIhBi+5Aj5r+MBvy4egg7wCbe//hSsT+RvDAG7s81tAvpL2XAE4w==} + engines: {node: '>=12'} + cpu: [ppc64] + os: [linux] + + '@esbuild/linux-ppc64@0.24.2': + resolution: {integrity: sha512-shsVrgCZ57Vr2L8mm39kO5PPIb+843FStGt7sGGoqiiWYconSxwTiuswC1VJZLCjNiMLAMh34jg4VSEQb+iEbw==} + engines: {node: '>=18'} + cpu: [ppc64] + os: [linux] + + '@esbuild/linux-riscv64@0.21.5': + resolution: {integrity: sha512-2HdXDMd9GMgTGrPWnJzP2ALSokE/0O5HhTUvWIbD3YdjME8JwvSCnNGBnTThKGEB91OZhzrJ4qIIxk/SBmyDDA==} + engines: {node: '>=12'} + cpu: [riscv64] + os: [linux] + + '@esbuild/linux-riscv64@0.24.2': + resolution: {integrity: sha512-4eSFWnU9Hhd68fW16GD0TINewo1L6dRrB+oLNNbYyMUAeOD2yCK5KXGK1GH4qD/kT+bTEXjsyTCiJGHPZ3eM9Q==} + engines: {node: '>=18'} + cpu: [riscv64] + os: [linux] + + '@esbuild/linux-s390x@0.21.5': + resolution: {integrity: sha512-zus5sxzqBJD3eXxwvjN1yQkRepANgxE9lgOW2qLnmr8ikMTphkjgXu1HR01K4FJg8h1kEEDAqDcZQtbrRnB41A==} + engines: {node: '>=12'} + cpu: [s390x] + os: [linux] + + '@esbuild/linux-s390x@0.24.2': + resolution: {integrity: sha512-S0Bh0A53b0YHL2XEXC20bHLuGMOhFDO6GN4b3YjRLK//Ep3ql3erpNcPlEFed93hsQAjAQDNsvcK+hV90FubSw==} + engines: {node: '>=18'} + cpu: [s390x] + os: [linux] + + '@esbuild/linux-x64@0.21.5': + resolution: {integrity: sha512-1rYdTpyv03iycF1+BhzrzQJCdOuAOtaqHTWJZCWvijKD2N5Xu0TtVC8/+1faWqcP9iBCWOmjmhoH94dH82BxPQ==} + engines: {node: '>=12'} + cpu: [x64] + os: [linux] + + '@esbuild/linux-x64@0.24.2': + resolution: {integrity: sha512-8Qi4nQcCTbLnK9WoMjdC9NiTG6/E38RNICU6sUNqK0QFxCYgoARqVqxdFmWkdonVsvGqWhmm7MO0jyTqLqwj0Q==} + engines: {node: '>=18'} + cpu: [x64] + os: [linux] + + '@esbuild/netbsd-arm64@0.24.2': + resolution: {integrity: sha512-wuLK/VztRRpMt9zyHSazyCVdCXlpHkKm34WUyinD2lzK07FAHTq0KQvZZlXikNWkDGoT6x3TD51jKQ7gMVpopw==} + engines: {node: '>=18'} + cpu: [arm64] + os: [netbsd] + + '@esbuild/netbsd-x64@0.21.5': + resolution: {integrity: sha512-Woi2MXzXjMULccIwMnLciyZH4nCIMpWQAs049KEeMvOcNADVxo0UBIQPfSmxB3CWKedngg7sWZdLvLczpe0tLg==} + engines: {node: '>=12'} + cpu: [x64] + os: [netbsd] + + '@esbuild/netbsd-x64@0.24.2': + resolution: {integrity: sha512-VefFaQUc4FMmJuAxmIHgUmfNiLXY438XrL4GDNV1Y1H/RW3qow68xTwjZKfj/+Plp9NANmzbH5R40Meudu8mmw==} + engines: {node: '>=18'} + cpu: [x64] + os: [netbsd] + + '@esbuild/openbsd-arm64@0.24.2': + resolution: {integrity: sha512-YQbi46SBct6iKnszhSvdluqDmxCJA+Pu280Av9WICNwQmMxV7nLRHZfjQzwbPs3jeWnuAhE9Jy0NrnJ12Oz+0A==} + engines: {node: '>=18'} + cpu: [arm64] + os: [openbsd] + + '@esbuild/openbsd-x64@0.21.5': + resolution: {integrity: sha512-HLNNw99xsvx12lFBUwoT8EVCsSvRNDVxNpjZ7bPn947b8gJPzeHWyNVhFsaerc0n3TsbOINvRP2byTZ5LKezow==} + engines: {node: '>=12'} + cpu: [x64] + os: [openbsd] + + '@esbuild/openbsd-x64@0.24.2': + resolution: {integrity: sha512-+iDS6zpNM6EnJyWv0bMGLWSWeXGN/HTaF/LXHXHwejGsVi+ooqDfMCCTerNFxEkM3wYVcExkeGXNqshc9iMaOA==} + engines: {node: '>=18'} + cpu: [x64] + os: [openbsd] + + '@esbuild/sunos-x64@0.21.5': + resolution: {integrity: sha512-6+gjmFpfy0BHU5Tpptkuh8+uw3mnrvgs+dSPQXQOv3ekbordwnzTVEb4qnIvQcYXq6gzkyTnoZ9dZG+D4garKg==} + engines: {node: '>=12'} + cpu: [x64] + os: [sunos] + + '@esbuild/sunos-x64@0.24.2': + resolution: {integrity: sha512-hTdsW27jcktEvpwNHJU4ZwWFGkz2zRJUz8pvddmXPtXDzVKTTINmlmga3ZzwcuMpUvLw7JkLy9QLKyGpD2Yxig==} + engines: {node: '>=18'} + cpu: [x64] + os: [sunos] + + '@esbuild/win32-arm64@0.21.5': + resolution: {integrity: sha512-Z0gOTd75VvXqyq7nsl93zwahcTROgqvuAcYDUr+vOv8uHhNSKROyU961kgtCD1e95IqPKSQKH7tBTslnS3tA8A==} + engines: {node: '>=12'} + cpu: [arm64] + os: [win32] + + '@esbuild/win32-arm64@0.24.2': + resolution: {integrity: sha512-LihEQ2BBKVFLOC9ZItT9iFprsE9tqjDjnbulhHoFxYQtQfai7qfluVODIYxt1PgdoyQkz23+01rzwNwYfutxUQ==} + engines: {node: '>=18'} + cpu: [arm64] + os: [win32] + + '@esbuild/win32-ia32@0.21.5': + resolution: {integrity: sha512-SWXFF1CL2RVNMaVs+BBClwtfZSvDgtL//G/smwAc5oVK/UPu2Gu9tIaRgFmYFFKrmg3SyAjSrElf0TiJ1v8fYA==} + engines: {node: '>=12'} + cpu: [ia32] + os: [win32] + + '@esbuild/win32-ia32@0.24.2': + resolution: {integrity: sha512-q+iGUwfs8tncmFC9pcnD5IvRHAzmbwQ3GPS5/ceCyHdjXubwQWI12MKWSNSMYLJMq23/IUCvJMS76PDqXe1fxA==} + engines: {node: '>=18'} + cpu: [ia32] + os: [win32] + + '@esbuild/win32-x64@0.21.5': + resolution: {integrity: sha512-tQd/1efJuzPC6rCFwEvLtci/xNFcTZknmXs98FYDfGE4wP9ClFV98nyKrzJKVPMhdDnjzLhdUyMX4PsQAPjwIw==} + engines: {node: '>=12'} + cpu: [x64] + os: [win32] + + '@esbuild/win32-x64@0.24.2': + resolution: {integrity: sha512-7VTgWzgMGvup6aSqDPLiW5zHaxYJGTO4OokMjIlrCtf+VpEL+cXKtCvg723iguPYI5oaUNdS+/V7OU2gvXVWEg==} + engines: {node: '>=18'} + cpu: [x64] + os: [win32] + + '@isaacs/cliui@8.0.2': + resolution: {integrity: sha512-O8jcjabXaleOG9DQ0+ARXWZBTfnP4WNAqzuiJK7ll44AmxGKv/J2M4TPjxjY3znBCfvBXFzucm1twdyFybFqEA==} + engines: {node: '>=12'} + + '@jridgewell/sourcemap-codec@1.5.0': + resolution: {integrity: sha512-gv3ZRaISU3fjPAgNsriBRqGWQL6quFx04YMPW/zD8XMLsU32mhCCbfbO6KZFLjvYpCZ8zyDEgqsgf+PwPaM7GQ==} + + '@lit-labs/ssr-dom-shim@1.3.0': + resolution: {integrity: sha512-nQIWonJ6eFAvUUrSlwyHDm/aE8PBDu5kRpL0vHMg6K8fK3Diq1xdPjTnsJSwxABhaZ+5eBi1btQB5ShUTKo4nQ==} + + '@lit/reactive-element@2.0.4': + resolution: {integrity: sha512-GFn91inaUa2oHLak8awSIigYz0cU0Payr1rcFsrkf5OJ5eSPxElyZfKh0f2p9FsTiZWXQdWGJeXZICEfXXYSXQ==} + + '@mapbox/node-pre-gyp@1.0.11': + resolution: {integrity: sha512-Yhlar6v9WQgUp/He7BdgzOz8lqMQ8sU+jkCq7Wx8Myc5YFJLbEe7lgui/V7G1qB1DJykHSGwreceSaD60Y0PUQ==} + hasBin: true + + '@mdit-vue/plugin-component@2.1.3': + resolution: {integrity: sha512-9AG17beCgpEw/4ldo/M6Y/1Rh4E1bqMmr/rCkWKmCAxy9tJz3lzY7HQJanyHMJufwsb3WL5Lp7Om/aPcQTZ9SA==} + + '@mdit-vue/plugin-frontmatter@2.1.3': + resolution: {integrity: sha512-KxsSCUVBEmn6sJcchSTiI5v9bWaoRxe68RBYRDGcSEY1GTnfQ5gQPMIsM48P4q1luLEIWurVGGrRu7u93//LDQ==} + + '@mdit-vue/plugin-headers@2.1.3': + resolution: {integrity: sha512-AcL7a7LHQR3ISINhfjGJNE/bHyM0dcl6MYm1Sr//zF7ZgokPGwD/HhD7TzwmrKA9YNYCcO9P3QmF/RN9XyA6CA==} + + '@mdit-vue/plugin-sfc@2.1.3': + resolution: {integrity: sha512-Ezl0dNvQNS639Yl4siXm+cnWtQvlqHrg+u+lnau/OHpj9Xh3LVap/BSQVugKIV37eR13jXXYf3VaAOP1fXPN+w==} + + '@mdit-vue/plugin-title@2.1.3': + resolution: {integrity: sha512-XWVOQoZqczoN97xCDrnQicmXKoqwOjIymIm9HQnRXhHnYKOgJPW1CxSGhkcOGzvDU1v0mD/adojVyyj/s6ggWw==} + + '@mdit-vue/plugin-toc@2.1.3': + resolution: {integrity: sha512-41Q+iXpLHZt0zJdApVwoVt7WF6za/xUjtjEPf90Z3KLzQO01TXsv48Xp9BsrFHPcPcm8tiZ0+O1/ICJO80V/MQ==} + + '@mdit-vue/shared@2.1.3': + resolution: {integrity: sha512-27YI8b0VVZsAlNwaWoaOCWbr4eL8B04HxiYk/y2ktblO/nMcOEOLt4p0RjuobvdyUyjHvGOS09RKhq7qHm1CHQ==} + + '@mdit-vue/types@2.1.0': + resolution: {integrity: sha512-TMBB/BQWVvwtpBdWD75rkZx4ZphQ6MN0O4QB2Bc0oI5PC2uE57QerhNxdRZ7cvBHE2iY2C+BUNUziCfJbjIRRA==} + + '@mdit/helper@0.16.0': + resolution: {integrity: sha512-vUmLSZp+7UXJIYxOya9BkD0OgjgQ+6gpX+htEnc4SKaDPx4S1E7h5TE6Wy4E9Gm/JhkMHoD6TdeoQwrN/I9cLQ==} + engines: {node: '>= 18'} + peerDependencies: + markdown-it: ^14.1.0 + peerDependenciesMeta: + markdown-it: + optional: true + + '@mdit/plugin-alert@0.16.0': + resolution: {integrity: sha512-T+0BUVhKjp+Azp6sNdDbiZwydDIcZP6/NAg9uivPvcsDnI9u4lMRCdXI090xNJOdhHO3l/lOsoO//s+++MJNtA==} + peerDependencies: + markdown-it: ^14.1.0 + peerDependenciesMeta: + markdown-it: + optional: true + + '@mdit/plugin-align@0.16.0': + resolution: {integrity: sha512-BJhOjX4Zobs+ZKEpDtxGrUCnppkFCTGIBLjXkCPmxeLf4Tsh7dqv5vVhbRueSOz/EIzc2RJzR0dlMLofsaCFeA==} + engines: {node: '>= 18'} + peerDependencies: + markdown-it: ^14.1.0 + peerDependenciesMeta: + markdown-it: + optional: true + + '@mdit/plugin-attrs@0.16.2': + resolution: {integrity: sha512-ftzyOo6mDquRfpwcrSYPu9DIUhIRvC9ZTjUq1lGUd/ts93PKF9v6YCio/L376CEKLMVibHdNYBQAkGTQFwAgnA==} + engines: {node: '>= 18'} + peerDependencies: + markdown-it: ^14.1.0 + peerDependenciesMeta: + markdown-it: + optional: true + + '@mdit/plugin-container@0.16.0': + resolution: {integrity: sha512-NCsyEiOmoJvXSEVJSY6vaEcvbE11sciRSx5qXBvQQZxUYGYsB+ObYSFVZDFPezsEN35X3b07rurLx8P2Mi9DgQ==} + engines: {node: '>= 18'} + peerDependencies: + markdown-it: ^14.1.0 + peerDependenciesMeta: + markdown-it: + optional: true + + '@mdit/plugin-demo@0.16.0': + resolution: {integrity: sha512-EoSpHz8ViLk5HLBCSzQZGOa36JXGHM4q5zOJ0ppgZymxnzRr6vUo+GX022uLivxyNMW1+l30IiF+jbse+JtBGw==} + peerDependencies: + markdown-it: ^14.1.0 + peerDependenciesMeta: + markdown-it: + optional: true + + '@mdit/plugin-figure@0.16.0': + resolution: {integrity: sha512-0lYZX3cCUNaygtQXXZH2fHXzmF7sMZ5Jbk5MXDxEDIk1Nkxj8ADo/SctvXN5exwyGpJyw8nTbm7CGgMqifDpmQ==} + engines: {node: '>= 18'} + peerDependencies: + markdown-it: ^14.1.0 + peerDependenciesMeta: + markdown-it: + optional: true + + '@mdit/plugin-footnote@0.16.0': + resolution: {integrity: sha512-vaJWhOsya7bYfplLlMHYBxGTbME0e46/eTVKBROemWtAf873DTkV4IhkAq7MzGqeYrw0L9gxQPgGDFphGfySMA==} + engines: {node: '>= 18'} + peerDependencies: + markdown-it: ^14.1.0 + + '@mdit/plugin-icon@0.16.5': + resolution: {integrity: sha512-9T34gnNrjCMdqNLnC1oi+kZT1iCnwlHAtH3D7sjVkcP8Cw4GoDoAGy50oyryivDlczrKubOFtF05lYAfXZauuA==} + peerDependencies: + markdown-it: ^14.1.0 + peerDependenciesMeta: + markdown-it: + optional: true + + '@mdit/plugin-img-lazyload@0.16.0': + resolution: {integrity: sha512-Ilf3e5SKG7hd+RAoYQalpjoz8LMCxCe3BBHFYerv8u4wLnKe/L0Gqc8kXSpR37flzv3Ncw/NMqmD4ZZ0QQnK9A==} + engines: {node: '>= 18'} + peerDependencies: + markdown-it: ^14.1.0 + peerDependenciesMeta: + markdown-it: + optional: true + + '@mdit/plugin-img-mark@0.16.0': + resolution: {integrity: sha512-BUYqQRWUxNKB0BbMb8SZtlTeDZNXxuJ9AuiuB54RIWlbx3iRlQkbQI3B/AxTT5/EbRMDhxOq0R8PumBuA1gNFA==} + engines: {node: '>= 18'} + peerDependencies: + markdown-it: ^14.1.0 + peerDependenciesMeta: + markdown-it: + optional: true + + '@mdit/plugin-img-size@0.16.0': + resolution: {integrity: sha512-4FBvIHYWT22bjU+kO1I00xLtnCi7aXdZ7QD3CJnK4Xl6gN8/WB9IkfqYnBPv8yDiaZrabduQo8Dh8Dm8hPOm2A==} + engines: {node: '>= 18'} + peerDependencies: + markdown-it: ^14.1.0 + peerDependenciesMeta: + markdown-it: + optional: true + + '@mdit/plugin-include@0.16.0': + resolution: {integrity: sha512-9ESwsc+/jYkS0hIzpWqMQ9bHgHG//35datnfp0KUOql/DSuLVhufPtNkKNe/SVNO/+AOBTTlRYzej9Jl7JjD7g==} + peerDependencies: + markdown-it: ^14.1.0 + peerDependenciesMeta: + markdown-it: + optional: true + + '@mdit/plugin-katex-slim@0.16.2': + resolution: {integrity: sha512-NVq2fL6Zlbd/se8a69qGoOJ43wfQ+WJ33oIPDuh/n+pBtOnXS2P8oq01k/peen40wdPQBo62rQDxTgd+sMCOsA==} + engines: {node: '>= 18'} + peerDependencies: + katex: ^0.16.9 + markdown-it: ^14.1.0 + peerDependenciesMeta: + katex: + optional: true + markdown-it: + optional: true + + '@mdit/plugin-mark@0.16.0': + resolution: {integrity: sha512-VY8HhLaNw6iO6E1pSZr3bG6MzyxcAdQmQ+S0r/l87S0EKHCBrUJusaUjxa9aTVHiBcgGUjg9aumribGrWfuitA==} + engines: {node: '>= 18'} + peerDependencies: + markdown-it: ^14.1.0 + peerDependenciesMeta: + markdown-it: + optional: true + + '@mdit/plugin-mathjax-slim@0.16.0': + resolution: {integrity: sha512-bbo6HtNOFdNMGZH/pxc3X1vZOvOW1FF9RMiAW2pkmyk7sPnMziB8uwxm0Ra1RajEC/NDxJ3wcF7xynkLmS6PfA==} + engines: {node: '>= 18'} + peerDependencies: + markdown-it: ^14.1.0 + mathjax-full: ^3.2.2 + peerDependenciesMeta: + markdown-it: + optional: true + mathjax-full: + optional: true + + '@mdit/plugin-plantuml@0.16.0': + resolution: {integrity: sha512-ZjGOWYxPcGFq/TAJ2wOU6vCYH82685ERFQAC+xUsd/f6G41oGmk5i2aNqfNYYPmoQvcPvimGUPky9L6k2IXKXw==} + peerDependencies: + markdown-it: ^14.1.0 + peerDependenciesMeta: + markdown-it: + optional: true + + '@mdit/plugin-spoiler@0.16.0': + resolution: {integrity: sha512-lm2lLx5H6649igzmbEe7KGsYfS6EOHn3Ps1ZdOHIFo0AY9eEh//gbjPOuJNJU58vtMnzLYzQHQKp/JqViYTIQQ==} + engines: {node: '>= 18'} + peerDependencies: + markdown-it: ^14.1.0 + peerDependenciesMeta: + markdown-it: + optional: true + + '@mdit/plugin-stylize@0.16.0': + resolution: {integrity: sha512-uxM9aFdgS5YCXOSNSdYyC+uXyCnmqv1VUPRNAv0g/iOts0pUp63ZEUEO2sNlbXj1rGGEWylXyXqh3OU9rRngzg==} + engines: {node: '>= 18'} + peerDependencies: + markdown-it: ^14.1.0 + peerDependenciesMeta: + markdown-it: + optional: true + + '@mdit/plugin-sub@0.16.0': + resolution: {integrity: sha512-XpGcZW11SAWuiWtx9aYugM67OLtQJSfN87Q/aZbEfm6ahgdbO5lAe/vBFTBmL9aDc2EVatytGeZL3kA7pfHlOA==} + engines: {node: '>= 18'} + peerDependencies: + markdown-it: ^14.1.0 + peerDependenciesMeta: + markdown-it: + optional: true + + '@mdit/plugin-sup@0.16.0': + resolution: {integrity: sha512-45Sws9TC9h9ZRB/IcXAae+uYXb+FkVr/rkr9eMYKMFKksjMBddN+WY3Gpl9O7LhaGPipqTkm68QZnRSS1jvFkw==} + engines: {node: '>= 18'} + peerDependencies: + markdown-it: ^14.1.0 + peerDependenciesMeta: + markdown-it: + optional: true + + '@mdit/plugin-tab@0.16.0': + resolution: {integrity: sha512-c+/oT319DIWaMHyx5chueW8cy4pjC7E09QOg3qp86abTCdG2ljGLOlMAQbst5i/iH684QG/i8EJpB4oUeQdhkw==} + peerDependencies: + markdown-it: ^14.1.0 + peerDependenciesMeta: + markdown-it: + optional: true + + '@mdit/plugin-tasklist@0.16.0': + resolution: {integrity: sha512-pxVxartDd8LYxhdYxyrh4c7JEAq+4cEMLI1HNCHTMK9cfO+SoVd/YpibfrDUg+LHvffc8Pf2Yc8pWXNoW34B1g==} + engines: {node: '>= 18'} + peerDependencies: + markdown-it: ^14.1.0 + peerDependenciesMeta: + markdown-it: + optional: true + + '@mdit/plugin-tex@0.16.0': + resolution: {integrity: sha512-VWb5rJYP0eBRRjYhcaRE3r8UQkUaBXzu0l42ck7DOp+MSPsgXfS+bmk8/tyHG6/X/Mig9H92Lh1jzTqp3f5yKg==} + engines: {node: '>= 18'} + peerDependencies: + markdown-it: ^14.1.0 + peerDependenciesMeta: + markdown-it: + optional: true + + '@mdit/plugin-uml@0.16.0': + resolution: {integrity: sha512-BIsq6PpmRgoThtVR2j4BGiRGis6jrcxxqQW3RICacrG52Ps2RWEGwu7B/IvXs+KJZJLJsrKFQ2Pqaxttbjx3kw==} + engines: {node: '>= 18'} + peerDependencies: + markdown-it: ^14.1.0 + peerDependenciesMeta: + markdown-it: + optional: true + + '@nodelib/fs.scandir@2.1.5': + resolution: {integrity: sha512-vq24Bq3ym5HEQm2NKCr3yXDwjc7vTsEThRDnkp2DK9p1uqLR+DHurm/NOTo0KG7HYHU7eppKZj3MyqYuMBf62g==} + engines: {node: '>= 8'} + + '@nodelib/fs.stat@2.0.5': + resolution: {integrity: sha512-RkhPPp2zrqDAQA/2jNhnztcPAlv64XdhIp7a7454A5ovI7Bukxgt7MX7udwAu3zg1DcpPU0rz3VV1SeaqvY4+A==} + engines: {node: '>= 8'} + + '@nodelib/fs.walk@1.2.8': + resolution: {integrity: sha512-oGB+UxlgWcgQkgwo8GcEGwemoTFt3FIO9ababBmaGwXIoBKZ+GTy0pP185beGg7Llih/NSHSV2XAs1lnznocSg==} + engines: {node: '>= 8'} + + '@npmcli/agent@2.2.2': + resolution: {integrity: sha512-OrcNPXdpSl9UX7qPVRWbmWMCSXrcDa2M9DvrbOTj7ao1S4PlqVFYv9/yLKMkrJKZ/V5A/kDBC690or307i26Og==} + engines: {node: ^16.14.0 || >=18.0.0} + + '@npmcli/fs@3.1.1': + resolution: {integrity: sha512-q9CRWjpHCMIh5sVyefoD1cA7PkvILqCZsnSOEUUivORLjxCO/Irmue2DprETiNgEqktDBZaM1Bi+jrarx1XdCg==} + engines: {node: ^14.17.0 || ^16.13.0 || >=18.0.0} + + '@pkgjs/parseargs@0.11.0': + resolution: {integrity: sha512-+1VkjdD0QBLPodGrJUeqarH8VAIvQODIbwh9XpP5Syisf7YoQgsJKPNFoqqLQlu+VQ/tVSshMR6loPMn8U+dPg==} + engines: {node: '>=14'} + + '@rollup/rollup-android-arm-eabi@4.30.1': + resolution: {integrity: sha512-pSWY+EVt3rJ9fQ3IqlrEUtXh3cGqGtPDH1FQlNZehO2yYxCHEX1SPsz1M//NXwYfbTlcKr9WObLnJX9FsS9K1Q==} + cpu: [arm] + os: [android] + + '@rollup/rollup-android-arm64@4.30.1': + resolution: {integrity: sha512-/NA2qXxE3D/BRjOJM8wQblmArQq1YoBVJjrjoTSBS09jgUisq7bqxNHJ8kjCHeV21W/9WDGwJEWSN0KQ2mtD/w==} + cpu: [arm64] + os: [android] + + '@rollup/rollup-darwin-arm64@4.30.1': + resolution: {integrity: sha512-r7FQIXD7gB0WJ5mokTUgUWPl0eYIH0wnxqeSAhuIwvnnpjdVB8cRRClyKLQr7lgzjctkbp5KmswWszlwYln03Q==} + cpu: [arm64] + os: [darwin] + + '@rollup/rollup-darwin-x64@4.30.1': + resolution: {integrity: sha512-x78BavIwSH6sqfP2xeI1hd1GpHL8J4W2BXcVM/5KYKoAD3nNsfitQhvWSw+TFtQTLZ9OmlF+FEInEHyubut2OA==} + cpu: [x64] + os: [darwin] + + '@rollup/rollup-freebsd-arm64@4.30.1': + resolution: {integrity: sha512-HYTlUAjbO1z8ywxsDFWADfTRfTIIy/oUlfIDmlHYmjUP2QRDTzBuWXc9O4CXM+bo9qfiCclmHk1x4ogBjOUpUQ==} + cpu: [arm64] + os: [freebsd] + + '@rollup/rollup-freebsd-x64@4.30.1': + resolution: {integrity: sha512-1MEdGqogQLccphhX5myCJqeGNYTNcmTyaic9S7CG3JhwuIByJ7J05vGbZxsizQthP1xpVx7kd3o31eOogfEirw==} + cpu: [x64] + os: [freebsd] + + '@rollup/rollup-linux-arm-gnueabihf@4.30.1': + resolution: {integrity: sha512-PaMRNBSqCx7K3Wc9QZkFx5+CX27WFpAMxJNiYGAXfmMIKC7jstlr32UhTgK6T07OtqR+wYlWm9IxzennjnvdJg==} + cpu: [arm] + os: [linux] + libc: [glibc] + + '@rollup/rollup-linux-arm-musleabihf@4.30.1': + resolution: {integrity: sha512-B8Rcyj9AV7ZlEFqvB5BubG5iO6ANDsRKlhIxySXcF1axXYUyqwBok+XZPgIYGBgs7LDXfWfifxhw0Ik57T0Yug==} + cpu: [arm] + os: [linux] + libc: [musl] + + '@rollup/rollup-linux-arm64-gnu@4.30.1': + resolution: {integrity: sha512-hqVyueGxAj3cBKrAI4aFHLV+h0Lv5VgWZs9CUGqr1z0fZtlADVV1YPOij6AhcK5An33EXaxnDLmJdQikcn5NEw==} + cpu: [arm64] + os: [linux] + libc: [glibc] + + '@rollup/rollup-linux-arm64-musl@4.30.1': + resolution: {integrity: sha512-i4Ab2vnvS1AE1PyOIGp2kXni69gU2DAUVt6FSXeIqUCPIR3ZlheMW3oP2JkukDfu3PsexYRbOiJrY+yVNSk9oA==} + cpu: [arm64] + os: [linux] + libc: [musl] + + '@rollup/rollup-linux-loongarch64-gnu@4.30.1': + resolution: {integrity: sha512-fARcF5g296snX0oLGkVxPmysetwUk2zmHcca+e9ObOovBR++9ZPOhqFUM61UUZ2EYpXVPN1redgqVoBB34nTpQ==} + cpu: [loong64] + os: [linux] + libc: [glibc] + + '@rollup/rollup-linux-powerpc64le-gnu@4.30.1': + resolution: {integrity: sha512-GLrZraoO3wVT4uFXh67ElpwQY0DIygxdv0BNW9Hkm3X34wu+BkqrDrkcsIapAY+N2ATEbvak0XQ9gxZtCIA5Rw==} + cpu: [ppc64] + os: [linux] + libc: [glibc] + + '@rollup/rollup-linux-riscv64-gnu@4.30.1': + resolution: {integrity: sha512-0WKLaAUUHKBtll0wvOmh6yh3S0wSU9+yas923JIChfxOaaBarmb/lBKPF0w/+jTVozFnOXJeRGZ8NvOxvk/jcw==} + cpu: [riscv64] + os: [linux] + libc: [glibc] + + '@rollup/rollup-linux-s390x-gnu@4.30.1': + resolution: {integrity: sha512-GWFs97Ruxo5Bt+cvVTQkOJ6TIx0xJDD/bMAOXWJg8TCSTEK8RnFeOeiFTxKniTc4vMIaWvCplMAFBt9miGxgkA==} + cpu: [s390x] + os: [linux] + libc: [glibc] + + '@rollup/rollup-linux-x64-gnu@4.30.1': + resolution: {integrity: sha512-UtgGb7QGgXDIO+tqqJ5oZRGHsDLO8SlpE4MhqpY9Llpzi5rJMvrK6ZGhsRCST2abZdBqIBeXW6WPD5fGK5SDwg==} + cpu: [x64] + os: [linux] + libc: [glibc] + + '@rollup/rollup-linux-x64-musl@4.30.1': + resolution: {integrity: sha512-V9U8Ey2UqmQsBT+xTOeMzPzwDzyXmnAoO4edZhL7INkwQcaW1Ckv3WJX3qrrp/VHaDkEWIBWhRwP47r8cdrOow==} + cpu: [x64] + os: [linux] + libc: [musl] + + '@rollup/rollup-win32-arm64-msvc@4.30.1': + resolution: {integrity: sha512-WabtHWiPaFF47W3PkHnjbmWawnX/aE57K47ZDT1BXTS5GgrBUEpvOzq0FI0V/UYzQJgdb8XlhVNH8/fwV8xDjw==} + cpu: [arm64] + os: [win32] + + '@rollup/rollup-win32-ia32-msvc@4.30.1': + resolution: {integrity: sha512-pxHAU+Zv39hLUTdQQHUVHf4P+0C47y/ZloorHpzs2SXMRqeAWmGghzAhfOlzFHHwjvgokdFAhC4V+6kC1lRRfw==} + cpu: [ia32] + os: [win32] + + '@rollup/rollup-win32-x64-msvc@4.30.1': + resolution: {integrity: sha512-D6qjsXGcvhTjv0kI4fU8tUuBDF/Ueee4SVX79VfNDXZa64TfCW1Slkb6Z7O1p7vflqZjcmOVdZlqf8gvJxc6og==} + cpu: [x64] + os: [win32] + + '@sec-ant/readable-stream@0.4.1': + resolution: {integrity: sha512-831qok9r2t8AlxLko40y2ebgSDhenenCatLVeW/uBtnHPyhHOvG0C7TvfgecV+wHzIm5KUICgzmVpWS+IMEAeg==} + + '@shikijs/core@1.26.2': + resolution: {integrity: sha512-ORyu3MrY7dCC7FDLDsFSkBM9b/AT9/Y8rH+UQ07Rtek48pp0ZhQOMPTKolqszP4bBCas6FqTZQYt18BBamVl/g==} + + '@shikijs/engine-javascript@1.26.2': + resolution: {integrity: sha512-ngkIu9swLVo9Zt5QBtz5Sk08vmPcwuj01r7pPK/Zjmo2U2WyKMK4WMUMmkdQiUacdcLth0zt8u1onp4zhkFXKQ==} + + '@shikijs/engine-oniguruma@1.26.2': + resolution: {integrity: sha512-mlN7Qrs+w60nKrd7at7XkXSwz6728Pe34taDmHrG6LRHjzCqQ+ysg+/AT6/D2LMk0s2lsr71DjpI73430QP4/w==} + + '@shikijs/langs@1.26.2': + resolution: {integrity: sha512-o5cdPycB2Kw3IgncHxWopWPiTkjAj7dG01fLkkUyj3glb5ftxL/Opecq9F54opMlrgXy7ZIqDERvFLlUzsCOuA==} + + '@shikijs/themes@1.26.2': + resolution: {integrity: sha512-y4Pn6PM5mODz/e3yF6jAUG7WLKJzqL2tJ5qMJCUkMUB1VRgtQVvoa1cHh7NScryGXyrYGJ8nPnRDhdv2rw0xpA==} + + '@shikijs/transformers@1.26.2': + resolution: {integrity: sha512-nAwivOhYDKudYsX9xOmA9ekkqYv+Q/IadX5ca0nV7qPTN+wf/tXHrjxVmJJlsEVtakCEuMR0a0AVL+V9QZxi7w==} + + '@shikijs/types@1.26.2': + resolution: {integrity: sha512-PO2jucx2FIdlLBPYbIUlMtWSLs5ulcRcuV93cR3T65lkK5SJP4MGBRt9kmWGXiQc0f7+FHj/0BEawditZcI/fQ==} + + '@shikijs/vscode-textmate@10.0.1': + resolution: {integrity: sha512-fTIQwLF+Qhuws31iw7Ncl1R3HUDtGwIipiJ9iU+UsDUwMhegFcQKQHd51nZjb7CArq0MvON8rbgCGQYWHUKAdg==} + + '@sindresorhus/merge-streams@2.3.0': + resolution: {integrity: sha512-LtoMMhxAlorcGhmFYI+LhPgbPZCkgP6ra1YL604EeF6U98pLlQ3iWIGMdWSC+vWmPBWBNgmDBAhnAobLROJmwg==} + engines: {node: '>=18'} + + '@sindresorhus/merge-streams@4.0.0': + resolution: {integrity: sha512-tlqY9xq5ukxTUZBmoOp+m61cqwQD5pHJtFY3Mn8CA8ps6yghLH/Hw8UPdqg4OLmFW3IFlcXnQNmo/dh8HzXYIQ==} + engines: {node: '>=18'} + + '@stackblitz/sdk@1.11.0': + resolution: {integrity: sha512-DFQGANNkEZRzFk1/rDP6TcFdM82ycHE+zfl9C/M/jXlH68jiqHWHFMQURLELoD8koxvu/eW5uhg94NSAZlYrUQ==} + + '@types/debug@4.1.12': + resolution: {integrity: sha512-vIChWdVG3LG1SMxEvI/AK+FWJthlrqlTu7fbrlywTkkaONwk/UAGaULXRlf8vkzFBLVm0zkMdCquhL5aOjhXPQ==} + + '@types/estree@1.0.6': + resolution: {integrity: sha512-AYnb1nQyY49te+VRAVgmzfcgjYS91mY5P0TKUDCLEM+gNnA+3T6rWITXRLYCpahpqSQbN5cE+gHpnPyXjHWxcw==} + + '@types/fs-extra@11.0.4': + resolution: {integrity: sha512-yTbItCNreRooED33qjunPthRcSjERP1r4MqCZc7wv0u2sUkzTFp45tgUfS5+r7FrZPdmCCNflLhVSP/o+SemsQ==} + + '@types/hash-sum@1.0.2': + resolution: {integrity: sha512-UP28RddqY8xcU0SCEp9YKutQICXpaAq9N8U2klqF5hegGha7KzTOL8EdhIIV3bOSGBzjEpN9bU/d+nNZBdJYVw==} + + '@types/hast@3.0.4': + resolution: {integrity: sha512-WPs+bbQw5aCj+x6laNGWLH3wviHtoCv/P3+otBhbOhJgG8qtpdAMlTCxLtsTWA7LH1Oh/bFCHsBn0TPS5m30EQ==} + + '@types/jsonfile@6.1.4': + resolution: {integrity: sha512-D5qGUYwjvnNNextdU59/+fI+spnwtTFmyQP0h+PfIOSkNfpU6AOICUOkm4i0OnSk+NyjdPJrxCDro0sJsWlRpQ==} + + '@types/katex@0.16.7': + resolution: {integrity: sha512-HMwFiRujE5PjrgwHQ25+bsLJgowjGjm5Z8FVSf0N6PwgJrwxH0QxzHYDcKsTfV3wva0vzrpqMTJS2jXPr5BMEQ==} + + '@types/linkify-it@5.0.0': + resolution: {integrity: sha512-sVDA58zAw4eWAffKOaQH5/5j3XeayukzDk+ewSsnv3p4yJEZHCCzMDiZM8e0OUrRvmpGZ85jf4yDHkHsgBNr9Q==} + + '@types/markdown-it-emoji@3.0.1': + resolution: {integrity: sha512-cz1j8R35XivBqq9mwnsrP2fsz2yicLhB8+PDtuVkKOExwEdsVBNI+ROL3sbhtR5occRZ66vT0QnwFZCqdjf3pA==} + + '@types/markdown-it@14.1.2': + resolution: {integrity: sha512-promo4eFwuiW+TfGxhi+0x3czqTYJkG8qB17ZUJiVF10Xm7NLVRSLUsfRTU/6h1e24VvRnXCx+hG7li58lkzog==} + + '@types/mdast@4.0.4': + resolution: {integrity: sha512-kGaNbPh1k7AFzgpud/gMdvIm5xuECykRR+JnWKQno9TAXVa6WIVCGTPvYGekIDL4uwCZQSYbUxNBSb1aUo79oA==} + + '@types/mdurl@2.0.0': + resolution: {integrity: sha512-RGdgjQUZba5p6QEFAVx2OGb8rQDL/cPRG7GiedRzMcJ1tYnUANBncjbSB1NRGwbvjcPeikRABz2nshyPk1bhWg==} + + '@types/ms@0.7.34': + resolution: {integrity: sha512-nG96G3Wp6acyAgJqGasjODb+acrI7KltPiRxzHPXnP3NgI28bpQDRv53olbqGXbfcgF5aiiHmO3xpwEpS5Ld9g==} + + '@types/node@17.0.45': + resolution: {integrity: sha512-w+tIMs3rq2afQdsPJlODhoUEKzFP1ayaoyl1CcnwtIlsVe7K7bA1NGm4s3PraqTLlXnbIN84zuBlxBWo1u9BLw==} + + '@types/node@22.10.5': + resolution: {integrity: sha512-F8Q+SeGimwOo86fiovQh8qiXfFEh2/ocYv7tU5pJ3EXMSSxk1Joj5wefpFK2fHTf/N6HKGSxIDBT9f3gCxXPkQ==} + + '@types/sax@1.2.7': + resolution: {integrity: sha512-rO73L89PJxeYM3s3pPPjiPgVVcymqU490g0YO5n5By0k2Erzj6tay/4lr1CHAAU4JyOWd1rpQ8bCf6cZfHU96A==} + + '@types/trusted-types@2.0.7': + resolution: {integrity: sha512-ScaPdn1dQczgbl0QFTeTOmVHFULt394XJgOQNoyVhZ6r2vLnMLJfBPd53SB52T/3G36VI1/g2MZaX0cwDuXsfw==} + + '@types/unist@2.0.11': + resolution: {integrity: sha512-CmBKiL6NNo/OqgmMn95Fk9Whlp2mtvIv+KNpQKN2F4SjvrEesubTRWGYSg+BnWZOnlCaSTU1sMpsBOzgbYhnsA==} + + '@types/unist@3.0.3': + resolution: {integrity: sha512-ko/gIFJRv177XgZsZcBwnqJN5x/Gien8qNOn0D5bQU/zAzVf9Zt3BlcUiLqhV9y4ARk0GbT3tnUiPNgnTXzc/Q==} + + '@types/web-bluetooth@0.0.20': + resolution: {integrity: sha512-g9gZnnXVq7gM7v3tJCWV/qw7w+KeOlSHAhgF9RytFyifW6AF61hdT2ucrYhPq9hLs5JIryeupHV3qGk95dH9ow==} + + '@ungap/structured-clone@1.2.1': + resolution: {integrity: sha512-fEzPV3hSkSMltkw152tJKNARhOupqbH96MZWyRjNaYZOMIzbrTeQDG+MTc6Mr2pgzFQzFxAfmhGDNP5QK++2ZA==} + + '@vitejs/plugin-vue@5.2.1': + resolution: {integrity: sha512-cxh314tzaWwOLqVes2gnnCtvBDcM1UMdn+iFR+UjAn411dPT3tOmqrJjbMd7koZpMAmBM/GqeV4n9ge7JSiJJQ==} + engines: {node: ^18.0.0 || >=20.0.0} + peerDependencies: + vite: ^5.0.0 || ^6.0.0 + vue: ^3.2.25 + + '@vue/compiler-core@3.5.13': + resolution: {integrity: sha512-oOdAkwqUfW1WqpwSYJce06wvt6HljgY3fGeM9NcVA1HaYOij3mZG9Rkysn0OHuyUAGMbEbARIpsG+LPVlBJ5/Q==} + + '@vue/compiler-dom@3.5.13': + resolution: {integrity: sha512-ZOJ46sMOKUjO3e94wPdCzQ6P1Lx/vhp2RSvfaab88Ajexs0AHeV0uasYhi99WPaogmBlRHNRuly8xV75cNTMDA==} + + '@vue/compiler-sfc@3.5.13': + resolution: {integrity: sha512-6VdaljMpD82w6c2749Zhf5T9u5uLBWKnVue6XWxprDobftnletJ8+oel7sexFfM3qIxNmVE7LSFGTpv6obNyaQ==} + + '@vue/compiler-ssr@3.5.13': + resolution: {integrity: sha512-wMH6vrYHxQl/IybKJagqbquvxpWCuVYpoUJfCqFZwa/JY1GdATAQ+TgVtgrwwMZ0D07QhA99rs/EAAWfvG6KpA==} + + '@vue/devtools-api@6.6.4': + resolution: {integrity: sha512-sGhTPMuXqZ1rVOk32RylztWkfXTRhuS7vgAKv0zjqk8gbsHkJ7xfFf+jbySxt7tWObEJwyKaHMikV/WGDiQm8g==} + + '@vue/devtools-api@7.7.0': + resolution: {integrity: sha512-bHEv6kT85BHtyGgDhE07bAUMAy7zpv6nnR004nSTd0wWMrAOtcrYoXO5iyr20Hkf5jR8obQOfS3byW+I3l2CCA==} + + '@vue/devtools-kit@7.7.0': + resolution: {integrity: sha512-5cvZ+6SA88zKC8XiuxUfqpdTwVjJbvYnQZY5NReh7qlSGPvVDjjzyEtW+gdzLXNSd8tStgOjAdMCpvDQamUXtA==} + + '@vue/devtools-shared@7.7.0': + resolution: {integrity: sha512-jtlQY26R5thQxW9YQTpXbI0HoK0Wf9Rd4ekidOkRvSy7ChfK0kIU6vvcBtjj87/EcpeOSK49fZAicaFNJcoTcQ==} + + '@vue/reactivity@3.5.13': + resolution: {integrity: sha512-NaCwtw8o48B9I6L1zl2p41OHo/2Z4wqYGGIK1Khu5T7yxrn+ATOixn/Udn2m+6kZKB/J7cuT9DbWWhRxqixACg==} + + '@vue/runtime-core@3.5.13': + resolution: {integrity: sha512-Fj4YRQ3Az0WTZw1sFe+QDb0aXCerigEpw418pw1HBUKFtnQHWzwojaukAs2X/c9DQz4MQ4bsXTGlcpGxU/RCIw==} + + '@vue/runtime-dom@3.5.13': + resolution: {integrity: sha512-dLaj94s93NYLqjLiyFzVs9X6dWhTdAlEAciC3Moq7gzAc13VJUdCnjjRurNM6uTLFATRHexHCTu/Xp3eW6yoog==} + + '@vue/server-renderer@3.5.13': + resolution: {integrity: sha512-wAi4IRJV/2SAW3htkTlB+dHeRmpTiVIK1OGLWV1yeStVSebSQQOwGwIq0D3ZIoBj2C2qpgz5+vX9iEBkTdk5YA==} + peerDependencies: + vue: 3.5.13 + + '@vue/shared@3.5.13': + resolution: {integrity: sha512-/hnE/qP5ZoGpol0a5mDi45bOd7t3tjYJBjsgCsivow7D48cJeV5l05RD82lPqi7gRiphZM37rnhW1l6ZoCNNnQ==} + + '@vuepress/bundler-vite@2.0.0-rc.19': + resolution: {integrity: sha512-Vn0wEVRcdAld+8NJeELSwrj5JEPObRn0xpRWtAau/UwVWHmMLo16RRkTvXdjSiwpDWeP/9ztC5buyTXVoeb7Dw==} + + '@vuepress/bundlerutils@2.0.0-rc.19': + resolution: {integrity: sha512-ln5htptK14OMJV3yeGRxAwYhSkVxrTwEHEaifeWrFvjuNxj2kLmkCl7MDdzr232jSOWwkCcmbOyafbxMsaRDkQ==} + + '@vuepress/cli@2.0.0-rc.19': + resolution: {integrity: sha512-QFicPNIj3RZAJbHoLbeYlPJsPchnQLGuw0n8xv0eeUi9ejEXO1huWA8sLoPbTGdiDW+PHr1MHnaVMkyUfwaKcQ==} + hasBin: true + + '@vuepress/client@2.0.0-rc.19': + resolution: {integrity: sha512-vUAU6n4qmtXqthxkb4LHq0D+VWSDenwBDf0jUs7RaBLuOVrbPtmH/hs4k1vLIlGdwC3Zs/G6tlB4UmuZiiwR8Q==} + + '@vuepress/core@2.0.0-rc.19': + resolution: {integrity: sha512-rvmBPMIWS2dey/2QjxZoO0OcrUU46NE3mSLk3oU7JOP0cG7xvRxf6U1OXiwYLC3fPO4g6XbHiKe6gihkmL6VDA==} + + '@vuepress/helper@2.0.0-rc.70': + resolution: {integrity: sha512-3v8m0x9GyPY3TC+GFBJ8eNQ0Pa3qYLXfT5wK4HtZw+ti4dff6fNufqUtH63a2CgTKMI0BHrdUddw/lmI1LobPw==} + peerDependencies: + vuepress: 2.0.0-rc.19 + + '@vuepress/highlighter-helper@2.0.0-rc.70': + resolution: {integrity: sha512-YNSY22RqLTvpp8ZJ6UQtJPwpqytWBj9EkxUcBX3zf+7p4+QgMg8gdjvKAS/UKC3n2eNFBEH1y+ZRytQBVMW/9g==} + peerDependencies: + '@vueuse/core': ^12.2.0 + vuepress: 2.0.0-rc.19 + peerDependenciesMeta: + '@vueuse/core': + optional: true + + '@vuepress/markdown@2.0.0-rc.19': + resolution: {integrity: sha512-6jgUXhpEK55PEEGtPhz7Hq/JqTbLU8n9w2D7emXiK2FYcbeKpjoRIbVRzmzB/dXeK3NzHChANu2IIqpOT6Ba1w==} + + '@vuepress/plugin-active-header-links@2.0.0-rc.70': + resolution: {integrity: sha512-t20HQsVTzkVH+nGyaaBtllV/xR4UKU/+yRSnUOo7jpbdHIpKAppke6JwOTVQAnSTDbTLqX7sD6LmI7WrVBmCVw==} + peerDependencies: + vuepress: 2.0.0-rc.19 + + '@vuepress/plugin-back-to-top@2.0.0-rc.70': + resolution: {integrity: sha512-zcHN13tTSl2lK+OwStrYpp553I41GvWFf0Havr2DHJ4LlyZBEvzLzcqwJ4kZhyGdU1u0nstkyzqkEyi5PsjlJw==} + peerDependencies: + vuepress: 2.0.0-rc.19 + + '@vuepress/plugin-blog@2.0.0-rc.70': + resolution: {integrity: sha512-2+lprMHSRbUb2GHhV4mqMmoEZjHdYyX08Q2Tp9A+v4EJ0SNcIbic2IVgpbyysRz9DXjMaTvojkensFZwHWiNvA==} + peerDependencies: + vuepress: 2.0.0-rc.19 + + '@vuepress/plugin-catalog@2.0.0-rc.70': + resolution: {integrity: sha512-mvdA4iTL6sPXjcKFIFGC4aYTye30R5xNJrezkodbmrc/bWfBSqc5NPbOXxBY4hSTYgiVOWx+dVVACIi8O6dBWg==} + peerDependencies: + vuepress: 2.0.0-rc.19 + + '@vuepress/plugin-comment@2.0.0-rc.70': + resolution: {integrity: sha512-AweFgxY75t2TzNKD3d+9ytBPHLT6i2OpPsJWkrwCnfx6IjKT0SExl9uzRDYa7Y9YDVKBZhDtmEMhs7BQEghq4Q==} + peerDependencies: + '@waline/client': ^3.4.3 + artalk: ^2.9.0 + twikoo: ^1.6.39 + vuepress: 2.0.0-rc.19 + peerDependenciesMeta: + '@waline/client': + optional: true + artalk: + optional: true + twikoo: + optional: true + + '@vuepress/plugin-copy-code@2.0.0-rc.70': + resolution: {integrity: sha512-hTYP2/SJ7qoD7NCEt9a9evZvaw+gfuHh60YISHlY/LmwJoicQyGMSjetraubWMRJFvaL77nGw4JtFxmzvKMqDA==} + peerDependencies: + vuepress: 2.0.0-rc.19 + + '@vuepress/plugin-copyright@2.0.0-rc.70': + resolution: {integrity: sha512-5W+ymDFKaGMw49R/lL73tr7x5ibhHP3YxvjdC8S1ftbP0sAvb5xekE2CXVxTxdJYTHMv1QlF4JLYgUq3G2Powg==} + peerDependencies: + vuepress: 2.0.0-rc.19 + + '@vuepress/plugin-feed@2.0.0-rc.70': + resolution: {integrity: sha512-hzAaxh8xi+X8bz/60tmzgT86jc4koSOIPRh7V5iwVrqNcBswk76OxGO+/3bEd8Tt45RXUatwaN3O5RTrjpoU5A==} + peerDependencies: + vuepress: 2.0.0-rc.19 + + '@vuepress/plugin-git@2.0.0-rc.68': + resolution: {integrity: sha512-k/tXBSIyQM26UrmDK/mN1/q6gw8PmF2uLyIaso+B39qCOFQKUBq4uJF2a0oYTq9tpjM5AHwwBpytPE5cdV/BPQ==} + peerDependencies: + vuepress: 2.0.0-rc.19 + + '@vuepress/plugin-icon@2.0.0-rc.70': + resolution: {integrity: sha512-nQLr5LnfO9pTynqmHIWPXhUZpqzKaxP0JjFW1lbIDOn5F3gLZ04vxcAPvtrJYis0erwmlCR6/yx20u8bGvakIg==} + peerDependencies: + vuepress: 2.0.0-rc.19 + + '@vuepress/plugin-links-check@2.0.0-rc.70': + resolution: {integrity: sha512-ZqqU9pjtr7O0lvOOk6bxtWj5EvktL8PMegRpRv2a10uN0kQGXljGg5XihmC2EW3AlVBl3je9gw40+DVHocMDVw==} + peerDependencies: + vuepress: 2.0.0-rc.19 + + '@vuepress/plugin-markdown-ext@2.0.0-rc.70': + resolution: {integrity: sha512-NwB2cAmATP8Nd3mBLPvLDANKXAFj2VxB9HZUOb9/Vrpt3+otSRQ17mBjUwnBllbnE9Yjz1zcAU6HjKuMdkJfEw==} + peerDependencies: + vuepress: 2.0.0-rc.19 + + '@vuepress/plugin-markdown-hint@2.0.0-rc.70': + resolution: {integrity: sha512-yd9HAKRw9hcG8jsatSpA5H6CVFFm9c8eXBq1OGKsm/1kCCLcznKdc6bdnL6c24ieOrS5se0IL+daCoaklruscQ==} + peerDependencies: + vuepress: 2.0.0-rc.19 + + '@vuepress/plugin-markdown-image@2.0.0-rc.70': + resolution: {integrity: sha512-Ao0WnZ0tR3HRClDNhGIFi3+/lhgqrhoU1HWRsfnWx1ONcvTTNZfuR2TrLzKvkPiDzg1D05HZaFH3PggCun9V8Q==} + peerDependencies: + vuepress: 2.0.0-rc.19 + + '@vuepress/plugin-markdown-include@2.0.0-rc.70': + resolution: {integrity: sha512-4C4HOieqdIThAF/zi6h5bWkw7l22qUn9vmR5PGPNmnQSZ2anr5L0+ulIYOOElGZxlXL35Io0FD749fXrV2alVg==} + peerDependencies: + vuepress: 2.0.0-rc.19 + + '@vuepress/plugin-markdown-math@2.0.0-rc.70': + resolution: {integrity: sha512-PSqMqA7C5d6z5qfhIYbDgCZC3AL+P0644dIEgOMDG4Twg9S6LtDawkbILOshT0nb6RVLlA9gy/0ME5yIc/+zjw==} + peerDependencies: + katex: ^0.16.10 + mathjax-full: ^3.2.2 + vuepress: 2.0.0-rc.19 + peerDependenciesMeta: + katex: + optional: true + mathjax-full: + optional: true + + '@vuepress/plugin-markdown-stylize@2.0.0-rc.70': + resolution: {integrity: sha512-Z0PTI+ePEXpA41llpOpXiBgCJsJvkU9HAJ0cjfeWODMHJ0wa6/Y9aA3w2aXPzg6qBGaHxSAXymRJz51a4H53EA==} + peerDependencies: + vuepress: 2.0.0-rc.19 + + '@vuepress/plugin-markdown-tab@2.0.0-rc.70': + resolution: {integrity: sha512-W/tRQ8dgPiOj94GUz9apWxJHWsLC88RNS10hRX9wBlSaqZ/S9YUcI4D8nWy0uf4KYo6PbOnucJL/y66uuhlrLw==} + peerDependencies: + vuepress: 2.0.0-rc.19 + + '@vuepress/plugin-notice@2.0.0-rc.70': + resolution: {integrity: sha512-a+Z/xJopfm8OZ0SRon78D94RIpkhi8CNbFDUhQYPciS71b/pqN28OFdFPFWNU/PACcRoe3IMBcXWsq1wKZ9/Rg==} + peerDependencies: + vuepress: 2.0.0-rc.19 + + '@vuepress/plugin-nprogress@2.0.0-rc.70': + resolution: {integrity: sha512-WYhCs5X1vPbzQ5PhYTWzh5cCRjsCnE52qBlzIzftzM9SNG2Fm8tdMS3V7bmCzpg1IgALcIfwX/9r7uUVcby0OQ==} + peerDependencies: + vuepress: 2.0.0-rc.19 + + '@vuepress/plugin-photo-swipe@2.0.0-rc.70': + resolution: {integrity: sha512-z3BlKQttCMycl3u/cBVO2XVdLGEI/1qtW8Rp9u4U5G6Utv3HU5/UL3GIZmz2mNP7l3k4mOE8wnnKaxDVbvIRug==} + peerDependencies: + vuepress: 2.0.0-rc.19 + + '@vuepress/plugin-reading-time@2.0.0-rc.70': + resolution: {integrity: sha512-AemYL6ogRsstEnLiOEDcaMULJecpeftH0RpAqdUTcAPiy3gsWdn4kMgMHlvpgm1J99aE5w6d8G511Kx+i6JxAQ==} + peerDependencies: + vuepress: 2.0.0-rc.19 + + '@vuepress/plugin-redirect@2.0.0-rc.70': + resolution: {integrity: sha512-je16tvDeuphNz21hqdxQoKHCn/LVfwd6YtB3xNb5t/cEBUUAdB+byrwfUlpUVY3HWqFjr5Ot0RJuQkHFVmBJUA==} + hasBin: true + peerDependencies: + vuepress: 2.0.0-rc.19 + + '@vuepress/plugin-rtl@2.0.0-rc.70': + resolution: {integrity: sha512-2odC4M3uRGZEKYFvDLRDwyQOwAJ8YFtRI0ZrDh0paNROqssWsK9JGpYlRSDCLedwtZuQYLp8L/NN44F0kyx+fw==} + peerDependencies: + vuepress: 2.0.0-rc.19 + + '@vuepress/plugin-sass-palette@2.0.0-rc.70': + resolution: {integrity: sha512-wy17yNlmc0liQFJSoqc9XEu96Nk3daSof2ncXNuRGJnCsfXbwd0QZr6bNdZos0Zmk5+KolzIffeEBhOGnRw+VA==} + peerDependencies: + sass: ^1.80.3 + sass-embedded: ^1.80.3 + sass-loader: ^16.0.2 + vuepress: 2.0.0-rc.19 + peerDependenciesMeta: + sass: + optional: true + sass-embedded: + optional: true + sass-loader: + optional: true + + '@vuepress/plugin-search@2.0.0-rc.70': + resolution: {integrity: sha512-Av8yFH4xeC4OUhit6NWMOJ50KwMd2J/LQzGT/e70o/oDn5wV7nomKU972mdnHILIpzin/HrJP0LhvSeXZrLrNQ==} + peerDependencies: + vuepress: 2.0.0-rc.19 + + '@vuepress/plugin-seo@2.0.0-rc.70': + resolution: {integrity: sha512-8l4BYboPcCQ96h53Wzmh7eCXOL3x9XzvNjude4pJHsuU4tSl/T0rsCyZI2fPOybKJQSPhahKMiM4sU1+e+/wQg==} + peerDependencies: + vuepress: 2.0.0-rc.19 + + '@vuepress/plugin-shiki@2.0.0-rc.70': + resolution: {integrity: sha512-N4E9mnpjUwcC2MRWdTFSGE3q3X7cQeG9HlBK5/Ts6M/qd15pn1q88cOdkemMGmUYGKBJv0PMlm2g2x8OTvjs6A==} + peerDependencies: + vuepress: 2.0.0-rc.19 + + '@vuepress/plugin-sitemap@2.0.0-rc.70': + resolution: {integrity: sha512-TL9Oblicr1O9WnJnBqwMuC7VZHad6Z4pOHpEEYQKD2O9vRCnlEeP6f7xYF+xWjXXNjQasq6mYGPzTBboqL/PDA==} + peerDependencies: + vuepress: 2.0.0-rc.19 + + '@vuepress/plugin-theme-data@2.0.0-rc.70': + resolution: {integrity: sha512-xIxEvWZb+rZkUNUy4T2h1vA/YXMMflvvXRu3VHjozlnzjDqdICxFGBgqHMDdQ/9cSXKLGJNNLi4MPm4lzMuCRw==} + peerDependencies: + vuepress: 2.0.0-rc.19 + + '@vuepress/shared@2.0.0-rc.19': + resolution: {integrity: sha512-xaDeZxX0Qetc2Y6/lrzO6M/40i3LmMm7Fk85bOftBBOaNehZ24RdsmIHBJDDv+bTUv+DBF++1/mOtbt6DBRzEA==} + + '@vuepress/utils@2.0.0-rc.19': + resolution: {integrity: sha512-cgzk8/aJquZKgFMNTuqdjbU5NrCzrPmdTyhYBcmliL/6N/He1OTWn3PD9QWUGJNODb1sPRJpklZnCpU07waLmg==} + + '@vueuse/core@12.4.0': + resolution: {integrity: sha512-XnjQYcJwCsyXyIafyA6SvyN/OBtfPnjvJmbxNxQjCcyWD198urwm5TYvIUUyAxEAN0K7HJggOgT15cOlWFyLeA==} + + '@vueuse/metadata@12.4.0': + resolution: {integrity: sha512-AhPuHs/qtYrKHUlEoNO6zCXufu8OgbR8S/n2oMw1OQuBQJ3+HOLQ+EpvXs+feOlZMa0p8QVvDWNlmcJJY8rW2g==} + + '@vueuse/shared@12.4.0': + resolution: {integrity: sha512-9yLgbHVIF12OSCojnjTIoZL1+UA10+O4E1aD6Hpfo/DKVm5o3SZIwz6CupqGy3+IcKI8d6Jnl26EQj/YucnW0Q==} + + abbrev@1.1.1: + resolution: {integrity: sha512-nne9/IiQ/hzIhY6pdDnbBtz7DjPTKrY00P/zvPSm5pOFkl6xuGrGnXn/VtTNNfNtAfZ9/1RtehkszU9qcTii0Q==} + + abbrev@2.0.0: + resolution: {integrity: sha512-6/mh1E2u2YgEsCHdY0Yx5oW+61gZU+1vXaoiHHrpKeuRNNgFvS+/jrwHiQhB5apAf5oB7UB7E19ol2R2LKH8hQ==} + engines: {node: ^14.17.0 || ^16.13.0 || >=18.0.0} + + agent-base@6.0.2: + resolution: {integrity: sha512-RZNwNclF7+MS/8bDg70amg32dyeZGZxiDuQmZxKLAlQjr3jGyLx+4Kkk58UO7D2QdgFIQCovuSuZESne6RG6XQ==} + engines: {node: '>= 6.0.0'} + + agent-base@7.1.3: + resolution: {integrity: sha512-jRR5wdylq8CkOe6hei19GGZnxM6rBGwFl3Bg0YItGDimvjGtAvdZk4Pu6Cl4u4Igsws4a1fd1Vq3ezrhn4KmFw==} + engines: {node: '>= 14'} + + aggregate-error@3.1.0: + resolution: {integrity: sha512-4I7Td01quW/RpocfNayFdFVk1qSuoh0E7JrbRJ16nH01HhKFQ88INq9Sd+nd72zqRySlr9BmDA8xlEJ6vJMrYA==} + engines: {node: '>=8'} + + ansi-regex@5.0.1: + resolution: {integrity: sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==} + engines: {node: '>=8'} + + ansi-regex@6.1.0: + resolution: {integrity: sha512-7HSX4QQb4CspciLpVFwyRe79O3xsIZDDLER21kERQ71oaPodF8jL725AgJMFAYbooIqolJoRLuM81SpeUkpkvA==} + engines: {node: '>=12'} + + ansi-styles@4.3.0: + resolution: {integrity: sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==} + engines: {node: '>=8'} + + ansi-styles@6.2.1: + resolution: {integrity: sha512-bN798gFfQX+viw3R7yrGWRqnrN2oRkEkUjjl4JNn4E8GxxbjtG3FbrEIIY3l8/hrwUwIeCZvi4QuOTP4MErVug==} + engines: {node: '>=12'} + + anymatch@3.1.3: + resolution: {integrity: sha512-KMReFUr0B4t+D+OBkjR3KYqvocp2XaSzO55UcB6mgQMd3KbcE+mWTyvVV7D/zsdEbNnV6acZUutkiHQXvTr1Rw==} + engines: {node: '>= 8'} + + aproba@2.0.0: + resolution: {integrity: sha512-lYe4Gx7QT+MKGbDsA+Z+he/Wtef0BiwDOlK/XkBrdfsh9J/jPPXbX0tE9x9cl27Tmu5gg3QUbUrQYa/y+KOHPQ==} + + are-we-there-yet@2.0.0: + resolution: {integrity: sha512-Ci/qENmwHnsYo9xKIcUJN5LeDKdJ6R1Z1j9V/J5wyq8nh/mYPEpIKJbBZXtZjG04HiK7zV/p6Vs9952MrMeUIw==} + engines: {node: '>=10'} + deprecated: This package is no longer supported. + + arg@5.0.2: + resolution: {integrity: sha512-PYjyFOLKQ9y57JvQ6QLo8dAgNqswh8M1RMJYdQduT6xbWSgK36P/Z/v+p888pM69jMMfS8Xd8F6I1kQ/I9HUGg==} + + argparse@1.0.10: + resolution: {integrity: sha512-o5Roy6tNG4SL/FOkCAN6RzjiakZS25RLYFrcMttJqbdd8BWrnA+fGz57iN5Pb06pvBGvl5gQ0B48dJlslXvoTg==} + + argparse@2.0.1: + resolution: {integrity: sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q==} + + autoprefixer@10.4.20: + resolution: {integrity: sha512-XY25y5xSv/wEoqzDyXXME4AFfkZI0P23z6Fs3YgymDnKJkCGOnkL0iTxCa85UTqaSgfcqyf3UA6+c7wUvx/16g==} + engines: {node: ^10 || ^12 || >=14} + hasBin: true + peerDependencies: + postcss: ^8.1.0 + + balanced-match@1.0.2: + resolution: {integrity: sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==} + + balloon-css@1.2.0: + resolution: {integrity: sha512-urXwkHgwp6GsXVF+it01485Z2Cj4pnW02ICnM0TemOlkKmCNnDLmyy+ZZiRXBpwldUXO+aRNr7Hdia4CBvXJ5A==} + + bcrypt-ts@5.0.3: + resolution: {integrity: sha512-2FcgD12xPbwCoe5i9/HK0jJ1xA1m+QfC1e6htG9Bl/hNOnLyaFmQSlqLKcfe3QdnoMPKpKEGFCbESBTg+SJNOw==} + engines: {node: '>=18'} + + binary-extensions@2.3.0: + resolution: {integrity: sha512-Ceh+7ox5qe7LJuLHoY0feh3pHuUDHAcRUeyL2VYghZwfpkNIy/+8Ocg0a3UuSoYzavmylwuLWQOf3hl0jjMMIw==} + engines: {node: '>=8'} + + birpc@0.2.19: + resolution: {integrity: sha512-5WeXXAvTmitV1RqJFppT5QtUiz2p1mRSYU000Jkft5ZUCLJIk4uQriYNO50HknxKwM6jd8utNc66K1qGIwwWBQ==} + + boolbase@1.0.0: + resolution: {integrity: sha512-JZOSA7Mo9sNGB8+UjSgzdLtokWAky1zbztM3WRLCbZ70/3cTANmQmOdR7y2g+J0e2WXywy1yS468tY+IruqEww==} + + brace-expansion@1.1.11: + resolution: {integrity: sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA==} + + brace-expansion@2.0.1: + resolution: {integrity: sha512-XnAIvQ8eM+kC6aULx6wuQiwVsnzsi9d3WxzV3FpWTGA19F621kwdbsAcFKXgKUHZWsy+mY6iL1sHTxWEFCytDA==} + + braces@3.0.3: + resolution: {integrity: sha512-yQbXgO/OSZVD2IsiLlro+7Hf6Q18EJrKSEsdoMzKePKXct3gvD8oLcOQdIzGupr5Fj+EDe8gO/lxc1BzfMpxvA==} + engines: {node: '>=8'} + + browserslist@4.24.4: + resolution: {integrity: sha512-KDi1Ny1gSePi1vm0q4oxSF8b4DR44GF4BbmS2YdhPLOEqd8pDviZOGH/GsmRwoWJ2+5Lr085X7naowMwKHDG1A==} + engines: {node: ^6 || ^7 || ^8 || ^9 || ^10 || ^11 || ^12 || >=13.7} + hasBin: true + + buffer-builder@0.2.0: + resolution: {integrity: sha512-7VPMEPuYznPSoR21NE1zvd2Xna6c/CloiZCfcMXR1Jny6PjX0N4Nsa38zcBFo/FMK+BlA+FLKbJCQ0i2yxp+Xg==} + + cac@6.7.14: + resolution: {integrity: sha512-b6Ilus+c3RrdDk+JhLKUAQfzzgLEPy6wcXqS7f/xe1EETvsDP6GORG7SFuOs6cID5YkqchW/LXZbX5bc8j7ZcQ==} + engines: {node: '>=8'} + + cacache@18.0.4: + resolution: {integrity: sha512-B+L5iIa9mgcjLbliir2th36yEwPftrzteHYujzsx3dFP/31GCHcIeS8f5MGd80odLOjaOvSpU3EEAmRQptkxLQ==} + engines: {node: ^16.14.0 || >=18.0.0} + + camelcase@5.3.1: + resolution: {integrity: sha512-L28STB170nwWS63UjtlEOE3dldQApaJXZkOI1uMFfzf3rRuPegHaHesyee+YxQ+W6SvRDQV6UrdOdRiR153wJg==} + engines: {node: '>=6'} + + caniuse-lite@1.0.30001692: + resolution: {integrity: sha512-A95VKan0kdtrsnMubMKxEKUKImOPSuCpYgxSQBo036P5YYgVIcOYJEgt/txJWqObiRQeISNCfef9nvlQ0vbV7A==} + + ccount@2.0.1: + resolution: {integrity: sha512-eyrF0jiFpY+3drT6383f1qhkbGsLSifNAjA61IUjZjmLCWjItY6LB9ft9YhoDgwfmclB2zhu51Lc7+95b8NRAg==} + + chalk@5.4.1: + resolution: {integrity: sha512-zgVZuo2WcZgfUEmsn6eO3kINexW8RAE4maiQ8QNs8CtpPCSyMiYsULR3HQYkm3w8FIA3SberyMJMSldGsW+U3w==} + engines: {node: ^12.17.0 || ^14.13 || >=16.0.0} + + character-entities-html4@2.1.0: + resolution: {integrity: sha512-1v7fgQRj6hnSwFpq1Eu0ynr/CDEw0rXo2B61qXrLNdHZmPKgb7fqS1a2JwF0rISo9q77jDI8VMEHoApn8qDoZA==} + + character-entities-legacy@3.0.0: + resolution: {integrity: sha512-RpPp0asT/6ufRm//AJVwpViZbGM/MkjQFxJccQRHmISF/22NBtsHqAWmL+/pmkPWoIUJdWyeVleTl1wydHATVQ==} + + character-entities@2.0.2: + resolution: {integrity: sha512-shx7oQ0Awen/BRIdkjkvz54PnEEI/EjwXDSIZp86/KKdbafHh1Df/RYGBhn4hbe2+uKC9FnT5UCEdyPz3ai9hQ==} + + character-reference-invalid@2.0.1: + resolution: {integrity: sha512-iBZ4F4wRbyORVsu0jPV7gXkOsGYjGHPmAyv+HiHG8gi5PtC9KI2j1+v8/tlibRvjoWX027ypmG/n0HtO5t7unw==} + + cheerio-select@2.1.0: + resolution: {integrity: sha512-9v9kG0LvzrlcungtnJtpGNxY+fzECQKhK4EGJX2vByejiMX84MFNQw4UxPJl3bFbTMw+Dfs37XaIkCwTZfLh4g==} + + cheerio@1.0.0: + resolution: {integrity: sha512-quS9HgjQpdaXOvsZz82Oz7uxtXiy6UIsIQcpBj7HRw2M63Skasm9qlDocAM7jNuaxdhpPU7c4kJN+gA5MCu4ww==} + engines: {node: '>=18.17'} + + chokidar@3.6.0: + resolution: {integrity: sha512-7VT13fmjotKpGipCW9JEQAusEPE+Ei8nl6/g4FBAmIm0GOOLMua9NDDo/DWp0ZAxCr3cPq5ZpBqmPAQgDda2Pw==} + engines: {node: '>= 8.10.0'} + + chokidar@4.0.3: + resolution: {integrity: sha512-Qgzu8kfBvo+cA4962jnP1KkS6Dop5NS6g7R5LFYJr4b8Ub94PPQXUksCw9PvXoeXPRRddRNC5C1JQUR2SMGtnA==} + engines: {node: '>= 14.16.0'} + + chownr@2.0.0: + resolution: {integrity: sha512-bIomtDF5KGpdogkLd9VspvFzk9KfpyyGlS8YFVZl7TGPBHL5snIOnxeshwVgPteQ9b4Eydl+pVbIyE1DcvCWgQ==} + engines: {node: '>=10'} + + clean-stack@2.2.0: + resolution: {integrity: sha512-4diC9HaTE+KRAMWhDhrGOECgWZxoevMc5TlkObMqNSsVU62PYzXZ/SMTjzyGAFF1YusgxGcSWTEXBhp0CPwQ1A==} + engines: {node: '>=6'} + + cli-cursor@5.0.0: + resolution: {integrity: sha512-aCj4O5wKyszjMmDT4tZj93kxyydN/K5zPWSCe6/0AV/AA1pqe5ZBIw0a2ZfPQV7lL5/yb5HsUreJ6UFAF1tEQw==} + engines: {node: '>=18'} + + cli-spinners@2.9.2: + resolution: {integrity: sha512-ywqV+5MmyL4E7ybXgKys4DugZbX0FC6LnwrhjuykIjnK9k8OQacQ7axGKnjDXWNhns0xot3bZI5h55H8yo9cJg==} + engines: {node: '>=6'} + + cliui@6.0.0: + resolution: {integrity: sha512-t6wbgtoCXvAzst7QgXxJYqPt0usEfbgQdftEPbLL/cvv6HPE5VgvqCuAIDR0NgU52ds6rFwqrgakNLrHEjCbrQ==} + + color-convert@2.0.1: + resolution: {integrity: sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==} + engines: {node: '>=7.0.0'} + + color-name@1.1.4: + resolution: {integrity: sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==} + + color-support@1.1.3: + resolution: {integrity: sha512-qiBjkpbMLO/HL68y+lh4q0/O1MZFj2RX6X/KmMa3+gJD3z+WwI1ZzDHysvqHGS3mP6mznPckpXmw1nI9cJjyRg==} + hasBin: true + + colorjs.io@0.5.2: + resolution: {integrity: sha512-twmVoizEW7ylZSN32OgKdXRmo1qg+wT5/6C3xu5b9QsWzSFAhHLn2xd8ro0diCsKfCj1RdaTP/nrcW+vAoQPIw==} + + comma-separated-tokens@2.0.3: + resolution: {integrity: sha512-Fu4hJdvzeylCfQPp9SGWidpzrMs7tTrlu6Vb8XGaRGck8QSNZJJp538Wrb60Lax4fPwR64ViY468OIUTbRlGZg==} + + commander@13.0.0: + resolution: {integrity: sha512-oPYleIY8wmTVzkvQq10AEok6YcTC4sRUBl8F9gVuwchGVUCTbl/vhLTaQqutuuySYOsu8YTgV+OxKc/8Yvx+mQ==} + engines: {node: '>=18'} + + commander@8.3.0: + resolution: {integrity: sha512-OkTL9umf+He2DZkUq8f8J9of7yL6RJKI24dVITBmNfZBmri9zYZQrKkuXiKhyfPSu8tUhnVBB1iKXevvnlR4Ww==} + engines: {node: '>= 12'} + + commander@9.2.0: + resolution: {integrity: sha512-e2i4wANQiSXgnrBlIatyHtP1odfUp0BbV5Y5nEGbxtIrStkEOAAzCUirvLBNXHLr7kwLvJl6V+4V3XV9x7Wd9w==} + engines: {node: ^12.20.0 || >=14} + + concat-map@0.0.1: + resolution: {integrity: sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg==} + + connect-history-api-fallback@2.0.0: + resolution: {integrity: sha512-U73+6lQFmfiNPrYbXqr6kZ1i1wiRqXnp2nhMsINseWXO8lDau0LGEffJ8kQi4EjLZympVgRdvqjAgiZ1tgzDDA==} + engines: {node: '>=0.8'} + + console-control-strings@1.1.0: + resolution: {integrity: sha512-ty/fTekppD2fIwRvnZAVdeOiGd1c7YXEixbgJTNzqcxJWKQnjJ/V1bNEEE6hygpM3WjwHFUVK6HTjWSzV4a8sQ==} + + copy-anything@3.0.5: + resolution: {integrity: sha512-yCEafptTtb4bk7GLEQoM8KVJpxAfdBJYaXyzQEgQQQgYrZiDp8SJmGKlYza6CYjEDNstAdNdKA3UuoULlEbS6w==} + engines: {node: '>=12.13'} + + create-codepen@2.0.0: + resolution: {integrity: sha512-ehJ0Zw5RSV2G4+/azUb7vEZWRSA/K9cW7HDock1Y9ViDexkgSJUZJRcObdw/YAWeXKjreEQV9l/igNSsJ1yw5A==} + engines: {node: '>=18'} + + cross-spawn@7.0.6: + resolution: {integrity: sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA==} + engines: {node: '>= 8'} + + css-select@5.1.0: + resolution: {integrity: sha512-nwoRF1rvRRnnCqqY7updORDsuqKzqYJ28+oSMaJMMgOauh3fvwHqMS7EZpIPqK8GL+g9mKxF1vP/ZjSeNjEVHg==} + + css-what@6.1.0: + resolution: {integrity: sha512-HTUrgRJ7r4dsZKU6GjmpfRK1O76h97Z8MfS1G0FozR+oF2kG6Vfe8JE6zwrkbxigziPHinCJ+gCPjA9EaBDtRw==} + engines: {node: '>= 6'} + + csstype@3.1.3: + resolution: {integrity: sha512-M1uQkMl8rQK/szD0LNhtqxIPLpimGm8sOBwU7lLnCpSbTyY3yeU1Vc7l4KT5zT4s/yOxHH5O7tIuuLOCnLADRw==} + + dayjs@1.11.13: + resolution: {integrity: sha512-oaMBel6gjolK862uaPQOVTA7q3TZhuSvuMQAAglQDOWYO9A91IrAOUJEyKVlqJlHE0vq5p5UXxzdPfMH/x6xNg==} + + debug@4.4.0: + resolution: {integrity: sha512-6WTZ/IxCY/T6BALoZHaE4ctp9xm+Z5kY/pzYaCHRFeyVhojxlrm+46y68HA6hr0TcwEssoxNiDEUJQjfPZ/RYA==} + engines: {node: '>=6.0'} + peerDependencies: + supports-color: '*' + peerDependenciesMeta: + supports-color: + optional: true + + decamelize@1.2.0: + resolution: {integrity: sha512-z2S+W9X73hAUUki+N+9Za2lBlun89zigOyGrsax+KUQ6wKW4ZoWpEYBkGhQjwAjjDCkWxhY0VKEhk8wzY7F5cA==} + engines: {node: '>=0.10.0'} + + decode-named-character-reference@1.0.2: + resolution: {integrity: sha512-O8x12RzrUF8xyVcY0KJowWsmaJxQbmy0/EtnNtHRpsOcT7dFk5W598coHqBVpmWo1oQQfsCqfCmkZN5DJrZVdg==} + + delegates@1.0.0: + resolution: {integrity: sha512-bd2L678uiWATM6m5Z1VzNCErI3jiGzt6HGY8OVICs40JQq/HALfbyNJmp0UDakEY4pMMaN0Ly5om/B1VI/+xfQ==} + + dequal@2.0.3: + resolution: {integrity: sha512-0je+qPKHEMohvfRTCEo3CrPG6cAzAYgmzKyxRiYSSDkS6eGJdyVJm7WaYA5ECaAD9wLB2T4EEeymA5aFVcYXCA==} + engines: {node: '>=6'} + + detect-libc@2.0.3: + resolution: {integrity: sha512-bwy0MGW55bG41VqxxypOsdSdGqLwXPI/focwgTYCFMbdUiBAxLg9CFzG08sz2aqzknwiX7Hkl0bQENjg8iLByw==} + engines: {node: '>=8'} + + devlop@1.1.0: + resolution: {integrity: sha512-RWmIqhcFf1lRYBvNmr7qTNuyCt/7/ns2jbpp1+PalgE/rDQcBT0fioSMUpJ93irlUhC5hrg4cYqe6U+0ImW0rA==} + + dijkstrajs@1.0.3: + resolution: {integrity: sha512-qiSlmBq9+BCdCA/L46dw8Uy93mloxsPSbwnm5yrKn2vMPiy8KyAskTF6zuV/j5BMsmOGZDPs7KjU+mjb670kfA==} + + dom-serializer@2.0.0: + resolution: {integrity: sha512-wIkAryiqt/nV5EQKqQpo3SToSOV9J0DnbJqwK7Wv/Trc92zIAYZ4FlMu+JPFW1DfGFt81ZTCGgDEabffXeLyJg==} + + domelementtype@2.3.0: + resolution: {integrity: sha512-OLETBj6w0OsagBwdXnPdN0cnMfF9opN69co+7ZrbfPGrdpPVNBUj02spi6B1N7wChLQiPn4CSH/zJvXw56gmHw==} + + domhandler@5.0.3: + resolution: {integrity: sha512-cgwlv/1iFQiFnU96XXgROh8xTeetsnJiDsTc7TYCLFd9+/WNkIqPTxiM/8pSd8VIrhXGTf1Ny1q1hquVqDJB5w==} + engines: {node: '>= 4'} + + domutils@3.2.2: + resolution: {integrity: sha512-6kZKyUajlDuqlHKVX1w7gyslj9MPIXzIFiz/rGu35uC1wMi+kMhQwGhl4lt9unC9Vb9INnY9Z3/ZA3+FhASLaw==} + + eastasianwidth@0.2.0: + resolution: {integrity: sha512-I88TYZWc9XiYHRQ4/3c5rjjfgkjhLyW2luGIheGERbNQ6OY7yTybanSpDXZa8y7VUP9YmDcYa+eyq4ca7iLqWA==} + + electron-to-chromium@1.5.80: + resolution: {integrity: sha512-LTrKpW0AqIuHwmlVNV+cjFYTnXtM9K37OGhpe0ZI10ScPSxqVSryZHIY3WnCS5NSYbBODRTZyhRMS2h5FAEqAw==} + + emoji-regex-xs@1.0.0: + resolution: {integrity: sha512-LRlerrMYoIDrT6jgpeZ2YYl/L8EulRTt5hQcYjy5AInh7HWXKimpqx68aknBFpGL2+/IcogTcaydJEgaTmOpDg==} + + emoji-regex@10.4.0: + resolution: {integrity: sha512-EC+0oUMY1Rqm4O6LLrgjtYDvcVYTy7chDnM4Q7030tP4Kwj3u/pR6gP9ygnp2CJMK5Gq+9Q2oqmrFJAz01DXjw==} + + emoji-regex@8.0.0: + resolution: {integrity: sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==} + + emoji-regex@9.2.2: + resolution: {integrity: sha512-L18DaJsXSUk2+42pv8mLs5jJT2hqFkFE4j21wOmgbUqsZ2hL72NsUU785g9RXgo3s0ZNgVl42TiHp3ZtOv/Vyg==} + + encoding-sniffer@0.2.0: + resolution: {integrity: sha512-ju7Wq1kg04I3HtiYIOrUrdfdDvkyO9s5XM8QAj/bN61Yo/Vb4vgJxy5vi4Yxk01gWHbrofpPtpxM8bKger9jhg==} + + encoding@0.1.13: + resolution: {integrity: sha512-ETBauow1T35Y/WZMkio9jiM0Z5xjHHmJ4XmjZOq1l/dXz3lr2sRn87nJy20RupqSh1F2m3HHPSp8ShIPQJrJ3A==} + + entities@4.5.0: + resolution: {integrity: sha512-V0hjH4dGPh9Ao5p0MoRY6BVqtwCjhz6vI5LT8AJ55H+4g9/4vbHx1I54fS0XuclLhDHArPQCiMjDxjaL8fPxhw==} + engines: {node: '>=0.12'} + + env-paths@2.2.1: + resolution: {integrity: sha512-+h1lkLKhZMTYjog1VEpJNG7NZJWcuc2DDk/qsqSTRRCOXiLjeQ1d1/udrUGhqMxUgAlwKNZ0cf2uqan5GLuS2A==} + engines: {node: '>=6'} + + envinfo@7.14.0: + resolution: {integrity: sha512-CO40UI41xDQzhLB1hWyqUKgFhs250pNcGbyGKe1l/e4FSaI/+YE4IMG76GDt0In67WLPACIITC+sOi08x4wIvg==} + engines: {node: '>=4'} + hasBin: true + + err-code@2.0.3: + resolution: {integrity: sha512-2bmlRpNKBxT/CRmPOlyISQpNj+qSeYvcym/uT0Jx2bMOlKLtSy1ZmLuVxSEKKyor/N5yhvp/ZiG1oE3DEYMSFA==} + + esbuild@0.21.5: + resolution: {integrity: sha512-mg3OPMV4hXywwpoDxu3Qda5xCKQi+vCTZq8S9J/EpkhB2HzKXq4SNFZE3+NK93JYxc8VMSep+lOUSC/RVKaBqw==} + engines: {node: '>=12'} + hasBin: true + + esbuild@0.24.2: + resolution: {integrity: sha512-+9egpBW8I3CD5XPe0n6BfT5fxLzxrlDzqydF3aviG+9ni1lDC/OvMHcxqEFV0+LANZG5R1bFMWfUrjVsdwxJvA==} + engines: {node: '>=18'} + hasBin: true + + escalade@3.2.0: + resolution: {integrity: sha512-WUj2qlxaQtO4g6Pq5c29GTcWGDyd8itL8zTlipgECz3JesAiiOKotd8JU6otB3PACgG6xkJUyVhboMS+bje/jA==} + engines: {node: '>=6'} + + esm@3.2.25: + resolution: {integrity: sha512-U1suiZ2oDVWv4zPO56S0NcR5QriEahGtdN2OR6FiOG4WJvcjBVFB0qI4+eKoWFH483PKGuLuu6V8Z4T5g63UVA==} + engines: {node: '>=6'} + + esprima@4.0.1: + resolution: {integrity: sha512-eGuFFw7Upda+g4p+QHvnW0RyTX/SVeJBDM/gCtMARO0cLuT2HcEKnTPvhjV6aGeqrCB/sbNop0Kszm0jsaWU4A==} + engines: {node: '>=4'} + hasBin: true + + estree-walker@2.0.2: + resolution: {integrity: sha512-Rfkk/Mp/DL7JVje3u18FxFujQlTNR2q6QfMSMB7AvCBx91NGj/ba3kCfza0f6dVDbw7YlRf/nDrn7pQrCCyQ/w==} + + execa@9.5.2: + resolution: {integrity: sha512-EHlpxMCpHWSAh1dgS6bVeoLAXGnJNdR93aabr4QCGbzOM73o5XmRfM/e5FUqsw3aagP8S8XEWUWFAxnRBnAF0Q==} + engines: {node: ^18.19.0 || >=20.5.0} + + exponential-backoff@3.1.1: + resolution: {integrity: sha512-dX7e/LHVJ6W3DE1MHWi9S1EYzDESENfLrYohG2G++ovZrYOkm4Knwa0mc1cn84xJOR4KEU0WSchhLbd0UklbHw==} + + extend-shallow@2.0.1: + resolution: {integrity: sha512-zCnTtlxNoAiDc3gqY2aYAWFx7XWWiasuF2K8Me5WbN8otHKTUKBwjPtNpRs/rbUZm7KxWAaNj7P1a/p52GbVug==} + engines: {node: '>=0.10.0'} + + fast-glob@3.3.3: + resolution: {integrity: sha512-7MptL8U0cqcFdzIzwOTHoilX9x5BrNqye7Z/LuC7kCMRio1EMSyqRK3BEAUD7sXRq4iT4AzTVuZdhgQ2TCvYLg==} + engines: {node: '>=8.6.0'} + + fastq@1.18.0: + resolution: {integrity: sha512-QKHXPW0hD8g4UET03SdOdunzSouc9N4AuHdsX8XNcTsuz+yYFILVNIX4l9yHABMhiEI9Db0JTTIpu0wB+Y1QQw==} + + fflate@0.8.2: + resolution: {integrity: sha512-cPJU47OaAoCbg0pBvzsgpTPhmhqI5eJjh/JIu8tPj5q+T7iLvW/JAYUqmE7KOB4R1ZyEhzBaIQpQpardBF5z8A==} + + figures@6.1.0: + resolution: {integrity: sha512-d+l3qxjSesT4V7v2fh+QnmFnUWv9lSpjarhShNTgBOfA0ttejbQUAlHLitbjkoRiDulW0OPoQPYIGhIC8ohejg==} + engines: {node: '>=18'} + + fill-range@7.1.1: + resolution: {integrity: sha512-YsGpe3WHLK8ZYi4tWDg2Jy3ebRz2rXowDxnld4bkQB00cc/1Zw9AWnC0i9ztDJitivtQvaI9KaLyKrc+hBW0yg==} + engines: {node: '>=8'} + + find-up@4.1.0: + resolution: {integrity: sha512-PpOwAdQ/YlXQ2vj8a3h8IipDuYRi3wceVQQGYWxNINccq40Anw7BlsEXCMbt1Zt+OLA6Fq9suIpIWD0OsnISlw==} + engines: {node: '>=8'} + + foreground-child@3.3.0: + resolution: {integrity: sha512-Ld2g8rrAyMYFXBhEqMz8ZAHBi4J4uS1i/CxGMDnjyFWddMXLVcDp051DZfu+t7+ab7Wv6SMqpWmyFIj5UbfFvg==} + engines: {node: '>=14'} + + fraction.js@4.3.7: + resolution: {integrity: sha512-ZsDfxO51wGAXREY55a7la9LScWpwv9RxIrYABrlvOFBlH/ShPnrtsXeuUIfXKKOVicNxQ+o8JTbJvjS4M89yew==} + + fs-extra@11.2.0: + resolution: {integrity: sha512-PmDi3uwK5nFuXh7XDTlVnS17xJS7vW36is2+w3xcv8SVxiB4NyATf4ctkVY5bkSjX0Y4nbvZCq1/EjtEyr9ktw==} + engines: {node: '>=14.14'} + + fs-minipass@2.1.0: + resolution: {integrity: sha512-V/JgOLFCS+R6Vcq0slCuaeWEdNC3ouDlJMNIsacH2VtALiu9mV4LPrHc5cDl8k5aw6J8jwgWWpiTo5RYhmIzvg==} + engines: {node: '>= 8'} + + fs-minipass@3.0.3: + resolution: {integrity: sha512-XUBA9XClHbnJWSfBzjkm6RvPsyg3sryZt06BEQoXcF7EK/xpGaQYJgQKDJSUH5SGZ76Y7pFx1QBnXz09rU5Fbw==} + engines: {node: ^14.17.0 || ^16.13.0 || >=18.0.0} + + fs.realpath@1.0.0: + resolution: {integrity: sha512-OO0pH2lK6a0hZnAdau5ItzHPI6pUlvI7jMVnxUQRtw4owF2wk8lOSabtGDCTP4Ggrg2MbGnWO9X8K1t4+fGMDw==} + + fsevents@2.3.3: + resolution: {integrity: sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==} + engines: {node: ^8.16.0 || ^10.6.0 || >=11.0.0} + os: [darwin] + + gauge@3.0.2: + resolution: {integrity: sha512-+5J6MS/5XksCuXq++uFRsnUd7Ovu1XenbeuIuNRJxYWjgQbPuFhT14lAvsWfqfAmnwluf1OwMjz39HjfLPci0Q==} + engines: {node: '>=10'} + deprecated: This package is no longer supported. + + get-caller-file@2.0.5: + resolution: {integrity: sha512-DyFP3BM/3YHTQOCUL/w0OZHR0lpKeGrxotcHWcqNEdnltqFwXVfhEBQ94eIo34AfQpo0rGki4cyIiftY06h2Fg==} + engines: {node: 6.* || 8.* || >= 10.*} + + get-east-asian-width@1.3.0: + resolution: {integrity: sha512-vpeMIQKxczTD/0s2CdEWHcb0eeJe6TFjxb+J5xgX7hScxqrGuyjmv4c1D4A/gelKfyox0gJJwIHF+fLjeaM8kQ==} + engines: {node: '>=18'} + + get-stream@9.0.1: + resolution: {integrity: sha512-kVCxPF3vQM/N0B1PmoqVUqgHP+EeVjmZSQn+1oCRPxd2P21P2F19lIgbR3HBosbB1PUhOAoctJnfEn2GbN2eZA==} + engines: {node: '>=18'} + + giscus@1.6.0: + resolution: {integrity: sha512-Zrsi8r4t1LVW950keaWcsURuZUQwUaMKjvJgTCY125vkW6OiEBkatE7ScJDbpqKHdZwb///7FVC21SE3iFK3PQ==} + + glob-parent@5.1.2: + resolution: {integrity: sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow==} + engines: {node: '>= 6'} + + glob@10.4.5: + resolution: {integrity: sha512-7Bv8RF0k6xjo7d4A/PxYLbUCfb6c+Vpd2/mB2yRDlew7Jb5hEXiCD9ibfO7wpk8i4sevK6DFny9h7EYbM3/sHg==} + hasBin: true + + glob@7.2.3: + resolution: {integrity: sha512-nFR0zLpU2YCaRxwoCJvL6UvCH2JFyFVIvwTLsIf21AuHlMskA1hhTdk+LlYJtOlYt9v6dvszD2BGRqBL+iQK9Q==} + deprecated: Glob versions prior to v9 are no longer supported + + globby@14.0.2: + resolution: {integrity: sha512-s3Fq41ZVh7vbbe2PN3nrW7yC7U7MFVc5c98/iTl9c2GawNMKx/J648KQRW6WKkuU8GIbbh2IXfIRQjOZnXcTnw==} + engines: {node: '>=18'} + + graceful-fs@4.2.11: + resolution: {integrity: sha512-RbJ5/jmFcNNCcDV5o9eTnBLJ/HszWV0P73bc+Ff4nS/rJj+YaS6IGyiOL0VoBYX+l1Wrl3k63h/KrH+nhJ0XvQ==} + + gray-matter@4.0.3: + resolution: {integrity: sha512-5v6yZd4JK3eMI3FqqCouswVqwugaA9r4dNZB1wwcmrD02QkV5H0y7XBQW8QwQqEaZY1pM9aqORSORhJRdNK44Q==} + engines: {node: '>=6.0'} + + has-flag@4.0.0: + resolution: {integrity: sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==} + engines: {node: '>=8'} + + has-unicode@2.0.1: + resolution: {integrity: sha512-8Rf9Y83NBReMnx0gFzA8JImQACstCYWUplepDa9xprwwtmgEZUF0h/i5xSA625zB/I37EtrswSST6OXxwaaIJQ==} + + hash-sum@2.0.0: + resolution: {integrity: sha512-WdZTbAByD+pHfl/g9QSsBIIwy8IT+EsPiKDs0KNX+zSHhdDLFKdZu0BQHljvO+0QI/BasbMSUa8wYNCZTvhslg==} + + hast-util-to-html@9.0.4: + resolution: {integrity: sha512-wxQzXtdbhiwGAUKrnQJXlOPmHnEehzphwkK7aluUPQ+lEc1xefC8pblMgpp2w5ldBTEfveRIrADcrhGIWrlTDA==} + + hast-util-whitespace@3.0.0: + resolution: {integrity: sha512-88JUN06ipLwsnv+dVn+OIYOvAuvBMy/Qoi6O7mQHxdPXpjy+Cd6xRkWwux7DKO+4sYILtLBRIKgsdpS2gQc7qw==} + + hookable@5.5.3: + resolution: {integrity: sha512-Yc+BQe8SvoXH1643Qez1zqLRmbA5rCL+sSmk6TVos0LWVfNIB7PGncdlId77WzLGSIB5KaWgTaNTs2lNVEI6VQ==} + + html-void-elements@3.0.0: + resolution: {integrity: sha512-bEqo66MRXsUGxWHV5IP0PUiAWwoEjba4VCzg0LjFJBpchPaTfyfCKTG6bc5F8ucKec3q5y6qOdGyYTSBEvhCrg==} + + htmlparser2@9.1.0: + resolution: {integrity: sha512-5zfg6mHUoaer/97TxnGpxmbR7zJtPwIYFMZ/H5ucTlPZhKvtum05yiPK3Mgai3a0DyVxv7qYqoweaEd2nrYQzQ==} + + http-cache-semantics@4.1.1: + resolution: {integrity: sha512-er295DKPVsV82j5kw1Gjt+ADA/XYHsajl82cGNQG2eyoPkvgUhX+nDIyelzhIWbbsXP39EHcI6l5tYs2FYqYXQ==} + + http-proxy-agent@7.0.2: + resolution: {integrity: sha512-T1gkAiYYDWYx3V5Bmyu7HcfcvL7mUrTWiM6yOfa3PIphViJ/gFPbvidQ+veqSOHci/PxBcDabeUNCzpOODJZig==} + engines: {node: '>= 14'} + + https-proxy-agent@5.0.1: + resolution: {integrity: sha512-dFcAjpTQFgoLMzC2VwU+C/CbS7uRL0lWmxDITmqm7C+7F0Odmj6s9l6alZc6AELXhrnggM2CeWSXHGOdX2YtwA==} + engines: {node: '>= 6'} + + https-proxy-agent@7.0.6: + resolution: {integrity: sha512-vK9P5/iUfdl95AI+JVyUuIcVtd4ofvtrOr3HNtM2yxC9bnMbEdp3x01OhQNnjb8IJYi38VlTE3mBXwcfvywuSw==} + engines: {node: '>= 14'} + + human-signals@8.0.0: + resolution: {integrity: sha512-/1/GPCpDUCCYwlERiYjxoczfP0zfvZMU/OWgQPMya9AbAE24vseigFdhAMObpc8Q4lc/kjutPfUddDYyAmejnA==} + engines: {node: '>=18.18.0'} + + husky@9.1.7: + resolution: {integrity: sha512-5gs5ytaNjBrh5Ow3zrvdUUY+0VxIuWVL4i9irt6friV+BqdCfmV11CQTWMiBYWHbXhco+J1kHfTOUkePhCDvMA==} + engines: {node: '>=18'} + hasBin: true + + iconv-lite@0.6.3: + resolution: {integrity: sha512-4fCk79wshMdzMp2rH06qWrJE4iolqLhCUH+OiuIgU++RB0+94NlDL81atO7GX55uUKueo0txHNtvEyI6D7WdMw==} + engines: {node: '>=0.10.0'} + + ignore@5.3.2: + resolution: {integrity: sha512-hsBTNUqQTDwkWtcdYI2i06Y/nUBEsNEDJKjWdigLvegy8kDuJAS8uRlpkkcQpyEXL0Z/pjDy5HBmMjRCJ2gq+g==} + engines: {node: '>= 4'} + + immutable@5.0.3: + resolution: {integrity: sha512-P8IdPQHq3lA1xVeBRi5VPqUm5HDgKnx0Ru51wZz5mjxHr5n3RWhjIpOFU7ybkUxfB+5IToy+OLaHYDBIWsv+uw==} + + imurmurhash@0.1.4: + resolution: {integrity: sha512-JmXMZ6wuvDmLiHEml9ykzqO6lwFbof0GG4IkcGaENdCRDDmMVnny7s5HsIgHCbaq0w2MyPhDqkhTUgS2LU2PHA==} + engines: {node: '>=0.8.19'} + + indent-string@4.0.0: + resolution: {integrity: sha512-EdDDZu4A2OyIK7Lr/2zG+w5jmbuk1DVBnEwREQvBzspBJkCEbRa8GxU1lghYcaGJCnRWibjDXlq779X1/y5xwg==} + engines: {node: '>=8'} + + inflight@1.0.6: + resolution: {integrity: sha512-k92I/b08q4wvFscXCLvqfsHCrjrF7yiXsQuIVvVE7N82W3+aqpzuUdBbfhWcy/FZR3/4IgflMgKLOsvPDrGCJA==} + deprecated: This module is not supported, and leaks memory. Do not use it. Check out lru-cache if you want a good and tested way to coalesce async requests by a key value, which is much more comprehensive and powerful. + + inherits@2.0.4: + resolution: {integrity: sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==} + + ip-address@9.0.5: + resolution: {integrity: sha512-zHtQzGojZXTwZTHQqra+ETKd4Sn3vgi7uBmlPoXVWZqYvuKmtI0l/VZTjqGmJY9x88GGOaZ9+G9ES8hC4T4X8g==} + engines: {node: '>= 12'} + + is-alphabetical@2.0.1: + resolution: {integrity: sha512-FWyyY60MeTNyeSRpkM2Iry0G9hpr7/9kD40mD/cGQEuilcZYS4okz8SN2Q6rLCJ8gbCt6fN+rC+6tMGS99LaxQ==} + + is-alphanumerical@2.0.1: + resolution: {integrity: sha512-hmbYhX/9MUMF5uh7tOXyK/n0ZvWpad5caBA17GsC6vyuCqaWliRG5K1qS9inmUhEMaOBIW7/whAnSwveW/LtZw==} + + is-binary-path@2.1.0: + resolution: {integrity: sha512-ZMERYes6pDydyuGidse7OsHxtbI7WVeUEozgR/g7rd0xUimYNlvZRE/K2MgZTjWy725IfelLeVcEM97mmtRGXw==} + engines: {node: '>=8'} + + is-decimal@2.0.1: + resolution: {integrity: sha512-AAB9hiomQs5DXWcRB1rqsxGUstbRroFOPPVAomNk/3XHR5JyEZChOyTWe2oayKnsSsr/kcGqF+z6yuH6HHpN0A==} + + is-extendable@0.1.1: + resolution: {integrity: sha512-5BMULNob1vgFX6EjQw5izWDxrecWK9AM72rugNr0TFldMOi0fj6Jk+zeKIt0xGj4cEfQIJth4w3OKWOJ4f+AFw==} + engines: {node: '>=0.10.0'} + + is-extglob@2.1.1: + resolution: {integrity: sha512-SbKbANkN603Vi4jEZv49LeVJMn4yGwsbzZworEoyEiutsN3nJYdbO36zfhGJ6QEDpOZIFkDtnq5JRxmvl3jsoQ==} + engines: {node: '>=0.10.0'} + + is-fullwidth-code-point@3.0.0: + resolution: {integrity: sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg==} + engines: {node: '>=8'} + + is-glob@4.0.3: + resolution: {integrity: sha512-xelSayHH36ZgE7ZWhli7pW34hNbNl8Ojv5KVmkJD4hBdD3th8Tfk9vYasLM+mXWOZhFkgZfxhLSnrwRr4elSSg==} + engines: {node: '>=0.10.0'} + + is-hexadecimal@2.0.1: + resolution: {integrity: sha512-DgZQp241c8oO6cA1SbTEWiXeoxV42vlcJxgH+B3hi1AiqqKruZR3ZGF8In3fj4+/y/7rHvlOZLZtgJ/4ttYGZg==} + + is-interactive@2.0.0: + resolution: {integrity: sha512-qP1vozQRI+BMOPcjFzrjXuQvdak2pHNUMZoeG2eRbiSqyvbEf/wQtEOTOX1guk6E3t36RkaqiSt8A/6YElNxLQ==} + engines: {node: '>=12'} + + is-lambda@1.0.1: + resolution: {integrity: sha512-z7CMFGNrENq5iFB9Bqo64Xk6Y9sg+epq1myIcdHaGnbMTYOxvzsEtdYqQUylB7LxfkvgrrjP32T6Ywciio9UIQ==} + + is-number@7.0.0: + resolution: {integrity: sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng==} + engines: {node: '>=0.12.0'} + + is-plain-obj@4.1.0: + resolution: {integrity: sha512-+Pgi+vMuUNkJyExiMBt5IlFoMyKnr5zhJ4Uspz58WOhBF5QoIZkFyNHIbBAtHwzVAgk5RtndVNsDRN61/mmDqg==} + engines: {node: '>=12'} + + is-stream@4.0.1: + resolution: {integrity: sha512-Dnz92NInDqYckGEUJv689RbRiTSEHCQ7wOVeALbkOz999YpqT46yMRIGtSNl2iCL1waAZSx40+h59NV/EwzV/A==} + engines: {node: '>=18'} + + is-unicode-supported@1.3.0: + resolution: {integrity: sha512-43r2mRvz+8JRIKnWJ+3j8JtjRKZ6GmjzfaE/qiBJnikNnYv/6bagRJ1kUhNk8R5EX/GkobD+r+sfxCPJsiKBLQ==} + engines: {node: '>=12'} + + is-unicode-supported@2.1.0: + resolution: {integrity: sha512-mE00Gnza5EEB3Ds0HfMyllZzbBrmLOX3vfWoj9A9PEnTfratQ/BcaJOuMhnkhjXvb2+FkY3VuHqtAGpTPmglFQ==} + engines: {node: '>=18'} + + is-what@4.1.16: + resolution: {integrity: sha512-ZhMwEosbFJkA0YhFnNDgTM4ZxDRsS6HqTo7qsZM08fehyRYIYa0yHu5R6mgo1n/8MgaPBXiPimPD77baVFYg+A==} + engines: {node: '>=12.13'} + + isexe@2.0.0: + resolution: {integrity: sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==} + + isexe@3.1.1: + resolution: {integrity: sha512-LpB/54B+/2J5hqQ7imZHfdU31OlgQqx7ZicVlkm9kzg9/w8GKLEcFfJl/t7DCEDueOyBAD6zCCwTO6Fzs0NoEQ==} + engines: {node: '>=16'} + + jackspeak@3.4.3: + resolution: {integrity: sha512-OGlZQpz2yfahA/Rd1Y8Cd9SIEsqvXkLVoSw/cgwhnhFMDbsQFeZYoJJ7bIZBS9BcamUW96asq/npPWugM+RQBw==} + + js-yaml@3.14.1: + resolution: {integrity: sha512-okMH7OXXJ7YrN9Ok3/SXrnu4iX9yOk+25nqX4imS2npuvTYDmo/QEZoqwZkYaIDk3jVvBOTOIEgEhaLOynBS9g==} + hasBin: true + + js-yaml@4.1.0: + resolution: {integrity: sha512-wpxZs9NoxZaJESJGIZTyDEaYpl0FKSA+FB9aJiyemKhMwkxQg63h4T1KJgUGHpTqPDNRcmmYLugrRjJlBtWvRA==} + hasBin: true + + jsbn@1.1.0: + resolution: {integrity: sha512-4bYVV3aAMtDTTu4+xsDYa6sy9GyJ69/amsu9sYF2zqjiEoZA5xJi3BrfX3uY+/IekIu7MwdObdbDWpoZdBv3/A==} + + jsonc-parser@3.3.1: + resolution: {integrity: sha512-HUgH65KyejrUFPvHFPbqOY0rsFip3Bo5wb4ngvdi1EpCYWUQDC5V+Y7mZws+DLkr4M//zQJoanu1SP+87Dv1oQ==} + + jsonfile@6.1.0: + resolution: {integrity: sha512-5dgndWOriYSm5cnYaJNhalLNDKOqFwyDB/rr1E9ZsGciGvKPs8R2xYGCacuf3z6K1YKDz182fd+fY3cn3pMqXQ==} + + katex@0.16.20: + resolution: {integrity: sha512-jjuLaMGD/7P8jUTpdKhA9IoqnH+yMFB3sdAFtq5QdAqeP2PjiSbnC3EaguKPNtv6dXXanHxp1ckwvF4a86LBig==} + hasBin: true + + kind-of@6.0.3: + resolution: {integrity: sha512-dcS1ul+9tmeD95T+x28/ehLgd9mENa3LsvDTtzm3vyBEO7RPptvAD+t44WVXaUjTBRcrpFeFlC8WCruUR456hw==} + engines: {node: '>=0.10.0'} + + lilconfig@3.1.3: + resolution: {integrity: sha512-/vlFKAoH5Cgt3Ie+JLhRbwOsCQePABiU3tJ1egGvyQ+33R/vcwM2Zl2QR/LzjsBeItPt3oSVXapn+m4nQDvpzw==} + engines: {node: '>=14'} + + linkify-it@5.0.0: + resolution: {integrity: sha512-5aHCbzQRADcdP+ATqnDuhhJ/MRIqDkZX5pyjFHRRysS8vZ5AbqGEoFIb6pYHPZ+L/OC2Lc+xT8uHVVR5CAK/wQ==} + + lit-element@4.1.1: + resolution: {integrity: sha512-HO9Tkkh34QkTeUmEdNYhMT8hzLid7YlMlATSi1q4q17HE5d9mrrEHJ/o8O2D0cMi182zK1F3v7x0PWFjrhXFew==} + + lit-html@3.2.1: + resolution: {integrity: sha512-qI/3lziaPMSKsrwlxH/xMgikhQ0EGOX2ICU73Bi/YHFvz2j/yMCIrw4+puF2IpQ4+upd3EWbvnHM9+PnJn48YA==} + + lit@3.2.1: + resolution: {integrity: sha512-1BBa1E/z0O9ye5fZprPtdqnc0BFzxIxTTOO/tQFmyC/hj1O3jL4TfmLBw0WEwjAokdLwpclkvGgDJwTIh0/22w==} + + locate-path@5.0.0: + resolution: {integrity: sha512-t7hw9pI+WvuwNJXwk5zVHpyhIqzg2qTlklJOf0mVxGSbe3Fp2VieZcduNYjaLDoy6p9uGpQEGWG87WpMKlNq8g==} + engines: {node: '>=8'} + + log-symbols@6.0.0: + resolution: {integrity: sha512-i24m8rpwhmPIS4zscNzK6MSEhk0DUWa/8iYQWxhffV8jkI4Phvs3F+quL5xvS0gdQR0FyTCMMH33Y78dDTzzIw==} + engines: {node: '>=18'} + + lru-cache@10.4.3: + resolution: {integrity: sha512-JNAzZcXrCt42VGLuYz0zfAzDfAvJWW6AfYlDBQyDV5DClI2m5sAmK+OIO7s59XfsRsWHp02jAJrRadPRGTt6SQ==} + + magic-string@0.30.17: + resolution: {integrity: sha512-sNPKHvyjVf7gyjwS4xGTaW/mCnF8wnjtifKBEhxfZ7E/S8tQ0rssrwGNn6q8JH/ohItJfSQp9mBtQYuTlH5QnA==} + + make-dir@3.1.0: + resolution: {integrity: sha512-g3FeP20LNwhALb/6Cz6Dd4F2ngze0jz7tbzrD2wAV+o9FeNHe4rL+yK2md0J/fiSf1sa1ADhXqi5+oVwOM/eGw==} + engines: {node: '>=8'} + + make-fetch-happen@13.0.1: + resolution: {integrity: sha512-cKTUFc/rbKUd/9meOvgrpJ2WrNzymt6jfRDdwg5UCnVzv9dTpEj9JS5m3wtziXVCjluIXyL8pcaukYqezIzZQA==} + engines: {node: ^16.14.0 || >=18.0.0} + + markdown-it-anchor@9.2.0: + resolution: {integrity: sha512-sa2ErMQ6kKOA4l31gLGYliFQrMKkqSO0ZJgGhDHKijPf0pNFM9vghjAh3gn26pS4JDRs7Iwa9S36gxm3vgZTzg==} + peerDependencies: + '@types/markdown-it': '*' + markdown-it: '*' + + markdown-it-emoji@3.0.0: + resolution: {integrity: sha512-+rUD93bXHubA4arpEZO3q80so0qgoFJEKRkRbjKX8RTdca89v2kfyF+xR3i2sQTwql9tpPZPOQN5B+PunspXRg==} + + markdown-it@14.1.0: + resolution: {integrity: sha512-a54IwgWPaeBCAAsv13YgmALOF1elABB08FxO9i+r4VFk5Vl4pKokRPeX8u5TCgSsPi6ec1otfLjdOpVcgbpshg==} + hasBin: true + + markdownlint-cli2-formatter-default@0.0.5: + resolution: {integrity: sha512-4XKTwQ5m1+Txo2kuQ3Jgpo/KmnG+X90dWt4acufg6HVGadTUG5hzHF/wssp9b5MBYOMCnZ9RMPaU//uHsszF8Q==} + peerDependencies: + markdownlint-cli2: '>=0.0.4' + + markdownlint-cli2@0.17.1: + resolution: {integrity: sha512-n1Im9lhKJJE12/u2N0GWBwPqeb0HGdylN8XpSFg9hbj35+QalY9Vi6mxwUQdG6wlSrrIq9ZDQ0Q85AQG9V2WOg==} + engines: {node: '>=18'} + hasBin: true + + markdownlint@0.37.3: + resolution: {integrity: sha512-eoQqH0291YCCjd+Pe1PUQ9AmWthlVmS0XWgcionkZ8q34ceZyRI+pYvsWksXJJL8OBkWCPwp1h/pnXxrPFC4oA==} + engines: {node: '>=18'} + + mathjax-full@3.2.2: + resolution: {integrity: sha512-+LfG9Fik+OuI8SLwsiR02IVdjcnRCy5MufYLi0C3TdMT56L/pjB0alMVGgoWJF8pN9Rc7FESycZB9BMNWIid5w==} + + mdast-util-to-hast@13.2.0: + resolution: {integrity: sha512-QGYKEuUsYT9ykKBCMOEDLsU5JRObWQusAolFMeko/tYPufNkRffBAQjIE+99jbA87xv6FgmjLtwjh9wBWajwAA==} + + mdurl@2.0.0: + resolution: {integrity: sha512-Lf+9+2r+Tdp5wXDXC4PcIBjTDtq4UKjCPMQhKIuzpJNW0b96kVqSwW0bT7FhRSfmAiFYgP+SCRvdrDozfh0U5w==} + + merge2@1.4.1: + resolution: {integrity: sha512-8q7VEgMJW4J8tcfVPy8g09NcQwZdbwFEqhe/WZkoIzjn/3TGDwtOCYtXGxA3O8tPzpczCCDgv+P2P5y00ZJOOg==} + engines: {node: '>= 8'} + + mhchemparser@4.2.1: + resolution: {integrity: sha512-kYmyrCirqJf3zZ9t/0wGgRZ4/ZJw//VwaRVGA75C4nhE60vtnIzhl9J9ndkX/h6hxSN7pjg/cE0VxbnNM+bnDQ==} + + micromark-core-commonmark@2.0.2: + resolution: {integrity: sha512-FKjQKbxd1cibWMM1P9N+H8TwlgGgSkWZMmfuVucLCHaYqeSvJ0hFeHsIa65pA2nYbes0f8LDHPMrd9X7Ujxg9w==} + + micromark-extension-directive@3.0.2: + resolution: {integrity: sha512-wjcXHgk+PPdmvR58Le9d7zQYWy+vKEU9Se44p2CrCDPiLr2FMyiT4Fyb5UFKFC66wGB3kPlgD7q3TnoqPS7SZA==} + + micromark-extension-gfm-autolink-literal@2.1.0: + resolution: {integrity: sha512-oOg7knzhicgQ3t4QCjCWgTmfNhvQbDDnJeVu9v81r7NltNCVmhPy1fJRX27pISafdjL+SVc4d3l48Gb6pbRypw==} + + micromark-extension-gfm-footnote@2.1.0: + resolution: {integrity: sha512-/yPhxI1ntnDNsiHtzLKYnE3vf9JZ6cAisqVDauhp4CEHxlb4uoOTxOCJ+9s51bIB8U1N1FJ1RXOKTIlD5B/gqw==} + + micromark-extension-gfm-table@2.1.0: + resolution: {integrity: sha512-Ub2ncQv+fwD70/l4ou27b4YzfNaCJOvyX4HxXU15m7mpYY+rjuWzsLIPZHJL253Z643RpbcP1oeIJlQ/SKW67g==} + + micromark-extension-math@3.1.0: + resolution: {integrity: sha512-lvEqd+fHjATVs+2v/8kg9i5Q0AP2k85H0WUOwpIVvUML8BapsMvh1XAogmQjOCsLpoKRCVQqEkQBB3NhVBcsOg==} + + micromark-factory-destination@2.0.1: + resolution: {integrity: sha512-Xe6rDdJlkmbFRExpTOmRj9N3MaWmbAgdpSrBQvCFqhezUn4AHqJHbaEnfbVYYiexVSs//tqOdY/DxhjdCiJnIA==} + + micromark-factory-label@2.0.1: + resolution: {integrity: sha512-VFMekyQExqIW7xIChcXn4ok29YE3rnuyveW3wZQWWqF4Nv9Wk5rgJ99KzPvHjkmPXF93FXIbBp6YdW3t71/7Vg==} + + micromark-factory-space@2.0.1: + resolution: {integrity: sha512-zRkxjtBxxLd2Sc0d+fbnEunsTj46SWXgXciZmHq0kDYGnck/ZSGj9/wULTV95uoeYiK5hRXP2mJ98Uo4cq/LQg==} + + micromark-factory-title@2.0.1: + resolution: {integrity: sha512-5bZ+3CjhAd9eChYTHsjy6TGxpOFSKgKKJPJxr293jTbfry2KDoWkhBb6TcPVB4NmzaPhMs1Frm9AZH7OD4Cjzw==} + + micromark-factory-whitespace@2.0.1: + resolution: {integrity: sha512-Ob0nuZ3PKt/n0hORHyvoD9uZhr+Za8sFoP+OnMcnWK5lngSzALgQYKMr9RJVOWLqQYuyn6ulqGWSXdwf6F80lQ==} + + micromark-util-character@2.1.1: + resolution: {integrity: sha512-wv8tdUTJ3thSFFFJKtpYKOYiGP2+v96Hvk4Tu8KpCAsTMs6yi+nVmGh1syvSCsaxz45J6Jbw+9DD6g97+NV67Q==} + + micromark-util-chunked@2.0.1: + resolution: {integrity: sha512-QUNFEOPELfmvv+4xiNg2sRYeS/P84pTW0TCgP5zc9FpXetHY0ab7SxKyAQCNCc1eK0459uoLI1y5oO5Vc1dbhA==} + + micromark-util-classify-character@2.0.1: + resolution: {integrity: sha512-K0kHzM6afW/MbeWYWLjoHQv1sgg2Q9EccHEDzSkxiP/EaagNzCm7T/WMKZ3rjMbvIpvBiZgwR3dKMygtA4mG1Q==} + + micromark-util-combine-extensions@2.0.1: + resolution: {integrity: sha512-OnAnH8Ujmy59JcyZw8JSbK9cGpdVY44NKgSM7E9Eh7DiLS2E9RNQf0dONaGDzEG9yjEl5hcqeIsj4hfRkLH/Bg==} + + micromark-util-decode-numeric-character-reference@2.0.2: + resolution: {integrity: sha512-ccUbYk6CwVdkmCQMyr64dXz42EfHGkPQlBj5p7YVGzq8I7CtjXZJrubAYezf7Rp+bjPseiROqe7G6foFd+lEuw==} + + micromark-util-encode@2.0.1: + resolution: {integrity: sha512-c3cVx2y4KqUnwopcO9b/SCdo2O67LwJJ/UyqGfbigahfegL9myoEFoDYZgkT7f36T0bLrM9hZTAaAyH+PCAXjw==} + + micromark-util-html-tag-name@2.0.1: + resolution: {integrity: sha512-2cNEiYDhCWKI+Gs9T0Tiysk136SnR13hhO8yW6BGNyhOC4qYFnwF1nKfD3HFAIXA5c45RrIG1ub11GiXeYd1xA==} + + micromark-util-normalize-identifier@2.0.1: + resolution: {integrity: sha512-sxPqmo70LyARJs0w2UclACPUUEqltCkJ6PhKdMIDuJ3gSf/Q+/GIe3WKl0Ijb/GyH9lOpUkRAO2wp0GVkLvS9Q==} + + micromark-util-resolve-all@2.0.1: + resolution: {integrity: sha512-VdQyxFWFT2/FGJgwQnJYbe1jjQoNTS4RjglmSjTUlpUMa95Htx9NHeYW4rGDJzbjvCsl9eLjMQwGeElsqmzcHg==} + + micromark-util-sanitize-uri@2.0.1: + resolution: {integrity: sha512-9N9IomZ/YuGGZZmQec1MbgxtlgougxTodVwDzzEouPKo3qFWvymFHWcnDi2vzV1ff6kas9ucW+o3yzJK9YB1AQ==} + + micromark-util-subtokenize@2.0.3: + resolution: {integrity: sha512-VXJJuNxYWSoYL6AJ6OQECCFGhIU2GGHMw8tahogePBrjkG8aCCas3ibkp7RnVOSTClg2is05/R7maAhF1XyQMg==} + + micromark-util-symbol@2.0.1: + resolution: {integrity: sha512-vs5t8Apaud9N28kgCrRUdEed4UJ+wWNvicHLPxCa9ENlYuAY31M0ETy5y1vA33YoNPDFTghEbnh6efaE8h4x0Q==} + + micromark-util-types@2.0.1: + resolution: {integrity: sha512-534m2WhVTddrcKVepwmVEVnUAmtrx9bfIjNoQHRqfnvdaHQiFytEhJoTgpWJvDEXCO5gLTQh3wYC1PgOJA4NSQ==} + + micromark@4.0.1: + resolution: {integrity: sha512-eBPdkcoCNvYcxQOAKAlceo5SNdzZWfF+FcSupREAzdAh9rRmE239CEQAiTwIgblwnoM8zzj35sZ5ZwvSEOF6Kw==} + + micromatch@4.0.8: + resolution: {integrity: sha512-PXwfBhYu0hBCPw8Dn0E+WDYb7af3dSLVWKi3HGv84IdF4TyFoC0ysxFd0Goxw7nSv4T/PzEJQxsYsEiFCKo2BA==} + engines: {node: '>=8.6'} + + mimic-function@5.0.1: + resolution: {integrity: sha512-VP79XUPxV2CigYP3jWwAUFSku2aKqBH7uTAapFWCBqutsbmDo96KY5o8uh6U+/YSIn5OxJnXp73beVkpqMIGhA==} + engines: {node: '>=18'} + + minimatch@3.1.2: + resolution: {integrity: sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==} + + minimatch@9.0.5: + resolution: {integrity: sha512-G6T0ZX48xgozx7587koeX9Ys2NYy6Gmv//P89sEte9V9whIapMNF4idKxnW2QtCcLiTWlb/wfCabAtAFWhhBow==} + engines: {node: '>=16 || 14 >=14.17'} + + minipass-collect@2.0.1: + resolution: {integrity: sha512-D7V8PO9oaz7PWGLbCACuI1qEOsq7UKfLotx/C0Aet43fCUB/wfQ7DYeq2oR/svFJGYDHPr38SHATeaj/ZoKHKw==} + engines: {node: '>=16 || 14 >=14.17'} + + minipass-fetch@3.0.5: + resolution: {integrity: sha512-2N8elDQAtSnFV0Dk7gt15KHsS0Fyz6CbYZ360h0WTYV1Ty46li3rAXVOQj1THMNLdmrD9Vt5pBPtWtVkpwGBqg==} + engines: {node: ^14.17.0 || ^16.13.0 || >=18.0.0} + + minipass-flush@1.0.5: + resolution: {integrity: sha512-JmQSYYpPUqX5Jyn1mXaRwOda1uQ8HP5KAT/oDSLCzt1BYRhQU0/hDtsB1ufZfEEzMZ9aAVmsBw8+FWsIXlClWw==} + engines: {node: '>= 8'} + + minipass-pipeline@1.2.4: + resolution: {integrity: sha512-xuIq7cIOt09RPRJ19gdi4b+RiNvDFYe5JH+ggNvBqGqpQXcru3PcRmOZuHBKWK1Txf9+cQ+HMVN4d6z46LZP7A==} + engines: {node: '>=8'} + + minipass-sized@1.0.3: + resolution: {integrity: sha512-MbkQQ2CTiBMlA2Dm/5cY+9SWFEN8pzzOXi6rlM5Xxq0Yqbda5ZQy9sU75a673FE9ZK0Zsbr6Y5iP6u9nktfg2g==} + engines: {node: '>=8'} + + minipass@3.3.6: + resolution: {integrity: sha512-DxiNidxSEK+tHG6zOIklvNOwm3hvCrbUrdtzY74U6HKTJxvIDfOUL5W5P2Ghd3DTkhhKPYGqeNUIh5qcM4YBfw==} + engines: {node: '>=8'} + + minipass@5.0.0: + resolution: {integrity: sha512-3FnjYuehv9k6ovOEbyOswadCDPX1piCfhV8ncmYtHOjuPwylVWsghTLo7rabjC3Rx5xD4HDx8Wm1xnMF7S5qFQ==} + engines: {node: '>=8'} + + minipass@7.1.2: + resolution: {integrity: sha512-qOOzS1cBTWYF4BH8fVePDBOO9iptMnGUEZwNc/cMWnTV2nVLZ7VoNWEPHkYczZA0pdoA7dl6e7FL659nX9S2aw==} + engines: {node: '>=16 || 14 >=14.17'} + + minizlib@2.1.2: + resolution: {integrity: sha512-bAxsR8BVfj60DWXHE3u30oHzfl4G7khkSuPW+qvpd7jFRHm7dLxOjUk1EHACJ/hxLY8phGJ0YhYHZo7jil7Qdg==} + engines: {node: '>= 8'} + + mitt@3.0.1: + resolution: {integrity: sha512-vKivATfr97l2/QBCYAkXYDbrIWPM2IIKEl7YPhjCvKlG3kE2gm+uBo6nEXK3M5/Ffh/FLpKExzOQ3JJoJGFKBw==} + + mj-context-menu@0.6.1: + resolution: {integrity: sha512-7NO5s6n10TIV96d4g2uDpG7ZDpIhMh0QNfGdJw/W47JswFcosz457wqz/b5sAKvl12sxINGFCn80NZHKwxQEXA==} + + mkdirp@1.0.4: + resolution: {integrity: sha512-vVqVZQyf3WLx2Shd0qJ9xuvqgAyKPLAiqITEtqW0oIUjzo3PePDd6fW9iFz30ef7Ysp/oiWqbhszeGWW2T6Gzw==} + engines: {node: '>=10'} + hasBin: true + + ms@2.1.3: + resolution: {integrity: sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==} + + nano-staged@0.8.0: + resolution: {integrity: sha512-QSEqPGTCJbkHU2yLvfY6huqYPjdBrOaTMKatO1F8nCSrkQGXeKwtCiCnsdxnuMhbg3DTVywKaeWLGCE5oJpq0g==} + engines: {node: ^12.20.0 || ^14.13.1 || >=16.0.0} + hasBin: true + + nanoid@3.3.8: + resolution: {integrity: sha512-WNLf5Sd8oZxOm+TzppcYk8gVOgP+l58xNy58D0nbUnOxOWRWvlcCV4kUF7ltmI6PsrLl/BgKEyS4mqsGChFN0w==} + engines: {node: ^10 || ^12 || ^13.7 || ^14 || >=15.0.1} + hasBin: true + + nanoid@5.0.9: + resolution: {integrity: sha512-Aooyr6MXU6HpvvWXKoVoXwKMs/KyVakWwg7xQfv5/S/RIgJMy0Ifa45H9qqYy7pTCszrHzP21Uk4PZq2HpEM8Q==} + engines: {node: ^18 || >=20} + hasBin: true + + negotiator@0.6.4: + resolution: {integrity: sha512-myRT3DiWPHqho5PrJaIRyaMv2kgYf0mUVgBNOYMuCH5Ki1yEiQaf/ZJuQ62nvpc44wL5WDbTX7yGJi1Neevw8w==} + engines: {node: '>= 0.6'} + + node-addon-api@8.3.0: + resolution: {integrity: sha512-8VOpLHFrOQlAH+qA0ZzuGRlALRA6/LVh8QJldbrC4DY0hXoMP0l4Acq8TzFC018HztWiRqyCEj2aTWY2UvnJUg==} + engines: {node: ^18 || ^20 || >= 21} + + node-fetch@2.7.0: + resolution: {integrity: sha512-c4FRfUm/dbcWZ7U+1Wq0AwCyFL+3nt2bEw05wfxSz+DWpWsitgmSgYmy2dQdWyKC1694ELPqMs/YzUSNozLt8A==} + engines: {node: 4.x || >=6.0.0} + peerDependencies: + encoding: ^0.1.0 + peerDependenciesMeta: + encoding: + optional: true + + node-gyp@10.3.1: + resolution: {integrity: sha512-Pp3nFHBThHzVtNY7U6JfPjvT/DTE8+o/4xKsLQtBoU+j2HLsGlhcfzflAoUreaJbNmYnX+LlLi0qjV8kpyO6xQ==} + engines: {node: ^16.14.0 || >=18.0.0} + hasBin: true + + node-releases@2.0.19: + resolution: {integrity: sha512-xxOWJsBKtzAq7DY0J+DTzuz58K8e7sJbdgwkbMWQe8UYB6ekmsQ45q0M/tJDsGaZmbC+l7n57UV8Hl5tHxO9uw==} + + nodejs-jieba@0.2.1: + resolution: {integrity: sha512-211M6vWoXBZn9+3C6cBuiAXRmwnidbV4eK5O63VZb7kK0miNMkWknUS5Usv/n5gUrT99kHgps+4xL9g/r0F89A==} + engines: {node: ^18.0.0 || ^20.0.0 || ^22.0.0} + + nopt@5.0.0: + resolution: {integrity: sha512-Tbj67rffqceeLpcRXrT7vKAN8CwfPeIBgM7E6iBkmKLV7bEMwpGgYLGv0jACUsECaa/vuxP0IjEont6umdMgtQ==} + engines: {node: '>=6'} + hasBin: true + + nopt@7.2.1: + resolution: {integrity: sha512-taM24ViiimT/XntxbPyJQzCG+p4EKOpgD3mxFwW38mGjVUrfERQOeY4EDHjdnptttfHuHQXFx+lTP08Q+mLa/w==} + engines: {node: ^14.17.0 || ^16.13.0 || >=18.0.0} + hasBin: true + + normalize-path@3.0.0: + resolution: {integrity: sha512-6eZs5Ls3WtCisHWp9S2GUy8dqkpGi4BVSz3GaqiE6ezub0512ESztXUwUB6C6IKbQkY2Pnb/mD4WYojCRwcwLA==} + engines: {node: '>=0.10.0'} -packages: + normalize-range@0.1.2: + resolution: {integrity: sha512-bdok/XvKII3nUpklnV6P2hxtMNrCboOjAcyBuQnWEhO665FwrSNRxU+AqpsyvO6LgGYPspN+lu5CLtw4jPRKNA==} + engines: {node: '>=0.10.0'} - /@ampproject/remapping@2.2.1: - resolution: {integrity: sha512-lFMjJTrFL3j7L9yBxwYfCq2k6qqwHyzuUl/XBnif78PWTJYyL/dfowQHWE3sp6U6ZzqWiiIZnpTMO96zhkjwtg==} - engines: {node: '>=6.0.0'} - dependencies: - '@jridgewell/gen-mapping': 0.3.3 - '@jridgewell/trace-mapping': 0.3.18 - dev: false + npm-run-path@6.0.0: + resolution: {integrity: sha512-9qny7Z9DsQU8Ou39ERsPU4OZQlSTP47ShQzuKZ6PRXpYLtIFgl/DEBYEXKlvcEa+9tHVcK8CF81Y2V72qaZhWA==} + engines: {node: '>=18'} + + npmlog@5.0.1: + resolution: {integrity: sha512-AqZtDUWOMKs1G/8lwylVjrdYgqA4d9nu8hc+0gzRxlDb1I10+FHBGMXs6aiQHFdCUUlqH99MUMuLfzWDNDtfxw==} + deprecated: This package is no longer supported. + + nth-check@2.1.1: + resolution: {integrity: sha512-lqjrjmaOoAnWfMmBPL+XNnynZh2+swxiX3WUE0s4yEHI6m+AwrK2UZOimIRl3X/4QctVqS8AiZjFqyOGrMXb/w==} + + object-assign@4.1.1: + resolution: {integrity: sha512-rJgTQnkUnH1sFw8yT6VSU3zD3sWmu6sZhIseY8VX+GRu3P6F7Fu+JNDoXfklElbLJSnc3FUQHVe4cU5hj+BcUg==} + engines: {node: '>=0.10.0'} + + once@1.4.0: + resolution: {integrity: sha512-lNaJgI+2Q5URQBkccEKHTQOPaXdUxnZZElQTZY0MFUAuaEqe1E+Nyvgdz/aIyNi6Z9MzO5dv1H8n58/GELp3+w==} + + onetime@7.0.0: + resolution: {integrity: sha512-VXJjc87FScF88uafS3JllDgvAm+c/Slfz06lorj2uAY34rlUu0Nt+v8wreiImcrgAjjIHp1rXpTDlLOGw29WwQ==} + engines: {node: '>=18'} + + oniguruma-to-es@1.0.0: + resolution: {integrity: sha512-kihvp0O4lFwf5tZMkfanwQLIZ9ORe9OeOFgZonH0BQeThgwfJiaZFeOfvvJVnJIM9TiVmx0RDD35hUJDR0++rQ==} + + ora@8.1.1: + resolution: {integrity: sha512-YWielGi1XzG1UTvOaCFaNgEnuhZVMSHYkW/FQ7UX8O26PtlpdM84c0f7wLPlkvx2RfiQmnzd61d/MGxmpQeJPw==} + engines: {node: '>=18'} - /@apideck/better-ajv-errors@0.3.6(ajv@8.12.0): - resolution: {integrity: sha512-P+ZygBLZtkp0qqOAJJVX4oX/sFo5JR3eBWwwuqHHhK0GIgQOKWrAfiAaWX0aArHkRWHMuggFEgAZNxVPwPZYaA==} + p-limit@2.3.0: + resolution: {integrity: sha512-//88mFWSJx8lxCzwdAABTJL2MyWB12+eIY7MDL2SqLmAkeKU9qxRvWuSyTjm3FUmpBEMuFfckAIqEaVGUDxb6w==} + engines: {node: '>=6'} + + p-locate@4.1.0: + resolution: {integrity: sha512-R79ZZ/0wAxKGu3oYMlz8jy/kbhsNrS7SKZ7PxEHBgJ5+F2mtFW2fK2cOtBh1cHYkQsbzFV7I+EoRKe6Yt0oK7A==} + engines: {node: '>=8'} + + p-map@4.0.0: + resolution: {integrity: sha512-/bjOqmgETBYB5BoEeGVea8dmvHb2m9GLy1E9W43yeyfP6QQCZGFNa+XRceJEuDB6zqr+gKpIAmlLebMpykw/MQ==} engines: {node: '>=10'} - peerDependencies: - ajv: '>=8' - dependencies: - ajv: 8.12.0 - json-schema: 0.4.0 - jsonpointer: 5.0.1 - leven: 3.1.0 - dev: false - /@babel/code-frame@7.21.4: - resolution: {integrity: sha512-LYvhNKfwWSPpocw8GI7gpK2nq3HSDuEPC/uSYaALSJu9xjsalaaYFOq0Pwt5KmVqwEbZlDu81aLXwBOmD/Fv9g==} - engines: {node: '>=6.9.0'} - dependencies: - '@babel/highlight': 7.18.6 - dev: false + p-try@2.2.0: + resolution: {integrity: sha512-R4nPAVTAU0B9D35/Gk3uJf/7XYbQcyohSKdvAxIRSNghFl4e71hVoGnBNQz9cWaXxO2I10KTC+3jMdvvoKw6dQ==} + engines: {node: '>=6'} - /@babel/compat-data@7.21.7: - resolution: {integrity: sha512-KYMqFYTaenzMK4yUtf4EW9wc4N9ef80FsbMtkwool5zpwl4YrT1SdWYSTRcT94KO4hannogdS+LxY7L+arP3gA==} - engines: {node: '>=6.9.0'} - dev: false + package-json-from-dist@1.0.1: + resolution: {integrity: sha512-UEZIS3/by4OC8vL3P2dTXRETpebLI2NiI5vIrjaD/5UtrkFX/tNbwjTSRAGC/+7CAo2pIcBaRgWmcBBHcsaCIw==} - /@babel/core@7.21.8: - resolution: {integrity: sha512-YeM22Sondbo523Sz0+CirSPnbj9bG3P0CdHcBZdqUuaeOaYEFbOLoGU7lebvGP6P5J/WE9wOn7u7C4J9HvS1xQ==} - engines: {node: '>=6.9.0'} - dependencies: - '@ampproject/remapping': 2.2.1 - '@babel/code-frame': 7.21.4 - '@babel/generator': 7.21.5 - '@babel/helper-compilation-targets': 7.21.5(@babel/core@7.21.8) - '@babel/helper-module-transforms': 7.21.5 - '@babel/helpers': 7.21.5 - '@babel/parser': 7.21.8 - '@babel/template': 7.20.7 - '@babel/traverse': 7.21.5 - '@babel/types': 7.21.5 - convert-source-map: 1.9.0 - debug: 4.3.4 - gensync: 1.0.0-beta.2 - json5: 2.2.3 - semver: 6.3.0 - transitivePeerDependencies: - - supports-color - dev: false + parse-entities@4.0.2: + resolution: {integrity: sha512-GG2AQYWoLgL877gQIKeRPGO1xF9+eG1ujIb5soS5gPvLQ1y2o8FL90w2QWNdf9I361Mpp7726c+lj3U0qK1uGw==} - /@babel/generator@7.21.5: - resolution: {integrity: sha512-SrKK/sRv8GesIW1bDagf9cCG38IOMYZusoe1dfg0D8aiUe3Amvoj1QtjTPAWcfrZFvIwlleLb0gxzQidL9w14w==} - engines: {node: '>=6.9.0'} - dependencies: - '@babel/types': 7.21.5 - '@jridgewell/gen-mapping': 0.3.3 - '@jridgewell/trace-mapping': 0.3.18 - jsesc: 2.5.2 - dev: false + parse-ms@4.0.0: + resolution: {integrity: sha512-TXfryirbmq34y8QBwgqCVLi+8oA3oWx2eAnSn62ITyEhEYaWRlVZ2DvMM9eZbMs/RfxPu/PK/aBLyGj4IrqMHw==} + engines: {node: '>=18'} - /@babel/helper-annotate-as-pure@7.18.6: - resolution: {integrity: sha512-duORpUiYrEpzKIop6iNbjnwKLAKnJ47csTyRACyEmWj0QdUrm5aqNJGHSSEQSUAvNW0ojX0dOmK9dZduvkfeXA==} - engines: {node: '>=6.9.0'} - dependencies: - '@babel/types': 7.21.5 - dev: false + parse5-htmlparser2-tree-adapter@7.1.0: + resolution: {integrity: sha512-ruw5xyKs6lrpo9x9rCZqZZnIUntICjQAd0Wsmp396Ul9lN/h+ifgVV1x1gZHi8euej6wTfpqX8j+BFQxF0NS/g==} - /@babel/helper-builder-binary-assignment-operator-visitor@7.21.5: - resolution: {integrity: sha512-uNrjKztPLkUk7bpCNC0jEKDJzzkvel/W+HguzbN8krA+LPfC1CEobJEvAvGka2A/M+ViOqXdcRL0GqPUJSjx9g==} - engines: {node: '>=6.9.0'} - dependencies: - '@babel/types': 7.21.5 - dev: false + parse5-parser-stream@7.1.2: + resolution: {integrity: sha512-JyeQc9iwFLn5TbvvqACIF/VXG6abODeB3Fwmv/TGdLk2LfbWkaySGY72at4+Ty7EkPZj854u4CrICqNk2qIbow==} - /@babel/helper-compilation-targets@7.21.5(@babel/core@7.21.8): - resolution: {integrity: sha512-1RkbFGUKex4lvsB9yhIfWltJM5cZKUftB2eNajaDv3dCMEp49iBG0K14uH8NnX9IPux2+mK7JGEOB0jn48/J6w==} - engines: {node: '>=6.9.0'} - peerDependencies: - '@babel/core': ^7.0.0 - dependencies: - '@babel/compat-data': 7.21.7 - '@babel/core': 7.21.8 - '@babel/helper-validator-option': 7.21.0 - browserslist: 4.21.5 - lru-cache: 5.1.1 - semver: 6.3.0 - dev: false - - /@babel/helper-create-class-features-plugin@7.21.8(@babel/core@7.21.8): - resolution: {integrity: sha512-+THiN8MqiH2AczyuZrnrKL6cAxFRRQDKW9h1YkBvbgKmAm6mwiacig1qT73DHIWMGo40GRnsEfN3LA+E6NtmSw==} - engines: {node: '>=6.9.0'} - peerDependencies: - '@babel/core': ^7.0.0 - dependencies: - '@babel/core': 7.21.8 - '@babel/helper-annotate-as-pure': 7.18.6 - '@babel/helper-environment-visitor': 7.21.5 - '@babel/helper-function-name': 7.21.0 - '@babel/helper-member-expression-to-functions': 7.21.5 - '@babel/helper-optimise-call-expression': 7.18.6 - '@babel/helper-replace-supers': 7.21.5 - '@babel/helper-skip-transparent-expression-wrappers': 7.20.0 - '@babel/helper-split-export-declaration': 7.18.6 - semver: 6.3.0 - transitivePeerDependencies: - - supports-color - dev: false + parse5@7.2.1: + resolution: {integrity: sha512-BuBYQYlv1ckiPdQi/ohiivi9Sagc9JG+Ozs0r7b/0iK3sKmrb0b9FdWdBbOdx6hBCM/F9Ir82ofnBhtZOjCRPQ==} - /@babel/helper-create-regexp-features-plugin@7.21.8(@babel/core@7.21.8): - resolution: {integrity: sha512-zGuSdedkFtsFHGbexAvNuipg1hbtitDLo2XE8/uf6Y9sOQV1xsYX/2pNbtedp/X0eU1pIt+kGvaqHCowkRbS5g==} - engines: {node: '>=6.9.0'} - peerDependencies: - '@babel/core': ^7.0.0 - dependencies: - '@babel/core': 7.21.8 - '@babel/helper-annotate-as-pure': 7.18.6 - regexpu-core: 5.3.2 - semver: 6.3.0 - dev: false + path-exists@4.0.0: + resolution: {integrity: sha512-ak9Qy5Q7jYb2Wwcey5Fpvg2KoAc/ZIhLSLOSBmRmygPsGwkVVt0fZa0qrtMz+m6tJTAHfZQ8FnmB4MG4LWy7/w==} + engines: {node: '>=8'} - /@babel/helper-define-polyfill-provider@0.3.3(@babel/core@7.21.8): - resolution: {integrity: sha512-z5aQKU4IzbqCC1XH0nAqfsFLMVSo22SBKUc0BxGrLkolTdPTructy0ToNnlO2zA4j9Q/7pjMZf0DSY+DSTYzww==} - peerDependencies: - '@babel/core': ^7.4.0-0 - dependencies: - '@babel/core': 7.21.8 - '@babel/helper-compilation-targets': 7.21.5(@babel/core@7.21.8) - '@babel/helper-plugin-utils': 7.21.5 - debug: 4.3.4 - lodash.debounce: 4.0.8 - resolve: 1.22.2 - semver: 6.3.0 - transitivePeerDependencies: - - supports-color - dev: false + path-is-absolute@1.0.1: + resolution: {integrity: sha512-AVbw3UJ2e9bq64vSaS9Am0fje1Pa8pbGqTTsmXfaIiMpnr5DlDhfJOuLj9Sf95ZPVDAUerDfEk88MPmPe7UCQg==} + engines: {node: '>=0.10.0'} - /@babel/helper-environment-visitor@7.21.5: - resolution: {integrity: sha512-IYl4gZ3ETsWocUWgsFZLM5i1BYx9SoemminVEXadgLBa9TdeorzgLKm8wWLA6J1N/kT3Kch8XIk1laNzYoHKvQ==} - engines: {node: '>=6.9.0'} - dev: false + path-key@3.1.1: + resolution: {integrity: sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q==} + engines: {node: '>=8'} - /@babel/helper-function-name@7.21.0: - resolution: {integrity: sha512-HfK1aMRanKHpxemaY2gqBmL04iAPOPRj7DxtNbiDOrJK+gdwkiNRVpCpUJYbUT+aZyemKN8brqTOxzCaG6ExRg==} - engines: {node: '>=6.9.0'} - dependencies: - '@babel/template': 7.20.7 - '@babel/types': 7.21.5 - dev: false + path-key@4.0.0: + resolution: {integrity: sha512-haREypq7xkM7ErfgIyA0z+Bj4AGKlMSdlQE2jvJo6huWD1EdkKYV+G/T4nq0YEF2vgTT8kqMFKo1uHn950r4SQ==} + engines: {node: '>=12'} - /@babel/helper-hoist-variables@7.18.6: - resolution: {integrity: sha512-UlJQPkFqFULIcyW5sbzgbkxn2FKRgwWiRexcuaR8RNJRy8+LLveqPjwZV/bwrLZCN0eUHD/x8D0heK1ozuoo6Q==} - engines: {node: '>=6.9.0'} - dependencies: - '@babel/types': 7.21.5 - dev: false + path-scurry@1.11.1: + resolution: {integrity: sha512-Xa4Nw17FS9ApQFJ9umLiJS4orGjm7ZzwUrwamcGQuHSzDyth9boKDaycYdDcZDuqYATXw4HFXgaqWTctW/v1HA==} + engines: {node: '>=16 || 14 >=14.18'} - /@babel/helper-member-expression-to-functions@7.21.5: - resolution: {integrity: sha512-nIcGfgwpH2u4n9GG1HpStW5Ogx7x7ekiFHbjjFRKXbn5zUvqO9ZgotCO4x1aNbKn/x/xOUaXEhyNHCwtFCpxWg==} - engines: {node: '>=6.9.0'} - dependencies: - '@babel/types': 7.21.5 - dev: false + path-type@5.0.0: + resolution: {integrity: sha512-5HviZNaZcfqP95rwpv+1HDgUamezbqdSYTyzjTvwtJSnIH+3vnbmWsItli8OFEndS984VT55M3jduxZbX351gg==} + engines: {node: '>=12'} - /@babel/helper-module-imports@7.21.4: - resolution: {integrity: sha512-orajc5T2PsRYUN3ZryCEFeMDYwyw09c/pZeaQEZPH0MpKzSvn3e0uXsDBu3k03VI+9DBiRo+l22BfKTpKwa/Wg==} - engines: {node: '>=6.9.0'} - dependencies: - '@babel/types': 7.21.5 - dev: false + perfect-debounce@1.0.0: + resolution: {integrity: sha512-xCy9V055GLEqoFaHoC1SoLIaLmWctgCUaBaWxDZ7/Zx4CTyX7cJQLJOok/orfjZAh9kEYpjJa4d0KcJmCbctZA==} - /@babel/helper-module-transforms@7.21.5: - resolution: {integrity: sha512-bI2Z9zBGY2q5yMHoBvJ2a9iX3ZOAzJPm7Q8Yz6YeoUjU/Cvhmi2G4QyTNyPBqqXSgTjUxRg3L0xV45HvkNWWBw==} - engines: {node: '>=6.9.0'} - dependencies: - '@babel/helper-environment-visitor': 7.21.5 - '@babel/helper-module-imports': 7.21.4 - '@babel/helper-simple-access': 7.21.5 - '@babel/helper-split-export-declaration': 7.18.6 - '@babel/helper-validator-identifier': 7.19.1 - '@babel/template': 7.20.7 - '@babel/traverse': 7.21.5 - '@babel/types': 7.21.5 - transitivePeerDependencies: - - supports-color - dev: false + photoswipe@5.4.4: + resolution: {integrity: sha512-WNFHoKrkZNnvFFhbHL93WDkW3ifwVOXSW3w1UuZZelSmgXpIGiZSNlZJq37rR8YejqME2rHs9EhH9ZvlvFH2NA==} + engines: {node: '>= 0.12.0'} - /@babel/helper-optimise-call-expression@7.18.6: - resolution: {integrity: sha512-HP59oD9/fEHQkdcbgFCnbmgH5vIQTJbxh2yf+CdM89/glUNnuzr87Q8GIjGEnOktTROemO0Pe0iPAYbqZuOUiA==} - engines: {node: '>=6.9.0'} - dependencies: - '@babel/types': 7.21.5 - dev: false + picocolors@1.1.1: + resolution: {integrity: sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==} - /@babel/helper-plugin-utils@7.21.5: - resolution: {integrity: sha512-0WDaIlXKOX/3KfBK/dwP1oQGiPh6rjMkT7HIRv7i5RR2VUMwrx5ZL0dwBkKx7+SW1zwNdgjHd34IMk5ZjTeHVg==} - engines: {node: '>=6.9.0'} - dev: false + picomatch@2.3.1: + resolution: {integrity: sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA==} + engines: {node: '>=8.6'} - /@babel/helper-remap-async-to-generator@7.18.9(@babel/core@7.21.8): - resolution: {integrity: sha512-dI7q50YKd8BAv3VEfgg7PS7yD3Rtbi2J1XMXaalXO0W0164hYLnh8zpjRS0mte9MfVp/tltvr/cfdXPvJr1opA==} - engines: {node: '>=6.9.0'} + pngjs@5.0.0: + resolution: {integrity: sha512-40QW5YalBNfQo5yRYmiw7Yz6TKKVr3h6970B2YE+3fQpsWcrbj1PzJgxeJ19DRQjhMbKPIuMY8rFaXc8moolVw==} + engines: {node: '>=10.13.0'} + + postcss-load-config@6.0.1: + resolution: {integrity: sha512-oPtTM4oerL+UXmx+93ytZVN82RrlY/wPUV8IeDxFrzIjXOLF1pN+EmKPLbubvKHT2HC20xXsCAH2Z+CKV6Oz/g==} + engines: {node: '>= 18'} peerDependencies: - '@babel/core': ^7.0.0 - dependencies: - '@babel/core': 7.21.8 - '@babel/helper-annotate-as-pure': 7.18.6 - '@babel/helper-environment-visitor': 7.21.5 - '@babel/helper-wrap-function': 7.20.5 - '@babel/types': 7.21.5 - transitivePeerDependencies: - - supports-color - dev: false + jiti: '>=1.21.0' + postcss: '>=8.0.9' + tsx: ^4.8.1 + yaml: ^2.4.2 + peerDependenciesMeta: + jiti: + optional: true + postcss: + optional: true + tsx: + optional: true + yaml: + optional: true - /@babel/helper-replace-supers@7.21.5: - resolution: {integrity: sha512-/y7vBgsr9Idu4M6MprbOVUfH3vs7tsIfnVWv/Ml2xgwvyH6LTngdfbf5AdsKwkJy4zgy1X/kuNrEKvhhK28Yrg==} - engines: {node: '>=6.9.0'} - dependencies: - '@babel/helper-environment-visitor': 7.21.5 - '@babel/helper-member-expression-to-functions': 7.21.5 - '@babel/helper-optimise-call-expression': 7.18.6 - '@babel/template': 7.20.7 - '@babel/traverse': 7.21.5 - '@babel/types': 7.21.5 - transitivePeerDependencies: - - supports-color - dev: false + postcss-value-parser@4.2.0: + resolution: {integrity: sha512-1NNCs6uurfkVbeXG4S8JFT9t19m45ICnif8zWLd5oPSZ50QnwMfK+H3jv408d4jw/7Bttv5axS5IiHoLaVNHeQ==} - /@babel/helper-simple-access@7.21.5: - resolution: {integrity: sha512-ENPDAMC1wAjR0uaCUwliBdiSl1KBJAVnMTzXqi64c2MG8MPR6ii4qf7bSXDqSFbr4W6W028/rf5ivoHop5/mkg==} - engines: {node: '>=6.9.0'} - dependencies: - '@babel/types': 7.21.5 - dev: false + postcss@8.5.0: + resolution: {integrity: sha512-27VKOqrYfPncKA2NrFOVhP5MGAfHKLYn/Q0mz9cNQyRAKYi3VNHwYU2qKKqPCqgBmeeJ0uAFB56NumXZ5ZReXg==} + engines: {node: ^10 || ^12 || >=14} - /@babel/helper-skip-transparent-expression-wrappers@7.20.0: - resolution: {integrity: sha512-5y1JYeNKfvnT8sZcK9DVRtpTbGiomYIHviSP3OQWmDPU3DeH4a1ZlT/N2lyQ5P8egjcRaT/Y9aNqUxK0WsnIIg==} - engines: {node: '>=6.9.0'} - dependencies: - '@babel/types': 7.21.5 - dev: false + prettier@3.4.2: + resolution: {integrity: sha512-e9MewbtFo+Fevyuxn/4rrcDAaq0IYxPGLvObpQjiZBMAzB9IGmzlnG9RZy3FFas+eBMu2vA0CszMeduow5dIuQ==} + engines: {node: '>=14'} + hasBin: true - /@babel/helper-split-export-declaration@7.18.6: - resolution: {integrity: sha512-bde1etTx6ZyTmobl9LLMMQsaizFVZrquTEHOqKeQESMKo4PlObf+8+JA25ZsIpZhT/WEd39+vOdLXAFG/nELpA==} - engines: {node: '>=6.9.0'} - dependencies: - '@babel/types': 7.21.5 - dev: false + pretty-ms@9.2.0: + resolution: {integrity: sha512-4yf0QO/sllf/1zbZWYnvWw3NxCQwLXKzIj0G849LSufP15BXKM0rbD2Z3wVnkMfjdn/CB0Dpp444gYAACdsplg==} + engines: {node: '>=18'} - /@babel/helper-string-parser@7.21.5: - resolution: {integrity: sha512-5pTUx3hAJaZIdW99sJ6ZUUgWq/Y+Hja7TowEnLNMm1VivRgZQL3vpBY3qUACVsvw+yQU6+YgfBVmcbLaZtrA1w==} - engines: {node: '>=6.9.0'} - dev: false + proc-log@4.2.0: + resolution: {integrity: sha512-g8+OnU/L2v+wyiVK+D5fA34J7EH8jZ8DDlvwhRCMxmMj7UCBvxiO1mGeN+36JXIKF4zevU4kRBd8lVgG9vLelA==} + engines: {node: ^14.17.0 || ^16.13.0 || >=18.0.0} - /@babel/helper-validator-identifier@7.19.1: - resolution: {integrity: sha512-awrNfaMtnHUr653GgGEs++LlAvW6w+DcPrOliSMXWCKo597CwL5Acf/wWdNkf/tfEQE3mjkeD1YOVZOUV/od1w==} - engines: {node: '>=6.9.0'} - dev: false + promise-retry@2.0.1: + resolution: {integrity: sha512-y+WKFlBR8BGXnsNlIHFGPZmyDf3DFMoLhaflAnyZgV6rG6xu+JwesTo2Q9R6XwYmtmwAFCkAk3e35jEdoeh/3g==} + engines: {node: '>=10'} - /@babel/helper-validator-option@7.21.0: - resolution: {integrity: sha512-rmL/B8/f0mKS2baE9ZpyTcTavvEuWhTTW8amjzXNvYG4AwBsqTLikfXsEofsJEfKHf+HQVQbFOHy6o+4cnC/fQ==} - engines: {node: '>=6.9.0'} - dev: false + property-information@6.5.0: + resolution: {integrity: sha512-PgTgs/BlvHxOu8QuEN7wi5A0OmXaBcHpmCSTehcs6Uuu9IkDIEo13Hy7n898RHfrQ49vKCoGeWZSaAK01nwVig==} - /@babel/helper-wrap-function@7.20.5: - resolution: {integrity: sha512-bYMxIWK5mh+TgXGVqAtnu5Yn1un+v8DDZtqyzKRLUzrh70Eal2O3aZ7aPYiMADO4uKlkzOiRiZ6GX5q3qxvW9Q==} - engines: {node: '>=6.9.0'} - dependencies: - '@babel/helper-function-name': 7.21.0 - '@babel/template': 7.20.7 - '@babel/traverse': 7.21.5 - '@babel/types': 7.21.5 - transitivePeerDependencies: - - supports-color - dev: false + punycode.js@2.3.1: + resolution: {integrity: sha512-uxFIHU0YlHYhDQtV4R9J6a52SLx28BCjT+4ieh7IGbgwVJWO+km431c4yRlREUAsAmt/uMjQUyQHNEPf0M39CA==} + engines: {node: '>=6'} - /@babel/helpers@7.21.5: - resolution: {integrity: sha512-BSY+JSlHxOmGsPTydUkPf1MdMQ3M81x5xGCOVgWM3G8XH77sJ292Y2oqcp0CbbgxhqBuI46iUz1tT7hqP7EfgA==} - engines: {node: '>=6.9.0'} - dependencies: - '@babel/template': 7.20.7 - '@babel/traverse': 7.21.5 - '@babel/types': 7.21.5 - transitivePeerDependencies: - - supports-color - dev: false + qrcode@1.5.4: + resolution: {integrity: sha512-1ca71Zgiu6ORjHqFBDpnSMTR2ReToX4l1Au1VFLyVeBTFavzQnv5JxMFr3ukHVKpSrSA2MCk0lNJSykjUfz7Zg==} + engines: {node: '>=10.13.0'} + hasBin: true - /@babel/highlight@7.18.6: - resolution: {integrity: sha512-u7stbOuYjaPezCuLj29hNW1v64M2Md2qupEKP1fHc7WdOA3DgLh37suiSrZYY7haUB7iBeQZ9P1uiRF359do3g==} - engines: {node: '>=6.9.0'} - dependencies: - '@babel/helper-validator-identifier': 7.19.1 - chalk: 2.4.2 - js-tokens: 4.0.0 - dev: false + queue-microtask@1.2.3: + resolution: {integrity: sha512-NuaNSa6flKT5JaSYQzJok04JzTL1CA6aGhv5rfLW3PgqA+M2ChpZQnAC8h8i4ZFkBS8X5RqkDBHA7r4hej3K9A==} - /@babel/parser@7.21.8: - resolution: {integrity: sha512-6zavDGdzG3gUqAdWvlLFfk+36RilI+Pwyuuh7HItyeScCWP3k6i8vKclAQ0bM/0y/Kz/xiwvxhMv9MgTJP5gmA==} - engines: {node: '>=6.0.0'} + readable-stream@3.6.2: + resolution: {integrity: sha512-9u/sniCrY3D5WdsERHzHE4G2YCXqoG5FTHUiCC4SIbr6XcLZBY05ya9EKjYek9O5xOAwjGq+1JdGBAS7Q9ScoA==} + engines: {node: '>= 6'} + + readdirp@3.6.0: + resolution: {integrity: sha512-hOS089on8RduqdbhvQ5Z37A0ESjsqz6qnRcffsMU3495FuTdqSm+7bhJ29JvIOsBDEEnan5DPu9t3To9VRlMzA==} + engines: {node: '>=8.10.0'} + + readdirp@4.1.1: + resolution: {integrity: sha512-h80JrZu/MHUZCyHu5ciuoI0+WxsCxzxJTILn6Fs8rxSnFPh+UVHYfeIxK1nVGugMqkfC4vJcBOYbkfkwYK0+gw==} + engines: {node: '>= 14.18.0'} + + regex-recursion@5.1.1: + resolution: {integrity: sha512-ae7SBCbzVNrIjgSbh7wMznPcQel1DNlDtzensnFxpiNpXt1U2ju/bHugH422r+4LAVS1FpW1YCwilmnNsjum9w==} + + regex-utilities@2.3.0: + resolution: {integrity: sha512-8VhliFJAWRaUiVvREIiW2NXXTmHs4vMNnSzuJVhscgmGav3g9VDxLrQndI3dZZVVdp0ZO/5v0xmX516/7M9cng==} + + regex@5.1.1: + resolution: {integrity: sha512-dN5I359AVGPnwzJm2jN1k0W9LPZ+ePvoOeVMMfqIMFz53sSwXkxaJoxr50ptnsC771lK95BnTrVSZxq0b9yCGw==} + + require-directory@2.1.1: + resolution: {integrity: sha512-fGxEI7+wsG9xrvdjsrlmL22OMTTiHRwAMroiEeMgq8gzoLC/PQr7RsRDSTLUg/bZAZtF+TVIkHc6/4RIKrui+Q==} + engines: {node: '>=0.10.0'} + + require-main-filename@2.0.0: + resolution: {integrity: sha512-NKN5kMDylKuldxYLSUfrbo5Tuzh4hd+2E8NPPX02mZtn1VuREQToYe/ZdlJy+J3uCpfaiGF05e7B8W0iXbQHmg==} + + restore-cursor@5.1.0: + resolution: {integrity: sha512-oMA2dcrw6u0YfxJQXm342bFKX/E4sG9rbTzO9ptUcR/e8A33cHuvStiYOwH7fszkZlZ1z/ta9AAoPk2F4qIOHA==} + engines: {node: '>=18'} + + retry@0.12.0: + resolution: {integrity: sha512-9LkiTwjUh6rT555DtE9rTX+BKByPfrMzEAtnlEtdEwr3Nkffwiihqe2bWADg+OQRjt9gl6ICdmB/ZFDCGAtSow==} + engines: {node: '>= 4'} + + reusify@1.0.4: + resolution: {integrity: sha512-U9nH88a3fc/ekCF1l0/UP1IosiuIjyTh7hBvXVMHYgVcfGvt897Xguj2UOLDeI5BG2m7/uwyaLVT6fbtCwTyzw==} + engines: {iojs: '>=1.0.0', node: '>=0.10.0'} + + rfdc@1.4.1: + resolution: {integrity: sha512-q1b3N5QkRUWUl7iyylaaj3kOpIT0N2i9MqIEQXP73GVsN9cw3fdx8X63cEmWhJGi2PPCF23Ijp7ktmd39rawIA==} + + rimraf@3.0.2: + resolution: {integrity: sha512-JZkJMZkAGFFPP2YqXZXPbMlMBgsxzE8ILs4lMIX/2o0L9UBw9O/Y3o6wFw/i9YLapcUJWwqbi3kdxIPdC62TIA==} + deprecated: Rimraf versions prior to v4 are no longer supported hasBin: true - dependencies: - '@babel/types': 7.21.5 - dev: false - /@babel/plugin-bugfix-safari-id-destructuring-collision-in-function-expression@7.18.6(@babel/core@7.21.8): - resolution: {integrity: sha512-Dgxsyg54Fx1d4Nge8UnvTrED63vrwOdPmyvPzlNN/boaliRP54pm3pGzZD1SJUwrBA+Cs/xdG8kXX6Mn/RfISQ==} - engines: {node: '>=6.9.0'} - peerDependencies: - '@babel/core': ^7.0.0 - dependencies: - '@babel/core': 7.21.8 - '@babel/helper-plugin-utils': 7.21.5 - dev: false + rollup@4.30.1: + resolution: {integrity: sha512-mlJ4glW020fPuLi7DkM/lN97mYEZGWeqBnrljzN0gs7GLctqX3lNWxKQ7Gl712UAX+6fog/L3jh4gb7R6aVi3w==} + engines: {node: '>=18.0.0', npm: '>=8.0.0'} + hasBin: true - /@babel/plugin-bugfix-v8-spread-parameters-in-optional-chaining@7.20.7(@babel/core@7.21.8): - resolution: {integrity: sha512-sbr9+wNE5aXMBBFBICk01tt7sBf2Oc9ikRFEcem/ZORup9IMUdNhW7/wVLEbbtlWOsEubJet46mHAL2C8+2jKQ==} - engines: {node: '>=6.9.0'} - peerDependencies: - '@babel/core': ^7.13.0 - dependencies: - '@babel/core': 7.21.8 - '@babel/helper-plugin-utils': 7.21.5 - '@babel/helper-skip-transparent-expression-wrappers': 7.20.0 - '@babel/plugin-proposal-optional-chaining': 7.21.0(@babel/core@7.21.8) - dev: false + run-parallel@1.2.0: + resolution: {integrity: sha512-5l4VyZR86LZ/lDxZTR6jqL8AFE2S0IFLMP26AbjsLVADxHdhB/c0GUsH+y39UfCi3dzz8OlQuPmnaJOMoDHQBA==} - /@babel/plugin-proposal-async-generator-functions@7.20.7(@babel/core@7.21.8): - resolution: {integrity: sha512-xMbiLsn/8RK7Wq7VeVytytS2L6qE69bXPB10YCmMdDZbKF4okCqY74pI/jJQ/8U0b/F6NrT2+14b8/P9/3AMGA==} - engines: {node: '>=6.9.0'} - peerDependencies: - '@babel/core': ^7.0.0-0 - dependencies: - '@babel/core': 7.21.8 - '@babel/helper-environment-visitor': 7.21.5 - '@babel/helper-plugin-utils': 7.21.5 - '@babel/helper-remap-async-to-generator': 7.18.9(@babel/core@7.21.8) - '@babel/plugin-syntax-async-generators': 7.8.4(@babel/core@7.21.8) - transitivePeerDependencies: - - supports-color - dev: false + rxjs@7.8.1: + resolution: {integrity: sha512-AA3TVj+0A2iuIoQkWEK/tqFjBq2j+6PO6Y0zJcvzLAFhEFIO3HL0vls9hWLncZbAAbK0mar7oZ4V079I/qPMxg==} - /@babel/plugin-proposal-class-properties@7.18.6(@babel/core@7.21.8): - resolution: {integrity: sha512-cumfXOF0+nzZrrN8Rf0t7M+tF6sZc7vhQwYQck9q1/5w2OExlD+b4v4RpMJFaV1Z7WcDRgO6FqvxqxGlwo+RHQ==} - engines: {node: '>=6.9.0'} - peerDependencies: - '@babel/core': ^7.0.0-0 - dependencies: - '@babel/core': 7.21.8 - '@babel/helper-create-class-features-plugin': 7.21.8(@babel/core@7.21.8) - '@babel/helper-plugin-utils': 7.21.5 - transitivePeerDependencies: - - supports-color - dev: false + safe-buffer@5.2.1: + resolution: {integrity: sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ==} - /@babel/plugin-proposal-class-static-block@7.21.0(@babel/core@7.21.8): - resolution: {integrity: sha512-XP5G9MWNUskFuP30IfFSEFB0Z6HzLIUcjYM4bYOPHXl7eiJ9HFv8tWj6TXTN5QODiEhDZAeI4hLok2iHFFV4hw==} - engines: {node: '>=6.9.0'} - peerDependencies: - '@babel/core': ^7.12.0 - dependencies: - '@babel/core': 7.21.8 - '@babel/helper-create-class-features-plugin': 7.21.8(@babel/core@7.21.8) - '@babel/helper-plugin-utils': 7.21.5 - '@babel/plugin-syntax-class-static-block': 7.14.5(@babel/core@7.21.8) - transitivePeerDependencies: - - supports-color - dev: false + safer-buffer@2.1.2: + resolution: {integrity: sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==} - /@babel/plugin-proposal-dynamic-import@7.18.6(@babel/core@7.21.8): - resolution: {integrity: sha512-1auuwmK+Rz13SJj36R+jqFPMJWyKEDd7lLSdOj4oJK0UTgGueSAtkrCvz9ewmgyU/P941Rv2fQwZJN8s6QruXw==} - engines: {node: '>=6.9.0'} - peerDependencies: - '@babel/core': ^7.0.0-0 - dependencies: - '@babel/core': 7.21.8 - '@babel/helper-plugin-utils': 7.21.5 - '@babel/plugin-syntax-dynamic-import': 7.8.3(@babel/core@7.21.8) - dev: false + sass-embedded-android-arm64@1.83.1: + resolution: {integrity: sha512-S63rlLPGCA9FCqYYOobDJrwcuBX0zbSOl7y0jT9DlfqeqNOkC6NIT1id6RpMFCs3uhd4gbBS2E/5WPv5J5qwbw==} + engines: {node: '>=14.0.0'} + cpu: [arm64] + os: [android] - /@babel/plugin-proposal-export-namespace-from@7.18.9(@babel/core@7.21.8): - resolution: {integrity: sha512-k1NtHyOMvlDDFeb9G5PhUXuGj8m/wiwojgQVEhJ/fsVsMCpLyOP4h0uGEjYJKrRI+EVPlb5Jk+Gt9P97lOGwtA==} - engines: {node: '>=6.9.0'} - peerDependencies: - '@babel/core': ^7.0.0-0 - dependencies: - '@babel/core': 7.21.8 - '@babel/helper-plugin-utils': 7.21.5 - '@babel/plugin-syntax-export-namespace-from': 7.8.3(@babel/core@7.21.8) - dev: false + sass-embedded-android-arm@1.83.1: + resolution: {integrity: sha512-FKfrmwDG84L5cfn8fmIew47qnCFFUdcoOTCzOw8ROItkRhLLH0hnIm6gEpG5T6OFf6kxzUxvE9D0FvYQUznZrw==} + engines: {node: '>=14.0.0'} + cpu: [arm] + os: [android] - /@babel/plugin-proposal-json-strings@7.18.6(@babel/core@7.21.8): - resolution: {integrity: sha512-lr1peyn9kOdbYc0xr0OdHTZ5FMqS6Di+H0Fz2I/JwMzGmzJETNeOFq2pBySw6X/KFL5EWDjlJuMsUGRFb8fQgQ==} - engines: {node: '>=6.9.0'} - peerDependencies: - '@babel/core': ^7.0.0-0 - dependencies: - '@babel/core': 7.21.8 - '@babel/helper-plugin-utils': 7.21.5 - '@babel/plugin-syntax-json-strings': 7.8.3(@babel/core@7.21.8) - dev: false + sass-embedded-android-ia32@1.83.1: + resolution: {integrity: sha512-AGlY2vFLJhF2hN0qOz12f4eDs6x0b5BUapOpgfRrqQLHIfJhxkvi39bInsiBgQ57U0jb4I7AaS2e2e+sj7+Rqw==} + engines: {node: '>=14.0.0'} + cpu: [ia32] + os: [android] - /@babel/plugin-proposal-logical-assignment-operators@7.20.7(@babel/core@7.21.8): - resolution: {integrity: sha512-y7C7cZgpMIjWlKE5T7eJwp+tnRYM89HmRvWM5EQuB5BoHEONjmQ8lSNmBUwOyy/GFRsohJED51YBF79hE1djug==} - engines: {node: '>=6.9.0'} - peerDependencies: - '@babel/core': ^7.0.0-0 - dependencies: - '@babel/core': 7.21.8 - '@babel/helper-plugin-utils': 7.21.5 - '@babel/plugin-syntax-logical-assignment-operators': 7.10.4(@babel/core@7.21.8) - dev: false + sass-embedded-android-riscv64@1.83.1: + resolution: {integrity: sha512-OyU4AnfAUVd/wBaT60XvHidmQdaEsVUnxvI71oyPM/id1v97aWTZX3SmGkwGb7uA/q6Soo2uNalgvOSNJn7PwA==} + engines: {node: '>=14.0.0'} + cpu: [riscv64] + os: [android] - /@babel/plugin-proposal-nullish-coalescing-operator@7.18.6(@babel/core@7.21.8): - resolution: {integrity: sha512-wQxQzxYeJqHcfppzBDnm1yAY0jSRkUXR2z8RePZYrKwMKgMlE8+Z6LUno+bd6LvbGh8Gltvy74+9pIYkr+XkKA==} - engines: {node: '>=6.9.0'} - peerDependencies: - '@babel/core': ^7.0.0-0 - dependencies: - '@babel/core': 7.21.8 - '@babel/helper-plugin-utils': 7.21.5 - '@babel/plugin-syntax-nullish-coalescing-operator': 7.8.3(@babel/core@7.21.8) - dev: false + sass-embedded-android-x64@1.83.1: + resolution: {integrity: sha512-NY5rwffhF4TnhXVErZnfFIjHqU3MNoWxCuSHumRN3dDI8hp8+IF59W5+Qw9AARlTXvyb+D0u5653aLSea5F40w==} + engines: {node: '>=14.0.0'} + cpu: [x64] + os: [android] - /@babel/plugin-proposal-numeric-separator@7.18.6(@babel/core@7.21.8): - resolution: {integrity: sha512-ozlZFogPqoLm8WBr5Z8UckIoE4YQ5KESVcNudyXOR8uqIkliTEgJ3RoketfG6pmzLdeZF0H/wjE9/cCEitBl7Q==} - engines: {node: '>=6.9.0'} - peerDependencies: - '@babel/core': ^7.0.0-0 - dependencies: - '@babel/core': 7.21.8 - '@babel/helper-plugin-utils': 7.21.5 - '@babel/plugin-syntax-numeric-separator': 7.10.4(@babel/core@7.21.8) - dev: false + sass-embedded-darwin-arm64@1.83.1: + resolution: {integrity: sha512-w1SBcSkIgIWgUfB7IKcPoTbSwnS3Kag5PVv3e3xfW6ZCsDweYZLQntUd2WGgaoekdm1uIbVuvPxnDH2t880iGQ==} + engines: {node: '>=14.0.0'} + cpu: [arm64] + os: [darwin] - /@babel/plugin-proposal-object-rest-spread@7.20.7(@babel/core@7.21.8): - resolution: {integrity: sha512-d2S98yCiLxDVmBmE8UjGcfPvNEUbA1U5q5WxaWFUGRzJSVAZqm5W6MbPct0jxnegUZ0niLeNX+IOzEs7wYg9Dg==} - engines: {node: '>=6.9.0'} - peerDependencies: - '@babel/core': ^7.0.0-0 - dependencies: - '@babel/compat-data': 7.21.7 - '@babel/core': 7.21.8 - '@babel/helper-compilation-targets': 7.21.5(@babel/core@7.21.8) - '@babel/helper-plugin-utils': 7.21.5 - '@babel/plugin-syntax-object-rest-spread': 7.8.3(@babel/core@7.21.8) - '@babel/plugin-transform-parameters': 7.21.3(@babel/core@7.21.8) - dev: false - - /@babel/plugin-proposal-optional-catch-binding@7.18.6(@babel/core@7.21.8): - resolution: {integrity: sha512-Q40HEhs9DJQyaZfUjjn6vE8Cv4GmMHCYuMGIWUnlxH6400VGxOuwWsPt4FxXxJkC/5eOzgn0z21M9gMT4MOhbw==} - engines: {node: '>=6.9.0'} - peerDependencies: - '@babel/core': ^7.0.0-0 - dependencies: - '@babel/core': 7.21.8 - '@babel/helper-plugin-utils': 7.21.5 - '@babel/plugin-syntax-optional-catch-binding': 7.8.3(@babel/core@7.21.8) - dev: false + sass-embedded-darwin-x64@1.83.1: + resolution: {integrity: sha512-RWrmLtUhEP5kvcGOAFdr99/ebZ/eW9z3FAktLldvgl2k96WSTC1Zr2ctL0E+Y+H3uLahEZsshIFk6RkVIRKIsA==} + engines: {node: '>=14.0.0'} + cpu: [x64] + os: [darwin] - /@babel/plugin-proposal-optional-chaining@7.21.0(@babel/core@7.21.8): - resolution: {integrity: sha512-p4zeefM72gpmEe2fkUr/OnOXpWEf8nAgk7ZYVqqfFiyIG7oFfVZcCrU64hWn5xp4tQ9LkV4bTIa5rD0KANpKNA==} - engines: {node: '>=6.9.0'} - peerDependencies: - '@babel/core': ^7.0.0-0 - dependencies: - '@babel/core': 7.21.8 - '@babel/helper-plugin-utils': 7.21.5 - '@babel/helper-skip-transparent-expression-wrappers': 7.20.0 - '@babel/plugin-syntax-optional-chaining': 7.8.3(@babel/core@7.21.8) - dev: false + sass-embedded-linux-arm64@1.83.1: + resolution: {integrity: sha512-HVIytzj8OO18fmBY6SVRIYErcJ+Nd9a5RNF6uArav/CqvwPLATlUV8dwqSyWQIzSsQUhDF/vFIlJIoNLKKzD3A==} + engines: {node: '>=14.0.0'} + cpu: [arm64] + os: [linux] - /@babel/plugin-proposal-private-methods@7.18.6(@babel/core@7.21.8): - resolution: {integrity: sha512-nutsvktDItsNn4rpGItSNV2sz1XwS+nfU0Rg8aCx3W3NOKVzdMjJRu0O5OkgDp3ZGICSTbgRpxZoWsxoKRvbeA==} - engines: {node: '>=6.9.0'} - peerDependencies: - '@babel/core': ^7.0.0-0 - dependencies: - '@babel/core': 7.21.8 - '@babel/helper-create-class-features-plugin': 7.21.8(@babel/core@7.21.8) - '@babel/helper-plugin-utils': 7.21.5 - transitivePeerDependencies: - - supports-color - dev: false + sass-embedded-linux-arm@1.83.1: + resolution: {integrity: sha512-y7rHuRgjg2YM284rin068PsEdthPljSGb653Slut5Wba4A2IP11UNVraSl6Je2AYTuoPRjQX0g7XdsrjXlzC3g==} + engines: {node: '>=14.0.0'} + cpu: [arm] + os: [linux] - /@babel/plugin-proposal-private-property-in-object@7.21.0(@babel/core@7.21.8): - resolution: {integrity: sha512-ha4zfehbJjc5MmXBlHec1igel5TJXXLDDRbuJ4+XT2TJcyD9/V1919BA8gMvsdHcNMBy4WBUBiRb3nw/EQUtBw==} - engines: {node: '>=6.9.0'} - peerDependencies: - '@babel/core': ^7.0.0-0 - dependencies: - '@babel/core': 7.21.8 - '@babel/helper-annotate-as-pure': 7.18.6 - '@babel/helper-create-class-features-plugin': 7.21.8(@babel/core@7.21.8) - '@babel/helper-plugin-utils': 7.21.5 - '@babel/plugin-syntax-private-property-in-object': 7.14.5(@babel/core@7.21.8) - transitivePeerDependencies: - - supports-color - dev: false + sass-embedded-linux-ia32@1.83.1: + resolution: {integrity: sha512-/pc+jHllyvfaYYLTRCoXseRc4+V3Z7IDPqsviTcfVdICAoR9mgK2RtIuIZanhm1NP/lDylDOgvj1NtjcA2dNvg==} + engines: {node: '>=14.0.0'} + cpu: [ia32] + os: [linux] + + sass-embedded-linux-musl-arm64@1.83.1: + resolution: {integrity: sha512-wjSIYYqdIQp3DjliSTYNFg04TVqQf/3Up/Stahol0Qf/TTjLkjHHtT2jnDaZI5GclHi2PVJqQF3wEGB8bGJMzQ==} + engines: {node: '>=14.0.0'} + cpu: [arm64] + os: [linux] + + sass-embedded-linux-musl-arm@1.83.1: + resolution: {integrity: sha512-sFM8GXOVoeR91j9MiwNRcFXRpTA7u4185SaGuvUjcRMb84mHvtWOJPGDvgZqbWdVClBRJp6J7+CShliWngy/og==} + engines: {node: '>=14.0.0'} + cpu: [arm] + os: [linux] + + sass-embedded-linux-musl-ia32@1.83.1: + resolution: {integrity: sha512-iwhTH5gwmoGt3VH6dn4WV8N6eWvthKAvUX5XPURq7e9KEsc7QP8YNHagwaAJh7TAPopb32buyEg6oaUmzxUI+Q==} + engines: {node: '>=14.0.0'} + cpu: [ia32] + os: [linux] + + sass-embedded-linux-musl-riscv64@1.83.1: + resolution: {integrity: sha512-FjFNWHU1n0Q6GpK1lAHQL5WmzlPjL8DTVLkYW2A/dq8EsutAdi3GfpeyWZk9bte8kyWdmPUWG3BHlnQl22xdoA==} + engines: {node: '>=14.0.0'} + cpu: [riscv64] + os: [linux] - /@babel/plugin-proposal-unicode-property-regex@7.18.6(@babel/core@7.21.8): - resolution: {integrity: sha512-2BShG/d5yoZyXZfVePH91urL5wTG6ASZU9M4o03lKK8u8UW1y08OMttBSOADTcJrnPMpvDXRG3G8fyLh4ovs8w==} - engines: {node: '>=4'} - peerDependencies: - '@babel/core': ^7.0.0-0 - dependencies: - '@babel/core': 7.21.8 - '@babel/helper-create-regexp-features-plugin': 7.21.8(@babel/core@7.21.8) - '@babel/helper-plugin-utils': 7.21.5 - dev: false + sass-embedded-linux-musl-x64@1.83.1: + resolution: {integrity: sha512-BUfYR5TIDvgGHWhxSIKwTJocXU88ECZ0BW89RJqtvr7m83fKdf5ylTFCOieU7BwcA7SORUeZzcQzVFIdPUM3BQ==} + engines: {node: '>=14.0.0'} + cpu: [x64] + os: [linux] - /@babel/plugin-syntax-async-generators@7.8.4(@babel/core@7.21.8): - resolution: {integrity: sha512-tycmZxkGfZaxhMRbXlPXuVFpdWlXpir2W4AMhSJgRKzk/eDlIXOhb2LHWoLpDF7TEHylV5zNhykX6KAgHJmTNw==} - peerDependencies: - '@babel/core': ^7.0.0-0 - dependencies: - '@babel/core': 7.21.8 - '@babel/helper-plugin-utils': 7.21.5 - dev: false + sass-embedded-linux-riscv64@1.83.1: + resolution: {integrity: sha512-KOBGSpMrJi8y+H+za3vAAVQImPUvQa5eUrvTbbOl+wkU7WAGhOu8xrxgmYYiz3pZVBBcfRjz4I2jBcDFKJmWSw==} + engines: {node: '>=14.0.0'} + cpu: [riscv64] + os: [linux] - /@babel/plugin-syntax-class-properties@7.12.13(@babel/core@7.21.8): - resolution: {integrity: sha512-fm4idjKla0YahUNgFNLCB0qySdsoPiZP3iQE3rky0mBUtMZ23yDJ9SJdg6dXTSDnulOVqiF3Hgr9nbXvXTQZYA==} - peerDependencies: - '@babel/core': ^7.0.0-0 - dependencies: - '@babel/core': 7.21.8 - '@babel/helper-plugin-utils': 7.21.5 - dev: false + sass-embedded-linux-x64@1.83.1: + resolution: {integrity: sha512-swUsMHKqlEU9dZQ/I5WADDaXz+QkmJS27x/Oeh+oz41YgZ0ppKd0l4Vwjn0LgOQn+rxH1zLFv6xXDycvj68F/w==} + engines: {node: '>=14.0.0'} + cpu: [x64] + os: [linux] - /@babel/plugin-syntax-class-static-block@7.14.5(@babel/core@7.21.8): - resolution: {integrity: sha512-b+YyPmr6ldyNnM6sqYeMWE+bgJcJpO6yS4QD7ymxgH34GBPNDM/THBh8iunyvKIZztiwLH4CJZ0RxTk9emgpjw==} - engines: {node: '>=6.9.0'} - peerDependencies: - '@babel/core': ^7.0.0-0 - dependencies: - '@babel/core': 7.21.8 - '@babel/helper-plugin-utils': 7.21.5 - dev: false + sass-embedded-win32-arm64@1.83.1: + resolution: {integrity: sha512-6lONEBN5TaFD5L/y68zUugryXqm4RAFuLdaOPeZQRu+7ay/AmfhtFYfE5gRssnIcIx1nlcoq7zA3UX+SN2jo1Q==} + engines: {node: '>=14.0.0'} + cpu: [arm64] + os: [win32] - /@babel/plugin-syntax-dynamic-import@7.8.3(@babel/core@7.21.8): - resolution: {integrity: sha512-5gdGbFon+PszYzqs83S3E5mpi7/y/8M9eC90MRTZfduQOYW76ig6SOSPNe41IG5LoP3FGBn2N0RjVDSQiS94kQ==} - peerDependencies: - '@babel/core': ^7.0.0-0 - dependencies: - '@babel/core': 7.21.8 - '@babel/helper-plugin-utils': 7.21.5 - dev: false + sass-embedded-win32-ia32@1.83.1: + resolution: {integrity: sha512-HxZDkAE9n6Gb8Rz6xd67VHuo5FkUSQ4xPb7cHKa4pE0ndwH5Oc0uEhbqjJobpgmnuTm1rQYNU2nof1sFhy2MFA==} + engines: {node: '>=14.0.0'} + cpu: [ia32] + os: [win32] - /@babel/plugin-syntax-export-namespace-from@7.8.3(@babel/core@7.21.8): - resolution: {integrity: sha512-MXf5laXo6c1IbEbegDmzGPwGNTsHZmEy6QGznu5Sh2UCWvueywb2ee+CCE4zQiZstxU9BMoQO9i6zUFSY0Kj0Q==} - peerDependencies: - '@babel/core': ^7.0.0-0 - dependencies: - '@babel/core': 7.21.8 - '@babel/helper-plugin-utils': 7.21.5 - dev: false + sass-embedded-win32-x64@1.83.1: + resolution: {integrity: sha512-5Q0aPfUaqRek8Ee1AqTUIC0o6yQSA8QwyhCgh7upsnHG3Ltm8pkJOYjzm+UgYPJeoMNppDjdDlRGQISE7qzd4g==} + engines: {node: '>=14.0.0'} + cpu: [x64] + os: [win32] - /@babel/plugin-syntax-import-assertions@7.20.0(@babel/core@7.21.8): - resolution: {integrity: sha512-IUh1vakzNoWalR8ch/areW7qFopR2AEw03JlG7BbrDqmQ4X3q9uuipQwSGrUn7oGiemKjtSLDhNtQHzMHr1JdQ==} - engines: {node: '>=6.9.0'} - peerDependencies: - '@babel/core': ^7.0.0-0 - dependencies: - '@babel/core': 7.21.8 - '@babel/helper-plugin-utils': 7.21.5 - dev: false + sass-embedded@1.83.1: + resolution: {integrity: sha512-LdKG6nxLEzpXbMUt0if12PhUNonGvy91n7IWHOZRZjvA6AWm9oVdhpO+KEXN/Sc+jjGvQeQcav9+Z8DwmII/pA==} + engines: {node: '>=16.0.0'} + hasBin: true - /@babel/plugin-syntax-import-meta@7.10.4(@babel/core@7.21.8): - resolution: {integrity: sha512-Yqfm+XDx0+Prh3VSeEQCPU81yC+JWZ2pDPFSS4ZdpfZhp4MkFMaDC1UqseovEKwSUpnIL7+vK+Clp7bfh0iD7g==} - peerDependencies: - '@babel/core': ^7.0.0-0 - dependencies: - '@babel/core': 7.21.8 - '@babel/helper-plugin-utils': 7.21.5 - dev: false + sax@1.4.1: + resolution: {integrity: sha512-+aWOz7yVScEGoKNd4PA10LZ8sk0A/z5+nXQG5giUO5rprX9jgYsTdov9qCchZiPIZezbZH+jRut8nPodFAX4Jg==} - /@babel/plugin-syntax-json-strings@7.8.3(@babel/core@7.21.8): - resolution: {integrity: sha512-lY6kdGpWHvjoe2vk4WrAapEuBR69EMxZl+RoGRhrFGNYVK8mOPAW8VfbT/ZgrFbXlDNiiaxQnAtgVCZ6jv30EA==} - peerDependencies: - '@babel/core': ^7.0.0-0 - dependencies: - '@babel/core': 7.21.8 - '@babel/helper-plugin-utils': 7.21.5 - dev: false + section-matter@1.0.0: + resolution: {integrity: sha512-vfD3pmTzGpufjScBh50YHKzEu2lxBWhVEHsNGoEXmCmn2hKGfeNLYMzCJpe8cD7gqX7TJluOVpBkAequ6dgMmA==} + engines: {node: '>=4'} - /@babel/plugin-syntax-logical-assignment-operators@7.10.4(@babel/core@7.21.8): - resolution: {integrity: sha512-d8waShlpFDinQ5MtvGU9xDAOzKH47+FFoney2baFIoMr952hKOLp1HR7VszoZvOsV/4+RRszNY7D17ba0te0ig==} - peerDependencies: - '@babel/core': ^7.0.0-0 - dependencies: - '@babel/core': 7.21.8 - '@babel/helper-plugin-utils': 7.21.5 - dev: false + semver@6.3.1: + resolution: {integrity: sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==} + hasBin: true - /@babel/plugin-syntax-nullish-coalescing-operator@7.8.3(@babel/core@7.21.8): - resolution: {integrity: sha512-aSff4zPII1u2QD7y+F8oDsz19ew4IGEJg9SVW+bqwpwtfFleiQDMdzA/R+UlWDzfnHFCxxleFT0PMIrR36XLNQ==} - peerDependencies: - '@babel/core': ^7.0.0-0 - dependencies: - '@babel/core': 7.21.8 - '@babel/helper-plugin-utils': 7.21.5 - dev: false + semver@7.6.3: + resolution: {integrity: sha512-oVekP1cKtI+CTDvHWYFUcMtsK/00wmAEfyqKfNdARm8u1wNVhSgaX7A8d4UuIlUI5e84iEwOhs7ZPYRmzU9U6A==} + engines: {node: '>=10'} + hasBin: true - /@babel/plugin-syntax-numeric-separator@7.10.4(@babel/core@7.21.8): - resolution: {integrity: sha512-9H6YdfkcK/uOnY/K7/aA2xpzaAgkQn37yzWUMRK7OaPOqOpGS1+n0H5hxT9AUw9EsSjPW8SVyMJwYRtWs3X3ug==} - peerDependencies: - '@babel/core': ^7.0.0-0 - dependencies: - '@babel/core': 7.21.8 - '@babel/helper-plugin-utils': 7.21.5 - dev: false + set-blocking@2.0.0: + resolution: {integrity: sha512-KiKBS8AnWGEyLzofFfmvKwpdPzqiy16LvQfK3yv/fVH7Bj13/wl3JSR1J+rfgRE9q7xUJK4qvgS8raSOeLUehw==} - /@babel/plugin-syntax-object-rest-spread@7.8.3(@babel/core@7.21.8): - resolution: {integrity: sha512-XoqMijGZb9y3y2XskN+P1wUGiVwWZ5JmoDRwx5+3GmEplNyVM2s2Dg8ILFQm8rWM48orGy5YpI5Bl8U1y7ydlA==} - peerDependencies: - '@babel/core': ^7.0.0-0 - dependencies: - '@babel/core': 7.21.8 - '@babel/helper-plugin-utils': 7.21.5 - dev: false + shebang-command@2.0.0: + resolution: {integrity: sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA==} + engines: {node: '>=8'} - /@babel/plugin-syntax-optional-catch-binding@7.8.3(@babel/core@7.21.8): - resolution: {integrity: sha512-6VPD0Pc1lpTqw0aKoeRTMiB+kWhAoT24PA+ksWSBrFtl5SIRVpZlwN3NNPQjehA2E/91FV3RjLWoVTglWcSV3Q==} - peerDependencies: - '@babel/core': ^7.0.0-0 - dependencies: - '@babel/core': 7.21.8 - '@babel/helper-plugin-utils': 7.21.5 - dev: false + shebang-regex@3.0.0: + resolution: {integrity: sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A==} + engines: {node: '>=8'} - /@babel/plugin-syntax-optional-chaining@7.8.3(@babel/core@7.21.8): - resolution: {integrity: sha512-KoK9ErH1MBlCPxV0VANkXW2/dw4vlbGDrFgz8bmUsBGYkFRcbRwMh6cIJubdPrkxRwuGdtCk0v/wPTKbQgBjkg==} - peerDependencies: - '@babel/core': ^7.0.0-0 - dependencies: - '@babel/core': 7.21.8 - '@babel/helper-plugin-utils': 7.21.5 - dev: false + shiki@1.26.2: + resolution: {integrity: sha512-iP7u2NA9A6JwRRCkIUREEX2cMhlYV5EBmbbSlfSRvPThwca8HBRbVkWuNWW+kw9+i6BSUZqqG6YeUs5dC2SjZw==} - /@babel/plugin-syntax-private-property-in-object@7.14.5(@babel/core@7.21.8): - resolution: {integrity: sha512-0wVnp9dxJ72ZUJDV27ZfbSj6iHLoytYZmh3rFcxNnvsJF3ktkzLDZPy/mA17HGsaQT3/DQsWYX1f1QGWkCoVUg==} - engines: {node: '>=6.9.0'} - peerDependencies: - '@babel/core': ^7.0.0-0 - dependencies: - '@babel/core': 7.21.8 - '@babel/helper-plugin-utils': 7.21.5 - dev: false + signal-exit@3.0.7: + resolution: {integrity: sha512-wnD2ZE+l+SPC/uoS0vXeE9L1+0wuaMqKlfz9AMUo38JsyLSBWSFcHR1Rri62LZc12vLr1gb3jl7iwQhgwpAbGQ==} - /@babel/plugin-syntax-top-level-await@7.14.5(@babel/core@7.21.8): - resolution: {integrity: sha512-hx++upLv5U1rgYfwe1xBQUhRmU41NEvpUvrp8jkrSCdvGSnM5/qdRMtylJ6PG5OFkBaHkbTAKTnd3/YyESRHFw==} - engines: {node: '>=6.9.0'} - peerDependencies: - '@babel/core': ^7.0.0-0 - dependencies: - '@babel/core': 7.21.8 - '@babel/helper-plugin-utils': 7.21.5 - dev: false + signal-exit@4.1.0: + resolution: {integrity: sha512-bzyZ1e88w9O1iNJbKnOlvYTrWPDl46O1bG0D3XInv+9tkPrxrN8jUUTiFlDkkmKWgn1M6CfIA13SuGqOa9Korw==} + engines: {node: '>=14'} - /@babel/plugin-transform-arrow-functions@7.21.5(@babel/core@7.21.8): - resolution: {integrity: sha512-wb1mhwGOCaXHDTcsRYMKF9e5bbMgqwxtqa2Y1ifH96dXJPwbuLX9qHy3clhrxVqgMz7nyNXs8VkxdH8UBcjKqA==} - engines: {node: '>=6.9.0'} - peerDependencies: - '@babel/core': ^7.0.0-0 - dependencies: - '@babel/core': 7.21.8 - '@babel/helper-plugin-utils': 7.21.5 - dev: false + sitemap@8.0.0: + resolution: {integrity: sha512-+AbdxhM9kJsHtruUF39bwS/B0Fytw6Fr1o4ZAIAEqA6cke2xcoO2GleBw9Zw7nRzILVEgz7zBM5GiTJjie1G9A==} + engines: {node: '>=14.0.0', npm: '>=6.0.0'} + hasBin: true - /@babel/plugin-transform-async-to-generator@7.20.7(@babel/core@7.21.8): - resolution: {integrity: sha512-Uo5gwHPT9vgnSXQxqGtpdufUiWp96gk7yiP4Mp5bm1QMkEmLXBO7PAGYbKoJ6DhAwiNkcHFBol/x5zZZkL/t0Q==} - engines: {node: '>=6.9.0'} - peerDependencies: - '@babel/core': ^7.0.0-0 - dependencies: - '@babel/core': 7.21.8 - '@babel/helper-module-imports': 7.21.4 - '@babel/helper-plugin-utils': 7.21.5 - '@babel/helper-remap-async-to-generator': 7.18.9(@babel/core@7.21.8) - transitivePeerDependencies: - - supports-color - dev: false + slash@5.1.0: + resolution: {integrity: sha512-ZA6oR3T/pEyuqwMgAKT0/hAv8oAXckzbkmR0UkUosQ+Mc4RxGoJkRmwHgHufaenlyAgE1Mxgpdcrf75y6XcnDg==} + engines: {node: '>=14.16'} - /@babel/plugin-transform-block-scoped-functions@7.18.6(@babel/core@7.21.8): - resolution: {integrity: sha512-ExUcOqpPWnliRcPqves5HJcJOvHvIIWfuS4sroBUenPuMdmW+SMHDakmtS7qOo13sVppmUijqeTv7qqGsvURpQ==} - engines: {node: '>=6.9.0'} - peerDependencies: - '@babel/core': ^7.0.0-0 - dependencies: - '@babel/core': 7.21.8 - '@babel/helper-plugin-utils': 7.21.5 - dev: false + smart-buffer@4.2.0: + resolution: {integrity: sha512-94hK0Hh8rPqQl2xXc3HsaBoOXKV20MToPkcXvwbISWLEs+64sBq5kFgn2kJDHb1Pry9yrP0dxrCI9RRci7RXKg==} + engines: {node: '>= 6.0.0', npm: '>= 3.0.0'} - /@babel/plugin-transform-block-scoping@7.21.0(@babel/core@7.21.8): - resolution: {integrity: sha512-Mdrbunoh9SxwFZapeHVrwFmri16+oYotcZysSzhNIVDwIAb1UV+kvnxULSYq9J3/q5MDG+4X6w8QVgD1zhBXNQ==} - engines: {node: '>=6.9.0'} - peerDependencies: - '@babel/core': ^7.0.0-0 - dependencies: - '@babel/core': 7.21.8 - '@babel/helper-plugin-utils': 7.21.5 - dev: false + socks-proxy-agent@8.0.5: + resolution: {integrity: sha512-HehCEsotFqbPW9sJ8WVYB6UbmIMv7kUUORIF2Nncq4VQvBfNBLibW9YZR5dlYCSUhwcD628pRllm7n+E+YTzJw==} + engines: {node: '>= 14'} - /@babel/plugin-transform-classes@7.21.0(@babel/core@7.21.8): - resolution: {integrity: sha512-RZhbYTCEUAe6ntPehC4hlslPWosNHDox+vAs4On/mCLRLfoDVHf6hVEd7kuxr1RnHwJmxFfUM3cZiZRmPxJPXQ==} - engines: {node: '>=6.9.0'} - peerDependencies: - '@babel/core': ^7.0.0-0 - dependencies: - '@babel/core': 7.21.8 - '@babel/helper-annotate-as-pure': 7.18.6 - '@babel/helper-compilation-targets': 7.21.5(@babel/core@7.21.8) - '@babel/helper-environment-visitor': 7.21.5 - '@babel/helper-function-name': 7.21.0 - '@babel/helper-optimise-call-expression': 7.18.6 - '@babel/helper-plugin-utils': 7.21.5 - '@babel/helper-replace-supers': 7.21.5 - '@babel/helper-split-export-declaration': 7.18.6 - globals: 11.12.0 - transitivePeerDependencies: - - supports-color - dev: false + socks@2.8.3: + resolution: {integrity: sha512-l5x7VUUWbjVFbafGLxPWkYsHIhEvmF85tbIeFZWc8ZPtoMyybuEhL7Jye/ooC4/d48FgOjSJXgsF/AJPYCW8Zw==} + engines: {node: '>= 10.0.0', npm: '>= 3.0.0'} - /@babel/plugin-transform-computed-properties@7.21.5(@babel/core@7.21.8): - resolution: {integrity: sha512-TR653Ki3pAwxBxUe8srfF3e4Pe3FTA46uaNHYyQwIoM4oWKSoOZiDNyHJ0oIoDIUPSRQbQG7jzgVBX3FPVne1Q==} - engines: {node: '>=6.9.0'} - peerDependencies: - '@babel/core': ^7.0.0-0 - dependencies: - '@babel/core': 7.21.8 - '@babel/helper-plugin-utils': 7.21.5 - '@babel/template': 7.20.7 - dev: false + source-map-js@1.2.1: + resolution: {integrity: sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA==} + engines: {node: '>=0.10.0'} - /@babel/plugin-transform-destructuring@7.21.3(@babel/core@7.21.8): - resolution: {integrity: sha512-bp6hwMFzuiE4HqYEyoGJ/V2LeIWn+hLVKc4pnj++E5XQptwhtcGmSayM029d/j2X1bPKGTlsyPwAubuU22KhMA==} - engines: {node: '>=6.9.0'} - peerDependencies: - '@babel/core': ^7.0.0-0 - dependencies: - '@babel/core': 7.21.8 - '@babel/helper-plugin-utils': 7.21.5 - dev: false + space-separated-tokens@2.0.2: + resolution: {integrity: sha512-PEGlAwrG8yXGXRjW32fGbg66JAlOAwbObuqVoJpv/mRgoWDQfgH1wDPvtzWyUSNAXBGSk8h755YDbbcEy3SH2Q==} - /@babel/plugin-transform-dotall-regex@7.18.6(@babel/core@7.21.8): - resolution: {integrity: sha512-6S3jpun1eEbAxq7TdjLotAsl4WpQI9DxfkycRcKrjhQYzU87qpXdknpBg/e+TdcMehqGnLFi7tnFUBR02Vq6wg==} - engines: {node: '>=6.9.0'} - peerDependencies: - '@babel/core': ^7.0.0-0 - dependencies: - '@babel/core': 7.21.8 - '@babel/helper-create-regexp-features-plugin': 7.21.8(@babel/core@7.21.8) - '@babel/helper-plugin-utils': 7.21.5 - dev: false + speakingurl@14.0.1: + resolution: {integrity: sha512-1POYv7uv2gXoyGFpBCmpDVSNV74IfsWlDW216UPjbWufNf+bSU6GdbDsxdcxtfwb4xlI3yxzOTKClUosxARYrQ==} + engines: {node: '>=0.10.0'} - /@babel/plugin-transform-duplicate-keys@7.18.9(@babel/core@7.21.8): - resolution: {integrity: sha512-d2bmXCtZXYc59/0SanQKbiWINadaJXqtvIQIzd4+hNwkWBgyCd5F/2t1kXoUdvPMrxzPvhK6EMQRROxsue+mfw==} - engines: {node: '>=6.9.0'} - peerDependencies: - '@babel/core': ^7.0.0-0 - dependencies: - '@babel/core': 7.21.8 - '@babel/helper-plugin-utils': 7.21.5 - dev: false + speech-rule-engine@4.0.7: + resolution: {integrity: sha512-sJrL3/wHzNwJRLBdf6CjJWIlxC04iYKkyXvYSVsWVOiC2DSkHmxsqOhEeMsBA9XK+CHuNcsdkbFDnoUfAsmp9g==} + hasBin: true - /@babel/plugin-transform-exponentiation-operator@7.18.6(@babel/core@7.21.8): - resolution: {integrity: sha512-wzEtc0+2c88FVR34aQmiz56dxEkxr2g8DQb/KfaFa1JYXOFVsbhvAonFN6PwVWj++fKmku8NP80plJ5Et4wqHw==} - engines: {node: '>=6.9.0'} - peerDependencies: - '@babel/core': ^7.0.0-0 - dependencies: - '@babel/core': 7.21.8 - '@babel/helper-builder-binary-assignment-operator-visitor': 7.21.5 - '@babel/helper-plugin-utils': 7.21.5 - dev: false + sprintf-js@1.0.3: + resolution: {integrity: sha512-D9cPgkvLlV3t3IzL0D0YLvGA9Ahk4PcvVwUbN0dSGr1aP0Nrt4AEnTUbuGvquEC0mA64Gqt1fzirlRs5ibXx8g==} - /@babel/plugin-transform-for-of@7.21.5(@babel/core@7.21.8): - resolution: {integrity: sha512-nYWpjKW/7j/I/mZkGVgHJXh4bA1sfdFnJoOXwJuj4m3Q2EraO/8ZyrkCau9P5tbHQk01RMSt6KYLCsW7730SXQ==} - engines: {node: '>=6.9.0'} - peerDependencies: - '@babel/core': ^7.0.0-0 - dependencies: - '@babel/core': 7.21.8 - '@babel/helper-plugin-utils': 7.21.5 - dev: false + sprintf-js@1.1.3: + resolution: {integrity: sha512-Oo+0REFV59/rz3gfJNKQiBlwfHaSESl1pcGyABQsnnIfWOFt6JNj5gCog2U6MLZ//IGYD+nA8nI+mTShREReaA==} - /@babel/plugin-transform-function-name@7.18.9(@babel/core@7.21.8): - resolution: {integrity: sha512-WvIBoRPaJQ5yVHzcnJFor7oS5Ls0PYixlTYE63lCj2RtdQEl15M68FXQlxnG6wdraJIXRdR7KI+hQ7q/9QjrCQ==} - engines: {node: '>=6.9.0'} - peerDependencies: - '@babel/core': ^7.0.0-0 - dependencies: - '@babel/core': 7.21.8 - '@babel/helper-compilation-targets': 7.21.5(@babel/core@7.21.8) - '@babel/helper-function-name': 7.21.0 - '@babel/helper-plugin-utils': 7.21.5 - dev: false + ssri@10.0.6: + resolution: {integrity: sha512-MGrFH9Z4NP9Iyhqn16sDtBpRRNJ0Y2hNa6D65h736fVSaPCHr4DM4sWUNvVaSuC+0OBGhwsrydQwmgfg5LncqQ==} + engines: {node: ^14.17.0 || ^16.13.0 || >=18.0.0} - /@babel/plugin-transform-literals@7.18.9(@babel/core@7.21.8): - resolution: {integrity: sha512-IFQDSRoTPnrAIrI5zoZv73IFeZu2dhu6irxQjY9rNjTT53VmKg9fenjvoiOWOkJ6mm4jKVPtdMzBY98Fp4Z4cg==} - engines: {node: '>=6.9.0'} - peerDependencies: - '@babel/core': ^7.0.0-0 - dependencies: - '@babel/core': 7.21.8 - '@babel/helper-plugin-utils': 7.21.5 - dev: false + stdin-discarder@0.2.2: + resolution: {integrity: sha512-UhDfHmA92YAlNnCfhmq0VeNL5bDbiZGg7sZ2IvPsXubGkiNa9EC+tUTsjBRsYUAz87btI6/1wf4XoVvQ3uRnmQ==} + engines: {node: '>=18'} - /@babel/plugin-transform-member-expression-literals@7.18.6(@babel/core@7.21.8): - resolution: {integrity: sha512-qSF1ihLGO3q+/g48k85tUjD033C29TNTVB2paCwZPVmOsjn9pClvYYrM2VeJpBY2bcNkuny0YUyTNRyRxJ54KA==} - engines: {node: '>=6.9.0'} - peerDependencies: - '@babel/core': ^7.0.0-0 - dependencies: - '@babel/core': 7.21.8 - '@babel/helper-plugin-utils': 7.21.5 - dev: false + string-width@4.2.3: + resolution: {integrity: sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==} + engines: {node: '>=8'} - /@babel/plugin-transform-modules-amd@7.20.11(@babel/core@7.21.8): - resolution: {integrity: sha512-NuzCt5IIYOW0O30UvqktzHYR2ud5bOWbY0yaxWZ6G+aFzOMJvrs5YHNikrbdaT15+KNO31nPOy5Fim3ku6Zb5g==} - engines: {node: '>=6.9.0'} - peerDependencies: - '@babel/core': ^7.0.0-0 - dependencies: - '@babel/core': 7.21.8 - '@babel/helper-module-transforms': 7.21.5 - '@babel/helper-plugin-utils': 7.21.5 - transitivePeerDependencies: - - supports-color - dev: false + string-width@5.1.2: + resolution: {integrity: sha512-HnLOCR3vjcY8beoNLtcjZ5/nxn2afmME6lhrDrebokqMap+XbeW8n9TXpPDOqdGK5qcI3oT0GKTW6wC7EMiVqA==} + engines: {node: '>=12'} - /@babel/plugin-transform-modules-commonjs@7.21.5(@babel/core@7.21.8): - resolution: {integrity: sha512-OVryBEgKUbtqMoB7eG2rs6UFexJi6Zj6FDXx+esBLPTCxCNxAY9o+8Di7IsUGJ+AVhp5ncK0fxWUBd0/1gPhrQ==} - engines: {node: '>=6.9.0'} - peerDependencies: - '@babel/core': ^7.0.0-0 - dependencies: - '@babel/core': 7.21.8 - '@babel/helper-module-transforms': 7.21.5 - '@babel/helper-plugin-utils': 7.21.5 - '@babel/helper-simple-access': 7.21.5 - transitivePeerDependencies: - - supports-color - dev: false + string-width@7.2.0: + resolution: {integrity: sha512-tsaTIkKW9b4N+AEj+SVA+WhJzV7/zMhcSu78mLKWSk7cXMOSHsBKFWUs0fWwq8QyK3MgJBQRX6Gbi4kYbdvGkQ==} + engines: {node: '>=18'} - /@babel/plugin-transform-modules-systemjs@7.20.11(@babel/core@7.21.8): - resolution: {integrity: sha512-vVu5g9BPQKSFEmvt2TA4Da5N+QVS66EX21d8uoOihC+OCpUoGvzVsXeqFdtAEfVa5BILAeFt+U7yVmLbQnAJmw==} - engines: {node: '>=6.9.0'} - peerDependencies: - '@babel/core': ^7.0.0-0 - dependencies: - '@babel/core': 7.21.8 - '@babel/helper-hoist-variables': 7.18.6 - '@babel/helper-module-transforms': 7.21.5 - '@babel/helper-plugin-utils': 7.21.5 - '@babel/helper-validator-identifier': 7.19.1 - transitivePeerDependencies: - - supports-color - dev: false + string_decoder@1.3.0: + resolution: {integrity: sha512-hkRX8U1WjJFd8LsDJ2yQ/wWWxaopEsABU1XfkM8A+j0+85JAGppt16cr1Whg6KIbb4okU6Mql6BOj+uup/wKeA==} - /@babel/plugin-transform-modules-umd@7.18.6(@babel/core@7.21.8): - resolution: {integrity: sha512-dcegErExVeXcRqNtkRU/z8WlBLnvD4MRnHgNs3MytRO1Mn1sHRyhbcpYbVMGclAqOjdW+9cfkdZno9dFdfKLfQ==} - engines: {node: '>=6.9.0'} - peerDependencies: - '@babel/core': ^7.0.0-0 - dependencies: - '@babel/core': 7.21.8 - '@babel/helper-module-transforms': 7.21.5 - '@babel/helper-plugin-utils': 7.21.5 - transitivePeerDependencies: - - supports-color - dev: false + stringify-entities@4.0.4: + resolution: {integrity: sha512-IwfBptatlO+QCJUo19AqvrPNqlVMpW9YEL2LIVY+Rpv2qsjCGxaDLNRgeGsQWJhfItebuJhsGSLjaBbNSQ+ieg==} - /@babel/plugin-transform-named-capturing-groups-regex@7.20.5(@babel/core@7.21.8): - resolution: {integrity: sha512-mOW4tTzi5iTLnw+78iEq3gr8Aoq4WNRGpmSlrogqaiCBoR1HFhpU4JkpQFOHfeYx3ReVIFWOQJS4aZBRvuZ6mA==} - engines: {node: '>=6.9.0'} - peerDependencies: - '@babel/core': ^7.0.0 - dependencies: - '@babel/core': 7.21.8 - '@babel/helper-create-regexp-features-plugin': 7.21.8(@babel/core@7.21.8) - '@babel/helper-plugin-utils': 7.21.5 - dev: false + strip-ansi@6.0.1: + resolution: {integrity: sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==} + engines: {node: '>=8'} - /@babel/plugin-transform-new-target@7.18.6(@babel/core@7.21.8): - resolution: {integrity: sha512-DjwFA/9Iu3Z+vrAn+8pBUGcjhxKguSMlsFqeCKbhb9BAV756v0krzVK04CRDi/4aqmk8BsHb4a/gFcaA5joXRw==} - engines: {node: '>=6.9.0'} - peerDependencies: - '@babel/core': ^7.0.0-0 - dependencies: - '@babel/core': 7.21.8 - '@babel/helper-plugin-utils': 7.21.5 - dev: false + strip-ansi@7.1.0: + resolution: {integrity: sha512-iq6eVVI64nQQTRYq2KtEg2d2uU7LElhTJwsH4YzIHZshxlgZms/wIc4VoDQTlG/IvVIrBKG06CrZnp0qv7hkcQ==} + engines: {node: '>=12'} - /@babel/plugin-transform-object-super@7.18.6(@babel/core@7.21.8): - resolution: {integrity: sha512-uvGz6zk+pZoS1aTZrOvrbj6Pp/kK2mp45t2B+bTDre2UgsZZ8EZLSJtUg7m/no0zOJUWgFONpB7Zv9W2tSaFlA==} - engines: {node: '>=6.9.0'} - peerDependencies: - '@babel/core': ^7.0.0-0 - dependencies: - '@babel/core': 7.21.8 - '@babel/helper-plugin-utils': 7.21.5 - '@babel/helper-replace-supers': 7.21.5 - transitivePeerDependencies: - - supports-color - dev: false + strip-bom-string@1.0.0: + resolution: {integrity: sha512-uCC2VHvQRYu+lMh4My/sFNmF2klFymLX1wHJeXnbEJERpV/ZsVuonzerjfrGpIGF7LBVa1O7i9kjiWvJiFck8g==} + engines: {node: '>=0.10.0'} - /@babel/plugin-transform-parameters@7.21.3(@babel/core@7.21.8): - resolution: {integrity: sha512-Wxc+TvppQG9xWFYatvCGPvZ6+SIUxQ2ZdiBP+PHYMIjnPXD+uThCshaz4NZOnODAtBjjcVQQ/3OKs9LW28purQ==} - engines: {node: '>=6.9.0'} - peerDependencies: - '@babel/core': ^7.0.0-0 - dependencies: - '@babel/core': 7.21.8 - '@babel/helper-plugin-utils': 7.21.5 - dev: false + strip-final-newline@4.0.0: + resolution: {integrity: sha512-aulFJcD6YK8V1G7iRB5tigAP4TsHBZZrOV8pjV++zdUwmeV8uzbY7yn6h9MswN62adStNZFuCIx4haBnRuMDaw==} + engines: {node: '>=18'} - /@babel/plugin-transform-property-literals@7.18.6(@babel/core@7.21.8): - resolution: {integrity: sha512-cYcs6qlgafTud3PAzrrRNbQtfpQ8+y/+M5tKmksS9+M1ckbH6kzY8MrexEM9mcA6JDsukE19iIRvAyYl463sMg==} - engines: {node: '>=6.9.0'} - peerDependencies: - '@babel/core': ^7.0.0-0 - dependencies: - '@babel/core': 7.21.8 - '@babel/helper-plugin-utils': 7.21.5 - dev: false + superjson@2.2.2: + resolution: {integrity: sha512-5JRxVqC8I8NuOUjzBbvVJAKNM8qoVuH0O77h4WInc/qC2q5IreqKxYwgkga3PfA22OayK2ikceb/B26dztPl+Q==} + engines: {node: '>=16'} - /@babel/plugin-transform-regenerator@7.21.5(@babel/core@7.21.8): - resolution: {integrity: sha512-ZoYBKDb6LyMi5yCsByQ5jmXsHAQDDYeexT1Szvlmui+lADvfSecr5Dxd/PkrTC3pAD182Fcju1VQkB4oCp9M+w==} - engines: {node: '>=6.9.0'} - peerDependencies: - '@babel/core': ^7.0.0-0 - dependencies: - '@babel/core': 7.21.8 - '@babel/helper-plugin-utils': 7.21.5 - regenerator-transform: 0.15.1 - dev: false + supports-color@8.1.1: + resolution: {integrity: sha512-MpUEN2OodtUzxvKQl72cUF7RQ5EiHsGvSsVG0ia9c5RbWGL2CI4C7EpPS8UTBIplnlzZiNuV56w+FuNxy3ty2Q==} + engines: {node: '>=10'} - /@babel/plugin-transform-reserved-words@7.18.6(@babel/core@7.21.8): - resolution: {integrity: sha512-oX/4MyMoypzHjFrT1CdivfKZ+XvIPMFXwwxHp/r0Ddy2Vuomt4HDFGmft1TAY2yiTKiNSsh3kjBAzcM8kSdsjA==} - engines: {node: '>=6.9.0'} - peerDependencies: - '@babel/core': ^7.0.0-0 - dependencies: - '@babel/core': 7.21.8 - '@babel/helper-plugin-utils': 7.21.5 - dev: false + sync-child-process@1.0.2: + resolution: {integrity: sha512-8lD+t2KrrScJ/7KXCSyfhT3/hRq78rC0wBFqNJXv3mZyn6hW2ypM05JmlSvtqRbeq6jqA94oHbxAr2vYsJ8vDA==} + engines: {node: '>=16.0.0'} - /@babel/plugin-transform-shorthand-properties@7.18.6(@babel/core@7.21.8): - resolution: {integrity: sha512-eCLXXJqv8okzg86ywZJbRn19YJHU4XUa55oz2wbHhaQVn/MM+XhukiT7SYqp/7o00dg52Rj51Ny+Ecw4oyoygw==} - engines: {node: '>=6.9.0'} - peerDependencies: - '@babel/core': ^7.0.0-0 - dependencies: - '@babel/core': 7.21.8 - '@babel/helper-plugin-utils': 7.21.5 - dev: false + sync-message-port@1.1.3: + resolution: {integrity: sha512-GTt8rSKje5FilG+wEdfCkOcLL7LWqpMlr2c3LRuKt/YXxcJ52aGSbGBAdI4L3aaqfrBt6y711El53ItyH1NWzg==} + engines: {node: '>=16.0.0'} - /@babel/plugin-transform-spread@7.20.7(@babel/core@7.21.8): - resolution: {integrity: sha512-ewBbHQ+1U/VnH1fxltbJqDeWBU1oNLG8Dj11uIv3xVf7nrQu0bPGe5Rf716r7K5Qz+SqtAOVswoVunoiBtGhxw==} - engines: {node: '>=6.9.0'} - peerDependencies: - '@babel/core': ^7.0.0-0 - dependencies: - '@babel/core': 7.21.8 - '@babel/helper-plugin-utils': 7.21.5 - '@babel/helper-skip-transparent-expression-wrappers': 7.20.0 - dev: false + tar@6.2.1: + resolution: {integrity: sha512-DZ4yORTwrbTj/7MZYq2w+/ZFdI6OZ/f9SFHR+71gIVUZhOQPHzVCLpvRnPgyaMpfWxxk/4ONva3GQSyNIKRv6A==} + engines: {node: '>=10'} - /@babel/plugin-transform-sticky-regex@7.18.6(@babel/core@7.21.8): - resolution: {integrity: sha512-kfiDrDQ+PBsQDO85yj1icueWMfGfJFKN1KCkndygtu/C9+XUfydLC8Iv5UYJqRwy4zk8EcplRxEOeLyjq1gm6Q==} - engines: {node: '>=6.9.0'} - peerDependencies: - '@babel/core': ^7.0.0-0 - dependencies: - '@babel/core': 7.21.8 - '@babel/helper-plugin-utils': 7.21.5 - dev: false + to-regex-range@5.0.1: + resolution: {integrity: sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ==} + engines: {node: '>=8.0'} - /@babel/plugin-transform-template-literals@7.18.9(@babel/core@7.21.8): - resolution: {integrity: sha512-S8cOWfT82gTezpYOiVaGHrCbhlHgKhQt8XH5ES46P2XWmX92yisoZywf5km75wv5sYcXDUCLMmMxOLCtthDgMA==} - engines: {node: '>=6.9.0'} - peerDependencies: - '@babel/core': ^7.0.0-0 - dependencies: - '@babel/core': 7.21.8 - '@babel/helper-plugin-utils': 7.21.5 - dev: false + tr46@0.0.3: + resolution: {integrity: sha512-N3WMsuqV66lT30CrXNbEjx4GEwlow3v6rr4mCcv6prnfwhS01rkgyFdjPNBYd9br7LpXV1+Emh01fHnq2Gdgrw==} - /@babel/plugin-transform-typeof-symbol@7.18.9(@babel/core@7.21.8): - resolution: {integrity: sha512-SRfwTtF11G2aemAZWivL7PD+C9z52v9EvMqH9BuYbabyPuKUvSWks3oCg6041pT925L4zVFqaVBeECwsmlguEw==} - engines: {node: '>=6.9.0'} - peerDependencies: - '@babel/core': ^7.0.0-0 - dependencies: - '@babel/core': 7.21.8 - '@babel/helper-plugin-utils': 7.21.5 - dev: false + trim-lines@3.0.1: + resolution: {integrity: sha512-kRj8B+YHZCc9kQYdWfJB2/oUl9rA99qbowYYBtr4ui4mZyAQ2JpvVBd/6U2YloATfqBhBTSMhTpgBHtU0Mf3Rg==} - /@babel/plugin-transform-unicode-escapes@7.21.5(@babel/core@7.21.8): - resolution: {integrity: sha512-LYm/gTOwZqsYohlvFUe/8Tujz75LqqVC2w+2qPHLR+WyWHGCZPN1KBpJCJn+4Bk4gOkQy/IXKIge6az5MqwlOg==} - engines: {node: '>=6.9.0'} - peerDependencies: - '@babel/core': ^7.0.0-0 - dependencies: - '@babel/core': 7.21.8 - '@babel/helper-plugin-utils': 7.21.5 - dev: false + tslib@2.8.1: + resolution: {integrity: sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==} - /@babel/plugin-transform-unicode-regex@7.18.6(@babel/core@7.21.8): - resolution: {integrity: sha512-gE7A6Lt7YLnNOL3Pb9BNeZvi+d8l7tcRrG4+pwJjK9hD2xX4mEvjlQW60G9EEmfXVYRPv9VRQcyegIVHCql/AA==} - engines: {node: '>=6.9.0'} - peerDependencies: - '@babel/core': ^7.0.0-0 - dependencies: - '@babel/core': 7.21.8 - '@babel/helper-create-regexp-features-plugin': 7.21.8(@babel/core@7.21.8) - '@babel/helper-plugin-utils': 7.21.5 - dev: false + uc.micro@2.1.0: + resolution: {integrity: sha512-ARDJmphmdvUk6Glw7y9DQ2bFkKBHwQHLi2lsaH6PPmz/Ka9sFOBsBluozhDltWmnv9u/cF6Rt87znRTPV+yp/A==} - /@babel/preset-env@7.21.5(@babel/core@7.21.8): - resolution: {integrity: sha512-wH00QnTTldTbf/IefEVyChtRdw5RJvODT/Vb4Vcxq1AZvtXj6T0YeX0cAcXhI6/BdGuiP3GcNIL4OQbI2DVNxg==} - engines: {node: '>=6.9.0'} - peerDependencies: - '@babel/core': ^7.0.0-0 - dependencies: - '@babel/compat-data': 7.21.7 - '@babel/core': 7.21.8 - '@babel/helper-compilation-targets': 7.21.5(@babel/core@7.21.8) - '@babel/helper-plugin-utils': 7.21.5 - '@babel/helper-validator-option': 7.21.0 - '@babel/plugin-bugfix-safari-id-destructuring-collision-in-function-expression': 7.18.6(@babel/core@7.21.8) - '@babel/plugin-bugfix-v8-spread-parameters-in-optional-chaining': 7.20.7(@babel/core@7.21.8) - '@babel/plugin-proposal-async-generator-functions': 7.20.7(@babel/core@7.21.8) - '@babel/plugin-proposal-class-properties': 7.18.6(@babel/core@7.21.8) - '@babel/plugin-proposal-class-static-block': 7.21.0(@babel/core@7.21.8) - '@babel/plugin-proposal-dynamic-import': 7.18.6(@babel/core@7.21.8) - '@babel/plugin-proposal-export-namespace-from': 7.18.9(@babel/core@7.21.8) - '@babel/plugin-proposal-json-strings': 7.18.6(@babel/core@7.21.8) - '@babel/plugin-proposal-logical-assignment-operators': 7.20.7(@babel/core@7.21.8) - '@babel/plugin-proposal-nullish-coalescing-operator': 7.18.6(@babel/core@7.21.8) - '@babel/plugin-proposal-numeric-separator': 7.18.6(@babel/core@7.21.8) - '@babel/plugin-proposal-object-rest-spread': 7.20.7(@babel/core@7.21.8) - '@babel/plugin-proposal-optional-catch-binding': 7.18.6(@babel/core@7.21.8) - '@babel/plugin-proposal-optional-chaining': 7.21.0(@babel/core@7.21.8) - '@babel/plugin-proposal-private-methods': 7.18.6(@babel/core@7.21.8) - '@babel/plugin-proposal-private-property-in-object': 7.21.0(@babel/core@7.21.8) - '@babel/plugin-proposal-unicode-property-regex': 7.18.6(@babel/core@7.21.8) - '@babel/plugin-syntax-async-generators': 7.8.4(@babel/core@7.21.8) - '@babel/plugin-syntax-class-properties': 7.12.13(@babel/core@7.21.8) - '@babel/plugin-syntax-class-static-block': 7.14.5(@babel/core@7.21.8) - '@babel/plugin-syntax-dynamic-import': 7.8.3(@babel/core@7.21.8) - '@babel/plugin-syntax-export-namespace-from': 7.8.3(@babel/core@7.21.8) - '@babel/plugin-syntax-import-assertions': 7.20.0(@babel/core@7.21.8) - '@babel/plugin-syntax-import-meta': 7.10.4(@babel/core@7.21.8) - '@babel/plugin-syntax-json-strings': 7.8.3(@babel/core@7.21.8) - '@babel/plugin-syntax-logical-assignment-operators': 7.10.4(@babel/core@7.21.8) - '@babel/plugin-syntax-nullish-coalescing-operator': 7.8.3(@babel/core@7.21.8) - '@babel/plugin-syntax-numeric-separator': 7.10.4(@babel/core@7.21.8) - '@babel/plugin-syntax-object-rest-spread': 7.8.3(@babel/core@7.21.8) - '@babel/plugin-syntax-optional-catch-binding': 7.8.3(@babel/core@7.21.8) - '@babel/plugin-syntax-optional-chaining': 7.8.3(@babel/core@7.21.8) - '@babel/plugin-syntax-private-property-in-object': 7.14.5(@babel/core@7.21.8) - '@babel/plugin-syntax-top-level-await': 7.14.5(@babel/core@7.21.8) - '@babel/plugin-transform-arrow-functions': 7.21.5(@babel/core@7.21.8) - '@babel/plugin-transform-async-to-generator': 7.20.7(@babel/core@7.21.8) - '@babel/plugin-transform-block-scoped-functions': 7.18.6(@babel/core@7.21.8) - '@babel/plugin-transform-block-scoping': 7.21.0(@babel/core@7.21.8) - '@babel/plugin-transform-classes': 7.21.0(@babel/core@7.21.8) - '@babel/plugin-transform-computed-properties': 7.21.5(@babel/core@7.21.8) - '@babel/plugin-transform-destructuring': 7.21.3(@babel/core@7.21.8) - '@babel/plugin-transform-dotall-regex': 7.18.6(@babel/core@7.21.8) - '@babel/plugin-transform-duplicate-keys': 7.18.9(@babel/core@7.21.8) - '@babel/plugin-transform-exponentiation-operator': 7.18.6(@babel/core@7.21.8) - '@babel/plugin-transform-for-of': 7.21.5(@babel/core@7.21.8) - '@babel/plugin-transform-function-name': 7.18.9(@babel/core@7.21.8) - '@babel/plugin-transform-literals': 7.18.9(@babel/core@7.21.8) - '@babel/plugin-transform-member-expression-literals': 7.18.6(@babel/core@7.21.8) - '@babel/plugin-transform-modules-amd': 7.20.11(@babel/core@7.21.8) - '@babel/plugin-transform-modules-commonjs': 7.21.5(@babel/core@7.21.8) - '@babel/plugin-transform-modules-systemjs': 7.20.11(@babel/core@7.21.8) - '@babel/plugin-transform-modules-umd': 7.18.6(@babel/core@7.21.8) - '@babel/plugin-transform-named-capturing-groups-regex': 7.20.5(@babel/core@7.21.8) - '@babel/plugin-transform-new-target': 7.18.6(@babel/core@7.21.8) - '@babel/plugin-transform-object-super': 7.18.6(@babel/core@7.21.8) - '@babel/plugin-transform-parameters': 7.21.3(@babel/core@7.21.8) - '@babel/plugin-transform-property-literals': 7.18.6(@babel/core@7.21.8) - '@babel/plugin-transform-regenerator': 7.21.5(@babel/core@7.21.8) - '@babel/plugin-transform-reserved-words': 7.18.6(@babel/core@7.21.8) - '@babel/plugin-transform-shorthand-properties': 7.18.6(@babel/core@7.21.8) - '@babel/plugin-transform-spread': 7.20.7(@babel/core@7.21.8) - '@babel/plugin-transform-sticky-regex': 7.18.6(@babel/core@7.21.8) - '@babel/plugin-transform-template-literals': 7.18.9(@babel/core@7.21.8) - '@babel/plugin-transform-typeof-symbol': 7.18.9(@babel/core@7.21.8) - '@babel/plugin-transform-unicode-escapes': 7.21.5(@babel/core@7.21.8) - '@babel/plugin-transform-unicode-regex': 7.18.6(@babel/core@7.21.8) - '@babel/preset-modules': 0.1.5(@babel/core@7.21.8) - '@babel/types': 7.21.5 - babel-plugin-polyfill-corejs2: 0.3.3(@babel/core@7.21.8) - babel-plugin-polyfill-corejs3: 0.6.0(@babel/core@7.21.8) - babel-plugin-polyfill-regenerator: 0.4.1(@babel/core@7.21.8) - core-js-compat: 3.30.2 - semver: 6.3.0 - transitivePeerDependencies: - - supports-color - dev: false + undici-types@6.20.0: + resolution: {integrity: sha512-Ny6QZ2Nju20vw1SRHe3d9jVu6gJ+4e3+MMpqu7pqE5HT6WsTSlce++GQmK5UXS8mzV8DSYHrQH+Xrf2jVcuKNg==} - /@babel/preset-modules@0.1.5(@babel/core@7.21.8): - resolution: {integrity: sha512-A57th6YRG7oR3cq/yt/Y84MvGgE0eJG2F1JLhKuyG+jFxEgrd/HAMJatiFtmOiZurz+0DkrvbheCLaV5f2JfjA==} - peerDependencies: - '@babel/core': ^7.0.0-0 - dependencies: - '@babel/core': 7.21.8 - '@babel/helper-plugin-utils': 7.21.5 - '@babel/plugin-proposal-unicode-property-regex': 7.18.6(@babel/core@7.21.8) - '@babel/plugin-transform-dotall-regex': 7.18.6(@babel/core@7.21.8) - '@babel/types': 7.21.5 - esutils: 2.0.3 - dev: false - - /@babel/regjsgen@0.8.0: - resolution: {integrity: sha512-x/rqGMdzj+fWZvCOYForTghzbtqPDZ5gPwaoNGHdgDfF2QA/XZbCBp4Moo5scrkAMPhB7z26XM/AaHuIJdgauA==} - dev: false - - /@babel/runtime@7.21.5: - resolution: {integrity: sha512-8jI69toZqqcsnqGGqwGS4Qb1VwLOEp4hz+CXPywcvjs60u3B4Pom/U/7rm4W8tMOYEB+E9wgD0mW1l3r8qlI9Q==} - engines: {node: '>=6.9.0'} - dependencies: - regenerator-runtime: 0.13.11 - dev: false + undici@6.21.0: + resolution: {integrity: sha512-BUgJXc752Kou3oOIuU1i+yZZypyZRqNPW0vqoMPl8VaoalSfeR0D8/t4iAS3yirs79SSMTxTag+ZC86uswv+Cw==} + engines: {node: '>=18.17'} - /@babel/template@7.20.7: - resolution: {integrity: sha512-8SegXApWe6VoNw0r9JHpSteLKTpTiLZ4rMlGIm9JQ18KiCtyQiAMEazujAHrUS5flrcqYZa75ukev3P6QmUwUw==} - engines: {node: '>=6.9.0'} - dependencies: - '@babel/code-frame': 7.21.4 - '@babel/parser': 7.21.8 - '@babel/types': 7.21.5 - dev: false + unicorn-magic@0.1.0: + resolution: {integrity: sha512-lRfVq8fE8gz6QMBuDM6a+LO3IAzTi05H6gCVaUpir2E1Rwpo4ZUog45KpNXKC/Mn3Yb9UDuHumeFTo9iV/D9FQ==} + engines: {node: '>=18'} - /@babel/traverse@7.21.5: - resolution: {integrity: sha512-AhQoI3YjWi6u/y/ntv7k48mcrCXmus0t79J9qPNlk/lAsFlCiJ047RmbfMOawySTHtywXhbXgpx/8nXMYd+oFw==} - engines: {node: '>=6.9.0'} - dependencies: - '@babel/code-frame': 7.21.4 - '@babel/generator': 7.21.5 - '@babel/helper-environment-visitor': 7.21.5 - '@babel/helper-function-name': 7.21.0 - '@babel/helper-hoist-variables': 7.18.6 - '@babel/helper-split-export-declaration': 7.18.6 - '@babel/parser': 7.21.8 - '@babel/types': 7.21.5 - debug: 4.3.4 - globals: 11.12.0 - transitivePeerDependencies: - - supports-color - dev: false + unicorn-magic@0.3.0: + resolution: {integrity: sha512-+QBBXBCvifc56fsbuxZQ6Sic3wqqc3WWaqxs58gvJrcOuN83HGTCwz3oS5phzU9LthRNE9VrJCFCLUgHeeFnfA==} + engines: {node: '>=18'} - /@babel/types@7.21.5: - resolution: {integrity: sha512-m4AfNvVF2mVC/F7fDEdH2El3HzUg9It/XsCxZiOTTA3m3qYfcSVSbTfM6Q9xG+hYDniZssYhlXKKUMD5m8tF4Q==} - engines: {node: '>=6.9.0'} - dependencies: - '@babel/helper-string-parser': 7.21.5 - '@babel/helper-validator-identifier': 7.19.1 - to-fast-properties: 2.0.0 - dev: false + unique-filename@3.0.0: + resolution: {integrity: sha512-afXhuC55wkAmZ0P18QsVE6kp8JaxrEokN2HGIoIVv2ijHQd419H0+6EigAFcIzXeMIkcIkNBpB3L/DXB3cTS/g==} + engines: {node: ^14.17.0 || ^16.13.0 || >=18.0.0} - /@braintree/sanitize-url@6.0.2: - resolution: {integrity: sha512-Tbsj02wXCbqGmzdnXNk0SOF19ChhRU70BsroIi4Pm6Ehp56in6vch94mfbdQ17DozxkL3BAVjbZ4Qc1a0HFRAg==} - dev: false + unique-slug@4.0.0: + resolution: {integrity: sha512-WrcA6AyEfqDX5bWige/4NQfPZMtASNVxdmWR76WESYQVAACSgWcR6e9i0mofqqBxYFtL4oAxPIptY73/0YE1DQ==} + engines: {node: ^14.17.0 || ^16.13.0 || >=18.0.0} - /@esbuild/android-arm64@0.17.19: - resolution: {integrity: sha512-KBMWvEZooR7+kzY0BtbTQn0OAYY7CsiydT63pVEaPtVYF0hXbUaOyZog37DKxK7NF3XacBJOpYT4adIJh+avxA==} - engines: {node: '>=12'} - cpu: [arm64] - os: [android] - requiresBuild: true - dev: false - optional: true + unist-util-is@6.0.0: + resolution: {integrity: sha512-2qCTHimwdxLfz+YzdGfkqNlH0tLi9xjTnHddPmJwtIG9MGsdbutfTc4P+haPD7l7Cjxf/WZj+we5qfVPvvxfYw==} - /@esbuild/android-arm@0.17.19: - resolution: {integrity: sha512-rIKddzqhmav7MSmoFCmDIb6e2W57geRsM94gV2l38fzhXMwq7hZoClug9USI2pFRGL06f4IOPHHpFNOkWieR8A==} - engines: {node: '>=12'} - cpu: [arm] - os: [android] - requiresBuild: true - dev: false - optional: true + unist-util-position@5.0.0: + resolution: {integrity: sha512-fucsC7HjXvkB5R3kTCO7kUjRdrS0BJt3M/FPxmHMBOm8JQi2BsHAHFsy27E0EolP8rp0NzXsJ+jNPyDWvOJZPA==} - /@esbuild/android-x64@0.17.19: - resolution: {integrity: sha512-uUTTc4xGNDT7YSArp/zbtmbhO0uEEK9/ETW29Wk1thYUJBz3IVnvgEiEwEa9IeLyvnpKrWK64Utw2bgUmDveww==} - engines: {node: '>=12'} - cpu: [x64] - os: [android] - requiresBuild: true - dev: false - optional: true + unist-util-stringify-position@4.0.0: + resolution: {integrity: sha512-0ASV06AAoKCDkS2+xw5RXJywruurpbC4JZSm7nr7MOt1ojAzvyyaO+UxZf18j8FCF6kmzCZKcAgN/yu2gm2XgQ==} - /@esbuild/darwin-arm64@0.17.19: - resolution: {integrity: sha512-80wEoCfF/hFKM6WE1FyBHc9SfUblloAWx6FJkFWTWiCoht9Mc0ARGEM47e67W9rI09YoUxJL68WHfDRYEAvOhg==} - engines: {node: '>=12'} - cpu: [arm64] - os: [darwin] - requiresBuild: true - dev: false - optional: true + unist-util-visit-parents@6.0.1: + resolution: {integrity: sha512-L/PqWzfTP9lzzEa6CKs0k2nARxTdZduw3zyh8d2NVBnsyvHjSX4TWse388YrrQKbvI8w20fGjGlhgT96WwKykw==} - /@esbuild/darwin-x64@0.17.19: - resolution: {integrity: sha512-IJM4JJsLhRYr9xdtLytPLSH9k/oxR3boaUIYiHkAawtwNOXKE8KoU8tMvryogdcT8AU+Bflmh81Xn6Q0vTZbQw==} - engines: {node: '>=12'} - cpu: [x64] - os: [darwin] - requiresBuild: true - dev: false - optional: true + unist-util-visit@5.0.0: + resolution: {integrity: sha512-MR04uvD+07cwl/yhVuVWAtw+3GOR/knlL55Nd/wAdblk27GCVt3lqpTivy/tkJcZoNPzTwS1Y+KMojlLDhoTzg==} - /@esbuild/freebsd-arm64@0.17.19: - resolution: {integrity: sha512-pBwbc7DufluUeGdjSU5Si+P3SoMF5DQ/F/UmTSb8HXO80ZEAJmrykPyzo1IfNbAoaqw48YRpv8shwd1NoI0jcQ==} - engines: {node: '>=12'} - cpu: [arm64] - os: [freebsd] - requiresBuild: true - dev: false - optional: true + universalify@2.0.1: + resolution: {integrity: sha512-gptHNQghINnc/vTGIk0SOFGFNXw7JVrlRUtConJRlvaw6DuX0wO5Jeko9sWrMBhh+PsYAZ7oXAiOnf/UKogyiw==} + engines: {node: '>= 10.0.0'} - /@esbuild/freebsd-x64@0.17.19: - resolution: {integrity: sha512-4lu+n8Wk0XlajEhbEffdy2xy53dpR06SlzvhGByyg36qJw6Kpfk7cp45DR/62aPH9mtJRmIyrXAS5UWBrJT6TQ==} - engines: {node: '>=12'} - cpu: [x64] - os: [freebsd] - requiresBuild: true - dev: false - optional: true + upath@2.0.1: + resolution: {integrity: sha512-1uEe95xksV1O0CYKXo8vQvN1JEbtJp7lb7C5U9HMsIp6IVwntkH/oNUzyVNQSd4S1sYk2FpSSW44FqMc8qee5w==} + engines: {node: '>=4'} - /@esbuild/linux-arm64@0.17.19: - resolution: {integrity: sha512-ct1Tg3WGwd3P+oZYqic+YZF4snNl2bsnMKRkb3ozHmnM0dGWuxcPTTntAF6bOP0Sp4x0PjSF+4uHQ1xvxfRKqg==} - engines: {node: '>=12'} - cpu: [arm64] - os: [linux] - requiresBuild: true - dev: false - optional: true + update-browserslist-db@1.1.2: + resolution: {integrity: sha512-PPypAm5qvlD7XMZC3BujecnaOxwhrtoFR+Dqkk5Aa/6DssiH0ibKoketaj9w8LP7Bont1rYeoV5plxD7RTEPRg==} + hasBin: true + peerDependencies: + browserslist: '>= 4.21.0' + + util-deprecate@1.0.2: + resolution: {integrity: sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw==} - /@esbuild/linux-arm@0.17.19: - resolution: {integrity: sha512-cdmT3KxjlOQ/gZ2cjfrQOtmhG4HJs6hhvm3mWSRDPtZ/lP5oe8FWceS10JaSJC13GBd4eH/haHnqf7hhGNLerA==} - engines: {node: '>=12'} - cpu: [arm] - os: [linux] - requiresBuild: true - dev: false - optional: true + varint@6.0.0: + resolution: {integrity: sha512-cXEIW6cfr15lFv563k4GuVuW/fiwjknytD37jIOLSdSWuOI6WnO/oKwmP2FQTU2l01LP8/M5TSAJpzUaGe3uWg==} - /@esbuild/linux-ia32@0.17.19: - resolution: {integrity: sha512-w4IRhSy1VbsNxHRQpeGCHEmibqdTUx61Vc38APcsRbuVgK0OPEnQ0YD39Brymn96mOx48Y2laBQGqgZ0j9w6SQ==} - engines: {node: '>=12'} - cpu: [ia32] - os: [linux] - requiresBuild: true - dev: false - optional: true + vfile-message@4.0.2: + resolution: {integrity: sha512-jRDZ1IMLttGj41KcZvlrYAaI3CfqpLpfpf+Mfig13viT6NKvRzWZ+lXz0Y5D60w6uJIBAOGq9mSHf0gktF0duw==} - /@esbuild/linux-loong64@0.17.19: - resolution: {integrity: sha512-2iAngUbBPMq439a+z//gE+9WBldoMp1s5GWsUSgqHLzLJ9WoZLZhpwWuym0u0u/4XmZ3gpHmzV84PonE+9IIdQ==} - engines: {node: '>=12'} - cpu: [loong64] - os: [linux] - requiresBuild: true - dev: false - optional: true + vfile@6.0.3: + resolution: {integrity: sha512-KzIbH/9tXat2u30jf+smMwFCsno4wHVdNmzFyL+T/L3UGqqk6JKfVqOFOZEpZSHADH1k40ab6NUIXZq422ov3Q==} - /@esbuild/linux-mips64el@0.17.19: - resolution: {integrity: sha512-LKJltc4LVdMKHsrFe4MGNPp0hqDFA1Wpt3jE1gEyM3nKUvOiO//9PheZZHfYRfYl6AwdTH4aTcXSqBerX0ml4A==} - engines: {node: '>=12'} - cpu: [mips64el] - os: [linux] - requiresBuild: true - dev: false - optional: true + vite@6.0.7: + resolution: {integrity: sha512-RDt8r/7qx9940f8FcOIAH9PTViRrghKaK2K1jY3RaAURrEUbm9Du1mJ72G+jlhtG3WwodnfzY8ORQZbBavZEAQ==} + engines: {node: ^18.0.0 || ^20.0.0 || >=22.0.0} + hasBin: true + peerDependencies: + '@types/node': ^18.0.0 || ^20.0.0 || >=22.0.0 + jiti: '>=1.21.0' + less: '*' + lightningcss: ^1.21.0 + sass: '*' + sass-embedded: '*' + stylus: '*' + sugarss: '*' + terser: ^5.16.0 + tsx: ^4.8.1 + yaml: ^2.4.2 + peerDependenciesMeta: + '@types/node': + optional: true + jiti: + optional: true + less: + optional: true + lightningcss: + optional: true + sass: + optional: true + sass-embedded: + optional: true + stylus: + optional: true + sugarss: + optional: true + terser: + optional: true + tsx: + optional: true + yaml: + optional: true - /@esbuild/linux-ppc64@0.17.19: - resolution: {integrity: sha512-/c/DGybs95WXNS8y3Ti/ytqETiW7EU44MEKuCAcpPto3YjQbyK3IQVKfF6nbghD7EcLUGl0NbiL5Rt5DMhn5tg==} - engines: {node: '>=12'} - cpu: [ppc64] - os: [linux] - requiresBuild: true - dev: false - optional: true + vue-router@4.5.0: + resolution: {integrity: sha512-HDuk+PuH5monfNuY+ct49mNmkCRK4xJAV9Ts4z9UFc4rzdDnxQLyCMGGc8pKhZhHTVzfanpNwB/lwqevcBwI4w==} + peerDependencies: + vue: ^3.2.0 - /@esbuild/linux-riscv64@0.17.19: - resolution: {integrity: sha512-FC3nUAWhvFoutlhAkgHf8f5HwFWUL6bYdvLc/TTuxKlvLi3+pPzdZiFKSWz/PF30TB1K19SuCxDTI5KcqASJqA==} - engines: {node: '>=12'} - cpu: [riscv64] - os: [linux] - requiresBuild: true - dev: false - optional: true + vue@3.5.13: + resolution: {integrity: sha512-wmeiSMxkZCSc+PM2w2VRsOYAZC8GdipNFRTsLSfodVqI9mbejKeXEGr8SckuLnrQPGe3oJN5c3K0vpoU9q/wCQ==} + peerDependencies: + typescript: '*' + peerDependenciesMeta: + typescript: + optional: true - /@esbuild/linux-s390x@0.17.19: - resolution: {integrity: sha512-IbFsFbxMWLuKEbH+7sTkKzL6NJmG2vRyy6K7JJo55w+8xDk7RElYn6xvXtDW8HCfoKBFK69f3pgBJSUSQPr+4Q==} - engines: {node: '>=12'} - cpu: [s390x] - os: [linux] - requiresBuild: true - dev: false - optional: true + vuepress-plugin-components@2.0.0-rc.68: + resolution: {integrity: sha512-JGll1AC40jMSvOlWNQUUN+ZTBFqgoOmuf2QAVWMPY3+D0a9xlBMxSsrK7Rvn0JrE+0txHyOlGJMNUT+f3/OMZQ==} + engines: {node: '>=18.19.0', npm: '>=8', pnpm: '>=7', yarn: '>=2'} + peerDependencies: + artplayer: ^5.0.0 + dashjs: 4.7.4 + hls.js: ^1.4.12 + mpegts.js: ^1.7.3 + sass: ^1.81.0 + sass-embedded: ^1.81.0 + sass-loader: ^16.0.2 + vidstack: ^1.12.9 + vuepress: 2.0.0-rc.19 + peerDependenciesMeta: + artplayer: + optional: true + dashjs: + optional: true + hls.js: + optional: true + mpegts.js: + optional: true + sass: + optional: true + sass-embedded: + optional: true + sass-loader: + optional: true + vidstack: + optional: true - /@esbuild/linux-x64@0.17.19: - resolution: {integrity: sha512-68ngA9lg2H6zkZcyp22tsVt38mlhWde8l3eJLWkyLrp4HwMUr3c1s/M2t7+kHIhvMjglIBrFpncX1SzMckomGw==} - engines: {node: '>=12'} - cpu: [x64] - os: [linux] - requiresBuild: true - dev: false - optional: true + vuepress-plugin-md-enhance@2.0.0-rc.68: + resolution: {integrity: sha512-fi0bkKIEAFihOqBDAYmrQRPImXhfRdx5mi2blX/lvSl7vCun+FZV6NHz8mvGQNh0DcT6vn5ksCNjyih7lkrtEw==} + engines: {node: '>=18.19.0', npm: '>=8', pnpm: '>=7', yarn: '>=2'} + peerDependencies: + '@vue/repl': ^4.1.1 + chart.js: ^4.0.0 + echarts: ^5.0.0 + flowchart.ts: ^3.0.0 + kotlin-playground: ^1.23.0 + markmap-lib: ^0.18.5 + markmap-toolbar: ^0.18.5 + markmap-view: ^0.18.5 + mermaid: ^11.2.0 + sandpack-vue3: ^3.0.0 + sass: ^1.81.0 + sass-embedded: ^1.81.0 + sass-loader: ^16.0.2 + vuepress: 2.0.0-rc.19 + peerDependenciesMeta: + '@vue/repl': + optional: true + chart.js: + optional: true + echarts: + optional: true + flowchart.ts: + optional: true + kotlin-playground: + optional: true + markmap-lib: + optional: true + markmap-toolbar: + optional: true + markmap-view: + optional: true + mermaid: + optional: true + sandpack-vue3: + optional: true + sass: + optional: true + sass-embedded: + optional: true + sass-loader: + optional: true - /@esbuild/netbsd-x64@0.17.19: - resolution: {integrity: sha512-CwFq42rXCR8TYIjIfpXCbRX0rp1jo6cPIUPSaWwzbVI4aOfX96OXY8M6KNmtPcg7QjYeDmN+DD0Wp3LaBOLf4Q==} - engines: {node: '>=12'} - cpu: [x64] - os: [netbsd] - requiresBuild: true - dev: false - optional: true + vuepress-shared@2.0.0-rc.68: + resolution: {integrity: sha512-wqKktaUUvEC6qMWNXuYo7uh5oEzYLhd7sY3ACvj+nu5JgXgQdk0sVoTN3vMp6PmVFmINjJfJB1zzn778hcOFGA==} + engines: {node: '>=18.19.0', npm: '>=8', pnpm: '>=7', yarn: '>=2'} + peerDependencies: + vuepress: 2.0.0-rc.19 - /@esbuild/openbsd-x64@0.17.19: - resolution: {integrity: sha512-cnq5brJYrSZ2CF6c35eCmviIN3k3RczmHz8eYaVlNasVqsNY+JKohZU5MKmaOI+KkllCdzOKKdPs762VCPC20g==} - engines: {node: '>=12'} - cpu: [x64] - os: [openbsd] - requiresBuild: true - dev: false - optional: true + vuepress-theme-hope@2.0.0-rc.68: + resolution: {integrity: sha512-4mMq/VACqkFZx/5gGp+5QnH9p3GYroj75rU7LGIuTIc1hCrsQMfdM/HTvfvoRnBoWUedT+KEAy17Z9ThNOSyDQ==} + engines: {node: '>=18.19.0', npm: '>=8', pnpm: '>=7', yarn: '>=2'} + peerDependencies: + '@vuepress/plugin-docsearch': 2.0.0-rc.70 + '@vuepress/plugin-feed': 2.0.0-rc.70 + '@vuepress/plugin-prismjs': 2.0.0-rc.70 + '@vuepress/plugin-pwa': 2.0.0-rc.70 + '@vuepress/plugin-revealjs': 2.0.0-rc.70 + '@vuepress/plugin-search': 2.0.0-rc.70 + '@vuepress/plugin-slimsearch': 2.0.0-rc.70 + '@vuepress/plugin-watermark': 2.0.0-rc.70 + nodejs-jieba: ^0.2.1 + sass: ^1.81.0 + sass-embedded: ^1.81.0 + sass-loader: ^16.0.2 + vuepress: 2.0.0-rc.19 + peerDependenciesMeta: + '@vuepress/plugin-docsearch': + optional: true + '@vuepress/plugin-feed': + optional: true + '@vuepress/plugin-prismjs': + optional: true + '@vuepress/plugin-pwa': + optional: true + '@vuepress/plugin-revealjs': + optional: true + '@vuepress/plugin-search': + optional: true + '@vuepress/plugin-slimsearch': + optional: true + '@vuepress/plugin-watermark': + optional: true + nodejs-jieba: + optional: true + sass: + optional: true + sass-embedded: + optional: true + sass-loader: + optional: true - /@esbuild/sunos-x64@0.17.19: - resolution: {integrity: sha512-vCRT7yP3zX+bKWFeP/zdS6SqdWB8OIpaRq/mbXQxTGHnIxspRtigpkUcDMlSCOejlHowLqII7K2JKevwyRP2rg==} - engines: {node: '>=12'} - cpu: [x64] - os: [sunos] - requiresBuild: true - dev: false - optional: true + vuepress@2.0.0-rc.19: + resolution: {integrity: sha512-JDeuPTu14Kprdqx2geAryjFJvUzVaMnOLewlAgwVuZTygDWb8cgXhu9/p6rqzzdHETtIrvjbASBhH7JPyqmxmA==} + engines: {node: ^18.19.0 || >=20.4.0} + hasBin: true + peerDependencies: + '@vuepress/bundler-vite': 2.0.0-rc.19 + '@vuepress/bundler-webpack': 2.0.0-rc.19 + vue: ^3.5.0 + peerDependenciesMeta: + '@vuepress/bundler-vite': + optional: true + '@vuepress/bundler-webpack': + optional: true - /@esbuild/win32-arm64@0.17.19: - resolution: {integrity: sha512-yYx+8jwowUstVdorcMdNlzklLYhPxjniHWFKgRqH7IFlUEa0Umu3KuYplf1HUZZ422e3NU9F4LGb+4O0Kdcaag==} - engines: {node: '>=12'} - cpu: [arm64] - os: [win32] - requiresBuild: true - dev: false - optional: true + webidl-conversions@3.0.1: + resolution: {integrity: sha512-2JAn3z8AR6rjK8Sm8orRC0h/bcl/DqL7tRPdGZ4I1CjdF+EaMLmYxBHyXuKL849eucPFhvBoxMsflfOb8kxaeQ==} - /@esbuild/win32-ia32@0.17.19: - resolution: {integrity: sha512-eggDKanJszUtCdlVs0RB+h35wNlb5v4TWEkq4vZcmVt5u/HiDZrTXe2bWFQUez3RgNHwx/x4sk5++4NSSicKkw==} - engines: {node: '>=12'} - cpu: [ia32] - os: [win32] - requiresBuild: true - dev: false - optional: true + whatwg-encoding@3.1.1: + resolution: {integrity: sha512-6qN4hJdMwfYBtE3YBTTHhoeuUrDBPZmbQaxWAqSALV/MeEnR5z1xd8UKud2RAkFoPkmB+hli1TZSnyi84xz1vQ==} + engines: {node: '>=18'} - /@esbuild/win32-x64@0.17.19: - resolution: {integrity: sha512-lAhycmKnVOuRYNtRtatQR1LPQf2oYCkRGkSFnseDAKPl8lu5SOsK/e1sXe5a0Pc5kHIHe6P2I/ilntNv2xf3cA==} - engines: {node: '>=12'} - cpu: [x64] - os: [win32] - requiresBuild: true - dev: false - optional: true + whatwg-mimetype@4.0.0: + resolution: {integrity: sha512-QaKxh0eNIi2mE9p2vEdzfagOKHCcj1pJ56EEHGQOVxp8r9/iszLUUV7v89x9O1p/T+NlTM5W7jW6+cz4Fq1YVg==} + engines: {node: '>=18'} - /@isaacs/cliui@8.0.2: - resolution: {integrity: sha512-O8jcjabXaleOG9DQ0+ARXWZBTfnP4WNAqzuiJK7ll44AmxGKv/J2M4TPjxjY3znBCfvBXFzucm1twdyFybFqEA==} - engines: {node: '>=12'} - dependencies: - string-width: 5.1.2 - string-width-cjs: /string-width@4.2.3 - strip-ansi: 7.0.1 - strip-ansi-cjs: /strip-ansi@6.0.1 - wrap-ansi: 8.1.0 - wrap-ansi-cjs: /wrap-ansi@7.0.0 - dev: false + whatwg-url@5.0.0: + resolution: {integrity: sha512-saE57nupxk6v3HY35+jzBwYa0rKSy0XR8JSxZPwgLr7ys0IBzhGviA1/TUGJLmSVqs8pb9AnvICXEuOHLprYTw==} - /@jridgewell/gen-mapping@0.3.3: - resolution: {integrity: sha512-HLhSWOLRi875zjjMG/r+Nv0oCW8umGb0BgEhyX3dDX3egwZtB8PqLnjz3yedt8R5StBrzcg4aBpnh8UA9D1BoQ==} - engines: {node: '>=6.0.0'} - dependencies: - '@jridgewell/set-array': 1.1.2 - '@jridgewell/sourcemap-codec': 1.4.15 - '@jridgewell/trace-mapping': 0.3.18 - dev: false + which-module@2.0.1: + resolution: {integrity: sha512-iBdZ57RDvnOR9AGBhML2vFZf7h8vmBjhoaZqODJBFWHVtKkDmKuHai3cx5PgVMrX5YDNp27AofYbAwctSS+vhQ==} - /@jridgewell/resolve-uri@3.1.0: - resolution: {integrity: sha512-F2msla3tad+Mfht5cJq7LSXcdudKTWCVYUgw6pLFOOHSTtZlj6SWNYAp+AhuqLmWdBO2X5hPrLcu8cVP8fy28w==} - engines: {node: '>=6.0.0'} - dev: false + which@2.0.2: + resolution: {integrity: sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA==} + engines: {node: '>= 8'} + hasBin: true - /@jridgewell/set-array@1.1.2: - resolution: {integrity: sha512-xnkseuNADM0gt2bs+BvhO0p78Mk762YnZdsuzFV018NoG1Sj1SCQvpSqa7XUaTam5vAGasABV9qXASMKnFMwMw==} - engines: {node: '>=6.0.0'} - dev: false + which@4.0.0: + resolution: {integrity: sha512-GlaYyEb07DPxYCKhKzplCWBJtvxZcZMrL+4UkrTSJHHPyZU4mYYTv3qaOe77H7EODLSSopAUFAc6W8U4yqvscg==} + engines: {node: ^16.13.0 || >=18.0.0} + hasBin: true - /@jridgewell/source-map@0.3.3: - resolution: {integrity: sha512-b+fsZXeLYi9fEULmfBrhxn4IrPlINf8fiNarzTof004v3lFdntdwa9PF7vFJqm3mg7s+ScJMxXaE3Acp1irZcg==} - dependencies: - '@jridgewell/gen-mapping': 0.3.3 - '@jridgewell/trace-mapping': 0.3.18 - dev: false + wicked-good-xpath@1.3.0: + resolution: {integrity: sha512-Gd9+TUn5nXdwj/hFsPVx5cuHHiF5Bwuc30jZ4+ronF1qHK5O7HD0sgmXWSEgwKquT3ClLoKPVbO6qGwVwLzvAw==} - /@jridgewell/sourcemap-codec@1.4.14: - resolution: {integrity: sha512-XPSJHWmi394fuUuzDnGz1wiKqWfo1yXecHQMRf2l6hztTO+nPru658AyDngaBe7isIxEkRsPR3FZh+s7iVa4Uw==} - dev: false + wide-align@1.1.5: + resolution: {integrity: sha512-eDMORYaPNZ4sQIuuYPDHdQvf4gyCF9rEEV/yPxGfwPkRodwEgiMUUXTx/dex+Me0wxx53S+NgUHaP7y3MGlDmg==} - /@jridgewell/sourcemap-codec@1.4.15: - resolution: {integrity: sha512-eF2rxCRulEKXHTRiDrDy6erMYWqNw4LPdQ8UQA4huuxaQsVeRPFl2oM8oDGxMFhJUWZf9McpLtJasDDZb/Bpeg==} - dev: false + wrap-ansi@6.2.0: + resolution: {integrity: sha512-r6lPcBGxZXlIcymEu7InxDMhdW0KDxpLgoFLcguasxCaJ/SOIZwINatK9KY/tf+ZrlywOKU0UDj3ATXUBfxJXA==} + engines: {node: '>=8'} - /@jridgewell/trace-mapping@0.3.18: - resolution: {integrity: sha512-w+niJYzMHdd7USdiH2U6869nqhD2nbfZXND5Yp93qIbEmnDNk7PD48o+YchRVpzMU7M6jVCbenTR7PA1FLQ9pA==} - dependencies: - '@jridgewell/resolve-uri': 3.1.0 - '@jridgewell/sourcemap-codec': 1.4.14 - dev: false + wrap-ansi@7.0.0: + resolution: {integrity: sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q==} + engines: {node: '>=10'} - /@khanacademy/simple-markdown@0.8.6(react-dom@16.14.0)(react@16.14.0): - resolution: {integrity: sha512-mAUlR9lchzfqunR89pFvNI51jQKsMpJeWYsYWw0DQcUXczn/T/V6510utgvm7X0N3zN87j1SvuKk8cMbl9IAFw==} - peerDependencies: - react: 16.14.0 - react-dom: 16.14.0 - dependencies: - '@types/react': 18.2.6 - react: 16.14.0 - react-dom: 16.14.0(react@16.14.0) - dev: false + wrap-ansi@8.1.0: + resolution: {integrity: sha512-si7QWI6zUMq56bESFvagtmzMdGOtoxfR+Sez11Mobfc7tm+VkUckk9bW2UeffTGVUbOksxmSw0AA2gs8g71NCQ==} + engines: {node: '>=12'} - /@kurkle/color@0.3.2: - resolution: {integrity: sha512-fuscdXJ9G1qb7W8VdHi+IwRqij3lBkosAm4ydQtEmbY58OzHXqQhvlxqEkoz0yssNVn38bcpRWgA9PP+OGoisw==} - dev: false + wrappy@1.0.2: + resolution: {integrity: sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ==} - /@lit-labs/ssr-dom-shim@1.1.1: - resolution: {integrity: sha512-kXOeFbfCm4fFf2A3WwVEeQj55tMZa8c8/f9AKHMobQMkzNUfUj+antR3fRPaZJawsa1aZiP/Da3ndpZrwEe4rQ==} - dev: false + xml-js@1.6.11: + resolution: {integrity: sha512-7rVi2KMfwfWFl+GpPg6m80IVMWXLRjO+PxTq7V2CDhoGak0wzYzFgUY2m4XJ47OGdXd8eLE8EmwfAmdjw7lC1g==} + hasBin: true - /@lit/reactive-element@1.6.1: - resolution: {integrity: sha512-va15kYZr7KZNNPZdxONGQzpUr+4sxVu7V/VG7a8mRfPPXUyhEYj5RzXCQmGrlP3tAh0L3HHm5AjBMFYRqlM9SA==} - dependencies: - '@lit-labs/ssr-dom-shim': 1.1.1 - dev: false + xmldom-sre@0.1.31: + resolution: {integrity: sha512-f9s+fUkX04BxQf+7mMWAp5zk61pciie+fFLC9hX9UVvCeJQfNHRHXpeo5MPcR0EUf57PYLdt+ZO4f3Ipk2oZUw==} + engines: {node: '>=0.1'} - /@mdit-vue/plugin-component@0.12.0: - resolution: {integrity: sha512-LrwV3f0Y6H7b7m/w1Y3bkGuR3HOiBK4QiHHW3HuRMza6MZodDQbj8Baik5/V5GiSg1/ltijS1CymVcycd1EfTw==} - dependencies: - '@types/markdown-it': 12.2.3 - markdown-it: 13.0.1 - dev: false + y18n@4.0.3: + resolution: {integrity: sha512-JKhqTOwSrqNA1NY5lSztJ1GrBiUodLMmIZuLiDaMRJ+itFd+ABVE8XBjOvIWL+rSqNDC74LCSFmlb/U4UZ4hJQ==} - /@mdit-vue/plugin-frontmatter@0.12.0: - resolution: {integrity: sha512-26Y3JktjGgNoCVH7NLqi5RcdAauAqxepTt2qXueRcRHtGpiRQV2/M1FveIhCOTCtHSuG5bBOHUxGaV6vRK3Vbw==} - dependencies: - '@mdit-vue/types': 0.12.0 - '@types/markdown-it': 12.2.3 - gray-matter: 4.0.3 - markdown-it: 13.0.1 - dev: false - - /@mdit-vue/plugin-headers@0.12.0: - resolution: {integrity: sha512-7qR63J2uc/rXbjHT77WoYBm9imwzx1tVESmRK+Uth6kqFvSWAXAFPcm4PBatGEE8TgzhklPs5BTcQtQhmmsyaw==} - dependencies: - '@mdit-vue/shared': 0.12.0 - '@mdit-vue/types': 0.12.0 - '@types/markdown-it': 12.2.3 - markdown-it: 13.0.1 - dev: false - - /@mdit-vue/plugin-sfc@0.12.0: - resolution: {integrity: sha512-mH+rHsERzDxGucAQJILspRiD723AIWMmtMhp7lDKdkCIbIhYfupFv/CkSeX+LAx5UY5greWvUTPGYVKn4gw/5Q==} - dependencies: - '@mdit-vue/types': 0.12.0 - '@types/markdown-it': 12.2.3 - markdown-it: 13.0.1 - dev: false - - /@mdit-vue/plugin-title@0.12.0: - resolution: {integrity: sha512-XrQcior1EmPgsDG88KsoF4LUSQw/RS1Nyfn5xNWGiurO70a2hml4kCe0XzT4sLKUAPG0HNbIY6b92ezNezqWTg==} - dependencies: - '@mdit-vue/shared': 0.12.0 - '@mdit-vue/types': 0.12.0 - '@types/markdown-it': 12.2.3 - markdown-it: 13.0.1 - dev: false - - /@mdit-vue/plugin-toc@0.12.0: - resolution: {integrity: sha512-tT985CqvLp17DFWHrSvmmJbh7qcy0Rl0dBbYN//Fn952a04dbr1mb2LqW0B1oStSAQj2q24HpK4ZPgYOt7Z1Jg==} - dependencies: - '@mdit-vue/shared': 0.12.0 - '@mdit-vue/types': 0.12.0 - '@types/markdown-it': 12.2.3 - markdown-it: 13.0.1 - dev: false - - /@mdit-vue/shared@0.12.0: - resolution: {integrity: sha512-E+sGSubhvnp+Gmb2hJXFDxdLwwQD1H52EVbA4yrxxI5q/cwtnPIN2eJU3zlZB9KcvzXYDFFwt/x2mfhK8RZKBg==} - dependencies: - '@mdit-vue/types': 0.12.0 - '@types/markdown-it': 12.2.3 - markdown-it: 13.0.1 - dev: false - - /@mdit-vue/types@0.12.0: - resolution: {integrity: sha512-mrC4y8n88BYvgcgzq9bvTlDgFyi2zuvzmPilRvRc3Uz1iIvq8mDhxJ0rHKFUNzPEScpDvJdIujqiDrulMqiudA==} - dev: false - - /@mdit/plugin-align@0.4.5: - resolution: {integrity: sha512-TwZXdfEPbPz2k8S+u9wML5RTO4R3vNxm5wLDuMbPmoT3hwp0cbA6mwYZp7XODEZ4FXLrlG4rnyHHsDoQ8kTBeA==} - engines: {node: '>= 14'} - dependencies: - '@mdit/plugin-container': 0.4.5 - '@types/markdown-it': 12.2.3 - markdown-it: 13.0.1 - dev: false + yallist@4.0.0: + resolution: {integrity: sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A==} - /@mdit/plugin-attrs@0.4.5: - resolution: {integrity: sha512-20sfnsVz0IhMMLXJMXhhp8nHF5H8nTnSINlA0bK1b4aaVNLTXXxHqOgXVsfKY+LcjH/rXA1VefItsat0xPN9aw==} - engines: {node: '>= 14'} - dependencies: - '@types/markdown-it': 12.2.3 - markdown-it: 13.0.1 - dev: false + yargs-parser@18.1.3: + resolution: {integrity: sha512-o50j0JeToy/4K6OZcaQmW6lyXXKhq7csREXcDwk2omFPJEwUNOVtJKvmDr9EI1fAJZUyZcRF7kxGBWmRXudrCQ==} + engines: {node: '>=6'} - /@mdit/plugin-container@0.4.5: - resolution: {integrity: sha512-fWZAaGYPN5WvJWFXghOiefOGw56lFQrU+UxE1PWb9M2am18Bn6XPbQvrIToHiqOdoSipYpLuZTp6a1Vcap85dQ==} - engines: {node: '>= 14'} - dependencies: - '@types/markdown-it': 12.2.3 - markdown-it: 13.0.1 - dev: false + yargs@15.4.1: + resolution: {integrity: sha512-aePbxDmcYW++PaqBsJ+HYUFwCdv4LVvdnhBy78E57PIor8/OVvhMrADFFEDh8DHDFRv/O9i3lPhsENjO7QX0+A==} + engines: {node: '>=8'} - /@mdit/plugin-figure@0.4.5: - resolution: {integrity: sha512-bCctdCFSa2OnB994NJ2276Ym4sS16HMkJJ/+y6IllWzN33oUowjneCnfoSizE6ysge44QtNDueMhXn2eSPC1TQ==} - engines: {node: '>= 14'} - dependencies: - '@types/markdown-it': 12.2.3 - markdown-it: 13.0.1 - dev: false + yoctocolors@2.1.1: + resolution: {integrity: sha512-GQHQqAopRhwU8Kt1DDM8NjibDXHC8eoh1erhGAJPEyveY9qqVeXvVikNKrDz69sHowPMorbPUrH/mx8c50eiBQ==} + engines: {node: '>=18'} - /@mdit/plugin-footnote@0.4.5: - resolution: {integrity: sha512-XKcnWlnu1vtFwgGdG4sykWLhCkt9mHhbPZlJvQGyXHWcC+W2wo27n82q8nT/fBfrSC+k7mzjGR7+jAtHLup5Xg==} - engines: {node: '>= 14'} - dependencies: - '@types/markdown-it': 12.2.3 - markdown-it: 13.0.1 - dev: false + zwitch@2.0.4: + resolution: {integrity: sha512-bXE4cR/kVZhKZX/RjPEflHaKVhUVl85noU3v6b8apfQEc1x4A+zBxjZ4lN8LqGd6WZ3dl98pY4o717VFmoPp+A==} - /@mdit/plugin-img-lazyload@0.4.5: - resolution: {integrity: sha512-eYG7YBBOPYwSPOP0ku8RHKWLlQXilcCD9BhqYUc1rMOE/eMNDJakicu8w0IsFunaJMaoFBjtQgn9gPby7+MZwg==} - engines: {node: '>= 14'} - dependencies: - '@types/markdown-it': 12.2.3 - markdown-it: 13.0.1 - dev: false +snapshots: - /@mdit/plugin-img-mark@0.4.5: - resolution: {integrity: sha512-e79VUTtW0aJkZXhFswHc4HR2lDsQVBa8i5E0kiFlR8/UiHrn3vP03QbXJs9SILUlaeWtSDmCUTQjsNg/Yd/RDg==} - engines: {node: '>= 14'} - dependencies: - '@types/markdown-it': 12.2.3 - markdown-it: 13.0.1 - dev: false + '@babel/helper-string-parser@7.25.9': {} - /@mdit/plugin-img-size@0.4.5: - resolution: {integrity: sha512-RYnpsi+YAAyG6I7N9WtsOAp4iPCucbABPmjmkqdnoFtFPV3VQHCTA8Y4sKMm/PP/d56L3DVny+UaVSMs0zsdRA==} - engines: {node: '>= 14'} - dependencies: - '@types/markdown-it': 12.2.3 - markdown-it: 13.0.1 - dev: false + '@babel/helper-validator-identifier@7.25.9': {} - /@mdit/plugin-include@0.4.5: - resolution: {integrity: sha512-oq2fIPWGFUAUfWxxobbs9XuVXk5w0rL5jtqVCCPBKiSHRiTtFtmGm28bNLiiDm5RY7kItOqOrQ6pREE9X8Kdug==} + '@babel/parser@7.26.5': dependencies: - '@types/markdown-it': 12.2.3 - markdown-it: 13.0.1 - upath: 2.0.1 - dev: false + '@babel/types': 7.26.5 - /@mdit/plugin-katex@0.4.5: - resolution: {integrity: sha512-ELW2n+s1f22sKBSATTllmTjEcui6ALFQbifJ7tdpGczfNZ3bRtUrdGYrN6C4vzataaELcp3MurG0XFfe0oIUCw==} - engines: {node: '>= 14'} + '@babel/types@7.26.5': dependencies: - '@mdit/plugin-tex': 0.4.5 - '@types/katex': 0.16.0 - '@types/markdown-it': 12.2.3 - katex: 0.16.7 - markdown-it: 13.0.1 - dev: false + '@babel/helper-string-parser': 7.25.9 + '@babel/helper-validator-identifier': 7.25.9 - /@mdit/plugin-mark@0.4.5: - resolution: {integrity: sha512-gqC7qUxTq/BaABYwTKg2PBPO4BsugAQso3Nb8VibfPvbEOEJxpg7sCFe1qoYspIQ8e5fxTKQ94cBuuQbTHEABQ==} - engines: {node: '>= 14'} - dependencies: - '@types/markdown-it': 12.2.3 - markdown-it: 13.0.1 - dev: false + '@bufbuild/protobuf@2.2.3': {} - /@mdit/plugin-mathjax@0.4.5: - resolution: {integrity: sha512-5AddT0QC+ODH17yvNmx2kAfiY2bVg24HHhsI0nXg0ZX21LMHCjKM6k0Fuvj4BQLB9jYoZDf9v7325W7sRsfedQ==} - engines: {node: '>= 14'} - dependencies: - '@mdit/plugin-tex': 0.4.5 - '@types/markdown-it': 12.2.3 - markdown-it: 13.0.1 - mathjax-full: 3.2.2 - upath: 2.0.1 - dev: false + '@esbuild/aix-ppc64@0.21.5': + optional: true - /@mdit/plugin-stylize@0.4.5: - resolution: {integrity: sha512-nRgYHFrEewL3lf55IrVsazpOgTwcxnEtLck/ruzYOYhxnvUVUb0tSo8c4rokLAcHq/2s7/UPXjQfb5fMCnwwWQ==} - engines: {node: '>= 14'} - dependencies: - '@types/markdown-it': 12.2.3 - markdown-it: 13.0.1 - dev: false + '@esbuild/aix-ppc64@0.24.2': + optional: true - /@mdit/plugin-sub@0.4.5: - resolution: {integrity: sha512-Q7RLah3SrQDP+OXl38yPLYhSqRLEquN2YELccC0DHGwjBfDHk8wHThnExNJngTaGgkxoBysLfoG6HGX6pby1Mg==} - engines: {node: '>= 14'} - dependencies: - '@types/markdown-it': 12.2.3 - markdown-it: 13.0.1 - dev: false + '@esbuild/android-arm64@0.21.5': + optional: true - /@mdit/plugin-sup@0.4.5: - resolution: {integrity: sha512-8EWIfqdWLOuA46X7f1NxgIiLJy6giW+DJXlPguwg0bhXQI7b6QZgd7v9nv0IFesRcHwzYdfSbpgXqKFdrUBivw==} - engines: {node: '>= 14'} - dependencies: - '@types/markdown-it': 12.2.3 - markdown-it: 13.0.1 - dev: false + '@esbuild/android-arm64@0.24.2': + optional: true - /@mdit/plugin-tab@0.4.5: - resolution: {integrity: sha512-ya3uDEA9I0+MSk8DQaJWrND/ZxBRnLspBouNvn49TqEqvMmLseK140DvKymb2PvnxE4AfJQY+eAeDPzpA7b7yw==} - dependencies: - '@types/markdown-it': 12.2.3 - markdown-it: 13.0.1 - dev: false + '@esbuild/android-arm@0.21.5': + optional: true - /@mdit/plugin-tasklist@0.4.5: - resolution: {integrity: sha512-c5agRjHLa+JWjm6ovs+MsYdjafDAVzo3KbmN8nj8bCm+Rn9CVyOzq/lW0xmCs2I86kiDNZBukaESsIiM8f2s4w==} - engines: {node: '>= 14'} - dependencies: - '@types/markdown-it': 12.2.3 - markdown-it: 13.0.1 - dev: false + '@esbuild/android-arm@0.24.2': + optional: true - /@mdit/plugin-tex@0.4.5: - resolution: {integrity: sha512-L7ekZLMdzBNqsHzFS00ULyV+6ADtNAw5n7wN48qkE63K7QRWpXKEdXkNQ3LyaUwPCfSm1GgUv/QzI7SYiiL6jg==} - engines: {node: '>= 14'} - dependencies: - '@types/markdown-it': 12.2.3 - markdown-it: 13.0.1 - dev: false + '@esbuild/android-x64@0.21.5': + optional: true - /@mdit/plugin-uml@0.4.5: - resolution: {integrity: sha512-f9Y2AWz9Lt4hxx0c6VAFh2DZ9/aACJ+Uzrj7dvEmrW13WBNQxOhzxfoyTyo+dQmi7HyNdbz8RDXctJe+Yh+cWg==} - engines: {node: '>= 14'} - dependencies: - '@types/markdown-it': 12.2.3 - markdown-it: 13.0.1 - dev: false + '@esbuild/android-x64@0.24.2': + optional: true - /@nodelib/fs.scandir@2.1.5: - resolution: {integrity: sha512-vq24Bq3ym5HEQm2NKCr3yXDwjc7vTsEThRDnkp2DK9p1uqLR+DHurm/NOTo0KG7HYHU7eppKZj3MyqYuMBf62g==} - engines: {node: '>= 8'} - dependencies: - '@nodelib/fs.stat': 2.0.5 - run-parallel: 1.2.0 - dev: false + '@esbuild/darwin-arm64@0.21.5': + optional: true - /@nodelib/fs.stat@2.0.5: - resolution: {integrity: sha512-RkhPPp2zrqDAQA/2jNhnztcPAlv64XdhIp7a7454A5ovI7Bukxgt7MX7udwAu3zg1DcpPU0rz3VV1SeaqvY4+A==} - engines: {node: '>= 8'} - dev: false + '@esbuild/darwin-arm64@0.24.2': + optional: true - /@nodelib/fs.walk@1.2.8: - resolution: {integrity: sha512-oGB+UxlgWcgQkgwo8GcEGwemoTFt3FIO9ababBmaGwXIoBKZ+GTy0pP185beGg7Llih/NSHSV2XAs1lnznocSg==} - engines: {node: '>= 8'} - dependencies: - '@nodelib/fs.scandir': 2.1.5 - fastq: 1.15.0 - dev: false + '@esbuild/darwin-x64@0.21.5': + optional: true - /@pkgjs/parseargs@0.11.0: - resolution: {integrity: sha512-+1VkjdD0QBLPodGrJUeqarH8VAIvQODIbwh9XpP5Syisf7YoQgsJKPNFoqqLQlu+VQ/tVSshMR6loPMn8U+dPg==} - engines: {node: '>=14'} - requiresBuild: true - dev: false + '@esbuild/darwin-x64@0.24.2': optional: true - /@rollup/plugin-babel@5.3.1(@babel/core@7.21.8)(rollup@2.79.1): - resolution: {integrity: sha512-WFfdLWU/xVWKeRQnKmIAQULUI7Il0gZnBIH/ZFO069wYIfPu+8zrfp/KMW0atmELoRDq8FbiP3VCss9MhCut7Q==} - engines: {node: '>= 10.0.0'} - peerDependencies: - '@babel/core': ^7.0.0 - '@types/babel__core': ^7.1.9 - rollup: ^1.20.0||^2.0.0 - peerDependenciesMeta: - '@types/babel__core': - optional: true - dependencies: - '@babel/core': 7.21.8 - '@babel/helper-module-imports': 7.21.4 - '@rollup/pluginutils': 3.1.0(rollup@2.79.1) - rollup: 2.79.1 - dev: false + '@esbuild/freebsd-arm64@0.21.5': + optional: true - /@rollup/plugin-node-resolve@11.2.1(rollup@2.79.1): - resolution: {integrity: sha512-yc2n43jcqVyGE2sqV5/YCmocy9ArjVAP/BeXyTtADTBBX6V0e5UMqwO8CdQ0kzjb6zu5P1qMzsScCMRvE9OlVg==} - engines: {node: '>= 10.0.0'} - peerDependencies: - rollup: ^1.20.0||^2.0.0 - dependencies: - '@rollup/pluginutils': 3.1.0(rollup@2.79.1) - '@types/resolve': 1.17.1 - builtin-modules: 3.3.0 - deepmerge: 4.3.1 - is-module: 1.0.0 - resolve: 1.22.2 - rollup: 2.79.1 - dev: false - - /@rollup/plugin-replace@2.4.2(rollup@2.79.1): - resolution: {integrity: sha512-IGcu+cydlUMZ5En85jxHH4qj2hta/11BHq95iHEyb2sbgiN0eCdzvUcHw5gt9pBL5lTi4JDYJ1acCoMGpTvEZg==} - peerDependencies: - rollup: ^1.20.0 || ^2.0.0 - dependencies: - '@rollup/pluginutils': 3.1.0(rollup@2.79.1) - magic-string: 0.25.9 - rollup: 2.79.1 - dev: false + '@esbuild/freebsd-arm64@0.24.2': + optional: true - /@rollup/pluginutils@3.1.0(rollup@2.79.1): - resolution: {integrity: sha512-GksZ6pr6TpIjHm8h9lSQ8pi8BE9VeubNT0OMJ3B5uZJ8pz73NPiqOtCog/x2/QzM1ENChPKxMDhiQuRHsqc+lg==} - engines: {node: '>= 8.0.0'} - peerDependencies: - rollup: ^1.20.0||^2.0.0 - dependencies: - '@types/estree': 0.0.39 - estree-walker: 1.0.1 - picomatch: 2.3.1 - rollup: 2.79.1 - dev: false + '@esbuild/freebsd-x64@0.21.5': + optional: true + + '@esbuild/freebsd-x64@0.24.2': + optional: true + + '@esbuild/linux-arm64@0.21.5': + optional: true - /@stackblitz/sdk@1.9.0: - resolution: {integrity: sha512-3m6C7f8pnR5KXys/Hqx2x6ylnpqOak6HtnZI6T5keEO0yT+E4Spkw37VEbdwuC+2oxmjdgq6YZEgiKX7hM1GmQ==} - dev: false + '@esbuild/linux-arm64@0.24.2': + optional: true - /@surma/rollup-plugin-off-main-thread@2.2.3: - resolution: {integrity: sha512-lR8q/9W7hZpMWweNiAKU7NQerBnzQQLvi8qnTDU/fxItPhtZVMbPV3lbCwjhIlNBe9Bbr5V+KHshvWmVSG9cxQ==} - dependencies: - ejs: 3.1.9 - json5: 2.2.3 - magic-string: 0.25.9 - string.prototype.matchall: 4.0.8 - dev: false + '@esbuild/linux-arm@0.21.5': + optional: true - /@types/debug@4.1.7: - resolution: {integrity: sha512-9AonUzyTjXXhEOa0DnqpzZi6VHlqKMswga9EXjpXnnqxwLtdvPPtlO8evrI5D9S6asFRCQ6v+wpiUKbw+vKqyg==} - dependencies: - '@types/ms': 0.7.31 - dev: false + '@esbuild/linux-arm@0.24.2': + optional: true - /@types/estree@0.0.39: - resolution: {integrity: sha512-EYNwp3bU+98cpU4lAWYYL7Zz+2gryWH1qbdDTidVd6hkiR6weksdbMadyXKXNPEkQFhXM+hVO9ZygomHXp+AIw==} - dev: false + '@esbuild/linux-ia32@0.21.5': + optional: true - /@types/fs-extra@11.0.1: - resolution: {integrity: sha512-MxObHvNl4A69ofaTRU8DFqvgzzv8s9yRtaPPm5gud9HDNvpB3GPQFvNuTWAI59B9huVGV5jXYJwbCsmBsOGYWA==} - dependencies: - '@types/jsonfile': 6.1.1 - '@types/node': 20.1.5 - dev: false + '@esbuild/linux-ia32@0.24.2': + optional: true - /@types/hash-sum@1.0.0: - resolution: {integrity: sha512-FdLBT93h3kcZ586Aee66HPCVJ6qvxVjBlDWNmxSGSbCZe9hTsjRKdSsl4y1T+3zfujxo9auykQMnFsfyHWD7wg==} - dev: false + '@esbuild/linux-loong64@0.21.5': + optional: true - /@types/js-yaml@4.0.5: - resolution: {integrity: sha512-FhpRzf927MNQdRZP0J5DLIdTXhjLYzeUTmLAu69mnVksLH9CJY3IuSeEgbKUki7GQZm0WqDkGzyxju2EZGD2wA==} - dev: false + '@esbuild/linux-loong64@0.24.2': + optional: true - /@types/jsonfile@6.1.1: - resolution: {integrity: sha512-GSgiRCVeapDN+3pqA35IkQwasaCh/0YFH5dEF6S88iDvEn901DjOeH3/QPY+XYP1DFzDZPvIvfeEgk+7br5png==} - dependencies: - '@types/node': 20.1.5 - dev: false + '@esbuild/linux-mips64el@0.21.5': + optional: true - /@types/katex@0.16.0: - resolution: {integrity: sha512-hz+S3nV6Mym5xPbT9fnO8dDhBFQguMYpY0Ipxv06JMi1ORgnEM4M1ymWDUhUNer3ElLmT583opRo4RzxKmh9jw==} - dev: false + '@esbuild/linux-mips64el@0.24.2': + optional: true - /@types/linkify-it@3.0.2: - resolution: {integrity: sha512-HZQYqbiFVWufzCwexrvh694SOim8z2d+xJl5UNamcvQFejLY/2YUtzXHYi3cHdI7PMlS8ejH2slRAOJQ32aNbA==} - dev: false + '@esbuild/linux-ppc64@0.21.5': + optional: true - /@types/markdown-it-emoji@2.0.2: - resolution: {integrity: sha512-2ln8Wjbcj/0oRi/6VnuMeWEHHuK8uapFttvcLmDIe1GKCsFBLOLBX+D+xhDa9oWOQV0IpvxwrSfKKssAqqroog==} - dependencies: - '@types/markdown-it': 12.2.3 - dev: false + '@esbuild/linux-ppc64@0.24.2': + optional: true - /@types/markdown-it@12.2.3: - resolution: {integrity: sha512-GKMHFfv3458yYy+v/N8gjufHO6MSZKCOXpZc5GXIWWy8uldwfmPn98vp81gZ5f9SVw8YYBctgfJ22a2d7AOMeQ==} - dependencies: - '@types/linkify-it': 3.0.2 - '@types/mdurl': 1.0.2 - dev: false + '@esbuild/linux-riscv64@0.21.5': + optional: true - /@types/mdurl@1.0.2: - resolution: {integrity: sha512-eC4U9MlIcu2q0KQmXszyn5Akca/0jrQmwDRgpAMJai7qBWq4amIQhZyNau4VYGtCeALvW1/NtjzJJ567aZxfKA==} - dev: false + '@esbuild/linux-riscv64@0.24.2': + optional: true - /@types/ms@0.7.31: - resolution: {integrity: sha512-iiUgKzV9AuaEkZqkOLDIvlQiL6ltuZd9tGcW3gwpnX8JbuiuhFlEGmmFXEXkN50Cvq7Os88IY2v0dkDqXYWVgA==} - dev: false + '@esbuild/linux-s390x@0.21.5': + optional: true - /@types/node@17.0.45: - resolution: {integrity: sha512-w+tIMs3rq2afQdsPJlODhoUEKzFP1ayaoyl1CcnwtIlsVe7K7bA1NGm4s3PraqTLlXnbIN84zuBlxBWo1u9BLw==} - dev: false + '@esbuild/linux-s390x@0.24.2': + optional: true - /@types/node@20.1.5: - resolution: {integrity: sha512-IvGD1CD/nego63ySR7vrAKEX3AJTcmrAN2kn+/sDNLi1Ff5kBzDeEdqWDplK+0HAEoLYej137Sk0cUU8OLOlMg==} - dev: false + '@esbuild/linux-x64@0.21.5': + optional: true - /@types/prop-types@15.7.5: - resolution: {integrity: sha512-JCB8C6SnDoQf0cNycqd/35A7MjcnK+ZTqE7judS6o7utxUCg6imJg3QK2qzHKszlTjcj2cn+NwMB2i96ubpj7w==} - dev: false + '@esbuild/linux-x64@0.24.2': + optional: true - /@types/raphael@2.3.3: - resolution: {integrity: sha512-Rhvq0q6wzyvipejki/9w87/pgapyE+s3gO66tdl1oD3qDrow+ek+4vVYAbRkeL58HCCK9EOZKwyjqYJ/TFkmtQ==} - dev: false + '@esbuild/netbsd-arm64@0.24.2': + optional: true - /@types/react@18.2.6: - resolution: {integrity: sha512-wRZClXn//zxCFW+ye/D2qY65UsYP1Fpex2YXorHc8awoNamkMZSvBxwxdYVInsHOZZd2Ppq8isnSzJL5Mpf8OA==} - dependencies: - '@types/prop-types': 15.7.5 - '@types/scheduler': 0.16.3 - csstype: 3.1.2 - dev: false + '@esbuild/netbsd-x64@0.21.5': + optional: true - /@types/resolve@1.17.1: - resolution: {integrity: sha512-yy7HuzQhj0dhGpD8RLXSZWEkLsV9ibvxvi6EiJ3bkqLAO1RGo0WbkWQiwpRlSFymTJRz0d3k5LM3kkx8ArDbLw==} - dependencies: - '@types/node': 20.1.5 - dev: false + '@esbuild/netbsd-x64@0.24.2': + optional: true - /@types/sax@1.2.4: - resolution: {integrity: sha512-pSAff4IAxJjfAXUG6tFkO7dsSbTmf8CtUpfhhZ5VhkRpC4628tJhh3+V6H1E+/Gs9piSzYKT5yzHO5M4GG9jkw==} - dependencies: - '@types/node': 17.0.45 - dev: false + '@esbuild/openbsd-arm64@0.24.2': + optional: true - /@types/scheduler@0.16.3: - resolution: {integrity: sha512-5cJ8CB4yAx7BH1oMvdU0Jh9lrEXyPkar6F9G/ERswkCuvP4KQZfZkSjcMbAICCpQTN4OuZn8tz0HiKv9TGZgrQ==} - dev: false + '@esbuild/openbsd-x64@0.21.5': + optional: true - /@types/trusted-types@2.0.3: - resolution: {integrity: sha512-NfQ4gyz38SL8sDNrSixxU2Os1a5xcdFxipAFxYEuLUlvU2uDwS4NUpsImcf1//SlWItCVMMLiylsxbmNMToV/g==} - dev: false + '@esbuild/openbsd-x64@0.24.2': + optional: true - /@types/web-bluetooth@0.0.17: - resolution: {integrity: sha512-4p9vcSmxAayx72yn70joFoL44c9MO/0+iVEBIQXe3v2h2SiAsEIo/G5v6ObFWvNKRFjbrVadNf9LqEEZeQPzdA==} - dev: false + '@esbuild/sunos-x64@0.21.5': + optional: true - /@vitejs/plugin-vue@4.2.3(vite@4.3.6)(vue@3.3.2): - resolution: {integrity: sha512-R6JDUfiZbJA9cMiguQ7jxALsgiprjBeHL5ikpXfJCH62pPHtI+JdJ5xWj6Ev73yXSlYl86+blXn1kZHQ7uElxw==} - engines: {node: ^14.18.0 || >=16.0.0} - peerDependencies: - vite: ^4.0.0 - vue: ^3.2.25 - dependencies: - vite: 4.3.6 - vue: 3.3.2 - dev: false + '@esbuild/sunos-x64@0.24.2': + optional: true - /@vue/compiler-core@3.3.2: - resolution: {integrity: sha512-CKZWo1dzsQYTNTft7whzjL0HsrEpMfiK7pjZ2WFE3bC1NA7caUjWioHSK+49y/LK7Bsm4poJZzAMnvZMQ7OTeg==} - dependencies: - '@babel/parser': 7.21.8 - '@vue/shared': 3.3.2 - estree-walker: 2.0.2 - source-map-js: 1.0.2 - dev: false + '@esbuild/win32-arm64@0.21.5': + optional: true - /@vue/compiler-dom@3.3.2: - resolution: {integrity: sha512-6gS3auANuKXLw0XH6QxkWqyPYPunziS2xb6VRenM3JY7gVfZcJvkCBHkb5RuNY1FCbBO3lkIi0CdXUCW1c7SXw==} - dependencies: - '@vue/compiler-core': 3.3.2 - '@vue/shared': 3.3.2 - dev: false + '@esbuild/win32-arm64@0.24.2': + optional: true - /@vue/compiler-sfc@3.3.2: - resolution: {integrity: sha512-jG4jQy28H4BqzEKsQqqW65BZgmo3vzdLHTBjF+35RwtDdlFE+Fk1VWJYUnDMMqkFBo6Ye1ltSKVOMPgkzYj7SQ==} - dependencies: - '@babel/parser': 7.21.8 - '@vue/compiler-core': 3.3.2 - '@vue/compiler-dom': 3.3.2 - '@vue/compiler-ssr': 3.3.2 - '@vue/reactivity-transform': 3.3.2 - '@vue/shared': 3.3.2 - estree-walker: 2.0.2 - magic-string: 0.30.0 - postcss: 8.4.23 - source-map-js: 1.0.2 - dev: false + '@esbuild/win32-ia32@0.21.5': + optional: true - /@vue/compiler-ssr@3.3.2: - resolution: {integrity: sha512-K8OfY5FQtZaSOJHHe8xhEfIfLrefL/Y9frv4k4NsyQL3+0lRKxr9QuJhfdBDjkl7Fhz8CzKh63mULvmOfx3l2w==} - dependencies: - '@vue/compiler-dom': 3.3.2 - '@vue/shared': 3.3.2 - dev: false + '@esbuild/win32-ia32@0.24.2': + optional: true - /@vue/devtools-api@6.5.0: - resolution: {integrity: sha512-o9KfBeaBmCKl10usN4crU53fYtC1r7jJwdGKjPT24t348rHxgfpZ0xL3Xm/gLUYnc0oTp8LAmrxOeLyu6tbk2Q==} - dev: false + '@esbuild/win32-x64@0.21.5': + optional: true - /@vue/reactivity-transform@3.3.2: - resolution: {integrity: sha512-iu2WaQvlJHdnONrsyv4ibIEnSsuKF+aHFngGj/y1lwpHQtalpVhKg9wsKMoiKXS9zPNjG9mNKzJS9vudvjzvyg==} - dependencies: - '@babel/parser': 7.21.8 - '@vue/compiler-core': 3.3.2 - '@vue/shared': 3.3.2 - estree-walker: 2.0.2 - magic-string: 0.30.0 - dev: false + '@esbuild/win32-x64@0.24.2': + optional: true - /@vue/reactivity@3.3.2: - resolution: {integrity: sha512-yX8C4uTgg2Tdj+512EEMnMKbLveoITl7YdQX35AYgx8vBvQGszKiiCN46g4RY6/deeo/5DLbeUUGxCq1qWMf5g==} + '@isaacs/cliui@8.0.2': dependencies: - '@vue/shared': 3.3.2 - dev: false + string-width: 5.1.2 + string-width-cjs: string-width@4.2.3 + strip-ansi: 7.1.0 + strip-ansi-cjs: strip-ansi@6.0.1 + wrap-ansi: 8.1.0 + wrap-ansi-cjs: wrap-ansi@7.0.0 + optional: true - /@vue/repl@1.4.1(vue@3.3.2): - resolution: {integrity: sha512-7ONz/o1OtS611jW6SdAOZXn4HdN8gfyatcOzcEu+3bDMvgbyr7ZUcbRV6Y4xdkxDARKDBzs+sb3/oz1Na5hAeQ==} - peerDependencies: - vue: ^3.2.13 - dependencies: - vue: 3.3.2 - dev: false + '@jridgewell/sourcemap-codec@1.5.0': {} - /@vue/runtime-core@3.3.2: - resolution: {integrity: sha512-qSl95qj0BvKfcsO+hICqFEoLhJn6++HtsPxmTkkadFbuhe3uQfJ8HmQwvEr7xbxBd2rcJB6XOJg7nWAn/ymC5A==} - dependencies: - '@vue/reactivity': 3.3.2 - '@vue/shared': 3.3.2 - dev: false + '@lit-labs/ssr-dom-shim@1.3.0': {} - /@vue/runtime-dom@3.3.2: - resolution: {integrity: sha512-+drStsJT+0mtgHdarT7cXZReCcTFfm6ptxMrz0kAW5hms6UNBd8Q1pi4JKlncAhu+Ld/TevsSp7pqAZxBBoGng==} + '@lit/reactive-element@2.0.4': dependencies: - '@vue/runtime-core': 3.3.2 - '@vue/shared': 3.3.2 - csstype: 3.1.2 - dev: false - - /@vue/server-renderer@3.3.2(vue@3.3.2): - resolution: {integrity: sha512-QCwh6OGwJg6GDLE0fbQhRTR6tnU+XDJ1iCsTYHXBiezCXAhqMygFRij7BiLF4ytvvHcg5kX9joX5R5vP85++wg==} - peerDependencies: - vue: 3.3.2 - dependencies: - '@vue/compiler-ssr': 3.3.2 - '@vue/shared': 3.3.2 - vue: 3.3.2 - dev: false - - /@vue/shared@3.3.2: - resolution: {integrity: sha512-0rFu3h8JbclbnvvKrs7Fe5FNGV9/5X2rPD7KmOzhLSUAiQH5//Hq437Gv0fR5Mev3u/nbtvmLl8XgwCU20/ZfQ==} - dev: false - - /@vuepress/bundler-vite@2.0.0-beta.62: - resolution: {integrity: sha512-Dpb4rJycssM1gs3MlQ5z0cwQ0KCx9Iliojt+qs5lVIUHP9vfw6ANYx51R3ojctt3dCoWfC4bAL4NhGQndGKvrQ==} - dependencies: - '@vitejs/plugin-vue': 4.2.3(vite@4.3.6)(vue@3.3.2) - '@vuepress/client': 2.0.0-beta.62 - '@vuepress/core': 2.0.0-beta.62 - '@vuepress/shared': 2.0.0-beta.62 - '@vuepress/utils': 2.0.0-beta.62 - autoprefixer: 10.4.14(postcss@8.4.23) - connect-history-api-fallback: 2.0.0 - postcss: 8.4.23 - postcss-load-config: 4.0.1(postcss@8.4.23) - rollup: 3.21.7 - vite: 4.3.6 - vue: 3.3.2 - vue-router: 4.2.0(vue@3.3.2) - transitivePeerDependencies: - - '@types/node' - - less - - sass - - stylus - - sugarss - - supports-color - - terser - - ts-node - dev: false + '@lit-labs/ssr-dom-shim': 1.3.0 - /@vuepress/cli@2.0.0-beta.62: - resolution: {integrity: sha512-z5mpxORVSZUWsSGtA0bqvsd4vhMDWXAGnQfHjYZ5ylUgnYMxBZMRWrQcpz9doMCk5Qkn56B2s2jKZEvhyFvdAg==} - hasBin: true + '@mapbox/node-pre-gyp@1.0.11(encoding@0.1.13)': dependencies: - '@vuepress/core': 2.0.0-beta.62 - '@vuepress/shared': 2.0.0-beta.62 - '@vuepress/utils': 2.0.0-beta.62 - cac: 6.7.14 - chokidar: 3.5.3 - envinfo: 7.8.1 - esbuild: 0.17.19 + detect-libc: 2.0.3 + https-proxy-agent: 5.0.1 + make-dir: 3.1.0 + node-fetch: 2.7.0(encoding@0.1.13) + nopt: 5.0.0 + npmlog: 5.0.1 + rimraf: 3.0.2 + semver: 7.6.3 + tar: 6.2.1 transitivePeerDependencies: + - encoding - supports-color - dev: false + optional: true - /@vuepress/client@2.0.0-beta.62: - resolution: {integrity: sha512-5JT0H6EibhZMVmg1fel2BWFFaAEv5zOoD397LOiMQmcEuUneeKNSwGcLrJDyvv8AOXz4wsXwET/to3TsOFoHDQ==} + '@mdit-vue/plugin-component@2.1.3': dependencies: - '@vue/devtools-api': 6.5.0 - '@vuepress/shared': 2.0.0-beta.62 - vue: 3.3.2 - vue-router: 4.2.0(vue@3.3.2) - dev: false + '@types/markdown-it': 14.1.2 + markdown-it: 14.1.0 - /@vuepress/core@2.0.0-beta.62: - resolution: {integrity: sha512-IyL1lxkRg2PO6oFDcioa5YKckKO8jEIwPaNG4mwv7bIEwaN5kpsROVtBeYHKkcnncWQMrbBG/z8aHDvjO/vFJA==} + '@mdit-vue/plugin-frontmatter@2.1.3': dependencies: - '@vuepress/client': 2.0.0-beta.62 - '@vuepress/markdown': 2.0.0-beta.62 - '@vuepress/shared': 2.0.0-beta.62 - '@vuepress/utils': 2.0.0-beta.62 - vue: 3.3.2 - transitivePeerDependencies: - - supports-color - dev: false - - /@vuepress/markdown@2.0.0-beta.62: - resolution: {integrity: sha512-OTGSHDALEE1zgAJgx9Py1AKR1JA/eLTjw63ul77ymt/5eNlU8/EVJg8Pj2nwL3cpvCpzB6sQ1Xkj4TF7D0aD1Q==} - dependencies: - '@mdit-vue/plugin-component': 0.12.0 - '@mdit-vue/plugin-frontmatter': 0.12.0 - '@mdit-vue/plugin-headers': 0.12.0 - '@mdit-vue/plugin-sfc': 0.12.0 - '@mdit-vue/plugin-title': 0.12.0 - '@mdit-vue/plugin-toc': 0.12.0 - '@mdit-vue/shared': 0.12.0 - '@mdit-vue/types': 0.12.0 - '@types/markdown-it': 12.2.3 - '@types/markdown-it-emoji': 2.0.2 - '@vuepress/shared': 2.0.0-beta.62 - '@vuepress/utils': 2.0.0-beta.62 - markdown-it: 13.0.1 - markdown-it-anchor: 8.6.7(@types/markdown-it@12.2.3)(markdown-it@13.0.1) - markdown-it-emoji: 2.0.2 - mdurl: 1.0.1 - transitivePeerDependencies: - - supports-color - dev: false + '@mdit-vue/types': 2.1.0 + '@types/markdown-it': 14.1.2 + gray-matter: 4.0.3 + markdown-it: 14.1.0 - /@vuepress/plugin-active-header-links@2.0.0-beta.62: - resolution: {integrity: sha512-NUoa0JP2npSydJQvM1oOPEtPCKRmtqpkPLxTeBCP6ucR/eHpCbBMrgYt3w6kdmMJykc/AWFd4oZA1QS/MAoEtw==} + '@mdit-vue/plugin-headers@2.1.3': dependencies: - '@vuepress/client': 2.0.0-beta.62 - '@vuepress/core': 2.0.0-beta.62 - '@vuepress/utils': 2.0.0-beta.62 - ts-debounce: 4.0.0 - vue: 3.3.2 - vue-router: 4.2.0(vue@3.3.2) - transitivePeerDependencies: - - supports-color - dev: false + '@mdit-vue/shared': 2.1.3 + '@mdit-vue/types': 2.1.0 + '@types/markdown-it': 14.1.2 + markdown-it: 14.1.0 - /@vuepress/plugin-back-to-top@2.0.0-beta.62: - resolution: {integrity: sha512-ndStdKobpq7/YxhtUg2YrSkd8FNoE0v4pPVdTBND6jlkPns4CCcyu+w6BZ8mkiiB2dzS27JrhKcXHz1Tsb0nUA==} + '@mdit-vue/plugin-sfc@2.1.3': dependencies: - '@vuepress/client': 2.0.0-beta.62 - '@vuepress/core': 2.0.0-beta.62 - '@vuepress/utils': 2.0.0-beta.62 - ts-debounce: 4.0.0 - vue: 3.3.2 - transitivePeerDependencies: - - supports-color - dev: false - - /@vuepress/plugin-container@2.0.0-beta.62: - resolution: {integrity: sha512-ibo0J8ye5KA6zkwIttkVqleSLy4Sq0rcSW+X8cTzyFfoKKs0Y+ECjmf4wRrDl79m+lgpA43mlFpCcbgtmV9aqw==} - dependencies: - '@types/markdown-it': 12.2.3 - '@vuepress/core': 2.0.0-beta.62 - '@vuepress/markdown': 2.0.0-beta.62 - '@vuepress/shared': 2.0.0-beta.62 - '@vuepress/utils': 2.0.0-beta.62 - markdown-it: 13.0.1 - markdown-it-container: 3.0.0 - transitivePeerDependencies: - - supports-color - dev: false + '@mdit-vue/types': 2.1.0 + '@types/markdown-it': 14.1.2 + markdown-it: 14.1.0 - /@vuepress/plugin-external-link-icon@2.0.0-beta.62: - resolution: {integrity: sha512-mQ7gj6pMHYCp7zk6N92omxUz9CjKYZtvZXkNmsloZsz0hiGS1SdG29vLo8yKm/qVzyu9F45WgVNcdQD5mkzx3Q==} + '@mdit-vue/plugin-title@2.1.3': dependencies: - '@vuepress/client': 2.0.0-beta.62 - '@vuepress/core': 2.0.0-beta.62 - '@vuepress/markdown': 2.0.0-beta.62 - '@vuepress/shared': 2.0.0-beta.62 - '@vuepress/utils': 2.0.0-beta.62 - vue: 3.3.2 - transitivePeerDependencies: - - supports-color - dev: false + '@mdit-vue/shared': 2.1.3 + '@mdit-vue/types': 2.1.0 + '@types/markdown-it': 14.1.2 + markdown-it: 14.1.0 - /@vuepress/plugin-git@2.0.0-beta.62: - resolution: {integrity: sha512-vTYUguI+X5G7JPTySDnZ6lcAGXBWlD1Nsw9IV42Hh4fvevWzZ3WIjkAhjZpdURIz+xQPEZBbgqnOKjBpbPx0jA==} + '@mdit-vue/plugin-toc@2.1.3': dependencies: - '@vuepress/core': 2.0.0-beta.62 - '@vuepress/utils': 2.0.0-beta.62 - execa: 7.1.1 - transitivePeerDependencies: - - supports-color - dev: false + '@mdit-vue/shared': 2.1.3 + '@mdit-vue/types': 2.1.0 + '@types/markdown-it': 14.1.2 + markdown-it: 14.1.0 - /@vuepress/plugin-medium-zoom@2.0.0-beta.62: - resolution: {integrity: sha512-1BolO1OE9Dxf4xLpEDEYjWTmx+luD6RSwjM+Wbgp7gBMK98yY8N9rHxWCzhLWbTffVezmAO0ze37l7hVd4ypTA==} + '@mdit-vue/shared@2.1.3': dependencies: - '@vuepress/client': 2.0.0-beta.62 - '@vuepress/core': 2.0.0-beta.62 - '@vuepress/utils': 2.0.0-beta.62 - medium-zoom: 1.0.8 - vue: 3.3.2 - transitivePeerDependencies: - - supports-color - dev: false + '@mdit-vue/types': 2.1.0 + '@types/markdown-it': 14.1.2 + markdown-it: 14.1.0 - /@vuepress/plugin-nprogress@2.0.0-beta.62: - resolution: {integrity: sha512-w1Qqw1pP7+fXN+Aznmbfdp62XnQJ2s/FJyoGfV7LjVfV+gWFtqymiJiahvd2aQpBi4/qZNvtFJ1SOQf5tn1CxA==} - dependencies: - '@vuepress/client': 2.0.0-beta.62 - '@vuepress/core': 2.0.0-beta.62 - '@vuepress/utils': 2.0.0-beta.62 - vue: 3.3.2 - vue-router: 4.2.0(vue@3.3.2) - transitivePeerDependencies: - - supports-color - dev: false + '@mdit-vue/types@2.1.0': {} - /@vuepress/plugin-palette@2.0.0-beta.62: - resolution: {integrity: sha512-Tw+KFxC8c3KIGeXANqMXFLoQ96ZQ/hJaKC0qm6iN04Wk9hKYazkxhPZTAZkOG3SrxaxvOrgnzvicpci6FJgnGA==} + '@mdit/helper@0.16.0(markdown-it@14.1.0)': dependencies: - '@vuepress/core': 2.0.0-beta.62 - '@vuepress/utils': 2.0.0-beta.62 - chokidar: 3.5.3 - transitivePeerDependencies: - - supports-color - dev: false + '@types/markdown-it': 14.1.2 + optionalDependencies: + markdown-it: 14.1.0 - /@vuepress/plugin-prismjs@2.0.0-beta.62: - resolution: {integrity: sha512-kPrlh+I4w+YyU6joahjvuMo2zMwbpB36drZYfjXtYFFIxpBQ5Xdse4xx89vYOX0KqckOQrNa/tnYnfBuHBkgAQ==} + '@mdit/plugin-alert@0.16.0(markdown-it@14.1.0)': dependencies: - '@vuepress/core': 2.0.0-beta.62 - prismjs: 1.29.0 - transitivePeerDependencies: - - supports-color - dev: false + '@types/markdown-it': 14.1.2 + optionalDependencies: + markdown-it: 14.1.0 - /@vuepress/plugin-theme-data@2.0.0-beta.62: - resolution: {integrity: sha512-q6XHIDnZcJ5W55TlynKrwHtHormZedEY5man9zT4hlZywr3vVBgToHztObNTqgn6CssFaW2BFXDlW17iyS2D2A==} + '@mdit/plugin-align@0.16.0(markdown-it@14.1.0)': dependencies: - '@vue/devtools-api': 6.5.0 - '@vuepress/client': 2.0.0-beta.62 - '@vuepress/core': 2.0.0-beta.62 - '@vuepress/shared': 2.0.0-beta.62 - '@vuepress/utils': 2.0.0-beta.62 - vue: 3.3.2 - transitivePeerDependencies: - - supports-color - dev: false + '@mdit/plugin-container': 0.16.0(markdown-it@14.1.0) + '@types/markdown-it': 14.1.2 + optionalDependencies: + markdown-it: 14.1.0 - /@vuepress/shared@2.0.0-beta.62: - resolution: {integrity: sha512-+OH8WzFz7+IUv+WbcBbCiy3ZTWZ4a2uVRd4GYHWkTE4Ux5V2Sx3KwY17POIGpn/PfMqNHHtjpDH6rO7qmaD+pg==} + '@mdit/plugin-attrs@0.16.2(markdown-it@14.1.0)': dependencies: - '@mdit-vue/types': 0.12.0 - '@vue/shared': 3.3.2 - dev: false + '@mdit/helper': 0.16.0(markdown-it@14.1.0) + '@types/markdown-it': 14.1.2 + optionalDependencies: + markdown-it: 14.1.0 - /@vuepress/theme-default@2.0.0-beta.62: - resolution: {integrity: sha512-J6wLH4tevMnn/2y+MrTpZEVDWf5yvikx0S9TIfpcxjR/nN4XD9eSZrMB3Lt8JqTW/lwFze5MVBdTtVafZs4b3g==} - peerDependencies: - sass-loader: ^13.2.1 - peerDependenciesMeta: - sass-loader: - optional: true + '@mdit/plugin-container@0.16.0(markdown-it@14.1.0)': dependencies: - '@vuepress/client': 2.0.0-beta.62 - '@vuepress/core': 2.0.0-beta.62 - '@vuepress/plugin-active-header-links': 2.0.0-beta.62 - '@vuepress/plugin-back-to-top': 2.0.0-beta.62 - '@vuepress/plugin-container': 2.0.0-beta.62 - '@vuepress/plugin-external-link-icon': 2.0.0-beta.62 - '@vuepress/plugin-git': 2.0.0-beta.62 - '@vuepress/plugin-medium-zoom': 2.0.0-beta.62 - '@vuepress/plugin-nprogress': 2.0.0-beta.62 - '@vuepress/plugin-palette': 2.0.0-beta.62 - '@vuepress/plugin-prismjs': 2.0.0-beta.62 - '@vuepress/plugin-theme-data': 2.0.0-beta.62 - '@vuepress/shared': 2.0.0-beta.62 - '@vuepress/utils': 2.0.0-beta.62 - '@vueuse/core': 10.1.2(vue@3.3.2) - sass: 1.62.1 - vue: 3.3.2 - vue-router: 4.2.0(vue@3.3.2) - transitivePeerDependencies: - - '@vue/composition-api' - - supports-color - dev: false - - /@vuepress/utils@2.0.0-beta.62: - resolution: {integrity: sha512-2hyGGrN1XCUapsSlckHc7FWkklSPZfqcM5eDYjxyIT9XpQrXKYn8r0CUVcveyFdHF76Tw0KyP57JCxUDTxHxVg==} - dependencies: - '@types/debug': 4.1.7 - '@types/fs-extra': 11.0.1 - '@types/hash-sum': 1.0.0 - '@vuepress/shared': 2.0.0-beta.62 - debug: 4.3.4 - fs-extra: 11.1.1 - globby: 13.1.4 - hash-sum: 2.0.0 - ora: 6.3.1 - picocolors: 1.0.0 - upath: 2.0.1 - transitivePeerDependencies: - - supports-color - dev: false + '@types/markdown-it': 14.1.2 + optionalDependencies: + markdown-it: 14.1.0 - /@vueuse/core@10.1.2(vue@3.3.2): - resolution: {integrity: sha512-roNn8WuerI56A5uiTyF/TEYX0Y+VKlhZAF94unUfdhbDUI+NfwQMn4FUnUscIRUhv3344qvAghopU4bzLPNFlA==} + '@mdit/plugin-demo@0.16.0(markdown-it@14.1.0)': dependencies: - '@types/web-bluetooth': 0.0.17 - '@vueuse/metadata': 10.1.2 - '@vueuse/shared': 10.1.2(vue@3.3.2) - vue-demi: 0.14.1(vue@3.3.2) - transitivePeerDependencies: - - '@vue/composition-api' - - vue - dev: false - - /@vueuse/metadata@10.1.2: - resolution: {integrity: sha512-3mc5BqN9aU2SqBeBuWE7ne4OtXHoHKggNgxZR2K+zIW4YLsy6xoZ4/9vErQs6tvoKDX6QAqm3lvsrv0mczAwIQ==} - dev: false + '@types/markdown-it': 14.1.2 + optionalDependencies: + markdown-it: 14.1.0 - /@vueuse/shared@10.1.2(vue@3.3.2): - resolution: {integrity: sha512-1uoUTPBlgyscK9v6ScGeVYDDzlPSFXBlxuK7SfrDGyUTBiznb3mNceqhwvZHjtDRELZEN79V5uWPTF1VDV8svA==} + '@mdit/plugin-figure@0.16.0(markdown-it@14.1.0)': dependencies: - vue-demi: 0.14.1(vue@3.3.2) - transitivePeerDependencies: - - '@vue/composition-api' - - vue - dev: false + '@types/markdown-it': 14.1.2 + optionalDependencies: + markdown-it: 14.1.0 - /@waline/client@2.15.4: - resolution: {integrity: sha512-wH5lO4gTlF9SSuKCNC5fDnk4brzZDn4u+bBpgwEyu0Tz0deD59hW6ttMizXAZUi5CGT2Me6XqwIVE4WWRBBs6A==} - engines: {node: '>=14'} + '@mdit/plugin-footnote@0.16.0(markdown-it@14.1.0)': dependencies: - '@vueuse/core': 10.1.2(vue@3.3.2) - autosize: 6.0.1 - marked: 4.3.0 - vue: 3.3.2 - transitivePeerDependencies: - - '@vue/composition-api' - dev: false - - /abortcontroller-polyfill@1.7.5: - resolution: {integrity: sha512-JMJ5soJWP18htbbxJjG7bG6yuI6pRhgJ0scHHTfkUjf6wjP912xZWvM+A4sJK3gqd9E8fcPbDnOefbA9Th/FIQ==} - dev: false - - /acorn@8.8.2: - resolution: {integrity: sha512-xjIYgE8HBrkpd/sJqOGNspf8uHG+NOHGOw6a/Urj8taM2EXfdNAH2oFcPeIFfsv3+kz/mJrS5VuMqbNLjCa2vw==} - engines: {node: '>=0.4.0'} - hasBin: true - dev: false + '@types/markdown-it': 14.1.2 + markdown-it: 14.1.0 - /ajv@8.12.0: - resolution: {integrity: sha512-sRu1kpcO9yLtYxBKvqfTeh9KzZEwO3STyX1HT+4CaDzC6HpTGYhIhPIzj9XuKU7KYDwnaeh5hcOwjy1QuJzBPA==} + '@mdit/plugin-icon@0.16.5(markdown-it@14.1.0)': dependencies: - fast-deep-equal: 3.1.3 - json-schema-traverse: 1.0.0 - require-from-string: 2.0.2 - uri-js: 4.4.1 - dev: false - - /ansi-regex@5.0.1: - resolution: {integrity: sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==} - engines: {node: '>=8'} - dev: false - - /ansi-regex@6.0.1: - resolution: {integrity: sha512-n5M855fKb2SsfMIiFFoVrABHJC8QtHwVx+mHWP3QcEqBHYienj5dHSgjbxtC0WEZXYt4wcD6zrQElDPhFuZgfA==} - engines: {node: '>=12'} - dev: false + '@mdit/helper': 0.16.0(markdown-it@14.1.0) + '@types/markdown-it': 14.1.2 + optionalDependencies: + markdown-it: 14.1.0 - /ansi-styles@3.2.1: - resolution: {integrity: sha512-VT0ZI6kZRdTh8YyJw3SMbYm/u+NqfsAxEpWO0Pf9sq8/e94WxxOpPKx9FR1FlyCtOVDNOQ+8ntlqFxiRc+r5qA==} - engines: {node: '>=4'} + '@mdit/plugin-img-lazyload@0.16.0(markdown-it@14.1.0)': dependencies: - color-convert: 1.9.3 - dev: false + '@types/markdown-it': 14.1.2 + optionalDependencies: + markdown-it: 14.1.0 - /ansi-styles@4.3.0: - resolution: {integrity: sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==} - engines: {node: '>=8'} + '@mdit/plugin-img-mark@0.16.0(markdown-it@14.1.0)': dependencies: - color-convert: 2.0.1 - dev: false - - /ansi-styles@6.2.1: - resolution: {integrity: sha512-bN798gFfQX+viw3R7yrGWRqnrN2oRkEkUjjl4JNn4E8GxxbjtG3FbrEIIY3l8/hrwUwIeCZvi4QuOTP4MErVug==} - engines: {node: '>=12'} - dev: false + '@types/markdown-it': 14.1.2 + optionalDependencies: + markdown-it: 14.1.0 - /anymatch@3.1.3: - resolution: {integrity: sha512-KMReFUr0B4t+D+OBkjR3KYqvocp2XaSzO55UcB6mgQMd3KbcE+mWTyvVV7D/zsdEbNnV6acZUutkiHQXvTr1Rw==} - engines: {node: '>= 8'} + '@mdit/plugin-img-size@0.16.0(markdown-it@14.1.0)': dependencies: - normalize-path: 3.0.0 - picomatch: 2.3.1 - dev: false - - /arg@5.0.2: - resolution: {integrity: sha512-PYjyFOLKQ9y57JvQ6QLo8dAgNqswh8M1RMJYdQduT6xbWSgK36P/Z/v+p888pM69jMMfS8Xd8F6I1kQ/I9HUGg==} - dev: false + '@types/markdown-it': 14.1.2 + optionalDependencies: + markdown-it: 14.1.0 - /argparse@1.0.10: - resolution: {integrity: sha512-o5Roy6tNG4SL/FOkCAN6RzjiakZS25RLYFrcMttJqbdd8BWrnA+fGz57iN5Pb06pvBGvl5gQ0B48dJlslXvoTg==} + '@mdit/plugin-include@0.16.0(markdown-it@14.1.0)': dependencies: - sprintf-js: 1.0.3 - dev: false - - /argparse@2.0.1: - resolution: {integrity: sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q==} - dev: false + '@mdit/helper': 0.16.0(markdown-it@14.1.0) + '@types/markdown-it': 14.1.2 + upath: 2.0.1 + optionalDependencies: + markdown-it: 14.1.0 - /array-buffer-byte-length@1.0.0: - resolution: {integrity: sha512-LPuwb2P+NrQw3XhxGc36+XSvuBPopovXYTR9Ew++Du9Yb/bx5AzBfrIsBoj0EZUifjQU+sHL21sseZ3jerWO/A==} + '@mdit/plugin-katex-slim@0.16.2(katex@0.16.20)(markdown-it@14.1.0)': dependencies: - call-bind: 1.0.2 - is-array-buffer: 3.0.2 - dev: false + '@mdit/helper': 0.16.0(markdown-it@14.1.0) + '@mdit/plugin-tex': 0.16.0(markdown-it@14.1.0) + '@types/markdown-it': 14.1.2 + optionalDependencies: + katex: 0.16.20 + markdown-it: 14.1.0 - /artalk@2.5.5: - resolution: {integrity: sha512-35Dq9sOquQQlxvqZhRtZMp9g2SS94ryNEfSJy9BP9yq005CZoC64DTCBI2WBG+pd9YenihfB5QWwa+jfCZ/5Ew==} + '@mdit/plugin-mark@0.16.0(markdown-it@14.1.0)': dependencies: - abortcontroller-polyfill: 1.7.5 - hanabi: 0.4.0 - insane: 2.6.2 - marked: 5.0.2 - dev: false + '@types/markdown-it': 14.1.2 + optionalDependencies: + markdown-it: 14.1.0 - /artplayer@5.0.9: - resolution: {integrity: sha512-IM/DShYdmKFEA9jl08LYbTK2Jfz9s7qIjEH0xWjnxvVArUKZZKcoqwr6i54U0c4grtc/Uvb4wtCd78kvtSVlgw==} + '@mdit/plugin-mathjax-slim@0.16.0(markdown-it@14.1.0)(mathjax-full@3.2.2)': dependencies: - option-validator: 2.0.6 - dev: false - - /assignment@2.0.0: - resolution: {integrity: sha512-naMULXjtgCs9SVUEtyvJNt68aF18em7/W+dhbR59kbz9cXWPEvUkCun2tqlgqRPSqZaKPpqLc5ZnwL8jVmJRvw==} - dev: false + '@mdit/plugin-tex': 0.16.0(markdown-it@14.1.0) + '@types/markdown-it': 14.1.2 + upath: 2.0.1 + optionalDependencies: + markdown-it: 14.1.0 + mathjax-full: 3.2.2 - /async@3.2.4: - resolution: {integrity: sha512-iAB+JbDEGXhyIUavoDl9WP/Jj106Kz9DEn1DPgYw5ruDn0e3Wgi3sKFm55sASdGBNOQB8F59d9qQ7deqrHA8wQ==} - dev: false + '@mdit/plugin-plantuml@0.16.0(markdown-it@14.1.0)': + dependencies: + '@mdit/plugin-uml': 0.16.0(markdown-it@14.1.0) + '@types/markdown-it': 14.1.2 + optionalDependencies: + markdown-it: 14.1.0 - /at-least-node@1.0.0: - resolution: {integrity: sha512-+q/t7Ekv1EDY2l6Gda6LLiX14rU9TV20Wa3ofeQmwPFZbOMo9DXrLbOjFaaclkXKWidIaopwAObQDqwWtGUjqg==} - engines: {node: '>= 4.0.0'} - dev: false + '@mdit/plugin-spoiler@0.16.0(markdown-it@14.1.0)': + dependencies: + '@types/markdown-it': 14.1.2 + optionalDependencies: + markdown-it: 14.1.0 - /autoprefixer@10.4.14(postcss@8.4.23): - resolution: {integrity: sha512-FQzyfOsTlwVzjHxKEqRIAdJx9niO6VCBCoEwax/VLSoQF29ggECcPuBqUMZ+u8jCZOPSy8b8/8KnuFbp0SaFZQ==} - engines: {node: ^10 || ^12 || >=14} - hasBin: true - peerDependencies: - postcss: ^8.1.0 + '@mdit/plugin-stylize@0.16.0(markdown-it@14.1.0)': dependencies: - browserslist: 4.21.5 - caniuse-lite: 1.0.30001487 - fraction.js: 4.2.0 - normalize-range: 0.1.2 - picocolors: 1.0.0 - postcss: 8.4.23 - postcss-value-parser: 4.2.0 - dev: false + '@types/markdown-it': 14.1.2 + optionalDependencies: + markdown-it: 14.1.0 - /autosize@6.0.1: - resolution: {integrity: sha512-f86EjiUKE6Xvczc4ioP1JBlWG7FKrE13qe/DxBCpe8GCipCq2nFw73aO8QEBKHfSbYGDN5eB9jXWKen7tspDqQ==} - dev: false + '@mdit/plugin-sub@0.16.0(markdown-it@14.1.0)': + dependencies: + '@mdit/helper': 0.16.0(markdown-it@14.1.0) + '@types/markdown-it': 14.1.2 + optionalDependencies: + markdown-it: 14.1.0 - /available-typed-arrays@1.0.5: - resolution: {integrity: sha512-DMD0KiN46eipeziST1LPP/STfDU0sufISXmjSgvVsoU2tqxctQeASejWcfNtxYKqETM1UxQ8sp2OrSBWpHY6sw==} - engines: {node: '>= 0.4'} - dev: false + '@mdit/plugin-sup@0.16.0(markdown-it@14.1.0)': + dependencies: + '@mdit/helper': 0.16.0(markdown-it@14.1.0) + '@types/markdown-it': 14.1.2 + optionalDependencies: + markdown-it: 14.1.0 - /babel-plugin-polyfill-corejs2@0.3.3(@babel/core@7.21.8): - resolution: {integrity: sha512-8hOdmFYFSZhqg2C/JgLUQ+t52o5nirNwaWM2B9LWteozwIvM14VSwdsCAUET10qT+kmySAlseadmfeeSWFCy+Q==} - peerDependencies: - '@babel/core': ^7.0.0-0 + '@mdit/plugin-tab@0.16.0(markdown-it@14.1.0)': dependencies: - '@babel/compat-data': 7.21.7 - '@babel/core': 7.21.8 - '@babel/helper-define-polyfill-provider': 0.3.3(@babel/core@7.21.8) - semver: 6.3.0 - transitivePeerDependencies: - - supports-color - dev: false + '@mdit/helper': 0.16.0(markdown-it@14.1.0) + '@types/markdown-it': 14.1.2 + optionalDependencies: + markdown-it: 14.1.0 - /babel-plugin-polyfill-corejs3@0.6.0(@babel/core@7.21.8): - resolution: {integrity: sha512-+eHqR6OPcBhJOGgsIar7xoAB1GcSwVUA3XjAd7HJNzOXT4wv6/H7KIdA/Nc60cvUlDbKApmqNvD1B1bzOt4nyA==} - peerDependencies: - '@babel/core': ^7.0.0-0 + '@mdit/plugin-tasklist@0.16.0(markdown-it@14.1.0)': dependencies: - '@babel/core': 7.21.8 - '@babel/helper-define-polyfill-provider': 0.3.3(@babel/core@7.21.8) - core-js-compat: 3.30.2 - transitivePeerDependencies: - - supports-color - dev: false + '@types/markdown-it': 14.1.2 + optionalDependencies: + markdown-it: 14.1.0 - /babel-plugin-polyfill-regenerator@0.4.1(@babel/core@7.21.8): - resolution: {integrity: sha512-NtQGmyQDXjQqQ+IzRkBVwEOz9lQ4zxAQZgoAYEtU9dJjnl1Oc98qnN7jcp+bE7O7aYzVpavXE3/VKXNzUbh7aw==} - peerDependencies: - '@babel/core': ^7.0.0-0 + '@mdit/plugin-tex@0.16.0(markdown-it@14.1.0)': dependencies: - '@babel/core': 7.21.8 - '@babel/helper-define-polyfill-provider': 0.3.3(@babel/core@7.21.8) - transitivePeerDependencies: - - supports-color - dev: false + '@types/markdown-it': 14.1.2 + optionalDependencies: + markdown-it: 14.1.0 - /balanced-match@1.0.2: - resolution: {integrity: sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==} - dev: false + '@mdit/plugin-uml@0.16.0(markdown-it@14.1.0)': + dependencies: + '@mdit/helper': 0.16.0(markdown-it@14.1.0) + '@types/markdown-it': 14.1.2 + optionalDependencies: + markdown-it: 14.1.0 - /balloon-css@1.2.0: - resolution: {integrity: sha512-urXwkHgwp6GsXVF+it01485Z2Cj4pnW02ICnM0TemOlkKmCNnDLmyy+ZZiRXBpwldUXO+aRNr7Hdia4CBvXJ5A==} - dev: false + '@nodelib/fs.scandir@2.1.5': + dependencies: + '@nodelib/fs.stat': 2.0.5 + run-parallel: 1.2.0 - /base64-js@1.5.1: - resolution: {integrity: sha512-AKpaYlHn8t4SVbOHCy+b5+KKgvR4vrsD8vbvrbiQJps7fKDTkjkDry6ji0rUJjC0kzbNePLwzxq8iypo41qeWA==} - dev: false + '@nodelib/fs.stat@2.0.5': {} - /bcp-47-match@1.0.3: - resolution: {integrity: sha512-LggQ4YTdjWQSKELZF5JwchnBa1u0pIQSZf5lSdOHEdbVP55h0qICA/FUp3+W99q0xqxYa1ZQizTUH87gecII5w==} - dev: false + '@nodelib/fs.walk@1.2.8': + dependencies: + '@nodelib/fs.scandir': 2.1.5 + fastq: 1.18.0 - /bcp-47-normalize@1.1.1: - resolution: {integrity: sha512-jWZ1Jdu3cs0EZdfCkS0UE9Gg01PtxnChjEBySeB+Zo6nkqtFfnvtoQQgP1qU1Oo4qgJgxhTI6Sf9y/pZIhPs0A==} + '@npmcli/agent@2.2.2': dependencies: - bcp-47: 1.0.8 - bcp-47-match: 1.0.3 - dev: false + agent-base: 7.1.3 + http-proxy-agent: 7.0.2 + https-proxy-agent: 7.0.6 + lru-cache: 10.4.3 + socks-proxy-agent: 8.0.5 + transitivePeerDependencies: + - supports-color + optional: true - /bcp-47@1.0.8: - resolution: {integrity: sha512-Y9y1QNBBtYtv7hcmoX0tR+tUNSFZGZ6OL6vKPObq8BbOhkCoyayF6ogfLTgAli/KuAEbsYHYUNq2AQuY6IuLag==} + '@npmcli/fs@3.1.1': dependencies: - is-alphabetical: 1.0.4 - is-alphanumerical: 1.0.4 - is-decimal: 1.0.4 - dev: false + semver: 7.6.3 + optional: true - /bcrypt-ts@3.0.1: - resolution: {integrity: sha512-rRvmZZ+wAhsV8PA8bW+BQq5kWZzqmh20VdwqI19D2WdB7TLwNnYUB0wzFkp83WLvgpSqOF4L0w/uPr90Rao66g==} - dev: false + '@pkgjs/parseargs@0.11.0': + optional: true - /binary-extensions@2.2.0: - resolution: {integrity: sha512-jDctJ/IVQbZoJykoeHbhXpOlNBqGNcwXJKJog42E5HDPUwQTSdjCHdihjj0DlnheQ7blbT6dHOafNAiS8ooQKA==} - engines: {node: '>=8'} - dev: false + '@rollup/rollup-android-arm-eabi@4.30.1': + optional: true - /bl@5.1.0: - resolution: {integrity: sha512-tv1ZJHLfTDnXE6tMHv73YgSJaWR2AFuPwMntBe7XL/GBFHnT0CLnsHMogfk5+GzCDC5ZWarSCYaIGATZt9dNsQ==} - dependencies: - buffer: 6.0.3 - inherits: 2.0.4 - readable-stream: 3.6.2 - dev: false + '@rollup/rollup-android-arm64@4.30.1': + optional: true - /boolbase@1.0.0: - resolution: {integrity: sha512-JZOSA7Mo9sNGB8+UjSgzdLtokWAky1zbztM3WRLCbZ70/3cTANmQmOdR7y2g+J0e2WXywy1yS468tY+IruqEww==} - dev: false + '@rollup/rollup-darwin-arm64@4.30.1': + optional: true - /brace-expansion@1.1.11: - resolution: {integrity: sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA==} - dependencies: - balanced-match: 1.0.2 - concat-map: 0.0.1 - dev: false + '@rollup/rollup-darwin-x64@4.30.1': + optional: true - /brace-expansion@2.0.1: - resolution: {integrity: sha512-XnAIvQ8eM+kC6aULx6wuQiwVsnzsi9d3WxzV3FpWTGA19F621kwdbsAcFKXgKUHZWsy+mY6iL1sHTxWEFCytDA==} - dependencies: - balanced-match: 1.0.2 - dev: false + '@rollup/rollup-freebsd-arm64@4.30.1': + optional: true - /braces@3.0.2: - resolution: {integrity: sha512-b8um+L1RzM3WDSzvhm6gIz1yfTbBt6YTlcEKAvsmqCZZFw46z626lVj9j1yEPW33H5H+lBQpZMP1k8l+78Ha0A==} - engines: {node: '>=8'} - dependencies: - fill-range: 7.0.1 - dev: false + '@rollup/rollup-freebsd-x64@4.30.1': + optional: true - /browserslist@4.21.5: - resolution: {integrity: sha512-tUkiguQGW7S3IhB7N+c2MV/HZPSCPAAiYBZXLsBhFB/PCy6ZKKsZrmBayHV9fdGV/ARIfJ14NkxKzRDjvp7L6w==} - engines: {node: ^6 || ^7 || ^8 || ^9 || ^10 || ^11 || ^12 || >=13.7} - hasBin: true - dependencies: - caniuse-lite: 1.0.30001487 - electron-to-chromium: 1.4.396 - node-releases: 2.0.10 - update-browserslist-db: 1.0.11(browserslist@4.21.5) - dev: false + '@rollup/rollup-linux-arm-gnueabihf@4.30.1': + optional: true - /buffer-from@1.1.2: - resolution: {integrity: sha512-E+XQCRwSbaaiChtv6k6Dwgc+bx+Bs6vuKJHHl5kox/BaKbhiXzqQOwK4cO22yElGp2OCmjwVhT3HmxgyPGnJfQ==} - dev: false + '@rollup/rollup-linux-arm-musleabihf@4.30.1': + optional: true - /buffer@6.0.3: - resolution: {integrity: sha512-FTiCpNxtwiZZHEZbcbTIcZjERVICn9yq/pDFkTl95/AxzD1naBctN7YO68riM/gLSDY7sdrMby8hofADYuuqOA==} - dependencies: - base64-js: 1.5.1 - ieee754: 1.2.1 - dev: false + '@rollup/rollup-linux-arm64-gnu@4.30.1': + optional: true - /builtin-modules@3.3.0: - resolution: {integrity: sha512-zhaCDicdLuWN5UbN5IMnFqNMhNfo919sH85y2/ea+5Yg9TsTkeZxpL+JLbp6cgYFS4sRLp3YV4S6yDuqVWHYOw==} - engines: {node: '>=6'} - dev: false + '@rollup/rollup-linux-arm64-musl@4.30.1': + optional: true - /cac@6.7.14: - resolution: {integrity: sha512-b6Ilus+c3RrdDk+JhLKUAQfzzgLEPy6wcXqS7f/xe1EETvsDP6GORG7SFuOs6cID5YkqchW/LXZbX5bc8j7ZcQ==} - engines: {node: '>=8'} - dev: false + '@rollup/rollup-linux-loongarch64-gnu@4.30.1': + optional: true - /call-bind@1.0.2: - resolution: {integrity: sha512-7O+FbCihrB5WGbFYesctwmTKae6rOiIzmz1icreWJ+0aA7LJfuqhEso2T9ncpcFtzMQtzXf2QGGueWJGTYsqrA==} - dependencies: - function-bind: 1.1.1 - get-intrinsic: 1.2.1 - dev: false + '@rollup/rollup-linux-powerpc64le-gnu@4.30.1': + optional: true - /camelcase@5.3.1: - resolution: {integrity: sha512-L28STB170nwWS63UjtlEOE3dldQApaJXZkOI1uMFfzf3rRuPegHaHesyee+YxQ+W6SvRDQV6UrdOdRiR153wJg==} - engines: {node: '>=6'} - dev: false + '@rollup/rollup-linux-riscv64-gnu@4.30.1': + optional: true - /caniuse-lite@1.0.30001487: - resolution: {integrity: sha512-83564Z3yWGqXsh2vaH/mhXfEM0wX+NlBCm1jYHOb97TrTWJEmPTccZgeLTPBUUb0PNVo+oomb7wkimZBIERClA==} - dev: false + '@rollup/rollup-linux-s390x-gnu@4.30.1': + optional: true - /chalk@2.4.2: - resolution: {integrity: sha512-Mti+f9lpJNcwF4tWV8/OrTTtF1gZi+f8FqlyAdouralcFWFQWF2+NgCHShjkCb+IFBLq9buZwE1xckQU4peSuQ==} - engines: {node: '>=4'} - dependencies: - ansi-styles: 3.2.1 - escape-string-regexp: 1.0.5 - supports-color: 5.5.0 - dev: false + '@rollup/rollup-linux-x64-gnu@4.30.1': + optional: true - /chalk@4.1.2: - resolution: {integrity: sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==} - engines: {node: '>=10'} - dependencies: - ansi-styles: 4.3.0 - supports-color: 7.2.0 - dev: false + '@rollup/rollup-linux-x64-musl@4.30.1': + optional: true - /chalk@5.2.0: - resolution: {integrity: sha512-ree3Gqw/nazQAPuJJEy+avdl7QfZMcUvmHIKgEZkGL+xOBzRvup5Hxo6LHuMceSxOabuJLJm5Yp/92R9eMmMvA==} - engines: {node: ^12.17.0 || ^14.13 || >=16.0.0} - dev: false + '@rollup/rollup-win32-arm64-msvc@4.30.1': + optional: true - /chart.js@4.3.0: - resolution: {integrity: sha512-ynG0E79xGfMaV2xAHdbhwiPLczxnNNnasrmPEXriXsPJGjmhOBYzFVEsB65w2qMDz+CaBJJuJD0inE/ab/h36g==} - engines: {pnpm: '>=7'} - dependencies: - '@kurkle/color': 0.3.2 - dev: false + '@rollup/rollup-win32-ia32-msvc@4.30.1': + optional: true - /cheerio-select@2.1.0: - resolution: {integrity: sha512-9v9kG0LvzrlcungtnJtpGNxY+fzECQKhK4EGJX2vByejiMX84MFNQw4UxPJl3bFbTMw+Dfs37XaIkCwTZfLh4g==} - dependencies: - boolbase: 1.0.0 - css-select: 5.1.0 - css-what: 6.1.0 - domelementtype: 2.3.0 - domhandler: 5.0.3 - domutils: 3.1.0 - dev: false + '@rollup/rollup-win32-x64-msvc@4.30.1': + optional: true - /cheerio@1.0.0-rc.12: - resolution: {integrity: sha512-VqR8m68vM46BNnuZ5NtnGBKIE/DfN0cRIzg9n40EIq9NOv90ayxLBXA8fXC5gquFRGJSTRqBq25Jt2ECLR431Q==} - engines: {node: '>= 6'} - dependencies: - cheerio-select: 2.1.0 - dom-serializer: 2.0.0 - domhandler: 5.0.3 - domutils: 3.1.0 - htmlparser2: 8.0.2 - parse5: 7.1.2 - parse5-htmlparser2-tree-adapter: 7.0.0 - dev: false - - /chokidar@3.5.3: - resolution: {integrity: sha512-Dr3sfKRP6oTcjf2JmUmFJfeVMvXBdegxB0iVQ5eb2V10uFJUCAS8OByZdVAyVb8xXNz3GjjTgj9kLWsZTqE6kw==} - engines: {node: '>= 8.10.0'} - dependencies: - anymatch: 3.1.3 - braces: 3.0.2 - glob-parent: 5.1.2 - is-binary-path: 2.1.0 - is-glob: 4.0.3 - normalize-path: 3.0.0 - readdirp: 3.6.0 - optionalDependencies: - fsevents: 2.3.2 - dev: false + '@sec-ant/readable-stream@0.4.1': {} - /cli-cursor@4.0.0: - resolution: {integrity: sha512-VGtlMu3x/4DOtIUwEkRezxUZ2lBacNJCHash0N0WeZDBS+7Ux1dm3XWAgWYxLJFMMdOeXMHXorshEFhbMSGelg==} - engines: {node: ^12.20.0 || ^14.13.1 || >=16.0.0} + '@shikijs/core@1.26.2': dependencies: - restore-cursor: 4.0.0 - dev: false + '@shikijs/engine-javascript': 1.26.2 + '@shikijs/engine-oniguruma': 1.26.2 + '@shikijs/types': 1.26.2 + '@shikijs/vscode-textmate': 10.0.1 + '@types/hast': 3.0.4 + hast-util-to-html: 9.0.4 - /cli-spinners@2.9.0: - resolution: {integrity: sha512-4/aL9X3Wh0yiMQlE+eeRhWP6vclO3QRtw1JHKIT0FFUs5FjpFmESqtMvYZ0+lbzBw900b95mS0hohy+qn2VK/g==} - engines: {node: '>=6'} - dev: false + '@shikijs/engine-javascript@1.26.2': + dependencies: + '@shikijs/types': 1.26.2 + '@shikijs/vscode-textmate': 10.0.1 + oniguruma-to-es: 1.0.0 - /cliui@6.0.0: - resolution: {integrity: sha512-t6wbgtoCXvAzst7QgXxJYqPt0usEfbgQdftEPbLL/cvv6HPE5VgvqCuAIDR0NgU52ds6rFwqrgakNLrHEjCbrQ==} + '@shikijs/engine-oniguruma@1.26.2': dependencies: - string-width: 4.2.3 - strip-ansi: 6.0.1 - wrap-ansi: 6.2.0 - dev: false + '@shikijs/types': 1.26.2 + '@shikijs/vscode-textmate': 10.0.1 - /clone@1.0.4: - resolution: {integrity: sha512-JQHZ2QMW6l3aH/j6xCqQThY/9OH4D/9ls34cgkUBiEeocRTU04tHfKPBsUK1PqZCUQM7GiA0IIXJSuXHI64Kbg==} - engines: {node: '>=0.8'} - dev: false + '@shikijs/langs@1.26.2': + dependencies: + '@shikijs/types': 1.26.2 - /codem-isoboxer@0.3.9: - resolution: {integrity: sha512-4XOTqEzBWrGOZaMd+sTED2hLpzfBbiQCf1W6OBGkIHqk1D8uwy8WFLazVbdQwfDpQ+vf39lqTGPa9IhWW0roTA==} - dev: false + '@shikijs/themes@1.26.2': + dependencies: + '@shikijs/types': 1.26.2 - /color-convert@1.9.3: - resolution: {integrity: sha512-QfAUtd+vFdAtFQcC8CCyYt1fYWxSqAiK2cSD6zDB8N3cpsEBAvRxp9zOGg6G/SHHJYAT88/az/IuDGALsNVbGg==} + '@shikijs/transformers@1.26.2': dependencies: - color-name: 1.1.3 - dev: false + shiki: 1.26.2 - /color-convert@2.0.1: - resolution: {integrity: sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==} - engines: {node: '>=7.0.0'} + '@shikijs/types@1.26.2': dependencies: - color-name: 1.1.4 - dev: false + '@shikijs/vscode-textmate': 10.0.1 + '@types/hast': 3.0.4 - /color-name@1.1.3: - resolution: {integrity: sha512-72fSenhMw2HZMTVHeCA9KCmpEIbzWiQsjN+BHcBbS9vr1mtt+vJjPdksIBNUmKAW8TFUDPJK5SUU3QhE9NEXDw==} - dev: false + '@shikijs/vscode-textmate@10.0.1': {} - /color-name@1.1.4: - resolution: {integrity: sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==} - dev: false + '@sindresorhus/merge-streams@2.3.0': {} - /commander@10.0.1: - resolution: {integrity: sha512-y4Mg2tXshplEbSGzx7amzPwKKOCGuoSRP/CjEdwwk0FOGlUbq6lKuoyDZTNZkmxHdJtp54hdfY/JUrdL7Xfdug==} - engines: {node: '>=14'} - dev: false + '@sindresorhus/merge-streams@4.0.0': {} - /commander@2.20.3: - resolution: {integrity: sha512-GpVkmM8vF2vQUkj2LvZmD35JxeJOLCwJ9cUkugyk2nuhbv3+mJvpLYYt+0+USMxE+oj+ey/lJEnhZw75x/OMcQ==} - dev: false + '@stackblitz/sdk@1.11.0': {} - /commander@7.2.0: - resolution: {integrity: sha512-QrWXB+ZQSVPmIWIhtEO9H+gwHaMGYiF5ChvoJ+K9ZGHG/sVsa6yiesAD1GC/x46sET00Xlwo1u49RVVVzvcSkw==} - engines: {node: '>= 10'} - dev: false + '@types/debug@4.1.12': + dependencies: + '@types/ms': 0.7.34 - /commander@8.3.0: - resolution: {integrity: sha512-OkTL9umf+He2DZkUq8f8J9of7yL6RJKI24dVITBmNfZBmri9zYZQrKkuXiKhyfPSu8tUhnVBB1iKXevvnlR4Ww==} - engines: {node: '>= 12'} - dev: false + '@types/estree@1.0.6': {} - /commander@9.2.0: - resolution: {integrity: sha512-e2i4wANQiSXgnrBlIatyHtP1odfUp0BbV5Y5nEGbxtIrStkEOAAzCUirvLBNXHLr7kwLvJl6V+4V3XV9x7Wd9w==} - engines: {node: ^12.20.0 || >=14} - dev: false + '@types/fs-extra@11.0.4': + dependencies: + '@types/jsonfile': 6.1.4 + '@types/node': 22.10.5 - /comment-regex@1.0.1: - resolution: {integrity: sha512-IWlN//Yfby92tOIje7J18HkNmWRR7JESA/BK8W7wqY/akITpU5B0JQWnbTjCfdChSrDNb0DrdA9jfAxiiBXyiQ==} - engines: {node: '>=0.10.0'} - dev: false + '@types/hash-sum@1.0.2': {} - /common-tags@1.8.2: - resolution: {integrity: sha512-gk/Z852D2Wtb//0I+kRFNKKE9dIIVirjoqPoA1wJU+XePVXZfGeBpk45+A1rKO4Q43prqWBNY/MiIeRLbPWUaA==} - engines: {node: '>=4.0.0'} - dev: false + '@types/hast@3.0.4': + dependencies: + '@types/unist': 3.0.3 - /concat-map@0.0.1: - resolution: {integrity: sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg==} - dev: false + '@types/jsonfile@6.1.4': + dependencies: + '@types/node': 22.10.5 - /connect-history-api-fallback@2.0.0: - resolution: {integrity: sha512-U73+6lQFmfiNPrYbXqr6kZ1i1wiRqXnp2nhMsINseWXO8lDau0LGEffJ8kQi4EjLZympVgRdvqjAgiZ1tgzDDA==} - engines: {node: '>=0.8'} - dev: false + '@types/katex@0.16.7': {} - /convert-source-map@1.9.0: - resolution: {integrity: sha512-ASFBup0Mz1uyiIjANan1jzLQami9z1PoYSZCiiYW2FczPbenXc45FZdBZLzOT+r6+iciuEModtmCti+hjaAk0A==} - dev: false + '@types/linkify-it@5.0.0': {} - /core-js-compat@3.30.2: - resolution: {integrity: sha512-nriW1nuJjUgvkEjIot1Spwakz52V9YkYHZAQG6A1eCgC8AA1p0zngrQEP9R0+V6hji5XilWKG1Bd0YRppmGimA==} + '@types/markdown-it-emoji@3.0.1': dependencies: - browserslist: 4.21.5 - dev: false + '@types/markdown-it': 14.1.2 - /core-js@3.30.2: - resolution: {integrity: sha512-uBJiDmwqsbJCWHAwjrx3cvjbMXP7xD72Dmsn5LOJpiRmE3WbBbN5rCqQ2Qh6Ek6/eOrjlWngEynBWo4VxerQhg==} - requiresBuild: true - dev: false + '@types/markdown-it@14.1.2': + dependencies: + '@types/linkify-it': 5.0.0 + '@types/mdurl': 2.0.0 - /cose-base@1.0.3: - resolution: {integrity: sha512-s9whTXInMSgAp/NVXVNuVxVKzGH2qck3aQlVHxDCdAEPgtMKwc4Wq6/QKhgdEdgbLSi9rBTAcPoRa6JpiG4ksg==} + '@types/mdast@4.0.4': dependencies: - layout-base: 1.0.2 - dev: false + '@types/unist': 3.0.3 + + '@types/mdurl@2.0.0': {} + + '@types/ms@0.7.34': {} - /cose-base@2.2.0: - resolution: {integrity: sha512-AzlgcsCbUMymkADOJtQm3wO9S3ltPfYOFD5033keQn9NJzIbtnZj+UdBJe7DYml/8TdbtHJW3j58SOnKhWY/5g==} + '@types/node@17.0.45': {} + + '@types/node@22.10.5': dependencies: - layout-base: 2.0.1 - dev: false + undici-types: 6.20.0 - /cross-spawn@7.0.3: - resolution: {integrity: sha512-iRDPJKUPVEND7dHPO8rkbOnPpyDygcDFtWjpeWNCgy8WP2rXcxXL8TskReQl6OrB2G7+UJrags1q15Fudc7G6w==} - engines: {node: '>= 8'} + '@types/sax@1.2.7': dependencies: - path-key: 3.1.1 - shebang-command: 2.0.0 - which: 2.0.2 - dev: false + '@types/node': 17.0.45 - /crypto-random-string@2.0.0: - resolution: {integrity: sha512-v1plID3y9r/lPhviJ1wrXpLeyUIGAZ2SHNYTEapm7/8A9nLPoyvVp3RK/EPFqn5kEznyWgYZNsRtYYIWbuG8KA==} - engines: {node: '>=8'} - dev: false + '@types/trusted-types@2.0.7': {} - /css-select@5.1.0: - resolution: {integrity: sha512-nwoRF1rvRRnnCqqY7updORDsuqKzqYJ28+oSMaJMMgOauh3fvwHqMS7EZpIPqK8GL+g9mKxF1vP/ZjSeNjEVHg==} - dependencies: - boolbase: 1.0.0 - css-what: 6.1.0 - domhandler: 5.0.3 - domutils: 3.1.0 - nth-check: 2.1.1 - dev: false + '@types/unist@2.0.11': {} - /css-what@6.1.0: - resolution: {integrity: sha512-HTUrgRJ7r4dsZKU6GjmpfRK1O76h97Z8MfS1G0FozR+oF2kG6Vfe8JE6zwrkbxigziPHinCJ+gCPjA9EaBDtRw==} - engines: {node: '>= 6'} - dev: false + '@types/unist@3.0.3': {} - /csstype@3.1.2: - resolution: {integrity: sha512-I7K1Uu0MBPzaFKg4nI5Q7Vs2t+3gWWW648spaF+Rg7pI9ds18Ugn+lvg4SHczUdKlHI5LWBXyqfS8+DufyBsgQ==} - dev: false + '@types/web-bluetooth@0.0.20': {} - /custom-event-polyfill@1.0.7: - resolution: {integrity: sha512-TDDkd5DkaZxZFM8p+1I3yAlvM3rSr1wbrOliG4yJiwinMZN8z/iGL7BTlDkrJcYTmgUSb4ywVCc3ZaUtOtC76w==} - dev: false + '@ungap/structured-clone@1.2.1': {} - /cytoscape-cose-bilkent@4.1.0(cytoscape@3.24.0): - resolution: {integrity: sha512-wgQlVIUJF13Quxiv5e1gstZ08rnZj2XaLHGoFMYXz7SkNfCDOOteKBE6SYRfA9WxxI/iBc3ajfDoc6hb/MRAHQ==} - peerDependencies: - cytoscape: ^3.2.0 + '@vitejs/plugin-vue@5.2.1(vite@6.0.7(@types/node@22.10.5)(sass-embedded@1.83.1))(vue@3.5.13)': dependencies: - cose-base: 1.0.3 - cytoscape: 3.24.0 - dev: false + vite: 6.0.7(@types/node@22.10.5)(sass-embedded@1.83.1) + vue: 3.5.13 - /cytoscape-fcose@2.2.0(cytoscape@3.24.0): - resolution: {integrity: sha512-ki1/VuRIHFCzxWNrsshHYPs6L7TvLu3DL+TyIGEsRcvVERmxokbf5Gdk7mFxZnTdiGtnA4cfSmjZJMviqSuZrQ==} - peerDependencies: - cytoscape: ^3.2.0 + '@vue/compiler-core@3.5.13': dependencies: - cose-base: 2.2.0 - cytoscape: 3.24.0 - dev: false + '@babel/parser': 7.26.5 + '@vue/shared': 3.5.13 + entities: 4.5.0 + estree-walker: 2.0.2 + source-map-js: 1.2.1 - /cytoscape@3.24.0: - resolution: {integrity: sha512-W9fJMrAfr/zKFzDCpRR/wn6uoEQ7gfbJmxPK5DadXj69XyAhZYi1QXLOE+UXJfXVXxqGM1o1eeiIrtxrtB43zA==} - engines: {node: '>=0.10'} + '@vue/compiler-dom@3.5.13': dependencies: - heap: 0.2.7 - lodash: 4.17.21 - dev: false + '@vue/compiler-core': 3.5.13 + '@vue/shared': 3.5.13 - /d3-array@3.2.3: - resolution: {integrity: sha512-JRHwbQQ84XuAESWhvIPaUV4/1UYTBOLiOPGWqgFDHZS1D5QN9c57FbH3QpEnQMYiOXNzKUQyGTZf+EVO7RT5TQ==} - engines: {node: '>=12'} + '@vue/compiler-sfc@3.5.13': dependencies: - internmap: 2.0.3 - dev: false - - /d3-axis@3.0.0: - resolution: {integrity: sha512-IH5tgjV4jE/GhHkRV0HiVYPDtvfjHQlQfJHs0usq7M30XcSBvOotpmH1IgkcXsO/5gEQZD43B//fc7SRT5S+xw==} - engines: {node: '>=12'} - dev: false + '@babel/parser': 7.26.5 + '@vue/compiler-core': 3.5.13 + '@vue/compiler-dom': 3.5.13 + '@vue/compiler-ssr': 3.5.13 + '@vue/shared': 3.5.13 + estree-walker: 2.0.2 + magic-string: 0.30.17 + postcss: 8.5.0 + source-map-js: 1.2.1 - /d3-brush@3.0.0: - resolution: {integrity: sha512-ALnjWlVYkXsVIGlOsuWH1+3udkYFI48Ljihfnh8FZPF2QS9o+PzGLBslO0PjzVoHLZ2KCVgAM8NVkXPJB2aNnQ==} - engines: {node: '>=12'} + '@vue/compiler-ssr@3.5.13': dependencies: - d3-dispatch: 3.0.1 - d3-drag: 3.0.0 - d3-interpolate: 3.0.1 - d3-selection: 3.0.0 - d3-transition: 3.0.1(d3-selection@3.0.0) - dev: false + '@vue/compiler-dom': 3.5.13 + '@vue/shared': 3.5.13 - /d3-chord@3.0.1: - resolution: {integrity: sha512-VE5S6TNa+j8msksl7HwjxMHDM2yNK3XCkusIlpX5kwauBfXuyLAtNg9jCp/iHH61tgI4sb6R/EIMWCqEIdjT/g==} - engines: {node: '>=12'} + '@vue/devtools-api@6.6.4': {} + + '@vue/devtools-api@7.7.0': dependencies: - d3-path: 3.1.0 - dev: false + '@vue/devtools-kit': 7.7.0 - /d3-color@3.1.0: - resolution: {integrity: sha512-zg/chbXyeBtMQ1LbD/WSoW2DpC3I0mpmPdW+ynRTj/x2DAWYrIY7qeZIHidozwV24m4iavr15lNwIwLxRmOxhA==} - engines: {node: '>=12'} - dev: false + '@vue/devtools-kit@7.7.0': + dependencies: + '@vue/devtools-shared': 7.7.0 + birpc: 0.2.19 + hookable: 5.5.3 + mitt: 3.0.1 + perfect-debounce: 1.0.0 + speakingurl: 14.0.1 + superjson: 2.2.2 - /d3-contour@4.0.2: - resolution: {integrity: sha512-4EzFTRIikzs47RGmdxbeUvLWtGedDUNkTcmzoeyg4sP/dvCexO47AaQL7VKy/gul85TOxw+IBgA8US2xwbToNA==} - engines: {node: '>=12'} + '@vue/devtools-shared@7.7.0': dependencies: - d3-array: 3.2.3 - dev: false + rfdc: 1.4.1 - /d3-delaunay@6.0.4: - resolution: {integrity: sha512-mdjtIZ1XLAM8bm/hx3WwjfHt6Sggek7qH043O8KEjDXN40xi3vx/6pYSVTwLjEgiXQTbvaouWKynLBiUZ6SK6A==} - engines: {node: '>=12'} + '@vue/reactivity@3.5.13': dependencies: - delaunator: 5.0.0 - dev: false + '@vue/shared': 3.5.13 - /d3-dispatch@3.0.1: - resolution: {integrity: sha512-rzUyPU/S7rwUflMyLc1ETDeBj0NRuHKKAcvukozwhshr6g6c5d8zh4c2gQjY2bZ0dXeGLWc1PF174P2tVvKhfg==} - engines: {node: '>=12'} - dev: false + '@vue/runtime-core@3.5.13': + dependencies: + '@vue/reactivity': 3.5.13 + '@vue/shared': 3.5.13 - /d3-drag@3.0.0: - resolution: {integrity: sha512-pWbUJLdETVA8lQNJecMxoXfH6x+mO2UQo8rSmZ+QqxcbyA3hfeprFgIT//HW2nlHChWeIIMwS2Fq+gEARkhTkg==} - engines: {node: '>=12'} + '@vue/runtime-dom@3.5.13': dependencies: - d3-dispatch: 3.0.1 - d3-selection: 3.0.0 - dev: false + '@vue/reactivity': 3.5.13 + '@vue/runtime-core': 3.5.13 + '@vue/shared': 3.5.13 + csstype: 3.1.3 - /d3-dsv@3.0.1: - resolution: {integrity: sha512-UG6OvdI5afDIFP9w4G0mNq50dSOsXHJaRE8arAS5o9ApWnIElp8GZw1Dun8vP8OyHOZ/QJUKUJwxiiCCnUwm+Q==} - engines: {node: '>=12'} - hasBin: true + '@vue/server-renderer@3.5.13(vue@3.5.13)': dependencies: - commander: 7.2.0 - iconv-lite: 0.6.3 - rw: 1.3.3 - dev: false + '@vue/compiler-ssr': 3.5.13 + '@vue/shared': 3.5.13 + vue: 3.5.13 - /d3-ease@3.0.1: - resolution: {integrity: sha512-wR/XK3D3XcLIZwpbvQwQ5fK+8Ykds1ip7A2Txe0yxncXSdq1L9skcG7blcedkOX+ZcgxGAmLX1FrRGbADwzi0w==} - engines: {node: '>=12'} - dev: false + '@vue/shared@3.5.13': {} - /d3-fetch@3.0.1: - resolution: {integrity: sha512-kpkQIM20n3oLVBKGg6oHrUchHM3xODkTzjMoj7aWQFq5QEM+R6E4WkzT5+tojDY7yjez8KgCBRoj4aEr99Fdqw==} - engines: {node: '>=12'} + '@vuepress/bundler-vite@2.0.0-rc.19(@types/node@22.10.5)(sass-embedded@1.83.1)': dependencies: - d3-dsv: 3.0.1 - dev: false + '@vitejs/plugin-vue': 5.2.1(vite@6.0.7(@types/node@22.10.5)(sass-embedded@1.83.1))(vue@3.5.13) + '@vuepress/bundlerutils': 2.0.0-rc.19 + '@vuepress/client': 2.0.0-rc.19 + '@vuepress/core': 2.0.0-rc.19 + '@vuepress/shared': 2.0.0-rc.19 + '@vuepress/utils': 2.0.0-rc.19 + autoprefixer: 10.4.20(postcss@8.5.0) + connect-history-api-fallback: 2.0.0 + postcss: 8.5.0 + postcss-load-config: 6.0.1(postcss@8.5.0) + rollup: 4.30.1 + vite: 6.0.7(@types/node@22.10.5)(sass-embedded@1.83.1) + vue: 3.5.13 + vue-router: 4.5.0(vue@3.5.13) + transitivePeerDependencies: + - '@types/node' + - jiti + - less + - lightningcss + - sass + - sass-embedded + - stylus + - sugarss + - supports-color + - terser + - tsx + - typescript + - yaml + + '@vuepress/bundlerutils@2.0.0-rc.19': + dependencies: + '@vuepress/client': 2.0.0-rc.19 + '@vuepress/core': 2.0.0-rc.19 + '@vuepress/shared': 2.0.0-rc.19 + '@vuepress/utils': 2.0.0-rc.19 + vue: 3.5.13 + vue-router: 4.5.0(vue@3.5.13) + transitivePeerDependencies: + - supports-color + - typescript - /d3-force@3.0.0: - resolution: {integrity: sha512-zxV/SsA+U4yte8051P4ECydjD/S+qeYtnaIyAs9tgHCqfguma/aAQDjo85A9Z6EKhBirHRJHXIgJUlffT4wdLg==} - engines: {node: '>=12'} + '@vuepress/cli@2.0.0-rc.19': dependencies: - d3-dispatch: 3.0.1 - d3-quadtree: 3.0.1 - d3-timer: 3.0.1 - dev: false + '@vuepress/core': 2.0.0-rc.19 + '@vuepress/shared': 2.0.0-rc.19 + '@vuepress/utils': 2.0.0-rc.19 + cac: 6.7.14 + chokidar: 3.6.0 + envinfo: 7.14.0 + esbuild: 0.21.5 + transitivePeerDependencies: + - supports-color + - typescript - /d3-format@3.1.0: - resolution: {integrity: sha512-YyUI6AEuY/Wpt8KWLgZHsIU86atmikuoOmCfommt0LYHiQSPjvX2AcFc38PX0CBpr2RCyZhjex+NS/LPOv6YqA==} - engines: {node: '>=12'} - dev: false + '@vuepress/client@2.0.0-rc.19': + dependencies: + '@vue/devtools-api': 7.7.0 + '@vuepress/shared': 2.0.0-rc.19 + vue: 3.5.13 + vue-router: 4.5.0(vue@3.5.13) + transitivePeerDependencies: + - typescript - /d3-geo@3.1.0: - resolution: {integrity: sha512-JEo5HxXDdDYXCaWdwLRt79y7giK8SbhZJbFWXqbRTolCHFI5jRqteLzCsq51NKbUoX0PjBVSohxrx+NoOUujYA==} - engines: {node: '>=12'} + '@vuepress/core@2.0.0-rc.19': dependencies: - d3-array: 3.2.3 - dev: false + '@vuepress/client': 2.0.0-rc.19 + '@vuepress/markdown': 2.0.0-rc.19 + '@vuepress/shared': 2.0.0-rc.19 + '@vuepress/utils': 2.0.0-rc.19 + vue: 3.5.13 + transitivePeerDependencies: + - supports-color + - typescript - /d3-hierarchy@3.1.2: - resolution: {integrity: sha512-FX/9frcub54beBdugHjDCdikxThEqjnR93Qt7PvQTOHxyiNCAlvMrHhclk3cD5VeAaq9fxmfRp+CnWw9rEMBuA==} - engines: {node: '>=12'} - dev: false + '@vuepress/helper@2.0.0-rc.70(vuepress@2.0.0-rc.19(@vuepress/bundler-vite@2.0.0-rc.19(@types/node@22.10.5)(sass-embedded@1.83.1))(vue@3.5.13))': + dependencies: + '@vue/shared': 3.5.13 + '@vueuse/core': 12.4.0 + cheerio: 1.0.0 + fflate: 0.8.2 + gray-matter: 4.0.3 + vue: 3.5.13 + vuepress: 2.0.0-rc.19(@vuepress/bundler-vite@2.0.0-rc.19(@types/node@22.10.5)(sass-embedded@1.83.1))(vue@3.5.13) + transitivePeerDependencies: + - typescript - /d3-interpolate@3.0.1: - resolution: {integrity: sha512-3bYs1rOD33uo8aqJfKP3JWPAibgw8Zm2+L9vBKEHJ2Rg+viTR7o5Mmv5mZcieN+FRYaAOWX5SJATX6k1PWz72g==} - engines: {node: '>=12'} + '@vuepress/highlighter-helper@2.0.0-rc.70(@vueuse/core@12.4.0)(vuepress@2.0.0-rc.19(@vuepress/bundler-vite@2.0.0-rc.19(@types/node@22.10.5)(sass-embedded@1.83.1))(vue@3.5.13))': dependencies: - d3-color: 3.1.0 - dev: false + vuepress: 2.0.0-rc.19(@vuepress/bundler-vite@2.0.0-rc.19(@types/node@22.10.5)(sass-embedded@1.83.1))(vue@3.5.13) + optionalDependencies: + '@vueuse/core': 12.4.0 + + '@vuepress/markdown@2.0.0-rc.19': + dependencies: + '@mdit-vue/plugin-component': 2.1.3 + '@mdit-vue/plugin-frontmatter': 2.1.3 + '@mdit-vue/plugin-headers': 2.1.3 + '@mdit-vue/plugin-sfc': 2.1.3 + '@mdit-vue/plugin-title': 2.1.3 + '@mdit-vue/plugin-toc': 2.1.3 + '@mdit-vue/shared': 2.1.3 + '@mdit-vue/types': 2.1.0 + '@types/markdown-it': 14.1.2 + '@types/markdown-it-emoji': 3.0.1 + '@vuepress/shared': 2.0.0-rc.19 + '@vuepress/utils': 2.0.0-rc.19 + markdown-it: 14.1.0 + markdown-it-anchor: 9.2.0(@types/markdown-it@14.1.2)(markdown-it@14.1.0) + markdown-it-emoji: 3.0.0 + mdurl: 2.0.0 + transitivePeerDependencies: + - supports-color - /d3-path@3.1.0: - resolution: {integrity: sha512-p3KP5HCf/bvjBSSKuXid6Zqijx7wIfNW+J/maPs+iwR35at5JCbLUT0LzF1cnjbCHWhqzQTIN2Jpe8pRebIEFQ==} - engines: {node: '>=12'} - dev: false + '@vuepress/plugin-active-header-links@2.0.0-rc.70(vuepress@2.0.0-rc.19(@vuepress/bundler-vite@2.0.0-rc.19(@types/node@22.10.5)(sass-embedded@1.83.1))(vue@3.5.13))': + dependencies: + '@vueuse/core': 12.4.0 + vue: 3.5.13 + vuepress: 2.0.0-rc.19(@vuepress/bundler-vite@2.0.0-rc.19(@types/node@22.10.5)(sass-embedded@1.83.1))(vue@3.5.13) + transitivePeerDependencies: + - typescript - /d3-polygon@3.0.1: - resolution: {integrity: sha512-3vbA7vXYwfe1SYhED++fPUQlWSYTTGmFmQiany/gdbiWgU/iEyQzyymwL9SkJjFFuCS4902BSzewVGsHHmHtXg==} - engines: {node: '>=12'} - dev: false + '@vuepress/plugin-back-to-top@2.0.0-rc.70(vuepress@2.0.0-rc.19(@vuepress/bundler-vite@2.0.0-rc.19(@types/node@22.10.5)(sass-embedded@1.83.1))(vue@3.5.13))': + dependencies: + '@vuepress/helper': 2.0.0-rc.70(vuepress@2.0.0-rc.19(@vuepress/bundler-vite@2.0.0-rc.19(@types/node@22.10.5)(sass-embedded@1.83.1))(vue@3.5.13)) + '@vueuse/core': 12.4.0 + vue: 3.5.13 + vuepress: 2.0.0-rc.19(@vuepress/bundler-vite@2.0.0-rc.19(@types/node@22.10.5)(sass-embedded@1.83.1))(vue@3.5.13) + transitivePeerDependencies: + - typescript - /d3-quadtree@3.0.1: - resolution: {integrity: sha512-04xDrxQTDTCFwP5H6hRhsRcb9xxv2RzkcsygFzmkSIOJy3PeRJP7sNk3VRIbKXcog561P9oU0/rVH6vDROAgUw==} - engines: {node: '>=12'} - dev: false + '@vuepress/plugin-blog@2.0.0-rc.70(vuepress@2.0.0-rc.19(@vuepress/bundler-vite@2.0.0-rc.19(@types/node@22.10.5)(sass-embedded@1.83.1))(vue@3.5.13))': + dependencies: + '@vuepress/helper': 2.0.0-rc.70(vuepress@2.0.0-rc.19(@vuepress/bundler-vite@2.0.0-rc.19(@types/node@22.10.5)(sass-embedded@1.83.1))(vue@3.5.13)) + chokidar: 3.6.0 + vue: 3.5.13 + vuepress: 2.0.0-rc.19(@vuepress/bundler-vite@2.0.0-rc.19(@types/node@22.10.5)(sass-embedded@1.83.1))(vue@3.5.13) + transitivePeerDependencies: + - typescript - /d3-random@3.0.1: - resolution: {integrity: sha512-FXMe9GfxTxqd5D6jFsQ+DJ8BJS4E/fT5mqqdjovykEB2oFbTMDVdg1MGFxfQW+FBOGoB++k8swBrgwSHT1cUXQ==} - engines: {node: '>=12'} - dev: false + '@vuepress/plugin-catalog@2.0.0-rc.70(vuepress@2.0.0-rc.19(@vuepress/bundler-vite@2.0.0-rc.19(@types/node@22.10.5)(sass-embedded@1.83.1))(vue@3.5.13))': + dependencies: + '@vuepress/helper': 2.0.0-rc.70(vuepress@2.0.0-rc.19(@vuepress/bundler-vite@2.0.0-rc.19(@types/node@22.10.5)(sass-embedded@1.83.1))(vue@3.5.13)) + vue: 3.5.13 + vuepress: 2.0.0-rc.19(@vuepress/bundler-vite@2.0.0-rc.19(@types/node@22.10.5)(sass-embedded@1.83.1))(vue@3.5.13) + transitivePeerDependencies: + - typescript - /d3-scale-chromatic@3.0.0: - resolution: {integrity: sha512-Lx9thtxAKrO2Pq6OO2Ua474opeziKr279P/TKZsMAhYyNDD3EnCffdbgeSYN5O7m2ByQsxtuP2CSDczNUIZ22g==} - engines: {node: '>=12'} + '@vuepress/plugin-comment@2.0.0-rc.70(vuepress@2.0.0-rc.19(@vuepress/bundler-vite@2.0.0-rc.19(@types/node@22.10.5)(sass-embedded@1.83.1))(vue@3.5.13))': dependencies: - d3-color: 3.1.0 - d3-interpolate: 3.0.1 - dev: false + '@vuepress/helper': 2.0.0-rc.70(vuepress@2.0.0-rc.19(@vuepress/bundler-vite@2.0.0-rc.19(@types/node@22.10.5)(sass-embedded@1.83.1))(vue@3.5.13)) + '@vueuse/core': 12.4.0 + giscus: 1.6.0 + vue: 3.5.13 + vuepress: 2.0.0-rc.19(@vuepress/bundler-vite@2.0.0-rc.19(@types/node@22.10.5)(sass-embedded@1.83.1))(vue@3.5.13) + transitivePeerDependencies: + - typescript - /d3-scale@4.0.2: - resolution: {integrity: sha512-GZW464g1SH7ag3Y7hXjf8RoUuAFIqklOAq3MRl4OaWabTFJY9PN/E1YklhXLh+OQ3fM9yS2nOkCoS+WLZ6kvxQ==} - engines: {node: '>=12'} + '@vuepress/plugin-copy-code@2.0.0-rc.70(vuepress@2.0.0-rc.19(@vuepress/bundler-vite@2.0.0-rc.19(@types/node@22.10.5)(sass-embedded@1.83.1))(vue@3.5.13))': dependencies: - d3-array: 3.2.3 - d3-format: 3.1.0 - d3-interpolate: 3.0.1 - d3-time: 3.1.0 - d3-time-format: 4.1.0 - dev: false + '@vuepress/helper': 2.0.0-rc.70(vuepress@2.0.0-rc.19(@vuepress/bundler-vite@2.0.0-rc.19(@types/node@22.10.5)(sass-embedded@1.83.1))(vue@3.5.13)) + '@vueuse/core': 12.4.0 + vue: 3.5.13 + vuepress: 2.0.0-rc.19(@vuepress/bundler-vite@2.0.0-rc.19(@types/node@22.10.5)(sass-embedded@1.83.1))(vue@3.5.13) + transitivePeerDependencies: + - typescript - /d3-selection@3.0.0: - resolution: {integrity: sha512-fmTRWbNMmsmWq6xJV8D19U/gw/bwrHfNXxrIN+HfZgnzqTHp9jOmKMhsTUjXOJnZOdZY9Q28y4yebKzqDKlxlQ==} - engines: {node: '>=12'} - dev: false + '@vuepress/plugin-copyright@2.0.0-rc.70(vuepress@2.0.0-rc.19(@vuepress/bundler-vite@2.0.0-rc.19(@types/node@22.10.5)(sass-embedded@1.83.1))(vue@3.5.13))': + dependencies: + '@vuepress/helper': 2.0.0-rc.70(vuepress@2.0.0-rc.19(@vuepress/bundler-vite@2.0.0-rc.19(@types/node@22.10.5)(sass-embedded@1.83.1))(vue@3.5.13)) + '@vueuse/core': 12.4.0 + vue: 3.5.13 + vuepress: 2.0.0-rc.19(@vuepress/bundler-vite@2.0.0-rc.19(@types/node@22.10.5)(sass-embedded@1.83.1))(vue@3.5.13) + transitivePeerDependencies: + - typescript - /d3-shape@3.2.0: - resolution: {integrity: sha512-SaLBuwGm3MOViRq2ABk3eLoxwZELpH6zhl3FbAoJ7Vm1gofKx6El1Ib5z23NUEhF9AsGl7y+dzLe5Cw2AArGTA==} - engines: {node: '>=12'} + '@vuepress/plugin-feed@2.0.0-rc.70(vuepress@2.0.0-rc.19(@vuepress/bundler-vite@2.0.0-rc.19(@types/node@22.10.5)(sass-embedded@1.83.1))(vue@3.5.13))': dependencies: - d3-path: 3.1.0 - dev: false + '@vuepress/helper': 2.0.0-rc.70(vuepress@2.0.0-rc.19(@vuepress/bundler-vite@2.0.0-rc.19(@types/node@22.10.5)(sass-embedded@1.83.1))(vue@3.5.13)) + vuepress: 2.0.0-rc.19(@vuepress/bundler-vite@2.0.0-rc.19(@types/node@22.10.5)(sass-embedded@1.83.1))(vue@3.5.13) + xml-js: 1.6.11 + transitivePeerDependencies: + - typescript - /d3-time-format@4.1.0: - resolution: {integrity: sha512-dJxPBlzC7NugB2PDLwo9Q8JiTR3M3e4/XANkreKSUxF8vvXKqm1Yfq4Q5dl8budlunRVlUUaDUgFt7eA8D6NLg==} - engines: {node: '>=12'} + '@vuepress/plugin-git@2.0.0-rc.68(vuepress@2.0.0-rc.19(@vuepress/bundler-vite@2.0.0-rc.19(@types/node@22.10.5)(sass-embedded@1.83.1))(vue@3.5.13))': dependencies: - d3-time: 3.1.0 - dev: false + execa: 9.5.2 + vuepress: 2.0.0-rc.19(@vuepress/bundler-vite@2.0.0-rc.19(@types/node@22.10.5)(sass-embedded@1.83.1))(vue@3.5.13) - /d3-time@3.1.0: - resolution: {integrity: sha512-VqKjzBLejbSMT4IgbmVgDjpkYrNWUYJnbCGo874u7MMKIWsILRX+OpX/gTk8MqjpT1A/c6HY2dCA77ZN0lkQ2Q==} - engines: {node: '>=12'} + '@vuepress/plugin-icon@2.0.0-rc.70(markdown-it@14.1.0)(vuepress@2.0.0-rc.19(@vuepress/bundler-vite@2.0.0-rc.19(@types/node@22.10.5)(sass-embedded@1.83.1))(vue@3.5.13))': dependencies: - d3-array: 3.2.3 - dev: false + '@mdit/plugin-icon': 0.16.5(markdown-it@14.1.0) + '@vuepress/helper': 2.0.0-rc.70(vuepress@2.0.0-rc.19(@vuepress/bundler-vite@2.0.0-rc.19(@types/node@22.10.5)(sass-embedded@1.83.1))(vue@3.5.13)) + '@vueuse/core': 12.4.0 + vue: 3.5.13 + vuepress: 2.0.0-rc.19(@vuepress/bundler-vite@2.0.0-rc.19(@types/node@22.10.5)(sass-embedded@1.83.1))(vue@3.5.13) + transitivePeerDependencies: + - markdown-it + - typescript - /d3-timer@3.0.1: - resolution: {integrity: sha512-ndfJ/JxxMd3nw31uyKoY2naivF+r29V+Lc0svZxe1JvvIRmi8hUsrMvdOwgS1o6uBHmiz91geQ0ylPP0aj1VUA==} - engines: {node: '>=12'} - dev: false + '@vuepress/plugin-links-check@2.0.0-rc.70(vuepress@2.0.0-rc.19(@vuepress/bundler-vite@2.0.0-rc.19(@types/node@22.10.5)(sass-embedded@1.83.1))(vue@3.5.13))': + dependencies: + '@vuepress/helper': 2.0.0-rc.70(vuepress@2.0.0-rc.19(@vuepress/bundler-vite@2.0.0-rc.19(@types/node@22.10.5)(sass-embedded@1.83.1))(vue@3.5.13)) + vuepress: 2.0.0-rc.19(@vuepress/bundler-vite@2.0.0-rc.19(@types/node@22.10.5)(sass-embedded@1.83.1))(vue@3.5.13) + transitivePeerDependencies: + - typescript - /d3-transition@3.0.1(d3-selection@3.0.0): - resolution: {integrity: sha512-ApKvfjsSR6tg06xrL434C0WydLr7JewBB3V+/39RMHsaXTOG0zmt/OAXeng5M5LBm0ojmxJrpomQVZ1aPvBL4w==} - engines: {node: '>=12'} - peerDependencies: - d3-selection: 2 - 3 - dependencies: - d3-color: 3.1.0 - d3-dispatch: 3.0.1 - d3-ease: 3.0.1 - d3-interpolate: 3.0.1 - d3-selection: 3.0.0 - d3-timer: 3.0.1 - dev: false - - /d3-zoom@3.0.0: - resolution: {integrity: sha512-b8AmV3kfQaqWAuacbPuNbL6vahnOJflOhexLzMMNLga62+/nh0JzvJ0aO/5a5MVgUFGS7Hu1P9P03o3fJkDCyw==} - engines: {node: '>=12'} + '@vuepress/plugin-markdown-ext@2.0.0-rc.70(markdown-it@14.1.0)(vuepress@2.0.0-rc.19(@vuepress/bundler-vite@2.0.0-rc.19(@types/node@22.10.5)(sass-embedded@1.83.1))(vue@3.5.13))': dependencies: - d3-dispatch: 3.0.1 - d3-drag: 3.0.0 - d3-interpolate: 3.0.1 - d3-selection: 3.0.0 - d3-transition: 3.0.1(d3-selection@3.0.0) - dev: false + '@mdit/plugin-container': 0.16.0(markdown-it@14.1.0) + '@mdit/plugin-footnote': 0.16.0(markdown-it@14.1.0) + '@mdit/plugin-tasklist': 0.16.0(markdown-it@14.1.0) + '@types/markdown-it': 14.1.2 + '@vuepress/helper': 2.0.0-rc.70(vuepress@2.0.0-rc.19(@vuepress/bundler-vite@2.0.0-rc.19(@types/node@22.10.5)(sass-embedded@1.83.1))(vue@3.5.13)) + js-yaml: 4.1.0 + vuepress: 2.0.0-rc.19(@vuepress/bundler-vite@2.0.0-rc.19(@types/node@22.10.5)(sass-embedded@1.83.1))(vue@3.5.13) + transitivePeerDependencies: + - markdown-it + - typescript - /d3@7.8.4: - resolution: {integrity: sha512-q2WHStdhiBtD8DMmhDPyJmXUxr6VWRngKyiJ5EfXMxPw+tqT6BhNjhJZ4w3BHsNm3QoVfZLY8Orq/qPFczwKRA==} - engines: {node: '>=12'} + '@vuepress/plugin-markdown-hint@2.0.0-rc.70(markdown-it@14.1.0)(vuepress@2.0.0-rc.19(@vuepress/bundler-vite@2.0.0-rc.19(@types/node@22.10.5)(sass-embedded@1.83.1))(vue@3.5.13))': dependencies: - d3-array: 3.2.3 - d3-axis: 3.0.0 - d3-brush: 3.0.0 - d3-chord: 3.0.1 - d3-color: 3.1.0 - d3-contour: 4.0.2 - d3-delaunay: 6.0.4 - d3-dispatch: 3.0.1 - d3-drag: 3.0.0 - d3-dsv: 3.0.1 - d3-ease: 3.0.1 - d3-fetch: 3.0.1 - d3-force: 3.0.0 - d3-format: 3.1.0 - d3-geo: 3.1.0 - d3-hierarchy: 3.1.2 - d3-interpolate: 3.0.1 - d3-path: 3.1.0 - d3-polygon: 3.0.1 - d3-quadtree: 3.0.1 - d3-random: 3.0.1 - d3-scale: 4.0.2 - d3-scale-chromatic: 3.0.0 - d3-selection: 3.0.0 - d3-shape: 3.2.0 - d3-time: 3.1.0 - d3-time-format: 4.1.0 - d3-timer: 3.0.1 - d3-transition: 3.0.1(d3-selection@3.0.0) - d3-zoom: 3.0.0 - dev: false - - /dagre-d3-es@7.0.10: - resolution: {integrity: sha512-qTCQmEhcynucuaZgY5/+ti3X/rnszKZhEQH/ZdWdtP1tA/y3VoHJzcVrO9pjjJCNpigfscAtoUB5ONcd2wNn0A==} - dependencies: - d3: 7.8.4 - lodash-es: 4.17.21 - dev: false - - /dashjs@4.7.0: - resolution: {integrity: sha512-+Sx5TJiT9eDOqODi3anXXBIx2oBM6ANMV5dzvCeSvYKwJ72SzX1bcJSPFLm7g7HTsRCh6pM1VL0XWfrzTlz9xw==} - dependencies: - bcp-47-match: 1.0.3 - bcp-47-normalize: 1.1.1 - codem-isoboxer: 0.3.9 - es6-promise: 4.2.8 - fast-deep-equal: 2.0.1 - html-entities: 1.4.0 - imsc: 1.1.3 - localforage: 1.10.0 - path-browserify: 1.0.1 - ua-parser-js: 1.0.35 - dev: false - - /dayjs@1.11.7: - resolution: {integrity: sha512-+Yw9U6YO5TQohxLcIkrXBeY73WP3ejHWVvx8XCk3gxvQDCTEmS48ZrSZCKciI7Bhl/uCMyxYtE9UqRILmFphkQ==} - dev: false - - /debug@4.3.4: - resolution: {integrity: sha512-PRWFHuSU3eDtQJPvnNY7Jcket1j0t5OuOsFzPPzsekD52Zl8qUfFIPEiswXqIvHWGVHOgX+7G/vCNNhehwxfkQ==} - engines: {node: '>=6.0'} - peerDependencies: - supports-color: '*' - peerDependenciesMeta: - supports-color: - optional: true + '@mdit/plugin-alert': 0.16.0(markdown-it@14.1.0) + '@mdit/plugin-container': 0.16.0(markdown-it@14.1.0) + '@types/markdown-it': 14.1.2 + '@vuepress/helper': 2.0.0-rc.70(vuepress@2.0.0-rc.19(@vuepress/bundler-vite@2.0.0-rc.19(@types/node@22.10.5)(sass-embedded@1.83.1))(vue@3.5.13)) + '@vueuse/core': 12.4.0 + vuepress: 2.0.0-rc.19(@vuepress/bundler-vite@2.0.0-rc.19(@types/node@22.10.5)(sass-embedded@1.83.1))(vue@3.5.13) + transitivePeerDependencies: + - markdown-it + - typescript + + '@vuepress/plugin-markdown-image@2.0.0-rc.70(markdown-it@14.1.0)(vuepress@2.0.0-rc.19(@vuepress/bundler-vite@2.0.0-rc.19(@types/node@22.10.5)(sass-embedded@1.83.1))(vue@3.5.13))': + dependencies: + '@mdit/plugin-figure': 0.16.0(markdown-it@14.1.0) + '@mdit/plugin-img-lazyload': 0.16.0(markdown-it@14.1.0) + '@mdit/plugin-img-mark': 0.16.0(markdown-it@14.1.0) + '@mdit/plugin-img-size': 0.16.0(markdown-it@14.1.0) + '@types/markdown-it': 14.1.2 + '@vuepress/helper': 2.0.0-rc.70(vuepress@2.0.0-rc.19(@vuepress/bundler-vite@2.0.0-rc.19(@types/node@22.10.5)(sass-embedded@1.83.1))(vue@3.5.13)) + vuepress: 2.0.0-rc.19(@vuepress/bundler-vite@2.0.0-rc.19(@types/node@22.10.5)(sass-embedded@1.83.1))(vue@3.5.13) + transitivePeerDependencies: + - markdown-it + - typescript + + '@vuepress/plugin-markdown-include@2.0.0-rc.70(markdown-it@14.1.0)(vuepress@2.0.0-rc.19(@vuepress/bundler-vite@2.0.0-rc.19(@types/node@22.10.5)(sass-embedded@1.83.1))(vue@3.5.13))': dependencies: - ms: 2.1.2 - dev: false + '@mdit/plugin-include': 0.16.0(markdown-it@14.1.0) + '@types/markdown-it': 14.1.2 + '@vuepress/helper': 2.0.0-rc.70(vuepress@2.0.0-rc.19(@vuepress/bundler-vite@2.0.0-rc.19(@types/node@22.10.5)(sass-embedded@1.83.1))(vue@3.5.13)) + vuepress: 2.0.0-rc.19(@vuepress/bundler-vite@2.0.0-rc.19(@types/node@22.10.5)(sass-embedded@1.83.1))(vue@3.5.13) + transitivePeerDependencies: + - markdown-it + - typescript - /decamelize@1.2.0: - resolution: {integrity: sha512-z2S+W9X73hAUUki+N+9Za2lBlun89zigOyGrsax+KUQ6wKW4ZoWpEYBkGhQjwAjjDCkWxhY0VKEhk8wzY7F5cA==} - engines: {node: '>=0.10.0'} - dev: false + '@vuepress/plugin-markdown-math@2.0.0-rc.70(katex@0.16.20)(markdown-it@14.1.0)(mathjax-full@3.2.2)(vuepress@2.0.0-rc.19(@vuepress/bundler-vite@2.0.0-rc.19(@types/node@22.10.5)(sass-embedded@1.83.1))(vue@3.5.13))': + dependencies: + '@mdit/plugin-katex-slim': 0.16.2(katex@0.16.20)(markdown-it@14.1.0) + '@mdit/plugin-mathjax-slim': 0.16.0(markdown-it@14.1.0)(mathjax-full@3.2.2) + '@types/markdown-it': 14.1.2 + '@vuepress/helper': 2.0.0-rc.70(vuepress@2.0.0-rc.19(@vuepress/bundler-vite@2.0.0-rc.19(@types/node@22.10.5)(sass-embedded@1.83.1))(vue@3.5.13)) + vue: 3.5.13 + vuepress: 2.0.0-rc.19(@vuepress/bundler-vite@2.0.0-rc.19(@types/node@22.10.5)(sass-embedded@1.83.1))(vue@3.5.13) + optionalDependencies: + katex: 0.16.20 + mathjax-full: 3.2.2 + transitivePeerDependencies: + - markdown-it + - typescript + + '@vuepress/plugin-markdown-stylize@2.0.0-rc.70(markdown-it@14.1.0)(vuepress@2.0.0-rc.19(@vuepress/bundler-vite@2.0.0-rc.19(@types/node@22.10.5)(sass-embedded@1.83.1))(vue@3.5.13))': + dependencies: + '@mdit/plugin-align': 0.16.0(markdown-it@14.1.0) + '@mdit/plugin-attrs': 0.16.2(markdown-it@14.1.0) + '@mdit/plugin-mark': 0.16.0(markdown-it@14.1.0) + '@mdit/plugin-spoiler': 0.16.0(markdown-it@14.1.0) + '@mdit/plugin-stylize': 0.16.0(markdown-it@14.1.0) + '@mdit/plugin-sub': 0.16.0(markdown-it@14.1.0) + '@mdit/plugin-sup': 0.16.0(markdown-it@14.1.0) + '@types/markdown-it': 14.1.2 + '@vuepress/helper': 2.0.0-rc.70(vuepress@2.0.0-rc.19(@vuepress/bundler-vite@2.0.0-rc.19(@types/node@22.10.5)(sass-embedded@1.83.1))(vue@3.5.13)) + vuepress: 2.0.0-rc.19(@vuepress/bundler-vite@2.0.0-rc.19(@types/node@22.10.5)(sass-embedded@1.83.1))(vue@3.5.13) + transitivePeerDependencies: + - markdown-it + - typescript - /deep-extend@0.6.0: - resolution: {integrity: sha512-LOHxIOaPYdHlJRtCQfDIVZtfw/ufM8+rVj649RIHzcm/vGwQRXFt6OPqIFWsm2XEMrNIEtWR64sY1LEKD2vAOA==} - engines: {node: '>=4.0.0'} - dev: false + '@vuepress/plugin-markdown-tab@2.0.0-rc.70(markdown-it@14.1.0)(vuepress@2.0.0-rc.19(@vuepress/bundler-vite@2.0.0-rc.19(@types/node@22.10.5)(sass-embedded@1.83.1))(vue@3.5.13))': + dependencies: + '@mdit/plugin-tab': 0.16.0(markdown-it@14.1.0) + '@types/markdown-it': 14.1.2 + '@vuepress/helper': 2.0.0-rc.70(vuepress@2.0.0-rc.19(@vuepress/bundler-vite@2.0.0-rc.19(@types/node@22.10.5)(sass-embedded@1.83.1))(vue@3.5.13)) + '@vueuse/core': 12.4.0 + vue: 3.5.13 + vuepress: 2.0.0-rc.19(@vuepress/bundler-vite@2.0.0-rc.19(@types/node@22.10.5)(sass-embedded@1.83.1))(vue@3.5.13) + transitivePeerDependencies: + - markdown-it + - typescript - /deepmerge@4.3.1: - resolution: {integrity: sha512-3sUqbMEc77XqpdNO7FRyRog+eW3ph+GYCbj+rK+uYyRMuwsVy0rMiVtPn+QJlKFvWP/1PYpapqYn0Me2knFn+A==} - engines: {node: '>=0.10.0'} - dev: false + '@vuepress/plugin-notice@2.0.0-rc.70(vuepress@2.0.0-rc.19(@vuepress/bundler-vite@2.0.0-rc.19(@types/node@22.10.5)(sass-embedded@1.83.1))(vue@3.5.13))': + dependencies: + '@vuepress/helper': 2.0.0-rc.70(vuepress@2.0.0-rc.19(@vuepress/bundler-vite@2.0.0-rc.19(@types/node@22.10.5)(sass-embedded@1.83.1))(vue@3.5.13)) + '@vueuse/core': 12.4.0 + vue: 3.5.13 + vuepress: 2.0.0-rc.19(@vuepress/bundler-vite@2.0.0-rc.19(@types/node@22.10.5)(sass-embedded@1.83.1))(vue@3.5.13) + transitivePeerDependencies: + - typescript - /defaults@1.0.4: - resolution: {integrity: sha512-eFuaLoy/Rxalv2kr+lqMlUnrDWV+3j4pljOIJgLIhI058IQfWJ7vXhyEIHu+HtC738klGALYxOKDO0bQP3tg8A==} + '@vuepress/plugin-nprogress@2.0.0-rc.70(vuepress@2.0.0-rc.19(@vuepress/bundler-vite@2.0.0-rc.19(@types/node@22.10.5)(sass-embedded@1.83.1))(vue@3.5.13))': dependencies: - clone: 1.0.4 - dev: false + '@vuepress/helper': 2.0.0-rc.70(vuepress@2.0.0-rc.19(@vuepress/bundler-vite@2.0.0-rc.19(@types/node@22.10.5)(sass-embedded@1.83.1))(vue@3.5.13)) + vue: 3.5.13 + vuepress: 2.0.0-rc.19(@vuepress/bundler-vite@2.0.0-rc.19(@types/node@22.10.5)(sass-embedded@1.83.1))(vue@3.5.13) + transitivePeerDependencies: + - typescript - /define-properties@1.2.0: - resolution: {integrity: sha512-xvqAVKGfT1+UAvPwKTVw/njhdQ8ZhXK4lI0bCIuCMrp2up9nPnaDftrLtmpTazqd1o+UY4zgzU+avtMbDP+ldA==} - engines: {node: '>= 0.4'} + '@vuepress/plugin-photo-swipe@2.0.0-rc.70(vuepress@2.0.0-rc.19(@vuepress/bundler-vite@2.0.0-rc.19(@types/node@22.10.5)(sass-embedded@1.83.1))(vue@3.5.13))': dependencies: - has-property-descriptors: 1.0.0 - object-keys: 1.1.1 - dev: false + '@vuepress/helper': 2.0.0-rc.70(vuepress@2.0.0-rc.19(@vuepress/bundler-vite@2.0.0-rc.19(@types/node@22.10.5)(sass-embedded@1.83.1))(vue@3.5.13)) + '@vueuse/core': 12.4.0 + photoswipe: 5.4.4 + vue: 3.5.13 + vuepress: 2.0.0-rc.19(@vuepress/bundler-vite@2.0.0-rc.19(@types/node@22.10.5)(sass-embedded@1.83.1))(vue@3.5.13) + transitivePeerDependencies: + - typescript - /delaunator@5.0.0: - resolution: {integrity: sha512-AyLvtyJdbv/U1GkiS6gUUzclRoAY4Gs75qkMygJJhU75LW4DNuSF2RMzpxs9jw9Oz1BobHjTdkG3zdP55VxAqw==} + '@vuepress/plugin-reading-time@2.0.0-rc.70(vuepress@2.0.0-rc.19(@vuepress/bundler-vite@2.0.0-rc.19(@types/node@22.10.5)(sass-embedded@1.83.1))(vue@3.5.13))': dependencies: - robust-predicates: 3.0.1 - dev: false + '@vuepress/helper': 2.0.0-rc.70(vuepress@2.0.0-rc.19(@vuepress/bundler-vite@2.0.0-rc.19(@types/node@22.10.5)(sass-embedded@1.83.1))(vue@3.5.13)) + vue: 3.5.13 + vuepress: 2.0.0-rc.19(@vuepress/bundler-vite@2.0.0-rc.19(@types/node@22.10.5)(sass-embedded@1.83.1))(vue@3.5.13) + transitivePeerDependencies: + - typescript - /dijkstrajs@1.0.3: - resolution: {integrity: sha512-qiSlmBq9+BCdCA/L46dw8Uy93mloxsPSbwnm5yrKn2vMPiy8KyAskTF6zuV/j5BMsmOGZDPs7KjU+mjb670kfA==} - dev: false + '@vuepress/plugin-redirect@2.0.0-rc.70(vuepress@2.0.0-rc.19(@vuepress/bundler-vite@2.0.0-rc.19(@types/node@22.10.5)(sass-embedded@1.83.1))(vue@3.5.13))': + dependencies: + '@vuepress/helper': 2.0.0-rc.70(vuepress@2.0.0-rc.19(@vuepress/bundler-vite@2.0.0-rc.19(@types/node@22.10.5)(sass-embedded@1.83.1))(vue@3.5.13)) + '@vueuse/core': 12.4.0 + commander: 13.0.0 + vue: 3.5.13 + vuepress: 2.0.0-rc.19(@vuepress/bundler-vite@2.0.0-rc.19(@types/node@22.10.5)(sass-embedded@1.83.1))(vue@3.5.13) + transitivePeerDependencies: + - typescript - /dir-glob@3.0.1: - resolution: {integrity: sha512-WkrWp9GR4KXfKGYzOLmTuGVi1UWFfws377n9cc55/tb6DuqyF6pcQ5AbiHEshaDpY9v6oaSr2XCDidGmMwdzIA==} - engines: {node: '>=8'} + '@vuepress/plugin-rtl@2.0.0-rc.70(vuepress@2.0.0-rc.19(@vuepress/bundler-vite@2.0.0-rc.19(@types/node@22.10.5)(sass-embedded@1.83.1))(vue@3.5.13))': dependencies: - path-type: 4.0.0 - dev: false + '@vuepress/helper': 2.0.0-rc.70(vuepress@2.0.0-rc.19(@vuepress/bundler-vite@2.0.0-rc.19(@types/node@22.10.5)(sass-embedded@1.83.1))(vue@3.5.13)) + '@vueuse/core': 12.4.0 + vue: 3.5.13 + vuepress: 2.0.0-rc.19(@vuepress/bundler-vite@2.0.0-rc.19(@types/node@22.10.5)(sass-embedded@1.83.1))(vue@3.5.13) + transitivePeerDependencies: + - typescript - /dom-serializer@2.0.0: - resolution: {integrity: sha512-wIkAryiqt/nV5EQKqQpo3SToSOV9J0DnbJqwK7Wv/Trc92zIAYZ4FlMu+JPFW1DfGFt81ZTCGgDEabffXeLyJg==} + '@vuepress/plugin-sass-palette@2.0.0-rc.70(sass-embedded@1.83.1)(vuepress@2.0.0-rc.19(@vuepress/bundler-vite@2.0.0-rc.19(@types/node@22.10.5)(sass-embedded@1.83.1))(vue@3.5.13))': dependencies: - domelementtype: 2.3.0 - domhandler: 5.0.3 - entities: 4.5.0 - dev: false + '@vuepress/helper': 2.0.0-rc.70(vuepress@2.0.0-rc.19(@vuepress/bundler-vite@2.0.0-rc.19(@types/node@22.10.5)(sass-embedded@1.83.1))(vue@3.5.13)) + chokidar: 4.0.3 + vuepress: 2.0.0-rc.19(@vuepress/bundler-vite@2.0.0-rc.19(@types/node@22.10.5)(sass-embedded@1.83.1))(vue@3.5.13) + optionalDependencies: + sass-embedded: 1.83.1 + transitivePeerDependencies: + - typescript - /domelementtype@2.3.0: - resolution: {integrity: sha512-OLETBj6w0OsagBwdXnPdN0cnMfF9opN69co+7ZrbfPGrdpPVNBUj02spi6B1N7wChLQiPn4CSH/zJvXw56gmHw==} - dev: false + '@vuepress/plugin-search@2.0.0-rc.70(vuepress@2.0.0-rc.19(@vuepress/bundler-vite@2.0.0-rc.19(@types/node@22.10.5)(sass-embedded@1.83.1))(vue@3.5.13))': + dependencies: + '@vuepress/helper': 2.0.0-rc.70(vuepress@2.0.0-rc.19(@vuepress/bundler-vite@2.0.0-rc.19(@types/node@22.10.5)(sass-embedded@1.83.1))(vue@3.5.13)) + chokidar: 3.6.0 + vue: 3.5.13 + vuepress: 2.0.0-rc.19(@vuepress/bundler-vite@2.0.0-rc.19(@types/node@22.10.5)(sass-embedded@1.83.1))(vue@3.5.13) + transitivePeerDependencies: + - typescript - /domhandler@5.0.3: - resolution: {integrity: sha512-cgwlv/1iFQiFnU96XXgROh8xTeetsnJiDsTc7TYCLFd9+/WNkIqPTxiM/8pSd8VIrhXGTf1Ny1q1hquVqDJB5w==} - engines: {node: '>= 4'} + '@vuepress/plugin-seo@2.0.0-rc.70(vuepress@2.0.0-rc.19(@vuepress/bundler-vite@2.0.0-rc.19(@types/node@22.10.5)(sass-embedded@1.83.1))(vue@3.5.13))': dependencies: - domelementtype: 2.3.0 - dev: false + '@vuepress/helper': 2.0.0-rc.70(vuepress@2.0.0-rc.19(@vuepress/bundler-vite@2.0.0-rc.19(@types/node@22.10.5)(sass-embedded@1.83.1))(vue@3.5.13)) + vuepress: 2.0.0-rc.19(@vuepress/bundler-vite@2.0.0-rc.19(@types/node@22.10.5)(sass-embedded@1.83.1))(vue@3.5.13) + transitivePeerDependencies: + - typescript - /dompurify@2.4.5: - resolution: {integrity: sha512-jggCCd+8Iqp4Tsz0nIvpcb22InKEBrGz5dw3EQJMs8HPJDsKbFIO3STYtAvCfDx26Muevn1MHVI0XxjgFfmiSA==} - dev: false + '@vuepress/plugin-shiki@2.0.0-rc.70(@vueuse/core@12.4.0)(vuepress@2.0.0-rc.19(@vuepress/bundler-vite@2.0.0-rc.19(@types/node@22.10.5)(sass-embedded@1.83.1))(vue@3.5.13))': + dependencies: + '@shikijs/transformers': 1.26.2 + '@vuepress/helper': 2.0.0-rc.70(vuepress@2.0.0-rc.19(@vuepress/bundler-vite@2.0.0-rc.19(@types/node@22.10.5)(sass-embedded@1.83.1))(vue@3.5.13)) + '@vuepress/highlighter-helper': 2.0.0-rc.70(@vueuse/core@12.4.0)(vuepress@2.0.0-rc.19(@vuepress/bundler-vite@2.0.0-rc.19(@types/node@22.10.5)(sass-embedded@1.83.1))(vue@3.5.13)) + nanoid: 5.0.9 + shiki: 1.26.2 + vuepress: 2.0.0-rc.19(@vuepress/bundler-vite@2.0.0-rc.19(@types/node@22.10.5)(sass-embedded@1.83.1))(vue@3.5.13) + transitivePeerDependencies: + - '@vueuse/core' + - typescript - /domutils@3.1.0: - resolution: {integrity: sha512-H78uMmQtI2AhgDJjWeQmHwJJ2bLPD3GMmO7Zja/ZZh84wkm+4ut+IUnUdRa8uCGX88DiVx1j6FRe1XfxEgjEZA==} + '@vuepress/plugin-sitemap@2.0.0-rc.70(vuepress@2.0.0-rc.19(@vuepress/bundler-vite@2.0.0-rc.19(@types/node@22.10.5)(sass-embedded@1.83.1))(vue@3.5.13))': dependencies: - dom-serializer: 2.0.0 - domelementtype: 2.3.0 - domhandler: 5.0.3 - dev: false + '@vuepress/helper': 2.0.0-rc.70(vuepress@2.0.0-rc.19(@vuepress/bundler-vite@2.0.0-rc.19(@types/node@22.10.5)(sass-embedded@1.83.1))(vue@3.5.13)) + sitemap: 8.0.0 + vuepress: 2.0.0-rc.19(@vuepress/bundler-vite@2.0.0-rc.19(@types/node@22.10.5)(sass-embedded@1.83.1))(vue@3.5.13) + transitivePeerDependencies: + - typescript - /eastasianwidth@0.2.0: - resolution: {integrity: sha512-I88TYZWc9XiYHRQ4/3c5rjjfgkjhLyW2luGIheGERbNQ6OY7yTybanSpDXZa8y7VUP9YmDcYa+eyq4ca7iLqWA==} - dev: false + '@vuepress/plugin-theme-data@2.0.0-rc.70(vuepress@2.0.0-rc.19(@vuepress/bundler-vite@2.0.0-rc.19(@types/node@22.10.5)(sass-embedded@1.83.1))(vue@3.5.13))': + dependencies: + '@vue/devtools-api': 7.7.0 + vue: 3.5.13 + vuepress: 2.0.0-rc.19(@vuepress/bundler-vite@2.0.0-rc.19(@types/node@22.10.5)(sass-embedded@1.83.1))(vue@3.5.13) + transitivePeerDependencies: + - typescript - /echarts@5.4.2: - resolution: {integrity: sha512-2W3vw3oI2tWJdyAz+b8DuWS0nfXtSDqlDmqgin/lfzbkB01cuMEN66KWBlmur3YMp5nEDEEt5s23pllnAzB4EA==} + '@vuepress/shared@2.0.0-rc.19': dependencies: - tslib: 2.3.0 - zrender: 5.4.3 - dev: false + '@mdit-vue/types': 2.1.0 - /ejs@3.1.9: - resolution: {integrity: sha512-rC+QVNMJWv+MtPgkt0y+0rVEIdbtxVADApW9JXrUVlzHetgcyczP/E7DJmWJ4fJCZF2cPcBk0laWO9ZHMG3DmQ==} - engines: {node: '>=0.10.0'} - hasBin: true + '@vuepress/utils@2.0.0-rc.19': dependencies: - jake: 10.8.6 - dev: false + '@types/debug': 4.1.12 + '@types/fs-extra': 11.0.4 + '@types/hash-sum': 1.0.2 + '@vuepress/shared': 2.0.0-rc.19 + debug: 4.4.0 + fs-extra: 11.2.0 + globby: 14.0.2 + hash-sum: 2.0.0 + ora: 8.1.1 + picocolors: 1.1.1 + upath: 2.0.1 + transitivePeerDependencies: + - supports-color - /electron-to-chromium@1.4.396: - resolution: {integrity: sha512-pqKTdqp/c5vsrc0xUPYXTDBo9ixZuGY8es4ZOjjd6HD6bFYbu5QA09VoW3fkY4LF1T0zYk86lN6bZnNlBuOpdQ==} - dev: false + '@vueuse/core@12.4.0': + dependencies: + '@types/web-bluetooth': 0.0.20 + '@vueuse/metadata': 12.4.0 + '@vueuse/shared': 12.4.0 + vue: 3.5.13 + transitivePeerDependencies: + - typescript - /elkjs@0.8.2: - resolution: {integrity: sha512-L6uRgvZTH+4OF5NE/MBbzQx/WYpru1xCBE9respNj6qznEewGUIfhzmm7horWWxbNO2M0WckQypGctR8lH79xQ==} - dev: false + '@vueuse/metadata@12.4.0': {} - /emoji-regex@8.0.0: - resolution: {integrity: sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==} - dev: false + '@vueuse/shared@12.4.0': + dependencies: + vue: 3.5.13 + transitivePeerDependencies: + - typescript - /emoji-regex@9.2.2: - resolution: {integrity: sha512-L18DaJsXSUk2+42pv8mLs5jJT2hqFkFE4j21wOmgbUqsZ2hL72NsUU785g9RXgo3s0ZNgVl42TiHp3ZtOv/Vyg==} - dev: false + abbrev@1.1.1: + optional: true - /encode-utf8@1.0.3: - resolution: {integrity: sha512-ucAnuBEhUK4boH2HjVYG5Q2mQyPorvv0u/ocS+zhdw0S8AlHYY+GOFhP1Gio5z4icpP2ivFSvhtFjQi8+T9ppw==} - dev: false + abbrev@2.0.0: + optional: true - /entities@3.0.1: - resolution: {integrity: sha512-WiyBqoomrwMdFG1e0kqvASYfnlb0lp8M5o5Fw2OFq1hNZxxcNk8Ik0Xm7LxzBhuidnZB/UtBqVCgUz3kBOP51Q==} - engines: {node: '>=0.12'} - dev: false + agent-base@6.0.2: + dependencies: + debug: 4.4.0 + transitivePeerDependencies: + - supports-color + optional: true - /entities@4.5.0: - resolution: {integrity: sha512-V0hjH4dGPh9Ao5p0MoRY6BVqtwCjhz6vI5LT8AJ55H+4g9/4vbHx1I54fS0XuclLhDHArPQCiMjDxjaL8fPxhw==} - engines: {node: '>=0.12'} - dev: false + agent-base@7.1.3: + optional: true - /envinfo@7.8.1: - resolution: {integrity: sha512-/o+BXHmB7ocbHEAs6F2EnG0ogybVVUdkRunTT2glZU9XAaGmhqskrvKwqXuDfNjEO0LZKWdejEEpnq8aM0tOaw==} - engines: {node: '>=4'} - hasBin: true - dev: false - - /es-abstract@1.21.2: - resolution: {integrity: sha512-y/B5POM2iBnIxCiernH1G7rC9qQoM77lLIMQLuob0zhp8C56Po81+2Nj0WFKnd0pNReDTnkYryc+zhOzpEIROg==} - engines: {node: '>= 0.4'} - dependencies: - array-buffer-byte-length: 1.0.0 - available-typed-arrays: 1.0.5 - call-bind: 1.0.2 - es-set-tostringtag: 2.0.1 - es-to-primitive: 1.2.1 - function.prototype.name: 1.1.5 - get-intrinsic: 1.2.1 - get-symbol-description: 1.0.0 - globalthis: 1.0.3 - gopd: 1.0.1 - has: 1.0.3 - has-property-descriptors: 1.0.0 - has-proto: 1.0.1 - has-symbols: 1.0.3 - internal-slot: 1.0.5 - is-array-buffer: 3.0.2 - is-callable: 1.2.7 - is-negative-zero: 2.0.2 - is-regex: 1.1.4 - is-shared-array-buffer: 1.0.2 - is-string: 1.0.7 - is-typed-array: 1.1.10 - is-weakref: 1.0.2 - object-inspect: 1.12.3 - object-keys: 1.1.1 - object.assign: 4.1.4 - regexp.prototype.flags: 1.5.0 - safe-regex-test: 1.0.0 - string.prototype.trim: 1.2.7 - string.prototype.trimend: 1.0.6 - string.prototype.trimstart: 1.0.6 - typed-array-length: 1.0.4 - unbox-primitive: 1.0.2 - which-typed-array: 1.1.9 - dev: false - - /es-set-tostringtag@2.0.1: - resolution: {integrity: sha512-g3OMbtlwY3QewlqAiMLI47KywjWZoEytKr8pf6iTC8uJq5bIAH52Z9pnQ8pVL6whrCto53JZDuUIsifGeLorTg==} - engines: {node: '>= 0.4'} - dependencies: - get-intrinsic: 1.2.1 - has: 1.0.3 - has-tostringtag: 1.0.0 - dev: false - - /es-to-primitive@1.2.1: - resolution: {integrity: sha512-QCOllgZJtaUo9miYBcLChTUaHNjJF3PYs1VidD7AwiEj1kYxKeQTctLAezAOH5ZKRH0g2IgPn6KwB4IT8iRpvA==} - engines: {node: '>= 0.4'} - dependencies: - is-callable: 1.2.7 - is-date-object: 1.0.5 - is-symbol: 1.0.4 - dev: false - - /es6-promise@4.2.8: - resolution: {integrity: sha512-HJDGx5daxeIvxdBxvG2cb9g4tEvwIk3i8+nhX0yGrYmZUzbkdg8QbDevheDB8gd0//uPj4c1EQua8Q+MViT0/w==} - dev: false - - /esbuild@0.17.19: - resolution: {integrity: sha512-XQ0jAPFkK/u3LcVRcvVHQcTIqD6E2H1fvZMA5dQPSOWb3suUbWbfbRf94pjc0bNzRYLfIrDRQXr7X+LHIm5oHw==} - engines: {node: '>=12'} - hasBin: true - requiresBuild: true - optionalDependencies: - '@esbuild/android-arm': 0.17.19 - '@esbuild/android-arm64': 0.17.19 - '@esbuild/android-x64': 0.17.19 - '@esbuild/darwin-arm64': 0.17.19 - '@esbuild/darwin-x64': 0.17.19 - '@esbuild/freebsd-arm64': 0.17.19 - '@esbuild/freebsd-x64': 0.17.19 - '@esbuild/linux-arm': 0.17.19 - '@esbuild/linux-arm64': 0.17.19 - '@esbuild/linux-ia32': 0.17.19 - '@esbuild/linux-loong64': 0.17.19 - '@esbuild/linux-mips64el': 0.17.19 - '@esbuild/linux-ppc64': 0.17.19 - '@esbuild/linux-riscv64': 0.17.19 - '@esbuild/linux-s390x': 0.17.19 - '@esbuild/linux-x64': 0.17.19 - '@esbuild/netbsd-x64': 0.17.19 - '@esbuild/openbsd-x64': 0.17.19 - '@esbuild/sunos-x64': 0.17.19 - '@esbuild/win32-arm64': 0.17.19 - '@esbuild/win32-ia32': 0.17.19 - '@esbuild/win32-x64': 0.17.19 - dev: false - - /escalade@3.1.1: - resolution: {integrity: sha512-k0er2gUkLf8O0zKJiAhmkTnJlTvINGv7ygDNPbeIsX/TJjGJZHuh9B2UxbsaEkmlEo9MfhrSzmhIlhRlI2GXnw==} - engines: {node: '>=6'} - dev: false + aggregate-error@3.1.0: + dependencies: + clean-stack: 2.2.0 + indent-string: 4.0.0 + optional: true - /escape-string-regexp@1.0.5: - resolution: {integrity: sha512-vbRorB5FUQWvla16U8R/qgaFIya2qGzwDrNmCZuYKrbdSUMG6I1ZCGQRefkRVhuOkIGVne7BQ35DSfo1qvJqFg==} - engines: {node: '>=0.8.0'} - dev: false + ansi-regex@5.0.1: {} - /esm@3.2.25: - resolution: {integrity: sha512-U1suiZ2oDVWv4zPO56S0NcR5QriEahGtdN2OR6FiOG4WJvcjBVFB0qI4+eKoWFH483PKGuLuu6V8Z4T5g63UVA==} - engines: {node: '>=6'} - dev: false + ansi-regex@6.1.0: {} - /esprima@4.0.1: - resolution: {integrity: sha512-eGuFFw7Upda+g4p+QHvnW0RyTX/SVeJBDM/gCtMARO0cLuT2HcEKnTPvhjV6aGeqrCB/sbNop0Kszm0jsaWU4A==} - engines: {node: '>=4'} - hasBin: true - dev: false + ansi-styles@4.3.0: + dependencies: + color-convert: 2.0.1 - /estree-walker@1.0.1: - resolution: {integrity: sha512-1fMXF3YP4pZZVozF8j/ZLfvnR8NSIljt56UhbZ5PeeDmmGHpgpdwQt7ITlGvYaQukCvuBRMLEiKiYC+oeIg4cg==} - dev: false + ansi-styles@6.2.1: + optional: true - /estree-walker@2.0.2: - resolution: {integrity: sha512-Rfkk/Mp/DL7JVje3u18FxFujQlTNR2q6QfMSMB7AvCBx91NGj/ba3kCfza0f6dVDbw7YlRf/nDrn7pQrCCyQ/w==} - dev: false + anymatch@3.1.3: + dependencies: + normalize-path: 3.0.0 + picomatch: 2.3.1 - /esutils@2.0.3: - resolution: {integrity: sha512-kVscqXk4OCp68SZ0dkgEKVi6/8ij300KBWTJq32P/dYeWTSwK41WyTxalN1eRmA5Z9UU/LX9D7FWSmV9SAYx6g==} - engines: {node: '>=0.10.0'} - dev: false - - /eve-raphael@0.5.0: - resolution: {integrity: sha512-jrxnPsCGqng1UZuEp9DecX/AuSyAszATSjf4oEcRxvfxa1Oux4KkIPKBAAWWnpdwfARtr+Q0o9aPYWjsROD7ug==} - dev: false - - /execa@7.1.1: - resolution: {integrity: sha512-wH0eMf/UXckdUYnO21+HDztteVv05rq2GXksxT4fCGeHkBhw1DROXh40wcjMcRqDOWE7iPJ4n3M7e2+YFP+76Q==} - engines: {node: ^14.18.0 || ^16.14.0 || >=18.0.0} - dependencies: - cross-spawn: 7.0.3 - get-stream: 6.0.1 - human-signals: 4.3.1 - is-stream: 3.0.0 - merge-stream: 2.0.0 - npm-run-path: 5.1.0 - onetime: 6.0.0 - signal-exit: 3.0.7 - strip-final-newline: 3.0.0 - dev: false + aproba@2.0.0: + optional: true - /extend-shallow@2.0.1: - resolution: {integrity: sha512-zCnTtlxNoAiDc3gqY2aYAWFx7XWWiasuF2K8Me5WbN8otHKTUKBwjPtNpRs/rbUZm7KxWAaNj7P1a/p52GbVug==} - engines: {node: '>=0.10.0'} + are-we-there-yet@2.0.0: dependencies: - is-extendable: 0.1.1 - dev: false + delegates: 1.0.0 + readable-stream: 3.6.2 + optional: true + + arg@5.0.2: {} - /fast-deep-equal@2.0.1: - resolution: {integrity: sha512-bCK/2Z4zLidyB4ReuIsvALH6w31YfAQDmXMqMx6FyfHqvBxtjC0eRumeSu4Bs3XtXwpyIywtSTrVT99BxY1f9w==} - dev: false + argparse@1.0.10: + dependencies: + sprintf-js: 1.0.3 - /fast-deep-equal@3.1.3: - resolution: {integrity: sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==} - dev: false + argparse@2.0.1: {} - /fast-glob@3.2.12: - resolution: {integrity: sha512-DVj4CQIYYow0BlaelwK1pHl5n5cRSJfM60UA0zK891sVInoPri2Ekj7+e1CT3/3qxXenpI+nBBmQAcJPJgaj4w==} - engines: {node: '>=8.6.0'} + autoprefixer@10.4.20(postcss@8.5.0): dependencies: - '@nodelib/fs.stat': 2.0.5 - '@nodelib/fs.walk': 1.2.8 - glob-parent: 5.1.2 - merge2: 1.4.1 - micromatch: 4.0.5 - dev: false + browserslist: 4.24.4 + caniuse-lite: 1.0.30001692 + fraction.js: 4.3.7 + normalize-range: 0.1.2 + picocolors: 1.1.1 + postcss: 8.5.0 + postcss-value-parser: 4.2.0 - /fast-json-stable-stringify@2.1.0: - resolution: {integrity: sha512-lhd/wF+Lk98HZoTCtlVraHtfh5XYijIjalXck7saUtuanSDyLMxnHhSXEDJqHxD7msR8D0uCmqlkwjCV8xvwHw==} - dev: false + balanced-match@1.0.2: + optional: true - /fastq@1.15.0: - resolution: {integrity: sha512-wBrocU2LCXXa+lWBt8RoIRD89Fi8OdABODa/kEnyeyjS5aZO5/GNvI5sEINADqP/h8M29UHTHUb53sUu5Ihqdw==} - dependencies: - reusify: 1.0.4 - dev: false + balloon-css@1.2.0: {} - /fflate@0.7.4: - resolution: {integrity: sha512-5u2V/CDW15QM1XbbgS+0DfPxVB+jUKhWEKuuFuHncbk3tEEqzmoXL+2KyOFuKGqOnmdIy0/davWF1CkuwtibCw==} - dev: false + bcrypt-ts@5.0.3: {} - /filelist@1.0.4: - resolution: {integrity: sha512-w1cEuf3S+DrLCQL7ET6kz+gmlJdbq9J7yXCSjK/OZCPA+qEN1WyF4ZAf0YYJa4/shHJra2t/d/r8SV4Ji+x+8Q==} - dependencies: - minimatch: 5.1.6 - dev: false + binary-extensions@2.3.0: {} - /fill-range@7.0.1: - resolution: {integrity: sha512-qOo9F+dMUmC2Lcb4BbVvnKJxTPjCm+RRpe4gDuGrzkL7mEVl/djYSu2OdQ2Pa302N4oqkSg9ir6jaLWJ2USVpQ==} - engines: {node: '>=8'} - dependencies: - to-regex-range: 5.0.1 - dev: false + birpc@0.2.19: {} - /find-up@4.1.0: - resolution: {integrity: sha512-PpOwAdQ/YlXQ2vj8a3h8IipDuYRi3wceVQQGYWxNINccq40Anw7BlsEXCMbt1Zt+OLA6Fq9suIpIWD0OsnISlw==} - engines: {node: '>=8'} - dependencies: - locate-path: 5.0.0 - path-exists: 4.0.0 - dev: false + boolbase@1.0.0: {} - /flowchart.ts@0.1.6: - resolution: {integrity: sha512-mPvyKIpsHuDnF/M1oQyClLcqRdnVzvxbyBBijlfz5YU8yJWlV9j2QHvFrqkRd3lFor7uQ1M46dRaTQ/bXXEsNg==} + brace-expansion@1.1.11: dependencies: - '@types/raphael': 2.3.3 - raphael: 2.3.0 - tslib: 2.5.0 - dev: false + balanced-match: 1.0.2 + concat-map: 0.0.1 + optional: true - /for-each@0.3.3: - resolution: {integrity: sha512-jqYfLp7mo9vIyQf8ykW2v7A+2N4QjeCeI5+Dz9XraiO1ign81wjiH7Fb9vSOWvQfNtmSa4H2RoQTrrXivdUZmw==} + brace-expansion@2.0.1: dependencies: - is-callable: 1.2.7 - dev: false + balanced-match: 1.0.2 + optional: true - /foreground-child@3.1.1: - resolution: {integrity: sha512-TMKDUnIte6bfb5nWv7V/caI169OHgvwjb7V4WkeUvbQQdjr5rWKqHFiKWb/fcOwB+CzBT+qbWjvj+DVwRskpIg==} - engines: {node: '>=14'} + braces@3.0.3: dependencies: - cross-spawn: 7.0.3 - signal-exit: 4.0.2 - dev: false - - /fraction.js@4.2.0: - resolution: {integrity: sha512-MhLuK+2gUcnZe8ZHlaaINnQLl0xRIGRfcGk2yl8xoQAfHrSsL3rYu6FCmBdkdbhc9EPlwyGHewaRsvwRMJtAlA==} - dev: false + fill-range: 7.1.1 - /fs-extra@11.1.1: - resolution: {integrity: sha512-MGIE4HOvQCeUCzmlHs0vXpih4ysz4wg9qiSAu6cd42lVwPbTM1TjV7RusoyQqMmk/95gdQZX72u+YW+c3eEpFQ==} - engines: {node: '>=14.14'} + browserslist@4.24.4: dependencies: - graceful-fs: 4.2.11 - jsonfile: 6.1.0 - universalify: 2.0.0 - dev: false + caniuse-lite: 1.0.30001692 + electron-to-chromium: 1.5.80 + node-releases: 2.0.19 + update-browserslist-db: 1.1.2(browserslist@4.24.4) - /fs-extra@9.1.0: - resolution: {integrity: sha512-hcg3ZmepS30/7BSFqRvoo3DOMQu7IjqxO5nCDt+zM9XWjb33Wg7ziNT+Qvqbuc3+gWpzO02JubVyk2G4Zvo1OQ==} - engines: {node: '>=10'} - dependencies: - at-least-node: 1.0.0 - graceful-fs: 4.2.11 - jsonfile: 6.1.0 - universalify: 2.0.0 - dev: false + buffer-builder@0.2.0: {} - /fs.realpath@1.0.0: - resolution: {integrity: sha512-OO0pH2lK6a0hZnAdau5ItzHPI6pUlvI7jMVnxUQRtw4owF2wk8lOSabtGDCTP4Ggrg2MbGnWO9X8K1t4+fGMDw==} - dev: false + cac@6.7.14: {} - /fsevents@2.3.2: - resolution: {integrity: sha512-xiqMQR4xAeHTuB9uWm+fFRcIOgKBMiOBP+eXiyT7jsgVCq1bkVygt00oASowB7EdtpOHaaPgKt812P9ab+DDKA==} - engines: {node: ^8.16.0 || ^10.6.0 || >=11.0.0} - os: [darwin] - requiresBuild: true - dev: false + cacache@18.0.4: + dependencies: + '@npmcli/fs': 3.1.1 + fs-minipass: 3.0.3 + glob: 10.4.5 + lru-cache: 10.4.3 + minipass: 7.1.2 + minipass-collect: 2.0.1 + minipass-flush: 1.0.5 + minipass-pipeline: 1.2.4 + p-map: 4.0.0 + ssri: 10.0.6 + tar: 6.2.1 + unique-filename: 3.0.0 optional: true - /function-bind@1.1.1: - resolution: {integrity: sha512-yIovAzMX49sF8Yl58fSCWJ5svSLuaibPxXQJFLmBObTuCr0Mf1KiPopGM9NiFjiYBCbfaa2Fh6breQ6ANVTI0A==} - dev: false - - /function.prototype.name@1.1.5: - resolution: {integrity: sha512-uN7m/BzVKQnCUF/iW8jYea67v++2u7m5UgENbHRtdDVclOUP+FMPlCNdmk0h/ysGyo2tavMJEDqJAkJdRa1vMA==} - engines: {node: '>= 0.4'} - dependencies: - call-bind: 1.0.2 - define-properties: 1.2.0 - es-abstract: 1.21.2 - functions-have-names: 1.2.3 - dev: false + camelcase@5.3.1: {} - /functions-have-names@1.2.3: - resolution: {integrity: sha512-xckBUXyTIqT97tq2x2AMb+g163b5JFysYk0x4qxNFwbfQkmNZoiRHb6sPzI9/QV33WeuvVYBUIiD4NzNIyqaRQ==} - dev: false + caniuse-lite@1.0.30001692: {} - /gensync@1.0.0-beta.2: - resolution: {integrity: sha512-3hN7NaskYvMDLQY55gnW3NQ+mesEAepTqlg+VEbj7zzqEMBVNhzcGYYeqFo/TlYz6eQiFcp1HcsCZO+nGgS8zg==} - engines: {node: '>=6.9.0'} - dev: false + ccount@2.0.1: {} - /get-caller-file@2.0.5: - resolution: {integrity: sha512-DyFP3BM/3YHTQOCUL/w0OZHR0lpKeGrxotcHWcqNEdnltqFwXVfhEBQ94eIo34AfQpo0rGki4cyIiftY06h2Fg==} - engines: {node: 6.* || 8.* || >= 10.*} - dev: false + chalk@5.4.1: {} - /get-intrinsic@1.2.1: - resolution: {integrity: sha512-2DcsyfABl+gVHEfCOaTrWgyt+tb6MSEGmKq+kI5HwLbIYgjgmMcV8KQ41uaKz1xxUcn9tJtgFbQUEVcEbd0FYw==} - dependencies: - function-bind: 1.1.1 - has: 1.0.3 - has-proto: 1.0.1 - has-symbols: 1.0.3 - dev: false + character-entities-html4@2.1.0: {} - /get-own-enumerable-property-symbols@3.0.2: - resolution: {integrity: sha512-I0UBV/XOz1XkIJHEUDMZAbzCThU/H8DxmSfmdGcKPnVhu2VfFqr34jr9777IyaTYvxjedWhqVIilEDsCdP5G6g==} - dev: false + character-entities-legacy@3.0.0: {} - /get-stdin@9.0.0: - resolution: {integrity: sha512-dVKBjfWisLAicarI2Sf+JuBE/DghV4UzNAVe9yhEJuzeREd3JhOTE9cUaJTeSa77fsbQUK3pcOpJfM59+VKZaA==} - engines: {node: '>=12'} - dev: false + character-entities@2.0.2: {} - /get-stream@6.0.1: - resolution: {integrity: sha512-ts6Wi+2j3jQjqi70w5AlN8DFnkSwC+MqmxEzdEALB2qXZYV3X/b1CTfgPLGJNMeAWxdPfU8FO1ms3NUfaHCPYg==} - engines: {node: '>=10'} - dev: false + character-reference-invalid@2.0.1: {} - /get-symbol-description@1.0.0: - resolution: {integrity: sha512-2EmdH1YvIQiZpltCNgkuiUnyukzxM/R6NDJX31Ke3BG1Nq5b0S2PhX59UKi9vZpPDQVdqn+1IcaAwnzTT5vCjw==} - engines: {node: '>= 0.4'} + cheerio-select@2.1.0: dependencies: - call-bind: 1.0.2 - get-intrinsic: 1.2.1 - dev: false + boolbase: 1.0.0 + css-select: 5.1.0 + css-what: 6.1.0 + domelementtype: 2.3.0 + domhandler: 5.0.3 + domutils: 3.2.2 - /giscus@1.2.8: - resolution: {integrity: sha512-pufrgQYt1W+4ztiWp/PilLPN8NdyKvpbQ8jNqbAa1g84t6qqyevXHfkOYCi4x4d+y191vJAUc6seL1Dq74yUeA==} + cheerio@1.0.0: dependencies: - lit: 2.7.4 - dev: false + cheerio-select: 2.1.0 + dom-serializer: 2.0.0 + domhandler: 5.0.3 + domutils: 3.2.2 + encoding-sniffer: 0.2.0 + htmlparser2: 9.1.0 + parse5: 7.2.1 + parse5-htmlparser2-tree-adapter: 7.1.0 + parse5-parser-stream: 7.1.2 + undici: 6.21.0 + whatwg-mimetype: 4.0.0 - /glob-parent@5.1.2: - resolution: {integrity: sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow==} - engines: {node: '>= 6'} + chokidar@3.6.0: dependencies: + anymatch: 3.1.3 + braces: 3.0.3 + glob-parent: 5.1.2 + is-binary-path: 2.1.0 is-glob: 4.0.3 - dev: false - - /glob@10.2.4: - resolution: {integrity: sha512-fDboBse/sl1oXSLhIp0FcCJgzW9KmhC/q8ULTKC82zc+DL3TL7FNb8qlt5qqXN53MsKEUSIcb+7DLmEygOE5Yw==} - engines: {node: '>=16 || 14 >=14.17'} - hasBin: true - dependencies: - foreground-child: 3.1.1 - jackspeak: 2.2.0 - minimatch: 9.0.0 - minipass: 6.0.1 - path-scurry: 1.9.1 - dev: false + normalize-path: 3.0.0 + readdirp: 3.6.0 + optionalDependencies: + fsevents: 2.3.3 - /glob@7.2.3: - resolution: {integrity: sha512-nFR0zLpU2YCaRxwoCJvL6UvCH2JFyFVIvwTLsIf21AuHlMskA1hhTdk+LlYJtOlYt9v6dvszD2BGRqBL+iQK9Q==} + chokidar@4.0.3: dependencies: - fs.realpath: 1.0.0 - inflight: 1.0.6 - inherits: 2.0.4 - minimatch: 3.1.2 - once: 1.4.0 - path-is-absolute: 1.0.1 - dev: false - - /globals@11.12.0: - resolution: {integrity: sha512-WOBp/EEGUiIsJSp7wcv/y6MO+lV9UoncWqxuFfm8eBwzWNgyfBd6Gz+IeKQ9jCmyhoH99g15M3T+QaVHFjizVA==} - engines: {node: '>=4'} - dev: false + readdirp: 4.1.1 - /globalthis@1.0.3: - resolution: {integrity: sha512-sFdI5LyBiNTHjRd7cGPWapiHWMOXKyuBNX/cWJ3NfzrZQVa8GI/8cofCl74AOVqq9W5kNmguTIzJ/1s2gyI9wA==} - engines: {node: '>= 0.4'} - dependencies: - define-properties: 1.2.0 - dev: false + chownr@2.0.0: + optional: true - /globby@13.1.4: - resolution: {integrity: sha512-iui/IiiW+QrJ1X1hKH5qwlMQyv34wJAYwH1vrf8b9kBA4sNiif3gKsMHa+BrdnOpEudWjpotfa7LrTzB1ERS/g==} - engines: {node: ^12.20.0 || ^14.13.1 || >=16.0.0} - dependencies: - dir-glob: 3.0.1 - fast-glob: 3.2.12 - ignore: 5.2.4 - merge2: 1.4.1 - slash: 4.0.0 - dev: false + clean-stack@2.2.0: + optional: true - /gopd@1.0.1: - resolution: {integrity: sha512-d65bNlIadxvpb/A2abVdlqKqV563juRnZ1Wtk6s1sIR8uNsXR70xqIzVqxVf1eTqDunwT2MkczEeaezCKTZhwA==} + cli-cursor@5.0.0: dependencies: - get-intrinsic: 1.2.1 - dev: false + restore-cursor: 5.1.0 - /graceful-fs@4.2.11: - resolution: {integrity: sha512-RbJ5/jmFcNNCcDV5o9eTnBLJ/HszWV0P73bc+Ff4nS/rJj+YaS6IGyiOL0VoBYX+l1Wrl3k63h/KrH+nhJ0XvQ==} - dev: false + cli-spinners@2.9.2: {} - /gray-matter@4.0.3: - resolution: {integrity: sha512-5v6yZd4JK3eMI3FqqCouswVqwugaA9r4dNZB1wwcmrD02QkV5H0y7XBQW8QwQqEaZY1pM9aqORSORhJRdNK44Q==} - engines: {node: '>=6.0'} + cliui@6.0.0: dependencies: - js-yaml: 3.14.1 - kind-of: 6.0.3 - section-matter: 1.0.0 - strip-bom-string: 1.0.0 - dev: false + string-width: 4.2.3 + strip-ansi: 6.0.1 + wrap-ansi: 6.2.0 - /hanabi@0.4.0: - resolution: {integrity: sha512-ixJH94fwmmVzUSdxl7TMkVZJmsq4d2JKrxedpM5V1V+91iVHL0q6NnJi4xiDahK6Vo00xT17H8H6b4F6RVbsOg==} + color-convert@2.0.1: dependencies: - comment-regex: 1.0.1 - dev: false + color-name: 1.1.4 - /has-bigints@1.0.2: - resolution: {integrity: sha512-tSvCKtBr9lkF0Ex0aQiP9N+OpV4zi2r/Nee5VkRDbaqv35RLYMzbwQfFSZZH0kR+Rd6302UJZ2p/bJCEoR3VoQ==} - dev: false + color-name@1.1.4: {} - /has-flag@3.0.0: - resolution: {integrity: sha512-sKJf1+ceQBr4SMkvQnBDNDtf4TXpVhVGateu0t918bl30FnbE2m4vNLX+VWe/dpjlb+HugGYzW7uQXH98HPEYw==} - engines: {node: '>=4'} - dev: false + color-support@1.1.3: + optional: true - /has-flag@4.0.0: - resolution: {integrity: sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==} - engines: {node: '>=8'} - dev: false + colorjs.io@0.5.2: {} - /has-property-descriptors@1.0.0: - resolution: {integrity: sha512-62DVLZGoiEBDHQyqG4w9xCuZ7eJEwNmJRWw2VY84Oedb7WFcA27fiEVe8oUQx9hAUJ4ekurquucTGwsyO1XGdQ==} - dependencies: - get-intrinsic: 1.2.1 - dev: false + comma-separated-tokens@2.0.3: {} - /has-proto@1.0.1: - resolution: {integrity: sha512-7qE+iP+O+bgF9clE5+UoBFzE65mlBiVj3tKCrlNQ0Ogwm0BjpT/gK4SlLYDMybDh5I3TCTKnPPa0oMG7JDYrhg==} - engines: {node: '>= 0.4'} - dev: false + commander@13.0.0: {} - /has-symbols@1.0.3: - resolution: {integrity: sha512-l3LCuF6MgDNwTDKkdYGEihYjt5pRPbEg46rtlmnSPlUbgmB8LOIrKJbYYFBSbnPaJexMKtiPO8hmeRjRz2Td+A==} - engines: {node: '>= 0.4'} - dev: false + commander@8.3.0: {} - /has-tostringtag@1.0.0: - resolution: {integrity: sha512-kFjcSNhnlGV1kyoGk7OXKSawH5JOb/LzUc5w9B02hOTO0dfFRjbHQKvg1d6cf3HbeUmtU9VbbV3qzZ2Teh97WQ==} - engines: {node: '>= 0.4'} - dependencies: - has-symbols: 1.0.3 - dev: false + commander@9.2.0: {} - /has@1.0.3: - resolution: {integrity: sha512-f2dvO0VU6Oej7RkWJGrehjbzMAjFp5/VKPp5tTpWIV4JHHZK1/BxbFRtf/siA2SWTe09caDmVtYYzWEIbBS4zw==} - engines: {node: '>= 0.4.0'} - dependencies: - function-bind: 1.1.1 - dev: false + concat-map@0.0.1: + optional: true - /hash-sum@2.0.0: - resolution: {integrity: sha512-WdZTbAByD+pHfl/g9QSsBIIwy8IT+EsPiKDs0KNX+zSHhdDLFKdZu0BQHljvO+0QI/BasbMSUa8wYNCZTvhslg==} - dev: false + connect-history-api-fallback@2.0.0: {} - /he@0.5.0: - resolution: {integrity: sha512-DoufbNNOFzwRPy8uecq+j+VCPQ+JyDelHTmSgygrA5TsR8Cbw4Qcir5sGtWiusB4BdT89nmlaVDhSJOqC/33vw==} - hasBin: true - dev: false + console-control-strings@1.1.0: + optional: true - /heap@0.2.7: - resolution: {integrity: sha512-2bsegYkkHO+h/9MGbn6KWcE45cHZgPANo5LXF7EvWdT0yT2EguSVO1nDgU5c8+ZOPwp2vMNa7YFsJhVcDR9Sdg==} - dev: false + copy-anything@3.0.5: + dependencies: + is-what: 4.1.16 - /hls.js@1.4.3: - resolution: {integrity: sha512-EE1MjIYDNO+ynbmCpAWfhUwQpyG8gUcKKuGDGgYgfRmW/g+inQUQ8sVVVY5WZaCxEGxDMGLbXhXGepkmDIMvdw==} - dev: false + create-codepen@2.0.0: {} - /html-entities@1.4.0: - resolution: {integrity: sha512-8nxjcBcd8wovbeKx7h3wTji4e6+rhaVuPNpMqwWgnHh+N9ToqsCs6XztWRBPQ+UtzsoMAdKZtUENoVzU/EMtZA==} - dev: false + cross-spawn@7.0.6: + dependencies: + path-key: 3.1.1 + shebang-command: 2.0.0 + which: 2.0.2 - /htmlparser2@8.0.2: - resolution: {integrity: sha512-GYdjWKDkbRLkZ5geuHs5NY1puJ+PXwP7+fHPRz06Eirsb9ugf6d8kkXav6ADhcODhFFPMIXyxkxSuMf3D6NCFA==} + css-select@5.1.0: dependencies: - domelementtype: 2.3.0 + boolbase: 1.0.0 + css-what: 6.1.0 domhandler: 5.0.3 - domutils: 3.1.0 - entities: 4.5.0 - dev: false + domutils: 3.2.2 + nth-check: 2.1.1 - /human-signals@4.3.1: - resolution: {integrity: sha512-nZXjEF2nbo7lIw3mgYjItAfgQXog3OjJogSbKa2CQIIvSGWcKgeJnQlNXip6NglNzYH45nSRiEVimMvYL8DDqQ==} - engines: {node: '>=14.18.0'} - dev: false + css-what@6.1.0: {} - /husky@8.0.3: - resolution: {integrity: sha512-+dQSyqPh4x1hlO1swXBiNb2HzTDN1I2IGLQx1GrBuiqFJfoMrnZWwVmatvSiO+Iz8fBUnf+lekwNo4c2LlXItg==} - engines: {node: '>=14'} - hasBin: true - dev: false + csstype@3.1.3: {} - /iconv-lite@0.6.3: - resolution: {integrity: sha512-4fCk79wshMdzMp2rH06qWrJE4iolqLhCUH+OiuIgU++RB0+94NlDL81atO7GX55uUKueo0txHNtvEyI6D7WdMw==} - engines: {node: '>=0.10.0'} + dayjs@1.11.13: {} + + debug@4.4.0: dependencies: - safer-buffer: 2.1.2 - dev: false + ms: 2.1.3 - /idb@7.1.1: - resolution: {integrity: sha512-gchesWBzyvGHRO9W8tzUWFDycow5gwjvFKfyV9FF32Y7F50yZMp7mP+T2mJIWFx49zicqyC4uefHM17o6xKIVQ==} - dev: false + decamelize@1.2.0: {} - /ieee754@1.2.1: - resolution: {integrity: sha512-dcyqhDvX1C46lXZcVqCpK+FtMRQVdIMN6/Df5js2zouUsqG7I6sFxitIC+7KYK29KdXOLHdu9zL4sFnoVQnqaA==} - dev: false + decode-named-character-reference@1.0.2: + dependencies: + character-entities: 2.0.2 - /ignore@5.2.4: - resolution: {integrity: sha512-MAb38BcSbH0eHNBxn7ql2NH/kX33OkB3lZ1BNdh7ENeRChHTYsTvWrMubiIAMNS2llXEEgZ1MUOBtXChP3kaFQ==} - engines: {node: '>= 4'} - dev: false + delegates@1.0.0: + optional: true - /immediate@3.0.6: - resolution: {integrity: sha512-XXOFtyqDjNDAQxVfYxuF7g9Il/IbWmmlQg2MYKOH8ExIT1qg6xc4zyS3HaEEATgs1btfzxq15ciUiY7gjSXRGQ==} - dev: false + dequal@2.0.3: {} - /immutable@4.3.0: - resolution: {integrity: sha512-0AOCmOip+xgJwEVTQj1EfiDDOkPmuyllDuTuEX+DDXUgapLAsBIfkg3sxCYyCEA8mQqZrrxPUGjcOQ2JS3WLkg==} - dev: false + detect-libc@2.0.3: + optional: true - /imsc@1.1.3: - resolution: {integrity: sha512-IY0hMkVTNoqoYwKEp5UvNNKp/A5jeJUOrIO7judgOyhHT+xC6PA4VBOMAOhdtAYbMRHx9DTgI8p6Z6jhYQPFDA==} + devlop@1.1.0: dependencies: - sax: 1.2.1 - dev: false + dequal: 2.0.3 - /inflight@1.0.6: - resolution: {integrity: sha512-k92I/b08q4wvFscXCLvqfsHCrjrF7yiXsQuIVvVE7N82W3+aqpzuUdBbfhWcy/FZR3/4IgflMgKLOsvPDrGCJA==} - dependencies: - once: 1.4.0 - wrappy: 1.0.2 - dev: false + dijkstrajs@1.0.3: {} - /inherits@2.0.4: - resolution: {integrity: sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==} - dev: false + dom-serializer@2.0.0: + dependencies: + domelementtype: 2.3.0 + domhandler: 5.0.3 + entities: 4.5.0 - /ini@3.0.1: - resolution: {integrity: sha512-it4HyVAUTKBc6m8e1iXWvXSTdndF7HbdN713+kvLrymxTaU4AUBWrJ4vEooP+V7fexnVD3LKcBshjGGPefSMUQ==} - engines: {node: ^12.13.0 || ^14.15.0 || >=16.0.0} - dev: false + domelementtype@2.3.0: {} - /insane@2.6.2: - resolution: {integrity: sha512-BqEL1CJsjJi+/C/zKZxv31zs3r6zkLH5Nz1WMFb7UBX2KHY2yXDpbFTSEmNHzomBbGDysIfkTX55A0mQZ2CQiw==} + domhandler@5.0.3: dependencies: - assignment: 2.0.0 - he: 0.5.0 - dev: false + domelementtype: 2.3.0 - /internal-slot@1.0.5: - resolution: {integrity: sha512-Y+R5hJrzs52QCG2laLn4udYVnxsfny9CpOhNhUvk/SSSVyF6T27FzRbF0sroPidSu3X8oEAkOn2K804mjpt6UQ==} - engines: {node: '>= 0.4'} + domutils@3.2.2: dependencies: - get-intrinsic: 1.2.1 - has: 1.0.3 - side-channel: 1.0.4 - dev: false + dom-serializer: 2.0.0 + domelementtype: 2.3.0 + domhandler: 5.0.3 - /internmap@2.0.3: - resolution: {integrity: sha512-5Hh7Y1wQbvY5ooGgPbDaL5iYLAPzMTUrjMulskHLH6wnv/A+1q5rgEaiuqEjB+oxGXIVZs1FF+R/KPN3ZSQYYg==} - engines: {node: '>=12'} - dev: false + eastasianwidth@0.2.0: + optional: true - /is-alphabetical@1.0.4: - resolution: {integrity: sha512-DwzsA04LQ10FHTZuL0/grVDk4rFoVH1pjAToYwBrHSxcrBIGQuXrQMtD5U1b0U2XVgKZCTLLP8u2Qxqhy3l2Vg==} - dev: false + electron-to-chromium@1.5.80: {} - /is-alphanumerical@1.0.4: - resolution: {integrity: sha512-UzoZUr+XfVz3t3v4KyGEniVL9BDRoQtY7tOyrRybkVNjDFWyo1yhXNGrrBTQxp3ib9BLAWs7k2YKBQsFRkZG9A==} - dependencies: - is-alphabetical: 1.0.4 - is-decimal: 1.0.4 - dev: false + emoji-regex-xs@1.0.0: {} - /is-array-buffer@3.0.2: - resolution: {integrity: sha512-y+FyyR/w8vfIRq4eQcM1EYgSTnmHXPqaF+IgzgraytCFq5Xh8lllDVmAZolPJiZttZLeFSINPYMaEJ7/vWUa1w==} - dependencies: - call-bind: 1.0.2 - get-intrinsic: 1.2.1 - is-typed-array: 1.1.10 - dev: false + emoji-regex@10.4.0: {} - /is-bigint@1.0.4: - resolution: {integrity: sha512-zB9CruMamjym81i2JZ3UMn54PKGsQzsJeo6xvN3HJJ4CAsQNB6iRutp2To77OfCNuoxspsIhzaPoO1zyCEhFOg==} - dependencies: - has-bigints: 1.0.2 - dev: false + emoji-regex@8.0.0: {} - /is-binary-path@2.1.0: - resolution: {integrity: sha512-ZMERYes6pDydyuGidse7OsHxtbI7WVeUEozgR/g7rd0xUimYNlvZRE/K2MgZTjWy725IfelLeVcEM97mmtRGXw==} - engines: {node: '>=8'} + emoji-regex@9.2.2: + optional: true + + encoding-sniffer@0.2.0: dependencies: - binary-extensions: 2.2.0 - dev: false + iconv-lite: 0.6.3 + whatwg-encoding: 3.1.1 - /is-boolean-object@1.1.2: - resolution: {integrity: sha512-gDYaKHJmnj4aWxyj6YHyXVpdQawtVLHU5cb+eztPGczf6cjuTdwve5ZIEfgXqH4e57An1D1AKf8CZ3kYrQRqYA==} - engines: {node: '>= 0.4'} + encoding@0.1.13: dependencies: - call-bind: 1.0.2 - has-tostringtag: 1.0.0 - dev: false + iconv-lite: 0.6.3 + optional: true - /is-callable@1.2.7: - resolution: {integrity: sha512-1BC0BVFhS/p0qtw6enp8e+8OD0UrK0oFLztSjNzhcKA3WDuJxxAPXzPuPtKkjEY9UUoEWlX/8fgKeu2S8i9JTA==} - engines: {node: '>= 0.4'} - dev: false + entities@4.5.0: {} - /is-core-module@2.12.0: - resolution: {integrity: sha512-RECHCBCd/viahWmwj6enj19sKbHfJrddi/6cBDsNTKbNq0f7VeaUkBo60BqzvPqo/W54ChS62Z5qyun7cfOMqQ==} - dependencies: - has: 1.0.3 - dev: false + env-paths@2.2.1: + optional: true - /is-date-object@1.0.5: - resolution: {integrity: sha512-9YQaSxsAiSwcvS33MBk3wTCVnWK+HhF8VZR2jRxehM16QcVOdHqPn4VPHmRK4lSr38n9JriurInLcP90xsYNfQ==} - engines: {node: '>= 0.4'} - dependencies: - has-tostringtag: 1.0.0 - dev: false + envinfo@7.14.0: {} - /is-decimal@1.0.4: - resolution: {integrity: sha512-RGdriMmQQvZ2aqaQq3awNA6dCGtKpiDFcOzrTWrDAT2MiWrKQVPmxLGHl7Y2nNu6led0kEyoX0enY0qXYsv9zw==} - dev: false + err-code@2.0.3: + optional: true - /is-extendable@0.1.1: - resolution: {integrity: sha512-5BMULNob1vgFX6EjQw5izWDxrecWK9AM72rugNr0TFldMOi0fj6Jk+zeKIt0xGj4cEfQIJth4w3OKWOJ4f+AFw==} - engines: {node: '>=0.10.0'} - dev: false + esbuild@0.21.5: + optionalDependencies: + '@esbuild/aix-ppc64': 0.21.5 + '@esbuild/android-arm': 0.21.5 + '@esbuild/android-arm64': 0.21.5 + '@esbuild/android-x64': 0.21.5 + '@esbuild/darwin-arm64': 0.21.5 + '@esbuild/darwin-x64': 0.21.5 + '@esbuild/freebsd-arm64': 0.21.5 + '@esbuild/freebsd-x64': 0.21.5 + '@esbuild/linux-arm': 0.21.5 + '@esbuild/linux-arm64': 0.21.5 + '@esbuild/linux-ia32': 0.21.5 + '@esbuild/linux-loong64': 0.21.5 + '@esbuild/linux-mips64el': 0.21.5 + '@esbuild/linux-ppc64': 0.21.5 + '@esbuild/linux-riscv64': 0.21.5 + '@esbuild/linux-s390x': 0.21.5 + '@esbuild/linux-x64': 0.21.5 + '@esbuild/netbsd-x64': 0.21.5 + '@esbuild/openbsd-x64': 0.21.5 + '@esbuild/sunos-x64': 0.21.5 + '@esbuild/win32-arm64': 0.21.5 + '@esbuild/win32-ia32': 0.21.5 + '@esbuild/win32-x64': 0.21.5 + + esbuild@0.24.2: + optionalDependencies: + '@esbuild/aix-ppc64': 0.24.2 + '@esbuild/android-arm': 0.24.2 + '@esbuild/android-arm64': 0.24.2 + '@esbuild/android-x64': 0.24.2 + '@esbuild/darwin-arm64': 0.24.2 + '@esbuild/darwin-x64': 0.24.2 + '@esbuild/freebsd-arm64': 0.24.2 + '@esbuild/freebsd-x64': 0.24.2 + '@esbuild/linux-arm': 0.24.2 + '@esbuild/linux-arm64': 0.24.2 + '@esbuild/linux-ia32': 0.24.2 + '@esbuild/linux-loong64': 0.24.2 + '@esbuild/linux-mips64el': 0.24.2 + '@esbuild/linux-ppc64': 0.24.2 + '@esbuild/linux-riscv64': 0.24.2 + '@esbuild/linux-s390x': 0.24.2 + '@esbuild/linux-x64': 0.24.2 + '@esbuild/netbsd-arm64': 0.24.2 + '@esbuild/netbsd-x64': 0.24.2 + '@esbuild/openbsd-arm64': 0.24.2 + '@esbuild/openbsd-x64': 0.24.2 + '@esbuild/sunos-x64': 0.24.2 + '@esbuild/win32-arm64': 0.24.2 + '@esbuild/win32-ia32': 0.24.2 + '@esbuild/win32-x64': 0.24.2 + + escalade@3.2.0: {} + + esm@3.2.25: {} + + esprima@4.0.1: {} + + estree-walker@2.0.2: {} + + execa@9.5.2: + dependencies: + '@sindresorhus/merge-streams': 4.0.0 + cross-spawn: 7.0.6 + figures: 6.1.0 + get-stream: 9.0.1 + human-signals: 8.0.0 + is-plain-obj: 4.1.0 + is-stream: 4.0.1 + npm-run-path: 6.0.0 + pretty-ms: 9.2.0 + signal-exit: 4.1.0 + strip-final-newline: 4.0.0 + yoctocolors: 2.1.1 + + exponential-backoff@3.1.1: + optional: true - /is-extglob@2.1.1: - resolution: {integrity: sha512-SbKbANkN603Vi4jEZv49LeVJMn4yGwsbzZworEoyEiutsN3nJYdbO36zfhGJ6QEDpOZIFkDtnq5JRxmvl3jsoQ==} - engines: {node: '>=0.10.0'} - dev: false + extend-shallow@2.0.1: + dependencies: + is-extendable: 0.1.1 - /is-fullwidth-code-point@3.0.0: - resolution: {integrity: sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg==} - engines: {node: '>=8'} - dev: false + fast-glob@3.3.3: + dependencies: + '@nodelib/fs.stat': 2.0.5 + '@nodelib/fs.walk': 1.2.8 + glob-parent: 5.1.2 + merge2: 1.4.1 + micromatch: 4.0.8 - /is-glob@4.0.3: - resolution: {integrity: sha512-xelSayHH36ZgE7ZWhli7pW34hNbNl8Ojv5KVmkJD4hBdD3th8Tfk9vYasLM+mXWOZhFkgZfxhLSnrwRr4elSSg==} - engines: {node: '>=0.10.0'} + fastq@1.18.0: dependencies: - is-extglob: 2.1.1 - dev: false + reusify: 1.0.4 - /is-interactive@2.0.0: - resolution: {integrity: sha512-qP1vozQRI+BMOPcjFzrjXuQvdak2pHNUMZoeG2eRbiSqyvbEf/wQtEOTOX1guk6E3t36RkaqiSt8A/6YElNxLQ==} - engines: {node: '>=12'} - dev: false + fflate@0.8.2: {} - /is-module@1.0.0: - resolution: {integrity: sha512-51ypPSPCoTEIN9dy5Oy+h4pShgJmPCygKfyRCISBI+JoWT/2oJvK8QPxmwv7b/p239jXrm9M1mlQbyKJ5A152g==} - dev: false + figures@6.1.0: + dependencies: + is-unicode-supported: 2.1.0 - /is-negative-zero@2.0.2: - resolution: {integrity: sha512-dqJvarLawXsFbNDeJW7zAz8ItJ9cd28YufuuFzh0G8pNHjJMnY08Dv7sYX2uF5UpQOwieAeOExEYAWWfu7ZZUA==} - engines: {node: '>= 0.4'} - dev: false + fill-range@7.1.1: + dependencies: + to-regex-range: 5.0.1 - /is-number-object@1.0.7: - resolution: {integrity: sha512-k1U0IRzLMo7ZlYIfzRu23Oh6MiIFasgpb9X76eqfFZAqwH44UI4KTBvBYIZ1dSL9ZzChTB9ShHfLkR4pdW5krQ==} - engines: {node: '>= 0.4'} + find-up@4.1.0: dependencies: - has-tostringtag: 1.0.0 - dev: false + locate-path: 5.0.0 + path-exists: 4.0.0 - /is-number@7.0.0: - resolution: {integrity: sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng==} - engines: {node: '>=0.12.0'} - dev: false + foreground-child@3.3.0: + dependencies: + cross-spawn: 7.0.6 + signal-exit: 4.1.0 + optional: true - /is-obj@1.0.1: - resolution: {integrity: sha512-l4RyHgRqGN4Y3+9JHVrNqO+tN0rV5My76uW5/nuO4K1b6vw5G8d/cmFjP9tRfEsdhZNt0IFdZuK/c2Vr4Nb+Qg==} - engines: {node: '>=0.10.0'} - dev: false + fraction.js@4.3.7: {} - /is-regex@1.1.4: - resolution: {integrity: sha512-kvRdxDsxZjhzUX07ZnLydzS1TU/TJlTUHHY4YLL87e37oUA49DfkLqgy+VjFocowy29cKvcSiu+kIv728jTTVg==} - engines: {node: '>= 0.4'} + fs-extra@11.2.0: dependencies: - call-bind: 1.0.2 - has-tostringtag: 1.0.0 - dev: false + graceful-fs: 4.2.11 + jsonfile: 6.1.0 + universalify: 2.0.1 - /is-regexp@1.0.0: - resolution: {integrity: sha512-7zjFAPO4/gwyQAAgRRmqeEeyIICSdmCqa3tsVHMdBzaXXRiqopZL4Cyghg/XulGWrtABTpbnYYzzIRffLkP4oA==} - engines: {node: '>=0.10.0'} - dev: false + fs-minipass@2.1.0: + dependencies: + minipass: 3.3.6 + optional: true - /is-shared-array-buffer@1.0.2: - resolution: {integrity: sha512-sqN2UDu1/0y6uvXyStCOzyhAjCSlHceFoMKJW8W9EU9cvic/QdsZ0kEU93HEy3IUEFZIiH/3w+AH/UQbPHNdhA==} + fs-minipass@3.0.3: dependencies: - call-bind: 1.0.2 - dev: false + minipass: 7.1.2 + optional: true - /is-stream@2.0.1: - resolution: {integrity: sha512-hFoiJiTl63nn+kstHGBtewWSKnQLpyb155KHheA1l39uvtO9nWIop1p3udqPcUd/xbF1VLMO4n7OI6p7RbngDg==} - engines: {node: '>=8'} - dev: false + fs.realpath@1.0.0: + optional: true - /is-stream@3.0.0: - resolution: {integrity: sha512-LnQR4bZ9IADDRSkvpqMGvt/tEJWclzklNgSw48V5EAaAeDd6qGvN8ei6k5p0tvxSR171VmGyHuTiAOfxAbr8kA==} - engines: {node: ^12.20.0 || ^14.13.1 || >=16.0.0} - dev: false + fsevents@2.3.3: + optional: true - /is-string@1.0.7: - resolution: {integrity: sha512-tE2UXzivje6ofPW7l23cjDOMa09gb7xlAqG6jG5ej6uPV32TlWP3NKPigtaGeHNu9fohccRYvIiZMfOOnOYUtg==} - engines: {node: '>= 0.4'} + gauge@3.0.2: dependencies: - has-tostringtag: 1.0.0 - dev: false + aproba: 2.0.0 + color-support: 1.1.3 + console-control-strings: 1.1.0 + has-unicode: 2.0.1 + object-assign: 4.1.1 + signal-exit: 3.0.7 + string-width: 4.2.3 + strip-ansi: 6.0.1 + wide-align: 1.1.5 + optional: true - /is-symbol@1.0.4: - resolution: {integrity: sha512-C/CPBqKWnvdcxqIARxyOh4v1UUEOCHpgDa0WYgpKDFMszcrPcffg5uhwSgPCLD2WWxmq6isisz87tzT01tuGhg==} - engines: {node: '>= 0.4'} - dependencies: - has-symbols: 1.0.3 - dev: false + get-caller-file@2.0.5: {} - /is-typed-array@1.1.10: - resolution: {integrity: sha512-PJqgEHiWZvMpaFZ3uTc8kHPM4+4ADTlDniuQL7cU/UDA0Ql7F70yGfHph3cLNe+c9toaigv+DFzTJKhc2CtO6A==} - engines: {node: '>= 0.4'} - dependencies: - available-typed-arrays: 1.0.5 - call-bind: 1.0.2 - for-each: 0.3.3 - gopd: 1.0.1 - has-tostringtag: 1.0.0 - dev: false + get-east-asian-width@1.3.0: {} - /is-unicode-supported@1.3.0: - resolution: {integrity: sha512-43r2mRvz+8JRIKnWJ+3j8JtjRKZ6GmjzfaE/qiBJnikNnYv/6bagRJ1kUhNk8R5EX/GkobD+r+sfxCPJsiKBLQ==} - engines: {node: '>=12'} - dev: false + get-stream@9.0.1: + dependencies: + '@sec-ant/readable-stream': 0.4.1 + is-stream: 4.0.1 - /is-weakref@1.0.2: - resolution: {integrity: sha512-qctsuLZmIQ0+vSSMfoVvyFe2+GSEvnmZ2ezTup1SBse9+twCCeial6EEi3Nc2KFcf6+qz2FBPnjXsk8xhKSaPQ==} + giscus@1.6.0: dependencies: - call-bind: 1.0.2 - dev: false + lit: 3.2.1 - /isexe@2.0.0: - resolution: {integrity: sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==} - dev: false + glob-parent@5.1.2: + dependencies: + is-glob: 4.0.3 - /jackspeak@2.2.0: - resolution: {integrity: sha512-r5XBrqIJfwRIjRt/Xr5fv9Wh09qyhHfKnYddDlpM+ibRR20qrYActpCAgU6U+d53EOEjzkvxPMVHSlgR7leXrQ==} - engines: {node: '>=14'} + glob@10.4.5: dependencies: - '@isaacs/cliui': 8.0.2 - optionalDependencies: - '@pkgjs/parseargs': 0.11.0 - dev: false + foreground-child: 3.3.0 + jackspeak: 3.4.3 + minimatch: 9.0.5 + minipass: 7.1.2 + package-json-from-dist: 1.0.1 + path-scurry: 1.11.1 + optional: true - /jake@10.8.6: - resolution: {integrity: sha512-G43Ub9IYEFfu72sua6rzooi8V8Gz2lkfk48rW20vEWCGizeaEPlKB1Kh8JIA84yQbiAEfqlPmSpGgCKKxH3rDA==} - engines: {node: '>=10'} - hasBin: true + glob@7.2.3: dependencies: - async: 3.2.4 - chalk: 4.1.2 - filelist: 1.0.4 + fs.realpath: 1.0.0 + inflight: 1.0.6 + inherits: 2.0.4 minimatch: 3.1.2 - dev: false + once: 1.4.0 + path-is-absolute: 1.0.1 + optional: true - /jest-worker@26.6.2: - resolution: {integrity: sha512-KWYVV1c4i+jbMpaBC+U++4Va0cp8OisU185o73T1vo99hqi7w8tSJfUXYswwqqrjzwxa6KpRK54WhPvwf5w6PQ==} - engines: {node: '>= 10.13.0'} + globby@14.0.2: dependencies: - '@types/node': 20.1.5 - merge-stream: 2.0.0 - supports-color: 7.2.0 - dev: false + '@sindresorhus/merge-streams': 2.3.0 + fast-glob: 3.3.3 + ignore: 5.3.2 + path-type: 5.0.0 + slash: 5.1.0 + unicorn-magic: 0.1.0 - /js-tokens@4.0.0: - resolution: {integrity: sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==} - dev: false + graceful-fs@4.2.11: {} - /js-yaml@3.14.1: - resolution: {integrity: sha512-okMH7OXXJ7YrN9Ok3/SXrnu4iX9yOk+25nqX4imS2npuvTYDmo/QEZoqwZkYaIDk3jVvBOTOIEgEhaLOynBS9g==} - hasBin: true + gray-matter@4.0.3: dependencies: - argparse: 1.0.10 - esprima: 4.0.1 - dev: false + js-yaml: 3.14.1 + kind-of: 6.0.3 + section-matter: 1.0.0 + strip-bom-string: 1.0.0 - /js-yaml@4.1.0: - resolution: {integrity: sha512-wpxZs9NoxZaJESJGIZTyDEaYpl0FKSA+FB9aJiyemKhMwkxQg63h4T1KJgUGHpTqPDNRcmmYLugrRjJlBtWvRA==} - hasBin: true - dependencies: - argparse: 2.0.1 - dev: false + has-flag@4.0.0: {} - /jsesc@0.5.0: - resolution: {integrity: sha512-uZz5UnB7u4T9LvwmFqXii7pZSouaRPorGs5who1Ip7VO0wxanFvBL7GkM6dTHlgX+jhBApRetaWpnDabOeTcnA==} - hasBin: true - dev: false + has-unicode@2.0.1: + optional: true - /jsesc@2.5.2: - resolution: {integrity: sha512-OYu7XEzjkCQ3C5Ps3QIZsQfNpqoJyZZA99wd9aWd05NCtC5pWOkShK2mkL6HXQR6/Cy2lbNdPlZBpuQHXE63gA==} - engines: {node: '>=4'} - hasBin: true - dev: false + hash-sum@2.0.0: {} - /json-schema-traverse@1.0.0: - resolution: {integrity: sha512-NM8/P9n3XjXhIZn1lLhkFaACTOURQXjWhV4BA/RnOv8xvgqtqpAX9IO4mRQxSx1Rlo4tqzeqb0sOlruaOy3dug==} - dev: false + hast-util-to-html@9.0.4: + dependencies: + '@types/hast': 3.0.4 + '@types/unist': 3.0.3 + ccount: 2.0.1 + comma-separated-tokens: 2.0.3 + hast-util-whitespace: 3.0.0 + html-void-elements: 3.0.0 + mdast-util-to-hast: 13.2.0 + property-information: 6.5.0 + space-separated-tokens: 2.0.2 + stringify-entities: 4.0.4 + zwitch: 2.0.4 - /json-schema@0.4.0: - resolution: {integrity: sha512-es94M3nTIfsEPisRafak+HDLfHXnKBhV3vU5eqPcS3flIWqcxJWgXHXiey3YrpaNsanY5ei1VoYEbOzijuq9BA==} - dev: false + hast-util-whitespace@3.0.0: + dependencies: + '@types/hast': 3.0.4 - /json5@2.2.3: - resolution: {integrity: sha512-XmOWe7eyHYH14cLdVPoyg+GOH3rYX++KpzrylJwSW98t3Nk+U8XOl8FWKOgwtzdb8lXGf6zYwDUzeHMWfxasyg==} - engines: {node: '>=6'} - hasBin: true - dev: false + hookable@5.5.3: {} - /jsonc-parser@3.2.0: - resolution: {integrity: sha512-gfFQZrcTc8CnKXp6Y4/CBT3fTc0OVuDofpre4aEeEpSBPV5X5v4+Vmx+8snU7RLPrNHPKSgLxGo9YuQzz20o+w==} - dev: false + html-void-elements@3.0.0: {} - /jsonfile@6.1.0: - resolution: {integrity: sha512-5dgndWOriYSm5cnYaJNhalLNDKOqFwyDB/rr1E9ZsGciGvKPs8R2xYGCacuf3z6K1YKDz182fd+fY3cn3pMqXQ==} + htmlparser2@9.1.0: dependencies: - universalify: 2.0.0 - optionalDependencies: - graceful-fs: 4.2.11 - dev: false + domelementtype: 2.3.0 + domhandler: 5.0.3 + domutils: 3.2.2 + entities: 4.5.0 - /jsonpointer@5.0.1: - resolution: {integrity: sha512-p/nXbhSEcu3pZRdkW1OfJhpsVtW1gd4Wa1fnQc9YLiTfAjn0312eMKimbdIQzuZl9aa9xUGaRlP9T/CJE/ditQ==} - engines: {node: '>=0.10.0'} - dev: false + http-cache-semantics@4.1.1: + optional: true - /katex@0.16.7: - resolution: {integrity: sha512-Xk9C6oGKRwJTfqfIbtr0Kes9OSv6IFsuhFGc7tW4urlpMJtuh+7YhzU6YEG9n8gmWKcMAFzkp7nr+r69kV0zrA==} - hasBin: true + http-proxy-agent@7.0.2: dependencies: - commander: 8.3.0 - dev: false - - /khroma@2.0.0: - resolution: {integrity: sha512-2J8rDNlQWbtiNYThZRvmMv5yt44ZakX+Tz5ZIp/mN1pt4snn+m030Va5Z4v8xA0cQFDXBwO/8i42xL4QPsVk3g==} - dev: false + agent-base: 7.1.3 + debug: 4.4.0 + transitivePeerDependencies: + - supports-color + optional: true - /kind-of@6.0.3: - resolution: {integrity: sha512-dcS1ul+9tmeD95T+x28/ehLgd9mENa3LsvDTtzm3vyBEO7RPptvAD+t44WVXaUjTBRcrpFeFlC8WCruUR456hw==} - engines: {node: '>=0.10.0'} - dev: false + https-proxy-agent@5.0.1: + dependencies: + agent-base: 6.0.2 + debug: 4.4.0 + transitivePeerDependencies: + - supports-color + optional: true - /layout-base@1.0.2: - resolution: {integrity: sha512-8h2oVEZNktL4BH2JCOI90iD1yXwL6iNW7KcCKT2QZgQJR2vbqDsldCTPRU9NifTCqHZci57XvQQ15YTu+sTYPg==} - dev: false + https-proxy-agent@7.0.6: + dependencies: + agent-base: 7.1.3 + debug: 4.4.0 + transitivePeerDependencies: + - supports-color + optional: true - /layout-base@2.0.1: - resolution: {integrity: sha512-dp3s92+uNI1hWIpPGH3jK2kxE2lMjdXdr+DH8ynZHpd6PUlH6x6cbuXnoMmiNumznqaNO31xu9e79F0uuZ0JFg==} - dev: false + human-signals@8.0.0: {} - /leven@3.1.0: - resolution: {integrity: sha512-qsda+H8jTaUaN/x5vzW2rzc+8Rw4TAQ/4KjB46IwK5VH+IlVeeeje/EoZRpiXvIqjFgK84QffqPztGI3VBLG1A==} - engines: {node: '>=6'} - dev: false + husky@9.1.7: {} - /lie@3.1.1: - resolution: {integrity: sha512-RiNhHysUjhrDQntfYSfY4MU24coXXdEOgw9WGcKHNeEwffDYbF//u87M1EWaMGzuFoSbqW0C9C6lEEhDOAswfw==} + iconv-lite@0.6.3: dependencies: - immediate: 3.0.6 - dev: false + safer-buffer: 2.1.2 - /lilconfig@2.1.0: - resolution: {integrity: sha512-utWOt/GHzuUxnLKxB6dk81RoOeoNeHgbrXiuGk4yyF5qlRz+iIVWu56E2fqGHFrXz0QNUhLB/8nKqvRH66JKGQ==} - engines: {node: '>=10'} - dev: false + ignore@5.3.2: {} - /linkify-it@4.0.1: - resolution: {integrity: sha512-C7bfi1UZmoj8+PQx22XyeXCuBlokoyWQL5pWSP+EI6nzRylyThouddufc2c1NDIcP9k5agmN9fLpA7VNJfIiqw==} - dependencies: - uc.micro: 1.0.6 - dev: false + immutable@5.0.3: {} - /lit-element@3.3.2: - resolution: {integrity: sha512-xXAeVWKGr4/njq0rGC9dethMnYCq5hpKYrgQZYTzawt9YQhMiXfD+T1RgrdY3NamOxwq2aXlb0vOI6e29CKgVQ==} - dependencies: - '@lit-labs/ssr-dom-shim': 1.1.1 - '@lit/reactive-element': 1.6.1 - lit-html: 2.7.4 - dev: false + imurmurhash@0.1.4: + optional: true + + indent-string@4.0.0: + optional: true - /lit-html@2.7.4: - resolution: {integrity: sha512-/Jw+FBpeEN+z8X6PJva5n7+0MzCVAH2yypN99qHYYkq8bI+j7I39GH+68Z/MZD6rGKDK9RpzBw7CocfmHfq6+g==} + inflight@1.0.6: dependencies: - '@types/trusted-types': 2.0.3 - dev: false + once: 1.4.0 + wrappy: 1.0.2 + optional: true - /lit@2.7.4: - resolution: {integrity: sha512-cgD7xrZoYr21mbrkZIuIrj98YTMw/snJPg52deWVV4A8icLyNHI3bF70xsJeAgwTuiq5Kkd+ZR8gybSJDCPB7g==} + inherits@2.0.4: + optional: true + + ip-address@9.0.5: dependencies: - '@lit/reactive-element': 1.6.1 - lit-element: 3.3.2 - lit-html: 2.7.4 - dev: false + jsbn: 1.1.0 + sprintf-js: 1.1.3 + optional: true - /loadjs@4.2.0: - resolution: {integrity: sha512-AgQGZisAlTPbTEzrHPb6q+NYBMD+DP9uvGSIjSUM5uG+0jG15cb8axWpxuOIqrmQjn6scaaH8JwloiP27b2KXA==} - dev: false + is-alphabetical@2.0.1: {} - /localforage@1.10.0: - resolution: {integrity: sha512-14/H1aX7hzBBmmh7sGPd+AOMkkIrHM3Z1PAyGgZigA1H1p5O5ANnMyWzvpAETtG68/dC4pC0ncy3+PPGzXZHPg==} + is-alphanumerical@2.0.1: dependencies: - lie: 3.1.1 - dev: false + is-alphabetical: 2.0.1 + is-decimal: 2.0.1 - /locate-path@5.0.0: - resolution: {integrity: sha512-t7hw9pI+WvuwNJXwk5zVHpyhIqzg2qTlklJOf0mVxGSbe3Fp2VieZcduNYjaLDoy6p9uGpQEGWG87WpMKlNq8g==} - engines: {node: '>=8'} + is-binary-path@2.1.0: dependencies: - p-locate: 4.1.0 - dev: false + binary-extensions: 2.3.0 - /lodash-es@4.17.21: - resolution: {integrity: sha512-mKnC+QJ9pWVzv+C4/U3rRsHapFfHvQFoFB92e52xeyGMcX6/OlIl78je1u8vePzYZSkkogMPJ2yjxxsb89cxyw==} - dev: false + is-decimal@2.0.1: {} - /lodash.debounce@4.0.8: - resolution: {integrity: sha512-FT1yDzDYEoYWhnSGnpE/4Kj1fLZkDFyqRb7fNt6FdYOSxlUWAtp42Eh6Wb0rGIv/m9Bgo7x4GhQbm5Ys4SG5ow==} - dev: false + is-extendable@0.1.1: {} - /lodash.sortby@4.7.0: - resolution: {integrity: sha512-HDWXG8isMntAyRF5vZ7xKuEvOhT4AhlRt/3czTSjvGUxjYCBVRQY48ViDHyfYz9VIoBkW4TMGQNapx+l3RUwdA==} - dev: false + is-extglob@2.1.1: {} - /lodash@4.17.21: - resolution: {integrity: sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg==} - dev: false + is-fullwidth-code-point@3.0.0: {} - /log-symbols@5.1.0: - resolution: {integrity: sha512-l0x2DvrW294C9uDCoQe1VSU4gf529FkSZ6leBl4TiqZH/e+0R7hSfHQBNut2mNygDgHwvYHfFLn6Oxb3VWj2rA==} - engines: {node: '>=12'} + is-glob@4.0.3: dependencies: - chalk: 5.2.0 - is-unicode-supported: 1.3.0 - dev: false + is-extglob: 2.1.1 - /loose-envify@1.4.0: - resolution: {integrity: sha512-lyuxPGr/Wfhrlem2CL/UcnUc1zcqKAImBDzukY7Y5F/yQiNdko6+fRLevlw1HgMySw7f611UIY408EtxRSoK3Q==} - hasBin: true - dependencies: - js-tokens: 4.0.0 - dev: false + is-hexadecimal@2.0.1: {} - /lru-cache@5.1.1: - resolution: {integrity: sha512-KpNARQA3Iwv+jTA0utUVVbrh+Jlrr1Fv0e56GGzAFOXN7dk/FviaDW8LHmK52DlcH4WP2n6gI8vN1aesBFgo9w==} - dependencies: - yallist: 3.1.1 - dev: false + is-interactive@2.0.0: {} - /lru-cache@6.0.0: - resolution: {integrity: sha512-Jo6dJ04CmSjuznwJSS3pUeWmd/H0ffTlkXXgwZi+eq1UCmqQwCh+eLsYOYCwY991i2Fah4h1BEMCx4qThGbsiA==} - engines: {node: '>=10'} - dependencies: - yallist: 4.0.0 - dev: false + is-lambda@1.0.1: + optional: true - /lru-cache@9.1.1: - resolution: {integrity: sha512-65/Jky17UwSb0BuB9V+MyDpsOtXKmYwzhyl+cOa9XUiI4uV2Ouy/2voFP3+al0BjZbJgMBD8FojMpAf+Z+qn4A==} - engines: {node: 14 || >=16.14} - dev: false + is-number@7.0.0: {} - /magic-string@0.25.9: - resolution: {integrity: sha512-RmF0AsMzgt25qzqqLc1+MbHmhdx0ojF2Fvs4XnOqz2ZOBXzzkEwc/dJQZCYHAn7v1jbVOjAZfK8msRn4BxO4VQ==} - dependencies: - sourcemap-codec: 1.4.8 - dev: false + is-plain-obj@4.1.0: {} - /magic-string@0.30.0: - resolution: {integrity: sha512-LA+31JYDJLs82r2ScLrlz1GjSgu66ZV518eyWT+S8VhyQn/JL0u9MeBOvQMGYiPk1DBiSN9DDMOcXvigJZaViQ==} - engines: {node: '>=12'} - dependencies: - '@jridgewell/sourcemap-codec': 1.4.15 - dev: false + is-stream@4.0.1: {} - /markdown-it-anchor@8.6.7(@types/markdown-it@12.2.3)(markdown-it@13.0.1): - resolution: {integrity: sha512-FlCHFwNnutLgVTflOYHPW2pPcl2AACqVzExlkGQNsi4CJgqOHN7YTgDd4LuhgN1BFO3TS0vLAruV1Td6dwWPJA==} - peerDependencies: - '@types/markdown-it': '*' - markdown-it: '*' - dependencies: - '@types/markdown-it': 12.2.3 - markdown-it: 13.0.1 - dev: false + is-unicode-supported@1.3.0: {} - /markdown-it-container@3.0.0: - resolution: {integrity: sha512-y6oKTq4BB9OQuY/KLfk/O3ysFhB3IMYoIWhGJEidXt1NQFocFK2sA2t0NYZAMyMShAGL6x5OPIbrmXPIqaN9rw==} - dev: false + is-unicode-supported@2.1.0: {} - /markdown-it-emoji@2.0.2: - resolution: {integrity: sha512-zLftSaNrKuYl0kR5zm4gxXjHaOI3FAOEaloKmRA5hijmJZvSjmxcokOLlzycb/HXlUFWzXqpIEoyEMCE4i9MvQ==} - dev: false + is-what@4.1.16: {} - /markdown-it@13.0.1: - resolution: {integrity: sha512-lTlxriVoy2criHP0JKRhO2VDG9c2ypWCsT237eDiLqi09rmbKoUetyGHq2uOIRoRS//kfoJckS0eUzzkDR+k2Q==} - hasBin: true - dependencies: - argparse: 2.0.1 - entities: 3.0.1 - linkify-it: 4.0.1 - mdurl: 1.0.1 - uc.micro: 1.0.6 - dev: false - - /markdownlint-cli@0.34.0: - resolution: {integrity: sha512-4G9I++VBTZkaye6Yfc/7dU6HQHcyldZEVB+bYyQJLcpJOHKk/q5ZpGqK80oKMIdlxzsA3aWOJLZ4DkoaoUWXbQ==} - engines: {node: '>=14'} - hasBin: true - dependencies: - commander: 10.0.1 - get-stdin: 9.0.0 - glob: 10.2.4 - ignore: 5.2.4 - js-yaml: 4.1.0 - jsonc-parser: 3.2.0 - markdownlint: 0.28.2 - minimatch: 9.0.0 - run-con: 1.2.11 - dev: false - - /markdownlint-micromark@0.1.2: - resolution: {integrity: sha512-jRxlQg8KpOfM2IbCL9RXM8ZiYWz2rv6DlZAnGv8ASJQpUh6byTBnEsbuMZ6T2/uIgntyf7SKg/mEaEBo1164fQ==} - engines: {node: '>=14.18.0'} - dev: false - - /markdownlint@0.28.2: - resolution: {integrity: sha512-yYaQXoKKPV1zgrFsyAuZPEQoe+JrY9GDag9ObKpk09twx4OCU5lut+0/kZPrQ3W7w82SmgKhd7D8m34aG1unVw==} - engines: {node: '>=14.18.0'} - dependencies: - markdown-it: 13.0.1 - markdownlint-micromark: 0.1.2 - dev: false - - /marked@4.3.0: - resolution: {integrity: sha512-PRsaiG84bK+AMvxziE/lCFss8juXjNaWzVbN5tXAm4XjeaS9NAHhop+PjQxz2A9h8Q4M/xGmzP8vqNwy6JeK0A==} - engines: {node: '>= 12'} - hasBin: true - dev: false + isexe@2.0.0: {} - /marked@5.0.2: - resolution: {integrity: sha512-TXksm9GwqXCRNbFUZmMtqNLvy3K2cQHuWmyBDLOrY1e6i9UvZpOTJXoz7fBjYkJkaUFzV9hBFxMuZSyQt8R6KQ==} - engines: {node: '>= 18'} - hasBin: true - dev: false + isexe@3.1.1: + optional: true - /mathjax-full@3.2.2: - resolution: {integrity: sha512-+LfG9Fik+OuI8SLwsiR02IVdjcnRCy5MufYLi0C3TdMT56L/pjB0alMVGgoWJF8pN9Rc7FESycZB9BMNWIid5w==} + jackspeak@3.4.3: dependencies: - esm: 3.2.25 - mhchemparser: 4.1.1 - mj-context-menu: 0.6.1 - speech-rule-engine: 4.0.7 - dev: false + '@isaacs/cliui': 8.0.2 + optionalDependencies: + '@pkgjs/parseargs': 0.11.0 + optional: true - /mdurl@1.0.1: - resolution: {integrity: sha512-/sKlQJCBYVY9Ers9hqzKou4H6V5UWc/M59TH2dvkt+84itfnq7uFOMLpOiOS4ujvHP4etln18fmIxA5R5fll0g==} - dev: false + js-yaml@3.14.1: + dependencies: + argparse: 1.0.10 + esprima: 4.0.1 - /medium-zoom@1.0.8: - resolution: {integrity: sha512-CjFVuFq/IfrdqesAXfg+hzlDKu6A2n80ZIq0Kl9kWjoHh9j1N9Uvk5X0/MmN0hOfm5F9YBswlClhcwnmtwz7gA==} - dev: false + js-yaml@4.1.0: + dependencies: + argparse: 2.0.1 - /merge-stream@2.0.0: - resolution: {integrity: sha512-abv/qOcuPfk3URPfDzmZU1LKmuw8kT+0nIHvKrKgFrwifol/doWcdA4ZqsWQ8ENrFKkd67Mfpo/LovbIUsbt3w==} - dev: false + jsbn@1.1.0: + optional: true - /merge2@1.4.1: - resolution: {integrity: sha512-8q7VEgMJW4J8tcfVPy8g09NcQwZdbwFEqhe/WZkoIzjn/3TGDwtOCYtXGxA3O8tPzpczCCDgv+P2P5y00ZJOOg==} - engines: {node: '>= 8'} - dev: false - - /mermaid@10.1.0(react-dom@16.14.0)(react@16.14.0): - resolution: {integrity: sha512-LYekSMNJygI1VnMizAPUddY95hZxOjwZxr7pODczILInO0dhQKuhXeu4sargtnuTwCilSuLS7Uiq/Qn7HTVrmA==} - dependencies: - '@braintree/sanitize-url': 6.0.2 - '@khanacademy/simple-markdown': 0.8.6(react-dom@16.14.0)(react@16.14.0) - cytoscape: 3.24.0 - cytoscape-cose-bilkent: 4.1.0(cytoscape@3.24.0) - cytoscape-fcose: 2.2.0(cytoscape@3.24.0) - d3: 7.8.4 - dagre-d3-es: 7.0.10 - dayjs: 1.11.7 - dompurify: 2.4.5 - elkjs: 0.8.2 - khroma: 2.0.0 - lodash-es: 4.17.21 - non-layered-tidy-tree-layout: 2.0.2 - stylis: 4.2.0 - ts-dedent: 2.2.0 - uuid: 9.0.0 - web-worker: 1.2.0 - transitivePeerDependencies: - - react - - react-dom - dev: false + jsonc-parser@3.3.1: {} - /mhchemparser@4.1.1: - resolution: {integrity: sha512-R75CUN6O6e1t8bgailrF1qPq+HhVeFTM3XQ0uzI+mXTybmphy3b6h4NbLOYhemViQ3lUs+6CKRkC3Ws1TlYREA==} - dev: false + jsonfile@6.1.0: + dependencies: + universalify: 2.0.1 + optionalDependencies: + graceful-fs: 4.2.11 - /micromatch@4.0.5: - resolution: {integrity: sha512-DMy+ERcEW2q8Z2Po+WNXuw3c5YaUSFjAO5GsJqfEl7UjvtIuFKO6ZrKvcItdy98dwFI2N1tg3zNIdKaQT+aNdA==} - engines: {node: '>=8.6'} + katex@0.16.20: dependencies: - braces: 3.0.2 - picomatch: 2.3.1 - dev: false + commander: 8.3.0 - /mimic-fn@2.1.0: - resolution: {integrity: sha512-OqbOk5oEQeAZ8WXWydlu9HJjz9WVdEIvamMCcXmuqUYjTknH/sqsWvhQ3vgwKFRR1HpjvNBKQ37nbJgYzGqGcg==} - engines: {node: '>=6'} - dev: false + kind-of@6.0.3: {} - /mimic-fn@4.0.0: - resolution: {integrity: sha512-vqiC06CuhBTUdZH+RYl8sFrL096vA45Ok5ISO6sE/Mr1jRbGH4Csnhi8f3wKVl7x8mO4Au7Ir9D3Oyv1VYMFJw==} - engines: {node: '>=12'} - dev: false + lilconfig@3.1.3: {} - /minimatch@3.1.2: - resolution: {integrity: sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==} + linkify-it@5.0.0: dependencies: - brace-expansion: 1.1.11 - dev: false + uc.micro: 2.1.0 - /minimatch@5.1.6: - resolution: {integrity: sha512-lKwV/1brpG6mBUFHtb7NUmtABCb2WZZmm2wNiOA5hAb8VdCS4B3dtMWyvcoViccwAW/COERjXLt0zP1zXUN26g==} - engines: {node: '>=10'} + lit-element@4.1.1: dependencies: - brace-expansion: 2.0.1 - dev: false + '@lit-labs/ssr-dom-shim': 1.3.0 + '@lit/reactive-element': 2.0.4 + lit-html: 3.2.1 - /minimatch@9.0.0: - resolution: {integrity: sha512-0jJj8AvgKqWN05mrwuqi8QYKx1WmYSUoKSxu5Qhs9prezTz10sxAHGNZe9J9cqIJzta8DWsleh2KaVaLl6Ru2w==} - engines: {node: '>=16 || 14 >=14.17'} + lit-html@3.2.1: dependencies: - brace-expansion: 2.0.1 - dev: false - - /minimist@1.2.8: - resolution: {integrity: sha512-2yyAR8qBkN3YuheJanUpWC5U3bb5osDywNB8RzDVlDwDHbocAJveqqj1u8+SVD7jkWT4yvsHCpWqqWqAxb0zCA==} - dev: false - - /minipass@6.0.1: - resolution: {integrity: sha512-Tenl5QPpgozlOGBiveNYHg2f6y+VpxsXRoIHFUVJuSmTonXRAE6q9b8Mp/O46762/2AlW4ye4Nkyvx0fgWDKbw==} - engines: {node: '>=16 || 14 >=14.17'} - dev: false + '@types/trusted-types': 2.0.7 - /mitt@3.0.0: - resolution: {integrity: sha512-7dX2/10ITVyqh4aOSVI9gdape+t9l2/8QxHrFmUXu4EEUpdlxl6RudZUPZoc+zuY2hk1j7XxVroIVIan/pD/SQ==} - dev: false + lit@3.2.1: + dependencies: + '@lit/reactive-element': 2.0.4 + lit-element: 4.1.1 + lit-html: 3.2.1 - /mj-context-menu@0.6.1: - resolution: {integrity: sha512-7NO5s6n10TIV96d4g2uDpG7ZDpIhMh0QNfGdJw/W47JswFcosz457wqz/b5sAKvl12sxINGFCn80NZHKwxQEXA==} - dev: false + locate-path@5.0.0: + dependencies: + p-locate: 4.1.0 - /mpegts.js@1.7.3: - resolution: {integrity: sha512-kqZ1C1IsbAQN72cK8vMrzKeM7hwrwSBbFAwVAc7PPweOeoZxCANrc7fAVDKMfYUzxdNkMTnec9tVmlxmKZB0TQ==} + log-symbols@6.0.0: dependencies: - es6-promise: 4.2.8 - webworkify-webpack: 2.1.5 - dev: false + chalk: 5.4.1 + is-unicode-supported: 1.3.0 - /ms@2.1.2: - resolution: {integrity: sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==} - dev: false + lru-cache@10.4.3: + optional: true - /nano-staged@0.8.0: - resolution: {integrity: sha512-QSEqPGTCJbkHU2yLvfY6huqYPjdBrOaTMKatO1F8nCSrkQGXeKwtCiCnsdxnuMhbg3DTVywKaeWLGCE5oJpq0g==} - engines: {node: ^12.20.0 || ^14.13.1 || >=16.0.0} - hasBin: true + magic-string@0.30.17: dependencies: - picocolors: 1.0.0 - dev: false + '@jridgewell/sourcemap-codec': 1.5.0 - /nanoid@3.3.6: - resolution: {integrity: sha512-BGcqMMJuToF7i1rt+2PWSNVnWIkGCU78jBG3RxO/bZlnZPK2Cmi2QaffxGO/2RvWi9sL+FAiRiXMgsyxQ1DIDA==} - engines: {node: ^10 || ^12 || ^13.7 || ^14 || >=15.0.1} - hasBin: true - dev: false + make-dir@3.1.0: + dependencies: + semver: 6.3.1 + optional: true - /node-releases@2.0.10: - resolution: {integrity: sha512-5GFldHPXVG/YZmFzJvKK2zDSzPKhEp0+ZR5SVaoSag9fsL5YgHbUHDfnG5494ISANDcK4KwPXAx2xqVEydmd7w==} - dev: false + make-fetch-happen@13.0.1: + dependencies: + '@npmcli/agent': 2.2.2 + cacache: 18.0.4 + http-cache-semantics: 4.1.1 + is-lambda: 1.0.1 + minipass: 7.1.2 + minipass-fetch: 3.0.5 + minipass-flush: 1.0.5 + minipass-pipeline: 1.2.4 + negotiator: 0.6.4 + proc-log: 4.2.0 + promise-retry: 2.0.1 + ssri: 10.0.6 + transitivePeerDependencies: + - supports-color + optional: true - /non-layered-tidy-tree-layout@2.0.2: - resolution: {integrity: sha512-gkXMxRzUH+PB0ax9dUN0yYF0S25BqeAYqhgMaLUFmpXLEk7Fcu8f4emJuOAY0V8kjDICxROIKsTAKsV/v355xw==} - dev: false + markdown-it-anchor@9.2.0(@types/markdown-it@14.1.2)(markdown-it@14.1.0): + dependencies: + '@types/markdown-it': 14.1.2 + markdown-it: 14.1.0 - /normalize-path@3.0.0: - resolution: {integrity: sha512-6eZs5Ls3WtCisHWp9S2GUy8dqkpGi4BVSz3GaqiE6ezub0512ESztXUwUB6C6IKbQkY2Pnb/mD4WYojCRwcwLA==} - engines: {node: '>=0.10.0'} - dev: false + markdown-it-emoji@3.0.0: {} - /normalize-range@0.1.2: - resolution: {integrity: sha512-bdok/XvKII3nUpklnV6P2hxtMNrCboOjAcyBuQnWEhO665FwrSNRxU+AqpsyvO6LgGYPspN+lu5CLtw4jPRKNA==} - engines: {node: '>=0.10.0'} - dev: false + markdown-it@14.1.0: + dependencies: + argparse: 2.0.1 + entities: 4.5.0 + linkify-it: 5.0.0 + mdurl: 2.0.0 + punycode.js: 2.3.1 + uc.micro: 2.1.0 - /npm-run-path@5.1.0: - resolution: {integrity: sha512-sJOdmRGrY2sjNTRMbSvluQqg+8X7ZK61yvzBEIDhz4f8z1TZFYABsqjjCBd/0PUNE9M6QDgHJXQkGUEm7Q+l9Q==} - engines: {node: ^12.20.0 || ^14.13.1 || >=16.0.0} + markdownlint-cli2-formatter-default@0.0.5(markdownlint-cli2@0.17.1): dependencies: - path-key: 4.0.0 - dev: false + markdownlint-cli2: 0.17.1 - /nth-check@2.1.1: - resolution: {integrity: sha512-lqjrjmaOoAnWfMmBPL+XNnynZh2+swxiX3WUE0s4yEHI6m+AwrK2UZOimIRl3X/4QctVqS8AiZjFqyOGrMXb/w==} + markdownlint-cli2@0.17.1: dependencies: - boolbase: 1.0.0 - dev: false + globby: 14.0.2 + js-yaml: 4.1.0 + jsonc-parser: 3.3.1 + markdownlint: 0.37.3 + markdownlint-cli2-formatter-default: 0.0.5(markdownlint-cli2@0.17.1) + micromatch: 4.0.8 + transitivePeerDependencies: + - supports-color - /object-assign@4.1.1: - resolution: {integrity: sha512-rJgTQnkUnH1sFw8yT6VSU3zD3sWmu6sZhIseY8VX+GRu3P6F7Fu+JNDoXfklElbLJSnc3FUQHVe4cU5hj+BcUg==} - engines: {node: '>=0.10.0'} - dev: false + markdownlint@0.37.3: + dependencies: + markdown-it: 14.1.0 + micromark: 4.0.1 + micromark-core-commonmark: 2.0.2 + micromark-extension-directive: 3.0.2 + micromark-extension-gfm-autolink-literal: 2.1.0 + micromark-extension-gfm-footnote: 2.1.0 + micromark-extension-gfm-table: 2.1.0 + micromark-extension-math: 3.1.0 + micromark-util-types: 2.0.1 + transitivePeerDependencies: + - supports-color - /object-inspect@1.12.3: - resolution: {integrity: sha512-geUvdk7c+eizMNUDkRpW1wJwgfOiOeHbxBR/hLXK1aT6zmVSO0jsQcs7fj6MGw89jC/cjGfLcNOrtMYtGqm81g==} - dev: false + mathjax-full@3.2.2: + dependencies: + esm: 3.2.25 + mhchemparser: 4.2.1 + mj-context-menu: 0.6.1 + speech-rule-engine: 4.0.7 - /object-keys@1.1.1: - resolution: {integrity: sha512-NuAESUOUMrlIXOfHKzD6bpPu3tYt3xvjNdRIQ+FeT0lNb4K8WR70CaDxhuNguS2XG+GjkyMwOzsN5ZktImfhLA==} - engines: {node: '>= 0.4'} - dev: false + mdast-util-to-hast@13.2.0: + dependencies: + '@types/hast': 3.0.4 + '@types/mdast': 4.0.4 + '@ungap/structured-clone': 1.2.1 + devlop: 1.1.0 + micromark-util-sanitize-uri: 2.0.1 + trim-lines: 3.0.1 + unist-util-position: 5.0.0 + unist-util-visit: 5.0.0 + vfile: 6.0.3 + + mdurl@2.0.0: {} + + merge2@1.4.1: {} + + mhchemparser@4.2.1: {} - /object.assign@4.1.4: - resolution: {integrity: sha512-1mxKf0e58bvyjSCtKYY4sRe9itRk3PJpquJOjeIkz885CczcI4IvJJDLPS72oowuSh+pBxUFROpX+TU++hxhZQ==} - engines: {node: '>= 0.4'} + micromark-core-commonmark@2.0.2: dependencies: - call-bind: 1.0.2 - define-properties: 1.2.0 - has-symbols: 1.0.3 - object-keys: 1.1.1 - dev: false + decode-named-character-reference: 1.0.2 + devlop: 1.1.0 + micromark-factory-destination: 2.0.1 + micromark-factory-label: 2.0.1 + micromark-factory-space: 2.0.1 + micromark-factory-title: 2.0.1 + micromark-factory-whitespace: 2.0.1 + micromark-util-character: 2.1.1 + micromark-util-chunked: 2.0.1 + micromark-util-classify-character: 2.0.1 + micromark-util-html-tag-name: 2.0.1 + micromark-util-normalize-identifier: 2.0.1 + micromark-util-resolve-all: 2.0.1 + micromark-util-subtokenize: 2.0.3 + micromark-util-symbol: 2.0.1 + micromark-util-types: 2.0.1 - /once@1.4.0: - resolution: {integrity: sha512-lNaJgI+2Q5URQBkccEKHTQOPaXdUxnZZElQTZY0MFUAuaEqe1E+Nyvgdz/aIyNi6Z9MzO5dv1H8n58/GELp3+w==} + micromark-extension-directive@3.0.2: dependencies: - wrappy: 1.0.2 - dev: false + devlop: 1.1.0 + micromark-factory-space: 2.0.1 + micromark-factory-whitespace: 2.0.1 + micromark-util-character: 2.1.1 + micromark-util-symbol: 2.0.1 + micromark-util-types: 2.0.1 + parse-entities: 4.0.2 - /onetime@5.1.2: - resolution: {integrity: sha512-kbpaSSGJTWdAY5KPVeMOKXSrPtr8C8C7wodJbcsd51jRnmD+GZu8Y0VoU6Dm5Z4vWr0Ig/1NKuWRKf7j5aaYSg==} - engines: {node: '>=6'} + micromark-extension-gfm-autolink-literal@2.1.0: dependencies: - mimic-fn: 2.1.0 - dev: false + micromark-util-character: 2.1.1 + micromark-util-sanitize-uri: 2.0.1 + micromark-util-symbol: 2.0.1 + micromark-util-types: 2.0.1 - /onetime@6.0.0: - resolution: {integrity: sha512-1FlR+gjXK7X+AsAHso35MnyN5KqGwJRi/31ft6x0M194ht7S+rWAvd7PHss9xSKMzE0asv1pyIHaJYq+BbacAQ==} - engines: {node: '>=12'} + micromark-extension-gfm-footnote@2.1.0: dependencies: - mimic-fn: 4.0.0 - dev: false + devlop: 1.1.0 + micromark-core-commonmark: 2.0.2 + micromark-factory-space: 2.0.1 + micromark-util-character: 2.1.1 + micromark-util-normalize-identifier: 2.0.1 + micromark-util-sanitize-uri: 2.0.1 + micromark-util-symbol: 2.0.1 + micromark-util-types: 2.0.1 - /option-validator@2.0.6: - resolution: {integrity: sha512-tmZDan2LRIRQyhUGvkff68/O0R8UmF+Btmiiz0SmSw2ng3CfPZB9wJlIjHpe/MKUZqyIZkVIXCrwr1tIN+0Dzg==} + micromark-extension-gfm-table@2.1.0: dependencies: - kind-of: 6.0.3 - dev: false + devlop: 1.1.0 + micromark-factory-space: 2.0.1 + micromark-util-character: 2.1.1 + micromark-util-symbol: 2.0.1 + micromark-util-types: 2.0.1 - /ora@6.3.1: - resolution: {integrity: sha512-ERAyNnZOfqM+Ao3RAvIXkYh5joP220yf59gVe2X/cI6SiCxIdi4c9HZKZD8R6q/RDXEje1THBju6iExiSsgJaQ==} - engines: {node: ^12.20.0 || ^14.13.1 || >=16.0.0} + micromark-extension-math@3.1.0: dependencies: - chalk: 5.2.0 - cli-cursor: 4.0.0 - cli-spinners: 2.9.0 - is-interactive: 2.0.0 - is-unicode-supported: 1.3.0 - log-symbols: 5.1.0 - stdin-discarder: 0.1.0 - strip-ansi: 7.0.1 - wcwidth: 1.0.1 - dev: false + '@types/katex': 0.16.7 + devlop: 1.1.0 + katex: 0.16.20 + micromark-factory-space: 2.0.1 + micromark-util-character: 2.1.1 + micromark-util-symbol: 2.0.1 + micromark-util-types: 2.0.1 - /p-limit@2.3.0: - resolution: {integrity: sha512-//88mFWSJx8lxCzwdAABTJL2MyWB12+eIY7MDL2SqLmAkeKU9qxRvWuSyTjm3FUmpBEMuFfckAIqEaVGUDxb6w==} - engines: {node: '>=6'} + micromark-factory-destination@2.0.1: dependencies: - p-try: 2.2.0 - dev: false + micromark-util-character: 2.1.1 + micromark-util-symbol: 2.0.1 + micromark-util-types: 2.0.1 - /p-locate@4.1.0: - resolution: {integrity: sha512-R79ZZ/0wAxKGu3oYMlz8jy/kbhsNrS7SKZ7PxEHBgJ5+F2mtFW2fK2cOtBh1cHYkQsbzFV7I+EoRKe6Yt0oK7A==} - engines: {node: '>=8'} + micromark-factory-label@2.0.1: dependencies: - p-limit: 2.3.0 - dev: false - - /p-try@2.2.0: - resolution: {integrity: sha512-R4nPAVTAU0B9D35/Gk3uJf/7XYbQcyohSKdvAxIRSNghFl4e71hVoGnBNQz9cWaXxO2I10KTC+3jMdvvoKw6dQ==} - engines: {node: '>=6'} - dev: false + devlop: 1.1.0 + micromark-util-character: 2.1.1 + micromark-util-symbol: 2.0.1 + micromark-util-types: 2.0.1 - /parse5-htmlparser2-tree-adapter@7.0.0: - resolution: {integrity: sha512-B77tOZrqqfUfnVcOrUvfdLbz4pu4RopLD/4vmu3HUPswwTA8OH0EMW9BlWR2B0RCoiZRAHEUu7IxeP1Pd1UU+g==} + micromark-factory-space@2.0.1: dependencies: - domhandler: 5.0.3 - parse5: 7.1.2 - dev: false + micromark-util-character: 2.1.1 + micromark-util-types: 2.0.1 - /parse5@7.1.2: - resolution: {integrity: sha512-Czj1WaSVpaoj0wbhMzLmWD69anp2WH7FXMB9n1Sy8/ZFF9jolSQVMu1Ij5WIyGmcBmhk7EOndpO4mIpihVqAXw==} + micromark-factory-title@2.0.1: dependencies: - entities: 4.5.0 - dev: false - - /path-browserify@1.0.1: - resolution: {integrity: sha512-b7uo2UCUOYZcnF/3ID0lulOJi/bafxa1xPe7ZPsammBSpjSWQkjNxlt635YGS2MiR9GjvuXCtz2emr3jbsz98g==} - dev: false - - /path-exists@4.0.0: - resolution: {integrity: sha512-ak9Qy5Q7jYb2Wwcey5Fpvg2KoAc/ZIhLSLOSBmRmygPsGwkVVt0fZa0qrtMz+m6tJTAHfZQ8FnmB4MG4LWy7/w==} - engines: {node: '>=8'} - dev: false - - /path-is-absolute@1.0.1: - resolution: {integrity: sha512-AVbw3UJ2e9bq64vSaS9Am0fje1Pa8pbGqTTsmXfaIiMpnr5DlDhfJOuLj9Sf95ZPVDAUerDfEk88MPmPe7UCQg==} - engines: {node: '>=0.10.0'} - dev: false + micromark-factory-space: 2.0.1 + micromark-util-character: 2.1.1 + micromark-util-symbol: 2.0.1 + micromark-util-types: 2.0.1 - /path-key@3.1.1: - resolution: {integrity: sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q==} - engines: {node: '>=8'} - dev: false + micromark-factory-whitespace@2.0.1: + dependencies: + micromark-factory-space: 2.0.1 + micromark-util-character: 2.1.1 + micromark-util-symbol: 2.0.1 + micromark-util-types: 2.0.1 - /path-key@4.0.0: - resolution: {integrity: sha512-haREypq7xkM7ErfgIyA0z+Bj4AGKlMSdlQE2jvJo6huWD1EdkKYV+G/T4nq0YEF2vgTT8kqMFKo1uHn950r4SQ==} - engines: {node: '>=12'} - dev: false + micromark-util-character@2.1.1: + dependencies: + micromark-util-symbol: 2.0.1 + micromark-util-types: 2.0.1 - /path-parse@1.0.7: - resolution: {integrity: sha512-LDJzPVEEEPR+y48z93A0Ed0yXb8pAByGWo/k5YYdYgpY2/2EsOsksJrq7lOHxryrVOn1ejG6oAp8ahvOIQD8sw==} - dev: false + micromark-util-chunked@2.0.1: + dependencies: + micromark-util-symbol: 2.0.1 - /path-scurry@1.9.1: - resolution: {integrity: sha512-UgmoiySyjFxP6tscZDgWGEAgsW5ok8W3F5CJDnnH2pozwSTGE6eH7vwTotMwATWA2r5xqdkKdxYPkwlJjAI/3g==} - engines: {node: '>=16 || 14 >=14.17'} + micromark-util-classify-character@2.0.1: dependencies: - lru-cache: 9.1.1 - minipass: 6.0.1 - dev: false + micromark-util-character: 2.1.1 + micromark-util-symbol: 2.0.1 + micromark-util-types: 2.0.1 - /path-type@4.0.0: - resolution: {integrity: sha512-gDKb8aZMDeD/tZWs9P6+q0J9Mwkdl6xMV8TjnGP3qJVJ06bdMgkbBlLU8IdfOsIsFz2BW1rNVT3XuNEl8zPAvw==} - engines: {node: '>=8'} - dev: false + micromark-util-combine-extensions@2.0.1: + dependencies: + micromark-util-chunked: 2.0.1 + micromark-util-types: 2.0.1 - /photoswipe@5.3.7: - resolution: {integrity: sha512-zsyLsTTLFrj0XR1m4/hO7qNooboFKUrDy+Zt5i2d6qjFPAtBjzaj/Xtydso4uxzcXpcqbTmyxDibb3BcSISseg==} - engines: {node: '>= 0.12.0'} - dev: false + micromark-util-decode-numeric-character-reference@2.0.2: + dependencies: + micromark-util-symbol: 2.0.1 - /picocolors@1.0.0: - resolution: {integrity: sha512-1fygroTLlHu66zi26VoTDv8yRgm0Fccecssto+MhsZ0D/DGW2sm8E8AjW7NU5VVTRt5GxbeZ5qBuJr+HyLYkjQ==} - dev: false + micromark-util-encode@2.0.1: {} - /picomatch@2.3.1: - resolution: {integrity: sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA==} - engines: {node: '>=8.6'} - dev: false + micromark-util-html-tag-name@2.0.1: {} - /plyr@3.7.8: - resolution: {integrity: sha512-yG/EHDobwbB/uP+4Bm6eUpJ93f8xxHjjk2dYcD1Oqpe1EcuQl5tzzw9Oq+uVAzd2lkM11qZfydSiyIpiB8pgdA==} + micromark-util-normalize-identifier@2.0.1: dependencies: - core-js: 3.30.2 - custom-event-polyfill: 1.0.7 - loadjs: 4.2.0 - rangetouch: 2.0.1 - url-polyfill: 1.1.12 - dev: false + micromark-util-symbol: 2.0.1 - /pngjs@5.0.0: - resolution: {integrity: sha512-40QW5YalBNfQo5yRYmiw7Yz6TKKVr3h6970B2YE+3fQpsWcrbj1PzJgxeJ19DRQjhMbKPIuMY8rFaXc8moolVw==} - engines: {node: '>=10.13.0'} - dev: false - - /postcss-load-config@4.0.1(postcss@8.4.23): - resolution: {integrity: sha512-vEJIc8RdiBRu3oRAI0ymerOn+7rPuMvRXslTvZUKZonDHFIczxztIyJ1urxM1x9JXEikvpWWTUUqal5j/8QgvA==} - engines: {node: '>= 14'} - peerDependencies: - postcss: '>=8.0.9' - ts-node: '>=9.0.0' - peerDependenciesMeta: - postcss: - optional: true - ts-node: - optional: true + micromark-util-resolve-all@2.0.1: dependencies: - lilconfig: 2.1.0 - postcss: 8.4.23 - yaml: 2.2.2 - dev: false - - /postcss-value-parser@4.2.0: - resolution: {integrity: sha512-1NNCs6uurfkVbeXG4S8JFT9t19m45ICnif8zWLd5oPSZ50QnwMfK+H3jv408d4jw/7Bttv5axS5IiHoLaVNHeQ==} - dev: false + micromark-util-types: 2.0.1 - /postcss@8.4.23: - resolution: {integrity: sha512-bQ3qMcpF6A/YjR55xtoTr0jGOlnPOKAIMdOWiv0EIT6HVPEaJiJB4NLljSbiHoC2RX7DN5Uvjtpbg1NPdwv1oA==} - engines: {node: ^10 || ^12 || >=14} + micromark-util-sanitize-uri@2.0.1: dependencies: - nanoid: 3.3.6 - picocolors: 1.0.0 - source-map-js: 1.0.2 - dev: false + micromark-util-character: 2.1.1 + micromark-util-encode: 2.0.1 + micromark-util-symbol: 2.0.1 - /prettier@2.8.8: - resolution: {integrity: sha512-tdN8qQGvNjw4CHbY+XXk0JgCXn9QiF21a55rBe5LJAU+kDyC4WQn4+awm2Xfk2lQMk5fKup9XgzTZtGkjBdP9Q==} - engines: {node: '>=10.13.0'} - hasBin: true - dev: false + micromark-util-subtokenize@2.0.3: + dependencies: + devlop: 1.1.0 + micromark-util-chunked: 2.0.1 + micromark-util-symbol: 2.0.1 + micromark-util-types: 2.0.1 - /pretty-bytes@5.6.0: - resolution: {integrity: sha512-FFw039TmrBqFK8ma/7OL3sDz/VytdtJr044/QUJtH0wK9lb9jLq9tJyIxUwtQJHwar2BqtiA4iCWSwo9JLkzFg==} - engines: {node: '>=6'} - dev: false + micromark-util-symbol@2.0.1: {} - /prismjs@1.29.0: - resolution: {integrity: sha512-Kx/1w86q/epKcmte75LNrEoT+lX8pBpavuAbvJWRXar7Hz8jrtF+e3vY751p0R8H9HdArwaCTNDDzHg/ScJK1Q==} - engines: {node: '>=6'} - dev: false + micromark-util-types@2.0.1: {} - /prop-types@15.8.1: - resolution: {integrity: sha512-oj87CgZICdulUohogVAR7AjlC0327U4el4L6eAvOqCeudMDVU0NThNaV+b9Df4dXgSP1gXMTnPdhfe/2qDH5cg==} + micromark@4.0.1: dependencies: - loose-envify: 1.4.0 - object-assign: 4.1.1 - react-is: 16.13.1 - dev: false - - /punycode@2.3.0: - resolution: {integrity: sha512-rRV+zQD8tVFys26lAGR9WUuS4iUAngJScM+ZRSKtvl5tKeZ2t5bvdNFdNHBW9FWR4guGHlgmsZ1G7BSm2wTbuA==} - engines: {node: '>=6'} - dev: false + '@types/debug': 4.1.12 + debug: 4.4.0 + decode-named-character-reference: 1.0.2 + devlop: 1.1.0 + micromark-core-commonmark: 2.0.2 + micromark-factory-space: 2.0.1 + micromark-util-character: 2.1.1 + micromark-util-chunked: 2.0.1 + micromark-util-combine-extensions: 2.0.1 + micromark-util-decode-numeric-character-reference: 2.0.2 + micromark-util-encode: 2.0.1 + micromark-util-normalize-identifier: 2.0.1 + micromark-util-resolve-all: 2.0.1 + micromark-util-sanitize-uri: 2.0.1 + micromark-util-subtokenize: 2.0.3 + micromark-util-symbol: 2.0.1 + micromark-util-types: 2.0.1 + transitivePeerDependencies: + - supports-color - /qrcode@1.5.3: - resolution: {integrity: sha512-puyri6ApkEHYiVl4CFzo1tDkAZ+ATcnbJrJ6RiBM1Fhctdn/ix9MTE3hRph33omisEbC/2fcfemsseiKgBPKZg==} - engines: {node: '>=10.13.0'} - hasBin: true + micromatch@4.0.8: dependencies: - dijkstrajs: 1.0.3 - encode-utf8: 1.0.3 - pngjs: 5.0.0 - yargs: 15.4.1 - dev: false + braces: 3.0.3 + picomatch: 2.3.1 - /queue-microtask@1.2.3: - resolution: {integrity: sha512-NuaNSa6flKT5JaSYQzJok04JzTL1CA6aGhv5rfLW3PgqA+M2ChpZQnAC8h8i4ZFkBS8X5RqkDBHA7r4hej3K9A==} - dev: false + mimic-function@5.0.1: {} - /randombytes@2.1.0: - resolution: {integrity: sha512-vYl3iOX+4CKUWuxGi9Ukhie6fsqXqS9FE2Zaic4tNFD2N2QQaXOMFbuKK4QmDHC0JO6B1Zp41J0LpT0oR68amQ==} + minimatch@3.1.2: dependencies: - safe-buffer: 5.2.1 - dev: false - - /rangetouch@2.0.1: - resolution: {integrity: sha512-sln+pNSc8NGaHoLzwNBssFSf/rSYkqeBXzX1AtJlkJiUaVSJSbRAWJk+4omsXkN+EJalzkZhWQ3th1m0FpR5xA==} - dev: false + brace-expansion: 1.1.11 + optional: true - /raphael@2.3.0: - resolution: {integrity: sha512-w2yIenZAQnp257XUWGni4bLMVxpUpcIl7qgxEgDIXtmSypYtlNxfXWpOBxs7LBTps5sDwhRnrToJrMUrivqNTQ==} + minimatch@9.0.5: dependencies: - eve-raphael: 0.5.0 - dev: false + brace-expansion: 2.0.1 + optional: true - /react-dom@16.14.0(react@16.14.0): - resolution: {integrity: sha512-1gCeQXDLoIqMgqD3IO2Ah9bnf0w9kzhwN5q4FGnHZ67hBm9yePzB5JJAIQCc8x3pFnNlwFq4RidZggNAAkzWWw==} - peerDependencies: - react: ^16.14.0 + minipass-collect@2.0.1: dependencies: - loose-envify: 1.4.0 - object-assign: 4.1.1 - prop-types: 15.8.1 - react: 16.14.0 - scheduler: 0.19.1 - dev: false + minipass: 7.1.2 + optional: true - /react-is@16.13.1: - resolution: {integrity: sha512-24e6ynE2H+OKt4kqsOvNd8kBpV65zoxbA4BVsEOB3ARVWQki/DHzaUoC5KuON/BiccDaCCTZBuOcfZs70kR8bQ==} - dev: false + minipass-fetch@3.0.5: + dependencies: + minipass: 7.1.2 + minipass-sized: 1.0.3 + minizlib: 2.1.2 + optionalDependencies: + encoding: 0.1.13 + optional: true - /react@16.14.0: - resolution: {integrity: sha512-0X2CImDkJGApiAlcf0ODKIneSwBPhqJawOa5wCtKbu7ZECrmS26NvtSILynQ66cgkT/RJ4LidJOc3bUESwmU8g==} - engines: {node: '>=0.10.0'} + minipass-flush@1.0.5: dependencies: - loose-envify: 1.4.0 - object-assign: 4.1.1 - prop-types: 15.8.1 - dev: false + minipass: 3.3.6 + optional: true - /readable-stream@3.6.2: - resolution: {integrity: sha512-9u/sniCrY3D5WdsERHzHE4G2YCXqoG5FTHUiCC4SIbr6XcLZBY05ya9EKjYek9O5xOAwjGq+1JdGBAS7Q9ScoA==} - engines: {node: '>= 6'} + minipass-pipeline@1.2.4: dependencies: - inherits: 2.0.4 - string_decoder: 1.3.0 - util-deprecate: 1.0.2 - dev: false + minipass: 3.3.6 + optional: true - /readdirp@3.6.0: - resolution: {integrity: sha512-hOS089on8RduqdbhvQ5Z37A0ESjsqz6qnRcffsMU3495FuTdqSm+7bhJ29JvIOsBDEEnan5DPu9t3To9VRlMzA==} - engines: {node: '>=8.10.0'} + minipass-sized@1.0.3: dependencies: - picomatch: 2.3.1 - dev: false + minipass: 3.3.6 + optional: true - /regenerate-unicode-properties@10.1.0: - resolution: {integrity: sha512-d1VudCLoIGitcU/hEg2QqvyGZQmdC0Lf8BqdOMXGFSvJP4bNV1+XqbPQeHHLD51Jh4QJJ225dlIFvY4Ly6MXmQ==} - engines: {node: '>=4'} + minipass@3.3.6: dependencies: - regenerate: 1.4.2 - dev: false + yallist: 4.0.0 + optional: true - /regenerate@1.4.2: - resolution: {integrity: sha512-zrceR/XhGYU/d/opr2EKO7aRHUeiBI8qjtfHqADTwZd6Szfy16la6kqD0MIUs5z5hx6AaKa+PixpPrR289+I0A==} - dev: false + minipass@5.0.0: + optional: true - /regenerator-runtime@0.13.11: - resolution: {integrity: sha512-kY1AZVr2Ra+t+piVaJ4gxaFaReZVH40AKNo7UCX6W+dEwBo/2oZJzqfuN1qLq1oL45o56cPaTXELwrTh8Fpggg==} - dev: false + minipass@7.1.2: + optional: true - /regenerator-transform@0.15.1: - resolution: {integrity: sha512-knzmNAcuyxV+gQCufkYcvOqX/qIIfHLv0u5x79kRxuGojfYVky1f15TzZEu2Avte8QGepvUNTnLskf8E6X6Vyg==} + minizlib@2.1.2: dependencies: - '@babel/runtime': 7.21.5 - dev: false + minipass: 3.3.6 + yallist: 4.0.0 + optional: true - /regexp.prototype.flags@1.5.0: - resolution: {integrity: sha512-0SutC3pNudRKgquxGoRGIz946MZVHqbNfPjBdxeOhBrdgDKlRoXmYLQN9xRbrR09ZXWeGAdPuif7egofn6v5LA==} - engines: {node: '>= 0.4'} - dependencies: - call-bind: 1.0.2 - define-properties: 1.2.0 - functions-have-names: 1.2.3 - dev: false + mitt@3.0.1: {} - /regexpu-core@5.3.2: - resolution: {integrity: sha512-RAM5FlZz+Lhmo7db9L298p2vHP5ZywrVXmVXpmAD9GuL5MPH6t9ROw1iA/wfHkQ76Qe7AaPF0nGuim96/IrQMQ==} - engines: {node: '>=4'} - dependencies: - '@babel/regjsgen': 0.8.0 - regenerate: 1.4.2 - regenerate-unicode-properties: 10.1.0 - regjsparser: 0.9.1 - unicode-match-property-ecmascript: 2.0.0 - unicode-match-property-value-ecmascript: 2.1.0 - dev: false + mj-context-menu@0.6.1: {} + + mkdirp@1.0.4: + optional: true - /register-service-worker@1.7.2: - resolution: {integrity: sha512-CiD3ZSanZqcMPRhtfct5K9f7i3OLCcBBWsJjLh1gW9RO/nS94sVzY59iS+fgYBOBqaBpf4EzfqUF3j9IG+xo8A==} - dev: false + ms@2.1.3: {} - /regjsparser@0.9.1: - resolution: {integrity: sha512-dQUtn90WanSNl+7mQKcXAgZxvUe7Z0SqXlgzv0za4LwiUhyzBC58yQO3liFoUgu8GiJVInAhJjkj1N0EtQ5nkQ==} - hasBin: true + nano-staged@0.8.0: dependencies: - jsesc: 0.5.0 - dev: false + picocolors: 1.1.1 - /require-directory@2.1.1: - resolution: {integrity: sha512-fGxEI7+wsG9xrvdjsrlmL22OMTTiHRwAMroiEeMgq8gzoLC/PQr7RsRDSTLUg/bZAZtF+TVIkHc6/4RIKrui+Q==} - engines: {node: '>=0.10.0'} - dev: false + nanoid@3.3.8: {} - /require-from-string@2.0.2: - resolution: {integrity: sha512-Xf0nWe6RseziFMu+Ap9biiUbmplq6S9/p+7w7YXP/JBHhrUDDUhwa+vANyubuqfZWTveU//DYVGsDG7RKL/vEw==} - engines: {node: '>=0.10.0'} - dev: false + nanoid@5.0.9: {} - /require-main-filename@2.0.0: - resolution: {integrity: sha512-NKN5kMDylKuldxYLSUfrbo5Tuzh4hd+2E8NPPX02mZtn1VuREQToYe/ZdlJy+J3uCpfaiGF05e7B8W0iXbQHmg==} - dev: false + negotiator@0.6.4: + optional: true - /resolve@1.22.2: - resolution: {integrity: sha512-Sb+mjNHOULsBv818T40qSPeRiuWLyaGMa5ewydRLFimneixmVy2zdivRl+AF6jaYPC8ERxGDmFSiqui6SfPd+g==} - hasBin: true + node-addon-api@8.3.0: + optional: true + + node-fetch@2.7.0(encoding@0.1.13): dependencies: - is-core-module: 2.12.0 - path-parse: 1.0.7 - supports-preserve-symlinks-flag: 1.0.0 - dev: false + whatwg-url: 5.0.0 + optionalDependencies: + encoding: 0.1.13 + optional: true - /restore-cursor@4.0.0: - resolution: {integrity: sha512-I9fPXU9geO9bHOt9pHHOhOkYerIMsmVaWB0rA2AI9ERh/+x/i7MV5HKBNrg+ljO5eoPVgCcnFuRjJ9uH6I/3eg==} - engines: {node: ^12.20.0 || ^14.13.1 || >=16.0.0} + node-gyp@10.3.1: dependencies: - onetime: 5.1.2 - signal-exit: 3.0.7 - dev: false + env-paths: 2.2.1 + exponential-backoff: 3.1.1 + glob: 10.4.5 + graceful-fs: 4.2.11 + make-fetch-happen: 13.0.1 + nopt: 7.2.1 + proc-log: 4.2.0 + semver: 7.6.3 + tar: 6.2.1 + which: 4.0.0 + transitivePeerDependencies: + - supports-color + optional: true - /reusify@1.0.4: - resolution: {integrity: sha512-U9nH88a3fc/ekCF1l0/UP1IosiuIjyTh7hBvXVMHYgVcfGvt897Xguj2UOLDeI5BG2m7/uwyaLVT6fbtCwTyzw==} - engines: {iojs: '>=1.0.0', node: '>=0.10.0'} - dev: false + node-releases@2.0.19: {} - /reveal.js@4.5.0: - resolution: {integrity: sha512-Lx1hUWhJR7Y7ScQNyGt7TFzxeviDAswK2B0cn9RwbPZogTMRgS8+FTr+/12KNHOegjvWKH0H0EGwBARNDPTgWQ==} - engines: {node: '>=10.0.0'} - dev: false + nodejs-jieba@0.2.1(encoding@0.1.13): + dependencies: + '@mapbox/node-pre-gyp': 1.0.11(encoding@0.1.13) + node-addon-api: 8.3.0 + node-gyp: 10.3.1 + transitivePeerDependencies: + - encoding + - supports-color + optional: true - /robust-predicates@3.0.1: - resolution: {integrity: sha512-ndEIpszUHiG4HtDsQLeIuMvRsDnn8c8rYStabochtUeCvfuvNptb5TUbVD68LRAILPX7p9nqQGh4xJgn3EHS/g==} - dev: false + nopt@5.0.0: + dependencies: + abbrev: 1.1.1 + optional: true - /rollup-plugin-terser@7.0.2(rollup@2.79.1): - resolution: {integrity: sha512-w3iIaU4OxcF52UUXiZNsNeuXIMDvFrr+ZXK6bFZ0Q60qyVfq4uLptoS4bbq3paG3x216eQllFZX7zt6TIImguQ==} - deprecated: This package has been deprecated and is no longer maintained. Please use @rollup/plugin-terser - peerDependencies: - rollup: ^2.0.0 - dependencies: - '@babel/code-frame': 7.21.4 - jest-worker: 26.6.2 - rollup: 2.79.1 - serialize-javascript: 4.0.0 - terser: 5.17.3 - dev: false - - /rollup@2.79.1: - resolution: {integrity: sha512-uKxbd0IhMZOhjAiD5oAFp7BqvkA4Dv47qpOCtaNvng4HBwdbWtdOh8f5nZNuk2rp51PMGk3bzfWu5oayNEuYnw==} - engines: {node: '>=10.0.0'} - hasBin: true - optionalDependencies: - fsevents: 2.3.2 - dev: false + nopt@7.2.1: + dependencies: + abbrev: 2.0.0 + optional: true - /rollup@3.21.7: - resolution: {integrity: sha512-KXPaEuR8FfUoK2uHwNjxTmJ18ApyvD6zJpYv9FOJSqLStmt6xOY84l1IjK2dSolQmoXknrhEFRaPRgOPdqCT5w==} - engines: {node: '>=14.18.0', npm: '>=8.0.0'} - hasBin: true - optionalDependencies: - fsevents: 2.3.2 - dev: false + normalize-path@3.0.0: {} - /run-con@1.2.11: - resolution: {integrity: sha512-NEMGsUT+cglWkzEr4IFK21P4Jca45HqiAbIIZIBdX5+UZTB24Mb/21iNGgz9xZa8tL6vbW7CXmq7MFN42+VjNQ==} - hasBin: true + normalize-range@0.1.2: {} + + npm-run-path@6.0.0: dependencies: - deep-extend: 0.6.0 - ini: 3.0.1 - minimist: 1.2.8 - strip-json-comments: 3.1.1 - dev: false + path-key: 4.0.0 + unicorn-magic: 0.3.0 - /run-parallel@1.2.0: - resolution: {integrity: sha512-5l4VyZR86LZ/lDxZTR6jqL8AFE2S0IFLMP26AbjsLVADxHdhB/c0GUsH+y39UfCi3dzz8OlQuPmnaJOMoDHQBA==} + npmlog@5.0.1: dependencies: - queue-microtask: 1.2.3 - dev: false + are-we-there-yet: 2.0.0 + console-control-strings: 1.1.0 + gauge: 3.0.2 + set-blocking: 2.0.0 + optional: true - /rw@1.3.3: - resolution: {integrity: sha512-PdhdWy89SiZogBLaw42zdeqtRJ//zFd2PgQavcICDUgJT5oW10QCRKbJ6bg4r0/UY2M6BWd5tkxuGFRvCkgfHQ==} - dev: false + nth-check@2.1.1: + dependencies: + boolbase: 1.0.0 - /safe-buffer@5.2.1: - resolution: {integrity: sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ==} - dev: false + object-assign@4.1.1: + optional: true - /safe-regex-test@1.0.0: - resolution: {integrity: sha512-JBUUzyOgEwXQY1NuPtvcj/qcBDbDmEvWufhlnXZIm75DEHp+afM1r1ujJpJsV/gSM4t59tpDyPi1sd6ZaPFfsA==} + once@1.4.0: dependencies: - call-bind: 1.0.2 - get-intrinsic: 1.2.1 - is-regex: 1.1.4 - dev: false + wrappy: 1.0.2 + optional: true - /safer-buffer@2.1.2: - resolution: {integrity: sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==} - dev: false + onetime@7.0.0: + dependencies: + mimic-function: 5.0.1 - /sass@1.62.1: - resolution: {integrity: sha512-NHpxIzN29MXvWiuswfc1W3I0N8SXBd8UR26WntmDlRYf0bSADnwnOjsyMZ3lMezSlArD33Vs3YFhp7dWvL770A==} - engines: {node: '>=14.0.0'} - hasBin: true + oniguruma-to-es@1.0.0: dependencies: - chokidar: 3.5.3 - immutable: 4.3.0 - source-map-js: 1.0.2 - dev: false + emoji-regex-xs: 1.0.0 + regex: 5.1.1 + regex-recursion: 5.1.1 - /sax@1.2.1: - resolution: {integrity: sha512-8I2a3LovHTOpm7NV5yOyO8IHqgVsfK4+UuySrXU8YXkSRX7k6hCV9b3HrkKCr3nMpgj+0bmocaJJWpvp1oc7ZA==} - dev: false + ora@8.1.1: + dependencies: + chalk: 5.4.1 + cli-cursor: 5.0.0 + cli-spinners: 2.9.2 + is-interactive: 2.0.0 + is-unicode-supported: 2.1.0 + log-symbols: 6.0.0 + stdin-discarder: 0.2.2 + string-width: 7.2.0 + strip-ansi: 7.1.0 - /sax@1.2.4: - resolution: {integrity: sha512-NqVDv9TpANUjFm0N8uM5GxL36UgKi9/atZw+x7YFnQ8ckwFGKrl4xX4yWtrey3UJm5nP1kUbnYgLopqWNSRhWw==} - dev: false + p-limit@2.3.0: + dependencies: + p-try: 2.2.0 - /scheduler@0.19.1: - resolution: {integrity: sha512-n/zwRWRYSUj0/3g/otKDRPMh6qv2SYMWNq85IEa8iZyAv8od9zDYpGSnpBEjNgcMNq6Scbu5KfIPxNF72R/2EA==} + p-locate@4.1.0: dependencies: - loose-envify: 1.4.0 - object-assign: 4.1.1 - dev: false + p-limit: 2.3.0 - /section-matter@1.0.0: - resolution: {integrity: sha512-vfD3pmTzGpufjScBh50YHKzEu2lxBWhVEHsNGoEXmCmn2hKGfeNLYMzCJpe8cD7gqX7TJluOVpBkAequ6dgMmA==} - engines: {node: '>=4'} + p-map@4.0.0: dependencies: - extend-shallow: 2.0.1 - kind-of: 6.0.3 - dev: false + aggregate-error: 3.1.0 + optional: true - /semver@6.3.0: - resolution: {integrity: sha512-b39TBaTSfV6yBrapU89p5fKekE2m/NwnDocOVruQFS1/veMgdzuPcnOM34M6CwxW8jH/lxEa5rBoDeUwu5HHTw==} - hasBin: true - dev: false + p-try@2.2.0: {} - /semver@7.5.1: - resolution: {integrity: sha512-Wvss5ivl8TMRZXXESstBA4uR5iXgEN/VC5/sOcuXdVLzcdkz4HWetIoRfG5gb5X+ij/G9rw9YoGn3QoQ8OCSpw==} - engines: {node: '>=10'} - hasBin: true - dependencies: - lru-cache: 6.0.0 - dev: false + package-json-from-dist@1.0.1: + optional: true - /serialize-javascript@4.0.0: - resolution: {integrity: sha512-GaNA54380uFefWghODBWEGisLZFj00nS5ACs6yHa9nLqlLpVLO8ChDGeKRjZnV4Nh4n0Qi7nhYZD/9fCPzEqkw==} + parse-entities@4.0.2: dependencies: - randombytes: 2.1.0 - dev: false + '@types/unist': 2.0.11 + character-entities-legacy: 3.0.0 + character-reference-invalid: 2.0.1 + decode-named-character-reference: 1.0.2 + is-alphanumerical: 2.0.1 + is-decimal: 2.0.1 + is-hexadecimal: 2.0.1 - /set-blocking@2.0.0: - resolution: {integrity: sha512-KiKBS8AnWGEyLzofFfmvKwpdPzqiy16LvQfK3yv/fVH7Bj13/wl3JSR1J+rfgRE9q7xUJK4qvgS8raSOeLUehw==} - dev: false + parse-ms@4.0.0: {} - /shebang-command@2.0.0: - resolution: {integrity: sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA==} - engines: {node: '>=8'} + parse5-htmlparser2-tree-adapter@7.1.0: dependencies: - shebang-regex: 3.0.0 - dev: false + domhandler: 5.0.3 + parse5: 7.2.1 - /shebang-regex@3.0.0: - resolution: {integrity: sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A==} - engines: {node: '>=8'} - dev: false + parse5-parser-stream@7.1.2: + dependencies: + parse5: 7.2.1 - /side-channel@1.0.4: - resolution: {integrity: sha512-q5XPytqFEIKHkGdiMIrY10mvLRvnQh42/+GoBlFW3b2LXLE2xxJpZFdm94we0BaoV3RwJyGqg5wS7epxTv0Zvw==} + parse5@7.2.1: dependencies: - call-bind: 1.0.2 - get-intrinsic: 1.2.1 - object-inspect: 1.12.3 - dev: false + entities: 4.5.0 - /signal-exit@3.0.7: - resolution: {integrity: sha512-wnD2ZE+l+SPC/uoS0vXeE9L1+0wuaMqKlfz9AMUo38JsyLSBWSFcHR1Rri62LZc12vLr1gb3jl7iwQhgwpAbGQ==} - dev: false + path-exists@4.0.0: {} - /signal-exit@4.0.2: - resolution: {integrity: sha512-MY2/qGx4enyjprQnFaZsHib3Yadh3IXyV2C321GY0pjGfVBu4un0uDJkwgdxqO+Rdx8JMT8IfJIRwbYVz3Ob3Q==} - engines: {node: '>=14'} - dev: false + path-is-absolute@1.0.1: + optional: true - /sitemap@7.1.1: - resolution: {integrity: sha512-mK3aFtjz4VdJN0igpIJrinf3EO8U8mxOPsTBzSsy06UtjZQJ3YY3o3Xa7zSc5nMqcMrRwlChHZ18Kxg0caiPBg==} - engines: {node: '>=12.0.0', npm: '>=5.6.0'} - hasBin: true + path-key@3.1.1: {} + + path-key@4.0.0: {} + + path-scurry@1.11.1: dependencies: - '@types/node': 17.0.45 - '@types/sax': 1.2.4 - arg: 5.0.2 - sax: 1.2.4 - dev: false + lru-cache: 10.4.3 + minipass: 7.1.2 + optional: true - /slash@4.0.0: - resolution: {integrity: sha512-3dOsAHXXUkQTpOYcoAxLIorMTp4gIQr5IW3iVb7A7lFIp0VHhnynm9izx6TssdrIcVIESAlVjtnO2K8bg+Coew==} - engines: {node: '>=12'} - dev: false + path-type@5.0.0: {} - /source-map-js@1.0.2: - resolution: {integrity: sha512-R0XvVJ9WusLiqTCEiGCmICCMplcCkIwwR11mOSD9CR5u+IXYdiseeEuXCVAjS54zqwkLcPNnmU4OeJ6tUrWhDw==} - engines: {node: '>=0.10.0'} - dev: false + perfect-debounce@1.0.0: {} - /source-map-support@0.5.21: - resolution: {integrity: sha512-uBHU3L3czsIyYXKX88fdrGovxdSCoTGDRZ6SYXtSRxLZUzHg5P/66Ht6uoUlHu9EZod+inXhKo3qQgwXUT/y1w==} - dependencies: - buffer-from: 1.1.2 - source-map: 0.6.1 - dev: false + photoswipe@5.4.4: {} - /source-map@0.6.1: - resolution: {integrity: sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==} - engines: {node: '>=0.10.0'} - dev: false + picocolors@1.1.1: {} - /source-map@0.8.0-beta.0: - resolution: {integrity: sha512-2ymg6oRBpebeZi9UUNsgQ89bhx01TcTkmNTGnNO88imTmbSgy4nfujrgVEFKWpMTEGA11EDkTt7mqObTPdigIA==} - engines: {node: '>= 8'} - dependencies: - whatwg-url: 7.1.0 - dev: false + picomatch@2.3.1: {} - /sourcemap-codec@1.4.8: - resolution: {integrity: sha512-9NykojV5Uih4lgo5So5dtw+f0JgJX30KCNI8gwhz2J9A15wD0Ml6tjHKwf6fTSa6fAdVBdZeNOs9eJ71qCk8vA==} - deprecated: Please use @jridgewell/sourcemap-codec instead - dev: false + pngjs@5.0.0: {} - /speech-rule-engine@4.0.7: - resolution: {integrity: sha512-sJrL3/wHzNwJRLBdf6CjJWIlxC04iYKkyXvYSVsWVOiC2DSkHmxsqOhEeMsBA9XK+CHuNcsdkbFDnoUfAsmp9g==} - hasBin: true + postcss-load-config@6.0.1(postcss@8.5.0): dependencies: - commander: 9.2.0 - wicked-good-xpath: 1.3.0 - xmldom-sre: 0.1.31 - dev: false + lilconfig: 3.1.3 + optionalDependencies: + postcss: 8.5.0 - /sprintf-js@1.0.3: - resolution: {integrity: sha512-D9cPgkvLlV3t3IzL0D0YLvGA9Ahk4PcvVwUbN0dSGr1aP0Nrt4AEnTUbuGvquEC0mA64Gqt1fzirlRs5ibXx8g==} - dev: false + postcss-value-parser@4.2.0: {} - /stdin-discarder@0.1.0: - resolution: {integrity: sha512-xhV7w8S+bUwlPTb4bAOUQhv8/cSS5offJuX8GQGq32ONF0ZtDWKfkdomM3HMRA+LhX6um/FZ0COqlwsjD53LeQ==} - engines: {node: ^12.20.0 || ^14.13.1 || >=16.0.0} + postcss@8.5.0: dependencies: - bl: 5.1.0 - dev: false + nanoid: 3.3.8 + picocolors: 1.1.1 + source-map-js: 1.2.1 - /string-width@4.2.3: - resolution: {integrity: sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==} - engines: {node: '>=8'} - dependencies: - emoji-regex: 8.0.0 - is-fullwidth-code-point: 3.0.0 - strip-ansi: 6.0.1 - dev: false + prettier@3.4.2: {} - /string-width@5.1.2: - resolution: {integrity: sha512-HnLOCR3vjcY8beoNLtcjZ5/nxn2afmME6lhrDrebokqMap+XbeW8n9TXpPDOqdGK5qcI3oT0GKTW6wC7EMiVqA==} - engines: {node: '>=12'} - dependencies: - eastasianwidth: 0.2.0 - emoji-regex: 9.2.2 - strip-ansi: 7.0.1 - dev: false - - /string.prototype.matchall@4.0.8: - resolution: {integrity: sha512-6zOCOcJ+RJAQshcTvXPHoxoQGONa3e/Lqx90wUA+wEzX78sg5Bo+1tQo4N0pohS0erG9qtCqJDjNCQBjeWVxyg==} - dependencies: - call-bind: 1.0.2 - define-properties: 1.2.0 - es-abstract: 1.21.2 - get-intrinsic: 1.2.1 - has-symbols: 1.0.3 - internal-slot: 1.0.5 - regexp.prototype.flags: 1.5.0 - side-channel: 1.0.4 - dev: false - - /string.prototype.trim@1.2.7: - resolution: {integrity: sha512-p6TmeT1T3411M8Cgg9wBTMRtY2q9+PNy9EV1i2lIXUN/btt763oIfxwN3RR8VU6wHX8j/1CFy0L+YuThm6bgOg==} - engines: {node: '>= 0.4'} - dependencies: - call-bind: 1.0.2 - define-properties: 1.2.0 - es-abstract: 1.21.2 - dev: false - - /string.prototype.trimend@1.0.6: - resolution: {integrity: sha512-JySq+4mrPf9EsDBEDYMOb/lM7XQLulwg5R/m1r0PXEFqrV0qHvl58sdTilSXtKOflCsK2E8jxf+GKC0T07RWwQ==} - dependencies: - call-bind: 1.0.2 - define-properties: 1.2.0 - es-abstract: 1.21.2 - dev: false - - /string.prototype.trimstart@1.0.6: - resolution: {integrity: sha512-omqjMDaY92pbn5HOX7f9IccLA+U1tA9GvtU4JrodiXFfYB7jPzzHpRzpglLAjtUV6bB557zwClJezTqnAiYnQA==} - dependencies: - call-bind: 1.0.2 - define-properties: 1.2.0 - es-abstract: 1.21.2 - dev: false - - /string_decoder@1.3.0: - resolution: {integrity: sha512-hkRX8U1WjJFd8LsDJ2yQ/wWWxaopEsABU1XfkM8A+j0+85JAGppt16cr1Whg6KIbb4okU6Mql6BOj+uup/wKeA==} + pretty-ms@9.2.0: dependencies: - safe-buffer: 5.2.1 - dev: false + parse-ms: 4.0.0 - /stringify-object@3.3.0: - resolution: {integrity: sha512-rHqiFh1elqCQ9WPLIC8I0Q/g/wj5J1eMkyoiD6eoQApWHP0FtlK7rqnhmabL5VUY9JQCcqwwvlOaSuutekgyrw==} - engines: {node: '>=4'} - dependencies: - get-own-enumerable-property-symbols: 3.0.2 - is-obj: 1.0.1 - is-regexp: 1.0.0 - dev: false + proc-log@4.2.0: + optional: true - /strip-ansi@6.0.1: - resolution: {integrity: sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==} - engines: {node: '>=8'} + promise-retry@2.0.1: dependencies: - ansi-regex: 5.0.1 - dev: false + err-code: 2.0.3 + retry: 0.12.0 + optional: true - /strip-ansi@7.0.1: - resolution: {integrity: sha512-cXNxvT8dFNRVfhVME3JAe98mkXDYN2O1l7jmcwMnOslDeESg1rF/OZMtK0nRAhiari1unG5cD4jG3rapUAkLbw==} - engines: {node: '>=12'} - dependencies: - ansi-regex: 6.0.1 - dev: false + property-information@6.5.0: {} - /strip-bom-string@1.0.0: - resolution: {integrity: sha512-uCC2VHvQRYu+lMh4My/sFNmF2klFymLX1wHJeXnbEJERpV/ZsVuonzerjfrGpIGF7LBVa1O7i9kjiWvJiFck8g==} - engines: {node: '>=0.10.0'} - dev: false + punycode.js@2.3.1: {} - /strip-comments@2.0.1: - resolution: {integrity: sha512-ZprKx+bBLXv067WTCALv8SSz5l2+XhpYCsVtSqlMnkAXMWDq+/ekVbl1ghqP9rUHTzv6sm/DwCOiYutU/yp1fw==} - engines: {node: '>=10'} - dev: false + qrcode@1.5.4: + dependencies: + dijkstrajs: 1.0.3 + pngjs: 5.0.0 + yargs: 15.4.1 - /strip-final-newline@3.0.0: - resolution: {integrity: sha512-dOESqjYr96iWYylGObzd39EuNTa5VJxyvVAEm5Jnh7KGo75V43Hk1odPQkNDyXNmUR6k+gEiDVXnjB8HJ3crXw==} - engines: {node: '>=12'} - dev: false + queue-microtask@1.2.3: {} - /strip-json-comments@3.1.1: - resolution: {integrity: sha512-6fPc+R4ihwqP6N/aIv2f1gMH8lOVtWQHoqC4yK6oSDVVocumAsfCqjkXnqiYMhmMwS/mEHLp7Vehlt3ql6lEig==} - engines: {node: '>=8'} - dev: false + readable-stream@3.6.2: + dependencies: + inherits: 2.0.4 + string_decoder: 1.3.0 + util-deprecate: 1.0.2 + optional: true - /striptags@3.2.0: - resolution: {integrity: sha512-g45ZOGzHDMe2bdYMdIvdAfCQkCTDMGBazSw1ypMowwGIee7ZQ5dU0rBJ8Jqgl+jAKIv4dbeE1jscZq9wid1Tkw==} - dev: false + readdirp@3.6.0: + dependencies: + picomatch: 2.3.1 - /stylis@4.2.0: - resolution: {integrity: sha512-Orov6g6BB1sDfYgzWfTHDOxamtX1bE/zo104Dh9e6fqJ3PooipYyfJ0pUmrZO2wAvO8YbEyeFrkV91XTsGMSrw==} - dev: false + readdirp@4.1.1: {} - /supports-color@5.5.0: - resolution: {integrity: sha512-QjVjwdXIt408MIiAqCX4oUKsgU2EqAGzs2Ppkm4aQYbjm+ZEWEcW4SfFNTr4uMNZma0ey4f5lgLrkB0aX0QMow==} - engines: {node: '>=4'} + regex-recursion@5.1.1: dependencies: - has-flag: 3.0.0 - dev: false + regex: 5.1.1 + regex-utilities: 2.3.0 - /supports-color@7.2.0: - resolution: {integrity: sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==} - engines: {node: '>=8'} + regex-utilities@2.3.0: {} + + regex@5.1.1: dependencies: - has-flag: 4.0.0 - dev: false + regex-utilities: 2.3.0 - /supports-preserve-symlinks-flag@1.0.0: - resolution: {integrity: sha512-ot0WnXS9fgdkgIcePe6RHNk1WA8+muPa6cSjeR3V8K27q9BB1rTE3R1p7Hv0z1ZyAc8s6Vvv8DIyWf681MAt0w==} - engines: {node: '>= 0.4'} - dev: false + require-directory@2.1.1: {} - /temp-dir@2.0.0: - resolution: {integrity: sha512-aoBAniQmmwtcKp/7BzsH8Cxzv8OL736p7v1ihGb5e9DJ9kTwGWHrQrVB5+lfVDzfGrdRzXch+ig7LHaY1JTOrg==} - engines: {node: '>=8'} - dev: false + require-main-filename@2.0.0: {} - /tempy@0.6.0: - resolution: {integrity: sha512-G13vtMYPT/J8A4X2SjdtBTphZlrp1gKv6hZiOjw14RCWg6GbHuQBGtjlx75xLbYV/wEc0D7G5K4rxKP/cXk8Bw==} - engines: {node: '>=10'} + restore-cursor@5.1.0: dependencies: - is-stream: 2.0.1 - temp-dir: 2.0.0 - type-fest: 0.16.0 - unique-string: 2.0.0 - dev: false + onetime: 7.0.0 + signal-exit: 4.1.0 - /terser@5.17.3: - resolution: {integrity: sha512-AudpAZKmZHkG9jueayypz4duuCFJMMNGRMwaPvQKWfxKedh8Z2x3OCoDqIIi1xx5+iwx1u6Au8XQcc9Lke65Yg==} - engines: {node: '>=10'} - hasBin: true - dependencies: - '@jridgewell/source-map': 0.3.3 - acorn: 8.8.2 - commander: 2.20.3 - source-map-support: 0.5.21 - dev: false + retry@0.12.0: + optional: true - /to-fast-properties@2.0.0: - resolution: {integrity: sha512-/OaKK0xYrs3DmxRYqL/yDc+FxFUVYhDlXMhRmv3z915w2HF1tnN1omB354j8VUGO/hbRzyD6Y3sA7v7GS/ceog==} - engines: {node: '>=4'} - dev: false + reusify@1.0.4: {} - /to-regex-range@5.0.1: - resolution: {integrity: sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ==} - engines: {node: '>=8.0'} + rfdc@1.4.1: {} + + rimraf@3.0.2: dependencies: - is-number: 7.0.0 - dev: false + glob: 7.2.3 + optional: true - /tr46@1.0.1: - resolution: {integrity: sha512-dTpowEjclQ7Kgx5SdBkqRzVhERQXov8/l9Ft9dVM9fmg0W0KQSVaXX9T4i6twCPNtYiZM53lpSSUAwJbFPOHxA==} + rollup@4.30.1: + dependencies: + '@types/estree': 1.0.6 + optionalDependencies: + '@rollup/rollup-android-arm-eabi': 4.30.1 + '@rollup/rollup-android-arm64': 4.30.1 + '@rollup/rollup-darwin-arm64': 4.30.1 + '@rollup/rollup-darwin-x64': 4.30.1 + '@rollup/rollup-freebsd-arm64': 4.30.1 + '@rollup/rollup-freebsd-x64': 4.30.1 + '@rollup/rollup-linux-arm-gnueabihf': 4.30.1 + '@rollup/rollup-linux-arm-musleabihf': 4.30.1 + '@rollup/rollup-linux-arm64-gnu': 4.30.1 + '@rollup/rollup-linux-arm64-musl': 4.30.1 + '@rollup/rollup-linux-loongarch64-gnu': 4.30.1 + '@rollup/rollup-linux-powerpc64le-gnu': 4.30.1 + '@rollup/rollup-linux-riscv64-gnu': 4.30.1 + '@rollup/rollup-linux-s390x-gnu': 4.30.1 + '@rollup/rollup-linux-x64-gnu': 4.30.1 + '@rollup/rollup-linux-x64-musl': 4.30.1 + '@rollup/rollup-win32-arm64-msvc': 4.30.1 + '@rollup/rollup-win32-ia32-msvc': 4.30.1 + '@rollup/rollup-win32-x64-msvc': 4.30.1 + fsevents: 2.3.3 + + run-parallel@1.2.0: dependencies: - punycode: 2.3.0 - dev: false + queue-microtask: 1.2.3 - /ts-debounce@4.0.0: - resolution: {integrity: sha512-+1iDGY6NmOGidq7i7xZGA4cm8DAa6fqdYcvO5Z6yBevH++Bdo9Qt/mN0TzHUgcCcKv1gmh9+W5dHqz8pMWbCbg==} - dev: false + rxjs@7.8.1: + dependencies: + tslib: 2.8.1 - /ts-dedent@2.2.0: - resolution: {integrity: sha512-q5W7tVM71e2xjHZTlgfTDoPF/SmqKG5hddq9SzR49CH2hayqRKJtQ4mtRlSxKaJlR/+9rEM+mnBHf7I2/BQcpQ==} - engines: {node: '>=6.10'} - dev: false + safe-buffer@5.2.1: + optional: true - /tslib@2.3.0: - resolution: {integrity: sha512-N82ooyxVNm6h1riLCoyS9e3fuJ3AMG2zIZs2Gd1ATcSFjSA23Q0fzjjZeh0jbJvWVDZ0cJT8yaNNaaXHzueNjg==} - dev: false + safer-buffer@2.1.2: {} - /tslib@2.5.0: - resolution: {integrity: sha512-336iVw3rtn2BUK7ORdIAHTyxHGRIHVReokCR3XjbckJMK7ms8FysBfhLR8IXnAgy7T0PTPNBWKiH514FOW/WSg==} - dev: false + sass-embedded-android-arm64@1.83.1: + optional: true - /twikoo@1.6.16: - resolution: {integrity: sha512-U+yaZsM2h0WBoju5TKrm1sq+pb0WEzIynS8B/x4g7UMS30YlUbKePYU7nKU2bf00xubZvkbmICKRNy07naZhhQ==} - dev: false + sass-embedded-android-arm@1.83.1: + optional: true - /type-fest@0.16.0: - resolution: {integrity: sha512-eaBzG6MxNzEn9kiwvtre90cXaNLkmadMWa1zQMs3XORCXNbsH/OewwbxC5ia9dCxIxnTAsSxXJaa/p5y8DlvJg==} - engines: {node: '>=10'} - dev: false + sass-embedded-android-ia32@1.83.1: + optional: true - /typed-array-length@1.0.4: - resolution: {integrity: sha512-KjZypGq+I/H7HI5HlOoGHkWUUGq+Q0TPhQurLbyrVrvnKTBgzLhIJ7j6J/XTQOi0d1RjyZ0wdas8bKs2p0x3Ng==} - dependencies: - call-bind: 1.0.2 - for-each: 0.3.3 - is-typed-array: 1.1.10 - dev: false + sass-embedded-android-riscv64@1.83.1: + optional: true - /ua-parser-js@1.0.35: - resolution: {integrity: sha512-fKnGuqmTBnIE+/KXSzCn4db8RTigUzw1AN0DmdU6hJovUTbYJKyqj+8Mt1c4VfRDnOVJnENmfYkIPZ946UrSAA==} - dev: false + sass-embedded-android-x64@1.83.1: + optional: true - /uc.micro@1.0.6: - resolution: {integrity: sha512-8Y75pvTYkLJW2hWQHXxoqRgV7qb9B+9vFEtidML+7koHUFapnVJAZ6cKs+Qjz5Aw3aZWHMC6u0wJE3At+nSGwA==} - dev: false + sass-embedded-darwin-arm64@1.83.1: + optional: true - /unbox-primitive@1.0.2: - resolution: {integrity: sha512-61pPlCD9h51VoreyJ0BReideM3MDKMKnh6+V9L08331ipq6Q8OFXZYiqP6n/tbHx4s5I9uRhcye6BrbkizkBDw==} - dependencies: - call-bind: 1.0.2 - has-bigints: 1.0.2 - has-symbols: 1.0.3 - which-boxed-primitive: 1.0.2 - dev: false + sass-embedded-darwin-x64@1.83.1: + optional: true - /unicode-canonical-property-names-ecmascript@2.0.0: - resolution: {integrity: sha512-yY5PpDlfVIU5+y/BSCxAJRBIS1Zc2dDG3Ujq+sR0U+JjUevW2JhocOF+soROYDSaAezOzOKuyyixhD6mBknSmQ==} - engines: {node: '>=4'} - dev: false + sass-embedded-linux-arm64@1.83.1: + optional: true - /unicode-match-property-ecmascript@2.0.0: - resolution: {integrity: sha512-5kaZCrbp5mmbz5ulBkDkbY0SsPOjKqVS35VpL9ulMPfSl0J0Xsm+9Evphv9CoIZFwre7aJoa94AY6seMKGVN5Q==} - engines: {node: '>=4'} - dependencies: - unicode-canonical-property-names-ecmascript: 2.0.0 - unicode-property-aliases-ecmascript: 2.1.0 - dev: false + sass-embedded-linux-arm@1.83.1: + optional: true - /unicode-match-property-value-ecmascript@2.1.0: - resolution: {integrity: sha512-qxkjQt6qjg/mYscYMC0XKRn3Rh0wFPlfxB0xkt9CfyTvpX1Ra0+rAmdX2QyAobptSEvuy4RtpPRui6XkV+8wjA==} - engines: {node: '>=4'} - dev: false + sass-embedded-linux-ia32@1.83.1: + optional: true - /unicode-property-aliases-ecmascript@2.1.0: - resolution: {integrity: sha512-6t3foTQI9qne+OZoVQB/8x8rk2k1eVy1gRXhV3oFQ5T6R1dqQ1xtin3XqSlx3+ATBkliTaR/hHyJBm+LVPNM8w==} - engines: {node: '>=4'} - dev: false + sass-embedded-linux-musl-arm64@1.83.1: + optional: true - /unique-string@2.0.0: - resolution: {integrity: sha512-uNaeirEPvpZWSgzwsPGtU2zVSTrn/8L5q/IexZmH0eH6SA73CmAA5U4GwORTxQAZs95TAXLNqeLoPPNO5gZfWg==} - engines: {node: '>=8'} - dependencies: - crypto-random-string: 2.0.0 - dev: false + sass-embedded-linux-musl-arm@1.83.1: + optional: true - /universalify@2.0.0: - resolution: {integrity: sha512-hAZsKq7Yy11Zu1DE0OzWjw7nnLZmJZYTDZZyEFHZdUhV8FkH5MCfoU1XMaxXovpyW5nq5scPqq0ZDP9Zyl04oQ==} - engines: {node: '>= 10.0.0'} - dev: false + sass-embedded-linux-musl-ia32@1.83.1: + optional: true - /upath@1.2.0: - resolution: {integrity: sha512-aZwGpamFO61g3OlfT7OQCHqhGnW43ieH9WZeP7QxN/G/jS4jfqUkZxoryvJgVPEcrl5NL/ggHsSmLMHuH64Lhg==} - engines: {node: '>=4'} - dev: false + sass-embedded-linux-musl-riscv64@1.83.1: + optional: true - /upath@2.0.1: - resolution: {integrity: sha512-1uEe95xksV1O0CYKXo8vQvN1JEbtJp7lb7C5U9HMsIp6IVwntkH/oNUzyVNQSd4S1sYk2FpSSW44FqMc8qee5w==} - engines: {node: '>=4'} - dev: false + sass-embedded-linux-musl-x64@1.83.1: + optional: true - /update-browserslist-db@1.0.11(browserslist@4.21.5): - resolution: {integrity: sha512-dCwEFf0/oT85M1fHBg4F0jtLwJrutGoHSQXCh7u4o2t1drG+c0a9Flnqww6XUKSfQMPpJBRjU8d4RXB09qtvaA==} - hasBin: true - peerDependencies: - browserslist: '>= 4.21.0' - dependencies: - browserslist: 4.21.5 - escalade: 3.1.1 - picocolors: 1.0.0 - dev: false + sass-embedded-linux-riscv64@1.83.1: + optional: true - /uri-js@4.4.1: - resolution: {integrity: sha512-7rKUyy33Q1yc98pQ1DAmLtwX109F7TIfWlW1Ydo8Wl1ii1SeHieeh0HHfPeL2fMXK6z0s8ecKs9frCuLJvndBg==} - dependencies: - punycode: 2.3.0 - dev: false + sass-embedded-linux-x64@1.83.1: + optional: true - /url-polyfill@1.1.12: - resolution: {integrity: sha512-mYFmBHCapZjtcNHW0MDq9967t+z4Dmg5CJ0KqysK3+ZbyoNOWQHksGCTWwDhxGXllkWlOc10Xfko6v4a3ucM6A==} - dev: false + sass-embedded-win32-arm64@1.83.1: + optional: true - /util-deprecate@1.0.2: - resolution: {integrity: sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw==} - dev: false + sass-embedded-win32-ia32@1.83.1: + optional: true - /uuid@9.0.0: - resolution: {integrity: sha512-MXcSTerfPa4uqyzStbRoTgt5XIe3x5+42+q1sDuy3R5MDk66URdLMOZe5aPX/SQd+kuYAh0FdP/pO28IkQyTeg==} - hasBin: true - dev: false + sass-embedded-win32-x64@1.83.1: + optional: true - /vite@4.3.6: - resolution: {integrity: sha512-cqIyLSbA6gornMS659AXTVKF7cvSHMdKmJJwQ9DXq3lwsT1uZSdktuBRlpHQ8VnOWx0QHtjDwxPpGtyo9Fh/Qg==} - engines: {node: ^14.18.0 || >=16.0.0} - hasBin: true - peerDependencies: - '@types/node': '>= 14' - less: '*' - sass: '*' - stylus: '*' - sugarss: '*' - terser: ^5.4.0 - peerDependenciesMeta: - '@types/node': - optional: true - less: - optional: true - sass: - optional: true - stylus: - optional: true - sugarss: - optional: true - terser: - optional: true + sass-embedded@1.83.1: dependencies: - esbuild: 0.17.19 - postcss: 8.4.23 - rollup: 3.21.7 + '@bufbuild/protobuf': 2.2.3 + buffer-builder: 0.2.0 + colorjs.io: 0.5.2 + immutable: 5.0.3 + rxjs: 7.8.1 + supports-color: 8.1.1 + sync-child-process: 1.0.2 + varint: 6.0.0 optionalDependencies: - fsevents: 2.3.2 - dev: false - - /vue-demi@0.14.1(vue@3.3.2): - resolution: {integrity: sha512-rt+yuCtXvscYot9SQQj3WKZJVSriPNqVkpVBNEHPzSgBv7QIYzsS410VqVgvx8f9AAPgjg+XPKvmV3vOqqkJQQ==} - engines: {node: '>=12'} - hasBin: true - requiresBuild: true - peerDependencies: - '@vue/composition-api': ^1.0.0-rc.1 - vue: ^3.0.0-0 || ^2.6.0 - peerDependenciesMeta: - '@vue/composition-api': - optional: true + sass-embedded-android-arm: 1.83.1 + sass-embedded-android-arm64: 1.83.1 + sass-embedded-android-ia32: 1.83.1 + sass-embedded-android-riscv64: 1.83.1 + sass-embedded-android-x64: 1.83.1 + sass-embedded-darwin-arm64: 1.83.1 + sass-embedded-darwin-x64: 1.83.1 + sass-embedded-linux-arm: 1.83.1 + sass-embedded-linux-arm64: 1.83.1 + sass-embedded-linux-ia32: 1.83.1 + sass-embedded-linux-musl-arm: 1.83.1 + sass-embedded-linux-musl-arm64: 1.83.1 + sass-embedded-linux-musl-ia32: 1.83.1 + sass-embedded-linux-musl-riscv64: 1.83.1 + sass-embedded-linux-musl-x64: 1.83.1 + sass-embedded-linux-riscv64: 1.83.1 + sass-embedded-linux-x64: 1.83.1 + sass-embedded-win32-arm64: 1.83.1 + sass-embedded-win32-ia32: 1.83.1 + sass-embedded-win32-x64: 1.83.1 + + sax@1.4.1: {} + + section-matter@1.0.0: dependencies: - vue: 3.3.2 - dev: false + extend-shallow: 2.0.1 + kind-of: 6.0.3 - /vue-router@4.2.0(vue@3.3.2): - resolution: {integrity: sha512-c+usESa6ZoWsm4PPdzRSyenp5A4dsUtnDJnrI03fY1IpIihA9TK3x5ffgkFDpjhLJZewsXoKURapNLFdZjuqTg==} - peerDependencies: - vue: ^3.2.0 - dependencies: - '@vue/devtools-api': 6.5.0 - vue: 3.3.2 - dev: false + semver@6.3.1: + optional: true - /vue@3.3.2: - resolution: {integrity: sha512-98hJcAhyDwZoOo2flAQBSPVYG/o0HA9ivIy2ktHshjE+6/q8IMQ+kvDKQzOZTFPxvnNMcGM+zS2A00xeZMA7tA==} - dependencies: - '@vue/compiler-dom': 3.3.2 - '@vue/compiler-sfc': 3.3.2 - '@vue/runtime-dom': 3.3.2 - '@vue/server-renderer': 3.3.2(vue@3.3.2) - '@vue/shared': 3.3.2 - dev: false + semver@7.6.3: + optional: true - /vuepress-plugin-auto-catalog@2.0.0-beta.211(vuepress@2.0.0-beta.62): - resolution: {integrity: sha512-ywLZB1WUhpDrtMtFRbQX0jSVNFQqqHX8Cz8e0eA2UpkJIkK/m0jOLE4bBglakqN+/t5yqo5qYofvO5f6tHiRbA==} - engines: {node: '>=16.0.0', npm: '>=8', pnpm: '>=7'} - peerDependencies: - sass-loader: ^13.2.2 - vuepress: 2.0.0-beta.62 - vuepress-vite: 2.0.0-beta.62 - vuepress-webpack: 2.0.0-beta.62 - peerDependenciesMeta: - sass-loader: - optional: true - vuepress: - optional: true - vuepress-vite: - optional: true - vuepress-webpack: - optional: true - dependencies: - '@vuepress/client': 2.0.0-beta.62 - '@vuepress/core': 2.0.0-beta.62 - '@vuepress/shared': 2.0.0-beta.62 - '@vuepress/utils': 2.0.0-beta.62 - vue: 3.3.2 - vue-router: 4.2.0(vue@3.3.2) - vuepress: 2.0.0-beta.62(@vuepress/client@2.0.0-beta.62)(vue@3.3.2) - vuepress-plugin-components: 2.0.0-beta.211(vuepress@2.0.0-beta.62) - vuepress-plugin-sass-palette: 2.0.0-beta.211(vuepress@2.0.0-beta.62) - vuepress-shared: 2.0.0-beta.211(vuepress@2.0.0-beta.62) - transitivePeerDependencies: - - '@vue/composition-api' - - supports-color - dev: false + set-blocking@2.0.0: {} - /vuepress-plugin-blog2@2.0.0-beta.211(vuepress@2.0.0-beta.62): - resolution: {integrity: sha512-Wqiu0yxN3Sny7SSiHAzKsFmoe8lwfnXoxMKb3Y85QJTyJ/jP1QOe1Kj0yhPnVrBd0FCFjIatSwZzkJsnNR2ymQ==} - engines: {node: '>=16.0.0', npm: '>=8', pnpm: '>=7'} - peerDependencies: - vuepress: 2.0.0-beta.62 - vuepress-vite: 2.0.0-beta.62 - vuepress-webpack: 2.0.0-beta.62 - peerDependenciesMeta: - vuepress: - optional: true - vuepress-vite: - optional: true - vuepress-webpack: - optional: true + shebang-command@2.0.0: dependencies: - '@vuepress/client': 2.0.0-beta.62 - '@vuepress/core': 2.0.0-beta.62 - '@vuepress/shared': 2.0.0-beta.62 - '@vuepress/utils': 2.0.0-beta.62 - chokidar: 3.5.3 - vue: 3.3.2 - vue-router: 4.2.0(vue@3.3.2) - vuepress: 2.0.0-beta.62(@vuepress/client@2.0.0-beta.62)(vue@3.3.2) - vuepress-shared: 2.0.0-beta.211(vuepress@2.0.0-beta.62) - transitivePeerDependencies: - - '@vue/composition-api' - - supports-color - dev: false + shebang-regex: 3.0.0 - /vuepress-plugin-comment2@2.0.0-beta.211(vuepress@2.0.0-beta.62): - resolution: {integrity: sha512-TSZTaX9Gc37S/oSnXVVWiAeqYOLl1l+shUmipEspqLpPrrphtdEcs0TzakXo/V5ctF79OJArirxn7nFlPxn7jg==} - engines: {node: '>=16.0.0', npm: '>=8', pnpm: '>=7'} - peerDependencies: - sass-loader: ^13.2.2 - vuepress: 2.0.0-beta.62 - vuepress-vite: 2.0.0-beta.62 - vuepress-webpack: 2.0.0-beta.62 - peerDependenciesMeta: - sass-loader: - optional: true - vuepress: - optional: true - vuepress-vite: - optional: true - vuepress-webpack: - optional: true - dependencies: - '@vuepress/client': 2.0.0-beta.62 - '@vuepress/shared': 2.0.0-beta.62 - '@vuepress/utils': 2.0.0-beta.62 - '@waline/client': 2.15.4 - artalk: 2.5.5 - giscus: 1.2.8 - twikoo: 1.6.16 - vue: 3.3.2 - vue-router: 4.2.0(vue@3.3.2) - vuepress: 2.0.0-beta.62(@vuepress/client@2.0.0-beta.62)(vue@3.3.2) - vuepress-plugin-sass-palette: 2.0.0-beta.211(vuepress@2.0.0-beta.62) - vuepress-shared: 2.0.0-beta.211(vuepress@2.0.0-beta.62) - transitivePeerDependencies: - - '@vue/composition-api' - - supports-color - dev: false + shebang-regex@3.0.0: {} - /vuepress-plugin-components@2.0.0-beta.211(vuepress@2.0.0-beta.62): - resolution: {integrity: sha512-zyBWdS2pxkVjB6zkAT+mN7CsF0ctmNLsoMJqI/DMWo/9Amg5v0eBAJkX7ZLgtR6wlvrqdQrp/bj2vv5zldxCoA==} - engines: {node: '>=16.0.0', npm: '>=8', pnpm: '>=7'} - peerDependencies: - sass-loader: ^13.2.2 - vuepress: 2.0.0-beta.62 - vuepress-vite: 2.0.0-beta.62 - vuepress-webpack: 2.0.0-beta.62 - peerDependenciesMeta: - sass-loader: - optional: true - vuepress: - optional: true - vuepress-vite: - optional: true - vuepress-webpack: - optional: true + shiki@1.26.2: dependencies: - '@stackblitz/sdk': 1.9.0 - '@vuepress/client': 2.0.0-beta.62 - '@vuepress/shared': 2.0.0-beta.62 - '@vuepress/utils': 2.0.0-beta.62 - '@vueuse/core': 10.1.2(vue@3.3.2) - artplayer: 5.0.9 - balloon-css: 1.2.0 - dashjs: 4.7.0 - hls.js: 1.4.3 - mpegts.js: 1.7.3 - plyr: 3.7.8 - qrcode: 1.5.3 - vue: 3.3.2 - vue-router: 4.2.0(vue@3.3.2) - vuepress: 2.0.0-beta.62(@vuepress/client@2.0.0-beta.62)(vue@3.3.2) - vuepress-plugin-reading-time2: 2.0.0-beta.211(vuepress@2.0.0-beta.62) - vuepress-plugin-sass-palette: 2.0.0-beta.211(vuepress@2.0.0-beta.62) - vuepress-shared: 2.0.0-beta.211(vuepress@2.0.0-beta.62) - transitivePeerDependencies: - - '@vue/composition-api' - - supports-color - dev: false + '@shikijs/core': 1.26.2 + '@shikijs/engine-javascript': 1.26.2 + '@shikijs/engine-oniguruma': 1.26.2 + '@shikijs/langs': 1.26.2 + '@shikijs/themes': 1.26.2 + '@shikijs/types': 1.26.2 + '@shikijs/vscode-textmate': 10.0.1 + '@types/hast': 3.0.4 - /vuepress-plugin-copy-code2@2.0.0-beta.211(vuepress@2.0.0-beta.62): - resolution: {integrity: sha512-6lrhLSOP6zpEqKpSN1lKvuKed5Hq5ufXmbgOLl8aU3pFN1dagJIPFHyS2O0BNTkxbmPzJQt4ppy66C9yQbvKSw==} - engines: {node: '>=16.0.0', npm: '>=8', pnpm: '>=7'} - peerDependencies: - sass-loader: ^13.2.2 - vuepress: 2.0.0-beta.62 - vuepress-vite: 2.0.0-beta.62 - vuepress-webpack: 2.0.0-beta.62 - peerDependenciesMeta: - sass-loader: - optional: true - vuepress: - optional: true - vuepress-vite: - optional: true - vuepress-webpack: - optional: true + signal-exit@3.0.7: + optional: true + + signal-exit@4.1.0: {} + + sitemap@8.0.0: + dependencies: + '@types/node': 17.0.45 + '@types/sax': 1.2.7 + arg: 5.0.2 + sax: 1.4.1 + + slash@5.1.0: {} + + smart-buffer@4.2.0: + optional: true + + socks-proxy-agent@8.0.5: dependencies: - '@vuepress/client': 2.0.0-beta.62 - '@vuepress/shared': 2.0.0-beta.62 - '@vuepress/utils': 2.0.0-beta.62 - '@vueuse/core': 10.1.2(vue@3.3.2) - balloon-css: 1.2.0 - vue: 3.3.2 - vue-router: 4.2.0(vue@3.3.2) - vuepress: 2.0.0-beta.62(@vuepress/client@2.0.0-beta.62)(vue@3.3.2) - vuepress-plugin-sass-palette: 2.0.0-beta.211(vuepress@2.0.0-beta.62) - vuepress-shared: 2.0.0-beta.211(vuepress@2.0.0-beta.62) + agent-base: 7.1.3 + debug: 4.4.0 + socks: 2.8.3 transitivePeerDependencies: - - '@vue/composition-api' - supports-color - dev: false + optional: true - /vuepress-plugin-copyright2@2.0.0-beta.211(vuepress@2.0.0-beta.62): - resolution: {integrity: sha512-8x/a0NQPBDw3LcM5h8ruRVqK5cIH13dV7xzS/TfpG2WLd2HgItv8lUqL32bhzGAP1OswWLsKw8NZbE6katJMxA==} - engines: {node: '>=16.0.0', npm: '>=8', pnpm: '>=7'} - peerDependencies: - vuepress: 2.0.0-beta.62 - vuepress-vite: 2.0.0-beta.62 - vuepress-webpack: 2.0.0-beta.62 - peerDependenciesMeta: - vuepress: - optional: true - vuepress-vite: - optional: true - vuepress-webpack: - optional: true + socks@2.8.3: dependencies: - '@vuepress/client': 2.0.0-beta.62 - '@vuepress/shared': 2.0.0-beta.62 - '@vuepress/utils': 2.0.0-beta.62 - '@vueuse/core': 10.1.2(vue@3.3.2) - vue: 3.3.2 - vue-router: 4.2.0(vue@3.3.2) - vuepress: 2.0.0-beta.62(@vuepress/client@2.0.0-beta.62)(vue@3.3.2) - vuepress-shared: 2.0.0-beta.211(vuepress@2.0.0-beta.62) - transitivePeerDependencies: - - '@vue/composition-api' - - supports-color - dev: false + ip-address: 9.0.5 + smart-buffer: 4.2.0 + optional: true - /vuepress-plugin-feed2@2.0.0-beta.211(vuepress@2.0.0-beta.62): - resolution: {integrity: sha512-Ir0XQtlLrYFUOZChvULLG///EOdjFAHPLJRNsFJhezjRiBY95nHK1Z9gsRLXBNi5j5mUEHJPWIU90LUQt+CYWQ==} - engines: {node: '>=16.0.0', npm: '>=8', pnpm: '>=7'} - peerDependencies: - vuepress: 2.0.0-beta.62 - vuepress-vite: 2.0.0-beta.62 - vuepress-webpack: 2.0.0-beta.62 - peerDependenciesMeta: - vuepress: - optional: true - vuepress-vite: - optional: true - vuepress-webpack: - optional: true + source-map-js@1.2.1: {} + + space-separated-tokens@2.0.2: {} + + speakingurl@14.0.1: {} + + speech-rule-engine@4.0.7: dependencies: - '@vuepress/shared': 2.0.0-beta.62 - '@vuepress/utils': 2.0.0-beta.62 - cheerio: 1.0.0-rc.12 - vuepress: 2.0.0-beta.62(@vuepress/client@2.0.0-beta.62)(vue@3.3.2) - vuepress-shared: 2.0.0-beta.211(vuepress@2.0.0-beta.62) - xml-js: 1.6.11 - transitivePeerDependencies: - - '@vue/composition-api' - - supports-color - dev: false + commander: 9.2.0 + wicked-good-xpath: 1.3.0 + xmldom-sre: 0.1.31 - /vuepress-plugin-md-enhance@2.0.0-beta.211(react-dom@16.14.0)(react@16.14.0)(vuepress@2.0.0-beta.62): - resolution: {integrity: sha512-zJTlJU1sWLr2PmUkAVtwpq+OSA3x4Sq2C/k4ZGya+NGjMSR8FbrEnCNsFfCRqCSgpzgNUYwCSLHmRlVZpSfy/A==} - engines: {node: '>=16.0.0', npm: '>=8', pnpm: '>=7'} - peerDependencies: - sass-loader: ^13.2.2 - vuepress: 2.0.0-beta.62 - vuepress-vite: 2.0.0-beta.62 - vuepress-webpack: 2.0.0-beta.62 - peerDependenciesMeta: - sass-loader: - optional: true - vuepress: - optional: true - vuepress-vite: - optional: true - vuepress-webpack: - optional: true + sprintf-js@1.0.3: {} + + sprintf-js@1.1.3: + optional: true + + ssri@10.0.6: dependencies: - '@babel/core': 7.21.8 - '@mdit/plugin-align': 0.4.5 - '@mdit/plugin-attrs': 0.4.5 - '@mdit/plugin-container': 0.4.5 - '@mdit/plugin-figure': 0.4.5 - '@mdit/plugin-footnote': 0.4.5 - '@mdit/plugin-img-lazyload': 0.4.5 - '@mdit/plugin-img-mark': 0.4.5 - '@mdit/plugin-img-size': 0.4.5 - '@mdit/plugin-include': 0.4.5 - '@mdit/plugin-katex': 0.4.5 - '@mdit/plugin-mark': 0.4.5 - '@mdit/plugin-mathjax': 0.4.5 - '@mdit/plugin-stylize': 0.4.5 - '@mdit/plugin-sub': 0.4.5 - '@mdit/plugin-sup': 0.4.5 - '@mdit/plugin-tab': 0.4.5 - '@mdit/plugin-tasklist': 0.4.5 - '@mdit/plugin-tex': 0.4.5 - '@mdit/plugin-uml': 0.4.5 - '@types/js-yaml': 4.0.5 - '@types/markdown-it': 12.2.3 - '@vue/repl': 1.4.1(vue@3.3.2) - '@vuepress/client': 2.0.0-beta.62 - '@vuepress/shared': 2.0.0-beta.62 - '@vuepress/utils': 2.0.0-beta.62 - '@vueuse/core': 10.1.2(vue@3.3.2) - balloon-css: 1.2.0 - chart.js: 4.3.0 - echarts: 5.4.2 - flowchart.ts: 0.1.6 - js-yaml: 4.1.0 - katex: 0.16.7 - markdown-it: 13.0.1 - mermaid: 10.1.0(react-dom@16.14.0)(react@16.14.0) - reveal.js: 4.5.0 - vue: 3.3.2 - vue-router: 4.2.0(vue@3.3.2) - vuepress: 2.0.0-beta.62(@vuepress/client@2.0.0-beta.62)(vue@3.3.2) - vuepress-plugin-sass-palette: 2.0.0-beta.211(vuepress@2.0.0-beta.62) - vuepress-shared: 2.0.0-beta.211(vuepress@2.0.0-beta.62) - transitivePeerDependencies: - - '@vue/composition-api' - - react - - react-dom - - supports-color - dev: false + minipass: 7.1.2 + optional: true - /vuepress-plugin-photo-swipe@2.0.0-beta.211(vuepress@2.0.0-beta.62): - resolution: {integrity: sha512-WW/pnZRTPvw/RkGim+cBUWb/HrtISkPRvZYAygPMua0GAYH+cCjnLuHWqD5vSPZFKjJ8S/CdGPgduCPQ5NugZg==} - engines: {node: '>=16.0.0', npm: '>=8', pnpm: '>=7'} - peerDependencies: - sass-loader: ^13.2.2 - vuepress: 2.0.0-beta.62 - vuepress-vite: 2.0.0-beta.62 - vuepress-webpack: 2.0.0-beta.62 - peerDependenciesMeta: - sass-loader: - optional: true - vuepress: - optional: true - vuepress-vite: - optional: true - vuepress-webpack: - optional: true + stdin-discarder@0.2.2: {} + + string-width@4.2.3: dependencies: - '@vuepress/client': 2.0.0-beta.62 - '@vuepress/shared': 2.0.0-beta.62 - '@vuepress/utils': 2.0.0-beta.62 - '@vueuse/core': 10.1.2(vue@3.3.2) - photoswipe: 5.3.7 - vue: 3.3.2 - vue-router: 4.2.0(vue@3.3.2) - vuepress: 2.0.0-beta.62(@vuepress/client@2.0.0-beta.62)(vue@3.3.2) - vuepress-plugin-sass-palette: 2.0.0-beta.211(vuepress@2.0.0-beta.62) - vuepress-shared: 2.0.0-beta.211(vuepress@2.0.0-beta.62) - transitivePeerDependencies: - - '@vue/composition-api' - - supports-color - dev: false + emoji-regex: 8.0.0 + is-fullwidth-code-point: 3.0.0 + strip-ansi: 6.0.1 - /vuepress-plugin-pwa2@2.0.0-beta.211(vuepress@2.0.0-beta.62): - resolution: {integrity: sha512-ZLvICpqJZcM88KEuRsLYklI6cdEdlYVhMi5Cx55Ll9orjzkbJwz7ZtD17hw7axWNGRzu/YReEaWo0rD1R0OV1w==} - engines: {node: '>=16.0.0', npm: '>=8', pnpm: '>=7'} - peerDependencies: - sass-loader: ^13.2.2 - vuepress: 2.0.0-beta.62 - vuepress-vite: 2.0.0-beta.62 - vuepress-webpack: 2.0.0-beta.62 - peerDependenciesMeta: - sass-loader: - optional: true - vuepress: - optional: true - vuepress-vite: - optional: true - vuepress-webpack: - optional: true + string-width@5.1.2: dependencies: - '@vuepress/client': 2.0.0-beta.62 - '@vuepress/shared': 2.0.0-beta.62 - '@vuepress/utils': 2.0.0-beta.62 - '@vueuse/core': 10.1.2(vue@3.3.2) - mitt: 3.0.0 - register-service-worker: 1.7.2 - vue: 3.3.2 - vue-router: 4.2.0(vue@3.3.2) - vuepress: 2.0.0-beta.62(@vuepress/client@2.0.0-beta.62)(vue@3.3.2) - vuepress-plugin-sass-palette: 2.0.0-beta.211(vuepress@2.0.0-beta.62) - vuepress-shared: 2.0.0-beta.211(vuepress@2.0.0-beta.62) - workbox-build: 6.5.4 - transitivePeerDependencies: - - '@types/babel__core' - - '@vue/composition-api' - - supports-color - dev: false + eastasianwidth: 0.2.0 + emoji-regex: 9.2.2 + strip-ansi: 7.1.0 + optional: true - /vuepress-plugin-reading-time2@2.0.0-beta.211(vuepress@2.0.0-beta.62): - resolution: {integrity: sha512-yN6zJRgPauAV4O8M9bax+wYwdnwDfSUvbwLcj/l047Ss/ip/T/vahDMOGL0q2bkgRzTsiHR7N3eRFwZ/ZhSijA==} - engines: {node: '>=16.0.0', npm: '>=8', pnpm: '>=7'} - peerDependencies: - vuepress: 2.0.0-beta.62 - vuepress-vite: 2.0.0-beta.62 - vuepress-webpack: 2.0.0-beta.62 - peerDependenciesMeta: - vuepress: - optional: true - vuepress-vite: - optional: true - vuepress-webpack: - optional: true + string-width@7.2.0: dependencies: - '@vuepress/client': 2.0.0-beta.62 - vue: 3.3.2 - vuepress: 2.0.0-beta.62(@vuepress/client@2.0.0-beta.62)(vue@3.3.2) - vuepress-shared: 2.0.0-beta.211(vuepress@2.0.0-beta.62) - transitivePeerDependencies: - - '@vue/composition-api' - - supports-color - dev: false + emoji-regex: 10.4.0 + get-east-asian-width: 1.3.0 + strip-ansi: 7.1.0 - /vuepress-plugin-rtl@2.0.0-beta.211(vuepress@2.0.0-beta.62): - resolution: {integrity: sha512-VKQC0crK2rohmvb7BF4T7V0HhvkmqioKgXU9UkZOSMqGLDdm0XLzYXPFHSckKfO4xTLxk2gzXi30FQdnKqsUCw==} - engines: {node: '>=16.0.0', npm: '>=8', pnpm: '>=7'} - peerDependencies: - vuepress: 2.0.0-beta.62 - vuepress-vite: 2.0.0-beta.62 - vuepress-webpack: 2.0.0-beta.62 - peerDependenciesMeta: - vuepress: - optional: true - vuepress-vite: - optional: true - vuepress-webpack: - optional: true + string_decoder@1.3.0: dependencies: - '@vuepress/client': 2.0.0-beta.62 - '@vuepress/shared': 2.0.0-beta.62 - '@vuepress/utils': 2.0.0-beta.62 - vue: 3.3.2 - vuepress: 2.0.0-beta.62(@vuepress/client@2.0.0-beta.62)(vue@3.3.2) - vuepress-shared: 2.0.0-beta.211(vuepress@2.0.0-beta.62) - transitivePeerDependencies: - - '@vue/composition-api' - - supports-color - dev: false + safe-buffer: 5.2.1 + optional: true - /vuepress-plugin-sass-palette@2.0.0-beta.211(vuepress@2.0.0-beta.62): - resolution: {integrity: sha512-XGoVvKL6mNKcf3PYGAuyK0eGcH2p6uOjAQzlsAWWb27XlcToKrktkTiJAKFS4DKt1q2cgz696kISwVrAJvd6Mw==} - engines: {node: '>=16.0.0', npm: '>=8', pnpm: '>=7'} - peerDependencies: - sass-loader: ^13.2.2 - vuepress: 2.0.0-beta.62 - vuepress-vite: 2.0.0-beta.62 - vuepress-webpack: 2.0.0-beta.62 - peerDependenciesMeta: - sass-loader: - optional: true - vuepress: - optional: true - vuepress-vite: - optional: true - vuepress-webpack: - optional: true + stringify-entities@4.0.4: dependencies: - '@vuepress/shared': 2.0.0-beta.62 - '@vuepress/utils': 2.0.0-beta.62 - chokidar: 3.5.3 - sass: 1.62.1 - vuepress: 2.0.0-beta.62(@vuepress/client@2.0.0-beta.62)(vue@3.3.2) - vuepress-shared: 2.0.0-beta.211(vuepress@2.0.0-beta.62) - transitivePeerDependencies: - - '@vue/composition-api' - - supports-color - dev: false + character-entities-html4: 2.1.0 + character-entities-legacy: 3.0.0 - /vuepress-plugin-search-pro@2.0.0-beta.211(vuepress@2.0.0-beta.62): - resolution: {integrity: sha512-4Dzh/dJWruHdKb3p9jThtLYkYtnElrUYwzR71GAl7eslyvcfNGr7SC2DRkN3wruRqBEkalTYoGUuuwy2Q0FpGA==} - engines: {node: '>=16.0.0', npm: '>=8', pnpm: '>=7'} - peerDependencies: - sass-loader: ^13.2.2 - vuepress: 2.0.0-beta.62 - vuepress-vite: 2.0.0-beta.62 - vuepress-webpack: 2.0.0-beta.62 - peerDependenciesMeta: - sass-loader: - optional: true - vuepress: - optional: true - vuepress-vite: - optional: true - vuepress-webpack: - optional: true + strip-ansi@6.0.1: dependencies: - '@vuepress/client': 2.0.0-beta.62 - '@vuepress/shared': 2.0.0-beta.62 - '@vuepress/utils': 2.0.0-beta.62 - '@vueuse/core': 10.1.2(vue@3.3.2) - cheerio: 1.0.0-rc.12 - chokidar: 3.5.3 - vue: 3.3.2 - vue-router: 4.2.0(vue@3.3.2) - vuepress: 2.0.0-beta.62(@vuepress/client@2.0.0-beta.62)(vue@3.3.2) - vuepress-plugin-sass-palette: 2.0.0-beta.211(vuepress@2.0.0-beta.62) - vuepress-shared: 2.0.0-beta.211(vuepress@2.0.0-beta.62) - transitivePeerDependencies: - - '@vue/composition-api' - - supports-color - dev: false + ansi-regex: 5.0.1 - /vuepress-plugin-seo2@2.0.0-beta.211(vuepress@2.0.0-beta.62): - resolution: {integrity: sha512-maBaHjUg86lJeAb/kEfE9Q29Sy3Klc39hW8txLLNPmvtpfLy6VnHh+v2Q6cSWOZHQ2o5cGvsIAGNoDlNevCl6g==} - engines: {node: '>=16.0.0', npm: '>=8', pnpm: '>=7'} - peerDependencies: - vuepress: 2.0.0-beta.62 - vuepress-vite: 2.0.0-beta.62 - vuepress-webpack: 2.0.0-beta.62 - peerDependenciesMeta: - vuepress: - optional: true - vuepress-vite: - optional: true - vuepress-webpack: - optional: true + strip-ansi@7.1.0: dependencies: - '@vuepress/shared': 2.0.0-beta.62 - '@vuepress/utils': 2.0.0-beta.62 - vuepress: 2.0.0-beta.62(@vuepress/client@2.0.0-beta.62)(vue@3.3.2) - vuepress-shared: 2.0.0-beta.211(vuepress@2.0.0-beta.62) - transitivePeerDependencies: - - '@vue/composition-api' - - supports-color - dev: false + ansi-regex: 6.1.0 - /vuepress-plugin-sitemap2@2.0.0-beta.211(vuepress@2.0.0-beta.62): - resolution: {integrity: sha512-eJN9uPcs5LvgICRSx5mOA5mwyK4/Ak4PBh/494f3HaQn/oMEZt5LJcKe0Fxj6s6h4b+KHkWL7ckbLWB93N1KpQ==} - engines: {node: '>=16.0.0', npm: '>=8', pnpm: '>=7'} - peerDependencies: - vuepress: 2.0.0-beta.62 - vuepress-vite: 2.0.0-beta.62 - vuepress-webpack: 2.0.0-beta.62 - peerDependenciesMeta: - vuepress: - optional: true - vuepress-vite: - optional: true - vuepress-webpack: - optional: true + strip-bom-string@1.0.0: {} + + strip-final-newline@4.0.0: {} + + superjson@2.2.2: dependencies: - '@vuepress/shared': 2.0.0-beta.62 - '@vuepress/utils': 2.0.0-beta.62 - sitemap: 7.1.1 - vuepress: 2.0.0-beta.62(@vuepress/client@2.0.0-beta.62)(vue@3.3.2) - vuepress-shared: 2.0.0-beta.211(vuepress@2.0.0-beta.62) - transitivePeerDependencies: - - '@vue/composition-api' - - supports-color - dev: false + copy-anything: 3.0.5 - /vuepress-shared@2.0.0-beta.211(vuepress@2.0.0-beta.62): - resolution: {integrity: sha512-CHrLS0i/FsHj3XdgPbHKpCH7AEmFu04JEe8EIwIIPZswPphSbTFb8P+6K1jupoJm6I27owHzAY/FAntXXnyqhQ==} - engines: {node: '>=16.0.0', npm: '>=8', pnpm: '>=7'} - peerDependencies: - vuepress: 2.0.0-beta.62 - vuepress-vite: 2.0.0-beta.62 - vuepress-webpack: 2.0.0-beta.62 - peerDependenciesMeta: - vuepress: - optional: true - vuepress-vite: - optional: true - vuepress-webpack: - optional: true + supports-color@8.1.1: dependencies: - '@vuepress/client': 2.0.0-beta.62 - '@vuepress/shared': 2.0.0-beta.62 - '@vuepress/utils': 2.0.0-beta.62 - '@vueuse/core': 10.1.2(vue@3.3.2) - cheerio: 1.0.0-rc.12 - dayjs: 1.11.7 - execa: 7.1.1 - fflate: 0.7.4 - gray-matter: 4.0.3 - semver: 7.5.1 - striptags: 3.2.0 - vue: 3.3.2 - vue-router: 4.2.0(vue@3.3.2) - vuepress: 2.0.0-beta.62(@vuepress/client@2.0.0-beta.62)(vue@3.3.2) - transitivePeerDependencies: - - '@vue/composition-api' - - supports-color - dev: false + has-flag: 4.0.0 - /vuepress-theme-hope@2.0.0-beta.211(react-dom@16.14.0)(react@16.14.0)(vuepress@2.0.0-beta.62): - resolution: {integrity: sha512-vXSiMrwLXwUBmPuG/ipiGR78wSyH66aJ7BUh3Sv9HFrmLS7iTncVUftiFAtvv1wVYk55BnTSFqNPG3LtyUg1nQ==} - engines: {node: '>=16.0.0', npm: '>=8', pnpm: '>=7'} - peerDependencies: - sass-loader: ^13.2.2 - vuepress: 2.0.0-beta.62 - vuepress-vite: 2.0.0-beta.62 - vuepress-webpack: 2.0.0-beta.62 - peerDependenciesMeta: - sass-loader: - optional: true - vuepress: - optional: true - vuepress-vite: - optional: true - vuepress-webpack: - optional: true + sync-child-process@1.0.2: dependencies: - '@vuepress/cli': 2.0.0-beta.62 - '@vuepress/client': 2.0.0-beta.62 - '@vuepress/core': 2.0.0-beta.62 - '@vuepress/plugin-active-header-links': 2.0.0-beta.62 - '@vuepress/plugin-container': 2.0.0-beta.62 - '@vuepress/plugin-external-link-icon': 2.0.0-beta.62 - '@vuepress/plugin-git': 2.0.0-beta.62 - '@vuepress/plugin-nprogress': 2.0.0-beta.62 - '@vuepress/plugin-palette': 2.0.0-beta.62 - '@vuepress/plugin-prismjs': 2.0.0-beta.62 - '@vuepress/plugin-theme-data': 2.0.0-beta.62 - '@vuepress/shared': 2.0.0-beta.62 - '@vuepress/utils': 2.0.0-beta.62 - '@vueuse/core': 10.1.2(vue@3.3.2) - balloon-css: 1.2.0 - bcrypt-ts: 3.0.1 - cheerio: 1.0.0-rc.12 - chokidar: 3.5.3 - gray-matter: 4.0.3 - vue: 3.3.2 - vue-router: 4.2.0(vue@3.3.2) - vuepress: 2.0.0-beta.62(@vuepress/client@2.0.0-beta.62)(vue@3.3.2) - vuepress-plugin-auto-catalog: 2.0.0-beta.211(vuepress@2.0.0-beta.62) - vuepress-plugin-blog2: 2.0.0-beta.211(vuepress@2.0.0-beta.62) - vuepress-plugin-comment2: 2.0.0-beta.211(vuepress@2.0.0-beta.62) - vuepress-plugin-components: 2.0.0-beta.211(vuepress@2.0.0-beta.62) - vuepress-plugin-copy-code2: 2.0.0-beta.211(vuepress@2.0.0-beta.62) - vuepress-plugin-copyright2: 2.0.0-beta.211(vuepress@2.0.0-beta.62) - vuepress-plugin-feed2: 2.0.0-beta.211(vuepress@2.0.0-beta.62) - vuepress-plugin-md-enhance: 2.0.0-beta.211(react-dom@16.14.0)(react@16.14.0)(vuepress@2.0.0-beta.62) - vuepress-plugin-photo-swipe: 2.0.0-beta.211(vuepress@2.0.0-beta.62) - vuepress-plugin-pwa2: 2.0.0-beta.211(vuepress@2.0.0-beta.62) - vuepress-plugin-reading-time2: 2.0.0-beta.211(vuepress@2.0.0-beta.62) - vuepress-plugin-rtl: 2.0.0-beta.211(vuepress@2.0.0-beta.62) - vuepress-plugin-sass-palette: 2.0.0-beta.211(vuepress@2.0.0-beta.62) - vuepress-plugin-seo2: 2.0.0-beta.211(vuepress@2.0.0-beta.62) - vuepress-plugin-sitemap2: 2.0.0-beta.211(vuepress@2.0.0-beta.62) - vuepress-shared: 2.0.0-beta.211(vuepress@2.0.0-beta.62) - transitivePeerDependencies: - - '@types/babel__core' - - '@vue/composition-api' - - react - - react-dom - - supports-color - dev: false + sync-message-port: 1.1.3 - /vuepress-vite@2.0.0-beta.62(@vuepress/client@2.0.0-beta.62)(vue@3.3.2): - resolution: {integrity: sha512-C93T5ZCFMnbdXkZ/R/romtwPPP2zjPN38YZhrM6w6wWjSMDvrG26IFRwluXy+W84O0Pg7xOwqRom0wvO4kCxmA==} - engines: {node: '>=16.19.0'} - hasBin: true - peerDependencies: - '@vuepress/client': 2.0.0-beta.62 - vue: ^3.3.1 - dependencies: - '@vuepress/bundler-vite': 2.0.0-beta.62 - '@vuepress/cli': 2.0.0-beta.62 - '@vuepress/client': 2.0.0-beta.62 - '@vuepress/core': 2.0.0-beta.62 - '@vuepress/theme-default': 2.0.0-beta.62 - vue: 3.3.2 - transitivePeerDependencies: - - '@types/node' - - '@vue/composition-api' - - less - - sass - - sass-loader - - stylus - - sugarss - - supports-color - - terser - - ts-node - dev: false + sync-message-port@1.1.3: {} - /vuepress@2.0.0-beta.62(@vuepress/client@2.0.0-beta.62)(vue@3.3.2): - resolution: {integrity: sha512-kwoC7RA6PGetWSU/NwV6dJ3VItg+R+K2IpAJ4bKsnRueIqGpDZwPr423nRK0VwDhh2sN7lUn6LoyaybPwWrGZg==} - engines: {node: '>=16.19.0'} - hasBin: true + tar@6.2.1: dependencies: - vuepress-vite: 2.0.0-beta.62(@vuepress/client@2.0.0-beta.62)(vue@3.3.2) - transitivePeerDependencies: - - '@types/node' - - '@vue/composition-api' - - '@vuepress/client' - - less - - sass - - sass-loader - - stylus - - sugarss - - supports-color - - terser - - ts-node - - vue - dev: false + chownr: 2.0.0 + fs-minipass: 2.1.0 + minipass: 5.0.0 + minizlib: 2.1.2 + mkdirp: 1.0.4 + yallist: 4.0.0 + optional: true - /wcwidth@1.0.1: - resolution: {integrity: sha512-XHPEwS0q6TaxcvG85+8EYkbiCux2XtWG2mkc47Ng2A77BQu9+DqIOJldST4HgPkuea7dvKSj5VgX3P1d4rW8Tg==} + to-regex-range@5.0.1: dependencies: - defaults: 1.0.4 - dev: false + is-number: 7.0.0 - /web-worker@1.2.0: - resolution: {integrity: sha512-PgF341avzqyx60neE9DD+XS26MMNMoUQRz9NOZwW32nPQrF6p77f1htcnjBSEV8BGMKZ16choqUG4hyI0Hx7mA==} - dev: false + tr46@0.0.3: + optional: true + + trim-lines@3.0.1: {} + + tslib@2.8.1: {} + + uc.micro@2.1.0: {} - /webidl-conversions@4.0.2: - resolution: {integrity: sha512-YQ+BmxuTgd6UXZW3+ICGfyqRyHXVlD5GtQr5+qjiNW7bF0cqrzX500HVXPBOvgXb5YnzDd+h0zqyv61KUD7+Sg==} - dev: false + undici-types@6.20.0: {} - /webworkify-webpack@2.1.5: - resolution: {integrity: sha512-2akF8FIyUvbiBBdD+RoHpoTbHMQF2HwjcxfDvgztAX5YwbZNyrtfUMgvfgFVsgDhDPVTlkbb5vyasqDHfIDPQw==} - dev: false + undici@6.21.0: {} - /whatwg-url@7.1.0: - resolution: {integrity: sha512-WUu7Rg1DroM7oQvGWfOiAK21n74Gg+T4elXEQYkOhtyLeWiJFoOGLXPKI/9gzIie9CtwVLm8wtw6YJdKyxSjeg==} + unicorn-magic@0.1.0: {} + + unicorn-magic@0.3.0: {} + + unique-filename@3.0.0: dependencies: - lodash.sortby: 4.7.0 - tr46: 1.0.1 - webidl-conversions: 4.0.2 - dev: false + unique-slug: 4.0.0 + optional: true - /which-boxed-primitive@1.0.2: - resolution: {integrity: sha512-bwZdv0AKLpplFY2KZRX6TvyuN7ojjr7lwkg6ml0roIy9YeuSr7JS372qlNW18UQYzgYK9ziGcerWqZOmEn9VNg==} + unique-slug@4.0.0: dependencies: - is-bigint: 1.0.4 - is-boolean-object: 1.1.2 - is-number-object: 1.0.7 - is-string: 1.0.7 - is-symbol: 1.0.4 - dev: false + imurmurhash: 0.1.4 + optional: true - /which-module@2.0.1: - resolution: {integrity: sha512-iBdZ57RDvnOR9AGBhML2vFZf7h8vmBjhoaZqODJBFWHVtKkDmKuHai3cx5PgVMrX5YDNp27AofYbAwctSS+vhQ==} - dev: false + unist-util-is@6.0.0: + dependencies: + '@types/unist': 3.0.3 - /which-typed-array@1.1.9: - resolution: {integrity: sha512-w9c4xkx6mPidwp7180ckYWfMmvxpjlZuIudNtDf4N/tTAUB8VJbX25qZoAsrtGuYNnGw3pa0AXgbGKRB8/EceA==} - engines: {node: '>= 0.4'} + unist-util-position@5.0.0: dependencies: - available-typed-arrays: 1.0.5 - call-bind: 1.0.2 - for-each: 0.3.3 - gopd: 1.0.1 - has-tostringtag: 1.0.0 - is-typed-array: 1.1.10 - dev: false + '@types/unist': 3.0.3 - /which@2.0.2: - resolution: {integrity: sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA==} - engines: {node: '>= 8'} - hasBin: true + unist-util-stringify-position@4.0.0: dependencies: - isexe: 2.0.0 - dev: false + '@types/unist': 3.0.3 - /wicked-good-xpath@1.3.0: - resolution: {integrity: sha512-Gd9+TUn5nXdwj/hFsPVx5cuHHiF5Bwuc30jZ4+ronF1qHK5O7HD0sgmXWSEgwKquT3ClLoKPVbO6qGwVwLzvAw==} - dev: false - - /workbox-background-sync@6.5.4: - resolution: {integrity: sha512-0r4INQZMyPky/lj4Ou98qxcThrETucOde+7mRGJl13MPJugQNKeZQOdIJe/1AchOP23cTqHcN/YVpD6r8E6I8g==} - dependencies: - idb: 7.1.1 - workbox-core: 6.5.4 - dev: false - - /workbox-broadcast-update@6.5.4: - resolution: {integrity: sha512-I/lBERoH1u3zyBosnpPEtcAVe5lwykx9Yg1k6f8/BGEPGaMMgZrwVrqL1uA9QZ1NGGFoyE6t9i7lBjOlDhFEEw==} - dependencies: - workbox-core: 6.5.4 - dev: false - - /workbox-build@6.5.4: - resolution: {integrity: sha512-kgRevLXEYvUW9WS4XoziYqZ8Q9j/2ziJYEtTrjdz5/L/cTUa2XfyMP2i7c3p34lgqJ03+mTiz13SdFef2POwbA==} - engines: {node: '>=10.0.0'} - dependencies: - '@apideck/better-ajv-errors': 0.3.6(ajv@8.12.0) - '@babel/core': 7.21.8 - '@babel/preset-env': 7.21.5(@babel/core@7.21.8) - '@babel/runtime': 7.21.5 - '@rollup/plugin-babel': 5.3.1(@babel/core@7.21.8)(rollup@2.79.1) - '@rollup/plugin-node-resolve': 11.2.1(rollup@2.79.1) - '@rollup/plugin-replace': 2.4.2(rollup@2.79.1) - '@surma/rollup-plugin-off-main-thread': 2.2.3 - ajv: 8.12.0 - common-tags: 1.8.2 - fast-json-stable-stringify: 2.1.0 - fs-extra: 9.1.0 - glob: 7.2.3 - lodash: 4.17.21 - pretty-bytes: 5.6.0 - rollup: 2.79.1 - rollup-plugin-terser: 7.0.2(rollup@2.79.1) - source-map: 0.8.0-beta.0 - stringify-object: 3.3.0 - strip-comments: 2.0.1 - tempy: 0.6.0 - upath: 1.2.0 - workbox-background-sync: 6.5.4 - workbox-broadcast-update: 6.5.4 - workbox-cacheable-response: 6.5.4 - workbox-core: 6.5.4 - workbox-expiration: 6.5.4 - workbox-google-analytics: 6.5.4 - workbox-navigation-preload: 6.5.4 - workbox-precaching: 6.5.4 - workbox-range-requests: 6.5.4 - workbox-recipes: 6.5.4 - workbox-routing: 6.5.4 - workbox-strategies: 6.5.4 - workbox-streams: 6.5.4 - workbox-sw: 6.5.4 - workbox-window: 6.5.4 - transitivePeerDependencies: - - '@types/babel__core' - - supports-color - dev: false + unist-util-visit-parents@6.0.1: + dependencies: + '@types/unist': 3.0.3 + unist-util-is: 6.0.0 + + unist-util-visit@5.0.0: + dependencies: + '@types/unist': 3.0.3 + unist-util-is: 6.0.0 + unist-util-visit-parents: 6.0.1 + + universalify@2.0.1: {} - /workbox-cacheable-response@6.5.4: - resolution: {integrity: sha512-DCR9uD0Fqj8oB2TSWQEm1hbFs/85hXXoayVwFKLVuIuxwJaihBsLsp4y7J9bvZbqtPJ1KlCkmYVGQKrBU4KAug==} + upath@2.0.1: {} + + update-browserslist-db@1.1.2(browserslist@4.24.4): dependencies: - workbox-core: 6.5.4 - dev: false + browserslist: 4.24.4 + escalade: 3.2.0 + picocolors: 1.1.1 + + util-deprecate@1.0.2: + optional: true - /workbox-core@6.5.4: - resolution: {integrity: sha512-OXYb+m9wZm8GrORlV2vBbE5EC1FKu71GGp0H4rjmxmF4/HLbMCoTFws87M3dFwgpmg0v00K++PImpNQ6J5NQ6Q==} - dev: false + varint@6.0.0: {} - /workbox-expiration@6.5.4: - resolution: {integrity: sha512-jUP5qPOpH1nXtjGGh1fRBa1wJL2QlIb5mGpct3NzepjGG2uFFBn4iiEBiI9GUmfAFR2ApuRhDydjcRmYXddiEQ==} + vfile-message@4.0.2: dependencies: - idb: 7.1.1 - workbox-core: 6.5.4 - dev: false + '@types/unist': 3.0.3 + unist-util-stringify-position: 4.0.0 - /workbox-google-analytics@6.5.4: - resolution: {integrity: sha512-8AU1WuaXsD49249Wq0B2zn4a/vvFfHkpcFfqAFHNHwln3jK9QUYmzdkKXGIZl9wyKNP+RRX30vcgcyWMcZ9VAg==} + vfile@6.0.3: dependencies: - workbox-background-sync: 6.5.4 - workbox-core: 6.5.4 - workbox-routing: 6.5.4 - workbox-strategies: 6.5.4 - dev: false + '@types/unist': 3.0.3 + vfile-message: 4.0.2 - /workbox-navigation-preload@6.5.4: - resolution: {integrity: sha512-IIwf80eO3cr8h6XSQJF+Hxj26rg2RPFVUmJLUlM0+A2GzB4HFbQyKkrgD5y2d84g2IbJzP4B4j5dPBRzamHrng==} + vite@6.0.7(@types/node@22.10.5)(sass-embedded@1.83.1): dependencies: - workbox-core: 6.5.4 - dev: false + esbuild: 0.24.2 + postcss: 8.5.0 + rollup: 4.30.1 + optionalDependencies: + '@types/node': 22.10.5 + fsevents: 2.3.3 + sass-embedded: 1.83.1 - /workbox-precaching@6.5.4: - resolution: {integrity: sha512-hSMezMsW6btKnxHB4bFy2Qfwey/8SYdGWvVIKFaUm8vJ4E53JAY+U2JwLTRD8wbLWoP6OVUdFlXsTdKu9yoLTg==} + vue-router@4.5.0(vue@3.5.13): dependencies: - workbox-core: 6.5.4 - workbox-routing: 6.5.4 - workbox-strategies: 6.5.4 - dev: false + '@vue/devtools-api': 6.6.4 + vue: 3.5.13 - /workbox-range-requests@6.5.4: - resolution: {integrity: sha512-Je2qR1NXCFC8xVJ/Lux6saH6IrQGhMpDrPXWZWWS8n/RD+WZfKa6dSZwU+/QksfEadJEr/NfY+aP/CXFFK5JFg==} + vue@3.5.13: dependencies: - workbox-core: 6.5.4 - dev: false + '@vue/compiler-dom': 3.5.13 + '@vue/compiler-sfc': 3.5.13 + '@vue/runtime-dom': 3.5.13 + '@vue/server-renderer': 3.5.13(vue@3.5.13) + '@vue/shared': 3.5.13 - /workbox-recipes@6.5.4: - resolution: {integrity: sha512-QZNO8Ez708NNwzLNEXTG4QYSKQ1ochzEtRLGaq+mr2PyoEIC1xFW7MrWxrONUxBFOByksds9Z4//lKAX8tHyUA==} + vuepress-plugin-components@2.0.0-rc.68(sass-embedded@1.83.1)(vuepress@2.0.0-rc.19(@vuepress/bundler-vite@2.0.0-rc.19(@types/node@22.10.5)(sass-embedded@1.83.1))(vue@3.5.13)): dependencies: - workbox-cacheable-response: 6.5.4 - workbox-core: 6.5.4 - workbox-expiration: 6.5.4 - workbox-precaching: 6.5.4 - workbox-routing: 6.5.4 - workbox-strategies: 6.5.4 - dev: false + '@stackblitz/sdk': 1.11.0 + '@vuepress/helper': 2.0.0-rc.70(vuepress@2.0.0-rc.19(@vuepress/bundler-vite@2.0.0-rc.19(@types/node@22.10.5)(sass-embedded@1.83.1))(vue@3.5.13)) + '@vuepress/plugin-sass-palette': 2.0.0-rc.70(sass-embedded@1.83.1)(vuepress@2.0.0-rc.19(@vuepress/bundler-vite@2.0.0-rc.19(@types/node@22.10.5)(sass-embedded@1.83.1))(vue@3.5.13)) + '@vueuse/core': 12.4.0 + balloon-css: 1.2.0 + create-codepen: 2.0.0 + qrcode: 1.5.4 + vue: 3.5.13 + vuepress: 2.0.0-rc.19(@vuepress/bundler-vite@2.0.0-rc.19(@types/node@22.10.5)(sass-embedded@1.83.1))(vue@3.5.13) + vuepress-shared: 2.0.0-rc.68(vuepress@2.0.0-rc.19(@vuepress/bundler-vite@2.0.0-rc.19(@types/node@22.10.5)(sass-embedded@1.83.1))(vue@3.5.13)) + optionalDependencies: + sass-embedded: 1.83.1 + transitivePeerDependencies: + - typescript + + vuepress-plugin-md-enhance@2.0.0-rc.68(markdown-it@14.1.0)(sass-embedded@1.83.1)(vuepress@2.0.0-rc.19(@vuepress/bundler-vite@2.0.0-rc.19(@types/node@22.10.5)(sass-embedded@1.83.1))(vue@3.5.13)): + dependencies: + '@mdit/plugin-container': 0.16.0(markdown-it@14.1.0) + '@mdit/plugin-demo': 0.16.0(markdown-it@14.1.0) + '@mdit/plugin-plantuml': 0.16.0(markdown-it@14.1.0) + '@mdit/plugin-uml': 0.16.0(markdown-it@14.1.0) + '@types/markdown-it': 14.1.2 + '@vuepress/helper': 2.0.0-rc.70(vuepress@2.0.0-rc.19(@vuepress/bundler-vite@2.0.0-rc.19(@types/node@22.10.5)(sass-embedded@1.83.1))(vue@3.5.13)) + '@vuepress/plugin-sass-palette': 2.0.0-rc.70(sass-embedded@1.83.1)(vuepress@2.0.0-rc.19(@vuepress/bundler-vite@2.0.0-rc.19(@types/node@22.10.5)(sass-embedded@1.83.1))(vue@3.5.13)) + '@vueuse/core': 12.4.0 + balloon-css: 1.2.0 + js-yaml: 4.1.0 + vue: 3.5.13 + vuepress: 2.0.0-rc.19(@vuepress/bundler-vite@2.0.0-rc.19(@types/node@22.10.5)(sass-embedded@1.83.1))(vue@3.5.13) + vuepress-shared: 2.0.0-rc.68(vuepress@2.0.0-rc.19(@vuepress/bundler-vite@2.0.0-rc.19(@types/node@22.10.5)(sass-embedded@1.83.1))(vue@3.5.13)) + optionalDependencies: + sass-embedded: 1.83.1 + transitivePeerDependencies: + - markdown-it + - typescript - /workbox-routing@6.5.4: - resolution: {integrity: sha512-apQswLsbrrOsBUWtr9Lf80F+P1sHnQdYodRo32SjiByYi36IDyL2r7BH1lJtFX8fwNHDa1QOVY74WKLLS6o5Pg==} + vuepress-shared@2.0.0-rc.68(vuepress@2.0.0-rc.19(@vuepress/bundler-vite@2.0.0-rc.19(@types/node@22.10.5)(sass-embedded@1.83.1))(vue@3.5.13)): dependencies: - workbox-core: 6.5.4 - dev: false + '@vuepress/helper': 2.0.0-rc.70(vuepress@2.0.0-rc.19(@vuepress/bundler-vite@2.0.0-rc.19(@types/node@22.10.5)(sass-embedded@1.83.1))(vue@3.5.13)) + '@vueuse/core': 12.4.0 + dayjs: 1.11.13 + vue: 3.5.13 + vuepress: 2.0.0-rc.19(@vuepress/bundler-vite@2.0.0-rc.19(@types/node@22.10.5)(sass-embedded@1.83.1))(vue@3.5.13) + transitivePeerDependencies: + - typescript + + vuepress-theme-hope@2.0.0-rc.68(@vuepress/plugin-feed@2.0.0-rc.70(vuepress@2.0.0-rc.19(@vuepress/bundler-vite@2.0.0-rc.19(@types/node@22.10.5)(sass-embedded@1.83.1))(vue@3.5.13)))(@vuepress/plugin-search@2.0.0-rc.70(vuepress@2.0.0-rc.19(@vuepress/bundler-vite@2.0.0-rc.19(@types/node@22.10.5)(sass-embedded@1.83.1))(vue@3.5.13)))(katex@0.16.20)(markdown-it@14.1.0)(mathjax-full@3.2.2)(nodejs-jieba@0.2.1(encoding@0.1.13))(sass-embedded@1.83.1)(vuepress@2.0.0-rc.19(@vuepress/bundler-vite@2.0.0-rc.19(@types/node@22.10.5)(sass-embedded@1.83.1))(vue@3.5.13)): + dependencies: + '@vuepress/helper': 2.0.0-rc.70(vuepress@2.0.0-rc.19(@vuepress/bundler-vite@2.0.0-rc.19(@types/node@22.10.5)(sass-embedded@1.83.1))(vue@3.5.13)) + '@vuepress/plugin-active-header-links': 2.0.0-rc.70(vuepress@2.0.0-rc.19(@vuepress/bundler-vite@2.0.0-rc.19(@types/node@22.10.5)(sass-embedded@1.83.1))(vue@3.5.13)) + '@vuepress/plugin-back-to-top': 2.0.0-rc.70(vuepress@2.0.0-rc.19(@vuepress/bundler-vite@2.0.0-rc.19(@types/node@22.10.5)(sass-embedded@1.83.1))(vue@3.5.13)) + '@vuepress/plugin-blog': 2.0.0-rc.70(vuepress@2.0.0-rc.19(@vuepress/bundler-vite@2.0.0-rc.19(@types/node@22.10.5)(sass-embedded@1.83.1))(vue@3.5.13)) + '@vuepress/plugin-catalog': 2.0.0-rc.70(vuepress@2.0.0-rc.19(@vuepress/bundler-vite@2.0.0-rc.19(@types/node@22.10.5)(sass-embedded@1.83.1))(vue@3.5.13)) + '@vuepress/plugin-comment': 2.0.0-rc.70(vuepress@2.0.0-rc.19(@vuepress/bundler-vite@2.0.0-rc.19(@types/node@22.10.5)(sass-embedded@1.83.1))(vue@3.5.13)) + '@vuepress/plugin-copy-code': 2.0.0-rc.70(vuepress@2.0.0-rc.19(@vuepress/bundler-vite@2.0.0-rc.19(@types/node@22.10.5)(sass-embedded@1.83.1))(vue@3.5.13)) + '@vuepress/plugin-copyright': 2.0.0-rc.70(vuepress@2.0.0-rc.19(@vuepress/bundler-vite@2.0.0-rc.19(@types/node@22.10.5)(sass-embedded@1.83.1))(vue@3.5.13)) + '@vuepress/plugin-git': 2.0.0-rc.68(vuepress@2.0.0-rc.19(@vuepress/bundler-vite@2.0.0-rc.19(@types/node@22.10.5)(sass-embedded@1.83.1))(vue@3.5.13)) + '@vuepress/plugin-icon': 2.0.0-rc.70(markdown-it@14.1.0)(vuepress@2.0.0-rc.19(@vuepress/bundler-vite@2.0.0-rc.19(@types/node@22.10.5)(sass-embedded@1.83.1))(vue@3.5.13)) + '@vuepress/plugin-links-check': 2.0.0-rc.70(vuepress@2.0.0-rc.19(@vuepress/bundler-vite@2.0.0-rc.19(@types/node@22.10.5)(sass-embedded@1.83.1))(vue@3.5.13)) + '@vuepress/plugin-markdown-ext': 2.0.0-rc.70(markdown-it@14.1.0)(vuepress@2.0.0-rc.19(@vuepress/bundler-vite@2.0.0-rc.19(@types/node@22.10.5)(sass-embedded@1.83.1))(vue@3.5.13)) + '@vuepress/plugin-markdown-hint': 2.0.0-rc.70(markdown-it@14.1.0)(vuepress@2.0.0-rc.19(@vuepress/bundler-vite@2.0.0-rc.19(@types/node@22.10.5)(sass-embedded@1.83.1))(vue@3.5.13)) + '@vuepress/plugin-markdown-image': 2.0.0-rc.70(markdown-it@14.1.0)(vuepress@2.0.0-rc.19(@vuepress/bundler-vite@2.0.0-rc.19(@types/node@22.10.5)(sass-embedded@1.83.1))(vue@3.5.13)) + '@vuepress/plugin-markdown-include': 2.0.0-rc.70(markdown-it@14.1.0)(vuepress@2.0.0-rc.19(@vuepress/bundler-vite@2.0.0-rc.19(@types/node@22.10.5)(sass-embedded@1.83.1))(vue@3.5.13)) + '@vuepress/plugin-markdown-math': 2.0.0-rc.70(katex@0.16.20)(markdown-it@14.1.0)(mathjax-full@3.2.2)(vuepress@2.0.0-rc.19(@vuepress/bundler-vite@2.0.0-rc.19(@types/node@22.10.5)(sass-embedded@1.83.1))(vue@3.5.13)) + '@vuepress/plugin-markdown-stylize': 2.0.0-rc.70(markdown-it@14.1.0)(vuepress@2.0.0-rc.19(@vuepress/bundler-vite@2.0.0-rc.19(@types/node@22.10.5)(sass-embedded@1.83.1))(vue@3.5.13)) + '@vuepress/plugin-markdown-tab': 2.0.0-rc.70(markdown-it@14.1.0)(vuepress@2.0.0-rc.19(@vuepress/bundler-vite@2.0.0-rc.19(@types/node@22.10.5)(sass-embedded@1.83.1))(vue@3.5.13)) + '@vuepress/plugin-notice': 2.0.0-rc.70(vuepress@2.0.0-rc.19(@vuepress/bundler-vite@2.0.0-rc.19(@types/node@22.10.5)(sass-embedded@1.83.1))(vue@3.5.13)) + '@vuepress/plugin-nprogress': 2.0.0-rc.70(vuepress@2.0.0-rc.19(@vuepress/bundler-vite@2.0.0-rc.19(@types/node@22.10.5)(sass-embedded@1.83.1))(vue@3.5.13)) + '@vuepress/plugin-photo-swipe': 2.0.0-rc.70(vuepress@2.0.0-rc.19(@vuepress/bundler-vite@2.0.0-rc.19(@types/node@22.10.5)(sass-embedded@1.83.1))(vue@3.5.13)) + '@vuepress/plugin-reading-time': 2.0.0-rc.70(vuepress@2.0.0-rc.19(@vuepress/bundler-vite@2.0.0-rc.19(@types/node@22.10.5)(sass-embedded@1.83.1))(vue@3.5.13)) + '@vuepress/plugin-redirect': 2.0.0-rc.70(vuepress@2.0.0-rc.19(@vuepress/bundler-vite@2.0.0-rc.19(@types/node@22.10.5)(sass-embedded@1.83.1))(vue@3.5.13)) + '@vuepress/plugin-rtl': 2.0.0-rc.70(vuepress@2.0.0-rc.19(@vuepress/bundler-vite@2.0.0-rc.19(@types/node@22.10.5)(sass-embedded@1.83.1))(vue@3.5.13)) + '@vuepress/plugin-sass-palette': 2.0.0-rc.70(sass-embedded@1.83.1)(vuepress@2.0.0-rc.19(@vuepress/bundler-vite@2.0.0-rc.19(@types/node@22.10.5)(sass-embedded@1.83.1))(vue@3.5.13)) + '@vuepress/plugin-seo': 2.0.0-rc.70(vuepress@2.0.0-rc.19(@vuepress/bundler-vite@2.0.0-rc.19(@types/node@22.10.5)(sass-embedded@1.83.1))(vue@3.5.13)) + '@vuepress/plugin-shiki': 2.0.0-rc.70(@vueuse/core@12.4.0)(vuepress@2.0.0-rc.19(@vuepress/bundler-vite@2.0.0-rc.19(@types/node@22.10.5)(sass-embedded@1.83.1))(vue@3.5.13)) + '@vuepress/plugin-sitemap': 2.0.0-rc.70(vuepress@2.0.0-rc.19(@vuepress/bundler-vite@2.0.0-rc.19(@types/node@22.10.5)(sass-embedded@1.83.1))(vue@3.5.13)) + '@vuepress/plugin-theme-data': 2.0.0-rc.70(vuepress@2.0.0-rc.19(@vuepress/bundler-vite@2.0.0-rc.19(@types/node@22.10.5)(sass-embedded@1.83.1))(vue@3.5.13)) + '@vueuse/core': 12.4.0 + balloon-css: 1.2.0 + bcrypt-ts: 5.0.3 + chokidar: 3.6.0 + vue: 3.5.13 + vuepress: 2.0.0-rc.19(@vuepress/bundler-vite@2.0.0-rc.19(@types/node@22.10.5)(sass-embedded@1.83.1))(vue@3.5.13) + vuepress-plugin-components: 2.0.0-rc.68(sass-embedded@1.83.1)(vuepress@2.0.0-rc.19(@vuepress/bundler-vite@2.0.0-rc.19(@types/node@22.10.5)(sass-embedded@1.83.1))(vue@3.5.13)) + vuepress-plugin-md-enhance: 2.0.0-rc.68(markdown-it@14.1.0)(sass-embedded@1.83.1)(vuepress@2.0.0-rc.19(@vuepress/bundler-vite@2.0.0-rc.19(@types/node@22.10.5)(sass-embedded@1.83.1))(vue@3.5.13)) + vuepress-shared: 2.0.0-rc.68(vuepress@2.0.0-rc.19(@vuepress/bundler-vite@2.0.0-rc.19(@types/node@22.10.5)(sass-embedded@1.83.1))(vue@3.5.13)) + optionalDependencies: + '@vuepress/plugin-feed': 2.0.0-rc.70(vuepress@2.0.0-rc.19(@vuepress/bundler-vite@2.0.0-rc.19(@types/node@22.10.5)(sass-embedded@1.83.1))(vue@3.5.13)) + '@vuepress/plugin-search': 2.0.0-rc.70(vuepress@2.0.0-rc.19(@vuepress/bundler-vite@2.0.0-rc.19(@types/node@22.10.5)(sass-embedded@1.83.1))(vue@3.5.13)) + nodejs-jieba: 0.2.1(encoding@0.1.13) + sass-embedded: 1.83.1 + transitivePeerDependencies: + - '@vue/repl' + - '@waline/client' + - artalk + - artplayer + - chart.js + - dashjs + - echarts + - flowchart.ts + - hls.js + - katex + - kotlin-playground + - markdown-it + - markmap-lib + - markmap-toolbar + - markmap-view + - mathjax-full + - mermaid + - mpegts.js + - sandpack-vue3 + - twikoo + - typescript + - vidstack + + vuepress@2.0.0-rc.19(@vuepress/bundler-vite@2.0.0-rc.19(@types/node@22.10.5)(sass-embedded@1.83.1))(vue@3.5.13): + dependencies: + '@vuepress/cli': 2.0.0-rc.19 + '@vuepress/client': 2.0.0-rc.19 + '@vuepress/core': 2.0.0-rc.19 + '@vuepress/markdown': 2.0.0-rc.19 + '@vuepress/shared': 2.0.0-rc.19 + '@vuepress/utils': 2.0.0-rc.19 + vue: 3.5.13 + optionalDependencies: + '@vuepress/bundler-vite': 2.0.0-rc.19(@types/node@22.10.5)(sass-embedded@1.83.1) + transitivePeerDependencies: + - supports-color + - typescript - /workbox-strategies@6.5.4: - resolution: {integrity: sha512-DEtsxhx0LIYWkJBTQolRxG4EI0setTJkqR4m7r4YpBdxtWJH1Mbg01Cj8ZjNOO8etqfA3IZaOPHUxCs8cBsKLw==} + webidl-conversions@3.0.1: + optional: true + + whatwg-encoding@3.1.1: dependencies: - workbox-core: 6.5.4 - dev: false + iconv-lite: 0.6.3 + + whatwg-mimetype@4.0.0: {} - /workbox-streams@6.5.4: - resolution: {integrity: sha512-FXKVh87d2RFXkliAIheBojBELIPnWbQdyDvsH3t74Cwhg0fDheL1T8BqSM86hZvC0ZESLsznSYWw+Va+KVbUzg==} + whatwg-url@5.0.0: dependencies: - workbox-core: 6.5.4 - workbox-routing: 6.5.4 - dev: false + tr46: 0.0.3 + webidl-conversions: 3.0.1 + optional: true - /workbox-sw@6.5.4: - resolution: {integrity: sha512-vo2RQo7DILVRoH5LjGqw3nphavEjK4Qk+FenXeUsknKn14eCNedHOXWbmnvP4ipKhlE35pvJ4yl4YYf6YsJArA==} - dev: false + which-module@2.0.1: {} - /workbox-window@6.5.4: - resolution: {integrity: sha512-HnLZJDwYBE+hpG25AQBO8RUWBJRaCsI9ksQJEp3aCOFCaG5kqaToAYXFRAHxzRluM2cQbGzdQF5rjKPWPA1fug==} + which@2.0.2: dependencies: - '@types/trusted-types': 2.0.3 - workbox-core: 6.5.4 - dev: false + isexe: 2.0.0 - /wrap-ansi@6.2.0: - resolution: {integrity: sha512-r6lPcBGxZXlIcymEu7InxDMhdW0KDxpLgoFLcguasxCaJ/SOIZwINatK9KY/tf+ZrlywOKU0UDj3ATXUBfxJXA==} - engines: {node: '>=8'} + which@4.0.0: + dependencies: + isexe: 3.1.1 + optional: true + + wicked-good-xpath@1.3.0: {} + + wide-align@1.1.5: + dependencies: + string-width: 4.2.3 + optional: true + + wrap-ansi@6.2.0: dependencies: ansi-styles: 4.3.0 string-width: 4.2.3 strip-ansi: 6.0.1 - dev: false - /wrap-ansi@7.0.0: - resolution: {integrity: sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q==} - engines: {node: '>=10'} + wrap-ansi@7.0.0: dependencies: ansi-styles: 4.3.0 string-width: 4.2.3 strip-ansi: 6.0.1 - dev: false + optional: true - /wrap-ansi@8.1.0: - resolution: {integrity: sha512-si7QWI6zUMq56bESFvagtmzMdGOtoxfR+Sez11Mobfc7tm+VkUckk9bW2UeffTGVUbOksxmSw0AA2gs8g71NCQ==} - engines: {node: '>=12'} + wrap-ansi@8.1.0: dependencies: ansi-styles: 6.2.1 string-width: 5.1.2 - strip-ansi: 7.0.1 - dev: false + strip-ansi: 7.1.0 + optional: true - /wrappy@1.0.2: - resolution: {integrity: sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ==} - dev: false + wrappy@1.0.2: + optional: true - /xml-js@1.6.11: - resolution: {integrity: sha512-7rVi2KMfwfWFl+GpPg6m80IVMWXLRjO+PxTq7V2CDhoGak0wzYzFgUY2m4XJ47OGdXd8eLE8EmwfAmdjw7lC1g==} - hasBin: true + xml-js@1.6.11: dependencies: - sax: 1.2.4 - dev: false - - /xmldom-sre@0.1.31: - resolution: {integrity: sha512-f9s+fUkX04BxQf+7mMWAp5zk61pciie+fFLC9hX9UVvCeJQfNHRHXpeo5MPcR0EUf57PYLdt+ZO4f3Ipk2oZUw==} - engines: {node: '>=0.1'} - dev: false - - /y18n@4.0.3: - resolution: {integrity: sha512-JKhqTOwSrqNA1NY5lSztJ1GrBiUodLMmIZuLiDaMRJ+itFd+ABVE8XBjOvIWL+rSqNDC74LCSFmlb/U4UZ4hJQ==} - dev: false + sax: 1.4.1 - /yallist@3.1.1: - resolution: {integrity: sha512-a4UGQaWPH59mOXUYnAG2ewncQS4i4F43Tv3JoAM+s2VDAmS9NsK8GpDMLrCHPksFT7h3K6TOoUNn2pb7RoXx4g==} - dev: false + xmldom-sre@0.1.31: {} - /yallist@4.0.0: - resolution: {integrity: sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A==} - dev: false + y18n@4.0.3: {} - /yaml@2.2.2: - resolution: {integrity: sha512-CBKFWExMn46Foo4cldiChEzn7S7SRV+wqiluAb6xmueD/fGyRHIhX8m14vVGgeFWjN540nKCNVj6P21eQjgTuA==} - engines: {node: '>= 14'} - dev: false + yallist@4.0.0: + optional: true - /yargs-parser@18.1.3: - resolution: {integrity: sha512-o50j0JeToy/4K6OZcaQmW6lyXXKhq7csREXcDwk2omFPJEwUNOVtJKvmDr9EI1fAJZUyZcRF7kxGBWmRXudrCQ==} - engines: {node: '>=6'} + yargs-parser@18.1.3: dependencies: camelcase: 5.3.1 decamelize: 1.2.0 - dev: false - /yargs@15.4.1: - resolution: {integrity: sha512-aePbxDmcYW++PaqBsJ+HYUFwCdv4LVvdnhBy78E57PIor8/OVvhMrADFFEDh8DHDFRv/O9i3lPhsENjO7QX0+A==} - engines: {node: '>=8'} + yargs@15.4.1: dependencies: cliui: 6.0.0 decamelize: 1.2.0 @@ -6426,10 +5951,7 @@ packages: which-module: 2.0.1 y18n: 4.0.3 yargs-parser: 18.1.3 - dev: false - /zrender@5.4.3: - resolution: {integrity: sha512-DRUM4ZLnoaT0PBVvGBDO9oWIDBKFdAVieNWxWwK0niYzJCMwGchRk21/hsE+RKkIveH3XHCyvXcJDkgLVvfizQ==} - dependencies: - tslib: 2.3.0 - dev: false + yoctocolors@2.1.1: {} + + zwitch@2.0.4: {}