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 ba166cb77b7..242ea3b9602 100644
--- a/.gitignore
+++ b/.gitignore
@@ -10,10 +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
-pnpm-lock.yaml
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 14bb6fdb877..da7542cae15 100755
--- a/README.md
+++ b/README.md
@@ -1,23 +1,18 @@
推荐你通过在线阅读网站进行阅读,体验更好,速度更快!地址:[javaguide.cn](https://javaguide.cn/)。
-[ ](https://javaguide.cn/about-the-author/zhishixingqiu-two-years.html)
-
[](https://github.com/Snailclimb/JavaGuide)
-[](https://javaguide.cn/)
-
-
-
-
[GitHub](https://github.com/Snailclimb/JavaGuide) | [Gitee](https://gitee.com/SnailClimb/JavaGuide)
+
+
> - **面试专版**:准备 Java 面试的小伙伴可以考虑面试专版:**[《Java 面试指北 》](./docs/zhuanlan/java-mian-shi-zhi-bei.md)** (质量很高,专为面试打造,配合 JavaGuide 食用)。
-> - **知识星球**:专属面试小册/一对一交流/简历修改/专属求职指南,欢迎加入 **[JavaGuide 知识星球](./about-the-author/zhishixingqiu-two-years.md)**(点击链接即可查看星球的详细介绍,一定确定自己真的需要再加入)。
-> - **使用建议** :有水平的面试官都是顺着项目经历挖掘技术问题。一定不要死记硬背技术八股文!详细的学习建议请参考:[JavaGuide 使用建议](./javaguide/use-suggestion.md)。
+> - **知识星球**:专属面试小册/一对一交流/简历修改/专属求职指南,欢迎加入 **[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 原创,转载请在文首注明出处。如发现恶意抄袭/搬运,会动用法律武器维护自己的权益。让我们一起维护一个良好的技术创作环境!
@@ -93,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)
@@ -103,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)**
@@ -128,6 +125,8 @@ JVM 这部分内容主要参考 [JVM 虚拟机规范-Java8 ](https://docs.oracle
- [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)
## 计算机基础
@@ -183,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)
@@ -314,15 +313,13 @@ JVM 这部分内容主要参考 [JVM 虚拟机规范-Java8 ](https://docs.oracle
- [JWT 优缺点分析以及常见问题解决方案](./docs/system-design/security/advantages-and-disadvantages-of-jwt.md)
- [SSO 单点登录详解](./docs/system-design/security/sso-intro.md)
- [权限系统设计详解](./docs/system-design/security/design-of-authority-system.md)
-- [常见加密算法总结](./docs/system-design/security/encryption-algorithms.md)
-
-#### 数据脱敏
-数据脱敏说的就是我们根据特定的规则对敏感信息数据进行变形,比如我们把手机号、身份证号某些位数使用 \* 来代替。
+#### 数据安全
-#### 敏感词过滤
-
-[敏感词过滤方案总结](./docs/system-design/security/sentive-words-filter.md)
+- [常见加密算法总结](./docs/system-design/security/encryption-algorithms.md)
+- [敏感词过滤方案总结](./docs/system-design/security/sentive-words-filter.md)
+- [数据脱敏方案总结](./docs/system-design/security/data-desensitization.md)
+- [为什么前后端都要做数据校验](./docs/system-design/security/data-validation.md)
### 定时任务
@@ -378,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 b8b2364da30..eed17cf0e1d 100644
--- a/docs/.vuepress/config.ts
+++ b/docs/.vuepress/config.ts
@@ -1,13 +1,13 @@
+import { viteBundler } from "@vuepress/bundler-vite";
import { defineUserConfig } from "vuepress";
-import { searchPlugin } from "@vuepress/plugin-search";
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: [
@@ -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" }],
// 添加百度统计
[
@@ -46,41 +54,12 @@ export default defineUserConfig({
],
],
- theme,
+ bundler: viteBundler(),
- plugins: [
- searchPlugin({
- // https://v2.vuepress.vuejs.org/zh/reference/plugin/search.html
- // 排除首页
- isSearchable: (page) => page.path !== "/",
- maxSuggestions: 10,
- hotKeys: ["s", "/"],
- // 用于在页面的搜索索引中添加额外字段
- getExtraFields: () => [],
- locales: {
- "/": {
- placeholder: "搜索",
- },
- },
- }),
- // searchProPlugin({
- // indexContent: true,
- // indexOptions: {
- // tokenize: (text, fieldName) =>
- // fieldName === "id" ? [text] : cut(text, true),
- // },
- // customFields: [
- // {
- // getter: ({ frontmatter }) =>
- // frontmatter.category ?? null,
- // formatter: "分类: $content",
- // },
- // ],
- // suggestDelay: 60,
- // }),
- ],
+ theme,
pagePatterns: ["**/*.md", "!**/*.snippet.md", "!.vuepress", "!node_modules"],
shouldPrefetch: false,
+ shouldPreload: false,
});
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 69f09754717..8da4200b7e1 100644
--- a/docs/.vuepress/sidebar/high-quality-technical-articles.ts
+++ b/docs/.vuepress/sidebar/high-quality-technical-articles.ts
@@ -10,6 +10,7 @@ export const highQualityTechnicalArticles = arraySidebar([
"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",
@@ -33,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 7ffbbf2ce21..6a3c73769d4 100644
--- a/docs/.vuepress/sidebar/index.ts
+++ b/docs/.vuepress/sidebar/index.ts
@@ -20,14 +20,14 @@ export default sidebar({
// 必须放在最后面
"/": [
{
- text: "必看",
+ text: "项目介绍",
icon: "star",
collapsible: true,
prefix: "javaguide/",
children: ["intro", "use-suggestion", "contribution-guideline", "faq"],
},
{
- text: "面试准备",
+ text: "面试准备(必看)",
icon: "interview",
collapsible: true,
prefix: "interview-preparation/",
@@ -35,9 +35,10 @@ export default sidebar({
"teach-you-how-to-prepare-for-the-interview-hand-in-hand",
"resume-guide",
"key-points-of-interview",
+ "java-roadmap",
"project-experience-guide",
- "interview-experience",
- "self-test-of-common-interview-questions",
+ "how-to-handle-interview-nerves",
+ "internship-experience",
],
},
{
@@ -112,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",
@@ -167,6 +169,8 @@ export default sidebar({
"java19",
"java20",
"java21",
+ "java22-23",
+ "java24",
],
},
],
@@ -184,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",
@@ -321,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",
@@ -409,6 +416,7 @@ export default sidebar({
"spring-transaction",
"spring-design-patterns-summary",
"spring-boot-auto-assembly-principles",
+ "async",
],
},
],
@@ -424,7 +432,7 @@ export default sidebar({
collapsible: true,
children: [
{
- text: "基础",
+ text: "基础知识",
prefix: "basis/",
icon: "basic",
collapsible: true,
@@ -440,7 +448,7 @@ export default sidebar({
],
},
{
- text: "安全",
+ text: "认证授权",
prefix: "security/",
icon: "security-fill",
collapsible: true,
@@ -450,9 +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",
@@ -541,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",
],
},
{
@@ -566,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 6c42937c58e..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";
@@ -11,8 +11,6 @@ export default hopeTheme({
logo: "/logo.png",
favicon: "/favicon.ico",
- iconAssets: "//at.alicdn.com/t/c/font_2922463_kweia6fbo9.css",
-
author: {
name: "Guide",
url: "https://javaguide.cn/article/",
@@ -20,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,
@@ -29,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/",
@@ -49,6 +39,25 @@ 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,
@@ -67,25 +76,13 @@ export default hopeTheme({
rss: true,
},
- mdEnhance: {
- align: true,
- codetabs: true,
- container: true,
- figure: true,
- gfm: true,
- include: {
- resolvePath: (file, cwd) => {
- if (file.startsWith("@"))
- return path.resolve(
- __dirname,
- "../snippets",
- file.replace("@", "./"),
- );
+ icon: {
+ assets: "//at.alicdn.com/t/c/font_2922463_o9q9dxmps9.css",
+ },
- return path.resolve(cwd, file);
- },
- },
- tasklist: true,
+ search: {
+ isSearchable: (page) => page.path !== "/",
+ maxSuggestions: 10,
},
},
});
diff --git a/docs/readme.md b/docs/README.md
similarity index 62%
rename from docs/readme.md
rename to docs/README.md
index 115bddd4f8a..e8102879f5d 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,13 +16,11 @@ footer: |-
鄂ICP备2020015769号-1 | 主题: VuePress Theme Hope
---
-[](./about-the-author/zhishixingqiu-two-years.md)
-
## 关于网站
-JavaGuide 已经持续维护 5 年多了,累计提交了 **5000+** commit ,共有 **440** 多位朋友参与维护。真心希望能够把这个项目做好,真正能够帮助到有需要的朋友!
+JavaGuide 已经持续维护 6 年多了,累计提交了接近 **6000** commit ,共有 **570+** 多位贡献者共同参与维护和完善。真心希望能够把这个项目做好,真正能够帮助到有需要的朋友!
-如果觉得 JavaGuide 的内容对你有帮助的话,还请点个免费的 Star(绝不强制点 Star,你觉得内容不错再点赞就好),这是对我最大的鼓励,感谢各位一起同行,共勉!传送门:[GitHub](https://github.com/Snailclimb/JavaGuide) | [Gitee](https://gitee.com/SnailClimb/JavaGuide)。
+如果觉得 JavaGuide 的内容对你有帮助的话,还请点个免费的 Star(绝不强制点 Star,觉得内容不错有收获再点赞就好),这是对我最大的鼓励,感谢各位一路同行,共勉!传送门:[GitHub](https://github.com/Snailclimb/JavaGuide) | [Gitee](https://gitee.com/SnailClimb/JavaGuide)。
- [项目介绍](./javaguide/intro.md)
- [贡献指南](./javaguide/contribution-guideline.md)
diff --git a/docs/about-the-author/readme.md b/docs/about-the-author/README.md
similarity index 96%
rename from docs/about-the-author/readme.md
rename to docs/about-the-author/README.md
index 032f1ed56f4..12f6eab7f3f 100644
--- a/docs/about-the-author/readme.md
+++ b/docs/about-the-author/README.md
@@ -45,9 +45,11 @@ category: 走近作者
如果你也想通过接私活变现的话,可以在我的公众号后台回复“**接私活**”来了解一些我的个人经验分享。
-
-
-
+::: center
+
+
+
+:::
## 为什么自称 Guide?
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..0146d71c4a3
--- /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 后端开发的大门。
+
+
+
+这本书一共 **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/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:
最开始接触电脑是在我刚上五年级的时候,那时候家里没电脑,刚开始上网都是在黑网吧玩的。
-在黑网吧上网的经历也是一波三折,经常会遇到警察来检查或者碰到大孩子骚扰。在黑网吧上网的一年多中,我一共两次碰到警察来检查,主要是看有没有未成年人(当时黑网吧里几乎全是未成年人),实际感觉像是要问黑网吧老板要点好处。碰到大孩子骚扰的次数就比较多,大孩子经常抢我电脑,还威胁我把身上所有的钱给他们。我当时一个人也比较怂,被打了几次之后,就尽量避开大孩子来玩的时间去黑网吧,身上也只带很少的钱。小时候的性格就比较独立,在外遇到事情我一般也不会给家里人说。
+黑网吧大概就是下面这样式儿的,一个没有窗户的房间里放了很多台老式电脑,非常拥挤。
+
+
+
+在黑网吧上网的经历也是一波三折,经常会遇到警察来检查或者碰到大孩子骚扰。在黑网吧上网的一年多中,我一共两次碰到警察来检查,主要是看有没有未成年人(当时黑网吧里几乎全是未成年人),实际感觉像是要问黑网吧老板要点好处。碰到大孩子骚扰的次数就比较多,大孩子经常抢我电脑,还威胁我把身上所有的钱给他们。我当时一个人也比较怂,被打了几次之后,就尽量避开大孩子来玩的时间去黑网吧,身上也只带很少的钱。小时候的性格就比较独立,在外遇到事情我一般也不会给家里人说(因为说了也没什么用,家人给我的安全感很少)。
我现在已经记不太清当时是被我哥还是我姐带进网吧的,好像是我姐。
@@ -57,6 +61,10 @@ QQ 飞车这款戏当时还挺火的,很多 90 后的小伙伴应该比较熟
我的最终军衔停留在了两个钻石,玩过的小伙伴应该清楚这在当时要玩多少把(现在升级比较简单)。
+
+
+ps: 回坑 CF 快一年了,目前的军衔是到了两颗星中校 3 了。
+
那时候成绩挺差的。这样说吧!我当时在很普通的一个县级市的高中,全年级有 500 来人,我基本都是在 280 名左右。而且,整个初二我都没有学物理,上物理课就睡觉,考试就交白卷。
为什么对物理这么抵触呢?这是因为开学不久的一次物理课,物理老师误会我在上课吃东西还狡辩,扇了我一巴掌。那时候心里一直记仇到大学,想着以后自己早晚有时间把这个物理老师暴打一顿。
@@ -81,6 +89,8 @@ QQ 飞车这款戏当时还挺火的,很多 90 后的小伙伴应该比较熟
## 高中从小班掉到平行班
+
+
由于参加了高中提前招生考试,我提前 4 个月就来到了高中,进入了小班,开始学习高中的课程。
上了高中的之后,我上课就偷偷看小说,神印王座、斗罗大陆、斗破苍穹很多小说都是当时看的。中午和晚上回家之后,就在家里玩几把 DNF。当时家里也买了电脑,姥爷给买的,是对自己顺利进入二中的奖励。到我卸载 DNF 的时候,已经练了 4 个满级的号,两个接近满级的号。
@@ -117,9 +127,11 @@ QQ 飞车这款戏当时还挺火的,很多 90 后的小伙伴应该比较熟

-高考那几天的失眠,我觉得可能和我喝了老师推荐的安神补脑液有关系,又或者是我自己太过于紧张了。因为那几天睡觉总会感觉有很多蚂蚁在身上爬一样,身上还起了一些小痘痘。
+高考那几天的失眠,我觉得可能和我喝了老师推荐的安神补脑液有关系,又或者是我自己太过于紧张了。因为那几天睡觉总会感觉有很多蚂蚁在身上爬一样,身上还起了一些小痘痘(有点像是过敏)。
+
+这里要格外说明一点,避免引起误导:**睡不着本身就是自身的问题,上述言论并没有责怪这个补脑液的意思。** 另外, 这款安神补脑液我去各个平台都查了一下,发现大家对他的评价都挺好,和我们老师当时推荐的理由差不多。如果大家需要改善睡眠的话,可以咨询相关医生之后尝试一下。
-然后,这里要格外说明一点,避免引起误导:**睡不着本身就是自身的问题,上述言论并没有责怪这个补脑液的意思。** 另外, 这款安神补脑液我去各个平台都查了一下,发现大家对他的评价都挺好,和我们老师当时推荐的理由差不多。如果大家需要改善睡眠的话,可以咨询相关医生之后尝试一下。
+高考也确实没发挥好,整个人在考场都是懵的状态。高考成绩出来之后,比我自己预估的还低了几十分,最后只上了一个双非一本。不过,好在专业选的好,吃了一些计算机专业的红利,大学期间也挺努力的。
## 大学生活
diff --git a/docs/about-the-author/zhishixingqiu-two-years.md b/docs/about-the-author/zhishixingqiu-two-years.md
index c3bdc72609b..644478b455a 100644
--- a/docs/about-the-author/zhishixingqiu-two-years.md
+++ b/docs/about-the-author/zhishixingqiu-two-years.md
@@ -1,20 +1,18 @@
---
-title: 我的知识星球快 3 岁了!
+title: 我的知识星球 4 岁了!
category: 知识星球
star: 2
---
-截止到今天,我认真维护了接近四年的星球已经有 **2.3w+** 的同学加入了,也算是达成了自己当初的目标。虽然比不上很多大佬,但这于我来说也算是小有成就了,真的很满足了!我确信自己是一个普通人,能做成这些,也不过是在兴趣和运气的加持下赶上了时代而已。
-
-
-
-在 **2019 年 12 月 29 号**,经过了大概一年左右的犹豫期,我正式确定要开始做一个自己的星球,帮助学习 Java 和准备 Java 面试的同学。一转眼,马上快要四年了。感谢大家一路陪伴,我会信守承诺,继续认真维护这个纯粹的 Java 知识星球,不让信任我的读者失望。
+在 **2019 年 12 月 29 号**,经过了大概一年左右的犹豫期,我正式确定要开始做一个自己的星球,帮助学习 Java 和准备 Java 面试的同学。一转眼,已经四年多了。感谢大家一路陪伴,我会信守承诺,继续认真维护这个纯粹的 Java 知识星球,不让信任我的读者失望。

-我应该是最早一批做星球的技术号主,最开始的一两年,纯粹靠爱发电。当初定价非常低(一顿饭钱),加上刚工作的时候比较忙,提供的服务也没有现在这么多。
+我是比较早一批做星球的技术号主,也是坚持做下来的那一少部人(大部分博主割一波韭菜就不维护星球了)。最开始的一两年,纯粹靠爱发电。当初定价非常低(一顿饭钱),加上刚工作的时候比较忙,提供的服务也没有现在这么多。
+
+慢慢的价格提上来,星球的收入确实慢慢也上来了。不过,考虑到我的受众主要是学生,定价依然比同类星球低很多。另外,我也没有弄训练营的打算,虽然训练营对于我这个流量来说可以赚到更多钱。
**我有自己的原则,不割韭菜,用心做内容,真心希望帮助到他人!**
@@ -30,7 +28,7 @@ star: 2
1. 6 个高质量的专栏永久阅读,内容涵盖面试,源码解析,项目实战等内容!
2. 多本原创 PDF 版本面试手册免费领取。
-3. 免费的简历修改服务(已经累计帮助 5000+ 位球友修改简历)。
+3. 免费的简历修改服务(已经累计帮助 7000+ 位球友修改简历)。
4. 一对一免费提问交流(专属建议,走心回答)。
5. 专属求职指南和建议,让你少走弯路,效率翻倍!
6. 海量 Java 优质面试资源分享。
@@ -58,7 +56,7 @@ star: 2
### PDF 面试手册
-免费赠送多本优质 PDF 面试手册。
+进入星球就免费赠送多本优质 PDF 面试手册。

@@ -84,7 +82,7 @@ JavaGuide 知识星球优质主题汇总传送门: 本文转自:,JavaGuide 对其做了补充完善。
+
+
## 引言
所谓排序,就是使一串记录,按照其中的某个或某些关键字的大小,递增或递减的排列起来的操作。排序算法,就是如何使得记录按照要求排列的方法。排序算法在很多领域得到相当地重视,尤其是在大量数据的处理方面。一个优秀的算法可以节省大量的资源。在各个领域中考虑到数据的各种限制和规范,要得到一个符合实际的优秀算法,得经过大量的推理和分析。
-两年前,我曾在[博客园](https://www.cnblogs.com/guoyaohua/)发布过一篇[《十大经典排序算法最强总结(含 JAVA 代码实现)》](https://www.cnblogs.com/guoyaohua/p/8600214.html)博文,简要介绍了比较经典的十大排序算法,不过在之前的博文中,仅给出了 Java 版本的代码实现,并且有一些细节上的错误。所以,今天重新写一篇文章,深入了解下十大经典排序算法的原理及实现。
-
## 简介
-排序算法可以分为:
-
-- **内部排序**:数据记录在内存中进行排序。
-- **[外部排序](https://zh.wikipedia.org/wiki/外排序)**:因排序的数据很大,一次不能容纳全部的排序记录,在排序过程中需要访问外存。
-
-常见的内部排序算法有:**插入排序**、**希尔排序**、**选择排序**、**冒泡排序**、**归并排序**、**快速排序**、**堆排序**、**基数排序**等,本文只讲解内部排序算法。用一张图概括:
-
-
-
-上图存在错误:
-
-1. 插入排序的最好时间复杂度为 $O(n)$ 而不是 $O(n^2)$。
-2. 希尔排序的平均时间复杂度为 $O(nlogn)$。
-
-**图片名词解释:**
-
-- **n**:数据规模
-- **k**:“桶” 的个数
-- **In-place**:占用常数内存,不占用额外内存
-- **Out-place**:占用额外内存
-
-### 术语说明
-
+### 排序算法总结
+
+常见的内部排序算法有:**插入排序**、**希尔排序**、**选择排序**、**冒泡排序**、**归并排序**、**快速排序**、**堆排序**、**基数排序**等,本文只讲解内部排序算法。用一张表格概括:
+
+| 排序算法 | 时间复杂度(平均) | 时间复杂度(最差) | 时间复杂度(最好) | 空间复杂度 | 排序方式 | 稳定性 |
+| -------- | ------------------ | ------------------ | ------------------ | ---------- | -------- | ------ |
+| 冒泡排序 | 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 的后面。
-- **内排序**:所有排序操作都在内存中完成。
-- **外排序**:由于数据太大,因此把数据放在磁盘中,而排序通过磁盘和内存的数据传输才能进行。
- **时间复杂度**:定性描述一个算法执行所耗费的时间。
- **空间复杂度**:定性描述一个算法执行所需内存的大小。
-### 算法分类
+### 排序算法分类
十种常见排序算法可以分类两大类别:**比较类排序**和**非比较类排序**。
@@ -369,31 +367,60 @@ public static int[] merge(int[] arr_1, int[] arr_2) {
### 代码实现
-> 来源:[使用 Java 实现快速排序(详解)](https://segmentfault.com/a/1190000040022056)
-
```java
-public static int partition(int[] array, int low, int high) {
- int pivot = array[high];
- int pointer = low;
- for (int i = low; i < high; i++) {
- if (array[i] <= pivot) {
- int temp = array[i];
- array[i] = array[pointer];
- array[pointer] = temp;
- pointer++;
+import java.util.concurrent.ThreadLocalRandom;
+
+class Solution {
+ public int[] sortArray(int[] a) {
+ quick(a, 0, a.length - 1);
+ return a;
+ }
+
+ // 快速排序的核心递归函数
+ void quick(int[] a, int left, int right) {
+ if (left >= right) { // 递归终止条件:区间只有一个或没有元素
+ return;
}
- System.out.println(Arrays.toString(array));
+ int p = partition(a, left, right); // 分区操作,返回分区点索引
+ quick(a, left, p - 1); // 对左侧子数组递归排序
+ quick(a, p + 1, right); // 对右侧子数组递归排序
}
- int temp = array[pointer];
- array[pointer] = array[high];
- array[high] = temp;
- return pointer;
-}
-public static void quickSort(int[] array, int low, int high) {
- if (low < high) {
- int position = partition(array, low, high);
- quickSort(array, low, position - 1);
- quickSort(array, position + 1, high);
+
+ // 分区函数:将数组分为两部分,小于基准值的在左,大于基准值的在右
+ int partition(int[] a, int left, int right) {
+ // 随机选择一个基准点,避免最坏情况(如数组接近有序)
+ int idx = ThreadLocalRandom.current().nextInt(right - left + 1) + left;
+ swap(a, left, idx); // 将基准点放在数组的最左端
+ int pv = a[left]; // 基准值
+ int i = left + 1; // 左指针,指向当前需要检查的元素
+ int j = right; // 右指针,从右往左寻找比基准值小的元素
+
+ while (i <= j) {
+ // 左指针向右移动,直到找到一个大于等于基准值的元素
+ while (i <= j && a[i] < pv) {
+ i++;
+ }
+ // 右指针向左移动,直到找到一个小于等于基准值的元素
+ while (i <= j && a[j] > pv) {
+ j--;
+ }
+ // 如果左指针尚未越过右指针,交换两个不符合位置的元素
+ if (i <= j) {
+ swap(a, i, j);
+ i++;
+ j--;
+ }
+ }
+ // 将基准值放到分区点位置,使得基准值左侧小于它,右侧大于它
+ swap(a, j, left);
+ return j;
+ }
+
+ // 交换数组中两个元素的位置
+ void swap(int[] a, int i, int j) {
+ int t = a[i];
+ a[i] = a[j];
+ a[j] = t;
}
}
```
@@ -401,7 +428,7 @@ public static void quickSort(int[] array, int low, int high) {
### 算法分析
- **稳定性**:不稳定
-- **时间复杂度**:最佳:$O(nlogn)$, 最差:$O(nlogn)$,平均:$O(nlogn)$
+- **时间复杂度**:最佳:$O(nlogn)$, 最差:$O(n^2)$,平均:$O(nlogn)$
- **空间复杂度**:$O(logn)$
## 堆排序 (Heap Sort)
@@ -565,13 +592,13 @@ public static int[] countingSort(int[] arr) {
}
```
-## 算法分析
+### 算法分析
当输入的元素是 `n` 个 `0` 到 `k` 之间的整数时,它的运行时间是 $O(n+k)$。计数排序不是比较排序,排序的速度快于任何比较排序算法。由于用来计数的数组 `C` 的长度取决于待排序数组中数据的范围(等于待排序数组的**最大值与最小值的差加上 1**),这使得计数排序对于数据范围很大的数组,需要大量额外内存空间。
- **稳定性**:稳定
- **时间复杂度**:最佳:$O(n+k)$ 最差:$O(n+k)$ 平均:$O(n+k)$
-- **空间复杂度**:`O(k)`
+- **空间复杂度**:$O(k)$
## 桶排序 (Bucket Sort)
diff --git a/docs/cs-basics/algorithms/classical-algorithm-problems-recommendations.md b/docs/cs-basics/algorithms/classical-algorithm-problems-recommendations.md
index 79c28cf599b..3a6a01a210f 100644
--- a/docs/cs-basics/algorithms/classical-algorithm-problems-recommendations.md
+++ b/docs/cs-basics/algorithms/classical-algorithm-problems-recommendations.md
@@ -20,15 +20,15 @@ tag:
### LeetCode
-455.分发饼干:https://leetcode.cn/problems/assign-cookies/
+455.分发饼干:
-121.买卖股票的最佳时机:https://leetcode.cn/problems/best-time-to-buy-and-sell-stock/
+121.买卖股票的最佳时机:
-122.买卖股票的最佳时机 II:https://leetcode.cn/problems/best-time-to-buy-and-sell-stock-ii/
+122.买卖股票的最佳时机 II:
-55.跳跃游戏:https://leetcode.cn/problems/jump-game/
+55.跳跃游戏:
-45.跳跃游戏 II:https://leetcode.cn/problems/jump-game-ii/
+45.跳跃游戏 II:
## 动态规划
@@ -48,17 +48,17 @@ tag:
### LeetCode
-509.斐波那契数:https://leetcode.cn/problems/fibonacci-number/
+509.斐波那契数:
-746.使用最小花费爬楼梯:https://leetcode.cn/problems/min-cost-climbing-stairs/
+746.使用最小花费爬楼梯:
-416.分割等和子集:https://leetcode.cn/problems/partition-equal-subset-sum/
+416.分割等和子集:
-518.零钱兑换:https://leetcode.cn/problems/coin-change-ii/
+518.零钱兑换:
-647.回文子串:https://leetcode.cn/problems/palindromic-substrings/
+647.回文子串:
-516.最长回文子序列:https://leetcode.cn/problems/longest-palindromic-subsequence/
+516.最长回文子序列:
## 回溯算法
@@ -78,17 +78,17 @@ tag:
### leetcode
-77.组合:https://leetcode.cn/problems/combinations/
+77.组合:
-39.组合总和:https://leetcode.cn/problems/combination-sum/
+39.组合总和:
-40.组合总和 II:https://leetcode.cn/problems/combination-sum-ii/
+40.组合总和 II:
-78.子集:https://leetcode.cn/problems/subsets/
+78.子集:
-90.子集 II:https://leetcode.cn/problems/subsets-ii/
+90.子集 II:
-51.N 皇后:https://leetcode.cn/problems/n-queens/
+51.N 皇后:
## 分治算法
@@ -106,9 +106,8 @@ tag:
### LeetCode
-108.将有序数组转换成二叉搜索数:https://leetcode.cn/problems/convert-sorted-array-to-binary-search-tree/
+108.将有序数组转换成二叉搜索数:
-148.排序列表:https://leetcode.cn/problems/sort-list/
-
-23.合并 k 个升序链表:https://leetcode.cn/problems/merge-k-sorted-lists/
+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
index 8510c112454..51d9225730f 100644
--- a/docs/cs-basics/algorithms/common-data-structures-leetcode-recommendations.md
+++ b/docs/cs-basics/algorithms/common-data-structures-leetcode-recommendations.md
@@ -7,61 +7,58 @@ tag:
## 数组
-704.二分查找:https://leetcode.cn/problems/binary-search/
+704.二分查找:
-80.删除有序数组中的重复项 II:https://leetcode.cn/problems/remove-duplicates-from-sorted-array-ii
+80.删除有序数组中的重复项 II:
-977.有序数组的平方:https://leetcode.cn/problems/squares-of-a-sorted-array/
+977.有序数组的平方:
## 链表
-707.设计链表:https://leetcode.cn/problems/design-linked-list/
+707.设计链表:
-206.反转链表:https://leetcode.cn/problems/reverse-linked-list/
+206.反转链表:
-92.反转链表 II:https://leetcode.cn/problems/reverse-linked-list-ii/
+92.反转链表 II:
-61.旋转链表:https://leetcode.cn/problems/rotate-list/
+61.旋转链表:
## 栈与队列
-232.用栈实现队列:https://leetcode.cn/problems/implement-queue-using-stacks/
+232.用栈实现队列:
-225.用队列实现栈:https://leetcode.cn/problems/implement-stack-using-queues/
+225.用队列实现栈:
-347.前 K 个高频元素:https://leetcode.cn/problems/top-k-frequent-elements/
+347.前 K 个高频元素:
-239.滑动窗口最大值:https://leetcode.cn/problems/sliding-window-maximum/
+239.滑动窗口最大值:
## 二叉树
-105.从前序与中序遍历构造二叉树:https://leetcode.cn/problems/construct-binary-tree-from-preorder-and-inorder-traversal/
+105.从前序与中序遍历构造二叉树:
-117.填充每个节点的下一个右侧节点指针 II:https://leetcode.cn/problems/populating-next-right-pointers-in-each-node-ii
+117.填充每个节点的下一个右侧节点指针 II:
-236.二叉树的最近公共祖先:https://leetcode.cn/problems/lowest-common-ancestor-of-a-binary-tree/
+236.二叉树的最近公共祖先:
-129.求根节点到叶节点数字之和:https://leetcode.cn/problems/sum-root-to-leaf-numbers/
+129.求根节点到叶节点数字之和:
-102.二叉树的层序遍历:https://leetcode.cn/problems/binary-tree-level-order-traversal/
+102.二叉树的层序遍历:
-530.二叉搜索树的最小绝对差:https://leetcode.cn/problems/minimum-absolute-difference-in-bst/
+530.二叉搜索树的最小绝对差:
## 图
-200.岛屿数量:https://leetcode.cn/problems/number-of-islands/
+200.岛屿数量:
-207.课程表:https://leetcode.cn/problems/course-schedule/
+207.课程表:
-210.课程表 II:https://leetcode.cn/problems/course-schedule-ii/
+210.课程表 II:
## 堆
-215. 数组中的第 K 个最大元素:https://leetcode.cn/problems/kth-largest-element-in-an-array/
-
-215. 数据流的中位数:https://leetcode.cn/problems/find-median-from-data-stream/
-
-215. 前 K 个高频元素:https://leetcode.cn/problems/top-k-frequent-elements/
-
+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 280d4a064ef..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. 两数相加
### 题目描述
@@ -25,7 +27,7 @@ tag:
Leetcode 官方详细解答地址:
-https://leetcode-cn.com/problems/add-two-numbers/solution/
+
> 要对头结点进行操作时,考虑创建哑节点 dummy,使用 dummy->next 表示真正的头节点。这样可以避免处理头节点为空的边界问题。
diff --git a/docs/cs-basics/algorithms/string-algorithm-problems.md b/docs/cs-basics/algorithms/string-algorithm-problems.md
index a8d23bae670..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. 替换空格
@@ -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,8 +158,8 @@ 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:
@@ -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,14 +357,14 @@ 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
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 0e7b32c1b1a..73d296d0dc3 100644
--- a/docs/cs-basics/algorithms/the-sword-refers-to-offer.md
+++ b/docs/cs-basics/algorithms/the-sword-refers-to-offer.md
@@ -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)
@@ -622,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,所以我们继续压栈,直到相等以后开始出栈,出栈一个元素,则将出栈顺序向后移动一位,直到不相等,这样循环等压栈顺序遍历完成,如果辅助栈还不为空,说明弹出序列不是该栈的弹出顺序。
diff --git a/docs/cs-basics/data-structure/bloom-filter.md b/docs/cs-basics/data-structure/bloom-filter.md
index 06662ed2e29..be17c1a53aa 100644
--- a/docs/cs-basics/data-structure/bloom-filter.md
+++ b/docs/cs-basics/data-structure/bloom-filter.md
@@ -148,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)));
}
}
@@ -243,20 +243,20 @@ 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** 然后在排除广告的第一条搜素结果就找到了我们想要的答案(这是我平常解决问题的一种方式,分享一下),具体地址: (介绍的很详细 )。
**具体操作如下:**
@@ -273,10 +273,10 @@ root@21396d02c252:/data# redis-cli
> 注意: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` 命令需要单独介绍一下:
diff --git a/docs/cs-basics/data-structure/linear-data-structure.md b/docs/cs-basics/data-structure/linear-data-structure.md
index 154fb50e189..e8ae63a19d5 100644
--- a/docs/cs-basics/data-structure/linear-data-structure.md
+++ b/docs/cs-basics/data-structure/linear-data-structure.md
@@ -99,7 +99,7 @@ tag:

-### 3.2. 栈的常见应用常见应用场景
+### 3.2. 栈的常见应用场景
当我们我们要处理的数据只涉及在一端插入和删除数据,并且满足 **后进先出(LIFO, Last In First Out)** 的特性时,我们就可以使用栈这个数据结构。
@@ -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. 栈的实现
@@ -316,13 +321,14 @@ myStack.pop();//报错:java.lang.IllegalArgumentException: Stack is empty.
虽然优先队列的底层并非严格的线性结构,但是在我们使用的过程中,我们是感知不到**堆**的,从使用者的眼中优先队列可以被认为是一种线性的数据结构:一种会自动排序的线性队列。
-### 4.3. 常见应用场景
+### 4.3. 队列的常见应用场景
当我们需要按照一定顺序来处理数据的时候可以考虑使用队列这个数据结构。
- **阻塞队列:** 阻塞队列可以看成在队列基础上加了阻塞操作的队列。当队列为空的时候,出队操作阻塞,当队列满的时候,入队操作阻塞。使用阻塞队列我们可以很容易实现“生产者 - 消费者“模型。
-- **线程池中的请求/任务队列:** 线程池中没有空闲线程时,新的任务请求线程资源时,线程池该如何处理呢?答案是将这些请求放在队列中,当有空闲线程的时候,会循环中反复从队列中获取任务来执行。队列分为无界队列(基于链表)和有界队列(基于数组)。无界队列的特点就是可以一直入列,除非系统资源耗尽,比如:`FixedThreadPool` 使用无界队列 `LinkedBlockingQueue`。但是有界队列就不一样了,当队列满的话后面再有任务/请求就会拒绝,在 Java 中的体现就是会抛出`java.util.concurrent.RejectedExecutionException` 异常。
-- 栈:双端队列天生便可以实现栈的全部功能(`push`、`pop` 和 `peek`),并且在 Deque 接口中已经实现了相关方法。Stack 类已经和 Vector 一样被遗弃,现在在 Java 中普遍使用双端队列(Deque)来实现栈。
+- **线程池中的请求/任务队列:** 当线程池中没有空闲线程时,新的任务请求线程资源会被如何处理呢?答案是这些任务会被放入任务队列中,等待线程池中的线程空闲后再从队列中取出任务执行。任务队列分为无界队列(基于链表实现)和有界队列(基于数组实现)。无界队列的特点是队列容量理论上没有限制,任务可以持续入队,直到系统资源耗尽。例如:`FixedThreadPool` 使用的阻塞队列 `LinkedBlockingQueue`,其默认容量为 `Integer.MAX_VALUE`,因此可以被视为“无界队列”。而有界队列则不同,当队列已满时,如果再有新任务提交,由于队列无法继续容纳任务,线程池会拒绝这些任务,并抛出 `java.util.concurrent.RejectedExecutionException` 异常。
+- **栈**:双端队列天生便可以实现栈的全部功能(`push`、`pop` 和 `peek`),并且在 Deque 接口中已经实现了相关方法。Stack 类已经和 Vector 一样被遗弃,现在在 Java 中普遍使用双端队列(Deque)来实现栈。
+- **广度优先搜索(BFS)**:在图的广度优先搜索过程中,队列被用于存储待访问的节点,保证按照层次顺序遍历图的节点。
- Linux 内核进程队列(按优先级排队)
- 现实生活中的派对,播放器上的播放列表;
- 消息队列
diff --git a/docs/cs-basics/data-structure/red-black-tree.md b/docs/cs-basics/data-structure/red-black-tree.md
index 113607c6c15..462010e910e 100644
--- a/docs/cs-basics/data-structure/red-black-tree.md
+++ b/docs/cs-basics/data-structure/red-black-tree.md
@@ -21,17 +21,16 @@ tag:
红黑树的诞生就是为了解决二叉查找树的缺陷,因为二叉查找树在某些情况下会退化成一个线性结构。
-## **红黑树特点**
+## **红黑树特点**
1. 每个节点非红即黑。黑色决定平衡,红色不决定平衡。这对应了 2-3 树中一个节点内可以存放 1~2 个节点。
2. 根节点总是黑色的。
3. 每个叶子节点都是黑色的空节点(NIL 节点)。这里指的是红黑树都会有一个空的叶子节点,是红黑树自己的规则。
-4. 如果节点是红色的,则它的子节点必须是黑色的(反之不一定)。通常这条规则也叫不会有连续的红色节点。一个节点最多临时会有 3 个节点,中间是黑色节点,左右是红色节点。
-5. 从根节点到叶节点或空子节点的每条路径,必须包含相同数目的黑色节点(即相同的黑色高度)。每一层都只是有一个节点贡献了树高决定平衡性,也就是对应红黑树中的黑色节点。
+4. 如果节点是红色的,则它的子节点必须是黑色的(反之不一定)。通常这条规则也叫不会有连续的红色节点。一个节点最多临时会有 3 个子节点,中间是黑色节点,左右是红色节点。
+5. 从任意节点到它的叶子节点或空子节点的每条路径,必须包含相同数目的黑色节点(即相同的黑色高度)。每一层都只是有一个节点贡献了树高决定平衡性,也就是对应红黑树中的黑色节点。
正是这些特点才保证了红黑树的平衡,让红黑树的高度不会超过 2log(n+1)。
-
## 红黑树数据结构
建立在 BST 二叉搜索树的基础上,AVL、2-3 树、红黑树都是自平衡二叉树(统称 B-树)。但相比于 AVL 树,高度平衡所带来的时间复杂度,红黑树对平衡的控制要宽松一些,红黑树只需要保证黑色节点平衡即可。
@@ -51,7 +50,7 @@ public class Node {
public int height;
// 红黑树所需属性
public Color color = Color.RED;
-
+
}
```
diff --git a/docs/cs-basics/data-structure/tree.md b/docs/cs-basics/data-structure/tree.md
index 9727359c114..de9c6eb6a27 100644
--- a/docs/cs-basics/data-structure/tree.md
+++ b/docs/cs-basics/data-structure/tree.md
@@ -50,7 +50,7 @@ tag:
### 完全二叉树
-除最后一层外,若其余层都是满的,并且最后一层或者是满的,或者是在右边缺少连续若干节点,则这个二叉树就是 **完全二叉树** 。
+除最后一层外,若其余层都是满的,并且最后一层是满的或者是在右边缺少连续若干节点,则这个二叉树就是 **完全二叉树** 。
大家可以想象为一棵树从根结点开始扩展,扩展完左子节点才能开始扩展右子节点,每扩展完一层,才能继续扩展下一层。如下图所示:
diff --git a/docs/cs-basics/network/application-layer-protocol.md b/docs/cs-basics/network/application-layer-protocol.md
index 09a7958f4c4..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 的常见应用场景:
+
+- 视频弹幕
+- 实时消息推送,详见[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 是一种不安全的协议,因为它在传输过程中不会对数据进行加密。因此,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 服务器)通过公钥交换生成共享的对称加密密钥,用于后续的加密通信。
+

## RTP:实时传输协议
@@ -110,6 +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 b0bb5f2664b..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 地址
diff --git a/docs/cs-basics/network/computer-network-xiexiren-summary.md b/docs/cs-basics/network/computer-network-xiexiren-summary.md
index ac7e5d18b97..85f7c3f54d2 100644
--- a/docs/cs-basics/network/computer-network-xiexiren-summary.md
+++ b/docs/cs-basics/network/computer-network-xiexiren-summary.md
@@ -20,36 +20,36 @@ tag:
3. **主机(host)**:连接在因特网上的计算机。
4. **ISP(Internet Service Provider)**:因特网服务提供者(提供商)。
-
+ 
5. **IXP(Internet eXchange Point)**:互联网交换点 IXP 的主要作用就是允许两个网络直接相连并交换分组,而不需要再通过第三个网络来转发分组。
-
+ 
-https://labs.ripe.net/Members/fergalc/ixp-traffic-during-stratos-skydive
+ https://labs.ripe.net/Members/fergalc/ixp-traffic-during-stratos-skydive
6. **RFC(Request For Comments)**:意思是“请求评议”,包含了关于 Internet 几乎所有的重要的文字资料。
7. **广域网 WAN(Wide Area Network)**:任务是通过长距离运送主机发送的数据。
8. **城域网 MAN(Metropolitan Area Network)**:用来将多个局域网进行互连。
9. **局域网 LAN(Local Area Network)**:学校或企业大多拥有多个互连的局域网。
-
+ 
-http://conexionesmanwman.blogspot.com/
+ http://conexionesmanwman.blogspot.com/
10. **个人区域网 PAN(Personal Area Network)**:在个人工作的地方把属于个人使用的电子设备用无线技术连接起来的网络 。
-
+ 
-https://www.itrelease.com/2018/07/advantages-and-disadvantages-of-personal-area-network-pan/
+ https://www.itrelease.com/2018/07/advantages-and-disadvantages-of-personal-area-network-pan/
-12. **分组(packet )**:因特网中传送的数据单元。由首部 header 和数据段组成。分组又称为包,首部可称为包头。
-13. **存储转发(store and forward )**:路由器收到一个分组,先检查分组是否正确,并过滤掉冲突包错误。确定包正确后,取出目的地址,通过查找表找到想要发送的输出端口地址,然后将该包发送出去。
+11. **分组(packet )**:因特网中传送的数据单元。由首部 header 和数据段组成。分组又称为包,首部可称为包头。
+12. **存储转发(store and forward )**:路由器收到一个分组,先检查分组是否正确,并过滤掉冲突包错误。确定包正确后,取出目的地址,通过查找表找到想要发送的输出端口地址,然后将该包发送出去。
-
+ 
-14. **带宽(bandwidth)**:在计算机网络中,表示在单位时间内从网络中的某一点到另一点所能通过的“最高数据率”。常用来表示网络的通信线路所能传送数据的能力。单位是“比特每秒”,记为 b/s。
-15. **吞吐量(throughput )**:表示在单位时间内通过某个网络(或信道、接口)的数据量。吞吐量更经常地用于对现实世界中的网络的一种测量,以便知道实际上到底有多少数据量能够通过网络。吞吐量受网络的带宽或网络的额定速率的限制。
+13. **带宽(bandwidth)**:在计算机网络中,表示在单位时间内从网络中的某一点到另一点所能通过的“最高数据率”。常用来表示网络的通信线路所能传送数据的能力。单位是“比特每秒”,记为 b/s。
+14. **吞吐量(throughput )**:表示在单位时间内通过某个网络(或信道、接口)的数据量。吞吐量更经常地用于对现实世界中的网络的一种测量,以便知道实际上到底有多少数据量能够通过网络。吞吐量受网络的带宽或网络的额定速率的限制。
### 1.2. 重要知识点总结
@@ -81,11 +81,11 @@ tag:
5. **半双工(half duplex )**:通信的双方都可以发送信息,但不能双方同时发送(当然也就不能同时接收)。
6. **全双工(full duplex)**:通信的双方可以同时发送和接收信息。
-
+ 
7. **失真**:失去真实性,主要是指接受到的信号和发送的信号不同,有磨损和衰减。影响失真程度的因素:1.码元传输速率 2.信号传输距离 3.噪声干扰 4.传输媒体质量
-
+ 
8. **奈氏准则**:在任何信道中,码元的传输的效率是有上限的,传输速率超过此上限,就会出现严重的码间串扰问题,使接收端对码元的判决(即识别)成为不可能。
9. **香农定理**:在带宽受限且有噪声的信道中,为了不产生误差,信息的数据传输速率有上限值。
@@ -95,7 +95,7 @@ tag:
13. **信噪比(signal-to-noise ratio )**:指信号的平均功率和噪声的平均功率之比,记为 S/N。信噪比(dB)=10\*log10(S/N)。
14. **信道复用(channel multiplexing )**:指多个用户共享同一个信道。(并不一定是同时)。
-
+ 
15. **比特率(bit rate )**:单位时间(每秒)内传送的比特数。
16. **波特率(baud rate)**:单位时间载波调制状态改变的次数。针对数据信号对载波的调制速率。
@@ -151,7 +151,7 @@ tag:

8. **MAC 地址(Media Access Control 或者 Medium Access Control)**:意译为媒体访问控制,或称为物理地址、硬件地址,用来定义网络设备的位置。在 OSI 模型中,第三层网络层负责 IP 地址,第二层数据链路层则负责 MAC 地址。因此一个主机会有一个 MAC 地址,而每个网络位置会有一个专属于它的 IP 地址 。地址是识别某个系统的重要标识符,“名字指出我们所要寻找的资源,地址指出资源所在的地方,路由告诉我们如何到达该处。”
-
+ 
9. **网桥(bridge)**:一种用于数据链路层实现中继,连接两个或多个局域网的网络互连设备。
10. **交换机(switch )**:广义的来说,交换机指的是一种通信系统中完成信息交换的设备。这里工作在数据链路层的交换机指的是交换式集线器,其实质是一个多接口的网桥
@@ -191,7 +191,7 @@ tag:
5. **子网掩码(subnet mask )**:它是一种用来指明一个 IP 地址的哪些位标识的是主机所在的子网以及哪些位标识的是主机的位掩码。子网掩码不能单独存在,它必须结合 IP 地址一起使用。
6. **CIDR( Classless Inter-Domain Routing )**:无分类域间路由选择 (特点是消除了传统的 A 类、B 类和 C 类地址以及划分子网的概念,并使用各种长度的“网络前缀”(network-prefix)来代替分类地址中的网络号和子网号)。
7. **默认路由(default route)**:当在路由表中查不到能到达目的地址的路由时,路由器选择的路由。默认路由还可以减小路由表所占用的空间和搜索路由表所用的时间。
-8. **路由选择算法(Virtual Circuit)**:路由选择协议的核心部分。因特网采用自适应的,分层次的路由选择协议。
+8. **路由选择算法(Routing Algorithm)**:路由选择协议的核心部分。因特网采用自适应的,分层次的路由选择协议。
### 4.2. 重要知识点总结
@@ -218,7 +218,7 @@ tag:
4. **TCP(Transmission Control Protocol)**:传输控制协议。
5. **UDP(User Datagram Protocol)**:用户数据报协议。
-
+ 
6. **端口(port)**:端口的目的是为了确认对方机器的哪个进程在与自己进行交互,比如 MSN 和 QQ 的端口不同,如果没有端口就可能出现 QQ 进程和 MSN 交互错误。端口又称协议端口号。
7. **停止等待协议(stop-and-wait)**:指发送方每发送完一个分组就停止发送,等待对方确认,在收到确认之后在发送下一个分组。
@@ -267,34 +267,34 @@ tag:
1. **域名系统(DNS)**:域名系统(DNS,Domain Name System)将人类可读的域名 (例如,www.baidu.com) 转换为机器可读的 IP 地址 (例如,220.181.38.148)。我们可以将其理解为专为互联网设计的电话薄。
-
+ 
-https://www.seobility.net/en/wiki/HTTP_headers
+ https://www.seobility.net/en/wiki/HTTP_headers
2. **文件传输协议(FTP)**:FTP 是 File Transfer Protocol(文件传输协议)的英文简称,而中文简称为“文传协议”。用于 Internet 上的控制文件的双向传输。同时,它也是一个应用程序(Application)。基于不同的操作系统有不同的 FTP 应用程序,而所有这些应用程序都遵守同一种协议以传输文件。在 FTP 的使用当中,用户经常遇到两个概念:"下载"(Download)和"上传"(Upload)。 "下载"文件就是从远程主机拷贝文件至自己的计算机上;"上传"文件就是将文件从自己的计算机中拷贝至远程主机上。用 Internet 语言来说,用户可通过客户机程序向(从)远程主机上传(下载)文件。
-
+ 
3. **简单文件传输协议(TFTP)**:TFTP(Trivial File Transfer Protocol,简单文件传输协议)是 TCP/IP 协议族中的一个用来在客户机与服务器之间进行简单文件传输的协议,提供不复杂、开销不大的文件传输服务。端口号为 69。
4. **远程终端协议(TELNET)**:Telnet 协议是 TCP/IP 协议族中的一员,是 Internet 远程登陆服务的标准协议和主要方式。它为用户提供了在本地计算机上完成远程主机工作的能力。在终端使用者的电脑上使用 telnet 程序,用它连接到服务器。终端使用者可以在 telnet 程序中输入命令,这些命令会在服务器上运行,就像直接在服务器的控制台上输入一样。可以在本地就能控制服务器。要开始一个 telnet 会话,必须输入用户名和密码来登录服务器。Telnet 是常用的远程控制 Web 服务器的方法。
5. **万维网(WWW)**:WWW 是环球信息网的缩写,(亦作“Web”、“WWW”、“'W3'”,英文全称为“World Wide Web”),中文名字为“万维网”,"环球网"等,常简称为 Web。分为 Web 客户端和 Web 服务器程序。WWW 可以让 Web 客户端(常用浏览器)访问浏览 Web 服务器上的页面。是一个由许多互相链接的超文本组成的系统,通过互联网访问。在这个系统中,每个有用的事物,称为一样“资源”;并且由一个全局“统一资源标识符”(URI)标识;这些资源通过超文本传输协议(Hypertext Transfer Protocol)传送给用户,而后者通过点击链接来获得资源。万维网联盟(英语:World Wide Web Consortium,简称 W3C),又称 W3C 理事会。1994 年 10 月在麻省理工学院(MIT)计算机科学实验室成立。万维网联盟的创建者是万维网的发明者蒂姆·伯纳斯-李。万维网并不等同互联网,万维网只是互联网所能提供的服务其中之一,是靠着互联网运行的一项服务。
6. **万维网的大致工作工程:**
-
+ 
7. **统一资源定位符(URL)**:统一资源定位符是对可以从互联网上得到的资源的位置和访问方法的一种简洁的表示,是互联网上标准资源的地址。互联网上的每个文件都有一个唯一的 URL,它包含的信息指出文件的位置以及浏览器应该怎么处理它。
8. **超文本传输协议(HTTP)**:超文本传输协议(HTTP,HyperText Transfer Protocol)是互联网上应用最为广泛的一种网络协议。所有的 WWW 文件都必须遵守这个标准。设计 HTTP 最初的目的是为了提供一种发布和接收 HTML 页面的方法。1960 年美国人 Ted Nelson 构思了一种通过计算机处理文本信息的方法,并称之为超文本(hypertext),这成为了 HTTP 超文本传输协议标准架构的发展根基。
-HTTP 协议的本质就是一种浏览器与服务器之间约定好的通信格式。HTTP 的原理如下图所示:
+ HTTP 协议的本质就是一种浏览器与服务器之间约定好的通信格式。HTTP 的原理如下图所示:
-
+ 
-10. **代理服务器(Proxy Server)**:代理服务器(Proxy Server)是一种网络实体,它又称为万维网高速缓存。 代理服务器把最近的一些请求和响应暂存在本地磁盘中。当新请求到达时,若代理服务器发现这个请求与暂时存放的的请求相同,就返回暂存的响应,而不需要按 URL 的地址再次去互联网访问该资源。代理服务器可在客户端或服务器工作,也可以在中间系统工作。
-11. **简单邮件传输协议(SMTP)** : SMTP(Simple Mail Transfer Protocol)即简单邮件传输协议,它是一组用于由源地址到目的地址传送邮件的规则,由它来控制信件的中转方式。 SMTP 协议属于 TCP/IP 协议簇,它帮助每台计算机在发送或中转信件时找到下一个目的地。 通过 SMTP 协议所指定的服务器,就可以把 E-mail 寄到收信人的服务器上了,整个过程只要几分钟。SMTP 服务器则是遵循 SMTP 协议的发送邮件服务器,用来发送或中转发出的电子邮件。
+9. **代理服务器(Proxy Server)**:代理服务器(Proxy Server)是一种网络实体,它又称为万维网高速缓存。 代理服务器把最近的一些请求和响应暂存在本地磁盘中。当新请求到达时,若代理服务器发现这个请求与暂时存放的的请求相同,就返回暂存的响应,而不需要按 URL 的地址再次去互联网访问该资源。代理服务器可在客户端或服务器工作,也可以在中间系统工作。
+10. **简单邮件传输协议(SMTP)** : SMTP(Simple Mail Transfer Protocol)即简单邮件传输协议,它是一组用于由源地址到目的地址传送邮件的规则,由它来控制信件的中转方式。 SMTP 协议属于 TCP/IP 协议簇,它帮助每台计算机在发送或中转信件时找到下一个目的地。 通过 SMTP 协议所指定的服务器,就可以把 E-mail 寄到收信人的服务器上了,整个过程只要几分钟。SMTP 服务器则是遵循 SMTP 协议的发送邮件服务器,用来发送或中转发出的电子邮件。
-
+ 
-https://www.campaignmonitor.com/resources/knowledge-base/what-is-the-code-that-makes-bcc-or-cc-operate-in-an-email/
+ https://www.campaignmonitor.com/resources/knowledge-base/what-is-the-code-that-makes-bcc-or-cc-operate-in-an-email/
11. **搜索引擎** :搜索引擎(Search Engine)是指根据一定的策略、运用特定的计算机程序从互联网上搜集信息,在对信息进行组织和处理后,为用户提供检索服务,将用户检索相关的信息展示给用户的系统。搜索引擎包括全文索引、目录索引、元搜索引擎、垂直搜索引擎、集合式搜索引擎、门户搜索引擎与免费链接列表等。
diff --git a/docs/cs-basics/network/dns.md b/docs/cs-basics/network/dns.md
index eba55083eb2..3d3ef0e2254 100644
--- a/docs/cs-basics/network/dns.md
+++ b/docs/cs-basics/network/dns.md
@@ -15,6 +15,8 @@ DNS(Domain Name System)域名管理系统,是当用户使用浏览器访

+## 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 服务

-另外,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`四个字段的四元组。

@@ -97,8 +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 ca4e2a10374..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 状态码更准确点来说是创建一个或多个新的资源,可以参考:。
+
+
这里格外提一下 204 状态码,平时学习/工作中见到的次数并不多。
@@ -64,9 +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/http1.0-vs-http1.1.md b/docs/cs-basics/network/http1.0-vs-http1.1.md
index 41931829edf..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,7 +53,7 @@ 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`字段的报文头部将会是:
@@ -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)` 状态码的主要作用是确保客户端和代理服务器能正确识别部分内容响应,避免将其误认为完整资源并错误地缓存。这对于正确处理范围请求和缓存管理非常重要。
-在范围响应中,`Content-Range`头部标志指示出了该数据块的偏移量和数据块的长度。
+一个典型的 HTTP/1.1 范围请求示例:
+
+```bash
+# 获取一个文件的前 1024 个字节
+GET /z4d4kWk.jpg HTTP/1.1
+Host: i.imgur.com
+Range: bytes=0-1023
+```
+
+`206 Partial Content` 响应:
+
+```bash
+
+HTTP/1.1 206 Partial Content
+Content-Range: bytes 0-1023/146515
+Content-Length: 1024
+…
+(二进制内容)
+```
+
+简单解释一下 HTTP 范围响应头部中的字段:
+
+- **`Content-Range` 头部**:指示返回数据在整个资源中的位置,包括起始和结束字节以及资源的总长度。例如,`Content-Range: bytes 0-1023/146515` 表示服务器端返回了第 0 到 1023 字节的数据(共 1024 字节),而整个资源的总长度是 146,515 字节。
+- **`Content-Length` 头部**:指示此次响应中实际传输的字节数。例如,`Content-Length: 1024` 表示服务器端传输了 1024 字节的数据。
+
+`Range` 请求头不仅可以请求单个字节范围,还可以一次性请求多个范围。这种方式被称为“多重范围请求”(multiple range requests)。
+
+客户端想要获取资源的第 0 到 499 字节以及第 1000 到 1499 字节:
+
+```bash
+GET /path/to/resource HTTP/1.1
+Host: example.com
+Range: bytes=0-499,1000-1499
+```
+
+服务器端返回多个字节范围,每个范围的内容以分隔符分开:
+
+```bash
+HTTP/1.1 206 Partial Content
+Content-Type: multipart/byteranges; boundary=3d6b6a416f9b5
+Content-Length: 376
+
+--3d6b6a416f9b5
+Content-Type: application/octet-stream
+Content-Range: bytes 0-99/2000
+
+(第 0 到 99 字节的数据块)
+
+--3d6b6a416f9b5
+Content-Type: application/octet-stream
+Content-Range: bytes 500-599/2000
+
+(第 500 到 599 字节的数据块)
+
+--3d6b6a416f9b5
+Content-Type: application/octet-stream
+Content-Range: bytes 1000-1099/2000
+
+(第 1000 到 1099 字节的数据块)
+
+--3d6b6a416f9b5--
+```
### 状态码 100
diff --git a/docs/cs-basics/network/nat.md b/docs/cs-basics/network/nat.md
index 5634ba07387..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,6 +55,6 @@ SOHO 子网的“代理人”,也就是和外界的窗口,通常由路由器
3. WAN 的 ISP 变更接口地址时,无需通告 LAN 内主机。
4. LAN 主机对 WAN 不可见,不可直接寻址,可以保证一定程度的安全性。
-然而,NAT 协议由于其独特性,存在着一些争议。比如,可能你已经注意到了,**NAT 协议在 LAN 以外,标识一个内部主机时,使用的是端口号,因为 IP 地址都是相同的。**这种将端口号作为主机寻址的行为,可能会引发一些误会。此外,路由器作为网络层的设备,修改了传输层的分组内容(修改了源 IP 地址和端口号),同样是不规范的行为。但是,尽管如此,NAT 协议作为 IPv4 时代的产物,极大地方便了一些本来棘手的问题,一直被沿用至今。
+然而,NAT 协议由于其独特性,存在着一些争议。比如,可能你已经注意到了,**NAT 协议在 LAN 以外,标识一个内部主机时,使用的是端口号,因为 IP 地址都是相同的**。这种将端口号作为主机寻址的行为,可能会引发一些误会。此外,路由器作为网络层的设备,修改了传输层的分组内容(修改了源 IP 地址和端口号),同样是不规范的行为。但是,尽管如此,NAT 协议作为 IPv4 时代的产物,极大地方便了一些本来棘手的问题,一直被沿用至今。
diff --git a/docs/cs-basics/network/network-attack-means.md b/docs/cs-basics/network/network-attack-means.md
index 9117a4ad22b..748999d6eba 100644
--- a/docs/cs-basics/network/network-attack-means.md
+++ b/docs/cs-basics/network/network-attack-means.md
@@ -199,13 +199,13 @@ DNS Flood 对传统上基于放大的攻击方法做出了改变。借助轻易
可以使用 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 连接发送给另一个窗口并打印出来。
@@ -328,7 +328,7 @@ DES 使用的密钥表面上是 64 位的,然而只有其中的 56 位被实
**SM1 和 SM4**
-之前几种都是国外的,我们国内自行研究了国密 **SM1 **和 **SM4**。其中 S 都属于国家标准,算法公开。优点就是国家的大力支持和认可
+之前几种都是国外的,我们国内自行研究了国密 **SM1**和 **SM4**。其中 S 都属于国家标准,算法公开。优点就是国家的大力支持和认可
**总结**:
@@ -357,17 +357,17 @@ DES 使用的密钥表面上是 64 位的,然而只有其中的 56 位被实
这个大家应该更加熟悉了,比如我们平常使用的 MD5 校验,在很多时候,我并不是拿来进行加密,而是用来获得唯一性 ID。在做系统的过程中,存储用户的各种密码信息,通常都会通过散列算法,最终存储其散列值。
-**MD5**
+**MD5**(不推荐)
MD5 可以用来生成一个 128 位的消息摘要,它是目前应用比较普遍的散列算法,具体的应用场景你可以自行 参阅。虽然,因为算法的缺陷,它的唯一性已经被破解了,但是大部分场景下,这并不会构成安全问题。但是,如果不是长度受限(32 个字符),我还是不推荐你继续使用 **MD5** 的。
**SHA**
-安全散列算法。**SHA** 分为 **SHA1** 和 **SH2** 两个版本。该算法的思想是接收一段明文,然后以一种不可逆的方式将它转换成一段(通常更小)密文,也可以简单的理解为取一串输入码(称为预映射或信息),并把它们转化为长度较短、位数固定的输出序列即散列值(也称为信息摘要或信息认证代码)的过程。
+安全散列算法。**SHA** 包括**SHA-1**、**SHA-2**和**SHA-3**三个版本。该算法的基本思想是:接收一段明文数据,通过不可逆的方式将其转换为固定长度的密文。简单来说,SHA 将输入数据(即预映射或消息)转化为固定长度、较短的输出值,称为散列值(或信息摘要、信息认证码)。SHA-1 已被证明不够安全,因此逐渐被 SHA-2 取代,而 SHA-3 则作为 SHA 系列的最新版本,采用不同的结构(Keccak 算法)提供更高的安全性和灵活性。
**SM3**
-国密算法**SM3**。加密强度和 SHA-256 想不多。主要是收到国家的支持。
+国密算法**SM3**。加密强度和 SHA-256 算法 相差不多。主要是受到了国家的支持。
**总结**:
@@ -462,9 +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 424d837507a..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 七层模型虽然失败了,但是却提供了很多不错的理论基础

-- **TCP(Transmisson Control Protocol,传输控制协议 )**:提供 **面向连接** 的,**可靠** 的数据传输服务。
+- **TCP(Transmission Control Protocol,传输控制协议 )**:提供 **面向连接** 的,**可靠** 的数据传输服务。
- **UDP(User Datagram Protocol,用户数据协议)**:提供 **无连接** 的,**尽最大努力** 的数据传输服务(不保证数据传输的可靠性),简单高效。
### 网络层(Network layer)
@@ -189,7 +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 5ee30f686ef..d63765ec799 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 七层模型** 是国际标准化组织提出的一个网络分层模型,其大体结构以及每一层提供的功能如下图所示:

@@ -27,7 +27,7 @@ tag:

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

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

@@ -94,82 +94,80 @@ tag:
- **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)的路由选择协议,具有高度的灵活性和可扩展性。
## HTTP
-### 从输入 URL 到页面展示到底发生了什么?(非常重要)
+### ⭐️从输入 URL 到页面展示到底发生了什么?(非常重要)
> 类似的问题:打开一个网页,整个过程会使用哪些协议?
-图解(图片来源:《图解 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 连接,或者等待服务器的关闭请求。
-具体可以参考下面这两篇文章:
+详细介绍可以查看这篇文章:[访问网页的全过程(知识串联)](https://javaguide.cn/cs-basics/network/the-whole-process-of-accessing-web-pages.html)(强烈推荐)。
-- [从输入 URL 到页面加载发生了什么?](https://segmentfault.com/a/1190000006879700)
-- [浏览器从输入网址到页面展示的过程](https://cloud.tencent.com/developer/article/1879758)
-
-### HTTP 状态码有哪些?
+### ⭐️HTTP 状态码有哪些?
HTTP 状态码用于描述 HTTP 请求的结果,比如 2xx 就代表请求被成功处理。

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

@@ -178,26 +176,27 @@ HTTP 状态码用于描述 HTTP 请求的结果,比如 2xx 就代表请求被
- **安全性和资源消耗**:HTTP 协议运行在 TCP 之上,所有传输的内容都是明文,客户端和服务器端都无法验证对方的身份。HTTPS 是运行在 SSL/TLS 之上的 HTTP 协议,SSL/TLS 运行在 TCP 之上。所有传输的内容都经过加密,加密采用对称加密,但对称加密的密钥用服务器方的证书进行了非对称加密。所以说,HTTP 安全性没有 HTTPS 高,但是 HTTPS 比 HTTP 耗费更多服务器资源。
- **SEO(搜索引擎优化)**:搜索引擎通常会更青睐使用 HTTPS 协议的网站,因为 HTTPS 能够提供更高的安全性和用户隐私保护。使用 HTTPS 协议的网站在搜索结果中可能会被优先显示,从而对 SEO 产生影响。
-关于 HTTP 和 HTTPS 更详细的对比总结,可以看我写的这篇文章:[HTTP vs HTTPS(应用层)](./http-vs-https.md) 。
+关于 HTTP 和 HTTPS 更详细的对比总结,可以看我写的这篇文章:[HTTP vs HTTPS(应用层)](https://javaguide.cn/cs-basics/network/http-vs-https.html) 。
### HTTP/1.0 和 HTTP/1.1 有什么区别?

-- **连接方式** : 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),这样就方便了开发者自由的选择以便于充分利用带宽和连接。
- **Host 头(Host Header)处理** :HTTP/1.1 引入了 Host 头字段,允许在同一 IP 地址上托管多个域名,从而支持虚拟主机的功能。而 HTTP/1.0 没有 Host 头字段,无法实现虚拟主机。
-关于 HTTP/1.0 和 HTTP/1.1 更详细的对比总结,可以看我写的这篇文章:[HTTP/1.0 vs HTTP/1.1(应用层)](./http1.0-vs-http1.1.md) 。
+关于 HTTP/1.0 和 HTTP/1.1 更详细的对比总结,可以看我写的这篇文章:[HTTP/1.0 vs HTTP/1.1(应用层)](https://javaguide.cn/cs-basics/network/http1.0-vs-http1.1.html) 。
-### HTTP/1.1 和 HTTP/2.0 有什么区别?
+### ⭐️HTTP/1.1 和 HTTP/2.0 有什么区别?

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

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

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

-相关阅读:[NAT 协议详解(网络层)](./nat.md)。
+相关阅读:[NAT 协议详解(网络层)](https://javaguide.cn/cs-basics/network/nat.html)。
## ARP
@@ -162,19 +238,19 @@ 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 地址的一些问题。
### ARP 协议的工作原理?
-[ARP 协议详解(网络层)](./arp.md)
+[ARP 协议详解(网络层)](https://javaguide.cn/cs-basics/network/arp.html)
## 复习建议
@@ -185,6 +261,7 @@ ARP 协议,全称 **地址解析协议(Address Resolution Protocol)**,
- 《图解 HTTP》
- 《计算机网络自顶向下方法》(第七版)
- 什么是 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 cc09bdce505..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,8 +58,8 @@ tag:
断开一个 TCP 连接则需要“四次挥手”,缺一不可:
-1. **第一次挥手**:客户端发送一个 FIN(SEQ=x) 标志的数据包->服务端,用来关闭客户端到服务器的数据传送。然后客户端进入 **FIN-WAIT-1** 状态。
-2. **第二次挥手**:服务器收到这个 FIN(SEQ=X) 标志的数据包,它发送一个 ACK (ACK=x+1)标志的数据包->客户端 。然后服务端进入 **CLOSE-WAIT** 状态,客户端进入 **FIN-WAIT-2** 状态。
+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,6 +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 bd64a5ae843..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,18 +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:
+ - 计算机网络
+---
+
+开发岗中总是会考很多计算机网络的知识点,但如果让面试官只考一道题,便涵盖最多的计网知识点,那可能就是 **网页浏览的全过程** 了。本篇文章将带大家从头到尾过一遍这道被考烂的面试题,必会!!!
+
+总的来说,网络通信模型可以用下图来表示,也就是大家只要熟记网络结构五层模型,按照这个体系,很多知识点都能顺出来了。访问网页的过程也是如此。
+
+
+
+开始之前,我们先简单过一遍完整流程:
+
+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 的组成结构
+
+
+
+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 ce433c17d4e..bb7ad9a49b6 100644
--- a/docs/cs-basics/operating-system/linux-intro.md
+++ b/docs/cs-basics/operating-system/linux-intro.md
@@ -70,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 的数量。
@@ -166,7 +166,7 @@ Linux 使用一种称为目录树的层次结构来组织文件和目录。目
下面只是给出了一些比较常用的命令。
-推荐一个 Linux 命令快查网站,非常不错,大家如果遗忘某些命令或者对某些命令不理解都可以在这里得到解决。Linux 命令在线速查手册:https://wangchujiang.com/linux-command/ 。
+推荐一个 Linux 命令快查网站,非常不错,大家如果遗忘某些命令或者对某些命令不理解都可以在这里得到解决。Linux 命令在线速查手册: 。

@@ -185,8 +185,8 @@ Linux 使用一种称为目录树的层次结构来组织文件和目录。目
### 目录操作
- `ls`:显示目录中的文件和子目录的列表。例如:`ls /home`,显示 `/home` 目录下的文件和子目录列表。
-- `ll`:`ll` 是 `ls -l` 的别名,ll 命令可以看到该目录下的所有目录和文件的详细信息
-- `mkdir [选项] 目录名`:创建新目录(增)。例如:`mkdir -m 755 my_directory`,创建一个名为 `my_directory` 的新目录,并将其权限设置为 755,即所有用户对该目录有读、写和执行的权限。
+- `ll`:`ll` 是 `ls -l` 的别名,ll 命令可以看到该目录下的所有目录和文件的详细信息。
+- `mkdir [选项] 目录名`:创建新目录(增)。例如:`mkdir -m 755 my_directory`,创建一个名为 `my_directory` 的新目录,并将其权限设置为 755,其中所有者拥有读、写、执行权限,所属组和其他用户只有读、执行权限,无法修改目录内容(如创建或删除文件)。如果希望所有用户(包括所属组和其他用户)对目录都拥有读、写、执行权限,则应设置权限为 `777`,即:`mkdir -m 777 my_directory`。
- `find [路径] [表达式]`:在指定目录及其子目录中搜索文件或目录(查),非常强大灵活。例如:① 列出当前目录及子目录下所有文件和文件夹: `find .`;② 在`/home`目录下查找以 `.txt` 结尾的文件名:`find /home -name "*.txt"` ,忽略大小写: `find /home -i name "*.txt"` ;③ 当前目录及子目录下查找所有以 `.txt` 和 `.pdf` 结尾的文件:`find . \( -name "*.txt" -o -name "*.pdf" \)`或`find . -name "*.txt" -o -name "*.pdf"`。
- `pwd`:显示当前工作目录的路径。
- `rmdir [选项] 目录名`:删除空目录(删)。例如:`rmdir -p my_directory`,删除名为 `my_directory` 的空目录,并且会递归删除`my_directory`的空父目录,直到遇到非空目录或根目录。
@@ -237,7 +237,7 @@ Linux 中的打包文件一般是以 `.tar` 结尾的,压缩的命令一般是
### 文件权限
-操作系统中每个文件都拥有特定的权限、所属用户和所属组。权限是操作系统用来限制资源访问的机制,在 Linux 中权限一般分为读(readable)、写(writable)和执行(excutable),分为三组。分别对应文件的属主(owner),属组(group)和其他用户(other),通过这样的机制来限制哪些用户、哪些组可以对特定的文件进行什么样的操作。
+操作系统中每个文件都拥有特定的权限、所属用户和所属组。权限是操作系统用来限制资源访问的机制,在 Linux 中权限一般分为读(readable)、写(writable)和执行(executable),分为三组。分别对应文件的属主(owner),属组(group)和其他用户(other),通过这样的机制来限制哪些用户、哪些组可以对特定的文件进行什么样的操作。
通过 **`ls -l`** 命令我们可以 查看某个目录下的文件或目录的权限
@@ -285,11 +285,11 @@ Linux 中的打包文件一般是以 `.tar` 结尾的,压缩的命令一般是
需要注意的是:**超级用户可以无视普通用户的权限,即使文件目录权限是 000,依旧可以访问。**
-**在 linux 中的每个用户必须属于一个组,不能独立于组外。在 linux 中每个文件有所有者、所在组、其它组的概念。**
+**在 Linux 中的每个用户必须属于一个组,不能独立于组外。在 linux 中每个文件有所有者、所在组、其它组的概念。**
-- **所有者(u)**:一般为文件的创建者,谁创建了该文件,就天然的成为该文件的所有者,用 `ls ‐ahl` 命令可以看到文件的所有者 也可以使用 chown 用户名 文件名来修改文件的所有者 。
-- **文件所在组(g)**:当某个用户创建了一个文件后,这个文件的所在组就是该用户所在的组用 `ls ‐ahl`命令可以看到文件的所有组也可以使用 chgrp 组名 文件名来修改文件所在的组。
-- **其它组(o)**:除开文件的所有者和所在组的用户外,系统的其它用户都是文件的其它组。
+- **所有者(u)** :一般为文件的创建者,谁创建了该文件,就天然的成为该文件的所有者,用 `ls ‐ahl` 命令可以看到文件的所有者 也可以使用 chown 用户名 文件名来修改文件的所有者 。
+- **文件所在组(g)** :当某个用户创建了一个文件后,这个文件的所在组就是该用户所在的组用 `ls ‐ahl`命令可以看到文件的所有组也可以使用 chgrp 组名 文件名来修改文件所在的组。
+- **其它组(o)** :除开文件的所有者和所在组的用户外,系统的其它用户都是文件的其它组。
> 我们再来看看如何修改文件/目录的权限。
@@ -355,11 +355,13 @@ Linux 系统是一个多用户多任务的分时操作系统,任何一个要
- `ifconfig` 或 `ip`:用于查看系统的网络接口信息,包括网络接口的 IP 地址、MAC 地址、状态等。
- `netstat [选项]`:用于查看系统的网络连接状态和网络统计信息,可以查看当前的网络连接情况、监听端口、网络协议等。
- `ss [选项]`:比 `netstat` 更好用,提供了更快速、更详细的网络连接信息。
+- `nload`:`sar` 和 `nload` 都可以监控网络流量,但`sar` 的输出是文本形式的数据,不够直观。`nload` 则是一个专门用于实时监控网络流量的工具,提供图形化的终端界面,更加直观。不过,`nload` 不保存历史数据,所以它不适合用于长期趋势分析。并且,系统并没有默认安装它,需要手动安装。
+- `sudo hostnamectl set-hostname 新主机名`:更改主机名,并且重启后依然有效。`sudo hostname 新主机名`也可以更改主机名。不过需要注意的是,使用 `hostname` 命令直接更改主机名只是临时生效,系统重启后会恢复为原来的主机名。
### 其他
- `sudo + 其他命令`:以系统管理者的身份执行指令,也就是说,经由 sudo 所执行的指令就好像是 root 亲自执行。
-- `grep 要搜索的字符串 要搜索的文件 --color`:搜索命令,--color 代表高亮显示。
+- `grep [选项] "搜索内容" 文件路径`:非常强大且常用的文本搜索命令,它可以根据指定的字符串或正则表达式,在文件或命令输出中进行匹配查找,适用于日志分析、文本过滤、快速定位等多种场景。示例:忽略大小写搜索 syslog 中所有包含 error 的行:`grep -i "error" /var/log/syslog`,查找所有与 java 相关的进程:`ps -ef | grep "java"`。
- `kill -9 进程的pid`:杀死进程(-9 表示强制终止)先用 ps 查找进程,然后用 kill 杀掉。
- `shutdown`:`shutdown -h now`:指定现在立即关机;`shutdown +5 "System will shutdown after 5 minutes"`:指定 5 分钟后关机,同时送出警告信息给登入用户。
- `reboot`:`reboot`:重开机。`reboot -w`:做个重开机的模拟(只有纪录并不会真的重开机)。
diff --git a/docs/cs-basics/operating-system/operating-system-basic-questions-01.md b/docs/cs-basics/operating-system/operating-system-basic-questions-01.md
index de1a9073519..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`)切换回用户态,恢复用户态的上下文,继续执行用户程序。

@@ -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 是什么?包含哪些信息?
@@ -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 方式执行。可以根据内存要求,时间要求或任何其他资源要求来确定优先级。
### 什么是僵尸进程和孤儿进程?
@@ -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,8 +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 a7df8275a09..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
@@ -68,7 +68,7 @@ head:
非连续内存管理存在下面 3 种方式:
-- **段式管理**:以段(—段连续的物理内存)的形式管理/分配物理内存。应用程序的虚拟地址空间被分为大小不等的段,段是有实际意义的,每个段定义了一组逻辑信息,例如有主程序段 MAIN、子程序段 X、数据段 D 及栈段 S 等。
+- **段式管理**:以段(一段连续的物理内存)的形式管理/分配物理内存。应用程序的虚拟地址空间被分为大小不等的段,段是有实际意义的,每个段定义了一组逻辑信息,例如有主程序段 MAIN、子程序段 X、数据段 D 及栈段 S 等。
- **页式管理**:把物理内存分为连续等长的物理页,应用程序的虚拟地址空间也被划分为连续等长的虚拟页,是现代操作系统广泛使用的一种内存管理方式。
- **段页式管理机制**:结合了段式管理和页式管理的一种内存管理机制,把物理内存先分成若干段,每个段又继续分成若干大小相等的页。
@@ -131,7 +131,7 @@ MMU 将虚拟地址翻译为物理地址的主要机制有 3 种:
### 分段机制
-**分段机制(Segmentation)** 以段(—段 **连续** 的物理内存)的形式管理/分配物理内存。应用程序的虚拟地址空间被分为大小不等的段,段是有实际意义的,每个段定义了一组逻辑信息,例如有主程序段 MAIN、子程序段 X、数据段 D 及栈段 S 等。
+**分段机制(Segmentation)** 以段(一段 **连续** 的物理内存)的形式管理/分配物理内存。应用程序的虚拟地址空间被分为大小不等的段,段是有实际意义的,每个段定义了一组逻辑信息,例如有主程序段 MAIN、子程序段 X、数据段 D 及栈段 S 等。
#### 段表有什么用?地址翻译过程是怎样的?
@@ -188,7 +188,7 @@ MMU 将虚拟地址翻译为物理地址的主要机制有 3 种:

-在分页机制下,每个应用程序都会有一个对应的页表。
+在分页机制下,每个进程都会有一个对应的页表。
分页机制下的虚拟地址由两部分组成:
@@ -211,11 +211,11 @@ MMU 将虚拟地址翻译为物理地址的主要机制有 3 种:
#### 单级页表有什么问题?为什么需要多级页表?
-以 32 位的环境为例,虚拟地址空间范围共有 2^32(4G)。假设 一个页的大小是 2^12(4KB),那页表项共有 4G / 4K = 2^20 个。每个页表项为一个地址,占用 4 字节,2^20 * 2^2/1024*1024= 4MB。也就是说一个程序啥都不干,页表大小就得占用 4M。
+以 32 位的环境为例,虚拟地址空间范围共有 2^32(4G)。假设 一个页的大小是 2^12(4KB),那页表项共有 4G / 4K = 2^20 个。每个页表项为一个地址,占用 4 字节,`2^20 * 2^2 / 1024 * 1024= 4MB`。也就是说一个程序啥都不干,页表大小就得占用 4M。
系统运行的应用程序多起来的话,页表的开销还是非常大的。而且,绝大部分应用程序可能只能用到页表中的几项,其他的白白浪费了。
-为了解决这个问题,操作系统引入了 **多级页表** ,多级页表对应多个页表,每个页表也前一个页表相关联。32 位系统一般为二级页表,64 位系统一般为四级页表。
+为了解决这个问题,操作系统引入了 **多级页表** ,多级页表对应多个页表,每个页表与前一个页表相关联。32 位系统一般为二级页表,64 位系统一般为四级页表。
这里以二级页表为例进行介绍:二级列表分为一级页表和二级页表。一级页表共有 1024 个页表项,一级页表又关联二级页表,二级页表同样共有 1024 个页表项。二级页表中的一级页表项是一对多的关系,二级页表按需加载(只会用到很少一部分二级页表),进而节省空间占用。
@@ -282,7 +282,7 @@ TLB 的设计思想非常简单,但命中率往往非常高,效果很好。

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. **页式地址映射(线性地址 → 物理地址):**
+ - 线性地址 = 页号 + 页内偏移。
+ - 根据页号查页表,找到物理页框号,加上页内偏移得到物理地址。
### 局部性原理
@@ -404,10 +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 1c45e4316f3..48066214c23 100644
--- a/docs/cs-basics/operating-system/shell-intro.md
+++ b/docs/cs-basics/operating-system/shell-intro.md
@@ -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
```
diff --git a/docs/database/basis.md b/docs/database/basis.md
index 86ddfc4b54a..1df5d538fb8 100644
--- a/docs/database/basis.md
+++ b/docs/database/basis.md
@@ -65,7 +65,7 @@ ER 图由下面 3 个要素组成:
- **函数依赖(functional dependency)**:若在一张表中,在属性(或属性组)X 的值确定的情况下,必定能确定属性 Y 的值,那么就可以说 Y 函数依赖于 X,写作 X → Y。
- **部分函数依赖(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(第三范式)
@@ -86,8 +86,8 @@ ER 图由下面 3 个要素组成:
为什么不要用外键呢?大部分人可能会这样回答:
-1. **增加了复杂性:** a. 每次做 DELETE 或者 UPDATE 都必须考虑外键约束,会导致开发的时候很痛苦, 测试数据极为不方便; b. 外键的主从关系是定的,假如那天需求有变化,数据库中的这个字段根本不需要和其他表有关联的话就会增加很多麻烦。
-2. **增加了额外工作**:数据库需要增加维护外键的工作,比如当我们做一些涉及外键字段的增,删,更新操作之后,需要触发相关操作去检查,保证数据的的一致性和正确性,这样会不得不消耗资源;(个人觉得这个不是不用外键的原因,因为即使你不使用外键,你在应用层面也还是要保证的。所以,我觉得这个影响可以忽略不计。)
+1. **增加了复杂性:** a. 每次做 DELETE 或者 UPDATE 都必须考虑外键约束,会导致开发的时候很痛苦, 测试数据极为不方便; b. 外键的主从关系是定的,假如哪天需求有变化,数据库中的这个字段根本不需要和其他表有关联的话就会增加很多麻烦。
+2. **增加了额外工作**:数据库需要增加维护外键的工作,比如当我们做一些涉及外键字段的增,删,更新操作之后,需要触发相关操作去检查,保证数据的的一致性和正确性,这样会不得不消耗数据库资源。如果在应用层面去维护的话,可以减小数据库压力;
3. **对分库分表不友好**:因为分库分表下外键是无法生效的。
4. ……
diff --git a/docs/database/character-set.md b/docs/database/character-set.md
index 3c2d008de06..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
为了更加适合本国语言,诞生了很多种字符集。
@@ -100,7 +106,7 @@ UTF-32 的规则最简单,不过缺陷也比较明显,对于英文字母这
## MySQL 字符集
-MySQL 支持很多种字符编码的方式,比如 UTF-8、GB2312、GBK、BIG5。
+MySQL 支持很多种字符集的方式,比如 GB2312、GBK、BIG5、多种 Unicode 字符集(UTF-8 编码、UTF-16 编码、UCS-2 编码、UTF-32 编码等等)。
### 查看支持的字符集
@@ -252,13 +258,13 @@ set names utf8mb4
# SET collation_connection = utf8mb4;
```
-### jdbc 对连接字符集的影响
+### JDBC 对连接字符集的影响
-不知道你们有没有碰到过存储 emoji 表情正常,但是使用类似 Navicat 之类的软件的进行查询的时候,发现 emoji 表情变成了问号的情况。这个问题很有可能就是 jdbc 驱动引起的。
+不知道你们有没有碰到过存储 emoji 表情正常,但是使用类似 Navicat 之类的软件的进行查询的时候,发现 emoji 表情变成了问号的情况。这个问题很有可能就是 JDBC 驱动引起的。
-根据前面的内容,我们知道连接字符集也是会影响我们存储的数据的,而 jdbc 驱动会影响连接字符集。
+根据前面的内容,我们知道连接字符集也是会影响我们存储的数据的,而 JDBC 驱动会影响连接字符集。
-`mysql-connector-java` (jdbc 驱动)主要通过这几个属性影响连接字符集:
+`mysql-connector-java` (JDBC 驱动)主要通过这几个属性影响连接字符集:
- `characterEncoding`
- `characterSetResults`
diff --git a/docs/database/mongodb/mongodb-questions-01.md b/docs/database/mongodb/mongodb-questions-01.md
index 81ed013956c..81b7db98890 100644
--- a/docs/database/mongodb/mongodb-questions-01.md
+++ b/docs/database/mongodb/mongodb-questions-01.md
@@ -149,7 +149,7 @@ WiredTiger maintains a table's data in memory using a data structure called a B-
此外,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 官方文档(主要参考资料,以官方文档为准):
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 b3467c98f34..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。
diff --git a/docs/database/mysql/how-sql-executed-in-mysql.md b/docs/database/mysql/how-sql-executed-in-mysql.md
index d9f1d07d539..0b01d9a4da3 100644
--- a/docs/database/mysql/how-sql-executed-in-mysql.md
+++ b/docs/database/mysql/how-sql-executed-in-mysql.md
@@ -100,7 +100,7 @@ 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 为提交状态。
- 更新完成。
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 10a8fe9eb87..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
+> 原文:
## 前言
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 f069f426448..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
+> 原文:
众所周知,自增主键可以让聚集索引尽量地保持递增顺序插入,避免了随机查询,从而提高了查询效率。
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 dc6a49fcdd2..38c333b3308 100644
--- a/docs/database/mysql/mysql-high-performance-optimization-specification-recommendations.md
+++ b/docs/database/mysql/mysql-high-performance-optimization-specification-recommendations.md
@@ -11,17 +11,17 @@ tag:
## 数据库命名规范
-- 所有数据库对象名称必须使用小写字母并用下划线分割
-- 所有数据库对象名称禁止使用 MySQL 保留关键字(如果表名中包含关键字查询时,需要将其用单引号括起来)
-- 数据库对象的命名要能做到见名识意,并且最后不要超过 32 个字符
-- 临时库表必须以 `tmp_` 为前缀并以日期为后缀,备份表必须以 `bak_` 为前缀并以日期 (时间戳) 为后缀
-- 所有存储相同数据的列名和列类型必须一致(一般作为关联列,如果查询时关联列类型不一致会自动进行数据类型隐式转换,会造成列上的索引失效,导致查询效率降低)
+- 所有数据库对象名称必须使用小写字母并用下划线分割。
+- 所有数据库对象名称禁止使用 MySQL 保留关键字(如果表名中包含关键字查询时,需要将其用单引号括起来)。
+- 数据库对象的命名要能做到见名识义,并且最好不要超过 32 个字符。
+- 临时库表必须以 `tmp_` 为前缀并以日期为后缀,备份表必须以 `bak_` 为前缀并以日期 (时间戳) 为后缀。
+- 所有存储相同数据的列名和列类型必须一致(一般作为关联列,如果查询时关联列类型不一致会自动进行数据类型隐式转换,会造成列上的索引失效,导致查询效率降低)。
## 数据库基本设计规范
### 所有表必须使用 InnoDB 存储引擎
-没有特殊要求(即 InnoDB 无法满足的功能如:列存储,存储空间数据等)的情况下,所有表必须使用 InnoDB 存储引擎(MySQL5.5 之前默认使用 Myisam,5.6 以后默认的为 InnoDB)。
+没有特殊要求(即 InnoDB 无法满足的功能如:列存储、存储空间数据等)的情况下,所有表必须使用 InnoDB 存储引擎(MySQL5.5 之前默认使用 MyISAM,5.6 以后默认的为 InnoDB)。
InnoDB 支持事务,支持行级锁,更好的恢复性,高并发下性能更好。
@@ -33,19 +33,19 @@ InnoDB 支持事务,支持行级锁,更好的恢复性,高并发下性能
### 所有表和字段都需要添加注释
-使用 comment 从句添加表和列的备注,从一开始就进行数据字典的维护
+使用 comment 从句添加表和列的备注,从一开始就进行数据字典的维护。
### 尽量控制单表数据量的大小,建议控制在 500 万以内
500 万并不是 MySQL 数据库的限制,过大会造成修改表结构,备份,恢复都会有很大的问题。
-可以用历史数据归档(应用于日志数据),分库分表(应用于业务数据)等手段来控制数据量大小
+可以用历史数据归档(应用于日志数据),分库分表(应用于业务数据)等手段来控制数据量大小。
### 谨慎使用 MySQL 分区表
-分区表在物理上表现为多个文件,在逻辑上表现为一个表;
+分区表在物理上表现为多个文件,在逻辑上表现为一个表。
-谨慎选择分区键,跨分区查询效率可能更低;
+谨慎选择分区键,跨分区查询效率可能更低。
建议采用物理分表的方式管理大数据。
@@ -71,7 +71,7 @@ InnoDB 支持事务,支持行级锁,更好的恢复性,高并发下性能
### 禁止在线上做数据库压力测试
-### 禁止从开发环境,测试环境直接连接生产环境数据库
+### 禁止从开发环境、测试环境直接连接生产环境数据库
安全隐患极大,要对生产环境抱有敬畏之心!
@@ -79,22 +79,22 @@ InnoDB 支持事务,支持行级锁,更好的恢复性,高并发下性能
### 优先选择符合存储需要的最小的数据类型
-存储字节越小,占用也就空间越小,性能也越好。
+存储字节越小,占用空间也就越小,性能也越好。
-**a.某些字符串可以转换成数字类型存储比如可以将 IP 地址转换成整型数据。**
+**a.某些字符串可以转换成数字类型存储,比如可以将 IP 地址转换成整型数据。**
数字是连续的,性能更好,占用空间也更小。
-MySQL 提供了两个方法来处理 ip 地址
+MySQL 提供了两个方法来处理 ip 地址:
-- `INET_ATON()`:把 ip 转为无符号整型 (4-8 位)
-- `INET_NTOA()` :把整型的 ip 转为地址
+- `INET_ATON()`:把 ip 转为无符号整型 (4-8 位);
+- `INET_NTOA()`:把整型的 ip 转为地址。
-插入数据前,先用 `INET_ATON()` 把 ip 地址转为整型,显示数据时,使用 `INET_NTOA()` 把整型的 ip 地址转为地址显示即可。
+插入数据前,先用 `INET_ATON()` 把 ip 地址转为整型;显示数据时,使用 `INET_NTOA()` 把整型的 ip 地址转为地址显示即可。
-**b.对于非负型的数据 (如自增 ID,整型 IP,年龄) 来说,要优先使用无符号整型来存储。**
+**b.对于非负型的数据 (如自增 ID、整型 IP、年龄) 来说,要优先使用无符号整型来存储。**
-无符号相对于有符号可以多出一倍的存储空间
+无符号相对于有符号可以多出一倍的存储空间:
```sql
SIGNED INT -2147483648~2147483647
@@ -103,7 +103,7 @@ UNSIGNED INT 0~4294967295
**c.小数值类型(比如年龄、状态表示如 0/1)优先使用 TINYINT 类型。**
-### 避免使用 TEXT,BLOB 数据类型,最常见的 TEXT 类型可以存储 64k 的数据
+### 避免使用 TEXT、BLOB 数据类型,最常见的 TEXT 类型可以存储 64k 的数据
**a. 建议把 BLOB 或是 TEXT 列分离到单独的扩展表中。**
@@ -113,30 +113,30 @@ MySQL 内存临时表不支持 TEXT、BLOB 这样的大数据类型,如果查
**2、TEXT 或 BLOB 类型只能使用前缀索引**
-因为 MySQL 对索引字段长度是有限制的,所以 TEXT 类型只能使用前缀索引,并且 TEXT 列上是不能有默认值的
+因为 MySQL 对索引字段长度是有限制的,所以 TEXT 类型只能使用前缀索引,并且 TEXT 列上是不能有默认值的。
### 避免使用 ENUM 类型
-- 修改 ENUM 值需要使用 ALTER 语句;
-- ENUM 类型的 ORDER BY 操作效率低,需要额外操作;
-- ENUM 数据类型存在一些限制比如建议不要使用数值作为 ENUM 的枚举值。
+- 修改 ENUM 值需要使用 ALTER 语句。
+- ENUM 类型的 ORDER BY 操作效率低,需要额外操作。
+- ENUM 数据类型存在一些限制,比如建议不要使用数值作为 ENUM 的枚举值。
相关阅读:[是否推荐使用 MySQL 的 enum 类型? - 架构文摘 - 知乎](https://www.zhihu.com/question/404422255/answer/1661698499) 。
### 尽可能把所有列定义为 NOT NULL
-除非有特别的原因使用 NULL 值,应该总是让字段保持 NOT NULL。
+除非有特别的原因使用 NULL 值,否则应该总是让字段保持 NOT NULL。
-- 索引 NULL 列需要额外的空间来保存,所以要占用更多的空间;
+- 索引 NULL 列需要额外的空间来保存,所以要占用更多的空间。
- 进行比较和计算时要对 NULL 值做特别的处理。
相关阅读:[技术分享 | MySQL 默认值选型(是空,还是 NULL)](https://opensource.actionsky.com/20190710-mysql/) 。
### 一定不要用字符串存储日期
-对于日期类型来说, 一定不要用字符串存储日期。可以考虑 DATETIME、TIMESTAMP 和 数值型时间戳。
+对于日期类型来说,一定不要用字符串存储日期。可以考虑 DATETIME、TIMESTAMP 和数值型时间戳。
-这三种种方式都有各自的优势,根据实际场景选择最合适的才是王道。下面再对这三种方式做一个简单的对比,以供大家实际开发中选择正确的存放时间的数据类型:
+这三种种方式都有各自的优势,根据实际场景选择最合适的才是王道。下面再对这三种方式做一个简单的对比,以供大家在实际开发中选择正确的存放时间的数据类型:
| 类型 | 存储空间 | 日期格式 | 日期范围 | 是否带时区信息 |
| ------------ | -------- | ------------------------------ | ------------------------------------------------------------ | -------------- |
@@ -148,10 +148,10 @@ MySQL 时间类型选择的详细介绍请看这篇:[MySQL 时间类型数据
### 同财务相关的金额类数据必须使用 decimal 类型
-- **非精准浮点**:float,double
+- **非精准浮点**:float、double
- **精准浮点**:decimal
-decimal 类型为精准浮点数,在计算时不会丢失精度。占用空间由定义的宽度决定,每 4 个字节可以存储 9 位数字,并且小数点要占用一个字节。并且,decimal 可用于存储比 bigint 更大的整型数据
+decimal 类型为精准浮点数,在计算时不会丢失精度。占用空间由定义的宽度决定,每 4 个字节可以存储 9 位数字,并且小数点要占用一个字节。并且,decimal 可用于存储比 bigint 更大的整型数据。
不过, 由于 decimal 需要额外的空间和计算开销,应该尽量只在需要对数据进行精确计算时才使用 decimal 。
@@ -161,13 +161,13 @@ decimal 类型为精准浮点数,在计算时不会丢失精度。占用空间
## 索引设计规范
-### 限制每张表上的索引数量,建议单张表索引不超过 5 个
+### 限制每张表上的索引数量,建议单张表索引不超过 5 个
-索引并不是越多越好!索引可以提高效率同样可以降低效率。
+索引并不是越多越好!索引可以提高效率,同样可以降低效率。
索引可以增加查询效率,但同样也会降低插入和更新的效率,甚至有些情况下会降低查询效率。
-因为 MySQL 优化器在选择如何优化查询时,会根据统一信息,对每一个可以用到的索引来进行评估,以生成出一个最好的执行计划,如果同时有很多个索引都可以用于查询,就会增加 MySQL 优化器生成执行计划的时间,同样会降低查询性能。
+因为 MySQL 优化器在选择如何优化查询时,会根据统一信息,对每一个可以用到的索引来进行评估,以生成出一个最好的执行计划。如果同时有很多个索引都可以用于查询,就会增加 MySQL 优化器生成执行计划的时间,同样会降低查询性能。
### 禁止使用全文索引
@@ -175,46 +175,46 @@ decimal 类型为精准浮点数,在计算时不会丢失精度。占用空间
### 禁止给表中的每一列都建立单独的索引
-5.6 版本之前,一个 sql 只能使用到一个表中的一个索引,5.6 以后,虽然有了合并索引的优化方式,但是还是远远没有使用一个联合索引的查询方式好。
+5.6 版本之前,一个 sql 只能使用到一个表中的一个索引;5.6 以后,虽然有了合并索引的优化方式,但是还是远远没有使用一个联合索引的查询方式好。
### 每个 InnoDB 表必须有个主键
InnoDB 是一种索引组织表:数据的存储的逻辑顺序和索引的顺序是相同的。每个表都可以有多个索引,但是表的存储顺序只能有一种。
-InnoDB 是按照主键索引的顺序来组织表的
+InnoDB 是按照主键索引的顺序来组织表的。
-- 不要使用更新频繁的列作为主键,不使用多列主键(相当于联合索引)
-- 不要使用 UUID,MD5,HASH,字符串列作为主键(无法保证数据的顺序增长)
-- 主键建议使用自增 ID 值
+- 不要使用更新频繁的列作为主键,不使用多列主键(相当于联合索引)。
+- 不要使用 UUID、MD5、HASH、字符串列作为主键(无法保证数据的顺序增长)。
+- 主键建议使用自增 ID 值。
### 常见索引列建议
-- 出现在 SELECT、UPDATE、DELETE 语句的 WHERE 从句中的列
-- 包含在 ORDER BY、GROUP BY、DISTINCT 中的字段
-- 并不要将符合 1 和 2 中的字段的列都建立一个索引, 通常将 1、2 中的字段建立联合索引效果更好
-- 多表 join 的关联列
+- 出现在 SELECT、UPDATE、DELETE 语句的 WHERE 从句中的列。
+- 包含在 ORDER BY、GROUP BY、DISTINCT 中的字段。
+- 不要将符合 1 和 2 中的字段的列都建立一个索引,通常将 1、2 中的字段建立联合索引效果更好。
+- 多表 join 的关联列。
### 如何选择索引列的顺序
-建立索引的目的是:希望通过索引进行数据查找,减少随机 IO,增加查询性能 ,索引能过滤出越少的数据,则从磁盘中读入的数据也就越少。
+建立索引的目的是:希望通过索引进行数据查找,减少随机 IO,增加查询性能,索引能过滤出越少的数据,则从磁盘中读入的数据也就越少。
-- 区分度最高的放在联合索引的最左侧(区分度=列中不同值的数量/列的总行数)
-- 尽量把字段长度小的列放在联合索引的最左侧(因为字段长度越小,一页能存储的数据量越大,IO 性能也就越好)
-- 使用最频繁的列放到联合索引的左侧(这样可以比较少的建立一些索引)
+- **区分度最高的列放在联合索引的最左侧**:这是最重要的原则。区分度越高,通过索引筛选出的数据就越少,I/O 操作也就越少。计算区分度的方法是 `count(distinct column) / count(*)`。
+- **最频繁使用的列放在联合索引的左侧**:这符合最左前缀匹配原则。将最常用的查询条件列放在最左侧,可以最大程度地利用索引。
+- **字段长度**:字段长度对联合索引非叶子节点的影响很小,因为它存储了所有联合索引字段的值。字段长度主要影响主键和包含在其他索引中的字段的存储空间,以及这些索引的叶子节点的大小。因此,在选择联合索引列的顺序时,字段长度的优先级最低。对于主键和包含在其他索引中的字段,选择较短的字段长度可以节省存储空间和提高 I/O 性能。
### 避免建立冗余索引和重复索引(增加了查询优化器生成执行计划的时间)
-- 重复索引示例:primary key(id)、index(id)、unique index(id)
-- 冗余索引示例:index(a,b,c)、index(a,b)、index(a)
+- 重复索引示例:primary key(id)、index(id)、unique index(id)。
+- 冗余索引示例:index(a,b,c)、index(a,b)、index(a)。
-### 对于频繁的查询优先考虑使用覆盖索引
+### 对于频繁的查询,优先考虑使用覆盖索引
-> 覆盖索引:就是包含了所有查询字段 (where,select,order by,group by 包含的字段) 的索引
+> 覆盖索引:就是包含了所有查询字段 (where、select、order by、group by 包含的字段) 的索引
-**覆盖索引的好处:**
+**覆盖索引的好处**:
-- **避免 InnoDB 表进行索引的二次查询:** InnoDB 是以聚集索引的顺序来存储的,对于 InnoDB 来说,二级索引在叶子节点中所保存的是行的主键信息,如果是用二级索引查询数据的话,在查找到相应的键值后,还要通过主键进行二次查询才能获取我们真实所需要的数据。而在覆盖索引中,二级索引的键值中可以获取所有的数据,避免了对主键的二次查询 ,减少了 IO 操作,提升了查询效率。
-- **可以把随机 IO 变成顺序 IO 加快查询效率:** 由于覆盖索引是按键值的顺序存储的,对于 IO 密集型的范围查找来说,对比随机从磁盘读取每一行的数据 IO 要少的多,因此利用覆盖索引在访问时也可以把磁盘的随机读取的 IO 转变成索引查找的顺序 IO。
+- **避免 InnoDB 表进行索引的二次查询,也就是回表操作**:InnoDB 是以聚集索引的顺序来存储的,对于 InnoDB 来说,二级索引在叶子节点中所保存的是行的主键信息,如果是用二级索引查询数据的话,在查找到相应的键值后,还要通过主键进行二次查询才能获取我们真实所需要的数据。而在覆盖索引中,二级索引的键值中可以获取所有的数据,避免了对主键的二次查询(回表),减少了 IO 操作,提升了查询效率。
+- **可以把随机 IO 变成顺序 IO 加快查询效率**:由于覆盖索引是按键值的顺序存储的,对于 IO 密集型的范围查找来说,对比随机从磁盘读取每一行的数据 IO 要少的多,因此利用覆盖索引在访问时也可以把磁盘的随机读取的 IO 转变成索引查找的顺序 IO。
---
@@ -222,9 +222,9 @@ InnoDB 是按照主键索引的顺序来组织表的
**尽量避免使用外键约束**
-- 不建议使用外键约束(foreign key),但一定要在表与表之间的关联键上建立索引
-- 外键可用于保证数据的参照完整性,但建议在业务端实现
-- 外键会影响父表和子表的写操作从而降低性能
+- 不建议使用外键约束(foreign key),但一定要在表与表之间的关联键上建立索引。
+- 外键可用于保证数据的参照完整性,但建议在业务端实现。
+- 外键会影响父表和子表的写操作从而降低性能。
## 数据库 SQL 开发规范
@@ -238,7 +238,7 @@ InnoDB 是按照主键索引的顺序来组织表的
### 充分利用表上已经存在的索引
-避免使用双%号的查询条件。如:`a like '%123%'`,(如果无前置%,只有后置%,是可以用到列上的索引的)
+避免使用双%号的查询条件。如:`a like '%123%'`(如果无前置%,只有后置%,是可以用到列上的索引的)。
一个 SQL 只能利用到复合索引中的一列进行范围查询。如:有 a,b,c 列的联合索引,在查询条件中有 a 列的范围查询,则在 b,c 列上的索引将不会被用到。
@@ -248,18 +248,18 @@ InnoDB 是按照主键索引的顺序来组织表的
- `SELECT *` 会消耗更多的 CPU。
- `SELECT *` 无用字段增加网络带宽资源消耗,增加数据传输时间,尤其是大字段(如 varchar、blob、text)。
-- `SELECT *` 无法使用 MySQL 优化器覆盖索引的优化(基于 MySQL 优化器的“覆盖索引”策略又是速度极快,效率极高,业界极为推荐的查询优化方式)
-- `SELECT <字段列表>` 可减少表结构变更带来的影响、
+- `SELECT *` 无法使用 MySQL 优化器覆盖索引的优化(基于 MySQL 优化器的“覆盖索引”策略又是速度极快、效率极高、业界极为推荐的查询优化方式)。
+- `SELECT <字段列表>` 可减少表结构变更带来的影响。
### 禁止使用不含字段列表的 INSERT 语句
-如:
+**不推荐**:
```sql
insert into t values ('a','b','c');
```
-应使用:
+**推荐**:
```sql
insert into t(c1,c2,c3) values ('a','b','c');
@@ -273,7 +273,7 @@ insert into t(c1,c2,c3) values ('a','b','c');
### 避免数据类型的隐式转换
-隐式转换会导致索引失效如:
+隐式转换会导致索引失效,如:
```sql
select name,phone from customer where id = '111';
@@ -283,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 关联太多的表
@@ -293,7 +293,7 @@ select name,phone from customer where id = '111';
在 MySQL 中,对于同一个 SQL 多关联(join)一个表,就会多分配一个关联缓存,如果在一个 SQL 中关联的表越多,所占用的内存也就越大。
-如果程序中大量的使用了多表关联的操作,同时 join_buffer_size 设置的也不合理的情况下,就容易造成服务器内存溢出的情况,就会影响到服务器数据库性能的稳定性。
+如果程序中大量地使用了多表关联的操作,同时 join_buffer_size 设置得也不合理,就容易造成服务器内存溢出的情况,就会影响到服务器数据库性能的稳定性。
同时对于关联操作来说,会产生临时表操作,影响查询效率,MySQL 最多允许关联 61 个表,建议不超过 5 个。
@@ -303,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'
@@ -329,43 +329,43 @@ where create_time >= '20190101' and create_time < '20190102'
### 在明显不会有重复值时使用 UNION ALL 而不是 UNION
-- UNION 会把两个结果集的所有数据放到临时表中后再进行去重操作
-- UNION ALL 不会再对结果集进行去重操作
+- UNION 会把两个结果集的所有数据放到临时表中后再进行去重操作。
+- UNION ALL 不会再对结果集进行去重操作。
### 拆分复杂的大 SQL 为多个小 SQL
-- 大 SQL 逻辑上比较复杂,需要占用大量 CPU 进行计算的 SQL
-- MySQL 中,一个 SQL 只能使用一个 CPU 进行计算
-- SQL 拆分后可以通过并行执行来提高处理效率
+- 大 SQL 逻辑上比较复杂,需要占用大量 CPU 进行计算的 SQL。
+- MySQL 中,一个 SQL 只能使用一个 CPU 进行计算。
+- SQL 拆分后可以通过并行执行来提高处理效率。
### 程序连接不同的数据库使用不同的账号,禁止跨库查询
-- 为数据库迁移和分库分表留出余地
-- 降低业务耦合度
-- 避免权限过大而产生的安全风险
+- 为数据库迁移和分库分表留出余地。
+- 降低业务耦合度。
+- 避免权限过大而产生的安全风险。
## 数据库操作行为规范
-### 超 100 万行的批量写 (UPDATE,DELETE,INSERT) 操作,要分批多次进行操作
+### 超 100 万行的批量写 (UPDATE、DELETE、INSERT) 操作,要分批多次进行操作
**大批量操作可能会造成严重的主从延迟**
-主从环境中,大批量操作可能会造成严重的主从延迟,大批量的写操作一般都需要执行一定长的时间,而只有当主库上执行完成后,才会在其他从库上执行,所以会造成主库与从库长时间的延迟情况
+主从环境中,大批量操作可能会造成严重的主从延迟,大批量的写操作一般都需要执行一定长的时间,而只有当主库上执行完成后,才会在其他从库上执行,所以会造成主库与从库长时间的延迟情况。
**binlog 日志为 row 格式时会产生大量的日志**
-大批量写操作会产生大量日志,特别是对于 row 格式二进制数据而言,由于在 row 格式中会记录每一行数据的修改,我们一次修改的数据越多,产生的日志量也就会越多,日志的传输和恢复所需要的时间也就越长,这也是造成主从延迟的一个原因
+大批量写操作会产生大量日志,特别是对于 row 格式二进制数据而言,由于在 row 格式中会记录每一行数据的修改,我们一次修改的数据越多,产生的日志量也就会越多,日志的传输和恢复所需要的时间也就越长,这也是造成主从延迟的一个原因。
**避免产生大事务操作**
大批量修改数据,一定是在一个事务中进行的,这就会造成表中大批量数据进行锁定,从而导致大量的阻塞,阻塞会对 MySQL 的性能产生非常大的影响。
-特别是长时间的阻塞会占满所有数据库的可用连接,这会使生产环境中的其他应用无法连接到数据库,因此一定要注意大批量写操作要进行分批
+特别是长时间的阻塞会占满所有数据库的可用连接,这会使生产环境中的其他应用无法连接到数据库,因此一定要注意大批量写操作要进行分批。
### 对于大表使用 pt-online-schema-change 修改表结构
-- 避免大表修改产生的主从延迟
-- 避免在对表字段进行修改时进行锁表
+- 避免大表修改产生的主从延迟。
+- 避免在对表字段进行修改时进行锁表。
对大表数据结构的修改一定要谨慎,会造成严重的锁表操作,尤其是生产环境,是不能容忍的。
@@ -373,13 +373,13 @@ pt-online-schema-change 它会首先建立一个与原表结构相同的新表
### 禁止为程序使用的账号赋予 super 权限
-- 当达到最大连接数限制时,还运行 1 个有 super 权限的用户连接
-- super 权限只能留给 DBA 处理问题的账号使用
+- 当达到最大连接数限制时,还运行 1 个有 super 权限的用户连接。
+- super 权限只能留给 DBA 处理问题的账号使用。
-### 对于程序连接数据库账号,遵循权限最小原则
+### 对于程序连接数据库账号,遵循权限最小原则
-- 程序使用数据库账号只能在一个 DB 下使用,不准跨库
-- 程序使用的账号原则上不准有 drop 权限
+- 程序使用数据库账号只能在一个 DB 下使用,不准跨库。
+- 程序使用的账号原则上不准有 drop 权限。
## 推荐阅读
diff --git a/docs/database/mysql/mysql-index.md b/docs/database/mysql/mysql-index.md
index a842db8453e..d035d4729f1 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,31 +15,37 @@ tag:
**索引是一种用于快速查询和检索数据的数据结构,其本质可以看成是一种排序好的数据结构。**
-索引的作用就相当于书的目录。打个比方: 我们在查字典的时候,如果没有目录,那我们就只能一页一页的去找我们需要查的那个字,速度很慢。如果有目录了,我们只需要先去目录里查找字的位置,然后直接翻到那一页就行了。
+索引的作用就相当于书的目录。打个比方:我们在查字典的时候,如果没有目录,那我们就只能一页一页地去找我们需要查的那个字,速度很慢;如果有目录了,我们只需要先去目录里查找字的位置,然后直接翻到那一页就行了。
-索引底层数据结构存在很多种类型,常见的索引结构有: B 树, B+树 和 Hash、红黑树。在 MySQL 中,无论是 Innodb 还是 MyIsam,都使用了 B+树作为索引结构。
+索引底层数据结构存在很多种类型,常见的索引结构有:B 树、 B+ 树 和 Hash、红黑树。在 MySQL 中,无论是 Innodb 还是 MyISAM,都使用了 B+ 树作为索引结构。
## 索引的优缺点
-**优点**:
+**索引的优点:**
-- 使用索引可以大大加快 数据的检索速度(大大减少检索的数据量), 这也是创建索引的最主要的原因。
-- 通过创建唯一性索引,可以保证数据库表中每一行数据的唯一性。
+1. **查询速度起飞 (主要目的)**:通过索引,数据库可以**大幅减少需要扫描的数据量**,直接定位到符合条件的记录,从而显著加快数据检索速度,减少磁盘 I/O 次数。
+2. **保证数据唯一性**:通过创建**唯一索引 (Unique Index)**,可以确保表中的某一列(或几列组合)的值是独一无二的,比如用户ID、邮箱等。主键本身就是一种唯一索引。
+3. **加速排序和分组**:如果查询中的 ORDER BY 或 GROUP BY 子句涉及的列建有索引,数据库往往可以直接利用索引已经排好序的特性,避免额外的排序操作,从而提升性能。
-**缺点**:
+**索引的缺点:**
+
+1. **创建和维护耗时**:创建索引本身需要时间,特别是对大表操作时。更重要的是,当对表中的数据进行**增、删、改 (DML操作)** 时,不仅要操作数据本身,相关的索引也必须动态更新和维护,这会**降低这些 DML 操作的执行效率**。
+2. **占用存储空间**:索引本质上也是一种数据结构,需要以物理文件(或内存结构)的形式存储,因此会**额外占用一定的磁盘空间**。索引越多、越大,占用的空间也就越多。
+3. **可能被误用或失效**:如果索引设计不当,或者查询语句写得不好,数据库优化器可能不会选择使用索引(或者选错索引),反而导致性能下降。
-- 创建索引和维护索引需要耗费许多时间。当对表中的数据进行增删改的时候,如果数据有索引,那么索引也需要动态的修改,会降低 SQL 执行效率。
-- 索引需要使用物理文件存储,也会耗费一定空间。
+**那么,用了索引就一定能提高查询性能吗?**
-但是,**使用索引一定能提高查询性能吗?**
+**不一定。** 大多数情况下,合理使用索引确实比全表扫描快得多。但也有例外:
-大多数情况下,索引查询都是比全表扫描要快的。但是如果数据库的数据量不大,那么使用索引也不一定能够带来很大提升。
+- **数据量太小**:如果表里的数据非常少(比如就几百条),全表扫描可能比通过索引查找更快,因为走索引本身也有开销。
+- **查询结果集占比过大**:如果要查询的数据占了整张表的大部分(比如超过20%-30%),优化器可能会认为全表扫描更划算,因为通过索引多次回表(随机I/O)的成本可能高于一次顺序的全表扫描。
+- **索引维护不当或统计信息过时**:导致优化器做出错误判断。
## 索引底层数据结构选型
### Hash 表
-哈希表是键值对的集合,通过键(key)即可快速取出对应的值(value),因此哈希表可以快速检索数据(接近 O(1))。
+哈希表是键值对的集合,通过键(key)即可快速取出对应的值(value),因此哈希表可以快速检索数据(接近 O(1))。
**为何能够通过 key 快速取出 value 呢?** 原因在于 **哈希算法**(也叫散列算法)。通过哈希算法,我们可以快速找到 key 对应的 index,找到了 index 也就找到了对应的 value。
@@ -50,7 +56,7 @@ index = hash % array_size

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

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

@@ -88,11 +94,11 @@ SELECT * FROM tb1 WHERE id < 500;
AVL 树是计算机科学中最早被发明的自平衡二叉查找树,它的名称来自于发明者 G.M. Adelson-Velsky 和 E.M. Landis 的名字缩写。AVL 树的特点是保证任何节点的左右子树高度之差不超过 1,因此也被称为高度平衡二叉树,它的查找、插入和删除在平均和最坏情况下的时间复杂度都是 O(logn)。
-
+
AVL 树采用了旋转操作来保持平衡。主要有四种旋转操作:LL 旋转、RR 旋转、LR 旋转和 RL 旋转。其中 LL 旋转和 RR 旋转分别用于处理左左和右右失衡,而 LR 旋转和 RL 旋转则用于处理左右和右左失衡。
-由于 AVL 树需要频繁地进行旋转操作来保持平衡,因此会有较大的计算开销进而降低了查询性能。并且, 在使用 AVL 树时,每个树节点仅存储一个数据,而每次进行磁盘 IO 时只能读取一个节点的数据,如果需要查询的数据分布在多个节点上,那么就需要进行多次磁盘 IO。 **磁盘 IO 是一项耗时的操作,在设计数据库索引时,我们需要优先考虑如何最大限度地减少磁盘 IO 操作的次数。**
+由于 AVL 树需要频繁地进行旋转操作来保持平衡,因此会有较大的计算开销进而降低了数据库写操作的性能。并且, 在使用 AVL 树时,每个树节点仅存储一个数据,而每次进行磁盘 IO 时只能读取一个节点的数据,如果需要查询的数据分布在多个节点上,那么就需要进行多次磁盘 IO。**磁盘 IO 是一项耗时的操作,在设计数据库索引时,我们需要优先考虑如何最大限度地减少磁盘 IO 操作的次数。**
实际应用中,AVL 树使用的并不多。
@@ -104,7 +110,7 @@ AVL 树采用了旋转操作来保持平衡。主要有四种旋转操作:LL
2. 根节点总是黑色的;
3. 每个叶子节点都是黑色的空节点(NIL 节点);
4. 如果节点是红色的,则它的子节点必须是黑色的(反之不一定);
-5. 从根节点到叶节点或空子节点的每条路径,必须包含相同数目的黑色节点(即相同的黑色高度)。
+5. 从任意节点到它的叶子节点或空子节点的每条路径,必须包含相同数目的黑色节点(即相同的黑色高度)。

@@ -112,26 +118,26 @@ AVL 树采用了旋转操作来保持平衡。主要有四种旋转操作:LL
**红黑树的应用还是比较广泛的,TreeMap、TreeSet 以及 JDK1.8 的 HashMap 底层都用到了红黑树。对于数据在内存中的这种情况来说,红黑树的表现是非常优异的。**
-### B 树& B+树
+### B 树& B+ 树
-B 树也称 B-树,全称为 **多路平衡查找树** ,B+ 树是 B 树的一种变体。B 树和 B+树中的 B 是 `Balanced` (平衡)的意思。
+B 树也称 B- 树,全称为 **多路平衡查找树**,B+ 树是 B 树的一种变体。B 树和 B+ 树中的 B 是 `Balanced`(平衡)的意思。
目前大部分数据库系统及文件系统都采用 B-Tree 或其变种 B+Tree 作为索引结构。
-**B 树& B+树两者有何异同呢?**
+**B 树& B+ 树两者有何异同呢?**
-- B 树的所有节点既存放键(key) 也存放数据(data),而 B+树只有叶子节点存放 key 和 data,其他内节点只存放 key。
-- B 树的叶子节点都是独立的;B+树的叶子节点有一条引用链指向与它相邻的叶子节点。
-- B 树的检索的过程相当于对范围内的每个节点的关键字做二分查找,可能还没有到达叶子节点,检索就结束了。而 B+树的检索效率就很稳定了,任何查找都是从根节点到叶子节点的过程,叶子节点的顺序检索很明显。
-- 在 B 树中进行范围查询时,首先找到要查找的下限,然后对 B 树进行中序遍历,直到找到查找的上限;而 B+树的范围查询,只需要对链表进行遍历即可。
+- B 树的所有节点既存放键(key)也存放数据(data),而 B+ 树只有叶子节点存放 key 和 data,其他内节点只存放 key。
+- B 树的叶子节点都是独立的;B+ 树的叶子节点有一条引用链指向与它相邻的叶子节点。
+- B 树的检索的过程相当于对范围内的每个节点的关键字做二分查找,可能还没有到达叶子节点,检索就结束了。而 B+ 树的检索效率就很稳定了,任何查找都是从根节点到叶子节点的过程,叶子节点的顺序检索很明显。
+- 在 B 树中进行范围查询时,首先找到要查找的下限,然后对 B 树进行中序遍历,直到找到查找的上限;而 B+ 树的范围查询,只需要对链表进行遍历即可。
-综上,B+树与 B 树相比,具备更少的 IO 次数、更稳定的查询效率和更适于范围查询这些优势。
+综上,B+ 树与 B 树相比,具备更少的 IO 次数、更稳定的查询效率和更适于范围查询这些优势。
在 MySQL 中,MyISAM 引擎和 InnoDB 引擎都是使用 B+Tree 作为索引结构,但是,两者的实现方式不太一样。(下面的内容整理自《Java 工程师修炼之道》)
> MyISAM 引擎中,B+Tree 叶节点的 data 域存放的是数据记录的地址。在索引检索的时候,首先按照 B+Tree 搜索算法搜索索引,如果指定的 Key 存在,则取出其 data 域的值,然后以 data 域的值为地址读取相应的数据记录。这被称为“**非聚簇索引(非聚集索引)**”。
>
-> InnoDB 引擎中,其数据文件本身就是索引文件。相比 MyISAM,索引文件和数据文件是分离的,其表数据文件本身就是按 B+Tree 组织的一个索引结构,树的叶节点 data 域保存了完整的数据记录。这个索引的 key 是数据表的主键,因此 InnoDB 表数据文件本身就是主索引。这被称为“**聚簇索引(聚集索引)**”,而其余的索引都作为 **辅助索引** ,辅助索引的 data 域存储相应记录主键的值而不是地址,这也是和 MyISAM 不同的地方。在根据主索引搜索时,直接找到 key 所在的节点即可取出数据;在根据辅助索引查找时,则需要先取出主键的值,再走一遍主索引。 因此,在设计表的时候,不建议使用过长的字段作为主键,也不建议使用非单调的字段作为主键,这样会造成主索引频繁分裂。
+> InnoDB 引擎中,其数据文件本身就是索引文件。相比 MyISAM,索引文件和数据文件是分离的,其表数据文件本身就是按 B+Tree 组织的一个索引结构,树的叶节点 data 域保存了完整的数据记录。这个索引的 key 是数据表的主键,因此 InnoDB 表数据文件本身就是主索引。这被称为“**聚簇索引(聚集索引)**”,而其余的索引都作为 **辅助索引**,辅助索引的 data 域存储相应记录主键的值而不是地址,这也是和 MyISAM 不同的地方。在根据主索引搜索时,直接找到 key 所在的节点即可取出数据;在根据辅助索引查找时,则需要先取出主键的值,再走一遍主索引。 因此,在设计表的时候,不建议使用过长的字段作为主键,也不建议使用非单调的字段作为主键,这样会造成主索引频繁分裂。
## 索引类型总结
@@ -140,12 +146,12 @@ B 树也称 B-树,全称为 **多路平衡查找树** ,B+ 树是 B 树的一
- BTree 索引:MySQL 里默认和最常用的索引类型。只有叶子节点存储 value,非叶子节点只有指针和 key。存储引擎 MyISAM 和 InnoDB 实现 BTree 索引都是使用 B+Tree,但二者实现方式不一样(前面已经介绍了)。
- 哈希索引:类似键值对的形式,一次即可定位。
- RTree 索引:一般不会使用,仅支持 geometry 数据类型,优势在于范围查找,效率较低,通常使用搜索引擎如 ElasticSearch 代替。
-- 全文索引:对文本的内容进行分词,进行搜索。目前只有 `CHAR`、`VARCHAR` ,`TEXT` 列上可以创建全文索引。一般不会使用,效率较低,通常使用搜索引擎如 ElasticSearch 代替。
+- 全文索引:对文本的内容进行分词,进行搜索。目前只有 `CHAR`、`VARCHAR`、`TEXT` 列上可以创建全文索引。一般不会使用,效率较低,通常使用搜索引擎如 ElasticSearch 代替。
按照底层存储方式角度划分:
- 聚簇索引(聚集索引):索引结构和数据一起存放的索引,InnoDB 中的主键索引就属于聚簇索引。
-- 非聚簇索引(非聚集索引):索引结构和数据分开存放的索引,二级索引(辅助索引)就属于非聚簇索引。MySQL 的 MyISAM 引擎,不管主键还是非主键,使用的都是非聚簇索引。
+- 非聚簇索引(非聚集索引):索引结构和数据分开存放的索引,二级索引(辅助索引)就属于非聚簇索引。MySQL 的 MyISAM 引擎,不管主键还是非主键,使用的都是非聚簇索引。
按照应用维度划分:
@@ -154,7 +160,8 @@ B 树也称 B-树,全称为 **多路平衡查找树** ,B+ 树是 B 树的一
- 唯一索引:加速查询 + 列值唯一(可以有 NULL)。
- 覆盖索引:一个索引包含(或者说覆盖)所有需要查询的字段的值。
- 联合索引:多列值组成一个索引,专门用于组合搜索,其效率大于索引合并。
-- 全文索引:对文本的内容进行分词,进行搜索。目前只有 `CHAR`、`VARCHAR` ,`TEXT` 列上可以创建全文索引。一般不会使用,效率较低,通常使用搜索引擎如 ElasticSearch 代替。
+- 全文索引:对文本的内容进行分词,进行搜索。目前只有 `CHAR`、`VARCHAR`、`TEXT` 列上可以创建全文索引。一般不会使用,效率较低,通常使用搜索引擎如 ElasticSearch 代替。
+- 前缀索引:对文本的前几个字符创建索引,相比普通索引建立的数据更小,因为只取前几个字符。
MySQL 8.x 中实现的索引新特性:
@@ -162,7 +169,7 @@ MySQL 8.x 中实现的索引新特性:
- 降序索引:之前的版本就支持通过 desc 来指定索引为降序,但实际上创建的仍然是常规的升序索引。直到 MySQL 8.x 版本才开始真正支持降序索引。另外,在 MySQL 8.x 版本中,不再对 GROUP BY 语句进行隐式排序。
- 函数索引:从 MySQL 8.0.13 版本开始支持在索引中使用函数或者表达式的值,也就是在索引中可以包含函数或者表达式。
-## 主键索引(Primary Key)
+## 主键索引(Primary Key)
数据表的主键列使用的就是主键索引。
@@ -174,19 +181,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 也支持了全文索引。
-二级索引:
+二级索引:

@@ -196,27 +202,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 引擎,不管主键还是非主键,使用的都是非聚簇索引。
非聚簇索引的叶子节点并不一定存放数据的指针,因为二级索引的叶子节点就存放的是主键,根据主键再回表查数据。
@@ -224,22 +230,22 @@ PS: 不懂的同学可以暂存疑,慢慢往下看,后面会有答案的,
**优点**:
-更新代价比聚簇索引要小 。非聚簇索引的更新代价就没有聚簇索引那么大了,非聚簇索引的叶子节点是不存放数据的
+更新代价比聚簇索引要小。非聚簇索引的更新代价就没有聚簇索引那么大了,非聚簇索引的叶子节点是不存放数据的。
**缺点**:
-- **依赖于有序的数据**:跟聚簇索引一样,非聚簇索引也依赖于有序的数据
-- **可能会二次查询(回表)**:这应该是非聚簇索引最大的缺点了。 当查到索引对应的指针或主键后,可能还需要根据指针或主键再到数据文件或表中查询。
+- **依赖于有序的数据**:跟聚簇索引一样,非聚簇索引也依赖于有序的数据。
+- **可能会二次查询(回表)**:这应该是非聚簇索引最大的缺点了。当查到索引对应的指针或主键后,可能还需要根据指针或主键再到数据文件或表中查询。
-这是 MySQL 的表的文件截图:
+这是 MySQL 的表的文件截图:

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

-#### 非聚簇索引一定回表查询吗(覆盖索引)?
+#### 非聚簇索引一定回表查询吗(覆盖索引)?
**非聚簇索引不一定回表查询。**
@@ -251,7 +257,7 @@ PS: 不懂的同学可以暂存疑,慢慢往下看,后面会有答案的,
那么这个索引的 key 本身就是 name,查到对应的 name 直接返回就行了,无需回表查询。
-即使是 MYISAM 也是这样,虽然 MYISAM 的主键索引确实需要回表,因为它的主键索引的叶子节点存放的是指针。但是!**如果 SQL 查的就是主键呢?**
+即使是 MyISAM 也是这样,虽然 MyISAM 的主键索引确实需要回表,因为它的主键索引的叶子节点存放的是指针。但是!**如果 SQL 查的就是主键呢?**
```sql
SELECT id FROM table WHERE id=1;
@@ -263,7 +269,9 @@ SELECT id FROM table WHERE id=1;
### 覆盖索引
-如果一个索引包含(或者说覆盖)所有需要查询的字段的值,我们就称之为 **覆盖索引(Covering Index)** 。我们知道在 InnoDB 存储引擎中,如果不是主键索引,叶子节点存储的是主键+列值。最终还是要“回表”,也就是要通过主键再查找一次,这样就会比较慢。而覆盖索引就是把要查询出的列和索引是对应的,不做回表操作!
+如果一个索引包含(或者说覆盖)所有需要查询的字段的值,我们就称之为 **覆盖索引(Covering Index)**。
+
+在 InnoDB 存储引擎中,非主键索引的叶子节点包含的是主键的值。这意味着,当使用非主键索引进行查询时,数据库会先找到对应的主键值,然后再通过主键索引来定位和检索完整的行数据。这个过程被称为“回表”。
**覆盖索引即需要查询的字段正好是索引的字段,那么直接根据该索引,就可以查到数据了,而无需回表查询。**
@@ -274,7 +282,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` (
@@ -314,10 +322,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`,我们发现是没有用到覆盖索引的。

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

-通过 `Extra` 这一列的 `Using index` ,说明这条 SQL 语句成功使用了覆盖索引。
+通过 `Extra` 这一列的 `Using index`,说明这条 SQL 语句成功使用了覆盖索引。
关于 `EXPLAIN` 命令的详细介绍请看:[MySQL 执行计划分析](./mysql-query-execution-plan.md)这篇文章。
@@ -349,19 +358,114 @@ 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 语句。
+
+
+
+```sql
+# 可以命中索引
+SELECT * FROM student WHERE name = 'Anne Henry';
+EXPLAIN SELECT * FROM student WHERE name = 'Anne Henry' AND class = 'lIrm08RYVk';
+# 无法命中索引
+SELECT * FROM student WHERE class = 'lIrm08RYVk';
+```
+
+再来看一个常见的面试题:如果有索引 `联合索引(a,b,c)`,查询 `a=1 AND c=1` 会走索引么?`c=1` 呢?`b=1 AND c=1` 呢? `b = 1 AND a = 1 AND c = 1` 呢?
+
+先不要往下看答案,给自己 3 分钟时间想一想。
+
+1. 查询 `a=1 AND c=1`:根据最左前缀匹配原则,查询可以使用索引的前缀部分。因此,该查询仅在 `a=1` 上使用索引,然后对结果进行 `c=1` 的过滤。
+2. 查询 `c=1`:由于查询中不包含最左列 `a`,根据最左前缀匹配原则,整个索引都无法被使用。
+3. 查询 `b=1 AND c=1`:和第二种一样的情况,整个索引都不会使用。
+4. 查询 `b=1 AND a=1 AND c=1`:这个查询是可以用到索引的。查询优化器分析 SQL 语句时,对于联合索引,会对查询条件进行重排序,以便用到索引。会将 `b=1` 和 `a=1` 的条件进行重排序,变成 `a=1 AND b=1 AND c=1`。
-相关阅读:[联合索引的最左匹配原则全网都在说的一个错误结论](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`。这样,只有同时满足条件的记录才会被返回,减少了回表次数。
+
+
+
+
+
+再来讲讲索引下推的具体原理,先看下面这张 MySQL 简要架构图。
+
+
+
+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 条件查询的字段,应该被考虑建立索引。
- **频繁需要排序的字段**:索引已经排序,这样查询可以利用索引的排序,加快排序查询时间。
@@ -373,7 +477,7 @@ ALTER TABLE `cus_order` ADD INDEX id_score_name(score, name);
### 限制每张表上的索引数量
-索引并不是越多越好,建议单张表索引不超过 5 个!索引可以提高效率同样可以降低效率。
+索引并不是越多越好,建议单张表索引不超过 5 个!索引可以提高效率,同样可以降低效率。
索引可以增加查询效率,但同样也会降低插入和更新的效率,甚至有些情况下会降低查询效率。
@@ -381,11 +485,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)这两个索引就是冗余索引,能够命中前者的查询肯定是能够命中后者的。在大多数情况下,都应该尽量扩展已有的索引而不是创建新索引。
### 字符串类型的字段使用前缀索引代替普通索引
@@ -395,13 +499,13 @@ ALTER TABLE `cus_order` ADD INDEX id_score_name(score, name);
索引失效也是慢查询的主要原因之一,常见的导致索引失效的情况有下面这些:
-- ~~使用 `SELECT *` 进行查询;~~ `SELECT *` 不会直接导致索引失效(如果不走索引大概率是因为 where 查询范围过大导致的),但它可能会带来一些其他的性能问题比如造成网络传输和数据处理的浪费、无法使用索引覆盖;
-- 创建了组合索引,但查询条件未准守最左匹配原则;
-- 在索引列上进行计算、函数、类型转换等操作;
-- 以 % 开头的 LIKE 查询比如 `LIKE '%abc';`;
-- 查询条件中使用 OR,且 OR 的前后条件中有一个列没有索引,涉及的索引都不会被使用到;
-- IN 的取值范围较大时会导致索引失效,走全表扫描(NOT IN 和 IN 的失效场景相同);
-- 发生[隐式转换](https://javaguide.cn/database/mysql/index-invalidation-caused-by-implicit-conversion.html);
+- ~~使用 `SELECT *` 进行查询;~~ `SELECT *` 不会直接导致索引失效(如果不走索引大概率是因为 where 查询范围过大导致的),但它可能会带来一些其他的性能问题比如造成网络传输和数据处理的浪费、无法使用索引覆盖;
+- 创建了组合索引,但查询条件未遵守最左匹配原则;
+- 在索引列上进行计算、函数、类型转换等操作;
+- 以 % 开头的 LIKE 查询比如 `LIKE '%abc';`;
+- 查询条件中使用 OR,且 OR 的前后条件中有一个列没有索引,涉及的索引都不会被使用到;
+- IN 的取值范围较大时会导致索引失效,走全表扫描(NOT IN 和 IN 的失效场景相同);
+- 发生[隐式转换](https://javaguide.cn/database/mysql/index-invalidation-caused-by-implicit-conversion.html);
- ……
推荐阅读这篇文章:[美团暑期实习一面:MySQl 索引失效的场景有哪些?](https://mp.weixin.qq.com/s/mwME3qukHBFul57WQLkOYg)。
@@ -412,7 +516,7 @@ ALTER TABLE `cus_order` ADD INDEX id_score_name(score, name);
MySQL 5.7 可以通过查询 `sys` 库的 `schema_unused_indexes` 视图来查询哪些索引从未被使用。
-### 知道如何分析语句是否走索引查询
+### 知道如何分析 SQL 语句是否走索引查询
我们可以使用 `EXPLAIN` 命令来分析 SQL 的 **执行计划** ,这样就知道语句是否命中索引了。执行计划是指一条 SQL 语句在经过 MySQL 查询优化器的优化会后,具体的执行方式。
diff --git a/docs/database/mysql/mysql-logs.md b/docs/database/mysql/mysql-logs.md
index 49a9da118d8..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(回滚日志)。

-今天就来聊聊 `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 恢复数据,保证数据的持久性与完整性。

-`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 文件里。

@@ -64,15 +64,15 @@ InnoDB 将 redo log 刷到磁盘上有几种情况:
刷盘策略`innodb_flush_log_at_trx_commit` 的默认值为 1,设置为 1 的时候才不会丢失任何数据。为了保证事务的持久性,我们必须将其设置为 1。
-另外,`InnoDB` 存储引擎有一个后台线程,每隔`1` 秒,就会把 `redo log buffer` 中的内容写到文件系统缓存(`page cache`),然后调用 `fsync` 刷盘。
+另外,InnoDB 存储引擎有一个后台线程,每隔`1` 秒,就会把 `redo log buffer` 中的内容写到文件系统缓存(`page cache`),然后调用 `fsync` 刷盘。

-也就是说,一个没有提交事务的 `redo log` 记录,也可能会刷盘。
+也就是说,一个没有提交事务的 redo log 记录,也可能会刷盘。
**为什么呢?**
-因为在事务执行过程 `redo log` 记录是会写入`redo log buffer` 中,这些 `redo log` 记录会被后台线程刷盘。
+因为在事务执行过程 redo log 记录是会写入`redo log buffer` 中,这些 redo log 记录会被后台线程刷盘。

@@ -84,15 +84,15 @@ InnoDB 将 redo log 刷到磁盘上有几种情况:

-为`0`时,如果`MySQL`挂了或宕机可能会有`1`秒数据的丢失。
+为`0`时,如果 MySQL 挂了或宕机可能会有`1`秒数据的丢失。
#### innodb_flush_log_at_trx_commit=1

-为`1`时, 只要事务提交成功,`redo log`记录就一定在硬盘里,不会有任何数据丢失。
+为`1`时, 只要事务提交成功,redo log 记录就一定在硬盘里,不会有任何数据丢失。
-如果事务执行期间`MySQL`挂了或宕机,这部分日志丢了,但是事务并没有提交,所以日志丢了也不会有损失。
+如果事务执行期间 MySQL 挂了或宕机,这部分日志丢了,但是事务并没有提交,所以日志丢了也不会有损失。
#### innodb_flush_log_at_trx_commit=2
@@ -100,32 +100,32 @@ InnoDB 将 redo log 刷到磁盘上有几种情况:
为`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`的内容。
它采用的是环形数组形式,从头开始写,写到末尾又回到头循环写,如下图所示。

-在个**日志文件组**中还有两个重要的属性,分别是 `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 记录。

-如果 `write pos` 追上 `checkpoint` ,表示**日志文件组**满了,这时候不能再写入新的 `redo log` 记录,`MySQL` 得停下来,清空一些记录,把 `checkpoint` 推进一下。
+如果 `write pos` 追上 `checkpoint` ,表示**日志文件组**满了,这时候不能再写入新的 redo log 记录,MySQL 得停下来,清空一些记录,把 `checkpoint` 推进一下。

@@ -172,9 +172,9 @@ MySQL830 mysql:8.0.32
### redo log 小结
-相信大家都知道 `redo log` 的作用和它的刷盘时机、存储形式。
+相信大家都知道 redo log 的作用和它的刷盘时机、存储形式。
-现在我们来思考一个问题:**只要每次把修改后的数据页直接刷盘不就好了,还有 `redo log` 什么事?**
+现在我们来思考一个问题:**只要每次把修改后的数据页直接刷盘不就好了,还有 redo log 什么事?**
它们不都是刷盘么?差别在哪里?
@@ -190,32 +190,32 @@ MySQL830 mysql:8.0.32
而且数据页刷盘是随机写,因为一个数据页对应的位置可能在硬盘文件的随机位置,所以性能是很差。
-如果是写 `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 来同步数据,保证数据一致性。

-`binlog`会记录所有涉及更新数据的逻辑操作,并且是顺序写。
+binlog 会记录所有涉及更新数据的逻辑操作,并且是顺序写。
### 记录格式
-`binlog` 日志有三种格式,可以通过`binlog_format`参数指定。
+binlog 日志有三种格式,可以通过`binlog_format`参数指定。
- **statement**
- **row**
@@ -237,21 +237,21 @@ MySQL830 mysql:8.0.32
这样就能保证同步数据的一致性,通常情况下都是指定为`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 日志刷盘流程如下

@@ -272,57 +272,63 @@ MySQL830 mysql:8.0.32

-在出现`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 的写入时机不一样。

-回到正题,`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 日志写期间发生了异常,会出现什么情况呢?

-由于`binlog`没写完就异常,这时候`binlog`里面没有对应的修改记录。因此,之后用`binlog`日志恢复数据时,就会少这一次更新,恢复出来的这一行`c`值是`0`,而原库因为`redo log`日志恢复,这一行`c`值是`1`,最终数据不一致。
+由于 binlog 没写完就异常,这时候 binlog 里面没有对应的修改记录。因此,之后用 binlog 日志恢复数据时,就会少这一次更新,恢复出来的这一行`c`值是`0`,而原库因为 redo log 日志恢复,这一行`c`值是`1`,最终数据不一致。

-为了解决两份日志之间的逻辑一致问题,`InnoDB`存储引擎使用**两阶段提交**方案。
+为了解决两份日志之间的逻辑一致问题,InnoDB 存储引擎使用**两阶段提交**方案。
-原理很简单,将`redo log`的写入拆成了两个步骤`prepare`和`commit`,这就是**两阶段提交**。
+原理很简单,将 redo log 的写入拆成了两个步骤`prepare`和`commit`,这就是**两阶段提交**。

-使用**两阶段提交**后,写入`binlog`时发生异常也不会有影响,因为`MySQL`根据`redo log`日志恢复数据时,发现`redo log`还处于`prepare`阶段,并且没有对应`binlog`日志,就会回滚该事务。
+使用**两阶段提交**后,写入 binlog 时发生异常也不会有影响,因为 MySQL 根据 redo log 日志恢复数据时,发现 redo log 还处于`prepare`阶段,并且没有对应 binlog 日志,就会回滚该事务。

-再看一个场景,`redo log`设置`commit`阶段发生异常,那会不会回滚事务呢?
+再看一个场景,redo log 设置`commit`阶段发生异常,那会不会回滚事务呢?

-并不会回滚事务,它会执行上图框住的逻辑,虽然`redo log`是处于`prepare`阶段,但是能通过事务`id`找到对应的`binlog`日志,所以`MySQL`认为是完整的,就会提交事务恢复数据。
+并不会回滚事务,它会执行上图框住的逻辑,虽然 redo log 是处于`prepare`阶段,但是能通过事务`id`找到对应的 binlog 日志,所以 MySQL 认为是完整的,就会提交事务恢复数据。
## undo log
> 这部分内容为 JavaGuide 的补充:
-我们知道如果想要保证事务的原子性,就需要在异常发生时,对已经执行的操作进行**回滚**,在 MySQL 中,恢复机制是通过 **回滚日志(undo log)** 实现的,所有事务进行的修改都会先记录到这个回滚日志中,然后再执行相关的操作。如果执行过程中遇到异常的话,我们直接利用 **回滚日志** 中的信息将数据回滚到修改之前的样子即可!并且,回滚日志会先于数据持久化到磁盘上。这样就保证了即使遇到数据库突然宕机等情况,当用户再次启动数据库的时候,数据库还能够通过查询回滚日志来回滚将之前未完成的事务。
+每一个事务对数据的修改都会被记录到 undo log ,当执行事务过程中出现错误或者需要执行回滚操作的话,MySQL 可以利用 undo log 将数据恢复到事务开始之前的状态。
-另外,`MVCC` 的实现依赖于:**隐藏字段、Read View、undo log**。在内部实现中,`InnoDB` 通过数据行的 `DB_TRX_ID` 和 `Read View` 来判断数据的可见性,如不可见,则通过数据行的 `DB_ROLL_PTR` 找到 `undo log` 中的历史版本。每个事务读到的数据版本可能是不一样的,在同一个事务中,用户只能看到该事务创建 `Read View` 之前已经提交的修改和该事务本身做的修改
+undo log 属于逻辑日志,记录的是 SQL 语句,比如说事务执行一条 DELETE 语句,那 undo log 就会记录一条相对应的 INSERT 语句。同时,undo log 的信息也会被记录到 redo log 中,因为 undo log 也要实现持久性保护。并且,undo-log 本身是会被删除清理的,例如 INSERT 操作,在事务提交之后就可以清除掉了;UPDATE/DELETE 操作在事务提交不会立即删除,会加入 history list,由后台线程 purge 进行清理。
+
+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` 之前已经提交的修改和该事务本身做的修改
## 总结
@@ -330,7 +336,7 @@ MySQL830 mysql:8.0.32
MySQL InnoDB 引擎使用 **redo log(重做日志)** 保证事务的**持久性**,使用 **undo log(回滚日志)** 来保证事务的**原子性**。
-`MySQL`数据库的**数据备份、主备、主主、主从**都离不开`binlog`,需要依靠`binlog`来同步数据,保证数据一致性。
+MySQL 数据库的**数据备份、主备、主主、主从**都离不开 binlog,需要依靠 binlog 来同步数据,保证数据一致性。
## 参考
diff --git a/docs/database/mysql/mysql-query-execution-plan.md b/docs/database/mysql/mysql-query-execution-plan.md
index 0fa5ca1e643..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。
@@ -94,7 +94,9 @@ id 如果相同,从上往下依次执行。id 不同,id 值越大,执行
### 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
常见的几种类型具体含义如下:
@@ -138,7 +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 a547b3cc95a..7f93eb605e6 100644
--- a/docs/database/mysql/mysql-questions-01.md
+++ b/docs/database/mysql/mysql-questions-01.md
@@ -158,19 +158,28 @@ DATETIME 类型没有时区信息,TIMESTAMP 和时区有关。
TIMESTAMP 只需要使用 4 个字节的存储空间,但是 DATETIME 需要耗费 8 个字节的存储空间。但是,这样同样造成了一个问题,Timestamp 表示的时间范围更小。
-- DATETIME:1000-01-01 00:00:00 ~ 9999-12-31 23:59:59
-- Timestamp:1970-01-01 00:00:01 ~ 2037-12-31 23:59:59
+- DATETIME:'1000-01-01 00:00:00.000000' 到 '9999-12-31 23:59:59.999999'
+- Timestamp:'1970-01-01 00:00:01.000000' UTC 到 '2038-01-19 03:14:07.999999' UTC
-关于两者的详细对比,请参考我写的[MySQL 时间类型数据存储建议](./some-thoughts-on-database-storage-time.md)。
+关于两者的详细对比,请参考我写的 [MySQL 时间类型数据存储建议](./some-thoughts-on-database-storage-time.md)。
### NULL 和 '' 的区别是什么?
-`NULL` 跟 `''`(空字符串)是两个完全不一样的值,区别如下:
-
-- `NULL` 代表一个不确定的值,就算是两个 `NULL`,它俩也不一定相等。例如,`SELECT NULL=NULL`的结果为 false,但是在我们使用`DISTINCT`,`GROUP BY`,`ORDER BY`时,`NULL`又被认为是相等的。
-- `''`的长度是 0,是不占用空间的,而`NULL` 是需要占用空间的。
-- `NULL` 会影响聚合函数的结果。例如,`SUM`、`AVG`、`MIN`、`MAX` 等聚合函数会忽略 `NULL` 值。 `COUNT` 的处理方式取决于参数的类型。如果参数是 `*`(`COUNT(*)`),则会统计所有的记录数,包括 `NULL` 值;如果参数是某个字段名(`COUNT(列名)`),则会忽略 `NULL` 值,只统计非空值的个数。
-- 查询 `NULL` 值时,必须使用 `IS NULL` 或 `IS NOT NULLl` 来判断,而不能使用 =、!=、 <、> 之类的比较运算符。而`''`是可以使用这些比较运算符的。
+`NULL` 和 `''` (空字符串) 是两个完全不同的值,它们分别表示不同的含义,并在数据库中有着不同的行为。`NULL` 代表缺失或未知的数据,而 `''` 表示一个已知存在的空字符串。它们的主要区别如下:
+
+1. **含义**:
+ - `NULL` 代表一个不确定的值,它不等于任何值,包括它自身。因此,`SELECT NULL = NULL` 的结果是 `NULL`,而不是 `true` 或 `false`。 `NULL` 意味着缺失或未知的信息。虽然 `NULL` 不等于任何值,但在某些操作中,数据库系统会将 `NULL` 值视为相同的类别进行处理,例如:`DISTINCT`,`GROUP BY`,`ORDER BY`。需要注意的是,这些操作将 `NULL` 值视为相同的类别进行处理,并不意味着 `NULL` 值之间是相等的。 它们只是在特定操作中被特殊处理,以保证结果的正确性和一致性。 这种处理方式是为了方便数据操作,而不是改变了 `NULL` 的语义。
+ - `''` 表示一个空字符串,它是一个已知的值。
+2. **存储空间**:
+ - `NULL` 的存储空间占用取决于数据库的实现,通常需要一些空间来标记该值为空。
+ - `''` 的存储空间占用通常较小,因为它只存储一个空字符串的标志,不需要存储实际的字符。
+3. **比较运算**:
+ - 任何值与 `NULL` 进行比较(例如 `=`, `!=`, `>`, `<` 等)的结果都是 `NULL`,表示结果不确定。要判断一个值是否为 `NULL`,必须使用 `IS NULL` 或 `IS NOT NULL`。
+ - `''` 可以像其他字符串一样进行比较运算。例如,`'' = ''` 的结果是 `true`。
+4. **聚合函数**:
+ - 大多数聚合函数(例如 `SUM`, `AVG`, `MIN`, `MAX`)会忽略 `NULL` 值。
+ - `COUNT(*)` 会统计所有行数,包括包含 `NULL` 值的行。`COUNT(列名)` 会统计指定列中非 `NULL` 值的行数。
+ - 空字符串 `''` 会被聚合函数计算在内。例如,`SUM` 会将其视为 0,`MIN` 和 `MAX` 会将其视为一个空字符串。
看了上面的介绍之后,相信你对另外一个高频面试题:“为什么 MySQL 不建议使用 `NULL` 作为列默认值?”也有了答案。
@@ -178,6 +187,37 @@ TIMESTAMP 只需要使用 4 个字节的存储空间,但是 DATETIME 需要耗
MySQL 中没有专门的布尔类型,而是用 TINYINT(1) 类型来表示布尔值。TINYINT(1) 类型可以存储 0 或 1,分别对应 false 或 true。
+### 手机号存储用 INT 还是 VARCHAR?
+
+存储手机号,**强烈推荐使用 VARCHAR 类型**,而不是 INT 或 BIGINT。主要原因如下:
+
+1. **格式兼容性与完整性:**
+ - 手机号可能包含前导零(如某些地区的固话区号)、国家代码前缀('+'),甚至可能带有分隔符('-' 或空格)。INT 或 BIGINT 这种数字类型会自动丢失这些重要的格式信息(比如前导零会被去掉,'+' 和 '-' 无法存储)。
+ - VARCHAR 可以原样存储各种格式的号码,无论是国内的 11 位手机号,还是带有国家代码的国际号码,都能完美兼容。
+2. **非算术性:**手机号虽然看起来是数字,但我们从不对它进行数学运算(比如求和、平均值)。它本质上是一个标识符,更像是一个字符串。用 VARCHAR 更符合其数据性质。
+3. **查询灵活性:**
+ - 业务中常常需要根据号段(前缀)进行查询,例如查找所有 "138" 开头的用户。使用 VARCHAR 类型配合 `LIKE '138%'` 这样的 SQL 查询既直观又高效。
+ - 如果使用数字类型,进行类似的前缀匹配通常需要复杂的函数转换(如 CAST 或 SUBSTRING),或者使用范围查询(如 `WHERE phone >= 13800000000 AND phone < 13900000000`),这不仅写法繁琐,而且可能无法有效利用索引,导致性能下降。
+4. **加密存储的要求(非常关键):**
+ - 出于数据安全和隐私合规的要求,手机号这类敏感个人信息通常必须加密存储在数据库中。
+ - 加密后的数据(密文)是一长串字符串(通常由字母、数字、符号组成,或经过 Base64/Hex 编码),INT 或 BIGINT 类型根本无法存储这种密文。只有 VARCHAR、TEXT 或 BLOB 等类型可以。
+
+**关于 VARCHAR 长度的选择:**
+
+- **如果不加密存储(强烈不推荐!):** 考虑到国际号码和可能的格式符,VARCHAR(20) 到 VARCHAR(32) 通常是一个比较安全的范围,足以覆盖全球绝大多数手机号格式。VARCHAR(15) 可能对某些带国家码和格式符的号码来说不够用。
+- **如果进行加密存储(推荐的标准做法):** 长度必须根据所选加密算法产生的密文最大长度,以及可能的编码方式(如 Base64 会使长度增加约 1/3)来精确计算和设定。通常会需要更长的 VARCHAR 长度,例如 VARCHAR(128), VARCHAR(256) 甚至更长。
+
+最后,来一张表格总结一下:
+
+| 对比维度 | VARCHAR 类型(推荐) | INT/BIGINT 类型(不推荐) | 说明/备注 |
+| ---------------- | --------------------------------- | ---------------------------- | --------------------------------------------------------------------------- |
+| **格式兼容性** | ✔ 能存前导零、"+"、"-"、空格等 | ✘ 自动丢失前导零,不能存符号 | VARCHAR 能原样存储各种手机号格式,INT/BIGINT 只支持单纯数字,且前导零会消失 |
+| **完整性** | ✔ 不丢失任何格式信息 | ✘ 丢失格式信息 | 例如 "013800012345" 存进 INT 会变成 13800012345,"+" 也无法存储 |
+| **非算术性** | ✔ 适合存储“标识符” | ✘ 只适合做数值运算 | 手机号本质是字符串标识符,不做数学运算,VARCHAR 更贴合实际用途 |
+| **查询灵活性** | ✔ 支持 `LIKE '138%'` 等 | ✘ 查询前缀不方便或性能差 | 使用 VARCHAR 可高效按号段/前缀查询,数字类型需转为字符串或其他复杂处理 |
+| **加密存储支持** | ✔ 可存储加密密文(字母、符号等) | ✘ 无法存储密文 | 加密手机号后密文是字符串/二进制,只有 VARCHAR、TEXT、BLOB 等能兼容 |
+| **长度设置建议** | 15~20(未加密),加密视情况而定 | 无意义 | 不加密时 VARCHAR(15~20) 通用,加密后长度取决于算法和编码方式 |
+
## MySQL 基础架构
> 建议配合 [SQL 语句在 MySQL 中的执行过程](./how-sql-executed-in-mysql.md) 这篇文章来理解 MySQL 基础架构。另外,“一个 SQL 语句在 MySQL 中的执行流程”也是面试中比较常问的一个问题。
@@ -193,7 +233,7 @@ MySQL 中没有专门的布尔类型,而是用 TINYINT(1) 类型来表示布
- **分析器:** 没有命中缓存的话,SQL 语句就会经过分析器,分析器说白了就是要先看你的 SQL 语句要干嘛,再检查你的 SQL 语句语法是否正确。
- **优化器:** 按照 MySQL 认为最优的方案去执行。
- **执行器:** 执行语句,然后从存储引擎返回数据。 执行语句之前会先判断是否有权限,如果没有权限的话,就会报错。
-- **插件式存储引擎**:主要负责数据的存储和读取,采用的是插件式架构,支持 InnoDB、MyISAM、Memory 等多种存储引擎。
+- **插件式存储引擎**:主要负责数据的存储和读取,采用的是插件式架构,支持 InnoDB、MyISAM、Memory 等多种存储引擎。InnoDB 是 MySQL 的默认存储引擎,绝大部分场景使用 InnoDB 就是最好的选择。
## MySQL 存储引擎
@@ -249,7 +289,11 @@ mysql> SHOW VARIABLES LIKE '%storage_engine%';
MySQL 存储引擎采用的是 **插件式架构** ,支持多种存储引擎,我们甚至可以为不同的数据库表设置不同的存储引擎以适应不同场景的需要。**存储引擎是基于表的,而不是数据库。**
-并且,你还可以根据 MySQL 定义的存储引擎实现标准接口来编写一个属于自己的存储引擎。这些非官方提供的存储引擎可以称为第三方存储引擎,区别于官方存储引擎。像目前最常用的 InnoDB 其实刚开始就是一个第三方存储引擎,后面由于过于优秀,其被 Oracle 直接收购了。
+下图展示了具有可插拔存储引擎的 MySQL 架构():
+
+
+
+你还可以根据 MySQL 定义的存储引擎实现标准接口来编写一个属于自己的存储引擎。这些非官方提供的存储引擎可以称为第三方存储引擎,区别于官方存储引擎。像目前最常用的 InnoDB 其实刚开始就是一个第三方存储引擎,后面由于过于优秀,其被 Oracle 直接收购了。
MySQL 官方文档也有介绍到如何编写一个自定义存储引擎,地址: 。
@@ -263,13 +307,13 @@ MySQL 5.5 版本之后,InnoDB 是 MySQL 的默认存储引擎。
言归正传!咱们下面还是来简单对比一下两者:
-**1.是否支持行级锁**
+**1、是否支持行级锁**
MyISAM 只有表级锁(table-level locking),而 InnoDB 支持行级锁(row-level locking)和表级锁,默认为行级锁。
也就说,MyISAM 一锁就是锁住了整张表,这在并发写的情况下是多么滴憨憨啊!这也是为什么 InnoDB 在并发写的时候,性能更牛皮了!
-**2.是否支持事务**
+**2、是否支持事务**
MyISAM 不提供事务支持。
@@ -277,7 +321,7 @@ InnoDB 提供事务支持,实现了 SQL 标准定义了四个隔离级别,
关于 MySQL 事务的详细介绍,可以看看我写的这篇文章:[MySQL 事务隔离级别详解](./transaction-isolation-level.md)。
-**3.是否支持外键**
+**3、是否支持外键**
MyISAM 不支持,而 InnoDB 支持。
@@ -291,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 作为索引结构,但是两者的实现方式不太一样。
@@ -311,12 +355,16 @@ InnoDB 引擎中,其数据文件本身就是索引文件。相比 MyISAM,索
详细区别,推荐你看看我写的这篇文章:[MySQL 索引详解](./mysql-index.md)。
-**7.性能有差别。**
+**7、性能有差别。**
InnoDB 的性能比 MyISAM 更强大,不管是在读写混合模式下还是只读模式下,随着 CPU 核数的增加,InnoDB 的读写能力呈线性增长。MyISAM 因为读写不能并发,它的处理能力跟核数没关系。

+**8、数据缓存策略和机制实现不同。**
+
+InnoDB 使用缓冲池(Buffer Pool)缓存数据页和索引页,MyISAM 使用键缓存(Key Cache)仅缓存索引页而不缓存数据页。
+
**总结**:
- InnoDB 支持行级别的锁粒度,MyISAM 不支持,只支持表级别的锁粒度。
@@ -333,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 索引
@@ -349,7 +395,7 @@ MySQL 索引相关的问题比较多,对于面试和工作都比较重要,
## MySQL 查询缓存
-执行查询语句的时候,会先查询缓存。不过,MySQL 8.0 版本后移除,因为这个功能不太实用
+MySQL 查询缓存是查询结果缓存。执行查询语句的时候,会先查询缓存,如果缓存中有对应的查询结果,就会直接返回。
`my.cnf` 加入以下配置,重启 MySQL 开启查询缓存
@@ -365,7 +411,7 @@ set global query_cache_type=1;
set global query_cache_size=600000;
```
-如上,**开启查询缓存后在同样的查询条件以及数据情况下,会直接在缓存中返回结果**。这里的查询条件包括查询本身、当前要查询的数据库、客户端协议版本号等一些可能影响结果的信息。
+查询缓存会在同样的查询条件和数据情况下,直接返回缓存中的结果。但需要注意的是,查询缓存的匹配条件非常严格,任何细微的差异都会导致缓存无法命中。这里的查询条件包括查询语句本身、当前使用的数据库、以及其他可能影响结果的因素,如客户端协议版本号等。
**查询缓存不命中的情况:**
@@ -373,12 +419,16 @@ 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 日志
MySQL 日志常见的面试题有:
@@ -508,7 +558,7 @@ COMMIT;
- 不可重复读的重点是内容修改或者记录减少比如多次读取一条记录发现其中某些记录的值被修改;
- 幻读的重点在于记录新增比如多次执行同一条查询语句(DQL)时,发现查到的记录增加了。
-幻读其实可以看作是不可重复读的一种特殊情况,单独把区分幻读的原因主要是解决幻读和不可重复读的方案不一样。
+幻读其实可以看作是不可重复读的一种特殊情况,单独把幻读区分出来的原因主要是解决幻读和不可重复读的方案不一样。
举个例子:执行 `delete` 和 `update` 操作的时候,可以直接对记录加锁,保证事务安全。而执行 `insert` 操作的时候,由于记录锁(Record Lock)只能锁住已经存在的记录,为了避免插入新记录,需要依赖间隙锁(Gap Lock)。也就是说执行 `insert` 操作的时候需要依赖 Next-Key Lock(Record Lock+Gap Lock) 进行加锁来保证不出现幻读。
@@ -516,7 +566,7 @@ COMMIT;
MySQL 中并发事务的控制方式无非就两种:**锁** 和 **MVCC**。锁可以看作是悲观控制的模式,多版本并发控制(MVCC,Multiversion concurrency control)可以看作是乐观控制的模式。
-**锁** 控制方式下会通过锁来显示控制共享资源而不是通过调度手段,MySQL 中主要是通过 **读写锁** 来实现并发控制。
+**锁** 控制方式下会通过锁来显式控制共享资源而不是通过调度手段,MySQL 中主要是通过 **读写锁** 来实现并发控制。
- **共享锁(S 锁)**:又称读锁,事务在读取记录的时候获取共享锁,允许多个事务同时获取(锁兼容)。
- **排他锁(X 锁)**:又称写锁/独占锁,事务在修改记录的时候获取排他锁,不允许多个事务同时获取。如果一个记录已经被加了排他锁,那其他事务不能再对这条记录加任何类型的锁(锁不兼容)。
@@ -534,31 +584,26 @@ MVCC 在 MySQL 中实现所依赖的手段主要是: **隐藏字段、read view
### SQL 标准定义了哪些事务隔离级别?
-SQL 标准定义了四个隔离级别:
+SQL 标准定义了四种事务隔离级别,用来平衡事务的隔离性(Isolation)和并发性能。级别越高,数据一致性越好,但并发性能可能越低。这四个级别是:
-- **READ-UNCOMMITTED(读取未提交)** :最低的隔离级别,允许读取尚未提交的数据变更,可能会导致脏读、幻读或不可重复读。
-- **READ-COMMITTED(读取已提交)** :允许读取并发事务已经提交的数据,可以阻止脏读,但是幻读或不可重复读仍有可能发生。
-- **REPEATABLE-READ(可重复读)** :对同一字段的多次读取结果都是一致的,除非数据是被本身事务自己所修改,可以阻止脏读和不可重复读,但幻读仍有可能发生。
+- **READ-UNCOMMITTED(读取未提交)** :最低的隔离级别,允许读取尚未提交的数据变更,可能会导致脏读、幻读或不可重复读。这种级别在实际应用中很少使用,因为它对数据一致性的保证太弱。
+- **READ-COMMITTED(读取已提交)** :允许读取并发事务已经提交的数据,可以阻止脏读,但是幻读或不可重复读仍有可能发生。这是大多数数据库(如 Oracle, SQL Server)的默认隔离级别。
+- **REPEATABLE-READ(可重复读)** :对同一字段的多次读取结果都是一致的,除非数据是被本身事务自己所修改,可以阻止脏读和不可重复读,但幻读仍有可能发生。MySQL InnoDB 存储引擎的默认隔离级别正是 REPEATABLE READ。并且,InnoDB 在此级别下通过 MVCC(多版本并发控制) 和 Next-Key Locks(间隙锁+行锁) 机制,在很大程度上解决了幻读问题。
- **SERIALIZABLE(可串行化)** :最高的隔离级别,完全服从 ACID 的隔离级别。所有的事务依次逐个执行,这样事务之间就完全不可能产生干扰,也就是说,该级别可以防止脏读、不可重复读以及幻读。
----
-
-| 隔离级别 | 脏读 | 不可重复读 | 幻读 |
-| :--------------: | :--: | :--------: | :--: |
-| READ-UNCOMMITTED | √ | √ | √ |
-| READ-COMMITTED | × | √ | √ |
-| REPEATABLE-READ | × | × | √ |
-| SERIALIZABLE | × | × | × |
-
-### MySQL 的隔离级别是基于锁实现的吗?
-
-MySQL 的隔离级别基于锁和 MVCC 机制共同实现的。
-
-SERIALIZABLE 隔离级别是通过锁来实现的,READ-COMMITTED 和 REPEATABLE-READ 隔离级别是基于 MVCC 实现的。不过, SERIALIZABLE 之外的其他隔离级别可能也需要用到锁机制,就比如 REPEATABLE-READ 在当前读情况下需要使用加锁读来保证不会出现幻读。
+| 隔离级别 | 脏读 (Dirty Read) | 不可重复读 (Non-Repeatable Read) | 幻读 (Phantom Read) |
+| ---------------- | ----------------- | -------------------------------- | ---------------------- |
+| READ UNCOMMITTED | √ | √ | √ |
+| READ COMMITTED | × | √ | √ |
+| REPEATABLE READ | × | × | √ (标准) / ≈× (InnoDB) |
+| SERIALIZABLE | × | × | × |
### MySQL 的默认隔离级别是什么?
-MySQL InnoDB 存储引擎的默认支持的隔离级别是 **REPEATABLE-READ(可重读)**。我们可以通过`SELECT @@tx_isolation;`命令来查看,MySQL 8.0 该命令改为`SELECT @@transaction_isolation;`
+MySQL InnoDB 存储引擎的默认隔离级别是 **REPEATABLE READ**。可以通过以下命令查看:
+
+- MySQL 8.0 之前:`SELECT @@tx_isolation;`
+- MySQL 8.0 及之后:`SELECT @@transaction_isolation;`
```sql
mysql> SELECT @@tx_isolation;
@@ -571,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 锁
锁是一种常见的并发事务的控制方式。
@@ -596,7 +647,7 @@ InnoDB 的行锁是针对索引字段加的锁,表级锁是针对非索引字
InnoDB 行锁是通过对索引数据页上的记录加锁实现的,MySQL InnoDB 支持三种行锁定方式:
-- **记录锁(Record Lock)**:也被称为记录锁,属于单个行记录上的锁。
+- **记录锁(Record Lock)**:属于单个行记录上的锁。
- **间隙锁(Gap Lock)**:锁定一个范围,不包括记录本身。
- **临键锁(Next-Key Lock)**:Record Lock+Gap Lock,锁定一个范围,包含记录本身,主要目的是为了解决幻读问题(MySQL 事务部分提到过)。记录锁只能锁住已经存在的记录,为了避免插入新记录,需要依赖间隙锁。
@@ -638,7 +689,7 @@ SELECT ... FOR UPDATE;
- **意向共享锁(Intention Shared Lock,IS 锁)**:事务有意向对表中的某些记录加共享锁(S 锁),加共享锁前必须先取得该表的 IS 锁。
- **意向排他锁(Intention Exclusive Lock,IX 锁)**:事务有意向对表中的某些记录加排他锁(X 锁),加排他锁之前必须先取得该表的 IX 锁。
-**意向锁是由数据引擎自己维护的,用户无法手动操作意向锁,在为数据行加共享/排他锁之前,InooDB 会先获取该数据行所在在数据表的对应意向锁。**
+**意向锁是由数据引擎自己维护的,用户无法手动操作意向锁,在为数据行加共享/排他锁之前,InnoDB 会先获取该数据行所在在数据表的对应意向锁。**
意向锁之间是互相兼容的。
@@ -811,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) 。
diff --git a/docs/database/mysql/some-thoughts-on-database-storage-time.md b/docs/database/mysql/some-thoughts-on-database-storage-time.md
index 75329434c1b..e22ce2800da 100644
--- a/docs/database/mysql/some-thoughts-on-database-storage-time.md
+++ b/docs/database/mysql/some-thoughts-on-database-storage-time.md
@@ -3,30 +3,43 @@ title: MySQL日期类型选择建议
category: 数据库
tag:
- MySQL
+head:
+ - - meta
+ - name: keywords
+ content: MySQL 日期类型选择, MySQL 时间存储最佳实践, MySQL 时间存储效率, MySQL DATETIME 和 TIMESTAMP 区别, MySQL 时间戳存储, MySQL 数据库时间存储类型, MySQL 开发日期推荐, MySQL 字符串存储日期的缺点, MySQL 时区设置方法, MySQL 日期范围对比, 高性能 MySQL 日期存储, MySQL UNIX_TIMESTAMP 用法, 数值型时间戳优缺点, MySQL 时间存储性能优化, MySQL TIMESTAMP 时区转换, MySQL 时间格式转换, MySQL 时间存储空间对比, MySQL 时间类型选择建议, MySQL 日期类型性能分析, 数据库时间存储优化
---
-我们平时开发中不可避免的就是要存储时间,比如我们要记录操作表中这条记录的时间、记录转账的交易时间、记录出发时间、用户下单时间等等。你会发现时间这个东西与我们开发的联系还是非常紧密的,用的好与不好会给我们的业务甚至功能带来很大的影响。所以,我们有必要重新出发,好好认识一下这个东西。
+在日常的软件开发工作中,存储时间是一项基础且常见的需求。无论是记录数据的操作时间、金融交易的发生时间,还是行程的出发时间、用户的下单时间等等,时间信息与我们的业务逻辑和系统功能紧密相关。因此,正确选择和使用 MySQL 的日期时间类型至关重要,其恰当与否甚至可能对业务的准确性和系统的稳定性产生显著影响。
+
+本文旨在帮助开发者重新审视并深入理解 MySQL 中不同的时间存储方式,以便做出更合适项目业务场景的选择。
## 不要用字符串存储日期
-和绝大部分对数据库不太了解的新手一样,我在大学的时候就这样干过,甚至认为这样是一个不错的表示日期的方法。毕竟简单直白,容易上手。
+和许多数据库初学者一样,笔者在早期学习阶段也曾尝试使用字符串(如 VARCHAR)类型来存储日期和时间,甚至一度认为这是一种简单直观的方法。毕竟,'YYYY-MM-DD HH:MM:SS' 这样的格式看起来清晰易懂。
但是,这是不正确的做法,主要会有下面两个问题:
-1. 字符串占用的空间更大!
-2. 字符串存储的日期效率比较低(逐个字符进行比对),无法用日期相关的 API 进行计算和比较。
+1. **空间效率**:与 MySQL 内建的日期时间类型相比,字符串通常需要占用更多的存储空间来表示相同的时间信息。
+2. **查询与计算效率低下**:
+ - **比较操作复杂且低效**:基于字符串的日期比较需要按照字典序逐字符进行,这不仅不直观(例如,'2024-05-01' 会小于 '2024-1-10'),而且效率远低于使用原生日期时间类型进行的数值或时间点比较。
+ - **计算功能受限**:无法直接利用数据库提供的丰富日期时间函数进行运算(例如,计算两个日期之间的间隔、对日期进行加减操作等),需要先转换格式,增加了复杂性。
+ - **索引性能不佳**:基于字符串的索引在处理范围查询(如查找特定时间段内的数据)时,其效率和灵活性通常不如原生日期时间类型的索引。
-## Datetime 和 Timestamp 之间的抉择
+## DATETIME 和 TIMESTAMP 选择
-Datetime 和 Timestamp 是 MySQL 提供的两种比较相似的保存时间的数据类型,可以精确到秒。他们两者究竟该如何选择呢?
+`DATETIME` 和 `TIMESTAMP` 是 MySQL 中两种非常常用的、用于存储包含日期和时间信息的数据类型。它们都可以存储精确到秒(MySQL 5.6.4+ 支持更高精度的小数秒)的时间值。那么,在实际应用中,我们应该如何在这两者之间做出选择呢?
-下面我们来简单对比一下二者。
+下面我们从几个关键维度对它们进行对比:
### 时区信息
-**DateTime 类型是没有时区信息的(时区无关)** ,DateTime 类型保存的时间都是当前会话所设置的时区对应的时间。这样就会有什么问题呢?当你的时区更换之后,比如你的服务器更换地址或者更换客户端连接时区设置的话,就会导致你从数据库中读出的时间错误。
+`DATETIME` 类型存储的是**字面量的日期和时间值**,它本身**不包含任何时区信息**。当你插入一个 `DATETIME` 值时,MySQL 存储的就是你提供的那个确切的时间,不会进行任何时区转换。
+
+**这样就会有什么问题呢?** 如果你的应用需要支持多个时区,或者服务器、客户端的时区可能发生变化,那么使用 `DATETIME` 时,应用程序需要自行处理时区的转换和解释。如果处理不当(例如,假设所有存储的时间都属于同一个时区,但实际环境变化了),可能会导致时间显示或计算上的混乱。
-**Timestamp 和时区有关**。Timestamp 类型字段的值会随着服务器时区的变化而变化,自动换算成相应的时间,说简单点就是在不同时区,查询到同一个条记录此字段的值会不一样。
+**`TIMESTAMP` 和时区有关**。存储时,MySQL 会将当前会话时区下的时间值转换成 UTC(协调世界时)进行内部存储。当查询 `TIMESTAMP` 字段时,MySQL 又会将存储的 UTC 时间转换回当前会话所设置的时区来显示。
+
+这意味着,对于同一条记录的 `TIMESTAMP` 字段,在不同的会话时区设置下查询,可能会看到不同的本地时间表示,但它们都对应着同一个绝对时间点(UTC 时间)。这对于需要全球化、多时区支持的应用来说非常有用。
下面实际演示一下!
@@ -41,16 +54,16 @@ CREATE TABLE `time_zone_test` (
) ENGINE=InnoDB DEFAULT CHARSET=utf8;
```
-插入数据:
+插入一条数据(假设当前会话时区为系统默认,例如 UTC+0)::
```sql
INSERT INTO time_zone_test(date_time,time_stamp) VALUES(NOW(),NOW());
```
-查看数据:
+查询数据(在同一时区会话下):
```sql
-select date_time,time_stamp from time_zone_test;
+SELECT date_time, time_stamp FROM time_zone_test;
```
结果:
@@ -63,17 +76,16 @@ select date_time,time_stamp from time_zone_test;
+---------------------+---------------------+
```
-现在我们运行
-
-修改当前会话的时区:
+现在,修改当前会话的时区为东八区 (UTC+8):
```sql
-set time_zone='+8:00';
+SET time_zone = '+8:00';
```
-再次查看数据:
+再次查询数据:
-```plain
+```bash
+# TIMESTAMP 的值自动转换为 UTC+8 时间
+---------------------+---------------------+
| date_time | time_stamp |
+---------------------+---------------------+
@@ -81,7 +93,7 @@ set time_zone='+8:00';
+---------------------+---------------------+
```
-**扩展:一些关于 MySQL 时区设置的一个常用 sql 命令**
+**扩展:MySQL 时区设置常用 SQL 命令**
```sql
# 查看当前会话时区
@@ -102,28 +114,26 @@ SET GLOBAL time_zone = 'Europe/Helsinki';

-在 MySQL 5.6.4 之前,DateTime 和 Timestamp 的存储空间是固定的,分别为 8 字节和 4 字节。但是从 MySQL 5.6.4 开始,它们的存储空间会根据毫秒精度的不同而变化,DateTime 的范围是 5~8 字节,Timestamp 的范围是 4~7 字节。
+在 MySQL 5.6.4 之前,DateTime 和 TIMESTAMP 的存储空间是固定的,分别为 8 字节和 4 字节。但是从 MySQL 5.6.4 开始,它们的存储空间会根据毫秒精度的不同而变化,DateTime 的范围是 5~8 字节,TIMESTAMP 的范围是 4~7 字节。
### 表示范围
-Timestamp 表示的时间范围更小,只能到 2038 年:
+`TIMESTAMP` 表示的时间范围更小,只能到 2038 年:
-- DateTime:1000-01-01 00:00:00.000000 ~ 9999-12-31 23:59:59.499999
-- Timestamp:1970-01-01 00:00:01.000000 ~ 2038-01-19 03:14:07.499999
+- `DATETIME`:'1000-01-01 00:00:00.000000' 到 '9999-12-31 23:59:59.999999'
+- `TIMESTAMP`:'1970-01-01 00:00:01.000000' UTC 到 '2038-01-19 03:14:07.999999' UTC
### 性能
-由于 TIMESTAMP 需要根据时区进行转换,所以从毫秒数转换到 TIMESTAMP 时,不仅要调用一个简单的函数,还要调用操作系统底层的系统函数。这个系统函数为了保证操作系统时区的一致性,需要进行加锁操作,这就降低了效率。
-
-DATETIME 不涉及时区转换,所以不会有这个问题。
+由于 `TIMESTAMP` 在存储和检索时需要进行 UTC 与当前会话时区的转换,这个过程可能涉及到额外的计算开销,尤其是在需要调用操作系统底层接口获取或处理时区信息时。虽然现代数据库和操作系统对此进行了优化,但在某些极端高并发或对延迟极其敏感的场景下,`DATETIME` 因其不涉及时区转换,处理逻辑相对更简单直接,可能会表现出微弱的性能优势。
-为了避免 TIMESTAMP 的时区转换问题,建议使用指定的时区,而不是依赖于操作系统时区。
+为了获得可预测的行为并可能减少 `TIMESTAMP` 的转换开销,推荐的做法是在应用程序层面统一管理时区,或者在数据库连接/会话级别显式设置 `time_zone` 参数,而不是依赖服务器的默认或操作系统时区。
## 数值时间戳是更好的选择吗?
-很多时候,我们也会使用 int 或者 bigint 类型的数值也就是数值时间戳来表示时间。
+除了上述两种类型,实践中也常用整数类型(`INT` 或 `BIGINT`)来存储所谓的“Unix 时间戳”(即从 1970 年 1 月 1 日 00:00:00 UTC 起至目标时间的总秒数,或毫秒数)。
-这种存储方式的具有 Timestamp 类型的所具有一些优点,并且使用它的进行日期排序以及对比等操作的效率会更高,跨系统也很方便,毕竟只是存放的数值。缺点也很明显,就是数据的可读性太差了,你无法直观的看到具体时间。
+这种存储方式的具有 `TIMESTAMP` 类型的所具有一些优点,并且使用它的进行日期排序以及对比等操作的效率会更高,跨系统也很方便,毕竟只是存放的数值。缺点也很明显,就是数据的可读性太差了,你无法直观的看到具体时间。
时间戳的定义如下:
@@ -132,7 +142,8 @@ DATETIME 不涉及时区转换,所以不会有这个问题。
数据库中实际操作:
```sql
-mysql> select UNIX_TIMESTAMP('2020-01-11 09:53:32');
+-- 将日期时间字符串转换为 Unix 时间戳 (秒)
+mysql> SELECT UNIX_TIMESTAMP('2020-01-11 09:53:32');
+---------------------------------------+
| UNIX_TIMESTAMP('2020-01-11 09:53:32') |
+---------------------------------------+
@@ -140,7 +151,8 @@ mysql> select UNIX_TIMESTAMP('2020-01-11 09:53:32');
+---------------------------------------+
1 row in set (0.00 sec)
-mysql> select FROM_UNIXTIME(1578707612);
+-- 将 Unix 时间戳 (秒) 转换为日期时间格式
+mysql> SELECT FROM_UNIXTIME(1578707612);
+---------------------------+
| FROM_UNIXTIME(1578707612) |
+---------------------------+
@@ -149,13 +161,26 @@ mysql> select FROM_UNIXTIME(1578707612);
1 row in set (0.01 sec)
```
+## PostgreSQL 中没有 DATETIME
+
+由于有读者提到 PostgreSQL(PG) 的时间类型,因此这里拓展补充一下。PG 官方文档对时间类型的描述地址:。
+
+
+
+可以看到,PG 没有名为 `DATETIME` 的类型:
+
+- PG 的 `TIMESTAMP WITHOUT TIME ZONE`在功能上最接近 MySQL 的 `DATETIME`。它存储日期和时间,但不包含任何时区信息,存储的是字面值。
+- PG 的`TIMESTAMP WITH TIME ZONE` (或 `TIMESTAMPTZ`) 相当于 MySQL 的 `TIMESTAMP`。它在存储时会将输入值转换为 UTC,并在检索时根据当前会话的时区进行转换显示。
+
+对于绝大多数需要记录精确发生时间点的应用场景,`TIMESTAMPTZ`是 PostgreSQL 中最推荐、最健壮的选择,因为它能最好地处理时区复杂性。
+
## 总结
-MySQL 中时间到底怎么存储才好?Datetime?Timestamp?还是数值时间戳?
+MySQL 中时间到底怎么存储才好?`DATETIME`?`TIMESTAMP`?还是数值时间戳?
并没有一个银弹,很多程序员会觉得数值型时间戳是真的好,效率又高还各种兼容,但是很多人又觉得它表现的不够直观。
-《高性能 MySQL 》这本神书的作者就是推荐 Timestamp,原因是数值表示时间不够直观。下面是原文:
+《高性能 MySQL 》这本神书的作者就是推荐 TIMESTAMP,原因是数值表示时间不够直观。下面是原文:
@@ -167,4 +192,10 @@ MySQL 中时间到底怎么存储才好?Datetime?Timestamp?还是数值时间
| TIMESTAMP | 4~7 字节 | YYYY-MM-DD hh:mm:ss[.fraction] | 1970-01-01 00:00:01[.000000] ~ 2038-01-19 03:14:07[.999999] | 是 |
| 数值型时间戳 | 4 字节 | 全数字如 1578707612 | 1970-01-01 00:00:01 之后的时间 | 否 |
+**选择建议小结:**
+
+- `TIMESTAMP` 的核心优势在于其内建的时区处理能力。数据库负责 UTC 存储和基于会话时区的自动转换,简化了需要处理多时区应用的开发。如果应用需要处理多时区,或者希望数据库能自动管理时区转换,`TIMESTAMP` 是自然的选择(注意其时间范围限制,也就是 2038 年问题)。
+- 如果应用场景不涉及时区转换,或者希望应用程序完全控制时区逻辑,并且需要表示 2038 年之后的时间,`DATETIME` 是更稳妥的选择。
+- 如果极度关注比较性能,或者需要频繁跨系统传递时间数据,并且可以接受可读性的牺牲(或总是在应用层转换),数值时间戳是一个强大的选项。
+
diff --git a/docs/database/mysql/transaction-isolation-level.md b/docs/database/mysql/transaction-isolation-level.md
index 52ad40f4a47..8b706640ea6 100644
--- a/docs/database/mysql/transaction-isolation-level.md
+++ b/docs/database/mysql/transaction-isolation-level.md
@@ -11,43 +11,46 @@ tag:
## 事务隔离级别总结
-SQL 标准定义了四个隔离级别:
+SQL 标准定义了四种事务隔离级别,用来平衡事务的隔离性(Isolation)和并发性能。级别越高,数据一致性越好,但并发性能可能越低。这四个级别是:
-- **READ-UNCOMMITTED(读取未提交)** :最低的隔离级别,允许读取尚未提交的数据变更,可能会导致脏读、幻读或不可重复读。
-- **READ-COMMITTED(读取已提交)** :允许读取并发事务已经提交的数据,可以阻止脏读,但是幻读或不可重复读仍有可能发生。
-- **REPEATABLE-READ(可重复读)** :对同一字段的多次读取结果都是一致的,除非数据是被本身事务自己所修改,可以阻止脏读和不可重复读,但幻读仍有可能发生。
+- **READ-UNCOMMITTED(读取未提交)** :最低的隔离级别,允许读取尚未提交的数据变更,可能会导致脏读、幻读或不可重复读。这种级别在实际应用中很少使用,因为它对数据一致性的保证太弱。
+- **READ-COMMITTED(读取已提交)** :允许读取并发事务已经提交的数据,可以阻止脏读,但是幻读或不可重复读仍有可能发生。这是大多数数据库(如 Oracle, SQL Server)的默认隔离级别。
+- **REPEATABLE-READ(可重复读)** :对同一字段的多次读取结果都是一致的,除非数据是被本身事务自己所修改,可以阻止脏读和不可重复读,但幻读仍有可能发生。MySQL InnoDB 存储引擎的默认隔离级别正是 REPEATABLE READ。并且,InnoDB 在此级别下通过 MVCC(多版本并发控制) 和 Next-Key Locks(间隙锁+行锁) 机制,在很大程度上解决了幻读问题。
- **SERIALIZABLE(可串行化)** :最高的隔离级别,完全服从 ACID 的隔离级别。所有的事务依次逐个执行,这样事务之间就完全不可能产生干扰,也就是说,该级别可以防止脏读、不可重复读以及幻读。
----
+| 隔离级别 | 脏读 (Dirty Read) | 不可重复读 (Non-Repeatable Read) | 幻读 (Phantom Read) |
+| ---------------- | ----------------- | -------------------------------- | ---------------------- |
+| READ UNCOMMITTED | √ | √ | √ |
+| READ COMMITTED | × | √ | √ |
+| REPEATABLE READ | × | × | √ (标准) / ≈× (InnoDB) |
+| SERIALIZABLE | × | × | × |
-| 隔离级别 | 脏读 | 不可重复读 | 幻读 |
-| :--------------: | :--: | :--------: | :--: |
-| READ-UNCOMMITTED | √ | √ | √ |
-| READ-COMMITTED | × | √ | √ |
-| REPEATABLE-READ | × | × | √ |
-| SERIALIZABLE | × | × | × |
+**默认级别查询:**
-MySQL InnoDB 存储引擎的默认支持的隔离级别是 **REPEATABLE-READ(可重读)**。我们可以通过`SELECT @@tx_isolation;`命令来查看,MySQL 8.0 该命令改为`SELECT @@transaction_isolation;`
+MySQL InnoDB 存储引擎的默认隔离级别是 **REPEATABLE READ**。可以通过以下命令查看:
-```sql
-MySQL> SELECT @@tx_isolation;
-+-----------------+
-| @@tx_isolation |
-+-----------------+
-| REPEATABLE-READ |
-+-----------------+
+- MySQL 8.0 之前:`SELECT @@tx_isolation;`
+- MySQL 8.0 及之后:`SELECT @@transaction_isolation;`
+
+```bash
+mysql> SELECT @@transaction_isolation;
++-------------------------+
+| @@transaction_isolation |
++-------------------------+
+| REPEATABLE-READ |
++-------------------------+
```
-从上面对 SQL 标准定义了四个隔离级别的介绍可以看出,标准的 SQL 隔离级别定义里,REPEATABLE-READ(可重复读)是不可以防止幻读的。
+**InnoDB 的 REPEATABLE READ 对幻读的处理:**
-但是!InnoDB 实现的 REPEATABLE-READ 隔离级别其实是可以解决幻读问题发生的,主要有下面两种情况:
+标准的 SQL 隔离级别定义里,REPEATABLE READ 是无法防止幻读的。但 InnoDB 的实现通过以下机制很大程度上避免了幻读:
-- **快照读**:由 MVCC 机制来保证不出现幻读。
-- **当前读**:使用 Next-Key Lock 进行加锁来保证不出现幻读,Next-Key Lock 是行锁(Record Lock)和间隙锁(Gap Lock)的结合,行锁只能锁住已经存在的行,为了避免插入新行,需要依赖间隙锁。
+- **快照读 (Snapshot Read)**:普通的 SELECT 语句,通过 **MVCC** 机制实现。事务启动时创建一个数据快照,后续的快照读都读取这个版本的数据,从而避免了看到其他事务新插入的行(幻读)或修改的行(不可重复读)。
+- **当前读 (Current Read)**:像 `SELECT ... FOR UPDATE`, `SELECT ... LOCK IN SHARE MODE`, `INSERT`, `UPDATE`, `DELETE` 这些操作。InnoDB 使用 **Next-Key Lock** 来锁定扫描到的索引记录及其间的范围(间隙),防止其他事务在这个范围内插入新的记录,从而避免幻读。Next-Key Lock 是行锁(Record Lock)和间隙锁(Gap Lock)的组合。
-因为隔离级别越低,事务请求的锁越少,所以大部分数据库系统的隔离级别都是 **READ-COMMITTED** ,但是你要知道的是 InnoDB 存储引擎默认使用 **REPEATABLE-READ** 并不会有任何性能损失。
+值得注意的是,虽然通常认为隔离级别越高、并发性越差,但 InnoDB 存储引擎通过 MVCC 机制优化了 REPEATABLE READ 级别。对于许多常见的只读或读多写少的场景,其性能**与 READ COMMITTED 相比可能没有显著差异**。不过,在写密集型且并发冲突较高的场景下,RR 的间隙锁机制可能会比 RC 带来更多的锁等待。
-InnoDB 存储引擎在分布式事务的情况下一般会用到 SERIALIZABLE 隔离级别。
+此外,在某些特定场景下,如需要严格一致性的分布式事务(XA Transactions),InnoDB 可能要求或推荐使用 SERIALIZABLE 隔离级别来确保全局数据的一致性。
《MySQL 技术内幕:InnoDB 存储引擎(第 2 版)》7.7 章这样写到:
diff --git a/docs/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 6394a0c572d..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 种常见的缓存读写策略对于实际工作中使用缓存以及面试中被问到缓存都是非常有帮助的!
diff --git a/docs/database/redis/redis-cluster.md b/docs/database/redis/redis-cluster.md
index 60cd7dda858..e3ef2efd04c 100644
--- a/docs/database/redis/redis-cluster.md
+++ b/docs/database/redis/redis-cluster.md
@@ -7,7 +7,7 @@ tag:
**Redis 集群** 相关的面试题为我的 [知识星球](../../about-the-author/zhishixingqiu-two-years.md)(点击链接即可查看详细介绍以及加入方法)专属内容,已经整理到了[《Java 面试指北》](../../zhuanlan/java-mian-shi-zhi-bei.md)中。
-
+
diff --git a/docs/database/redis/redis-common-blocking-problems-summary.md b/docs/database/redis/redis-common-blocking-problems-summary.md
index 2b5d84a32aa..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 的时候应该格外注意!
@@ -136,7 +136,7 @@ Swap 对于 Redis 来说是非常致命的,Redis 保证高性能的一个重
1、查询 Redis 进程号
```bash
-reids-cli -p 6383 info server | grep process_id
+redis-cli -p 6383 info server | grep process_id
process_id: 4476
```
@@ -164,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`统计信息分析出命令不合理开销时间,查看是否是因为高算法复杂度或者过度的内存优化问题。
## 网络问题
@@ -172,7 +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 999e88b4014..7d993752138 100644
--- a/docs/database/redis/redis-data-structures-01.md
+++ b/docs/database/redis/redis-data-structures-01.md
@@ -182,7 +182,7 @@ Redis 中的 List 其实就是链表数据结构的实现。我在 [线性数据
"value3"
```
-我专门画了一个图方便大家理解 `RPUSH` , `LPOP` , `lpush` , `RPOP` 命令:
+我专门画了一个图方便大家理解 `RPUSH` , `LPOP` , `LPUSH` , `RPOP` 命令:

@@ -474,7 +474,7 @@ value1

-[《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 来设计制作一个排行榜。

diff --git a/docs/database/redis/redis-data-structures-02.md b/docs/database/redis/redis-data-structures-02.md
index a083761fac4..9e5fbcee59b 100644
--- a/docs/database/redis/redis-data-structures-02.md
+++ b/docs/database/redis/redis-data-structures-02.md
@@ -36,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 基本操作演示**:
@@ -123,9 +123,9 @@ HyperLogLog 相关的命令非常少,最常用的也就 3 个。
### 应用场景
-**数量量巨大(百万、千万级别以上)的计数场景**
+**数量巨大(百万、千万级别以上)的计数场景**
-- 举例:热门网站每日/每周/每月访问 ip 数统计、热门帖子 uv 统计、
+- 举例:热门网站每日/每周/每月访问 ip 数统计、热门帖子 uv 统计。
- 相关命令:`PFADD`、`PFCOUNT` 。
## Geospatial (地理位置)
@@ -219,8 +219,8 @@ user2
## 参考
-- 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。
+
+
+
+在 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 服务器删除 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 1290b1e2936..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) {
@@ -59,11 +59,11 @@ void *zmalloc(size_t size) {

-文档地址:https://redis.io/topics/memory-optimization 。
+文档地址: 。
## 如何查看 Redis 内存碎片的信息?
-使用 `info memory` 命令即可查看 Redis 内存相关的信息。下图中每个参数具体的含义,Redis 官方文档有详细的介绍:https://redis.io/commands/INFO 。
+使用 `info memory` 命令即可查看 Redis 内存相关的信息。下图中每个参数具体的含义,Redis 官方文档有详细的介绍: 。

@@ -117,8 +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 a7eaa524fa7..2b61a1250ad 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 。
+官方文档地址: 。

@@ -71,7 +71,7 @@ AOF 持久化功能的实现可以简单分为 5 步:
1. **命令追加(append)**:所有的写命令会追加到 AOF 缓冲区中。
2. **文件写入(write)**:将 AOF 缓冲区的数据写入到 AOF 文件中。这一步需要调用`write`函数(系统调用),`write`将数据写入到了系统内核缓冲区之后直接返回了(延迟写)。注意!!!此时并没有同步到磁盘。
-3. **文件同步(fsync)**:AOF 缓冲区根据对应的持久化方式( `fsync` 策略)向硬盘做同步操作。这一步需要调用 `fsync` 函数(系统调用), `fsync` 针对单个文件操作,对其进行强制硬盘同步,`fsync` 将阻塞直到写入磁盘完成后返回,保证了数据持久化。
+3. **文件同步(fsync)**:这一步才是持久化的核心!根据你在 `redis.conf` 文件里 `appendfsync` 配置的策略,Redis 会在不同的时机,调用 `fsync` 函数(系统调用)。`fsync` 针对单个文件操作,对其进行强制硬盘同步(文件在内核缓冲区里的数据写到硬盘),`fsync` 将阻塞直到写入磁盘完成后返回,保证了数据持久化。
4. **文件重写(rewrite)**:随着 AOF 文件越来越大,需要定期对 AOF 文件进行重写,达到压缩的目的。
5. **重启加载(load)**:当 Redis 重启时,可以加载 AOF 文件进行数据恢复。
@@ -90,9 +90,9 @@ AOF 工作流程图如下:
在 Redis 的配置文件中存在三种不同的 AOF 持久化方式( `fsync`策略),它们分别是:
-1. `appendfsync always`:主线程调用 `write` 执行写操作后,后台线程( `aof_fsync` 线程)立即会调用 `fsync` 函数同步 AOF 文件(刷盘),`fsync` 完成后线程返回,这样会严重降低 Redis 的性能(`write` + `fsync`)。
-2. `appendfsync everysec`:主线程调用 `write` 执行写操作后立即返回,由后台线程( `aof_fsync` 线程)每秒钟调用 `fsync` 函数(系统调用)同步一次 AOF 文件(`write`+`fsync`,`fsync`间隔为 1 秒)
-3. `appendfsync no`:主线程调用 `write` 执行写操作后立即返回,让操作系统决定何时进行同步,Linux 下一般为 30 秒一次(`write`但不`fsync`,`fsync` 的时机由操作系统决定)。
+1. `appendfsync always`:主线程调用 `write` 执行写操作后,会立刻调用 `fsync` 函数同步 AOF 文件(刷盘)。主线程会阻塞,直到 `fsync` 将数据完全刷到磁盘后才会返回。这种方式数据最安全,理论上不会有任何数据丢失。但因为每个写操作都会同步阻塞主线程,所以性能极差。
+2. `appendfsync everysec`:主线程调用 `write` 执行写操作后立即返回,由后台线程( `aof_fsync` 线程)每秒钟调用 `fsync` 函数(系统调用)同步一次 AOF 文件(`write`+`fsync`,`fsync`间隔为 1 秒)。这种方式主线程的性能基本不受影响。在性能和数据安全之间做出了绝佳的平衡。不过,在 Redis 异常宕机时,最多可能丢失最近 1 秒内的数据。
+3. `appendfsync no`:主线程调用 `write` 执行写操作后立即返回,让操作系统决定何时进行同步,Linux 下一般为 30 秒一次(`write`但不`fsync`,`fsync` 的时机由操作系统决定)。 这种方式性能最好,因为避免了 `fsync` 的阻塞。但数据安全性最差,宕机时丢失的数据量不可控,取决于操作系统上一次同步的时间点。
可以看出:**这 3 种持久化方式的主要区别在于 `fsync` 同步 AOF 文件的时机(刷盘)**。
@@ -153,9 +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
+官方文档地址:

@@ -191,9 +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 87609252b00..913ad8b2e51 100644
--- a/docs/database/redis/redis-questions-01.md
+++ b/docs/database/redis/redis-questions-01.md
@@ -20,7 +20,7 @@ head:
[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)。

@@ -30,31 +30,50 @@ Redis 没有外部依赖,Linux 和 OS X 是 Redis 开发和测试最多的两

-全世界有非常多的网站使用到了 Redis ,[techstacks.io](https://techstacks.io/) 专门维护了一个[使用 Redis 的热门站点列表](https://techstacks.io/tech/redis) ,感兴趣的话可以看看。
+全世界有非常多的网站使用到了 Redis,[techstacks.io](https://techstacks.io/) 专门维护了一个[使用 Redis 的热门站点列表](https://techstacks.io/tech/redis),感兴趣的话可以看看。
### Redis 为什么这么快?
-Redis 内部做了非常多的性能优化,比较重要的有下面 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)。

-### 分布式缓存常见的技术选型方案有哪些?
+那既然都这么快了,为什么不直接用 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 替代品还是下面这两个开源分布式缓存(都是通过碰瓷 Redis 火的):
-关于 Redis 和 Tendis 的对比,腾讯官方曾经发过一篇文章:[Redis vs Tendis:冷热混合存储版架构揭秘](https://mp.weixin.qq.com/s/MeYkfOIdnU6LYlsGb24KjQ) ,可以简单参考一下。
+- [Dragonfly](https://github.com/dragonflydb/dragonfly):一种针对现代应用程序负荷需求而构建的内存数据库,完全兼容 Redis 和 Memcached 的 API,迁移时无需修改任何代码,号称全世界最快的内存数据库。
+- [KeyDB](https://github.com/Snapchat/KeyDB):Redis 的一个高性能分支,专注于多线程、内存效率和高吞吐量。
-从这个项目的 GitHub 提交记录可以看出,Tendis 开源版几乎已经没有被维护更新了,加上其关注度并不高,使用的公司也比较少。因此,不建议你使用 Tendis 来实现分布式缓存。
+不过,个人还是建议分布式缓存首选 Redis,毕竟经过了这么多年的考验,生态非常优秀,资料也很全面!
+
+PS:篇幅问题,我这并没有对上面提到的分布式缓存选型做详细介绍和对比,感兴趣的话,可以自行研究一下。
### 说一下 Redis 和 Memcached 的区别和共同点
@@ -68,64 +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/为什么要用缓存?
-
-下面我们主要从“高性能”和“高并发”这两点来回答这个问题。
+### 为什么要用 Redis?
-**1、高性能**
+**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 来做分布式锁是一种比较常见的方式。通常情况下,我们都是基于 Redisson 来实现分布式锁。关于 Redis 实现分布式锁的详细介绍,可以看我写的这篇文章:[分布式锁详解](https://javaguide.cn/distributed-system/distributed-lock.html)。
+- **限流**:一般是通过 Redis + Lua 脚本的方式来实现限流。如果不想自己写 Lua 脚本的话,也可以直接利用 Redisson 中的 `RRateLimiter` 来实现分布式限流,其底层实现就是基于 Lua 代码+令牌桶算法。
- **消息队列**:Redis 自带的 List 数据结构可以作为一个简单的队列使用。Redis 5.0 中增加的 Stream 类型的数据结构更加适合用来做消息队列。它比较类似于 Kafka,有主题和消费组的概念,支持消息持久化以及 ACK 机制。
- **延时队列**:Redisson 内置了延时队列(基于 Sorted Set 实现的)。
-- **分布式 Session** :利用 String 或者 Hash 数据类型保存 Session 数据,所有的服务器都可以访问。
-- **复杂业务场景**:通过 Redis 以及 Redis 扩展(比如 Redisson)提供的数据结构,我们可以很方便地完成很多复杂的业务场景比如通过 Bitmap 统计活跃用户、通过 Sorted Set 维护排行榜。
+- **分布式 Session**:利用 String 或者 Hash 数据类型保存 Session 数据,所有的服务器都可以访问。
+- **复杂业务场景**:通过 Redis 以及 Redis 扩展(比如 Redisson)提供的数据结构,我们可以很方便地完成很多复杂的业务场景,比如通过 Bitmap 统计活跃用户、通过 Sorted Set 维护排行榜、通过 HyperLogLog 统计网站 UV 和 PV。
- ……
### 如何基于 Redis 实现分布式锁?
-关于 Redis 实现分布式锁的详细介绍,可以看我写的这篇文章:[分布式锁详解](https://javaguide.cn/distributed-system/distributed-lock.html) 。
+关于 Redis 实现分布式锁的详细介绍,可以看我写的这篇文章:[分布式锁详解](https://javaguide.cn/distributed-system/distributed-lock-implementations.html)。
### Redis 可以做消息队列么?
-> 实际项目中也没见谁使用 Redis 来做消息队列,对于这部分知识点大家了解就好了。
+> 实际项目中使用 Redis 来做消息队列的非常少,毕竟有更成熟的消息队列中间件可以用。
先说结论:**可以是可以,但不建议使用 Redis 来做消息队列。和专业的消息队列相比,还是有很多欠缺的地方。**
**Redis 2.0 之前,如果想要使用 Redis 来做消息队列的话,只能通过 List 来实现。**
-通过 `RPUSH/LPOP` 或者 `LPUSH/RPOP`即可实现简易版消息队列:
+通过 `RPUSH/LPOP` 或者 `LPUSH/RPOP` 即可实现简易版消息队列:
```bash
# 生产者生产消息
@@ -138,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
@@ -170,19 +216,97 @@ pub/sub 既能单播又能广播,还支持 channel 的简单正则匹配。不
为此,Redis 5.0 新增加的一个数据结构 `Stream` 来做消息队列。`Stream` 支持:
-- 发布 / 订阅模式
-- 按照消费者组进行消费
-- 消息持久化( RDB 和 AOF)
+- 发布 / 订阅模式;
+- 按照消费者组进行消费(借鉴了 Kafka 消费者组的概念);
+- 消息持久化( RDB 和 AOF);
+- ACK 机制(通过确认机制来告知已经成功处理了消息);
+- 阻塞式获取消息。
+
+`Stream` 的结构如下:
+
+
+
+这是一个有序的消息链表,每个消息都有一个唯一的 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 是可以实现全文搜索引擎功能的,需要借助 **RediSearch**,这是一个基于 Redis 的搜索引擎模块。
+
+RediSearch 支持中文分词、聚合统计、停用词、同义词、拼写检查、标签查询、向量相似度查询、多关键词搜索、分页搜索等功能,算是一个功能比较完善的全文搜索引擎了。
+
+相比较于 Elasticsearch 来说,RediSearch 主要在下面两点上表现更优异一些:
+
+1. 性能更优秀:依赖 Redis 自身的高性能,基于内存操作(Elasticsearch 基于磁盘)。
+2. 较低内存占用实现快速索引:RediSearch 内部使用压缩的倒排索引,所以可以用较低的内存占用来实现索引的快速构建。
+
+对于小型项目的简单搜索场景来说,使用 RediSearch 来作为搜索引擎还是没有问题的(搭配 RedisJSON 使用)。
+
+对于比较复杂或者数据规模较大的搜索场景,还是不太建议使用 RediSearch 来作为搜索引擎,主要是因为下面这些限制和问题:
+
+1. 数据量限制:Elasticsearch 可以支持 PB 级别的数据量,可以轻松扩展到多个节点,利用分片机制提高可用性和性能。RedisSearch 是基于 Redis 实现的,其能存储的数据量受限于 Redis 的内存容量,不太适合存储大规模的数据(内存昂贵,扩展能力较差)。
+2. 分布式能力较差:Elasticsearch 是为分布式环境设计的,可以轻松扩展到多个节点。虽然 RedisSearch 支持分布式部署,但在实际应用中可能会面临一些挑战,如数据分片、节点间通信、数据一致性等问题。
+3. 聚合功能较弱:Elasticsearch 提供了丰富的聚合功能,而 RediSearch 的聚合功能相对较弱,只支持简单的聚合操作。
+4. 生态较差:Elasticsearch 可以轻松和常见的一些系统/软件集成比如 Hadoop、Spark、Kibana,而 RedisSearch 则不具备该优势。
+
+Elasticsearch 适用于全文搜索、复杂查询、实时数据分析和聚合的场景,而 RediSearch 适用于快速数据存储、缓存和简单查询的场景。
+
+### 如何基于 Redis 实现延时任务?
+
+> 类似的问题:
+>
+> - 订单在 10 分钟后未支付就失效,如何用 Redis 实现?
+> - 红包 24 小时未被查收自动退还,如何用 Redis 实现?
+
+基于 Redis 实现延时任务的功能无非就下面两种方案:
+
+1. Redis 过期事件监听。
+2. Redisson 内置的延时队列。
+
+Redis 过期事件监听存在时效性较差、丢消息、多服务实例下消息重复消费等问题,不被推荐使用。
+
+Redisson 内置的延时队列具备下面这些优势:
+
+1. **减少了丢消息的可能**:DelayedQueue 中的消息会被持久化,即使 Redis 宕机了,根据持久化机制,也只可能丢失一点消息,影响不大。当然了,你也可以使用扫描数据库的方法作为补偿机制。
+2. **消息不存在重复消费问题**:每个客户端都是从同一个目标队列中获取任务的,不存在重复消费的问题。
+
+关于 Redis 实现延时任务的详细介绍,可以看我写的这篇文章:[如何基于 Redis 实现延时任务?](./redis-delayed-task.md)。
+
## Redis 数据类型
-关于 Redis 5 种基础数据类型和 3 种特殊数据类型的详细介绍请看下面这两篇文章以及 [Redis 官方文档](https://redis.io/docs/data-types/) :
+关于 Redis 5 种基础数据类型和 3 种特殊数据类型的详细介绍请看下面这两篇文章以及 [Redis 官方文档](https://redis.io/docs/data-types/):
- [Redis 5 种基本数据类型详解](https://javaguide.cn/database/redis/redis-data-structures-01.html)
- [Redis 3 种特殊数据类型详解](https://javaguide.cn/database/redis/redis-data-structures-02.html)
@@ -204,25 +328,32 @@ String 的常见应用场景如下:
- 常规数据(比如 Session、Token、序列化后的对象、图片的路径)的缓存;
- 计数比如用户单位时间的请求数(简单限流可以用到)、页面单位时间的访问数;
-- 分布式锁(利用 `SETNX key value` 命令可以实现一个最简易的分布式锁);
+- 分布式锁(利用 `SETNX key value` 命令可以实现一个最简易的分布式锁);
- ……
关于 String 的详细介绍请看这篇文章:[Redis 5 种基本数据类型详解](https://javaguide.cn/database/redis/redis-data-structures-01.html)。
### String 还是 Hash 存储对象数据更好呢?
-- String 存储的是序列化后的对象数据,存放的是整个对象。Hash 是对对象的每个字段单独存储,可以获取部分字段的信息,也可以修改或者添加部分字段,节省网络流量。如果对象中某些字段需要经常变动或者经常需要单独查询对象中的个别字段信息,Hash 就非常适合。
-- String 存储相对来说更加节省内存,缓存相同数量的对象数据,String 消耗的内存约是 Hash 的一半。并且,存储具有多层嵌套的对象时也方便很多。如果系统对性能和资源消耗非常敏感的话,String 就非常适合。
+简单对比一下二者:
-在绝大部分情况,我们建议使用 String 来存储对象数据即可!
+- **对象存储方式**:String 存储的是序列化后的对象数据,存放的是整个对象,操作简单直接。Hash 是对对象的每个字段单独存储,可以获取部分字段的信息,也可以修改或者添加部分字段,节省网络流量。如果对象中某些字段需要经常变动或者经常需要单独查询对象中的个别字段信息,Hash 就非常适合。
+- **内存消耗**:Hash 通常比 String 更节省内存,特别是在字段较多且字段长度较短时。Redis 对小型 Hash 进行优化(如使用 ziplist 存储),进一步降低内存占用。
+- **复杂对象存储**:String 在处理多层嵌套或复杂结构的对象时更方便,因为无需处理每个字段的独立存储和操作。
+- **性能**:String 的操作通常具有 O(1) 的时间复杂度,因为它存储的是整个对象,操作简单直接,整体读写的性能较好。Hash 由于需要处理多个字段的增删改查操作,在字段较多且经常变动的情况下,可能会带来额外的性能开销。
+
+总结:
+
+- 在绝大多数情况下,**String** 更适合存储对象数据,尤其是当对象结构简单且整体读写是主要操作时。
+- 如果你需要频繁操作对象的部分字段或节省内存,**Hash** 可能是更好的选择。
### String 的底层实现是什么?
-Redis 是基于 C 语言编写的,但 Redis 的 String 类型的底层实现并不是 C 语言中的字符串(即以空字符 `\0` 结尾的字符数组),而是自己编写了 [SDS](https://github.com/antirez/sds)(Simple Dynamic String,简单动态字符串) 来作为底层实现。
+Redis 是基于 C 语言编写的,但 Redis 的 String 类型的底层实现并不是 C 语言中的字符串(即以空字符 `\0` 结尾的字符数组),而是自己编写了 [SDS](https://github.com/antirez/sds)(Simple Dynamic String,简单动态字符串)来作为底层实现。
SDS 最早是 Redis 作者为日常 C 语言开发而设计的 C 字符串,后来被应用到了 Redis 上,并经过了大量的修改完善以适合高性能操作。
-Redis7.0 的 SDS 的部分源码如下(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.
@@ -257,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 会根据初始化的长度决定使用哪种类型,从而减少内存的使用。
| 类型 | 字节 | 位 |
| -------- | ---- | --- |
@@ -269,10 +400,10 @@ struct __attribute__ ((__packed__)) sdshdr64 {
对于后四种实现都包含了下面这 4 个属性:
-- `len`:字符串的长度也就是已经使用的字节数
-- `alloc`:总共可用的字符空间大小,alloc-len 就是 SDS 剩余的空间大小
-- `buf[]`:实际存储字符串的数组
-- `flags`:低三位保存类型标志
+- `len`:字符串的长度也就是已经使用的字节数。
+- `alloc`:总共可用的字符空间大小,alloc-len 就是 SDS 剩余的空间大小。
+- `buf[]`:实际存储字符串的数组。
+- `flags`:低三位保存类型标志。
SDS 相比于 C 语言中的字符串有如下提升:
@@ -314,9 +445,9 @@ struct sdshdr {
### 使用 Redis 实现一个排行榜怎么做?
-Redis 中有一个叫做 `Sorted Set` 的数据类型经常被用在各种排行榜的场景,比如直播间送礼物的排行榜、朋友圈的微信步数排行榜、王者荣耀中的段位排行榜、话题热度排行榜等等。
+Redis 中有一个叫做 `Sorted Set`(有序集合)的数据类型经常被用在各种排行榜的场景,比如直播间送礼物的排行榜、朋友圈的微信步数排行榜、王者荣耀中的段位排行榜、话题热度排行榜等等。
-相关的一些 Redis 命令: `ZRANGE` (从小到大排序)、 `ZREVRANGE` (从大到小排序)、`ZREVRANK` (指定元素排名)。
+相关的一些 Redis 命令:`ZRANGE`(从小到大排序)、`ZREVRANGE`(从大到小排序)、`ZREVRANK`(指定元素排名)。

@@ -324,14 +455,24 @@ Redis 中有一个叫做 `Sorted Set` 的数据类型经常被用在各种排行

+### 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` 的常见应用场景如下:
-- 存放的数据不能重复的场景:网站 UV 统计(数据量巨大的场景还是 `HyperLogLog`更适合一些)、文章点赞、动态点赞等等。
-- 需要获取多个数据源交集、并集和差集的场景:共同好友(交集)、共同粉丝(交集)、共同关注(交集)、好友推荐(差集)、音乐推荐(差集)、订阅号推荐(差集+交集) 等等。
+- 存放的数据不能重复的场景:网站 UV 统计(数据量巨大的场景还是 `HyperLogLog` 更适合一些)、文章点赞、动态点赞等等。
+- 需要获取多个数据源交集、并集和差集的场景:共同好友(交集)、共同粉丝(交集)、共同关注(交集)、好友推荐(差集)、音乐推荐(差集)、订阅号推荐(差集+交集)等等。
- 需要随机获取数据源中的元素的场景:抽奖系统、随机点名等等。
### 使用 Set 实现抽奖系统怎么做?
@@ -340,11 +481,11 @@ Redis 中 `Set` 是一种无序集合,集合中的元素没有先后顺序但
- `SADD key member1 member2 ...`:向指定集合添加一个或多个元素。
- `SPOP key count`:随机移除并获取指定集合中一个或多个元素,适合不允许重复中奖的场景。
-- `SRANDMEMBER key count` : 随机获取指定集合中指定数量的元素,适合允许重复中奖的场景。
+- `SRANDMEMBER key count`:随机获取指定集合中指定数量的元素,适合允许重复中奖的场景。
### 使用 Bitmap 统计活跃用户怎么做?
-Bitmap 存储的是连续的二进制数字(0 和 1),通过 Bitmap, 只需要一个 bit 位来表示某个元素对应的值或者状态,key 就是对应元素本身 。我们知道 8 个 bit 可以组成一个 byte,所以 Bitmap 本身会极大的节省储存空间。
+Bitmap 存储的是连续的二进制数字(0 和 1),通过 Bitmap,只需要一个 bit 位来表示某个元素对应的值或者状态,key 就是对应元素本身。我们知道 8 个 bit 可以组成一个 byte,所以 Bitmap 本身会极大的节省储存空间。
你可以将 Bitmap 看作是一个存储二进制数字(0 和 1)的数组,数组中每个元素的下标叫做 offset(偏移量)。
@@ -363,7 +504,7 @@ Bitmap 存储的是连续的二进制数字(0 和 1),通过 Bitmap, 只需
(integer) 0
```
-统计 20210308~20210309 总活跃用户数:
+统计 20210308~20210309 总活跃用户数:
```bash
> BITOP and desk1 20210308 20210309
@@ -372,7 +513,7 @@ Bitmap 存储的是连续的二进制数字(0 和 1),通过 Bitmap, 只需
(integer) 1
```
-统计 20210308~20210309 在线活跃用户数:
+统计 20210308~20210309 在线活跃用户数:
```bash
> BITOP or desk2 20210308 20210309
@@ -402,17 +543,17 @@ PFCOUNT PAGE_1:UV
## Redis 持久化机制(重要)
-Redis 持久化机制(RDB 持久化、AOF 持久化、RDB 和 AOF 的混合持久化) 相关的问题比较多,也比较重要,于是我单独抽了一篇文章来总结 Redis 持久化机制相关的知识点和问题:[Redis 持久化机制详解](https://javaguide.cn/database/redis/redis-persistence.html) 。
+Redis 持久化机制(RDB 持久化、AOF 持久化、RDB 和 AOF 的混合持久化)相关的问题比较多,也比较重要,于是我单独抽了一篇文章来总结 Redis 持久化机制相关的知识点和问题:[Redis 持久化机制详解](https://javaguide.cn/database/redis/redis-persistence.html)。
## Redis 线程模型(重要)
-对于读写命令来说,Redis 一直是单线程模型。不过,在 Redis 4.0 版本之后引入了多线程来执行一些大键值对的异步删除操作, Redis 6.0 版本之后引入了多线程来处理网络请求(提高网络 IO 读写性能)。
+对于读写命令来说,Redis 一直是单线程模型。不过,在 Redis 4.0 版本之后引入了多线程来执行一些大键值对的异步删除操作,Redis 6.0 版本之后引入了多线程来处理网络请求(提高网络 IO 读写性能)。
### Redis 单线程模型了解吗?
-**Redis 基于 Reactor 模式设计开发了一套高效的事件处理模型** (Netty 的线程模型也基于 Reactor 模式,Reactor 模式不愧是高性能 IO 的基石),这套事件处理模型对应的是 Redis 中的文件事件处理器(file event handler)。由于文件事件处理器(file event handler)是单线程方式运行的,所以我们一般都说 Redis 是单线程模型。
+**Redis 基于 Reactor 模式设计开发了一套高效的事件处理模型**(Netty 的线程模型也基于 Reactor 模式,Reactor 模式不愧是高性能 IO 的基石),这套事件处理模型对应的是 Redis 中的文件事件处理器(file event handler)。由于文件事件处理器(file event handler)是单线程方式运行的,所以我们一般都说 Redis 是单线程模型。
-《Redis 设计与实现》有一段话是如是介绍文件事件处理器的,我觉得写得挺不错。
+《Redis 设计与实现》有一段话是这样介绍文件事件处理器的,我觉得写得挺不错。
> Redis 基于 Reactor 模式开发了自己的网络事件处理器:这个处理器被称为文件事件处理器(file event handler)。
>
@@ -436,27 +577,31 @@ Redis 通过 **IO 多路复用程序** 来监听来自客户端的大量连接

-相关阅读:[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 之后新增了`UNLINK`(可以看作是 `DEL` 的异步版本)、`FLUSHALL ASYNC`(清空所有数据库的所有 key,不仅仅是当前 `SELECT` 的数据库)、`FLUSHDB ASYNC`(清空当前 `SELECT` 数据库中的所有 key)等异步命令。
+为此,Redis 4.0 之后新增了几个异步命令:
+
+- `UNLINK`:可以看作是 `DEL` 命令的异步版本。
+- `FLUSHALL ASYNC`:用于清空所有数据库的所有键,不限于当前 `SELECT` 的数据库。
+- `FLUSHDB ASYNC`:用于清空当前 `SELECT` 数据库中的所有键。

-大体上来说,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 之后为何引入了多线程?
@@ -475,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
```
-但是官网描述开启多线程读并不能有太大提升,因此一般情况下并不建议开启
+但是官网描述开启多线程读并不能有太大提升,因此一般情况下并不建议开启。
相关阅读:
@@ -493,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
@@ -523,11 +668,11 @@ void bioKillThreads(void);
## Redis 内存管理
-### Redis 给缓存数据设置过期时间有啥用?
+### Redis 给缓存数据设置过期时间有什么用?
一般情况下,我们设置保存的缓存数据的时候都会设置一个过期时间。为什么呢?
-因为内存是有限的,如果缓存中的所有数据都是一直保存的话,分分钟直接 Out of memory。
+内存是有限且珍贵的,如果不对缓存数据设置过期时间,那内存占用就会一直增长,最终可能会导致 OOM 问题。通过设置合理的过期时间,Redis 会自动删除暂时不需要的数据,为新的缓存数据腾出空间。
Redis 自带了给缓存数据设置过期时间的功能,比如:
@@ -540,7 +685,7 @@ OK
(integer) 56
```
-注意:**Redis 中除了字符串类型有自己独有设置过期时间的命令 `setex` 外,其他方法都需要依靠 `expire` 命令来设置过期时间 。另外, `persist` 命令可以移除一个键的过期时间。**
+注意 ⚠️:Redis 中除了字符串类型有自己独有设置过期时间的命令 `setex` 外,其他方法都需要依靠 `expire` 命令来设置过期时间 。另外,`persist` 命令可以移除一个键的过期时间。
**过期时间除了有助于缓解内存的消耗,还有什么其他用么?**
@@ -550,9 +695,9 @@ OK
### Redis 是如何判断数据是否过期的呢?
-Redis 通过一个叫做过期字典(可以看作是 hash 表)来保存数据过期的时间。过期字典的键指向 Redis 数据库中的某个 key(键),过期字典的值是一个 long long 类型的整数,这个整数保存了 key 所指向的数据库键的过期时间(毫秒精度的 UNIX 时间戳)。
+Redis 通过一个叫做过期字典(可以看作是 hash 表)来保存数据过期的时间。过期字典的键指向 Redis 数据库中的某个 key(键),过期字典的值是一个 long long 类型的整数,这个整数保存了 key 所指向的数据库键的过期时间(毫秒精度的 UNIX 时间戳)。
-
+
过期字典是存储在 redisDb 这个结构里的:
@@ -566,44 +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 版本)。
+
+
+
+类似的参数还有一个 **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 04e9bc854ad..39c889cbb04 100644
--- a/docs/database/redis/redis-questions-02.md
+++ b/docs/database/redis/redis-questions-02.md
@@ -28,7 +28,7 @@ Redis 事务实际开发中使用的非常少,功能比较鸡肋,不要将
### 如何使用 Redis 事务?
-Redis 可以通过 **`MULTI`,`EXEC`,`DISCARD` 和 `WATCH`** 等命令来实现事务(Transaction)功能。
+Redis 可以通过 **`MULTI`、`EXEC`、`DISCARD` 和 `WATCH`** 等命令来实现事务(Transaction)功能。
```bash
> MULTI
@@ -47,8 +47,8 @@ QUEUED
这个过程是这样的:
1. 开始事务(`MULTI`);
-2. 命令入队(批量操作 Redis 的命令,先进先出(FIFO)的顺序执行);
-3. 执行事务(`EXEC`)。
+2. 命令入队(批量操作 Redis 的命令,先进先出(FIFO)的顺序执行);
+3. 执行事务(`EXEC`)。
你也可以通过 [`DISCARD`](https://redis.io/commands/discard) 命令取消一个事务,它会清空事务队列中保存的所有命令。
@@ -138,10 +138,10 @@ Redis 官网相关介绍 [https://redis.io/topics/transactions](https://redis.io
Redis 的事务和我们平时理解的关系型数据库的事务不同。我们知道事务具有四大特性:**1. 原子性**,**2. 隔离性**,**3. 持久性**,**4. 一致性**。
-1. **原子性(Atomicity):** 事务是最小的执行单位,不允许分割。事务的原子性确保动作要么全部完成,要么完全不起作用;
-2. **隔离性(Isolation):** 并发访问数据库时,一个用户的事务不被其他事务所干扰,各并发事务之间数据库是独立的;
-3. **持久性(Durability):** 一个事务被提交之后。它对数据库中数据的改变是持久的,即使数据库发生故障也不应该对其有任何影响。
-4. **一致性(Consistency):** 执行事务前后,数据保持一致,多个事务对同一个数据读取的结果是相同的;
+1. **原子性(Atomicity)**:事务是最小的执行单位,不允许分割。事务的原子性确保动作要么全部完成,要么完全不起作用;
+2. **隔离性(Isolation)**:并发访问数据库时,一个用户的事务不被其他事务所干扰,各并发事务之间数据库是独立的;
+3. **持久性(Durability)**:一个事务被提交之后。它对数据库中数据的改变是持久的,即使数据库发生故障也不应该对其有任何影响;
+4. **一致性(Consistency)**:执行事务前后,数据保持一致,多个事务对同一个数据读取的结果是相同的。
Redis 事务在运行错误的情况下,除了执行过程中出现错误的命令外,其他命令都能正常执行。并且,Redis 事务是不支持回滚(roll back)操作的。因此,Redis 事务其实是不满足原子性的。
@@ -149,28 +149,28 @@ Redis 官网也解释了自己为啥不支持回滚。简单来说就是 Redis

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

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

@@ -381,7 +381,7 @@ bigkey 的常见处理以及优化办法如下(这些方法可以配合起来
- **分割 bigkey**:将一个 bigkey 分割为多个小 key。例如,将一个含有上万字段数量的 Hash 按照一定策略(比如二次哈希)拆分为多个 Hash。
- **手动清理**:Redis 4.0+ 可以使用 `UNLINK` 命令来异步删除一个或多个指定的 key。Redis 4.0 以下可以考虑使用 `SCAN` 命令结合 `DEL` 命令来分批次删除。
- **采用合适的数据结构**:例如,文件二进制数据不使用 String 保存、使用 HyperLogLog 统计页面 UV、Bitmap 保存状态信息(0/1)。
-- **开启 lazy-free(惰性删除/延迟释放)** :lazy-free 特性是 Redis 4.0 开始引入的,指的是让 Redis 采用异步方式延迟释放 key 使用的内存,将该操作交给单独的子线程处理,避免阻塞主线程。
+- **开启 lazy-free(惰性删除/延迟释放)**:lazy-free 特性是 Redis 4.0 开始引入的,指的是让 Redis 采用异步方式延迟释放 key 使用的内存,将该操作交给单独的子线程处理,避免阻塞主线程。
### Redis hotkey(热 Key)
@@ -432,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
@@ -473,7 +473,7 @@ OK
如果你用的是公有云的 Redis 服务的话,可以看看其是否提供了 key 分析功能(一般都提供了)。
-这里以阿里云 Redis 为例说明,它支持 hotkey 实时分析、发现,文档地址: 。
+这里以阿里云 Redis 为例说明,它支持 hotkey 实时分析、发现,文档地址:。

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

@@ -616,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 代码展示的话,差不多是下面这样的:
@@ -645,37 +647,35 @@ public Object getObjectInclNullById(Integer id) {
**2)布隆过滤器**
-布隆过滤器是一个非常神奇的数据结构,通过它我们可以非常方便地判断一个给定数据是否存在于海量数据中。我们需要的就是判断 key 是否合法,有没有感觉布隆过滤器就是我们想要找的那个“人”。
+布隆过滤器是一个非常神奇的数据结构,通过它我们可以非常方便地判断一个给定数据是否存在于海量数据中。我们可以把它看作由二进制向量(或者说位数组)和一系列随机映射函数(哈希函数)两部分组成的数据结构。相比于我们平时常用的 List、Map、Set 等数据结构,它占用空间更少并且效率更高,但是缺点是其返回的结果是概率性的,而不是非常准确的。理论情况下添加到集合中的元素越多,误报的可能性就越大。并且,存放在布隆过滤器的数据不容易删除。
-具体是这样做的:把所有可能存在的请求的值都存放在布隆过滤器中,当用户请求过来,先判断用户发来的请求的值是否存在于布隆过滤器中。不存在的话,直接返回请求参数错误信息给客户端,存在的话才会走下面的流程。
+
-加入布隆过滤器之后的缓存处理流程图如下。
+Bloom Filter 会使用一个较大的 bit 数组来保存所有的数据,数组中的每个元素都只占用 1 bit ,并且每个元素只能是 0 或者 1(代表 false 或者 true),这也是 Bloom Filter 节省内存的核心所在。这样来算的话,申请一个 100w 个元素的位数组只占用 1000000Bit / 8 = 125000 Byte = 125000/1024 KB ≈ 122KB 的空间。
-
+
-但是,需要注意的是布隆过滤器可能会存在误判的情况。总结来说就是:**布隆过滤器说某个元素存在,小概率会误判。布隆过滤器说某个元素不在,那么这个元素一定不在。**
+具体是这样做的:把所有可能存在的请求的值都存放在布隆过滤器中,当用户请求过来,先判断用户发来的请求的值是否存在于布隆过滤器中。不存在的话,直接返回请求参数错误信息给客户端,存在的话才会走下面的流程。
-_为什么会出现误判的情况呢? 我们还要从布隆过滤器的原理来说!_
+加入布隆过滤器之后的缓存处理流程图如下:
-我们先来看一下,**当一个元素加入布隆过滤器中的时候,会进行哪些操作:**
+
-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 对应的是 **热点数据**,该数据 **存在于数据库中,但不存在于缓存中(通常是因为缓存中的那份数据已经过期)**。这就可能会导致瞬时大量的请求直接打到了数据库上,对数据库造成了巨大的压力,可能直接就被这么多请求弄宕机了。

@@ -683,9 +683,9 @@ _为什么会出现误判的情况呢? 我们还要从布隆过滤器的原理
#### 有哪些解决办法?
-- 设置热点数据永不过期或者过期时间比较长。
-- 针对热点数据提前预热,将其存入缓存中并设置合理的过期时间比如秒杀场景下的数据在秒杀结束之前不过期。
-- 请求数据库写数据到缓存之前,先获取互斥锁,保证只有一个请求会落到数据库上,减少数据库的压力。
+1. **永不过期**(不推荐):设置热点数据永不过期或者过期时间比较长。
+2. **提前预热**(推荐):针对热点数据提前预热,将其存入缓存中并设置合理的过期时间比如秒杀场景下的数据在秒杀结束之前不过期。
+3. **加锁**(看情况):在缓存失效后,通过设置互斥锁确保只有一个请求去查询数据库并更新缓存。
#### 缓存穿透和缓存击穿有什么区别?
@@ -705,37 +705,56 @@ _为什么会出现误判的情况呢? 我们还要从布隆过滤器的原理

-举个例子:数据库中的大量数据在同一时间过期,这个时候突然有大量的请求需要访问这些过期的数据。这就导致大量的请求直接落到数据库上,对数据库造成了巨大的压力。
+举个例子:缓存中的大量数据在同一时间过期,这个时候突然有大量的请求需要访问这些过期的数据。这就导致大量的请求直接落到数据库上,对数据库造成了巨大的压力。
#### 有哪些解决办法?
-**针对 Redis 服务不可用的情况:**
+**针对 Redis 服务不可用的情况**:
+
+1. **Redis 集群**:采用 Redis 集群,避免单机出现问题整个缓存服务都没办法使用。Redis Cluster 和 Redis Sentinel 是两种最常用的 Redis 集群实现方案,详细介绍可以参考:[Redis 集群详解(付费)](https://javaguide.cn/database/redis/redis-cluster.html)。
+2. **多级缓存**:设置多级缓存,例如本地缓存+Redis 缓存的二级缓存组合,当 Redis 缓存出现问题时,还可以从本地缓存中获取到部分数据。
+
+**针对大量缓存同时失效的情况**:
-1. 采用 Redis 集群,避免单机出现问题整个缓存服务都没办法使用。
-2. 限流,避免同时处理大量的请求。
+1. **设置随机失效时间**(可选):为缓存设置随机的失效时间,例如在固定过期时间的基础上加上一个随机值,这样可以避免大量缓存同时到期,从而减少缓存雪崩的风险。
+2. **提前预热**(推荐):针对热点数据提前预热,将其存入缓存中并设置合理的过期时间,比如秒杀场景下的数据在秒杀结束之前不过期。
+3. **持久缓存策略**(看情况):虽然一般不推荐设置缓存永不过期,但对于某些关键性和变化不频繁的数据,可以考虑这种策略。
-**针对热点缓存失效的情况:**
+#### 缓存预热如何实现?
-1. 设置不同的失效时间比如随机设置缓存的失效时间。
-2. 缓存永不失效(不太推荐,实用性太差)。
-3. 设置二级缓存。
+常见的缓存预热方式有两种:
+
+1. 使用定时任务,比如 xxl-job,来定时触发缓存预热的逻辑,将数据库中的热点数据查询出来并存入缓存中。
+2. 使用消息队列,比如 Kafka,来异步地进行缓存预热,将数据库中的热点数据的主键或者 ID 发送到消息队列中,然后由缓存服务消费消息队列中的数据,根据主键或者 ID 查询数据库并更新缓存。
#### 缓存雪崩和缓存击穿有什么区别?
-缓存雪崩和缓存击穿比较像,但缓存雪崩导致的原因是缓存中的大量或者所有数据失效,缓存击穿导致的原因主要是某个热点数据不存在与缓存中(通常是因为缓存中的那份数据已经过期)。
+缓存雪崩和缓存击穿比较像,但缓存雪崩导致的原因是缓存中的大量或者所有数据失效,缓存击穿导致的原因主要是某个热点数据不存在于缓存中(通常是因为缓存中的那份数据已经过期)。
### 如何保证缓存和数据库数据的一致性?
-细说的话可以扯很多,但是我觉得其实没太大必要(小声 BB:很多解决方案我也没太弄明白)。我个人觉得引入缓存之后,如果为了短时间的不一致性问题,选择让系统设计变得更加复杂的话,完全没必要。
+缓存和数据库一致性是个挺常见的技术挑战。引入缓存主要是为了提升性能、减轻数据库压力,但确实会带来数据不一致的风险。绝对的一致性往往意味着更高的系统复杂度和性能开销,所以实践中我们通常会根据业务场景选择合适的策略,在性能和一致性之间找到一个平衡点。
+
+下面单独对 **Cache Aside Pattern(旁路缓存模式)** 来聊聊。这是非常常用的一种缓存读写策略,它的读写逻辑是这样的:
+
+- **读操作**:
+ 1. 先尝试从缓存读取数据。
+ 2. 如果缓存命中,直接返回数据。
+ 3. 如果缓存未命中,从数据库查询数据,将查到的数据放入缓存并返回数据。
+- **写操作**:
+ 1. 先更新数据库。
+ 2. 再直接删除缓存中对应的数据。
+
+图解如下:
-下面单独对 **Cache Aside Pattern(旁路缓存模式)** 来聊聊。
+
-Cache Aside Pattern 中遇到写请求是这样的:更新 DB,然后直接删除 cache 。
+
-如果更新数据库成功,而删除缓存这一步失败的情况的话,简单说两个解决方案:
+如果更新数据库成功,而删除缓存这一步失败的情况的话,简单说有两个解决方案:
-1. **缓存失效时间变短(不推荐,治标不治本)**:我们让缓存数据的过期时间变短,这样的话缓存就会从数据库中加载数据。另外,这种解决办法对于先操作缓存后操作数据库的场景不适用。
-2. **增加 cache 更新重试机制(常用)**:如果 cache 服务当前不可用导致缓存删除失败的话,我们就隔一段时间进行重试,重试次数可以自己定。如果多次重试还是失败的话,我们可以把当前更新失败的 key 存入队列中,等缓存服务可用之后,再将缓存中对应的 key 删除即可。
+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)。
@@ -748,18 +767,18 @@ Cache Aside Pattern 中遇到写请求是这样的:更新 DB,然后直接删
**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 扩容缩容期间可以提供服务吗?
@@ -772,23 +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. ……
-相关文章推荐:[阿里云 Redis 开发规范](https://developer.aliyun.com/article/531067) 。
+相关文章推荐:[阿里云 Redis 开发规范](https://developer.aliyun.com/article/531067)。
## 参考
- 《Redis 开发与运维》
- 《Redis 设计与实现》
-- Redis Transactions :
+- Redis Transactions:
- What is Redis Pipeline:
- 一文详解 Redis 中 BigKey、HotKey 的发现与处理:
-- Bigkey 问题的解决思路与方式探索:
+- Bigkey 问题的解决思路与方式探索:
- Redis 延迟问题全面排障指南:
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 的有序集合底层实现的跳表有着更深刻的理解和掌握。
+
+
+
+## 跳表在 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 级索引,每级索引的索引个数都是基于下层元素个数的一半。
+
+
+
+假如我们需要查询元素 6,其工作流程如下:
+
+1. 从 2 级索引开始,先来到节点 4。
+2. 查看 4 的后继节点,是 8 的 2 级索引,这个值大于 6,说明 2 级索引后续的索引都是大于 6 的,没有再往后搜寻的必要,我们索引向下查找。
+3. 来到 4 的 1 级索引,比对其后继节点为 6,查找结束。
+
+相较于原始有序链表需要 6 次,我们的跳表通过建立多级索引,我们只需两次就直接定位到了目标元素,其查寻的复杂度被直接优化为**O(log n)**。
+
+
+
+对应的添加也是一个道理,假如我们需要在这个有序集合中添加一个元素 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 插入。
+
+
+
+这里我们又面临一个问题,我们是否需要为元素 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 级索引:
+
+
+
+最后我们再来说说删除,假设我们这里要删除元素 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 删除。
+
+
+
+### 模板定义
+
+有了整体的思路之后,我们可以开始实现一个跳表了,首先定义一下跳表中的节点**Node**,从上文的演示中可以看出每一个**Node**它都包含以下几个元素:
+
+1. 存储的**value**值。
+2. 后继节点的地址。
+3. 多级索引。
+
+为了更方便统一管理**Node**后继节点地址和多级索引指向的元素地址,笔者在**Node**中设置了一个**forwards**数组,用于记录原始链表节点的后继节点和多级索引的后继节点指向。
+
+以下图为例,我们**forwards**数组长度为 5,其中**索引 0**记录的是原始链表节点的后继节点地址,而其余自底向上表示从 1 级索引到 4 级索引的后继节点指向。
+
+
+
+于是我们的就有了这样一个代码定义,可以看出笔者对于数组的长度设置为固定的 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,三级索引为空。
+
+
+
+然后我们基于这个数组**maxOfMinArr** 定位到各级的后继节点,让插入的元素 5 指向这些后继节点,而**maxOfMinArr**指向 5,结果如下图:
+
+
+
+转化成代码就是下面这个形式,是不是很简单呢?我们继续:
+
+```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:
+
+
+
+- **从最高层级开始 (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 回收。
+
+
+
+```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)** 。
+
+对于范围查询来说,它也可以通过中序遍历的方式达到和跳表一样的效果。但是它的每一次插入或者删除操作都需要保证整颗树左右节点的绝对平衡,只要不平衡就要通过旋转操作来保持平衡,这个过程是比较耗时的。
+
+
+
+跳表诞生的初衷就是为了克服平衡树的一些缺点,跳表的发明者在论文[《Skip lists: a probabilistic alternative to balanced trees》](https://15721.courses.cs.cmu.edu/spring2018/papers/08-oltpindexes1/pugh-skiplists-cacm1990.pdf)中有详细提到:
+
+
+
+> 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)。
+
+相比较于红黑树来说,跳表的实现也更简单一些。并且,按照区间来查找数据这个操作,红黑树的效率没有跳表高。
+
+
+
+对应红黑树添加的核心代码如下,读者可自行参阅理解:
+
+```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+树插入时可能会导致数据重新分布,使得数据在整棵树分布更加均匀,保证范围查询和删除效率。
+
+
+
+所以,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 228546164a7..4bf08f0fa0b 100644
--- a/docs/database/sql/sql-questions-01.md
+++ b/docs/database/sql/sql-questions-01.md
@@ -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`
@@ -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
diff --git a/docs/database/sql/sql-questions-03.md b/docs/database/sql/sql-questions-03.md
index 78590e2a97d..f5acd8fc5c8 100644
--- a/docs/database/sql/sql-questions-03.md
+++ b/docs/database/sql/sql-questions-03.md
@@ -216,9 +216,9 @@ WHERE info.exam_id = record.exam_id
| total_pv | complete_pv | complete_exam_cnt |
| -------- | ----------- | ----------------- |
-| 11 | 7 | 2 |
+| 10 | 7 | 2 |
-解释:表示截止当前,有 11 次试卷作答记录,已完成的作答次数为 7 次(中途退出的为未完成状态,其交卷时间和份数为 NULL),已完成的试卷有 9001 和 9002 两份。
+解释:表示截止当前,有 10 次试卷作答记录,已完成的作答次数为 7 次(中途退出的为未完成状态,其交卷时间和份数为 NULL),已完成的试卷有 9001 和 9002 两份。
**思路**: 这题一看到统计次数,肯定第一时间就要想到用`COUNT`这个函数来解决,问题是要统计不同的记录,该怎么来写?使用子查询就能解决这个题目(这题用 case when 也能写出来,解法类似,逻辑不同而已);首先在做这个题之前,让我们先来了解一下`COUNT`的基本用法;
@@ -858,7 +858,7 @@ ORDER BY level_cnt DESC
**解释**:“试卷”有 3 人共练习 3 次试卷 9001,1 人作答 3 次 9002;“刷题”有 3 人刷 5 次 8001,有 2 人刷 2 次 8002
-**思路**:这题的难点和易错点在于`UNOIN`和`ORDER BY` 同时使用的问题
+**思路**:这题的难点和易错点在于`UNION`和`ORDER BY` 同时使用的问题
有以下几种情况:使用`union`和多个`order by`不加括号,报错!
diff --git a/docs/database/sql/sql-questions-04.md b/docs/database/sql/sql-questions-04.md
index c15eb8e2243..84f1a2b3c8c 100644
--- a/docs/database/sql/sql-questions-04.md
+++ b/docs/database/sql/sql-questions-04.md
@@ -142,20 +142,20 @@ WHERE ranking <= 3
试卷作答记录表 `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:01: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 | 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 降序排序。由示例数据结果输出如下:
@@ -163,7 +163,7 @@ WHERE ranking <= 3
| ------- | -------- | ------------------- |
| 9001 | 60 | 2021-09-01 06:00:00 |
-**解释**:试卷 9001 被作答用时有 50 分钟、50 分钟、30 分 1 秒、11 分钟、10 分钟,第二快和第二慢用时之差为 50 分钟-11 分钟=39 分钟,试卷时长为 60 分钟,因此满足大于试卷时长一半的条件,输出试卷 ID、时长、发布时间。
+**解释**:试卷 9001 被作答用时有 50 分钟、58 分钟、30 分 1 秒、19 分钟、10 分钟,第二快和第二慢用时之差为 50 分钟-19 分钟=31 分钟,试卷时长为 60 分钟,因此满足大于试卷时长一半的条件,输出试卷 ID、时长、发布时间。
**思路:**
diff --git a/docs/database/sql/sql-questions-05.md b/docs/database/sql/sql-questions-05.md
index c20af2cad39..88084dddfbe 100644
--- a/docs/database/sql/sql-questions-05.md
+++ b/docs/database/sql/sql-questions-05.md
@@ -41,26 +41,53 @@ tag:
写法 1:
```sql
-SELECT exam_id,
- count(submit_time IS NULL OR NULL) incomplete_cnt,
- ROUND(count(submit_time IS NULL OR NULL) / count(*), 3) complete_rate
-FROM exam_record
-GROUP BY exam_id
-HAVING incomplete_cnt <> 0
+SELECT
+ exam_id,
+ (COUNT(*) - COUNT(submit_time)) AS incomplete_cnt,
+ ROUND((COUNT(*) - COUNT(submit_time)) / COUNT(*), 3) AS incomplete_rate
+FROM
+ exam_record
+GROUP BY
+ exam_id
+HAVING
+ (COUNT(*) - COUNT(submit_time)) > 0;
```
+利用 `COUNT(*) `统计分组内的总记录数,`COUNT(submit_time)` 只统计 `submit_time` 字段不为 NULL 的记录数(即已完成数)。两者相减,就是未完成数。
+
写法 2:
```sql
-SELECT exam_id,
- count(submit_time IS NULL OR NULL) incomplete_cnt,
- ROUND(count(submit_time IS NULL OR NULL) / count(*), 3) complete_rate
-FROM exam_record
-GROUP BY exam_id
-HAVING incomplete_cnt <> 0
+SELECT
+ exam_id,
+ COUNT(CASE WHEN submit_time IS NULL THEN 1 END) AS incomplete_cnt,
+ ROUND(COUNT(CASE WHEN submit_time IS NULL THEN 1 END) / COUNT(*), 3) AS incomplete_rate
+FROM
+ exam_record
+GROUP BY
+ exam_id
+HAVING
+ COUNT(CASE WHEN submit_time IS NULL THEN 1 END) > 0;
+```
+
+使用 `CASE` 表达式,当条件满足时返回一个非 `NULL` 值(例如 1),否则返回 `NULL`。然后用 `COUNT` 函数来统计非 `NULL` 值的数量。
+
+写法 3:
+
+```sql
+SELECT
+ exam_id,
+ SUM(submit_time IS NULL) AS incomplete_cnt,
+ ROUND(SUM(submit_time IS NULL) / COUNT(*), 3) AS incomplete_rate
+FROM
+ exam_record
+GROUP BY
+ exam_id
+HAVING
+ incomplete_cnt > 0;
```
-两种写法都可以,只有中间的写法不一样,一个是对符合条件的才`COUNT`,一个是直接上`IF`,后者更为直观,最后这个`having`解释一下, 无论是 `complete_rate` 还是 `incomplete_cnt`,只要不为 0 即可,不为 0 就意味着有未完成的。
+利用 `SUM` 函数对一个表达式求和。当 `submit_time` 为 `NULL` 时,表达式 `(submit_time IS NULL)` 的值为 1 (TRUE),否则为 0 (FALSE)。将这些 1 和 0 加起来,就得到了未完成的数量。
### 0 级用户高难度试卷的平均用时和平均得分
diff --git a/docs/database/sql/sql-syntax-summary.md b/docs/database/sql/sql-syntax-summary.md
index 467a6f14688..cff0b931495 100644
--- a/docs/database/sql/sql-syntax-summary.md
+++ b/docs/database/sql/sql-syntax-summary.md
@@ -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`**:
@@ -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;
@@ -867,7 +867,7 @@ COMMIT;
## 权限控制
-要授予用户帐户权限,可以用`GRANT`命令。有撤销用户的权限,可以用`REVOKE`命令。这里以 MySQl 为例,介绍权限控制实际应用。
+要授予用户帐户权限,可以用`GRANT`命令。要撤销用户的权限,可以用`REVOKE`命令。这里以 MySQL 为例,介绍权限控制实际应用。
`GRANT`授予权限语法:
@@ -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 版之前,可以为每个表定义最多六个触发器。
diff --git a/docs/distributed-system/distributed-id-design.md b/docs/distributed-system/distributed-id-design.md
index adbf61c9803..5b737f34593 100644
--- a/docs/distributed-system/distributed-id-design.md
+++ b/docs/distributed-system/distributed-id-design.md
@@ -99,7 +99,7 @@ UA 是一个特殊字符串头,服务器依次可以识别出客户使用的
abcdefghijklmnopqrstuvwxyzABCDEFGHJKLMNPQRSTUVWXZY0123456789
-之前说过,兑换码要求近可能简洁,那么设计时就需要考虑兑换码的字符数,假设上限为 12 位,而字符空间有 60 位,那么可以表示的空间范围为 60^12=130606940160000000000000(也就是可以 12 位的兑换码可以生成天量,应该够运营同学挥霍了),转换成 2 进制:
+之前说过,兑换码要求尽可能简洁,那么设计时就需要考虑兑换码的字符数,假设上限为 12 位,而字符空间有 60 位,那么可以表示的空间范围为 60^12=130606940160000000000000(也就是可以 12 位的兑换码可以生成天量,应该够运营同学挥霍了),转换成 2 进制:
1001000100000000101110011001101101110011000000000000000000000(61 位)
diff --git a/docs/distributed-system/distributed-id.md b/docs/distributed-system/distributed-id.md
index b314258cbb8..9920f8f7753 100644
--- a/docs/distributed-system/distributed-id.md
+++ b/docs/distributed-system/distributed-id.md
@@ -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 举例,我们通过下面的方式即可。
@@ -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,7 +265,7 @@ int version = uuid.version();// 4
最后,我们再简单分析一下 **UUID 的优缺点** (面试的时候可能会被问到的哦!) :
-- **优点**:生成速度比较快、简单易用
+- **优点**:生成速度通常比较快、简单易用
- **缺点**:存储消耗空间大(32 个字符串,128 位)、 不安全(基于 MAC 地址生成 UUID 的算法会造成 MAC 地址泄露)、无序(非自增)、没有具体业务含义、需要解决重复 ID 问题(当机器时间不对的情况下,可能导致会产生重复 ID)
#### Snowflake(雪花算法)
@@ -270,10 +274,10 @@ Snowflake 是 Twitter 开源的分布式 ID 生成算法。Snowflake 由 64 bit

-- **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。
+- **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 算法生成的 ID 中加入业务类型信息。
@@ -299,10 +303,10 @@ Snowflake 是 Twitter 开源的分布式 ID 生成算法。Snowflake 由 64 bit

-- **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 个并发。
+- **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 的组成不太一样。并且,上面这些参数我们都可以自定义。
@@ -374,9 +378,9 @@ IdGenerator 生成的唯一 ID 组成如下:

-- **timestamp (位数不固定)**,时间差,是生成 ID 时的系统时间减去 BaseTime(基础时间,也称基点时间、原点时间、纪元时间,默认值为 2020 年) 的总时间差(毫秒单位)。初始为 5bits,随着运行时间而增加。如果觉得默认值太老,你可以重新设置,不过要注意,这个值以后最好不变。
-- **worker id (默认 6 bits)**: 机器 id,机器码,最重要参数,是区分不同机器或不同应用的唯一 ID,最大值由 `WorkerIdBitLength`(默认 6)限定。如果一台服务器部署多个独立服务,需要为每个服务指定不同的 WorkerId。
-- **sequence (默认 6 bits)**,序列数,是每毫秒下的序列数,由参数中的 `SeqBitLength`(默认 6)限定。增加 `SeqBitLength` 会让性能更高,但生成的 ID 也会更长。
+- **timestamp (位数不固定)**:时间差,是生成 ID 时的系统时间减去 BaseTime(基础时间,也称基点时间、原点时间、纪元时间,默认值为 2020 年) 的总时间差(毫秒单位)。初始为 5bits,随着运行时间而增加。如果觉得默认值太老,你可以重新设置,不过要注意,这个值以后最好不变。
+- **worker id (默认 6 bits)**:机器 id,机器码,最重要参数,是区分不同机器或不同应用的唯一 ID,最大值由 `WorkerIdBitLength`(默认 6)限定。如果一台服务器部署多个独立服务,需要为每个服务指定不同的 WorkerId。
+- **sequence (默认 6 bits)**:序列数,是每毫秒下的序列数,由参数中的 `SeqBitLength`(默认 6)限定。增加 `SeqBitLength` 会让性能更高,但生成的 ID 也会更长。
Java 语言使用示例:。
diff --git a/docs/distributed-system/distributed-lock-implementations.md b/docs/distributed-system/distributed-lock-implementations.md
index 73fe6059eaf..cb4504c4a7a 100644
--- a/docs/distributed-system/distributed-lock-implementations.md
+++ b/docs/distributed-system/distributed-lock-implementations.md
@@ -56,7 +56,7 @@ OK
```
- **lockKey**:加锁的锁名;
-- **uniqueValue**:能够唯一标示锁的随机字符串;
+- **uniqueValue**:能够唯一标识锁的随机字符串;
- **NX**:只有当 lockKey 对应的 key 值不存在的时候才能 SET 成功;
- **EX**:过期时间设置(秒为单位)EX 3 标示这个锁有一个 3 秒的自动过期时间。与 EX 对应的是 PX(毫秒为单位),这两个都是过期时间设置。
@@ -202,13 +202,11 @@ Redlock 是直接操作 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 来做,只是性能会差一些。
+实际项目中不建议使用 Redlock 算法,成本和收益不成正比,可以考虑基于 Redis 主从复制+哨兵模式实现分布式锁。
## 基于 ZooKeeper 实现分布式锁
-Redis 实现分布式锁性能较高,ZooKeeper 实现分布式锁可靠性更高。实际项目中,我们应该根据业务的具体需求来选择。
+ZooKeeper 相比于 Redis 实现分布式锁,除了提供相对更高的可靠性之外,在功能层面还有一个非常有用的特性:**Watch 机制**。这个机制可以用来实现公平的分布式锁。不过,使用 ZooKeeper 实现的分布式锁在性能方面相对较差,因此如果对性能要求比较高的话,ZooKeeper 可能就不太适合了。
### 如何基于 ZooKeeper 实现分布式锁?
@@ -365,6 +363,19 @@ private static class LockData
## 总结
-这篇文章我们介绍了实现分布式锁的两种常见方式。至于具体选择 Redis 还是 ZooKeeper 来实现分布式锁,还是要看业务的具体需求。如果对性能要求比较高的话,建议使用 Redis 实现分布式锁。如果对可靠性要求比较高的话,建议使用 ZooKeeper 实现分布式锁。
+在这篇文章中,我介绍了实现分布式锁的两种常见方式:**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 2f7f0289ee2..ba53f443d03 100644
--- a/docs/distributed-system/distributed-lock.md
+++ b/docs/distributed-system/distributed-lock.md
@@ -30,7 +30,7 @@ category: 分布式
悲观锁总是假设最坏的情况,认为共享资源每次被访问的时候就会出现问题(比如共享数据被修改),所以每次在获取资源操作的时候都会上锁,这样其他线程想拿到这个资源就会阻塞直到锁被上一个持有者释放。也就是说,**共享资源每次只给一个线程使用,其它线程阻塞,用完后再把资源转让给其它线程**。
-对于单机多线程来说,在 Java 中,我们通常使用 `ReetrantLock` 类、`synchronized` 关键字这类 JDK 自带的 **本地锁** 来控制一个 JVM 进程内的多个线程对本地共享资源的访问。
+对于单机多线程来说,在 Java 中,我们通常使用 `ReentrantLock` 类、`synchronized` 关键字这类 JDK 自带的 **本地锁** 来控制一个 JVM 进程内的多个线程对本地共享资源的访问。
下面是我对本地锁画的一张示意图。
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 c0c6d11b775..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,5 +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 4cdb7486a2f..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,7 +9,7 @@ tag:
## 什么是 ZooKeeper
-`ZooKeeper` 由 `Yahoo` 开发,后来捐赠给了 `Apache` ,现已成为 `Apache` 顶级项目。`ZooKeeper` 是一个开源的分布式应用程序协调服务器,其为分布式系统提供一致性服务。其一致性是通过基于 `Paxos` 算法的 `ZAB` 协议完成的。其主要功能包括:配置维护、分布式同步、集群管理、分布式事务等。
+`ZooKeeper` 由 `Yahoo` 开发,后来捐赠给了 `Apache` ,现已成为 `Apache` 顶级项目。`ZooKeeper` 是一个开源的分布式应用程序协调服务器,其为分布式系统提供一致性服务。其一致性是通过基于 `Paxos` 算法的 `ZAB` 协议完成的。其主要功能包括:配置维护、分布式同步、集群管理等。
简单来说, `ZooKeeper` 是一个 **分布式协调服务框架** 。分布式?协调服务?这啥玩意?🤔🤔
@@ -299,6 +299,24 @@ tag:
总的来说,我们可以完全 **利用 临时节点、节点状态 和 `watcher` 来实现选主的功能**,临时节点主要用来选举,节点状态和`watcher` 可以用来判断 `master` 的活性和进行重新选举。
+### 数据发布/订阅
+
+还记得 Zookeeper 的 `Watcher` 机制吗? Zookeeper 通过这种推拉相结合的方式实现客户端与服务端的交互:客户端向服务端注册节点,一旦相应节点的数据变更,服务端就会向“监听”该节点的客户端发送 `Watcher` 事件通知,客户端接收到通知后需要 **主动** 到服务端获取最新的数据。基于这种方式,Zookeeper 实现了 **数据发布/订阅** 功能。
+
+一个典型的应用场景为 **全局配置信息的集中管理**。 客户端在启动时会主动到 Zookeeper 服务端获取配置信息,同时 **在指定节点注册一个** `Watcher` **监听**。当配置信息发生变更,服务端通知所有订阅的客户端重新获取配置信息,实现配置信息的实时更新。
+
+上面所提到的全局配置信息通常包括机器列表信息、运行时的开关配置、数据库配置信息等。需要注意的是,这类全局配置信息通常具备以下特性:
+
+- 数据量较小
+- 数据内容在运行时动态变化
+- 集群中机器共享一致配置
+
+### 负载均衡
+
+可以通过 Zookeeper 的 **临时节点** 实现负载均衡。回顾一下临时节点的特性:当创建节点的客户端与服务端之间断开连接,即客户端会话(session)消失时,对应节点也会自动消失。因此,我们可以使用临时节点来维护 Server 的地址列表,从而保证请求不会被分配到已停机的服务上。
+
+具体地,我们需要在集群的每一个 Server 中都使用 Zookeeper 客户端连接 Zookeeper 服务端,同时用 Server **自身的地址信息**在服务端指定目录下创建临时节点。当客户端请求调用集群服务时,首先通过 Zookeeper 获取该目录下的节点列表 (即所有可用的 Server),随后根据不同的负载均衡策略将请求转发到某一具体的 Server。
+
### 分布式锁
分布式锁的实现方式有很多种,比如 `Redis`、数据库、`zookeeper` 等。个人认为 `zookeeper` 在实现分布式锁这方面是非常非常简单的。
diff --git a/docs/distributed-system/protocol/gossip-protocl.md b/docs/distributed-system/protocol/gossip-protocl.md
index f6c7ecb1ca7..5590401a9b6 100644
--- a/docs/distributed-system/protocol/gossip-protocl.md
+++ b/docs/distributed-system/protocol/gossip-protocl.md
@@ -53,7 +53,7 @@ Redis Cluster 的节点之间会相互发送多种 Gossip 消息:

-有了 Redis Cluster 之后,不需要专门部署 Sentinel 集群服务了。Redis Cluster 相当于是内置了 Sentinel 机制,Redis Cluster 内部的各个 Redis 节点通过 Gossip 协议互相探测健康状态,在故障时可以自动切换。
+有了 Redis Cluster 之后,不需要专门部署 Sentinel 集群服务了。Redis Cluster 相当于是内置了 Sentinel 机制,Redis Cluster 内部的各个 Redis 节点通过 Gossip 协议共享集群内信息。
关于 Redis Cluster 的详细介绍,可以查看这篇文章 [Redis 集群详解(付费)](https://javaguide.cn/database/redis/redis-cluster.html) 。
@@ -67,7 +67,7 @@ Gossip 设计了两种可能的消息传播模式:**反熵(Anti-Entropy)**
> 熵的概念最早起源于[物理学](https://zh.wikipedia.org/wiki/物理学),用于度量一个热力学系统的混乱程度。熵最好理解为不确定性的量度而不是确定性的量度,因为越随机的信源的熵越大。
-在这里,你可以把反熵中的熵了解为节点之间数据的混乱程度/差异性,反熵就是指消除不同节点中数据的差异,提升节点间数据的相似度,从而降低熵值。
+在这里,你可以把反熵中的熵理解为节点之间数据的混乱程度/差异性,反熵就是指消除不同节点中数据的差异,提升节点间数据的相似度,从而降低熵值。
具体是如何反熵的呢?集群中的节点,每隔段时间就随机选择某个其他节点,然后通过互相交换自己的所有数据来消除两者之间的差异,实现数据的最终一致性。
@@ -81,7 +81,7 @@ Gossip 设计了两种可能的消息传播模式:**反熵(Anti-Entropy)**

-在我们实际应用场景中,一般不会采用随机的节点进行反熵,而是需要可以的设计一个闭环。这样的话,我们能够在一个确定的时间范围内实现各个节点数据的最终一致性,而不是基于随机的概率。像 InfluxDB 就是这样来实现反熵的。
+在我们实际应用场景中,一般不会采用随机的节点进行反熵,而是可以设计成一个闭环。这样的话,我们能够在一个确定的时间范围内实现各个节点数据的最终一致性,而不是基于随机的概率。像 InfluxDB 就是这样来实现反熵的。

@@ -98,7 +98,7 @@ Gossip 设计了两种可能的消息传播模式:**反熵(Anti-Entropy)**
如下图所示(下图来自于[INTRODUCTION TO GOSSIP](https://managementfromscratch.wordpress.com/2016/04/01/introduction-to-gossip/) 这篇文章):
-
+
伪代码如下:
@@ -138,7 +138,7 @@ 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 45aaf72cd9e..c820209f4a8 100644
--- a/docs/distributed-system/protocol/paxos-algorithm.md
+++ b/docs/distributed-system/protocol/paxos-algorithm.md
@@ -67,7 +67,7 @@ Basic Paxos 中存在 3 个重要的角色:
## Multi Paxos 思想
-Basic Paxos 算法的仅能就单个值达成共识,为了能够对一系列的值达成共识,我们需要用到 Basic Paxos 思想。
+Basic Paxos 算法的仅能就单个值达成共识,为了能够对一系列的值达成共识,我们需要用到 Multi Paxos 思想。
⚠️**注意**:Multi-Paxos 只是一种思想,这种思想的核心就是通过多个 Basic Paxos 实例就一系列值达成共识。也就是说,Basic Paxos 是 Multi-Paxos 思想的核心,Multi-Paxos 就是多执行几次 Basic Paxos。
@@ -77,7 +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 e61cc9b36fe..18d2c2eb0cb 100644
--- a/docs/distributed-system/protocol/raft-algorithm.md
+++ b/docs/distributed-system/protocol/raft-algorithm.md
@@ -106,13 +106,13 @@ raft 使用了随机的选举超时时间来避免上述情况。每一个 Candi
## 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 保证以下两个性质:
@@ -163,9 +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/http&rpc.md b/docs/distributed-system/rpc/http&rpc.md
index 7cd2b160230..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 协议?
diff --git a/docs/distributed-system/rpc/rpc-intro.md b/docs/distributed-system/rpc/rpc-intro.md
index b31a3e9effd..d2c5fb5e9c7 100644
--- a/docs/distributed-system/rpc/rpc-intro.md
+++ b/docs/distributed-system/rpc/rpc-intro.md
@@ -25,7 +25,7 @@ tag:
1. **客户端(服务消费端)**:调用远程方法的一端。
1. **客户端 Stub(桩)**:这其实就是一代理类。代理类主要做的事情很简单,就是把你调用方法、类、方法参数等信息传递到服务端。
-1. **网络传输**:网络传输就是你要把你调用的方法的信息比如说参数啊这些东西传输到服务端,然后服务端执行完之后再把返回结果通过网络传输给你传输回来。网络传输的实现方式有很多种比如最近基本的 Socket 或者性能以及封装更加优秀的 Netty(推荐)。
+1. **网络传输**:网络传输就是你要把你调用的方法的信息比如说参数啊这些东西传输到服务端,然后服务端执行完之后再把返回结果通过网络传输给你传输回来。网络传输的实现方式有很多种比如最基本的 Socket 或者性能以及封装更加优秀的 Netty(推荐)。
1. **服务端 Stub(桩)**:这个桩就不是代理类了。我觉得理解为桩实际不太好,大家注意一下就好。这里的服务端 Stub 实际指的就是接收到客户端执行方法的请求后,去执行对应的方法然后返回结果给客户端的类。
1. **服务端(服务提供端)**:提供远程方法的一端。
@@ -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
diff --git a/docs/high-availability/fallback-and-circuit-breaker.md b/docs/high-availability/fallback-and-circuit-breaker.md
index 63bebb7ac03..59725fa0521 100644
--- a/docs/high-availability/fallback-and-circuit-breaker.md
+++ b/docs/high-availability/fallback-and-circuit-breaker.md
@@ -1,10 +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)中。
+
+
diff --git a/docs/high-availability/high-availability-system-design.md b/docs/high-availability/high-availability-system-design.md
index 3c05f2b8f30..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
---
## 什么是高可用?可用性的判断标准是啥?
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)中。
+
+
+
+
+
+
diff --git a/docs/high-availability/limit-request.md b/docs/high-availability/limit-request.md
index a7ba1192008..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,重新开始计数。
-**这种限流算法无法保证限流速率,因而无法保证突然激增的流量。**
+
-就比如说我们限制某个接口 1 分钟只能访问 1000 次,该接口的 QPS 为 500,前 55s 这个接口 1 个请求没有接收,后 1s 突然接收了 1000 个请求。然后,在当前场景下,这 1000 个请求在 1s 内是没办法被处理的,系统直接就被瞬时的大量请求给击垮了。
+优点:实现简单,易于理解。
-
+缺点:
+
+- 限流不够平滑。例如,我们限制某个接口每分钟只能访问 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(窗口数)` 的请求, 如果当前窗口的请求计数总和超过了限制的数量的话就不再处理其他请求。
很显然, **当滑动窗口的格子划分的越多,滑动窗口的滚动就越平滑,限流的统计就会越精确。**

+优点:
+
+- 相比于固定窗口算法,滑动窗口计数器算法可以应对突然激增的流量。
+- 相比于固定窗口算法,滑动窗口计数器算法的颗粒度更小,可以提供更精确的限流控制。
+
+缺点:
+
+- 与固定窗口计数器算法类似,滑动窗口计数器算法依然存在限流不够平滑的问题。
+- 相比较于固定窗口计数器算法,滑动窗口计数器算法实现和理解起来更复杂一些。
+
### 漏桶算法
我们可以把发请求的动作比作成注水到桶中,我们处理请求的过程可以比喻为漏桶漏水。我们往桶中以任意速率流入水,以一定速率流出水。当水超过桶流量则丢弃,因为桶容量是不变的,保证了整体的速率。
@@ -51,12 +66,48 @@ category: 高可用

+优点:
+
+- 实现简单,易于理解。
+- 可以控制限流速率,避免网络拥塞和系统过载。
+
+缺点:
+
+- 无法应对突然激增的流量,因为只能以固定的速率处理请求,对系统资源利用不够友好。
+- 桶流入水(发请求)的速率如果一直大于桶流出水(处理请求)的速率的话,那么桶会一直是满的,一部分新的请求会被丢弃,导致服务质量下降。
+
+实际业务场景中,基本不会使用漏桶算法。
+
### 令牌桶算法
令牌桶算法也比较简单。和漏桶算法算法一样,我们的主角还是桶(这限流算法和桶过不去啊)。不过现在桶里装的是令牌了,请求在被处理之前需要拿到一个令牌,请求处理完毕之后将这个令牌丢弃(删除)。我们根据限流大小,按照一定的速率往桶里添加令牌。如果桶装满了,就不能继续往里面继续添加令牌了。

+优点:
+
+- 可以限制平均速率和应对突然激增的流量。
+- 可以动态调整生成令牌的速率。
+
+缺点:
+
+- 如果令牌产生速率和桶的容量设置不合理,可能会出现问题比如大量的请求被丢弃、系统过载。
+- 相比于其他限流算法,实现和理解起来更复杂一些。
+
+## 针对什么来进行限流?
+
+实际项目中,还需要确定限流对象,也就是针对什么来进行限流。常见的限流对象如下:
+
+- 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,12 +253,46 @@ Resilience4j 不仅提供限流,还提供了熔断、负载保护、自动重
> ShenYu 地址:
-
+
+
+另外,如果不想自己写 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 2fc9bfc980d..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 开发人员
+### 开发人员
用户与开发人员都关注速度,这个速度实际上就是我们的系统**处理用户请求的速度**。
@@ -30,7 +31,7 @@ category: 高可用
7. 项目使用的 Redis 缓存多大?服务器性能如何?用的是机械硬盘还是固态硬盘?
8. ……
-### 1.3 测试人员
+### 测试人员
测试人员一般会根据性能测试工具来测试,然后一般会做出一个表格。这个表格可能会涵盖下面这些重要的内容:
@@ -39,63 +40,87 @@ category: 高可用
3. 吞吐量;
4. ……
-### 1.4 运维人员
+### 运维人员
运维人员会倾向于根据基础设施和资源的利用率来判断网站的性能,比如我们的服务器资源使用是否合理、数据库资源是否存在滥用的情况、当然,这是传统的运维人员,现在 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)
-### 3.4 性能计数器
+独立访客,统计 1 天内访问某站点的用户数。1 天内相同访客多次访问网站,只计算为 1 个独立访客。UV 是从用户个体的角度来统计的。
-**性能计数器是描述服务器或者操作系统的一些数据指标如内存使用、CPU 使用、磁盘与网络 I/O 等情况。**
+### DAU(Daily Active User)
-### 四 几种常见的性能测试
+日活跃用户数量。
+
+### 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 后端常用
+推荐 4 个比较常用的性能测试工具:
-没记错的话,除了 LoadRunner 其他几款性能测试工具都是开源免费的。
+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 旗下的一款测试工具,非常实用。
+没记错的话,除了 **LoadRunner** 其他几款性能测试工具都是开源免费的。
-### 5.2 前端常用
+### 前端常用
-1. Fiddler:抓包工具,它可以修改请求的数据,甚至可以修改服务器返回的数据,功能非常强大,是 Web 调试的利器。
-2. HttpWatch: 可用于录制 HTTP 请求信息的工具。
+1. **Fiddler**:抓包工具,它可以修改请求的数据,甚至可以修改服务器返回的数据,功能非常强大,是 Web 调试的利器。
+2. **HttpWatch**: 可用于录制 HTTP 请求信息的工具。
-## 六 常见的性能优化策略
+## 常见的性能优化策略
性能优化之前我们需要对请求经历的各个环节进行分析,排查出可能出现性能瓶颈的地方,定位问题。
diff --git a/docs/high-availability/redundancy.md b/docs/high-availability/redundancy.md
index 5cec473e54c..9d14d726675 100644
--- a/docs/high-availability/redundancy.md
+++ b/docs/high-availability/redundancy.md
@@ -1,6 +1,7 @@
---
title: 冗余设计详解
category: 高可用
+icon: cluster
---
冗余设计是保证系统和数据高可用的最常的手段。
diff --git a/docs/high-availability/timeout-and-retry.md b/docs/high-availability/timeout-and-retry.md
index 8db69f11cb2..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、服务器宕机、操作系统崩溃等问题的不确定性,我们的系统或者服务永远不可能保证时刻都是可用的状态。
@@ -65,7 +66,7 @@ category: 高可用
重试的次数通常建议设为 3 次。大部分情况下,我们还是更建议使用梯度间隔重试策略,比如说我们要重试 3 次的话,第 1 次请求失败后,等待 1 秒再进行重试,第 2 次请求失败后,等待 2 秒再进行重试,第 3 次请求失败后,等待 3 秒再进行重试。
-### 重试幂等
+### 什么是重试幂等?
超时和重试机制在实际项目中使用的话,需要注意保证同一个请求没有被多次执行。
@@ -73,6 +74,10 @@ category: 高可用
举个例子:用户支付购买某个课程,结果用户支付的请求由于重试的问题导致用户购买同一门课程支付了两次。对于这种情况,我们在执行用户购买课程的请求的时候需要判断一下用户是否已经购买过。这样的话,就不会因为重试的问题导致重复购买了。
+### Java 中如何实现重试?
+
+如果要手动编写代码实现重试逻辑的话,可以通过循环(例如 while 或 for 循环)或者递归实现。不过,一般不建议自己动手实现,有很多第三方开源库提供了更完善的重试机制实现,例如 Spring Retry、Resilience4j、Guava Retrying。
+
## 参考
- 微服务之间调用超时的设置治理:
diff --git a/docs/high-performance/cdn.md b/docs/high-performance/cdn.md
index 1fa32023d19..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
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 的查询优化器可能会选择全表扫描而不是利用索引来优化查询。这是因为扫描索引和跳过大量记录可能比直接全表扫描更耗费资源。
+
+
+
+不同机器上这个查询偏移量过大的临界点可能不同,取决于多个因素,包括硬件配置(如 CPU 性能、磁盘速度)、表的大小、索引的类型和统计信息等。
+
+
+
+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 开发手册》中也有对应的描述:
+
+> 利用延迟关联或者子查询优化超多分页场景。
+>
+> 
+
+```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/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 da0577ccb18..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:
负载均衡可以简单分为 **服务端负载均衡** 和 **客户端负载均衡** 这两种。
-服务端负载均衡涉及到的知识点更多,工作中遇到的也比较多,因为,我会花更多时间来介绍。
+服务端负载均衡涉及到的知识点更多,工作中遇到的也比较多,因此,我会花更多时间来介绍。
### 服务端负载均衡
@@ -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,21 +113,45 @@ Java 领域主流的微服务框架 Dubbo、Spring Cloud 等都内置了开箱
未加权重的轮询算法适合于服务器性能相近的集群,其中每个服务器承载相同的负载。加权轮询算法适合于服务器性能不等的集群,权重的存在可以使请求分配更加合理化。
+在加权轮询的基础上,还有进一步改进得到的负载均衡算法,比如平滑的加权轮训算法。
+
+平滑的加权轮训算法最早是在 Nginx 中被实现,可以参考这个 commit:。如果你认真学习过 Dubbo 负载均衡策略的话,就会发现 Dubbo 的加权轮询就借鉴了该算法实现并进一步做了优化。
+
+
+
+### 两次随机法
+
+两次随机法在随机法的基础上多增加了一次随机,多选出一个服务器。随后再根据两台服务器的负载等情况,从其中选择出一个最合适的服务器。
+
+两次随机法的好处是可以动态地调节后端节点的负载,使其更加均衡。如果只使用一次随机法,可能会导致某些服务器过载,而某些服务器空闲。
+
+### 哈希法
+
+将请求的参数信息通过哈希函数转换成一个哈希值,然后根据哈希值来决定请求被哪一台服务器处理。
+
+在服务器数量不变的情况下,相同参数的请求总是发到同一台服务器处理,比如同个 IP 的请求、同一个用户的请求。
+
### 一致性 Hash 法
-相同参数的请求总是发到同一台服务器处理,比如同个 IP 的请求。
+和哈希法类似,一致性 Hash 法也可以让相同参数的请求总是发到同一台服务器处理。不过,它解决了哈希法存在的一些问题。
+
+常规哈希法在服务器数量变化时,哈希值会重新落在不同的服务器上,这明显违背了使用哈希法的本意。而一致性哈希法的核心思想是将数据和节点都映射到一个哈希环上,然后根据哈希值的顺序来确定数据属于哪个节点。当服务器增加或删除时,只影响该服务器的哈希,而不会导致整个服务集群的哈希键值重新分布。
### 最小连接法
-当有新的请求出现时,遍历服务器节点列表并选取其中活动连接数最小的一台服务器来响应当前请求。活动连接数可以理解为当前正在处理的请求数。
+当有新的请求出现时,遍历服务器节点列表并选取其中连接数最小的一台服务器来响应当前请求。相同连接的情况下,可以进行加权随机。
-最小连接法可以尽可能最大地使请求分配更加合理化,提高服务器的利用率。不过,这种方法实现起来也最复杂,需要监控每一台服务器处理的请求连接数。
+最少连接数基于一个服务器连接数越多,负载就越高这一理想假设。然而, 实际情况是连接数并不能代表服务器的实际负载,有些连接耗费系统资源更多,有些连接不怎么耗费系统资源。
-### 两次随机法
+### 最少活跃法
-两次随机法在随机法的基础上多增加了一次随机,多选出一个服务器。随后再根据两台服务器的负载等情况,从其中选择出一个最合适的服务器。
+最少活跃法和最小连接法类似,但要更科学一些。最少活跃法以活动连接数为标准,活动连接数可以理解为当前正在处理的请求数。活跃数越低,说明处理能力越强,这样就可以使处理能力强的服务器处理更多请求。相同活跃数的情况下,可以进行加权随机。
-两次随机法的好处是可以动态地调节后端节点的负载,使其更加均衡。如果只使用一次随机法,可能会导致某些服务器过载,而某些服务器空闲。
+### 最快响应时间法
+
+不同于最小连接法和最少活跃法,最快响应时间法以响应时间为标准来选择具体是哪一台服务器处理。客户端会维持每个服务器的响应时间,每次请求挑选响应时间最短的。相同响应时间的情况下,可以进行加权随机。
+
+这种算法可以使得请求被更快处理,但可能会造成流量过于集中于高性能服务器的问题。
## 七层负载均衡可以怎么做?
diff --git a/docs/high-performance/message-queue/disruptor-questions.md b/docs/high-performance/message-queue/disruptor-questions.md
index 4833bd7c36b..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 等项目。

@@ -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,7 +134,7 @@ CPU 缓存是通过将最近使用的数据存储在高速缓存中来实现更
## 参考
-- Disruptor 高性能之道-等待策略:
+- 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 68ca0dd36b2..070858cc1f8 100644
--- a/docs/high-performance/message-queue/kafka-questions-01.md
+++ b/docs/high-performance/message-queue/kafka-questions-01.md
@@ -99,7 +99,7 @@ Kafka 将生产者发布的消息发送到 **Topic(主题)** 中,需要这
### Zookeeper 在 Kafka 中的作用是什么?
-> 要想搞懂 zookeeper 在 Kafka 中的作用 一定要自己搭建一个 Kafka 环境然后自己进 zookeeper 去看一下有哪些文件夹和 Kafka 有关,每个节点又保存了什么信息。 一定不要光看不实践,这样学来的也终会忘记!这部分内容参考和借鉴了这篇文章:https://www.jianshu.com/p/a036405f989c 。
+> 要想搞懂 zookeeper 在 Kafka 中的作用 一定要自己搭建一个 Kafka 环境然后自己进 zookeeper 去看一下有哪些文件夹和 Kafka 有关,每个节点又保存了什么信息。 一定不要光看不实践,这样学来的也终会忘记!这部分内容参考和借鉴了这篇文章: 。
下图就是我的本地 Zookeeper ,它成功和我本地的 Kafka 关联上(以下文件夹结构借助 idea 插件 Zookeeper tool 实现)。
@@ -120,7 +120,7 @@ ZooKeeper 主要为 Kafka 提供元数据的管理的功能。
不过,要提示一下:**如果要使用 KRaft 模式的话,建议选择较高版本的 Kafka,因为这个功能还在持续完善优化中。Kafka 3.3.1 版本是第一个将 KRaft(Kafka Raft)共识协议标记为生产就绪的版本。**
-
+
## Kafka 消费顺序、消息丢失和重复消费
@@ -435,7 +435,7 @@ private void customer(String message) {
## 参考
-- Kafka 官方文档:https://kafka.apache.org/documentation/
+- Kafka 官方文档:
- 极客时间—《Kafka 核心技术与实战》第 11 节:无消息丢失配置怎么实现?
diff --git a/docs/high-performance/message-queue/message-queue.md b/docs/high-performance/message-queue/message-queue.md
index 310b9a9523c..f34c8825da4 100644
--- a/docs/high-performance/message-queue/message-queue.md
+++ b/docs/high-performance/message-queue/message-queue.md
@@ -23,9 +23,9 @@ tag:
参与消息传递的双方称为 **生产者** 和 **消费者** ,生产者负责发送消息,消费者负责处理消息。
-
+
-我们知道操作系统中的进程通信的一种很重要的方式就是消息队列。我们这里提到的消息队列稍微有点区别,更多指的是各个服务以及系统内部各个组件/模块之前的通信,属于一种 **中间件** 。
+操作系统中的进程通信的一种很重要的方式就是消息队列。我们这里提到的消息队列稍微有点区别,更多指的是各个服务以及系统内部各个组件/模块之前的通信,属于一种 **中间件** 。
维基百科是这样介绍中间件的:
@@ -35,25 +35,27 @@ tag:
除了消息队列之外,常见的中间件还有 RPC 框架、分布式组件、HTTP 服务器、任务调度框架、配置中心、数据库层的分库分表工具和数据迁移工具等等。
-关于中间件比较详细的介绍可以参考阿里巴巴淘系技术的一篇回答:https://www.zhihu.com/question/19730582/answer/1663627873 。
+关于中间件比较详细的介绍可以参考阿里巴巴淘系技术的一篇回答: 。
随着分布式和微服务系统的发展,消息队列在系统设计中有了更大的发挥空间,使用消息队列可以降低系统耦合性、实现任务异步、有效地进行流量削峰,是分布式和微服务系统中重要的组件之一。
## 消息队列有什么用?
-通常来说,使用消息队列能为我们的系统带来下面三点好处:
+通常来说,使用消息队列主要能为我们的系统带来下面三点好处:
-1. **通过异步处理提高系统性能(减少响应所需时间)**
-2. **削峰/限流**
-3. **降低系统耦合性。**
+1. 异步处理
+2. 削峰/限流
+3. 降低系统耦合性
+
+除了这三点之外,消息队列还有其他的一些应用场景,例如实现分布式事务、顺序保证和数据流处理。
如果在面试的时候你被面试官问到这个问题的话,一般情况是你在你的简历上涉及到消息队列这方面的内容,这个时候推荐你结合你自己的项目来回答。
-### 通过异步处理提高系统性能(减少响应所需时间)
+### 异步处理

-将用户的请求数据存储到消息队列之后就立即返回结果。随后,系统再对消息进行消费。
+将用户请求中包含的耗时操作,通过消息队列实现异步处理,将对应的消息发送到消息队列之后就立即返回结果,减少响应时间,提高用户体验。随后,系统再对消息进行消费。
因为用户请求数据写入消息队列之后就立即返回给用户了,但是请求数据在后续的业务校验、写数据库等操作中可能失败。因此,**使用消息队列进行异步处理之后,需要适当修改业务流程进行配合**,比如用户在提交订单之后,订单数据写入消息队列,不能立即返回用户订单提交成功,需要在消息队列的订单消费者进程真正处理完该订单之后,甚至出库后,再通过电子邮件或短信通知用户订单成功,以免交易纠纷。这就类似我们平时手机订火车票和电影票。
@@ -67,15 +69,17 @@ tag:
### 降低系统耦合性
-使用消息队列还可以降低系统耦合性。我们知道如果模块之间不存在直接调用,那么新增模块或者修改模块就对其他模块影响较小,这样系统的可扩展性无疑更好一些。还是直接上图吧:
+使用消息队列还可以降低系统耦合性。如果模块之间不存在直接调用,那么新增模块或者修改模块就对其他模块影响较小,这样系统的可扩展性无疑更好一些。
-
+生产者(客户端)发送消息到消息队列中去,消费者(服务端)处理消息,需要消费的系统直接去消息队列取消息进行消费即可而不需要和其他系统有耦合,这显然也提高了系统的扩展性。
-生产者(客户端)发送消息到消息队列中去,接受者(服务端)处理消息,需要消费的系统直接去消息队列取消息进行消费即可而不需要和其他系统有耦合,这显然也提高了系统的扩展性。
+
**消息队列使用发布-订阅模式工作,消息发送者(生产者)发布消息,一个或多个消息接受者(消费者)订阅消息。** 从上图可以看到**消息发送者(生产者)和消息接受者(消费者)之间没有直接耦合**,消息发送者将消息发送至分布式消息队列即结束对消息的处理,消息接受者从分布式消息队列获取该消息后进行后续处理,并不需要知道该消息从何而来。**对新增业务,只要对该类消息感兴趣,即可订阅该消息,对原有系统和业务没有任何影响,从而实现网站业务的可扩展性设计**。
-消息接受者对消息进行过滤、处理、包装后,构造成一个新的消息类型,将消息继续发送出去,等待其他消息接受者订阅该消息。因此基于事件(消息对象)驱动的业务架构可以是一系列流程。
+例如,我们商城系统分为用户、订单、财务、仓储、消息通知、物流、风控等多个服务。用户在完成下单后,需要调用财务(扣款)、仓储(库存管理)、物流(发货)、消息通知(通知用户发货)、风控(风险评估)等服务。使用消息队列后,下单操作和后续的扣款、发货、通知等操作就解耦了,下单完成发送一个消息到消息队列,需要用到的地方去订阅这个消息进行消费即可。
+
+
另外,为了避免消息队列服务器宕机造成消息丢失,会将成功发送到消息队列的消息存储在消息生产者服务器上,等消息真正被消费者服务器处理后才删除消息。在消息队列服务器宕机后,生产者服务器会选择分布式消息队列服务器集群中的其他服务器发布消息。
@@ -83,7 +87,7 @@ tag:
### 实现分布式事务
-我们知道分布式事务的解决方案之一就是 MQ 事务。
+分布式事务的解决方案之一就是 MQ 事务。
RocketMQ、 Kafka、Pulsar、QMQ 都提供了事务相关的功能。事务允许事件流应用将消费,处理,生产消息整个过程定义为一个原子操作。
@@ -91,6 +95,26 @@ RocketMQ、 Kafka、Pulsar、QMQ 都提供了事务相关的功能。事务允

+### 顺序保证
+
+在很多应用场景中,处理数据的顺序至关重要。消息队列保证数据按照特定的顺序被处理,适用于那些对数据顺序有严格要求的场景。大部分消息队列,例如 RocketMQ、RabbitMQ、Pulsar、Kafka,都支持顺序消息。
+
+### 延时/定时处理
+
+消息发送后不会立即被消费,而是指定一个时间,到时间后再消费。大部分消息队列,例如 RocketMQ、RabbitMQ、Pulsar,都支持定时/延时消息。
+
+
+
+### 即时通讯
+
+MQTT(消息队列遥测传输协议)是一种轻量级的通讯协议,采用发布/订阅模式,非常适合于物联网(IoT)等需要在低带宽、高延迟或不可靠网络环境下工作的应用。它支持即时消息传递,即使在网络条件较差的情况下也能保持通信的稳定性。
+
+RabbitMQ 内置了 MQTT 插件用于实现 MQTT 功能(默认不启用,需要手动开启)。
+
+### 数据流处理
+
+针对分布式系统产生的海量数据流,如业务日志、监控数据、用户行为等,消息队列可以实时或批量收集这些数据,并将其导入到大数据处理引擎中,实现高效的数据流管理和处理。
+
## 使用消息队列会带来哪些问题?
- **系统可用性降低:** 系统可用性在某种程度上降低,为什么这样说呢?在加入 MQ 之前,你不用考虑消息丢失或者说 MQ 挂掉等等的情况,但是,引入 MQ 之后你就需要去考虑了!
@@ -117,13 +141,13 @@ JMS 定义了五种不同的消息正文格式以及调用的消息类型,允
#### 点到点(P2P)模型
-
+
使用**队列(Queue)**作为消息通信载体;满足**生产者与消费者模式**,一条消息只能被一个消费者使用,未被消费的消息在队列中保留直到被消费或超时。比如:我们生产者发送 100 条消息的话,两个消费者来消费一般情况下两个消费者会按照消息发送的顺序各自消费一半(也就是你一个我一个的消费。)
#### 发布/订阅(Pub/Sub)模型
-
+
发布订阅模型(Pub/Sub) 使用**主题(Topic)**作为消息通信载体,类似于**广播模式**;发布者发布一条消息,该消息通过主题传递给所有的订阅者。
@@ -182,11 +206,11 @@ Kafka 是一个分布式系统,由通过高性能 TCP 网络协议进行通信
不过,要提示一下:**如果要使用 KRaft 模式的话,建议选择较高版本的 Kafka,因为这个功能还在持续完善优化中。Kafka 3.3.1 版本是第一个将 KRaft(Kafka Raft)共识协议标记为生产就绪的版本。**
-
+
-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,7 +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 8fbb53a952e..e971eaf3604 100644
--- a/docs/high-performance/message-queue/rabbitmq-questions.md
+++ b/docs/high-performance/message-queue/rabbitmq-questions.md
@@ -142,7 +142,7 @@ RabbitMQ 中的交换器、交换器类型、队列、绑定、路由键等都
- **Module Layer**:协议最高层,主要定义了一些客户端调用的命令,客户端可以用这些命令实现自己的业务逻辑。
- **Session Layer**:中间层,主要负责客户端命令发送给服务器,再将服务端应答返回客户端,提供可靠性同步机制和错误处理。
-- **TransportLayer**:最底层,主要传输二进制数据流,提供帧的处理、信道服用、错误检测和数据表示等。
+- **TransportLayer**:最底层,主要传输二进制数据流,提供帧的处理、信道复用、错误检测和数据表示等。
**AMQP 模型的三大组件**:
@@ -170,7 +170,7 @@ RabbitMQ 中的交换器、交换器类型、队列、绑定、路由键等都
## 什么是死信队列?如何导致的?
-DLX,全称为 `Dead-Letter-Exchange`,死信交换器,死信邮箱。当消息在一个队列中变成死信 (`dead message`) 之后,它能被重新被发送到另一个交换器中,这个交换器就是 DLX,绑定 DLX 的队列就称之为死信队列。
+DLX,全称为 `Dead-Letter-Exchange`,死信交换器,死信邮箱。当消息在一个队列中变成死信 (`dead message`) 之后,它能被重新发送到另一个交换器中,这个交换器就是 DLX,绑定 DLX 的队列就称之为死信队列。
**导致的死信的几种原因**:
diff --git a/docs/high-performance/message-queue/rocketmq-questions.md b/docs/high-performance/message-queue/rocketmq-questions.md
index b1334908cf0..9591e5d2612 100644
--- a/docs/high-performance/message-queue/rocketmq-questions.md
+++ b/docs/high-performance/message-queue/rocketmq-questions.md
@@ -19,7 +19,7 @@ tag:
### 消息队列为什么会出现?
-消息队``列算是作为后端程序员的一个必备技能吧,因为**分布式应用必定涉及到各个系统之间的通信问题**,这个时候消息队列也应运而生了。可以说分布式的产生是消息队列的基础,而分布式怕是一个很古老的概念了吧,所以消息队列也是一个很古老的中间件了。
+消息队列算是作为后端程序员的一个必备技能吧,因为**分布式应用必定涉及到各个系统之间的通信问题**,这个时候消息队列也应运而生了。可以说分布式的产生是消息队列的基础,而分布式怕是一个很古老的概念了吧,所以消息队列也是一个很古老的中间件了。
### 消息队列能用来干什么?
@@ -291,7 +291,7 @@ tag:
#### 定时消息
-在分布式定时调度触发、任务超时处理等场景,需要实现精准、可靠的定时事件触发。使用 RocketMQ 的定时消息可以简化定时调度任务的开发逻辑,实现高性能、可扩展、高可靠的定时触发能力。定时消息仅支持在 MessageType 为 Delay 的主题内使用,即定时消息只能发送至类型为定时消息的主题中,发送的消息的类型必须和主题的类型一致。
+在分布式定时调度触发、任务超时处理等场景,需要实现精准、可靠的定时事件触发。使用 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 来指定延迟时间或定时时间。
基于定时消息的超时任务处理具备如下优势:
@@ -319,7 +319,7 @@ tag:
#### 事务消息
-施工中。。。
+事务消息是 Apache RocketMQ 提供的一种高级消息类型,支持在分布式场景下保障消息生产和本地事务的最终一致性。简单来讲,就是将本地事务(数据库的 DML 操作)与发送消息合并在同一个事务中。例如,新增一个订单。在事务未提交之前,不发送订阅的消息。发送消息的动作随着事务的成功提交而发送,随着事务的回滚而取消。当然真正地处理过程不止这么简单,包含了半消息、事务监听和事务回查等概念,下面有更详细的说明。
## 关于发送消息
@@ -431,7 +431,7 @@ RocketMQ 服务端 5.x 版本开始,**生产者是匿名的**,无需管理
RocketMQ 服务端 5.x 版本:上述消费者的消费行为从关联的消费者分组中统一获取,因此,同一分组内所有消费者的消费行为必然是一致的,客户端无需关注。
-RocketMQ 服务端 3.x/4.x 历史版本:上述消费逻辑由消费者客户端接口定义,因此,您需要自己在消费者客户端设置时保证同一分组下的消费者的消费行为一致。[来自官方网站]
+RocketMQ 服务端 3.x/4.x 历史版本:上述消费逻辑由消费者客户端接口定义,因此,您需要自己在消费者客户端设置时保证同一分组下的消费者的消费行为一致。(来自官方网站)
## 如何解决顺序消费和重复消费?
@@ -540,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));
+ }
+}
+```
+
## 如何解决消息堆积问题?
在上面我们提到了消息队列一个很重要的功能——**削峰** 。那么如果这个峰值太大了导致消息堆积在队列中怎么办呢?
@@ -564,7 +774,7 @@ emmm,就两个字—— **幂等** 。在编程中一个*幂等* 操作的特
### 传统 IO 方式
-
+
传统的 IO 读写其实就是 read + write 的操作,整个过程会分为如下几步
@@ -587,7 +797,7 @@ mmap(memory map)是一种内存映射文件的方法,即将一个文件或
简单地说就是内核缓冲区和应用缓冲区共享,从而减少了从读缓冲区到用户缓冲区的一次 CPU 拷贝。基于此上述架构图可变为:
-
+
基于 mmap IO 读写其实就变成 mmap + write 的操作,也就是用 mmap 替代传统 IO 中的 read 操作。
@@ -604,7 +814,7 @@ MappedByteBuffer mappedByteBuffer = fileChannel.map(FileChannel.MapMode.READ_WRI
sendfile()跟 mmap()一样,也会减少一次 CPU 拷贝,但是它同时也会减少两次上下文切换。
-
+
如图,用户在发起 sendfile()调用时会发生切换 1,之后数据通过 DMA 拷贝到内核缓冲区,之后再将内核缓冲区的数据 CPU 拷贝到 Socket 缓冲区,最后拷贝到网卡,sendfile()返回,发生切换 2。发生了 3 次拷贝和两次切换。Java 也提供了相应 api:
@@ -681,13 +891,13 @@ RocketMQ 内部主要是使用基于 mmap 实现的零拷贝(其实就是调用

-`RocketMQ` 采用的是 **混合型的存储结构** ,即为 `Broker` 单个实例下所有的队列共用一个日志数据文件来存储消息。有意思的是在同样高并发的 `Kafka` 中会为每个 `Topic` 分配一个存储文件。这就有点类似于我们有一大堆书需要装上书架,`RockeMQ` 是不分书的种类直接成批的塞上去的,而 `Kafka` 是将书本放入指定的分类区域的。
+`RocketMQ` 采用的是 **混合型的存储结构** ,即为 `Broker` 单个实例下所有的队列共用一个日志数据文件来存储消息。有意思的是在同样高并发的 `Kafka` 中会为每个 `Topic` 分配一个存储文件。这就有点类似于我们有一大堆书需要装上书架,`RocketMQ` 是不分书的种类直接成批的塞上去的,而 `Kafka` 是将书本放入指定的分类区域的。
而 `RocketMQ` 为什么要这么做呢?原因是 **提高数据的写入效率** ,不分 `Topic` 意味着我们有更大的几率获取 **成批** 的消息进行数据写入,但也会带来一个麻烦就是读取消息的时候需要遍历整个大文件,这是非常耗时的。
所以,在 `RocketMQ` 中又使用了 `ConsumeQueue` 作为每个队列的索引文件来 **提升读取消息的效率**。我们可以直接根据队列的消息序号,计算出索引的全局位置(索引序号\*索引固定⻓度 20),然后直接读取这条索引,再根据索引中记录的消息的全局位置,找到消息。
-讲到这里,你可能对 `RockeMQ` 的存储架构还有些模糊,没事,我们结合着图来理解一下。
+讲到这里,你可能对 `RocketMQ` 的存储架构还有些模糊,没事,我们结合着图来理解一下。

@@ -715,7 +925,7 @@ 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 6b9a5a90be2..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
@@ -63,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 )。
怎么样?看了我对主从复制这个过程的讲解,你应该搞明白了吧!
@@ -194,18 +194,37 @@ 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 后缀。
+
### 分库分表会带来什么问题呢?
记住,你在公司做的任何技术决策,不光是要考虑这个技术能不能满足我们的要求,是否适合当前业务场景,还要重点考虑其带来的成本。
@@ -214,7 +233,7 @@ MySQL 主从同步延时是指从库的数据落后于主库的数据,这种
- **join 操作**:同一个数据库中的表分布在了不同的数据库中,导致无法使用 join 操作。这样就导致我们需要手动进行数据的封装,比如你在一个数据库中查询到一个数据之后,再根据这个数据去另外一个数据库中找对应的数据。不过,很多大厂的资深 DBA 都是建议尽量不要使用 join 操作。因为 join 的效率低,并且会对分库分表造成影响。对于需要用到 join 操作的地方,可以采用多次查询业务层进行数据组装的方法。不过,这种方法需要考虑业务上多次查询的事务性的容忍度。
- **事务问题**:同一个数据库中的表分布在了不同的数据库中,如果单个操作涉及到多个数据库,那么数据库自带的事务就无法满足我们的要求了。这个时候,我们就需要引入分布式事务了。关于分布式事务常见解决方案总结,网站上也有对应的总结: 。
-- **分布式 ID**:分库之后, 数据遍布在不同服务器上的数据库,数据库的自增主键已经没办法满足生成的主键唯一了。我们如何为不同的数据节点生成全局唯一主键呢?这个时候,我们就需要为我们的系统引入分布式 ID 了。关于分布式 ID 的详细介绍&实现方案总结,网站上也有对应的总结: 。
+- **分布式 ID**:分库之后, 数据遍布在不同服务器上的数据库,数据库的自增主键已经没办法满足生成的主键唯一了。我们如何为不同的数据节点生成全局唯一主键呢?这个时候,我们就需要为我们的系统引入分布式 ID 了。关于分布式 ID 的详细介绍&实现方案总结,可以看我写的这篇文章:[分布式 ID 介绍&实现方案总结](https://javaguide.cn/distributed-system/distributed-id.html)。
- **跨库聚合查询问题**:分库分表会导致常规聚合查询操作,如 group by,order by 等变得异常复杂。这是因为这些操作需要在多个分片上进行数据汇总和排序,而不是在单个数据库上进行。为了实现这些操作,需要编写复杂的业务代码,或者使用中间件来协调分片间的通信和数据传输。这样会增加开发和维护的成本,以及影响查询的性能和可扩展性。
- ……
@@ -244,7 +263,7 @@ ShardingSphere 的优势如下(摘自 ShardingSphere 官方文档:
diff --git a/docs/high-quality-technical-articles/readme.md b/docs/high-quality-technical-articles/README.md
similarity index 93%
rename from docs/high-quality-technical-articles/readme.md
rename to docs/high-quality-technical-articles/README.md
index 2ec9d70a5b4..149ddba7a4f 100644
--- a/docs/high-quality-technical-articles/readme.md
+++ b/docs/high-quality-technical-articles/README.md
@@ -9,6 +9,7 @@
- [程序员如何快速学习新技术](./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)
@@ -22,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)
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 c1bc95f5425..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,8 +8,6 @@ tag:
> **推荐语**:Kaito 大佬的一篇文章,很实用的建议!
>
->
->
> **原文地址:**
我想你肯定遇到过这样一类程序员:**他们无论是写代码,还是写文档,又或是和别人沟通,都显得特别专业**。每次遇到这类人,我都在想,他们到底是怎么做到的?
diff --git a/docs/high-quality-technical-articles/advanced-programmer/meituan-three-year-summary-lesson-10.md b/docs/high-quality-technical-articles/advanced-programmer/meituan-three-year-summary-lesson-10.md
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
index 2edf63c3cc0..3cd553a182e 100644
--- a/docs/high-quality-technical-articles/advanced-programmer/programmer-quickly-learn-new-technology.md
+++ b/docs/high-quality-technical-articles/advanced-programmer/programmer-quickly-learn-new-technology.md
@@ -17,7 +17,7 @@ tag:
比如说我们在学习 Spring 的时候,通过 Spring 官方文档你就可以知道 Spring 最新的技术动态,Spring 包含哪些模块 以及 Spring 可以帮你解决什么问题。
-
+
再比如说我在学习消息队列的时候,我会先去了解这个消息队列一般在系统中有什么作用,帮助我们解决了什么问题。消息队列的种类很多,具体学习研究某个消息队列的时候,我会将其和自己已经学习过的消息队列作比较。像我自己在学习 RocketMQ 的时候,就会先将其和自己曾经学习过的第 1 个消息队列 ActiveMQ 进行比较,思考 RocketMQ 相对于 ActiveMQ 有了哪些提升,解决了 ActiveMQ 的哪些痛点,两者有哪些相似的地方,又有哪些不同的地方。
@@ -29,7 +29,7 @@ tag:
如果你看不太懂官网的文档,你也可以搜索相关的关键词找一些高质量的博客或者视频来看。 **一定不要一上来就想着要搞懂这个技术的原理。**
-就比如说我们在学习 Spring 框架的时候,我建议你在搞懂 Spring 框架所解决的问题之后,不是直接去开始研究 Spring 框架的原理或者源码,而是先实际去体验一下 Spring 框架提供的核心功能 IoC(Inverse of Control:控制反转) 和 AOP(Aspect-Oriented Programming:面向切面编程),使用 Spring 框架写一些 Demo,甚至是使用 Spring 框架做一些小项目。
+就比如说我们在学习 Spring 框架的时候,我建议你在搞懂 Spring 框架所解决的问题之后,不是直接去开始研究 Spring 框架的原理或者源码,而是先实际去体验一下 Spring 框架提供的核心功能 IoC(Inverse of Control:控制反转) 和 AOP(Aspect-Oriented Programming:面向切面编程),使用 Spring 框架写一些 Demo,甚至是使用 Spring 框架做一些小项目。
一言以蔽之, **在研究这个技术的原理之前,先要搞懂这个技术是怎么使用的。**
@@ -48,4 +48,3 @@ tag:
很多人一提到八股文,就是一脸不屑。在我看来,如果你不是死记硬背八股文,而是去所思考这些面试题的本质。那你在准备八股文的过程中,同样也能让你加深对这项技术的了解。
最后,最重要同时也是最难的还是 **知行合一!知行合一!知行合一!** 不论是编程还是其他领域,最重要不是你知道的有多少,而是要尽量做到知行合一。
-
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 eca78e675b4..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
@@ -8,8 +8,6 @@ tag:
> **推荐语**:普通程序员要想成长为高级程序员甚至是专家等更高级别,应该注意在哪些方面注意加强?开发内功修炼号主飞哥在这篇文章中就给出了七条实用的建议。
>
->
->
> **内容概览**:
>
> 1. 刻意加强需求评审能力
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 56da00e3818..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
@@ -8,8 +8,6 @@ tag:
> **推荐语**:这篇文章的作者有着丰富的工作经验,曾在大厂工作了 12 年。结合自己走过的弯路和接触过的优秀技术人,他总结出了一些对于个人成长具有普遍指导意义的经验和特质。
>
->
->
> **原文地址:**
最近这段时间,有好几个年轻的同学和我聊到自己的迷茫。其中有关于技术成长的、有关于晋升的、有关于择业的。我很高兴他们愿意听我这个“过来人”分享自己的经验。
diff --git a/docs/high-quality-technical-articles/advanced-programmer/the-growth-strategy-of-the-technological-giant.md b/docs/high-quality-technical-articles/advanced-programmer/the-growth-strategy-of-the-technological-giant.md
index f5b9d5a91f3..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,8 +8,6 @@ tag:
> **推荐语**:波波老师的一篇文章,写的非常好,不光是对技术成长有帮助,其他领域也是同样适用的!建议反复阅读,形成一套自己的技术成长策略。
>
->
->
> **原文地址:**
## 1. 前言
diff --git a/docs/high-quality-technical-articles/advanced-programmer/thinking-about-technology-and-business-after-five-years-of-work.md b/docs/high-quality-technical-articles/advanced-programmer/thinking-about-technology-and-business-after-five-years-of-work.md
index 1368bf74946..d32f586b449 100644
--- a/docs/high-quality-technical-articles/advanced-programmer/thinking-about-technology-and-business-after-five-years-of-work.md
+++ b/docs/high-quality-technical-articles/advanced-programmer/thinking-about-technology-and-business-after-five-years-of-work.md
@@ -8,8 +8,6 @@ tag:
> **推荐语**:这是我在两年前看到的一篇对我触动比较深的文章。确实要学会适应变化,并积累能力。积累解决问题的能力,优化思考方式,拓宽自己的认知。
>
->
->
> **原文地址:**
苦海无边,回头无岸。
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 19ce98987bf..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
+> **原文地址**:
## 灵魂三连问
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 204be1f5438..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/
+> **原文地址**:
## 基本情况
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 34865443b2a..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
+> **原文地址**:
## 前言
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 e821f031c5b..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
+> **原文地址:**
最近我的工作稍微轻松些,就被安排去校招面试了
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 b3d215d51c6..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)
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 6a7d200ebf1..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
+> **原文地址:**
## 考察目标和思路
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 e86d7853baf..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
+> **原文地址**:
从每一段经历中学习,在每一件事情中修行。善于从挫折中学习。
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 a7541fb88be..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。现在想起来也是有点唏嘘,毕竟拿得再多也只能选择一家。不过许多朋友想让我分享下互联网面试方法,今天就来给大家仔细讲讲打法!
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 948f7c5dcc8..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
+> **原文地址**:
---
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 465929cc9c8..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
+> 原文地址:
研究生毕业后, 一直在腾讯工作,不知不觉就过了四年。个人本身没有刻意总结的习惯,以前只顾着往前奔跑了,忘了停下来思考总结。记得看过一个职业规划文档,说的三年一个阶段,五年一个阶段的说法,现在恰巧是四年,同时又从腾讯离开,该做一个总结了。
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 9bacc732405..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
+> **原文地址**:
## 时间线
@@ -94,7 +92,7 @@ d) 你的加班一定要提加班申请电子流换 Double 薪资,不然只能
**答案是:真的。**
-据各类非官方渠道(比如知乎上的一些分享),转华为自有是有条件的(https://www.zhihu.com/question/356592219/answer/1562692667):
+据各类非官方渠道(比如知乎上的一些分享),转华为自有是有条件的():
1)入职时间:一年以上
2)绩效要求:连续两次绩效 A
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 d3296a4e761..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
@@ -18,7 +18,7 @@ tag:
> - 平时积极总结沉淀,多跟别人交流,形成方法论。
> - ……
>
-> **原文地址**:https://www.nowcoder.com/discuss/351805
+> **原文地址**:
先简单交代一下背景吧,某不知名 985 的本硕,17 年毕业加入滴滴,当时找工作时候也是在牛客这里跟大家一起奋战的。今年下半年跳槽到了头条,一直从事后端研发相关的工作。之前没有实习经历,算是两年半的工作经验吧。这两年半之间完成了一次晋升,换了一家公司,有过开心满足的时光,也有过迷茫挣扎的日子,不过还算顺利地从一只职场小菜鸟转变为了一名资深划水员。在这个过程中,总结出了一些还算实用的划水经验,有些是自己领悟到的,有些是跟别人交流学到的,在这里跟大家分享一下。
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 9501aece761..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
+> **原文地址**:
古有三不朽, 所谓立德、立功、立言。程序员出一本属于自己的书,如果说是立言,可能过于高大上,但终究也算一件雅事。
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(项目管理专业人士认证),软考高项的难度更大,特别是论文部分,绝大部分人都挂在了论文部分。过了软考高项,在一些单位可以内部挂证,每个月多拿几百。
+
+
+
+官网地址: 。
+
+备考建议:[2024 年上半年,一次通过软考高级架构师考试的备考秘诀 - 阿里云开发者](https://mp.weixin.qq.com/s/9aUXHJ7dXgrHuT19jRhCnw)
+
+## PAT
+
+攀拓计算机能力测评(PAT)是一个专注于考察算法能力的测评体系,由浙江大学主办。该测评分为四个级别:基础级、乙级、甲级和顶级。
+
+通过 PAT 测评并达到联盟企业规定的相应评级和分数,可以跳过学历门槛,免除筛选简历和笔试环节,直接获得面试机会。具体有哪些公司可以去官网看看: 。
+
+对于考研浙江大学的同学来说,PAT(甲级)成绩在一年内可以作为硕士研究生招生考试上机复试成绩。
+
+
+
+## PMP
+
+PMP(Project Management Professional)认证由美国项目管理协会(PMI)提供,是全球范围内认可度最高的项目管理专业人士资格认证。PMP 认证旨在提升项目管理专业人士的知识和技能,确保项目顺利完成。
+
+
+
+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)。
+
+
+
+## 阿里云认证
+
+阿里云(Alibaba Cloud)提供的专业认证,认证方向包括云计算、大数据、人工智能、Devops 等。职业认证分为 ACA、ACP、ACE 三个等级,除了职业认证之外,还有一个开发者 Clouder 认证,这是专门为开发者设立的专项技能认证。
+
+
+
+官网地址: 。
+
+## 华为认证
+
+华为认证是由华为技术有限公司提供的面向 ICT(信息与通信技术)领域的专业认证,认证方向包括网络、存储、云计算、大数据、人工智能等,非常庞大的认证体系。
+
+
+
+## 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 云平台,分为基础级别、助理级别和专家级别,认证方向包括云计算、数据管理、开发、生产力工具等。
+
+
+
+## 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 集群等核心技能。
+
+
+
+## 其他
+
+- 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 20c7ed1bd00..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 担任资深架构,那么其它话甚至都不用讲了。
diff --git a/docs/high-quality-technical-articles/work/employee-performance.md b/docs/high-quality-technical-articles/work/employee-performance.md
index c9371583e9c..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 季度,被打了绩效,也给下属打了绩效,感慨颇深。
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 71b928fb243..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,9 +7,7 @@ tag:
> **推荐语**:强烈建议每一位即将入职/在职的小伙伴看看这篇文章,看完之后可以帮助你少踩很多坑。整篇文章逻辑清晰,内容全面!
>
->
->
-> **原文地址**:https://www.cnblogs.com/hunternet/p/14675348.html
+> **原文地址**:

diff --git a/docs/home.md b/docs/home.md
index 4f32107263a..047a09fe9ad 100644
--- a/docs/home.md
+++ b/docs/home.md
@@ -13,21 +13,6 @@ title: JavaGuide(Java学习&面试指南)
:::
-
-
-[](https://github.com/Snailclimb/JavaGuide)
-
-[](https://javaguide.cn/)
-
-
-
-
-[GitHub](https://github.com/Snailclimb/JavaGuide) | [Gitee](https://gitee.com/SnailClimb/JavaGuide)
-
-
-
-[](./about-the-author/zhishixingqiu-two-years.md)
-
## Java
### 基础
@@ -56,7 +41,7 @@ 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)
**源码分析**:
@@ -87,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)
@@ -122,6 +109,8 @@ JVM 这部分内容主要参考 [JVM 虚拟机规范-Java8](https://docs.oracle.
- [Java 19 新特性概览](./java/new-features/java19.md)
- [Java 20 新特性概览](./java/new-features/java20.md)
- [Java 21 新特性概览](./java/new-features/java21.md)
+- [Java 22 & 23 新特性概览](./java/new-features/java22-23.md)
+- [Java 24 新特性概览](./java/new-features/java24.md)
## 计算机基础
@@ -310,15 +299,13 @@ JVM 这部分内容主要参考 [JVM 虚拟机规范-Java8](https://docs.oracle.
- [JWT 优缺点分析以及常见问题解决方案](./system-design/security/advantages-and-disadvantages-of-jwt.md)
- [SSO 单点登录详解](./system-design/security/sso-intro.md)
- [权限系统设计详解](./system-design/security/design-of-authority-system.md)
-- [常见加密算法总结](./system-design/security/encryption-algorithms.md)
-
-#### 数据脱敏
-
-数据脱敏说的就是我们根据特定的规则对敏感信息数据进行变形,比如我们把手机号、身份证号某些位数使用 \* 来代替。
-#### 敏感词过滤
+#### 数据安全
-[敏感词过滤方案总结](./system-design/security/sentive-words-filter.md)
+- [常见加密算法总结](./system-design/security/encryption-algorithms.md)
+- [敏感词过滤方案总结](./system-design/security/sentive-words-filter.md)
+- [数据脱敏方案总结](./system-design/security/data-desensitization.md)
+- [为什么前后端都要做数据校验](./system-design/security/data-validation.md)
### 定时任务
@@ -374,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) 的 「技术面试题自测篇」概览:
+
+
+
+### 多表达
+
+平时要多说,多表达出来,不要只是在心里面想,不然真正面试的时候会发现想的和说的不太一样。
+
+我前面推荐的模拟面试和自测,有一部分原因就是为了能够多多表达。
+
+### 多面试
+
+- **先小厂后大厂**:可以先去一些规模较小或者对你来说压力没那么大的公司试试手,积累一些实战经验,增加一些信心;等熟悉了面试流程、能够更从容地回答问题后,再去挑战自己心仪的大厂或热门公司。
+- **积累“失败经验”**:不要怕被拒,有些时候被拒绝却能从中学到更多。多复盘,多思考到底是哪个环节出了问题,再用更好的状态迎接下一次面试。
+
+### 保证休息
+
+- **留出充裕时间**:面试前尽量不要排太多事情,保证自己能有个好状态去参加面试。
+- **保证休息**:充足睡眠有助于情绪稳定,也能让你在面试时更清晰地思考问题。
+
+## 遇到不会的问题不要慌
+
+一场面试,不太可能面试官提的每一个问题你都能轻松应对,除非这场面试非常简单。
+
+在面试过程中,遇到不会的问题,首先要做的是快速回顾自己过往的知识,看是否能找到突破口。如果实在没有思路的话,可以真诚地向面试要一些提示比如谈谈你对这个问题的理解以及困惑点。一定不要觉得向面试官要提示很可耻,只要沟通没问题,这其实是很正常的。最怕的就是自己不会,还乱回答一通,这样会让面试官觉得你技术态度有问题。
+
+## 面试结束后的复盘
+
+很多人关注面试前的准备,却忽略了面试后的复盘,这一步真的非常非常非常重要:
+
+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 后端学习路线。这份学习路线共包含 **4w+** 字,但你完全不用担心内容过多而学不完。我会根据学习难度,划分出适合找小厂工作必学的内容,以及适合逐步提升 Java 后端开发能力的学习路径。
+
+
+
+对于初学者,你可以按照这篇文章推荐的学习路线和资料进行系统性的学习;对于有经验的开发者,你可以根据这篇文章更一步地深入学习 Java 后端开发,提升个人竞争力。
+
+在看这份学习路线的过程中,建议搭配 [Java 面试重点总结(重要)](https://javaguide.cn/interview-preparation/key-points-of-interview.html),可以让你在学习过程中更有目的性。
+
+由于这份学习路线内容太多,因此我将其整理成了 PDF 版本(共 **61** 页),方便大家阅读。这份 PDF 有黑夜和白天两种阅读版本,满足大家的不同需求。
+
+这份学习路线的获取方法很简单:直接在公众号「**JavaGuide**」后台回复“**学习路线**”即可获取。
+
+
diff --git a/docs/interview-preparation/key-points-of-interview.md b/docs/interview-preparation/key-points-of-interview.md
index 83cb3facce2..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 f2e93df82b3..0546626f889 100644
--- a/docs/interview-preparation/project-experience-guide.md
+++ b/docs/interview-preparation/project-experience-guide.md
@@ -5,7 +5,7 @@ icon: project
---
::: tip 友情提示
-本文节选自 **[《Java 面试指北》](../zhuanlan/java-mian-shi-zhi-bei.md)**。这是一份教你如何更高效地准备面试的小册,涵盖常见八股文(系统设计、常见框架、分布式、高并发 ……)、优质面经等内容。
+本文节选自 **[《Java 面试指北》](../zhuanlan/java-mian-shi-zhi-bei.md)**。这是一份教你如何更高效地准备面试的专栏,内容和 JavaGuide 互补,涵盖常见八股文(系统设计、常见框架、分布式、高并发 ……)、优质面经等内容。
:::
## 没有项目经验怎么办?
diff --git a/docs/interview-preparation/resume-guide.md b/docs/interview-preparation/resume-guide.md
index dd6a6b33854..396ef4b47e4 100644
--- a/docs/interview-preparation/resume-guide.md
+++ b/docs/interview-preparation/resume-guide.md
@@ -1,5 +1,5 @@
---
-title: 程序员简历编写指南(重要)
+title: 程序员简历编写指南
category: 面试准备
icon: jianli
---
@@ -162,17 +162,23 @@ icon: jianli
- 使用 xxx 技术优化了 xxx 模块,响应时间从 2s 降低到 0.2s。
- ……
-个人职责介绍示例 :
+个人职责介绍示例(这里只是举例,不要照搬,结合自己项目经历自己去写,不然面试的时候容易被问倒) :
- 基于 Spring Cloud Gateway + Spring Security OAuth2 + JWT 实现微服务统一认证授权和鉴权,使用 RBAC 权限模型实现动态权限控制。
-- 参与项目订单模块的开发,负责订单创建、删除、查询等功能。
-- 整合 Canal + RocketMQ 将 MySQL 增量数据(如商品、订单数据)同步到 ES。
+- 参与项目订单模块的开发,负责订单创建、删除、查询等功能,基于 Spring 状态机实现订单状态流转。
+- 商品和订单搜索场景引入 Elasticsearch,并且实现了相关商品推荐以及搜索提示功能。
+- 整合 Canal + RabbitMQ 将 MySQL 增量数据(如商品、订单数据)同步到 Elasticsearch。
+- 利用 RabbitMQ 官方提供的延迟队列插件实现延时任务场景比如订单超时自动取消、优惠券过期提醒、退款处理。
+- 消息推送系统引入 RabbitMQ 实现异步处理、削峰填谷和服务解耦,最高推送速度 10w/s,单日最大消息量 2000 万。
+- 使用 MAT 工具分析 dump 文件解决了广告服务新版本上线后导致大量的服务超时告警的问题。
- 排查并解决扣费模块由于扣费父任务和反作弊子任务使用同一个线程池导致的死锁问题。
+- 基于 EasyExcel 实现广告投放数据的导入导出,通过 MyBatis 批处理插入数据,基于任务表实现异步。
- 负责用户统计模块的开发,使用 CompletableFuture 并行加载后台用户统计模块的数据信息,平均相应时间从 3.5s 降低到 1s。
-- 使用 Sharding-JDBC 以用户 ID 后 4 位作为 Shard Key 对订单表进行分库分表,共 3 个库,每个库 2 个订单表,单表数据量保持在 500w 以下。自定义雪花算法生成订单 ID 的规则,把分片键同时作为的订单 ID 一部分,避免了额外存储订单 ID 与路由键的关系。
+- 基于 Sentinel 对核心场景(如用户登入注册、收货地址查询等)进行限流、降级,保护系统,提升用户体验。
- 热门数据(如首页、热门博客)使用 Redis+Caffeine 两级缓存,解决了缓存击穿和穿透问题,查询速度毫秒级,QPS 30w+。
- 使用 CompletableFuture 优化购物车查询模块,对获取用户信息、商品详情、优惠券信息等异步 RPC 调用进行编排,响应时间从 2s 降低为 0.2s。
- 搭建 EasyMock 服务,用于模拟第三方平台接口,方便了在网络隔离情况下的接口对接工作。
+- 基于 SkyWalking + Elasticsearch 搭建分布式链路追踪系统实现全链路监控。
**4、如果你觉得你的项目技术比较落后的话,可以自己私下进行改进。重要的是让项目比较有亮点,通过什么方式就无所谓了。**
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 2e813f5e7c5..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. 最后再按照简历的要求去学习和提升。
这其实也是 **以终为始** 思想的运用。
diff --git a/docs/java/basis/bigdecimal.md b/docs/java/basis/bigdecimal.md
index 54e3ab3df86..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 除数
@@ -260,11 +260,11 @@ public class BigDecimalUtil {
}
/**
- * 提供精确的小数位四舍五入处理。
+ * 提供精确的小数位四舍六入五成双处理。
*
- * @param v 需要四舍五入的数字
+ * @param v 需要四舍六入五成双的数字
* @param scale 小数点后保留几位
- * @return 四舍五入后的结果
+ * @return 四舍六入五成双后的结果
*/
public static double round(double v, int scale) {
if (scale < 0) {
@@ -288,7 +288,7 @@ public class BigDecimalUtil {
}
/**
- * 提供精确的类型转换(Int)不进行四舍五入
+ * 提供精确的类型转换(Int)不进行四舍六入五成双
*
* @param v 需要被转换的数字
* @return 返回转换结果
@@ -351,7 +351,7 @@ public class BigDecimalUtil {
}
```
-相关 issue:[建议对保留规则设置为 RoundingMode.HALF_EVEN,即四舍六入五成双](<[#2129](https://github.com/Snailclimb/JavaGuide/issues/2129)>) 。
+相关 issue:[建议对保留规则设置为 RoundingMode.HALF_EVEN,即四舍六入五成双,#2129](https://github.com/Snailclimb/JavaGuide/issues/2129) 。

diff --git a/docs/java/basis/generics-and-wildcards.md b/docs/java/basis/generics-and-wildcards.md
index f65781e01e6..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/#/) 的补充完善,两者可以配合使用。
diff --git a/docs/java/basis/java-basic-questions-01.md b/docs/java/basis/java-basic-questions-01.md
index faf5d8507d6..28f3eee8588 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,7 +18,7 @@ head:
### Java 语言有哪些特点?
-1. 简单易学;
+1. 简单易学(语法简单,上手容易);
2. 面向对象(封装,继承,多态);
3. 平台无关性( Java 虚拟机实现平台无关性);
4. 支持多线程( C++ 语言没有内置的多线程机制,因此必须调用操作系统的多线程功能来进行多线程程序设计,而 Java 语言却提供了多线程支持);
@@ -29,7 +29,7 @@ head:
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`来创建线程。参考链接:
🌈 拓展一下:
@@ -44,11 +44,13 @@ head:
除了 Java SE 和 Java EE,还有一个 Java ME(Java Platform,Micro Edition)。Java ME 是 Java 的微型版本,主要用于开发嵌入式消费电子设备的应用程序,例如手机、PDA、机顶盒、冰箱、空调等。Java ME 无需重点关注,知道有这个东西就好了,现在已经用不上了。
-### JVM vs JDK vs JRE
+### ⭐️JVM vs JDK vs JRE
#### JVM
-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)上运行。

@@ -60,13 +62,20 @@ 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 程序的工具。
-JRE(Java Runtime Environment) 是 Java 运行时环境。它是运行已编译 Java 程序所需的所有内容的集合,主要包括 Java 虚拟机(JVM)、Java 基础类库(Class Library)。
+如果需要编写、编译 Java 程序或使用 Java API 文档,就需要安装 JDK。某些需要 Java 特性的应用程序(如 JSP 转换为 Servlet 或使用反射)也可能需要 JDK 来编译和运行 Java 代码。因此,即使不进行 Java 开发工作,有时也可能需要安装 JDK。
-也就是说,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、JRE 和 JVM 的关系。
-
+
不过,从 JDK 9 开始,就不需要区分 JDK 和 JRE 的关系了,取而代之的是模块系统(JDK 被重新组织成 94 个模块)+ [jlink](http://openjdk.java.net/jeps/282) 工具 (随 Java 9 一起发布的新命令行工具,用于生成自定义 Java 运行时映像,该映像仅包含给定应用程序所需的模块) 。并且,从 JDK 11 开始,Oracle 不再提供单独的 JRE 下载。
@@ -78,7 +87,7 @@ JRE(Java Runtime Environment) 是 Java 运行时环境。它是运行已编
定制的、模块化的 Java 运行时映像有助于简化 Java 应用的部署和节省内存并增强安全性和可维护性。这对于满足现代应用程序架构的需求,如虚拟化、容器化、微服务和云原生开发,是非常重要的。
-### 什么是字节码?采用字节码的好处是什么?
+### ⭐️什么是字节码?采用字节码的好处是什么?
在 Java 中,JVM 可以理解的代码就叫做字节码(即扩展名为 `.class` 的文件),它不面向任何特定的处理器,只面向虚拟机。Java 语言通过字节码的方式,在一定程度上解决了传统解释型语言执行效率低的问题,同时又保留了解释型语言可移植的特点。所以, Java 程序运行时相对来说还是高效的(不过,和 C、 C++,Rust,Go 等语言还是有一定差距的),而且,由于字节码并不针对一种特定的机器,因此,Java 程序无须重新编译便可在多种不同操作系统的计算机上运行。
@@ -88,6 +97,11 @@ JRE(Java Runtime Environment) 是 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)
+

> HotSpot 采用了惰性评估(Lazy Evaluation)的做法,根据二八定律,消耗大部分系统资源的只有那一小部分的代码(热点代码),而这也就是 JIT 所需要编译的部分。JVM 会根据代码每次被执行的情况收集信息并相应地做出一些优化,因此执行的次数越多,它的速度就越快。
@@ -100,7 +114,7 @@ JDK、JRE、JVM、JIT 这四者的关系如下图所示。

-### 为什么说 Java 语言“编译与解释并存”?
+### ⭐️为什么说 Java 语言“编译与解释并存”?
其实这个问题我们讲字节码的时候已经提到过,因为比较重要,所以我们这里再提一下。
@@ -113,7 +127,7 @@ JDK、JRE、JVM、JIT 这四者的关系如下图所示。
根据维基百科介绍:
-> 为了改善编译语言的效率而发展出的[即时编译](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)
@@ -127,7 +141,7 @@ JDK 9 引入了一种新的编译模式 **AOT(Ahead of Time Compilation)** 。
**JIT 与 AOT 两者的关键指标对比**:
-
+
可以看出,AOT 的主要优势在于启动时间、内存占用和打包体积。JIT 的主要优势在于具备更高的极限处理能力,可以降低请求的最大延迟。
@@ -166,9 +180,9 @@ JDK 9 引入了一种新的编译模式 **AOT(Ahead of Time Compilation)** 。
>
> 答:
>
-> 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 还是有存在的必要的!
@@ -201,8 +215,6 @@ JDK 9 引入了一种新的编译模式 **AOT(Ahead of Time Compilation)** 。
Java 中的注释有三种:
-
-
1. **单行注释**:通常用于解释方法内某单行代码的作用。
2. **多行注释**:通常用于解释一段代码的作用。
@@ -240,7 +252,7 @@ Java 中的注释有三种:
在我们编写程序的时候,需要大量地为程序、类、变量、方法等取名字,于是就有了 **标识符** 。简单来说, **标识符就是一个名字** 。
-有一些标识符,Java 语言已经赋予了其特殊的含义,只能用于特定的地方,这些特殊的标识符就是 **关键字** 。简单来说,**关键字是被赋予特殊含义的标识**符 。比如,在我们的日常生活中,如果我们想要开一家店,则要给这个店起一个名字,起的这个“名字”就叫标识符。但是我们店的名字不能叫“警察局”,因为“警察局”这个名字已经被赋予了特殊的含义,而“警察局”就是我们日常生活中的关键字。
+有一些标识符,Java 语言已经赋予了其特殊的含义,只能用于特定的地方,这些特殊的标识符就是 **关键字** 。简单来说,**关键字是被赋予特殊含义的标识符** 。比如,在我们的日常生活中,如果我们想要开一家店,则要给这个店起一个名字,起的这个“名字”就叫标识符。但是我们店的名字不能叫“警察局”,因为“警察局”这个名字已经被赋予了特殊的含义,而“警察局”就是我们日常生活中的关键字。
### Java 语言关键字有哪些?
@@ -270,13 +282,30 @@ Java 中的注释有三种:
官方文档:[https://docs.oracle.com/javase/tutorial/java/nutsandbolts/\_keywords.html](https://docs.oracle.com/javase/tutorial/java/nutsandbolts/_keywords.html)
-### 自增自减运算符
+### ⭐️自增自减运算符
+
+在写代码的过程中,常见的一种情况是需要某个整数类型变量增加 1 或减少 1。Java 提供了自增运算符 (`++`) 和自减运算符 (`--`) 来简化这种操作。
-在写代码的过程中,常见的一种情况是需要某个整数类型变量增加 1 或减少 1,Java 提供了一种特殊的运算符,用于这种表达式,叫做自增运算符(++)和自减运算符(--)。
+`++` 和 `--` 运算符可以放在变量之前,也可以放在变量之后:
-++ 和 -- 运算符可以放在变量之前,也可以放在变量之后,当运算符放在变量之前时(前缀),先自增/减,再赋值;当运算符放在变量之后时(后缀),先赋值,再自增/减。例如,当 `b = ++a` 时,先自增(自己增加 1),再赋值(赋值给 b);当 `b = a++` 时,先赋值(赋值给 b),再自增(自己增加 1)。也就是,++a 输出的是 a+1 的值,a++输出的是 a 值。用一句口诀就是:“符号在前就先加/减,符号在后就后加/减”。
+- **前缀形式**(例如 `++a` 或 `--a`):先自增/自减变量的值,然后再使用该变量,例如,`b = ++a` 先将 `a` 增加 1,然后把增加后的值赋给 `b`。
+- **后缀形式**(例如 `a++` 或 `a--`):先使用变量的当前值,然后再自增/自减变量的值。例如,`b = a++` 先将 `a` 的当前值赋给 `b`,然后再将 `a` 增加 1。
-### 移位运算符
+为了方便记忆,可以使用下面的口诀:**符号在前就先加/减,符号在后就后加/减**。
+
+下面来看一个考察自增自减运算符的高频笔试题:执行下面的代码后,`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`。
+
+### ⭐️移位运算符
移位运算符是最基本的运算符之一,几乎每种编程语言都包含这一运算符。移位操作中,被操作的数据被视为二进制数,移位就是将其向左或向右移动若干位的运算。
@@ -293,18 +322,29 @@ static final int hash(Object key) {
```
-在 Java 代码里使用 `<<`、 `>>` 和`>>>`转换成的指令码运行起来会更高效些。
+**使用移位运算符的主要原因**:
+
+1. **高效**:移位运算符直接对应于处理器的移位指令。现代处理器具有专门的硬件指令来执行这些移位操作,这些指令通常在一个时钟周期内完成。相比之下,乘法和除法等算术运算在硬件层面上需要更多的时钟周期来完成。
+2. **节省内存**:通过移位操作,可以使用一个整数(如 `int` 或 `long`)来存储多个布尔值或标志位,从而节省内存。
+
+移位运算符最常用于快速乘以或除以 2 的幂次方。除此之外,它还在以下方面发挥着重要作用:
+
+- **位字段管理**:例如存储和操作多个布尔值。
+- **哈希算法和加密解密**:通过移位和与、或等操作来混淆数据。
+- **数据压缩**:例如霍夫曼编码通过移位运算符可以快速处理和操作二进制数据,以生成紧凑的压缩格式。
+- **数据校验**:例如 CRC(循环冗余校验)通过移位和多项式除法生成和校验数据完整性。
+- **内存对齐**:通过移位操作,可以轻松计算和调整数据的对齐地址。
掌握最基本的移位运算符知识还是很有必要的,这不光可以帮助我们在代码中使用,还可以帮助我们理解源码中涉及到移位运算符的代码。
Java 中有三种移位运算符:
-
-
-- `<<` :左移运算符,向左移若干位,高位丢弃,低位补零。`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`类型再操作。
@@ -363,31 +403,31 @@ 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");
+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");
}
- if (flag) {
- System.out.println("haha");
- return;
- }
- System.out.println("heihei");
+ System.out.println("xixi");
+ }
+ if (flag) {
+ System.out.println("haha");
+ return;
}
+ System.out.println("heihei");
+}
```
运行结果:
@@ -402,7 +442,7 @@ xixi
haha
```
-## 基本数据类型
+## ⭐️基本数据类型
### Java 中的几种基本数据类型了解么?
@@ -436,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` 。
### 基本类型和包装类型的区别?
-
-
- **用途**:除了定义一些常量和局部变量之外,我们在其他地方比如方法参数、对象属性中很少会使用基本类型来定义变量。并且,包装类型可用于泛型,而基本类型不可以。
- **存储方式**:基本数据类型的局部变量存放在 Java 虚拟机栈中的局部变量表中,基本数据类型的成员变量(未被 `static` 修饰 )存放在 Java 虚拟机的堆中。包装类型属于对象类型,我们知道几乎所有对象实例都存在于堆中。
- **占用空间**:相比于包装类型(对象类型), 基本数据类型占用的空间往往非常小。
@@ -452,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 修饰局部变量
+ }
}
```
@@ -464,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 缓存源码:**
@@ -619,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
```
@@ -649,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)。
@@ -680,9 +736,7 @@ System.out.println(l + 1 == Long.MIN_VALUE); // true
## 变量
-### 成员变量与局部变量的区别?
-
-
+### ⭐️成员变量与局部变量的区别?
- **语法形式**:从语法形式上看,成员变量是属于类的,而局部变量是在代码块或方法中定义的变量或是方法的参数;成员变量可以被 `public`,`private`,`static` 等修饰符所修饰,而局部变量不能被访问控制修饰符及 `static` 所修饰;但是,成员变量和局部变量都能被 `final` 所修饰。
- **存储方式**:从变量在内存中的存储方式来看,如果成员变量是使用 `static` 修饰的,那么这个成员变量是属于类的,如果没有使用 `static` 修饰,这个成员变量是属于实例的。而对象存在于堆内存,局部变量则存在于栈内存。
@@ -860,7 +914,7 @@ public class Example {
}
```
-### 静态方法和实例方法有何不同?
+### ⭐️静态方法和实例方法有何不同?
**1、调用方式**
@@ -893,7 +947,7 @@ public class Person {
静态方法在访问本类的成员时,只允许访问静态成员(即静态成员变量和静态方法),不允许访问实例成员(即实例成员变量和实例方法),而实例方法不存在这个限制。
-### 重载和重写有什么区别?
+### ⭐️重载和重写有什么区别?
> 重载就是同样的一个方法能够根据输入数据的不同,做出不同的处理
>
@@ -930,14 +984,13 @@ public class Person {
综上:**重写就是子类对父类方法的重新改造,外部样子不能改变,内部逻辑可以改变。**
-| 区别点 | 重载方法 | 重写方法 |
-| :--------- | :------- | :--------------------------------------------------------------- |
-| 发生范围 | 同一个类 | 子类 |
-| 参数列表 | 必须修改 | 一定不能修改 |
-| 返回类型 | 可修改 | 子类方法返回值类型应比父类方法返回值类型更小或相等 |
-| 异常 | 可修改 | 子类方法声明抛出的异常类应比父类方法声明抛出的异常类更小或相等; |
-| 访问修饰符 | 可修改 | 一定不能做更严格的限制(可以降低限制) |
-| 发生阶段 | 编译期 | 运行期 |
+| 区别点 | 重载 (Overloading) | 重写 (Overriding) |
+| -------------- | ------------------------------------------------------------------------------------ | -------------------------------------------------------------------------------------------- |
+| **发生范围** | 同一个类中。 | 父类与子类之间(存在继承关系)。 |
+| **方法签名** | 方法名**必须相同**,但**参数列表必须不同**(参数的类型、个数或顺序至少有一项不同)。 | 方法名、参数列表**必须完全相同**。 |
+| **返回类型** | 与返回值类型**无关**,可以任意修改。 | 子类方法的返回类型必须与父类方法的返回类型**相同**,或者是其**子类**。 |
+| **访问修饰符** | 与访问修饰符**无关**,可以任意修改。 | 子类方法的访问权限**不能低于**父类方法的访问权限。(public > protected > default > private) |
+| **绑定时期** | 编译时绑定或称静态绑定 | 运行时绑定 (Run-time Binding) 或称动态绑定 |
**方法的重写要遵循“两同两小一大”**(以下内容摘录自《疯狂 Java 讲义》,[issue#892](https://github.com/Snailclimb/JavaGuide/issues/892) ):
@@ -964,6 +1017,7 @@ public class SuperMan extends Hero{
}
public class SuperSuperMan extends SuperMan {
+ @Override
public String name() {
return "超级超级英雄";
}
@@ -977,7 +1031,7 @@ public class SuperSuperMan extends SuperMan {
### 什么是可变长参数?
-从 Java5 开始,Java 支持定义可变长参数,所谓可变长参数就是允许在调用方法时传入不定长度的参数。就比如下面的这个 `printVariable` 方法就可以接受 0 个或者多个参数。
+从 Java5 开始,Java 支持定义可变长参数,所谓可变长参数就是允许在调用方法时传入不定长度的参数。就比如下面这个方法就可以接受 0 个或者多个参数。
```java
public static void method1(String... args) {
@@ -1056,9 +1110,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 49ba61f0d98..60a4ff07555 100644
--- a/docs/java/basis/java-basic-questions-02.md
+++ b/docs/java/basis/java-basic-questions-02.md
@@ -6,7 +6,7 @@ tag:
head:
- - meta
- name: keywords
- content: 面向对象,构造方法,接口,抽象类,String,Object
+ content: 面向对象, 面向过程, OOP, POP, Java对象, 构造方法, 封装, 继承, 多态, 接口, 抽象类, 默认方法, 静态方法, 私有方法, 深拷贝, 浅拷贝, 引用拷贝, Object类, equals, hashCode, ==, 字符串, String, StringBuffer, StringBuilder, 不可变性, 字符串常量池, intern, 字符串拼接, Java基础, 面试题
- - meta
- name: description
content: 全网质量最高的Java基础常见知识点和面试题总结,希望对你有帮助!
@@ -16,16 +16,28 @@ head:
## 面向对象基础
-### 面向对象和面向过程的区别
+### ⭐️面向对象和面向过程的区别
-两者的主要区别在于解决问题的方式不同:
+面向过程编程(Procedural-Oriented Programming,POP)和面向对象编程(Object-Oriented Programming,OOP)是两种常见的编程范式,两者的主要区别在于解决问题的方式不同:
-- 面向过程把解决问题的过程拆成一个个方法,通过一个个方法的执行解决问题。
-- 面向对象会先抽象出对象,然后用对象执行方法的方式解决问题。
+- **面向过程编程(POP)**:面向过程把解决问题的过程拆成一个个方法,通过一个个方法的执行解决问题。
+- **面向对象编程(OOP)**:面向对象会先抽象出对象,然后用对象执行方法的方式解决问题。
-另外,面向对象开发的程序一般更易维护、易复用、易扩展。
+相比较于 POP,OOP 开发的程序一般具有下面这些优点:
-相关 issue : [面向过程:面向过程性能比面向对象高??](https://github.com/Snailclimb/JavaGuide/issues/431) 。
+- **易维护**:由于良好的结构和封装性,OOP 程序通常更容易维护。
+- **易复用**:通过继承和多态,OOP 设计使得代码更具复用性,方便扩展功能。
+- **易扩展**:模块化设计使得系统扩展变得更加容易和灵活。
+
+POP 的编程方式通常更为简单和直接,适合处理一些较简单的任务。
+
+POP 和 OOP 的性能差异主要取决于它们的运行机制,而不仅仅是编程范式本身。因此,简单地比较两者的性能是一个常见的误区(相关 issue : [面向过程:面向过程性能比面向对象高??](https://github.com/Snailclimb/JavaGuide/issues/431) )。
+
+
+
+在选择编程范式时,性能并不是唯一的考虑因素。代码的可维护性、可扩展性和开发效率同样重要。
+
+现代编程语言基本都支持多种编程范式,既可以用来进行面向过程编程,也可以进行面向对象编程。
下面是一个求圆的面积和周长的示例,简单分别展示了面向对象和面向过程两种不同的解决方案。
@@ -87,13 +99,12 @@ public class Main {
### 创建一个对象用什么运算符?对象实体与对象引用有何不同?
-new 运算符,new 创建对象实例(对象实例在
-内存中),对象引用指向对象实例(对象引用存放在栈内存中)。
+new 运算符,new 创建对象实例(对象实例在堆内存中),对象引用指向对象实例(对象引用存放在栈内存中)。
- 一个对象引用可以指向 0 个或 1 个对象(一根绳子可以不系气球,也可以系一个气球);
- 一个对象可以有 n 个引用指向它(可以用 n 条绳子系住一个气球)。
-### 对象的相等和引用相等的区别
+### ⭐️对象的相等和引用相等的区别
- 对象的相等一般比较的是内存中存放的内容是否相等。
- 引用相等一般比较的是他们指向的内存地址是否相等。
@@ -137,15 +148,15 @@ true
### 构造方法有哪些特点?是否可被 override?
-构造方法特点如下:
+构造方法具有以下特点:
-- 名字与类名相同。
-- 没有返回值,但不能用 void 声明构造函数。
-- 生成类的对象时自动执行,无需调用。
+- **名称与类名相同**:构造方法的名称必须与类名完全一致。
+- **没有返回值**:构造方法没有返回类型,且不能使用 `void` 声明。
+- **自动执行**:在生成类的对象时,构造方法会自动执行,无需显式调用。
-构造方法不能被 override(重写),但是可以 overload(重载),所以你可以看到一个类中有多个构造函数的情况。
+构造方法**不能被重写(override)**,但**可以被重载(overload)**。因此,一个类中可以有多个构造方法,这些构造方法可以具有不同的参数列表,以提供不同的对象初始化方式。
-### 面向对象三大特征
+### ⭐️面向对象三大特征
#### 封装
@@ -197,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 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.");
+ }
+}
+```
-- 接口主要用于对类的行为进行约束,你实现了某个接口就具有了对应的行为。抽象类主要用于代码复用,强调的是所属关系。
-- 一个类只能继承一个类,但是可以实现多个接口。
-- 接口中的成员变量只能是 `public static final` 类型的,不能被修改且必须有初始值,而抽象类的成员变量默认 default,可在子类中被重新定义,也可被重新赋值。
+Java 9 允许在接口中使用 `private` 方法。`private`方法可以用于在接口内部共享代码,不对外暴露。
+
+```java
+public interface MyInterface {
+ // default 方法
+ default void defaultMethod() {
+ commonMethod();
+ }
+
+ // static 方法
+ static void staticMethod() {
+ commonMethod();
+ }
+
+ // 私有静态方法,可以被 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.");
+ }
+}
+```
### 深拷贝和浅拷贝区别了解吗?什么是引用拷贝?
@@ -300,13 +361,13 @@ System.out.println(person1.getAddress() == person1Copy.getAddress());
我专门画了一张图来描述浅拷贝、深拷贝、引用拷贝:
-
+
-## Object
+## ⭐️Object
### Object 类的常见方法有哪些?
-Object 类是一个特殊的类,是所有类的父类。它主要提供了以下 11 个方法:
+Object 类是一个特殊的类,是所有类的父类,主要提供了以下 11 个方法:
```java
/**
@@ -430,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();
@@ -490,7 +551,7 @@ public native int hashCode();
## String
-### String、StringBuffer、StringBuilder 的区别?
+### ⭐️String、StringBuffer、StringBuilder 的区别?
**可变性**
@@ -510,7 +571,7 @@ abstract class AbstractStringBuilder implements Appendable, CharSequence {
count += len;
return this;
}
- //...
+ //...
}
```
@@ -524,18 +585,18 @@ abstract class AbstractStringBuilder implements Appendable, CharSequence {
**对于三者使用的总结:**
-1. 操作少量的数据: 适用 `String`
-2. 单线程操作字符串缓冲区下操作大量数据: 适用 `StringBuilder`
-3. 多线程操作字符串缓冲区下操作大量数据: 适用 `StringBuffer`
+- 操作少量的数据: 适用 `String`
+- 单线程操作字符串缓冲区下操作大量数据: 适用 `StringBuilder`
+- 多线程操作字符串缓冲区下操作大量数据: 适用 `StringBuffer`
-### String 为什么是不可变的?
+### ⭐️String 为什么是不可变的?
`String` 类中使用 `final` 关键字修饰字符数组来保存字符串,~~所以`String` 对象是不可变的。~~
```java
public final class String implements java.io.Serializable, Comparable, CharSequence {
private final char value[];
- //...
+ //...
}
```
@@ -573,9 +634,9 @@ public final class String implements java.io.Serializable, Comparable, C
>
> 如果字符串中包含的汉字超过 Latin-1 可表示范围内的字符,`byte` 和 `char` 所占用的空间是一样的。
>
-> 这是官方的介绍:https://openjdk.java.net/jeps/254 。
+> 这是官方的介绍: 。
-### 字符串拼接用“+” 还是 StringBuilder?
+### ⭐️字符串拼接用“+” 还是 StringBuilder?
Java 语言本身并不支持运算符重载,“+”和“+=”是专门为 String 类重载过的运算符,也是 Java 中仅有的两个重载过的运算符。
@@ -622,32 +683,37 @@ 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() 有何区别?
`String` 中的 `equals` 方法是被重写过的,比较的是 String 字符串的值是否相等。 `Object` 的 `equals` 方法是比较的对象的内存地址。
-### 字符串常量池的作用了解吗?
+### ⭐️字符串常量池的作用了解吗?
**字符串常量池** 是 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");这句话创建了几个字符串对象?
+### ⭐️String s1 = new String("abc");这句话创建了几个字符串对象?
-会创建 1 或 2 个字符串对象。
+先说答案:会创建 1 或 2 个字符串对象。
-1、如果字符串常量池中不存在字符串对象“abc”的引用,那么它会在堆上创建两个字符串对象,其中一个字符串对象的引用会被保存在字符串常量池中。
+1. 字符串常量池中不存在 "abc":会创建 2 个 字符串对象。一个在字符串常量池中,由 `ldc` 指令触发创建。一个在堆中,由 `new String()` 创建,并使用常量池中的 "abc" 进行初始化。
+2. 字符串常量池中已存在 "abc":会创建 1 个 字符串对象。该对象在堆中,由 `new String()` 创建,并使用常量池中的 "abc" 进行初始化。
+
+下面开始详细分析。
+
+1、如果字符串常量池中不存在字符串对象 “abc”,那么它首先会在字符串常量池中创建字符串对象 "abc",然后在堆内存中再创建其中一个字符串对象 "abc"。
示例代码(JDK 1.8):
@@ -657,16 +723,40 @@ String s1 = new String("abc");
对应的字节码:
-
+```java
+// 在堆内存中分配一个尚未初始化的 String 对象。
+// #2 是常量池中的一个符号引用,指向 java/lang/String 类。
+// 在类加载的解析阶段,这个符号引用会被解析成直接引用,即指向实际的 java/lang/String 类。
+0 new #2
+// 复制栈顶的 String 对象引用,为后续的构造函数调用做准备。
+// 此时操作数栈中有两个相同的对象引用:一个用于传递给构造函数,另一个用于保持对新对象的引用,后续将其存储到局部变量表。
+3 dup
+// JVM 先检查字符串常量池中是否存在 "abc"。
+// 如果常量池中已存在 "abc",则直接返回该字符串的引用;
+// 如果常量池中不存在 "abc",则 JVM 会在常量池中创建该字符串字面量并返回它的引用。
+// 这个引用被压入操作数栈,用作构造函数的参数。
+4 ldc #3
+// 调用构造方法,使用从常量池中加载的 "abc" 初始化堆中的 String 对象
+// 新的 String 对象将包含与常量池中的 "abc" 相同的内容,但它是一个独立的对象,存储于堆中。
+6 invokespecial #4 : (Ljava/lang/String;)V>
+// 将堆中的 String 对象引用存储到局部变量表
+9 astore_1
+// 返回,结束方法
+10 return
+```
+
+`ldc (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");
@@ -674,35 +764,48 @@ String s2 = new String("abc");
对应的字节码:
-
+```java
+0 ldc #2
+2 astore_1
+3 new #3
+6 dup
+7 ldc #2
+9 invokespecial #4 : (Ljava/lang/String;)V>
+12 astore_2
+13 return
+```
这里就不对上面的字节码进行详细注释了,7 这个位置的 `ldc` 命令不会在堆中创建新的字符串对象“abc”,这是因为 0 这个位置已经执行了一次 `ldc` 命令,已经在堆中创建过一次字符串对象“abc”了。7 这个位置执行 `ldc` 命令会直接返回字符串常量池中字符串对象“abc”对应的引用。
### String#intern 方法有什么作用?
-`String.intern()` 是一个 native(本地)方法,其作用是将指定的字符串对象的引用保存在字符串常量池中,可以简单分为两种情况:
+`String.intern()` 是一个 `native` (本地) 方法,用来处理字符串常量池中的字符串对象引用。它的工作流程可以概括为以下两种情况:
+
+1. **常量池中已有相同内容的字符串对象**:如果字符串常量池中已经有一个与调用 `intern()` 方法的字符串内容相同的 `String` 对象,`intern()` 方法会直接返回常量池中该对象的引用。
+2. **常量池中没有相同内容的字符串对象**:如果字符串常量池中还没有一个与调用 `intern()` 方法的字符串内容相同的对象,`intern()` 方法会将当前字符串对象的引用添加到字符串常量池中,并返回该引用。
+
+总结:
-- 如果字符串常量池中保存了对应的字符串对象的引用,就直接返回该引用。
-- 如果字符串常量池中没有保存了对应的字符串对象的引用,那就在常量池中创建一个指向该字符串对象的引用并返回。
+- `intern()` 方法的主要作用是确保字符串引用在常量池中的唯一性。
+- 当调用 `intern()` 时,如果常量池中已经存在相同内容的字符串,则返回常量池中已有对象的引用;否则,将该字符串添加到常量池并返回其引用。
示例代码(JDK 1.8) :
```java
-// 在堆中创建字符串对象”Java“
-// 将字符串对象”Java“的引用保存在字符串常量池中
+// s1 指向字符串常量池中的 "Java" 对象
String s1 = "Java";
-// 直接返回字符串常量池中字符串对象”Java“对应的引用
+// s2 也指向字符串常量池中的 "Java" 对象,和 s1 是同一个对象
String s2 = s1.intern();
-// 会在堆中在单独创建一个字符串对象
+// 在堆中创建一个新的 "Java" 对象,s3 指向它
String s3 = new String("Java");
-// 直接返回字符串常量池中字符串对象”Java“对应的引用
+// s4 指向字符串常量池中的 "Java" 对象,和 s1 是同一个对象
String s4 = s3.intern();
-// s1 和 s2 指向的是堆中的同一个对象
+// s1 和 s2 指向的是同一个常量池中的对象
System.out.println(s1 == s2); // true
-// s3 和 s4 指向的是堆中不同的对象
+// s3 指向堆中的对象,s4 指向常量池中的对象,所以不同
System.out.println(s3 == s4); // false
-// s1 和 s4 指向的是堆中的同一个对象
-System.out.println(s1 == s4); //true
+// s1 和 s4 都指向常量池中的同一个对象
+System.out.println(s1 == s4); // true
```
### String 类型的变量和常量做“+”运算时发生了什么?
@@ -783,6 +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 d744d0679dc..0b5df73b741 100644
--- a/docs/java/basis/java-basic-questions-03.md
+++ b/docs/java/basis/java-basic-questions-03.md
@@ -6,7 +6,7 @@ tag:
head:
- - meta
- name: keywords
- content: Java异常,泛型,反射,IO,注解
+ content: Java异常处理, Java泛型, Java反射, Java注解, Java SPI机制, Java序列化, Java反序列化, Java IO流, Java语法糖, Java基础面试题, Checked Exception, Unchecked Exception, try-with-resources, 反射应用场景, 序列化协议, BIO, NIO, AIO, IO模型
- - meta
- name: description
content: 全网质量最高的Java基础常见知识点和面试题总结,希望对你有帮助!
@@ -27,7 +27,7 @@ head:
- **`Exception`** :程序本身可以处理的异常,可以通过 `catch` 来进行捕获。`Exception` 又可以分为 Checked Exception (受检查异常,必须处理) 和 Unchecked Exception (不受检查异常,可以不处理)。
- **`Error`**:`Error` 属于程序无法处理的错误 ,~~我们没办法通过 `catch` 来进行捕获~~不建议通过`catch`捕获 。例如 Java 虚拟机运行错误(`Virtual MachineError`)、虚拟机内存不够错误(`OutOfMemoryError`)、类定义错误(`NoClassDefFoundError`)等 。这些异常发生时,Java 虚拟机(JVM)一般会选择线程终止。
-### Checked Exception 和 Unchecked Exception 有什么区别?
+### ⭐️Checked Exception 和 Unchecked Exception 有什么区别?
**Checked Exception** 即 受检查异常 ,Java 代码在编译过程中,如果受检查异常没有被 `catch`或者`throws` 关键字处理的话,就没办法通过编译。
@@ -55,8 +55,8 @@ head:
### Throwable 类常用方法有哪些?
-- `String getMessage()`: 返回异常发生时的简要描述
-- `String toString()`: 返回异常发生时的详细信息
+- `String getMessage()`: 返回异常发生时的详细信息
+- `String toString()`: 返回异常发生时的简要描述
- `String getLocalizedMessage()`: 返回异常对象的本地化信息。使用 `Throwable` 的子类覆盖这个方法,可以生成本地化信息。如果子类没有覆盖该方法,则该方法返回的信息与 `getMessage()`返回的结果相同
- `void printStackTrace()`: 在控制台上打印 `Throwable` 对象封装的异常信息
@@ -89,14 +89,6 @@ Finally
**注意:不要在 finally 语句块中使用 return!** 当 try 语句和 finally 语句中都有 return 语句时,try 语句块中的 return 语句会被忽略。这是因为 try 语句中的 return 返回值会先被暂存在一个本地变量中,当执行到 finally 语句中的 return 之后,这个本地变量的值就变为了 finally 语句中的 return 返回值。
-[jvm 官方文档](https://docs.oracle.com/javase/specs/jvms/se7/html/jvms-4.html#jvms-4.10.2.5)中有明确提到:
-
-> If the `try` clause executes a _return_, the compiled code does the following:
->
-> 1. Saves the return value (if any) in a local variable.
-> 2. Executes a _jsr_ to the code for the `finally` clause.
-> 3. Upon return from the `finally` clause, returns the value saved in the local variable.
-
代码示例:
```java
@@ -213,12 +205,12 @@ catch (IOException e) {
}
```
-### 异常使用有哪些需要注意的地方?
+### ⭐️异常使用有哪些需要注意的地方?
- 不要把异常定义为静态变量,因为这样会导致异常栈信息错乱。每次手动抛出异常,我们都需要手动 new 一个异常对象抛出。
- 抛出的异常信息一定要有意义。
-- 建议抛出更加具体的异常比如字符串转换为数字格式错误的时候应该抛出`NumberFormatException`而不是其父类`IllegalArgumentException`。
-- 使用日志打印异常之后就不要再抛出异常了(两者不要同时存在一段代码逻辑中)。
+- 建议抛出更加具体的异常,比如字符串转换为数字格式错误的时候应该抛出`NumberFormatException`而不是其父类`IllegalArgumentException`。
+- 避免重复记录日志:如果在捕获异常的地方已经记录了足够的信息(包括异常类型、错误信息和堆栈跟踪等),那么在业务代码中再次抛出这个异常时,就不应该再次记录相同的错误信息。重复记录日志会使得日志文件膨胀,并且可能会掩盖问题的实际原因,使得问题更难以追踪和解决。
- ……
## 泛型
@@ -286,7 +278,7 @@ class GeneratorImpl implements Generator{
实现泛型接口,指定类型:
```java
-class GeneratorImpl implements Generator{
+class GeneratorImpl implements Generator {
@Override
public String method() {
return "hello";
@@ -325,56 +317,72 @@ printArray( stringArray );
- 构建集合工具类(参考 `Collections` 中的 `sort`, `binarySearch` 方法)。
- ……
-## 反射
+## ⭐️反射
+
+关于反射的详细解读,请看这篇文章 [Java 反射机制详解](https://javaguide.cn/java/basis/reflection.html) 。
+
+### 什么是反射?
-关于反射的详细解读,请看这篇文章 [Java 反射机制详解](./reflection.md) 。
+简单来说,Java 反射 (Reflection) 是一种**在程序运行时,动态地获取类的信息并操作类或对象(方法、属性)的能力**。
-### 何谓反射?
+通常情况下,我们写的代码在编译时类型就已经确定了,要调用哪个方法、访问哪个字段都是明确的。但反射允许我们在**运行时**才去探知一个类有哪些方法、哪些属性、它的构造函数是怎样的,甚至可以动态地创建对象、调用方法或修改属性,哪怕这些方法或属性是私有的。
-如果说大家研究过框架的底层原理或者咱们自己写过框架的话,一定对反射这个概念不陌生。反射之所以被称为框架的灵魂,主要是因为它赋予了我们在运行时分析类以及执行类中方法的能力。通过反射你可以获取任意一个类的所有属性和方法,你还可以调用这些方法和属性。
+正是这种在运行时“反观自身”并进行操作的能力,使得反射成为许多**通用框架和库的基石**。它让代码更加灵活,能够处理在编译时未知的类型。
-### 反射的优缺点?
+### 反射有什么优缺点?
-反射可以让我们的代码更加灵活、为各种框架提供开箱即用的功能提供了便利。
+**优点:**
-不过,反射让我们在运行时有了分析操作类的能力的同时,也增加了安全问题,比如可以无视泛型参数的安全检查(泛型参数的安全检查发生在编译时)。另外,反射的性能也要稍差点,不过,对于框架来说实际是影响不大的。
+1. **灵活性和动态性**:反射允许程序在运行时动态地加载类、创建对象、调用方法和访问字段。这样可以根据实际需求(如配置文件、用户输入、注解等)动态地适应和扩展程序的行为,显著提高了系统的灵活性和适应性。
+2. **框架开发的基础**:许多现代 Java 框架(如 Spring、Hibernate、MyBatis)都大量使用反射来实现依赖注入(DI)、面向切面编程(AOP)、对象关系映射(ORM)、注解处理等核心功能。反射是实现这些“魔法”功能不可或缺的基础工具。
+3. **解耦合和通用性**:通过反射,可以编写更通用、可重用和高度解耦的代码,降低模块之间的依赖。例如,可以通过反射实现通用的对象拷贝、序列化、Bean 工具等。
+
+**缺点:**
+
+1. **性能开销**:反射操作通常比直接代码调用要慢。因为涉及到动态类型解析、方法查找以及 JIT 编译器的优化受限等因素。不过,对于大多数框架场景,这种性能损耗通常是可以接受的,或者框架本身会做一些缓存优化。
+2. **安全性问题**:反射可以绕过 Java 语言的访问控制机制(如访问 `private` 字段和方法),破坏了封装性,可能导致数据泄露或程序被恶意篡改。此外,还可以绕过泛型检查,带来类型安全隐患。
+3. **代码可读性和维护性**:过度使用反射会使代码变得复杂、难以理解和调试。错误通常在运行时才会暴露,不像编译期错误那样容易发现。
相关阅读:[Java Reflection: Why is it so slow?](https://stackoverflow.com/questions/1392351/java-reflection-why-is-it-so-slow) 。
### 反射的应用场景?
-像咱们平时大部分时候都是在写业务代码,很少会接触到直接使用反射机制的场景。但是!这并不代表反射没有用。相反,正是因为反射,你才能这么轻松地使用各种框架。像 Spring/Spring Boot、MyBatis 等等框架中都大量使用了反射机制。
+我们平时写业务代码可能很少直接跟 Java 的反射(Reflection)打交道。但你可能没意识到,你天天都在享受反射带来的便利!**很多流行的框架,比如 Spring/Spring Boot、MyBatis 等,底层都大量运用了反射机制**,这才让它们能够那么灵活和强大。
+
+下面简单列举几个最场景的场景帮助大家理解。
+
+**1.依赖注入与控制反转(IoC)**
-**这些框架中也大量使用了动态代理,而动态代理的实现也依赖反射。**
+以 Spring/Spring Boot 为代表的 IoC 框架,会在启动时扫描带有特定注解(如 `@Component`, `@Service`, `@Repository`, `@Controller`)的类,利用反射实例化对象(Bean),并通过反射注入依赖(如 `@Autowired`、构造器注入等)。
-比如下面是通过 JDK 实现动态代理的示例代码,其中就使用了反射类 `Method` 来调用指定的方法。
+**2.注解处理**
+
+注解本身只是个“标记”,得有人去读这个标记才知道要做什么。反射就是那个“读取器”。框架通过反射检查类、方法、字段上有没有特定的注解,然后根据注解信息执行相应的逻辑。比如,看到 `@Value`,就用反射读取注解内容,去配置文件找对应的值,再用反射把值设置给字段。
+
+**3.动态代理与 AOP**
+
+想在调用某个方法前后自动加点料(比如打日志、开事务、做权限检查)?AOP(面向切面编程)就是干这个的,而动态代理是实现 AOP 的常用手段。JDK 自带的动态代理(Proxy 和 InvocationHandler)就离不开反射。代理对象在内部调用真实对象的方法时,就是通过反射的 `Method.invoke` 来完成的。
```java
public class DebugInvocationHandler implements InvocationHandler {
- /**
- * 代理类中的真实对象
- */
- private final Object target;
+ private final Object target; // 真实对象
- public DebugInvocationHandler(Object target) {
- this.target = target;
- }
+ public DebugInvocationHandler(Object target) { this.target = target; }
- public Object invoke(Object proxy, Method method, Object[] args) throws InvocationTargetException, IllegalAccessException {
- System.out.println("before method " + method.getName());
+ // proxy: 代理对象, method: 被调用的方法, args: 方法参数
+ public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
+ System.out.println("切面逻辑:调用方法 " + method.getName() + " 之前");
+ // 通过反射调用真实对象的同名方法
Object result = method.invoke(target, args);
- System.out.println("after method " + method.getName());
+ System.out.println("切面逻辑:调用方法 " + method.getName() + " 之后");
return result;
}
}
-
```
-另外,像 Java 中的一大利器 **注解** 的实现也用到了反射。
-
-为什么你使用 Spring 的时候 ,一个`@Component`注解就声明了一个类为 Spring Bean 呢?为什么你通过一个 `@Value`注解就读取到配置文件中的值呢?究竟是怎么起作用的呢?
+**4.对象关系映射(ORM)**
-这些都是因为你可以基于反射分析类,然后获取到类/属性/方法/方法的参数上的注解。你获取到注解之后,就可以做进一步的处理。
+像 MyBatis、Hibernate 这种框架,能帮你把数据库查出来的一行行数据,自动变成一个个 Java 对象。它是怎么知道数据库字段对应哪个 Java 属性的?还是靠反射。它通过反射获取 Java 类的属性列表,然后把查询结果按名字或配置对应起来,再用反射调用 setter 或直接修改字段值。反过来,保存对象到数据库时,也是用反射读取属性值来拼 SQL。
## 注解
@@ -405,9 +413,9 @@ JDK 提供了很多内置的注解(比如 `@Override`、`@Deprecated`),同
- **编译期直接扫描**:编译器在编译 Java 代码的时候扫描对应的注解并处理,比如某个方法使用`@Override` 注解,编译器在编译的时候就会检测当前的方法是否重写了父类对应的方法。
- **运行期通过反射处理**:像框架中自带的注解(比如 Spring 框架的 `@Value`、`@Component`)都是通过反射来进行处理的。
-## SPI
+## ⭐️SPI
-关于 SPI 的详细解读,请看这篇文章 [Java SPI 机制详解](./spi.md) 。
+关于 SPI 的详细解读,请看这篇文章 [Java SPI 机制详解](https://javaguide.cn/java/basis/spi.html) 。
### 何谓 SPI?
@@ -417,21 +425,20 @@ SPI 将服务接口和具体的服务实现分离开来,将服务调用方和
很多框架都使用了 Java 的 SPI 机制,比如:Spring 框架、数据库加载驱动、日志接口、以及 Dubbo 的扩展实现等等。
-
+
### SPI 和 API 有什么区别?
**那 SPI 和 API 有啥区别?**
-说到 SPI 就不得不说一下 API 了,从广义上来说它们都属于接口,而且很容易混淆。下面先用一张图说明一下:
-
-
+说到 SPI 就不得不说一下 API(Application Programming Interface) 了,从广义上来说它们都属于接口,而且很容易混淆。下面先用一张图说明一下:
-一般模块之间都是通过接口进行通讯,那我们在服务调用方和服务实现方(也称服务提供者)之间引入一个“接口”。
+
-当实现方提供了接口和实现,我们可以通过调用实现方的接口从而拥有实现方给我们提供的能力,这就是 API ,这种接口和实现都是放在实现方的。
+一般模块之间都是通过接口进行通讯,因此我们在服务调用方和服务实现方(也称服务提供者)之间引入一个“接口”。
-当接口存在于调用方这边时,就是 SPI ,由接口调用方确定接口规则,然后由不同的厂商去根据这个规则对这个接口进行实现,从而提供服务。
+- 当实现方提供了接口和实现,我们可以通过调用实现方的接口从而拥有实现方给我们提供的能力,这就是 **API**。这种情况下,接口和实现都是放在实现方的包中。调用方通过接口调用实现方的功能,而不需要关心具体的实现细节。
+- 当接口存在于调用方这边时,这就是 **SPI** 。由接口调用方确定接口规则,然后由不同的厂商根据这个规则对这个接口进行实现,从而提供服务。
举个通俗易懂的例子:公司 H 是一家科技公司,新设计了一款芯片,然后现在需要量产了,而市面上有好几家芯片制造业公司,这个时候,只要 H 公司指定好了这芯片生产的标准(定义好了接口标准),那么这些合作的芯片公司(服务提供者)就按照标准交付自家特色的芯片(提供不同方案的实现,但是给出来的结果是一样的)。
@@ -442,9 +449,9 @@ SPI 将服务接口和具体的服务实现分离开来,将服务调用方和
- 需要遍历加载所有的实现类,不能做到按需加载,这样效率还是相对较低的。
- 当多个 `ServiceLoader` 同时 `load` 时,会有并发问题。
-## 序列化和反序列化
+## ⭐️序列化和反序列化
-关于序列化和反序列化的详细解读,请看这篇文章 [Java 序列化详解](./serialization.md) ,里面涉及到的知识点和面试题更全面。
+关于序列化和反序列化的详细解读,请看这篇文章 [Java 序列化详解](https://javaguide.cn/java/basis/serialization.html) ,里面涉及到的知识点和面试题更全面。
### 什么是序列化?什么是反序列化?
@@ -452,8 +459,8 @@ SPI 将服务接口和具体的服务实现分离开来,将服务调用方和
简单来说:
-- **序列化**:将数据结构或对象转换成二进制字节流的过程
-- **反序列化**:将在序列化过程中所生成的二进制字节流转换成数据结构或者对象的过程
+- **序列化**:将数据结构或对象转换成可以存储或传输的形式,通常是二进制字节流,也可以是 JSON, XML 等文本格式
+- **反序列化**:将在序列化过程中所生成的数据转换为原始数据结构或者对象的过程
对于 Java 这种面向对象编程语言来说,我们序列化的都是对象(Object)也就是实例化后的类(Class),但是在 C++这种半面向对象的语言中,struct(结构体)定义的是数据结构类型,而 class 对应的是对象类型。
@@ -519,9 +526,9 @@ JDK 自带的序列化方式一般不会用 ,因为序列化效率低并且存
关于 I/O 的详细解读,请看下面这几篇文章,里面涉及到的知识点和面试题更全面。
-- [Java IO 基础知识总结](../io/io-basis.md)
-- [Java IO 设计模式总结](../io/io-design-patterns.md)
-- [Java IO 模型详解](../io/io-model.md)
+- [Java IO 基础知识总结](https://javaguide.cn/java/io/io-basis.html)
+- [Java IO 设计模式总结](https://javaguide.cn/java/io/io-design-patterns.html)
+- [Java IO 模型详解](https://javaguide.cn/java/io/io-model.html)
### Java IO 流了解吗?
@@ -543,11 +550,11 @@ Java IO 流的 40 多个类都是从如下 4 个抽象类基类中派生出来
### Java IO 中的设计模式有哪些?
-参考答案:[Java IO 设计模式总结](../io/io-design-patterns.md)
+参考答案:[Java IO 设计模式总结](https://javaguide.cn/java/io/io-design-patterns.html)
### BIO、NIO 和 AIO 的区别?
-参考答案:[Java IO 模型详解](../io/io-model.md)
+参考答案:[Java IO 模型详解](https://javaguide.cn/java/io/io-model.html)
## 语法糖
@@ -560,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);
}
```
diff --git a/docs/java/basis/java-keyword-summary.md b/docs/java/basis/java-keyword-summary.md
index e7a6b4f4cae..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 关键字详解
@@ -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:
@@ -302,8 +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 85902688e60..1045fbd0f88 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());
@@ -330,10 +330,10 @@ public class DebugMethodInterceptor implements MethodInterceptor {
/**
- * @param o 被代理的对象(需要增强的对象)
+ * @param o 代理对象本身(注意不是原始对象,如果使用method.invoke(o, args)会导致循环调用)
* @param method 被拦截的方法(需要增强的方法)
* @param args 方法入参
- * @param methodProxy 用于调用原始方法
+ * @param methodProxy 高性能的方法调用机制,避免反射开销
*/
@Override
public Object intercept(Object o, Method method, Object[] args, MethodProxy methodProxy) throws Throwable {
@@ -387,7 +387,7 @@ after method send
### 3.3. JDK 动态代理和 CGLIB 动态代理对比
-1. **JDK 动态代理只能代理实现了接口的类或者直接代理接口,而 CGLIB 可以代理未实现任何接口的类。** 另外, CGLIB 动态代理是通过生成一个被代理类的子类来拦截被代理类的方法调用,因此不能代理声明为 final 类型的类和方法。
+1. **JDK 动态代理只能代理实现了接口的类或者直接代理接口,而 CGLIB 可以代理未实现任何接口的类。** 另外, CGLIB 动态代理是通过生成一个被代理类的子类来拦截被代理类的方法调用,因此不能代理声明为 final 类型的类和方法,private 方法也无法代理。
2. 就二者的效率来说,大部分情况都是 JDK 动态代理更优秀,随着 JDK 版本的升级,这个优势更加明显。
## 4. 静态代理和动态代理的对比
diff --git a/docs/java/basis/reflection.md b/docs/java/basis/reflection.md
index 769d85e620f..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;
@@ -177,7 +177,7 @@ I love JavaGuide
value is JavaGuide
```
-**注意** : 有读者提到上面代码运行会抛出 `ClassNotFoundException` 异常,具体原因是你没有下面把这段代码的包名替换成自己创建的 `TargetObject` 所在的包 。
+**注意** : 有读者提到上面代码运行会抛出 `ClassNotFoundException` 异常,具体原因是你没有下面把这段代码的包名替换成自己创建的 `TargetObject` 所在的包 。
可以参考: 这篇文章。
```java
diff --git a/docs/java/basis/serialization.md b/docs/java/basis/serialization.md
index 406a7ce38d5..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 对应的是对象类型。
@@ -83,7 +83,11 @@ public class RpcRequest implements Serializable {
~~`static` 修饰的变量是静态变量,位于方法区,本身是不会被序列化的。 `static` 变量是属于类的而不是对象。你反序列之后,`static` 变量的值就像是默认赋予给了对象一样,看着就像是 `static` 变量被序列化,实际只是假象罢了。~~
-**🐛 修正(参见:[issue#2174](https://github.com/Snailclimb/JavaGuide/issues/2174))**:`static` 修饰的变量是静态变量,位于方法区,本身是不会被序列化的。但是,`serialVersionUID` 的序列化做了特殊处理,在序列化时,会将 `serialVersionUID` 序列化到二进制字节流中;在反序列化时,也会解析它并做一致性判断。
+**🐛 修正(参见:[issue#2174](https://github.com/Snailclimb/JavaGuide/issues/2174))**:
+
+通常情况下,`static` 变量是属于类的,不属于任何单个对象实例,所以它们本身不会被包含在对象序列化的数据流里。序列化保存的是对象的状态(也就是实例变量的值)。然而,`serialVersionUID` 是一个特例,`serialVersionUID` 的序列化做了特殊处理。关键在于,`serialVersionUID` 不是作为对象状态的一部分被序列化的,而是被序列化机制本身用作一个特殊的“指纹”或“版本号”。
+
+当一个对象被序列化时,`serialVersionUID` 会被写入到序列化的二进制流中(像是在保存一个版本号,而不是保存 `static` 变量本身的状态);在反序列化时,也会解析它并做一致性判断,以此来验证序列化对象的版本一致性。如果两者不匹配,反序列化过程将抛出 `InvalidClassException`,因为这通常意味着序列化的类的定义已经发生了更改,可能不再兼容。
官方说明如下:
@@ -91,7 +95,7 @@ public class RpcRequest implements Serializable {
>
> 如果想显式指定 `serialVersionUID` ,则需要在类中使用 `static` 和 `final` 关键字来修饰一个 `long` 类型的变量,变量名字必须为 `"serialVersionUID"` 。
-也就是说,`serialVersionUID` 只是用来被 JVM 识别,实际并没有被序列化。
+也就是说,`serialVersionUID` 本身(作为 static 变量)确实不作为对象状态被序列化。但是,它的值被 Java 序列化机制特殊处理了——作为一个版本标识符被读取并写入序列化流中,用于在反序列化时进行版本兼容性检查。
**如果有些字段不想进行序列化怎么办?**
@@ -198,7 +202,7 @@ GitHub 地址:[https://github.com/protocolbuffers/protobuf](https://github.com
### ProtoStuff
-由于 Protobuf 的易用性,它的哥哥 Protostuff 诞生了。
+由于 Protobuf 的易用性较差,它的哥哥 Protostuff 诞生了。
protostuff 基于 Google protobuf,但是提供了更多的功能和更简易的用法。虽然更加易用,但是不代表 ProtoStuff 性能更差。
diff --git a/docs/java/basis/spi.md b/docs/java/basis/spi.md
index baada46fb95..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 的扩展实现等等。
-
+
### SPI 和 API 有什么区别?
**那 SPI 和 API 有啥区别?**
-说到 SPI 就不得不说一下 API 了,从广义上来说它们都属于接口,而且很容易混淆。下面先用一张图说明一下:
+说到 SPI 就不得不说一下 API(Application Programming Interface) 了,从广义上来说它们都属于接口,而且很容易混淆。下面先用一张图说明一下:
-
+
-一般模块之间都是通过通过接口进行通讯,那我们在服务调用方和服务实现方(也称服务提供者)之间引入一个“接口”。
+一般模块之间都是通过接口进行通讯,因此我们在服务调用方和服务实现方(也称服务提供者)之间引入一个“接口”。
-当实现方提供了接口和实现,我们可以通过调用实现方的接口从而拥有实现方给我们提供的能力,这就是 API ,这种接口和实现都是放在实现方的。
-
-当接口存在于调用方这边时,就是 SPI ,由接口调用方确定接口规则,然后由不同的厂商去根据这个规则对这个接口进行实现,从而提供服务。
+- 当实现方提供了接口和实现,我们可以通过调用实现方的接口从而拥有实现方给我们提供的能力,这就是 **API**。这种情况下,接口和实现都是放在实现方的包中。调用方通过接口调用实现方的功能,而不需要关心具体的实现细节。
+- 当接口存在于调用方这边时,这就是 **SPI** 。由接口调用方确定接口规则,然后由不同的厂商根据这个规则对这个接口进行实现,从而提供服务。
举个通俗易懂的例子:公司 H 是一家科技公司,新设计了一款芯片,然后现在需要量产了,而市面上有好几家芯片制造业公司,这个时候,只要 H 公司指定好了这芯片生产的标准(定义好了接口标准),那么这些合作的芯片公司(服务提供者)就按照标准交付自家特色的芯片(提供不同方案的实现,但是给出来的结果是一样的)。
@@ -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,7 +555,7 @@ public class MyServiceLoader {
其实不难发现,SPI 机制的具体实现本质上还是通过反射完成的。即:**我们按照规定将要暴露对外使用的具体实现类在 `META-INF/services/` 文件下声明。**
-另外,SPI 机制在很多框架中都有应用:Spring 框架的基本原理也是类似的方式。还有 Dubbo 框架提供同样的 SPI 扩展机制,只不过 Dubbo 和 spring 框架中的 SPI 机制具体实现方式跟咱们今天学得这个有些细微的区别,不过整体的原理都是一致的,相信大家通过对 JDK 中 SPI 机制的学习,能够一通百通,加深对其他高深框的理解。
+另外,SPI 机制在很多框架中都有应用:Spring 框架的基本原理也是类似的方式。还有 Dubbo 框架提供同样的 SPI 扩展机制,只不过 Dubbo 和 spring 框架中的 SPI 机制具体实现方式跟咱们今天学得这个有些细微的区别,不过整体的原理都是一致的,相信大家通过对 JDK 中 SPI 机制的学习,能够一通百通,加深对其他高深框架的理解。
通过 SPI 机制能够大大地提高接口设计的灵活性,但是 SPI 机制也存在一些缺点,比如:
diff --git a/docs/java/basis/syntactic-sugar.md b/docs/java/basis/syntactic-sugar.md
index 3cc1831bbeb..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 面试常问的一个知识点。
@@ -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`其实是一个变量。
### 自动装箱与拆箱
diff --git a/docs/java/basis/unsafe.md b/docs/java/basis/unsafe.md
index 2516a11880a..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` 的类。
那这个类主要是用来干什么的呢?有什么使用场景呢?这篇文章就带你搞清楚!
@@ -322,7 +324,7 @@ public class 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));
+ System.out.println("value after putInt: " + unsafe.getInt(main, offset));
}
private static Unsafe reflectGetUnsafe() {
@@ -421,7 +423,7 @@ public void objTest() throws Exception{
#### 典型应用
-- **常规对象实例化方式**:我们通常所用到的创建对象的方式,从本质上来讲,都是通过 new 机制来实现对象的创建。但是,new 机制有个特点就是当类只提供有参的构造函数且无显示声明无参构造函数时,则必须使用有参构造函数进行对象构造,而使用有参构造函数时,必须传递相应个数的参数才能完成对象实例化。
+- **常规对象实例化方式**:我们通常所用到的创建对象的方式,从本质上来讲,都是通过 new 机制来实现对象的创建。但是,new 机制有个特点就是当类只提供有参的构造函数且无显式声明无参构造函数时,则必须使用有参构造函数进行对象构造,而使用有参构造函数时,必须传递相应个数的参数才能完成对象实例化。
- **非常规的实例化方式**:而 Unsafe 中提供 allocateInstance 方法,仅通过 Class 对象就可以创建此类的实例对象,而且不需要调用其构造函数、初始化代码、JVM 安全检查等。它抑制修饰符检测,也就是即使构造器是 private 修饰的也能通过此方法实例化,只需提类对象即可创建相应的对象。由于这种特性,allocateInstance 在 java.lang.invoke、Objenesis(提供绕过类构造器的对象生成方式)、Gson(反序列化时用到)中都有相应的应用。
### 数组操作
@@ -451,7 +453,7 @@ public native int arrayIndexScale(Class> arrayClass);
```java
/**
- * CAS
+ * CAS
* @param o 包含要修改field的对象
* @param offset 对象中某field的偏移量
* @param expected 期望值
@@ -514,11 +516,94 @@ private void increment(int x){
1 2 3 4 5 6 7 8 9
```
-在上面的例子中,使用两个线程去修改`int`型属性`a`的值,并且只有在`a`的值等于传入的参数`x`减一时,才会将`a`的值变为`x`,也就是实现对`a`的加一的操作。流程如下所示:
+如果你把上面这段代码贴到 IDE 中运行,会发现并不能得到目标输出结果。有朋友已经在 Github 上指出了这个问题:[issue#2650](https://github.com/Snailclimb/JavaGuide/issues/2650)。下面是修正后的代码:
+
+```java
+private volatile int a = 0; // 共享变量,初始值为 0
+private static final Unsafe unsafe;
+private static final long fieldOffset;
+
+static {
+ try {
+ // 获取 Unsafe 实例
+ Field theUnsafe = Unsafe.class.getDeclaredField("theUnsafe");
+ theUnsafe.setAccessible(true);
+ unsafe = (Unsafe) theUnsafe.get(null);
+ // 获取 a 字段的内存偏移量
+ fieldOffset = unsafe.objectFieldOffset(CasTest.class.getDeclaredField("a"));
+ } catch (Exception e) {
+ throw new RuntimeException("Failed to initialize Unsafe or field offset", e);
+ }
+}
+
+public static void main(String[] args) {
+ CasTest casTest = new CasTest();
+
+ Thread t1 = new Thread(() -> {
+ for (int i = 1; i <= 4; i++) {
+ casTest.incrementAndPrint(i);
+ }
+ });
+
+ Thread t2 = new Thread(() -> {
+ for (int i = 5; i <= 9; i++) {
+ casTest.incrementAndPrint(i);
+ }
+ });
+
+ t1.start();
+ t2.start();
+
+ // 等待线程结束,以便观察完整输出 (可选,用于演示)
+ try {
+ t1.join();
+ t2.join();
+ } catch (InterruptedException e) {
+ Thread.currentThread().interrupt();
+ }
+}
+
+// 将递增和打印操作封装在一个原子性更强的方法内
+private void incrementAndPrint(int targetValue) {
+ while (true) {
+ int currentValue = a; // 读取当前 a 的值
+ // 只有当 a 的当前值等于目标值的前一个值时,才尝试更新
+ if (currentValue == targetValue - 1) {
+ if (unsafe.compareAndSwapInt(this, fieldOffset, currentValue, targetValue)) {
+ // CAS 成功,说明成功将 a 更新为 targetValue
+ System.out.print(targetValue + " ");
+ break; // 成功更新并打印后退出循环
+ }
+ // 如果 CAS 失败,意味着在读取 currentValue 和执行 CAS 之间,a 的值被其他线程修改了,
+ // 此时 currentValue 已经不是 a 的最新值,需要重新读取并重试。
+ }
+ // 如果 currentValue != targetValue - 1,说明还没轮到当前线程更新,
+ // 或者已经被其他线程更新超过了,让出CPU给其他线程机会。
+ // 对于严格顺序递增的场景,如果 current > targetValue - 1,可能意味着逻辑错误或死循环,
+ // 但在此示例中,我们期望线程能按顺序执行。
+ Thread.yield(); // 提示CPU调度器可以切换线程,减少无效自旋
+ }
+}
+```
+
+在上述例子中,我们创建了两个线程,它们都尝试修改共享变量 a。每个线程在调用 `incrementAndPrint(targetValue)` 方法时:
+
+1. 会先读取 a 的当前值 `currentValue`。
+2. 检查 `currentValue` 是否等于 `targetValue - 1` (即期望的前一个值)。
+3. 如果条件满足,则调用`unsafe.compareAndSwapInt()` 尝试将 `a` 从 `currentValue` 更新到 `targetValue`。
+4. 如果 CAS 操作成功(返回 true),则打印 `targetValue` 并退出循环。
+5. 如果 CAS 操作失败,或者 `currentValue` 不满足条件,则当前线程会继续循环(自旋),并通过 `Thread.yield()` 尝试让出 CPU,直到成功更新并打印或者条件满足。
+
+这种机制确保了每个数字(从 1 到 9)只会被成功设置并打印一次,并且是按顺序进行的。

-需要注意的是,在调用`compareAndSwapInt`方法后,会直接返回`true`或`false`的修改结果,因此需要我们在代码中手动添加自旋的逻辑。在`AtomicInteger`类的设计中,也是采用了将`compareAndSwapInt`的结果作为循环条件,直至修改成功才退出死循环的方式来实现的原子性的自增操作。
+需要注意的是:
+
+1. **自旋逻辑:** `compareAndSwapInt` 方法本身只执行一次比较和交换操作,并立即返回结果。因此,为了确保操作最终成功(在值符合预期的情况下),我们需要在代码中显式地实现自旋逻辑(如 `while(true)` 循环),不断尝试直到 CAS 操作成功。
+2. **`AtomicInteger` 的实现:** JDK 中的 `java.util.concurrent.atomic.AtomicInteger` 类内部正是利用了类似的 CAS 操作和自旋逻辑来实现其原子性的 `getAndIncrement()`, `compareAndSet()` 等方法。直接使用 `AtomicInteger` 通常是更安全、更推荐的做法,因为它封装了底层的复杂性。
+3. **ABA 问题:** CAS 操作本身存在 ABA 问题(一个值从 A 变为 B,再变回 A,CAS 检查时会认为值没有变过)。在某些场景下,如果值的变化历史很重要,可能需要使用 `AtomicStampedReference` 来解决。但在本例的简单递增场景中,ABA 问题通常不构成影响。
+4. **CPU 消耗:** 长时间的自旋会消耗 CPU 资源。在竞争激烈或条件长时间不满足的情况下,可以考虑加入更复杂的退避策略(如 `Thread.sleep()` 或 `LockSupport.parkNanos()`)来优化。
### 线程调度
diff --git a/docs/java/basis/why-there-only-value-passing-in-java.md b/docs/java/basis/why-there-only-value-passing-in-java.md
index e9f9a7c2f20..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 只有值传递?
@@ -84,17 +84,17 @@ 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;
- }
+ }
```
输出:
diff --git a/docs/java/collection/arrayblockingqueue-source-code.md b/docs/java/collection/arrayblockingqueue-source-code.md
index 3a96767e769..4c923ef0d29 100644
--- a/docs/java/collection/arrayblockingqueue-source-code.md
+++ b/docs/java/collection/arrayblockingqueue-source-code.md
@@ -11,7 +11,7 @@ tag:
Java 阻塞队列的历史可以追溯到 JDK1.5 版本,当时 Java 平台增加了 `java.util.concurrent`,即我们常说的 JUC 包,其中包含了各种并发流程控制工具、并发容器、原子类等。这其中自然也包含了我们这篇文章所讨论的阻塞队列。
-为了解决高并发场景下多线程之间数据共享的问题,JDK1.5 版本中出现了 `ArrayBlockingQueue` 和 `LinkedBlockingQueue`,它们是带有生产者-消费者模式实现的并发容器。其中,`ArrayBlockingQueue` 是有界队列,即添加的元素达到上限之后,再次添加就会被阻塞或者抛出异常。而 `LinkedBlockingQueue` 则由链表构成的队列,正是因为链表的特性,所以 `LinkedBlockingQueue` 在添加元素上并不会向 `ArrayBlockingQueue` 那样有着较多的约束,所以 `LinkedBlockingQueue` 设置队列是否有界是可选的(注意这里的无界并不是指可以添加任务数量的元素,而是说队列的大小默认为 `Integer.MAX_VALUE`,近乎于无限大)。
+为了解决高并发场景下多线程之间数据共享的问题,JDK1.5 版本中出现了 `ArrayBlockingQueue` 和 `LinkedBlockingQueue`,它们是带有生产者-消费者模式实现的并发容器。其中,`ArrayBlockingQueue` 是有界队列,即添加的元素达到上限之后,再次添加就会被阻塞或者抛出异常。而 `LinkedBlockingQueue` 则由链表构成的队列,正是因为链表的特性,所以 `LinkedBlockingQueue` 在添加元素上并不会向 `ArrayBlockingQueue` 那样有着较多的约束,所以 `LinkedBlockingQueue` 设置队列是否有界是可选的(注意这里的无界并不是指可以添加任意数量的元素,而是说队列的大小默认为 `Integer.MAX_VALUE`,近乎于无限大)。
随着 Java 的不断发展,JDK 后续的几个版本又对阻塞队列进行了不少的更新和完善:
@@ -28,7 +28,7 @@ Java 阻塞队列的历史可以追溯到 JDK1.5 版本,当时 Java 平台增
3. 当阻塞队列因为消费者消费过慢或者生产者存放元素过快导致队列填满时无法容纳新元素时,生产者就会被阻塞,等待队列非满时继续存放元素。
4. 当消费者从队列中消费一个元素之后,队列就会通知生产者队列非满,生产者可以继续填充数据了。
-总结一下:阻塞队列就说基于非空和非满两个条件实现生产者和消费者之间的交互,尽管这些交互流程和等待通知的机制实现非常复杂,好在 Doug Lea 的操刀之下已将阻塞队列的细节屏蔽,我们只需调用 `put`、`take`、`offfer`、`poll` 等 API 即可实现多线程之间的生产和消费。
+总结一下:阻塞队列就说基于非空和非满两个条件实现生产者和消费者之间的交互,尽管这些交互流程和等待通知的机制实现非常复杂,好在 Doug Lea 的操刀之下已将阻塞队列的细节屏蔽,我们只需调用 `put`、`take`、`offer`、`poll` 等 API 即可实现多线程之间的生产和消费。
这也使得阻塞队列在多线程开发中有着广泛的运用,最常见的例子无非是我们的线程池,从源码中我们就能看出当核心线程无法及时处理任务时,这些任务都会扔到 `workQueue` 中。
@@ -119,8 +119,8 @@ public class ProducerConsumerExample {
生产者添加元素:2
消费者取出元素:1
消费者取出元素:2
-消费者取出元素:3
生产者添加元素:3
+消费者取出元素:3
生产者添加元素:4
生产者添加元素:5
消费者取出元素:4
@@ -238,7 +238,7 @@ public class DrainToExample {
public abstract class AbstractQueue
extends AbstractCollection
implements Queue {
- //...
+ //...
}
```
@@ -249,11 +249,11 @@ public abstract class AbstractQueue
```java
public boolean add(E e) {
- if (offer(e))
- return true;
- else
- throw new IllegalStateException("Queue full");
- }
+ if (offer(e))
+ return true;
+ else
+ throw new IllegalStateException("Queue full");
+}
```
而 `AbstractQueue` 中并没有对 `Queue` 的 `offer` 的实现,很明显这样做的目的是定义好了 `add` 的核心逻辑,将 `offer` 的细节交由其子类即我们的 `ArrayBlockingQueue` 实现。
@@ -265,13 +265,13 @@ public boolean add(E e) {
```java
public interface BlockingQueue extends Queue {
- //元素入队成功返回true,反之则会抛出异常IllegalStateException
+ //元素入队成功返回true,反之则会抛出异常IllegalStateException
boolean add(E e);
- //元素入队成功返回true,反之返回false
+ //元素入队成功返回true,反之返回false
boolean offer(E e);
- //元素入队成功则直接返回,如果队列已满元素不可入队则将线程阻塞,因为阻塞期间可能会被打断,所以这里方法签名抛出了InterruptedException
+ //元素入队成功则直接返回,如果队列已满元素不可入队则将线程阻塞,因为阻塞期间可能会被打断,所以这里方法签名抛出了InterruptedException
void put(E e) throws InterruptedException;
//和上一个方法一样,只不过队列满时只会阻塞单位为unit,时间为timeout的时长,如果在等待时长内没有入队成功则直接返回false。
@@ -281,20 +281,20 @@ public interface BlockingQueue extends Queue {
//从队头取出一个元素,如果队列为空则阻塞等待,因为会阻塞线程的缘故,所以该方法可能会被打断,所以签名定义了InterruptedException
E take() throws InterruptedException;
- //取出队头的元素并返回,如果当前队列为空则阻塞等待timeout且单位为unit的时长,如果这个时间段没有元素则直接返回null。
+ //取出队头的元素并返回,如果当前队列为空则阻塞等待timeout且单位为unit的时长,如果这个时间段没有元素则直接返回null。
E poll(long timeout, TimeUnit unit)
throws InterruptedException;
- //获取队列剩余元素个数
+ //获取队列剩余元素个数
int remainingCapacity();
- //删除我们指定的对象,如果成功返回true,反之返回false。
+ //删除我们指定的对象,如果成功返回true,反之返回false。
boolean remove(Object o);
//判断队列中是否包含指定元素
public boolean contains(Object o);
- //将队列中的元素全部存到指定的集合中
+ //将队列中的元素全部存到指定的集合中
int drainTo(Collection super E> c);
//转移maxElements个元素到集合中
@@ -306,12 +306,12 @@ public interface BlockingQueue extends Queue {
```java
public boolean add(E e) {
- //AbstractQueue的offer来自下层的ArrayBlockingQueue从BlockingQueue继承并实现的offer方法
- if (offer(e))
- return true;
- else
- throw new IllegalStateException("Queue full");
- }
+ //AbstractQueue的offer来自下层的ArrayBlockingQueue从BlockingQueue继承并实现的offer方法
+ if (offer(e))
+ return true;
+ else
+ throw new IllegalStateException("Queue full");
+}
```
### 初始化
@@ -321,17 +321,17 @@ public boolean add(E e) {
```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();
- }
+ //如果设置的队列大小小于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` (非满) ,需要我们格外留意,它们是实现生产者和消费者有序工作的关键所在,这一点笔者会在后续的源码解析中详细说明,这里我们只需初步了解一下阻塞队列的构造即可。
@@ -349,32 +349,32 @@ public ArrayBlockingQueue(int capacity, boolean fair) {
```java
public ArrayBlockingQueue(int capacity, boolean fair,
Collection extends E> 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();
- }
- }
+ //初始化容量和锁的公平性
+ 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();
+ }
+}
```
### 阻塞式获取和新增元素
@@ -402,42 +402,42 @@ public ArrayBlockingQueue(int capacity, boolean fair,
```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();
- }
+ //确保插入的元素不为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();
- }
+ //获取队列底层的数组
+ final Object[] items = this.items;
+ //将putindex位置的值设置为我们传入的x
+ items[putIndex] = x;
+ //更新putindex,如果putindex等于数组长度,则更新为0
+ if (++putIndex == items.length)
+ putIndex = 0;
+ //队列长度+1
+ count++;
+ //通知队列非空,那些因为获取元素而阻塞的线程可以继续工作了
+ notEmpty.signal();
+}
```
从源码中可以看到入队操作的逻辑就是在数组中追加一个新元素,整体执行步骤为:
@@ -452,20 +452,20 @@ private void enqueue(E x) {
```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();
- }
- }
+ //获取锁
+ 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` 方法等待队列非空时获取并移除元素(非空条件)。
@@ -474,24 +474,24 @@ public E take() throws InterruptedException {
```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;
- }
+ //获取阻塞队列底层的数组
+ 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` 方法(入队)的步骤大致类似,这里就不重复介绍了。
@@ -523,7 +523,7 @@ public boolean offer(E e) {
final ReentrantLock lock = this.lock;
lock.lock();
try {
- //队列已满直接返回false
+ //队列已满直接返回false
if (count == items.length)
return false;
else {
@@ -546,7 +546,7 @@ public E poll() {
//上锁
lock.lock();
try {
- //如果队列为空直接返回null,反之出队返回元素值
+ //如果队列为空直接返回null,反之出队返回元素值
return (count == 0) ? null : dequeue();
} finally {
lock.unlock();
@@ -558,13 +558,12 @@ public E poll() {
```java
public boolean add(E e) {
- //调用下方的add
return super.add(e);
}
public boolean add(E e) {
- //调用offer如果失败直接抛出异常
+ //调用offer方法如果失败直接抛出异常
if (offer(e))
return true;
else
@@ -644,7 +643,7 @@ public E poll(long timeout, TimeUnit unit) throws InterruptedException {
final ReentrantLock lock = this.lock;
lock.lockInterruptibly();
try {
- //队列为空,循环等待,若时间到还是空的,则直接返回null
+ //队列为空,循环等待,若时间到还是空的,则直接返回null
while (count == 0) {
if (nanos <= 0)
return null;
diff --git a/docs/java/collection/arraylist-source-code.md b/docs/java/collection/arraylist-source-code.md
index 30d80f5b1bd..5c71801b699 100644
--- a/docs/java/collection/arraylist-source-code.md
+++ b/docs/java/collection/arraylist-source-code.md
@@ -159,19 +159,22 @@ public class ArrayList extends AbstractList
* @param minCapacity 所需的最小容量
*/
public void ensureCapacity(int minCapacity) {
- //如果是true,minExpand的值为0,如果是false,minExpand的值为10
+ // 如果不是默认空数组,则minExpand的值为0;
+ // 如果是默认空数组,则minExpand的值为10
int minExpand = (elementData != DEFAULTCAPACITY_EMPTY_ELEMENTDATA)
- // any size if not default element table
+ // 如果不是默认元素表,则可以使用任意大小
? 0
- // larger than default for default empty table. It's already
- // supposed to be at default size.
+ // 如果是默认空数组,它应该已经是默认大小
: DEFAULT_CAPACITY;
- //如果最小容量大于已有的最大容量
+
+ // 如果最小容量大于已有的最大容量
if (minCapacity > minExpand) {
+ // 根据需要的最小容量,确保容量足够
ensureExplicitCapacity(minCapacity);
}
}
+
// 根据给定的最小容量和当前数组元素来计算所需容量。
private static int calculateCapacity(Object[] elementData, int minCapacity) {
// 如果当前数组元素为空数组(初始情况),返回默认容量和最小容量中的较大值作为所需容量
@@ -215,7 +218,7 @@ 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:
@@ -305,8 +308,11 @@ public class ArrayList extends AbstractList
/**
* 以正确的顺序(从第一个到最后一个元素)返回一个包含此列表中所有元素的数组。
- * 返回的数组将是“安全的”,因为该列表不保留对它的引用。 (换句话说,这个方法必须分配一个新的数组)。
- * 因此,调用者可以自由地修改返回的数组。 此方法充当基于阵列和基于集合的API之间的桥梁。
+ * 返回的数组将是“安全的”,因为该列表不保留对它的引用。
+ * (换句话说,这个方法必须分配一个新的数组)。
+ * 因此,调用者可以自由地修改返回的数组结构。
+ * 注意:如果元素是引用类型,修改元素的内容会影响到原列表中的对象。
+ * 此方法充当基于数组和基于集合的API之间的桥梁。
*/
public Object[] toArray() {
return Arrays.copyOf(elementData, size);
@@ -426,8 +432,7 @@ public class ArrayList extends AbstractList
}
/*
- * Private remove method that skips bounds checking and does not
- * return the value removed.
+ * 该方法为私有的移除方法,跳过了边界检查,并且不返回被移除的值。
*/
private void fastRemove(int index) {
modCount++;
@@ -435,7 +440,7 @@ public class ArrayList extends AbstractList
if (numMoved > 0)
System.arraycopy(elementData, index + 1, elementData, index,
numMoved);
- elementData[--size] = null; // clear to let GC do its work
+ elementData[--size] = null; // 在移除元素后,将该位置的元素设为 null,以便垃圾回收器(GC)能够回收该元素。
}
/**
@@ -729,7 +734,7 @@ private void grow(int minCapacity) {
**我们再来通过例子探究一下`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,不会进入 huge`C`apacity 方法。数组容量扩为 15,add 方法中 return true,size 增为 11。
+- 当 `add` 第 11 个元素进入 `grow` 方法时,`newCapacity` 为 15,比 `minCapacity`(为 11)大,第一个 if 判断不成立。新容量没有大于数组最大 size,不会进入 `hugeCapacity` 方法。数组容量扩为 15,add 方法中 return true,size 增为 11。
- 以此类推······
**这里补充一点比较重要,但是容易被忽视掉的知识点:**
@@ -804,19 +809,19 @@ private static int hugeCapacity(int minCapacity) {
```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] + " ");
+ }
+ }
}
```
@@ -833,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;
@@ -859,14 +864,14 @@ 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);
+ }
}
```
@@ -917,17 +922,17 @@ 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));
+
+ }
}
```
diff --git a/docs/java/collection/concurrent-hash-map-source-code.md b/docs/java/collection/concurrent-hash-map-source-code.md
index 9a3a4041b06..d0d210aacdf 100644
--- a/docs/java/collection/concurrent-hash-map-source-code.md
+++ b/docs/java/collection/concurrent-hash-map-source-code.md
@@ -5,7 +5,7 @@ tag:
- Java集合
---
-> 本文来自公众号:末读代码的投稿,原文地址:https://mp.weixin.qq.com/s/AHWzboztt53ZfFZmsSnMSw 。
+> 本文来自公众号:末读代码的投稿,原文地址: 。
上一篇文章介绍了 HashMap 源码,反响不错,也有很多同学发表了自己的观点,这次又来了,这次是 `ConcurrentHashMap` 了,作为线程安全的 HashMap ,它的使用频率也是很高。那么它的存储结构和实现原理是怎么样的呢?
@@ -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 已经初始化。
diff --git a/docs/java/collection/copyonwritearraylist-source-code.md b/docs/java/collection/copyonwritearraylist-source-code.md
index 6c2d6140960..9aceb83bc4e 100644
--- a/docs/java/collection/copyonwritearraylist-source-code.md
+++ b/docs/java/collection/copyonwritearraylist-source-code.md
@@ -198,7 +198,7 @@ public E remove(int index) {
// 加锁
lock.lock();
try {
- //获取当前array数组
+ //获取当前array数组
Object[] elements = getArray();
// 获取当前array长度
int len = elements.length;
@@ -207,7 +207,7 @@ public E remove(int index) {
int numMoved = len - index - 1;
// 判断删除的是否是最后一个元素
if (numMoved == 0)
- // 如果删除的是最后一个元素,直接复制该元素前的所有元素到新的数组
+ // 如果删除的是最后一个元素,直接复制该元素前的所有元素到新的数组
setArray(Arrays.copyOf(elements, len - 1));
else {
// 分段复制,将index前的元素和index+1后的元素复制到新数组
@@ -221,7 +221,7 @@ public E remove(int index) {
}
return oldValue;
} finally {
- // 解锁
+ // 解锁
lock.unlock();
}
}
diff --git a/docs/java/collection/delayqueue-source-code.md b/docs/java/collection/delayqueue-source-code.md
index 791df508d6f..5fb6f4affad 100644
--- a/docs/java/collection/delayqueue-source-code.md
+++ b/docs/java/collection/delayqueue-source-code.md
@@ -267,7 +267,7 @@ public E take() throws InterruptedException {
}
}
} finally {
- //收尾逻辑:如果leader不为空且q有元素,则说明有任务没人认领,直接发起通知唤醒因为锁被当前消费者持有而导致阻塞的生产者(即调用put、add、offer的线程)
+ // 收尾逻辑:当leader为null,并且队列中有任务时,唤醒等待的获取元素的线程。
if (leader == null && q.peek() != null)
available.signal();
//释放锁
diff --git a/docs/java/collection/hashmap-source-code.md b/docs/java/collection/hashmap-source-code.md
index a4f92b8a38e..0e9342f0edf 100644
--- a/docs/java/collection/hashmap-source-code.md
+++ b/docs/java/collection/hashmap-source-code.md
@@ -90,7 +90,7 @@ public class HashMap extends AbstractMap implements Map, Cloneabl
static final int MIN_TREEIFY_CAPACITY = 64;
// 存储元素的数组,总是2的幂次倍
transient Node[] table;
- // 存放具体元素的集
+ // 一个包含了映射中所有键值对的集合视图
transient Set> entrySet;
// 存放元素的个数,注意这个不等于数组的长度。
transient int size;
diff --git a/docs/java/collection/java-collection-precautions-for-use.md b/docs/java/collection/java-collection-precautions-for-use.md
index e636ff5a77a..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。
## 集合遍历
diff --git a/docs/java/collection/java-collection-questions-01.md b/docs/java/collection/java-collection-questions-01.md
index 1c0a44488a8..0e5622f60cc 100644
--- a/docs/java/collection/java-collection-questions-01.md
+++ b/docs/java/collection/java-collection-questions-01.md
@@ -14,11 +14,13 @@ head:
+
+
## 集合概述
### Java 集合概览
-Java 集合, 也叫作容器,主要是由两大接口派生而来:一个是 `Collection`接口,主要用于存放单一元素;另一个是 `Map` 接口,主要用于存放键值对。对于`Collection` 接口,下面又有三个主要的子接口:`List`、`Set` 和 `Queue`。
+Java 集合,也叫作容器,主要是由两大接口派生而来:一个是 `Collection`接口,主要用于存放单一元素;另一个是 `Map` 接口,主要用于存放键值对。对于`Collection` 接口,下面又有三个主要的子接口:`List`、`Set` 、 `Queue`。
Java 集合框架如下图所示:
@@ -26,7 +28,7 @@ Java 集合框架如下图所示:
注:图中只列举了主要的继承派生关系,并没有列举所有关系。比方省略了`AbstractList`, `NavigableSet`等抽象类以及其他的一些辅助类,如想深入了解,可自行查看源码。
-### 说说 List, Set, Queue, Map 四者的区别?
+### ⭐️说说 List, Set, Queue, Map 四者的区别?
- `List`(对付顺序的好帮手): 存储的元素是有序的、可重复的。
- `Set`(注重独一无二的性质): 存储的元素不可重复的。
@@ -77,7 +79,7 @@ Java 集合框架如下图所示:
## List
-### ArrayList 和 Array(数组)的区别?
+### ⭐️ArrayList 和 Array(数组)的区别?
`ArrayList` 内部基于动态数组实现,比 `Array`(静态数组) 使用起来更加灵活:
@@ -152,7 +154,7 @@ System.out.println(listOfStrings);
[null, java]
```
-### ArrayList 插入和删除元素的时间复杂度?
+### ⭐️ArrayList 插入和删除元素的时间复杂度?
对于插入:
@@ -186,13 +188,13 @@ System.out.println(listOfStrings);
0 1 2 3 4 5 6 7 8 9
```
-### LinkedList 插入和删除元素的时间复杂度?
+### ⭐️LinkedList 插入和删除元素的时间复杂度?
- 头部插入/删除:只需要修改头结点的指针即可完成插入/删除操作,因此时间复杂度为 O(1)。
- 尾部插入/删除:只需要修改尾结点的指针即可完成插入/删除操作,因此时间复杂度为 O(1)。
-- 指定位置插入/删除:需要先移动到指定位置,再修改指定节点的指针完成插入/删除,因此需要移动平均 n/2 个元素,时间复杂度为 O(n)。
+- 指定位置插入/删除:需要先移动到指定位置,再修改指定节点的指针完成插入/删除,不过由于有头尾指针,可以从较近的指针出发,因此需要遍历平均 n/4 个元素,时间复杂度为 O(n)。
-这里简单列举一个例子:假如我们要删除节点 9 的话,需要先遍历链表找到该节点。然后,再执行相应节点指针指向的更改,具体的源码可以参考:[LinkedList 源码分析](./linkedlist-source-code.md) 。
+这里简单列举一个例子:假如我们要删除节点 9 的话,需要先遍历链表找到该节点。然后,再执行相应节点指针指向的更改,具体的源码可以参考:[LinkedList 源码分析](https://javaguide.cn/java/collection/linkedlist-source-code.html) 。

@@ -200,7 +202,7 @@ System.out.println(listOfStrings);
`RandomAccess` 是一个标记接口,用来表明实现该接口的类支持随机访问(即可以通过索引快速访问元素)。由于 `LinkedList` 底层数据结构是链表,内存地址不连续,只能通过指针来定位,不支持随机快速访问,所以不能实现 `RandomAccess` 接口。
-### ArrayList 与 LinkedList 区别?
+### ⭐️ArrayList 与 LinkedList 区别?
- **是否保证线程安全:** `ArrayList` 和 `LinkedList` 都是不同步的,也就是不保证线程安全;
- **底层数据结构:** `ArrayList` 底层使用的是 **`Object` 数组**;`LinkedList` 底层使用的是 **双向链表** 数据结构(JDK1.6 之前为循环链表,JDK1.7 取消了循环。注意双向链表和双向循环链表的区别,下面有介绍到!)
@@ -235,7 +237,7 @@ public interface RandomAccess {
查看源码我们发现实际上 `RandomAccess` 接口中什么都没有定义。所以,在我看来 `RandomAccess` 接口不过是一个标识罢了。标识什么? 标识实现这个接口的类具有随机访问功能。
-在 `binarySearch()` 方法中,它要判断传入的 list 是否 `RandomAccess` 的实例,如果是,调用`indexedBinarySearch()`方法,如果不是,那么调用`iteratorBinarySearch()`方法
+在 `binarySearch()` 方法中,它要判断传入的 list 是否 `RandomAccess` 的实例,如果是,调用`indexedBinarySearch()`方法,如果不是,那么调用`iteratorBinarySearch()`方法
```java
public static
@@ -249,9 +251,105 @@ public interface RandomAccess {
`ArrayList` 实现了 `RandomAccess` 接口, 而 `LinkedList` 没有实现。为什么呢?我觉得还是和底层数据结构有关!`ArrayList` 底层是数组,而 `LinkedList` 底层是链表。数组天然支持随机访问,时间复杂度为 O(1),所以称为快速随机访问。链表需要遍历到特定位置才能访问特定位置的元素,时间复杂度为 O(n),所以不支持快速随机访问。`ArrayList` 实现了 `RandomAccess` 接口,就表明了他具有快速随机访问功能。 `RandomAccess` 接口只是标识,并不是说 `ArrayList` 实现 `RandomAccess` 接口才具有快速随机访问功能的!
-### 说一说 ArrayList 的扩容机制吧
+### ⭐️说一说 ArrayList 的扩容机制吧
+
+详见笔主的这篇文章: [ArrayList 扩容机制分析](https://javaguide.cn/java/collection/arraylist-source-code.html#arraylist-扩容机制分析)。
+
+### ⭐️集合中的 fail-fast 和 fail-safe 是什么?
+
+关于`fail-fast`引用`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`:
+
+
+
+对此我们也给出`for`循环底层迭代器获取下一个元素时的`next`方法,可以看到其内部的`checkForComodification`具有针对修改次数比对的逻辑:
+
+```java
+ public E next() {
+ //检查是否存在并发修改
+ checkForComodification();
+ //......
+ //返回下一个元素
+ return (E) elementData[lastRet = i];
+ }
+
+final void checkForComodification() {
+ //当前循环遍历次数和预期修改次数不一致时,就会抛出ConcurrentModificationException
+ if (modCount != expectedModCount)
+ throw new ConcurrentModificationException();
+ }
+
+```
-详见笔主的这篇文章: [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-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`底层的数组引用指向这个新的数组空间,由此避免迭代时被并发修改所干扰所导致并发操作安全问题,当然这种做法也存在缺点,即进行遍历操作时无法获得实时结果:
+
+
+
+对应我们也给出`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
@@ -473,7 +571,7 @@ 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`:延迟队列,其中的元素只有到了其指定的延迟时间,才能够从队列中出队。
@@ -481,7 +579,7 @@ Java 中常用的阻塞队列实现类有以下几种:
日常开发中,这些队列使用的其实都不多,了解即可。
-### ArrayBlockingQueue 和 LinkedBlockingQueue 有什么区别?
+### ⭐️ArrayBlockingQueue 和 LinkedBlockingQueue 有什么区别?
`ArrayBlockingQueue` 和 `LinkedBlockingQueue` 是 Java 并发包中常用的两种阻塞队列实现,它们都是线程安全的。不过,不过它们之间也存在下面这些区别:
diff --git a/docs/java/collection/java-collection-questions-02.md b/docs/java/collection/java-collection-questions-02.md
index 190c928e627..f71b5128b06 100644
--- a/docs/java/collection/java-collection-questions-02.md
+++ b/docs/java/collection/java-collection-questions-02.md
@@ -16,13 +16,14 @@ head:
## Map(重要)
-### HashMap 和 Hashtable 的区别
+### ⭐️HashMap 和 Hashtable 的区别
- **线程是否安全:** `HashMap` 是非线程安全的,`Hashtable` 是线程安全的,因为 `Hashtable` 内部的方法基本都经过`synchronized` 修饰。(如果你要保证线程安全的话就使用 `ConcurrentHashMap` 吧!);
- **效率:** 因为线程安全的问题,`HashMap` 要比 `Hashtable` 效率高一点。另外,`Hashtable` 基本被淘汰,不要在代码中使用它;
- **对 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` 中带有初始容量的构造函数:**
@@ -47,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 区别
@@ -72,7 +73,7 @@ head:
| 调用 `put()`向 map 中添加元素 | 调用 `add()`方法向 `Set` 中添加元素 |
| `HashMap` 使用键(Key)计算 `hashcode` | `HashSet` 使用成员对象来计算 `hashcode` 值,对于两个对象来说 `hashcode` 可能相同,所以`equals()`方法用来判断对象的相等性 |
-### HashMap 和 TreeMap 区别
+### ⭐️HashMap 和 TreeMap 区别
`TreeMap` 和`HashMap` 都继承自`AbstractMap` ,但是需要注意的是`TreeMap`它还实现了`NavigableMap`接口和`SortedMap` 接口。
@@ -80,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
@@ -138,7 +148,7 @@ TreeMap treeMap = new TreeMap<>((person1, person2) -> {
});
```
-**综上,相比于`HashMap`来说 `TreeMap` 主要多了对集合中的元素根据键排序的能力以及对集合内元素的搜索的能力。**
+**综上,相比于`HashMap`来说, `TreeMap` 主要多了对集合中的元素根据键排序的能力以及对集合内元素的搜索的能力。**
### HashSet 如何检查重复?
@@ -169,13 +179,13 @@ final V putVal(int hash, K key, V value, boolean onlyIfAbsent,
也就是说,在 JDK1.8 中,实际上无论`HashSet`中是否已经存在了某元素,`HashSet`都会直接插入,只是会在`add()`方法的返回值处告诉我们插入前是否存在相同元素。
-### HashMap 的底层实现
+### ⭐️HashMap 的底层实现
#### JDK1.8 之前
JDK1.8 之前 `HashMap` 底层是 **数组和链表** 结合在一起使用也就是 **链表散列**。HashMap 通过 key 的 `hashcode` 经过扰动函数处理过后得到 hash 值,然后通过 `(n - 1) & hash` 判断当前元素存放的位置(这里的 n 指的是数组的长度),如果当前位置存在元素的话,就判断该元素与要存入的元素的 hash 值以及 key 是否相同,如果相同的话,直接覆盖,不相同就通过拉链法解决冲突。
-所谓扰动函数指的就是 HashMap 的 `hash` 方法。使用 `hash` 方法也就是扰动函数是为了防止一些实现比较差的 `hashCode()` 方法 换句话说使用扰动函数之后可以减少碰撞。
+`HashMap` 中的扰动函数(`hash` 方法)是用来优化哈希值的分布。通过对原始的 `hashCode()` 进行额外处理,扰动函数可以减小由于糟糕的 `hashCode()` 实现导致的碰撞,从而提高数据的分布均匀性。
**JDK 1.8 HashMap 的 hash 方法源码:**
@@ -212,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) 的性能差异不明显。但当链表变长时,查询性能会显著下降。

+**为什么优先扩容而非直接转为红黑树?**
+
+数组扩容能减少哈希冲突的发生概率(即将元素重新分散到新的、更大的数组中),这在多数情况下比直接转换为红黑树更高效。
+
+红黑树需要保持自平衡,维护成本较高。并且,过早引入红黑树反而会增加复杂度。
+
+**为什么选择阈值 8 和 64?**
+
+1. 泊松分布表明,链表长度达到 8 的概率极低(小于千万分之一)。在绝大多数情况下,链表长度都不会超过 8。阈值设置为 8,可以保证性能和空间效率的平衡。
+2. 数组长度阈值 64 同样是经过实践验证的经验值。在小数组中扩容成本低,优先扩容可以避免过早引入红黑树。数组大小达到 64 时,冲突概率较高,此时红黑树的性能优势开始显现。
+
> TreeMap、TreeSet 以及 JDK1.8 之后的 HashMap 底层都用到了红黑树。红黑树就是为了解决二叉查找树的缺陷,因为二叉查找树在某些情况下会退化成一个线性结构。
我们来结合源码分析一下 `HashMap` 链表到红黑树的转换。
@@ -230,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,15 +297,59 @@ final void treeifyBin(Node[] tab, int hash) {
将链表转换成红黑树前会判断,如果当前数组的长度小于 64,那么会选择先进行数组扩容,而不是转换为红黑树。
-### HashMap 的长度为什么是 2 的幂次方
+### ⭐️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 多线程操作导致死循环问题
+### ⭐️HashMap 多线程操作导致死循环问题
JDK1.7 及之前版本的 `HashMap` 在多线程环境下扩容操作可能存在死循环问题,这是由于当一个桶位中有多个元素需要进行扩容时,多个线程同时对链表进行操作,头插法可能会导致链表中的节点指向错误的位置,从而形成一个环形链表,进而使得查询元素的操作陷入死循环无法结束。
@@ -290,7 +357,7 @@ JDK1.7 及之前版本的 `HashMap` 在多线程环境下扩容操作可能存
一般面试中这样介绍就差不多,不需要记各种细节,个人觉得也没必要记。如果想要详细了解 `HashMap` 扩容导致死循环问题,可以看看耗子叔的这篇文章:[Java HashMap 的死循环](https://coolshell.cn/articles/9606.html)。
-### HashMap 为什么线程不安全?
+### ⭐️HashMap 为什么线程不安全?
JDK1.7 及之前版本,在多线程环境下,`HashMap` 扩容时会造成死循环和数据丢失的问题。
@@ -374,11 +441,11 @@ Test.lambda avgt 5 1551065180.000 ± 19164407.426 ns/op
Test.parallelStream avgt 5 186345456.667 ± 3210435.590 ns/op
```
-### ConcurrentHashMap 和 Hashtable 的区别
+### ⭐️ConcurrentHashMap 和 Hashtable 的区别
`ConcurrentHashMap` 和 `Hashtable` 的区别主要体现在实现线程安全的方式上不同。
-- **底层数据结构:** JDK1.7 的 `ConcurrentHashMap` 底层采用 **分段的数组+链表** 实现,JDK1.8 采用的数据结构跟 `HashMap1.8` 的结构一样,数组+链表/红黑二叉树。`Hashtable` 和 JDK1.8 之前的 `HashMap` 的底层数据结构类似都是采用 **数组+链表** 的形式,数组是 HashMap 的主体,链表则是主要为了解决哈希冲突而存在的;
+- **底层数据结构:** JDK1.7 的 `ConcurrentHashMap` 底层采用 **分段的数组+链表** 实现,在 JDK1.8 中采用的数据结构跟 `HashMap` 的结构一样,数组+链表/红黑二叉树。`Hashtable` 和 JDK1.8 之前的 `HashMap` 的底层数据结构类似都是采用 **数组+链表** 的形式,数组是 HashMap 的主体,链表则是主要为了解决哈希冲突而存在的;
- **实现线程安全的方式(重要):**
- 在 JDK1.7 的时候,`ConcurrentHashMap` 对整个桶数组进行了分割分段(`Segment`,分段锁),每一把锁只锁容器其中一部分数据(下面有示意图),多线程访问容器里不同数据段的数据,就不会存在锁竞争,提高并发访问率。
- 到了 JDK1.8 的时候,`ConcurrentHashMap` 已经摒弃了 `Segment` 的概念,而是直接用 `Node` 数组+链表+红黑树的数据结构来实现,并发控制使用 `synchronized` 和 CAS 来操作。(JDK1.6 以后 `synchronized` 锁做了很多优化) 整个看起来就像是优化过且线程安全的 `HashMap`,虽然在 JDK1.8 中还能看到 `Segment` 的数据结构,但是已经简化了属性,只是为了兼容旧版本;
@@ -422,7 +489,7 @@ static final class TreeBin extends Node {
}
```
-### ConcurrentHashMap 线程安全的具体实现方式/底层具体实现
+### ⭐️ConcurrentHashMap 线程安全的具体实现方式/底层具体实现
#### JDK1.8 之前
@@ -453,7 +520,7 @@ Java 8 几乎完全重写了 `ConcurrentHashMap`,代码量从原来 Java 7 中
Java 8 中,锁粒度更细,`synchronized` 只锁定当前链表或红黑二叉树的首节点,这样只要 hash 不冲突,就不会产生并发,就不会影响其他 Node 的读写,效率大幅提升。
-### JDK 1.7 和 JDK 1.8 的 ConcurrentHashMap 实现有什么不同?
+### ⭐️JDK 1.7 和 JDK 1.8 的 ConcurrentHashMap 实现有什么不同?
- **线程安全实现方式**:JDK 1.7 采用 `Segment` 分段锁来保证安全, `Segment` 是继承自 `ReentrantLock`。JDK1.8 放弃了 `Segment` 分段锁的设计,采用 `Node + CAS + synchronized` 保证线程安全,锁粒度更细,`synchronized` 只锁定当前链表或红黑二叉树的首节点。
- **Hash 碰撞解决方法** : JDK 1.7 采用拉链法,JDK1.8 采用拉链法结合红黑树(链表长度超过一定阈值时,将链表转换为红黑树)。
@@ -490,7 +557,7 @@ public static final Object NULL = new Object();
翻译过来之后的,大致意思还是单线程下可以容忍歧义,而多线程下无法容忍。
-### ConcurrentHashMap 能保证复合操作的原子性吗?
+### ⭐️ConcurrentHashMap 能保证复合操作的原子性吗?
`ConcurrentHashMap` 是线程安全的,意味着它可以保证多个线程同时对它进行读写操作时,不会出现数据不一致的情况,也不会导致 JDK1.7 及之前版本的 `HashMap` 多线程操作导致死循环问题。但是,这并不意味着它可以保证所有的复合操作都是原子性的,一定不要搞混了!
diff --git a/docs/java/collection/linkedhashmap-source-code.md b/docs/java/collection/linkedhashmap-source-code.md
index d25a6ce377e..08c9a2bcb28 100644
--- a/docs/java/collection/linkedhashmap-source-code.md
+++ b/docs/java/collection/linkedhashmap-source-code.md
@@ -111,15 +111,16 @@ public class LRUCache extends LinkedHashMap {
}
```
-测试代码如下,笔者初始化缓存容量为 2,然后按照次序先后添加 4 个元素。
+测试代码如下,笔者初始化缓存容量为 3,然后按照次序先后添加 4 个元素。
```java
-LRUCache < Integer, String > cache = new LRUCache < > (2);
+LRUCache cache = new LRUCache<>(3);
cache.put(1, "one");
cache.put(2, "two");
cache.put(3, "three");
cache.put(4, "four");
-for (int i = 0; i < 4; i++) {
+cache.put(5, "five");
+for (int i = 1; i <= 5; i++) {
System.out.println(cache.get(i));
}
```
@@ -131,9 +132,10 @@ null
null
three
four
+five
```
-从输出结果来看,由于缓存容量为 2 ,因此,添加第 3 个元素时,第 1 个元素会被删除。添加第 4 个元素时,第 2 个元素会被删除。
+从输出结果来看,由于缓存容量为 3 ,因此,添加第 4 个元素时,第 1 个元素会被删除。添加第 5 个元素时,第 2 个元素会被删除。
## LinkedHashMap 源码解析
@@ -180,7 +182,7 @@ static class Entry extends HashMap.Node {
```java
static final class TreeNode extends LinkedHashMap.Entry {
- //略
+ //略
}
```
@@ -256,7 +258,7 @@ public V get(Object key) {
```java
void afterNodeAccess(Node < K, V > e) { // move node to last
LinkedHashMap.Entry < K, V > last;
- //如果accessOrder 且当前节点不未链表尾节点
+ //如果accessOrder 且当前节点不为链表尾节点
if (accessOrder && (last = tail) != e) {
//获取当前节点、以及前驱节点和后继节点
@@ -270,7 +272,7 @@ void afterNodeAccess(Node < K, V > e) { // move node to last
if (b == null)
head = a;
else
- //如果后继节点不为空,则让前驱节点指向后继节点
+ //如果前驱节点不为空,则让前驱节点指向后继节点
b.after = a;
//如果后继节点不为空,则让后继节点指向前驱节点
@@ -346,20 +348,20 @@ void afterNodeRemoval(Node p) { }
```java
void afterNodeRemoval(Node e) { // unlink
- //获取当前节点p、以及e的前驱节点b和后继节点a
+ //获取当前节点p、以及e的前驱节点b和后继节点a
LinkedHashMap.Entry p =
(LinkedHashMap.Entry)e, b = p.before, a = p.after;
- //将p的前驱和后继指针都设置为null,使其和前驱、后继节点断开联系
+ //将p的前驱和后继指针都设置为null,使其和前驱、后继节点断开联系
p.before = p.after = null;
- //如果前驱节点为空,则说明当前节点p是链表首节点,让head指针指向后继节点a即可
+ //如果前驱节点为空,则说明当前节点p是链表首节点,让head指针指向后继节点a即可
if (b == null)
head = a;
else
//如果前驱节点b不为空,则让b直接指向后继节点a
b.after = a;
- //如果后继节点为空,则说明当前节点p在链表末端,所以直接让tail指针指向前驱节点a即可
+ //如果后继节点为空,则说明当前节点p在链表末端,所以直接让tail指针指向前驱节点a即可
if (a == null)
tail = b;
else
@@ -370,10 +372,10 @@ void afterNodeRemoval(Node