commit 7b7e32ddd435892ce0e18b7d416f81a30793aba9 Author: meowrain Date: Fri Jan 2 00:03:49 2026 +0800 init diff --git a/.github/workflows/del-esa-code.yml b/.github/workflows/del-esa-code.yml new file mode 100644 index 0000000..dd30c1f --- /dev/null +++ b/.github/workflows/del-esa-code.yml @@ -0,0 +1,113 @@ +name: Clean ESA Versions on Main + +on: + push: + branches: [ main ] + +permissions: + contents: read + +jobs: + clean-esa-versions: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + + # 下载阿里云CLI https://help.aliyun.com/zh/cli/ + - name: Download Aliyun Cli + run: | + set -euo pipefail + /bin/bash -c "$(curl -fsSL https://aliyuncli.alicdn.com/install.sh)" + + # 配置阿里云CLI + - name: Configure Aliyun CLI + run: | + aliyun configure set \ + --profile AkProfile \ + --mode AK \ + --access-key-id ${{ secrets.ALIYUN_ACCESS_KEY_ID }} \ + --access-key-secret ${{ secrets.ALIYUN_ACCESS_KEY_SECRET }} \ + --region "cn-hangzhou" + + # 获取Pages名称(可配置) + - name: Get Pages Name + id: get-pages-name + run: | + # 从配置中读取Pages名称,这里使用默认值blog,可根据需要修改 + echo "pages_name=fuwari" >> $GITHUB_OUTPUT + + # 获取所有代码版本并清理旧版本 + - name: Clean ESA Code Versions + env: + PAGES_NAME: ${{ steps.get-pages-name.outputs.pages_name }} + # 配置保留的版本数,默认为1(只保留最新版本) + RETAIN_VERSIONS: ${{ vars.RETAIN_VERSIONS || '1' }} + run: | + set -euo pipefail + + echo "开始清理阿里云ESA版本..." + echo "目标Pages: $PAGES_NAME" + echo "保留版本数: $RETAIN_VERSIONS" + + # 获取所有版本信息 + VERSIONS_JSON=$(aliyun esa ListRoutineCodeVersions --region cn-hangzhou --Name $PAGES_NAME) + + # 解析版本数量 + TOTAL_COUNT=$(echo $VERSIONS_JSON | jq -r '.TotalCount') + echo "当前共有 $TOTAL_COUNT 个版本" + + # 如果版本数不超过保留数量,不需要清理 + if [ "$TOTAL_COUNT" -le "$RETAIN_VERSIONS" ]; then + echo "版本数 ($TOTAL_COUNT) 未超过保留数量 ($RETAIN_VERSIONS),无需清理" + echo "ESA会自动检测main分支更新并部署" + exit 0 + fi + + # 计算需要删除的版本数(保留指定数量的最新版本) + DELETE_COUNT=$((TOTAL_COUNT - RETAIN_VERSIONS)) + echo "需要删除 $DELETE_COUNT 个旧版本,保留最新的 $RETAIN_VERSIONS 个版本" + + # 解析版本列表,按创建时间排序(最早的在前) + VERSIONS_TO_DELETE=$(echo $VERSIONS_JSON | jq -r '.CodeVersions | sort_by(.CreateTime) | .[0:'$DELETE_COUNT'] | .[].CodeVersion') + + echo "将要删除的版本: $VERSIONS_TO_DELETE" + + # 删除旧版本 + DELETED_COUNT=0 + for VERSION in $VERSIONS_TO_DELETE; do + echo "正在删除版本: $VERSION" + DELETE_RESULT=$(aliyun esa DeleteRoutineCodeVersion --region cn-hangzhou --Name $PAGES_NAME --CodeVersion $VERSION) + STATUS=$(echo $DELETE_RESULT | jq -r '.Status') + + if [ "$STATUS" = "OK" ]; then + echo "版本 $VERSION 删除成功" + DELETED_COUNT=$((DELETED_COUNT + 1)) + else + echo "版本 $VERSION 删除失败: $DELETE_RESULT" + exit 1 + fi + done + + echo "版本清理完成!成功删除 $DELETED_COUNT 个旧版本" + + # 验证清理结果 + - name: Verify Clean Result + env: + PAGES_NAME: ${{ steps.get-pages-name.outputs.pages_name }} + RETAIN_VERSIONS: ${{ vars.RETAIN_VERSIONS || '1' }} + run: | + set -euo pipefail + + echo "验证清理结果..." + RESULT_JSON=$(aliyun esa ListRoutineCodeVersions --region cn-hangzhou --Name $PAGES_NAME) + REMAINING_COUNT=$(echo $RESULT_JSON | jq -r '.TotalCount') + + echo "清理后剩余版本数: $REMAINING_COUNT" + + if [ "$REMAINING_COUNT" -le "$RETAIN_VERSIONS" ]; then + echo "版本清理成功,当前版本数: $REMAINING_COUNT (保留设置: $RETAIN_VERSIONS)" + echo "ESA会自动检测main分支更新并开始部署" + else + echo "版本清理失败,仍有 $REMAINING_COUNT 个版本,期望保留: $RETAIN_VERSIONS" + exit 1 + fi \ No newline at end of file diff --git a/.github/workflows/deploy.yml-nouse b/.github/workflows/deploy.yml-nouse new file mode 100644 index 0000000..26cecd2 --- /dev/null +++ b/.github/workflows/deploy.yml-nouse @@ -0,0 +1,27 @@ +name: Build and Deploy +on: + push: + branches: + - main +jobs: + build-and-deploy: + runs-on: ubuntu-latest + permissions: + contents: write + steps: + - name: Checkout + uses: actions/checkout@v3 + + - name: Install Dependencies + run: npm install + + - name: Build + run: npm run build + + - name: Deploy to page branch + uses: peaceiris/actions-gh-pages@v3 + with: + github_token: ${{ secrets.GITHUB_TOKEN }} + publish_dir: ./dist + publish_branch: page + cname: ${{ secrets.CNAME }} \ No newline at end of file diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..dbee63a --- /dev/null +++ b/.gitignore @@ -0,0 +1,32 @@ +# build output +dist/ + +# generated types +.astro/ + +# dependencies +node_modules/ + +# logs +npm-debug.log* +yarn-debug.log* +yarn-error.log* +pnpm-debug.log* + +# environment variables +.env +.env.production + +# macOS-specific files +.DS_Store + +.vercel + +package-lock.json +bun.lockb +yarn.lock + +.vscode + +# 忽略上游仓库 +ori diff --git a/.npmrc b/.npmrc new file mode 100644 index 0000000..6baea02 --- /dev/null +++ b/.npmrc @@ -0,0 +1 @@ +manage-package-manager-versions = true \ No newline at end of file diff --git a/.obsidian/app.json b/.obsidian/app.json new file mode 100644 index 0000000..9e26dfe --- /dev/null +++ b/.obsidian/app.json @@ -0,0 +1 @@ +{} \ No newline at end of file diff --git a/.obsidian/appearance.json b/.obsidian/appearance.json new file mode 100644 index 0000000..9e26dfe --- /dev/null +++ b/.obsidian/appearance.json @@ -0,0 +1 @@ +{} \ No newline at end of file diff --git a/.obsidian/core-plugins.json b/.obsidian/core-plugins.json new file mode 100644 index 0000000..3f2ab43 --- /dev/null +++ b/.obsidian/core-plugins.json @@ -0,0 +1,32 @@ +{ + "file-explorer": true, + "global-search": true, + "switcher": true, + "graph": true, + "backlink": true, + "canvas": true, + "outgoing-link": true, + "tag-pane": true, + "footnotes": false, + "properties": true, + "page-preview": true, + "daily-notes": true, + "templates": true, + "note-composer": true, + "command-palette": true, + "slash-command": false, + "editor-status": true, + "bookmarks": true, + "markdown-importer": false, + "zk-prefixer": false, + "random-note": false, + "outline": true, + "word-count": true, + "slides": false, + "audio-recorder": false, + "workspaces": false, + "file-recovery": true, + "publish": false, + "sync": true, + "bases": true +} \ No newline at end of file diff --git a/.obsidian/workspace-mobile.json b/.obsidian/workspace-mobile.json new file mode 100644 index 0000000..e9bed91 --- /dev/null +++ b/.obsidian/workspace-mobile.json @@ -0,0 +1,166 @@ +{ + "main": { + "id": "29fed73056d4c788", + "type": "split", + "children": [ + { + "id": "fa71f00e822e8432", + "type": "tabs", + "children": [ + { + "id": "ca9029e77020d44e", + "type": "leaf", + "state": { + "type": "empty", + "state": {}, + "icon": "lucide-file", + "title": "New tab" + } + } + ] + } + ], + "direction": "vertical" + }, + "left": { + "id": "c058602dcce78a26", + "type": "mobile-drawer", + "children": [ + { + "id": "d3fd5617bfecca06", + "type": "leaf", + "state": { + "type": "file-explorer", + "state": { + "sortOrder": "alphabetical", + "autoReveal": false + }, + "icon": "lucide-folder-closed", + "title": "Files" + } + }, + { + "id": "4d4379f2270ea3e0", + "type": "leaf", + "state": { + "type": "search", + "state": { + "query": "", + "matchingCase": false, + "explainSearch": false, + "collapseAll": false, + "extraContext": false, + "sortOrder": "alphabetical" + }, + "icon": "lucide-search", + "title": "Search" + } + }, + { + "id": "d99abf483fcdcb1a", + "type": "leaf", + "state": { + "type": "tag", + "state": { + "sortOrder": "frequency", + "useHierarchy": true, + "showSearch": false, + "searchQuery": "" + }, + "icon": "lucide-tags", + "title": "Tags" + } + }, + { + "id": "e546445f9f92dc38", + "type": "leaf", + "state": { + "type": "all-properties", + "state": { + "sortOrder": "frequency", + "showSearch": false, + "searchQuery": "" + }, + "icon": "lucide-archive", + "title": "All properties" + } + }, + { + "id": "e6c7e20438cddc79", + "type": "leaf", + "state": { + "type": "bookmarks", + "state": {}, + "icon": "lucide-bookmark", + "title": "Bookmarks" + } + } + ], + "currentTab": 0 + }, + "right": { + "id": "86a48de8a457f6a9", + "type": "mobile-drawer", + "children": [ + { + "id": "35be497b23adb2a9", + "type": "leaf", + "state": { + "type": "backlink", + "state": { + "collapseAll": false, + "extraContext": false, + "sortOrder": "alphabetical", + "showSearch": false, + "searchQuery": "", + "backlinkCollapsed": false, + "unlinkedCollapsed": true + }, + "icon": "links-coming-in", + "title": "Backlinks" + } + }, + { + "id": "612fb695f1f31b4f", + "type": "leaf", + "state": { + "type": "outgoing-link", + "state": { + "linksCollapsed": false, + "unlinkedCollapsed": true + }, + "icon": "links-going-out", + "title": "Outgoing links" + } + }, + { + "id": "1d0fadefe7de7b83", + "type": "leaf", + "state": { + "type": "outline", + "state": { + "followCursor": false, + "showSearch": false, + "searchQuery": "" + }, + "icon": "lucide-list", + "title": "Outline" + } + } + ], + "currentTab": 0 + }, + "left-ribbon": { + "hiddenItems": { + "switcher:Open quick switcher": false, + "graph:Open graph view": false, + "canvas:Create new canvas": false, + "daily-notes:Open today's daily note": false, + "templates:Insert template": false, + "command-palette:Open command palette": false, + "bases:Create new base": false + } + }, + "active": "ca9029e77020d44e", + "lastOpenFiles": [] +} \ No newline at end of file diff --git a/CLAUDE.md b/CLAUDE.md new file mode 100644 index 0000000..22b8ebe --- /dev/null +++ b/CLAUDE.md @@ -0,0 +1,136 @@ +# CLAUDE.md + +This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository. + +## Project Overview + +**Fuwari** is a personal blog theme built with Astro 5.7, customized for AcoFork. It's a static site generator focused on technical blogging with Chinese language support. + +## Common Commands + +```bash +# Install dependencies (uses pnpm) +pnpm install + +# Development server +pnpm dev # or pnpm start + +# Build for production +pnpm build + +# Preview production build +pnpm preview + +# Type checking +pnpm type-check + +# Create new blog post (creates file in src/content/posts/) +pnpm new-post + +# Clean unused images from src/content/assets/ +pnpm clean + +# Format code (uses Biome) +pnpm format + +# Lint and fix code (uses Biome) +pnpm lint +``` + +## Architecture + +### Framework & Build System +- **Astro 5.7** - Static site generator with component islands architecture +- **Vite** - Build tool (configured via astro.config.mjs) +- **Output**: Static HTML/CSS/JS to `dist/` directory + +### Styling Architecture +- **Tailwind CSS** - Utility-first CSS with Nesting plugin +- **Stylus** - Preprocessor for global styles and CSS variables +- **Theme System**: HSL-based color system with configurable hue in `src/config.ts` + - Uses CSS custom properties defined in `src/styles/variables.styl` + - Dark/light mode toggle via `data-theme` attribute + +### Component Architecture +- **Svelte 5** - Interactive components (`.svelte` files in `src/components/`) +- **Astro components** - Static components (`.astro` files) +- **Layout hierarchy**: `Layout.astro` (root) → page-specific layouts + +### Content System +- **Blog posts**: `src/content/posts/*.md` with frontmatter schema validation +- **Assets**: `src/content/assets/` for images/media +- **Content collections**: Defined in Astro config with TypeScript schemas + +### Plugin System (src/plugins/) +Custom remark/rehype plugins for Markdown processing: +- `remark-reading-time.mjs` - Calculates reading time +- `remark-excerpt.js` - Generates post excerpts +- `remark-directive-rehype.js` - Parses custom directives +- `rehype-component-admonition.mjs` - GitHub-style callouts (note, tip, warning, etc.) +- `rehype-component-github-card.mjs` - GitHub repo embedding +- `rehype-image-fallback.mjs` - Image fallback on failure +- `expressive-code/custom-copy-button.ts` - Custom code block copy button + +## Configuration Files + +- **`src/config.ts`** - Main site configuration + - Site metadata (title, description, lang) + - Theme colors (hue, dark mode) + - Navigation links + - Profile info + - Banner/background settings + - License, analytics, edit links + +- **`astro.config.mjs`** - Build configuration + - Integrations (Tailwind, Svelte, Swup, Sitemap, Icon) + - Markdown remark/rehype plugins pipeline + - Expressive Code settings + - Redirects + - Image service (passthrough mode) + +- **`tailwind.config.cjs`** - Tailwind configuration + - Dark mode (`class` strategy) + - Typography plugin + - Font family extensions + +## Key Integrations + +- **Swup** - Page transitions with SPA-like navigation (configured containers: `main`, `#toc`) +- **astro-icon** - Icon system with multiple icon sets (FontAwesome, Simple Icons, Material Symbols) +- **@astrojs/sitemap** - Automatic sitemap generation +- **Expressive Code** - Code highlighting with GitHub dark theme, collapsible sections, line numbers + +## Code Quality + +- **Biome** - Primary linter and formatter (replaces ESLint/Prettier) +- **TypeScript** - Strict mode enabled, path aliases configured +- **Type checking**: `pnpm type-check` uses `tsc --noEmit --isolatedDeclarations` + +## Content Frontmatter Schema + +Posts use this frontmatter format: +```yaml +--- +title: Post Title +published: 2024-01-01T12:00:00 +description: Post description +image: ./cover.jpg # Relative to post +tags: [tag1, tag2] +category: Category +draft: false +lang: zh_CN # Optional, overrides site default +--- +``` + +## Custom Markdown Features + +- **GitHub Admonitions**: `> [!NOTE]`, `> [!WARNING]`, etc. +- **Math**: KaTeX support via `$...$` (inline) and `$$...$$` (block) +- **Custom directives**: `:::github[repo](url)` for repo cards +- **Auto TOC**: Generated from headings (configurable depth in `siteConfig.toc.depth`) + +## Deployment + +- **Static site**: Builds to `dist/` directory +- **CI/CD**: GitHub Actions for Alibaba Cloud ESA deployment +- **Redirects**: Configured in `astro.config.mjs` redirects section diff --git a/LICENSE b/LICENSE new file mode 100644 index 0000000..08fa51c --- /dev/null +++ b/LICENSE @@ -0,0 +1,21 @@ +MIT License + +Copyright (c) 2024 saicaca + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/README.md b/README.md new file mode 100644 index 0000000..f6fd75c --- /dev/null +++ b/README.md @@ -0,0 +1,179 @@ +# Fuwari For AcoFork + +> [!CAUTION] +> 该仓库由 AcoFork 深度定制,并包含了最新的文章,如果你想以此为模板进行二改,需要一定的动手能力。 + +<img width="1858" height="948" alt="image" src="https://github.com/user-attachments/assets/55c2c63b-0dac-436e-aaa0-451ad2dfb65a" /> + + +一个基于 Astro 构建的现代化个人博客主题,专注于技术分享与实践。 + +## ✨ 特性 + +- 🚀 基于 Astro 4.0+ 构建,性能卓越 +- 📱 完全响应式设计,支持移动端 +- 🌙 支持深色/浅色主题切换 +- 📝 支持 Markdown 和 MDX 格式 +- 🔍 内置搜索功能 +- 📊 文章阅读时间统计 +- 🏷️ 标签和分类系统 +- 📈 SEO 优化 +- 🎨 可自定义配置 +- 💬 评论系统支持 +- 📡 RSS 订阅支持 + +## 🛠️ 技术栈 + +- **框架**: Astro +- **样式**: Tailwind CSS + Stylus +- **交互**: Svelte +- **构建工具**: Vite +- **包管理**: pnpm +- **代码规范**: Biome + +## 🚀 快速开始 + +### 环境要求 + +- Node.js 18+ +- pnpm + +### 安装依赖 + +```bash +pnpm install +``` + +### 开发模式 + +```bash +pnpm dev +``` + +### 构建生产版本 + +```bash +pnpm build +``` + +### 预览构建结果 + +```bash +pnpm preview +``` + +## 📝 使用指南 + +### 创建新文章 + +使用内置脚本快速创建新文章: + +```bash +pnpm new-post helloword +``` + +### 清理未使用的图片 + +清理 `src/content/assets` 目录下未被引用的图片文件: + +```bash +pnpm clean +``` + +### 配置博客 + +编辑 `src/config.ts` 文件来自定义博客配置: + +```typescript +export const siteConfig: SiteConfig = { + title: "Fuwari", + subtitle: "技术分享与实践", + lang: "zh_CN", + themeColor: { + hue: 250, + fixed: false, + }, + banner: { + enable: false, + src: "assets/images/demo-banner.png", + position: "center", + }, + favicon: [ + { + src: "/favicon/icon.png", + } + ] +} +``` + +### 文章格式 + +文章使用 Markdown 格式,支持 frontmatter: + +```markdown +--- +title: 文章标题 +published: 2024-01-01 +description: 文章描述 +image: ./cover.jpg +tags: [标签1, 标签2] +category: 分类 +draft: false +--- + +# 文章内容 + +这里是文章正文... +``` + +## 📁 项目结构 + +``` +├── public/ # 静态资源 +├── src/ +│ ├── components/ # 组件 +│ ├── content/ # 内容 +│ │ ├── posts/ # 博客文章 +│ │ └── assets/ # 资源文件 +│ ├── layouts/ # 布局 +│ ├── pages/ # 页面 +│ ├── styles/ # 样式 +│ └── config.ts # 配置文件 +├── scripts/ # 脚本工具 +└── package.json +``` + +## 🎨 自定义 + +### 主题颜色 + +在 `src/config.ts` 中修改 `themeColor` 配置: + +```typescript +themeColor: { + hue: 250, // 主色调 (0-360) + fixed: false, // 是否固定颜色 +} +``` + +### 样式定制 + +- 全局样式:`src/styles/main.css` +- Markdown 样式:`src/styles/markdown.css` +- 变量定义:`src/styles/variables.styl` + +## 📦 部署 + +构建后的静态文件位于 `dist/` 目录,可部署到任何静态托管平台。 + +## 🤝 贡献 + +欢迎提交 Issue 和 Pull Request! + +## 📄 许可证 + +[MIT License](LICENSE) + +## 🙏 致谢 + +感谢所有为这个项目做出贡献的开发者们!尤其感谢[上游仓库](https://github.com/saicaca/fuwari) diff --git a/astro.config.mjs b/astro.config.mjs new file mode 100644 index 0000000..8d57fc4 --- /dev/null +++ b/astro.config.mjs @@ -0,0 +1,212 @@ +import sitemap from "@astrojs/sitemap"; +import svelte from "@astrojs/svelte"; +import tailwind from "@astrojs/tailwind"; +import swup from "@swup/astro"; +import icon from "astro-icon"; +import { defineConfig } from "astro/config"; +import rehypeAutolinkHeadings from "rehype-autolink-headings"; +import rehypeComponents from "rehype-components"; /* Render the custom directive content */ +import rehypeKatex from "rehype-katex"; +import rehypeSlug from "rehype-slug"; +import remarkDirective from "remark-directive"; /* Handle directives */ +import remarkGithubAdmonitionsToDirectives from "remark-github-admonitions-to-directives"; +import remarkMath from "remark-math"; +import remarkSectionize from "remark-sectionize"; +import { imageFallbackConfig, siteConfig } from "./src/config.ts"; +import { AdmonitionComponent } from "./src/plugins/rehype-component-admonition.mjs"; +import { GithubCardComponent } from "./src/plugins/rehype-component-github-card.mjs"; +import rehypeImageFallback from "./src/plugins/rehype-image-fallback.mjs"; +import { parseDirectiveNode } from "./src/plugins/remark-directive-rehype.js"; +import { remarkExcerpt } from "./src/plugins/remark-excerpt.js"; +import { remarkReadingTime } from "./src/plugins/remark-reading-time.mjs"; +import rehypeExternalLinks from "rehype-external-links"; +import expressiveCode from "astro-expressive-code"; +import { pluginCollapsibleSections } from "@expressive-code/plugin-collapsible-sections"; +import { pluginLineNumbers } from "@expressive-code/plugin-line-numbers"; +import { expressiveCodeConfig } from "./src/config.ts"; +// import { pluginLanguageBadge } from "./src/plugins/expressive-code/language-badge.ts"; +import { pluginCustomCopyButton } from "./src/plugins/expressive-code/custom-copy-button.js"; +import { defineConfig, passthroughImageService } from "astro/config"; + +// https://astro.build/config +export default defineConfig({ + server: { + port: 25544, + }, + image: { + service: passthroughImageService(), + }, + site: "https://blog.acofork.com", + base: "/", + trailingSlash: "always", + output: "static", + redirects: { + "/donate": "/sponsors", + "/ak": + "https://akile.io/register?aff_code=503fe5ea-e7c5-4d68-ae05-6de99513680e", + "/kook": "https://kook.vip/K29zpT", + "/long": + "https://iiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiii.iiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiii.in/", + "/mly": "https://muleyun.com/aff/GOTRJLPN", + "/tg": "https://t.me/+_07DERp7k1ljYTc1", + "/tit": "/posts/pin/", + "/tly": "https://tianlicloud.cn/aff/HNNCFKGP", + "/wly": "https://wl.awcmam.com/#/register?code=FNQwOQBM", + "/yyb": "https://www.rainyun.com/acofork_?s=bilibili", + "/iku": "https://ikuuu.de/auth/register?code=Bjou", + "/esa": + "https://tianchi.aliyun.com/specials/promotion/freetier/esa?taskCode=25254&recordId=c856e61228828a0423417a767828d166", + }, + integrations: [ + tailwind({ + nesting: true, + }), + swup({ + theme: false, + animationClass: "transition-swup-", // see https://swup.js.org/options/#animationselector + // the default value `transition-` cause transition delay + // when the Tailwind class `transition-all` is used + containers: ["main", "#toc"], + smoothScrolling: true, + cache: true, + preload: true, + accessibility: true, + updateHead: true, + updateBodyClass: false, + globalInstance: true, + }), + icon({ + include: { + "preprocess: vitePreprocess(),": ["*"], + "fa6-brands": ["*"], + "fa6-regular": ["*"], + "fa6-solid": ["*"], + "simple-icons": ["*"], + "material-symbols-light": ["*"], + "material-symbols": ["*"], + }, + }), + svelte(), + sitemap(), + expressiveCode({ + themes: [expressiveCodeConfig.theme, expressiveCodeConfig.theme], + plugins: [ + pluginCollapsibleSections(), + pluginLineNumbers(), + // pluginLanguageBadge(), + pluginCustomCopyButton(), + ], + defaultProps: { + wrap: true, + overridesByLang: { + shellsession: { + showLineNumbers: false, + }, + }, + }, + styleOverrides: { + codeBackground: "var(--codeblock-bg)", + borderRadius: "0.25rem", + borderColor: "none", + codeFontSize: "0.875rem", + codeFontFamily: + "'JetBrains Mono Variable', ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, 'Liberation Mono', 'Courier New', monospace", + codeLineHeight: "1.5rem", + frames: { + editorBackground: "var(--codeblock-bg)", + terminalBackground: "var(--codeblock-bg)", + terminalTitlebarBackground: "var(--codeblock-topbar-bg)", + editorTabBarBackground: "var(--codeblock-topbar-bg)", + editorActiveTabBackground: "none", + editorActiveTabIndicatorBottomColor: "var(--primary)", + editorActiveTabIndicatorTopColor: "none", + editorTabBarBorderBottomColor: "var(--codeblock-topbar-bg)", + terminalTitlebarBorderBottomColor: "none", + }, + textMarkers: { + delHue: 0, + insHue: 180, + markHue: 250, + }, + }, + frames: { + showCopyToClipboardButton: false, + }, + }), + ], + markdown: { + remarkPlugins: [ + remarkMath, + remarkReadingTime, + remarkExcerpt, + remarkGithubAdmonitionsToDirectives, + remarkDirective, + remarkSectionize, + parseDirectiveNode, + ], + rehypePlugins: [ + rehypeKatex, + rehypeSlug, + [rehypeImageFallback, imageFallbackConfig], + [ + rehypeComponents, + { + components: { + github: GithubCardComponent, + note: (x, y) => AdmonitionComponent(x, y, "note"), + tip: (x, y) => AdmonitionComponent(x, y, "tip"), + important: (x, y) => AdmonitionComponent(x, y, "important"), + caution: (x, y) => AdmonitionComponent(x, y, "caution"), + warning: (x, y) => AdmonitionComponent(x, y, "warning"), + }, + }, + ], + [ + rehypeExternalLinks, + { + target: "_blank", + }, + ], + [ + rehypeAutolinkHeadings, + { + behavior: "append", + properties: { + className: ["anchor"], + }, + content: { + type: "element", + tagName: "span", + properties: { + className: ["anchor-icon"], + "data-pagefind-ignore": true, + }, + children: [ + { + type: "text", + value: "#", + }, + ], + }, + }, + ], + ], + }, + vite: { + assetsInclude: ["**/*.awebp"], + build: { + rollupOptions: { + onwarn(warning, warn) { + // temporarily suppress this warning + if ( + warning.message.includes("is dynamically imported by") && + warning.message.includes("but also statically imported by") + ) { + return; + } + warn(warning); + }, + }, + }, + }, +}); diff --git a/biome.json b/biome.json new file mode 100644 index 0000000..4ccad49 --- /dev/null +++ b/biome.json @@ -0,0 +1,43 @@ +{ + "$schema": "https://biomejs.dev/schemas/1.9.4/schema.json", + "vcs": { + "enabled": false, + "clientKind": "git", + "useIgnoreFile": false + }, + "files": { + "ignoreUnknown": false, + "ignore": ["src/**/*.css","src/public/**/*", "dist/**/*", "node_modules/**/*"] + }, + "formatter": { + "enabled": true, + "indentStyle": "tab" + }, + "organizeImports": { + "enabled": true + }, + "linter": { + "enabled": true, + "rules": { + "recommended": true + } + }, + "javascript": { + "formatter": { + "quoteStyle": "double" + } + }, + "overrides": [ + { + "include": ["*.svelte", "*.astro", "*.vue"], + "linter": { + "rules": { + "style": { + "useConst": "off", + "useImportType": "off" + } + } + } + } + ] +} diff --git a/docker-compose.yml b/docker-compose.yml new file mode 100644 index 0000000..6a6ff5f --- /dev/null +++ b/docker-compose.yml @@ -0,0 +1,16 @@ +version: '3.3' +services: + easyimage: + image: ddsderek/easyimage:latest + container_name: easyimage + ports: + - '8087:80' + environment: + - TZ=Asia/Shanghai + - PUID=1001 + - PGID=1001 + - DEBUG=false + volumes: + - './public/config:/app/web/config' + - './public/api/i:/app/web/i' + restart: unless-stopped diff --git a/edgeone.json b/edgeone.json new file mode 100644 index 0000000..a14c92b --- /dev/null +++ b/edgeone.json @@ -0,0 +1,59 @@ +{ + "redirects": [ + { + "source": "/ak", + "destination": "https://akile.io/register?aff_code=503fe5ea-e7c5-4d68-ae05-6de99513680e", + "statusCode": 302 + }, + { + "source": "/kook", + "destination": "https://kook.vip/K29zpT", + "statusCode": 302 + }, + { + "source": "/long", + "destination": "https://iiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiii.iiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiii.in/", + "statusCode": 302 + }, + { + "source": "/mly", + "destination": "https://muleyun.com/aff/GOTRJLPN", + "statusCode": 302 + }, + { + "source": "/tg", + "destination": "https://t.me/+_07DERp7k1ljYTc1", + "statusCode": 302 + }, + { + "source": "/tit", + "destination": "/posts/pin/", + "statusCode": 302 + }, + { + "source": "/tly", + "destination": "https://tianlicloud.cn/aff/HNNCFKGP", + "statusCode": 302 + }, + { + "source": "/wly", + "destination": "https://wl.awcmam.com/#/register?code=FNQwOQBM", + "statusCode": 302 + }, + { + "source": "/yyb", + "destination": "https://www.rainyun.com/acofork_?s=bilibili", + "statusCode": 302 + }, + { + "source": "/iku", + "destination": "https://ikuuu.de/auth/register?code=Bjou", + "statusCode": 302 + }, + { + "source": "/esa", + "destination": "https://tianchi.aliyun.com/specials/promotion/freetier/esa?taskCode=25254&recordId=c856e61228828a0423417a767828d166", + "statusCode": 302 + } + ] +} \ No newline at end of file diff --git a/frontmatter.json b/frontmatter.json new file mode 100644 index 0000000..ac17e0a --- /dev/null +++ b/frontmatter.json @@ -0,0 +1,62 @@ +{ + "$schema": "https://frontmatter.codes/frontmatter.schema.json", + "frontMatter.framework.id": "astro", + "frontMatter.preview.host": "http://localhost:4321", + "frontMatter.content.publicFolder": "public", + "frontMatter.content.pageFolders": [ + { + "title": "posts", + "path": "[[workspace]]/src/content/posts" + } + ], + "frontMatter.taxonomy.contentTypes": [ + { + "name": "default", + "pageBundle": true, + "previewPath": "'blog'", + "filePrefix": null, + "clearEmpty": true, + "fields": [ + { + "title": "title", + "name": "title", + "type": "string", + "single": true + }, + { + "title": "description", + "name": "description", + "type": "string" + }, + { + "title": "published", + "name": "published", + "type": "datetime", + "default": "{{now}}", + "isPublishDate": true + }, + { + "title": "preview", + "name": "image", + "type": "image", + "isPreviewImage": true + }, + { + "title": "tags", + "name": "tags", + "type": "list" + }, + { + "title": "draft", + "name": "draft", + "type": "boolean" + }, + { + "title": "language", + "name": "language", + "type": "string" + } + ] + } + ] +} diff --git a/package.json b/package.json new file mode 100644 index 0000000..9bc6908 --- /dev/null +++ b/package.json @@ -0,0 +1,88 @@ +{ + "name": "fuwari", + "type": "module", + "version": "0.0.1", + "scripts": { + "dev": "astro dev", + "start": "astro dev", + "prebuild": "node scripts/generate-gallery-index.js", + "build": "astro build", + "preview": "astro preview", + "astro": "astro", + "type-check": "tsc --noEmit --isolatedDeclarations", + "new-post": "node scripts/new-post.js", + "clean": "node scripts/clean-unused-images.js", + "format": "biome format --write ./src", + "lint": "biome check --write ./src" + }, + "dependencies": { + "@astrojs/check": "^0.9.4", + "@astrojs/rss": "^4.0.11", + "@astrojs/sitemap": "^3.3.1", + "@astrojs/svelte": "7.0.12", + "@astrojs/tailwind": "^6.0.2", + "@astrojs/vercel": "^8.2.7", + "@expressive-code/core": "^0.41.3", + "@expressive-code/plugin-collapsible-sections": "^0.41.3", + "@expressive-code/plugin-line-numbers": "^0.41.3", + "@fancyapps/ui": "^6.0.5", + "@fontsource-variable/jetbrains-mono": "^5.2.5", + "@fontsource/roboto": "^5.2.5", + "@iconify-json/fa6-brands": "^1.2.5", + "@iconify-json/fa6-regular": "^1.2.3", + "@iconify-json/fa6-solid": "^1.2.3", + "@iconify-json/material-symbols": "^1.2.20", + "@iconify-json/simple-icons": "^1.2.42", + "@iconify/svelte": "^4.2.0", + "@swup/astro": "^1.6.0", + "@tailwindcss/typography": "^0.5.16", + "@vercel/analytics": "^1.5.0", + "astro": "5.7.9", + "astro-expressive-code": "^0.41.3", + "astro-icon": "^1.1.5", + "glob": "^11.0.3", + "hastscript": "^9.0.1", + "katex": "^0.16.22", + "markdown-it": "^14.1.0", + "mdast-util-to-string": "^4.0.0", + "node-html-parser": "^7.0.1", + "overlayscrollbars": "^2.11.1", + "photoswipe": "^5.4.4", + "reading-time": "^1.5.0", + "rehype-autolink-headings": "^7.1.0", + "rehype-components": "^0.3.0", + "rehype-external-links": "^3.0.0", + "rehype-katex": "^7.0.1", + "rehype-slug": "^6.0.0", + "remark-directive": "^3.0.1", + "remark-directive-rehype": "^0.4.2", + "remark-github-admonitions-to-directives": "^1.0.5", + "remark-math": "^6.0.0", + "remark-sectionize": "^2.1.0", + "sanitize-html": "^2.16.0", + "sharp": "^0.34.1", + "stylus": "^0.64.0", + "svelte": "^5.28.2", + "tailwindcss": "^3.4.17", + "typescript": "^5.8.3", + "unist-util-visit": "^5.0.0" + }, + "devDependencies": { + "@astrojs/ts-plugin": "^1.10.4", + "@biomejs/biome": "1.9.4", + "@iconify-json/material-symbols-light": "^1.2.49", + "@rollup/plugin-yaml": "^4.1.2", + "@types/markdown-it": "^14.1.2", + "@types/mdast": "^4.0.4", + "@types/sanitize-html": "^2.15.0", + "postcss-import": "^16.1.0", + "postcss-nesting": "^13.0.1", + "rehype": "^13.0.2" + }, + "packageManager": "pnpm@9.14.4", + "pnpm": { + "patchedDependencies": { + "astro": "patches/astro.patch" + } + } +} diff --git a/patches/astro.patch b/patches/astro.patch new file mode 100644 index 0000000..01c219f --- /dev/null +++ b/patches/astro.patch @@ -0,0 +1,29 @@ +diff --git a/dist/assets/utils/transformToPath.js b/dist/assets/utils/transformToPath.js +index cca8548dec42090b0621d1f21c86f503d5bba1be..8b0a3cfcea73abc4d63592709bb9ba2b2f83989a 100644 +--- a/dist/assets/utils/transformToPath.js ++++ b/dist/assets/utils/transformToPath.js +@@ -13,7 +13,9 @@ function propsToFilename(filePath, transform, hash) { + } + const prefixDirname = isESMImportedImage(transform.src) ? dirname(filePath) : ""; + let outputExt = transform.format ? `.${transform.format}` : ext; +- return decodeURIComponent(`${prefixDirname}/${filename}_${hash}${outputExt}`); ++ ++ // Force disable image optimization - return original path without hash and format conversion ++ return decodeURIComponent(`${prefixDirname}/${filename}${ext}`); + } + function hashTransform(transform, imageService, propertiesToHash) { + const hashFields = propertiesToHash.reduce( +diff --git a/dist/core/build/generate.js b/dist/core/build/generate.js +index 3144f4c058b161b9e6eb3c8d891b743b34783653..0ba275b320204e154307c6aff75452e9dcb2300d 100644 +--- a/dist/core/build/generate.js ++++ b/dist/core/build/generate.js +@@ -91,7 +91,8 @@ ${bgGreen(black(` ${verb} static routes `))}`); + `) + ); + const staticImageList = getStaticImageList(); +- if (staticImageList.size) { ++ // Force disable image optimization - hardcoded ++ if (false) { + logger.info("SKIP_FORMAT", `${bgGreen(black(` generating optimized images `))}`); + const totalCount = Array.from(staticImageList.values()).map((x) => x.transforms.size).reduce((a, b) => a + b, 0); + const cpuCount = os.cpus().length; diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml new file mode 100644 index 0000000..ad553e4 --- /dev/null +++ b/pnpm-lock.yaml @@ -0,0 +1,11643 @@ +lockfileVersion: '9.0' + +settings: + autoInstallPeers: true + excludeLinksFromLockfile: false + +patchedDependencies: + astro: + hash: ysrllw3gj6hylybllnynya5oma + path: patches/astro.patch + +importers: + + .: + dependencies: + '@astrojs/check': + specifier: ^0.9.4 + version: 0.9.4(typescript@5.8.3) + '@astrojs/rss': + specifier: ^4.0.11 + version: 4.0.11 + '@astrojs/sitemap': + specifier: ^3.3.1 + version: 3.3.1 + '@astrojs/svelte': + specifier: 7.0.12 + version: 7.0.12(@types/node@22.14.1)(astro@5.7.9(patch_hash=ysrllw3gj6hylybllnynya5oma)(@types/node@22.14.1)(jiti@1.21.7)(lightningcss@1.29.3)(rollup@2.79.2)(sass@1.80.4)(stylus@0.64.0)(terser@5.39.0)(typescript@5.8.3)(yaml@2.7.0))(jiti@1.21.7)(lightningcss@1.29.3)(sass@1.80.4)(stylus@0.64.0)(svelte@5.28.2)(terser@5.39.0)(typescript@5.8.3)(yaml@2.7.0) + '@astrojs/tailwind': + specifier: ^6.0.2 + version: 6.0.2(astro@5.7.9(patch_hash=ysrllw3gj6hylybllnynya5oma)(@types/node@22.14.1)(jiti@1.21.7)(lightningcss@1.29.3)(rollup@2.79.2)(sass@1.80.4)(stylus@0.64.0)(terser@5.39.0)(typescript@5.8.3)(yaml@2.7.0))(tailwindcss@3.4.17) + '@astrojs/vercel': + specifier: ^8.2.7 + version: 8.2.7(astro@5.7.9(patch_hash=ysrllw3gj6hylybllnynya5oma)(@types/node@22.14.1)(jiti@1.21.7)(lightningcss@1.29.3)(rollup@2.79.2)(sass@1.80.4)(stylus@0.64.0)(terser@5.39.0)(typescript@5.8.3)(yaml@2.7.0))(rollup@2.79.2)(svelte@5.28.2) + '@expressive-code/core': + specifier: ^0.41.3 + version: 0.41.3 + '@expressive-code/plugin-collapsible-sections': + specifier: ^0.41.3 + version: 0.41.3 + '@expressive-code/plugin-line-numbers': + specifier: ^0.41.3 + version: 0.41.3 + '@fancyapps/ui': + specifier: ^6.0.5 + version: 6.0.5 + '@fontsource-variable/jetbrains-mono': + specifier: ^5.2.5 + version: 5.2.5 + '@fontsource/roboto': + specifier: ^5.2.5 + version: 5.2.5 + '@iconify-json/fa6-brands': + specifier: ^1.2.5 + version: 1.2.5 + '@iconify-json/fa6-regular': + specifier: ^1.2.3 + version: 1.2.3 + '@iconify-json/fa6-solid': + specifier: ^1.2.3 + version: 1.2.3 + '@iconify-json/material-symbols': + specifier: ^1.2.20 + version: 1.2.20 + '@iconify-json/simple-icons': + specifier: ^1.2.42 + version: 1.2.42 + '@iconify/svelte': + specifier: ^4.2.0 + version: 4.2.0(svelte@5.28.2) + '@swup/astro': + specifier: ^1.6.0 + version: 1.6.0(@types/babel__core@7.20.5) + '@tailwindcss/typography': + specifier: ^0.5.16 + version: 0.5.16(tailwindcss@3.4.17) + '@vercel/analytics': + specifier: ^1.5.0 + version: 1.5.0(svelte@5.28.2) + astro: + specifier: 5.7.9 + version: 5.7.9(patch_hash=ysrllw3gj6hylybllnynya5oma)(@types/node@22.14.1)(jiti@1.21.7)(lightningcss@1.29.3)(rollup@2.79.2)(sass@1.80.4)(stylus@0.64.0)(terser@5.39.0)(typescript@5.8.3)(yaml@2.7.0) + astro-expressive-code: + specifier: ^0.41.3 + version: 0.41.3(astro@5.7.9(patch_hash=ysrllw3gj6hylybllnynya5oma)(@types/node@22.14.1)(jiti@1.21.7)(lightningcss@1.29.3)(rollup@2.79.2)(sass@1.80.4)(stylus@0.64.0)(terser@5.39.0)(typescript@5.8.3)(yaml@2.7.0)) + astro-icon: + specifier: ^1.1.5 + version: 1.1.5 + glob: + specifier: ^11.0.3 + version: 11.0.3 + hastscript: + specifier: ^9.0.1 + version: 9.0.1 + katex: + specifier: ^0.16.22 + version: 0.16.22 + markdown-it: + specifier: ^14.1.0 + version: 14.1.0 + mdast-util-to-string: + specifier: ^4.0.0 + version: 4.0.0 + node-html-parser: + specifier: ^7.0.1 + version: 7.0.1 + overlayscrollbars: + specifier: ^2.11.1 + version: 2.11.1 + photoswipe: + specifier: ^5.4.4 + version: 5.4.4 + reading-time: + specifier: ^1.5.0 + version: 1.5.0 + rehype-autolink-headings: + specifier: ^7.1.0 + version: 7.1.0 + rehype-components: + specifier: ^0.3.0 + version: 0.3.0 + rehype-external-links: + specifier: ^3.0.0 + version: 3.0.0 + rehype-katex: + specifier: ^7.0.1 + version: 7.0.1 + rehype-slug: + specifier: ^6.0.0 + version: 6.0.0 + remark-directive: + specifier: ^3.0.1 + version: 3.0.1 + remark-directive-rehype: + specifier: ^0.4.2 + version: 0.4.2 + remark-github-admonitions-to-directives: + specifier: ^1.0.5 + version: 1.0.5 + remark-math: + specifier: ^6.0.0 + version: 6.0.0 + remark-sectionize: + specifier: ^2.1.0 + version: 2.1.0 + sanitize-html: + specifier: ^2.16.0 + version: 2.16.0 + sharp: + specifier: ^0.34.1 + version: 0.34.1 + stylus: + specifier: ^0.64.0 + version: 0.64.0 + svelte: + specifier: ^5.28.2 + version: 5.28.2 + tailwindcss: + specifier: ^3.4.17 + version: 3.4.17 + typescript: + specifier: ^5.8.3 + version: 5.8.3 + unist-util-visit: + specifier: ^5.0.0 + version: 5.0.0 + devDependencies: + '@astrojs/ts-plugin': + specifier: ^1.10.4 + version: 1.10.4 + '@biomejs/biome': + specifier: 1.9.4 + version: 1.9.4 + '@iconify-json/material-symbols-light': + specifier: ^1.2.49 + version: 1.2.49 + '@rollup/plugin-yaml': + specifier: ^4.1.2 + version: 4.1.2(rollup@2.79.2) + '@types/markdown-it': + specifier: ^14.1.2 + version: 14.1.2 + '@types/mdast': + specifier: ^4.0.4 + version: 4.0.4 + '@types/sanitize-html': + specifier: ^2.15.0 + version: 2.15.0 + postcss-import: + specifier: ^16.1.0 + version: 16.1.0(postcss@8.5.3) + postcss-nesting: + specifier: ^13.0.1 + version: 13.0.1(postcss@8.5.3) + rehype: + specifier: ^13.0.2 + version: 13.0.2 + +packages: + + '@adobe/css-tools@4.3.3': + resolution: {integrity: sha512-rE0Pygv0sEZ4vBWHlAgJLGDU7Pm8xoO6p3wsEceb7GYAjScrOHpEo8KK/eVkAcnSM+slAEtXjA2JpdjLp4fJQQ==} + + '@alloc/quick-lru@5.2.0': + resolution: {integrity: sha512-UrcABB+4bUrFABwbluTIBErXwvbsU/V7TZWfmbgJfbkwiBuziS9gxdODUyuiecfdGQ85jglMW6juS3+z5TsKLw==} + engines: {node: '>=10'} + + '@ampproject/remapping@2.3.0': + resolution: {integrity: sha512-30iZtAPgz+LTIYoeivqYo853f02jBYSd5uGnGpkFV0M3xOt9aN73erkgYAmZU43x4VfqcnLxW9Kpg3R5LC4YYw==} + engines: {node: '>=6.0.0'} + + '@antfu/install-pkg@1.0.0': + resolution: {integrity: sha512-xvX6P/lo1B3ej0OsaErAjqgFYzYVcJpamjLAFLYh9vRJngBrMoUG7aVnrGTeqM7yxbyTD5p3F2+0/QUEh8Vzhw==} + + '@antfu/utils@8.1.1': + resolution: {integrity: sha512-Mex9nXf9vR6AhcXmMrlz/HVgYYZpVGJ6YlPgwl7UnaFpnshXs6EK/oa5Gpf3CzENMjkvEx2tQtntGnb7UtSTOQ==} + + '@astrojs/check@0.9.4': + resolution: {integrity: sha512-IOheHwCtpUfvogHHsvu0AbeRZEnjJg3MopdLddkJE70mULItS/Vh37BHcI00mcOJcH1vhD3odbpvWokpxam7xA==} + hasBin: true + peerDependencies: + typescript: ^5.0.0 + + '@astrojs/compiler@2.11.0': + resolution: {integrity: sha512-zZOO7i+JhojO8qmlyR/URui6LyfHJY6m+L9nwyX5GiKD78YoRaZ5tzz6X0fkl+5bD3uwlDHayf6Oe8Fu36RKNg==} + + '@astrojs/internal-helpers@0.6.1': + resolution: {integrity: sha512-l5Pqf6uZu31aG+3Lv8nl/3s4DbUzdlxTWDof4pEpto6GUJNhhCbelVi9dEyurOVyqaelwmS9oSyOWOENSfgo9A==} + + '@astrojs/internal-helpers@0.7.2': + resolution: {integrity: sha512-KCkCqR3Goym79soqEtbtLzJfqhTWMyVaizUi35FLzgGSzBotSw8DB1qwsu7U96ihOJgYhDk2nVPz+3LnXPeX6g==} + + '@astrojs/language-server@2.15.4': + resolution: {integrity: sha512-JivzASqTPR2bao9BWsSc/woPHH7OGSGc9aMxXL4U6egVTqBycB3ZHdBJPuOCVtcGLrzdWTosAqVPz1BVoxE0+A==} + hasBin: true + peerDependencies: + prettier: ^3.0.0 + prettier-plugin-astro: '>=0.11.0' + peerDependenciesMeta: + prettier: + optional: true + prettier-plugin-astro: + optional: true + + '@astrojs/markdown-remark@6.3.1': + resolution: {integrity: sha512-c5F5gGrkczUaTVgmMW9g1YMJGzOtRvjjhw6IfGuxarM6ct09MpwysP10US729dy07gg8y+ofVifezvP3BNsWZg==} + + '@astrojs/prism@3.2.0': + resolution: {integrity: sha512-GilTHKGCW6HMq7y3BUv9Ac7GMe/MO9gi9GW62GzKtth0SwukCu/qp2wLiGpEujhY+VVhaG9v7kv/5vFzvf4NYw==} + engines: {node: ^18.17.1 || ^20.3.0 || >=22.0.0} + + '@astrojs/rss@4.0.11': + resolution: {integrity: sha512-3e3H8i6kc97KGnn9iaZBJpIkdoQi8MmR5zH5R+dWsfCM44lLTszOqy1OBfGGxDt56mpQkYVtZJWoxMyWuUZBfw==} + + '@astrojs/sitemap@3.3.1': + resolution: {integrity: sha512-GRnDUCTviBSNfXJ0Jmur+1/C+z3g36jy79VyYggfe1uNyEYSTcmAfTTCmbytrRvJRNyJJnSfB/77Gnm9PiXRRg==} + + '@astrojs/svelte@7.0.12': + resolution: {integrity: sha512-V11vC2YfdrJln40YTyhi6tiQCgHP3rjeJLpIWn7JJe3iT0JKOlE1SXH3kmJ9SiFgXJ5TbpsysY5p/bf/RMp0kg==} + engines: {node: ^18.17.1 || ^20.3.0 || >=22.0.0} + peerDependencies: + astro: ^5.0.0 + svelte: ^5.1.16 + typescript: ^5.3.3 + + '@astrojs/tailwind@6.0.2': + resolution: {integrity: sha512-j3mhLNeugZq6A8dMNXVarUa8K6X9AW+QHU9u3lKNrPLMHhOQ0S7VeWhHwEeJFpEK1BTKEUY1U78VQv2gN6hNGg==} + peerDependencies: + astro: ^3.0.0 || ^4.0.0 || ^5.0.0 + tailwindcss: ^3.0.24 + + '@astrojs/telemetry@3.2.1': + resolution: {integrity: sha512-SSVM820Jqc6wjsn7qYfV9qfeQvePtVc1nSofhyap7l0/iakUKywj3hfy3UJAOV4sGV4Q/u450RD4AaCaFvNPlg==} + engines: {node: ^18.17.1 || ^20.3.0 || >=22.0.0} + + '@astrojs/ts-plugin@1.10.4': + resolution: {integrity: sha512-rapryQINgv5VLZF884R/wmgX3mM9eH1PC/I3kkPV9rP6lEWrRN1YClF3bGcDHFrf8EtTLc0Wqxne1Uetpevozg==} + + '@astrojs/vercel@8.2.7': + resolution: {integrity: sha512-QeozkGU/0qch8MZlKJDKt+Dp8IUKA/E4MxnecVz1J0eVaXB6ao/HYcW7Tcr8LpWYU77Twhc60SgHIjrE3j2nYg==} + peerDependencies: + astro: ^5.0.0 + + '@astrojs/yaml2ts@0.2.2': + resolution: {integrity: sha512-GOfvSr5Nqy2z5XiwqTouBBpy5FyI6DEe+/g/Mk5am9SjILN1S5fOEvYK0GuWHg98yS/dobP4m8qyqw/URW35fQ==} + + '@babel/code-frame@7.26.2': + resolution: {integrity: sha512-RJlIHRueQgwWitWgF8OdFYGZX328Ax5BCemNGlqHfplnRT9ESi8JkFlvaVYbS+UubVY6dpv87Fs2u5M29iNFVQ==} + engines: {node: '>=6.9.0'} + + '@babel/compat-data@7.26.8': + resolution: {integrity: sha512-oH5UPLMWR3L2wEFLnFJ1TZXqHufiTKAiLfqw5zkhS4dKXLJ10yVztfil/twG8EDTA4F/tvVNw9nOl4ZMslB8rQ==} + engines: {node: '>=6.9.0'} + + '@babel/core@7.26.10': + resolution: {integrity: sha512-vMqyb7XCDMPvJFFOaT9kxtiRh42GwlZEg1/uIgtZshS5a/8OaduUfCi7kynKgc3Tw/6Uo2D+db9qBttghhmxwQ==} + engines: {node: '>=6.9.0'} + + '@babel/generator@7.27.0': + resolution: {integrity: sha512-VybsKvpiN1gU1sdMZIp7FcqphVVKEwcuj02x73uvcHE0PTihx1nlBcowYWhDwjpoAXRv43+gDzyggGnn1XZhVw==} + engines: {node: '>=6.9.0'} + + '@babel/helper-annotate-as-pure@7.25.9': + resolution: {integrity: sha512-gv7320KBUFJz1RnylIg5WWYPRXKZ884AGkYpgpWW02TH66Dl+HaC1t1CKd0z3R4b6hdYEcmrNZHUmfCP+1u3/g==} + engines: {node: '>=6.9.0'} + + '@babel/helper-compilation-targets@7.27.0': + resolution: {integrity: sha512-LVk7fbXml0H2xH34dFzKQ7TDZ2G4/rVTOrq9V+icbbadjbVxxeFeDsNHv2SrZeWoA+6ZiTyWYWtScEIW07EAcA==} + engines: {node: '>=6.9.0'} + + '@babel/helper-create-class-features-plugin@7.27.0': + resolution: {integrity: sha512-vSGCvMecvFCd/BdpGlhpXYNhhC4ccxyvQWpbGL4CWbvfEoLFWUZuSuf7s9Aw70flgQF+6vptvgK2IfOnKlRmBg==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0 + + '@babel/helper-create-regexp-features-plugin@7.27.0': + resolution: {integrity: sha512-fO8l08T76v48BhpNRW/nQ0MxfnSdoSKUJBMjubOAYffsVuGG5qOfMq7N6Es7UJvi7Y8goXXo07EfcHZXDPuELQ==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0 + + '@babel/helper-define-polyfill-provider@0.6.4': + resolution: {integrity: sha512-jljfR1rGnXXNWnmQg2K3+bvhkxB51Rl32QRaOTuwwjviGrHzIbSc8+x9CpraDtbT7mfyjXObULP4w/adunNwAw==} + peerDependencies: + '@babel/core': ^7.4.0 || ^8.0.0-0 <8.0.0 + + '@babel/helper-member-expression-to-functions@7.25.9': + resolution: {integrity: sha512-wbfdZ9w5vk0C0oyHqAJbc62+vet5prjj01jjJ8sKn3j9h3MQQlflEdXYvuqRWjHnM12coDEqiC1IRCi0U/EKwQ==} + engines: {node: '>=6.9.0'} + + '@babel/helper-module-imports@7.25.9': + resolution: {integrity: sha512-tnUA4RsrmflIM6W6RFTLFSXITtl0wKjgpnLgXyowocVPrbYrLUXSBXDgTs8BlbmIzIdlBySRQjINYs2BAkiLtw==} + engines: {node: '>=6.9.0'} + + '@babel/helper-module-transforms@7.26.0': + resolution: {integrity: sha512-xO+xu6B5K2czEnQye6BHA7DolFFmS3LB7stHZFaOLb1pAwO1HWLS8fXA+eh0A2yIvltPVmx3eNNDBJA2SLHXFw==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0 + + '@babel/helper-optimise-call-expression@7.25.9': + resolution: {integrity: sha512-FIpuNaz5ow8VyrYcnXQTDRGvV6tTjkNtCK/RYNDXGSLlUD6cBuQTSw43CShGxjvfBTfcUA/r6UhUCbtYqkhcuQ==} + engines: {node: '>=6.9.0'} + + '@babel/helper-plugin-utils@7.26.5': + resolution: {integrity: sha512-RS+jZcRdZdRFzMyr+wcsaqOmld1/EqTghfaBGQQd/WnRdzdlvSZ//kF7U8VQTxf1ynZ4cjUcYgjVGx13ewNPMg==} + engines: {node: '>=6.9.0'} + + '@babel/helper-remap-async-to-generator@7.25.9': + resolution: {integrity: sha512-IZtukuUeBbhgOcaW2s06OXTzVNJR0ybm4W5xC1opWFFJMZbwRj5LCk+ByYH7WdZPZTt8KnFwA8pvjN2yqcPlgw==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0 + + '@babel/helper-replace-supers@7.26.5': + resolution: {integrity: sha512-bJ6iIVdYX1YooY2X7w1q6VITt+LnUILtNk7zT78ykuwStx8BauCzxvFqFaHjOpW1bVnSUM1PN1f0p5P21wHxvg==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0 + + '@babel/helper-skip-transparent-expression-wrappers@7.25.9': + resolution: {integrity: sha512-K4Du3BFa3gvyhzgPcntrkDgZzQaq6uozzcpGbOO1OEJaI+EJdqWIMTLgFgQf6lrfiDFo5FU+BxKepI9RmZqahA==} + engines: {node: '>=6.9.0'} + + '@babel/helper-string-parser@7.25.9': + resolution: {integrity: sha512-4A/SCr/2KLd5jrtOMFzaKjVtAei3+2r/NChoBNoZ3EyP/+GlhoaEGoWOZUmFmoITP7zOJyHIMm+DYRd8o3PvHA==} + engines: {node: '>=6.9.0'} + + '@babel/helper-string-parser@7.27.1': + resolution: {integrity: sha512-qMlSxKbpRlAridDExk92nSobyDdpPijUq2DW6oDnUqd0iOGxmQjyqhMIihI9+zv4LPyZdRje2cavWPbCbWm3eA==} + engines: {node: '>=6.9.0'} + + '@babel/helper-validator-identifier@7.25.9': + resolution: {integrity: sha512-Ed61U6XJc3CVRfkERJWDz4dJwKe7iLmmJsbOGu9wSloNSFttHV0I8g6UAgb7qnK5ly5bGLPd4oXZlxCdANBOWQ==} + engines: {node: '>=6.9.0'} + + '@babel/helper-validator-identifier@7.27.1': + resolution: {integrity: sha512-D2hP9eA+Sqx1kBZgzxZh0y1trbuU+JoDkiEwqhQ36nodYqJwyEIhPSdMNd7lOm/4io72luTPWH20Yda0xOuUow==} + engines: {node: '>=6.9.0'} + + '@babel/helper-validator-option@7.25.9': + resolution: {integrity: sha512-e/zv1co8pp55dNdEcCynfj9X7nyUKUXoUEwfXqaZt0omVOmDe9oOTdKStH4GmAw6zxMFs50ZayuMfHDKlO7Tfw==} + engines: {node: '>=6.9.0'} + + '@babel/helper-wrap-function@7.25.9': + resolution: {integrity: sha512-ETzz9UTjQSTmw39GboatdymDq4XIQbR8ySgVrylRhPOFpsd+JrKHIuF0de7GCWmem+T4uC5z7EZguod7Wj4A4g==} + engines: {node: '>=6.9.0'} + + '@babel/helpers@7.27.0': + resolution: {integrity: sha512-U5eyP/CTFPuNE3qk+WZMxFkp/4zUzdceQlfzf7DdGdhp+Fezd7HD+i8Y24ZuTMKX3wQBld449jijbGq6OdGNQg==} + engines: {node: '>=6.9.0'} + + '@babel/parser@7.27.0': + resolution: {integrity: sha512-iaepho73/2Pz7w2eMS0Q5f83+0RKI7i4xmiYeBmDzfRVbQtTOG7Ts0S4HzJVsTMGI9keU8rNfuZr8DKfSt7Yyg==} + engines: {node: '>=6.0.0'} + hasBin: true + + '@babel/parser@7.28.0': + resolution: {integrity: sha512-jVZGvOxOuNSsuQuLRTh13nU0AogFlw32w/MT+LV6D3sP5WdbW61E77RnkbaO2dUvmPAYrBDJXGn5gGS6tH4j8g==} + engines: {node: '>=6.0.0'} + hasBin: true + + '@babel/plugin-bugfix-firefox-class-in-computed-class-key@7.25.9': + resolution: {integrity: sha512-ZkRyVkThtxQ/J6nv3JFYv1RYY+JT5BvU0y3k5bWrmuG4woXypRa4PXmm9RhOwodRkYFWqC0C0cqcJ4OqR7kW+g==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0 + + '@babel/plugin-bugfix-safari-class-field-initializer-scope@7.25.9': + resolution: {integrity: sha512-MrGRLZxLD/Zjj0gdU15dfs+HH/OXvnw/U4jJD8vpcP2CJQapPEv1IWwjc/qMg7ItBlPwSv1hRBbb7LeuANdcnw==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0 + + '@babel/plugin-bugfix-safari-id-destructuring-collision-in-function-expression@7.25.9': + resolution: {integrity: sha512-2qUwwfAFpJLZqxd02YW9btUCZHl+RFvdDkNfZwaIJrvB8Tesjsk8pEQkTvGwZXLqXUx/2oyY3ySRhm6HOXuCug==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0 + + '@babel/plugin-bugfix-v8-spread-parameters-in-optional-chaining@7.25.9': + resolution: {integrity: sha512-6xWgLZTJXwilVjlnV7ospI3xi+sl8lN8rXXbBD6vYn3UYDlGsag8wrZkKcSI8G6KgqKP7vNFaDgeDnfAABq61g==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.13.0 + + '@babel/plugin-bugfix-v8-static-class-fields-redefine-readonly@7.25.9': + resolution: {integrity: sha512-aLnMXYPnzwwqhYSCyXfKkIkYgJ8zv9RK+roo9DkTXz38ynIhd9XCbN08s3MGvqL2MYGVUGdRQLL/JqBIeJhJBg==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0 + + '@babel/plugin-proposal-class-properties@7.12.1': + resolution: {integrity: sha512-cKp3dlQsFsEs5CWKnN7BnSHOd0EOW8EKpEjkoz1pO2E5KzIDNV9Ros1b0CnmbVgAGXJubOYVBOGCT1OmJwOI7w==} + deprecated: This proposal has been merged to the ECMAScript standard and thus this plugin is no longer maintained. Please use @babel/plugin-transform-class-properties instead. + peerDependencies: + '@babel/core': ^7.0.0-0 + + '@babel/plugin-proposal-private-property-in-object@7.21.0-placeholder-for-preset-env.2': + resolution: {integrity: sha512-SOSkfJDddaM7mak6cPEpswyTRnuRltl429hMraQEglW+OkovnCzsiszTmsrlY//qLFjCpQDFRvjdm2wA5pPm9w==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0-0 + + '@babel/plugin-syntax-flow@7.26.0': + resolution: {integrity: sha512-B+O2DnPc0iG+YXFqOxv2WNuNU97ToWjOomUQ78DouOENWUaM5sVrmet9mcomUGQFwpJd//gvUagXBSdzO1fRKg==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0-0 + + '@babel/plugin-syntax-import-assertions@7.26.0': + resolution: {integrity: sha512-QCWT5Hh830hK5EQa7XzuqIkQU9tT/whqbDz7kuaZMHFl1inRRg7JnuAEOQ0Ur0QUl0NufCk1msK2BeY79Aj/eg==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0-0 + + '@babel/plugin-syntax-import-attributes@7.26.0': + resolution: {integrity: sha512-e2dttdsJ1ZTpi3B9UYGLw41hifAubg19AtCu/2I/F1QNVclOBr1dYpTdmdyZ84Xiz43BS/tCUkMAZNLv12Pi+A==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0-0 + + '@babel/plugin-syntax-import-meta@7.10.4': + resolution: {integrity: sha512-Yqfm+XDx0+Prh3VSeEQCPU81yC+JWZ2pDPFSS4ZdpfZhp4MkFMaDC1UqseovEKwSUpnIL7+vK+Clp7bfh0iD7g==} + peerDependencies: + '@babel/core': ^7.0.0-0 + + '@babel/plugin-syntax-jsx@7.25.9': + resolution: {integrity: sha512-ld6oezHQMZsZfp6pWtbjaNDF2tiiCYYDqQszHt5VV437lewP9aSi2Of99CK0D0XB21k7FLgnLcmQKyKzynfeAA==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0-0 + + '@babel/plugin-syntax-unicode-sets-regex@7.18.6': + resolution: {integrity: sha512-727YkEAPwSIQTv5im8QHz3upqp92JTWhidIC81Tdx4VJYIte/VndKf1qKrfnnhPLiPghStWfvC/iFaMCQu7Nqg==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0 + + '@babel/plugin-transform-arrow-functions@7.25.9': + resolution: {integrity: sha512-6jmooXYIwn9ca5/RylZADJ+EnSxVUS5sjeJ9UPk6RWRzXCmOJCy6dqItPJFpw2cuCangPK4OYr5uhGKcmrm5Qg==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0-0 + + '@babel/plugin-transform-async-generator-functions@7.26.8': + resolution: {integrity: sha512-He9Ej2X7tNf2zdKMAGOsmg2MrFc+hfoAhd3po4cWfo/NWjzEAKa0oQruj1ROVUdl0e6fb6/kE/G3SSxE0lRJOg==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0-0 + + '@babel/plugin-transform-async-to-generator@7.25.9': + resolution: {integrity: sha512-NT7Ejn7Z/LjUH0Gv5KsBCxh7BH3fbLTV0ptHvpeMvrt3cPThHfJfst9Wrb7S8EvJ7vRTFI7z+VAvFVEQn/m5zQ==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0-0 + + '@babel/plugin-transform-block-scoped-functions@7.26.5': + resolution: {integrity: sha512-chuTSY+hq09+/f5lMj8ZSYgCFpppV2CbYrhNFJ1BFoXpiWPnnAb7R0MqrafCpN8E1+YRrtM1MXZHJdIx8B6rMQ==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0-0 + + '@babel/plugin-transform-block-scoping@7.27.0': + resolution: {integrity: sha512-u1jGphZ8uDI2Pj/HJj6YQ6XQLZCNjOlprjxB5SVz6rq2T6SwAR+CdrWK0CP7F+9rDVMXdB0+r6Am5G5aobOjAQ==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0-0 + + '@babel/plugin-transform-class-properties@7.25.9': + resolution: {integrity: sha512-bbMAII8GRSkcd0h0b4X+36GksxuheLFjP65ul9w6C3KgAamI3JqErNgSrosX6ZPj+Mpim5VvEbawXxJCyEUV3Q==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0-0 + + '@babel/plugin-transform-class-static-block@7.26.0': + resolution: {integrity: sha512-6J2APTs7BDDm+UMqP1useWqhcRAXo0WIoVj26N7kPFB6S73Lgvyka4KTZYIxtgYXiN5HTyRObA72N2iu628iTQ==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.12.0 + + '@babel/plugin-transform-classes@7.25.9': + resolution: {integrity: sha512-mD8APIXmseE7oZvZgGABDyM34GUmK45Um2TXiBUt7PnuAxrgoSVf123qUzPxEr/+/BHrRn5NMZCdE2m/1F8DGg==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0-0 + + '@babel/plugin-transform-computed-properties@7.25.9': + resolution: {integrity: sha512-HnBegGqXZR12xbcTHlJ9HGxw1OniltT26J5YpfruGqtUHlz/xKf/G2ak9e+t0rVqrjXa9WOhvYPz1ERfMj23AA==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0-0 + + '@babel/plugin-transform-destructuring@7.25.9': + resolution: {integrity: sha512-WkCGb/3ZxXepmMiX101nnGiU+1CAdut8oHyEOHxkKuS1qKpU2SMXE2uSvfz8PBuLd49V6LEsbtyPhWC7fnkgvQ==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0-0 + + '@babel/plugin-transform-dotall-regex@7.25.9': + resolution: {integrity: sha512-t7ZQ7g5trIgSRYhI9pIJtRl64KHotutUJsh4Eze5l7olJv+mRSg4/MmbZ0tv1eeqRbdvo/+trvJD/Oc5DmW2cA==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0-0 + + '@babel/plugin-transform-duplicate-keys@7.25.9': + resolution: {integrity: sha512-LZxhJ6dvBb/f3x8xwWIuyiAHy56nrRG3PeYTpBkkzkYRRQ6tJLu68lEF5VIqMUZiAV7a8+Tb78nEoMCMcqjXBw==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0-0 + + '@babel/plugin-transform-duplicate-named-capturing-groups-regex@7.25.9': + resolution: {integrity: sha512-0UfuJS0EsXbRvKnwcLjFtJy/Sxc5J5jhLHnFhy7u4zih97Hz6tJkLU+O+FMMrNZrosUPxDi6sYxJ/EA8jDiAog==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0 + + '@babel/plugin-transform-dynamic-import@7.25.9': + resolution: {integrity: sha512-GCggjexbmSLaFhqsojeugBpeaRIgWNTcgKVq/0qIteFEqY2A+b9QidYadrWlnbWQUrW5fn+mCvf3tr7OeBFTyg==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0-0 + + '@babel/plugin-transform-exponentiation-operator@7.26.3': + resolution: {integrity: sha512-7CAHcQ58z2chuXPWblnn1K6rLDnDWieghSOEmqQsrBenH0P9InCUtOJYD89pvngljmZlJcz3fcmgYsXFNGa1ZQ==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0-0 + + '@babel/plugin-transform-export-namespace-from@7.25.9': + resolution: {integrity: sha512-2NsEz+CxzJIVOPx2o9UsW1rXLqtChtLoVnwYHHiB04wS5sgn7mrV45fWMBX0Kk+ub9uXytVYfNP2HjbVbCB3Ww==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0-0 + + '@babel/plugin-transform-flow-strip-types@7.26.5': + resolution: {integrity: sha512-eGK26RsbIkYUns3Y8qKl362juDDYK+wEdPGHGrhzUl6CewZFo55VZ7hg+CyMFU4dd5QQakBN86nBMpRsFpRvbQ==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0-0 + + '@babel/plugin-transform-for-of@7.26.9': + resolution: {integrity: sha512-Hry8AusVm8LW5BVFgiyUReuoGzPUpdHQQqJY5bZnbbf+ngOHWuCuYFKw/BqaaWlvEUrF91HMhDtEaI1hZzNbLg==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0-0 + + '@babel/plugin-transform-function-name@7.25.9': + resolution: {integrity: sha512-8lP+Yxjv14Vc5MuWBpJsoUCd3hD6V9DgBon2FVYL4jJgbnVQ9fTgYmonchzZJOVNgzEgbxp4OwAf6xz6M/14XA==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0-0 + + '@babel/plugin-transform-json-strings@7.25.9': + resolution: {integrity: sha512-xoTMk0WXceiiIvsaquQQUaLLXSW1KJ159KP87VilruQm0LNNGxWzahxSS6T6i4Zg3ezp4vA4zuwiNUR53qmQAw==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0-0 + + '@babel/plugin-transform-literals@7.25.9': + resolution: {integrity: sha512-9N7+2lFziW8W9pBl2TzaNht3+pgMIRP74zizeCSrtnSKVdUl8mAjjOP2OOVQAfZ881P2cNjDj1uAMEdeD50nuQ==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0-0 + + '@babel/plugin-transform-logical-assignment-operators@7.25.9': + resolution: {integrity: sha512-wI4wRAzGko551Y8eVf6iOY9EouIDTtPb0ByZx+ktDGHwv6bHFimrgJM/2T021txPZ2s4c7bqvHbd+vXG6K948Q==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0-0 + + '@babel/plugin-transform-member-expression-literals@7.25.9': + resolution: {integrity: sha512-PYazBVfofCQkkMzh2P6IdIUaCEWni3iYEerAsRWuVd8+jlM1S9S9cz1dF9hIzyoZ8IA3+OwVYIp9v9e+GbgZhA==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0-0 + + '@babel/plugin-transform-modules-amd@7.25.9': + resolution: {integrity: sha512-g5T11tnI36jVClQlMlt4qKDLlWnG5pP9CSM4GhdRciTNMRgkfpo5cR6b4rGIOYPgRRuFAvwjPQ/Yk+ql4dyhbw==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0-0 + + '@babel/plugin-transform-modules-commonjs@7.26.3': + resolution: {integrity: sha512-MgR55l4q9KddUDITEzEFYn5ZsGDXMSsU9E+kh7fjRXTIC3RHqfCo8RPRbyReYJh44HQ/yomFkqbOFohXvDCiIQ==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0-0 + + '@babel/plugin-transform-modules-systemjs@7.25.9': + resolution: {integrity: sha512-hyss7iIlH/zLHaehT+xwiymtPOpsiwIIRlCAOwBB04ta5Tt+lNItADdlXw3jAWZ96VJ2jlhl/c+PNIQPKNfvcA==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0-0 + + '@babel/plugin-transform-modules-umd@7.25.9': + resolution: {integrity: sha512-bS9MVObUgE7ww36HEfwe6g9WakQ0KF07mQF74uuXdkoziUPfKyu/nIm663kz//e5O1nPInPFx36z7WJmJ4yNEw==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0-0 + + '@babel/plugin-transform-named-capturing-groups-regex@7.25.9': + resolution: {integrity: sha512-oqB6WHdKTGl3q/ItQhpLSnWWOpjUJLsOCLVyeFgeTktkBSCiurvPOsyt93gibI9CmuKvTUEtWmG5VhZD+5T/KA==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0 + + '@babel/plugin-transform-new-target@7.25.9': + resolution: {integrity: sha512-U/3p8X1yCSoKyUj2eOBIx3FOn6pElFOKvAAGf8HTtItuPyB+ZeOqfn+mvTtg9ZlOAjsPdK3ayQEjqHjU/yLeVQ==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0-0 + + '@babel/plugin-transform-nullish-coalescing-operator@7.26.6': + resolution: {integrity: sha512-CKW8Vu+uUZneQCPtXmSBUC6NCAUdya26hWCElAWh5mVSlSRsmiCPUUDKb3Z0szng1hiAJa098Hkhg9o4SE35Qw==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0-0 + + '@babel/plugin-transform-numeric-separator@7.25.9': + resolution: {integrity: sha512-TlprrJ1GBZ3r6s96Yq8gEQv82s8/5HnCVHtEJScUj90thHQbwe+E5MLhi2bbNHBEJuzrvltXSru+BUxHDoog7Q==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0-0 + + '@babel/plugin-transform-object-rest-spread@7.25.9': + resolution: {integrity: sha512-fSaXafEE9CVHPweLYw4J0emp1t8zYTXyzN3UuG+lylqkvYd7RMrsOQ8TYx5RF231be0vqtFC6jnx3UmpJmKBYg==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0-0 + + '@babel/plugin-transform-object-super@7.25.9': + resolution: {integrity: sha512-Kj/Gh+Rw2RNLbCK1VAWj2U48yxxqL2x0k10nPtSdRa0O2xnHXalD0s+o1A6a0W43gJ00ANo38jxkQreckOzv5A==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0-0 + + '@babel/plugin-transform-optional-catch-binding@7.25.9': + resolution: {integrity: sha512-qM/6m6hQZzDcZF3onzIhZeDHDO43bkNNlOX0i8n3lR6zLbu0GN2d8qfM/IERJZYauhAHSLHy39NF0Ctdvcid7g==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0-0 + + '@babel/plugin-transform-optional-chaining@7.25.9': + resolution: {integrity: sha512-6AvV0FsLULbpnXeBjrY4dmWF8F7gf8QnvTEoO/wX/5xm/xE1Xo8oPuD3MPS+KS9f9XBEAWN7X1aWr4z9HdOr7A==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0-0 + + '@babel/plugin-transform-parameters@7.25.9': + resolution: {integrity: sha512-wzz6MKwpnshBAiRmn4jR8LYz/g8Ksg0o80XmwZDlordjwEk9SxBzTWC7F5ef1jhbrbOW2DJ5J6ayRukrJmnr0g==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0-0 + + '@babel/plugin-transform-private-methods@7.25.9': + resolution: {integrity: sha512-D/JUozNpQLAPUVusvqMxyvjzllRaF8/nSrP1s2YGQT/W4LHK4xxsMcHjhOGTS01mp9Hda8nswb+FblLdJornQw==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0-0 + + '@babel/plugin-transform-private-property-in-object@7.25.9': + resolution: {integrity: sha512-Evf3kcMqzXA3xfYJmZ9Pg1OvKdtqsDMSWBDzZOPLvHiTt36E75jLDQo5w1gtRU95Q4E5PDttrTf25Fw8d/uWLw==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0-0 + + '@babel/plugin-transform-property-literals@7.25.9': + resolution: {integrity: sha512-IvIUeV5KrS/VPavfSM/Iu+RE6llrHrYIKY1yfCzyO/lMXHQ+p7uGhonmGVisv6tSBSVgWzMBohTcvkC9vQcQFA==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0-0 + + '@babel/plugin-transform-react-display-name@7.25.9': + resolution: {integrity: sha512-KJfMlYIUxQB1CJfO3e0+h0ZHWOTLCPP115Awhaz8U0Zpq36Gl/cXlpoyMRnUWlhNUBAzldnCiAZNvCDj7CrKxQ==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0-0 + + '@babel/plugin-transform-react-jsx-development@7.25.9': + resolution: {integrity: sha512-9mj6rm7XVYs4mdLIpbZnHOYdpW42uoiBCTVowg7sP1thUOiANgMb4UtpRivR0pp5iL+ocvUv7X4mZgFRpJEzGw==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0-0 + + '@babel/plugin-transform-react-jsx@7.25.9': + resolution: {integrity: sha512-s5XwpQYCqGerXl+Pu6VDL3x0j2d82eiV77UJ8a2mDHAW7j9SWRqQ2y1fNo1Z74CdcYipl5Z41zvjj4Nfzq36rw==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0-0 + + '@babel/plugin-transform-react-pure-annotations@7.25.9': + resolution: {integrity: sha512-KQ/Takk3T8Qzj5TppkS1be588lkbTp5uj7w6a0LeQaTMSckU/wK0oJ/pih+T690tkgI5jfmg2TqDJvd41Sj1Cg==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0-0 + + '@babel/plugin-transform-regenerator@7.27.0': + resolution: {integrity: sha512-LX/vCajUJQDqE7Aum/ELUMZAY19+cDpghxrnyt5I1tV6X5PyC86AOoWXWFYFeIvauyeSA6/ktn4tQVn/3ZifsA==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0-0 + + '@babel/plugin-transform-regexp-modifiers@7.26.0': + resolution: {integrity: sha512-vN6saax7lrA2yA/Pak3sCxuD6F5InBjn9IcrIKQPjpsLvuHYLVroTxjdlVRHjjBWxKOqIwpTXDkOssYT4BFdRw==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0 + + '@babel/plugin-transform-reserved-words@7.25.9': + resolution: {integrity: sha512-7DL7DKYjn5Su++4RXu8puKZm2XBPHyjWLUidaPEkCUBbE7IPcsrkRHggAOOKydH1dASWdcUBxrkOGNxUv5P3Jg==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0-0 + + '@babel/plugin-transform-shorthand-properties@7.25.9': + resolution: {integrity: sha512-MUv6t0FhO5qHnS/W8XCbHmiRWOphNufpE1IVxhK5kuN3Td9FT1x4rx4K42s3RYdMXCXpfWkGSbCSd0Z64xA7Ng==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0-0 + + '@babel/plugin-transform-spread@7.25.9': + resolution: {integrity: sha512-oNknIB0TbURU5pqJFVbOOFspVlrpVwo2H1+HUIsVDvp5VauGGDP1ZEvO8Nn5xyMEs3dakajOxlmkNW7kNgSm6A==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0-0 + + '@babel/plugin-transform-sticky-regex@7.25.9': + resolution: {integrity: sha512-WqBUSgeVwucYDP9U/xNRQam7xV8W5Zf+6Eo7T2SRVUFlhRiMNFdFz58u0KZmCVVqs2i7SHgpRnAhzRNmKfi2uA==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0-0 + + '@babel/plugin-transform-template-literals@7.26.8': + resolution: {integrity: sha512-OmGDL5/J0CJPJZTHZbi2XpO0tyT2Ia7fzpW5GURwdtp2X3fMmN8au/ej6peC/T33/+CRiIpA8Krse8hFGVmT5Q==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0-0 + + '@babel/plugin-transform-typeof-symbol@7.27.0': + resolution: {integrity: sha512-+LLkxA9rKJpNoGsbLnAgOCdESl73vwYn+V6b+5wHbrE7OGKVDPHIQvbFSzqE6rwqaCw2RE+zdJrlLkcf8YOA0w==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0-0 + + '@babel/plugin-transform-unicode-escapes@7.25.9': + resolution: {integrity: sha512-s5EDrE6bW97LtxOcGj1Khcx5AaXwiMmi4toFWRDP9/y0Woo6pXC+iyPu/KuhKtfSrNFd7jJB+/fkOtZy6aIC6Q==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0-0 + + '@babel/plugin-transform-unicode-property-regex@7.25.9': + resolution: {integrity: sha512-Jt2d8Ga+QwRluxRQ307Vlxa6dMrYEMZCgGxoPR8V52rxPyldHu3hdlHspxaqYmE7oID5+kB+UKUB/eWS+DkkWg==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0-0 + + '@babel/plugin-transform-unicode-regex@7.25.9': + resolution: {integrity: sha512-yoxstj7Rg9dlNn9UQxzk4fcNivwv4nUYz7fYXBaKxvw/lnmPuOm/ikoELygbYq68Bls3D/D+NBPHiLwZdZZ4HA==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0-0 + + '@babel/plugin-transform-unicode-sets-regex@7.25.9': + resolution: {integrity: sha512-8BYqO3GeVNHtx69fdPshN3fnzUNLrWdHhk/icSwigksJGczKSizZ+Z6SBCxTs723Fr5VSNorTIK7a+R2tISvwQ==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0 + + '@babel/preset-env@7.26.9': + resolution: {integrity: sha512-vX3qPGE8sEKEAZCWk05k3cpTAE3/nOYca++JA+Rd0z2NCNzabmYvEiSShKzm10zdquOIAVXsy2Ei/DTW34KlKQ==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0-0 + + '@babel/preset-flow@7.25.9': + resolution: {integrity: sha512-EASHsAhE+SSlEzJ4bzfusnXSHiU+JfAYzj+jbw2vgQKgq5HrUr8qs+vgtiEL5dOH6sEweI+PNt2D7AqrDSHyqQ==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0-0 + + '@babel/preset-modules@0.1.6-no-external-plugins': + resolution: {integrity: sha512-HrcgcIESLm9aIR842yhJ5RWan/gebQUJ6E/E5+rf0y9o6oj7w0Br+sWuL6kEQ/o/AdfvR1Je9jG18/gnpwjEyA==} + peerDependencies: + '@babel/core': ^7.0.0-0 || ^8.0.0-0 <8.0.0 + + '@babel/preset-react@7.26.3': + resolution: {integrity: sha512-Nl03d6T9ky516DGK2YMxrTqvnpUW63TnJMOMonj+Zae0JiPC5BC9xPMSL6L8fiSpA5vP88qfygavVQvnLp+6Cw==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0-0 + + '@babel/runtime@7.27.0': + resolution: {integrity: sha512-VtPOkrdPHZsKc/clNqyi9WUA8TINkZ4cGk63UUE3u4pmB2k+ZMQRDuIOagv8UVd6j7k0T3+RRIb7beKTebNbcw==} + engines: {node: '>=6.9.0'} + + '@babel/template@7.27.0': + resolution: {integrity: sha512-2ncevenBqXI6qRMukPlXwHKHchC7RyMuu4xv5JBXRfOGVcTy1mXCD12qrp7Jsoxll1EV3+9sE4GugBVRjT2jFA==} + engines: {node: '>=6.9.0'} + + '@babel/traverse@7.27.0': + resolution: {integrity: sha512-19lYZFzYVQkkHkl4Cy4WrAVcqBkgvV2YM2TU3xG6DIwO7O3ecbDPfW3yM3bjAGcqcQHi+CCtjMR3dIEHxsd6bA==} + engines: {node: '>=6.9.0'} + + '@babel/types@7.27.0': + resolution: {integrity: sha512-H45s8fVLYjbhFH62dIJ3WtmJ6RSPt/3DRO0ZcT2SUiYiQyz3BLVb9ADEnLl91m74aQPS3AzzeajZHYOalWe3bg==} + engines: {node: '>=6.9.0'} + + '@babel/types@7.28.2': + resolution: {integrity: sha512-ruv7Ae4J5dUYULmeXw1gmb7rYRz57OWCPM57pHojnLq/3Z1CK2lNSLTCVjxVk1F/TZHwOZZrOWi0ur95BbLxNQ==} + engines: {node: '>=6.9.0'} + + '@biomejs/biome@1.9.4': + resolution: {integrity: sha512-1rkd7G70+o9KkTn5KLmDYXihGoTaIGO9PIIN2ZB7UJxFrWw04CZHPYiMRjYsaDvVV7hP1dYNRLxSANLaBFGpog==} + engines: {node: '>=14.21.3'} + hasBin: true + + '@biomejs/cli-darwin-arm64@1.9.4': + resolution: {integrity: sha512-bFBsPWrNvkdKrNCYeAp+xo2HecOGPAy9WyNyB/jKnnedgzl4W4Hb9ZMzYNbf8dMCGmUdSavlYHiR01QaYR58cw==} + engines: {node: '>=14.21.3'} + cpu: [arm64] + os: [darwin] + + '@biomejs/cli-darwin-x64@1.9.4': + resolution: {integrity: sha512-ngYBh/+bEedqkSevPVhLP4QfVPCpb+4BBe2p7Xs32dBgs7rh9nY2AIYUL6BgLw1JVXV8GlpKmb/hNiuIxfPfZg==} + engines: {node: '>=14.21.3'} + cpu: [x64] + os: [darwin] + + '@biomejs/cli-linux-arm64-musl@1.9.4': + resolution: {integrity: sha512-v665Ct9WCRjGa8+kTr0CzApU0+XXtRgwmzIf1SeKSGAv+2scAlW6JR5PMFo6FzqqZ64Po79cKODKf3/AAmECqA==} + engines: {node: '>=14.21.3'} + cpu: [arm64] + os: [linux] + libc: [musl] + + '@biomejs/cli-linux-arm64@1.9.4': + resolution: {integrity: sha512-fJIW0+LYujdjUgJJuwesP4EjIBl/N/TcOX3IvIHJQNsAqvV2CHIogsmA94BPG6jZATS4Hi+xv4SkBBQSt1N4/g==} + engines: {node: '>=14.21.3'} + cpu: [arm64] + os: [linux] + libc: [glibc] + + '@biomejs/cli-linux-x64-musl@1.9.4': + resolution: {integrity: sha512-gEhi/jSBhZ2m6wjV530Yy8+fNqG8PAinM3oV7CyO+6c3CEh16Eizm21uHVsyVBEB6RIM8JHIl6AGYCv6Q6Q9Tg==} + engines: {node: '>=14.21.3'} + cpu: [x64] + os: [linux] + libc: [musl] + + '@biomejs/cli-linux-x64@1.9.4': + resolution: {integrity: sha512-lRCJv/Vi3Vlwmbd6K+oQ0KhLHMAysN8lXoCI7XeHlxaajk06u7G+UsFSO01NAs5iYuWKmVZjmiOzJ0OJmGsMwg==} + engines: {node: '>=14.21.3'} + cpu: [x64] + os: [linux] + libc: [glibc] + + '@biomejs/cli-win32-arm64@1.9.4': + resolution: {integrity: sha512-tlbhLk+WXZmgwoIKwHIHEBZUwxml7bRJgk0X2sPyNR3S93cdRq6XulAZRQJ17FYGGzWne0fgrXBKpl7l4M87Hg==} + engines: {node: '>=14.21.3'} + cpu: [arm64] + os: [win32] + + '@biomejs/cli-win32-x64@1.9.4': + resolution: {integrity: sha512-8Y5wMhVIPaWe6jw2H+KlEm4wP/f7EW3810ZLmDlrEEy5KvBsb9ECEfu/kMWD484ijfQ8+nIi0giMgu9g1UAuuA==} + engines: {node: '>=14.21.3'} + cpu: [x64] + os: [win32] + + '@capsizecss/unpack@2.4.0': + resolution: {integrity: sha512-GrSU71meACqcmIUxPYOJvGKF0yryjN/L1aCuE9DViCTJI7bfkjgYDPD1zbNDcINJwSSP6UaBZY9GAbYDO7re0Q==} + + '@csstools/selector-resolve-nested@3.0.0': + resolution: {integrity: sha512-ZoK24Yku6VJU1gS79a5PFmC8yn3wIapiKmPgun0hZgEI5AOqgH2kiPRsPz1qkGv4HL+wuDLH83yQyk6inMYrJQ==} + engines: {node: '>=18'} + peerDependencies: + postcss-selector-parser: ^7.0.0 + + '@csstools/selector-specificity@5.0.0': + resolution: {integrity: sha512-PCqQV3c4CoVm3kdPhyeZ07VmBRdH2EpMFA/pd9OASpOEC3aXNGoqPDAZ80D0cLpMBxnmk0+yNhGsEx31hq7Gtw==} + engines: {node: '>=18'} + peerDependencies: + postcss-selector-parser: ^7.0.0 + + '@ctrl/tinycolor@4.1.0': + resolution: {integrity: sha512-WyOx8cJQ+FQus4Mm4uPIZA64gbk3Wxh0so5Lcii0aJifqwoVOlfFtorjLE0Hen4OYyHZMXDWqMmaQemBhgxFRQ==} + engines: {node: '>=14'} + + '@emmetio/abbreviation@2.3.3': + resolution: {integrity: sha512-mgv58UrU3rh4YgbE/TzgLQwJ3pFsHHhCLqY20aJq+9comytTXUDNGG/SMtSeMJdkpxgXSXunBGLD8Boka3JyVA==} + + '@emmetio/css-abbreviation@2.1.8': + resolution: {integrity: sha512-s9yjhJ6saOO/uk1V74eifykk2CBYi01STTK3WlXWGOepyKa23ymJ053+DNQjpFcy1ingpaO7AxCcwLvHFY9tuw==} + + '@emmetio/css-parser@0.4.0': + resolution: {integrity: sha512-z7wkxRSZgrQHXVzObGkXG+Vmj3uRlpM11oCZ9pbaz0nFejvCDmAiNDpY75+wgXOcffKpj4rzGtwGaZxfJKsJxw==} + + '@emmetio/html-matcher@1.3.0': + resolution: {integrity: sha512-NTbsvppE5eVyBMuyGfVu2CRrLvo7J4YHb6t9sBFLyY03WYhXET37qA4zOYUjBWFCRHO7pS1B9khERtY0f5JXPQ==} + + '@emmetio/scanner@1.0.4': + resolution: {integrity: sha512-IqRuJtQff7YHHBk4G8YZ45uB9BaAGcwQeVzgj/zj8/UdOhtQpEIupUhSk8dys6spFIWVZVeK20CzGEnqR5SbqA==} + + '@emmetio/stream-reader-utils@0.1.0': + resolution: {integrity: sha512-ZsZ2I9Vzso3Ho/pjZFsmmZ++FWeEd/txqybHTm4OgaZzdS8V9V/YYWQwg5TC38Z7uLWUV1vavpLLbjJtKubR1A==} + + '@emmetio/stream-reader@2.2.0': + resolution: {integrity: sha512-fXVXEyFA5Yv3M3n8sUGT7+fvecGrZP4k6FnWWMSZVQf69kAq0LLpaBQLGcPR30m3zMmKYhECP4k/ZkzvhEW5kw==} + + '@emnapi/runtime@1.4.0': + resolution: {integrity: sha512-64WYIf4UYcdLnbKn/umDlNjQDSS8AgZrI/R9+x5ilkUVFxXcA1Ebl+gQLc/6mERA4407Xof0R7wEyEuj091CVw==} + + '@emnapi/runtime@1.4.3': + resolution: {integrity: sha512-pBPWdu6MLKROBX05wSNKcNb++m5Er+KQ9QkB+WVM+pW2Kx9hoSrVTnu3BdkI5eBLZoKu/J6mW/B6i6bJB2ytXQ==} + + '@esbuild/aix-ppc64@0.25.3': + resolution: {integrity: sha512-W8bFfPA8DowP8l//sxjJLSLkD8iEjMc7cBVyP+u4cEv9sM7mdUCkgsj+t0n/BWPFtv7WWCN5Yzj0N6FJNUUqBQ==} + engines: {node: '>=18'} + cpu: [ppc64] + os: [aix] + + '@esbuild/android-arm64@0.25.3': + resolution: {integrity: sha512-XelR6MzjlZuBM4f5z2IQHK6LkK34Cvv6Rj2EntER3lwCBFdg6h2lKbtRjpTTsdEjD/WSe1q8UyPBXP1x3i/wYQ==} + engines: {node: '>=18'} + cpu: [arm64] + os: [android] + + '@esbuild/android-arm@0.25.3': + resolution: {integrity: sha512-PuwVXbnP87Tcff5I9ngV0lmiSu40xw1At6i3GsU77U7cjDDB4s0X2cyFuBiDa1SBk9DnvWwnGvVaGBqoFWPb7A==} + engines: {node: '>=18'} + cpu: [arm] + os: [android] + + '@esbuild/android-x64@0.25.3': + resolution: {integrity: sha512-ogtTpYHT/g1GWS/zKM0cc/tIebFjm1F9Aw1boQ2Y0eUQ+J89d0jFY//s9ei9jVIlkYi8AfOjiixcLJSGNSOAdQ==} + engines: {node: '>=18'} + cpu: [x64] + os: [android] + + '@esbuild/darwin-arm64@0.25.3': + resolution: {integrity: sha512-eESK5yfPNTqpAmDfFWNsOhmIOaQA59tAcF/EfYvo5/QWQCzXn5iUSOnqt3ra3UdzBv073ykTtmeLJZGt3HhA+w==} + engines: {node: '>=18'} + cpu: [arm64] + os: [darwin] + + '@esbuild/darwin-x64@0.25.3': + resolution: {integrity: sha512-Kd8glo7sIZtwOLcPbW0yLpKmBNWMANZhrC1r6K++uDR2zyzb6AeOYtI6udbtabmQpFaxJ8uduXMAo1gs5ozz8A==} + engines: {node: '>=18'} + cpu: [x64] + os: [darwin] + + '@esbuild/freebsd-arm64@0.25.3': + resolution: {integrity: sha512-EJiyS70BYybOBpJth3M0KLOus0n+RRMKTYzhYhFeMwp7e/RaajXvP+BWlmEXNk6uk+KAu46j/kaQzr6au+JcIw==} + engines: {node: '>=18'} + cpu: [arm64] + os: [freebsd] + + '@esbuild/freebsd-x64@0.25.3': + resolution: {integrity: sha512-Q+wSjaLpGxYf7zC0kL0nDlhsfuFkoN+EXrx2KSB33RhinWzejOd6AvgmP5JbkgXKmjhmpfgKZq24pneodYqE8Q==} + engines: {node: '>=18'} + cpu: [x64] + os: [freebsd] + + '@esbuild/linux-arm64@0.25.3': + resolution: {integrity: sha512-xCUgnNYhRD5bb1C1nqrDV1PfkwgbswTTBRbAd8aH5PhYzikdf/ddtsYyMXFfGSsb/6t6QaPSzxtbfAZr9uox4A==} + engines: {node: '>=18'} + cpu: [arm64] + os: [linux] + + '@esbuild/linux-arm@0.25.3': + resolution: {integrity: sha512-dUOVmAUzuHy2ZOKIHIKHCm58HKzFqd+puLaS424h6I85GlSDRZIA5ycBixb3mFgM0Jdh+ZOSB6KptX30DD8YOQ==} + engines: {node: '>=18'} + cpu: [arm] + os: [linux] + + '@esbuild/linux-ia32@0.25.3': + resolution: {integrity: sha512-yplPOpczHOO4jTYKmuYuANI3WhvIPSVANGcNUeMlxH4twz/TeXuzEP41tGKNGWJjuMhotpGabeFYGAOU2ummBw==} + engines: {node: '>=18'} + cpu: [ia32] + os: [linux] + + '@esbuild/linux-loong64@0.25.3': + resolution: {integrity: sha512-P4BLP5/fjyihmXCELRGrLd793q/lBtKMQl8ARGpDxgzgIKJDRJ/u4r1A/HgpBpKpKZelGct2PGI4T+axcedf6g==} + engines: {node: '>=18'} + cpu: [loong64] + os: [linux] + + '@esbuild/linux-mips64el@0.25.3': + resolution: {integrity: sha512-eRAOV2ODpu6P5divMEMa26RRqb2yUoYsuQQOuFUexUoQndm4MdpXXDBbUoKIc0iPa4aCO7gIhtnYomkn2x+bag==} + engines: {node: '>=18'} + cpu: [mips64el] + os: [linux] + + '@esbuild/linux-ppc64@0.25.3': + resolution: {integrity: sha512-ZC4jV2p7VbzTlnl8nZKLcBkfzIf4Yad1SJM4ZMKYnJqZFD4rTI+pBG65u8ev4jk3/MPwY9DvGn50wi3uhdaghg==} + engines: {node: '>=18'} + cpu: [ppc64] + os: [linux] + + '@esbuild/linux-riscv64@0.25.3': + resolution: {integrity: sha512-LDDODcFzNtECTrUUbVCs6j9/bDVqy7DDRsuIXJg6so+mFksgwG7ZVnTruYi5V+z3eE5y+BJZw7VvUadkbfg7QA==} + engines: {node: '>=18'} + cpu: [riscv64] + os: [linux] + + '@esbuild/linux-s390x@0.25.3': + resolution: {integrity: sha512-s+w/NOY2k0yC2p9SLen+ymflgcpRkvwwa02fqmAwhBRI3SC12uiS10edHHXlVWwfAagYSY5UpmT/zISXPMW3tQ==} + engines: {node: '>=18'} + cpu: [s390x] + os: [linux] + + '@esbuild/linux-x64@0.25.3': + resolution: {integrity: sha512-nQHDz4pXjSDC6UfOE1Fw9Q8d6GCAd9KdvMZpfVGWSJztYCarRgSDfOVBY5xwhQXseiyxapkiSJi/5/ja8mRFFA==} + engines: {node: '>=18'} + cpu: [x64] + os: [linux] + + '@esbuild/netbsd-arm64@0.25.3': + resolution: {integrity: sha512-1QaLtOWq0mzK6tzzp0jRN3eccmN3hezey7mhLnzC6oNlJoUJz4nym5ZD7mDnS/LZQgkrhEbEiTn515lPeLpgWA==} + engines: {node: '>=18'} + cpu: [arm64] + os: [netbsd] + + '@esbuild/netbsd-x64@0.25.3': + resolution: {integrity: sha512-i5Hm68HXHdgv8wkrt+10Bc50zM0/eonPb/a/OFVfB6Qvpiirco5gBA5bz7S2SHuU+Y4LWn/zehzNX14Sp4r27g==} + engines: {node: '>=18'} + cpu: [x64] + os: [netbsd] + + '@esbuild/openbsd-arm64@0.25.3': + resolution: {integrity: sha512-zGAVApJEYTbOC6H/3QBr2mq3upG/LBEXr85/pTtKiv2IXcgKV0RT0QA/hSXZqSvLEpXeIxah7LczB4lkiYhTAQ==} + engines: {node: '>=18'} + cpu: [arm64] + os: [openbsd] + + '@esbuild/openbsd-x64@0.25.3': + resolution: {integrity: sha512-fpqctI45NnCIDKBH5AXQBsD0NDPbEFczK98hk/aa6HJxbl+UtLkJV2+Bvy5hLSLk3LHmqt0NTkKNso1A9y1a4w==} + engines: {node: '>=18'} + cpu: [x64] + os: [openbsd] + + '@esbuild/sunos-x64@0.25.3': + resolution: {integrity: sha512-ROJhm7d8bk9dMCUZjkS8fgzsPAZEjtRJqCAmVgB0gMrvG7hfmPmz9k1rwO4jSiblFjYmNvbECL9uhaPzONMfgA==} + engines: {node: '>=18'} + cpu: [x64] + os: [sunos] + + '@esbuild/win32-arm64@0.25.3': + resolution: {integrity: sha512-YWcow8peiHpNBiIXHwaswPnAXLsLVygFwCB3A7Bh5jRkIBFWHGmNQ48AlX4xDvQNoMZlPYzjVOQDYEzWCqufMQ==} + engines: {node: '>=18'} + cpu: [arm64] + os: [win32] + + '@esbuild/win32-ia32@0.25.3': + resolution: {integrity: sha512-qspTZOIGoXVS4DpNqUYUs9UxVb04khS1Degaw/MnfMe7goQ3lTfQ13Vw4qY/Nj0979BGvMRpAYbs/BAxEvU8ew==} + engines: {node: '>=18'} + cpu: [ia32] + os: [win32] + + '@esbuild/win32-x64@0.25.3': + resolution: {integrity: sha512-ICgUR+kPimx0vvRzf+N/7L7tVSQeE3BYY+NhHRHXS1kBuPO7z2+7ea2HbhDyZdTephgvNvKrlDDKUexuCVBVvg==} + engines: {node: '>=18'} + cpu: [x64] + os: [win32] + + '@expressive-code/core@0.41.3': + resolution: {integrity: sha512-9qzohqU7O0+JwMEEgQhnBPOw5DtsQRBXhW++5fvEywsuX44vCGGof1SL5OvPElvNgaWZ4pFZAFSlkNOkGyLwSQ==} + + '@expressive-code/plugin-collapsible-sections@0.41.3': + resolution: {integrity: sha512-cuHIN7Ipl7gUcaWFfsgy6G3wn0Svk8dQ6WKXNQha63BURbm7CSBhD6y9qFGeIOrxaJtvH4Pj3Xb4C2Ni0OVwYA==} + + '@expressive-code/plugin-frames@0.41.3': + resolution: {integrity: sha512-rFQtmf/3N2CK3Cq/uERweMTYZnBu+CwxBdHuOftEmfA9iBE7gTVvwpbh82P9ZxkPLvc40UMhYt7uNuAZexycRQ==} + + '@expressive-code/plugin-line-numbers@0.41.3': + resolution: {integrity: sha512-eig82a4CRC3XgVPQ2S/TMDcLiHJokOCD/mAdNVImpD3segVewxfjGgtj5DXQRo0E0q6f0R0EH34YzTFl5CEPqg==} + + '@expressive-code/plugin-shiki@0.41.3': + resolution: {integrity: sha512-RlTARoopzhFJIOVHLGvuXJ8DCEme/hjV+ZnRJBIxzxsKVpGPW4Oshqg9xGhWTYdHstTsxO663s0cdBLzZj9TQA==} + + '@expressive-code/plugin-text-markers@0.41.3': + resolution: {integrity: sha512-SN8tkIzDpA0HLAscEYD2IVrfLiid6qEdE9QLlGVSxO1KEw7qYvjpbNBQjUjMr5/jvTJ7ys6zysU2vLPHE0sb2g==} + + '@fancyapps/ui@6.0.5': + resolution: {integrity: sha512-pDowgTxM57wj8Q9XfgVbl4MKpr0Nb71LnGxSZIHngEXgkd7751+3tPdRyGzF1n2bXhted6zM9CQ7Q0SVV4cH2A==} + + '@fontsource-variable/jetbrains-mono@5.2.5': + resolution: {integrity: sha512-G3sN1xq1moZd0JL+hFaA4MEdsiQS+JXC/z7m+EqA5/Fzn5CQlXGUaaNKFGQdDsFuLTnCfW0KOOSWHjygNfjEPw==} + + '@fontsource/roboto@5.2.5': + resolution: {integrity: sha512-70r2UZ0raqLn5W+sPeKhqlf8wGvUXFWlofaDlcbt/S3d06+17gXKr3VNqDODB0I1ASme3dGT5OJj9NABt7OTZQ==} + + '@iconify-json/fa6-brands@1.2.5': + resolution: {integrity: sha512-U/iFfziz6jSN9zArOJZYTtoj2tQyh6MxPdI8M84DQ2kEulPaj8j+h9bqvjmzszNHmD7v+kmmKd/MLkMKk+3Zuw==} + + '@iconify-json/fa6-regular@1.2.3': + resolution: {integrity: sha512-NGV2j5mn4j49mpP7MABCljp1WPWKjwgfjN6NGwsmlFuQ7bdnz36nZB/5aRXLOcT36NXaBeCoT05G9aatdtMr3A==} + + '@iconify-json/fa6-solid@1.2.3': + resolution: {integrity: sha512-C5o8YJF+ekrS4wRb/6/0SE2KjRyJlCg++IOVC/fineiRinITivsmzFRNW1MQX2xfDZ1T7bxeKxLN6lcaTG3jGA==} + + '@iconify-json/material-symbols-light@1.2.49': + resolution: {integrity: sha512-EpKeZ9NifWfU0mfxC7eULjuVtbRdbgg0cNDOlJZucKulC4bTvCcmlNtK5wqsyRICKi4xcfHlSTsmBMiFjF7GOQ==} + + '@iconify-json/material-symbols@1.2.20': + resolution: {integrity: sha512-+KqOT+3fD+LC2FbWiV8gd4+JLMiVUtmqrjzpKN1ji7rfMQTwvYJ94RT0WQlmL+vfDNJ5MTRe3rBzzJyvIH/aSg==} + + '@iconify-json/simple-icons@1.2.42': + resolution: {integrity: sha512-G/EED0hUV1wMNUsWaFdQYLibm6SO7rP2GZP1+CvhszB5WAFYYibD3zoWp3X96xSIWpYQFvccvE17ewpd0Q1hWQ==} + + '@iconify/svelte@4.2.0': + resolution: {integrity: sha512-fEl0T7SAPonK7xk6xUlRPDmFDZVDe2Z7ZstlqeDS/sS8ve2uyU+Qa8rTWbIqzZJlRvONkK5kVXiUf9nIc+6OOQ==} + peerDependencies: + svelte: '>4.0.0' + + '@iconify/tools@4.1.2': + resolution: {integrity: sha512-q6NzLQYEN9zkDfcyBqD3vItHcZw97w/s++3H3TBxUORr57EfHxj6tOW6fyufDjMq+Vl56WXWaPx1csBPYlI5CA==} + + '@iconify/types@2.0.0': + resolution: {integrity: sha512-+wluvCrRhXrhyOmRDJ3q8mux9JkKy5SJ/v8ol2tu4FVjyYvtEzkc/3pK15ET6RKg4b4w4BmTk1+gsCUhf21Ykg==} + + '@iconify/utils@2.3.0': + resolution: {integrity: sha512-GmQ78prtwYW6EtzXRU1rY+KwOKfz32PD7iJh6Iyqw68GiKuoZ2A6pRtzWONz5VQJbp50mEjXh/7NkumtrAgRKA==} + + '@img/sharp-darwin-arm64@0.33.5': + resolution: {integrity: sha512-UT4p+iz/2H4twwAoLCqfA9UH5pI6DggwKEGuaPy7nCVQ8ZsiY5PIcrRvD1DzuY3qYL07NtIQcWnBSY/heikIFQ==} + engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0} + cpu: [arm64] + os: [darwin] + + '@img/sharp-darwin-arm64@0.34.1': + resolution: {integrity: sha512-pn44xgBtgpEbZsu+lWf2KNb6OAf70X68k+yk69Ic2Xz11zHR/w24/U49XT7AeRwJ0Px+mhALhU5LPci1Aymk7A==} + engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0} + cpu: [arm64] + os: [darwin] + + '@img/sharp-darwin-x64@0.33.5': + resolution: {integrity: sha512-fyHac4jIc1ANYGRDxtiqelIbdWkIuQaI84Mv45KvGRRxSAa7o7d1ZKAOBaYbnepLC1WqxfpimdeWfvqqSGwR2Q==} + engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0} + cpu: [x64] + os: [darwin] + + '@img/sharp-darwin-x64@0.34.1': + resolution: {integrity: sha512-VfuYgG2r8BpYiOUN+BfYeFo69nP/MIwAtSJ7/Zpxc5QF3KS22z8Pvg3FkrSFJBPNQ7mmcUcYQFBmEQp7eu1F8Q==} + engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0} + cpu: [x64] + os: [darwin] + + '@img/sharp-libvips-darwin-arm64@1.0.4': + resolution: {integrity: sha512-XblONe153h0O2zuFfTAbQYAX2JhYmDHeWikp1LM9Hul9gVPjFY427k6dFEcOL72O01QxQsWi761svJ/ev9xEDg==} + cpu: [arm64] + os: [darwin] + + '@img/sharp-libvips-darwin-arm64@1.1.0': + resolution: {integrity: sha512-HZ/JUmPwrJSoM4DIQPv/BfNh9yrOA8tlBbqbLz4JZ5uew2+o22Ik+tHQJcih7QJuSa0zo5coHTfD5J8inqj9DA==} + cpu: [arm64] + os: [darwin] + + '@img/sharp-libvips-darwin-x64@1.0.4': + resolution: {integrity: sha512-xnGR8YuZYfJGmWPvmlunFaWJsb9T/AO2ykoP3Fz/0X5XV2aoYBPkX6xqCQvUTKKiLddarLaxpzNe+b1hjeWHAQ==} + cpu: [x64] + os: [darwin] + + '@img/sharp-libvips-darwin-x64@1.1.0': + resolution: {integrity: sha512-Xzc2ToEmHN+hfvsl9wja0RlnXEgpKNmftriQp6XzY/RaSfwD9th+MSh0WQKzUreLKKINb3afirxW7A0fz2YWuQ==} + cpu: [x64] + os: [darwin] + + '@img/sharp-libvips-linux-arm64@1.0.4': + resolution: {integrity: sha512-9B+taZ8DlyyqzZQnoeIvDVR/2F4EbMepXMc/NdVbkzsJbzkUjhXv/70GQJ7tdLA4YJgNP25zukcxpX2/SueNrA==} + cpu: [arm64] + os: [linux] + libc: [glibc] + + '@img/sharp-libvips-linux-arm64@1.1.0': + resolution: {integrity: sha512-IVfGJa7gjChDET1dK9SekxFFdflarnUB8PwW8aGwEoF3oAsSDuNUTYS+SKDOyOJxQyDC1aPFMuRYLoDInyV9Ew==} + cpu: [arm64] + os: [linux] + libc: [glibc] + + '@img/sharp-libvips-linux-arm@1.0.5': + resolution: {integrity: sha512-gvcC4ACAOPRNATg/ov8/MnbxFDJqf/pDePbBnuBDcjsI8PssmjoKMAz4LtLaVi+OnSb5FK/yIOamqDwGmXW32g==} + cpu: [arm] + os: [linux] + libc: [glibc] + + '@img/sharp-libvips-linux-arm@1.1.0': + resolution: {integrity: sha512-s8BAd0lwUIvYCJyRdFqvsj+BJIpDBSxs6ivrOPm/R7piTs5UIwY5OjXrP2bqXC9/moGsyRa37eYWYCOGVXxVrA==} + cpu: [arm] + os: [linux] + libc: [glibc] + + '@img/sharp-libvips-linux-ppc64@1.1.0': + resolution: {integrity: sha512-tiXxFZFbhnkWE2LA8oQj7KYR+bWBkiV2nilRldT7bqoEZ4HiDOcePr9wVDAZPi/Id5fT1oY9iGnDq20cwUz8lQ==} + cpu: [ppc64] + os: [linux] + libc: [glibc] + + '@img/sharp-libvips-linux-s390x@1.0.4': + resolution: {integrity: sha512-u7Wz6ntiSSgGSGcjZ55im6uvTrOxSIS8/dgoVMoiGE9I6JAfU50yH5BoDlYA1tcuGS7g/QNtetJnxA6QEsCVTA==} + cpu: [s390x] + os: [linux] + libc: [glibc] + + '@img/sharp-libvips-linux-s390x@1.1.0': + resolution: {integrity: sha512-xukSwvhguw7COyzvmjydRb3x/09+21HykyapcZchiCUkTThEQEOMtBj9UhkaBRLuBrgLFzQ2wbxdeCCJW/jgJA==} + cpu: [s390x] + os: [linux] + libc: [glibc] + + '@img/sharp-libvips-linux-x64@1.0.4': + resolution: {integrity: sha512-MmWmQ3iPFZr0Iev+BAgVMb3ZyC4KeFc3jFxnNbEPas60e1cIfevbtuyf9nDGIzOaW9PdnDciJm+wFFaTlj5xYw==} + cpu: [x64] + os: [linux] + libc: [glibc] + + '@img/sharp-libvips-linux-x64@1.1.0': + resolution: {integrity: sha512-yRj2+reB8iMg9W5sULM3S74jVS7zqSzHG3Ol/twnAAkAhnGQnpjj6e4ayUz7V+FpKypwgs82xbRdYtchTTUB+Q==} + cpu: [x64] + os: [linux] + libc: [glibc] + + '@img/sharp-libvips-linuxmusl-arm64@1.0.4': + resolution: {integrity: sha512-9Ti+BbTYDcsbp4wfYib8Ctm1ilkugkA/uscUn6UXK1ldpC1JjiXbLfFZtRlBhjPZ5o1NCLiDbg8fhUPKStHoTA==} + cpu: [arm64] + os: [linux] + libc: [musl] + + '@img/sharp-libvips-linuxmusl-arm64@1.1.0': + resolution: {integrity: sha512-jYZdG+whg0MDK+q2COKbYidaqW/WTz0cc1E+tMAusiDygrM4ypmSCjOJPmFTvHHJ8j/6cAGyeDWZOsK06tP33w==} + cpu: [arm64] + os: [linux] + libc: [musl] + + '@img/sharp-libvips-linuxmusl-x64@1.0.4': + resolution: {integrity: sha512-viYN1KX9m+/hGkJtvYYp+CCLgnJXwiQB39damAO7WMdKWlIhmYTfHjwSbQeUK/20vY154mwezd9HflVFM1wVSw==} + cpu: [x64] + os: [linux] + libc: [musl] + + '@img/sharp-libvips-linuxmusl-x64@1.1.0': + resolution: {integrity: sha512-wK7SBdwrAiycjXdkPnGCPLjYb9lD4l6Ze2gSdAGVZrEL05AOUJESWU2lhlC+Ffn5/G+VKuSm6zzbQSzFX/P65A==} + cpu: [x64] + os: [linux] + libc: [musl] + + '@img/sharp-linux-arm64@0.33.5': + resolution: {integrity: sha512-JMVv+AMRyGOHtO1RFBiJy/MBsgz0x4AWrT6QoEVVTyh1E39TrCUpTRI7mx9VksGX4awWASxqCYLCV4wBZHAYxA==} + engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0} + cpu: [arm64] + os: [linux] + libc: [glibc] + + '@img/sharp-linux-arm64@0.34.1': + resolution: {integrity: sha512-kX2c+vbvaXC6vly1RDf/IWNXxrlxLNpBVWkdpRq5Ka7OOKj6nr66etKy2IENf6FtOgklkg9ZdGpEu9kwdlcwOQ==} + engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0} + cpu: [arm64] + os: [linux] + libc: [glibc] + + '@img/sharp-linux-arm@0.33.5': + resolution: {integrity: sha512-JTS1eldqZbJxjvKaAkxhZmBqPRGmxgu+qFKSInv8moZ2AmT5Yib3EQ1c6gp493HvrvV8QgdOXdyaIBrhvFhBMQ==} + engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0} + cpu: [arm] + os: [linux] + libc: [glibc] + + '@img/sharp-linux-arm@0.34.1': + resolution: {integrity: sha512-anKiszvACti2sGy9CirTlNyk7BjjZPiML1jt2ZkTdcvpLU1YH6CXwRAZCA2UmRXnhiIftXQ7+Oh62Ji25W72jA==} + engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0} + cpu: [arm] + os: [linux] + libc: [glibc] + + '@img/sharp-linux-s390x@0.33.5': + resolution: {integrity: sha512-y/5PCd+mP4CA/sPDKl2961b+C9d+vPAveS33s6Z3zfASk2j5upL6fXVPZi7ztePZ5CuH+1kW8JtvxgbuXHRa4Q==} + engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0} + cpu: [s390x] + os: [linux] + libc: [glibc] + + '@img/sharp-linux-s390x@0.34.1': + resolution: {integrity: sha512-7s0KX2tI9mZI2buRipKIw2X1ufdTeaRgwmRabt5bi9chYfhur+/C1OXg3TKg/eag1W+6CCWLVmSauV1owmRPxA==} + engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0} + cpu: [s390x] + os: [linux] + libc: [glibc] + + '@img/sharp-linux-x64@0.33.5': + resolution: {integrity: sha512-opC+Ok5pRNAzuvq1AG0ar+1owsu842/Ab+4qvU879ippJBHvyY5n2mxF1izXqkPYlGuP/M556uh53jRLJmzTWA==} + engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0} + cpu: [x64] + os: [linux] + libc: [glibc] + + '@img/sharp-linux-x64@0.34.1': + resolution: {integrity: sha512-wExv7SH9nmoBW3Wr2gvQopX1k8q2g5V5Iag8Zk6AVENsjwd+3adjwxtp3Dcu2QhOXr8W9NusBU6XcQUohBZ5MA==} + engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0} + cpu: [x64] + os: [linux] + libc: [glibc] + + '@img/sharp-linuxmusl-arm64@0.33.5': + resolution: {integrity: sha512-XrHMZwGQGvJg2V/oRSUfSAfjfPxO+4DkiRh6p2AFjLQztWUuY/o8Mq0eMQVIY7HJ1CDQUJlxGGZRw1a5bqmd1g==} + engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0} + cpu: [arm64] + os: [linux] + libc: [musl] + + '@img/sharp-linuxmusl-arm64@0.34.1': + resolution: {integrity: sha512-DfvyxzHxw4WGdPiTF0SOHnm11Xv4aQexvqhRDAoD00MzHekAj9a/jADXeXYCDFH/DzYruwHbXU7uz+H+nWmSOQ==} + engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0} + cpu: [arm64] + os: [linux] + libc: [musl] + + '@img/sharp-linuxmusl-x64@0.33.5': + resolution: {integrity: sha512-WT+d/cgqKkkKySYmqoZ8y3pxx7lx9vVejxW/W4DOFMYVSkErR+w7mf2u8m/y4+xHe7yY9DAXQMWQhpnMuFfScw==} + engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0} + cpu: [x64] + os: [linux] + libc: [musl] + + '@img/sharp-linuxmusl-x64@0.34.1': + resolution: {integrity: sha512-pax/kTR407vNb9qaSIiWVnQplPcGU8LRIJpDT5o8PdAx5aAA7AS3X9PS8Isw1/WfqgQorPotjrZL3Pqh6C5EBg==} + engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0} + cpu: [x64] + os: [linux] + libc: [musl] + + '@img/sharp-wasm32@0.33.5': + resolution: {integrity: sha512-ykUW4LVGaMcU9lu9thv85CbRMAwfeadCJHRsg2GmeRa/cJxsVY9Rbd57JcMxBkKHag5U/x7TSBpScF4U8ElVzg==} + engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0} + cpu: [wasm32] + + '@img/sharp-wasm32@0.34.1': + resolution: {integrity: sha512-YDybQnYrLQfEpzGOQe7OKcyLUCML4YOXl428gOOzBgN6Gw0rv8dpsJ7PqTHxBnXnwXr8S1mYFSLSa727tpz0xg==} + engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0} + cpu: [wasm32] + + '@img/sharp-win32-ia32@0.33.5': + resolution: {integrity: sha512-T36PblLaTwuVJ/zw/LaH0PdZkRz5rd3SmMHX8GSmR7vtNSP5Z6bQkExdSK7xGWyxLw4sUknBuugTelgw2faBbQ==} + engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0} + cpu: [ia32] + os: [win32] + + '@img/sharp-win32-ia32@0.34.1': + resolution: {integrity: sha512-WKf/NAZITnonBf3U1LfdjoMgNO5JYRSlhovhRhMxXVdvWYveM4kM3L8m35onYIdh75cOMCo1BexgVQcCDzyoWw==} + engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0} + cpu: [ia32] + os: [win32] + + '@img/sharp-win32-x64@0.33.5': + resolution: {integrity: sha512-MpY/o8/8kj+EcnxwvrP4aTJSWw/aZ7JIGR4aBeZkZw5B7/Jn+tY9/VNwtcoGmdT7GfggGIU4kygOMSbYnOrAbg==} + engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0} + cpu: [x64] + os: [win32] + + '@img/sharp-win32-x64@0.34.1': + resolution: {integrity: sha512-hw1iIAHpNE8q3uMIRCgGOeDoz9KtFNarFLQclLxr/LK1VBkj8nby18RjFvr6aP7USRYAjTZW6yisnBWMX571Tw==} + engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0} + cpu: [x64] + os: [win32] + + '@isaacs/balanced-match@4.0.1': + resolution: {integrity: sha512-yzMTt9lEb8Gv7zRioUilSglI0c0smZ9k5D65677DLWLtWJaXIS3CqcGyUFByYKlnUj6TkjLVs54fBl6+TiGQDQ==} + engines: {node: 20 || >=22} + + '@isaacs/brace-expansion@5.0.0': + resolution: {integrity: sha512-ZT55BDLV0yv0RBm2czMiZ+SqCGO7AvmOM3G/w2xhVPH+te0aKgFjmBvGlL1dH+ql2tgGO3MVrbb3jCKyvpgnxA==} + engines: {node: 20 || >=22} + + '@isaacs/cliui@8.0.2': + resolution: {integrity: sha512-O8jcjabXaleOG9DQ0+ARXWZBTfnP4WNAqzuiJK7ll44AmxGKv/J2M4TPjxjY3znBCfvBXFzucm1twdyFybFqEA==} + engines: {node: '>=12'} + + '@isaacs/fs-minipass@4.0.1': + resolution: {integrity: sha512-wgm9Ehl2jpeqP3zw/7mo3kRHFp5MEDhqAdwy1fTGkHAwnkGOVsgpvQhL8B5n1qlb01jV3n/bI0ZfZp5lWA1k4w==} + engines: {node: '>=18.0.0'} + + '@jridgewell/gen-mapping@0.3.8': + resolution: {integrity: sha512-imAbBGkb+ebQyxKgzv5Hu2nmROxoDOXHh80evxdoXNOrvAnVx7zimzc1Oo5h9RlfV4vPXaE2iM5pOFbvOCClWA==} + engines: {node: '>=6.0.0'} + + '@jridgewell/resolve-uri@3.1.2': + resolution: {integrity: sha512-bRISgCIjP20/tbWSPWMEi54QVPRZExkuD9lJL+UIxUKtwVJA8wW1Trb1jMs1RFXo1CBTNZ/5hpC9QvmKWdopKw==} + engines: {node: '>=6.0.0'} + + '@jridgewell/set-array@1.2.1': + resolution: {integrity: sha512-R8gLRTZeyp03ymzP/6Lil/28tGeGEzhx1q2k703KGWRAI1VdvPIXdG70VJc2pAMw3NA6JKL5hhFu1sJX0Mnn/A==} + engines: {node: '>=6.0.0'} + + '@jridgewell/source-map@0.3.6': + resolution: {integrity: sha512-1ZJTZebgqllO79ue2bm3rIGud/bOe0pP5BjSRCRxxYkEZS8STV7zN84UBbiYu7jy+eCKSnVIUgoWWE/tt+shMQ==} + + '@jridgewell/sourcemap-codec@1.5.0': + resolution: {integrity: sha512-gv3ZRaISU3fjPAgNsriBRqGWQL6quFx04YMPW/zD8XMLsU32mhCCbfbO6KZFLjvYpCZ8zyDEgqsgf+PwPaM7GQ==} + + '@jridgewell/trace-mapping@0.3.25': + resolution: {integrity: sha512-vNk6aEwybGtawWmy/PzwnGDOjCkLWSD2wqvjGGAgOAwCGWySYXfYoxt00IJkTF+8Lb57DwOb3Aa0o9CApepiYQ==} + + '@mapbox/node-pre-gyp@2.0.0': + resolution: {integrity: sha512-llMXd39jtP0HpQLVI37Bf1m2ADlEb35GYSh1SDSLsBhR+5iCxiNGlT31yqbNtVHygHAtMy6dWFERpU2JgufhPg==} + engines: {node: '>=18'} + hasBin: true + + '@nodelib/fs.scandir@2.1.5': + resolution: {integrity: sha512-vq24Bq3ym5HEQm2NKCr3yXDwjc7vTsEThRDnkp2DK9p1uqLR+DHurm/NOTo0KG7HYHU7eppKZj3MyqYuMBf62g==} + engines: {node: '>= 8'} + + '@nodelib/fs.stat@2.0.5': + resolution: {integrity: sha512-RkhPPp2zrqDAQA/2jNhnztcPAlv64XdhIp7a7454A5ovI7Bukxgt7MX7udwAu3zg1DcpPU0rz3VV1SeaqvY4+A==} + engines: {node: '>= 8'} + + '@nodelib/fs.walk@1.2.8': + resolution: {integrity: sha512-oGB+UxlgWcgQkgwo8GcEGwemoTFt3FIO9ababBmaGwXIoBKZ+GTy0pP185beGg7Llih/NSHSV2XAs1lnznocSg==} + engines: {node: '>= 8'} + + '@oslojs/encoding@1.1.0': + resolution: {integrity: sha512-70wQhgYmndg4GCPxPPxPGevRKqTIJ2Nh4OkiMWmDAVYsTQ+Ta7Sq+rPevXyXGdzr30/qZBnyOalCszoMxlyldQ==} + + '@parcel/watcher-android-arm64@2.5.1': + resolution: {integrity: sha512-KF8+j9nNbUN8vzOFDpRMsaKBHZ/mcjEjMToVMJOhTozkDonQFFrRcfdLWn6yWKCmJKmdVxSgHiYvTCef4/qcBA==} + engines: {node: '>= 10.0.0'} + cpu: [arm64] + os: [android] + + '@parcel/watcher-darwin-arm64@2.5.1': + resolution: {integrity: sha512-eAzPv5osDmZyBhou8PoF4i6RQXAfeKL9tjb3QzYuccXFMQU0ruIc/POh30ePnaOyD1UXdlKguHBmsTs53tVoPw==} + engines: {node: '>= 10.0.0'} + cpu: [arm64] + os: [darwin] + + '@parcel/watcher-darwin-x64@2.5.1': + resolution: {integrity: sha512-1ZXDthrnNmwv10A0/3AJNZ9JGlzrF82i3gNQcWOzd7nJ8aj+ILyW1MTxVk35Db0u91oD5Nlk9MBiujMlwmeXZg==} + engines: {node: '>= 10.0.0'} + cpu: [x64] + os: [darwin] + + '@parcel/watcher-freebsd-x64@2.5.1': + resolution: {integrity: sha512-SI4eljM7Flp9yPuKi8W0ird8TI/JK6CSxju3NojVI6BjHsTyK7zxA9urjVjEKJ5MBYC+bLmMcbAWlZ+rFkLpJQ==} + engines: {node: '>= 10.0.0'} + cpu: [x64] + os: [freebsd] + + '@parcel/watcher-linux-arm-glibc@2.5.1': + resolution: {integrity: sha512-RCdZlEyTs8geyBkkcnPWvtXLY44BCeZKmGYRtSgtwwnHR4dxfHRG3gR99XdMEdQ7KeiDdasJwwvNSF5jKtDwdA==} + engines: {node: '>= 10.0.0'} + cpu: [arm] + os: [linux] + libc: [glibc] + + '@parcel/watcher-linux-arm-musl@2.5.1': + resolution: {integrity: sha512-6E+m/Mm1t1yhB8X412stiKFG3XykmgdIOqhjWj+VL8oHkKABfu/gjFj8DvLrYVHSBNC+/u5PeNrujiSQ1zwd1Q==} + engines: {node: '>= 10.0.0'} + cpu: [arm] + os: [linux] + libc: [musl] + + '@parcel/watcher-linux-arm64-glibc@2.5.1': + resolution: {integrity: sha512-LrGp+f02yU3BN9A+DGuY3v3bmnFUggAITBGriZHUREfNEzZh/GO06FF5u2kx8x+GBEUYfyTGamol4j3m9ANe8w==} + engines: {node: '>= 10.0.0'} + cpu: [arm64] + os: [linux] + libc: [glibc] + + '@parcel/watcher-linux-arm64-musl@2.5.1': + resolution: {integrity: sha512-cFOjABi92pMYRXS7AcQv9/M1YuKRw8SZniCDw0ssQb/noPkRzA+HBDkwmyOJYp5wXcsTrhxO0zq1U11cK9jsFg==} + engines: {node: '>= 10.0.0'} + cpu: [arm64] + os: [linux] + libc: [musl] + + '@parcel/watcher-linux-x64-glibc@2.5.1': + resolution: {integrity: sha512-GcESn8NZySmfwlTsIur+49yDqSny2IhPeZfXunQi48DMugKeZ7uy1FX83pO0X22sHntJ4Ub+9k34XQCX+oHt2A==} + engines: {node: '>= 10.0.0'} + cpu: [x64] + os: [linux] + libc: [glibc] + + '@parcel/watcher-linux-x64-musl@2.5.1': + resolution: {integrity: sha512-n0E2EQbatQ3bXhcH2D1XIAANAcTZkQICBPVaxMeaCVBtOpBZpWJuf7LwyWPSBDITb7In8mqQgJ7gH8CILCURXg==} + engines: {node: '>= 10.0.0'} + cpu: [x64] + os: [linux] + libc: [musl] + + '@parcel/watcher-win32-arm64@2.5.1': + resolution: {integrity: sha512-RFzklRvmc3PkjKjry3hLF9wD7ppR4AKcWNzH7kXR7GUe0Igb3Nz8fyPwtZCSquGrhU5HhUNDr/mKBqj7tqA2Vw==} + engines: {node: '>= 10.0.0'} + cpu: [arm64] + os: [win32] + + '@parcel/watcher-win32-ia32@2.5.1': + resolution: {integrity: sha512-c2KkcVN+NJmuA7CGlaGD1qJh1cLfDnQsHjE89E60vUEMlqduHGCdCLJCID5geFVM0dOtA3ZiIO8BoEQmzQVfpQ==} + engines: {node: '>= 10.0.0'} + cpu: [ia32] + os: [win32] + + '@parcel/watcher-win32-x64@2.5.1': + resolution: {integrity: sha512-9lHBdJITeNR++EvSQVUcaZoWupyHfXe1jZvGZ06O/5MflPcuPLtEphScIBL+AiCWBO46tDSHzWyD0uDmmZqsgA==} + engines: {node: '>= 10.0.0'} + cpu: [x64] + os: [win32] + + '@parcel/watcher@2.5.1': + resolution: {integrity: sha512-dfUnCxiN9H4ap84DvD2ubjw+3vUNpstxa0TneY/Paat8a3R4uQZDLSvWjmznAY/DoahqTHl9V46HF/Zs3F29pg==} + engines: {node: '>= 10.0.0'} + + '@pkgjs/parseargs@0.11.0': + resolution: {integrity: sha512-+1VkjdD0QBLPodGrJUeqarH8VAIvQODIbwh9XpP5Syisf7YoQgsJKPNFoqqLQlu+VQ/tVSshMR6loPMn8U+dPg==} + engines: {node: '>=14'} + + '@rollup/plugin-alias@3.1.9': + resolution: {integrity: sha512-QI5fsEvm9bDzt32k39wpOwZhVzRcL5ydcffUHMyLVaVaLeC70I8TJZ17F1z1eMoLu4E/UOcH9BWVkKpIKdrfiw==} + engines: {node: '>=8.0.0'} + peerDependencies: + rollup: ^1.20.0||^2.0.0 + + '@rollup/plugin-babel@5.3.1': + resolution: {integrity: sha512-WFfdLWU/xVWKeRQnKmIAQULUI7Il0gZnBIH/ZFO069wYIfPu+8zrfp/KMW0atmELoRDq8FbiP3VCss9MhCut7Q==} + engines: {node: '>= 10.0.0'} + peerDependencies: + '@babel/core': ^7.0.0 + '@types/babel__core': ^7.1.9 + rollup: ^1.20.0||^2.0.0 + peerDependenciesMeta: + '@types/babel__core': + optional: true + + '@rollup/plugin-commonjs@17.1.0': + resolution: {integrity: sha512-PoMdXCw0ZyvjpCMT5aV4nkL0QywxP29sODQsSGeDpr/oI49Qq9tRtAsb/LbYbDzFlOydVEqHmmZWFtXJEAX9ew==} + engines: {node: '>= 8.0.0'} + peerDependencies: + rollup: ^2.30.0 + + '@rollup/plugin-json@4.1.0': + resolution: {integrity: sha512-yfLbTdNS6amI/2OpmbiBoW12vngr5NW2jCJVZSBEz+H5KfUJZ2M7sDjk0U6GOOdCWFVScShte29o9NezJ53TPw==} + peerDependencies: + rollup: ^1.20.0 || ^2.0.0 + + '@rollup/plugin-node-resolve@11.2.1': + resolution: {integrity: sha512-yc2n43jcqVyGE2sqV5/YCmocy9ArjVAP/BeXyTtADTBBX6V0e5UMqwO8CdQ0kzjb6zu5P1qMzsScCMRvE9OlVg==} + engines: {node: '>= 10.0.0'} + peerDependencies: + rollup: ^1.20.0||^2.0.0 + + '@rollup/plugin-yaml@4.1.2': + resolution: {integrity: sha512-RpupciIeZMUqhgFE97ba0s98mOFS7CWzN3EJNhJkqSv9XLlWYtwVdtE6cDw6ASOF/sZVFS7kRJXftaqM2Vakdw==} + engines: {node: '>=14.0.0'} + peerDependencies: + rollup: ^1.20.0||^2.0.0||^3.0.0||^4.0.0 + peerDependenciesMeta: + rollup: + optional: true + + '@rollup/pluginutils@3.1.0': + resolution: {integrity: sha512-GksZ6pr6TpIjHm8h9lSQ8pi8BE9VeubNT0OMJ3B5uZJ8pz73NPiqOtCog/x2/QzM1ENChPKxMDhiQuRHsqc+lg==} + engines: {node: '>= 8.0.0'} + peerDependencies: + rollup: ^1.20.0||^2.0.0 + + '@rollup/pluginutils@4.2.1': + resolution: {integrity: sha512-iKnFXr7NkdZAIHiIWE+BX5ULi/ucVFYWD6TbAV+rZctiRTY2PL6tsIKhoIOaoskiWAkgu+VsbXgUVDNLHf+InQ==} + engines: {node: '>= 8.0.0'} + + '@rollup/pluginutils@5.1.4': + resolution: {integrity: sha512-USm05zrsFxYLPdWWq+K3STlWiT/3ELn3RcV5hJMghpeAIhxfsUIg6mt12CBJBInWMV4VneoV7SfGv8xIwo2qNQ==} + engines: {node: '>=14.0.0'} + peerDependencies: + rollup: ^1.20.0||^2.0.0||^3.0.0||^4.0.0 + peerDependenciesMeta: + rollup: + optional: true + + '@rollup/rollup-android-arm-eabi@4.40.1': + resolution: {integrity: sha512-kxz0YeeCrRUHz3zyqvd7n+TVRlNyTifBsmnmNPtk3hQURUyG9eAB+usz6DAwagMusjx/zb3AjvDUvhFGDAexGw==} + cpu: [arm] + os: [android] + + '@rollup/rollup-android-arm64@4.40.1': + resolution: {integrity: sha512-PPkxTOisoNC6TpnDKatjKkjRMsdaWIhyuMkA4UsBXT9WEZY4uHezBTjs6Vl4PbqQQeu6oION1w2voYZv9yquCw==} + cpu: [arm64] + os: [android] + + '@rollup/rollup-darwin-arm64@4.40.1': + resolution: {integrity: sha512-VWXGISWFY18v/0JyNUy4A46KCFCb9NVsH+1100XP31lud+TzlezBbz24CYzbnA4x6w4hx+NYCXDfnvDVO6lcAA==} + cpu: [arm64] + os: [darwin] + + '@rollup/rollup-darwin-x64@4.40.1': + resolution: {integrity: sha512-nIwkXafAI1/QCS7pxSpv/ZtFW6TXcNUEHAIA9EIyw5OzxJZQ1YDrX+CL6JAIQgZ33CInl1R6mHet9Y/UZTg2Bw==} + cpu: [x64] + os: [darwin] + + '@rollup/rollup-freebsd-arm64@4.40.1': + resolution: {integrity: sha512-BdrLJ2mHTrIYdaS2I99mriyJfGGenSaP+UwGi1kB9BLOCu9SR8ZpbkmmalKIALnRw24kM7qCN0IOm6L0S44iWw==} + cpu: [arm64] + os: [freebsd] + + '@rollup/rollup-freebsd-x64@4.40.1': + resolution: {integrity: sha512-VXeo/puqvCG8JBPNZXZf5Dqq7BzElNJzHRRw3vjBE27WujdzuOPecDPc/+1DcdcTptNBep3861jNq0mYkT8Z6Q==} + cpu: [x64] + os: [freebsd] + + '@rollup/rollup-linux-arm-gnueabihf@4.40.1': + resolution: {integrity: sha512-ehSKrewwsESPt1TgSE/na9nIhWCosfGSFqv7vwEtjyAqZcvbGIg4JAcV7ZEh2tfj/IlfBeZjgOXm35iOOjadcg==} + cpu: [arm] + os: [linux] + libc: [glibc] + + '@rollup/rollup-linux-arm-musleabihf@4.40.1': + resolution: {integrity: sha512-m39iO/aaurh5FVIu/F4/Zsl8xppd76S4qoID8E+dSRQvTyZTOI2gVk3T4oqzfq1PtcvOfAVlwLMK3KRQMaR8lg==} + cpu: [arm] + os: [linux] + libc: [musl] + + '@rollup/rollup-linux-arm64-gnu@4.40.1': + resolution: {integrity: sha512-Y+GHnGaku4aVLSgrT0uWe2o2Rq8te9hi+MwqGF9r9ORgXhmHK5Q71N757u0F8yU1OIwUIFy6YiJtKjtyktk5hg==} + cpu: [arm64] + os: [linux] + libc: [glibc] + + '@rollup/rollup-linux-arm64-musl@4.40.1': + resolution: {integrity: sha512-jEwjn3jCA+tQGswK3aEWcD09/7M5wGwc6+flhva7dsQNRZZTe30vkalgIzV4tjkopsTS9Jd7Y1Bsj6a4lzz8gQ==} + cpu: [arm64] + os: [linux] + libc: [musl] + + '@rollup/rollup-linux-loongarch64-gnu@4.40.1': + resolution: {integrity: sha512-ySyWikVhNzv+BV/IDCsrraOAZ3UaC8SZB67FZlqVwXwnFhPihOso9rPOxzZbjp81suB1O2Topw+6Ug3JNegejQ==} + cpu: [loong64] + os: [linux] + libc: [glibc] + + '@rollup/rollup-linux-powerpc64le-gnu@4.40.1': + resolution: {integrity: sha512-BvvA64QxZlh7WZWqDPPdt0GH4bznuL6uOO1pmgPnnv86rpUpc8ZxgZwcEgXvo02GRIZX1hQ0j0pAnhwkhwPqWg==} + cpu: [ppc64] + os: [linux] + libc: [glibc] + + '@rollup/rollup-linux-riscv64-gnu@4.40.1': + resolution: {integrity: sha512-EQSP+8+1VuSulm9RKSMKitTav89fKbHymTf25n5+Yr6gAPZxYWpj3DzAsQqoaHAk9YX2lwEyAf9S4W8F4l3VBQ==} + cpu: [riscv64] + os: [linux] + libc: [glibc] + + '@rollup/rollup-linux-riscv64-musl@4.40.1': + resolution: {integrity: sha512-n/vQ4xRZXKuIpqukkMXZt9RWdl+2zgGNx7Uda8NtmLJ06NL8jiHxUawbwC+hdSq1rrw/9CghCpEONor+l1e2gA==} + cpu: [riscv64] + os: [linux] + libc: [musl] + + '@rollup/rollup-linux-s390x-gnu@4.40.1': + resolution: {integrity: sha512-h8d28xzYb98fMQKUz0w2fMc1XuGzLLjdyxVIbhbil4ELfk5/orZlSTpF/xdI9C8K0I8lCkq+1En2RJsawZekkg==} + cpu: [s390x] + os: [linux] + libc: [glibc] + + '@rollup/rollup-linux-x64-gnu@4.40.1': + resolution: {integrity: sha512-XiK5z70PEFEFqcNj3/zRSz/qX4bp4QIraTy9QjwJAb/Z8GM7kVUsD0Uk8maIPeTyPCP03ChdI+VVmJriKYbRHQ==} + cpu: [x64] + os: [linux] + libc: [glibc] + + '@rollup/rollup-linux-x64-musl@4.40.1': + resolution: {integrity: sha512-2BRORitq5rQ4Da9blVovzNCMaUlyKrzMSvkVR0D4qPuOy/+pMCrh1d7o01RATwVy+6Fa1WBw+da7QPeLWU/1mQ==} + cpu: [x64] + os: [linux] + libc: [musl] + + '@rollup/rollup-win32-arm64-msvc@4.40.1': + resolution: {integrity: sha512-b2bcNm9Kbde03H+q+Jjw9tSfhYkzrDUf2d5MAd1bOJuVplXvFhWz7tRtWvD8/ORZi7qSCy0idW6tf2HgxSXQSg==} + cpu: [arm64] + os: [win32] + + '@rollup/rollup-win32-ia32-msvc@4.40.1': + resolution: {integrity: sha512-DfcogW8N7Zg7llVEfpqWMZcaErKfsj9VvmfSyRjCyo4BI3wPEfrzTtJkZG6gKP/Z92wFm6rz2aDO7/JfiR/whA==} + cpu: [ia32] + os: [win32] + + '@rollup/rollup-win32-x64-msvc@4.40.1': + resolution: {integrity: sha512-ECyOuDeH3C1I8jH2MK1RtBJW+YPMvSfT0a5NN0nHfQYnDSJ6tUiZH3gzwVP5/Kfh/+Tt7tpWVF9LXNTnhTJ3kA==} + cpu: [x64] + os: [win32] + + '@shikijs/core@3.3.0': + resolution: {integrity: sha512-CovkFL2WVaHk6PCrwv6ctlmD4SS1qtIfN8yEyDXDYWh4ONvomdM9MaFw20qHuqJOcb8/xrkqoWQRJ//X10phOQ==} + + '@shikijs/engine-javascript@3.3.0': + resolution: {integrity: sha512-XlhnFGv0glq7pfsoN0KyBCz9FJU678LZdQ2LqlIdAj6JKsg5xpYKay3DkazXWExp3DTJJK9rMOuGzU2911pg7Q==} + + '@shikijs/engine-oniguruma@3.3.0': + resolution: {integrity: sha512-l0vIw+GxeNU7uGnsu6B+Crpeqf+WTQ2Va71cHb5ZYWEVEPdfYwY5kXwYqRJwHrxz9WH+pjSpXQz+TJgAsrkA5A==} + + '@shikijs/langs@3.3.0': + resolution: {integrity: sha512-zt6Kf/7XpBQKSI9eqku+arLkAcDQ3NHJO6zFjiChI8w0Oz6Jjjay7pToottjQGjSDCFk++R85643WbyINcuL+g==} + + '@shikijs/themes@3.3.0': + resolution: {integrity: sha512-tXeCvLXBnqq34B0YZUEaAD1lD4lmN6TOHAhnHacj4Owh7Ptb/rf5XCDeROZt2rEOk5yuka3OOW2zLqClV7/SOg==} + + '@shikijs/types@3.3.0': + resolution: {integrity: sha512-KPCGnHG6k06QG/2pnYGbFtFvpVJmC3uIpXrAiPrawETifujPBv0Se2oUxm5qYgjCvGJS9InKvjytOdN+bGuX+Q==} + + '@shikijs/vscode-textmate@10.0.2': + resolution: {integrity: sha512-83yeghZ2xxin3Nj8z1NMd/NCuca+gsYXswywDy5bHvwlWL8tpTQmzGeUuHd9FC3E/SBEMvzJRwWEOz5gGes9Qg==} + + '@surma/rollup-plugin-off-main-thread@2.2.3': + resolution: {integrity: sha512-lR8q/9W7hZpMWweNiAKU7NQerBnzQQLvi8qnTDU/fxItPhtZVMbPV3lbCwjhIlNBe9Bbr5V+KHshvWmVSG9cxQ==} + + '@sveltejs/acorn-typescript@1.0.5': + resolution: {integrity: sha512-IwQk4yfwLdibDlrXVE04jTZYlLnwsTT2PIOQQGNLWfjavGifnk1JD1LcZjZaBTRcxZu2FfPfNLOE04DSu9lqtQ==} + peerDependencies: + acorn: ^8.9.0 + + '@sveltejs/vite-plugin-svelte-inspector@4.0.1': + resolution: {integrity: sha512-J/Nmb2Q2y7mck2hyCX4ckVHcR5tu2J+MtBEQqpDrrgELZ2uvraQcK/ioCV61AqkdXFgriksOKIceDcQmqnGhVw==} + engines: {node: ^18.0.0 || ^20.0.0 || >=22} + peerDependencies: + '@sveltejs/vite-plugin-svelte': ^5.0.0 + svelte: ^5.0.0 + vite: ^6.0.0 + + '@sveltejs/vite-plugin-svelte@5.0.3': + resolution: {integrity: sha512-MCFS6CrQDu1yGwspm4qtli0e63vaPCehf6V7pIMP15AsWgMKrqDGCPFF/0kn4SP0ii4aySu4Pa62+fIRGFMjgw==} + engines: {node: ^18.0.0 || ^20.0.0 || >=22} + peerDependencies: + svelte: ^5.0.0 + vite: ^6.0.0 + + '@swc/helpers@0.5.17': + resolution: {integrity: sha512-5IKx/Y13RsYd+sauPb2x+U/xZikHjolzfuDgTAl/Tdf3Q8rslRvC19NKDLgAJQ6wsqADk10ntlv08nPFw/gO/A==} + + '@swup/a11y-plugin@5.0.0': + resolution: {integrity: sha512-t0pVWAVea+Imjj05n9QMPfqZhw9i5rx7BV/l3Ejeic+X3Qs0VZwVvEJIcdqireCVJgAJGZAPlmgbiuaj5UHJaQ==} + peerDependencies: + swup: ^4.0.0 + + '@swup/astro@1.6.0': + resolution: {integrity: sha512-xj87BM/tAJSDWG8gwr4Tx7H7FIkimhnAlHP2/xM7oF32BAu9dTVXNVJwX1sNcNv9estAyHFygzuxz4Jug1ZATg==} + + '@swup/body-class-plugin@3.3.0': + resolution: {integrity: sha512-4h/6mAgDd0+ml8Gc2kX8tikIR0HZZLF+WTnfm0JTVrGDw74bgEBenbsaWFfIS+gn4RWKZdWBUOfaZ+Im5J7Gvw==} + peerDependencies: + swup: ^4.6.0 + + '@swup/browserslist-config@1.0.1': + resolution: {integrity: sha512-/3nBqG7LqmK1uqaCSTA6s2NwQBDQXNyLAFBzlX6uaxqjIQcAZyq6K+sgcQ40oj02Vn/2mLSkeL9DOfP7BPOwVA==} + + '@swup/debug-plugin@4.1.0': + resolution: {integrity: sha512-R+RP4hjoeXrO+wowSswaTM4P05lu6iDg6UwdbQn0wfJ082KiUpH+DDwHYNOatxusKVExFuhLec+X9IbyAyiv8w==} + peerDependencies: + swup: ^4.0.0 + + '@swup/fade-theme@2.0.1': + resolution: {integrity: sha512-viQ23vcv47lTz2nYFSDqYR0cbfAEvWOtsNepERGDD221guwSHU7lNz3zDfvWsFGSjP/nCmMUGS5p204rizkdgw==} + peerDependencies: + swup: ^4.0.0 + + '@swup/forms-plugin@3.6.0': + resolution: {integrity: sha512-pRN6OR5wR27LvJqQOlCgu1fP9Z0ZsGauxWOE6nRCtjP1+98wlGTMiMsQ+jGvvs+IOK9IDTJQ7jvprnJAZaw9zw==} + peerDependencies: + swup: ^4.6.0 + + '@swup/head-plugin@2.3.1': + resolution: {integrity: sha512-kdv60fO9c0/3+K40f7Fj/uV34nSuuPeCsld83J5uVVFizOmoovA737C5M1Iqac5Spf2GLU7q+mhN6AbohSIBjA==} + peerDependencies: + swup: ^4.6.0 + + '@swup/overlay-theme@2.0.1': + resolution: {integrity: sha512-8SGIvq8av5cFTxaYy1isZsQXWme2d1onE/cy4QpsyRlxiyhzXbYDAgrBLZzY0Ev8TGf3dYFxXg195eOMW1OP+Q==} + peerDependencies: + swup: ^4.0.0 + + '@swup/parallel-plugin@0.4.0': + resolution: {integrity: sha512-un37RpdFz2vuJV1r9Hr0nCl5qgloN/Z3SczC3wi1XfkiHtzZ8kFTcm5pIW5rUkDVwtCTil44mlAA/STouBEgDw==} + peerDependencies: + swup: ^4.6.0 + + '@swup/plugin@3.0.1': + resolution: {integrity: sha512-A9yiJeKTmQ9kac2Eo3MbMWW+Tiw23W5OSzAHVTCfW6n5zze6dexY3FLEUSDTcvRgciknvXfMZ9JTnebbvCKKWw==} + hasBin: true + + '@swup/plugin@4.0.0': + resolution: {integrity: sha512-3Kq31BJxnzoPg643YxGoWQggoU6VPKZpdE5CqqmP7wwkpCYTzkRmrfcQ29mGhsSS7xfS7D33iZoBiwY+wPoo2A==} + + '@swup/preload-plugin@3.2.11': + resolution: {integrity: sha512-dq50u+d8L+63/pU6dUDnypuBoF49ubjRzRJqitGem2K7wRn7xccxRSvdyeGKdA2vNvzGDAmLfwVTZdSotvvDrw==} + peerDependencies: + swup: ^4.0.0 + + '@swup/prettier-config@1.1.0': + resolution: {integrity: sha512-EF4DMdIGieEsuY2XK0PuLf7Uw7yUQOMbA6IdCMvvRvKXj03WLLpnNIFfFp+6hmMtXRSUE88VBpRyp6Giiu1Pbg==} + + '@swup/progress-plugin@3.2.0': + resolution: {integrity: sha512-Ur+D4+aee9swR6pn7JbEyGMwVP1BTcN5ygjlno7IaSQ/vMZbuJ73po1RkKlIi86p+/PZ+1kynkH4K4eMZuHKKg==} + peerDependencies: + swup: ^4.0.0 + + '@swup/route-name-plugin@4.1.0': + resolution: {integrity: sha512-1tw3WeExEKwI3pVMXTptCGxFUDOSEpc63D741eeUCjjGW/f9q7ekuqEaPQd5YJ6POpzDjdt1jjuC9yv54CbeXA==} + peerDependencies: + swup: ^4.0.0 + + '@swup/scripts-plugin@2.1.0': + resolution: {integrity: sha512-JSMFsFCN9gn4q3m1Ccv0gq3gwRoZl6UGALOQO3OeQ8wOIq9vPC5dcUD3CMBuaPanksjR4GC8ZoukIjHrlT52fg==} + peerDependencies: + swup: ^4.2.0 + + '@swup/scroll-plugin@3.3.2': + resolution: {integrity: sha512-jwngTz8LZza8p7ZWqaqQIzkH8x4hwyPh8RbrJSwTKussx24YUQuV9sgjDCzvJ16k/aYk9NCCvqLbb+4TcT3jqA==} + peerDependencies: + swup: ^4.2.0 + + '@swup/slide-theme@2.0.1': + resolution: {integrity: sha512-tTKYg5qNie2FtLVBj64FqQqDwXO0B3UgNmvO5MU2A8RtPpNFYaWmtbomrVQX862SXOEjWF/arHzrNfxhuRVnAg==} + peerDependencies: + swup: ^4.0.0 + + '@swup/theme@2.1.0': + resolution: {integrity: sha512-nwAzx+GYySIYs6uSCFYGNdpLWv2z/mEryRD1gvmIqsaSP2N7sVd4mKAboraJAzIzbasRhTsTQzyN1LfLeti3AA==} + peerDependencies: + swup: ^4.0.0 + + '@tailwindcss/typography@0.5.16': + resolution: {integrity: sha512-0wDLwCVF5V3x3b1SGXPCDcdsbDHMBe+lkFzBRaHeLvNi+nrrnZ1lA18u+OTWO8iSWU2GxUOCvlXtDuqftc1oiA==} + peerDependencies: + tailwindcss: '>=3.0.0 || insiders || >=4.0.0-alpha.20 || >=4.0.0-beta.1' + + '@trysound/sax@0.2.0': + resolution: {integrity: sha512-L7z9BgrNEcYyUYtF+HaEfiS5ebkh9jXqbszz7pC0hRBPaatV0XjSD3+eHrpqFemQfgwiFF0QPIarnIihIDn7OA==} + engines: {node: '>=10.13.0'} + + '@types/babel__core@7.20.5': + resolution: {integrity: sha512-qoQprZvz5wQFJwMDqeseRXWv3rqMvhgpbXFfVyWhbx9X47POIA6i/+dXefEmZKoAgOaTdaIgNSMqMIU61yRyzA==} + + '@types/babel__generator@7.27.0': + resolution: {integrity: sha512-ufFd2Xi92OAVPYsy+P4n7/U7e68fex0+Ee8gSG9KX7eo084CWiQ4sdxktvdl0bOPupXtVJPY19zk6EwWqUQ8lg==} + + '@types/babel__template@7.4.4': + resolution: {integrity: sha512-h/NUaSyG5EyxBIp8YRxo4RMe2/qQgvyowRwVMzhYhBCONbW8PUsg4lkFMrhgZhUe5z3L3MiLDuvyJ/CaPa2A8A==} + + '@types/babel__traverse@7.28.0': + resolution: {integrity: sha512-8PvcXf70gTDZBgt9ptxJ8elBeBjcLOAcOtoO/mPJjtji1+CdGbHgm77om1GrsPxsiE+uXIpNSK64UYaIwQXd4Q==} + + '@types/debug@4.1.12': + resolution: {integrity: sha512-vIChWdVG3LG1SMxEvI/AK+FWJthlrqlTu7fbrlywTkkaONwk/UAGaULXRlf8vkzFBLVm0zkMdCquhL5aOjhXPQ==} + + '@types/estree@0.0.39': + resolution: {integrity: sha512-EYNwp3bU+98cpU4lAWYYL7Zz+2gryWH1qbdDTidVd6hkiR6weksdbMadyXKXNPEkQFhXM+hVO9ZygomHXp+AIw==} + + '@types/estree@1.0.7': + resolution: {integrity: sha512-w28IoSUCJpidD/TGviZwwMJckNESJZXFu7NBZ5YJ4mEUnNraUn9Pm8HSZm/jDF1pDWYKspWE7oVphigUPRakIQ==} + + '@types/hast@2.3.10': + resolution: {integrity: sha512-McWspRw8xx8J9HurkVBfYj0xKoE25tOFlHGdx4MJ5xORQrMGZNqJhVQWaIbm6Oyla5kYOXtDiopzKRJzEOkwJw==} + + '@types/hast@3.0.4': + resolution: {integrity: sha512-WPs+bbQw5aCj+x6laNGWLH3wviHtoCv/P3+otBhbOhJgG8qtpdAMlTCxLtsTWA7LH1Oh/bFCHsBn0TPS5m30EQ==} + + '@types/katex@0.16.7': + resolution: {integrity: sha512-HMwFiRujE5PjrgwHQ25+bsLJgowjGjm5Z8FVSf0N6PwgJrwxH0QxzHYDcKsTfV3wva0vzrpqMTJS2jXPr5BMEQ==} + + '@types/linkify-it@5.0.0': + resolution: {integrity: sha512-sVDA58zAw4eWAffKOaQH5/5j3XeayukzDk+ewSsnv3p4yJEZHCCzMDiZM8e0OUrRvmpGZ85jf4yDHkHsgBNr9Q==} + + '@types/markdown-it@14.1.2': + resolution: {integrity: sha512-promo4eFwuiW+TfGxhi+0x3czqTYJkG8qB17ZUJiVF10Xm7NLVRSLUsfRTU/6h1e24VvRnXCx+hG7li58lkzog==} + + '@types/mdast@4.0.4': + resolution: {integrity: sha512-kGaNbPh1k7AFzgpud/gMdvIm5xuECykRR+JnWKQno9TAXVa6WIVCGTPvYGekIDL4uwCZQSYbUxNBSb1aUo79oA==} + + '@types/mdurl@2.0.0': + resolution: {integrity: sha512-RGdgjQUZba5p6QEFAVx2OGb8rQDL/cPRG7GiedRzMcJ1tYnUANBncjbSB1NRGwbvjcPeikRABz2nshyPk1bhWg==} + + '@types/ms@2.1.0': + resolution: {integrity: sha512-GsCCIZDE/p3i96vtEqx+7dBUGXrc7zeSK3wwPHIaRThS+9OhWIXRqzs4d6k1SVU8g91DrNRWxWUGhp5KXQb2VA==} + + '@types/nlcst@2.0.3': + resolution: {integrity: sha512-vSYNSDe6Ix3q+6Z7ri9lyWqgGhJTmzRjZRqyq15N0Z/1/UnVsno9G/N40NBijoYx2seFDIl0+B2mgAb9mezUCA==} + + '@types/node@17.0.45': + resolution: {integrity: sha512-w+tIMs3rq2afQdsPJlODhoUEKzFP1ayaoyl1CcnwtIlsVe7K7bA1NGm4s3PraqTLlXnbIN84zuBlxBWo1u9BLw==} + + '@types/node@22.14.1': + resolution: {integrity: sha512-u0HuPQwe/dHrItgHHpmw3N2fYCR6x4ivMNbPHRkBVP4CvN+kiRrKHWk3i8tXiO/joPwXLMYvF9TTF0eqgHIuOw==} + + '@types/parse-json@4.0.2': + resolution: {integrity: sha512-dISoDXWWQwUquiKsyZ4Ng+HX2KsPL7LyHKHQwgGFEA3IaKac4Obd+h2a/a6waisAoepJlBcx9paWqjA8/HVjCw==} + + '@types/resolve@1.17.1': + resolution: {integrity: sha512-yy7HuzQhj0dhGpD8RLXSZWEkLsV9ibvxvi6EiJ3bkqLAO1RGo0WbkWQiwpRlSFymTJRz0d3k5LM3kkx8ArDbLw==} + + '@types/sanitize-html@2.15.0': + resolution: {integrity: sha512-71Z6PbYsVKfp4i6Jvr37s5ql6if1Q/iJQT80NbaSi7uGaG8CqBMXP0pk/EsURAOuGdk5IJCd/vnzKrR7S3Txsw==} + + '@types/sax@1.2.7': + resolution: {integrity: sha512-rO73L89PJxeYM3s3pPPjiPgVVcymqU490g0YO5n5By0k2Erzj6tay/4lr1CHAAU4JyOWd1rpQ8bCf6cZfHU96A==} + + '@types/tar@6.1.13': + resolution: {integrity: sha512-IznnlmU5f4WcGTh2ltRu/Ijpmk8wiWXfF0VA4s+HPjHZgvFggk1YaIkbo5krX/zUCzWF8N/l4+W/LNxnvAJ8nw==} + + '@types/unist@2.0.11': + resolution: {integrity: sha512-CmBKiL6NNo/OqgmMn95Fk9Whlp2mtvIv+KNpQKN2F4SjvrEesubTRWGYSg+BnWZOnlCaSTU1sMpsBOzgbYhnsA==} + + '@types/unist@3.0.3': + resolution: {integrity: sha512-ko/gIFJRv177XgZsZcBwnqJN5x/Gien8qNOn0D5bQU/zAzVf9Zt3BlcUiLqhV9y4ARk0GbT3tnUiPNgnTXzc/Q==} + + '@types/yauzl@2.10.3': + resolution: {integrity: sha512-oJoftv0LSuaDZE3Le4DbKX+KS9G36NzOeSap90UIK0yMA/NhKJhqlSGtNDORNRaIbQfzjXDrQa0ytJ6mNRGz/Q==} + + '@ungap/structured-clone@1.3.0': + resolution: {integrity: sha512-WmoN8qaIAo7WTYWbAZuG8PYEhn5fkz7dZrqTBZ7dtt//lL2Gwms1IcnQ5yHqjDfX8Ft5j4YzDM23f87zBfDe9g==} + + '@vercel/analytics@1.5.0': + resolution: {integrity: sha512-MYsBzfPki4gthY5HnYN7jgInhAZ7Ac1cYDoRWFomwGHWEX7odTEzbtg9kf/QSo7XEsEAqlQugA6gJ2WS2DEa3g==} + peerDependencies: + '@remix-run/react': ^2 + '@sveltejs/kit': ^1 || ^2 + next: '>= 13' + react: ^18 || ^19 || ^19.0.0-rc + svelte: '>= 4' + vue: ^3 + vue-router: ^4 + peerDependenciesMeta: + '@remix-run/react': + optional: true + '@sveltejs/kit': + optional: true + next: + optional: true + react: + optional: true + svelte: + optional: true + vue: + optional: true + vue-router: + optional: true + + '@vercel/functions@2.2.13': + resolution: {integrity: sha512-14ArBSIIcOBx9nrEgaJb4Bw+en1gl6eSoJWh8qjifLl5G3E4dRXCFOT8HP+w66vb9Wqyd1lAQBrmRhRwOj9X9A==} + engines: {node: '>= 18'} + peerDependencies: + '@aws-sdk/credential-provider-web-identity': '*' + peerDependenciesMeta: + '@aws-sdk/credential-provider-web-identity': + optional: true + + '@vercel/nft@0.29.4': + resolution: {integrity: sha512-6lLqMNX3TuycBPABycx7A9F1bHQR7kiQln6abjFbPrf5C/05qHM9M5E4PeTE59c7z8g6vHnx1Ioihb2AQl7BTA==} + engines: {node: '>=18'} + hasBin: true + + '@vercel/oidc@2.0.2': + resolution: {integrity: sha512-59PBFx3T+k5hLTEWa3ggiMpGRz1OVvl9eN8SUai+A43IsqiOuAe7qPBf+cray/Fj6mkgnxm/D7IAtjc8zSHi7g==} + engines: {node: '>= 18'} + + '@vercel/routing-utils@5.1.1': + resolution: {integrity: sha512-EyOik06V2fPXAbKY087BM7DMOQOJK+9mubwwox1TkDi21tMeJcMYwsXwepm6ZmyZ5u0j1TpJW172fP4MbzaCcg==} + + '@volar/kit@2.4.12': + resolution: {integrity: sha512-f9JE8oy9C2rBcCWxUYKUF23hOXz4mwgVXFjk7nHhxzplaoVjEOsKpBm8NI2nBH7Cwu8DRxDwBsbIxMl/8wlLxw==} + peerDependencies: + typescript: '*' + + '@volar/language-core@2.4.12': + resolution: {integrity: sha512-RLrFdXEaQBWfSnYGVxvR2WrO6Bub0unkdHYIdC31HzIEqATIuuhRRzYu76iGPZ6OtA4Au1SnW0ZwIqPP217YhA==} + + '@volar/language-server@2.4.12': + resolution: {integrity: sha512-KC0YqTXCZMaImMWyAKC+dLB2BXjfz80kqesJkV6oXxJsGEQPfmdqug299idwtrT6FVSmZ7q5UrPfvgKwA0S3JA==} + + '@volar/language-service@2.4.12': + resolution: {integrity: sha512-nifOPGYYPnCmxja6/ML/Gl2EgFkUdw4gLbYqbh8FjqX3gSpXSZl/0ebqORjKo1KW56YWHWRZd1jFutEtCiRYhA==} + + '@volar/source-map@2.4.12': + resolution: {integrity: sha512-bUFIKvn2U0AWojOaqf63ER0N/iHIBYZPpNGogfLPQ68F5Eet6FnLlyho7BS0y2HJ1jFhSif7AcuTx1TqsCzRzw==} + + '@volar/typescript@2.4.12': + resolution: {integrity: sha512-HJB73OTJDgPc80K30wxi3if4fSsZZAOScbj2fcicMuOPoOkcf9NNAINb33o+DzhBdF9xTKC1gnPmIRDous5S0g==} + + '@vscode/emmet-helper@2.11.0': + resolution: {integrity: sha512-QLxjQR3imPZPQltfbWRnHU6JecWTF1QSWhx3GAKQpslx7y3Dp6sIIXhKjiUJ/BR9FX8PVthjr9PD6pNwOJfAzw==} + + '@vscode/l10n@0.0.18': + resolution: {integrity: sha512-KYSIHVmslkaCDyw013pphY+d7x1qV8IZupYfeIfzNA+nsaWHbn5uPuQRvdRFsa9zFzGeudPuoGoZ1Op4jrJXIQ==} + + abbrev@3.0.1: + resolution: {integrity: sha512-AO2ac6pjRB3SJmGJo+v5/aK6Omggp6fsLrs6wN9bd35ulu4cCwaAU9+7ZhXjeqHVkaHThLuzH0nZr0YpCDhygg==} + engines: {node: ^18.17.0 || >=20.5.0} + + acorn-import-attributes@1.9.5: + resolution: {integrity: sha512-n02Vykv5uA3eHGM/Z2dQrcD56kL8TyDb2p1+0P83PClMnC/nc+anbQRhIOWnSq4Ke/KvDPrY3C9hDtC/A3eHnQ==} + peerDependencies: + acorn: ^8 + + acorn@8.14.1: + resolution: {integrity: sha512-OvQ/2pUDKmgfCg++xsTX1wGxfTaszcHVcTctW4UJB4hibJx2HXxxO5UmVgyjMa+ZDsiaf5wWLXYpRWMmBI0QHg==} + engines: {node: '>=0.4.0'} + hasBin: true + + agent-base@7.1.4: + resolution: {integrity: sha512-MnA+YT8fwfJPgBx3m60MNqakm30XOkyIoH1y6huTQvC0PwZG7ki8NacLBcrPbNoo8vEZy7Jpuk7+jMO+CUovTQ==} + engines: {node: '>= 14'} + + ajv@6.12.6: + resolution: {integrity: sha512-j3fVLgvTo527anyYyJOGTYJbG+vnnQYvE0m5mmkc1TK+nxAppkCLMIL0aZ4dblVCNoGShhm+kzE4ZUykBoMg4g==} + + ajv@8.17.1: + resolution: {integrity: sha512-B/gBuNg5SiMTrPkC+A2+cW0RszwxYmn6VYxB/inlBStS5nx6xHIt/ehKRhIMhqusl7a8LjQoZnjCs5vhwxOQ1g==} + + ansi-align@3.0.1: + resolution: {integrity: sha512-IOfwwBF5iczOjp/WeY4YxyjqAFMQoZufdQWDd19SEExbVLNXqvpzSJ/M7Za4/sCPmQ0+GRquoA7bGcINcxew6w==} + + ansi-regex@2.1.1: + resolution: {integrity: sha512-TIGnTpdo+E3+pCyAluZvtED5p5wCqLdezCyhPZzKPcxvFplEt4i+W7OONCKgeZFT3+y5NZZfOOS/Bdcanm1MYA==} + engines: {node: '>=0.10.0'} + + ansi-regex@5.0.1: + resolution: {integrity: sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==} + engines: {node: '>=8'} + + ansi-regex@6.1.0: + resolution: {integrity: sha512-7HSX4QQb4CspciLpVFwyRe79O3xsIZDDLER21kERQ71oaPodF8jL725AgJMFAYbooIqolJoRLuM81SpeUkpkvA==} + engines: {node: '>=12'} + + ansi-styles@2.2.1: + resolution: {integrity: sha512-kmCevFghRiWM7HB5zTPULl4r9bVFSWjz62MhqizDGUrq2NWuNMQyuv4tHHoKJHs69M/MF64lEcHdYIocrdWQYA==} + engines: {node: '>=0.10.0'} + + ansi-styles@4.3.0: + resolution: {integrity: sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==} + engines: {node: '>=8'} + + ansi-styles@6.2.1: + resolution: {integrity: sha512-bN798gFfQX+viw3R7yrGWRqnrN2oRkEkUjjl4JNn4E8GxxbjtG3FbrEIIY3l8/hrwUwIeCZvi4QuOTP4MErVug==} + engines: {node: '>=12'} + + any-promise@1.3.0: + resolution: {integrity: sha512-7UvmKalWRt1wgjL1RrGxoSJW/0QZFIegpeGvZG9kjp8vrRu55XTHbwnqq2GpXm9uLbcuhxm3IqX9OB4MZR1b2A==} + + anymatch@3.1.3: + resolution: {integrity: sha512-KMReFUr0B4t+D+OBkjR3KYqvocp2XaSzO55UcB6mgQMd3KbcE+mWTyvVV7D/zsdEbNnV6acZUutkiHQXvTr1Rw==} + engines: {node: '>= 8'} + + arg@5.0.2: + resolution: {integrity: sha512-PYjyFOLKQ9y57JvQ6QLo8dAgNqswh8M1RMJYdQduT6xbWSgK36P/Z/v+p888pM69jMMfS8Xd8F6I1kQ/I9HUGg==} + + argparse@2.0.1: + resolution: {integrity: sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q==} + + aria-query@5.3.2: + resolution: {integrity: sha512-COROpnaoap1E2F000S62r6A60uHZnmlvomhfyT2DlTcrY1OrBKn2UhH7qn5wTC9zMvD0AY7csdPSNwKP+7WiQw==} + engines: {node: '>= 0.4'} + + array-buffer-byte-length@1.0.2: + resolution: {integrity: sha512-LHE+8BuR7RYGDKvnrmcuSq3tDcKv9OFEXQt/HpbZhY7V6h0zlUXutnAD82GiFx9rdieCMjkvtcsPqBwgUl1Iiw==} + engines: {node: '>= 0.4'} + + array-iterate@2.0.1: + resolution: {integrity: sha512-I1jXZMjAgCMmxT4qxXfPXa6SthSoE8h6gkSI9BGGNv8mP8G/v0blc+qFnZu6K42vTOiuME596QaLO0TP3Lk0xg==} + + arraybuffer.prototype.slice@1.0.4: + resolution: {integrity: sha512-BNoCY6SXXPQ7gF2opIP4GBE+Xw7U+pHMYKuzjgCN3GwiaIR09UUeKfheyIry77QtrCBlC0KK0q5/TER/tYh3PQ==} + engines: {node: '>= 0.4'} + + astro-expressive-code@0.41.3: + resolution: {integrity: sha512-u+zHMqo/QNLE2eqYRCrK3+XMlKakv33Bzuz+56V1gs8H0y6TZ0hIi3VNbIxeTn51NLn+mJfUV/A0kMNfE4rANw==} + peerDependencies: + astro: ^4.0.0-beta || ^5.0.0-beta || ^3.3.0 + + astro-icon@1.1.5: + resolution: {integrity: sha512-CJYS5nWOw9jz4RpGWmzNQY7D0y2ZZacH7atL2K9DeJXJVaz7/5WrxeyIxO8KASk1jCM96Q4LjRx/F3R+InjJrw==} + + astro@5.7.9: + resolution: {integrity: sha512-P0cLijpmu4xZEXIh83ROOJqiCWRA7KSo24nhRLIjbMkiPElXu2qL1c28m7xhylCXeHa0lnCGPLyNjs1Wx3GyxA==} + engines: {node: ^18.17.1 || ^20.3.0 || >=22.0.0, npm: '>=9.6.5', pnpm: '>=7.1.0'} + hasBin: true + + async-function@1.0.0: + resolution: {integrity: sha512-hsU18Ae8CDTR6Kgu9DYf0EbCr/a5iGL0rytQDobUcdpYOKokk8LEjVphnXkDkgpi0wYVsqrXuP0bZxJaTqdgoA==} + engines: {node: '>= 0.4'} + + async-sema@3.1.1: + resolution: {integrity: sha512-tLRNUXati5MFePdAk8dw7Qt7DpxPB60ofAgn8WRhW6a2rcimZnYBP9oxHiv0OHy+Wz7kPMG+t4LGdt31+4EmGg==} + + async@3.2.6: + resolution: {integrity: sha512-htCUDlxyyCLMgaM3xXg0C0LW2xqfuQ6p05pCEIsXuyQ+a1koYKTuBMzRNwmybfLgvJDMd0r1LTn4+E0Ti6C2AA==} + + asynckit@0.4.0: + resolution: {integrity: sha512-Oei9OH4tRh0YqU3GxhX79dM/mwVgvbZJaSNaRk+bshkj0S5cfHcgYakreBjrHwatXKbz+IoIdYLxrKim2MjW0Q==} + + asyncro@3.0.0: + resolution: {integrity: sha512-nEnWYfrBmA3taTiuiOoZYmgJ/CNrSoQLeLs29SeLcPu60yaw/mHDBHV0iOZ051fTvsTHxpCY+gXibqT9wbQYfg==} + + autoprefixer@10.4.21: + resolution: {integrity: sha512-O+A6LWV5LDHSJD3LjHYoNi4VLsj/Whi7k6zG12xTYaU4cQ8oxQGckXNX8cRHK5yOZ/ppVHe0ZBXGzSV9jXdVbQ==} + engines: {node: ^10 || ^12 || >=14} + hasBin: true + peerDependencies: + postcss: ^8.1.0 + + available-typed-arrays@1.0.7: + resolution: {integrity: sha512-wvUjBtSGN7+7SjNpq/9M2Tg350UZD3q62IFZLbRAR1bSMlCo1ZaeW+BJ+D090e4hIIZLBcTDWe4Mh4jvUDajzQ==} + engines: {node: '>= 0.4'} + + axios@1.8.4: + resolution: {integrity: sha512-eBSYY4Y68NNlHbHBMdeDmKNtDgXWhQsJcGqzO3iLUM0GraQFSS9cVgPX5I9b3lbdFKyYoAEGAZF1DwhTaljNAw==} + + axobject-query@4.1.0: + resolution: {integrity: sha512-qIj0G9wZbMGNLjLmg1PT6v2mE9AH2zlnADJD/2tC6E00hgmhUOfEB6greHPAfLRSufHqROIUTkw6E+M3lH0PTQ==} + engines: {node: '>= 0.4'} + + babel-plugin-macros@3.1.0: + resolution: {integrity: sha512-Cg7TFGpIr01vOQNODXOOaGz2NpCU5gl8x1qJFbb6hbZxR7XrcE2vtbAsTAbJ7/xwJtUuJEw8K8Zr/AE0LHlesg==} + engines: {node: '>=10', npm: '>=6'} + + babel-plugin-polyfill-corejs2@0.4.13: + resolution: {integrity: sha512-3sX/eOms8kd3q2KZ6DAhKPc0dgm525Gqq5NtWKZ7QYYZEv57OQ54KtblzJzH1lQF/eQxO8KjWGIK9IPUJNus5g==} + peerDependencies: + '@babel/core': ^7.4.0 || ^8.0.0-0 <8.0.0 + + babel-plugin-polyfill-corejs3@0.11.1: + resolution: {integrity: sha512-yGCqvBT4rwMczo28xkH/noxJ6MZ4nJfkVYdoDaC/utLtWrXxv27HVrzAeSbqR8SxDsp46n0YF47EbHoixy6rXQ==} + peerDependencies: + '@babel/core': ^7.4.0 || ^8.0.0-0 <8.0.0 + + babel-plugin-polyfill-regenerator@0.6.4: + resolution: {integrity: sha512-7gD3pRadPrbjhjLyxebmx/WrFYcuSjZ0XbdUujQMZ/fcE9oeewk2U/7PCvez84UeuK3oSjmPZ0Ch0dlupQvGzw==} + peerDependencies: + '@babel/core': ^7.4.0 || ^8.0.0-0 <8.0.0 + + babel-plugin-transform-async-to-promises@0.8.18: + resolution: {integrity: sha512-WpOrF76nUHijnNn10eBGOHZmXQC8JYRME9rOLxStOga7Av2VO53ehVFvVNImMksVtQuL2/7ZNxEgxnx7oo/3Hw==} + + babel-plugin-transform-replace-expressions@0.2.0: + resolution: {integrity: sha512-Eh1rRd9hWEYgkgoA3D0kGp7xJ/wgVshgsqmq60iC4HVWD+Lux+fNHSHBa2v1Hsv+dHflShC71qKhiH40OiPtDA==} + peerDependencies: + '@babel/core': ^7.0.0-0 + + bail@2.0.2: + resolution: {integrity: sha512-0xO6mYd7JB2YesxDKplafRpsiOzPt9V02ddPCLbY1xYGPOX24NTyN50qnUxgCPcSoYMhKpAuBTjQoRZCAkUDRw==} + + balanced-match@1.0.2: + resolution: {integrity: sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==} + + base-64@1.0.0: + resolution: {integrity: sha512-kwDPIFCGx0NZHog36dj+tHiwP4QMzsZ3AgMViUBKI0+V5n4U0ufTCUMhnQ04diaRI8EX/QcPfql7zlhZ7j4zgg==} + + base64-js@1.5.1: + resolution: {integrity: sha512-AKpaYlHn8t4SVbOHCy+b5+KKgvR4vrsD8vbvrbiQJps7fKDTkjkDry6ji0rUJjC0kzbNePLwzxq8iypo41qeWA==} + + bcp-47-match@2.0.3: + resolution: {integrity: sha512-JtTezzbAibu8G0R9op9zb3vcWZd9JF6M0xOYGPn0fNCd7wOpRB1mU2mH9T8gaBGbAAyIIVgB2G7xG0GP98zMAQ==} + + binary-extensions@2.3.0: + resolution: {integrity: sha512-Ceh+7ox5qe7LJuLHoY0feh3pHuUDHAcRUeyL2VYghZwfpkNIy/+8Ocg0a3UuSoYzavmylwuLWQOf3hl0jjMMIw==} + engines: {node: '>=8'} + + bindings@1.5.0: + resolution: {integrity: sha512-p2q/t/mhvuOj/UeLlV6566GD/guowlr0hHxClI0W9m7MWYkL1F0hLo+0Aexs9HSPCtR1SXQ0TD3MMKrXZajbiQ==} + + blob-to-buffer@1.2.9: + resolution: {integrity: sha512-BF033y5fN6OCofD3vgHmNtwZWRcq9NLyyxyILx9hfMy1sXYy4ojFl765hJ2lP0YaN2fuxPaLO2Vzzoxy0FLFFA==} + + boolbase@1.0.0: + resolution: {integrity: sha512-JZOSA7Mo9sNGB8+UjSgzdLtokWAky1zbztM3WRLCbZ70/3cTANmQmOdR7y2g+J0e2WXywy1yS468tY+IruqEww==} + + boxen@8.0.1: + resolution: {integrity: sha512-F3PH5k5juxom4xktynS7MoFY+NUWH5LC4CnH11YB8NPew+HLpmBLCybSAEyb2F+4pRXhuhWqFesoQd6DAyc2hw==} + engines: {node: '>=18'} + + brace-expansion@1.1.11: + resolution: {integrity: sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA==} + + brace-expansion@2.0.1: + resolution: {integrity: sha512-XnAIvQ8eM+kC6aULx6wuQiwVsnzsi9d3WxzV3FpWTGA19F621kwdbsAcFKXgKUHZWsy+mY6iL1sHTxWEFCytDA==} + + braces@3.0.3: + resolution: {integrity: sha512-yQbXgO/OSZVD2IsiLlro+7Hf6Q18EJrKSEsdoMzKePKXct3gvD8oLcOQdIzGupr5Fj+EDe8gO/lxc1BzfMpxvA==} + engines: {node: '>=8'} + + brotli-size@4.0.0: + resolution: {integrity: sha512-uA9fOtlTRC0iqKfzff1W34DXUA3GyVqbUaeo3Rw3d4gd1eavKVCETXrn3NzO74W+UVkG3UHu8WxUi+XvKI/huA==} + engines: {node: '>= 10.16.0'} + + brotli@1.3.3: + resolution: {integrity: sha512-oTKjJdShmDuGW94SyyaoQvAjf30dZaHnjJ8uAF+u2/vGJkJbJPJAT1gDiOJP5v1Zb6f9KEyW/1HpuaWIXtGHPg==} + + browserslist@4.24.4: + resolution: {integrity: sha512-KDi1Ny1gSePi1vm0q4oxSF8b4DR44GF4BbmS2YdhPLOEqd8pDviZOGH/GsmRwoWJ2+5Lr085X7naowMwKHDG1A==} + engines: {node: ^6 || ^7 || ^8 || ^9 || ^10 || ^11 || ^12 || >=13.7} + hasBin: true + + buffer-crc32@0.2.13: + resolution: {integrity: sha512-VO9Ht/+p3SN7SKWqcrgEzjGbRSJYTx+Q1pTQC0wrWqHx0vpJraQ6GtHx8tvcg1rlK1byhU5gccxgOgj7B0TDkQ==} + + buffer-from@1.1.2: + resolution: {integrity: sha512-E+XQCRwSbaaiChtv6k6Dwgc+bx+Bs6vuKJHHl5kox/BaKbhiXzqQOwK4cO22yElGp2OCmjwVhT3HmxgyPGnJfQ==} + + builtin-modules@3.3.0: + resolution: {integrity: sha512-zhaCDicdLuWN5UbN5IMnFqNMhNfo919sH85y2/ea+5Yg9TsTkeZxpL+JLbp6cgYFS4sRLp3YV4S6yDuqVWHYOw==} + engines: {node: '>=6'} + + call-bind-apply-helpers@1.0.2: + resolution: {integrity: sha512-Sp1ablJ0ivDkSzjcaJdxEunN5/XvksFJ2sMBFfq6x0ryhQV/2b/KwFe21cMpmHtPOSij8K99/wSfoEuTObmuMQ==} + engines: {node: '>= 0.4'} + + call-bind@1.0.8: + resolution: {integrity: sha512-oKlSFMcMwpUg2ednkhQ454wfWiU/ul3CkJe/PEHcTKuiX6RpbehUiFMXu13HalGZxfUwCQzZG747YXBn1im9ww==} + engines: {node: '>= 0.4'} + + call-bound@1.0.4: + resolution: {integrity: sha512-+ys997U96po4Kx/ABpBCqhA9EuxJaQWDQg7295H4hBphv3IZg0boBKuwYpt4YXp6MZ5AmZQnU/tyMTlRpaSejg==} + engines: {node: '>= 0.4'} + + callsites@3.1.0: + resolution: {integrity: sha512-P8BjAsXvZS+VIDUI11hHCQEv74YT67YUi5JJFNWIqL235sBmjX4+qx9Muvls5ivyNENctx46xQLQ3aTuE7ssaQ==} + engines: {node: '>=6'} + + camelcase-css@2.0.1: + resolution: {integrity: sha512-QOSvevhslijgYwRx6Rv7zKdMF8lbRmx+uQGx2+vDc+KI/eBnsy9kit5aj23AgGu3pa4t9AgwbnXWqS+iOY+2aA==} + engines: {node: '>= 6'} + + camelcase@6.3.0: + resolution: {integrity: sha512-Gmy6FhYlCY7uOElZUSbxo2UCDH8owEk996gkbrpsgGtrJLM3J7jGxl9Ic7Qwwj4ivOE5AWZWRMecDdF7hqGjFA==} + engines: {node: '>=10'} + + camelcase@8.0.0: + resolution: {integrity: sha512-8WB3Jcas3swSvjIeA2yvCJ+Miyz5l1ZmB6HFb9R1317dt9LCQoswg/BGrmAmkWVEszSrrg4RwmO46qIm2OEnSA==} + engines: {node: '>=16'} + + caniuse-api@3.0.0: + resolution: {integrity: sha512-bsTwuIg/BZZK/vreVTYYbSWoe2F+71P7K5QGEX+pT250DZbfU1MQ5prOKpPR+LL6uWKK3KMwMCAS74QB3Um1uw==} + + caniuse-lite@1.0.30001733: + resolution: {integrity: sha512-e4QKw/O2Kavj2VQTKZWrwzkt3IxOmIlU6ajRb6LP64LHpBo1J67k2Hi4Vu/TgJWsNtynurfS0uK3MaUTCPfu5Q==} + + ccount@2.0.1: + resolution: {integrity: sha512-eyrF0jiFpY+3drT6383f1qhkbGsLSifNAjA61IUjZjmLCWjItY6LB9ft9YhoDgwfmclB2zhu51Lc7+95b8NRAg==} + + chalk@1.1.3: + resolution: {integrity: sha512-U3lRVLMSlsCfjqYPbLyVv11M9CPW4I728d6TCKMAOJueEeB9/8o+eSsMnxPJD+Q+K909sdESg7C+tIkoH6on1A==} + engines: {node: '>=0.10.0'} + + chalk@4.1.2: + resolution: {integrity: sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==} + engines: {node: '>=10'} + + chalk@5.4.1: + resolution: {integrity: sha512-zgVZuo2WcZgfUEmsn6eO3kINexW8RAE4maiQ8QNs8CtpPCSyMiYsULR3HQYkm3w8FIA3SberyMJMSldGsW+U3w==} + engines: {node: ^12.17.0 || ^14.13 || >=16.0.0} + + character-entities-html4@2.1.0: + resolution: {integrity: sha512-1v7fgQRj6hnSwFpq1Eu0ynr/CDEw0rXo2B61qXrLNdHZmPKgb7fqS1a2JwF0rISo9q77jDI8VMEHoApn8qDoZA==} + + character-entities-legacy@3.0.0: + resolution: {integrity: sha512-RpPp0asT/6ufRm//AJVwpViZbGM/MkjQFxJccQRHmISF/22NBtsHqAWmL+/pmkPWoIUJdWyeVleTl1wydHATVQ==} + + character-entities@2.0.2: + resolution: {integrity: sha512-shx7oQ0Awen/BRIdkjkvz54PnEEI/EjwXDSIZp86/KKdbafHh1Df/RYGBhn4hbe2+uKC9FnT5UCEdyPz3ai9hQ==} + + character-reference-invalid@2.0.1: + resolution: {integrity: sha512-iBZ4F4wRbyORVsu0jPV7gXkOsGYjGHPmAyv+HiHG8gi5PtC9KI2j1+v8/tlibRvjoWX027ypmG/n0HtO5t7unw==} + + cheerio-select@2.1.0: + resolution: {integrity: sha512-9v9kG0LvzrlcungtnJtpGNxY+fzECQKhK4EGJX2vByejiMX84MFNQw4UxPJl3bFbTMw+Dfs37XaIkCwTZfLh4g==} + + cheerio@1.0.0: + resolution: {integrity: sha512-quS9HgjQpdaXOvsZz82Oz7uxtXiy6UIsIQcpBj7HRw2M63Skasm9qlDocAM7jNuaxdhpPU7c4kJN+gA5MCu4ww==} + engines: {node: '>=18.17'} + + chokidar@3.6.0: + resolution: {integrity: sha512-7VT13fmjotKpGipCW9JEQAusEPE+Ei8nl6/g4FBAmIm0GOOLMua9NDDo/DWp0ZAxCr3cPq5ZpBqmPAQgDda2Pw==} + engines: {node: '>= 8.10.0'} + + chokidar@4.0.3: + resolution: {integrity: sha512-Qgzu8kfBvo+cA4962jnP1KkS6Dop5NS6g7R5LFYJr4b8Ub94PPQXUksCw9PvXoeXPRRddRNC5C1JQUR2SMGtnA==} + engines: {node: '>= 14.16.0'} + + chownr@2.0.0: + resolution: {integrity: sha512-bIomtDF5KGpdogkLd9VspvFzk9KfpyyGlS8YFVZl7TGPBHL5snIOnxeshwVgPteQ9b4Eydl+pVbIyE1DcvCWgQ==} + engines: {node: '>=10'} + + chownr@3.0.0: + resolution: {integrity: sha512-+IxzY9BZOQd/XuYPRmrvEVjF/nqj5kgT4kEq7VofrDoM1MxoRjEWkrCC3EtLi59TVawxTAn+orJwFQcrqEN1+g==} + engines: {node: '>=18'} + + ci-info@4.2.0: + resolution: {integrity: sha512-cYY9mypksY8NRqgDB1XD1RiJL338v/551niynFTGkZOO2LHuB2OmOYxDIe/ttN9AHwrqdum1360G3ald0W9kCg==} + engines: {node: '>=8'} + + cli-boxes@3.0.0: + resolution: {integrity: sha512-/lzGpEWL/8PfI0BmBOPRwp0c/wFNX1RdUML3jK/RcSBA9T8mZDdQpqYBKtCFTOfQbwPqWEOpjqW+Fnayc0969g==} + engines: {node: '>=10'} + + cliui@8.0.1: + resolution: {integrity: sha512-BSeNnyus75C4//NQ9gQt1/csTXyo/8Sb+afLAkzAptFuMsod9HFokGNudZpi/oQV73hnVK+sR+5PVRMd+Dr7YQ==} + engines: {node: '>=12'} + + clone@2.1.2: + resolution: {integrity: sha512-3Pe/CF1Nn94hyhIYpjtiLhdCoEoz0DqQ+988E9gmeEdQZlojxnOb74wctFyuwWQHzqyf9X7C7MG8juUpqBJT8w==} + engines: {node: '>=0.8'} + + clsx@2.1.1: + resolution: {integrity: sha512-eYm0QWBtUrBWZWG0d386OGAw16Z995PiOVo2B7bjWSbHedGl5e0ZWaq65kOGgUSNesEIDkB9ISbTg/JK9dhCZA==} + engines: {node: '>=6'} + + color-convert@2.0.1: + resolution: {integrity: sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==} + engines: {node: '>=7.0.0'} + + color-name@1.1.4: + resolution: {integrity: sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==} + + color-string@1.9.1: + resolution: {integrity: sha512-shrVawQFojnZv6xM40anx4CkoDP+fZsw/ZerEMsW/pyzsRbElpsL/DBVW7q3ExxwusdNXI3lXpuhEZkzs8p5Eg==} + + color@4.2.3: + resolution: {integrity: sha512-1rXeuUUiGGrykh+CeBdu5Ie7OJwinCgQY0bc7GCRxy5xVHy+moaqkpL/jqQq0MtQOeYcrqEz4abc5f0KtU7W4A==} + engines: {node: '>=12.5.0'} + + colord@2.9.3: + resolution: {integrity: sha512-jeC1axXpnb0/2nn/Y1LPuLdgXBLH7aDcHu4KEKfqw3CUhX7ZpfBSlPKyqXE6btIgEzfWtrX3/tyBCaCvXvMkOw==} + + combined-stream@1.0.8: + resolution: {integrity: sha512-FQN4MRfuJeHf7cBbBMJFXhKSDq+2kAArBlmRBvcvFE5BB1HZKXtSFASDhdlz9zOYwxh8lDdnvmMOe/+5cdoEdg==} + engines: {node: '>= 0.8'} + + comma-separated-tokens@2.0.3: + resolution: {integrity: sha512-Fu4hJdvzeylCfQPp9SGWidpzrMs7tTrlu6Vb8XGaRGck8QSNZJJp538Wrb60Lax4fPwR64ViY468OIUTbRlGZg==} + + commander@2.20.3: + resolution: {integrity: sha512-GpVkmM8vF2vQUkj2LvZmD35JxeJOLCwJ9cUkugyk2nuhbv3+mJvpLYYt+0+USMxE+oj+ey/lJEnhZw75x/OMcQ==} + + commander@4.1.1: + resolution: {integrity: sha512-NOKm8xhkzAjzFx8B2v5OAHT+u5pRQc2UCa2Vq9jYL/31o2wi9mxBA7LIFs3sV5VSC49z6pEhfbMULvShKj26WA==} + engines: {node: '>= 6'} + + commander@7.2.0: + resolution: {integrity: sha512-QrWXB+ZQSVPmIWIhtEO9H+gwHaMGYiF5ChvoJ+K9ZGHG/sVsa6yiesAD1GC/x46sET00Xlwo1u49RVVVzvcSkw==} + engines: {node: '>= 10'} + + commander@8.3.0: + resolution: {integrity: sha512-OkTL9umf+He2DZkUq8f8J9of7yL6RJKI24dVITBmNfZBmri9zYZQrKkuXiKhyfPSu8tUhnVBB1iKXevvnlR4Ww==} + engines: {node: '>= 12'} + + common-ancestor-path@1.0.1: + resolution: {integrity: sha512-L3sHRo1pXXEqX8VU28kfgUY+YGsk09hPqZiZmLacNib6XNTCM8ubYeT7ryXQw8asB1sKgcU5lkB7ONug08aB8w==} + + commondir@1.0.1: + resolution: {integrity: sha512-W9pAhw0ja1Edb5GVdIF1mjZw/ASI0AlShXM83UUGe2DVr5TdAPEA1OA8m/g8zWp9x6On7gqufY+FatDbC3MDQg==} + + concat-map@0.0.1: + resolution: {integrity: sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg==} + + concat-with-sourcemaps@1.1.0: + resolution: {integrity: sha512-4gEjHJFT9e+2W/77h/DS5SGUgwDaOwprX8L/gl5+3ixnzkVJJsZWDSelmN3Oilw3LNDZjZV0yqH1hLG3k6nghg==} + + confbox@0.1.8: + resolution: {integrity: sha512-RMtmw0iFkeR4YV+fUOSucriAQNb9g8zFR52MWCtl+cCZOFRNL6zeB395vPzFhEjjn4fMxXudmELnl/KF/WrK6w==} + + confbox@0.2.1: + resolution: {integrity: sha512-hkT3yDPFbs95mNCy1+7qNKC6Pro+/ibzYxtM2iqEigpf0sVw+bg4Zh9/snjsBcf990vfIsg5+1U7VyiyBb3etg==} + + consola@3.4.2: + resolution: {integrity: sha512-5IKcdX0nnYavi6G7TtOhwkYzyjfJlatbjMjuLSfE2kYT5pMDOilZ4OvMhi637CcDICTmz3wARPoyhqyX1Y+XvA==} + engines: {node: ^14.18.0 || >=16.10.0} + + convert-source-map@2.0.0: + resolution: {integrity: sha512-Kvp459HrV2FEJ1CAsi1Ku+MY3kasH19TFykTz2xWmMeq6bk2NU3XXvfJ+Q61m0xktWwt+1HSYf3JZsTms3aRJg==} + + cookie-es@1.2.2: + resolution: {integrity: sha512-+W7VmiVINB+ywl1HGXJXmrqkOhpKrIiVZV6tQuV54ZyQC7MMuBt81Vc336GMLoHBq5hV/F9eXgt5Mnx0Rha5Fg==} + + cookie@1.0.2: + resolution: {integrity: sha512-9Kr/j4O16ISv8zBBhJoi4bXOYNTkFLOqSL3UDB0njXxCXNezjeyVrJyGOWtgfs/q2km1gwBcfH8q1yEGoMYunA==} + engines: {node: '>=18'} + + core-js-compat@3.41.0: + resolution: {integrity: sha512-RFsU9LySVue9RTwdDVX/T0e2Y6jRYWXERKElIjpuEOEnxaXffI0X7RUwVzfYLfzuLXSNJDYoRYUAmRUcyln20A==} + + cosmiconfig@7.1.0: + resolution: {integrity: sha512-AdmX6xUzdNASswsFtmwSt7Vj8po9IuqXm0UXz7QKPuEUmPB4XyjGfaAr2PSuELMwkRMVH1EpIkX5bTZGRB3eCA==} + engines: {node: '>=10'} + + cross-fetch@3.2.0: + resolution: {integrity: sha512-Q+xVJLoGOeIMXZmbUK4HYk+69cQH6LudR0Vu/pRm2YlU/hDV9CiS0gKUMaWY5f2NeUH9C1nV3bsTlCo0FsTV1Q==} + + cross-spawn@7.0.6: + resolution: {integrity: sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA==} + engines: {node: '>= 8'} + + crossws@0.3.4: + resolution: {integrity: sha512-uj0O1ETYX1Bh6uSgktfPvwDiPYGQ3aI4qVsaC/LWpkIzGj1nUYm5FK3K+t11oOlpN01lGbprFCH4wBlKdJjVgw==} + + css-declaration-sorter@6.4.1: + resolution: {integrity: sha512-rtdthzxKuyq6IzqX6jEcIzQF/YqccluefyCYheovBOLhFT/drQA9zj/UbRAa9J7C0o6EG6u3E6g+vKkay7/k3g==} + engines: {node: ^10 || ^12 || >=14} + peerDependencies: + postcss: ^8.0.9 + + css-select@4.3.0: + resolution: {integrity: sha512-wPpOYtnsVontu2mODhA19JrqWxNsfdatRKd64kmpRbQgh1KtItko5sTnEpPdpSaJszTOhEMlF/RPz28qj4HqhQ==} + + css-select@5.1.0: + resolution: {integrity: sha512-nwoRF1rvRRnnCqqY7updORDsuqKzqYJ28+oSMaJMMgOauh3fvwHqMS7EZpIPqK8GL+g9mKxF1vP/ZjSeNjEVHg==} + + css-selector-parser@3.1.3: + resolution: {integrity: sha512-gJMigczVZqYAk0hPVzx/M4Hm1D9QOtqkdQk9005TNzDIUGzo5cnHEDiKUT7jGPximL/oYb+LIitcHFQ4aKupxg==} + + css-tree@1.1.3: + resolution: {integrity: sha512-tRpdppF7TRazZrjJ6v3stzv93qxRcSsFmW6cX0Zm2NVKpxE1WV1HblnghVv9TreireHkqI/VDEsfolRF1p6y7Q==} + engines: {node: '>=8.0.0'} + + css-tree@2.2.1: + resolution: {integrity: sha512-OA0mILzGc1kCOCSJerOeqDxDQ4HOh+G8NbOJFOTgOCzpw7fCBubk0fEyxp8AgOL/jvLgYA/uV0cMbe43ElF1JA==} + engines: {node: ^10 || ^12.20.0 || ^14.13.0 || >=15.0.0, npm: '>=7.0.0'} + + css-tree@2.3.1: + resolution: {integrity: sha512-6Fv1DV/TYw//QF5IzQdqsNDjx/wc8TrMBZsqjL9eW01tWb7R7k/mq+/VXfJCl7SoD5emsJop9cOByJZfs8hYIw==} + engines: {node: ^10 || ^12.20.0 || ^14.13.0 || >=15.0.0} + + css-tree@3.1.0: + resolution: {integrity: sha512-0eW44TGN5SQXU1mWSkKwFstI/22X2bG1nYzZTYMAWjylYURhse752YgbE4Cx46AC+bAvI+/dYTPRk1LqSUnu6w==} + engines: {node: ^10 || ^12.20.0 || ^14.13.0 || >=15.0.0} + + css-what@6.1.0: + resolution: {integrity: sha512-HTUrgRJ7r4dsZKU6GjmpfRK1O76h97Z8MfS1G0FozR+oF2kG6Vfe8JE6zwrkbxigziPHinCJ+gCPjA9EaBDtRw==} + engines: {node: '>= 6'} + + cssesc@3.0.0: + resolution: {integrity: sha512-/Tb/JcjK111nNScGob5MNtsntNM1aCNUDipB/TkwZFhyDrrE47SOx/18wF2bbjgc3ZzCSKW1T5nt5EbFoAz/Vg==} + engines: {node: '>=4'} + hasBin: true + + cssnano-preset-default@5.2.14: + resolution: {integrity: sha512-t0SFesj/ZV2OTylqQVOrFgEh5uanxbO6ZAdeCrNsUQ6fVuXwYTxJPNAGvGTxHbD68ldIJNec7PyYZDBrfDQ+6A==} + engines: {node: ^10 || ^12 || >=14.0} + peerDependencies: + postcss: ^8.2.15 + + cssnano-utils@3.1.0: + resolution: {integrity: sha512-JQNR19/YZhz4psLX/rQ9M83e3z2Wf/HdJbryzte4a3NSuafyp9w/I4U+hx5C2S9g41qlstH7DEWnZaaj83OuEA==} + engines: {node: ^10 || ^12 || >=14.0} + peerDependencies: + postcss: ^8.2.15 + + cssnano@5.1.15: + resolution: {integrity: sha512-j+BKgDcLDQA+eDifLx0EO4XSA56b7uut3BQFH+wbSaSTuGLuiyTa/wbRYthUXX8LC9mLg+WWKe8h+qJuwTAbHw==} + engines: {node: ^10 || ^12 || >=14.0} + peerDependencies: + postcss: ^8.2.15 + + csso@4.2.0: + resolution: {integrity: sha512-wvlcdIbf6pwKEk7vHj8/Bkc0B4ylXZruLvOgs9doS5eOsOpuodOV2zJChSpkp+pRpYQLQMeF04nr3Z68Sta9jA==} + engines: {node: '>=8.0.0'} + + csso@5.0.5: + resolution: {integrity: sha512-0LrrStPOdJj+SPCCrGhzryycLjwcgUSHBtxNA8aIDxf0GLsRh1cKYhB00Gd1lDOS4yGH69+SNn13+TWbVHETFQ==} + engines: {node: ^10 || ^12.20.0 || ^14.13.0 || >=15.0.0, npm: '>=7.0.0'} + + data-view-buffer@1.0.2: + resolution: {integrity: sha512-EmKO5V3OLXh1rtK2wgXRansaK1/mtVdTUEiEI0W8RkvgT05kfxaH29PliLnpLP73yYO6142Q72QNa8Wx/A5CqQ==} + engines: {node: '>= 0.4'} + + data-view-byte-length@1.0.2: + resolution: {integrity: sha512-tuhGbE6CfTM9+5ANGf+oQb72Ky/0+s3xKUpHvShfiz2RxMFgFPjsXuRLBVMtvMs15awe45SRb83D6wH4ew6wlQ==} + engines: {node: '>= 0.4'} + + data-view-byte-offset@1.0.1: + resolution: {integrity: sha512-BS8PfmtDGnrgYdOonGZQdLZslWIeCGFP9tpan0hi1Co2Zr2NKADsvGYA8XxuG/4UWgJ6Cjtv+YJnB6MM69QGlQ==} + engines: {node: '>= 0.4'} + + debug@4.4.0: + resolution: {integrity: sha512-6WTZ/IxCY/T6BALoZHaE4ctp9xm+Z5kY/pzYaCHRFeyVhojxlrm+46y68HA6hr0TcwEssoxNiDEUJQjfPZ/RYA==} + engines: {node: '>=6.0'} + peerDependencies: + supports-color: '*' + peerDependenciesMeta: + supports-color: + optional: true + + decode-named-character-reference@1.1.0: + resolution: {integrity: sha512-Wy+JTSbFThEOXQIR2L6mxJvEs+veIzpmqD7ynWxMXGpnk3smkHQOp6forLdHsKpAMW9iJpaBBIxz285t1n1C3w==} + + dedent-js@1.0.1: + resolution: {integrity: sha512-OUepMozQULMLUmhxS95Vudo0jb0UchLimi3+pQ2plj61Fcy8axbP9hbiD4Sz6DPqn6XG3kfmziVfQ1rSys5AJQ==} + + deepmerge@4.3.1: + resolution: {integrity: sha512-3sUqbMEc77XqpdNO7FRyRog+eW3ph+GYCbj+rK+uYyRMuwsVy0rMiVtPn+QJlKFvWP/1PYpapqYn0Me2knFn+A==} + engines: {node: '>=0.10.0'} + + define-data-property@1.1.4: + resolution: {integrity: sha512-rBMvIzlpA8v6E+SJZoo++HAYqsLrkg7MSfIinMPFhmkorw7X+dOXVJQs+QT69zGkzMyfDnIMN2Wid1+NbL3T+A==} + engines: {node: '>= 0.4'} + + define-lazy-prop@2.0.0: + resolution: {integrity: sha512-Ds09qNh8yw3khSjiJjiUInaGX9xlqZDY7JVryGxdxV7NPeuqQfplOpQ66yJFZut3jLa5zOwkXw1g9EI2uKh4Og==} + engines: {node: '>=8'} + + define-properties@1.2.1: + resolution: {integrity: sha512-8QmQKqEASLd5nx0U1B1okLElbUuuttJ/AnYmRXbbbGDWh6uS208EjD4Xqq/I9wK7u0v6O08XhTWnt5XtEbR6Dg==} + engines: {node: '>= 0.4'} + + defu@6.1.4: + resolution: {integrity: sha512-mEQCMmwJu317oSz8CwdIOdwf3xMif1ttiM8LTufzc3g6kR+9Pe236twL8j3IYT1F7GfRgGcW6MWxzZjLIkuHIg==} + + delayed-stream@1.0.0: + resolution: {integrity: sha512-ZySD7Nf91aLB0RxL4KGrKHBXl7Eds1DAmEdcoVawXnLD7SDhpNgtuII2aAkg7a7QS41jxPSZ17p4VdGnMHk3MQ==} + engines: {node: '>=0.4.0'} + + delegate-it@6.2.1: + resolution: {integrity: sha512-3/P/rwj+zal/99EEml7y1+bXjBY+Wok/WSg0EngWAtdvHK6iKTPbABQE84RyyRfR0Fmejs93BrkZQWlgyeWuFQ==} + + dequal@2.0.3: + resolution: {integrity: sha512-0je+qPKHEMohvfRTCEo3CrPG6cAzAYgmzKyxRiYSSDkS6eGJdyVJm7WaYA5ECaAD9wLB2T4EEeymA5aFVcYXCA==} + engines: {node: '>=6'} + + destr@2.0.5: + resolution: {integrity: sha512-ugFTXCtDZunbzasqBxrK93Ik/DRYsO6S/fedkWEMKqt04xZ4csmnmwGDBAb07QWNaGMAmnTIemsYZCksjATwsA==} + + detect-libc@1.0.3: + resolution: {integrity: sha512-pGjwhsmsp4kL2RTz08wcOlGN83otlqHeD/Z5T8GXZB+/YcpQ/dgo+lbU8ZsGxV0HIvqqxo9l7mqYwyYMD9bKDg==} + engines: {node: '>=0.10'} + hasBin: true + + detect-libc@2.0.3: + resolution: {integrity: sha512-bwy0MGW55bG41VqxxypOsdSdGqLwXPI/focwgTYCFMbdUiBAxLg9CFzG08sz2aqzknwiX7Hkl0bQENjg8iLByw==} + engines: {node: '>=8'} + + detect-libc@2.0.4: + resolution: {integrity: sha512-3UDv+G9CsCKO1WKMGw9fwq/SWJYbI0c5Y7LU1AXYoDdbhE2AHQ6N6Nb34sG8Fj7T5APy8qXDCKuuIHd1BR0tVA==} + engines: {node: '>=8'} + + deterministic-object-hash@2.0.2: + resolution: {integrity: sha512-KxektNH63SrbfUyDiwXqRb1rLwKt33AmMv+5Nhsw1kqZ13SJBRTgZHtGbE+hH3a1mVW1cz+4pqSWVPAtLVXTzQ==} + engines: {node: '>=18'} + + devalue@5.1.1: + resolution: {integrity: sha512-maua5KUiapvEwiEAe+XnlZ3Rh0GD+qI1J/nb9vrJc3muPXvcF/8gXYTWF76+5DAqHyDUtOIImEuo0YKE9mshVw==} + + devlop@1.1.0: + resolution: {integrity: sha512-RWmIqhcFf1lRYBvNmr7qTNuyCt/7/ns2jbpp1+PalgE/rDQcBT0fioSMUpJ93irlUhC5hrg4cYqe6U+0ImW0rA==} + + dfa@1.2.0: + resolution: {integrity: sha512-ED3jP8saaweFTjeGX8HQPjeC1YYyZs98jGNZx6IiBvxW7JG5v492kamAQB3m2wop07CvU/RQmzcKr6bgcC5D/Q==} + + didyoumean@1.2.2: + resolution: {integrity: sha512-gxtyfqMg7GKyhQmb056K7M3xszy/myH8w+B4RT+QXBQsvAOdc3XymqDDPHx1BgPgsdAA5SIifona89YtRATDzw==} + + diff@5.2.0: + resolution: {integrity: sha512-uIFDxqpRZGZ6ThOk84hEfqWoHx2devRFvpTZcTHur85vImfaxUbTW9Ryh4CpCuDnToOP1CEtXKIgytHBPVff5A==} + engines: {node: '>=0.3.1'} + + direction@2.0.1: + resolution: {integrity: sha512-9S6m9Sukh1cZNknO1CWAr2QAWsbKLafQiyM5gZ7VgXHeuaoUwffKN4q6NC4A/Mf9iiPlOXQEKW/Mv/mh9/3YFA==} + hasBin: true + + dlv@1.1.3: + resolution: {integrity: sha512-+HlytyjlPKnIG8XuRG8WvmBP8xs8P71y+SKKS6ZXWoEgLuePxtDoUEiH7WkdePWrQ5JBpE6aoVqfZfJUQkjXwA==} + + dom-serializer@1.4.1: + resolution: {integrity: sha512-VHwB3KfrcOOkelEG2ZOfxqLZdfkil8PtJi4P8N2MMXucZq2yLp75ClViUlOVwyoHEDjYU433Aq+5zWP61+RGag==} + + dom-serializer@2.0.0: + resolution: {integrity: sha512-wIkAryiqt/nV5EQKqQpo3SToSOV9J0DnbJqwK7Wv/Trc92zIAYZ4FlMu+JPFW1DfGFt81ZTCGgDEabffXeLyJg==} + + domelementtype@2.3.0: + resolution: {integrity: sha512-OLETBj6w0OsagBwdXnPdN0cnMfF9opN69co+7ZrbfPGrdpPVNBUj02spi6B1N7wChLQiPn4CSH/zJvXw56gmHw==} + + domhandler@4.3.1: + resolution: {integrity: sha512-GrwoxYN+uWlzO8uhUXRl0P+kHE4GtVPfYzVLcUxPL7KNdHKj66vvlhiweIHqYYXWlw+T8iLMp42Lm67ghw4WMQ==} + engines: {node: '>= 4'} + + domhandler@5.0.3: + resolution: {integrity: sha512-cgwlv/1iFQiFnU96XXgROh8xTeetsnJiDsTc7TYCLFd9+/WNkIqPTxiM/8pSd8VIrhXGTf1Ny1q1hquVqDJB5w==} + engines: {node: '>= 4'} + + domutils@2.8.0: + resolution: {integrity: sha512-w96Cjofp72M5IIhpjgobBimYEfoPjx1Vx0BSX9P30WBdZW2WIKU0T1Bd0kz2eNZ9ikjKgHbEyKx8BB6H1L3h3A==} + + domutils@3.2.2: + resolution: {integrity: sha512-6kZKyUajlDuqlHKVX1w7gyslj9MPIXzIFiz/rGu35uC1wMi+kMhQwGhl4lt9unC9Vb9INnY9Z3/ZA3+FhASLaw==} + + dset@3.1.4: + resolution: {integrity: sha512-2QF/g9/zTaPDc3BjNcVTGoBbXBgYfMTTceLaYcFJ/W9kggFUkhxD/hMEeuLKbugyef9SqAx8cpgwlIP/jinUTA==} + engines: {node: '>=4'} + + dunder-proto@1.0.1: + resolution: {integrity: sha512-KIN/nDJBQRcXw0MLVhZE9iQHmG68qAVIBg9CqmUYjmQIhgij9U5MFvrqkUL5FbtyyzZuOeOt0zdeRe4UY7ct+A==} + engines: {node: '>= 0.4'} + + duplexer@0.1.1: + resolution: {integrity: sha512-sxNZ+ljy+RA1maXoUReeqBBpBC6RLKmg5ewzV+x+mSETmWNoKdZN6vcQjpFROemza23hGFskJtFNoUWUaQ+R4Q==} + + duplexer@0.1.2: + resolution: {integrity: sha512-jtD6YG370ZCIi/9GTaJKQxWTZD045+4R4hTk/x1UyoqadyJ9x9CgSi1RlVDQF8U2sxLLSnFkCaMihqljHIWgMg==} + + eastasianwidth@0.2.0: + resolution: {integrity: sha512-I88TYZWc9XiYHRQ4/3c5rjjfgkjhLyW2luGIheGERbNQ6OY7yTybanSpDXZa8y7VUP9YmDcYa+eyq4ca7iLqWA==} + + ejs@3.1.10: + resolution: {integrity: sha512-UeJmFfOrAQS8OJWPZ4qtgHyWExa088/MtK5UEyoJGFH67cDEXkZSviOiKRCZ4Xij0zxI3JECgYs3oKx+AizQBA==} + engines: {node: '>=0.10.0'} + hasBin: true + + electron-to-chromium@1.5.126: + resolution: {integrity: sha512-AtH1uLcTC72LA4vfYcEJJkrMk/MY/X0ub8Hv7QGAePW2JkeUFHEL/QfS4J77R6M87Sss8O0OcqReSaN1bpyA+Q==} + + emmet@2.4.11: + resolution: {integrity: sha512-23QPJB3moh/U9sT4rQzGgeyyGIrcM+GH5uVYg2C6wZIxAIJq7Ng3QLT79tl8FUwDXhyq9SusfknOrofAKqvgyQ==} + + emoji-regex@10.4.0: + resolution: {integrity: sha512-EC+0oUMY1Rqm4O6LLrgjtYDvcVYTy7chDnM4Q7030tP4Kwj3u/pR6gP9ygnp2CJMK5Gq+9Q2oqmrFJAz01DXjw==} + + emoji-regex@8.0.0: + resolution: {integrity: sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==} + + emoji-regex@9.2.2: + resolution: {integrity: sha512-L18DaJsXSUk2+42pv8mLs5jJT2hqFkFE4j21wOmgbUqsZ2hL72NsUU785g9RXgo3s0ZNgVl42TiHp3ZtOv/Vyg==} + + encoding-sniffer@0.2.0: + resolution: {integrity: sha512-ju7Wq1kg04I3HtiYIOrUrdfdDvkyO9s5XM8QAj/bN61Yo/Vb4vgJxy5vi4Yxk01gWHbrofpPtpxM8bKger9jhg==} + + end-of-stream@1.4.4: + resolution: {integrity: sha512-+uw1inIHVPQoaVuHzRyXd21icM+cnt4CzD5rW+NC1wjOUSTOs+Te7FOv7AhN7vS9x/oIyhLP5PR1H+phQAHu5Q==} + + entities@2.2.0: + resolution: {integrity: sha512-p92if5Nz619I0w+akJrLZH0MX0Pb5DX39XOwQTtXSdQQOaYH03S1uIQp4mhOZtAXrxq4ViO67YTiLBo2638o9A==} + + entities@4.5.0: + resolution: {integrity: sha512-V0hjH4dGPh9Ao5p0MoRY6BVqtwCjhz6vI5LT8AJ55H+4g9/4vbHx1I54fS0XuclLhDHArPQCiMjDxjaL8fPxhw==} + engines: {node: '>=0.12'} + + entities@6.0.0: + resolution: {integrity: sha512-aKstq2TDOndCn4diEyp9Uq/Flu2i1GlLkc6XIDQSDMuaFE3OPW5OphLCyQ5SpSJZTb4reN+kTcYru5yIfXoRPw==} + engines: {node: '>=0.12'} + + error-ex@1.3.2: + resolution: {integrity: sha512-7dFHNmqeFSEt2ZBsCriorKnn3Z2pj+fd9kmI6QoWw4//DL+icEBfc0U7qJCisqrTsKTjw4fNFy2pW9OqStD84g==} + + es-abstract@1.23.9: + resolution: {integrity: sha512-py07lI0wjxAC/DcfK1S6G7iANonniZwTISvdPzk9hzeH0IZIshbuuFxLIU96OyF89Yb9hiqWn8M/bY83KY5vzA==} + engines: {node: '>= 0.4'} + + es-define-property@1.0.1: + resolution: {integrity: sha512-e3nRfgfUZ4rNGL232gUgX06QNyyez04KdjFrF+LTRoOXmrOgFKDg4BCdsjW8EnT69eqdYGmRpJwiPVYNrCaW3g==} + engines: {node: '>= 0.4'} + + es-errors@1.3.0: + resolution: {integrity: sha512-Zf5H2Kxt2xjTvbJvP2ZWLEICxA6j+hAmMzIlypy4xcBg1vKVnx89Wy0GbS+kf5cwCVFFzdCFh2XSCFNULS6csw==} + engines: {node: '>= 0.4'} + + es-module-lexer@1.7.0: + resolution: {integrity: sha512-jEQoCwk8hyb2AZziIOLhDqpm5+2ww5uIE6lkO/6jcOCusfk6LhMHpXXfBLXTZ7Ydyt0j4VoUQv6uGNYbdW+kBA==} + + es-object-atoms@1.1.1: + resolution: {integrity: sha512-FGgH2h8zKNim9ljj7dankFPcICIK9Cp5bm+c2gQSYePhpaG5+esrLODihIorn+Pe6FGJzWhXQotPv73jTaldXA==} + engines: {node: '>= 0.4'} + + es-set-tostringtag@2.1.0: + resolution: {integrity: sha512-j6vWzfrGVfyXxge+O0x5sh6cvxAog0a/4Rdd2K36zCMV5eJ+/+tOAngRO8cODMNWbVRdVlmGZQL2YS3yR8bIUA==} + engines: {node: '>= 0.4'} + + es-to-primitive@1.3.0: + resolution: {integrity: sha512-w+5mJ3GuFL+NjVtJlvydShqE1eN3h3PbI7/5LAsYJP/2qtuMXjfL2LpHSRqo4b4eSF5K/DH1JXKUAHSB2UW50g==} + engines: {node: '>= 0.4'} + + esbuild@0.25.3: + resolution: {integrity: sha512-qKA6Pvai73+M2FtftpNKRxJ78GIjmFXFxd/1DVBqGo/qNhLSfv+G12n9pNoWdytJC8U00TrViOwpjT0zgqQS8Q==} + engines: {node: '>=18'} + hasBin: true + + escalade@3.2.0: + resolution: {integrity: sha512-WUj2qlxaQtO4g6Pq5c29GTcWGDyd8itL8zTlipgECz3JesAiiOKotd8JU6otB3PACgG6xkJUyVhboMS+bje/jA==} + engines: {node: '>=6'} + + escape-string-regexp@1.0.5: + resolution: {integrity: sha512-vbRorB5FUQWvla16U8R/qgaFIya2qGzwDrNmCZuYKrbdSUMG6I1ZCGQRefkRVhuOkIGVne7BQ35DSfo1qvJqFg==} + engines: {node: '>=0.8.0'} + + escape-string-regexp@4.0.0: + resolution: {integrity: sha512-TtpcNJ3XAzx3Gq8sWRzJaVajRs0uVxA2YAkdb1jm2YkPz4G6egUFAyA3n5vtEIZefPk5Wa4UXbKuS5fKkJWdgA==} + engines: {node: '>=10'} + + escape-string-regexp@5.0.0: + resolution: {integrity: sha512-/veY75JbMK4j1yjvuUxuVsiS/hr/4iHs9FTT6cgTexxdE0Ly/glccBAkloH/DofkjRbZU3bnoj38mOmhkZ0lHw==} + engines: {node: '>=12'} + + esm-env@1.2.2: + resolution: {integrity: sha512-Epxrv+Nr/CaL4ZcFGPJIYLWFom+YeV1DqMLHJoEd9SYRxNbaFruBwfEX/kkHUJf55j2+TUbmDcmuilbP1TmXHA==} + + esrap@1.4.6: + resolution: {integrity: sha512-F/D2mADJ9SHY3IwksD4DAXjTt7qt7GWUf3/8RhCNWmC/67tyb55dpimHmy7EplakFaflV0R/PC+fdSPqrRHAQw==} + + estree-walker@0.6.1: + resolution: {integrity: sha512-SqmZANLWS0mnatqbSfRP5g8OXZC12Fgg1IwNtLsyHDzJizORW4khDfjPqJZsemPWBB2uqykUah5YpQ6epsqC/w==} + + estree-walker@1.0.1: + resolution: {integrity: sha512-1fMXF3YP4pZZVozF8j/ZLfvnR8NSIljt56UhbZ5PeeDmmGHpgpdwQt7ITlGvYaQukCvuBRMLEiKiYC+oeIg4cg==} + + estree-walker@2.0.2: + resolution: {integrity: sha512-Rfkk/Mp/DL7JVje3u18FxFujQlTNR2q6QfMSMB7AvCBx91NGj/ba3kCfza0f6dVDbw7YlRf/nDrn7pQrCCyQ/w==} + + estree-walker@3.0.3: + resolution: {integrity: sha512-7RUKfXgSMMkzt6ZuXmqapOurLGPPfgj6l9uRZ7lRGolvk0y2yocc35LdcxKC5PQZdn2DMqioAQ2NoWcrTKmm6g==} + + esutils@2.0.3: + resolution: {integrity: sha512-kVscqXk4OCp68SZ0dkgEKVi6/8ij300KBWTJq32P/dYeWTSwK41WyTxalN1eRmA5Z9UU/LX9D7FWSmV9SAYx6g==} + engines: {node: '>=0.10.0'} + + eventemitter3@4.0.7: + resolution: {integrity: sha512-8guHBZCwKnFhYdHr2ysuRWErTwhoN2X8XELRlrRwpmfeY2jjuUN4taQMsULKUVo1K4DvZl+0pgfyoysHxvmvEw==} + + eventemitter3@5.0.1: + resolution: {integrity: sha512-GWkBvjiSZK87ELrYOSESUYeVIc9mvLLf/nXalMOS5dYrgZq9o5OVkbZAVM06CVxYsCwH9BDZFPlQTlPA1j4ahA==} + + expressive-code@0.41.3: + resolution: {integrity: sha512-YLnD62jfgBZYrXIPQcJ0a51Afv9h8VlWqEGK9uU2T5nL/5rb8SnA86+7+mgCZe5D34Tff5RNEA5hjNVJYHzrFg==} + + exsolve@1.0.4: + resolution: {integrity: sha512-xsZH6PXaER4XoV+NiT7JHp1bJodJVT+cxeSH1G0f0tlT0lJqYuHUP3bUx2HtfTDvOagMINYp8rsqusxud3RXhw==} + + extend@3.0.2: + resolution: {integrity: sha512-fjquC59cD7CyW6urNXK0FBufkZcoiGG80wTuPujX590cB5Ttln20E2UB4S/WARVqhXffZl2LNgS+gQdPIIim/g==} + + extract-zip@2.0.1: + resolution: {integrity: sha512-GDhU9ntwuKyGXdZBUgTIe+vXnWj0fppUEtMDL0+idd5Sta8TGpHssn/eusA9mrPr9qNDym6SxAYZjNvCn/9RBg==} + engines: {node: '>= 10.17.0'} + hasBin: true + + fast-deep-equal@3.1.3: + resolution: {integrity: sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==} + + fast-glob@3.3.3: + resolution: {integrity: sha512-7MptL8U0cqcFdzIzwOTHoilX9x5BrNqye7Z/LuC7kCMRio1EMSyqRK3BEAUD7sXRq4iT4AzTVuZdhgQ2TCvYLg==} + engines: {node: '>=8.6.0'} + + fast-json-stable-stringify@2.1.0: + resolution: {integrity: sha512-lhd/wF+Lk98HZoTCtlVraHtfh5XYijIjalXck7saUtuanSDyLMxnHhSXEDJqHxD7msR8D0uCmqlkwjCV8xvwHw==} + + fast-uri@3.0.6: + resolution: {integrity: sha512-Atfo14OibSv5wAp4VWNsFYE1AchQRTv9cBGWET4pZWHzYshFSS9NQI6I57rdKn9croWVMbYFbLhJ+yJvmZIIHw==} + + fast-xml-parser@4.5.3: + resolution: {integrity: sha512-RKihhV+SHsIUGXObeVy9AXiBbFwkVk7Syp8XgwN5U3JV416+Gwp/GO9i0JYKmikykgz/UHRrrV4ROuZEo/T0ig==} + hasBin: true + + fastq@1.19.1: + resolution: {integrity: sha512-GwLTyxkCXjXbxqIhTsMI2Nui8huMPtnxg7krajPJAjnEG/iiOS7i+zCtWGZR9G0NBKbXKh6X9m9UIsYX/N6vvQ==} + + fd-slicer@1.1.0: + resolution: {integrity: sha512-cE1qsB/VwyQozZ+q1dGxR8LBYNZeofhEdUNGSMbQD3Gw2lAzX9Zb3uIU6Ebc/Fmyjo9AWWfnn0AUCHqtevs/8g==} + + fdir@6.4.4: + resolution: {integrity: sha512-1NZP+GK4GfuAv3PqKvxQRDMjdSRZjnkq7KfhlNrCNNlZ0ygQFpebfrnfnq/W7fpUnAv9aGWmY1zKx7FYL3gwhg==} + peerDependencies: + picomatch: ^3 || ^4 + peerDependenciesMeta: + picomatch: + optional: true + + figures@1.7.0: + resolution: {integrity: sha512-UxKlfCRuCBxSXU4C6t9scbDyWZ4VlaFFdojKtzJuSkuOBQ5CNFum+zZXFwHjo+CxBC1t6zlYPgHIgFjL8ggoEQ==} + engines: {node: '>=0.10.0'} + + file-uri-to-path@1.0.0: + resolution: {integrity: sha512-0Zt+s3L7Vf1biwWZ29aARiVYLx7iMGnEUl9x33fbB/j3jR81u/O2LbqK+Bm1CDSNDKVtJ/YjwY7TUd5SkeLQLw==} + + filelist@1.0.4: + resolution: {integrity: sha512-w1cEuf3S+DrLCQL7ET6kz+gmlJdbq9J7yXCSjK/OZCPA+qEN1WyF4ZAf0YYJa4/shHJra2t/d/r8SV4Ji+x+8Q==} + + filesize@6.4.0: + resolution: {integrity: sha512-mjFIpOHC4jbfcTfoh4rkWpI31mF7viw9ikj/JyLoKzqlwG/YsefKfvYlYhdYdg/9mtK2z1AzgN/0LvVQ3zdlSQ==} + engines: {node: '>= 0.4.0'} + + fill-range@7.1.1: + resolution: {integrity: sha512-YsGpe3WHLK8ZYi4tWDg2Jy3ebRz2rXowDxnld4bkQB00cc/1Zw9AWnC0i9ztDJitivtQvaI9KaLyKrc+hBW0yg==} + engines: {node: '>=8'} + + find-cache-dir@3.3.2: + resolution: {integrity: sha512-wXZV5emFEjrridIgED11OoUKLxiYjAcqot/NJdAkOhlJ+vGzwhOAfcG5OX1jP+S0PcjEn8bdMJv+g2jwQ3Onig==} + engines: {node: '>=8'} + + find-up@4.1.0: + resolution: {integrity: sha512-PpOwAdQ/YlXQ2vj8a3h8IipDuYRi3wceVQQGYWxNINccq40Anw7BlsEXCMbt1Zt+OLA6Fq9suIpIWD0OsnISlw==} + engines: {node: '>=8'} + + flattie@1.1.1: + resolution: {integrity: sha512-9UbaD6XdAL97+k/n+N7JwX46K/M6Zc6KcFYskrYL8wbBV/Uyk0CTAMY0VT+qiK5PM7AIc9aTWYtq65U7T+aCNQ==} + engines: {node: '>=8'} + + focus-options-polyfill@1.6.0: + resolution: {integrity: sha512-uyrAmLZrPnUItQY5wTdg31TO9GGZRGsh/jmohUg9oLmLi/sw5y7LlTV/mwyd6rvbxIOGwmRiv6LcTS8w7Bk9NQ==} + + follow-redirects@1.15.9: + resolution: {integrity: sha512-gew4GsXizNgdoRyqmyfMHyAmXsZDk6mHkSxZFCzW9gwlbtOW44CDtYavM+y+72qD/Vq2l550kMF52DT8fOLJqQ==} + engines: {node: '>=4.0'} + peerDependencies: + debug: '*' + peerDependenciesMeta: + debug: + optional: true + + fontkit@2.0.4: + resolution: {integrity: sha512-syetQadaUEDNdxdugga9CpEYVaQIxOwk7GlwZWWZ19//qW4zE5bknOKeMBDYAASwnpaSHKJITRLMF9m1fp3s6g==} + + for-each@0.3.5: + resolution: {integrity: sha512-dKx12eRCVIzqCxFGplyFKJMPvLEWgmNtUrpTiJIR5u97zEhRG8ySrtboPHZXx7daLxQVrl643cTzbab2tkQjxg==} + engines: {node: '>= 0.4'} + + foreground-child@3.3.1: + resolution: {integrity: sha512-gIXjKqtFuWEgzFRJA9WCQeSJLZDjgJUOMCMzxtvFq/37KojM1BFGufqsCy0r4qSQmYLsZYMeyRqzIWOMup03sw==} + engines: {node: '>=14'} + + form-data@4.0.2: + resolution: {integrity: sha512-hGfm/slu0ZabnNt4oaRZ6uREyfCj6P4fT/n6A1rGV+Z0VdGXjfOhVUpkn6qVQONHGIFwmveGXyDs75+nr6FM8w==} + engines: {node: '>= 6'} + + fraction.js@4.3.7: + resolution: {integrity: sha512-ZsDfxO51wGAXREY55a7la9LScWpwv9RxIrYABrlvOFBlH/ShPnrtsXeuUIfXKKOVicNxQ+o8JTbJvjS4M89yew==} + + fs-extra@10.1.0: + resolution: {integrity: sha512-oRXApq54ETRj4eMiFzGnHWGy+zo5raudjuxN0b8H7s/RU2oW0Wvsx9O0ACRN/kRq9E8Vu/ReskGB5o3ji+FzHQ==} + engines: {node: '>=12'} + + fs-minipass@2.1.0: + resolution: {integrity: sha512-V/JgOLFCS+R6Vcq0slCuaeWEdNC3ouDlJMNIsacH2VtALiu9mV4LPrHc5cDl8k5aw6J8jwgWWpiTo5RYhmIzvg==} + engines: {node: '>= 8'} + + fs.realpath@1.0.0: + resolution: {integrity: sha512-OO0pH2lK6a0hZnAdau5ItzHPI6pUlvI7jMVnxUQRtw4owF2wk8lOSabtGDCTP4Ggrg2MbGnWO9X8K1t4+fGMDw==} + + fsevents@2.3.3: + resolution: {integrity: sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==} + engines: {node: ^8.16.0 || ^10.6.0 || >=11.0.0} + os: [darwin] + + function-bind@1.1.2: + resolution: {integrity: sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA==} + + function.prototype.name@1.1.8: + resolution: {integrity: sha512-e5iwyodOHhbMr/yNrc7fDYG4qlbIvI5gajyzPnb5TCwyhjApznQh1BMFou9b30SevY43gCJKXycoCBjMbsuW0Q==} + engines: {node: '>= 0.4'} + + functions-have-names@1.2.3: + resolution: {integrity: sha512-xckBUXyTIqT97tq2x2AMb+g163b5JFysYk0x4qxNFwbfQkmNZoiRHb6sPzI9/QV33WeuvVYBUIiD4NzNIyqaRQ==} + + generic-names@4.0.0: + resolution: {integrity: sha512-ySFolZQfw9FoDb3ed9d80Cm9f0+r7qj+HJkWjeD9RBfpxEVTlVhol+gvaQB/78WbwYfbnNh8nWHHBSlg072y6A==} + + gensync@1.0.0-beta.2: + resolution: {integrity: sha512-3hN7NaskYvMDLQY55gnW3NQ+mesEAepTqlg+VEbj7zzqEMBVNhzcGYYeqFo/TlYz6eQiFcp1HcsCZO+nGgS8zg==} + engines: {node: '>=6.9.0'} + + get-caller-file@2.0.5: + resolution: {integrity: sha512-DyFP3BM/3YHTQOCUL/w0OZHR0lpKeGrxotcHWcqNEdnltqFwXVfhEBQ94eIo34AfQpo0rGki4cyIiftY06h2Fg==} + engines: {node: 6.* || 8.* || >= 10.*} + + get-east-asian-width@1.3.0: + resolution: {integrity: sha512-vpeMIQKxczTD/0s2CdEWHcb0eeJe6TFjxb+J5xgX7hScxqrGuyjmv4c1D4A/gelKfyox0gJJwIHF+fLjeaM8kQ==} + engines: {node: '>=18'} + + get-intrinsic@1.3.0: + resolution: {integrity: sha512-9fSjSaos/fRIVIp+xSJlE6lfwhES7LNtKaCBIamHsjr2na1BiABJPo0mOjjz8GJDURarmCPGqaiVg5mfjb98CQ==} + engines: {node: '>= 0.4'} + + get-proto@1.0.1: + resolution: {integrity: sha512-sTSfBjoXBp89JvIKIefqw7U2CCebsc74kiY6awiGogKtoSGbgjYE/G/+l9sF3MWFPNc9IcoOC4ODfKHfxFmp0g==} + engines: {node: '>= 0.4'} + + get-stream@5.2.0: + resolution: {integrity: sha512-nBF+F1rAZVCu/p7rjzgA+Yb4lfYXrpl7a6VmJrU8wF9I1CKvP/QwPNZHnOlwbTkY6dvtFIzFMSyQXbLoTQPRpA==} + engines: {node: '>=8'} + + get-symbol-description@1.1.0: + resolution: {integrity: sha512-w9UMqWwJxHNOvoNzSJ2oPF5wvYcvP7jUvYzhp67yEhTi17ZDBBC1z9pTdGuzjD+EFIqLSYRweZjqfiPzQ06Ebg==} + engines: {node: '>= 0.4'} + + github-slugger@2.0.0: + resolution: {integrity: sha512-IaOQ9puYtjrkq7Y0Ygl9KDZnrf/aiUJYUpVf89y8kyaxbRG7Y1SrX/jaumrv81vc61+kiMempujsM3Yw7w5qcw==} + + glob-parent@5.1.2: + resolution: {integrity: sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow==} + engines: {node: '>= 6'} + + glob-parent@6.0.2: + resolution: {integrity: sha512-XxwI8EOhVQgWp6iDL+3b0r86f4d6AX6zSU55HfB4ydCEuXLXc5FcYeOu+nnGftS4TEju/11rt4KJPTMgbfmv4A==} + engines: {node: '>=10.13.0'} + + glob@10.4.5: + resolution: {integrity: sha512-7Bv8RF0k6xjo7d4A/PxYLbUCfb6c+Vpd2/mB2yRDlew7Jb5hEXiCD9ibfO7wpk8i4sevK6DFny9h7EYbM3/sHg==} + hasBin: true + + glob@11.0.3: + resolution: {integrity: sha512-2Nim7dha1KVkaiF4q6Dj+ngPPMdfvLJEOpZk/jKiUAkqKebpGAWQXAq9z1xu9HKu5lWfqw/FASuccEjyznjPaA==} + engines: {node: 20 || >=22} + hasBin: true + + glob@7.2.3: + resolution: {integrity: sha512-nFR0zLpU2YCaRxwoCJvL6UvCH2JFyFVIvwTLsIf21AuHlMskA1hhTdk+LlYJtOlYt9v6dvszD2BGRqBL+iQK9Q==} + deprecated: Glob versions prior to v9 are no longer supported + + globals@11.12.0: + resolution: {integrity: sha512-WOBp/EEGUiIsJSp7wcv/y6MO+lV9UoncWqxuFfm8eBwzWNgyfBd6Gz+IeKQ9jCmyhoH99g15M3T+QaVHFjizVA==} + engines: {node: '>=4'} + + globals@15.15.0: + resolution: {integrity: sha512-7ACyT3wmyp3I61S4fG682L0VA2RGD9otkqGJIwNUMF1SWUombIIk+af1unuDYgMm082aHYwD+mzJvv9Iu8dsgg==} + engines: {node: '>=18'} + + globalthis@1.0.4: + resolution: {integrity: sha512-DpLKbNU4WylpxJykQujfCcwYWiV/Jhm50Goo0wrVILAv5jOr9d+H+UR3PhSCD2rCCEIg0uc+G+muBTwD54JhDQ==} + engines: {node: '>= 0.4'} + + globalyzer@0.1.0: + resolution: {integrity: sha512-40oNTM9UfG6aBmuKxk/giHn5nQ8RVz/SS4Ir6zgzOv9/qC3kKZ9v4etGTcJbEl/NyVQH7FGU7d+X1egr57Md2Q==} + + globrex@0.1.2: + resolution: {integrity: sha512-uHJgbwAMwNFf5mLst7IWLNg14x1CkeqglJb/K3doi4dw6q2IvAAmM/Y81kevy83wP+Sst+nutFTYOGg3d1lsxg==} + + gopd@1.2.0: + resolution: {integrity: sha512-ZUKRh6/kUFoAiTAtTYPZJ3hw9wNxx+BIBOijnlG9PnrJsCcSjs1wyyD6vJpaYtgnzDrKYRSqf3OO6Rfa93xsRg==} + engines: {node: '>= 0.4'} + + graceful-fs@4.2.11: + resolution: {integrity: sha512-RbJ5/jmFcNNCcDV5o9eTnBLJ/HszWV0P73bc+Ff4nS/rJj+YaS6IGyiOL0VoBYX+l1Wrl3k63h/KrH+nhJ0XvQ==} + + gzip-size@3.0.0: + resolution: {integrity: sha512-6s8trQiK+OMzSaCSVXX+iqIcLV9tC+E73jrJrJTyS4h/AJhlxHvzFKqM1YLDJWRGgHX8uLkBeXkA0njNj39L4w==} + engines: {node: '>=0.12.0'} + + gzip-size@6.0.0: + resolution: {integrity: sha512-ax7ZYomf6jqPTQ4+XCpUGyXKHk5WweS+e05MBO4/y3WJ5RkmPXNKvX+bx1behVILVwr6JSQvZAku021CHPXG3Q==} + engines: {node: '>=10'} + + h3@1.15.3: + resolution: {integrity: sha512-z6GknHqyX0h9aQaTx22VZDf6QyZn+0Nh+Ym8O/u0SGSkyF5cuTJYKlc8MkzW3Nzf9LE1ivcpmYC3FUGpywhuUQ==} + + has-ansi@2.0.0: + resolution: {integrity: sha512-C8vBJ8DwUCx19vhm7urhTuUsr4/IyP6l4VzNQDv+ryHQObW3TTTp9yB68WpYgRe2bbaGuZ/se74IqFeVnMnLZg==} + engines: {node: '>=0.10.0'} + + has-bigints@1.1.0: + resolution: {integrity: sha512-R3pbpkcIqv2Pm3dUwgjclDRVmWpTJW2DcMzcIhEXEx1oh/CEMObMm3KLmRJOdvhM7o4uQBnwr8pzRK2sJWIqfg==} + engines: {node: '>= 0.4'} + + has-flag@4.0.0: + resolution: {integrity: sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==} + engines: {node: '>=8'} + + has-property-descriptors@1.0.2: + resolution: {integrity: sha512-55JNKuIW+vq4Ke1BjOTjM2YctQIvCT7GFzHwmfZPGo5wnrgkid0YQtnAleFSqumZm4az3n2BS+erby5ipJdgrg==} + + has-proto@1.2.0: + resolution: {integrity: sha512-KIL7eQPfHQRC8+XluaIw7BHUwwqL19bQn4hzNgdr+1wXoU0KKj6rufu47lhY7KbJR2C6T6+PfyN0Ea7wkSS+qQ==} + engines: {node: '>= 0.4'} + + has-symbols@1.1.0: + resolution: {integrity: sha512-1cDNdwJ2Jaohmb3sg4OmKaMBwuC48sYni5HUw2DvsC8LjGTLK9h+eb1X6RyuOHe4hT0ULCW68iomhjUoKUqlPQ==} + engines: {node: '>= 0.4'} + + has-tostringtag@1.0.2: + resolution: {integrity: sha512-NqADB8VjPFLM2V0VvHUewwwsw0ZWBaIdgo+ieHtK3hasLz4qeCRjYcqfB6AQrBggRKppKF8L52/VqdVsO47Dlw==} + engines: {node: '>= 0.4'} + + hasown@2.0.2: + resolution: {integrity: sha512-0hJU9SCPvmMzIBdZFqNPXWa6dqh7WdH0cII9y+CyS8rG3nL48Bclra9HmKhVVUHyPWNH5Y7xDwAB7bfgSjkUMQ==} + engines: {node: '>= 0.4'} + + hast-util-from-dom@5.0.1: + resolution: {integrity: sha512-N+LqofjR2zuzTjCPzyDUdSshy4Ma6li7p/c3pA78uTwzFgENbgbUrm2ugwsOdcjI1muO+o6Dgzp9p8WHtn/39Q==} + + hast-util-from-html-isomorphic@2.0.0: + resolution: {integrity: sha512-zJfpXq44yff2hmE0XmwEOzdWin5xwH+QIhMLOScpX91e/NSGPsAzNCvLQDIEPyO2TXi+lBmU6hjLIhV8MwP2kw==} + + hast-util-from-html@2.0.3: + resolution: {integrity: sha512-CUSRHXyKjzHov8yKsQjGOElXy/3EKpyX56ELnkHH34vDVw1N1XSQ1ZcAvTyAPtGqLTuKP/uxM+aLkSPqF/EtMw==} + + hast-util-from-parse5@8.0.3: + resolution: {integrity: sha512-3kxEVkEKt0zvcZ3hCRYI8rqrgwtlIOFMWkbclACvjlDw8Li9S2hk/d51OI0nr/gIpdMHNepwgOKqZ/sy0Clpyg==} + + hast-util-has-property@3.0.0: + resolution: {integrity: sha512-MNilsvEKLFpV604hwfhVStK0usFY/QmM5zX16bo7EjnAEGofr5YyI37kzopBlZJkHD4t887i+q/C8/tr5Q94cA==} + + hast-util-heading-rank@3.0.0: + resolution: {integrity: sha512-EJKb8oMUXVHcWZTDepnr+WNbfnXKFNf9duMesmr4S8SXTJBJ9M4Yok08pu9vxdJwdlGRhVumk9mEhkEvKGifwA==} + + hast-util-is-element@3.0.0: + resolution: {integrity: sha512-Val9mnv2IWpLbNPqc/pUem+a7Ipj2aHacCwgNfTiK0vJKl0LF+4Ba4+v1oPHFpf3bLYmreq0/l3Gud9S5OH42g==} + + hast-util-parse-selector@3.1.1: + resolution: {integrity: sha512-jdlwBjEexy1oGz0aJ2f4GKMaVKkA9jwjr4MjAAI22E5fM/TXVZHuS5OpONtdeIkRKqAaryQ2E9xNQxijoThSZA==} + + hast-util-parse-selector@4.0.0: + resolution: {integrity: sha512-wkQCkSYoOGCRKERFWcxMVMOcYE2K1AaNLU8DXS9arxnLOUEWbOXKXiJUNzEpqZ3JOKpnha3jkFrumEjVliDe7A==} + + hast-util-raw@9.1.0: + resolution: {integrity: sha512-Y8/SBAHkZGoNkpzqqfCldijcuUKh7/su31kEBp67cFY09Wy0mTRgtsLYsiIxMJxlu0f6AA5SUTbDR8K0rxnbUw==} + + hast-util-select@6.0.4: + resolution: {integrity: sha512-RqGS1ZgI0MwxLaKLDxjprynNzINEkRHY2i8ln4DDjgv9ZhcYVIHN9rlpiYsqtFwrgpYU361SyWDQcGNIBVu3lw==} + + hast-util-to-html@9.0.5: + resolution: {integrity: sha512-OguPdidb+fbHQSU4Q4ZiLKnzWo8Wwsf5bZfbvu7//a9oTYoqD/fWpe96NuHkoS9h0ccGOTe0C4NGXdtS0iObOw==} + + hast-util-to-parse5@8.0.0: + resolution: {integrity: sha512-3KKrV5ZVI8if87DVSi1vDeByYrkGzg4mEfeu4alwgmmIeARiBLKCZS2uw5Gb6nU9x9Yufyj3iudm6i7nl52PFw==} + + hast-util-to-string@3.0.1: + resolution: {integrity: sha512-XelQVTDWvqcl3axRfI0xSeoVKzyIFPwsAGSLIsKdJKQMXDYJS4WYrBNF/8J7RdhIcFI2BOHgAifggsvsxp/3+A==} + + hast-util-to-text@4.0.2: + resolution: {integrity: sha512-KK6y/BN8lbaq654j7JgBydev7wuNMcID54lkRav1P0CaE1e47P72AWWPiGKXTJU271ooYzcvTAn/Zt0REnvc7A==} + + hast-util-whitespace@3.0.0: + resolution: {integrity: sha512-88JUN06ipLwsnv+dVn+OIYOvAuvBMy/Qoi6O7mQHxdPXpjy+Cd6xRkWwux7DKO+4sYILtLBRIKgsdpS2gQc7qw==} + + hastscript@7.2.0: + resolution: {integrity: sha512-TtYPq24IldU8iKoJQqvZOuhi5CyCQRAbvDOX0x1eW6rsHSxa/1i2CCiptNTotGHJ3VoHRGmqiv6/D3q113ikkw==} + + hastscript@9.0.1: + resolution: {integrity: sha512-g7df9rMFX/SPi34tyGCyUBREQoKkapwdY/T04Qn9TDWfHhAYt4/I0gMVirzK5wEzeUqIjEB+LXC/ypb7Aqno5w==} + + he@1.2.0: + resolution: {integrity: sha512-F/1DnUGPopORZi0ni+CvrCgHQ5FyEAHRLSApuYWMmrbSwoN2Mn/7k+Gl38gJnR7yyDZk6WLXwiGod1JOWNDKGw==} + hasBin: true + + html-escaper@3.0.3: + resolution: {integrity: sha512-RuMffC89BOWQoY0WKGpIhn5gX3iI54O6nRA0yC124NYVtzjmFWBIiFd8M0x+ZdX0P9R4lADg1mgP8C7PxGOWuQ==} + + html-void-elements@3.0.0: + resolution: {integrity: sha512-bEqo66MRXsUGxWHV5IP0PUiAWwoEjba4VCzg0LjFJBpchPaTfyfCKTG6bc5F8ucKec3q5y6qOdGyYTSBEvhCrg==} + + htmlparser2@8.0.2: + resolution: {integrity: sha512-GYdjWKDkbRLkZ5geuHs5NY1puJ+PXwP7+fHPRz06Eirsb9ugf6d8kkXav6ADhcODhFFPMIXyxkxSuMf3D6NCFA==} + + htmlparser2@9.1.0: + resolution: {integrity: sha512-5zfg6mHUoaer/97TxnGpxmbR7zJtPwIYFMZ/H5ucTlPZhKvtum05yiPK3Mgai3a0DyVxv7qYqoweaEd2nrYQzQ==} + + http-cache-semantics@4.1.1: + resolution: {integrity: sha512-er295DKPVsV82j5kw1Gjt+ADA/XYHsajl82cGNQG2eyoPkvgUhX+nDIyelzhIWbbsXP39EHcI6l5tYs2FYqYXQ==} + + https-proxy-agent@7.0.6: + resolution: {integrity: sha512-vK9P5/iUfdl95AI+JVyUuIcVtd4ofvtrOr3HNtM2yxC9bnMbEdp3x01OhQNnjb8IJYi38VlTE3mBXwcfvywuSw==} + engines: {node: '>= 14'} + + iconv-lite@0.6.3: + resolution: {integrity: sha512-4fCk79wshMdzMp2rH06qWrJE4iolqLhCUH+OiuIgU++RB0+94NlDL81atO7GX55uUKueo0txHNtvEyI6D7WdMw==} + engines: {node: '>=0.10.0'} + + icss-replace-symbols@1.1.0: + resolution: {integrity: sha512-chIaY3Vh2mh2Q3RGXttaDIzeiPvaVXJ+C4DAh/w3c37SKZ/U6PGMmuicR2EQQp9bKG8zLMCl7I+PtIoOOPp8Gg==} + + icss-utils@5.1.0: + resolution: {integrity: sha512-soFhflCVWLfRNOPU3iv5Z9VUdT44xFRbzjLsEzSr5AQmgqPMTHdU3PMT1Cf1ssx8fLNJDA1juftYl+PUcv3MqA==} + engines: {node: ^10 || ^12 || >= 14} + peerDependencies: + postcss: ^8.1.0 + + immutable@4.3.7: + resolution: {integrity: sha512-1hqclzwYwjRDFLjcFxOM5AYkkG0rpFPpr1RLPMEuGczoS7YA8gLhy8SWXYRAA/XwfEHpfo3cw5JGioS32fnMRw==} + + import-cwd@3.0.0: + resolution: {integrity: sha512-4pnzH16plW+hgvRECbDWpQl3cqtvSofHWh44met7ESfZ8UZOWWddm8hEyDTqREJ9RbYHY8gi8DqmaelApoOGMg==} + engines: {node: '>=8'} + + import-fresh@3.3.1: + resolution: {integrity: sha512-TR3KfrTZTYLPB6jUjfx6MF9WcWrHL9su5TObK4ZkYgBdWKPOFoSoQIdEuTuR82pmtxH2spWG9h6etwfr1pLBqQ==} + engines: {node: '>=6'} + + import-from@3.0.0: + resolution: {integrity: sha512-CiuXOFFSzkU5x/CR0+z7T91Iht4CXgfCxVOFRhh2Zyhg5wOpWvvDLQUsWl+gcN+QscYBjez8hDCt85O7RLDttQ==} + engines: {node: '>=8'} + + import-meta-resolve@4.1.0: + resolution: {integrity: sha512-I6fiaX09Xivtk+THaMfAwnA3MVA5Big1WHF1Dfx9hFuvNIWpXnorlkzhcQf6ehrqQiiZECRt1poOAkPmer3ruw==} + + inflight@1.0.6: + resolution: {integrity: sha512-k92I/b08q4wvFscXCLvqfsHCrjrF7yiXsQuIVvVE7N82W3+aqpzuUdBbfhWcy/FZR3/4IgflMgKLOsvPDrGCJA==} + deprecated: This module is not supported, and leaks memory. Do not use it. Check out lru-cache if you want a good and tested way to coalesce async requests by a key value, which is much more comprehensive and powerful. + + inherits@2.0.4: + resolution: {integrity: sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==} + + internal-slot@1.1.0: + resolution: {integrity: sha512-4gd7VpWNQNB4UKKCFFVcp1AVv+FMOgs9NKzjHKusc8jTMhd5eL1NqQqOpE0KzMds804/yHlglp3uxgluOqAPLw==} + engines: {node: '>= 0.4'} + + interpret@1.4.0: + resolution: {integrity: sha512-agE4QfB2Lkp9uICn7BAqoscw4SZP9kTE2hxiFI3jBPmXJfdqiahTbUuKGsMoN2GtqL9AxhYioAcVvgsb1HvRbA==} + engines: {node: '>= 0.10'} + + iron-webcrypto@1.2.1: + resolution: {integrity: sha512-feOM6FaSr6rEABp/eDfVseKyTMDt+KGpeB35SkVn9Tyn0CqvVsY3EwI0v5i8nMHyJnzCIQf7nsy3p41TPkJZhg==} + + is-absolute-url@4.0.1: + resolution: {integrity: sha512-/51/TKE88Lmm7Gc4/8btclNXWS+g50wXhYJq8HWIBAGUBnoAdRu1aXeh364t/O7wXDAcTJDP8PNuNKWUDWie+A==} + engines: {node: ^12.20.0 || ^14.13.1 || >=16.0.0} + + is-alphabetical@2.0.1: + resolution: {integrity: sha512-FWyyY60MeTNyeSRpkM2Iry0G9hpr7/9kD40mD/cGQEuilcZYS4okz8SN2Q6rLCJ8gbCt6fN+rC+6tMGS99LaxQ==} + + is-alphanumerical@2.0.1: + resolution: {integrity: sha512-hmbYhX/9MUMF5uh7tOXyK/n0ZvWpad5caBA17GsC6vyuCqaWliRG5K1qS9inmUhEMaOBIW7/whAnSwveW/LtZw==} + + is-array-buffer@3.0.5: + resolution: {integrity: sha512-DDfANUiiG2wC1qawP66qlTugJeL5HyzMpfr8lLK+jMQirGzNod0B12cFB/9q838Ru27sBwfw78/rdoU7RERz6A==} + engines: {node: '>= 0.4'} + + is-arrayish@0.2.1: + resolution: {integrity: sha512-zz06S8t0ozoDXMG+ube26zeCTNXcKIPJZJi8hBrF4idCLms4CG9QtK7qBl1boi5ODzFpjswb5JPmHCbMpjaYzg==} + + is-arrayish@0.3.2: + resolution: {integrity: sha512-eVRqCvVlZbuw3GrM63ovNSNAeA1K16kaR/LRY/92w0zxQ5/1YzwblUX652i4Xs9RwAGjW9d9y6X88t8OaAJfWQ==} + + is-async-function@2.1.1: + resolution: {integrity: sha512-9dgM/cZBnNvjzaMYHVoxxfPj2QXt22Ev7SuuPrs+xav0ukGB0S6d4ydZdEiM48kLx5kDV+QBPrpVnFyefL8kkQ==} + engines: {node: '>= 0.4'} + + is-bigint@1.1.0: + resolution: {integrity: sha512-n4ZT37wG78iz03xPRKJrHTdZbe3IicyucEtdRsV5yglwc3GyUfbAfpSeD0FJ41NbUNSt5wbhqfp1fS+BgnvDFQ==} + engines: {node: '>= 0.4'} + + is-binary-path@2.1.0: + resolution: {integrity: sha512-ZMERYes6pDydyuGidse7OsHxtbI7WVeUEozgR/g7rd0xUimYNlvZRE/K2MgZTjWy725IfelLeVcEM97mmtRGXw==} + engines: {node: '>=8'} + + is-boolean-object@1.2.2: + resolution: {integrity: sha512-wa56o2/ElJMYqjCjGkXri7it5FbebW5usLw/nPmCMs5DeZ7eziSYZhSmPRn0txqeW4LnAmQQU7FgqLpsEFKM4A==} + engines: {node: '>= 0.4'} + + is-callable@1.2.7: + resolution: {integrity: sha512-1BC0BVFhS/p0qtw6enp8e+8OD0UrK0oFLztSjNzhcKA3WDuJxxAPXzPuPtKkjEY9UUoEWlX/8fgKeu2S8i9JTA==} + engines: {node: '>= 0.4'} + + is-core-module@2.16.1: + resolution: {integrity: sha512-UfoeMA6fIJ8wTYFEUjelnaGI67v6+N7qXJEvQuIGa99l4xsCruSYOVSQ0uPANn4dAzm8lkYPaKLrrijLq7x23w==} + engines: {node: '>= 0.4'} + + is-data-view@1.0.2: + resolution: {integrity: sha512-RKtWF8pGmS87i2D6gqQu/l7EYRlVdfzemCJN/P3UOs//x1QE7mfhvzHIApBTRf7axvT6DMGwSwBXYCT0nfB9xw==} + engines: {node: '>= 0.4'} + + is-date-object@1.1.0: + resolution: {integrity: sha512-PwwhEakHVKTdRNVOw+/Gyh0+MzlCl4R6qKvkhuvLtPMggI1WAHt9sOwZxQLSGpUaDnrdyDsomoRgNnCfKNSXXg==} + engines: {node: '>= 0.4'} + + is-decimal@2.0.1: + resolution: {integrity: sha512-AAB9hiomQs5DXWcRB1rqsxGUstbRroFOPPVAomNk/3XHR5JyEZChOyTWe2oayKnsSsr/kcGqF+z6yuH6HHpN0A==} + + is-docker@2.2.1: + resolution: {integrity: sha512-F+i2BKsFrH66iaUFc0woD8sLy8getkwTwtOBjvs56Cx4CgJDeKQeqfz8wAYiSb8JOprWhHH5p77PbmYCvvUuXQ==} + engines: {node: '>=8'} + hasBin: true + + is-docker@3.0.0: + resolution: {integrity: sha512-eljcgEDlEns/7AXFosB5K/2nCM4P7FQPkGc/DWLy5rmFEWvZayGrik1d9/QIY5nJ4f9YsVvBkA6kJpHn9rISdQ==} + engines: {node: ^12.20.0 || ^14.13.1 || >=16.0.0} + hasBin: true + + is-extglob@2.1.1: + resolution: {integrity: sha512-SbKbANkN603Vi4jEZv49LeVJMn4yGwsbzZworEoyEiutsN3nJYdbO36zfhGJ6QEDpOZIFkDtnq5JRxmvl3jsoQ==} + engines: {node: '>=0.10.0'} + + is-finalizationregistry@1.1.1: + resolution: {integrity: sha512-1pC6N8qWJbWoPtEjgcL2xyhQOP491EQjeUo3qTKcmV8YSDDJrOepfG8pcC7h/QgnQHYSv0mJ3Z/ZWxmatVrysg==} + engines: {node: '>= 0.4'} + + is-fullwidth-code-point@3.0.0: + resolution: {integrity: sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg==} + engines: {node: '>=8'} + + is-generator-function@1.1.0: + resolution: {integrity: sha512-nPUB5km40q9e8UfN/Zc24eLlzdSf9OfKByBw9CIdw4H1giPMeA0OIJvbchsCu4npfI2QcMVBsGEBHKZ7wLTWmQ==} + engines: {node: '>= 0.4'} + + is-glob@4.0.3: + resolution: {integrity: sha512-xelSayHH36ZgE7ZWhli7pW34hNbNl8Ojv5KVmkJD4hBdD3th8Tfk9vYasLM+mXWOZhFkgZfxhLSnrwRr4elSSg==} + engines: {node: '>=0.10.0'} + + is-hexadecimal@2.0.1: + resolution: {integrity: sha512-DgZQp241c8oO6cA1SbTEWiXeoxV42vlcJxgH+B3hi1AiqqKruZR3ZGF8In3fj4+/y/7rHvlOZLZtgJ/4ttYGZg==} + + is-inside-container@1.0.0: + resolution: {integrity: sha512-KIYLCCJghfHZxqjYBE7rEy0OBuTd5xCHS7tHVgvCLkx7StIoaxwNW3hCALgEUjFfeRk+MG/Qxmp/vtETEF3tRA==} + engines: {node: '>=14.16'} + hasBin: true + + is-map@2.0.3: + resolution: {integrity: sha512-1Qed0/Hr2m+YqxnM09CjA2d/i6YZNfF6R2oRAOj36eUdS6qIV/huPJNSEpKbupewFs+ZsJlxsjjPbc0/afW6Lw==} + engines: {node: '>= 0.4'} + + is-module@1.0.0: + resolution: {integrity: sha512-51ypPSPCoTEIN9dy5Oy+h4pShgJmPCygKfyRCISBI+JoWT/2oJvK8QPxmwv7b/p239jXrm9M1mlQbyKJ5A152g==} + + is-number-object@1.1.1: + resolution: {integrity: sha512-lZhclumE1G6VYD8VHe35wFaIif+CTy5SJIi5+3y4psDgWu4wPDoBhF8NxUOinEc7pHgiTsT6MaBb92rKhhD+Xw==} + engines: {node: '>= 0.4'} + + is-number@7.0.0: + resolution: {integrity: sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng==} + engines: {node: '>=0.12.0'} + + is-plain-obj@4.1.0: + resolution: {integrity: sha512-+Pgi+vMuUNkJyExiMBt5IlFoMyKnr5zhJ4Uspz58WOhBF5QoIZkFyNHIbBAtHwzVAgk5RtndVNsDRN61/mmDqg==} + engines: {node: '>=12'} + + is-plain-object@5.0.0: + resolution: {integrity: sha512-VRSzKkbMm5jMDoKLbltAkFQ5Qr7VDiTFGXxYFXXowVj387GeGNOCsOH6Msy00SGZ3Fp84b1Naa1psqgcCIEP5Q==} + engines: {node: '>=0.10.0'} + + is-reference@1.2.1: + resolution: {integrity: sha512-U82MsXXiFIrjCK4otLT+o2NA2Cd2g5MLoOVXUZjIOhLurrRxpEXzI8O0KZHr3IjLvlAH1kTPYSuqer5T9ZVBKQ==} + + is-reference@3.0.3: + resolution: {integrity: sha512-ixkJoqQvAP88E6wLydLGGqCJsrFUnqoH6HnaczB8XmDH1oaWU+xxdptvikTgaEhtZ53Ky6YXiBuUI2WXLMCwjw==} + + is-regex@1.2.1: + resolution: {integrity: sha512-MjYsKHO5O7mCsmRGxWcLWheFqN9DJ/2TmngvjKXihe6efViPqc274+Fx/4fYj/r03+ESvBdTXK0V6tA3rgez1g==} + engines: {node: '>= 0.4'} + + is-set@2.0.3: + resolution: {integrity: sha512-iPAjerrse27/ygGLxw+EBR9agv9Y6uLeYVJMu+QNCoouJ1/1ri0mGrcWpfCqFZuzzx3WjtwxG098X+n4OuRkPg==} + engines: {node: '>= 0.4'} + + is-shared-array-buffer@1.0.4: + resolution: {integrity: sha512-ISWac8drv4ZGfwKl5slpHG9OwPNty4jOWPRIhBpxOoD+hqITiwuipOQ2bNthAzwA3B4fIjO4Nln74N0S9byq8A==} + engines: {node: '>= 0.4'} + + is-string@1.1.1: + resolution: {integrity: sha512-BtEeSsoaQjlSPBemMQIrY1MY0uM6vnS1g5fmufYOtnxLGUZM2178PKbhsk7Ffv58IX+ZtcvoGwccYsh0PglkAA==} + engines: {node: '>= 0.4'} + + is-symbol@1.1.1: + resolution: {integrity: sha512-9gGx6GTtCQM73BgmHQXfDmLtfjjTUDSyoxTCbp5WtoixAhfgsDirWIcVQ/IHpvI5Vgd5i/J5F7B9cN/WlVbC/w==} + engines: {node: '>= 0.4'} + + is-typed-array@1.1.15: + resolution: {integrity: sha512-p3EcsicXjit7SaskXHs1hA91QxgTw46Fv6EFKKGS5DRFLD8yKnohjF3hxoju94b/OcMZoQukzpPpBE9uLVKzgQ==} + engines: {node: '>= 0.4'} + + is-weakmap@2.0.2: + resolution: {integrity: sha512-K5pXYOm9wqY1RgjpL3YTkF39tni1XajUIkawTLUo9EZEVUFga5gSQJF8nNS7ZwJQ02y+1YCNYcMh+HIf1ZqE+w==} + engines: {node: '>= 0.4'} + + is-weakref@1.1.1: + resolution: {integrity: sha512-6i9mGWSlqzNMEqpCp93KwRS1uUOodk2OJ6b+sq7ZPDSy2WuI5NFIxp/254TytR8ftefexkWn5xNiHUNpPOfSew==} + engines: {node: '>= 0.4'} + + is-weakset@2.0.4: + resolution: {integrity: sha512-mfcwb6IzQyOKTs84CQMrOwW4gQcaTOAWJ0zzJCl2WSPDrWk/OzDaImWFH3djXhb24g4eudZfLRozAvPGw4d9hQ==} + engines: {node: '>= 0.4'} + + is-wsl@2.2.0: + resolution: {integrity: sha512-fKzAra0rGJUUBwGBgNkHZuToZcn+TtXHpeCgmkMJMMYx1sQDYaCSyjJBSCa2nH1DGm7s3n1oBnohoVTBaN7Lww==} + engines: {node: '>=8'} + + is-wsl@3.1.0: + resolution: {integrity: sha512-UcVfVfaK4Sc4m7X3dUSoHoozQGBEFeDC+zVo06t98xe8CzHSZZBekNXH+tu0NalHolcJ/QAGqS46Hef7QXBIMw==} + engines: {node: '>=16'} + + isarray@2.0.5: + resolution: {integrity: sha512-xHjhDr3cNBK0BzdUJSPXZntQUx/mwMS5Rw4A7lPJ90XGAO6ISP/ePDNuo0vhqOZU+UD5JoodwCAAoZQd3FeAKw==} + + isexe@2.0.0: + resolution: {integrity: sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==} + + jackspeak@3.4.3: + resolution: {integrity: sha512-OGlZQpz2yfahA/Rd1Y8Cd9SIEsqvXkLVoSw/cgwhnhFMDbsQFeZYoJJ7bIZBS9BcamUW96asq/npPWugM+RQBw==} + + jackspeak@4.1.1: + resolution: {integrity: sha512-zptv57P3GpL+O0I7VdMJNBZCu+BPHVQUk55Ft8/QCJjTVxrnJHuVuX/0Bl2A6/+2oyR/ZMEuFKwmzqqZ/U5nPQ==} + engines: {node: 20 || >=22} + + jake@10.9.2: + resolution: {integrity: sha512-2P4SQ0HrLQ+fw6llpLnOaGAvN2Zu6778SJMrCUwns4fOoG9ayrTiZk3VV8sCPkVZF8ab0zksVpS8FDY5pRCNBA==} + engines: {node: '>=10'} + hasBin: true + + jest-worker@26.6.2: + resolution: {integrity: sha512-KWYVV1c4i+jbMpaBC+U++4Va0cp8OisU185o73T1vo99hqi7w8tSJfUXYswwqqrjzwxa6KpRK54WhPvwf5w6PQ==} + engines: {node: '>= 10.13.0'} + + jiti@1.21.7: + resolution: {integrity: sha512-/imKNG4EbWNrVjoNC/1H5/9GFy+tqjGBHCaSsN+P2RnPqjsLmv6UD3Ej+Kj8nBWaRAwyk7kK5ZUc+OEatnTR3A==} + hasBin: true + + js-tokens@4.0.0: + resolution: {integrity: sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==} + + js-yaml@4.1.0: + resolution: {integrity: sha512-wpxZs9NoxZaJESJGIZTyDEaYpl0FKSA+FB9aJiyemKhMwkxQg63h4T1KJgUGHpTqPDNRcmmYLugrRjJlBtWvRA==} + hasBin: true + + jsesc@3.0.2: + resolution: {integrity: sha512-xKqzzWXDttJuOcawBt4KnKHHIf5oQ/Cxax+0PWFG+DFDgHNAdi+TXECADI+RYiFUMmx8792xsMbbgXj4CwnP4g==} + engines: {node: '>=6'} + hasBin: true + + jsesc@3.1.0: + resolution: {integrity: sha512-/sM3dO2FOzXjKQhJuo0Q173wf2KOo8t4I8vHy6lF9poUp7bKT0/NHE8fPX23PwfhnykfqnC2xRxOnVw5XuGIaA==} + engines: {node: '>=6'} + hasBin: true + + json-parse-even-better-errors@2.3.1: + resolution: {integrity: sha512-xyFwyhro/JEof6Ghe2iz2NcXoj2sloNsWr/XsERDK/oiPCfaNhl5ONfp+jQdAZRQQ0IJWNzH9zIZF7li91kh2w==} + + json-schema-traverse@0.4.1: + resolution: {integrity: sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg==} + + json-schema-traverse@1.0.0: + resolution: {integrity: sha512-NM8/P9n3XjXhIZn1lLhkFaACTOURQXjWhV4BA/RnOv8xvgqtqpAX9IO4mRQxSx1Rlo4tqzeqb0sOlruaOy3dug==} + + json5@2.2.3: + resolution: {integrity: sha512-XmOWe7eyHYH14cLdVPoyg+GOH3rYX++KpzrylJwSW98t3Nk+U8XOl8FWKOgwtzdb8lXGf6zYwDUzeHMWfxasyg==} + engines: {node: '>=6'} + hasBin: true + + jsonc-parser@2.3.1: + resolution: {integrity: sha512-H8jvkz1O50L3dMZCsLqiuB2tA7muqbSg1AtGEkN0leAqGjsUzDJir3Zwr02BhqdcITPg3ei3mZ+HjMocAknhhg==} + + jsonc-parser@3.3.1: + resolution: {integrity: sha512-HUgH65KyejrUFPvHFPbqOY0rsFip3Bo5wb4ngvdi1EpCYWUQDC5V+Y7mZws+DLkr4M//zQJoanu1SP+87Dv1oQ==} + + jsonfile@6.1.0: + resolution: {integrity: sha512-5dgndWOriYSm5cnYaJNhalLNDKOqFwyDB/rr1E9ZsGciGvKPs8R2xYGCacuf3z6K1YKDz182fd+fY3cn3pMqXQ==} + + katex@0.16.22: + resolution: {integrity: sha512-XCHRdUw4lf3SKBaJe4EvgqIuWwkPSo9XoeO8GjQW94Bp7TWv9hNhzZjZ+OH9yf1UmLygb7DIT5GSFQiyt16zYg==} + hasBin: true + + kleur@3.0.3: + resolution: {integrity: sha512-eTIzlVOSUR+JxdDFepEYcBMtZ9Qqdef+rnzWdRZuMbOywu5tO2w2N7rqjoANZ5k9vywhL6Br1VRjUIgTQx4E8w==} + engines: {node: '>=6'} + + kleur@4.1.5: + resolution: {integrity: sha512-o+NO+8WrRiQEE4/7nwRJhN1HWpVmJm511pBHUxPLtp0BUISzlBplORYSmTclCnJvQq2tKu/sgl3xVpkc7ZWuQQ==} + engines: {node: '>=6'} + + kolorist@1.8.0: + resolution: {integrity: sha512-Y+60/zizpJ3HRH8DCss+q95yr6145JXZo46OTpFvDZWLfRCE4qChOyk1b26nMaNpfHHgxagk9dXT5OP0Tfe+dQ==} + + lightningcss-darwin-arm64@1.29.3: + resolution: {integrity: sha512-fb7raKO3pXtlNbQbiMeEu8RbBVHnpyqAoxTyTRMEWFQWmscGC2wZxoHzZ+YKAepUuKT9uIW5vL2QbFivTgprZg==} + engines: {node: '>= 12.0.0'} + cpu: [arm64] + os: [darwin] + + lightningcss-darwin-x64@1.29.3: + resolution: {integrity: sha512-KF2XZ4ZdmDGGtEYmx5wpzn6u8vg7AdBHaEOvDKu8GOs7xDL/vcU2vMKtTeNe1d4dogkDdi3B9zC77jkatWBwEQ==} + engines: {node: '>= 12.0.0'} + cpu: [x64] + os: [darwin] + + lightningcss-freebsd-x64@1.29.3: + resolution: {integrity: sha512-VUWeVf+V1UM54jv9M4wen9vMlIAyT69Krl9XjI8SsRxz4tdNV/7QEPlW6JASev/pYdiynUCW0pwaFquDRYdxMw==} + engines: {node: '>= 12.0.0'} + cpu: [x64] + os: [freebsd] + + lightningcss-linux-arm-gnueabihf@1.29.3: + resolution: {integrity: sha512-UhgZ/XVNfXQVEJrMIWeK1Laj8KbhjbIz7F4znUk7G4zeGw7TRoJxhb66uWrEsonn1+O45w//0i0Fu0wIovYdYg==} + engines: {node: '>= 12.0.0'} + cpu: [arm] + os: [linux] + + lightningcss-linux-arm64-gnu@1.29.3: + resolution: {integrity: sha512-Pqau7jtgJNmQ/esugfmAT1aCFy/Gxc92FOxI+3n+LbMHBheBnk41xHDhc0HeYlx9G0xP5tK4t0Koy3QGGNqypw==} + engines: {node: '>= 12.0.0'} + cpu: [arm64] + os: [linux] + libc: [glibc] + + lightningcss-linux-arm64-musl@1.29.3: + resolution: {integrity: sha512-dxakOk66pf7KLS7VRYFO7B8WOJLecE5OPL2YOk52eriFd/yeyxt2Km5H0BjLfElokIaR+qWi33gB8MQLrdAY3A==} + engines: {node: '>= 12.0.0'} + cpu: [arm64] + os: [linux] + libc: [musl] + + lightningcss-linux-x64-gnu@1.29.3: + resolution: {integrity: sha512-ySZTNCpbfbK8rqpKJeJR2S0g/8UqqV3QnzcuWvpI60LWxnFN91nxpSSwCbzfOXkzKfar9j5eOuOplf+klKtINg==} + engines: {node: '>= 12.0.0'} + cpu: [x64] + os: [linux] + libc: [glibc] + + lightningcss-linux-x64-musl@1.29.3: + resolution: {integrity: sha512-3pVZhIzW09nzi10usAXfIGTTSTYQ141dk88vGFNCgawIzayiIzZQxEcxVtIkdvlEq2YuFsL9Wcj/h61JHHzuFQ==} + engines: {node: '>= 12.0.0'} + cpu: [x64] + os: [linux] + libc: [musl] + + lightningcss-win32-arm64-msvc@1.29.3: + resolution: {integrity: sha512-VRnkAvtIkeWuoBJeGOTrZxsNp4HogXtcaaLm8agmbYtLDOhQdpgxW6NjZZjDXbvGF+eOehGulXZ3C1TiwHY4QQ==} + engines: {node: '>= 12.0.0'} + cpu: [arm64] + os: [win32] + + lightningcss-win32-x64-msvc@1.29.3: + resolution: {integrity: sha512-IszwRPu2cPnDQsZpd7/EAr0x2W7jkaWqQ1SwCVIZ/tSbZVXPLt6k8s6FkcyBjViCzvB5CW0We0QbbP7zp2aBjQ==} + engines: {node: '>= 12.0.0'} + cpu: [x64] + os: [win32] + + lightningcss@1.29.3: + resolution: {integrity: sha512-GlOJwTIP6TMIlrTFsxTerwC0W6OpQpCGuX1ECRLBUVRh6fpJH3xTqjCjRgQHTb4ZXexH9rtHou1Lf03GKzmhhQ==} + engines: {node: '>= 12.0.0'} + + lilconfig@2.1.0: + resolution: {integrity: sha512-utWOt/GHzuUxnLKxB6dk81RoOeoNeHgbrXiuGk4yyF5qlRz+iIVWu56E2fqGHFrXz0QNUhLB/8nKqvRH66JKGQ==} + engines: {node: '>=10'} + + lilconfig@3.1.3: + resolution: {integrity: sha512-/vlFKAoH5Cgt3Ie+JLhRbwOsCQePABiU3tJ1egGvyQ+33R/vcwM2Zl2QR/LzjsBeItPt3oSVXapn+m4nQDvpzw==} + engines: {node: '>=14'} + + lines-and-columns@1.2.4: + resolution: {integrity: sha512-7ylylesZQ/PV29jhEDl3Ufjo6ZX7gCqJr5F7PKrqc93v7fzSymt1BpwEU8nAUXs8qzzvqhbjhK5QZg6Mt/HkBg==} + + linkify-it@5.0.0: + resolution: {integrity: sha512-5aHCbzQRADcdP+ATqnDuhhJ/MRIqDkZX5pyjFHRRysS8vZ5AbqGEoFIb6pYHPZ+L/OC2Lc+xT8uHVVR5CAK/wQ==} + + loader-utils@3.3.1: + resolution: {integrity: sha512-FMJTLMXfCLMLfJxcX9PFqX5qD88Z5MRGaZCVzfuqeZSPsyiBzs+pahDQjbIWz2QIzPZz0NX9Zy4FX3lmK6YHIg==} + engines: {node: '>= 12.13.0'} + + local-pkg@0.5.1: + resolution: {integrity: sha512-9rrA30MRRP3gBD3HTGnC6cDFpaE1kVDWxWgqWJUN0RvDNAo+Nz/9GxB+nHOH0ifbVFy0hSA1V6vFDvnx54lTEQ==} + engines: {node: '>=14'} + + local-pkg@1.1.1: + resolution: {integrity: sha512-WunYko2W1NcdfAFpuLUoucsgULmgDBRkdxHxWQ7mK0cQqwPiy8E1enjuRBrhLtZkB5iScJ1XIPdhVEFK8aOLSg==} + engines: {node: '>=14'} + + locate-character@3.0.0: + resolution: {integrity: sha512-SW13ws7BjaeJ6p7Q6CO2nchbYEc3X3J6WrmTTDto7yMPqVSZTUyY5Tjbid+Ab8gLnATtygYtiDIJGQRRn2ZOiA==} + + locate-path@5.0.0: + resolution: {integrity: sha512-t7hw9pI+WvuwNJXwk5zVHpyhIqzg2qTlklJOf0mVxGSbe3Fp2VieZcduNYjaLDoy6p9uGpQEGWG87WpMKlNq8g==} + engines: {node: '>=8'} + + lodash.camelcase@4.3.0: + resolution: {integrity: sha512-TwuEnCnxbc3rAvhf/LbG7tJUDzhqXyFnv3dtzLOPgCG/hODL7WFnsbwktkD7yUV0RrreP/l1PALq/YSg6VvjlA==} + + lodash.castarray@4.4.0: + resolution: {integrity: sha512-aVx8ztPv7/2ULbArGJ2Y42bG1mEQ5mGjpdvrbJcJFU3TbYybe+QlLS4pst9zV52ymy2in1KpFPiZnAOATxD4+Q==} + + lodash.debounce@4.0.8: + resolution: {integrity: sha512-FT1yDzDYEoYWhnSGnpE/4Kj1fLZkDFyqRb7fNt6FdYOSxlUWAtp42Eh6Wb0rGIv/m9Bgo7x4GhQbm5Ys4SG5ow==} + + lodash.isplainobject@4.0.6: + resolution: {integrity: sha512-oSXzaWypCMHkPC3NvBEaPHf0KsA5mvPrOPgQWDsbg8n7orZ290M0BmC/jgRZ4vcJ6DTAhjrsSYgdsW/F+MFOBA==} + + lodash.memoize@4.1.2: + resolution: {integrity: sha512-t7j+NzmgnQzTAYXcsHYLgimltOV1MXHtlOWf6GjL9Kj8GK5FInw5JotxvbOs+IvV1/Dzo04/fCGfLVs7aXb4Ag==} + + lodash.merge@4.6.2: + resolution: {integrity: sha512-0KpjqXRVvrYyCsX1swR/XTK0va6VQkQM6MNo7PqW77ByjAhoARA8EfrP1N4+KlKj8YS0ZUCtRT/YUuhyYDujIQ==} + + lodash.uniq@4.5.0: + resolution: {integrity: sha512-xfBaXQd9ryd9dlSDvnvI0lvxfLJlYAZzXomUYzLKtUeOQvOP5piqAWuGtrhWeqaXK9hhoM/iyJc5AV+XfsX3HQ==} + + lodash@4.17.21: + resolution: {integrity: sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg==} + + longest-streak@3.1.0: + resolution: {integrity: sha512-9Ri+o0JYgehTaVBBDoMqIl8GXtbWg711O3srftcHhZ0dqnETqLaoIK0x17fUw9rFSlK/0NlsKe0Ahhyl5pXE2g==} + + lower-case@2.0.2: + resolution: {integrity: sha512-7fm3l3NAF9WfN6W3JOmf5drwpVqX78JtoGJ3A6W0a6ZnldM41w2fV5D490psKFTpMds8TJse/eHLFFsNHHjHgg==} + + lru-cache@10.4.3: + resolution: {integrity: sha512-JNAzZcXrCt42VGLuYz0zfAzDfAvJWW6AfYlDBQyDV5DClI2m5sAmK+OIO7s59XfsRsWHp02jAJrRadPRGTt6SQ==} + + lru-cache@11.2.1: + resolution: {integrity: sha512-r8LA6i4LP4EeWOhqBaZZjDWwehd1xUJPCJd9Sv300H0ZmcUER4+JPh7bqqZeqs1o5pgtgvXm+d9UGrB5zZGDiQ==} + engines: {node: 20 || >=22} + + lru-cache@5.1.1: + resolution: {integrity: sha512-KpNARQA3Iwv+jTA0utUVVbrh+Jlrr1Fv0e56GGzAFOXN7dk/FviaDW8LHmK52DlcH4WP2n6gI8vN1aesBFgo9w==} + + magic-string@0.25.9: + resolution: {integrity: sha512-RmF0AsMzgt25qzqqLc1+MbHmhdx0ojF2Fvs4XnOqz2ZOBXzzkEwc/dJQZCYHAn7v1jbVOjAZfK8msRn4BxO4VQ==} + + magic-string@0.30.17: + resolution: {integrity: sha512-sNPKHvyjVf7gyjwS4xGTaW/mCnF8wnjtifKBEhxfZ7E/S8tQ0rssrwGNn6q8JH/ohItJfSQp9mBtQYuTlH5QnA==} + + magicast@0.3.5: + resolution: {integrity: sha512-L0WhttDl+2BOsybvEOLK7fW3UA0OQ0IQ2d6Zl2x/a6vVRs3bAY0ECOSHHeL5jD+SbOpOCUEi0y1DgHEn9Qn1AQ==} + + make-dir@3.1.0: + resolution: {integrity: sha512-g3FeP20LNwhALb/6Cz6Dd4F2ngze0jz7tbzrD2wAV+o9FeNHe4rL+yK2md0J/fiSf1sa1ADhXqi5+oVwOM/eGw==} + engines: {node: '>=8'} + + markdown-it@14.1.0: + resolution: {integrity: sha512-a54IwgWPaeBCAAsv13YgmALOF1elABB08FxO9i+r4VFk5Vl4pKokRPeX8u5TCgSsPi6ec1otfLjdOpVcgbpshg==} + hasBin: true + + markdown-table@3.0.4: + resolution: {integrity: sha512-wiYz4+JrLyb/DqW2hkFJxP7Vd7JuTDm77fvbM8VfEQdmSMqcImWeeRbHwZjBjIFki/VaMK2BhFi7oUUZeM5bqw==} + + math-intrinsics@1.1.0: + resolution: {integrity: sha512-/IXtbwEk5HTPyEwyKX6hGkYXxM9nbj64B+ilVJnC/R6B0pH5G4V3b0pVbL7DBj4tkhBAppbQUlf6F6Xl9LHu1g==} + engines: {node: '>= 0.4'} + + maxmin@2.1.0: + resolution: {integrity: sha512-NWlApBjW9az9qRPaeg7CX4sQBWwytqz32bIEo1PW9pRW+kBP9KLRfJO3UC+TV31EcQZEUq7eMzikC7zt3zPJcw==} + engines: {node: '>=0.12'} + + mdast-util-definitions@6.0.0: + resolution: {integrity: sha512-scTllyX6pnYNZH/AIp/0ePz6s4cZtARxImwoPJ7kS42n+MnVsI4XbnG6d4ibehRIldYMWM2LD7ImQblVhUejVQ==} + + mdast-util-directive@3.1.0: + resolution: {integrity: sha512-I3fNFt+DHmpWCYAT7quoM6lHf9wuqtI+oCOfvILnoicNIqjh5E3dEJWiXuYME2gNe8vl1iMQwyUHa7bgFmak6Q==} + + mdast-util-find-and-replace@3.0.2: + resolution: {integrity: sha512-Tmd1Vg/m3Xz43afeNxDIhWRtFZgM2VLyaf4vSTYwudTyeuTneoL3qtWMA5jeLyz/O1vDJmmV4QuScFCA2tBPwg==} + + mdast-util-from-markdown@2.0.2: + resolution: {integrity: sha512-uZhTV/8NBuw0WHkPTrCqDOl0zVe1BIng5ZtHoDk49ME1qqcjYmmLmOf0gELgcRMxN4w2iuIeVso5/6QymSrgmA==} + + mdast-util-gfm-autolink-literal@2.0.1: + resolution: {integrity: sha512-5HVP2MKaP6L+G6YaxPNjuL0BPrq9orG3TsrZ9YXbA3vDw/ACI4MEsnoDpn6ZNm7GnZgtAcONJyPhOP8tNJQavQ==} + + mdast-util-gfm-footnote@2.1.0: + resolution: {integrity: sha512-sqpDWlsHn7Ac9GNZQMeUzPQSMzR6Wv0WKRNvQRg0KqHh02fpTz69Qc1QSseNX29bhz1ROIyNyxExfawVKTm1GQ==} + + mdast-util-gfm-strikethrough@2.0.0: + resolution: {integrity: sha512-mKKb915TF+OC5ptj5bJ7WFRPdYtuHv0yTRxK2tJvi+BDqbkiG7h7u/9SI89nRAYcmap2xHQL9D+QG/6wSrTtXg==} + + mdast-util-gfm-table@2.0.0: + resolution: {integrity: sha512-78UEvebzz/rJIxLvE7ZtDd/vIQ0RHv+3Mh5DR96p7cS7HsBhYIICDBCu8csTNWNO6tBWfqXPWekRuj2FNOGOZg==} + + mdast-util-gfm-task-list-item@2.0.0: + resolution: {integrity: sha512-IrtvNvjxC1o06taBAVJznEnkiHxLFTzgonUdy8hzFVeDun0uTjxxrRGVaNFqkU1wJR3RBPEfsxmU6jDWPofrTQ==} + + mdast-util-gfm@3.1.0: + resolution: {integrity: sha512-0ulfdQOM3ysHhCJ1p06l0b0VKlhU0wuQs3thxZQagjcjPrlFRqY215uZGHHJan9GEAXd9MbfPjFJz+qMkVR6zQ==} + + mdast-util-math@3.0.0: + resolution: {integrity: sha512-Tl9GBNeG/AhJnQM221bJR2HPvLOSnLE/T9cJI9tlc6zwQk2nPk/4f0cHkOdEixQPC/j8UtKDdITswvLAy1OZ1w==} + + mdast-util-phrasing@4.1.0: + resolution: {integrity: sha512-TqICwyvJJpBwvGAMZjj4J2n0X8QWp21b9l0o7eXyVJ25YNWYbJDVIyD1bZXE6WtV6RmKJVYmQAKWa0zWOABz2w==} + + mdast-util-to-hast@13.2.0: + resolution: {integrity: sha512-QGYKEuUsYT9ykKBCMOEDLsU5JRObWQusAolFMeko/tYPufNkRffBAQjIE+99jbA87xv6FgmjLtwjh9wBWajwAA==} + + mdast-util-to-markdown@2.1.2: + resolution: {integrity: sha512-xj68wMTvGXVOKonmog6LwyJKrYXZPvlwabaryTjLh9LuvovB/KAH+kvi8Gjj+7rJjsFi23nkUxRQv1KqSroMqA==} + + mdast-util-to-string@4.0.0: + resolution: {integrity: sha512-0H44vDimn51F0YwvxSJSm0eCDOJTRlmN0R1yBh4HLj9wiV1Dn0QoXGbvFAWj2hSItVTlCmBF1hqKlIyUBVFLPg==} + + mdn-data@2.0.14: + resolution: {integrity: sha512-dn6wd0uw5GsdswPFfsgMp5NSB0/aDe6fK94YJV/AJDYXL6HVLWBsxeq7js7Ad+mU2K9LAlwpk6kN2D5mwCPVow==} + + mdn-data@2.0.28: + resolution: {integrity: sha512-aylIc7Z9y4yzHYAJNuESG3hfhC+0Ibp/MAMiaOZgNv4pmEdFyfZhhhny4MNiAfWdBQ1RQ2mfDWmM1x8SvGyp8g==} + + mdn-data@2.0.30: + resolution: {integrity: sha512-GaqWWShW4kv/G9IEucWScBx9G1/vsFZZJUO+tD26M8J8z3Kw5RDQjaoZe03YAClgeS/SWPOcb4nkFBTEi5DUEA==} + + mdn-data@2.12.2: + resolution: {integrity: sha512-IEn+pegP1aManZuckezWCO+XZQDplx1366JoVhTpMpBB1sPey/SbveZQUosKiKiGYjg1wH4pMlNgXbCiYgihQA==} + + mdurl@2.0.0: + resolution: {integrity: sha512-Lf+9+2r+Tdp5wXDXC4PcIBjTDtq4UKjCPMQhKIuzpJNW0b96kVqSwW0bT7FhRSfmAiFYgP+SCRvdrDozfh0U5w==} + + merge-stream@2.0.0: + resolution: {integrity: sha512-abv/qOcuPfk3URPfDzmZU1LKmuw8kT+0nIHvKrKgFrwifol/doWcdA4ZqsWQ8ENrFKkd67Mfpo/LovbIUsbt3w==} + + merge2@1.4.1: + resolution: {integrity: sha512-8q7VEgMJW4J8tcfVPy8g09NcQwZdbwFEqhe/WZkoIzjn/3TGDwtOCYtXGxA3O8tPzpczCCDgv+P2P5y00ZJOOg==} + engines: {node: '>= 8'} + + microbundle@0.15.1: + resolution: {integrity: sha512-aAF+nwFbkSIJGfrJk+HyzmJOq3KFaimH6OIFBU6J2DPjQeg1jXIYlIyEv81Gyisb9moUkudn+wj7zLNYMOv75Q==} + hasBin: true + + micromark-core-commonmark@2.0.3: + resolution: {integrity: sha512-RDBrHEMSxVFLg6xvnXmb1Ayr2WzLAWjeSATAoxwKYJV94TeNavgoIdA0a9ytzDSVzBy2YKFK+emCPOEibLeCrg==} + + micromark-extension-directive@3.0.2: + resolution: {integrity: sha512-wjcXHgk+PPdmvR58Le9d7zQYWy+vKEU9Se44p2CrCDPiLr2FMyiT4Fyb5UFKFC66wGB3kPlgD7q3TnoqPS7SZA==} + + micromark-extension-gfm-autolink-literal@2.1.0: + resolution: {integrity: sha512-oOg7knzhicgQ3t4QCjCWgTmfNhvQbDDnJeVu9v81r7NltNCVmhPy1fJRX27pISafdjL+SVc4d3l48Gb6pbRypw==} + + micromark-extension-gfm-footnote@2.1.0: + resolution: {integrity: sha512-/yPhxI1ntnDNsiHtzLKYnE3vf9JZ6cAisqVDauhp4CEHxlb4uoOTxOCJ+9s51bIB8U1N1FJ1RXOKTIlD5B/gqw==} + + micromark-extension-gfm-strikethrough@2.1.0: + resolution: {integrity: sha512-ADVjpOOkjz1hhkZLlBiYA9cR2Anf8F4HqZUO6e5eDcPQd0Txw5fxLzzxnEkSkfnD0wziSGiv7sYhk/ktvbf1uw==} + + micromark-extension-gfm-table@2.1.1: + resolution: {integrity: sha512-t2OU/dXXioARrC6yWfJ4hqB7rct14e8f7m0cbI5hUmDyyIlwv5vEtooptH8INkbLzOatzKuVbQmAYcbWoyz6Dg==} + + micromark-extension-gfm-tagfilter@2.0.0: + resolution: {integrity: sha512-xHlTOmuCSotIA8TW1mDIM6X2O1SiX5P9IuDtqGonFhEK0qgRI4yeC6vMxEV2dgyr2TiD+2PQ10o+cOhdVAcwfg==} + + micromark-extension-gfm-task-list-item@2.1.0: + resolution: {integrity: sha512-qIBZhqxqI6fjLDYFTBIa4eivDMnP+OZqsNwmQ3xNLE4Cxwc+zfQEfbs6tzAo2Hjq+bh6q5F+Z8/cksrLFYWQQw==} + + micromark-extension-gfm@3.0.0: + resolution: {integrity: sha512-vsKArQsicm7t0z2GugkCKtZehqUm31oeGBV/KVSorWSy8ZlNAv7ytjFhvaryUiCUJYqs+NoE6AFhpQvBTM6Q4w==} + + micromark-extension-math@3.1.0: + resolution: {integrity: sha512-lvEqd+fHjATVs+2v/8kg9i5Q0AP2k85H0WUOwpIVvUML8BapsMvh1XAogmQjOCsLpoKRCVQqEkQBB3NhVBcsOg==} + + micromark-factory-destination@2.0.1: + resolution: {integrity: sha512-Xe6rDdJlkmbFRExpTOmRj9N3MaWmbAgdpSrBQvCFqhezUn4AHqJHbaEnfbVYYiexVSs//tqOdY/DxhjdCiJnIA==} + + micromark-factory-label@2.0.1: + resolution: {integrity: sha512-VFMekyQExqIW7xIChcXn4ok29YE3rnuyveW3wZQWWqF4Nv9Wk5rgJ99KzPvHjkmPXF93FXIbBp6YdW3t71/7Vg==} + + micromark-factory-space@2.0.1: + resolution: {integrity: sha512-zRkxjtBxxLd2Sc0d+fbnEunsTj46SWXgXciZmHq0kDYGnck/ZSGj9/wULTV95uoeYiK5hRXP2mJ98Uo4cq/LQg==} + + micromark-factory-title@2.0.1: + resolution: {integrity: sha512-5bZ+3CjhAd9eChYTHsjy6TGxpOFSKgKKJPJxr293jTbfry2KDoWkhBb6TcPVB4NmzaPhMs1Frm9AZH7OD4Cjzw==} + + micromark-factory-whitespace@2.0.1: + resolution: {integrity: sha512-Ob0nuZ3PKt/n0hORHyvoD9uZhr+Za8sFoP+OnMcnWK5lngSzALgQYKMr9RJVOWLqQYuyn6ulqGWSXdwf6F80lQ==} + + micromark-util-character@2.1.1: + resolution: {integrity: sha512-wv8tdUTJ3thSFFFJKtpYKOYiGP2+v96Hvk4Tu8KpCAsTMs6yi+nVmGh1syvSCsaxz45J6Jbw+9DD6g97+NV67Q==} + + micromark-util-chunked@2.0.1: + resolution: {integrity: sha512-QUNFEOPELfmvv+4xiNg2sRYeS/P84pTW0TCgP5zc9FpXetHY0ab7SxKyAQCNCc1eK0459uoLI1y5oO5Vc1dbhA==} + + micromark-util-classify-character@2.0.1: + resolution: {integrity: sha512-K0kHzM6afW/MbeWYWLjoHQv1sgg2Q9EccHEDzSkxiP/EaagNzCm7T/WMKZ3rjMbvIpvBiZgwR3dKMygtA4mG1Q==} + + micromark-util-combine-extensions@2.0.1: + resolution: {integrity: sha512-OnAnH8Ujmy59JcyZw8JSbK9cGpdVY44NKgSM7E9Eh7DiLS2E9RNQf0dONaGDzEG9yjEl5hcqeIsj4hfRkLH/Bg==} + + micromark-util-decode-numeric-character-reference@2.0.2: + resolution: {integrity: sha512-ccUbYk6CwVdkmCQMyr64dXz42EfHGkPQlBj5p7YVGzq8I7CtjXZJrubAYezf7Rp+bjPseiROqe7G6foFd+lEuw==} + + micromark-util-decode-string@2.0.1: + resolution: {integrity: sha512-nDV/77Fj6eH1ynwscYTOsbK7rR//Uj0bZXBwJZRfaLEJ1iGBR6kIfNmlNqaqJf649EP0F3NWNdeJi03elllNUQ==} + + micromark-util-encode@2.0.1: + resolution: {integrity: sha512-c3cVx2y4KqUnwopcO9b/SCdo2O67LwJJ/UyqGfbigahfegL9myoEFoDYZgkT7f36T0bLrM9hZTAaAyH+PCAXjw==} + + micromark-util-html-tag-name@2.0.1: + resolution: {integrity: sha512-2cNEiYDhCWKI+Gs9T0Tiysk136SnR13hhO8yW6BGNyhOC4qYFnwF1nKfD3HFAIXA5c45RrIG1ub11GiXeYd1xA==} + + micromark-util-normalize-identifier@2.0.1: + resolution: {integrity: sha512-sxPqmo70LyARJs0w2UclACPUUEqltCkJ6PhKdMIDuJ3gSf/Q+/GIe3WKl0Ijb/GyH9lOpUkRAO2wp0GVkLvS9Q==} + + micromark-util-resolve-all@2.0.1: + resolution: {integrity: sha512-VdQyxFWFT2/FGJgwQnJYbe1jjQoNTS4RjglmSjTUlpUMa95Htx9NHeYW4rGDJzbjvCsl9eLjMQwGeElsqmzcHg==} + + micromark-util-sanitize-uri@2.0.1: + resolution: {integrity: sha512-9N9IomZ/YuGGZZmQec1MbgxtlgougxTodVwDzzEouPKo3qFWvymFHWcnDi2vzV1ff6kas9ucW+o3yzJK9YB1AQ==} + + micromark-util-subtokenize@2.1.0: + resolution: {integrity: sha512-XQLu552iSctvnEcgXw6+Sx75GflAPNED1qx7eBJ+wydBb2KCbRZe+NwvIEEMM83uml1+2WSXpBAcp9IUCgCYWA==} + + micromark-util-symbol@2.0.1: + resolution: {integrity: sha512-vs5t8Apaud9N28kgCrRUdEed4UJ+wWNvicHLPxCa9ENlYuAY31M0ETy5y1vA33YoNPDFTghEbnh6efaE8h4x0Q==} + + micromark-util-types@2.0.2: + resolution: {integrity: sha512-Yw0ECSpJoViF1qTU4DC6NwtC4aWGt1EkzaQB8KPPyCRR8z9TWeV0HbEFGTO+ZY1wB22zmxnJqhPyTpOVCpeHTA==} + + micromark@4.0.2: + resolution: {integrity: sha512-zpe98Q6kvavpCr1NPVSCMebCKfD7CA2NqZ+rykeNhONIJBpc1tFKt9hucLGwha3jNTNI8lHpctWJWoimVF4PfA==} + + micromatch@4.0.8: + resolution: {integrity: sha512-PXwfBhYu0hBCPw8Dn0E+WDYb7af3dSLVWKi3HGv84IdF4TyFoC0ysxFd0Goxw7nSv4T/PzEJQxsYsEiFCKo2BA==} + engines: {node: '>=8.6'} + + mime-db@1.52.0: + resolution: {integrity: sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg==} + engines: {node: '>= 0.6'} + + mime-types@2.1.35: + resolution: {integrity: sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw==} + engines: {node: '>= 0.6'} + + minimatch@10.0.3: + resolution: {integrity: sha512-IPZ167aShDZZUMdRk66cyQAW3qr0WzbHkPdMYa8bzZhlHhO3jALbKdxcaak7W9FfT2rZNpQuUu4Od7ILEpXSaw==} + engines: {node: 20 || >=22} + + minimatch@3.1.2: + resolution: {integrity: sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==} + + minimatch@5.1.6: + resolution: {integrity: sha512-lKwV/1brpG6mBUFHtb7NUmtABCb2WZZmm2wNiOA5hAb8VdCS4B3dtMWyvcoViccwAW/COERjXLt0zP1zXUN26g==} + engines: {node: '>=10'} + + minimatch@9.0.5: + resolution: {integrity: sha512-G6T0ZX48xgozx7587koeX9Ys2NYy6Gmv//P89sEte9V9whIapMNF4idKxnW2QtCcLiTWlb/wfCabAtAFWhhBow==} + engines: {node: '>=16 || 14 >=14.17'} + + minipass@3.3.6: + resolution: {integrity: sha512-DxiNidxSEK+tHG6zOIklvNOwm3hvCrbUrdtzY74U6HKTJxvIDfOUL5W5P2Ghd3DTkhhKPYGqeNUIh5qcM4YBfw==} + engines: {node: '>=8'} + + minipass@4.2.8: + resolution: {integrity: sha512-fNzuVyifolSLFL4NzpF+wEF4qrgqaaKX0haXPQEdQ7NKAN+WecoKMHV09YcuL/DHxrUsYQOK3MiuDf7Ip2OXfQ==} + engines: {node: '>=8'} + + minipass@5.0.0: + resolution: {integrity: sha512-3FnjYuehv9k6ovOEbyOswadCDPX1piCfhV8ncmYtHOjuPwylVWsghTLo7rabjC3Rx5xD4HDx8Wm1xnMF7S5qFQ==} + engines: {node: '>=8'} + + minipass@7.1.2: + resolution: {integrity: sha512-qOOzS1cBTWYF4BH8fVePDBOO9iptMnGUEZwNc/cMWnTV2nVLZ7VoNWEPHkYczZA0pdoA7dl6e7FL659nX9S2aw==} + engines: {node: '>=16 || 14 >=14.17'} + + minizlib@2.1.2: + resolution: {integrity: sha512-bAxsR8BVfj60DWXHE3u30oHzfl4G7khkSuPW+qvpd7jFRHm7dLxOjUk1EHACJ/hxLY8phGJ0YhYHZo7jil7Qdg==} + engines: {node: '>= 8'} + + minizlib@3.0.2: + resolution: {integrity: sha512-oG62iEk+CYt5Xj2YqI5Xi9xWUeZhDI8jjQmC5oThVH5JGCTgIjr7ciJDzC7MBzYd//WvR1OTmP5Q38Q8ShQtVA==} + engines: {node: '>= 18'} + + mkdirp@1.0.4: + resolution: {integrity: sha512-vVqVZQyf3WLx2Shd0qJ9xuvqgAyKPLAiqITEtqW0oIUjzo3PePDd6fW9iFz30ef7Ysp/oiWqbhszeGWW2T6Gzw==} + engines: {node: '>=10'} + hasBin: true + + mkdirp@3.0.1: + resolution: {integrity: sha512-+NsyUUAZDmo6YVHzL/stxSu3t9YS1iljliy3BSDrXJ/dkn1KYdmtZODGGjLcc9XLgVVpH4KshHB8XmZgMhaBXg==} + engines: {node: '>=10'} + hasBin: true + + mlly@1.7.4: + resolution: {integrity: sha512-qmdSIPC4bDJXgZTCR7XosJiNKySV7O215tsPtDN9iEO/7q/76b/ijtgRu/+epFXSJhijtTCCGp3DWS549P3xKw==} + + morphdom@2.7.4: + resolution: {integrity: sha512-ATTbWMgGa+FaMU3FhnFYB6WgulCqwf6opOll4CBzmVDTLvPMmUPrEv8CudmLPK0MESa64+6B89fWOxP3+YIlxQ==} + + mri@1.2.0: + resolution: {integrity: sha512-tzzskb3bG8LvYGFF/mDTpq3jpI6Q9wc3LEmBaghu+DdCssd1FakN7Bc0hVNmEyGq1bq3RgfkCb3cmQLpNPOroA==} + engines: {node: '>=4'} + + mrmime@2.0.1: + resolution: {integrity: sha512-Y3wQdFg2Va6etvQ5I82yUhGdsKrcYox6p7FfL1LbK2J4V01F9TGlepTIhnK24t7koZibmg82KGglhA1XK5IsLQ==} + engines: {node: '>=10'} + + ms@2.1.3: + resolution: {integrity: sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==} + + muggle-string@0.4.1: + resolution: {integrity: sha512-VNTrAak/KhO2i8dqqnqnAHOa3cYBwXEZe9h+D5h/1ZqFSTEFHdM65lR7RoIqq3tBBYavsOXV84NoHXZ0AkPyqQ==} + + mz@2.7.0: + resolution: {integrity: sha512-z81GNO7nnYMEhrGh9LeymoE4+Yr0Wn5McHIZMK5cfQCl+NDX08sCZgUc9/6MHni9IWuFLm1Z3HTCXu2z9fN62Q==} + + nanoid@3.3.11: + resolution: {integrity: sha512-N8SpfPUnUp1bK+PMYW8qSWdl9U+wwNWI4QKxOYDy9JAro3WMX7p2OeVRF9v+347pnakNevPmiHhNmZ2HbFA76w==} + engines: {node: ^10 || ^12 || ^13.7 || ^14 || >=15.0.1} + hasBin: true + + neotraverse@0.6.18: + resolution: {integrity: sha512-Z4SmBUweYa09+o6pG+eASabEpP6QkQ70yHj351pQoEXIs8uHbaU2DWVmzBANKgflPa47A50PtB2+NgRpQvr7vA==} + engines: {node: '>= 10'} + + nlcst-to-string@4.0.0: + resolution: {integrity: sha512-YKLBCcUYKAg0FNlOBT6aI91qFmSiFKiluk655WzPF+DDMA02qIyy8uiRqI8QXtcFpEvll12LpL5MXqEmAZ+dcA==} + + no-case@3.0.4: + resolution: {integrity: sha512-fgAN3jGAh+RoxUGZHTSOLJIqUc2wmoBwGR4tbpNAKmmovFoWq0OdRkb0VkldReO2a2iBT/OEulG9XSUc10r3zg==} + + node-addon-api@7.1.1: + resolution: {integrity: sha512-5m3bsyrjFWE1xf7nz7YXdN4udnVtXK6/Yfgn5qnahL6bCkf2yKt4k3nuTKAtT4r3IG8JNR2ncsIMdZuAzJjHQQ==} + + node-fetch-native@1.6.6: + resolution: {integrity: sha512-8Mc2HhqPdlIfedsuZoc3yioPuzp6b+L5jRCRY1QzuWZh2EGJVQrGppC6V6cF0bLdbW0+O2YpqCA25aF/1lvipQ==} + + node-fetch@2.7.0: + resolution: {integrity: sha512-c4FRfUm/dbcWZ7U+1Wq0AwCyFL+3nt2bEw05wfxSz+DWpWsitgmSgYmy2dQdWyKC1694ELPqMs/YzUSNozLt8A==} + engines: {node: 4.x || >=6.0.0} + peerDependencies: + encoding: ^0.1.0 + peerDependenciesMeta: + encoding: + optional: true + + node-gyp-build@4.8.4: + resolution: {integrity: sha512-LA4ZjwlnUblHVgq0oBF3Jl/6h/Nvs5fzBLwdEF4nuxnFdsfajde4WfxtJr3CaiH+F6ewcIB/q4jQ4UzPyid+CQ==} + hasBin: true + + node-html-parser@7.0.1: + resolution: {integrity: sha512-KGtmPY2kS0thCWGK0VuPyOS+pBKhhe8gXztzA2ilAOhbUbxa9homF1bOyKvhGzMLXUoRds9IOmr/v5lr/lqNmA==} + + node-mock-http@1.0.0: + resolution: {integrity: sha512-0uGYQ1WQL1M5kKvGRXWQ3uZCHtLTO8hln3oBjIusM75WoesZ909uQJs/Hb946i2SS+Gsrhkaa6iAO17jRIv6DQ==} + + node-releases@2.0.19: + resolution: {integrity: sha512-xxOWJsBKtzAq7DY0J+DTzuz58K8e7sJbdgwkbMWQe8UYB6ekmsQ45q0M/tJDsGaZmbC+l7n57UV8Hl5tHxO9uw==} + + nopt@8.1.0: + resolution: {integrity: sha512-ieGu42u/Qsa4TFktmaKEwM6MQH0pOWnaB3htzh0JRtx84+Mebc0cbZYN5bC+6WTZ4+77xrL9Pn5m7CV6VIkV7A==} + engines: {node: ^18.17.0 || >=20.5.0} + hasBin: true + + normalize-path@3.0.0: + resolution: {integrity: sha512-6eZs5Ls3WtCisHWp9S2GUy8dqkpGi4BVSz3GaqiE6ezub0512ESztXUwUB6C6IKbQkY2Pnb/mD4WYojCRwcwLA==} + engines: {node: '>=0.10.0'} + + normalize-range@0.1.2: + resolution: {integrity: sha512-bdok/XvKII3nUpklnV6P2hxtMNrCboOjAcyBuQnWEhO665FwrSNRxU+AqpsyvO6LgGYPspN+lu5CLtw4jPRKNA==} + engines: {node: '>=0.10.0'} + + normalize-url@6.1.0: + resolution: {integrity: sha512-DlL+XwOy3NxAQ8xuC0okPgK46iuVNAK01YN7RueYBqqFeGsBjV9XmCAzAdgt+667bCl5kPh9EqKKDwnaPG1I7A==} + engines: {node: '>=10'} + + nth-check@2.1.1: + resolution: {integrity: sha512-lqjrjmaOoAnWfMmBPL+XNnynZh2+swxiX3WUE0s4yEHI6m+AwrK2UZOimIRl3X/4QctVqS8AiZjFqyOGrMXb/w==} + + number-is-nan@1.0.1: + resolution: {integrity: sha512-4jbtZXNAsfZbAHiiqjLPBiCl16dES1zI4Hpzzxw61Tk+loF+sBDBKx1ICKKKwIqQ7M0mFn1TmkN7euSncWgHiQ==} + engines: {node: '>=0.10.0'} + + object-assign@4.1.1: + resolution: {integrity: sha512-rJgTQnkUnH1sFw8yT6VSU3zD3sWmu6sZhIseY8VX+GRu3P6F7Fu+JNDoXfklElbLJSnc3FUQHVe4cU5hj+BcUg==} + engines: {node: '>=0.10.0'} + + object-hash@3.0.0: + resolution: {integrity: sha512-RSn9F68PjH9HqtltsSnqYC1XXoWe9Bju5+213R98cNGttag9q9yAOTzdbsqvIa7aNm5WffBZFpWYr2aWrklWAw==} + engines: {node: '>= 6'} + + object-inspect@1.13.4: + resolution: {integrity: sha512-W67iLl4J2EXEGTbfeHCffrjDfitvLANg0UlX3wFUUSTx92KXRFegMHUVgSqE+wvhAbi4WqjGg9czysTV2Epbew==} + engines: {node: '>= 0.4'} + + object-keys@1.1.1: + resolution: {integrity: sha512-NuAESUOUMrlIXOfHKzD6bpPu3tYt3xvjNdRIQ+FeT0lNb4K8WR70CaDxhuNguS2XG+GjkyMwOzsN5ZktImfhLA==} + engines: {node: '>= 0.4'} + + object.assign@4.1.7: + resolution: {integrity: sha512-nK28WOo+QIjBkDduTINE4JkF/UJJKyf2EJxvJKfblDpyg0Q+pkOHNTL0Qwy6NP6FhE/EnzV73BxxqcJaXY9anw==} + engines: {node: '>= 0.4'} + + ofetch@1.4.1: + resolution: {integrity: sha512-QZj2DfGplQAr2oj9KzceK9Hwz6Whxazmn85yYeVuS3u9XTMOGMRx0kO95MQ+vLsj/S/NwBDMMLU5hpxvI6Tklw==} + + ohash@2.0.11: + resolution: {integrity: sha512-RdR9FQrFwNBNXAr4GixM8YaRZRJ5PUWbKYbE5eOsrwAjJW0q2REGcf79oYPsLyskQCZG1PLN+S/K1V00joZAoQ==} + + once@1.4.0: + resolution: {integrity: sha512-lNaJgI+2Q5URQBkccEKHTQOPaXdUxnZZElQTZY0MFUAuaEqe1E+Nyvgdz/aIyNi6Z9MzO5dv1H8n58/GELp3+w==} + + oniguruma-parser@0.12.0: + resolution: {integrity: sha512-fD9o5ebCmEAA9dLysajdQvuKzLL7cj+w7DQjuO3Cb6IwafENfx6iL+RGkmyW82pVRsvgzixsWinHvgxTMJvdIA==} + + oniguruma-to-es@4.3.1: + resolution: {integrity: sha512-VtX1kepWO+7HG7IWV5v72JhiqofK7XsiHmtgnvurnNOTdIvE5mrdWYtsOrQyrXCv1L2Ckm08hywp+MFO7rC4Ug==} + + open@8.4.2: + resolution: {integrity: sha512-7x81NCL719oNbsq/3mh+hVrAWmFuEYUqrq/Iw3kUzH8ReypT9QQ0BLoJS7/G9k6N81XjW4qHWtjWwe/9eLy1EQ==} + engines: {node: '>=12'} + + opencollective-postinstall@2.0.3: + resolution: {integrity: sha512-8AV/sCtuzUeTo8gQK5qDZzARrulB3egtLzFgteqB2tcT4Mw7B8Kt7JcDHmltjz6FOAHsvTevk70gZEbhM4ZS9Q==} + hasBin: true + + overlayscrollbars@2.11.1: + resolution: {integrity: sha512-kogaNaBTIizRenQ2GTzt2cpkEH9B0nUBXseRxqQblH/YicJ3TaWuvn8E5TXPPfJCVoHYSgBYZzzva40kCERKHg==} + + own-keys@1.0.1: + resolution: {integrity: sha512-qFOyK5PjiWZd+QQIh+1jhdb9LpxTF0qs7Pm8o5QHYZ0M3vKqSqzsZaEB6oWlxZ+q2sJBMI/Ktgd2N5ZwQoRHfg==} + engines: {node: '>= 0.4'} + + p-finally@1.0.0: + resolution: {integrity: sha512-LICb2p9CB7FS+0eR1oqWnHhp0FljGLZCWBE9aix0Uye9W8LTQPwMTYVGWQWIw9RdQiDg4+epXQODwIYJtSJaow==} + engines: {node: '>=4'} + + p-limit@2.3.0: + resolution: {integrity: sha512-//88mFWSJx8lxCzwdAABTJL2MyWB12+eIY7MDL2SqLmAkeKU9qxRvWuSyTjm3FUmpBEMuFfckAIqEaVGUDxb6w==} + engines: {node: '>=6'} + + p-limit@6.2.0: + resolution: {integrity: sha512-kuUqqHNUqoIWp/c467RI4X6mmyuojY5jGutNU0wVTmEOOfcuwLqyMVoAi9MKi2Ak+5i9+nhmrK4ufZE8069kHA==} + engines: {node: '>=18'} + + p-locate@4.1.0: + resolution: {integrity: sha512-R79ZZ/0wAxKGu3oYMlz8jy/kbhsNrS7SKZ7PxEHBgJ5+F2mtFW2fK2cOtBh1cHYkQsbzFV7I+EoRKe6Yt0oK7A==} + engines: {node: '>=8'} + + p-queue@6.6.2: + resolution: {integrity: sha512-RwFpb72c/BhQLEXIZ5K2e+AhgNVmIejGlTgiB9MzZ0e93GRvqZ7uSi0dvRF7/XIXDeNkra2fNHBxTyPDGySpjQ==} + engines: {node: '>=8'} + + p-queue@8.1.0: + resolution: {integrity: sha512-mxLDbbGIBEXTJL0zEx8JIylaj3xQ7Z/7eEVjcF9fJX4DBiH9oqe+oahYnlKKxm0Ci9TlWTyhSHgygxMxjIB2jw==} + engines: {node: '>=18'} + + p-timeout@3.2.0: + resolution: {integrity: sha512-rhIwUycgwwKcP9yTOOFK/AKsAopjjCakVqLHePO3CC6Mir1Z99xT+R63jZxAT5lFZLa2inS5h+ZS2GvR99/FBg==} + engines: {node: '>=8'} + + p-timeout@6.1.4: + resolution: {integrity: sha512-MyIV3ZA/PmyBN/ud8vV9XzwTrNtR4jFrObymZYnZqMmW0zA8Z17vnT0rBgFE/TlohB+YCHqXMgZzb3Csp49vqg==} + engines: {node: '>=14.16'} + + p-try@2.2.0: + resolution: {integrity: sha512-R4nPAVTAU0B9D35/Gk3uJf/7XYbQcyohSKdvAxIRSNghFl4e71hVoGnBNQz9cWaXxO2I10KTC+3jMdvvoKw6dQ==} + engines: {node: '>=6'} + + package-json-from-dist@1.0.1: + resolution: {integrity: sha512-UEZIS3/by4OC8vL3P2dTXRETpebLI2NiI5vIrjaD/5UtrkFX/tNbwjTSRAGC/+7CAo2pIcBaRgWmcBBHcsaCIw==} + + package-manager-detector@0.2.11: + resolution: {integrity: sha512-BEnLolu+yuz22S56CU1SUKq3XC3PkwD5wv4ikR4MfGvnRVcmzXR9DwSlW2fEamyTPyXHomBJRzgapeuBvRNzJQ==} + + package-manager-detector@1.2.0: + resolution: {integrity: sha512-PutJepsOtsqVfUsxCzgTTpyXmiAgvKptIgY4th5eq5UXXFhj5PxfQ9hnGkypMeovpAvVshFRItoFHYO18TCOqA==} + + pako@0.2.9: + resolution: {integrity: sha512-NUcwaKxUxWrZLpDG+z/xZaCgQITkA/Dv4V/T6bw7VON6l1Xz/VnrBqrYjZQ12TamKHzITTfOEIYUj48y2KXImA==} + + parent-module@1.0.1: + resolution: {integrity: sha512-GQ2EWRpQV8/o+Aw8YqtfZZPfNRWZYkbidE9k5rpl/hC3vtHHBfGm2Ifi6qWV+coDGkrUKZAxE3Lot5kcsRlh+g==} + engines: {node: '>=6'} + + parse-entities@4.0.2: + resolution: {integrity: sha512-GG2AQYWoLgL877gQIKeRPGO1xF9+eG1ujIb5soS5gPvLQ1y2o8FL90w2QWNdf9I361Mpp7726c+lj3U0qK1uGw==} + + parse-json@5.2.0: + resolution: {integrity: sha512-ayCKvm/phCGxOkYRSCM82iDwct8/EonSEgCSxWxD7ve6jHggsFl4fZVQBPRNgQoKiuV/odhFrGzQXZwbifC8Rg==} + engines: {node: '>=8'} + + parse-latin@7.0.0: + resolution: {integrity: sha512-mhHgobPPua5kZ98EF4HWiH167JWBfl4pvAIXXdbaVohtK7a6YBOy56kvhCqduqyo/f3yrHFWmqmiMg/BkBkYYQ==} + + parse-srcset@1.0.2: + resolution: {integrity: sha512-/2qh0lav6CmI15FzA3i/2Bzk2zCgQhGMkvhOhKNcBVQ1ldgpbfiNTVslmooUmWJcADi1f1kIeynbDRVzNlfR6Q==} + + parse5-htmlparser2-tree-adapter@7.1.0: + resolution: {integrity: sha512-ruw5xyKs6lrpo9x9rCZqZZnIUntICjQAd0Wsmp396Ul9lN/h+ifgVV1x1gZHi8euej6wTfpqX8j+BFQxF0NS/g==} + + parse5-parser-stream@7.1.2: + resolution: {integrity: sha512-JyeQc9iwFLn5TbvvqACIF/VXG6abODeB3Fwmv/TGdLk2LfbWkaySGY72at4+Ty7EkPZj854u4CrICqNk2qIbow==} + + parse5@7.2.1: + resolution: {integrity: sha512-BuBYQYlv1ckiPdQi/ohiivi9Sagc9JG+Ozs0r7b/0iK3sKmrb0b9FdWdBbOdx6hBCM/F9Ir82ofnBhtZOjCRPQ==} + + parse5@7.3.0: + resolution: {integrity: sha512-IInvU7fabl34qmi9gY8XOVxhYyMyuH2xUNpb2q8/Y+7552KlejkRvqvD19nMoUW/uQGGbqNpA6Tufu5FL5BZgw==} + + pascal-case@3.1.2: + resolution: {integrity: sha512-uWlGT3YSnK9x3BQJaOdcZwrnV6hPpd8jFH1/ucpiLRPh/2zCVJKS19E4GvYHvaCcACn3foXZ0cLB9Wrx1KGe5g==} + + path-browserify@1.0.1: + resolution: {integrity: sha512-b7uo2UCUOYZcnF/3ID0lulOJi/bafxa1xPe7ZPsammBSpjSWQkjNxlt635YGS2MiR9GjvuXCtz2emr3jbsz98g==} + + path-exists@4.0.0: + resolution: {integrity: sha512-ak9Qy5Q7jYb2Wwcey5Fpvg2KoAc/ZIhLSLOSBmRmygPsGwkVVt0fZa0qrtMz+m6tJTAHfZQ8FnmB4MG4LWy7/w==} + engines: {node: '>=8'} + + path-is-absolute@1.0.1: + resolution: {integrity: sha512-AVbw3UJ2e9bq64vSaS9Am0fje1Pa8pbGqTTsmXfaIiMpnr5DlDhfJOuLj9Sf95ZPVDAUerDfEk88MPmPe7UCQg==} + engines: {node: '>=0.10.0'} + + path-key@3.1.1: + resolution: {integrity: sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q==} + engines: {node: '>=8'} + + path-parse@1.0.7: + resolution: {integrity: sha512-LDJzPVEEEPR+y48z93A0Ed0yXb8pAByGWo/k5YYdYgpY2/2EsOsksJrq7lOHxryrVOn1ejG6oAp8ahvOIQD8sw==} + + path-scurry@1.11.1: + resolution: {integrity: sha512-Xa4Nw17FS9ApQFJ9umLiJS4orGjm7ZzwUrwamcGQuHSzDyth9boKDaycYdDcZDuqYATXw4HFXgaqWTctW/v1HA==} + engines: {node: '>=16 || 14 >=14.18'} + + path-scurry@2.0.0: + resolution: {integrity: sha512-ypGJsmGtdXUOeM5u93TyeIEfEhM6s+ljAhrk5vAvSx8uyY/02OvrZnA0YNGUrPXfpJMgI1ODd3nwz8Npx4O4cg==} + engines: {node: 20 || >=22} + + path-to-regexp@6.1.0: + resolution: {integrity: sha512-h9DqehX3zZZDCEm+xbfU0ZmwCGFCAAraPJWMXJ4+v32NjZJilVg3k1TcKsRgIb8IQ/izZSaydDc1OhJCZvs2Dw==} + + path-to-regexp@6.3.0: + resolution: {integrity: sha512-Yhpw4T9C6hPpgPeA28us07OJeqZ5EzQTkbfwuhsUg0c237RomFoETJgmp2sa3F/41gfLE6G5cqcYwznmeEeOlQ==} + + path-type@4.0.0: + resolution: {integrity: sha512-gDKb8aZMDeD/tZWs9P6+q0J9Mwkdl6xMV8TjnGP3qJVJ06bdMgkbBlLU8IdfOsIsFz2BW1rNVT3XuNEl8zPAvw==} + engines: {node: '>=8'} + + pathe@1.1.2: + resolution: {integrity: sha512-whLdWMYL2TwI08hn8/ZqAbrVemu0LNaNNJZX73O6qaIdCTfXutsLhMkjdENX0qhsQ9uIimo4/aQOmXkoon2nDQ==} + + pathe@2.0.3: + resolution: {integrity: sha512-WUjGcAqP1gQacoQe+OBJsFA7Ld4DyXuUIjZ5cc75cLHvJ7dtNsTugphxIADwspS+AraAUePCKrSVtPLFj/F88w==} + + pend@1.2.0: + resolution: {integrity: sha512-F3asv42UuXchdzt+xXqfW1OGlVBe+mxa2mqI0pg5yAHZPvFmY3Y6drSf/GQ1A86WgWEN9Kzh/WrgKa6iGcHXLg==} + + photoswipe@5.4.4: + resolution: {integrity: sha512-WNFHoKrkZNnvFFhbHL93WDkW3ifwVOXSW3w1UuZZelSmgXpIGiZSNlZJq37rR8YejqME2rHs9EhH9ZvlvFH2NA==} + engines: {node: '>= 0.12.0'} + + picocolors@1.1.1: + resolution: {integrity: sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==} + + picomatch@2.3.1: + resolution: {integrity: sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA==} + engines: {node: '>=8.6'} + + picomatch@4.0.2: + resolution: {integrity: sha512-M7BAV6Rlcy5u+m6oPhAPFgJTzAioX/6B0DxyvDlo9l8+T3nLKbrczg2WLUyzd45L8RqfUMyGPzekbMvX2Ldkwg==} + engines: {node: '>=12'} + + pify@2.3.0: + resolution: {integrity: sha512-udgsAY+fTnvv7kI7aaxbqwWNb0AHiB0qBO89PZKPkoTmGOgdbrHDKD+0B2X4uTfJ/FT1R09r9gTsjUjNJotuog==} + engines: {node: '>=0.10.0'} + + pify@5.0.0: + resolution: {integrity: sha512-eW/gHNMlxdSP6dmG6uJip6FXN0EQBwm2clYYd8Wul42Cwu/DK8HEftzsapcNdYe2MfLiIwZqsDk2RDEsTE79hA==} + engines: {node: '>=10'} + + pirates@4.0.7: + resolution: {integrity: sha512-TfySrs/5nm8fQJDcBDuUng3VOUKsd7S+zqvbOTiGXHfxX4wK31ard+hoNuvkicM/2YFzlpDgABOevKSsB4G/FA==} + engines: {node: '>= 6'} + + pkg-dir@4.2.0: + resolution: {integrity: sha512-HRDzbaKjC+AOWVXxAU/x54COGeIv9eb+6CkDSQoNTt4XyWoIJvuPsXizxu/Fr23EiekbtZwmh1IcIG/l/a10GQ==} + engines: {node: '>=8'} + + pkg-types@1.3.1: + resolution: {integrity: sha512-/Jm5M4RvtBFVkKWRu2BLUTNP8/M2a+UwuAX+ae4770q1qVGtfjG+WTCupoZixokjmHiry8uI+dlY8KXYV5HVVQ==} + + pkg-types@2.1.0: + resolution: {integrity: sha512-wmJwA+8ihJixSoHKxZJRBQG1oY8Yr9pGLzRmSsNms0iNWyHHAlZCa7mmKiFR10YPZuz/2k169JiS/inOjBCZ2A==} + + possible-typed-array-names@1.1.0: + resolution: {integrity: sha512-/+5VFTchJDoVj3bhoqi6UeymcD00DAwb1nJwamzPvHEszJ4FpF6SNNbUbOS8yI56qHzdV8eK0qEfOSiodkTdxg==} + engines: {node: '>= 0.4'} + + postcss-calc@8.2.4: + resolution: {integrity: sha512-SmWMSJmB8MRnnULldx0lQIyhSNvuDl9HfrZkaqqE/WHAhToYsAvDq+yAsA/kIyINDszOp3Rh0GFoNuH5Ypsm3Q==} + peerDependencies: + postcss: ^8.2.2 + + postcss-colormin@5.3.1: + resolution: {integrity: sha512-UsWQG0AqTFQmpBegeLLc1+c3jIqBNB0zlDGRWR+dQ3pRKJL1oeMzyqmH3o2PIfn9MBdNrVPWhDbT769LxCTLJQ==} + engines: {node: ^10 || ^12 || >=14.0} + peerDependencies: + postcss: ^8.2.15 + + postcss-convert-values@5.1.3: + resolution: {integrity: sha512-82pC1xkJZtcJEfiLw6UXnXVXScgtBrjlO5CBmuDQc+dlb88ZYheFsjTn40+zBVi3DkfF7iezO0nJUPLcJK3pvA==} + engines: {node: ^10 || ^12 || >=14.0} + peerDependencies: + postcss: ^8.2.15 + + postcss-discard-comments@5.1.2: + resolution: {integrity: sha512-+L8208OVbHVF2UQf1iDmRcbdjJkuBF6IS29yBDSiWUIzpYaAhtNl6JYnYm12FnkeCwQqF5LeklOu6rAqgfBZqQ==} + engines: {node: ^10 || ^12 || >=14.0} + peerDependencies: + postcss: ^8.2.15 + + postcss-discard-duplicates@5.1.0: + resolution: {integrity: sha512-zmX3IoSI2aoenxHV6C7plngHWWhUOV3sP1T8y2ifzxzbtnuhk1EdPwm0S1bIUNaJ2eNbWeGLEwzw8huPD67aQw==} + engines: {node: ^10 || ^12 || >=14.0} + peerDependencies: + postcss: ^8.2.15 + + postcss-discard-empty@5.1.1: + resolution: {integrity: sha512-zPz4WljiSuLWsI0ir4Mcnr4qQQ5e1Ukc3i7UfE2XcrwKK2LIPIqE5jxMRxO6GbI3cv//ztXDsXwEWT3BHOGh3A==} + engines: {node: ^10 || ^12 || >=14.0} + peerDependencies: + postcss: ^8.2.15 + + postcss-discard-overridden@5.1.0: + resolution: {integrity: sha512-21nOL7RqWR1kasIVdKs8HNqQJhFxLsyRfAnUDm4Fe4t4mCWL9OJiHvlHPjcd8zc5Myu89b/7wZDnOSjFgeWRtw==} + engines: {node: ^10 || ^12 || >=14.0} + peerDependencies: + postcss: ^8.2.15 + + postcss-import@15.1.0: + resolution: {integrity: sha512-hpr+J05B2FVYUAXHeK1YyI267J/dDDhMU6B6civm8hSY1jYJnBXxzKDKDswzJmtLHryrjhnDjqqp/49t8FALew==} + engines: {node: '>=14.0.0'} + peerDependencies: + postcss: ^8.0.0 + + postcss-import@16.1.0: + resolution: {integrity: sha512-7hsAZ4xGXl4MW+OKEWCnF6T5jqBw80/EE9aXg1r2yyn1RsVEU8EtKXbijEODa+rg7iih4bKf7vlvTGYR4CnPNg==} + engines: {node: '>=18.0.0'} + peerDependencies: + postcss: ^8.0.0 + + postcss-js@4.0.1: + resolution: {integrity: sha512-dDLF8pEO191hJMtlHFPRa8xsizHaM82MLfNkUHdUtVEV3tgTp5oj+8qbEqYM57SLfc74KSbw//4SeJma2LRVIw==} + engines: {node: ^12 || ^14 || >= 16} + peerDependencies: + postcss: ^8.4.21 + + postcss-load-config@3.1.4: + resolution: {integrity: sha512-6DiM4E7v4coTE4uzA8U//WhtPwyhiim3eyjEMFCnUpzbrkK9wJHgKDT2mR+HbtSrd/NubVaYTOpSpjUl8NQeRg==} + engines: {node: '>= 10'} + peerDependencies: + postcss: '>=8.0.9' + ts-node: '>=9.0.0' + peerDependenciesMeta: + postcss: + optional: true + ts-node: + optional: true + + postcss-load-config@4.0.2: + resolution: {integrity: sha512-bSVhyJGL00wMVoPUzAVAnbEoWyqRxkjv64tUl427SKnPrENtq6hJwUojroMz2VB+Q1edmi4IfrAPpami5VVgMQ==} + engines: {node: '>= 14'} + peerDependencies: + postcss: '>=8.0.9' + ts-node: '>=9.0.0' + peerDependenciesMeta: + postcss: + optional: true + ts-node: + optional: true + + postcss-merge-longhand@5.1.7: + resolution: {integrity: sha512-YCI9gZB+PLNskrK0BB3/2OzPnGhPkBEwmwhfYk1ilBHYVAZB7/tkTHFBAnCrvBBOmeYyMYw3DMjT55SyxMBzjQ==} + engines: {node: ^10 || ^12 || >=14.0} + peerDependencies: + postcss: ^8.2.15 + + postcss-merge-rules@5.1.4: + resolution: {integrity: sha512-0R2IuYpgU93y9lhVbO/OylTtKMVcHb67zjWIfCiKR9rWL3GUk1677LAqD/BcHizukdZEjT8Ru3oHRoAYoJy44g==} + engines: {node: ^10 || ^12 || >=14.0} + peerDependencies: + postcss: ^8.2.15 + + postcss-minify-font-values@5.1.0: + resolution: {integrity: sha512-el3mYTgx13ZAPPirSVsHqFzl+BBBDrXvbySvPGFnQcTI4iNslrPaFq4muTkLZmKlGk4gyFAYUBMH30+HurREyA==} + engines: {node: ^10 || ^12 || >=14.0} + peerDependencies: + postcss: ^8.2.15 + + postcss-minify-gradients@5.1.1: + resolution: {integrity: sha512-VGvXMTpCEo4qHTNSa9A0a3D+dxGFZCYwR6Jokk+/3oB6flu2/PnPXAh2x7x52EkY5xlIHLm+Le8tJxe/7TNhzw==} + engines: {node: ^10 || ^12 || >=14.0} + peerDependencies: + postcss: ^8.2.15 + + postcss-minify-params@5.1.4: + resolution: {integrity: sha512-+mePA3MgdmVmv6g+30rn57USjOGSAyuxUmkfiWpzalZ8aiBkdPYjXWtHuwJGm1v5Ojy0Z0LaSYhHaLJQB0P8Jw==} + engines: {node: ^10 || ^12 || >=14.0} + peerDependencies: + postcss: ^8.2.15 + + postcss-minify-selectors@5.2.1: + resolution: {integrity: sha512-nPJu7OjZJTsVUmPdm2TcaiohIwxP+v8ha9NehQ2ye9szv4orirRU3SDdtUmKH+10nzn0bAyOXZ0UEr7OpvLehg==} + engines: {node: ^10 || ^12 || >=14.0} + peerDependencies: + postcss: ^8.2.15 + + postcss-modules-extract-imports@3.1.0: + resolution: {integrity: sha512-k3kNe0aNFQDAZGbin48pL2VNidTF0w4/eASDsxlyspobzU3wZQLOGj7L9gfRe0Jo9/4uud09DsjFNH7winGv8Q==} + engines: {node: ^10 || ^12 || >= 14} + peerDependencies: + postcss: ^8.1.0 + + postcss-modules-local-by-default@4.2.0: + resolution: {integrity: sha512-5kcJm/zk+GJDSfw+V/42fJ5fhjL5YbFDl8nVdXkJPLLW+Vf9mTD5Xe0wqIaDnLuL2U6cDNpTr+UQ+v2HWIBhzw==} + engines: {node: ^10 || ^12 || >= 14} + peerDependencies: + postcss: ^8.1.0 + + postcss-modules-scope@3.2.1: + resolution: {integrity: sha512-m9jZstCVaqGjTAuny8MdgE88scJnCiQSlSrOWcTQgM2t32UBe+MUmFSO5t7VMSfAf/FJKImAxBav8ooCHJXCJA==} + engines: {node: ^10 || ^12 || >= 14} + peerDependencies: + postcss: ^8.1.0 + + postcss-modules-values@4.0.0: + resolution: {integrity: sha512-RDxHkAiEGI78gS2ofyvCsu7iycRv7oqw5xMWn9iMoR0N/7mf9D50ecQqUo5BZ9Zh2vH4bCUR/ktCqbB9m8vJjQ==} + engines: {node: ^10 || ^12 || >= 14} + peerDependencies: + postcss: ^8.1.0 + + postcss-modules@4.3.1: + resolution: {integrity: sha512-ItUhSUxBBdNamkT3KzIZwYNNRFKmkJrofvC2nWab3CPKhYBQ1f27XXh1PAPE27Psx58jeelPsxWB/+og+KEH0Q==} + peerDependencies: + postcss: ^8.0.0 + + postcss-nested@6.2.0: + resolution: {integrity: sha512-HQbt28KulC5AJzG+cZtj9kvKB93CFCdLvog1WFLf1D+xmMvPGlBstkpTEZfK5+AN9hfJocyBFCNiqyS48bpgzQ==} + engines: {node: '>=12.0'} + peerDependencies: + postcss: ^8.2.14 + + postcss-nesting@13.0.1: + resolution: {integrity: sha512-VbqqHkOBOt4Uu3G8Dm8n6lU5+9cJFxiuty9+4rcoyRPO9zZS1JIs6td49VIoix3qYqELHlJIn46Oih9SAKo+yQ==} + engines: {node: '>=18'} + peerDependencies: + postcss: ^8.4 + + postcss-normalize-charset@5.1.0: + resolution: {integrity: sha512-mSgUJ+pd/ldRGVx26p2wz9dNZ7ji6Pn8VWBajMXFf8jk7vUoSrZ2lt/wZR7DtlZYKesmZI680qjr2CeFF2fbUg==} + engines: {node: ^10 || ^12 || >=14.0} + peerDependencies: + postcss: ^8.2.15 + + postcss-normalize-display-values@5.1.0: + resolution: {integrity: sha512-WP4KIM4o2dazQXWmFaqMmcvsKmhdINFblgSeRgn8BJ6vxaMyaJkwAzpPpuvSIoG/rmX3M+IrRZEz2H0glrQNEA==} + engines: {node: ^10 || ^12 || >=14.0} + peerDependencies: + postcss: ^8.2.15 + + postcss-normalize-positions@5.1.1: + resolution: {integrity: sha512-6UpCb0G4eofTCQLFVuI3EVNZzBNPiIKcA1AKVka+31fTVySphr3VUgAIULBhxZkKgwLImhzMR2Bw1ORK+37INg==} + engines: {node: ^10 || ^12 || >=14.0} + peerDependencies: + postcss: ^8.2.15 + + postcss-normalize-repeat-style@5.1.1: + resolution: {integrity: sha512-mFpLspGWkQtBcWIRFLmewo8aC3ImN2i/J3v8YCFUwDnPu3Xz4rLohDO26lGjwNsQxB3YF0KKRwspGzE2JEuS0g==} + engines: {node: ^10 || ^12 || >=14.0} + peerDependencies: + postcss: ^8.2.15 + + postcss-normalize-string@5.1.0: + resolution: {integrity: sha512-oYiIJOf4T9T1N4i+abeIc7Vgm/xPCGih4bZz5Nm0/ARVJ7K6xrDlLwvwqOydvyL3RHNf8qZk6vo3aatiw/go3w==} + engines: {node: ^10 || ^12 || >=14.0} + peerDependencies: + postcss: ^8.2.15 + + postcss-normalize-timing-functions@5.1.0: + resolution: {integrity: sha512-DOEkzJ4SAXv5xkHl0Wa9cZLF3WCBhF3o1SKVxKQAa+0pYKlueTpCgvkFAHfk+Y64ezX9+nITGrDZeVGgITJXjg==} + engines: {node: ^10 || ^12 || >=14.0} + peerDependencies: + postcss: ^8.2.15 + + postcss-normalize-unicode@5.1.1: + resolution: {integrity: sha512-qnCL5jzkNUmKVhZoENp1mJiGNPcsJCs1aaRmURmeJGES23Z/ajaln+EPTD+rBeNkSryI+2WTdW+lwcVdOikrpA==} + engines: {node: ^10 || ^12 || >=14.0} + peerDependencies: + postcss: ^8.2.15 + + postcss-normalize-url@5.1.0: + resolution: {integrity: sha512-5upGeDO+PVthOxSmds43ZeMeZfKH+/DKgGRD7TElkkyS46JXAUhMzIKiCa7BabPeIy3AQcTkXwVVN7DbqsiCew==} + engines: {node: ^10 || ^12 || >=14.0} + peerDependencies: + postcss: ^8.2.15 + + postcss-normalize-whitespace@5.1.1: + resolution: {integrity: sha512-83ZJ4t3NUDETIHTa3uEg6asWjSBYL5EdkVB0sDncx9ERzOKBVJIUeDO9RyA9Zwtig8El1d79HBp0JEi8wvGQnA==} + engines: {node: ^10 || ^12 || >=14.0} + peerDependencies: + postcss: ^8.2.15 + + postcss-ordered-values@5.1.3: + resolution: {integrity: sha512-9UO79VUhPwEkzbb3RNpqqghc6lcYej1aveQteWY+4POIwlqkYE21HKWaLDF6lWNuqCobEAyTovVhtI32Rbv2RQ==} + engines: {node: ^10 || ^12 || >=14.0} + peerDependencies: + postcss: ^8.2.15 + + postcss-reduce-initial@5.1.2: + resolution: {integrity: sha512-dE/y2XRaqAi6OvjzD22pjTUQ8eOfc6m/natGHgKFBK9DxFmIm69YmaRVQrGgFlEfc1HePIurY0TmDeROK05rIg==} + engines: {node: ^10 || ^12 || >=14.0} + peerDependencies: + postcss: ^8.2.15 + + postcss-reduce-transforms@5.1.0: + resolution: {integrity: sha512-2fbdbmgir5AvpW9RLtdONx1QoYG2/EtqpNQbFASDlixBbAYuTcJ0dECwlqNqH7VbaUnEnh8SrxOe2sRIn24XyQ==} + engines: {node: ^10 || ^12 || >=14.0} + peerDependencies: + postcss: ^8.2.15 + + postcss-selector-parser@6.0.10: + resolution: {integrity: sha512-IQ7TZdoaqbT+LCpShg46jnZVlhWD2w6iQYAcYXfHARZ7X1t/UGhhceQDs5X0cGqKvYlHNOuv7Oa1xmb0oQuA3w==} + engines: {node: '>=4'} + + postcss-selector-parser@6.1.2: + resolution: {integrity: sha512-Q8qQfPiZ+THO/3ZrOrO0cJJKfpYCagtMUkXbnEfmgUjwXg6z/WBeOyS9APBBPCTSiDV+s4SwQGu8yFsiMRIudg==} + engines: {node: '>=4'} + + postcss-selector-parser@7.1.0: + resolution: {integrity: sha512-8sLjZwK0R+JlxlYcTuVnyT2v+htpdrjDOKuMcOVdYjt52Lh8hWRYpxBPoKx/Zg+bcjc3wx6fmQevMmUztS/ccA==} + engines: {node: '>=4'} + + postcss-svgo@5.1.0: + resolution: {integrity: sha512-D75KsH1zm5ZrHyxPakAxJWtkyXew5qwS70v56exwvw542d9CRtTo78K0WeFxZB4G7JXKKMbEZtZayTGdIky/eA==} + engines: {node: ^10 || ^12 || >=14.0} + peerDependencies: + postcss: ^8.2.15 + + postcss-unique-selectors@5.1.1: + resolution: {integrity: sha512-5JiODlELrz8L2HwxfPnhOWZYWDxVHWL83ufOv84NrcgipI7TaeRsatAhK4Tr2/ZiYldpK/wBvw5BD3qfaK96GA==} + engines: {node: ^10 || ^12 || >=14.0} + peerDependencies: + postcss: ^8.2.15 + + postcss-value-parser@4.2.0: + resolution: {integrity: sha512-1NNCs6uurfkVbeXG4S8JFT9t19m45ICnif8zWLd5oPSZ50QnwMfK+H3jv408d4jw/7Bttv5axS5IiHoLaVNHeQ==} + + postcss@8.5.3: + resolution: {integrity: sha512-dle9A3yYxlBSrt8Fu+IpjGT8SY8hN0mlaA6GY8t0P5PjIOZemULz/E2Bnm/2dcUOena75OTNkHI76uZBNUUq3A==} + engines: {node: ^10 || ^12 || >=14} + + prettier@2.8.7: + resolution: {integrity: sha512-yPngTo3aXUUmyuTjeTUT75txrf+aMh9FiD7q9ZE/i6r0bPb22g4FsE6Y338PQX1bmfy08i9QQCB7/rcUAVntfw==} + engines: {node: '>=10.13.0'} + hasBin: true + + prettier@2.8.8: + resolution: {integrity: sha512-tdN8qQGvNjw4CHbY+XXk0JgCXn9QiF21a55rBe5LJAU+kDyC4WQn4+awm2Xfk2lQMk5fKup9XgzTZtGkjBdP9Q==} + engines: {node: '>=10.13.0'} + hasBin: true + + pretty-bytes@3.0.1: + resolution: {integrity: sha512-eb7ZAeUTgfh294cElcu51w+OTRp/6ItW758LjwJSK72LDevcuJn0P4eD71PLMDGPwwatXmAmYHTkzvpKlJE3ow==} + engines: {node: '>=0.10.0'} + + pretty-bytes@5.6.0: + resolution: {integrity: sha512-FFw039TmrBqFK8ma/7OL3sDz/VytdtJr044/QUJtH0wK9lb9jLq9tJyIxUwtQJHwar2BqtiA4iCWSwo9JLkzFg==} + engines: {node: '>=6'} + + prismjs@1.30.0: + resolution: {integrity: sha512-DEvV2ZF2r2/63V+tK8hQvrR2ZGn10srHbXviTlcv7Kpzw8jWiNTqbVgjO3IY8RxrrOUF8VPMQQFysYYYv0YZxw==} + engines: {node: '>=6'} + + promise.series@0.2.0: + resolution: {integrity: sha512-VWQJyU2bcDTgZw8kpfBpB/ejZASlCrzwz5f2hjb/zlujOEB4oeiAhHygAWq8ubsX2GVkD4kCU5V2dwOTaCY5EQ==} + engines: {node: '>=0.12'} + + prompts@2.4.2: + resolution: {integrity: sha512-NxNv/kLguCA7p3jE8oL2aEBsrJWgAakBpgmgK6lpPWV+WuOmY6r2/zbAVnP+T8bQlA0nzHXSJSJW0Hq7ylaD2Q==} + engines: {node: '>= 6'} + + property-information@6.5.0: + resolution: {integrity: sha512-PgTgs/BlvHxOu8QuEN7wi5A0OmXaBcHpmCSTehcs6Uuu9IkDIEo13Hy7n898RHfrQ49vKCoGeWZSaAK01nwVig==} + + property-information@7.0.0: + resolution: {integrity: sha512-7D/qOz/+Y4X/rzSB6jKxKUsQnphO046ei8qxG59mtM3RG3DHgTK81HrxrmoDVINJb8NKT5ZsRbwHvQ6B68Iyhg==} + + proxy-from-env@1.1.0: + resolution: {integrity: sha512-D+zkORCbA9f1tdWRK0RaCR3GPv50cMxcrz4X8k5LTSUD1Dkw47mKJEZQNunItRTkWwgtaUSo1RVFRIG9ZXiFYg==} + + pump@3.0.2: + resolution: {integrity: sha512-tUPXtzlGM8FE3P0ZL6DVs/3P58k9nk8/jZeQCurTJylQA8qFYzHFfhBJkuqyE0FifOsQ0uKWekiZ5g8wtr28cw==} + + punycode.js@2.3.1: + resolution: {integrity: sha512-uxFIHU0YlHYhDQtV4R9J6a52SLx28BCjT+4ieh7IGbgwVJWO+km431c4yRlREUAsAmt/uMjQUyQHNEPf0M39CA==} + engines: {node: '>=6'} + + punycode@2.3.1: + resolution: {integrity: sha512-vYt7UD1U9Wg6138shLtLOvdAu+8DsC/ilFtEVHcH+wydcSpNE20AfSOduf6MkRFahL5FY7X1oU7nKVZFtfq8Fg==} + engines: {node: '>=6'} + + quansync@0.2.10: + resolution: {integrity: sha512-t41VRkMYbkHyCYmOvx/6URnN80H7k4X0lLdBMGsz+maAwrJQYB1djpV6vHrQIBE0WBSGqhtEHrK9U3DWWH8v7A==} + + queue-microtask@1.2.3: + resolution: {integrity: sha512-NuaNSa6flKT5JaSYQzJok04JzTL1CA6aGhv5rfLW3PgqA+M2ChpZQnAC8h8i4ZFkBS8X5RqkDBHA7r4hej3K9A==} + + radix3@1.1.2: + resolution: {integrity: sha512-b484I/7b8rDEdSDKckSSBA8knMpcdsXudlE/LNL639wFoHKwLbEkQFZHWEYwDC0wa0FKUcCY+GAF73Z7wxNVFA==} + + randombytes@2.1.0: + resolution: {integrity: sha512-vYl3iOX+4CKUWuxGi9Ukhie6fsqXqS9FE2Zaic4tNFD2N2QQaXOMFbuKK4QmDHC0JO6B1Zp41J0LpT0oR68amQ==} + + read-cache@1.0.0: + resolution: {integrity: sha512-Owdv/Ft7IjOgm/i0xvNDZ1LrRANRfew4b2prF3OWMQLxLfu3bS8FVhCsrSCMK4lR56Y9ya+AThoTpDCTxCmpRA==} + + readdirp@3.6.0: + resolution: {integrity: sha512-hOS089on8RduqdbhvQ5Z37A0ESjsqz6qnRcffsMU3495FuTdqSm+7bhJ29JvIOsBDEEnan5DPu9t3To9VRlMzA==} + engines: {node: '>=8.10.0'} + + readdirp@4.1.2: + resolution: {integrity: sha512-GDhwkLfywWL2s6vEjyhri+eXmfH6j1L7JE27WhqLeYzoh/A3DBaYGEj2H/HFZCn/kMfim73FXxEJTw06WtxQwg==} + engines: {node: '>= 14.18.0'} + + reading-time@1.5.0: + resolution: {integrity: sha512-onYyVhBNr4CmAxFsKS7bz+uTLRakypIe4R+5A824vBSkQy/hB3fZepoVEf8OVAxzLvK+H/jm9TzpI3ETSm64Kg==} + + rechoir@0.6.2: + resolution: {integrity: sha512-HFM8rkZ+i3zrV+4LQjwQ0W+ez98pApMGM3HUrN04j3CqzPOzl9nmP15Y8YXNm8QHGv/eacOVEjqhmWpkRV0NAw==} + engines: {node: '>= 0.10'} + + reflect.getprototypeof@1.0.10: + resolution: {integrity: sha512-00o4I+DVrefhv+nX0ulyi3biSHCPDe+yLv5o/p6d/UVlirijB8E16FtfwSAi4g3tcqrQ4lRAqQSoFEZJehYEcw==} + engines: {node: '>= 0.4'} + + regenerate-unicode-properties@10.2.0: + resolution: {integrity: sha512-DqHn3DwbmmPVzeKj9woBadqmXxLvQoQIwu7nopMc72ztvxVmVk2SBhSnx67zuye5TP+lJsb/TBQsjLKhnDf3MA==} + engines: {node: '>=4'} + + regenerate@1.4.2: + resolution: {integrity: sha512-zrceR/XhGYU/d/opr2EKO7aRHUeiBI8qjtfHqADTwZd6Szfy16la6kqD0MIUs5z5hx6AaKa+PixpPrR289+I0A==} + + regenerator-runtime@0.14.1: + resolution: {integrity: sha512-dYnhHh0nJoMfnkZs6GmmhFknAGRrLznOu5nc9ML+EJxGvrx6H7teuevqVqCuPcPK//3eDrrjQhehXVx9cnkGdw==} + + regenerator-transform@0.15.2: + resolution: {integrity: sha512-hfMp2BoF0qOk3uc5V20ALGDS2ddjQaLrdl7xrGXvAIow7qeWRM2VA2HuCHkUKk9slq3VwEwLNK3DFBqDfPGYtg==} + + regex-recursion@6.0.2: + resolution: {integrity: sha512-0YCaSCq2VRIebiaUviZNs0cBz1kg5kVS2UKUfNIx8YVs1cN3AV7NTctO5FOKBA+UT2BPJIWZauYHPqJODG50cg==} + + regex-utilities@2.3.0: + resolution: {integrity: sha512-8VhliFJAWRaUiVvREIiW2NXXTmHs4vMNnSzuJVhscgmGav3g9VDxLrQndI3dZZVVdp0ZO/5v0xmX516/7M9cng==} + + regex@6.0.1: + resolution: {integrity: sha512-uorlqlzAKjKQZ5P+kTJr3eeJGSVroLKoHmquUj4zHWuR+hEyNqlXsSKlYYF5F4NI6nl7tWCs0apKJ0lmfsXAPA==} + + regexp.prototype.flags@1.5.4: + resolution: {integrity: sha512-dYqgNSZbDwkaJ2ceRd9ojCGjBq+mOm9LmtXnAnEGyHhN/5R7iDW2TRw3h+o/jCFxus3P2LfWIIiwowAjANm7IA==} + engines: {node: '>= 0.4'} + + regexpu-core@6.2.0: + resolution: {integrity: sha512-H66BPQMrv+V16t8xtmq+UC0CBpiTBA60V8ibS1QVReIp8T1z8hwFxqcGzm9K6lgsN7sB5edVH8a+ze6Fqm4weA==} + engines: {node: '>=4'} + + regjsgen@0.8.0: + resolution: {integrity: sha512-RvwtGe3d7LvWiDQXeQw8p5asZUmfU1G/l6WbUXeHta7Y2PEIvBTwH6E2EfmYUK8pxcxEdEmaomqyp0vZZ7C+3Q==} + + regjsparser@0.12.0: + resolution: {integrity: sha512-cnE+y8bz4NhMjISKbgeVJtqNbtf5QpjZP+Bslo+UqkIt9QPnX9q095eiRRASJG1/tz6dlNr6Z5NsBiWYokp6EQ==} + hasBin: true + + rehype-autolink-headings@7.1.0: + resolution: {integrity: sha512-rItO/pSdvnvsP4QRB1pmPiNHUskikqtPojZKJPPPAVx9Hj8i8TwMBhofrrAYRhYOOBZH9tgmG5lPqDLuIWPWmw==} + + rehype-components@0.3.0: + resolution: {integrity: sha512-yl2bUkZi+sU0gxwVCun7IkjiDLPczSs1SKMKHmjlSLkk4mMryBd/aYba5J8suhJdquBEKSw6ZNxU3MvVQ9xqoQ==} + + rehype-expressive-code@0.41.3: + resolution: {integrity: sha512-8d9Py4c/V6I/Od2VIXFAdpiO2kc0SV2qTJsRAaqSIcM9aruW4ASLNe2kOEo1inXAAkIhpFzAHTc358HKbvpNUg==} + + rehype-external-links@3.0.0: + resolution: {integrity: sha512-yp+e5N9V3C6bwBeAC4n796kc86M4gJCdlVhiMTxIrJG5UHDMh+PJANf9heqORJbt1nrCbDwIlAZKjANIaVBbvw==} + + rehype-katex@7.0.1: + resolution: {integrity: sha512-OiM2wrZ/wuhKkigASodFoo8wimG3H12LWQaH8qSPVJn9apWKFSH3YOCtbKpBorTVw/eI7cuT21XBbvwEswbIOA==} + + rehype-parse@9.0.1: + resolution: {integrity: sha512-ksCzCD0Fgfh7trPDxr2rSylbwq9iYDkSn8TCDmEJ49ljEUBxDVCzCHv7QNzZOfODanX4+bWQ4WZqLCRWYLfhag==} + + rehype-raw@7.0.0: + resolution: {integrity: sha512-/aE8hCfKlQeA8LmyeyQvQF3eBiLRGNlfBJEvWH7ivp9sBqs7TNqBL5X3v157rM4IFETqDnIOO+z5M/biZbo9Ww==} + + rehype-slug@6.0.0: + resolution: {integrity: sha512-lWyvf/jwu+oS5+hL5eClVd3hNdmwM1kAC0BUvEGD19pajQMIzcNUd/k9GsfQ+FfECvX+JE+e9/btsKH0EjJT6A==} + + rehype-stringify@10.0.1: + resolution: {integrity: sha512-k9ecfXHmIPuFVI61B9DeLPN0qFHfawM6RsuX48hoqlaKSF61RskNjSm1lI8PhBEM0MRdLxVVm4WmTqJQccH9mA==} + + rehype@13.0.2: + resolution: {integrity: sha512-j31mdaRFrwFRUIlxGeuPXXKWQxet52RBQRvCmzl5eCefn/KGbomK5GMHNMsOJf55fgo3qw5tST5neDuarDYR2A==} + + remark-directive-rehype@0.4.2: + resolution: {integrity: sha512-T6e+IG+BwqU4++MK54vFb+KDFjs3a+tHeK6E0T0ctR1FSyngolfDtAEzqxHWlRzQZqGi2sB4DFXry6oqH87D/g==} + engines: {node: ^12.20.0 || ^14.13.1 || >=16.0.0} + + remark-directive@3.0.1: + resolution: {integrity: sha512-gwglrEQEZcZYgVyG1tQuA+h58EZfq5CSULw7J90AFuCTyib1thgHPoqQ+h9iFvU6R+vnZ5oNFQR5QKgGpk741A==} + + remark-gfm@4.0.1: + resolution: {integrity: sha512-1quofZ2RQ9EWdeN34S79+KExV1764+wCUGop5CPL1WGdD0ocPpu91lzPGbwWMECpEpd42kJGQwzRfyov9j4yNg==} + + remark-github-admonitions-to-directives@1.0.5: + resolution: {integrity: sha512-MSRzDs51HGbUrHJ0es8POuxwJiUycWw4aYCTN2RZhdOm5UvyqdB8ApWoGBj9QAiPSHKw2HWl1hd5rRzWxVfNew==} + + remark-math@6.0.0: + resolution: {integrity: sha512-MMqgnP74Igy+S3WwnhQ7kqGlEerTETXMvJhrUzDikVZ2/uogJCb+WHUg97hK9/jcfc0dkD73s3LN8zU49cTEtA==} + + remark-parse@11.0.0: + resolution: {integrity: sha512-FCxlKLNGknS5ba/1lmpYijMUzX2esxW5xQqjWxw2eHFfS2MSdaHVINFmhjo+qN1WhZhNimq0dZATN9pH0IDrpA==} + + remark-rehype@11.1.2: + resolution: {integrity: sha512-Dh7l57ianaEoIpzbp0PC9UKAdCSVklD8E5Rpw7ETfbTl3FqcOOgq5q2LVDhgGCkaBv7p24JXikPdvhhmHvKMsw==} + + remark-sectionize@2.1.0: + resolution: {integrity: sha512-R/pHt1RLYrEqrbwOVXx8HnvvwOg+mxg8pE4kIWpIYE3/CuZhU8/PAx/0y1BbHWUA0jmTLTeWpUlDrS/B0pyd0g==} + + remark-smartypants@3.0.2: + resolution: {integrity: sha512-ILTWeOriIluwEvPjv67v7Blgrcx+LZOkAUVtKI3putuhlZm84FnqDORNXPPm+HY3NdZOMhyDwZ1E+eZB/Df5dA==} + engines: {node: '>=16.0.0'} + + remark-stringify@11.0.0: + resolution: {integrity: sha512-1OSmLd3awB/t8qdoEOMazZkNsfVTeY4fTsgzcQFdXNq8ToTN4ZGwrMnlda4K6smTFKD+GRV6O48i6Z4iKgPPpw==} + + request-light@0.5.8: + resolution: {integrity: sha512-3Zjgh+8b5fhRJBQZoy+zbVKpAQGLyka0MPgW3zruTF4dFFJ8Fqcfu9YsAvi/rvdcaTeWG3MkbZv4WKxAn/84Lg==} + + request-light@0.7.0: + resolution: {integrity: sha512-lMbBMrDoxgsyO+yB3sDcrDuX85yYt7sS8BfQd11jtbW/z5ZWgLZRcEGLsLoYw7I0WSUGQBs8CC8ScIxkTX1+6Q==} + + require-directory@2.1.1: + resolution: {integrity: sha512-fGxEI7+wsG9xrvdjsrlmL22OMTTiHRwAMroiEeMgq8gzoLC/PQr7RsRDSTLUg/bZAZtF+TVIkHc6/4RIKrui+Q==} + engines: {node: '>=0.10.0'} + + require-from-string@2.0.2: + resolution: {integrity: sha512-Xf0nWe6RseziFMu+Ap9biiUbmplq6S9/p+7w7YXP/JBHhrUDDUhwa+vANyubuqfZWTveU//DYVGsDG7RKL/vEw==} + engines: {node: '>=0.10.0'} + + resolve-from@4.0.0: + resolution: {integrity: sha512-pb/MYmXstAkysRFx8piNI1tGFNQIFA3vkE3Gq4EuA1dF6gHp/+vgZqsCGJapvy8N3Q+4o7FwvquPJcnZ7RYy4g==} + engines: {node: '>=4'} + + resolve-from@5.0.0: + resolution: {integrity: sha512-qYg9KP24dD5qka9J47d0aVky0N+b4fTU89LN9iDnjB5waksiC49rvMB0PrUJQGoTmH50XPiqOvAjDfaijGxYZw==} + engines: {node: '>=8'} + + resolve@1.22.10: + resolution: {integrity: sha512-NPRy+/ncIMeDlTAsuqwKIiferiawhefFJtkNSW0qZJEqMEb+qBt/77B/jGeeek+F0uOeN05CDa6HXbbIgtVX4w==} + engines: {node: '>= 0.4'} + hasBin: true + + restructure@3.0.2: + resolution: {integrity: sha512-gSfoiOEA0VPE6Tukkrr7I0RBdE0s7H1eFCDBk05l1KIQT1UIKNc5JZy6jdyW6eYH3aR3g5b3PuL77rq0hvwtAw==} + + retext-latin@4.0.0: + resolution: {integrity: sha512-hv9woG7Fy0M9IlRQloq/N6atV82NxLGveq+3H2WOi79dtIYWN8OaxogDm77f8YnVXJL2VD3bbqowu5E3EMhBYA==} + + retext-smartypants@6.2.0: + resolution: {integrity: sha512-kk0jOU7+zGv//kfjXEBjdIryL1Acl4i9XNkHxtM7Tm5lFiCog576fjNC9hjoR7LTKQ0DsPWy09JummSsH1uqfQ==} + + retext-stringify@4.0.0: + resolution: {integrity: sha512-rtfN/0o8kL1e+78+uxPTqu1Klt0yPzKuQ2BfWwwfgIUSayyzxpM1PJzkKt4V8803uB9qSy32MvI7Xep9khTpiA==} + + retext@9.0.0: + resolution: {integrity: sha512-sbMDcpHCNjvlheSgMfEcVrZko3cDzdbe1x/e7G66dFp0Ff7Mldvi2uv6JkJQzdRcvLYE8CA8Oe8siQx8ZOgTcA==} + + reusify@1.1.0: + resolution: {integrity: sha512-g6QUff04oZpHs0eG5p83rFLhHeV00ug/Yf9nZM6fLeUrPguBTkTQOdpAWWspMh55TZfVQDPaN3NQJfbVRAxdIw==} + engines: {iojs: '>=1.0.0', node: '>=0.10.0'} + + rollup-plugin-bundle-size@1.0.3: + resolution: {integrity: sha512-aWj0Pvzq90fqbI5vN1IvUrlf4utOqy+AERYxwWjegH1G8PzheMnrRIgQ5tkwKVtQMDP0bHZEACW/zLDF+XgfXQ==} + + rollup-plugin-postcss@4.0.2: + resolution: {integrity: sha512-05EaY6zvZdmvPUDi3uCcAQoESDcYnv8ogJJQRp6V5kZ6J6P7uAVJlrTZcaaA20wTH527YTnKfkAoPxWI/jPp4w==} + engines: {node: '>=10'} + peerDependencies: + postcss: 8.x + + rollup-plugin-terser@7.0.2: + resolution: {integrity: sha512-w3iIaU4OxcF52UUXiZNsNeuXIMDvFrr+ZXK6bFZ0Q60qyVfq4uLptoS4bbq3paG3x216eQllFZX7zt6TIImguQ==} + deprecated: This package has been deprecated and is no longer maintained. Please use @rollup/plugin-terser + peerDependencies: + rollup: ^2.0.0 + + rollup-plugin-typescript2@0.32.1: + resolution: {integrity: sha512-RanO8bp1WbeMv0bVlgcbsFNCn+Y3rX7wF97SQLDxf0fMLsg0B/QFF005t4AsGUcDgF3aKJHoqt4JF2xVaABeKw==} + peerDependencies: + rollup: '>=1.26.3' + typescript: '>=2.4.0' + + rollup-plugin-visualizer@5.14.0: + resolution: {integrity: sha512-VlDXneTDaKsHIw8yzJAFWtrzguoJ/LnQ+lMpoVfYJ3jJF4Ihe5oYLAqLklIK/35lgUY+1yEzCkHyZ1j4A5w5fA==} + engines: {node: '>=18'} + hasBin: true + peerDependencies: + rolldown: 1.x + rollup: 2.x || 3.x || 4.x + peerDependenciesMeta: + rolldown: + optional: true + rollup: + optional: true + + rollup-pluginutils@2.8.2: + resolution: {integrity: sha512-EEp9NhnUkwY8aif6bxgovPHMoMoNr2FulJziTndpt5H9RdwC47GSGuII9XxpSdzVGM0GWrNPHV6ie1LTNJPaLQ==} + + rollup@2.79.2: + resolution: {integrity: sha512-fS6iqSPZDs3dr/y7Od6y5nha8dW1YnbgtsyotCVvoFGKbERG++CVRFv1meyGDE1SNItQA8BrnCw7ScdAhRJ3XQ==} + engines: {node: '>=10.0.0'} + hasBin: true + + rollup@4.40.1: + resolution: {integrity: sha512-C5VvvgCCyfyotVITIAv+4efVytl5F7wt+/I2i9q9GZcEXW9BP52YYOXC58igUi+LFZVHukErIIqQSWwv/M3WRw==} + engines: {node: '>=18.0.0', npm: '>=8.0.0'} + hasBin: true + + run-parallel@1.2.0: + resolution: {integrity: sha512-5l4VyZR86LZ/lDxZTR6jqL8AFE2S0IFLMP26AbjsLVADxHdhB/c0GUsH+y39UfCi3dzz8OlQuPmnaJOMoDHQBA==} + + sade@1.8.1: + resolution: {integrity: sha512-xal3CZX1Xlo/k4ApwCFrHVACi9fBqJ7V+mwhBsuf/1IOKbBy098Fex+Wa/5QMubw09pSZ/u8EY8PWgevJsXp1A==} + engines: {node: '>=6'} + + safe-array-concat@1.1.3: + resolution: {integrity: sha512-AURm5f0jYEOydBj7VQlVvDrjeFgthDdEF5H1dP+6mNpoXOMo1quQqJ4wvJDyRZ9+pO3kGWoOdmV08cSv2aJV6Q==} + engines: {node: '>=0.4'} + + safe-buffer@5.2.1: + resolution: {integrity: sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ==} + + safe-identifier@0.4.2: + resolution: {integrity: sha512-6pNbSMW6OhAi9j+N8V+U715yBQsaWJ7eyEUaOrawX+isg5ZxhUlV1NipNtgaKHmFGiABwt+ZF04Ii+3Xjkg+8w==} + + safe-push-apply@1.0.0: + resolution: {integrity: sha512-iKE9w/Z7xCzUMIZqdBsp6pEQvwuEebH4vdpjcDWnyzaI6yl6O9FHvVpmGelvEHNsoY6wGblkxR6Zty/h00WiSA==} + engines: {node: '>= 0.4'} + + safe-regex-test@1.1.0: + resolution: {integrity: sha512-x/+Cz4YrimQxQccJf5mKEbIa1NzeCRNI5Ecl/ekmlYaampdNLPalVyIcCZNNH3MvmqBugV5TMYZXv0ljslUlaw==} + engines: {node: '>= 0.4'} + + safer-buffer@2.1.2: + resolution: {integrity: sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==} + + sanitize-html@2.16.0: + resolution: {integrity: sha512-0s4caLuHHaZFVxFTG74oW91+j6vW7gKbGD6CD2+miP73CE6z6YtOBN0ArtLd2UGyi4IC7K47v3ENUbQX4jV3Mg==} + + sass@1.80.4: + resolution: {integrity: sha512-rhMQ2tSF5CsuuspvC94nPM9rToiAFw2h3JTrLlgmNw1MH79v8Cr3DH6KF6o6r+8oofY3iYVPUf66KzC8yuVN1w==} + engines: {node: '>=14.0.0'} + hasBin: true + + sax@1.4.1: + resolution: {integrity: sha512-+aWOz7yVScEGoKNd4PA10LZ8sk0A/z5+nXQG5giUO5rprX9jgYsTdov9qCchZiPIZezbZH+jRut8nPodFAX4Jg==} + + scrl@2.0.0: + resolution: {integrity: sha512-BbbVXxrOn58Ge4wjOORIRVZamssQu08ISLL/AC2z9aATIsKqZLESwZVW5YR0Yz0C7qqDRHb4yNXJlQ8yW0SGHw==} + + semver@6.3.1: + resolution: {integrity: sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==} + hasBin: true + + semver@7.7.1: + resolution: {integrity: sha512-hlq8tAfn0m/61p4BVRcPzIGr6LKiMwo4VM6dGi6pt4qcRkmNzTcWq6eCEjEh+qXjkMDvPlOFFSGwQjoEa6gyMA==} + engines: {node: '>=10'} + hasBin: true + + serialize-javascript@4.0.0: + resolution: {integrity: sha512-GaNA54380uFefWghODBWEGisLZFj00nS5ACs6yHa9nLqlLpVLO8ChDGeKRjZnV4Nh4n0Qi7nhYZD/9fCPzEqkw==} + + set-function-length@1.2.2: + resolution: {integrity: sha512-pgRc4hJ4/sNjWCSS9AmnS40x3bNMDTknHgL5UaMBTMyJnU90EgWh1Rz+MC9eFu4BuN/UwZjKQuY/1v3rM7HMfg==} + engines: {node: '>= 0.4'} + + set-function-name@2.0.2: + resolution: {integrity: sha512-7PGFlmtwsEADb0WYyvCMa1t+yke6daIG4Wirafur5kcf+MhUnPms1UeR0CKQdTZD81yESwMHbtn+TR+dMviakQ==} + engines: {node: '>= 0.4'} + + set-proto@1.0.0: + resolution: {integrity: sha512-RJRdvCo6IAnPdsvP/7m6bsQqNnn1FCBX5ZNtFL98MmFF/4xAIJTIg1YbHW5DC2W5SKZanrC6i4HsJqlajw/dZw==} + engines: {node: '>= 0.4'} + + sharp@0.33.5: + resolution: {integrity: sha512-haPVm1EkS9pgvHrQ/F3Xy+hgcuMV0Wm9vfIBSiwZ05k+xgb0PkBQpGsAA/oWdDobNaZTH5ppvHtzCFbnSEwHVw==} + engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0} + + sharp@0.34.1: + resolution: {integrity: sha512-1j0w61+eVxu7DawFJtnfYcvSv6qPFvfTaqzTQ2BLknVhHTwGS8sc63ZBF4rzkWMBVKybo4S5OBtDdZahh2A1xg==} + engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0} + + shebang-command@2.0.0: + resolution: {integrity: sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA==} + engines: {node: '>=8'} + + shebang-regex@3.0.0: + resolution: {integrity: sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A==} + engines: {node: '>=8'} + + shelljs-live@0.0.5: + resolution: {integrity: sha512-IR5+gA7f+v/V8ao7ZKE4TQpbG6ABeGxQhwL0seIbOXvHdoFAHw3MEiUICrhUfuroRREKL0n7HDA5b/R5it8KHg==} + peerDependencies: + shelljs: ^0.8.4 + + shelljs@0.8.5: + resolution: {integrity: sha512-TiwcRcrkhHvbrZbnRcFYMLl30Dfov3HKqzp5tO5b4pt6G/SezKcYhmDg15zXVBswHmctSAQKznqNW2LO5tTDow==} + engines: {node: '>=4'} + hasBin: true + + shiki@3.3.0: + resolution: {integrity: sha512-j0Z1tG5vlOFGW8JVj0Cpuatzvshes7VJy5ncDmmMaYcmnGW0Js1N81TOW98ivTFNZfKRn9uwEg/aIm638o368g==} + + side-channel-list@1.0.0: + resolution: {integrity: sha512-FCLHtRD/gnpCiCHEiJLOwdmFP+wzCmDEkc9y7NsYxeF4u7Btsn1ZuwgwJGxImImHicJArLP4R0yX4c2KCrMrTA==} + engines: {node: '>= 0.4'} + + side-channel-map@1.0.1: + resolution: {integrity: sha512-VCjCNfgMsby3tTdo02nbjtM/ewra6jPHmpThenkTYh8pG9ucZ/1P8So4u4FGBek/BjpOVsDCMoLA/iuBKIFXRA==} + engines: {node: '>= 0.4'} + + side-channel-weakmap@1.0.2: + resolution: {integrity: sha512-WPS/HvHQTYnHisLo9McqBHOJk2FkHO/tlpvldyrnem4aeQp4hai3gythswg6p01oSoTl58rcpiFAjF2br2Ak2A==} + engines: {node: '>= 0.4'} + + side-channel@1.1.0: + resolution: {integrity: sha512-ZX99e6tRweoUXqR+VBrslhda51Nh5MTQwou5tnUDgbtyM0dBgmhEDtWGP/xbKn6hqfPRHujUNwz5fy/wbbhnpw==} + engines: {node: '>= 0.4'} + + signal-exit@4.1.0: + resolution: {integrity: sha512-bzyZ1e88w9O1iNJbKnOlvYTrWPDl46O1bG0D3XInv+9tkPrxrN8jUUTiFlDkkmKWgn1M6CfIA13SuGqOa9Korw==} + engines: {node: '>=14'} + + simple-swizzle@0.2.2: + resolution: {integrity: sha512-JA//kQgZtbuY83m+xT+tXJkmJncGMTFT+C+g2h2R9uxkYIrE2yy9sgmcLhCnw57/WSD+Eh3J97FPEDFnbXnDUg==} + + sisteransi@1.0.5: + resolution: {integrity: sha512-bLGGlR1QxBcynn2d5YmDX4MGjlZvy2MRBDRNHLJ8VI6l6+9FUiyTFNJ0IveOSP0bcXgVDPRcfGqA0pjaqUpfVg==} + + sitemap@8.0.0: + resolution: {integrity: sha512-+AbdxhM9kJsHtruUF39bwS/B0Fytw6Fr1o4ZAIAEqA6cke2xcoO2GleBw9Zw7nRzILVEgz7zBM5GiTJjie1G9A==} + engines: {node: '>=14.0.0', npm: '>=6.0.0'} + hasBin: true + + slash@3.0.0: + resolution: {integrity: sha512-g9Q1haeby36OSStwb4ntCGGGaKsaVSjQ68fBxoQcutl5fS1vuY18H3wSt3jFyFtrkx+Kz0V1G85A4MyAdDMi2Q==} + engines: {node: '>=8'} + + smol-toml@1.3.4: + resolution: {integrity: sha512-UOPtVuYkzYGee0Bd2Szz8d2G3RfMfJ2t3qVdZUAozZyAk+a0Sxa+QKix0YCwjL/A1RR0ar44nCxaoN9FxdJGwA==} + engines: {node: '>= 18'} + + source-map-js@1.2.1: + resolution: {integrity: sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA==} + engines: {node: '>=0.10.0'} + + source-map-support@0.5.21: + resolution: {integrity: sha512-uBHU3L3czsIyYXKX88fdrGovxdSCoTGDRZ6SYXtSRxLZUzHg5P/66Ht6uoUlHu9EZod+inXhKo3qQgwXUT/y1w==} + + source-map@0.6.1: + resolution: {integrity: sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==} + engines: {node: '>=0.10.0'} + + source-map@0.7.4: + resolution: {integrity: sha512-l3BikUxvPOcn5E74dZiq5BGsTb5yEwhaTSzccU6t4sDOH8NWJCstKO5QT2CvtFoK6F0saL7p9xHAqHOlCPJygA==} + engines: {node: '>= 8'} + + sourcemap-codec@1.4.8: + resolution: {integrity: sha512-9NykojV5Uih4lgo5So5dtw+f0JgJX30KCNI8gwhz2J9A15wD0Ml6tjHKwf6fTSa6fAdVBdZeNOs9eJ71qCk8vA==} + deprecated: Please use @jridgewell/sourcemap-codec instead + + space-separated-tokens@2.0.2: + resolution: {integrity: sha512-PEGlAwrG8yXGXRjW32fGbg66JAlOAwbObuqVoJpv/mRgoWDQfgH1wDPvtzWyUSNAXBGSk8h755YDbbcEy3SH2Q==} + + stable@0.1.8: + resolution: {integrity: sha512-ji9qxRnOVfcuLDySj9qzhGSEFVobyt1kIOSkj1qZzYLzq7Tos/oUUWvotUPQLlrsidqsK6tBH89Bc9kL5zHA6w==} + deprecated: 'Modern JS already guarantees Array#sort() is a stable sort, so this library is deprecated. See the compatibility table on MDN: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Array/sort#browser_compatibility' + + stream-replace-string@2.0.0: + resolution: {integrity: sha512-TlnjJ1C0QrmxRNrON00JvaFFlNh5TTG00APw23j74ET7gkQpTASi6/L2fuiav8pzK715HXtUeClpBTw2NPSn6w==} + + string-hash@1.1.3: + resolution: {integrity: sha512-kJUvRUFK49aub+a7T1nNE66EJbZBMnBgoC1UbCZ5n6bsZKBRga4KgBRTMn/pFkeCZSYtNeSyMxPDM0AXWELk2A==} + + string-width@4.2.3: + resolution: {integrity: sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==} + engines: {node: '>=8'} + + string-width@5.1.2: + resolution: {integrity: sha512-HnLOCR3vjcY8beoNLtcjZ5/nxn2afmME6lhrDrebokqMap+XbeW8n9TXpPDOqdGK5qcI3oT0GKTW6wC7EMiVqA==} + engines: {node: '>=12'} + + string-width@7.2.0: + resolution: {integrity: sha512-tsaTIkKW9b4N+AEj+SVA+WhJzV7/zMhcSu78mLKWSk7cXMOSHsBKFWUs0fWwq8QyK3MgJBQRX6Gbi4kYbdvGkQ==} + engines: {node: '>=18'} + + string.prototype.matchall@4.0.12: + resolution: {integrity: sha512-6CC9uyBL+/48dYizRf7H7VAYCMCNTBeM78x/VTUe9bFEaxBepPJDa1Ow99LqI/1yF7kuy7Q3cQsYMrcjGUcskA==} + engines: {node: '>= 0.4'} + + string.prototype.trim@1.2.10: + resolution: {integrity: sha512-Rs66F0P/1kedk5lyYyH9uBzuiI/kNRmwJAR9quK6VOtIpZ2G+hMZd+HQbbv25MgCA6gEffoMZYxlTod4WcdrKA==} + engines: {node: '>= 0.4'} + + string.prototype.trimend@1.0.9: + resolution: {integrity: sha512-G7Ok5C6E/j4SGfyLCloXTrngQIQU3PWtXGst3yM7Bea9FRURf1S42ZHlZZtsNque2FN2PoUhfZXYLNWwEr4dLQ==} + engines: {node: '>= 0.4'} + + string.prototype.trimstart@1.0.8: + resolution: {integrity: sha512-UXSH262CSZY1tfu3G3Secr6uGLCFVPMhIqHjlgCUtCCcgihYc/xKs9djMTMUOb2j1mVSeU8EU6NWc/iQKU6Gfg==} + engines: {node: '>= 0.4'} + + stringify-entities@4.0.4: + resolution: {integrity: sha512-IwfBptatlO+QCJUo19AqvrPNqlVMpW9YEL2LIVY+Rpv2qsjCGxaDLNRgeGsQWJhfItebuJhsGSLjaBbNSQ+ieg==} + + strip-ansi@3.0.1: + resolution: {integrity: sha512-VhumSSbBqDTP8p2ZLKj40UjBCV4+v8bUSEpUb4KjRgWk9pbqGF4REFj6KEagidb2f/M6AzC0EmFyDNGaw9OCzg==} + engines: {node: '>=0.10.0'} + + strip-ansi@6.0.1: + resolution: {integrity: sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==} + engines: {node: '>=8'} + + strip-ansi@7.1.0: + resolution: {integrity: sha512-iq6eVVI64nQQTRYq2KtEg2d2uU7LElhTJwsH4YzIHZshxlgZms/wIc4VoDQTlG/IvVIrBKG06CrZnp0qv7hkcQ==} + engines: {node: '>=12'} + + strnum@1.1.2: + resolution: {integrity: sha512-vrN+B7DBIoTTZjnPNewwhx6cBA/H+IS7rfW68n7XxC1y7uoiGQBxaKzqucGUgavX15dJgiGztLJ8vxuEzwqBdA==} + + style-inject@0.3.0: + resolution: {integrity: sha512-IezA2qp+vcdlhJaVm5SOdPPTUu0FCEqfNSli2vRuSIBbu5Nq5UvygTk/VzeCqfLz2Atj3dVII5QBKGZRZ0edzw==} + + stylehacks@5.1.1: + resolution: {integrity: sha512-sBpcd5Hx7G6seo7b1LkpttvTz7ikD0LlH5RmdcBNb6fFR0Fl7LQwHDFr300q4cwUqi+IYrFGmsIHieMBfnN/Bw==} + engines: {node: ^10 || ^12 || >=14.0} + peerDependencies: + postcss: ^8.2.15 + + stylus@0.64.0: + resolution: {integrity: sha512-ZIdT8eUv8tegmqy1tTIdJv9We2DumkNZFdCF5mz/Kpq3OcTaxSuCAYZge6HKK2CmNC02G1eJig2RV7XTw5hQrA==} + engines: {node: '>=16'} + hasBin: true + + sucrase@3.35.0: + resolution: {integrity: sha512-8EbVDiu9iN/nESwxeSxDKe0dunta1GOlHufmSSXxMD2z2/tMZpDMpvXQGsc+ajGo8y2uYUmixaSRUc/QPoQ0GA==} + engines: {node: '>=16 || 14 >=14.17'} + hasBin: true + + supports-color@2.0.0: + resolution: {integrity: sha512-KKNVtd6pCYgPIKU4cp2733HWYCpplQhddZLBUryaAHou723x+FRzQ5Df824Fj+IyyuiQTRoub4SnIFfIcrp70g==} + engines: {node: '>=0.8.0'} + + supports-color@7.2.0: + resolution: {integrity: sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==} + engines: {node: '>=8'} + + supports-preserve-symlinks-flag@1.0.0: + resolution: {integrity: sha512-ot0WnXS9fgdkgIcePe6RHNk1WA8+muPa6cSjeR3V8K27q9BB1rTE3R1p7Hv0z1ZyAc8s6Vvv8DIyWf681MAt0w==} + engines: {node: '>= 0.4'} + + svelte2tsx@0.7.36: + resolution: {integrity: sha512-nBlERuCZRwmpebC8m0vDqZ9oaKsqW8frQS2l3zwFQW1voQIkItYtHxh1F5OTZEmE0meDIH6cxU36eIOQVOxlCw==} + peerDependencies: + svelte: ^3.55 || ^4.0.0-next.0 || ^4.0 || ^5.0.0-next.0 + typescript: ^4.9.4 || ^5.0.0 + + svelte@5.28.2: + resolution: {integrity: sha512-FbWBxgWOpQfhKvoGJv/TFwzqb4EhJbwCD17dB0tEpQiw1XyUEKZJtgm4nA4xq3LLsMo7hu5UY/BOFmroAxKTMg==} + engines: {node: '>=18'} + + svgo@2.8.0: + resolution: {integrity: sha512-+N/Q9kV1+F+UeWYoSiULYo4xYSDQlTgb+ayMobAXPwMnLvop7oxKMo9OzIrX5x3eS4L4f2UHhc9axXwY8DpChg==} + engines: {node: '>=10.13.0'} + hasBin: true + + svgo@3.3.2: + resolution: {integrity: sha512-OoohrmuUlBs8B8o6MB2Aevn+pRIH9zDALSR+6hhqVfa6fRwG/Qw9VUMSMW9VNg2CFc/MTIfabtdOVl9ODIJjpw==} + engines: {node: '>=14.0.0'} + hasBin: true + + swup-morph-plugin@1.3.0: + resolution: {integrity: sha512-vTqWYA5ZFkWMo54K8jlol5OCvboqRsELLfM1PUkS2IiL+1dDDChzMHa4ZBI5+yfl7bZUCWgd8EmuhMd/i/o+Qg==} + peerDependencies: + swup: ^4.6.0 + + swup@4.8.1: + resolution: {integrity: sha512-MEXrQUvsUE9lyt1SX8KEZcGmPigAvtkVvHwJf22MOKsjb+aDgiljTc5LTXz0ehsA/1OOXRqvtS4kWLey26i/hQ==} + + tailwindcss@3.4.17: + resolution: {integrity: sha512-w33E2aCvSDP0tW9RZuNXadXlkHXqFzSkQew/aIa2i/Sj8fThxwovwlXHSPXTbAHwEIhBFXAedUhP2tueAKP8Og==} + engines: {node: '>=14.0.0'} + hasBin: true + + tar@6.2.1: + resolution: {integrity: sha512-DZ4yORTwrbTj/7MZYq2w+/ZFdI6OZ/f9SFHR+71gIVUZhOQPHzVCLpvRnPgyaMpfWxxk/4ONva3GQSyNIKRv6A==} + engines: {node: '>=10'} + + tar@7.4.3: + resolution: {integrity: sha512-5S7Va8hKfV7W5U6g3aYxXmlPoZVAwUMy9AOKyF2fVuZa2UD3qZjg578OrLRt8PcNN1PleVaL/5/yYATNL0ICUw==} + engines: {node: '>=18'} + + terser@5.39.0: + resolution: {integrity: sha512-LBAhFyLho16harJoWMg/nZsQYgTrg5jXOn2nCYjRUcZZEdE3qa2zb8QEDRUGVZBW4rlazf2fxkg8tztybTaqWw==} + engines: {node: '>=10'} + hasBin: true + + thenify-all@1.6.0: + resolution: {integrity: sha512-RNxQH/qI8/t3thXJDwcstUO4zeqo64+Uy/+sNVRBx4Xn2OX+OZ9oP+iJnNFqplFra2ZUVeKCSa2oVWi3T4uVmA==} + engines: {node: '>=0.8'} + + thenify@3.3.1: + resolution: {integrity: sha512-RVZSIV5IG10Hk3enotrhvz0T9em6cyHBLkH/YAZuKqd8hRkKhSfCGIcP2KUY0EPxndzANBmNllzWPwak+bheSw==} + + tiny-glob@0.2.9: + resolution: {integrity: sha512-g/55ssRPUjShh+xkfx9UPDXqhckHEsHr4Vd9zX55oSdGZc/MD0m3sferOkwWtp98bv+kcVfEHtRJgBVJzelrzg==} + + tiny-inflate@1.0.3: + resolution: {integrity: sha512-pkY1fj1cKHb2seWDy0B16HeWyczlJA9/WW3u3c4z/NiWDsO3DOU5D7nhTLE9CF0yXv/QZFY7sEJmj24dK+Rrqw==} + + tinyexec@0.3.2: + resolution: {integrity: sha512-KQQR9yN7R5+OSwaK0XQoj22pwHoTlgYqmUscPYoknOoWCWfj/5/ABTMRi69FrKU5ffPVh5QcFikpWJI/P1ocHA==} + + tinyglobby@0.2.13: + resolution: {integrity: sha512-mEwzpUgrLySlveBwEVDMKk5B57bhLPYovRfPAXD5gA/98Opn0rCDj3GtLwFvCvH5RK9uPCExUROW5NjDwvqkxw==} + engines: {node: '>=12.0.0'} + + to-regex-range@5.0.1: + resolution: {integrity: sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ==} + engines: {node: '>=8.0'} + + tosource@2.0.0-alpha.3: + resolution: {integrity: sha512-KAB2lrSS48y91MzFPFuDg4hLbvDiyTjOVgaK7Erw+5AmZXNq4sFRVn8r6yxSLuNs15PaokrDRpS61ERY9uZOug==} + engines: {node: '>=10'} + + tr46@0.0.3: + resolution: {integrity: sha512-N3WMsuqV66lT30CrXNbEjx4GEwlow3v6rr4mCcv6prnfwhS01rkgyFdjPNBYd9br7LpXV1+Emh01fHnq2Gdgrw==} + + trim-lines@3.0.1: + resolution: {integrity: sha512-kRj8B+YHZCc9kQYdWfJB2/oUl9rA99qbowYYBtr4ui4mZyAQ2JpvVBd/6U2YloATfqBhBTSMhTpgBHtU0Mf3Rg==} + + trough@2.2.0: + resolution: {integrity: sha512-tmMpK00BjZiUyVyvrBK7knerNgmgvcV/KLVyuma/SC+TQN167GrMRciANTz09+k3zW8L8t60jWO1GpfkZdjTaw==} + + ts-interface-checker@0.1.13: + resolution: {integrity: sha512-Y/arvbn+rrz3JCKl9C4kVNfTfSm2/mEp5FSz5EsZSANGPSlQrpRI5M4PKF+mJnE52jOO90PnPSc3Ur3bTQw0gA==} + + tsconfck@3.1.5: + resolution: {integrity: sha512-CLDfGgUp7XPswWnezWwsCRxNmgQjhYq3VXHM0/XIRxhVrKw0M1if9agzryh1QS3nxjCROvV+xWxoJO1YctzzWg==} + engines: {node: ^18 || >=20} + hasBin: true + peerDependencies: + typescript: ^5.0.0 + peerDependenciesMeta: + typescript: + optional: true + + tslib@2.8.1: + resolution: {integrity: sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==} + + type-fest@4.40.1: + resolution: {integrity: sha512-9YvLNnORDpI+vghLU/Nf+zSv0kL47KbVJ1o3sKgoTefl6i+zebxbiDQWoe/oWWqPhIgQdRZRT1KA9sCPL810SA==} + engines: {node: '>=16'} + + typed-array-buffer@1.0.3: + resolution: {integrity: sha512-nAYYwfY3qnzX30IkA6AQZjVbtK6duGontcQm1WSG1MD94YLqK0515GNApXkoxKOWMusVssAHWLh9SeaoefYFGw==} + engines: {node: '>= 0.4'} + + typed-array-byte-length@1.0.3: + resolution: {integrity: sha512-BaXgOuIxz8n8pIq3e7Atg/7s+DpiYrxn4vdot3w9KbnBhcRQq6o3xemQdIfynqSeXeDrF32x+WvfzmOjPiY9lg==} + engines: {node: '>= 0.4'} + + typed-array-byte-offset@1.0.4: + resolution: {integrity: sha512-bTlAFB/FBYMcuX81gbL4OcpH5PmlFHqlCCpAl8AlEzMz5k53oNDvN8p1PNOWLEmI2x4orp3raOFB51tv9X+MFQ==} + engines: {node: '>= 0.4'} + + typed-array-length@1.0.7: + resolution: {integrity: sha512-3KS2b+kL7fsuk/eJZ7EQdnEmQoaho/r6KUef7hxvltNA5DR8NAUM+8wJMbJyZ4G9/7i3v5zPBIMN5aybAh2/Jg==} + engines: {node: '>= 0.4'} + + typed-query-selector@2.12.0: + resolution: {integrity: sha512-SbklCd1F0EiZOyPiW192rrHZzZ5sBijB6xM+cpmrwDqObvdtunOHHIk9fCGsoK5JVIYXoyEp4iEdE3upFH3PAg==} + + typesafe-path@0.2.2: + resolution: {integrity: sha512-OJabfkAg1WLZSqJAJ0Z6Sdt3utnbzr/jh+NAHoyWHJe8CMSy79Gm085094M9nvTPy22KzTVn5Zq5mbapCI/hPA==} + + typescript-auto-import-cache@0.3.5: + resolution: {integrity: sha512-fAIveQKsoYj55CozUiBoj4b/7WpN0i4o74wiGY5JVUEoD0XiqDk1tJqTEjgzL2/AizKQrXxyRosSebyDzBZKjw==} + + typescript@4.9.5: + resolution: {integrity: sha512-1FXk9E2Hm+QzZQ7z+McJiHL4NW1F2EzMu9Nq9i3zAaGqibafqYwCVU6WyWAuyQRRzOlxou8xZSyXLEN8oKj24g==} + engines: {node: '>=4.2.0'} + hasBin: true + + typescript@5.8.3: + resolution: {integrity: sha512-p1diW6TqL9L07nNxvRMM7hMMw4c5XOo/1ibL4aAIGmSAt9slTE1Xgw5KWuof2uTOvCg9BY7ZRi+GaF+7sfgPeQ==} + engines: {node: '>=14.17'} + hasBin: true + + uc.micro@2.1.0: + resolution: {integrity: sha512-ARDJmphmdvUk6Glw7y9DQ2bFkKBHwQHLi2lsaH6PPmz/Ka9sFOBsBluozhDltWmnv9u/cF6Rt87znRTPV+yp/A==} + + ufo@1.6.1: + resolution: {integrity: sha512-9a4/uxlTWJ4+a5i0ooc1rU7C7YOw3wT+UGqdeNNHWnOF9qcMBgLRS+4IYUqbczewFx4mLEig6gawh7X6mFlEkA==} + + ultrahtml@1.6.0: + resolution: {integrity: sha512-R9fBn90VTJrqqLDwyMph+HGne8eqY1iPfYhPzZrvKpIfwkWZbcYlfpsb8B9dTvBfpy1/hqAD7Wi8EKfP9e8zdw==} + + unbox-primitive@1.1.0: + resolution: {integrity: sha512-nWJ91DjeOkej/TA8pXQ3myruKpKEYgqvpw9lz4OPHj/NWFNluYrjbz9j01CJ8yKQd2g4jFoOkINCTW2I5LEEyw==} + engines: {node: '>= 0.4'} + + uncrypto@0.1.3: + resolution: {integrity: sha512-Ql87qFHB3s/De2ClA9e0gsnS6zXG27SkTiSJwjCc9MebbfapQfuPzumMIUMi38ezPZVNFcHI9sUIepeQfw8J8Q==} + + undici-types@6.21.0: + resolution: {integrity: sha512-iwDZqg0QAGrg9Rav5H4n0M64c3mkR59cJ6wQp+7C4nI0gsmExaedaYLNO44eT4AtBBwjbTiGPMlt2Md0T9H9JQ==} + + undici@6.21.2: + resolution: {integrity: sha512-uROZWze0R0itiAKVPsYhFov9LxrPMHLMEQFszeI2gCN6bnIIZ8twzBCJcN2LJrBBLfrP0t1FW0g+JmKVl8Vk1g==} + engines: {node: '>=18.17'} + + unicode-canonical-property-names-ecmascript@2.0.1: + resolution: {integrity: sha512-dA8WbNeb2a6oQzAQ55YlT5vQAWGV9WXOsi3SskE3bcCdM0P4SDd+24zS/OCacdRq5BkdsRj9q3Pg6YyQoxIGqg==} + engines: {node: '>=4'} + + unicode-match-property-ecmascript@2.0.0: + resolution: {integrity: sha512-5kaZCrbp5mmbz5ulBkDkbY0SsPOjKqVS35VpL9ulMPfSl0J0Xsm+9Evphv9CoIZFwre7aJoa94AY6seMKGVN5Q==} + engines: {node: '>=4'} + + unicode-match-property-value-ecmascript@2.2.0: + resolution: {integrity: sha512-4IehN3V/+kkr5YeSSDDQG8QLqO26XpL2XP3GQtqwlT/QYSECAwFztxVHjlbh0+gjJ3XmNLS0zDsbgs9jWKExLg==} + engines: {node: '>=4'} + + unicode-properties@1.4.1: + resolution: {integrity: sha512-CLjCCLQ6UuMxWnbIylkisbRj31qxHPAurvena/0iwSVbQ2G1VY5/HjV0IRabOEbDHlzZlRdCrD4NhB0JtU40Pg==} + + unicode-property-aliases-ecmascript@2.1.0: + resolution: {integrity: sha512-6t3foTQI9qne+OZoVQB/8x8rk2k1eVy1gRXhV3oFQ5T6R1dqQ1xtin3XqSlx3+ATBkliTaR/hHyJBm+LVPNM8w==} + engines: {node: '>=4'} + + unicode-trie@2.0.0: + resolution: {integrity: sha512-x7bc76x0bm4prf1VLg79uhAzKw8DVboClSN5VxJuQ+LKDOVEW9CdH+VY7SP+vX7xCYQqzzgQpFqz15zeLvAtZQ==} + + unified@11.0.5: + resolution: {integrity: sha512-xKvGhPWw3k84Qjh8bI3ZeJjqnyadK+GEFtazSfZv/rKeTkTjOJho6mFqh2SM96iIcZokxiOpg78GazTSg8+KHA==} + + unifont@0.4.1: + resolution: {integrity: sha512-zKSY9qO8svWYns+FGKjyVdLvpGPwqmsCjeJLN1xndMiqxHWBAhoWDMYMG960MxeV48clBmG+fDP59dHY1VoZvg==} + + unist-util-find-after@4.0.1: + resolution: {integrity: sha512-QO/PuPMm2ERxC6vFXEPtmAutOopy5PknD+Oq64gGwxKtk4xwo9Z97t9Av1obPmGU0IyTa6EKYUfTrK2QJS3Ozw==} + + unist-util-find-after@5.0.0: + resolution: {integrity: sha512-amQa0Ep2m6hE2g72AugUItjbuM8X8cGQnFoHk0pGfrFeT9GZhzN5SW8nRsiGKK7Aif4CrACPENkA6P/Lw6fHGQ==} + + unist-util-is@5.2.1: + resolution: {integrity: sha512-u9njyyfEh43npf1M+yGKDGVPbY/JWEemg5nH05ncKPfi+kBbKBJoTdsogMu33uhytuLlv9y0O7GH7fEdwLdLQw==} + + unist-util-is@6.0.0: + resolution: {integrity: sha512-2qCTHimwdxLfz+YzdGfkqNlH0tLi9xjTnHddPmJwtIG9MGsdbutfTc4P+haPD7l7Cjxf/WZj+we5qfVPvvxfYw==} + + unist-util-map@3.1.3: + resolution: {integrity: sha512-4/mDauoxqZ6geK97lJ6n2kDk6JK88Vh+hWMSJqyaaP/7eqN1dDhjcjnNxKNm3YU6Sw7PVJtcFMUbnmHvYzb6Vg==} + + unist-util-modify-children@4.0.0: + resolution: {integrity: sha512-+tdN5fGNddvsQdIzUF3Xx82CU9sMM+fA0dLgR9vOmT0oPT2jH+P1nd5lSqfCfXAw+93NhcXNY2qqvTUtE4cQkw==} + + unist-util-position@5.0.0: + resolution: {integrity: sha512-fucsC7HjXvkB5R3kTCO7kUjRdrS0BJt3M/FPxmHMBOm8JQi2BsHAHFsy27E0EolP8rp0NzXsJ+jNPyDWvOJZPA==} + + unist-util-remove-position@5.0.0: + resolution: {integrity: sha512-Hp5Kh3wLxv0PHj9m2yZhhLt58KzPtEYKQQ4yxfYFEO7EvHwzyDYnduhHnY1mDxoqr7VUwVuHXk9RXKIiYS1N8Q==} + + unist-util-stringify-position@4.0.0: + resolution: {integrity: sha512-0ASV06AAoKCDkS2+xw5RXJywruurpbC4JZSm7nr7MOt1ojAzvyyaO+UxZf18j8FCF6kmzCZKcAgN/yu2gm2XgQ==} + + unist-util-visit-children@3.0.0: + resolution: {integrity: sha512-RgmdTfSBOg04sdPcpTSD1jzoNBjt9a80/ZCzp5cI9n1qPzLZWF9YdvWGN2zmTumP1HWhXKdUWexjy/Wy/lJ7tA==} + + unist-util-visit-parents@5.1.3: + resolution: {integrity: sha512-x6+y8g7wWMyQhL1iZfhIPhDAs7Xwbn9nRosDXl7qoPTSCy0yNxnKc+hWokFifWQIDGi154rdUqKvbCa4+1kLhg==} + + unist-util-visit-parents@6.0.1: + resolution: {integrity: sha512-L/PqWzfTP9lzzEa6CKs0k2nARxTdZduw3zyh8d2NVBnsyvHjSX4TWse388YrrQKbvI8w20fGjGlhgT96WwKykw==} + + unist-util-visit@4.1.2: + resolution: {integrity: sha512-MSd8OUGISqHdVvfY9TPhyK2VdUrPgxkUtWSuMHF6XAAFuL4LokseigBnZtPnJMu+FbynTkFNnFlyjxpVKujMRg==} + + unist-util-visit@5.0.0: + resolution: {integrity: sha512-MR04uvD+07cwl/yhVuVWAtw+3GOR/knlL55Nd/wAdblk27GCVt3lqpTivy/tkJcZoNPzTwS1Y+KMojlLDhoTzg==} + + universalify@2.0.1: + resolution: {integrity: sha512-gptHNQghINnc/vTGIk0SOFGFNXw7JVrlRUtConJRlvaw6DuX0wO5Jeko9sWrMBhh+PsYAZ7oXAiOnf/UKogyiw==} + engines: {node: '>= 10.0.0'} + + unstorage@1.16.0: + resolution: {integrity: sha512-WQ37/H5A7LcRPWfYOrDa1Ys02xAbpPJq6q5GkO88FBXVSQzHd7+BjEwfRqyaSWCv9MbsJy058GWjjPjcJ16GGA==} + peerDependencies: + '@azure/app-configuration': ^1.8.0 + '@azure/cosmos': ^4.2.0 + '@azure/data-tables': ^13.3.0 + '@azure/identity': ^4.6.0 + '@azure/keyvault-secrets': ^4.9.0 + '@azure/storage-blob': ^12.26.0 + '@capacitor/preferences': ^6.0.3 || ^7.0.0 + '@deno/kv': '>=0.9.0' + '@netlify/blobs': ^6.5.0 || ^7.0.0 || ^8.1.0 + '@planetscale/database': ^1.19.0 + '@upstash/redis': ^1.34.3 + '@vercel/blob': '>=0.27.1' + '@vercel/kv': ^1.0.1 + aws4fetch: ^1.0.20 + db0: '>=0.2.1' + idb-keyval: ^6.2.1 + ioredis: ^5.4.2 + uploadthing: ^7.4.4 + peerDependenciesMeta: + '@azure/app-configuration': + optional: true + '@azure/cosmos': + optional: true + '@azure/data-tables': + optional: true + '@azure/identity': + optional: true + '@azure/keyvault-secrets': + optional: true + '@azure/storage-blob': + optional: true + '@capacitor/preferences': + optional: true + '@deno/kv': + optional: true + '@netlify/blobs': + optional: true + '@planetscale/database': + optional: true + '@upstash/redis': + optional: true + '@vercel/blob': + optional: true + '@vercel/kv': + optional: true + aws4fetch: + optional: true + db0: + optional: true + idb-keyval: + optional: true + ioredis: + optional: true + uploadthing: + optional: true + + update-browserslist-db@1.1.3: + resolution: {integrity: sha512-UxhIZQ+QInVdunkDAaiazvvT/+fXL5Osr0JZlJulepYu6Jd7qJtDZjlur0emRlT71EN3ScPoE7gvsuIKKNavKw==} + hasBin: true + peerDependencies: + browserslist: '>= 4.21.0' + + uri-js@4.4.1: + resolution: {integrity: sha512-7rKUyy33Q1yc98pQ1DAmLtwX109F7TIfWlW1Ydo8Wl1ii1SeHieeh0HHfPeL2fMXK6z0s8ecKs9frCuLJvndBg==} + + util-deprecate@1.0.2: + resolution: {integrity: sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw==} + + vfile-location@5.0.3: + resolution: {integrity: sha512-5yXvWDEgqeiYiBe1lbxYF7UMAIm/IcopxMHrMQDq3nvKcjPKIhZklUKL+AE7J7uApI4kwe2snsK+eI6UTj9EHg==} + + vfile-message@4.0.2: + resolution: {integrity: sha512-jRDZ1IMLttGj41KcZvlrYAaI3CfqpLpfpf+Mfig13viT6NKvRzWZ+lXz0Y5D60w6uJIBAOGq9mSHf0gktF0duw==} + + vfile@6.0.3: + resolution: {integrity: sha512-KzIbH/9tXat2u30jf+smMwFCsno4wHVdNmzFyL+T/L3UGqqk6JKfVqOFOZEpZSHADH1k40ab6NUIXZq422ov3Q==} + + vite@6.3.3: + resolution: {integrity: sha512-5nXH+QsELbFKhsEfWLkHrvgRpTdGJzqOZ+utSdmPTvwHmvU6ITTm3xx+mRusihkcI8GeC7lCDyn3kDtiki9scw==} + engines: {node: ^18.0.0 || ^20.0.0 || >=22.0.0} + hasBin: true + peerDependencies: + '@types/node': ^18.0.0 || ^20.0.0 || >=22.0.0 + jiti: '>=1.21.0' + less: '*' + lightningcss: ^1.21.0 + sass: '*' + sass-embedded: '*' + stylus: '*' + sugarss: '*' + terser: ^5.16.0 + tsx: ^4.8.1 + yaml: ^2.4.2 + peerDependenciesMeta: + '@types/node': + optional: true + jiti: + optional: true + less: + optional: true + lightningcss: + optional: true + sass: + optional: true + sass-embedded: + optional: true + stylus: + optional: true + sugarss: + optional: true + terser: + optional: true + tsx: + optional: true + yaml: + optional: true + + vitefu@1.0.6: + resolution: {integrity: sha512-+Rex1GlappUyNN6UfwbVZne/9cYC4+R2XDk9xkNXBKMw6HQagdX9PgZ8V2v1WUSK1wfBLp7qbI1+XSNIlB1xmA==} + peerDependencies: + vite: ^3.0.0 || ^4.0.0 || ^5.0.0 || ^6.0.0 + peerDependenciesMeta: + vite: + optional: true + + volar-service-css@0.0.62: + resolution: {integrity: sha512-JwNyKsH3F8PuzZYuqPf+2e+4CTU8YoyUHEHVnoXNlrLe7wy9U3biomZ56llN69Ris7TTy/+DEX41yVxQpM4qvg==} + peerDependencies: + '@volar/language-service': ~2.4.0 + peerDependenciesMeta: + '@volar/language-service': + optional: true + + volar-service-emmet@0.0.62: + resolution: {integrity: sha512-U4dxWDBWz7Pi4plpbXf4J4Z/ss6kBO3TYrACxWNsE29abu75QzVS0paxDDhI6bhqpbDFXlpsDhZ9aXVFpnfGRQ==} + peerDependencies: + '@volar/language-service': ~2.4.0 + peerDependenciesMeta: + '@volar/language-service': + optional: true + + volar-service-html@0.0.62: + resolution: {integrity: sha512-Zw01aJsZRh4GTGUjveyfEzEqpULQUdQH79KNEiKVYHZyuGtdBRYCHlrus1sueSNMxwwkuF5WnOHfvBzafs8yyQ==} + peerDependencies: + '@volar/language-service': ~2.4.0 + peerDependenciesMeta: + '@volar/language-service': + optional: true + + volar-service-prettier@0.0.62: + resolution: {integrity: sha512-h2yk1RqRTE+vkYZaI9KYuwpDfOQRrTEMvoHol0yW4GFKc75wWQRrb5n/5abDrzMPrkQbSip8JH2AXbvrRtYh4w==} + peerDependencies: + '@volar/language-service': ~2.4.0 + prettier: ^2.2 || ^3.0 + peerDependenciesMeta: + '@volar/language-service': + optional: true + prettier: + optional: true + + volar-service-typescript-twoslash-queries@0.0.62: + resolution: {integrity: sha512-KxFt4zydyJYYI0kFAcWPTh4u0Ha36TASPZkAnNY784GtgajerUqM80nX/W1d0wVhmcOFfAxkVsf/Ed+tiYU7ng==} + peerDependencies: + '@volar/language-service': ~2.4.0 + peerDependenciesMeta: + '@volar/language-service': + optional: true + + volar-service-typescript@0.0.62: + resolution: {integrity: sha512-p7MPi71q7KOsH0eAbZwPBiKPp9B2+qrdHAd6VY5oTo9BUXatsOAdakTm9Yf0DUj6uWBAaOT01BSeVOPwucMV1g==} + peerDependencies: + '@volar/language-service': ~2.4.0 + peerDependenciesMeta: + '@volar/language-service': + optional: true + + volar-service-yaml@0.0.62: + resolution: {integrity: sha512-k7gvv7sk3wa+nGll3MaSKyjwQsJjIGCHFjVkl3wjaSP2nouKyn9aokGmqjrl39mi88Oy49giog2GkZH526wjig==} + peerDependencies: + '@volar/language-service': ~2.4.0 + peerDependenciesMeta: + '@volar/language-service': + optional: true + + vscode-css-languageservice@6.3.3: + resolution: {integrity: sha512-xXa+ftMPv6JxRgzkvPwZuDCafIdwDW3kyijGcfij1a2qBVScr2qli6MfgJzYm/AMYdbHq9I/4hdpKV0Thim2EA==} + + vscode-html-languageservice@5.3.3: + resolution: {integrity: sha512-AK/jJM0VIWRrlfqkDBMZxNMnxYT5I2uoMVRoNJ5ePSplnSaT9mbYjqJlxxeLvUrOW7MEH0vVIDzU48u44QZE0w==} + + vscode-json-languageservice@4.1.8: + resolution: {integrity: sha512-0vSpg6Xd9hfV+eZAaYN63xVVMOTmJ4GgHxXnkLCh+9RsQBkWKIghzLhW2B9ebfG+LQQg8uLtsQ2aUKjTgE+QOg==} + engines: {npm: '>=7.0.0'} + + vscode-jsonrpc@6.0.0: + resolution: {integrity: sha512-wnJA4BnEjOSyFMvjZdpiOwhSq9uDoK8e/kpRJDTaMYzwlkrhG1fwDIZI94CLsLzlCK5cIbMMtFlJlfR57Lavmg==} + engines: {node: '>=8.0.0 || >=10.0.0'} + + vscode-jsonrpc@8.2.0: + resolution: {integrity: sha512-C+r0eKJUIfiDIfwJhria30+TYWPtuHJXHtI7J0YlOmKAo7ogxP20T0zxB7HZQIFhIyvoBPwWskjxrvAtfjyZfA==} + engines: {node: '>=14.0.0'} + + vscode-languageserver-protocol@3.16.0: + resolution: {integrity: sha512-sdeUoAawceQdgIfTI+sdcwkiK2KU+2cbEYA0agzM2uqaUy2UpnnGHtWTHVEtS0ES4zHU0eMFRGN+oQgDxlD66A==} + + vscode-languageserver-protocol@3.17.5: + resolution: {integrity: sha512-mb1bvRJN8SVznADSGWM9u/b07H7Ecg0I3OgXDuLdn307rl/J3A9YD6/eYOssqhecL27hK1IPZAsaqh00i/Jljg==} + + vscode-languageserver-textdocument@1.0.12: + resolution: {integrity: sha512-cxWNPesCnQCcMPeenjKKsOCKQZ/L6Tv19DTRIGuLWe32lyzWhihGVJ/rcckZXJxfdKCFvRLS3fpBIsV/ZGX4zA==} + + vscode-languageserver-types@3.16.0: + resolution: {integrity: sha512-k8luDIWJWyenLc5ToFQQMaSrqCHiLwyKPHKPQZ5zz21vM+vIVUSvsRpcbiECH4WR88K2XZqc4ScRcZ7nk/jbeA==} + + vscode-languageserver-types@3.17.5: + resolution: {integrity: sha512-Ld1VelNuX9pdF39h2Hgaeb5hEZM2Z3jUrrMgWQAu82jMtZp7p3vJT3BzToKtZI7NgQssZje5o0zryOrhQvzQAg==} + + vscode-languageserver@7.0.0: + resolution: {integrity: sha512-60HTx5ID+fLRcgdHfmz0LDZAXYEV68fzwG0JWwEPBode9NuMYTIxuYXPg4ngO8i8+Ou0lM7y6GzaYWbiDL0drw==} + hasBin: true + + vscode-languageserver@9.0.1: + resolution: {integrity: sha512-woByF3PDpkHFUreUa7Hos7+pUWdeWMXRd26+ZX2A8cFx6v/JPTtd4/uN0/jB6XQHYaOlHbio03NTHCqrgG5n7g==} + hasBin: true + + vscode-nls@5.2.0: + resolution: {integrity: sha512-RAaHx7B14ZU04EU31pT+rKz2/zSl7xMsfIZuo8pd+KZO6PXtQmpevpq3vxvWNcrGbdmhM/rr5Uw5Mz+NBfhVng==} + + vscode-uri@3.1.0: + resolution: {integrity: sha512-/BpdSx+yCQGnCvecbyXdxHDkuk55/G3xwnC0GqY4gmQ3j+A+g8kzzgB4Nk/SINjqn6+waqw3EgbVF2QKExkRxQ==} + + web-namespaces@2.0.1: + resolution: {integrity: sha512-bKr1DkiNa2krS7qxNtdrtHAmzuYGFQLiQ13TsorsdT6ULTkPLKuu5+GsFpDlg6JFjUTwX2DyhMPG2be8uPrqsQ==} + + webidl-conversions@3.0.1: + resolution: {integrity: sha512-2JAn3z8AR6rjK8Sm8orRC0h/bcl/DqL7tRPdGZ4I1CjdF+EaMLmYxBHyXuKL849eucPFhvBoxMsflfOb8kxaeQ==} + + whatwg-encoding@3.1.1: + resolution: {integrity: sha512-6qN4hJdMwfYBtE3YBTTHhoeuUrDBPZmbQaxWAqSALV/MeEnR5z1xd8UKud2RAkFoPkmB+hli1TZSnyi84xz1vQ==} + engines: {node: '>=18'} + + whatwg-mimetype@4.0.0: + resolution: {integrity: sha512-QaKxh0eNIi2mE9p2vEdzfagOKHCcj1pJ56EEHGQOVxp8r9/iszLUUV7v89x9O1p/T+NlTM5W7jW6+cz4Fq1YVg==} + engines: {node: '>=18'} + + whatwg-url@5.0.0: + resolution: {integrity: sha512-saE57nupxk6v3HY35+jzBwYa0rKSy0XR8JSxZPwgLr7ys0IBzhGviA1/TUGJLmSVqs8pb9AnvICXEuOHLprYTw==} + + which-boxed-primitive@1.1.1: + resolution: {integrity: sha512-TbX3mj8n0odCBFVlY8AxkqcHASw3L60jIuF8jFP78az3C2YhmGvqbHBpAjTRH2/xqYunrJ9g1jSyjCjpoWzIAA==} + engines: {node: '>= 0.4'} + + which-builtin-type@1.2.1: + resolution: {integrity: sha512-6iBczoX+kDQ7a3+YJBnh3T+KZRxM/iYNPXicqk66/Qfm1b93iu+yOImkg0zHbj5LNOcNv1TEADiZ0xa34B4q6Q==} + engines: {node: '>= 0.4'} + + which-collection@1.0.2: + resolution: {integrity: sha512-K4jVyjnBdgvc86Y6BkaLZEN933SwYOuBFkdmBu9ZfkcAbdVbpITnDmjvZ/aQjRXQrv5EPkTnD1s39GiiqbngCw==} + engines: {node: '>= 0.4'} + + which-pm-runs@1.1.0: + resolution: {integrity: sha512-n1brCuqClxfFfq/Rb0ICg9giSZqCS+pLtccdag6C2HyufBrh3fBOiy9nb6ggRMvWOVH5GrdJskj5iGTZNxd7SA==} + engines: {node: '>=4'} + + which-typed-array@1.1.19: + resolution: {integrity: sha512-rEvr90Bck4WZt9HHFC4DJMsjvu7x+r6bImz0/BrbWb7A2djJ8hnZMrWnHo9F8ssv0OMErasDhftrfROTyqSDrw==} + engines: {node: '>= 0.4'} + + which@2.0.2: + resolution: {integrity: sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA==} + engines: {node: '>= 8'} + hasBin: true + + widest-line@5.0.0: + resolution: {integrity: sha512-c9bZp7b5YtRj2wOe6dlj32MK+Bx/M/d+9VB2SHM1OtsUHR0aV0tdP6DWh/iMt0kWi1t5g1Iudu6hQRNd1A4PVA==} + engines: {node: '>=18'} + + wrap-ansi@7.0.0: + resolution: {integrity: sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q==} + engines: {node: '>=10'} + + wrap-ansi@8.1.0: + resolution: {integrity: sha512-si7QWI6zUMq56bESFvagtmzMdGOtoxfR+Sez11Mobfc7tm+VkUckk9bW2UeffTGVUbOksxmSw0AA2gs8g71NCQ==} + engines: {node: '>=12'} + + wrap-ansi@9.0.0: + resolution: {integrity: sha512-G8ura3S+3Z2G+mkgNRq8dqaFZAuxfsxpBB8OCTGRTCtp+l/v9nbFNmCUP1BZMts3G1142MsZfn6eeUKrr4PD1Q==} + engines: {node: '>=18'} + + wrappy@1.0.2: + resolution: {integrity: sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ==} + + xxhash-wasm@1.1.0: + resolution: {integrity: sha512-147y/6YNh+tlp6nd/2pWq38i9h6mz/EuQ6njIrmW8D1BS5nCqs0P6DG+m6zTGnNz5I+uhZ0SHxBs9BsPrwcKDA==} + + y18n@5.0.8: + resolution: {integrity: sha512-0pfFzegeDWJHJIAmTLRP2DwHjdF5s7jo9tuztdQxAhINCdvS+3nGINqPd00AphqJR/0LhANUS6/+7SCb98YOfA==} + engines: {node: '>=10'} + + yallist@3.1.1: + resolution: {integrity: sha512-a4UGQaWPH59mOXUYnAG2ewncQS4i4F43Tv3JoAM+s2VDAmS9NsK8GpDMLrCHPksFT7h3K6TOoUNn2pb7RoXx4g==} + + yallist@4.0.0: + resolution: {integrity: sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A==} + + yallist@5.0.0: + resolution: {integrity: sha512-YgvUTfwqyc7UXVMrB+SImsVYSmTS8X/tSrtdNZMImM+n7+QTriRXyXim0mBrTXNeqzVF0KWGgHPeiyViFFrNDw==} + engines: {node: '>=18'} + + yaml-language-server@1.15.0: + resolution: {integrity: sha512-N47AqBDCMQmh6mBLmI6oqxryHRzi33aPFPsJhYy3VTUGCdLHYjGh4FZzpUjRlphaADBBkDmnkM/++KNIOHi5Rw==} + hasBin: true + + yaml@1.10.2: + resolution: {integrity: sha512-r3vXyErRCYJ7wg28yvBY5VSoAF8ZvlcW9/BwUzEtUsjvX/DKs24dIkuwjtuprwJJHsbyUbLApepYTR1BN4uHrg==} + engines: {node: '>= 6'} + + yaml@2.2.2: + resolution: {integrity: sha512-CBKFWExMn46Foo4cldiChEzn7S7SRV+wqiluAb6xmueD/fGyRHIhX8m14vVGgeFWjN540nKCNVj6P21eQjgTuA==} + engines: {node: '>= 14'} + + yaml@2.7.0: + resolution: {integrity: sha512-+hSoy/QHluxmC9kCIJyL/uyFmLmc+e5CFR5Wa+bpIhIj85LVb9ZH2nVnqrHoSvKogwODv0ClqZkmiSSaIH5LTA==} + engines: {node: '>= 14'} + hasBin: true + + yargs-parser@21.1.1: + resolution: {integrity: sha512-tVpsJW7DdjecAiFpbIB1e3qxIQsE6NoPc5/eTdrbbIC4h0LVsWhnoa3g+m2HclBIujHzsxZ4VJVA+GUuc2/LBw==} + engines: {node: '>=12'} + + yargs@17.7.2: + resolution: {integrity: sha512-7dSzzRQ++CKnNI/krKnYRV7JKKPUXMEh61soaHKg9mrWEhzFWhFnxPxGl+69cD1Ou63C13NUPCnmIcrvqCuM6w==} + engines: {node: '>=12'} + + yauzl@2.10.0: + resolution: {integrity: sha512-p4a9I6X6nu6IhoGmBqAcbJy1mlC4j27vEPZX9F4L4/vZT3Lyq1VkFHw/V/PUcB9Buo+DG3iHkT0x3Qya58zc3g==} + + yocto-queue@1.2.1: + resolution: {integrity: sha512-AyeEbWOu/TAXdxlV9wmGcR0+yh2j3vYPGOECcIj2S7MkrLyC7ne+oye2BKTItt0ii2PHk4cDy+95+LshzbXnGg==} + engines: {node: '>=12.20'} + + yocto-spinner@0.2.2: + resolution: {integrity: sha512-21rPcM3e4vCpOXThiFRByX8amU5By1R0wNS8Oex+DP3YgC8xdU0vEJ/K8cbPLiIJVosSSysgcFof6s6MSD5/Vw==} + engines: {node: '>=18.19'} + + yoctocolors@2.1.1: + resolution: {integrity: sha512-GQHQqAopRhwU8Kt1DDM8NjibDXHC8eoh1erhGAJPEyveY9qqVeXvVikNKrDz69sHowPMorbPUrH/mx8c50eiBQ==} + engines: {node: '>=18'} + + zimmerframe@1.1.2: + resolution: {integrity: sha512-rAbqEGa8ovJy4pyBxZM70hg4pE6gDgaQ0Sl9M3enG3I0d6H4XSAM3GeNGLKnsBpuijUow064sf7ww1nutC5/3w==} + + zod-to-json-schema@3.24.5: + resolution: {integrity: sha512-/AuWwMP+YqiPbsJx5D6TfgRTc4kTLjsh5SOcd4bLsfUg2RcEXrFMJl1DGgdHy2aCfsIA/cr/1JM0xcB2GZji8g==} + peerDependencies: + zod: ^3.24.1 + + zod-to-ts@1.2.0: + resolution: {integrity: sha512-x30XE43V+InwGpvTySRNz9kB7qFU8DlyEy7BsSTCHPH1R0QasMmHWZDCzYm6bVXtj/9NNJAZF3jW8rzFvH5OFA==} + peerDependencies: + typescript: ^4.9.4 || ^5.0.2 + zod: ^3 + + zod@3.24.3: + resolution: {integrity: sha512-HhY1oqzWCQWuUqvBFnsyrtZRhyPeR7SUGv+C4+MsisMuVfSPx8HpwWqH8tRahSlt6M3PiFAcoeFhZAqIXTxoSg==} + + zwitch@2.0.4: + resolution: {integrity: sha512-bXE4cR/kVZhKZX/RjPEflHaKVhUVl85noU3v6b8apfQEc1x4A+zBxjZ4lN8LqGd6WZ3dl98pY4o717VFmoPp+A==} + +snapshots: + + '@adobe/css-tools@4.3.3': {} + + '@alloc/quick-lru@5.2.0': {} + + '@ampproject/remapping@2.3.0': + dependencies: + '@jridgewell/gen-mapping': 0.3.8 + '@jridgewell/trace-mapping': 0.3.25 + + '@antfu/install-pkg@1.0.0': + dependencies: + package-manager-detector: 0.2.11 + tinyexec: 0.3.2 + + '@antfu/utils@8.1.1': {} + + '@astrojs/check@0.9.4(typescript@5.8.3)': + dependencies: + '@astrojs/language-server': 2.15.4(typescript@5.8.3) + chokidar: 4.0.3 + kleur: 4.1.5 + typescript: 5.8.3 + yargs: 17.7.2 + transitivePeerDependencies: + - prettier + - prettier-plugin-astro + + '@astrojs/compiler@2.11.0': {} + + '@astrojs/internal-helpers@0.6.1': {} + + '@astrojs/internal-helpers@0.7.2': {} + + '@astrojs/language-server@2.15.4(typescript@5.8.3)': + dependencies: + '@astrojs/compiler': 2.11.0 + '@astrojs/yaml2ts': 0.2.2 + '@jridgewell/sourcemap-codec': 1.5.0 + '@volar/kit': 2.4.12(typescript@5.8.3) + '@volar/language-core': 2.4.12 + '@volar/language-server': 2.4.12 + '@volar/language-service': 2.4.12 + fast-glob: 3.3.3 + muggle-string: 0.4.1 + volar-service-css: 0.0.62(@volar/language-service@2.4.12) + volar-service-emmet: 0.0.62(@volar/language-service@2.4.12) + volar-service-html: 0.0.62(@volar/language-service@2.4.12) + volar-service-prettier: 0.0.62(@volar/language-service@2.4.12) + volar-service-typescript: 0.0.62(@volar/language-service@2.4.12) + volar-service-typescript-twoslash-queries: 0.0.62(@volar/language-service@2.4.12) + volar-service-yaml: 0.0.62(@volar/language-service@2.4.12) + vscode-html-languageservice: 5.3.3 + vscode-uri: 3.1.0 + transitivePeerDependencies: + - typescript + + '@astrojs/markdown-remark@6.3.1': + dependencies: + '@astrojs/internal-helpers': 0.6.1 + '@astrojs/prism': 3.2.0 + github-slugger: 2.0.0 + hast-util-from-html: 2.0.3 + hast-util-to-text: 4.0.2 + import-meta-resolve: 4.1.0 + js-yaml: 4.1.0 + mdast-util-definitions: 6.0.0 + rehype-raw: 7.0.0 + rehype-stringify: 10.0.1 + remark-gfm: 4.0.1 + remark-parse: 11.0.0 + remark-rehype: 11.1.2 + remark-smartypants: 3.0.2 + shiki: 3.3.0 + smol-toml: 1.3.4 + unified: 11.0.5 + unist-util-remove-position: 5.0.0 + unist-util-visit: 5.0.0 + unist-util-visit-parents: 6.0.1 + vfile: 6.0.3 + transitivePeerDependencies: + - supports-color + + '@astrojs/prism@3.2.0': + dependencies: + prismjs: 1.30.0 + + '@astrojs/rss@4.0.11': + dependencies: + fast-xml-parser: 4.5.3 + kleur: 4.1.5 + + '@astrojs/sitemap@3.3.1': + dependencies: + sitemap: 8.0.0 + stream-replace-string: 2.0.0 + zod: 3.24.3 + + '@astrojs/svelte@7.0.12(@types/node@22.14.1)(astro@5.7.9(patch_hash=ysrllw3gj6hylybllnynya5oma)(@types/node@22.14.1)(jiti@1.21.7)(lightningcss@1.29.3)(rollup@2.79.2)(sass@1.80.4)(stylus@0.64.0)(terser@5.39.0)(typescript@5.8.3)(yaml@2.7.0))(jiti@1.21.7)(lightningcss@1.29.3)(sass@1.80.4)(stylus@0.64.0)(svelte@5.28.2)(terser@5.39.0)(typescript@5.8.3)(yaml@2.7.0)': + dependencies: + '@sveltejs/vite-plugin-svelte': 5.0.3(svelte@5.28.2)(vite@6.3.3(@types/node@22.14.1)(jiti@1.21.7)(lightningcss@1.29.3)(sass@1.80.4)(stylus@0.64.0)(terser@5.39.0)(yaml@2.7.0)) + astro: 5.7.9(patch_hash=ysrllw3gj6hylybllnynya5oma)(@types/node@22.14.1)(jiti@1.21.7)(lightningcss@1.29.3)(rollup@2.79.2)(sass@1.80.4)(stylus@0.64.0)(terser@5.39.0)(typescript@5.8.3)(yaml@2.7.0) + svelte: 5.28.2 + svelte2tsx: 0.7.36(svelte@5.28.2)(typescript@5.8.3) + typescript: 5.8.3 + vite: 6.3.3(@types/node@22.14.1)(jiti@1.21.7)(lightningcss@1.29.3)(sass@1.80.4)(stylus@0.64.0)(terser@5.39.0)(yaml@2.7.0) + transitivePeerDependencies: + - '@types/node' + - jiti + - less + - lightningcss + - sass + - sass-embedded + - stylus + - sugarss + - supports-color + - terser + - tsx + - yaml + + '@astrojs/tailwind@6.0.2(astro@5.7.9(patch_hash=ysrllw3gj6hylybllnynya5oma)(@types/node@22.14.1)(jiti@1.21.7)(lightningcss@1.29.3)(rollup@2.79.2)(sass@1.80.4)(stylus@0.64.0)(terser@5.39.0)(typescript@5.8.3)(yaml@2.7.0))(tailwindcss@3.4.17)': + dependencies: + astro: 5.7.9(patch_hash=ysrllw3gj6hylybllnynya5oma)(@types/node@22.14.1)(jiti@1.21.7)(lightningcss@1.29.3)(rollup@2.79.2)(sass@1.80.4)(stylus@0.64.0)(terser@5.39.0)(typescript@5.8.3)(yaml@2.7.0) + autoprefixer: 10.4.21(postcss@8.5.3) + postcss: 8.5.3 + postcss-load-config: 4.0.2(postcss@8.5.3) + tailwindcss: 3.4.17 + transitivePeerDependencies: + - ts-node + + '@astrojs/telemetry@3.2.1': + dependencies: + ci-info: 4.2.0 + debug: 4.4.0 + dlv: 1.1.3 + dset: 3.1.4 + is-docker: 3.0.0 + is-wsl: 3.1.0 + which-pm-runs: 1.1.0 + transitivePeerDependencies: + - supports-color + + '@astrojs/ts-plugin@1.10.4': + dependencies: + '@astrojs/compiler': 2.11.0 + '@astrojs/yaml2ts': 0.2.2 + '@jridgewell/sourcemap-codec': 1.5.0 + '@volar/language-core': 2.4.12 + '@volar/typescript': 2.4.12 + semver: 7.7.1 + vscode-languageserver-textdocument: 1.0.12 + + '@astrojs/vercel@8.2.7(astro@5.7.9(patch_hash=ysrllw3gj6hylybllnynya5oma)(@types/node@22.14.1)(jiti@1.21.7)(lightningcss@1.29.3)(rollup@2.79.2)(sass@1.80.4)(stylus@0.64.0)(terser@5.39.0)(typescript@5.8.3)(yaml@2.7.0))(rollup@2.79.2)(svelte@5.28.2)': + dependencies: + '@astrojs/internal-helpers': 0.7.2 + '@vercel/analytics': 1.5.0(svelte@5.28.2) + '@vercel/functions': 2.2.13 + '@vercel/nft': 0.29.4(rollup@2.79.2) + '@vercel/routing-utils': 5.1.1 + astro: 5.7.9(patch_hash=ysrllw3gj6hylybllnynya5oma)(@types/node@22.14.1)(jiti@1.21.7)(lightningcss@1.29.3)(rollup@2.79.2)(sass@1.80.4)(stylus@0.64.0)(terser@5.39.0)(typescript@5.8.3)(yaml@2.7.0) + esbuild: 0.25.3 + tinyglobby: 0.2.13 + transitivePeerDependencies: + - '@aws-sdk/credential-provider-web-identity' + - '@remix-run/react' + - '@sveltejs/kit' + - encoding + - next + - react + - rollup + - supports-color + - svelte + - vue + - vue-router + + '@astrojs/yaml2ts@0.2.2': + dependencies: + yaml: 2.7.0 + + '@babel/code-frame@7.26.2': + dependencies: + '@babel/helper-validator-identifier': 7.25.9 + js-tokens: 4.0.0 + picocolors: 1.1.1 + + '@babel/compat-data@7.26.8': {} + + '@babel/core@7.26.10': + dependencies: + '@ampproject/remapping': 2.3.0 + '@babel/code-frame': 7.26.2 + '@babel/generator': 7.27.0 + '@babel/helper-compilation-targets': 7.27.0 + '@babel/helper-module-transforms': 7.26.0(@babel/core@7.26.10) + '@babel/helpers': 7.27.0 + '@babel/parser': 7.27.0 + '@babel/template': 7.27.0 + '@babel/traverse': 7.27.0 + '@babel/types': 7.27.0 + convert-source-map: 2.0.0 + debug: 4.4.0 + gensync: 1.0.0-beta.2 + json5: 2.2.3 + semver: 6.3.1 + transitivePeerDependencies: + - supports-color + + '@babel/generator@7.27.0': + dependencies: + '@babel/parser': 7.27.0 + '@babel/types': 7.27.0 + '@jridgewell/gen-mapping': 0.3.8 + '@jridgewell/trace-mapping': 0.3.25 + jsesc: 3.1.0 + + '@babel/helper-annotate-as-pure@7.25.9': + dependencies: + '@babel/types': 7.27.0 + + '@babel/helper-compilation-targets@7.27.0': + dependencies: + '@babel/compat-data': 7.26.8 + '@babel/helper-validator-option': 7.25.9 + browserslist: 4.24.4 + lru-cache: 5.1.1 + semver: 6.3.1 + + '@babel/helper-create-class-features-plugin@7.27.0(@babel/core@7.26.10)': + dependencies: + '@babel/core': 7.26.10 + '@babel/helper-annotate-as-pure': 7.25.9 + '@babel/helper-member-expression-to-functions': 7.25.9 + '@babel/helper-optimise-call-expression': 7.25.9 + '@babel/helper-replace-supers': 7.26.5(@babel/core@7.26.10) + '@babel/helper-skip-transparent-expression-wrappers': 7.25.9 + '@babel/traverse': 7.27.0 + semver: 6.3.1 + transitivePeerDependencies: + - supports-color + + '@babel/helper-create-regexp-features-plugin@7.27.0(@babel/core@7.26.10)': + dependencies: + '@babel/core': 7.26.10 + '@babel/helper-annotate-as-pure': 7.25.9 + regexpu-core: 6.2.0 + semver: 6.3.1 + + '@babel/helper-define-polyfill-provider@0.6.4(@babel/core@7.26.10)': + dependencies: + '@babel/core': 7.26.10 + '@babel/helper-compilation-targets': 7.27.0 + '@babel/helper-plugin-utils': 7.26.5 + debug: 4.4.0 + lodash.debounce: 4.0.8 + resolve: 1.22.10 + transitivePeerDependencies: + - supports-color + + '@babel/helper-member-expression-to-functions@7.25.9': + dependencies: + '@babel/traverse': 7.27.0 + '@babel/types': 7.27.0 + transitivePeerDependencies: + - supports-color + + '@babel/helper-module-imports@7.25.9': + dependencies: + '@babel/traverse': 7.27.0 + '@babel/types': 7.27.0 + transitivePeerDependencies: + - supports-color + + '@babel/helper-module-transforms@7.26.0(@babel/core@7.26.10)': + dependencies: + '@babel/core': 7.26.10 + '@babel/helper-module-imports': 7.25.9 + '@babel/helper-validator-identifier': 7.25.9 + '@babel/traverse': 7.27.0 + transitivePeerDependencies: + - supports-color + + '@babel/helper-optimise-call-expression@7.25.9': + dependencies: + '@babel/types': 7.27.0 + + '@babel/helper-plugin-utils@7.26.5': {} + + '@babel/helper-remap-async-to-generator@7.25.9(@babel/core@7.26.10)': + dependencies: + '@babel/core': 7.26.10 + '@babel/helper-annotate-as-pure': 7.25.9 + '@babel/helper-wrap-function': 7.25.9 + '@babel/traverse': 7.27.0 + transitivePeerDependencies: + - supports-color + + '@babel/helper-replace-supers@7.26.5(@babel/core@7.26.10)': + dependencies: + '@babel/core': 7.26.10 + '@babel/helper-member-expression-to-functions': 7.25.9 + '@babel/helper-optimise-call-expression': 7.25.9 + '@babel/traverse': 7.27.0 + transitivePeerDependencies: + - supports-color + + '@babel/helper-skip-transparent-expression-wrappers@7.25.9': + dependencies: + '@babel/traverse': 7.27.0 + '@babel/types': 7.27.0 + transitivePeerDependencies: + - supports-color + + '@babel/helper-string-parser@7.25.9': {} + + '@babel/helper-string-parser@7.27.1': + optional: true + + '@babel/helper-validator-identifier@7.25.9': {} + + '@babel/helper-validator-identifier@7.27.1': + optional: true + + '@babel/helper-validator-option@7.25.9': {} + + '@babel/helper-wrap-function@7.25.9': + dependencies: + '@babel/template': 7.27.0 + '@babel/traverse': 7.27.0 + '@babel/types': 7.27.0 + transitivePeerDependencies: + - supports-color + + '@babel/helpers@7.27.0': + dependencies: + '@babel/template': 7.27.0 + '@babel/types': 7.27.0 + + '@babel/parser@7.27.0': + dependencies: + '@babel/types': 7.27.0 + + '@babel/parser@7.28.0': + dependencies: + '@babel/types': 7.28.2 + optional: true + + '@babel/plugin-bugfix-firefox-class-in-computed-class-key@7.25.9(@babel/core@7.26.10)': + dependencies: + '@babel/core': 7.26.10 + '@babel/helper-plugin-utils': 7.26.5 + '@babel/traverse': 7.27.0 + transitivePeerDependencies: + - supports-color + + '@babel/plugin-bugfix-safari-class-field-initializer-scope@7.25.9(@babel/core@7.26.10)': + dependencies: + '@babel/core': 7.26.10 + '@babel/helper-plugin-utils': 7.26.5 + + '@babel/plugin-bugfix-safari-id-destructuring-collision-in-function-expression@7.25.9(@babel/core@7.26.10)': + dependencies: + '@babel/core': 7.26.10 + '@babel/helper-plugin-utils': 7.26.5 + + '@babel/plugin-bugfix-v8-spread-parameters-in-optional-chaining@7.25.9(@babel/core@7.26.10)': + dependencies: + '@babel/core': 7.26.10 + '@babel/helper-plugin-utils': 7.26.5 + '@babel/helper-skip-transparent-expression-wrappers': 7.25.9 + '@babel/plugin-transform-optional-chaining': 7.25.9(@babel/core@7.26.10) + transitivePeerDependencies: + - supports-color + + '@babel/plugin-bugfix-v8-static-class-fields-redefine-readonly@7.25.9(@babel/core@7.26.10)': + dependencies: + '@babel/core': 7.26.10 + '@babel/helper-plugin-utils': 7.26.5 + '@babel/traverse': 7.27.0 + transitivePeerDependencies: + - supports-color + + '@babel/plugin-proposal-class-properties@7.12.1(@babel/core@7.26.10)': + dependencies: + '@babel/core': 7.26.10 + '@babel/helper-create-class-features-plugin': 7.27.0(@babel/core@7.26.10) + '@babel/helper-plugin-utils': 7.26.5 + transitivePeerDependencies: + - supports-color + + '@babel/plugin-proposal-private-property-in-object@7.21.0-placeholder-for-preset-env.2(@babel/core@7.26.10)': + dependencies: + '@babel/core': 7.26.10 + + '@babel/plugin-syntax-flow@7.26.0(@babel/core@7.26.10)': + dependencies: + '@babel/core': 7.26.10 + '@babel/helper-plugin-utils': 7.26.5 + + '@babel/plugin-syntax-import-assertions@7.26.0(@babel/core@7.26.10)': + dependencies: + '@babel/core': 7.26.10 + '@babel/helper-plugin-utils': 7.26.5 + + '@babel/plugin-syntax-import-attributes@7.26.0(@babel/core@7.26.10)': + dependencies: + '@babel/core': 7.26.10 + '@babel/helper-plugin-utils': 7.26.5 + + '@babel/plugin-syntax-import-meta@7.10.4(@babel/core@7.26.10)': + dependencies: + '@babel/core': 7.26.10 + '@babel/helper-plugin-utils': 7.26.5 + + '@babel/plugin-syntax-jsx@7.25.9(@babel/core@7.26.10)': + dependencies: + '@babel/core': 7.26.10 + '@babel/helper-plugin-utils': 7.26.5 + + '@babel/plugin-syntax-unicode-sets-regex@7.18.6(@babel/core@7.26.10)': + dependencies: + '@babel/core': 7.26.10 + '@babel/helper-create-regexp-features-plugin': 7.27.0(@babel/core@7.26.10) + '@babel/helper-plugin-utils': 7.26.5 + + '@babel/plugin-transform-arrow-functions@7.25.9(@babel/core@7.26.10)': + dependencies: + '@babel/core': 7.26.10 + '@babel/helper-plugin-utils': 7.26.5 + + '@babel/plugin-transform-async-generator-functions@7.26.8(@babel/core@7.26.10)': + dependencies: + '@babel/core': 7.26.10 + '@babel/helper-plugin-utils': 7.26.5 + '@babel/helper-remap-async-to-generator': 7.25.9(@babel/core@7.26.10) + '@babel/traverse': 7.27.0 + transitivePeerDependencies: + - supports-color + + '@babel/plugin-transform-async-to-generator@7.25.9(@babel/core@7.26.10)': + dependencies: + '@babel/core': 7.26.10 + '@babel/helper-module-imports': 7.25.9 + '@babel/helper-plugin-utils': 7.26.5 + '@babel/helper-remap-async-to-generator': 7.25.9(@babel/core@7.26.10) + transitivePeerDependencies: + - supports-color + + '@babel/plugin-transform-block-scoped-functions@7.26.5(@babel/core@7.26.10)': + dependencies: + '@babel/core': 7.26.10 + '@babel/helper-plugin-utils': 7.26.5 + + '@babel/plugin-transform-block-scoping@7.27.0(@babel/core@7.26.10)': + dependencies: + '@babel/core': 7.26.10 + '@babel/helper-plugin-utils': 7.26.5 + + '@babel/plugin-transform-class-properties@7.25.9(@babel/core@7.26.10)': + dependencies: + '@babel/core': 7.26.10 + '@babel/helper-create-class-features-plugin': 7.27.0(@babel/core@7.26.10) + '@babel/helper-plugin-utils': 7.26.5 + transitivePeerDependencies: + - supports-color + + '@babel/plugin-transform-class-static-block@7.26.0(@babel/core@7.26.10)': + dependencies: + '@babel/core': 7.26.10 + '@babel/helper-create-class-features-plugin': 7.27.0(@babel/core@7.26.10) + '@babel/helper-plugin-utils': 7.26.5 + transitivePeerDependencies: + - supports-color + + '@babel/plugin-transform-classes@7.25.9(@babel/core@7.26.10)': + dependencies: + '@babel/core': 7.26.10 + '@babel/helper-annotate-as-pure': 7.25.9 + '@babel/helper-compilation-targets': 7.27.0 + '@babel/helper-plugin-utils': 7.26.5 + '@babel/helper-replace-supers': 7.26.5(@babel/core@7.26.10) + '@babel/traverse': 7.27.0 + globals: 11.12.0 + transitivePeerDependencies: + - supports-color + + '@babel/plugin-transform-computed-properties@7.25.9(@babel/core@7.26.10)': + dependencies: + '@babel/core': 7.26.10 + '@babel/helper-plugin-utils': 7.26.5 + '@babel/template': 7.27.0 + + '@babel/plugin-transform-destructuring@7.25.9(@babel/core@7.26.10)': + dependencies: + '@babel/core': 7.26.10 + '@babel/helper-plugin-utils': 7.26.5 + + '@babel/plugin-transform-dotall-regex@7.25.9(@babel/core@7.26.10)': + dependencies: + '@babel/core': 7.26.10 + '@babel/helper-create-regexp-features-plugin': 7.27.0(@babel/core@7.26.10) + '@babel/helper-plugin-utils': 7.26.5 + + '@babel/plugin-transform-duplicate-keys@7.25.9(@babel/core@7.26.10)': + dependencies: + '@babel/core': 7.26.10 + '@babel/helper-plugin-utils': 7.26.5 + + '@babel/plugin-transform-duplicate-named-capturing-groups-regex@7.25.9(@babel/core@7.26.10)': + dependencies: + '@babel/core': 7.26.10 + '@babel/helper-create-regexp-features-plugin': 7.27.0(@babel/core@7.26.10) + '@babel/helper-plugin-utils': 7.26.5 + + '@babel/plugin-transform-dynamic-import@7.25.9(@babel/core@7.26.10)': + dependencies: + '@babel/core': 7.26.10 + '@babel/helper-plugin-utils': 7.26.5 + + '@babel/plugin-transform-exponentiation-operator@7.26.3(@babel/core@7.26.10)': + dependencies: + '@babel/core': 7.26.10 + '@babel/helper-plugin-utils': 7.26.5 + + '@babel/plugin-transform-export-namespace-from@7.25.9(@babel/core@7.26.10)': + dependencies: + '@babel/core': 7.26.10 + '@babel/helper-plugin-utils': 7.26.5 + + '@babel/plugin-transform-flow-strip-types@7.26.5(@babel/core@7.26.10)': + dependencies: + '@babel/core': 7.26.10 + '@babel/helper-plugin-utils': 7.26.5 + '@babel/plugin-syntax-flow': 7.26.0(@babel/core@7.26.10) + + '@babel/plugin-transform-for-of@7.26.9(@babel/core@7.26.10)': + dependencies: + '@babel/core': 7.26.10 + '@babel/helper-plugin-utils': 7.26.5 + '@babel/helper-skip-transparent-expression-wrappers': 7.25.9 + transitivePeerDependencies: + - supports-color + + '@babel/plugin-transform-function-name@7.25.9(@babel/core@7.26.10)': + dependencies: + '@babel/core': 7.26.10 + '@babel/helper-compilation-targets': 7.27.0 + '@babel/helper-plugin-utils': 7.26.5 + '@babel/traverse': 7.27.0 + transitivePeerDependencies: + - supports-color + + '@babel/plugin-transform-json-strings@7.25.9(@babel/core@7.26.10)': + dependencies: + '@babel/core': 7.26.10 + '@babel/helper-plugin-utils': 7.26.5 + + '@babel/plugin-transform-literals@7.25.9(@babel/core@7.26.10)': + dependencies: + '@babel/core': 7.26.10 + '@babel/helper-plugin-utils': 7.26.5 + + '@babel/plugin-transform-logical-assignment-operators@7.25.9(@babel/core@7.26.10)': + dependencies: + '@babel/core': 7.26.10 + '@babel/helper-plugin-utils': 7.26.5 + + '@babel/plugin-transform-member-expression-literals@7.25.9(@babel/core@7.26.10)': + dependencies: + '@babel/core': 7.26.10 + '@babel/helper-plugin-utils': 7.26.5 + + '@babel/plugin-transform-modules-amd@7.25.9(@babel/core@7.26.10)': + dependencies: + '@babel/core': 7.26.10 + '@babel/helper-module-transforms': 7.26.0(@babel/core@7.26.10) + '@babel/helper-plugin-utils': 7.26.5 + transitivePeerDependencies: + - supports-color + + '@babel/plugin-transform-modules-commonjs@7.26.3(@babel/core@7.26.10)': + dependencies: + '@babel/core': 7.26.10 + '@babel/helper-module-transforms': 7.26.0(@babel/core@7.26.10) + '@babel/helper-plugin-utils': 7.26.5 + transitivePeerDependencies: + - supports-color + + '@babel/plugin-transform-modules-systemjs@7.25.9(@babel/core@7.26.10)': + dependencies: + '@babel/core': 7.26.10 + '@babel/helper-module-transforms': 7.26.0(@babel/core@7.26.10) + '@babel/helper-plugin-utils': 7.26.5 + '@babel/helper-validator-identifier': 7.25.9 + '@babel/traverse': 7.27.0 + transitivePeerDependencies: + - supports-color + + '@babel/plugin-transform-modules-umd@7.25.9(@babel/core@7.26.10)': + dependencies: + '@babel/core': 7.26.10 + '@babel/helper-module-transforms': 7.26.0(@babel/core@7.26.10) + '@babel/helper-plugin-utils': 7.26.5 + transitivePeerDependencies: + - supports-color + + '@babel/plugin-transform-named-capturing-groups-regex@7.25.9(@babel/core@7.26.10)': + dependencies: + '@babel/core': 7.26.10 + '@babel/helper-create-regexp-features-plugin': 7.27.0(@babel/core@7.26.10) + '@babel/helper-plugin-utils': 7.26.5 + + '@babel/plugin-transform-new-target@7.25.9(@babel/core@7.26.10)': + dependencies: + '@babel/core': 7.26.10 + '@babel/helper-plugin-utils': 7.26.5 + + '@babel/plugin-transform-nullish-coalescing-operator@7.26.6(@babel/core@7.26.10)': + dependencies: + '@babel/core': 7.26.10 + '@babel/helper-plugin-utils': 7.26.5 + + '@babel/plugin-transform-numeric-separator@7.25.9(@babel/core@7.26.10)': + dependencies: + '@babel/core': 7.26.10 + '@babel/helper-plugin-utils': 7.26.5 + + '@babel/plugin-transform-object-rest-spread@7.25.9(@babel/core@7.26.10)': + dependencies: + '@babel/core': 7.26.10 + '@babel/helper-compilation-targets': 7.27.0 + '@babel/helper-plugin-utils': 7.26.5 + '@babel/plugin-transform-parameters': 7.25.9(@babel/core@7.26.10) + + '@babel/plugin-transform-object-super@7.25.9(@babel/core@7.26.10)': + dependencies: + '@babel/core': 7.26.10 + '@babel/helper-plugin-utils': 7.26.5 + '@babel/helper-replace-supers': 7.26.5(@babel/core@7.26.10) + transitivePeerDependencies: + - supports-color + + '@babel/plugin-transform-optional-catch-binding@7.25.9(@babel/core@7.26.10)': + dependencies: + '@babel/core': 7.26.10 + '@babel/helper-plugin-utils': 7.26.5 + + '@babel/plugin-transform-optional-chaining@7.25.9(@babel/core@7.26.10)': + dependencies: + '@babel/core': 7.26.10 + '@babel/helper-plugin-utils': 7.26.5 + '@babel/helper-skip-transparent-expression-wrappers': 7.25.9 + transitivePeerDependencies: + - supports-color + + '@babel/plugin-transform-parameters@7.25.9(@babel/core@7.26.10)': + dependencies: + '@babel/core': 7.26.10 + '@babel/helper-plugin-utils': 7.26.5 + + '@babel/plugin-transform-private-methods@7.25.9(@babel/core@7.26.10)': + dependencies: + '@babel/core': 7.26.10 + '@babel/helper-create-class-features-plugin': 7.27.0(@babel/core@7.26.10) + '@babel/helper-plugin-utils': 7.26.5 + transitivePeerDependencies: + - supports-color + + '@babel/plugin-transform-private-property-in-object@7.25.9(@babel/core@7.26.10)': + dependencies: + '@babel/core': 7.26.10 + '@babel/helper-annotate-as-pure': 7.25.9 + '@babel/helper-create-class-features-plugin': 7.27.0(@babel/core@7.26.10) + '@babel/helper-plugin-utils': 7.26.5 + transitivePeerDependencies: + - supports-color + + '@babel/plugin-transform-property-literals@7.25.9(@babel/core@7.26.10)': + dependencies: + '@babel/core': 7.26.10 + '@babel/helper-plugin-utils': 7.26.5 + + '@babel/plugin-transform-react-display-name@7.25.9(@babel/core@7.26.10)': + dependencies: + '@babel/core': 7.26.10 + '@babel/helper-plugin-utils': 7.26.5 + + '@babel/plugin-transform-react-jsx-development@7.25.9(@babel/core@7.26.10)': + dependencies: + '@babel/core': 7.26.10 + '@babel/plugin-transform-react-jsx': 7.25.9(@babel/core@7.26.10) + transitivePeerDependencies: + - supports-color + + '@babel/plugin-transform-react-jsx@7.25.9(@babel/core@7.26.10)': + dependencies: + '@babel/core': 7.26.10 + '@babel/helper-annotate-as-pure': 7.25.9 + '@babel/helper-module-imports': 7.25.9 + '@babel/helper-plugin-utils': 7.26.5 + '@babel/plugin-syntax-jsx': 7.25.9(@babel/core@7.26.10) + '@babel/types': 7.27.0 + transitivePeerDependencies: + - supports-color + + '@babel/plugin-transform-react-pure-annotations@7.25.9(@babel/core@7.26.10)': + dependencies: + '@babel/core': 7.26.10 + '@babel/helper-annotate-as-pure': 7.25.9 + '@babel/helper-plugin-utils': 7.26.5 + + '@babel/plugin-transform-regenerator@7.27.0(@babel/core@7.26.10)': + dependencies: + '@babel/core': 7.26.10 + '@babel/helper-plugin-utils': 7.26.5 + regenerator-transform: 0.15.2 + + '@babel/plugin-transform-regexp-modifiers@7.26.0(@babel/core@7.26.10)': + dependencies: + '@babel/core': 7.26.10 + '@babel/helper-create-regexp-features-plugin': 7.27.0(@babel/core@7.26.10) + '@babel/helper-plugin-utils': 7.26.5 + + '@babel/plugin-transform-reserved-words@7.25.9(@babel/core@7.26.10)': + dependencies: + '@babel/core': 7.26.10 + '@babel/helper-plugin-utils': 7.26.5 + + '@babel/plugin-transform-shorthand-properties@7.25.9(@babel/core@7.26.10)': + dependencies: + '@babel/core': 7.26.10 + '@babel/helper-plugin-utils': 7.26.5 + + '@babel/plugin-transform-spread@7.25.9(@babel/core@7.26.10)': + dependencies: + '@babel/core': 7.26.10 + '@babel/helper-plugin-utils': 7.26.5 + '@babel/helper-skip-transparent-expression-wrappers': 7.25.9 + transitivePeerDependencies: + - supports-color + + '@babel/plugin-transform-sticky-regex@7.25.9(@babel/core@7.26.10)': + dependencies: + '@babel/core': 7.26.10 + '@babel/helper-plugin-utils': 7.26.5 + + '@babel/plugin-transform-template-literals@7.26.8(@babel/core@7.26.10)': + dependencies: + '@babel/core': 7.26.10 + '@babel/helper-plugin-utils': 7.26.5 + + '@babel/plugin-transform-typeof-symbol@7.27.0(@babel/core@7.26.10)': + dependencies: + '@babel/core': 7.26.10 + '@babel/helper-plugin-utils': 7.26.5 + + '@babel/plugin-transform-unicode-escapes@7.25.9(@babel/core@7.26.10)': + dependencies: + '@babel/core': 7.26.10 + '@babel/helper-plugin-utils': 7.26.5 + + '@babel/plugin-transform-unicode-property-regex@7.25.9(@babel/core@7.26.10)': + dependencies: + '@babel/core': 7.26.10 + '@babel/helper-create-regexp-features-plugin': 7.27.0(@babel/core@7.26.10) + '@babel/helper-plugin-utils': 7.26.5 + + '@babel/plugin-transform-unicode-regex@7.25.9(@babel/core@7.26.10)': + dependencies: + '@babel/core': 7.26.10 + '@babel/helper-create-regexp-features-plugin': 7.27.0(@babel/core@7.26.10) + '@babel/helper-plugin-utils': 7.26.5 + + '@babel/plugin-transform-unicode-sets-regex@7.25.9(@babel/core@7.26.10)': + dependencies: + '@babel/core': 7.26.10 + '@babel/helper-create-regexp-features-plugin': 7.27.0(@babel/core@7.26.10) + '@babel/helper-plugin-utils': 7.26.5 + + '@babel/preset-env@7.26.9(@babel/core@7.26.10)': + dependencies: + '@babel/compat-data': 7.26.8 + '@babel/core': 7.26.10 + '@babel/helper-compilation-targets': 7.27.0 + '@babel/helper-plugin-utils': 7.26.5 + '@babel/helper-validator-option': 7.25.9 + '@babel/plugin-bugfix-firefox-class-in-computed-class-key': 7.25.9(@babel/core@7.26.10) + '@babel/plugin-bugfix-safari-class-field-initializer-scope': 7.25.9(@babel/core@7.26.10) + '@babel/plugin-bugfix-safari-id-destructuring-collision-in-function-expression': 7.25.9(@babel/core@7.26.10) + '@babel/plugin-bugfix-v8-spread-parameters-in-optional-chaining': 7.25.9(@babel/core@7.26.10) + '@babel/plugin-bugfix-v8-static-class-fields-redefine-readonly': 7.25.9(@babel/core@7.26.10) + '@babel/plugin-proposal-private-property-in-object': 7.21.0-placeholder-for-preset-env.2(@babel/core@7.26.10) + '@babel/plugin-syntax-import-assertions': 7.26.0(@babel/core@7.26.10) + '@babel/plugin-syntax-import-attributes': 7.26.0(@babel/core@7.26.10) + '@babel/plugin-syntax-unicode-sets-regex': 7.18.6(@babel/core@7.26.10) + '@babel/plugin-transform-arrow-functions': 7.25.9(@babel/core@7.26.10) + '@babel/plugin-transform-async-generator-functions': 7.26.8(@babel/core@7.26.10) + '@babel/plugin-transform-async-to-generator': 7.25.9(@babel/core@7.26.10) + '@babel/plugin-transform-block-scoped-functions': 7.26.5(@babel/core@7.26.10) + '@babel/plugin-transform-block-scoping': 7.27.0(@babel/core@7.26.10) + '@babel/plugin-transform-class-properties': 7.25.9(@babel/core@7.26.10) + '@babel/plugin-transform-class-static-block': 7.26.0(@babel/core@7.26.10) + '@babel/plugin-transform-classes': 7.25.9(@babel/core@7.26.10) + '@babel/plugin-transform-computed-properties': 7.25.9(@babel/core@7.26.10) + '@babel/plugin-transform-destructuring': 7.25.9(@babel/core@7.26.10) + '@babel/plugin-transform-dotall-regex': 7.25.9(@babel/core@7.26.10) + '@babel/plugin-transform-duplicate-keys': 7.25.9(@babel/core@7.26.10) + '@babel/plugin-transform-duplicate-named-capturing-groups-regex': 7.25.9(@babel/core@7.26.10) + '@babel/plugin-transform-dynamic-import': 7.25.9(@babel/core@7.26.10) + '@babel/plugin-transform-exponentiation-operator': 7.26.3(@babel/core@7.26.10) + '@babel/plugin-transform-export-namespace-from': 7.25.9(@babel/core@7.26.10) + '@babel/plugin-transform-for-of': 7.26.9(@babel/core@7.26.10) + '@babel/plugin-transform-function-name': 7.25.9(@babel/core@7.26.10) + '@babel/plugin-transform-json-strings': 7.25.9(@babel/core@7.26.10) + '@babel/plugin-transform-literals': 7.25.9(@babel/core@7.26.10) + '@babel/plugin-transform-logical-assignment-operators': 7.25.9(@babel/core@7.26.10) + '@babel/plugin-transform-member-expression-literals': 7.25.9(@babel/core@7.26.10) + '@babel/plugin-transform-modules-amd': 7.25.9(@babel/core@7.26.10) + '@babel/plugin-transform-modules-commonjs': 7.26.3(@babel/core@7.26.10) + '@babel/plugin-transform-modules-systemjs': 7.25.9(@babel/core@7.26.10) + '@babel/plugin-transform-modules-umd': 7.25.9(@babel/core@7.26.10) + '@babel/plugin-transform-named-capturing-groups-regex': 7.25.9(@babel/core@7.26.10) + '@babel/plugin-transform-new-target': 7.25.9(@babel/core@7.26.10) + '@babel/plugin-transform-nullish-coalescing-operator': 7.26.6(@babel/core@7.26.10) + '@babel/plugin-transform-numeric-separator': 7.25.9(@babel/core@7.26.10) + '@babel/plugin-transform-object-rest-spread': 7.25.9(@babel/core@7.26.10) + '@babel/plugin-transform-object-super': 7.25.9(@babel/core@7.26.10) + '@babel/plugin-transform-optional-catch-binding': 7.25.9(@babel/core@7.26.10) + '@babel/plugin-transform-optional-chaining': 7.25.9(@babel/core@7.26.10) + '@babel/plugin-transform-parameters': 7.25.9(@babel/core@7.26.10) + '@babel/plugin-transform-private-methods': 7.25.9(@babel/core@7.26.10) + '@babel/plugin-transform-private-property-in-object': 7.25.9(@babel/core@7.26.10) + '@babel/plugin-transform-property-literals': 7.25.9(@babel/core@7.26.10) + '@babel/plugin-transform-regenerator': 7.27.0(@babel/core@7.26.10) + '@babel/plugin-transform-regexp-modifiers': 7.26.0(@babel/core@7.26.10) + '@babel/plugin-transform-reserved-words': 7.25.9(@babel/core@7.26.10) + '@babel/plugin-transform-shorthand-properties': 7.25.9(@babel/core@7.26.10) + '@babel/plugin-transform-spread': 7.25.9(@babel/core@7.26.10) + '@babel/plugin-transform-sticky-regex': 7.25.9(@babel/core@7.26.10) + '@babel/plugin-transform-template-literals': 7.26.8(@babel/core@7.26.10) + '@babel/plugin-transform-typeof-symbol': 7.27.0(@babel/core@7.26.10) + '@babel/plugin-transform-unicode-escapes': 7.25.9(@babel/core@7.26.10) + '@babel/plugin-transform-unicode-property-regex': 7.25.9(@babel/core@7.26.10) + '@babel/plugin-transform-unicode-regex': 7.25.9(@babel/core@7.26.10) + '@babel/plugin-transform-unicode-sets-regex': 7.25.9(@babel/core@7.26.10) + '@babel/preset-modules': 0.1.6-no-external-plugins(@babel/core@7.26.10) + babel-plugin-polyfill-corejs2: 0.4.13(@babel/core@7.26.10) + babel-plugin-polyfill-corejs3: 0.11.1(@babel/core@7.26.10) + babel-plugin-polyfill-regenerator: 0.6.4(@babel/core@7.26.10) + core-js-compat: 3.41.0 + semver: 6.3.1 + transitivePeerDependencies: + - supports-color + + '@babel/preset-flow@7.25.9(@babel/core@7.26.10)': + dependencies: + '@babel/core': 7.26.10 + '@babel/helper-plugin-utils': 7.26.5 + '@babel/helper-validator-option': 7.25.9 + '@babel/plugin-transform-flow-strip-types': 7.26.5(@babel/core@7.26.10) + + '@babel/preset-modules@0.1.6-no-external-plugins(@babel/core@7.26.10)': + dependencies: + '@babel/core': 7.26.10 + '@babel/helper-plugin-utils': 7.26.5 + '@babel/types': 7.27.0 + esutils: 2.0.3 + + '@babel/preset-react@7.26.3(@babel/core@7.26.10)': + dependencies: + '@babel/core': 7.26.10 + '@babel/helper-plugin-utils': 7.26.5 + '@babel/helper-validator-option': 7.25.9 + '@babel/plugin-transform-react-display-name': 7.25.9(@babel/core@7.26.10) + '@babel/plugin-transform-react-jsx': 7.25.9(@babel/core@7.26.10) + '@babel/plugin-transform-react-jsx-development': 7.25.9(@babel/core@7.26.10) + '@babel/plugin-transform-react-pure-annotations': 7.25.9(@babel/core@7.26.10) + transitivePeerDependencies: + - supports-color + + '@babel/runtime@7.27.0': + dependencies: + regenerator-runtime: 0.14.1 + + '@babel/template@7.27.0': + dependencies: + '@babel/code-frame': 7.26.2 + '@babel/parser': 7.27.0 + '@babel/types': 7.27.0 + + '@babel/traverse@7.27.0': + dependencies: + '@babel/code-frame': 7.26.2 + '@babel/generator': 7.27.0 + '@babel/parser': 7.27.0 + '@babel/template': 7.27.0 + '@babel/types': 7.27.0 + debug: 4.4.0 + globals: 11.12.0 + transitivePeerDependencies: + - supports-color + + '@babel/types@7.27.0': + dependencies: + '@babel/helper-string-parser': 7.25.9 + '@babel/helper-validator-identifier': 7.25.9 + + '@babel/types@7.28.2': + dependencies: + '@babel/helper-string-parser': 7.27.1 + '@babel/helper-validator-identifier': 7.27.1 + optional: true + + '@biomejs/biome@1.9.4': + optionalDependencies: + '@biomejs/cli-darwin-arm64': 1.9.4 + '@biomejs/cli-darwin-x64': 1.9.4 + '@biomejs/cli-linux-arm64': 1.9.4 + '@biomejs/cli-linux-arm64-musl': 1.9.4 + '@biomejs/cli-linux-x64': 1.9.4 + '@biomejs/cli-linux-x64-musl': 1.9.4 + '@biomejs/cli-win32-arm64': 1.9.4 + '@biomejs/cli-win32-x64': 1.9.4 + + '@biomejs/cli-darwin-arm64@1.9.4': + optional: true + + '@biomejs/cli-darwin-x64@1.9.4': + optional: true + + '@biomejs/cli-linux-arm64-musl@1.9.4': + optional: true + + '@biomejs/cli-linux-arm64@1.9.4': + optional: true + + '@biomejs/cli-linux-x64-musl@1.9.4': + optional: true + + '@biomejs/cli-linux-x64@1.9.4': + optional: true + + '@biomejs/cli-win32-arm64@1.9.4': + optional: true + + '@biomejs/cli-win32-x64@1.9.4': + optional: true + + '@capsizecss/unpack@2.4.0': + dependencies: + blob-to-buffer: 1.2.9 + cross-fetch: 3.2.0 + fontkit: 2.0.4 + transitivePeerDependencies: + - encoding + + '@csstools/selector-resolve-nested@3.0.0(postcss-selector-parser@7.1.0)': + dependencies: + postcss-selector-parser: 7.1.0 + + '@csstools/selector-specificity@5.0.0(postcss-selector-parser@7.1.0)': + dependencies: + postcss-selector-parser: 7.1.0 + + '@ctrl/tinycolor@4.1.0': {} + + '@emmetio/abbreviation@2.3.3': + dependencies: + '@emmetio/scanner': 1.0.4 + + '@emmetio/css-abbreviation@2.1.8': + dependencies: + '@emmetio/scanner': 1.0.4 + + '@emmetio/css-parser@0.4.0': + dependencies: + '@emmetio/stream-reader': 2.2.0 + '@emmetio/stream-reader-utils': 0.1.0 + + '@emmetio/html-matcher@1.3.0': + dependencies: + '@emmetio/scanner': 1.0.4 + + '@emmetio/scanner@1.0.4': {} + + '@emmetio/stream-reader-utils@0.1.0': {} + + '@emmetio/stream-reader@2.2.0': {} + + '@emnapi/runtime@1.4.0': + dependencies: + tslib: 2.8.1 + optional: true + + '@emnapi/runtime@1.4.3': + dependencies: + tslib: 2.8.1 + optional: true + + '@esbuild/aix-ppc64@0.25.3': + optional: true + + '@esbuild/android-arm64@0.25.3': + optional: true + + '@esbuild/android-arm@0.25.3': + optional: true + + '@esbuild/android-x64@0.25.3': + optional: true + + '@esbuild/darwin-arm64@0.25.3': + optional: true + + '@esbuild/darwin-x64@0.25.3': + optional: true + + '@esbuild/freebsd-arm64@0.25.3': + optional: true + + '@esbuild/freebsd-x64@0.25.3': + optional: true + + '@esbuild/linux-arm64@0.25.3': + optional: true + + '@esbuild/linux-arm@0.25.3': + optional: true + + '@esbuild/linux-ia32@0.25.3': + optional: true + + '@esbuild/linux-loong64@0.25.3': + optional: true + + '@esbuild/linux-mips64el@0.25.3': + optional: true + + '@esbuild/linux-ppc64@0.25.3': + optional: true + + '@esbuild/linux-riscv64@0.25.3': + optional: true + + '@esbuild/linux-s390x@0.25.3': + optional: true + + '@esbuild/linux-x64@0.25.3': + optional: true + + '@esbuild/netbsd-arm64@0.25.3': + optional: true + + '@esbuild/netbsd-x64@0.25.3': + optional: true + + '@esbuild/openbsd-arm64@0.25.3': + optional: true + + '@esbuild/openbsd-x64@0.25.3': + optional: true + + '@esbuild/sunos-x64@0.25.3': + optional: true + + '@esbuild/win32-arm64@0.25.3': + optional: true + + '@esbuild/win32-ia32@0.25.3': + optional: true + + '@esbuild/win32-x64@0.25.3': + optional: true + + '@expressive-code/core@0.41.3': + dependencies: + '@ctrl/tinycolor': 4.1.0 + hast-util-select: 6.0.4 + hast-util-to-html: 9.0.5 + hast-util-to-text: 4.0.2 + hastscript: 9.0.1 + postcss: 8.5.3 + postcss-nested: 6.2.0(postcss@8.5.3) + unist-util-visit: 5.0.0 + unist-util-visit-parents: 6.0.1 + + '@expressive-code/plugin-collapsible-sections@0.41.3': + dependencies: + '@expressive-code/core': 0.41.3 + + '@expressive-code/plugin-frames@0.41.3': + dependencies: + '@expressive-code/core': 0.41.3 + + '@expressive-code/plugin-line-numbers@0.41.3': + dependencies: + '@expressive-code/core': 0.41.3 + + '@expressive-code/plugin-shiki@0.41.3': + dependencies: + '@expressive-code/core': 0.41.3 + shiki: 3.3.0 + + '@expressive-code/plugin-text-markers@0.41.3': + dependencies: + '@expressive-code/core': 0.41.3 + + '@fancyapps/ui@6.0.5': {} + + '@fontsource-variable/jetbrains-mono@5.2.5': {} + + '@fontsource/roboto@5.2.5': {} + + '@iconify-json/fa6-brands@1.2.5': + dependencies: + '@iconify/types': 2.0.0 + + '@iconify-json/fa6-regular@1.2.3': + dependencies: + '@iconify/types': 2.0.0 + + '@iconify-json/fa6-solid@1.2.3': + dependencies: + '@iconify/types': 2.0.0 + + '@iconify-json/material-symbols-light@1.2.49': + dependencies: + '@iconify/types': 2.0.0 + + '@iconify-json/material-symbols@1.2.20': + dependencies: + '@iconify/types': 2.0.0 + + '@iconify-json/simple-icons@1.2.42': + dependencies: + '@iconify/types': 2.0.0 + + '@iconify/svelte@4.2.0(svelte@5.28.2)': + dependencies: + '@iconify/types': 2.0.0 + svelte: 5.28.2 + + '@iconify/tools@4.1.2': + dependencies: + '@iconify/types': 2.0.0 + '@iconify/utils': 2.3.0 + '@types/tar': 6.1.13 + axios: 1.8.4 + cheerio: 1.0.0 + domhandler: 5.0.3 + extract-zip: 2.0.1 + local-pkg: 0.5.1 + pathe: 1.1.2 + svgo: 3.3.2 + tar: 6.2.1 + transitivePeerDependencies: + - debug + - supports-color + + '@iconify/types@2.0.0': {} + + '@iconify/utils@2.3.0': + dependencies: + '@antfu/install-pkg': 1.0.0 + '@antfu/utils': 8.1.1 + '@iconify/types': 2.0.0 + debug: 4.4.0 + globals: 15.15.0 + kolorist: 1.8.0 + local-pkg: 1.1.1 + mlly: 1.7.4 + transitivePeerDependencies: + - supports-color + + '@img/sharp-darwin-arm64@0.33.5': + optionalDependencies: + '@img/sharp-libvips-darwin-arm64': 1.0.4 + optional: true + + '@img/sharp-darwin-arm64@0.34.1': + optionalDependencies: + '@img/sharp-libvips-darwin-arm64': 1.1.0 + optional: true + + '@img/sharp-darwin-x64@0.33.5': + optionalDependencies: + '@img/sharp-libvips-darwin-x64': 1.0.4 + optional: true + + '@img/sharp-darwin-x64@0.34.1': + optionalDependencies: + '@img/sharp-libvips-darwin-x64': 1.1.0 + optional: true + + '@img/sharp-libvips-darwin-arm64@1.0.4': + optional: true + + '@img/sharp-libvips-darwin-arm64@1.1.0': + optional: true + + '@img/sharp-libvips-darwin-x64@1.0.4': + optional: true + + '@img/sharp-libvips-darwin-x64@1.1.0': + optional: true + + '@img/sharp-libvips-linux-arm64@1.0.4': + optional: true + + '@img/sharp-libvips-linux-arm64@1.1.0': + optional: true + + '@img/sharp-libvips-linux-arm@1.0.5': + optional: true + + '@img/sharp-libvips-linux-arm@1.1.0': + optional: true + + '@img/sharp-libvips-linux-ppc64@1.1.0': + optional: true + + '@img/sharp-libvips-linux-s390x@1.0.4': + optional: true + + '@img/sharp-libvips-linux-s390x@1.1.0': + optional: true + + '@img/sharp-libvips-linux-x64@1.0.4': + optional: true + + '@img/sharp-libvips-linux-x64@1.1.0': + optional: true + + '@img/sharp-libvips-linuxmusl-arm64@1.0.4': + optional: true + + '@img/sharp-libvips-linuxmusl-arm64@1.1.0': + optional: true + + '@img/sharp-libvips-linuxmusl-x64@1.0.4': + optional: true + + '@img/sharp-libvips-linuxmusl-x64@1.1.0': + optional: true + + '@img/sharp-linux-arm64@0.33.5': + optionalDependencies: + '@img/sharp-libvips-linux-arm64': 1.0.4 + optional: true + + '@img/sharp-linux-arm64@0.34.1': + optionalDependencies: + '@img/sharp-libvips-linux-arm64': 1.1.0 + optional: true + + '@img/sharp-linux-arm@0.33.5': + optionalDependencies: + '@img/sharp-libvips-linux-arm': 1.0.5 + optional: true + + '@img/sharp-linux-arm@0.34.1': + optionalDependencies: + '@img/sharp-libvips-linux-arm': 1.1.0 + optional: true + + '@img/sharp-linux-s390x@0.33.5': + optionalDependencies: + '@img/sharp-libvips-linux-s390x': 1.0.4 + optional: true + + '@img/sharp-linux-s390x@0.34.1': + optionalDependencies: + '@img/sharp-libvips-linux-s390x': 1.1.0 + optional: true + + '@img/sharp-linux-x64@0.33.5': + optionalDependencies: + '@img/sharp-libvips-linux-x64': 1.0.4 + optional: true + + '@img/sharp-linux-x64@0.34.1': + optionalDependencies: + '@img/sharp-libvips-linux-x64': 1.1.0 + optional: true + + '@img/sharp-linuxmusl-arm64@0.33.5': + optionalDependencies: + '@img/sharp-libvips-linuxmusl-arm64': 1.0.4 + optional: true + + '@img/sharp-linuxmusl-arm64@0.34.1': + optionalDependencies: + '@img/sharp-libvips-linuxmusl-arm64': 1.1.0 + optional: true + + '@img/sharp-linuxmusl-x64@0.33.5': + optionalDependencies: + '@img/sharp-libvips-linuxmusl-x64': 1.0.4 + optional: true + + '@img/sharp-linuxmusl-x64@0.34.1': + optionalDependencies: + '@img/sharp-libvips-linuxmusl-x64': 1.1.0 + optional: true + + '@img/sharp-wasm32@0.33.5': + dependencies: + '@emnapi/runtime': 1.4.3 + optional: true + + '@img/sharp-wasm32@0.34.1': + dependencies: + '@emnapi/runtime': 1.4.0 + optional: true + + '@img/sharp-win32-ia32@0.33.5': + optional: true + + '@img/sharp-win32-ia32@0.34.1': + optional: true + + '@img/sharp-win32-x64@0.33.5': + optional: true + + '@img/sharp-win32-x64@0.34.1': + optional: true + + '@isaacs/balanced-match@4.0.1': {} + + '@isaacs/brace-expansion@5.0.0': + dependencies: + '@isaacs/balanced-match': 4.0.1 + + '@isaacs/cliui@8.0.2': + dependencies: + string-width: 5.1.2 + string-width-cjs: string-width@4.2.3 + strip-ansi: 7.1.0 + strip-ansi-cjs: strip-ansi@6.0.1 + wrap-ansi: 8.1.0 + wrap-ansi-cjs: wrap-ansi@7.0.0 + + '@isaacs/fs-minipass@4.0.1': + dependencies: + minipass: 7.1.2 + + '@jridgewell/gen-mapping@0.3.8': + dependencies: + '@jridgewell/set-array': 1.2.1 + '@jridgewell/sourcemap-codec': 1.5.0 + '@jridgewell/trace-mapping': 0.3.25 + + '@jridgewell/resolve-uri@3.1.2': {} + + '@jridgewell/set-array@1.2.1': {} + + '@jridgewell/source-map@0.3.6': + dependencies: + '@jridgewell/gen-mapping': 0.3.8 + '@jridgewell/trace-mapping': 0.3.25 + + '@jridgewell/sourcemap-codec@1.5.0': {} + + '@jridgewell/trace-mapping@0.3.25': + dependencies: + '@jridgewell/resolve-uri': 3.1.2 + '@jridgewell/sourcemap-codec': 1.5.0 + + '@mapbox/node-pre-gyp@2.0.0': + dependencies: + consola: 3.4.2 + detect-libc: 2.0.4 + https-proxy-agent: 7.0.6 + node-fetch: 2.7.0 + nopt: 8.1.0 + semver: 7.7.1 + tar: 7.4.3 + transitivePeerDependencies: + - encoding + - supports-color + + '@nodelib/fs.scandir@2.1.5': + dependencies: + '@nodelib/fs.stat': 2.0.5 + run-parallel: 1.2.0 + + '@nodelib/fs.stat@2.0.5': {} + + '@nodelib/fs.walk@1.2.8': + dependencies: + '@nodelib/fs.scandir': 2.1.5 + fastq: 1.19.1 + + '@oslojs/encoding@1.1.0': {} + + '@parcel/watcher-android-arm64@2.5.1': + optional: true + + '@parcel/watcher-darwin-arm64@2.5.1': + optional: true + + '@parcel/watcher-darwin-x64@2.5.1': + optional: true + + '@parcel/watcher-freebsd-x64@2.5.1': + optional: true + + '@parcel/watcher-linux-arm-glibc@2.5.1': + optional: true + + '@parcel/watcher-linux-arm-musl@2.5.1': + optional: true + + '@parcel/watcher-linux-arm64-glibc@2.5.1': + optional: true + + '@parcel/watcher-linux-arm64-musl@2.5.1': + optional: true + + '@parcel/watcher-linux-x64-glibc@2.5.1': + optional: true + + '@parcel/watcher-linux-x64-musl@2.5.1': + optional: true + + '@parcel/watcher-win32-arm64@2.5.1': + optional: true + + '@parcel/watcher-win32-ia32@2.5.1': + optional: true + + '@parcel/watcher-win32-x64@2.5.1': + optional: true + + '@parcel/watcher@2.5.1': + dependencies: + detect-libc: 1.0.3 + is-glob: 4.0.3 + micromatch: 4.0.8 + node-addon-api: 7.1.1 + optionalDependencies: + '@parcel/watcher-android-arm64': 2.5.1 + '@parcel/watcher-darwin-arm64': 2.5.1 + '@parcel/watcher-darwin-x64': 2.5.1 + '@parcel/watcher-freebsd-x64': 2.5.1 + '@parcel/watcher-linux-arm-glibc': 2.5.1 + '@parcel/watcher-linux-arm-musl': 2.5.1 + '@parcel/watcher-linux-arm64-glibc': 2.5.1 + '@parcel/watcher-linux-arm64-musl': 2.5.1 + '@parcel/watcher-linux-x64-glibc': 2.5.1 + '@parcel/watcher-linux-x64-musl': 2.5.1 + '@parcel/watcher-win32-arm64': 2.5.1 + '@parcel/watcher-win32-ia32': 2.5.1 + '@parcel/watcher-win32-x64': 2.5.1 + optional: true + + '@pkgjs/parseargs@0.11.0': + optional: true + + '@rollup/plugin-alias@3.1.9(rollup@2.79.2)': + dependencies: + rollup: 2.79.2 + slash: 3.0.0 + + '@rollup/plugin-babel@5.3.1(@babel/core@7.26.10)(@types/babel__core@7.20.5)(rollup@2.79.2)': + dependencies: + '@babel/core': 7.26.10 + '@babel/helper-module-imports': 7.25.9 + '@rollup/pluginutils': 3.1.0(rollup@2.79.2) + rollup: 2.79.2 + optionalDependencies: + '@types/babel__core': 7.20.5 + transitivePeerDependencies: + - supports-color + + '@rollup/plugin-commonjs@17.1.0(rollup@2.79.2)': + dependencies: + '@rollup/pluginutils': 3.1.0(rollup@2.79.2) + commondir: 1.0.1 + estree-walker: 2.0.2 + glob: 7.2.3 + is-reference: 1.2.1 + magic-string: 0.25.9 + resolve: 1.22.10 + rollup: 2.79.2 + + '@rollup/plugin-json@4.1.0(rollup@2.79.2)': + dependencies: + '@rollup/pluginutils': 3.1.0(rollup@2.79.2) + rollup: 2.79.2 + + '@rollup/plugin-node-resolve@11.2.1(rollup@2.79.2)': + dependencies: + '@rollup/pluginutils': 3.1.0(rollup@2.79.2) + '@types/resolve': 1.17.1 + builtin-modules: 3.3.0 + deepmerge: 4.3.1 + is-module: 1.0.0 + resolve: 1.22.10 + rollup: 2.79.2 + + '@rollup/plugin-yaml@4.1.2(rollup@2.79.2)': + dependencies: + '@rollup/pluginutils': 5.1.4(rollup@2.79.2) + js-yaml: 4.1.0 + tosource: 2.0.0-alpha.3 + optionalDependencies: + rollup: 2.79.2 + + '@rollup/pluginutils@3.1.0(rollup@2.79.2)': + dependencies: + '@types/estree': 0.0.39 + estree-walker: 1.0.1 + picomatch: 2.3.1 + rollup: 2.79.2 + + '@rollup/pluginutils@4.2.1': + dependencies: + estree-walker: 2.0.2 + picomatch: 2.3.1 + + '@rollup/pluginutils@5.1.4(rollup@2.79.2)': + dependencies: + '@types/estree': 1.0.7 + estree-walker: 2.0.2 + picomatch: 4.0.2 + optionalDependencies: + rollup: 2.79.2 + + '@rollup/rollup-android-arm-eabi@4.40.1': + optional: true + + '@rollup/rollup-android-arm64@4.40.1': + optional: true + + '@rollup/rollup-darwin-arm64@4.40.1': + optional: true + + '@rollup/rollup-darwin-x64@4.40.1': + optional: true + + '@rollup/rollup-freebsd-arm64@4.40.1': + optional: true + + '@rollup/rollup-freebsd-x64@4.40.1': + optional: true + + '@rollup/rollup-linux-arm-gnueabihf@4.40.1': + optional: true + + '@rollup/rollup-linux-arm-musleabihf@4.40.1': + optional: true + + '@rollup/rollup-linux-arm64-gnu@4.40.1': + optional: true + + '@rollup/rollup-linux-arm64-musl@4.40.1': + optional: true + + '@rollup/rollup-linux-loongarch64-gnu@4.40.1': + optional: true + + '@rollup/rollup-linux-powerpc64le-gnu@4.40.1': + optional: true + + '@rollup/rollup-linux-riscv64-gnu@4.40.1': + optional: true + + '@rollup/rollup-linux-riscv64-musl@4.40.1': + optional: true + + '@rollup/rollup-linux-s390x-gnu@4.40.1': + optional: true + + '@rollup/rollup-linux-x64-gnu@4.40.1': + optional: true + + '@rollup/rollup-linux-x64-musl@4.40.1': + optional: true + + '@rollup/rollup-win32-arm64-msvc@4.40.1': + optional: true + + '@rollup/rollup-win32-ia32-msvc@4.40.1': + optional: true + + '@rollup/rollup-win32-x64-msvc@4.40.1': + optional: true + + '@shikijs/core@3.3.0': + dependencies: + '@shikijs/types': 3.3.0 + '@shikijs/vscode-textmate': 10.0.2 + '@types/hast': 3.0.4 + hast-util-to-html: 9.0.5 + + '@shikijs/engine-javascript@3.3.0': + dependencies: + '@shikijs/types': 3.3.0 + '@shikijs/vscode-textmate': 10.0.2 + oniguruma-to-es: 4.3.1 + + '@shikijs/engine-oniguruma@3.3.0': + dependencies: + '@shikijs/types': 3.3.0 + '@shikijs/vscode-textmate': 10.0.2 + + '@shikijs/langs@3.3.0': + dependencies: + '@shikijs/types': 3.3.0 + + '@shikijs/themes@3.3.0': + dependencies: + '@shikijs/types': 3.3.0 + + '@shikijs/types@3.3.0': + dependencies: + '@shikijs/vscode-textmate': 10.0.2 + '@types/hast': 3.0.4 + + '@shikijs/vscode-textmate@10.0.2': {} + + '@surma/rollup-plugin-off-main-thread@2.2.3': + dependencies: + ejs: 3.1.10 + json5: 2.2.3 + magic-string: 0.25.9 + string.prototype.matchall: 4.0.12 + + '@sveltejs/acorn-typescript@1.0.5(acorn@8.14.1)': + dependencies: + acorn: 8.14.1 + + '@sveltejs/vite-plugin-svelte-inspector@4.0.1(@sveltejs/vite-plugin-svelte@5.0.3(svelte@5.28.2)(vite@6.3.3(@types/node@22.14.1)(jiti@1.21.7)(lightningcss@1.29.3)(sass@1.80.4)(stylus@0.64.0)(terser@5.39.0)(yaml@2.7.0)))(svelte@5.28.2)(vite@6.3.3(@types/node@22.14.1)(jiti@1.21.7)(lightningcss@1.29.3)(sass@1.80.4)(stylus@0.64.0)(terser@5.39.0)(yaml@2.7.0))': + dependencies: + '@sveltejs/vite-plugin-svelte': 5.0.3(svelte@5.28.2)(vite@6.3.3(@types/node@22.14.1)(jiti@1.21.7)(lightningcss@1.29.3)(sass@1.80.4)(stylus@0.64.0)(terser@5.39.0)(yaml@2.7.0)) + debug: 4.4.0 + svelte: 5.28.2 + vite: 6.3.3(@types/node@22.14.1)(jiti@1.21.7)(lightningcss@1.29.3)(sass@1.80.4)(stylus@0.64.0)(terser@5.39.0)(yaml@2.7.0) + transitivePeerDependencies: + - supports-color + + '@sveltejs/vite-plugin-svelte@5.0.3(svelte@5.28.2)(vite@6.3.3(@types/node@22.14.1)(jiti@1.21.7)(lightningcss@1.29.3)(sass@1.80.4)(stylus@0.64.0)(terser@5.39.0)(yaml@2.7.0))': + dependencies: + '@sveltejs/vite-plugin-svelte-inspector': 4.0.1(@sveltejs/vite-plugin-svelte@5.0.3(svelte@5.28.2)(vite@6.3.3(@types/node@22.14.1)(jiti@1.21.7)(lightningcss@1.29.3)(sass@1.80.4)(stylus@0.64.0)(terser@5.39.0)(yaml@2.7.0)))(svelte@5.28.2)(vite@6.3.3(@types/node@22.14.1)(jiti@1.21.7)(lightningcss@1.29.3)(sass@1.80.4)(stylus@0.64.0)(terser@5.39.0)(yaml@2.7.0)) + debug: 4.4.0 + deepmerge: 4.3.1 + kleur: 4.1.5 + magic-string: 0.30.17 + svelte: 5.28.2 + vite: 6.3.3(@types/node@22.14.1)(jiti@1.21.7)(lightningcss@1.29.3)(sass@1.80.4)(stylus@0.64.0)(terser@5.39.0)(yaml@2.7.0) + vitefu: 1.0.6(vite@6.3.3(@types/node@22.14.1)(jiti@1.21.7)(lightningcss@1.29.3)(sass@1.80.4)(stylus@0.64.0)(terser@5.39.0)(yaml@2.7.0)) + transitivePeerDependencies: + - supports-color + + '@swc/helpers@0.5.17': + dependencies: + tslib: 2.8.1 + + '@swup/a11y-plugin@5.0.0(swup@4.8.1)': + dependencies: + '@swup/plugin': 4.0.0 + focus-options-polyfill: 1.6.0 + swup: 4.8.1 + + '@swup/astro@1.6.0(@types/babel__core@7.20.5)': + dependencies: + '@swup/a11y-plugin': 5.0.0(swup@4.8.1) + '@swup/body-class-plugin': 3.3.0(swup@4.8.1) + '@swup/debug-plugin': 4.1.0(swup@4.8.1) + '@swup/fade-theme': 2.0.1(swup@4.8.1) + '@swup/forms-plugin': 3.6.0(swup@4.8.1) + '@swup/head-plugin': 2.3.1(swup@4.8.1) + '@swup/overlay-theme': 2.0.1(swup@4.8.1) + '@swup/parallel-plugin': 0.4.0(@types/babel__core@7.20.5)(swup@4.8.1) + '@swup/preload-plugin': 3.2.11(swup@4.8.1) + '@swup/progress-plugin': 3.2.0(swup@4.8.1) + '@swup/route-name-plugin': 4.1.0(@types/babel__core@7.20.5)(swup@4.8.1) + '@swup/scripts-plugin': 2.1.0(swup@4.8.1) + '@swup/scroll-plugin': 3.3.2(swup@4.8.1) + '@swup/slide-theme': 2.0.1(swup@4.8.1) + swup: 4.8.1 + swup-morph-plugin: 1.3.0(swup@4.8.1) + transitivePeerDependencies: + - '@types/babel__core' + - rolldown + - supports-color + - ts-node + + '@swup/body-class-plugin@3.3.0(swup@4.8.1)': + dependencies: + '@swup/plugin': 4.0.0 + swup: 4.8.1 + + '@swup/browserslist-config@1.0.1': {} + + '@swup/debug-plugin@4.1.0(swup@4.8.1)': + dependencies: + '@swup/plugin': 4.0.0 + swup: 4.8.1 + + '@swup/fade-theme@2.0.1(swup@4.8.1)': + dependencies: + '@swup/theme': 2.1.0(swup@4.8.1) + swup: 4.8.1 + + '@swup/forms-plugin@3.6.0(swup@4.8.1)': + dependencies: + '@swup/plugin': 4.0.0 + swup: 4.8.1 + + '@swup/head-plugin@2.3.1(swup@4.8.1)': + dependencies: + '@swup/plugin': 4.0.0 + swup: 4.8.1 + + '@swup/overlay-theme@2.0.1(swup@4.8.1)': + dependencies: + '@swup/theme': 2.1.0(swup@4.8.1) + swup: 4.8.1 + + '@swup/parallel-plugin@0.4.0(@types/babel__core@7.20.5)(swup@4.8.1)': + dependencies: + '@swup/plugin': 3.0.1(@types/babel__core@7.20.5) + swup: 4.8.1 + transitivePeerDependencies: + - '@types/babel__core' + - rolldown + - supports-color + - ts-node + + '@swup/plugin@3.0.1(@types/babel__core@7.20.5)': + dependencies: + '@swup/browserslist-config': 1.0.1 + '@swup/prettier-config': 1.1.0 + chalk: 5.4.1 + microbundle: 0.15.1(@types/babel__core@7.20.5) + prettier: 2.8.8 + shelljs: 0.8.5 + shelljs-live: 0.0.5(shelljs@0.8.5) + swup: 4.8.1 + transitivePeerDependencies: + - '@types/babel__core' + - rolldown + - supports-color + - ts-node + + '@swup/plugin@4.0.0': + dependencies: + swup: 4.8.1 + + '@swup/preload-plugin@3.2.11(swup@4.8.1)': + dependencies: + '@swup/plugin': 4.0.0 + swup: 4.8.1 + + '@swup/prettier-config@1.1.0': {} + + '@swup/progress-plugin@3.2.0(swup@4.8.1)': + dependencies: + '@swup/plugin': 4.0.0 + swup: 4.8.1 + + '@swup/route-name-plugin@4.1.0(@types/babel__core@7.20.5)(swup@4.8.1)': + dependencies: + '@swup/plugin': 3.0.1(@types/babel__core@7.20.5) + swup: 4.8.1 + transitivePeerDependencies: + - '@types/babel__core' + - rolldown + - supports-color + - ts-node + + '@swup/scripts-plugin@2.1.0(swup@4.8.1)': + dependencies: + '@swup/plugin': 4.0.0 + swup: 4.8.1 + + '@swup/scroll-plugin@3.3.2(swup@4.8.1)': + dependencies: + '@swup/plugin': 4.0.0 + scrl: 2.0.0 + swup: 4.8.1 + + '@swup/slide-theme@2.0.1(swup@4.8.1)': + dependencies: + '@swup/theme': 2.1.0(swup@4.8.1) + swup: 4.8.1 + + '@swup/theme@2.1.0(swup@4.8.1)': + dependencies: + '@swup/plugin': 4.0.0 + swup: 4.8.1 + + '@tailwindcss/typography@0.5.16(tailwindcss@3.4.17)': + dependencies: + lodash.castarray: 4.4.0 + lodash.isplainobject: 4.0.6 + lodash.merge: 4.6.2 + postcss-selector-parser: 6.0.10 + tailwindcss: 3.4.17 + + '@trysound/sax@0.2.0': {} + + '@types/babel__core@7.20.5': + dependencies: + '@babel/parser': 7.28.0 + '@babel/types': 7.28.2 + '@types/babel__generator': 7.27.0 + '@types/babel__template': 7.4.4 + '@types/babel__traverse': 7.28.0 + optional: true + + '@types/babel__generator@7.27.0': + dependencies: + '@babel/types': 7.28.2 + optional: true + + '@types/babel__template@7.4.4': + dependencies: + '@babel/parser': 7.28.0 + '@babel/types': 7.28.2 + optional: true + + '@types/babel__traverse@7.28.0': + dependencies: + '@babel/types': 7.28.2 + optional: true + + '@types/debug@4.1.12': + dependencies: + '@types/ms': 2.1.0 + + '@types/estree@0.0.39': {} + + '@types/estree@1.0.7': {} + + '@types/hast@2.3.10': + dependencies: + '@types/unist': 2.0.11 + + '@types/hast@3.0.4': + dependencies: + '@types/unist': 3.0.3 + + '@types/katex@0.16.7': {} + + '@types/linkify-it@5.0.0': {} + + '@types/markdown-it@14.1.2': + dependencies: + '@types/linkify-it': 5.0.0 + '@types/mdurl': 2.0.0 + + '@types/mdast@4.0.4': + dependencies: + '@types/unist': 3.0.3 + + '@types/mdurl@2.0.0': {} + + '@types/ms@2.1.0': {} + + '@types/nlcst@2.0.3': + dependencies: + '@types/unist': 3.0.3 + + '@types/node@17.0.45': {} + + '@types/node@22.14.1': + dependencies: + undici-types: 6.21.0 + + '@types/parse-json@4.0.2': {} + + '@types/resolve@1.17.1': + dependencies: + '@types/node': 22.14.1 + + '@types/sanitize-html@2.15.0': + dependencies: + htmlparser2: 8.0.2 + + '@types/sax@1.2.7': + dependencies: + '@types/node': 17.0.45 + + '@types/tar@6.1.13': + dependencies: + '@types/node': 22.14.1 + minipass: 4.2.8 + + '@types/unist@2.0.11': {} + + '@types/unist@3.0.3': {} + + '@types/yauzl@2.10.3': + dependencies: + '@types/node': 22.14.1 + optional: true + + '@ungap/structured-clone@1.3.0': {} + + '@vercel/analytics@1.5.0(svelte@5.28.2)': + optionalDependencies: + svelte: 5.28.2 + + '@vercel/functions@2.2.13': + dependencies: + '@vercel/oidc': 2.0.2 + + '@vercel/nft@0.29.4(rollup@2.79.2)': + dependencies: + '@mapbox/node-pre-gyp': 2.0.0 + '@rollup/pluginutils': 5.1.4(rollup@2.79.2) + acorn: 8.14.1 + acorn-import-attributes: 1.9.5(acorn@8.14.1) + async-sema: 3.1.1 + bindings: 1.5.0 + estree-walker: 2.0.2 + glob: 10.4.5 + graceful-fs: 4.2.11 + node-gyp-build: 4.8.4 + picomatch: 4.0.2 + resolve-from: 5.0.0 + transitivePeerDependencies: + - encoding + - rollup + - supports-color + + '@vercel/oidc@2.0.2': + dependencies: + '@types/ms': 2.1.0 + ms: 2.1.3 + + '@vercel/routing-utils@5.1.1': + dependencies: + path-to-regexp: 6.1.0 + path-to-regexp-updated: path-to-regexp@6.3.0 + optionalDependencies: + ajv: 6.12.6 + + '@volar/kit@2.4.12(typescript@5.8.3)': + dependencies: + '@volar/language-service': 2.4.12 + '@volar/typescript': 2.4.12 + typesafe-path: 0.2.2 + typescript: 5.8.3 + vscode-languageserver-textdocument: 1.0.12 + vscode-uri: 3.1.0 + + '@volar/language-core@2.4.12': + dependencies: + '@volar/source-map': 2.4.12 + + '@volar/language-server@2.4.12': + dependencies: + '@volar/language-core': 2.4.12 + '@volar/language-service': 2.4.12 + '@volar/typescript': 2.4.12 + path-browserify: 1.0.1 + request-light: 0.7.0 + vscode-languageserver: 9.0.1 + vscode-languageserver-protocol: 3.17.5 + vscode-languageserver-textdocument: 1.0.12 + vscode-uri: 3.1.0 + + '@volar/language-service@2.4.12': + dependencies: + '@volar/language-core': 2.4.12 + vscode-languageserver-protocol: 3.17.5 + vscode-languageserver-textdocument: 1.0.12 + vscode-uri: 3.1.0 + + '@volar/source-map@2.4.12': {} + + '@volar/typescript@2.4.12': + dependencies: + '@volar/language-core': 2.4.12 + path-browserify: 1.0.1 + vscode-uri: 3.1.0 + + '@vscode/emmet-helper@2.11.0': + dependencies: + emmet: 2.4.11 + jsonc-parser: 2.3.1 + vscode-languageserver-textdocument: 1.0.12 + vscode-languageserver-types: 3.17.5 + vscode-uri: 3.1.0 + + '@vscode/l10n@0.0.18': {} + + abbrev@3.0.1: {} + + acorn-import-attributes@1.9.5(acorn@8.14.1): + dependencies: + acorn: 8.14.1 + + acorn@8.14.1: {} + + agent-base@7.1.4: {} + + ajv@6.12.6: + dependencies: + fast-deep-equal: 3.1.3 + fast-json-stable-stringify: 2.1.0 + json-schema-traverse: 0.4.1 + uri-js: 4.4.1 + optional: true + + ajv@8.17.1: + dependencies: + fast-deep-equal: 3.1.3 + fast-uri: 3.0.6 + json-schema-traverse: 1.0.0 + require-from-string: 2.0.2 + + ansi-align@3.0.1: + dependencies: + string-width: 4.2.3 + + ansi-regex@2.1.1: {} + + ansi-regex@5.0.1: {} + + ansi-regex@6.1.0: {} + + ansi-styles@2.2.1: {} + + ansi-styles@4.3.0: + dependencies: + color-convert: 2.0.1 + + ansi-styles@6.2.1: {} + + any-promise@1.3.0: {} + + anymatch@3.1.3: + dependencies: + normalize-path: 3.0.0 + picomatch: 2.3.1 + + arg@5.0.2: {} + + argparse@2.0.1: {} + + aria-query@5.3.2: {} + + array-buffer-byte-length@1.0.2: + dependencies: + call-bound: 1.0.4 + is-array-buffer: 3.0.5 + + array-iterate@2.0.1: {} + + arraybuffer.prototype.slice@1.0.4: + dependencies: + array-buffer-byte-length: 1.0.2 + call-bind: 1.0.8 + define-properties: 1.2.1 + es-abstract: 1.23.9 + es-errors: 1.3.0 + get-intrinsic: 1.3.0 + is-array-buffer: 3.0.5 + + astro-expressive-code@0.41.3(astro@5.7.9(patch_hash=ysrllw3gj6hylybllnynya5oma)(@types/node@22.14.1)(jiti@1.21.7)(lightningcss@1.29.3)(rollup@2.79.2)(sass@1.80.4)(stylus@0.64.0)(terser@5.39.0)(typescript@5.8.3)(yaml@2.7.0)): + dependencies: + astro: 5.7.9(patch_hash=ysrllw3gj6hylybllnynya5oma)(@types/node@22.14.1)(jiti@1.21.7)(lightningcss@1.29.3)(rollup@2.79.2)(sass@1.80.4)(stylus@0.64.0)(terser@5.39.0)(typescript@5.8.3)(yaml@2.7.0) + rehype-expressive-code: 0.41.3 + + astro-icon@1.1.5: + dependencies: + '@iconify/tools': 4.1.2 + '@iconify/types': 2.0.0 + '@iconify/utils': 2.3.0 + transitivePeerDependencies: + - debug + - supports-color + + astro@5.7.9(patch_hash=ysrllw3gj6hylybllnynya5oma)(@types/node@22.14.1)(jiti@1.21.7)(lightningcss@1.29.3)(rollup@2.79.2)(sass@1.80.4)(stylus@0.64.0)(terser@5.39.0)(typescript@5.8.3)(yaml@2.7.0): + dependencies: + '@astrojs/compiler': 2.11.0 + '@astrojs/internal-helpers': 0.6.1 + '@astrojs/markdown-remark': 6.3.1 + '@astrojs/telemetry': 3.2.1 + '@capsizecss/unpack': 2.4.0 + '@oslojs/encoding': 1.1.0 + '@rollup/pluginutils': 5.1.4(rollup@2.79.2) + acorn: 8.14.1 + aria-query: 5.3.2 + axobject-query: 4.1.0 + boxen: 8.0.1 + ci-info: 4.2.0 + clsx: 2.1.1 + common-ancestor-path: 1.0.1 + cookie: 1.0.2 + cssesc: 3.0.0 + debug: 4.4.0 + deterministic-object-hash: 2.0.2 + devalue: 5.1.1 + diff: 5.2.0 + dlv: 1.1.3 + dset: 3.1.4 + es-module-lexer: 1.7.0 + esbuild: 0.25.3 + estree-walker: 3.0.3 + flattie: 1.1.1 + github-slugger: 2.0.0 + html-escaper: 3.0.3 + http-cache-semantics: 4.1.1 + js-yaml: 4.1.0 + kleur: 4.1.5 + magic-string: 0.30.17 + magicast: 0.3.5 + mrmime: 2.0.1 + neotraverse: 0.6.18 + p-limit: 6.2.0 + p-queue: 8.1.0 + package-manager-detector: 1.2.0 + picomatch: 4.0.2 + prompts: 2.4.2 + rehype: 13.0.2 + semver: 7.7.1 + shiki: 3.3.0 + tinyexec: 0.3.2 + tinyglobby: 0.2.13 + tsconfck: 3.1.5(typescript@5.8.3) + ultrahtml: 1.6.0 + unifont: 0.4.1 + unist-util-visit: 5.0.0 + unstorage: 1.16.0 + vfile: 6.0.3 + vite: 6.3.3(@types/node@22.14.1)(jiti@1.21.7)(lightningcss@1.29.3)(sass@1.80.4)(stylus@0.64.0)(terser@5.39.0)(yaml@2.7.0) + vitefu: 1.0.6(vite@6.3.3(@types/node@22.14.1)(jiti@1.21.7)(lightningcss@1.29.3)(sass@1.80.4)(stylus@0.64.0)(terser@5.39.0)(yaml@2.7.0)) + xxhash-wasm: 1.1.0 + yargs-parser: 21.1.1 + yocto-spinner: 0.2.2 + zod: 3.24.3 + zod-to-json-schema: 3.24.5(zod@3.24.3) + zod-to-ts: 1.2.0(typescript@5.8.3)(zod@3.24.3) + optionalDependencies: + sharp: 0.33.5 + transitivePeerDependencies: + - '@azure/app-configuration' + - '@azure/cosmos' + - '@azure/data-tables' + - '@azure/identity' + - '@azure/keyvault-secrets' + - '@azure/storage-blob' + - '@capacitor/preferences' + - '@deno/kv' + - '@netlify/blobs' + - '@planetscale/database' + - '@types/node' + - '@upstash/redis' + - '@vercel/blob' + - '@vercel/kv' + - aws4fetch + - db0 + - encoding + - idb-keyval + - ioredis + - jiti + - less + - lightningcss + - rollup + - sass + - sass-embedded + - stylus + - sugarss + - supports-color + - terser + - tsx + - typescript + - uploadthing + - yaml + + async-function@1.0.0: {} + + async-sema@3.1.1: {} + + async@3.2.6: {} + + asynckit@0.4.0: {} + + asyncro@3.0.0: {} + + autoprefixer@10.4.21(postcss@8.5.3): + dependencies: + browserslist: 4.24.4 + caniuse-lite: 1.0.30001733 + fraction.js: 4.3.7 + normalize-range: 0.1.2 + picocolors: 1.1.1 + postcss: 8.5.3 + postcss-value-parser: 4.2.0 + + available-typed-arrays@1.0.7: + dependencies: + possible-typed-array-names: 1.1.0 + + axios@1.8.4: + dependencies: + follow-redirects: 1.15.9 + form-data: 4.0.2 + proxy-from-env: 1.1.0 + transitivePeerDependencies: + - debug + + axobject-query@4.1.0: {} + + babel-plugin-macros@3.1.0: + dependencies: + '@babel/runtime': 7.27.0 + cosmiconfig: 7.1.0 + resolve: 1.22.10 + + babel-plugin-polyfill-corejs2@0.4.13(@babel/core@7.26.10): + dependencies: + '@babel/compat-data': 7.26.8 + '@babel/core': 7.26.10 + '@babel/helper-define-polyfill-provider': 0.6.4(@babel/core@7.26.10) + semver: 6.3.1 + transitivePeerDependencies: + - supports-color + + babel-plugin-polyfill-corejs3@0.11.1(@babel/core@7.26.10): + dependencies: + '@babel/core': 7.26.10 + '@babel/helper-define-polyfill-provider': 0.6.4(@babel/core@7.26.10) + core-js-compat: 3.41.0 + transitivePeerDependencies: + - supports-color + + babel-plugin-polyfill-regenerator@0.6.4(@babel/core@7.26.10): + dependencies: + '@babel/core': 7.26.10 + '@babel/helper-define-polyfill-provider': 0.6.4(@babel/core@7.26.10) + transitivePeerDependencies: + - supports-color + + babel-plugin-transform-async-to-promises@0.8.18: {} + + babel-plugin-transform-replace-expressions@0.2.0(@babel/core@7.26.10): + dependencies: + '@babel/core': 7.26.10 + '@babel/parser': 7.27.0 + + bail@2.0.2: {} + + balanced-match@1.0.2: {} + + base-64@1.0.0: {} + + base64-js@1.5.1: {} + + bcp-47-match@2.0.3: {} + + binary-extensions@2.3.0: {} + + bindings@1.5.0: + dependencies: + file-uri-to-path: 1.0.0 + + blob-to-buffer@1.2.9: {} + + boolbase@1.0.0: {} + + boxen@8.0.1: + dependencies: + ansi-align: 3.0.1 + camelcase: 8.0.0 + chalk: 5.4.1 + cli-boxes: 3.0.0 + string-width: 7.2.0 + type-fest: 4.40.1 + widest-line: 5.0.0 + wrap-ansi: 9.0.0 + + brace-expansion@1.1.11: + dependencies: + balanced-match: 1.0.2 + concat-map: 0.0.1 + + brace-expansion@2.0.1: + dependencies: + balanced-match: 1.0.2 + + braces@3.0.3: + dependencies: + fill-range: 7.1.1 + + brotli-size@4.0.0: + dependencies: + duplexer: 0.1.1 + + brotli@1.3.3: + dependencies: + base64-js: 1.5.1 + + browserslist@4.24.4: + dependencies: + caniuse-lite: 1.0.30001733 + electron-to-chromium: 1.5.126 + node-releases: 2.0.19 + update-browserslist-db: 1.1.3(browserslist@4.24.4) + + buffer-crc32@0.2.13: {} + + buffer-from@1.1.2: {} + + builtin-modules@3.3.0: {} + + call-bind-apply-helpers@1.0.2: + dependencies: + es-errors: 1.3.0 + function-bind: 1.1.2 + + call-bind@1.0.8: + dependencies: + call-bind-apply-helpers: 1.0.2 + es-define-property: 1.0.1 + get-intrinsic: 1.3.0 + set-function-length: 1.2.2 + + call-bound@1.0.4: + dependencies: + call-bind-apply-helpers: 1.0.2 + get-intrinsic: 1.3.0 + + callsites@3.1.0: {} + + camelcase-css@2.0.1: {} + + camelcase@6.3.0: {} + + camelcase@8.0.0: {} + + caniuse-api@3.0.0: + dependencies: + browserslist: 4.24.4 + caniuse-lite: 1.0.30001733 + lodash.memoize: 4.1.2 + lodash.uniq: 4.5.0 + + caniuse-lite@1.0.30001733: {} + + ccount@2.0.1: {} + + chalk@1.1.3: + dependencies: + ansi-styles: 2.2.1 + escape-string-regexp: 1.0.5 + has-ansi: 2.0.0 + strip-ansi: 3.0.1 + supports-color: 2.0.0 + + chalk@4.1.2: + dependencies: + ansi-styles: 4.3.0 + supports-color: 7.2.0 + + chalk@5.4.1: {} + + character-entities-html4@2.1.0: {} + + character-entities-legacy@3.0.0: {} + + character-entities@2.0.2: {} + + character-reference-invalid@2.0.1: {} + + cheerio-select@2.1.0: + dependencies: + boolbase: 1.0.0 + css-select: 5.1.0 + css-what: 6.1.0 + domelementtype: 2.3.0 + domhandler: 5.0.3 + domutils: 3.2.2 + + cheerio@1.0.0: + dependencies: + cheerio-select: 2.1.0 + dom-serializer: 2.0.0 + domhandler: 5.0.3 + domutils: 3.2.2 + encoding-sniffer: 0.2.0 + htmlparser2: 9.1.0 + parse5: 7.2.1 + parse5-htmlparser2-tree-adapter: 7.1.0 + parse5-parser-stream: 7.1.2 + undici: 6.21.2 + whatwg-mimetype: 4.0.0 + + chokidar@3.6.0: + dependencies: + anymatch: 3.1.3 + braces: 3.0.3 + glob-parent: 5.1.2 + is-binary-path: 2.1.0 + is-glob: 4.0.3 + normalize-path: 3.0.0 + readdirp: 3.6.0 + optionalDependencies: + fsevents: 2.3.3 + + chokidar@4.0.3: + dependencies: + readdirp: 4.1.2 + + chownr@2.0.0: {} + + chownr@3.0.0: {} + + ci-info@4.2.0: {} + + cli-boxes@3.0.0: {} + + cliui@8.0.1: + dependencies: + string-width: 4.2.3 + strip-ansi: 6.0.1 + wrap-ansi: 7.0.0 + + clone@2.1.2: {} + + clsx@2.1.1: {} + + color-convert@2.0.1: + dependencies: + color-name: 1.1.4 + + color-name@1.1.4: {} + + color-string@1.9.1: + dependencies: + color-name: 1.1.4 + simple-swizzle: 0.2.2 + + color@4.2.3: + dependencies: + color-convert: 2.0.1 + color-string: 1.9.1 + + colord@2.9.3: {} + + combined-stream@1.0.8: + dependencies: + delayed-stream: 1.0.0 + + comma-separated-tokens@2.0.3: {} + + commander@2.20.3: {} + + commander@4.1.1: {} + + commander@7.2.0: {} + + commander@8.3.0: {} + + common-ancestor-path@1.0.1: {} + + commondir@1.0.1: {} + + concat-map@0.0.1: {} + + concat-with-sourcemaps@1.1.0: + dependencies: + source-map: 0.6.1 + + confbox@0.1.8: {} + + confbox@0.2.1: {} + + consola@3.4.2: {} + + convert-source-map@2.0.0: {} + + cookie-es@1.2.2: {} + + cookie@1.0.2: {} + + core-js-compat@3.41.0: + dependencies: + browserslist: 4.24.4 + + cosmiconfig@7.1.0: + dependencies: + '@types/parse-json': 4.0.2 + import-fresh: 3.3.1 + parse-json: 5.2.0 + path-type: 4.0.0 + yaml: 1.10.2 + + cross-fetch@3.2.0: + dependencies: + node-fetch: 2.7.0 + transitivePeerDependencies: + - encoding + + cross-spawn@7.0.6: + dependencies: + path-key: 3.1.1 + shebang-command: 2.0.0 + which: 2.0.2 + + crossws@0.3.4: + dependencies: + uncrypto: 0.1.3 + + css-declaration-sorter@6.4.1(postcss@8.5.3): + dependencies: + postcss: 8.5.3 + + css-select@4.3.0: + dependencies: + boolbase: 1.0.0 + css-what: 6.1.0 + domhandler: 4.3.1 + domutils: 2.8.0 + nth-check: 2.1.1 + + css-select@5.1.0: + dependencies: + boolbase: 1.0.0 + css-what: 6.1.0 + domhandler: 5.0.3 + domutils: 3.2.2 + nth-check: 2.1.1 + + css-selector-parser@3.1.3: {} + + css-tree@1.1.3: + dependencies: + mdn-data: 2.0.14 + source-map: 0.6.1 + + css-tree@2.2.1: + dependencies: + mdn-data: 2.0.28 + source-map-js: 1.2.1 + + css-tree@2.3.1: + dependencies: + mdn-data: 2.0.30 + source-map-js: 1.2.1 + + css-tree@3.1.0: + dependencies: + mdn-data: 2.12.2 + source-map-js: 1.2.1 + + css-what@6.1.0: {} + + cssesc@3.0.0: {} + + cssnano-preset-default@5.2.14(postcss@8.5.3): + dependencies: + css-declaration-sorter: 6.4.1(postcss@8.5.3) + cssnano-utils: 3.1.0(postcss@8.5.3) + postcss: 8.5.3 + postcss-calc: 8.2.4(postcss@8.5.3) + postcss-colormin: 5.3.1(postcss@8.5.3) + postcss-convert-values: 5.1.3(postcss@8.5.3) + postcss-discard-comments: 5.1.2(postcss@8.5.3) + postcss-discard-duplicates: 5.1.0(postcss@8.5.3) + postcss-discard-empty: 5.1.1(postcss@8.5.3) + postcss-discard-overridden: 5.1.0(postcss@8.5.3) + postcss-merge-longhand: 5.1.7(postcss@8.5.3) + postcss-merge-rules: 5.1.4(postcss@8.5.3) + postcss-minify-font-values: 5.1.0(postcss@8.5.3) + postcss-minify-gradients: 5.1.1(postcss@8.5.3) + postcss-minify-params: 5.1.4(postcss@8.5.3) + postcss-minify-selectors: 5.2.1(postcss@8.5.3) + postcss-normalize-charset: 5.1.0(postcss@8.5.3) + postcss-normalize-display-values: 5.1.0(postcss@8.5.3) + postcss-normalize-positions: 5.1.1(postcss@8.5.3) + postcss-normalize-repeat-style: 5.1.1(postcss@8.5.3) + postcss-normalize-string: 5.1.0(postcss@8.5.3) + postcss-normalize-timing-functions: 5.1.0(postcss@8.5.3) + postcss-normalize-unicode: 5.1.1(postcss@8.5.3) + postcss-normalize-url: 5.1.0(postcss@8.5.3) + postcss-normalize-whitespace: 5.1.1(postcss@8.5.3) + postcss-ordered-values: 5.1.3(postcss@8.5.3) + postcss-reduce-initial: 5.1.2(postcss@8.5.3) + postcss-reduce-transforms: 5.1.0(postcss@8.5.3) + postcss-svgo: 5.1.0(postcss@8.5.3) + postcss-unique-selectors: 5.1.1(postcss@8.5.3) + + cssnano-utils@3.1.0(postcss@8.5.3): + dependencies: + postcss: 8.5.3 + + cssnano@5.1.15(postcss@8.5.3): + dependencies: + cssnano-preset-default: 5.2.14(postcss@8.5.3) + lilconfig: 2.1.0 + postcss: 8.5.3 + yaml: 1.10.2 + + csso@4.2.0: + dependencies: + css-tree: 1.1.3 + + csso@5.0.5: + dependencies: + css-tree: 2.2.1 + + data-view-buffer@1.0.2: + dependencies: + call-bound: 1.0.4 + es-errors: 1.3.0 + is-data-view: 1.0.2 + + data-view-byte-length@1.0.2: + dependencies: + call-bound: 1.0.4 + es-errors: 1.3.0 + is-data-view: 1.0.2 + + data-view-byte-offset@1.0.1: + dependencies: + call-bound: 1.0.4 + es-errors: 1.3.0 + is-data-view: 1.0.2 + + debug@4.4.0: + dependencies: + ms: 2.1.3 + + decode-named-character-reference@1.1.0: + dependencies: + character-entities: 2.0.2 + + dedent-js@1.0.1: {} + + deepmerge@4.3.1: {} + + define-data-property@1.1.4: + dependencies: + es-define-property: 1.0.1 + es-errors: 1.3.0 + gopd: 1.2.0 + + define-lazy-prop@2.0.0: {} + + define-properties@1.2.1: + dependencies: + define-data-property: 1.1.4 + has-property-descriptors: 1.0.2 + object-keys: 1.1.1 + + defu@6.1.4: {} + + delayed-stream@1.0.0: {} + + delegate-it@6.2.1: + dependencies: + typed-query-selector: 2.12.0 + + dequal@2.0.3: {} + + destr@2.0.5: {} + + detect-libc@1.0.3: + optional: true + + detect-libc@2.0.3: {} + + detect-libc@2.0.4: {} + + deterministic-object-hash@2.0.2: + dependencies: + base-64: 1.0.0 + + devalue@5.1.1: {} + + devlop@1.1.0: + dependencies: + dequal: 2.0.3 + + dfa@1.2.0: {} + + didyoumean@1.2.2: {} + + diff@5.2.0: {} + + direction@2.0.1: {} + + dlv@1.1.3: {} + + dom-serializer@1.4.1: + dependencies: + domelementtype: 2.3.0 + domhandler: 4.3.1 + entities: 2.2.0 + + dom-serializer@2.0.0: + dependencies: + domelementtype: 2.3.0 + domhandler: 5.0.3 + entities: 4.5.0 + + domelementtype@2.3.0: {} + + domhandler@4.3.1: + dependencies: + domelementtype: 2.3.0 + + domhandler@5.0.3: + dependencies: + domelementtype: 2.3.0 + + domutils@2.8.0: + dependencies: + dom-serializer: 1.4.1 + domelementtype: 2.3.0 + domhandler: 4.3.1 + + domutils@3.2.2: + dependencies: + dom-serializer: 2.0.0 + domelementtype: 2.3.0 + domhandler: 5.0.3 + + dset@3.1.4: {} + + dunder-proto@1.0.1: + dependencies: + call-bind-apply-helpers: 1.0.2 + es-errors: 1.3.0 + gopd: 1.2.0 + + duplexer@0.1.1: {} + + duplexer@0.1.2: {} + + eastasianwidth@0.2.0: {} + + ejs@3.1.10: + dependencies: + jake: 10.9.2 + + electron-to-chromium@1.5.126: {} + + emmet@2.4.11: + dependencies: + '@emmetio/abbreviation': 2.3.3 + '@emmetio/css-abbreviation': 2.1.8 + + emoji-regex@10.4.0: {} + + emoji-regex@8.0.0: {} + + emoji-regex@9.2.2: {} + + encoding-sniffer@0.2.0: + dependencies: + iconv-lite: 0.6.3 + whatwg-encoding: 3.1.1 + + end-of-stream@1.4.4: + dependencies: + once: 1.4.0 + + entities@2.2.0: {} + + entities@4.5.0: {} + + entities@6.0.0: {} + + error-ex@1.3.2: + dependencies: + is-arrayish: 0.2.1 + + es-abstract@1.23.9: + dependencies: + array-buffer-byte-length: 1.0.2 + arraybuffer.prototype.slice: 1.0.4 + available-typed-arrays: 1.0.7 + call-bind: 1.0.8 + call-bound: 1.0.4 + data-view-buffer: 1.0.2 + data-view-byte-length: 1.0.2 + data-view-byte-offset: 1.0.1 + es-define-property: 1.0.1 + es-errors: 1.3.0 + es-object-atoms: 1.1.1 + es-set-tostringtag: 2.1.0 + es-to-primitive: 1.3.0 + function.prototype.name: 1.1.8 + get-intrinsic: 1.3.0 + get-proto: 1.0.1 + get-symbol-description: 1.1.0 + globalthis: 1.0.4 + gopd: 1.2.0 + has-property-descriptors: 1.0.2 + has-proto: 1.2.0 + has-symbols: 1.1.0 + hasown: 2.0.2 + internal-slot: 1.1.0 + is-array-buffer: 3.0.5 + is-callable: 1.2.7 + is-data-view: 1.0.2 + is-regex: 1.2.1 + is-shared-array-buffer: 1.0.4 + is-string: 1.1.1 + is-typed-array: 1.1.15 + is-weakref: 1.1.1 + math-intrinsics: 1.1.0 + object-inspect: 1.13.4 + object-keys: 1.1.1 + object.assign: 4.1.7 + own-keys: 1.0.1 + regexp.prototype.flags: 1.5.4 + safe-array-concat: 1.1.3 + safe-push-apply: 1.0.0 + safe-regex-test: 1.1.0 + set-proto: 1.0.0 + string.prototype.trim: 1.2.10 + string.prototype.trimend: 1.0.9 + string.prototype.trimstart: 1.0.8 + typed-array-buffer: 1.0.3 + typed-array-byte-length: 1.0.3 + typed-array-byte-offset: 1.0.4 + typed-array-length: 1.0.7 + unbox-primitive: 1.1.0 + which-typed-array: 1.1.19 + + es-define-property@1.0.1: {} + + es-errors@1.3.0: {} + + es-module-lexer@1.7.0: {} + + es-object-atoms@1.1.1: + dependencies: + es-errors: 1.3.0 + + es-set-tostringtag@2.1.0: + dependencies: + es-errors: 1.3.0 + get-intrinsic: 1.3.0 + has-tostringtag: 1.0.2 + hasown: 2.0.2 + + es-to-primitive@1.3.0: + dependencies: + is-callable: 1.2.7 + is-date-object: 1.1.0 + is-symbol: 1.1.1 + + esbuild@0.25.3: + optionalDependencies: + '@esbuild/aix-ppc64': 0.25.3 + '@esbuild/android-arm': 0.25.3 + '@esbuild/android-arm64': 0.25.3 + '@esbuild/android-x64': 0.25.3 + '@esbuild/darwin-arm64': 0.25.3 + '@esbuild/darwin-x64': 0.25.3 + '@esbuild/freebsd-arm64': 0.25.3 + '@esbuild/freebsd-x64': 0.25.3 + '@esbuild/linux-arm': 0.25.3 + '@esbuild/linux-arm64': 0.25.3 + '@esbuild/linux-ia32': 0.25.3 + '@esbuild/linux-loong64': 0.25.3 + '@esbuild/linux-mips64el': 0.25.3 + '@esbuild/linux-ppc64': 0.25.3 + '@esbuild/linux-riscv64': 0.25.3 + '@esbuild/linux-s390x': 0.25.3 + '@esbuild/linux-x64': 0.25.3 + '@esbuild/netbsd-arm64': 0.25.3 + '@esbuild/netbsd-x64': 0.25.3 + '@esbuild/openbsd-arm64': 0.25.3 + '@esbuild/openbsd-x64': 0.25.3 + '@esbuild/sunos-x64': 0.25.3 + '@esbuild/win32-arm64': 0.25.3 + '@esbuild/win32-ia32': 0.25.3 + '@esbuild/win32-x64': 0.25.3 + + escalade@3.2.0: {} + + escape-string-regexp@1.0.5: {} + + escape-string-regexp@4.0.0: {} + + escape-string-regexp@5.0.0: {} + + esm-env@1.2.2: {} + + esrap@1.4.6: + dependencies: + '@jridgewell/sourcemap-codec': 1.5.0 + + estree-walker@0.6.1: {} + + estree-walker@1.0.1: {} + + estree-walker@2.0.2: {} + + estree-walker@3.0.3: + dependencies: + '@types/estree': 1.0.7 + + esutils@2.0.3: {} + + eventemitter3@4.0.7: {} + + eventemitter3@5.0.1: {} + + expressive-code@0.41.3: + dependencies: + '@expressive-code/core': 0.41.3 + '@expressive-code/plugin-frames': 0.41.3 + '@expressive-code/plugin-shiki': 0.41.3 + '@expressive-code/plugin-text-markers': 0.41.3 + + exsolve@1.0.4: {} + + extend@3.0.2: {} + + extract-zip@2.0.1: + dependencies: + debug: 4.4.0 + get-stream: 5.2.0 + yauzl: 2.10.0 + optionalDependencies: + '@types/yauzl': 2.10.3 + transitivePeerDependencies: + - supports-color + + fast-deep-equal@3.1.3: {} + + fast-glob@3.3.3: + dependencies: + '@nodelib/fs.stat': 2.0.5 + '@nodelib/fs.walk': 1.2.8 + glob-parent: 5.1.2 + merge2: 1.4.1 + micromatch: 4.0.8 + + fast-json-stable-stringify@2.1.0: + optional: true + + fast-uri@3.0.6: {} + + fast-xml-parser@4.5.3: + dependencies: + strnum: 1.1.2 + + fastq@1.19.1: + dependencies: + reusify: 1.1.0 + + fd-slicer@1.1.0: + dependencies: + pend: 1.2.0 + + fdir@6.4.4(picomatch@4.0.2): + optionalDependencies: + picomatch: 4.0.2 + + figures@1.7.0: + dependencies: + escape-string-regexp: 1.0.5 + object-assign: 4.1.1 + + file-uri-to-path@1.0.0: {} + + filelist@1.0.4: + dependencies: + minimatch: 5.1.6 + + filesize@6.4.0: {} + + fill-range@7.1.1: + dependencies: + to-regex-range: 5.0.1 + + find-cache-dir@3.3.2: + dependencies: + commondir: 1.0.1 + make-dir: 3.1.0 + pkg-dir: 4.2.0 + + find-up@4.1.0: + dependencies: + locate-path: 5.0.0 + path-exists: 4.0.0 + + flattie@1.1.1: {} + + focus-options-polyfill@1.6.0: {} + + follow-redirects@1.15.9: {} + + fontkit@2.0.4: + dependencies: + '@swc/helpers': 0.5.17 + brotli: 1.3.3 + clone: 2.1.2 + dfa: 1.2.0 + fast-deep-equal: 3.1.3 + restructure: 3.0.2 + tiny-inflate: 1.0.3 + unicode-properties: 1.4.1 + unicode-trie: 2.0.0 + + for-each@0.3.5: + dependencies: + is-callable: 1.2.7 + + foreground-child@3.3.1: + dependencies: + cross-spawn: 7.0.6 + signal-exit: 4.1.0 + + form-data@4.0.2: + dependencies: + asynckit: 0.4.0 + combined-stream: 1.0.8 + es-set-tostringtag: 2.1.0 + mime-types: 2.1.35 + + fraction.js@4.3.7: {} + + fs-extra@10.1.0: + dependencies: + graceful-fs: 4.2.11 + jsonfile: 6.1.0 + universalify: 2.0.1 + + fs-minipass@2.1.0: + dependencies: + minipass: 3.3.6 + + fs.realpath@1.0.0: {} + + fsevents@2.3.3: + optional: true + + function-bind@1.1.2: {} + + function.prototype.name@1.1.8: + dependencies: + call-bind: 1.0.8 + call-bound: 1.0.4 + define-properties: 1.2.1 + functions-have-names: 1.2.3 + hasown: 2.0.2 + is-callable: 1.2.7 + + functions-have-names@1.2.3: {} + + generic-names@4.0.0: + dependencies: + loader-utils: 3.3.1 + + gensync@1.0.0-beta.2: {} + + get-caller-file@2.0.5: {} + + get-east-asian-width@1.3.0: {} + + get-intrinsic@1.3.0: + dependencies: + call-bind-apply-helpers: 1.0.2 + es-define-property: 1.0.1 + es-errors: 1.3.0 + es-object-atoms: 1.1.1 + function-bind: 1.1.2 + get-proto: 1.0.1 + gopd: 1.2.0 + has-symbols: 1.1.0 + hasown: 2.0.2 + math-intrinsics: 1.1.0 + + get-proto@1.0.1: + dependencies: + dunder-proto: 1.0.1 + es-object-atoms: 1.1.1 + + get-stream@5.2.0: + dependencies: + pump: 3.0.2 + + get-symbol-description@1.1.0: + dependencies: + call-bound: 1.0.4 + es-errors: 1.3.0 + get-intrinsic: 1.3.0 + + github-slugger@2.0.0: {} + + glob-parent@5.1.2: + dependencies: + is-glob: 4.0.3 + + glob-parent@6.0.2: + dependencies: + is-glob: 4.0.3 + + glob@10.4.5: + dependencies: + foreground-child: 3.3.1 + jackspeak: 3.4.3 + minimatch: 9.0.5 + minipass: 7.1.2 + package-json-from-dist: 1.0.1 + path-scurry: 1.11.1 + + glob@11.0.3: + dependencies: + foreground-child: 3.3.1 + jackspeak: 4.1.1 + minimatch: 10.0.3 + minipass: 7.1.2 + package-json-from-dist: 1.0.1 + path-scurry: 2.0.0 + + glob@7.2.3: + dependencies: + fs.realpath: 1.0.0 + inflight: 1.0.6 + inherits: 2.0.4 + minimatch: 3.1.2 + once: 1.4.0 + path-is-absolute: 1.0.1 + + globals@11.12.0: {} + + globals@15.15.0: {} + + globalthis@1.0.4: + dependencies: + define-properties: 1.2.1 + gopd: 1.2.0 + + globalyzer@0.1.0: {} + + globrex@0.1.2: {} + + gopd@1.2.0: {} + + graceful-fs@4.2.11: {} + + gzip-size@3.0.0: + dependencies: + duplexer: 0.1.2 + + gzip-size@6.0.0: + dependencies: + duplexer: 0.1.2 + + h3@1.15.3: + dependencies: + cookie-es: 1.2.2 + crossws: 0.3.4 + defu: 6.1.4 + destr: 2.0.5 + iron-webcrypto: 1.2.1 + node-mock-http: 1.0.0 + radix3: 1.1.2 + ufo: 1.6.1 + uncrypto: 0.1.3 + + has-ansi@2.0.0: + dependencies: + ansi-regex: 2.1.1 + + has-bigints@1.1.0: {} + + has-flag@4.0.0: {} + + has-property-descriptors@1.0.2: + dependencies: + es-define-property: 1.0.1 + + has-proto@1.2.0: + dependencies: + dunder-proto: 1.0.1 + + has-symbols@1.1.0: {} + + has-tostringtag@1.0.2: + dependencies: + has-symbols: 1.1.0 + + hasown@2.0.2: + dependencies: + function-bind: 1.1.2 + + hast-util-from-dom@5.0.1: + dependencies: + '@types/hast': 3.0.4 + hastscript: 9.0.1 + web-namespaces: 2.0.1 + + hast-util-from-html-isomorphic@2.0.0: + dependencies: + '@types/hast': 3.0.4 + hast-util-from-dom: 5.0.1 + hast-util-from-html: 2.0.3 + unist-util-remove-position: 5.0.0 + + hast-util-from-html@2.0.3: + dependencies: + '@types/hast': 3.0.4 + devlop: 1.1.0 + hast-util-from-parse5: 8.0.3 + parse5: 7.3.0 + vfile: 6.0.3 + vfile-message: 4.0.2 + + hast-util-from-parse5@8.0.3: + dependencies: + '@types/hast': 3.0.4 + '@types/unist': 3.0.3 + devlop: 1.1.0 + hastscript: 9.0.1 + property-information: 7.0.0 + vfile: 6.0.3 + vfile-location: 5.0.3 + web-namespaces: 2.0.1 + + hast-util-has-property@3.0.0: + dependencies: + '@types/hast': 3.0.4 + + hast-util-heading-rank@3.0.0: + dependencies: + '@types/hast': 3.0.4 + + hast-util-is-element@3.0.0: + dependencies: + '@types/hast': 3.0.4 + + hast-util-parse-selector@3.1.1: + dependencies: + '@types/hast': 2.3.10 + + hast-util-parse-selector@4.0.0: + dependencies: + '@types/hast': 3.0.4 + + hast-util-raw@9.1.0: + dependencies: + '@types/hast': 3.0.4 + '@types/unist': 3.0.3 + '@ungap/structured-clone': 1.3.0 + hast-util-from-parse5: 8.0.3 + hast-util-to-parse5: 8.0.0 + html-void-elements: 3.0.0 + mdast-util-to-hast: 13.2.0 + parse5: 7.3.0 + unist-util-position: 5.0.0 + unist-util-visit: 5.0.0 + vfile: 6.0.3 + web-namespaces: 2.0.1 + zwitch: 2.0.4 + + hast-util-select@6.0.4: + dependencies: + '@types/hast': 3.0.4 + '@types/unist': 3.0.3 + bcp-47-match: 2.0.3 + comma-separated-tokens: 2.0.3 + css-selector-parser: 3.1.3 + devlop: 1.1.0 + direction: 2.0.1 + hast-util-has-property: 3.0.0 + hast-util-to-string: 3.0.1 + hast-util-whitespace: 3.0.0 + nth-check: 2.1.1 + property-information: 7.0.0 + space-separated-tokens: 2.0.2 + unist-util-visit: 5.0.0 + zwitch: 2.0.4 + + hast-util-to-html@9.0.5: + dependencies: + '@types/hast': 3.0.4 + '@types/unist': 3.0.3 + ccount: 2.0.1 + comma-separated-tokens: 2.0.3 + hast-util-whitespace: 3.0.0 + html-void-elements: 3.0.0 + mdast-util-to-hast: 13.2.0 + property-information: 7.0.0 + space-separated-tokens: 2.0.2 + stringify-entities: 4.0.4 + zwitch: 2.0.4 + + hast-util-to-parse5@8.0.0: + dependencies: + '@types/hast': 3.0.4 + comma-separated-tokens: 2.0.3 + devlop: 1.1.0 + property-information: 6.5.0 + space-separated-tokens: 2.0.2 + web-namespaces: 2.0.1 + zwitch: 2.0.4 + + hast-util-to-string@3.0.1: + dependencies: + '@types/hast': 3.0.4 + + hast-util-to-text@4.0.2: + dependencies: + '@types/hast': 3.0.4 + '@types/unist': 3.0.3 + hast-util-is-element: 3.0.0 + unist-util-find-after: 5.0.0 + + hast-util-whitespace@3.0.0: + dependencies: + '@types/hast': 3.0.4 + + hastscript@7.2.0: + dependencies: + '@types/hast': 2.3.10 + comma-separated-tokens: 2.0.3 + hast-util-parse-selector: 3.1.1 + property-information: 6.5.0 + space-separated-tokens: 2.0.2 + + hastscript@9.0.1: + dependencies: + '@types/hast': 3.0.4 + comma-separated-tokens: 2.0.3 + hast-util-parse-selector: 4.0.0 + property-information: 7.0.0 + space-separated-tokens: 2.0.2 + + he@1.2.0: {} + + html-escaper@3.0.3: {} + + html-void-elements@3.0.0: {} + + htmlparser2@8.0.2: + dependencies: + domelementtype: 2.3.0 + domhandler: 5.0.3 + domutils: 3.2.2 + entities: 4.5.0 + + htmlparser2@9.1.0: + dependencies: + domelementtype: 2.3.0 + domhandler: 5.0.3 + domutils: 3.2.2 + entities: 4.5.0 + + http-cache-semantics@4.1.1: {} + + https-proxy-agent@7.0.6: + dependencies: + agent-base: 7.1.4 + debug: 4.4.0 + transitivePeerDependencies: + - supports-color + + iconv-lite@0.6.3: + dependencies: + safer-buffer: 2.1.2 + + icss-replace-symbols@1.1.0: {} + + icss-utils@5.1.0(postcss@8.5.3): + dependencies: + postcss: 8.5.3 + + immutable@4.3.7: + optional: true + + import-cwd@3.0.0: + dependencies: + import-from: 3.0.0 + + import-fresh@3.3.1: + dependencies: + parent-module: 1.0.1 + resolve-from: 4.0.0 + + import-from@3.0.0: + dependencies: + resolve-from: 5.0.0 + + import-meta-resolve@4.1.0: {} + + inflight@1.0.6: + dependencies: + once: 1.4.0 + wrappy: 1.0.2 + + inherits@2.0.4: {} + + internal-slot@1.1.0: + dependencies: + es-errors: 1.3.0 + hasown: 2.0.2 + side-channel: 1.1.0 + + interpret@1.4.0: {} + + iron-webcrypto@1.2.1: {} + + is-absolute-url@4.0.1: {} + + is-alphabetical@2.0.1: {} + + is-alphanumerical@2.0.1: + dependencies: + is-alphabetical: 2.0.1 + is-decimal: 2.0.1 + + is-array-buffer@3.0.5: + dependencies: + call-bind: 1.0.8 + call-bound: 1.0.4 + get-intrinsic: 1.3.0 + + is-arrayish@0.2.1: {} + + is-arrayish@0.3.2: {} + + is-async-function@2.1.1: + dependencies: + async-function: 1.0.0 + call-bound: 1.0.4 + get-proto: 1.0.1 + has-tostringtag: 1.0.2 + safe-regex-test: 1.1.0 + + is-bigint@1.1.0: + dependencies: + has-bigints: 1.1.0 + + is-binary-path@2.1.0: + dependencies: + binary-extensions: 2.3.0 + + is-boolean-object@1.2.2: + dependencies: + call-bound: 1.0.4 + has-tostringtag: 1.0.2 + + is-callable@1.2.7: {} + + is-core-module@2.16.1: + dependencies: + hasown: 2.0.2 + + is-data-view@1.0.2: + dependencies: + call-bound: 1.0.4 + get-intrinsic: 1.3.0 + is-typed-array: 1.1.15 + + is-date-object@1.1.0: + dependencies: + call-bound: 1.0.4 + has-tostringtag: 1.0.2 + + is-decimal@2.0.1: {} + + is-docker@2.2.1: {} + + is-docker@3.0.0: {} + + is-extglob@2.1.1: {} + + is-finalizationregistry@1.1.1: + dependencies: + call-bound: 1.0.4 + + is-fullwidth-code-point@3.0.0: {} + + is-generator-function@1.1.0: + dependencies: + call-bound: 1.0.4 + get-proto: 1.0.1 + has-tostringtag: 1.0.2 + safe-regex-test: 1.1.0 + + is-glob@4.0.3: + dependencies: + is-extglob: 2.1.1 + + is-hexadecimal@2.0.1: {} + + is-inside-container@1.0.0: + dependencies: + is-docker: 3.0.0 + + is-map@2.0.3: {} + + is-module@1.0.0: {} + + is-number-object@1.1.1: + dependencies: + call-bound: 1.0.4 + has-tostringtag: 1.0.2 + + is-number@7.0.0: {} + + is-plain-obj@4.1.0: {} + + is-plain-object@5.0.0: {} + + is-reference@1.2.1: + dependencies: + '@types/estree': 1.0.7 + + is-reference@3.0.3: + dependencies: + '@types/estree': 1.0.7 + + is-regex@1.2.1: + dependencies: + call-bound: 1.0.4 + gopd: 1.2.0 + has-tostringtag: 1.0.2 + hasown: 2.0.2 + + is-set@2.0.3: {} + + is-shared-array-buffer@1.0.4: + dependencies: + call-bound: 1.0.4 + + is-string@1.1.1: + dependencies: + call-bound: 1.0.4 + has-tostringtag: 1.0.2 + + is-symbol@1.1.1: + dependencies: + call-bound: 1.0.4 + has-symbols: 1.1.0 + safe-regex-test: 1.1.0 + + is-typed-array@1.1.15: + dependencies: + which-typed-array: 1.1.19 + + is-weakmap@2.0.2: {} + + is-weakref@1.1.1: + dependencies: + call-bound: 1.0.4 + + is-weakset@2.0.4: + dependencies: + call-bound: 1.0.4 + get-intrinsic: 1.3.0 + + is-wsl@2.2.0: + dependencies: + is-docker: 2.2.1 + + is-wsl@3.1.0: + dependencies: + is-inside-container: 1.0.0 + + isarray@2.0.5: {} + + isexe@2.0.0: {} + + jackspeak@3.4.3: + dependencies: + '@isaacs/cliui': 8.0.2 + optionalDependencies: + '@pkgjs/parseargs': 0.11.0 + + jackspeak@4.1.1: + dependencies: + '@isaacs/cliui': 8.0.2 + + jake@10.9.2: + dependencies: + async: 3.2.6 + chalk: 4.1.2 + filelist: 1.0.4 + minimatch: 3.1.2 + + jest-worker@26.6.2: + dependencies: + '@types/node': 22.14.1 + merge-stream: 2.0.0 + supports-color: 7.2.0 + + jiti@1.21.7: {} + + js-tokens@4.0.0: {} + + js-yaml@4.1.0: + dependencies: + argparse: 2.0.1 + + jsesc@3.0.2: {} + + jsesc@3.1.0: {} + + json-parse-even-better-errors@2.3.1: {} + + json-schema-traverse@0.4.1: + optional: true + + json-schema-traverse@1.0.0: {} + + json5@2.2.3: {} + + jsonc-parser@2.3.1: {} + + jsonc-parser@3.3.1: {} + + jsonfile@6.1.0: + dependencies: + universalify: 2.0.1 + optionalDependencies: + graceful-fs: 4.2.11 + + katex@0.16.22: + dependencies: + commander: 8.3.0 + + kleur@3.0.3: {} + + kleur@4.1.5: {} + + kolorist@1.8.0: {} + + lightningcss-darwin-arm64@1.29.3: + optional: true + + lightningcss-darwin-x64@1.29.3: + optional: true + + lightningcss-freebsd-x64@1.29.3: + optional: true + + lightningcss-linux-arm-gnueabihf@1.29.3: + optional: true + + lightningcss-linux-arm64-gnu@1.29.3: + optional: true + + lightningcss-linux-arm64-musl@1.29.3: + optional: true + + lightningcss-linux-x64-gnu@1.29.3: + optional: true + + lightningcss-linux-x64-musl@1.29.3: + optional: true + + lightningcss-win32-arm64-msvc@1.29.3: + optional: true + + lightningcss-win32-x64-msvc@1.29.3: + optional: true + + lightningcss@1.29.3: + dependencies: + detect-libc: 2.0.4 + optionalDependencies: + lightningcss-darwin-arm64: 1.29.3 + lightningcss-darwin-x64: 1.29.3 + lightningcss-freebsd-x64: 1.29.3 + lightningcss-linux-arm-gnueabihf: 1.29.3 + lightningcss-linux-arm64-gnu: 1.29.3 + lightningcss-linux-arm64-musl: 1.29.3 + lightningcss-linux-x64-gnu: 1.29.3 + lightningcss-linux-x64-musl: 1.29.3 + lightningcss-win32-arm64-msvc: 1.29.3 + lightningcss-win32-x64-msvc: 1.29.3 + optional: true + + lilconfig@2.1.0: {} + + lilconfig@3.1.3: {} + + lines-and-columns@1.2.4: {} + + linkify-it@5.0.0: + dependencies: + uc.micro: 2.1.0 + + loader-utils@3.3.1: {} + + local-pkg@0.5.1: + dependencies: + mlly: 1.7.4 + pkg-types: 1.3.1 + + local-pkg@1.1.1: + dependencies: + mlly: 1.7.4 + pkg-types: 2.1.0 + quansync: 0.2.10 + + locate-character@3.0.0: {} + + locate-path@5.0.0: + dependencies: + p-locate: 4.1.0 + + lodash.camelcase@4.3.0: {} + + lodash.castarray@4.4.0: {} + + lodash.debounce@4.0.8: {} + + lodash.isplainobject@4.0.6: {} + + lodash.memoize@4.1.2: {} + + lodash.merge@4.6.2: {} + + lodash.uniq@4.5.0: {} + + lodash@4.17.21: {} + + longest-streak@3.1.0: {} + + lower-case@2.0.2: + dependencies: + tslib: 2.8.1 + + lru-cache@10.4.3: {} + + lru-cache@11.2.1: {} + + lru-cache@5.1.1: + dependencies: + yallist: 3.1.1 + + magic-string@0.25.9: + dependencies: + sourcemap-codec: 1.4.8 + + magic-string@0.30.17: + dependencies: + '@jridgewell/sourcemap-codec': 1.5.0 + + magicast@0.3.5: + dependencies: + '@babel/parser': 7.27.0 + '@babel/types': 7.27.0 + source-map-js: 1.2.1 + + make-dir@3.1.0: + dependencies: + semver: 6.3.1 + + markdown-it@14.1.0: + dependencies: + argparse: 2.0.1 + entities: 4.5.0 + linkify-it: 5.0.0 + mdurl: 2.0.0 + punycode.js: 2.3.1 + uc.micro: 2.1.0 + + markdown-table@3.0.4: {} + + math-intrinsics@1.1.0: {} + + maxmin@2.1.0: + dependencies: + chalk: 1.1.3 + figures: 1.7.0 + gzip-size: 3.0.0 + pretty-bytes: 3.0.1 + + mdast-util-definitions@6.0.0: + dependencies: + '@types/mdast': 4.0.4 + '@types/unist': 3.0.3 + unist-util-visit: 5.0.0 + + mdast-util-directive@3.1.0: + dependencies: + '@types/mdast': 4.0.4 + '@types/unist': 3.0.3 + ccount: 2.0.1 + devlop: 1.1.0 + mdast-util-from-markdown: 2.0.2 + mdast-util-to-markdown: 2.1.2 + parse-entities: 4.0.2 + stringify-entities: 4.0.4 + unist-util-visit-parents: 6.0.1 + transitivePeerDependencies: + - supports-color + + mdast-util-find-and-replace@3.0.2: + dependencies: + '@types/mdast': 4.0.4 + escape-string-regexp: 5.0.0 + unist-util-is: 6.0.0 + unist-util-visit-parents: 6.0.1 + + mdast-util-from-markdown@2.0.2: + dependencies: + '@types/mdast': 4.0.4 + '@types/unist': 3.0.3 + decode-named-character-reference: 1.1.0 + devlop: 1.1.0 + mdast-util-to-string: 4.0.0 + micromark: 4.0.2 + micromark-util-decode-numeric-character-reference: 2.0.2 + micromark-util-decode-string: 2.0.1 + micromark-util-normalize-identifier: 2.0.1 + micromark-util-symbol: 2.0.1 + micromark-util-types: 2.0.2 + unist-util-stringify-position: 4.0.0 + transitivePeerDependencies: + - supports-color + + mdast-util-gfm-autolink-literal@2.0.1: + dependencies: + '@types/mdast': 4.0.4 + ccount: 2.0.1 + devlop: 1.1.0 + mdast-util-find-and-replace: 3.0.2 + micromark-util-character: 2.1.1 + + mdast-util-gfm-footnote@2.1.0: + dependencies: + '@types/mdast': 4.0.4 + devlop: 1.1.0 + mdast-util-from-markdown: 2.0.2 + mdast-util-to-markdown: 2.1.2 + micromark-util-normalize-identifier: 2.0.1 + transitivePeerDependencies: + - supports-color + + mdast-util-gfm-strikethrough@2.0.0: + dependencies: + '@types/mdast': 4.0.4 + mdast-util-from-markdown: 2.0.2 + mdast-util-to-markdown: 2.1.2 + transitivePeerDependencies: + - supports-color + + mdast-util-gfm-table@2.0.0: + dependencies: + '@types/mdast': 4.0.4 + devlop: 1.1.0 + markdown-table: 3.0.4 + mdast-util-from-markdown: 2.0.2 + mdast-util-to-markdown: 2.1.2 + transitivePeerDependencies: + - supports-color + + mdast-util-gfm-task-list-item@2.0.0: + dependencies: + '@types/mdast': 4.0.4 + devlop: 1.1.0 + mdast-util-from-markdown: 2.0.2 + mdast-util-to-markdown: 2.1.2 + transitivePeerDependencies: + - supports-color + + mdast-util-gfm@3.1.0: + dependencies: + mdast-util-from-markdown: 2.0.2 + mdast-util-gfm-autolink-literal: 2.0.1 + mdast-util-gfm-footnote: 2.1.0 + mdast-util-gfm-strikethrough: 2.0.0 + mdast-util-gfm-table: 2.0.0 + mdast-util-gfm-task-list-item: 2.0.0 + mdast-util-to-markdown: 2.1.2 + transitivePeerDependencies: + - supports-color + + mdast-util-math@3.0.0: + dependencies: + '@types/hast': 3.0.4 + '@types/mdast': 4.0.4 + devlop: 1.1.0 + longest-streak: 3.1.0 + mdast-util-from-markdown: 2.0.2 + mdast-util-to-markdown: 2.1.2 + unist-util-remove-position: 5.0.0 + transitivePeerDependencies: + - supports-color + + mdast-util-phrasing@4.1.0: + dependencies: + '@types/mdast': 4.0.4 + unist-util-is: 6.0.0 + + mdast-util-to-hast@13.2.0: + dependencies: + '@types/hast': 3.0.4 + '@types/mdast': 4.0.4 + '@ungap/structured-clone': 1.3.0 + devlop: 1.1.0 + micromark-util-sanitize-uri: 2.0.1 + trim-lines: 3.0.1 + unist-util-position: 5.0.0 + unist-util-visit: 5.0.0 + vfile: 6.0.3 + + mdast-util-to-markdown@2.1.2: + dependencies: + '@types/mdast': 4.0.4 + '@types/unist': 3.0.3 + longest-streak: 3.1.0 + mdast-util-phrasing: 4.1.0 + mdast-util-to-string: 4.0.0 + micromark-util-classify-character: 2.0.1 + micromark-util-decode-string: 2.0.1 + unist-util-visit: 5.0.0 + zwitch: 2.0.4 + + mdast-util-to-string@4.0.0: + dependencies: + '@types/mdast': 4.0.4 + + mdn-data@2.0.14: {} + + mdn-data@2.0.28: {} + + mdn-data@2.0.30: {} + + mdn-data@2.12.2: {} + + mdurl@2.0.0: {} + + merge-stream@2.0.0: {} + + merge2@1.4.1: {} + + microbundle@0.15.1(@types/babel__core@7.20.5): + dependencies: + '@babel/core': 7.26.10 + '@babel/plugin-proposal-class-properties': 7.12.1(@babel/core@7.26.10) + '@babel/plugin-syntax-import-meta': 7.10.4(@babel/core@7.26.10) + '@babel/plugin-syntax-jsx': 7.25.9(@babel/core@7.26.10) + '@babel/plugin-transform-flow-strip-types': 7.26.5(@babel/core@7.26.10) + '@babel/plugin-transform-react-jsx': 7.25.9(@babel/core@7.26.10) + '@babel/plugin-transform-regenerator': 7.27.0(@babel/core@7.26.10) + '@babel/preset-env': 7.26.9(@babel/core@7.26.10) + '@babel/preset-flow': 7.25.9(@babel/core@7.26.10) + '@babel/preset-react': 7.26.3(@babel/core@7.26.10) + '@rollup/plugin-alias': 3.1.9(rollup@2.79.2) + '@rollup/plugin-babel': 5.3.1(@babel/core@7.26.10)(@types/babel__core@7.20.5)(rollup@2.79.2) + '@rollup/plugin-commonjs': 17.1.0(rollup@2.79.2) + '@rollup/plugin-json': 4.1.0(rollup@2.79.2) + '@rollup/plugin-node-resolve': 11.2.1(rollup@2.79.2) + '@surma/rollup-plugin-off-main-thread': 2.2.3 + asyncro: 3.0.0 + autoprefixer: 10.4.21(postcss@8.5.3) + babel-plugin-macros: 3.1.0 + babel-plugin-transform-async-to-promises: 0.8.18 + babel-plugin-transform-replace-expressions: 0.2.0(@babel/core@7.26.10) + brotli-size: 4.0.0 + builtin-modules: 3.3.0 + camelcase: 6.3.0 + escape-string-regexp: 4.0.0 + filesize: 6.4.0 + gzip-size: 6.0.0 + kleur: 4.1.5 + lodash.merge: 4.6.2 + postcss: 8.5.3 + pretty-bytes: 5.6.0 + rollup: 2.79.2 + rollup-plugin-bundle-size: 1.0.3 + rollup-plugin-postcss: 4.0.2(postcss@8.5.3) + rollup-plugin-terser: 7.0.2(rollup@2.79.2) + rollup-plugin-typescript2: 0.32.1(rollup@2.79.2)(typescript@4.9.5) + rollup-plugin-visualizer: 5.14.0(rollup@2.79.2) + sade: 1.8.1 + terser: 5.39.0 + tiny-glob: 0.2.9 + tslib: 2.8.1 + typescript: 4.9.5 + transitivePeerDependencies: + - '@types/babel__core' + - rolldown + - supports-color + - ts-node + + micromark-core-commonmark@2.0.3: + dependencies: + decode-named-character-reference: 1.1.0 + devlop: 1.1.0 + micromark-factory-destination: 2.0.1 + micromark-factory-label: 2.0.1 + micromark-factory-space: 2.0.1 + micromark-factory-title: 2.0.1 + micromark-factory-whitespace: 2.0.1 + micromark-util-character: 2.1.1 + micromark-util-chunked: 2.0.1 + micromark-util-classify-character: 2.0.1 + micromark-util-html-tag-name: 2.0.1 + micromark-util-normalize-identifier: 2.0.1 + micromark-util-resolve-all: 2.0.1 + micromark-util-subtokenize: 2.1.0 + micromark-util-symbol: 2.0.1 + micromark-util-types: 2.0.2 + + micromark-extension-directive@3.0.2: + dependencies: + devlop: 1.1.0 + micromark-factory-space: 2.0.1 + micromark-factory-whitespace: 2.0.1 + micromark-util-character: 2.1.1 + micromark-util-symbol: 2.0.1 + micromark-util-types: 2.0.2 + parse-entities: 4.0.2 + + micromark-extension-gfm-autolink-literal@2.1.0: + dependencies: + micromark-util-character: 2.1.1 + micromark-util-sanitize-uri: 2.0.1 + micromark-util-symbol: 2.0.1 + micromark-util-types: 2.0.2 + + micromark-extension-gfm-footnote@2.1.0: + dependencies: + devlop: 1.1.0 + micromark-core-commonmark: 2.0.3 + micromark-factory-space: 2.0.1 + micromark-util-character: 2.1.1 + micromark-util-normalize-identifier: 2.0.1 + micromark-util-sanitize-uri: 2.0.1 + micromark-util-symbol: 2.0.1 + micromark-util-types: 2.0.2 + + micromark-extension-gfm-strikethrough@2.1.0: + dependencies: + devlop: 1.1.0 + micromark-util-chunked: 2.0.1 + micromark-util-classify-character: 2.0.1 + micromark-util-resolve-all: 2.0.1 + micromark-util-symbol: 2.0.1 + micromark-util-types: 2.0.2 + + micromark-extension-gfm-table@2.1.1: + dependencies: + devlop: 1.1.0 + micromark-factory-space: 2.0.1 + micromark-util-character: 2.1.1 + micromark-util-symbol: 2.0.1 + micromark-util-types: 2.0.2 + + micromark-extension-gfm-tagfilter@2.0.0: + dependencies: + micromark-util-types: 2.0.2 + + micromark-extension-gfm-task-list-item@2.1.0: + dependencies: + devlop: 1.1.0 + micromark-factory-space: 2.0.1 + micromark-util-character: 2.1.1 + micromark-util-symbol: 2.0.1 + micromark-util-types: 2.0.2 + + micromark-extension-gfm@3.0.0: + dependencies: + micromark-extension-gfm-autolink-literal: 2.1.0 + micromark-extension-gfm-footnote: 2.1.0 + micromark-extension-gfm-strikethrough: 2.1.0 + micromark-extension-gfm-table: 2.1.1 + micromark-extension-gfm-tagfilter: 2.0.0 + micromark-extension-gfm-task-list-item: 2.1.0 + micromark-util-combine-extensions: 2.0.1 + micromark-util-types: 2.0.2 + + micromark-extension-math@3.1.0: + dependencies: + '@types/katex': 0.16.7 + devlop: 1.1.0 + katex: 0.16.22 + micromark-factory-space: 2.0.1 + micromark-util-character: 2.1.1 + micromark-util-symbol: 2.0.1 + micromark-util-types: 2.0.2 + + micromark-factory-destination@2.0.1: + dependencies: + micromark-util-character: 2.1.1 + micromark-util-symbol: 2.0.1 + micromark-util-types: 2.0.2 + + micromark-factory-label@2.0.1: + dependencies: + devlop: 1.1.0 + micromark-util-character: 2.1.1 + micromark-util-symbol: 2.0.1 + micromark-util-types: 2.0.2 + + micromark-factory-space@2.0.1: + dependencies: + micromark-util-character: 2.1.1 + micromark-util-types: 2.0.2 + + micromark-factory-title@2.0.1: + dependencies: + micromark-factory-space: 2.0.1 + micromark-util-character: 2.1.1 + micromark-util-symbol: 2.0.1 + micromark-util-types: 2.0.2 + + micromark-factory-whitespace@2.0.1: + dependencies: + micromark-factory-space: 2.0.1 + micromark-util-character: 2.1.1 + micromark-util-symbol: 2.0.1 + micromark-util-types: 2.0.2 + + micromark-util-character@2.1.1: + dependencies: + micromark-util-symbol: 2.0.1 + micromark-util-types: 2.0.2 + + micromark-util-chunked@2.0.1: + dependencies: + micromark-util-symbol: 2.0.1 + + micromark-util-classify-character@2.0.1: + dependencies: + micromark-util-character: 2.1.1 + micromark-util-symbol: 2.0.1 + micromark-util-types: 2.0.2 + + micromark-util-combine-extensions@2.0.1: + dependencies: + micromark-util-chunked: 2.0.1 + micromark-util-types: 2.0.2 + + micromark-util-decode-numeric-character-reference@2.0.2: + dependencies: + micromark-util-symbol: 2.0.1 + + micromark-util-decode-string@2.0.1: + dependencies: + decode-named-character-reference: 1.1.0 + micromark-util-character: 2.1.1 + micromark-util-decode-numeric-character-reference: 2.0.2 + micromark-util-symbol: 2.0.1 + + micromark-util-encode@2.0.1: {} + + micromark-util-html-tag-name@2.0.1: {} + + micromark-util-normalize-identifier@2.0.1: + dependencies: + micromark-util-symbol: 2.0.1 + + micromark-util-resolve-all@2.0.1: + dependencies: + micromark-util-types: 2.0.2 + + micromark-util-sanitize-uri@2.0.1: + dependencies: + micromark-util-character: 2.1.1 + micromark-util-encode: 2.0.1 + micromark-util-symbol: 2.0.1 + + micromark-util-subtokenize@2.1.0: + dependencies: + devlop: 1.1.0 + micromark-util-chunked: 2.0.1 + micromark-util-symbol: 2.0.1 + micromark-util-types: 2.0.2 + + micromark-util-symbol@2.0.1: {} + + micromark-util-types@2.0.2: {} + + micromark@4.0.2: + dependencies: + '@types/debug': 4.1.12 + debug: 4.4.0 + decode-named-character-reference: 1.1.0 + devlop: 1.1.0 + micromark-core-commonmark: 2.0.3 + micromark-factory-space: 2.0.1 + micromark-util-character: 2.1.1 + micromark-util-chunked: 2.0.1 + micromark-util-combine-extensions: 2.0.1 + micromark-util-decode-numeric-character-reference: 2.0.2 + micromark-util-encode: 2.0.1 + micromark-util-normalize-identifier: 2.0.1 + micromark-util-resolve-all: 2.0.1 + micromark-util-sanitize-uri: 2.0.1 + micromark-util-subtokenize: 2.1.0 + micromark-util-symbol: 2.0.1 + micromark-util-types: 2.0.2 + transitivePeerDependencies: + - supports-color + + micromatch@4.0.8: + dependencies: + braces: 3.0.3 + picomatch: 2.3.1 + + mime-db@1.52.0: {} + + mime-types@2.1.35: + dependencies: + mime-db: 1.52.0 + + minimatch@10.0.3: + dependencies: + '@isaacs/brace-expansion': 5.0.0 + + minimatch@3.1.2: + dependencies: + brace-expansion: 1.1.11 + + minimatch@5.1.6: + dependencies: + brace-expansion: 2.0.1 + + minimatch@9.0.5: + dependencies: + brace-expansion: 2.0.1 + + minipass@3.3.6: + dependencies: + yallist: 4.0.0 + + minipass@4.2.8: {} + + minipass@5.0.0: {} + + minipass@7.1.2: {} + + minizlib@2.1.2: + dependencies: + minipass: 3.3.6 + yallist: 4.0.0 + + minizlib@3.0.2: + dependencies: + minipass: 7.1.2 + + mkdirp@1.0.4: {} + + mkdirp@3.0.1: {} + + mlly@1.7.4: + dependencies: + acorn: 8.14.1 + pathe: 2.0.3 + pkg-types: 1.3.1 + ufo: 1.6.1 + + morphdom@2.7.4: {} + + mri@1.2.0: {} + + mrmime@2.0.1: {} + + ms@2.1.3: {} + + muggle-string@0.4.1: {} + + mz@2.7.0: + dependencies: + any-promise: 1.3.0 + object-assign: 4.1.1 + thenify-all: 1.6.0 + + nanoid@3.3.11: {} + + neotraverse@0.6.18: {} + + nlcst-to-string@4.0.0: + dependencies: + '@types/nlcst': 2.0.3 + + no-case@3.0.4: + dependencies: + lower-case: 2.0.2 + tslib: 2.8.1 + + node-addon-api@7.1.1: + optional: true + + node-fetch-native@1.6.6: {} + + node-fetch@2.7.0: + dependencies: + whatwg-url: 5.0.0 + + node-gyp-build@4.8.4: {} + + node-html-parser@7.0.1: + dependencies: + css-select: 5.1.0 + he: 1.2.0 + + node-mock-http@1.0.0: {} + + node-releases@2.0.19: {} + + nopt@8.1.0: + dependencies: + abbrev: 3.0.1 + + normalize-path@3.0.0: {} + + normalize-range@0.1.2: {} + + normalize-url@6.1.0: {} + + nth-check@2.1.1: + dependencies: + boolbase: 1.0.0 + + number-is-nan@1.0.1: {} + + object-assign@4.1.1: {} + + object-hash@3.0.0: {} + + object-inspect@1.13.4: {} + + object-keys@1.1.1: {} + + object.assign@4.1.7: + dependencies: + call-bind: 1.0.8 + call-bound: 1.0.4 + define-properties: 1.2.1 + es-object-atoms: 1.1.1 + has-symbols: 1.1.0 + object-keys: 1.1.1 + + ofetch@1.4.1: + dependencies: + destr: 2.0.5 + node-fetch-native: 1.6.6 + ufo: 1.6.1 + + ohash@2.0.11: {} + + once@1.4.0: + dependencies: + wrappy: 1.0.2 + + oniguruma-parser@0.12.0: {} + + oniguruma-to-es@4.3.1: + dependencies: + oniguruma-parser: 0.12.0 + regex: 6.0.1 + regex-recursion: 6.0.2 + + open@8.4.2: + dependencies: + define-lazy-prop: 2.0.0 + is-docker: 2.2.1 + is-wsl: 2.2.0 + + opencollective-postinstall@2.0.3: {} + + overlayscrollbars@2.11.1: {} + + own-keys@1.0.1: + dependencies: + get-intrinsic: 1.3.0 + object-keys: 1.1.1 + safe-push-apply: 1.0.0 + + p-finally@1.0.0: {} + + p-limit@2.3.0: + dependencies: + p-try: 2.2.0 + + p-limit@6.2.0: + dependencies: + yocto-queue: 1.2.1 + + p-locate@4.1.0: + dependencies: + p-limit: 2.3.0 + + p-queue@6.6.2: + dependencies: + eventemitter3: 4.0.7 + p-timeout: 3.2.0 + + p-queue@8.1.0: + dependencies: + eventemitter3: 5.0.1 + p-timeout: 6.1.4 + + p-timeout@3.2.0: + dependencies: + p-finally: 1.0.0 + + p-timeout@6.1.4: {} + + p-try@2.2.0: {} + + package-json-from-dist@1.0.1: {} + + package-manager-detector@0.2.11: + dependencies: + quansync: 0.2.10 + + package-manager-detector@1.2.0: {} + + pako@0.2.9: {} + + parent-module@1.0.1: + dependencies: + callsites: 3.1.0 + + parse-entities@4.0.2: + dependencies: + '@types/unist': 2.0.11 + character-entities-legacy: 3.0.0 + character-reference-invalid: 2.0.1 + decode-named-character-reference: 1.1.0 + is-alphanumerical: 2.0.1 + is-decimal: 2.0.1 + is-hexadecimal: 2.0.1 + + parse-json@5.2.0: + dependencies: + '@babel/code-frame': 7.26.2 + error-ex: 1.3.2 + json-parse-even-better-errors: 2.3.1 + lines-and-columns: 1.2.4 + + parse-latin@7.0.0: + dependencies: + '@types/nlcst': 2.0.3 + '@types/unist': 3.0.3 + nlcst-to-string: 4.0.0 + unist-util-modify-children: 4.0.0 + unist-util-visit-children: 3.0.0 + vfile: 6.0.3 + + parse-srcset@1.0.2: {} + + parse5-htmlparser2-tree-adapter@7.1.0: + dependencies: + domhandler: 5.0.3 + parse5: 7.2.1 + + parse5-parser-stream@7.1.2: + dependencies: + parse5: 7.2.1 + + parse5@7.2.1: + dependencies: + entities: 4.5.0 + + parse5@7.3.0: + dependencies: + entities: 6.0.0 + + pascal-case@3.1.2: + dependencies: + no-case: 3.0.4 + tslib: 2.8.1 + + path-browserify@1.0.1: {} + + path-exists@4.0.0: {} + + path-is-absolute@1.0.1: {} + + path-key@3.1.1: {} + + path-parse@1.0.7: {} + + path-scurry@1.11.1: + dependencies: + lru-cache: 10.4.3 + minipass: 7.1.2 + + path-scurry@2.0.0: + dependencies: + lru-cache: 11.2.1 + minipass: 7.1.2 + + path-to-regexp@6.1.0: {} + + path-to-regexp@6.3.0: {} + + path-type@4.0.0: {} + + pathe@1.1.2: {} + + pathe@2.0.3: {} + + pend@1.2.0: {} + + photoswipe@5.4.4: {} + + picocolors@1.1.1: {} + + picomatch@2.3.1: {} + + picomatch@4.0.2: {} + + pify@2.3.0: {} + + pify@5.0.0: {} + + pirates@4.0.7: {} + + pkg-dir@4.2.0: + dependencies: + find-up: 4.1.0 + + pkg-types@1.3.1: + dependencies: + confbox: 0.1.8 + mlly: 1.7.4 + pathe: 2.0.3 + + pkg-types@2.1.0: + dependencies: + confbox: 0.2.1 + exsolve: 1.0.4 + pathe: 2.0.3 + + possible-typed-array-names@1.1.0: {} + + postcss-calc@8.2.4(postcss@8.5.3): + dependencies: + postcss: 8.5.3 + postcss-selector-parser: 6.1.2 + postcss-value-parser: 4.2.0 + + postcss-colormin@5.3.1(postcss@8.5.3): + dependencies: + browserslist: 4.24.4 + caniuse-api: 3.0.0 + colord: 2.9.3 + postcss: 8.5.3 + postcss-value-parser: 4.2.0 + + postcss-convert-values@5.1.3(postcss@8.5.3): + dependencies: + browserslist: 4.24.4 + postcss: 8.5.3 + postcss-value-parser: 4.2.0 + + postcss-discard-comments@5.1.2(postcss@8.5.3): + dependencies: + postcss: 8.5.3 + + postcss-discard-duplicates@5.1.0(postcss@8.5.3): + dependencies: + postcss: 8.5.3 + + postcss-discard-empty@5.1.1(postcss@8.5.3): + dependencies: + postcss: 8.5.3 + + postcss-discard-overridden@5.1.0(postcss@8.5.3): + dependencies: + postcss: 8.5.3 + + postcss-import@15.1.0(postcss@8.5.3): + dependencies: + postcss: 8.5.3 + postcss-value-parser: 4.2.0 + read-cache: 1.0.0 + resolve: 1.22.10 + + postcss-import@16.1.0(postcss@8.5.3): + dependencies: + postcss: 8.5.3 + postcss-value-parser: 4.2.0 + read-cache: 1.0.0 + resolve: 1.22.10 + + postcss-js@4.0.1(postcss@8.5.3): + dependencies: + camelcase-css: 2.0.1 + postcss: 8.5.3 + + postcss-load-config@3.1.4(postcss@8.5.3): + dependencies: + lilconfig: 2.1.0 + yaml: 1.10.2 + optionalDependencies: + postcss: 8.5.3 + + postcss-load-config@4.0.2(postcss@8.5.3): + dependencies: + lilconfig: 3.1.3 + yaml: 2.7.0 + optionalDependencies: + postcss: 8.5.3 + + postcss-merge-longhand@5.1.7(postcss@8.5.3): + dependencies: + postcss: 8.5.3 + postcss-value-parser: 4.2.0 + stylehacks: 5.1.1(postcss@8.5.3) + + postcss-merge-rules@5.1.4(postcss@8.5.3): + dependencies: + browserslist: 4.24.4 + caniuse-api: 3.0.0 + cssnano-utils: 3.1.0(postcss@8.5.3) + postcss: 8.5.3 + postcss-selector-parser: 6.1.2 + + postcss-minify-font-values@5.1.0(postcss@8.5.3): + dependencies: + postcss: 8.5.3 + postcss-value-parser: 4.2.0 + + postcss-minify-gradients@5.1.1(postcss@8.5.3): + dependencies: + colord: 2.9.3 + cssnano-utils: 3.1.0(postcss@8.5.3) + postcss: 8.5.3 + postcss-value-parser: 4.2.0 + + postcss-minify-params@5.1.4(postcss@8.5.3): + dependencies: + browserslist: 4.24.4 + cssnano-utils: 3.1.0(postcss@8.5.3) + postcss: 8.5.3 + postcss-value-parser: 4.2.0 + + postcss-minify-selectors@5.2.1(postcss@8.5.3): + dependencies: + postcss: 8.5.3 + postcss-selector-parser: 6.1.2 + + postcss-modules-extract-imports@3.1.0(postcss@8.5.3): + dependencies: + postcss: 8.5.3 + + postcss-modules-local-by-default@4.2.0(postcss@8.5.3): + dependencies: + icss-utils: 5.1.0(postcss@8.5.3) + postcss: 8.5.3 + postcss-selector-parser: 7.1.0 + postcss-value-parser: 4.2.0 + + postcss-modules-scope@3.2.1(postcss@8.5.3): + dependencies: + postcss: 8.5.3 + postcss-selector-parser: 7.1.0 + + postcss-modules-values@4.0.0(postcss@8.5.3): + dependencies: + icss-utils: 5.1.0(postcss@8.5.3) + postcss: 8.5.3 + + postcss-modules@4.3.1(postcss@8.5.3): + dependencies: + generic-names: 4.0.0 + icss-replace-symbols: 1.1.0 + lodash.camelcase: 4.3.0 + postcss: 8.5.3 + postcss-modules-extract-imports: 3.1.0(postcss@8.5.3) + postcss-modules-local-by-default: 4.2.0(postcss@8.5.3) + postcss-modules-scope: 3.2.1(postcss@8.5.3) + postcss-modules-values: 4.0.0(postcss@8.5.3) + string-hash: 1.1.3 + + postcss-nested@6.2.0(postcss@8.5.3): + dependencies: + postcss: 8.5.3 + postcss-selector-parser: 6.1.2 + + postcss-nesting@13.0.1(postcss@8.5.3): + dependencies: + '@csstools/selector-resolve-nested': 3.0.0(postcss-selector-parser@7.1.0) + '@csstools/selector-specificity': 5.0.0(postcss-selector-parser@7.1.0) + postcss: 8.5.3 + postcss-selector-parser: 7.1.0 + + postcss-normalize-charset@5.1.0(postcss@8.5.3): + dependencies: + postcss: 8.5.3 + + postcss-normalize-display-values@5.1.0(postcss@8.5.3): + dependencies: + postcss: 8.5.3 + postcss-value-parser: 4.2.0 + + postcss-normalize-positions@5.1.1(postcss@8.5.3): + dependencies: + postcss: 8.5.3 + postcss-value-parser: 4.2.0 + + postcss-normalize-repeat-style@5.1.1(postcss@8.5.3): + dependencies: + postcss: 8.5.3 + postcss-value-parser: 4.2.0 + + postcss-normalize-string@5.1.0(postcss@8.5.3): + dependencies: + postcss: 8.5.3 + postcss-value-parser: 4.2.0 + + postcss-normalize-timing-functions@5.1.0(postcss@8.5.3): + dependencies: + postcss: 8.5.3 + postcss-value-parser: 4.2.0 + + postcss-normalize-unicode@5.1.1(postcss@8.5.3): + dependencies: + browserslist: 4.24.4 + postcss: 8.5.3 + postcss-value-parser: 4.2.0 + + postcss-normalize-url@5.1.0(postcss@8.5.3): + dependencies: + normalize-url: 6.1.0 + postcss: 8.5.3 + postcss-value-parser: 4.2.0 + + postcss-normalize-whitespace@5.1.1(postcss@8.5.3): + dependencies: + postcss: 8.5.3 + postcss-value-parser: 4.2.0 + + postcss-ordered-values@5.1.3(postcss@8.5.3): + dependencies: + cssnano-utils: 3.1.0(postcss@8.5.3) + postcss: 8.5.3 + postcss-value-parser: 4.2.0 + + postcss-reduce-initial@5.1.2(postcss@8.5.3): + dependencies: + browserslist: 4.24.4 + caniuse-api: 3.0.0 + postcss: 8.5.3 + + postcss-reduce-transforms@5.1.0(postcss@8.5.3): + dependencies: + postcss: 8.5.3 + postcss-value-parser: 4.2.0 + + postcss-selector-parser@6.0.10: + dependencies: + cssesc: 3.0.0 + util-deprecate: 1.0.2 + + postcss-selector-parser@6.1.2: + dependencies: + cssesc: 3.0.0 + util-deprecate: 1.0.2 + + postcss-selector-parser@7.1.0: + dependencies: + cssesc: 3.0.0 + util-deprecate: 1.0.2 + + postcss-svgo@5.1.0(postcss@8.5.3): + dependencies: + postcss: 8.5.3 + postcss-value-parser: 4.2.0 + svgo: 2.8.0 + + postcss-unique-selectors@5.1.1(postcss@8.5.3): + dependencies: + postcss: 8.5.3 + postcss-selector-parser: 6.1.2 + + postcss-value-parser@4.2.0: {} + + postcss@8.5.3: + dependencies: + nanoid: 3.3.11 + picocolors: 1.1.1 + source-map-js: 1.2.1 + + prettier@2.8.7: + optional: true + + prettier@2.8.8: {} + + pretty-bytes@3.0.1: + dependencies: + number-is-nan: 1.0.1 + + pretty-bytes@5.6.0: {} + + prismjs@1.30.0: {} + + promise.series@0.2.0: {} + + prompts@2.4.2: + dependencies: + kleur: 3.0.3 + sisteransi: 1.0.5 + + property-information@6.5.0: {} + + property-information@7.0.0: {} + + proxy-from-env@1.1.0: {} + + pump@3.0.2: + dependencies: + end-of-stream: 1.4.4 + once: 1.4.0 + + punycode.js@2.3.1: {} + + punycode@2.3.1: + optional: true + + quansync@0.2.10: {} + + queue-microtask@1.2.3: {} + + radix3@1.1.2: {} + + randombytes@2.1.0: + dependencies: + safe-buffer: 5.2.1 + + read-cache@1.0.0: + dependencies: + pify: 2.3.0 + + readdirp@3.6.0: + dependencies: + picomatch: 2.3.1 + + readdirp@4.1.2: {} + + reading-time@1.5.0: {} + + rechoir@0.6.2: + dependencies: + resolve: 1.22.10 + + reflect.getprototypeof@1.0.10: + dependencies: + call-bind: 1.0.8 + define-properties: 1.2.1 + es-abstract: 1.23.9 + es-errors: 1.3.0 + es-object-atoms: 1.1.1 + get-intrinsic: 1.3.0 + get-proto: 1.0.1 + which-builtin-type: 1.2.1 + + regenerate-unicode-properties@10.2.0: + dependencies: + regenerate: 1.4.2 + + regenerate@1.4.2: {} + + regenerator-runtime@0.14.1: {} + + regenerator-transform@0.15.2: + dependencies: + '@babel/runtime': 7.27.0 + + regex-recursion@6.0.2: + dependencies: + regex-utilities: 2.3.0 + + regex-utilities@2.3.0: {} + + regex@6.0.1: + dependencies: + regex-utilities: 2.3.0 + + regexp.prototype.flags@1.5.4: + dependencies: + call-bind: 1.0.8 + define-properties: 1.2.1 + es-errors: 1.3.0 + get-proto: 1.0.1 + gopd: 1.2.0 + set-function-name: 2.0.2 + + regexpu-core@6.2.0: + dependencies: + regenerate: 1.4.2 + regenerate-unicode-properties: 10.2.0 + regjsgen: 0.8.0 + regjsparser: 0.12.0 + unicode-match-property-ecmascript: 2.0.0 + unicode-match-property-value-ecmascript: 2.2.0 + + regjsgen@0.8.0: {} + + regjsparser@0.12.0: + dependencies: + jsesc: 3.0.2 + + rehype-autolink-headings@7.1.0: + dependencies: + '@types/hast': 3.0.4 + '@ungap/structured-clone': 1.3.0 + hast-util-heading-rank: 3.0.0 + hast-util-is-element: 3.0.0 + unified: 11.0.5 + unist-util-visit: 5.0.0 + + rehype-components@0.3.0: + dependencies: + hast-util-is-element: 3.0.0 + unist-util-visit: 5.0.0 + + rehype-expressive-code@0.41.3: + dependencies: + expressive-code: 0.41.3 + + rehype-external-links@3.0.0: + dependencies: + '@types/hast': 3.0.4 + '@ungap/structured-clone': 1.3.0 + hast-util-is-element: 3.0.0 + is-absolute-url: 4.0.1 + space-separated-tokens: 2.0.2 + unist-util-visit: 5.0.0 + + rehype-katex@7.0.1: + dependencies: + '@types/hast': 3.0.4 + '@types/katex': 0.16.7 + hast-util-from-html-isomorphic: 2.0.0 + hast-util-to-text: 4.0.2 + katex: 0.16.22 + unist-util-visit-parents: 6.0.1 + vfile: 6.0.3 + + rehype-parse@9.0.1: + dependencies: + '@types/hast': 3.0.4 + hast-util-from-html: 2.0.3 + unified: 11.0.5 + + rehype-raw@7.0.0: + dependencies: + '@types/hast': 3.0.4 + hast-util-raw: 9.1.0 + vfile: 6.0.3 + + rehype-slug@6.0.0: + dependencies: + '@types/hast': 3.0.4 + github-slugger: 2.0.0 + hast-util-heading-rank: 3.0.0 + hast-util-to-string: 3.0.1 + unist-util-visit: 5.0.0 + + rehype-stringify@10.0.1: + dependencies: + '@types/hast': 3.0.4 + hast-util-to-html: 9.0.5 + unified: 11.0.5 + + rehype@13.0.2: + dependencies: + '@types/hast': 3.0.4 + rehype-parse: 9.0.1 + rehype-stringify: 10.0.1 + unified: 11.0.5 + + remark-directive-rehype@0.4.2: + dependencies: + hastscript: 7.2.0 + unist-util-map: 3.1.3 + + remark-directive@3.0.1: + dependencies: + '@types/mdast': 4.0.4 + mdast-util-directive: 3.1.0 + micromark-extension-directive: 3.0.2 + unified: 11.0.5 + transitivePeerDependencies: + - supports-color + + remark-gfm@4.0.1: + dependencies: + '@types/mdast': 4.0.4 + mdast-util-gfm: 3.1.0 + micromark-extension-gfm: 3.0.0 + remark-parse: 11.0.0 + remark-stringify: 11.0.0 + unified: 11.0.5 + transitivePeerDependencies: + - supports-color + + remark-github-admonitions-to-directives@1.0.5: + dependencies: + '@types/mdast': 4.0.4 + mdast-util-directive: 3.1.0 + unified: 11.0.5 + unist-util-visit: 5.0.0 + transitivePeerDependencies: + - supports-color + + remark-math@6.0.0: + dependencies: + '@types/mdast': 4.0.4 + mdast-util-math: 3.0.0 + micromark-extension-math: 3.1.0 + unified: 11.0.5 + transitivePeerDependencies: + - supports-color + + remark-parse@11.0.0: + dependencies: + '@types/mdast': 4.0.4 + mdast-util-from-markdown: 2.0.2 + micromark-util-types: 2.0.2 + unified: 11.0.5 + transitivePeerDependencies: + - supports-color + + remark-rehype@11.1.2: + dependencies: + '@types/hast': 3.0.4 + '@types/mdast': 4.0.4 + mdast-util-to-hast: 13.2.0 + unified: 11.0.5 + vfile: 6.0.3 + + remark-sectionize@2.1.0: + dependencies: + unist-util-find-after: 4.0.1 + unist-util-visit: 4.1.2 + + remark-smartypants@3.0.2: + dependencies: + retext: 9.0.0 + retext-smartypants: 6.2.0 + unified: 11.0.5 + unist-util-visit: 5.0.0 + + remark-stringify@11.0.0: + dependencies: + '@types/mdast': 4.0.4 + mdast-util-to-markdown: 2.1.2 + unified: 11.0.5 + + request-light@0.5.8: {} + + request-light@0.7.0: {} + + require-directory@2.1.1: {} + + require-from-string@2.0.2: {} + + resolve-from@4.0.0: {} + + resolve-from@5.0.0: {} + + resolve@1.22.10: + dependencies: + is-core-module: 2.16.1 + path-parse: 1.0.7 + supports-preserve-symlinks-flag: 1.0.0 + + restructure@3.0.2: {} + + retext-latin@4.0.0: + dependencies: + '@types/nlcst': 2.0.3 + parse-latin: 7.0.0 + unified: 11.0.5 + + retext-smartypants@6.2.0: + dependencies: + '@types/nlcst': 2.0.3 + nlcst-to-string: 4.0.0 + unist-util-visit: 5.0.0 + + retext-stringify@4.0.0: + dependencies: + '@types/nlcst': 2.0.3 + nlcst-to-string: 4.0.0 + unified: 11.0.5 + + retext@9.0.0: + dependencies: + '@types/nlcst': 2.0.3 + retext-latin: 4.0.0 + retext-stringify: 4.0.0 + unified: 11.0.5 + + reusify@1.1.0: {} + + rollup-plugin-bundle-size@1.0.3: + dependencies: + chalk: 1.1.3 + maxmin: 2.1.0 + + rollup-plugin-postcss@4.0.2(postcss@8.5.3): + dependencies: + chalk: 4.1.2 + concat-with-sourcemaps: 1.1.0 + cssnano: 5.1.15(postcss@8.5.3) + import-cwd: 3.0.0 + p-queue: 6.6.2 + pify: 5.0.0 + postcss: 8.5.3 + postcss-load-config: 3.1.4(postcss@8.5.3) + postcss-modules: 4.3.1(postcss@8.5.3) + promise.series: 0.2.0 + resolve: 1.22.10 + rollup-pluginutils: 2.8.2 + safe-identifier: 0.4.2 + style-inject: 0.3.0 + transitivePeerDependencies: + - ts-node + + rollup-plugin-terser@7.0.2(rollup@2.79.2): + dependencies: + '@babel/code-frame': 7.26.2 + jest-worker: 26.6.2 + rollup: 2.79.2 + serialize-javascript: 4.0.0 + terser: 5.39.0 + + rollup-plugin-typescript2@0.32.1(rollup@2.79.2)(typescript@4.9.5): + dependencies: + '@rollup/pluginutils': 4.2.1 + find-cache-dir: 3.3.2 + fs-extra: 10.1.0 + resolve: 1.22.10 + rollup: 2.79.2 + tslib: 2.8.1 + typescript: 4.9.5 + + rollup-plugin-visualizer@5.14.0(rollup@2.79.2): + dependencies: + open: 8.4.2 + picomatch: 4.0.2 + source-map: 0.7.4 + yargs: 17.7.2 + optionalDependencies: + rollup: 2.79.2 + + rollup-pluginutils@2.8.2: + dependencies: + estree-walker: 0.6.1 + + rollup@2.79.2: + optionalDependencies: + fsevents: 2.3.3 + + rollup@4.40.1: + dependencies: + '@types/estree': 1.0.7 + optionalDependencies: + '@rollup/rollup-android-arm-eabi': 4.40.1 + '@rollup/rollup-android-arm64': 4.40.1 + '@rollup/rollup-darwin-arm64': 4.40.1 + '@rollup/rollup-darwin-x64': 4.40.1 + '@rollup/rollup-freebsd-arm64': 4.40.1 + '@rollup/rollup-freebsd-x64': 4.40.1 + '@rollup/rollup-linux-arm-gnueabihf': 4.40.1 + '@rollup/rollup-linux-arm-musleabihf': 4.40.1 + '@rollup/rollup-linux-arm64-gnu': 4.40.1 + '@rollup/rollup-linux-arm64-musl': 4.40.1 + '@rollup/rollup-linux-loongarch64-gnu': 4.40.1 + '@rollup/rollup-linux-powerpc64le-gnu': 4.40.1 + '@rollup/rollup-linux-riscv64-gnu': 4.40.1 + '@rollup/rollup-linux-riscv64-musl': 4.40.1 + '@rollup/rollup-linux-s390x-gnu': 4.40.1 + '@rollup/rollup-linux-x64-gnu': 4.40.1 + '@rollup/rollup-linux-x64-musl': 4.40.1 + '@rollup/rollup-win32-arm64-msvc': 4.40.1 + '@rollup/rollup-win32-ia32-msvc': 4.40.1 + '@rollup/rollup-win32-x64-msvc': 4.40.1 + fsevents: 2.3.3 + + run-parallel@1.2.0: + dependencies: + queue-microtask: 1.2.3 + + sade@1.8.1: + dependencies: + mri: 1.2.0 + + safe-array-concat@1.1.3: + dependencies: + call-bind: 1.0.8 + call-bound: 1.0.4 + get-intrinsic: 1.3.0 + has-symbols: 1.1.0 + isarray: 2.0.5 + + safe-buffer@5.2.1: {} + + safe-identifier@0.4.2: {} + + safe-push-apply@1.0.0: + dependencies: + es-errors: 1.3.0 + isarray: 2.0.5 + + safe-regex-test@1.1.0: + dependencies: + call-bound: 1.0.4 + es-errors: 1.3.0 + is-regex: 1.2.1 + + safer-buffer@2.1.2: {} + + sanitize-html@2.16.0: + dependencies: + deepmerge: 4.3.1 + escape-string-regexp: 4.0.0 + htmlparser2: 8.0.2 + is-plain-object: 5.0.0 + parse-srcset: 1.0.2 + postcss: 8.5.3 + + sass@1.80.4: + dependencies: + '@parcel/watcher': 2.5.1 + chokidar: 4.0.3 + immutable: 4.3.7 + source-map-js: 1.2.1 + optional: true + + sax@1.4.1: {} + + scrl@2.0.0: {} + + semver@6.3.1: {} + + semver@7.7.1: {} + + serialize-javascript@4.0.0: + dependencies: + randombytes: 2.1.0 + + set-function-length@1.2.2: + dependencies: + define-data-property: 1.1.4 + es-errors: 1.3.0 + function-bind: 1.1.2 + get-intrinsic: 1.3.0 + gopd: 1.2.0 + has-property-descriptors: 1.0.2 + + set-function-name@2.0.2: + dependencies: + define-data-property: 1.1.4 + es-errors: 1.3.0 + functions-have-names: 1.2.3 + has-property-descriptors: 1.0.2 + + set-proto@1.0.0: + dependencies: + dunder-proto: 1.0.1 + es-errors: 1.3.0 + es-object-atoms: 1.1.1 + + sharp@0.33.5: + dependencies: + color: 4.2.3 + detect-libc: 2.0.4 + semver: 7.7.1 + optionalDependencies: + '@img/sharp-darwin-arm64': 0.33.5 + '@img/sharp-darwin-x64': 0.33.5 + '@img/sharp-libvips-darwin-arm64': 1.0.4 + '@img/sharp-libvips-darwin-x64': 1.0.4 + '@img/sharp-libvips-linux-arm': 1.0.5 + '@img/sharp-libvips-linux-arm64': 1.0.4 + '@img/sharp-libvips-linux-s390x': 1.0.4 + '@img/sharp-libvips-linux-x64': 1.0.4 + '@img/sharp-libvips-linuxmusl-arm64': 1.0.4 + '@img/sharp-libvips-linuxmusl-x64': 1.0.4 + '@img/sharp-linux-arm': 0.33.5 + '@img/sharp-linux-arm64': 0.33.5 + '@img/sharp-linux-s390x': 0.33.5 + '@img/sharp-linux-x64': 0.33.5 + '@img/sharp-linuxmusl-arm64': 0.33.5 + '@img/sharp-linuxmusl-x64': 0.33.5 + '@img/sharp-wasm32': 0.33.5 + '@img/sharp-win32-ia32': 0.33.5 + '@img/sharp-win32-x64': 0.33.5 + optional: true + + sharp@0.34.1: + dependencies: + color: 4.2.3 + detect-libc: 2.0.3 + semver: 7.7.1 + optionalDependencies: + '@img/sharp-darwin-arm64': 0.34.1 + '@img/sharp-darwin-x64': 0.34.1 + '@img/sharp-libvips-darwin-arm64': 1.1.0 + '@img/sharp-libvips-darwin-x64': 1.1.0 + '@img/sharp-libvips-linux-arm': 1.1.0 + '@img/sharp-libvips-linux-arm64': 1.1.0 + '@img/sharp-libvips-linux-ppc64': 1.1.0 + '@img/sharp-libvips-linux-s390x': 1.1.0 + '@img/sharp-libvips-linux-x64': 1.1.0 + '@img/sharp-libvips-linuxmusl-arm64': 1.1.0 + '@img/sharp-libvips-linuxmusl-x64': 1.1.0 + '@img/sharp-linux-arm': 0.34.1 + '@img/sharp-linux-arm64': 0.34.1 + '@img/sharp-linux-s390x': 0.34.1 + '@img/sharp-linux-x64': 0.34.1 + '@img/sharp-linuxmusl-arm64': 0.34.1 + '@img/sharp-linuxmusl-x64': 0.34.1 + '@img/sharp-wasm32': 0.34.1 + '@img/sharp-win32-ia32': 0.34.1 + '@img/sharp-win32-x64': 0.34.1 + + shebang-command@2.0.0: + dependencies: + shebang-regex: 3.0.0 + + shebang-regex@3.0.0: {} + + shelljs-live@0.0.5(shelljs@0.8.5): + dependencies: + cross-spawn: 7.0.6 + shelljs: 0.8.5 + + shelljs@0.8.5: + dependencies: + glob: 7.2.3 + interpret: 1.4.0 + rechoir: 0.6.2 + + shiki@3.3.0: + dependencies: + '@shikijs/core': 3.3.0 + '@shikijs/engine-javascript': 3.3.0 + '@shikijs/engine-oniguruma': 3.3.0 + '@shikijs/langs': 3.3.0 + '@shikijs/themes': 3.3.0 + '@shikijs/types': 3.3.0 + '@shikijs/vscode-textmate': 10.0.2 + '@types/hast': 3.0.4 + + side-channel-list@1.0.0: + dependencies: + es-errors: 1.3.0 + object-inspect: 1.13.4 + + side-channel-map@1.0.1: + dependencies: + call-bound: 1.0.4 + es-errors: 1.3.0 + get-intrinsic: 1.3.0 + object-inspect: 1.13.4 + + side-channel-weakmap@1.0.2: + dependencies: + call-bound: 1.0.4 + es-errors: 1.3.0 + get-intrinsic: 1.3.0 + object-inspect: 1.13.4 + side-channel-map: 1.0.1 + + side-channel@1.1.0: + dependencies: + es-errors: 1.3.0 + object-inspect: 1.13.4 + side-channel-list: 1.0.0 + side-channel-map: 1.0.1 + side-channel-weakmap: 1.0.2 + + signal-exit@4.1.0: {} + + simple-swizzle@0.2.2: + dependencies: + is-arrayish: 0.3.2 + + sisteransi@1.0.5: {} + + sitemap@8.0.0: + dependencies: + '@types/node': 17.0.45 + '@types/sax': 1.2.7 + arg: 5.0.2 + sax: 1.4.1 + + slash@3.0.0: {} + + smol-toml@1.3.4: {} + + source-map-js@1.2.1: {} + + source-map-support@0.5.21: + dependencies: + buffer-from: 1.1.2 + source-map: 0.6.1 + + source-map@0.6.1: {} + + source-map@0.7.4: {} + + sourcemap-codec@1.4.8: {} + + space-separated-tokens@2.0.2: {} + + stable@0.1.8: {} + + stream-replace-string@2.0.0: {} + + string-hash@1.1.3: {} + + string-width@4.2.3: + dependencies: + emoji-regex: 8.0.0 + is-fullwidth-code-point: 3.0.0 + strip-ansi: 6.0.1 + + string-width@5.1.2: + dependencies: + eastasianwidth: 0.2.0 + emoji-regex: 9.2.2 + strip-ansi: 7.1.0 + + string-width@7.2.0: + dependencies: + emoji-regex: 10.4.0 + get-east-asian-width: 1.3.0 + strip-ansi: 7.1.0 + + string.prototype.matchall@4.0.12: + dependencies: + call-bind: 1.0.8 + call-bound: 1.0.4 + define-properties: 1.2.1 + es-abstract: 1.23.9 + es-errors: 1.3.0 + es-object-atoms: 1.1.1 + get-intrinsic: 1.3.0 + gopd: 1.2.0 + has-symbols: 1.1.0 + internal-slot: 1.1.0 + regexp.prototype.flags: 1.5.4 + set-function-name: 2.0.2 + side-channel: 1.1.0 + + string.prototype.trim@1.2.10: + dependencies: + call-bind: 1.0.8 + call-bound: 1.0.4 + define-data-property: 1.1.4 + define-properties: 1.2.1 + es-abstract: 1.23.9 + es-object-atoms: 1.1.1 + has-property-descriptors: 1.0.2 + + string.prototype.trimend@1.0.9: + dependencies: + call-bind: 1.0.8 + call-bound: 1.0.4 + define-properties: 1.2.1 + es-object-atoms: 1.1.1 + + string.prototype.trimstart@1.0.8: + dependencies: + call-bind: 1.0.8 + define-properties: 1.2.1 + es-object-atoms: 1.1.1 + + stringify-entities@4.0.4: + dependencies: + character-entities-html4: 2.1.0 + character-entities-legacy: 3.0.0 + + strip-ansi@3.0.1: + dependencies: + ansi-regex: 2.1.1 + + strip-ansi@6.0.1: + dependencies: + ansi-regex: 5.0.1 + + strip-ansi@7.1.0: + dependencies: + ansi-regex: 6.1.0 + + strnum@1.1.2: {} + + style-inject@0.3.0: {} + + stylehacks@5.1.1(postcss@8.5.3): + dependencies: + browserslist: 4.24.4 + postcss: 8.5.3 + postcss-selector-parser: 6.1.2 + + stylus@0.64.0: + dependencies: + '@adobe/css-tools': 4.3.3 + debug: 4.4.0 + glob: 10.4.5 + sax: 1.4.1 + source-map: 0.7.4 + transitivePeerDependencies: + - supports-color + + sucrase@3.35.0: + dependencies: + '@jridgewell/gen-mapping': 0.3.8 + commander: 4.1.1 + glob: 10.4.5 + lines-and-columns: 1.2.4 + mz: 2.7.0 + pirates: 4.0.7 + ts-interface-checker: 0.1.13 + + supports-color@2.0.0: {} + + supports-color@7.2.0: + dependencies: + has-flag: 4.0.0 + + supports-preserve-symlinks-flag@1.0.0: {} + + svelte2tsx@0.7.36(svelte@5.28.2)(typescript@5.8.3): + dependencies: + dedent-js: 1.0.1 + pascal-case: 3.1.2 + svelte: 5.28.2 + typescript: 5.8.3 + + svelte@5.28.2: + dependencies: + '@ampproject/remapping': 2.3.0 + '@jridgewell/sourcemap-codec': 1.5.0 + '@sveltejs/acorn-typescript': 1.0.5(acorn@8.14.1) + '@types/estree': 1.0.7 + acorn: 8.14.1 + aria-query: 5.3.2 + axobject-query: 4.1.0 + clsx: 2.1.1 + esm-env: 1.2.2 + esrap: 1.4.6 + is-reference: 3.0.3 + locate-character: 3.0.0 + magic-string: 0.30.17 + zimmerframe: 1.1.2 + + svgo@2.8.0: + dependencies: + '@trysound/sax': 0.2.0 + commander: 7.2.0 + css-select: 4.3.0 + css-tree: 1.1.3 + csso: 4.2.0 + picocolors: 1.1.1 + stable: 0.1.8 + + svgo@3.3.2: + dependencies: + '@trysound/sax': 0.2.0 + commander: 7.2.0 + css-select: 5.1.0 + css-tree: 2.3.1 + css-what: 6.1.0 + csso: 5.0.5 + picocolors: 1.1.1 + + swup-morph-plugin@1.3.0(swup@4.8.1): + dependencies: + '@swup/plugin': 4.0.0 + morphdom: 2.7.4 + swup: 4.8.1 + + swup@4.8.1: + dependencies: + delegate-it: 6.2.1 + opencollective-postinstall: 2.0.3 + path-to-regexp: 6.3.0 + + tailwindcss@3.4.17: + dependencies: + '@alloc/quick-lru': 5.2.0 + arg: 5.0.2 + chokidar: 3.6.0 + didyoumean: 1.2.2 + dlv: 1.1.3 + fast-glob: 3.3.3 + glob-parent: 6.0.2 + is-glob: 4.0.3 + jiti: 1.21.7 + lilconfig: 3.1.3 + micromatch: 4.0.8 + normalize-path: 3.0.0 + object-hash: 3.0.0 + picocolors: 1.1.1 + postcss: 8.5.3 + postcss-import: 15.1.0(postcss@8.5.3) + postcss-js: 4.0.1(postcss@8.5.3) + postcss-load-config: 4.0.2(postcss@8.5.3) + postcss-nested: 6.2.0(postcss@8.5.3) + postcss-selector-parser: 6.1.2 + resolve: 1.22.10 + sucrase: 3.35.0 + transitivePeerDependencies: + - ts-node + + tar@6.2.1: + dependencies: + chownr: 2.0.0 + fs-minipass: 2.1.0 + minipass: 5.0.0 + minizlib: 2.1.2 + mkdirp: 1.0.4 + yallist: 4.0.0 + + tar@7.4.3: + dependencies: + '@isaacs/fs-minipass': 4.0.1 + chownr: 3.0.0 + minipass: 7.1.2 + minizlib: 3.0.2 + mkdirp: 3.0.1 + yallist: 5.0.0 + + terser@5.39.0: + dependencies: + '@jridgewell/source-map': 0.3.6 + acorn: 8.14.1 + commander: 2.20.3 + source-map-support: 0.5.21 + + thenify-all@1.6.0: + dependencies: + thenify: 3.3.1 + + thenify@3.3.1: + dependencies: + any-promise: 1.3.0 + + tiny-glob@0.2.9: + dependencies: + globalyzer: 0.1.0 + globrex: 0.1.2 + + tiny-inflate@1.0.3: {} + + tinyexec@0.3.2: {} + + tinyglobby@0.2.13: + dependencies: + fdir: 6.4.4(picomatch@4.0.2) + picomatch: 4.0.2 + + to-regex-range@5.0.1: + dependencies: + is-number: 7.0.0 + + tosource@2.0.0-alpha.3: {} + + tr46@0.0.3: {} + + trim-lines@3.0.1: {} + + trough@2.2.0: {} + + ts-interface-checker@0.1.13: {} + + tsconfck@3.1.5(typescript@5.8.3): + optionalDependencies: + typescript: 5.8.3 + + tslib@2.8.1: {} + + type-fest@4.40.1: {} + + typed-array-buffer@1.0.3: + dependencies: + call-bound: 1.0.4 + es-errors: 1.3.0 + is-typed-array: 1.1.15 + + typed-array-byte-length@1.0.3: + dependencies: + call-bind: 1.0.8 + for-each: 0.3.5 + gopd: 1.2.0 + has-proto: 1.2.0 + is-typed-array: 1.1.15 + + typed-array-byte-offset@1.0.4: + dependencies: + available-typed-arrays: 1.0.7 + call-bind: 1.0.8 + for-each: 0.3.5 + gopd: 1.2.0 + has-proto: 1.2.0 + is-typed-array: 1.1.15 + reflect.getprototypeof: 1.0.10 + + typed-array-length@1.0.7: + dependencies: + call-bind: 1.0.8 + for-each: 0.3.5 + gopd: 1.2.0 + is-typed-array: 1.1.15 + possible-typed-array-names: 1.1.0 + reflect.getprototypeof: 1.0.10 + + typed-query-selector@2.12.0: {} + + typesafe-path@0.2.2: {} + + typescript-auto-import-cache@0.3.5: + dependencies: + semver: 7.7.1 + + typescript@4.9.5: {} + + typescript@5.8.3: {} + + uc.micro@2.1.0: {} + + ufo@1.6.1: {} + + ultrahtml@1.6.0: {} + + unbox-primitive@1.1.0: + dependencies: + call-bound: 1.0.4 + has-bigints: 1.1.0 + has-symbols: 1.1.0 + which-boxed-primitive: 1.1.1 + + uncrypto@0.1.3: {} + + undici-types@6.21.0: {} + + undici@6.21.2: {} + + unicode-canonical-property-names-ecmascript@2.0.1: {} + + unicode-match-property-ecmascript@2.0.0: + dependencies: + unicode-canonical-property-names-ecmascript: 2.0.1 + unicode-property-aliases-ecmascript: 2.1.0 + + unicode-match-property-value-ecmascript@2.2.0: {} + + unicode-properties@1.4.1: + dependencies: + base64-js: 1.5.1 + unicode-trie: 2.0.0 + + unicode-property-aliases-ecmascript@2.1.0: {} + + unicode-trie@2.0.0: + dependencies: + pako: 0.2.9 + tiny-inflate: 1.0.3 + + unified@11.0.5: + dependencies: + '@types/unist': 3.0.3 + bail: 2.0.2 + devlop: 1.1.0 + extend: 3.0.2 + is-plain-obj: 4.1.0 + trough: 2.2.0 + vfile: 6.0.3 + + unifont@0.4.1: + dependencies: + css-tree: 3.1.0 + ohash: 2.0.11 + + unist-util-find-after@4.0.1: + dependencies: + '@types/unist': 2.0.11 + unist-util-is: 5.2.1 + + unist-util-find-after@5.0.0: + dependencies: + '@types/unist': 3.0.3 + unist-util-is: 6.0.0 + + unist-util-is@5.2.1: + dependencies: + '@types/unist': 2.0.11 + + unist-util-is@6.0.0: + dependencies: + '@types/unist': 3.0.3 + + unist-util-map@3.1.3: + dependencies: + '@types/unist': 2.0.11 + + unist-util-modify-children@4.0.0: + dependencies: + '@types/unist': 3.0.3 + array-iterate: 2.0.1 + + unist-util-position@5.0.0: + dependencies: + '@types/unist': 3.0.3 + + unist-util-remove-position@5.0.0: + dependencies: + '@types/unist': 3.0.3 + unist-util-visit: 5.0.0 + + unist-util-stringify-position@4.0.0: + dependencies: + '@types/unist': 3.0.3 + + unist-util-visit-children@3.0.0: + dependencies: + '@types/unist': 3.0.3 + + unist-util-visit-parents@5.1.3: + dependencies: + '@types/unist': 2.0.11 + unist-util-is: 5.2.1 + + unist-util-visit-parents@6.0.1: + dependencies: + '@types/unist': 3.0.3 + unist-util-is: 6.0.0 + + unist-util-visit@4.1.2: + dependencies: + '@types/unist': 2.0.11 + unist-util-is: 5.2.1 + unist-util-visit-parents: 5.1.3 + + unist-util-visit@5.0.0: + dependencies: + '@types/unist': 3.0.3 + unist-util-is: 6.0.0 + unist-util-visit-parents: 6.0.1 + + universalify@2.0.1: {} + + unstorage@1.16.0: + dependencies: + anymatch: 3.1.3 + chokidar: 4.0.3 + destr: 2.0.5 + h3: 1.15.3 + lru-cache: 10.4.3 + node-fetch-native: 1.6.6 + ofetch: 1.4.1 + ufo: 1.6.1 + + update-browserslist-db@1.1.3(browserslist@4.24.4): + dependencies: + browserslist: 4.24.4 + escalade: 3.2.0 + picocolors: 1.1.1 + + uri-js@4.4.1: + dependencies: + punycode: 2.3.1 + optional: true + + util-deprecate@1.0.2: {} + + vfile-location@5.0.3: + dependencies: + '@types/unist': 3.0.3 + vfile: 6.0.3 + + vfile-message@4.0.2: + dependencies: + '@types/unist': 3.0.3 + unist-util-stringify-position: 4.0.0 + + vfile@6.0.3: + dependencies: + '@types/unist': 3.0.3 + vfile-message: 4.0.2 + + vite@6.3.3(@types/node@22.14.1)(jiti@1.21.7)(lightningcss@1.29.3)(sass@1.80.4)(stylus@0.64.0)(terser@5.39.0)(yaml@2.7.0): + dependencies: + esbuild: 0.25.3 + fdir: 6.4.4(picomatch@4.0.2) + picomatch: 4.0.2 + postcss: 8.5.3 + rollup: 4.40.1 + tinyglobby: 0.2.13 + optionalDependencies: + '@types/node': 22.14.1 + fsevents: 2.3.3 + jiti: 1.21.7 + lightningcss: 1.29.3 + sass: 1.80.4 + stylus: 0.64.0 + terser: 5.39.0 + yaml: 2.7.0 + + vitefu@1.0.6(vite@6.3.3(@types/node@22.14.1)(jiti@1.21.7)(lightningcss@1.29.3)(sass@1.80.4)(stylus@0.64.0)(terser@5.39.0)(yaml@2.7.0)): + optionalDependencies: + vite: 6.3.3(@types/node@22.14.1)(jiti@1.21.7)(lightningcss@1.29.3)(sass@1.80.4)(stylus@0.64.0)(terser@5.39.0)(yaml@2.7.0) + + volar-service-css@0.0.62(@volar/language-service@2.4.12): + dependencies: + vscode-css-languageservice: 6.3.3 + vscode-languageserver-textdocument: 1.0.12 + vscode-uri: 3.1.0 + optionalDependencies: + '@volar/language-service': 2.4.12 + + volar-service-emmet@0.0.62(@volar/language-service@2.4.12): + dependencies: + '@emmetio/css-parser': 0.4.0 + '@emmetio/html-matcher': 1.3.0 + '@vscode/emmet-helper': 2.11.0 + vscode-uri: 3.1.0 + optionalDependencies: + '@volar/language-service': 2.4.12 + + volar-service-html@0.0.62(@volar/language-service@2.4.12): + dependencies: + vscode-html-languageservice: 5.3.3 + vscode-languageserver-textdocument: 1.0.12 + vscode-uri: 3.1.0 + optionalDependencies: + '@volar/language-service': 2.4.12 + + volar-service-prettier@0.0.62(@volar/language-service@2.4.12): + dependencies: + vscode-uri: 3.1.0 + optionalDependencies: + '@volar/language-service': 2.4.12 + + volar-service-typescript-twoslash-queries@0.0.62(@volar/language-service@2.4.12): + dependencies: + vscode-uri: 3.1.0 + optionalDependencies: + '@volar/language-service': 2.4.12 + + volar-service-typescript@0.0.62(@volar/language-service@2.4.12): + dependencies: + path-browserify: 1.0.1 + semver: 7.7.1 + typescript-auto-import-cache: 0.3.5 + vscode-languageserver-textdocument: 1.0.12 + vscode-nls: 5.2.0 + vscode-uri: 3.1.0 + optionalDependencies: + '@volar/language-service': 2.4.12 + + volar-service-yaml@0.0.62(@volar/language-service@2.4.12): + dependencies: + vscode-uri: 3.1.0 + yaml-language-server: 1.15.0 + optionalDependencies: + '@volar/language-service': 2.4.12 + + vscode-css-languageservice@6.3.3: + dependencies: + '@vscode/l10n': 0.0.18 + vscode-languageserver-textdocument: 1.0.12 + vscode-languageserver-types: 3.17.5 + vscode-uri: 3.1.0 + + vscode-html-languageservice@5.3.3: + dependencies: + '@vscode/l10n': 0.0.18 + vscode-languageserver-textdocument: 1.0.12 + vscode-languageserver-types: 3.17.5 + vscode-uri: 3.1.0 + + vscode-json-languageservice@4.1.8: + dependencies: + jsonc-parser: 3.3.1 + vscode-languageserver-textdocument: 1.0.12 + vscode-languageserver-types: 3.17.5 + vscode-nls: 5.2.0 + vscode-uri: 3.1.0 + + vscode-jsonrpc@6.0.0: {} + + vscode-jsonrpc@8.2.0: {} + + vscode-languageserver-protocol@3.16.0: + dependencies: + vscode-jsonrpc: 6.0.0 + vscode-languageserver-types: 3.16.0 + + vscode-languageserver-protocol@3.17.5: + dependencies: + vscode-jsonrpc: 8.2.0 + vscode-languageserver-types: 3.17.5 + + vscode-languageserver-textdocument@1.0.12: {} + + vscode-languageserver-types@3.16.0: {} + + vscode-languageserver-types@3.17.5: {} + + vscode-languageserver@7.0.0: + dependencies: + vscode-languageserver-protocol: 3.16.0 + + vscode-languageserver@9.0.1: + dependencies: + vscode-languageserver-protocol: 3.17.5 + + vscode-nls@5.2.0: {} + + vscode-uri@3.1.0: {} + + web-namespaces@2.0.1: {} + + webidl-conversions@3.0.1: {} + + whatwg-encoding@3.1.1: + dependencies: + iconv-lite: 0.6.3 + + whatwg-mimetype@4.0.0: {} + + whatwg-url@5.0.0: + dependencies: + tr46: 0.0.3 + webidl-conversions: 3.0.1 + + which-boxed-primitive@1.1.1: + dependencies: + is-bigint: 1.1.0 + is-boolean-object: 1.2.2 + is-number-object: 1.1.1 + is-string: 1.1.1 + is-symbol: 1.1.1 + + which-builtin-type@1.2.1: + dependencies: + call-bound: 1.0.4 + function.prototype.name: 1.1.8 + has-tostringtag: 1.0.2 + is-async-function: 2.1.1 + is-date-object: 1.1.0 + is-finalizationregistry: 1.1.1 + is-generator-function: 1.1.0 + is-regex: 1.2.1 + is-weakref: 1.1.1 + isarray: 2.0.5 + which-boxed-primitive: 1.1.1 + which-collection: 1.0.2 + which-typed-array: 1.1.19 + + which-collection@1.0.2: + dependencies: + is-map: 2.0.3 + is-set: 2.0.3 + is-weakmap: 2.0.2 + is-weakset: 2.0.4 + + which-pm-runs@1.1.0: {} + + which-typed-array@1.1.19: + dependencies: + available-typed-arrays: 1.0.7 + call-bind: 1.0.8 + call-bound: 1.0.4 + for-each: 0.3.5 + get-proto: 1.0.1 + gopd: 1.2.0 + has-tostringtag: 1.0.2 + + which@2.0.2: + dependencies: + isexe: 2.0.0 + + widest-line@5.0.0: + dependencies: + string-width: 7.2.0 + + wrap-ansi@7.0.0: + dependencies: + ansi-styles: 4.3.0 + string-width: 4.2.3 + strip-ansi: 6.0.1 + + wrap-ansi@8.1.0: + dependencies: + ansi-styles: 6.2.1 + string-width: 5.1.2 + strip-ansi: 7.1.0 + + wrap-ansi@9.0.0: + dependencies: + ansi-styles: 6.2.1 + string-width: 7.2.0 + strip-ansi: 7.1.0 + + wrappy@1.0.2: {} + + xxhash-wasm@1.1.0: {} + + y18n@5.0.8: {} + + yallist@3.1.1: {} + + yallist@4.0.0: {} + + yallist@5.0.0: {} + + yaml-language-server@1.15.0: + dependencies: + ajv: 8.17.1 + lodash: 4.17.21 + request-light: 0.5.8 + vscode-json-languageservice: 4.1.8 + vscode-languageserver: 7.0.0 + vscode-languageserver-textdocument: 1.0.12 + vscode-languageserver-types: 3.17.5 + vscode-nls: 5.2.0 + vscode-uri: 3.1.0 + yaml: 2.2.2 + optionalDependencies: + prettier: 2.8.7 + + yaml@1.10.2: {} + + yaml@2.2.2: {} + + yaml@2.7.0: {} + + yargs-parser@21.1.1: {} + + yargs@17.7.2: + dependencies: + cliui: 8.0.1 + escalade: 3.2.0 + get-caller-file: 2.0.5 + require-directory: 2.1.1 + string-width: 4.2.3 + y18n: 5.0.8 + yargs-parser: 21.1.1 + + yauzl@2.10.0: + dependencies: + buffer-crc32: 0.2.13 + fd-slicer: 1.1.0 + + yocto-queue@1.2.1: {} + + yocto-spinner@0.2.2: + dependencies: + yoctocolors: 2.1.1 + + yoctocolors@2.1.1: {} + + zimmerframe@1.1.2: {} + + zod-to-json-schema@3.24.5(zod@3.24.3): + dependencies: + zod: 3.24.3 + + zod-to-ts@1.2.0(typescript@5.8.3)(zod@3.24.3): + dependencies: + typescript: 5.8.3 + zod: 3.24.3 + + zod@3.24.3: {} + + zwitch@2.0.4: {} diff --git a/postcss.config.mjs b/postcss.config.mjs new file mode 100644 index 0000000..632eeda --- /dev/null +++ b/postcss.config.mjs @@ -0,0 +1,11 @@ +import postcssImport from 'postcss-import'; +import postcssNesting from 'tailwindcss/nesting/index.js'; +import tailwindcss from 'tailwindcss'; + +export default { + plugins: { + 'postcss-import': postcssImport, // to combine multiple css files + 'tailwindcss/nesting': postcssNesting, + tailwindcss: tailwindcss, + } +}; diff --git a/public/_redirects b/public/_redirects new file mode 100644 index 0000000..8e31692 --- /dev/null +++ b/public/_redirects @@ -0,0 +1,12 @@ +/donate /sponsors 302 +/ak https://akile.io/register?aff_code=503fe5ea-e7c5-4d68-ae05-6de99513680e 302 +/kook https://kook.vip/K29zpT 302 +/long https://iiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiii.iiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiii.in/ 302 +/mly https://muleyun.com/aff/GOTRJLPN 302 +/tg https://t.me/+_07DERp7k1ljYTc1 302 +/tit /posts/pin/ 302 +/tly https://tianlicloud.cn/aff/HNNCFKGP 302 +/wly https://wl.awcmam.com/#/register?code=FNQwOQBM 302 +/yyb https://www.rainyun.com/acofork_?s=bilibili 302 +/iku https://ikuuu.de/auth/register?code=Bjou 302 +/esa https://tianchi.aliyun.com/specials/promotion/freetier/esa?taskCode=25254&recordId=c856e61228828a0423417a767828d166 302 diff --git a/public/ads.txt b/public/ads.txt new file mode 100644 index 0000000..543484f --- /dev/null +++ b/public/ads.txt @@ -0,0 +1 @@ +google.com, pub-1683686345039700, DIRECT, f08c47fec0942fa0 \ No newline at end of file diff --git a/public/aff/secbit-banner-1.gif b/public/aff/secbit-banner-1.gif new file mode 100644 index 0000000..c093116 Binary files /dev/null and b/public/aff/secbit-banner-1.gif differ diff --git a/public/config/EasyIamge.lock b/public/config/EasyIamge.lock new file mode 100755 index 0000000..6be4f4a --- /dev/null +++ b/public/config/EasyIamge.lock @@ -0,0 +1 @@ +安装环境检测锁定文件,如需再次展示请删除此文件! \ No newline at end of file diff --git a/public/config/api_key.php b/public/config/api_key.php new file mode 100755 index 0000000..323b8d5 --- /dev/null +++ b/public/config/api_key.php @@ -0,0 +1,16 @@ +<?php +$tokenList=Array + ( + '1c17b11693cb5ec63859b091c5b9c1b2'=>Array + ( + 'id'=>0, + 'expired'=>1751608714, + 'add_time'=>1680497928 + ), + '4ab8829b49738fc562b40e66991b4504'=>Array + ( + 'id'=>1, + 'expired'=>'1.728E+23', + 'add_time'=>1751608724 + ) + ); \ No newline at end of file diff --git a/public/config/config.guest.php b/public/config/config.guest.php new file mode 100755 index 0000000..b2aa9a4 --- /dev/null +++ b/public/config/config.guest.php @@ -0,0 +1,10 @@ +<?php +$guestConfig=Array + ( + 'guest'=>Array + ( + 'password'=>'84983c60f7daadc1cb8698621f802c0d9f9a3c3c295c810748fb048115c186ec', + 'expired'=>1752905698, + 'add_time'=>1678988356 + ) + ); \ No newline at end of file diff --git a/public/config/config.manager.php b/public/config/config.manager.php new file mode 100755 index 0000000..c77c029 --- /dev/null +++ b/public/config/config.manager.php @@ -0,0 +1,148 @@ +<?php + +/*---------------tinyfilemanager管理配置(默认已经配置好了 你也可以自定义)-------------------*/ + +require __DIR__ . '/config.php'; + +/* Default Configuration + * 默认配置 + */ +// $CONFIG = '{"lang":"zh-CN","error_reporting":false,"show_hidden":false,"hide_Cols":false,"calc_folder":false}'; + +/* + * Auth with login/password + * set true/false to enable/disable it + * Is independent from IP white- and blacklisting + * 开启登录 + */ + +// Auth with login/password +// set true/false to enable/disable it +// Is independent from IP white- and blacklisting +$use_auth = false; + +// Login user name and password +// Users: array('Username' => 'Password', 'Username2' => 'Password2', ...) +// Generate secure password hash - https://tinyfilemanager.github.io/docs/pwd.html +// 登录和管理密码 - Admin管理密码请在图床配置中修改 +$auth_users = array( + 'admin' => password_hash($config['password'], PASSWORD_DEFAULT), // 登录密码 + 'user' => '$2y$10$iPtSuvQnv0FnqdWdQsuWMOGxlul/VQzcKl3q1K7VU/QTw102IU5yi' //密码:CQ4CdBGjGJnA + // 先写一个密码然后获取密码Hash填上去- https://tinyfilemanager.github.io/docs/pwd.html +); + +// Readonly users +// e.g. array('users', 'guest', ...) +// 只读的用户 +$readonly_users = array( + 'user' +); + +// Enable highlight.js (https://highlightjs.org/) on view's page +$use_highlightjs = true; + +// highlight.js style +// for dark theme use 'ir-black' +// 主题 白天 vs/ 黑夜 ir-black +$highlightjs_style = 'vs'; + +// Enable ace.js (https://ace.c9.io/) on view's page +$edit_files = true; + +// Default timezone for date() and time() +// Doc - http://php.net/manual/en/timezones.php +// 时区 +$default_timezone = 'Asia/Shanghai'; // UTC + +// Root path for file manager +// use absolute path of directory i.e: '/var/www/folder' or $_SERVER['DOCUMENT_ROOT'].'/folder' +// 管理的目录 +$root_path = $_SERVER['DOCUMENT_ROOT'] . $config['path']; + +// Root url for links in file manager.Relative to $http_host. Variants: '', 'path/to/subfolder' +// Will not working if $root_path will be outside of server document root +// 文件的路径 +$root_url = $config['path']; + +// Server hostname. Can set manually if wrong +$http_host = $_SERVER['HTTP_HOST']; +// $http_host = $config['imgurl']; + + +// user specific directories +// array('Username' => 'Directory path', 'Username2' => 'Directory path', ...) +// 用户路径 +$directories_users = array(); + +// input encoding for iconv +$iconv_input_encoding = 'UTF-8'; + +// date() format for file modification date +// Doc - https://www.php.net/manual/en/datetime.format.php +$datetime_format = 'Y.m.d H:i:s'; + +// Allowed file extensions for create and rename files +// e.g. 'txt,html,css,js' +// 允许创建的文件格式 +$allowed_file_extensions = ''; + +// Allowed file extensions for upload files +// e.g. 'gif,png,jpg,html,txt' +// 允许上传的文件格式 +$allowed_upload_extensions = ''; + +// Favicon path. This can be either a full url to an .PNG image, or a path based on the document root. +// full path, e.g http://example.com/favicon.png +// local path, e.g images/icons/favicon.png +// Favicon图标路径 +$favicon_path = $config['domain'] . '/favicon.ico'; + +// Files and folders to excluded from listing +// e.g. array('myfile.html', 'personal-folder', '*.php', ...) +// 不显示的文件类型或文件夹 +$exclude_items = array(''); + +// Online office Docs Viewer +// Availabe rules are 'google', 'microsoft' or false +// google => View documents using Google Docs Viewer +// microsoft => View documents using Microsoft Web Apps Viewer +// false => disable online doc viewer +// 文档查看引擎 'google', 'microsoft' or false +$online_viewer = 'microsoft'; + +// Sticky Nav bar +// true => enable sticky header +// false => disable sticky header +// 启用导航栏? +$sticky_navbar = false; + + +// max upload file size +// 文件最大上传大小 +$max_upload_size_bytes = 5000; + +// Possible rules are 'OFF', 'AND' or 'OR' +// OFF => Don't check connection IP, defaults to OFF +// AND => Connection must be on the whitelist, and not on the blacklist +// OR => Connection must be on the whitelist, or not on the blacklist +// 开启登录IP管理 +// OFF 关闭 AND 需在白名单内 OR 必须是白名单内或者不是黑名单内 +$ip_ruleset = 'OFF'; + +// Should users be notified of their block? +// 告诉用户当前IP不可访问? +$ip_silent = true; + +// IP-addresses, both ipv4 and ipv6 +// 登录白名单 +$ip_whitelist = array( + '127.0.0.1', // local ipv4 + '::1' // local ipv6 +); + +// IP-addresses, both ipv4 and ipv6 +// 登录黑名单 +$ip_blacklist = array( + '0.0.0.0', // non-routable meta ipv4 + '::' // non-routable meta ipv6 +); diff --git a/public/config/config.php b/public/config/config.php new file mode 100755 index 0000000..9a20b04 --- /dev/null +++ b/public/config/config.php @@ -0,0 +1,145 @@ +<?php +$config=Array + ( + 'title'=>'简单图床 - EasyImage', + 'keywords'=>'简单图床,easyimage,easyimage2.0,无数据库图床,免费图床,PHP多图长传程序,自适应页面,一键复制链接,HTML5,markdown,bbscode', + 'description'=>'简单图床EasyImage是一款支持多文件上传的无数据库图床,可以完美替代PHP多图上传程序,最新html5自适应页面兼容手机电脑,上传后返回图片直链,markdown图片,论坛贴图bbscode链接,简单方便支持一键复制,支持多域名,api上传', + 'tips'=>'<a href="https://github.com/icret/EasyImages2.0" target="_black"><i class="icon icon-github"> 如果你喜欢这种图床风格就下载喔 (๑•̀ㅂ•́)و✧</i></a> +<a href="https://github.com/icret/EasyImages2.0" target="_black"><i class="icon icon-heart"> 简单图床是一款开源图床, 支持多文件上传无数据库</i></a> +<a><li class="icon icon-bullhorn text-muted"> 单文件≤10M,单次上传≤30张</li></a>', + 'notice_status'=>0, + 'notice'=>'<p>简单图床是一款开源图床, 支持多文件上传无数据库;</p> +<p>如果你喜欢这种图床风格就<a href="https://github.com/icret/EasyImages2.0" target="_black">下载</a>喔 (๑•̀ㅂ•́)و✧</p>', + 'domain'=>'http://127.0.0.1:8087', + 'imgurl'=>'https://blog.meowrain.cn/api', + 'user'=>'meowrain', + 'password'=>'ef797c8118f02dfb649607dd5d3f8c7623048c9c063d532cc95c5ed7a898a64f', + 'ftp_status'=>0, + 'ftp_host'=>'', + 'ftp_port'=>21, + 'ftp_user'=>'', + 'ftp_pass'=>'', + 'ftp_mode'=>2, + 'ftp_pasv'=>1, + 'ftp_ssl'=>0, + 'ftp_time'=>30, + 'ftp_complete_del_local'=>0, + 'ftp_delloc_sync'=>1, + 'captcha'=>0, + 'mustLogin'=>0, + 'apiStatus'=>1, + 'path'=>'/i/', + 'storage_path'=>'Y/m/d/', + 'mime'=>'image/*,video/*', + 'imgName'=>'default', + 'maxSize'=>35651584, + 'maxUploadFiles'=>30, + 'watermark'=>0, + 'waterText'=>'简单图床 - png.cm', + 'waterPosition'=>9, + 'textColor'=>'rgba(255,0,0,1)', + 'textSize'=>20, + 'textFont'=>'/public/static/pang_men_zheng_dao_biao_ti_ti_3.0.ttf', + 'waterImg'=>'/public/images/watermark.png', + 'extensions'=>'jpg,jpeg,png,gif,bmp,webp,ico,jfif,tif,tga,svg', + 'compress'=>0, + 'compress_ratio'=>50, + 'thumbnail'=>1, + 'thumbnail_w'=>258, + 'thumbnail_h'=>258, + 'imgConvert'=>'webp', + 'maxWidth'=>18432, + 'maxHeight'=>10240, + 'minWidth'=>5, + 'minHeight'=>5, + 'imgRatio'=>0, + 'image_x'=>200, + 'image_y'=>200, + 'imgRatio_quality'=>80, + 'imgRatio_crop'=>1, + 'imgRatio_preserve_headers'=>1, + 'static_cdn'=>0, + 'theme'=>'default', + 'static_cdn_url'=>'https://fastly.jsdelivr.net/gh/icret/EasyImages2.0', + 'TinyPng_key'=>'', + 'checkImg'=>0, + 'checkImg_value'=>80, + 'moderatecontent_key'=>'', + 'nsfwjs_url'=>'', + 'showSwitch'=>1, + 'history'=>1, + 'showSort'=>1, + 'listNumber'=>20, + 'listDate'=>10, + 'customize'=>'', + 'checkEnv'=>0, + 'allowed'=>1, + 'upload_logs'=>0, + 'cache_freq'=>2, + 'upload_first_show'=>1, + 'dark-mode'=>1, + 'show_admin_inc'=>1, + 'show_user_hash_del'=>1, + 'show_exif_info'=>1, + 'info_rand_pic'=>1, + 'chart_on'=>1, + 'check_ip'=>0, + 'check_ip_model'=>0, + 'check_ip_list'=>'', + 'md5_black'=>0, + 'md5_blacklist'=>'', + 'auto_delete'=>0, + 'timezone'=>'Asia/Shanghai', + 'ip_upload_counts'=>0, + 'public'=>1, + 'public_list'=>Array + ( + 0=>'time', + 1=>'today', + 2=>'yesterday', + 3=>'total_space', + 4=>'used_space', + 5=>'free_space', + 6=>'image_used', + 7=>'file', + 8=>'dir', + 9=>'month' + ), + 'language'=>0, + 'login_bg'=>'../app/bing.php', + 'report'=>'', + 'image_recycl'=>1, + 'tinyfilemanager'=>1, + 'file_manage'=>1, + 'delDir'=>'cache/', + 'hide'=>0, + 'hide_key'=>'EasyImage2.0', + 'hide_path'=>0, + 'admin_path_status'=>0, + 'guest_path_status'=>0, + 'token_path_status'=>0, + 'admin_path'=>'u', + 'chunks'=>0, + 'NProgress_default'=>'#000000', + 'NProgress_Progress'=>'#03aa62', + 'footer'=>'<script>var _hmt=_hmt||[];(function(){var hm=document.createElement("script");hm.src="https://hm.baidu.com/hm.js?c790ac2bdc2f385757ecd0183206108d";var s=document.getElementsByTagName("script")[0];s.parentNode.insertBefore(hm,s)})();</script>', + 'ad_top'=>0, + 'ad_top_info'=>' <!--广告 按照这个范例替换相应链接,如果想多几个广告,就多复制几个--> +<div class="col-md-12" style="text-align: center;margin:2px;"><a href="https://app.cloudcone.com.cn/?ref=3521" target="_blank"><img src="/public/images/EasyImage2.0.png" /></a></div>', + 'ad_bot'=>0, + 'ad_bot_info'=>'<div class="col-md-12" style="text-align: center;margin-bottom:10px;"><span data-toggle="tooltip" title="您的赞美是我开发的动力!"><button type="button" class="btn btn-mini btn-danger" data-toggle="modal" data-moveable="true" data-size="sm" data-icon="heart" data-title="您的赞美是我开发的动力!" data-custom="<ul class=\'nav nav-tabs\'><li class=\'active\'><a data-tab href=\'#wechat\'><i class=\'icon icon-wechat\' style=\'color:#329d38;\'> 微信</i></a></li><li><a data-tab href=\'#alipay\'><i class=\'icon icon-zhifubao-square\' style=\'color:#1970fc;\'> 支付宝</i></a></li></ul><div class=\'tab-content\'><div class=\'tab-pane active\' id=\'wechat\'><img src=\'https://icret.gitee.io/easyimages2.0/images/wechat.jpg\'></div><div class=\'tab-pane\' id=\'alipay\'><img src=\'https://icret.gitee.io/easyimages2.0/images/alipay.jpg\'></div></div>"><i class="icon icon-heart"></i> 打赏</button></span></div>', + 'set_notice'=>'<div class="alert alert-primary alert-dismissable"> +<button type="button" class="close" data-dismiss="alert" aria-hidden="true">x</button> +<h5>目录保存以 年/月/日/ 递进,非必要请勿修改! 否则会导致部分操作不可用;</h5> +<h5>本人仅为程序开源创作,如遇非法网站使用与本人无关,请勿用于商业用途;</h5> +<h5>更改配置或内容应注意闭合标签, 否则会导致页面显示不全或者功能异常;</h5> +<h5>作为开发者你可以对相应功能进行扩展(增删改相应代码), 但请保留代码中源作者信息。</h5> +<a href="https://png.cm/?admin.inc" target="_blank"><button type="button" class="btn btn-mini"><i class="icon icon-external-link"></i> 演示</button></a> +<a href="https://icret.github.io/EasyImages2.0/#/" target="_blank"><button type="button" class="btn btn-mini"><i class="icon icon-book"></i> 手册</button></a> +<a href="https://github.com/icret/EasyImages2.0/discussions" target="_blank"><button type="button" class="btn btn-mini"><i class="icon icon-comments-alt"></i> 社区</button></a> +<a href="https://t.me/Easy_Image" target="_blank" data-toggle="tooltip" title="EasyImage Telegram Group"><button type="button" class="btn btn-mini"><i class="icon icon-plane"></i> Telegram</button></a> +<span data-toggle="tooltip" title="您的赞美是我开发的动力!"><button type="button" class="btn btn-mini btn-primary" data-toggle="modal" data-moveable="true" data-size="sm" data-icon="heart" data-title="您的赞美是我开发的动力!" data-custom="<ul class=\'nav nav-tabs\'><li class=\'active\'><a data-tab href=\'#wechat\'><i class=\'icon icon-wechat\' style=\'color:#329d38;\'> 微信</i></a></li><li><a data-tab href=\'#alipay\'><i class=\'icon icon-zhifubao-square\' style=\'color:#1970fc;\'> 支付宝</i></a></li></ul><div class=\'tab-content\'><div class=\'tab-pane active\' id=\'wechat\'><img src=\'../public/images/wechat.jpg\'></div><div class=\'tab-pane\' id=\'alipay\'><img src=\'../public/images/alipay.jpg\'></div></div>"><i class="icon icon-yen"></i>打赏作者</button></span> +</div>', + 'terms'=>'<div class="row"><div class="col-md-12 col-xs-12"><div class="col-md-3 col-xs-3"><ul class="nav nav-tabs nav-stacked"><li class="active"><a href="#Terms" data-target="#tab3Content1" data-toggle="tab">服务条款</a></li><li><a href="#Privacy" data-target="#tab3Content2" data-toggle="tab">隐私政策</a></li><li><a href="#DMCA" data-target="#tab3Content3" data-toggle="tab">DMCA</a></li></ul></div><div class="col-md-9 col-xs-9"><div class="tab-content col-md-9 col-xs-9"><div class="tab-pane fade active in" id="tab3Content1"><h4>服务条款</h4><hr><p>访问我们网站除主页和本“条款”页面以外的任何页面,即表示您同意这些使用条款和我们的隐私政策。如果您不同意,请不要使用我们的网站。</p><ol><li>您使用我们的网站进行除简单访问/查看之外的任何事情(即上传、下载、评论等),不仅构成您的同意,而且构成您的电子签名,这意味着您受这些条款的合同约束,并且通过我们的隐私政策。</li><li>我们保留在使用过多带宽或以其他方式滥用系统的用户帐户上禁用直接链接的权利。</li><li>请勿上传儿童色情内容或威胁、骚扰、诽谤或鼓励非法行为的材料。不要使用本网站作为内容交付网络。如果你这样做(我们将成为法官),或者如果你做了任何违法的事情,除了我们可能拥有的任何其他合法权利之外,我们将禁止你以及你从中盗链的网站,删除你的所有图片,报告如有必要,您可以向当局报告,并阻止您查看本网站上托管的任何图像。我们是认真的。</li><li>用户必须同意遵守适用于其所在地的所有法律,包括版权和商标法。不允许使用侵犯版权或商标的图片。如果有人对您提出侵权索赔,您将被要求删除受版权保护的文件,直到问题得到解决。如果本网站的参与者之间存在争议,我们没有义务参与其中。</li><li>您可以匿名上传图像,并与您的朋友、家人、在线站点以及社交网络在线共享。</li><li>如果您在我们的网站上看到任何不应该出现的内容,因为它违反了我们的政策或出于任何其他原因,请通过电子邮件联系告知我们</li><li>声明通知中的信息准确无误,否则会受到伪证处罚。为此,请包括以下声明: “我发誓,通知中的信息准确无误,并且我是(版权)所有者或被授权代表专有权的所有者行事,在作伪证的处罚下涉嫌侵权”。</li></ol><p>如果发生调查,本网站承诺与任何和所有法律机构合作。</p></div><div class="tab-pane fade" id="tab3Content2"><h4>隐私政策</h4><hr><h4>一般:</h4><p>当您访问本网站或使用本政策中进一步概述的服务(“服务”)时,本网站致力于保护您的隐私。作为使用本网站服务的条件,您同意向本网站提供某些个人信息。该信息包括但不限于: 电子邮件地址和您计算机的唯一 IP 地址(如果有)、财务信息(您的 Paypal 帐户使用的电子邮件地址)和人口统计信息(例如,邮政编码、邮政编码、家乡、性别、购买历史信息和年龄以及不是您计算机独有的 IP 地址)。请注意,我们不会在本网站的任何地方故意收集 13 岁以下儿童的联系信息或财务信息。请定期查看本隐私政策,因为我们可能会不时对其进行更新。本隐私政策的最后修订日期为 2021年10月25日。您每次访问本网站、使用服务或向我们提供信息时,即表示您当时接受本隐私政策中描述的做法。您同意,通过使用本网站,您明确并肯定地同意我们使用和披露您提供的信息,并同意接收电子邮件,如下面的隐私政策所述。</p><h4>隐私政策变更:</h4><p>随着新功能添加到网站或我们纳入用户的建议,本政策可能会随着时间的推移进行修订。如果我们打算以与我们收集信息时声明的方式大不相同的方式使用或披露您的个人身份信息,您将可以选择我们是否以这种新方式使用或披露您的信息。我们还将在我们的网站显着位置发布隐私政策已修订的通知,以便您可以随时查看我们收集的信息、我们将如何使用该信息以及在何种条件下我们将向任何人披露这些信息。</p><h4>如何使用您的信息:</h4><p>我们使用联系信息(即您的电子邮件地址)来帮助我们有效地执行帐户任务(更改密码、找回丢失的密码)、提供您请求的服务、执行质量保证、销售分析和其他业务分析,并就相关事宜与您联系与您向我们下的任何订单。除非满足任何法律、法规、政府要求或司法命令,否则不会将您的财务信息用于其他用途。当您通过本网站进行购买或在本网站注册时,您将向我们提供一个电子邮件地址,我们或这些服务提供商可能会出于本段所述目的与您联系。</p><h4>联系您获取优惠和促销信息:</h4><p>您同意,考虑到我们提供的服务的使用,允许我们使用您的个人信息向您发送营销和促销材料。我们也可能向您发送宣传第三方产品的营销和宣传材料。我们不会出租或出售您的个人信息以供第三方使用。这些材料可能包括但不限于: 向您提供有关如何使用服务的附加信息的时事通讯,以及来自我们或第三方的商品和服务的促销优惠。</p><h4>人口统计信息的使用:</h4><p>我们可能会使用您的人口统计信息进行业务分析或根据您的兴趣定制网站和通讯。我们可能会与广告商和其他第三方共享匿名的汇总人口统计信息,以便他们可以针对适当的受众定制广告和通信。在本隐私政策允许我们共享您的联系信息或财务信息的任何时候,我们也可能会同时共享您的人口统计信息。</p><h4>向政府实体披露:</h4><p>当我们确定此类披露是遵守法律、与执法部门合作或寻求执法协助或保护我们或网站其他访问者或用户的利益或安全时,我们可能会披露特定的联系信息。服务。此外,如果我们发生合并、收购、合并、剥离或破产,您的联系信息可能会传递给第三方。</p><h4>Cookie 和其他网站跟踪数据的使用:</h4><p>Cookies : “cookie”是我们保存在您计算机硬盘上的包含非个人信息的小文件。这些 cookie 有助于让您更快地访问您已经访问过的页面。它们还允许您个性化您的页面,并优化您在我们网站上的体验。我们还使用 cookie 来帮助我们了解有多少人访问了我们的网站、他们访问了我们的哪些网页以及他们在那里停留的时间。此信息可帮助我们确定我们的哪些网络功能非常成功以及哪些网站可能需要改进。您可以通过在浏览器的首选项或选项菜单中指明这一点来禁用计算机上的 cookie。大多数浏览器会自动接受 cookie,但允许您禁用它们。禁用 cookie 可能会阻止您正确使用服务或访问网站。</p><h4>会话 ID:</h4><p>“会话 ID”允许我们在多个网页请求中识别特定用户。此会话 ID 会尽可能保存在您的 cookie 文件中。如果 cookie 未启用,或者如果用户的 Internet 浏览器程序不支持 cookie,则我们将在请求的网页中放置会话 ID。这使最终用户不必为每个网页请求不断地重新输入某些信息,例如帐户名和密码。每当用户关闭其 Internet 浏览器时,此会话 ID 就会过期。</p><h4>网站跟踪、报告:</h4><p>我们的许多网页还包含特殊的电子图像(称为“单像素 gif”或“gif”),使我们能够仅收集非个人身份的流量统计数据和有关我们网站访问者的其他汇总信息。本站(通过自身或第三方)使用此技术收集和积累匿名数据,帮助我们了解和分析访问我们网站的人的体验,并连同您提供的其他信息,定制您未来的访问并改进我们的网站网站。例如,我们捕获有关所用浏览器类型、操作系统软件(例如 Windows 95 与 98 或 Macintosh)、cookie 偏好(用户是否打开或关闭它们)和搜索引擎关键字(哪些关键字做了什么)的数据。访问者用于访问我们的网站)。我们还记录了访问次数,所采取的路径,以及在我们网站内的站点和页面上花费的时间。请记住,这些信息都不是个人身份信息,我们只将这些信息分发给我们的内部员工以及与我们签署了保密协议的合作伙伴。我们与合作伙伴共享的任何信息都反映了整个网站或 Internet 使用趋势,而不是个别信息。</p><h4>IP地址:</h4><p>每次您访问本网站时,我们都会自动收集您的 IP 地址和您来自的网页。为了为您管理和优化站点并诊断我们站点的问题,我们使用您的 IP 地址来帮助识别您的身份并收集有关您的广泛人口统计信息。</p><h4>来自其他网站的信息:</h4><p>我们可能会放置指向其他方运营的其他网站的链接,并且可能会不时在我们的促销电子邮件中包含指向第三方网站的信息和链接。其中一些其他网站包含我们的品牌名称和商标以及我们拥有的其他知识产权;其他人没有。当您点击这些链接并访问这些其他网站时,无论它们是否包含我们的品牌名称、商标和其他知识产权,您都需要注意,我们不控制这些其他网站或这些其他网站”商业惯例,并且本隐私政策不适用于这些其他网站。因此,这些其他网站的运营商可能会收集有关您的不同类型的信息,并且可能以不同于我们在网站上收集信息的方式使用和披露该信息。我们鼓励您查看他们的隐私政策,并提醒您我们不对他们的行为负责。</p><h4>信息存储:</h4><p>您理解并同意我们在服务器上存储和处理您的信息,并且通过向我们提供任何数据,您同意将此类信息传输到网站服务器。当您从我们帐户中删除文件时,您与该文件之间将不再存在任何链接。删除的文件可能会缓存在我们服务器中以节省带宽,以防其他人请求它们。</p><h4>如何联系我们:</h4><p>如果您对您提交给我们这个隐私政策或信息有任何疑问,您可以通过邮件方式联系我们</p></div><div class="tab-pane fade" id="tab3Content3"><h4>数字千年版权法案</h4><hr><p>要向我们提交版权侵权通知,您需要发送书面通知,其中包含《数字千年版权法》第 512(c)(3) 条要求和规定的信息。</p><p>要撰写适当的 DMCA 通知,请说明以下信息:</p><ol><li>表明自己是您认为受到侵犯的版权作品或专有权的所有者,或代表此类所有者行事的人,并提供物理签名(纸质形式时)或电子签名(电子形式时) )。</li><li>指明您认为受到侵权的受版权保护的作品,或者如果有大量作品受到侵权,请提供作品的代表性清单。</li><li>通过在我们站点上提供包含这些材料的 Web URL,确定侵犯您的版权作品的材料的位置。请不要发送附加的图像、pdf 或其他文件格式的文件,而只能发送包含我们网址的列表。在信函正文中提供所有 URL 是帮助我们快速处理您的请求的最佳方式。</li><li>说明您的联系信息,包括您的姓名、街道地址、电话号码和电子邮件地址。如果您代表版权所有者行事,请同时说明您与版权所有者的关系(例如律师、供应商、代理人)。</li><li>声明您“真诚地相信上述材料的使用未经版权所有者、其代理人或法律授权”。</li><li>声明通知中的信息准确无误,否则会受到伪证处罚。为此,请包括以下声明: “我发誓,通知中的信息准确无误,并且我是(版权)所有者或被授权代表专有权的所有者行事,在作伪证的处罚下涉嫌侵权”。</li><li>请用中文书写,所有电子邮件是任何其他语言都将被忽略。</li><li>要行使您的 DMCA 权利,您必须将适当的 DMCA 通知发送至我们的指定代理至电子邮件。</li></ol><p>未能包含上述所有信息可能会导致 DMCA 通知的处理延迟。请注意,根据 DMCA 第 512(f) 条,任何故意歪曲材料或活动侵权的人都可能需要承担责任。</p><p>如果我们收到声称侵犯版权的适当通知,它将通过删除或禁止访问声称侵权或成为侵权活动主题的材料迅速做出回应。请注意,我们不一定会向报告者发送有关删除操作的确认。</p></div></div></div></div></div>', + 'update'=>'2025-12-31 23:36:32' + ); \ No newline at end of file diff --git a/public/config/install.lock b/public/config/install.lock new file mode 100755 index 0000000..a491d9a --- /dev/null +++ b/public/config/install.lock @@ -0,0 +1 @@ +安装程序锁定文件。 \ No newline at end of file diff --git a/public/favicon/22.png b/public/favicon/22.png new file mode 100644 index 0000000..446e826 Binary files /dev/null and b/public/favicon/22.png differ diff --git a/public/favicon/foot-ga.png b/public/favicon/foot-ga.png new file mode 100644 index 0000000..08a72ef Binary files /dev/null and b/public/favicon/foot-ga.png differ diff --git a/public/favicon/foot-icp.png b/public/favicon/foot-icp.png new file mode 100644 index 0000000..04d9209 Binary files /dev/null and b/public/favicon/foot-icp.png differ diff --git a/public/favicon/ie-cx.png b/public/favicon/ie-cx.png new file mode 100644 index 0000000..79516bc Binary files /dev/null and b/public/favicon/ie-cx.png differ diff --git a/public/favicon/nodeseek.png b/public/favicon/nodeseek.png new file mode 100644 index 0000000..fd378d5 Binary files /dev/null and b/public/favicon/nodeseek.png differ diff --git a/public/favicon/yurn.png b/public/favicon/yurn.png new file mode 100644 index 0000000..ea3926a Binary files /dev/null and b/public/favicon/yurn.png differ diff --git a/public/js/random.js b/public/js/random.js new file mode 100644 index 0000000..7e56046 --- /dev/null +++ b/public/js/random.js @@ -0,0 +1,134 @@ +/** + * Static Random Pic API + * Generated by build script + */ +(function() { + var counts = {"h":979,"v":3596}; + var domain = 'https://pic.acofork.com'; + + // State management for session consistency + var sessionRandomH = null; + var sessionRandomV = null; + + // Helper: Get random URL for a type (h or v), persistent per session + function getRandomUrl(type) { + if (!counts[type] || counts[type] === 0) return ''; + + // Return existing session URL if available + if (type === 'h' && sessionRandomH) return sessionRandomH; + if (type === 'v' && sessionRandomV) return sessionRandomV; + + // Generate new if not exists + var num = Math.floor(Math.random() * counts[type]) + 1; + var url = domain + '/ri/' + type + '/' + num + '.webp'; + + // Save to session state + if (type === 'h') sessionRandomH = url; + if (type === 'v') sessionRandomV = url; + + return url; + } + + // Expose global functions + window.getRandomPicH = function() { return getRandomUrl('h'); }; + window.getRandomPicV = function() { return getRandomUrl('v'); }; + + // 1. Logic for Background (Customized based on user request) + function setRandomBackground() { + // Get random URL using the helper (Dynamic count & domain) + const bgUrl = getRandomUrl('h'); + + // Find the background box element + const bgBox = document.getElementById('bg-box'); + + if (bgBox) { + // Preload image + const img = new Image(); + img.onload = function() { + bgBox.style.backgroundImage = `url('${bgUrl}')`; + bgBox.classList.add('loaded'); + console.log('Random background loaded:', bgUrl); + + // Set CSS variables for transparency effects + document.documentElement.style.setProperty('--card-bg', 'var(--card-bg-transparent)'); + document.documentElement.style.setProperty('--float-panel-bg', 'var(--float-panel-bg-transparent)'); + }; + img.onerror = function() { + console.error('Failed to load background image:', bgUrl); + }; + img.src = bgUrl; + } else { + // Fallback: If no #bg-box, check for data-random-bg for backward compatibility/other elements + // This keeps the generic functionality available if needed, but prioritizes the user's specific logic above. + initGenericBackgrounds(); + } + } + + // 2. Logic for Image Tags (Generic) + function initImgTags() { + var imgTags = document.getElementsByTagName('img'); + for (var i = 0; i < imgTags.length; i++) { + var img = imgTags[i]; + var alt = img.getAttribute('alt'); + var src = img.getAttribute('src'); + + if (alt === 'random:h' || (src && src.indexOf('/random/h') !== -1)) { + img.src = getRandomUrl('h'); + } else if (alt === 'random:v' || (src && src.indexOf('/random/v') !== -1)) { + img.src = getRandomUrl('v'); + } + } + } + + // Helper for generic data-random-bg (as a backup or secondary feature) + function initGenericBackgrounds() { + var bgElements = document.querySelectorAll('[data-random-bg]'); + bgElements.forEach(function(el) { + // Skip if it is the bg-box we already handled (though setRandomBackground handles #bg-box specifically) + if (el.id === 'bg-box') return; + + var type = el.getAttribute('data-random-bg'); + if (type === 'h' || type === 'v') { + var url = getRandomUrl(type); + if (url) { + var img = new Image(); + img.onload = function() { + el.style.backgroundImage = 'url("' + url + '")'; + el.classList.add('loaded'); + }; + img.src = url; + } + } + }); + } + + function init() { + setRandomBackground(); + initImgTags(); + } + + // Run on initial load + if (document.readyState === 'loading') { + document.addEventListener('DOMContentLoaded', init); + } else { + init(); + } + + // Swup integration + function setupSwup() { + if (window.swup && window.swup.hooks) { + // Register hook for content replacement + window.swup.hooks.on('content:replace', init); + console.log('Random Pic API: Registered with Swup hooks.'); + } + } + + if (window.swup) { + setupSwup(); + } else { + document.addEventListener('swup:enable', setupSwup); + } + + // Legacy Swup support + document.addEventListener('swup:contentReplaced', init); +})(); \ No newline at end of file diff --git a/public/js/umami-share.js b/public/js/umami-share.js new file mode 100644 index 0000000..08900a1 --- /dev/null +++ b/public/js/umami-share.js @@ -0,0 +1,91 @@ +(function (global) { + const cacheKey = 'umami-share-cache'; + const cacheTTL = 3600_000; // 1h + + async function fetchShareData(baseUrl, shareId) { + const cached = localStorage.getItem(cacheKey); + if (cached) { + try { + const parsed = JSON.parse(cached); + if (Date.now() - parsed.timestamp < cacheTTL) { + return parsed.value; + } + } catch { + localStorage.removeItem(cacheKey); + } + } + const res = await fetch(`${baseUrl}/api/share/${shareId}`); + if (!res.ok) { + throw new Error('获取 Umami 分享信息失败'); + } + const data = await res.json(); + localStorage.setItem(cacheKey, JSON.stringify({ timestamp: Date.now(), value: data })); + return data; + } + + /** + * 获取 Umami 分享数据(websiteId、token) + * 在缓存 TTL 内复用;并用全局 Promise 避免并发请求 + * @param {string} baseUrl + * @param {string} shareId + * @returns {Promise<{websiteId: string, token: string}>} + */ + global.getUmamiShareData = function (baseUrl, shareId) { + if (!global.__umamiSharePromise) { + global.__umamiSharePromise = fetchShareData(baseUrl, shareId).catch((err) => { + delete global.__umamiSharePromise; + throw err; + }); + } + return global.__umamiSharePromise; + }; + + global.clearUmamiShareCache = function () { + localStorage.removeItem(cacheKey); + delete global.__umamiSharePromise; + }; + + /** + * 获取 Umami 统计数据 + * 自动处理 token 获取和过期重试 + * @param {string} baseUrl + * @param {string} shareId + * @param {object} queryParams + * @returns {Promise<any>} + */ + global.fetchUmamiStats = async function (baseUrl, shareId, queryParams) { + async function doFetch(isRetry = false) { + const { websiteId, token } = await global.getUmamiShareData(baseUrl, shareId); + const currentTimestamp = Date.now(); + const params = new URLSearchParams({ + startAt: 0, + endAt: currentTimestamp, + unit: 'hour', + timezone: queryParams.timezone || 'Asia/Shanghai', + compare: false, + ...queryParams + }); + + const statsUrl = `${baseUrl}/api/websites/${websiteId}/stats?${params.toString()}`; + + const res = await fetch(statsUrl, { + headers: { + 'x-umami-share-token': token + } + }); + + if (!res.ok) { + if (res.status === 401 && !isRetry) { + global.clearUmamiShareCache(); + return doFetch(true); + } + throw new Error('获取统计数据失败'); + } + + return await res.json(); + } + + return doFetch(); + }; + +})(window); \ No newline at end of file diff --git a/public/sponsors/alipay.svg b/public/sponsors/alipay.svg new file mode 100644 index 0000000..fbeb73c --- /dev/null +++ b/public/sponsors/alipay.svg @@ -0,0 +1,867 @@ +<?xml version="1.0" encoding="utf-8"?> +<svg width="540" height="540" viewBox="0 0 540 540" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink"> +<rect width="540" height="540" fill="#ffffff"/> +<defs> +<rect id="p" width="12" height="12" /> +</defs> +<g> +<use x="24" y="24" xlink:href="#p" fill="#000000" /> +<use x="36" y="24" xlink:href="#p" fill="#000000" /> +<use x="48" y="24" xlink:href="#p" fill="#000000" /> +<use x="60" y="24" xlink:href="#p" fill="#000000" /> +<use x="72" y="24" xlink:href="#p" fill="#000000" /> +<use x="84" y="24" xlink:href="#p" fill="#000000" /> +<use x="96" y="24" xlink:href="#p" fill="#000000" /> +<use x="120" y="24" xlink:href="#p" fill="#000000" /> +<use x="156" y="24" xlink:href="#p" fill="#000000" /> +<use x="180" y="24" xlink:href="#p" fill="#000000" /> +<use x="204" y="24" xlink:href="#p" fill="#000000" /> +<use x="216" y="24" xlink:href="#p" fill="#000000" /> +<use x="228" y="24" xlink:href="#p" fill="#000000" /> +<use x="252" y="24" xlink:href="#p" fill="#000000" /> +<use x="276" y="24" xlink:href="#p" fill="#000000" /> +<use x="288" y="24" xlink:href="#p" fill="#000000" /> +<use x="300" y="24" xlink:href="#p" fill="#000000" /> +<use x="336" y="24" xlink:href="#p" fill="#000000" /> +<use x="360" y="24" xlink:href="#p" fill="#000000" /> +<use x="372" y="24" xlink:href="#p" fill="#000000" /> +<use x="384" y="24" xlink:href="#p" fill="#000000" /> +<use x="396" y="24" xlink:href="#p" fill="#000000" /> +<use x="432" y="24" xlink:href="#p" fill="#000000" /> +<use x="444" y="24" xlink:href="#p" fill="#000000" /> +<use x="456" y="24" xlink:href="#p" fill="#000000" /> +<use x="468" y="24" xlink:href="#p" fill="#000000" /> +<use x="480" y="24" xlink:href="#p" fill="#000000" /> +<use x="492" y="24" xlink:href="#p" fill="#000000" /> +<use x="504" y="24" xlink:href="#p" fill="#000000" /> +<use x="24" y="36" xlink:href="#p" fill="#000000" /> +<use x="96" y="36" xlink:href="#p" fill="#000000" /> +<use x="132" y="36" xlink:href="#p" fill="#000000" /> +<use x="192" y="36" xlink:href="#p" fill="#000000" /> +<use x="216" y="36" xlink:href="#p" fill="#000000" /> +<use x="240" y="36" xlink:href="#p" fill="#000000" /> +<use x="252" y="36" xlink:href="#p" fill="#000000" /> +<use x="300" y="36" xlink:href="#p" fill="#000000" /> +<use x="312" y="36" xlink:href="#p" fill="#000000" /> +<use x="324" y="36" xlink:href="#p" fill="#000000" /> +<use x="348" y="36" xlink:href="#p" fill="#000000" /> +<use x="360" y="36" xlink:href="#p" fill="#000000" /> +<use x="384" y="36" xlink:href="#p" fill="#000000" /> +<use x="432" y="36" xlink:href="#p" fill="#000000" /> +<use x="504" y="36" xlink:href="#p" fill="#000000" /> +<use x="24" y="48" xlink:href="#p" fill="#000000" /> +<use x="48" y="48" xlink:href="#p" fill="#000000" /> +<use x="60" y="48" xlink:href="#p" fill="#000000" /> +<use x="72" y="48" xlink:href="#p" fill="#000000" /> +<use x="96" y="48" xlink:href="#p" fill="#000000" /> +<use x="132" y="48" xlink:href="#p" fill="#000000" /> +<use x="156" y="48" xlink:href="#p" fill="#000000" /> +<use x="192" y="48" xlink:href="#p" fill="#000000" /> +<use x="216" y="48" xlink:href="#p" fill="#000000" /> +<use x="240" y="48" xlink:href="#p" fill="#000000" /> +<use x="252" y="48" xlink:href="#p" fill="#000000" /> +<use x="264" y="48" xlink:href="#p" fill="#000000" /> +<use x="276" y="48" xlink:href="#p" fill="#000000" /> +<use x="288" y="48" xlink:href="#p" fill="#000000" /> +<use x="348" y="48" xlink:href="#p" fill="#000000" /> +<use x="372" y="48" xlink:href="#p" fill="#000000" /> +<use x="384" y="48" xlink:href="#p" fill="#000000" /> +<use x="432" y="48" xlink:href="#p" fill="#000000" /> +<use x="456" y="48" xlink:href="#p" fill="#000000" /> +<use x="468" y="48" xlink:href="#p" fill="#000000" /> +<use x="480" y="48" xlink:href="#p" fill="#000000" /> +<use x="504" y="48" xlink:href="#p" fill="#000000" /> +<use x="24" y="60" xlink:href="#p" fill="#000000" /> +<use x="48" y="60" xlink:href="#p" fill="#000000" /> +<use x="60" y="60" xlink:href="#p" fill="#000000" /> +<use x="72" y="60" xlink:href="#p" fill="#000000" /> +<use x="96" y="60" xlink:href="#p" fill="#000000" /> +<use x="120" y="60" xlink:href="#p" fill="#000000" /> +<use x="132" y="60" xlink:href="#p" fill="#000000" /> +<use x="144" y="60" xlink:href="#p" fill="#000000" /> +<use x="168" y="60" xlink:href="#p" fill="#000000" /> +<use x="180" y="60" xlink:href="#p" fill="#000000" /> +<use x="192" y="60" xlink:href="#p" fill="#000000" /> +<use x="216" y="60" xlink:href="#p" fill="#000000" /> +<use x="240" y="60" xlink:href="#p" fill="#000000" /> +<use x="252" y="60" xlink:href="#p" fill="#000000" /> +<use x="264" y="60" xlink:href="#p" fill="#000000" /> +<use x="288" y="60" xlink:href="#p" fill="#000000" /> +<use x="300" y="60" xlink:href="#p" fill="#000000" /> +<use x="312" y="60" xlink:href="#p" fill="#000000" /> +<use x="324" y="60" xlink:href="#p" fill="#000000" /> +<use x="360" y="60" xlink:href="#p" fill="#000000" /> +<use x="372" y="60" xlink:href="#p" fill="#000000" /> +<use x="384" y="60" xlink:href="#p" fill="#000000" /> +<use x="432" y="60" xlink:href="#p" fill="#000000" /> +<use x="456" y="60" xlink:href="#p" fill="#000000" /> +<use x="468" y="60" xlink:href="#p" fill="#000000" /> +<use x="480" y="60" xlink:href="#p" fill="#000000" /> +<use x="504" y="60" xlink:href="#p" fill="#000000" /> +<use x="24" y="72" xlink:href="#p" fill="#000000" /> +<use x="48" y="72" xlink:href="#p" fill="#000000" /> +<use x="60" y="72" xlink:href="#p" fill="#000000" /> +<use x="72" y="72" xlink:href="#p" fill="#000000" /> +<use x="96" y="72" xlink:href="#p" fill="#000000" /> +<use x="132" y="72" xlink:href="#p" fill="#000000" /> +<use x="204" y="72" xlink:href="#p" fill="#000000" /> +<use x="240" y="72" xlink:href="#p" fill="#000000" /> +<use x="276" y="72" xlink:href="#p" fill="#000000" /> +<use x="288" y="72" xlink:href="#p" fill="#000000" /> +<use x="348" y="72" xlink:href="#p" fill="#000000" /> +<use x="384" y="72" xlink:href="#p" fill="#000000" /> +<use x="396" y="72" xlink:href="#p" fill="#000000" /> +<use x="432" y="72" xlink:href="#p" fill="#000000" /> +<use x="456" y="72" xlink:href="#p" fill="#000000" /> +<use x="468" y="72" xlink:href="#p" fill="#000000" /> +<use x="480" y="72" xlink:href="#p" fill="#000000" /> +<use x="504" y="72" xlink:href="#p" fill="#000000" /> +<use x="24" y="84" xlink:href="#p" fill="#000000" /> +<use x="96" y="84" xlink:href="#p" fill="#000000" /> +<use x="180" y="84" xlink:href="#p" fill="#000000" /> +<use x="192" y="84" xlink:href="#p" fill="#000000" /> +<use x="204" y="84" xlink:href="#p" fill="#000000" /> +<use x="216" y="84" xlink:href="#p" fill="#000000" /> +<use x="228" y="84" xlink:href="#p" fill="#000000" /> +<use x="240" y="84" xlink:href="#p" fill="#000000" /> +<use x="264" y="84" xlink:href="#p" fill="#000000" /> +<use x="276" y="84" xlink:href="#p" fill="#000000" /> +<use x="288" y="84" xlink:href="#p" fill="#000000" /> +<use x="312" y="84" xlink:href="#p" fill="#000000" /> +<use x="324" y="84" xlink:href="#p" fill="#000000" /> +<use x="336" y="84" xlink:href="#p" fill="#000000" /> +<use x="348" y="84" xlink:href="#p" fill="#000000" /> +<use x="360" y="84" xlink:href="#p" fill="#000000" /> +<use x="372" y="84" xlink:href="#p" fill="#000000" /> +<use x="396" y="84" xlink:href="#p" fill="#000000" /> +<use x="432" y="84" xlink:href="#p" fill="#000000" /> +<use x="504" y="84" xlink:href="#p" fill="#000000" /> +<use x="24" y="96" xlink:href="#p" fill="#000000" /> +<use x="36" y="96" xlink:href="#p" fill="#000000" /> +<use x="48" y="96" xlink:href="#p" fill="#000000" /> +<use x="60" y="96" xlink:href="#p" fill="#000000" /> +<use x="72" y="96" xlink:href="#p" fill="#000000" /> +<use x="84" y="96" xlink:href="#p" fill="#000000" /> +<use x="96" y="96" xlink:href="#p" fill="#000000" /> +<use x="120" y="96" xlink:href="#p" fill="#000000" /> +<use x="144" y="96" xlink:href="#p" fill="#000000" /> +<use x="168" y="96" xlink:href="#p" fill="#000000" /> +<use x="192" y="96" xlink:href="#p" fill="#000000" /> +<use x="216" y="96" xlink:href="#p" fill="#000000" /> +<use x="240" y="96" xlink:href="#p" fill="#000000" /> +<use x="264" y="96" xlink:href="#p" fill="#000000" /> +<use x="288" y="96" xlink:href="#p" fill="#000000" /> +<use x="312" y="96" xlink:href="#p" fill="#000000" /> +<use x="336" y="96" xlink:href="#p" fill="#000000" /> +<use x="360" y="96" xlink:href="#p" fill="#000000" /> +<use x="384" y="96" xlink:href="#p" fill="#000000" /> +<use x="408" y="96" xlink:href="#p" fill="#000000" /> +<use x="432" y="96" xlink:href="#p" fill="#000000" /> +<use x="444" y="96" xlink:href="#p" fill="#000000" /> +<use x="456" y="96" xlink:href="#p" fill="#000000" /> +<use x="468" y="96" xlink:href="#p" fill="#000000" /> +<use x="480" y="96" xlink:href="#p" fill="#000000" /> +<use x="492" y="96" xlink:href="#p" fill="#000000" /> +<use x="504" y="96" xlink:href="#p" fill="#000000" /> +<use x="132" y="108" xlink:href="#p" fill="#000000" /> +<use x="168" y="108" xlink:href="#p" fill="#000000" /> +<use x="180" y="108" xlink:href="#p" fill="#000000" /> +<use x="192" y="108" xlink:href="#p" fill="#000000" /> +<use x="252" y="108" xlink:href="#p" fill="#000000" /> +<use x="264" y="108" xlink:href="#p" fill="#000000" /> +<use x="276" y="108" xlink:href="#p" fill="#000000" /> +<use x="300" y="108" xlink:href="#p" fill="#000000" /> +<use x="312" y="108" xlink:href="#p" fill="#000000" /> +<use x="336" y="108" xlink:href="#p" fill="#000000" /> +<use x="348" y="108" xlink:href="#p" fill="#000000" /> +<use x="360" y="108" xlink:href="#p" fill="#000000" /> +<use x="372" y="108" xlink:href="#p" fill="#000000" /> +<use x="384" y="108" xlink:href="#p" fill="#000000" /> +<use x="408" y="108" xlink:href="#p" fill="#000000" /> +<use x="48" y="120" xlink:href="#p" fill="#000000" /> +<use x="72" y="120" xlink:href="#p" fill="#000000" /> +<use x="84" y="120" xlink:href="#p" fill="#000000" /> +<use x="96" y="120" xlink:href="#p" fill="#000000" /> +<use x="120" y="120" xlink:href="#p" fill="#000000" /> +<use x="144" y="120" xlink:href="#p" fill="#000000" /> +<use x="168" y="120" xlink:href="#p" fill="#000000" /> +<use x="180" y="120" xlink:href="#p" fill="#000000" /> +<use x="192" y="120" xlink:href="#p" fill="#000000" /> +<use x="204" y="120" xlink:href="#p" fill="#000000" /> +<use x="216" y="120" xlink:href="#p" fill="#000000" /> +<use x="228" y="120" xlink:href="#p" fill="#000000" /> +<use x="240" y="120" xlink:href="#p" fill="#000000" /> +<use x="288" y="120" xlink:href="#p" fill="#000000" /> +<use x="300" y="120" xlink:href="#p" fill="#000000" /> +<use x="336" y="120" xlink:href="#p" fill="#000000" /> +<use x="360" y="120" xlink:href="#p" fill="#000000" /> +<use x="372" y="120" xlink:href="#p" fill="#000000" /> +<use x="420" y="120" xlink:href="#p" fill="#000000" /> +<use x="468" y="120" xlink:href="#p" fill="#000000" /> +<use x="504" y="120" xlink:href="#p" fill="#000000" /> +<use x="36" y="132" xlink:href="#p" fill="#000000" /> +<use x="48" y="132" xlink:href="#p" fill="#000000" /> +<use x="60" y="132" xlink:href="#p" fill="#000000" /> +<use x="72" y="132" xlink:href="#p" fill="#000000" /> +<use x="84" y="132" xlink:href="#p" fill="#000000" /> +<use x="108" y="132" xlink:href="#p" fill="#000000" /> +<use x="132" y="132" xlink:href="#p" fill="#000000" /> +<use x="168" y="132" xlink:href="#p" fill="#000000" /> +<use x="180" y="132" xlink:href="#p" fill="#000000" /> +<use x="192" y="132" xlink:href="#p" fill="#000000" /> +<use x="204" y="132" xlink:href="#p" fill="#000000" /> +<use x="264" y="132" xlink:href="#p" fill="#000000" /> +<use x="276" y="132" xlink:href="#p" fill="#000000" /> +<use x="288" y="132" xlink:href="#p" fill="#000000" /> +<use x="312" y="132" xlink:href="#p" fill="#000000" /> +<use x="360" y="132" xlink:href="#p" fill="#000000" /> +<use x="384" y="132" xlink:href="#p" fill="#000000" /> +<use x="408" y="132" xlink:href="#p" fill="#000000" /> +<use x="420" y="132" xlink:href="#p" fill="#000000" /> +<use x="432" y="132" xlink:href="#p" fill="#000000" /> +<use x="444" y="132" xlink:href="#p" fill="#000000" /> +<use x="456" y="132" xlink:href="#p" fill="#000000" /> +<use x="468" y="132" xlink:href="#p" fill="#000000" /> +<use x="480" y="132" xlink:href="#p" fill="#000000" /> +<use x="492" y="132" xlink:href="#p" fill="#000000" /> +<use x="24" y="144" xlink:href="#p" fill="#000000" /> +<use x="60" y="144" xlink:href="#p" fill="#000000" /> +<use x="96" y="144" xlink:href="#p" fill="#000000" /> +<use x="108" y="144" xlink:href="#p" fill="#000000" /> +<use x="144" y="144" xlink:href="#p" fill="#000000" /> +<use x="180" y="144" xlink:href="#p" fill="#000000" /> +<use x="192" y="144" xlink:href="#p" fill="#000000" /> +<use x="204" y="144" xlink:href="#p" fill="#000000" /> +<use x="240" y="144" xlink:href="#p" fill="#000000" /> +<use x="264" y="144" xlink:href="#p" fill="#000000" /> +<use x="276" y="144" xlink:href="#p" fill="#000000" /> +<use x="288" y="144" xlink:href="#p" fill="#000000" /> +<use x="300" y="144" xlink:href="#p" fill="#000000" /> +<use x="312" y="144" xlink:href="#p" fill="#000000" /> +<use x="324" y="144" xlink:href="#p" fill="#000000" /> +<use x="360" y="144" xlink:href="#p" fill="#000000" /> +<use x="372" y="144" xlink:href="#p" fill="#000000" /> +<use x="384" y="144" xlink:href="#p" fill="#000000" /> +<use x="420" y="144" xlink:href="#p" fill="#000000" /> +<use x="456" y="144" xlink:href="#p" fill="#000000" /> +<use x="480" y="144" xlink:href="#p" fill="#000000" /> +<use x="24" y="156" xlink:href="#p" fill="#000000" /> +<use x="60" y="156" xlink:href="#p" fill="#000000" /> +<use x="84" y="156" xlink:href="#p" fill="#000000" /> +<use x="108" y="156" xlink:href="#p" fill="#000000" /> +<use x="120" y="156" xlink:href="#p" fill="#000000" /> +<use x="132" y="156" xlink:href="#p" fill="#000000" /> +<use x="156" y="156" xlink:href="#p" fill="#000000" /> +<use x="180" y="156" xlink:href="#p" fill="#000000" /> +<use x="192" y="156" xlink:href="#p" fill="#000000" /> +<use x="204" y="156" xlink:href="#p" fill="#000000" /> +<use x="240" y="156" xlink:href="#p" fill="#000000" /> +<use x="252" y="156" xlink:href="#p" fill="#000000" /> +<use x="288" y="156" xlink:href="#p" fill="#000000" /> +<use x="348" y="156" xlink:href="#p" fill="#000000" /> +<use x="360" y="156" xlink:href="#p" fill="#000000" /> +<use x="372" y="156" xlink:href="#p" fill="#000000" /> +<use x="384" y="156" xlink:href="#p" fill="#000000" /> +<use x="408" y="156" xlink:href="#p" fill="#000000" /> +<use x="420" y="156" xlink:href="#p" fill="#000000" /> +<use x="432" y="156" xlink:href="#p" fill="#000000" /> +<use x="456" y="156" xlink:href="#p" fill="#000000" /> +<use x="492" y="156" xlink:href="#p" fill="#000000" /> +<use x="504" y="156" xlink:href="#p" fill="#000000" /> +<use x="24" y="168" xlink:href="#p" fill="#000000" /> +<use x="36" y="168" xlink:href="#p" fill="#000000" /> +<use x="96" y="168" xlink:href="#p" fill="#000000" /> +<use x="108" y="168" xlink:href="#p" fill="#000000" /> +<use x="120" y="168" xlink:href="#p" fill="#000000" /> +<use x="132" y="168" xlink:href="#p" fill="#000000" /> +<use x="168" y="168" xlink:href="#p" fill="#000000" /> +<use x="180" y="168" xlink:href="#p" fill="#000000" /> +<use x="192" y="168" xlink:href="#p" fill="#000000" /> +<use x="276" y="168" xlink:href="#p" fill="#000000" /> +<use x="288" y="168" xlink:href="#p" fill="#000000" /> +<use x="336" y="168" xlink:href="#p" fill="#000000" /> +<use x="348" y="168" xlink:href="#p" fill="#000000" /> +<use x="360" y="168" xlink:href="#p" fill="#000000" /> +<use x="372" y="168" xlink:href="#p" fill="#000000" /> +<use x="396" y="168" xlink:href="#p" fill="#000000" /> +<use x="420" y="168" xlink:href="#p" fill="#000000" /> +<use x="468" y="168" xlink:href="#p" fill="#000000" /> +<use x="492" y="168" xlink:href="#p" fill="#000000" /> +<use x="504" y="168" xlink:href="#p" fill="#000000" /> +<use x="36" y="180" xlink:href="#p" fill="#000000" /> +<use x="48" y="180" xlink:href="#p" fill="#000000" /> +<use x="60" y="180" xlink:href="#p" fill="#000000" /> +<use x="84" y="180" xlink:href="#p" fill="#000000" /> +<use x="156" y="180" xlink:href="#p" fill="#000000" /> +<use x="180" y="180" xlink:href="#p" fill="#000000" /> +<use x="192" y="180" xlink:href="#p" fill="#000000" /> +<use x="204" y="180" xlink:href="#p" fill="#000000" /> +<use x="240" y="180" xlink:href="#p" fill="#000000" /> +<use x="264" y="180" xlink:href="#p" fill="#000000" /> +<use x="288" y="180" xlink:href="#p" fill="#000000" /> +<use x="300" y="180" xlink:href="#p" fill="#000000" /> +<use x="324" y="180" xlink:href="#p" fill="#000000" /> +<use x="348" y="180" xlink:href="#p" fill="#000000" /> +<use x="360" y="180" xlink:href="#p" fill="#000000" /> +<use x="384" y="180" xlink:href="#p" fill="#000000" /> +<use x="396" y="180" xlink:href="#p" fill="#000000" /> +<use x="420" y="180" xlink:href="#p" fill="#000000" /> +<use x="468" y="180" xlink:href="#p" fill="#000000" /> +<use x="492" y="180" xlink:href="#p" fill="#000000" /> +<use x="504" y="180" xlink:href="#p" fill="#000000" /> +<use x="48" y="192" xlink:href="#p" fill="#000000" /> +<use x="96" y="192" xlink:href="#p" fill="#000000" /> +<use x="108" y="192" xlink:href="#p" fill="#000000" /> +<use x="132" y="192" xlink:href="#p" fill="#000000" /> +<use x="144" y="192" xlink:href="#p" fill="#000000" /> +<use x="180" y="192" xlink:href="#p" fill="#000000" /> +<use x="204" y="192" xlink:href="#p" fill="#000000" /> +<use x="240" y="192" xlink:href="#p" fill="#000000" /> +<use x="276" y="192" xlink:href="#p" fill="#000000" /> +<use x="288" y="192" xlink:href="#p" fill="#000000" /> +<use x="312" y="192" xlink:href="#p" fill="#000000" /> +<use x="336" y="192" xlink:href="#p" fill="#000000" /> +<use x="348" y="192" xlink:href="#p" fill="#000000" /> +<use x="360" y="192" xlink:href="#p" fill="#000000" /> +<use x="432" y="192" xlink:href="#p" fill="#000000" /> +<use x="444" y="192" xlink:href="#p" fill="#000000" /> +<use x="468" y="192" xlink:href="#p" fill="#000000" /> +<use x="480" y="192" xlink:href="#p" fill="#000000" /> +<use x="492" y="192" xlink:href="#p" fill="#000000" /> +<use x="36" y="204" xlink:href="#p" fill="#000000" /> +<use x="60" y="204" xlink:href="#p" fill="#000000" /> +<use x="120" y="204" xlink:href="#p" fill="#000000" /> +<use x="228" y="204" xlink:href="#p" fill="#000000" /> +<use x="264" y="204" xlink:href="#p" fill="#000000" /> +<use x="276" y="204" xlink:href="#p" fill="#000000" /> +<use x="288" y="204" xlink:href="#p" fill="#000000" /> +<use x="312" y="204" xlink:href="#p" fill="#000000" /> +<use x="324" y="204" xlink:href="#p" fill="#000000" /> +<use x="336" y="204" xlink:href="#p" fill="#000000" /> +<use x="360" y="204" xlink:href="#p" fill="#000000" /> +<use x="372" y="204" xlink:href="#p" fill="#000000" /> +<use x="384" y="204" xlink:href="#p" fill="#000000" /> +<use x="396" y="204" xlink:href="#p" fill="#000000" /> +<use x="432" y="204" xlink:href="#p" fill="#000000" /> +<use x="444" y="204" xlink:href="#p" fill="#000000" /> +<use x="24" y="216" xlink:href="#p" fill="#000000" /> +<use x="48" y="216" xlink:href="#p" fill="#000000" /> +<use x="96" y="216" xlink:href="#p" fill="#000000" /> +<use x="120" y="216" xlink:href="#p" fill="#000000" /> +<use x="180" y="216" xlink:href="#p" fill="#000000" /> +<use x="204" y="216" xlink:href="#p" fill="#000000" /> +<use x="228" y="216" xlink:href="#p" fill="#000000" /> +<use x="252" y="216" xlink:href="#p" fill="#000000" /> +<use x="264" y="216" xlink:href="#p" fill="#000000" /> +<use x="288" y="216" xlink:href="#p" fill="#000000" /> +<use x="300" y="216" xlink:href="#p" fill="#000000" /> +<use x="324" y="216" xlink:href="#p" fill="#000000" /> +<use x="348" y="216" xlink:href="#p" fill="#000000" /> +<use x="360" y="216" xlink:href="#p" fill="#000000" /> +<use x="372" y="216" xlink:href="#p" fill="#000000" /> +<use x="408" y="216" xlink:href="#p" fill="#000000" /> +<use x="432" y="216" xlink:href="#p" fill="#000000" /> +<use x="444" y="216" xlink:href="#p" fill="#000000" /> +<use x="468" y="216" xlink:href="#p" fill="#000000" /> +<use x="504" y="216" xlink:href="#p" fill="#000000" /> +<use x="24" y="228" xlink:href="#p" fill="#000000" /> +<use x="60" y="228" xlink:href="#p" fill="#000000" /> +<use x="84" y="228" xlink:href="#p" fill="#000000" /> +<use x="108" y="228" xlink:href="#p" fill="#000000" /> +<use x="120" y="228" xlink:href="#p" fill="#000000" /> +<use x="156" y="228" xlink:href="#p" fill="#000000" /> +<use x="168" y="228" xlink:href="#p" fill="#000000" /> +<use x="180" y="228" xlink:href="#p" fill="#000000" /> +<use x="216" y="228" xlink:href="#p" fill="#000000" /> +<use x="228" y="228" xlink:href="#p" fill="#000000" /> +<use x="240" y="228" xlink:href="#p" fill="#000000" /> +<use x="252" y="228" xlink:href="#p" fill="#000000" /> +<use x="264" y="228" xlink:href="#p" fill="#000000" /> +<use x="276" y="228" xlink:href="#p" fill="#000000" /> +<use x="288" y="228" xlink:href="#p" fill="#000000" /> +<use x="324" y="228" xlink:href="#p" fill="#000000" /> +<use x="336" y="228" xlink:href="#p" fill="#000000" /> +<use x="372" y="228" xlink:href="#p" fill="#000000" /> +<use x="432" y="228" xlink:href="#p" fill="#000000" /> +<use x="480" y="228" xlink:href="#p" fill="#000000" /> +<use x="492" y="228" xlink:href="#p" fill="#000000" /> +<use x="504" y="228" xlink:href="#p" fill="#000000" /> +<use x="24" y="240" xlink:href="#p" fill="#000000" /> +<use x="84" y="240" xlink:href="#p" fill="#000000" /> +<use x="96" y="240" xlink:href="#p" fill="#000000" /> +<use x="120" y="240" xlink:href="#p" fill="#000000" /> +<use x="132" y="240" xlink:href="#p" fill="#000000" /> +<use x="144" y="240" xlink:href="#p" fill="#000000" /> +<use x="180" y="240" xlink:href="#p" fill="#000000" /> +<use x="204" y="240" xlink:href="#p" fill="#000000" /> +<use x="228" y="240" xlink:href="#p" fill="#000000" /> +<use x="240" y="240" xlink:href="#p" fill="#000000" /> +<use x="252" y="240" xlink:href="#p" fill="#000000" /> +<use x="276" y="240" xlink:href="#p" fill="#000000" /> +<use x="348" y="240" xlink:href="#p" fill="#000000" /> +<use x="360" y="240" xlink:href="#p" fill="#000000" /> +<use x="384" y="240" xlink:href="#p" fill="#000000" /> +<use x="420" y="240" xlink:href="#p" fill="#000000" /> +<use x="456" y="240" xlink:href="#p" fill="#000000" /> +<use x="468" y="240" xlink:href="#p" fill="#000000" /> +<use x="480" y="240" xlink:href="#p" fill="#000000" /> +<use x="492" y="240" xlink:href="#p" fill="#000000" /> +<use x="504" y="240" xlink:href="#p" fill="#000000" /> +<use x="24" y="252" xlink:href="#p" fill="#000000" /> +<use x="48" y="252" xlink:href="#p" fill="#000000" /> +<use x="60" y="252" xlink:href="#p" fill="#000000" /> +<use x="108" y="252" xlink:href="#p" fill="#000000" /> +<use x="132" y="252" xlink:href="#p" fill="#000000" /> +<use x="144" y="252" xlink:href="#p" fill="#000000" /> +<use x="168" y="252" xlink:href="#p" fill="#000000" /> +<use x="264" y="252" xlink:href="#p" fill="#000000" /> +<use x="312" y="252" xlink:href="#p" fill="#000000" /> +<use x="324" y="252" xlink:href="#p" fill="#000000" /> +<use x="360" y="252" xlink:href="#p" fill="#000000" /> +<use x="372" y="252" xlink:href="#p" fill="#000000" /> +<use x="384" y="252" xlink:href="#p" fill="#000000" /> +<use x="408" y="252" xlink:href="#p" fill="#000000" /> +<use x="432" y="252" xlink:href="#p" fill="#000000" /> +<use x="444" y="252" xlink:href="#p" fill="#000000" /> +<use x="480" y="252" xlink:href="#p" fill="#000000" /> +<use x="492" y="252" xlink:href="#p" fill="#000000" /> +<use x="24" y="264" xlink:href="#p" fill="#000000" /> +<use x="36" y="264" xlink:href="#p" fill="#000000" /> +<use x="48" y="264" xlink:href="#p" fill="#000000" /> +<use x="84" y="264" xlink:href="#p" fill="#000000" /> +<use x="96" y="264" xlink:href="#p" fill="#000000" /> +<use x="132" y="264" xlink:href="#p" fill="#000000" /> +<use x="144" y="264" xlink:href="#p" fill="#000000" /> +<use x="168" y="264" xlink:href="#p" fill="#000000" /> +<use x="180" y="264" xlink:href="#p" fill="#000000" /> +<use x="192" y="264" xlink:href="#p" fill="#000000" /> +<use x="228" y="264" xlink:href="#p" fill="#000000" /> +<use x="252" y="264" xlink:href="#p" fill="#000000" /> +<use x="288" y="264" xlink:href="#p" fill="#000000" /> +<use x="300" y="264" xlink:href="#p" fill="#000000" /> +<use x="348" y="264" xlink:href="#p" fill="#000000" /> +<use x="384" y="264" xlink:href="#p" fill="#000000" /> +<use x="396" y="264" xlink:href="#p" fill="#000000" /> +<use x="408" y="264" xlink:href="#p" fill="#000000" /> +<use x="420" y="264" xlink:href="#p" fill="#000000" /> +<use x="432" y="264" xlink:href="#p" fill="#000000" /> +<use x="444" y="264" xlink:href="#p" fill="#000000" /> +<use x="456" y="264" xlink:href="#p" fill="#000000" /> +<use x="480" y="264" xlink:href="#p" fill="#000000" /> +<use x="504" y="264" xlink:href="#p" fill="#000000" /> +<use x="24" y="276" xlink:href="#p" fill="#000000" /> +<use x="36" y="276" xlink:href="#p" fill="#000000" /> +<use x="48" y="276" xlink:href="#p" fill="#000000" /> +<use x="60" y="276" xlink:href="#p" fill="#000000" /> +<use x="84" y="276" xlink:href="#p" fill="#000000" /> +<use x="120" y="276" xlink:href="#p" fill="#000000" /> +<use x="132" y="276" xlink:href="#p" fill="#000000" /> +<use x="144" y="276" xlink:href="#p" fill="#000000" /> +<use x="168" y="276" xlink:href="#p" fill="#000000" /> +<use x="180" y="276" xlink:href="#p" fill="#000000" /> +<use x="192" y="276" xlink:href="#p" fill="#000000" /> +<use x="204" y="276" xlink:href="#p" fill="#000000" /> +<use x="216" y="276" xlink:href="#p" fill="#000000" /> +<use x="228" y="276" xlink:href="#p" fill="#000000" /> +<use x="240" y="276" xlink:href="#p" fill="#000000" /> +<use x="276" y="276" xlink:href="#p" fill="#000000" /> +<use x="312" y="276" xlink:href="#p" fill="#000000" /> +<use x="336" y="276" xlink:href="#p" fill="#000000" /> +<use x="372" y="276" xlink:href="#p" fill="#000000" /> +<use x="384" y="276" xlink:href="#p" fill="#000000" /> +<use x="408" y="276" xlink:href="#p" fill="#000000" /> +<use x="420" y="276" xlink:href="#p" fill="#000000" /> +<use x="432" y="276" xlink:href="#p" fill="#000000" /> +<use x="444" y="276" xlink:href="#p" fill="#000000" /> +<use x="468" y="276" xlink:href="#p" fill="#000000" /> +<use x="492" y="276" xlink:href="#p" fill="#000000" /> +<use x="504" y="276" xlink:href="#p" fill="#000000" /> +<use x="84" y="288" xlink:href="#p" fill="#000000" /> +<use x="96" y="288" xlink:href="#p" fill="#000000" /> +<use x="108" y="288" xlink:href="#p" fill="#000000" /> +<use x="168" y="288" xlink:href="#p" fill="#000000" /> +<use x="180" y="288" xlink:href="#p" fill="#000000" /> +<use x="192" y="288" xlink:href="#p" fill="#000000" /> +<use x="204" y="288" xlink:href="#p" fill="#000000" /> +<use x="228" y="288" xlink:href="#p" fill="#000000" /> +<use x="264" y="288" xlink:href="#p" fill="#000000" /> +<use x="288" y="288" xlink:href="#p" fill="#000000" /> +<use x="300" y="288" xlink:href="#p" fill="#000000" /> +<use x="312" y="288" xlink:href="#p" fill="#000000" /> +<use x="324" y="288" xlink:href="#p" fill="#000000" /> +<use x="348" y="288" xlink:href="#p" fill="#000000" /> +<use x="360" y="288" xlink:href="#p" fill="#000000" /> +<use x="372" y="288" xlink:href="#p" fill="#000000" /> +<use x="384" y="288" xlink:href="#p" fill="#000000" /> +<use x="420" y="288" xlink:href="#p" fill="#000000" /> +<use x="444" y="288" xlink:href="#p" fill="#000000" /> +<use x="456" y="288" xlink:href="#p" fill="#000000" /> +<use x="480" y="288" xlink:href="#p" fill="#000000" /> +<use x="492" y="288" xlink:href="#p" fill="#000000" /> +<use x="504" y="288" xlink:href="#p" fill="#000000" /> +<use x="48" y="300" xlink:href="#p" fill="#000000" /> +<use x="60" y="300" xlink:href="#p" fill="#000000" /> +<use x="84" y="300" xlink:href="#p" fill="#000000" /> +<use x="132" y="300" xlink:href="#p" fill="#000000" /> +<use x="144" y="300" xlink:href="#p" fill="#000000" /> +<use x="168" y="300" xlink:href="#p" fill="#000000" /> +<use x="180" y="300" xlink:href="#p" fill="#000000" /> +<use x="192" y="300" xlink:href="#p" fill="#000000" /> +<use x="228" y="300" xlink:href="#p" fill="#000000" /> +<use x="240" y="300" xlink:href="#p" fill="#000000" /> +<use x="252" y="300" xlink:href="#p" fill="#000000" /> +<use x="264" y="300" xlink:href="#p" fill="#000000" /> +<use x="300" y="300" xlink:href="#p" fill="#000000" /> +<use x="324" y="300" xlink:href="#p" fill="#000000" /> +<use x="336" y="300" xlink:href="#p" fill="#000000" /> +<use x="348" y="300" xlink:href="#p" fill="#000000" /> +<use x="360" y="300" xlink:href="#p" fill="#000000" /> +<use x="372" y="300" xlink:href="#p" fill="#000000" /> +<use x="408" y="300" xlink:href="#p" fill="#000000" /> +<use x="420" y="300" xlink:href="#p" fill="#000000" /> +<use x="444" y="300" xlink:href="#p" fill="#000000" /> +<use x="468" y="300" xlink:href="#p" fill="#000000" /> +<use x="24" y="312" xlink:href="#p" fill="#000000" /> +<use x="72" y="312" xlink:href="#p" fill="#000000" /> +<use x="96" y="312" xlink:href="#p" fill="#000000" /> +<use x="108" y="312" xlink:href="#p" fill="#000000" /> +<use x="120" y="312" xlink:href="#p" fill="#000000" /> +<use x="168" y="312" xlink:href="#p" fill="#000000" /> +<use x="180" y="312" xlink:href="#p" fill="#000000" /> +<use x="192" y="312" xlink:href="#p" fill="#000000" /> +<use x="204" y="312" xlink:href="#p" fill="#000000" /> +<use x="240" y="312" xlink:href="#p" fill="#000000" /> +<use x="264" y="312" xlink:href="#p" fill="#000000" /> +<use x="276" y="312" xlink:href="#p" fill="#000000" /> +<use x="300" y="312" xlink:href="#p" fill="#000000" /> +<use x="324" y="312" xlink:href="#p" fill="#000000" /> +<use x="336" y="312" xlink:href="#p" fill="#000000" /> +<use x="360" y="312" xlink:href="#p" fill="#000000" /> +<use x="372" y="312" xlink:href="#p" fill="#000000" /> +<use x="384" y="312" xlink:href="#p" fill="#000000" /> +<use x="396" y="312" xlink:href="#p" fill="#000000" /> +<use x="408" y="312" xlink:href="#p" fill="#000000" /> +<use x="432" y="312" xlink:href="#p" fill="#000000" /> +<use x="456" y="312" xlink:href="#p" fill="#000000" /> +<use x="480" y="312" xlink:href="#p" fill="#000000" /> +<use x="36" y="324" xlink:href="#p" fill="#000000" /> +<use x="48" y="324" xlink:href="#p" fill="#000000" /> +<use x="84" y="324" xlink:href="#p" fill="#000000" /> +<use x="120" y="324" xlink:href="#p" fill="#000000" /> +<use x="144" y="324" xlink:href="#p" fill="#000000" /> +<use x="156" y="324" xlink:href="#p" fill="#000000" /> +<use x="204" y="324" xlink:href="#p" fill="#000000" /> +<use x="216" y="324" xlink:href="#p" fill="#000000" /> +<use x="252" y="324" xlink:href="#p" fill="#000000" /> +<use x="360" y="324" xlink:href="#p" fill="#000000" /> +<use x="420" y="324" xlink:href="#p" fill="#000000" /> +<use x="432" y="324" xlink:href="#p" fill="#000000" /> +<use x="492" y="324" xlink:href="#p" fill="#000000" /> +<use x="504" y="324" xlink:href="#p" fill="#000000" /> +<use x="48" y="336" xlink:href="#p" fill="#000000" /> +<use x="60" y="336" xlink:href="#p" fill="#000000" /> +<use x="96" y="336" xlink:href="#p" fill="#000000" /> +<use x="132" y="336" xlink:href="#p" fill="#000000" /> +<use x="204" y="336" xlink:href="#p" fill="#000000" /> +<use x="216" y="336" xlink:href="#p" fill="#000000" /> +<use x="240" y="336" xlink:href="#p" fill="#000000" /> +<use x="252" y="336" xlink:href="#p" fill="#000000" /> +<use x="300" y="336" xlink:href="#p" fill="#000000" /> +<use x="312" y="336" xlink:href="#p" fill="#000000" /> +<use x="336" y="336" xlink:href="#p" fill="#000000" /> +<use x="384" y="336" xlink:href="#p" fill="#000000" /> +<use x="432" y="336" xlink:href="#p" fill="#000000" /> +<use x="444" y="336" xlink:href="#p" fill="#000000" /> +<use x="480" y="336" xlink:href="#p" fill="#000000" /> +<use x="492" y="336" xlink:href="#p" fill="#000000" /> +<use x="504" y="336" xlink:href="#p" fill="#000000" /> +<use x="24" y="348" xlink:href="#p" fill="#000000" /> +<use x="60" y="348" xlink:href="#p" fill="#000000" /> +<use x="84" y="348" xlink:href="#p" fill="#000000" /> +<use x="108" y="348" xlink:href="#p" fill="#000000" /> +<use x="120" y="348" xlink:href="#p" fill="#000000" /> +<use x="144" y="348" xlink:href="#p" fill="#000000" /> +<use x="156" y="348" xlink:href="#p" fill="#000000" /> +<use x="180" y="348" xlink:href="#p" fill="#000000" /> +<use x="192" y="348" xlink:href="#p" fill="#000000" /> +<use x="216" y="348" xlink:href="#p" fill="#000000" /> +<use x="228" y="348" xlink:href="#p" fill="#000000" /> +<use x="240" y="348" xlink:href="#p" fill="#000000" /> +<use x="252" y="348" xlink:href="#p" fill="#000000" /> +<use x="288" y="348" xlink:href="#p" fill="#000000" /> +<use x="300" y="348" xlink:href="#p" fill="#000000" /> +<use x="312" y="348" xlink:href="#p" fill="#000000" /> +<use x="336" y="348" xlink:href="#p" fill="#000000" /> +<use x="348" y="348" xlink:href="#p" fill="#000000" /> +<use x="372" y="348" xlink:href="#p" fill="#000000" /> +<use x="408" y="348" xlink:href="#p" fill="#000000" /> +<use x="456" y="348" xlink:href="#p" fill="#000000" /> +<use x="492" y="348" xlink:href="#p" fill="#000000" /> +<use x="504" y="348" xlink:href="#p" fill="#000000" /> +<use x="84" y="360" xlink:href="#p" fill="#000000" /> +<use x="96" y="360" xlink:href="#p" fill="#000000" /> +<use x="120" y="360" xlink:href="#p" fill="#000000" /> +<use x="156" y="360" xlink:href="#p" fill="#000000" /> +<use x="192" y="360" xlink:href="#p" fill="#000000" /> +<use x="204" y="360" xlink:href="#p" fill="#000000" /> +<use x="228" y="360" xlink:href="#p" fill="#000000" /> +<use x="252" y="360" xlink:href="#p" fill="#000000" /> +<use x="264" y="360" xlink:href="#p" fill="#000000" /> +<use x="276" y="360" xlink:href="#p" fill="#000000" /> +<use x="300" y="360" xlink:href="#p" fill="#000000" /> +<use x="384" y="360" xlink:href="#p" fill="#000000" /> +<use x="396" y="360" xlink:href="#p" fill="#000000" /> +<use x="408" y="360" xlink:href="#p" fill="#000000" /> +<use x="432" y="360" xlink:href="#p" fill="#000000" /> +<use x="468" y="360" xlink:href="#p" fill="#000000" /> +<use x="492" y="360" xlink:href="#p" fill="#000000" /> +<use x="504" y="360" xlink:href="#p" fill="#000000" /> +<use x="48" y="372" xlink:href="#p" fill="#000000" /> +<use x="60" y="372" xlink:href="#p" fill="#000000" /> +<use x="144" y="372" xlink:href="#p" fill="#000000" /> +<use x="156" y="372" xlink:href="#p" fill="#000000" /> +<use x="168" y="372" xlink:href="#p" fill="#000000" /> +<use x="204" y="372" xlink:href="#p" fill="#000000" /> +<use x="252" y="372" xlink:href="#p" fill="#000000" /> +<use x="264" y="372" xlink:href="#p" fill="#000000" /> +<use x="324" y="372" xlink:href="#p" fill="#000000" /> +<use x="336" y="372" xlink:href="#p" fill="#000000" /> +<use x="348" y="372" xlink:href="#p" fill="#000000" /> +<use x="384" y="372" xlink:href="#p" fill="#000000" /> +<use x="396" y="372" xlink:href="#p" fill="#000000" /> +<use x="408" y="372" xlink:href="#p" fill="#000000" /> +<use x="468" y="372" xlink:href="#p" fill="#000000" /> +<use x="480" y="372" xlink:href="#p" fill="#000000" /> +<use x="492" y="372" xlink:href="#p" fill="#000000" /> +<use x="24" y="384" xlink:href="#p" fill="#000000" /> +<use x="48" y="384" xlink:href="#p" fill="#000000" /> +<use x="72" y="384" xlink:href="#p" fill="#000000" /> +<use x="96" y="384" xlink:href="#p" fill="#000000" /> +<use x="108" y="384" xlink:href="#p" fill="#000000" /> +<use x="132" y="384" xlink:href="#p" fill="#000000" /> +<use x="156" y="384" xlink:href="#p" fill="#000000" /> +<use x="192" y="384" xlink:href="#p" fill="#000000" /> +<use x="204" y="384" xlink:href="#p" fill="#000000" /> +<use x="216" y="384" xlink:href="#p" fill="#000000" /> +<use x="228" y="384" xlink:href="#p" fill="#000000" /> +<use x="240" y="384" xlink:href="#p" fill="#000000" /> +<use x="252" y="384" xlink:href="#p" fill="#000000" /> +<use x="312" y="384" xlink:href="#p" fill="#000000" /> +<use x="396" y="384" xlink:href="#p" fill="#000000" /> +<use x="408" y="384" xlink:href="#p" fill="#000000" /> +<use x="468" y="384" xlink:href="#p" fill="#000000" /> +<use x="480" y="384" xlink:href="#p" fill="#000000" /> +<use x="36" y="396" xlink:href="#p" fill="#000000" /> +<use x="72" y="396" xlink:href="#p" fill="#000000" /> +<use x="108" y="396" xlink:href="#p" fill="#000000" /> +<use x="120" y="396" xlink:href="#p" fill="#000000" /> +<use x="132" y="396" xlink:href="#p" fill="#000000" /> +<use x="144" y="396" xlink:href="#p" fill="#000000" /> +<use x="156" y="396" xlink:href="#p" fill="#000000" /> +<use x="180" y="396" xlink:href="#p" fill="#000000" /> +<use x="204" y="396" xlink:href="#p" fill="#000000" /> +<use x="228" y="396" xlink:href="#p" fill="#000000" /> +<use x="240" y="396" xlink:href="#p" fill="#000000" /> +<use x="252" y="396" xlink:href="#p" fill="#000000" /> +<use x="264" y="396" xlink:href="#p" fill="#000000" /> +<use x="276" y="396" xlink:href="#p" fill="#000000" /> +<use x="300" y="396" xlink:href="#p" fill="#000000" /> +<use x="312" y="396" xlink:href="#p" fill="#000000" /> +<use x="324" y="396" xlink:href="#p" fill="#000000" /> +<use x="348" y="396" xlink:href="#p" fill="#000000" /> +<use x="408" y="396" xlink:href="#p" fill="#000000" /> +<use x="420" y="396" xlink:href="#p" fill="#000000" /> +<use x="432" y="396" xlink:href="#p" fill="#000000" /> +<use x="456" y="396" xlink:href="#p" fill="#000000" /> +<use x="468" y="396" xlink:href="#p" fill="#000000" /> +<use x="492" y="396" xlink:href="#p" fill="#000000" /> +<use x="24" y="408" xlink:href="#p" fill="#000000" /> +<use x="48" y="408" xlink:href="#p" fill="#000000" /> +<use x="60" y="408" xlink:href="#p" fill="#000000" /> +<use x="72" y="408" xlink:href="#p" fill="#000000" /> +<use x="96" y="408" xlink:href="#p" fill="#000000" /> +<use x="108" y="408" xlink:href="#p" fill="#000000" /> +<use x="144" y="408" xlink:href="#p" fill="#000000" /> +<use x="180" y="408" xlink:href="#p" fill="#000000" /> +<use x="192" y="408" xlink:href="#p" fill="#000000" /> +<use x="216" y="408" xlink:href="#p" fill="#000000" /> +<use x="228" y="408" xlink:href="#p" fill="#000000" /> +<use x="240" y="408" xlink:href="#p" fill="#000000" /> +<use x="252" y="408" xlink:href="#p" fill="#000000" /> +<use x="264" y="408" xlink:href="#p" fill="#000000" /> +<use x="288" y="408" xlink:href="#p" fill="#000000" /> +<use x="300" y="408" xlink:href="#p" fill="#000000" /> +<use x="324" y="408" xlink:href="#p" fill="#000000" /> +<use x="336" y="408" xlink:href="#p" fill="#000000" /> +<use x="360" y="408" xlink:href="#p" fill="#000000" /> +<use x="396" y="408" xlink:href="#p" fill="#000000" /> +<use x="408" y="408" xlink:href="#p" fill="#000000" /> +<use x="420" y="408" xlink:href="#p" fill="#000000" /> +<use x="432" y="408" xlink:href="#p" fill="#000000" /> +<use x="444" y="408" xlink:href="#p" fill="#000000" /> +<use x="456" y="408" xlink:href="#p" fill="#000000" /> +<use x="492" y="408" xlink:href="#p" fill="#000000" /> +<use x="120" y="420" xlink:href="#p" fill="#000000" /> +<use x="168" y="420" xlink:href="#p" fill="#000000" /> +<use x="180" y="420" xlink:href="#p" fill="#000000" /> +<use x="204" y="420" xlink:href="#p" fill="#000000" /> +<use x="252" y="420" xlink:href="#p" fill="#000000" /> +<use x="264" y="420" xlink:href="#p" fill="#000000" /> +<use x="288" y="420" xlink:href="#p" fill="#000000" /> +<use x="312" y="420" xlink:href="#p" fill="#000000" /> +<use x="396" y="420" xlink:href="#p" fill="#000000" /> +<use x="408" y="420" xlink:href="#p" fill="#000000" /> +<use x="456" y="420" xlink:href="#p" fill="#000000" /> +<use x="480" y="420" xlink:href="#p" fill="#000000" /> +<use x="504" y="420" xlink:href="#p" fill="#000000" /> +<use x="24" y="432" xlink:href="#p" fill="#000000" /> +<use x="36" y="432" xlink:href="#p" fill="#000000" /> +<use x="48" y="432" xlink:href="#p" fill="#000000" /> +<use x="60" y="432" xlink:href="#p" fill="#000000" /> +<use x="72" y="432" xlink:href="#p" fill="#000000" /> +<use x="84" y="432" xlink:href="#p" fill="#000000" /> +<use x="96" y="432" xlink:href="#p" fill="#000000" /> +<use x="156" y="432" xlink:href="#p" fill="#000000" /> +<use x="168" y="432" xlink:href="#p" fill="#000000" /> +<use x="192" y="432" xlink:href="#p" fill="#000000" /> +<use x="204" y="432" xlink:href="#p" fill="#000000" /> +<use x="240" y="432" xlink:href="#p" fill="#000000" /> +<use x="252" y="432" xlink:href="#p" fill="#000000" /> +<use x="264" y="432" xlink:href="#p" fill="#000000" /> +<use x="288" y="432" xlink:href="#p" fill="#000000" /> +<use x="300" y="432" xlink:href="#p" fill="#000000" /> +<use x="336" y="432" xlink:href="#p" fill="#000000" /> +<use x="348" y="432" xlink:href="#p" fill="#000000" /> +<use x="360" y="432" xlink:href="#p" fill="#000000" /> +<use x="372" y="432" xlink:href="#p" fill="#000000" /> +<use x="408" y="432" xlink:href="#p" fill="#000000" /> +<use x="432" y="432" xlink:href="#p" fill="#000000" /> +<use x="456" y="432" xlink:href="#p" fill="#000000" /> +<use x="492" y="432" xlink:href="#p" fill="#000000" /> +<use x="504" y="432" xlink:href="#p" fill="#000000" /> +<use x="24" y="444" xlink:href="#p" fill="#000000" /> +<use x="96" y="444" xlink:href="#p" fill="#000000" /> +<use x="120" y="444" xlink:href="#p" fill="#000000" /> +<use x="144" y="444" xlink:href="#p" fill="#000000" /> +<use x="156" y="444" xlink:href="#p" fill="#000000" /> +<use x="180" y="444" xlink:href="#p" fill="#000000" /> +<use x="216" y="444" xlink:href="#p" fill="#000000" /> +<use x="228" y="444" xlink:href="#p" fill="#000000" /> +<use x="252" y="444" xlink:href="#p" fill="#000000" /> +<use x="288" y="444" xlink:href="#p" fill="#000000" /> +<use x="300" y="444" xlink:href="#p" fill="#000000" /> +<use x="312" y="444" xlink:href="#p" fill="#000000" /> +<use x="324" y="444" xlink:href="#p" fill="#000000" /> +<use x="348" y="444" xlink:href="#p" fill="#000000" /> +<use x="360" y="444" xlink:href="#p" fill="#000000" /> +<use x="396" y="444" xlink:href="#p" fill="#000000" /> +<use x="408" y="444" xlink:href="#p" fill="#000000" /> +<use x="456" y="444" xlink:href="#p" fill="#000000" /> +<use x="24" y="456" xlink:href="#p" fill="#000000" /> +<use x="48" y="456" xlink:href="#p" fill="#000000" /> +<use x="60" y="456" xlink:href="#p" fill="#000000" /> +<use x="72" y="456" xlink:href="#p" fill="#000000" /> +<use x="96" y="456" xlink:href="#p" fill="#000000" /> +<use x="120" y="456" xlink:href="#p" fill="#000000" /> +<use x="216" y="456" xlink:href="#p" fill="#000000" /> +<use x="240" y="456" xlink:href="#p" fill="#000000" /> +<use x="252" y="456" xlink:href="#p" fill="#000000" /> +<use x="300" y="456" xlink:href="#p" fill="#000000" /> +<use x="324" y="456" xlink:href="#p" fill="#000000" /> +<use x="336" y="456" xlink:href="#p" fill="#000000" /> +<use x="348" y="456" xlink:href="#p" fill="#000000" /> +<use x="408" y="456" xlink:href="#p" fill="#000000" /> +<use x="420" y="456" xlink:href="#p" fill="#000000" /> +<use x="432" y="456" xlink:href="#p" fill="#000000" /> +<use x="444" y="456" xlink:href="#p" fill="#000000" /> +<use x="456" y="456" xlink:href="#p" fill="#000000" /> +<use x="24" y="468" xlink:href="#p" fill="#000000" /> +<use x="48" y="468" xlink:href="#p" fill="#000000" /> +<use x="60" y="468" xlink:href="#p" fill="#000000" /> +<use x="72" y="468" xlink:href="#p" fill="#000000" /> +<use x="96" y="468" xlink:href="#p" fill="#000000" /> +<use x="144" y="468" xlink:href="#p" fill="#000000" /> +<use x="156" y="468" xlink:href="#p" fill="#000000" /> +<use x="180" y="468" xlink:href="#p" fill="#000000" /> +<use x="192" y="468" xlink:href="#p" fill="#000000" /> +<use x="204" y="468" xlink:href="#p" fill="#000000" /> +<use x="216" y="468" xlink:href="#p" fill="#000000" /> +<use x="240" y="468" xlink:href="#p" fill="#000000" /> +<use x="252" y="468" xlink:href="#p" fill="#000000" /> +<use x="264" y="468" xlink:href="#p" fill="#000000" /> +<use x="288" y="468" xlink:href="#p" fill="#000000" /> +<use x="312" y="468" xlink:href="#p" fill="#000000" /> +<use x="336" y="468" xlink:href="#p" fill="#000000" /> +<use x="348" y="468" xlink:href="#p" fill="#000000" /> +<use x="408" y="468" xlink:href="#p" fill="#000000" /> +<use x="420" y="468" xlink:href="#p" fill="#000000" /> +<use x="456" y="468" xlink:href="#p" fill="#000000" /> +<use x="480" y="468" xlink:href="#p" fill="#000000" /> +<use x="24" y="480" xlink:href="#p" fill="#000000" /> +<use x="48" y="480" xlink:href="#p" fill="#000000" /> +<use x="60" y="480" xlink:href="#p" fill="#000000" /> +<use x="72" y="480" xlink:href="#p" fill="#000000" /> +<use x="96" y="480" xlink:href="#p" fill="#000000" /> +<use x="120" y="480" xlink:href="#p" fill="#000000" /> +<use x="132" y="480" xlink:href="#p" fill="#000000" /> +<use x="144" y="480" xlink:href="#p" fill="#000000" /> +<use x="168" y="480" xlink:href="#p" fill="#000000" /> +<use x="204" y="480" xlink:href="#p" fill="#000000" /> +<use x="228" y="480" xlink:href="#p" fill="#000000" /> +<use x="276" y="480" xlink:href="#p" fill="#000000" /> +<use x="288" y="480" xlink:href="#p" fill="#000000" /> +<use x="336" y="480" xlink:href="#p" fill="#000000" /> +<use x="360" y="480" xlink:href="#p" fill="#000000" /> +<use x="372" y="480" xlink:href="#p" fill="#000000" /> +<use x="396" y="480" xlink:href="#p" fill="#000000" /> +<use x="408" y="480" xlink:href="#p" fill="#000000" /> +<use x="444" y="480" xlink:href="#p" fill="#000000" /> +<use x="456" y="480" xlink:href="#p" fill="#000000" /> +<use x="504" y="480" xlink:href="#p" fill="#000000" /> +<use x="24" y="492" xlink:href="#p" fill="#000000" /> +<use x="96" y="492" xlink:href="#p" fill="#000000" /> +<use x="144" y="492" xlink:href="#p" fill="#000000" /> +<use x="156" y="492" xlink:href="#p" fill="#000000" /> +<use x="192" y="492" xlink:href="#p" fill="#000000" /> +<use x="204" y="492" xlink:href="#p" fill="#000000" /> +<use x="240" y="492" xlink:href="#p" fill="#000000" /> +<use x="276" y="492" xlink:href="#p" fill="#000000" /> +<use x="336" y="492" xlink:href="#p" fill="#000000" /> +<use x="348" y="492" xlink:href="#p" fill="#000000" /> +<use x="372" y="492" xlink:href="#p" fill="#000000" /> +<use x="396" y="492" xlink:href="#p" fill="#000000" /> +<use x="408" y="492" xlink:href="#p" fill="#000000" /> +<use x="432" y="492" xlink:href="#p" fill="#000000" /> +<use x="444" y="492" xlink:href="#p" fill="#000000" /> +<use x="480" y="492" xlink:href="#p" fill="#000000" /> +<use x="492" y="492" xlink:href="#p" fill="#000000" /> +<use x="24" y="504" xlink:href="#p" fill="#000000" /> +<use x="36" y="504" xlink:href="#p" fill="#000000" /> +<use x="48" y="504" xlink:href="#p" fill="#000000" /> +<use x="60" y="504" xlink:href="#p" fill="#000000" /> +<use x="72" y="504" xlink:href="#p" fill="#000000" /> +<use x="84" y="504" xlink:href="#p" fill="#000000" /> +<use x="96" y="504" xlink:href="#p" fill="#000000" /> +<use x="132" y="504" xlink:href="#p" fill="#000000" /> +<use x="168" y="504" xlink:href="#p" fill="#000000" /> +<use x="180" y="504" xlink:href="#p" fill="#000000" /> +<use x="192" y="504" xlink:href="#p" fill="#000000" /> +<use x="204" y="504" xlink:href="#p" fill="#000000" /> +<use x="216" y="504" xlink:href="#p" fill="#000000" /> +<use x="228" y="504" xlink:href="#p" fill="#000000" /> +<use x="240" y="504" xlink:href="#p" fill="#000000" /> +<use x="252" y="504" xlink:href="#p" fill="#000000" /> +<use x="300" y="504" xlink:href="#p" fill="#000000" /> +<use x="324" y="504" xlink:href="#p" fill="#000000" /> +<use x="360" y="504" xlink:href="#p" fill="#000000" /> +<use x="372" y="504" xlink:href="#p" fill="#000000" /> +<use x="408" y="504" xlink:href="#p" fill="#000000" /> +<use x="420" y="504" xlink:href="#p" fill="#000000" /> +<use x="432" y="504" xlink:href="#p" fill="#000000" /> +<use x="444" y="504" xlink:href="#p" fill="#000000" /> +<use x="456" y="504" xlink:href="#p" fill="#000000" /> +<use x="480" y="504" xlink:href="#p" fill="#000000" /> +<use x="492" y="504" xlink:href="#p" fill="#000000" /> +<use x="504" y="504" xlink:href="#p" fill="#000000" /> +</g> +</svg> \ No newline at end of file diff --git a/public/sponsors/mjt.jpg b/public/sponsors/mjt.jpg new file mode 100644 index 0000000..7489e9d Binary files /dev/null and b/public/sponsors/mjt.jpg differ diff --git a/public/sponsors/wechat.svg b/public/sponsors/wechat.svg new file mode 100644 index 0000000..8fb58d1 --- /dev/null +++ b/public/sponsors/wechat.svg @@ -0,0 +1,1257 @@ +<?xml version="1.0" encoding="utf-8"?> +<svg width="636" height="636" viewBox="0 0 636 636" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink"> +<rect width="636" height="636" fill="#ffffff"/> +<defs> +<rect id="p" width="12" height="12" /> +</defs> +<g> +<use x="24" y="24" xlink:href="#p" fill="#000000" /> +<use x="36" y="24" xlink:href="#p" fill="#000000" /> +<use x="48" y="24" xlink:href="#p" fill="#000000" /> +<use x="60" y="24" xlink:href="#p" fill="#000000" /> +<use x="72" y="24" xlink:href="#p" fill="#000000" /> +<use x="84" y="24" xlink:href="#p" fill="#000000" /> +<use x="96" y="24" xlink:href="#p" fill="#000000" /> +<use x="120" y="24" xlink:href="#p" fill="#000000" /> +<use x="132" y="24" xlink:href="#p" fill="#000000" /> +<use x="144" y="24" xlink:href="#p" fill="#000000" /> +<use x="156" y="24" xlink:href="#p" fill="#000000" /> +<use x="180" y="24" xlink:href="#p" fill="#000000" /> +<use x="216" y="24" xlink:href="#p" fill="#000000" /> +<use x="240" y="24" xlink:href="#p" fill="#000000" /> +<use x="252" y="24" xlink:href="#p" fill="#000000" /> +<use x="264" y="24" xlink:href="#p" fill="#000000" /> +<use x="276" y="24" xlink:href="#p" fill="#000000" /> +<use x="288" y="24" xlink:href="#p" fill="#000000" /> +<use x="300" y="24" xlink:href="#p" fill="#000000" /> +<use x="312" y="24" xlink:href="#p" fill="#000000" /> +<use x="348" y="24" xlink:href="#p" fill="#000000" /> +<use x="372" y="24" xlink:href="#p" fill="#000000" /> +<use x="396" y="24" xlink:href="#p" fill="#000000" /> +<use x="420" y="24" xlink:href="#p" fill="#000000" /> +<use x="432" y="24" xlink:href="#p" fill="#000000" /> +<use x="456" y="24" xlink:href="#p" fill="#000000" /> +<use x="504" y="24" xlink:href="#p" fill="#000000" /> +<use x="528" y="24" xlink:href="#p" fill="#000000" /> +<use x="540" y="24" xlink:href="#p" fill="#000000" /> +<use x="552" y="24" xlink:href="#p" fill="#000000" /> +<use x="564" y="24" xlink:href="#p" fill="#000000" /> +<use x="576" y="24" xlink:href="#p" fill="#000000" /> +<use x="588" y="24" xlink:href="#p" fill="#000000" /> +<use x="600" y="24" xlink:href="#p" fill="#000000" /> +<use x="24" y="36" xlink:href="#p" fill="#000000" /> +<use x="96" y="36" xlink:href="#p" fill="#000000" /> +<use x="132" y="36" xlink:href="#p" fill="#000000" /> +<use x="144" y="36" xlink:href="#p" fill="#000000" /> +<use x="168" y="36" xlink:href="#p" fill="#000000" /> +<use x="180" y="36" xlink:href="#p" fill="#000000" /> +<use x="192" y="36" xlink:href="#p" fill="#000000" /> +<use x="204" y="36" xlink:href="#p" fill="#000000" /> +<use x="216" y="36" xlink:href="#p" fill="#000000" /> +<use x="228" y="36" xlink:href="#p" fill="#000000" /> +<use x="240" y="36" xlink:href="#p" fill="#000000" /> +<use x="252" y="36" xlink:href="#p" fill="#000000" /> +<use x="276" y="36" xlink:href="#p" fill="#000000" /> +<use x="300" y="36" xlink:href="#p" fill="#000000" /> +<use x="312" y="36" xlink:href="#p" fill="#000000" /> +<use x="348" y="36" xlink:href="#p" fill="#000000" /> +<use x="360" y="36" xlink:href="#p" fill="#000000" /> +<use x="372" y="36" xlink:href="#p" fill="#000000" /> +<use x="384" y="36" xlink:href="#p" fill="#000000" /> +<use x="396" y="36" xlink:href="#p" fill="#000000" /> +<use x="408" y="36" xlink:href="#p" fill="#000000" /> +<use x="480" y="36" xlink:href="#p" fill="#000000" /> +<use x="492" y="36" xlink:href="#p" fill="#000000" /> +<use x="504" y="36" xlink:href="#p" fill="#000000" /> +<use x="528" y="36" xlink:href="#p" fill="#000000" /> +<use x="600" y="36" xlink:href="#p" fill="#000000" /> +<use x="24" y="48" xlink:href="#p" fill="#000000" /> +<use x="48" y="48" xlink:href="#p" fill="#000000" /> +<use x="60" y="48" xlink:href="#p" fill="#000000" /> +<use x="72" y="48" xlink:href="#p" fill="#000000" /> +<use x="96" y="48" xlink:href="#p" fill="#000000" /> +<use x="132" y="48" xlink:href="#p" fill="#000000" /> +<use x="156" y="48" xlink:href="#p" fill="#000000" /> +<use x="204" y="48" xlink:href="#p" fill="#000000" /> +<use x="240" y="48" xlink:href="#p" fill="#000000" /> +<use x="264" y="48" xlink:href="#p" fill="#000000" /> +<use x="336" y="48" xlink:href="#p" fill="#000000" /> +<use x="360" y="48" xlink:href="#p" fill="#000000" /> +<use x="372" y="48" xlink:href="#p" fill="#000000" /> +<use x="384" y="48" xlink:href="#p" fill="#000000" /> +<use x="396" y="48" xlink:href="#p" fill="#000000" /> +<use x="420" y="48" xlink:href="#p" fill="#000000" /> +<use x="432" y="48" xlink:href="#p" fill="#000000" /> +<use x="444" y="48" xlink:href="#p" fill="#000000" /> +<use x="456" y="48" xlink:href="#p" fill="#000000" /> +<use x="492" y="48" xlink:href="#p" fill="#000000" /> +<use x="504" y="48" xlink:href="#p" fill="#000000" /> +<use x="528" y="48" xlink:href="#p" fill="#000000" /> +<use x="552" y="48" xlink:href="#p" fill="#000000" /> +<use x="564" y="48" xlink:href="#p" fill="#000000" /> +<use x="576" y="48" xlink:href="#p" fill="#000000" /> +<use x="600" y="48" xlink:href="#p" fill="#000000" /> +<use x="24" y="60" xlink:href="#p" fill="#000000" /> +<use x="48" y="60" xlink:href="#p" fill="#000000" /> +<use x="60" y="60" xlink:href="#p" fill="#000000" /> +<use x="72" y="60" xlink:href="#p" fill="#000000" /> +<use x="96" y="60" xlink:href="#p" fill="#000000" /> +<use x="120" y="60" xlink:href="#p" fill="#000000" /> +<use x="132" y="60" xlink:href="#p" fill="#000000" /> +<use x="144" y="60" xlink:href="#p" fill="#000000" /> +<use x="156" y="60" xlink:href="#p" fill="#000000" /> +<use x="192" y="60" xlink:href="#p" fill="#000000" /> +<use x="228" y="60" xlink:href="#p" fill="#000000" /> +<use x="312" y="60" xlink:href="#p" fill="#000000" /> +<use x="336" y="60" xlink:href="#p" fill="#000000" /> +<use x="348" y="60" xlink:href="#p" fill="#000000" /> +<use x="360" y="60" xlink:href="#p" fill="#000000" /> +<use x="372" y="60" xlink:href="#p" fill="#000000" /> +<use x="408" y="60" xlink:href="#p" fill="#000000" /> +<use x="492" y="60" xlink:href="#p" fill="#000000" /> +<use x="528" y="60" xlink:href="#p" fill="#000000" /> +<use x="552" y="60" xlink:href="#p" fill="#000000" /> +<use x="564" y="60" xlink:href="#p" fill="#000000" /> +<use x="576" y="60" xlink:href="#p" fill="#000000" /> +<use x="600" y="60" xlink:href="#p" fill="#000000" /> +<use x="24" y="72" xlink:href="#p" fill="#000000" /> +<use x="48" y="72" xlink:href="#p" fill="#000000" /> +<use x="60" y="72" xlink:href="#p" fill="#000000" /> +<use x="72" y="72" xlink:href="#p" fill="#000000" /> +<use x="96" y="72" xlink:href="#p" fill="#000000" /> +<use x="144" y="72" xlink:href="#p" fill="#000000" /> +<use x="168" y="72" xlink:href="#p" fill="#000000" /> +<use x="180" y="72" xlink:href="#p" fill="#000000" /> +<use x="204" y="72" xlink:href="#p" fill="#000000" /> +<use x="228" y="72" xlink:href="#p" fill="#000000" /> +<use x="264" y="72" xlink:href="#p" fill="#000000" /> +<use x="288" y="72" xlink:href="#p" fill="#000000" /> +<use x="300" y="72" xlink:href="#p" fill="#000000" /> +<use x="312" y="72" xlink:href="#p" fill="#000000" /> +<use x="324" y="72" xlink:href="#p" fill="#000000" /> +<use x="336" y="72" xlink:href="#p" fill="#000000" /> +<use x="360" y="72" xlink:href="#p" fill="#000000" /> +<use x="396" y="72" xlink:href="#p" fill="#000000" /> +<use x="408" y="72" xlink:href="#p" fill="#000000" /> +<use x="420" y="72" xlink:href="#p" fill="#000000" /> +<use x="432" y="72" xlink:href="#p" fill="#000000" /> +<use x="456" y="72" xlink:href="#p" fill="#000000" /> +<use x="468" y="72" xlink:href="#p" fill="#000000" /> +<use x="528" y="72" xlink:href="#p" fill="#000000" /> +<use x="552" y="72" xlink:href="#p" fill="#000000" /> +<use x="564" y="72" xlink:href="#p" fill="#000000" /> +<use x="576" y="72" xlink:href="#p" fill="#000000" /> +<use x="600" y="72" xlink:href="#p" fill="#000000" /> +<use x="24" y="84" xlink:href="#p" fill="#000000" /> +<use x="96" y="84" xlink:href="#p" fill="#000000" /> +<use x="132" y="84" xlink:href="#p" fill="#000000" /> +<use x="144" y="84" xlink:href="#p" fill="#000000" /> +<use x="156" y="84" xlink:href="#p" fill="#000000" /> +<use x="204" y="84" xlink:href="#p" fill="#000000" /> +<use x="228" y="84" xlink:href="#p" fill="#000000" /> +<use x="240" y="84" xlink:href="#p" fill="#000000" /> +<use x="252" y="84" xlink:href="#p" fill="#000000" /> +<use x="264" y="84" xlink:href="#p" fill="#000000" /> +<use x="276" y="84" xlink:href="#p" fill="#000000" /> +<use x="288" y="84" xlink:href="#p" fill="#000000" /> +<use x="336" y="84" xlink:href="#p" fill="#000000" /> +<use x="348" y="84" xlink:href="#p" fill="#000000" /> +<use x="360" y="84" xlink:href="#p" fill="#000000" /> +<use x="384" y="84" xlink:href="#p" fill="#000000" /> +<use x="396" y="84" xlink:href="#p" fill="#000000" /> +<use x="420" y="84" xlink:href="#p" fill="#000000" /> +<use x="432" y="84" xlink:href="#p" fill="#000000" /> +<use x="444" y="84" xlink:href="#p" fill="#000000" /> +<use x="480" y="84" xlink:href="#p" fill="#000000" /> +<use x="528" y="84" xlink:href="#p" fill="#000000" /> +<use x="600" y="84" xlink:href="#p" fill="#000000" /> +<use x="24" y="96" xlink:href="#p" fill="#000000" /> +<use x="36" y="96" xlink:href="#p" fill="#000000" /> +<use x="48" y="96" xlink:href="#p" fill="#000000" /> +<use x="60" y="96" xlink:href="#p" fill="#000000" /> +<use x="72" y="96" xlink:href="#p" fill="#000000" /> +<use x="84" y="96" xlink:href="#p" fill="#000000" /> +<use x="96" y="96" xlink:href="#p" fill="#000000" /> +<use x="120" y="96" xlink:href="#p" fill="#000000" /> +<use x="144" y="96" xlink:href="#p" fill="#000000" /> +<use x="168" y="96" xlink:href="#p" fill="#000000" /> +<use x="192" y="96" xlink:href="#p" fill="#000000" /> +<use x="216" y="96" xlink:href="#p" fill="#000000" /> +<use x="240" y="96" xlink:href="#p" fill="#000000" /> +<use x="264" y="96" xlink:href="#p" fill="#000000" /> +<use x="288" y="96" xlink:href="#p" fill="#000000" /> +<use x="312" y="96" xlink:href="#p" fill="#000000" /> +<use x="336" y="96" xlink:href="#p" fill="#000000" /> +<use x="360" y="96" xlink:href="#p" fill="#000000" /> +<use x="384" y="96" xlink:href="#p" fill="#000000" /> +<use x="408" y="96" xlink:href="#p" fill="#000000" /> +<use x="432" y="96" xlink:href="#p" fill="#000000" /> +<use x="456" y="96" xlink:href="#p" fill="#000000" /> +<use x="480" y="96" xlink:href="#p" fill="#000000" /> +<use x="504" y="96" xlink:href="#p" fill="#000000" /> +<use x="528" y="96" xlink:href="#p" fill="#000000" /> +<use x="540" y="96" xlink:href="#p" fill="#000000" /> +<use x="552" y="96" xlink:href="#p" fill="#000000" /> +<use x="564" y="96" xlink:href="#p" fill="#000000" /> +<use x="576" y="96" xlink:href="#p" fill="#000000" /> +<use x="588" y="96" xlink:href="#p" fill="#000000" /> +<use x="600" y="96" xlink:href="#p" fill="#000000" /> +<use x="156" y="108" xlink:href="#p" fill="#000000" /> +<use x="168" y="108" xlink:href="#p" fill="#000000" /> +<use x="180" y="108" xlink:href="#p" fill="#000000" /> +<use x="192" y="108" xlink:href="#p" fill="#000000" /> +<use x="204" y="108" xlink:href="#p" fill="#000000" /> +<use x="240" y="108" xlink:href="#p" fill="#000000" /> +<use x="264" y="108" xlink:href="#p" fill="#000000" /> +<use x="276" y="108" xlink:href="#p" fill="#000000" /> +<use x="288" y="108" xlink:href="#p" fill="#000000" /> +<use x="336" y="108" xlink:href="#p" fill="#000000" /> +<use x="348" y="108" xlink:href="#p" fill="#000000" /> +<use x="372" y="108" xlink:href="#p" fill="#000000" /> +<use x="384" y="108" xlink:href="#p" fill="#000000" /> +<use x="432" y="108" xlink:href="#p" fill="#000000" /> +<use x="468" y="108" xlink:href="#p" fill="#000000" /> +<use x="480" y="108" xlink:href="#p" fill="#000000" /> +<use x="492" y="108" xlink:href="#p" fill="#000000" /> +<use x="48" y="120" xlink:href="#p" fill="#000000" /> +<use x="72" y="120" xlink:href="#p" fill="#000000" /> +<use x="84" y="120" xlink:href="#p" fill="#000000" /> +<use x="96" y="120" xlink:href="#p" fill="#000000" /> +<use x="120" y="120" xlink:href="#p" fill="#000000" /> +<use x="156" y="120" xlink:href="#p" fill="#000000" /> +<use x="192" y="120" xlink:href="#p" fill="#000000" /> +<use x="264" y="120" xlink:href="#p" fill="#000000" /> +<use x="288" y="120" xlink:href="#p" fill="#000000" /> +<use x="300" y="120" xlink:href="#p" fill="#000000" /> +<use x="312" y="120" xlink:href="#p" fill="#000000" /> +<use x="324" y="120" xlink:href="#p" fill="#000000" /> +<use x="336" y="120" xlink:href="#p" fill="#000000" /> +<use x="348" y="120" xlink:href="#p" fill="#000000" /> +<use x="384" y="120" xlink:href="#p" fill="#000000" /> +<use x="396" y="120" xlink:href="#p" fill="#000000" /> +<use x="408" y="120" xlink:href="#p" fill="#000000" /> +<use x="420" y="120" xlink:href="#p" fill="#000000" /> +<use x="456" y="120" xlink:href="#p" fill="#000000" /> +<use x="468" y="120" xlink:href="#p" fill="#000000" /> +<use x="480" y="120" xlink:href="#p" fill="#000000" /> +<use x="504" y="120" xlink:href="#p" fill="#000000" /> +<use x="516" y="120" xlink:href="#p" fill="#000000" /> +<use x="564" y="120" xlink:href="#p" fill="#000000" /> +<use x="600" y="120" xlink:href="#p" fill="#000000" /> +<use x="24" y="132" xlink:href="#p" fill="#000000" /> +<use x="36" y="132" xlink:href="#p" fill="#000000" /> +<use x="48" y="132" xlink:href="#p" fill="#000000" /> +<use x="72" y="132" xlink:href="#p" fill="#000000" /> +<use x="84" y="132" xlink:href="#p" fill="#000000" /> +<use x="120" y="132" xlink:href="#p" fill="#000000" /> +<use x="144" y="132" xlink:href="#p" fill="#000000" /> +<use x="156" y="132" xlink:href="#p" fill="#000000" /> +<use x="192" y="132" xlink:href="#p" fill="#000000" /> +<use x="216" y="132" xlink:href="#p" fill="#000000" /> +<use x="252" y="132" xlink:href="#p" fill="#000000" /> +<use x="264" y="132" xlink:href="#p" fill="#000000" /> +<use x="276" y="132" xlink:href="#p" fill="#000000" /> +<use x="288" y="132" xlink:href="#p" fill="#000000" /> +<use x="312" y="132" xlink:href="#p" fill="#000000" /> +<use x="324" y="132" xlink:href="#p" fill="#000000" /> +<use x="336" y="132" xlink:href="#p" fill="#000000" /> +<use x="348" y="132" xlink:href="#p" fill="#000000" /> +<use x="396" y="132" xlink:href="#p" fill="#000000" /> +<use x="408" y="132" xlink:href="#p" fill="#000000" /> +<use x="552" y="132" xlink:href="#p" fill="#000000" /> +<use x="564" y="132" xlink:href="#p" fill="#000000" /> +<use x="600" y="132" xlink:href="#p" fill="#000000" /> +<use x="24" y="144" xlink:href="#p" fill="#000000" /> +<use x="36" y="144" xlink:href="#p" fill="#000000" /> +<use x="60" y="144" xlink:href="#p" fill="#000000" /> +<use x="84" y="144" xlink:href="#p" fill="#000000" /> +<use x="96" y="144" xlink:href="#p" fill="#000000" /> +<use x="120" y="144" xlink:href="#p" fill="#000000" /> +<use x="180" y="144" xlink:href="#p" fill="#000000" /> +<use x="192" y="144" xlink:href="#p" fill="#000000" /> +<use x="300" y="144" xlink:href="#p" fill="#000000" /> +<use x="312" y="144" xlink:href="#p" fill="#000000" /> +<use x="324" y="144" xlink:href="#p" fill="#000000" /> +<use x="336" y="144" xlink:href="#p" fill="#000000" /> +<use x="348" y="144" xlink:href="#p" fill="#000000" /> +<use x="372" y="144" xlink:href="#p" fill="#000000" /> +<use x="396" y="144" xlink:href="#p" fill="#000000" /> +<use x="432" y="144" xlink:href="#p" fill="#000000" /> +<use x="456" y="144" xlink:href="#p" fill="#000000" /> +<use x="468" y="144" xlink:href="#p" fill="#000000" /> +<use x="480" y="144" xlink:href="#p" fill="#000000" /> +<use x="492" y="144" xlink:href="#p" fill="#000000" /> +<use x="504" y="144" xlink:href="#p" fill="#000000" /> +<use x="516" y="144" xlink:href="#p" fill="#000000" /> +<use x="540" y="144" xlink:href="#p" fill="#000000" /> +<use x="576" y="144" xlink:href="#p" fill="#000000" /> +<use x="588" y="144" xlink:href="#p" fill="#000000" /> +<use x="600" y="144" xlink:href="#p" fill="#000000" /> +<use x="24" y="156" xlink:href="#p" fill="#000000" /> +<use x="36" y="156" xlink:href="#p" fill="#000000" /> +<use x="48" y="156" xlink:href="#p" fill="#000000" /> +<use x="60" y="156" xlink:href="#p" fill="#000000" /> +<use x="84" y="156" xlink:href="#p" fill="#000000" /> +<use x="108" y="156" xlink:href="#p" fill="#000000" /> +<use x="156" y="156" xlink:href="#p" fill="#000000" /> +<use x="216" y="156" xlink:href="#p" fill="#000000" /> +<use x="252" y="156" xlink:href="#p" fill="#000000" /> +<use x="264" y="156" xlink:href="#p" fill="#000000" /> +<use x="276" y="156" xlink:href="#p" fill="#000000" /> +<use x="300" y="156" xlink:href="#p" fill="#000000" /> +<use x="312" y="156" xlink:href="#p" fill="#000000" /> +<use x="324" y="156" xlink:href="#p" fill="#000000" /> +<use x="336" y="156" xlink:href="#p" fill="#000000" /> +<use x="348" y="156" xlink:href="#p" fill="#000000" /> +<use x="396" y="156" xlink:href="#p" fill="#000000" /> +<use x="444" y="156" xlink:href="#p" fill="#000000" /> +<use x="468" y="156" xlink:href="#p" fill="#000000" /> +<use x="480" y="156" xlink:href="#p" fill="#000000" /> +<use x="492" y="156" xlink:href="#p" fill="#000000" /> +<use x="516" y="156" xlink:href="#p" fill="#000000" /> +<use x="552" y="156" xlink:href="#p" fill="#000000" /> +<use x="600" y="156" xlink:href="#p" fill="#000000" /> +<use x="36" y="168" xlink:href="#p" fill="#000000" /> +<use x="72" y="168" xlink:href="#p" fill="#000000" /> +<use x="84" y="168" xlink:href="#p" fill="#000000" /> +<use x="96" y="168" xlink:href="#p" fill="#000000" /> +<use x="120" y="168" xlink:href="#p" fill="#000000" /> +<use x="132" y="168" xlink:href="#p" fill="#000000" /> +<use x="144" y="168" xlink:href="#p" fill="#000000" /> +<use x="180" y="168" xlink:href="#p" fill="#000000" /> +<use x="228" y="168" xlink:href="#p" fill="#000000" /> +<use x="276" y="168" xlink:href="#p" fill="#000000" /> +<use x="288" y="168" xlink:href="#p" fill="#000000" /> +<use x="300" y="168" xlink:href="#p" fill="#000000" /> +<use x="312" y="168" xlink:href="#p" fill="#000000" /> +<use x="360" y="168" xlink:href="#p" fill="#000000" /> +<use x="372" y="168" xlink:href="#p" fill="#000000" /> +<use x="396" y="168" xlink:href="#p" fill="#000000" /> +<use x="420" y="168" xlink:href="#p" fill="#000000" /> +<use x="432" y="168" xlink:href="#p" fill="#000000" /> +<use x="444" y="168" xlink:href="#p" fill="#000000" /> +<use x="456" y="168" xlink:href="#p" fill="#000000" /> +<use x="468" y="168" xlink:href="#p" fill="#000000" /> +<use x="516" y="168" xlink:href="#p" fill="#000000" /> +<use x="528" y="168" xlink:href="#p" fill="#000000" /> +<use x="552" y="168" xlink:href="#p" fill="#000000" /> +<use x="564" y="168" xlink:href="#p" fill="#000000" /> +<use x="588" y="168" xlink:href="#p" fill="#000000" /> +<use x="600" y="168" xlink:href="#p" fill="#000000" /> +<use x="24" y="180" xlink:href="#p" fill="#000000" /> +<use x="36" y="180" xlink:href="#p" fill="#000000" /> +<use x="48" y="180" xlink:href="#p" fill="#000000" /> +<use x="60" y="180" xlink:href="#p" fill="#000000" /> +<use x="72" y="180" xlink:href="#p" fill="#000000" /> +<use x="84" y="180" xlink:href="#p" fill="#000000" /> +<use x="132" y="180" xlink:href="#p" fill="#000000" /> +<use x="144" y="180" xlink:href="#p" fill="#000000" /> +<use x="156" y="180" xlink:href="#p" fill="#000000" /> +<use x="168" y="180" xlink:href="#p" fill="#000000" /> +<use x="180" y="180" xlink:href="#p" fill="#000000" /> +<use x="192" y="180" xlink:href="#p" fill="#000000" /> +<use x="204" y="180" xlink:href="#p" fill="#000000" /> +<use x="240" y="180" xlink:href="#p" fill="#000000" /> +<use x="252" y="180" xlink:href="#p" fill="#000000" /> +<use x="276" y="180" xlink:href="#p" fill="#000000" /> +<use x="300" y="180" xlink:href="#p" fill="#000000" /> +<use x="348" y="180" xlink:href="#p" fill="#000000" /> +<use x="372" y="180" xlink:href="#p" fill="#000000" /> +<use x="396" y="180" xlink:href="#p" fill="#000000" /> +<use x="408" y="180" xlink:href="#p" fill="#000000" /> +<use x="456" y="180" xlink:href="#p" fill="#000000" /> +<use x="540" y="180" xlink:href="#p" fill="#000000" /> +<use x="564" y="180" xlink:href="#p" fill="#000000" /> +<use x="576" y="180" xlink:href="#p" fill="#000000" /> +<use x="588" y="180" xlink:href="#p" fill="#000000" /> +<use x="600" y="180" xlink:href="#p" fill="#000000" /> +<use x="24" y="192" xlink:href="#p" fill="#000000" /> +<use x="48" y="192" xlink:href="#p" fill="#000000" /> +<use x="60" y="192" xlink:href="#p" fill="#000000" /> +<use x="84" y="192" xlink:href="#p" fill="#000000" /> +<use x="96" y="192" xlink:href="#p" fill="#000000" /> +<use x="108" y="192" xlink:href="#p" fill="#000000" /> +<use x="132" y="192" xlink:href="#p" fill="#000000" /> +<use x="168" y="192" xlink:href="#p" fill="#000000" /> +<use x="180" y="192" xlink:href="#p" fill="#000000" /> +<use x="216" y="192" xlink:href="#p" fill="#000000" /> +<use x="240" y="192" xlink:href="#p" fill="#000000" /> +<use x="252" y="192" xlink:href="#p" fill="#000000" /> +<use x="336" y="192" xlink:href="#p" fill="#000000" /> +<use x="348" y="192" xlink:href="#p" fill="#000000" /> +<use x="360" y="192" xlink:href="#p" fill="#000000" /> +<use x="456" y="192" xlink:href="#p" fill="#000000" /> +<use x="480" y="192" xlink:href="#p" fill="#000000" /> +<use x="492" y="192" xlink:href="#p" fill="#000000" /> +<use x="540" y="192" xlink:href="#p" fill="#000000" /> +<use x="552" y="192" xlink:href="#p" fill="#000000" /> +<use x="564" y="192" xlink:href="#p" fill="#000000" /> +<use x="576" y="192" xlink:href="#p" fill="#000000" /> +<use x="588" y="192" xlink:href="#p" fill="#000000" /> +<use x="600" y="192" xlink:href="#p" fill="#000000" /> +<use x="48" y="204" xlink:href="#p" fill="#000000" /> +<use x="60" y="204" xlink:href="#p" fill="#000000" /> +<use x="108" y="204" xlink:href="#p" fill="#000000" /> +<use x="120" y="204" xlink:href="#p" fill="#000000" /> +<use x="132" y="204" xlink:href="#p" fill="#000000" /> +<use x="156" y="204" xlink:href="#p" fill="#000000" /> +<use x="168" y="204" xlink:href="#p" fill="#000000" /> +<use x="180" y="204" xlink:href="#p" fill="#000000" /> +<use x="228" y="204" xlink:href="#p" fill="#000000" /> +<use x="276" y="204" xlink:href="#p" fill="#000000" /> +<use x="300" y="204" xlink:href="#p" fill="#000000" /> +<use x="312" y="204" xlink:href="#p" fill="#000000" /> +<use x="324" y="204" xlink:href="#p" fill="#000000" /> +<use x="348" y="204" xlink:href="#p" fill="#000000" /> +<use x="360" y="204" xlink:href="#p" fill="#000000" /> +<use x="372" y="204" xlink:href="#p" fill="#000000" /> +<use x="384" y="204" xlink:href="#p" fill="#000000" /> +<use x="408" y="204" xlink:href="#p" fill="#000000" /> +<use x="432" y="204" xlink:href="#p" fill="#000000" /> +<use x="480" y="204" xlink:href="#p" fill="#000000" /> +<use x="504" y="204" xlink:href="#p" fill="#000000" /> +<use x="516" y="204" xlink:href="#p" fill="#000000" /> +<use x="552" y="204" xlink:href="#p" fill="#000000" /> +<use x="588" y="204" xlink:href="#p" fill="#000000" /> +<use x="60" y="216" xlink:href="#p" fill="#000000" /> +<use x="84" y="216" xlink:href="#p" fill="#000000" /> +<use x="96" y="216" xlink:href="#p" fill="#000000" /> +<use x="108" y="216" xlink:href="#p" fill="#000000" /> +<use x="144" y="216" xlink:href="#p" fill="#000000" /> +<use x="204" y="216" xlink:href="#p" fill="#000000" /> +<use x="348" y="216" xlink:href="#p" fill="#000000" /> +<use x="360" y="216" xlink:href="#p" fill="#000000" /> +<use x="372" y="216" xlink:href="#p" fill="#000000" /> +<use x="396" y="216" xlink:href="#p" fill="#000000" /> +<use x="444" y="216" xlink:href="#p" fill="#000000" /> +<use x="456" y="216" xlink:href="#p" fill="#000000" /> +<use x="492" y="216" xlink:href="#p" fill="#000000" /> +<use x="504" y="216" xlink:href="#p" fill="#000000" /> +<use x="516" y="216" xlink:href="#p" fill="#000000" /> +<use x="540" y="216" xlink:href="#p" fill="#000000" /> +<use x="552" y="216" xlink:href="#p" fill="#000000" /> +<use x="564" y="216" xlink:href="#p" fill="#000000" /> +<use x="24" y="228" xlink:href="#p" fill="#000000" /> +<use x="72" y="228" xlink:href="#p" fill="#000000" /> +<use x="84" y="228" xlink:href="#p" fill="#000000" /> +<use x="132" y="228" xlink:href="#p" fill="#000000" /> +<use x="156" y="228" xlink:href="#p" fill="#000000" /> +<use x="168" y="228" xlink:href="#p" fill="#000000" /> +<use x="180" y="228" xlink:href="#p" fill="#000000" /> +<use x="204" y="228" xlink:href="#p" fill="#000000" /> +<use x="216" y="228" xlink:href="#p" fill="#000000" /> +<use x="228" y="228" xlink:href="#p" fill="#000000" /> +<use x="276" y="228" xlink:href="#p" fill="#000000" /> +<use x="324" y="228" xlink:href="#p" fill="#000000" /> +<use x="336" y="228" xlink:href="#p" fill="#000000" /> +<use x="360" y="228" xlink:href="#p" fill="#000000" /> +<use x="396" y="228" xlink:href="#p" fill="#000000" /> +<use x="408" y="228" xlink:href="#p" fill="#000000" /> +<use x="432" y="228" xlink:href="#p" fill="#000000" /> +<use x="456" y="228" xlink:href="#p" fill="#000000" /> +<use x="480" y="228" xlink:href="#p" fill="#000000" /> +<use x="492" y="228" xlink:href="#p" fill="#000000" /> +<use x="516" y="228" xlink:href="#p" fill="#000000" /> +<use x="552" y="228" xlink:href="#p" fill="#000000" /> +<use x="588" y="228" xlink:href="#p" fill="#000000" /> +<use x="72" y="240" xlink:href="#p" fill="#000000" /> +<use x="84" y="240" xlink:href="#p" fill="#000000" /> +<use x="96" y="240" xlink:href="#p" fill="#000000" /> +<use x="132" y="240" xlink:href="#p" fill="#000000" /> +<use x="156" y="240" xlink:href="#p" fill="#000000" /> +<use x="168" y="240" xlink:href="#p" fill="#000000" /> +<use x="204" y="240" xlink:href="#p" fill="#000000" /> +<use x="216" y="240" xlink:href="#p" fill="#000000" /> +<use x="240" y="240" xlink:href="#p" fill="#000000" /> +<use x="312" y="240" xlink:href="#p" fill="#000000" /> +<use x="324" y="240" xlink:href="#p" fill="#000000" /> +<use x="336" y="240" xlink:href="#p" fill="#000000" /> +<use x="348" y="240" xlink:href="#p" fill="#000000" /> +<use x="360" y="240" xlink:href="#p" fill="#000000" /> +<use x="372" y="240" xlink:href="#p" fill="#000000" /> +<use x="384" y="240" xlink:href="#p" fill="#000000" /> +<use x="396" y="240" xlink:href="#p" fill="#000000" /> +<use x="408" y="240" xlink:href="#p" fill="#000000" /> +<use x="432" y="240" xlink:href="#p" fill="#000000" /> +<use x="456" y="240" xlink:href="#p" fill="#000000" /> +<use x="480" y="240" xlink:href="#p" fill="#000000" /> +<use x="492" y="240" xlink:href="#p" fill="#000000" /> +<use x="504" y="240" xlink:href="#p" fill="#000000" /> +<use x="528" y="240" xlink:href="#p" fill="#000000" /> +<use x="564" y="240" xlink:href="#p" fill="#000000" /> +<use x="588" y="240" xlink:href="#p" fill="#000000" /> +<use x="600" y="240" xlink:href="#p" fill="#000000" /> +<use x="24" y="252" xlink:href="#p" fill="#000000" /> +<use x="60" y="252" xlink:href="#p" fill="#000000" /> +<use x="72" y="252" xlink:href="#p" fill="#000000" /> +<use x="84" y="252" xlink:href="#p" fill="#000000" /> +<use x="120" y="252" xlink:href="#p" fill="#000000" /> +<use x="132" y="252" xlink:href="#p" fill="#000000" /> +<use x="144" y="252" xlink:href="#p" fill="#000000" /> +<use x="180" y="252" xlink:href="#p" fill="#000000" /> +<use x="192" y="252" xlink:href="#p" fill="#000000" /> +<use x="216" y="252" xlink:href="#p" fill="#000000" /> +<use x="252" y="252" xlink:href="#p" fill="#000000" /> +<use x="300" y="252" xlink:href="#p" fill="#000000" /> +<use x="324" y="252" xlink:href="#p" fill="#000000" /> +<use x="336" y="252" xlink:href="#p" fill="#000000" /> +<use x="360" y="252" xlink:href="#p" fill="#000000" /> +<use x="396" y="252" xlink:href="#p" fill="#000000" /> +<use x="420" y="252" xlink:href="#p" fill="#000000" /> +<use x="432" y="252" xlink:href="#p" fill="#000000" /> +<use x="444" y="252" xlink:href="#p" fill="#000000" /> +<use x="492" y="252" xlink:href="#p" fill="#000000" /> +<use x="516" y="252" xlink:href="#p" fill="#000000" /> +<use x="528" y="252" xlink:href="#p" fill="#000000" /> +<use x="552" y="252" xlink:href="#p" fill="#000000" /> +<use x="588" y="252" xlink:href="#p" fill="#000000" /> +<use x="72" y="264" xlink:href="#p" fill="#000000" /> +<use x="84" y="264" xlink:href="#p" fill="#000000" /> +<use x="96" y="264" xlink:href="#p" fill="#000000" /> +<use x="108" y="264" xlink:href="#p" fill="#000000" /> +<use x="120" y="264" xlink:href="#p" fill="#000000" /> +<use x="144" y="264" xlink:href="#p" fill="#000000" /> +<use x="156" y="264" xlink:href="#p" fill="#000000" /> +<use x="168" y="264" xlink:href="#p" fill="#000000" /> +<use x="180" y="264" xlink:href="#p" fill="#000000" /> +<use x="192" y="264" xlink:href="#p" fill="#000000" /> +<use x="216" y="264" xlink:href="#p" fill="#000000" /> +<use x="228" y="264" xlink:href="#p" fill="#000000" /> +<use x="240" y="264" xlink:href="#p" fill="#000000" /> +<use x="252" y="264" xlink:href="#p" fill="#000000" /> +<use x="276" y="264" xlink:href="#p" fill="#000000" /> +<use x="288" y="264" xlink:href="#p" fill="#000000" /> +<use x="312" y="264" xlink:href="#p" fill="#000000" /> +<use x="360" y="264" xlink:href="#p" fill="#000000" /> +<use x="384" y="264" xlink:href="#p" fill="#000000" /> +<use x="396" y="264" xlink:href="#p" fill="#000000" /> +<use x="408" y="264" xlink:href="#p" fill="#000000" /> +<use x="420" y="264" xlink:href="#p" fill="#000000" /> +<use x="432" y="264" xlink:href="#p" fill="#000000" /> +<use x="444" y="264" xlink:href="#p" fill="#000000" /> +<use x="456" y="264" xlink:href="#p" fill="#000000" /> +<use x="504" y="264" xlink:href="#p" fill="#000000" /> +<use x="552" y="264" xlink:href="#p" fill="#000000" /> +<use x="564" y="264" xlink:href="#p" fill="#000000" /> +<use x="132" y="276" xlink:href="#p" fill="#000000" /> +<use x="192" y="276" xlink:href="#p" fill="#000000" /> +<use x="204" y="276" xlink:href="#p" fill="#000000" /> +<use x="216" y="276" xlink:href="#p" fill="#000000" /> +<use x="228" y="276" xlink:href="#p" fill="#000000" /> +<use x="240" y="276" xlink:href="#p" fill="#000000" /> +<use x="252" y="276" xlink:href="#p" fill="#000000" /> +<use x="276" y="276" xlink:href="#p" fill="#000000" /> +<use x="288" y="276" xlink:href="#p" fill="#000000" /> +<use x="324" y="276" xlink:href="#p" fill="#000000" /> +<use x="336" y="276" xlink:href="#p" fill="#000000" /> +<use x="408" y="276" xlink:href="#p" fill="#000000" /> +<use x="456" y="276" xlink:href="#p" fill="#000000" /> +<use x="468" y="276" xlink:href="#p" fill="#000000" /> +<use x="516" y="276" xlink:href="#p" fill="#000000" /> +<use x="540" y="276" xlink:href="#p" fill="#000000" /> +<use x="600" y="276" xlink:href="#p" fill="#000000" /> +<use x="36" y="288" xlink:href="#p" fill="#000000" /> +<use x="72" y="288" xlink:href="#p" fill="#000000" /> +<use x="84" y="288" xlink:href="#p" fill="#000000" /> +<use x="96" y="288" xlink:href="#p" fill="#000000" /> +<use x="108" y="288" xlink:href="#p" fill="#000000" /> +<use x="120" y="288" xlink:href="#p" fill="#000000" /> +<use x="132" y="288" xlink:href="#p" fill="#000000" /> +<use x="144" y="288" xlink:href="#p" fill="#000000" /> +<use x="156" y="288" xlink:href="#p" fill="#000000" /> +<use x="168" y="288" xlink:href="#p" fill="#000000" /> +<use x="180" y="288" xlink:href="#p" fill="#000000" /> +<use x="192" y="288" xlink:href="#p" fill="#000000" /> +<use x="204" y="288" xlink:href="#p" fill="#000000" /> +<use x="216" y="288" xlink:href="#p" fill="#000000" /> +<use x="228" y="288" xlink:href="#p" fill="#000000" /> +<use x="240" y="288" xlink:href="#p" fill="#000000" /> +<use x="252" y="288" xlink:href="#p" fill="#000000" /> +<use x="288" y="288" xlink:href="#p" fill="#000000" /> +<use x="300" y="288" xlink:href="#p" fill="#000000" /> +<use x="312" y="288" xlink:href="#p" fill="#000000" /> +<use x="324" y="288" xlink:href="#p" fill="#000000" /> +<use x="336" y="288" xlink:href="#p" fill="#000000" /> +<use x="360" y="288" xlink:href="#p" fill="#000000" /> +<use x="408" y="288" xlink:href="#p" fill="#000000" /> +<use x="468" y="288" xlink:href="#p" fill="#000000" /> +<use x="492" y="288" xlink:href="#p" fill="#000000" /> +<use x="504" y="288" xlink:href="#p" fill="#000000" /> +<use x="516" y="288" xlink:href="#p" fill="#000000" /> +<use x="528" y="288" xlink:href="#p" fill="#000000" /> +<use x="540" y="288" xlink:href="#p" fill="#000000" /> +<use x="552" y="288" xlink:href="#p" fill="#000000" /> +<use x="588" y="288" xlink:href="#p" fill="#000000" /> +<use x="600" y="288" xlink:href="#p" fill="#000000" /> +<use x="36" y="300" xlink:href="#p" fill="#000000" /> +<use x="48" y="300" xlink:href="#p" fill="#000000" /> +<use x="72" y="300" xlink:href="#p" fill="#000000" /> +<use x="120" y="300" xlink:href="#p" fill="#000000" /> +<use x="132" y="300" xlink:href="#p" fill="#000000" /> +<use x="156" y="300" xlink:href="#p" fill="#000000" /> +<use x="180" y="300" xlink:href="#p" fill="#000000" /> +<use x="228" y="300" xlink:href="#p" fill="#000000" /> +<use x="252" y="300" xlink:href="#p" fill="#000000" /> +<use x="264" y="300" xlink:href="#p" fill="#000000" /> +<use x="276" y="300" xlink:href="#p" fill="#000000" /> +<use x="288" y="300" xlink:href="#p" fill="#000000" /> +<use x="336" y="300" xlink:href="#p" fill="#000000" /> +<use x="348" y="300" xlink:href="#p" fill="#000000" /> +<use x="360" y="300" xlink:href="#p" fill="#000000" /> +<use x="372" y="300" xlink:href="#p" fill="#000000" /> +<use x="420" y="300" xlink:href="#p" fill="#000000" /> +<use x="456" y="300" xlink:href="#p" fill="#000000" /> +<use x="468" y="300" xlink:href="#p" fill="#000000" /> +<use x="480" y="300" xlink:href="#p" fill="#000000" /> +<use x="504" y="300" xlink:href="#p" fill="#000000" /> +<use x="552" y="300" xlink:href="#p" fill="#000000" /> +<use x="588" y="300" xlink:href="#p" fill="#000000" /> +<use x="600" y="300" xlink:href="#p" fill="#000000" /> +<use x="24" y="312" xlink:href="#p" fill="#000000" /> +<use x="36" y="312" xlink:href="#p" fill="#000000" /> +<use x="60" y="312" xlink:href="#p" fill="#000000" /> +<use x="72" y="312" xlink:href="#p" fill="#000000" /> +<use x="96" y="312" xlink:href="#p" fill="#000000" /> +<use x="120" y="312" xlink:href="#p" fill="#000000" /> +<use x="132" y="312" xlink:href="#p" fill="#000000" /> +<use x="144" y="312" xlink:href="#p" fill="#000000" /> +<use x="156" y="312" xlink:href="#p" fill="#000000" /> +<use x="180" y="312" xlink:href="#p" fill="#000000" /> +<use x="204" y="312" xlink:href="#p" fill="#000000" /> +<use x="216" y="312" xlink:href="#p" fill="#000000" /> +<use x="228" y="312" xlink:href="#p" fill="#000000" /> +<use x="240" y="312" xlink:href="#p" fill="#000000" /> +<use x="264" y="312" xlink:href="#p" fill="#000000" /> +<use x="276" y="312" xlink:href="#p" fill="#000000" /> +<use x="288" y="312" xlink:href="#p" fill="#000000" /> +<use x="312" y="312" xlink:href="#p" fill="#000000" /> +<use x="336" y="312" xlink:href="#p" fill="#000000" /> +<use x="348" y="312" xlink:href="#p" fill="#000000" /> +<use x="360" y="312" xlink:href="#p" fill="#000000" /> +<use x="372" y="312" xlink:href="#p" fill="#000000" /> +<use x="408" y="312" xlink:href="#p" fill="#000000" /> +<use x="420" y="312" xlink:href="#p" fill="#000000" /> +<use x="444" y="312" xlink:href="#p" fill="#000000" /> +<use x="456" y="312" xlink:href="#p" fill="#000000" /> +<use x="468" y="312" xlink:href="#p" fill="#000000" /> +<use x="480" y="312" xlink:href="#p" fill="#000000" /> +<use x="504" y="312" xlink:href="#p" fill="#000000" /> +<use x="528" y="312" xlink:href="#p" fill="#000000" /> +<use x="552" y="312" xlink:href="#p" fill="#000000" /> +<use x="24" y="324" xlink:href="#p" fill="#000000" /> +<use x="60" y="324" xlink:href="#p" fill="#000000" /> +<use x="72" y="324" xlink:href="#p" fill="#000000" /> +<use x="120" y="324" xlink:href="#p" fill="#000000" /> +<use x="132" y="324" xlink:href="#p" fill="#000000" /> +<use x="204" y="324" xlink:href="#p" fill="#000000" /> +<use x="216" y="324" xlink:href="#p" fill="#000000" /> +<use x="228" y="324" xlink:href="#p" fill="#000000" /> +<use x="240" y="324" xlink:href="#p" fill="#000000" /> +<use x="288" y="324" xlink:href="#p" fill="#000000" /> +<use x="336" y="324" xlink:href="#p" fill="#000000" /> +<use x="360" y="324" xlink:href="#p" fill="#000000" /> +<use x="384" y="324" xlink:href="#p" fill="#000000" /> +<use x="456" y="324" xlink:href="#p" fill="#000000" /> +<use x="492" y="324" xlink:href="#p" fill="#000000" /> +<use x="504" y="324" xlink:href="#p" fill="#000000" /> +<use x="552" y="324" xlink:href="#p" fill="#000000" /> +<use x="24" y="336" xlink:href="#p" fill="#000000" /> +<use x="48" y="336" xlink:href="#p" fill="#000000" /> +<use x="60" y="336" xlink:href="#p" fill="#000000" /> +<use x="72" y="336" xlink:href="#p" fill="#000000" /> +<use x="84" y="336" xlink:href="#p" fill="#000000" /> +<use x="96" y="336" xlink:href="#p" fill="#000000" /> +<use x="108" y="336" xlink:href="#p" fill="#000000" /> +<use x="120" y="336" xlink:href="#p" fill="#000000" /> +<use x="144" y="336" xlink:href="#p" fill="#000000" /> +<use x="156" y="336" xlink:href="#p" fill="#000000" /> +<use x="168" y="336" xlink:href="#p" fill="#000000" /> +<use x="204" y="336" xlink:href="#p" fill="#000000" /> +<use x="264" y="336" xlink:href="#p" fill="#000000" /> +<use x="276" y="336" xlink:href="#p" fill="#000000" /> +<use x="288" y="336" xlink:href="#p" fill="#000000" /> +<use x="300" y="336" xlink:href="#p" fill="#000000" /> +<use x="312" y="336" xlink:href="#p" fill="#000000" /> +<use x="324" y="336" xlink:href="#p" fill="#000000" /> +<use x="336" y="336" xlink:href="#p" fill="#000000" /> +<use x="372" y="336" xlink:href="#p" fill="#000000" /> +<use x="384" y="336" xlink:href="#p" fill="#000000" /> +<use x="408" y="336" xlink:href="#p" fill="#000000" /> +<use x="456" y="336" xlink:href="#p" fill="#000000" /> +<use x="468" y="336" xlink:href="#p" fill="#000000" /> +<use x="480" y="336" xlink:href="#p" fill="#000000" /> +<use x="492" y="336" xlink:href="#p" fill="#000000" /> +<use x="504" y="336" xlink:href="#p" fill="#000000" /> +<use x="516" y="336" xlink:href="#p" fill="#000000" /> +<use x="528" y="336" xlink:href="#p" fill="#000000" /> +<use x="540" y="336" xlink:href="#p" fill="#000000" /> +<use x="552" y="336" xlink:href="#p" fill="#000000" /> +<use x="588" y="336" xlink:href="#p" fill="#000000" /> +<use x="600" y="336" xlink:href="#p" fill="#000000" /> +<use x="36" y="348" xlink:href="#p" fill="#000000" /> +<use x="48" y="348" xlink:href="#p" fill="#000000" /> +<use x="72" y="348" xlink:href="#p" fill="#000000" /> +<use x="84" y="348" xlink:href="#p" fill="#000000" /> +<use x="108" y="348" xlink:href="#p" fill="#000000" /> +<use x="120" y="348" xlink:href="#p" fill="#000000" /> +<use x="132" y="348" xlink:href="#p" fill="#000000" /> +<use x="144" y="348" xlink:href="#p" fill="#000000" /> +<use x="156" y="348" xlink:href="#p" fill="#000000" /> +<use x="168" y="348" xlink:href="#p" fill="#000000" /> +<use x="180" y="348" xlink:href="#p" fill="#000000" /> +<use x="192" y="348" xlink:href="#p" fill="#000000" /> +<use x="240" y="348" xlink:href="#p" fill="#000000" /> +<use x="252" y="348" xlink:href="#p" fill="#000000" /> +<use x="288" y="348" xlink:href="#p" fill="#000000" /> +<use x="300" y="348" xlink:href="#p" fill="#000000" /> +<use x="324" y="348" xlink:href="#p" fill="#000000" /> +<use x="336" y="348" xlink:href="#p" fill="#000000" /> +<use x="348" y="348" xlink:href="#p" fill="#000000" /> +<use x="360" y="348" xlink:href="#p" fill="#000000" /> +<use x="384" y="348" xlink:href="#p" fill="#000000" /> +<use x="396" y="348" xlink:href="#p" fill="#000000" /> +<use x="420" y="348" xlink:href="#p" fill="#000000" /> +<use x="432" y="348" xlink:href="#p" fill="#000000" /> +<use x="456" y="348" xlink:href="#p" fill="#000000" /> +<use x="468" y="348" xlink:href="#p" fill="#000000" /> +<use x="492" y="348" xlink:href="#p" fill="#000000" /> +<use x="504" y="348" xlink:href="#p" fill="#000000" /> +<use x="516" y="348" xlink:href="#p" fill="#000000" /> +<use x="528" y="348" xlink:href="#p" fill="#000000" /> +<use x="540" y="348" xlink:href="#p" fill="#000000" /> +<use x="588" y="348" xlink:href="#p" fill="#000000" /> +<use x="600" y="348" xlink:href="#p" fill="#000000" /> +<use x="24" y="360" xlink:href="#p" fill="#000000" /> +<use x="36" y="360" xlink:href="#p" fill="#000000" /> +<use x="48" y="360" xlink:href="#p" fill="#000000" /> +<use x="60" y="360" xlink:href="#p" fill="#000000" /> +<use x="72" y="360" xlink:href="#p" fill="#000000" /> +<use x="96" y="360" xlink:href="#p" fill="#000000" /> +<use x="132" y="360" xlink:href="#p" fill="#000000" /> +<use x="144" y="360" xlink:href="#p" fill="#000000" /> +<use x="216" y="360" xlink:href="#p" fill="#000000" /> +<use x="228" y="360" xlink:href="#p" fill="#000000" /> +<use x="240" y="360" xlink:href="#p" fill="#000000" /> +<use x="252" y="360" xlink:href="#p" fill="#000000" /> +<use x="288" y="360" xlink:href="#p" fill="#000000" /> +<use x="312" y="360" xlink:href="#p" fill="#000000" /> +<use x="324" y="360" xlink:href="#p" fill="#000000" /> +<use x="348" y="360" xlink:href="#p" fill="#000000" /> +<use x="372" y="360" xlink:href="#p" fill="#000000" /> +<use x="384" y="360" xlink:href="#p" fill="#000000" /> +<use x="396" y="360" xlink:href="#p" fill="#000000" /> +<use x="408" y="360" xlink:href="#p" fill="#000000" /> +<use x="432" y="360" xlink:href="#p" fill="#000000" /> +<use x="456" y="360" xlink:href="#p" fill="#000000" /> +<use x="480" y="360" xlink:href="#p" fill="#000000" /> +<use x="504" y="360" xlink:href="#p" fill="#000000" /> +<use x="528" y="360" xlink:href="#p" fill="#000000" /> +<use x="540" y="360" xlink:href="#p" fill="#000000" /> +<use x="552" y="360" xlink:href="#p" fill="#000000" /> +<use x="564" y="360" xlink:href="#p" fill="#000000" /> +<use x="24" y="372" xlink:href="#p" fill="#000000" /> +<use x="36" y="372" xlink:href="#p" fill="#000000" /> +<use x="60" y="372" xlink:href="#p" fill="#000000" /> +<use x="108" y="372" xlink:href="#p" fill="#000000" /> +<use x="156" y="372" xlink:href="#p" fill="#000000" /> +<use x="168" y="372" xlink:href="#p" fill="#000000" /> +<use x="180" y="372" xlink:href="#p" fill="#000000" /> +<use x="192" y="372" xlink:href="#p" fill="#000000" /> +<use x="276" y="372" xlink:href="#p" fill="#000000" /> +<use x="336" y="372" xlink:href="#p" fill="#000000" /> +<use x="360" y="372" xlink:href="#p" fill="#000000" /> +<use x="384" y="372" xlink:href="#p" fill="#000000" /> +<use x="444" y="372" xlink:href="#p" fill="#000000" /> +<use x="456" y="372" xlink:href="#p" fill="#000000" /> +<use x="468" y="372" xlink:href="#p" fill="#000000" /> +<use x="492" y="372" xlink:href="#p" fill="#000000" /> +<use x="528" y="372" xlink:href="#p" fill="#000000" /> +<use x="48" y="384" xlink:href="#p" fill="#000000" /> +<use x="72" y="384" xlink:href="#p" fill="#000000" /> +<use x="96" y="384" xlink:href="#p" fill="#000000" /> +<use x="108" y="384" xlink:href="#p" fill="#000000" /> +<use x="132" y="384" xlink:href="#p" fill="#000000" /> +<use x="144" y="384" xlink:href="#p" fill="#000000" /> +<use x="192" y="384" xlink:href="#p" fill="#000000" /> +<use x="204" y="384" xlink:href="#p" fill="#000000" /> +<use x="216" y="384" xlink:href="#p" fill="#000000" /> +<use x="240" y="384" xlink:href="#p" fill="#000000" /> +<use x="288" y="384" xlink:href="#p" fill="#000000" /> +<use x="312" y="384" xlink:href="#p" fill="#000000" /> +<use x="324" y="384" xlink:href="#p" fill="#000000" /> +<use x="348" y="384" xlink:href="#p" fill="#000000" /> +<use x="360" y="384" xlink:href="#p" fill="#000000" /> +<use x="372" y="384" xlink:href="#p" fill="#000000" /> +<use x="396" y="384" xlink:href="#p" fill="#000000" /> +<use x="408" y="384" xlink:href="#p" fill="#000000" /> +<use x="420" y="384" xlink:href="#p" fill="#000000" /> +<use x="444" y="384" xlink:href="#p" fill="#000000" /> +<use x="456" y="384" xlink:href="#p" fill="#000000" /> +<use x="468" y="384" xlink:href="#p" fill="#000000" /> +<use x="492" y="384" xlink:href="#p" fill="#000000" /> +<use x="516" y="384" xlink:href="#p" fill="#000000" /> +<use x="528" y="384" xlink:href="#p" fill="#000000" /> +<use x="540" y="384" xlink:href="#p" fill="#000000" /> +<use x="552" y="384" xlink:href="#p" fill="#000000" /> +<use x="588" y="384" xlink:href="#p" fill="#000000" /> +<use x="600" y="384" xlink:href="#p" fill="#000000" /> +<use x="60" y="396" xlink:href="#p" fill="#000000" /> +<use x="72" y="396" xlink:href="#p" fill="#000000" /> +<use x="84" y="396" xlink:href="#p" fill="#000000" /> +<use x="120" y="396" xlink:href="#p" fill="#000000" /> +<use x="144" y="396" xlink:href="#p" fill="#000000" /> +<use x="180" y="396" xlink:href="#p" fill="#000000" /> +<use x="192" y="396" xlink:href="#p" fill="#000000" /> +<use x="204" y="396" xlink:href="#p" fill="#000000" /> +<use x="216" y="396" xlink:href="#p" fill="#000000" /> +<use x="228" y="396" xlink:href="#p" fill="#000000" /> +<use x="252" y="396" xlink:href="#p" fill="#000000" /> +<use x="264" y="396" xlink:href="#p" fill="#000000" /> +<use x="276" y="396" xlink:href="#p" fill="#000000" /> +<use x="312" y="396" xlink:href="#p" fill="#000000" /> +<use x="360" y="396" xlink:href="#p" fill="#000000" /> +<use x="384" y="396" xlink:href="#p" fill="#000000" /> +<use x="396" y="396" xlink:href="#p" fill="#000000" /> +<use x="420" y="396" xlink:href="#p" fill="#000000" /> +<use x="432" y="396" xlink:href="#p" fill="#000000" /> +<use x="444" y="396" xlink:href="#p" fill="#000000" /> +<use x="468" y="396" xlink:href="#p" fill="#000000" /> +<use x="480" y="396" xlink:href="#p" fill="#000000" /> +<use x="528" y="396" xlink:href="#p" fill="#000000" /> +<use x="588" y="396" xlink:href="#p" fill="#000000" /> +<use x="600" y="396" xlink:href="#p" fill="#000000" /> +<use x="48" y="408" xlink:href="#p" fill="#000000" /> +<use x="84" y="408" xlink:href="#p" fill="#000000" /> +<use x="96" y="408" xlink:href="#p" fill="#000000" /> +<use x="144" y="408" xlink:href="#p" fill="#000000" /> +<use x="192" y="408" xlink:href="#p" fill="#000000" /> +<use x="204" y="408" xlink:href="#p" fill="#000000" /> +<use x="240" y="408" xlink:href="#p" fill="#000000" /> +<use x="252" y="408" xlink:href="#p" fill="#000000" /> +<use x="264" y="408" xlink:href="#p" fill="#000000" /> +<use x="288" y="408" xlink:href="#p" fill="#000000" /> +<use x="348" y="408" xlink:href="#p" fill="#000000" /> +<use x="384" y="408" xlink:href="#p" fill="#000000" /> +<use x="396" y="408" xlink:href="#p" fill="#000000" /> +<use x="420" y="408" xlink:href="#p" fill="#000000" /> +<use x="432" y="408" xlink:href="#p" fill="#000000" /> +<use x="444" y="408" xlink:href="#p" fill="#000000" /> +<use x="528" y="408" xlink:href="#p" fill="#000000" /> +<use x="552" y="408" xlink:href="#p" fill="#000000" /> +<use x="576" y="408" xlink:href="#p" fill="#000000" /> +<use x="600" y="408" xlink:href="#p" fill="#000000" /> +<use x="48" y="420" xlink:href="#p" fill="#000000" /> +<use x="84" y="420" xlink:href="#p" fill="#000000" /> +<use x="144" y="420" xlink:href="#p" fill="#000000" /> +<use x="204" y="420" xlink:href="#p" fill="#000000" /> +<use x="228" y="420" xlink:href="#p" fill="#000000" /> +<use x="264" y="420" xlink:href="#p" fill="#000000" /> +<use x="288" y="420" xlink:href="#p" fill="#000000" /> +<use x="300" y="420" xlink:href="#p" fill="#000000" /> +<use x="372" y="420" xlink:href="#p" fill="#000000" /> +<use x="396" y="420" xlink:href="#p" fill="#000000" /> +<use x="408" y="420" xlink:href="#p" fill="#000000" /> +<use x="444" y="420" xlink:href="#p" fill="#000000" /> +<use x="456" y="420" xlink:href="#p" fill="#000000" /> +<use x="504" y="420" xlink:href="#p" fill="#000000" /> +<use x="516" y="420" xlink:href="#p" fill="#000000" /> +<use x="564" y="420" xlink:href="#p" fill="#000000" /> +<use x="600" y="420" xlink:href="#p" fill="#000000" /> +<use x="60" y="432" xlink:href="#p" fill="#000000" /> +<use x="96" y="432" xlink:href="#p" fill="#000000" /> +<use x="144" y="432" xlink:href="#p" fill="#000000" /> +<use x="168" y="432" xlink:href="#p" fill="#000000" /> +<use x="216" y="432" xlink:href="#p" fill="#000000" /> +<use x="228" y="432" xlink:href="#p" fill="#000000" /> +<use x="240" y="432" xlink:href="#p" fill="#000000" /> +<use x="276" y="432" xlink:href="#p" fill="#000000" /> +<use x="300" y="432" xlink:href="#p" fill="#000000" /> +<use x="348" y="432" xlink:href="#p" fill="#000000" /> +<use x="384" y="432" xlink:href="#p" fill="#000000" /> +<use x="396" y="432" xlink:href="#p" fill="#000000" /> +<use x="420" y="432" xlink:href="#p" fill="#000000" /> +<use x="444" y="432" xlink:href="#p" fill="#000000" /> +<use x="468" y="432" xlink:href="#p" fill="#000000" /> +<use x="480" y="432" xlink:href="#p" fill="#000000" /> +<use x="516" y="432" xlink:href="#p" fill="#000000" /> +<use x="528" y="432" xlink:href="#p" fill="#000000" /> +<use x="540" y="432" xlink:href="#p" fill="#000000" /> +<use x="564" y="432" xlink:href="#p" fill="#000000" /> +<use x="576" y="432" xlink:href="#p" fill="#000000" /> +<use x="600" y="432" xlink:href="#p" fill="#000000" /> +<use x="48" y="444" xlink:href="#p" fill="#000000" /> +<use x="60" y="444" xlink:href="#p" fill="#000000" /> +<use x="72" y="444" xlink:href="#p" fill="#000000" /> +<use x="84" y="444" xlink:href="#p" fill="#000000" /> +<use x="108" y="444" xlink:href="#p" fill="#000000" /> +<use x="120" y="444" xlink:href="#p" fill="#000000" /> +<use x="132" y="444" xlink:href="#p" fill="#000000" /> +<use x="144" y="444" xlink:href="#p" fill="#000000" /> +<use x="156" y="444" xlink:href="#p" fill="#000000" /> +<use x="168" y="444" xlink:href="#p" fill="#000000" /> +<use x="180" y="444" xlink:href="#p" fill="#000000" /> +<use x="192" y="444" xlink:href="#p" fill="#000000" /> +<use x="204" y="444" xlink:href="#p" fill="#000000" /> +<use x="240" y="444" xlink:href="#p" fill="#000000" /> +<use x="252" y="444" xlink:href="#p" fill="#000000" /> +<use x="264" y="444" xlink:href="#p" fill="#000000" /> +<use x="276" y="444" xlink:href="#p" fill="#000000" /> +<use x="288" y="444" xlink:href="#p" fill="#000000" /> +<use x="312" y="444" xlink:href="#p" fill="#000000" /> +<use x="348" y="444" xlink:href="#p" fill="#000000" /> +<use x="360" y="444" xlink:href="#p" fill="#000000" /> +<use x="384" y="444" xlink:href="#p" fill="#000000" /> +<use x="396" y="444" xlink:href="#p" fill="#000000" /> +<use x="408" y="444" xlink:href="#p" fill="#000000" /> +<use x="420" y="444" xlink:href="#p" fill="#000000" /> +<use x="432" y="444" xlink:href="#p" fill="#000000" /> +<use x="480" y="444" xlink:href="#p" fill="#000000" /> +<use x="492" y="444" xlink:href="#p" fill="#000000" /> +<use x="504" y="444" xlink:href="#p" fill="#000000" /> +<use x="528" y="444" xlink:href="#p" fill="#000000" /> +<use x="552" y="444" xlink:href="#p" fill="#000000" /> +<use x="588" y="444" xlink:href="#p" fill="#000000" /> +<use x="600" y="444" xlink:href="#p" fill="#000000" /> +<use x="36" y="456" xlink:href="#p" fill="#000000" /> +<use x="60" y="456" xlink:href="#p" fill="#000000" /> +<use x="72" y="456" xlink:href="#p" fill="#000000" /> +<use x="96" y="456" xlink:href="#p" fill="#000000" /> +<use x="120" y="456" xlink:href="#p" fill="#000000" /> +<use x="132" y="456" xlink:href="#p" fill="#000000" /> +<use x="168" y="456" xlink:href="#p" fill="#000000" /> +<use x="204" y="456" xlink:href="#p" fill="#000000" /> +<use x="228" y="456" xlink:href="#p" fill="#000000" /> +<use x="240" y="456" xlink:href="#p" fill="#000000" /> +<use x="252" y="456" xlink:href="#p" fill="#000000" /> +<use x="276" y="456" xlink:href="#p" fill="#000000" /> +<use x="300" y="456" xlink:href="#p" fill="#000000" /> +<use x="312" y="456" xlink:href="#p" fill="#000000" /> +<use x="324" y="456" xlink:href="#p" fill="#000000" /> +<use x="336" y="456" xlink:href="#p" fill="#000000" /> +<use x="348" y="456" xlink:href="#p" fill="#000000" /> +<use x="360" y="456" xlink:href="#p" fill="#000000" /> +<use x="372" y="456" xlink:href="#p" fill="#000000" /> +<use x="384" y="456" xlink:href="#p" fill="#000000" /> +<use x="396" y="456" xlink:href="#p" fill="#000000" /> +<use x="408" y="456" xlink:href="#p" fill="#000000" /> +<use x="432" y="456" xlink:href="#p" fill="#000000" /> +<use x="444" y="456" xlink:href="#p" fill="#000000" /> +<use x="456" y="456" xlink:href="#p" fill="#000000" /> +<use x="468" y="456" xlink:href="#p" fill="#000000" /> +<use x="480" y="456" xlink:href="#p" fill="#000000" /> +<use x="564" y="456" xlink:href="#p" fill="#000000" /> +<use x="576" y="456" xlink:href="#p" fill="#000000" /> +<use x="588" y="456" xlink:href="#p" fill="#000000" /> +<use x="600" y="456" xlink:href="#p" fill="#000000" /> +<use x="36" y="468" xlink:href="#p" fill="#000000" /> +<use x="48" y="468" xlink:href="#p" fill="#000000" /> +<use x="60" y="468" xlink:href="#p" fill="#000000" /> +<use x="72" y="468" xlink:href="#p" fill="#000000" /> +<use x="84" y="468" xlink:href="#p" fill="#000000" /> +<use x="108" y="468" xlink:href="#p" fill="#000000" /> +<use x="132" y="468" xlink:href="#p" fill="#000000" /> +<use x="156" y="468" xlink:href="#p" fill="#000000" /> +<use x="204" y="468" xlink:href="#p" fill="#000000" /> +<use x="228" y="468" xlink:href="#p" fill="#000000" /> +<use x="240" y="468" xlink:href="#p" fill="#000000" /> +<use x="336" y="468" xlink:href="#p" fill="#000000" /> +<use x="372" y="468" xlink:href="#p" fill="#000000" /> +<use x="396" y="468" xlink:href="#p" fill="#000000" /> +<use x="408" y="468" xlink:href="#p" fill="#000000" /> +<use x="420" y="468" xlink:href="#p" fill="#000000" /> +<use x="444" y="468" xlink:href="#p" fill="#000000" /> +<use x="468" y="468" xlink:href="#p" fill="#000000" /> +<use x="492" y="468" xlink:href="#p" fill="#000000" /> +<use x="516" y="468" xlink:href="#p" fill="#000000" /> +<use x="528" y="468" xlink:href="#p" fill="#000000" /> +<use x="564" y="468" xlink:href="#p" fill="#000000" /> +<use x="36" y="480" xlink:href="#p" fill="#000000" /> +<use x="84" y="480" xlink:href="#p" fill="#000000" /> +<use x="96" y="480" xlink:href="#p" fill="#000000" /> +<use x="156" y="480" xlink:href="#p" fill="#000000" /> +<use x="204" y="480" xlink:href="#p" fill="#000000" /> +<use x="228" y="480" xlink:href="#p" fill="#000000" /> +<use x="252" y="480" xlink:href="#p" fill="#000000" /> +<use x="264" y="480" xlink:href="#p" fill="#000000" /> +<use x="276" y="480" xlink:href="#p" fill="#000000" /> +<use x="288" y="480" xlink:href="#p" fill="#000000" /> +<use x="324" y="480" xlink:href="#p" fill="#000000" /> +<use x="336" y="480" xlink:href="#p" fill="#000000" /> +<use x="372" y="480" xlink:href="#p" fill="#000000" /> +<use x="396" y="480" xlink:href="#p" fill="#000000" /> +<use x="408" y="480" xlink:href="#p" fill="#000000" /> +<use x="420" y="480" xlink:href="#p" fill="#000000" /> +<use x="468" y="480" xlink:href="#p" fill="#000000" /> +<use x="504" y="480" xlink:href="#p" fill="#000000" /> +<use x="516" y="480" xlink:href="#p" fill="#000000" /> +<use x="528" y="480" xlink:href="#p" fill="#000000" /> +<use x="540" y="480" xlink:href="#p" fill="#000000" /> +<use x="552" y="480" xlink:href="#p" fill="#000000" /> +<use x="576" y="480" xlink:href="#p" fill="#000000" /> +<use x="588" y="480" xlink:href="#p" fill="#000000" /> +<use x="600" y="480" xlink:href="#p" fill="#000000" /> +<use x="36" y="492" xlink:href="#p" fill="#000000" /> +<use x="48" y="492" xlink:href="#p" fill="#000000" /> +<use x="60" y="492" xlink:href="#p" fill="#000000" /> +<use x="108" y="492" xlink:href="#p" fill="#000000" /> +<use x="120" y="492" xlink:href="#p" fill="#000000" /> +<use x="132" y="492" xlink:href="#p" fill="#000000" /> +<use x="144" y="492" xlink:href="#p" fill="#000000" /> +<use x="156" y="492" xlink:href="#p" fill="#000000" /> +<use x="168" y="492" xlink:href="#p" fill="#000000" /> +<use x="180" y="492" xlink:href="#p" fill="#000000" /> +<use x="192" y="492" xlink:href="#p" fill="#000000" /> +<use x="216" y="492" xlink:href="#p" fill="#000000" /> +<use x="228" y="492" xlink:href="#p" fill="#000000" /> +<use x="240" y="492" xlink:href="#p" fill="#000000" /> +<use x="264" y="492" xlink:href="#p" fill="#000000" /> +<use x="276" y="492" xlink:href="#p" fill="#000000" /> +<use x="288" y="492" xlink:href="#p" fill="#000000" /> +<use x="300" y="492" xlink:href="#p" fill="#000000" /> +<use x="312" y="492" xlink:href="#p" fill="#000000" /> +<use x="336" y="492" xlink:href="#p" fill="#000000" /> +<use x="360" y="492" xlink:href="#p" fill="#000000" /> +<use x="372" y="492" xlink:href="#p" fill="#000000" /> +<use x="384" y="492" xlink:href="#p" fill="#000000" /> +<use x="408" y="492" xlink:href="#p" fill="#000000" /> +<use x="420" y="492" xlink:href="#p" fill="#000000" /> +<use x="456" y="492" xlink:href="#p" fill="#000000" /> +<use x="468" y="492" xlink:href="#p" fill="#000000" /> +<use x="552" y="492" xlink:href="#p" fill="#000000" /> +<use x="24" y="504" xlink:href="#p" fill="#000000" /> +<use x="36" y="504" xlink:href="#p" fill="#000000" /> +<use x="48" y="504" xlink:href="#p" fill="#000000" /> +<use x="96" y="504" xlink:href="#p" fill="#000000" /> +<use x="108" y="504" xlink:href="#p" fill="#000000" /> +<use x="120" y="504" xlink:href="#p" fill="#000000" /> +<use x="144" y="504" xlink:href="#p" fill="#000000" /> +<use x="156" y="504" xlink:href="#p" fill="#000000" /> +<use x="168" y="504" xlink:href="#p" fill="#000000" /> +<use x="180" y="504" xlink:href="#p" fill="#000000" /> +<use x="204" y="504" xlink:href="#p" fill="#000000" /> +<use x="216" y="504" xlink:href="#p" fill="#000000" /> +<use x="240" y="504" xlink:href="#p" fill="#000000" /> +<use x="252" y="504" xlink:href="#p" fill="#000000" /> +<use x="276" y="504" xlink:href="#p" fill="#000000" /> +<use x="288" y="504" xlink:href="#p" fill="#000000" /> +<use x="300" y="504" xlink:href="#p" fill="#000000" /> +<use x="312" y="504" xlink:href="#p" fill="#000000" /> +<use x="324" y="504" xlink:href="#p" fill="#000000" /> +<use x="336" y="504" xlink:href="#p" fill="#000000" /> +<use x="372" y="504" xlink:href="#p" fill="#000000" /> +<use x="408" y="504" xlink:href="#p" fill="#000000" /> +<use x="420" y="504" xlink:href="#p" fill="#000000" /> +<use x="432" y="504" xlink:href="#p" fill="#000000" /> +<use x="456" y="504" xlink:href="#p" fill="#000000" /> +<use x="468" y="504" xlink:href="#p" fill="#000000" /> +<use x="480" y="504" xlink:href="#p" fill="#000000" /> +<use x="504" y="504" xlink:href="#p" fill="#000000" /> +<use x="516" y="504" xlink:href="#p" fill="#000000" /> +<use x="528" y="504" xlink:href="#p" fill="#000000" /> +<use x="540" y="504" xlink:href="#p" fill="#000000" /> +<use x="552" y="504" xlink:href="#p" fill="#000000" /> +<use x="564" y="504" xlink:href="#p" fill="#000000" /> +<use x="576" y="504" xlink:href="#p" fill="#000000" /> +<use x="120" y="516" xlink:href="#p" fill="#000000" /> +<use x="132" y="516" xlink:href="#p" fill="#000000" /> +<use x="144" y="516" xlink:href="#p" fill="#000000" /> +<use x="156" y="516" xlink:href="#p" fill="#000000" /> +<use x="180" y="516" xlink:href="#p" fill="#000000" /> +<use x="204" y="516" xlink:href="#p" fill="#000000" /> +<use x="216" y="516" xlink:href="#p" fill="#000000" /> +<use x="228" y="516" xlink:href="#p" fill="#000000" /> +<use x="240" y="516" xlink:href="#p" fill="#000000" /> +<use x="288" y="516" xlink:href="#p" fill="#000000" /> +<use x="336" y="516" xlink:href="#p" fill="#000000" /> +<use x="360" y="516" xlink:href="#p" fill="#000000" /> +<use x="396" y="516" xlink:href="#p" fill="#000000" /> +<use x="408" y="516" xlink:href="#p" fill="#000000" /> +<use x="420" y="516" xlink:href="#p" fill="#000000" /> +<use x="444" y="516" xlink:href="#p" fill="#000000" /> +<use x="468" y="516" xlink:href="#p" fill="#000000" /> +<use x="504" y="516" xlink:href="#p" fill="#000000" /> +<use x="552" y="516" xlink:href="#p" fill="#000000" /> +<use x="564" y="516" xlink:href="#p" fill="#000000" /> +<use x="576" y="516" xlink:href="#p" fill="#000000" /> +<use x="600" y="516" xlink:href="#p" fill="#000000" /> +<use x="24" y="528" xlink:href="#p" fill="#000000" /> +<use x="36" y="528" xlink:href="#p" fill="#000000" /> +<use x="48" y="528" xlink:href="#p" fill="#000000" /> +<use x="60" y="528" xlink:href="#p" fill="#000000" /> +<use x="72" y="528" xlink:href="#p" fill="#000000" /> +<use x="84" y="528" xlink:href="#p" fill="#000000" /> +<use x="96" y="528" xlink:href="#p" fill="#000000" /> +<use x="132" y="528" xlink:href="#p" fill="#000000" /> +<use x="168" y="528" xlink:href="#p" fill="#000000" /> +<use x="180" y="528" xlink:href="#p" fill="#000000" /> +<use x="192" y="528" xlink:href="#p" fill="#000000" /> +<use x="204" y="528" xlink:href="#p" fill="#000000" /> +<use x="264" y="528" xlink:href="#p" fill="#000000" /> +<use x="276" y="528" xlink:href="#p" fill="#000000" /> +<use x="288" y="528" xlink:href="#p" fill="#000000" /> +<use x="312" y="528" xlink:href="#p" fill="#000000" /> +<use x="336" y="528" xlink:href="#p" fill="#000000" /> +<use x="372" y="528" xlink:href="#p" fill="#000000" /> +<use x="384" y="528" xlink:href="#p" fill="#000000" /> +<use x="420" y="528" xlink:href="#p" fill="#000000" /> +<use x="432" y="528" xlink:href="#p" fill="#000000" /> +<use x="444" y="528" xlink:href="#p" fill="#000000" /> +<use x="456" y="528" xlink:href="#p" fill="#000000" /> +<use x="468" y="528" xlink:href="#p" fill="#000000" /> +<use x="480" y="528" xlink:href="#p" fill="#000000" /> +<use x="492" y="528" xlink:href="#p" fill="#000000" /> +<use x="504" y="528" xlink:href="#p" fill="#000000" /> +<use x="528" y="528" xlink:href="#p" fill="#000000" /> +<use x="552" y="528" xlink:href="#p" fill="#000000" /> +<use x="576" y="528" xlink:href="#p" fill="#000000" /> +<use x="600" y="528" xlink:href="#p" fill="#000000" /> +<use x="24" y="540" xlink:href="#p" fill="#000000" /> +<use x="96" y="540" xlink:href="#p" fill="#000000" /> +<use x="120" y="540" xlink:href="#p" fill="#000000" /> +<use x="144" y="540" xlink:href="#p" fill="#000000" /> +<use x="192" y="540" xlink:href="#p" fill="#000000" /> +<use x="252" y="540" xlink:href="#p" fill="#000000" /> +<use x="276" y="540" xlink:href="#p" fill="#000000" /> +<use x="288" y="540" xlink:href="#p" fill="#000000" /> +<use x="336" y="540" xlink:href="#p" fill="#000000" /> +<use x="384" y="540" xlink:href="#p" fill="#000000" /> +<use x="408" y="540" xlink:href="#p" fill="#000000" /> +<use x="432" y="540" xlink:href="#p" fill="#000000" /> +<use x="444" y="540" xlink:href="#p" fill="#000000" /> +<use x="456" y="540" xlink:href="#p" fill="#000000" /> +<use x="468" y="540" xlink:href="#p" fill="#000000" /> +<use x="504" y="540" xlink:href="#p" fill="#000000" /> +<use x="552" y="540" xlink:href="#p" fill="#000000" /> +<use x="24" y="552" xlink:href="#p" fill="#000000" /> +<use x="48" y="552" xlink:href="#p" fill="#000000" /> +<use x="60" y="552" xlink:href="#p" fill="#000000" /> +<use x="72" y="552" xlink:href="#p" fill="#000000" /> +<use x="96" y="552" xlink:href="#p" fill="#000000" /> +<use x="120" y="552" xlink:href="#p" fill="#000000" /> +<use x="144" y="552" xlink:href="#p" fill="#000000" /> +<use x="168" y="552" xlink:href="#p" fill="#000000" /> +<use x="180" y="552" xlink:href="#p" fill="#000000" /> +<use x="192" y="552" xlink:href="#p" fill="#000000" /> +<use x="216" y="552" xlink:href="#p" fill="#000000" /> +<use x="228" y="552" xlink:href="#p" fill="#000000" /> +<use x="252" y="552" xlink:href="#p" fill="#000000" /> +<use x="276" y="552" xlink:href="#p" fill="#000000" /> +<use x="288" y="552" xlink:href="#p" fill="#000000" /> +<use x="300" y="552" xlink:href="#p" fill="#000000" /> +<use x="312" y="552" xlink:href="#p" fill="#000000" /> +<use x="324" y="552" xlink:href="#p" fill="#000000" /> +<use x="336" y="552" xlink:href="#p" fill="#000000" /> +<use x="372" y="552" xlink:href="#p" fill="#000000" /> +<use x="384" y="552" xlink:href="#p" fill="#000000" /> +<use x="396" y="552" xlink:href="#p" fill="#000000" /> +<use x="408" y="552" xlink:href="#p" fill="#000000" /> +<use x="420" y="552" xlink:href="#p" fill="#000000" /> +<use x="444" y="552" xlink:href="#p" fill="#000000" /> +<use x="456" y="552" xlink:href="#p" fill="#000000" /> +<use x="492" y="552" xlink:href="#p" fill="#000000" /> +<use x="504" y="552" xlink:href="#p" fill="#000000" /> +<use x="516" y="552" xlink:href="#p" fill="#000000" /> +<use x="528" y="552" xlink:href="#p" fill="#000000" /> +<use x="540" y="552" xlink:href="#p" fill="#000000" /> +<use x="552" y="552" xlink:href="#p" fill="#000000" /> +<use x="564" y="552" xlink:href="#p" fill="#000000" /> +<use x="24" y="564" xlink:href="#p" fill="#000000" /> +<use x="48" y="564" xlink:href="#p" fill="#000000" /> +<use x="60" y="564" xlink:href="#p" fill="#000000" /> +<use x="72" y="564" xlink:href="#p" fill="#000000" /> +<use x="96" y="564" xlink:href="#p" fill="#000000" /> +<use x="132" y="564" xlink:href="#p" fill="#000000" /> +<use x="144" y="564" xlink:href="#p" fill="#000000" /> +<use x="228" y="564" xlink:href="#p" fill="#000000" /> +<use x="240" y="564" xlink:href="#p" fill="#000000" /> +<use x="252" y="564" xlink:href="#p" fill="#000000" /> +<use x="276" y="564" xlink:href="#p" fill="#000000" /> +<use x="300" y="564" xlink:href="#p" fill="#000000" /> +<use x="336" y="564" xlink:href="#p" fill="#000000" /> +<use x="348" y="564" xlink:href="#p" fill="#000000" /> +<use x="372" y="564" xlink:href="#p" fill="#000000" /> +<use x="384" y="564" xlink:href="#p" fill="#000000" /> +<use x="396" y="564" xlink:href="#p" fill="#000000" /> +<use x="444" y="564" xlink:href="#p" fill="#000000" /> +<use x="492" y="564" xlink:href="#p" fill="#000000" /> +<use x="528" y="564" xlink:href="#p" fill="#000000" /> +<use x="540" y="564" xlink:href="#p" fill="#000000" /> +<use x="552" y="564" xlink:href="#p" fill="#000000" /> +<use x="564" y="564" xlink:href="#p" fill="#000000" /> +<use x="576" y="564" xlink:href="#p" fill="#000000" /> +<use x="588" y="564" xlink:href="#p" fill="#000000" /> +<use x="24" y="576" xlink:href="#p" fill="#000000" /> +<use x="48" y="576" xlink:href="#p" fill="#000000" /> +<use x="60" y="576" xlink:href="#p" fill="#000000" /> +<use x="72" y="576" xlink:href="#p" fill="#000000" /> +<use x="96" y="576" xlink:href="#p" fill="#000000" /> +<use x="120" y="576" xlink:href="#p" fill="#000000" /> +<use x="180" y="576" xlink:href="#p" fill="#000000" /> +<use x="192" y="576" xlink:href="#p" fill="#000000" /> +<use x="216" y="576" xlink:href="#p" fill="#000000" /> +<use x="300" y="576" xlink:href="#p" fill="#000000" /> +<use x="312" y="576" xlink:href="#p" fill="#000000" /> +<use x="348" y="576" xlink:href="#p" fill="#000000" /> +<use x="360" y="576" xlink:href="#p" fill="#000000" /> +<use x="444" y="576" xlink:href="#p" fill="#000000" /> +<use x="456" y="576" xlink:href="#p" fill="#000000" /> +<use x="492" y="576" xlink:href="#p" fill="#000000" /> +<use x="516" y="576" xlink:href="#p" fill="#000000" /> +<use x="528" y="576" xlink:href="#p" fill="#000000" /> +<use x="540" y="576" xlink:href="#p" fill="#000000" /> +<use x="552" y="576" xlink:href="#p" fill="#000000" /> +<use x="564" y="576" xlink:href="#p" fill="#000000" /> +<use x="576" y="576" xlink:href="#p" fill="#000000" /> +<use x="588" y="576" xlink:href="#p" fill="#000000" /> +<use x="600" y="576" xlink:href="#p" fill="#000000" /> +<use x="24" y="588" xlink:href="#p" fill="#000000" /> +<use x="96" y="588" xlink:href="#p" fill="#000000" /> +<use x="156" y="588" xlink:href="#p" fill="#000000" /> +<use x="180" y="588" xlink:href="#p" fill="#000000" /> +<use x="228" y="588" xlink:href="#p" fill="#000000" /> +<use x="240" y="588" xlink:href="#p" fill="#000000" /> +<use x="252" y="588" xlink:href="#p" fill="#000000" /> +<use x="348" y="588" xlink:href="#p" fill="#000000" /> +<use x="384" y="588" xlink:href="#p" fill="#000000" /> +<use x="420" y="588" xlink:href="#p" fill="#000000" /> +<use x="432" y="588" xlink:href="#p" fill="#000000" /> +<use x="444" y="588" xlink:href="#p" fill="#000000" /> +<use x="468" y="588" xlink:href="#p" fill="#000000" /> +<use x="480" y="588" xlink:href="#p" fill="#000000" /> +<use x="492" y="588" xlink:href="#p" fill="#000000" /> +<use x="504" y="588" xlink:href="#p" fill="#000000" /> +<use x="516" y="588" xlink:href="#p" fill="#000000" /> +<use x="540" y="588" xlink:href="#p" fill="#000000" /> +<use x="552" y="588" xlink:href="#p" fill="#000000" /> +<use x="588" y="588" xlink:href="#p" fill="#000000" /> +<use x="24" y="600" xlink:href="#p" fill="#000000" /> +<use x="36" y="600" xlink:href="#p" fill="#000000" /> +<use x="48" y="600" xlink:href="#p" fill="#000000" /> +<use x="60" y="600" xlink:href="#p" fill="#000000" /> +<use x="72" y="600" xlink:href="#p" fill="#000000" /> +<use x="84" y="600" xlink:href="#p" fill="#000000" /> +<use x="96" y="600" xlink:href="#p" fill="#000000" /> +<use x="168" y="600" xlink:href="#p" fill="#000000" /> +<use x="180" y="600" xlink:href="#p" fill="#000000" /> +<use x="192" y="600" xlink:href="#p" fill="#000000" /> +<use x="204" y="600" xlink:href="#p" fill="#000000" /> +<use x="240" y="600" xlink:href="#p" fill="#000000" /> +<use x="264" y="600" xlink:href="#p" fill="#000000" /> +<use x="276" y="600" xlink:href="#p" fill="#000000" /> +<use x="288" y="600" xlink:href="#p" fill="#000000" /> +<use x="336" y="600" xlink:href="#p" fill="#000000" /> +<use x="348" y="600" xlink:href="#p" fill="#000000" /> +<use x="384" y="600" xlink:href="#p" fill="#000000" /> +<use x="396" y="600" xlink:href="#p" fill="#000000" /> +<use x="408" y="600" xlink:href="#p" fill="#000000" /> +<use x="420" y="600" xlink:href="#p" fill="#000000" /> +<use x="432" y="600" xlink:href="#p" fill="#000000" /> +<use x="444" y="600" xlink:href="#p" fill="#000000" /> +<use x="456" y="600" xlink:href="#p" fill="#000000" /> +<use x="492" y="600" xlink:href="#p" fill="#000000" /> +<use x="516" y="600" xlink:href="#p" fill="#000000" /> +<use x="576" y="600" xlink:href="#p" fill="#000000" /> +<use x="588" y="600" xlink:href="#p" fill="#000000" /> +<use x="600" y="600" xlink:href="#p" fill="#000000" /> +</g> +</svg> \ No newline at end of file diff --git a/public/test b/public/test new file mode 100644 index 0000000..f198841 Binary files /dev/null and b/public/test differ diff --git a/scripts/clean-unused-images.js b/scripts/clean-unused-images.js new file mode 100644 index 0000000..96a80a8 --- /dev/null +++ b/scripts/clean-unused-images.js @@ -0,0 +1,246 @@ +#!/usr/bin/env node + +import fs from 'fs'; +import path from 'path'; +import { glob } from 'glob'; +import { fileURLToPath } from 'url'; + +const __filename = fileURLToPath(import.meta.url); +const __dirname = path.dirname(__filename); + +/** + * 清理未使用的图片资源脚本 + * 扫描 src/content/posts 下的所有 markdown 文件, + * 查找 src/content/assets 中未被引用的图片并删除 + */ + +const CONTENT_DIR = path.join(process.cwd(), 'src/content'); +const POSTS_DIR = path.join(CONTENT_DIR, 'posts'); +const ASSETS_DIR = path.join(CONTENT_DIR, 'assets'); + +// 支持的图片格式 +const IMAGE_EXTENSIONS = ['.jpg', '.jpeg', '.png', '.gif', '.webp', '.svg', '.avif']; + +/** + * 获取所有 markdown 文件 + */ +async function getAllMarkdownFiles() { + try { + const pattern = path.join(POSTS_DIR, '**/*.md').replace(/\\/g, '/'); + return await glob(pattern); + } catch (error) { + console.error('获取 markdown 文件失败:', error.message); + return []; + } +} + +/** + * 获取所有图片文件 + */ +async function getAllImageFiles() { + try { + const extensions = IMAGE_EXTENSIONS.join(','); + const pattern = path.join(ASSETS_DIR, `**/*{${extensions}}`).replace(/\\/g, '/'); + return await glob(pattern); + } catch (error) { + console.error('获取图片文件失败:', error.message); + return []; + } +} + +/** + * 从 markdown 内容中提取图片引用 + */ +function extractImageReferences(content) { + const references = new Set(); + + // 匹配 YAML frontmatter 中的 image 字段(支持带引号和不带引号的值) + const yamlImageRegex = /^---[\s\S]*?image:\s*(?:['"]([^'"]+)['"]|([^\s\n]+))[\s\S]*?^---/m; + let match = yamlImageRegex.exec(content); + if (match) { + // match[1] 是带引号的值,match[2] 是不带引号的值 + references.add(match[1] || match[2]); + } + + // 匹配 markdown 图片语法: ![alt](path) + const markdownImageRegex = /!\[.*?\]\(([^)]+)\)/g; + while ((match = markdownImageRegex.exec(content)) !== null) { + references.add(match[1]); + } + + // 匹配 HTML img 标签: <img src="path"> + const htmlImageRegex = /<img[^>]+src=["']([^"']+)["'][^>]*>/gi; + while ((match = htmlImageRegex.exec(content)) !== null) { + references.add(match[1]); + } + + // 匹配 Astro Image 组件引用 + const astroImageRegex = /import\s+.*?\s+from\s+["']([^"']+\.(jpg|jpeg|png|gif|webp|svg|avif))["']/gi; + while ((match = astroImageRegex.exec(content)) !== null) { + references.add(match[1]); + } + + return Array.from(references); +} + +/** + * 规范化路径,处理相对路径和绝对路径 + */ +function normalizePath(imagePath, markdownFilePath) { + // 跳过外部 URL + if (imagePath.startsWith('http://') || imagePath.startsWith('https://')) { + return null; + } + + // 跳过以 / 开头的绝对路径(通常指向 public 目录) + if (imagePath.startsWith('/')) { + return null; + } + + // 处理相对路径 + if (imagePath.startsWith('./') || imagePath.startsWith('../')) { + const markdownDir = path.dirname(markdownFilePath); + return path.resolve(markdownDir, imagePath); + } + + // 处理直接的文件名或相对路径 + const markdownDir = path.dirname(markdownFilePath); + return path.resolve(markdownDir, imagePath); +} + +/** + * 主函数 + */ +async function cleanUnusedImages() { + console.log('🔍 开始扫描未使用的图片资源...'); + + // 检查目录是否存在 + if (!fs.existsSync(POSTS_DIR)) { + console.error(`❌ Posts 目录不存在: ${POSTS_DIR}`); + return; + } + + if (!fs.existsSync(ASSETS_DIR)) { + console.log(`ℹ️ Assets 目录不存在: ${ASSETS_DIR}`); + return; + } + + // 获取所有文件 + const markdownFiles = await getAllMarkdownFiles(); + const imageFiles = await getAllImageFiles(); + + console.log(`📄 找到 ${markdownFiles.length} 个 markdown 文件`); + console.log(`🖼️ 找到 ${imageFiles.length} 个图片文件`); + + if (imageFiles.length === 0) { + console.log('✅ 没有找到图片文件,无需清理'); + return; + } + + // 收集所有被引用的图片 + const referencedImages = new Set(); + + for (const mdFile of markdownFiles) { + try { + const content = fs.readFileSync(mdFile, 'utf-8'); + const references = extractImageReferences(content); + + for (const ref of references) { + const normalizedPath = normalizePath(ref, mdFile); + if (normalizedPath) { + const resolvedPath = path.resolve(normalizedPath); + referencedImages.add(resolvedPath); + } + } + } catch (error) { + console.warn(`⚠️ 读取文件失败: ${mdFile} - ${error.message}`); + } + } + + console.log(`🔗 找到 ${referencedImages.size} 个被引用的图片`); + + // 找出未被引用的图片 + const unusedImages = []; + + for (const imageFile of imageFiles) { + const resolvedImagePath = path.resolve(imageFile); + const isReferenced = referencedImages.has(resolvedImagePath); + + if (!isReferenced) { + unusedImages.push(imageFile); + } + } + + console.log(`🗑️ 找到 ${unusedImages.length} 个未使用的图片`); + + if (unusedImages.length === 0) { + console.log('✅ 所有图片都在使用中,无需清理'); + return; + } + + // 删除未使用的图片 + let deletedCount = 0; + + for (const unusedImage of unusedImages) { + try { + fs.unlinkSync(unusedImage); + console.log(`🗑️ 已删除: ${path.relative(process.cwd(), unusedImage)}`); + deletedCount++; + } catch (error) { + console.error(`❌ 删除失败: ${unusedImage} - ${error.message}`); + } + } + + // 清理空目录 + try { + cleanEmptyDirectories(ASSETS_DIR); + } catch (error) { + console.warn(`⚠️ 清理空目录时出错: ${error.message}`); + } + + console.log(`\n✅ 清理完成!删除了 ${deletedCount} 个未使用的图片文件`); +} + +/** + * 递归清理空目录 + */ +function cleanEmptyDirectories(dir) { + if (!fs.existsSync(dir)) return; + + const files = fs.readdirSync(dir); + + if (files.length === 0) { + fs.rmdirSync(dir); + console.log(`🗑️ 已删除空目录: ${path.relative(process.cwd(), dir)}`); + return; + } + + for (const file of files) { + const filePath = path.join(dir, file); + if (fs.statSync(filePath).isDirectory()) { + cleanEmptyDirectories(filePath); + } + } + + // 再次检查目录是否为空 + const remainingFiles = fs.readdirSync(dir); + if (remainingFiles.length === 0) { + fs.rmdirSync(dir); + console.log(`🗑️ 已删除空目录: ${path.relative(process.cwd(), dir)}`); + } +} + +// 运行脚本 +// 检查是否直接运行此脚本 +const scriptPath = fileURLToPath(import.meta.url); +const isMainModule = process.argv[1] && path.resolve(process.argv[1]) === path.resolve(scriptPath); + +if (isMainModule) { + cleanUnusedImages().catch(error => { + console.error('❌ 脚本执行失败:', error.message); + console.error(error.stack); + process.exit(1); + }); +} + +export { cleanUnusedImages }; \ No newline at end of file diff --git a/scripts/convert_friends.py b/scripts/convert_friends.py new file mode 100644 index 0000000..c2bf07c --- /dev/null +++ b/scripts/convert_friends.py @@ -0,0 +1,74 @@ +import re +import json +import os + +def extract_friend_data(html_content): + """从HTML内容中提取友链数据""" + # 匹配友链卡片的正则表达式 + pattern = r'<a href="([^"]+)"[^>]*class="friend-card">\s*' \ + r'<div class="flex items-center gap-2">\s*' \ + r'<img src="([^"]+)"[^>]*>\s*' \ + r'<div class="font-bold[^"]*">([^<]+)</div>\s*' \ + r'</div>\s*' \ + r'<div class="text-sm[^"]*">([^<]+)</div>' + + friends = [] + for match in re.finditer(pattern, html_content, re.DOTALL): + url, avatar, name, description = match.groups() + friend = { + "name": name.strip(), + "avatar": avatar.strip(), + "description": description.strip(), + "url": url.strip() + } + friends.append(friend) + return friends + +def read_friends_astro(): + """读取friends.astro文件内容""" + file_path = os.path.join(os.path.dirname(os.path.dirname(__file__)), + 'src', 'pages', 'friends.astro') + with open(file_path, 'r', encoding='utf-8') as f: + return f.read() + +def read_existing_friends_json(): + """读取现有的friends.json文件内容""" + file_path = os.path.join(os.path.dirname(os.path.dirname(__file__)), + 'src', 'data', 'friends.json') + try: + with open(file_path, 'r', encoding='utf-8') as f: + return json.load(f) + except (FileNotFoundError, json.JSONDecodeError): + return {"friends": []} + +def write_friends_json(friends_data): + """将友链数据写入friends.json文件""" + file_path = os.path.join(os.path.dirname(os.path.dirname(__file__)), + 'src', 'data', 'friends.json') + with open(file_path, 'w', encoding='utf-8') as f: + json.dump(friends_data, f, ensure_ascii=False, indent=2) + +def main(): + # 读取friends.astro内容 + astro_content = read_friends_astro() + + # 提取友链数据 + new_friends = extract_friend_data(astro_content) + + # 读取现有的friends.json + existing_data = read_existing_friends_json() + + # 将新的友链数据添加到现有数据中 + # 使用URL作为唯一标识符,避免重复 + existing_urls = {friend["url"] for friend in existing_data["friends"]} + for friend in new_friends: + if friend["url"] not in existing_urls: + existing_data["friends"].append(friend) + existing_urls.add(friend["url"]) + + # 写入更新后的数据 + write_friends_json(existing_data) + print(f"成功提取并添加了 {len(new_friends)} 个友链数据") + +if __name__ == "__main__": + main() \ No newline at end of file diff --git a/scripts/generate-gallery-index.js b/scripts/generate-gallery-index.js new file mode 100644 index 0000000..b787de0 --- /dev/null +++ b/scripts/generate-gallery-index.js @@ -0,0 +1,81 @@ +import fs from 'fs'; +import path from 'path'; +import { fileURLToPath } from 'url'; +import { glob } from 'glob'; + +const __filename = fileURLToPath(import.meta.url); +const __dirname = path.dirname(__filename); +const rootDir = path.resolve(__dirname, '..'); + +const IMAGE_DIR = path.join(rootDir, 'public', 'api', 'i'); +const OUTPUT_FILE = path.join(IMAGE_DIR, 'images.json'); + +// 支持的图片扩展名 +const IMAGE_EXTENSIONS = new Set([ + '.jpg', '.jpeg', '.png', '.gif', '.webp', '.svg', '.bmp', '.ico', '.avif' +]); + +function parseImagePath(relativePath) { + // 路径格式: public/api/i/YYYY/MM/DD/filename.ext + const match = relativePath.match(/(\d{4})\/(\d{1,2})\/(\d{1,2})\/([^/]+)\.(.+)$/); + if (!match) return null; + + const [, year, month, day, filename, ext] = match; + + return { + url: `/api/i/${year}/${month}/${day}/${filename}.${ext}`, + filename: `${filename}.${ext}`, + year, + month: month.padStart(2, '0'), + day: day.padStart(2, '0'), + date: `${year}-${month.padStart(2, '0')}-${day.padStart(2, '0')}` + }; +} + +async function generateGalleryIndex() { + console.log('正在扫描图片目录...'); + + // 扫描所有图片文件 + const pattern = path.join(IMAGE_DIR, '**/*.*').replace(/\\/g, '/'); + const files = await glob(pattern, { + ignore: [ + '**/images.json', // 忽略索引文件本身 + '**/cache/**', // 忽略缓存目录 + '**/gallery-meow/**', // 忽略其他目录 + '**/favicon.ico', // 忽略 favicon + '**/index.html' // 忽略 index.html + ] + }); + + console.log(`找到 ${files.length} 个文件`); + + // 解析并过滤图片 + const images = []; + for (const file of files) { + // 获取相对路径 + const relativePath = path.relative(IMAGE_DIR, file).replace(/\\/g, '/'); + + // 检查文件扩展名 + const ext = path.extname(file).toLowerCase(); + if (!IMAGE_EXTENSIONS.has(ext)) { + continue; + } + + // 解析路径信息 + const parsed = parseImagePath(relativePath); + if (parsed) { + images.push(parsed); + } + } + + // 按日期倒序排序 + images.sort((a, b) => b.date.localeCompare(a.date)); + + console.log(`共找到 ${images.length} 张图片`); + + // 写入 JSON 文件 + fs.writeFileSync(OUTPUT_FILE, JSON.stringify(images, null, 2), 'utf-8'); + console.log(`已生成索引文件: ${OUTPUT_FILE}`); +} + +generateGalleryIndex().catch(console.error); diff --git a/scripts/new-post.js b/scripts/new-post.js new file mode 100644 index 0000000..ce2b95c --- /dev/null +++ b/scripts/new-post.js @@ -0,0 +1,61 @@ +/* This is a script to create a new post markdown file with front-matter */ + +import fs from "fs" +import path from "path" + +function getDate() { + const today = new Date() + const year = today.getFullYear() + const month = String(today.getMonth() + 1).padStart(2, "0") + const day = String(today.getDate()).padStart(2, "0") + const hours = String(today.getHours()).padStart(2, "0") + const minutes = String(today.getMinutes()).padStart(2, "0") + const seconds = String(today.getSeconds()).padStart(2, "0") + + return `${year}-${month}-${day}T${hours}:${minutes}:${seconds}` +} + +const args = process.argv.slice(2) + +if (args.length === 0) { + console.error(`Error: No filename argument provided +Usage: npm run new-post -- <filename>`) + process.exit(1) // Terminate the script and return error code 1 +} + +let fileName = args[0] + +// Add .md extension if not present +const fileExtensionRegex = /\.(md|mdx)$/i +if (!fileExtensionRegex.test(fileName)) { + fileName += ".md" +} + +const targetDir = "./src/content/posts/" +const fullPath = path.join(targetDir, fileName) + +if (fs.existsSync(fullPath)) { + console.error(`Error: File ${fullPath} already exists `) + process.exit(1) +} + +// recursive mode creates multi-level directories +const dirPath = path.dirname(fullPath) +if (!fs.existsSync(dirPath)) { + fs.mkdirSync(dirPath, { recursive: true }) +} + +const content = `--- +title: ${args[0]} +published: ${getDate()} +description: '' +image: '' + +draft: false +lang: '' +--- +` + +fs.writeFileSync(path.join(targetDir, fileName), content) + +console.log(`Post ${fullPath} created`) diff --git a/scripts/test-category-logic.js b/scripts/test-category-logic.js new file mode 100644 index 0000000..05c0cde --- /dev/null +++ b/scripts/test-category-logic.js @@ -0,0 +1,78 @@ +const CATEGORY_SEPARATOR = " > "; + +function parseCategoryPath(categoryString) { + return categoryString + .split(CATEGORY_SEPARATOR) + .map((s) => s.trim()) + .filter(Boolean); +} + +function stringifyCategoryPath(path) { + return path.join(CATEGORY_SEPARATOR); +} + +function getPostsByCategory(posts, categoryPath) { + const targetPathString = stringifyCategoryPath(categoryPath); + console.log(`Target Path: "${targetPathString}"`); + + return posts.filter((post) => { + const categories = post.data.category; + const categoryArray = + typeof categories === "string" ? [categories] : categories; + + return categoryArray.some((cat) => { + const catPath = parseCategoryPath(cat); + const catPathString = stringifyCategoryPath(catPath); + console.log( + ` Checking Post: "${post.data.title}", Category: "${cat}", Parsed: "${catPathString}"` + ); + + const match = + catPathString === targetPathString || + catPathString.startsWith(targetPathString + CATEGORY_SEPARATOR); + console.log(` Match: ${match}`); + return match; + }); + }); +} + +// Mock Data +const posts = [ + { + data: { + title: "Post 1", + category: "Java > Spring", + }, + }, + { + data: { + title: "Post 2", + category: ["Java > Spring"], + }, + }, + { + data: { + title: "Post 3", + category: "Java > Spring > Boot", + }, + }, + { + data: { + title: "Post 4", + category: "Java", + }, + }, + { + data: { + title: "Post 5", + category: "Other > Spring", + }, + }, +]; + +const targetPath = ["Java", "Spring"]; + +console.log("Testing getPostsByCategory..."); +const result = getPostsByCategory(posts, targetPath); +console.log(`Found ${result.length} posts.`); +result.forEach((p) => console.log(` - ${p.data.title}`)); diff --git a/src/components/ArchivePanel.astro b/src/components/ArchivePanel.astro new file mode 100644 index 0000000..2a95f48 --- /dev/null +++ b/src/components/ArchivePanel.astro @@ -0,0 +1,93 @@ +--- + +import { getSortedPosts } from "../utils/content-utils"; +import { getPostUrlBySlug } from "../utils/url-utils"; + +interface Props { + keyword?: string; +} + +let posts = await getSortedPosts(); + +const groups: { year: number; posts: typeof posts }[] = (() => { + const groupedPosts = posts.reduce( + (grouped: { [year: number]: typeof posts }, post) => { + const year = post.data.published.getFullYear(); + if (!grouped[year]) { + grouped[year] = []; + } + grouped[year].push(post); + return grouped; + }, + {}, + ); + + // convert the object to an array + const groupedPostsArray = Object.keys(groupedPosts).map((key) => ({ + year: Number.parseInt(key), + posts: groupedPosts[Number.parseInt(key)], + })); + + // sort years by latest first + groupedPostsArray.sort((a, b) => b.year - a.year); + return groupedPostsArray; +})(); + +function formatDate(date: Date) { + const month = (date.getMonth() + 1).toString().padStart(2, "0"); + const day = date.getDate().toString().padStart(2, "0"); + return `${month}-${day}`; +} +--- + +<div class="card-base px-8 py-6"> + { + groups.map(group => ( + <div> + <div class="flex flex-row w-full items-center h-[3.75rem]"> + <div class="w-[15%] md:w-[10%] transition text-2xl font-bold text-right text-75">{group.year}</div> + <div class="w-[15%] md:w-[10%]"> + <div class="h-3 w-3 bg-none rounded-full outline outline-[var(--primary)] mx-auto -outline-offset-[2px] z-50 outline-3"></div> + </div> + <div class="w-[70%] md:w-[80%] transition text-left text-50">{group.posts.length} 篇文章</div> + </div> + {group.posts.map(post => ( + <a href={getPostUrlBySlug(post.slug)} + aria-label={post.data.title} + class="group btn-plain !block h-10 w-full rounded-lg hover:text-[initial]" + > + <div class="flex flex-row justify-start items-center h-full"> + <!-- date --> + <div class="w-[15%] md:w-[10%] transition text-sm text-right text-50"> + {formatDate(post.data.published)} + </div> + <!-- dot and line --> + <div class="w-[15%] md:w-[10%] relative dash-line h-full flex items-center"> + <div class="transition-all mx-auto w-1 h-1 rounded group-hover:h-5 + bg-[oklch(0.5_0.05_var(--hue))] group-hover:bg-[var(--primary)] + outline outline-4 z-50 + outline-[var(--card-bg)] + group-hover:outline-[var(--btn-plain-bg-hover)] + group-active:outline-[var(--btn-plain-bg-active)] + " + ></div> + </div> + <!-- post title --> + <div class="w-[70%] md:max-w-[65%] md:w-[65%] text-left font-bold + group-hover:translate-x-1 transition-all group-hover:text-[var(--primary)] + text-75 pr-8 whitespace-nowrap overflow-ellipsis overflow-hidden" + > + {post.data.title} + </div> + <!-- tag list --> + <div class="hidden md:block md:w-[15%] text-left text-sm transition + whitespace-nowrap overflow-ellipsis overflow-hidden + text-30" + ></div> + </div> + </a> + ))} + </div> + )) + } +</div> \ No newline at end of file diff --git a/src/components/CategoryPanel.astro b/src/components/CategoryPanel.astro new file mode 100644 index 0000000..ac7d4a5 --- /dev/null +++ b/src/components/CategoryPanel.astro @@ -0,0 +1,37 @@ +--- +import type { CollectionEntry } from "astro:content"; +import { getPostUrlBySlug } from "../utils/url-utils"; +import { formatDateToYYYYMMDD } from "../utils/date-utils"; + +interface Props { + posts: CollectionEntry<"posts">[]; +} + +const { posts } = Astro.props; +--- + +<div class="flex flex-col gap-3"> + {posts.map((post) => ( + <a + href={getPostUrlBySlug(post.slug)} + class="card-base px-6 py-4 rounded-lg hover:border-[var(--primary)] transition-all group" + > + <div class="flex items-start gap-4"> + <div class="text-sm text-50 whitespace-nowrap"> + {formatDateToYYYYMMDD(post.data.published)} + </div> + <div class="w-px h-5 bg-[var(--line-divider)]"></div> + <div class="flex-1 min-w-0"> + <h3 class="font-bold text-75 group-hover:text-[var(--primary)] transition-all"> + {post.data.title} + </h3> + {post.data.description && ( + <p class="text-sm text-50 mt-1 line-clamp-1"> + {post.data.description} + </p> + )} + </div> + </div> + </a> + ))} +</div> diff --git a/src/components/CategoryTree.astro b/src/components/CategoryTree.astro new file mode 100644 index 0000000..5784248 --- /dev/null +++ b/src/components/CategoryTree.astro @@ -0,0 +1,62 @@ +--- +import type { CategoryNode } from "@/types/config"; +import { Icon } from "astro-icon/components"; + +interface Props { + node: CategoryNode; + depth?: number; +} + +const { node, depth = 0 } = Astro.props; +--- + +<div class="relative transition-all"> + <div + class:list={[ + "flex items-center justify-between rounded-lg hover:bg-[var(--btn-plain-bg-hover)] transition-all group select-none", + depth === 0 + ? "p-4 bg-[var(--card-bg)] border border-[var(--line-divider)] mb-2" + : "p-2", + ]} + > + <a href={node.url} class="flex items-center gap-2 flex-1 min-w-0"> + <Icon + name={depth === 0 + ? "material-symbols:folder-open-rounded" + : "material-symbols:folder-outline-rounded"} + class:list={[ + "text-[var(--primary)] shrink-0", + depth === 0 ? "text-2xl" : "text-xl", + ]} + /> + <span + class:list={[ + "truncate group-hover:text-[var(--primary)] transition-all", + depth === 0 ? "font-bold text-lg text-90" : "font-medium text-75", + ]}>{node.name}</span + > + </a> + <span + class="text-sm text-50 bg-black/5 dark:bg-white/10 px-2 py-0.5 rounded shrink-0 ml-2" + > + {node.count} + </span> + </div> + + { + node.children && node.children.length > 0 && ( + <div + class:list={[ + "flex flex-col gap-1", + depth === 0 + ? "mt-1 mb-3 ml-6 pl-4 border-l-2 border-[var(--line-divider)]" + : "ml-4 pl-4 border-l-2 border-[var(--line-divider)]", + ]} + > + {node.children.map((child) => ( + <Astro.self node={child} depth={depth + 1} /> + ))} + </div> + ) + } +</div> diff --git a/src/components/ConfigCarrier.astro b/src/components/ConfigCarrier.astro new file mode 100644 index 0000000..68b3dde --- /dev/null +++ b/src/components/ConfigCarrier.astro @@ -0,0 +1,7 @@ +--- + +import { siteConfig } from "../config"; +--- + +<div id="config-carrier" data-hue={siteConfig.themeColor.hue}> +</div> diff --git a/src/components/Footer.astro b/src/components/Footer.astro new file mode 100644 index 0000000..98e260f --- /dev/null +++ b/src/components/Footer.astro @@ -0,0 +1,170 @@ +--- +import { profileConfig } from "../config"; +import { url } from "../utils/url-utils"; +import { execSync } from "child_process"; + +const currentYear = new Date().getFullYear(); + +let commitHash = "unknown"; +let buildDate = "unknown"; + +try { + commitHash = execSync("git rev-parse --short=7 HEAD").toString().trim(); + + const date = new Date(); + const year = date.getFullYear(); + const month = (date.getMonth() + 1).toString().padStart(2, "0"); + const day = date.getDate().toString().padStart(2, "0"); + const hours = date.getHours().toString().padStart(2, "0"); + const minutes = date.getMinutes().toString().padStart(2, "0"); + const seconds = date.getSeconds().toString().padStart(2, "0"); + buildDate = `${year}-${month}-${day} ${hours}:${minutes}:${seconds}`; +} catch (e) { + console.warn("Failed to get git info", e); +} +--- + +<!--<div class="border-t border-[var(--primary)] mx-16 border-dashed py-8 max-w-[var(--page-width)] flex flex-col items-center justify-center px-6">--> +<div + class="transition border-t border-black/10 dark:border-white/15 my-10 border-dashed mx-4 md:mx-32" +> +</div> +<div class="card-base w-fit mx-auto rounded-xl mt-4 mb-4"> + <div class="transition text-50 text-sm text-center p-6"> + © <span id="copyright-year">2024 - {currentYear}</span> + <a + class="transition link text-[var(--primary)] font-medium" + target="_blank" + href="https://space.bilibili.com/325903362" + > + {profileConfig.name} + </a>,采用 + <a + class="transition link text-[var(--primary)] font-medium" + target="_blank" + href="https://creativecommons.org/licenses/by-nc-sa/4.0/" + >CC BY-NC-SA 4.0</a + > 许可 + <br /> + <a + class="transition link text-[var(--primary)] font-medium" + target="_blank" + href={url("rss.xml")}>RSS</a + > / + <a + class="transition link text-[var(--primary)] font-medium" + target="_blank" + href={url("sitemap-index.xml")}>网站地图</a + > + <br /> + 由 + <a + class="transition link text-[var(--primary)] font-medium" + target="_blank" + href="https://astro.build">Astro</a + > 和 + <a + class="transition link text-[var(--primary)] font-medium" + target="_blank" + href="https://github.com/saicaca/fuwari">Fuwari</a + > 强力驱动 + <br /> + 本网站代码 + <a + class="transition link text-[var(--primary)] font-medium" + target="_blank" + href="https://github.com/afoim/fuwari">已开源</a + > + <a + class="transition link text-black/30 dark:text-white/30 hover:text-[var(--primary)] text-xs ml-1" + target="_blank" + href={`https://github.com/afoim/fuwari/commit/${commitHash}`} + >({commitHash} @ {buildDate})</a + > + <br /> + + <a + class="transition link text-[var(--primary)] font-medium inline-flex items-center" + href="https://beian.miit.gov.cn/#/Integrated/index" + target="_blank" + ><img + alt="" + src="/favicon/foot-icp.png" + class="h-4 mr-1" + />晋ICP备2025071728号-1</a + > + <br /> + <div class="server-info-wrapper flex items-center justify-center"> + <span class="server-info text-black/30 dark:text-white/30 text-xs"></span> + <img + class="server-icon h-5 ml-1 hidden bg-white rounded p-0.5" + alt="Server Icon" + /> + </div> + + <!-- <a class="transition link text-[var(--primary)] font-medium inline-flex items-center" href="https://beian.mps.gov.cn/#/query/webSearch?code=34010302002608" target="_blank"><img alt="" src="/favicon/foot-ga.png" class="h-4 mr-1">皖公网安备34010302002608号</a> + <br> --> + </div> +</div> + +<script> + function updateServerInfo(server) { + const wrappers = document.querySelectorAll(".server-info-wrapper"); + wrappers.forEach((wrapper) => { + const serverInfo = wrapper.querySelector(".server-info"); + const serverIcon = wrapper.querySelector(".server-icon"); + if (serverInfo) { + if (server) { + serverInfo.innerText = `访问节点:${server}`; + if (serverIcon) { + const serverLower = server.toLowerCase(); + if (serverLower === "edgeone-pages") { + serverIcon.src = "/cdn/eo.png"; + serverIcon.classList.remove("hidden"); + serverInfo.classList.add("hidden"); + } else if (serverLower === "cloudflare") { + serverIcon.src = "/cdn/cf.svg"; + serverIcon.classList.remove("hidden"); + serverInfo.classList.add("hidden"); + } else if (serverLower === "esa") { + serverIcon.src = "/cdn/esa.svg"; + serverIcon.classList.remove("hidden"); + serverInfo.classList.add("hidden"); + } else { + serverIcon.classList.add("hidden"); + serverInfo.classList.remove("hidden"); + } + } + } else { + serverInfo.innerText = "访问节点:未知"; + serverInfo.classList.remove("hidden"); + if (serverIcon) serverIcon.classList.add("hidden"); + } + } + }); + } + + function initServerInfo() { + const isDevMode = localStorage.getItem("dev-mode") === "true"; + if (isDevMode) { + const devServer = localStorage.getItem("dev-server"); + updateServerInfo(devServer); + } else { + const url = window.location.href; + fetch(url, { method: "HEAD" }) + .then((response) => { + const server = response.headers.get("server"); + updateServerInfo(server); + }) + .catch((error) => { + console.error("Failed to fetch server info:", error); + updateServerInfo(null); + }); + } + } + + initServerInfo(); + document.addEventListener("astro:after-swap", initServerInfo); // Support View Transitions if enabled + // Support Swup if enabled (listen to content replacement) + document.addEventListener("content:replace", initServerInfo); +</script> diff --git a/src/components/Navbar.astro b/src/components/Navbar.astro new file mode 100644 index 0000000..10e1644 --- /dev/null +++ b/src/components/Navbar.astro @@ -0,0 +1,127 @@ +--- +import { Icon } from "astro-icon/components"; +import { navBarConfig, siteConfig } from "../config"; +import { LinkPresets } from "../constants/link-presets"; +import { LinkPreset, type NavBarLink } from "../types/config"; +import { url } from "../utils/url-utils"; + +import Search from "./Search.svelte"; +import DisplaySettings from "./widget/DisplaySettings.svelte"; +import NavMenuPanel from "./widget/NavMenuPanel.astro"; +const className = Astro.props.class; + +let links: NavBarLink[] = navBarConfig.links.map( + (item: NavBarLink | LinkPreset): NavBarLink => { + if (typeof item === "number") { + return LinkPresets[item]; + } + return item; + } +); +--- + +<div id="navbar" class="z-50 onload-animation"> + <div + class="absolute h-8 left-0 right-0 -top-8 bg-[var(--card-bg)] transition" + > + </div> + <!-- used for onload animation --> + <div + class:list={[ + className, + "card-base border border-black/10 dark:border-white/10 !overflow-visible max-w-[var(--page-width)] h-[4.5rem] !rounded-t-none mx-auto flex items-center justify-between px-4", + ]} + > + <a + href={url("/")} + class="btn-plain scale-animation rounded-lg h-[3.25rem] px-5 font-bold active:scale-95" + > + <div class="flex flex-row text-[var(--primary)] items-center text-md"> + <Icon + name="material-symbols:home-outline-rounded" + class="text-[1.75rem] mb-1 mr-2" + /> + {siteConfig.title} + </div> + </a> + <div class="hidden md:flex"> + { + links.map((l) => { + return ( + <a + aria-label={l.name} + href={l.external ? l.url : url(l.url)} + target={l.external ? "_blank" : null} + class="btn-plain scale-animation rounded-lg h-11 font-bold px-5 active:scale-95" + > + <div class="flex items-center"> + {l.name} + {l.external && ( + <Icon + name="fa6-solid:arrow-up-right-from-square" + class="text-[0.875rem] transition -translate-y-[1px] ml-1 text-black/[0.2] dark:text-white/[0.2]" + /> + )} + </div> + </a> + ); + }) + } + </div> + <div class="flex"> + <!--<SearchPanel client:load>--> + <Search client:only="svelte" /> + { + !siteConfig.themeColor.fixed && ( + <button + aria-label="Display Settings" + class="btn-plain scale-animation rounded-lg h-11 w-11 active:scale-90" + id="display-settings-switch" + > + <Icon + name="material-symbols:palette-outline" + class="text-[1.25rem]" + /> + </button> + ) + } + + <button + aria-label="Menu" + name="Nav Menu" + class="btn-plain scale-animation rounded-lg w-11 h-11 active:scale-90 md:!hidden" + id="nav-menu-switch" + > + <Icon name="material-symbols:menu-rounded" class="text-[1.25rem]" /> + </button> + </div> + <NavMenuPanel links={links} /> + <DisplaySettings client:only="svelte" /> + </div> +</div> + +<script> + function loadButtonScript() { + let settingBtn = document.getElementById("display-settings-switch"); + if (settingBtn) { + settingBtn.onclick = function () { + let settingPanel = document.getElementById("display-setting"); + if (settingPanel) { + settingPanel.classList.toggle("float-panel-closed"); + } + }; + } + + let menuBtn = document.getElementById("nav-menu-switch"); + if (menuBtn) { + menuBtn.onclick = function () { + let menuPanel = document.getElementById("nav-menu-panel"); + if (menuPanel) { + menuPanel.classList.toggle("float-panel-closed"); + } + }; + } + } + + loadButtonScript(); +</script> diff --git a/src/components/PostCard.astro b/src/components/PostCard.astro new file mode 100644 index 0000000..faa5fe2 --- /dev/null +++ b/src/components/PostCard.astro @@ -0,0 +1,112 @@ +--- +import path from "node:path"; +import type { CollectionEntry } from "astro:content"; +import { Icon } from "astro-icon/components"; + +import { getDir } from "../utils/url-utils"; +import { formatDateToYYYYMMDD } from "../utils/date-utils"; +import PostMetadata from "./PostMeta.astro"; +import ImageWrapper from "./misc/ImageWrapper.astro"; +import { umamiConfig } from "../config"; + +interface Props { + class?: string; + entry: CollectionEntry<"posts">; + title: string; + url: string; + published: Date; + updated?: Date; + image: string; + description: string; + draft: boolean; + style: string; + category?: string | string[]; +} +const { + entry, + title, + url, + published, + updated, + image, + description, + style, + category, +} = Astro.props; + +const isPinned = entry.data.pinned === true; +const className = Astro.props.class; + +const hasCover = image !== undefined && image !== null && image !== ""; + +const coverWidth = "28%"; + +const { remarkPluginFrontmatter } = await entry.render(); +--- +<div class:list={["card-base border border-black/10 dark:border-white/10 flex flex-col w-full rounded-[var(--radius-large)] overflow-hidden relative", className]} style={style}> + <div class:list={["pl-9 pr-2 pt-6 pb-6 relative", {"w-[calc(100%_-_52px_-_12px)]": !hasCover, "w-[calc(100%_-_var(--coverWidth)_-_12px)]": hasCover}]}> + <a href={url} + class="transition group w-full block font-bold mb-2 md:mb-3 text-xl md:text-3xl text-90 relative + hover:text-[var(--primary)] dark:hover:text-[var(--primary)] + active:text-[var(--title-active)] dark:active:text-[var(--title-active)] + before:w-1 before:h-5 before:rounded-md before:bg-[var(--primary)] + before:absolute before:top-1 md:before:top-2 before:-left-[1.125rem] before:block + "> + + {title} + <Icon class="text-[var(--primary)] text-[2rem] transition inline absolute translate-y-0.5 opacity-0 group-hover:opacity-100 -translate-x-1 group-hover:translate-x-0" name="material-symbols:chevron-right-rounded"></Icon> + </a> + + <!-- description --> + <div class:list={["transition text-75 mb-3.5 pr-4 line-clamp-1 md:line-clamp-2"]}> + { description || remarkPluginFrontmatter.excerpt } + </div> + + <!-- metadata --> + <PostMetadata published={published} updated={updated} hideUpdateDate={true} hidePublishedDate={true} slug={entry.slug} category={category} class="mb-2 md:mb-4"></PostMetadata> + + <!-- word count, read time and page views --> + <div class="text-sm text-black/30 dark:text-white/30 flex gap-4 transition"> + <div>{formatDateToYYYYMMDD(published)}</div> + <div>|</div> + <div>{remarkPluginFrontmatter.words} 字</div> + <div>|</div> + <div>{remarkPluginFrontmatter.minutes} 分钟</div> + </div> + + </div> + + {hasCover && <a href={url} aria-label={title} + class:list={["group", + "max-h-none mx-0 mt-0", + "w-[var(--coverWidth)] absolute top-3 bottom-3 right-3 rounded-xl overflow-hidden active:scale-95" + ]} > + <div class="absolute pointer-events-none z-10 w-full h-full group-hover:bg-black/30 group-active:bg-black/50 transition"></div> + <!-- 封面图上的箭头 --> + <div class="absolute pointer-events-none z-20 w-full h-full flex items-center justify-center "> + <Icon name="material-symbols:chevron-right-rounded" + class="transition opacity-0 group-hover:opacity-100 scale-50 group-hover:scale-100 text-white text-5xl"> + </Icon> + </div> + <ImageWrapper src={image} basePath={path.join("content/posts/", getDir(entry.id))} alt="Cover Image of the Post" + class="w-full h-full"> + </ImageWrapper> + </a>} + + {!hasCover && + <a href={url} aria-label={title} class="!flex btn-regular w-[3.25rem] + absolute right-3 top-3 bottom-3 rounded-xl bg-[var(--enter-btn-bg)] + hover:bg-[var(--enter-btn-bg-hover)] active:bg-[var(--enter-btn-bg-active)] active:scale-95 + "> + <Icon name="material-symbols:chevron-right-rounded" + class="transition text-[var(--primary)] text-4xl mx-auto"> + </Icon> + </a> + } +</div> +<div class="transition border-t-[1px] border-dashed mx-6 border-black/10 dark:border-white/[0.15] last:border-t-0 hidden"></div> + + + +<style define:vars={{coverWidth}}> +</style> \ No newline at end of file diff --git a/src/components/PostMeta.astro b/src/components/PostMeta.astro new file mode 100644 index 0000000..827dbf2 --- /dev/null +++ b/src/components/PostMeta.astro @@ -0,0 +1,136 @@ +--- +import { Icon } from "astro-icon/components"; + +import { getDir, url } from "../utils/url-utils"; +import { formatDateToYYYYMMDD } from "../utils/date-utils"; +import { umamiConfig } from "../config"; +import { getPostCategories, getCategoryUrl, parseCategoryPath } from "../utils/category-utils"; + +interface Props { + class: string; + published: Date; + updated?: Date; + hideUpdateDate?: boolean; + hidePublishedDate?: boolean; + slug?: string; + category?: string | string[]; +} +const { + published, + updated, + hideUpdateDate = false, + hidePublishedDate = false, + slug, + category, +} = Astro.props; +const className = Astro.props.class; + +// Process category data +const categories = typeof category === "string" + ? [category] + : category || []; +--- + +<div class:list={["flex flex-wrap text-neutral-500 dark:text-neutral-400 items-center gap-4 gap-x-4 gap-y-2", className]}> + <!-- publish date --> + {!hidePublishedDate && ( + <div class="flex items-center"> + <div class="meta-icon"> + <Icon name="material-symbols:calendar-today-outline-rounded" class="text-xl"></Icon> + </div> + <span class="text-50 text-sm font-medium">{formatDateToYYYYMMDD(published)}</span> + </div> + )} + + <!-- update date --> + {!hideUpdateDate && updated && updated.getTime() !== published.getTime() && ( + <div class="flex items-center"> + <div class="meta-icon" + > + <Icon name="material-symbols:edit-calendar-outline-rounded" class="text-xl"></Icon> + </div> + <span class="text-50 text-sm font-medium">{formatDateToYYYYMMDD(updated)}</span> + </div> + )} + + <!-- page views & visitors --> + {slug && ( + <> + <div class="flex items-center"> + <div class="meta-icon"> + <Icon name="material-symbols:visibility-outline-rounded" class="text-xl"></Icon> + </div> + <span class="text-50 text-sm font-medium" id={`page-views-${slug}`}>-</span> + </div> + <div class="flex items-center"> + <div class="meta-icon"> + <Icon name="material-symbols:person" class="text-xl"></Icon> + </div> + <span class="text-50 text-sm font-medium" id={`page-visitors-${slug}`}>-</span> + </div> + </> + )} + + <!-- categories --> + {categories.length > 0 && ( + <div class="flex items-center flex-wrap gap-2"> + <div class="meta-icon"> + <Icon name="material-symbols:folder-outline-rounded" class="text-xl"></Icon> + </div> + {categories.map((cat, index) => { + const catPath = parseCategoryPath(cat); + const catUrl = getCategoryUrl(catPath); + const catName = catPath[catPath.length - 1]; // Show last level only + + return ( + <> + {index > 0 && <span class="text-30">,</span>} + <a + href={catUrl} + class="text-50 text-sm font-medium hover:text-[var(--primary)] transition" + > + {catName} + </a> + </> + ); + })} + </div> + )} +</div> + +{slug && ( + <script define:vars={{ slug, umamiConfig }}> + + // 获取访问量统计 + async function fetchPageViews() { + if (!umamiConfig.enable) return; + try { + const statsData = await fetchUmamiStats(umamiConfig.baseUrl, umamiConfig.shareId, { + timezone: umamiConfig.timezone, + path: `eq./posts/${slug}/` + }); + + const pageViews = statsData.pageviews || 0; + const visits = statsData.visitors || 0; + + const viewsElement = document.getElementById(`page-views-${slug}`); + const visitorsElement = document.getElementById(`page-visitors-${slug}`); + if (viewsElement) viewsElement.textContent = `${pageViews} 次`; + if (visitorsElement) visitorsElement.textContent = `${visits} 人`; + } catch (error) { + console.error('Error fetching page views:', error); + const viewsElement = document.getElementById(`page-views-${slug}`); + const visitorsElement = document.getElementById(`page-visitors-${slug}`); + if (viewsElement) viewsElement.textContent = '-'; + if (visitorsElement) visitorsElement.textContent = '-'; + } + } + + // 页面加载完成后获取统计数据 + if (document.readyState === 'loading') { + document.addEventListener('DOMContentLoaded', fetchPageViews); + } else { + fetchPageViews(); + } + </script> +)} diff --git a/src/components/PostPage.astro b/src/components/PostPage.astro new file mode 100644 index 0000000..29e9a68 --- /dev/null +++ b/src/components/PostPage.astro @@ -0,0 +1,29 @@ +--- +import type { CollectionEntry } from "astro:content"; +import { getPostUrlBySlug } from "@utils/url-utils"; +import PostCard from "./PostCard.astro"; + +const { page } = Astro.props; + +let delay = 0; +const interval = 50; +--- + +<div class="transition flex flex-col rounded-[var(--radius-large)] bg-transparent gap-4 mb-4"> + + {page.data.map((entry: CollectionEntry<"posts">) => ( + <PostCard + entry={entry} + title={entry.data.title} + published={entry.data.published} + updated={entry.data.updated} + url={getPostUrlBySlug(entry.slug)} + image={entry.data.image} + description={entry.data.description} + draft={entry.data.draft} + category={entry.data.category} + class:list="onload-animation" + style={`animation-delay: calc(var(--content-delay) + ${delay++ * interval}ms);`} + ></PostCard> + ))} +</div> \ No newline at end of file diff --git a/src/components/Search.svelte b/src/components/Search.svelte new file mode 100644 index 0000000..7849c41 --- /dev/null +++ b/src/components/Search.svelte @@ -0,0 +1,206 @@ +<script lang="ts"> + +import Icon from "@iconify/svelte"; +import { url } from "@utils/url-utils.ts"; +import { onMount } from "svelte"; + +interface SearchResult { + url: string; + meta: { + title: string; + }; + excerpt: string; + urlPath?: string; +} + +let keywordDesktop = ""; +let keywordMobile = ""; +let result: SearchResult[] = []; +let isSearching = false; +let posts: any[] = []; + +const togglePanel = () => { + const panel = document.getElementById("search-panel"); + panel?.classList.toggle("float-panel-closed"); +}; + +const setPanelVisibility = (show: boolean, isDesktop: boolean): void => { + const panel = document.getElementById("search-panel"); + if (!panel || !isDesktop) return; + + if (show) { + panel.classList.remove("float-panel-closed"); + } else { + panel.classList.add("float-panel-closed"); + } +}; + +const highlightText = (text: string, keyword: string): string => { + if (!keyword) return text; + const regex = new RegExp(`(${keyword})`, "gi"); + return text.replace(regex, "<mark>$1</mark>"); +}; + +const search = async (keyword: string, isDesktop: boolean): Promise<void> => { + if (!keyword) { + setPanelVisibility(false, isDesktop); + result = []; + return; + } + + isSearching = true; + + try { + const searchResults = posts + .filter((post) => { + const keywordLower = keyword.toLowerCase(); + const searchText = + `${post.title} ${post.description} ${post.content}`.toLowerCase(); + const urlPath = `/posts/${post.link}`; + + // 支持内容搜索和URL后缀搜索 + return searchText.includes(keywordLower) || + urlPath.toLowerCase().includes(keywordLower) || + post.link.toLowerCase().includes(keywordLower); + }) + .map((post) => { + const contentLower = post.content.toLowerCase(); + const keywordLower = keyword.toLowerCase(); + const contentIndex = contentLower.indexOf(keywordLower); + + let excerpt = ''; + if (contentIndex !== -1) { + const start = Math.max(0, contentIndex - 50); + const end = Math.min(post.content.length, contentIndex + 100); + excerpt = post.content.substring(start, end); + if (start > 0) excerpt = '...' + excerpt; + if (end < post.content.length) excerpt = excerpt + '...'; + } else { + excerpt = post.description || post.content.substring(0, 150) + '...'; + } + + return { + url: url(`/posts/${post.link}/`), + meta: { + title: post.title + }, + excerpt: highlightText(excerpt, keyword), + urlPath: `/posts/${post.link}` + }; + }); + + result = searchResults; + setPanelVisibility(result.length > 0, isDesktop); + } catch (error) { + console.error("Search error:", error); + result = []; + setPanelVisibility(false, isDesktop); + } finally { + isSearching = false; + } +}; + +onMount(async () => { + try { + const response = await fetch("/rss.xml"); + const text = await response.text(); + const parser = new DOMParser(); + const xml = parser.parseFromString(text, "text/xml"); + const items = xml.querySelectorAll("item"); + + posts = Array.from(items).map((item) => { + // 尝试多种方式获取content:encoded内容 + let content = ""; + const contentEncoded = + item.getElementsByTagNameNS("*", "encoded")[0]?.textContent || + item.querySelector("*|encoded")?.textContent || + ""; + + if (contentEncoded) { + content = contentEncoded.replace(/<[^>]*>/g, ""); + } + + return { + title: item.querySelector("title")?.textContent || "", + description: item.querySelector("description")?.textContent || "", + content: content, + link: item.querySelector("link")?.textContent?.replace(/.*\/posts\/(.*?)\//, "$1") || "", + }; + }); + } catch (error) { + console.error("Error fetching RSS:", error); + } +}); + +$: search(keywordDesktop, true); +$: search(keywordMobile, false); +</script> + +<!-- search bar for desktop view --> +<div id="search-bar" class="hidden lg:flex transition-all items-center h-11 mr-2 rounded-lg + bg-black/[0.04] hover:bg-black/[0.06] focus-within:bg-black/[0.06] + dark:bg-white/5 dark:hover:bg-white/10 dark:focus-within:bg-white/10 +"> + <Icon icon="material-symbols:search" class="absolute text-[1.25rem] pointer-events-none ml-3 transition my-auto text-black/30 dark:text-white/30"></Icon> + <input placeholder="搜索" bind:value={keywordDesktop} on:focus={() => search(keywordDesktop, true)} + class="transition-all pl-10 text-sm bg-transparent outline-0 + h-full w-40 active:w-60 focus:w-60 text-black/50 dark:text-white/50" + > +</div> + +<!-- toggle btn for phone/tablet view --> +<button on:click={togglePanel} aria-label="Search Panel" id="search-switch" + class="btn-plain scale-animation lg:!hidden rounded-lg w-11 h-11 active:scale-90"> + <Icon icon="material-symbols:search" class="text-[1.25rem]"></Icon> +</button> + +<!-- search panel --> +<div id="search-panel" class="float-panel float-panel-closed search-panel absolute md:w-[30rem] +top-20 left-4 md:left-[unset] right-4 shadow-2xl rounded-2xl p-2"> + + <!-- search bar inside panel for phone/tablet --> + <div id="search-bar-inside" class="flex relative lg:hidden transition-all items-center h-11 rounded-xl + bg-black/[0.04] hover:bg-black/[0.06] focus-within:bg-black/[0.06] + dark:bg-white/5 dark:hover:bg-white/10 dark:focus-within:bg-white/10 + "> + <Icon icon="material-symbols:search" class="absolute text-[1.25rem] pointer-events-none ml-3 transition my-auto text-black/30 dark:text-white/30"></Icon> + <input placeholder="Search" bind:value={keywordMobile} + class="pl-10 absolute inset-0 text-sm bg-transparent outline-0 + focus:w-60 text-black/50 dark:text-white/50" + > + </div> + + <!-- search results --> + {#each result as item} + <a href={item.url} + class="transition first-of-type:mt-2 lg:first-of-type:mt-0 group block + rounded-xl text-lg px-3 py-2 hover:bg-[var(--btn-plain-bg-hover)] active:bg-[var(--btn-plain-bg-active)]"> + <div class="transition text-90 inline-flex font-bold group-hover:text-[var(--primary)]"> + {item.meta.title}<Icon icon="fa6-solid:chevron-right" class="transition text-[0.75rem] translate-x-1 my-auto text-[var(--primary)]"></Icon> + </div> + <div class="transition text-xs text-white mb-1 font-mono"> + {item.urlPath} + </div> + <div class="transition text-sm text-50"> + {@html item.excerpt} + </div> + </a> + {/each} +</div> + +<style> + input:focus { + outline: 0; + } + .search-panel { + background-color: var(--float-panel-bg-opaque); + max-height: calc(100vh - 100px); + overflow-y: auto; + scrollbar-width: none; /* Firefox */ + -ms-overflow-style: none; /* IE and Edge */ + } + + .search-panel::-webkit-scrollbar { + display: none; /* Chrome, Safari and Opera */ + } +</style> diff --git a/src/components/control/BackToTop.astro b/src/components/control/BackToTop.astro new file mode 100644 index 0000000..0a93af4 --- /dev/null +++ b/src/components/control/BackToTop.astro @@ -0,0 +1,49 @@ +--- +import { Icon } from "astro-icon/components"; +--- + +<!-- There can't be a filter on parent element, or it will break `fixed` --> +<div class="back-to-top-wrapper hidden lg:block"> + <div id="back-to-top-btn" class="back-to-top-btn hide flex items-center rounded-2xl overflow-hidden transition" onclick="backToTop()"> + <button aria-label="Back to Top" class="btn-card h-[3.75rem] w-[3.75rem]"> + <Icon name="material-symbols:keyboard-arrow-up-rounded" class="mx-auto"></Icon> + </button> + </div> +</div> + +<style lang="stylus"> +.back-to-top-wrapper + width: 3.75rem + height: 3.75rem + position: absolute + right: 0 + top: 0 + pointer-events: none + +.back-to-top-btn + color: var(--primary) + font-size: 2.25rem + font-weight: bold + border: none + position: fixed + bottom: 10rem + opacity: 1 + cursor: pointer + transform: translateX(5rem) + pointer-events: auto + i + font-size: 1.75rem + &.hide + transform: translateX(5rem) scale(0.9) + opacity: 0 + pointer-events: none + &:active + transform: translateX(5rem) scale(0.9) + +</style> + +<script is:raw is:inline> +function backToTop() { + window.scroll({ top: 0, behavior: 'smooth' }); +} +</script> diff --git a/src/components/control/Pagination.astro b/src/components/control/Pagination.astro new file mode 100644 index 0000000..d16ddcb --- /dev/null +++ b/src/components/control/Pagination.astro @@ -0,0 +1,83 @@ +--- +import type { Page } from "astro"; +import { Icon } from "astro-icon/components"; +import { url } from "../../utils/url-utils"; +interface Props { + page: Page; + class?: string; + style?: string; +} + +const { page, style } = Astro.props; + +const HIDDEN = -1; + +const className = Astro.props.class; + +const ADJ_DIST = 2; +const VISIBLE = ADJ_DIST * 2 + 1; + +// for test +let count = 1; +let l = page.currentPage; +let r = page.currentPage; +while (0 < l - 1 && r + 1 <= page.lastPage && count + 2 <= VISIBLE) { + count += 2; + l--; + r++; +} +while (0 < l - 1 && count < VISIBLE) { + count++; + l--; +} +while (r + 1 <= page.lastPage && count < VISIBLE) { + count++; + r++; +} + +let pages: number[] = []; +if (l > 1) pages.push(1); +if (l === 3) pages.push(2); +if (l > 3) pages.push(HIDDEN); +for (let i = l; i <= r; i++) pages.push(i); +if (r < page.lastPage - 2) pages.push(HIDDEN); +if (r === page.lastPage - 2) pages.push(page.lastPage - 1); +if (r < page.lastPage) pages.push(page.lastPage); + +const getPageUrl = (p: number) => { + if (p === 1) return "/"; + return `/${p}/`; +}; +--- + +<div class:list={[className, "flex flex-row gap-3 justify-center"]} style={style}> + <a href={page.url.prev || ""} aria-label={page.url.prev ? "Previous Page" : null} + class:list={["btn-card overflow-hidden rounded-lg text-[var(--primary)] w-11 h-11", + {"disabled": page.url.prev == undefined} + ]} + > + <Icon name="material-symbols:chevron-left-rounded" class="text-[1.75rem]"></Icon> + </a> + <div class="bg-[var(--card-bg)] flex flex-row rounded-lg items-center text-neutral-700 dark:text-neutral-300 font-bold" style="backdrop-filter: blur(12px); -webkit-backdrop-filter: blur(12px);"> + {pages.map((p) => { + if (p == HIDDEN) + return <Icon name="material-symbols:more-horiz" class="mx-1"/>; + if (p == page.currentPage) + return <div class="h-11 w-11 rounded-lg bg-[var(--primary)] flex items-center justify-center + font-bold text-white dark:text-black/70" + > + {p} + </div> + return <a href={url(getPageUrl(p))} aria-label=`Page ${p}` + class="transition flex items-center justify-center w-11 h-11 rounded-lg overflow-hidden active:scale-[0.85] hover:bg-[var(--btn-card-bg-hover)] active:bg-[var(--btn-card-bg-active)] text-black/75 dark:text-white/75" + >{p}</a> + })} + </div> + <a href={page.url.next || ""} aria-label={page.url.next ? "Next Page" : null} + class:list={["btn-card overflow-hidden rounded-lg text-[var(--primary)] w-11 h-11", + {"disabled": page.url.next == undefined} + ]} + > + <Icon name="material-symbols:chevron-right-rounded" class="text-[1.75rem]"></Icon> + </a> +</div> \ No newline at end of file diff --git a/src/components/misc/ImageWrapper.astro b/src/components/misc/ImageWrapper.astro new file mode 100644 index 0000000..d668da2 --- /dev/null +++ b/src/components/misc/ImageWrapper.astro @@ -0,0 +1,90 @@ +--- +import path from "node:path"; +interface Props { + id?: string; + src: string; + class?: string; + alt?: string; + position?: string; + basePath?: string; +} +import { Image } from "astro:assets"; +import { url } from "../../utils/url-utils"; +import { imageFallbackConfig, siteConfig } from "../../config"; + +const { id, src, alt, position = "center", basePath = "/" } = Astro.props; +const className = Astro.props.class; + +const isLocal = !( + src.startsWith("/") || + src.startsWith("http") || + src.startsWith("https") || + src.startsWith("data:") +); +const isPublic = src.startsWith("/"); + +// TODO temporary workaround for images dynamic import +// https://github.com/withastro/astro/issues/3373 +// biome-ignore lint/suspicious/noImplicitAnyLet: <explanation> +let img; +if (isLocal) { + const files = import.meta.glob<ImageMetadata>("../../**", { + import: "default", + }); + let normalizedPath = path + .normalize(path.join("../../", basePath, src)) + .replace(/\\/g, "/"); + const file = files[normalizedPath]; + if (!file) { + console.error( + `\n[ERROR] Image file not found: ${normalizedPath.replace("../../", "src/")}`, + ); + } + img = await file(); +} + +const imageClass = "w-full h-full object-cover"; +const imageStyle = `object-position: ${position}`; +--- +<div id={id} class:list={[className, 'overflow-hidden relative image-wrapper']} style={`--theme-hue: ${siteConfig.themeColor.hue}`}> + <!-- 加载条 --> + <div class="loading-bar absolute top-1/2 left-1/2 transform -translate-x-1/2 -translate-y-1/2 w-32 h-1 bg-gray-200 dark:bg-gray-700 z-10 rounded-full overflow-hidden"> + <div class="loading-progress h-full w-8 bg-[oklch(0.70_0.14_var(--theme-hue))] animate-loading-progress rounded-full"></div> + </div> + + <!-- 图片内容 --> + {isLocal && img && <Image src={img} alt={alt || ""} class={`${imageClass} image-content opacity-0 transition-opacity duration-500`} style={imageStyle} onload="this.style.opacity='1'; this.parentElement.querySelector('.loading-bar').style.opacity='0';"/>} + {!isLocal && ( + imageFallbackConfig.enable && src.includes(imageFallbackConfig.originalDomain) ? + <img src={isPublic ? url(src) : src} alt={alt || ""} class={`${imageClass} image-content opacity-0 transition-opacity duration-500`} style={imageStyle} onload="this.style.opacity='1'; this.parentElement.querySelector('.loading-bar').style.opacity='0';" onerror={`this.onerror=null; this.src='${(isPublic ? url(src) : src).replace(imageFallbackConfig.originalDomain, imageFallbackConfig.fallbackDomain)}';`}/> : + <img src={isPublic ? url(src) : src} alt={alt || ""} class={`${imageClass} image-content opacity-0 transition-opacity duration-500`} style={imageStyle} onload="this.style.opacity='1'; this.parentElement.querySelector('.loading-bar').style.opacity='0';"/> + )} +</div> + +<style> + .loading-bar { + transition: opacity 0.5s ease-out; + } + + @keyframes loading-progress { + 0% { + transform: translateX(-100%); + } + 100% { + transform: translateX(400%); + } + } + + .animate-loading-progress { + animation: loading-progress 1.5s ease-in-out infinite; + } + + .image-content { + transition: transform 0.3s ease-out, opacity 0.5s ease-out; + } + + .image-wrapper:hover .image-content { + transform: scale(1.05); + } +</style> + diff --git a/src/components/misc/License.astro b/src/components/misc/License.astro new file mode 100644 index 0000000..6f826fa --- /dev/null +++ b/src/components/misc/License.astro @@ -0,0 +1,60 @@ +--- +import { Icon } from "astro-icon/components"; +import { licenseConfig, profileConfig } from "../../config"; + +import { formatDateToYYYYMMDD } from "../../utils/date-utils"; + +interface Props { + title: string; + slug: string; + pubDate: Date; + class: string; +} + +const { title, pubDate } = Astro.props; +const className = Astro.props.class; +const profileConf = profileConfig; +const licenseConf = licenseConfig; +const postUrl = decodeURIComponent(Astro.url.toString()); +--- +<div class=`relative transition overflow-hidden bg-[var(--license-block-bg)] py-5 px-6 ${className}`> + <div class="transition font-bold text-black/75 dark:text-white/75"> + {title} + </div> + + <a id="current-link" style="display:none;" class="link text-[var(--primary)]"></a> + + <script> + const currentLink = document.getElementById('current-link'); + + if (currentLink) { + console.log(`[license] old url: ${window.location.href}`); + const url = new URL(window.location.href); + url.search = ''; + const address = url.toString(); + console.log(`[license] new url: ${address}`); + + currentLink.textContent = address; // 显示文本 = 当前 URL + currentLink.href = address; // 点击跳转 = 当前 URL + currentLink.style.display = 'inline'; // 显示出来 + } + </script> + + <div class="flex gap-6 mt-2"> + <div> + <div class="transition text-black/30 dark:text-white/30 text-sm">作者</div> + <div class="transition text-black/75 dark:text-white/75 line-clamp-2">{profileConf.name}</div> + </div> + <div> + <div class="transition text-black/30 dark:text-white/30 text-sm">发布于</div> + <div class="transition text-black/75 dark:text-white/75 line-clamp-2">{formatDateToYYYYMMDD(pubDate)}</div> + </div> + <div> + <div class="transition text-black/30 dark:text-white/30 text-sm">许可协议</div> + <a href={licenseConf.url} target="_blank" class="link text-[var(--primary)] line-clamp-2">{licenseConf.name}</a> + </div> + </div> + <Icon name="fa6-brands:creative-commons" class="transition text-[15rem] absolute pointer-events-none right-6 top-1/2 -translate-y-1/2 text-black/5 dark:text-white/5"></Icon> +</div> + + diff --git a/src/components/misc/Markdown.astro b/src/components/misc/Markdown.astro new file mode 100644 index 0000000..10a415a --- /dev/null +++ b/src/components/misc/Markdown.astro @@ -0,0 +1,42 @@ +--- +import "@fontsource-variable/jetbrains-mono"; +import "@fontsource-variable/jetbrains-mono/wght-italic.css"; + +interface Props { + class: string; +} +const className = Astro.props.class; +--- +<div data-pagefind-body class=`prose dark:prose-invert prose-base !max-w-none custom-md ${className}`> + <!--<div class="prose dark:prose-invert max-w-none custom-md">--> + <!--<div class="max-w-none custom-md">--> + <slot/> +</div> + +<script> +document.addEventListener("click", function (e: MouseEvent) { + const target = e.target as Element | null; + if (target && target.classList.contains("copy-btn")) { + const preEle = target.closest("pre"); + const codeEle = preEle?.querySelector("code"); + const code = Array.from(codeEle?.querySelectorAll(".code:not(summary *)") ?? []) + .map(el => el.textContent) + .map(t => t === "\n" ? "" : t) + .join("\n"); + navigator.clipboard.writeText(code); + + const timeoutId = target.getAttribute("data-timeout-id"); + if (timeoutId) { + clearTimeout(parseInt(timeoutId)); + } + + target.classList.add("success"); + + // 设置新的timeout并保存ID到按钮的自定义属性中 + const newTimeoutId = setTimeout(() => { + target.classList.remove("success"); + }, 1000); + target.setAttribute("data-timeout-id", newTimeoutId.toString()); + } +}); +</script> diff --git a/src/components/widget/CategoryDrawer.astro b/src/components/widget/CategoryDrawer.astro new file mode 100644 index 0000000..01dd732 --- /dev/null +++ b/src/components/widget/CategoryDrawer.astro @@ -0,0 +1,61 @@ +--- +import type { CategoryNode } from "@/types/config"; +import { Icon } from "astro-icon/components"; + +interface Props { + category: CategoryNode; + depth?: number; +} + +const { category, depth = 0 } = Astro.props; +const hasChildren = category.children && category.children.length > 0; +--- + +<div class="category-drawer relative"> + <div + class="flex items-center justify-between px-3 py-2 rounded hover:bg-[var(--btn-plain-bg-hover)] transition-all group w-full" + > + <div class="flex items-center gap-2 overflow-hidden w-full"> + { + hasChildren ? ( + <button + class="drawer-toggle text-50 hover:text-[var(--primary)] transition-colors cursor-pointer shrink-0 flex items-center justify-center h-6 w-6 rounded-md hover:bg-black/5 dark:hover:bg-white/10" + aria-label="Toggle children" + > + <Icon + name="material-symbols:chevron-right-rounded" + class="text-xl transition-transform duration-200" + /> + </button> + ) : ( + <span class="w-6 shrink-0" /> + ) + } + + <a + href={category.url} + class="text-75 group-hover:text-[var(--primary)] transition-all truncate flex-1" + > + {category.name} + </a> + </div> + + <span + class="text-sm text-50 bg-black/5 dark:bg-white/10 px-2 py-0.5 rounded shrink-0 ml-2" + > + {category.count} + </span> + </div> + + { + hasChildren && ( + <div class="drawer-content grid grid-rows-[0fr] transition-[grid-template-rows] duration-200 ease-out pl-3 ml-3 border-l border-[var(--line-divider)]"> + <div class="overflow-hidden"> + {category.children.map((child) => ( + <Astro.self category={child} depth={depth + 1} /> + ))} + </div> + </div> + ) + } +</div> diff --git a/src/components/widget/CategoryList.astro b/src/components/widget/CategoryList.astro new file mode 100644 index 0000000..9086463 --- /dev/null +++ b/src/components/widget/CategoryList.astro @@ -0,0 +1,85 @@ +--- +import { getSortedPosts } from "@utils/content-utils"; +import { buildCategoryTree } from "@utils/category-utils"; +import { Icon } from "astro-icon/components"; +import CategoryDrawer from "./CategoryDrawer.astro"; + +const allPosts = await getSortedPosts(); +const categoryTree = await buildCategoryTree(allPosts); + +function getTotalCount(cat: any): number { + let count = cat.count; + if (cat.children) { + for (const child of cat.children) { + count += getTotalCount(child); + } + } + return count; +} + +// Get only top-level categories, sort by total post count (including children), max 15 +const topCategories = Object.values(categoryTree) + .filter((cat) => cat.path.length === 1) + .map((cat) => ({ ...cat, totalCount: getTotalCount(cat) })) + .sort((a, b) => b.totalCount - a.totalCount) + .slice(0, 15); +--- + +<div class="card-base px-6 py-5"> + <div class="flex items-center gap-2 mb-4"> + <Icon + name="material-symbols:folder-outline-rounded" + class="text-[var(--primary)]" + /> + <h2 class="text-lg font-bold text-90">分类</h2> + </div> + + <div class="flex flex-col gap-0"> + {topCategories.map((category) => <CategoryDrawer category={category} />)} + </div> + + <a + href="/category/" + class="mt-4 text-center text-sm text-[var(--primary)] hover:underline block" + > + 查看全部分类 → + </a> +</div> + +<script> + function setupCategoryDrawer() { + const toggles = document.querySelectorAll(".drawer-toggle"); + toggles.forEach((toggle) => { + if (toggle.hasAttribute("data-bound")) return; + toggle.setAttribute("data-bound", "true"); + + toggle.addEventListener("click", (e) => { + e.preventDefault(); + e.stopPropagation(); + + const drawer = toggle.closest(".category-drawer"); + const content = drawer.querySelector(".drawer-content"); + const icon = toggle.querySelector("svg"); + + if (!content || !icon) return; + + const isExpanded = content.style.gridTemplateRows === "1fr"; + + if (isExpanded) { + content.style.gridTemplateRows = "0fr"; + icon.style.transform = "rotate(0deg)"; + } else { + content.style.gridTemplateRows = "1fr"; + icon.style.transform = "rotate(90deg)"; + } + }); + }); + } + + setupCategoryDrawer(); + + // Re-run on Swup navigation + if (window.swup) { + window.swup.hooks.on("content:replace", setupCategoryDrawer); + } +</script> diff --git a/src/components/widget/DisplaySettings.svelte b/src/components/widget/DisplaySettings.svelte new file mode 100644 index 0000000..5db84f6 --- /dev/null +++ b/src/components/widget/DisplaySettings.svelte @@ -0,0 +1,331 @@ +<script lang="ts"> +import { onMount } from "svelte"; +import Icon from "@iconify/svelte"; +import { + getDefaultHue, + getHue, + setHue, + getStoredTheme, + setTheme, + getRainbowMode, + setRainbowMode, + getRainbowSpeed, + setRainbowSpeed, + getBgBlur, + setBgBlur, + setBgHueRotate, + getHideBg, + setHideBg, + getDevMode, + setDevMode, + getDevServer, + setDevServer, +} from "@utils/setting-utils"; +import { AUTO_MODE, DARK_MODE, LIGHT_MODE } from "@constants/constants"; + +let hue = getHue(); +let theme = getStoredTheme(); +let isRainbowMode = getRainbowMode(); +let rainbowSpeed = getRainbowSpeed(); +let bgBlur = getBgBlur(); +let hideBg = getHideBg(); +let isDevMode = getDevMode(); +let devServer = getDevServer(); +let animationId: number; +let lastUpdate = 0; +let rainbowHue = 0; // Independent hue for background rotation + +const defaultHue = getDefaultHue(); + +function resetHue() { + hue = getDefaultHue(); +} + +$: if ((hue || hue === 0) && !isRainbowMode) { + setHue(hue); +} + +$: { + setBgBlur(bgBlur); +} + +function switchTheme(newTheme: string) { + theme = newTheme; + setTheme(newTheme); +} + +function updateRainbow() { + if (!isRainbowMode) return; + + hue = (hue + rainbowSpeed * 0.05) % 360; + setHue(hue, false); + + animationId = requestAnimationFrame(updateRainbow); +} + +function toggleRainbow() { + isRainbowMode = !isRainbowMode; + setRainbowMode(isRainbowMode); + + if (isRainbowMode) { + lastUpdate = performance.now(); + rainbowHue = 0; // Reset rotation start + animationId = requestAnimationFrame(updateRainbow); + } else { + cancelAnimationFrame(animationId); + // Reset background rotation to 0 when stopped + setBgHueRotate(0); + } +} + +function toggleHideBg() { + hideBg = !hideBg; + setHideBg(hideBg); +} + +function toggleDevMode() { + isDevMode = !isDevMode; + setDevMode(isDevMode); +} + +function onDevServerChange() { + setDevServer(devServer); +} + +function onSpeedChange() { + setRainbowSpeed(rainbowSpeed); +} + +onMount(() => { + if (isRainbowMode) { + updateRainbow(); + } + return () => { + if (animationId) cancelAnimationFrame(animationId); + } +}); +</script> + +<div id="display-setting" class="float-panel float-panel-closed absolute transition-all w-80 right-4 px-4 py-4"> + <div class="flex flex-row gap-2 mb-3 items-center justify-between"> + <div class="flex gap-2 font-bold text-lg text-neutral-900 dark:text-neutral-100 transition relative ml-3 + before:w-1 before:h-4 before:rounded-md before:bg-[var(--primary)] + before:absolute before:-left-3 before:top-[0.33rem]" + > + 主题模式 + </div> + <div class="flex gap-1"> + <button aria-label="Light Mode" + class="w-10 h-7 rounded-md transition flex items-center justify-center active:scale-90 + {theme === LIGHT_MODE ? 'bg-[var(--primary)] text-white' : 'bg-[var(--btn-regular-bg)] text-[var(--btn-content)] hover:bg-[var(--btn-regular-bg-hover)]'}" + on:click={() => switchTheme(LIGHT_MODE)} + > + <Icon icon="material-symbols:wb-sunny-rounded" class="text-[1.1rem]"></Icon> + </button> + <button aria-label="Dark Mode" + class="w-10 h-7 rounded-md transition flex items-center justify-center active:scale-90 + {theme === DARK_MODE ? 'bg-[var(--primary)] text-white' : 'bg-[var(--btn-regular-bg)] text-[var(--btn-content)] hover:bg-[var(--btn-regular-bg-hover)]'}" + on:click={() => switchTheme(DARK_MODE)} + > + <Icon icon="material-symbols:dark-mode-rounded" class="text-[1.1rem]"></Icon> + </button> + <button aria-label="Auto Mode" + class="w-10 h-7 rounded-md transition flex items-center justify-center active:scale-90 + {theme === AUTO_MODE ? 'bg-[var(--primary)] text-white' : 'bg-[var(--btn-regular-bg)] text-[var(--btn-content)] hover:bg-[var(--btn-regular-bg-hover)]'}" + on:click={() => switchTheme(AUTO_MODE)} + > + <Icon icon="material-symbols:hdr-auto-rounded" class="text-[1.1rem]"></Icon> + </button> + </div> + </div> + + <div class="flex flex-row gap-2 mb-3 items-center justify-between"> + <div class="flex gap-2 font-bold text-lg text-neutral-900 dark:text-neutral-100 transition relative ml-3 + before:w-1 before:h-4 before:rounded-md before:bg-[var(--primary)] + before:absolute before:-left-3 before:top-[0.33rem]" + > + 主题色彩 + <button aria-label="Reset to Default" class="btn-regular w-7 h-7 rounded-md active:scale-90" + class:opacity-0={hue === defaultHue} class:pointer-events-none={hue === defaultHue} on:click={resetHue}> + <div class="text-[var(--btn-content)]"> + <Icon icon="fa6-solid:arrow-rotate-left" class="text-[0.875rem]"></Icon> + </div> + </button> + </div> + <div class="flex gap-1"> + <input aria-label="Hue Value" id="hueValue" type="number" min="0" max="360" value={Math.round(hue)} on:input={(e) => hue = e.currentTarget.valueAsNumber} disabled={isRainbowMode} + class="transition bg-[var(--btn-regular-bg)] w-12 h-7 rounded-md text-center font-bold text-sm text-[var(--btn-content)] outline-none" + /> + </div> + </div> + <div class="w-full h-6 px-1 bg-[oklch(0.80_0.10_0)] dark:bg-[oklch(0.70_0.10_0)] rounded select-none mb-3"> + <input aria-label="主题色彩" type="range" min="0" max="360" bind:value={hue} disabled={isRainbowMode} + class="slider" id="colorSlider" step="1" style="width: 100%"> + </div> + + <div class="flex flex-row gap-2 mb-3 items-center justify-between"> + <div class="flex gap-2 font-bold text-lg text-neutral-900 dark:text-neutral-100 transition relative ml-3 + before:w-1 before:h-4 before:rounded-md before:bg-[var(--primary)] + before:absolute before:-left-3 before:top-[0.33rem]" + > + 禁用背景 + </div> + <input type="checkbox" class="toggle-switch" checked={hideBg} on:change={toggleHideBg} /> + </div> + + <div class="flex flex-row gap-2 mb-3 items-center justify-between"> + <div class="flex gap-2 font-bold text-lg text-neutral-900 dark:text-neutral-100 transition relative ml-3 + before:w-1 before:h-4 before:rounded-md before:bg-[var(--primary)] + before:absolute before:-left-3 before:top-[0.33rem]" + > + 彩虹模式 + </div> + <input type="checkbox" class="toggle-switch" checked={isRainbowMode} on:change={toggleRainbow} /> + </div> + + {#if isRainbowMode} + <div class="flex flex-row gap-2 mb-3 items-center justify-between transition-all" > + <div class="flex gap-2 font-bold text-lg text-neutral-900 dark:text-neutral-100 transition relative ml-3 + before:w-1 before:h-4 before:rounded-md before:bg-[var(--primary)] + before:absolute before:-left-3 before:top-[0.33rem]" + > + 变换速率 + </div> + <div class="flex gap-1"> + <div class="transition bg-[var(--btn-regular-bg)] w-10 h-7 rounded-md flex justify-center + font-bold text-sm items-center text-[var(--btn-content)]"> + {rainbowSpeed} + </div> + </div> + </div> + <div class="w-full h-6 px-1 bg-[var(--btn-regular-bg)] rounded select-none"> + <input aria-label="变换速率" type="range" min="1" max="100" bind:value={rainbowSpeed} on:change={onSpeedChange} + class="slider" step="1" style="width: 100%"> + </div> + {/if} + + <div class="flex flex-row gap-2 mb-3 mt-3 items-center justify-between"> + <div class="flex gap-2 font-bold text-lg text-neutral-900 dark:text-neutral-100 transition relative ml-3 + before:w-1 before:h-4 before:rounded-md before:bg-[var(--primary)] + before:absolute before:-left-3 before:top-[0.33rem]" + > + 背景模糊 + </div> + <div class="flex gap-1"> + <div class="transition bg-[var(--btn-regular-bg)] w-10 h-7 rounded-md flex justify-center + font-bold text-sm items-center text-[var(--btn-content)]"> + {bgBlur}px + </div> + </div> + </div> + <div class="w-full h-6 px-1 bg-[var(--btn-regular-bg)] rounded select-none"> + <input aria-label="背景模糊" type="range" min="0" max="20" bind:value={bgBlur} + class="slider" step="1" style="width: 100%"> + </div> + + <div class="flex flex-row gap-2 mb-3 mt-3 items-center justify-between"> + <div class="flex gap-2 font-bold text-lg text-neutral-900 dark:text-neutral-100 transition relative ml-3 + before:w-1 before:h-4 before:rounded-md before:bg-[var(--primary)] + before:absolute before:-left-3 before:top-[0.33rem]" + > + 开发模式 + </div> + <input type="checkbox" class="toggle-switch" checked={isDevMode} on:change={toggleDevMode} /> + </div> + + {#if isDevMode} + <div class="flex flex-row gap-2 mb-3 items-center justify-between transition-all" > + <div class="flex gap-2 font-bold text-lg text-neutral-900 dark:text-neutral-100 transition relative ml-3 + before:w-1 before:h-4 before:rounded-md before:bg-[var(--primary)] + before:absolute before:-left-3 before:top-[0.33rem]" + > + Server + </div> + <div class="flex gap-1"> + <input aria-label="Server Value" type="text" bind:value={devServer} on:input={onDevServerChange} + class="transition bg-[var(--btn-regular-bg)] w-32 h-7 rounded-md text-center font-bold text-sm text-[var(--btn-content)] outline-none" + /> + </div> + </div> + {/if} +</div> + + +<style lang="stylus"> + #display-setting + input[type="number"] + -moz-appearance textfield + &::-webkit-inner-spin-button + &::-webkit-outer-spin-button + -webkit-appearance none + margin 0 + + input[type="range"] + -webkit-appearance none + height 1.5rem + background-image var(--color-selection-bar) + transition background-image 0.15s ease-in-out + + /* Input Thumb */ + &::-webkit-slider-thumb + -webkit-appearance none + height 1rem + width 0.5rem + border-radius 0.125rem + background rgba(255, 255, 255, 0.7) + box-shadow none + &:hover + background rgba(255, 255, 255, 0.8) + &:active + background rgba(255, 255, 255, 0.6) + + &::-moz-range-thumb + -webkit-appearance none + height 1rem + width 0.5rem + border-radius 0.125rem + border-width 0 + background rgba(255, 255, 255, 0.7) + box-shadow none + &:hover + background rgba(255, 255, 255, 0.8) + &:active + background rgba(255, 255, 255, 0.6) + + &::-ms-thumb + -webkit-appearance none + height 1rem + width 0.5rem + border-radius 0.125rem + background rgba(255, 255, 255, 0.7) + box-shadow none + &:hover + background rgba(255, 255, 255, 0.8) + &:active + background rgba(255, 255, 255, 0.6) + + .toggle-switch + appearance none + width 3rem + height 1.5rem + background var(--btn-regular-bg) + border-radius 999px + position relative + cursor pointer + transition background 0.3s + &::after + content '' + position absolute + top 0.25rem + left 0.25rem + width 1rem + height 1rem + background var(--btn-content) + border-radius 50% + transition transform 0.3s + &:checked + background var(--primary) + &::after + transform translateX(1.5rem) + background white +</style> diff --git a/src/components/widget/NavMenuPanel.astro b/src/components/widget/NavMenuPanel.astro new file mode 100644 index 0000000..cf141e3 --- /dev/null +++ b/src/components/widget/NavMenuPanel.astro @@ -0,0 +1,32 @@ +--- +import { Icon } from "astro-icon/components"; +import { type NavBarLink } from "../../types/config"; +import { url } from "../../utils/url-utils"; + +interface Props { + links: NavBarLink[]; +} + +const links = Astro.props.links; +--- +<div id="nav-menu-panel" class:list={["float-panel float-panel-closed absolute transition-all fixed right-4 px-2 py-2"]}> + {links.map((link) => ( + <a href={link.external ? link.url : url(link.url)} class="group flex justify-between items-center py-2 pl-3 pr-1 rounded-lg gap-8 + hover:bg-[var(--btn-plain-bg-hover)] active:bg-[var(--btn-plain-bg-active)] transition + " + target={link.external ? "_blank" : null} + > + <div class="transition text-black/75 dark:text-white/75 font-bold group-hover:text-[var(--primary)] group-active:text-[var(--primary)]"> + {link.name} + </div> + {!link.external && <Icon name="material-symbols:chevron-right-rounded" + class="transition text-[1.25rem] text-[var(--primary)]" + > + </Icon>} + {link.external && <Icon name="fa6-solid:arrow-up-right-from-square" + class="transition text-[0.75rem] text-black/25 dark:text-white/25 -translate-x-1" + > + </Icon>} + </a> + ))} +</div> diff --git a/src/components/widget/Profile.astro b/src/components/widget/Profile.astro new file mode 100644 index 0000000..9abedf6 --- /dev/null +++ b/src/components/widget/Profile.astro @@ -0,0 +1,93 @@ +--- +import { Icon } from "astro-icon/components"; +import { profileConfig, umamiConfig, siteConfig } from "../../config"; + +const config = profileConfig; +--- +<div class="card-base p-3 border border-black/10 dark:border-white/10"> + <div class="relative mx-auto mt-1 lg:mx-0 lg:mt-0 mb-3 max-w-[12rem] lg:max-w-none rounded-xl overflow-hidden" style={`--theme-hue: ${siteConfig.themeColor.hue}`}> + <div class="loading-bar absolute top-1/2 left-1/2 transform -translate-x-1/2 -translate-y-1/2 w-32 h-1 bg-gray-200 dark:bg-gray-700 z-10 rounded-full overflow-hidden"> + <div class="loading-progress h-full w-8 bg-[oklch(0.70_0.14_var(--theme-hue))] animate-loading-progress rounded-full"></div> + </div> + <img src={config.avatar} alt="Profile Image of the Author" class="w-full h-full object-cover opacity-0 transition-opacity duration-500" onload="this.style.opacity='1'; this.parentElement.querySelector('.loading-bar').style.opacity='0';"/> + </div> + <div class="px-2"> + <div class="font-bold text-xl text-center mb-1 dark:text-neutral-50 transition">{config.name}</div> + <div class="h-1 w-5 bg-[var(--primary)] mx-auto rounded-full mb-2 transition"></div> + <div class="text-center text-neutral-400 mb-2.5 transition">{config.bio}</div> + <div class="flex flex-wrap gap-2 justify-center mb-1"> + {config.links.length > 1 && config.links.map(item => + <a rel="me" aria-label={item.name} href={item.url} target="_blank" class="btn-regular rounded-lg h-10 w-10 active:scale-90"> + <Icon name={item.icon} class="text-[1.5rem]"></Icon> + </a> + )} + {config.links.length == 1 && <a rel="me" aria-label={config.links[0].name} href={config.links[0].url} target="_blank" + class="btn-regular rounded-lg h-10 gap-2 px-3 font-bold active:scale-95"> + <Icon name={config.links[0].icon} class="text-[1.5rem]"></Icon> + {config.links[0].name} + </a>} + </div> + + <!-- 全站访问量统计 --> + <div class="grid grid-cols-2 mt-3 pt-3 border-t border-neutral-200 dark:border-neutral-700"> + <div class="text-center"> + <div class="text-xs text-neutral-500 mb-1 flex items-center justify-center gap-1"> + <Icon name="material-symbols:visibility-outline" class="text-base"></Icon> + <span class="text-xs">访问量</span> + </div> + <div id="site-views" class="font-bold text-lg text-neutral-700 dark:text-neutral-300">-</div> + </div> + <div class="text-center border-l border-neutral-200 dark:border-neutral-700"> + <div class="text-xs text-neutral-500 mb-1 flex items-center justify-center gap-1"> + <Icon name="material-symbols:person" class="text-base"></Icon> + <span class="text-xs">访客数</span> + </div> + <div id="site-visitors" class="font-bold text-lg text-neutral-700 dark:text-neutral-300">-</div> + </div> + </div> + </div> +</div> + +<style> + .loading-bar { + transition: opacity 0.5s ease-out; + } + + @keyframes loading-progress { + 0% { + transform: translateX(-100%); + } + 100% { + transform: translateX(400%); + } + } + + .animate-loading-progress { + animation: loading-progress 1.5s ease-in-out infinite; + } +</style> + +<script define:vars={{ umamiConfig}}> + // 获取全站访问量统计 + async function loadSiteStats() { + if (!umamiConfig.enable) return; + try { + const statsData = await fetchUmamiStats(umamiConfig.baseUrl, umamiConfig.shareId, { + timezone: umamiConfig.timezone + }); + + const pageviews = statsData.pageviews || 0; + const visitors = statsData.visitors || 0; + + const viewsElement = document.getElementById('site-views'); + const visitorsElement = document.getElementById('site-visitors'); + if (viewsElement) viewsElement.textContent = pageviews; + if (visitorsElement) visitorsElement.textContent = visitors; + } catch (error) { + console.error('获取全站统计失败:', error); + } + } + + // 页面加载完成后获取统计数据 + document.addEventListener('DOMContentLoaded', loadSiteStats); +</script> \ No newline at end of file diff --git a/src/components/widget/SideBar.astro b/src/components/widget/SideBar.astro new file mode 100644 index 0000000..3b96e46 --- /dev/null +++ b/src/components/widget/SideBar.astro @@ -0,0 +1,24 @@ +--- +import type { MarkdownHeading } from "astro"; + +import Profile from "./Profile.astro"; +import CategoryList from "./CategoryList.astro"; + +interface Props { + class?: string; + headings?: MarkdownHeading[]; +} + +const className = Astro.props.class; +--- +<div id="sidebar" class:list={[className, "w-full"]}> + <div class="flex flex-col w-full gap-4 mb-4"> + <Profile></Profile> + <CategoryList /> + </div> + <div id="sidebar-sticky" class="transition-all duration-700 flex flex-col w-full gap-4 top-4 sticky top-4"> + </div> +</div> + + + diff --git a/src/components/widget/TOC.astro b/src/components/widget/TOC.astro new file mode 100644 index 0000000..0a6625a --- /dev/null +++ b/src/components/widget/TOC.astro @@ -0,0 +1,268 @@ +--- +import type { MarkdownHeading } from "astro"; +import { siteConfig } from "../../config"; + +interface Props { + class?: string; + headings: MarkdownHeading[]; +} + +let { headings = [] } = Astro.props; + +let minDepth = 10; +for (const heading of headings) { + minDepth = Math.min(minDepth, heading.depth); +} + +const className = Astro.props.class; + +const isPostsRoute = Astro.url.pathname.startsWith("/posts/"); + +const removeTailingHash = (text: string) => { + let lastIndexOfHash = text.lastIndexOf("#"); + if (lastIndexOfHash !== text.length - 1) { + return text; + } + + return text.substring(0, lastIndexOfHash); +}; + +let heading1Count = 1; + +const maxLevel = siteConfig.toc.depth; +--- +{isPostsRoute && +<table-of-contents class:list={[className, "group"]}> + {headings.filter((heading) => heading.depth < minDepth + maxLevel).map((heading) => + <a href={`#${heading.slug}`} class="px-2 flex gap-2 relative transition w-full min-h-9 rounded-xl + hover:bg-[var(--toc-btn-hover)] active:bg-[var(--toc-btn-active)] py-2 + "> + <div class:list={["transition w-5 h-5 shrink-0 rounded-lg text-xs flex items-center justify-center font-bold", + { + "bg-[var(--toc-badge-bg)] text-[var(--btn-content)]": heading.depth == minDepth, + "ml-4": heading.depth == minDepth + 1, + "ml-8": heading.depth == minDepth + 2, + } + ]} + > + {heading.depth == minDepth && heading1Count++} + {heading.depth == minDepth + 1 && <div class="transition w-2 h-2 rounded-[0.1875rem] bg-[var(--toc-badge-bg)]"></div>} + {heading.depth == minDepth + 2 && <div class="transition w-1.5 h-1.5 rounded-sm bg-black/5 dark:bg-white/10"></div>} + </div> + <div class:list={["transition text-sm", { + "text-50": heading.depth == minDepth || heading.depth == minDepth + 1, + "text-30": heading.depth == minDepth + 2, + }]}>{removeTailingHash(heading.text)}</div> + </a> + )} + <div id="active-indicator" class:list={[{'hidden': headings.length == 0}, "-z-10 absolute bg-[var(--toc-btn-hover)] left-0 right-0 rounded-xl transition-all " + + "group-hover:bg-transparent border-2 border-[var(--toc-btn-hover)] group-hover:border-[var(--toc-btn-active)] border-dashed"]}></div> +</table-of-contents>} + +<script> +class TableOfContents extends HTMLElement { + tocEl: HTMLElement | null = null; + visibleClass = "visible"; + observer: IntersectionObserver; + anchorNavTarget: HTMLElement | null = null; + headingIdxMap = new Map<string, number>(); + headings: HTMLElement[] = []; + sections: HTMLElement[] = []; + tocEntries: HTMLAnchorElement[] = []; + active: boolean[] = []; + activeIndicator: HTMLElement | null = null; + + constructor() { + super(); + this.observer = new IntersectionObserver( + this.markVisibleSection, { threshold: 0 } + ); + }; + + markVisibleSection = (entries: IntersectionObserverEntry[]) => { + entries.forEach((entry) => { + const id = entry.target.children[0]?.getAttribute("id"); + const idx = id ? this.headingIdxMap.get(id) : undefined; + if (idx != undefined) + this.active[idx] = entry.isIntersecting; + + if (entry.isIntersecting && this.anchorNavTarget == entry.target.firstChild) + this.anchorNavTarget = null; + }); + + if (!this.active.includes(true)) { + // 当没有任何标题在视窗中时,隐藏 active-indicator + this.activeIndicator?.setAttribute("style", "display: none;"); + this.fallback(); + return; + } + this.update(); + }; + + toggleActiveHeading = () => { + let i = this.active.length - 1; + let min = this.active.length - 1, max = 0; + while (i >= 0 && !this.active[i]) { + this.tocEntries[i].classList.remove(this.visibleClass); + i--; + } + while (i >= 0 && this.active[i]) { + this.tocEntries[i].classList.add(this.visibleClass); + min = Math.min(min, i); + max = Math.max(max, i); + i--; + } + while (i >= 0) { + this.tocEntries[i].classList.remove(this.visibleClass); + i--; + } + let parentOffset = this.tocEl?.getBoundingClientRect().top || 0; + let scrollOffset = this.tocEl?.scrollTop || 0; + let top = this.tocEntries[min].getBoundingClientRect().top - parentOffset + scrollOffset; + let bottom = this.tocEntries[max].getBoundingClientRect().bottom - parentOffset + scrollOffset; + this.activeIndicator?.setAttribute("style", `display: block; top: ${top}px; height: ${bottom - top}px`); + }; + + scrollToActiveHeading = () => { + // If the TOC widget can accommodate both the topmost + // and bottommost items, scroll to the topmost item. + // Otherwise, scroll to the bottommost one. + + if (this.anchorNavTarget || !this.tocEl) return; + const activeHeading = + document.querySelectorAll<HTMLDivElement>(`#toc .${this.visibleClass}`); + if (!activeHeading.length) return; + + const topmost = activeHeading[0]; + const bottommost = activeHeading[activeHeading.length - 1]; + const tocHeight = this.tocEl.clientHeight; + + let top; + if (bottommost.getBoundingClientRect().bottom - + topmost.getBoundingClientRect().top < 0.9 * tocHeight) + top = topmost.offsetTop - 32; + else + top = bottommost.offsetTop - tocHeight * 0.8; + + this.tocEl.scrollTo({ + top, + left: 0, + behavior: "smooth", + }); + }; + + update = () => { + requestAnimationFrame(() => { + this.toggleActiveHeading(); + // requestAnimationFrame(() => { + this.scrollToActiveHeading(); + // }); + }); + }; + + fallback = () => { + if (!this.sections.length) return; + + for (let i = 0; i < this.sections.length; i++) { + let offsetTop = this.sections[i].getBoundingClientRect().top; + let offsetBottom = this.sections[i].getBoundingClientRect().bottom; + + if (this.isInRange(offsetTop, 0, window.innerHeight) + || this.isInRange(offsetBottom, 0, window.innerHeight) + || (offsetTop < 0 && offsetBottom > window.innerHeight)) { + this.markActiveHeading(i); + } + else if (offsetTop > window.innerHeight) break; + } + }; + + markActiveHeading = (idx: number)=> { + this.active[idx] = true; + }; + + handleAnchorClick = (event: Event) => { + const anchor = event + .composedPath() + .find((element) => element instanceof HTMLAnchorElement); + + if (anchor) { + const id = decodeURIComponent(anchor.hash?.substring(1)); + const idx = this.headingIdxMap.get(id); + if (idx !== undefined) { + this.anchorNavTarget = this.headings[idx]; + } else { + this.anchorNavTarget = null; + } + } + }; + + isInRange(value: number, min: number, max: number) { + return min < value && value < max; + }; + + connectedCallback() { + // wait for the onload animation to finish, which makes the `getBoundingClientRect` return correct values + const element = document.querySelector('.prose'); + if (element) { + element.addEventListener('animationend', () => { + this.init(); + }, { once: true }); + } else { + console.debug('Animation element not found'); + } + }; + + init() { + this.tocEl = document.getElementById( + "toc-inner-wrapper" + ); + + if (!this.tocEl) return; + + this.tocEl.addEventListener("click", this.handleAnchorClick, { + capture: true, + }); + + this.activeIndicator = document.getElementById("active-indicator"); + + this.tocEntries = Array.from( + document.querySelectorAll<HTMLAnchorElement>("#toc a[href^='#']") + ); + + if (this.tocEntries.length === 0) return; + + this.sections = new Array(this.tocEntries.length); + this.headings = new Array(this.tocEntries.length); + for (let i = 0; i < this.tocEntries.length; i++) { + const id = decodeURIComponent(this.tocEntries[i].hash?.substring(1)); + const heading = document.getElementById(id); + const section = heading?.parentElement; + if (heading instanceof HTMLElement && section instanceof HTMLElement) { + this.headings[i] = heading; + this.sections[i] = section; + this.headingIdxMap.set(id, i); + } + } + this.active = new Array(this.tocEntries.length).fill(false); + + this.sections.forEach((section) => + this.observer.observe(section) + ); + + this.fallback(); + this.update(); + }; + + disconnectedCallback() { + this.sections.forEach((section) => + this.observer.unobserve(section) + ); + this.observer.disconnect(); + this.tocEl?.removeEventListener("click", this.handleAnchorClick); + }; +} + +if (!customElements.get("table-of-contents")) { + customElements.define("table-of-contents", TableOfContents); +} +</script> \ No newline at end of file diff --git a/src/config.ts b/src/config.ts new file mode 100644 index 0000000..ea62c84 --- /dev/null +++ b/src/config.ts @@ -0,0 +1,158 @@ +import type { + ExpressiveCodeConfig, + GitHubEditConfig, + ImageFallbackConfig, + LicenseConfig, + NavBarConfig, + ProfileConfig, + SiteConfig, + UmamiConfig, +} from "./types/config"; +import { LinkPreset } from "./types/config"; + +export const siteConfig: SiteConfig = { + title: "MeowRain的技术博客", + subtitle: "技术分享与实践", + description: + "分享软件开发、编程语言、框架和工具的技术博客,涵盖实用教程、最佳实践和行业动态,帮助开发者提升技能。", + + keywords: [], + lang: "zh_CN", // 'en', 'zh_CN', 'zh_TW', 'ja', 'ko', 'es', 'th' + themeColor: { + hue: 361, // Default hue for the theme color, from 0 to 360. e.g. red: 0, teal: 200, cyan: 250, pink: 345 + fixed: false, // Hide the theme color picker for visitors + forceDarkMode: false, // Force dark mode and hide theme switcher + }, + banner: { + enable: false, + src: "/xinghui.avif", // Relative to the /src directory. Relative to the /public directory if it starts with '/' + + position: "center", // Equivalent to object-position, only supports 'top', 'center', 'bottom'. 'center' by default + credit: { + enable: true, // Display the credit text of the banner image + text: "Pixiv @chokei", // Credit text to be displayed + + url: "https://www.pixiv.net/artworks/122782209", // (Optional) URL link to the original artwork or artist's page + }, + }, + background: { + enable: true, // Enable background image + src: "", // Background image URL (supports HTTPS) + position: "center", // Background position: 'top', 'center', 'bottom' + size: "cover", // Background size: 'cover', 'contain', 'auto' + repeat: "no-repeat", // Background repeat: 'no-repeat', 'repeat', 'repeat-x', 'repeat-y' + attachment: "fixed", // Background attachment: 'fixed', 'scroll', 'local' + opacity: 1, // Background opacity (0-1) + }, + toc: { + enable: true, // Display the table of contents on the right side of the post + depth: 2, // Maximum heading depth to show in the table, from 1 to 3 + }, + favicon: [ + // Leave this array empty to use the default favicon + { + src: "https://q2.qlogo.cn/headimg_dl?dst_uin=2726730791&spec=0", // Path of the favicon, relative to the /public directory + // theme: 'light', // (Optional) Either 'light' or 'dark', set only if you have different favicons for light and dark mode + // sizes: '32x32', // (Optional) Size of the favicon, set only if you have favicons of different sizes + }, + ], + officialSites: [ + { url: "https://blog.meowrain.cn", alias: "EdgeOne CN" }, + { url: "https://blog2.meowrain.cn", alias: "Global" }, + { url: "https://www.meowrain.cn", alias: "Global" }, + ], +}; + +export const navBarConfig: NavBarConfig = { + links: [ + LinkPreset.Home, + LinkPreset.Archive, + { + name: "分类", + url: "/category/", + external: false, + }, + { + name: "相册", + url: "/gallery/", + external: false, + }, + { + name: "友链", + url: "/friends/", // Internal links should not include the base path, as it is automatically added + external: false, // Show an external link icon and will open in a new tab + }, + { + name: "赞助", + url: "/sponsors/", // Internal links should not include the base path, as it is automatically added + external: false, // Show an external link icon and will open in a new tab + }, + // { + // name: "统计", + // url: "https://umami.acofork.com/share/CdkXbGgZr6ECKOyK", // Internal links should not include the base path, as it is automatically added + // external: true, // Show an external link icon and will open in a new tab + // }, + // { + // name: "监控", + // url: "https://eoddos.2x.nz", // Internal links should not include the base path, as it is automatically added + // external: true, // Show an external link icon and will open in a new tab + // }, + ], +}; + +export const profileConfig: ProfileConfig = { + avatar: "https://blog.meowrain.cn/api/i/2025/07/18/zn3t6t-1.webp", // Relative to the /src directory. Relative to the /public directory if it starts with '/' + name: "MeowRain", + bio: "A developer who loves to code and learn new things,build code for love❤️ and fun🎉", + links: [ + { + name: "GitHub", + icon: "fa6-brands:github", + url: "https://github.com/meowrain", + }, + { + name: "我的OpenWebUI站", + icon: "fa6-brands:airbnb", + url: "https://ai.meowrain.cn", + }, + { + name: "服务器状态监控", + icon: "fa6-solid:server", + url: "https://status.meowrain.cn", + }, + { + name: "bilibili", + icon: "fa6-brands:bilibili", + url: "https://space.bilibili.com/386388600", + }, + ], +}; +export const licenseConfig: LicenseConfig = { + enable: true, + name: "CC BY-NC-SA 4.0", + url: "https://creativecommons.org/licenses/by-nc-sa/4.0/", +}; + +export const imageFallbackConfig: ImageFallbackConfig = { + enable: false, + originalDomain: "https://eopfapi.acofork.com/pic?img=ua", + fallbackDomain: "https://eopfapi.acofork.com/pic?img=ua", +}; + +export const umamiConfig: UmamiConfig = { + enable: true, + baseUrl: "https://umami.acofork.com", + shareId: "CdkXbGgZr6ECKOyK", + timezone: "Asia/Shanghai", +}; + +export const expressiveCodeConfig: ExpressiveCodeConfig = { + theme: "github-dark", +}; + +export const gitHubEditConfig: GitHubEditConfig = { + enable: true, + baseUrl: "https://github.com/afoim/fuwari/blob/main/src/content/posts", +}; + +// todoConfig removed from here diff --git a/src/constants/constants.ts b/src/constants/constants.ts new file mode 100644 index 0000000..896150d --- /dev/null +++ b/src/constants/constants.ts @@ -0,0 +1,17 @@ +export const PAGE_SIZE = 8; + +export const LIGHT_MODE = "light", + DARK_MODE = "dark", + AUTO_MODE = "auto"; +export const DEFAULT_THEME = AUTO_MODE; + +// Banner height unit: vh +export const BANNER_HEIGHT = 35; +export const BANNER_HEIGHT_EXTEND = 30; +export const BANNER_HEIGHT_HOME = BANNER_HEIGHT + BANNER_HEIGHT_EXTEND; + +// The height the main panel overlaps the banner, unit: rem +export const MAIN_PANEL_OVERLAPS_BANNER_HEIGHT = 3.5; + +// Page width: rem +export const PAGE_WIDTH = 75; diff --git a/src/constants/icon.ts b/src/constants/icon.ts new file mode 100644 index 0000000..f03efba --- /dev/null +++ b/src/constants/icon.ts @@ -0,0 +1,44 @@ +import type { Favicon } from "@/types/config.ts"; + +export const defaultFavicons: Favicon[] = [ + { + src: "/favicon/favicon-light-32.png", + theme: "light", + sizes: "32x32", + }, + { + src: "/favicon/favicon-light-128.png", + theme: "light", + sizes: "128x128", + }, + { + src: "/favicon/favicon-light-180.png", + theme: "light", + sizes: "180x180", + }, + { + src: "/favicon/favicon-light-192.png", + theme: "light", + sizes: "192x192", + }, + { + src: "/favicon/favicon-dark-32.png", + theme: "dark", + sizes: "32x32", + }, + { + src: "/favicon/favicon-dark-128.png", + theme: "dark", + sizes: "128x128", + }, + { + src: "/favicon/favicon-dark-180.png", + theme: "dark", + sizes: "180x180", + }, + { + src: "/favicon/favicon-dark-192.png", + theme: "dark", + sizes: "192x192", + }, +]; diff --git a/src/constants/link-presets.ts b/src/constants/link-presets.ts new file mode 100644 index 0000000..d2d2fe8 --- /dev/null +++ b/src/constants/link-presets.ts @@ -0,0 +1,13 @@ +import { LinkPreset, type NavBarLink } from "@/types/config"; + + +export const LinkPresets: { [key in LinkPreset]: NavBarLink } = { + [LinkPreset.Home]: { + name: "首页", + url: "/", + }, + [LinkPreset.Archive]: { + name: "归档", + url: "/archive/", + }, +}; diff --git a/src/content/.obsidian/app.json b/src/content/.obsidian/app.json new file mode 100644 index 0000000..4391702 --- /dev/null +++ b/src/content/.obsidian/app.json @@ -0,0 +1,6 @@ +{ + "attachmentFolderPath": "assets/images", + "newLinkFormat": "relative", + "useMarkdownLinks": true, + "uriCallbacks": false +} \ No newline at end of file diff --git a/src/content/.obsidian/appearance.json b/src/content/.obsidian/appearance.json new file mode 100644 index 0000000..9e26dfe --- /dev/null +++ b/src/content/.obsidian/appearance.json @@ -0,0 +1 @@ +{} \ No newline at end of file diff --git a/src/content/.obsidian/community-plugins.json b/src/content/.obsidian/community-plugins.json new file mode 100644 index 0000000..5eeb481 --- /dev/null +++ b/src/content/.obsidian/community-plugins.json @@ -0,0 +1,3 @@ +[ + "obsidian-paste-image-rename" +] \ No newline at end of file diff --git a/src/content/.obsidian/core-plugins.json b/src/content/.obsidian/core-plugins.json new file mode 100644 index 0000000..0faa60d --- /dev/null +++ b/src/content/.obsidian/core-plugins.json @@ -0,0 +1,33 @@ +{ + "file-explorer": true, + "global-search": true, + "switcher": true, + "graph": true, + "backlink": true, + "canvas": true, + "outgoing-link": true, + "tag-pane": true, + "footnotes": false, + "properties": false, + "page-preview": true, + "daily-notes": true, + "templates": true, + "note-composer": true, + "command-palette": true, + "slash-command": false, + "editor-status": true, + "bookmarks": true, + "markdown-importer": false, + "zk-prefixer": false, + "random-note": false, + "outline": true, + "word-count": true, + "slides": false, + "audio-recorder": false, + "workspaces": false, + "file-recovery": true, + "publish": false, + "sync": true, + "bases": true, + "webviewer": false +} \ No newline at end of file diff --git a/src/content/.obsidian/plugins/obsidian-paste-image-rename/data.json b/src/content/.obsidian/plugins/obsidian-paste-image-rename/data.json new file mode 100644 index 0000000..70514b3 --- /dev/null +++ b/src/content/.obsidian/plugins/obsidian-paste-image-rename/data.json @@ -0,0 +1,10 @@ +{ + "imageNamePattern": "{{fileName}}", + "dupNumberAtStart": false, + "dupNumberDelimiter": "-", + "dupNumberAlways": false, + "autoRename": true, + "handleAllAttachments": false, + "excludeExtensionPattern": "", + "disableRenameNotice": false +} \ No newline at end of file diff --git a/src/content/.obsidian/plugins/obsidian-paste-image-rename/main.js b/src/content/.obsidian/plugins/obsidian-paste-image-rename/main.js new file mode 100644 index 0000000..4a560fc --- /dev/null +++ b/src/content/.obsidian/plugins/obsidian-paste-image-rename/main.js @@ -0,0 +1,944 @@ +/* THIS IS A GENERATED/BUNDLED FILE BY ESBUILD */ +var __defProp = Object.defineProperty; +var __defProps = Object.defineProperties; +var __getOwnPropDesc = Object.getOwnPropertyDescriptor; +var __getOwnPropDescs = Object.getOwnPropertyDescriptors; +var __getOwnPropNames = Object.getOwnPropertyNames; +var __getOwnPropSymbols = Object.getOwnPropertySymbols; +var __hasOwnProp = Object.prototype.hasOwnProperty; +var __propIsEnum = Object.prototype.propertyIsEnumerable; +var __defNormalProp = (obj, key, value) => key in obj ? __defProp(obj, key, { enumerable: true, configurable: true, writable: true, value }) : obj[key] = value; +var __spreadValues = (a, b) => { + for (var prop in b || (b = {})) + if (__hasOwnProp.call(b, prop)) + __defNormalProp(a, prop, b[prop]); + if (__getOwnPropSymbols) + for (var prop of __getOwnPropSymbols(b)) { + if (__propIsEnum.call(b, prop)) + __defNormalProp(a, prop, b[prop]); + } + return a; +}; +var __spreadProps = (a, b) => __defProps(a, __getOwnPropDescs(b)); +var __commonJS = (cb, mod) => function __require() { + return mod || (0, cb[__getOwnPropNames(cb)[0]])((mod = { exports: {} }).exports, mod), mod.exports; +}; +var __export = (target, all) => { + for (var name in all) + __defProp(target, name, { get: all[name], enumerable: true }); +}; +var __copyProps = (to, from, except, desc) => { + if (from && typeof from === "object" || typeof from === "function") { + for (let key of __getOwnPropNames(from)) + if (!__hasOwnProp.call(to, key) && key !== except) + __defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable }); + } + return to; +}; +var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod); +var __async = (__this, __arguments, generator) => { + return new Promise((resolve, reject) => { + var fulfilled = (value) => { + try { + step(generator.next(value)); + } catch (e) { + reject(e); + } + }; + var rejected = (value) => { + try { + step(generator.throw(value)); + } catch (e) { + reject(e); + } + }; + var step = (x) => x.done ? resolve(x.value) : Promise.resolve(x.value).then(fulfilled, rejected); + step((generator = generator.apply(__this, __arguments)).next()); + }); +}; + +// package.json +var require_package = __commonJS({ + "package.json"(exports, module2) { + module2.exports = { + name: "obsidian-paste-image-rename", + version: "1.6.1", + main: "main.js", + scripts: { + start: "node esbuild.config.mjs", + build: "tsc -noEmit -skipLibCheck && BUILD_ENV=production node esbuild.config.mjs && cp manifest.json build", + version: "node version-bump.mjs && git add manifest.json versions.json", + release: "npm run build && gh release create ${npm_package_version} build/*" + }, + keywords: [], + author: "Reorx", + license: "MIT", + devDependencies: { + "@types/node": "^18.11.18", + "@typescript-eslint/eslint-plugin": "^5.49.0", + "@typescript-eslint/parser": "^5.49.0", + "builtin-modules": "^3.3.0", + esbuild: "0.16.17", + obsidian: "^1.1.1", + tslib: "2.5.0", + typescript: "4.9.4" + }, + dependencies: { + "cash-dom": "^8.1.2" + } + }; + } +}); + +// src/main.ts +var main_exports = {}; +__export(main_exports, { + default: () => PasteImageRenamePlugin +}); +module.exports = __toCommonJS(main_exports); +var import_obsidian2 = require("obsidian"); + +// src/batch.ts +var import_obsidian = require("obsidian"); + +// src/utils.ts +var DEBUG = false; +if (DEBUG) + console.log("DEBUG is enabled"); +function debugLog(...args) { + if (DEBUG) { + console.log(new Date().toISOString().slice(11, 23), ...args); + } +} +function createElementTree(rootEl, opts) { + const result = { + el: rootEl.createEl(opts.tag, opts), + children: [] + }; + const children = opts.children || []; + for (const child of children) { + result.children.push(createElementTree(result.el, child)); + } + return result; +} +var path = { + // Credit: @creationix/path.js + join(...partSegments) { + let parts = []; + for (let i = 0, l = partSegments.length; i < l; i++) { + parts = parts.concat(partSegments[i].split("/")); + } + const newParts = []; + for (let i = 0, l = parts.length; i < l; i++) { + const part = parts[i]; + if (!part || part === ".") + continue; + else + newParts.push(part); + } + if (parts[0] === "") + newParts.unshift(""); + return newParts.join("/"); + }, + // returns the last part of a path, e.g. 'foo.jpg' + basename(fullpath) { + const sp = fullpath.split("/"); + return sp[sp.length - 1]; + }, + // return extension without dot, e.g. 'jpg' + extension(fullpath) { + const positions = [...fullpath.matchAll(new RegExp("\\.", "gi"))].map((a) => a.index); + return fullpath.slice(positions[positions.length - 1] + 1); + } +}; +var filenameNotAllowedChars = /[^\p{L}0-9~`!@$&*()\-_=+{};'",<.>? ]/ug; +var sanitizer = { + filename(s) { + return s.replace(filenameNotAllowedChars, "").trim(); + }, + delimiter(s) { + s = this.filename(s); + if (!s) + s = "-"; + return s; + } +}; +function escapeRegExp(s) { + return s.replace(/[.*+?^${}()|[\]\\]/g, "\\$&"); +} +function lockInputMethodComposition(el) { + const state = { + lock: false + }; + el.addEventListener("compositionstart", () => { + state.lock = true; + }); + el.addEventListener("compositionend", () => { + state.lock = false; + }); + return state; +} + +// src/batch.ts +var ImageBatchRenameModal = class extends import_obsidian.Modal { + constructor(app, activeFile, renameFunc, onClose) { + super(app); + this.activeFile = activeFile; + this.renameFunc = renameFunc; + this.onCloseExtra = onClose; + this.state = { + namePattern: "", + extPattern: "", + nameReplace: "", + renameTasks: [] + }; + } + onOpen() { + this.containerEl.addClass("image-rename-modal"); + const { contentEl, titleEl } = this; + titleEl.setText("Batch rename embeded files"); + const namePatternSetting = new import_obsidian.Setting(contentEl).setName("Name pattern").setDesc("Please input the name pattern to match files (regex)").addText((text) => text.setValue(this.state.namePattern).onChange( + (value) => __async(this, null, function* () { + this.state.namePattern = value; + }) + )); + const npInputEl = namePatternSetting.controlEl.children[0]; + npInputEl.focus(); + const npInputState = lockInputMethodComposition(npInputEl); + npInputEl.addEventListener("keydown", (e) => __async(this, null, function* () { + if (e.key === "Enter" && !npInputState.lock) { + e.preventDefault(); + if (!this.state.namePattern) { + errorEl.innerText = 'Error: "Name pattern" could not be empty'; + errorEl.style.display = "block"; + return; + } + this.matchImageNames(tbodyEl); + } + })); + const extPatternSetting = new import_obsidian.Setting(contentEl).setName("Extension pattern").setDesc("Please input the extension pattern to match files (regex)").addText((text) => text.setValue(this.state.extPattern).onChange( + (value) => __async(this, null, function* () { + this.state.extPattern = value; + }) + )); + const extInputEl = extPatternSetting.controlEl.children[0]; + extInputEl.addEventListener("keydown", (e) => __async(this, null, function* () { + if (e.key === "Enter") { + e.preventDefault(); + this.matchImageNames(tbodyEl); + } + })); + const nameReplaceSetting = new import_obsidian.Setting(contentEl).setName("Name replace").setDesc("Please input the string to replace the matched name (use $1, $2 for regex groups)").addText((text) => text.setValue(this.state.nameReplace).onChange( + (value) => __async(this, null, function* () { + this.state.nameReplace = value; + }) + )); + const nrInputEl = nameReplaceSetting.controlEl.children[0]; + const nrInputState = lockInputMethodComposition(nrInputEl); + nrInputEl.addEventListener("keydown", (e) => __async(this, null, function* () { + if (e.key === "Enter" && !nrInputState.lock) { + e.preventDefault(); + this.matchImageNames(tbodyEl); + } + })); + const matchedContainer = contentEl.createDiv({ + cls: "matched-container" + }); + const tableET = createElementTree(matchedContainer, { + tag: "table", + children: [ + { + tag: "thead", + children: [ + { + tag: "tr", + children: [ + { + tag: "td", + text: "Original path" + }, + { + tag: "td", + text: "Renamed Name" + } + ] + } + ] + }, + { + tag: "tbody" + } + ] + }); + const tbodyEl = tableET.children[1].el; + const errorEl = contentEl.createDiv({ + cls: "error", + attr: { + style: "display: none;" + } + }); + new import_obsidian.Setting(contentEl).addButton((button) => { + button.setButtonText("Rename all").setClass("mod-cta").onClick(() => { + new ConfirmModal( + this.app, + "Confirm rename all", + `Are you sure? This will rename all the ${this.state.renameTasks.length} images matched the pattern.`, + () => { + this.renameAll(); + this.close(); + } + ).open(); + }); + }).addButton((button) => { + button.setButtonText("Cancel").onClick(() => { + this.close(); + }); + }); + } + onClose() { + const { contentEl } = this; + contentEl.empty(); + this.onCloseExtra(); + } + renameAll() { + return __async(this, null, function* () { + debugLog("renameAll", this.state); + for (const task of this.state.renameTasks) { + yield this.renameFunc(task.file, task.name); + } + }); + } + matchImageNames(tbodyEl) { + const { state } = this; + const renameTasks = []; + tbodyEl.empty(); + const fileCache = this.app.metadataCache.getFileCache(this.activeFile); + if (!fileCache || !fileCache.embeds) + return; + const namePatternRegex = new RegExp(state.namePattern, "g"); + const extPatternRegex = new RegExp(state.extPattern); + fileCache.embeds.forEach((embed) => { + const file = this.app.metadataCache.getFirstLinkpathDest(embed.link, this.activeFile.path); + if (!file) { + console.warn("file not found", embed.link); + return; + } + if (state.extPattern) { + const m0 = extPatternRegex.exec(file.extension); + if (!m0) + return; + } + const stem = file.basename; + namePatternRegex.lastIndex = 0; + const m1 = namePatternRegex.exec(stem); + if (!m1) + return; + let renamedName = file.name; + if (state.nameReplace) { + namePatternRegex.lastIndex = 0; + renamedName = stem.replace(namePatternRegex, state.nameReplace); + renamedName = `${renamedName}.${file.extension}`; + } + renameTasks.push({ + file, + name: renamedName + }); + createElementTree(tbodyEl, { + tag: "tr", + children: [ + { + tag: "td", + children: [ + { + tag: "span", + text: file.name + }, + { + tag: "div", + text: file.path, + attr: { + class: "file-path" + } + } + ] + }, + { + tag: "td", + children: [ + { + tag: "span", + text: renamedName + }, + { + tag: "div", + text: path.join(file.parent.path, renamedName), + attr: { + class: "file-path" + } + } + ] + } + ] + }); + }); + debugLog("new renameTasks", renameTasks); + state.renameTasks = renameTasks; + } +}; +var ConfirmModal = class extends import_obsidian.Modal { + constructor(app, title, message, onConfirm) { + super(app); + this.title = title; + this.message = message; + this.onConfirm = onConfirm; + } + onOpen() { + const { contentEl, titleEl } = this; + titleEl.setText(this.title); + contentEl.createEl("p", { + text: this.message + }); + new import_obsidian.Setting(contentEl).addButton((button) => { + button.setButtonText("Yes").setClass("mod-warning").onClick(() => { + this.onConfirm(); + this.close(); + }); + }).addButton((button) => { + button.setButtonText("No").onClick(() => { + this.close(); + }); + }); + } +}; + +// src/template.ts +var dateTmplRegex = /{{DATE:([^}]+)}}/gm; +var frontmatterTmplRegex = /{{frontmatter:([^}]+)}}/gm; +var replaceDateVar = (s, date) => { + const m = dateTmplRegex.exec(s); + if (!m) + return s; + return s.replace(m[0], date.format(m[1])); +}; +var replaceFrontmatterVar = (s, frontmatter) => { + if (!frontmatter) + return s; + const m = frontmatterTmplRegex.exec(s); + if (!m) + return s; + return s.replace(m[0], frontmatter[m[1]] || ""); +}; +var renderTemplate = (tmpl, data, frontmatter) => { + const now = window.moment(); + let text = tmpl; + let newtext; + while ((newtext = replaceDateVar(text, now)) != text) { + text = newtext; + } + while ((newtext = replaceFrontmatterVar(text, frontmatter)) != text) { + text = newtext; + } + text = text.replace(/{{imageNameKey}}/gm, data.imageNameKey).replace(/{{fileName}}/gm, data.fileName).replace(/{{dirName}}/gm, data.dirName).replace(/{{firstHeading}}/gm, data.firstHeading); + return text; +}; + +// src/main.ts +var DEFAULT_SETTINGS = { + imageNamePattern: "{{fileName}}", + dupNumberAtStart: false, + dupNumberDelimiter: "-", + dupNumberAlways: false, + autoRename: false, + handleAllAttachments: false, + excludeExtensionPattern: "", + disableRenameNotice: false +}; +var PASTED_IMAGE_PREFIX = "Pasted image "; +var PasteImageRenamePlugin = class extends import_obsidian2.Plugin { + constructor() { + super(...arguments); + this.modals = []; + } + onload() { + return __async(this, null, function* () { + const pkg = require_package(); + console.log(`Plugin loading: ${pkg.name} ${pkg.version} BUILD_ENV=${"production"}`); + yield this.loadSettings(); + this.registerEvent( + this.app.vault.on("create", (file) => { + if (!(file instanceof import_obsidian2.TFile)) + return; + const timeGapMs = new Date().getTime() - file.stat.ctime; + if (timeGapMs > 1e3) + return; + if (isMarkdownFile(file)) + return; + if (isPastedImage(file)) { + debugLog("pasted image created", file); + this.startRenameProcess(file, this.settings.autoRename); + } else { + if (this.settings.handleAllAttachments) { + debugLog("handleAllAttachments for file", file); + if (this.testExcludeExtension(file)) { + debugLog("excluded file by ext", file); + return; + } + this.startRenameProcess(file, this.settings.autoRename); + } + } + }) + ); + const startBatchRenameProcess = () => { + this.openBatchRenameModal(); + }; + this.addCommand({ + id: "batch-rename-embeded-files", + name: "Batch rename embeded files (in the current file)", + callback: startBatchRenameProcess + }); + if (DEBUG) { + this.addRibbonIcon("wand-glyph", "Batch rename embeded files", startBatchRenameProcess); + } + const batchRenameAllImages = () => { + this.batchRenameAllImages(); + }; + this.addCommand({ + id: "batch-rename-all-images", + name: "Batch rename all images instantly (in the current file)", + callback: batchRenameAllImages + }); + if (DEBUG) { + this.addRibbonIcon("wand-glyph", "Batch rename all images instantly (in the current file)", batchRenameAllImages); + } + this.addSettingTab(new SettingTab(this.app, this)); + }); + } + startRenameProcess(file, autoRename = false) { + return __async(this, null, function* () { + const activeFile = this.getActiveFile(); + if (!activeFile) { + new import_obsidian2.Notice("Error: No active file found."); + return; + } + const { stem, newName, isMeaningful } = this.generateNewName(file, activeFile); + debugLog("generated newName:", newName, isMeaningful); + if (!isMeaningful || !autoRename) { + this.openRenameModal(file, isMeaningful ? stem : "", activeFile.path); + return; + } + this.renameFile(file, newName, activeFile.path, true); + }); + } + renameFile(file, inputNewName, sourcePath, replaceCurrentLine) { + return __async(this, null, function* () { + const { name: newName } = yield this.deduplicateNewName(inputNewName, file); + debugLog("deduplicated newName:", newName); + const originName = file.name; + const linkText = this.app.fileManager.generateMarkdownLink(file, sourcePath); + const newPath = path.join(file.parent.path, newName); + try { + yield this.app.fileManager.renameFile(file, newPath); + } catch (err) { + new import_obsidian2.Notice(`Failed to rename ${newName}: ${err}`); + throw err; + } + if (!replaceCurrentLine) { + return; + } + const newLinkText = this.app.fileManager.generateMarkdownLink(file, sourcePath); + debugLog("replace text", linkText, newLinkText); + const editor = this.getActiveEditor(); + if (!editor) { + new import_obsidian2.Notice(`Failed to rename ${newName}: no active editor`); + return; + } + const cursor = editor.getCursor(); + const line = editor.getLine(cursor.line); + const replacedLine = line.replace(linkText, newLinkText); + debugLog("current line -> replaced line", line, replacedLine); + editor.transaction({ + changes: [ + { + from: __spreadProps(__spreadValues({}, cursor), { ch: 0 }), + to: __spreadProps(__spreadValues({}, cursor), { ch: line.length }), + text: replacedLine + } + ] + }); + if (!this.settings.disableRenameNotice) { + new import_obsidian2.Notice(`Renamed ${originName} to ${newName}`); + } + }); + } + openRenameModal(file, newName, sourcePath) { + const modal = new ImageRenameModal( + this.app, + file, + newName, + (confirmedName) => { + debugLog("confirmedName:", confirmedName); + this.renameFile(file, confirmedName, sourcePath, true); + }, + () => { + this.modals.splice(this.modals.indexOf(modal), 1); + } + ); + this.modals.push(modal); + modal.open(); + debugLog("modals count", this.modals.length); + } + openBatchRenameModal() { + const activeFile = this.getActiveFile(); + const modal = new ImageBatchRenameModal( + this.app, + activeFile, + (file, name) => __async(this, null, function* () { + yield this.renameFile(file, name, activeFile.path); + }), + () => { + this.modals.splice(this.modals.indexOf(modal), 1); + } + ); + this.modals.push(modal); + modal.open(); + } + batchRenameAllImages() { + return __async(this, null, function* () { + const activeFile = this.getActiveFile(); + const fileCache = this.app.metadataCache.getFileCache(activeFile); + if (!fileCache || !fileCache.embeds) + return; + const extPatternRegex = /jpe?g|png|gif|tiff|webp/i; + for (const embed of fileCache.embeds) { + const file = this.app.metadataCache.getFirstLinkpathDest(embed.link, activeFile.path); + if (!file) { + console.warn("file not found", embed.link); + return; + } + const m0 = extPatternRegex.exec(file.extension); + if (!m0) + return; + const { newName, isMeaningful } = this.generateNewName(file, activeFile); + debugLog("generated newName:", newName, isMeaningful); + if (!isMeaningful) { + new import_obsidian2.Notice("Failed to batch rename images: the generated name is not meaningful"); + break; + } + yield this.renameFile(file, newName, activeFile.path, false); + } + }); + } + // returns a new name for the input file, with extension + generateNewName(file, activeFile) { + let imageNameKey = ""; + let firstHeading = ""; + let frontmatter; + const fileCache = this.app.metadataCache.getFileCache(activeFile); + if (fileCache) { + debugLog("frontmatter", fileCache.frontmatter); + frontmatter = fileCache.frontmatter; + imageNameKey = (frontmatter == null ? void 0 : frontmatter.imageNameKey) || ""; + firstHeading = getFirstHeading(fileCache.headings); + } else { + console.warn("could not get file cache from active file", activeFile.name); + } + const stem = renderTemplate( + this.settings.imageNamePattern, + { + imageNameKey, + fileName: activeFile.basename, + dirName: activeFile.parent.name, + firstHeading + }, + frontmatter + ); + const meaninglessRegex = new RegExp(`[${this.settings.dupNumberDelimiter}\\s]`, "gm"); + return { + stem, + newName: stem + "." + file.extension, + isMeaningful: stem.replace(meaninglessRegex, "") !== "" + }; + } + // newName: foo.ext + deduplicateNewName(newName, file) { + return __async(this, null, function* () { + const dir = file.parent.path; + const listed = yield this.app.vault.adapter.list(dir); + debugLog("sibling files", listed); + const newNameExt = path.extension(newName), newNameStem = newName.slice(0, newName.length - newNameExt.length - 1), newNameStemEscaped = escapeRegExp(newNameStem), delimiter = this.settings.dupNumberDelimiter, delimiterEscaped = escapeRegExp(delimiter); + let dupNameRegex; + if (this.settings.dupNumberAtStart) { + dupNameRegex = new RegExp( + `^(?<number>\\d+)${delimiterEscaped}(?<name>${newNameStemEscaped})\\.${newNameExt}$` + ); + } else { + dupNameRegex = new RegExp( + `^(?<name>${newNameStemEscaped})${delimiterEscaped}(?<number>\\d+)\\.${newNameExt}$` + ); + } + debugLog("dupNameRegex", dupNameRegex); + const dupNameNumbers = []; + let isNewNameExist = false; + for (let sibling of listed.files) { + sibling = path.basename(sibling); + if (sibling == newName) { + isNewNameExist = true; + continue; + } + const m = dupNameRegex.exec(sibling); + if (!m) + continue; + dupNameNumbers.push(parseInt(m.groups.number)); + } + if (isNewNameExist || this.settings.dupNumberAlways) { + const newNumber = dupNameNumbers.length > 0 ? Math.max(...dupNameNumbers) + 1 : 1; + if (this.settings.dupNumberAtStart) { + newName = `${newNumber}${delimiter}${newNameStem}.${newNameExt}`; + } else { + newName = `${newNameStem}${delimiter}${newNumber}.${newNameExt}`; + } + } + return { + name: newName, + stem: newName.slice(0, newName.length - newNameExt.length - 1), + extension: newNameExt + }; + }); + } + getActiveFile() { + const view = this.app.workspace.getActiveViewOfType(import_obsidian2.MarkdownView); + const file = view == null ? void 0 : view.file; + debugLog("active file", file == null ? void 0 : file.path); + return file; + } + getActiveEditor() { + const view = this.app.workspace.getActiveViewOfType(import_obsidian2.MarkdownView); + return view == null ? void 0 : view.editor; + } + onunload() { + this.modals.map((modal) => modal.close()); + } + testExcludeExtension(file) { + const pattern = this.settings.excludeExtensionPattern; + if (!pattern) + return false; + return new RegExp(pattern).test(file.extension); + } + loadSettings() { + return __async(this, null, function* () { + this.settings = Object.assign({}, DEFAULT_SETTINGS, yield this.loadData()); + }); + } + saveSettings() { + return __async(this, null, function* () { + yield this.saveData(this.settings); + }); + } +}; +function getFirstHeading(headings) { + if (headings && headings.length > 0) { + for (const heading of headings) { + if (heading.level === 1) { + return heading.heading; + } + } + } + return ""; +} +function isPastedImage(file) { + if (file instanceof import_obsidian2.TFile) { + if (file.name.startsWith(PASTED_IMAGE_PREFIX)) { + return true; + } + } + return false; +} +function isMarkdownFile(file) { + if (file instanceof import_obsidian2.TFile) { + if (file.extension === "md") { + return true; + } + } + return false; +} +var ImageRenameModal = class extends import_obsidian2.Modal { + constructor(app, src, stem, renameFunc, onClose) { + super(app); + this.src = src; + this.stem = stem; + this.renameFunc = renameFunc; + this.onCloseExtra = onClose; + } + onOpen() { + this.containerEl.addClass("image-rename-modal"); + const { contentEl, titleEl } = this; + titleEl.setText("Rename image"); + const imageContainer = contentEl.createDiv({ + cls: "image-container" + }); + imageContainer.createEl("img", { + attr: { + src: this.app.vault.getResourcePath(this.src) + } + }); + let stem = this.stem; + const ext = this.src.extension; + const getNewName = (stem2) => stem2 + "." + ext; + const getNewPath = (stem2) => path.join(this.src.parent.path, getNewName(stem2)); + const infoET = createElementTree(contentEl, { + tag: "ul", + cls: "info", + children: [ + { + tag: "li", + children: [ + { + tag: "span", + text: "Origin path" + }, + { + tag: "span", + text: this.src.path + } + ] + }, + { + tag: "li", + children: [ + { + tag: "span", + text: "New path" + }, + { + tag: "span", + text: getNewPath(stem) + } + ] + } + ] + }); + const doRename = () => __async(this, null, function* () { + debugLog("doRename", `stem=${stem}`); + this.renameFunc(getNewName(stem)); + }); + const nameSetting = new import_obsidian2.Setting(contentEl).setName("New name").setDesc("Please input the new name for the image (without extension)").addText((text) => text.setValue(stem).onChange( + (value) => __async(this, null, function* () { + stem = sanitizer.filename(value); + infoET.children[1].children[1].el.innerText = getNewPath(stem); + }) + )); + const nameInputEl = nameSetting.controlEl.children[0]; + nameInputEl.focus(); + const nameInputState = lockInputMethodComposition(nameInputEl); + nameInputEl.addEventListener("keydown", (e) => __async(this, null, function* () { + if (e.key === "Enter" && !nameInputState.lock) { + e.preventDefault(); + if (!stem) { + errorEl.innerText = 'Error: "New name" could not be empty'; + errorEl.style.display = "block"; + return; + } + doRename(); + this.close(); + } + })); + const errorEl = contentEl.createDiv({ + cls: "error", + attr: { + style: "display: none;" + } + }); + new import_obsidian2.Setting(contentEl).addButton((button) => { + button.setButtonText("Rename").onClick(() => { + doRename(); + this.close(); + }); + }).addButton((button) => { + button.setButtonText("Cancel").onClick(() => { + this.close(); + }); + }); + } + onClose() { + const { contentEl } = this; + contentEl.empty(); + this.onCloseExtra(); + } +}; +var imageNamePatternDesc = ` +The pattern indicates how the new name should be generated. + +Available variables: +- {{fileName}}: name of the active file, without ".md" extension. +- {{imageNameKey}}: this variable is read from the markdown file's frontmatter, from the same key "imageNameKey". +- {{DATE:$FORMAT}}: use "$FORMAT" to format the current date, "$FORMAT" must be a Moment.js format string, e.g. {{DATE:YYYY-MM-DD}}. + +Here are some examples from pattern to image names (repeat in sequence), variables: fileName = "My note", imageNameKey = "foo": +- {{fileName}}: My note, My note-1, My note-2 +- {{imageNameKey}}: foo, foo-1, foo-2 +- {{imageNameKey}}-{{DATE:YYYYMMDD}}: foo-20220408, foo-20220408-1, foo-20220408-2 +`; +var SettingTab = class extends import_obsidian2.PluginSettingTab { + constructor(app, plugin) { + super(app, plugin); + this.plugin = plugin; + } + display() { + const { containerEl } = this; + containerEl.empty(); + new import_obsidian2.Setting(containerEl).setName("Image name pattern").setDesc(imageNamePatternDesc).setClass("long-description-setting-item").addText((text) => text.setPlaceholder("{{imageNameKey}}").setValue(this.plugin.settings.imageNamePattern).onChange( + (value) => __async(this, null, function* () { + this.plugin.settings.imageNamePattern = value; + yield this.plugin.saveSettings(); + }) + )); + new import_obsidian2.Setting(containerEl).setName("Duplicate number at start (or end)").setDesc(`If enabled, duplicate number will be added at the start as prefix for the image name, otherwise it will be added at the end as suffix for the image name.`).addToggle((toggle) => toggle.setValue(this.plugin.settings.dupNumberAtStart).onChange( + (value) => __async(this, null, function* () { + this.plugin.settings.dupNumberAtStart = value; + yield this.plugin.saveSettings(); + }) + )); + new import_obsidian2.Setting(containerEl).setName("Duplicate number delimiter").setDesc(`The delimiter to generate the number prefix/suffix for duplicated names. For example, if the value is "-", the suffix will be like "-1", "-2", "-3", and the prefix will be like "1-", "2-", "3-". Only characters that are valid in file names are allowed.`).addText((text) => text.setValue(this.plugin.settings.dupNumberDelimiter).onChange( + (value) => __async(this, null, function* () { + this.plugin.settings.dupNumberDelimiter = sanitizer.delimiter(value); + yield this.plugin.saveSettings(); + }) + )); + new import_obsidian2.Setting(containerEl).setName("Always add duplicate number").setDesc(`If enabled, duplicate number will always be added to the image name. Otherwise, it will only be added when the name is duplicated.`).addToggle((toggle) => toggle.setValue(this.plugin.settings.dupNumberAlways).onChange( + (value) => __async(this, null, function* () { + this.plugin.settings.dupNumberAlways = value; + yield this.plugin.saveSettings(); + }) + )); + new import_obsidian2.Setting(containerEl).setName("Auto rename").setDesc(`By default, the rename modal will always be shown to confirm before renaming, if this option is set, the image will be auto renamed after pasting.`).addToggle((toggle) => toggle.setValue(this.plugin.settings.autoRename).onChange( + (value) => __async(this, null, function* () { + this.plugin.settings.autoRename = value; + yield this.plugin.saveSettings(); + }) + )); + new import_obsidian2.Setting(containerEl).setName("Handle all attachments").setDesc(`By default, the plugin only handles images that starts with "Pasted image " in name, + which is the prefix Obsidian uses to create images from pasted content. + If this option is set, the plugin will handle all attachments that are created in the vault.`).addToggle((toggle) => toggle.setValue(this.plugin.settings.handleAllAttachments).onChange( + (value) => __async(this, null, function* () { + this.plugin.settings.handleAllAttachments = value; + yield this.plugin.saveSettings(); + }) + )); + new import_obsidian2.Setting(containerEl).setName("Exclude extension pattern").setDesc(`This option is only useful when "Handle all attachments" is enabled. + Write a Regex pattern to exclude certain extensions from being handled. Only the first line will be used.`).setClass("single-line-textarea").addTextArea((text) => text.setPlaceholder("docx?|xlsx?|pptx?|zip|rar").setValue(this.plugin.settings.excludeExtensionPattern).onChange( + (value) => __async(this, null, function* () { + this.plugin.settings.excludeExtensionPattern = value; + yield this.plugin.saveSettings(); + }) + )); + new import_obsidian2.Setting(containerEl).setName("Disable rename notice").setDesc(`Turn off this option if you don't want to see the notice when renaming images. + Note that Obsidian may display a notice when a link has changed, this option cannot disable that.`).addToggle((toggle) => toggle.setValue(this.plugin.settings.disableRenameNotice).onChange( + (value) => __async(this, null, function* () { + this.plugin.settings.disableRenameNotice = value; + yield this.plugin.saveSettings(); + }) + )); + } +}; + +/* nosourcemap */ \ No newline at end of file diff --git a/src/content/.obsidian/plugins/obsidian-paste-image-rename/manifest.json b/src/content/.obsidian/plugins/obsidian-paste-image-rename/manifest.json new file mode 100644 index 0000000..152d913 --- /dev/null +++ b/src/content/.obsidian/plugins/obsidian-paste-image-rename/manifest.json @@ -0,0 +1,10 @@ +{ + "id": "obsidian-paste-image-rename", + "name": "Paste image rename", + "version": "1.6.1", + "minAppVersion": "0.12.0", + "description": "Rename pasted images and all the other attchments added to the vault", + "author": "Reorx", + "authorUrl": "https://github.com/reorx", + "isDesktopOnly": false +} \ No newline at end of file diff --git a/src/content/.obsidian/plugins/obsidian-paste-image-rename/styles.css b/src/content/.obsidian/plugins/obsidian-paste-image-rename/styles.css new file mode 100644 index 0000000..d542d56 --- /dev/null +++ b/src/content/.obsidian/plugins/obsidian-paste-image-rename/styles.css @@ -0,0 +1,79 @@ +/* src/styles.css */ +:root { + --shadow-color: 0deg 0% 0%; + --shadow-elevation-medium: + 0.5px 0.5px 0.7px hsl(var(--shadow-color) / 0.14), + 1.1px 1.1px 1.5px -0.9px hsl(var(--shadow-color) / 0.12), + 2.4px 2.5px 3.3px -1.8px hsl(var(--shadow-color) / 0.1), + 5.3px 5.6px 7.3px -2.7px hsl(var(--shadow-color) / 0.09), + 11px 11.4px 15.1px -3.6px hsl(var(--shadow-color) / 0.07); +} +.image-rename-modal .modal { + width: 65%; + min-width: 600px; +} +.image-rename-modal .modal-content { + padding: 10px 5px; +} +.image-rename-modal .image-container { + display: flex; + justify-content: center; +} +.image-rename-modal .info { + padding: 10px 0; + color: var(--text-muted); + user-select: text; +} +.image-rename-modal .info li > span:nth-of-type(1) { + display: inline-block; + width: 6em; + margin-right: .5em; +} +.image-rename-modal .info li > span:nth-of-type(1):after { + content: ":"; + float: right; +} +.image-rename-modal .image-container img { + display: block; + max-height: 300px; + box-shadow: var(--shadow-elevation-medium); +} +.image-rename-modal .setting-item-control input { + min-width: 300px; +} +.image-rename-modal .error { + border: 1px solid rgb(201, 90, 90); + color: rgb(134, 22, 22); + padding: 10px; +} +.image-rename-modal table { + font-size: .9em; + line-height: 1.8; + margin-bottom: 1.5em; + user-select: text; +} +.image-rename-modal table td { + padding-right: 1em; +} +.image-rename-modal table thead td { + font-weight: 700; +} +.image-rename-modal table tbody td .file-path { + font-size: .8em; + color: var(--text-faint); + line-height: 1; +} +.long-description-setting-item { + flex-wrap: wrap; +} +.long-description-setting-item .setting-item-description { + white-space: pre-wrap; + line-height: 1.3em; +} +.long-description-setting-item .setting-item-control { + padding-top: 10px; +} +.long-description-setting-item .setting-item-control input { + min-width: 300px; + width: 50%; +} diff --git a/src/content/.obsidian/workspace-mobile.json b/src/content/.obsidian/workspace-mobile.json new file mode 100644 index 0000000..ea94a82 --- /dev/null +++ b/src/content/.obsidian/workspace-mobile.json @@ -0,0 +1,181 @@ +{ + "main": { + "id": "11c40c0eff369230", + "type": "split", + "children": [ + { + "id": "a3b78448b99fd05e", + "type": "tabs", + "children": [ + { + "id": "a48cfb809f321ac1", + "type": "leaf", + "state": { + "type": "markdown", + "state": { + "file": "posts/mobile-git.md", + "mode": "source", + "source": false + }, + "icon": "lucide-file", + "title": "mobile-git" + } + } + ] + } + ], + "direction": "vertical" + }, + "left": { + "id": "084d99fc7dfae6d5", + "type": "mobile-drawer", + "children": [ + { + "id": "913290ce74f3c790", + "type": "leaf", + "state": { + "type": "file-explorer", + "state": { + "sortOrder": "alphabetical", + "autoReveal": false + }, + "icon": "lucide-folder-closed", + "title": "文件列表" + } + }, + { + "id": "715c6bf7efb3af34", + "type": "leaf", + "state": { + "type": "search", + "state": { + "query": "", + "matchingCase": false, + "explainSearch": false, + "collapseAll": false, + "extraContext": false, + "sortOrder": "alphabetical" + }, + "icon": "lucide-search", + "title": "搜索" + } + }, + { + "id": "1867f88e0e5be91e", + "type": "leaf", + "state": { + "type": "tag", + "state": { + "sortOrder": "frequency", + "useHierarchy": true, + "showSearch": false, + "searchQuery": "" + }, + "icon": "lucide-tags", + "title": "标签" + } + }, + { + "id": "c81497cc77ac87dc", + "type": "leaf", + "state": { + "type": "bookmarks", + "state": {}, + "icon": "lucide-bookmark", + "title": "书签" + } + } + ], + "currentTab": 0 + }, + "right": { + "id": "00f2d94402e22f34", + "type": "mobile-drawer", + "children": [ + { + "id": "c18847a8fc329542", + "type": "leaf", + "state": { + "type": "backlink", + "state": { + "collapseAll": false, + "extraContext": false, + "sortOrder": "alphabetical", + "showSearch": false, + "searchQuery": "", + "backlinkCollapsed": false, + "unlinkedCollapsed": true + }, + "icon": "links-coming-in", + "title": "反向链接" + } + }, + { + "id": "a0983a2e5ea8f256", + "type": "leaf", + "state": { + "type": "outgoing-link", + "state": { + "linksCollapsed": false, + "unlinkedCollapsed": true + }, + "icon": "links-going-out", + "title": "出链" + } + }, + { + "id": "289ab02c28f6d4b5", + "type": "leaf", + "state": { + "type": "outline", + "state": { + "followCursor": false, + "showSearch": false, + "searchQuery": "" + }, + "icon": "lucide-list", + "title": "大纲" + } + } + ], + "currentTab": 0 + }, + "left-ribbon": { + "hiddenItems": { + "switcher:打开快速切换": false, + "graph:查看关系图谱": false, + "canvas:新建白板": false, + "daily-notes:打开/创建今天的日记": false, + "templates:插入模板": false, + "command-palette:打开命令面板": false, + "bases:创建新数据库": false + } + }, + "active": "a48cfb809f321ac1", + "lastOpenFiles": [ + "posts/163-free-domain-email.md", + "posts/acg-randompic-api.md", + "posts/mobile-git.md", + "posts/warden-worker.md", + "posts/record.md", + "assets/images/Screenshot_2025-11-24-07-56-33-62_a2e3670364a4153bdb03dad30c8d4108.jpg", + "assets/images/Screenshot_2025-11-24-07-56-23-48_a2e3670364a4153bdb03dad30c8d4108 1.jpg", + "assets/images/Screenshot_2025-11-24-07-55-54-35_df198e732186825c8df26e3c5a10d7cd 1.jpg", + "assets/images/Screenshot_2025-11-24-07-56-23-48_a2e3670364a4153bdb03dad30c8d4108.jpg", + "assets/images/Screenshot_2025-11-24-07-55-54-35_df198e732186825c8df26e3c5a10d7cd.jpg", + "posts/wx-zfb-card.md", + "assets/images/Screenshot_2025-11-11-14-18-53-34_51606159b24eff83e24a54116878fe3e.jpg", + "assets/images/Screenshot_2025-11-11-14-17-32-08_51606159b24eff83e24a54116878fe3e.jpg", + "assets/images/Screenshot_2025-11-11-14-15-59-46_51606159b24eff83e24a54116878fe3e.jpg", + "assets/images/Screenshot_2025-11-11-14-15-01-63_b5a5c5cb02ca09c784c5d88160e2ec24.jpg", + "assets/images/Screenshot_2025-11-11-14-13-03-99_a2e3670364a4153bdb03dad30c8d4108.jpg", + "posts/check-notebook.md", + "未命名.md", + "posts/zte-f450-bridge.md", + "posts/unknown-upload.md", + "posts/rvc.md", + "posts/index-tts2.md", + "posts/hook-steam-drm.md", + "posts/first-pc.md" + ] +} \ No newline at end of file diff --git a/src/content/.obsidian/workspace.json b/src/content/.obsidian/workspace.json new file mode 100644 index 0000000..edb9d39 --- /dev/null +++ b/src/content/.obsidian/workspace.json @@ -0,0 +1,211 @@ +{ + "main": { + "id": "c6989ce27d23c45d", + "type": "split", + "children": [ + { + "id": "7f06a11af95175a9", + "type": "tabs", + "children": [ + { + "id": "96b79d1156396696", + "type": "leaf", + "state": { + "type": "markdown", + "state": { + "file": "posts/random-url-gen.md", + "mode": "source", + "source": false + }, + "icon": "lucide-file", + "title": "random-url-gen" + } + } + ] + } + ], + "direction": "vertical" + }, + "left": { + "id": "adad91cfb262dcee", + "type": "split", + "children": [ + { + "id": "03f1023b2d857cdc", + "type": "tabs", + "children": [ + { + "id": "7be4ba729d2aae5a", + "type": "leaf", + "state": { + "type": "file-explorer", + "state": { + "sortOrder": "alphabetical", + "autoReveal": false + }, + "icon": "lucide-folder-closed", + "title": "文件列表" + } + }, + { + "id": "5174a9cd87b0f8f9", + "type": "leaf", + "state": { + "type": "search", + "state": { + "query": "", + "matchingCase": false, + "explainSearch": false, + "collapseAll": false, + "extraContext": false, + "sortOrder": "alphabetical" + }, + "icon": "lucide-search", + "title": "搜索" + } + }, + { + "id": "f33d418216a86348", + "type": "leaf", + "state": { + "type": "bookmarks", + "state": {}, + "icon": "lucide-bookmark", + "title": "书签" + } + } + ] + } + ], + "direction": "horizontal", + "width": 300 + }, + "right": { + "id": "f6608acce08382a1", + "type": "split", + "children": [ + { + "id": "c4b67f4636c6ac01", + "type": "tabs", + "children": [ + { + "id": "a6b54f415fc99d0e", + "type": "leaf", + "state": { + "type": "backlink", + "state": { + "file": "posts/obsidian.md", + "collapseAll": false, + "extraContext": false, + "sortOrder": "alphabetical", + "showSearch": false, + "searchQuery": "", + "backlinkCollapsed": false, + "unlinkedCollapsed": true + }, + "icon": "links-coming-in", + "title": "obsidian 的反向链接列表" + } + }, + { + "id": "630fd596ceed61e1", + "type": "leaf", + "state": { + "type": "outgoing-link", + "state": { + "file": "posts/obsidian.md", + "linksCollapsed": false, + "unlinkedCollapsed": true + }, + "icon": "links-going-out", + "title": "obsidian 的出链列表" + } + }, + { + "id": "52896168f27b3092", + "type": "leaf", + "state": { + "type": "tag", + "state": { + "sortOrder": "frequency", + "useHierarchy": true, + "showSearch": false, + "searchQuery": "" + }, + "icon": "lucide-tags", + "title": "标签" + } + }, + { + "id": "6f2f79f39b3c9de9", + "type": "leaf", + "state": { + "type": "outline", + "state": { + "file": "posts/obsidian.md", + "followCursor": false, + "showSearch": false, + "searchQuery": "" + }, + "icon": "lucide-list", + "title": "obsidian 的大纲" + } + } + ] + } + ], + "direction": "horizontal", + "width": 300, + "collapsed": true + }, + "left-ribbon": { + "hiddenItems": { + "switcher:打开快速切换": false, + "graph:查看关系图谱": false, + "canvas:新建白板": false, + "daily-notes:打开/创建今天的日记": false, + "templates:插入模板": false, + "command-palette:打开命令面板": false, + "bases:创建新数据库": false + } + }, + "active": "96b79d1156396696", + "lastOpenFiles": [ + "assets/images/random-url-gen-11.png", + "assets/images/random-url-gen-10.png", + "assets/images/random-url-gen-9.png", + "assets/images/random-url-gen-8.png", + "assets/images/random-url-gen-7.png", + "assets/images/random-url-gen-6.png", + "assets/images/random-url-gen-5.png", + "assets/images/random-url-gen-4.png", + "assets/images/random-url-gen-3.png", + "assets/images/random-url-gen-2.png", + "assets/images/random-url-gen-1.png", + "posts/eo-umami.md", + "posts/fuwari.md", + "posts/ddos-6t.md", + "posts/py-uploadserver.md", + "posts/umami-migration.md", + "posts/serverless-function.md", + "posts/pin.md", + "posts/anuneko.md", + "posts/win11-to-win10.md", + "posts/remote.md", + "posts/warden-worker.md", + "posts/expressive-code.md", + "posts/lskypro-local.md", + "posts/ms-e3.md", + "posts/onedrive-index.md", + "posts/why-not-icp.md", + "posts/ipfs.md", + "posts/swup-js.md", + "posts/static-view.md", + "posts/oci.md", + "posts/wx-zfb-card.md", + "posts/mobile-git.md", + "posts/check-notebook.md", + "posts/unknown-upload.md", + "posts/zte-f450-bridge.md" + ] +} \ No newline at end of file diff --git a/src/content/config.ts b/src/content/config.ts new file mode 100644 index 0000000..f7ccadc --- /dev/null +++ b/src/content/config.ts @@ -0,0 +1,38 @@ +import { defineCollection, z } from "astro:content"; + +const postsCollection = defineCollection({ + schema: z.object({ + title: z.string(), + published: z.date(), + updated: z.date().optional(), + draft: z.boolean().optional().default(false), + description: z.string().optional().default(""), + image: z.string().optional().default(""), + tags: z.array(z.string()).optional().default([]), + category: z.union([ + z.string(), + z.array(z.string()), + ]).optional(), + lang: z.string().optional().default(""), + pinned: z.boolean().optional().default(false), + + /* For internal use */ + prevTitle: z.string().default(""), + prevSlug: z.string().default(""), + nextTitle: z.string().default(""), + nextSlug: z.string().default(""), + }), +}); + +const assetsCollection = defineCollection({ + type: 'data', + schema: z.object({ + title: z.string().optional(), + description: z.string().optional(), + }), +}); + +export const collections = { + posts: postsCollection, + assets: assetsCollection, +}; diff --git a/src/content/posts/Golang/Gin框架快速入门.md b/src/content/posts/Golang/Gin框架快速入门.md new file mode 100644 index 0000000..c53c96a --- /dev/null +++ b/src/content/posts/Golang/Gin框架快速入门.md @@ -0,0 +1,1969 @@ +--- +title: Gin框架快速入门 +published: 2025-07-19 +description: '' +image: 'https://blog.meowrain.cn/api/i/2025/07/19/uje4vo-1.webp' +tags: [Gin, Go, 框架, 快速入门] +category: 'Go' +draft: false +lang: '' +--- + +# Go 语言 Web 框架 Gin + +> 参考 +> <https://docs.fengfengzhidao.com> +> +> <https://www.liwenzhou.com/posts/Go/gin/#c-0-7-2> + +# 返回各种值 + +## 返回字符串 + +```go +package main + + + +import ( + + "net/http" + + + + "github.com/gin-gonic/gin" + +) + + + +func main() { + + router := gin.Default() + + router.GET("/", func(c *gin.Context) { + + c.String(http.StatusOK, "helloworld") + + }) + + router.Run(":8080") + +} +``` + +## 返回 json + +```go +package main + + + +import ( + + "net/http" + + + + "github.com/gin-gonic/gin" + +) + + + +type Student struct { + + Name string `json:"name"` + + Age int `json:"age"` + + Number string `json: "number"` + +} + + + +func main() { + + router := gin.Default() + + router.GET("/", func(c *gin.Context) { + + var student Student = Student{ + + Name: "meowrain", + + Age: 20, + + Number: "10086", + + } + + c.JSON(http.StatusOK, student) + + }) + + router.Run(":8080") + +} +``` + +![](https://blog.meowrain.cn/api/i/2024/03/07/vs14ff-3.webp) + +## 返回 map + +```go +package main + + + +import ( + + "net/http" + + + + "github.com/gin-gonic/gin" + +) + + + +func main() { + + router := gin.Default() + + router.GET("/", func(c *gin.Context) { + + userMap := map[string]any{ + + "username": "meowrain", + + "age": 20, + + "number": 10086, + + } + + c.JSON(http.StatusOK, userMap) + + }) + + router.Run(":8080") + +} +``` + +![](https://blog.meowrain.cn/api/i/2024/03/07/vu58ct-3.webp) + +## 返回原始 json + +```go +package main + + + +import ( + + "net/http" + + + + "github.com/gin-gonic/gin" + +) + + + +func main() { + + router := gin.Default() + + router.GET("/", func(c *gin.Context) { + + + + c.JSON(http.StatusOK, gin.H{ + + "username": "meowrain", + + + + "age": 20, + + + + "number": 10086, + + }) + + }) + + router.Run(":8080") + +} +``` + +![](https://blog.meowrain.cn/api/i/2024/03/07/vuxhez-3.webp) + +> ![](https://blog.meowrain.cn/api/i/2024/03/07/vv144f-3.webp) + +## 返回 html 并传递参数 + +```go +package main + + + +import ( + + "net/http" + + + + "github.com/gin-gonic/gin" + +) + + + +func _html(c *gin.Context) { + + type UserInfo struct { + + Username string `json:"username"` + + Age int `json:"age"` + + Password string `json:"-"` + + } + + user := UserInfo{ + + Username: "meowrain", + + Age: 20, + + Password: "12345678", + + } + + c.HTML(http.StatusOK, "index.html", gin.H{"obj": user}) + +} + +func main() { + + router := gin.Default() + + router.LoadHTMLGlob("template/*") + + router.GET("/", _html) + + router.Run(":8080") + +} +``` + +```html +<!DOCTYPE html> + +<html lang="en"> + <head> + <meta charset="UTF-8" /> + + <meta name="viewport" content="width=device-width, initial-scale=1.0" /> + + <title>Document + + + +

User Information

+ +

Username: {{.obj.Username}}

+ +

Age: {{.obj.Age}}

+ + +``` + +![](https://blog.meowrain.cn/api/i/2024/03/07/10gipeq-3.webp) + +### 静态文件配置 + +`router.Static`和`router.StaticFS`都是用于处理静态文件的 Gin 框架路由处理方法,但它们有一些区别。 + +1. **`router.Static`**: + + - 使用 `router.Static` 时,Gin 会简单地将请求的 URL 路径与提供的本地文件系统路径进行映射。通常,这适用于将 URL 路径直接映射到一个静态文件或目录。 + - 示例:`router.Static("/static", "./static")` 将 `/static` 映射到当前工作目录下的 `./static` 文件夹。 + +2. **`router.StaticFS`**: + - `router.StaticFS` 则允许你使用 `http.FileSystem` 对象,这可以提供更多的灵活性。你可以使用 `http.Dir` 创建 `http.FileSystem`,并将其传递给 `router.StaticFS`。 + - 这允许你更灵活地处理静态文件,例如从不同的源(内存、数据库等)加载静态文件,而不仅限于本地文件系统。 + - 示例:`router.StaticFS("/static", http.Dir("/path/to/static/files"))` 使用本地文件系统路径创建一个 `http.FileSystem` 对象,然后将 `/static` 映射到这个文件系统。 + +总体而言,`router.Static`更简单,适用于基本的静态文件服务,而`router.StaticFS`提供了更多的灵活性,允许你自定义静态文件的加载方式。选择使用哪一个取决于你的具体需求。 + +```go +package main + + + +import ( + + "net/http" + + + + "github.com/gin-gonic/gin" + +) + + + +func _html(c *gin.Context) { + + type UserInfo struct { + + Username string `json:"username"` + + Age int `json:"age"` + + Password string `json:"-"` + + } + + user := UserInfo{ + + Username: "meowrain", + + Age: 20, + + Password: "12345678", + + } + + c.HTML(http.StatusOK, "index.html", gin.H{"obj": user}) + +} + +func main() { + + router := gin.Default() + + router.LoadHTMLGlob("template/*") + + router.Static("/static/", "./static") + + router.GET("/", _html) + + router.Run(":8080") + +} +``` + +![](https://blog.meowrain.cn/api/i/2024/03/07/10q03tz-3.webp) + +```html + + + + + + + + + Document + + + +

User Information

+ +

Username: {{.obj.Username}}

+ +

Age: {{.obj.Age}}

+ + + + +``` + +![](https://blog.meowrain.cn/api/i/2024/03/07/10qf6z6-3.webp) + +# 重定向 + +```go +package main + + + +import ( + + "net/http" + + + + "github.com/gin-gonic/gin" + +) + + + +func _html(c *gin.Context) { + + type UserInfo struct { + + Username string `json:"username"` + + Age int `json:"age"` + + Password string `json:"-"` + + } + + user := UserInfo{ + + Username: "meowrain", + + Age: 20, + + Password: "12345678", + + } + + c.HTML(http.StatusOK, "index.html", gin.H{"obj": user}) + +} + +func _redirect(c *gin.Context) { + + c.Redirect(301, "https://www.baidu.com") + +} + +func main() { + + router := gin.Default() + + router.LoadHTMLGlob("template/*") + + router.Static("/static/", "./static") + + router.GET("/", _html) + + router.GET("/baidu", _redirect) + + router.Run(":8080") + +} +``` + +![](https://blog.meowrain.cn/api/i/2024/03/07/10xi5tr-3.webp) + +### 301 和 302 的区别 + +HTTP 状态码中的 301 和 302 分别表示重定向(Redirect)。它们之间的主要区别在于重定向的性质和原因: + +1. **301 Moved Permanently(永久重定向)**: + + - 当服务器返回状态码 301 时,它告诉客户端请求的资源已经被永久移动到新的位置。 + - 客户端收到 301 响应后,应该更新书签、链接等,将这个新的位置作为将来所有对该资源的请求的目标。 + - 搜索引擎在遇到 301 时,通常会更新索引,将原始 URL 替换为新的 URL。 + +2. **302 Found(临时重定向)**: + - 当服务器返回状态码 302 时,它表示请求的资源暂时被移动到了另一个位置。 + - 客户端收到 302 响应后,可以在不更新书签和链接的情况下继续使用原始 URL。 + - 搜索引擎在遇到 302 时,通常会保留原始 URL 在索引中,并不会立即更新为新的 URL。 + +总体来说,使用 301 通常是在确定资源永久移动的情况下,而 302 通常用于暂时性的重定向,即资源可能在将来回到原始位置。选择使用哪种状态码取决于你希望客户端和搜索引擎如何处理被重定向的资源。 + +# 路由 + +## 默认路由 + +> 当访问路径不被匹配的时候返回默认路由内容 + +目录结构 + +![image-20240308205926484](https://blog.meowrain.cn/api/i/2024/03/08/y21i4t-3.webp) + +```go +//main.go +package main + +import ( + "awesomeProject/pkg/controller" + "github.com/gin-gonic/gin" +) + +func main() { + router := gin.Default() + router.LoadHTMLGlob("templates/*") + router.GET("/", func(c *gin.Context) { + c.String(200, "helloworld") + }) + router.NoRoute(controller.Default_route) + router.Run(":80") +} + +``` + +```go +//server.go +package controller + +import ( + "github.com/gin-gonic/gin" + "net/http" +) + +func Default_route(c *gin.Context) { + c.HTML(http.StatusNotFound, "404.html", nil) +} + +``` + +```html + + + + + + 404 NOT FOUND + + +

404 Not Found

+ + +``` + +> 效果 + +![](https://blog.meowrain.cn/api/i/2024/03/08/y25trv-3.webp) + +![image-20240308210004320](https://blog.meowrain.cn/api/i/2024/03/08/yqb64w-3.webp) + +## 路由组 + +> 参考: + +我们可以将拥有共同 URL 前缀的路由划分为一个路由组。习惯性一对`{}`包裹同组的路由,这只是为了看着清晰,你用不用`{}`包裹功能上没什么区别。 + +![](https://blog.meowrain.cn/api/i/2024/03/08/z7tbui-3.webp) + +```go +//main.go +package main + +import ( + "awesomeProject/pkg/controller" + "github.com/gin-gonic/gin" +) + +func main() { + + router := gin.Default() + userGroup := router.Group("/user") + { + userGroup.GET("/all", controller.GetUserList) + userGroup.GET("/detail", controller.GetUserDetail) + } + router.LoadHTMLGlob("templates/*") + router.NoRoute(controller.Default_route) + router.Run(":80") +} + +``` + +```go +//controller/userController.go +package controller + +import ( + . "awesomeProject/pkg/entity" + "github.com/gin-gonic/gin" + "net/http" + "strconv" +) + +func GetUserList(c *gin.Context) { + c.JSON(http.StatusOK, Response{ + Code: http.StatusOK, + Data: UserList, + Msg: "返回成功", + }) +} +func GetUserDetail(c *gin.Context) { + id := c.Query("id") + for _, res := range UserList { + if strconv.Itoa(res.ID) == id { + c.JSON(http.StatusOK, Response{ + Code: http.StatusOK, + Data: res, + Msg: "get successfully", + }) + } + } +} + +``` + +```go +//user.go +package entity + +type User struct { + ID int `json:"id"` + Name string `json:"name"` + Age int `json:"age"` +} + +type Response struct { + Code int `json:"code"` + Data any `json:"data"` + Msg string `json:"msg"` +} + +var UserList []User = []User{ + { + ID: 1, + Name: "meowrian", + Age: 20, + }, + { + ID: 2, + Name: "Mike", + Age: 30, + }, + { + ID: 3, + Name: "Amy", + Age: 23, + }, + { + ID: 4, + Name: "John", + Age: 24, + }, +} + + +``` + +```go +//server.go +package controller + +import ( + "github.com/gin-gonic/gin" + "net/http" +) + +func Default_route(c *gin.Context) { + c.HTML(http.StatusNotFound, "404.html", nil) +} + +``` + +![image-20240308213055236](https://blog.meowrain.cn/api/i/2024/03/08/z8h8cb-3.webp) + +![image-20240308213116279](https://blog.meowrain.cn/api/i/2024/03/08/z8u72c-3.webp) + +> 路由组也是支持嵌套的 + +# 参数 + +## 查询参数 + +```go +package main + + + +import ( + + "net/http" + + + + "github.com/gin-gonic/gin" + +) + + + +func _query(c *gin.Context) { + + user := c.Query("user") + + c.HTML(http.StatusOK, "index.html", gin.H{ + + "user": user, + + }) + +} + +func main() { + + router := gin.Default() + + router.LoadHTMLGlob("template/*") + + router.Static("/static", "./static") + + router.GET("/", _query) + + router.Run(":8080") + +} +``` + +![](https://blog.meowrain.cn/api/i/2024/03/07/112i3vb-3.webp) + +```go +package main + +import ( + "fmt" + "net/http" + "github.com/gin-gonic/gin") + +func _query(c *gin.Context) { + user, ok := c.GetQuery("user") + ids := c.QueryArray("id") //拿到多个相同的查询参数 + maps := c.QueryMap("id") + fmt.Println(maps) + if ok { + c.HTML(http.StatusOK, "index.html", gin.H{ + "user": user, + "id": ids, + }) + } else { + c.String(http.StatusOK, "No query!") + } +} + +func main() { + router := gin.Default() + router.LoadHTMLGlob("template/*") + router.Static("static", "./static") + router.GET("/", _query) + router.Run(":8080") +} +``` + +> 请求为: > ![](https://blog.meowrain.cn/api/i/2024/03/07/12532gz-3.webp) > ![](https://blog.meowrain.cn/api/i/2024/03/07/1267bmh-3.webp) + +## 动态参数 + +```go +package main + +import ( + "fmt" + "github.com/gin-gonic/gin" "net/http") + +func _param(c *gin.Context) { + param := c.Param("user_id") + fmt.Println(param) + c.HTML(http.StatusOK, "index.html", gin.H{ + "param": param, + }) + +} +func main() { + router := gin.Default() + router.LoadHTMLGlob("template/*") + router.Static("static", "./static") + router.GET("/param/:user_id", _param) + router.Run(":8080") +} +``` + +![](https://blog.meowrain.cn/api/i/2024/03/07/12ac8nv-3.webp) + +## 表单参数 PostForm + +```go +package main + +import ( + "github.com/gin-gonic/gin" + "net/http") + +func postForm(c *gin.Context) { + name := c.PostForm("name") + password := c.PostForm("password") + c.JSON(http.StatusOK, gin.H{ + "name": name, + "password": password, + }) +} +func index(c *gin.Context) { + c.HTML(http.StatusOK, "index.html", gin.H{}) +} +func main() { + router := gin.Default() + router.LoadHTMLGlob("template/*") + router.Static("static", "./static") + router.GET("/", index) + router.POST("/post", postForm) + router.Run(":8080") +} +``` + +```html + + + + + + Post Form Test + + +

Post Form Test

+
+ + +
+ + +
+ +
+

Response:

+ + + +``` + +![](https://blog.meowrain.cn/api/i/2024/03/07/12lcebn-3.webp) + +postFormArray 函数 + +```go +package main + +import ( + "github.com/gin-gonic/gin" + "net/http") + +func postForm(c *gin.Context) { + name := c.PostForm("name") + password := c.PostForm("password") + respArr := c.PostFormArray("name") + c.JSON(http.StatusOK, gin.H{ + "name": name, + "password": password, + "respArray": respArr, + }) +} +func index(c *gin.Context) { + c.HTML(http.StatusOK, "index.html", gin.H{}) +} +func main() { + router := gin.Default() + router.LoadHTMLGlob("template/*") + router.Static("static", "./static") + router.GET("/", index) + router.POST("/post", postForm) + router.Run(":8080") +} +``` + +![](https://blog.meowrain.cn/api/i/2024/03/07/12n0072-3.webp) + +## 原始参数 + +```go +/* +原始参数 +*/ +package main + +import ( + "fmt" + "github.com/gin-gonic/gin") + +func _raw(c *gin.Context) { + buf, err := c.GetRawData() + if err != nil { + fmt.Println("error:", err) + return + } + fmt.Println(string(buf)) +} +func main() { + router := gin.Default() + router.POST("/", _raw) + router.Run(":8080") +} +``` + +![](https://blog.meowrain.cn/api/i/2024/03/08/extkqj-3.webp) + +![](https://blog.meowrain.cn/api/i/2024/03/08/exvpa7-3.webp) + +### 解析 json 数据 + +```go +/* +原始参数 +*/ +package main + +import ( + "encoding/json" + "fmt" "github.com/gin-gonic/gin") + +func bindJSON(c *gin.Context, obj any) error { + body, err := c.GetRawData() + contentType := c.GetHeader("Content-Type") + fmt.Println("ContentType:", contentType) + if err != nil { + fmt.Println("error:", err) + return err + } + switch contentType { + case "application/json": + err := json.Unmarshal(body, obj) + if err != nil { + fmt.Println(err.Error()) + return err + } + } + return nil +} + +func raw(c *gin.Context) { + type User struct { + Name string `json:"name"` + Age int `json:"age"` + Password string `json:"-"` + } + var user User + err := bindJSON(c, &user) + if err != nil { + fmt.Println("Error binding JSON:", err) + return + } + fmt.Println(user) +} + +func main() { + router := gin.Default() + router.POST("/", raw) + router.Run(":8080") +} +``` + +![](https://blog.meowrain.cn/api/i/2024/03/08/fkacjn-3.webp) + +![](https://blog.meowrain.cn/api/i/2024/03/08/fki60h-3.webp) + +# 四大请求方式 + +![](https://blog.meowrain.cn/api/i/2024/03/08/fnulpk-3.webp) + +## 简单实现以下 CRUD + +```go +package main + +import ( + "github.com/gin-gonic/gin" + "net/http" "strconv") + +type Article struct { + Id int `json:"id"` + Title string `json:"title"` + Content string `json:"content"` + Author string `json:"author"` +} + +type Response struct { + Code int `json:"code"` + Data any `json:"data"` + Msg string `json:"msg"` +} + +var articleList []Article = []Article{ + { + 1, + "Go语言从入门到精通", + "Learn better", + "Mike Jason", + }, + { + 2, + "Java从入门到精通", + "Java is good", + "Jack Smith", + }, + { + 3, + "Javascript从入门到精通", + "Javascript is a nice programming language!", + "Amy Gorden", + }, + { + 4, + "Python从入门到精通", + "Python is a simple language!", + "Jack Buffer", + }, +} + +/*简单增删改查*/ +func _getList(c *gin.Context) { + + c.JSON(http.StatusOK, Response{Code: 200, Data: articleList, Msg: "获取成功"}) +} +func _getDetail(c *gin.Context) { + id := c.Param("id") + flag := false + for _, res := range articleList { + if strconv.Itoa(res.Id) == id { + flag = true + c.JSON(http.StatusOK, Response{ + Code: 200, + Data: res, + Msg: "获取成功!", + }) + } + } + if flag == false { + c.JSON(404, Response{ + Code: 404, + Data: "Not Found the data", + Msg: "获取失败,因为数据不存在", + }) + } +} +func _create(c *gin.Context) { + id, _ := strconv.ParseInt(c.PostForm("id"), 10, 0) + title := c.PostForm("title") + content := c.PostForm("content") + author := c.PostForm("author") + var article Article = Article{ + Id: int(id), + Title: title, + Content: content, + Author: author, + } + articleList = append(articleList, article) + c.JSON(200, Response{Code: 200, Data: article, Msg: "添加成功!"}) +} +func _delete(c *gin.Context) { + id := c.Param("id") + index := -1 + for i, res := range articleList { + if strconv.Itoa(res.Id) == id { + index = i + break + } + } + if index != -1 { + articleList = append(articleList[:index], articleList[index+1:]...) + c.JSON(http.StatusOK, Response{Code: 200, Data: nil, Msg: "删除成功"}) + } else { + c.JSON(http.StatusNotFound, Response{Code: 404, Data: "Not Found the data", Msg: "删除失败,数据不存在"}) + } +} +func _update(c *gin.Context) { + id, _ := strconv.Atoi(c.Param("id")) + title := c.PostForm("title") + content := c.PostForm("content") + author := c.PostForm("author") + found := false + for i, res := range articleList { + if res.Id == id { + found = true + articleList[i] = Article{ + id, + title, + content, + author, + } + break + } + } + if found { + c.JSON(http.StatusOK, Response{ + Code: 200, + Data: nil, + Msg: "更新成功", + }) + return + } else { + c.JSON(http.StatusNotFound, Response{ + Code: 404, + Data: "Not found the data", + Msg: "更新失败,因为数据不存在", + }) + } + +} + +func main() { + router := gin.Default() + router.GET("/articles", _getList) + router.GET("/articles/:id", _getDetail) + router.POST("/articles", _create) + router.PUT("/articles/:id", _update) + router.DELETE("/articles/:id", _delete) + router.Run(":8080") + +} +``` + +## 文件上传 + +### 上传单个文件 + +```go +package main + +import ( + "awesomeProject/pkg/controller" + "github.com/gin-gonic/gin" +) + +func main() { + + router := gin.Default() + router.LoadHTMLGlob("templates/*") + router.GET("/", func(c *gin.Context) { + c.HTML(200, "upload.html", nil) + }) + router.POST("/upload",controller.Upload_file) + router.NoRoute(controller.Default_route) + router.Run(":80") +} + +``` + +```go +package controller + +import ( + "fmt" + "github.com/gin-gonic/gin" + "log" + "net/http" +) + +func Default_route(c *gin.Context) { + c.HTML(http.StatusNotFound, "404.html", nil) +} +//文件上传 +func Upload_file(c *gin.Context) { + file, err := c.FormFile("f1") + if err != nil { + c.JSON(http.StatusInternalServerError, gin.H{ + "message": err.Error(), + }) + return + } + log.Println(file.Filename) + dst := fmt.Sprintf("./tmp/%s", file.Filename) + c.SaveUploadedFile(file, dst) + c.JSON(http.StatusOK, gin.H{ + "message": fmt.Sprintf("'%s' uploaded", file.Filename), + }) +} + +``` + +```html + + + + 上传文件示例 + + + +
+ + +
+ + + + +``` + +![image-20240308214643811](https://blog.meowrain.cn/api/i/2024/03/08/zhxp6e-3.webp) + +![image-20240308220222511](https://blog.meowrain.cn/api/i/2024/03/08/10f5j2o-3.webp) + +### 上传多个文件 + +```go +package main + +import ( + "awesomeProject/pkg/controller" + "github.com/gin-gonic/gin" +) + +func main() { + + router := gin.Default() + router.LoadHTMLGlob("templates/*") + router.GET("/", func(c *gin.Context) { + c.HTML(200, "upload.html", nil) + }) + router.POST("/upload", controller.UploadFiles) + router.NoRoute(controller.Default_route) + router.Run(":80") +} + +``` + +```go +package controller + +import ( + "fmt" + "github.com/gin-gonic/gin" + "log" + "net/http" +) + +func Default_route(c *gin.Context) { + c.HTML(http.StatusNotFound, "404.html", nil) +} +func UploadFiles(c *gin.Context) { + err := c.Request.ParseMultipartForm(100 << 20) // 100 MB limit + if err != nil { + c.JSON(http.StatusInternalServerError, gin.H{ + "message": err.Error(), + }) + return + } + + form := c.Request.MultipartForm + if form == nil || form.File == nil { + c.JSON(http.StatusBadRequest, gin.H{ + "message": "No files provided in the request", + }) + return + } + + files := form.File["f1"] + + for _, file := range files { + dst := fmt.Sprintf("./tmp/%s", file.Filename) + if err := c.SaveUploadedFile(file, dst); err != nil { + c.JSON(http.StatusInternalServerError, gin.H{ + "message": fmt.Sprintf("Failed to save file %s: %s", file.Filename, err.Error()), + }) + return + } + log.Println(file.Filename) + } + + c.JSON(http.StatusOK, gin.H{ + "message": "Files uploaded successfully", + }) +} + +``` + +```html + + + + 上传文件示例 + + + +
+ + +
+ + + + +``` + +![image-20240308220131880](https://blog.meowrain.cn/api/i/2024/03/08/10em1sq-3.webp) + +![](https://blog.meowrain.cn/api/i/2024/03/08/10enova-3.webp) + +![image-20240308220155287](https://blog.meowrain.cn/api/i/2024/03/08/10er6r4-3.webp) + +### 判断上传文件的类型 + +在 Gin 框架中,可以使用`binding`模块提供的`FormFile`函数来获取上传的文件,然后检查文件的 MIME 类型。具体步骤如下: + +1. 在处理函数中使用`c.FormFile`获取上传的文件: + +```go +file, err := c.FormFile("file") +if err != nil { + c.String(http.StatusBadRequest, "获取文件失败") + return +} +``` + +2. 打开文件并读取文件头部的几个字节,以识别文件的 MIME 类型: + +```go +f, err := file.Open() +if err != nil { + c.String(http.StatusInternalServerError, "打开文件失败") + return +} +defer f.Close() + +buffer := make([]byte, 512) +_, err = f.Read(buffer) +if err != nil { + c.String(http.StatusInternalServerError, "读取文件失败") + return +} +``` + +3. 使用`http.DetectContentType`函数检测文件的 MIME 类型: + +```go +contentType := http.DetectContentType(buffer) +``` + +4. 判断文件类型是否允许: + +```go +allowedTypes := []string{"image/jpeg", "image/png", "application/pdf"} +allowed := false +for _, t := range allowedTypes { + if t == contentType { + allowed = true + break + } +} + +if !allowed { + c.String(http.StatusBadRequest, "不支持的文件类型") + return +} +``` + +完整的示例代码如下: + +```go +func uploadFile(c *gin.Context) { + file, err := c.FormFile("file") + if err != nil { + c.String(http.StatusBadRequest, "获取文件失败") + return + } + + f, err := file.Open() + if err != nil { + c.String(http.StatusInternalServerError, "打开文件失败") + return + } + defer f.Close() + + buffer := make([]byte, 512) + _, err = f.Read(buffer) + if err != nil { + c.String(http.StatusInternalServerError, "读取文件失败") + return + } + + contentType := http.DetectContentType(buffer) + allowedTypes := []string{"image/jpeg", "image/png", "application/pdf"} + allowed := false + for _, t := range allowedTypes { + if t == contentType { + allowed = true + break + } + } + + if !allowed { + c.String(http.StatusBadRequest, "不支持的文件类型") + return + } + + // 处理文件... +} +``` + +在上面的示例中,我们定义了一个允许的 MIME 类型列表`allowedTypes`,包括`image/jpeg`、`image/png`和`application/pdf`。如果上传的文件类型不在允许列表中,就会返回错误响应。你可以根据需求修改允许的文件类型列表。 + +### 使用 gin 编写文件服务器 + +```go +package controller + +import ( + "fmt" + "github.com/gin-gonic/gin" + "log" + "net/http" + "os" +) + +func Default_route(c *gin.Context) { + c.HTML(http.StatusNotFound, "404.html", nil) +} +func UploadFiles(c *gin.Context) { + err := c.Request.ParseMultipartForm(100 << 20) // 100 MB limit + if err != nil { + c.JSON(http.StatusInternalServerError, gin.H{ + "message": err.Error(), + }) + return + } + + form := c.Request.MultipartForm + if form == nil || form.File == nil { + c.JSON(http.StatusBadRequest, gin.H{ + "message": "No files provided in the request", + }) + return + } + + files := form.File["f1"] + + for _, file := range files { + dst := fmt.Sprintf("./tmp/%s", file.Filename) + if err := c.SaveUploadedFile(file, dst); err != nil { + c.JSON(http.StatusInternalServerError, gin.H{ + "message": fmt.Sprintf("Failed to save file %s: %s", file.Filename, err.Error()), + }) + return + } + log.Println(file.Filename) + } + + c.JSON(http.StatusOK, gin.H{ + "message": "Files uploaded successfully", + }) +} +func ListFiles(c *gin.Context) { + // 读取 ./tmp 目录下的所有文件 + files, err := os.ReadDir("./tmp") + if err != nil { + c.String(http.StatusInternalServerError, err.Error()) + return + } + + // 渲染模板 + c.HTML(http.StatusOK, "download.html", gin.H{ + "Files": files, + }) +} + +``` + +```go +package main + +import ( + "awesomeProject/pkg/controller" + "github.com/gin-gonic/gin" +) + +func main() { + r := gin.Default() + + // 设置静态文件路径为 ./tmp + r.Static("/tmp", "./tmp") + + // 设置模板目录 + r.LoadHTMLGlob("templates/*") + + // 定义路由 + r.GET("/", func(c *gin.Context) { + c.HTML(200, "upload.html", nil) + }) + r.POST("/upload", controller.UploadFiles) + + //文件列表服务器 + r.GET("/files", controller.ListFiles) + + // 启动HTTP服务器 + r.Run(":8080") +} + +``` + +```html + + + + + File List + + +

File List

+ + + +``` + +```html + + + + + File List + + + +

File List

+ + + +``` + +![image-20240308222026615](https://blog.meowrain.cn/api/i/2024/03/08/10pw52c-3.webp) + +![image-20240308222225060](https://blog.meowrain.cn/api/i/2024/03/08/10r2oqf-3.webp) + +![image-20240308222527025](https://blog.meowrain.cn/api/i/2024/03/08/10svloq-3.webp) + +# 请求头相关 + +## 获取所有请求头 + +![](https://blog.meowrain.cn/api/i/2024/03/08/iu6r5m-3.webp) + +```go +package main + +import ( + "fmt" + "github.com/gin-gonic/gin" "net/http") + +func main() { + router := gin.Default() + router.LoadHTMLGlob("template/*") + router.Static("static", "./static") + router.GET("/", func(c *gin.Context) { + c.HTML(http.StatusOK, "index.html", gin.H{ + "header": c.Request.Header, + }) + fmt.Println(c.Request.Header) + }) + router.Run(":8080") +} +``` + +```html + + + + + + Post Form Test + + +

Header Test

+

Header: {{.header}}

+ + +``` + +## 绑定参数 bind + +> 绑定 post 发送的 json 数据转换为 Student 结构体的成员变量值,然后再把这个结构体转换为 json 对象 + +```go +package main + +import ( + "fmt" + "github.com/gin-gonic/gin" "net/http") + +type Student struct { + Name string `json:"name"` + Age int `json:"age"` +} + +func main() { + router := gin.Default() + router.POST("/", func(c *gin.Context) { + var stu Student + err := c.BindJSON(&stu) + if err != nil { + fmt.Println("error: ", err) + c.JSON(http.StatusBadGateway, err) + return + } + c.JSON(http.StatusOK, stu) + }) + router.Run(":8080") +} +``` + +![](https://blog.meowrain.cn/api/i/2024/03/08/shcott-3.webp) + +> 绑定查询参数 + +```go +package main + +import ( + "fmt" + "github.com/gin-gonic/gin" "net/http") + +type Student struct { + Name string `json:"name" form:"name"` + Age int `json:"age" form:"age"` +} + +func main() { + router := gin.Default() + router.GET("/", func(c *gin.Context) { + var stu Student + err := c.BindQuery(&stu) + if err != nil { + fmt.Println("error: ", err) + c.JSON(http.StatusBadGateway, err) + return + } + c.JSON(http.StatusOK, stu) + }) + router.Run(":8080") +} +``` + +![](https://blog.meowrain.cn/api/i/2024/03/08/sjnhl2-3.webp) + +> bind URI + +```go +package main + +import ( + "fmt" + "github.com/gin-gonic/gin" "net/http") + +type Student struct { + Name string `json:"name" form:"name" uri:"name"` + Age int `json:"age" form:"age" uri:"age"` +} + +func main() { + router := gin.Default() + router.GET("/uri/:name/:age", func(c *gin.Context) { + var stu Student + err := c.ShouldBindUri(&stu) + if err != nil { + fmt.Println("error: ", err) + c.JSON(http.StatusBadGateway, err) + return + } + c.JSON(http.StatusOK, stu) + }) + router.Run(":8080") +} +``` + +![](https://blog.meowrain.cn/api/i/2024/03/08/sm69pi-3.webp) + +## 常用验证器 + +``` +// 不能为空,并且不能没有这个字段 +required: 必填字段,如:binding:"required" + +// 针对字符串的长度 +min 最小长度,如:binding:"min=5" +max 最大长度,如:binding:"max=10" +len 长度,如:binding:"len=6" + +// 针对数字的大小 +eq 等于,如:binding:"eq=3" +ne 不等于,如:binding:"ne=12" +gt 大于,如:binding:"gt=10" +gte 大于等于,如:binding:"gte=10" +lt 小于,如:binding:"lt=10" +lte 小于等于,如:binding:"lte=10" + +// 针对同级字段的 +eqfield 等于其他字段的值,如:PassWord string `binding:"eqfield=Password"` +nefield 不等于其他字段的值 + + +- 忽略字段,如:binding:"-" +``` + +```go +package main + +import ( + "github.com/gin-gonic/gin" + "net/http") + +type User struct { + Name string `json:"name" binding:"required"` + Password string `json:"password" binding:"eqfield=Re_Password"` + Re_Password string `json:"re_password"` +} +type Response struct { + Code int `json:"code"` + Data any `json:"data"` + Msg string `json:"msg"` +} + +func main() { + router := gin.Default() + router.POST("/login", func(c *gin.Context) { + var user User + err := c.ShouldBindJSON(&user) + if err != nil { + c.JSON(http.StatusBadGateway, Response{ + Code: http.StatusBadGateway, + Data: err.Error(), + Msg: "bad response", + }) + return + } + c.JSON(http.StatusOK, Response{ + Code: http.StatusOK, + Data: user, + Msg: "post successfully", + }) + }) + router.Run(":8080") +} +``` + +> 密码相同 +> ![](https://blog.meowrain.cn/api/i/2024/03/08/t2zolw-3.webp) + +> 密码不同 +> ![](https://blog.meowrain.cn/api/i/2024/03/08/t3ebk2-3.webp) + +> 我们看到报错对用户不是很友好,我们可以自定义验证的错误信息 +> +> TODO + +## [gin 内置验证器](https://docs.fengfengzhidao.com/#/docs/Gin%E6%A1%86%E6%9E%B6%E6%96%87%E6%A1%A3/4.bind%E7%BB%91%E5%AE%9A%E5%99%A8?id=gin%e5%86%85%e7%bd%ae%e9%aa%8c%e8%af%81%e5%99%a8) + +``` +// 枚举 只能是red 或green +oneof=red green + +// 字符串 +contains=fengfeng // 包含fengfeng的字符串 +excludes // 不包含 +startswith // 字符串前缀 +endswith // 字符串后缀 + +// 数组 +dive // dive后面的验证就是针对数组中的每一个元素 + +// 网络验证 +ip +ipv4 +ipv6 +uri +url +// uri 在于I(Identifier)是统一资源标示符,可以唯一标识一个资源。 +// url 在于Locater,是统一资源定位符,提供找到该资源的确切路径 + +// 日期验证 1月2号下午3点4分5秒在2006年 +datetime=2006-01-02 +``` + +--- + +# Gin 中间件 + +> + +Gin 中的中间件必须是一个`gin.HandlerFunc`类型。 + +Gin 框架允许开发者在处理请求的过程中,加入用户自己的钩子(Hook)函数。这个钩子函数就叫中间件,中间件适合处理一些公共的业务逻辑,比如登录认证、权限校验、数据分页、记录日志、耗时统计等。 diff --git a/src/content/posts/Golang/Go_map底层结构.md b/src/content/posts/Golang/Go_map底层结构.md new file mode 100644 index 0000000..bb22ffc --- /dev/null +++ b/src/content/posts/Golang/Go_map底层结构.md @@ -0,0 +1,326 @@ +--- +title: Go_map底层结构 +published: 2025-07-19 +description: '' +image: 'https://blog.meowrain.cn/api/i/2025/07/19/uje4vo-1.webp' +tags: [切片, Golang, Go] +category: 'Go' +draft: false +lang: '' +--- +# Golang map底层数据结构 + + + +[Golang map 实现原理](https://mp.weixin.qq.com/s?__biz=MzkxMjQzMjA0OQ==&mid=2247483868&idx=1&sn=6e954af8e5e98ec0a9d9fc5c8ceb9072&chksm=c10c4f02f67bc614ff40a152a848508aa1631008eb5a600006c7552915d187179c08d4adf8d7&scene=0&xtrack=1&subscene=90#rd) + +## 概述 + +map是一种常用的数据结构,核心特征包括下面三点: + +- 存储基于key-value对映射的模式 +- 基于key维度实现存储数据的去重 +- 读,写,删操作控制,时间复杂度O(1) + +![image-20250402212335440](https://blog.meowrain.cn/api/i/2025/04/02/n5y1Lh1743600215837401704.avif) + +### 初始化方法 + +```go +map1 := make(map[string]int) + +map2 := map[string]int{ + "m1": 1, + "m2":2, +} + +``` + +### key 类型要求 + +map中,key的数据类型必须是可以比较的类型,slice,chan,func,map不可比较,所以不能作为map的key + +![image-20250402210528197](https://blog.meowrain.cn/api/i/2025/04/02/fFJnr51743599129052146367.avif) + +![image-20250402210536019](https://blog.meowrain.cn/api/i/2025/04/02/3eTMZz1743599137424628575.avif) + +![image-20250402210601926](https://blog.meowrain.cn/api/i/2025/04/02/yP4UYM1743599162191602503.avif) + +![image-20250402210607311](https://blog.meowrain.cn/api/i/2025/04/02/KZhyJp1743599167648587617.avif) + +![image-20250402210620988](https://blog.meowrain.cn/api/i/2025/04/02/QFpAk21743599181398433044.avif) + +# 核心原理 + +map又称为hash map,算法上基于hash实现key的映射和寻址,在数据结构上基于桶数组实现key-value对的存储 + +以一组key-value对写入map的流程进行简述: + +1. 通过哈希方法去的key的hash值‘ +2. hash值对同数组长度取模,确定它所属的桶 +3. 在桶中插入key value对 + +![图片](https://blog.meowrain.cn/api/i/2025/04/02/MmAiV11743599321939050023.avif) + +## hash + +hash 译作散列,是一种将任意长度的输入压缩到某一固定长度的输出摘要的过程,由于这种转换属于压缩映射,输入空间远大于输出空间,因此不同输入可能会映射成相同的输出结果. 此外,hash在压缩过程中会存在部分信息的遗失,因此这种映射关系具有不可逆的特质. + +1. hash的可重入性: 相同的key,必然产生相同的hash值 +2. hash的离散性: 只要两个key不相同,不论他们相似度的高低,产生的hash值会在整个输出域内均匀地离散化 +3. hash的单向性: 企图通过hash值反向映射会key是无迹可寻的。 +4. hash冲突: 由于输入域无穷大,输出域有限,必然存在不同key映射到相同hash值的情况,这种情况叫做哈希冲突 + +![图片](https://blog.meowrain.cn/api/i/2025/04/02/RV0Syj1743599459574600284.avif) + +## 桶数组 + +map中,会通过长度为2的整数次幂的桶数组进行key-value对的存储 + +1. 每个桶固定可以存放8个key-value对 +2. 倘若超过8个key-value对打到桶数组的同一个索引当中,此时会通过创建桶链表的方式来化解这个问题。 + +![图片](https://blog.meowrain.cn/api/i/2025/04/02/X7NMOa1743599952016346994.avif) + +## 拉链法解决hash冲突 + +首先,由于hash冲突的存在,不同的key可能存在相同的hash值 + +再者,hash值会对桶数组长度取模,因此不同的hash值可能被打到同一个桶中 + +综上,不同的key-value可能被映射到map的同一个桶当中。 + +拉链法中,将命中同一个桶的元素通过链表的形式进行连接,因此便于动态扩展 + +> 只有当一个桶已经满了(8 个 kv 对),并且又有新的 key 哈希到这个桶时,才会创建溢出桶,并将新的 key-value 对存储到溢出桶中,然后将该溢出桶链接到原桶的尾部。 后续再有冲突的 kv 对,也会被添加到溢出桶或者新的溢出桶中,形成一个链表。 + +![img](https://blog.meowrain.cn/api/i/2025/04/02/lgobAo1743600543664079674.avif) + +## 开放寻址法解决hash冲突 + +> 开放寻址法是一种解决哈希冲突的方法,它在哈希表中寻找另一个空闲位置存储冲突的元素,也就是说,所有元素都直接存储在哈希表的桶中 +> +> 开放寻址法是一种在哈希表中解决冲突的方法。当两个不同的键映射到同一个索引位置时,就会发生冲突。开放寻址法不是使用链表等额外的数据结构来存储冲突的键值对,而是尝试在哈希表本身中寻找一个空闲的位置来存储新的键值对。 + +![图片](https://blog.meowrain.cn/api/i/2025/04/02/GNSRsu1743600902616857141.avif) + +常见开放寻址技术: + +- 线性寻址: 如果在索引`i`发生冲突,线性探测会依次检查`i+1`,`i+2`,`i+3`等位置,直到找到一个空闲的槽位 +- 二次探测检查 `i + 1^2`、`i + 2^2`、`i + 3^2` 等位置。与线性探测相比,这有助于减少聚集现象。 +- 双重哈希: 双重哈希使用第二个哈希函数来确定探测的步长。如果第一个哈希函数在索引`i`导致哈希冲突,第二个哈希函数hash2(key)用于确定探测的间隔(例如,`i + hash2(key)`、`i + 2*hash2(key)`、`i + 3*hash2(key)` 等)。 + +![image-20250402213515236](https://blog.meowrain.cn/api/i/2025/04/02/lsNJNR1743600915626536212.avif) + +我们的golang map解决哈希冲突的方式结合了拉链法和开放寻址法。 + +- 桶: map的底层数据结构是一个桶数组,每个桶严格意义上是一个单向桶链表 +- 桶的大小: 每个桶可以固定存放8个key value对 +- 当key命中一个桶的时候,首先根据开放寻址法,在桶的8个位置中寻找空位进行插入 +- 倘若8个位置都已经被占满,就基于桶的溢出桶指针,找到下一个桶(重复第三步) +- 倘若遍历到链表尾部,还没找到空位,就用拉链法,在桶链表尾部接入新桶,并且插入key-value对 + +![image-20250402215431186](https://blog.meowrain.cn/api/i/2025/04/02/PB9PuR1743602071901331051.avif) + +![图片](https://blog.meowrain.cn/api/i/2025/04/02/Xlpg4R1743602154258822359.avif) + +## 扩容性能优化 + +倘若map的桶数组长度固定不变,那么随着key-value对数量的增长,当一个桶下挂载的key-value达到一定的量级,此时操作的时间复杂度会趋于线性,无法满足诉求。 + +**桶数组长度固定不变 + key-value 对数量持续增加 => 哈希冲突加剧 => Bucket 链表变长 => 查找/插入/删除 需要遍历长链表 => 操作时间复杂度接近 O(n) (线性)** + +因此在设计上,map桶的数组长度会随着key-value对的数量变化而实时调整。保证每个桶内的key-value对数量始终控制在常量级别。 + +扩容类型分为: + +- 增量扩容 +- 等量扩容 + +### 增量扩容 + +触发条件: `key-value总数 / 桶数组长度 > 6.5`的时候,发生增量扩容 + +扩容方式: 桶数组长度增长为原来的`两倍` + +目的: 减少负载因子,降低平均查找时间 + +负载因子: `key-value总数 / 桶的数量` + +![image-20250402225053461](https://blog.meowrain.cn/api/i/2025/04/02/exh1He1743605454120683710.avif) + +### 等量扩容 + +触发条件: 当桶内溢出桶数量大于等于2^B时(B 为桶数组长度的指数,B 最大取 15),发生等量扩容。) + +扩容方式: 桶的长度保持为原来的值 + +**目的:** 解决哈希冲突严重的问题,可能由于哈希函数选择不佳导致大量 key 映射到相同的桶,即使负载因子不高,也会出现大量溢出桶。 等量扩容旨在重新组织数据,减少溢出桶的数量。 + +![image-20250402231943679](https://blog.meowrain.cn/api/i/2025/04/02/m4tdlZ1743607184556640257.avif) + +![image-20250402231929805](https://blog.meowrain.cn/api/i/2025/04/02/7Rrm4l1743607170611676452.avif) + +### 渐进式扩容 + +![image-20250402233251365](https://blog.meowrain.cn/api/i/2025/04/02/8hpZdr1743607972891808021.avif) + +![图片](https://blog.meowrain.cn/api/i/2025/04/02/2Cb2MO1743608023551743628.avif) + +# 数据结构 + +## hmap + +```go +type hmap struct { + count int // map中键值对的数量 + flags uint8 // map的状态标志位,用来指示map的当前状态(正在写入,正在扩容等) + B uint8 // buckets 数组的对数大小,2^B 是buckets数组的长度,比如B是5,那么桶数组的长度就是2^5 = 32 + noverflow uint16 //溢出桶数量的近似值 用来判断是否需要扩容 + hash0 uint32 // 哈希种子 + buckets unsafe.Pointer //指向bucket数组的指针,数组大小为2 ^ B,如果count == 0,那么buckets可能为nil + oldbuckets unsafe.Pointer // 如果发生扩容,指向旧的buckets数组 + nevacuate uintptr // 扩容的时候,表示旧buckcet数组已经迁移到新bucket数组的数量计数器 + extra *mapextra // 可选字段,用来保存overflow buckets的信息 +} +``` + +flags: map状态标识,其包含的主要状态为(这里面牵扯到很多概念还没有涉及,可以先大致的了解一下各自的含义) + +- iterator(`0b0001`): 当前map可能正在被遍历 +- oldIterator(`0b0010`): 当前map的旧桶可能正在被遍历 +- hashWrting(`0b0100`): 一个goroutine正在向map中写入数据 +- sameSizeGrow(`0b1000`): 等量扩容标志字段 + +## bmap + +![](https://blog.meowrain.cn/api/i/2025/04/04/Nb8mWR1743757559555396698.avif) + +![](https://blog.meowrain.cn/api/i/2025/04/04/R3jihc1743757664615047610.avif) + +> bmap就是map中的桶,可以存储8组key-value对数据,以及一个只想下一个溢出桶的指针 + +![](https://blog.meowrain.cn/api/i/2025/04/04/cH27qX1743757980953367677.avif) + +每一组key-value对数据包含key高8位hash值tophash,key,value三部分 + +我们来看看bmap(桶)的内存模型 + +![](https://blog.meowrain.cn/api/i/2025/04/04/4iwDeb1743757807687319535.avif) + +如果按照 `key/value/key/value/...` 这样的模式存储,那在每一个 key/value 对之后都要额外 padding 7 个字节;而将所有的 key,value 分别绑定到一起,这种形式 `key/key/.../value/value/...`,则只需要在最后添加 padding。 + +每个 bucket 设计成最多只能放 8 个 key-value 对,如果有第 9 个 key-value 落入当前的 bucket,那就需要再构建一个 bucket ,通过 `overflow` 指针连接起来。 + +### tophash的作用? + +是key 哈希值的高8位 + +tophash的核心作用是**判断一个键是否可能存在于当前桶中,从而优化查询效率。** + +## 溢出桶数据结构 mapextra + +在map初始化的时候会根据初始数据量不同,自动创建不同数量的溢出桶。在物理结构上初始的正常同和溢出桶是连续存放的,正常桶和溢出桶之间的关系是靠链表来维护的。 + +> `mapextra` 就是在扩容时提供了一批预备的 `bmap`,然后利用 `bmap.overflow` 把它们链接起来。 + +```go +type mapextra struct { + overflow *[]*bmap // overflow buckets 的指针数组 + oldoverflow *[]*bmap // 旧的 overflow buckets 的指针数组 + + nextOverflow *bmap // 指向空闲的 overflow bucket +} + +``` + +在map初始化的时候,倘若容量过大,会提前申请好一批溢出桶,供后续使用,这部分溢出桶存放在hmap.mapextra当中: + +mapextra.overflow 是一个指向溢出桶切片的指针,这个切片里面的溢出桶是当前使用的,用于存储hmap.buckets中的桶的溢出数据。 + +mapextra.oldoverflow 也是一个指向溢出桶切片的指针,但是它指向的是旧的桶数组的溢出桶。 + +nextOverflow指向下一个可用的溢出桶 + +![](https://blog.meowrain.cn/api/i/2025/04/04/eZLvxe1743757352736850834.avif) + +--- + +# 什么是哈希种子? + +哈希种子(hash seed)是一个随机生成的数值,被用作哈希函数的一部分,来增加哈希值的随机性和不可预测性,可以把它理解为哈希函数的“盐” + +# go map 如何根据key的哈希值确定键值存储到哪个桶中? + +## 哈希值的作用 + +- 首先,当你在 Go map 中插入一个键值对时,Go runtime 会对键进行哈希运算,生成一个哈希值(一个整数)。 优秀的哈希函数应该能够将不同的键尽可能均匀地映射到不同的哈希值,以减少哈希碰撞的概率。 +- 这个哈希值是确定键值对存储位置的关键。 + +## go map 数据结构中hmap 中B的作用 + +我们通过哈希值的低B位作为bucket数组的索引, 来选择键值该存储到哪个bucket中。 + +公式 `bucketIndex = hash & ((1 << B) - 1)` + +上面的公式 用来**保留 `hash` 的低 `B` 位,并将其他位设置为 0**。 + +![image-20250402234237409](https://blog.meowrain.cn/api/i/2025/04/02/Vtatge1743608558267235069.avif) + +# key定位过程 + +key经过哈希计算后得到哈希值,共64个bit位,计算它到底要落在哪个桶的时候,只会用到最后B个bit位(log2BucketCount) + +例如,现在有一个key经过哈希函数计算后,得到的哈希结果是: + +``` + 10010111 | 000011110110110010001111001010100010010110010101010 │ 01010 +``` + +而我们的B是5,也就是有2^5 = 32个桶 + +取最后五位,也就是 **01010** 转换为10进制也就是10,也就是 **10号桶**,这个操作其实是 **取余操作**,但是取余数开销太大,就用上面的位运算代替了。 + +接下来我们再用 **hash值的高8位**找到key在 **10号桶**中的位置 **1001011转换为10进制也就是 75**.最开始桶内还没有 key,新加入的 key 会找到第一个空位,放入。 + +![](https://blog.meowrain.cn/api/i/2025/04/04/JcLsW91743759107613607466.avif) + +![](https://blog.meowrain.cn/api/i/2025/04/04/dCIofJ1743759450720106234.avif) + +# 流程 + +![](https://blog.meowrain.cn/api/i/2025/04/05/VUcqQy1743839544909227250.avif) + +# 写入流程 + +写入流程: + +- 进行hmap是否为nil的检查,如果为空,就触发panic +- 进行并发读写的检查,倘若已经设置了并发读写标记,就抛出"concurrent map writes"异常。 +- 处理桶迁移。如果正在扩容,把key所在的旧桶数据迁移到新桶,同时迁移index位h.nevacuate的桶,迁移完成后h.nevacuate自增。更新迁移进度。如果所有桶迁移完毕,清除正在扩容的标记。 +- 查找 key 所在的位置,并记录桶链表的第一个空闲位置(若此 key 之前不存在,则将该位置作为插入位置)。 +- 若此 key 在桶链表中不存在,判断是否需要扩容,若溢出桶过多,则进行相同容量的扩容,否则进行双倍容量的扩容。 +- 若桶链表没有空闲位置,则申请溢出桶来存放 key - value 对。 +- 设置 key 和 tophash[i] 的值。 +- 返回 value 的地址。 + +# 删除流程 + +删除流程: + +- 进行并发读写检查。 +- 处理桶迁移,如果map处于正在扩容的状态,就迁移两个桶 +- 定位key所在的位置 +- 删除kv对的占用,这里是伪删除,只有在下次扩容的时候,被删除的key所占用的同空间才会得到释放。 +- map首先会将对应位置的tophash[i]设置为emptyOne,表示该位置被删除 +- 如果tophash[i]后面还有有效的节点,就仅设置为emptyOne标志,意味着这个节点后面仍然存在有效的key-value对 ,后续在查找某个key的时候,这个节点只后仍然需要继续查找 +- 要是tophash[i]是桶链表的最后一个有效节点,那么从这个节点往前遍历,将链表最后面所有标志位emptyOne的位置,都设置为emptyRest。这样在查找某个key的时候,emptyRest之后的节点不需要继续查找。 + +> - **`emptyOne`:** 表示当前 cell 是空的,但**不能保证**后面的 cell 也是空的。 +> - **`emptyRest`:** 表示当前 cell 是空的,并且**保证**后面的所有 cell 也是空的,直到遇到一个非空 cell 或者到达桶的末尾。 + +# 迭代流程 + +在每次对 map 进行循环时,会调用 mapiterinit 函数,以确定迭代从哪个桶以及桶内的哪个位置起始。由于 mapiterinit 内部是通过随机数来决定起始位置的,所以 map 循环是无序的,每次循环所返回的 key - value 对的顺序都各不相同。 + +![](https://blog.meowrain.cn/api/i/2025/04/05/TABXTR1743840105513843585.avif) diff --git a/src/content/posts/Golang/Go_slice切片原理.md b/src/content/posts/Golang/Go_slice切片原理.md new file mode 100644 index 0000000..34fc001 --- /dev/null +++ b/src/content/posts/Golang/Go_slice切片原理.md @@ -0,0 +1,189 @@ +--- +title: Go_slice切片原理 +published: 2025-07-19 +description: '' +image: 'https://blog.meowrain.cn/api/i/2025/07/19/uje4vo-1.webp' +tags: [切片, Golang, Go] +category: 'Go' +draft: false +lang: '' +--- +# slice数据结构 + +数据结构 +我们每定义一个slice变量,golang底层都会构建一个slice结构的对象。slice结构体由3个成员变量构成: + +array表示数组指针,数组用于存储数据。 +len表示切片长度,也就是数组index从0到len-1已存储数据。 +cap表示切片容量,当切片长度超过最大容量时,需要扩容申请更大长度的数组。 + +```go +type slice struct { + array unsafe.Pointer // 数组指针 + len int // 切片长度 + cap int // 切片容量 +} +``` + +# 扩容原理 + +切片的扩容流程源码位于 runtime/slice.go 文件的 growslice 方法当中,其中核心步骤如下: + +• 倘若扩容后预期的新容量小于原切片的容量,则 panic + +• 倘若切片元素大小为 0(元素类型为 struct{}),则直接复用一个全局的 zerobase 实例,直接返回 + +• 倘若预期的新容量超过老容量的两倍,则直接采用预期的新容量 + +• 倘若老容量小于 256,则直接采用老容量的2倍作为新容量 + +• 倘若老容量已经大于等于 256,则在老容量的基础上扩容 1/4 的比例并且累加上 192 的数值,持续这样处理,直到得到的新容量已经大于等于预期的新容量为止 + +• 结合 mallocgc 流程中,对内存分配单元 mspan 的等级制度,推算得到实际需要申请的内存空间大小 + +• 调用 mallocgc,对新切片进行内存初始化 + +• 调用 memmove 方法,将老切片中的内容拷贝到新切片中 + +• 返回扩容后的新切片 + +```go +// nextslicecap computes the next appropriate slice length. +func nextslicecap(newLen, oldCap int) int { + newcap := oldCap // 将新容量初始化为旧容量 + doublecap := newcap + newcap // 计算旧容量的两倍 + + // 如果所需的新长度大于旧容量的两倍,则直接使用所需的新长度 + if newLen > doublecap { + return newLen + } + + const threshold = 256 // 定义一个阈值,用于区分小切片和大切片 + + // 如果旧容量小于阈值,则直接将新容量设置为旧容量的两倍 + // 这种策略适用于小切片,可以快速扩容,减少扩容次数 + if oldCap < threshold { + return doublecap + } + + // 对于大切片,使用更平滑的扩容策略,避免过度分配内存 + // 从 2 倍增长过渡到 1.25 倍增长。 此公式给出了两者之间的平滑过渡。 + for { + // 每次循环,将新容量增加 (newcap + 3*threshold) / 4 + // 相当于 newcap 增加 1/4 的比例,再加上 3/4 的 threshold(256),即 192 + // 这样可以在一定程度上减少内存浪费,并保证切片的增长 + newcap += (newcap + 3*threshold) >> 2 + + // Check for overflow and determine if the new calculated capacity + // is greater or equal to the required new length. + // newLen is guaranteed to be larger than zero, hence + // when newcap overflows then `uint(newcap) > uint(newLen)`. + // This allows to check for both with the same comparison. + + // 我们需要检查`newcap >= newLen`以及`newcap`是否溢出。 + // 保证 newLen 大于零,因此当 newcap 溢出时,'uint(newcap) > uint(newLen)'。 + // 这允许使用相同的比较来检查两者。 + + // 检查新容量是否大于等于所需的新长度,并且检查是否发生了溢出 + if uint(newcap) >= uint(newLen) { + break // 如果新容量足够大,或者发生了溢出,则退出循环 + } + } + + // 当新容量计算溢出时,将新容量设置为请求的容量。 + // 如果计算过程中发生了溢出,则直接将新容量设置为所需的新长度,以确保切片能够容纳所有元素 + if newcap <= 0 { + return newLen + } + + return newcap // 返回计算得到的新容量 +} +``` + +# Golang 切片原理 + +![](https://blog.meowrain.cn/api/i/2025/01/27/STHBnZ1737969258402080877.avif) + +![](https://blog.meowrain.cn/api/i/2025/01/27/L5OPBU1737969429035465587.avif) + +## 扩容规律 + +![](https://blog.meowrain.cn/api/i/2025/01/27/my5VWv1737969803395420365.avif) + +## 切片作为参数 + +Go 语言的函数参数传递,只有值传递,没有引用传递,切片作为参数也是如此 + +我们来验证这一点 + +![](https://blog.meowrain.cn/api/i/2025/01/27/34ZRq21737970293711745015.avif) + +```go +package main + +import "fmt" + +func main() { + sl := []int{6, 6, 6} + f(sl) + fmt.Println(sl) +} + +func f(sl []int) { + for i := 0; i < 3; i++ { + sl = append(sl, i) + } + fmt.Println(sl) +} + +``` + +可以看到,输出的 sl 的值是不一样的,也就是说,f 函数没能修改主函数中的 sl 变量,而只是修改了形参 sl 变量的内容 + +当我们传递一个切片给函数的时候,函数接收到的其实是这个切片的一个副本,但是他们的 array 字段指向的是同一个底层数组。 + +这意味着,如果我们修改底层数组,是会影响到实参和形参的。 + +我们看下面的例子:形参通过改变底层数组影响实参 + +```go +package main + +import "fmt" + +func main() { + sl := []int{6, 6, 6} + f(sl) + fmt.Println(sl) +} + +func f(sl []int) { + sl[1] = 1 + sl[2] = 2 +} + +``` + +![](https://blog.meowrain.cn/api/i/2025/01/27/f395pe1737970003488259606.avif) + +### 通过指针传递影响实参 + +```go +package main + +import "fmt" + +func main() { + sl := []int{6, 6, 6} + f(&sl) + fmt.Println(sl) +} + +func f(sl *[]int) { + *sl = append(*sl, 200) +} + + +``` + +![](https://blog.meowrain.cn/api/i/2025/01/27/igiBeJ1737970227764617103.avif) diff --git a/src/content/posts/Golang/Golang垃圾回收机制.md b/src/content/posts/Golang/Golang垃圾回收机制.md new file mode 100644 index 0000000..3038ad9 --- /dev/null +++ b/src/content/posts/Golang/Golang垃圾回收机制.md @@ -0,0 +1,244 @@ +--- +title: Golang垃圾回收机制 +published: 2025-07-19 +description: '' +image: 'https://blog.meowrain.cn/api/i/2025/07/19/uje4vo-1.webp' +tags: [垃圾回收, Golang, GC] +category: 'Go' +draft: false +lang: '' +--- + +# Go GC机制 + +> [5、Golang三色标记混合写屏障GC模式全分析 (yuque.com)](https://www.yuque.com/aceld/golang/zhzanb#77fdf35b) + +> 垃圾回收(Garbage Collection,简称GC)是编程语言中提供的自动的内存管理机制,自动释放不需要的内存对象,让出存储器资源。GC过程中无需程序员手动执行。GC机制在现代很多编程语言都支持,GC能力的性能与优劣也是不同语言之间对比度指标之一。 + +## 发展过程 + +Go V1.3之前的标记-清除(mark and sweep)算法,Go V1.3之前的标记-清扫(mark and sweep)的缺点 + +## Go V1.3之前的标记-清除(mark and sweep)算法 + +![image-20240709121221919](https://blog.meowrain.cn/api/i/2024/07/09/C6W4Y71720498342584015950.webp) + +接下来我们来看一下在Golang1.3之前的时候主要用的普通的标记-清除算法,此算法主要有两个主要的步骤: + +- 标记(Mark phase) +- 清除(Sweep phase) + +![image-20240709120731505](https://blog.meowrain.cn/api/i/2024/07/09/ANh9c11720498052447247658.webp) + +![image-20240709120757145](https://blog.meowrain.cn/api/i/2024/07/09/yWrUwk1720498077557020958.webp) + +> STW会对可达对象做上标记,然后对不可达对象进行GC回收 + +![image-20240709120900088](https://blog.meowrain.cn/api/i/2024/07/09/29Wcxv1720498140387778591.webp) + +> 操作非常简单,但是有一点需要额外注意:mark and sweep算法在执行的时候,需要程序暂停!即 `STW(stop the world)`,STW的过程中,CPU不执行用户代码,全部用于垃圾回收,这个过程的影响很大,所以STW也是一些回收机制最大的难题和希望优化的点。所以在执行第三步的这段时间,程序会暂定停止任何工作,卡在那等待回收执行完毕。 + +### mark and sweep 算法 缺点 + +1. STW会让程序暂停,使程序出现卡顿(重要问题) +2. 标记需要扫描整个heap +3. 清除数据会产生heap碎片 + +stw暂停范围 + +![image-20240709121953696](https://blog.meowrain.cn/api/i/2024/07/09/kMFipT1720498794174933847.webp) + +从上图来看,全部的GC时间都是包裹在STW范围之内的,这样貌似程序暂停的时间过长,影响程序的运行性能。所以Go V1.3 做了简单的优化,将STW的步骤提前, 减少STW暂停的时间范围.如下所示 + +![54-STW2.png](https://blog.meowrain.cn/api/i/2024/07/09/rI4lNh1720498833454407229.webp) + +上图主要是将STW的步骤提前了一步,因为在Sweep清除的时候,可以不需要STW停止,因为这些对象已经是不可达对象了,不会出现回收写冲突等问题。 + +但是无论怎么优化,Go V1.3都面临这个一个重要问题,就是**mark-and-sweep 算法会暂停整个程序** 。 + +Go是如何面对并这个问题的呢?接下来G V1.5版本 就用**三色并发标记法**来优化这个问题. + +## GoV1.5三色标记法 + +![image-20240709122423404](https://blog.meowrain.cn/api/i/2024/07/09/u0ZJ951720499063811507708.webp) + +![image-20240709122647686](https://blog.meowrain.cn/api/i/2024/07/09/MRhIFy1720499208514108528.webp) + +![image-20240709122753872](https://blog.meowrain.cn/api/i/2024/07/09/Z6DyjS1720499274479089970.webp) + +![image-20240709122920596](https://blog.meowrain.cn/api/i/2024/07/09/OPgFix1720499361118341644.webp) + +![image-20240709123017964](https://blog.meowrain.cn/api/i/2024/07/09/ZkEIjD1720499418393168076.webp) + +![image-20240709123108479](https://blog.meowrain.cn/api/i/2024/07/09/wULnvE1720499469045471792.webp) + +![image-20240709123127729](https://blog.meowrain.cn/api/i/2024/07/09/VpPh5n1720499488250837040.webp) + +![image-20240709123144258](https://blog.meowrain.cn/api/i/2024/07/09/lGPm8C1720499504716064921.webp) + +![image-20240709123228889](https://blog.meowrain.cn/api/i/2024/07/09/qkpbys1720499549310981229.webp) + +## 三色标记法无STW的问题 + +我们加入如果没有STW,那么也就不会再存在性能上的问题,那么接下来我们假设如果三色标记法不加入STW会发生什么事情? +我们还是基于上述的三色并发标记法来说, 他是一定要依赖STW的. 因为如果不暂停程序, 程序的逻辑改变对象引用关系, 这种动作如果在标记阶段做了修改,会影响标记结果的正确性,我们来看看一个场景,如果三色标记法, 标记过程不使用STW将会发生什么事情? + +我们把初始状态设置为已经经历了第一轮扫描,目前黑色的有对象1和对象4, 灰色的有对象2和对象7,其他的为白色对象,且对象2是通过指针p指向对象3的,如图所示。 + +![55-三色标记问题1.jpeg](https://blog.meowrain.cn/api/i/2024/07/09/V3y0mh1720502068945434434.webp) + +现在如何三色标记过程不启动STW,那么在GC扫描过程中,任意的对象均可能发生读写操作,如图所示,在还没有扫描到对象2的时候,已经标记为黑色的对象4,此时创建指针q,并且指向白色的对象3。 + +![56-三色标记问题2.jpeg](https://blog.meowrain.cn/api/i/2024/07/09/FKfHeh1720502103967556957.webp) + +与此同时灰色的对象2将指针p移除,那么白色的对象3实则就是被挂在了已经扫描完成的黑色的对象4下,如图所示。 + +![57-三色标记问题3.jpeg](https://blog.meowrain.cn/api/i/2024/07/09/vja2PL1720502115722049746.webp) + +然后我们正常指向三色标记的算法逻辑,将所有灰色的对象标记为黑色,那么对象2和对象7就被标记成了黑色,如图所示。 + +![58-三色标记问题4.jpeg](https://blog.meowrain.cn/api/i/2024/07/09/w2ane51720502140258068700.webp) + +那么就执行了三色标记的最后一步,将所有白色对象当做垃圾进行回收,如图所示。 + +![59-三色标记问题5.jpeg](https://blog.meowrain.cn/api/i/2024/07/09/bzlj9c1720502156829093691.webp) + +但是最后我们才发现,本来是对象4合法引用的对象3,却被GC给“误杀”回收掉了。 + +### GC误杀条件 + +可以看出,有两种情况,在三色标记法中,是不希望被发生的。 + +- 条件1: 一个白色对象被黑色对象引用**(白色被挂在黑色下)** +- 条件2: 灰色对象与它之间的可达关系的白色对象遭到破坏**(灰色同时丢了该白色)** + 如果当以上两个条件同时满足时,就会出现对象丢失现象! + +## 屏障机制 + +> 为了防止这种现象的发生,最简单的方式就是STW,直接禁止掉其他用户程序对对象引用关系的干扰,但是**STW的过程有明显的资源浪费,对所有的用户程序都有很大影响**。那么是否可以在保证对象不丢失的情况下合理的尽可能的提高GC效率,减少STW时间呢?答案是可以的,我们只要使用一种机制,尝试去破坏上面的两个必要条件就可以了。 + +![image-20240709132144714](https://blog.meowrain.cn/api/i/2024/07/09/2cO2yL1720502505096278545.webp) + +### 强三色不变式 + +强制性的不允许黑色对象引用白色对象 + +> 破坏条件1 + +![image-20240709131813359](https://blog.meowrain.cn/api/i/2024/07/09/iZbHhI1720502294165051623.webp) + +### 弱三色不变式 + +黑色对象可以引用白色对象,但是要保证白色独享存在其它灰色对象对它的引用,或者可达它的链路上游存在灰色对象 + +> 破坏条件2 + +![image-20240709132012351](https://blog.meowrain.cn/api/i/2024/07/09/SwzQBu1720502412929353413.webp) + +为了遵循上述的两个方式,GC算法演进到两种屏障方式,他们“插入屏障”, “删除屏障”。 + +![image-20240709133322663](https://blog.meowrain.cn/api/i/2024/07/09/JjkcAo1720503203424780995.webp) + +### 插入屏蔽 + +> 不在栈上使用 + +`具体操作`: 在A对象引用B对象的时候,B对象被标记为灰色。(将B挂在A下游,B必须被标记为灰色) + +`满足`: **强三色不变式**. (不存在黑色对象引用白色对象的情况了, 因为白色会强制变成灰色) + +```go +添加下游对象(当前下游对象slot, 新下游对象ptr) { + //1 + 标记灰色(新下游对象ptr) + + //2 + 当前下游对象slot = 新下游对象ptr +} +``` + +这里说一下这个过程,首先因为插入屏障不在栈上使用 + +下面的图里面,已经进行了一次三色标记,外界向对象4添加对象8,对象1添加对象9,但是我们知道,对象1在栈上,所以它不会应用插入屏障,也就是说,这个时候对象 9不会按照插入屏障的规则设置为灰色,而对象4在堆上,因此它会应用插入屏障,所以会把对象8设置为灰色,然后我们进行第二次三色标记,从灰色对象出发(对象2,对象7,对象8) ,找可达对象(对象3),因此将对象3设置为灰色,然后对象2,7,8设置为黑色,接着进行第三次三色标记,从灰色对象出发(对象3),发现没有可达对象,因此设置对象3为黑色,这个时候我们有黑色对象: 对象1,对象2,对象3,对象4,对象7,对象8. + +按照常理我们这个时候应该进行垃圾回收了对吧,其实不然,我们这个时候要把栈空间的对象全部设置为白色,然后使用STW暂停栈空间(对象1,对象2,对象3,对象9,对象5),防止外界干扰(再有对象被添加到黑色对象下) + +然后我们对栈空间重新进行一次三色标记,直到没有灰色对象 + +过程如下: + +从对象1出发,设置对象1为灰色,接下来看从对象1走的可达对象,发现可达对象有对象2和对象9,因此我们把对象2和对象9设置为灰色对象,把对象1设置为黑色对象,然后我们再从灰色对象出发(对象2和对象9),发现对象2可达对象3,对象9没有可达对象,因此把对象3设置为灰色对象,对象2,9设置为黑色对象,接下来从灰色对象(此时只有对象3)出发,发现对象3没有可达对象,设置对象3为黑色对象。至此栈里面已经没有灰色对象,我们先暂停STW,然后进行最后的GC回收,可以发现白色对象只有 对象5,对象6,因此对白色对象进行清除。 + +至此,GC三色标记并发情况下的插入屏障流程完毕 + +![image-20240709135123289](https://blog.meowrain.cn/api/i/2024/07/09/LKKoCr1720504284631136649.webp) + +![image-20240709135153851](https://blog.meowrain.cn/api/i/2024/07/09/c9akf61720504314509112134.webp) + +![image-20240709135240616](https://blog.meowrain.cn/api/i/2024/07/09/9ggDq01720504361239129518.webp) + +![image-20240709135330243](https://blog.meowrain.cn/api/i/2024/07/09/brrrcs1720504410886565715.webp) + +![image-20240709135410526](https://blog.meowrain.cn/api/i/2024/07/09/huazYX1720504451233838741.webp) + +![image-20240709135448742](https://blog.meowrain.cn/api/i/2024/07/09/WENeFq1720504489239707269.webp) + +![image-20240709135535312](https://blog.meowrain.cn/api/i/2024/07/09/AYQ3tv1720504535821911058.webp) + +### 删除屏蔽 + +`具体操作`: 被删除的对象,如果自身为灰色或者白色,那么被标记为灰色。 + +`满足`: **弱三色不变式**. (保护灰色对象到白色对象的路径不会断) + +``` +添加下游对象(当前下游对象slot, 新下游对象ptr) { + //1 + if (当前下游对象slot是灰色 || 当前下游对象slot是白色) { + 标记灰色(当前下游对象slot) //slot为被删除对象, 标记为灰色 + } + + //2 + 当前下游对象slot = 新下游对象ptr +} +``` + +![72-三色标记删除写屏障1.jpeg](https://blog.meowrain.cn/api/i/2024/07/09/YIsQlm1720506425416637589.webp) + +![73-三色标记删除写屏障2.jpeg](https://blog.meowrain.cn/api/i/2024/07/09/l7zqib1720506436481765589.webp) +![74-三色标记删除写屏障3.jpeg](https://blog.meowrain.cn/api/i/2024/07/09/9CZbQB1720506459636243158.webp) + +![75-三色标记删除写屏障4.jpeg](https://blog.meowrain.cn/api/i/2024/07/09/jsDCqs1720506469140624748.webp) + +![76-三色标记删除写屏障5.jpeg](https://blog.meowrain.cn/api/i/2024/07/09/ccDlph1720506476790209274.webp) + +![77-三色标记删除写屏障6.jpeg](https://blog.meowrain.cn/api/i/2024/07/09/zWf7Gz1720506482597765808.webp) + +![78-三色标记删除写屏障7.jpeg](https://blog.meowrain.cn/api/i/2024/07/09/bLHxhy1720506492796935675.webp) + +这种方式的回收精度低,一个对象即使被删除了最后一个指向它的指针也依旧可以活过这一轮,在下一轮GC中被清理掉。 + +### 混合屏障Go V1.8 + +插入写屏障和删除写屏障的短板: + +● 插入写屏障:结束时需要STW来重新扫描栈,标记栈上引用的白色对象的存活; +● 删除写屏障:回收精度低,GC开始时STW扫描堆栈来记录初始快照,这个过程会保护开始时刻的所有存活对象。 + +Go V1.8版本引入了混合写屏障机制(hybrid write barrier),避免了对栈re-scan的过程,极大的减少了STW的时间。结合了两者的优点。 + +![image-20240709142925523](https://blog.meowrain.cn/api/i/2024/07/09/mIzEEG1720506565775368039.webp) + +![79-三色标记混合写屏障1.jpeg](https://blog.meowrain.cn/api/i/2024/07/09/WfkvFx1720506886093721996.webp) + +![80-三色标记混合写屏障2.jpeg](https://blog.meowrain.cn/api/i/2024/07/09/mhPr4L1720506893765506689.webp) + +`具体操作`: + +1、GC开始将栈上的对象全部扫描并标记为黑色(之后不再进行第二次重复扫描,无需STW), + +2、GC期间,任何在栈上创建的新对象,均为黑色。 + +3、被删除的对象标记为灰色。 + +4、被添加的对象标记为灰色。 diff --git a/src/content/posts/Java/BigDecimal高精度计算.md b/src/content/posts/Java/BigDecimal高精度计算.md new file mode 100644 index 0000000..f102870 --- /dev/null +++ b/src/content/posts/Java/BigDecimal高精度计算.md @@ -0,0 +1,374 @@ +--- +title: BigDecimal高精度计算 +published: 2025-07-26 +description: '' +image: '' +tags: ['Java', '面试','高精度计算','BigDecimal'] +category: 'Java > 面试题' +draft: false +lang: '' +--- +https://javaguide.cn/java/basis/bigdecimal.html + +# BigDecimal详解 +Java中,浮点数的运算有精度丢失的风险 + +为什么浮点数运算的时候会有精度丢失的风险? + 计算机是二进制的,浮点数在计算机中是通过二进制的方式来表示的。但是,浮点数的表示方式是有限的,所以在进行浮点数运算的时候,会存在精度丢失的风险。 + + 例如,在Java中,浮点数的表示方式是 IEEE 754 标准,使用 64 位二进制来表示一个浮点数。其中,1 位用于表示符号位,11 位用于表示指数位,52 位用于表示尾数位。但是,浮点数的表示方式是有限的,所以在进行浮点数运算的时候,会存在精度丢失的风险。 + +# BigDecimal 类的常用方法 +BigDecimal可以实现对小数的运算,不会造成精度损失 + +通常情况下,大部分需要小数精确运算结果的业务场景都是通过BigDecimal来做的。 + +《阿里巴巴 Java 开发手册》中提到:浮点数之间的等值判断,基本数据类型不能用 == 来比较,包装数据类型不能用 equals 来判断。 + +## 创建 +我们在使用BigDecimal的时候,需要注意以下几点: + +1. 不能使用new BigDecimal(double)的方式来创建BigDecimal对象,因为double类型的精度是有限的,所以在创建BigDecimal对象的时候,会存在精度丢失的风险。 + +2. 可以使用new BigDecimal(String)的方式来创建BigDecimal对象,因为String类型的精度是无限的,所以在创建BigDecimal对象的时候,不会存在精度丢失的风险。 + +3. 可以使用BigDecimal的valueOf()方法来创建BigDecimal对象,因为valueOf()方法的参数是double类型,但是在内部会将double类型的参数转换为String类型,所以在创建BigDecimal对象的时候,不会存在精度丢失的风险。 + +![](https://blog.meowrain.cn/api/i/2025/07/26/zjj9sd-1.webp) + +## 加减乘除 +add +subtract +multiply +divide + +divide可以指定保留的小数位数,以及四舍五入的方式。 +```java +public BigDecimal divide(BigDecimal divisor, int scale, RoundingMode roundingMode) { + return divide(divisor, scale, roundingMode.oldMode); +} +``` + +我们使用 divide 方法的时候尽量使用 3 个参数版本,并且RoundingMode 不要选择 UNNECESSARY,否则很可能会遇到 ArithmeticException(无法除尽出现无限循环小数的时候),其中 scale 表示要保留几位小数,roundingMode 代表保留规则。 + +scale是保留几位小数,roundingMode是保留规则。 + +roundingMode: +- UP 向上四舍五入 +- DOWN 向下截取 +- CEILING 向上截取 +- FLOOR 向下截取 +- HALF_UP 四舍五入 +- HALF_DOWN 五舍六入 +- HALF_EVEN 四舍六入五取偶 + +# RoundingMode枚举详解 📚📊 + +## 各种舍入模式详细说明 + +### 1. UP - 向上舍入 ⬆️ +```java +// 绝对值增大方向舍入,远离零的方向 +2.4 -> 3 // 正数向上 +1.6 -> 2 // 正数向上 +-1.6 -> -2 // 负数向更小(绝对值更大) +-2.4 -> -3 // 负数向更小 +``` + +### 2. DOWN - 向下舍入 ⬇️ +```java +// 绝对值减小方向舍入,趋向零的方向 +2.4 -> 2 // 正数向下 +1.6 -> 1 // 正数向下 +-1.6 -> -1 // 负数向更大(绝对值更小) +-2.4 -> -2 // 负数向更大 +``` + +### 3. CEILING - 向正无穷舍入 ☁️ +```java +// 向数轴右侧舍入 +2.4 -> 3 // 正数向上 +1.6 -> 2 // 正数向上 +-1.6 -> -1 // 负数向更大(向右) +-2.4 -> -2 // 负数向更大 +``` + +### 4. FLOOR - 向负无穷舍入 ⚡ +```java +// 向数轴左侧舍入 +2.4 -> 2 // 正数向下 +1.6 -> 1 // 正数向下 +-1.6 -> -2 // 负数向更小(向左) +-2.4 -> -3 // 负数向更小 +``` + +### 5. HALF_UP - 四舍五入 🎯 +```java +// 遇5向上舍入 +2.4 -> 2 // 小于5,向下 +2.5 -> 3 // 等于5,向上 +2.6 -> 3 // 大于5,向上 +-1.5 -> -2 // 负数也一样,-1.5 -> -2 +-1.4 -> -1 // -1.4 -> -1 +``` + +## 实际代码示例 💡 + +```java +import java.math.BigDecimal; +import java.math.RoundingMode; + +public class RoundingModeDemo { + public static void main(String[] args) { + BigDecimal[] testNumbers = { + new BigDecimal("2.4"), + new BigDecimal("2.5"), + new BigDecimal("2.6"), + new BigDecimal("-1.4"), + new BigDecimal("-1.5"), + new BigDecimal("-1.6") + }; + + for (BigDecimal num : testNumbers) { + System.out.println("\n原数: " + num); + System.out.println("UP: " + num.setScale(0, RoundingMode.UP)); + System.out.println("DOWN: " + num.setScale(0, RoundingMode.DOWN)); + System.out.println("CEILING: " + num.setScale(0, RoundingMode.CEILING)); + System.out.println("FLOOR: " + num.setScale(0, RoundingMode.FLOOR)); + System.out.println("HALF_UP: " + num.setScale(0, RoundingMode.HALF_UP)); + } + } +} +``` + +## 输出结果展示 📊 + +``` +原数: 2.4 +UP: 3 +DOWN: 2 +CEILING: 3 +FLOOR: 2 +HALF_UP: 2 + +原数: 2.5 +UP: 3 +DOWN: 2 +CEILING: 3 +FLOOR: 2 +HALF_UP: 3 + +原数: -1.5 +UP: -2 +DOWN: -1 +CEILING: -1 +FLOOR: -2 +HALF_UP: -2 +``` + +## 使用场景建议 🎯 + +```java +public class RoundingMode应用场景 { + public static void main(String[] args) { + // 金融计算 - 通常使用HALF_UP(银行家舍入) + BigDecimal money = new BigDecimal("123.455"); + BigDecimal roundedMoney = money.setScale(2, RoundingMode.HALF_UP); + + // 统计计算 - 可能使用HALF_EVEN(银行家舍入) + BigDecimal average = new BigDecimal("87.345"); + BigDecimal roundedAvg = average.setScale(2, RoundingMode.HALF_EVEN); + + // 科学计算 - 根据需要选择合适的模式 + BigDecimal scientific = new BigDecimal("99.999"); + BigDecimal ceilingResult = scientific.setScale(2, RoundingMode.CEILING); + } +} +``` + + + +# BigDecimal等值比较 +使用compareTo进行比较,因为equals会比较值和精度,但是compareTo会忽略精度 + +compareTo() 方法可以比较两个 BigDecimal 的值,如果相等就返回 0,如果第 1 个数比第 2 个数大则返回 1,反之返回-1。 + +# BigDecimal工具类 +```java +import java.math.BigDecimal; +import java.math.RoundingMode; + +/** + * 简化BigDecimal计算的小工具类 + */ +public class BigDecimalUtil { + + /** + * 默认除法运算精度 + */ + private static final int DEF_DIV_SCALE = 10; + + private BigDecimalUtil() { + } + + /** + * 提供精确的加法运算。 + * + * @param v1 被加数 + * @param v2 加数 + * @return 两个参数的和 + */ + public static double add(double v1, double v2) { + BigDecimal b1 = BigDecimal.valueOf(v1); + BigDecimal b2 = BigDecimal.valueOf(v2); + return b1.add(b2).doubleValue(); + } + + /** + * 提供精确的减法运算。 + * + * @param v1 被减数 + * @param v2 减数 + * @return 两个参数的差 + */ + public static double subtract(double v1, double v2) { + BigDecimal b1 = BigDecimal.valueOf(v1); + BigDecimal b2 = BigDecimal.valueOf(v2); + return b1.subtract(b2).doubleValue(); + } + + /** + * 提供精确的乘法运算。 + * + * @param v1 被乘数 + * @param v2 乘数 + * @return 两个参数的积 + */ + public static double multiply(double v1, double v2) { + BigDecimal b1 = BigDecimal.valueOf(v1); + BigDecimal b2 = BigDecimal.valueOf(v2); + return b1.multiply(b2).doubleValue(); + } + + /** + * 提供(相对)精确的除法运算,当发生除不尽的情况时,精确到 + * 小数点以后10位,以后的数字四舍六入五成双。 + * + * @param v1 被除数 + * @param v2 除数 + * @return 两个参数的商 + */ + public static double divide(double v1, double v2) { + return divide(v1, v2, DEF_DIV_SCALE); + } + + /** + * 提供(相对)精确的除法运算。当发生除不尽的情况时,由scale参数指 + * 定精度,以后的数字四舍六入五成双。 + * + * @param v1 被除数 + * @param v2 除数 + * @param scale 表示表示需要精确到小数点以后几位。 + * @return 两个参数的商 + */ + public static double divide(double v1, double v2, int scale) { + if (scale < 0) { + throw new IllegalArgumentException( + "The scale must be a positive integer or zero"); + } + BigDecimal b1 = BigDecimal.valueOf(v1); + BigDecimal b2 = BigDecimal.valueOf(v2); + return b1.divide(b2, scale, RoundingMode.HALF_EVEN).doubleValue(); + } + + /** + * 提供精确的小数位四舍六入五成双处理。 + * + * @param v 需要四舍六入五成双的数字 + * @param scale 小数点后保留几位 + * @return 四舍六入五成双后的结果 + */ + public static double round(double v, int scale) { + if (scale < 0) { + throw new IllegalArgumentException( + "The scale must be a positive integer or zero"); + } + BigDecimal b = BigDecimal.valueOf(v); + BigDecimal one = new BigDecimal("1"); + return b.divide(one, scale, RoundingMode.HALF_UP).doubleValue(); + } + + /** + * 提供精确的类型转换(Float) + * + * @param v 需要被转换的数字 + * @return 返回转换结果 + */ + public static float convertToFloat(double v) { + BigDecimal b = new BigDecimal(v); + return b.floatValue(); + } + + /** + * 提供精确的类型转换(Int)不进行四舍六入五成双 + * + * @param v 需要被转换的数字 + * @return 返回转换结果 + */ + public static int convertsToInt(double v) { + BigDecimal b = new BigDecimal(v); + return b.intValue(); + } + + /** + * 提供精确的类型转换(Long) + * + * @param v 需要被转换的数字 + * @return 返回转换结果 + */ + public static long convertsToLong(double v) { + BigDecimal b = new BigDecimal(v); + return b.longValue(); + } + + /** + * 返回两个数中大的一个值 + * + * @param v1 需要被对比的第一个数 + * @param v2 需要被对比的第二个数 + * @return 返回两个数中大的一个值 + */ + public static double returnMax(double v1, double v2) { + BigDecimal b1 = new BigDecimal(v1); + BigDecimal b2 = new BigDecimal(v2); + return b1.max(b2).doubleValue(); + } + + /** + * 返回两个数中小的一个值 + * + * @param v1 需要被对比的第一个数 + * @param v2 需要被对比的第二个数 + * @return 返回两个数中小的一个值 + */ + public static double returnMin(double v1, double v2) { + BigDecimal b1 = new BigDecimal(v1); + BigDecimal b2 = new BigDecimal(v2); + return b1.min(b2).doubleValue(); + } + + /** + * 精确对比两个数字 + * + * @param v1 需要被对比的第一个数 + * @param v2 需要被对比的第二个数 + * @return 如果两个数一样则返回0,如果第一个数比第二个数大则返回1,反之返回-1 + */ + public static int compareTo(double v1, double v2) { + BigDecimal b1 = BigDecimal.valueOf(v1); + BigDecimal b2 = BigDecimal.valueOf(v2); + return b1.compareTo(b2); + } + +} +``` diff --git a/src/content/posts/Java/JUC/ABA问题.md b/src/content/posts/Java/JUC/ABA问题.md new file mode 100644 index 0000000..a1a4b4d --- /dev/null +++ b/src/content/posts/Java/JUC/ABA问题.md @@ -0,0 +1,271 @@ +--- +title: ABA问题 +published: 2025-07-19 +description: '' +image: '' +tags: [JUC, ABA问题] +category: 'Java > JUC' +draft: false +lang: '' +--- + +# 介绍 + +ABA问题是并发编程中,在使用无锁(lock-free)算法,特别是基于 比较并交换(Compare-And-Swap, CAS) 操作时可能出现的一种逻辑错误。 + +它之所以被称为"ABA"问题,是因为一个变量的值从 A 变成了 B,然后又变回了 A。对于一个只检查当前值是否等于期望值的CAS操作来说,它会认为值没有发生变化,从而成功执行操作,但实际上变量在期间已经被修改过了。 + +## **ABA问题发生的场景及危害** + +想象一个无锁的栈(Stack),其 `pop()` 操作需要原子地更新栈顶元素。 + +**假设初始状态:** +栈顶 `top` 指向元素 `A`。 + +**正常 `pop` 操作流程:** + +1. 线程1读取当前栈顶元素 `A`。 +2. 线程1准备将栈顶更新为 `A.next` (假设是 `null`)。 +3. 线程1执行 `top.compareAndSet(A, A.next)`,如果成功,`A` 被弹出。 + +**ABA问题发生过程:** + +1. **线程1** 读取当前栈顶元素,发现是 `A`。它记下 `A`,并准备执行 `CAS(A, C)`。 + + ``` + top -> A -> B -> D + Thread 1 reads top: A + ``` + +2. **线程2** 此时突然执行,它将 `A` 弹出。 + + ``` + top -> B -> D (A is now removed) + Thread 2 pops A + ``` + +3. **线程2** 又将一个**新的元素 `A` (或者一个值和 `A` 相同但实际上是不同对象的元素)**压入栈。 + *注意:这里的“新的元素A”指的是一个与最开始的A值相同,但内存地址可能不同,或者即便内存地址相同,其内部状态已经发生过变化的对象。* + + ``` + top -> A -> B -> D (This A is NOT the original A, it's a new one!) + Thread 2 pushes A back + ``` + +4. **线程1** 恢复执行 `CAS(A, C)`。它检查当前栈顶是否是它之前读取的 `A`。 + 由于栈顶现在又指向了 `A`(尽管是新的 `A`),`compareAndSet` 操作会认为当前值等于期望值 `A`,并成功将栈顶更新为 `C`。 + + ``` + top -> C (Thread 1's CAS(A, C) succeeds!) + ``` + +**危害:** +尽管线程1的CAS操作成功了,但它操作的实际上是一个**新的 `A`**,而不是它最初读取的那个 `A`。如果 `A` 的内部状态(比如它的 `next` 指针)在这期间被改变了,那么线程1的后续操作可能会导致: + +* **数据结构损坏**:例如,在链表中,节点指针可能指向错误的位置。 +* **逻辑错误**:程序基于过时的或不正确的状态信息做出决策。 +* **内存泄漏**:旧的 `A` (或其他被弹出又压入的元素)可能永远无法被垃圾回收。 + +--- + +```java +package org.example.aba; + +import java.util.concurrent.atomic.AtomicReference; + +class Node { + public final String item; // 节点内容 + public Node next; // 下一个节点的引用 + + public Node(String item) { + this.item = item; + } + + @Override + public String toString() { + return item; + } +} + +class LockFreeStackABA { + private AtomicReference top = new AtomicReference<>(); + + // 压入栈顶 + public void push(String item) { + Node newHead = new Node(item); + Node oldHead; + do { + oldHead = top.get(); + newHead.next = oldHead; + } while (!top.compareAndSet(oldHead, newHead)); + System.out.println(Thread.currentThread().getName() + " 压入: " + item + " (当前栈顶: " + top.get() + ")"); + } + + // 弹出栈顶 + public Node pop() { + Node oldHead; + Node newHead; + do { + oldHead = top.get(); + if (oldHead == null) { + System.out.println(Thread.currentThread().getName() + " 尝试弹出,但栈为空!"); + return null; + } + newHead = oldHead.next; + System.out.println(Thread.currentThread().getName() + " 尝试弹出 " + oldHead.item + + " (期望栈顶: " + oldHead + ", 更新栈顶至: " + newHead + ")"); + } while (!top.compareAndSet(oldHead, newHead)); // CAS操作:如果当前栈顶仍是oldHead,则更新为newHead + System.out.println(Thread.currentThread().getName() + " 成功弹出: " + oldHead.item + " (当前栈顶: " + top.get() + ")"); + return oldHead; + } + + // 打印栈内容 + public void printStack() { + System.out.print("当前栈: "); + Node current = top.get(); + if (current == null) { + System.out.println("空"); + return; + } + StringBuilder sb = new StringBuilder(); + while (current != null) { + sb.append(current.item).append(" -> "); + current = current.next; + } + sb.setLength(sb.length() - 4); // 移除最后的 " -> " + System.out.println(sb.toString()); + } + + // 获取栈顶节点 + public Node getTop() { + return top.get(); + } +} + +public class AbaAppear { + public static void main(String[] args) throws InterruptedException { + LockFreeStackABA stack = new LockFreeStackABA(); + + // 1. 初始状态:栈中逐步压入 A、B、C + stack.push("C"); // 栈顶:C + stack.push("B"); // 栈顶:B → C + stack.push("A"); // 栈顶:A → B → C + + Node originalNodeA = stack.getTop(); // 获取当前栈顶的 A 节点引用 + + System.out.println("\n--- 初始栈内容 ---"); + stack.printStack(); + + // 2. 线程1 启动,读取栈顶元素后等待 + Thread thread1 = new Thread(() -> { + Node readNode = stack.getTop(); // 线程1在原栈中看到栈顶元素 A + System.out.println("\n线程-1 读取到栈顶节点: " + readNode); + try { + Thread.sleep(200); // 等待线程2的干扰行为发生 + } catch (InterruptedException e) { + e.printStackTrace(); + } + System.out.println("\n线程-1 开始尝试弹出栈顶节点..."); + Node popNode = stack.pop(); // 线程1尝试弹出栈顶(被线程2修改为新 A) + if (readNode == popNode) { + System.out.println("同一个节点"); + }else { + System.out.println("不是同一个节点,ABA问题已重现!"); + } + }, "线程-1"); + + // 3. 线程2 启动,执行 ABA 序列 + Thread thread2 = new Thread(() -> { + System.out.println("\n--- 线程-2 执行 ABA 序列 ---"); + stack.pop(); // 弹出 A,栈顶变为 B + stack.pop(); // 弹出 B,栈顶变为 C + stack.push("X"); // 压入一个新节点 X,栈顶变为 X → C + stack.push("A"); // 再压入一个新的 A,栈顶变为 A → X → C + System.out.println("--- 线程-2 完成 ABA 序列 ---"); + stack.printStack(); + }, "线程-2"); + + thread1.start(); // 启动线程1 + thread2.start(); // 启动线程2 + + thread1.join(); // 等待线程1完成 + thread2.join(); // 等待线程2完成 + + System.out.println("\n--- 最终栈内容 ---"); + stack.printStack(); + System.out.println("当前栈顶节点: " + stack.getTop()); + if (stack.getTop() != null) { + System.out.println("栈顶节点的 next: " + stack.getTop().next); + } + } +} + +``` + +![](https://blog.meowrain.cn/api/i/2025/05/28/117anny-0.webp) + +![](https://blog.meowrain.cn/api/i/2025/05/28/11banhc-0.webp) + +![](https://blog.meowrain.cn/api/i/2025/05/28/11betve-0.webp) + +# 如何解决ABA问题 + +解决ABA问题的主要方法是引入一个 版本号(或时间戳) 机制。每次修改变量时,不仅修改值,也同时修改版本号。CAS操作时,需要同时比较值和版本号。 + +![](https://blog.meowrain.cn/api/i/2025/05/28/10lo3io-0.webp) + +使用AtomicStampedReference解决问题 + +```java +package org.example.aba; + +import lombok.extern.slf4j.Slf4j; + +import java.util.concurrent.TimeUnit; +import java.util.concurrent.atomic.AtomicStampedReference; + +@Slf4j +public class AbaSolve { + static AtomicStampedReference ref = new AtomicStampedReference<>("A", 0); + + public static void main(String[] args) { + log.debug("main start ...."); + String prev = ref.getReference(); + int stamp = ref.getStamp(); + log.debug("stamp: {}", stamp); + + other(); + try { + TimeUnit.SECONDS.sleep(1); + } catch (InterruptedException e) { + throw new RuntimeException(e); + } + log.debug("change A -> C {} ", ref.compareAndSet(prev, "C", stamp, stamp + 1)); + } + + private static void other() { + new Thread(() -> { + int stamp = ref.getStamp(); + log.debug("{} 's stamp is : {}",Thread.currentThread().getName(),stamp); + log.debug("change A-> B {} ", ref.compareAndSet(ref.getReference(), "B", stamp, stamp + 1)); + stamp = ref.getStamp(); + log.debug("{} 's changed stamp is : {}",Thread.currentThread().getName(),stamp); + }, "t1").start(); + try { + TimeUnit.MILLISECONDS.sleep(500); + } catch (InterruptedException e) { + throw new RuntimeException(e); + } + new Thread(()->{ + int stamp = ref.getStamp(); + log.debug("{} 's stamp is : {}",Thread.currentThread().getName(),stamp); + log.debug("change B->A {}",ref.compareAndSet(ref.getReference(),"A",stamp,stamp + 1)); + stamp = ref.getStamp(); + log.debug("{} 's changed stamp is : {}",Thread.currentThread().getName(),stamp); + },"t2").start(); + } +} + +``` + +![](https://blog.meowrain.cn/api/i/2025/05/31/t2rzcb-0.webp) diff --git a/src/content/posts/Java/JUC/ConcurrentHashMap1.7和1.8的区别.md b/src/content/posts/Java/JUC/ConcurrentHashMap1.7和1.8的区别.md new file mode 100644 index 0000000..c6d9537 --- /dev/null +++ b/src/content/posts/Java/JUC/ConcurrentHashMap1.7和1.8的区别.md @@ -0,0 +1,48 @@ +--- +title: ConcurrentHashMap1.7和1.8的区别 +published: 2025-09-15 +description: '' +image: '' +tags: [Java,ConcurrentHashMap] +category: 'Java' +draft: false +lang: '' +--- + +# JDK1.7 +ConcurrentHashMap用的是分段锁,每个Segment是独立的,可以并发访问不同的Segment,默认是16个Segment,所以最多有16个线程可以并发执行。 + +![](https://blog.meowrain.cn/api/i/2025/09/15/119ff4d-1.webp) + +先通过key的hash判断得到Segment数组的下标,将这个Segment上锁,然后再次通过key的hash得到Segment里面HashEntry数组的下标。可以这么理解:每个Segment数组存放的就是一个单独的HashMap + +缺点是Segment数组一旦初始化了之后就不会扩容,只有HashEntry数组会扩容,这就导致并发度过于死板 + +# JDK1.8 +移除了分段锁,锁的粒度更加细化,锁只在链表或者红黑树**节点级别**上进行。通过CAS进行插入操作,只有在更新链表或者红黑树的时候才使用`synchronized`,并且只锁住链表或者树的头节点,进一步减少了锁的竞争,并发度大大增加。 + +![](https://blog.meowrain.cn/api/i/2025/09/15/12al42k-1.webp) + + +1.8版本的ConcurrentHashMap也不借助ReentrantLock了,直接用synchronized。 + +当塞入一个值的时候,先计算key的hash后的下标,如果计算到的下标还没有Node,那么就通过CAS塞入新的Node,如果已经有node,就通过synchronized给这个node上锁,这样别的线程就无法访问这个node和它之后的所有节点了。 +然后判断key是不是相等,相等就直接替换value,反之新增一个node。 + +# 扩容上面的区别 +JDK1.7的扩容: +- 基于Segment: ConcurrentHashMap是由多个Segment组成的,每个Segment中包含一个HashMap,当某个Segment内的HashMap达到扩容阈值的时候,单独为该Segment进行扩容,不会影响到其他Segment +- 扩容过程: 每个Segment维护自己的负载因子,当Segment中的元素数量超过阈值的时候,这个Segment的HashMap会扩容,整体的ConcurrentHashMap并不是一次性全部扩容。 + +JDK1.8的扩容: +- 全局扩容: ConcurrentHashMap取消了Segment,变成了一个全局的数组(类似于HashMap)。因此当ConcurrentHashMap中任意位置的元素超过阈值的时候,整个ConcurrentHashMap的数组都会被扩容。 + +- 基于CAS扩容: 扩容的时候,ConcurrentHashMap采用了类似HashMap的方式。通过CAS确保线程安全,避免锁住整个数组。扩容的时候,多个线程可以同时帮助完成扩容操作。 + +- 渐进性扩容: JDK1.8的ConcurrentHashMap引入了渐进式扩容机制, + + +# size逻辑区别 +1.7 是尝试,调用size方法的时候不加锁,三次结果一样那说明没有线程竞争,如果不一样,就加锁计算。 + +1.8的话,是直接计算返回结果,用的是LongAdder完成的累加。 \ No newline at end of file diff --git a/src/content/posts/Java/JUC/JUC笔记.md b/src/content/posts/Java/JUC/JUC笔记.md new file mode 100644 index 0000000..67c498f --- /dev/null +++ b/src/content/posts/Java/JUC/JUC笔记.md @@ -0,0 +1,14403 @@ +--- +title: JUC笔记 +published: 2025-07-18 +description: '' +image: '' +tags: [JUC,Java] +category: 'Java > JUC' +draft: false +lang: '' +--- + +# JUC + +## 进程 + +### 概述 + +进程:程序是静止的,进程实体的运行过程就是进程,是系统进行**资源分配的基本单位** + +进程的特征:并发性、异步性、动态性、独立性、结构性 + +**线程**:线程是属于进程的,是一个基本的 CPU 执行单元,是程序执行流的最小单元。线程是进程中的一个实体,是系统**独立调度的基本单位**,线程本身不拥有系统资源,只拥有一点在运行中必不可少的资源,与同属一个进程的其他线程共享进程所拥有的全部资源 + +关系:一个进程可以包含多个线程,这就是多线程,比如看视频是进程,图画、声音、广告等就是多个线程 + +线程的作用:使多道程序更好的并发执行,提高资源利用率和系统吞吐量,增强操作系统的并发性能 + +并发并行: + +* 并行:在同一时刻,有多个指令在多个 CPU 上同时执行 +* 并发:在同一时刻,有多个指令在单个 CPU 上交替执行 + +同步异步: + +* 需要等待结果返回,才能继续运行就是同步 +* 不需要等待结果返回,就能继续运行就是异步 + +参考视频: + +笔记的整体结构依据视频编写,并随着学习的深入补充了很多知识 + +*** + +### 对比 + +线程进程对比: + +* 进程基本上相互独立的,而线程存在于进程内,是进程的一个子集 + +* 进程拥有共享的资源,如内存空间等,供其**内部的线程共享** + +* 进程间通信较为复杂 + + 同一台计算机的进程通信称为 IPC(Inter-process communication) + + * 信号量:信号量是一个计数器,用于多进程对共享数据的访问,解决同步相关的问题并避免竞争条件 + * 共享存储:多个进程可以访问同一块内存空间,需要使用信号量用来同步对共享存储的访问 + * 管道通信:管道是用于连接一个读进程和一个写进程以实现它们之间通信的一个共享文件 pipe 文件,该文件同一时间只允许一个进程访问,所以只支持**半双工通信** + * 匿名管道(Pipes):用于具有亲缘关系的父子进程间或者兄弟进程之间的通信 + * 命名管道(Names Pipes):以磁盘文件的方式存在,可以实现本机任意两个进程通信,遵循 FIFO + * 消息队列:内核中存储消息的链表,由消息队列标识符标识,能在不同进程之间提供**全双工通信**,对比管道: + * 匿名管道存在于内存中的文件;命名管道存在于实际的磁盘介质或者文件系统;消息队列存放在内核中,只有在内核重启(操作系统重启)或者显示地删除一个消息队列时,该消息队列才被真正删除 + * 读进程可以根据消息类型有选择地接收消息,而不像 FIFO 那样只能默认地接收 + + 不同计算机之间的**进程通信**,需要通过网络,并遵守共同的协议,例如 HTTP + + * 套接字:与其它通信机制不同的是,可用于不同机器间的互相通信 + +* 线程通信相对简单,因为线程之间共享进程内的内存,一个例子是多个线程可以访问同一个共享变量 + + **Java 中的通信机制**:volatile、等待/通知机制、join 方式、InheritableThreadLocal、MappedByteBuffer + +* 线程更轻量,线程上下文切换成本一般上要比进程上下文切换低 + +*** + +## 线程 + +### 创建线程 + +#### Thread + +Thread 创建线程方式:创建线程类,匿名内部类方式 + +* **start() 方法底层其实是给 CPU 注册当前线程,并且触发 run() 方法执行** +* 线程的启动必须调用 start() 方法,如果线程直接调用 run() 方法,相当于变成了普通类的执行,此时主线程将只有执行该线程 +* 建议线程先创建子线程,主线程的任务放在之后,否则主线程(main)永远是先执行完 + +Thread 构造器: + +* `public Thread()` +* `public Thread(String name)` + +```java +public class ThreadDemo { + public static void main(String[] args) { + Thread t = new MyThread(); + t.start(); + for(int i = 0 ; i < 100 ; i++ ){ + System.out.println("main线程" + i) + } + // main线程输出放在上面 就变成有先后顺序了,因为是 main 线程驱动的子线程运行 + } +} +class MyThread extends Thread { + @Override + public void run() { + for(int i = 0 ; i < 100 ; i++ ) { + System.out.println("子线程输出:"+i) + } + } +} +``` + +继承 Thread 类的优缺点: + +* 优点:编码简单 +* 缺点:线程类已经继承了 Thread 类无法继承其他类了,功能不能通过继承拓展(单继承的局限性) + +*** + +#### Runnable + +Runnable 创建线程方式:创建线程类,匿名内部类方式 + +Thread 的构造器: + +* `public Thread(Runnable target)` +* `public Thread(Runnable target, String name)` + +```java +public class ThreadDemo { + public static void main(String[] args) { + Runnable target = new MyRunnable(); + Thread t1 = new Thread(target,"1号线程"); + t1.start(); + Thread t2 = new Thread(target);//Thread-0 + } +} + +public class MyRunnable implements Runnable{ + @Override + public void run() { + for(int i = 0 ; i < 10 ; i++ ){ + System.out.println(Thread.currentThread().getName() + "->" + i); + } + } +} +``` + +**Thread 类本身也是实现了 Runnable 接口**,Thread 类中持有 Runnable 的属性,执行线程 run 方法底层是调用 Runnable#run: + +```java +public class Thread implements Runnable { + private Runnable target; + + public void run() { + if (target != null) { + // 底层调用的是 Runnable 的 run 方法 + target.run(); + } + } +} +``` + +Runnable 方式的优缺点: + +* 缺点:代码复杂一点。 + +* 优点: + + 1. 线程任务类只是实现了 Runnable 接口,可以继续继承其他类,避免了单继承的局限性 + + 2. 同一个线程任务对象可以被包装成多个线程对象 + + 3. 适合多个多个线程去共享同一个资源 + + 4. 实现解耦操作,线程任务代码可以被多个线程共享,线程任务代码和线程独立 + + 5. 线程池可以放入实现 Runnable 或 Callable 线程任务对象 + +​ + +**** + +#### Callable + +实现 Callable 接口: + +1. 定义一个线程任务类实现 Callable 接口,申明线程执行的结果类型 +2. 重写线程任务类的 call 方法,这个方法可以直接返回执行的结果 +3. 创建一个 Callable 的线程任务对象 +4. 把 Callable 的线程任务对象**包装成一个未来任务对象** +5. 把未来任务对象包装成线程对象 +6. 调用线程的 start() 方法启动线程 + +`public FutureTask(Callable callable)`:未来任务对象,在线程执行完后得到线程的执行结果 + +* FutureTask 就是 Runnable 对象,因为 **Thread 类只能执行 Runnable 实例的任务对象**,所以把 Callable 包装成未来任务对象 +* 线程池部分详解了 FutureTask 的源码 + +`public V get()`:同步等待 task 执行完毕的结果,如果在线程中获取另一个线程执行结果,会阻塞等待,用于线程同步 + +* get() 线程会阻塞等待任务执行完成 +* run() 执行完后会把结果设置到 FutureTask 的一个成员变量,get() 线程可以获取到该变量的值 + +优缺点: + +* 优点:同 Runnable,并且能得到线程执行的结果 +* 缺点:编码复杂 + +```java +public class ThreadDemo { + public static void main(String[] args) { + Callable call = new MyCallable(); + FutureTask task = new FutureTask<>(call); + Thread t = new Thread(task); + t.start(); + try { + String s = task.get(); // 获取call方法返回的结果(正常/异常结果) + System.out.println(s); + } catch (Exception e) { + e.printStackTrace(); + } + } + +public class MyCallable implements Callable { + @Override//重写线程任务类方法 + public String call() throws Exception { + return Thread.currentThread().getName() + "->" + "Hello World"; + } +} +``` + +*** + +### 线程方法 + +#### API + +Thread 类 API: + +| 方法 | 说明 | +| ------------------------------------------- | ------------------------------------------------------------ | +| public void start() | 启动一个新线程,Java虚拟机调用此线程的 run 方法 | +| public void run() | 线程启动后调用该方法 | +| public void setName(String name) | 给当前线程取名字 | +| public void getName() | 获取当前线程的名字
线程存在默认名称:子线程是 Thread-索引,主线程是 main | +| public static Thread currentThread() | 获取当前线程对象,代码在哪个线程中执行 | +| public static void sleep(long time) | 让当前线程休眠多少毫秒再继续执行
**Thread.sleep(0)** : 让操作系统立刻重新进行一次 CPU 竞争 | +| public static native void yield() | 提示线程调度器让出当前线程对 CPU 的使用 | +| public final int getPriority() | 返回此线程的优先级 | +| public final void setPriority(int priority) | 更改此线程的优先级,常用 1 5 10 | +| public void interrupt() | 中断这个线程,异常处理机制 | +| public static boolean interrupted() | 判断当前线程是否被打断,清除打断标记 | +| public boolean isInterrupted() | 判断当前线程是否被打断,不清除打断标记 | +| public final void join() | 等待这个线程结束 | +| public final void join(long millis) | 等待这个线程死亡 millis 毫秒,0 意味着永远等待 | +| public final native boolean isAlive() | 线程是否存活(还没有运行完毕) | +| public final void setDaemon(boolean on) | 将此线程标记为守护线程或用户线程 | + +*** + +#### run start + +run:称为线程体,包含了要执行的这个线程的内容,方法运行结束,此线程随即终止。直接调用 run 是在主线程中执行了 run,没有启动新的线程,需要顺序执行 + +start:使用 start 是启动新的线程,此线程处于就绪(可运行)状态,通过新的线程间接执行 run 中的代码 + +说明:**线程控制资源类** + +run() 方法中的异常不能抛出,只能 try/catch + +* 因为父类中没有抛出任何异常,子类不能比父类抛出更多的异常 +* **异常不能跨线程传播回 main() 中**,因此必须在本地进行处理 + +*** + +#### sleep yield + +sleep: + +* 调用 sleep 会让当前线程从 `Running` 进入 `Timed Waiting` 状态(阻塞) +* sleep() 方法的过程中,**线程不会释放对象锁** +* 其它线程可以使用 interrupt 方法打断正在睡眠的线程,这时 sleep 方法会抛出 InterruptedException +* 睡眠结束后的线程未必会立刻得到执行,需要抢占 CPU +* 建议用 TimeUnit 的 sleep 代替 Thread 的 sleep 来获得更好的可读性 + +yield: + +* 调用 yield 会让提示线程调度器让出当前线程对 CPU 的使用 +* 具体的实现依赖于操作系统的任务调度器 +* **会放弃 CPU 资源,锁资源不会释放** + +*** + +#### join + +public final void join():等待这个线程结束 + +原理:调用者轮询检查线程 alive 状态,t1.join() 等价于: + +```java +public final synchronized void join(long millis) throws InterruptedException { + // 调用者线程进入 thread 的 waitSet 等待, 直到当前线程运行结束 + while (isAlive()) { + wait(0); + } +} +``` + +* join 方法是被 synchronized 修饰的,本质上是一个对象锁,其内部的 wait 方法调用也是释放锁的,但是**释放的是当前的线程对象锁,而不是外面的锁** + +* 当调用某个线程(t1)的 join 方法后,该线程(t1)抢占到 CPU 资源,就不再释放,直到线程执行完毕 + +线程同步: + +* join 实现线程同步,因为会阻塞等待另一个线程的结束,才能继续向下运行 + * 需要外部共享变量,不符合面向对象封装的思想 + * 必须等待线程结束,不能配合线程池使用 +* Future 实现(同步):get() 方法阻塞等待执行结果 + * main 线程接收结果 + * get 方法是让调用线程同步等待 + +```java +public class Test { + static int r = 0; + public static void main(String[] args) throws InterruptedException { + test1(); + } + private static void test1() throws InterruptedException { + Thread t1 = new Thread(() -> { + try { + Thread.sleep(1000); + } catch (InterruptedException e) { + e.printStackTrace(); + } + r = 10; + }); + t1.start(); + t1.join();//不等待线程执行结束,输出的10 + System.out.println(r); + } +} +``` + +*** + +#### interrupt + +##### 打断线程 + +`public void interrupt()`:打断这个线程,异常处理机制 + +`public static boolean interrupted()`:判断当前线程是否被打断,打断返回 true,**清除打断标记**,连续调用两次一定返回 false + +`public boolean isInterrupted()`:判断当前线程是否被打断,不清除打断标记 + +打断的线程会发生上下文切换,操作系统会保存线程信息,抢占到 CPU 后会从中断的地方接着运行(打断不是停止) + +* sleep、wait、join 方法都会让线程进入阻塞状态,打断线程**会清空打断状态**(false) + + ```java + public static void main(String[] args) throws InterruptedException { + Thread t1 = new Thread(()->{ + try { + Thread.sleep(1000); + } catch (InterruptedException e) { + e.printStackTrace(); + } + }, "t1"); + t1.start(); + Thread.sleep(500); + t1.interrupt(); + System.out.println(" 打断状态: {}" + t1.isInterrupted());// 打断状态: {}false + } + ``` + +* 打断正常运行的线程:不会清空打断状态(true) + + ```java + public static void main(String[] args) throws Exception { + Thread t2 = new Thread(()->{ + while(true) { + Thread current = Thread.currentThread(); + boolean interrupted = current.isInterrupted(); + if(interrupted) { + System.out.println(" 打断状态: {}" + interrupted);//打断状态: {}true + break; + } + } + }, "t2"); + t2.start(); + Thread.sleep(500); + t2.interrupt(); + } + ``` + +*** + +##### 打断 park + +park 作用类似 sleep,打断 park 线程,不会清空打断状态(true) + +```java +public static void main(String[] args) throws Exception { + Thread t1 = new Thread(() -> { + System.out.println("park..."); + LockSupport.park(); + System.out.println("unpark..."); + System.out.println("打断状态:" + Thread.currentThread().isInterrupted());//打断状态:true + }, "t1"); + t1.start(); + Thread.sleep(2000); + t1.interrupt(); +} +``` + +如果打断标记已经是 true, 则 park 会失效 + +```java +LockSupport.park(); +System.out.println("unpark..."); +LockSupport.park();//失效,不会阻塞 +System.out.println("unpark...");//和上一个unpark同时执行 +``` + +可以修改获取打断状态方法,使用 `Thread.interrupted()`,清除打断标记 + +LockSupport 类在 同步 → park-un 详解 + +*** + +##### 终止模式 + +终止模式之两阶段终止模式:Two Phase Termination + +目标:在一个线程 T1 中如何优雅终止线程 T2?优雅指的是给 T2 一个后置处理器 + +错误思想: + +* 使用线程对象的 stop() 方法停止线程:stop 方法会真正杀死线程,如果这时线程锁住了共享资源,当它被杀死后就再也没有机会释放锁,其它线程将永远无法获取锁 +* 使用 System.exit(int) 方法停止线程:目的仅是停止一个线程,但这种做法会让整个程序都停止 + +两阶段终止模式图示: + + + +打断线程可能在任何时间,所以需要考虑在任何时刻被打断的处理方法: + +```java +public class Test { + public static void main(String[] args) throws InterruptedException { + TwoPhaseTermination tpt = new TwoPhaseTermination(); + tpt.start(); + Thread.sleep(3500); + tpt.stop(); + } +} +class TwoPhaseTermination { + private Thread monitor; + // 启动监控线程 + public void start() { + monitor = new Thread(new Runnable() { + @Override + public void run() { + while (true) { + Thread thread = Thread.currentThread(); + if (thread.isInterrupted()) { + System.out.println("后置处理"); + break; + } + try { + Thread.sleep(1000); // 睡眠 + System.out.println("执行监控记录"); // 在此被打断不会异常 + } catch (InterruptedException e) { // 在睡眠期间被打断,进入异常处理的逻辑 + e.printStackTrace(); + // 重新设置打断标记,打断 sleep 会清除打断状态 + thread.interrupt(); + } + } + } + }); + monitor.start(); + } + // 停止监控线程 + public void stop() { + monitor.interrupt(); + } +} +``` + +*** + +#### daemon + +`public final void setDaemon(boolean on)`:如果是 true ,将此线程标记为守护线程 + +线程**启动前**调用此方法: + +```java +Thread t = new Thread() { + @Override + public void run() { + System.out.println("running"); + } +}; +// 设置该线程为守护线程 +t.setDaemon(true); +t.start(); +``` + +用户线程:平常创建的普通线程 + +守护线程:服务于用户线程,只要其它非守护线程运行结束了,即使守护线程代码没有执行完,也会强制结束。守护进程是**脱离于终端并且在后台运行的进程**,脱离终端是为了避免在执行的过程中的信息在终端上显示 + +说明:当运行的线程都是守护线程,Java 虚拟机将退出,因为普通线程执行完后,JVM 是守护线程,不会继续运行下去 + +常见的守护线程: + +* 垃圾回收器线程就是一种守护线程 +* Tomcat 中的 Acceptor 和 Poller 线程都是守护线程,所以 Tomcat 接收到 shutdown 命令后,不会等待它们处理完当前请求 + +*** + +#### 不推荐 + +不推荐使用的方法,这些方法已过时,容易破坏同步代码块,造成线程死锁: + +* `public final void stop()`:停止线程运行 + + 废弃原因:方法粗暴,除非可能执行 finally 代码块以及释放 synchronized 外,线程将直接被终止,如果线程持有 JUC 的互斥锁可能导致锁来不及释放,造成其他线程永远等待的局面 + +* `public final void suspend()`:**挂起(暂停)线程运行** + + 废弃原因:如果目标线程在暂停时对系统资源持有锁,则在目标线程恢复之前没有线程可以访问该资源,如果**恢复目标线程的线程**在调用 resume 之前会尝试访问此共享资源,则会导致死锁 + +* `public final void resume()`:恢复线程运行 + +*** + +### 线程原理 + +#### 运行机制 + +Java Virtual Machine Stacks(Java 虚拟机栈):每个线程启动后,虚拟机就会为其分配一块栈内存 + +* 每个栈由多个栈帧(Frame)组成,对应着每次方法调用时所占用的内存 +* 每个线程只能有一个活动栈帧,对应着当前正在执行的那个方法 + +线程上下文切换(Thread Context Switch):一些原因导致 CPU 不再执行当前线程,转而执行另一个线程 + +* 线程的 CPU 时间片用完 +* 垃圾回收 +* 有更高优先级的线程需要运行 +* 线程自己调用了 sleep、yield、wait、join、park 等方法 + +程序计数器(Program Counter Register):记住下一条 JVM 指令的执行地址,是线程私有的 + +当 Context Switch 发生时,需要由操作系统保存当前线程的状态(PCB 中),并恢复另一个线程的状态,包括程序计数器、虚拟机栈中每个栈帧的信息,如局部变量、操作数栈、返回地址等 + +JVM 规范并没有限定线程模型,以 HotSopot 为例: + +* Java 的线程是内核级线程(1:1 线程模型),每个 Java 线程都映射到一个操作系统原生线程,需要消耗一定的内核资源(堆栈) +* **线程的调度是在内核态运行的,而线程中的代码是在用户态运行**,所以线程切换(状态改变)会导致用户与内核态转换进行系统调用,这是非常消耗性能 + +Java 中 main 方法启动的是一个进程也是一个主线程,main 方法里面的其他线程均为子线程,main 线程是这些线程的父线程 + +*** + +#### 线程调度 + +线程调度指系统为线程分配处理器使用权的过程,方式有两种:协同式线程调度、抢占式线程调度(Java 选择) + +协同式线程调度:线程的执行时间由线程本身控制 + +* 优点:线程做完任务才通知系统切换到其他线程,相当于所有线程串行执行,不会出现线程同步问题 +* 缺点:线程执行时间不可控,如果代码编写出现问题,可能导致程序一直阻塞,引起系统的奔溃 + +抢占式线程调度:线程的执行时间由系统分配 + +* 优点:线程执行时间可控,不会因为一个线程的问题而导致整体系统不可用 +* 缺点:无法主动为某个线程多分配时间 + +Java 提供了线程优先级的机制,优先级会提示(hint)调度器优先调度该线程,但这仅仅是一个提示,调度器可以忽略它。在线程的就绪状态时,如果 CPU 比较忙,那么优先级高的线程会获得更多的时间片,但 CPU 闲时,优先级几乎没作用 + +说明:并不能通过优先级来判断线程执行的先后顺序 + +*** + +#### 未来优化 + +内核级线程调度的成本较大,所以引入了更轻量级的协程。用户线程的调度由用户自己实现(多对一的线程模型,多**个用户线程映射到一个内核级线程**),被设计为协同式调度,所以叫协程 + +* 有栈协程:协程会完整的做调用栈的保护、恢复工作,所以叫有栈协程 +* 无栈协程:本质上是一种有限状态机,状态保存在闭包里,比有栈协程更轻量,但是功能有限 + +有栈协程中有一种特例叫纤程,在新并发模型中,一段纤程的代码被分为两部分,执行过程和调度器: + +* 执行过程:用于维护执行现场,保护、恢复上下文状态 +* 调度器:负责编排所有要执行的代码顺序 + +**** + +### 线程状态 + +进程的状态参考操作系统:创建态、就绪态、运行态、阻塞态、终止态 + +线程由生到死的完整过程(生命周期):当线程被创建并启动以后,既不是一启动就进入了执行状态,也不是一直处于执行状态,在 API 中 `java.lang.Thread.State` 这个枚举中给出了六种线程状态: + +| 线程状态 | 导致状态发生条件 | +| -------------------------- | ------------------------------------------------------------ | +| NEW(新建) | 线程刚被创建,但是并未启动,还没调用 start 方法,只有线程对象,没有线程特征 | +| Runnable(可运行) | 线程可以在 Java 虚拟机中运行的状态,可能正在运行自己代码,也可能没有,这取决于操作系统处理器,调用了 t.start() 方法:就绪(经典叫法) | +| Blocked(阻塞) | 当一个线程试图获取一个对象锁,而该对象锁被其他的线程持有,则该线程进入 Blocked 状态;当该线程持有锁时,该线程将变成 Runnable 状态 | +| Waiting(无限等待) | 一个线程在等待另一个线程执行一个(唤醒)动作时,该线程进入 Waiting 状态,进入这个状态后不能自动唤醒,必须等待另一个线程调用 notify 或者 notifyAll 方法才能唤醒 | +| Timed Waiting (限期等待) | 有几个方法有超时参数,调用将进入 Timed Waiting 状态,这一状态将一直保持到超时期满或者接收到唤醒通知。带有超时参数的常用方法有 Thread.sleep 、Object.wait | +| Teminated(结束) | run 方法正常退出而死亡,或者因为没有捕获的异常终止了 run 方法而死亡 | + +![](https://seazean.oss-cn-beijing.aliyuncs.com/img/Java/JUC-线程6种状态.png) + +* NEW → RUNNABLE:当调用 t.start() 方法时,由 NEW → RUNNABLE + +* RUNNABLE <--> WAITING: + + * 调用 obj.wait() 方法时 + + 调用 obj.notify()、obj.notifyAll()、t.interrupt(): + + * 竞争锁成功,t 线程从 WAITING → RUNNABLE + * 竞争锁失败,t 线程从 WAITING → BLOCKED + + * 当前线程调用 t.join() 方法,注意是当前线程在 t 线程对象的监视器上等待 + + * 当前线程调用 LockSupport.park() 方法 + +* RUNNABLE <--> TIMED_WAITING:调用 obj.wait(long n) 方法、当前线程调用 t.join(long n) 方法、当前线程调用 Thread.sleep(long n) + +* RUNNABLE <--> BLOCKED:t 线程用 synchronized(obj) 获取了对象锁时竞争失败 + +*** + +### 查看线程 + +Windows: + +* 任务管理器可以查看进程和线程数,也可以用来杀死进程 +* tasklist 查看进程 +* taskkill 杀死进程 + +Linux: + +* ps -ef 查看所有进程 +* ps -fT -p 查看某个进程(PID)的所有线程 +* kill 杀死进程 +* top 按大写 H 切换是否显示线程 +* top -H -p 查看某个进程(PID)的所有线程 + +Java: + +* jps 命令查看所有 Java 进程 +* jstack 查看某个 Java 进程(PID)的所有线程状态 +* jconsole 来查看某个 Java 进程中线程的运行情况(图形界面) + +*** + +## 同步 + +### 临界区 + +临界资源:一次仅允许一个进程使用的资源成为临界资源 + +临界区:访问临界资源的代码块 + +竞态条件:多个线程在临界区内执行,由于代码的执行序列不同而导致结果无法预测,称之为发生了竞态条件 + +一个程序运行多个线程是没有问题,多个线程读共享资源也没有问题,在多个线程对共享资源读写操作时发生指令交错,就会出现问题 + +为了避免临界区的竞态条件发生(解决线程安全问题): + +* 阻塞式的解决方案:synchronized,lock +* 非阻塞式的解决方案:原子变量 + +管程(monitor):由局部于自己的若干公共变量和所有访问这些公共变量的过程所组成的软件模块,保证同一时刻只有一个进程在管程内活动,即管程内定义的操作在同一时刻只被一个进程调用(由编译器实现) + +**synchronized:对象锁,保证了临界区内代码的原子性**,采用互斥的方式让同一时刻至多只有一个线程能持有对象锁,其它线程获取这个对象锁时会阻塞,保证拥有锁的线程可以安全的执行临界区内的代码,不用担心线程上下文切换 + +互斥和同步都可以采用 synchronized 关键字来完成,区别: + +* 互斥是保证临界区的竞态条件发生,同一时刻只能有一个线程执行临界区代码 +* 同步是由于线程执行的先后、顺序不同、需要一个线程等待其它线程运行到某个点 + +性能: + +* 线程安全,性能差 +* 线程不安全性能好,假如开发中不会存在多线程安全问题,建议使用线程不安全的设计类 + +*** + +### syn-ed + +#### 使用锁 + +##### 同步块 + +锁对象:理论上可以是**任意的唯一对象** + +synchronized 是可重入、不公平的重量级锁 + +原则上: + +* 锁对象建议使用共享资源 +* 在实例方法中使用 this 作为锁对象,锁住的 this 正好是共享资源 +* 在静态方法中使用类名 .class 字节码作为锁对象,因为静态成员属于类,被所有实例对象共享,所以需要锁住类 + +同步代码块格式: + +```java +synchronized(锁对象){ + // 访问共享资源的核心代码 +} +``` + +实例: + +```java +public class demo { + static int counter = 0; + //static修饰,则元素是属于类本身的,不属于对象 ,与类一起加载一次,只有一个 + static final Object room = new Object(); + public static void main(String[] args) throws InterruptedException { + Thread t1 = new Thread(() -> { + for (int i = 0; i < 5000; i++) { + synchronized (room) { + counter++; + } + } + }, "t1"); + Thread t2 = new Thread(() -> { + for (int i = 0; i < 5000; i++) { + synchronized (room) { + counter--; + } + } + }, "t2"); + t1.start(); + t2.start(); + t1.join(); + t2.join(); + System.out.println(counter); + } +} +``` + +*** + +##### 同步方法 + +把出现线程安全问题的核心方法锁起来,每次只能一个线程进入访问 + +synchronized 修饰的方法的不具备继承性,所以子类是线程不安全的,如果子类的方法也被 synchronized 修饰,两个锁对象其实是一把锁,而且是**子类对象作为锁** + +用法:直接给方法加上一个修饰符 synchronized + +```java +//同步方法 +修饰符 synchronized 返回值类型 方法名(方法参数) { + 方法体; +} +//同步静态方法 +修饰符 static synchronized 返回值类型 方法名(方法参数) { + 方法体; +} +``` + +同步方法底层也是有锁对象的: + +* 如果方法是实例方法:同步方法默认用 this 作为的锁对象 + + ```java + public synchronized void test() {} //等价于 + public void test() { + synchronized(this) {} + } + ``` + +* 如果方法是静态方法:同步方法默认用类名 .class 作为的锁对象 + + ```java + class Test{ + public synchronized static void test() {} + } + //等价于 + class Test{ + public void test() { + synchronized(Test.class) {} + } + } + ``` + +*** + +##### 线程八锁 + +线程八锁就是考察 synchronized 锁住的是哪个对象,直接百度搜索相关的实例 + +说明:主要关注锁住的对象是不是同一个 + +* 锁住类对象,所有类的实例的方法都是安全的,类的所有实例都相当于同一把锁 +* 锁住 this 对象,只有在当前实例对象的线程内是安全的,如果有多个实例就不安全 + +线程不安全:因为锁住的不是同一个对象,线程 1 调用 a 方法锁住的类对象,线程 2 调用 b 方法锁住的 n2 对象,不是同一个对象 + +```java +class Number{ + public static synchronized void a(){ + Thread.sleep(1000); + System.out.println("1"); + } + public synchronized void b() { + System.out.println("2"); + } +} +public static void main(String[] args) { + Number n1 = new Number(); + Number n2 = new Number(); + new Thread(()->{ n1.a(); }).start(); + new Thread(()->{ n2.b(); }).start(); +} +``` + +线程安全:因为 n1 调用 a() 方法,锁住的是类对象,n2 调用 b() 方法,锁住的也是类对象,所以线程安全 + +```java +class Number{ + public static synchronized void a(){ + Thread.sleep(1000); + System.out.println("1"); + } + public static synchronized void b() { + System.out.println("2"); + } +} +public static void main(String[] args) { + Number n1 = new Number(); + Number n2 = new Number(); + new Thread(()->{ n1.a(); }).start(); + new Thread(()->{ n2.b(); }).start(); +} +``` + +*** + +#### 锁原理 + +##### Monitor + +Monitor 被翻译为监视器或管程 + +每个 Java 对象都可以关联一个 Monitor 对象,Monitor 也是 class,其**实例存储在堆中**,如果使用 synchronized 给对象上锁(重量级)之后,该对象头的 Mark Word 中就被设置指向 Monitor 对象的指针,这就是重量级锁 + +* Mark Word 结构:最后两位是**锁标志位** + + ![](https://seazean.oss-cn-beijing.aliyuncs.com/img/Java/JUC-Monitor-MarkWord结构32位.png) + +* 64 位虚拟机 Mark Word: + + ![](https://seazean.oss-cn-beijing.aliyuncs.com/img/Java/JUC-Monitor-MarkWord结构64位.png) + +工作流程: + +* 开始时 Monitor 中 Owner 为 null +* 当 Thread-2 执行 synchronized(obj) 就会将 Monitor 的所有者 Owner 置为 Thread-2,Monitor 中只能有一个 Owner,**obj 对象的 Mark Word 指向 Monitor**,把**对象原有的 MarkWord 存入线程栈中的锁记录**中(轻量级锁部分详解) + +* 在 Thread-2 上锁的过程,Thread-3、Thread-4、Thread-5 也执行 synchronized(obj),就会进入 EntryList BLOCKED(双向链表) +* Thread-2 执行完同步代码块的内容,根据 obj 对象头中 Monitor 地址寻找,设置 Owner 为空,把线程栈的锁记录中的对象头的值设置回 MarkWord +* 唤醒 EntryList 中等待的线程来竞争锁,竞争是**非公平的**,如果这时有新的线程想要获取锁,可能直接就抢占到了,阻塞队列的线程就会继续阻塞 +* WaitSet 中的 Thread-0,是以前获得过锁,但条件不满足进入 WAITING 状态的线程(wait-notify 机制) + +![](https://seazean.oss-cn-beijing.aliyuncs.com/img/Java/JUC-Monitor工作原理2.png) + +注意: + +* synchronized 必须是进入同一个对象的 Monitor 才有上述的效果 +* 不加 synchronized 的对象不会关联监视器,不遵从以上规则 + +**** + +##### 字节码 + +代码: + +```java +public static void main(String[] args) { + Object lock = new Object(); + synchronized (lock) { + System.out.println("ok"); + } +} +``` + +```java +0: new #2 // new Object +3: dup +4: invokespecial #1 // invokespecial :()V,非虚方法 +7: astore_1 // lock引用 -> lock +8: aload_1 // lock (synchronized开始) +9: dup // 一份用来初始化,一份用来引用 +10: astore_2 // lock引用 -> slot 2 +11: monitorenter // 【将 lock对象 MarkWord 置为 Monitor 指针】 +12: getstatic #3 // System.out +15: ldc #4 // "ok" +17: invokevirtual #5 // invokevirtual println:(Ljava/lang/String;)V +20: aload_2 // slot 2(lock引用) +21: monitorexit // 【将 lock对象 MarkWord 重置, 唤醒 EntryList】 +22: goto 30 +25: astore_3 // any -> slot 3 +26: aload_2 // slot 2(lock引用) +27: monitorexit // 【将 lock对象 MarkWord 重置, 唤醒 EntryList】 +28: aload_3 +29: athrow +30: return +Exception table: + from to target type + 12 22 25 any + 25 28 25 any +LineNumberTable: ... +LocalVariableTable: + Start Length Slot Name Signature + 0 31 0 args [Ljava/lang/String; + 8 23 1 lock Ljava/lang/Object; +``` + +说明: + +* 通过异常 **try-catch 机制**,确保一定会被解锁 +* 方法级别的 synchronized 不会在字节码指令中有所体现 + +*** + +#### 锁升级 + +##### 升级过程 + +**synchronized 是可重入、不公平的重量级锁**,所以可以对其进行优化 + +```java +无锁 -> 偏向锁 -> 轻量级锁 -> 重量级锁 // 随着竞争的增加,只能锁升级,不能降级 +``` + +![](https://seazean.oss-cn-beijing.aliyuncs.com/img/Java/JUC-锁升级过程.png) + +*** + +##### 偏向锁 + +偏向锁的思想是偏向于让第一个获取锁对象的线程,这个线程之后重新获取该锁不再需要同步操作: + +* 当锁对象第一次被线程获得的时候进入偏向状态,标记为 101,同时**使用 CAS 操作将线程 ID 记录到 Mark Word**。如果 CAS 操作成功,这个线程以后进入这个锁相关的同步块,查看这个线程 ID 是自己的就表示没有竞争,就不需要再进行任何同步操作 + +* 当有另外一个线程去尝试获取这个锁对象时,偏向状态就宣告结束,此时撤销偏向(Revoke Bias)后恢复到未锁定或轻量级锁状态 + + + +一个对象创建时: + +* 如果开启了偏向锁(默认开启),那么对象创建后,MarkWord 值为 0x05 即最后 3 位为 101,thread、epoch、age 都为 0 +* 偏向锁是默认是延迟的,不会在程序启动时立即生效,如果想避免延迟,可以加 VM 参数 `-XX:BiasedLockingStartupDelay=0` 来禁用延迟。JDK 8 延迟 4s 开启偏向锁原因:在刚开始执行代码时,会有好多线程来抢锁,如果开偏向锁效率反而降低 + +* 当一个对象已经计算过 hashCode,就再也无法进入偏向状态了 +* 添加 VM 参数 `-XX:-UseBiasedLocking` 禁用偏向锁 + +撤销偏向锁的状态: + +* 调用对象的 hashCode:偏向锁的对象 MarkWord 中存储的是线程 id,调用 hashCode 导致偏向锁被撤销 +* 当有其它线程使用偏向锁对象时,会将偏向锁升级为轻量级锁 +* 调用 wait/notify,需要申请 Monitor,进入 WaitSet + +**批量撤销**:如果对象被多个线程访问,但没有竞争,这时偏向了线程 T1 的对象仍有机会重新偏向 T2,重偏向会重置对象的 Thread ID + +* 批量重偏向:当撤销偏向锁阈值超过 20 次后,JVM 会觉得是不是偏向错了,于是在给这些对象加锁时重新偏向至加锁线程 + +* 批量撤销:当撤销偏向锁阈值超过 40 次后,JVM 会觉得自己确实偏向错了,根本就不该偏向,于是整个类的所有对象都会变为不可偏向的,新建的对象也是不可偏向的 + +*** + +##### 轻量级锁 + +一个对象有多个线程要加锁,但加锁的时间是错开的(没有竞争),可以使用轻量级锁来优化,轻量级锁对使用者是透明的(不可见) + +可重入锁:线程可以进入任何一个它已经拥有的锁所同步着的代码块,可重入锁最大的作用是**避免死锁** + +轻量级锁在没有竞争时(锁重入时),每次重入仍然需要执行 CAS 操作,Java 6 才引入的偏向锁来优化 + +锁重入实例: + +```java +static final Object obj = new Object(); +public static void method1() { + synchronized( obj ) { + // 同步块 A + method2(); + } +} +public static void method2() { + synchronized( obj ) { + // 同步块 B + } +} +``` + +* 创建锁记录(Lock Record)对象,每个线程的**栈帧**都会包含一个锁记录的结构,存储锁定对象的 Mark Word + + ![](https://seazean.oss-cn-beijing.aliyuncs.com/img/Java/JUC-轻量级锁原理1.png) + +* 让锁记录中 Object reference 指向锁住的对象,并尝试用 CAS 替换 Object 的 Mark Word,将 Mark Word 的值存入锁记录 + +* 如果 CAS 替换成功,对象头中存储了锁记录地址和状态 00(轻量级锁) ,表示由该线程给对象加锁 + ![](https://seazean.oss-cn-beijing.aliyuncs.com/img/Java/JUC-轻量级锁原理2.png) + +* 如果 CAS 失败,有两种情况: + + * 如果是其它线程已经持有了该 Object 的轻量级锁,这时表明有竞争,进入锁膨胀过程 + * 如果是线程自己执行了 synchronized 锁重入,就添加一条 Lock Record 作为重入的计数 + + ![](https://seazean.oss-cn-beijing.aliyuncs.com/img/Java/JUC-轻量级锁原理3.png) + +* 当退出 synchronized 代码块(解锁时) + + * 如果有取值为 null 的锁记录,表示有重入,这时重置锁记录,表示重入计数减 1 + * 如果锁记录的值不为 null,这时使用 CAS **将 Mark Word 的值恢复给对象头** + * 成功,则解锁成功 + * 失败,说明轻量级锁进行了锁膨胀或已经升级为重量级锁,进入重量级锁解锁流程 + +*** + +##### 锁膨胀 + +在尝试加轻量级锁的过程中,CAS 操作无法成功,可能是其它线程为此对象加上了轻量级锁(有竞争),这时需要进行锁膨胀,将轻量级锁变为**重量级锁** + +* 当 Thread-1 进行轻量级加锁时,Thread-0 已经对该对象加了轻量级锁 + + ![](https://seazean.oss-cn-beijing.aliyuncs.com/img/Java/JUC-重量级锁原理1.png) + +* Thread-1 加轻量级锁失败,进入锁膨胀流程:为 Object 对象申请 Monitor 锁,**通过 Object 对象头获取到持锁线程**,将 Monitor 的 Owner 置为 Thread-0,将 Object 的对象头指向重量级锁地址,然后自己进入 Monitor 的 EntryList BLOCKED + + ![](https://seazean.oss-cn-beijing.aliyuncs.com/img/Java/JUC-重量级锁原理2.png) + +* 当 Thread-0 退出同步块解锁时,使用 CAS 将 Mark Word 的值恢复给对象头失败,这时进入重量级解锁流程,即按照 Monitor 地址找到 Monitor 对象,设置 Owner 为 null,唤醒 EntryList 中 BLOCKED 线程 + +*** + +#### 锁优化 + +##### 自旋锁 + +重量级锁竞争时,尝试获取锁的线程不会立即阻塞,可以使用**自旋**(默认 10 次)来进行优化,采用循环的方式去尝试获取锁 + +注意: + +* 自旋占用 CPU 时间,单核 CPU 自旋就是浪费时间,因为同一时刻只能运行一个线程,多核 CPU 自旋才能发挥优势 +* 自旋失败的线程会进入阻塞状态 + +优点:不会进入阻塞状态,**减少线程上下文切换的消耗** + +缺点:当自旋的线程越来越多时,会不断的消耗 CPU 资源 + +自旋锁情况: + +* 自旋成功的情况: + + +* 自旋失败的情况: + + + +自旋锁说明: + +* 在 Java 6 之后自旋锁是自适应的,比如对象刚刚的一次自旋操作成功过,那么认为这次自旋成功的可能性会高,就多自旋几次;反之,就少自旋甚至不自旋,比较智能 +* Java 7 之后不能控制是否开启自旋功能,由 JVM 控制 + +```java +//手写自旋锁 +public class SpinLock { + // 泛型装的是Thread,原子引用线程 + AtomicReference atomicReference = new AtomicReference<>(); + + public void lock() { + Thread thread = Thread.currentThread(); + System.out.println(thread.getName() + " come in"); + + //开始自旋,期望值为null,更新值是当前线程 + while (!atomicReference.compareAndSet(null, thread)) { + Thread.sleep(1000); + System.out.println(thread.getName() + " 正在自旋"); + } + System.out.println(thread.getName() + " 自旋成功"); + } + + public void unlock() { + Thread thread = Thread.currentThread(); + + //线程使用完锁把引用变为null + atomicReference.compareAndSet(thread, null); + System.out.println(thread.getName() + " invoke unlock"); + } + + public static void main(String[] args) throws InterruptedException { + SpinLock lock = new SpinLock(); + new Thread(() -> { + //占有锁 + lock.lock(); + Thread.sleep(10000); + + //释放锁 + lock.unlock(); + },"t1").start(); + + // 让main线程暂停1秒,使得t1线程,先执行 + Thread.sleep(1000); + + new Thread(() -> { + lock.lock(); + lock.unlock(); + },"t2").start(); + } +} +``` + +*** + +##### 锁消除 + +锁消除是指对于被检测出不可能存在竞争的共享数据的锁进行消除,这是 JVM **即时编译器的优化** + +锁消除主要是通过**逃逸分析**来支持,如果堆上的共享数据不可能逃逸出去被其它线程访问到,那么就可以把它们当成私有数据对待,也就可以将它们的锁进行消除(同步消除:JVM 逃逸分析) + +*** + +##### 锁粗化 + +对相同对象多次加锁,导致线程发生多次重入,频繁的加锁操作就会导致性能损耗,可以使用锁粗化方式优化 + +如果虚拟机探测到一串的操作都对同一个对象加锁,将会把加锁的范围扩展(粗化)到整个操作序列的外部 + +* 一些看起来没有加锁的代码,其实隐式的加了很多锁: + + ```java + public static String concatString(String s1, String s2, String s3) { + return s1 + s2 + s3; + } + ``` + +* String 是一个不可变的类,编译器会对 String 的拼接自动优化。在 JDK 1.5 之前,转化为 StringBuffer 对象的连续 append() 操作,每个 append() 方法中都有一个同步块 + + ```java + public static String concatString(String s1, String s2, String s3) { + StringBuffer sb = new StringBuffer(); + sb.append(s1); + sb.append(s2); + sb.append(s3); + return sb.toString(); + } + ``` + +扩展到第一个 append() 操作之前直至最后一个 append() 操作之后,只需要加锁一次就可以 + +**** + +#### 多把锁 + +多把不相干的锁:一间大屋子有两个功能睡觉、学习,互不相干。现在一人要学习,一人要睡觉,如果只用一间屋子(一个对象锁)的话,那么并发度很低 + +将锁的粒度细分: + +* 好处,是可以增强并发度 +* 坏处,如果一个线程需要同时获得多把锁,就容易发生死锁 + +解决方法:准备多个对象锁 + +```java +public static void main(String[] args) { + BigRoom bigRoom = new BigRoom(); + new Thread(() -> { bigRoom.study(); }).start(); + new Thread(() -> { bigRoom.sleep(); }).start(); +} +class BigRoom { + private final Object studyRoom = new Object(); + private final Object sleepRoom = new Object(); + + public void sleep() throws InterruptedException { + synchronized (sleepRoom) { + System.out.println("sleeping 2 小时"); + Thread.sleep(2000); + } + } + + public void study() throws InterruptedException { + synchronized (studyRoom) { + System.out.println("study 1 小时"); + Thread.sleep(1000); + } + } +} +``` + +*** + +#### 活跃性 + +##### 死锁 + +###### 形成 + +死锁:多个线程同时被阻塞,它们中的一个或者全部都在等待某个资源被释放,由于线程被无限期地阻塞,因此程序不可能正常终止 + +Java 死锁产生的四个必要条件: + +1. 互斥条件,即当资源被一个线程使用(占有)时,别的线程不能使用 +2. 不可剥夺条件,资源请求者不能强制从资源占有者手中夺取资源,资源只能由资源占有者主动释放 +3. 请求和保持条件,即当资源请求者在请求其他的资源的同时保持对原有资源的占有 +4. 循环等待条件,即存在一个等待循环队列:p1 要 p2 的资源,p2 要 p1 的资源,形成了一个等待环路 + +四个条件都成立的时候,便形成死锁。死锁情况下打破上述任何一个条件,便可让死锁消失 + +```java +public class Dead { + public static Object resources1 = new Object(); + public static Object resources2 = new Object(); + public static void main(String[] args) { + new Thread(() -> { + // 线程1:占用资源1 ,请求资源2 + synchronized(resources1){ + System.out.println("线程1已经占用了资源1,开始请求资源2"); + Thread.sleep(2000);//休息两秒,防止线程1直接运行完成。 + //2秒内线程2肯定可以锁住资源2 + synchronized (resources2){ + System.out.println("线程1已经占用了资源2"); + } + }).start(); + new Thread(() -> { + // 线程2:占用资源2 ,请求资源1 + synchronized(resources2){ + System.out.println("线程2已经占用了资源2,开始请求资源1"); + Thread.sleep(2000); + synchronized (resources1){ + System.out.println("线程2已经占用了资源1"); + } + }} + }).start(); + } +} +``` + +*** + +###### 定位 + +定位死锁的方法: + +* 使用 jps 定位进程 id,再用 `jstack id` 定位死锁,找到死锁的线程去查看源码,解决优化 + + ```sh + "Thread-1" #12 prio=5 os_prio=0 tid=0x000000001eb69000 nid=0xd40 waiting formonitor entry [0x000000001f54f000] + java.lang.Thread.State: BLOCKED (on object monitor) + #省略 + "Thread-1" #12 prio=5 os_prio=0 tid=0x000000001eb69000 nid=0xd40 waiting for monitor entry [0x000000001f54f000] + java.lang.Thread.State: BLOCKED (on object monitor) + #省略 + + Found one Java-level deadlock: + =================================================== + "Thread-1": + waiting to lock monitor 0x000000000361d378 (object 0x000000076b5bf1c0, a java.lang.Object), + which is held by "Thread-0" + "Thread-0": + waiting to lock monitor 0x000000000361e768 (object 0x000000076b5bf1d0, a java.lang.Object), + which is held by "Thread-1" + + Java stack information for the threads listed above: + =================================================== + "Thread-1": + at thread.TestDeadLock.lambda$main$1(TestDeadLock.java:28) + - waiting to lock <0x000000076b5bf1c0> (a java.lang.Object) + - locked <0x000000076b5bf1d0> (a java.lang.Object) + at thread.TestDeadLock$$Lambda$2/883049899.run(Unknown Source) + at java.lang.Thread.run(Thread.java:745) + "Thread-0": + at thread.TestDeadLock.lambda$main$0(TestDeadLock.java:15) + - waiting to lock <0x000000076b5bf1d0> (a java.lang.Object) + - locked <0x000000076b5bf1c0> (a java.lang.Object) + at thread.TestDeadLock$$Lambda$1/495053715 + ``` + +* Linux 下可以通过 top 先定位到 CPU 占用高的 Java 进程,再利用 `top -Hp 进程id` 来定位是哪个线程,最后再用 jstack 的输出来看各个线程栈 + +* 避免死锁:避免死锁要注意加锁顺序 + +* 可以使用 jconsole 工具,在 `jdk\bin` 目录下 + +*** + +##### 活锁 + +活锁:指的是任务或者执行者没有被阻塞,由于某些条件没有满足,导致一直重复尝试—失败—尝试—失败的过程 + +两个线程互相改变对方的结束条件,最后谁也无法结束: + +```java +class TestLiveLock { + static volatile int count = 10; + static final Object lock = new Object(); + public static void main(String[] args) { + new Thread(() -> { + // 期望减到 0 退出循环 + while (count > 0) { + Thread.sleep(200); + count--; + System.out.println("线程一count:" + count); + } + }, "t1").start(); + new Thread(() -> { + // 期望超过 20 退出循环 + while (count < 20) { + Thread.sleep(200); + count++; + System.out.println("线程二count:"+ count); + } + }, "t2").start(); + } +} +``` + +*** + +##### 饥饿 + +饥饿:一个线程由于优先级太低,始终得不到 CPU 调度执行,也不能够结束 + +*** + +### wait-ify + +#### 基本使用 + +需要获取对象锁后才可以调用 `锁对象.wait()`,notify 随机唤醒一个线程,notifyAll 唤醒所有线程去竞争 CPU + +Object 类 API: + +```java +public final void notify():唤醒正在等待对象监视器的单个线程。 +public final void notifyAll():唤醒正在等待对象监视器的所有线程。 +public final void wait():导致当前线程等待,直到另一个线程调用该对象的 notify() 方法或 notifyAll()方法。 +public final native void wait(long timeout):有时限的等待, 到n毫秒后结束等待,或是被唤醒 +``` + +说明:**wait 是挂起线程,需要唤醒的都是挂起操作**,阻塞线程可以自己去争抢锁,挂起的线程需要唤醒后去争抢锁 + +对比 sleep(): + +* 原理不同:sleep() 方法是属于 Thread 类,是线程用来控制自身流程的,使此线程暂停执行一段时间而把执行机会让给其他线程;wait() 方法属于 Object 类,用于线程间通信 +* 对**锁的处理机制**不同:调用 sleep() 方法的过程中,线程不会释放对象锁,当调用 wait() 方法的时候,线程会放弃对象锁,进入等待此对象的等待锁定池(不释放锁其他线程怎么抢占到锁执行唤醒操作),但是都会释放 CPU +* 使用区域不同:wait() 方法必须放在**同步控制方法和同步代码块(先获取锁)**中使用,sleep() 方法则可以放在任何地方使用 + +底层原理: + +* Owner 线程发现条件不满足,调用 wait 方法,即可进入 WaitSet 变为 WAITING 状态 +* BLOCKED 和 WAITING 的线程都处于阻塞状态,不占用 CPU 时间片 +* BLOCKED 线程会在 Owner 线程释放锁时唤醒 +* WAITING 线程会在 Owner 线程调用 notify 或 notifyAll 时唤醒,唤醒后并不意味者立刻获得锁,**需要进入 EntryList 重新竞争** + +![](https://seazean.oss-cn-beijing.aliyuncs.com/img/Java/JUC-Monitor工作原理2.png) + +使用wait和notify编写生产者消费者: + +```java +package cn.meowrain; + +import java.util.LinkedList; +import java.util.Queue; +import java.util.concurrent.TimeUnit; + +public class WaitNotifyExample { + public static void main(String[] args) { + SharedQueue sharedQueue = new SharedQueue(100); + Producer producer = new Producer(sharedQueue); + Consumer consumer = new Consumer(sharedQueue); + new Thread(producer,"producer").start(); + new Thread(consumer,"consumer").start(); + } +} + +class SharedQueue { + private final Queue queue = new LinkedList<>(); + private final int maxSize; + + public SharedQueue(int maxSize) { + this.maxSize = maxSize; + } + + public synchronized void produce(int value) throws InterruptedException { + //如果队列满了,生产者等待 + while (queue.size() == maxSize) { + System.out.println(Thread.currentThread().getName() + ":Queue is full,waiting for consumer to consume..."); + wait(); + } + queue.add(value); + System.out.println(Thread.currentThread().getName() + ": Produced " + value + ", Queue size: " + queue.size()); + notify(); + } + + public synchronized void consume() throws InterruptedException { + while (queue.isEmpty()) { + System.out.println(Thread.currentThread().getName() + ":Queue is empty,waiting for producer to produce..."); + wait(); + } + int value = queue.poll(); + System.out.println(Thread.currentThread().getName() + ": Consumed " + value + ", Queue size: " + queue.size()); + notify(); + + } + +} + +class Producer implements Runnable { + private final SharedQueue sharedQueue; + + public Producer(SharedQueue sharedQueue) { + this.sharedQueue = sharedQueue; + } + + @Override + public void run() { + try { + for (int i = 1; i <= 100; i++) { + sharedQueue.produce(i); + TimeUnit.MILLISECONDS.sleep(500); + } + } catch (InterruptedException e) { + throw new RuntimeException(e); + } + } +} + +class Consumer implements Runnable { + private final SharedQueue sharedQueue; + + public Consumer(SharedQueue sharedQueue) { + this.sharedQueue = sharedQueue; + } + + @Override + public void run() { + try { + for (int i = 1; i <= 100; i++) { + sharedQueue.consume(); + TimeUnit.MILLISECONDS.sleep(1000); + } + } catch (InterruptedException e) { + throw new RuntimeException(e); + } + } +} +``` + +*** + +#### 代码优化 + +虚假唤醒:notify 只能随机唤醒一个 WaitSet 中的线程,这时如果有其它线程也在等待,那么就可能唤醒不了正确的线程 + +解决方法:采用 notifyAll + +notifyAll 仅解决某个线程的唤醒问题,使用 if + wait 判断仅有一次机会,一旦条件不成立,无法重新判断 + +解决方法:用 while + wait,当条件不成立,再次 wait + +```java +@Slf4j(topic = "c.demo") +public class demo { + static final Object room = new Object(); + static boolean hasCigarette = false; //有没有烟 + static boolean hasTakeout = false; + + public static void main(String[] args) throws InterruptedException { + new Thread(() -> { + synchronized (room) { + log.debug("有烟没?[{}]", hasCigarette); + while (!hasCigarette) {//while防止虚假唤醒 + log.debug("没烟,先歇会!"); + try { + room.wait(); + } catch (InterruptedException e) { + e.printStackTrace(); + } + } + log.debug("有烟没?[{}]", hasCigarette); + if (hasCigarette) { + log.debug("可以开始干活了"); + } else { + log.debug("没干成活..."); + } + } + }, "小南").start(); + + new Thread(() -> { + synchronized (room) { + Thread thread = Thread.currentThread(); + log.debug("外卖送到没?[{}]", hasTakeout); + if (!hasTakeout) { + log.debug("没外卖,先歇会!"); + try { + room.wait(); + } catch (InterruptedException e) { + e.printStackTrace(); + } + } + log.debug("外卖送到没?[{}]", hasTakeout); + if (hasTakeout) { + log.debug("可以开始干活了"); + } else { + log.debug("没干成活..."); + } + } + }, "小女").start(); + + + Thread.sleep(1000); + new Thread(() -> { + // 这里能不能加 synchronized (room)? + synchronized (room) { + hasTakeout = true; + //log.debug("烟到了噢!"); + log.debug("外卖到了噢!"); + room.notifyAll(); + } + }, "送外卖的").start(); + } +} +``` + +**** + +### park-un + +LockSupport 是用来创建锁和其他同步类的**线程原语** + +LockSupport 类方法: + +* `LockSupport.park()`:暂停当前线程,挂起原语 +* `LockSupport.unpark(暂停的线程对象)`:恢复某个线程的运行 + +```java +public static void main(String[] args) { + Thread t1 = new Thread(() -> { + System.out.println("start..."); //1 + Thread.sleep(1000);// Thread.sleep(3000) + // 先 park 再 unpark 和先 unpark 再 park 效果一样,都会直接恢复线程的运行 + System.out.println("park..."); //2 + LockSupport.park(); + System.out.println("resume...");//4 + },"t1"); + t1.start(); + Thread.sleep(2000); + System.out.println("unpark..."); //3 + LockSupport.unpark(t1); +} +``` + +LockSupport 出现就是为了增强 wait & notify 的功能: + +* wait,notify 和 notifyAll 必须配合 Object Monitor 一起使用,而 park、unpark 不需要 +* park & unpark **以线程为单位**来阻塞和唤醒线程,而 notify 只能随机唤醒一个等待线程,notifyAll 是唤醒所有等待线程 +* park & unpark 可以先 unpark,而 wait & notify 不能先 notify。类比生产消费,先消费发现有产品就消费,没有就等待;先生产就直接产生商品,然后线程直接消费 +* wait 会释放锁资源进入等待队列,**park 不会释放锁资源**,只负责阻塞当前线程,会释放 CPU + +原理:类似生产者消费者 + +* 先 park: + 1. 当前线程调用 Unsafe.park() 方法 + 2. 检查 _counter ,本情况为 0,这时获得_mutex 互斥锁 + 3. 线程进入 _cond 条件变量挂起 + 4. 调用 Unsafe.unpark(Thread_0) 方法,设置_counter 为 1 + 5. 唤醒 _cond 条件变量中的 Thread_0,Thread_0 恢复运行,设置_counter 为 0 + +![](https://seazean.oss-cn-beijing.aliyuncs.com/img/Java/JUC-park原理1.png) + +* 先 unpark: + + 1. 调用 Unsafe.unpark(Thread_0) 方法,设置_counter 为 1 + 2. 当前线程调用 Unsafe.park() 方法 + 3. 检查 _counter ,本情况为 1,这时线程无需挂起,继续运行,设置_counter 为 0 + + ![](https://seazean.oss-cn-beijing.aliyuncs.com/img/Java/JUC-park原理2.png) + +*** + +### 安全分析 + +成员变量和静态变量: + +* 如果它们没有共享,则线程安全 +* 如果它们被共享了,根据它们的状态是否能够改变,分两种情况: + * 如果只有读操作,则线程安全 + * 如果有读写操作,则这段代码是临界区,需要考虑线程安全问题 + +局部变量: + +* 局部变量是线程安全的 +* 局部变量引用的对象不一定线程安全(逃逸分析): + * 如果该对象没有逃离方法的作用访问,它是线程安全的(每一个方法有一个栈帧) + * 如果该对象逃离方法的作用范围,需要考虑线程安全问题(暴露引用) + +常见线程安全类:String、Integer、StringBuffer、Random、Vector、Hashtable、java.util.concurrent 包 + +* 线程安全的是指,多个线程调用它们同一个实例的某个方法时,是线程安全的 + +* **每个方法是原子的,但多个方法的组合不是原子的**,只能保证调用的方法内部安全: + + ```java + Hashtable table = new Hashtable(); + // 线程1,线程2 + if(table.get("key") == null) { + table.put("key", value); + } + ``` + +无状态类线程安全,就是没有成员变量的类 + +不可变类线程安全:String、Integer 等都是不可变类,**内部的状态不可以改变**,所以方法是线程安全 + +* replace 等方法底层是新建一个对象,复制过去 + + ```java + Map map = new HashMap<>(); // 线程不安全 + String S1 = "..."; // 线程安全 + final String S2 = "..."; // 线程安全 + Date D1 = new Date(); // 线程不安全 + final Date D2 = new Date(); // 线程不安全,final让D2引用的对象不能变,但对象的内容可以变 + ``` + +抽象方法如果有参数,被重写后行为不确定可能造成线程不安全,被称之为外星方法:`public abstract foo(Student s);` + +*** + +### 同步模式 + +#### 保护性暂停 + +##### 单任务版 + +Guarded Suspension,用在一个线程等待另一个线程的执行结果 + +* 有一个结果需要从一个线程传递到另一个线程,让它们关联同一个 GuardedObject +* 如果有结果不断从一个线程到另一个线程那么可以使用消息队列(见生产者/消费者) +* JDK 中,join 的实现、Future 的实现,采用的就是此模式 + +![](https://seazean.oss-cn-beijing.aliyuncs.com/img/Java/JUC-保护性暂停.png) + +```java +public static void main(String[] args) { + GuardedObject object = new GuardedObjectV2(); + new Thread(() -> { + sleep(1); + object.complete(Arrays.asList("a", "b", "c")); + }).start(); + + Object response = object.get(2500); + if (response != null) { + log.debug("get response: [{}] lines", ((List) response).size()); + } else { + log.debug("can't get response"); + } +} + +class GuardedObject { + private Object response; + private final Object lock = new Object(); + + //获取结果 + //timeout :最大等待时间 + public Object get(long millis) { + synchronized (lock) { + // 1) 记录最初时间 + long begin = System.currentTimeMillis(); + // 2) 已经经历的时间 + long timePassed = 0; + while (response == null) { + // 4) 假设 millis 是 1000,结果在 400 时唤醒了,那么还有 600 要等 + long waitTime = millis - timePassed; + log.debug("waitTime: {}", waitTime); + //经历时间超过最大等待时间退出循环 + if (waitTime <= 0) { + log.debug("break..."); + break; + } + try { + lock.wait(waitTime); + } catch (InterruptedException e) { + e.printStackTrace(); + } + // 3) 如果提前被唤醒,这时已经经历的时间假设为 400 + timePassed = System.currentTimeMillis() - begin; + log.debug("timePassed: {}, object is null {}", + timePassed, response == null); + } + return response; + } + } + + //产生结果 + public void complete(Object response) { + synchronized (lock) { + // 条件满足,通知等待线程 + this.response = response; + log.debug("notify..."); + lock.notifyAll(); + } + } +} +``` + +##### 多任务版 + +多任务版保护性暂停: + +![](https://seazean.oss-cn-beijing.aliyuncs.com/img/Java/JUC-保护性暂停多任务版.png) + +```java +public static void main(String[] args) throws InterruptedException { + for (int i = 0; i < 3; i++) { + new People().start(); + } + Thread.sleep(1000); + for (Integer id : Mailboxes.getIds()) { + new Postman(id, id + "号快递到了").start(); + } +} + +@Slf4j(topic = "c.People") +class People extends Thread{ + @Override + public void run() { + // 收信 + GuardedObject guardedObject = Mailboxes.createGuardedObject(); + log.debug("开始收信i d:{}", guardedObject.getId()); + Object mail = guardedObject.get(5000); + log.debug("收到信id:{},内容:{}", guardedObject.getId(),mail); + } +} + +class Postman extends Thread{ + private int id; + private String mail; + //构造方法 + @Override + public void run() { + GuardedObject guardedObject = Mailboxes.getGuardedObject(id); + log.debug("开始送信i d:{},内容:{}", guardedObject.getId(),mail); + guardedObject.complete(mail); + } +} + +class Mailboxes { + private static Map boxes = new Hashtable<>(); + private static int id = 1; + + //产生唯一的id + private static synchronized int generateId() { + return id++; + } + + public static GuardedObject getGuardedObject(int id) { + return boxes.remove(id); + } + + public static GuardedObject createGuardedObject() { + GuardedObject go = new GuardedObject(generateId()); + boxes.put(go.getId(), go); + return go; + } + + public static Set getIds() { + return boxes.keySet(); + } +} +class GuardedObject { + //标识,Guarded Object + private int id;//添加get set方法 +} +``` + +**** + +#### 顺序输出 + +顺序输出 2 1 + +```java +public static void main(String[] args) throws InterruptedException { + Thread t1 = new Thread(() -> { + while (true) { + //try { Thread.sleep(1000); } catch (InterruptedException e) { } + // 当没有许可时,当前线程暂停运行;有许可时,用掉这个许可,当前线程恢复运行 + LockSupport.park(); + System.out.println("1"); + } + }); + Thread t2 = new Thread(() -> { + while (true) { + System.out.println("2"); + // 给线程 t1 发放『许可』(多次连续调用 unpark 只会发放一个『许可』) + LockSupport.unpark(t1); + try { Thread.sleep(500); } catch (InterruptedException e) { } + } + }); + t1.start(); + t2.start(); +} +``` + +*** + +#### 交替输出 + +连续输出 5 次 abc + +```java +public class day2_14 { + public static void main(String[] args) throws InterruptedException { + AwaitSignal awaitSignal = new AwaitSignal(5); + Condition a = awaitSignal.newCondition(); + Condition b = awaitSignal.newCondition(); + Condition c = awaitSignal.newCondition(); + new Thread(() -> { + awaitSignal.print("a", a, b); + }).start(); + new Thread(() -> { + awaitSignal.print("b", b, c); + }).start(); + new Thread(() -> { + awaitSignal.print("c", c, a); + }).start(); + + Thread.sleep(1000); + awaitSignal.lock(); + try { + a.signal(); + } finally { + awaitSignal.unlock(); + } + } +} + +class AwaitSignal extends ReentrantLock { + private int loopNumber; + + public AwaitSignal(int loopNumber) { + this.loopNumber = loopNumber; + } + //参数1:打印内容 参数二:条件变量 参数二:唤醒下一个 + public void print(String str, Condition condition, Condition next) { + for (int i = 0; i < loopNumber; i++) { + lock(); + try { + condition.await(); + System.out.print(str); + next.signal(); + } catch (InterruptedException e) { + e.printStackTrace(); + } finally { + unlock(); + } + } + } +} +``` + +*** + +### 异步模式 + +#### 传统版 + +异步模式之生产者/消费者: + +```java +class ShareData { + private int number = 0; + private Lock lock = new ReentrantLock(); + private Condition condition = lock.newCondition(); + + public void increment() throws Exception{ + // 同步代码块,加锁 + lock.lock(); + try { + // 判断 防止虚假唤醒 + while(number != 0) { + // 等待不能生产 + condition.await(); + } + // 干活 + number++; + System.out.println(Thread.currentThread().getName() + "\t " + number); + // 通知 唤醒 + condition.signalAll(); + } catch (Exception e) { + e.printStackTrace(); + } finally { + lock.unlock(); + } + } + + public void decrement() throws Exception{ + // 同步代码块,加锁 + lock.lock(); + try { + // 判断 防止虚假唤醒 + while(number == 0) { + // 等待不能消费 + condition.await(); + } + // 干活 + number--; + System.out.println(Thread.currentThread().getName() + "\t " + number); + // 通知 唤醒 + condition.signalAll(); + } catch (Exception e) { + e.printStackTrace(); + } finally { + lock.unlock(); + } + } +} + +public class TraditionalProducerConsumer { + public static void main(String[] args) { + ShareData shareData = new ShareData(); + // t1线程,生产 + new Thread(() -> { + for (int i = 0; i < 5; i++) { + shareData.increment(); + } + }, "t1").start(); + + // t2线程,消费 + new Thread(() -> { + for (int i = 0; i < 5; i++) { + shareData.decrement(); + } + }, "t2").start(); + } +} +``` + +#### 改进版 + +异步模式之生产者/消费者: + +* 消费队列可以用来平衡生产和消费的线程资源,不需要产生结果和消费结果的线程一一对应 +* 生产者仅负责产生结果数据,不关心数据该如何处理,而消费者专心处理结果数据 +* 消息队列是有容量限制的,满时不会再加入数据,空时不会再消耗数据 +* JDK 中各种阻塞队列,采用的就是这种模式 + +![](https://seazean.oss-cn-beijing.aliyuncs.com/img/Java/JUC-生产者消费者模式.png) + +```java +public class demo { + public static void main(String[] args) { + MessageQueue queue = new MessageQueue(2); + for (int i = 0; i < 3; i++) { + int id = i; + new Thread(() -> { + queue.put(new Message(id,"值"+id)); + }, "生产者" + i).start(); + } + + new Thread(() -> { + while (true) { + try { + Thread.sleep(1000); + Message message = queue.take(); + } catch (InterruptedException e) { + e.printStackTrace(); + } + } + },"消费者").start(); + } +} + +//消息队列类,Java间线程之间通信 +class MessageQueue { + private LinkedList list = new LinkedList<>();//消息的队列集合 + private int capacity;//队列容量 + public MessageQueue(int capacity) { + this.capacity = capacity; + } + + //获取消息 + public Message take() { + //检查队列是否为空 + synchronized (list) { + while (list.isEmpty()) { + try { + sout(Thread.currentThread().getName() + ":队列为空,消费者线程等待"); + list.wait(); + } catch (InterruptedException e) { + e.printStackTrace(); + } + } + //从队列的头部获取消息返回 + Message message = list.removeFirst(); + sout(Thread.currentThread().getName() + ":已消费消息--" + message); + list.notifyAll(); + return message; + } + } + + //存入消息 + public void put(Message message) { + synchronized (list) { + //检查队列是否满 + while (list.size() == capacity) { + try { + sout(Thread.currentThread().getName()+":队列为已满,生产者线程等待"); + list.wait(); + } catch (InterruptedException e) { + e.printStackTrace(); + } + } + //将消息加入队列尾部 + list.addLast(message); + sout(Thread.currentThread().getName() + ":已生产消息--" + message); + list.notifyAll(); + } + } +} + +final class Message { + private int id; + private Object value; + //get set +} +``` + +*** + +#### 阻塞队列 + +```java +public static void main(String[] args) { + ExecutorService consumer = Executors.newFixedThreadPool(1); + ExecutorService producer = Executors.newFixedThreadPool(1); + BlockingQueue queue = new SynchronousQueue<>(); + producer.submit(() -> { + try { + System.out.println("生产..."); + Thread.sleep(1000); + queue.put(10); + } catch (InterruptedException e) { + e.printStackTrace(); + } + }); + consumer.submit(() -> { + try { + System.out.println("等待消费..."); + Integer result = queue.take(); + System.out.println("结果为:" + result); + } catch (InterruptedException e) { + e.printStackTrace(); + } + }); +} +``` + +**** + +## 内存 + +### JMM + +#### 内存模型 + +Java 内存模型是 Java Memory Model(JMM),本身是一种**抽象的概念**,实际上并不存在,描述的是一组规则或规范,通过这组规范定义了程序中各个变量(包括实例字段,静态字段和构成数组对象的元素)的访问方式 + +JMM 作用: + +* 屏蔽各种硬件和操作系统的内存访问差异,实现让 Java 程序在各种平台下都能达到一致的内存访问效果 +* 规定了线程和内存之间的一些关系 + +根据 JMM 的设计,系统存在一个主内存(Main Memory),Java 中所有变量都存储在主存中,对于所有线程都是共享的;每条线程都有自己的工作内存(Working Memory),工作内存中保存的是主存中某些**变量的拷贝**,线程对所有变量的操作都是先对变量进行拷贝,然后在工作内存中进行,不能直接操作主内存中的变量;线程之间无法相互直接访问,线程间的通信(传递)必须通过主内存来完成 + +![](https://seazean.oss-cn-beijing.aliyuncs.com/img/Java/JMM内存模型.png) + +主内存和工作内存: + +* 主内存:计算机的内存,也就是经常提到的 8G 内存,16G 内存,存储所有共享变量的值 +* 工作内存:存储该线程使用到的共享变量在主内存的的值的副本拷贝 + +**JVM 和 JMM 之间的关系**:JMM 中的主内存、工作内存与 JVM 中的 Java 堆、栈、方法区等并不是同一个层次的内存划分,这两者基本上是没有关系的,如果两者一定要勉强对应起来: + +* 主内存主要对应于 Java 堆中的对象实例数据部分,而工作内存则对应于虚拟机栈中的部分区域 +* 从更低层次上说,主内存直接对应于物理硬件的内存,工作内存对应寄存器和高速缓存 + +*** + +#### 内存交互 + +Java 内存模型定义了 8 个操作来完成主内存和工作内存的交互操作,每个操作都是**原子**的 + +非原子协定:没有被 volatile 修饰的 long、double 外,默认按照两次 32 位的操作 + + + +* lock:作用于主内存,将一个变量标识为被一个线程独占状态(对应 monitorenter) +* unclock:作用于主内存,将一个变量从独占状态释放出来,释放后的变量才可以被其他线程锁定(对应 monitorexit) +* read:作用于主内存,把一个变量的值从主内存传输到工作内存中 +* load:作用于工作内存,在 read 之后执行,把 read 得到的值放入工作内存的变量副本中 +* use:作用于工作内存,把工作内存中一个变量的值传递给**执行引擎**,每当遇到一个使用到变量的操作时都要使用该指令 +* assign:作用于工作内存,把从执行引擎接收到的一个值赋给工作内存的变量 +* store:作用于工作内存,把工作内存的一个变量的值传送到主内存中 +* write:作用于主内存,在 store 之后执行,把 store 得到的值放入主内存的变量中 + +参考文章: + +*** + +#### 三大特性 + +##### 可见性 + +可见性:是指当多个线程访问同一个变量时,一个线程修改了这个变量的值,其他线程能够立即看得到修改的值 + +存在不可见问题的根本原因是由于缓存的存在,线程持有的是共享变量的副本,无法感知其他线程对于共享变量的更改,导致读取的值不是最新的。但是 final 修饰的变量是**不可变**的,就算有缓存,也不会存在不可见的问题 + +main 线程对 run 变量的修改对于 t 线程不可见,导致了 t 线程无法停止: + +```java +static boolean run = true; //添加volatile +public static void main(String[] args) throws InterruptedException { + Thread t = new Thread(()->{ + while(run){ + // .... + } + }); + t.start(); + sleep(1); + run = false; // 线程t不会如预想的停下来 +} +``` + +原因: + +* 初始状态, t 线程刚开始从主内存读取了 run 的值到工作内存 +* 因为 t 线程要频繁从主内存中读取 run 的值,JIT 编译器会将 run 的值缓存至自己工作内存中的高速缓存中,减少对主存中 run 的访问,提高效率 +* 1 秒之后,main 线程修改了 run 的值,并同步至主存,而 t 是从自己工作内存中的高速缓存中读取这个变量的值,结果永远是旧值 + +![](https://seazean.oss-cn-beijing.aliyuncs.com/img/Java/JMM-可见性例子.png) + +*** + +##### 原子性 + +原子性:不可分割,完整性,也就是说某个线程正在做某个具体业务时,中间不可以被分割,需要具体完成,要么同时成功,要么同时失败,保证指令不会受到线程上下文切换的影响 + +定义原子操作的使用规则: + +1. 不允许 read 和 load、store 和 write 操作之一单独出现,必须顺序执行,但是不要求连续 +2. 不允许一个线程丢弃 assign 操作,必须同步回主存 +3. 不允许一个线程无原因地(没有发生过任何 assign 操作)把数据从工作内存同步会主内存中 +4. 一个新的变量只能在主内存中诞生,不允许在工作内存中直接使用一个未被初始化(assign 或者 load)的变量,即对一个变量实施 use 和 store 操作之前,必须先自行 assign 和 load 操作 +5. 一个变量在同一时刻只允许一条线程对其进行 lock 操作,但 lock 操作可以被同一线程重复执行多次,多次执行 lock 后,只有**执行相同次数的 unlock** 操作,变量才会被解锁,**lock 和 unlock 必须成对出现** +6. 如果对一个变量执行 lock 操作,将会**清空工作内存中此变量的值**,在执行引擎使用这个变量之前需要重新从主存加载 +7. 如果一个变量事先没有被 lock 操作锁定,则不允许执行 unlock 操作,也不允许去 unlock 一个被其他线程锁定的变量 +8. 对一个变量执行 unlock 操作之前,必须**先把此变量同步到主内存**中(执行 store 和 write 操作) + +*** + +##### 有序性 + +有序性:在本线程内观察,所有操作都是有序的;在一个线程观察另一个线程,所有操作都是无序的,无序是因为发生了指令重排序 + +CPU 的基本工作是执行存储的指令序列,即程序,程序的执行过程实际上是不断地取出指令、分析指令、执行指令的过程,为了提高性能,编译器和处理器会对指令重排,一般分为以下三种: + +```java +源代码 -> 编译器优化的重排 -> 指令并行的重排 -> 内存系统的重排 -> 最终执行指令 +``` + +现代 CPU 支持多级指令流水线,几乎所有的冯•诺伊曼型计算机的 CPU,其工作都可以分为 5 个阶段:取指令、指令译码、执行指令、访存取数和结果写回,可以称之为**五级指令流水线**。CPU 可以在一个时钟周期内,同时运行五条指令的**不同阶段**(每个线程不同的阶段),本质上流水线技术并不能缩短单条指令的执行时间,但变相地提高了指令地吞吐率 + +处理器在进行重排序时,必须要考虑**指令之间的数据依赖性** + +* 单线程环境也存在指令重排,由于存在依赖性,最终执行结果和代码顺序的结果一致 +* 多线程环境中线程交替执行,由于编译器优化重排,会获取其他线程处在不同阶段的指令同时执行 + +补充知识: + +* 指令周期是取出一条指令并执行这条指令的时间,一般由若干个机器周期组成 +* 机器周期也称为 CPU 周期,一条指令的执行过程划分为若干个阶段(如取指、译码、执行等),每一阶段完成一个基本操作,完成一个基本操作所需要的时间称为机器周期 +* 振荡周期指周期性信号作周期性重复变化的时间间隔 + +*** + +### cache + +#### 缓存机制 + +##### 缓存结构 + +在计算机系统中,CPU 高速缓存(CPU Cache,简称缓存)是用于减少处理器访问内存所需平均时间的部件;在存储体系中位于自顶向下的第二层,仅次于 CPU 寄存器;其容量远小于内存,但速度却可以接近处理器的频率 + +CPU 处理器速度远远大于在主内存中的,为了解决速度差异,在它们之间架设了多级缓存,如 L1、L2、L3 级别的缓存,这些缓存离 CPU 越近就越快,将频繁操作的数据缓存到这里,加快访问速度 + + + +| 从 CPU 到 | 大约需要的时钟周期 | +| --------- | --------------------------------- | +| 寄存器 | 1 cycle (4GHz 的 CPU 约为 0.25ns) | +| L1 | 3~4 cycle | +| L2 | 10~20 cycle | +| L3 | 40~45 cycle | +| 内存 | 120~240 cycle | + +##### 缓存使用 + +当处理器发出内存访问请求时,会先查看缓存内是否有请求数据,如果存在(命中),则不用访问内存直接返回该数据;如果不存在(失效),则要先把内存中的相应数据载入缓存,再将其返回处理器 + +缓存之所以有效,主要因为程序运行时对内存的访问呈现局部性(Locality)特征。既包括空间局部性(Spatial Locality),也包括时间局部性(Temporal Locality),有效利用这种局部性,缓存可以达到极高的命中率 + +*** + +#### 伪共享 + +**缓存以缓存行 cache line 为单位**,每个缓存行对应着一块内存,一般是 64 byte(8 个 long),在 CPU 从主存获取数据时,以 cache line 为单位加载,于是相邻的数据会一并加载到缓存中 + +缓存会造成数据副本的产生,即同一份数据会缓存在不同核心的缓存行中,CPU 要保证数据的一致性,需要做到某个 CPU 核心更改了数据,其它 CPU 核心对应的**整个缓存行必须失效**,这就是伪共享 + + + +解决方法: + +* padding:通过填充,让数据落在不同的 cache line 中 + +* @Contended:原理参考 无锁 → Adder → 优化机制 → 伪共享 + +Linux 查看 CPU 缓存行: + +* 命令:`cat /sys/devices/system/cpu/cpu0/cache/index0/coherency_line_size64` +* 内存地址格式:[高位组标记] [低位索引] [偏移量] + +*** + +#### 缓存一致 + +缓存一致性:当多个处理器运算任务都涉及到同一块主内存区域的时候,将可能导致各自的缓存数据不一样 + + + +MESI(Modified Exclusive Shared Or Invalid)是一种广泛使用的**支持写回策略的缓存一致性协议**,CPU 中每个缓存行(caceh line)使用 4 种状态进行标记(使用额外的两位 bit 表示): + +* M:被修改(Modified) + + 该缓存行只被缓存在该 CPU 的缓存中,并且是被修改过的,与主存中的数据不一致 (dirty),该缓存行中的内存需要写回 (write back) 主存。该状态的数据再次被修改不会发送广播,因为其他核心的数据已经在第一次修改时失效一次 + + 当被写回主存之后,该缓存行的状态会变成独享 (exclusive) 状态 + +* E:独享的(Exclusive) + + 该缓存行只被缓存在该 CPU 的缓存中,是未被修改过的 (clear),与主存中数据一致,修改数据不需要通知其他 CPU 核心,该状态可以在任何时刻有其它 CPU 读取该内存时变成共享状态 (shared) + + 当 CPU 修改该缓存行中内容时,该状态可以变成 Modified 状态 + +* S:共享的(Shared) + + 该状态意味着该缓存行可能被多个 CPU 缓存,并且各个缓存中的数据与主存数据一致,当 CPU 修改该缓存行中,会向其它 CPU 核心广播一个请求,使该缓存行变成无效状态 (Invalid),然后再更新当前 Cache 里的数据 + +* I:无效的(Invalid) + + 该缓存是无效的,可能有其它 CPU 修改了该缓存行 + +解决方法:各个处理器访问缓存时都遵循一些协议,在读写时要根据协议进行操作,协议主要有 MSI、MESI 等 + +**** + +#### 处理机制 + +单核 CPU 处理器会自动保证基本内存操作的原子性 + +多核 CPU 处理器,每个 CPU 处理器内维护了一块内存,每个内核内部维护着一块缓存,当多线程并发读写时,就会出现缓存数据不一致的情况。处理器提供: + +* 总线锁定:当处理器要操作共享变量时,在 BUS 总线上发出一个 LOCK 信号,其他处理器就无法操作这个共享变量,该操作会导致大量阻塞,从而增加系统的性能开销(**平台级别的加锁**) +* 缓存锁定:当处理器对缓存中的共享变量进行了操作,其他处理器有嗅探机制,将各自缓存中的该共享变量的失效,读取时会重新从主内存中读取最新的数据,基于 MESI 缓存一致性协议来实现 + +有如下两种情况处理器不会使用缓存锁定: + +* 当操作的数据跨多个缓存行,或没被缓存在处理器内部,则处理器会使用总线锁定 + +* 有些处理器不支持缓存锁定,比如:Intel 486 和 Pentium 处理器也会调用总线锁定 + +总线机制: + +* 总线嗅探:每个处理器通过嗅探在总线上传播的数据来检查自己缓存值是否过期了,当处理器发现自己的缓存对应的内存地址的数据被修改,就**将当前处理器的缓存行设置为无效状态**,当处理器对这个数据进行操作时,会重新从内存中把数据读取到处理器缓存中 + +* 总线风暴:当某个 CPU 核心更新了 Cache 中的数据,要把该事件广播通知到其他核心(**写传播**),CPU 需要每时每刻监听总线上的一切活动,但是不管别的核心的 Cache 是否缓存相同的数据,都需要发出一个广播事件,不断的从主内存嗅探和 CAS 循环,无效的交互会导致总线带宽达到峰值;因此不要大量使用 volatile 关键字,使用 volatile、syschonized 都需要根据实际场景 + +*** + +### volatile + +#### 同步机制 + +volatile 是 Java 虚拟机提供的**轻量级**的同步机制(三大特性) + +* 保证可见性 +* 不保证原子性 +* 保证有序性(禁止指令重排) + +性能:volatile 修饰的变量进行读操作与普通变量几乎没什么差别,但是写操作相对慢一些,因为需要在本地代码中插入很多内存屏障来保证指令不会发生乱序执行,但是开销比锁要小 + +synchronized 无法禁止指令重排和处理器优化,为什么可以保证有序性可见性 + +* 加了锁之后,只能有一个线程获得到了锁,获得不到锁的线程就要阻塞,所以同一时间只有一个线程执行,相当于单线程,由于数据依赖性的存在,单线程的指令重排是没有问题的 +* 线程加锁前,将**清空工作内存**中共享变量的值,使用共享变量时需要从主内存中重新读取最新的值;线程解锁前,必须把共享变量的最新值**刷新到主内存**中(JMM 内存交互章节有讲) + +*** + +#### 指令重排 + +volatile 修饰的变量,可以禁用指令重排 + +指令重排实例: + +* example 1: + + ```java + public void mySort() { + int x = 11; //语句1 + int y = 12; //语句2 谁先执行效果一样 + x = x + 5; //语句3 + y = x * x; //语句4 + } + ``` + + 执行顺序是:1 2 3 4、2 1 3 4、1 3 2 4 + + 指令重排也有限制不会出现:4321,语句 4 需要依赖于 y 以及 x 的申明,因为存在数据依赖,无法首先执行 + +* example 2: + + ```java + int num = 0; + boolean ready = false; + // 线程1 执行此方法 + public void actor1(I_Result r) { + if(ready) { + r.r1 = num + num; + } else { + r.r1 = 1; + } + } + // 线程2 执行此方法 + public void actor2(I_Result r) { + num = 2; + ready = true; + } + ``` + + 情况一:线程 1 先执行,ready = false,结果为 r.r1 = 1 + + 情况二:线程 2 先执行 num = 2,但还没执行 ready = true,线程 1 执行,结果为 r.r1 = 1 + + 情况三:线程 2 先执行 ready = true,线程 1 执行,进入 if 分支结果为 r.r1 = 4 + + 情况四:线程 2 执行 ready = true,切换到线程 1,进入 if 分支为 r.r1 = 0,再切回线程 2 执行 num = 2,发生指令重排 + +**** + +#### 底层原理 + +##### 缓存一致 + +使用 volatile 修饰的共享变量,底层通过汇编 lock 前缀指令进行缓存锁定,在线程修改完共享变量后写回主存,其他的 CPU 核心上运行的线程通过 CPU 总线嗅探机制会修改其共享变量为失效状态,读取时会重新从主内存中读取最新的数据 + +lock 前缀指令就相当于内存屏障,Memory Barrier(Memory Fence) + +* 对 volatile 变量的写指令后会加入写屏障 +* 对 volatile 变量的读指令前会加入读屏障 + +内存屏障有三个作用: + +* 确保对内存的读-改-写操作原子执行 +* 阻止屏障两侧的指令重排序 +* 强制把缓存中的脏数据写回主内存,让缓存行中相应的数据失效 + +*** + +##### 内存屏障 + +保证**可见性**: + +* 写屏障(sfence,Store Barrier)保证在该屏障之前的,对共享变量的改动,都同步到主存当中 + + ```java + public void actor2(I_Result r) { + num = 2; + ready = true; // ready 是 volatile 赋值带写屏障 + // 写屏障 + } + ``` + +* 读屏障(lfence,Load Barrier)保证在该屏障之后的,对共享变量的读取,从主存刷新变量值,加载的是主存中最新数据 + + ```java + public void actor1(I_Result r) { + // 读屏障 + // ready 是 volatile 读取值带读屏障 + if(ready) { + r.r1 = num + num; + } else { + r.r1 = 1; + } + } + ``` + + + +* 全能屏障:mfence(modify/mix Barrier),兼具 sfence 和 lfence 的功能 + +保证**有序性**: + +* 写屏障会确保指令重排序时,不会将写屏障之前的代码排在写屏障之后 +* 读屏障会确保指令重排序时,不会将读屏障之后的代码排在读屏障之前 + +不能解决指令交错: + +* 写屏障仅仅是保证之后的读能够读到最新的结果,但不能保证其他线程的读跑到写屏障之前 + +* 有序性的保证也只是保证了本线程内相关代码不被重排序 + + ```java + volatile i = 0; + new Thread(() -> {i++}); + new Thread(() -> {i--}); + ``` + + i++ 反编译后的指令: + + ```java + 0: iconst_1 // 当int取值 -1~5 时,JVM采用iconst指令将常量压入栈中 + 1: istore_1 // 将操作数栈顶数据弹出,存入局部变量表的 slot 1 + 2: iinc 1, 1 + ``` + + + +**** + +##### 交互规则 + +对于 volatile 修饰的变量: + +* 线程对变量的 use 与 load、read 操作是相关联的,所以变量使用前必须先从主存加载 +* 线程对变量的 assign 与 store、write 操作是相关联的,所以变量使用后必须同步至主存 +* 线程 1 和线程 2 谁先对变量执行 read 操作,就会先进行 write 操作,防止指令重排 + +*** + +#### 双端检锁 + +##### 检锁机制 + +Double-Checked Locking:双端检锁机制 + +DCL(双端检锁)机制不一定是线程安全的,原因是有指令重排的存在,加入 volatile 可以禁止指令重排 + +```java +public final class Singleton { + private Singleton() { } + private static Singleton INSTANCE = null; + + public static Singleton getInstance() { + if(INSTANCE == null) { // t2,这里的判断不是线程安全的 + // 首次访问会同步,而之后的使用没有 synchronized + synchronized(Singleton.class) { + // 这里是线程安全的判断,防止其他线程在当前线程等待锁的期间完成了初始化 + if (INSTANCE == null) { + INSTANCE = new Singleton(); + } + } + } + return INSTANCE; + } +} +``` + +不锁 INSTANCE 的原因: + +* INSTANCE 要重新赋值 +* INSTANCE 是 null,线程加锁之前需要获取对象的引用,设置对象头,null 没有引用 + +实现特点: + +* 懒惰初始化 +* 首次使用 getInstance() 才使用 synchronized 加锁,后续使用时无需加锁 +* 第一个 if 使用了 INSTANCE 变量,是在同步块之外,但在多线程环境下会产生问题 + +*** + +##### DCL问题 + +getInstance 方法对应的字节码为: + +```java +0: getstatic #2 // Field INSTANCE:Ltest/Singleton; +3: ifnonnull 37 +6: ldc #3 // class test/Singleton +8: dup +9: astore_0 +10: monitorenter +11: getstatic #2 // Field INSTANCE:Ltest/Singleton; +14: ifnonnull 27 +17: new #3 // class test/Singleton +20: dup +21: invokespecial #4 // Method "":()V +24: putstatic #2 // Field INSTANCE:Ltest/Singleton; +27: aload_0 +28: monitorexit +29: goto 37 +32: astore_1 +33: aload_0 +34: monitorexit +35: aload_1 +36: athrow +37: getstatic #2 // Field INSTANCE:Ltest/Singleton; +40: areturn +``` + +* 17 表示创建对象,将对象引用入栈 +* 20 表示复制一份对象引用,引用地址 +* 21 表示利用一个对象引用,调用构造方法初始化对象 +* 24 表示利用一个对象引用,赋值给 static INSTANCE + +**步骤 21 和 24 之间不存在数据依赖关系**,而且无论重排前后,程序的执行结果在单线程中并没有改变,因此这种重排优化是允许的 + +* 关键在于 0:getstatic 这行代码在 monitor 控制之外,可以越过 monitor 读取 INSTANCE 变量的值 +* 当其他线程访问 INSTANCE 不为 null 时,由于 INSTANCE 实例未必已初始化,那么 t2 拿到的是将是一个未初始化完毕的单例返回,这就造成了线程安全的问题 + +![](https://seazean.oss-cn-beijing.aliyuncs.com/img/Java/JMM-DCL出现的问题.png) + +*** + +##### 解决方法 + +指令重排只会保证串行语义的执行一致性(单线程),但并不会关系多线程间的语义一致性 + +引入 volatile,来保证出现指令重排的问题,从而保证单例模式的线程安全性: + +```java +private static volatile SingletonDemo INSTANCE = null; +``` + +*** + +### ha-be + +happens-before 先行发生 + +Java 内存模型具备一些先天的“有序性”,即不需要通过任何同步手段(volatile、synchronized 等)就能够得到保证的安全,这个通常也称为 happens-before 原则,它是可见性与有序性的一套规则总结 + +不符合 happens-before 规则,JMM 并不能保证一个线程的可见性和有序性 + +1. 程序次序规则 (Program Order Rule):一个线程内,逻辑上书写在前面的操作先行发生于书写在后面的操作 ,因为多个操作之间有先后依赖关系,则不允许对这些操作进行重排序 + +2. 锁定规则 (Monitor Lock Rule):一个 unlock 操作先行发生于后面(时间的先后)对同一个锁的 lock 操作,所以线程解锁 m 之前对变量的写(解锁前会刷新到主内存中),对于接下来对 m 加锁的其它线程对该变量的读可见 + +3. **volatile 变量规则** (Volatile Variable Rule):对 volatile 变量的写操作先行发生于后面对这个变量的读 + +4. 传递规则 (Transitivity):具有传递性,如果操作 A 先行发生于操作 B,而操作 B 又先行发生于操作 C,则可以得出操作 A 先行发生于操作 C + +5. 线程启动规则 (Thread Start Rule):Thread 对象的 start()方 法先行发生于此线程中的每一个操作 + + ```java + static int x = 10;//线程 start 前对变量的写,对该线程开始后对该变量的读可见 + new Thread(()->{ System.out.println(x); },"t1").start(); + ``` + +6. 线程中断规则 (Thread Interruption Rule):对线程 interrupt() 方法的调用先行发生于被中断线程的代码检测到中断事件的发生 + +7. 线程终止规则 (Thread Termination Rule):线程中所有的操作都先行发生于线程的终止检测,可以通过 Thread.join() 方法结束、Thread.isAlive() 的返回值手段检测到线程已经终止执行 + +8. 对象终结规则(Finaizer Rule):一个对象的初始化完成(构造函数执行结束)先行发生于它的 finalize() 方法的开始 + +*** + +### 设计模式 + +#### 终止模式 + +终止模式之两阶段终止模式:停止标记用 volatile 是为了保证该变量在多个线程之间的可见性 + +```java +class TwoPhaseTermination { + // 监控线程 + private Thread monitor; + // 停止标记 + private volatile boolean stop = false;; + + // 启动监控线程 + public void start() { + monitor = new Thread(() -> { + while (true) { + Thread thread = Thread.currentThread(); + if (stop) { + System.out.println("后置处理"); + break; + } + try { + Thread.sleep(1000);// 睡眠 + System.out.println(thread.getName() + "执行监控记录"); + } catch (InterruptedException e) { + System.out.println("被打断,退出睡眠"); + } + } + }); + monitor.start(); + } + + // 停止监控线程 + public void stop() { + stop = true; + monitor.interrupt();// 让线程尽快退出Timed Waiting + } +} +// 测试 +public static void main(String[] args) throws InterruptedException { + TwoPhaseTermination tpt = new TwoPhaseTermination(); + tpt.start(); + Thread.sleep(3500); + System.out.println("停止监控"); + tpt.stop(); +} +``` + +**** + +#### Balking + +Balking (犹豫)模式用在一个线程发现另一个线程或本线程已经做了某一件相同的事,那么本线程就无需再做了,直接结束返回 + +```java +public class MonitorService { + // 用来表示是否已经有线程已经在执行启动了 + private volatile boolean starting = false; + public void start() { + System.out.println("尝试启动监控线程..."); + synchronized (this) { + if (starting) { + return; + } + starting = true; + } + // 真正启动监控线程... + } +} +``` + +对比保护性暂停模式:保护性暂停模式用在一个线程等待另一个线程的执行结果,当条件不满足时线程等待 + +例子:希望 doInit() 方法仅被调用一次,下面的实现出现的问题: + +* 当 t1 线程进入 init() 准备 doInit(),t2 线程进来,initialized 还为f alse,则 t2 就又初始化一次 +* volatile 适合一个线程写,其他线程读的情况,这个代码需要加锁 + +```java +public class TestVolatile { + volatile boolean initialized = false; + + void init() { + if (initialized) { + return; + } + doInit(); + initialized = true; + } + private void doInit() { + } +} +``` + +**** + +## 无锁 + +### CAS + +#### 原理 + +无锁编程:Lock Free + +CAS 的全称是 Compare-And-Swap,是 **CPU 并发原语** + +* CAS 并发原语体现在 Java 语言中就是 sun.misc.Unsafe 类的各个方法,调用 UnSafe 类中的 CAS 方法,JVM 会实现出 CAS 汇编指令,这是一种完全依赖于硬件的功能,实现了原子操作 +* CAS 是一种系统原语,原语属于操作系统范畴,是由若干条指令组成 ,用于完成某个功能的一个过程,并且原语的执行必须是连续的,执行过程中不允许被中断,所以 CAS 是一条 CPU 的原子指令,不会造成数据不一致的问题,是线程安全的 + +底层原理:CAS 的底层是 `lock cmpxchg` 指令(X86 架构),在单核和多核 CPU 下都能够保证比较交换的原子性 + +* 程序是在单核处理器上运行,会省略 lock 前缀,单处理器自身会维护处理器内的顺序一致性,不需要 lock 前缀的内存屏障效果 + +* 程序是在多核处理器上运行,会为 cmpxchg 指令加上 lock 前缀。当某个核执行到带 lock 的指令时,CPU 会执行**总线锁定或缓存锁定**,将修改的变量写入到主存,这个过程不会被线程的调度机制所打断,保证了多个线程对内存操作的原子性 + +作用:比较当前工作内存中的值和主物理内存中的值,如果相同则执行规定操作,否则继续比较直到主内存和工作内存的值一致为止 + +CAS 特点: + +* CAS 体现的是**无锁并发、无阻塞并发**,线程不会陷入阻塞,线程不需要频繁切换状态(上下文切换,系统调用) +* CAS 是基于乐观锁的思想 + +CAS 缺点: + +* 执行的是循环操作,如果比较不成功一直在循环,最差的情况某个线程一直取到的值和预期值都不一样,就会无限循环导致饥饿,**使用 CAS 线程数不要超过 CPU 的核心数**,采用分段 CAS 和自动迁移机制 +* 只能保证一个共享变量的原子操作 + * 对于一个共享变量执行操作时,可以通过循环 CAS 的方式来保证原子操作 + * 对于多个共享变量操作时,循环 CAS 就无法保证操作的原子性,这个时候**只能用锁来保证原子性** +* 引出来 ABA 问题 + +*** + +#### 乐观锁 + +CAS 与 synchronized 总结: + +* synchronized 是从悲观的角度出发:总是假设最坏的情况,每次去拿数据的时候都认为别人会修改,所以每次在拿数据的时候都会上锁,这样别人想拿这个数据就会阻塞(共享资源每次只给一个线程使用,其它线程阻塞,用完后再把资源转让给其它线程),因此 synchronized 也称之为悲观锁,ReentrantLock 也是一种悲观锁,性能较差 +* CAS 是从乐观的角度出发:总是假设最好的情况,每次去拿数据的时候都认为别人不会修改,所以不会上锁,但是在更新的时候会判断一下在此期间别人有没有去更新这个数据。**如果别人修改过,则获取现在最新的值,如果别人没修改过,直接修改共享数据的值**,CAS 这种机制也称之为乐观锁,综合性能较好 + +*** + +### Atomic + +#### 常用API + +常见原子类:AtomicInteger、AtomicBoolean、AtomicLong + +构造方法: + +* `public AtomicInteger()`:初始化一个默认值为 0 的原子型 Integer +* `public AtomicInteger(int initialValue)`:初始化一个指定值的原子型 Integer + +常用API: + +| 方法 | 作用 | +| ------------------------------------- | ------------------------------------------------------------ | +| public final int get() | 获取 AtomicInteger 的值 | +| public final int getAndIncrement() | 以原子方式将当前值加 1,返回的是自增前的值 | +| public final int incrementAndGet() | 以原子方式将当前值加 1,返回的是自增后的值 | +| public final int getAndSet(int value) | 以原子方式设置为 newValue 的值,返回旧值 | +| public final int addAndGet(int data) | 以原子方式将输入的数值与实例中的值相加并返回
实例:AtomicInteger 里的 value | + +*** + +#### 原理分析 + +**AtomicInteger 原理**:自旋锁 + CAS 算法 + +CAS 算法:有 3 个操作数(内存值 V, 旧的预期值 A,要修改的值 B) + +* 当旧的预期值 A == 内存值 V 此时可以修改,将 V 改为 B +* 当旧的预期值 A != 内存值 V 此时不能修改,并重新获取现在的最新值,重新获取的动作就是自旋 + +分析 getAndSet 方法: + +* AtomicInteger: + + ```java + public final int getAndSet(int newValue) { + /** + * this: 当前对象 + * valueOffset: 内存偏移量,内存地址 + */ + return unsafe.getAndSetInt(this, valueOffset, newValue); + } + ``` + + valueOffset:偏移量表示该变量值相对于当前对象地址的偏移,Unsafe 就是根据内存偏移地址获取数据 + + ```java + valueOffset = unsafe.objectFieldOffset + (AtomicInteger.class.getDeclaredField("value")); + //调用本地方法 --> + public native long objectFieldOffset(Field var1); + ``` + +* unsafe 类: + + ```java + // val1: AtomicInteger对象本身,var2: 该对象值得引用地址,var4: 需要变动的数 + public final int getAndSetInt(Object var1, long var2, int var4) { + int var5; + do { + // var5: 用 var1 和 var2 找到的内存中的真实值 + var5 = this.getIntVolatile(var1, var2); + } while(!this.compareAndSwapInt(var1, var2, var5, var4)); + + return var5; + } + ``` + + var5:从主内存中拷贝到工作内存中的值(每次都要从主内存拿到最新的值到本地内存),然后执行 `compareAndSwapInt()` 再和主内存的值进行比较,假设方法返回 false,那么就一直执行 while 方法,直到期望的值和真实值一样,修改数据 + +* 变量 value 用 volatile 修饰,保证了多线程之间的内存可见性,避免线程从工作缓存中获取失效的变量 + + ```java + private volatile int value + ``` + + **CAS 必须借助 volatile 才能读取到共享变量的最新值来实现比较并交换的效果** + +分析 getAndUpdate 方法: + +* getAndUpdate: + + ```java + public final int getAndUpdate(IntUnaryOperator updateFunction) { + int prev, next; + do { + prev = get(); //当前值,cas的期望值 + next = updateFunction.applyAsInt(prev);//期望值更新到该值 + } while (!compareAndSet(prev, next));//自旋 + return prev; + } + ``` + + 函数式接口:可以自定义操作逻辑 + + ```java + AtomicInteger a = new AtomicInteger(); + a.getAndUpdate(i -> i + 10); + ``` + +* compareAndSet: + + ```java + public final boolean compareAndSet(int expect, int update) { + /** + * this: 当前对象 + * valueOffset: 内存偏移量,内存地址 + * expect: 期望的值 + * update: 更新的值 + */ + return unsafe.compareAndSwapInt(this, valueOffset, expect, update); + } + ``` + +*** + +#### 原子引用 + +原子引用:对 Object 进行原子操作,提供一种读和写都是原子性的对象引用变量 + +原子引用类:AtomicReference、AtomicStampedReference、AtomicMarkableReference + +AtomicReference 类: + +* 构造方法:`AtomicReference atomicReference = new AtomicReference()` + +* 常用 API: + * `public final boolean compareAndSet(V expectedValue, V newValue)`:CAS 操作 + * `public final void set(V newValue)`:将值设置为 newValue + * `public final V get()`:返回当前值 + +```java +public class AtomicReferenceDemo { + public static void main(String[] args) { + Student s1 = new Student(33, "z3"); + + // 创建原子引用包装类 + AtomicReference atomicReference = new AtomicReference<>(); + // 设置主内存共享变量为s1 + atomicReference.set(s1); + + // 比较并交换,如果现在主物理内存的值为 z3,那么交换成 l4 + while (true) { + Student s2 = new Student(44, "l4"); + if (atomicReference.compareAndSet(s1, s2)) { + break; + } + } + System.out.println(atomicReference.get()); + } +} + +class Student { + private int id; + private String name; + //。。。。 +} +``` + +*** + +#### 原子数组 + +原子数组类:AtomicIntegerArray、AtomicLongArray、AtomicReferenceArray + +AtomicIntegerArray 类方法: + +```java +/** +* i the index +* expect the expected value +* update the new value +*/ +public final boolean compareAndSet(int i, int expect, int update) { + return compareAndSetRaw(checkedByteOffset(i), expect, update); +} +``` + +*** + +#### 原子更新器 + +原子更新器类:AtomicReferenceFieldUpdater、AtomicIntegerFieldUpdater、AtomicLongFieldUpdater + +利用字段更新器,可以针对对象的某个域(Field)进行原子操作,只能配合 volatile 修饰的字段使用,否则会出现异常 `IllegalArgumentException: Must be volatile type` + +常用 API: + +* `static AtomicIntegerFieldUpdater newUpdater(Class c, String fieldName)`:构造方法 +* `abstract boolean compareAndSet(T obj, int expect, int update)`:CAS + +```java +public class UpdateDemo { + private volatile int field; + + public static void main(String[] args) { + AtomicIntegerFieldUpdater fieldUpdater = AtomicIntegerFieldUpdater + .newUpdater(UpdateDemo.class, "field"); + UpdateDemo updateDemo = new UpdateDemo(); + fieldUpdater.compareAndSet(updateDemo, 0, 10); + System.out.println(updateDemo.field);//10 + } +} +``` + +*** + +#### 原子累加器 + +原子累加器类:LongAdder、DoubleAdder、LongAccumulator、DoubleAccumulator + +LongAdder 和 LongAccumulator 区别: + +相同点: + +* LongAddr 与 LongAccumulator 类都是使用非阻塞算法 CAS 实现的 +* LongAddr 类是 LongAccumulator 类的一个特例,只是 LongAccumulator 提供了更强大的功能,可以自定义累加规则,当accumulatorFunction 为 null 时就等价于 LongAddr + +不同点: + +* 调用 casBase 时,LongAccumulator 使用 function.applyAsLong(b = base, x) 来计算,LongAddr 使用 casBase(b = base, b + x) +* LongAccumulator 类功能更加强大,构造方法参数中 + + * accumulatorFunction 是一个双目运算器接口,可以指定累加规则,比如累加或者相乘,其根据输入的两个参数返回一个计算值,LongAdder 内置累加规则 + * identity 则是 LongAccumulator 累加器的初始值,LongAccumulator 可以为累加器提供非0的初始值,而 LongAdder 只能提供默认的 0 + +*** + +### Adder + +#### 优化机制 + +LongAdder 是 Java8 提供的类,跟 AtomicLong 有相同的效果,但对 CAS 机制进行了优化,尝试使用分段 CAS 以及自动分段迁移的方式来大幅度提升多线程高并发执行 CAS 操作的性能 + +CAS 底层实现是在一个循环中不断地尝试修改目标值,直到修改成功。如果竞争不激烈修改成功率很高,否则失败率很高,失败后这些重复的原子性操作会耗费性能(导致大量线程**空循环,自旋转**) + +优化核心思想:数据分离,将 AtomicLong 的**单点的更新压力分担到各个节点,空间换时间**,在低并发的时候直接更新,可以保障和 AtomicLong 的性能基本一致,而在高并发的时候通过分散减少竞争,提高了性能 + +**分段 CAS 机制**: + +* 在发生竞争时,创建 Cell 数组用于将不同线程的操作离散(通过 hash 等算法映射)到不同的节点上 +* 设置多个累加单元(会根据需要扩容,最大为 CPU 核数),Therad-0 累加 Cell[0],而 Thread-1 累加 Cell[1] 等,最后将结果汇总 +* 在累加时操作的不同的 Cell 变量,因此减少了 CAS 重试失败,从而提高性能 + +**自动分段迁移机制**:某个 Cell 的 value 执行 CAS 失败,就会自动寻找另一个 Cell 分段内的 value 值进行 CAS 操作 + +*** + +#### 伪共享 + +Cell 为累加单元:数组访问索引是通过 Thread 里的 threadLocalRandomProbe 域取模实现的,这个域是 ThreadLocalRandom 更新的 + +```java +// Striped64.Cell +@sun.misc.Contended static final class Cell { + volatile long value; + Cell(long x) { value = x; } + // 用 cas 方式进行累加, prev 表示旧值, next 表示新值 + final boolean cas(long prev, long next) { + return UNSAFE.compareAndSwapLong(this, valueOffset, prev, next); + } + // 省略不重要代码 +} +``` + +Cell 是数组形式,**在内存中是连续存储的**,64 位系统中,一个 Cell 为 24 字节(16 字节的对象头和 8 字节的 value),每一个 cache line 为 64 字节,因此缓存行可以存下 2 个的 Cell 对象,当 Core-0 要修改 Cell[0]、Core-1 要修改 Cell[1],无论谁修改成功都会导致当前缓存行失效,从而导致对方的数据失效,需要重新去主存获取,影响效率 + +![](https://seazean.oss-cn-beijing.aliyuncs.com/img/Java/JUC-伪共享1.png) + +@sun.misc.Contended:防止缓存行伪共享,在使用此注解的对象或字段的前后各增加 128 字节大小的 padding,使用 2 倍于大多数硬件缓存行让 CPU 将对象预读至缓存时**占用不同的缓存行**,这样就不会造成对方缓存行的失效 + +![](https://seazean.oss-cn-beijing.aliyuncs.com/img/Java/JUC-伪共享2.png) + +*** + +#### 源码解析 + +Striped64 类成员属性: + +```java +// 表示当前计算机CPU数量 +static final int NCPU = Runtime.getRuntime().availableProcessors() +// 累加单元数组, 懒惰初始化 +transient volatile Cell[] cells; +// 基础值, 如果没有竞争, 则用 cas 累加这个域,当 cells 扩容时,也会将数据写到 base 中 +transient volatile long base; +// 在 cells 初始化或扩容时只能有一个线程执行, 通过 CAS 更新 cellsBusy 置为 1 来实现一个锁 +transient volatile int cellsBusy; +``` + +工作流程: + +* cells 占用内存是相对比较大的,是惰性加载的,在无竞争或者其他线程正在初始化 cells 数组的情况下,直接更新 base 域 + +* 在第一次发生竞争时(casBase 失败)会创建一个大小为 2 的 cells 数组,将当前累加的值包装为 Cell 对象,放入映射的槽位上 +* 分段累加的过程中,如果当前线程对应的 cells 槽位为空,就会新建 Cell 填充,如果出现竞争,就会重新计算线程对应的槽位,继续自旋尝试修改 +* 分段迁移后还出现竞争就会扩容 cells 数组长度为原来的两倍,然后 rehash,**数组长度总是 2 的 n 次幂**,默认最大为 CPU 核数,但是可以超过,如果核数是 6 核,数组最长是 8 + +方法分析: + +* LongAdder#add:累加方法 + + ```java + public void add(long x) { + // as 为累加单元数组的引用,b 为基础值,v 表示期望值 + // m 表示 cells 数组的长度 - 1,a 表示当前线程命中的 cell 单元格 + Cell[] as; long b, v; int m; Cell a; + + // cells 不为空说明 cells 已经被初始化,线程发生了竞争,去更新对应的 cell 槽位 + // 进入 || 后的逻辑去更新 base 域,更新失败表示发生竞争进入条件 + if ((as = cells) != null || !casBase(b = base, b + x)) { + // uncontended 为 true 表示 cell 没有竞争 + boolean uncontended = true; + + // 条件一: true 说明 cells 未初始化,多线程写 base 发生竞争需要进行初始化 cells 数组 + // fasle 说明 cells 已经初始化,进行下一个条件寻找自己的 cell 去累加 + // 条件二: getProbe() 获取 hash 值,& m 的逻辑和 HashMap 的逻辑相同,保证散列的均匀性 + // true 说明当前线程对应下标的 cell 为空,需要创建 cell + // false 说明当前线程对应的 cell 不为空,进行下一个条件【将 x 值累加到对应的 cell 中】 + // 条件三: 有取反符号,false 说明 cas 成功,直接返回,true 说明失败,当前线程对应的 cell 有竞争 + if (as == null || (m = as.length - 1) < 0 || + (a = as[getProbe() & m]) == null || + !(uncontended = a.cas(v = a.value, v + x))) + longAccumulate(x, null, uncontended); + // 【uncontended 在对应的 cell 上累加失败的时候才为 false,其余情况均为 true】 + } + } + ``` + +* Striped64#longAccumulate:cell 数组创建 + + ```java + // x null false | true + final void longAccumulate(long x, LongBinaryOperator fn, boolean wasUncontended) { + int h; + // 当前线程还没有对应的 cell, 需要随机生成一个 hash 值用来将当前线程绑定到 cell + if ((h = getProbe()) == 0) { + // 初始化 probe,获取 hash 值 + ThreadLocalRandom.current(); + h = getProbe(); + // 默认情况下 当前线程肯定是写入到了 cells[0] 位置,不把它当做一次真正的竞争 + wasUncontended = true; + } + // 表示【扩容意向】,false 一定不会扩容,true 可能会扩容 + boolean collide = false; + //自旋 + for (;;) { + // as 表示cells引用,a 表示当前线程命中的 cell,n 表示 cells 数组长度,v 表示 期望值 + Cell[] as; Cell a; int n; long v; + // 【CASE1】: 表示 cells 已经初始化了,当前线程应该将数据写入到对应的 cell 中 + if ((as = cells) != null && (n = as.length) > 0) { + // CASE1.1: true 表示当前线程对应的索引下标的 Cell 为 null,需要创建 new Cell + if ((a = as[(n - 1) & h]) == null) { + // 判断 cellsBusy 是否被锁 + if (cellsBusy == 0) { + // 创建 cell, 初始累加值为 x + Cell r = new Cell(x); + // 加锁 + if (cellsBusy == 0 && casCellsBusy()) { + // 创建成功标记,进入【创建 cell 逻辑】 + boolean created = false; + try { + Cell[] rs; int m, j; + // 把当前 cells 数组赋值给 rs,并且不为 null + if ((rs = cells) != null && + (m = rs.length) > 0 && + // 再次判断防止其它线程初始化过该位置,当前线程再次初始化该位置会造成数据丢失 + // 因为这里是线程安全的判断,进行的逻辑不会被其他线程影响 + rs[j = (m - 1) & h] == null) { + // 把新创建的 cell 填充至当前位置 + rs[j] = r; + created = true; // 表示创建完成 + } + } finally { + cellsBusy = 0; // 解锁 + } + if (created) // true 表示创建完成,可以推出循环了 + break; + continue; + } + } + collide = false; + } + // CASE1.2: 条件成立说明线程对应的 cell 有竞争, 改变线程对应的 cell 来重试 cas + else if (!wasUncontended) + wasUncontended = true; + // CASE 1.3: 当前线程 rehash 过,如果新命中的 cell 不为空,就尝试累加,false 说明新命中也有竞争 + else if (a.cas(v = a.value, ((fn == null) ? v + x : fn.applyAsLong(v, x)))) + break; + // CASE 1.4: cells 长度已经超过了最大长度 CPU 内核的数量或者已经扩容 + else if (n >= NCPU || cells != as) + collide = false; // 扩容意向改为false,【表示不能扩容了】 + // CASE 1.5: 更改扩容意向,如果 n >= NCPU,这里就永远不会执行到,case1.4 永远先于 1.5 执行 + else if (!collide) + collide = true; + // CASE 1.6: 【扩容逻辑】,进行加锁 + else if (cellsBusy == 0 && casCellsBusy()) { + try { + // 线程安全的检查,防止期间被其他线程扩容了 + if (cells == as) { + // 扩容为以前的 2 倍 + Cell[] rs = new Cell[n << 1]; + // 遍历移动值 + for (int i = 0; i < n; ++i) + rs[i] = as[i]; + // 把扩容后的引用给 cells + cells = rs; + } + } finally { + cellsBusy = 0; // 解锁 + } + collide = false; // 扩容意向改为 false,表示不扩容了 + continue; + } + // 重置当前线程 Hash 值,这就是【分段迁移机制】 + h = advanceProbe(h); + } + + // 【CASE2】: 运行到这说明 cells 还未初始化,as 为null + // 判断是否没有加锁,没有加锁就用 CAS 加锁 + // 条件二判断是否其它线程在当前线程给 as 赋值之后修改了 cells,这里不是线程安全的判断 + else if (cellsBusy == 0 && cells == as && casCellsBusy()) { + // 初始化标志,开始 【初始化 cells 数组】 + boolean init = false; + try { + // 再次判断 cells == as 防止其它线程已经提前初始化了,当前线程再次初始化导致丢失数据 + // 因为这里是【线程安全的,重新检查,经典 DCL】 + if (cells == as) { + Cell[] rs = new Cell[2]; // 初始化数组大小为2 + rs[h & 1] = new Cell(x); // 填充线程对应的cell + cells = rs; + init = true; // 初始化成功,标记置为 true + } + } finally { + cellsBusy = 0; // 解锁啊 + } + if (init) + break; // 初始化成功直接跳出自旋 + } + // 【CASE3】: 运行到这说明其他线程在初始化 cells,当前线程将值累加到 base,累加成功直接结束自旋 + else if (casBase(v = base, ((fn == null) ? v + x : + fn.applyAsLong(v, x)))) + break; + } + } + ``` + +* sum:获取最终结果通过 sum 整合,**保证最终一致性,不保证强一致性** + + ```java + public long sum() { + Cell[] as = cells; Cell a; + long sum = base; + if (as != null) { + // 遍历 累加 + for (int i = 0; i < as.length; ++i) { + if ((a = as[i]) != null) + sum += a.value; + } + } + return sum; + } + ``` + +*** + +### ABA + +ABA 问题:当进行获取主内存值时,该内存值在写入主内存时已经被修改了 N 次,但是最终又改成原来的值 + +其他线程先把 A 改成 B 又改回 A,主线程**仅能判断出共享变量的值与最初值 A 是否相同**,不能感知到这种从 A 改为 B 又 改回 A 的情况,这时 CAS 虽然成功,但是过程存在问题 + +* 构造方法: + * `public AtomicStampedReference(V initialRef, int initialStamp)`:初始值和初始版本号 + +* 常用API: + * `public boolean compareAndSet(V expectedReference, V newReference, int expectedStamp, int newStamp)`:**期望引用和期望版本号都一致**才进行 CAS 修改数据 + * `public void set(V newReference, int newStamp)`:设置值和版本号 + * `public V getReference()`:返回引用的值 + * `public int getStamp()`:返回当前版本号 + +```java +public static void main(String[] args) { + AtomicStampedReference atomicReference = new AtomicStampedReference<>(100,1); + int startStamp = atomicReference.getStamp(); + new Thread(() ->{ + int stamp = atomicReference.getStamp(); + atomicReference.compareAndSet(100, 101, stamp, stamp + 1); + stamp = atomicReference.getStamp(); + atomicReference.compareAndSet(101, 100, stamp, stamp + 1); + },"t1").start(); + + new Thread(() ->{ + try { + Thread.sleep(1000); + } catch (InterruptedException e) { + e.printStackTrace(); + } + if (!atomicReference.compareAndSet(100, 200, startStamp, startStamp + 1)) { + System.out.println(atomicReference.getReference());//100 + System.out.println(Thread.currentThread().getName() + "线程修改失败"); + } + },"t2").start(); +} +``` + +*** + +### Unsafe + +Unsafe 是 CAS 的核心类,由于 Java 无法直接访问底层系统,需要通过本地(Native)方法来访问 + +Unsafe 类存在 sun.misc 包,其中所有方法都是 native 修饰的,都是直接调用**操作系统底层资源**执行相应的任务,基于该类可以直接操作特定的内存数据,其内部方法操作类似 C 的指针 + +模拟实现原子整数: + +```java +public static void main(String[] args) { + MyAtomicInteger atomicInteger = new MyAtomicInteger(10); + if (atomicInteger.compareAndSwap(20)) { + System.out.println(atomicInteger.getValue()); + } +} + +class MyAtomicInteger { + private static final Unsafe UNSAFE; + private static final long VALUE_OFFSET; + private volatile int value; + + static { + try { + //Unsafe unsafe = Unsafe.getUnsafe()这样会报错,需要反射获取 + Field theUnsafe = Unsafe.class.getDeclaredField("theUnsafe"); + theUnsafe.setAccessible(true); + UNSAFE = (Unsafe) theUnsafe.get(null); + // 获取 value 属性的内存地址,value 属性指向该地址,直接设置该地址的值可以修改 value 的值 + VALUE_OFFSET = UNSAFE.objectFieldOffset( + MyAtomicInteger.class.getDeclaredField("value")); + } catch (NoSuchFieldException | IllegalAccessException e) { + e.printStackTrace(); + throw new RuntimeException(); + } + } + + public MyAtomicInteger(int value) { + this.value = value; + } + public int getValue() { + return value; + } + + public boolean compareAndSwap(int update) { + while (true) { + int prev = this.value; + int next = update; + // 当前对象 内存偏移量 期望值 更新值 + if (UNSAFE.compareAndSwapInt(this, VALUE_OFFSET, prev, update)) { + System.out.println("CAS成功"); + return true; + } + } + } +} +``` + +*** + +### final + +#### 原理 + +```java +public class TestFinal { + final int a = 20; +} +``` + +字节码: + +```java +0: aload_0 +1: invokespecial #1 // Method java/lang/Object."":()V +4: aload_0 +5: bipush 20 // 将值直接放入栈中 +7: putfield #2 // Field a:I +<-- 写屏障 +10: return +``` + +final 变量的赋值通过 putfield 指令来完成,在这条指令之后也会加入写屏障,保证在其它线程读到它的值时不会出现为 0 的情况 + +其他线程访问 final 修饰的变量 + +* **复制一份放入栈中**直接访问,效率高 +* 大于 short 最大值会将其复制到类的常量池,访问时从常量池获取 + +*** + +#### 不可变 + +不可变:如果一个对象不能够修改其内部状态(属性),那么就是不可变对象 + +不可变对象线程安全的,不存在并发修改和可见性问题,是另一种避免竞争的方式 + +String 类也是不可变的,该类和类中所有属性都是 final 的 + +* 类用 final 修饰保证了该类中的方法不能被覆盖,防止子类无意间破坏不可变性 + +* 无写入方法(set)确保外部不能对内部属性进行修改 + +* 属性用 final 修饰保证了该属性是只读的,不能修改 + + ```java + public final class String + implements java.io.Serializable, Comparable, CharSequence { + /** The value is used for character storage. */ + private final char value[]; + //.... + } + ``` + +* 更改 String 类数据时,会构造新字符串对象,生成新的 char[] value,通过**创建副本对象来避免共享的方式称之为保护性拷贝** + +*** + +### State + +无状态:成员变量保存的数据也可以称为状态信息,无状态就是没有成员变量 + +Servlet 为了保证其线程安全,一般不为 Servlet 设置成员变量,这种没有任何成员变量的类是线程安全的 + +*** + +### Local + +#### 基本介绍 + +ThreadLocal 类用来提供线程内部的局部变量,这种变量在多线程环境下访问(通过 get 和 set 方法访问)时能保证各个线程的变量相对独立于其他线程内的变量,分配在堆内的 **TLAB** 中 + +ThreadLocal 实例通常来说都是 `private static` 类型的,属于一个线程的本地变量,用于关联线程和线程上下文。每个线程都会在 ThreadLocal 中保存一份该线程独有的数据,所以是线程安全的 + +ThreadLocal 作用: + +* 线程并发:应用在多线程并发的场景下 + +* 传递数据:通过 ThreadLocal 实现在同一线程不同函数或组件中传递公共变量,减少传递复杂度 + +* 线程隔离:每个线程的变量都是独立的,不会互相影响 + +对比 synchronized: + +| | synchronized | ThreadLocal | +| ------ | ------------------------------------------------------------ | ------------------------------------------------------------ | +| 原理 | 同步机制采用**以时间换空间**的方式,只提供了一份变量,让不同的线程排队访问 | ThreadLocal 采用**以空间换时间**的方式,为每个线程都提供了一份变量的副本,从而实现同时访问而相不干扰 | +| 侧重点 | 多个线程之间访问资源的同步 | 多线程中让每个线程之间的数据相互隔离 | + +*** + +#### 基本使用 + +##### 常用方法 + +| 方法 | 描述 | +| -------------------------- | ---------------------------- | +| ThreadLocal<>() | 创建 ThreadLocal 对象 | +| protected T initialValue() | 返回当前线程局部变量的初始值 | +| public void set( T value) | 设置当前线程绑定的局部变量 | +| public T get() | 获取当前线程绑定的局部变量 | +| public void remove() | 移除当前线程绑定的局部变量 | + +```java +public class MyDemo { + + private static ThreadLocal tl = new ThreadLocal<>(); + + private String content; + + private String getContent() { + // 获取当前线程绑定的变量 + return tl.get(); + } + + private void setContent(String content) { + // 变量content绑定到当前线程 + tl.set(content); + } + + public static void main(String[] args) { + MyDemo demo = new MyDemo(); + for (int i = 0; i < 5; i++) { + Thread thread = new Thread(new Runnable() { + @Override + public void run() { + // 设置数据 + demo.setContent(Thread.currentThread().getName() + "的数据"); + System.out.println("-----------------------"); + System.out.println(Thread.currentThread().getName() + "--->" + demo.getContent()); + } + }); + thread.setName("线程" + i); + thread.start(); + } + } +} +``` + +*** + +##### 应用场景 + +ThreadLocal 适用于下面两种场景: + +* 每个线程需要有自己单独的实例 +* 实例需要在多个方法中共享,但不希望被多线程共享 + +ThreadLocal 方案有两个突出的优势: + +1. 传递数据:保存每个线程绑定的数据,在需要的地方可以直接获取,避免参数直接传递带来的代码耦合问题 +2. 线程隔离:各线程之间的数据相互隔离却又具备并发性,避免同步方式带来的性能损失 + +ThreadLocal 用于数据连接的事务管理: + +```java +public class JdbcUtils { + // ThreadLocal对象,将connection绑定在当前线程中 + private static final ThreadLocal tl = new ThreadLocal(); + // c3p0 数据库连接池对象属性 + private static final ComboPooledDataSource ds = new ComboPooledDataSource(); + // 获取连接 + public static Connection getConnection() throws SQLException { + //取出当前线程绑定的connection对象 + Connection conn = tl.get(); + if (conn == null) { + //如果没有,则从连接池中取出 + conn = ds.getConnection(); + //再将connection对象绑定到当前线程中,非常重要的操作 + tl.set(conn); + } + return conn; + } + // ... +} +``` + +用 ThreadLocal 使 SimpleDateFormat 从独享变量变成单个线程变量: + +```java +public class ThreadLocalDateUtil { + private static ThreadLocal threadLocal = new ThreadLocal() { + @Override + protected DateFormat initialValue() { + return new SimpleDateFormat("yyyy-MM-dd HH:mm:ss"); + } + }; + + public static Date parse(String dateStr) throws ParseException { + return threadLocal.get().parse(dateStr); + } + + public static String format(Date date) { + return threadLocal.get().format(date); + } +} +``` + +**** + +#### 实现原理 + +##### 底层结构 + +JDK8 以前:每个 ThreadLocal 都创建一个 Map,然后用线程作为 Map 的 key,要存储的局部变量作为 Map 的 value,达到各个线程的局部变量隔离的效果。这种结构会造成 Map 结构过大和内存泄露,因为 Thread 停止后无法通过 key 删除对应的数据 + +![](https://seazean.oss-cn-beijing.aliyuncs.com/img/Java/JUC-ThreadLocal数据结构JDK8前.png) + +JDK8 以后:每个 Thread 维护一个 ThreadLocalMap,这个 Map 的 key 是 ThreadLocal 实例本身,value 是真正要存储的值 + +* **每个 Thread 线程内部都有一个 Map (ThreadLocalMap)** +* Map 里面存储 ThreadLocal 对象(key)和线程的私有变量(value) +* Thread 内部的 Map 是由 ThreadLocal 维护的,由 ThreadLocal 负责向 map 获取和设置线程的变量值 +* 对于不同的线程,每次获取副本值时,别的线程并不能获取到当前线程的副本值,形成副本的隔离,互不干扰 + +![](https://seazean.oss-cn-beijing.aliyuncs.com/img/Java/JUC-ThreadLocal数据结构JDK8后.png) + +JDK8 前后对比: + +* 每个 Map 存储的 Entry 数量会变少,因为之前的存储数量由 Thread 的数量决定,现在由 ThreadLocal 的数量决定,在实际编程当中,往往 ThreadLocal 的数量要少于 Thread 的数量 +* 当 Thread 销毁之后,对应的 ThreadLocalMap 也会随之销毁,能减少内存的使用,**防止内存泄露** + +*** + +##### 成员变量 + +* Thread 类的相关属性:**每一个线程持有一个 ThreadLocalMap 对象**,存放由 ThreadLocal 和数据组成的 Entry 键值对 + + ```java + ThreadLocal.ThreadLocalMap threadLocals = null + ``` + +* 计算 ThreadLocal 对象的哈希值: + + ```java + private final int threadLocalHashCode = nextHashCode() + ``` + + 使用 `threadLocalHashCode & (table.length - 1)` 计算当前 entry 需要存放的位置 + +* 每创建一个 ThreadLocal 对象就会使用 nextHashCode 分配一个 hash 值给这个对象: + + ```java + private static AtomicInteger nextHashCode = new AtomicInteger() + ``` + +* 斐波那契数也叫黄金分割数,hash 的**增量**就是这个数字,带来的好处是 hash 分布非常均匀: + + ```java + private static final int HASH_INCREMENT = 0x61c88647 + ``` + +*** + +##### 成员方法 + +方法都是线程安全的,因为 ThreadLocal 属于一个线程的,ThreadLocal 中的方法,逻辑都是获取当前线程维护的 ThreadLocalMap 对象,然后进行数据的增删改查,没有指定初始值的 threadlcoal 对象默认赋值为 null + +* initialValue():返回该线程局部变量的初始值 + + * 延迟调用的方法,在执行 get 方法时才执行 + * 该方法缺省(默认)实现直接返回一个 null + * 如果想要一个初始值,可以重写此方法, 该方法是一个 `protected` 的方法,为了让子类覆盖而设计的 + + ```java + protected T initialValue() { + return null; + } + ``` + +* nextHashCode():计算哈希值,ThreadLocal 的散列方式称之为**斐波那契散列**,每次获取哈希值都会加上 HASH_INCREMENT,这样做可以尽量避免 hash 冲突,让哈希值能均匀的分布在 2 的 n 次方的数组中 + + ```java + private static int nextHashCode() { + // 哈希值自增一个 HASH_INCREMENT 数值 + return nextHashCode.getAndAdd(HASH_INCREMENT); + } + ``` + +* set():修改当前线程与当前 threadlocal 对象相关联的线程局部变量 + + ```java + public void set(T value) { + // 获取当前线程对象 + Thread t = Thread.currentThread(); + // 获取此线程对象中维护的 ThreadLocalMap 对象 + ThreadLocalMap map = getMap(t); + // 判断 map 是否存在 + if (map != null) + // 调用 threadLocalMap.set 方法进行重写或者添加 + map.set(this, value); + else + // map 为空,调用 createMap 进行 ThreadLocalMap 对象的初始化。参数1是当前线程,参数2是局部变量 + createMap(t, value); + } + ``` + + ```java + // 获取当前线程 Thread 对应维护的 ThreadLocalMap + ThreadLocalMap getMap(Thread t) { + return t.threadLocals; + } + // 创建当前线程Thread对应维护的ThreadLocalMap + void createMap(Thread t, T firstValue) { + // 【这里的 this 是调用此方法的 threadLocal】,创建一个新的 Map 并设置第一个数据 + t.threadLocals = new ThreadLocalMap(this, firstValue); + } + ``` + +* get():获取当前线程与当前 ThreadLocal 对象相关联的线程局部变量 + + ```java + public T get() { + Thread t = Thread.currentThread(); + ThreadLocalMap map = getMap(t); + // 如果此map存在 + if (map != null) { + // 以当前的 ThreadLocal 为 key,调用 getEntry 获取对应的存储实体 e + ThreadLocalMap.Entry e = map.getEntry(this); + // 对 e 进行判空 + if (e != null) { + // 获取存储实体 e 对应的 value值 + T result = (T)e.value; + return result; + } + } + /*有两种情况有执行当前代码 + 第一种情况: map 不存在,表示此线程没有维护的 ThreadLocalMap 对象 + 第二种情况: map 存在, 但是【没有与当前 ThreadLocal 关联的 entry】,就会设置为默认值 */ + // 初始化当前线程与当前 threadLocal 对象相关联的 value + return setInitialValue(); + } + ``` + + ```java + private T setInitialValue() { + // 调用initialValue获取初始化的值,此方法可以被子类重写, 如果不重写默认返回 null + T value = initialValue(); + Thread t = Thread.currentThread(); + ThreadLocalMap map = getMap(t); + // 判断 map 是否初始化过 + if (map != null) + // 存在则调用 map.set 设置此实体 entry,value 是默认的值 + map.set(this, value); + else + // 调用 createMap 进行 ThreadLocalMap 对象的初始化中 + createMap(t, value); + // 返回线程与当前 threadLocal 关联的局部变量 + return value; + } + ``` + +* remove():移除当前线程与当前 threadLocal 对象相关联的线程局部变量 + + ```java + public void remove() { + // 获取当前线程对象中维护的 ThreadLocalMap 对象 + ThreadLocalMap m = getMap(Thread.currentThread()); + if (m != null) + // map 存在则调用 map.remove,this时当前ThreadLocal,以this为key删除对应的实体 + m.remove(this); + } + ``` + +*** + +#### LocalMap + +##### 成员属性 + +ThreadLocalMap 是 ThreadLocal 的内部类,没有实现 Map 接口,用独立的方式实现了 Map 的功能,其内部 Entry 也是独立实现 + +```java +// 初始化当前 map 内部散列表数组的初始长度 16 +private static final int INITIAL_CAPACITY = 16; + +// 存放数据的table,数组长度必须是2的整次幂。 +private Entry[] table; + +// 数组里面 entrys 的个数,可以用于判断 table 当前使用量是否超过阈值 +private int size = 0; + +// 进行扩容的阈值,表使用量大于它的时候进行扩容。 +private int threshold; +``` + +存储结构 Entry: + +* Entry 继承 WeakReference,key 是弱引用,目的是将 ThreadLocal 对象的生命周期和线程生命周期解绑 +* Entry 限制只能用 ThreadLocal 作为 key,key 为 null (entry.get() == null) 意味着 key 不再被引用,entry 也可以从 table 中清除 + +```java +static class Entry extends WeakReference> { + Object value; + Entry(ThreadLocal k, Object v) { + // this.referent = referent = key; + super(k); + value = v; + } +} +``` + +构造方法:延迟初始化的,线程第一次存储 threadLocal - value 时才会创建 threadLocalMap 对象 + +```java +ThreadLocalMap(ThreadLocal firstKey, Object firstValue) { + // 初始化table,创建一个长度为16的Entry数组 + table = new Entry[INITIAL_CAPACITY]; + // 【寻址算法】计算索引 + int i = firstKey.threadLocalHashCode & (INITIAL_CAPACITY - 1); + // 创建 entry 对象,存放到指定位置的 slot 中 + table[i] = new Entry(firstKey, firstValue); + // 数据总量是 1 + size = 1; + // 将阈值设置为 (当前数组长度 * 2)/ 3。 + setThreshold(INITIAL_CAPACITY); +} +``` + +*** + +##### 成员方法 + +* set():添加数据,ThreadLocalMap 使用**线性探测法来解决哈希冲突** + + * 该方法会一直探测下一个地址,直到有空的地址后插入,若插入后 Map 数量超过阈值,数组会扩容为原来的 2 倍 + + 假设当前 table 长度为16,计算出来 key 的 hash 值为 14,如果 table[14] 上已经有值,并且其 key 与当前 key 不一致,那么就发生了 hash 冲突,这个时候将 14 加 1 得到 15,取 table[15] 进行判断,如果还是冲突会回到 0,取 table[0],以此类推,直到可以插入,可以把 Entry[] table 看成一个**环形数组** + + * 线性探测法会出现**堆积问题**,可以采取平方探测法解决 + + * 在探测过程中 ThreadLocal 会复用 key 为 null 的脏 Entry 对象,并进行垃圾清理,防止出现内存泄漏 + + ```java + private void set(ThreadLocal key, Object value) { + // 获取散列表 + ThreadLocal.ThreadLocalMap.Entry[] tab = table; + int len = tab.length; + // 哈希寻址 + int i = key.threadLocalHashCode & (len-1); + // 使用线性探测法向后查找元素,碰到 entry 为空时停止探测 + for (ThreadLocal.ThreadLocalMap.Entry e = tab[i]; e != null; e = tab[i = nextIndex(i, len)]) { + // 获取当前元素 key + ThreadLocal k = e.get(); + // ThreadLocal 对应的 key 存在,【直接覆盖之前的值】 + if (k == key) { + e.value = value; + return; + } + // 【这两个条件谁先成立不一定,所以 replaceStaleEntry 中还需要判断 k == key 的情况】 + + // key 为 null,但是值不为 null,说明之前的 ThreadLocal 对象已经被回收了,当前是【过期数据】 + if (k == null) { + // 【碰到一个过期的 slot,当前数据复用该槽位,替换过期数据】 + // 这个方法还进行了垃圾清理动作,防止内存泄漏 + replaceStaleEntry(key, value, i); + return; + } + } + // 逻辑到这说明碰到 slot == null 的位置,则在空元素的位置创建一个新的 Entry + tab[i] = new Entry(key, value); + // 数量 + 1 + int sz = ++size; + + // 【做一次启发式清理】,如果没有清除任何 entry 并且【当前使用量达到了负载因子所定义,那么进行 rehash + if (!cleanSomeSlots(i, sz) && sz >= threshold) + // 扩容 + rehash(); + } + ``` + + ```java + // 获取【环形数组】的下一个索引 + private static int nextIndex(int i, int len) { + // 索引越界后从 0 开始继续获取 + return ((i + 1 < len) ? i + 1 : 0); + } + ``` + + ```java + // 在指定位置插入指定的数据 + private void replaceStaleEntry(ThreadLocal key, Object value, int staleSlot) { + // 获取散列表 + Entry[] tab = table; + int len = tab.length; + Entry e; + // 探测式清理的开始下标,默认从当前 staleSlot 开始 + int slotToExpunge = staleSlot; + // 以当前 staleSlot 开始【向前迭代查找】,找到索引靠前过期数据,找到以后替换 slotToExpunge 值 + // 【保证在一个区间段内,从最前面的过期数据开始清理】 + for (int i = prevIndex(staleSlot, len); (e = tab[i]) != null; i = prevIndex(i, len)) + if (e.get() == null) + slotToExpunge = i; + + // 以 staleSlot 【向后去查找】,直到碰到 null 为止,还是线性探测 + for (int i = nextIndex(staleSlot, len); (e = tab[i]) != null; i = nextIndex(i, len)) { + // 获取当前节点的 key + ThreadLocal k = e.get(); + // 条件成立说明是【替换逻辑】 + if (k == key) { + e.value = value; + // 因为本来要在 staleSlot 索引处插入该数据,现在找到了i索引处的key与数据一致 + // 但是 i 位置距离正确的位置更远,因为是向后查找,所以还是要在 staleSlot 位置插入当前 entry + // 然后将 table[staleSlot] 这个过期数据放到当前循环到的 table[i] 这个位置, + tab[i] = tab[staleSlot]; + tab[staleSlot] = e; + + // 条件成立说明向前查找过期数据并未找到过期的 entry,但 staleSlot 位置已经不是过期数据了,i 位置才是 + if (slotToExpunge == staleSlot) + slotToExpunge = i; + + // 【清理过期数据,expungeStaleEntry 探测式清理,cleanSomeSlots 启发式清理】 + cleanSomeSlots(expungeStaleEntry(slotToExpunge), len); + return; + } + // 条件成立说明当前遍历的 entry 是一个过期数据,并且该位置前面也没有过期数据 + if (k == null && slotToExpunge == staleSlot) + // 探测式清理过期数据的开始下标修改为当前循环的 index,因为 staleSlot 会放入要添加的数据 + slotToExpunge = i; + } + // 向后查找过程中并未发现 k == key 的 entry,说明当前是一个【取代过期数据逻辑】 + // 删除原有的数据引用,防止内存泄露 + tab[staleSlot].value = null; + // staleSlot 位置添加数据,【上面的所有逻辑都不会更改 staleSlot 的值】 + tab[staleSlot] = new Entry(key, value); + + // 条件成立说明除了 staleSlot 以外,还发现其它的过期 slot,所以要【开启清理数据的逻辑】 + if (slotToExpunge != staleSlot) + cleanSomeSlots(expungeStaleEntry(slotToExpunge), len); + } + ``` + + ![](https://seazean.oss-cn-beijing.aliyuncs.com/img/Java/JUC-replaceStaleEntry流程.png) + + ```java + private static int prevIndex(int i, int len) { + // 形成一个环绕式的访问,头索引越界后置为尾索引 + return ((i - 1 >= 0) ? i - 1 : len - 1); + } + ``` + +* getEntry():ThreadLocal 的 get 方法以当前的 ThreadLocal 为 key,调用 getEntry 获取对应的存储实体 e + + ```java + private Entry getEntry(ThreadLocal key) { + // 哈希寻址 + int i = key.threadLocalHashCode & (table.length - 1); + // 访问散列表中指定指定位置的 slot + Entry e = table[i]; + // 条件成立,说明 slot 有值并且 key 就是要寻找的 key,直接返回 + if (e != null && e.get() == key) + return e; + else + // 进行线性探测 + return getEntryAfterMiss(key, i, e); + } + // 线性探测寻址 + private Entry getEntryAfterMiss(ThreadLocal key, int i, Entry e) { + // 获取散列表 + Entry[] tab = table; + int len = tab.length; + + // 开始遍历,碰到 slot == null 的情况,搜索结束 + while (e != null) { + // 获取当前 slot 中 entry 对象的 key + ThreadLocal k = e.get(); + // 条件成立说明找到了,直接返回 + if (k == key) + return e; + if (k == null) + // 过期数据,【探测式过期数据回收】 + expungeStaleEntry(i); + else + // 更新 index 继续向后走 + i = nextIndex(i, len); + // 获取下一个槽位中的 entry + e = tab[i]; + } + // 说明当前区段没有找到相应数据 + // 【因为存放数据是线性的向后寻找槽位,都是紧挨着的,不可能越过一个 空槽位 在后面放】,可以减少遍历的次数 + return null; + } + ``` + +* rehash():触发一次全量清理,如果数组长度大于等于长度的 `2/3 * 3/4 = 1/2`,则进行 resize + + ```java + private void rehash() { + // 清楚当前散列表内的【所有】过期的数据 + expungeStaleEntries(); + + // threshold = len * 2 / 3,就是 2/3 * (1 - 1/4) + if (size >= threshold - threshold / 4) + resize(); + } + ``` + + ```java + private void expungeStaleEntries() { + Entry[] tab = table; + int len = tab.length; + // 【遍历所有的槽位,清理过期数据】 + for (int j = 0; j < len; j++) { + Entry e = tab[j]; + if (e != null && e.get() == null) + expungeStaleEntry(j); + } + } + ``` + + Entry **数组为扩容为原来的 2 倍** ,重新计算 key 的散列值,如果遇到 key 为 null 的情况,会将其 value 也置为 null,帮助 GC + + ```java + private void resize() { + Entry[] oldTab = table; + int oldLen = oldTab.length; + // 新数组的长度是老数组的二倍 + int newLen = oldLen * 2; + Entry[] newTab = new Entry[newLen]; + // 统计新table中的entry数量 + int count = 0; + // 遍历老表,进行【数据迁移】 + for (int j = 0; j < oldLen; ++j) { + // 访问老表的指定位置的 entry + Entry e = oldTab[j]; + // 条件成立说明老表中该位置有数据,可能是过期数据也可能不是 + if (e != null) { + ThreadLocal k = e.get(); + // 过期数据 + if (k == null) { + e.value = null; // Help the GC + } else { + // 非过期数据,在新表中进行哈希寻址 + int h = k.threadLocalHashCode & (newLen - 1); + // 【线程探测】 + while (newTab[h] != null) + h = nextIndex(h, newLen); + // 将数据存放到新表合适的 slot 中 + newTab[h] = e; + count++; + } + } + } + // 设置下一次触发扩容的指标:threshold = len * 2 / 3; + setThreshold(newLen); + size = count; + // 将扩容后的新表赋值给 threadLocalMap 内部散列表数组引用 + table = newTab; + } + ``` + +* remove():删除 Entry + + ```java + private void remove(ThreadLocal key) { + Entry[] tab = table; + int len = tab.length; + // 哈希寻址 + int i = key.threadLocalHashCode & (len-1); + for (Entry e = tab[i]; e != null; e = tab[i = nextIndex(i, len)]) { + // 找到了对应的 key + if (e.get() == key) { + // 设置 key 为 null + e.clear(); + // 探测式清理 + expungeStaleEntry(i); + return; + } + } + } + ``` + +*** + +##### 清理方法 + +* 探测式清理:沿着开始位置向后探测清理过期数据,沿途中碰到未过期数据则将此数据 rehash 在 table 数组中的定位,重定位后的元素理论上更接近 `i = entry.key & (table.length - 1)`,让**数据的排列更紧凑**,会优化整个散列表查询性能 + + ```java + // table[staleSlot] 是一个过期数据,以这个位置开始继续向后查找过期数据 + private int expungeStaleEntry(int staleSlot) { + // 获取散列表和数组长度 + Entry[] tab = table; + int len = tab.length; + + // help gc,先把当前过期的 entry 置空,在取消对 entry 的引用 + tab[staleSlot].value = null; + tab[staleSlot] = null; + // 数量-1 + size--; + + Entry e; + int i; + // 从 staleSlot 开始向后遍历,直到碰到 slot == null 结束,【区间内清理过期数据】 + for (i = nextIndex(staleSlot, len); (e = tab[i]) != null; i = nextIndex(i, len)) { + ThreadLocal k = e.get(); + // 当前 entry 是过期数据 + if (k == null) { + // help gc + e.value = null; + tab[i] = null; + size--; + } else { + // 当前 entry 不是过期数据的逻辑,【rehash】 + // 重新计算当前 entry 对应的 index + int h = k.threadLocalHashCode & (len - 1); + // 条件成立说明当前 entry 存储时发生过 hash 冲突,向后偏移过了 + if (h != i) { + // 当前位置置空 + tab[i] = null; + // 以正确位置 h 开始,向后查找第一个可以存放 entry 的位置 + while (tab[h] != null) + h = nextIndex(h, len); + // 将当前元素放入到【距离正确位置更近的位置,有可能就是正确位置】 + tab[h] = e; + } + } + } + // 返回 slot = null 的槽位索引,图例是 7,这个索引代表【索引前面的区间已经清理完成垃圾了】 + return i; + } + ``` + + + + + +* 启发式清理:向后循环扫描过期数据,发现过期数据调用探测式清理方法,如果连续几次的循环都没有发现过期数据,就停止扫描 + + ```java + // i 表示启发式清理工作开始位置,一般是空 slot,n 一般传递的是 table.length + private boolean cleanSomeSlots(int i, int n) { + // 表示启发式清理工作是否清除了过期数据 + boolean removed = false; + // 获取当前 map 的散列表引用 + Entry[] tab = table; + int len = tab.length; + do { + // 获取下一个索引,因为探测式返回的 slot 为 null + i = nextIndex(i, len); + Entry e = tab[i]; + // 条件成立说明是过期的数据,key 被 gc 了 + if (e != null && e.get() == null) { + // 【发现过期数据重置 n 为数组的长度】 + n = len; + // 表示清理过过期数据 + removed = true; + // 以当前过期的 slot 为开始节点 做一次探测式清理工作 + i = expungeStaleEntry(i); + } + // 假设 table 长度为 16 + // 16 >>> 1 ==> 8,8 >>> 1 ==> 4,4 >>> 1 ==> 2,2 >>> 1 ==> 1,1 >>> 1 ==> 0 + // 连续经过这么多次循环【没有扫描到过期数据】,就停止循环,扫描到空 slot 不算,因为不是过期数据 + } while ((n >>>= 1) != 0); + + // 返回清除标记 + return removed; + } + ``` + +参考视频: + +*** + +#### 内存泄漏 + +Memory leak:内存泄漏是指程序中动态分配的堆内存由于某种原因未释放或无法释放,造成系统内存的浪费,导致程序运行速度减慢甚至系统崩溃等严重后果,内存泄漏的堆积终将导致内存溢出 + +* 如果 key 使用强引用:使用完 ThreadLocal ,threadLocal Ref 被回收,但是 threadLocalMap 的 Entry 强引用了 threadLocal,造成 threadLocal 无法被回收,无法完全避免内存泄漏 + + + +* 如果 key 使用弱引用:使用完 ThreadLocal ,threadLocal Ref 被回收,ThreadLocalMap 只持有 ThreadLocal 的弱引用,所以threadlocal 也可以被回收,此时 Entry 中的 key = null。但没有手动删除这个 Entry 或者 CurrentThread 依然运行,依然存在强引用链,value 不会被回收,而这块 value 永远不会被访问到,也会导致 value 内存泄漏 + + + +* 两个主要原因: + + * 没有手动删除这个 Entry + * CurrentThread 依然运行 + +根本原因:ThreadLocalMap 是 Thread的一个属性,**生命周期跟 Thread 一样长**,如果没有手动删除对应 Entry 就会导致内存泄漏 + +解决方法:使用完 ThreadLocal 中存储的内容后将它 remove 掉就可以 + +ThreadLocal 内部解决方法:在 ThreadLocalMap 中的 set/getEntry 方法中,通过线性探测法对 key 进行判断,如果 key 为 null(ThreadLocal 为 null)会对 Entry 进行垃圾回收。所以**使用弱引用比强引用多一层保障**,就算不调用 remove,也有机会进行 GC + +*** + +#### 变量传递 + +##### 基本使用 + +父子线程:创建子线程的线程是父线程,比如实例中的 main 线程就是父线程 + +ThreadLocal 中存储的是线程的局部变量,如果想**实现线程间局部变量传递**可以使用 InheritableThreadLocal 类 + +```java +public static void main(String[] args) { + ThreadLocal threadLocal = new InheritableThreadLocal<>(); + threadLocal.set("父线程设置的值"); + + new Thread(() -> System.out.println("子线程输出:" + threadLocal.get())).start(); +} +// 子线程输出:父线程设置的值 +``` + +*** + +##### 实现原理 + +InheritableThreadLocal 源码: + +```java +public class InheritableThreadLocal extends ThreadLocal { + protected T childValue(T parentValue) { + return parentValue; + } + ThreadLocalMap getMap(Thread t) { + return t.inheritableThreadLocals; + } + void createMap(Thread t, T firstValue) { + t.inheritableThreadLocals = new ThreadLocalMap(this, firstValue); + } +} +``` + +实现父子线程间的局部变量共享需要追溯到 Thread 对象的构造方法: + +```java +private void init(ThreadGroup g, Runnable target, String name, long stackSize, AccessControlContext acc, + // 该参数默认是 true + boolean inheritThreadLocals) { + // ... + Thread parent = currentThread(); + + // 判断父线程(创建子线程的线程)的 inheritableThreadLocals 属性不为 null + if (inheritThreadLocals && parent.inheritableThreadLocals != null) { + // 复制父线程的 inheritableThreadLocals 属性,实现父子线程局部变量共享 + this.inheritableThreadLocals = ThreadLocal.createInheritedMap(parent.inheritableThreadLocals); + } + // .. +} +// 【本质上还是创建 ThreadLocalMap,只是把父类中的可继承数据设置进去了】 +static ThreadLocalMap createInheritedMap(ThreadLocalMap parentMap) { + return new ThreadLocalMap(parentMap); +} +``` + +```java +private ThreadLocalMap(ThreadLocalMap parentMap) { + // 获取父线程的哈希表 + Entry[] parentTable = parentMap.table; + int len = parentTable.length; + setThreshold(len); + table = new Entry[len]; + // 【逐个复制父线程 ThreadLocalMap 中的数据】 + for (int j = 0; j < len; j++) { + Entry e = parentTable[j]; + if (e != null) { + ThreadLocal key = (ThreadLocal) e.get(); + if (key != null) { + // 调用的是 InheritableThreadLocal#childValue(T parentValue) + Object value = key.childValue(e.value); + Entry c = new Entry(key, value); + int h = key.threadLocalHashCode & (len - 1); + // 线性探测 + while (table[h] != null) + h = nextIndex(h, len); + table[h] = c; + size++; + } + } + } +} +``` + +参考文章: + +*** + +## 线程池 + +### 基本概述 + +线程池:一个容纳多个线程的容器,容器中的线程可以重复使用,省去了频繁创建和销毁线程对象的操作 + +线程池作用: + +1. 降低资源消耗,减少了创建和销毁线程的次数,每个工作线程都可以被重复利用,可执行多个任务 +2. 提高响应速度,当任务到达时,如果有线程可以直接用,不会出现系统僵死 +3. 提高线程的可管理性,如果无限制的创建线程,不仅会消耗系统资源,还会降低系统的稳定性,使用线程池可以进行统一的分配,调优和监控 + +线程池的核心思想:**线程复用**,同一个线程可以被重复使用,来处理多个任务 + +池化技术 (Pool) :一种编程技巧,核心思想是资源复用,在请求量大时能优化应用性能,降低系统频繁建连的资源开销 + +*** + +### 阻塞队列 + +#### 基本介绍 + +有界队列和无界队列: + +* 有界队列:有固定大小的队列,比如设定了固定大小的 LinkedBlockingQueue,又或者大小为 0 + +* 无界队列:没有设置固定大小的队列,这些队列可以直接入队,直到溢出(超过 Integer.MAX_VALUE),所以相当于无界 + +java.util.concurrent.BlockingQueue 接口有以下阻塞队列的实现:**FIFO 队列** + +* ArrayBlockQueue:由数组结构组成的有界阻塞队列 +* LinkedBlockingQueue:由链表结构组成的无界(默认大小 Integer.MAX_VALUE)的阻塞队列 +* PriorityBlockQueue:支持优先级排序的无界阻塞队列 +* DelayedWorkQueue:使用优先级队列实现的延迟无界阻塞队列 +* SynchronousQueue:不存储元素的阻塞队列,每一个生产线程会阻塞到有一个 put 的线程放入元素为止 +* LinkedTransferQueue:由链表结构组成的无界阻塞队列 +* LinkedBlockingDeque:由链表结构组成的**双向**阻塞队列 + +与普通队列(LinkedList、ArrayList等)的不同点在于阻塞队列中阻塞添加和阻塞删除方法,以及线程安全: + +* 阻塞添加 put():当阻塞队列元素已满时,添加队列元素的线程会被阻塞,直到队列元素不满时才重新唤醒线程执行 +* 阻塞删除 take():在队列元素为空时,删除队列元素的线程将被阻塞,直到队列不为空再执行删除操作(一般会返回被删除的元素) + +*** + +#### 核心方法 + +| 方法类型 | 抛出异常 | 特殊值 | 阻塞 | 超时 | +| ---------------- | --------- | -------- | ------ | ------------------ | +| 插入(尾) | add(e) | offer(e) | put(e) | offer(e,time,unit) | +| 移除(头) | remove() | poll() | take() | poll(time,unit) | +| 检查(队首元素) | element() | peek() | 不可用 | 不可用 | + +* 抛出异常组: + * 当阻塞队列满时:在往队列中 add 插入元素会抛出 IIIegalStateException: Queue full + * 当阻塞队列空时:再往队列中 remove 移除元素,会抛出 NoSuchException +* 特殊值组: + * 插入方法:成功 true,失败 false + * 移除方法:成功返回出队列元素,队列没有就返回 null +* 阻塞组: + * 当阻塞队列满时,生产者继续往队列里 put 元素,队列会一直阻塞生产线程直到队列有空间 put 数据或响应中断退出 + * 当阻塞队列空时,消费者线程试图从队列里 take 元素,队列会一直阻塞消费者线程直到队列中有可用元素 +* 超时退出:当阻塞队列满时,队里会阻塞生产者线程一定时间,超过限时后生产者线程会退出 + +*** + +#### 链表队列 + +##### 入队出队 + +LinkedBlockingQueue 源码: + +```java +public class LinkedBlockingQueue extends AbstractQueue + implements BlockingQueue, java.io.Serializable { + static class Node { + E item; + /** + * 下列三种情况之一 + * - 真正的后继节点 + * - 自己, 发生在出队时 + * - null, 表示是没有后继节点, 是尾节点了 + */ + Node next; + + Node(E x) { item = x; } + } +} +``` + +入队:**尾插法** + +* 初始化链表 `last = head = new Node(null)`,**Dummy 节点用来占位**,item 为 null + + ```java + public LinkedBlockingQueue(int capacity) { + // 默认是 Integer.MAX_VALUE + if (capacity <= 0) throw new IllegalArgumentException(); + this.capacity = capacity; + last = head = new Node(null); + } + ``` + +* 当一个节点入队: + + ```java + private void enqueue(Node node) { + // 从右向左计算 + last = last.next = node; + } + ``` + + ![](https://seazean.oss-cn-beijing.aliyuncs.com/img/Java/JUC-LinkedBlockingQueue入队流程.png) + +* 再来一个节点入队 `last = last.next = node` + +出队:**出队头节点**,FIFO + +* 出队源码: + + ```java + private E dequeue() { + Node h = head; + // 获取临头节点 + Node first = h.next; + // 自己指向自己,help GC + h.next = h; + head = first; + // 出队的元素 + E x = first.item; + // 【当前节点置为 Dummy 节点】 + first.item = null; + return x; + } + ``` + +* `h = head` → `first = h.next` + + ![](https://seazean.oss-cn-beijing.aliyuncs.com/img/Java/JUC-LinkedBlockingQueue出队流程1.png) + +* `h.next = h` → `head = first` + + ![](https://seazean.oss-cn-beijing.aliyuncs.com/img/Java/JUC-LinkedBlockingQueue出队流程2.png) + + * `first.item = null`:当前节点置为 Dummy 节点 + +*** + +##### 加锁分析 + +用了两把锁和 dummy 节点: + +* 用一把锁,同一时刻,最多只允许有一个线程(生产者或消费者,二选一)执行 +* 用两把锁,同一时刻,可以允许两个线程同时(一个生产者与一个消费者)执行 + * 消费者与消费者线程仍然串行 + * 生产者与生产者线程仍然串行 + +线程安全分析: + +* 当节点总数大于 2 时(包括 dummy 节点),**putLock 保证的是 last 节点的线程安全,takeLock 保证的是 head 节点的线程安全**,两把锁保证了入队和出队没有竞争 + +* 当节点总数等于 2 时(即一个 dummy 节点,一个正常节点)这时候,仍然是两把锁锁两个对象,不会竞争 + +* 当节点总数等于 1 时(就一个 dummy 节点)这时 take 线程会被 notEmpty 条件阻塞,有竞争,会阻塞 + + ```java + // 用于 put(阻塞) offer(非阻塞) + private final ReentrantLock putLock = new ReentrantLock(); + private final Condition notFull = putLock.newCondition(); // 阻塞等待不满,说明已经满了 + + // 用于 take(阻塞) poll(非阻塞) + private final ReentrantLock takeLock = new ReentrantLock(); + private final Condition notEmpty = takeLock.newCondition(); // 阻塞等待不空,说明已经是空的 + ``` + +入队出队: + +* put 操作: + + ```java + public void put(E e) throws InterruptedException { + // 空指针异常 + if (e == null) throw new NullPointerException(); + int c = -1; + // 把待添加的元素封装为 node 节点 + Node node = new Node(e); + // 获取全局生产锁 + final ReentrantLock putLock = this.putLock; + // count 用来维护元素计数 + final AtomicInteger count = this.count; + // 获取可打断锁,会抛出异常 + putLock.lockInterruptibly(); + try { + // 队列满了等待 + while (count.get() == capacity) { + // 【等待队列不满时,就可以生产数据】,线程处于 Waiting + notFull.await(); + } + // 有空位, 入队且计数加一,尾插法 + enqueue(node); + // 返回自增前的数字 + c = count.getAndIncrement(); + // put 完队列还有空位, 唤醒其他生产 put 线程,唤醒一个减少竞争 + if (c + 1 < capacity) + notFull.signal(); + } finally { + // 解锁 + putLock.unlock(); + } + // c自增前是0,说明生产了一个元素,唤醒一个 take 线程 + if (c == 0) + signalNotEmpty(); + } + ``` + + ```java + private void signalNotEmpty() { + final ReentrantLock takeLock = this.takeLock; + takeLock.lock(); + try { + // 调用 notEmpty.signal(),而不是 notEmpty.signalAll() 是为了减少竞争,因为只剩下一个元素 + notEmpty.signal(); + } finally { + takeLock.unlock(); + } + } + ``` + +* take 操作: + + ```java + public E take() throws InterruptedException { + E x; + int c = -1; + // 元素个数 + final AtomicInteger count = this.count; + // 获取全局消费锁 + final ReentrantLock takeLock = this.takeLock; + // 可打断锁 + takeLock.lockInterruptibly(); + try { + // 没有元素可以出队 + while (count.get() == 0) { + // 【阻塞等待队列不空,就可以消费数据】,线程处于 Waiting + notEmpty.await(); + } + // 出队,计数减一,FIFO,出队头节点 + x = dequeue(); + // 返回自减前的数字 + c = count.getAndDecrement(); + // 队列还有元素 + if (c > 1) + // 唤醒一个消费take线程 + notEmpty.signal(); + } finally { + takeLock.unlock(); + } + // c 是消费前的数据,消费前满了,消费一个后还剩一个空位,唤醒生产线程 + if (c == capacity) + // 调用的是 notFull.signal() 而不是 notFull.signalAll() 是为了减少竞争 + signalNotFull(); + return x; + } + ``` + +*** + +##### 性能比较 + +主要列举 LinkedBlockingQueue 与 ArrayBlockingQueue 的性能比较: + +* Linked 支持有界,Array 强制有界 +* Linked 实现是链表,Array 实现是数组 +* Linked 是懒惰的,而 Array 需要提前初始化 Node 数组 +* Linked 每次入队会生成新 Node,而 Array 的 Node 是提前创建好的 +* Linked 两把锁,Array 一把锁 + +*** + +#### 同步队列 + +##### 成员属性 + +SynchronousQueue 是一个不存储元素的 BlockingQueue,**每一个生产者必须阻塞匹配到一个消费者** + +成员变量: + +* 运行当前程序的平台拥有 CPU 的数量: + + ```java + static final int NCPUS = Runtime.getRuntime().availableProcessors() + ``` + +* 指定超时时间后,当前线程最大自旋次数: + + ```java + // 只有一个 CPU 时自旋次数为 0,所有程序都是串行执行,多核 CPU 时自旋 32 次是一个经验值 + static final int maxTimedSpins = (NCPUS < 2) ? 0 : 32; + ``` + + 自旋的原因:线程挂起唤醒需要进行上下文切换,涉及到用户态和内核态的转变,是非常消耗资源的。自旋期间线程会一直检查自己的状态是否被匹配到,如果自旋期间被匹配到,那么直接就返回了,如果自旋次数达到某个指标后,还是会将当前线程挂起 + +* 未指定超时时间,当前线程最大自旋次数: + + ```java + static final int maxUntimedSpins = maxTimedSpins * 16; // maxTimedSpins 的 16 倍 + ``` + +* 指定超时限制的阈值,小于该值的线程不会被挂起: + + ```java + static final long spinForTimeoutThreshold = 1000L; // 纳秒 + ``` + + 超时时间设置的小于该值,就会被禁止挂起,阻塞再唤醒的成本太高,不如选择自旋空转 + +* 转换器: + + ```java + private transient volatile Transferer transferer; + abstract static class Transferer { + /** + * 参数一:可以为 null,null 时表示这个请求是一个 REQUEST 类型的请求,反之是一个 DATA 类型的请求 + * 参数二:如果为 true 表示指定了超时时间,如果为 false 表示不支持超时,会一直阻塞到匹配或者被打断 + * 参数三:超时时间限制,单位是纳秒 + + * 返回值:返回值如果不为 null 表示匹配成功,DATA 类型的请求返回当前线程 put 的数据 + * 如果返回 null,表示请求超时或被中断 + */ + abstract E transfer(E e, boolean timed, long nanos); + } + ``` + +* 构造方法: + + ```java + public SynchronousQueue(boolean fair) { + // fair 默认 false + // 非公平模式实现的数据结构是栈,公平模式的数据结构是队列 + transferer = fair ? new TransferQueue() : new TransferStack(); + } + ``` + +* 成员方法: + + ```java + public boolean offer(E e) { + if (e == null) throw new NullPointerException(); + return transferer.transfer(e, true, 0) != null; + } + public E poll() { + return transferer.transfer(null, true, 0); + } + ``` + +**** + +##### 非公实现 + +TransferStack 是非公平的同步队列,因为所有的请求都被压入栈中,栈顶的元素会最先得到匹配,造成栈底的等待线程饥饿 + +TransferStack 类成员变量: + +* 请求类型: + + ```java + // 表示 Node 类型为请求类型 + static final int REQUEST = 0; + // 表示 Node类 型为数据类型 + static final int DATA = 1; + // 表示 Node 类型为匹配中类型 + // 假设栈顶元素为 REQUEST-NODE,当前请求类型为 DATA,入栈会修改类型为 FULFILLING 【栈顶 & 栈顶之下的一个node】 + // 假设栈顶元素为 DATA-NODE,当前请求类型为 REQUEST,入栈会修改类型为 FULFILLING 【栈顶 & 栈顶之下的一个node】 + static final int FULFILLING = 2; + ``` + +* 栈顶元素: + + ```java + volatile SNode head; + ``` + +内部类 SNode: + +* 成员变量: + + ```java + static final class SNode { + // 指向下一个栈帧 + volatile SNode next; + // 与当前 node 匹配的节点 + volatile SNode match; + // 假设当前node对应的线程自旋期间未被匹配成功,那么node对应的线程需要挂起, + // 挂起前 waiter 保存对应的线程引用,方便匹配成功后,被唤醒。 + volatile Thread waiter; + + // 数据域,不为空表示当前 Node 对应的请求类型为 DATA 类型,反之则表示 Node 为 REQUEST 类型 + Object item; + // 表示当前Node的模式 【DATA/REQUEST/FULFILLING】 + int mode; + } + ``` + +* 构造方法: + + ```java + SNode(Object item) { + this.item = item; + } + ``` + +* 设置方法:设置 Node 对象的 next 字段,此处**对 CAS 进行了优化**,提升了 CAS 的效率 + + ```java + boolean casNext(SNode cmp, SNode val) { + //【优化:cmp == next】,可以提升一部分性能。 cmp == next 不相等,就没必要走 cas指令。 + return cmp == next && UNSAFE.compareAndSwapObject(this, nextOffset, cmp, val); + } + ``` + +* 匹配方法: + + ```java + boolean tryMatch(SNode s) { + // 当前 node 尚未与任何节点发生过匹配,CAS 设置 match 字段为 s 节点,表示当前 node 已经被匹配 + if (match == null && UNSAFE.compareAndSwapObject(this, matchOffset, null, s)) { + // 当前 node 如果自旋结束,会 park 阻塞,阻塞前将 node 对应的 Thread 保留到 waiter 字段 + // 获取当前 node 对应的阻塞线程 + Thread w = waiter; + // 条件成立说明 node 对应的 Thread 正在阻塞 + if (w != null) { + waiter = null; + // 使用 unpark 方式唤醒线程 + LockSupport.unpark(w); + } + return true; + } + // 匹配成功返回 true + return match == s; + } + ``` + +* 取消方法: + + ```java + // 取消节点的方法 + void tryCancel() { + // match 字段指向自己,表示这个 node 是取消状态,取消状态的 node,最终会被强制移除出栈 + UNSAFE.compareAndSwapObject(this, matchOffset, null, this); + } + + boolean isCancelled() { + return match == this; + } + ``` + +TransferStack 类成员方法: + +* snode():填充节点方法 + + ```java + static SNode snode(SNode s, Object e, SNode next, int mode) { + // 引用指向空时,snode 方法会创建一个 SNode 对象 + if (s == null) s = new SNode(e); + // 填充数据 + s.mode = mode; + s.next = next; + return s; + } + ``` + +* transfer():核心方法,请求匹配出栈,不匹配阻塞 + + ```java + E transfer(E e, boolean timed, long nanos) { + // 包装当前线程的 node + SNode s = null; + // 根据元素判断当前的请求类型 + int mode = (e == null) ? REQUEST : DATA; + // 自旋 + for (;;) { + // 获取栈顶指针 + SNode h = head; + // 【CASE1】:当前栈为空或者栈顶 node 模式与当前请求模式一致无法匹配,做入栈操作 + if (h == null || h.mode == mode) { + // 当前请求是支持超时的,但是 nanos <= 0 说明这个请求不支持 “阻塞等待” + if (timed && nanos <= 0) { + // 栈顶元素是取消状态 + if (h != null && h.isCancelled()) + // 栈顶出栈,设置新的栈顶 + casHead(h, h.next); + else + // 表示【匹配失败】 + return null; + // 入栈 + } else if (casHead(h, s = snode(s, e, h, mode))) { + // 等待被匹配的逻辑,正常情况返回匹配的节点;取消情况返回当前节点,就是 s + SNode m = awaitFulfill(s, timed, nanos); + // 说明当前 node 是【取消状态】 + if (m == s) { + // 将取消节点出栈 + clean(s); + return null; + } + // 执行到这说明【匹配成功】了 + // 栈顶有节点并且 匹配节点还未出栈,需要协助出栈 + if ((h = head) != null && h.next == s) + casHead(h, s.next); + // 当前 node 模式为 REQUEST 类型,返回匹配节点的 m.item 数据域 + // 当前 node 模式为 DATA 类型:返回 node.item 数据域,当前请求提交的数据 e + return (E) ((mode == REQUEST) ? m.item : s.item); + } + // 【CASE2】:逻辑到这说明请求模式不一致,如果栈顶不是 FULFILLING 说明没被其他节点匹配,【当前可以匹配】 + } else if (!isFulfilling(h.mode)) { + // 头节点是取消节点,match 指向自己,协助出栈 + if (h.isCancelled()) + casHead(h, h.next); + // 入栈当前请求的节点 + else if (casHead(h, s=snode(s, e, h, FULFILLING|mode))) { + for (;;) { + // m 是 s 的匹配的节点 + SNode m = s.next; + // m 节点在 awaitFulfill 方法中被中断,clean 了自己 + if (m == null) { + // 清空栈 + casHead(s, null); + s = null; + // 返回到外层自旋中 + break; + } + // 获取匹配节点的下一个节点 + SNode mn = m.next; + // 尝试匹配,【匹配成功】,则将 fulfilling 和 m 一起出栈,并且唤醒被匹配的节点的线程 + if (m.tryMatch(s)) { + casHead(s, mn); + return (E) ((mode == REQUEST) ? m.item : s.item); + } else + // 匹配失败,出栈 m + s.casNext(m, mn); + } + } + // 【CASE3】:栈顶模式为 FULFILLING 模式,表示【栈顶和栈顶下面的节点正在发生匹配】,当前请求需要做协助工作 + } else { + // h 表示的是 fulfilling 节点,m 表示 fulfilling 匹配的节点 + SNode m = h.next; + if (m == null) + // 清空栈 + casHead(h, null); + else { + SNode mn = m.next; + // m 和 h 匹配,唤醒 m 中的线程 + if (m.tryMatch(h)) + casHead(h, mn); + else + h.casNext(m, mn); + } + } + } + } + ``` + +* awaitFulfill():阻塞当前线程等待被匹配,返回匹配的节点,或者被取消的节点 + + ```java + SNode awaitFulfill(SNode s, boolean timed, long nanos) { + // 等待的截止时间 + final long deadline = timed ? System.nanoTime() + nanos : 0L; + // 当前线程 + Thread w = Thread.currentThread(); + // 表示当前请求线程在下面的 for(;;) 自旋检查的次数 + int spins = (shouldSpin(s) ? (timed ? maxTimedSpins : maxUntimedSpins) : 0); + // 自旋检查逻辑:是否匹配、是否超时、是否被中断 + for (;;) { + // 当前线程收到中断信号,需要设置 node 状态为取消状态 + if (w.isInterrupted()) + s.tryCancel(); + // 获取与当前 s 匹配的节点 + SNode m = s.match; + if (m != null) + // 可能是正常的匹配的,也可能是取消的 + return m; + // 执行了超时限制就判断是否超时 + if (timed) { + nanos = deadline - System.nanoTime(); + // 【超时了,取消节点】 + if (nanos <= 0L) { + s.tryCancel(); + continue; + } + } + // 说明当前线程还可以进行自旋检查 + if (spins > 0) + // 自旋一次 递减 1 + spins = shouldSpin(s) ? (spins - 1) : 0; + // 说明没有自旋次数了 + else if (s.waiter == null) + //【把当前 node 对应的 Thread 保存到 node.waiter 字段中,要阻塞了】 + s.waiter = w; + // 没有超时限制直接阻塞 + else if (!timed) + LockSupport.park(this); + // nanos > 1000 纳秒的情况下,才允许挂起当前线程 + else if (nanos > spinForTimeoutThreshold) + LockSupport.parkNanos(this, nanos); + } + } + ``` + + ```java + boolean shouldSpin(SNode s) { + // 获取栈顶 + SNode h = head; + // 条件一成立说明当前 s 就是栈顶,允许自旋检查 + // 条件二成立说明当前 s 节点自旋检查期间,又来了一个与当前 s 节点匹配的请求,双双出栈后条件会成立 + // 条件三成立前提当前 s 不是栈顶元素,并且当前栈顶正在匹配中,这种状态栈顶下面的元素,都允许自旋检查 + return (h == s || h == null || isFulfilling(h.mode)); + } + ``` + +* clear():指定节点出栈 + + ```java + void clean(SNode s) { + // 清空数据域和关联线程 + s.item = null; + s.waiter = null; + + // 获取取消节点的下一个节点 + SNode past = s.next; + // 判断后继节点是不是取消节点,是就更新 past + if (past != null && past.isCancelled()) + past = past.next; + + SNode p; + // 从栈顶开始向下检查,【将栈顶开始向下的 取消状态 的节点全部清理出去】,直到碰到 past 或者不是取消状态为止 + while ((p = head) != null && p != past && p.isCancelled()) + // 修改的是内存地址对应的值,p 指向该内存地址所以数据一直在变化 + casHead(p, p.next); + // 说明中间遇到了不是取消状态的节点,继续迭代下去 + while (p != null && p != past) { + SNode n = p.next; + if (n != null && n.isCancelled()) + p.casNext(n, n.next); + else + p = n; + } + } + ``` + +*** + +##### 公平实现 + +TransferQueue 是公平的同步队列,采用 FIFO 的队列实现,请求节点与队尾模式不同,需要与队头发生匹配 + +TransferQueue 类成员变量: + +* 指向队列的 dummy 节点: + + ```java + transient volatile QNode head; + ``` + +* 指向队列的尾节点: + + ```java + transient volatile QNode tail; + ``` + +* 被清理节点的前驱节点: + + ```java + transient volatile QNode cleanMe; + ``` + + 入队操作是两步完成的,第一步是 t.next = newNode,第二步是 tail = newNode,所以队尾节点出队,是一种非常特殊的情况 + +TransferQueue 内部类: + +* QNode: + + ```java + static final class QNode { + // 指向当前节点的下一个节点 + volatile QNode next; + // 数据域,Node 代表的是 DATA 类型 item 表示数据,否则 Node 代表的 REQUEST 类型,item == null + volatile Object item; + // 假设当前 node 对应的线程自旋期间未被匹配成功,那么 node 对应的线程需要挂起, + // 挂起前 waiter 保存对应的线程引用,方便匹配成功后被唤醒。 + volatile Thread waiter; + // true 当前 Node 是一个 DATA 类型,false 表示当前 Node 是一个 REQUEST 类型 + final boolean isData; + + // 构建方法 + QNode(Object item, boolean isData) { + this.item = item; + this.isData = isData; + } + + // 尝试取消当前 node,取消状态的 node 的 item 域指向自己 + void tryCancel(Object cmp) { + UNSAFE.compareAndSwapObject(this, itemOffset, cmp, this); + } + + // 判断当前 node 是否为取消状态 + boolean isCancelled() { + return item == this; + } + + // 判断当前节点是否 “不在” 队列内,当 next 指向自己时,说明节点已经出队。 + boolean isOffList() { + return next == this; + } + } + ``` + +TransferQueue 类成员方法: + +* 设置头尾节点: + + ```java + void advanceHead(QNode h, QNode nh) { + // 设置头指针指向新的节点, + if (h == head && UNSAFE.compareAndSwapObject(this, headOffset, h, nh)) + // 老的头节点出队 + h.next = h; + } + void advanceTail(QNode t, QNode nt) { + if (tail == t) + // 更新队尾节点为新的队尾 + UNSAFE.compareAndSwapObject(this, tailOffset, t, nt); + } + ``` + +* transfer():核心方法 + + ```java + E transfer(E e, boolean timed, long nanos) { + // s 指向当前请求对应的 node + QNode s = null; + // 是否是 DATA 类型的请求 + boolean isData = (e != null); + // 自旋 + for (;;) { + QNode t = tail; + QNode h = head; + if (t == null || h == null) + continue; + // head 和 tail 同时指向 dummy 节点,说明是空队列 + // 队尾节点与当前请求类型是一致的情况,说明阻塞队列中都无法匹配, + if (h == t || t.isData == isData) { + // 获取队尾 t 的 next 节点 + QNode tn = t.next; + // 多线程环境中其他线程可能修改尾节点 + if (t != tail) + continue; + // 已经有线程入队了,更新 tail + if (tn != null) { + advanceTail(t, tn); + continue; + } + // 允许超时,超时时间小于 0,这种方法不支持阻塞等待 + if (timed && nanos <= 0) + return null; + // 创建 node 的逻辑 + if (s == null) + s = new QNode(e, isData); + // 将 node 添加到队尾 + if (!t.casNext(null, s)) + continue; + // 更新队尾指针 + advanceTail(t, s); + + // 当前节点 等待匹配.... + Object x = awaitFulfill(s, e, timed, nanos); + + // 说明【当前 node 状态为 取消状态】,需要做出队逻辑 + if (x == s) { + clean(t, s); + return null; + } + // 说明当前 node 仍然在队列内,匹配成功,需要做出队逻辑 + if (!s.isOffList()) { + // t 是当前 s 节点的前驱节点,判断 t 是不是头节点,是就更新 dummy 节点为 s 节点 + advanceHead(t, s); + // s 节点已经出队,所以需要把它的 item 域设置为它自己,表示它是个取消状态 + if (x != null) + s.item = s; + s.waiter = null; + } + return (x != null) ? (E)x : e; + // 队尾节点与当前请求节点【互补匹配】 + } else { + // h.next 节点,【请求节点与队尾模式不同,需要与队头发生匹配】,TransferQueue 是一个【公平模式】 + QNode m = h.next; + // 并发导致其他线程修改了队尾节点,或者已经把 head.next 匹配走了 + if (t != tail || m == null || h != head) + continue; + // 获取匹配节点的数据域保存到 x + Object x = m.item; + // 判断是否匹配成功 + if (isData == (x != null) || + x == m || + !m.casItem(x, e)) { + advanceHead(h, m); + continue; + } + // 【匹配完成】,将头节点出队,让这个新的头结点成为 dummy 节点 + advanceHead(h, m); + // 唤醒该匹配节点的线程 + LockSupport.unpark(m.waiter); + return (x != null) ? (E)x : e; + } + } + } + ``` + +* awaitFulfill():阻塞当前线程等待被匹配 + + ```java + Object awaitFulfill(QNode s, E e, boolean timed, long nanos) { + // 表示等待截止时间 + final long deadline = timed ? System.nanoTime() + nanos : 0L; + Thread w = Thread.currentThread(); + // 自选检查的次数 + int spins = ((head.next == s) ? (timed ? maxTimedSpins : maxUntimedSpins) : 0); + for (;;) { + // 被打断就取消节点 + if (w.isInterrupted()) + s.tryCancel(e); + // 获取当前 Node 数据域 + Object x = s.item; + + // 当前请求为 DATA 模式时:e 请求带来的数据 + // s.item 修改为 this,说明当前 QNode 对应的线程 取消状态 + // s.item 修改为 null 表示已经有匹配节点了,并且匹配节点拿走了 item 数据 + + // 当前请求为 REQUEST 模式时:e == null + // s.item 修改为 this,说明当前 QNode 对应的线程 取消状态 + // s.item != null 且 item != this 表示当前 REQUEST 类型的 Node 已经匹配到 DATA 了 + if (x != e) + return x; + // 超时检查 + if (timed) { + nanos = deadline - System.nanoTime(); + if (nanos <= 0L) { + s.tryCancel(e); + continue; + } + } + // 自旋次数减一 + if (spins > 0) + --spins; + // 没有自旋次数了,把当前线程封装进去 waiter + else if (s.waiter == null) + s.waiter = w; + // 阻塞 + else if (!timed) + LockSupport.park(this); + else if (nanos > spinForTimeoutThreshold) + LockSupport.parkNanos(this, nanos); + } + } + ``` + +*** + +### 操作Pool + +#### 创建方式 + +##### Executor + +存放线程的容器: + +```java +private final HashSet workers = new HashSet(); +``` + +构造方法: + +```java +public ThreadPoolExecutor(int corePoolSize, + int maximumPoolSize, + long keepAliveTime, + TimeUnit unit, + BlockingQueue workQueue, + ThreadFactory threadFactory, + RejectedExecutionHandler handler) +``` + +参数介绍: + +* corePoolSize:核心线程数,定义了最小可以同时运行的线程数量 + +* maximumPoolSize:最大线程数,当队列中存放的任务达到队列容量时,当前可以同时运行的数量变为最大线程数,创建线程并立即执行最新的任务,与核心线程数之间的差值又叫救急线程数 + +* keepAliveTime:救急线程最大存活时间,当线程池中的线程数量大于 `corePoolSize` 的时候,如果这时没有新的任务提交,核心线程外的线程不会立即销毁,而是会等到 `keepAliveTime` 时间超过销毁 + +* unit:`keepAliveTime` 参数的时间单位 + +* workQueue:阻塞队列,存放被提交但尚未被执行的任务 + +* threadFactory:线程工厂,创建新线程时用到,可以为线程创建时起名字 + +* handler:拒绝策略,线程到达最大线程数仍有新任务时会执行拒绝策略 + + RejectedExecutionHandler 下有 4 个实现类: + + * AbortPolicy:让调用者抛出 RejectedExecutionException 异常,**默认策略** + * CallerRunsPolicy:让调用者运行的调节机制,将某些任务回退到调用者,从而降低新任务的流量 + * DiscardPolicy:直接丢弃任务,不予任何处理也不抛出异常 + * DiscardOldestPolicy:放弃队列中最早的任务,把当前任务加入队列中尝试再次提交当前任务 + + 补充:其他框架拒绝策略 + + * Dubbo:在抛出 RejectedExecutionException 异常前记录日志,并 dump 线程栈信息,方便定位问题 + * Netty:创建一个新线程来执行任务 + * ActiveMQ:带超时等待(60s)尝试放入队列 + * PinPoint:它使用了一个拒绝策略链,会逐一尝试策略链中每种拒绝策略 + +工作原理: + +![](https://seazean.oss-cn-beijing.aliyuncs.com/img/Java/JUC-线程池工作原理.png) + +1. 创建线程池,这时没有创建线程(**懒惰**),等待提交过来的任务请求,调用 execute 方法才会创建线程 + +2. 当调用 execute() 方法添加一个请求任务时,线程池会做如下判断: + * 如果正在运行的线程数量小于 corePoolSize,那么马上创建线程运行这个任务 + * 如果正在运行的线程数量大于或等于 corePoolSize,那么将这个任务放入队列 + * 如果这时队列满了且正在运行的线程数量还小于 maximumPoolSize,那么会创建非核心线程**立刻运行这个任务**,对于阻塞队列中的任务不公平。这是因为创建每个 Worker(线程)对象会绑定一个初始任务,启动 Worker 时会优先执行 + * 如果队列满了且正在运行的线程数量大于或等于 maximumPoolSize,那么线程池会启动饱和**拒绝策略**来执行 +3. 当一个线程完成任务时,会从队列中取下一个任务来执行 + +4. 当一个线程空闲超过一定的时间(keepAliveTime)时,线程池会判断:如果当前运行的线程数大于 corePoolSize,那么这个线程就被停掉,所以线程池的所有任务完成后最终会收缩到 corePoolSize 大小 + +图片来源: + +*** + +##### Executors + +Executors 提供了四种线程池的创建:newCachedThreadPool、newFixedThreadPool、newSingleThreadExecutor、newScheduledThreadPool + +* newFixedThreadPool:创建一个拥有 n 个线程的线程池 + + ```java + public static ExecutorService newFixedThreadPool(int nThreads) { + return new ThreadPoolExecutor(nThreads, nThreads, 0L, TimeUnit.MILLISECONDS, + new LinkedBlockingQueue()); + } + ``` + + * 核心线程数 == 最大线程数(没有救急线程被创建),因此也无需超时时间 + * LinkedBlockingQueue 是一个单向链表实现的阻塞队列,默认大小为 `Integer.MAX_VALUE`,也就是无界队列,可以放任意数量的任务,在任务比较多的时候会导致 OOM(内存溢出) + * 适用于任务量已知,相对耗时的长期任务 + +* newCachedThreadPool:创建一个可扩容的线程池 + + ```java + public static ExecutorService newCachedThreadPool() { + return new ThreadPoolExecutor(0, Integer.MAX_VALUE, 60L, TimeUnit.SECONDS, + new SynchronousQueue()); + } + ``` + + * 核心线程数是 0, 最大线程数是 29 个 1,全部都是救急线程(60s 后可以回收),可能会创建大量线程,从而导致 **OOM** + * SynchronousQueue 作为阻塞队列,没有容量,对于每一个 take 的线程会阻塞直到有一个 put 的线程放入元素为止(类似一手交钱、一手交货) + + * 适合任务数比较密集,但每个任务执行时间较短的情况 + +* newSingleThreadExecutor:创建一个只有 1 个线程的单线程池 + + ```java + public static ExecutorService newSingleThreadExecutor() { + return new FinalizableDelegatedExecutorService + (new ThreadPoolExecutor(1, 1,0L, TimeUnit.MILLISECONDS, + new LinkedBlockingQueue())); + } + ``` + + * 保证所有任务按照**指定顺序执行**,线程数固定为 1,任务数多于 1 时会放入无界队列排队,任务执行完毕,这唯一的线程也不会被释放 + +对比: + +* 创建一个单线程串行执行任务,如果任务执行失败而终止那么没有任何补救措施,线程池会新建一个线程,保证池的正常工作 + +* Executors.newSingleThreadExecutor() 线程个数始终为 1,不能修改。FinalizableDelegatedExecutorService 应用的是装饰器模式,只对外暴露了 ExecutorService 接口,因此不能调用 ThreadPoolExecutor 中特有的方法 + + 原因:父类不能直接调用子类中的方法,需要反射或者创建对象的方式,可以调用子类静态方法 + +* Executors.newFixedThreadPool(1) 初始时为 1,可以修改。对外暴露的是 ThreadPoolExecutor 对象,可以强转后调用 setCorePoolSize 等方法进行修改 + +![](https://seazean.oss-cn-beijing.aliyuncs.com/img/Java/JUC-newSingleThreadExecutor.png) + +*** + +##### 开发要求 + +阿里巴巴 Java 开发手册要求: + +* **线程资源必须通过线程池提供,不允许在应用中自行显式创建线程** + + * 使用线程池的好处是减少在创建和销毁线程上所消耗的时间以及系统资源的开销,解决资源不足的问题 + * 如果不使用线程池,有可能造成系统创建大量同类线程而导致消耗完内存或者过度切换的问题 + +* 线程池不允许使用 Executors 去创建,而是通过 ThreadPoolExecutor 的方式,这样的处理方式更加明确线程池的运行规则,规避资源耗尽的风险 + + Executors 返回的线程池对象弊端如下: + + * FixedThreadPool 和 SingleThreadPool:请求队列长度为 Integer.MAX_VALUE,可能会堆积大量的请求,从而导致 OOM + * CacheThreadPool 和 ScheduledThreadPool:允许创建线程数量为 Integer.MAX_VALUE,可能会创建大量的线程,导致 OOM + +创建多大容量的线程池合适? + +* 一般来说池中**总线程数是核心池线程数量两倍**,确保当核心池有线程停止时,核心池外有线程进入核心池 + +* 过小会导致程序不能充分地利用系统资源、容易导致饥饿 + +* 过大会导致更多的线程上下文切换,占用更多内存 + + 上下文切换:当前任务在执行完 CPU 时间片切换到另一个任务之前会先保存自己的状态,以便下次再切换回这个任务时,可以再加载这个任务的状态,任务从保存到再加载的过程就是一次上下文切换 + +核心线程数常用公式: + +* **CPU 密集型任务 (N+1):** 这种任务消耗的是 CPU 资源,可以将核心线程数设置为 N (CPU 核心数) + 1,比 CPU 核心数多出来的一个线程是为了防止线程发生缺页中断,或者其它原因导致的任务暂停而带来的影响。一旦任务暂停,CPU 某个核心就会处于空闲状态,而在这种情况下多出来的一个线程就可以充分利用 CPU 的空闲时间 + + CPU 密集型简单理解就是利用 CPU 计算能力的任务比如在内存中对大量数据进行分析 + +* **I/O 密集型任务:** 这种系统 CPU 处于阻塞状态,用大部分的时间来处理 I/O 交互,而线程在处理 I/O 的时间段内不会占用 CPU 来处理,这时就可以将 CPU 交出给其它线程使用,因此在 I/O 密集型任务的应用中,我们可以多配置一些线程,具体的计算方法是 2N 或 CPU 核数/ (1-阻塞系数),阻塞系数在 0.8~0.9 之间 + + IO 密集型就是涉及到网络读取,文件读取此类任务 ,特点是 CPU 计算耗费时间相比于等待 IO 操作完成的时间来说很少,大部分时间都花在了等待 IO 操作完成上 + +*** + +#### 提交方法 + +ExecutorService 类 API: + +| 方法 | 说明 | +| ------------------------------------------------------------ | ------------------------------------------------------------ | +| void execute(Runnable command) | 执行任务(Executor 类 API) | +| Future submit(Runnable task) | 提交任务 task() | +| Future submit(Callable task) | 提交任务 task,用返回值 Future 获得任务执行结果 | +| List> invokeAll(Collection> tasks) | 提交 tasks 中所有任务 | +| List> invokeAll(Collection> tasks, long timeout, TimeUnit unit) | 提交 tasks 中所有任务,超时时间针对所有task,超时会取消没有执行完的任务,并抛出超时异常 | +| T invokeAny(Collection> tasks) | 提交 tasks 中所有任务,哪个任务先成功执行完毕,返回此任务执行结果,其它任务取消 | + +execute 和 submit 都属于线程池的方法,对比: + +* execute 只能执行 Runnable 类型的任务,没有返回值; submit 既能提交 Runnable 类型任务也能提交 Callable 类型任务,底层是**封装成 FutureTask,然后调用 execute 执行** + +* execute 会直接抛出任务执行时的异常,submit 会吞掉异常,可通过 Future 的 get 方法将任务执行时的异常重新抛出 + +*** + +#### 关闭方法 + +ExecutorService 类 API: + +| 方法 | 说明 | +| ----------------------------------------------------- | ------------------------------------------------------------ | +| void shutdown() | 线程池状态变为 SHUTDOWN,等待任务执行完后关闭线程池,不会接收新任务,但已提交任务会执行完,而且也可以添加线程(不绑定任务) | +| List shutdownNow() | 线程池状态变为 STOP,用 interrupt 中断正在执行的任务,直接关闭线程池,不会接收新任务,会将队列中的任务返回 | +| boolean isShutdown() | 不在 RUNNING 状态的线程池,此执行者已被关闭,方法返回 true | +| boolean isTerminated() | 线程池状态是否是 TERMINATED,如果所有任务在关闭后完成,返回 true | +| boolean awaitTermination(long timeout, TimeUnit unit) | 调用 shutdown 后,由于调用线程不会等待所有任务运行结束,如果它想在线程池 TERMINATED 后做些事情,可以利用此方法等待 | + +*** + +#### 处理异常 + +execute 会直接抛出任务执行时的异常,submit 会吞掉异常,有两种处理方法 + +方法 1:主动捉异常 + +```java +ExecutorService executorService = Executors.newFixedThreadPool(1); +pool.submit(() -> { + try { + System.out.println("task1"); + int i = 1 / 0; + } catch (Exception e) { + e.printStackTrace(); + } +}); +``` + +方法 2:使用 Future 对象 + +```java +ExecutorService executorService = Executors.newFixedThreadPool(1); +Future future = pool.submit(() -> { + System.out.println("task1"); + int i = 1 / 0; + return true; +}); +System.out.println(future.get()); +``` + +*** + +### 工作原理 + +#### 状态信息 + +ThreadPoolExecutor 使用 int 的**高 3 位来表示线程池状态,低 29 位表示线程数量**。这些信息存储在一个原子变量 ctl 中,目的是将线程池状态与线程个数合二为一,这样就可以用一次 CAS 原子操作进行赋值 + +* 状态表示: + + ```java + // 高3位:表示当前线程池运行状态,除去高3位之后的低位:表示当前线程池中所拥有的线程数量 + private final AtomicInteger ctl = new AtomicInteger(ctlOf(RUNNING, 0)); + // 表示在 ctl 中,低 COUNT_BITS 位,是用于存放当前线程数量的位 + private static final int COUNT_BITS = Integer.SIZE - 3; + // 低 COUNT_BITS 位所能表达的最大数值,000 11111111111111111111 => 5亿多 + private static final int CAPACITY = (1 << COUNT_BITS) - 1; + ``` + + ![](https://seazean.oss-cn-beijing.aliyuncs.com/img/Java/JUC-线程池状态转换图.png) + +* 四种状态: + + ```java + // 111 000000000000000000,转换成整数后其实就是一个【负数】 + private static final int RUNNING = -1 << COUNT_BITS; + // 000 000000000000000000 + private static final int SHUTDOWN = 0 << COUNT_BITS; + // 001 000000000000000000 + private static final int STOP = 1 << COUNT_BITS; + // 010 000000000000000000 + private static final int TIDYING = 2 << COUNT_BITS; + // 011 000000000000000000 + private static final int TERMINATED = 3 << COUNT_BITS; + ``` + + | 状态 | 高3位 | 接收新任务 | 处理阻塞任务队列 | 说明 | + | ---------- | ----- | ---------- | ---------------- | ----------------------------------------- | + | RUNNING | 111 | Y | Y | | + | SHUTDOWN | 000 | N | Y | 不接收新任务,但处理阻塞队列剩余任务 | + | STOP | 001 | N | N | 中断正在执行的任务,并抛弃阻塞队列任务 | + | TIDYING | 010 | - | - | 任务全执行完毕,活动线程为 0 即将进入终结 | + | TERMINATED | 011 | - | - | 终止状态 | + +* 获取当前线程池运行状态: + + ```java + // ~CAPACITY = ~000 11111111111111111111 = 111 000000000000000000000(取反) + // c == ctl = 111 000000000000000000111 + // 111 000000000000000000111 + // 111 000000000000000000000 + // 111 000000000000000000000 获取到了运行状态 + private static int runStateOf(int c) { return c & ~CAPACITY; } + ``` + +* 获取当前线程池线程数量: + + ```java + // c = 111 000000000000000000111 + // CAPACITY = 000 111111111111111111111 + // 000 000000000000000000111 => 7 + private static int workerCountOf(int c) { return c & CAPACITY; } + ``` + +* 重置当前线程池状态 ctl: + + ```java + // rs 表示线程池状态,wc 表示当前线程池中 worker(线程)数量,相与以后就是合并后的状态 + private static int ctlOf(int rs, int wc) { return rs | wc; } + ``` + +* 比较当前线程池 ctl 所表示的状态: + + ```java + // 比较当前线程池 ctl 所表示的状态,是否小于某个状态 s + // 状态对比:RUNNING < SHUTDOWN < STOP < TIDYING < TERMINATED + private static boolean runStateLessThan(int c, int s) { return c < s; } + // 比较当前线程池 ctl 所表示的状态,是否大于等于某个状态s + private static boolean runStateAtLeast(int c, int s) { return c >= s; } + // 小于 SHUTDOWN 的一定是 RUNNING,SHUTDOWN == 0 + private static boolean isRunning(int c) { return c < SHUTDOWN; } + ``` + +* 设置线程池 ctl: + + ```java + // 使用 CAS 方式 让 ctl 值 +1 ,成功返回 true, 失败返回 false + private boolean compareAndIncrementWorkerCount(int expect) { + return ctl.compareAndSet(expect, expect + 1); + } + // 使用 CAS 方式 让 ctl 值 -1 ,成功返回 true, 失败返回 false + private boolean compareAndDecrementWorkerCount(int expect) { + return ctl.compareAndSet(expect, expect - 1); + } + // 将 ctl 值减一,do while 循环会一直重试,直到成功为止 + private void decrementWorkerCount() { + do {} while (!compareAndDecrementWorkerCount(ctl.get())); + } + ``` + +**** + +#### 成员属性 + +成员变量 + +* **线程池中存放 Worker 的容器**:线程池没有初始化,直接往池中加线程即可 + + ```java + private final HashSet workers = new HashSet(); + ``` + +* 线程全局锁: + + ```java + // 增加减少 worker 或者时修改线程池运行状态需要持有 mainLock + private final ReentrantLock mainLock = new ReentrantLock(); + ``` + +* 可重入锁的条件变量: + + ```java + // 当外部线程调用 awaitTermination() 方法时,会等待当前线程池状态为 Termination 为止 + private final Condition termination = mainLock.newCondition() + ``` + +* 线程池相关参数: + + ```java + private volatile int corePoolSize; // 核心线程数量 + private volatile int maximumPoolSize; // 线程池最大线程数量 + private volatile long keepAliveTime; // 空闲线程存活时间 + private volatile ThreadFactory threadFactory; // 创建线程时使用的线程工厂,默认是 DefaultThreadFactory + private final BlockingQueue workQueue;// 【超过核心线程提交任务就放入 阻塞队列】 + ``` + + ```java + private volatile RejectedExecutionHandler handler; // 拒绝策略,juc包提供了4中方式 + private static final RejectedExecutionHandler defaultHandler = new AbortPolicy();// 默认策略 + ``` + +* 记录线程池相关属性的数值: + + ```java + private int largestPoolSize; // 记录线程池生命周期内线程数最大值 + private long completedTaskCount; // 记录线程池所完成任务总数,当某个 worker 退出时将完成的任务累加到该属性 + ``` + +* 控制**核心线程数量内的线程是否可以被回收**: + + ```java + // false(默认)代表不可以,为 true 时核心线程空闲超过 keepAliveTime 也会被回收 + // allowCoreThreadTimeOut(boolean value) 方法可以设置该值 + private volatile boolean allowCoreThreadTimeOut; + ``` + +内部类: + +* Worker 类:**每个 Worker 对象会绑定一个初始任务**,启动 Worker 时优先执行,这也是造成线程池不公平的原因。Worker 继承自 AQS,本身具有锁的特性,采用独占锁模式,state = 0 表示未被占用,> 0 表示被占用,< 0 表示初始状态不能被抢锁 + + ```java + private final class Worker extends AbstractQueuedSynchronizer implements Runnable { + final Thread thread; // worker 内部封装的工作线程 + Runnable firstTask; // worker 第一个执行的任务,普通的 Runnable 实现类或者是 FutureTask + volatile long completedTasks; // 记录当前 worker 所完成任务数量 + + // 构造方法 + Worker(Runnable firstTask) { + // 设置AQS独占模式为初始化中状态,这个状态不能被抢占锁 + setState(-1); + // firstTask不为空时,当worker启动后,内部线程会优先执行firstTask,执行完后会到queue中去获取下个任务 + this.firstTask = firstTask; + // 使用线程工厂创建一个线程,并且【将当前worker指定为Runnable】,所以thread启动时会调用 worker.run() + this.thread = getThreadFactory().newThread(this); + } + // 【不可重入锁】 + protected boolean tryAcquire(int unused) { + if (compareAndSetState(0, 1)) { + setExclusiveOwnerThread(Thread.currentThread()); + return true; + } + return false; + } + } + ``` + + ```java + public Thread newThread(Runnable r) { + // 将当前 worker 指定为 thread 的执行方法,线程调用 start 会调用 r.run() + Thread t = new Thread(group, r, namePrefix + threadNumber.getAndIncrement(), 0); + if (t.isDaemon()) + t.setDaemon(false); + if (t.getPriority() != Thread.NORM_PRIORITY) + t.setPriority(Thread.NORM_PRIORITY); + return t; + } + ``` + +* 拒绝策略相关的内部类 + +*** + +#### 成员方法 + +##### 提交方法 + +* AbstractExecutorService#submit():提交任务,**把 Runnable 或 Callable 任务封装成 FutureTask 执行**,可以通过方法返回的任务对象,调用 get 阻塞获取任务执行的结果或者异常,源码分析在笔记的 Future 部分 + + ```java + public Future submit(Runnable task) { + // 空指针异常 + if (task == null) throw new NullPointerException(); + // 把 Runnable 封装成未来任务对象,执行结果就是 null,也可以通过参数指定 FutureTask#get 返回数据 + RunnableFuture ftask = newTaskFor(task, null); + // 执行方法 + execute(ftask); + return ftask; + } + public Future submit(Callable task) { + if (task == null) throw new NullPointerException(); + // 把 Callable 封装成未来任务对象 + RunnableFuture ftask = newTaskFor(task); + // 执行方法 + execute(ftask); + // 返回未来任务对象,用来获取返回值 + return ftask; + } + ``` + + ```java + protected RunnableFuture newTaskFor(Runnable runnable, T value) { + // Runnable 封装成 FutureTask,【指定返回值】 + return new FutureTask(runnable, value); + } + protected RunnableFuture newTaskFor(Callable callable) { + // Callable 直接封装成 FutureTask + return new FutureTask(callable); + } + ``` + +* execute():执行任务,**但是没有返回值,没办法获取任务执行结果**,出现异常会直接抛出任务执行时的异常。根据线程池中的线程数,选择添加任务时的处理方式 + + ```java + // command 可以是普通的 Runnable 实现类,也可以是 FutureTask,不能是 Callable + public void execute(Runnable command) { + // 非空判断 + if (command == null) + throw new NullPointerException(); + // 获取 ctl 最新值赋值给 c,ctl 高 3 位表示线程池状态,低位表示当前线程池线程数量。 + int c = ctl.get(); + // 【1】当前线程数量小于核心线程数,此次提交任务直接创建一个新的 worker,线程池中多了一个新的线程 + if (workerCountOf(c) < corePoolSize) { + // addWorker 为创建线程的过程,会创建 worker 对象并且将 command 作为 firstTask,优先执行 + if (addWorker(command, true)) + return; + + // 执行到这条语句,说明 addWorker 一定是失败的,存在并发现象或者线程池状态被改变,重新获取状态 + // SHUTDOWN 状态下也有可能创建成功,前提 firstTask == null 而且当前 queue 不为空(特殊情况) + c = ctl.get(); + } + // 【2】执行到这说明当前线程数量已经达到核心线程数量 或者 addWorker 失败 + // 判断当前线程池是否处于running状态,成立就尝试将 task 放入到 workQueue 中 + if (isRunning(c) && workQueue.offer(command)) { + int recheck = ctl.get(); + // 条件一成立说明线程池状态被外部线程给修改了,可能是执行了 shutdown() 方法,该状态不能接收新提交的任务 + // 所以要把刚提交的任务删除,删除成功说明提交之后线程池中的线程还未消费(处理)该任务 + if (!isRunning(recheck) && remove(command)) + // 任务出队成功,走拒绝策略 + reject(command); + // 执行到这说明线程池是 running 状态,获取线程池中的线程数量,判断是否是 0 + // 【担保机制】,保证线程池在 running 状态下,最起码得有一个线程在工作 + else if (workerCountOf(recheck) == 0) + addWorker(null, false); + } + // 【3】offer失败说明queue满了 + // 如果线程数量尚未达到 maximumPoolSize,会创建非核心 worker 线程直接执行 command,【这也是不公平的原因】 + // 如果当前线程数量达到 maximumPoolSiz,这里 addWorker 也会失败,走拒绝策略 + else if (!addWorker(command, false)) + reject(command); + } + ``` + +*** + +##### 添加线程 + +* prestartAllCoreThreads():**提前预热**,创建所有的核心线程 + + ```java + public int prestartAllCoreThreads() { + int n = 0; + while (addWorker(null, true)) + ++n; + return n; + } + ``` + +* addWorker():**添加线程到线程池**,返回 true 表示创建 Worker 成功,且线程启动。首先判断线程池是否允许添加线程,允许就让线程数量 + 1,然后去创建 Worker 加入线程池 + + 注意:SHUTDOWN 状态也能添加线程,但是要求新加的 Woker 没有 firstTask,而且当前 queue 不为空,所以创建一个线程来帮助线程池执行队列中的任务 + + ```java + // core == true 表示采用核心线程数量限制,false 表示采用 maximumPoolSize + private boolean addWorker(Runnable firstTask, boolean core) { + // 自旋【判断当前线程池状态是否允许创建线程】,允许就设置线程数量 + 1 + retry: + for (;;) { + // 获取 ctl 的值 + int c = ctl.get(); + // 获取当前线程池运行状态 + int rs = runStateOf(c); + + // 判断当前线程池状态【是否允许添加线程】 + + // 当前线程池是 SHUTDOWN 状态,但是队列里面还有任务尚未处理完,需要处理完 queue 中的任务 + // 【不允许再提交新的 task,所以 firstTask 为空,但是可以继续添加 worker】 + if (rs >= SHUTDOWN && !(rs == SHUTDOWN && firstTask == null && !workQueue.isEmpty())) + return false; + for (;;) { + // 获取线程池中线程数量 + int wc = workerCountOf(c); + // 条件一一般不成立,CAPACITY是5亿多,根据 core 判断使用哪个大小限制线程数量,超过了返回 false + if (wc >= CAPACITY || wc >= (core ? corePoolSize : maximumPoolSize)) + return false; + // 记录线程数量已经加 1,类比于申请到了一块令牌,条件失败说明其他线程修改了数量 + if (compareAndIncrementWorkerCount(c)) + // 申请成功,跳出了 retry 这个 for 自旋 + break retry; + // CAS 失败,没有成功的申请到令牌 + c = ctl.get(); + // 判断当前线程池状态是否发生过变化,被其他线程修改了,可能其他线程调用了 shutdown() 方法 + if (runStateOf(c) != rs) + // 返回外层循环检查是否能创建线程,在 if 语句中返回 false + continue retry; + + } + } + + //【令牌申请成功,开始创建线程】 + + // 运行标记,表示创建的 worker 是否已经启动,false未启动 true启动 + boolean workerStarted = false; + // 添加标记,表示创建的 worker 是否添加到池子中了,默认false未添加,true是添加。 + boolean workerAdded = false; + Worker w = null; + try { + // 【创建 Worker,底层通过线程工厂 newThread 方法创建执行线程,指定了首先执行的任务】 + w = new Worker(firstTask); + // 将新创建的 worker 节点中的线程赋值给 t + final Thread t = w.thread; + // 这里的判断为了防止 程序员自定义的 ThreadFactory 实现类有 bug,创造不出线程 + if (t != null) { + final ReentrantLock mainLock = this.mainLock; + // 加互斥锁,要添加 worker 了 + mainLock.lock(); + try { + // 获取最新线程池运行状态保存到 rs + int rs = runStateOf(ctl.get()); + // 判断线程池是否为RUNNING状态,不是再【判断当前是否为SHUTDOWN状态且firstTask为空,特殊情况】 + if (rs < SHUTDOWN || (rs == SHUTDOWN && firstTask == null)) { + // 当线程start后,线程isAlive会返回true,这里还没开始启动线程,如果被启动了就需要报错 + if (t.isAlive()) + throw new IllegalThreadStateException(); + + //【将新建的 Worker 添加到线程池中】 + workers.add(w); + int s = workers.size(); + // 当前池中的线程数量是一个新高,更新 largestPoolSize + if (s > largestPoolSize) + largestPoolSize = s; + // 添加标记置为 true + workerAdded = true; + } + } finally { + // 解锁啊 + mainLock.unlock(); + } + // 添加成功就【启动线程执行任务】 + if (workerAdded) { + // Thread 类中持有 Runnable 任务对象,调用的是 Runnable 的 run ,也就是 FutureTask + t.start(); + // 运行标记置为 true + workerStarted = true; + } + } + } finally { + // 如果启动线程失败,做清理工作 + if (! workerStarted) + addWorkerFailed(w); + } + // 返回新创建的线程是否启动 + return workerStarted; + } + ``` + +* addWorkerFailed():清理任务 + + ```java + private void addWorkerFailed(Worker w) { + final ReentrantLock mainLock = this.mainLock; + // 持有线程池全局锁,因为操作的是线程池相关的东西 + mainLock.lock(); + try { + //条件成立需要将 worker 在 workers 中清理出去。 + if (w != null) + workers.remove(w); + // 将线程池计数 -1,相当于归还令牌。 + decrementWorkerCount(); + // 尝试停止线程池 + tryTerminate(); + } finally { + //释放线程池全局锁。 + mainLock.unlock(); + } + } + ``` + +**** + +##### 运行方法 + +* Worker#run:Worker 实现了 Runnable 接口,当线程启动时,会调用 Worker 的 run() 方法 + + ```java + public void run() { + // ThreadPoolExecutor#runWorker() + runWorker(this); + } + ``` + +* runWorker():线程启动就要**执行任务**,会一直 while 循环获取任务并执行 + + ```java + final void runWorker(Worker w) { + Thread wt = Thread.currentThread(); + // 获取 worker 的 firstTask + Runnable task = w.firstTask; + // 引用置空,【防止复用该线程时重复执行该任务】 + w.firstTask = null; + // 初始化 worker 时设置 state = -1,表示不允许抢占锁 + // 这里需要设置 state = 0 和 exclusiveOwnerThread = null,开始独占模式抢锁 + w.unlock(); + // true 表示发生异常退出,false 表示正常退出。 + boolean completedAbruptly = true; + try { + // firstTask 不是 null 就直接运行,否则去 queue 中获取任务 + // 【getTask 如果是阻塞获取任务,会一直阻塞在take方法,直到获取任务,不会走返回null的逻辑】 + while (task != null || (task = getTask()) != null) { + // worker 加锁,shutdown 时会判断当前 worker 状态,【根据独占锁状态判断是否空闲】 + w.lock(); + + // 说明线程池状态大于 STOP,目前处于 STOP/TIDYING/TERMINATION,此时给线程一个中断信号 + if ((runStateAtLeast(ctl.get(), STOP) || + // 说明线程处于 RUNNING 或者 SHUTDOWN 状态,清除打断标记 + (Thread.interrupted() && runStateAtLeast(ctl.get(), STOP))) && !wt.isInterrupted()) + // 中断线程,设置线程的中断标志位为 true + wt.interrupt(); + try { + // 钩子方法,【任务执行的前置处理】 + beforeExecute(wt, task); + Throwable thrown = null; + try { + // 【执行任务】 + task.run(); + } catch (Exception x) { + //..... + } finally { + // 钩子方法,【任务执行的后置处理】 + afterExecute(task, thrown); + } + } finally { + task = null; // 将局部变量task置为null,代表任务执行完成 + w.completedTasks++; // 更新worker完成任务数量 + w.unlock(); // 解锁 + } + } + // getTask()方法返回null时会走到这里,表示queue为空并且线程空闲超过保活时间,【当前线程执行退出逻辑】 + completedAbruptly = false; + } finally { + // 正常退出 completedAbruptly = false + // 异常退出 completedAbruptly = true,【从 task.run() 内部抛出异常】时,跳到这一行 + processWorkerExit(w, completedAbruptly); + } + } + ``` + +* unlock():重置锁 + + ```java + public void unlock() { release(1); } + // 外部不会直接调用这个方法 这个方法是 AQS 内调用的,外部调用 unlock 时触发此方法 + protected boolean tryRelease(int unused) { + setExclusiveOwnerThread(null); // 设置持有者为 null + setState(0); // 设置 state = 0 + return true; + } + ``` + +* getTask():获取任务,线程空闲时间超过 keepAliveTime 就会被回收,判断的依据是**当前线程阻塞获取任务超过保活时间**,方法返回 null 就代表当前线程要被回收了,返回到 runWorker 执行线程退出逻辑。线程池具有担保机制,对于 RUNNING 状态下的超时回收,要保证线程池中最少有一个线程运行,或者任务阻塞队列已经是空 + + ```java + private Runnable getTask() { + // 超时标记,表示当前线程获取任务是否超时,true 表示已超时 + boolean timedOut = false; + for (;;) { + int c = ctl.get(); + // 获取线程池当前运行状态 + int rs = runStateOf(c); + + // 【tryTerminate】打断线程后执行到这,此时线程池状态为STOP或者线程池状态为SHUTDOWN并且队列已经是空 + // 所以下面的 if 条件一定是成立的,可以直接返回 null,线程就应该退出了 + if (rs >= SHUTDOWN && (rs >= STOP || workQueue.isEmpty())) { + // 使用 CAS 自旋的方式让 ctl 值 -1 + decrementWorkerCount(); + return null; + } + + // 获取线程池中的线程数量 + int wc = workerCountOf(c); + + // 线程没有明确的区分谁是核心或者非核心线程,是根据当前池中的线程数量判断 + + // timed = false 表示当前这个线程 获取task时不支持超时机制的,当前线程会使用 queue.take() 阻塞获取 + // timed = true 表示当前这个线程 获取task时支持超时机制,使用 queue.poll(xxx,xxx) 超时获取 + // 条件一代表允许回收核心线程,那就无所谓了,全部线程都执行超时回收 + // 条件二成立说明线程数量大于核心线程数,当前线程认为是非核心线程,有保活时间,去超时获取任务 + boolean timed = allowCoreThreadTimeOut || wc > corePoolSize; + + // 如果线程数量是否超过最大线程数,直接回收 + // 如果当前线程【允许超时回收并且已经超时了】,就应该被回收了,由于【担保机制】还要做判断: + // wc > 1 说明线程池还用其他线程,当前线程可以直接回收 + // workQueue.isEmpty() 前置条件是 wc = 1,【如果当前任务队列也是空了,最后一个线程就可以退出】 + if ((wc > maximumPoolSize || (timed && timedOut)) && (wc > 1 || workQueue.isEmpty())) { + // 使用 CAS 机制将 ctl 值 -1 ,减 1 成功的线程,返回 null,代表可以退出 + if (compareAndDecrementWorkerCount(c)) + return null; + continue; + } + + try { + // 根据当前线程是否需要超时回收,【选择从队列获取任务的方法】是超时获取或者阻塞获取 + Runnable r = timed ? + workQueue.poll(keepAliveTime, TimeUnit.NANOSECONDS) : workQueue.take(); + // 获取到任务返回任务,【阻塞获取会阻塞到获取任务为止】,不会返回 null + if (r != null) + return r; + // 获取任务为 null 说明超时了,将超时标记设置为 true,下次自旋时返 null + timedOut = true; + } catch (InterruptedException retry) { + // 阻塞线程被打断后超时标记置为 false,【说明被打断不算超时】,要继续获取,直到超时或者获取到任务 + // 如果线程池 SHUTDOWN 状态下的打断,会在循环获取任务前判断,返回 null + timedOut = false; + } + } + } + ``` + +* processWorkerExit():**线程退出线程池**,也有担保机制,保证队列中的任务被执行 + + ```java + // 正常退出 completedAbruptly = false,异常退出为 true + private void processWorkerExit(Worker w, boolean completedAbruptly) { + // 条件成立代表当前 worker 是发生异常退出的,task 任务执行过程中向上抛出异常了 + if (completedAbruptly) + // 从异常时到这里 ctl 一直没有 -1,需要在这里 -1 + decrementWorkerCount(); + + final ReentrantLock mainLock = this.mainLock; + // 加锁 + mainLock.lock(); + try { + // 将当前 worker 完成的 task 数量,汇总到线程池的 completedTaskCount + completedTaskCount += w.completedTasks; + // 将 worker 从线程池中移除 + workers.remove(w); + } finally { + mainLock.unlock(); // 解锁 + } + // 尝试停止线程池,唤醒下一个线程 + tryTerminate(); + + int c = ctl.get(); + // 线程池不是停止状态就应该有线程运行【担保机制】 + if (runStateLessThan(c, STOP)) { + // 正常退出的逻辑,是对空闲线程回收,不是执行出错 + if (!completedAbruptly) { + // 根据是否回收核心线程确定【线程池中的线程数量最小值】 + int min = allowCoreThreadTimeOut ? 0 : corePoolSize; + // 最小值为 0,但是线程队列不为空,需要一个线程来完成任务担保机制 + if (min == 0 && !workQueue.isEmpty()) + min = 1; + // 线程池中的线程数量大于最小值可以直接返回 + if (workerCountOf(c) >= min) + return; + } + // 执行 task 时发生异常,有个线程因为异常终止了,需要添加 + // 或者线程池中的数量小于最小值,这里要创建一个新 worker 加进线程池 + addWorker(null, false); + } + } + ``` + +**** + +##### 停止方法 + +* shutdown():停止线程池 + + ```java + public void shutdown() { + final ReentrantLock mainLock = this.mainLock; + // 获取线程池全局锁 + mainLock.lock(); + try { + checkShutdownAccess(); + // 设置线程池状态为 SHUTDOWN,如果线程池状态大于 SHUTDOWN,就不会设置直接返回 + advanceRunState(SHUTDOWN); + // 中断空闲线程 + interruptIdleWorkers(); + // 空方法,子类可以扩展 + onShutdown(); + } finally { + // 释放线程池全局锁 + mainLock.unlock(); + } + tryTerminate(); + } + ``` + +* interruptIdleWorkers():shutdown 方法会**中断所有空闲线程**,根据是否可以获取 AQS 独占锁判断是否处于工作状态。线程之所以空闲是因为阻塞队列没有任务,不会中断正在运行的线程,所以 shutdown 方法会让所有的任务执行完毕 + + ```java + // onlyOne == true 说明只中断一个线程 ,false 则中断所有线程 + private void interruptIdleWorkers(boolean onlyOne) { + final ReentrantLock mainLock = this.mainLock; + / /持有全局锁 + mainLock.lock(); + try { + // 遍历所有 worker + for (Worker w : workers) { + // 获取当前 worker 的线程 + Thread t = w.thread; + // 条件一成立:说明当前迭代的这个线程尚未中断 + // 条件二成立:说明【当前worker处于空闲状态】,阻塞在poll或者take,因为worker执行task时是要加锁的 + // 每个worker有一个独占锁,w.tryLock()尝试加锁,加锁成功返回 true + if (!t.isInterrupted() && w.tryLock()) { + try { + // 中断线程,处于 queue 阻塞的线程会被唤醒,进入下一次自旋,返回 null,执行退出相逻辑 + t.interrupt(); + } catch (SecurityException ignore) { + } finally { + // 释放worker的独占锁 + w.unlock(); + } + } + // false,代表中断所有的线程 + if (onlyOne) + break; + } + + } finally { + // 释放全局锁 + mainLock.unlock(); + } + } + ``` + +* shutdownNow():直接关闭线程池,不会等待任务执行完成 + + ```java + public List shutdownNow() { + // 返回值引用 + List tasks; + final ReentrantLock mainLock = this.mainLock; + // 获取线程池全局锁 + mainLock.lock(); + try { + checkShutdownAccess(); + // 设置线程池状态为STOP + advanceRunState(STOP); + // 中断线程池中【所有线程】 + interruptWorkers(); + // 从阻塞队列中导出未处理的task + tasks = drainQueue(); + } finally { + mainLock.unlock(); + } + + tryTerminate(); + // 返回当前任务队列中 未处理的任务。 + return tasks; + } + ``` + +* tryTerminate():设置为 TERMINATED 状态 if either (SHUTDOWN and pool and queue empty) or (STOP and pool empty) + + ```java + final void tryTerminate() { + for (;;) { + // 获取 ctl 的值 + int c = ctl.get(); + // 线程池正常,或者有其他线程执行了状态转换的方法,当前线程直接返回 + if (isRunning(c) || runStateAtLeast(c, TIDYING) || + // 线程池是 SHUTDOWN 并且任务队列不是空,需要去处理队列中的任务 + (runStateOf(c) == SHUTDOWN && ! workQueue.isEmpty())) + return; + + // 执行到这里说明线程池状态为 STOP 或者线程池状态为 SHUTDOWN 并且队列已经是空 + // 判断线程池中线程的数量 + if (workerCountOf(c) != 0) { + // 【中断一个空闲线程】,在 queue.take() | queue.poll() 阻塞空闲 + // 唤醒后的线程会在getTask()方法返回null, + // 执行 processWorkerExit 退出逻辑时会再次调用 tryTerminate() 唤醒下一个空闲线程 + interruptIdleWorkers(ONLY_ONE); + return; + } + // 池中的线程数量为 0 来到这里 + final ReentrantLock mainLock = this.mainLock; + // 加全局锁 + mainLock.lock(); + try { + // 设置线程池状态为 TIDYING 状态,线程数量为 0 + if (ctl.compareAndSet(c, ctlOf(TIDYING, 0))) { + try { + // 结束线程池 + terminated(); + } finally { + // 设置线程池状态为TERMINATED状态。 + ctl.set(ctlOf(TERMINATED, 0)); + // 【唤醒所有调用 awaitTermination() 方法的线程】 + termination.signalAll(); + } + return; + } + } finally { + // 释放线程池全局锁 + mainLock.unlock(); + } + } + } + ``` + +**** + +#### Future + +##### 线程使用 + +FutureTask 未来任务对象,继承 Runnable、Future 接口,用于包装 Callable 对象,实现任务的提交 + +```java +public static void main(String[] args) throws ExecutionException, InterruptedException { + FutureTask task = new FutureTask<>(new Callable() { + @Override + public String call() throws Exception { + return "Hello World"; + } + }); + new Thread(task).start(); //启动线程 + String msg = task.get(); //获取返回任务数据 + System.out.println(msg); +} +``` + +构造方法: + +```java +public FutureTask(Callable callable){ + this.callable = callable; // 属性注入 + this.state = NEW; // 任务状态设置为 new +} +``` + +```java +public FutureTask(Runnable runnable, V result) { + // 适配器模式 + this.callable = Executors.callable(runnable, result); + this.state = NEW; +} +public static Callable callable(Runnable task, T result) { + if (task == null) throw new NullPointerException(); + // 使用装饰者模式将 runnable 转换成 callable 接口,外部线程通过 get 获取 + // 当前任务执行结果时,结果可能为 null 也可能为传进来的值,【传进来什么返回什么】 + return new RunnableAdapter(task, result); +} +static final class RunnableAdapter implements Callable { + final Runnable task; + final T result; + // 构造方法 + RunnableAdapter(Runnable task, T result) { + this.task = task; + this.result = result; + } + public T call() { + // 实则调用 Runnable#run 方法 + task.run(); + // 返回值为构造 FutureTask 对象时传入的返回值或者是 null + return result; + } +} +``` + +*** + +##### 成员属性 + +FutureTask 类的成员属性: + +* 任务状态: + + ```java + // 表示当前task状态 + private volatile int state; + // 当前任务尚未执行 + private static final int NEW = 0; + // 当前任务正在结束,尚未完全结束,一种临界状态 + private static final int COMPLETING = 1; + // 当前任务正常结束 + private static final int NORMAL = 2; + // 当前任务执行过程中发生了异常,内部封装的 callable.run() 向上抛出异常了 + private static final int EXCEPTIONAL = 3; + // 当前任务被取消 + private static final int CANCELLED = 4; + // 当前任务中断中 + private static final int INTERRUPTING = 5; + // 当前任务已中断 + private static final int INTERRUPTED = 6; + ``` + +* 任务对象: + + ```java + private Callable callable; // Runnable 使用装饰者模式伪装成 Callable + ``` + +* **存储任务执行的结果**,这是 run 方法返回值是 void 也可以获取到执行结果的原因: + + ```java + // 正常情况下:任务正常执行结束,outcome 保存执行结果,callable 返回值 + // 非正常情况:callable 向上抛出异常,outcome 保存异常 + private Object outcome; + ``` + +* 执行当前任务的线程对象: + + ```java + private volatile Thread runner; // 当前任务被线程执行期间,保存当前执行任务的线程对象引用 + ``` + +* **线程阻塞队列的头节点**: + + ```java + // 会有很多线程去 get 当前任务的结果,这里使用了一种数据结构头插头取(类似栈)的一个队列来保存所有的 get 线程 + private volatile WaitNode waiters; + ``` + +* 内部类: + + ```java + static final class WaitNode { + // 单向链表 + volatile Thread thread; + volatile WaitNode next; + WaitNode() { thread = Thread.currentThread(); } + } + ``` + +*** + +##### 成员方法 + +FutureTask 类的成员方法: + +* **FutureTask#run**:任务执行入口 + + ```java + public void run() { + //条件一:成立说明当前 task 已经被执行过了或者被 cancel 了,非 NEW 状态的任务,线程就不需要处理了 + //条件二:线程是 NEW 状态,尝试设置当前任务对象的线程是当前线程,设置失败说明其他线程抢占了该任务,直接返回 + if (state != NEW || + !UNSAFE.compareAndSwapObject(this, runnerOffset, null, Thread.currentThread())) + return; + try { + // 执行到这里,当前 task 一定是 NEW 状态,而且【当前线程也抢占 task 成功】 + Callable c = callable; + // 判断任务是否为空,防止空指针异常;判断 state 状态,防止外部线程在此期间 cancel 掉当前任务 + // 【因为 task 的执行者已经设置为当前线程,所以这里是线程安全的】 + if (c != null && state == NEW) { + V result; + // true 表示 callable.run 代码块执行成功 未抛出异常 + // false 表示 callable.run 代码块执行失败 抛出异常 + boolean ran; + try { + // 【调用自定义的方法,执行结果赋值给 result】 + result = c.call(); + // 没有出现异常 + ran = true; + } catch (Throwable ex) { + // 出现异常,返回值置空,ran 置为 false + result = null; + ran = false; + // 设置返回的异常 + setException(ex); + } + // 代码块执行正常 + if (ran) + // 设置返回的结果 + set(result); + } + } finally { + // 任务执行完成,取消线程的引用,help GC + runner = null; + int s = state; + // 判断任务是不是被中断 + if (s >= INTERRUPTING) + // 执行中断处理方法 + handlePossibleCancellationInterrupt(s); + } + } + ``` + + FutureTask#set:设置正常返回值,首先将任务状态设置为 COMPLETING 状态代表完成中,逻辑执行完设置为 NORMAL 状态代表任务正常执行完成,最后唤醒 get() 阻塞线程 + + ```java + protected void set(V v) { + // CAS 方式设置当前任务状态为完成中,设置失败说明其他线程取消了该任务 + if (UNSAFE.compareAndSwapInt(this, stateOffset, NEW, COMPLETING)) { + // 【将结果赋值给 outcome】 + outcome = v; + // 将当前任务状态修改为 NORMAL 正常结束状态。 + UNSAFE.putOrderedInt(this, stateOffset, NORMAL); + finishCompletion(); + } + } + ``` + + FutureTask#setException:设置异常返回值 + + ```java + protected void setException(Throwable t) { + if (UNSAFE.compareAndSwapInt(this, stateOffset, NEW, COMPLETING)) { + // 赋值给返回结果,用来向上层抛出来的异常 + outcome = t; + // 将当前任务的状态 修改为 EXCEPTIONAL + UNSAFE.putOrderedInt(this, stateOffset, EXCEPTIONAL); + finishCompletion(); + } + } + ``` + + FutureTask#finishCompletion:**唤醒 get() 阻塞线程** + + ```java + private void finishCompletion() { + // 遍历所有的等待的节点,q 指向头节点 + for (WaitNode q; (q = waiters) != null;) { + // 使用cas设置 waiters 为 null,防止外部线程使用cancel取消当前任务,触发finishCompletion方法重复执行 + if (UNSAFE.compareAndSwapObject(this, waitersOffset, q, null)) { + // 自旋 + for (;;) { + // 获取当前 WaitNode 节点封装的 thread + Thread t = q.thread; + // 当前线程不为 null,唤醒当前 get() 等待获取数据的线程 + if (t != null) { + q.thread = null; + LockSupport.unpark(t); + } + // 获取当前节点的下一个节点 + WaitNode next = q.next; + // 当前节点是最后一个节点了 + if (next == null) + break; + // 断开链表 + q.next = null; // help gc + q = next; + } + break; + } + } + done(); + callable = null; // help GC + } + ``` + + FutureTask#handlePossibleCancellationInterrupt:任务中断处理 + + ```java + private void handlePossibleCancellationInterrupt(int s) { + if (s == INTERRUPTING) + // 中断状态中 + while (state == INTERRUPTING) + // 等待中断完成 + Thread.yield(); + } + ``` + +* **FutureTask#get**:获取任务执行的返回值,执行 run 和 get 的不是同一个线程,一般有多个线程 get,只有一个线程 run + + ```java + public V get() throws InterruptedException, ExecutionException { + // 获取当前任务状态 + int s = state; + // 条件成立说明任务还没执行完成 + if (s <= COMPLETING) + // 返回 task 当前状态,可能当前线程在里面已经睡了一会 + s = awaitDone(false, 0L); + return report(s); + } + ``` + + FutureTask#awaitDone:**get 线程封装成 WaitNode 对象进入阻塞队列阻塞等待** + + ```java + private int awaitDone(boolean timed, long nanos) throws InterruptedException { + // 0 不带超时 + final long deadline = timed ? System.nanoTime() + nanos : 0L; + // 引用当前线程,封装成 WaitNode 对象 + WaitNode q = null; + // 表示当前线程 waitNode 对象,是否进入阻塞队列 + boolean queued = false; + // 【三次自旋开始休眠】 + for (;;) { + // 判断当前 get() 线程是否被打断,打断返回 true,清除打断标记 + if (Thread.interrupted()) { + // 当前线程对应的等待 node 出队, + removeWaiter(q); + throw new InterruptedException(); + } + // 获取任务状态 + int s = state; + // 条件成立说明当前任务执行完成已经有结果了 + if (s > COMPLETING) { + // 条件成立说明已经为当前线程创建了 WaitNode,置空 help GC + if (q != null) + q.thread = null; + // 返回当前的状态 + return s; + } + // 条件成立说明当前任务接近完成状态,这里让当前线程释放一下 cpu ,等待进行下一次抢占 cpu + else if (s == COMPLETING) + Thread.yield(); + // 【第一次自旋】,当前线程还未创建 WaitNode 对象,此时为当前线程创建 WaitNode对象 + else if (q == null) + q = new WaitNode(); + // 【第二次自旋】,当前线程已经创建 WaitNode 对象了,但是node对象还未入队 + else if (!queued) + // waiters 指向队首,让当前 WaitNode 成为新的队首,【头插法】,失败说明其他线程修改了新的队首 + queued = UNSAFE.compareAndSwapObject(this, waitersOffset, q.next = waiters, q); + // 【第三次自旋】,会到这里,或者 else 内 + else if (timed) { + nanos = deadline - System.nanoTime(); + if (nanos <= 0L) { + removeWaiter(q); + return state; + } + // 阻塞指定的时间 + LockSupport.parkNanos(this, nanos); + } + // 条件成立:说明需要阻塞 + else + // 【当前 get 操作的线程被 park 阻塞】,除非有其它线程将唤醒或者将当前线程中断 + LockSupport.park(this); + } + } + ``` + + FutureTask#report:封装运行结果,可以获取 run() 方法中设置的成员变量 outcome,**这是 run 方法的返回值是 void 也可以获取到任务执行的结果的原因** + + ```java + private V report(int s) throws ExecutionException { + // 获取执行结果,是在一个 futuretask 对象中的属性,可以直接获取 + Object x = outcome; + // 当前任务状态正常结束 + if (s == NORMAL) + return (V)x; // 直接返回 callable 的逻辑结果 + // 当前任务被取消或者中断 + if (s >= CANCELLED) + throw new CancellationException(); // 抛出异常 + // 执行到这里说明自定义的 callable 中的方法有异常,使用 outcome 上层抛出异常 + throw new ExecutionException((Throwable)x); + } + ``` + +* FutureTask#cancel:任务取消,打断正在执行该任务的线程 + + ```java + public boolean cancel(boolean mayInterruptIfRunning) { + // 条件一:表示当前任务处于运行中或者处于线程池任务队列中 + // 条件二:表示修改状态,成功可以去执行下面逻辑,否则返回 false 表示 cancel 失败 + if (!(state == NEW && + UNSAFE.compareAndSwapInt(this, stateOffset, NEW, + mayInterruptIfRunning ? INTERRUPTING : CANCELLED))) + return false; + try { + // 如果任务已经被执行,是否允许打断 + if (mayInterruptIfRunning) { + try { + // 获取执行当前 FutureTask 的线程 + Thread t = runner; + if (t != null) + // 打断执行的线程 + t.interrupt(); + } finally { + // 设置任务状态为【中断完成】 + UNSAFE.putOrderedInt(this, stateOffset, INTERRUPTED); + } + } + } finally { + // 唤醒所有 get() 阻塞的线程 + finishCompletion(); + } + return true; + } + ``` + +**** + +### 任务调度 + +#### Timer + +Timer 实现定时功能,Timer 的优点在于简单易用,但由于所有任务都是由同一个线程来调度,因此所有任务都是串行执行的,同一时间只能有一个任务在执行,前一个任务的延迟或异常都将会影响到之后的任务 + +```java +private static void method1() { + Timer timer = new Timer(); + TimerTask task1 = new TimerTask() { + @Override + public void run() { + System.out.println("task 1"); + //int i = 1 / 0;//任务一的出错会导致任务二无法执行 + Thread.sleep(2000); + } + }; + TimerTask task2 = new TimerTask() { + @Override + public void run() { + System.out.println("task 2"); + } + }; + // 使用 timer 添加两个任务,希望它们都在 1s 后执行 + // 但由于 timer 内只有一个线程来顺序执行队列中的任务,因此任务1的延时,影响了任务2的执行 + timer.schedule(task1, 1000);//17:45:56 c.ThreadPool [Timer-0] - task 1 + timer.schedule(task2, 1000);//17:45:58 c.ThreadPool [Timer-0] - task 2 +} +``` + +*** + +#### Scheduled + +任务调度线程池 ScheduledThreadPoolExecutor 继承 ThreadPoolExecutor: + +* 使用内部类 ScheduledFutureTask 封装任务 +* 使用内部类 DelayedWorkQueue 作为线程池队列 +* 重写 onShutdown 方法去处理 shutdown 后的任务 +* 提供 decorateTask 方法作为 ScheduledFutureTask 的修饰方法,以便开发者进行扩展 + +构造方法:`Executors.newScheduledThreadPool(int corePoolSize)` + +```java +public ScheduledThreadPoolExecutor(int corePoolSize) { + // 最大线程数固定为 Integer.MAX_VALUE,保活时间 keepAliveTime 固定为 0 + super(corePoolSize, Integer.MAX_VALUE, 0, NANOSECONDS, + // 阻塞队列是 DelayedWorkQueue + new DelayedWorkQueue()); +} +``` + +常用 API: + +* `ScheduledFuture schedule(Runnable/Callable, long delay, TimeUnit u)`:延迟执行任务 +* `ScheduledFuture scheduleAtFixedRate(Runnable/Callable, long initialDelay, long period, TimeUnit unit)`:定时执行周期任务,不考虑执行的耗时,参数为初始延迟时间、间隔时间、单位 +* `ScheduledFuture scheduleWithFixedDelay(Runnable/Callable, long initialDelay, long delay, TimeUnit unit)`:定时执行周期任务,考虑执行的耗时,参数为初始延迟时间、间隔时间、单位 + +基本使用: + +* 延迟任务,但是出现异常并不会在控制台打印,也不会影响其他线程的执行 + + ```java + public static void main(String[] args){ + // 线程池大小为1时也是串行执行 + ScheduledExecutorService executor = Executors.newScheduledThreadPool(2); + // 添加两个任务,都在 1s 后同时执行 + executor.schedule(() -> { + System.out.println("任务1,执行时间:" + new Date()); + //int i = 1 / 0; + try { Thread.sleep(2000); } catch (InterruptedException e) { } + }, 1000, TimeUnit.MILLISECONDS); + + executor.schedule(() -> { + System.out.println("任务2,执行时间:" + new Date()); + }, 1000, TimeUnit.MILLISECONDS); + } + ``` + +* 定时任务 scheduleAtFixedRate:**一次任务的启动到下一次任务的启动**之间只要大于等于间隔时间,抢占到 CPU 就会立即执行 + + ```java + public static void main(String[] args) { + ScheduledExecutorService pool = Executors.newScheduledThreadPool(1); + System.out.println("start..." + new Date()); + + pool.scheduleAtFixedRate(() -> { + System.out.println("running..." + new Date()); + Thread.sleep(2000); + }, 1, 1, TimeUnit.SECONDS); + } + + /*start...Sat Apr 24 18:08:12 CST 2021 + running...Sat Apr 24 18:08:13 CST 2021 + running...Sat Apr 24 18:08:15 CST 2021 + running...Sat Apr 24 18:08:17 CST 2021 + ``` + +* 定时任务 scheduleWithFixedDelay:**一次任务的结束到下一次任务的启动之间**等于间隔时间,抢占到 CPU 就会立即执行,这个方法才是真正的设置两个任务之间的间隔 + + ```java + public static void main(String[] args){ + ScheduledExecutorService pool = Executors.newScheduledThreadPool(3); + System.out.println("start..." + new Date()); + + pool.scheduleWithFixedDelay(() -> { + System.out.println("running..." + new Date()); + Thread.sleep(2000); + }, 1, 1, TimeUnit.SECONDS); + } + /*start...Sat Apr 24 18:11:41 CST 2021 + running...Sat Apr 24 18:11:42 CST 2021 + running...Sat Apr 24 18:11:45 CST 2021 + running...Sat Apr 24 18:11:48 CST 2021 + ``` + +*** + +#### 成员属性 + +##### 成员变量 + +* shutdown 后是否继续执行周期任务: + + ```java + private volatile boolean continueExistingPeriodicTasksAfterShutdown; + ``` + +* shutdown 后是否继续执行延迟任务: + + ```java + private volatile boolean executeExistingDelayedTasksAfterShutdown = true; + ``` + +* 取消方法是否将该任务从队列中移除: + + ```java + // 默认 false,不移除,等到线程拿到任务之后抛弃 + private volatile boolean removeOnCancel = false; + ``` + +* 任务的序列号,可以用来比较优先级: + + ```java + private static final AtomicLong sequencer = new AtomicLong(); + ``` + +*** + +##### 延迟任务 + +ScheduledFutureTask 继承 FutureTask,实现 RunnableScheduledFuture 接口,具有延迟执行的特点,覆盖 FutureTask 的 run 方法来实现对**延时执行、周期执行**的支持。对于延时任务调用 FutureTask#run,而对于周期性任务则调用 FutureTask#runAndReset 并且在成功之后根据 fixed-delay/fixed-rate 模式来设置下次执行时间并重新将任务塞到工作队列 + +在调度线程池中无论是 runnable 还是 callable,无论是否需要延迟和定时,所有的任务都会被封装成 ScheduledFutureTask + +成员变量: + +* 任务序列号: + + ```java + private final long sequenceNumber; + ``` + +* 执行时间: + + ```java + private long time; // 任务可以被执行的时间,交付时间,以纳秒表示 + private final long period; // 0 表示非周期任务,正数表示 fixed-rate 模式的周期,负数表示 fixed-delay 模式 + ``` + + fixed-rate:两次开始启动的间隔,fixed-delay:一次执行结束到下一次开始启动 + +* 实际的任务对象: + + ```java + RunnableScheduledFuture outerTask = this; + ``` + +* 任务在队列数组中的索引下标: + + ```java + // DelayedWorkQueue 底层使用的数据结构是最小堆,记录当前任务在堆中的索引,-1 代表删除 + int heapIndex; + ``` + +成员方法: + +* 构造方法: + + ```java + ScheduledFutureTask(Runnable r, V result, long ns, long period) { + super(r, result); + // 任务的触发时间 + this.time = ns; + // 任务的周期,多长时间执行一次 + this.period = period; + // 任务的序号 + this.sequenceNumber = sequencer.getAndIncrement(); + } + ``` + +* compareTo():ScheduledFutureTask 根据执行时间 time 正序排列,如果执行时间相同,在按照序列号 sequenceNumber 正序排列,任务需要放入 DelayedWorkQueue,延迟队列中使用该方法按照从小到大进行排序 + + ```java + public int compareTo(Delayed other) { + if (other == this) // compare zero if same object + return 0; + if (other instanceof ScheduledFutureTask) { + // 类型强转 + ScheduledFutureTask x = (ScheduledFutureTask)other; + // 比较者 - 被比较者的执行时间 + long diff = time - x.time; + // 比较者先执行 + if (diff < 0) + return -1; + // 被比较者先执行 + else if (diff > 0) + return 1; + // 比较者的序列号小 + else if (sequenceNumber < x.sequenceNumber) + return -1; + else + return 1; + } + // 不是 ScheduledFutureTask 类型时,根据延迟时间排序 + long diff = getDelay(NANOSECONDS) - other.getDelay(NANOSECONDS); + return (diff < 0) ? -1 : (diff > 0) ? 1 : 0; + } + ``` + +* run():执行任务,非周期任务直接完成直接结束,**周期任务执行完后会设置下一次的执行时间,重新放入线程池的阻塞队列**,如果线程池中的线程数量少于核心线程,就会添加 Worker 开启新线程 + + ```java + public void run() { + // 是否周期性,就是判断 period 是否为 0 + boolean periodic = isPeriodic(); + // 根据是否是周期任务检查当前状态能否执行任务,不能执行就取消任务 + if (!canRunInCurrentRunState(periodic)) + cancel(false); + // 非周期任务,直接调用 FutureTask#run 执行 + else if (!periodic) + ScheduledFutureTask.super.run(); + // 周期任务的执行,返回 true 表示执行成功 + else if (ScheduledFutureTask.super.runAndReset()) { + // 设置周期任务的下一次执行时间 + setNextRunTime(); + // 任务的下一次执行安排,如果当前线程池状态可以执行周期任务,加入队列,并开启新线程 + reExecutePeriodic(outerTask); + } + } + ``` + + 周期任务正常完成后**任务的状态不会变化**,依旧是 NEW,不会设置 outcome 属性。但是如果本次任务执行出现异常,会进入 setException 方法将任务状态置为异常,把异常保存在 outcome 中,方法返回 false,后续的该任务将不会再周期的执行 + + ```java + protected boolean runAndReset() { + // 任务不是新建的状态了,或者被别的线程执行了,直接返回 false + if (state != NEW || + !UNSAFE.compareAndSwapObject(this, runnerOffset, null, Thread.currentThread())) + return false; + boolean ran = false; + int s = state; + try { + Callable c = callable; + if (c != null && s == NEW) { + try { + // 执行方法,没有返回值 + c.call(); + ran = true; + } catch (Throwable ex) { + // 出现异常,把任务设置为异常状态,唤醒所有的 get 阻塞线程 + setException(ex); + } + } + } finally { + // 执行完成把执行线程引用置为 null + runner = null; + s = state; + // 如果线程被中断进行中断处理 + if (s >= INTERRUPTING) + handlePossibleCancellationInterrupt(s); + } + // 如果正常执行,返回 true,并且任务状态没有被取消 + return ran && s == NEW; + } + ``` + + ```java + // 任务下一次的触发时间 + private void setNextRunTime() { + long p = period; + if (p > 0) + // fixed-rate 模式,【时间设置为上一次执行任务的时间 + p】,两次任务执行的时间差 + time += p; + else + // fixed-delay 模式,下一次执行时间是【当前这次任务结束的时间(就是现在) + delay 值】 + time = triggerTime(-p); + } + ``` + +* reExecutePeriodic()**:准备任务的下一次执行,重新放入阻塞任务队列** + + ```java + // ScheduledThreadPoolExecutor#reExecutePeriodic + void reExecutePeriodic(RunnableScheduledFuture task) { + if (canRunInCurrentRunState(true)) { + // 【放入任务队列】 + super.getQueue().add(task); + // 如果提交完任务之后,线程池状态变为了 shutdown 状态,需要再次检查是否可以执行, + // 如果不能执行且任务还在队列中未被取走,则取消任务 + if (!canRunInCurrentRunState(true) && remove(task)) + task.cancel(false); + else + // 当前线程池状态可以执行周期任务,加入队列,并【根据线程数量是否大于核心线程数确定是否开启新线程】 + ensurePrestart(); + } + } + ``` + +* cancel():取消任务 + + ```java + public boolean cancel(boolean mayInterruptIfRunning) { + // 调用父类 FutureTask#cancel 来取消任务 + boolean cancelled = super.cancel(mayInterruptIfRunning); + // removeOnCancel 用于控制任务取消后是否应该从阻塞队列中移除 + if (cancelled && removeOnCancel && heapIndex >= 0) + // 从等待队列中删除该任务,并调用 tryTerminate() 判断是否需要停止线程池 + remove(this); + return cancelled; + } + ``` + +*** + +##### 延迟队列 + +DelayedWorkQueue 是支持延时获取元素的阻塞队列,内部采用优先队列 PriorityQueue(小根堆、满二叉树)存储元素 + +其他阻塞队列存储节点的数据结构大都是链表,**延迟队列是数组**,所以延迟队列出队头元素后需要**让其他元素(尾)替换到头节点**,防止空指针异常 + +成员变量: + +* 容量: + + ```java + private static final int INITIAL_CAPACITY = 16; // 初始容量 + private int size = 0; // 节点数量 + private RunnableScheduledFuture[] queue = + new RunnableScheduledFuture[INITIAL_CAPACITY]; // 存放节点 + ``` + +* 锁: + + ```java + private final ReentrantLock lock = new ReentrantLock(); // 控制并发 + private final Condition available = lock.newCondition();// 条件队列 + ``` + +* 阻塞等待头节点的线程:线程池内的某个线程去 take() 获取任务时,如果延迟队列顶层节点不为 null(队列内有任务),但是节点任务还不到触发时间,线程就去检查**队列的 leader字段**是否被占用 + + * 如果未被占用,则当前线程占用该字段,然后当前线程到 available 条件队列指定超时时间 `堆顶任务.time - now()` 挂起 + * 如果被占用,当前线程直接到 available 条件队列不指定超时时间的挂起 + + ```java + // leader 在 available 条件队列内是首元素,它超时之后会醒过来,然后再次将堆顶元素获取走,获取走之后,take()结束之前,会调用是 available.signal() 唤醒下一个条件队列内的等待者,然后释放 lock,下一个等待者被唤醒后去到 AQS 队列,做 acquireQueue(node) 逻辑 + private Thread leader = null; + ``` + +成员方法 + +* offer():插入节点 + + ```java + public boolean offer(Runnable x) { + // 判空 + if (x == null) + throw new NullPointerException(); + RunnableScheduledFuture e = (RunnableScheduledFuture)x; + // 队列锁,增加删除数据时都要加锁 + final ReentrantLock lock = this.lock; + lock.lock(); + try { + int i = size; + // 队列数量大于存放节点的数组长度,需要扩容 + if (i >= queue.length) + // 扩容为原来长度的 1.5 倍 + grow(); + size = i + 1; + // 当前是第一个要插入的节点 + if (i == 0) { + queue[0] = e; + // 修改 ScheduledFutureTask 的 heapIndex 属性,表示该对象在队列里的下标 + setIndex(e, 0); + } else { + // 向上调整元素的位置,并更新 heapIndex + siftUp(i, e); + } + // 情况1:当前任务是第一个加入到 queue 内的任务,所以在当前任务加入到 queue 之前,take() 线程会直接 + // 到 available 队列不设置超时的挂起,并不会去占用 leader 字段,这时需会唤醒一个线程 让它去消费 + // 情况2:当前任务【优先级最高】,原堆顶任务可能还未到触发时间,leader 线程设置超时的在 available 挂起 + // 原先的 leader 等待的是原先的头节点,所以 leader 已经无效,需要将 leader 线程唤醒, + // 唤醒之后它会检查堆顶,如果堆顶任务可以被消费,则直接获取走,否则继续成为 leader 等待新堆顶任务 + if (queue[0] == e) { + // 将 leader 设置为 null + leader = null; + // 直接随便唤醒等待头结点的阻塞线程 + available.signal(); + } + } finally { + lock.unlock(); + } + return true; + } + ``` + + ```java + // 插入新节点后对堆进行调整,进行节点上移,保持其特性【节点的值小于子节点的值】,小顶堆 + private void siftUp(int k, RunnableScheduledFuture key) { + while (k > 0) { + // 父节点,就是堆排序 + int parent = (k - 1) >>> 1; + RunnableScheduledFuture e = queue[parent]; + // key 和父节点比,如果大于父节点可以直接返回,否则就继续上浮 + if (key.compareTo(e) >= 0) + break; + queue[k] = e; + setIndex(e, k); + k = parent; + } + queue[k] = key; + setIndex(key, k); + } + ``` + +* poll():非阻塞获取头结点,**获取执行时间最近并且可以执行的** + + ```java + // 非阻塞获取 + public RunnableScheduledFuture poll() { + final ReentrantLock lock = this.lock; + lock.lock(); + try { + // 获取队头节点,因为是小顶堆 + RunnableScheduledFuture first = queue[0]; + // 头结点为空或者的延迟时间没到返回 null + if (first == null || first.getDelay(NANOSECONDS) > 0) + return null; + else + // 头结点达到延迟时间,【尾节点成为替代节点下移调整堆结构】,返回头结点 + return finishPoll(first); + } finally { + lock.unlock(); + } + } + ``` + + ```java + private RunnableScheduledFuture finishPoll(RunnableScheduledFuture f) { + // 获取尾索引 + int s = --size; + // 获取尾节点 + RunnableScheduledFuture x = queue[s]; + // 将堆结构最后一个节点占用的 slot 设置为 null,因为该节点要尝试升级成堆顶,会根据特性下调 + queue[s] = null; + // s == 0 说明 当前堆结构只有堆顶一个节点,此时不需要做任何的事情 + if (s != 0) + // 从索引处 0 开始向下调整 + siftDown(0, x); + // 出队的元素索引设置为 -1 + setIndex(f, -1); + return f; + } + ``` + +* take():阻塞获取头节点,读取当前堆中最小的也就是触发时间最近的任务 + + ```java + public RunnableScheduledFuture take() throws InterruptedException { + final ReentrantLock lock = this.lock; + // 保证线程安全 + lock.lockInterruptibly(); + try { + for (;;) { + // 头节点 + RunnableScheduledFuture first = queue[0]; + if (first == null) + // 等待队列不空,直至有任务通过 offer 入队并唤醒 + available.await(); + else { + // 获取头节点的延迟时间是否到时 + long delay = first.getDelay(NANOSECONDS); + if (delay <= 0) + // 到达触发时间,获取头节点并调整堆,重新选择延迟时间最小的节点放入头部 + return finishPoll(first); + + // 逻辑到这说明头节点的延迟时间还没到 + first = null; + // 说明有 leader 线程在等待获取头节点,当前线程直接去阻塞等待 + if (leader != null) + available.await(); + else { + // 没有 leader 线程,【当前线程作为leader线程,并设置头结点的延迟时间作为阻塞时间】 + Thread thisThread = Thread.currentThread(); + leader = thisThread; + try { + // 在条件队列 available 使用带超时的挂起(堆顶任务.time - now() 纳秒值..) + available.awaitNanos(delay); + // 到达阻塞时间时,当前线程会从这里醒来来 + } finally { + // t堆顶更新,leader 置为 null,offer 方法释放锁后, + // 有其它线程通过 take/poll 拿到锁,读到 leader == null,然后将自身更新为leader。 + if (leader == thisThread) + // leader 置为 null 用以接下来判断是否需要唤醒后继线程 + leader = null; + } + } + } + } + } finally { + // 没有 leader 线程,头结点不为 null,唤醒阻塞获取头节点的线程, + // 【如果没有这一步,就会出现有了需要执行的任务,但是没有线程去执行】 + if (leader == null && queue[0] != null) + available.signal(); + lock.unlock(); + } + } + ``` + +* remove():删除节点,堆移除一个元素的时间复杂度是 O(log n),**延迟任务维护了 heapIndex**,直接访问的时间复杂度是 O(1),从而可以更快的移除元素,任务在队列中被取消后会进入该逻辑 + + ```java + public boolean remove(Object x) { + final ReentrantLock lock = this.lock; + lock.lock(); + try { + // 查找对象在队列数组中的下标 + int i = indexOf(x); + // 节点不存在,返回 false + if (i < 0) + return false; + // 修改元素的 heapIndex,-1 代表删除 + setIndex(queue[i], -1); + // 尾索引是长度-1 + int s = --size; + // 尾节点作为替代节点 + RunnableScheduledFuture replacement = queue[s]; + queue[s] = null; + // s == i 说明头节点就是尾节点,队列空了 + if (s != i) { + // 向下调整 + siftDown(i, replacement); + // 说明没发生调整 + if (queue[i] == replacement) + // 上移和下移不可能同时发生,替代节点大于子节点时下移,否则上移 + siftUp(i, replacement); + } + return true; + } finally { + lock.unlock(); + } + } + ``` + +**** + +#### 成员方法 + +##### 提交任务 + +* schedule():延迟执行方法,并指定执行的时间,默认是当前时间 + + ```java + public void execute(Runnable command) { + // 以零延时任务的形式实现 + schedule(command, 0, NANOSECONDS); + } + ``` + + ```java + public ScheduledFuture schedule(Runnable command, long delay, TimeUnit unit) { + // 判空 + if (command == null || unit == null) throw new NullPointerException(); + // 没有做任何操作,直接将 task 返回,该方法主要目的是用于子类扩展,并且【根据延迟时间设置任务触发的时间点】 + RunnableScheduledFuture t = decorateTask(command, new ScheduledFutureTask( + command, null, triggerTime(delay, unit))); + // 延迟执行 + delayedExecute(t); + return t; + } + ``` + + ```java + // 返回【当前时间 + 延迟时间】,就是触发当前任务执行的时间 + private long triggerTime(long delay, TimeUnit unit) { + // 设置触发的时间 + return triggerTime(unit.toNanos((delay < 0) ? 0 : delay)); + } + long triggerTime(long delay) { + // 如果 delay < Long.Max_VALUE/2,则下次执行时间为当前时间 +delay + // 否则为了避免队列中出现由于溢出导致的排序紊乱,需要调用overflowFree来修正一下delay + return now() + ((delay < (Long.MAX_VALUE >> 1)) ? delay : overflowFree(delay)); + } + ``` + + overflowFree 的原因:如果某个任务的 delay 为负数,说明当前可以执行(其实早该执行了)。阻塞队列中维护任务顺序是基于 compareTo 比较的,比较两个任务的顺序会用 time 相减。那么可能出现一个 delay 为正数减去另一个为负数的 delay,结果上溢为负数,则会导致 compareTo 产生错误的结果 + + ```java + private long overflowFree(long delay) { + Delayed head = (Delayed) super.getQueue().peek(); + if (head != null) { + long headDelay = head.getDelay(NANOSECONDS); + // 判断一下队首的delay是不是负数,如果是正数就不用管,怎么减都不会溢出 + // 否则拿当前 delay 减去队首的 delay 来比较看,如果不出现上溢,排序不会乱 + // 不然就把当前 delay 值给调整为 Long.MAX_VALUE + 队首 delay + if (headDelay < 0 && (delay - headDelay < 0)) + delay = Long.MAX_VALUE + headDelay; + } + return delay; + } + ``` + +* scheduleAtFixedRate():定时执行,一次任务的启动到下一次任务的启动的间隔 + + ```java + public ScheduledFuture scheduleAtFixedRate(Runnable command, long initialDelay, long period, + TimeUnit unit) { + if (command == null || unit == null) + throw new NullPointerException(); + if (period <= 0) + throw new IllegalArgumentException(); + // 任务封装,【指定初始的延迟时间和周期时间】 + ScheduledFutureTask sft =new ScheduledFutureTask(command, null, + triggerTime(initialDelay, unit), unit.toNanos(period)); + // 默认返回本身 + RunnableScheduledFuture t = decorateTask(command, sft); + sft.outerTask = t; + // 开始执行这个任务 + delayedExecute(t); + return t; + } + ``` + +* scheduleWithFixedDelay():定时执行,一次任务的结束到下一次任务的启动的间隔 + + ```java + public ScheduledFuture scheduleWithFixedDelay(Runnable command, long initialDelay, long delay, + TimeUnit unit) { + if (command == null || unit == null) + throw new NullPointerException(); + if (delay <= 0) + throw new IllegalArgumentException(); + // 任务封装,【指定初始的延迟时间和周期时间】,周期时间为 - 表示是 fixed-delay 模式 + ScheduledFutureTask sft = new ScheduledFutureTask(command, null, + triggerTime(initialDelay, unit), unit.toNanos(-delay)); + RunnableScheduledFuture t = decorateTask(command, sft); + sft.outerTask = t; + delayedExecute(t); + return t; + } + ``` + +*** + +##### 运行任务 + +* delayedExecute():**校验线程池状态**,延迟或周期性任务的主要执行方法 + + ```java + private void delayedExecute(RunnableScheduledFuture task) { + // 线程池是 SHUTDOWN 状态,需要执行拒绝策略 + if (isShutdown()) + reject(task); + else { + // 把当前任务放入阻塞队列,因为需要【获取执行时间最近的】,当前任务需要比较 + super.getQueue().add(task); + // 线程池状态为 SHUTDOWN 并且不允许执行任务了,就从队列删除该任务,并设置任务的状态为取消状态 + if (isShutdown() && !canRunInCurrentRunState(task.isPeriodic()) && remove(task)) + task.cancel(false); + else + // 可以执行 + ensurePrestart(); + } + } + ``` + +* ensurePrestart():**开启线程执行任务** + + ```java + // ThreadPoolExecutor#ensurePrestart + void ensurePrestart() { + int wc = workerCountOf(ctl.get()); + // worker数目小于corePoolSize,则添加一个worker。 + if (wc < corePoolSize) + // 第二个参数 true 表示采用核心线程数量限制,false 表示采用 maximumPoolSize + addWorker(null, true); + // corePoolSize = 0的情况,至少开启一个线程,【担保机制】 + else if (wc == 0) + addWorker(null, false); + } + ``` + +* canRunInCurrentRunState():任务运行时都会被调用以校验当前状态是否可以运行任务 + + ```java + boolean canRunInCurrentRunState(boolean periodic) { + // 根据是否是周期任务判断,在线程池 shutdown 后是否继续执行该任务,默认非周期任务是继续执行的 + return isRunningOrShutdown(periodic ? continueExistingPeriodicTasksAfterShutdown : + executeExistingDelayedTasksAfterShutdown); + } + ``` + +* onShutdown():删除并取消工作队列中的不需要再执行的任务 + + ```java + void onShutdown() { + BlockingQueue q = super.getQueue(); + // shutdown 后是否仍然执行延时任务 + boolean keepDelayed = getExecuteExistingDelayedTasksAfterShutdownPolicy(); + // shutdown 后是否仍然执行周期任务 + boolean keepPeriodic = getContinueExistingPeriodicTasksAfterShutdownPolicy(); + // 如果两者皆不可,则对队列中【所有任务】调用 cancel 取消并清空队列 + if (!keepDelayed && !keepPeriodic) { + for (Object e : q.toArray()) + if (e instanceof RunnableScheduledFuture) + ((RunnableScheduledFuture) e).cancel(false); + q.clear(); + } + else { + for (Object e : q.toArray()) { + if (e instanceof RunnableScheduledFuture) { + RunnableScheduledFuture t = (RunnableScheduledFuture)e; + // 不需要执行的任务删除并取消,已经取消的任务也需要从队列中删除 + if ((t.isPeriodic() ? !keepPeriodic : !keepDelayed) || + t.isCancelled()) { + if (q.remove(t)) + t.cancel(false); + } + } + } + } + // 因为任务被从队列中清理掉,所以需要调用 tryTerminate 尝试【改变线程池的状态】 + tryTerminate(); + } + ``` + +**** + +### ForkJoin + +Fork/Join:线程池的实现,体现是分治思想,适用于能够进行任务拆分的 CPU 密集型运算,用于**并行计算** + +任务拆分:将一个大任务拆分为算法上相同的小任务,直至不能拆分可以直接求解。跟递归相关的一些计算,如归并排序、斐波那契数列都可以用分治思想进行求解 + +* Fork/Join 在**分治的基础上加入了多线程**,把每个任务的分解和合并交给不同的线程来完成,提升了运算效率 + +* ForkJoin 使用 ForkJoinPool 来启动,是一个特殊的线程池,默认会创建与 CPU 核心数大小相同的线程池 +* 任务有返回值继承 RecursiveTask,没有返回值继承 RecursiveAction + +```java +public static void main(String[] args) { + ForkJoinPool pool = new ForkJoinPool(4); + System.out.println(pool.invoke(new MyTask(5))); + //拆分 5 + MyTask(4) --> 4 + MyTask(3) --> +} + +// 1~ n 之间整数的和 +class MyTask extends RecursiveTask { + private int n; + + public MyTask(int n) { + this.n = n; + } + + @Override + public String toString() { + return "MyTask{" + "n=" + n + '}'; + } + + @Override + protected Integer compute() { + // 如果 n 已经为 1,可以求得结果了 + if (n == 1) { + return n; + } + // 将任务进行拆分(fork) + MyTask t1 = new MyTask(n - 1); + t1.fork(); + // 合并(join)结果 + int result = n + t1.join(); + return result; + } +} +``` + +继续拆分优化: + +```java +class AddTask extends RecursiveTask { + int begin; + int end; + public AddTask(int begin, int end) { + this.begin = begin; + this.end = end; + } + + @Override + public String toString() { + return "{" + begin + "," + end + '}'; + } + + @Override + protected Integer compute() { + // 5, 5 + if (begin == end) { + return begin; + } + // 4, 5 防止多余的拆分 提高效率 + if (end - begin == 1) { + return end + begin; + } + // 1 5 + int mid = (end + begin) / 2; // 3 + AddTask t1 = new AddTask(begin, mid); // 1,3 + t1.fork(); + AddTask t2 = new AddTask(mid + 1, end); // 4,5 + t2.fork(); + int result = t1.join() + t2.join(); + return result; + } +} +``` + +ForkJoinPool 实现了**工作窃取算法**来提高 CPU 的利用率: + +* 每个线程都维护了一个**双端队列**,用来存储需要执行的任务 +* 工作窃取算法允许空闲的线程从其它线程的双端队列中窃取一个任务来执行 +* 窃取的必须是**最晚的任务**,避免和队列所属线程发生竞争,但是队列中只有一个任务时还是会发生竞争 + +*** + +### 享元模式 + +享元模式(Flyweight pattern): 用于减少创建对象的数量,以减少内存占用和提高性能,这种类型的设计模式属于结构型模式,它提供了减少对象数量从而改善应用所需的对象结构的方式 + +异步模式:让有限的工作线程(Worker Thread)来轮流异步处理无限多的任务,也可将其归类为分工模式,典型实现就是线程池 + +工作机制:享元模式尝试重用现有的同类对象,如果未找到匹配的对象,则创建新对象 + +自定义连接池: + +```java +public static void main(String[] args) { + Pool pool = new Pool(2); + for (int i = 0; i < 5; i++) { + new Thread(() -> { + Connection con = pool.borrow(); + try { + Thread.sleep(new Random().nextInt(1000)); + } catch (InterruptedException e) { + e.printStackTrace(); + } + pool.free(con); + }).start(); + } +} +class Pool { + //连接池的大小 + private final int poolSize; + //连接对象的数组 + private Connection[] connections; + //连接状态数组 0表示空闲 1表示繁忙 + private AtomicIntegerArray states; //int[] -> AtomicIntegerArray + + //构造方法 + public Pool(int poolSize) { + this.poolSize = poolSize; + this.connections = new Connection[poolSize]; + this.states = new AtomicIntegerArray(new int[poolSize]); + for (int i = 0; i < poolSize; i++) { + connections[i] = new MockConnection("连接" + (i + 1)); + } + } + + //使用连接 + public Connection borrow() { + while (true) { + for (int i = 0; i < poolSize; i++) { + if (states.get(i) == 0) { + if (states.compareAndSet(i, 0, 1)) { + System.out.println(Thread.currentThread().getName() + " borrow " + connections[i]); + return connections[i]; + } + } + } + //如果没有空闲连接,当前线程等待 + synchronized (this) { + try { + System.out.println(Thread.currentThread().getName() + " wait..."); + this.wait(); + } catch (InterruptedException e) { + e.printStackTrace(); + } + } + } + } + + //归还连接 + public void free(Connection con) { + for (int i = 0; i < poolSize; i++) { + if (connections[i] == con) {//判断是否是同一个对象 + states.set(i, 0);//不用cas的原因是只会有一个线程使用该连接 + synchronized (this) { + System.out.println(Thread.currentThread().getName() + " free " + con); + this.notifyAll(); + } + break; + } + } + } + +} + +class MockConnection implements Connection { + private String name; + //..... +} +``` + +**** + +## 同步器 + +### AQS + +#### 核心思想 + +AQS:AbstractQueuedSynchronizer,是阻塞式锁和相关的同步器工具的框架,许多同步类实现都依赖于该同步器 + +AQS 用状态属性来表示资源的状态(分**独占模式和共享模式**),子类需要定义如何维护这个状态,控制如何获取锁和释放锁 + +* 独占模式是只有一个线程能够访问资源,如 ReentrantLock +* 共享模式允许多个线程访问资源,如 Semaphore,ReentrantReadWriteLock 是组合式 + +AQS 核心思想: + +* 如果被请求的共享资源空闲,则将当前请求资源的线程设置为有效的工作线程,并将共享资源设置锁定状态 + +* 请求的共享资源被占用,AQS 用队列实现线程阻塞等待以及被唤醒时锁分配的机制,将暂时获取不到锁的线程加入到队列中 + + CLH 是一种基于单向链表的**高性能、公平的自旋锁**,AQS 是将每条请求共享资源的线程封装成一个 CLH 锁队列的一个结点(Node)来实现锁的分配 + + + +*** + +#### 设计原理 + +设计原理: + +* 获取锁: + + ```java + while(state 状态不允许获取) { // tryAcquire(arg) + if(队列中还没有此线程) { + 入队并阻塞 park + } + } + 当前线程出队 + ``` + +* 释放锁: + + ```java + if(state 状态允许了) { // tryRelease(arg) + 恢复阻塞的线程(s) unpark + } + ``` + +AbstractQueuedSynchronizer 中 state 设计: + +* state 使用了 32bit int 来维护同步状态,独占模式 0 表示未加锁状态,大于 0 表示已经加锁状态 + + ```java + private volatile int state; + ``` + +* state **使用 volatile 修饰配合 cas** 保证其修改时的原子性 + +* state 表示**线程重入的次数(独占模式)或者剩余许可数(共享模式)** + +* state API: + + * `protected final int getState()`:获取 state 状态 + * `protected final void setState(int newState)`:设置 state 状态 + * `protected final boolean compareAndSetState(int expect,int update)`:**CAS** 安全设置 state + +封装线程的 Node 节点中 waitstate 设计: + +* 使用 **volatile 修饰配合 CAS** 保证其修改时的原子性 + +* 表示 Node 节点的状态,有以下几种状态: + + ```java + // 默认为 0 + volatile int waitStatus; + // 由于超时或中断,此节点被取消,不会再改变状态 + static final int CANCELLED = 1; + // 此节点后面的节点已(或即将)被阻止(通过park),【当前节点在释放或取消时必须唤醒后面的节点】 + static final int SIGNAL = -1; + // 此节点当前在条件队列中 + static final int CONDITION = -2; + // 将releaseShared传播到其他节点 + static final int PROPAGATE = -3; + ``` + +阻塞恢复设计: + +* 使用 park & unpark 来实现线程的暂停和恢复,因为命令的先后顺序不影响结果 +* park & unpark 是针对线程的,而不是针对同步器的,因此控制粒度更为精细 +* park 线程可以通过 interrupt 打断 + +队列设计: + +* 使用了 FIFO 先入先出队列,并不支持优先级队列,**同步队列是双向链表,便于出队入队** + + ```java + // 头结点,指向哑元节点 + private transient volatile Node head; + // 阻塞队列的尾节点,阻塞队列不包含头结点,从 head.next → tail 认为是阻塞队列 + private transient volatile Node tail; + + static final class Node { + // 枚举:共享模式 + static final Node SHARED = new Node(); + // 枚举:独占模式 + static final Node EXCLUSIVE = null; + // node 需要构建成 FIFO 队列,prev 指向前继节点 + volatile Node prev; + // next 指向后继节点 + volatile Node next; + // 当前 node 封装的线程 + volatile Thread thread; + // 条件队列是单向链表,只有后继指针,条件队列使用该属性 + Node nextWaiter; + } + ``` + + ![](https://seazean.oss-cn-beijing.aliyuncs.com/img/Java/JUC-AQS队列设计.png) + +* 条件变量来实现等待、唤醒机制,支持多个条件变量,类似于 Monitor 的 WaitSet,**条件队列是单向链表** + + ````java + public class ConditionObject implements Condition, java.io.Serializable { + // 指向条件队列的第一个 node 节点 + private transient Node firstWaiter; + // 指向条件队列的最后一个 node 节点 + private transient Node lastWaiter; + } + ```` + +*** + +#### 模板对象 + +同步器的设计是基于模板方法模式,该模式是基于继承的,主要是为了在不改变模板结构的前提下在子类中重新定义模板中的内容以实现复用代码 + +* 使用者继承 `AbstractQueuedSynchronizer` 并重写指定的方法 +* 将 AQS 组合在自定义同步组件的实现中,并调用其模板方法,这些模板方法会调用使用者重写的方法 + +AQS 使用了模板方法模式,自定义同步器时需要重写下面几个 AQS 提供的模板方法: + +```java +isHeldExclusively() //该线程是否正在独占资源。只有用到condition才需要去实现它 +tryAcquire(int) //独占方式。尝试获取资源,成功则返回true,失败则返回false +tryRelease(int) //独占方式。尝试释放资源,成功则返回true,失败则返回false +tryAcquireShared(int) //共享方式。尝试获取资源。负数表示失败;0表示成功但没有剩余可用资源;正数表示成功且有剩余资源 +tryReleaseShared(int) //共享方式。尝试释放资源,成功则返回true,失败则返回false +``` + +* 默认情况下,每个方法都抛出 `UnsupportedOperationException` +* 这些方法的实现必须是内部线程安全的 +* AQS 类中的其他方法都是 final ,所以无法被其他类使用,只有这几个方法可以被其他类使用 + +*** + +#### 自定义 + +自定义一个不可重入锁: + +```java +class MyLock implements Lock { + //独占锁 不可重入 + class MySync extends AbstractQueuedSynchronizer { + @Override + protected boolean tryAcquire(int arg) { + if (compareAndSetState(0, 1)) { + // 加上锁 设置 owner 为当前线程 + setExclusiveOwnerThread(Thread.currentThread()); + return true; + } + return false; + } + @Override //解锁 + protected boolean tryRelease(int arg) { + setExclusiveOwnerThread(null); + setState(0);//volatile 修饰的变量放在后面,防止指令重排 + return true; + } + @Override //是否持有独占锁 + protected boolean isHeldExclusively() { + return getState() == 1; + } + public Condition newCondition() { + return new ConditionObject(); + } + } + + private MySync sync = new MySync(); + + @Override //加锁(不成功进入等待队列等待) + public void lock() { + sync.acquire(1); + } + + @Override //加锁 可打断 + public void lockInterruptibly() throws InterruptedException { + sync.acquireInterruptibly(1); + } + + @Override //尝试加锁,尝试一次 + public boolean tryLock() { + return sync.tryAcquire(1); + } + + @Override //尝试加锁,带超时 + public boolean tryLock(long time, TimeUnit unit) throws InterruptedException { + return sync.tryAcquireNanos(1, unit.toNanos(time)); + } + + @Override //解锁 + public void unlock() { + sync.release(1); + } + + @Override //条件变量 + public Condition newCondition() { + return sync.newCondition(); + } +} +``` + +*** + +### Re-Lock + +#### 锁对比 + +ReentrantLock 相对于 synchronized 具备如下特点: + +1. 锁的实现:synchronized 是 JVM 实现的,而 ReentrantLock 是 JDK 实现的 +2. 性能:新版本 Java 对 synchronized 进行了很多优化,synchronized 与 ReentrantLock 大致相同 +3. 使用:ReentrantLock 需要手动解锁,synchronized 执行完代码块自动解锁 +4. **可中断**:ReentrantLock 可中断,而 synchronized 不行 +5. **公平锁**:公平锁是指多个线程在等待同一个锁时,必须按照申请锁的时间顺序来依次获得锁 + * ReentrantLock 可以设置公平锁,synchronized 中的锁是非公平的 + * 不公平锁的含义是阻塞队列内公平,队列外非公平 +6. 锁超时:尝试获取锁,超时获取不到直接放弃,不进入阻塞队列 + * ReentrantLock 可以设置超时时间,synchronized 会一直等待 +7. 锁绑定多个条件:一个 ReentrantLock 可以同时绑定多个 Condition 对象,更细粒度的唤醒线程 +8. 两者都是可重入锁 + +*** + +#### 使用锁 + +构造方法:`ReentrantLock lock = new ReentrantLock();` + +ReentrantLock 类 API: + +* `public void lock()`:获得锁 + * 如果锁没有被另一个线程占用,则将锁定计数设置为 1 + + * 如果当前线程已经保持锁定,则保持计数增加 1 + + * 如果锁被另一个线程保持,则当前线程被禁用线程调度,并且在锁定已被获取之前处于休眠状态 + +* `public void unlock()`:尝试释放锁 + * 如果当前线程是该锁的持有者,则保持计数递减 + * 如果保持计数现在为零,则锁定被释放 + * 如果当前线程不是该锁的持有者,则抛出异常 + +基本语法: + +```java +// 获取锁 +reentrantLock.lock(); +try { + // 临界区 +} finally { + // 释放锁 + reentrantLock.unlock(); +} +``` + +*** + +#### 公平锁 + +##### 基本使用 + +构造方法:`ReentrantLock lock = new ReentrantLock(true)` + +```java +public ReentrantLock(boolean fair) { + sync = fair ? new FairSync() : new NonfairSync(); +} +``` + +ReentrantLock 默认是不公平的: + +```java +public ReentrantLock() { + sync = new NonfairSync(); +} +``` + +说明:公平锁一般没有必要,会降低并发度 + +*** + +##### 非公原理 + +###### 加锁 + +NonfairSync 继承自 AQS + +```java +public void lock() { + sync.lock(); +} +``` + +* 没有竞争:ExclusiveOwnerThread 属于 Thread-0,state 设置为 1 + + ```java + // ReentrantLock.NonfairSync#lock + final void lock() { + // 用 cas 尝试(仅尝试一次)将 state 从 0 改为 1, 如果成功表示【获得了独占锁】 + if (compareAndSetState(0, 1)) + // 设置当前线程为独占线程 + setExclusiveOwnerThread(Thread.currentThread()); + else + acquire(1);//失败进入 + } + ``` + +* 第一个竞争出现:Thread-1 执行,CAS 尝试将 state 由 0 改为 1,结果失败(第一次),进入 acquire 逻辑 + + ```java + // AbstractQueuedSynchronizer#acquire + public final void acquire(int arg) { + // tryAcquire 尝试获取锁失败时, 会调用 addWaiter 将当前线程封装成node入队,acquireQueued 阻塞当前线程, + // acquireQueued 返回 true 表示挂起过程中线程被中断唤醒过,false 表示未被中断过 + if (!tryAcquire(arg) && acquireQueued(addWaiter(Node.EXCLUSIVE), arg)) + // 如果线程被中断了逻辑来到这,完成一次真正的打断效果 + selfInterrupt(); + } + ``` + + + +* 进入 tryAcquire 尝试获取锁逻辑,这时 state 已经是1,结果仍然失败(第二次),加锁成功有两种情况: + + * 当前 AQS 处于无锁状态 + * 加锁线程就是当前线程,说明发生了锁重入 + + ```java + // ReentrantLock.NonfairSync#tryAcquire + protected final boolean tryAcquire(int acquires) { + return nonfairTryAcquire(acquires); + } + // 抢占成功返回 true,抢占失败返回 false + final boolean nonfairTryAcquire(int acquires) { + final Thread current = Thread.currentThread(); + // state 值 + int c = getState(); + // 条件成立说明当前处于【无锁状态】 + if (c == 0) { + //如果还没有获得锁,尝试用cas获得,这里体现非公平性: 不去检查 AQS 队列是否有阻塞线程直接获取锁 + if (compareAndSetState(0, acquires)) { + // 获取锁成功设置当前线程为独占锁线程。 + setExclusiveOwnerThread(current); + return true; + } + } + // 如果已经有线程获得了锁, 独占锁线程还是当前线程, 表示【发生了锁重入】 + else if (current == getExclusiveOwnerThread()) { + // 更新锁重入的值 + int nextc = c + acquires; + // 越界判断,当重入的深度很深时,会导致 nextc < 0,int值达到最大之后再 + 1 变负数 + if (nextc < 0) // overflow + throw new Error("Maximum lock count exceeded"); + // 更新 state 的值,这里不使用 cas 是因为当前线程正在持有锁,所以这里的操作相当于在一个管程内 + setState(nextc); + return true; + } + // 获取失败 + return false; + } + ``` + +* 接下来进入 addWaiter 逻辑,构造 Node 队列(不是阻塞队列),前置条件是当前线程获取锁失败,说明有线程占用了锁 + + * 图中黄色三角表示该 Node 的 waitStatus 状态,其中 0 为默认**正常状态** + * Node 的创建是懒惰的,其中第一个 Node 称为 **Dummy(哑元)或哨兵**,用来占位,并不关联线程 + + ```java + // AbstractQueuedSynchronizer#addWaiter,返回当前线程的 node 节点 + private Node addWaiter(Node mode) { + // 将当前线程关联到一个 Node 对象上, 模式为独占模式 + Node node = new Node(Thread.currentThread(), mode); + Node pred = tail; + // 快速入队,如果 tail 不为 null,说明存在队列 + if (pred != null) { + // 将当前节点的前驱节点指向 尾节点 + node.prev = pred; + // 通过 cas 将 Node 对象加入 AQS 队列,成为尾节点,【尾插法】 + if (compareAndSetTail(pred, node)) { + pred.next = node;// 双向链表 + return node; + } + } + // 初始时队列为空,或者 CAS 失败进入这里 + enq(node); + return node; + } + ``` + + ```java + // AbstractQueuedSynchronizer#enq + private Node enq(final Node node) { + // 自旋入队,必须入队成功才结束循环 + for (;;) { + Node t = tail; + // 说明当前锁被占用,且当前线程可能是【第一个获取锁失败】的线程,【还没有建立队列】 + if (t == null) { + // 设置一个【哑元节点】,头尾指针都指向该节点 + if (compareAndSetHead(new Node())) + tail = head; + } else { + // 自旋到这,普通入队方式,首先赋值尾节点的前驱节点【尾插法】 + node.prev = t; + // 【在设置完尾节点后,才更新的原始尾节点的后继节点,所以此时从前往后遍历会丢失尾节点】 + if (compareAndSetTail(t, node)) { + //【此时 t.next = null,并且这里已经 CAS 结束,线程并不是安全的】 + t.next = node; + return t; // 返回当前 node 的前驱节点 + } + } + } + } + ``` + + + +* 线程节点加入队列成功,进入 AbstractQueuedSynchronizer#acquireQueued 逻辑阻塞线程 + + * acquireQueued 会在一个自旋中不断尝试获得锁,失败后进入 park 阻塞 + + * 如果当前线程是在 head 节点后,会再次 tryAcquire 尝试获取锁,state 仍为 1 则失败(第三次) + + ```java + final boolean acquireQueued(final Node node, int arg) { + // true 表示当前线程抢占锁失败,false 表示成功 + boolean failed = true; + try { + // 中断标记,表示当前线程是否被中断 + boolean interrupted = false; + for (;;) { + // 获得当前线程节点的前驱节点 + final Node p = node.predecessor(); + // 前驱节点是 head, FIFO 队列的特性表示轮到当前线程可以去获取锁 + if (p == head && tryAcquire(arg)) { + // 获取成功, 设置当前线程自己的 node 为 head + setHead(node); + p.next = null; // help GC + // 表示抢占锁成功 + failed = false; + // 返回当前线程是否被中断 + return interrupted; + } + // 判断是否应当 park,返回 false 后需要新一轮的循环,返回 true 进入条件二阻塞线程 + if (shouldParkAfterFailedAcquire(p, node) && parkAndCheckInterrupt()) + // 条件二返回结果是当前线程是否被打断,没有被打断返回 false 不进入这里的逻辑 + // 【就算被打断了,也会继续循环,并不会返回】 + interrupted = true; + } + } finally { + // 【可打断模式下才会进入该逻辑】 + if (failed) + cancelAcquire(node); + } + } + ``` + + * 进入 shouldParkAfterFailedAcquire 逻辑,**将前驱 node 的 waitStatus 改为 -1**,返回 false;waitStatus 为 -1 的节点用来唤醒下一个节点 + + ```java + private static boolean shouldParkAfterFailedAcquire(Node pred, Node node) { + int ws = pred.waitStatus; + // 表示前置节点是个可以唤醒当前节点的节点,返回 true + if (ws == Node.SIGNAL) + return true; + // 前置节点的状态处于取消状态,需要【删除前面所有取消的节点】, 返回到外层循环重试 + if (ws > 0) { + do { + node.prev = pred = pred.prev; + } while (pred.waitStatus > 0); + // 获取到非取消的节点,连接上当前节点 + pred.next = node; + // 默认情况下 node 的 waitStatus 是 0,进入这里的逻辑 + } else { + // 【设置上一个节点状态为 Node.SIGNAL】,返回外层循环重试 + compareAndSetWaitStatus(pred, ws, Node.SIGNAL); + } + // 返回不应该 park,再次尝试一次 + return false; + } + ``` + + * shouldParkAfterFailedAcquire 执行完毕回到 acquireQueued ,再次 tryAcquire 尝试获取锁,这时 state 仍为 1 获取失败(第四次) + * 当再次进入 shouldParkAfterFailedAcquire 时,这时其前驱 node 的 waitStatus 已经是 -1 了,返回 true + * 进入 parkAndCheckInterrupt, Thread-1 park(灰色表示) + + ```java + private final boolean parkAndCheckInterrupt() { + // 阻塞当前线程,如果打断标记已经是 true, 则 park 会失效 + LockSupport.park(this); + // 判断当前线程是否被打断,清除打断标记 + return Thread.interrupted(); + } + ``` + +* 再有多个线程经历竞争失败后: + + ![](https://seazean.oss-cn-beijing.aliyuncs.com/img/Java/JUC-ReentrantLock-非公平锁3.png) + +*** + +###### 解锁 + +ReentrantLock#unlock:释放锁 + +```java +public void unlock() { + sync.release(1); +} +``` + +Thread-0 释放锁,进入 release 流程 + +* 进入 tryRelease,设置 exclusiveOwnerThread 为 null,state = 0 + +* 当前队列不为 null,并且 head 的 waitStatus = -1,进入 unparkSuccessor + + ```java + // AbstractQueuedSynchronizer#release + public final boolean release(int arg) { + // 尝试释放锁,tryRelease 返回 true 表示当前线程已经【完全释放锁,重入的释放了】 + if (tryRelease(arg)) { + // 队列头节点 + Node h = head; + // 头节点什么时候是空?没有发生锁竞争,没有竞争线程创建哑元节点 + // 条件成立说明阻塞队列有等待线程,需要唤醒 head 节点后面的线程 + if (h != null && h.waitStatus != 0) + unparkSuccessor(h); + return true; + } + return false; + } + ``` + + ```java + // ReentrantLock.Sync#tryRelease + protected final boolean tryRelease(int releases) { + // 减去释放的值,可能重入 + int c = getState() - releases; + // 如果当前线程不是持有锁的线程直接报错 + if (Thread.currentThread() != getExclusiveOwnerThread()) + throw new IllegalMonitorStateException(); + // 是否已经完全释放锁 + boolean free = false; + // 支持锁重入, 只有 state 减为 0, 才完全释放锁成功 + if (c == 0) { + free = true; + setExclusiveOwnerThread(null); + } + // 当前线程就是持有锁线程,所以可以直接更新锁,不需要使用 CAS + setState(c); + return free; + } + ``` + +* 进入 AbstractQueuedSynchronizer#unparkSuccessor 方法,唤醒当前节点的后继节点 + + * 找到队列中距离 head 最近的一个没取消的 Node,unpark 恢复其运行,本例中即为 Thread-1 + * 回到 Thread-1 的 acquireQueued 流程 + + ```java + private void unparkSuccessor(Node node) { + // 当前节点的状态 + int ws = node.waitStatus; + if (ws < 0) + // 【尝试重置状态为 0】,因为当前节点要完成对后续节点的唤醒任务了,不需要 -1 了 + compareAndSetWaitStatus(node, ws, 0); + // 找到需要 unpark 的节点,当前节点的下一个 + Node s = node.next; + // 已取消的节点不能唤醒,需要找到距离头节点最近的非取消的节点 + if (s == null || s.waitStatus > 0) { + s = null; + // AQS 队列【从后至前】找需要 unpark 的节点,直到 t == 当前的 node 为止,找不到就不唤醒了 + for (Node t = tail; t != null && t != node; t = t.prev) + // 说明当前线程状态需要被唤醒 + if (t.waitStatus <= 0) + // 置换引用 + s = t; + } + // 【找到合适的可以被唤醒的 node,则唤醒线程】 + if (s != null) + LockSupport.unpark(s.thread); + } + ``` + + **从后向前的唤醒的原因**:enq 方法中,节点是尾插法,首先赋值的是尾节点的前驱节点,此时前驱节点的 next 并没有指向尾节点,从前遍历会丢失尾节点 + +* 唤醒的线程会从 park 位置开始执行,如果加锁成功(没有竞争),会设置 + + * exclusiveOwnerThread 为 Thread-1,state = 1 + * head 指向刚刚 Thread-1 所在的 Node,该 Node 会清空 Thread + * 原本的 head 因为从链表断开,而可被垃圾回收(图中有错误,原来的头节点的 waitStatus 被改为 0 了) + + ![](https://seazean.oss-cn-beijing.aliyuncs.com/img/Java/JUC-ReentrantLock-非公平锁4.png) + +* 如果这时有其它线程来竞争**(非公平)**,例如这时有 Thread-4 来了并抢占了锁 + + * Thread-4 被设置为 exclusiveOwnerThread,state = 1 + * Thread-1 再次进入 acquireQueued 流程,获取锁失败,重新进入 park 阻塞 + + ![](https://seazean.oss-cn-beijing.aliyuncs.com/img/Java/JUC-ReentrantLock-非公平锁5.png) + +*** + +##### 公平原理 + +与非公平锁主要区别在于 tryAcquire 方法:先检查 AQS 队列中是否有前驱节点,没有才去 CAS 竞争 + +```java +static final class FairSync extends Sync { + private static final long serialVersionUID = -3000897897090466540L; + final void lock() { + acquire(1); + } + + protected final boolean tryAcquire(int acquires) { + final Thread current = Thread.currentThread(); + int c = getState(); + if (c == 0) { + // 先检查 AQS 队列中是否有前驱节点, 没有(false)才去竞争 + if (!hasQueuedPredecessors() && + compareAndSetState(0, acquires)) { + setExclusiveOwnerThread(current); + return true; + } + } + // 锁重入 + return false; + } +} +``` + +```java +public final boolean hasQueuedPredecessors() { + Node t = tail; + Node h = head; + Node s; + // 头尾指向一个节点,链表为空,返回false + return h != t && + // 头尾之间有节点,判断头节点的下一个是不是空 + // 不是空进入最后的判断,第二个节点的线程是否是本线程,不是返回 true,表示当前节点有前驱节点 + ((s = h.next) == null || s.thread != Thread.currentThread()); +} +``` + +*** + +#### 可重入 + +可重入是指同一个线程如果首次获得了这把锁,那么它是这把锁的拥有者,因此有权利再次获取这把锁,如果不可重入锁,那么第二次获得锁时,自己也会被锁挡住,直接造成死锁 + +源码解析参考:`nonfairTryAcquire(int acquires))` 和 `tryRelease(int releases)` + +```java +static ReentrantLock lock = new ReentrantLock(); +public static void main(String[] args) { + method1(); +} +public static void method1() { + lock.lock(); + try { + System.out.println(Thread.currentThread().getName() + " execute method1"); + method2(); + } finally { + lock.unlock(); + } +} +public static void method2() { + lock.lock(); + try { + System.out.println(Thread.currentThread().getName() + " execute method2"); + } finally { + lock.unlock(); + } +} +``` + +在 Lock 方法加两把锁会是什么情况呢? + +* 加锁两次解锁两次:正常执行 +* 加锁两次解锁一次:程序直接卡死,线程不能出来,也就说明**申请几把锁,最后需要解除几把锁** +* 加锁一次解锁两次:运行程序会直接报错 + +```java +public void getLock() { + lock.lock(); + lock.lock(); + try { + System.out.println(Thread.currentThread().getName() + "\t get Lock"); + } finally { + lock.unlock(); + //lock.unlock(); + } +} +``` + +**** + +#### 可打断 + +##### 基本使用 + +`public void lockInterruptibly()`:获得可打断的锁 + +* 如果没有竞争此方法就会获取 lock 对象锁 +* 如果有竞争就进入阻塞队列,可以被其他线程用 interrupt 打断 + +注意:如果是不可中断模式,那么即使使用了 interrupt 也不会让等待状态中的线程中断 + +```java +public static void main(String[] args) throws InterruptedException { + ReentrantLock lock = new ReentrantLock(); + Thread t1 = new Thread(() -> { + try { + System.out.println("尝试获取锁"); + lock.lockInterruptibly(); + } catch (InterruptedException e) { + System.out.println("没有获取到锁,被打断,直接返回"); + return; + } + try { + System.out.println("获取到锁"); + } finally { + lock.unlock(); + } + }, "t1"); + lock.lock(); + t1.start(); + Thread.sleep(2000); + System.out.println("主线程进行打断锁"); + t1.interrupt(); +} +``` + +*** + +##### 实现原理 + +* 不可打断模式:即使它被打断,仍会驻留在 AQS 阻塞队列中,一直要**等到获得锁后才能得知自己被打断**了 + + ```java + public final void acquire(int arg) { + if (!tryAcquire(arg) && acquireQueued(addWaiter(Node.EXCLUSIVE), arg))//阻塞等待 + // 如果acquireQueued返回true,打断状态 interrupted = true + selfInterrupt(); + } + static void selfInterrupt() { + // 知道自己被打断了,需要重新产生一次中断完成中断效果 + Thread.currentThread().interrupt(); + } + ``` + + ```java + final boolean acquireQueued(final Node node, int arg) { + try { + boolean interrupted = false; + for (;;) { + final Node p = node.predecessor(); + if (p == head && tryAcquire(arg)) { + setHead(node); + p.next = null; // help GC + failed = false; + // 还是需要获得锁后, 才能返回打断状态 + return interrupted; + } + if (shouldParkAfterFailedAcquire(p, node) && parkAndCheckInterrupt()){ + // 条件二中判断当前线程是否被打断,被打断返回true,设置中断标记为 true,【获取锁后返回】 + interrupted = true; + } + } + } finally { + if (failed) + cancelAcquire(node); + } + } + private final boolean parkAndCheckInterrupt() { + // 阻塞当前线程,如果打断标记已经是 true, 则 park 会失效 + LockSupport.park(this); + // 判断当前线程是否被打断,清除打断标记,被打断返回true + return Thread.interrupted(); + } + ``` + +* 可打断模式:AbstractQueuedSynchronizer#acquireInterruptibly,**被打断后会直接抛出异常** + + ```java + public void lockInterruptibly() throws InterruptedException { + sync.acquireInterruptibly(1); + } + public final void acquireInterruptibly(int arg) { + // 被其他线程打断了直接返回 false + if (Thread.interrupted()) + throw new InterruptedException(); + if (!tryAcquire(arg)) + // 没获取到锁,进入这里 + doAcquireInterruptibly(arg); + } + ``` + + ```java + private void doAcquireInterruptibly(int arg) throws InterruptedException { + // 返回封装当前线程的节点 + final Node node = addWaiter(Node.EXCLUSIVE); + boolean failed = true; + try { + for (;;) { + //... + if (shouldParkAfterFailedAcquire(p, node) && parkAndCheckInterrupt()) + // 【在 park 过程中如果被 interrupt 会抛出异常】, 而不会再次进入循环获取锁后才完成打断效果 + throw new InterruptedException(); + } + } finally { + // 抛出异常前会进入这里 + if (failed) + // 取消当前线程的节点 + cancelAcquire(node); + } + } + ``` + + ```java + // 取消节点出队的逻辑 + private void cancelAcquire(Node node) { + // 判空 + if (node == null) + return; + // 把当前节点封装的 Thread 置为空 + node.thread = null; + // 获取当前取消的 node 的前驱节点 + Node pred = node.prev; + // 前驱节点也被取消了,循环找到前面最近的没被取消的节点 + while (pred.waitStatus > 0) + node.prev = pred = pred.prev; + + // 获取前驱节点的后继节点,可能是当前 node,也可能是 waitStatus > 0 的节点 + Node predNext = pred.next; + + // 把当前节点的状态设置为 【取消状态 1】 + node.waitStatus = Node.CANCELLED; + + // 条件成立说明当前节点是尾节点,把当前节点的前驱节点设置为尾节点 + if (node == tail && compareAndSetTail(node, pred)) { + // 把前驱节点的后继节点置空,这里直接把所有的取消节点出队 + compareAndSetNext(pred, predNext, null); + } else { + // 说明当前节点不是 tail 节点 + int ws; + // 条件一成立说明当前节点不是 head.next 节点 + if (pred != head && + // 判断前驱节点的状态是不是 -1,不成立说明前驱状态可能是 0 或者刚被其他线程取消排队了 + ((ws = pred.waitStatus) == Node.SIGNAL || + // 如果状态不是 -1,设置前驱节点的状态为 -1 + (ws <= 0 && compareAndSetWaitStatus(pred, ws, Node.SIGNAL))) && + // 前驱节点的线程不为null + pred.thread != null) { + + Node next = node.next; + // 当前节点的后继节点是正常节点 + if (next != null && next.waitStatus <= 0) + // 把 前驱节点的后继节点 设置为 当前节点的后继节点,【从队列中删除了当前节点】 + compareAndSetNext(pred, predNext, next); + } else { + // 当前节点是 head.next 节点,唤醒当前节点的后继节点 + unparkSuccessor(node); + } + node.next = node; // help GC + } + } + ``` + +*** + +#### 锁超时 + +##### 基本使用 + +`public boolean tryLock()`:尝试获取锁,获取到返回 true,获取不到直接放弃,不进入阻塞队列 + +`public boolean tryLock(long timeout, TimeUnit unit)`:在给定时间内获取锁,获取不到就退出 + +注意:tryLock 期间也可以被打断 + +```java +public static void main(String[] args) { + ReentrantLock lock = new ReentrantLock(); + Thread t1 = new Thread(() -> { + try { + if (!lock.tryLock(2, TimeUnit.SECONDS)) { + System.out.println("获取不到锁"); + return; + } + } catch (InterruptedException e) { + System.out.println("被打断,获取不到锁"); + return; + } + try { + log.debug("获取到锁"); + } finally { + lock.unlock(); + } + }, "t1"); + lock.lock(); + System.out.println("主线程获取到锁"); + t1.start(); + + Thread.sleep(1000); + try { + System.out.println("主线程释放了锁"); + } finally { + lock.unlock(); + } +} +``` + +*** + +##### 实现原理 + +* 成员变量:指定超时限制的阈值,小于该值的线程不会被挂起 + + ```java + static final long spinForTimeoutThreshold = 1000L; + ``` + + 超时时间设置的小于该值,就会被禁止挂起,因为阻塞在唤醒的成本太高,不如选择自旋空转 + +* tryLock() + + ```java + public boolean tryLock() { + // 只尝试一次 + return sync.nonfairTryAcquire(1); + } + ``` + +* tryLock(long timeout, TimeUnit unit) + + ```java + public final boolean tryAcquireNanos(int arg, long nanosTimeout) { + if (Thread.interrupted()) + throw new InterruptedException(); + // tryAcquire 尝试一次 + return tryAcquire(arg) || doAcquireNanos(arg, nanosTimeout); + } + protected final boolean tryAcquire(int acquires) { + return nonfairTryAcquire(acquires); + } + ``` + + ```java + private boolean doAcquireNanos(int arg, long nanosTimeout) { + if (nanosTimeout <= 0L) + return false; + // 获取最后期限的时间戳 + final long deadline = System.nanoTime() + nanosTimeout; + //... + try { + for (;;) { + //... + // 计算还需等待的时间 + nanosTimeout = deadline - System.nanoTime(); + if (nanosTimeout <= 0L) //时间已到 + return false; + if (shouldParkAfterFailedAcquire(p, node) && + // 如果 nanosTimeout 大于该值,才有阻塞的意义,否则直接自旋会好点 + nanosTimeout > spinForTimeoutThreshold) + LockSupport.parkNanos(this, nanosTimeout); + // 【被打断会报异常】 + if (Thread.interrupted()) + throw new InterruptedException(); + } + } + } + ``` + +*** + +##### 哲学家就餐 + +```java +public static void main(String[] args) { + Chopstick c1 = new Chopstick("1");//... + Chopstick c5 = new Chopstick("5"); + new Philosopher("苏格拉底", c1, c2).start(); + new Philosopher("柏拉图", c2, c3).start(); + new Philosopher("亚里士多德", c3, c4).start(); + new Philosopher("赫拉克利特", c4, c5).start(); + new Philosopher("阿基米德", c5, c1).start(); +} +class Philosopher extends Thread { + Chopstick left; + Chopstick right; + public void run() { + while (true) { + // 尝试获得左手筷子 + if (left.tryLock()) { + try { + // 尝试获得右手筷子 + if (right.tryLock()) { + try { + System.out.println("eating..."); + Thread.sleep(1000); + } finally { + right.unlock(); + } + } + } finally { + left.unlock(); + } + } + } + } +} +class Chopstick extends ReentrantLock { + String name; + public Chopstick(String name) { + this.name = name; + } + @Override + public String toString() { + return "筷子{" + name + '}'; + } +} +``` + +*** + +#### 条件变量 + +##### 基本使用 + +synchronized 的条件变量,是当条件不满足时进入 WaitSet 等待;ReentrantLock 的条件变量比 synchronized 强大之处在于支持多个条件变量 + +ReentrantLock 类获取 Condition 对象:`public Condition newCondition()` + +Condition 类 API: + +* `void await()`:当前线程从运行状态进入等待状态,释放锁 +* `void signal()`:唤醒一个等待在 Condition 上的线程,但是必须获得与该 Condition 相关的锁 + +使用流程: + +* **await / signal 前需要获得锁** +* await 执行后,会释放锁进入 ConditionObject 等待 +* await 的线程被唤醒去重新竞争 lock 锁 + +* **线程在条件队列被打断会抛出中断异常** + +* 竞争 lock 锁成功后,从 await 后继续执行 + +```java +public static void main(String[] args) throws InterruptedException { + ReentrantLock lock = new ReentrantLock(); + //创建一个新的条件变量 + Condition condition1 = lock.newCondition(); + Condition condition2 = lock.newCondition(); + new Thread(() -> { + try { + lock.lock(); + System.out.println("进入等待"); + //进入休息室等待 + condition1.await(); + System.out.println("被唤醒了"); + } catch (InterruptedException e) { + e.printStackTrace(); + } finally { + lock.unlock(); + } + }).start(); + Thread.sleep(1000); + //叫醒 + new Thread(() -> { + try { + lock.lock(); + //唤醒 + condition2.signal(); + } finally { + lock.unlock(); + } + }).start(); +} +``` + +**** + +##### 实现原理 + +###### await + +总体流程是将 await 线程包装成 node 节点放入 ConditionObject 的条件队列,如果被唤醒就将 node 转移到 AQS 的执行阻塞队列,等待获取锁,**每个 Condition 对象都包含一个等待队列** + +* 开始 Thread-0 持有锁,调用 await,线程进入 ConditionObject 等待,直到被唤醒或打断,调用 await 方法的线程都是持锁状态的,所以说逻辑里**不存在并发** + + ```java + public final void await() throws InterruptedException { + // 判断当前线程是否是中断状态,是就直接给个中断异常 + if (Thread.interrupted()) + throw new InterruptedException(); + // 将调用 await 的线程包装成 Node,添加到条件队列并返回 + Node node = addConditionWaiter(); + // 完全释放节点持有的锁,因为其他线程唤醒当前线程的前提是【持有锁】 + int savedState = fullyRelease(node); + + // 设置打断模式为没有被打断,状态码为 0 + int interruptMode = 0; + + // 如果该节点还没有转移至 AQS 阻塞队列, park 阻塞,等待进入阻塞队列 + while (!isOnSyncQueue(node)) { + LockSupport.park(this); + // 如果被打断,退出等待队列,对应的 node 【也会被迁移到阻塞队列】尾部,状态设置为 0 + if ((interruptMode = checkInterruptWhileWaiting(node)) != 0) + break; + } + // 逻辑到这说明当前线程退出等待队列,进入【阻塞队列】 + + // 尝试枪锁,释放了多少锁就【重新获取多少锁】,获取锁成功判断打断模式 + if (acquireQueued(node, savedState) && interruptMode != THROW_IE) + interruptMode = REINTERRUPT; + + // node 在条件队列时 如果被外部线程中断唤醒,会加入到阻塞队列,但是并未设 nextWaiter = null + if (node.nextWaiter != null) + // 清理条件队列内所有已取消的 Node + unlinkCancelledWaiters(); + // 条件成立说明挂起期间发生过中断 + if (interruptMode != 0) + // 应用打断模式 + reportInterruptAfterWait(interruptMode); + } + ``` + + ```java + // 打断模式 - 在退出等待时重新设置打断状态 + private static final int REINTERRUPT = 1; + // 打断模式 - 在退出等待时抛出异常 + private static final int THROW_IE = -1; + ``` + + ![](https://seazean.oss-cn-beijing.aliyuncs.com/img/Java/JUC-ReentrantLock-条件变量1.png) + +* **创建新的 Node 状态为 -2(Node.CONDITION)**,关联 Thread-0,加入等待队列尾部 + + ```java + private Node addConditionWaiter() { + // 获取当前条件队列的尾节点的引用,保存到局部变量 t 中 + Node t = lastWaiter; + // 当前队列中不是空,并且节点的状态不是 CONDITION(-2),说明当前节点发生了中断 + if (t != null && t.waitStatus != Node.CONDITION) { + // 清理条件队列内所有已取消的 Node + unlinkCancelledWaiters(); + // 清理完成重新获取 尾节点 的引用 + t = lastWaiter; + } + // 创建一个关联当前线程的新 node, 设置状态为 CONDITION(-2),添加至队列尾部 + Node node = new Node(Thread.currentThread(), Node.CONDITION); + if (t == null) + firstWaiter = node; // 空队列直接放在队首【不用CAS因为执行线程是持锁线程,并发安全】 + else + t.nextWaiter = node; // 非空队列队尾追加 + lastWaiter = node; // 更新队尾的引用 + return node; + } + ``` + + ```java + // 清理条件队列内所有已取消(不是CONDITION)的 node,【链表删除的逻辑】 + private void unlinkCancelledWaiters() { + // 从头节点开始遍历【FIFO】 + Node t = firstWaiter; + // 指向正常的 CONDITION 节点 + Node trail = null; + // 等待队列不空 + while (t != null) { + // 获取当前节点的后继节点 + Node next = t.nextWaiter; + // 判断 t 节点是不是 CONDITION 节点,条件队列内不是 CONDITION 就不是正常的 + if (t.waitStatus != Node.CONDITION) { + // 不是正常节点,需要 t 与下一个节点断开 + t.nextWaiter = null; + // 条件成立说明遍历到的节点还未碰到过正常节点 + if (trail == null) + // 更新 firstWaiter 指针为下个节点 + firstWaiter = next; + else + // 让上一个正常节点指向 当前取消节点的 下一个节点,【删除非正常的节点】 + trail.nextWaiter = next; + // t 是尾节点了,更新 lastWaiter 指向最后一个正常节点 + if (next == null) + lastWaiter = trail; + } else { + // trail 指向的是正常节点 + trail = t; + } + // 把 t.next 赋值给 t,循环遍历 + t = next; + } + } + ``` + +* 接下来 Thread-0 进入 AQS 的 fullyRelease 流程,释放同步器上的锁 + + ```java + // 线程可能重入,需要将 state 全部释放 + final int fullyRelease(Node node) { + // 完全释放锁是否成功,false 代表成功 + boolean failed = true; + try { + // 获取当前线程所持有的 state 值总数 + int savedState = getState(); + // release -> tryRelease 解锁重入锁 + if (release(savedState)) { + // 释放成功 + failed = false; + // 返回解锁的深度 + return savedState; + } else { + // 解锁失败抛出异常 + throw new IllegalMonitorStateException(); + } + } finally { + // 没有释放成功,将当前 node 设置为取消状态 + if (failed) + node.waitStatus = Node.CANCELLED; + } + } + ``` + +* fullyRelease 中会 unpark AQS 队列中的下一个节点竞争锁,假设 Thread-1 竞争成功 + + ![](https://seazean.oss-cn-beijing.aliyuncs.com/img/Java/JUC-ReentrantLock-条件变量2.png) + +* Thread-0 进入 isOnSyncQueue 逻辑判断节点**是否移动到阻塞队列**,没有就 park 阻塞 Thread-0 + + ```java + final boolean isOnSyncQueue(Node node) { + // node 的状态是 CONDITION,signal 方法是先修改状态再迁移,所以前驱节点为空证明还【没有完成迁移】 + if (node.waitStatus == Node.CONDITION || node.prev == null) + return false; + // 说明当前节点已经成功入队到阻塞队列,且当前节点后面已经有其它 node,因为条件队列的 next 指针为 null + if (node.next != null) + return true; + // 说明【可能在阻塞队列,但是是尾节点】 + // 从阻塞队列的尾节点开始向前【遍历查找 node】,如果查找到返回 true,查找不到返回 false + return findNodeFromTail(node); + } + ``` + +* await 线程 park 后如果被 unpark 或者被打断,都会进入 checkInterruptWhileWaiting 判断线程是否被打断:**在条件队列被打断的线程需要抛出异常** + + ```java + private int checkInterruptWhileWaiting(Node node) { + // Thread.interrupted() 返回当前线程中断标记位,并且重置当前标记位 为 false + // 如果被中断了,根据是否在条件队列被中断的,设置中断状态码 + return Thread.interrupted() ?(transferAfterCancelledWait(node) ? THROW_IE : REINTERRUPT) : 0; + } + ``` + + ```java + // 这个方法只有在线程是被打断唤醒时才会调用 + final boolean transferAfterCancelledWait(Node node) { + // 条件成立说明当前node一定是在条件队列内,因为 signal 迁移节点到阻塞队列时,会将节点的状态修改为 0 + if (compareAndSetWaitStatus(node, Node.CONDITION, 0)) { + // 把【中断唤醒的 node 加入到阻塞队列中】 + enq(node); + // 表示是在条件队列内被中断了,设置为 THROW_IE 为 -1 + return true; + } + + //执行到这里的情况: + //1.当前node已经被外部线程调用 signal 方法将其迁移到 阻塞队列 内了 + //2.当前node正在被外部线程调用 signal 方法将其迁移至 阻塞队列 进行中状态 + + // 如果当前线程还没到阻塞队列,一直释放 CPU + while (!isOnSyncQueue(node)) + Thread.yield(); + + // 表示当前节点被中断唤醒时不在条件队列了,设置为 REINTERRUPT 为 1 + return false; + } + ``` + +* 最后开始处理中断状态: + + ```java + private void reportInterruptAfterWait(int interruptMode) throws InterruptedException { + // 条件成立说明【在条件队列内发生过中断,此时 await 方法抛出中断异常】 + if (interruptMode == THROW_IE) + throw new InterruptedException(); + + // 条件成立说明【在条件队列外发生的中断,此时设置当前线程的中断标记位为 true】 + else if (interruptMode == REINTERRUPT) + // 进行一次自己打断,产生中断的效果 + selfInterrupt(); + } + ``` + +*** + +###### signal + +* 假设 Thread-1 要来唤醒 Thread-0,进入 ConditionObject 的 doSignal 流程,**取得等待队列中第一个 Node**,即 Thread-0 所在 Node,必须持有锁才能唤醒, 因此 doSignal 内线程安全 + + ```java + public final void signal() { + // 判断调用 signal 方法的线程是否是独占锁持有线程 + if (!isHeldExclusively()) + throw new IllegalMonitorStateException(); + // 获取条件队列中第一个 Node + Node first = firstWaiter; + // 不为空就将第该节点【迁移到阻塞队列】 + if (first != null) + doSignal(first); + } + ``` + + ```java + // 唤醒 - 【将没取消的第一个节点转移至 AQS 队列尾部】 + private void doSignal(Node first) { + do { + // 成立说明当前节点的下一个节点是 null,当前节点是尾节点了,队列中只有当前一个节点了 + if ((firstWaiter = first.nextWaiter) == null) + lastWaiter = null; + first.nextWaiter = null; + // 将等待队列中的 Node 转移至 AQS 队列,不成功且还有节点则继续循环 + } while (!transferForSignal(first) && (first = firstWaiter) != null); + } + + // signalAll() 会调用这个函数,唤醒所有的节点 + private void doSignalAll(Node first) { + lastWaiter = firstWaiter = null; + do { + Node next = first.nextWaiter; + first.nextWaiter = null; + transferForSignal(first); + first = next; + // 唤醒所有的节点,都放到阻塞队列中 + } while (first != null); + } + ``` + +* 执行 transferForSignal,**先将节点的 waitStatus 改为 0,然后加入 AQS 阻塞队列尾部**,将 Thread-3 的 waitStatus 改为 -1 + + ```java + // 如果节点状态是取消, 返回 false 表示转移失败, 否则转移成功 + final boolean transferForSignal(Node node) { + // CAS 修改当前节点的状态,修改为 0,因为当前节点马上要迁移到阻塞队列了 + // 如果状态已经不是 CONDITION, 说明线程被取消(await 释放全部锁失败)或者被中断(可打断 cancelAcquire) + if (!compareAndSetWaitStatus(node, Node.CONDITION, 0)) + // 返回函数调用处继续寻找下一个节点 + return false; + + // 【先改状态,再进行迁移】 + // 将当前 node 入阻塞队列,p 是当前节点在阻塞队列的【前驱节点】 + Node p = enq(node); + int ws = p.waitStatus; + + // 如果前驱节点被取消或者不能设置状态为 Node.SIGNAL,就 unpark 取消当前节点线程的阻塞状态, + // 让 thread-0 线程竞争锁,重新同步状态 + if (ws > 0 || !compareAndSetWaitStatus(p, ws, Node.SIGNAL)) + LockSupport.unpark(node.thread); + return true; + } + ``` + + ![](https://seazean.oss-cn-beijing.aliyuncs.com/img/Java/JUC-ReentrantLock-条件变量3.png) + +* Thread-1 释放锁,进入 unlock 流程 + +*** + +### ReadWrite + +#### 读写锁 + +独占锁:指该锁一次只能被一个线程所持有,对 ReentrantLock 和 Synchronized 而言都是独占锁 + +共享锁:指该锁可以被多个线程锁持有 + +ReentrantReadWriteLock 其**读锁是共享锁,写锁是独占锁** + +作用:多个线程同时读一个资源类没有任何问题,为了满足并发量,读取共享资源应该同时进行,但是如果一个线程想去写共享资源,就不应该再有其它线程可以对该资源进行读或写 + +使用规则: + +* 加锁解锁格式: + + ```java + r.lock(); + try { + // 临界区 + } finally { + r.unlock(); + } + ``` + +* 读-读能共存、读-写不能共存、写-写不能共存 + +* 读锁不支持条件变量 + +* **重入时升级不支持**:持有读锁的情况下去获取写锁会导致获取写锁永久等待,需要先释放读,再去获得写 + +* **重入时降级支持**:持有写锁的情况下去获取读锁,造成只有当前线程会持有读锁,因为写锁会互斥其他的锁 + + ```java + w.lock(); + try { + r.lock();// 降级为读锁, 释放写锁, 这样能够让其它线程读取缓存 + try { + // ... + } finally{ + w.unlock();// 要在写锁释放之前获取读锁 + } + } finally{ + r.unlock(); + } + ``` + +构造方法: + +* `public ReentrantReadWriteLock()`:默认构造方法,非公平锁 +* `public ReentrantReadWriteLock(boolean fair)`:true 为公平锁 + +常用API: + +* `public ReentrantReadWriteLock.ReadLock readLock()`:返回读锁 +* `public ReentrantReadWriteLock.WriteLock writeLock()`:返回写锁 +* `public void lock()`:加锁 +* `public void unlock()`:解锁 +* `public boolean tryLock()`:尝试获取锁 + +读读并发: + +```java +public static void main(String[] args) { + ReentrantReadWriteLock rw = new ReentrantReadWriteLock(); + ReentrantReadWriteLock.ReadLock r = rw.readLock(); + ReentrantReadWriteLock.WriteLock w = rw.writeLock(); + + new Thread(() -> { + r.lock(); + try { + Thread.sleep(2000); + System.out.println("Thread 1 running " + new Date()); + } finally { + r.unlock(); + } + },"t1").start(); + new Thread(() -> { + r.lock(); + try { + Thread.sleep(2000); + System.out.println("Thread 2 running " + new Date()); + } finally { + r.unlock(); + } + },"t2").start(); +} +``` + +*** + +#### 缓存应用 + +缓存更新时,是先清缓存还是先更新数据库 + +* 先清缓存:可能造成刚清理缓存还没有更新数据库,线程直接查询了数据库更新过期数据到缓存 + +* 先更新据库:可能造成刚更新数据库,还没清空缓存就有线程从缓存拿到了旧数据 + +* 补充情况:查询线程 A 查询数据时恰好缓存数据由于时间到期失效,或是第一次查询 + + + +可以使用读写锁进行操作 + +*** + +#### 实现原理 + +##### 成员属性 + +读写锁用的是同一个 Sycn 同步器,因此等待队列、state 等也是同一个,原理与 ReentrantLock 加锁相比没有特殊之处,不同是**写锁状态占了 state 的低 16 位,而读锁使用的是 state 的高 16 位** + +* 读写锁: + + ```java + private final ReentrantReadWriteLock.ReadLock readerLock; + private final ReentrantReadWriteLock.WriteLock writerLock; + ``` + +* 构造方法:默认是非公平锁,可以指定参数创建公平锁 + + ```java + public ReentrantReadWriteLock(boolean fair) { + // true 为公平锁 + sync = fair ? new FairSync() : new NonfairSync(); + // 这两个 lock 共享同一个 sync 实例,都是由 ReentrantReadWriteLock 的 sync 提供同步实现 + readerLock = new ReadLock(this); + writerLock = new WriteLock(this); + } + ``` + +Sync 类的属性: + +* 统计变量: + + ```java + // 用来移位 + static final int SHARED_SHIFT = 16; + // 高16位的1 + static final int SHARED_UNIT = (1 << SHARED_SHIFT); + // 65535,16个1,代表写锁的最大重入次数 + static final int MAX_COUNT = (1 << SHARED_SHIFT) - 1; + // 低16位掩码:0b 1111 1111 1111 1111,用来获取写锁重入的次数 + static final int EXCLUSIVE_MASK = (1 << SHARED_SHIFT) - 1; + ``` + +* 获取读写锁的次数: + + ```java + // 获取读写锁的读锁分配的总次数 + static int sharedCount(int c) { return c >>> SHARED_SHIFT; } + // 写锁(独占)锁的重入次数 + static int exclusiveCount(int c) { return c & EXCLUSIVE_MASK; } + ``` + +* 内部类: + + ```java + // 记录读锁线程自己的持有读锁的数量(重入次数),因为 state 高16位记录的是全局范围内所有的读线程获取读锁的总量 + static final class HoldCounter { + int count = 0; + // Use id, not reference, to avoid garbage retention + final long tid = getThreadId(Thread.currentThread()); + } + // 线程安全的存放线程各自的 HoldCounter 对象 + static final class ThreadLocalHoldCounter extends ThreadLocal { + public HoldCounter initialValue() { + return new HoldCounter(); + } + } + ``` + +* 内部类实例: + + ```java + // 当前线程持有的可重入读锁的数量,计数为 0 时删除 + private transient ThreadLocalHoldCounter readHolds; + // 记录最后一个获取【读锁】线程的 HoldCounter 对象 + private transient HoldCounter cachedHoldCounter; + ``` + +* 首次获取锁: + + ```java + // 第一个获取读锁的线程 + private transient Thread firstReader = null; + // 记录该线程持有的读锁次数(读锁重入次数) + private transient int firstReaderHoldCount; + ``` + +* Sync 构造方法: + + ```java + Sync() { + readHolds = new ThreadLocalHoldCounter(); + // 确保其他线程的数据可见性,state 是 volatile 修饰的变量,重写该值会将线程本地缓存数据【同步至主存】 + setState(getState()); + } + ``` + +*** + +##### 加锁原理 + +* t1 线程:w.lock(**写锁**),成功上锁 state = 0_1 + + ```java + // lock() -> sync.acquire(1); + public void lock() { + sync.acquire(1); + } + public final void acquire(int arg) { + // 尝试获得写锁,获得写锁失败,将当前线程关联到一个 Node 对象上, 模式为独占模式 + if (!tryAcquire(arg) && acquireQueued(addWaiter(Node.EXCLUSIVE), arg)) + selfInterrupt(); + } + ``` + + ```java + protected final boolean tryAcquire(int acquires) { + Thread current = Thread.currentThread(); + int c = getState(); + // 获得低 16 位, 代表写锁的 state 计数 + int w = exclusiveCount(c); + // 说明有读锁或者写锁 + if (c != 0) { + // c != 0 and w == 0 表示有读锁,【读锁不能升级】,直接返回 false + // w != 0 说明有写锁,写锁的拥有者不是自己,获取失败 + if (w == 0 || current != getExclusiveOwnerThread()) + return false; + + // 执行到这里只有一种情况:【写锁重入】,所以下面几行代码不存在并发 + if (w + exclusiveCount(acquires) > MAX_COUNT) + throw new Error("Maximum lock count exceeded"); + // 写锁重入, 获得锁成功,没有并发,所以不使用 CAS + setState(c + acquires); + return true; + } + + // c == 0,说明没有任何锁,判断写锁是否该阻塞,是 false 就尝试获取锁,失败返回 false + if (writerShouldBlock() || !compareAndSetState(c, c + acquires)) + return false; + // 获得锁成功,设置锁的持有线程为当前线程 + setExclusiveOwnerThread(current); + return true; + } + // 非公平锁 writerShouldBlock 总是返回 false, 无需阻塞 + final boolean writerShouldBlock() { + return false; + } + // 公平锁会检查 AQS 队列中是否有前驱节点, 没有(false)才去竞争 + final boolean writerShouldBlock() { + return hasQueuedPredecessors(); + } + ``` + +* t2 r.lock(**读锁**),进入 tryAcquireShared 流程: + + * 返回 -1 表示失败 + * 如果返回 0 表示成功 + * 返回正数表示还有多少后继节点支持共享模式,读写锁返回 1 + + ```java + public void lock() { + sync.acquireShared(1); + } + public final void acquireShared(int arg) { + // tryAcquireShared 返回负数, 表示获取读锁失败 + if (tryAcquireShared(arg) < 0) + doAcquireShared(arg); + } + ``` + + ```java + // 尝试以共享模式获取 + protected final int tryAcquireShared(int unused) { + Thread current = Thread.currentThread(); + int c = getState(); + // exclusiveCount(c) 代表低 16 位, 写锁的 state,成立说明有线程持有写锁 + // 写锁的持有者不是当前线程,则获取读锁失败,【写锁允许降级】 + if (exclusiveCount(c) != 0 && getExclusiveOwnerThread() != current) + return -1; + + // 高 16 位,代表读锁的 state,共享锁分配出去的总次数 + int r = sharedCount(c); + // 读锁是否应该阻塞 + if (!readerShouldBlock() && r < MAX_COUNT && + compareAndSetState(c, c + SHARED_UNIT)) { // 尝试增加读锁计数 + // 加锁成功 + // 加锁之前读锁为 0,说明当前线程是第一个读锁线程 + if (r == 0) { + firstReader = current; + firstReaderHoldCount = 1; + // 第一个读锁线程是自己就发生了读锁重入 + } else if (firstReader == current) { + firstReaderHoldCount++; + } else { + // cachedHoldCounter 设置为当前线程的 holdCounter 对象,即最后一个获取读锁的线程 + HoldCounter rh = cachedHoldCounter; + // 说明还没设置 rh + if (rh == null || rh.tid != getThreadId(current)) + // 获取当前线程的锁重入的对象,赋值给 cachedHoldCounter + cachedHoldCounter = rh = readHolds.get(); + // 还没重入 + else if (rh.count == 0) + readHolds.set(rh); + // 重入 + 1 + rh.count++; + } + // 读锁加锁成功 + return 1; + } + // 逻辑到这 应该阻塞,或者 cas 加锁失败 + // 会不断尝试 for (;;) 获取读锁, 执行过程中无阻塞 + return fullTryAcquireShared(current); + } + // 非公平锁 readerShouldBlock 偏向写锁一些,看 AQS 阻塞队列中第一个节点是否是写锁,是则阻塞,反之不阻塞 + // 防止一直有读锁线程,导致写锁线程饥饿 + // true 则该阻塞, false 则不阻塞 + final boolean readerShouldBlock() { + return apparentlyFirstQueuedIsExclusive(); + } + final boolean readerShouldBlock() { + return hasQueuedPredecessors(); + } + ``` + + ```java + final int fullTryAcquireShared(Thread current) { + // 当前读锁线程持有的读锁次数对象 + HoldCounter rh = null; + for (;;) { + int c = getState(); + // 说明有线程持有写锁 + if (exclusiveCount(c) != 0) { + // 写锁不是自己则获取锁失败 + if (getExclusiveOwnerThread() != current) + return -1; + } else if (readerShouldBlock()) { + // 条件成立说明当前线程是 firstReader,当前锁是读忙碌状态,而且当前线程也是读锁重入 + if (firstReader == current) { + // assert firstReaderHoldCount > 0; + } else { + if (rh == null) { + // 最后一个读锁的 HoldCounter + rh = cachedHoldCounter; + // 说明当前线程也不是最后一个读锁 + if (rh == null || rh.tid != getThreadId(current)) { + // 获取当前线程的 HoldCounter + rh = readHolds.get(); + // 条件成立说明 HoldCounter 对象是上一步代码新建的 + // 当前线程不是锁重入,在 readerShouldBlock() 返回 true 时需要去排队 + if (rh.count == 0) + // 防止内存泄漏 + readHolds.remove(); + } + } + if (rh.count == 0) + return -1; + } + } + // 越界判断 + if (sharedCount(c) == MAX_COUNT) + throw new Error("Maximum lock count exceeded"); + // 读锁加锁,条件内的逻辑与 tryAcquireShared 相同 + if (compareAndSetState(c, c + SHARED_UNIT)) { + if (sharedCount(c) == 0) { + firstReader = current; + firstReaderHoldCount = 1; + } else if (firstReader == current) { + firstReaderHoldCount++; + } else { + if (rh == null) + rh = cachedHoldCounter; + if (rh == null || rh.tid != getThreadId(current)) + rh = readHolds.get(); + else if (rh.count == 0) + readHolds.set(rh); + rh.count++; + cachedHoldCounter = rh; // cache for release + } + return 1; + } + } + } + ``` + +* 获取读锁失败,进入 sync.doAcquireShared(1) 流程开始阻塞,首先也是调用 addWaiter 添加节点,不同之处在于节点被设置为 Node.SHARED 模式而非 Node.EXCLUSIVE 模式,注意此时 t2 仍处于活跃状态 + + ```java + private void doAcquireShared(int arg) { + // 将当前线程关联到一个 Node 对象上, 模式为共享模式 + final Node node = addWaiter(Node.SHARED); + boolean failed = true; + try { + boolean interrupted = false; + for (;;) { + // 获取前驱节点 + final Node p = node.predecessor(); + // 如果前驱节点就头节点就去尝试获取锁 + if (p == head) { + // 再一次尝试获取读锁 + int r = tryAcquireShared(arg); + // r >= 0 表示获取成功 + if (r >= 0) { + //【这里会设置自己为头节点,唤醒相连的后序的共享节点】 + setHeadAndPropagate(node, r); + p.next = null; // help GC + if (interrupted) + selfInterrupt(); + failed = false; + return; + } + } + // 是否在获取读锁失败时阻塞 park 当前线程 + if (shouldParkAfterFailedAcquire(p, node) && parkAndCheckInterrupt()) + interrupted = true; + } + } finally { + if (failed) + cancelAcquire(node); + } + } + ``` + + 如果没有成功,在 doAcquireShared 内 for (;;) 循环一次,shouldParkAfterFailedAcquire 内把前驱节点的 waitStatus 改为 -1,再 for (;;) 循环一次尝试 tryAcquireShared,不成功在 parkAndCheckInterrupt() 处 park + + + +* 这种状态下,假设又有 t3 r.lock,t4 w.lock,这期间 t1 仍然持有锁,就变成了下面的样子 + + ![](https://seazean.oss-cn-beijing.aliyuncs.com/img/Java/JUC-ReentrantReadWriteLock加锁2.png) + +*** + +##### 解锁原理 + +* t1 w.unlock, 写锁解锁 + + ```java + public void unlock() { + // 释放锁 + sync.release(1); + } + public final boolean release(int arg) { + // 尝试释放锁 + if (tryRelease(arg)) { + Node h = head; + // 头节点不为空并且不是等待状态不是 0,唤醒后继的非取消节点 + if (h != null && h.waitStatus != 0) + unparkSuccessor(h); + return true; + } + return false; + } + protected final boolean tryRelease(int releases) { + if (!isHeldExclusively()) + throw new IllegalMonitorStateException(); + int nextc = getState() - releases; + // 因为可重入的原因, 写锁计数为 0, 才算释放成功 + boolean free = exclusiveCount(nextc) == 0; + if (free) + setExclusiveOwnerThread(null); + setState(nextc); + return free; + } + ``` + +* 唤醒流程 sync.unparkSuccessor,这时 t2 在 doAcquireShared 的 parkAndCheckInterrupt() 处恢复运行,继续循环,执行 tryAcquireShared 成功则让读锁计数加一 + +* 接下来 t2 调用 setHeadAndPropagate(node, 1),它原本所在节点被置为头节点;还会检查下一个节点是否是 shared,如果是则调用 doReleaseShared() 将 head 的状态从 -1 改为 0 并唤醒下一个节点,这时 t3 在 doAcquireShared 内 parkAndCheckInterrupt() 处恢复运行,**唤醒连续的所有的共享节点** + + ```java + private void setHeadAndPropagate(Node node, int propagate) { + Node h = head; + // 设置自己为 head 节点 + setHead(node); + // propagate 表示有共享资源(例如共享读锁或信号量),为 0 就没有资源 + if (propagate > 0 || h == null || h.waitStatus < 0 || + (h = head) == null || h.waitStatus < 0) { + // 获取下一个节点 + Node s = node.next; + // 如果当前是最后一个节点,或者下一个节点是【等待共享读锁的节点】 + if (s == null || s.isShared()) + // 唤醒后继节点 + doReleaseShared(); + } + } + ``` + + ```java + private void doReleaseShared() { + // 如果 head.waitStatus == Node.SIGNAL ==> 0 成功, 下一个节点 unpark + // 如果 head.waitStatus == 0 ==> Node.PROPAGATE + for (;;) { + Node h = head; + if (h != null && h != tail) { + int ws = h.waitStatus; + // SIGNAL 唤醒后继 + if (ws == Node.SIGNAL) { + // 因为读锁共享,如果其它线程也在释放读锁,那么需要将 waitStatus 先改为 0 + // 防止 unparkSuccessor 被多次执行 + if (!compareAndSetWaitStatus(h, Node.SIGNAL, 0)) + continue; + // 唤醒后继节点 + unparkSuccessor(h); + } + // 如果已经是 0 了,改为 -3,用来解决传播性 + else if (ws == 0 && !compareAndSetWaitStatus(h, 0, Node.PROPAGATE)) + continue; + } + // 条件不成立说明被唤醒的节点非常积极,直接将自己设置为了新的 head, + // 此时唤醒它的节点(前驱)执行 h == head 不成立,所以不会跳出循环,会继续唤醒新的 head 节点的后继节点 + if (h == head) + break; + } + } + ``` + + + +* 下一个节点不是 shared 了,因此不会继续唤醒 t4 所在节点 + +* t2 读锁解锁,进入 sync.releaseShared(1) 中,调用 tryReleaseShared(1) 让计数减一,但计数还不为零,t3 同样让计数减一,计数为零,进入doReleaseShared() 将头节点从 -1 改为 0 并唤醒下一个节点 + + ```java + public void unlock() { + sync.releaseShared(1); + } + public final boolean releaseShared(int arg) { + if (tryReleaseShared(arg)) { + doReleaseShared(); + return true; + } + return false; + } + ``` + + ```java + protected final boolean tryReleaseShared(int unused) { + + for (;;) { + int c = getState(); + int nextc = c - SHARED_UNIT; + // 读锁的计数不会影响其它获取读锁线程, 但会影响其它获取写锁线程,计数为 0 才是真正释放 + if (compareAndSetState(c, nextc)) + // 返回是否已经完全释放了 + return nextc == 0; + } + } + ``` + +* t4 在 acquireQueued 中 parkAndCheckInterrupt 处恢复运行,再次 for (;;) 这次自己是头节点的临节点,并且没有其他节点竞争,tryAcquire(1) 成功,修改头结点,流程结束 + + + +*** + +#### Stamped + +StampedLock:读写锁,该类自 JDK 8 加入,是为了进一步优化读性能 + +特点: + +* 在使用读锁、写锁时都必须配合戳使用 + +* StampedLock 不支持条件变量 +* StampedLock **不支持重入** + +基本用法 + +* 加解读锁: + + ```java + long stamp = lock.readLock(); + lock.unlockRead(stamp); // 类似于 unpark,解指定的锁 + ``` + +* 加解写锁: + + ```java + long stamp = lock.writeLock(); + lock.unlockWrite(stamp); + ``` + +* 乐观读,StampedLock 支持 `tryOptimisticRead()` 方法,读取完毕后做一次**戳校验**,如果校验通过,表示这期间没有其他线程的写操作,数据可以安全使用,如果校验没通过,需要重新获取读锁,保证数据一致性 + + ```java + long stamp = lock.tryOptimisticRead(); + // 验戳 + if(!lock.validate(stamp)){ + // 锁升级 + } + ``` + +提供一个数据容器类内部分别使用读锁保护数据的 read() 方法,写锁保护数据的 write() 方法: + +* 读-读可以优化 +* 读-写优化读,补加读锁 + +```java +public static void main(String[] args) throws InterruptedException { + DataContainerStamped dataContainer = new DataContainerStamped(1); + new Thread(() -> { + dataContainer.read(1000); + },"t1").start(); + Thread.sleep(500); + + new Thread(() -> { + dataContainer.write(1000); + },"t2").start(); +} + +class DataContainerStamped { + private int data; + private final StampedLock lock = new StampedLock(); + + public int read(int readTime) throws InterruptedException { + long stamp = lock.tryOptimisticRead(); + System.out.println(new Date() + " optimistic read locking" + stamp); + Thread.sleep(readTime); + // 戳有效,直接返回数据 + if (lock.validate(stamp)) { + Sout(new Date() + " optimistic read finish..." + stamp); + return data; + } + + // 说明其他线程更改了戳,需要锁升级了,从乐观读升级到读锁 + System.out.println(new Date() + " updating to read lock" + stamp); + try { + stamp = lock.readLock(); + System.out.println(new Date() + " read lock" + stamp); + Thread.sleep(readTime); + System.out.println(new Date() + " read finish..." + stamp); + return data; + } finally { + System.out.println(new Date() + " read unlock " + stamp); + lock.unlockRead(stamp); + } + } + + public void write(int newData) { + long stamp = lock.writeLock(); + System.out.println(new Date() + " write lock " + stamp); + try { + Thread.sleep(2000); + this.data = newData; + } catch (InterruptedException e) { + e.printStackTrace(); + } finally { + System.out.println(new Date() + " write unlock " + stamp); + lock.unlockWrite(stamp); + } + } +} +``` + +*** + +### CountDown + +#### 基本使用 + +CountDownLatch:计数器,用来进行线程同步协作,**等待所有线程完成** + +构造器: + +* `public CountDownLatch(int count)`:初始化唤醒需要的 down 几步 + +常用API: + +* `public void await()`:让当前线程等待,必须 down 完初始化的数字才可以被唤醒,否则进入无限等待 +* `public void countDown()`:计数器进行减 1(down 1) + +应用:同步等待多个 Rest 远程调用结束 + +```java +// LOL 10人进入游戏倒计时 +public static void main(String[] args) throws InterruptedException { + CountDownLatch latch = new CountDownLatch(10); + ExecutorService service = Executors.newFixedThreadPool(10); + String[] all = new String[10]; + Random random = new Random(); + + for (int j = 0; j < 10; j++) { + int finalJ = j;//常量 + service.submit(() -> { + for (int i = 0; i <= 100; i++) { + Thread.sleep(random.nextInt(100)); //随机休眠 + all[finalJ] = i + "%"; + System.out.print("\r" + Arrays.toString(all)); // \r代表覆盖 + } + latch.countDown(); + }); + } + latch.await(); + System.out.println("\n游戏开始"); + service.shutdown(); +} +/* +[100%, 100%, 100%, 100%, 100%, 100%, 100%, 100%, 100%, 100%] +游戏开始 +``` + +*** + +#### 实现原理 + +阻塞等待: + +* 线程调用 await() 等待其他线程完成任务:支持打断 + + ```java + public void await() throws InterruptedException { + sync.acquireSharedInterruptibly(1); + } + // AbstractQueuedSynchronizer#acquireSharedInterruptibly + public final void acquireSharedInterruptibly(int arg) throws InterruptedException { + // 判断线程是否被打断,抛出打断异常 + if (Thread.interrupted()) + throw new InterruptedException(); + // 尝试获取共享锁,条件成立说明 state > 0,此时线程入队阻塞等待,等待其他线程获取共享资源 + // 条件不成立说明 state = 0,此时不需要阻塞线程,直接结束函数调用 + if (tryAcquireShared(arg) < 0) + doAcquireSharedInterruptibly(arg); + } + // CountDownLatch.Sync#tryAcquireShared + protected int tryAcquireShared(int acquires) { + return (getState() == 0) ? 1 : -1; + } + ``` + +* 线程进入 AbstractQueuedSynchronizer#doAcquireSharedInterruptibly 函数阻塞挂起,等待 latch 变为 0: + + ```java + private void doAcquireSharedInterruptibly(int arg) throws InterruptedException { + // 将调用latch.await()方法的线程 包装成 SHARED 类型的 node 加入到 AQS 的阻塞队列中 + final Node node = addWaiter(Node.SHARED); + boolean failed = true; + try { + for (;;) { + // 获取当前节点的前驱节点 + final Node p = node.predecessor(); + // 前驱节点时头节点就可以尝试获取锁 + if (p == head) { + // 再次尝试获取锁,获取成功返回 1 + int r = tryAcquireShared(arg); + if (r >= 0) { + // 获取锁成功,设置当前节点为 head 节点,并且向后传播 + setHeadAndPropagate(node, r); + p.next = null; // help GC + failed = false; + return; + } + } + // 阻塞在这里 + if (shouldParkAfterFailedAcquire(p, node) && parkAndCheckInterrupt()) + throw new InterruptedException(); + } + } finally { + // 阻塞线程被中断后抛出异常,进入取消节点的逻辑 + if (failed) + cancelAcquire(node); + } + } + ``` + +* 获取共享锁成功,进入唤醒阻塞队列中与头节点相连的 SHARED 模式的节点: + + ```java + private void setHeadAndPropagate(Node node, int propagate) { + Node h = head; + // 将当前节点设置为新的 head 节点,前驱节点和持有线程置为 null + setHead(node); + // propagate = 1,条件一成立 + if (propagate > 0 || h == null || h.waitStatus < 0 || (h = head) == null || h.waitStatus < 0) { + // 获取当前节点的后继节点 + Node s = node.next; + // 当前节点是尾节点时 next 为 null,或者后继节点是 SHARED 共享模式 + if (s == null || s.isShared()) + // 唤醒所有的等待共享锁的节点 + doReleaseShared(); + } + } + ``` + +计数减一: + +* 线程进入 countDown() 完成计数器减一(释放锁)的操作 + + ```java + public void countDown() { + sync.releaseShared(1); + } + public final boolean releaseShared(int arg) { + // 尝试释放共享锁 + if (tryReleaseShared(arg)) { + // 释放锁成功开始唤醒阻塞节点 + doReleaseShared(); + return true; + } + return false; + } + ``` + +* 更新 state 值,每调用一次,state 值减一,当 state -1 正好为 0 时,返回 true + + ```java + protected boolean tryReleaseShared(int releases) { + for (;;) { + int c = getState(); + // 条件成立说明前面【已经有线程触发唤醒操作】了,这里返回 false + if (c == 0) + return false; + // 计数器减一 + int nextc = c-1; + if (compareAndSetState(c, nextc)) + // 计数器为 0 时返回 true + return nextc == 0; + } + } + ``` + +* state = 0 时,当前线程需要执行**唤醒阻塞节点的任务** + + ```java + private void doReleaseShared() { + for (;;) { + Node h = head; + // 判断队列是否是空队列 + if (h != null && h != tail) { + int ws = h.waitStatus; + // 头节点的状态为 signal,说明后继节点没有被唤醒过 + if (ws == Node.SIGNAL) { + // cas 设置头节点的状态为 0,设置失败继续自旋 + if (!compareAndSetWaitStatus(h, Node.SIGNAL, 0)) + continue; + // 唤醒后继节点 + unparkSuccessor(h); + } + // 如果有其他线程已经设置了头节点的状态,重新设置为 PROPAGATE 传播属性 + else if (ws == 0 && !compareAndSetWaitStatus(h, 0, Node.PROPAGATE)) + continue; + } + // 条件不成立说明被唤醒的节点非常积极,直接将自己设置为了新的head, + // 此时唤醒它的节点(前驱)执行 h == head 不成立,所以不会跳出循环,会继续唤醒新的 head 节点的后继节点 + if (h == head) + break; + } + } + ``` + +*** + +### CyclicBarrier + +#### 基本使用 + +CyclicBarrier:循环屏障,用来进行线程协作,等待线程满足某个计数,才能触发自己执行 + +常用方法: + +* `public CyclicBarrier(int parties, Runnable barrierAction)`:用于在线程到达屏障 parties 时,执行 barrierAction + * parties:代表多少个线程到达屏障开始触发线程任务 + * barrierAction:线程任务 +* `public int await()`:线程调用 await 方法通知 CyclicBarrier 本线程已经到达屏障 + +与 CountDownLatch 的区别:CyclicBarrier 是可以重用的 + +应用:可以实现多线程中,某个任务在等待其他线程执行完毕以后触发 + +```java +public static void main(String[] args) { + ExecutorService service = Executors.newFixedThreadPool(2); + CyclicBarrier barrier = new CyclicBarrier(2, () -> { + System.out.println("task1 task2 finish..."); + }); + + for (int i = 0; i < 3; i++) { // 循环重用 + service.submit(() -> { + System.out.println("task1 begin..."); + try { + Thread.sleep(1000); + barrier.await(); // 2 - 1 = 1 + } catch (InterruptedException | BrokenBarrierException e) { + e.printStackTrace(); + } + }); + + service.submit(() -> { + System.out.println("task2 begin..."); + try { + Thread.sleep(2000); + barrier.await(); // 1 - 1 = 0 + } catch (InterruptedException | BrokenBarrierException e) { + e.printStackTrace(); + } + }); + } + service.shutdown(); +} +``` + +*** + +#### 实现原理 + +##### 成员属性 + +* 全局锁:利用可重入锁实现的工具类 + + ```java + // barrier 实现是依赖于Condition条件队列,condition 条件队列必须依赖lock才能使用 + private final ReentrantLock lock = new ReentrantLock(); + // 线程挂起实现使用的 condition 队列,当前代所有线程到位,这个条件队列内的线程才会被唤醒 + private final Condition trip = lock.newCondition(); + ``` + +* 线程数量: + + ```java + private final int parties; // 代表多少个线程到达屏障开始触发线程任务 + private int count; // 表示当前“代”还有多少个线程未到位,初始值为 parties + ``` + +* 当前代中最后一个线程到位后要执行的事件: + + ```java + private final Runnable barrierCommand; + ``` + +* 代: + + ```java + // 表示 barrier 对象当前 代 + private Generation generation = new Generation(); + private static class Generation { + // 表示当前“代”是否被打破,如果被打破再来到这一代的线程 就会直接抛出 BrokenException 异常 + // 且在这一代挂起的线程都会被唤醒,然后抛出 BrokerException 异常。 + boolean broken = false; + } + ``` + +* 构造方法: + + ```java + public CyclicBarrie(int parties, Runnable barrierAction) { + // 因为小于等于 0 的 barrier 没有任何意义 + if (parties <= 0) throw new IllegalArgumentException(); + + this.parties = parties; + this.count = parties; + // 可以为 null + this.barrierCommand = barrierAction; + } + ``` + + + +*** + +##### 成员方法 + +* await():阻塞等待所有线程到位 + + ```java + public int await() throws InterruptedException, BrokenBarrierException { + try { + return dowait(false, 0L); + } catch (TimeoutException toe) { + throw new Error(toe); // cannot happen + } + } + ``` + + ```java + // timed:表示当前调用await方法的线程是否指定了超时时长,如果 true 表示线程是响应超时的 + // nanos:线程等待超时时长,单位是纳秒 + private int dowait(boolean timed, long nanos) { + final ReentrantLock lock = this.lock; + // 加锁 + lock.lock(); + try { + // 获取当前代 + final Generation g = generation; + + // 【如果当前代是已经被打破状态,则当前调用await方法的线程,直接抛出Broken异常】 + if (g.broken) + throw new BrokenBarrierException(); + // 如果当前线程被中断了,则打破当前代,然后当前线程抛出中断异常 + if (Thread.interrupted()) { + // 设置当前代的状态为 broken 状态,唤醒在 trip 条件队列内的线程 + breakBarrier(); + throw new InterruptedException(); + } + + // 逻辑到这说明,当前线程中断状态是 false, 当前代的 broken 为 false(未打破状态) + + // 假设 parties 给的是 5,那么index对应的值为 4,3,2,1,0 + int index = --count; + // 条件成立说明当前线程是最后一个到达 barrier 的线程,【需要开启新代,唤醒阻塞线程】 + if (index == 0) { + // 栅栏任务启动标记 + boolean ranAction = false; + try { + final Runnable command = barrierCommand; + if (command != null) + // 启动触发的任务 + command.run(); + // run()未抛出异常的话,启动标记设置为 true + ranAction = true; + // 开启新的一代,这里会【唤醒所有的阻塞队列】 + nextGeneration(); + // 返回 0 因为当前线程是此代最后一个到达的线程,index == 0 + return 0; + } finally { + // 如果 command.run() 执行抛出异常的话,会进入到这里 + if (!ranAction) + breakBarrier(); + } + } + + // 自旋,一直到条件满足、当前代被打破、线程被中断,等待超时 + for (;;) { + try { + // 根据是否需要超时等待选择阻塞方法 + if (!timed) + // 当前线程释放掉 lock,【进入到 trip 条件队列的尾部挂起自己】,等待被唤醒 + trip.await(); + else if (nanos > 0L) + nanos = trip.awaitNanos(nanos); + } catch (InterruptedException ie) { + // 被中断后来到这里的逻辑 + + // 当前代没有变化并且没有被打破 + if (g == generation && !g.broken) { + // 打破屏障 + breakBarrier(); + // node 节点在【条件队列】内收到中断信号时 会抛出中断异常 + throw ie; + } else { + // 等待过程中代变化了,完成一次自我打断 + Thread.currentThread().interrupt(); + } + } + // 唤醒后的线程,【判断当前代已经被打破,线程唤醒后依次抛出 BrokenBarrier 异常】 + if (g.broken) + throw new BrokenBarrierException(); + + // 当前线程挂起期间,最后一个线程到位了,然后触发了开启新的一代的逻辑 + if (g != generation) + return index; + // 当前线程 trip 中等待超时,然后主动转移到阻塞队列 + if (timed && nanos <= 0L) { + breakBarrier(); + // 抛出超时异常 + throw new TimeoutException(); + } + } + } finally { + // 解锁 + lock.unlock(); + } + } + ``` + +* breakBarrier():打破 Barrier 屏障 + + ```java + private void breakBarrier() { + // 将代中的 broken 设置为 true,表示这一代是被打破了,再来到这一代的线程,直接抛出异常 + generation.broken = true; + // 重置 count 为 parties + count = parties; + // 将在trip条件队列内挂起的线程全部唤醒,唤醒后的线程会检查当前是否是打破的,然后抛出异常 + trip.signalAll(); + } + ``` + +* nextGeneration():开启新的下一代 + + ```java + private void nextGeneration() { + // 将在 trip 条件队列内挂起的线程全部唤醒 + trip.signalAll(); + // 重置 count 为 parties + count = parties; + + // 开启新的一代,使用一个新的generation对象,表示新的一代,新的一代和上一代【没有任何关系】 + generation = new Generation(); + } + ``` + +参考视频: + +**** + +### Semaphore + +#### 基本使用 + +synchronized 可以起到锁的作用,但某个时间段内,只能有一个线程允许执行 + +Semaphore(信号量)用来限制能同时访问共享资源的线程上限,非重入锁 + +构造方法: + +* `public Semaphore(int permits)`:permits 表示许可线程的数量(state) +* `public Semaphore(int permits, boolean fair)`:fair 表示公平性,如果设为 true,下次执行的线程会是等待最久的线程 + +常用API: + +* `public void acquire()`:表示获取许可 +* `public void release()`:表示释放许可,acquire() 和 release() 方法之间的代码为同步代码 + +```java +public static void main(String[] args) { + // 1.创建Semaphore对象 + Semaphore semaphore = new Semaphore(3); + + // 2. 10个线程同时运行 + for (int i = 0; i < 10; i++) { + new Thread(() -> { + try { + // 3. 获取许可 + semaphore.acquire(); + sout(Thread.currentThread().getName() + " running..."); + Thread.sleep(1000); + sout(Thread.currentThread().getName() + " end..."); + } catch (InterruptedException e) { + e.printStackTrace(); + } finally { + // 4. 释放许可 + semaphore.release(); + } + }).start(); + } +} +``` + +*** + +#### 实现原理 + +加锁流程: + +* Semaphore 的 permits(state)为 3,这时 5 个线程来获取资源 + + ```java + Sync(int permits) { + setState(permits); + } + ``` + + 假设其中 Thread-1,Thread-2,Thread-4 CAS 竞争成功,permits 变为 0,而 Thread-0 和 Thread-3 竞争失败,进入 AQS 队列park 阻塞 + + ```java + // acquire() -> sync.acquireSharedInterruptibly(1),可中断 + public final void acquireSharedInterruptibly(int arg) { + if (Thread.interrupted()) + throw new InterruptedException(); + // 尝试获取通行证,获取成功返回 >= 0的值 + if (tryAcquireShared(arg) < 0) + // 获取许可证失败,进入阻塞 + doAcquireSharedInterruptibly(arg); + } + + // tryAcquireShared() -> nonfairTryAcquireShared() + // 非公平,公平锁会在循环内 hasQueuedPredecessors()方法判断阻塞队列是否有临头节点(第二个节点) + final int nonfairTryAcquireShared(int acquires) { + for (;;) { + // 获取 state ,state 这里【表示通行证】 + int available = getState(); + // 计算当前线程获取通行证完成之后,通行证还剩余数量 + int remaining = available - acquires; + // 如果许可已经用完, 返回负数, 表示获取失败, + if (remaining < 0 || + // 许可证足够分配的,如果 cas 重试成功, 返回正数, 表示获取成功 + compareAndSetState(available, remaining)) + return remaining; + } + } + ``` + + ```java + private void doAcquireSharedInterruptibly(int arg) { + // 将调用 Semaphore.aquire 方法的线程,包装成 node 加入到 AQS 的阻塞队列中 + final Node node = addWaiter(Node.SHARED); + // 获取标记 + boolean failed = true; + try { + for (;;) { + final Node p = node.predecessor(); + // 前驱节点是头节点可以再次获取许可 + if (p == head) { + // 再次尝试获取许可,【返回剩余的许可证数量】 + int r = tryAcquireShared(arg); + if (r >= 0) { + // 成功后本线程出队(AQS), 所在 Node设置为 head + // r 表示【可用资源数】, 为 0 则不会继续传播 + setHeadAndPropagate(node, r); + p.next = null; // help GC + failed = false; + return; + } + } + // 不成功, 设置上一个节点 waitStatus = Node.SIGNAL, 下轮进入 park 阻塞 + if (shouldParkAfterFailedAcquire(p, node) && parkAndCheckInterrupt()) + throw new InterruptedException(); + } + } finally { + // 被打断后进入该逻辑 + if (failed) + cancelAcquire(node); + } + } + ``` + + ```java + private void setHeadAndPropagate(Node node, int propagate) { + Node h = head; + // 设置自己为 head 节点 + setHead(node); + // propagate 表示有【共享资源】(例如共享读锁或信号量) + // head waitStatus == Node.SIGNAL 或 Node.PROPAGATE,doReleaseShared 函数中设置的 + if (propagate > 0 || h == null || h.waitStatus < 0 || + (h = head) == null || h.waitStatus < 0) { + Node s = node.next; + // 如果是最后一个节点或者是等待共享读锁的节点,做一次唤醒 + if (s == null || s.isShared()) + doReleaseShared(); + } + } + ``` + + ![](https://seazean.oss-cn-beijing.aliyuncs.com/img/Java/JUC-Semaphore工作流程1.png) + +* 这时 Thread-4 释放了 permits,状态如下 + + ```java + // release() -> releaseShared() + public final boolean releaseShared(int arg) { + // 尝试释放锁 + if (tryReleaseShared(arg)) { + doReleaseShared(); + return true; + } + return false; + } + protected final boolean tryReleaseShared(int releases) { + for (;;) { + // 获取当前锁资源的可用许可证数量 + int current = getState(); + int next = current + releases; + // 索引越界判断 + if (next < current) + throw new Error("Maximum permit count exceeded"); + // 释放锁 + if (compareAndSetState(current, next)) + return true; + } + } + private void doReleaseShared() { + // PROPAGATE 详解 + // 如果 head.waitStatus == Node.SIGNAL ==> 0 成功, 下一个节点 unpark + // 如果 head.waitStatus == 0 ==> Node.PROPAGATE + } + ``` + + ![](https://seazean.oss-cn-beijing.aliyuncs.com/img/Java/JUC-Semaphore工作流程2.png) + +* 接下来 Thread-0 竞争成功,permits 再次设置为 0,设置自己为 head 节点,并且 unpark 接下来的共享状态的 Thread-3 节点,但由于 permits 是 0,因此 Thread-3 在尝试不成功后再次进入 park 状态 + +**** + +#### PROPAGATE + +假设存在某次循环中队列里排队的结点情况为 `head(-1) → t1(-1) → t2(0)`,存在将要释放信号量的 T3 和 T4,释放顺序为先 T3 后 T4 + +```java +// 老版本代码 +private void setHeadAndPropagate(Node node, int propagate) { + setHead(node); + // 有空闲资源 + if (propagate > 0 && node.waitStatus != 0) { + Node s = node.next; + // 下一个 + if (s == null || s.isShared()) + unparkSuccessor(node); + } +} +``` + +正常流程: + +* T3 调用 releaseShared(1),直接调用了 unparkSuccessor(head),head.waitStatus 从 -1 变为 0 +* T1 由于 T3 释放信号量被唤醒,然后 T4 释放,唤醒 T2 + +BUG 流程: + +* T3 调用 releaseShared(1),直接调用了 unparkSuccessor(head),head.waitStatus 从 -1 变为 0 +* T1 由于 T3 释放信号量被唤醒,调用 tryAcquireShared,返回值为 0(获取锁成功,但没有剩余资源量) +* T1 还没调用 setHeadAndPropagate 方法,T4 调用 releaseShared(1),此时 head.waitStatus 为 0(此时读到的 head 和 1 中为同一个 head),不满足条件,因此不调用 unparkSuccessor(head) +* T1 获取信号量成功,调用 setHeadAndPropagate(t1.node, 0) 时,因为不满足 propagate > 0(剩余资源量 == 0),从而不会唤醒后继结点, **T2 线程得不到唤醒** + +更新后流程: + +* T3 调用 releaseShared(1),直接调用了 unparkSuccessor(head),head.waitStatus 从 -1 变为 0 +* T1 由于 T3 释放信号量被唤醒,调用 tryAcquireShared,返回值为 0(获取锁成功,但没有剩余资源量) + +* T1 还没调用 setHeadAndPropagate 方法,T4 调用 releaseShared(),此时 head.waitStatus 为 0(此时读到的 head 和 1 中为同一个 head),调用 doReleaseShared() 将等待状态置为 **PROPAGATE(-3)** +* T1 获取信号量成功,调用 setHeadAndPropagate 时,读到 h.waitStatus < 0,从而调用 doReleaseShared() 唤醒 T2 + +```java +private void setHeadAndPropagate(Node node, int propagate) { + Node h = head; + // 设置自己为 head 节点 + setHead(node); + // propagate 表示有共享资源(例如共享读锁或信号量) + // head waitStatus == Node.SIGNAL 或 Node.PROPAGATE + if (propagate > 0 || h == null || h.waitStatus < 0 || + (h = head) == null || h.waitStatus < 0) { + Node s = node.next; + // 如果是最后一个节点或者是等待共享读锁的节点,做一次唤醒 + if (s == null || s.isShared()) + doReleaseShared(); + } +} +``` + +```java +// 唤醒 +private void doReleaseShared() { + // 如果 head.waitStatus == Node.SIGNAL ==> 0 成功, 下一个节点 unpark + // 如果 head.waitStatus == 0 ==> Node.PROPAGATE + for (;;) { + Node h = head; + if (h != null && h != tail) { + int ws = h.waitStatus; + if (ws == Node.SIGNAL) { + // 防止 unparkSuccessor 被多次执行 + if (!compareAndSetWaitStatus(h, Node.SIGNAL, 0)) + continue; + // 唤醒后继节点 + unparkSuccessor(h); + } + // 如果已经是 0 了,改为 -3,用来解决传播性 + else if (ws == 0 && !compareAndSetWaitStatus(h, 0, Node.PROPAGATE)) + continue; + } + if (h == head) + break; + } +} +``` + +*** + +### Exchanger + +Exchanger:交换器,是一个用于线程间协作的工具类,用于进行线程间的数据交换 + +工作流程:两个线程通过 exchange 方法交换数据,如果第一个线程先执行 exchange() 方法,它会一直等待第二个线程也执行 exchange 方法,当两个线程都到达同步点时,这两个线程就可以交换数据 + +常用方法: + +* `public Exchanger()`:创建一个新的交换器 +* `public V exchange(V x)`:等待另一个线程到达此交换点 +* `public V exchange(V x, long timeout, TimeUnit unit)`:等待一定的时间 + +```java +public class ExchangerDemo { + public static void main(String[] args) { + // 创建交换对象(信使) + Exchanger exchanger = new Exchanger<>(); + new ThreadA(exchanger).start(); + new ThreadB(exchanger).start(); + } +} +class ThreadA extends Thread{ + private Exchanger exchanger(); + + public ThreadA(Exchanger exchanger){ + this.exchanger = exchanger; + } + + @Override + public void run() { + try{ + sout("线程A,做好了礼物A,等待线程B送来的礼物B"); + //如果等待了5s还没有交换就死亡(抛出异常)! + String s = exchanger.exchange("礼物A",5,TimeUnit.SECONDS); + sout("线程A收到线程B的礼物:" + s); + } catch (Exception e) { + System.out.println("线程A等待了5s,没有收到礼物,最终就执行结束了!"); + } + } +} +class ThreadB extends Thread{ + private Exchanger exchanger; + + public ThreadB(Exchanger exchanger) { + this.exchanger = exchanger; + } + + @Override + public void run() { + try { + sout("线程B,做好了礼物B,等待线程A送来的礼物A....."); + // 开始交换礼物。参数是送给其他线程的礼物! + sout("线程B收到线程A的礼物:" + exchanger.exchange("礼物B")); + } catch (Exception e) { + e.printStackTrace(); + } + } +} +``` + +*** + +## 并发包 + +### ConHashMap + +#### 并发集合 + +##### 集合对比 + +三种集合: + +* HashMap 是线程不安全的,性能好 +* Hashtable 线程安全基于 synchronized,综合性能差,已经被淘汰 +* ConcurrentHashMap 保证了线程安全,综合性能较好,不止线程安全,而且效率高,性能好 + +集合对比: + +1. Hashtable 继承 Dictionary 类,HashMap、ConcurrentHashMap 继承 AbstractMap,均实现 Map 接口 +2. Hashtable 底层是数组 + 链表,JDK8 以后 HashMap 和 ConcurrentHashMap 底层是数组 + 链表 + 红黑树 +3. HashMap 线程非安全,Hashtable 线程安全,Hashtable 的方法都加了 synchronized 关来确保线程同步 +4. ConcurrentHashMap、Hashtable **不允许 null 值**,HashMap 允许 null 值 +5. ConcurrentHashMap、HashMap 的初始容量为 16,Hashtable 初始容量为11,填充因子默认都是 0.75,两种 Map 扩容是当前容量翻倍:capacity *2,Hashtable 扩容时是容量翻倍 + 1:capacity*2 + 1 + +![ConcurrentHashMap数据结构](https://seazean.oss-cn-beijing.aliyuncs.com/img/Java/ConcurrentHashMap数据结构.png) + +工作步骤: + +1. 初始化,使用 cas 来保证并发安全,懒惰初始化 table + +2. 树化,当 table.length < 64 时,先尝试扩容,超过 64 时,并且 bin.length > 8 时,会将**链表树化**,树化过程会用 synchronized 锁住链表头 + + 说明:锁住某个槽位的对象头,是一种很好的**细粒度的加锁**方式,类似 MySQL 中的行锁 + +3. put,如果该 bin 尚未创建,只需要使用 cas 创建 bin;如果已经有了,锁住链表头进行后续 put 操作,元素添加至 bin 的尾部 + +4. get,无锁操作仅需要保证可见性,扩容过程中 get 操作拿到的是 ForwardingNode 会让 get 操作在新 table 进行搜索 + +5. 扩容,扩容时以 bin 为单位进行,需要对 bin 进行 synchronized,但这时其它竞争线程也不是无事可做,它们会帮助把其它 bin 进行扩容 + +6. size,元素个数保存在 baseCount 中,并发时的个数变动保存在 CounterCell[] 当中,最后统计数量时累加 + +```java +//需求:多个线程同时往HashMap容器中存入数据会出现安全问题 +public class ConcurrentHashMapDemo{ + public static Map map = new ConcurrentHashMap(); + + public static void main(String[] args){ + new AddMapDataThread().start(); + new AddMapDataThread().start(); + + Thread.sleep(1000 * 5);//休息5秒,确保两个线程执行完毕 + System.out.println("Map大小:" + map.size());//20万 + } +} + +public class AddMapDataThread extends Thread{ + @Override + public void run() { + for(int i = 0 ; i < 1000000 ; i++ ){ + ConcurrentHashMapDemo.map.put("键:"+i , "值"+i); + } + } +} +``` + +**** + +##### 并发死链 + +JDK1.7 的 HashMap 采用的头插法(拉链法)进行节点的添加,HashMap 的扩容长度为原来的 2 倍 + +resize() 中节点(Entry)转移的源代码: + +```java +void transfer(Entry[] newTable, boolean rehash) { + int newCapacity = newTable.length;//得到新数组的长度 + // 遍历整个数组对应下标下的链表,e代表一个节点 + for (Entry e : table) { + // 当e == null时,则该链表遍历完了,继续遍历下一数组下标的链表 + while(null != e) { + // 先把e节点的下一节点存起来 + Entry next = e.next; + if (rehash) { //得到新的hash值 + e.hash = null == e.key ? 0 : hash(e.key); + } + // 在新数组下得到新的数组下标 + int i = indexFor(e.hash, newCapacity); + // 将e的next指针指向新数组下标的位置 + e.next = newTable[i]; + // 将该数组下标的节点变为e节点 + newTable[i] = e; + // 遍历链表的下一节点 + e = next; + } + } +} +``` + +JDK 8 虽然将扩容算法做了调整,改用了尾插法,但仍不意味着能够在多线程环境下能够安全扩容,还会出现其它问题(如扩容丢数据) + +B站视频解析: + +*** + +#### 成员属性 + +##### 变量 + +* 存储数组: + + ```java + transient volatile Node[] table; + ``` + +* 散列表的长度: + + ```java + private static final int MAXIMUM_CAPACITY = 1 << 30; // 最大长度 + private static final int DEFAULT_CAPACITY = 16; // 默认长度 + ``` + +* 并发级别,JDK7 遗留下来,1.8 中不代表并发级别: + + ```java + private static final int DEFAULT_CONCURRENCY_LEVEL = 16; + ``` + +* 负载因子,JDK1.8 的 ConcurrentHashMap 中是固定值: + + ```java + private static final float LOAD_FACTOR = 0.75f; + ``` + +* 阈值: + + ```java + static final int TREEIFY_THRESHOLD = 8; // 链表树化的阈值 + static final int UNTREEIFY_THRESHOLD = 6; // 红黑树转化为链表的阈值 + static final int MIN_TREEIFY_CAPACITY = 64; // 当数组长度达到64且某个桶位中的链表长度超过8,才会真正树化 + ``` + +* 扩容相关: + + ```java + private static final int MIN_TRANSFER_STRIDE = 16; // 线程迁移数据【最小步长】,控制线程迁移任务的最小区间 + private static int RESIZE_STAMP_BITS = 16; // 用来计算扩容时生成的【标识戳】 + private static final int MAX_RESIZERS = (1 << (32 - RESIZE_STAMP_BITS)) - 1;// 65535-1并发扩容最多线程数 + private static final int RESIZE_STAMP_SHIFT = 32 - RESIZE_STAMP_BITS; // 扩容时使用 + ``` + +* 节点哈希值: + + ```java + static final int MOVED = -1; // 表示当前节点是 FWD 节点 + static final int TREEBIN = -2; // 表示当前节点已经树化,且当前节点为 TreeBin 对象 + static final int RESERVED = -3; // 表示节点时临时节点 + static final int HASH_BITS = 0x7fffffff; // 正常节点的哈希值的可用的位数 + ``` + +* 扩容过程:volatile 修饰保证多线程的可见性 + + ```java + // 扩容过程中,会将扩容中的新 table 赋值给 nextTable 保持引用,扩容结束之后,这里会被设置为 null + private transient volatile Node[] nextTable; + // 记录扩容进度,所有线程都要从 0 - transferIndex 中分配区间任务,简单说就是老表转移到哪了,索引从高到低转移 + private transient volatile int transferIndex; + ``` + +* 累加统计: + + ```java + // LongAdder 中的 baseCount 未发生竞争时或者当前LongAdder处于加锁状态时,增量累到到 baseCount 中 + private transient volatile long baseCount; + // LongAdder 中的 cellsBuzy,0 表示当前 LongAdder 对象无锁状态,1 表示当前 LongAdder 对象加锁状态 + private transient volatile int cellsBusy; + // LongAdder 中的 cells 数组, + private transient volatile CounterCell[] counterCells; + ``` + +* 控制变量: + + **sizeCtl** < 0: + + * -1 表示当前 table 正在初始化(有线程在创建 table 数组),当前线程需要自旋等待 + + * 其他负数表示当前 map 的 table 数组正在进行扩容,高 16 位表示扩容的标识戳;低 16 位表示 (1 + nThread) 当前参与并发扩容的线程数量 + 1 + + sizeCtl = 0,表示创建 table 数组时使用 DEFAULT_CAPACITY 为数组大小 + + sizeCtl > 0: + + * 如果 table 未初始化,表示初始化大小 + * 如果 table 已经初始化,表示下次扩容时的触发条件(阈值,元素个数,不是数组的长度) + + ```java + private transient volatile int sizeCtl; // volatile 保持可见性 + ``` + +*** + +##### 内部类 + +* Node 节点: + + ```java + static class Node implements Entry { + // 节点哈希值 + final int hash; + final K key; + volatile V val; + // 单向链表 + volatile Node next; + } + ``` + +* TreeBin 节点: + + ```java + static final class TreeBin extends Node { + // 红黑树根节点 + TreeNode root; + // 链表的头节点 + volatile TreeNode first; + // 等待者线程 + volatile Thread waiter; + + volatile int lockState; + // 写锁状态 写锁是独占状态,以散列表来看,真正进入到 TreeBin 中的写线程同一时刻只有一个线程 + static final int WRITER = 1; + // 等待者状态(写线程在等待),当 TreeBin 中有读线程目前正在读取数据时,写线程无法修改数据 + static final int WAITER = 2; + // 读锁状态是共享,同一时刻可以有多个线程 同时进入到 TreeBi 对象中获取数据,每一个线程都给 lockState + 4 + static final int READER = 4; + } + ``` + +* TreeNode 节点: + + ```java + static final class TreeNode extends Node { + TreeNode parent; // red-black tree links + TreeNode left; + TreeNode right; + TreeNode prev; //双向链表 + boolean red; + } + ``` + +* ForwardingNode 节点:转移节点 + + ```java + static final class ForwardingNode extends Node { + // 持有扩容后新的哈希表的引用 + final Node[] nextTable; + ForwardingNode(Node[] tab) { + // ForwardingNode 节点的 hash 值设为 -1 + super(MOVED, null, null, null); + this.nextTable = tab; + } + } + ``` + +*** + +##### 代码块 + +* 变量: + + ```java + // 表示sizeCtl属性在 ConcurrentHashMap 中内存偏移地址 + private static final long SIZECTL; + // 表示transferIndex属性在 ConcurrentHashMap 中内存偏移地址 + private static final long TRANSFERINDEX; + // 表示baseCount属性在 ConcurrentHashMap 中内存偏移地址 + private static final long BASECOUNT; + // 表示cellsBusy属性在 ConcurrentHashMap 中内存偏移地址 + private static final long CELLSBUSY; + // 表示cellValue属性在 CounterCell 中内存偏移地址 + private static final long CELLVALUE; + // 表示数组第一个元素的偏移地址 + private static final long ABASE; + // 用位移运算替代乘法 + private static final int ASHIFT; + ``` + +* 赋值方法: + + ```java + // 表示数组单元所占用空间大小,scale 表示 Node[] 数组中每一个单元所占用空间大小,int 是 4 字节 + int scale = U.arrayIndexScale(ak); + // 判断一个数是不是 2 的 n 次幂,比如 8:1000 & 0111 = 0000 + if ((scale & (scale - 1)) != 0) + throw new Error("data type scale not a power of two"); + + // numberOfLeadingZeros(n):返回当前数值转换为二进制后,从高位到低位开始统计,看有多少个0连续在一起 + // 8 → 1000 numberOfLeadingZeros(8) = 28 + // 4 → 100 numberOfLeadingZeros(4) = 29 int 值就是占4个字节 + ASHIFT = 31 - Integer.numberOfLeadingZeros(scale); + + // ASHIFT = 31 - 29 = 2 ,int 的大小就是 2 的 2 次方,获取次方数 + // ABASE + (5 << ASHIFT) 用位移运算替代了乘法,获取 arr[5] 的值 + ``` + +*** + +#### 构造方法 + +* 无参构造, 散列表结构延迟初始化,默认的数组大小是 16: + + ```java + public ConcurrentHashMap() { + } + ``` + +* 有参构造: + + ```java + public ConcurrentHashMap(int initialCapacity) { + // 指定容量初始化 + if (initialCapacity < 0) throw new IllegalArgumentException(); + int cap = ((initialCapacity >= (MAXIMUM_CAPACITY >>> 1)) ? + MAXIMUM_CAPACITY : + // 假如传入的参数是 16,16 + 8 + 1 ,最后得到 32 + // 传入 12, 12 + 6 + 1 = 19,最后得到 32,尽可能的大,与 HashMap不一样 + tableSizeFor(initialCapacity + (initialCapacity >>> 1) + 1)); + // sizeCtl > 0,当目前 table 未初始化时,sizeCtl 表示初始化容量 + this.sizeCtl = cap; + } + ``` + + ```java + private static final int tableSizeFor(int c) { + int n = c - 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 部分详解了该函数,核心思想就是**把最高位是 1 的位以及右边的位全部置 1**,结果加 1 后就是 2 的 n 次幂 + +* 多个参数构造方法: + + ```java + public ConcurrentHashMap(int initialCapacity, float loadFactor, int concurrencyLevel) { + if (!(loadFactor > 0.0f) || initialCapacity < 0 || concurrencyLevel <= 0) + throw new IllegalArgumentException(); + // 初始容量小于并发级别 + if (initialCapacity < concurrencyLevel) + // 把并发级别赋值给初始容量 + initialCapacity = concurrencyLevel; + // loadFactor 默认是 0.75 + long size = (long)(1.0 + (long)initialCapacity / loadFactor); + int cap = (size >= (long)MAXIMUM_CAPACITY) ? + MAXIMUM_CAPACITY : tableSizeFor((int)size); + // sizeCtl > 0,当目前 table 未初始化时,sizeCtl 表示初始化容量 + this.sizeCtl = cap; + } + ``` + +* 集合构造方法: + + ```java + public ConcurrentHashMap(Map m) { + this.sizeCtl = DEFAULT_CAPACITY; // 默认16 + putAll(m); + } + public void putAll(Map m) { + // 尝试触发扩容 + tryPresize(m.size()); + for (Entry e : m.entrySet()) + putVal(e.getKey(), e.getValue(), false); + } + ``` + + ```java + private final void tryPresize(int size) { + // 扩容为大于 2 倍的最小的 2 的 n 次幂 + int c = (size >= (MAXIMUM_CAPACITY >>> 1)) ? MAXIMUM_CAPACITY : + tableSizeFor(size + (size >>> 1) + 1); + int sc; + while ((sc = sizeCtl) >= 0) { + Node[] tab = table; int n; + // 数组还未初始化,【一般是调用集合构造方法才会成立,put 后调用该方法都是不成立的】 + if (tab == null || (n = tab.length) == 0) { + n = (sc > c) ? sc : c; + if (U.compareAndSwapInt(this, SIZECTL, sc, -1)) { + try { + if (table == tab) { + Node[] nt = (Node[])new Node[n]; + table = nt; + sc = n - (n >>> 2);// 扩容阈值:n - 1/4 n + } + } finally { + sizeCtl = sc; // 扩容阈值赋值给sizeCtl + } + } + } + // 未达到扩容阈值或者数组长度已经大于最大长度 + else if (c <= sc || n >= MAXIMUM_CAPACITY) + break; + // 与 addCount 逻辑相同 + else if (tab == table) { + + } + } + } + ``` + +*** + +#### 成员方法 + +##### 数据访存 + +* tabAt():获取数组某个槽位的**头节点**,类似于数组中的直接寻址 arr[i] + + ```java + // i 是数组索引 + static final Node tabAt(Node[] tab, int i) { + // (i << ASHIFT) + ABASE == ABASE + i * 4 (一个 int 占 4 个字节),这就相当于寻址,替代了乘法 + return (Node)U.getObjectVolatile(tab, ((long)i << ASHIFT) + ABASE); + } + ``` + +* casTabAt():指定数组索引位置修改原值为指定的值 + + ```java + static final boolean casTabAt(Node[] tab, int i, Node c, Node v) { + return U.compareAndSwapObject(tab, ((long)i << ASHIFT) + ABASE, c, v); + } + ``` + +* setTabAt():指定数组索引位置设置值 + + ```java + static final void setTabAt(Node[] tab, int i, Node v) { + U.putObjectVolatile(tab, ((long)i << ASHIFT) + ABASE, v); + } + ``` + +*** + +##### 添加方法 + +```java +public V put(K key, V value) { + // 第三个参数 onlyIfAbsent 为 false 表示哈希表中存在相同的 key 时【用当前数据覆盖旧数据】 + return putVal(key, value, false); +} +``` + +* putVal() + + ```java + final V putVal(K key, V value, boolean onlyIfAbsent) { + // 【ConcurrentHashMap 不能存放 null 值】 + if (key == null || value == null) throw new NullPointerException(); + // 扰动运算,高低位都参与寻址运算 + int hash = spread(key.hashCode()); + // 表示当前 k-v 封装成 node 后插入到指定桶位后,在桶位中的所属链表的下标位置 + int binCount = 0; + // tab 引用当前 map 的数组 table,开始自旋 + for (Node[] tab = table;;) { + // f 表示桶位的头节点,n 表示哈希表数组的长度 + // i 表示 key 通过寻址计算后得到的桶位下标,fh 表示桶位头结点的 hash 值 + Node f; int n, i, fh; + + // 【CASE1】:表示当前 map 中的 table 尚未初始化 + if (tab == null || (n = tab.length) == 0) + //【延迟初始化】 + tab = initTable(); + + // 【CASE2】:i 表示 key 使用【寻址算法】得到 key 对应数组的下标位置,tabAt 获取指定桶位的头结点f + else if ((f = tabAt(tab, i = (n - 1) & hash)) == null) { + // 对应的数组为 null 说明没有哈希冲突,直接新建节点添加到表中 + if (casTabAt(tab, i, null, new Node(hash, key, value, null))) + break; + } + // 【CASE3】:逻辑说明数组已经被初始化,并且当前 key 对应的位置不为 null + // 条件成立表示当前桶位的头结点为 FWD 结点,表示目前 map 正处于扩容过程中 + else if ((fh = f.hash) == MOVED) + // 当前线程【需要去帮助哈希表完成扩容】 + tab = helpTransfer(tab, f); + + // 【CASE4】:哈希表没有在扩容,当前桶位可能是链表也可能是红黑树 + else { + // 当插入 key 存在时,会将旧值赋值给 oldVal 返回 + V oldVal = null; + // 【锁住当前 key 寻址的桶位的头节点】 + synchronized (f) { + // 这里重新获取一下桶的头节点有没有被修改,因为可能被其他线程修改过,这里是线程安全的获取 + if (tabAt(tab, i) == f) { + // 【头节点的哈希值大于 0 说明当前桶位是普通的链表节点】 + if (fh >= 0) { + // 当前的插入操作没出现重复的 key,追加到链表的末尾,binCount表示链表长度 -1 + // 插入的key与链表中的某个元素的 key 一致,变成替换操作,binCount 表示第几个节点冲突 + binCount = 1; + // 迭代循环当前桶位的链表,e 是每次循环处理节点,e 初始是头节点 + for (Node e = f;; ++binCount) { + // 当前循环节点 key + K ek; + // key 的哈希值与当前节点的哈希一致,并且 key 的值也相同 + if (e.hash == hash && + ((ek = e.key) == key || + (ek != null && key.equals(ek)))) { + // 把当前节点的 value 赋值给 oldVal + oldVal = e.val; + // 允许覆盖 + if (!onlyIfAbsent) + // 新数据覆盖旧数据 + e.val = value; + // 跳出循环 + break; + } + Node pred = e; + // 如果下一个节点为空,把数据封装成节点插入链表尾部,【binCount 代表长度 - 1】 + if ((e = e.next) == null) { + pred.next = new Node(hash, key, + value, null); + break; + } + } + } + // 当前桶位头节点是红黑树 + else if (f instanceof TreeBin) { + Node p; + binCount = 2; + if ((p = ((TreeBin)f).putTreeVal(hash, key, + value)) != null) { + oldVal = p.val; + if (!onlyIfAbsent) + p.val = value; + } + } + } + } + + // 条件成立说明当前是链表或者红黑树 + if (binCount != 0) { + // 如果 binCount >= 8 表示处理的桶位一定是链表,说明长度是 9 + if (binCount >= TREEIFY_THRESHOLD) + // 树化 + treeifyBin(tab, i); + if (oldVal != null) + return oldVal; + break; + } + } + } + // 统计当前 table 一共有多少数据,判断是否达到扩容阈值标准,触发扩容 + // binCount = 0 表示当前桶位为 null,node 可以直接放入,2 表示当前桶位已经是红黑树 + addCount(1L, binCount); + return null; + } + ``` + +* spread():扰动函数 + + 将 hashCode 无符号右移 16 位,高 16bit 和低 16bit 做异或,最后与 HASH_BITS 相与变成正数,**与树化节点和转移节点区分**,把高低位都利用起来减少哈希冲突,保证散列的均匀性 + + ```java + static final int spread(int h) { + return (h ^ (h >>> 16)) & HASH_BITS; // 0111 1111 1111 1111 1111 1111 1111 1111 + } + ``` + +* initTable():初始化数组,延迟初始化 + + ```java + private final Node[] initTable() { + // tab 引用 map.table,sc 引用 sizeCtl + Node[] tab; int sc; + // table 尚未初始化,开始自旋 + while ((tab = table) == null || tab.length == 0) { + // sc < 0 说明 table 正在初始化或者正在扩容,当前线程可以释放 CPU 资源 + if ((sc = sizeCtl) < 0) + Thread.yield(); + // sizeCtl 设置为 -1,相当于加锁,【设置的是 SIZECTL 位置的数据】, + // 因为是 sizeCtl 是基本类型,不是引用类型,所以 sc 保存的是数据的副本 + else if (U.compareAndSwapInt(this, SIZECTL, sc, -1)) { + try { + // 线程安全的逻辑,再进行一次判断 + if ((tab = table) == null || tab.length == 0) { + // sc > 0 创建 table 时使用 sc 为指定大小,否则使用 16 默认值 + int n = (sc > 0) ? sc : DEFAULT_CAPACITY; + // 创建哈希表数组 + Node[] nt = (Node[])new Node[n]; + table = tab = nt; + // 扩容阈值,n >>> 2 => 等于 1/4 n ,n - (1/4)n = 3/4 n => 0.75 * n + sc = n - (n >>> 2); + } + } finally { + // 解锁,把下一次扩容的阈值赋值给 sizeCtl + sizeCtl = sc; + } + break; + } + } + return tab; + } + ``` + +* treeifyBin():树化方法 + + ```java + private final void treeifyBin(Node[] tab, int index) { + Node b; int n, sc; + if (tab != null) { + // 条件成立:【说明当前 table 数组长度未达到 64,此时不进行树化操作,进行扩容操作】 + if ((n = tab.length) < MIN_TREEIFY_CAPACITY) + // 当前容量的 2 倍 + tryPresize(n << 1); + + // 条件成立:说明当前桶位有数据,且是普通 node 数据。 + else if ((b = tabAt(tab, index)) != null && b.hash >= 0) { + // 【树化加锁】 + synchronized (b) { + // 条件成立:表示加锁没问题。 + if (tabAt(tab, index) == b) { + TreeNode hd = null, tl = null; + for (Node e = b; e != null; e = e.next) { + TreeNode p = new TreeNode(e.hash, e.key, e.val,null, null); + if ((p.prev = tl) == null) + hd = p; + else + tl.next = p; + tl = p; + } + setTabAt(tab, index, new TreeBin(hd)); + } + } + } + } + } + ``` + +* addCount():添加计数,**代表哈希表中的数据总量** + + ```java + private final void addCount(long x, int check) { + // 【上面这部分的逻辑就是 LongAdder 的累加逻辑】 + CounterCell[] as; long b, s; + // 判断累加数组 cells 是否初始化,没有就去累加 base 域,累加失败进入条件内逻辑 + if ((as = counterCells) != null || + !U.compareAndSwapLong(this, BASECOUNT, b = baseCount, s = b + x)) { + CounterCell a; long v; int m; + // true 未竞争,false 发生竞争 + boolean uncontended = true; + // 判断 cells 是否被其他线程初始化 + if (as == null || (m = as.length - 1) < 0 || + // 前面的条件为 fasle 说明 cells 被其他线程初始化,通过 hash 寻址对应的槽位 + (a = as[ThreadLocalRandom.getProbe() & m]) == null || + // 尝试去对应的槽位累加,累加失败进入 fullAddCount 进行重试或者扩容 + !(uncontended = U.compareAndSwapLong(a, CELLVALUE, v = a.value, v + x))) { + // 与 Striped64#longAccumulate 方法相同 + fullAddCount(x, uncontended); + return; + } + // 表示当前桶位是 null,或者一个链表节点 + if (check <= 1) + return; + // 【获取当前散列表元素个数】,这是一个期望值 + s = sumCount(); + } + + // 表示一定 【是一个 put 操作调用的 addCount】 + if (check >= 0) { + Node[] tab, nt; int n, sc; + + // 条件一:true 说明当前 sizeCtl 可能为一个负数表示正在扩容中,或者 sizeCtl 是一个正数,表示扩容阈值 + // false 表示哈希表的数据的数量没达到扩容条件 + // 然后判断当前 table 数组是否初始化了,当前 table 长度是否小于最大值限制,就可以进行扩容 + while (s >= (long)(sc = sizeCtl) && (tab = table) != null && + (n = tab.length) < MAXIMUM_CAPACITY) { + // 16 -> 32 扩容 标识为:1000 0000 0001 1011,【负数,扩容批次唯一标识戳】 + int rs = resizeStamp(n); + + // 表示当前 table,【正在扩容】,sc 高 16 位是扩容标识戳,低 16 位是线程数 + 1 + if (sc < 0) { + // 条件一:判断扩容标识戳是否一样,fasle 代表一样 + // 勘误两个条件: + // 条件二是:sc == (rs << 16 ) + 1,true 代表扩容完成,因为低16位是1代表没有线程扩容了 + // 条件三是:sc == (rs << 16) + MAX_RESIZERS,判断是否已经超过最大允许的并发扩容线程数 + // 条件四:判断新表的引用是否是 null,代表扩容完成 + // 条件五:【扩容是从高位到低位转移】,transferIndex < 0 说明没有区间需要扩容了 + if ((sc >>> RESIZE_STAMP_SHIFT) != rs || sc == rs + 1 || + sc == rs + MAX_RESIZERS || (nt = nextTable) == null || + transferIndex <= 0) + break; + + // 设置当前线程参与到扩容任务中,将 sc 低 16 位值加 1,表示多一个线程参与扩容 + // 设置失败其他线程或者 transfer 内部修改了 sizeCtl 值 + if (U.compareAndSwapInt(this, SIZECTL, sc, sc + 1)) + //【协助扩容线程】,持有nextTable参数 + transfer(tab, nt); + } + // 逻辑到这说明当前线程是触发扩容的第一个线程,线程数量 + 2 + // 1000 0000 0001 1011 0000 0000 0000 0000 +2 => 1000 0000 0001 1011 0000 0000 0000 0010 + else if (U.compareAndSwapInt(this, SIZECTL, sc,(rs << RESIZE_STAMP_SHIFT) + 2)) + //【触发扩容条件的线程】,不持有 nextTable,初始线程会新建 nextTable + transfer(tab, null); + s = sumCount(); + } + } + } + ``` + +* resizeStamp():扩容标识符,**每次扩容都会产生一个,不是每个线程都产生**,16 扩容到 32 产生一个,32 扩容到 64 产生一个 + + ```java + /** + * 扩容的标识符 + * 16 -> 32 从16扩容到32 + * numberOfLeadingZeros(16) => 1 0000 => 32 - 5 = 27 => 0000 0000 0001 1011 + * (1 << (RESIZE_STAMP_BITS - 1)) => 1000 0000 0000 0000 => 32768 + * --------------------------------------------------------------- + * 0000 0000 0001 1011 + * 1000 0000 0000 0000 + * 1000 0000 0001 1011 + * 永远是负数 + */ + static final int resizeStamp(int n) { + // 或运算 + return Integer.numberOfLeadingZeros(n) | (1 << (RESIZE_STAMP_BITS - 1)); // (16 -1 = 15) + } + ``` + +*** + +##### 扩容方法 + +扩容机制: + +* 当链表中元素个数超过 8 个,数组的大小还未超过 64 时,此时进行数组的扩容,如果超过则将链表转化成红黑树 +* put 数据后调用 addCount() 方法,判断当前哈希表的容量超过阈值 sizeCtl,超过进行扩容 +* 增删改线程发现其他线程正在扩容,帮其扩容 + +常见方法: + +* transfer():数据转移到新表中,完成扩容 + + ```java + private final void transfer(Node[] tab, Node[] nextTab) { + // n 表示扩容之前 table 数组的长度 + int n = tab.length, stride; + // stride 表示分配给线程任务的步长,默认就是 16 + if ((stride = (NCPU > 1) ? (n >>> 3) / NCPU : n) < MIN_TRANSFER_STRIDE) + stride = MIN_TRANSFER_STRIDE; + // 如果当前线程为触发本次扩容的线程,需要做一些扩容准备工作,【协助线程不做这一步】 + if (nextTab == null) { + try { + // 创建一个容量是之前【二倍的 table 数组】 + Node[] nt = (Node[])new Node[n << 1]; + nextTab = nt; + } catch (Throwable ex) { + sizeCtl = Integer.MAX_VALUE; + return; + } + // 把新表赋值给对象属性 nextTable,方便其他线程获取新表 + nextTable = nextTab; + // 记录迁移数据整体位置的一个标记,transferIndex 计数从1开始不是 0,所以这里是长度,不是长度-1 + transferIndex = n; + } + // 新数组的长度 + int nextn = nextTab.length; + // 当某个桶位数据处理完毕后,将此桶位设置为 fwd 节点,其它写线程或读线程看到后,可以从中获取到新表 + ForwardingNode fwd = new ForwardingNode(nextTab); + // 推进标记 + boolean advance = true; + // 完成标记 + boolean finishing = false; + + // i 表示分配给当前线程任务,执行到的桶位 + // bound 表示分配给当前线程任务的下界限制,因为是倒序迁移,16 迁移完 迁移 15,15完成去迁移14 + for (int i = 0, bound = 0;;) { + Node f; int fh; + + // 给当前线程【分配任务区间】 + while (advance) { + // 分配任务的开始下标,分配任务的结束下标 + int nextIndex, nextBound; + + // --i 让当前线程处理下一个索引,true说明当前的迁移任务尚未完成,false说明线程已经完成或者还未分配 + if (--i >= bound || finishing) + advance = false; + // 迁移的开始下标,小于0说明没有区间需要迁移了,设置当前线程的 i 变量为 -1 跳出循环 + else if ((nextIndex = transferIndex) <= 0) { + i = -1; + advance = false; + } + // 逻辑到这说明还有区间需要分配,然后给当前线程分配任务, + else if (U.compareAndSwapInt(this, TRANSFERINDEX, nextIndex, + // 判断区间是否还够一个步长,不够就全部分配 + nextBound = (nextIndex > stride ? nextIndex - stride : 0))) { + // 当前线程的结束下标 + bound = nextBound; + // 当前线程的开始下标,上一个线程结束的下标的下一个索引就是这个线程开始的下标 + i = nextIndex - 1; + // 任务分配结束,跳出循环执行迁移操作 + advance = false; + } + } + + // 【分配完成,开始数据迁移操作】 + // 【CASE1】:i < 0 成立表示当前线程未分配到任务,或者任务执行完了 + if (i < 0 || i >= n || i + n >= nextn) { + int sc; + // 如果迁移完成 + if (finishing) { + nextTable = null; // help GC + table = nextTab; // 新表赋值给当前对象 + sizeCtl = (n << 1) - (n >>> 1);// 扩容阈值为 2n - n/2 = 3n/2 = 0.75*(2n) + return; + } + // 当前线程完成了分配的任务区间,可以退出,先把 sizeCtl 赋值给 sc 保留 + if (U.compareAndSwapInt(this, SIZECTL, sc = sizeCtl, sc - 1)) { + // 判断当前线程是不是最后一个线程,不是的话直接 return, + if ((sc - 2) != resizeStamp(n) << RESIZE_STAMP_SHIFT) + return; + // 所以最后一个线程退出的时候,sizeCtl 的低 16 位为 1 + finishing = advance = true; + // 【这里表示最后一个线程需要重新检查一遍是否有漏掉的区间】 + i = n; + } + } + + // 【CASE2】:当前桶位未存放数据,只需要将此处设置为 fwd 节点即可。 + else if ((f = tabAt(tab, i)) == null) + advance = casTabAt(tab, i, null, fwd); + // 【CASE3】:说明当前桶位已经迁移过了,当前线程不用再处理了,直接处理下一个桶位即可 + else if ((fh = f.hash) == MOVED) + advance = true; + // 【CASE4】:当前桶位有数据,而且 node 节点不是 fwd 节点,说明这些数据需要迁移 + else { + // 【锁住头节点】 + synchronized (f) { + // 二次检查,防止头节点已经被修改了,因为这里才是线程安全的访问 + if (tabAt(tab, i) == f) { + // 【迁移数据的逻辑,和 HashMap 相似】 + + // ln 表示低位链表引用 + // hn 表示高位链表引用 + Node ln, hn; + // 哈希 > 0 表示当前桶位是链表桶位 + if (fh >= 0) { + // 和 HashMap 的处理方式一致,与老数组长度相与,16 是 10000 + // 判断对应的 1 的位置上是 0 或 1 分成高低位链表 + int runBit = fh & n; + Node lastRun = f; + // 遍历链表,寻找【逆序看】最长的对应位相同的链表,看下面的图更好的理解 + for (Node p = f.next; p != null; p = p.next) { + // 将当前节点的哈希 与 n + int b = p.hash & n; + // 如果当前值与前面节点的值 对应位 不同,则修改 runBit,把 lastRun 指向当前节点 + if (b != runBit) { + runBit = b; + lastRun = p; + } + } + // 判断筛选出的链表是低位的还是高位的 + if (runBit == 0) { + ln = lastRun; // ln 指向该链表 + hn = null; // hn 为 null + } + // 说明 lastRun 引用的链表为高位链表,就让 hn 指向高位链表头节点 + else { + hn = lastRun; + ln = null; + } + // 从头开始遍历所有的链表节点,迭代到 p == lastRun 节点跳出循环 + for (Node p = f; p != lastRun; p = p.next) { + int ph = p.hash; K pk = p.key; V pv = p.val; + if ((ph & n) == 0) + // 【头插法】,从右往左看,首先 ln 指向的是上一个节点, + // 所以这次新建的节点的 next 指向上一个节点,然后更新 ln 的引用 + ln = new Node(ph, pk, pv, ln); + else + hn = new Node(ph, pk, pv, hn); + } + // 高低位链设置到新表中的指定位置 + setTabAt(nextTab, i, ln); + setTabAt(nextTab, i + n, hn); + // 老表中的该桶位设置为 fwd 节点 + setTabAt(tab, i, fwd); + advance = true; + } + // 条件成立:表示当前桶位是 红黑树结点 + else if (f instanceof TreeBin) { + TreeBin t = (TreeBin)f; + TreeNode lo = null, loTail = null; + TreeNode hi = null, hiTail = null; + int lc = 0, hc = 0; + // 迭代 TreeBin 中的双向链表,从头结点至尾节点 + for (Node e = t.first; e != null; e = e.next) { + // 迭代的当前元素的 hash + int h = e.hash; + TreeNode p = new TreeNode + (h, e.key, e.val, null, null); + // 条件成立表示当前循环节点属于低位链节点 + if ((h & n) == 0) { + if ((p.prev = loTail) == null) + lo = p; + else + //【尾插法】 + loTail.next = p; + // loTail 指向尾节点 + loTail = p; + ++lc; + } + else { + if ((p.prev = hiTail) == null) + hi = p; + else + hiTail.next = p; + hiTail = p; + ++hc; + } + } + // 拆成的高位低位两个链,【判断是否需要需要转化为链表】,反之保持树化 + ln = (lc <= UNTREEIFY_THRESHOLD) ? untreeify(lo) : + (hc != 0) ? new TreeBin(lo) : t; + hn = (hc <= UNTREEIFY_THRESHOLD) ? untreeify(hi) : + (lc != 0) ? new TreeBin(hi) : t; + setTabAt(nextTab, i, ln); + setTabAt(nextTab, i + n, hn); + setTabAt(tab, i, fwd); + advance = true; + } + } + } + } + } + } + ``` + + 链表处理的 LastRun 机制,**可以减少节点的创建** + + ![](https://seazean.oss-cn-beijing.aliyuncs.com/img/Java/JUC-ConcurrentHashMap-LastRun机制.png) + +* helpTransfer():帮助扩容机制 + + ```java + final Node[] helpTransfer(Node[] tab, Node f) { + Node[] nextTab; int sc; + // 数组不为空,节点是转发节点,获取转发节点指向的新表开始协助主线程扩容 + if (tab != null && (f instanceof ForwardingNode) && + (nextTab = ((ForwardingNode)f).nextTable) != null) { + // 扩容标识戳 + int rs = resizeStamp(tab.length); + // 判断数据迁移是否完成,迁移完成会把 新表赋值给 nextTable 属性 + while (nextTab == nextTable && table == tab && (sc = sizeCtl) < 0) { + if ((sc >>> RESIZE_STAMP_SHIFT) != rs || sc == rs + 1 || + sc == rs + MAX_RESIZERS || transferIndex <= 0) + break; + // 设置扩容线程数量 + 1 + if (U.compareAndSwapInt(this, SIZECTL, sc, sc + 1)) { + // 协助扩容 + transfer(tab, nextTab); + break; + } + } + return nextTab; + } + return table; + } + ``` + +*** + +##### 获取方法 + +ConcurrentHashMap 使用 get() 方法获取指定 key 的数据 + +* get():获取指定数据的方法 + + ```java + public V get(Object key) { + Node[] tab; Node e, p; int n, eh; K ek; + // 扰动运算,获取 key 的哈希值 + int h = spread(key.hashCode()); + // 判断当前哈希表的数组是否初始化 + if ((tab = table) != null && (n = tab.length) > 0 && + // 如果 table 已经初始化,进行【哈希寻址】,映射到数组对应索引处,获取该索引处的头节点 + (e = tabAt(tab, (n - 1) & h)) != null) { + // 对比头结点 hash 与查询 key 的 hash 是否一致 + if ((eh = e.hash) == h) { + // 进行值的判断,如果成功就说明当前节点就是要查询的节点,直接返回 + if ((ek = e.key) == key || (ek != null && key.equals(ek))) + return e.val; + } + // 当前槽位的【哈希值小于0】说明是红黑树节点或者是正在扩容的 fwd 节点 + else if (eh < 0) + return (p = e.find(h, key)) != null ? p.val : null; + // 当前桶位是【链表】,循环遍历查找 + while ((e = e.next) != null) { + if (e.hash == h && + ((ek = e.key) == key || (ek != null && key.equals(ek)))) + return e.val; + } + } + return null; + } + ``` + +* ForwardingNode#find:转移节点的查找方法 + + ```java + Node find(int h, Object k) { + // 获取新表的引用 + outer: for (Node[] tab = nextTable;;) { + // e 表示在扩容而创建新表使用寻址算法得到的桶位头结点,n 表示为扩容而创建的新表的长度 + Node e; int n; + + if (k == null || tab == null || (n = tab.length) == 0 || + // 在新表中重新定位 hash 对应的头结点,表示在 oldTable 中对应的桶位在迁移之前就是 null + (e = tabAt(tab, (n - 1) & h)) == null) + return null; + + for (;;) { + int eh; K ek; + // 【哈希相同值也相同】,表示新表当前命中桶位中的数据,即为查询想要数据 + if ((eh = e.hash) == h && ((ek = e.key) == k || (ek != null && k.equals(ek)))) + return e; + + // eh < 0 说明当前新表中该索引的头节点是 TreeBin 类型,或者是 FWD 类型 + if (eh < 0) { + // 在并发很大的情况下新扩容的表还没完成可能【再次扩容】,在此方法处再次拿到 FWD 类型 + if (e instanceof ForwardingNode) { + // 继续获取新的 fwd 指向的新数组的地址,递归了 + tab = ((ForwardingNode)e).nextTable; + continue outer; + } + else + // 说明此桶位为 TreeBin 节点,使用TreeBin.find 查找红黑树中相应节点。 + return e.find(h, k); + } + + // 逻辑到这说明当前桶位是链表,将当前元素指向链表的下一个元素,判断当前元素的下一个位置是否为空 + if ((e = e.next) == null) + // 条件成立说明迭代到链表末尾,【未找到对应的数据,返回 null】 + return null; + } + } + } + ``` + +**** + +##### 删除方法 + +* remove():删除指定元素 + + ```java + public V remove(Object key) { + return replaceNode(key, null, null); + } + ``` + +* replaceNode():替代指定的元素,会协助扩容,**增删改(写)都会协助扩容,查询(读)操作不会**,因为读操作不涉及加锁 + + ```java + final V replaceNode(Object key, V value, Object cv) { + // 计算 key 扰动运算后的 hash + int hash = spread(key.hashCode()); + // 开始自旋 + for (Node[] tab = table;;) { + Node f; int n, i, fh; + + // 【CASE1】:table 还未初始化或者哈希寻址的数组索引处为 null,直接结束自旋,返回 null + if (tab == null || (n = tab.length) == 0 || (f = tabAt(tab, i = (n - 1) & hash)) == null) + break; + // 【CASE2】:条件成立说明当前 table 正在扩容,【当前是个写操作,所以当前线程需要协助 table 完成扩容】 + else if ((fh = f.hash) == MOVED) + tab = helpTransfer(tab, f); + // 【CASE3】:当前桶位可能是 链表 也可能是 红黑树 + else { + // 保留替换之前数据引用 + V oldVal = null; + // 校验标记 + boolean validated = false; + // 【加锁当前桶位头结点】,加锁成功之后会进入代码块 + synchronized (f) { + // 双重检查 + if (tabAt(tab, i) == f) { + // 说明当前节点是链表节点 + if (fh >= 0) { + validated = true; + //遍历所有的节点 + for (Node e = f, pred = null;;) { + K ek; + // hash 和值都相同,定位到了具体的节点 + if (e.hash == hash && + ((ek = e.key) == key || + (ek != null && key.equals(ek)))) { + // 当前节点的value + V ev = e.val; + if (cv == null || cv == ev || + (ev != null && cv.equals(ev))) { + // 将当前节点的值 赋值给 oldVal 后续返回会用到 + oldVal = ev; + if (value != null) // 条件成立说明是替换操作 + e.val = value; + else if (pred != null) // 非头节点删除操作,断开链表 + pred.next = e.next; + else + // 说明当前节点即为头结点,将桶位头节点设置为以前头节点的下一个节点 + setTabAt(tab, i, e.next); + } + break; + } + pred = e; + if ((e = e.next) == null) + break; + } + } + // 说明是红黑树节点 + else if (f instanceof TreeBin) { + validated = true; + TreeBin t = (TreeBin)f; + TreeNode r, p; + if ((r = t.root) != null && + (p = r.findTreeNode(hash, key, null)) != null) { + V pv = p.val; + if (cv == null || cv == pv || + (pv != null && cv.equals(pv))) { + oldVal = pv; + // 条件成立说明替换操作 + if (value != null) + p.val = value; + // 删除操作 + else if (t.removeTreeNode(p)) + setTabAt(tab, i, untreeify(t.first)); + } + } + } + } + } + // 其他线程修改过桶位头结点时,当前线程 sync 头结点锁错对象,validated 为 false,会进入下次 for 自旋 + if (validated) { + if (oldVal != null) { + // 替换的值为 null,【说明当前是一次删除操作,更新当前元素个数计数器】 + if (value == null) + addCount(-1L, -1); + return oldVal; + } + break; + } + } + } + return null; + } + ``` + +参考视频: + +*** + +#### JDK7原理 + +ConcurrentHashMap 对锁粒度进行了优化,**分段锁技术**,将整张表分成了多个数组(Segment),每个数组又是一个类似 HashMap 数组的结构。允许多个修改操作并发进行,Segment 是一种可重入锁,继承 ReentrantLock,并发时锁住的是每个 Segment,其他 Segment 还是可以操作的,这样不同 Segment 之间就可以实现并发,大大提高效率。 + +底层结构: **Segment 数组 + HashEntry 数组 + 链表**(数组 + 链表是 HashMap 的结构) + +* 优点:如果多个线程访问不同的 segment,实际是没有冲突的,这与 JDK8 中是类似的 + +* 缺点:Segments 数组默认大小为16,这个容量初始化指定后就不能改变了,并且不是懒惰初始化 + + ![]( 1.7底层结构.png) + +*** + +### CopyOnWrite + +#### 原理分析 + +CopyOnWriteArrayList 采用了**写入时拷贝**的思想,增删改操作会将底层数组拷贝一份,在新数组上执行操作,不影响其它线程的**并发读,读写分离** + +CopyOnWriteArraySet 底层对 CopyOnWriteArrayList 进行了包装,装饰器模式 + +```java +public CopyOnWriteArraySet() { + al = new CopyOnWriteArrayList(); +} +``` + +* 存储结构: + + ```java + private transient volatile Object[] array; // volatile 保证了读写线程之间的可见性 + ``` + +* 全局锁:保证线程的执行安全 + + ```java + final transient ReentrantLock lock = new ReentrantLock(); + ``` + +* 新增数据:需要加锁,**创建新的数组操作** + + ```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; + // 替换旧的数组,【这个操作以后,其他线程获取数组就是获取的新数组了】 + setArray(newElements); + return true; + } finally { + lock.unlock(); + } + } + ``` + +* 读操作:不加锁,**在原数组上操作** + + ```java + public E get(int index) { + return get(getArray(), index); + } + private E get(Object[] a, int index) { + return (E) a[index]; + } + ``` + + 适合读多写少的应用场景 + +* 迭代器:CopyOnWriteArrayList 在返回迭代器时,**创建一个内部数组当前的快照(引用)**,即使其他线程替换了原始数组,迭代器遍历的快照依然引用的是创建快照时的数组,所以这种实现方式也存在一定的数据延迟性,对其他线程并行添加的数据不可见 + + ```java + public Iterator iterator() { + // 获取到数组引用,整个遍历的过程该数组都不会变,一直引用的都是老数组, + return new COWIterator(getArray(), 0); + } + + // 迭代器会创建一个底层array的快照,故主类的修改不影响该快照 + static final class COWIterator implements ListIterator { + // 内部数组快照 + private final Object[] snapshot; + + private COWIterator(Object[] elements, int initialCursor) { + cursor = initialCursor; + // 数组的引用在迭代过程不会改变 + snapshot = elements; + } + // 【不支持写操作】,因为是在快照上操作,无法同步回去 + public void remove() { + throw new UnsupportedOperationException(); + } + } + ``` + +*** + +#### 弱一致性 + +数据一致性就是读到最新更新的数据: + +* 强一致性:当更新操作完成之后,任何多个后续进程或者线程的访问都会返回最新的更新过的值 + +* 弱一致性:系统并不保证进程或者线程的访问都会返回最新的更新过的值,也不会承诺多久之后可以读到 + + + +| 时间点 | 操作 | +| ------ | ---------------------------- | +| 1 | Thread-0 getArray() | +| 2 | Thread-1 getArray() | +| 3 | Thread-1 setArray(arrayCopy) | +| 4 | Thread-0 array[index] | + +Thread-0 读到了脏数据 + +不一定弱一致性就不好 + +* 数据库的**事务隔离级别**就是弱一致性的表现 +* 并发高和一致性是矛盾的,需要权衡 + +*** + +#### 安全失败 + +在 java.util 包的集合类就都是快速失败的,而 java.util.concurrent 包下的类都是安全失败 + +* 快速失败:在 A 线程使用**迭代器**对集合进行遍历的过程中,此时 B 线程对集合进行修改(增删改),或者 A 线程在遍历过程中对集合进行修改,都会导致 A 线程抛出 ConcurrentModificationException 异常 + + * AbstractList 类中的成员变量 modCount,用来记录 List 结构发生变化的次数,**结构发生变化**是指添加或者删除至少一个元素的操作,或者是调整内部数组的大小,仅仅设置元素的值不算结构发生变化 + * 在进行序列化或者迭代等操作时,需要比较操作前后 modCount 是否改变,如果改变了抛出 CME 异常 + +* 安全失败:采用安全失败机制的集合容器,在**迭代器**遍历时直接在原集合数组内容上访问,但其他线程的增删改都会新建数组进行修改,就算修改了集合底层的数组容器,迭代器依然引用着以前的数组(**快照思想**),所以不会出现异常 + + ConcurrentHashMap 不会出现并发时的迭代异常,因为在迭代过程中 CHM 的迭代器并没有判断结构的变化,迭代器还可以根据迭代的节点状态去寻找并发扩容时的新表进行迭代 + + ```java + ConcurrentHashMap map = new ConcurrentHashMap(); + // KeyIterator + Iterator iterator = map.keySet().iterator(); + ``` + + ```java + Traverser(Node[] tab, int size, int index, int limit) { + // 引用还是原来集合的 Node 数组,所以其他线程对数据的修改是可见的 + this.tab = tab; + this.baseSize = size; + this.baseIndex = this.index = index; + this.baseLimit = limit; + this.next = null; + } + ``` + + ```java + public final boolean hasNext() { return next != null; } + public final K next() { + Node p; + if ((p = next) == null) + throw new NoSuchElementException(); + K k = p.key; + lastReturned = p; + // 在方法中进行下一个节点的获取,会进行槽位头节点的状态判断 + advance(); + return k; + } + ``` + +*** + +### Collections + +Collections类是用来操作集合的工具类,提供了集合转换成线程安全的方法: + +```java + public static Collection synchronizedCollection(Collection c) { + return new SynchronizedCollection<>(c); + } +public static Map synchronizedMap(Map m) { + return new SynchronizedMap<>(m); +} +``` + +源码:底层也是对方法进行加锁 + +```java +public boolean add(E e) { + synchronized (mutex) {return c.add(e);} +} +``` + +*** + +### SkipListMap + +#### 底层结构 + +跳表 SkipList 是一个**有序的链表**,默认升序,底层是链表加多级索引的结构。跳表可以对元素进行快速查询,类似于平衡树,是一种利用空间换时间的算法 + +对于单链表,即使链表是有序的,如果查找数据也只能从头到尾遍历链表,所以采用链表上建索引的方式提高效率,跳表的查询时间复杂度是 **O(logn)**,空间复杂度 O(n) + +ConcurrentSkipListMap 提供了一种线程安全的并发访问的排序映射表,内部是跳表结构实现,通过 CAS + volatile 保证线程安全 + +平衡树和跳表的区别: + +* 对平衡树的插入和删除往往很可能导致平衡树进行一次全局的调整;而对跳表的插入和删除,**只需要对整个结构的局部进行操作** +* 在高并发的情况下,保证整个平衡树的线程安全需要一个全局锁;对于跳表则只需要部分锁,拥有更好的性能 + +![](https://seazean.oss-cn-beijing.aliyuncs.com/img/Java/JUC-ConcurrentSkipListMap数据结构.png) + +BaseHeader 存储数据,headIndex 存储索引,纵向上**所有索引都指向链表最下面的节点** + +*** + +#### 成员变量 + +* 标识索引头节点位置 + + ```java + private static final Object BASE_HEADER = new Object(); + ``` + +* 跳表的顶层索引 + + ```java + private transient volatile HeadIndex head; + ``` + +* 比较器,为 null 则使用自然排序 + + ```java + final Comparator comparator; + ``` + +* Node 节点 + + ```java + static final class Node{ + final K key; // key 是 final 的, 说明节点一旦定下来, 除了删除, 一般不会改动 key + volatile Object value; // 对应的 value + volatile Node next; // 下一个节点,单向链表 + } + ``` + +* 索引节点 Index,只有向下和向右的指针 + + ```java + static class Index{ + final Node node; // 索引指向的节点,每个都会指向数据节点 + final Index down; // 下边level层的Index,分层索引 + volatile Index right; // 右边的Index,单向 + + // 在 index 本身和 succ 之间插入一个新的节点 newSucc + final boolean link(Index succ, Index newSucc){ + Node n = node; + newSucc.right = succ; + // 把当前节点的右指针从 succ 改为 newSucc + return n.value != null && casRight(succ, newSucc); + } + + // 断开当前节点和 succ 节点,将当前的节点 index 设置其的 right 为 succ.right,就是把 succ 删除 + final boolean unlink(Index succ){ + return node.value != null && casRight(succ, succ.right); + } + } + ``` + +* 头索引节点 HeadIndex + + ```java + static final class HeadIndex extends Index { + final int level; // 表示索引层级,所有的 HeadIndex 都指向同一个 Base_header 节点 + HeadIndex(Node node, Index down, Index right, int level) { + super(node, down, right); + this.level = level; + } + } + ``` + +*** + +#### 成员方法 + +##### 其他方法 + +* 构造方法: + + ```java + public ConcurrentSkipListMap() { + this.comparator = null; // comparator 为 null,使用 key 的自然序,如字典序 + initialize(); + } + ``` + + ```java + private void initialize() { + keySet = null; + entrySet = null; + values = null; + descendingMap = null; + // 初始化索引头节点,Node 的 key 为 null,value 为 BASE_HEADER 对象,下一个节点为 null + // head 的分层索引 down 为 null,链表的后续索引 right 为 null,层级 level 为第 1 层 + head = new HeadIndex(new Node(null, BASE_HEADER, null), null, null, 1); + } + ``` + +* cpr:排序 + + ```java + // x 是比较者,y 是被比较者,比较者大于被比较者 返回正数,小于返回负数,相等返回 0 + static final int cpr(Comparator c, Object x, Object y) { + return (c != null) ? c.compare(x, y) : ((Comparable)x).compareTo(y); + } + ``` + +*** + +##### 添加方法 + +* findPredecessor():寻找前置节点 + + 从最上层的头索引开始向右查找(链表的后续索引),如果后续索引的节点的 key 大于要查找的 key,则头索引移到下层链表,在下层链表查找,以此反复,一直查找到没有下层的分层索引为止,返回该索引的节点。如果后续索引的节点的 key 小于要查找的 key,则在该层链表中向后查找。由于查找的 key 可能永远大于索引节点的 key,所以只能找到目标的前置索引节点。如果遇到空值索引的存在,通过 CAS 来断开索引 + + ```java + private Node findPredecessor(Object key, Comparator cmp) { + if (key == null) + throw new NullPointerException(); // don't postpone errors + for (;;) { + // 1.初始数据 q 是 head,r 是最顶层 h 的右 Index 节点 + for (Index q = head, r = q.right, d;;) { + // 2.右索引节点不为空,则进行向下查找 + if (r != null) { + Node n = r.node; + K k = n.key; + // 3.n.value 为 null 说明节点 n 正在删除的过程中,此时【当前线程帮其删除索引】 + if (n.value == null) { + // 在 index 层直接删除 r 索引节点 + if (!q.unlink(r)) + // 删除失败重新从 head 节点开始查找,break 一个 for 到步骤 1,又从初始值开始 + break; + + // 删除节点 r 成功,获取新的 r 节点, + r = q.right; + // 回到步骤 2,还是从这层索引开始向右遍历 + continue; + } + // 4.若参数 key > r.node.key,则继续向右遍历, continue 到步骤 2 处获取右节点 + // 若参数 key < r.node.key,说明需要进入下层索引,到步骤 5 + if (cpr(cmp, key, k) > 0) { + q = r; + r = r.right; + continue; + } + } + // 5.先让 d 指向 q 的下一层,判断是否是 null,是则说明已经到了数据层,也就是第一层 + if ((d = q.down) == null) + return q.node; + // 6.未到数据层, 进行重新赋值向下扫描 + q = d; // q 指向 d + r = d.right;// r 指向 q 的后续索引节点,此时(q.key < key < r.key) + } + } + } + ``` + + ![](https://seazean.oss-cn-beijing.aliyuncs.com/img/Java/JUC-ConcurrentSkipListMap-Put流程.png) + +* put():添加数据 + + ```java + public V put(K key, V value) { + // 非空判断,value不能为空 + if (value == null) + throw new NullPointerException(); + return doPut(key, value, false); + } + ``` + + ```java + private V doPut(K key, V value, boolean onlyIfAbsent) { + Node z; + // 非空判断,key 不能为空 + if (key == null) + throw new NullPointerException(); + Comparator cmp = comparator; + // outer 循环,【把待插入数据插入到数据层的合适的位置,并在扫描过程中处理已删除(value = null)的数据】 + outer: for (;;) { + //0.for (;;) + //1.将 key 对应的前继节点找到, b 为前继节点,是数据层的, n 是前继节点的 next, + // 若没发生条件竞争,最终 key 在 b 与 n 之间 (找到的 b 在 base_level 上) + for (Node b = findPredecessor(key, cmp), n = b.next;;) { + // 2.n 不为 null 说明 b 不是链表的最后一个节点 + if (n != null) { + Object v; int c; + // 3.获取 n 的右节点 + Node f = n.next; + // 4.条件竞争,并发下其他线程在 b 之后插入节点或直接删除节点 n, break 到步骤 0 + if (n != b.next) + break; + // 若节点 n 已经删除, 则调用 helpDelete 进行【帮助删除节点】 + if ((v = n.value) == null) { + n.helpDelete(b, f); + break; + } + // 5.节点 b 被删除中,则 break 到步骤 0, + // 【调用findPredecessor帮助删除index层的数据, node层的数据会通过helpDelete方法进行删除】 + if (b.value == null || v == n) + break; + // 6.若 key > n.key,则进行向后扫描 + // 若 key < n.key,则证明 key 应该存储在 b 和 n 之间 + if ((c = cpr(cmp, key, n.key)) > 0) { + b = n; + n = f; + continue; + } + // 7.key 的值和 n.key 相等,则可以直接覆盖赋值 + if (c == 0) { + // onlyIfAbsent 默认 false, + if (onlyIfAbsent || n.casValue(v, value)) { + @SuppressWarnings("unchecked") V vv = (V)v; + // 返回被覆盖的值 + return vv; + } + // cas失败,break 一层循环,返回 0 重试 + break; + } + // else c < 0; fall through + } + // 8.此时的情况 b.key < key < n.key,对应流程图1中的7,创建z节点指向n + z = new Node(key, value, n); + // 9.尝试把 b.next 从 n 设置成 z + if (!b.casNext(n, z)) + // cas失败,返回到步骤0,重试 + break; + // 10.break outer 后, 上面的 for 循环不会再执行, 而后执行下面的代码 + break outer; + } + } + // 【以上插入节点已经完成,剩下的任务要根据随机数的值来表示是否向上增加层数与上层索引】 + + // 随机数 + int rnd = ThreadLocalRandom.nextSecondarySeed(); + + // 如果随机数的二进制与 10000000000000000000000000000001 进行与运算为 0 + // 即随机数的二进制最高位与最末尾必须为 0,其他位无所谓,就进入该循环 + // 如果随机数的二进制最高位与最末位不为 0,不增加新节点的层数 + + // 11.判断是否需要添加 level,32 位 + if ((rnd & 0x80000001) == 0) { + // 索引层 level,从 1 开始,就是最底层 + int level = 1, max; + // 12.判断最低位前面有几个 1,有几个leve就加几:0..0 0001 1110,这是4个,则1+4=5 + // 【最大有30个就是 1 + 30 = 31 + while (((rnd >>>= 1) & 1) != 0) + ++level; + // 最终会指向 z 节点,就是添加的节点 + Index idx = null; + // 指向头索引节点 + HeadIndex h = head; + + // 13.判断level是否比当前最高索引小,图中 max 为 3 + if (level <= (max = h.level)) { + for (int i = 1; i <= level; ++i) + // 根据层数level不断创建新增节点的上层索引,索引的后继索引留空 + // 第一次idx为null,也就是下层索引为空,第二次把上次的索引作为下层索引,【类似头插法】 + idx = new Index(z, idx, null); + // 循环以后的索引结构 + // index-3 ← idx + // ↓ + // index-2 + // ↓ + // index-1 + // ↓ + // z-node + } + // 14.若 level > max,则【只增加一层 index 索引层】,3 + 1 = 4 + else { + level = max + 1; + //创建一个 index 数组,长度是 level+1,假设 level 是 4,创建的数组长度为 5 + Index[] idxs = (Index[])new Index[level+1]; + // index[0]的数组 slot 并没有使用,只使用 [1,level] 这些数组的 slot + for (int i = 1; i <= level; ++i) + idxs[i] = idx = new Index(z, idx, null); + // index-4 ← idx + // ↓ + // ...... + // ↓ + // index-1 + // ↓ + // z-node + + for (;;) { + h = head; + // 获取头索引的层数,3 + int oldLevel = h.level; + // 如果 level <= oldLevel,说明其他线程进行了 index 层增加操作,退出循环 + if (level <= oldLevel) + break; + // 定义一个新的头索引节点 + HeadIndex newh = h; + // 获取头索引的节点,就是 BASE_HEADER + Node oldbase = h.node; + // 升级 baseHeader 索引,升高一级,并发下可能升高多级 + for (int j = oldLevel + 1; j <= level; ++j) + // 参数1:底层node,参数二:down,为以前的头节点,参数三:right,新建 + newh = new HeadIndex(oldbase, newh, idxs[j], j); + // 执行完for循环之后,baseHeader 索引长这个样子,这里只升高一级 + // index-4 → index-4 ← idx + // ↓ ↓ + // index-3 index-3 + // ↓ ↓ + // index-2 index-2 + // ↓ ↓ + // index-1 index-1 + // ↓ ↓ + // baseHeader → .... → z-node + + // cas 成功后,head 字段指向最新的 headIndex,baseHeader 的 index-4 + if (casHead(h, newh)) { + // h 指向最新的 index-4 节点 + h = newh; + // 让 idx 指向 z-node 的 index-3 节点, + // 因为从 index-3 - index-1 的这些 z-node 索引节点 都没有插入到索引链表 + idx = idxs[level = oldLevel]; + break; + } + } + } + // 15.【把新加的索引插入索引链表中】,有上述两种情况,一种索引高度不变,另一种是高度加 1 + // 要插入的是第几层的索引 + splice: for (int insertionLevel = level;;) { + // 获取头索引的层数,情况 1 是 3,情况 2 是 4 + int j = h.level; + // 【遍历 insertionLevel 层的索引,找到合适的插入位置】 + for (Index q = h, r = q.right, t = idx;;) { + // 如果头索引为 null 或者新增节点索引为 null,退出插入索引的总循环 + if (q == null || t == null) + // 此处表示有其他线程删除了头索引或者新增节点的索引 + break splice; + // 头索引的链表后续索引存在,如果是新层则为新节点索引,如果是老层则为原索引 + if (r != null) { + // 获取r的节点 + Node n = r.node; + // 插入的key和n.key的比较值 + int c = cpr(cmp, key, n.key); + // 【删除空值索引】 + if (n.value == null) { + if (!q.unlink(r)) + break; + r = q.right; + continue; + } + // key > r.node.key,向右扫描 + if (c > 0) { + q = r; + r = r.right; + continue; + } + } + // 执行到这里,说明 key < r.node.key,判断是否是第 j 层插入新增节点的前置索引 + if (j == insertionLevel) { + // 【将新索引节点 t 插入 q r 之间】 + if (!q.link(r, t)) + break; + // 如果新增节点的值为 null,表示该节点已经被其他线程删除 + if (t.node.value == null) { + // 找到该节点 + findNode(key); + break splice; + } + // 插入层逐层自减,当为最底层时退出循环 + if (--insertionLevel == 0) + break splice; + } + // 其他节点随着插入节点的层数下移而下移 + if (--j >= insertionLevel && j < level) + t = t.down; + q = q.down; + r = q.right; + } + } + } + return null; + } + ``` + +* findNode() + + ```java + private Node findNode(Object key) { + // 原理与doGet相同,无非是 findNode 返回节点,doGet 返回 value + if ((c = cpr(cmp, key, n.key)) == 0) + return n; + } + ``` + +*** + +##### 获取方法 + +* get(key):获取对应的数据 + + ```java + public V get(Object key) { + return doGet(key); + } + ``` + +* doGet():扫描过程会对已 value = null 的元素进行删除处理 + + ```java + private V doGet(Object key) { + if (key == null) + throw new NullPointerException(); + Comparator cmp = comparator; + outer: for (;;) { + // 1.找到最底层节点的前置节点 + for (Node b = findPredecessor(key, cmp), n = b.next;;) { + Object v; int c; + // 2.【如果该前置节点的链表后续节点为 null,说明不存在该节点】 + if (n == null) + break outer; + // b → n → f + Node f = n.next; + // 3.如果n不为前置节点的后续节点,表示已经有其他线程删除了该节点 + if (n != b.next) + break; + // 4.如果后续节点的值为null,【需要帮助删除该节点】 + if ((v = n.value) == null) { + n.helpDelete(b, f); + break; + } + // 5.如果前置节点已被其他线程删除,重新循环 + if (b.value == null || v == n) + break; + // 6.如果要获取的key与后续节点的key相等,返回节点的value + if ((c = cpr(cmp, key, n.key)) == 0) { + @SuppressWarnings("unchecked") V vv = (V)v; + return vv; + } + // 7.key < n.key,因位 key > b.key,b 和 n 相连,说明不存在该节点或者被其他线程删除了 + if (c < 0) + break outer; + b = n; + n = f; + } + } + return null; + } + ``` + +**** + +##### 删除方法 + +* remove() + + ```java + public V remove(Object key) { + return doRemove(key, null); + } + final V doRemove(Object key, Object value) { + if (key == null) + throw new NullPointerException(); + Comparator cmp = comparator; + outer: for (;;) { + // 1.找到最底层目标节点的前置节点,b.key < key + for (Node b = findPredecessor(key, cmp), n = b.next;;) { + Object v; int c; + // 2.如果该前置节点的链表后续节点为 null,退出循环,说明不存在这个元素 + if (n == null) + break outer; + // b → n → f + Node f = n.next; + if (n != b.next) // inconsistent read + break; + if ((v = n.value) == null) { // n is deleted + n.helpDelete(b, f); + break; + } + if (b.value == null || v == n) // b is deleted + break; + //3.key < n.key,说明被其他线程删除了,或者不存在该节点 + if ((c = cpr(cmp, key, n.key)) < 0) + break outer; + //4.key > n.key,继续向后扫描 + if (c > 0) { + b = n; + n = f; + continue; + } + //5.到这里是 key = n.key,value 不为空的情况下判断 value 和 n.value 是否相等 + if (value != null && !value.equals(v)) + break outer; + //6.【把 n 节点的 value 置空】 + if (!n.casValue(v, null)) + break; + //7.【给 n 添加一个删除标志 mark】,mark.next = f,然后把 b.next 设置为 f,成功后n出队 + if (!n.appendMarker(f) || !b.casNext(n, f)) + // 对 key 对应的 index 进行删除,调用了 findPredecessor 方法 + findNode(key); + else { + // 进行操作失败后通过 findPredecessor 中进行 index 的删除 + findPredecessor(key, cmp); + if (head.right == null) + // 进行headIndex 对应的index 层的删除 + tryReduceLevel(); + } + @SuppressWarnings("unchecked") V vv = (V)v; + return vv; + } + } + return null; + } + ``` + + 经过 findPredecessor() 中的 unlink() 后索引已经被删除 + + ![](https://seazean.oss-cn-beijing.aliyuncs.com/img/Java/JUC-ConcurrentSkipListMap-remove流程.png) + +* appendMarker():添加删除标记节点 + + ```java + boolean appendMarker(Node f) { + // 通过 CAS 让 n.next 指向一个 key 为 null,value 为 this,next 为 f 的标记节点 + return casNext(f, new Node(f)); + } + ``` + +* helpDelete():将添加了删除标记的节点清除,参数是该节点的前驱和后继节点 + + ```java + void helpDelete(Node b, Node f) { + // this 节点的后续节点为 f,且本身为 b 的后续节点,一般都是正确的,除非被别的线程删除 + if (f == next && this == b.next) { + // 如果 n 还还没有被标记 + if (f == null || f.value != f) + casNext(f, new Node(f)); + else + // 通过 CAS,将 b 的下一个节点 n 变成 f.next,即成为图中的样式 + b.casNext(this, f.next); + } + } + ``` + +* tryReduceLevel():删除索引 + + ```java + private void tryReduceLevel() { + HeadIndex h = head; + HeadIndex d; + HeadIndex e; + if (h.level > 3 && + (d = (HeadIndex)h.down) != null && + (e = (HeadIndex)d.down) != null && + e.right == null && + d.right == null && + h.right == null && + // 设置头索引 + casHead(h, d) && + // 重新检查 + h.right != null) + // 重新检查返回true,说明其他线程增加了索引层级,把索引头节点设置回来 + casHead(d, h); + } + ``` + +参考文章: + +参考视频: + +*** + +### NoBlocking + +#### 非阻塞队列 + +并发编程中,需要用到安全的队列,实现安全队列可以使用 2 种方式: + +* 加锁,这种实现方式是阻塞队列 +* 使用循环 CAS 算法实现,这种方式是非阻塞队列 + +ConcurrentLinkedQueue 是一个基于链接节点的无界线程安全队列,采用先进先出的规则对节点进行排序,当添加一个元素时,会添加到队列的尾部,当获取一个元素时,会返回队列头部的元素 + +补充:ConcurrentLinkedDeque 是双向链表结构的无界并发队列 + +ConcurrentLinkedQueue 使用约定: + +1. 不允许 null 入列 +2. 队列中所有未删除的节点的 item 都不能为 null 且都能从 head 节点遍历到 +3. 删除节点是将 item 设置为 null,队列迭代时跳过 item 为 null 节点 +4. head 节点跟 tail 不一定指向头节点或尾节点,可能**存在滞后性** + +ConcurrentLinkedQueue 由 head 节点和 tail 节点组成,每个节点由节点元素和指向下一个节点的引用组成,组成一张链表结构的队列 + +```java +private transient volatile Node head; +private transient volatile Node tail; + +private static class Node { + volatile E item; + volatile Node next; + //..... +} +``` + +*** + +#### 构造方法 + +* 无参构造方法: + + ```java + public ConcurrentLinkedQueue() { + // 默认情况下 head 节点存储的元素为空,dummy 节点,tail 节点等于 head 节点 + head = tail = new Node(null); + } + ``` + +* 有参构造方法 + + ```java + public ConcurrentLinkedQueue(Collection c) { + Node h = null, t = null; + // 遍历节点 + for (E e : c) { + checkNotNull(e); + Node newNode = new Node(e); + if (h == null) + h = t = newNode; + else { + // 单向链表 + t.lazySetNext(newNode); + t = newNode; + } + } + if (h == null) + h = t = new Node(null); + head = h; + tail = t; + } + ``` + +*** + +#### 入队方法 + +与传统的链表不同,单线程入队的工作流程: + +* 将入队节点设置成当前队列尾节点的下一个节点 +* 更新 tail 节点,如果 tail 节点的 next 节点不为空,则将入队节点设置成 tail 节点;如果 tail 节点的 next 节点为空,则将入队节点设置成 tail 的 next 节点,所以 tail 节点不总是尾节点,**存在滞后性** + +```java +public boolean offer(E e) { + checkNotNull(e); + // 创建入队节点 + final Node newNode = new Node(e); + + // 循环 CAS 直到入队成功 + for (Node t = tail, p = t;;) { + // p 用来表示队列的尾节点,初始情况下等于 tail 节点,q 是 p 的 next 节点 + Node q = p.next; + // 条件成立说明 p 是尾节点 + if (q == null) { + // p 是尾节点,设置 p 节点的下一个节点为新节点 + // 设置成功则 casNext 返回 true,否则返回 false,说明有其他线程更新过尾节点,继续寻找尾节点,继续 CAS + if (p.casNext(null, newNode)) { + // 首次添加时,p 等于 t,不进行尾节点更新,所以尾节点存在滞后性 + if (p != t) + // 将 tail 设置成新入队的节点,设置失败表示其他线程更新了 tail 节点 + casTail(t, newNode); + return true; + } + } + else if (p == q) + // 当 tail 不指向最后节点时,如果执行出列操作,可能将 tail 也移除,tail 不在链表中 + // 此时需要对 tail 节点进行复位,复位到 head 节点 + p = (t != (t = tail)) ? t : head; + else + // 推动 tail 尾节点往队尾移动 + p = (p != t && t != (t = tail)) ? t : q; + } +} +``` + +图解入队: + +![](https://seazean.oss-cn-beijing.aliyuncs.com/img/Java/JUC-ConcurrentLinkedQueue入队操作1.png) + +![](https://seazean.oss-cn-beijing.aliyuncs.com/img/Java/JUC-ConcurrentLinkedQueue入队操作2.png) + +![](https://seazean.oss-cn-beijing.aliyuncs.com/img/Java/JUC-ConcurrentLinkedQueue入队操作3.png) + +当 tail 节点和尾节点的距离**大于等于 1** 时(每入队两次)更新 tail,可以减少 CAS 更新 tail 节点的次数,提高入队效率 + +线程安全问题: + +* 线程 1 线程 2 同时入队,无论从哪个位置开始并发入队,都可以循环 CAS,直到入队成功,线程安全 +* 线程 1 遍历,线程 2 入队,所以造成 ConcurrentLinkedQueue 的 size 是变化,需要加锁保证安全 +* 线程 1 线程 2 同时出列,线程也是安全的 + +*** + +#### 出队方法 + +出队列的就是从队列里返回一个节点元素,并清空该节点对元素的引用,并不是每次出队都更新 head 节点 + +* 当 head 节点里有元素时,直接弹出 head 节点里的元素,而不会更新 head 节点 +* 当 head 节点里没有元素时,出队操作才会更新 head 节点 + +**批处理方式**可以减少使用 CAS 更新 head 节点的消耗,从而提高出队效率 + +```java +public E poll() { + restartFromHead: + for (;;) { + // p 节点表示首节点,即需要出队的节点,FIFO + for (Node h = head, p = h, q;;) { + E item = p.item; + // 如果 p 节点的元素不为 null,则通过 CAS 来设置 p 节点引用元素为 null,成功返回 item + if (item != null && p.casItem(item, null)) { + if (p != h) + // 对 head 进行移动 + updateHead(h, ((q = p.next) != null) ? q : p); + return item; + } + // 逻辑到这说明头节点的元素为空或头节点发生了变化,头节点被另外一个线程修改了 + // 那么获取 p 节点的下一个节点,如果 p 节点的下一节点也为 null,则表明队列已经空了 + else if ((q = p.next) == null) { + updateHead(h, p); + return null; + } + // 第一轮操作失败,下一轮继续,调回到循环前 + else if (p == q) + continue restartFromHead; + // 如果下一个元素不为空,则将头节点的下一个节点设置成头节点 + else + p = q; + } + } +} +final void updateHead(Node h, Node p) { + if (h != p && casHead(h, p)) + // 将旧结点 h 的 next 域指向为 h,help gc + h.lazySetNext(h); +} +``` + +在更新完 head 之后,会将旧的头结点 h 的 next 域指向为 h,图中所示的虚线也就表示这个节点的自引用,被移动的节点(item 为 null 的节点)会被 GC 回收 + +![](https://seazean.oss-cn-beijing.aliyuncs.com/img/Java/JUC-ConcurrentLinkedQueue出队操作1.png) + +![](https://seazean.oss-cn-beijing.aliyuncs.com/img/Java/JUC-ConcurrentLinkedQueue出队操作2.png) + +![](https://seazean.oss-cn-beijing.aliyuncs.com/img/Java/JUC-ConcurrentLinkedQueue出队操作3.png) + +如果这时,有一个线程来添加元素,通过 tail 获取的 next 节点则仍然是它本身,这就出现了p == q 的情况,出现该种情况之后,则会触发执行 head 的更新,将 p 节点重新指向为 head + +参考文章: + +*** + +#### 成员方法 + +* peek():会改变 head 指向,执行 peek() 方法后 head 会指向第一个具有非空元素的节点 + + ```java + // 获取链表的首部元素,只读取而不移除 + public E peek() { + restartFromHead: + for (;;) { + for (Node h = head, p = h, q;;) { + E item = p.item; + if (item != null || (q = p.next) == null) { + // 更改h的位置为非空元素节点 + updateHead(h, p); + return item; + } + else if (p == q) + continue restartFromHead; + else + p = q; + } + } + } + ``` + +* size():用来获取当前队列的元素个数,因为整个过程都没有加锁,在并发环境中从调用 size 方法到返回结果期间有可能增删元素,导致统计的元素个数不精确 + + ```java + public int size() { + int count = 0; + // first() 获取第一个具有非空元素的节点,若不存在,返回 null + // succ(p) 方法获取 p 的后继节点,若 p == p.next,则返回 head + // 类似遍历链表 + for (Node p = first(); p != null; p = succ(p)) + if (p.item != null) + // 最大返回Integer.MAX_VALUE + if (++count == Integer.MAX_VALUE) + break; + return count; + } + ``` + +* remove():移除元素 + + ```java + public boolean remove(Object o) { + // 删除的元素不能为null + if (o != null) { + Node next, pred = null; + for (Node p = first(); p != null; pred = p, p = next) { + boolean removed = false; + E item = p.item; + // 节点元素不为null + if (item != null) { + // 若不匹配,则获取next节点继续匹配 + if (!o.equals(item)) { + next = succ(p); + continue; + } + // 若匹配,则通过 CAS 操作将对应节点元素置为 null + removed = p.casItem(item, null); + } + // 获取删除节点的后继节点 + next = succ(p); + // 将被删除的节点移除队列 + if (pred != null && next != null) // unlink + pred.casNext(p, next); + if (removed) + return true; + } + } + return false; + } + ``` + +*** + +# NET + +## DES + +### 网络编程 + +网络编程,就是在一定的协议下,实现两台计算机的通信的技术 + +通信一定是基于软件结构实现的: + +* C/S 结构 :全称为 Client/Server 结构,是指客户端和服务器结构,常见程序有 QQ、IDEA 等软件 +* B/S 结构 :全称为 Browser/Server 结构,是指浏览器和服务器结构 + +两种架构各有优势,但是无论哪种架构,都离不开网络的支持 + +网络通信的三要素: + +1. 协议:计算机网络客户端与服务端通信必须约定和彼此遵守的通信规则,HTTP、FTP、TCP、UDP、SMTP + +2. IP 地址:互联网协议地址(Internet Protocol Address),用来给一个网络中的计算机设备做唯一的编号 + + * IPv4:4 个字节,32 位组成,192.168.1.1 + * IPv6:可以实现为所有设备分配 IP,128 位 + + * ipconfig:查看本机的 IP + * ping 检查本机与某个 IP 指定的机器是否联通,或者说是检测对方是否在线。 + * ping 空格 IP地址 :ping 220.181.57.216,ping + + 特殊的IP地址: 本机IP地址,**127.0.0.1 == localhost**,回环测试 + +3. 端口:端口号就可以唯一标识设备中的进程(应用程序)。端口号是用两个字节表示的整数,取值范围是 0-65535,0-1023 之间的端口号用于一些知名的网络服务和应用普通的应用程序需要使用 1024 以上的端口号。如果端口号被另外一个服务或应用所占用,会导致当前程序启动失败,报出端口被占用异常 + +利用**协议+IP 地址+端口号**三元组合,就可以标识网络中的进程了,那么进程间的通信就可以利用这个标识与其它进程进行交互 + +参考视频: + +**** + +### 通信协议 + +网络通信协议:对计算机必须遵守的规则,只有遵守这些规则,计算机之间才能进行通信 + +通信**是进程与进程之间的通信**,不是主机与主机之间的通信 + +TCP/IP协议:传输控制协议 (Transmission Control Protocol) + +传输控制协议 TCP(Transmission Control Protocol)是面向连接的,提供可靠交付,有流量控制,拥塞控制,提供全双工通信,面向字节流,每一条 TCP 连接只能是点对点的(一对一) + +* 在通信之前必须确定对方在线并且连接成功才可以通信 +* 例如下载文件、浏览网页等(要求可靠传输) + +用户数据报协议 UDP(User Datagram Protocol)是无连接的,尽最大可能交付,不可靠,没有拥塞控制,面向报文,支持一对一、一对多、多对一和多对多的交互通信 + +* 直接发消息给对方,不管对方是否在线,发消息后也不需要确认 +* 无线(视频会议,通话),性能好,可能丢失一些数据 + +**** + +### Java模型 + +相关概念: + +* 同步:当前线程要自己进行数据的读写操作(自己去银行取钱) +* 异步:当前线程可以去做其他事情(委托别人拿银行卡到银行取钱,然后给你) +* 阻塞:在数据没有的情况下,还是要继续等待着读(排队等待) +* 非阻塞:在数据没有的情况下,会去做其他事情,一旦有了数据再来获取(柜台取款,取个号,然后坐在椅子上做其它事,等号广播会通知你办理) + +Java 中的通信模型: + +1. BIO 表示同步阻塞式通信,服务器实现模式为一个连接一个线程,即客户端有连接请求时服务器端就需要启动一个线程进行处理,如果这个连接不做任何事情会造成不必要的线程开销,可以通过线程池机制改善 + + 同步阻塞式性能极差:大量线程,大量阻塞 + +2. 伪异步通信:引入线程池,不需要一个客户端一个线程,实现线程复用来处理很多个客户端,线程可控 + + 高并发下性能还是很差:线程数量少,数据依然是阻塞的,数据没有来线程还是要等待 + +3. NIO 表示**同步非阻塞 IO**,服务器实现模式为请求对应一个线程,客户端发送的连接会注册到多路复用器上,多路复用器轮询到连接有 I/O 请求时才启动一个线程进行处理 + + 工作原理:1 个主线程专门负责接收客户端,1 个线程轮询所有的客户端,发来了数据才会开启线程处理 + + 同步:线程还要不断的接收客户端连接,以及处理数据 + + 非阻塞:如果一个管道没有数据,不需要等待,可以轮询下一个管道是否有数据 + +4. AIO 表示异步非阻塞 IO,AIO 引入异步通道的概念,采用了 Proactor 模式,有效的请求才启动线程,特点是先由操作系统完成后才通知服务端程序启动线程去处理,一般适用于连接数较多且连接时间较长的应用 + + 异步:服务端线程接收到了客户端管道以后就交给底层处理 IO 通信,线程可以做其他事情 + + 非阻塞:底层也是客户端有数据才会处理,有了数据以后处理好通知服务器应用来启动线程进行处理 + +各种模型应用场景: + +* BIO 适用于连接数目比较小且固定的架构,该方式对服务器资源要求比较高,并发局限于应用中,程序简单 +* NIO 适用于连接数目多且连接比较短(轻操作)的架构,如聊天服务器,并发局限于应用中,编程复杂,JDK 1.4 开始支持 +* AIO 适用于连接数目多且连接比较长(重操作)的架构,如相册服务器,充分调用操作系统参与并发操作,JDK 1.7 开始支持 + +**** + +## I/O + +### IO模型 + +#### 五种模型 + +对于一个套接字上的输入操作,第一步是等待数据从网络中到达,当数据到达时被复制到内核中的某个缓冲区。第二步就是把数据从内核缓冲区复制到应用进程缓冲区 + +Linux 有五种 I/O 模型: + +* 阻塞式 I/O +* 非阻塞式 I/O +* I/O 复用(select 和 poll) +* 信号驱动式 I/O(SIGIO) +* 异步 I/O(AIO) + +五种模型对比: + +* 同步 I/O 包括阻塞式 I/O、非阻塞式 I/O、I/O 复用和信号驱动 I/O ,它们的主要区别在第一个阶段,非阻塞式 I/O 、信号驱动 I/O 和异步 I/O 在第一阶段不会阻塞 + +* 同步 I/O:将数据从内核缓冲区复制到应用进程缓冲区的阶段(第二阶段),应用进程会阻塞 +* 异步 I/O:第二阶段应用进程不会阻塞 + +*** + +#### 阻塞式IO + +应用进程通过系统调用 recvfrom 接收数据,会被阻塞,直到数据从内核缓冲区复制到应用进程缓冲区中才返回。阻塞不意味着整个操作系统都被阻塞,其它应用进程还可以执行,只是当前阻塞进程不消耗 CPU 时间,这种模型的 CPU 利用率会比较高 + +recvfrom() 用于**接收 Socket 传来的数据,并复制到应用进程的缓冲区 buf 中**,把 recvfrom() 当成系统调用 + +![](https://seazean.oss-cn-beijing.aliyuncs.com/img/Java/IO模型-阻塞式IO.png) + +*** + +#### 非阻塞式 + +应用进程通过 recvfrom 调用不停的去和内核交互,直到内核准备好数据。如果没有准备好数据,内核返回一个错误码,过一段时间应用进程再执行 recvfrom 系统调用,在两次发送请求的时间段,进程可以进行其他任务,这种方式称为轮询(polling) + +由于 CPU 要处理更多的系统调用,因此这种模型的 CPU 利用率比较低 + +![](https://seazean.oss-cn-beijing.aliyuncs.com/img/Java/IO模型-非阻塞式IO.png) + +*** + +#### 信号驱动 + +应用进程使用 sigaction 系统调用,内核立即返回,应用进程可以继续执行,等待数据阶段应用进程是非阻塞的。当内核数据准备就绪时向应用进程发送 SIGIO 信号,应用进程收到之后在信号处理程序中调用 recvfrom 将数据从内核复制到应用进程中 + +相比于非阻塞式 I/O 的轮询方式,信号驱动 I/O 的 CPU 利用率更高 + +![](https://seazean.oss-cn-beijing.aliyuncs.com/img/Java/IO模型-信号驱动IO.png) + +*** + +#### IO 复用 + +IO 复用模型使用 select 或者 poll 函数等待数据,select 会监听所有注册好的 IO,**等待多个套接字中的任何一个变为可读**,等待过程会被阻塞,当某个套接字准备好数据变为可读时 select 调用就返回,然后调用 recvfrom 把数据从内核复制到进程中 + +IO 复用让单个进程具有处理多个 I/O 事件的能力,又被称为 Event Driven I/O,即**事件驱动 I/O** + +如果一个 Web 服务器没有 I/O 复用,那么每一个 Socket 连接都要创建一个线程去处理,如果同时有几万个连接,就需要创建相同数量的线程。相比于多进程和多线程技术,I/O 复用不需要进程线程创建和切换的开销,系统开销更小 + +![](https://seazean.oss-cn-beijing.aliyuncs.com/img/Java/IO模型-IO复用模型.png) + +*** + +#### 异步 IO + +应用进程执行 aio_read 系统调用会立即返回,给内核传递描述符、缓冲区指针、缓冲区大小等。应用进程可以继续执行不会被阻塞,内核会在所有操作完成之后向应用进程发送信号 + +异步 I/O 与信号驱动 I/O 的区别在于,异步 I/O 的信号是通知应用进程 I/O 完成,而信号驱动 I/O 的信号是通知应用进程可以开始 I/O + +![](https://seazean.oss-cn-beijing.aliyuncs.com/img/Java/IO模型-异步IO模型.png) + +**** + +### 多路复用 + +#### select + +##### 函数 + +Socket 不是文件,只是一个标识符,但是 Unix 操作系统把所有东西都**看作**是文件,所以 Socket 说成 file descriptor,也就是 fd + +select 允许应用程序监视一组文件描述符,等待一个或者多个描述符成为就绪状态,从而完成 I/O 操作。 + +```c +int select(int n, fd_set *readfds, fd_set *writefds, fd_set *exceptfds, struct timeval *timeout); +``` + +* fd_set 使用 **bitmap 数组**实现,数组大小用 FD_SETSIZE 定义,**单进程**只能监听少于 FD_SETSIZE 数量的描述符,32 位机默认是 1024 个,64 位机默认是 2048,可以对进行修改,然后重新编译内核 + +* fd_set 有三种类型的描述符:readset、writeset、exceptset,对应读、写、异常条件的描述符集合 + +* n 是监测的 socket 的最大数量 + +* timeout 为超时参数,调用 select 会一直**阻塞**直到有描述符的事件到达或者等待的时间超过 timeout + + ```c + struct timeval{ + long tv_sec; //秒 + long tv_usec; //微秒 + } + ``` + + * timeout == null:等待无限长的时间 + * tv_sec == 0 && tv_usec == 0:获取后直接返回,不阻塞等待 + * tv_sec != 0 || tv_usec != 0:等待指定时间 + +* 方法成功调用返回结果为**就绪的文件描述符个数**,出错返回结果为 -1,超时返回结果为 0 + +Linux 提供了一组宏为 fd_set 进行赋值操作: + +```c +int FD_ZERO(fd_set *fdset); // 将一个 fd_set 类型变量的所有值都置为 0 +int FD_CLR(int fd, fd_set *fdset); // 将一个 fd_set 类型变量的 fd 位置为 0 +int FD_SET(int fd, fd_set *fdset); // 将一个 fd_set 类型变量的 fd 位置为 1 +int FD_ISSET(int fd, fd_set *fdset);// 判断 fd 位是否被置为 1 +``` + +示例: + +```c +sockfd = socket(AF_INET, SOCK_STREAM, 0); +memset(&addr, 0, sizeof(addr))); +addr.sin_family = AF_INET; +addr.sin_port = htons(2000); +addr.sin_addr.s_addr = INADDR_ANY; +bind(sockfd, (struct sockaddr*)&addr, sizeof(addr));//绑定连接 +listen(sockfd, 5);//监听5个端口 +for(i = 0; i < 5; i++) { + memset(&client, e, sizeof(client)); + addrlen = sizeof(client); + fds[i] = accept(sockfd, (struct sockaddr*)&client, &addrlen); + //将监听的对应的文件描述符fd存入fds:[3,4,5,6,7] + if(fds[i] > max) + max = fds[i]; +} +while(1) { + FD_ZERO(&rset);//置为0 + for(i = 0; i < 5; i++) { + FD_SET(fds[i], &rset);//对应位置1 [0001 1111 00.....] + } + print("round again"); + select(max + 1, &rset, NULL, NULL, NULL);//监听 + + for(i = 0; i <5; i++) { + if(FD_ISSET(fds[i], &rset)) {//判断监听哪一个端口 + memset(buffer, 0, MAXBUF); + read(fds[i], buffer, MAXBUF);//进入内核态读数据 + print(buffer); + } + } +} +``` + +参考视频: + +**** + +##### 流程 + +select 调用流程图: + +![](https://seazean.oss-cn-beijing.aliyuncs.com/img/Java/IO-select调用过程.png) + +1. 使用 copy_from_user 从用户空间拷贝 fd_set 到内核空间,进程阻塞 +2. 注册回调函数 _pollwait +3. 遍历所有 fd,调用其对应的 poll 方法判断当前请求是否准备就绪,对于 socket,这个 poll 方法是 sock_poll,sock_poll 根据情况会调用到 tcp_poll、udp_poll 或者 datagram_poll,以 tcp_poll 为例,其核心实现就是 _pollwait +4. _pollwait 把 **current(调用 select 的进程)**挂到设备的等待队列,不同设备有不同的等待队列,对于 tcp_poll ,其等待队列是 sk → sk_sleep(把进程挂到等待队列中并不代表进程已经睡眠),在设备收到消息(网络设备)或填写完文件数据(磁盘设备)后,会唤醒设备等待队列上睡眠的进程,这时 current 便被唤醒,进入就绪队列 +5. poll 方法返回时会返回一个描述读写操作是否就绪的 mask 掩码,根据这个 mask 掩码给 fd_set 赋值 +6. 如果遍历完所有的 fd,还没有返回一个可读写的 mask 掩码,则会调用 schedule_timeout 让 current 进程进入睡眠。当设备驱动发生自身资源可读写后,会唤醒其等待队列上睡眠的进程,如果超过一定的超时时间(schedule_timeout)没有其他线程唤醒,则调用 select 的进程会重新被唤醒获得 CPU,进而重新遍历 fd,判断有没有就绪的 fd +7. 把 fd_set 从内核空间拷贝到用户空间,阻塞进程继续执行 + +参考文章: + +其他流程图: + +**** + +#### poll + +poll 的功能与 select 类似,也是等待一组描述符中的一个成为就绪状态 + +```c +int poll(struct pollfd *fds, unsigned int nfds, int timeout); +``` + +poll 中的描述符是 pollfd 类型的数组,pollfd 的定义如下: + +```c +struct pollfd { + int fd; /* file descriptor */ + short events; /* requested events */ + short revents; /* returned events */ +}; +``` + +select 和 poll 对比: + +* select 会修改描述符,而 poll 不会 +* select 的描述符类型使用数组实现,有描述符的限制;而 poll 使用**链表**实现,没有描述符数量的限制 +* poll 提供了更多的事件类型,并且对描述符的重复利用上比 select 高 + +* select 和 poll 速度都比较慢,**每次调用**都需要将全部描述符数组 fd 从应用进程缓冲区复制到内核缓冲区,同时每次都需要在内核遍历传递进来的所有 fd ,这个开销在 fd 很多时会很大 +* 几乎所有的系统都支持 select,但是只有比较新的系统支持 poll +* select 和 poll 的时间复杂度 O(n),对 socket 进行扫描时是线性扫描,即采用轮询的方法,效率较低,因为并不知道具体是哪个 socket 具有事件,所以随着 fd 数量的增加会造成遍历速度慢的**线性下降**性能问题 +* poll 还有一个特点是水平触发,如果报告了 fd 后,没有被处理,那么下次 poll 时会再次报告该 fd +* 如果一个线程对某个描述符调用了 select 或者 poll,另一个线程关闭了该描述符,会导致调用结果不确定 + +参考文章: + +**** + +#### epoll + +##### 函数 + +epoll 使用事件的就绪通知方式,通过 epoll_ctl() 向内核注册新的描述符或者是改变某个文件描述符的状态。已注册的描述符在内核中会被维护在一棵**红黑树**上,一旦该 fd 就绪,**内核通过 callback 回调函数将 I/O 准备好的描述符加入到一个链表中**管理,进程调用 epoll_wait() 便可以得到事件就绪的描述符 + +```c +int epoll_create(int size); +int epoll_ctl(int epfd, int op, int fd, struct epoll_event *event); +int epoll_wait(int epfd, struct epoll_event * events, int maxevents, int timeout); +``` + +* epall_create:一个系统函数,函数将在内核空间内创建一个 epoll 数据结构,可以理解为 epoll 结构空间,返回值为 epoll 的文件描述符编号,以后有 client 连接时,向该 epoll 结构中添加监听,所以 epoll 使用一个文件描述符管理多个描述符 + +* epall_ctl:epoll 的事件注册函数,select 函数是调用时指定需要监听的描述符和事件,epoll 先将用户感兴趣的描述符事件注册到 epoll 空间。此函数是非阻塞函数,用来增删改 epoll 空间内的描述符,参数解释: + + * epfd:epoll 结构的进程 fd 编号,函数将依靠该编号找到对应的 epoll 结构 + + * op:表示当前请求类型,有三个宏定义: + + * EPOLL_CTL_ADD:注册新的 fd 到 epfd 中 + * EPOLL_CTL_MOD:修改已经注册的 fd 的监听事件 + * EPOLL_CTI_DEL:从 epfd 中删除一个 fd + + * fd:需要监听的文件描述符,一般指 socket_fd + + * event:告诉内核对该 fd 资源感兴趣的事件,epoll_event 的结构: + + ```c + struct epoll_event { + _uint32_t events; /*epoll events*/ + epoll_data_t data; /*user data variable*/ + } + ``` + + events 可以是以下几个宏集合:EPOLLIN、EPOLOUT、EPOLLPRI、EPOLLERR、EPOLLHUP(挂断)、EPOLET(边缘触发)、EPOLLONESHOT(只监听一次,事件触发后自动清除该 fd,从 epoll 列表) + +* epoll_wait:等待事件的产生,类似于 select() 调用,返回值为本次就绪的 fd 个数,直接从就绪链表获取,时间复杂度 O(1) + + * epfd:**指定感兴趣的 epoll 事件列表** + * events:指向一个 epoll_event 结构数组,当函数返回时,内核会把就绪状态的数据拷贝到该数组 + * maxevents:标明 epoll_event 数组最多能接收的数据量,即本次操作最多能获取多少就绪数据 + * timeout:单位为毫秒 + * 0:表示立即返回,非阻塞调用 + * -1:阻塞调用,直到有用户感兴趣的事件就绪为止 + * 大于 0:阻塞调用,阻塞指定时间内如果有事件就绪则提前返回,否则等待指定时间后返回 + +epoll 的描述符事件有两种触发模式:LT(level trigger)和 ET(edge trigger): + +* LT 模式:当 epoll_wait() 检测到描述符事件到达时,将此事件通知进程,进程可以不立即处理该事件,下次调用 epoll_wait() 会再次通知进程,是默认的一种模式,并且同时支持 Blocking 和 No-Blocking +* ET 模式:通知之后进程必须立即处理事件,下次再调用 epoll_wait() 时不会再得到事件到达的通知。减少了 epoll 事件被重复触发的次数,因此效率要比 LT 模式高;只支持 No-Blocking,以避免由于一个 fd 的阻塞读/阻塞写操作把处理多个文件描述符的任务饥饿 + +```c +// 创建 epoll 描述符,每个应用程序只需要一个,用于监控所有套接字 +int pollingfd = epoll_create(0xCAFE); +if ( pollingfd < 0 )// report error +// 初始化 epoll 结构 +struct epoll_event ev = { 0 }; + +// 将连接类实例与事件相关联,可以关联任何想要的东西 +ev.data.ptr = pConnection1; + +// 监视输入,并且在事件发生后不自动重新准备描述符 +ev.events = EPOLLIN | EPOLLONESHOT; +// 将描述符添加到监控列表中,即使另一个线程在epoll_wait中等待,描述符将被正确添加 +if ( epoll_ctl( epollfd, EPOLL_CTL_ADD, pConnection1->getSocket(), &ev) != 0 ) + // report error + +// 最多等待 20 个事件 +struct epoll_event pevents[20]; + +// 等待10秒,检索20个并存入epoll_event数组 +int ready = epoll_wait(pollingfd, pevents, 20, 10000); +// 检查epoll是否成功 +if ( ret == -1)// report error and abort +else if ( ret == 0)// timeout; no event detected +else +{ + for (int i = 0; i < ready; i+ ) + { + if ( pevents[i].events & EPOLLIN ) + { + // 获取连接指针 + Connection * c = (Connection*) pevents[i].data.ptr; + c->handleReadEvent(); + } + } +} +``` + +流程图: + +参考视频: + +*** + +##### 特点 + +epoll 的特点: + +* epoll 仅适用于 Linux 系统 +* epoll 使用**一个文件描述符管理多个描述符**,将用户关心的文件描述符的事件存放到内核的一个事件表(个人理解成哑元节点) +* 没有最大描述符数量(并发连接)的限制,打开 fd 的上限远大于1024(1G 内存能监听约 10 万个端口) +* epoll 的时间复杂度 O(1),epoll 理解为 event poll,不同于忙轮询和无差别轮询,调用 epoll_wait **只是轮询就绪链表**。当监听列表有设备就绪时调用回调函数,把就绪 fd 放入就绪链表中,并唤醒在 epoll_wait 中阻塞的进程,所以 epoll 实际上是**事件驱动**(每个事件关联上fd)的,降低了 system call 的时间复杂度 +* epoll 内核中根据每个 fd 上的 callback 函数来实现,只有活跃的 socket 才会主动调用 callback,所以使用 epoll 没有前面两者的线性下降的性能问题,效率提高 + +* epoll 注册新的事件是注册到到内核中 epoll 句柄中,不需要每次调用 epoll_wait 时重复拷贝,对比前面两种只需要将描述符从进程缓冲区向内核缓冲区**拷贝一次**,也可以利用 **mmap() 文件映射内存**加速与内核空间的消息传递(只是可以用,并没有用) +* 前面两者要把 current 往设备等待队列中挂一次,epoll 也只把 current 往等待队列上挂一次,但是这里的等待队列并不是设备等待队列,只是一个 epoll 内部定义的等待队列,这样可以节省开销 +* epoll 对多线程编程更有友好,一个线程调用了 epoll_wait() 另一个线程关闭了同一个描述符,也不会产生像 select 和 poll 的不确定情况 + +参考文章: + +参考文章: + +*** + +#### 应用 + +应用场景: + +* select 应用场景: + * select 的 timeout 参数精度为微秒,poll 和 epoll 为毫秒,因此 select 适用**实时性要求比较高**的场景,比如核反应堆的控制 + * select 可移植性更好,几乎被所有主流平台所支持 + +* poll 应用场景:poll 没有最大描述符数量的限制,适用于平台支持并且对实时性要求不高的情况 + +* epoll 应用场景: + * 运行在 Linux 平台上,有大量的描述符需要同时轮询,并且这些连接最好是**长连接** + * 需要同时监控小于 1000 个描述符,没必要使用 epoll,因为这个应用场景下并不能体现 epoll 的优势 + * 需要监控的描述符状态变化多,而且是非常短暂的,就没有必要使用 epoll。因为 epoll 中的所有描述符都存储在内核中,每次对描述符的状态改变都需要通过 epoll_ctl() 进行系统调用,频繁系统调用降低效率,并且 epoll 的描述符存储在内核,不容易调试 + +参考文章: + +**** + +### 系统调用 + +#### 内核态 + +用户空间:用户代码、用户堆栈 + +内核空间:内核代码、内核调度程序、进程描述符(内核堆栈、thread_info 进程描述符) + +* 进程描述符和用户的进程是一一对应的 +* SYS_API 系统调用:如 read、write,系统调用就是 0X80 中断 +* 进程描述符 pd:进程从用户态切换到内核态时,需要**保存用户态时的上下文信息在 PCB 中** +* 线程上下文:用户程序基地址,程序计数器、cpu cache、寄存器等,方便程序切回用户态时恢复现场 +* 内核堆栈:**系统调用函数也是要创建变量的,**这些变量在内核堆栈上分配 + +![](https://seazean.oss-cn-beijing.aliyuncs.com/img/Java/IO-用户态和内核态.png) + +*** + +#### 80中断 + +在用户程序中调用操作系统提供的核心态级别的子功能,为了系统安全需要进行用户态和内核态转换,状态的转换需要进行 CPU 中断,中断分为硬中断和软中断: + +* 硬中断:如网络传输中,数据到达网卡后,网卡经过一系列操作后发起硬件中断 +* 软中断:如程序运行过程中本身产生的一些中断 + * 发起 `0X80` 中断 + * 程序执行碰到除 0 异常 + +系统调用 system_call 函数所对应的中断指令编号是 0X80(十进制是 8×16=128),而该指令编号对应的就是系统调用程序的入口,所以称系统调用为 80 中断 + +系统调用的流程: + +* 在 CPU 寄存器里存一个系统调用号,表示哪个系统函数,比如 read +* 将 CPU 的临时数据都保存到 thread_info 中 +* 执行 80 中断处理程序,找到刚刚存的系统调用号(read),先检查缓存中有没有对应的数据,没有就去磁盘中加载到内核缓冲区,然后从内核缓冲区拷贝到用户空间 +* 最后恢复到用户态,通过 thread_info 恢复现场,用户态继续执行 + +![](https://seazean.oss-cn-beijing.aliyuncs.com/img/Java/IO-系统调用的过程.jpg) + +参考视频: + +**** + +### 零拷贝 + +#### DMA + +DMA (Direct Memory Access) :直接存储器访问,让外部设备不通过 CPU 直接与系统内存交换数据的接口技术 + +作用:可以解决批量数据的输入/输出问题,使数据的传送速度取决于存储器和外设的工作速度 + +把内存数据传输到网卡然后发送: + +* 没有 DMA:CPU 读内存数据到 CPU 高速缓存,再写到网卡,这样就把 CPU 的速度拉低到和网卡一个速度 +* 使用 DMA:把数据读到 Socket 内核缓存区(CPU 复制),CPU 分配给 DMA 开始**异步**操作,DMA 读取 Socket 缓冲区到 DMA 缓冲区,然后写到网卡。DMA 执行完后**中断**(就是通知) CPU,这时 Socket 内核缓冲区为空,CPU 从用户态切换到内核态,执行中断处理程序,将需要使用 Socket 缓冲区的阻塞进程移到就绪队列 + +一个完整的 DMA 传输过程必须经历 DMA 请求、DMA 响应、DMA 传输、DMA 结束四个步骤: + + + +DMA 方式是一种完全由硬件进行信息传送的控制方式,通常系统总线由 CPU 管理,在 DMA 方式中,CPU 的主存控制信号被禁止使用,CPU 把总线(地址总线、数据总线、控制总线)让出来由 DMA 控制器接管,用来控制传送的字节数、判断 DMA 是否结束、以及发出 DMA 结束信号,所以 DMA 控制器必须有以下功能: + +* 接受外设发出的 DMA 请求,并向 CPU 发出总线接管请求 +* 当 CPU 发出允许接管信号后,进入 DMA 操作周期 +* 确定传送数据的主存单元地址及长度,并自动修改主存地址计数和传送长度计数 +* 规定数据在主存和外设间的传送方向,发出读写等控制信号,执行数据传送操作 +* 判断 DMA 传送是否结束,发出 DMA 结束信号,使 CPU 恢复正常工作状态(中断) + +*** + +#### BIO + +传统的 I/O 操作进行了 4 次用户空间与内核空间的上下文切换,以及 4 次数据拷贝: + +* JVM 发出 read 系统调用,OS 上下文切换到内核模式(切换 1)并将数据从网卡或硬盘等设备通过 DMA 读取到内核空间缓冲区(拷贝 1),内核缓冲区实际上是**磁盘高速缓存(PageCache)** +* OS 内核将数据复制到用户空间缓冲区(拷贝 2),然后 read 系统调用返回,又会导致一次内核空间到用户空间的上下文切换(切换 2) +* JVM 处理代码逻辑并发送 write() 系统调用,OS 上下文切换到内核模式(切换3)并从用户空间缓冲区复制数据到内核空间缓冲区(拷贝3) +* 将内核空间缓冲区中的数据写到 hardware(拷贝4),write 系统调用返回,导致内核空间到用户空间的再次上下文切换(切换4) + +流程图中的箭头反过来也成立,可以从网卡获取数据 + +![](https://seazean.oss-cn-beijing.aliyuncs.com/img/Java/IO-BIO工作流程.png) + +read 调用图示:read、write 都是系统调用指令 + + + +*** + +#### mmap + +mmap(Memory Mapped Files)内存映射加 write 实现零拷贝,**零拷贝就是没有数据从内核空间复制到用户空间** + +用户空间和内核空间都使用内存,所以可以共享同一块物理内存地址,省去用户态和内核态之间的拷贝。写网卡时,共享空间的内容拷贝到 Socket 缓冲区,然后交给 DMA 发送到网卡,只需要 3 次复制 + +进行了 4 次用户空间与内核空间的上下文切换,以及 3 次数据拷贝(2 次 DMA,一次 CPU 复制): + +* 发出 mmap 系统调用,DMA 拷贝到内核缓冲区,映射到共享缓冲区;mmap 系统调用返回,无需拷贝 +* 发出 write 系统调用,将数据从内核缓冲区拷贝到内核 Socket 缓冲区;write 系统调用返回,DMA 将内核空间 Socket 缓冲区中的数据传递到协议引擎 + +![](https://seazean.oss-cn-beijing.aliyuncs.com/img/Java/IO-mmap工作流程.png) + +原理:利用操作系统的 Page 来实现文件到物理内存的直接映射,完成映射后对物理内存的操作会**被同步**到硬盘上 + +缺点:不可靠,写到 mmap 中的数据并没有被真正的写到硬盘,操作系统会在程序主动调用 flush 的时候才把数据真正的写到硬盘 + +Java NIO 提供了 **MappedByteBuffer** 类可以用来实现 mmap 内存映射,MappedByteBuffer 类对象**只能通过调用 `FileChannel.map()` 获取** + +**** + +#### sendfile + +sendfile 实现零拷贝,打开文件的文件描述符 fd 和 socket 的 fd 传递给 sendfile,然后经过 3 次复制和 2 次用户态和内核态的切换 + +原理:数据根本不经过用户态,直接从内核缓冲区进入到 Socket Buffer,由于和用户态完全无关,就减少了两次上下文切换 + +说明:零拷贝技术是不允许进程对文件内容作进一步的加工的,比如压缩数据再发送 + +![](https://seazean.oss-cn-beijing.aliyuncs.com/img/Java/IO-sendfile工作流程.png) + +sendfile2.4 之后,sendfile 实现了更简单的方式,文件到达内核缓冲区后,不必再将数据全部复制到 socket buffer 缓冲区,而是只**将记录数据位置和长度相关等描述符信息**保存到 socket buffer,DMA 根据 Socket 缓冲区中描述符提供的位置和偏移量信息直接将内核空间缓冲区中的数据拷贝到协议引擎上(2 次复制 2 次切换) + +Java NIO 对 sendfile 的支持是 `FileChannel.transferTo()/transferFrom()`,把磁盘文件读取 OS 内核缓冲区后的 fileChannel,直接转给 socketChannel 发送,底层就是 sendfile + +参考文章: + +*** + +## BIO + +### Inet + +一个 InetAddress 类的对象就代表一个 IP 地址对象 + +成员方法: + +* `static InetAddress getLocalHost()`:获得本地主机 IP 地址对象 +* `static InetAddress getByName(String host)`:根据 IP 地址字符串或主机名获得对应的 IP 地址对象 +* `String getHostName()`:获取主机名 +* `String getHostAddress()`:获得 IP 地址字符串 + +```java +public class InetAddressDemo { + public static void main(String[] args) throws Exception { + // 1.获取本机地址对象 + InetAddress ip = InetAddress.getLocalHost(); + System.out.println(ip.getHostName());//DESKTOP-NNMBHQR + System.out.println(ip.getHostAddress());//192.168.11.1 + // 2.获取域名ip对象 + InetAddress ip2 = InetAddress.getByName("www.baidu.com"); + System.out.println(ip2.getHostName());//www.baidu.com + System.out.println(ip2.getHostAddress());//14.215.177.38 + // 3.获取公网IP对象。 + InetAddress ip3 = InetAddress.getByName("182.61.200.6"); + System.out.println(ip3.getHostName());//182.61.200.6 + System.out.println(ip3.getHostAddress());//182.61.200.6 + + // 4.判断是否能通: ping 5s之前测试是否可通 + System.out.println(ip2.isReachable(5000)); // ping百度 + } +} +``` + +*** + +### UDP + +#### 基本介绍 + +UDP(User Datagram Protocol)协议的特点: + +* 面向无连接的协议,发送端只管发送,不确认对方是否能收到,速度快,但是不可靠,会丢失数据 +* 尽最大努力交付,没有拥塞控制 +* 基于数据包进行数据传输,发送数据的包的大小限制 **64KB** 以内 +* 支持一对一、一对多、多对一、多对多的交互通信 + +UDP 协议的使用场景:在线视频、网络语音、电话 + +*** + +#### 实现UDP + +UDP 协议相关的两个类: + +* DatagramPacket(数据包对象):用来封装要发送或要接收的数据,比如:集装箱 +* DatagramSocket(发送对象):用来发送或接收数据包,比如:码头 + +**DatagramPacket**: + +* DatagramPacket 类: + + `public new DatagramPacket(byte[] buf, int length, InetAddress address, int port)`:创建发送端数据包对象 + + * buf:要发送的内容,字节数组 + * length:要发送内容的长度,单位是字节 + * address:接收端的IP地址对象 + * port:接收端的端口号 + + `public new DatagramPacket(byte[] buf, int length)`:创建接收端的数据包对象 + + * buf:用来存储接收到内容 + * length:能够接收内容的长度 + +* DatagramPacket 类常用方法: + + * `public int getLength()`:获得实际接收到的字节个数 + * `public byte[] getData()`:返回数据缓冲区 + +**DatagramSocket**: + +* DatagramSocket 类构造方法: + * `protected DatagramSocket()`:创建发送端的 Socket 对象,系统会随机分配一个端口号 + * `protected DatagramSocket(int port)`:创建接收端的 Socket 对象并指定端口号 +* DatagramSocket 类成员方法: + * `public void send(DatagramPacket dp)`:发送数据包 + * `public void receive(DatagramPacket p)`:接收数据包 + * `public void close()`:关闭数据报套接字 + +```java +public class UDPClientDemo { + public static void main(String[] args) throws Exception { + System.out.println("===启动客户端==="); + // 1.创建一个集装箱对象,用于封装需要发送的数据包! + byte[] buffer = "我学Java".getBytes(); + DatagramPacket packet = new DatagramPacket(buffer,bubffer.length,InetAddress.getLoclHost,8000); + // 2.创建一个码头对象 + DatagramSocket socket = new DatagramSocket(); + // 3.开始发送数据包对象 + socket.send(packet); + socket.close(); + } +} +public class UDPServerDemo{ + public static void main(String[] args) throws Exception { + System.out.println("==启动服务端程序=="); + // 1.创建一个接收客户都端的数据包对象(集装箱) + byte[] buffer = new byte[1024*64]; + DatagramPacket packet = new DatagramPacket(buffer, bubffer.length); + // 2.创建一个接收端的码头对象 + DatagramSocket socket = new DatagramSocket(8000); + // 3.开始接收 + socket.receive(packet); + // 4.从集装箱中获取本次读取的数据量 + int len = packet.getLength(); + // 5.输出数据 + // String rs = new String(socket.getData(), 0, len) + String rs = new String(buffer , 0 , len); + System.out.println(rs); + // 6.服务端还可以获取发来信息的客户端的IP和端口。 + String ip = packet.getAddress().getHostAdress(); + int port = packet.getPort(); + socket.close(); + } +} +``` + +*** + +#### 通讯方式 + +UDP 通信方式: + +* 单播:用于两个主机之间的端对端通信 + +* 组播:用于对一组特定的主机进行通信 + + IP : 224.0.1.0 + + Socket 对象 : MulticastSocket + +* 广播:用于一个主机对整个局域网上所有主机上的数据通信 + + IP : 255.255.255.255 + + Socket 对象 : DatagramSocket + +*** + +### TCP + +#### 基本介绍 + +TCP/IP (Transfer Control Protocol) 协议,传输控制协议 + +TCP/IP 协议的特点: + +* 面向连接的协议,提供可靠交互,速度慢 +* 点对点的全双工通信 +* 通过**三次握手**建立连接,连接成功形成数据传输通道;通过**四次挥手**断开连接 +* 基于字节流进行数据传输,传输数据大小没有限制 + +TCP 协议的使用场景:文件上传和下载、邮件发送和接收、远程登录 + +注意:**TCP 不会为没有数据的 ACK 超时重传** + +三次握手 + +四次挥手 + +推荐阅读: + +*** + +#### Socket + +TCP 通信也叫 **Socket 网络编程**,只要代码基于 Socket 开发,底层就是基于了可靠传输的 TCP 通信 + +双向通信:Java Socket 是全双工的,在任意时刻,线路上存在 `A -> B` 和 `B -> A` 的双向信号传输,即使是阻塞 IO,读和写也是可以同时进行的,只要分别采用读线程和写线程即可,读不会阻塞写、写也不会阻塞读 + +TCP 协议相关的类: + +* Socket:一个该类的对象就代表一个客户端程序。 +* ServerSocket:一个该类的对象就代表一个服务器端程序。 + +Socket 类: + +* 构造方法: + + * `Socket(InetAddress address,int port)`:创建流套接字并将其连接到指定 IP 指定端口号 + + * `Socket(String host, int port)`:根据 IP 地址字符串和端口号创建客户端 Socket 对象 + + 注意事项:**执行该方法,就会立即连接指定的服务器,连接成功,则表示三次握手通过**,反之抛出异常 + +* 常用 API: + + * `OutputStream getOutputStream()`:获得字节输出流对象 + * `InputStream getInputStream()`:获得字节输入流对象 + * `void shutdownInput()`:停止接受 + * `void shutdownOutput()`:停止发送数据,终止通信 + * `SocketAddress getRemoteSocketAddress()`:返回套接字连接到的端点的地址,未连接返回 null + +ServerSocket 类: + +* 构造方法:`public ServerSocket(int port)` + +* 常用 API:`public Socket accept()`,**阻塞等待**接收一个客户端的 Socket 管道连接请求,连接成功返回一个 Socket 对象 + + 三次握手后 TCP 连接建立成功,服务器内核会把连接从 SYN 半连接队列(一次握手时在服务端建立的队列)中移出,移入 accept 全连接队列,等待进程调用 accept 函数时把连接取出。如果进程不能及时调用 accept 函数,就会造成 accept 队列溢出,最终导致建立好的 TCP 连接被丢弃 + + + +**相当于**客户端和服务器建立一个数据管道(虚连接,不是真正的物理连接),管道一般不用 close + +*** + +#### 实现TCP + +##### 开发流程 + +客户端的开发流程: + +1. 客户端要请求于服务端的 Socket 管道连接 +2. 从 Socket 通信管道中得到一个字节输出流 +3. 通过字节输出流给服务端写出数据 + +服务端的开发流程: + +1. 用 ServerSocket 注册端口 +2. 接收客户端的 Socket 管道连接 +3. 从 Socket 通信管道中得到一个字节输入流 +4. 从字节输入流中读取客户端发来的数据 + +![](https://seazean.oss-cn-beijing.aliyuncs.com/img/Java/BIO工作机制.png) + +![](https://seazean.oss-cn-beijing.aliyuncs.com/img/Java/TCP-工作模型.png) + +* 如果输出缓冲区空间不够存放主机发送的数据,则会被阻塞,输入缓冲区同理 +* 缓冲区不属于应用程序,属于内核 +* TCP 从输出缓冲区读取数据会加锁阻塞线程 + +*** + +##### 实现通信 + +需求一:客户端发送一行数据,服务端接收一行数据 + +````java +public class ClientDemo { + public static void main(String[] args) throws Exception { + // 1.客户端要请求于服务端的socket管道连接。 + Socket socket = new Socket("127.0.0.1", 8080); + // 2.从socket通信管道中得到一个字节输出流 + OutputStream os = socket.getOutputStream(); + // 3.把低级的字节输出流包装成高级的打印流。 + PrintStream ps = new PrintStream(os); + // 4.开始发消息出去 + ps.println("我是客户端"); + ps.flush();//一般不关闭IO流 + System.out.println("客户端发送完毕~~~~"); + } +} +public class ServerDemo{ + public static void main(String[] args) throws Exception { + System.out.println("----服务端启动----"); + // 1.注册端口: public ServerSocket(int port) + ServerSocket serverSocket = new ServerSocket(8080); + // 2.开始等待接收客户端的Socket管道连接。 + Socket socket = serverSocket.accept(); + // 3.从socket通信管道中得到一个字节输入流。 + InputStream is = socket.getInputStream(); + // 4.把字节输入流转换成字符输入流 + BufferedReader br = new BufferedReader(new InputStreamReader(is)); + // 6.按照行读取消息 。 + String line; + if((line = br.readLine()) != null){ + System.out.println(line); + } + } +} +```` + +需求二:客户端可以反复发送数据,服务端可以反复数据 + +```java +public class ClientDemo { + public static void main(String[] args) throws Exception { + // 1.客户端要请求于服务端的socket管道连接。 + Socket socket = new Socket("127.0.0.1",8080); + // 2.从socket通信管道中得到一个字节输出流 + OutputStream os = socket.getOutputStream(); + // 3.把低级的字节输出流包装成高级的打印流。 + PrintStream ps = new PrintStream(os); + // 4.开始发消息出去 + while(true){ + Scanner sc = new Scanner(System.in); + System.out.print("请说:"); + ps.println(sc.nextLine()); + ps.flush(); + } + } +} +public class ServerDemo{ + public static void main(String[] args) throws Exception { + System.out.println("----服务端启动----"); + // 1.注册端口: public ServerSocket(int port) + ServerSocket serverSocket = new ServerSocket(8080); + // 2.开始等待接收客户端的Socket管道连接。 + Socket socket = serverSocket.accept(); + // 3.从socket通信管道中得到一个字节输入流。 + InputStream is = socket.getInputStream(); + // 4.把字节输入流转换成字符输入流 + BufferedReader br = new BufferedReader(new InputStreamReader(is)); + // 6.按照行读取消息 。 + String line; + while((line = br.readLine()) != null){ + System.out.println(line); + } + } +} +``` + +需求三:实现一个服务端可以同时接收多个客户端的消息 + +```java +public class ClientDemo { + public static void main(String[] args) throws Exception { + Socket socket = new Socket("127.0.0.1",8080); + OutputStream os = new socket.getOutputStream(); + PrintStream ps = new PrintStream(os); + while(true){ + Scanner sc = new Scanner(System.in); + System.out.print("请说:"); + ps.println(sc.nextLine()); + ps.flush(); + } + } +} +public class ServerDemo{ + public static void main(String[] args) throws Exception { + System.out.println("----服务端启动----"); + ServerSocket serverSocket = new ServerSocket(8080); + while(true){ + // 开始等待接收客户端的Socket管道连接。 + Socket socket = serverSocket.accept(); + // 每接收到一个客户端必须为这个客户端管道分配一个独立的线程来处理与之通信。 + new ServerReaderThread(socket).start(); + } + } +} +class ServerReaderThread extends Thread{ + privat Socket socket; + public ServerReaderThread(Socket socket){this.socket = socket;} + @Override + public void run() { + try(InputStream is = socket.getInputStream(); + BufferedReader br = new BufferedReader(new InputStreamReader(is)) + ){ + String line; + while((line = br.readLine()) != null){ + sout(socket.getRemoteSocketAddress() + ":" + line); + } + }catch(Exception e){ + sout(socket.getRemoteSocketAddress() + "下线了~~~~~~"); + } + } +} +``` + +*** + +##### 伪异步 + +一个客户端要一个线程,并发越高系统瘫痪的越快,可以在服务端引入线程池,使用线程池来处理与客户端的消息通信 + +* 优势:不会引起系统的死机,可以控制并发线程的数量 + +* 劣势:同时可以并发的线程将受到限制 + +```java +public class BIOServer { + public static void main(String[] args) throws Exception { + //线程池机制 + //创建一个线程池,如果有客户端连接,就创建一个线程,与之通讯(单独写一个方法) + ExecutorService newCachedThreadPool = Executors.newCachedThreadPool(); + //创建ServerSocket + ServerSocket serverSocket = new ServerSocket(6666); + System.out.println("服务器启动了"); + while (true) { + System.out.println("线程名字 = " + Thread.currentThread().getName()); + //监听,等待客户端连接 + System.out.println("等待连接...."); + final Socket socket = serverSocket.accept(); + System.out.println("连接到一个客户端"); + //创建一个线程,与之通讯 + newCachedThreadPool.execute(new Runnable() { + public void run() { + //可以和客户端通讯 + handler(socket); + } + }); + } + } + + //编写一个handler方法,和客户端通讯 + public static void handler(Socket socket) { + try { + System.out.println("线程名字 = " + Thread.currentThread().getName()); + byte[] bytes = new byte[1024]; + //通过socket获取输入流 + InputStream inputStream = socket.getInputStream(); + int len; + //循环的读取客户端发送的数据 + while ((len = inputStream.read(bytes)) != -1) { + System.out.println("线程名字 = " + Thread.currentThread().getName()); + //输出客户端发送的数据 + System.out.println(new String(bytes, 0, read)); + } + } catch (Exception e) { + e.printStackTrace(); + } finally { + System.out.println("关闭和client的连接"); + try { + socket.close(); + } catch (Exception e) { + e.printStackTrace(); + } + } + } +} +``` + +**** + +#### 文件传输 + +##### 字节流 + +客户端:本地图片: ‪E:\seazean\图片资源\beautiful.jpg +服务端:服务器路径:E:\seazean\图片服务器 + +UUID. randomUUID() : 方法生成随机的文件名 + +**socket.shutdownOutput()**:这个必须执行,不然服务器会一直循环等待数据,最后文件损坏,程序报错 + +```java +//常量包 +public class Constants { + public static final String SRC_IMAGE = "D:\\seazean\\图片资源\\beautiful.jpg"; + public static final String SERVER_DIR = "D:\\seazean\\图片服务器\\"; + public static final String SERVER_IP = "127.0.0.1"; + public static final int SERVER_PORT = 8888; + +} +public class ClientDemo { + public static void main(String[] args) throws Exception { + Socket socket = new Socket(Constants.ERVER_IP,Constants.SERVER_PORT); + BufferedOutputStream bos=new BufferedOutputStream(socket.getOutputStream()); + //提取本机的图片上传给服务端。Constants.SRC_IMAGE + BufferedInputStream bis = new BufferedInputStream(new FileInputStream()); + byte[] buffer = new byte[1024]; + int len ; + while((len = bis.read(buffer)) != -1) { + bos.write(buffer, 0 ,len); + } + bos.flush();// 刷新图片数据到服务端!! + socket.shutdownOutput();// 告诉服务端我的数据已经发送完毕,不要在等我了! + bis.close(); + + //等待着服务端的响应数据!! + BufferedReader br = new BufferedReader( + new InputStreamReader(socket.getInputStream())); + System.out.println("收到服务端响应:"+br.readLine()); + } +} +``` + +```java +public class ServerDemo { + public static void main(String[] args) throws Exception { + System.out.println("----服务端启动----"); + // 1.注册端口: + ServerSocket serverSocket = new ServerSocket(Constants.SERVER_PORT); + // 2.定义一个循环不断的接收客户端的连接请求 + while(true){ + // 3.开始等待接收客户端的Socket管道连接。 + Socket socket = serverSocket.accept(); + // 4.每接收到一个客户端必须为这个客户端管道分配一个独立的线程来处理与之通信。 + new ServerReaderThread(socket).start(); + } + } +} +class ServerReaderThread extends Thread{ + private Socket socket ; + public ServerReaderThread(Socket socket){this.socket = socket;} + @Override + public void run() { + try{ + InputStream is = socket.getInputStream(); + BufferedInputStream bis = new BufferedInputStream(is); + BufferedOutputStream bos = new BufferedOutputStream( + new FileOutputStream + (Constants.SERVER_DIR+UUID.randomUUID().toString()+".jpg")); + byte[] buffer = new byte[1024]; + int len; + while((len = bis.read(buffer)) != -1){ + bos.write(buffer,0,len); + } + bos.close(); + System.out.println("服务端接收完毕了!"); + + // 4.响应数据给客户端 + PrintStream ps = new PrintStream(socket.getOutputStream()); + ps.println("您好,已成功接收您上传的图片!"); + ps.flush(); + Thread.sleep(10000); + }catch (Exception e){ + sout(socket.getRemoteSocketAddress() + "下线了"); + } + } +} +``` + +**** + +##### 数据流 + +构造方法: + +* `DataOutputStream(OutputStream out)` : 创建一个新的数据输出流,以将数据写入指定的底层输出流 +* `DataInputStream(InputStream in)` : 创建使用指定的底层 InputStream 的 DataInputStream + +常用API: + +* `final void writeUTF(String str)` : 使用机器无关的方式使用 UTF-8 编码将字符串写入底层输出流 +* `final String readUTF()` : 读取以 modified UTF-8 格式编码的 Unicode 字符串,返回 String 类型 + +```java +public class Client { + public static void main(String[] args) { + InputStream is = new FileInputStream("path"); + // 1、请求与服务端的Socket链接 + Socket socket = new Socket("127.0.0.1" , 8888); + // 2、把字节输出流包装成一个数据输出流 + DataOutputStream dos = new DataOutputStream(socket.getOutputStream()); + // 3、先发送上传文件的后缀给服务端 + dos.writeUTF(".png"); + // 4、把文件数据发送给服务端进行接收 + byte[] buffer = new byte[1024]; + int len; + while((len = is.read(buffer)) > 0 ){ + dos.write(buffer , 0 , len); + } + dos.flush(); + Thread.sleep(10000); + } +} + +public class Server { + public static void main(String[] args) { + ServerSocket ss = new ServerSocket(8888); + Socket socket = ss.accept(); + // 1、得到一个数据输入流读取客户端发送过来的数据 + DataInputStream dis = new DataInputStream(socket.getInputStream()); + // 2、读取客户端发送过来的文件类型 + String suffix = dis.readUTF(); + // 3、定义一个字节输出管道负责把客户端发来的文件数据写出去 + OutputStream os = new FileOutputStream("path"+ + UUID.randomUUID().toString()+suffix); + // 4、从数据输入流中读取文件数据,写出到字节输出流中去 + byte[] buffer = new byte[1024]; + int len; + while((len = dis.read(buffer)) > 0){ + os.write(buffer,0, len); + } + os.close(); + System.out.println("服务端接收文件保存成功!"); + } +} +``` + +*** + +## NIO + +### 基本介绍 + +**NIO的介绍**: + +Java NIO(New IO、Java non-blocking IO),从 Java 1.4 版本开始引入的一个新的 IO API,可以替代标准的 Java IO API,NIO 支持面向缓冲区的、基于通道的 IO 操作,以更加高效的方式进行文件的读写操作 + +* NIO 有三大核心部分:**Channel(通道),Buffer(缓冲区),Selector(选择器)** +* NIO 是非阻塞 IO,传统 IO 的 read 和 write 只能阻塞执行,线程在读写 IO 期间不能干其他事情,比如调用 socket.accept(),如果服务器没有数据传输过来,线程就一直阻塞,而 NIO 中可以配置 Socket 为非阻塞模式 +* NIO 可以做到用一个线程来处理多个操作的。假设有 1000 个请求过来,根据实际情况可以分配 20 或者 80 个线程来处理,不像之前的阻塞 IO 那样分配 1000 个 + +NIO 和 BIO 的比较: + +* BIO 以流的方式处理数据,而 NIO 以块的方式处理数据,块 I/O 的效率比流 I/O 高很多 + +* BIO 是阻塞的,NIO 则是非阻塞的 + +* BIO 基于字节流和字符流进行操作,而 NIO 基于 Channel 和 Buffer 进行操作,数据从通道读取到缓冲区中,或者从缓冲区写入到通道中。Selector 用于监听多个通道的事件(比如:连接请求,数据到达等),因此使用单个线程就可以监听多个客户端通道 + + | NIO | BIO | + | ------------------------- | ------------------- | + | 面向缓冲区(Buffer) | 面向流(Stream) | + | 非阻塞(Non Blocking IO) | 阻塞IO(Blocking IO) | + | 选择器(Selectors) | | + +*** + +### 实现原理 + +NIO 三大核心部分:Channel (通道)、Buffer (缓冲区)、Selector (选择器) + +* Buffer 缓冲区 + + 缓冲区本质是一块可以写入数据、读取数据的内存,**底层是一个数组**,这块内存被包装成 NIO Buffer 对象,并且提供了方法用来操作这块内存,相比较直接对数组的操作,Buffer 的 API 更加容易操作和管理 + +* Channel 通道 + + Java NIO 的通道类似流,不同的是既可以从通道中读取数据,又可以写数据到通道,流的读写通常是单向的,通道可以非阻塞读取和写入通道,支持读取或写入缓冲区,也支持异步地读写 + +* Selector 选择器 + + Selector 是一个 Java NIO 组件,能够检查一个或多个 NIO 通道,并确定哪些通道已经准备好进行读取或写入,这样一个单独的线程可以管理多个 channel,从而管理多个网络连接,提高效率 + +NIO 的实现框架: + +![](https://seazean.oss-cn-beijing.aliyuncs.com/img/Java/NIO框架.png) + +* 每个 Channel 对应一个 Buffer +* 一个线程对应 Selector , 一个 Selector 对应多个 Channel(连接) +* 程序切换到哪个 Channel 是由事件决定的,Event 是一个重要的概念 +* Selector 会根据不同的事件,在各个通道上切换 +* Buffer 是一个内存块 , 底层是一个数组 +* 数据的读取写入是通过 Buffer 完成的 , BIO 中要么是输入流,或者是输出流,不能双向,NIO 的 Buffer 是可以读也可以写, flip() 切换 Buffer 的工作模式 + +Java NIO 系统的核心在于:通道和缓冲区,通道表示打开的 IO 设备(例如:文件、 套接字)的连接。若要使用 NIO 系统,获取用于连接 IO 设备的通道以及用于容纳数据的缓冲区,然后操作缓冲区,对数据进行处理。简而言之,Channel 负责传输, Buffer 负责存取数据 + +*** + +### 缓冲区 + +#### 基本介绍 + +缓冲区(Buffer):缓冲区本质上是一个**可以读写数据的内存块**,用于特定基本数据类型的容器,用于与 NIO 通道进行交互,数据是从通道读入缓冲区,从缓冲区写入通道中的 + +![](https://seazean.oss-cn-beijing.aliyuncs.com/img/Java/NIO-Buffer.png) + +**Buffer 底层是一个数组**,可以保存多个相同类型的数据,根据数据类型不同 ,有以下 Buffer 常用子类:ByteBuffer、CharBuffer、ShortBuffer、IntBuffer、LongBuffer、FloatBuffer、DoubleBuffer + +*** + +#### 基本属性 + +* 容量(capacity):作为一个内存块,Buffer 具有固定大小,缓冲区容量不能为负,并且创建后不能更改 + +* 限制 (limit):表示缓冲区中可以操作数据的大小(limit 后数据不能进行读写),缓冲区的限制不能为负,并且不能大于其容量。写入模式,limit 等于 buffer 的容量;读取模式下,limit 等于写入的数据量 + +* 位置(position):**下一个要读取或写入的数据的索引**,缓冲区的位置不能为负,并且不能大于其限制 + +* 标记(mark)与重置(reset):标记是一个索引,通过 Buffer 中的 mark() 方法指定 Buffer 中一个特定的位置,可以通过调用 reset() 方法恢复到这个 position + +* 位置、限制、容量遵守以下不变式: **0 <= position <= limit <= capacity** + + + +*** + +#### 常用API + +`static XxxBuffer allocate(int capacity)`:创建一个容量为 capacity 的 XxxBuffer 对象 + +Buffer 基本操作: + +| 方法 | 说明 | +| ------------------------------------------- | ------------------------------------------------------------ | +| public Buffer clear() | 清空缓冲区,不清空内容,将位置设置为零,限制设置为容量 | +| public Buffer flip() | 翻转缓冲区,将缓冲区的界限设置为当前位置,position 置 0 | +| public int capacity() | 返回 Buffer的 capacity 大小 | +| public final int limit() | 返回 Buffer 的界限 limit 的位置 | +| public Buffer limit(int n) | 设置缓冲区界限为 n | +| public Buffer mark() | 在此位置对缓冲区设置标记 | +| public final int position() | 返回缓冲区的当前位置 position | +| public Buffer position(int n) | 设置缓冲区的当前位置为n | +| public Buffer reset() | 将位置 position 重置为先前 mark 标记的位置 | +| public Buffer rewind() | 将位置设为为 0,取消设置的 mark | +| public final int remaining() | 返回当前位置 position 和 limit 之间的元素个数 | +| public final boolean hasRemaining() | 判断缓冲区中是否还有元素 | +| public static ByteBuffer wrap(byte[] array) | 将一个字节数组包装到缓冲区中 | +| abstract ByteBuffer asReadOnlyBuffer() | 创建一个新的只读字节缓冲区 | +| public abstract ByteBuffer compact() | 缓冲区当前位置与其限制(如果有)之间的字节被复制到缓冲区的开头 | + +Buffer 数据操作: + +| 方法 | 说明 | +| ------------------------------------------------- | ----------------------------------------------- | +| public abstract byte get() | 读取该缓冲区当前位置的单个字节,然后位置 + 1 | +| public ByteBuffer get(byte[] dst) | 读取多个字节到字节数组 dst 中 | +| public abstract byte get(int index) | 读取指定索引位置的字节,不移动 position | +| public abstract ByteBuffer put(byte b) | 将给定单个字节写入缓冲区的当前位置,position+1 | +| public final ByteBuffer put(byte[] src) | 将 src 字节数组写入缓冲区的当前位置 | +| public abstract ByteBuffer put(int index, byte b) | 将指定字节写入缓冲区的索引位置,不移动 position | + +提示:"\n",占用两个字节 + +**** + +#### 读写数据 + +使用 Buffer 读写数据一般遵循以下四个步骤: + +* 写入数据到 Buffer +* 调用 flip()方法,转换为读取模式 +* 从 Buffer 中读取数据 +* 调用 buffer.clear() 方法清除缓冲区(不是清空了数据,只是重置指针) + +```java +public class TestBuffer { + @Test + public void test(){ + String str = "seazean"; + //1. 分配一个指定大小的缓冲区 + ByteBuffer buffer = ByteBuffer.allocate(1024); + System.out.println("-----------------allocate()----------------"); + System.out.println(bufferf.position());//0 + System.out.println(buffer.limit());//1024 + System.out.println(buffer.capacity());//1024 + + //2. 利用 put() 存入数据到缓冲区中 + buffer.put(str.getBytes()); + System.out.println("-----------------put()----------------"); + System.out.println(bufferf.position());//7 + System.out.println(buffer.limit());//1024 + System.out.println(buffer.capacity());//1024 + + //3. 切换读取数据模式 + buffer.flip(); + System.out.println("-----------------flip()----------------"); + System.out.println(buffer.position());//0 + System.out.println(buffer.limit());//7 + System.out.println(buffer.capacity());//1024 + + //4. 利用 get() 读取缓冲区中的数据 + byte[] dst = new byte[buffer.limit()]; + buffer.get(dst); + System.out.println(dst.length); + System.out.println(new String(dst, 0, dst.length)); + System.out.println(buffer.position());//7 + System.out.println(buffer.limit());//7 + + //5. clear() : 清空缓冲区. 但是缓冲区中的数据依然存在,但是处于“被遗忘”状态 + System.out.println(buffer.hasRemaining());//true + buffer.clear(); + System.out.println(buffer.hasRemaining());//true + System.out.println("-----------------clear()----------------"); + System.out.println(buffer.position());//0 + System.out.println(buffer.limit());//1024 + System.out.println(buffer.capacity());//1024 + } +} +``` + +**** + +#### 粘包拆包 + +网络上有多条数据发送给服务端,数据之间使用 \n 进行分隔,但这些数据在接收时,被进行了重新组合 + +```java +// Hello,world\n +// I'm zhangsan\n +// How are you?\n +------ > 黏包,半包 +// Hello,world\nI'm zhangsan\nHo +// w are you?\n +``` + +```java +public static void main(String[] args) { + ByteBuffer source = ByteBuffer.allocate(32); + // 11 24 + source.put("Hello,world\nI'm zhangsan\nHo".getBytes()); + split(source); + + source.put("w are you?\nhaha!\n".getBytes()); + split(source); +} + +private static void split(ByteBuffer source) { + source.flip(); + int oldLimit = source.limit(); + for (int i = 0; i < oldLimit; i++) { + if (source.get(i) == '\n') { + // 根据数据的长度设置缓冲区 + ByteBuffer target = ByteBuffer.allocate(i + 1 - source.position()); + // 0 ~ limit + source.limit(i + 1); + target.put(source); // 从source 读,向 target 写 + // debugAll(target); 访问 buffer 的方法 + source.limit(oldLimit); + } + } + // 访问过的数据复制到开头 + source.compact(); +} +``` + +**** + +### 直接内存 + +#### 基本介绍 + +Byte Buffer 有两种类型,一种是基于直接内存(也就是非堆内存),另一种是非直接内存(也就是堆内存) + +Direct Memory 优点: + +* Java 的 NIO 库允许 Java 程序使用直接内存,使用 native 函数直接分配堆外内存 +* **读写性能高**,读写频繁的场合可能会考虑使用直接内存 +* 大大提高 IO 性能,避免了在 Java 堆和 native 堆来回复制数据 + +直接内存缺点: + +* 不能使用内核缓冲区 Page Cache 的缓存优势,无法缓存最近被访问的数据和使用预读功能 +* 分配回收成本较高,不受 JVM 内存回收管理 +* 可能导致 OutOfMemoryError 异常:OutOfMemoryError: Direct buffer memory +* 回收依赖 System.gc() 的调用,但这个调用 JVM 不保证执行、也不保证何时执行,行为是不可控的。程序一般需要自行管理,成对去调用 malloc、free + +应用场景: + +* 传输很大的数据文件,数据的生命周期很长,导致 Page Cache 没有起到缓存的作用,一般采用直接 IO 的方式 +* 适合频繁的 IO 操作,比如网络并发场景 + +数据流的角度: + +* 非直接内存的作用链:本地 IO → 内核缓冲区→ 用户(JVM)缓冲区 →内核缓冲区 → 本地 IO +* 直接内存是:本地 IO → 直接内存 → 本地 IO + +JVM 直接内存图解: + + + + + +*** + +#### 通信原理 + +堆外内存不受 JVM GC 控制,可以使用堆外内存进行通信,防止 GC 后缓冲区位置发生变化的情况 + +NIO 使用的 SocketChannel 也是使用的堆外内存,源码解析: + +* SocketChannel#write(java.nio.ByteBuffer) → SocketChannelImpl#write(java.nio.ByteBuffer) + + ```java + public int write(ByteBuffer var1) throws IOException { + do { + var3 = IOUtil.write(this.fd, var1, -1L, nd); + } while(var3 == -3 && this.isOpen()); + } + ``` + +* IOUtil#write(java.io.FileDescriptor, java.nio.ByteBuffer, long, sun.nio.ch.NativeDispatcher) + + ```java + static int write(FileDescriptor var0, ByteBuffer var1, long var2, NativeDispatcher var4) { + // 【判断是否是直接内存,是则直接写出,不是则封装到直接内存】 + if (var1 instanceof DirectBuffer) { + return writeFromNativeBuffer(var0, var1, var2, var4); + } else { + //.... + // 从堆内buffer拷贝到堆外buffer + ByteBuffer var8 = Util.getTemporaryDirectBuffer(var7); + var8.put(var1); + //... + // 从堆外写到内核缓冲区 + int var9 = writeFromNativeBuffer(var0, var8, var2, var4); + } + } + ``` + +* 读操作相同 + +*** + +#### 分配回收 + +直接内存创建 Buffer 对象:`static XxxBuffer allocateDirect(int capacity)` + +DirectByteBuffer 源码分析: + +```java +DirectByteBuffer(int cap) { + //.... + long base = 0; + try { + // 分配直接内存 + base = unsafe.allocateMemory(size); + } + // 内存赋值 + unsafe.setMemory(base, size, (byte) 0); + if (pa && (base % ps != 0)) { + address = base + ps - (base & (ps - 1)); + } else { + address = base; + } + // 创建回收函数 + cleaner = Cleaner.create(this, new Deallocator(base, size, cap)); +} +private static class Deallocator implements Runnable { + public void run() { + unsafe.freeMemory(address); + //... + } +} +``` + +**分配和回收原理**: + +* 使用了 Unsafe 对象的 allocateMemory 方法完成直接内存的分配,setMemory 方法完成赋值 +* ByteBuffer 的实现类内部,使用了 Cleaner(虚引用)来监测 ByteBuffer 对象,一旦 ByteBuffer 对象被垃圾回收,那么 ReferenceHandler 线程通过 Cleaner 的 clean 方法调用 Deallocator 的 run方法,最后通过 freeMemory 来释放直接内存 + +```java +/** + * 直接内存分配的底层原理:Unsafe + */ +public class Demo1_27 { + static int _1Gb = 1024 * 1024 * 1024; + + public static void main(String[] args) throws IOException { + Unsafe unsafe = getUnsafe(); + // 分配内存 + long base = unsafe.allocateMemory(_1Gb); + unsafe.setMemory(base, _1Gb, (byte) 0); + System.in.read(); + // 释放内存 + unsafe.freeMemory(base); + System.in.read(); + } + + public static Unsafe getUnsafe() { + try { + Field f = Unsafe.class.getDeclaredField("theUnsafe"); + f.setAccessible(true); + Unsafe unsafe = (Unsafe) f.get(null); + return unsafe; + } catch (NoSuchFieldException | IllegalAccessException e) { + throw new RuntimeException(e); + } + } +} +``` + +**** + +#### 共享内存 + +FileChannel 提供 map 方法返回 MappedByteBuffer 对象,把文件映射到内存,通常情况可以映射整个文件,如果文件比较大,可以进行分段映射,完成映射后对物理内存的操作会被**同步**到硬盘上 + +FileChannel 中的成员属性: + +* MapMode.mode:内存映像文件访问的方式,共三种: + * `MapMode.READ_ONLY`:只读,修改得到的缓冲区将导致抛出异常 + * `MapMode.READ_WRITE`:读/写,对缓冲区的更改最终将写入文件,但此次修改对映射到同一文件的其他程序不一定是可见 + * `MapMode.PRIVATE`:私用,可读可写,但是修改的内容不会写入文件,只是 buffer 自身的改变 + +* `public final FileLock lock()`:获取此文件通道的排他锁 + +MappedByteBuffer,可以让文件在直接内存(堆外内存)中进行修改,这种方式叫做**内存映射**,可以直接调用系统底层的缓存,没有 JVM 和 OS 之间的复制操作,提高了传输效率,作用: + +* **可以用于进程间的通信,能达到共享内存页的作用**,但在高并发下要对文件内存进行加锁,防止出现读写内容混乱和不一致性,Java 提供了文件锁 FileLock,但在父/子进程中锁定后另一进程会一直等待,效率不高 +* 读写那些太大而不能放进内存中的文件,**分段映射** + +MappedByteBuffer 较之 ByteBuffer 新增的三个方法: + +* `final MappedByteBuffer force()`:缓冲区是 READ_WRITE 模式下,对缓冲区内容的修改**强制写入文件** +* `final MappedByteBuffer load()`:将缓冲区的内容载入物理内存,并返回该缓冲区的引用 +* `final boolean isLoaded()`:如果缓冲区的内容在物理内存中,则返回真,否则返回假 + +```java +public class MappedByteBufferTest { + public static void main(String[] args) throws Exception { + // 读写模式 + RandomAccessFile ra = new RandomAccessFile("1.txt", "rw"); + // 获取对应的通道 + FileChannel channel = ra.getChannel(); + + /** + * 参数1 FileChannel.MapMode.READ_WRITE 使用的读写模式 + * 参数2 0: 文件映射时的起始位置 + * 参数3 5: 是映射到内存的大小(不是索引位置),即将 1.txt 的多少个字节映射到内存 + * 可以直接修改的范围就是 0-5 + * 实际类型 DirectByteBuffer + */ + MappedByteBuffer buffer = channel.map(FileChannel.MapMode.READ_WRITE, 0, 5); + + buffer.put(0, (byte) 'H'); + buffer.put(3, (byte) '9'); + buffer.put(5, (byte) 'Y'); //IndexOutOfBoundsException + + ra.close(); + System.out.println("修改成功~~"); + } +} +``` + +从硬盘上将文件读入内存,要经过文件系统进行数据拷贝,拷贝操作是由文件系统和硬件驱动实现。通过内存映射的方法访问硬盘上的文件,拷贝数据的效率要比 read 和 write 系统调用高: + +* read() 是系统调用,首先将文件从硬盘拷贝到内核空间的一个缓冲区,再将这些数据拷贝到用户空间,实际上进行了两次数据拷贝 +* mmap() 也是系统调用,但没有进行数据拷贝,当缺页中断发生时,直接将文件从硬盘拷贝到共享内存,只进行了一次数据拷贝 + +注意:mmap 的文件映射,在 Full GC 时才会进行释放,如果需要手动清除内存映射文件,可以反射调用 sun.misc.Cleaner 方法 + +参考文章: + +*** + +### 通道 + +#### 基本介绍 + +通道(Channel):表示 IO 源与目标打开的连接,Channel 类似于传统的流,只不过 Channel 本身不能直接访问数据,Channel 只能与 Buffer **进行交互** + +1. NIO 的通道类似于流,但有些区别如下: + * 通道可以同时进行读写,而流只能读或者只能写 + * 通道可以实现异步读写数据 + * 通道可以从缓冲读数据,也可以写数据到缓冲 + +2. BIO 中的 Stream 是单向的,NIO 中的 Channel 是双向的,可以读操作,也可以写操作 + +3. Channel 在 NIO 中是一个接口:`public interface Channel extends Closeable{}` + +Channel 实现类: + +* FileChannel:用于读取、写入、映射和操作文件的通道,**只能工作在阻塞模式下** + + * 通过 FileInputStream 获取的 Channel 只能读 + * 通过 FileOutputStream 获取的 Channel 只能写 + * 通过 RandomAccessFile 是否能读写根据构造 RandomAccessFile 时的读写模式决定 + +* DatagramChannel:通过 UDP 读写网络中的数据通道 + +* SocketChannel:通过 TCP 读写网络中的数据 + +* ServerSocketChannel:可以**监听**新进来的 TCP 连接,对每一个新进来的连接都会创建一个 SocketChannel + + 提示:ServerSocketChanne 类似 ServerSocket、SocketChannel 类似 Socket + +*** + +#### 常用API + +获取 Channel 方式: + +* 对支持通道的对象调用 `getChannel()` 方法 +* 通过通道的静态方法 `open()` 打开并返回指定通道 +* 使用 Files 类的静态方法 `newByteChannel()` 获取字节通道 + +Channel 基本操作:**读写都是相对于内存来看,也就是缓冲区** + +| 方法 | 说明 | +| ------------------------------------------ | -------------------------------------------------------- | +| public abstract int read(ByteBuffer dst) | 从 Channel 中读取数据到 ByteBuffer,从 position 开始储存 | +| public final long read(ByteBuffer[] dsts) | 将 Channel 中的数据分散到 ByteBuffer[] | +| public abstract int write(ByteBuffer src) | 将 ByteBuffer 中的数据写入 Channel,从 position 开始写出 | +| public final long write(ByteBuffer[] srcs) | 将 ByteBuffer[] 到中的数据聚集到 Channel | +| public abstract long position() | 返回此通道的文件位置 | +| FileChannel position(long newPosition) | 设置此通道的文件位置 | +| public abstract long size() | 返回此通道的文件的当前大小 | + +**SelectableChannel 的操作 API**: + +| 方法 | 说明 | +| -------------------------------------------------------- | ------------------------------------------------------------ | +| SocketChannel accept() | 如果通道处于非阻塞模式,没有请求连接时此方法将立即返回 NULL,否则将阻塞直到有新的连接或发生 I/O 错误,**通过该方法返回的套接字通道将处于阻塞模式** | +| SelectionKey register(Selector sel, int ops) | 将通道注册到选择器上,并指定监听事件 | +| SelectionKey register(Selector sel, int ops, Object att) | 将通道注册到选择器上,并在当前通道**绑定一个附件对象**,Object 代表可以是任何类型 | + +**** + +#### 文件读写 + +```java +public class ChannelTest { + @Test + public void write() throws Exception{ + // 1、字节输出流通向目标文件 + FileOutputStream fos = new FileOutputStream("data01.txt"); + // 2、得到字节输出流对应的通道 【FileChannel】 + FileChannel channel = fos.getChannel(); + // 3、分配缓冲区 + ByteBuffer buffer = ByteBuffer.allocate(1024); + buffer.put("hello,黑马Java程序员!".getBytes()); + // 4、把缓冲区切换成写出模式 + buffer.flip(); + channel.write(buffer); + channel.close(); + System.out.println("写数据到文件中!"); + } + @Test + public void read() throws Exception { + // 1、定义一个文件字节输入流与源文件接通 + FileInputStream fis = new FileInputStream("data01.txt"); + // 2、需要得到文件字节输入流的文件通道 + FileChannel channel = fis.getChannel(); + // 3、定义一个缓冲区 + ByteBuffer buffer = ByteBuffer.allocate(1024); + // 4、读取数据到缓冲区 + channel.read(buffer); + buffer.flip(); + // 5、读取出缓冲区中的数据并输出即可 + String rs = new String(buffer.array(),0,buffer.remaining()); + System.out.println(rs); + } +} +``` + +*** + +#### 文件复制 + +Channel 的方法:**sendfile 实现零拷贝** + +* `abstract long transferFrom(ReadableByteChannel src, long position, long count)`:从给定的可读字节通道将字节传输到该通道的文件中 + * src:源通道 + * position:文件中要进行传输的位置,必须是非负的 + * count:要传输的最大字节数,必须是非负的 + +* `abstract long transferTo(long position, long count, WritableByteChannel target)`:将该通道文件的字节传输到给定的可写字节通道。 + * position:传输开始的文件中的位置; 必须是非负的 + * count:要传输的最大字节数; 必须是非负的 + * target:目标通道 + +文件复制的两种方式: + +1. Buffer +2. 使用上述两种方法 + +![](https://seazean.oss-cn-beijing.aliyuncs.com/img/Java/NIO-复制文件.png) + +```java +public class ChannelTest { + @Test + public void copy1() throws Exception { + File srcFile = new File("C:\\壁纸.jpg"); + File destFile = new File("C:\\Users\\壁纸new.jpg"); + // 得到一个字节字节输入流 + FileInputStream fis = new FileInputStream(srcFile); + // 得到一个字节输出流 + FileOutputStream fos = new FileOutputStream(destFile); + // 得到的是文件通道 + FileChannel isChannel = fis.getChannel(); + FileChannel osChannel = fos.getChannel(); + // 分配缓冲区 + ByteBuffer buffer = ByteBuffer.allocate(1024); + while(true){ + // 必须先清空缓冲然后再写入数据到缓冲区 + buffer.clear(); + // 开始读取一次数据 + int flag = isChannel.read(buffer); + if(flag == -1){ + break; + } + // 已经读取了数据 ,把缓冲区的模式切换成可读模式 + buffer.flip(); + // 把数据写出到 + osChannel.write(buffer); + } + isChannel.close(); + osChannel.close(); + System.out.println("复制完成!"); + } + + @Test + public void copy02() throws Exception { + // 1、字节输入管道 + FileInputStream fis = new FileInputStream("data01.txt"); + FileChannel isChannel = fis.getChannel(); + // 2、字节输出流管道 + FileOutputStream fos = new FileOutputStream("data03.txt"); + FileChannel osChannel = fos.getChannel(); + // 3、复制 + osChannel.transferFrom(isChannel,isChannel.position(),isChannel.size()); + isChannel.close(); + osChannel.close(); + } + + @Test + public void copy03() throws Exception { + // 1、字节输入管道 + FileInputStream fis = new FileInputStream("data01.txt"); + FileChannel isChannel = fis.getChannel(); + // 2、字节输出流管道 + FileOutputStream fos = new FileOutputStream("data04.txt"); + FileChannel osChannel = fos.getChannel(); + // 3、复制 + isChannel.transferTo(isChannel.position() , isChannel.size() , osChannel); + isChannel.close(); + osChannel.close(); + } +} +``` + +*** + +#### 分散聚集 + +分散读取(Scatter ):是指把 Channel 通道的数据读入到多个缓冲区中去 + +聚集写入(Gathering ):是指将多个 Buffer 中的数据聚集到 Channel + +```java +public class ChannelTest { + @Test + public void test() throws IOException{ + // 1、字节输入管道 + FileInputStream is = new FileInputStream("data01.txt"); + FileChannel isChannel = is.getChannel(); + // 2、字节输出流管道 + FileOutputStream fos = new FileOutputStream("data02.txt"); + FileChannel osChannel = fos.getChannel(); + // 3、定义多个缓冲区做数据分散 + ByteBuffer buffer1 = ByteBuffer.allocate(4); + ByteBuffer buffer2 = ByteBuffer.allocate(1024); + ByteBuffer[] buffers = {buffer1 , buffer2}; + // 4、从通道中读取数据分散到各个缓冲区 + isChannel.read(buffers); + // 5、从每个缓冲区中查询是否有数据读取到了 + for(ByteBuffer buffer : buffers){ + buffer.flip();// 切换到读数据模式 + System.out.println(new String(buffer.array() , 0 , buffer.remaining())); + } + // 6、聚集写入到通道 + osChannel.write(buffers); + isChannel.close(); + osChannel.close(); + System.out.println("文件复制~~"); + } +} +``` + +*** + +### 选择器 + +#### 基本介绍 + +选择器(Selector) 是 SelectableChannle 对象的**多路复用器**,Selector 可以同时监控多个通道的状况,利用 Selector 可使一个单独的线程管理多个 Channel,**Selector 是非阻塞 IO 的核心** + +![](https://seazean.oss-cn-beijing.aliyuncs.com/img/Java/NIO-Selector.png) + +* Selector 能够检测多个注册的通道上是否有事件发生(多个 Channel 以事件的方式可以注册到同一个 Selector),如果有事件发生,就获取事件然后针对每个事件进行相应的处理,就可以只用一个单线程去管理多个通道,也就是管理多个连接和请求 +* 只有在连接/通道真正有读写事件发生时,才会进行读写,就大大地减少了系统开销,并且不必为每个连接都创建一个线程,不用去维护多个线程 +* 避免了多线程之间的上下文切换导致的开销 + +*** + +#### 常用API + +创建 Selector:`Selector selector = Selector.open();` + +向选择器注册通道:`SelectableChannel.register(Selector sel, int ops, Object att)` + +* 参数一:选择器,指定当前 Channel 注册到的选择器 +* 参数二:选择器对通道的监听事件,监听的事件类型用四个常量表示 + * 读 : SelectionKey.OP_READ (1) + * 写 : SelectionKey.OP_WRITE (4) + * 连接 : SelectionKey.OP_CONNECT (8) + * 接收 : SelectionKey.OP_ACCEPT (16) + * 若不止监听一个事件,使用位或操作符连接:`int interest = SelectionKey.OP_READ | SelectionKey.OP_WRITE` +* 参数三:可以关联一个附件,可以是任何对象 + +**Selector API**: + +| 方法 | 说明 | +| ------------------------------------------------ | ------------------------------------------- | +| public static Selector open() | 打开选择器 | +| public abstract void close() | 关闭此选择器 | +| public abstract int select() | **阻塞**选择一组通道准备好进行 I/O 操作的键 | +| public abstract int select(long timeout) | **阻塞**等待 timeout 毫秒 | +| public abstract int selectNow() | 获取一下,**不阻塞**,立刻返回 | +| public abstract Selector wakeup() | 唤醒正在阻塞的 selector | +| public abstract Set selectedKeys() | 返回此选择器的选择键集 | + +SelectionKey API: + +| 方法 | 说明 | +| ------------------------------------------- | -------------------------------------------------- | +| public abstract void cancel() | 取消该键的通道与其选择器的注册 | +| public abstract SelectableChannel channel() | 返回创建此键的通道,该方法在取消键之后仍将返回通道 | +| public final Object attachment() | 返回当前 key 关联的附件 | +| public final boolean isAcceptable() | 检测此密钥的通道是否已准备好接受新的套接字连接 | +| public final boolean isConnectable() | 检测此密钥的通道是否已完成或未完成其套接字连接操作 | +| public final boolean isReadable() | 检测此密钥的频道是否可以阅读 | +| public final boolean isWritable() | 检测此密钥的通道是否准备好进行写入 | + +基本步骤: + +```java +//1.获取通道 +ServerSocketChannel ssChannel = ServerSocketChannel.open(); +//2.切换非阻塞模式 +ssChannel.configureBlocking(false); +//3.绑定连接 +ssChannel.bin(new InetSocketAddress(9999)); +//4.获取选择器 +Selector selector = Selector.open(); +//5.将通道注册到选择器上,并且指定“监听接收事件” +ssChannel.register(selector, SelectionKey.OP_ACCEPT); +``` + +*** + +### NIO实现 + +#### 常用API + +* SelectableChannel_API + + | 方法 | 说明 | + | ------------------------------------------------------------ | -------------------------------------------- | + | public final SelectableChannel configureBlocking(boolean block) | 设置此通道的阻塞模式 | + | public final SelectionKey register(Selector sel, int ops) | 向给定的选择器注册此通道,并选择关注的的事件 | + +* SocketChannel_API: + + | 方法 | 说明 | + | :------------------------------------------------------ | ------------------------------ | + | public static SocketChannel open() | 打开套接字通道 | + | public static SocketChannel open(SocketAddress remote) | 打开套接字通道并连接到远程地址 | + | public abstract boolean connect(SocketAddress remote) | 连接此通道的到远程地址 | + | public abstract SocketChannel bind(SocketAddress local) | 将通道的套接字绑定到本地地址 | + | public abstract SocketAddress getLocalAddress() | 返回套接字绑定的本地套接字地址 | + | public abstract SocketAddress getRemoteAddress() | 返回套接字连接的远程套接字地址 | + +* ServerSocketChannel_API: + + | 方法 | 说明 | + | ---------------------------------------------------------- | ------------------------------------------------------------ | + | public static ServerSocketChannel open() | 打开服务器套接字通道 | + | public final ServerSocketChannel bind(SocketAddress local) | 将通道的套接字绑定到本地地址,并配置套接字以监听连接 | + | public abstract SocketChannel accept() | 接受与此通道套接字的连接,通过此方法返回的套接字通道将处于阻塞模式 | + + * 如果 ServerSocketChannel 处于非阻塞模式,如果没有挂起连接,则此方法将立即返回 null + * 如果通道处于阻塞模式,如果没有挂起连接将无限期地阻塞,直到有新的连接或发生 I/O 错误 + +*** + +#### 代码实现 + +服务端 : + +1. 获取通道,当客户端连接服务端时,服务端会通过 `ServerSocketChannel.accept` 得到 SocketChannel + +2. 切换非阻塞模式 + +3. 绑定连接 + +4. 获取选择器 + +5. 将通道注册到选择器上,并且指定监听接收事件 + +6. **轮询式**的获取选择器上已经准备就绪的事件 + +客户端: + +1. 获取通道:`SocketChannel sc = SocketChannel.open(new InetSocketAddress(HOST, PORT))` +2. 切换非阻塞模式 +3. 分配指定大小的缓冲区:`ByteBuffer buffer = ByteBuffer.allocate(1024)` +4. 发送数据给服务端 + +37 行代码,如果判断条件改为 !=-1,需要客户端 close 一下 + +```java +public class Server { + public static void main(String[] args){ + // 1、获取通道 + ServerSocketChannel serverSocketChannel = ServerSocketChannel.open(); + // 2、切换为非阻塞模式 + serverSocketChannel.configureBlocking(false); + // 3、绑定连接的端口 + serverSocketChannel.bind(new InetSocketAddress(9999)); + // 4、获取选择器Selector + Selector selector = Selector.open(); + // 5、将通道都注册到选择器上去,并且开始指定监听接收事件 + serverSocketChannel.register(selector, SelectionKey.OP_ACCEPT); + // 6、使用Selector选择器阻塞等待轮已经就绪好的事件 + while (selector.select() > 0) { + System.out.println("----开始新一轮的时间处理----"); + // 7、获取选择器中的所有注册的通道中已经就绪好的事件 + Set selectionKeys = selector.selectedKeys(); + Iterator it = selectionKeys.iterator(); + // 8、开始遍历这些准备好的事件 + while (it.hasNext()) { + SelectionKey key = it.next();// 提取当前这个事件 + // 9、判断这个事件具体是什么 + if (key.isAcceptable()) { + // 10、直接获取当前接入的客户端通道 + SocketChannel socketChannel = serverSocketChannel.accept(); + // 11 、切换成非阻塞模式 + socketChannel.configureBlocking(false); + /* + ByteBuffer buffer = ByteBuffer.allocate(16); + // 将一个 byteBuffer 作为附件【关联】到 selectionKey 上 + SelectionKey scKey = sc.register(selector, 0, buffer); + */ + // 12、将本客户端通道注册到选择器 + socketChannel.register(selector, SelectionKey.OP_READ); + } else if (key.isReadable()) { + // 13、获取当前选择器上的读就绪事件 + SelectableChannel channel = key.channel(); + SocketChannel socketChannel = (SocketChannel) channel; + // 14、读取数据 + ByteBuffer buffer = ByteBuffer.allocate(1024); + // 获取关联的附件 + // ByteBuffer buffer = (ByteBuffer) key.attachment(); + int len; + while ((len = socketChannel.read(buffer)) > 0) { + buffer.flip(); + System.out.println(socketChannel.getRemoteAddress() + ":" + new String(buffer.array(), 0, len)); + buffer.clear();// 清除之前的数据 + } + } + // 删除当前的 selectionKey,防止重复操作 + it.remove(); + } + } + } +} +``` + +```java +public class Client { + public static void main(String[] args) throws Exception { + // 1、获取通道 + SocketChannel socketChannel = SocketChannel.open(new InetSocketAddress("127.0.0.1", 9999)); + // 2、切换成非阻塞模式 + socketChannel.configureBlocking(false); + // 3、分配指定缓冲区大小 + ByteBuffer buffer = ByteBuffer.allocate(1024); + // 4、发送数据给服务端 + Scanner sc = new Scanner(System.in); + while (true){ + System.out.print("请说:"); + String msg = sc.nextLine(); + buffer.put(("Client:" + msg).getBytes()); + buffer.flip(); + socketChannel.write(buffer); + buffer.clear(); + } + } +} +``` + +*** + +## AIO + +Java AIO(NIO.2) : AsynchronousI/O,异步非阻塞,采用了 Proactor 模式。服务器实现模式为一个有效请求一个线程,客户端的 I/O 请求都是由 OS 先完成了再通知服务器应用去启动线程进行处理 + +```java +AIO异步非阻塞,基于NIO的,可以称之为NIO2.0 + BIO NIO AIO +Socket SocketChannel AsynchronousSocketChannel +ServerSocket ServerSocketChannel AsynchronousServerSocketChannel +``` + +当进行读写操作时,调用 API 的 read 或 write 方法,这两种方法均为异步的,完成后会主动调用回调函数: + +* 对于读操作,当有流可读取时,操作系统会将可读的流传入 read 方法的缓冲区 +* 对于写操作,当操作系统将 write 方法传递的流写入完毕时,操作系统主动通知应用程序 + +在 JDK1.7 中,这部分内容被称作 NIO.2,主要在 Java.nio.channels 包下增加了下面四个异步通道: +AsynchronousSocketChannel、AsynchronousServerSocketChannel、AsynchronousFileChannel、AsynchronousDatagramChannel + +**** diff --git a/src/content/posts/Java/JUC/volatile双重锁.md b/src/content/posts/Java/JUC/volatile双重锁.md new file mode 100644 index 0000000..2afd62f --- /dev/null +++ b/src/content/posts/Java/JUC/volatile双重锁.md @@ -0,0 +1,119 @@ +--- +title: volatile-实现单例模式的双重锁 +published: 2025-08-07 +description: '' +image: '' +tags: [JUC,JAVA,volatile,双重锁] +category: 'Java > JUC' +draft: false +lang: '' +--- + +# 什么是单例模式的双重锁 +单例模式的双重锁是一种实现单例模式的技术,通过两次检查实例是否为null,结合同步锁来保证在多线程环境下只创建一个实例,并试图通过减少同步的次数来提高性能。为了确保线程安全,尤其在涉及到对象创建的指令重排的问题的时候,通常需要使用 `volatile`关键字来修饰单例类的实例变量。 + +# 非线程安全的单例模式 +```java +public class Singleton { + private static Singleton instance; + private Singleton() { + + } + public static Singleton getInstance() { + if (instance == null) { + instance = new Singleton(); + } + return instance; + } +} + +``` + +> 多线程环境下,上面的简单实现在并发调用 `getInstance()`方法时候可能出现问题。 + +![](https://blog.meowrain.cn/api/i/2025/04/24/rajbl4-0.webp) + +常见的做法是使用synchronized +```java +public class Singleton { + private static Singleton instance; + private Singleton() { + + } + public static synchronized Singleton getInstance() { + if (instance == null) { + instance = new Singleton(); + } + return instance; + } +} + +``` + +这种同步方式能确保线程安全,因为在同一时间,只有一个线程能够进入getInstance方法,但是每次调用`getInstance`方法都需要获取锁,即使在实例已经创建之后也是如此,这样会带来额外的性能开销,尤其是在频繁调用`getInstance()`的情况下 +# 什么是单例模式的双重检查锁定 -> 可能会导致半初始化问题 +双重检查锁定就是为了保证在线程安全的前提下,尽量减少同步带来的性能开销 + +核心思想: +1. 第一次检查: 在进入同步块之前,先检查insatnce是否为null,如果不是null,说明实例已经创建,可以直接返回,避免进入同步块。 +2. 同步块: 如果第一次检查发现instance是null,就进入同步块 +3. 第二次检查: 在同步块内,再次检查instance是否为null,这是至关重要的一部,因为可能多个线程都通过了第一次检查,但只有一个线程进入同步块,在同步块内再次检查可以确保只有一个线程会智行对象的创建操作。 +4. 创建实例:如果第二次检查发现instance仍然为null,才真正创建对象并把引用赋值给instance + + + ```java + public class Singleton { + private static Singleton instance; + private Singleton() { + + } + public static Singleton getInstance() { + if (instance == null) { + synchronized (Singleton.class) { + if (instance == null) { + instance = new Singleton(); + } + } + } + return instance; + } +} + +``` + + +> 尽管双重检查锁看起来聪明地减少了同步磁化,但是在JMM(JAVA 内存模型)种,没有使用`volatile`的双重检查锁仍然存在`指令重排`的问题。 + +对象创建的过程 `instance = new SimpleSingleton();` 实际上能分解为三个步骤: +1. 为对象分配内存空间 +2. 初始化对象 +3. 将分配的内存空间的地址赋值给`instance`变量 + +在某些情况下,JVM为了优化性能,可能会对这三个步骤进行重排序,例如,可能会将步骤三排在步骤2之前 + + +![](https://blog.meowrain.cn/api/i/2025/04/24/s9qvuj-0.webp) + +# 为什么用volatile? +1. 可见性: volatile确保了所有线程都能看到instance变量的最新值,当一个线程修改了instance值,这个改变会立即对其他线程可见。 +2. 禁止指令重排:解决了半初始化的问题,确保instance变量被赋值为非null之前,对象已经被完全初始化。 + +```java +public class Singleton { + private static volatile Singleton instance; + private Singleton() { + + } + public static Singleton getInstance() { + if (instance == null) { + synchronized (Singleton.class) { + if (instance == null) { + instance = new Singleton(); + } + } + } + return instance; + } +} + +``` diff --git a/src/content/posts/Java/JUC/线程安全单例.md b/src/content/posts/Java/JUC/线程安全单例.md new file mode 100644 index 0000000..860e220 --- /dev/null +++ b/src/content/posts/Java/JUC/线程安全单例.md @@ -0,0 +1,308 @@ +--- +title: 线程安全单例 +published: 2025-08-07 +description: '' +image: '' +tags: [JUC,JAVA,volatile,线程安全,单例模式] +category: 'Java > JUC' +draft: false +lang: '' +--- + + +![](https://blog.meowrain.cn/api/i/2025/05/27/11a6ta3-0.webp) +# 1 解决反序列化导致的单例破坏现象 + +这里的单例问题是,如果对一个可序列化对象进行反序列化,会创建一个新的对象,这就违背了我们想要全局单例的目标。因此要重写readResolve方法。 + +![](https://blog.meowrain.cn/api/i/2025/05/26/sjvikb-0.webp) + +```java +package org.example.sigletons; + +import java.io.Serializable; + +public class Singleton1 implements Serializable { + private Singleton1(){} + private static final Singleton1 INSTANCE = new Singleton1(); + public Singleton1 getInstance() { + return INSTANCE; + } + public Object readResolve() { + return INSTANCE; + } +} + +``` + +# 2 使用枚举实现单例模式 +![](https://blog.meowrain.cn/api/i/2025/05/26/swwpe2-0.webp) + +```java +package org.example.sigletons; + +import java.io.IOException; +import java.io.InputStream; +import java.util.Properties; + +public enum Singleton2 { + INSTANCE; + private final Properties properties; + private Singleton2() { + properties = new Properties(); + String configFile = "application.properties"; + System.out.println("ConfigurationManager: Initializing and loading " + configFile); + try(InputStream inputStream = Singleton2.class.getClassLoader().getResourceAsStream(configFile)){ + if(inputStream == null){ + System.out.println("ConfigurationManager: Sorry, unable to find " + configFile); + // 在实际应用中,这里可能抛出异常或有更复杂的错误处理 + return; + } + properties.load(inputStream); + System.out.println("ConfigurationManager: Configuration loaded successfully."); + }catch (IOException e) { + e.printStackTrace(); + } + } + public String getProperty(String key) { + return properties.getProperty(key); + } + public String getProperty(String key,String defaultValue) { + return properties.getProperty(key, defaultValue); + } + + // 可以添加其他需要的方法,比如重新加载配置等(需要考虑线程安全) + public void listProperties() { + properties.forEach((key, value) -> System.out.println(key + "=" + value)); + } +} +class TestSingleton2 { + public static void main(String[] args) { + Singleton2 singleton2 = Singleton2.INSTANCE; + singleton2.listProperties(); + } +} +``` + +![](https://blog.meowrain.cn/api/i/2025/05/26/swav0i-0.webp) + + +💎 枚举单例:全面解答这些问题!📜✨ + +枚举单例是一种非常推荐的单例实现方式,因为它不仅简单、易用,还天然地具备线程安全和防止反序列化、反射破坏单例的能力。接下来,我们重点针对 **枚举单例** 来回答这些问题! + +--- + +### **问题 1:枚举单例是如何限制实例个数的?** + +枚举单例通过枚举的机制天然地保证: +1. 枚举类的每一个枚举实例(如单例对象)都在 **类加载阶段** 就完成初始化,并且整个应用程序中只有一个实例。 +2. 枚举类型底层由 JVM 的实现机制保证,它不像普通类那样允许通过反射或 `new` 额外创建实例。 + +#### 示例: +```java +public enum SingletonEnum { + INSTANCE; // 枚举单例实例 + + public void doSomething() { + System.out.println("Doing something..."); + } +} +``` + +#### 使用方式: +即便通过 `SingletonEnum.INSTANCE` 多次获取,得到的始终是同一个实例。 +```java +SingletonEnum instance1 = SingletonEnum.INSTANCE; +SingletonEnum instance2 = SingletonEnum.INSTANCE; +System.out.println(instance1 == instance2); // 输出:true +``` + +--- + +### **问题 2:枚举单例在创建时是否有并发问题?** + +枚举单例天然线程安全,因为: +1. 枚举类型的初始化由 JVM 保证,是在类加载时完成的。 +2. 类加载过程是线程安全的,JVM 使用了类加载的同步机制,保证枚举单例的初始化不会因多线程而发生竞争。 + +#### 举例: +即使多个线程同时调用 `SingletonEnum.INSTANCE`,它们都会得到在类加载阶段构造好的唯一对象,无需额外同步。 + +--- + +### **问题 3:枚举单例能否被反射破坏单例?** + +**不会!** 枚举类型的结构特殊,无法被反射破坏单例。这是因为: +1. 枚举的构造器是私有的,并且其底层会检测反射调用。 +2. 如果尝试通过反射显式调用枚举类的构造器,JVM 会抛出 `IllegalArgumentException`。 + +#### 验证代码: +```java +import java.lang.reflect.Constructor; + +public class EnumReflectionTest { + public static void main(String[] args) { + try { + // 获取枚举的构造器 + Constructor constructor = SingletonEnum.class.getDeclaredConstructor(); + constructor.setAccessible(true); + SingletonEnum instance = constructor.newInstance(); // 反射创建枚举对象 + } catch (Exception e) { + e.printStackTrace(); // 会抛出 IllegalArgumentException + } + } +} +``` + +#### 运行结果: +``` +java.lang.IllegalArgumentException: Cannot reflectively create enum objects +``` + +--- + +### **问题 4:枚举单例能否被反序列化破坏单例?** + +枚举单例天然具备防止反序列化破坏的特性,原因是: +1. `Enum` 类型的序列化机制是由 JVM 内部实现的,不走普通的对象序列化流程。 +2. 反序列化枚举对象时,JVM 会直接返回枚举类中的现有实例,而不是从序列化流中创建新对象。 + +#### 验证代码: +```java +import java.io.*; + +public class EnumSerializationTest { + public static void main(String[] args) throws IOException, ClassNotFoundException { + SingletonEnum instance1 = SingletonEnum.INSTANCE; + + // 序列化枚举对象 + ObjectOutputStream oos = new ObjectOutputStream(new FileOutputStream("enum_singleton.obj")); + oos.writeObject(instance1); + oos.close(); + + // 反序列化枚举对象 + ObjectInputStream ois = new ObjectInputStream(new FileInputStream("enum_singleton.obj")); + SingletonEnum instance2 = (SingletonEnum) ois.readObject(); + + // 判断是否破坏单例 + System.out.println(instance1 == instance2); // 输出:true,单例没有破坏 + } +} +``` + +--- + +### **问题 5:枚举单例属于懒汉式还是饿汉式?** + +**枚举单例本质上属于饿汉式单例**。它的特点是**在类加载阶段完成初始化**: +- 枚举的实例在类加载时就被创建并初始化。 +- 即使程序中从未访问过 `SingletonEnum.INSTANCE`,枚举实例依然会被加载。 + +#### 优点: +- 线程安全,无需为单例初始化额外编写同步代码。 +- 实现简洁,JVM 自动保证。 + +#### 缺点: +- 如果枚举实例较多,并且包含较大的初始化逻辑,会导致类加载阶段性能开销增加。 + +--- + +### **问题 6:枚举单例如果希望加入一些初始化逻辑,该如何做?** + +可以通过添加枚举的构造方法和静态方法来实现初始化逻辑。枚举的构造方法是私有的,可以用来在实例创建时执行初始化。 + +#### 修改代码: +```java +public enum SingletonEnum { + INSTANCE; // 枚举单例实例 + + private String configuration; + + // 枚举的构造方法 + SingletonEnum() { + // 初始化逻辑 + configuration = "System Configuration Loaded"; + } + + public String getConfiguration() { + return configuration; + } +} +``` + +#### 测试: +```java +public class TestEnumInitialization { + public static void main(String[] args) { + SingletonEnum instance = SingletonEnum.INSTANCE; + System.out.println(instance.getConfiguration()); // 输出:System Configuration Loaded + } +} +``` + +#### 分析: +- 枚举类型的构造器会在类加载时调用,且只调用一次。 +- 可用枚举构造器实现单例实例的初始化逻辑。 + +--- + +### 总结 + +**为何枚举单例完美适合单例模式?** +- 它是天生线程安全的,JVM 保障了枚举实例的唯一性。 +- 枚举实例不能通过反射或序列化破坏。 +- 枚举的初始化流程天然符合饿汉式单例的特点。 + +--- + +# 3 Double Check + +https://meowrain.cn/archives/volatile-shi-xian-dan-li-mo-shi-de-shuang-zhong-suo + + +```java +package cn.meowrain; + +public class DoubleSingleton { + private static volatile DoubleSingleton INSTANCE = null; + public static DoubleSingleton getInstance() { + if(INSTANCE != null) { + return INSTANCE; + } + synchronized (DoubleSingleton.class){ + if(INSTANCE != null) { + return INSTANCE; + } + INSTANCE = new DoubleSingleton(); + return INSTANCE; + } + } +} + +``` + +![](https://blog.meowrain.cn/api/i/2025/05/27/10zdcs1-0.webp) + + +# 4 静态内部类懒汉式创建线程安全单例 + +``` +package cn.meowrain; + +public class Singleton2 { + private Singleton2(){} + // 问题1: 属于懒汉式还是饿汉式 + private static class LazyLoader{ + static final Singleton2 INSTANCE = new Singleton2(); + } + // 在创建的时候是否有并发问题 + public static Singleton2 getInstance() { + return LazyLoader.INSTANCE; + } +} + +``` +![](https://blog.meowrain.cn/api/i/2025/05/27/1132c75-0.webp) + +![](https://blog.meowrain.cn/api/i/2025/05/27/1144xf0-0.webp) \ No newline at end of file diff --git a/src/content/posts/Java/JVM/GC相关参数.md b/src/content/posts/Java/JVM/GC相关参数.md new file mode 100644 index 0000000..f6f6c1c --- /dev/null +++ b/src/content/posts/Java/JVM/GC相关参数.md @@ -0,0 +1,117 @@ +--- +title: JVM GC相关参数 +published: 2025-07-18 +description: '' +image: '' +tags: [GC,JAVA,JVM] +category: 'Java > JVM' +draft: false +lang: '' +--- +# GC相关参数 + +![image.png](https://blog.meowrain.cn/api/i/2025/07/18/10kn2xz-1.webp) + +--- + +### 1. 堆初始大小 (`Xms`) + +- **参数:** `Xms` +- **含义:** 设置 JVM 启动时**初始分配的堆内存大小**。 +- **作用:** + - 决定了 Java 程序一启动时,JVM 向操作系统申请的内存大小。 + - 如果设置得太小,JVM 可能会在程序运行初期频繁地进行堆内存扩展,这会带来一定的性能开销。 +- **示例:** `Xms512m` 表示设置初始堆大小为 512MB。 +- **最佳实践:** 在生产环境中,通常建议将 `Xms` 和 `Xmx` 设置为相同的值,以避免堆的动态扩展和收缩带来的性能抖动。 + +--- + +### 2. 堆最大大小 (`Xmx` 或 `XX:MaxHeapSize`) + +- **参数:** `Xmx` 或 `XX:MaxHeapSize=` +- **含义:** 设置 JVM **允许分配的最大堆内存大小**。 +- **作用:** + - 这是堆内存的硬性上限。如果应用程序需要的内存超过了这个值,就会抛出 `java.lang.OutOfMemoryError`。 + - 合理设置此值可以防止应用程序因内存泄漏等问题耗尽所有服务器内存,从而影响其他进程。 +- **示例:** `Xmx2g` 表示设置最大堆大小为 2GB。 + +--- + +### 3. 新生代大小 (`Xmn` 或 `XX:NewSize` + `XX:MaxNewSize`) + +- **参数:** `Xmn` +- **含义:** 设置**新生代(Young Generation)的大小**。这是一个快捷参数。 +- **作用:** + - 新生代是绝大多数新对象产生的地方,也是 Minor GC 发生的主要区域。 + - 设置一个合理的新生代大小非常重要。 + - **过小:** 会导致 Minor GC 过于频繁。 + - **过大:** 会挤占老年代的空间,可能导致更频繁的 Full GC。同时,单次 Minor GC 的时间可能会变长。 +- **补充:** `Xmn` 实际上是同时设置了 `XX:NewSize`(新生代初始大小)和 `XX:MaxNewSize`(新生代最大大小)。如果希望新生代大小动态变化,可以分别设置这两个参数。 + +--- + +### 4. 幸存区比例 (`XX:SurvivorRatio`) + +- **参数:** `XX:SurvivorRatio=` +- **含义:** 设置新生代中 **Eden 区与一个 Survivor 区的大小比例**。 +- **计算公式:** `ratio = Eden区大小 / Survivor区大小`。 +- **作用:** + - 这个比例决定了新生代中用于创建新对象(Eden)和存放幸存对象(Survivor)的空间分配。 + - 例如,`XX:SurvivorRatio=8` 表示 Eden:S0:S1 的比例是 8:1:1。这意味着 Eden 区将占用新生代 8/10 的空间,而每个 Survivor 区占用 1/10。 +- **注意:** 这个参数在启用了自适应大小策略(`XX:+UseAdaptiveSizePolicy`,在某些 GC 算法中默认开启)时,其设置的固定比例可能会被 JVM 动态调整。 + +--- + +### 5. 幸存区比例 (动态) (`XX:InitialSurvivorRatio` 和 `XX:+UseAdaptiveSizePolicy`) + +- **参数:** `XX:+UseAdaptiveSizePolicy` +- **含义:** **启用 GC 自适应大小策略**。这个策略允许 JVM 根据应用程序的运行情况(如吞吐量、停顿时间目标)动态调整堆中各区域的大小,包括 Eden/Survivor 的比例。 +- **作用:** + - 开启后,JVM 会自动优化内存分配,省去了手动精细调优的麻烦。这是 Parallel GC 等收集器默认开启的。 + - `XX:InitialSurvivorRatio` 用于设定自适应策略下的**初始** SurvivorRatio 值,后续 JVM 可能会根据需要进行调整。 +- **结论:** 如果你看到这个参数,意味着 JVM 正在自动管理新生代的比例,`XX:SurvivorRatio` 的静态设置可能不会生效。 + +--- + +### 6. 晋升阈值 (`XX:MaxTenuringThreshold`) + +- **参数:** `XX:MaxTenuringThreshold=` +- **含义:** 设置对象从新生代晋升到老年代的**年龄阈值**。 +- **作用:** + - 一个对象在 Survivor 区每熬过一次 Minor GC,其年龄就加 1。当年龄达到这个阈值时,就会被移动到老年代。 + - 默认值通常是 15(或 6,取决于 GC)。 + - 如果设置得太高,对象可能长时间停留在 Survivor 区,增加了复制成本;如果设置得太低,可能导致一些生命周期不长的对象过早进入老年代,增加了 Full GC 的压力。 + +--- + +### 7. 晋升详情 (`XX:+PrintTenuringDistribution`) + +- **参数:** `XX:+PrintTenuringDistribution` +- **含义:** 一个诊断参数,用于在每次 Minor GC 后**打印出 Survivor 区中对象的年龄分布情况**。 +- **作用:** + - 这是调优 `XX:MaxTenuringThreshold` 的重要工具。 + - 通过观察日志,你可以看到每个年龄段有多少对象,以及 JVM 计算出的动态晋升阈值,从而判断当前设置是否合理。 + +--- + +### 8. GC 详情 (`XX:+PrintGCDetails` 和 `verbose:gc`) + +- **参数:** `XX:+PrintGCDetails` 或 `verbose:gc` +- **含义:** 打印详细的 GC 日志信息。 +- **作用:** + - 这是进行 GC 性能分析和故障排查的**必备参数**。 + - `verbose:gc` 是一个标准参数,输出基本的 GC 信息。 + - `XX:+PrintGCDetails` 会提供更详尽的信息,包括每次 GC 前后堆各区域的大小、GC 耗时等。 +- **推荐:** 通常与 `XX:+PrintGCTimeStamps` 或 `XX:+PrintGCDateStamps` 一起使用,为日志增加时间戳。 + +--- + +### 9. FullGC 前 MinorGC (`XX:+ScavengeBeforeFullGC`) + +- **参数:** `XX:+ScavengeBeforeFullGC` +- **含义:** 指示 JVM 在执行 Full GC 之前,先强制进行一次 Minor GC。 +- **作用:** + - 理论上,这可以清理掉新生代中大部分可以被回收的对象,从而减轻 Full GC 的负担,因为 Full GC 需要处理整个堆(包括新生代)。 +- **注意:** + - 此参数在现代的 GC(如 G1)中已不推荐使用或被废弃,因为它们有更智能的回收策略。 + - 在某些情况下,它可能会引入一次额外的、不必要的停顿(Minor GC 的停顿)。因此,除非有明确的测试数据支持,否则一般不建议开启。 diff --git a/src/content/posts/Java/JVM/JVM内存模型分区.md b/src/content/posts/Java/JVM/JVM内存模型分区.md new file mode 100644 index 0000000..16cc48e --- /dev/null +++ b/src/content/posts/Java/JVM/JVM内存模型分区.md @@ -0,0 +1,31 @@ +--- +title: JVM内存模型分区 +published: 2025-07-18 +description: '' +image: '' +tags: [JVM,内存模型] +category: 'Java > JVM' +draft: false +lang: '' +--- +# JVM内存模型分⼏个区,每个区放什么对象 + +![image.png](https://blog.meowrain.cn/api/i/2025/07/18/12an835-1.webp) + +![img](https://blog.meowrain.cn/api/i/2025/07/18/12b818c-1.webp) + +![image.png](https://blog.meowrain.cn/api/i/2025/07/18/12b1jr4-1.webp) + +![image.png](https://blog.meowrain.cn/api/i/2025/07/18/12awf70-1.webp) + +分为方法区,堆,本地方法栈,虚拟机栈,程序计数器 + +方法区(元空间):用于存储已经被虚拟机加载的类信息,常量,静态变量等数据。虽然方法区被描述为堆的逻辑部分,但有”非堆“的别名。方法区可以选择不实现垃圾收集,内存不足的时候,会抛出OutOfMemoryError异常。 + +程序计数器: 当前线程所执行的字节码的行号指示器,存储当前线程正在执行的Java方法的JVM指令地址。 + +JVM虚拟机栈:每个线程都有自己独立的Java虚拟机栈,生命周期和线程相同,每个方法在执行的时候都会创建一个栈帧,用来存储局部变量表,操作数栈,动态链接,方法出口等信息。 + +本地方法栈: 与Java虚拟机栈差不读多,执行本地方法,其中堆和方法区是线程共有的。 + +Java堆: 存放和管理对象实例,被所有线程共享。 diff --git a/src/content/posts/Java/JVM/JVM垃圾回收算法.md b/src/content/posts/Java/JVM/JVM垃圾回收算法.md new file mode 100644 index 0000000..09dd3a8 --- /dev/null +++ b/src/content/posts/Java/JVM/JVM垃圾回收算法.md @@ -0,0 +1,148 @@ +--- +title: JVM垃圾回收算法 +published: 2025-07-18 +description: '' +image: '' +tags: [JVM,垃圾回收,分代回收] +category: 'Java > JVM' +draft: false +lang: '' +--- +# 垃圾回收算法 + +![image.png](https://blog.meowrain.cn/api/i/2025/07/18/10nn4ww-1.webp) + +[【Java虚拟机】JVM垃圾回收机制和常见回收算法原理-腾讯云开发者社区-腾讯云](https://cloud.tencent.com/developer/article/2292267) + +### **垃圾回收机制** + +**(1)什么是垃圾回收机制(Garbage Collection, 简称GC)** + +- 指自动管理动态分配的内存空间的机制,自动回收不再使用的内存,以避免内存泄漏和内存溢出的问题 +- 最早是在1960年代提出的,程序员需要手动管理内存的分配和释放 +- 这往往会导致内存泄漏和内存溢出等问题,同时也增加了程序员的工作量,特别是C++/C语言开发的时候 +- Java语言是最早实现垃圾回收机制的语言之一,其他编程语言,如C#、Python和Ruby等,也都提供了垃圾回收机制 + +**(2)JVM自动垃圾回收机制** + +- 指Java虚拟机在运行Java程序时,自动回收不再使用的对象所占用的内存空间的过程 +- Java程序中的对象,一旦不再被引用会被标记为垃圾对象,JVM会在适当的时候自动回收这些垃圾对象所占用的内存空间 +- 优点 +- 减少了程序员的工作量,不需要手动管理内存 +- 动态地管理内存,根据应用程序的需要进行分配和回收,提高了内存利用率 +- 避免内存泄漏和野指针等问题,增加程序的稳定性和可靠 +- 缺点 +- 垃圾回收会占用一定的系统资源,可能会影响程序的性能 +- 垃圾回收过程中会停止程序的执行,可能会导致程序出现卡顿等问题 +- **不一定能够完全解决内存泄漏等问题,需要在编写代码时注意内存管理和编码规范** + +--- + +# 垃圾回收算法 + +## 引用计数法 + +跟踪每个对象被引用的次数,当引用次数为0 的时候,可以将该对象回收。 + +优点是实现简单,缺点是循环引用没办法回收,而且引用计数器消耗大。 + +![image.png](https://blog.meowrain.cn/api/i/2025/07/18/10nn7eb-1.webp) + +![image.png](https://blog.meowrain.cn/api/i/2025/07/18/10nngi3-1.webp) + +## **可达性分析算法** + +- 可达性分析算法的基本思想是通过一系列的“GC Roots”对象作为起点进行搜索。 +- 如果“GC Roots”和一个对象之间没有可达路径,则称该对象是不可达的,不过要注意的是被判定为不可达的对象不一定就会成为可回收对象。 +- 被判定为不可达的对象要成为回收对象,要至少经历两次标记过程。 +- 如果在这两次标记过程中仍然没有逃脱成为可回收对象的可能性,则基本上就真的成为可回收对象了。 + +通过一系列称为“GC Roots”的对象作为起始点,从这些节点开始向下搜索,搜索走过的路径称为“引用链”,当一个对象到 GC Roots 没有任何的引用链相连时(从 GC Roots 到这个对象不可达)时,证明此对象不可用。 + +![image.png](https://blog.meowrain.cn/api/i/2025/07/18/10nnmfv-1.webp) + +![image.png](https://blog.meowrain.cn/api/i/2025/07/18/10nnpgs-1.webp) + +### 什么是GC ROOT + +![image.png](https://blog.meowrain.cn/api/i/2025/07/18/10nntly-1.webp) + +![image.png](https://blog.meowrain.cn/api/i/2025/07/18/10nnyst-1.webp) + +## **垃圾回收算法之标记-复制算法** + +- 标记算法是一种常见的垃圾回收算法,它的基本思路是将Java堆分为两个区域:一个活动区域和一个空闲区域 +- 在垃圾回收过程中,首先标记所有被引用的对象 +- 然后将所有被标记的对象复制到空闲区域中,最后交换两个区域的角色,完成垃圾回收 +- 标记复制算法的详细实现步骤 + - 将Java堆分为两个区域:一个活动区域和一个空闲区域,初始时,所有对象都分配在活动区域中 + - 从GC Roots对象开始,遍历整个对象图,标记所有被引用的对象 + - 对所有被标记存活的对象进行遍历,将它们复制到空闲区域中,并更新所有指向它们的引用,使它们指向新的地址 + - 对所有未被标记的对象进行回收,将它们所占用的内存空间释放 + - 交换活动区域和空闲区域的角色,空闲区域变为新的活动区域,原来的活动区域变为空闲区域 + - 当空闲区域的内存空间不足时,进行一次垃圾回收,重复以上步骤。 +- 优点 + - 如果内存中的垃圾对象较多,需要复制的对象就较少,则效率高 + - 清理后,内存碎片少 +- 缺点 + - 标记复制算法的效率较高,但是预留一半的内存区域用来存放存活的对象,占用额外的内存空间 + - 如果出现存活对象数量比较多的时候,需要复制较多的对象 效率低 + - 假如是在老年代区域,99%的对象都是存活的,则性能底,所以老年代不适合这个算法 + +![image.png](https://blog.meowrain.cn/api/i/2025/07/18/10no08y-1.webp) + +复制过程如下,GC会将五个存活对象复制到to区,并且保证在to区内存空间上的连续性。 + +![image.png](https://blog.meowrain.cn/api/i/2025/07/18/10no6pp-1.webp) + +最后,将from区中的垃圾对象清除。 + +![image.png](https://blog.meowrain.cn/api/i/2025/07/18/10nojzj-1.webp) + +## **垃圾回收算法之标记-整理算法** + +标记-整理算法(Mark-Compact Algorithm) 是一种常见的垃圾回收(GC)算法,主要用于解决 标记-清除算法(Mark-Sweep) 产生的内存碎片问题。它通常被用于 Java 的老年代(Old Generation)垃圾回收中。 + +标记-整理算法主要分为两大阶段: + +标记阶段(Mark Phase) + +和标记-清除算法一样,从 GC Roots 出发,遍历所有可达对象,并将其标记为“存活”状态。 + +整理阶段(Compact Phase) + +将所有存活对象向内存的一端移动(通常是低地址方向)。 + +移动后会更新对象引用地址,以保证程序继续正确运行。 + +移动完成后,直接清理边界以后的内存空间。 + +![](https://blog.meowrain.cn/api/i/2025/07/18/10pzxij-1.webp) + +| **特点** | **标记-清除算法** | **标记-整理算法** | +| ------ | ------------- | ------------- | +| 内存碎片 | 会产生碎片 | 不会产生碎片 | +| 效率 | 清除快(只清除不可达对象) | 较慢(需要移动对象) | +| 适用场景 | 适用于对象回收率较高的情况 | 适用于对象存活率较高的情况 | + +## 垃圾回收算法之-分代算法 + +![image.png](https://blog.meowrain.cn/api/i/2025/07/18/10noe2w-1.webp) + +新生代分为eden区、from区、to区,老年代是一整块内存空间 + +分代算法将内存区域分为两部分:新生代和老年代。 + +根据新生代和老年代中对象的不同特点,使用不同的GC算法。 + +新生代对象的特点是:创建出来没多久就可以被回收(例如虚拟机栈中创建的对象,方法出栈就会销毁)。也就是说,每次回收时,大部分是垃圾对象,所以新生代适用于复制算法。 + +老年代的特点是:经过多次GC,依然存活。也就是说,每次GC时,大部分是存活对象,所以老年代适用于标记压缩算法。 + +![image.png](https://blog.meowrain.cn/api/i/2025/07/18/10nomg3-1.webp) + +![image.png](https://blog.meowrain.cn/api/i/2025/07/18/10nozio-1.webp) + +### 分代算法执行过程 + +--- diff --git a/src/content/posts/Java/JVM/Java类加载器与双亲委派机制.md b/src/content/posts/Java/JVM/Java类加载器与双亲委派机制.md new file mode 100644 index 0000000..7a3a328 --- /dev/null +++ b/src/content/posts/Java/JVM/Java类加载器与双亲委派机制.md @@ -0,0 +1,40 @@ +--- +title: Java类加载器与双亲委派机制 +published: 2025-07-18 +description: '' +image: '' +tags: [Java,类加载器,ClassLoader,双亲委派机制] +category: 'Java > JVM' +draft: false +lang: '' +--- +# Java类加载器和双亲委派机制 + +[Java 虚拟机之类加载](https://dunwu.github.io/waterdrop/pages/3e37ea6e/#%E7%B1%BB%E5%8A%A0%E8%BD%BD%E6%9C%BA%E5%88%B6) + +## Java类加载器层级 + +Java类加载器从高到低分为以下层级(以JDK 8为例): + +1. **启动类加载器(Bootstrap ClassLoader)** :加载JRE的`lib`目录下的核心类库(如`rt.jar`)。 +2. **扩展类加载器(Extension ClassLoader)** :加载`lib/ext`目录下的扩展类。 +3. **应用程序类加载器(Application ClassLoader)** :加载用户类路径(ClassPath)下的类。 +4. **自定义类加载器**:用户可自定义类加载器(需继承`ClassLoader`)。 + +## 什么是双亲委派机制 + +**双亲委派机制** 是Java类加载器的核心工作机制。它的核心思想是:当一个类加载器需要加载某个类时,不会直接尝试自己加载,而是将这个请求**逐级向上委托给父类加载器**处理。只有当所有父类加载器都无法完成加载时,子类加载器才会尝试自己加载。 + +## ## 示意图 + +![image.png](https://blog.meowrain.cn/api/i/2025/07/18/12c4ooa-1.webp) + +![image.png](https://blog.meowrain.cn/api/i/2025/07/18/12c4ix7-1.webp) + +[深入理解Java双亲委派机制:原理、意义与实战示例 - 云熙橙 - 博客园](https://www.cnblogs.com/xchangting/articles/18744083) + +## 双亲委派机制的好处 + +1. **保障核心类库的安全**防止用户自定义的类(如`java.lang.Object`)覆盖JVM核心类。例如,如果用户编写了一个恶意`String`类,双亲委派机制会优先加载核心库中的`String`,从而避免安全隐患。 +2. **避免重复加载**同一个类只会被一个类加载器加载一次,防止内存中出现多个相同类的副本,确保类的唯一性。 +3. **实现代码隔离**不同类加载器加载的类属于不同的命名空间,天然支持模块化(如Tomcat为每个Web应用分配独立的类加载器)。 diff --git a/src/content/posts/Java/JVM/Jvm分代回收机制.md b/src/content/posts/Java/JVM/Jvm分代回收机制.md new file mode 100644 index 0000000..4f4d4c1 --- /dev/null +++ b/src/content/posts/Java/JVM/Jvm分代回收机制.md @@ -0,0 +1,79 @@ +--- +title: Jvm分代回收机制 +published: 2025-07-18 +description: '' +image: '' +tags: [分代回收,JVM] +category: 'Java > JVM' +draft: false +lang: '' +--- +# 分代回收 + +[juejin.cn](https://juejin.cn/post/7474503566154858536) + +[【GC系列】JVM堆内存分代模型及常见的垃圾回收器-腾讯云开发者社区-腾讯云](https://cloud.tencent.com/developer/article/1755848) + +[Eden与Survivor区 · Homurax's Blog](https://blog.homurax.com/2018/09/17/eden-survivor/) + +[Java 虚拟机之垃圾收集](https://dunwu.github.io/waterdrop/pages/587898a0/) + +[JVM内存分配策略](https://linqiankun.github.io/hexoblog/md/jvm/JVM%E5%86%85%E5%AD%98%E5%88%86%E9%85%8D%E7%AD%96%E7%95%A5/) + +现代JVM堆内存的典型划分: + +1. 年轻代(Young Generation) +2. 老年代(Old Generation) +3. 永久代/元空间(Permanent Gen/Metaspace) + +## JDK7堆空间内部结构 + +![image.png](https://blog.meowrain.cn/api/i/2025/07/18/10mvpvg-1.webp) + +特点: + +永久代位于堆内存中 + +字符串常量池存放在永久代 + +方法区使用永久代实现 + +## JDK8堆空间内部结构 + +![image.png](https://blog.meowrain.cn/api/i/2025/07/18/10mvro2-1.webp) + +永久代被元空间替换,元空间不属于堆内存。 + +元空间使用本地内存 + +字符串常量池移至堆内存 + +方法区改由元空间实现。 + +## 年轻代与老年代 + +JVM 内置的通用垃圾回收原则。堆内存划分为 Eden、Survivor(年轻代) , Tenured/Old (老年代)空间: + +![image.png](https://blog.meowrain.cn/api/i/2025/07/18/10mvw5e-1.webp) + +核心规则: + +1. 对象优先在Eden区分配 +2. 大对象直接进入老年代 +3. 长期存活对象进入老年代(默认年龄阈值15) +4. 动态年龄判断(Survivor区中相同年龄对象总和超过50%时候晋升) + +在 JVM 中,**年龄阈值(Tenuring Threshold)** 是一个关键的参数,它决定了新生代(Young Generation)中的对象需要经历多少次垃圾回收(Minor GC)仍然存活,才会被晋升(Promotion)到老年代(Old Generation)。 + +年轻代分为Eden区和Survivor区,Survivor区又分为S0,S1,S0,S1其中一个作为使用区(from),一个作为空闲区(to)(不固定,可能S0是空闲区,也可能是使用区) +在Minor GC开始以后(会回收Eden区和使用区中的对象),逃过第一轮GC的,在Eden区和使用区中的对象,会被丢在空闲区,接下来将使用区和空闲区互换(空闲区变使用区,使用区变空闲区),等待下一次Eden区满进行Minor GC,以此不断循环(每复制一次,年龄就会 + 1) + +![image.png](https://blog.meowrain.cn/api/i/2025/07/18/10mw6vd-1.webp) + +![image.png](https://blog.meowrain.cn/api/i/2025/07/18/10mwdnp-1.webp) + +# 堆空间大小设置 + +![image.png](https://blog.meowrain.cn/api/i/2025/07/18/10mwjcx-1.webp) + +![image.png](https://blog.meowrain.cn/api/i/2025/07/18/10mwpa5-1.webp) diff --git a/src/content/posts/Java/JVM/Jvm常见垃圾收集器.md b/src/content/posts/Java/JVM/Jvm常见垃圾收集器.md new file mode 100644 index 0000000..920873d --- /dev/null +++ b/src/content/posts/Java/JVM/Jvm常见垃圾收集器.md @@ -0,0 +1,44 @@ +--- +title: Jvm常见垃圾收集器 +published: 2025-07-18 +description: '' +image: '' +tags: [Java,JVM,垃圾收集器] +category: 'Java > JVM' +draft: false +lang: '' +--- +# Java中常见的垃圾收集器 + +GC收集器有哪些? + +1.serial收集器 +单线程,工作时必须暂停其他工作线程。多用于client机器上,使用复制算法 +2.ParNew收集器 +serial收集器的多线程版本,server模式下虚拟机首选的新生代收集器。复制算法 +3.Parallel Scavenge收集器 +复制算法,可控制吞吐量的收集器。吞吐量即有效运行时间。 +4.Serial Old收集器 +serial的老年代版本,使用整理算法。 +5.Parallel Old收集器 +第三种收集器的老年代版本,多线程,标记整理 +6.CMS收集器 +目标是最短回收停顿时间。 + +7.G1收集器,基本思想是化整为零,将堆分为多个Region,优先收集回收价值最大的Region。 + +[垃圾收集器_java垃圾收集器-CSDN博客](https://blog.csdn.net/binbinxyz/article/details/141821712) + +![image.png](https://blog.meowrain.cn/api/i/2025/07/18/10lr31h-1.webp) + +![image.png](https://blog.meowrain.cn/api/i/2025/07/18/10lrj5p-1.webp) + +![image.png](https://blog.meowrain.cn/api/i/2025/07/18/10lrmof-1.webp) + +![image.png](https://blog.meowrain.cn/api/i/2025/07/18/10lrvwa-1.webp) + +![image.png](https://blog.meowrain.cn/api/i/2025/07/18/10ls1dm-1.webp) + +# G1垃圾回收器 + +[G1垃圾回收](Java%E4%B8%AD%E5%B8%B8%E8%A7%81%E7%9A%84%E5%9E%83%E5%9C%BE%E6%94%B6%E9%9B%86%E5%99%A8%2022a49a1194e98020a75ced52b5d871d7/G1%E5%9E%83%E5%9C%BE%E5%9B%9E%E6%94%B6%2022a49a1194e980e9bbf7e2a6c0f3e4c6.md) diff --git a/src/content/posts/Java/Java内存模型.md b/src/content/posts/Java/Java内存模型.md new file mode 100644 index 0000000..18c95a6 --- /dev/null +++ b/src/content/posts/Java/Java内存模型.md @@ -0,0 +1,24 @@ +--- +title: Java内存模型 +published: 2025-09-13 +description: ' Java内存模型 ' +image: '' +tags: ['JMM','Java内存模型'] +category: 'Java > 面试题' +draft: false +lang: '' +--- + + +Java内存模型,是Java虚拟机定义的一种规范,用来描述多线程程序中的变量如何在内存中读取数据,何时会把数据写回主内存。 + +JMM的核心目标是确保多线程环境下的**可见性,有序性和原子性**,从而避免由于硬件和编译器优化带来的不一致问题。 +- 可见性:确保一个线程对共享变量的修改,其他线程能够及时看到。关键字 `volatile`是用来保证可见性的,它强制线程每次读写的时候都从主内存中获取最新值。 +- 有序性:确保程序执行的顺序符合代码的书写顺序。JMM允许某些指令重排序,来提高性能,但会保证线程内的操作顺序不会被破坏,通过happens-before关系保证跨线程的有序性。 +- 原子性:确保操作的不可分割性,要么全部成功,要么全部失败。 例如synchronized关键字能确保方法或者代码块的原子性。 + + +## 参考 +JMM 会把内存分为本地内存和主存,每个线程都有它自己的私有化的本地内存,还有个存储共享数据的主存。 + +![](https://blog.meowrain.cn/api/i/2025/09/13/p26o8x-1.webp) diff --git a/src/content/posts/Java/Java函数式接口.md b/src/content/posts/Java/Java函数式接口.md new file mode 100644 index 0000000..c1b6cb8 --- /dev/null +++ b/src/content/posts/Java/Java函数式接口.md @@ -0,0 +1,372 @@ +--- +title: Java函数式接口 +published: 2025-07-19 +description: '' +image: '' +tags: [函数式接口, Java, 编程] +category: 'Java' +draft: false +lang: '' +--- + + + + + +![](https://blog.meowrain.cn/api/i/2025/05/31/x6m66n-0.webp) + +![](https://blog.meowrain.cn/api/i/2025/05/31/x722c1-0.webp) + +![](https://blog.meowrain.cn/api/i/2025/05/31/x74ils-0.webp) + +## 1. `Supplier` - 数据的供给者 🎁 + +**接口定义**:`@FunctionalInterface public interface Supplier { T get(); }` + +**核心作用**: +`Supplier` 接口的核心职责是**生产或提供数据**,它不接受任何参数,但会返回一个 `T` 类型的结果。你可以把它想象成一个“工厂”或者“源头”,当你需要一个特定类型的对象时,就调用它的 `get()` 方法。 + +**方法详解**: + +* `T get()`: 这是 `Supplier` 接口中唯一的抽象方法。调用它时,会执行你提供的 Lambda 表达式或方法引用所定义的逻辑,并返回一个结果。 + +**常见应用场景**: + +* **延迟加载/创建对象**:当某个对象的创建成本较高,或者并非立即需要时,可以使用 `Supplier` 来推迟其创建,直到真正使用时才调用 `get()`。 +* **生成默认值或配置信息**:提供一个默认对象或从某个源(如配置文件、数据库)获取配置。 +* **生成随机数据**:如示例中的随机数生成器。 +* **作为工厂方法**:在更复杂的场景中,`Supplier` 可以作为创建对象的简单工厂。 + +**您的示例代码分析** (`SupplierExample.java`): + +```java +import java.util.Random; +import java.util.function.Supplier; + +public class SupplierExample { + + // 示例方法1: 接收一个 Supplier 来获取随机整数 + public static Integer getRandomNumber(Supplier randomNumberSupplier) { + // 调用 randomNumberSupplier 的 get 方法来执行其提供的逻辑 + return randomNumberSupplier.get(); + } + + // 示例方法2: 接收一个 Supplier 来创建问候语字符串 + public static String createGreetingMessage(Supplier greetingSupplier) { + return greetingSupplier.get(); + } + + public static void main(String[] args) { + // 场景1: 获取随机数 + // Lambda 表达式实现 Supplier: () -> new Random().nextInt(100) + // 这个 Lambda 不接受参数,返回一个 0-99 的随机整数 + Supplier randomIntSupplier = () -> new Random().nextInt(100); + Integer num = getRandomNumber(randomIntSupplier); // 传递行为 + System.out.println("随机数: " + num); + + // 场景2: 获取固定数字 + // Lambda 表达式实现 Supplier: () -> 42 + // 这个 Lambda 总是返回固定的数字 42 + Supplier fixedIntSupplier = () -> 42; + Integer fixedNum = getRandomNumber(fixedIntSupplier); + System.out.println("固定数字: " + fixedNum); + + // 场景3: 创建不同的问候语 + Supplier englishGreeting = () -> "Hello, World!"; + System.out.println(createGreetingMessage(englishGreeting)); // 输出: Hello, World! + + Supplier spanishGreeting = () -> "¡Hola, Mundo!"; + System.out.println(createGreetingMessage(spanishGreeting)); // 输出: ¡Hola, Mundo! + } +} +``` + +**代码解读**: + +* `getRandomNumber` 和 `createGreetingMessage` 方法本身并不关心数字或字符串是如何产生的,它们只依赖传入的 `Supplier` 来提供结果。这体现了**行为参数化**——方法接受行为(通过函数式接口)作为参数。 +* 在 `main` 方法中: + * `randomIntSupplier`: 定义了一个行为——“生成一个0到99的随机整数”。 + * `fixedIntSupplier`: 定义了另一个行为——“总是提供数字42”。 + * `englishGreeting` 和 `spanishGreeting`: 定义了不同的行为来提供特定的字符串。 +* 通过将不同的 `Supplier` 实现传递给同一个方法 (`getRandomNumber` 或 `createGreetingMessage`),我们可以获得不同的结果,而无需修改方法本身。 + +**关键益处**: + +* **灵活性**:可以轻松替换不同的供给逻辑。 +* **解耦**:数据的使用者和数据的生产者解耦。 +* **可测试性**:可以方便地传入 mock 的 `Supplier` 进行单元测试。 + +--- + +## 2. `Function` - 数据的转换器/映射器 🔄 + +**接口定义**:`@FunctionalInterface public interface Function { R apply(T t); }` + +**核心作用**: +`Function` 接口的核心职责是**将一个类型 `T` 的输入参数转换或映射成另一个类型 `R` 的输出结果**。它就像一个数据处理管道中的一个环节,接收数据,进行处理,然后传递给下一个环节。 + +**方法详解**: + +* `R apply(T t)`: 这是 `Function` 的核心方法。它接受一个 `T` 类型的参数 `t`,对其执行Lambda表达式或方法引用中定义的转换逻辑,并返回一个 `R` 类型的结果。 + +**常见应用场景**: + +* **数据转换**:例如,将字符串转换为整数,将日期对象格式化为字符串,或者如示例中计算字符串长度、数字平方。 +* **对象属性提取**:从一个复杂对象中提取某个特定属性的值。例如,`Person -> String (person.getName())`。 +* **链式操作**:`Function` 接口提供了 `andThen()` 和 `compose()` 默认方法,可以方便地将多个 `Function` 串联起来形成一个处理流水线。 + +**您的示例代码分析** (`FunctionExample.java`): + +```java +import java.util.function.Function; + +public class FunctionExample { + + // 示例方法1: 接收一个 Function 来计算字符串长度 + public static Integer getStringLength(String text, Function lengthCalculator) { + // 调用 lengthCalculator 的 apply 方法,传入 text,执行其转换逻辑 + return lengthCalculator.apply(text); + } + + // 示例方法2: 接收一个 Function 来计算数字的平方 + public static Integer squareNumber(Integer number, Function squareFunction) { + return squareFunction.apply(number); + } + + public static void main(String[] args) { + // 场景1: 计算字符串长度 + String myString = "Java Functional"; + // Lambda 表达式实现 Function: s -> s.length() + // 这个 Lambda 接受一个 String s,返回其长度 (Integer) + Function lengthLambda = s -> s.length(); + Integer length = getStringLength(myString, lengthLambda); + System.out.println("字符串 '" + myString + "' 的长度是: " + length); + + // 使用方法引用 (Method Reference) 实现 Function: String::length + // String::length 等价于 s -> s.length(),更为简洁 + Integer lengthUsingMethodRef = getStringLength("Test", String::length); + System.out.println("字符串 'Test' 的长度是: " + lengthUsingMethodRef); + + // 场景2: 计算数字平方 + Integer num = 5; + // Lambda 表达式实现 Function: n -> n * n + // 接受一个 Integer n,返回 n 的平方 (Integer) + Function squareLambda = n -> n * n; + Integer squared = squareNumber(num, squareLambda); + System.out.println(num + " 的平方是: " + squared); + + Integer anotherNum = 10; + // 多行 Lambda 表达式 + Function verboseSquareLambda = x -> { + System.out.println("正在计算 " + x + " 的平方..."); // Lambda 可以包含多条语句 + return x * x; + }; + Integer squaredAgain = squareNumber(anotherNum, verboseSquareLambda); + System.out.println(anotherNum + " 的平方是: " + squaredAgain); + } +} +``` + +**代码解读**: + +* `getStringLength` 和 `squareNumber` 方法定义了操作的框架,但具体的转换逻辑由传入的 `Function` 对象决定。 +* 在 `main` 方法中: + * `s -> s.length()` 和 `String::length` 都是 `Function` 的实例,它们定义了“从字符串到其长度整数”的转换。 + * `n -> n * n` 是 `Function` 的实例,定义了“从整数到其平方整数”的转换。 + * 多行 Lambda `verboseSquareLambda` 展示了更复杂的转换逻辑可以被封装。 +* 这种方式使得我们可以为同一个通用方法(如 `getStringLength`)提供不同的转换策略。 + +**关键益处**: + +* **代码复用**:通用的转换逻辑可以被封装成 `Function` 并在多处使用。 +* **可组合性**:通过 `andThen` 和 `compose` 可以构建复杂的转换流。 +* **清晰性**:将数据转换的意图明确表达出来。 + +--- + +## 3. `BiConsumer` - 双参数的消费者/执行者 🤝 + +**接口定义**:`@FunctionalInterface public interface BiConsumer { void accept(T t, U u); }` + +**核心作用**: +`BiConsumer` 接口的核心职责是**对两个不同类型(或相同类型)的输入参数 `T` 和 `U` 执行某个操作或产生某种副作用,但它不返回任何结果 (void)**。你可以把它看作是需要两个输入才能完成其工作的“执行者”。 + +**方法详解**: + +* `void accept(T t, U u)`: 这是 `BiConsumer` 的核心方法。它接受两个参数 `t` 和 `u`,并对它们执行 Lambda 表达式或方法引用中定义的操作。由于返回类型是 `void`,它通常用于执行有副作用的操作,如打印、修改集合、更新数据库等。 + +**常见应用场景**: + +* **处理键值对**:非常适合用于迭代 `Map` 的条目,如 `Map.forEach()` 方法就接受一个 `BiConsumer`。 +* **同时操作两个相关对象**:当一个操作需要两个输入,并且不产生新的独立结果时。例如,将一个对象的属性设置到另一个对象上。 +* **配置或初始化**:使用两个参数来配置某个组件。 + +**您的示例代码分析** (`BiConsumerExample.java`): + +```java +import java.util.HashMap; +import java.util.Map; +import java.util.function.BiConsumer; + +public class BiConsumerExample { + + // 示例方法1: 接收 BiConsumer 来打印键和值 + public static void printMapEntry(K key, V value, BiConsumer entryPrinter) { + // 调用 entryPrinter 的 accept 方法,传入 key 和 value + entryPrinter.accept(key, value); + } + + // 示例2 在 main 中直接演示了更常见的 Map 操作方式 + + // 辅助内部类,如果 BiConsumer 需要一次性接收多个信息 (在此示例中未直接用于核心 BiConsumer 演示) + // static class Pair { + // F first; S second; + // Pair(F f, S s) { this.first = f; this.second = s; } + // } + + public static void main(String[] args) { + // 场景1: 使用 printMapEntry 打印键值 + // Lambda 表达式实现 BiConsumer: (k, v) -> System.out.println("键: " + k + ", 值: " + v) + // 接受一个 String k 和一个 Integer v,然后打印它们 + BiConsumer simplePrinter = (k, v) -> System.out.println("键: " + k + ", 值: " + v); + printMapEntry("年龄", 30, simplePrinter); + printMapEntry("数量", 100, simplePrinter); + + // 场景2: 使用 BiConsumer 来填充 Map + Map config = new HashMap<>(); + // Lambda 表达式实现 BiConsumer: (key, value) -> config.put(key, value) + // 这个 Lambda 捕获了外部的 'config' Map 对象。 + // 它接受 String key 和 String value,并将它们放入 config Map 中。 + BiConsumer mapPutter = (key, value) -> config.put(key, value); + + mapPutter.accept("user.name", "Alice"); // 执行操作:config.put("user.name", "Alice") + mapPutter.accept("user.role", "Admin"); // 执行操作:config.put("user.role", "Admin") + System.out.println("配置Map: " + config); + + // 场景3: Map.forEach() 的典型用法 + // Map 的 forEach 方法直接接受一个 BiConsumer + System.out.println("遍历Map:"); + config.forEach((key, value) -> { // 这里的 (key, value) -> {...} 就是一个 BiConsumer + System.out.println("配置项 - " + key + ": " + value); + }); + } +} +``` + +**代码解读**: + +* `printMapEntry` 方法接受一个键、一个值和一个 `BiConsumer`,该 `BiConsumer` 定义了如何处理这对键值。 +* 在 `main` 方法中: + * `simplePrinter`: 定义了一个行为——“接收一个键和一个值,并将它们打印到控制台”。 + * `mapPutter`: 定义了一个行为——“接收一个键和一个字符串值,并将它们存入外部的 `config` Map”。这里 Lambda 表达式捕获了外部变量 `config`,这是一种常见的用法。 + * `config.forEach(...)`: 这是 `BiConsumer` 最经典的用例之一。`forEach` 方法遍历 `Map` 中的每个条目,并对每个键值对执行提供的 `BiConsumer` 逻辑。 + +**关键益处**: + +* **处理成对数据**:专门设计用于需要两个输入的场景。 +* **与集合(尤其是Map)的良好集成**:`Map.forEach` 是一个很好的例子。 +* **封装副作用操作**:可以将对两个参数的副作用操作(如修改、打印)封装起来。 + +--- + +## 4. `Consumer` - 数据的消费者/执行者 🍽️ + +**接口定义**:`@FunctionalInterface public interface Consumer { void accept(T t); }` + +**核心作用**: +`Consumer` 接口的核心职责是**对单个输入参数 `T` 执行某个操作或产生某种副作用,它不返回任何结果 (void)**。你可以把它看作是数据的“终点”或某个动作的执行者,它“消费”数据但不产生新的输出数据。 + +**方法详解**: + +* `void accept(T t)`: 这是 `Consumer` 的核心方法。它接受一个 `T` 类型的参数 `t`,并对其执行 Lambda 表达式或方法引用中定义的操作。因为返回 `void`,它主要用于执行那些为了副作用而进行的操作(如打印、修改对象状态、写入文件等)。 + +**常见应用场景**: + +* **迭代集合并处理元素**:`List.forEach()` 方法接受一个 `Consumer`,对列表中的每个元素执行指定操作。 +* **打印/日志记录**:将信息输出到控制台、文件或其他日志系统。 +* **更新对象状态**:修改传入对象的属性。 +* **回调**:在某个异步操作完成后执行一个 `Consumer` 定义的动作。 + +**您的示例代码分析** (`ConsumerExample.java`): + +```java +import java.util.Arrays; +import java.util.List; +import java.util.function.Consumer; + +public class ConsumerExample { + + // 示例方法1: 接收 Consumer 来展示单个项目 + public static void displayItem(T item, Consumer itemDisplayer) { + // 调用 itemDisplayer 的 accept 方法,传入 item,执行其消费逻辑 + itemDisplayer.accept(item); + } + + // 示例方法2: 接收 Consumer 来处理列表中的每个项目 + public static void processListItems(List list, Consumer itemProcessor) { + for (T item : list) { + itemProcessor.accept(item); // 对列表中的每个 item 执行 itemProcessor 的逻辑 + } + } + + public static void main(String[] args) { + // 场景1: 使用 displayItem 打印信息 + // Lambda 表达式实现 Consumer: message -> System.out.println("消息: " + message) + // 接受一个 String message,然后打印它 + Consumer consolePrinter = message -> System.out.println("消息: " + message); + displayItem("你好,函数式接口!", consolePrinter); + + // 多行 Lambda 实现 Consumer,进行更复杂的打印 + Consumer detailedPrinter = number -> { + System.out.println("--- 数字详情 ---"); + System.out.println("值: " + number); + System.out.println("是否偶数: " + (number % 2 == 0)); + System.out.println("----------------"); + }; + displayItem(10, detailedPrinter); + displayItem(7, System.out::println); // 方法引用: System.out::println 等价于 x -> System.out.println(x) + + // 场景2: 使用 processListItems 处理列表 + List names = Arrays.asList("爱丽丝", "鲍勃", "查理"); + + System.out.println("\n打印名字:"); + // Lambda: name -> System.out.println("你好, " + name + "!") + // 对列表中的每个名字,执行打印问候语的操作 + processListItems(names, name -> System.out.println("你好, " + name + "!")); + + System.out.println("\n将名字转换为大写并打印 (仅打印,不修改原列表):"); + // Lambda: name -> System.out.println(name.toUpperCase()) + // 对列表中的每个名字,先转大写,然后打印 + processListItems(names, name -> System.out.println(name.toUpperCase())); + + // Consumer 也可以有副作用,比如修改外部状态 (通常需谨慎使用以避免复杂性) + StringBuilder allNames = new StringBuilder(); + // Lambda: name -> allNames.append(name).append(" ") + // 这个 Consumer 修改了外部的 allNames 对象 + processListItems(names, name -> allNames.append(name).append(" ")); + System.out.println("\n拼接所有名字: " + allNames.toString().trim()); + + // List.forEach 的典型用法 + System.out.println("\n使用 List.forEach 打印名字(大写):"); + names.forEach(name -> System.out.println(name.toUpperCase())); // name -> System.out.println(...) 是一个Consumer + } +} +``` + +**代码解读**: + +* `displayItem` 方法接受一个项目和一个 `Consumer`,该 `Consumer` 定义了如何“消费”或处理这个项目。 +* `processListItems` 方法遍历列表,并对每个元素应用传入的 `Consumer` 逻辑。这与 `List.forEach()` 的行为非常相似。 +* 在 `main` 方法中: + * `consolePrinter` 和 `detailedPrinter` 定义了不同的打印行为。`System.out::println` 是一个简洁的方法引用,用于直接打印。 + * 在处理 `names` 列表时,通过传递不同的 `Consumer` 给 `processListItems`,实现了不同的处理逻辑(简单问候、转换为大写打印、追加到 `StringBuilder`)。 + * `allNames.append(...)` 的例子展示了 `Consumer` 如何产生副作用(修改外部对象的状态)。虽然强大,但在复杂系统中应谨慎使用副作用,以保持代码的可预测性。 + * `names.forEach(...)` 直接使用了 `List` 接口内置的 `forEach` 方法,该方法就接受一个 `Consumer`。 + +**关键益处**: + +* **执行动作**:非常适合表示对数据执行的无返回值的操作。 +* **迭代与处理**:与集合框架(如 `List.forEach`)完美配合,简化迭代代码。 +* **封装副作用**:将有副作用的操作(如I/O、UI更新)封装到 `Consumer` 中,使得代码意图更清晰。 + +--- diff --git a/src/content/posts/Java/Spring/SpringBean生命周期.md b/src/content/posts/Java/Spring/SpringBean生命周期.md new file mode 100644 index 0000000..3457872 --- /dev/null +++ b/src/content/posts/Java/Spring/SpringBean生命周期.md @@ -0,0 +1,158 @@ +--- +title: SpringBean生命周期 +published: 2025-07-28 +description: '' +image: '' +tags: ["Spring","Java"] +category: 'Java > Spring' +draft: false +lang: '' +--- + +# Bean 的生命周期 + +![image](https://blog.meowrain.cn/api/i/2025/07/28/skj7xz.webp) + +Bean生命周期可以粗略的划分为五大步: + +第一步:实例化Bean +第二步:Bean属性赋值 +第三步:初始化Bean +第四步:使用Bean +第五步:销毁Bean +![image](https://blog.meowrain.cn/api/i/2025/07/28/si308s.webp) + +```java +package com.powercode.spring6.beans; + +public class User { + private String name; + public User() { + System.out.println("1.实例化Bean"); + } + + public void setName(String name) { + this.name = name; + System.out.println("2.Bean属性赋值"); + } + + public void initBean(){ + System.out.println("3.初始化Bean"); + } + + public void destroyBean(){ + System.out.println("5.销毁Bean"); + } + +} + +``` + +``` +2024-01-11 12:21:23 618 [main] DEBUG org.springframework.context.support.ClassPathXmlApplicationContext - Refreshing org.springframework.context.support.ClassPathXmlApplicationContext@183e8023 +2024-01-11 12:21:23 715 [main] DEBUG org.springframework.beans.factory.xml.XmlBeanDefinitionReader - Loaded 1 bean definitions from class path resource [spring12.xml] +2024-01-11 12:21:23 732 [main] DEBUG org.springframework.beans.factory.support.DefaultListableBeanFactory - Creating shared instance of singleton bean 'userBean' +1.实例化Bean +2.Bean属性赋值 +3.初始化Bean +4.使用Bean +2024-01-11 12:21:23 774 [main] DEBUG org.springframework.context.support.ClassPathXmlApplicationContext - Closing org.springframework.context.support.ClassPathXmlApplicationContext@183e8023, started on Thu Jan 11 12:21:23 CST 2024 +5.销毁Bean +2024-01-11 12:21:23 774 [main] DEBUG org.springframework.beans.factory.support.DisposableBeanAdapter - Custom destroy method 'destroyBean' on bean with name 'userBean' completed + +进程已结束,退出代码为 0 + +``` + +![image](https://blog.meowrain.cn/api/i/2025/07/28/sd8zrp.webp) + +## Bean后处理器 +加上后处理器就变成七步了: +![image](https://blog.meowrain.cn/api/i/2025/07/28/sj8wqs.webp) + + +### BeanPostProcessor 的核心作用 +BeanPostProcessor 本身并不属于某个特定 Bean 的生命周期,而是作用于容器中所有 Bean 的 “全局处理器”。它的核心功能是:在 Bean 完成实例化和属性赋值后、初始化方法(如 afterPropertiesSet() 或 init-method)执行前后,对 Bean 进行加工或增强。 + +上图中检查Bean是否实现了Aware的相关接口是什么意思? + + +## Aware相关接口 +Aware相关的接口包括:BeanNameAware、BeanClassLoaderAware、BeanFactoryAware + +当Bean实现了BeanNameAware,Spring会将Bean的名字传递给Bean。 +当Bean实现了BeanClassLoaderAware,Spring会将加载该Bean的类加载器传递给Bean。 +当Bean实现了BeanFactoryAware,Spring会将Bean工厂对象传递给Bean。 +测试以上10步,可以让User类实现5个接口,并实现所有方法: +- BeanNameAware +- BeanClassLoaderAware +- BeanFactoryAware +- InitializingBean +- DisposableBean + +## InitializingBean 的核心作用 +当一个 Bean 实现了 InitializingBean 接口后,Spring 容器会在该 Bean 的所有属性都被成功设置(即完成属性注入)之后,自动调用其 afterPropertiesSet() 方法。这一特性使得开发者可以在 Bean 正式投入使用前,进行一些必要的初始化操作,例如数据校验、资源加载、状态初始化等。 + +## DisposableBean核心作用 +DisposableBean 是 Spring 提供的销毁回调接口,其核心作用是在 Bean 即将被容器销毁前,触发自定义的清理操作。 +![image](https://blog.meowrain.cn/api/i/2025/07/28/sfkvri.webp) + +```java +package com.powercode.spring6.beans; + +import org.springframework.beans.BeansException; +import org.springframework.beans.factory.*; + +/** + * @author 动力节点 + * @version 1.0 + * @className User + * @since 1.0 + **/ +public class User implements BeanNameAware, BeanClassLoaderAware, BeanFactoryAware, InitializingBean, DisposableBean { + private String name; + + public User() { + System.out.println("1.实例化Bean"); + } + + public void setName(String name) { + this.name = name; + System.out.println("2.Bean属性赋值"); + } + + public void initBean(){ + System.out.println("6.初始化Bean"); + } + + public void destroyBean(){ + System.out.println("10.销毁Bean"); + } + + @Override + public void setBeanClassLoader(ClassLoader classLoader) { + System.out.println("3.类加载器:" + classLoader); + } + + @Override + public void setBeanFactory(BeanFactory beanFactory) throws BeansException { + System.out.println("3.Bean工厂:" + beanFactory); + } + + @Override + public void setBeanName(String name) { + System.out.println("3.bean名字:" + name); + } + + @Override + public void destroy() throws Exception { + System.out.println("9.DisposableBean destroy"); + } + + @Override + public void afterPropertiesSet() throws Exception { + System.out.println("5.afterPropertiesSet执行"); + } +} +``` + diff --git a/src/content/posts/Java/Spring/SpringBoot是如何实现自动配置的.md b/src/content/posts/Java/Spring/SpringBoot是如何实现自动配置的.md new file mode 100644 index 0000000..a7a1d07 --- /dev/null +++ b/src/content/posts/Java/Spring/SpringBoot是如何实现自动配置的.md @@ -0,0 +1,26 @@ +--- +title: SpringBoot是如何实现自动配置的 +published: 2025-09-15 +description: '' +image: '' +tags: ['SpringBoot','Java','自动配置'] +category: 'Java > Spring' +draft: false +lang: '' +--- + +# SpringBoot是如何实现自动配置的 + +Spring Boot的自动配置是通过 `@EnableAutoConfiguration` 注解来实现的。 +这个注解包含 `@Import({AutoConfigurationImportSelector.class})`注解 +导入的这两个类会扫描classpath下所有的`META-INF/spring.factories`中的文件,根据文件中指定的配置类加载相应的Bean的自动配置。 + +这些Bean通常会使用 `@ConditionOnClass`,`@ConditionOnMissingBean`,`@ConditionalOnProperty`等注解来控制自动配置的加载条件,例如仅在类路径中存在某个类的时候,才加载某些配置。 + +![](https://blog.meowrain.cn/api/i/2025/09/16/ip1efv-1.webp) + +![](https://blog.meowrain.cn/api/i/2025/09/16/ipewwv-1.webp) + +![](https://blog.meowrain.cn/api/i/2025/09/16/iqpz9m-1.webp) + +![](https://blog.meowrain.cn/api/i/2025/09/16/iquhuf-1.webp) diff --git a/src/content/posts/Java/Spring/SpringMVC工作流程.md b/src/content/posts/Java/Spring/SpringMVC工作流程.md new file mode 100644 index 0000000..b14eaa1 --- /dev/null +++ b/src/content/posts/Java/Spring/SpringMVC工作流程.md @@ -0,0 +1,17 @@ +--- +title: SpringMVC工作流程 +published: 2025-09-22 +description: '' +image: '' +tags: [SpringMVC,DispatcherServlet] +category: 'Java > Spring' +draft: false +lang: '' +--- + +![](https://blog.meowrain.cn/api/i/2025/09/22/ovqrfw-1.webp) + +![](https://blog.meowrain.cn/api/i/2025/09/22/oz4od3-1.webp) + + + diff --git a/src/content/posts/Java/Spring/Spring中拦截器和过滤器的区别.md b/src/content/posts/Java/Spring/Spring中拦截器和过滤器的区别.md new file mode 100644 index 0000000..b2b4c3b --- /dev/null +++ b/src/content/posts/Java/Spring/Spring中拦截器和过滤器的区别.md @@ -0,0 +1,141 @@ +--- +title: Spring中拦截器和过滤器的区别 +published: 2025-09-22 +description: '' +image: '' +tags: [SpringMVC,拦截器,过滤器] +category: 'Java > Spring' +draft: false +lang: '' +--- +https://www.mianshiya.com/question/1907425766060380162#heading-0 +![](https://blog.meowrain.cn/api/i/2025/09/22/p1p5sy-1.webp) + +# 过滤器 +## 实现机制 +过滤器是Servlet规范的一部分,独立于Spring存在,主要用于过滤请求和响应,可以对所有类型的请求进行处理。 +## 使用范围 +可以过滤所有的请求,包括静态资源,jsp,html等,因为它在Servlet容器层面生效。 +## 配置方法 +需要实现Filter接口,通过标准的Servlet配置方式进行注册: +https://www.cnblogs.com/xfeiyun/p/15790555.html + +https://juejin.cn/post/7000950677409103880 + +![](https://blog.meowrain.cn/api/i/2025/09/22/nk3hly-1.webp) +![](https://blog.meowrain.cn/api/i/2025/09/22/nker0x-1.webp) +![](https://blog.meowrain.cn/api/i/2025/09/22/nkq0yb-1.webp) +![](https://blog.meowrain.cn/api/i/2025/09/22/nl2e0e-1.webp) +![](https://blog.meowrain.cn/api/i/2025/09/22/nq0cv2-1.webp) +![](https://blog.meowrain.cn/api/i/2025/09/22/nq24rg-1.webp) +![](https://blog.meowrain.cn/api/i/2025/09/22/nq3xj5-1.webp) +![](https://blog.meowrain.cn/api/i/2025/09/22/nqp845-1.webp) +![](https://blog.meowrain.cn/api/i/2025/09/22/nqzx04-1.webp) +![](https://blog.meowrain.cn/api/i/2025/09/22/nr35ne-1.webp) + +## 执行顺序 +先于拦截器执行,因为过滤器作用于Servlet容器层面,拦截器作用在Spring MVC 的处理器映射器找到控制器前或者后执行。 + +## 功能侧重 +侧重于过滤请求和响应的内容,比如设置编码格式,安全控制等。 + +# 拦截器 +## 实现机制 +拦截器是Spring框架的一部分,基于Java的反射机制实现,主要针对的是Handler的调用 + +## 使用范围 +主要用于拦截访问DispathcherServlet的请求,通常只适用于Spring MVC的应用程序中的请求处理方法。 + + +## 配置方式 +需要实现org.springframework.web.servlet.HandlerInterceptor接口,并在Spring配置文件中进行注册。 +可以通过实现WebMvcConfigurer接口的addInterceptors方法来进行注册。 +![](https://blog.meowrain.cn/api/i/2025/09/22/p51up4-1.webp) +```java +package com.example.interceptor; + +import jakarta.servlet.http.HttpServletRequest; +import jakarta.servlet.http.HttpServletResponse; +import org.springframework.web.servlet.HandlerInterceptor; +import org.springframework.web.servlet.ModelAndView; + +/** + * @author wipe + * @version 1.0 + */ + +public class MyInterceptor1 implements HandlerInterceptor { + + @Override//目标资源方法执行前执行。 返回true:放行 返回false:不放行 + public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception { + + System.out.println("MyInterceptor1 ... preHandle"); + return true; + } + + @Override//目标资源方法执行后执行 + public void postHandle(HttpServletRequest request, HttpServletResponse response, Object handler, ModelAndView modelAndView) throws Exception { + System.out.println("MyInterceptor1 ... postHandle"); + } + + @Override//视图渲染完毕后执行,最后执行 + public void afterCompletion(HttpServletRequest request, HttpServletResponse response, Object handler, Exception ex) throws Exception { + System.out.println("MyInterceptor1 ... afterCompletion"); + } +} + +``` +```java +package com.example.config; + +import com.example.filter.MyFilter1; +import com.example.filter.MyFilter2; +import com.example.interceptor.MyInterceptor1; +import com.example.interceptor.MyInterceptor2; +import org.springframework.boot.web.servlet.FilterRegistrationBean; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.web.servlet.config.annotation.InterceptorRegistry; +import org.springframework.web.servlet.config.annotation.WebMvcConfigurer; +/** + * @author wipe + * @version 1.0 + */ +@Configuration +public class MyConfig implements WebMvcConfigurer { + + + @Override + public void addInterceptors(InterceptorRegistry registry) { + // 添加拦截器,并指定执行顺序,也可以通过将拦截器声明成 bean 对象,然后通过 @Order 注解或者实现 Order 接口指定执行顺序 + registry.addInterceptor(new MyInterceptor1()).order(1); + registry.addInterceptor(new MyInterceptor2()).order(2); + } + + + @Bean// 这样配置可以指定过滤器的执行顺序 + public FilterRegistrationBean myFilter1() { + FilterRegistrationBean filter = new FilterRegistrationBean<>(); + filter.setFilter(new MyFilter1()); + filter.addUrlPatterns("/*"); + filter.setOrder(1); + return filter; + } + + @Bean + public FilterRegistrationBean myFilter2() { + FilterRegistrationBean filter = new FilterRegistrationBean<>(); + filter.setFilter(new MyFilter2()); + filter.addUrlPatterns("/*"); + filter.setOrder(2); + return filter; + } +} + +``` +也可以直接用@Component注册Interceptor +## 执行顺序 +可以指定多个拦截器之间的执行顺序,通过实现Ordered接口或者使用@Order注解来控制。 + +## 功能侧重 +侧重于业务逻辑的前置检查,权限验证,日志记录等。 diff --git a/src/content/posts/Java/Spring/Spring中的BeanFactory与FactoryBean.md b/src/content/posts/Java/Spring/Spring中的BeanFactory与FactoryBean.md new file mode 100644 index 0000000..a546c03 --- /dev/null +++ b/src/content/posts/Java/Spring/Spring中的BeanFactory与FactoryBean.md @@ -0,0 +1,265 @@ +--- +title: Spring中的BeanFactory与FactoryBean +published: 2025-08-08 +description: 深入理解Spring容器的核心接口BeanFactory与特殊工厂Bean——FactoryBean的区别、使用场景与最佳实践 +image: '' +tags: [Java, Spring, IoC, DI, BeanFactory, FactoryBean] +category: 'Java > Spring' +draft: false +lang: zh-CN +--- + +# BeanFactory +BeanFactory是一个工厂接口,是一个负责生产和管理bean的一个工厂。BeanFactory是工厂的顶层接口,是IOC容器的核心接口,BeanFactory定义了管理Bean的通用方法,如getBean和containsBean等,它的职责包括: +- Bean实例化: 根据XML注解等创建Bean对象。 +- 依赖注入: 自动将Bean所需的依赖注入进去。 +- 生命周期管理: 管理Bean的初始化,销毁等生命周期方法。 +- 延迟加载: 默认采用懒加载策略,只有在调用getBean()时才创建Bean实例。 +- Bean获取: 提供getBean()方法来获取Bean实例。 + +![](https://blog.meowrain.cn/api/i/2025/08/08/fkc5xn-1.webp) + +BeanFactory只是一个接口,不是IOC容器的具体实现,所以Spring容器给出了很多种实现,如XmlBeanFactory、AnnotationConfigApplicationContext,ApplicationContext等。 + +## BeanFactory 的常见实现类 + +Spring 提供了多种 BeanFactory 的实现,每种实现都有其特定的使用场景: + +### DefaultListableBeanFactory +最常用的完整实现,支持完整的Bean生命周期管理: + +```java +import org.springframework.beans.factory.support.DefaultListableBeanFactory; +import org.springframework.beans.factory.support.RootBeanDefinition; + +public class BeanFactoryExample { + public static void main(String[] args) { + DefaultListableBeanFactory factory = new DefaultListableBeanFactory(); + + // 手动注册Bean定义 + RootBeanDefinition beanDefinition = new RootBeanDefinition(MyService.class); + factory.registerBeanDefinition("myService", beanDefinition); + + // 获取Bean + MyService service = factory.getBean("myService", MyService.class); + service.doSomething(); + } +} + +class MyService { + public void doSomething() { + System.out.println("Service is working!"); + } +} +``` + +### XmlBeanFactory(已废弃) +基于XML配置的BeanFactory实现,Spring 5.x后已废弃,推荐使用ApplicationContext: + +```java +// 传统用法(不推荐) +// XmlBeanFactory factory = new XmlBeanFactory(new ClassPathResource("beans.xml")); + +// 现代替代方案 +import org.springframework.context.support.ClassPathXmlApplicationContext; + +ClassPathXmlApplicationContext context = new ClassPathXmlApplicationContext("beans.xml"); +MyService service = context.getBean("myService", MyService.class); +``` + +### StaticListableBeanFactory +静态Bean工厂,适用于Bean集合固定的场景: + +```java +import org.springframework.beans.factory.support.StaticListableBeanFactory; + +StaticListableBeanFactory factory = new StaticListableBeanFactory(); +factory.addBean("myService", new MyService()); +factory.addBean("anotherService", new AnotherService()); + +MyService service = factory.getBean("myService", MyService.class); +``` + +### ApplicationContext实现类 +作为BeanFactory的高级实现,提供更多企业级特性: + +```java +// 注解配置 +import org.springframework.context.annotation.AnnotationConfigApplicationContext; + +AnnotationConfigApplicationContext context = + new AnnotationConfigApplicationContext(AppConfig.class); + +// XML配置 +import org.springframework.context.support.ClassPathXmlApplicationContext; + +ClassPathXmlApplicationContext xmlContext = + new ClassPathXmlApplicationContext("applicationContext.xml"); + +// Web环境 +import org.springframework.web.context.support.AnnotationConfigWebApplicationContext; + +AnnotationConfigWebApplicationContext webContext = + new AnnotationConfigWebApplicationContext(); +webContext.register(WebConfig.class); +``` + +### 实际应用示例 +结合Bean定义构建器的完整示例: + +```java +import org.springframework.beans.factory.support.BeanDefinitionBuilder; +import org.springframework.beans.factory.support.DefaultListableBeanFactory; + +public class CustomBeanFactoryDemo { + public static void main(String[] args) { + DefaultListableBeanFactory factory = new DefaultListableBeanFactory(); + + // 使用BeanDefinitionBuilder构建复杂Bean + BeanDefinitionBuilder builder = BeanDefinitionBuilder + .rootBeanDefinition(DatabaseService.class) + .addPropertyValue("url", "jdbc:mysql://localhost:3306/mydb") + .addPropertyValue("username", "root") + .setScope("singleton") + .setLazyInit(true); + + factory.registerBeanDefinition("dbService", builder.getBeanDefinition()); + + // 懒加载验证 + System.out.println("Bean定义已注册,但未实例化"); + + DatabaseService dbService = factory.getBean("dbService", DatabaseService.class); + System.out.println("现在Bean被实例化了"); + } +} + +class DatabaseService { + private String url; + private String username; + + // getters and setters + public void setUrl(String url) { this.url = url; } + public void setUsername(String username) { this.username = username; } + + public void connect() { + System.out.println("连接到: " + url + " 用户: " + username); + } +} +``` + + +## BeanFactory 与 ApplicationContext 的区别 +- 预实例化策略 + - BeanFactory:单例默认懒加载。 + - ApplicationContext:默认预实例化单例(提高启动后首次访问的吞吐)。 +- 扩展能力 + - ApplicationContext 额外提供国际化、事件发布、AOP自动代理、资源模式解析等企业特性。 +- 适用场景 + - BeanFactory:资源受限、极致冷启动、强控制懒加载/条件加载。 + - ApplicationContext:大多数应用优先选择。 +- 调优提示 + - 需要懒加载时,可结合ApplicationContext + @Lazy 或者使用ObjectProvider/Provider按需获取。 + +# FactoryBean +在Spring中,所有的Bean都是由BeanFactory管理的(IOC容器), +这个FactoryBean不是简单的Bean,而是一个能生产或者修饰对象生成的工厂Bean,它的实现与设计模式中的工厂模式和修饰器模式类似。 + + +## FactoryBean 的作用 + +![](https://blog.meowrain.cn/api/i/2025/08/08/gzh7kd-1.webp) + +- 将“复杂对象的创建逻辑”封装到工厂Bean中,对外暴露的是“产品对象”而不是工厂本身。 +- 常用于:动态代理(AOP/远程代理)、框架桥接(如MyBatis的SqlSessionFactoryBean)、复杂构建(连接池、客户端SDK)。 + +## 核心接口方法 +- getObject(): 返回实际产品对象(对外暴露的Bean)。 +- getObjectType(): 返回产品类型,便于类型匹配与自动装配。 +- isSingleton(): 决定产品是否为单例(影响缓存与生命周期)。 + +## 获取“产品”还是“工厂本身” +- 普通名:context.getBean("beanName") 获取的是产品对象(getObject返回值)。 +- 带&前缀:context.getBean("&beanName") 获取的是FactoryBean自身。 +- 命名规则:注册名为 x 的 FactoryBean,会对外暴露“产品”名为 x,“工厂自身”为 &x。 + +## 最小可运行示例 +```java +// 一个业务产品 +public class ApiClient { + private final String endpoint; + public ApiClient(String endpoint) { this.endpoint = endpoint; } + public String call(String path) { return "GET " + endpoint + path; } +} + +// FactoryBean 实现 +import org.springframework.beans.factory.FactoryBean; +import org.springframework.beans.factory.InitializingBean; +import org.springframework.util.Assert; + +public class ApiClientFactoryBean implements FactoryBean, InitializingBean { + private String endpoint; + public void setEndpoint(String endpoint) { this.endpoint = endpoint; } + + @Override + public ApiClient getObject() { + // 可在此放入复杂构建/代理/缓存等逻辑 + return new ApiClient(endpoint); + } + + @Override + public Class getObjectType() { return ApiClient.class; } + + @Override + public boolean isSingleton() { return true; } + + @Override + public void afterPropertiesSet() { + Assert.hasText(endpoint, "endpoint must not be empty"); + } +} + +// Java 配置注册 FactoryBean +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; + +@Configuration +public class AppConfig { + @Bean(name = "apiClient") + public ApiClientFactoryBean apiClientFactoryBean() { + ApiClientFactoryBean fb = new ApiClientFactoryBean(); + fb.setEndpoint("https://api.example.com"); + return fb; + } +} + +// 取产品与取工厂本身 +import org.springframework.context.ApplicationContext; +import org.springframework.context.annotation.AnnotationConfigApplicationContext; + +public class Demo { + public static void main(String[] args) { + ApplicationContext ctx = new AnnotationConfigApplicationContext(AppConfig.class); + ApiClient client = ctx.getBean("apiClient", ApiClient.class); // 产品 + ApiClientFactoryBean fb = ctx.getBean("&apiClient", ApiClientFactoryBean.class); // 工厂本身 + System.out.println(client.call("/ping")); + } +} +``` + +## 常见坑与最佳实践 +- getObjectType 切勿返回 null:尽量返回接口/具体类型,便于按类型注入与AOT分析。 +- isSingleton 与产品生命周期:单例将被容器缓存;非单例每次getBean都会重新创建产品。 +- 懒加载与预实例化:在ApplicationContext中,如希望延迟创建可使用@Lazy或将FactoryBean产品设为非单例。 +- 自动装配歧义:按类型注入时,注入到的是产品类型而非FactoryBean;需要注入工厂本身时使用@Qualifier("&name")或@Resource(name="&name")。 +- 原型产品与循环依赖:原型产品不参与循环依赖的三级缓存提前暴露,避免在原型链路中引入循环依赖。 +- 命名规范:确保文档/注释标明“&”语义,避免团队误用。 + +## 什么时候用哪一个? +- 仅需容器功能:优先ApplicationContext(功能更全,默认预实例化)。 +- 需要懒加载到极致:考虑BeanFactory或在ApplicationContext中对关键Bean标注@Lazy。 +- 对象构建复杂/需代理/外部SDK桥接:使用FactoryBean封装构建细节,对外仅暴露产品。 + +## 小结 +- BeanFactory 是IoC容器的最小抽象;ApplicationContext在其上增强了企业级特性。 +- FactoryBean 是“创建Bean的Bean”,对外暴露产品;使用“&name”获取工厂本身。 +- 合理利用FactoryBean可显著简化复杂对象创建,并保持应用装配的清晰与解耦。 \ No newline at end of file diff --git a/src/content/posts/Java/Spring/Spring常见面试题.md b/src/content/posts/Java/Spring/Spring常见面试题.md new file mode 100644 index 0000000..7c02154 --- /dev/null +++ b/src/content/posts/Java/Spring/Spring常见面试题.md @@ -0,0 +1,172 @@ +--- +title: Spring常见面试题 +published: 2025-07-28 +description: '' +image: '' +tags: ["Spring","Java"] +category: 'Java > Spring' +draft: false +lang: '' +--- + +# @Autowired 和 @Resource 的区别是什么? + +@Autowired属于Spring内置的注解,默认的注入方式是byType,也就是根据类型匹配,当有多个实现时 +byType就没办法正确注入了,这个时候可以结合@Qualifier注解一起使用,指定注入的名称。当然也可以使用byName,也就是根据名称注入,但是需要结合@Qualifier注解一起使用。 + +@Resource 是Java自带注解,属于J2EE的,默认注入方式是byName,也就是根据名称注入,当找不到与名称匹配的bean时,根据类型注入。当然也可以结合@Qualifier注解一起使用,指定注入的名称。 + +@Resource 有两个比较重要且日常开发常用的属性:name(名称)、type(类型)。 +如果仅指定 name 属性则注入方式为byName,如果仅指定type属性则注入方式为byType,如果同时指定name 和type属性(不建议这么做)则注入方式为byType+byName。 + +@Autowired 支持在构造函数、方法、字段和参数上使用。 +@Resource 主要用于字段和方法上的注入,不支持在构造函数或参数上使用。 + + +# Bean 的生命周期 + +![image](https://blog.meowrain.cn/api/i/2025/07/28/skj7xz.webp) + +Bean生命周期可以粗略的划分为五大步: + +第一步:实例化Bean +第二步:Bean属性赋值 +第三步:初始化Bean +第四步:使用Bean +第五步:销毁Bean +![image](https://blog.meowrain.cn/api/i/2025/07/28/si308s.webp) + +```java +package com.powercode.spring6.beans; + +public class User { + private String name; + public User() { + System.out.println("1.实例化Bean"); + } + + public void setName(String name) { + this.name = name; + System.out.println("2.Bean属性赋值"); + } + + public void initBean(){ + System.out.println("3.初始化Bean"); + } + + public void destroyBean(){ + System.out.println("5.销毁Bean"); + } + +} + +``` + +``` +2024-01-11 12:21:23 618 [main] DEBUG org.springframework.context.support.ClassPathXmlApplicationContext - Refreshing org.springframework.context.support.ClassPathXmlApplicationContext@183e8023 +2024-01-11 12:21:23 715 [main] DEBUG org.springframework.beans.factory.xml.XmlBeanDefinitionReader - Loaded 1 bean definitions from class path resource [spring12.xml] +2024-01-11 12:21:23 732 [main] DEBUG org.springframework.beans.factory.support.DefaultListableBeanFactory - Creating shared instance of singleton bean 'userBean' +1.实例化Bean +2.Bean属性赋值 +3.初始化Bean +4.使用Bean +2024-01-11 12:21:23 774 [main] DEBUG org.springframework.context.support.ClassPathXmlApplicationContext - Closing org.springframework.context.support.ClassPathXmlApplicationContext@183e8023, started on Thu Jan 11 12:21:23 CST 2024 +5.销毁Bean +2024-01-11 12:21:23 774 [main] DEBUG org.springframework.beans.factory.support.DisposableBeanAdapter - Custom destroy method 'destroyBean' on bean with name 'userBean' completed + +进程已结束,退出代码为 0 + +``` + +![image](https://blog.meowrain.cn/api/i/2025/07/28/sd8zrp.webp) + +## Bean后处理器 +加上后处理器就变成七步了: +![image](https://blog.meowrain.cn/api/i/2025/07/28/sj8wqs.webp) + + +### BeanPostProcessor 的核心作用 +BeanPostProcessor 本身并不属于某个特定 Bean 的生命周期,而是作用于容器中所有 Bean 的 “全局处理器”。它的核心功能是:在 Bean 完成实例化和属性赋值后、初始化方法(如 afterPropertiesSet() 或 init-method)执行前后,对 Bean 进行加工或增强。 + +上图中检查Bean是否实现了Aware的相关接口是什么意思? + + +## Aware相关接口 +Aware相关的接口包括:BeanNameAware、BeanClassLoaderAware、BeanFactoryAware + +当Bean实现了BeanNameAware,Spring会将Bean的名字传递给Bean。 +当Bean实现了BeanClassLoaderAware,Spring会将加载该Bean的类加载器传递给Bean。 +当Bean实现了BeanFactoryAware,Spring会将Bean工厂对象传递给Bean。 +测试以上10步,可以让User类实现5个接口,并实现所有方法: +- BeanNameAware +- BeanClassLoaderAware +- BeanFactoryAware +- InitializingBean +- DisposableBean + +## InitializingBean 的核心作用 +当一个 Bean 实现了 InitializingBean 接口后,Spring 容器会在该 Bean 的所有属性都被成功设置(即完成属性注入)之后,自动调用其 afterPropertiesSet() 方法。这一特性使得开发者可以在 Bean 正式投入使用前,进行一些必要的初始化操作,例如数据校验、资源加载、状态初始化等。 + +## DisposableBean核心作用 +DisposableBean 是 Spring 提供的销毁回调接口,其核心作用是在 Bean 即将被容器销毁前,触发自定义的清理操作。 +![image](https://blog.meowrain.cn/api/i/2025/07/28/sfkvri.webp) + +```java +package com.powercode.spring6.beans; + +import org.springframework.beans.BeansException; +import org.springframework.beans.factory.*; + +/** + * @author 动力节点 + * @version 1.0 + * @className User + * @since 1.0 + **/ +public class User implements BeanNameAware, BeanClassLoaderAware, BeanFactoryAware, InitializingBean, DisposableBean { + private String name; + + public User() { + System.out.println("1.实例化Bean"); + } + + public void setName(String name) { + this.name = name; + System.out.println("2.Bean属性赋值"); + } + + public void initBean(){ + System.out.println("6.初始化Bean"); + } + + public void destroyBean(){ + System.out.println("10.销毁Bean"); + } + + @Override + public void setBeanClassLoader(ClassLoader classLoader) { + System.out.println("3.类加载器:" + classLoader); + } + + @Override + public void setBeanFactory(BeanFactory beanFactory) throws BeansException { + System.out.println("3.Bean工厂:" + beanFactory); + } + + @Override + public void setBeanName(String name) { + System.out.println("3.bean名字:" + name); + } + + @Override + public void destroy() throws Exception { + System.out.println("9.DisposableBean destroy"); + } + + @Override + public void afterPropertiesSet() throws Exception { + System.out.println("5.afterPropertiesSet执行"); + } +} +``` + diff --git a/src/content/posts/Java/Spring/Spring配置相关的注解.md b/src/content/posts/Java/Spring/Spring配置相关的注解.md new file mode 100644 index 0000000..5d4e10e --- /dev/null +++ b/src/content/posts/Java/Spring/Spring配置相关的注解.md @@ -0,0 +1,291 @@ +--- +title: Spring配置相关的注解 +published: 2025-08-08 +description: '' +image: '' +tags: ["Spring","Java"] +category: 'Java > Spring' +draft: false +lang: '' +--- + + +当然!这是一个非常重要且实用的主题。在 Spring 和 Spring Boot 中,与属性(Property)相关的注解是实现“配置与代码分离”这一核心原则的关键。 + +我将为你全面、系统地讲解这些注解,从最基础到最常用,再到高级用法,并配上清晰的示例。 + +我们将主要围绕以下几个核心注解展开: + +1. **`@Value`**: 最基础的,用于注入单个属性值。 +2. **`@PropertySource`**: 用于加载指定的属性文件。 +3. **`@ConfigurationProperties`**: 最强大、最推荐的,用于类型安全地将一组属性绑定到Java对象上。 +4. **`@EnableConfigurationProperties`**: 与 `@ConfigurationProperties` 配合使用,用于激活属性绑定。 +5. **`@TestPropertySource`**: 在测试环境中加载或覆盖属性。 + +--- + +### 注解族谱概览 + +为了方便理解,我们可以把它们分为三类: + +* **值注入 (Value Injection)**: `@Value` +* **配置源 (Configuration Source)**: `@PropertySource`, `@TestPropertySource` +* **批量绑定 (Bulk Binding)**: `@ConfigurationProperties`, `@EnableConfigurationProperties` + +--- + +### 1. `@Value`:简单直接的“单兵作战” + +这是注入属性最基本的方式。 + +* **作用**: 将 Spring 环境(Environment)中的单个属性值注入到类的字段或方法参数中。 +* **语法**: 使用 SpEL (Spring Expression Language) 表达式 `"${property.key}"`。 +* **优点**: 简单、直接,适合注入少量、分散的配置。 +* **缺点**: + * 当属性很多时,代码会显得分散和混乱。 + * 类型安全性较弱(都是字符串,需要Spring转换)。 + * 重构时(如修改前缀)非常痛苦。 + +**示例:** + +**`src/main/resources/application.properties`** +```properties +app.name=My Awesome App +app.version=2.1.5 +app.author.name=Alex +# 如果某个属性可能不存在,可以提供默认值 +# mail.default.sender=default@example.com +``` + +**Java 类** +```java +import org.springframework.beans.factory.annotation.Value; +import org.springframework.stereotype.Component; + +@Component +public class AppInfoService { + + // 注入 app.name 属性 + @Value("${app.name}") + private String appName; + + // 注入 app.version 属性 + @Value("${app.version}") + private String appVersion; + + // 注入一个不存在的属性,但提供了默认值 "unknown" + @Value("${app.description:unknown description}") + private String appDescription; + + // 也可以注入其他 Bean 的属性(使用 SpEL) + @Value("#{someOtherBean.someProperty}") + private String otherProperty; + + public void printAppInfo() { + System.out.println("App Name: " + appName); + System.out.println("App Version: " + appVersion); + System.out.println("App Description: " + appDescription); + } +} +``` + +--- + +### 2. `@PropertySource`:指定“情报来源” + +默认情况下,Spring Boot 会自动加载 `application.properties` 或 `application.yml`。如果你想加载其他配置文件,就需要用到 `@PropertySource`。 + +* **作用**: 将指定的属性文件加载到 Spring 的 `Environment` 中。 +* **使用场景**: + * 模块化配置,将不同功能的配置放在不同文件里(如 `mail.properties`, `db.properties`)。 + * 加载 classpath 之外的文件系统中的配置。 +* **注意**: 它只负责**加载**,不负责注入。加载后,你可以用 `@Value` 或 `@ConfigurationProperties` 来使用这些属性。 + +**示例:** + +**`src/main/resources/mail.properties`** +```properties +mail.host=smtp.gmail.com +mail.port=587 +``` + +**Java 配置类** +```java +import org.springframework.context.annotation.Configuration; +import org.springframework.context.annotation.PropertySource; +import org.springframework.beans.factory.annotation.Value; + +@Configuration +// 加载 classpath 下的 mail.properties 文件 +@PropertySource("classpath:mail.properties") +public class MailConfig { + + @Value("${mail.host}") + private String host; + + @Value("${mail.port}") + private int port; + + // ... +} +``` + +--- + +### 3. `@ConfigurationProperties` + +这是 Spring Boot **最推荐**的属性管理方式。它将一组相关的属性映射到一个类型安全的 Java 对象(POJO)上。 + +* **作用**: 将具有相同前缀的属性批量绑定到一个 POJO 的字段上。 +* **优点**: + * **类型安全**: 直接映射到 `int`, `List`, `Duration` 等各种类型。 + * **结构清晰**: 将相关配置聚合在一个类中,非常易于管理和维护。 + * **强大的绑定**: 支持复杂的对象图,比如嵌套对象、列表、Map等。 + * **IDE 友好**: 主流 IDE(如 IntelliJ IDEA)支持对 `application.properties` 中这类属性的自动补全和导航(需要添加 `spring-boot-configuration-processor` 依赖)。 + +**示例:** + +**`application.properties`** +```properties +app.server.name=prod-server +app.server.ip-address=192.168.1.100 +app.server.timeout=30s # Spring Boot 2.x 支持时间单位 +app.server.admins[0].name=admin1 +app.server.admins[0].email=admin1@corp.com +app.server.admins[1].name=admin2 +app.server.admins[1].email=admin2@corp.com +``` + +**Java 属性类 (POJO)** + +```java +import org.springframework.boot.context.properties.ConfigurationProperties; +import java.time.Duration; +import java.util.List; + +// 告诉 Spring 这个类要绑定前缀为 "app.server" 的属性 +@ConfigurationProperties(prefix = "app.server") +public class ServerProperties { + + private String name; + private String ipAddress; + private Duration timeout; // 自动将 "30s" 转换为 Duration 对象 + private List admins; + + // 嵌套类 + public static class Admin { + private String name; + private String email; + // Getters and Setters for Admin + } + + // ⭐ 重要: 必须为所有字段提供 public Getters and Setters + // Spring 通过它们来注入值 + // ... Getters and Setters for ServerProperties ... +} +``` + +--- + +### 4. `@EnableConfigurationProperties` + +`@ConfigurationProperties` 只是一个声明,它本身不会让这个 POJO 成为一个 Spring Bean。你需要一种方式来“激活”它。`@EnableConfigurationProperties` 就是这个开关。 + +* **作用**: + 1. 告诉 Spring 去处理被 `@ConfigurationProperties` 注解的类。 + 2. 将被注解的类(如 `ServerProperties`)注册到 Spring 容器中,让它成为一个 Bean。这样你就可以在其他地方 `@Autowired` 注入它了。 +* **通常放在哪**: 主启动类或任何 `@Configuration` 类上。 + +**示例:** + +```java +import org.springframework.boot.SpringApplication; +import org.springframework.boot.autoconfigure.SpringBootApplication; +import org.springframework.boot.context.properties.EnableConfigurationProperties; + +@SpringBootApplication +// 激活对 ServerProperties 类的绑定,并将其注册为 Bean +@EnableConfigurationProperties(ServerProperties.class) +public class MyApplication { + + public static void main(String[] args) { + SpringApplication.run(MyApplication.class, args); + } +} + +// 现在可以在任何其他组件中注入它 +@Service +public class MyService { + private final ServerProperties serverProps; + + @Autowired + public MyService(ServerProperties serverProps) { + this.serverProps = serverProps; + System.out.println("Server Name: " + serverProps.getName()); + } +} +``` + +> **快捷方式**: 如果你在 `ServerProperties` 类上同时加上 `@Component` 和 `@ConfigurationProperties`,就可以省略 `@EnableConfigurationProperties`。但显式使用 `@EnableConfigurationProperties` 通常被认为是更清晰的做法,因为它明确表达了这是一个配置类。 + +--- + +### 5. `@TestPropertySource`:为测试“定制情报” + +在进行单元测试或集成测试时,我们经常需要使用一套不同于生产环境的配置(比如连接到内存数据库 H2)。 + +* **作用**: 在测试上下文中加载属性,它可以覆盖`application.properties`中的属性或添加新属性。 +* **常用属性**: + * `locations`: 指定要加载的属性文件路径。 + * `properties`: 以 `key=value` 形式直接定义内联属性。 + +**示例:** + +```java +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.test.context.SpringBootTest; +import org.springframework.core.env.Environment; +import org.springframework.test.context.TestPropertySource; +import org.junit.jupiter.api.Test; +import static org.assertj.core.api.Assertions.assertThat; + +@SpringBootTest +// 1. 加载 test.properties 文件 +// 2. 直接定义一个内联属性,它会覆盖 test.properties 或 application.properties 中的同名属性 +@TestPropertySource( + locations = "classpath:test.properties", + properties = "app.version=test-1.0" +) +public class AppInfoServiceTest { + + @Autowired + private Environment env; + + @Test + void testPropertiesAreLoaded() { + // 来自 test.properties + assertThat(env.getProperty("app.name")).isEqualTo("Test App"); + + // 被内联属性覆盖 + assertThat(env.getProperty("app.version")).isEqualTo("test-1.0"); + } +} +``` + +--- + +### 总结与最佳实践 + +| 注解 | 用途 | 何时使用 | +| :--- | :--- | :--- | +| **`@Value`** | 注入单个值 | 当你只需要一两个简单的配置时。 | +| **`@PropertySource`** | 加载额外的属性文件 | 当你的配置分散在多个自定义文件中时。 | +| **`@ConfigurationProperties`** | **批量**、**类型安全**地绑定属性到对象 | **首选方式**。当你有一组相关配置时(如数据库、邮件、API密钥等)。 | +| **`@EnableConfigurationProperties`** | 激活 `@ConfigurationProperties` 的类 | 总是与 `@ConfigurationProperties` 配合使用(除非用了`@Component`快捷方式)。 | +| **`@TestPropertySource`** | 在测试中覆盖或提供配置 | 编写需要特定配置的集成测试或单元测试时。 | + +**最佳实践**: + +* **优先使用 `@ConfigurationProperties`**:对于任何超过两三个的相关配置,都应创建一个专用的 `Properties` 类。这会让你的代码更健壮、更易于维护。 +* **集中管理**: 将 `@EnableConfigurationProperties` 放在主配置类或一个集中的 `AppConfig` 类中,而不是到处分散。 +* **利用元数据**: 添加 `spring-boot-configuration-processor` 依赖到 `pom.xml` 或 `build.gradle`,以获得强大的 IDE 支持。 \ No newline at end of file diff --git a/src/content/posts/Java/Spring/什么是循环依赖.md b/src/content/posts/Java/Spring/什么是循环依赖.md new file mode 100644 index 0000000..ba2619e --- /dev/null +++ b/src/content/posts/Java/Spring/什么是循环依赖.md @@ -0,0 +1,39 @@ +--- +title: 什么是循环依赖 +published: 2025-09-16 +description: '' +image: '' +tags: [Spring,循环依赖,Java] +category: 'Java > Spring' +draft: false +lang: '' +--- + +# 什么是循环依赖 +循环依赖就是指两个或者多个模块,类组件之间互相依赖,形成一个闭环 + +```java +@Service +public class A { + @Autowired + private B b; +} + +@Service +public class B { + @Autowired + private A a; +} + +//或者自己依赖自己 +@Service +public class A { + @Autowired + private A a; +} + +``` + +就像上面这种情况,就属于循环依赖 + + diff --git a/src/content/posts/Java/Spring/如何解决循环依赖.md b/src/content/posts/Java/Spring/如何解决循环依赖.md new file mode 100644 index 0000000..2cdebdd --- /dev/null +++ b/src/content/posts/Java/Spring/如何解决循环依赖.md @@ -0,0 +1,109 @@ +--- +title: Spring如何解决循环依赖 +published: 2025-09-16 +description: '' +image: '' +tags: [Spring,循环依赖,Java] +category: 'Java > Spring' +draft: false +lang: '' +--- + +# Spring如何解决循环依赖 + +关键是`提前暴露未完全创建完毕的Bean` +Spring中采用了`三级缓存`解决了循环依赖 + +![](https://blog.meowrain.cn/api/i/2025/09/16/p86lpo-1.webp) + +![](https://blog.meowrain.cn/api/i/2025/09/16/p88uqg-1.webp) + +![](https://blog.meowrain.cn/api/i/2025/09/16/p8o5kd-1.webp) + + + +我们拿下面这个例子来讲 +```java +@Service +public class A { + @Autowired + private B b; +} + +@Service +public class B { + @Autowired + private A a; +} + +//或者自己依赖自己 +@Service +public class A { + @Autowired + private A a; +} + +``` + +首先要创建Bean A,去一级缓存里面找,发现没有,二级缓存里面找,发现也没有,三级里面也没有 +这个时候进入Bean A 的对象创建流程 + +接下来我们利用反射创建对象A,调用其无参构造方法,创建一个对象A 的实例,并将其包装成ObjectFactory放入三级缓存中。 +![](https://blog.meowrain.cn/api/i/2025/09/16/p9xvzy-1.webp) + +![](https://blog.meowrain.cn/api/i/2025/09/16/pa0adk-1.webp) + +接下来要填充属性 +![](https://blog.meowrain.cn/api/i/2025/09/16/pac0j5-1.webp) + +因为A对象的属性是B对象 + +所以现在要开始创建Bean B +到一级,二级,三级缓存中找B对象,发现不存在,所以进入B对象的创建流程 + +依然是通过反射,调用B的无参构造方法创建B的实例,并将其包装成ObjectFactory放入三级缓存中。 +![](https://blog.meowrain.cn/api/i/2025/09/16/pb08w0-1.webp) + +接下来要填充B对象的属性,就又要进入Bean A的创建流程中,再去缓存中查找A对象,我们能发现在三级缓存中已经有A的ObjectFactory了 + +![](https://blog.meowrain.cn/api/i/2025/09/16/pbu2du-1.webp) +![](https://blog.meowrain.cn/api/i/2025/09/16/pc7z5d-1.webp) + +可以从源码中看到,我们会调用存放在三级缓存中A的ObjectFactory的getObject方法,创建单例对象,存放在earlySingletonObjects里面(二级缓存),然后从三级缓存中移除A的ObjectFactory + +![](https://blog.meowrain.cn/api/i/2025/09/16/pdc4ej-1.webp) + + +好的,这样的话,我们就可以把A填充到B对象需要的属性里面了 +![](https://blog.meowrain.cn/api/i/2025/09/16/pde1m5-1.webp) + +![](https://blog.meowrain.cn/api/i/2025/09/16/pdwvlu-1.webp) + +![](https://blog.meowrain.cn/api/i/2025/09/16/pdzjbq-1.webp) + +![](https://blog.meowrain.cn/api/i/2025/09/16/pe21e7-1.webp) + +我们看看缓存转移的关键源码 +![](https://blog.meowrain.cn/api/i/2025/09/16/pe4gjp-1.webp) +第一步先把B的完整对象放到一级缓存中,然后从三级缓存中移除B的ObjectFactory,再从二级缓存中移除B(当然二级缓存中也没有B),接下来完成B的单例注册。这样缓存转移就完成了。 + +![](https://blog.meowrain.cn/api/i/2025/09/16/pf7oj6-1.webp) +这样就完成了B对象的初始化 +![](https://blog.meowrain.cn/api/i/2025/09/16/pfvlkh-1.webp) + +但是我们B对象的创建流程是在A对象的填充属性流程里,所以会继续A的填充属性流程,这个时候再去一级缓存里找B,就能找到B了,填充B并进行缓存转移,移除二级,三级缓存中的A对象,就可以注入B完成A初始化了 +![](https://blog.meowrain.cn/api/i/2025/09/16/pgzg8n-1.webp) + + + +# 三级缓存的作用 +为了AOP +![](https://blog.meowrain.cn/api/i/2025/09/16/phhvgv-1.webp) + +![](https://blog.meowrain.cn/api/i/2025/09/16/phke37-1.webp) + +![](https://blog.meowrain.cn/api/i/2025/09/16/phmmeq-1.webp) + +当Spring没有循环依赖的情况下,是把普通对象创建好后再生成代理对象,Spring也没有办法提前知道对象之间的依赖关系。也不能把每个对象都创建出代理对象来,所以就需要把对象包装成objectFactory这个类型,通过其中的ObjectFactory对象中的getObject方法获取到生成的代理对象。 + +![](https://blog.meowrain.cn/api/i/2025/09/16/pitrj2-1.webp) \ No newline at end of file diff --git a/src/content/posts/Java/java基础面试题.md b/src/content/posts/Java/java基础面试题.md new file mode 100644 index 0000000..b68f85f --- /dev/null +++ b/src/content/posts/Java/java基础面试题.md @@ -0,0 +1,261 @@ +--- +title: java基础面试题 +published: 2025-07-25 +description: '' +image: '' +tags: [Java,面试题] +category: 'Java > 面试题' +draft: false +lang: '' +--- + +https://javaguide.cn/java + +# Java基本数据类型 + +整数型,浮点型,布尔型,字符型 +整数型: +byte,short,int,long +浮点型: float,double +布尔型: boolean +字符型: char + + +# 基本类型和包装类型的区别 +基本数据类型成员变量(未被static修饰) 存放在Java虚拟机的堆中 + +基本类型不一定被放在Java虚拟机的栈中,这取决于这个基本类型变量在哪个地方,如果它是作为方法中的局部变量,那么它是存放在栈中的,当这个基本类型变量被放在成员变量里面的时候,它才会被放到堆中。 + +当然被static修饰的基本类型一定是存放在Java虚拟机的堆中的。 + +# 包装类型的缓存机制 +Java基本数据类型的包装类大部分都用到了缓存机制来提升性能 +Byte,Short,Integer,Long这4种包装类默认创建了[-128,127]相应类型的缓存数据,Character创建了数值在[0,127]范围的缓存数据,Boolean直接返回TRUE或者FALSE + +对于Integer,可以通过JVM参数 -XX:AutoBoxCacheMax 来设定范围,但是不能修改下限 +实际使用的时候,不建议设置过大的值,防止浪费内存,或者OOM + +# equals方法和==的区别 +== 对于基本类型是判断值是否相等,对于引用类型是判断地址是否相等 +equals方法,因为所有类的顶层父类都是Object类,所以Object类中的equals方法判断的也是两个对象的内存地址是否相同 +因此需要重写equals方法,实现对象和对象之间的内容比较,当然了,重写equals方法的时候也需要重写hashCode方法,来保证在集合中使用的正确性。 + +# 自动装箱和自动拆箱 +什么是自动装箱? +自动装箱是Java在基本数据和包装类型之间的自动转换,在基本类型到包装类型转换时,会调用包装类型的valueOf方法 + + + +什么是自动拆箱? + +```java +package cn.meowrain; + +public class Main{ + public static void main(String[] args) { + Integer i = Integer.valueOf(10); + int j = i.intValue(); + } +} + +``` + + + +# 为什么浮点数运算的时候会有精度丢失的风险? +计算机在表示一个数字时,宽度是有限的,无限循环的小数存储在计算机时,只能被截断,所以就会导致小数精度发生损失的情况 + +# 如何解决浮点数运算的精度丢失问题? +BigDecimal 可以实现对浮点数的运算,不会造成精度丢失。通常情况下,大部分需要浮点数精确运算结果的业务场景(比如涉及到钱的场景)都是通过 BigDecimal 来做的。 + +BigDecimal的equals方法会比较精度还有值是否相等,BigDecimal的compareTo方法会比较值是否相等 + +# Java高精度 +BigDecimal, BigInteger + +# 面向对象和面向过程的区别 + +面边过程编程和面向对象编程是两种常见的编程范式,两者的主要区别在于解决问题的方式不同: + +面向过程编程: 面向过程会把解决问题的过程拆成一个一个方法,通过一个一个方法的执行去解决问题 + +面向对象编程: 会先抽象出对象,然后用对象执行方法的方式解决问题 + +面向对象编程开发的程序一般有下面的优点: +- 易维护: 由于良好的结构和封装性,面向对象程序通常更容易维护 +- 易复用: 通过继承和多态,OOP设计使得代码更具有复用性,方便扩展功能 +- 易扩展: 模块化设计使得系统扩展变得更加容易和灵活。 + + +# 封装继承多态 + +封装是指将对象的属性和(数据)和方法(行为)捆绑在一起,并隐藏对象的内部实现细节,只暴露必要的接口给外部世界。有助于保护数据不被直接访问和修改,从而提高代码的安全性和可维护性。 + +继承是面向对象编程中的另外一个核心概念,允许一个类从另一个类中继承属性和方法,从而实现代码复用和层次结构,子类可以扩展或者修改父类的行为,不需要重新编写代码。 + +多态是指允许不同的对象对同一消息做出不同的响应,即同一方法可以根据发送对象的不同而采用多种不同的行为方式。多态的实现方式有: +- 方法重载: 同一个类中,方法名相同,参数列表不同,返回值类型可以相同也可以不同 +- 方法重写: 子类中,方法名和参数列表与父类相同,返回值类型和异常类型也相同,但是方法体不同 +- 接口实现: 一个类实现了一个接口,那么这个类就可以被视为是这个接口的一个实例,从而可以调用接口中的方法。 + + +# 接口和抽象类的区别 +接口偏向于定义行为规范,是对行为的抽象,强调“能不能做”,“具备什么能力” +抽象类偏向于定义共同的属性和方法,是对类的抽象,强调“是什么”的关系 + +共同点: +接口和多态都不能被实例化,只能被实现或者继承后才能创建具体的对象。 + +# 为什么要有hashCode +当你把对象加入 HashSet 时,HashSet 会先计算对象的 hashCode 值来判断对象加入的位置,同时也会与其他已经加入的对象的 hashCode 值作比较,如果没有相符的 hashCode,HashSet 会假设对象没有重复出现。但是如果发现有相同 hashCode 值的对象,这时会调用 equals() 方法来检查 hashCode 相等的对象是否真的相同。如果两者相同,HashSet 就不会让其加入操作成功。如果不同的话,就会重新散列到其他位置。这样我们就大大减少了 equals 的次数,相应就大大提高了执行速度。 + + +那为什么 JDK 还要同时提供这两个方法呢? +这是因为在一些容器(比如 HashMap、HashSet)中,有了 hashCode() 之后,判断元素是否在对应容器中的效率会更高(参考添加元素进HashSet的过程)!我们在前面也提到了添加元素进HashSet的过程,如果 HashSet 在对比的时候,同样的 hashCode 有多个对象,它会继续使用 equals() 来判断是否真的相同。也就是说 hashCode 帮助我们大大缩小了查找成本。 + + +equals 方法判断两个对象是相等的,那这两个对象的 hashCode 值也要相等。 + + +# String类的不可变性是如何被保证的? +1. String类被final修饰,也就是说String类是不可被继承的,不可被继承意味着没人能通过继承String类来修改String类的行为,从而保证了String类的不可变性。 + +2. 底层的字符数组被final修饰 +```java + private final char value[]; +``` +这意味着这个value的引用是不可变的,不能指向其他数组,但是数组中的字符是可以变的。 +还需要进一步保护 + +3. 没有对外暴露value的引用,可以看到前面用了private修饰,无法被外部类通过数组引用修改数组内容 + +4. String类也没有提供可以修改String内部数组的方法 + +# String、StringBuffer、StringBuilder 的区别? + +String是不可变的,StringBuilder和StringBuffer都继承自AbstractStringBuilder类,在AbstractStringBuilder中也是使用字符数组保存字符串,不过没有使用final和private关键字修饰 + +StringBuffer是线程安全的,里面大量使用了synchronized关键字来保证线程安全,而StringBuilder是线程不安全的。 + + +# String#equals() 和 Object#equals() 有何区别? +因为String是引用类型,String中的equals方法是被重写过的,比较的是String字符串的值是否相等,Object中的equals方法是没有被重写的,比较的是对象的内存地址是否相等。 + +![](https://blog.meowrain.cn/api/i/2025/07/26/w2nt42-1.webp) + +```java + + /** + * Compares this string to the specified object. The result is {@code + * true} if and only if the argument is not {@code null} and is a {@code + * String} object that represents the same sequence of characters as this + * object. + * + * @param anObject + * The object to compare this {@code String} against + * + * @return {@code true} if the given object represents a {@code String} + * equivalent to this string, {@code false} otherwise + * + * @see #compareTo(String) + * @see #equalsIgnoreCase(String) + */ + public boolean equals(Object anObject) { + if (this == anObject) { + return true; + } + if (anObject instanceof String) { + String anotherString = (String)anObject; + int n = value.length; + if (n == anotherString.value.length) { + char v1[] = value; + char v2[] = anotherString.value; + int i = 0; + while (n-- != 0) { + if (v1[i] != v2[i]) + return false; + i++; + } + return true; + } + } + return false; + } +``` + + +# String#intern 方法有什么作用? +String.intern() 是一个 native (本地) 方法,用来处理字符串常量池中的字符串对象引用。它的工作流程可以概括为以下两种情况: +常量池中已有相同内容的字符串对象:如果字符串常量池中已经有一个与调用 intern() 方法的字符串内容相同的 String 对象,intern() 方法会直接返回常量池中该对象的引用。 + +常量池中没有相同内容的字符串对象:如果字符串常量池中还没有一个与调用 intern() 方法的字符串内容相同的对象,intern() 方法会将当前字符串对象的引用添加到字符串常量池中,并返回该引用。 + +```java +package org.example; + +public class Tests { + + public static void main(String[] args) { + // 已经有一个字符串常量 "abc" + String abc = "abc"; + String str = new String("abc"); + System.out.println(str.intern() == str); + System.out.println(abc == str.intern()); + } +} +``` + +![](https://blog.meowrain.cn/api/i/2025/07/26/w8e77e-1.webp) + +可以看到,str.intern() 返回的是字符串常量池中的引用,而不是字符串对象的引用,所以 str.intern() != str。 +而 abc 是字符串常量池中的引用,所以 abc == str.intern()。 + +```java +package org.example; + +public class Tests { + + public static void main(String[] args) { + // 字符串常量池中之前没有"abc",所以 intern() 方法会将其添加到常量池中,并返回这个新创建的字符串对象的引用。 + String str = new String("abc"); + System.out.println(str.intern() == str); + + } +} +``` +这个例子中,str.intern() == str 为 false,因为 str.intern() 返回的是字符串常量池中的引用,而 str 是字符串对象的引用。 + +也就是说new String("abc")的时候,字面量"abc"在编译时就已经确定在常量池中了,运行时的new String()操作是基于已存在的字面量创建新对象,放在堆内存中。 + + +# 异常 +## Exception 和 Error 有什么区别? + +在Java中,所有的异常都有一个共同的祖先java.lang包中的Throwable类,Throwable类有两个重要的子类: +- Exception: 程序本身可以处理的异常,可以通过Catch进行捕获,Exception可以分为Checked Exception和Unchecked Exception。 +- Error: 程序无法处理的异常,Error类的异常是由JVM抛出的 语法上虽然可以捕获,但是一般不建议捕获Error类的异常,因为Error类的异常是由JVM抛出的,程序中无法捕获,也无法处理。 + +## Checked Exception 和 Unchecked Exception 有什么区别? +Checked Exception 即 受检查异常 ,Java 代码在编译过程中,如果受检查异常没有被 catch或者throws 关键字处理的话,就没办法通过编译。 + +Unchecked Exception 即 非受检查异常 ,Java 代码在编译过程中,如果非受检查异常没有被 catch或者throws 关键字处理的话,也可以通过编译,但是在运行时会抛出异常。 +![](https://blog.meowrain.cn/api/i/2025/07/26/x7qqjv-1.webp) + +除了RuntimeException及其子类以外,其他的Exception类及其子类都属于受检查异常 。常见的受检查异常有:IO 相关的异常、ClassNotFoundException、SQLException...。 + +RuntimeException 及以下的异常类都被称为非受检查异常(Unchecked Exception),常见的非受检查异常有:ArrayIndexOutOfBoundsException、NullPointerException、ClassCastException...。 + + + +## Throwable 类常用方法有哪些? +- String getMessage(): 返回异常发生时的详细信息 +- String toString(): 返回异常发生时的简要描述 +- String getLocalizedMessage(): 返回异常对象的本地化信息。使用 Throwable 的子类覆盖这个方法,可以生成本地化信息。如果子类没有覆盖该方法,则该方法返回的信息与 getMessage()返回的结果相同 +- void printStackTrace(): 在控制台上打印 Throwable 对象封装的异常信息 + +> 不要在 finally 语句块中使用 return! 当 try 语句和 finally 语句中都有 return 语句时,try 语句块中的 return 语句会被忽略。 + + +# 什么是反射? +反射是一种在程序运行的时候,动态地获取类的信息并且操作类或者对象的能力。 + diff --git a/src/content/posts/Java/深入理解Java反射与泛型_类型擦除与强制类型转换.md b/src/content/posts/Java/深入理解Java反射与泛型_类型擦除与强制类型转换.md new file mode 100644 index 0000000..2d8c719 --- /dev/null +++ b/src/content/posts/Java/深入理解Java反射与泛型_类型擦除与强制类型转换.md @@ -0,0 +1,74 @@ +--- +title: 深入理解Java反射与泛型_类型擦除与强制类型转换 +published: 2025-07-19 +description: '' +image: '' +tags: [反射, 泛型, 类型擦除, 强制类型转换] +category: 'Java' +draft: false +lang: '' +--- + + +# 深入理解Java反射与泛型:类型擦除与强制类型转换 + +在 Java 编程中,反射(Reflection)和泛型(Generics)是两个强大且常用的特性。反射允许我们在运行时检查和操作类、方法、字段等,而泛型则允许我们编写更加通用和类型安全的代码。然而,Java 的泛型机制与类型擦除(Type Erasure)密切相关,这使得泛型在反射中的应用变得复杂。本文将深入探讨 Java 反射与泛型的结合使用,特别是类型擦除的影响以及如何通过强制类型转换来解决这些问题。 + +## 1. 泛型简介 + +![](https://blog.meowrain.cn/api/i/2025/07/04/10vqzk7-1.webp) + +## 类型擦除 + +### 1. 什么是类型擦除? + +类型擦除(Type Erasure)是 Java 泛型的核心机制。它指的是**在编译阶段,Java 会移除所有泛型类型信息**,即只在源代码层面检查泛型参数的类型,到了运行时,相关类型信息就被“擦除”掉了。 + +### 2. 为什么会有类型擦除? + +Java 为了兼容早期版本(Java 5 之前没有泛型),采用了类型擦除的方式实现泛型,这样泛型代码能够和老代码共存而不冲突。 + +### 3. 类型擦除具体表现 + +- **编译后不保留泛型类型参数信息。** + 示例: + + ```java + List stringList = new ArrayList<>(); + List integerList = new ArrayList<>(); + System.out.println(stringList.getClass() == integerList.getClass()); // true + ``` + + 运行时 `stringList` 和 `integerList` 其实都是 `ArrayList` 类型,不区分里面装的东西。 + +- **泛型类的字节码文件和“裸类型”一致。** + 例如 `List`、`List`、`List` 会被编译成一样的 `List` 类。 + +- **方法中的类型参数会被替换成它的限定类型(如果有),否则直接替换为 Object。** + + ```java + class Box { + T value; + } + // 编译后其实相当于 + class Box { + Object value; + } + ``` + +### 4. 类型擦除带来的影响 + +- **运行时无法通过反射获得泛型参数的具体类型。** 除非通过继承和明确指定泛型参数,否则无法在运行时获得泛型具体类型。 +- **不能直接创建泛型数组。** +- **某些类型强制转换失去编译器检查。** + +### 5. 可以通过什么方式间接获取泛型类型? + +- 通过创建“带泛型参数的子类”并用反射获取 `getGenericSuperclass()`,有时可以拿到实际类型参数。 +- 可以通过一些第三方库(如 Gson、Jackson)的特殊用法间接保存类型信息,但这些都是通过 hack 或特殊设计实现的。 + +--- + +### 总结一句话 + +Java 泛型只在编译阶段保证类型安全,运行阶段所有泛型信息都会被类型擦除,代码在运行时只知道原始类型,不再区分泛型参数。 diff --git a/src/content/posts/Java/集合/ArrayList和LinkedList的区别.md b/src/content/posts/Java/集合/ArrayList和LinkedList的区别.md new file mode 100644 index 0000000..2683512 --- /dev/null +++ b/src/content/posts/Java/集合/ArrayList和LinkedList的区别.md @@ -0,0 +1,35 @@ +--- +title: ArrayList和LinkedList的区别 +published: 2025-08-06 +description: '' +image: '' +tags: ['ArrayList', 'LinkedList'] +category: 'Java > 集合框架' +draft: false +lang: '' +--- + + +# Java ArrayList和LinkedList的区别 + +ArrayList基于动态数组实现,LinkedList基于双向链表实现,这是它们所有性能差异的根本原因 + +ArrayList随机访问是O(1),但是中间插入是O(n),LinkedList则相反,随机访问是O(n),但在已知位置的插入删除是O(1) + +LinkedList由于要存储前后节点的引用,每个元素的内存开销更大,ArrayList更节省内存,但可能因为扩容机制造成一定的浪费。 + +# 实际应用场景 + +在实际项目中,如果需要频繁随机访问元素,会选择ArrayList,如果需要频繁在两端添加删除元素,比如实现队列和栈,我会选择LinkedList + + + +ArrayList和LinkedList都是Java中常见的集合类,它们都实现了List接口。 +底层数据结构不同:ArrayList使用数组实现,通过索引进行快速访问元素。 +LinkedList使用链表实现,通过节点之间的指针进行元素的访问和操作。 +插入和删除操作的效率不同:ArrayList在尾部的插入和删除操作效率较高,但在中间或开头的插入和删除操作效率较低,需要移动元素。 +LinkedList在任意位置的插入和删除操作效率都比较高,因为只需要调整节点之间的指针。随机访问的效率不同:ArrayList支持通过索引进行快速随机访问,时间复杂度为O(1)。 +LinkedList需要从头或尾开始遍历链表,时间复杂度为O(n)。 +空间占用:ArrayList在创建时需要分配一段连续的内存空间,因此会占用较大的空间。LinkedList每个节点只需要存储元素和指针,因此相对较小。 +使用场景:ArrayList适用于频繁随机访问和尾部的插入删除操作,而LinkedList适用于频繁的中间插入删除操作和不需要随机访问的场景。 +线程安全:这两个集合都不是线程安全的,Vector是线程安全的 \ No newline at end of file diff --git a/src/content/posts/Java/集合/HashMap原理.md b/src/content/posts/Java/集合/HashMap原理.md new file mode 100644 index 0000000..0068eba --- /dev/null +++ b/src/content/posts/Java/集合/HashMap原理.md @@ -0,0 +1,610 @@ +--- +title: HashMap原理 +published: 2025-07-18 +description: '' +image: '' +tags: [HashMap,Java] +category: 'Java > 集合框架' +draft: false +lang: '' +--- + + +# 说说HashMap的原理 + +HashMap是基于哈希表的数据结构,用于存储键值对。 +核心是将键的哈希值映射到数组索引位置,通过数组+链表+红黑树来解决哈希冲突。 + +HashMap使用键的hashCode()方法计算哈希值,通过`(n-1) &hash`确定元素在数组中的存储位置。 + +哈希值是经过一定的扰动处理的,防止哈希值分布不均,从而减少冲突, + +HashMap的默认初始容量为16,负载因子为0.75,也就是说,当存储的元素数量超过16 * 0.75 = 12个的时候,HashMap会触发扩容操作,容量x2并重新分配元素位置,这种扩容是比较耗时的操作,频繁扩容会影响性能。 + +# 通过源码深入了解HashMap + +```java + // 默认初始容量 - 必须是 2 的幂次方。 + static final int DEFAULT_INITIAL_CAPACITY = 1 << 4; // 即 16 + + // 最大容量,如果构造函数中通过参数隐式指定了更高的值,则使用此最大容量。 + // 必须是小于等于 1 << 30 的 2 的幂次方。 + // 由于你可以随时指定非常大(甚至超过了1亿)的值,为了防止内存溢出或数组长度无效,HashMap内部通过MAXIMUM_CAPACITY做了一个“保险”,来确保容量不会超过某个安全极限。 + static final int MAXIMUM_CAPACITY = 1 << 30; + + // 构造函数中未指定时使用的负载因子。 + static final float DEFAULT_LOAD_FACTOR = 0.75f; + + // 在向存储单元添加元素时,存储单元使用树结构而不是链表结构的存储单元计数阈值。 + // 当向存储单元添加元素,且该存储单元至少有此数量的节点时,存储单元将转换为树结构。 + // 该值必须大于 2,并且应该至少为 8,以与移除元素时转换回普通存储单元的假设相匹配。 + static final int TREEIFY_THRESHOLD = 8; + + // 在调整大小操作期间将(拆分的)存储单元转换为非树结构存储单元的存储单元计数阈值。 + // 应该小于 TREEIFY_THRESHOLD,并且最多为 6,以与移除元素时的收缩检测相匹配。 + static final int UNTREEIFY_THRESHOLD = 6; + + // 存储单元可以树化的最小表容量。 + // (否则,如果存储单元中有太多节点,表将进行扩容。) + // 应该至少是 4 * TREEIFY_THRESHOLD,以避免扩容和树化阈值之间的冲突。 + static final int MIN_TREEIFY_CAPACITY = 64; + +``` + +![](https://blog.meowrain.cn/api/i/2025/06/13/nm8tvy-0.webp) + +# HashMap的存储结构 + +从源码上看,HashMap的每个存储单元都是一个链表或者红黑树,也就是下面的Node类,那么我们可以用下面的图来展示一个完成初始化的HashMap的存储结构。 + +![](https://blog.meowrain.cn/api/i/2025/06/13/nnlf4v-0.webp) + +### 为什么采用数组? + +因为数组的随机访问速度非常快,HashMap通过哈希函数将键映射到数组索引位置,从而实现快速查找。 + +数组的每一个元素称为一个桶(bucket),对于一个给定的键值对key,value,HashMap会计算出一个哈希值(计算的是key的hash),然后通过`(n-1) & hash`来确定该键值对在数组中的位置。 + +### 如何定位key value该存储在桶数组的哪个位置上?(获取index) + +HashMap通过`(n - 1) & hash`来计算索引位置,其中n是数组的长度,hash是键的哈希值。 + +### 如何计算hash值? + +HashMap使用键的`hashCode()`方法计算哈希值,然后对哈希值进行扰动处理,最后通过`(n-1) & hash`来确定元素在数组中的存储位置。 + +### 为什么要扰动处理? + +扰动处理是为了减少哈希冲突,防止哈希值分布不均。HashMap会对哈希值进行扰动处理,以确保不同的键能够更均匀地分布在数组中,从而减少冲突。 + +在Java 8中,HashMap使用了一个扰动函数来优化hash值的分布: + +```java +static final int hash(Object key) { + int h; + return (key == null) ? 0 : (h = key.hashCode()) ^ (h >>> 16); +} +``` + +这个函数的作用是: + +1. 首先获取key的hashCode()值 +2. 将hashCode的高16位与低16位进行异或运算 + +### 为什么用的是&运算而不是取模运算? + +在java中,我们会让HashMap的容量是2的幂次方,这样可以通过`(n-1) & hash`来快速计算出索引位置,避免了取模运算的性能开销。 + +这里`(n - 1) & hash` == `hash % n`,但&运算比取模运算更高效。 + +n是数组的长度,hash是键的哈希值。 + +### 为什么要让HashMap的容量是2的幂次方? + +因为当容量是2的幂次方时,`(n-1) & hash`可以快速计算出索引位置,而不需要进行取模运算。 + +![](https://blog.meowrain.cn/api/i/2025/06/13/nqocqh-0.webp) + +### 为什么会用到链表? + +我们在HashMap的使用过程中,可能会遇到哈希冲突的情况,也就是不同的键经过哈希函数计算后得到了相同的索引位置,使用链表我们可以把这些冲突的键值对存储在同一个桶中,用链表连接在一起,jdk8开始,链表节点不再使用头插法,而是使用尾插法,这样可以减少链表的长度,提升查找效率。 + +头插法还可能造成链表形成环形,导致死循环。 + +![](https://blog.meowrain.cn/api/i/2025/06/13/nva4ft-0.webp) + +## Node + +```java + static class Node implements Map.Entry { + final int hash; + final K key; + V value; + Node next; + + Node(int hash, K key, V value, Node next) { + this.hash = hash; + this.key = key; + this.value = value; + this.next = next; + } + + public final K getKey() { return key; } + public final V getValue() { return value; } + public final String toString() { return key + "=" + value; } + + public final int hashCode() { + return Objects.hashCode(key) ^ Objects.hashCode(value); + } + + public final V setValue(V newValue) { + V oldValue = value; + value = newValue; + return oldValue; + } + + public final boolean equals(Object o) { + if (o == this) + return true; + + return o instanceof Map.Entry e + && Objects.equals(key, e.getKey()) + && Objects.equals(value, e.getValue()); + } + } + +``` + +# HashMap的Put方法 + +HashMap的put方法是用来添加键值对到HashMap中的核心方法。它的实现逻辑如下: + +```java + /** + * 实现 Map.put 和相关方法。 + * + * @param hash key的哈希值 + * @param key 键 + * @param value 要放入的值 + * @param onlyIfAbsent 如果为 true,则不更改现有值 + * @param evict 如果为 false,则表处于创建模式。 + * @return 返回先前的值,如果没有则返回 null + */ + final V putVal(int hash, K key, V value, boolean onlyIfAbsent, + boolean evict) { // 📌 定义 putVal 方法,用于将键值对放入 HashMap + Node[] tab; Node p; int n, i; // 🏷️ 声明局部变量:tab (哈希表数组), p (当前节点), n (数组长度), i (数组索引) + // 检查哈希表是否为空或长度为0 + if ((tab = table) == null || (n = tab.length) == 0) + // 🏗️ 如果为空,则调用 resize() 方法初始化或扩容哈希表,并获取新的长度 + n = (tab = resize()).length; + // 🎯 计算键在哈希表中的索引位置 i,并检查该位置是否为空 + if ((p = tab[i = (n - 1) & hash]) == null) + // ✨ 如果为空,直接在该位置创建一个新节点 + tab[i] = newNode(hash, key, value, null); + else { // 🤔 如果该位置不为空(发生哈希冲突) + Node e; K k; // 🏷️ 声明局部变量:e (用于找到的已存在节点或新节点), k (临时键) + // 🔑 检查桶中第一个节点的哈希值和键是否与要插入的键值对匹配 + if (p.hash == hash && + ((k = p.key) == key || (key != null && key.equals(k)))) + // ✅ 如果匹配,将 e 指向该节点 p (表示键已存在) + e = p; + // 🌳 检查桶中的节点是否为 TreeNode (红黑树节点) + else if (p instanceof TreeNode) + // 🌲 如果是红黑树,调用 putTreeVal 方法将键值对插入红黑树 + e = ((TreeNode)p).putTreeVal(this, tab, hash, key, value); + else { // 🔗 如果是链表 + // 🔄 遍历链表 + for (int binCount = 0; ; ++binCount) { + // نهاية 检查当前节点的下一个节点是否为空 (到达链表尾部) + if ((e = p.next) == null) { + // ➕ 在链表尾部插入新节点 + p.next = newNode(hash, key, value, null); + // 🌲❓ 检查链表长度是否达到树化阈值 (TREEIFY_THRESHOLD - 1 因为 binCount 从0开始计数,且当前p是尾部的前一个节点) + if (binCount >= TREEIFY_THRESHOLD - 1) // -1 for 1st + // 🌳🔗➡️🌳 如果达到阈值,将链表转换为红黑树 + treeifyBin(tab, hash); + break; // 🛑 跳出循环,因为新节点已插入 + } + // 🔑 检查链表中节点的哈希值和键是否与要插入的键值对匹配 + if (e.hash == hash && + ((k = e.key) == key || (key != null && key.equals(k)))) + break; // ✅ 如果匹配,跳出循环 (表示键已存在,e 指向该节点) + // 👉 将 p 指向下一个节点,继续遍历 + p = e; + } + } + // 🔑❓ 检查 e 是否不为 null (表示键已存在于哈希表中,或者在红黑树中找到了/插入了节点) + if (e != null) { // existing mapping for key + // 💾 获取旧值 + V oldValue = e.value; + // 🔄❓ 根据 onlyIfAbsent 参数决定是否更新值 (如果 onlyIfAbsent 为 false,或者旧值为 null,则更新) + if (!onlyIfAbsent || oldValue == null) + // ⬆️ 更新节点的值 + e.value = value; + // 🔗 回调方法,用于 LinkedHashMap 等子类记录节点访问 + afterNodeAccess(e); + // ↩️ 返回旧值 + return oldValue; + } + } + // 🛠️ 修改计数器加1,用于迭代器快速失败机制 + ++modCount; + // 📈 检查当前元素数量是否超过阈值 (threshold = capacity * loadFactor) + if (++size > threshold) + // 🏗️ 如果超过阈值,调用 resize() 方法扩容哈希表 + resize(); + // 🔗 回调方法,用于 LinkedHashMap 等子类记录节点插入 + afterNodeInsertion(evict); + // ↩️ 如果是新插入的键值对,返回 null + return null; + } +``` + +![](https://blog.meowrain.cn/api/i/2025/06/13/nzkmzk-0.webp) + +# HashMap的Get方法 + +```java + /** + * 实现 Map.get 和相关方法。 + * + * @param key 要查找的键 + * @return 返回找到的节点,如果没有找到则返回 null + */ + final Node getNode(Object key) { // 📌 定义 getNode 方法,用于根据键查找节点 + Node[] tab; Node first, e; int n, hash; K k; // 🏷️ 声明局部变量:tab (哈希表数组), first (桶中第一个节点), e (当前节点), n (数组长度), hash (键的哈希值), k (临时键) + // 🔍 检查哈希表是否不为空且长度大于0,并且根据键的哈希值计算出的桶位置有节点 + if ((tab = table) != null && (n = tab.length) > 0 && + (first = tab[(n - 1) & (hash = hash(key))]) != null) { + // 🎯 首先检查桶中第一个节点的哈希值和键是否与要查找的键匹配 + if (first.hash == hash && // always check first node 总是先检查第一个节点 + ((k = first.key) == key || (key != null && key.equals(k)))) + // ✅ 如果匹配,直接返回第一个节点 + return first; + // 🔗 检查第一个节点是否有下一个节点(链表或红黑树) + if ((e = first.next) != null) { + // 🌳 如果第一个节点是 TreeNode(红黑树节点) + if (first instanceof TreeNode) + // 🌲 在红黑树中查找并返回节点 + return ((TreeNode)first).getTreeNode(hash, key); + // 🔄 如果是链表,遍历链表查找节点 + do { + // 🔑 检查当前节点的哈希值和键是否与要查找的键匹配 + if (e.hash == hash && + ((k = e.key) == key || (key != null && key.equals(k)))) + // ✅ 如果匹配,返回当前节点 + return e; + // 👉 移动到下一个节点,继续遍历直到链表末尾 + } while ((e = e.next) != null); + } + } + // ❌ 如果没有找到匹配的节点,返回 null + return null; + } +``` + +![](https://blog.meowrain.cn/api/i/2025/06/13/o2aa3y-0.webp) + +# HashMap的扩容 + +HashMap的扩容是指当存储的元素数量超过负载因子所允许的最大数量时,HashMap会自动增加其容量。 +扩容的过程包括以下几个步骤: + +1. **计算新的容量**:新的容量通常是当前容量的两倍。 +2. **创建新的数组**:创建一个新的数组来存储扩容后的元素。 +3. **重新计算索引位置**:对于每个元素,重新计算其在新数组中的索引位置,并将其移动到新数组中。 + +源码中是resize()函数 + +```java +/** + * 初始化或将表大小扩大一倍。如果为null,则根据字段threshold中保存的初始容量目标进行分配。 + * 否则,因为我们使用的是2的幂次方扩展,每个桶中的元素必须保持在相同的索引位置, + * 或者在新表中以2的幂次方偏移量移动。 + * + * @return 返回新的哈希表 + */ +final Node[] resize() { // 📏 定义扩容方法 + Node[] oldTab = table; // 🗂️ 保存旧的哈希表引用 + int oldCap = (oldTab == null) ? 0 : oldTab.length; // 📊 获取旧表的容量,如果为null则容量为0 + int oldThr = threshold; // 📋 保存旧的阈值 + int newCap, newThr = 0; // 🆕 声明新容量和新阈值变量 + + // 🔍 如果旧容量大于0(表已初始化) + if (oldCap > 0) { + // ⚠️ 如果旧容量已达到最大值,则不再扩容 + if (oldCap >= MAXIMUM_CAPACITY) { + threshold = Integer.MAX_VALUE; // 🔢 将阈值设为最大整数值 + return oldTab; // ↩️ 直接返回旧表,不扩容 + } + // 🔢 新容量 = 旧容量 * 2,且不超过最大容量,且旧容量 >= 默认初始容量 + else if ((newCap = oldCap << 1) < MAXIMUM_CAPACITY && + oldCap >= DEFAULT_INITIAL_CAPACITY) + newThr = oldThr << 1; // 📈 新阈值 = 旧阈值 * 2 + } + // 🎯 如果旧容量为0但旧阈值大于0(通过构造函数指定了初始容量) + else if (oldThr > 0) + newCap = oldThr; // 🆕 新容量等于旧阈值 + // 🌟 如果旧容量和旧阈值都为0(使用默认值初始化) + else { + newCap = DEFAULT_INITIAL_CAPACITY; // 🔢 新容量设为默认初始容量16 + newThr = (int)(DEFAULT_LOAD_FACTOR * DEFAULT_INITIAL_CAPACITY); // 📊 新阈值 = 0.75 * 16 = 12 + } + + // 🔧 如果新阈值为0,需要重新计算 + if (newThr == 0) { + float ft = (float)newCap * loadFactor; // 📐 计算新阈值 = 新容量 * 负载因子 + // ✅ 确保新阈值不超过最大值 + newThr = (newCap < MAXIMUM_CAPACITY && ft < (float)MAXIMUM_CAPACITY ? + (int)ft : Integer.MAX_VALUE); + } + + threshold = newThr; // 📋 更新阈值 + @SuppressWarnings({"rawtypes","unchecked"}) + Node[] newTab = (Node[])new Node[newCap]; // 🏗️ 创建新的哈希表数组 + table = newTab; // 🔄 将新表赋值给table字段 + + // 📦 如果旧表不为空,需要转移元素 + if (oldTab != null) { + // 🔄 遍历旧表的每个桶 + for (int j = 0; j < oldCap; ++j) { + Node e; // 🏷️ 当前节点 + // 🔍 如果当前桶不为空 + if ((e = oldTab[j]) != null) { + oldTab[j] = null; // 🧹 清空旧桶,帮助GC + + // 🔗 如果桶中只有一个节点(没有链表或红黑树) + if (e.next == null) + newTab[e.hash & (newCap - 1)] = e; // 🎯 直接重新计算位置并放入新表 + + // 🌳 如果是红黑树节点 + else if (e instanceof TreeNode) + ((TreeNode)e).split(this, newTab, j, oldCap); // 🌲 调用红黑树的分割方法 + + // 🔗 如果是链表 + else { + Node loHead = null, loTail = null; // 🔻 低位链表的头和尾节点 + Node hiHead = null, hiTail = null; // 🔺 高位链表的头和尾节点 + Node next; // ➡️ 下一个节点 + + // 🔄 遍历链表中的所有节点 + do { + next = e.next; // 📍 保存下一个节点 + + // 🎲 通过 (e.hash & oldCap) 判断节点应该放在哪个位置 + if ((e.hash & oldCap) == 0) { + // 🔻 放在原位置(低位链表) + if (loTail == null) + loHead = e; // 🎯 如果低位链表为空,设置头节点 + else + loTail.next = e; // 🔗 连接到低位链表尾部 + loTail = e; // 📍 更新尾节点 + } + else { + // 🔺 放在原位置+oldCap的位置(高位链表) + if (hiTail == null) + hiHead = e; // 🎯 如果高位链表为空,设置头节点 + else + hiTail.next = e; // 🔗 连接到高位链表尾部 + hiTail = e; // 📍 更新尾节点 + } + } while ((e = next) != null); // 🔄 继续遍历直到链表末尾 + + // 🔻 如果低位链表不为空,放入原位置 + if (loTail != null) { + loTail.next = null; // ✂️ 断开链表尾部 + newTab[j] = loHead; // 📍 放入新表的原位置 + } + + // 🔺 如果高位链表不为空,放入新位置 + if (hiTail != null) { + hiTail.next = null; // ✂️ 断开链表尾部 + newTab[j + oldCap] = hiHead; // 📍 放入新表的 j + oldCap 位置 + } + } + } + } + } + return newTab; // ↩️ 返回新的哈希表 +} +``` + +## 扩容的时候高位和低位链表详解 + +```java +else { + Node loHead = null, loTail = null; // 🔻 低位链表的头和尾节点 + Node hiHead = null, hiTail = null; // 🔺 高位链表的头和尾节点 + Node next; // ➡️ 下一个节点 + + // 🔄 遍历链表中的所有节点 + do { + next = e.next; // 📍 保存下一个节点 + + // 🎲 通过 (e.hash & oldCap) 判断节点应该放在哪个位置 + if ((e.hash & oldCap) == 0) { + // 🔻 放在原位置(低位链表) + if (loTail == null) + loHead = e; // 🎯 如果低位链表为空,设置头节点 + else + loTail.next = e; // 🔗 连接到低位链表尾部 + loTail = e; // 📍 更新尾节点 + } + else { + // 🔺 放在原位置+oldCap的位置(高位链表) + if (hiTail == null) + hiHead = e; // 🎯 如果高位链表为空,设置头节点 + else + hiTail.next = e; // 🔗 连接到高位链表尾部 + hiTail = e; // 📍 更新尾节点 + } + } while ((e = next) != null); // 🔄 继续遍历直到链表末尾 + + // 🔻 如果低位链表不为空,放入原位置 + if (loTail != null) { + loTail.next = null; // ✂️ 断开链表尾部 + newTab[j] = loHead; // 📍 放入新表的原位置 + } + + // 🔺 如果高位链表不为空,放入新位置 + if (hiTail != null) { + hiTail.next = null; // ✂️ 断开链表尾部 + newTab[j + oldCap] = hiHead; // 📍 放入新表的 j + oldCap 位置 + } + } +``` + +### 核心原理 + +当HashMap从容量n扩容到2n时,每个元素的新位置只有两种可能: + +- **保持原位置**(低位链表) +- **移动到原位置+n**(高位链表) + +判断依据: `(e.hash & oldCap) == 0`,如果为0,则放在原位置,否则放在原位置+n。 n是旧容量。 + +- 低位链表(lo list):满足 `(e.hash & oldCap) == 0` 的节点,扩容后**继续放在原位置** `j`。 +- 高位链表(hi list):满足 `(e.hash & oldCap) != 0` 的节点,扩容后放在新位置 `j + oldCap`。 + +#### 举例子 + +假设oldCap = 16,newCap = 32 + +oldCap=16 // 10000 + +newCap=32 // 100000 + +``` +hash1 = 5; // 000101 + +扩容前(cap = 16)计算index + +index1 = hash1 & (oldCap - 1) ==> 5 & 15 + 000101 +&000111 +`--------` + 000101 ==> 5 + +扩容后(cap = 32)计算index +我们要看要不要移动这个kv到桶中的新位置 +判断 (hash & oldCap) == 0 + +hash1 & oldCap ==> 5 & 16 + 00000101 +& 00010000 +`-----------` + 00000000 ==> 0 +所以这个kv会放在原位置5 +``` + +再举个例子 + +``` +hash2 = 20; // 10100 + +扩容前(cap = 16)计算index + +index2 = hash2 & (oldCap - 1) ==> 20 & 15 + 10100 +& 01111 +-------------- + 00100 ==> 4 + +扩容后(cap = 32)计算index +要先看hash & oldCap == 0 ? + +hash2 & oldCap ==> 20 & 16 + 00010100 +& 00010000 +-------------- + 00010000 ==> 16 + +不为0,所以这个20会放在新的位置 原来的位置+ 旧桶数组容量 = 4 + 16 = 20 + +``` + +你的理解非常正确!🎉👍 + +你已经掌握了HashMap扩容时分桶位置变更的本质原理,让我们用你的描述稍作归纳和梳理,验证你的思路: + +--- + +### 为什么判断的是与oldCap相与得到的值是1还是0来决定搬迁位置? + +当HashMap扩容时,容量从 `oldCap` 扩展到 `newCap`,比如从 16 扩展到 32。 + +- 原来 HashMap 的下标计算是:`index = hash & (oldCap-1)`,比如 `00001111`(低4位)。 +- 扩容后,计算下标变为:`index = hash & (newCap-1)`,比如 `00011111`(低5位),也就是多了一位。 +- 和 `oldCap`(如 `00010000`)相与,就相当于“掐头去尾”地只关注扩容新增的那一位: + - 如果 `(hash & oldCap) == 0`,说明这位是0,**扩容后的位置等于原index** + - 如果 `(hash & oldCap) != 0`,说明这位是1,**扩容后的位置等于原index + oldCap** +- 这种判断,让你高效知道节点该不该搬迁以及搬去哪里,无需重新完全计算index。 + +--- + +#### 举例验证(巩固印象) + +假如: + +- oldCap = 16 ⇒ 00010000 +- oldCap-1 = 15 ⇒ 00001111 +- newCap = 32 ⇒ 00100000 +- newCap-1 = 31 ⇒ 00011111 +- hash = 21 ⇒ 10101 + +**扩容前下标:** + +```java +index = 10101 & 01111 = 00101 = 5 +``` + +**扩容后下标:** + +```java +index = 10101 & 11111 = 10101 = 21 +``` + +**oldCap这一位的判断:** + +```java +10101 & 10000 = 10000 ≠ 0 +``` + +说明这位是1,扩容后下标变成原index+16=21。 + +--- + +### 扩容的条件是什么? + +当 HashMap 中存储的元素数量超过了「阈值」(threshold)时,就会进行扩容。 +这个「阈值」的计算公式是: + +``` +threshold = capacity * loadFactor +``` + +loadFactor 是负载因子,默认值为 0.75。 + +### 为什么要进行搬迁呢? + +HashMap扩容的主要目的是: +减少哈希冲突,提高查找、插入效率。 +让更多桶可用,降低碰撞链表队列的长度。 + +# jdk1.7和jdk1.8中hashmap的区别 + +![](https://blog.meowrain.cn/api/i/2025/06/13/pf960q-0.webp) + +![](https://blog.meowrain.cn/api/i/2025/06/13/pfb9eb-0.webp) + +![](https://blog.meowrain.cn/api/i/2025/06/13/pfedpt-0.webp) + +![](https://blog.meowrain.cn/api/i/2025/06/13/pfqbnm-0.webp) + +![](https://blog.meowrain.cn/api/i/2025/06/13/pfyw7c-0.webp) + +# 链表什么时候转红黑树? + +桶数组中某个桶的链表长度>=8 而且桶数组长度> 64的时候,hashmap会转换为红黑树 + +![](https://blog.meowrain.cn/api/i/2025/06/25/hi2z5e-0.webp) diff --git a/src/content/posts/Java/集合/HashMap和ConcurrentHashMap的区别.md b/src/content/posts/Java/集合/HashMap和ConcurrentHashMap的区别.md new file mode 100644 index 0000000..c5fed56 --- /dev/null +++ b/src/content/posts/Java/集合/HashMap和ConcurrentHashMap的区别.md @@ -0,0 +1,25 @@ +--- +title: HashMap和ConcurrentHashMap的区别 +published: 2025-08-11 +description: '' +image: '' +tags: [ConcurrentHashMap,Java] +category: 'Java > 集合框架' +draft: false +lang: '' +--- + +# JDK1.7版本 +- 内存结构: HashMap采用数组+链表的结构,数组是HashMap的主体,链表用于解决哈希冲突。当两个不同的键通过哈希函数计算得到相同的索引时,它们会被存储在同一个数组位置的链表中。ConcurrentHashMap在JDK1.7中采用了分段锁的机制,内部是一个Segment数组,每个Segment类似一个小的HashMap,有自己的数组和链表。 + +- 线程安全性: HashMap不是线程安全的,在多线程环境下,如果多个线程同时对HashMap进行读写操作,可能会导致数据不一致,死循环的问题。ConcurrentHashMap是线程安全的,它通过分段锁的机制来保证并发访问时的线程安全。只有当多个线程访问同一个Segment时,才会发生锁竞争,从而提高了并发性能。 + +- 性能: hashmap由于没有锁的开销,所以在单线程环境下性能较好,但是在多线程环境下,为了保证线程安全,需要额外的同步机制,这回降低性能。但是ConcurrentHashMap通过分段所机制,在多线程环境下可以实现更高的并发性能,不同的线程可以同时访问不同的Segment,从而减少了锁竞争的可能性。 + + +# JDK1.8版本 +内存结构: hashMap引入了红黑树,从Jdk1.8开始,hashmap采用数组+ 链表+ 红黑树的结构。当链表长度超过一定阈值(8)的时候,链表会转换为红黑树,小于6的时候会转换为链表,以提高查找效率。ConcurrentHashMap放弃了分段锁机制,采用`CAS + synchronized`的方式保证线程安全,内部结构和HashMap一样,也引入了红黑树,是数组+ 链表+ 红黑树的结构。 + +线程安全性: ConcurrentHashMap通过CAS和synchronized的方式保证线程安全。在插入元素的时候,首先会尝试用CAS更新节点,如果CAS失败,则使用synchronized锁住当前节点,再进行插入操作。 + +性能: hashmap在单线程环境下,由于红黑树的引入,当链表较长的时候查找效率会有所提升。ConcurrentHashMap在多线程环境下,由于摒弃了分段锁,减少了锁的粒度,进一步提高了并发性能。同时,红黑树的引入也提高了查找效率。 \ No newline at end of file diff --git a/src/content/posts/Java/集合/JavaHashMap为什么在jdk8引入红黑树.md b/src/content/posts/Java/集合/JavaHashMap为什么在jdk8引入红黑树.md new file mode 100644 index 0000000..6d26658 --- /dev/null +++ b/src/content/posts/Java/集合/JavaHashMap为什么在jdk8引入红黑树.md @@ -0,0 +1,40 @@ +--- +title: Java HashMap为什么在jdk8引入红黑树 +published: 2025-08-05 +description: '' +image: '' +tags: ['红黑树','HashMap'] +category: 'Java > 集合框架' +draft: false +lang: '' +--- + + +# Java HashMap为什么在jdk8引入红黑树 + +在JDK8之前,HashMap的内部实现主要依赖于数组+链表的结构 +当多个元素的哈希值相同的时候(也就是发生哈希冲突的时候),这些元素会被存储在同一个桶里面,形成一个链表。 +但这种实现方式在特定情况下会导致性能问题。 + +# JDK8之前的问题 + +1. 时间复杂度退化: 在最坏情况下(大量元素哈希到同一个桶),查找,插入和删除操作的时间复杂度会从理想的O(1)退化为O(n),其中n是链表的长度 +2. 哈希冲突攻击: 恶意攻击者可以构造大量哈希冲突的数据,使得HashMap的性能急剧下降,导致潜在的拒接服务攻击。 + +# 红黑树的引入 +JDK8对HashMap进行了优化,引入了红黑树来解决上面的问题: +- 性能提升: 当一个桶中的元素数量超过一定阈值的时候,链表会被转换成红黑树。红黑树是一种自平衡的二叉搜索树,即使在最坏的情况下,它查找,插入和删除操作的时间复杂度也能保持在O(logn),大大提高了性能。 + +- 阈值机制: + - 当桶中元素超过8个的时候,链表转换为红黑树 + - 当桶中元素少于6个的时候,红黑树会退化回链表 + +- 安全性增强: 通过引入红黑树,即使面对哈希冲突的攻击,HashMap也能保持相对稳定的性能,提高系统安全性 + + +# 为什么选择红黑树 +平衡性: 红黑树是一种近似平衡的二叉搜索树,能保证最坏情况下的O(logn)的性能。 + +内存占用: 相比AVL树等其它平衡树,红黑树的平衡条件较为宽松,旋转操作更少,内存占用更小。 + +实现更好的复杂度与性能平衡 diff --git a/src/content/posts/Java/集合/Java迭代器Iterator和Iterable.md b/src/content/posts/Java/集合/Java迭代器Iterator和Iterable.md new file mode 100644 index 0000000..4e6c5a5 --- /dev/null +++ b/src/content/posts/Java/集合/Java迭代器Iterator和Iterable.md @@ -0,0 +1,14 @@ +--- +title: Java迭代器Iterator和Iterable +published: 2025-08-05 +description: '' +image: '' +tags: [Iterator,Iterable] +category: 'Java > 集合框架' +draft: false +lang: '' +--- + +# Java迭代器Iterator和Iterable + + diff --git a/src/content/posts/Java/集合/concurrenthashmap的实现原理.md b/src/content/posts/Java/集合/concurrenthashmap的实现原理.md new file mode 100644 index 0000000..2dd326c --- /dev/null +++ b/src/content/posts/Java/集合/concurrenthashmap的实现原理.md @@ -0,0 +1,69 @@ +--- +title: concurrenthashmap的实现原理 +published: 2025-08-06 +description: '' +image: '' +tags: [ConcurrentHashMap,Java] +category: 'Java > 集合框架' +draft: false +lang: '' +--- + + +# ConcurrentHashMap实现原理 + +ConcurrentHashMap是Java并发包中一种线程安全的哈希表实现。 +HashMap在多线程环境下扩容会出现CPU接近100%的情况,因为HashMap并不是线程安全的,我们可以通过Collections里面的Map synchronizedMap(Map m) 把HashMap包装成一个线程安全的map + +比如SynchronizedMap的put方法就是加锁过的 + + +# ConcurrentHashMap的变化 +ConcurrentHashMap在JDK1.7中,提供了一种粒度更细的加锁机制,这种机制叫分段锁,整个哈希表被分为多个段,每个段都独立锁定。读取操作不需要锁,写入操作仅锁定相关的段,这减小了锁冲突的几率,提高了并发性能。 + +这种机制的优点是: 在并发环境下将实现更高的吞吐量,在单线程环境下只损失非常小的性能。 + +可以这样理解分段锁,就是将数据分段,对每一段数据分配一把锁,当一个线程占用锁访问其中一个段数据的时候,其他段的数据也能被其他线程访问。 + +有些方法需要跨段,比如size(),isEmpty(),containsValue(),它们可能需要锁定整个表而不仅仅是某个段,这需要按顺序锁定所有段,操作完以后,再按顺序释放所有段的锁。 + + +ConcurrentHashMap是由Segment数组结构和HashEntry构成的,Segment是一种可重入的锁,HashEntry则用于存储键值对数据。 + +一个ConcurrentHashMap里面包含一个Segment数组,Segment的结构和HashMap类似,是一种数组和链表结构,一个Segment里包含一个HashEntry数组,每个HashEntry是一个链表结构的元素,每个Segment守护着一个HashEntry数组里的元素,当HashEntry数组的数据进行修改的时候,必须首先获得它对应的Segment锁。 + +在外部:有一个 Segment 数组,作为并发控制的“总入口”,每个 Segment 都是一个独立的锁喵~ +在内部:每个 Segment 自己就是一个完整的小型 HashMap!它有自己的哈希表数组,里面的每个桶都可以通过 next 指针挂着一个或多个 Entry 组成的链表. + + + +# ConcurrentHashMap 读写过程 + +## get方法 +- 为输入的key做hash运算,得到hash值 +- 通过Hash值,定位到对应的Segment对象 +- 再次通过hash值,定位到Segment当中数组的具体位置 + + +## put方法 +- 为输入的key做hash运算,得到hash值 +- 通过hash值,定位到对应的Segment对象 +- 获取可重入锁 +- 再次通过hash值,定位到Segment当中数组的具体位置 +- 插入或者覆盖HashEntry对象 +- 释放锁 + + +# JDK1.8 +在JDK1.8中,ConcurrentHashMap主要做了两个优化: +- 和HashMap一样,链表也会在长度到达8的时候转换为红黑树,这样可以提升大量冲突的时候的查询效率。 +- 以某个位置的头结点为锁,配合自旋 + CAS 避免不必要的锁开销,进一步提升并发性能。 +- 相比JDK1.7中的ConcurrentHashMap,JDK1.8的ConcurrentHashMap取消了Segment分段锁,采用CAS + synchronized来保证并发安全性。整个容器只分为一个Segment,也就是table数组。 +- JDK1.8中的ConcurrentHashMap对节点Node类中的共享变量,和JDK1.7一样,使用volatile关键字,保证多线程操作的时候,变量的可见性。 + + +# ConcurrentHashMap的字段 +1. table +这个装载Node的数组,作为ConcurrentHashMap的底层容器,采用加载的方式,直到第一次插入数据的时候才会进行初始化操作 +数组的大小是2的幂次方。 + diff --git a/src/content/posts/中间件/MySQL/MVCC.md b/src/content/posts/中间件/MySQL/MVCC.md new file mode 100644 index 0000000..fc51e53 --- /dev/null +++ b/src/content/posts/中间件/MySQL/MVCC.md @@ -0,0 +1,105 @@ +--- +title: MVCC-多版本并发控制 +published: 2025-08-09 +description: '' +image: '' +tags: [MVCC] +category: '中间件 > MySQL' +draft: false +lang: '' +--- + + +# MVCC +MVCC,也就是多版本并发控制 + +它的目的是: 提高数据库并发性能,用更好的方式处理读写冲突,也就是即使有读写冲突的时候,也能做到不加锁。 + + +## 并发控制的挑战 +在数据库系统中,同时执行的事务可能涉及相同的数据,因此需要一种机制来保证数据的一致性,传统的锁机制可以实现并发控制,但会导致阻塞和死锁等问题。 + +## 传统锁机制 + + +## 当前读和快照读 + +### 当前读 +在MySQL中,当前读是一种读取数据的操作方式,它可以直接读取最新的数据版本,读取时还要保证其他并发事务不能修改当前记录,会对读取的记录进行加锁,MySQL提供了两种实现当前读的机制: + +- 锁定读: + - 锁定读是一种特殊情况下的当前读方式,在某些场景下使用 + - 在使用锁定读的时候,MySQL会在执行读取操作前获取共享锁或者排他锁,确保数据一致性。 + - 共享锁允许多个事务读取统一数据,而排他锁组织其他事务读取或者写入该数据。 + - 锁定读适用于需要严格控制并发访问的场景,但是由于加锁带来的性能开销较大,所以只在必要的时候才使用。 + +![](https://blog.meowrain.cn/api/i/2025/08/09/lsvw6z-1.webp) + + +![](https://blog.meowrain.cn/api/i/2025/08/09/ltng7m-1.webp) + +这种就属于悲观锁实现。 + +### 快照读 +快照读就是在读取数据的时候,读取一个一致性视图中的数据,MySQL通过MVCC机制来支持快照读。 + +具体而言,每个食物在开始的时候都会创建一个一致性视图,这个一致性视图会记录当前事务开始时已经提交的数据版本。 + +执行查询的时候,MySQL会根据事务的一致性视图来决定可见的数据版本。只有那些在事务开始之前就已经提交的数据版本才是可见的,未提交或在事务开始后修改的数据则对当前事务不可见。 + +像不加锁的select操作就是快照读,也就是不加锁的非阻塞读。 + + +- 一致性读: + - 默认隔离级别下(可重复读),MySQL使用一致性来实现当前读 + - 在事务开始的时候,MySQL会创建一个一致性视图,这个视图反映了事务开始时刻的数据库快照。 + - 在事务执行期间,无论其他事务对数据进行了何种修改,事务始终使用一致性视图来读取数据。 + - 可以保证在同一事务内多次查询返回的结果是一致的. + + +![](https://blog.meowrain.cn/api/i/2025/08/09/m7rmhb-1.webp) + + +快照读的前提是隔离级别不是串行级别,在串行级别下,事务之间完全串行执行,快照读会退化为当前读中的加锁读。 + +MVCC主要就是为了实现读-写冲突不加锁,这个读就是指的快照读,是乐观锁的实现。 + + +# 事务的mvcc机制原理是什么? +MVCC允许多个事务同时读取同一行数据,而不会彼此阻塞,每个事务看到的数据版本是该事务开始时候的数据版本,这意味着,如果其他事务在此期间修改了数据,正在运行的事务仍然看到的是它开始时候的数据状态,从而实现了非阻塞读操作。 + + +对于 `读已提交` 和 `可重复读` 隔离级别的事务来说,它们是通过ReadView来实现的,它们的区别在于创建ReadView的时机不同。 +ReadView可以理解为当时的一个快照视图,它记录了在创建时刻可见的数据版本。 + + +读提交隔离级别: 在每个select语句执行前,都会重新生成一个ReadView。每个SELECT生成新的ReadView +只能读到其他事务已提交的版本 +不能读到未提交事务的修改 +这保证了不会出现"脏读" +但会出现"不可重复读" + +可重复读隔离级别: 在事务中,执行第一条select语句的时候,生成一个ReadView,然后整个事务期间都在使用这个ReadView + +ReadView有四个重要字段: +- creator_trx_id 创建该Read View的事务的事务id +- m_ids 创建ReadView的时候,当前数据库中活跃且未提交的事务id列表,所谓活跃事务,指的就是启动了但是还没提交的事务 +- min_trx_id 创建ReadView的时候当前数据库中活跃且未提交的事务中最小的事务的事务id +- max_trx_id 创建ReadView的时候,当前数据库中应该给下一个事务的id值,也就是全局事务中最大的事务id + 1 + +对于使用InnoDB存储引擎的数据库表,它的聚簇索引记录中都包含下面两个隐藏列 +- trx_id 记录最后修改该行数据的事务的事务id +- roll_pointer 记录该行数据的回滚指针,用于实现MVCC(也就是undo日志) + +每次对某条聚簇索引记录进行改动的时候,都会把旧版本的记录写入到undo日志中,然后这个隐藏列是个指针,指向每个旧版本记录,于是就可以通过它找到修改前的记录。 + +![](https://blog.meowrain.cn/api/i/2025/08/09/plbf8e-1.webp) + +一个事务去访问记录的时候,除了自己的更新记录总是可见之外,还有这几种情况: + +如果记录的 trx_id 值小于 Read View 中的 min_trx_id 值,表示这个版本的记录是在创建 Read View 前已经提交的事务生成的,所以该版本的记录对当前事务可见。 +如果记录的 trx_id 值大于等于 Read View 中的 max_trx_id 值,表示这个版本的记录是在创建 Read View 后才启动的事务生成的,所以该版本的记录对当前事务不可见。 +如果记录的 trx_id 值在 Read View 的 min_trx_id 和 max_trx_id 之间,需要判断 trx_id 是否在 m_ids 列表中: +如果记录的 trx_id 在 m_ids 列表中,表示生成该版本记录的活跃事务依然活跃着(还没提交事务),所以该版本的记录对当前事务不可见。 +如果记录的 trx_id 不在 m_ids列表中,表示生成该版本记录的活跃事务已经被提交,所以该版本的记录对当前事务可见。 +这种通过「版本链」来控制并发事务访问同一个记录时的行为就叫 MVCC(多版本并发控制)。 \ No newline at end of file diff --git a/src/content/posts/中间件/MySQL/MySQL-VARCHAR支持的最大长度.md b/src/content/posts/中间件/MySQL/MySQL-VARCHAR支持的最大长度.md new file mode 100644 index 0000000..c1c32cf --- /dev/null +++ b/src/content/posts/中间件/MySQL/MySQL-VARCHAR支持的最大长度.md @@ -0,0 +1,59 @@ +--- +title: MySQL-VARCHAR支持的最大长度 +published: 2025-09-07 +description: '' +image: '' +tags: ['MySQL', 'VARCHAR'] +category: '中间件 > MySQL' +draft: false +lang: '' +--- + +MySQL中,最大行长度限制为65535字节,如果一行中仅仅有一个varchar字段,它的最大长度是多少呢? +(InnoDB/MyISAM 中一行最大长度限制是 65535 字节(65 KB 左右)。) +长度> 255,存储varchar长度需要2字节,长度<255,存储varchar长度需要1字节。 + +所以 +- 当长度>255且非空的时候,可以存储65535 - 2 = 65533 字节。 +- 当长度>255且可以为空的时候,可以存储65535 - 2 - 1(存储NULL标志) = 65532字节。 +- 当长度<255且非空的时候,可以存储65535 - 1 = 65534 字节。 +- 当长度<255且可以为空的时候,可以存储65535 - 1 - 1(存储NULL标志) = 65533字节。 + +如果只有一个 VARCHAR 字段 + +假设表里只有这一列: + +```sql +CREATE TABLE t ( + v VARCHAR(N) +) ENGINE=InnoDB; +``` + +行最大长度:65535 字节。 + +除了数据外,还有: + +NULL 标志位(至少 1 字节,即使只有一列)。 + +VARCHAR 长度字节(1 或 2)。 + +所以最大能用来存储 v 的 = 65535 - 1 (NULL 标志) - 2 (长度字节) = 65532 字节。 + +因此: + +✅ 单列 VARCHAR 最大可定义为 VARCHAR(65532) + + +> 如果是非null,就不需要占用那一字节null标志位了 +```sql +CREATE TABLE t ( + v VARCHAR(N) NOT NULL +) ENGINE=InnoDB; +``` + +这里N就可以是65533了 + + +![](https://blog.meowrain.cn/api/i/2025/09/07/xz52lm-1.webp) + +![](https://blog.meowrain.cn/api/i/2025/09/07/xy0628-1.webp) \ No newline at end of file diff --git a/src/content/posts/中间件/MySQL/MySQL-binlog.md b/src/content/posts/中间件/MySQL/MySQL-binlog.md new file mode 100644 index 0000000..169ed1b --- /dev/null +++ b/src/content/posts/中间件/MySQL/MySQL-binlog.md @@ -0,0 +1,39 @@ +--- +title: MySQL-binlog +published: 2025-09-16 +description: '' +image: '' +tags: [MYSQL,binlog,MySQL] +category: '中间件 > MySQL' +draft: false +lang: '' +--- + +![](https://blog.meowrain.cn/api/i/2025/09/16/11ci8hc-1.webp) +# binlog(二进制日志) +二进制日志主要用于记录所有针对数据库表结构的变更 +以及对表数据的修改操作,不包括SELECT,SHOW等读取类的操作。 +Binlog是在事务提交成功以后,在服务层生成的日志文件 + +作用: +1. 数据恢复: 通过详尽的记录所有影响数据状态的SQL命令,binlog为从特定时间点或者由于意外操作导致的数据丢失提供了恢复手段。一旦发生数据损坏或者丢失事件,可以通过重放binlog中的历史更改来恢复到先前的状态。 +2. 主从复制: 对于需要跨多台服务器实现数据备份的应用场景,binlog提供了基础。通过将主服务器的binlog传输到从服务器,从服务器可以重放这些日志以实现数据的同步。 + +# binlog格式类型 +MySQL支持三种类型的binlog格式: +`STATEMENT`,`ROW`和`MIXED ` + +- STATEMENT模式: 在这个模式下,每一条引起数据变化的SQL语句都会被记录下来。这种方式的优点在于减少了日志大小并且提高了处理速度。 +然而,如果使用了SYSDATE(),NOW()之类的非确定性函数,就有可能导致在执行数据恢复或主从复制过程中产生一致性问题。 + +- ROW模式: 与记录整个SQL不同,ROW模式仅追踪实际受到影响的数据行的变化情况。这种方法避免了STATEMENT模式下的动态内容带来的挑战,但是代价是增加了日志文件的体积 + +- MIXED模式: 前两者的折中方案。根据具体情况自动选择最合适的记录方式。当系统认为STATEMENT更优的时候,使用STATEMENT模式;当系统认为ROW更优的时候,使用ROW模式。 + + +# 记录方式 +![](https://blog.meowrain.cn/api/i/2025/09/16/11buhw1-1.webp) + +# 主从复制 + +![](https://blog.meowrain.cn/api/i/2025/09/16/11c1t5t-1.webp) diff --git a/src/content/posts/中间件/MySQL/MySQL-redolog.md b/src/content/posts/中间件/MySQL/MySQL-redolog.md new file mode 100644 index 0000000..1837b83 --- /dev/null +++ b/src/content/posts/中间件/MySQL/MySQL-redolog.md @@ -0,0 +1,39 @@ +--- +title: MySQL-redolog +published: 2025-09-16 +description: '' +image: '' +tags: [MYSQL,redolog,MySQL] +category: '中间件 > MySQL' +draft: false +lang: '' +--- +![](https://blog.meowrain.cn/api/i/2025/09/16/11ci8hc-1.webp) +# Redo Log(实现持久化) +在InnoDB存储引擎中,大部分Redo Log记录的是物理日志,也就是对特定数据页进行的具体修改。 +那么为啥要称呼它为大部分是物理日志呢?是因为Redo Log系统由两部分构成: +- 一是位于内存中的重做日志缓冲区(redolog buffer),这部分信息容易因为断电等原因丢失。 +- 二是保存于磁盘上的重做日志文件(redolog file)提供持久化存储 + +## 引入redo log的必要性 +尽管buffer pool确实极大提升了数据库操作的性能,但是由于它基于内存的特点,存在着固有的不稳定性,一旦发生系统崩溃或断电等故障,内存中的数据就可能会丢失。为了避免这种情况,Redo Log应运而生。 +通过与buffer pool和change buffer协同工作,redolog负责记录所有尚未同步到磁盘的更改操作,确保即使发生故障重启以后也能恢复这些更新,直到相关页面被最终安全地写入到磁盘为止。 + +![](https://blog.meowrain.cn/api/i/2025/09/16/10r7qnc-1.webp) + +## redo log和undo log之间的差异 + +- Redo Log专注于 记录事务完成后的新状态,也就是变更后的值 +- Undo Log用来追踪事务开始前的原始状态,保存的是变更前的旧值 + +![](https://blog.meowrain.cn/api/i/2025/09/16/10uklpi-1.webp) + +![](https://blog.meowrain.cn/api/i/2025/09/16/10wvrf2-1.webp) + +![](https://blog.meowrain.cn/api/i/2025/09/16/10xmq2l-1.webp) + +![](https://blog.meowrain.cn/api/i/2025/09/16/10xp98x-1.webp) + + +## 崩溃恢复 +![](https://blog.meowrain.cn/api/i/2025/09/16/10xs9rk-1.webp) \ No newline at end of file diff --git a/src/content/posts/中间件/MySQL/MySQL-relaylog.md b/src/content/posts/中间件/MySQL/MySQL-relaylog.md new file mode 100644 index 0000000..f40ce54 --- /dev/null +++ b/src/content/posts/中间件/MySQL/MySQL-relaylog.md @@ -0,0 +1,21 @@ +--- +title: MySQL-relaylog(中继日志) +published: 2025-09-16 +description: '' +image: '' +tags: [MYSQL,relaylog,MySQL] +category: '中间件 > MySQL' +draft: false +lang: '' +--- +# Relay Log(中继日志) + +中继日志(relay log)只在主从服务器架构的从服务器上存在。从服务器(slave)为了与主服务器(Master)保持一致,要从主服务器读取二进制日志的内容,并且把读取到的信息写入本地的日志文件中,这个从服务器本地的日志文件就叫中继日志。然后,从服务器读取中继日志,并根据中继日志的内容对从服务器的数据进行更新,完成主从服务器的数据同步。 + +搭建好主从服务器之后,中继日志默认会保存在从服务器的数据目录下。 + +文件名的格式是:从服务器名 - relay-bin.序号。中继日志还有一个索引文件:从服务器名 - relay-bin.index,用来定位当前正在使用的中继日志。 + +![](https://blog.meowrain.cn/api/i/2025/09/16/11ci8hc-1.webp) + +![](https://blog.meowrain.cn/api/i/2025/09/16/11d3ogv-1.webp) \ No newline at end of file diff --git a/src/content/posts/中间件/MySQL/MySQL-undolog.md b/src/content/posts/中间件/MySQL/MySQL-undolog.md new file mode 100644 index 0000000..fb18932 --- /dev/null +++ b/src/content/posts/中间件/MySQL/MySQL-undolog.md @@ -0,0 +1,35 @@ +--- +title: MySQL-undolog(回滚日志) +published: 2025-09-16 +description: '' +image: '' +tags: [MYSQL,undolog,MySQL] +category: '中间件 > MySQL' +draft: false +lang: '' +--- +![](https://blog.meowrain.cn/api/i/2025/09/16/11ci8hc-1.webp) +# 回滚日志 Undo Log + +回滚日志是数据库引擎层生成的一种日志,主要用于确保事务的ACID特性中的`原子性`。它记录的是逻辑操作,也就是**数据在被修改之前的状态。** +这些逻辑操作包括 插入,删除和更新 + +## 主要功能 +1. 事务回滚: 当事务需要回滚的时候,通过执行undo log记录的逆向操作来恢复到事务开始前的数据状态 +2. 多版本并发控制 MVCC: 结合ReadView机制,利用undo log实现多版本并发控制,从而支持高并发读写操作 + +## 记录内容 +![](https://blog.meowrain.cn/api/i/2025/09/16/u7dxwr-1.webp) + +![](https://blog.meowrain.cn/api/i/2025/09/16/u80n69-1.webp) + +## 事务回滚 +每条记录在进行更新操作的时候,产生的undo日志都包含一个roll_pointer指针和一个trx_id事务标识符。 +- trx_id用于识别对特定记录执行修改的操作的具体事务 +- roll_pointer 则允许把一系列相关的undolog日志链接起来形成所谓的**版本链** + +![](https://blog.meowrain.cn/api/i/2025/09/16/ua6n2r-1.webp) + +当某一个事务需要回滚的时候,并不是通过逆向执行SQL语句来恢复数据状态的,而是依据事务中roll_pointer指向的undolog日志条目来进行数据复原。 + +![](https://blog.meowrain.cn/api/i/2025/09/16/vqnibz-1.webp) \ No newline at end of file diff --git a/src/content/posts/中间件/MySQL/MySQLbinlog,redolog和undolog.md b/src/content/posts/中间件/MySQL/MySQLbinlog,redolog和undolog.md new file mode 100644 index 0000000..e1f9b91 --- /dev/null +++ b/src/content/posts/中间件/MySQL/MySQLbinlog,redolog和undolog.md @@ -0,0 +1,42 @@ +--- +title: MySQLbinlog,redolog和undolog +published: 2025-08-09 +description: '' +image: '' +tags: [MySQL, binlog, redolog, undolog] +category: '中间件 > MySQL' +draft: false +lang: '' +--- + +# MySQL的binlog、redolog和undolog详解 +![](https://blog.meowrain.cn/api/i/2025/08/09/kh6tf8-1.webp) + +## binlog +binlog +用途: +1. 主从复制 +2. 数据恢复 +3. 审计 + +## redolog(保证持久性) +redo log +目的: 确保事务的持久性 +作用: 记录了数据被修改之后的值。当事务提交以后,即使数据还没有完全写入磁盘,只要redo log已经落盘,数据库在发生宕机等意外情况之后,仍然可以通过redo log来'重做'这些修改,从而恢复到宕机前的最新状态,保证了已提交事务的数据不可丢失,这是一种前滚操作。 + + +## undolog(保证原子性) + +目的: 保证事务的原子性和实现多版本并发控制。 +作用: 记录的是数据被修改之前的旧版本。当一个事务需要回滚的时候,数据库可以利用undo log中的信息将数据恢复到事务开始前的状态。 + +![](https://blog.meowrain.cn/api/i/2025/08/09/lnk04g-1.webp) + +![](https://blog.meowrain.cn/api/i/2025/08/09/lmvm5q-1.webp) + + + +# 区别 +![](https://blog.meowrain.cn/api/i/2025/08/09/kexum3-1.webp) + +![](https://blog.meowrain.cn/api/i/2025/08/09/kgn410-1.webp) \ No newline at end of file diff --git a/src/content/posts/中间件/MySQL/MySQL中CHAR和VARCHAR的区别.md b/src/content/posts/中间件/MySQL/MySQL中CHAR和VARCHAR的区别.md new file mode 100644 index 0000000..c12d119 --- /dev/null +++ b/src/content/posts/中间件/MySQL/MySQL中CHAR和VARCHAR的区别.md @@ -0,0 +1,21 @@ +--- +title: MySQL中CHAR和VARCHAR的区别 +published: 2025-09-07 +description: '' +image: '' +tags: ['MySQL', 'CHAR', 'VARCHAR'] +category: '中间件 > MySQL' +draft: false +lang: '' +--- + +![](https://blog.meowrain.cn/api/i/2025/09/07/xy0628-1.webp) + +# CHAR(n) +char(n) 是固定长度的字符串,CHAR列的长度是固定的,即使存储的字符串长度小于定义的长度,MySQL也会在字符串的末尾填充空格以达到指定的长度。 + +# VARCHAR(n) +可变长度的字符串,varchar列的长度是可变的,存储的字符串长度与实际数据长度相等,并且在存储数据的时候会额外增加1到2个字节(字符串长度超过255,就用两个字节) 用于存储字符串的长度信息。 + +理论上char比varchar会快,因为varchar长度不固定,处理需要多一次运算,但是实际上这种运算耗时微乎其微,而固定大小在很多场景下比较浪费空间,除非存储的字符确认是固定大小或者本身就很短,不然业务上推荐使用varchar. + diff --git a/src/content/posts/中间件/MySQL/MySQL中几种count的区别.md b/src/content/posts/中间件/MySQL/MySQL中几种count的区别.md new file mode 100644 index 0000000..b115775 --- /dev/null +++ b/src/content/posts/中间件/MySQL/MySQL中几种count的区别.md @@ -0,0 +1,22 @@ +--- +title: MySQL中几种count的区别 +published: 2025-09-07 +description: "MySQL中count(*),count(1)和count(字段名)的区别" +image: "" +tags: ["count", "MySQL"] +category: "中间件 > MySQL" +draft: false +lang: "" +--- + +# count(\*) 和 count(1) + +是用来统计行数的聚合函数,统计表中的全部行的数量,包括 null 值 + +# count(字段名) + +也是用来统计行数的聚合函数,会统计指定字段下不为 null 的行数,这种写法会对指定的字段进行计数,只会统计字段值不为 null 的行。 + +![](https://blog.meowrain.cn/api/i/2025/09/07/zckucc-1.webp) + +![](https://blog.meowrain.cn/api/i/2025/09/07/zebv8e-1.webp) diff --git a/src/content/posts/中间件/MySQL/MySQL中发生死锁如何解决.md b/src/content/posts/中间件/MySQL/MySQL中发生死锁如何解决.md new file mode 100644 index 0000000..aa518e9 --- /dev/null +++ b/src/content/posts/中间件/MySQL/MySQL中发生死锁如何解决.md @@ -0,0 +1,238 @@ +--- +title: MySQL中发生死锁如何解决 +published: 2025-09-08 +description: 'MySQL中发生死锁如何解决' +image: '' +tags: ['MySQL','死锁'] +category: '中间件 > MySQL' +draft: false +lang: '' +--- + +![](https://blog.meowrain.cn/api/i/2025/09/08/10jgi3w-1.webp) + + + +# 自动检测与回滚 +MySQL自带死锁检测机制(innodb_deadlock_detect),当检测到死锁的时候,数据库会自动回滚其中一个事务,以接触死锁,通常会回滚事务中持有最少资源的那个。 + +也有锁等待超时的参数(innodb_lock_wait_timeout),当锁等待超过这个时间后,MySQL会自动回滚。 + +# 手动kill发生死锁的语句 +可以通过命令,手动快速找出被阻塞的事务以及线程ID,然后手动Kill掉,及时释放资源。 + +# 常见降低/排除死锁出现情况的方法 +- 避免大事务: 大事务占据锁耗时长,可以把大事务拆分成多个小事务执行快速释放锁,可以降低死锁产生的概率和冲突 +- 调整申请锁的顺序: 在写操作的时候保证能获得足够范围的锁,如修改操作的时候先获取排他锁再获取共享锁,固定顺序访问数据 +- 更改数据隔离级别: 可重复读比读已提交多了间隙锁和临键锁,使用读已提交能降低死锁出现的情况。 +- 合理建立索引,减少加锁范围 +- 开启死锁检测,适当调整锁等待超时时间 + +![](https://blog.meowrain.cn/api/i/2025/09/08/11d3nuc-1.webp) + +![](https://blog.meowrain.cn/api/i/2025/09/08/11d5hm7-1.webp) + +# 实际测试 +innodb_print_all_deadlocks:开启死锁打印 +```sql +show VARIABLES like 'innodb_print_all_deadlocks'; + +set GLOBAL innodb_print_all_deadlocks = 1; + +flush PRIVILEGES; + +``` + +```sql + +create table deadlock_test ( + id bigint not null, + name varchar(255), + primary key(id) +); +insert into deadlock_test values(1, 'zhangsan'); + +``` + +![](https://blog.meowrain.cn/api/i/2025/09/08/10tx11y-1.webp) + +![](https://blog.meowrain.cn/api/i/2025/09/08/10u159n-1.webp) + + +```sql +show engine innodb status; +``` + + +经典“交叉持锁、互等”死锁: +事务 (1) 当前语句是 select * from deadlock_test where id = 1 for update,日志显示它已持有 id=2 的记录锁,正等待获取 id=1 的锁。 +事务 (2) 当前语句是 select * from deadlock_test where id = 2 for update,日志显示它已持有 id=1 的记录锁,正等待获取 id=2 的锁。 +二者形成环:T1 持 id=2 等 id=1;T2 持 id=1 等 id=2。 +锁类型:lock_mode X locks rec but not gap 为记录级行锁(非 GAP 锁),锁定的是主键记录本身。 +仲裁结果:InnoDB 回滚了事务 (2)(“WE ROLL BACK TRANSACTION (2)”),说明它评估回滚成本更低(未必是开始时间靠后)。 + +``` +------------------------ +LATEST DETECTED DEADLOCK +------------------------ +2025-09-08 14:25:29 138110019045056 +*** (1) TRANSACTION: +TRANSACTION 48777, ACTIVE 96 sec starting index read +mysql tables in use 1, locked 1 +LOCK WAIT 3 lock struct(s), heap size 1128, 2 row lock(s), undo log entries 1 +MySQL thread id 638, OS thread handle 138111594002112, query id 479129 10.0.0.8 root statistics +select * from deadlock_test where id = 1 for update + +*** (1) HOLDS THE LOCK(S): +RECORD LOCKS space id 76 page no 4 n bits 72 index PRIMARY of table `deadlock`.`deadlock_test` trx id 48777 lock_mode X locks rec but not gap +Record lock, heap no 3 PHYSICAL RECORD: n_fields 4; compact format; info bits 0 + 0: len 8; hex 8000000000000002; asc ;; + 1: len 6; hex 00000000be89; asc ;; + 2: len 7; hex 82000001230110; asc # ;; + 3: len 7; hex 77616e6773616e; asc wangsan;; + + +*** (1) WAITING FOR THIS LOCK TO BE GRANTED: +RECORD LOCKS space id 76 page no 4 n bits 72 index PRIMARY of table `deadlock`.`deadlock_test` trx id 48777 lock_mode X locks rec but not gap waiting +Record lock, heap no 2 PHYSICAL RECORD: n_fields 4; compact format; info bits 0 + 0: len 8; hex 8000000000000001; asc ;; + 1: len 6; hex 00000000be80; asc ;; + 2: len 7; hex 01000000be2b14; asc + ;; + 3: len 4; hex 6c697369; asc lisi;; + + +*** (2) TRANSACTION: +TRANSACTION 48768, ACTIVE 149 sec starting index read +mysql tables in use 1, locked 1 +LOCK WAIT 3 lock struct(s), heap size 1128, 2 row lock(s), undo log entries 1 +MySQL thread id 636, OS thread handle 138111598196416, query id 479151 10.0.0.8 root statistics +select * from deadlock_test where id = 2 for update + +*** (2) HOLDS THE LOCK(S): +RECORD LOCKS space id 76 page no 4 n bits 72 index PRIMARY of table `deadlock`.`deadlock_test` trx id 48768 lock_mode X locks rec but not gap +Record lock, heap no 2 PHYSICAL RECORD: n_fields 4; compact format; info bits 0 + 0: len 8; hex 8000000000000001; asc ;; + 1: len 6; hex 00000000be80; asc ;; + 2: len 7; hex 01000000be2b14; asc + ;; + 3: len 4; hex 6c697369; asc lisi;; + + +*** (2) WAITING FOR THIS LOCK TO BE GRANTED: +RECORD LOCKS space id 76 page no 4 n bits 72 index PRIMARY of table `deadlock`.`deadlock_test` trx id 48768 lock_mode X locks rec but not gap waiting +Record lock, heap no 3 PHYSICAL RECORD: n_fields 4; compact format; info bits 0 + 0: len 8; hex 8000000000000002; asc ;; + 1: len 6; hex 00000000be89; asc ;; + 2: len 7; hex 82000001230110; asc # ;; + 3: len 7; hex 77616e6773616e; asc wangsan;; + +*** WE ROLL BACK TRANSACTION (2) +------------ +TRANSACTIONS +------------ +Trx id counter 48783 +Purge done for trx's n:o < 48783 undo n:o < 0 state: running but idle +History list length 1 +LIST OF TRANSACTIONS FOR EACH SESSION: +---TRANSACTION 419586600079360, not started +0 lock struct(s), heap size 1128, 0 row lock(s) +---TRANSACTION 419586600080976, not started +0 lock struct(s), heap size 1128, 0 row lock(s) +---TRANSACTION 419586600080168, not started +0 lock struct(s), heap size 1128, 0 row lock(s) +---TRANSACTION 419586600078552, not started +0 lock struct(s), heap size 1128, 0 row lock(s) +---TRANSACTION 419586600077744, not started +0 lock struct(s), heap size 1128, 0 row lock(s) +---TRANSACTION 419586600076936, not started +0 lock struct(s), heap size 1128, 0 row lock(s) +---TRANSACTION 48777, ACTIVE 121 sec +5 lock struct(s), heap size 1128, 2 row lock(s), undo log entries 1 +MySQL thread id 638, OS thread handle 138111594002112, query id 479167 10.0.0.8 root +``` + +通过MySQL系统库查询被阻塞的事务以及线程ID,手动kill释放资源 + +查询锁信息表: +```sql +-- 8.0 版本以前 +select * from information_schema.innodb_locks; +-- 8.0版本开始 +select * from performance_schema.data_locks; +``` + +## 关闭死锁检测 +```sql +SHOW VARIABLES LIKE 'innodb_deadlock_detect'; +``` + +![](https://blog.meowrain.cn/api/i/2025/09/08/110r5za-1.webp) + + +```sql + +SET GLOBAL innodb_deadlock_detect = 0; +``` + +![](https://blog.meowrain.cn/api/i/2025/09/08/111e4i9-1.webp) + + +接下来我们再次开两个事务 + +![](https://blog.meowrain.cn/api/i/2025/09/08/112zyds-1.webp) + +![](https://blog.meowrain.cn/api/i/2025/09/08/11321yv-1.webp) + +![](https://blog.meowrain.cn/api/i/2025/09/08/114vhsk-1.webp) + + +查询锁等待信息表 +```sql +-- 8.0版本之前 +select * from information_schema.innodb_lock_waits; +-- 8.0版本开始 +select * from performance_schema.data_lock_waits; +``` +![](https://blog.meowrain.cn/api/i/2025/09/08/114xwmu-1.webp) +![](https://blog.meowrain.cn/api/i/2025/09/08/115w1hh-1.webp) + +![](https://blog.meowrain.cn/api/i/2025/09/08/116hus4-1.webp) + +查询innodb事务信息 +```sql +SELECT * from information_schema.INNODB_TRX; + +``` + +![](https://blog.meowrain.cn/api/i/2025/09/08/11a35ow-1.webp) + + +![](https://blog.meowrain.cn/api/i/2025/09/08/118hy05-1.webp) +![](https://blog.meowrain.cn/api/i/2025/09/08/118d2ss-1.webp) +![](https://blog.meowrain.cn/api/i/2025/09/08/118f5gd-1.webp) +![](https://blog.meowrain.cn/api/i/2025/09/08/118gs9v-1.webp) + + + +```sql +-- 列出当前的阻塞者(含可直接 KILL 的进程号) +SELECT + b.ENGINE_TRANSACTION_ID AS blocking_trx_id, + th.PROCESSLIST_ID AS blocking_pid, + trx.trx_started, + trx.trx_state, + trx.trx_rows_locked, + trx.trx_query +FROM performance_schema.data_lock_waits w +JOIN performance_schema.data_locks b + ON w.blocking_engine_lock_id = b.engine_lock_id +JOIN information_schema.INNODB_TRX trx + ON b.engine_transaction_id = trx.trx_id +JOIN performance_schema.threads th + ON b.thread_id = th.thread_id +GROUP BY blocking_trx_id, blocking_pid, trx.trx_started, trx.trx_state, trx.trx_rows_locked, trx.trx_query; + +-- 杀掉阻塞会话 +KILL CONNECTION ; +``` +![](https://blog.meowrain.cn/api/i/2025/09/08/119g2ic-1.webp) + diff --git a/src/content/posts/中间件/MySQL/MySQL中如何解决深度分页问题.md b/src/content/posts/中间件/MySQL/MySQL中如何解决深度分页问题.md new file mode 100644 index 0000000..1f47764 --- /dev/null +++ b/src/content/posts/中间件/MySQL/MySQL中如何解决深度分页问题.md @@ -0,0 +1,141 @@ +--- +title: MySQL中如何解决深度分页问题 +published: 2025-09-09 +description: '' +image: '' +tags: ['MySQL', '分页','深度分页'] +category: '中间件 > MySQL' +draft: false +lang: '' +--- +# MySQL中如何解决深度分页问题 +![](https://blog.meowrain.cn/api/i/2025/09/09/xp3bld-1.webp) + +![](https://blog.meowrain.cn/api/i/2025/09/09/xh9385-1.webp) +## 问题描述 + +深度分页,指的是当数据量很大的时候,按照分页访问后面的数据,例如 `limit 9999990,10` 这会使得数据库要扫描前面的9999990条数据,才能得到最终的10条数据,大批量的扫描数据会增加数据库的负载,影响性能。 + + +## 三种优化方式 +### 记录id +每次分页都返回当前的最大id,然后下次查询的时候,带上这个id,就能利用id > maxid过滤了。 +这种查询适合 连续查询的情况,如果跳页的话就不生效了。 + +普通分页的痛点: +```sql +SELECT * FROM article ORDER BY id LIMIT 10 OFFSET 100000; +``` +MySQL 需要先扫描并跳过 前 100000 行,然后再返回后 10 行。(这里说下底层原因) + +OFFSET 越大,性能越差。 + +我们每次查询时带上 上一次返回的最大 id,下一页就只要取 id > last_id 的记录。 + +不依赖 OFFSET,直接利用索引顺序扫描。 + +```sql +select * from products limit 0,10; -- 第一页 +select * from products where id > 10 limit 10; -- 第二页 +select * from products where id > 20 limit 10; -- 第三页 +select * from products where id > 30 limit 10; -- 第四页 +``` + +### 子查询 + +这里其实和记录id的优化方式是一样的,只不过这里用的是子查询。理论上我们应该先去查询到上一页的最大id,然后再查询下一页的数据。 + +```sql +SELECT * from products where id > ( +SELECT id from products order by created_at desc limit 199999,1) order by created_at desc limit 10; +``` +这里我们给表的created_at建索引,可以利用created_at的二级索引进行扫描,然后利用id > 上一次查询的最大id进行过滤,最后再利用created_at的二级索引进行排序,最后再利用limit进行分页。 +![](https://blog.meowrain.cn/api/i/2025/09/09/xmphei-1.webp) + +子查询只读索引列(最好覆盖:筛选列 + 排序列 + id),IO 最小化。 +外层 JOIN 回表范围仅为一页大小(如 20 条),成本可控。 + + +相较于原来的 +```sql +SELECT * FROM products order by created_at desc limit 200000,10; + +``` +这个虽然可以利用created_at的二级索引进行扫描,但是它需要对每条记录进行一次回表操作,还要丢弃掉前200000条记录,性能较差。 + +![](https://blog.meowrain.cn/api/i/2025/09/09/xn0e7q-1.webp) + + +### join方法 + +```sql +SELECT p.* FROM products p INNER JOIN ( + SELECT id FROM products ORDER BY created_at DESC limit 10 OFFSET 200000 ) AS page_results on p.id = page_results.id order by p.created_at desc; + +``` + +这个和上面的子查询方式是一样的,只不过这里用的是join。 + + +### 使用es +直接上elasticsearch,利用它本身分页的特性,进行优化。 + +--- + +```SQL +use pages; +-- 创建商品表 +CREATE TABLE `products` ( + `id` BIGINT AUTO_INCREMENT COMMENT '自增主键ID', + `product_name` VARCHAR(255) NOT NULL COMMENT '商品名称', + `category_id` INT NOT NULL COMMENT '分类ID', + `price` DECIMAL(10, 2) NOT NULL COMMENT '价格', + `created_at` TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间', + PRIMARY KEY (`id`) +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='商品表'; + +-- 在排序字段上创建索引,这是至关重要的 +CREATE INDEX `idx_created_at` ON `products` (`created_at`); + +-- (可选)创建一个更实用的联合索引,例如按分类查找再按时间排序 +CREATE INDEX `idx_category_created` ON `products` (`category_id`, `created_at`); + + +-- 修改MySQL的语句结束符,以便在存储过程中使用分号 +DELIMITER $$ + +-- 创建一个名为 insert_mock_products 的存储过程 +CREATE PROCEDURE `insert_mock_products`(IN insert_count INT) +BEGIN + -- 定义一个循环计数器 + DECLARE i INT DEFAULT 1; + + -- 开始循环 + WHILE i <= insert_count DO + INSERT INTO `products` ( + `product_name`, + `category_id`, + `price`, + `created_at` + ) VALUES ( + -- 生成一个像 'Product 123' 这样的随机商品名 + CONCAT('Product ', i), + -- 生成一个 1 到 50 之间的随机分类ID + FLOOR(1 + RAND() * 50), + -- 生成一个 10.00 到 1000.99 之间的随机价格 + ROUND(10 + RAND() * 990.99, 2), + -- 生成一个从现在开始,逐步往前推移的时间,确保时间戳的唯一和顺序性 + -- 这里用秒作为递减单位,可以确保排序的稳定性 + DATE_SUB(NOW(), INTERVAL i SECOND) + ); + -- 计数器加1 + SET i = i + 1; + END WHILE; +END$$ + +-- 将语句结束符恢复为默认的分号 +DELIMITER ; + +-- 调用存储过程,并传入你想要插入的数据量 +CALL insert_mock_products(1000000); +``` diff --git a/src/content/posts/中间件/MySQL/MySQL中的全局锁表级锁行级锁机制.md b/src/content/posts/中间件/MySQL/MySQL中的全局锁表级锁行级锁机制.md new file mode 100644 index 0000000..092e78a --- /dev/null +++ b/src/content/posts/中间件/MySQL/MySQL中的全局锁表级锁行级锁机制.md @@ -0,0 +1,201 @@ +--- +title: MySQL全局锁,表级锁,行级锁 +published: 2025-08-09 +description: 'MySQL中的全局锁,表级锁,行级锁机制' +image: '' +tags: [全局锁,表级锁,行级锁] +category: '中间件 > MySQL' +draft: false +lang: '' +--- + + +![](https://blog.meowrain.cn/api/i/2025/08/09/qrldag-1.webp) + + +# MySQL的锁 +## 全局锁 +如果要使用全局锁 +要执行下面的命令: +```sql +flush table with read lock +``` + +![](https://blog.meowrain.cn/api/i/2025/08/09/quecvs-1.webp) + +执行全局锁以后,数据库就变成只读状态了,插入和更新操作都会被阻塞 + +这个全局锁一般是用于数据库全局备份的。在备份数据库期间,不会因为数据和表结构的更新,出现备份文件的数据和预期的不一样。 + +![](https://blog.meowrain.cn/api/i/2025/08/09/qwcl26-1.webp) + +可以看到会卡主 + +![](https://blog.meowrain.cn/api/i/2025/08/09/qwsqeo-1.webp) + +解锁以后就可以插入了 + +备份数据库的时候又不想停机,可以在用 mysqldump的时候加上 --single-transaction参数,就会在备份数据之前先开启事务。这种方法只适用于支持可重复读隔离级别的事务的存储引擎。 + + +## 表级锁 +MySQL中的表级锁有哪些? +- 表锁 +- 元数据锁 +- 意向锁 +- AUTO-INC锁 + +### 表锁 +如果我们相对student表加上表锁 +```sql +-- 允许当前会话读取被锁定的表,但是会组织其他会话对这些表进行写操作 +lock table student_t read; + +-- 表级别的独占锁,也就是写锁 +-- 允许当前会话对表进行读写操作,但会阻止其他会话对这些表进行任何操作 +lock table student_t write; +``` + +需要注意的是,表锁除了会限制别的线程的读写外,也会限制本线程接下来的读写操作。 + + +### 元数据锁 +元数据锁不需要显示调用,因为当我们对数据库表进行操作的时候,会自动给这个表加上MDL + +当我们对一张表进行CRUD操作的时候,加的是MDL读锁 +当我们对一张表做结构变更操作的时候,加的是MDL写锁 + +MDL是为了保证当用户对表执行CRUD操作的时候,防止其他线程对这个表结构做变更。 + +比如说,一个线程正在执行查询操作(加了MDL读锁),如果有其他线程来修改表结构,就会被阻塞,直到查询结束。 +同理,一个线程在修改表结构的时候(申请了MDL写锁),其他线程的查询操作就会被阻塞,直到说表结构变更完成 + + +### 意向锁 +- 在使用InnoDB引擎的表里对某些记录加上共享锁之前,需要先在表级别上加一个意向共享锁。 +- 在使用InnoDB引擎的表里对某些记录加上独占锁之前,需要先在表级别加上一个意向独占锁。 + +普通的select是不会加行级锁的,因为它是用MVCC(多版本并发控制)实现的,是无锁的。 + +不过select也是可以对记录加共享锁和独占锁的。 + +```sql +//先在表上加上意向共享锁,然后对读取的记录加共享锁 +select ... lock in share mode; + +//先表上加上意向独占锁,然后对读取的记录加独占锁 +select ... for update; +``` + + +> 意向共享锁和意向独占锁是表级锁,不会和行级的共享锁和独占锁发生冲突,而且意向锁之间也不会发生冲突,只会和共享表锁和独占锁发生冲突。 + +意向锁的目的是为了快速判断表里是否有记录被加锁。 +比如说,当一个事务想要对某个记录加锁时,可以先检查表级的意向锁,如果表级的意向锁是共享锁,就说明有其他事务正在读取这个表中的记录;如果是独占锁,就说明有其他事务正在修改这个表中的记录。 + +如果表级的意向锁是共享锁,那么其他事务可以对表上共享锁,但是不能加独占锁。如果是表级意向锁是独占锁,其他事务就不能对表上加任何锁。 + + +### AUTO-INC锁 +表里的主键通常会设置成自增的,这是通过主键字段声明 AUTO_INCREMENT 属性实现的。 + +之后可以在插入数据的时候,可以不指定主键的值,数据库会自动给主键赋值递增的值,这主要是通过 AUTO-INC锁实现的。 + +AUTO-INC锁是特殊的表锁机制,锁不是在一个事务提交后才释放,而是在执行完插入语句后就会立刻释放。 + +在插入数据的时候,会加一个表级别的AUTO-INC锁,然后为被 `AUTO_INCREMENT` 修饰的字段赋值递增的值,等插入语句执行完成后,才会把AUTO-INC锁释放掉。 + +那么在一个事务持有AUTO-INC锁的过程中,其他事务如果要向该表插入语句都会被阻塞,从而保证了插入数据的时候,被AUTO_INCREMENT修饰的字段的值是连续递增的。 + +因此,在MySQL5.1.22开始,InnoDB存储引擎提供了一种轻量级的锁来实现自增。 + +一样也是在插入数据的时候,会为被auto_increment修饰的字段加上轻量级锁,然后给该字段赋值一个自增的值,然后就把这个轻量级锁释放了,不需要等待整个插入语句执行完成后才释放锁。 + +## 行级锁 +InnoDB引擎是支持行级锁的,而MyISAM不支持行级锁 + +可以使用下面这两个方式,这种查询会加锁的语句称为锁定读。 +```sql +//对读取的记录加共享锁 +select ... lock in share mode; + +//对读取的记录加独占锁 +select ... for update; +``` + +![](https://blog.meowrain.cn/api/i/2025/08/09/10o8753-1.webp) + +### 行级锁类型 +有三类: +- Record Lock,记录锁,也就是仅仅把一条记录锁上 +- Gap Lock 间隙锁,锁定一个范围,但是不包含记录本身 +- Next-Key Lock: Record Lock + Gap Lock的组合,锁定一个范围,并且锁定记录本身 + + +### Record Lock 记录锁 +Record Lock被称为记录锁,锁住的锁一条记录,而且记录锁是有S锁和X锁之分的。 + +- 当一个事务对一条记录加了S型记录锁后,其他事务也可以继续对该记录加S型记录锁,但是不可以对该记录加X型记录锁 +- 当一个事务对一条记录加了X型记录锁后,其他事务不可以对该记录加S型记录锁,也不可对该记录加X型记录锁 + + +![](https://blog.meowrain.cn/api/i/2025/08/09/10qlyoo-1.webp) + +### Gap Lock 间隙锁 + +Gap Lock被称为间隙锁,存在于可重复读隔离级别和串行化隔离级别,目的是为了解决可重复读隔离级别下幻读的现象 + + +假设表中有一个范围id为(3,5)的间隙锁,那么其他事务就无法插入id = 4这条记录了,这样就有效地防止了幻读现象的发生。 + +间隙锁虽然也存在X型和S型间隙锁,但是没什么区别,间隙锁之间是兼容的,两个事务可以同时持有并包含共同间隙范围的间隙锁,并不存在互斥关系,因为间隙锁的目的是防止插入幻影记录而提出的。 + + +![](https://blog.meowrain.cn/api/i/2025/08/09/124cfwm-1.webp) + +![](https://blog.meowrain.cn/api/i/2025/08/09/124ikdt-1.webp) + +![](https://blog.meowrain.cn/api/i/2025/08/09/12542r3-1.webp) + + +### Next-Key Lock 临键锁 + +Next-Key-Lock称为临键锁,是Record Lock和Gap Lock的组合。锁定一个范围,并且锁定记录本身。 +假设表中有个范围id为(3,5]的next-key-lock,那么其它事务既不能插入id = 4的记录,也不能修改id = 5这条记录。 + +所以next-key lock既能保护该记录,又能阻止其它事务将新记录插入到被保护记录前面的间隙中。 + +Next-key lock 是数据库中 InnoDB 存储引擎(常见于 MySQL)使用的一种锁机制,主要用于防止 **幻读(Phantom Read)** 问题,确保事务在可重复读(Repeatable Read)隔离级别下的一致性。它的意义在于通过结合 **记录锁(Record Lock)** 和 **间隙锁(Gap Lock)**,对索引记录及其前后的间隙进行锁定,从而避免其他事务插入或修改数据导致的幻读现象。 + +#### 具体意义和作用: +1. **防止幻读**: + - 幻读是指在同一事务中,多次执行相同查询时,由于其他事务插入了新记录,导致查询结果集发生变化。 + - Next-key lock 锁定一个索引记录及其前后的间隙,防止其他事务插入新记录到这个范围内,从而保证查询结果的稳定性。 + +2. **结合记录锁和间隙锁**: + - **记录锁**:锁定具体的索引记录,防止其他事务修改或删除该记录。 + - **间隙锁**:锁定索引记录之间的“间隙”,防止其他事务在该间隙内插入新记录。 + - Next-key lock 是两者的结合,锁定一个记录及其左侧或右侧的间隙。例如,对于索引值 10,Next-key lock 可能锁定 (5, 10] 范围(假设 5 是前一个索引值)。 + +3. **提高并发控制的精度**: + - Next-key lock 是一种范围锁,比表级锁更精细,能够在保证数据一致性的同时,尽量减少锁的粒度,提高并发性能。 + +4. **支持可重复读隔离级别**: + - 在 MySQL 的可重复读(Repeatable Read)隔离级别下,Next-key lock 是默认的锁机制,用于确保事务在多次读取时看到一致的数据快照。 + +#### 工作原理: +- 当事务对某一行记录进行操作(例如 SELECT ... FOR UPDATE 或 UPDATE),InnoDB 会锁定该记录以及其前后的间隙。 +- 例如,假设表中有一个索引列 `id` 包含值 10、20、30。如果事务 A 对 `id = 20` 加锁,Next-key lock 可能会锁定 (10, 20] 或 (20, 30] 的范围,防止其他事务插入值在该范围内的记录。 + +#### 注意事项: +1. **性能影响**:Next-key lock 锁定范围较大,可能导致锁冲突,降低并发性能。 +2. **死锁风险**:多个事务竞争相同的间隙锁可能导致死锁,需要合理设计事务逻辑。 +3. **依赖索引**:Next-key lock 依赖于索引。如果查询没有使用索引,可能会退化为表级锁,影响性能。 + +总结来说,Next-key lock 的核心意义在于通过锁定记录和间隙,防止幻读,维护事务隔离级别的一致性,同时在高并发场景下提供较好的数据保护机制。 + + +### 插入意向锁 +一个事务在插入一条记录的时候,需要判断插入位置是否已被其他事务加了间隙锁(next-key lock 也包含间隙锁)。 + +如果有的话,插入操作就会发生阻塞,直到拥有间隙锁的那个事务提交为止(释放间隙锁的时刻),在此期间会生成一个插入意向锁,表明有事务想在某个区间插入新记录,但是现在处于等待状态。 \ No newline at end of file diff --git a/src/content/posts/中间件/MySQL/MySQL中的数据排序是怎么实现的.md b/src/content/posts/中间件/MySQL/MySQL中的数据排序是怎么实现的.md new file mode 100644 index 0000000..83bf1fc --- /dev/null +++ b/src/content/posts/中间件/MySQL/MySQL中的数据排序是怎么实现的.md @@ -0,0 +1,10 @@ +--- +title: MySQL中的数据排序是怎么实现的 +published: 2025-09-07 +description: '' +image: '' +tags: [] +category: '' +draft: false +lang: '' +--- diff --git a/src/content/posts/中间件/MySQL/MySQL事务的两阶段提交是什么.md b/src/content/posts/中间件/MySQL/MySQL事务的两阶段提交是什么.md new file mode 100644 index 0000000..5b23ef5 --- /dev/null +++ b/src/content/posts/中间件/MySQL/MySQL事务的两阶段提交是什么.md @@ -0,0 +1,10 @@ +--- +title: MySQL事务的两阶段提交是什么 +published: 2025-09-08 +description: '' +image: '' +tags: [] +category: '' +draft: false +lang: '' +--- diff --git a/src/content/posts/中间件/MySQL/MySQL事务隔离级别.md b/src/content/posts/中间件/MySQL/MySQL事务隔离级别.md new file mode 100644 index 0000000..35a9694 --- /dev/null +++ b/src/content/posts/中间件/MySQL/MySQL事务隔离级别.md @@ -0,0 +1,30 @@ +--- +title: MySQL事务隔离级别 +published: 2025-09-07 +description: "" +image: "" +tags: ["事务隔离级别", "MySQL"] +category: "中间件 > MySQL" +draft: false +lang: "" +--- + +![](https://blog.meowrain.cn/api/i/2025/09/07/yscnoh-1.webp) + +# 1 读未提交(脏读,不可重复读,幻读 问题) + +最低的事务隔离级别,在这个事务隔离级别下,一个事务能看到另外一个事务未提交的数据修改,会导致 **脏读** 的问题(读取到其他事务未提交的数据) + +# 2 读已提交(不可重复读,幻读) + +这个事务隔离级别虽然解决了脏读问题,也就是只能读取到另外一个事务已经提交的数据,读取不到另外一个事务没有提交的数据,但是它有**不可重复读**的问题(同一个事务中,相同的查询会返回不同的结果) + +# 3 可重复读(幻读) MySQL 默认事务隔离级别 + +这个事务隔离级别,使用 MVCC(快照读)的方式,解决了不可重复读的问题,但是还是有**幻读**的问题(幻读也就是在一个事务中,读取到另外一个事务插入的行,导致这个事务查询到的结果集行数不同) + +![](https://blog.meowrain.cn/api/i/2025/09/07/yu38tz-1.webp) + +# 4 串行化 + +最高的事务隔离级别,使用排他锁(Exclusive Lock)来保证事务的完全隔离。 diff --git a/src/content/posts/中间件/MySQL/MySQL索引类型有哪些.md b/src/content/posts/中间件/MySQL/MySQL索引类型有哪些.md new file mode 100644 index 0000000..7d95962 --- /dev/null +++ b/src/content/posts/中间件/MySQL/MySQL索引类型有哪些.md @@ -0,0 +1,75 @@ +--- +title: MySQL索引类型有哪些 +published: 2025-08-14 +description: "" +image: "" +tags: [索引, MySQL] +category: "中间件 > MySQL" +draft: false +lang: "" +--- + +# MySQL 索引类型有哪些 + +## 按数据结构分 + +分为 + +- B-Tree 索引 +- Hash 索引 +- Full-text 索引 + +![](https://blog.meowrain.cn/api/i/2025/08/15/5dxy1-1.webp) + +创建表的时候,InnoDB 存储引擎会根据不同的场景选择不同的列作为索引: + +- 有主键:会使用主键作为聚簇索引的索引键 +- 没有主键: 选择第一个不包含 NULL 值的唯一列作为聚簇索引的索引键 +- 上面两个都没有的情况下,InnoDB 会自动生成一个隐式自增 id 列作为聚簇索引的索引键。 + +其他索引都属于辅助索引,也就是非聚簇索引或者二级索引。 + +## 按物理存储分 + +分为 + +- 聚簇索引(主键索引) +- 二级索引(辅助索引) + +## 按字段特性分 + +- 主键索引 +- 唯一索引 +- 普通索引 +- 前缀索引 + +主键索引是唯一的,且不允许为 NULL。每个表只能有一个主键索引。 + +## 按字段个数分 + +- 单列索引 +- 联合索引 + +--- + +# 按数据结构分 + +- B+树索引 +- 哈希索引 +- 倒排索引(Full-text 索引) +- R-树索引 (多维树空间) + +从 InnoDB b+树索引来看,分为聚簇索引和非聚簇索引 +聚簇索引也就是主键索引,叶子节点存储整行的数据,非叶子节点存储主键值和指向子节点的指针。 +非聚簇索引叶子节点存储主键,非叶子节点存储主键值和指向子节点的指针。 +因此,非聚簇索引查询需要回表查询 + +# 从索引性质看 + +有 +普通索引 +主键索引 +唯一索引 +联合索引 +全文索引 +空间索引 diff --git a/src/content/posts/中间件/MySQL/MySQL联合索引失效情况.md b/src/content/posts/中间件/MySQL/MySQL联合索引失效情况.md new file mode 100644 index 0000000..9709522 --- /dev/null +++ b/src/content/posts/中间件/MySQL/MySQL联合索引失效情况.md @@ -0,0 +1,30 @@ +--- +title: MySQL联合索引失效情况 +published: 2025-09-15 +description: '' +image: '' +tags: ['MySQL','联合索引'] +category: '中间件 > MySQL' +draft: false +lang: '' +--- + +# MySQL联合索引失效情况 + + +## 1. 不满足最左匹配原则 + +## 2. 在索引上使用函数或者运算 + +## 3. 索引列参与隐式类型转换 + +## 4. 使用NOT IN,!=,<>等否定操作符 + +## 5. 模糊匹配 like %xxx% + +## 6.OR操作符 +如果在Where子句中使用了OR操作符,并且OR前的条件列是索引列,OR后的不是索引列,那么索引可能会失效。 + +## 7. 使用 not exists关键字,索引也会失效(本质上是Where查询范围太大) + +## 8. 使用Order By 注意最左匹配,要加limit或者Where关键字,否则索引会失效 \ No newline at end of file diff --git a/src/content/posts/中间件/MySQL/什么是MySQL的主从同步机制.md b/src/content/posts/中间件/MySQL/什么是MySQL的主从同步机制.md new file mode 100644 index 0000000..4c9b637 --- /dev/null +++ b/src/content/posts/中间件/MySQL/什么是MySQL的主从同步机制.md @@ -0,0 +1,50 @@ +--- +title: 什么是MySQL的主从同步机制 +published: 2025-09-09 +description: '' +image: '' +tags: ['MySQL', '主从同步'] +category: '中间件 > MySQL' +draft: false +lang: '' +--- +# 什么是MySQL的主从同步机制 + +MySQL的主从同步机制是一种数据复制技术,用于将住数据库上的数据同步到一个或者多个从数据库中。 +主要是通过二进制日志 binlog 实现数据的复制。 +主数据库在执行写操作的时候,会把这些操作记录在binlog里面,然后推送给从数据库,从数据库重放对应的日志即可完成复制。 + +# MySQL主从复制类型 + +MySQL支持异步复制,同步复制,半同步复制 + +异步复制: 主库不需要等待从库的响应(性能高,一致性低) +同步复制: 主库同步等待所有从库确认收到的数据(性能差,一致性高) +半同步复制: 主库等待至少一个从库确认收到数据(性能折中,数据一致性比较高) + +## 异步复制 + +![](https://blog.meowrain.cn/api/i/2025/09/09/y283pk-1.webp) + +MySQL默认是异步复制。 + + +# 主从复制流程 + +1. 线程创建,从服务器创建一个IO线程,一个SQL线程,IO线程负责读取主服务器上的binlog,并写入到本地relay log中,SQL线程负责读取relay log中的日志,并执行到从服务器上 +2. 连接建立: 从服务器的IO线程与主服务器建立连接,主服务器的binlog dump线程和从服务器的IO线程进行交互 +3. 从服务器的IO线程告诉主服务器开始日志传送的对应位置 +4. 主服务器更新的时候把记录保存到binlog中 +5. 主服务器dump线程检测到binlog变化,从指定位置开始读取,从服务器进行拉取。 +6. 中继日志存储: 从服务器的IO线程把接收到的内容保存到relay log中 +7. 数据写入: 从服务器的SQL线程读取relay log中的内容,进行数据写入。 + +# 主从复制延迟 + +主从复制延迟是指主服务器和从服务器之间数据同步的时间差。 +主从复制延迟的原因有很多,例如网络延迟,主服务器和从服务器之间的硬件差异,主服务器和从服务器之间的操作系统差异,主服务器和从服务器之间的MySQL版本差异,主服务器和从服务器之间的MySQL配置差异等。 + +解决方法: +优化网络 +提高从服务器性能 +利用MySQL并行复制功能提升效率,减少延迟。https://blog.csdn.net/weixin_42587823/article/details/144842206 \ No newline at end of file diff --git a/src/content/posts/中间件/MySQL/共享锁和排他锁区别.md b/src/content/posts/中间件/MySQL/共享锁和排他锁区别.md new file mode 100644 index 0000000..8cc6a35 --- /dev/null +++ b/src/content/posts/中间件/MySQL/共享锁和排他锁区别.md @@ -0,0 +1,219 @@ +--- +title: MySQL共享锁和排他锁的区别 +published: 2025-07-18 +description: '' +image: '' +tags: [mysql锁] +category: '中间件 > MySQL' +draft: false +lang: 'zh-cn' +--- + +# 共享锁和排他锁的区别 + +MySQL中的**共享锁(Shared Lock,简称S锁)**和**排他锁(Exclusive Lock,简称X锁)**是InnoDB存储引擎用于并发控制的两种锁机制,主要区别在于锁的兼容性和使用场景。以下是两者的详细对比: + +### 1. **定义** + +- **共享锁(S锁)**:允许多个事务同时对同一数据加共享锁,用于读取数据,防止其他事务修改数据,但允许多个事务同时读取。 +- **排他锁(X锁)**:只允许一个事务对数据加锁,用于修改数据,阻止其他事务对同一数据加任何锁(包括共享锁和排他锁)。 + +### 2. **锁的兼容性** + +| 锁类型 | 共享锁(S锁) | 排他锁(X锁) | +| ----------------- | ------------- | ------------- | +| **共享锁(S锁)** | 兼容 | 不兼容 | +| **排他锁(X锁)** | 不兼容 | 不兼容 | + +- **共享锁**:多个事务可以同时持有同一数据的S锁,适合并发读取。 +- **排他锁**:一旦某事务持有X锁,其他事务无法对同一数据加S锁或X锁,必须等待锁释放。 + +### 3. **使用场景** + +- **共享锁**: + + - 用于只读操作,如`SELECT`查询。 + + - 典型场景:多个事务需要读取同一数据,但不修改(如报表查询)。 + + - 显式获取方式:`SELECT ... LOCK IN SHARE MODE`。 + + - 示例: + 允许多个事务同时读取`id = 1`的行,但阻止其他事务修改该行。 + + ```sql + SELECT * FROM users WHERE id = 1 LOCK IN SHARE MODE; + + ``` + +- **排他锁**: + + - 用于写操作,如`UPDATE`、`DELETE`、`INSERT`。 + + - 典型场景:需要修改数据并确保数据一致性(如库存扣减、余额更新)。 + + - 显式获取方式:`SELECT ... FOR UPDATE`。 + + - 示例: + 锁定`id = 1`的行,阻止其他事务读取或修改,直到当前事务结束。 + + ```sql + SELECT * FROM users WHERE id = 1 FOR UPDATE; + + ``` + +### 4. **获取方式** + +- **共享锁**: + - 自动:某些情况下,InnoDB在`SELECT`查询时可能隐式加S锁(取决于隔离级别)。 + - 显式:`SELECT ... LOCK IN SHARE MODE`。 +- **排他锁**: + - 自动:执行`UPDATE`、`DELETE`等写操作时,InnoDB自动为受影响的行加X锁。 + - 显式:`SELECT ... FOR UPDATE`。 + +### 5. **锁粒度** + +- 两者都支持**行级锁**(InnoDB默认)和**表级锁**(如MyISAM或特定操作)。 +- 共享锁和排他锁在范围查询中可能涉及**间隙锁**或**下一键锁**(Next-Key Lock),用于防止幻读,具体取决于事务隔离级别(如`REPEATABLE READ`)。 + +### 6. **性能影响** + +- **共享锁**:允许多个事务并发读取,适合读多写少的场景,阻塞较少。 +- **排他锁**:阻止其他事务读写,适合写操作,但可能导致阻塞和死锁,尤其在高并发场景下。 + +### 7. **典型应用场景对比** + +- **共享锁**:多个用户同时查看商品库存、生成报表等。 +- **排他锁**:扣减库存、更新账户余额、防止并发修改导致数据不一致。 + +### 8. **死锁风险** + +- **共享锁**:死锁风险较低,因为S锁之间兼容。 +- **排他锁**:死锁风险较高,多个事务竞争X锁可能导致互相等待,InnoDB会检测并回滚一个事务。 + +### 示例对比 + +假设有`products`表,字段包括`id`和`stock`: + +```sql +-- 事务A:读取库存(共享锁) +BEGIN; +SELECT stock FROM products WHERE id = 1 LOCK IN SHARE MODE; +-- 其他事务可以同时读取,但不能修改 +COMMIT; + +-- 事务B:扣减库存(排他锁) +BEGIN; +SELECT stock FROM products WHERE id = 1 FOR UPDATE; +UPDATE products SET stock = stock - 1 WHERE id = 1; +COMMIT; + +``` + +- 事务A的共享锁允许其他事务读取`id = 1`的行,但阻止修改。 +- 事务B的排他锁阻止其他事务读取或修改`id = 1`的行,直到事务B结束。 + +### 总结 + +- **共享锁**适合读操作,允许多个事务并发读取,强调高并发读性能。 +- **排他锁**适合写操作,确保数据修改的独占性和一致性,但可能降低并发性能。 +- 在实际应用中,结合业务需求、索引优化和事务隔离级别(如`READ COMMITTED`或`REPEATABLE READ`)合理选择锁类型,以平衡一致性和性能。 + +# 排他锁 + +MySQL的排他锁(Exclusive Lock,简称X锁)是一种用于并发控制的锁机制,确保在同一时间只有一个事务可以修改特定数据,防止数据冲突和不一致。以下是对MySQL排他锁的详细讲解: + +### 1. **什么是排他锁?** + +排他锁是MySQL中用于写操作的锁类型。当一个事务对某行、表或数据对象加了排他锁后,其他事务无法对同一数据进行读(共享锁)或写(排他锁)操作,直到该锁被释放。这保证了数据修改的原子性和一致性。 + +- **特点**: + - 排他锁与任何其他锁(包括共享锁和排他锁)都不兼容。 + - 持有排他锁的事务可以安全地修改数据,而不被其他事务干扰。 + - 常用于`UPDATE`、`DELETE`、`INSERT`等写操作。 + +### 2. **排他锁的工作机制** + +在MySQL的InnoDB存储引擎中(默认支持事务和行级锁),排他锁主要通过以下方式实现: + +- **行级锁**:锁住特定的行记录,只有被锁定的行无法被其他事务访问。 +- **表级锁**:在某些情况下(如表级操作或MyISAM引擎),锁住整个表。 +- **间隙锁(Gap Lock)和下一键锁(Next-Key Lock)**:用于防止幻读,锁定某个范围的索引记录(常见于范围查询)。 + +当一个事务执行写操作(如`UPDATE`或`DELETE`)时,InnoDB会自动为受影响的行加排他锁。例如: + +```sql +UPDATE users SET age = 30 WHERE id = 1; + +``` + +MySQL会对`id = 1`的行加排他锁,直到事务提交(`COMMIT`)或回滚(`ROLLBACK`)才会释放锁。 + +### 3. **排他锁的获取方式** + +- **隐式获取**:通过DML操作(如`UPDATE`、`DELETE`)自动加锁。例如: + + ```sql + UPDATE table_name SET column = value WHERE condition; + + ``` + + InnoDB会自动为受影响的行加排他锁。 + +- **显式获取**:使用`SELECT ... FOR UPDATE`语句显式加排他锁。例如: + + ```sql + SELECT * FROM users WHERE id = 1 FOR UPDATE; + + ``` + + 这会锁定`id = 1`的行,阻止其他事务读取或修改该行,直到当前事务结束。 + +### 4. **排他锁的兼容性** + +排他锁与其他锁的兼容性如下: + +- **排他锁与排他锁**:不兼容,两个事务不能同时对同一数据加X锁。 +- **排他锁与共享锁(S锁)**:不兼容,持有X锁的数据无法被其他事务加S锁读取。 +- **结果**:排他锁会导致其他事务等待(阻塞),直到锁释放。 + +### 5. **排他锁的场景** + +- **数据修改**:如`UPDATE`、`DELETE`操作,确保数据一致性。 +- **防止并发冲突**:在高并发场景下,避免多个事务同时修改同一行导致数据不一致。 +- **悲观锁机制**:通过`SELECT ... FOR UPDATE`实现悲观锁,适合需要严格控制并发访问的业务场景(如库存扣减)。 + +示例(库存扣减): + +```sql +BEGIN; +SELECT stock FROM products WHERE id = 1 FOR UPDATE; +-- 假设查询到stock=10 +UPDATE products SET stock = stock - 1 WHERE id = 1; +COMMIT; + +``` + +`FOR UPDATE`加排他锁,确保在事务期间其他事务无法修改`id = 1`的记录。 + +### 6. **可能的问题** + +- **死锁**:当多个事务互相等待对方持有的排他锁时,可能发生死锁。InnoDB会自动检测死锁并回滚一个事务。 +- **性能影响**:排他锁会阻塞其他事务,可能降低并发性能,尤其在高并发场景下。 +- **锁范围过大**:若锁住的范围过大(例如表锁或范围查询的间隙锁),可能导致更多事务阻塞。 + +### 7. **如何优化排他锁的使用** + +- **尽量使用行级锁**:确保查询条件使用索引,避免锁住过多行。 +- **缩短事务时间**:尽快提交或回滚事务,减少锁的持有时间。 +- **避免死锁**:按照固定顺序访问资源(如按表或主键顺序加锁)。 +- **选择合适的隔离级别**:如降低隔离级别(从`REPEATABLE READ`到`READ COMMITTED`),减少间隙锁的使用。 + +### 8. **与共享锁的对比** + +- **共享锁(S锁)**:允许多个事务同时读取数据,但不允许修改。常用于`SELECT ... LOCK IN SHARE MODE`。 +- **排他锁(X锁)**:只允许一个事务修改数据,阻塞其他读写操作。 + +### 总结 + +MySQL的排他锁是确保数据写操作一致性的重要机制,广泛用于事务性操作。通过合理设计查询和事务,可以最大程度减少锁冲突和性能问题。在高并发场景下,建议结合索引优化、事务管理以及合适的隔离级别来平衡一致性和性能。 diff --git a/src/content/posts/中间件/MySQL/分库分表场景.md b/src/content/posts/中间件/MySQL/分库分表场景.md new file mode 100644 index 0000000..aa63f69 --- /dev/null +++ b/src/content/posts/中间件/MySQL/分库分表场景.md @@ -0,0 +1,62 @@ +--- +title: 分库分表场景 +published: 2025-09-13 +description: '' +image: '' +tags: ['中间件','MySQL','分库分表'] +category: '中间件 > MySQL' +draft: false +lang: '' +--- + + +# 分库分表 + +## 什么场景分库 +- 当单个数据库支持的连接数不足以满足客户端需求 +- 数据量超过了单个数据库实例的处理能力 + +## 什么场景分表 +- 单表数据量太大 +- 单表存在较高的写入场景 +- 当表中存在大量的TEXT,LONGTEXT 或者BLOB字段 + +## 什么场景分库分表 +- 高并发写入场景: 当应用面临高并发的写入请求的时候,单库单表无法满足需求,需要进行分库分表。 +- 海量数据场景: 当数据量非常大的时候,单库单表无法满足需求,需要进行分库分表。 + +## 分库分表的优缺点 +### 优点 +- 提高系统的性能和可扩展性 +- 提高系统的可用性和可靠性 +- 提高系统的可维护性 + +### 缺点 +- 增加系统的复杂性 +- 增加系统的成本 +- 增加系统的维护成本 + + +## 分库分表如何设计 + +我们分库分表,是有分片键的,这个分片键怎么用的呢? +分片键是用来决定一条数据应该存储在哪个库或者表中的字段,它直接影响数据的分布,查询效率和系统扩展性。 + +举个例子: +1. Hash分片 +原理: 对分片键的值进行哈希运算,然后对库或者表取模 +适用场景: 数据访问随机性强,读写均衡 +![](https://blog.meowrain.cn/api/i/2025/09/13/n8ryrj-1.webp) + +2. Range分片(范围分片) +原理: 根据分片键的值范围划分数据 +使用场景: 时间序列数据,范围查询频繁 +![](https://blog.meowrain.cn/api/i/2025/09/13/n99aoy-1.webp) + +3. Lookup映射分片 +原理: 维护一个映射表,指定某个分片键值属于哪个分片 +使用场景: 分片键是枚举值,比如国家,地区等 +![](https://blog.meowrain.cn/api/i/2025/09/13/n9u3ge-1.webp) + + +![](https://blog.meowrain.cn/api/i/2025/09/13/nmff34-1.webp) \ No newline at end of file diff --git a/src/content/posts/中间件/MySQL/如何处理MySQL的主从同步延迟.md b/src/content/posts/中间件/MySQL/如何处理MySQL的主从同步延迟.md new file mode 100644 index 0000000..b90e6b8 --- /dev/null +++ b/src/content/posts/中间件/MySQL/如何处理MySQL的主从同步延迟.md @@ -0,0 +1,26 @@ +--- +title: 如何处理MySQL的主从同步延迟 +published: 2025-09-09 +description: '' +image: '' +tags: ['MySQL', '主从同步', '延迟'] +category: '中间件 > MySQL' +draft: false +lang: '' +--- + +# 如何处理MySQL的主从同步延迟 + +当我们开启主从同步以后,这种延迟就是必然存在的,不论怎么优化都是没办法避免延迟的存在的,只能说去减少延迟的时间。 + +常见的解决方案: +- 二次查询。如果说从库查不到数据,就去主库查一遍,用API封装这个逻辑就行,当作兜底策略。不过这样等于读的压力又转移到主库上去了,如果有人故意查询不存在的记录,那就会把查询的读请求都打到主库上了。 +- 强制把写之后立马读的操作转移到主库上(写后读主策略)。 写请求完成后,后续一段时间或者同一会话内的查询强制路由到主库,或者在从库追上指定位点前都读主。这个方法虽然简单可靠,能保证用户操作后的可见性,但是会增加主库的读压力,削弱负载分摊。 +- 关键业务读写都走主库。像我们用户注册这种,就可以读写主库,就不会出现说登录报用户不存在的问题了,这种访问量的频率也不高。 +- 使用缓存。 主库写入一行同步到缓存里面,这样查询的时候可以先查缓存,避免延迟,但是这样又有数据一致性问题了,我们就要去考虑数据库和缓存的数据一致性问题了。 + + +还有可能是 **主库的配置高,从库的配置低**,这样的话,也会导致主从同步延迟,我们可以提高从库的配置。 + + +![](https://blog.meowrain.cn/api/i/2025/09/10/k90qnk-1.webp) \ No newline at end of file diff --git a/src/content/posts/中间件/MySQL/搭建MySQL主从服务器.md b/src/content/posts/中间件/MySQL/搭建MySQL主从服务器.md new file mode 100644 index 0000000..e941106 --- /dev/null +++ b/src/content/posts/中间件/MySQL/搭建MySQL主从服务器.md @@ -0,0 +1,313 @@ +--- +title: 搭建MySQL主从服务器 +published: 2025-09-09 +description: '' +image: '' +tags: ['MySQL', '主从同步'] +category: '中间件 > MySQL' +draft: false +lang: '' +--- + +# 买服务器 +![](https://blog.meowrain.cn/api/i/2025/09/09/zh72vh-1.webp) +先买两台服务器,装ubuntu系统 + +# 安装mysql +![](https://blog.meowrain.cn/api/i/2025/09/09/zgrhyz-1.webp) + +![](https://blog.meowrain.cn/api/i/2025/09/09/zh539x-1.webp) + +![](https://blog.meowrain.cn/api/i/2025/09/09/zhoxxy-1.webp) + +![](https://blog.meowrain.cn/api/i/2025/09/09/zhr1zz-1.webp) +# 修改配置,让两个服务器能够互相连接 +![](https://blog.meowrain.cn/api/i/2025/09/09/zhxile-1.webp) + +## 开启监听 +root@ecs-f95f-0002:/etc/mysql/mysql.conf.d# vim mysqld.cnf + +![](https://blog.meowrain.cn/api/i/2025/09/09/zlvtzi-1.webp) + +修改从库 +![](https://blog.meowrain.cn/api/i/2025/09/09/zmmg8c-1.webp) + +root@ecs-f95f-0001:/etc/mysql/mysql.conf.d# sudo systemctl restart mysql +root@ecs-f95f-0002:/etc/mysql/mysql.conf.d# sudo systemctl restart mysql + + +重启一下主库和从库的mysql + + +![](https://blog.meowrain.cn/api/i/2025/09/09/zo9kra-1.webp) + +![](https://blog.meowrain.cn/api/i/2025/09/09/zoe4t0-1.webp) + +## 为root开启远程访问 + +mysql> ALTER USER 'root'@'%' IDENTIFIED WITH mysql_native_password BY 'root'; +Query OK, 0 rows affected (0.00 sec) + +mysql> FLUSH PRIVILEGES; +Query OK, 0 rows affected (0.00 sec) + +启用密码验证 + + +![](https://blog.meowrain.cn/api/i/2025/09/09/10irv8h-1.webp) + +![](https://blog.meowrain.cn/api/i/2025/09/09/10iulwy-1.webp) + +现在可以连接上了 + + +## 修改主库配置 +``` +[mysqld] +server-id = 153 +# 启用二进制日志功能,这是复制的基础 +log-bin = /var/log/mysql/mysql-bin.log +# (可选) 设置二进制日志的格式,建议使用ROW格式,可以更好地保证数据一致性 +binlog_format = ROW +binlog_ignore_db = mysql + +``` + +## 创建远程用户 +```sql +-- 创建远程用户 +CREATE USER 'repl_user'@'%' IDENTIFIED WITH mysql_native_password BY 'remote'; +-- 给予复制权限 + GRANT REPLICATION SLAVE ON *.* TO 'repl_user'@'%'; + +-- 刷新权限 +FLUSH PRIVILEGES; +``` + +![](https://blog.meowrain.cn/api/i/2025/09/09/10notnv-1.webp) + +```sql +-- 锁定所有表,防止新的数据写入,确保数据一致性 +FLUSH TABLES WITH READ LOCK; + +-- 查看主服务器状态 +SHOW MASTER STATUS; +``` + +![](https://blog.meowrain.cn/api/i/2025/09/09/10o5bfr-1.webp) + + +## 备份主数据库,传到从服务器 + +```bash + mysqldump -u root -p --all-databases --source-data > ./master_backup.sql +``` + +![](https://blog.meowrain.cn/api/i/2025/09/09/10pdpxq-1.webp) + +![](https://blog.meowrain.cn/api/i/2025/09/09/10qxsbs-1.webp) + + +创建密钥 +```bash +ssh-keygen -t rsa -b 4096 +``` + +密钥创建好以后,把公钥放到从服务器 +```bash +ssh-copy-id -i ~/.ssh/id_rsa.pub root@192.168.0.93 +``` + + + +![](https://blog.meowrain.cn/api/i/2025/09/09/10r22n7-1.webp) + +![](https://blog.meowrain.cn/api/i/2025/09/09/10rla5b-1.webp) + +``` +Host mysql-slave + HostName 192.168.0.93 + User root + Port 22 + IdentityFile ~/.ssh/id_rsa +``` + +![](https://blog.meowrain.cn/api/i/2025/09/09/10txbl0-1.webp) + +从服务器配置: +```bash +ssh-keygen -t rsa -b 4096 +``` +```bash +ssh-copy-id -i ~/.ssh/id_rsa.pub root@192.168.0.153 +``` + +![](https://blog.meowrain.cn/api/i/2025/09/09/10s7nn8-1.webp) + +![](https://blog.meowrain.cn/api/i/2025/09/09/10sa32o-1.webp) + + +``` +Host mysql-master + HostName 192.168.0.153 + User root + Port 22 + IdentityFile ~/.ssh/id_rsa +``` + +![](https://blog.meowrain.cn/api/i/2025/09/09/10tejl0-1.webp) + + +从主服务器上把备份好的数据库传到从库上 +``` +root@ecs-f95f-0002:~# scp master_backup.sql mysql-slave:~ +master_backup.sql +``` + +从库已经可以看到了 +![](https://blog.meowrain.cn/api/i/2025/09/09/10ujgs1-1.webp) + +导入 +![](https://blog.meowrain.cn/api/i/2025/09/09/122nj25-1.webp) + + +主库解锁 +![](https://blog.meowrain.cn/api/i/2025/09/09/122tx0s-1.webp) + + +从库mysql配置文件 +``` +[mysqld] +server-id = 93 +relay-log = /var/log/mysql/mysql-relay-bin +read-only = 1 +# (可选但推荐) 记录从服务器的数据更改到自己的二进制日志,以便将来可以作为其他从服务器的主服务器 +log-bin = /var/log/mysql/mysql-bin.log +``` + +重启从库并且登录从库 +![](https://blog.meowrain.cn/api/i/2025/09/09/125oncs-1.webp) + +```sql +stop slave; +CHANGE MASTER TO + MASTER_HOST = '192.168.0.153', + MASTER_USER = 'repl_user', + MASTER_PASSWORD = 'remote', + MASTER_LOG_FILE = 'mysql-bin.000005', + MASTER_LOG_POS = 157; +START SLAVE; +``` + +![](https://blog.meowrain.cn/api/i/2025/09/09/129zoko-1.webp) + +``` + +mysql> show slave status \G; +*************************** 1. row *************************** + Slave_IO_State: Waiting for source to send event + Master_Host: 192.168.0.153 + Master_User: repl_user + Master_Port: 3306 + Connect_Retry: 60 + Master_Log_File: mysql-bin.000005 + Read_Master_Log_Pos: 1826 + Relay_Log_File: mysql-relay-bin.000002 + Relay_Log_Pos: 326 + Relay_Master_Log_File: mysql-bin.000005 + Slave_IO_Running: Yes + Slave_SQL_Running: Yes + Replicate_Do_DB: + Replicate_Ignore_DB: + Replicate_Do_Table: + Replicate_Ignore_Table: + Replicate_Wild_Do_Table: + Replicate_Wild_Ignore_Table: + Last_Errno: 0 + Last_Error: + Skip_Counter: 0 + Exec_Master_Log_Pos: 1826 + Relay_Log_Space: 536 + Until_Condition: None + Until_Log_File: + Until_Log_Pos: 0 + Master_SSL_Allowed: No + Master_SSL_CA_File: + Master_SSL_CA_Path: + Master_SSL_Cert: + Master_SSL_Cipher: + Master_SSL_Key: + Seconds_Behind_Master: 0 +Master_SSL_Verify_Server_Cert: No + Last_IO_Errno: 0 + Last_IO_Error: + Last_SQL_Errno: 0 + Last_SQL_Error: + Replicate_Ignore_Server_Ids: + Master_Server_Id: 153 + Master_UUID: 014654cd-8d83-11f0-b940-fa163e8fe780 + Master_Info_File: mysql.slave_master_info + SQL_Delay: 0 + SQL_Remaining_Delay: NULL + Slave_SQL_Running_State: Replica has read all relay log; waiting for more updates + Master_Retry_Count: 86400 + Master_Bind: + Last_IO_Error_Timestamp: + Last_SQL_Error_Timestamp: + Master_SSL_Crl: + Master_SSL_Crlpath: + Retrieved_Gtid_Set: + Executed_Gtid_Set: + Auto_Position: 0 + Replicate_Rewrite_DB: + Channel_Name: + Master_TLS_Version: + Master_public_key_path: + Get_master_public_key: 0 + Network_Namespace: +1 row in set, 1 warning (0.00 sec) + +ERROR: +No query specified +``` + + +![](https://blog.meowrain.cn/api/i/2025/09/09/12i6cmk-1.webp) + +在主库里面 +创建库,表,插入数据 +```sql +-- 创建 UTF8MB4 数据库 +CREATE DATABASE testdb +CHARACTER SET utf8mb4 +COLLATE utf8mb4_general_ci; + +use testdb; +CREATE TABLE user_data ( + id INT AUTO_INCREMENT PRIMARY KEY, -- 主键,自动递增 + username VARCHAR(255) NOT NULL, -- 用户名,最大长度255 + comment TEXT CHARACTER SET utf8mb4 -- 评论,支持存储表情符号和其他Unicode字符 +) ENGINE=InnoDB CHARSET=utf8mb4; + +-- 插入常规数据 +INSERT INTO user_data (username, comment) VALUES ('Alice', 'Hello, world!'); + +-- 插入带表情符号的数据 +INSERT INTO user_data (username, comment) VALUES ('Bob', 'This is fun! 😊'); + +-- 插入中文字符 +INSERT INTO user_data (username, comment) VALUES ('Charlie', '你好,世界!'); + +-- 插入混合内容 +INSERT INTO user_data (username, comment) VALUES ('David', '中文+Emoji 🌟😄'); + +``` + +![](https://blog.meowrain.cn/api/i/2025/09/09/12jzoy9-1.webp) + +![](https://blog.meowrain.cn/api/i/2025/09/09/12k2qso-1.webp) + +接下来我们看看从库状态 + + +![](https://blog.meowrain.cn/api/i/2025/09/09/12kp3qc-1.webp) \ No newline at end of file diff --git a/src/content/posts/中间件/MySQL/数据库ACID四大特性.md b/src/content/posts/中间件/MySQL/数据库ACID四大特性.md new file mode 100644 index 0000000..857decb --- /dev/null +++ b/src/content/posts/中间件/MySQL/数据库ACID四大特性.md @@ -0,0 +1,69 @@ +--- +title: 数据库ACID四大特性 +published: 2025-08-07 +description: '' +image: '' +tags: [ACID,四大特性] +category: '中间件 > MySQL' +draft: false +lang: '' +--- + + +# 什么是ACID四大特性 +A : Atomicity(原子性) +C : Consistency(一致性) +I : Isolation(隔离性) +D : Durability(持久性) + +![](https://blog.meowrain.cn/api/i/2025/08/09/qrczot-1.webp) + +# Atomicity(原子性) +这里要先讲一下什么是事务:  简单说,事务就是一组原子性的SQL执行单元。如果数据库引擎能够成功地对数据库应 用该组査询的全部语句,那么就执行该组SQL。如果其中有任何一条语句因为崩溃或其 他原因无法执行,那么所有的语句都不会执行。要么全部执行成功(commit),要么全部执行失败(rollback)。 + +**原子性指的是一个事务中所有操作要么全部成功要么全部失败。** + +# Consistency(一致性) +数据库的一致性指的是: 每个事务必须使数据库从一个合法的状态,转变到另一个合法的状态,并且在事务执行前后,数据库的各种完整性约束都得以保持。 + +什么是完整性约束呢? +完整性约束是指数据库中数据的规则和限制,比如主键约束、外键约束、唯一性约束等。 +主键约束: 确保每条记录都有唯一标识。 +外键约束: 确保数据之间的引用关系正确。 +唯一性约束: 确保某列的值在表中是唯一的 + +![](https://blog.meowrain.cn/api/i/2025/08/08/f8ed5e-1.webp) + +举个例子: +当我们向订单表插入一条记录的时候,如果指定的`customer_id`在客户表中不存在,那么这个事务就不应该被提交,因为这会破坏数据的一致性。因此,外键约束会阻止事务提交,确保数据库的一致性。抛出外键约束异常 + + +# Isolation(隔离性) +数据库的隔离性指的是: 每个事务的执行都应该是独立的,互不干扰。即使多个事务同时执行,也不会影响彼此的结果。 +隔离性确保了事务之间的独立性,防止了脏读、不可重复读和幻读等问题。 + +如果没有隔离性,在多个用户并发访问数据库的情况下,可能会出现以下问题: +- 脏读(Dirty Read): + 一个事务读取到另一个事务尚未提交的修改,如果该事务回滚,那么读取到的就是无效数据。 +- 不可重复读(Non-repeatable Read): + 一个事务在同一事务内多次读取同一数据,却得到不同的结果,这是因为其他事务修改并提交了数据。 +- 幻读(Phantom Read): + 一个事务在同一事务内多次执行相同的查询,但是每次查询返回的结果集不同,这是因为其他事务插入或删除了数据。 + +![](https://blog.meowrain.cn/api/i/2025/08/08/f82hdp-1.webp) + +事务隔离级别: +- 读未提交 >> 事务可以读取其他事务未提交的数据,可能会导致脏读。 +- 读已提交 >> 事务只能读取已提交的数据,防止脏读。 +- 可重复读 >> 事务在执行期间多次读取同一数据,结果保持一致,防止不可重复读。 +- 串行化 >> 事务完全隔离,按顺序执行,防止幻读。 + + +数据库的默认隔离级别是**可重复读(Repeatable Read)**,它可以防止脏读和不可重复读,但可能会出现幻读。(也就是无法避免读取数据的时候,其他事务提交新的数据或者删除数据,导致查询的结果集发生变化。 + +> 之前老把幻读和不可重复读搞混,现在再讲一下,所谓幻读,就是说读取数据过程中,另外一个数据库事务插入或者删除了数据,导致查询的数据结果集发生变化。而不可重复读是指在同一个事务中多次读取同一个数据,期间其他事务修改了数据,导致数据结果集不一致。 + +每个隔离级别都提供了不同程度的隔离性和性能,具体选择取决于应用场景和需求。 +# Durability(持久性) +数据库的持久性指的是: 一旦事务提交,对数据库的修改就会永久保存,即使系统崩溃也不会丢失。 +持久性确保了数据的可靠性和稳定性,即使在系统故障或崩溃后,已提交的事务数据仍然可以恢复。 \ No newline at end of file diff --git a/src/content/posts/中间件/Nginx/Nginx编译安装.md b/src/content/posts/中间件/Nginx/Nginx编译安装.md new file mode 100644 index 0000000..18342dc --- /dev/null +++ b/src/content/posts/中间件/Nginx/Nginx编译安装.md @@ -0,0 +1,41 @@ +--- +title: Nginx编译安装 +published: 2025-07-18 +description: '' +image: '' +tags: [Nginx编译安装] +category: '中间件 > Nginx' +draft: false +lang: 'zh-cn' +--- + + +# nginx编译安装 + +``` +wget https://nginx.org/download/nginx-1.28.0.tar.gz +tar -zxvf ninx-1.28.0.tar.gz +sudo apt install -y build-essential libtool zlib1g-dev openssl libpcre3 libpcre3-dev libssl-dev libgeoip-dev +sudo apt install libpcre2-dev + +# 常用模块配置 +./configure \ +--prefix=/usr/local/nginx \ +--pid-path=/var/run/nginx/nginx.pid \ +--lock-path=/var/lock/nginx.lock \ +--error-log-path=/var/log/nginx/error.log \ +--http-log-path=/var/log/nginx/access.log \ +--with-http_gzip_static_module \ +--http-client-body-temp-path=/var/temp/nginx/client \ +--http-proxy-temp-path=/var/temp/nginx/proxy \ +--http-fastcgi-temp-path=/var/temp/nginx/fastcgi \ +--http-uwsgi-temp-path=/var/temp/nginx/uwsgi \ +--http-scgi-temp-path=/var/temp/nginx/scgi \ +--with-stream \ +--with-http_ssl_module \ +--with-stream_ssl_preread_module # 新增:支持 ssl_preread 指令 + +make +make install + +``` diff --git a/src/content/posts/中间件/Postgresql/postgresql一些容易混淆的概念.md b/src/content/posts/中间件/Postgresql/postgresql一些容易混淆的概念.md new file mode 100644 index 0000000..1cd947b --- /dev/null +++ b/src/content/posts/中间件/Postgresql/postgresql一些容易混淆的概念.md @@ -0,0 +1,102 @@ +--- +title: postgresql一些容易混淆的概念 +published: 2025-07-21 +description: '' +image: '' +tags: [PostgreSQL, PostgreSQL容易混淆的概念] +category: '中间件 > PostgreSQL' +draft: false +lang: '' +--- + + +# PostgreSQL一些容易混淆的概念 + +https://www.cnblogs.com/noodlesmars/p/11850559.html + +# Schema + +在数据库创建的同时,就已经默认为数据库创建了一个模式--public,这也是该数据库的默认模式。所有为此数据库创建的对象(表、函数、试图、索引、序列等)都是创建在这个模式中的 + +一个数据库包含一个或多个Schema,一个Schema包含一个或多个表,一个表包含一个或多个字段,一个字段包含一个或多个值。 +我拿我们熟悉的MySQL数据库举例子,MySQL数据库中,数据库就是Schema,表就是Table,字段就是Column,值就是Value。 +但是在PgSQL中,数据库不是像MySQL那样的数据库了,它下面可以放很多Schema,而Schema下面才是Table,字段就是Column,值就是Value。 + +打个比方,mysql的database就是一个书柜,table是一个个的抽屉,column是抽屉里的书,value就是书的内容。 +但是在pgsql中,database是一个书房,schema是一个个书柜,table是一个个抽屉,column是抽屉里的书,value就是书的内容。 + +--- +PostgreSQL 的分层结构 (Database -> Schema -> Table) 的优势: + +更好的逻辑隔离和组织: 在一个数据库内部,您可以使用 Schema 来对表、视图、函数等数据库对象进行逻辑分组。这对于大型项目、多租户应用或需要区分不同模块的数据非常有用。例如,在一个 my_app_db 数据库中,您可以有 public Schema (默认)、users Schema、orders Schema、analytics Schema 等。 +避免命名冲突: 不同的 Schema 可以包含同名的表。例如,users.accounts 和 orders.accounts 可以是两个完全不同的表,而不会冲突。这在 MySQL 中是不可能的,因为所有表都直接位于数据库下。 +权限管理: 您可以对 Schema 设置权限,控制用户对特定 Schema 内对象的访问,这提供了更细粒度的权限控制。 +数据迁移和管理: 在某些情况下,可以更容易地在 Schema 级别进行数据迁移或管理。 +MySQL 的扁平结构 (Database -> Table) 的特点: + +简单直观: 对于小型项目或初学者来说,MySQL 的结构可能更直接和易于理解,因为没有额外的 Schema 层。 +兼容性: 许多其他数据库系统(如 SQL Server)也支持 Schema,但其概念可能与 PostgreSQL 更接近,而与 MySQL 的“数据库即 Schema”有所不同。 + +--- + + + + +# User & Role + +在PostgreSQL中,存在两个容易混淆的概念:角色/用户。之所以说这两个概念容易混淆,是因为对于PostgreSQL来说,这是完全相同的两个对象。唯一的区别是在创建的时候: + +1.我用下面的psql创建了角色custom: +```sql +CREATE ROLE custom PASSWORD 'custom'; +``` + +接着我使用新创建的角色custom登录,PostgreSQL给出拒绝信息: +FATAL:role 'custom' is not permitted to log in. + +说明该角色没有登录权限,系统拒绝其登录 + +2.我又使用下面的psql创建了用户guest: +```sql +CREATE USER guest PASSWORD 'guest'; +``` +接着我使用guest登录,登录成功 + +> 难道这两者有区别吗?查看文档,又这么一段说明: +```sql +CREATE USER is the same as CREATE ROLE except that it implies LOGIN. ----CREATE USER除了默认具有LOGIN权限之外,其他与CREATE ROLE是完全相同的。 +``` + + +# 表空间 +表空间是数据库中一个逻辑上的存储单元,它将数据库的物理存储位置(磁盘上的目录或文件)抽象出来,供数据库对象(如表、索引、大对象等)使用。 + +简单来说,你可以把表空间想象成数据库在磁盘上的一个个“存储分区”或“数据仓库”。数据库管理员 (DBA) 可以创建这些“仓库”,并指定它们实际位于哪个硬盘、哪个目录下。 + +核心要点: + +逻辑与物理的桥梁: 表空间是连接数据库逻辑结构(Schema、表)与物理存储(文件系统)的桥梁。 +存储位置的抽象: 它不存储数据本身,而是定义了数据应该存储在哪个物理位置。 +管理单元: 它是 DBA 管理和优化存储资源的基本单位。 + +创建表空间 (CREATE TABLESPACE) +这是定义一个新的存储位置的第一步。您需要指定表空间的名称和它在文件系统上的物理路径。 + +```sql +-- 示例 1: 创建一个用于快速访问数据的表空间 (假设路径在 SSD 上) +CREATE TABLESPACE fast_data_ts LOCATION '/mnt/ssd_data/pg_tablespaces/fast_data'; + +-- 示例 2: 创建一个用于归档或不常用数据的表空间 (假设路径在 HDD 上) +CREATE TABLESPACE archive_data_ts LOCATION '/mnt/hdd_data/pg_tablespaces/archive_data'; + +-- 示例 3: 创建一个用于索引的专用表空间 +CREATE TABLESPACE index_ts LOCATION '/mnt/ssd_data/pg_tablespaces/indexes'; + +``` + + +重要提示: + +LOCATION 指定的路径必须是绝对路径。 +PostgreSQL 服务器进程必须对该路径拥有读写权限。 +在创建表空间之前,您需要手动在文件系统上创建这些目录(例如:mkdir -p /mnt/ssd_data/pg_tablespaces/fast_data)。 \ No newline at end of file diff --git a/src/content/posts/中间件/Redis/RedisString底层.md b/src/content/posts/中间件/Redis/RedisString底层.md new file mode 100644 index 0000000..26199ee --- /dev/null +++ b/src/content/posts/中间件/Redis/RedisString底层.md @@ -0,0 +1,17 @@ +--- +title: RedisString底层 +published: 2025-09-12 +description: ' RedisString底层 ' +image: '' +tags: ['Redis', '中间件','RedisString底层'] +category: '中间件 > Redis' +draft: false +lang: '' +--- + +# RedisString底层 + +RedisString底层是使用SDS(Simple Dynamic String)实现的,SDS是一个动态字符串,可以动态扩容。 + + +![](https://blog.meowrain.cn/api/i/2025/09/12/12boul3-1.webp) \ No newline at end of file diff --git a/src/content/posts/中间件/Redis/RedisZset实现原理.md b/src/content/posts/中间件/Redis/RedisZset实现原理.md new file mode 100644 index 0000000..d1fde9f --- /dev/null +++ b/src/content/posts/中间件/Redis/RedisZset实现原理.md @@ -0,0 +1,27 @@ +--- +title: RedisZset实现原理 +published: 2025-09-11 +description: '' +image: '' +tags: ['Redis'] +category: '中间件 > Redis' +draft: false +lang: '' +--- + +# Redis ZSet实现原理 + +是一种由跳表和哈希表组成的数据结构。ZSet结合了集合的特性和排序功能,能存储具有唯一性的成员,并且根据成员的分数进行排序。 + +由 +- 跳表: 用于存储数据的排序和快速查找 +- 哈希表: 用于存储成员和它分数的映射,提供快速查找 + + +当元素数量较少的时候,Redis采用压缩列表来节省内存。 +元素个数<=zset-max-ziplist-entries,并且每个元素的值小于zset-max-ziplist-value + +如果任何一个条件都不满足,Zset采用跳表加哈希表作为底层实现。 + + +![](https://blog.meowrain.cn/api/i/2025/09/11/12i8qms-1.webp) \ No newline at end of file diff --git a/src/content/posts/中间件/Redis/Redisson延时队列架构.md b/src/content/posts/中间件/Redis/Redisson延时队列架构.md new file mode 100644 index 0000000..e3dd50c --- /dev/null +++ b/src/content/posts/中间件/Redis/Redisson延时队列架构.md @@ -0,0 +1,173 @@ +--- +title: Redisson延时队列架构 +published: 2025-07-27 +description: '' +image: '' +tags: ['Redis','Redisson','延时队列'] +category: '中间件 > Redis' +draft: false +lang: '' +--- + +延时队列是一种特殊的消息队列,消息在发送后不会立即被消费,而是等待指定的时间后才被消费者处理。就像设置了一个"闹钟",到时间才响。 + +# 阻塞队列 RBlockingDeque - 阻塞双端队列 +特点: +- 双端: 可以从两端插入和取出元素 +- 阻塞: 当队列为空的时候,取元素会阻塞等待 +- 线程安全: 多个线程可以安全操作 + + +```java +// 特点: +// - 双端:可以从两端插入和取出元素 +// - 阻塞:当队列为空时,取元素会阻塞等待 +// - 线程安全:多个线程可以安全操作 + +RBlockingDeque deque = redissonClient.getBlockingDeque("myDeque"); +deque.offerFirst("头部元素"); +deque.offerLast("尾部元素"); +String element = deque.takeFirst(); // 阻塞获取 + +``` + +# RDelayedQueue - 延时队列 + +特点: +- 自动延时:消息在指定时间后自动变为可消费状态 +- 精确控制:可以精确控制每个消息的延时时间 +- Redis实现:基于Redis的有序集合(ZSet)实现 + +```java + +RDelayedQueue delayedQueue = redissonClient.getDelayedQueue(deque); +delayedQueue.offer("消息内容", 30, TimeUnit.SECONDS); // 30秒后可消费 +``` + +## 完整实现示例 + +### 生产者端(消息发送) +```java +@Service +public class DelayQueueProducer { + + @Autowired + private RedissonClient redissonClient; + + public void sendDelayedMessage(String message, long delaySeconds) { + try { + // 创建队列 + RBlockingDeque blockingDeque = redissonClient + .getBlockingDeque("DELAY_QUEUE_EXAMPLE"); + RDelayedQueue delayedQueue = redissonClient + .getDelayedQueue(blockingDeque); + + // 发送延时消息 + delayedQueue.offer(message, delaySeconds, TimeUnit.SECONDS); + + System.out.println("发送延时消息: " + message + + ", 延时: " + delaySeconds + "秒"); + } catch (Exception e) { + log.error("发送延时消息失败", e); + } + } +} +``` + +### 消费者端(消息处理) +```java +@Component +public class DelayQueueConsumer { + + @Autowired + private RedissonClient redissonClient; + + @PostConstruct + public void startConsumer() { + // 启动独立线程消费延时消息 + new Thread(this::consumeMessages, "DelayQueueConsumer").start(); + } + + private void consumeMessages() { + try { + RBlockingDeque blockingDeque = redissonClient + .getBlockingDeque("DELAY_QUEUE_EXAMPLE"); + + while (!Thread.currentThread().isInterrupted()) { + // 阻塞获取消息(自动等待延时到期) + String message = blockingDeque.take(); + System.out.println("消费延时消息: " + message); + + // 处理业务逻辑 + processMessage(message); + } + } catch (InterruptedException e) { + Thread.currentThread().interrupt(); + log.info("消费者线程被中断"); + } catch (Exception e) { + log.error("消费消息异常", e); + } + } + + private void processMessage(String message) { + // 实际的业务处理逻辑 + System.out.println("处理业务消息: " + message); + } +} +``` + + +## 底层实现原理 + +### Redis数据结构使用 +```bash +# Redisson使用以下数据结构: +# 1. 有序集合(ZSet) - 存储延时消息和到期时间 +ZADD delay_queue 1640995200 "message1" # 到期时间戳作为score + +# 2. 列表(List) - 存储已到期可消费的消息 +LPUSH ready_queue "message1" + +# 3. 定时任务 - 定期检查到期消息 +# Redisson内部使用定时任务扫描ZSet,将到期消息移动到List +``` + +### 延时检查机制 +```java +// Redisson内部逻辑(简化版) +public class DelayedQueueChecker { + public void checkExpiredMessages() { + long now = System.currentTimeMillis(); + + // 从有序集合中获取已到期的消息 + Set expiredMessages = redisTemplate + .opsForZSet() + .rangeByScore("delay_queue", 0, now); + + for (String message : expiredMessages) { + // 移动到可消费队列 + redisTemplate.opsForList().leftPush("ready_queue", message); + redisTemplate.opsForZSet().remove("delay_queue", message); + } + } +} +``` +## 使用场景 + +```java +// 1. 订单超时处理 +public void handleOrderTimeout(String orderId) { + delayedQueue.offer(orderId, 30, TimeUnit.MINUTES); +} + +// 2. 优惠券到期提醒 +public void couponExpireReminder(String couponId) { + delayedQueue.offer(couponId, 24, TimeUnit.HOURS); +} + +// 3. 消息重试机制 +public void messageRetry(String messageId) { + delayedQueue.offer(messageId, 5, TimeUnit.SECONDS); // 5秒后重试 +} +``` + diff --git a/src/content/posts/中间件/Redis/Redis中常见的数据类型有哪些.md b/src/content/posts/中间件/Redis/Redis中常见的数据类型有哪些.md new file mode 100644 index 0000000..024f70d --- /dev/null +++ b/src/content/posts/中间件/Redis/Redis中常见的数据类型有哪些.md @@ -0,0 +1,91 @@ +--- +title: Redis中常见的数据类型有哪些 +published: 2025-09-10 +description: 'Redis中常见的数据类型有哪些' +image: '' +tags: [Redis, 中间件, Redis数据类型] +category: '中间件 > Redis' +draft: false +lang: '' +--- +https://www.mianshiya.com/question/1780933295593254915 + +# Redis中常见的数据类型有哪些 +常见的五种数据结构 + +5种数据类型示意图 +![](https://blog.meowrain.cn/api/i/2025/09/10/suhd1q-1.webp) + +# String +字符串是Redis种最基本的数据类型,可以存储任何类型的数据,包括文本,数字和二进制数据,最大长度是512MB + +使用场景: +- 缓存: 存储临时数据,比如用户会话,页面缓存 +- 计数器: 用于统计访问量,点赞数等,通过原子操作增加或者减少 +- 分布式锁: 用于分布式锁,通过原子操作设置和释放锁 + +![](https://blog.meowrain.cn/api/i/2025/09/10/su1qk8-1.webp) +![](https://blog.meowrain.cn/api/i/2025/09/10/suonmx-1.webp) +![](https://blog.meowrain.cn/api/i/2025/09/10/suzbio-1.webp) +![](https://blog.meowrain.cn/api/i/2025/09/10/sv0s0r-1.webp) +![](https://blog.meowrain.cn/api/i/2025/09/10/sv2leb-1.webp) +![](https://blog.meowrain.cn/api/i/2025/09/10/sv4dpg-1.webp) + + +# List +列表是有序的字符串集合,支持从两端推入和弹出元素,底层实现是双向链表 + +使用场景: +- 消息队列: 用于简单任务调度,消息传递场景,通过LPUSH和RPOP操作实现生产者和消费者模式 +- 历史记录: 存储用户操作的历史记录,便于快速访问。 +![](https://blog.meowrain.cn/api/i/2025/09/10/svjzbo-1.webp) +![](https://blog.meowrain.cn/api/i/2025/09/10/svm8vj-1.webp) +![](https://blog.meowrain.cn/api/i/2025/09/10/svoe51-1.webp) +![](https://blog.meowrain.cn/api/i/2025/09/10/svqxit-1.webp) + + + +# Set +集合是无需而且不重复的字符串集合,使用哈希表实现,支持快速查找和去重操作。 +使用场景: +- 标签: 用于存储标签,便于快速查找 +- 集合运算: 用于存储集合,便于进行集合运算,如交集,差集,并集等 + +![](https://blog.meowrain.cn/api/i/2025/09/10/sxh62n-1.webp) +![](https://blog.meowrain.cn/api/i/2025/09/10/sxjlh8-1.webp) +![](https://blog.meowrain.cn/api/i/2025/09/10/sxlmx2-1.webp) +![](https://blog.meowrain.cn/api/i/2025/09/10/sxn9zd-1.webp) +![](https://blog.meowrain.cn/api/i/2025/09/10/sxxtmw-1.webp) +![](https://blog.meowrain.cn/api/i/2025/09/10/sxzjm2-1.webp) + +![](https://blog.meowrain.cn/api/i/2025/09/10/sy11fg-1.webp) +# ZSet +有序集合是按分数排序的字符串集合,使用跳表实现,支持快速查找和范围查询。 +使用场景: +- 排行榜: 用于存储排行榜,便于快速查找 +![](https://blog.meowrain.cn/api/i/2025/09/10/sy75as-1.webp) +![](https://blog.meowrain.cn/api/i/2025/09/10/sy8xwf-1.webp) +![](https://blog.meowrain.cn/api/i/2025/09/10/sympbg-1.webp) +![](https://blog.meowrain.cn/api/i/2025/09/10/syqfgo-1.webp) +![](https://blog.meowrain.cn/api/i/2025/09/10/sysm37-1.webp) +![](https://blog.meowrain.cn/api/i/2025/09/10/syuiyd-1.webp) +![](https://blog.meowrain.cn/api/i/2025/09/10/sz50a7-1.webp) +![](https://blog.meowrain.cn/api/i/2025/09/10/sz7ftu-1.webp) +![](https://blog.meowrain.cn/api/i/2025/09/10/sz8r8r-1.webp) + +# Hash +哈希是键值对的集合,使用哈希表实现,支持快速查找和存储对象。 +使用场景: +- 对象存储: 可以用来缓存对象,比如用户信息,商品信息等 + +![](https://blog.meowrain.cn/api/i/2025/09/10/sw5vdr-1.webp) +![](https://blog.meowrain.cn/api/i/2025/09/10/sw8g23-1.webp) +![](https://blog.meowrain.cn/api/i/2025/09/10/swc1j3-1.webp) +![](https://blog.meowrain.cn/api/i/2025/09/10/sweoot-1.webp) +![](https://blog.meowrain.cn/api/i/2025/09/10/swzinu-1.webp) + +![](https://blog.meowrain.cn/api/i/2025/09/10/sxei2s-1.webp) + + +# 其它数据结构 +![](https://blog.meowrain.cn/api/i/2025/09/10/szef96-1.webp) \ No newline at end of file diff --git a/src/content/posts/中间件/Redis/Redis为什么快.md b/src/content/posts/中间件/Redis/Redis为什么快.md new file mode 100644 index 0000000..7f054f0 --- /dev/null +++ b/src/content/posts/中间件/Redis/Redis为什么快.md @@ -0,0 +1,31 @@ +--- +title: Redis为什么快 +published: 2025-09-10 +description: '' +image: 'https://blog.meowrain.cn/api/i/2025/07/19/p9zr81-1.webp' +tags: [Redis, 中间件] +category: '中间件 > Redis' +draft: false +lang: '' +--- + +# Redis为什么快 + +1. 使用内存存储 +2. Redis采用了IO多路复用技术的事件驱动模型来处理客户端请求,执行Redis命令 +3. Redis6.0引入多线程机制,把网络和I/O处理放到多个线程中,减少了单线程的瓶颈,网络IO交给线程池处理,命令仍然在主线程中进行。充分利用CPU多核的优势,提升了性能。 + ![](https://blog.meowrain.cn/api/i/2025/07/19/sbddqg-1.webp) +4. Redis 对底层数据结构做了极致的优化,比如说 String 的底层数据结构动态字符串支持动态扩容、预分配冗余空间,能够减少内存碎片和内存分配的开销。 + + + +1.基于内存,内存IO比磁盘快 +2. 采用 单线程 + 非阻塞 I/O + I/O 多路复用(高效处理并发)模型 +3. 网络IO用上了多线程 +4. 底层数据结构被专门优化过 + +- **单线程**:单线程可以避免多线程的数据竞争和上下文切换开销。 +- **非阻塞IO**,Redis对客户端连接的I/O操作设置为非阻塞,主线程发起I/O操作后,不需要等待结果返回,可以继续处理其它事件。 +- **I/O多路复用**: 主线程可以同时监听多个客户端连接的I/O事件,一旦某个事件就绪,在进行集中处理。 +- + diff --git a/src/content/posts/中间件/Redis/Redis主从复制的实现原理是什么.md b/src/content/posts/中间件/Redis/Redis主从复制的实现原理是什么.md new file mode 100644 index 0000000..a697f49 --- /dev/null +++ b/src/content/posts/中间件/Redis/Redis主从复制的实现原理是什么.md @@ -0,0 +1,33 @@ +--- +title: Redis主从复制的实现原理是什么 +published: 2025-09-17 +description: '' +image: '' +tags: [主从复制,中间件,Redis] +category: '中间件 > Redis' +draft: false +lang: '' +--- + +# 为什么需要主从复制 +1. 降低数据冗余 +2. 提高故障恢复 +3. 支持负载均衡 +4. 高可用 + + +# 主从库之间采用读写分离方式: +![](https://blog.meowrain.cn/api/i/2025/09/17/upct3j-1.webp) + + +# 两种同步方式 +1. 全量复制: 第一次同步的时候 +2. 增量复制: 只会把主从库网络断联期间主库收到的命令同步给从库 + +## 全量复制 +![](https://blog.meowrain.cn/api/i/2025/09/17/w9rfaa-1.webp) + + +## 增量复制 + +![](https://blog.meowrain.cn/api/i/2025/09/17/w9vq4z-1.webp) \ No newline at end of file diff --git a/src/content/posts/中间件/Redis/Redis大Key问题.md b/src/content/posts/中间件/Redis/Redis大Key问题.md new file mode 100644 index 0000000..e596a1a --- /dev/null +++ b/src/content/posts/中间件/Redis/Redis大Key问题.md @@ -0,0 +1,31 @@ +--- +title: Redis大Key问题 +published: 2025-09-15 +description: '' +image: '' +tags: ['中间件','Redis','大Key'] +category: '中间件 > Redis' +draft: false +lang: '' +--- + +![](https://blog.meowrain.cn/api/i/2025/09/15/ucp5bt-1.webp) + + +Redis中的 big key指一个内存空间占用比较大的键,它有什么危害? +- 内存分布不均,集群模式下,不同slot分配到不同实例中,如果大key被映射到同一个实例,那么分布不均,查询效率也会受到影响。 +- Redis单线程执行命令,操作大Key的时候耗时比较长,会导致Redis出现其他命令 阻塞的问题。 +- 大key对资源占用很大,在进行网络I/O传输的时候,导致获取过程中产生的网络流量较大,从而产生网络传输时间延长甚至网络传输发生阻塞现象。 +- 客户端超时,因为操作大key时的时耗比较长,所以可能导致客户端超时。 + + +如何解决大Key问题? +- 开发方面,对要存储的数据进行压缩,压缩之后再存储。 +- 大化小,大对象拆分成小对象。把一个大Key拆分成若干小key,降低单个Key的内存大小。 +- 使用合适的数据结构进行存储,比如一些用String存储的场景,可以考虑用Hash,Set等结构进行优化 + +业务层面: +根据实际情况,调整存储策略,不要把不必要的信息存储到里面 + +数据分布方面 +- 采用Redis集群方式进行Redis的部署,然后把大Key拆分散落到不同的服务器上面,加快响应速度。 diff --git a/src/content/posts/中间件/Redis/Redis如何实现分布式锁.md b/src/content/posts/中间件/Redis/Redis如何实现分布式锁.md new file mode 100644 index 0000000..b05408e --- /dev/null +++ b/src/content/posts/中间件/Redis/Redis如何实现分布式锁.md @@ -0,0 +1,42 @@ +--- +title: Redis如何实现分布式锁 +published: 2025-09-13 +description: '' +image: '' +tags: ['Redis','中间件','分布式锁'] +category: '中间件 > Redis' +draft: false +lang: '' +--- + + +# 分布式锁 +在Redis中实现分布式锁的常见方法是通过 set ex nx命令+lua脚本组合使用,确保多个客户端不会获得同一个资源锁的同时,也保证了安全解锁和意外情况下锁的自动释放。 + +## 理解Redis实现的分布式锁 +如果基于Redis来实现分布式锁,需要使用set ex nx命令+ lua脚本 + +加锁: `SET lock_key uniqueValue EX expiretime NX` +解锁: 使用lua脚本,先get获取key的value判断锁是否是自己加的,如果是则del +```lua +if redis.call("GET",KEYS[1]) == ARGV[1] +then + return redis.call("DEL",KEYS[1]) +else + return 0 +end +``` + +锁需要有过期机制,假设某个客户端加锁后宕机了,锁没设置过期机制,就会让其他客户端抢不到锁。 + +EX expiretime 设置的单位是秒,PX expiretime设置的是毫秒 + +上面为啥要用`uniqueValue`呢,这个就是唯一的值,是为了防止锁被其他客户端释放掉。 + +## 实现分布式锁的步骤 +1. 加锁:使用`SET lock_key uniqueValue EX expiretime NX`命令加锁 +2. 解锁:使用lua脚本,先get获取key的value判断锁是否是自己加的,如果是则del +3. 锁需要有过期机制,假设某个客户端加锁后宕机了,锁没设置过期机制,就会让其他客户端抢不到锁。 +4. EX expiretime 设置的单位是秒,PX expiretime设置的是毫秒 +5. 上面为啥要用`uniqueValue`呢,这个就是唯一的值,是为了防止锁被其他客户端释放掉。 +6. 锁需要有过期机制,假设某个客户端加锁后宕机了,锁没设置过期机制,就会让其他客户端抢不到锁。 diff --git a/src/content/posts/中间件/Redis/Redis实现分布式锁的时候可能遇到的问题有哪些.md b/src/content/posts/中间件/Redis/Redis实现分布式锁的时候可能遇到的问题有哪些.md new file mode 100644 index 0000000..ae4500f --- /dev/null +++ b/src/content/posts/中间件/Redis/Redis实现分布式锁的时候可能遇到的问题有哪些.md @@ -0,0 +1,11 @@ +--- +title: Redis实现分布式锁的时候可能遇到的问题有哪些 +published: 2025-09-13 +description: '' +image: '' +tags: ['Redis','中间件','分布式锁'] +category: '中间件 > Redis' +draft: false +lang: '' +--- +![](https://blog.meowrain.cn/api/i/2025/09/13/lqg62s-1.webp) \ No newline at end of file diff --git a/src/content/posts/中间件/Redis/Redis持久化机制.md b/src/content/posts/中间件/Redis/Redis持久化机制.md new file mode 100644 index 0000000..3df3877 --- /dev/null +++ b/src/content/posts/中间件/Redis/Redis持久化机制.md @@ -0,0 +1,69 @@ +--- +title: Redis持久化机制 +published: 2025-09-17 +description: '' +image: '' +tags: [] +category: '中间件 > Redis' +draft: false +lang: '' +--- +![](https://blog.meowrain.cn/api/i/2025/09/17/umd7ep-1.webp) + +# Redis 持久化机制 +# Redis和Memcached的不同 +Redis 不同于 Memcached 的很重要一点就是,Redis 支持持久化,而且支持 3 种持久化方式: +- 快照(snapshotting,RDB) +- 只追加文件(append-only file, AOF) +- RDB 和 AOF 的混合持久化(Redis 4.0 新增) + + +Redis支持丰富的数据结构,Memcached仅仅支持简单的键值对存储,而且只能是字符串 + + +![](https://blog.meowrain.cn/api/i/2025/02/22/EXaKzG1740216393513470290.avif) + +# RDB持久化 +Redis 可以通过创建快照来获得存储在内存里面的数据在 某个时间点 上的副本。Redis 创建快照之后,可以对快照进行备份,可以将快照复制到其他服务器从而创建具有相同数据的服务器副本(Redis 主从结构,主要用来提高 Redis 性能),还可以将快照留在原地以便重启服务器的时候使用。 + +快照持久化是 Redis 默认采用的持久化方式,在 redis.conf 配置文件中默认有此下配置: +``` +save 900 1 #在900秒(15分钟)之后,如果至少有1个key发生变化,Redis就会自动触发bgsave命令创建快照。 + +save 300 10 #在300秒(5分钟)之后,如果至少有10个key发生变化,Redis就会自动触发bgsave命令创建快照。 + +save 60 10000 #在60秒(1分钟)之后,如果至少有10000个key发生变化,Redis就会自动触发bgsave命令创建快照。 +``` +# RDB 创建快照时会阻塞主线程吗? +![](https://blog.meowrain.cn/api/i/2025/02/22/IzY7nu1740216454633699951.avif) + +# 什么是 AOF 持久化? +与快照持久化相比,AOF 持久化的实时性更好。默认情况下 Redis 没有开启 AOF(append only file)方式的持久化(Redis 6.0 之后已经默认是开启了),可以通过 appendonly 参数开启: +``` +appendonly yes +``` +开启 AOF 持久化后每执行一条会更改 Redis 中的数据的命令,Redis 就会将该命令写入到 AOF 缓冲区 server.aof_buf 中,然后再写入到 AOF 文件中(此时还在系统内核缓存区未同步到磁盘),最后再根据持久化方式( fsync策略)的配置来决定何时将系统内核缓存区的数据同步到硬盘中的。 + +只有同步到磁盘中才算持久化保存了,否则依然存在数据丢失的风险,比如说:系统内核缓存区的数据还未同步,磁盘机器就宕机了,那这部分数据就算丢失了。 + +AOF 文件的保存位置和 RDB 文件的位置相同,都是通过 dir 参数设置的,默认的文件名是 appendonly.aof。 + +AOF持久化功能的实现: + +1. 命令追加 +2. 文件写入 (写到内核缓冲区了,还没同步到硬盘) +3. 文件同步 (根据持久化方式向硬盘做同步操作) +4. 文件重写: 随着AOF文件越来越大,需要定期对AOF文件进行重写,达到压缩目的。 +5. 重启加载: Redis重启的时候,可以加载AOF文件进行数据恢复。 + +![](https://blog.meowrain.cn/api/i/2025/02/22/XSEcK61740216646975756864.avif) + + +# AOF 持久化方式有哪些? +![](https://blog.meowrain.cn/api/i/2025/02/22/DZSVso1740216693705553035.avif) + + +# AOF 为什么是在执行完命令之后记录日志 +![](https://blog.meowrain.cn/api/i/2025/02/22/3nEOWT1740216736909899485.avif) + + diff --git a/src/content/posts/中间件/Redis/Redis数据过期后的删除策略.md b/src/content/posts/中间件/Redis/Redis数据过期后的删除策略.md new file mode 100644 index 0000000..13c9571 --- /dev/null +++ b/src/content/posts/中间件/Redis/Redis数据过期后的删除策略.md @@ -0,0 +1,34 @@ +--- +title: Redis数据过期后的删除策略 +published: 2025-09-17 +description: '' +image: '' +tags: [Redis,删除策略,缓存过期] +category: '中间件 > Redis' +draft: false +lang: '' +--- + +![](https://blog.meowrain.cn/api/i/2025/09/17/10egvdw-1.webp) + +Redis数据过期主要有两种删除策略 + +![](https://blog.meowrain.cn/api/i/2025/09/17/10hp6uw-1.webp) +## 定期删除 +Redis每隔一段时间会随机检查一定数量的键,如果发现过期的键,就把它删除。这种方式能够在后台持续清除过期数据,防止内存膨胀。 + +缺点: CPU占用稍微有点儿大 +优点:能及时清除过期的键,防止内存膨胀。 +## 惰性删除 + +在每次访问键的时候,去看这个键是不是已经过期了,如果过期了就删除它。 +这种策略保证了在使用过程中只删除不再需要的数据,但在不访问过期键的时候不会被清除。 + +优点: 减少CPU占用 +缺点: 如果一直没查到某个key,这个键就可能不会被删除,时间久了可能导致内存膨胀。 + +# 内存淘汰策略 +![](https://blog.meowrain.cn/api/i/2025/09/17/10iw8w4-1.webp) + +# Redis键过期时间的设置 +![](https://blog.meowrain.cn/api/i/2025/09/17/10f1n1j-1.webp) \ No newline at end of file diff --git a/src/content/posts/中间件/Redis/Redis有哪些数据类型.md b/src/content/posts/中间件/Redis/Redis有哪些数据类型.md new file mode 100644 index 0000000..7b092d2 --- /dev/null +++ b/src/content/posts/中间件/Redis/Redis有哪些数据类型.md @@ -0,0 +1,102 @@ +--- +title: Redis有哪些数据类型 +published: 2025-07-19 +description: '' +image: 'https://blog.meowrain.cn/api/i/2025/07/19/p9zr81-1.webp' +tags: [Redis, 中间件, Redis数据类型] +category: '中间件 > Redis' +draft: false +lang: '' +--- + + +# 官方文档 + +[Redis官方文档](https://redis.io/docs/latest/develop/data-types/) +![](https://blog.meowrain.cn/api/i/2025/07/19/pesc98-1.webp) + +![JavaBetter](https://javabetter.cn/sidebar/sanfene/redis.html#_3-%F0%9F%8C%9Fredis%E6%9C%89%E5%93%AA%E4%BA%9B%E6%95%B0%E6%8D%AE%E7%B1%BB%E5%9E%8B) + +# 基本数据类型 + +Redis支持五种基本数据类型 + +## 字符串 + +![](https://blog.meowrain.cn/api/i/2025/07/19/pfuo04-1.webp) + +![Redis数据类型-字符串](https://redis.io/docs/latest/develop/data-types/#strings) +![详细文档](https://redis.io/docs/latest/develop/data-types/strings/) + + 字符串是最基本的数据类型,可以存储文本,数字或者二进制数据,最大的容量是512MB。适合缓存单个对象,比如验证码,token,计数器等。 + +## 列表 + +![](https://blog.meowrain.cn/api/i/2025/07/19/phht75-1.webp) + +![Redis数据类型-列表](https://redis.io/docs/latest/develop/data-types/#lists) +![详细文档](https://redis.io/docs/latest/develop/data-types/lists/) + + 列表是一个有序的字符串集合,可以在头部或尾部插入元素,适合用于消息队列,任务调度等场景。 + +## 哈希 + +![](https://blog.meowrain.cn/api/i/2025/07/19/pldpst-1.webp) + +![Redis数据类型-哈希](https://redis.io/docs/latest/develop/data-types/#hashes) +![详细文档](https://redis.io/docs/latest/develop/data-types/hashes/) + + 哈希是一个键值对集合,适合用于存储对象。可以通过字段名快速访问字段值,支持对单个字段的操作,节省内存。 + +## 集合 + +![](https://blog.meowrain.cn/api/i/2025/07/19/plo11d-1.webp) + +![Redis数据类型-集合](https://redis.io/docs/latest/develop/data-types/#sets) +![详细文档](https://redis.io/docs/latest/develop/data-types/sets/) + + 集合是一个无序的字符串集合,支持快速的成员查找,适合用于标签,好友关系等场景。 + 可以进行集合运算,如交集,差集,并集等。 + 平常拿来做一些去重操作。 + +## 有序集合 + +![](https://blog.meowrain.cn/api/i/2025/07/19/plwl0i-1.webp) + +![Redis数据类型-有序集合](https://redis.io/docs/latest/develop/data-types/#sorted-sets) +![详细文档](https://redis.io/docs/latest/develop/data-types/sorted-sets/) + + 有序集合是一个有序的字符串集合,每个元素都有一个分数,支持根据分数进行范围查询,适合用于排行榜,消息队列等场景。 + +![](https://blog.meowrain.cn/api/i/2025/07/19/pmk3s0-1.webp) + +![](https://blog.meowrain.cn/api/i/2025/07/19/pn3s6u-1.webp) + +# 扩展数据类型 + +[redis3种特殊类型详解](https://pdai.tech/md/db/nosql-redis/db-redis-data-type-special.html#redis%E5%85%A5%E9%97%A8---%E6%95%B0%E6%8D%AE%E7%B1%BB%E5%9E%8B3%E7%A7%8D%E7%89%B9%E6%AE%8A%E7%B1%BB%E5%9E%8B%E8%AF%A6%E8%A7%A3) + +## 位图bitmap + +[详细文档](https://redis.io/docs/latest/develop/data-types/bitmaps/) + +![](https://blog.meowrain.cn/api/i/2025/07/19/rfx8t3-1.webp) + +位图是一个特殊的字符串类型,用于存储二进制位。可以用来统计用户活跃度,签到等场景。 + +## 基数统计HyperLogLog + +[详细文档](https://redis.io/docs/latest/develop/data-types/probabilistic/hyperloglogs/) + +![](https://blog.meowrain.cn/api/i/2025/07/19/rek2t9-1.webp) +![](https://blog.meowrain.cn/api/i/2025/07/19/repxn1-1.webp) + +基数统计通常用于统计不重复的元素数量,比如网站访问量,用户注册量等。 + +## 地理位置Geo + +存储地理信息 + +## Bloom Filter + +[详细文档](https://redis.io/docs/latest/develop/data-types/probabilistic/bloom-filter/) diff --git a/src/content/posts/中间件/Redis/Redis的Hash是什么.md b/src/content/posts/中间件/Redis/Redis的Hash是什么.md new file mode 100644 index 0000000..c91903b --- /dev/null +++ b/src/content/posts/中间件/Redis/Redis的Hash是什么.md @@ -0,0 +1,51 @@ +--- +title: Redis的Hash是什么 +published: 2025-09-11 +description: '' +image: '' +tags: ['Redis'] +category: '中间件 > Redis' +draft: false +lang: '' +--- + +# Redis的Hash是什么 + + +Redis的Hash是一种键值对集合,可以把多个字段和值存储在同一个键中,便于管理一些关联数据 + +比如一个用户信息,可以存储在Hash中,key为user:1,value为name:张三,age:18,gender:男 +命令: HSET user:1 name 张三 age 18 gender 男 + +适合存储小数据,能够在内存中高效存储和操作 +支持快速字段操作,比如增删改查,适合存储对象属性 + +![](https://blog.meowrain.cn/api/i/2025/09/11/111qad1-1.webp) +![](https://blog.meowrain.cn/api/i/2025/09/11/111rxfa-1.webp) + +# Hash底层 +Hash是Redis中的一种数据基础数据结构,类似于数据结构中的哈希表,一个Hash可以存储2的32次方-1个键值对(差不多40亿)。底层结构按照Redis版本分成两种情况: +- Redis6.0之前,Hash的底层是压缩列表加上哈希表的数据结构(ziplist + hashtable) +- Redis7之后,Hash的底层是紧凑列表(Listpack)加上哈希表的数据结构(Listpack + hashtable) + +ziplist和listpack的效率差不多,时间复杂度都是O(n) +但是listpack解决了ziplist的级联更新问题 + + + +![](https://blog.meowrain.cn/api/i/2025/09/11/114aurs-1.webp) + + +hash-max-ziplist-entries 512:这是 Hash 类型使用 ziplist(压缩列表)编码的最大条目数阈值。当 Hash 的字段-值对数量 ≤ 512 时,Redis 会优先使用 ziplist 进行内存高效存储,以减少开销。 +hash-max-ziplist-value 64:这是每个字段名(key)和值(value)的最大字节长度阈值。当所有字段名和值的字节长度均 ≤ 64 字节时,结合条目数阈值,Hash 会被编码为 ziplist。 +优化目的:ziplist 是一种紧凑的序列化存储方式,能显著降低内存使用(平均节省 5 倍,最高 10 倍),但当超过阈值时,Redis 会自动转换为标准哈希表(hashtable),以保证性能。 +Redis 版本注意:在 Redis 7.0 中,ziplist 被 listpack 取代(后者是其改进版),配置参数也相应更新为 hash-max-listpack-entries 和 hash-max-listpack-value,但默认值和行为保持一致。为了向后兼容,旧参数仍被支持作为别名。你的描述适用于 7.0 及更早版本。 + + +# Hashtable +当Hash的键值对数量超过hash-max-ziplist-entries或者键和值的长度大于hash-max-ziplist-value时,Redis会自动转换为Hashtable + +Hashtable其实就是哈希表实现,查询时间复杂度是O(1),效率很快 + +# rehash +![](https://blog.meowrain.cn/api/i/2025/09/11/11b5y0t-1.webp) \ No newline at end of file diff --git a/src/content/posts/中间件/Redis/Redis缓存击穿缓存穿透缓存雪崩.md b/src/content/posts/中间件/Redis/Redis缓存击穿缓存穿透缓存雪崩.md new file mode 100644 index 0000000..3ed83f6 --- /dev/null +++ b/src/content/posts/中间件/Redis/Redis缓存击穿缓存穿透缓存雪崩.md @@ -0,0 +1,32 @@ +--- +title: Redis缓存击穿缓存穿透缓存雪崩 +published: 2025-09-12 +description: '' +image: '' +tags: ['Redis', '中间件','缓存击穿'] +category: '中间件 > Redis' +draft: false +lang: '' +--- + +# 缓存击穿 +缓存击穿说的是,某个热点数据在缓存中失效,导致大量请求打到数据库。这时候由于瞬间的高并发,可能导致数据库崩溃。 +## 解决方案 +- 分布式锁: 在获取缓存的时候,使用分布式锁,保证只有一个请求获取到缓存。这样的话,就不会因为热点数据过期失效,而让请求都打到数据库里了。 +- 热点数据永不过期 + +# 缓存穿透 +缓存穿透说的是,某个热点数据在缓存中不存在,导致大量请求打到数据库。这时候由于瞬间的高并发,可能导致数据库崩溃。 + +## 解决方案 +- 使用布隆过滤器,布隆过滤器对于不存在的数据是能100%判断的,所以用布隆过滤器能过滤掉不存在的请求。 +- 对查询的空结果进行缓存,第二次访问直接返回空值。 + +# 缓存雪崩 + +多个缓存数据在同一时刻过期,导致大量请求同时访问数据库,导致数据库崩溃。 + +## 解决方案 +- 采用随机时间过期策略,避免多个数据同时过期。 +- 使用双缓存策略,把数据存储在两层缓存里面,减少数据库的直接请求。 + diff --git a/src/content/posts/中间件/Redis/Redis跳表.md b/src/content/posts/中间件/Redis/Redis跳表.md new file mode 100644 index 0000000..1dd6235 --- /dev/null +++ b/src/content/posts/中间件/Redis/Redis跳表.md @@ -0,0 +1,29 @@ +--- +title: Redis跳表 +published: 2025-09-11 +description: '' +image: '' +tags: ['Redis'] +category: '中间件 > Redis' +draft: false +lang: '' +--- + +# Redis跳表 + +https://www.cnblogs.com/yfceshi/p/19079750 + +![](https://blog.meowrain.cn/api/i/2025/09/11/126uk28-1.webp) + + + +## 跳表的应用场景 +Redis 中的有序集合(Sorted Set,zset) + +用跳表实现的,可以敏捷完成范围查询和排序。 + +内存数据库 / 搜索引擎 + +用来做索引,加速查找。 + +![](https://blog.meowrain.cn/api/i/2025/09/11/128jwcb-1.webp) \ No newline at end of file diff --git a/src/content/posts/中间件/Redis/redis配置序列化.md b/src/content/posts/中间件/Redis/redis配置序列化.md new file mode 100644 index 0000000..5decf69 --- /dev/null +++ b/src/content/posts/中间件/Redis/redis配置序列化.md @@ -0,0 +1,157 @@ +--- +title: redis配置json序列化 +published: 2025-08-08 +description: '' +image: 'https://blog.meowrain.cn/api/i/2025/07/19/p9zr81-1.webp' +tags: [Redis, 中间件, Redis数据类型] +category: '中间件 > Redis' +draft: false +lang: '' +--- + +# Redis配置序列化 +序列化的最终目的是为了对象可以跨平台存储,进行网络传输。 +Redis默认用的是JdkSerializationRedisSerializer,它使用JDK提供的序列化功能,优点是反序列化的时候不需要提供类型信息,但缺点是序列化后的数据体积较大,性能较低。 +因此,通常会使用更高效的序列化方式,如JSON、Protobuf等 + +Jackson2JsonRedisSerializer: 使用Jackson库将对象序列化为JSON字符串。 +优点是速度快,序列化后的字符串短小精悍,不需要实现Serializable接口。 + +但缺点也非常致命,那就是此类的构造函数中有一个类型参数,必须提供要序列化对象的类型信息(.class对象)。 通过查看源代码,发现其只在反序列化过程中用到了类型信息。 + +现在的问题是: 如果使用默认的JDK序列化方式,在Redis可视化工具中查看kv值的时候会出现乱码,而使用Jackson2JsonRedisSerializer序列化后,kv值在Redis可视化工具中查看时是正常的。 + + +StringRedisTemplate → Key 和 Value 都是 String 序列化,简单粗暴,适合存验证码、token、计数器、纯 JSON 文本之类的轻量数据。 + +自定义 RedisTemplate → 用了 JSON 序列化器,直接存 Java 对象,取出来就能反序列化成原类型,适合缓存业务对象、集合、复杂结构等。 + + +# 配置 +```java + +package org.example.config; + + +import com.fasterxml.jackson.annotation.JsonAutoDetect; +import com.fasterxml.jackson.annotation.PropertyAccessor; +import com.fasterxml.jackson.databind.ObjectMapper; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.data.redis.connection.RedisConnectionFactory; +import org.springframework.data.redis.core.RedisTemplate; +import org.springframework.data.redis.serializer.GenericJackson2JsonRedisSerializer; +import org.springframework.data.redis.serializer.StringRedisSerializer; + +@Configuration +public class MyRedisConfig { + + /** + * RedisTemplate 配置类 + * - 使用自定义的 ObjectMapper + GenericJackson2JsonRedisSerializer + * - 实现 key 用字符串序列化,value 用 JSON 序列化 + */ + @Bean + public RedisTemplate redisTemplate(RedisConnectionFactory factory) { + // 创建 RedisTemplate,指定 key 类型为 String,value 类型为 Object + RedisTemplate template = new RedisTemplate<>(); + + // 设置 Redis 连接工厂(由 Spring Boot 配置的连接信息决定) + template.setConnectionFactory(factory); + + // 创建并配置 Jackson 的 ObjectMapper(用于 JSON 序列化/反序列化) + ObjectMapper mapper = new ObjectMapper(); + + // 设置可见性:让所有字段(包括 private)都参与序列化和反序列化 + mapper.setVisibility(PropertyAccessor.ALL, JsonAutoDetect.Visibility.ANY); + + // 启用默认类型信息(多态类型序列化) + // NON_FINAL 表示对所有非 final 类(如普通类、接口实现类)写入类型信息 + // 好处:反序列化时可以还原原始对象类型(避免反成 LinkedHashMap) + mapper.activateDefaultTyping( + mapper.getPolymorphicTypeValidator(), // 类型验证器,防止反序列化攻击 + ObjectMapper.DefaultTyping.NON_FINAL // 应用于所有非 final 的类 + ); + + // 创建 JSON 序列化器,并注入自定义的 ObjectMapper + GenericJackson2JsonRedisSerializer serializer = + new GenericJackson2JsonRedisSerializer(mapper); + + // key 采用字符串序列化器,保证可读性(在 Redis CLI 中能直接看到) + template.setKeySerializer(new StringRedisSerializer()); + // value 采用 JSON 序列化器,支持存储任意对象 + template.setValueSerializer(serializer); + + // hash 结构的 key 也用字符串序列化 + template.setHashKeySerializer(new StringRedisSerializer()); + // hash 结构的 value 也用 JSON 序列化 + template.setHashValueSerializer(serializer); + + // 初始化 RedisTemplate 的配置 + template.afterPropertiesSet(); + + return template; + } + +} +``` + +> 什么是反序列化攻击? +反序列化攻击是指攻击者通过构造恶意的序列化数据,利用应用程序在反序列化过程中执行不安全的代码或操作,从而导致安全漏洞。攻击者可以通过发送特制的序列化数据包,触发应用程序执行未授权的操作、获取敏感信息或执行任意代码。 + + + # 使用 +```java +package org.example; + +import org.example.entity.Human; +import org.springframework.boot.WebApplicationType; +import org.springframework.boot.autoconfigure.SpringBootApplication; +import org.springframework.boot.builder.SpringApplicationBuilder; +import org.springframework.context.ApplicationContext; +import org.springframework.data.redis.core.RedisTemplate; + +@SpringBootApplication +public class Main { + public static void main(String[] args) { + ApplicationContext ctx = new SpringApplicationBuilder(Main.class) + .web(WebApplicationType.NONE) // 不启动 Tomcat + .run(args); + + // 按名字拿,避免和 stringRedisTemplate 冲突 + RedisTemplate redisTemplate = + (RedisTemplate) ctx.getBean("redisTemplate"); + + // 打印一下序列化器,确认确实是你配置的 + System.out.println("KeySerializer = " + redisTemplate.getKeySerializer().getClass().getName()); + System.out.println("ValueSerializer = " + redisTemplate.getValueSerializer().getClass().getName()); + + String key = "test:human:" + System.currentTimeMillis(); + Human h = new Human("jackv", "dfasdfssfsdf"); + + redisTemplate.opsForValue().set(key, h); + Object v = redisTemplate.opsForValue().get(key); + + System.out.println("Fetched value class = " + (v == null ? "null" : v.getClass().getName())); + System.out.println("Fetched value = " + v); + + // 简单校验:能拿回对象、类型是你期望的 + if (!(v instanceof Human)) { + throw new IllegalStateException("不是 Human,而是:" + (v == null ? "null" : v.getClass())); + } + // 可选:清理 + redisTemplate.delete(key); + + System.out.println("OK, JSON 序列化/反序列化正常。"); + } +} + +``` + +在其他类的时候直接@Autowired注入RedisTemplate即可使用。 + +```java +@Autowired +@Qualifier("redisTemplate") +private RedisTemplate redisTemplate; +``` \ No newline at end of file diff --git a/src/content/posts/中间件/Redis/为什么Redis设计为单线程而后面版本为啥又引入多线程呢.md b/src/content/posts/中间件/Redis/为什么Redis设计为单线程而后面版本为啥又引入多线程呢.md new file mode 100644 index 0000000..ec94533 --- /dev/null +++ b/src/content/posts/中间件/Redis/为什么Redis设计为单线程而后面版本为啥又引入多线程呢.md @@ -0,0 +1,29 @@ +--- +title: 为什么Redis设计为单线程而后面版本为啥又引入多线程呢 +published: 2025-09-10 +description: '' +image: '' +tags: [Redis, 中间件] +category: '中间件 > Redis' +draft: false +lang: '' +--- + +# 为什么Redis设计为单线程而后面版本为啥又引入多线程呢 + +## 单线程设计原因: +1. Redis基于内存,大多数操作性能瓶颈来自于CPU +2. 使用单线程模型,代码简单,处理逻辑清晰,也减少了上下文切换带来的性能开销。 +3. Redis在单线程情况下,使用I/O多路复用模型,就能提高Redis的I/O利用率了。 + + +## 6.0 引入多线程的原因 +主要是为了应对网络I/O的瓶颈,提高网络I/O处理速度。 + +随着数据规模的增长和请求量的增加,Redis的执行瓶颈主要在网络I/O,引入多线程能提高网络I/O处理速度。 + +## Redis引入多线程后,有没有线程安全问题 +没有 +Redis6.0只针对网络请求模块采用的是多线程,对于读写命令部分还是用的单线程,所以所谓的线程安全问题也就不存在了。 + +Redis6.0想要开启多线程,需要配置`io-threads-do-reads`参数为yes \ No newline at end of file diff --git a/src/content/posts/中间件/Redis/什么是Redis.md b/src/content/posts/中间件/Redis/什么是Redis.md new file mode 100644 index 0000000..9500c46 --- /dev/null +++ b/src/content/posts/中间件/Redis/什么是Redis.md @@ -0,0 +1,58 @@ +--- +title: 什么是Redis +published: 2025-07-19 +description: '' +image: 'https://blog.meowrain.cn/api/i/2025/07/19/p9zr81-1.webp' +tags: [Redis, 中间件] +category: '中间件 > Redis' +draft: false +lang: '' +--- + +# 什么是Redis + +Redis是一个开源的额高性能键值对存储系统,它可以用作数据库、缓存和消息代理。Redis支持多种数据结构,如字符串、哈希、列表、集合和有序集合等。 +主要特点是把数据存放在内存中,相比于直接访问磁盘的关系型数据库,读写速度会更快。 + +## Redis的特点 + +1. **高性能**:Redis可以每秒处理数百万个请求,读写速度非常快。 +2. **持久化**:Redis支持将数据持久化到磁盘,可以在重启后恢复数据。 +3. **丰富的数据结构**:支持字符串、哈希、列表、集合、有序集合等多种数据类型,适用于不同的应用场景。 +4. **原子操作**:Redis支持对数据的原子操作,保证数据的一致性。 +5. **分布式**:支持主从复制、分片和高可用集群,适合大规模应用。 +6. **发布/订阅**:支持发布/订阅模式,可以实现消息通知和实时数据更新。 +7. **Lua脚本**:支持Lua脚本,可以在服务器端执行复杂的操作,减少网络传输延迟。 +8. **事务支持**:支持事务操作,可以保证一组命令要么全部执行成功,要么全部不执行。 +9. **地理位置支持**:支持地理位置数据,可以进行地理位置查询和计算。 +10. **多种客户端支持**:提供多种编程语言的客户端库,如Java、Python、Node.js等 +11. **易于部署和使用**:Redis的安装和配置相对简单,社区活跃,有丰富的文档和教程。 + +# 使用场景 + +Redis常用于以下场景: + +1. **缓存**:可以用来缓存数据库查询结果,减少数据库负载,提高 +2. **会话存储**:可以用来存储用户会话信息,支持高并发访问。 +3. **实时数据分析**:可以用来存储实时数据,如用户行为分析 +4. **消息队列**:可以用作消息队列系统,支持发布/订阅模式。 +5. **排行榜**:可以用来实现排行榜功能,支持有序集合数据结构。 +6. **分布式锁**:可以用来实现分布式锁,支持高并发场景下的资源控制。 +7. **地理位置服务**:可以用来存储地理位置信息,支持地理位置查询。 +8. **计数器**:可以用来实现计数器功能,如网站访问量统计。 + +# Redis分布式部署的方式 + +Redis的分布式部署方式主要有以下几种: + +1. **主从复制(Master-Slave Replication)**:通过设置主节点和多个从节点,实现数据的复制和备份。主节点负责写操作,从节点负责读操作,可以提高读性能和数据安全性。 +2. **分片(Sharding)**:将数据分布到多个Redis实例中,每个实例存储一部分数据。可以通过哈希算法将数据分配到不同的实例,实现数据的水平扩展。常用的分片方式有一致性哈希(Consistent Hashing)和范围分片(Range Sharding)。 +3. **Redis集群(Redis Cluster)**:Redis官方提供的集群模式,支持自动分片和故障转移。Redis集群可以将数据分布到多个节点上,每个节点存储一部分数据,并且支持动态扩容和缩容。集群模式下,客户端可以通过集群节点的地址直接访问数据,无需额外的代理层。 +4. **Sentinel模式**:Redis Sentinel是Redis的高可用解决方案,可以监控Redis实例的状态,并在主节点发生故障时自动进行故障转移。Sentinel可以与主从复制结合使用,提供高可用性和自动恢复能力。 + +# 和MySQL的区别 + +Redis不是关系型数据库,而是一个键值对存储系统。 +Redis把数据存放在内存中,读写速度非常快,而MySQL是基于磁盘的关系型数据库,读写速度相对较慢。 + +实际开发中,会把Redis作为缓存层,存储一些热点数据,减少对MySQL的访问压力,提高系统性能。 diff --git a/src/content/posts/中间件/Redis/单点故障问题.md b/src/content/posts/中间件/Redis/单点故障问题.md new file mode 100644 index 0000000..5e2afd5 --- /dev/null +++ b/src/content/posts/中间件/Redis/单点故障问题.md @@ -0,0 +1,32 @@ +--- +title: Redis单点故障问题 +published: 2025-09-13 +description: '' +image: '' +tags: ['Redis','单点故障'] +category: '中间件 > Redis' +draft: false +lang: '' +--- + +# 单点故障问题 + +单台Redis实现分布式锁存在单点故障问题,如果采用主从读写分离架构,如果一个客户端在主节点上锁成功,但是主节点突然宕机,由于主从延迟导致节点还没有同步到这个锁,此时可能有另一个客户端抢到新晋升的主节点,这个时候会导致两个客户端抢到了锁,产生数据不一致。 + +# 红锁 +红锁基本思想: +- 部署多个Redis实例(通常5个) +- 客户端在大多数实例(至少3个)上请求锁,并在一定时间内获得成功,表示加锁成功。 +- 使用RedLock可以提供更高的容错性,即使部分Redis实例故障,仍然能获得锁。 + +## 红锁实现步骤 +- 客户端尝试在每个Redis实例上加锁,必须在有限时间内完成所有实例的加锁。 +- 如果大多数实例(N / 2 + 1)加锁成功,就表示加锁成功。 +- 否则,客户端将会释放所有已经加锁的实例,重新尝试。 + +## 红锁缺点 +- 复杂性: 实现ReadLock要多个Redis实例,会增加复杂性。 +- 性能: 需要多个Redis实例,会增加性能开销。 +- 可靠性: 需要多个Redis实例,会增加可靠性开销。 +- 成本: 需要多个Redis实例,会增加成本。 + diff --git a/src/content/posts/中间件/Redis/如何解决Redis中热点key的问题.md b/src/content/posts/中间件/Redis/如何解决Redis中热点key的问题.md new file mode 100644 index 0000000..3400ddc --- /dev/null +++ b/src/content/posts/中间件/Redis/如何解决Redis中热点key的问题.md @@ -0,0 +1,23 @@ +--- +title: 如何解决Redis中热点key的问题 +published: 2025-09-17 +description: '' +image: '' +tags: [热点key,Redis] +category: '中间件 > Redis' +draft: false +lang: '' +--- + + +# 如何解决Redis中热点key的问题 +Redis中的热点Key问题指的是某些Key被频繁访问,导致Redis的压力过大,进而影响整体性能甚至导致集群节点故障。 +解决热点Key问题的主要方法包括: +- 热点Key拆分: 把热点数据分散到多个Key中,例如通过引入随机前缀,使得不同的用户请求能分散到多个Key,多个key分布在多实例中,避免几种访问单一key +- 多级缓存: 在Redis前增加其他缓存层(比如CDN,本地缓存),来分担Redis的访问压力 +- 读写分离: 通过Redis主从复制,把读请求分发到多个从节点,减轻单节点压力 +- 限流和降级: 在热点Key访问过高的时候,应用限流策略,减少对Redis的请求,或者在必要的时候返回降级的数据或空值。 + +![](https://blog.meowrain.cn/api/i/2025/09/18/8d4y9-1.webp) + +![](https://blog.meowrain.cn/api/i/2025/09/18/8or7m-1.webp) \ No newline at end of file diff --git a/src/content/posts/中间件/Redis/缓存与数据库一致性问题.md b/src/content/posts/中间件/Redis/缓存与数据库一致性问题.md new file mode 100644 index 0000000..418f849 --- /dev/null +++ b/src/content/posts/中间件/Redis/缓存与数据库一致性问题.md @@ -0,0 +1,636 @@ +--- +title: 缓存与数据库一致性问题 +published: 2025-09-12 +description: '' +image: '' +tags: ['Redis', '中间件','缓存与数据库一致性'] +category: '中间件 > Redis' +draft: false +lang: '' +--- +# 缓存与数据库一致性 + +缓存和数据库一致性是说在使用缓存的情况下,保证缓存中的数据和数据库中数据一致的问题。 + +**“先写缓存再写数据库”、“先写数据库再写缓存”、“先删除缓存再写数据库”这三种策略确实存在较大的数据不一致风险,因此通常不建议直接使用**,特别是在高并发、分布式系统中。 + + + +# 问题 + +## 先写缓存再写数据库 + +- **策略:先写缓存 (Redis),再写数据库 (MySQL)** + + **图示场景:** 有两个写请求 A 和 B,同时尝试更新票务余票。初始余票是 17,经过两个用户的扣减,**期望**最终余票是 15。 + + **并发执行时序 (按图中箭头顺序):** + + 1. 写请求 A: + + 更新 Redis 缓存 + + ,将余票设为 16。 + + - 此时 Redis: 16,MySQL: 17 + + 2. 写请求 B: + + 更新 Redis 缓存 + + ,将余票设为 15。 + + - 此时 Redis: 15,MySQL: 17 (请求 B 覆盖了请求 A 在 Redis 里的值) + + 3. 写请求 B: + + 更新 MySQL 数据库 + + ,将余票设为 15。 + + - 此时 Redis: 15,MySQL: 15 (到这里,Redis 和 MySQL 暂时一致) + + 4. 写请求 A: + + 更新 MySQL 数据库 + + ,将余票设为 16。 + + - 此时 Redis: 15,MySQL: 16 (请求 A 的数据库更新发生在 B 之后,覆盖了 B 在数据库里的值) + +## 先写数据库再写缓存 + +**策略:先写数据库 (MySQL),再写缓存 (Redis)** + +**图示场景:** 同样是两个写请求 A 和 B,尝试更新余票,初始 17,期望最终是 15。 + +**并发执行时序 (按图中箭头顺序):** + +1. 写请求 A: + + 更新 MySQL 数据库 + + ,将余票设为 16。 + + - 此时 Redis: 17 (假设初始),MySQL: 16 + +2. 写请求 B: + + 更新 MySQL 数据库 + + ,将余票设为 15。 + + - 此时 Redis: 17,MySQL: 15 (数据库被 B 更新为最终期望值) + +3. 写请求 B: + + 更新 Redis 缓存 + + ,将余票设为 15。 + + - 此时 Redis: 15,MySQL: 15 (暂时一致,且是正确期望值) + +4. 写请求 A: + + 更新 Redis 缓存 + + ,将余票设为 16。 + + - 此时 Redis: 16,MySQL: 15 (请求 A 的缓存更新发生在 B 的缓存更新之后,覆盖了 B 在缓存里的值) + +**最终结果:** + +- Redis 缓存中的余票是 **16**。 +- MySQL 数据库中的余票是 **15**。 + +## 先删除缓存,再写数据库 + +**策略:先删除缓存 (Redis),再写数据库 (MySQL)** + +**图示场景:** 这次涉及一个写请求和一个读请求并发执行。初始时,假设 Redis 和 MySQL 中的余票都是 16。写请求想将余票更新为 15。 + +**并发执行时序 (按图中箭头顺序):** + +1. 写请求: + + 删除 Redis 缓存 + + ,删除车站余票的 key (假定初始值是 16,所以图中箭头描述为“删除车站余票缓存 16”,但这只表示删的是这个key对应的旧数据)。 + + - 此时 Redis: 空,MySQL: 16 + +2. 读请求: + + 在写请求 + + 还未来得及更新数据库之前 + + ,并发地发起 + + 读操作 + + 。读请求先 + + 查询缓存 + + ,发现 + + 缓存为空 + + 。 + + - 此时 Redis: 空,MySQL: 16 + +3. 读请求: + + 缓存未命中,读请求转向 + + 查询 MySQL 数据库 + + 。此时写请求尚未完成数据库更新,所以从数据库中读到的是 + + 旧数据 16 + + 。 + + - 此时 Redis: 空,MySQL: 16,读请求获取到数据 16 + +4. 写请求: + + 更新 MySQL 数据库 + + ,将余票设为 15。 + + - 此时 Redis: 空,MySQL: 15 + +5. 读请求: + + 将从数据库读到的 + + 旧数据 16 回写到 Redis 缓存 + + 中。 + + - 此时 Redis: 16,MySQL: 15 + +**最终结果:** + +- Redis 缓存中的余票是 **16** (旧值)。 +- MySQL 数据库中的余票是 **15** (新值)。 + +# 解决方案 + +根据业务场景选择下面的缓存一致性方案: + +- **缓存双删**:如果公司现有消息队列中间件,可以考虑使用该方案,反之则不需要考虑。 +- **先写数据库再删缓存**:这种方案从实时性以及技术实现复杂度来说都比较不错,推荐大家使用这种方案。 +- **Binlog 异步更新缓存**:如果希望实现最终一致性以及数据多中心模式,该方案无疑是最合适的。 + +## 缓存双删 + + + +**先删除缓存 -> 写数据库 -> 延迟一段时间 -> 再次删除缓存** + +![img](https://blog.meowrain.cn/api/i/2025/05/20/qoh9qh-0.webp) + + + +![image-20250520164546344](https://blog.meowrain.cn/api/i/2025/05/20/r7ob3u-0.webp) + +## 先写入数据库,再删除缓存 + +**"先写 DB 再删除缓存"是一种常用的缓存一致性解决方案,也被称为“写回策略”或“Write-Through 策略”。** + +1. 当应用程序进行写操作时,首先将数据写入数据库。 +2. 然后,立即删除相应的缓存数据(或使缓存数据失效)。 +3. 当下一个读取请求到达时,会发现缓存中没有相应的数据,于是从数据库中读取最新的数据,并将其存储在缓存中。 + +这种策略的优势在于它保证了数据库和缓存之间的一致性。如果应用程序写入数据库并删除缓存,下一个读取操作将从数据库中获取最新数据,从而避免了数据不一致的情况。 + +![image-20250520164901899](https://blog.meowrain.cn/api/i/2025/05/20/r9sbzc-0.webp) + +当然,这种模型也不是完全没问题,如果说恰巧读缓存失效了,就会出现这种情况。 + +当缓存过期(可能是缓存正常过期也可能是 Redis 内存满了触发清理策略)条件满足,同时读请求的回写缓存 Redis 的执行周期在数据库删除之前,那么就有可能触发缓存数据库不一致问题。 + +上面说的两种情况,缺一不可,不过能同时满足这两种情况概率极低,低到可以忽略这种情况。 + +![img](https://blog.meowrain.cn/api/i/2025/05/20/rb7n2u-0.webp) + +这种模型会存在一个很小周期的缓存与数据库**不一致**的情况,不过对于绝大多数的情况来说,是可以容忍的。除去一些电商库存、列车余票等对数据比较敏感的情况,比较适合绝大多数业务场景。 + +# AI问答,一些思考 + +# 数据库缓存方案:先删后写与双删 + +[(99 封私信 / 80 条消息) 『延迟双删』和『先修改数据库,再删除缓存』哪个方案更好? - 知乎](https://www.zhihu.com/question/556172201) + + + +### 🧑‍💻 User + +所以先写数据库,再删缓存 +和缓存双删 +都是企业中正常使用的方案? + +--- + +### 🤖 Assistant + +
+ 已深度思考
+ 好的,用户现在在问关于数据库和缓存操作的两种常见方案:“先写数据库,再删缓存”和“缓存双删”是否都是企业中正常使用的方案。我需要先解释这两种方案,然后说明它们的适用场景和企业中的实际使用情况。

首先,我需要回忆一下这两种策略的基本概念。先写数据库再删缓存,是指在更新数据时,先更新数据库,然后删除缓存中的旧值,这样下次读取时会从数据库重新加载最新数据到缓存。而缓存双删,是在更新数据库前后各删除一次缓存,防止在更新过程中有并发读请求导致缓存脏数据。

接下来,我需要解释为什么企业会使用这些方案。先写数据库再删缓存是比较常见的策略,因为它简单且在大多数情况下有效,尤其是在并发不高的场景。但可能存在的问题是,如果在删除缓存之前有读请求,会读取到旧的缓存值,不过这种情况概率较低,因为数据库更新通常比缓存操作快。

然后是缓存双删,这是为了解决先写后删可能存在的并发问题。比如,当更新数据库时,有一个读请求在数据库更新之后但缓存删除之前读取,这时候会从数据库拿到新数据,但可能在缓存删除前另一个写操作导致数据不一致。双删策略通过在更新前删一次,更新后等一段时间再删一次,来减少这种情况的发生。不过需要注意等待时间的设置,这取决于数据库的主从同步时间或其他延迟。

接下来要说明企业中是否正常使用。实际上,这两种方案都是常见的,但适用场景不同。先写后删更简单,适用于大部分场景;双删则在高并发或对一致性要求较高的场景中使用。同时,可能还要提到其他方案,比如写缓存(更新缓存而不是删除),但这种方式可能有一致性问题,需要结合其他机制。

最后,需要总结这两种方案都是企业常用的,但需要根据具体场景选择,并可能结合其他优化措施,比如延迟双删中的等待时间,或者使用消息队列来确保缓存删除成功。

现在需要按照用户要求的格式来组织回答:标题带emoji,解释部分要详细但易懂,可能用例子或类比,最后一个问题。

标题可能用“数据库与缓存更新策略:先写后删 vs 双删 🗄️🔄”这样的。

解释部分需要分点说明两种策略,它们的流程、优缺点、适用场景,以及企业中的实际应用情况。可能用一个例子,比如用户修改个人信息,先更新数据库再删缓存,或者双删的情况。

最后,问用户是否想了解更多细节,比如具体实现或其他缓存策略。
+
+ + + +数据库与缓存更新策略:先写后删 vs 双删 🗄️🔄 + +在企业级系统中,数据库(持久化存储)和缓存(高速临时存储)的协同工作是性能优化的核心。为了保证数据一致性(数据库和缓存中的数据同步),常见的两种策略是“先写数据库,再删缓存”和“缓存双删”,它们都是企业中**根据场景灵活选用**的方案,我们逐一拆解: + + +### 1. 先写数据库,再删缓存:最基础的“保守派” + +**流程**:当需要更新数据时,先完成数据库的写操作(确保持久化存储是最新的),然后删除缓存中对应的旧值。下次读取时,由于缓存缺失,系统会从数据库读取最新数据并重新写入缓存(即“缓存击穿”后的自动填充)。 + +**为什么企业用它?** + +- **简单可靠**:逻辑清晰,只有两步操作(写库→删缓存),易于实现和维护。 +- **风险可控**:数据库是“数据源头”,优先保证数据库的正确性(即使缓存删除失败,最多缓存中暂时保留旧数据,下次读取会自动更新)。 + +**潜在问题**: +如果在“写数据库”和“删缓存”之间,有一个读请求到达,此时缓存中的旧数据还未被删除,读请求会拿到旧值。但这种情况发生概率较低(数据库写操作通常比缓存操作快,时间窗口短),因此在**并发量不高或对一致性要求不是极端严格**的场景(如用户信息、商品详情)中,企业普遍采用此方案。 + + +### 2. 缓存双删:高并发下的“增强版” + +**流程**:为了进一步降低“先写后删”中旧缓存被读取的概率,在更新数据库前后各删除一次缓存(故称“双删”): +① **先删缓存**:删除旧缓存(防止读请求拿到旧值); +② **写数据库**:更新数据库; +③ **延迟后再删缓存**:等待一段时间(比如1-5秒,具体取决于数据库主从同步或业务延迟),再次删除缓存(防止在写数据库期间,有新的读请求将旧数据重新写入缓存)。 + +**为什么企业用它?** + +- **提升一致性**:通过两次删除,大幅缩小“旧缓存被读取”的时间窗口,适用于**高并发场景**(如秒杀活动、高频更新的订单状态)。 +- **应对复杂延迟**:如果数据库是主从架构(写主库→同步到从库有延迟),或缓存写入存在异步操作,双删能更好地覆盖这些延迟带来的不一致风险。 + +**潜在问题**: +需要额外处理“延迟删除”的逻辑(比如用定时任务或消息队列实现),增加了系统复杂度;延迟时间过长可能影响性能,过短可能无法覆盖所有延迟场景,需要根据业务经验调优。 + + +### 企业实际使用场景总结 + +- **先写后删**:是“默认方案”,适用于大多数业务(如用户个人信息、商品基础属性),简单高效,满足90%以上场景的一致性需求。 +- **双删策略**:作为“增强方案”,用于高并发、强一致性要求的场景(如库存、交易订单),通过牺牲一定复杂度换取更高的数据一致性。 + + +你更想深入了解哪一种策略的具体实现细节?比如“先写后删”如何处理缓存删除失败,或“双删”中延迟时间如何计算? 😊 + +--- + +### 🧑‍💻 User + +问题是缓存双删,你删了缓存,在删除和写入之间有读请求,哪个请求还是会拿到旧数据啊,这时候还是会让缓存是旧数据,但是数据库里是新的(短暂的),当然最后缓存还是要被二次删除 + +--- + +### 🤖 Assistant + +
+ 已深度思考
+ 好的,用户现在在问缓存双删策略中的一个具体问题:在第一次删缓存之后,写数据库之前,如果有读请求进来,这时候读请求会从数据库读取旧数据并重新写入缓存,导致缓存里是旧数据,而数据库之后会被更新为新数据。这时候虽然最后会二次删除缓存,但中间可能存在短暂的不一致。我需要解释这个问题,并说明为什么双删策略仍然有效,或者如何处理这种情况。

首先,用户的问题核心是双删策略中的时间窗口问题,即在第一次删除缓存后,写数据库前,读请求可能重新填充旧数据到缓存。这时候需要分析这个时间窗口的长度,以及如何通过延迟二次删除来解决。

需要回顾双删的流程:第一次删缓存→写数据库→延迟后二次删缓存。用户提到的情况是在第一步删缓存后,写数据库前,有读请求,此时数据库还是旧数据(因为还没写),所以读请求会从数据库读旧数据,然后写入缓存。这时候缓存里是旧数据,之后数据库被更新为新数据,这时候缓存里的旧数据会存在,直到二次删除触发。

需要解释为什么这种情况的影响有限,或者如何通过调整延迟时间来覆盖这个窗口。比如,假设写数据库的时间是T1,读请求的时间是在T0(第一次删缓存后,写数据库前),那么读请求会把旧数据写入缓存,之后数据库在T1被更新。此时缓存中的旧数据会存在,直到二次删除在T1+延迟时间执行。这时候如果延迟时间足够长,覆盖了可能的读请求重新填充缓存的时间,那么二次删除就能清除这个旧缓存,下次读请求就会从数据库读取新数据。

另外,可能需要提到这种情况的概率较低,因为写数据库通常是一个相对较快的操作,时间窗口较短。或者企业中如何通过其他手段(如分布式锁、异步补偿)来进一步降低风险。

还要注意用户可能的深层需求:是否双删策略真的能解决一致性问题,或者是否有其他更优的方案。需要明确双删是权衡后的选择,并非完美,但在大多数高并发场景下足够用。

最后,保持解释清晰,用例子或时间线来帮助理解,然后以问题结束,引导用户进一步提问。
+
+ + + +缓存双删中的“旧数据回灌”问题:时间窗口与延迟删除的奥秘 🕒🔍 + +你观察得非常细致!缓存双删策略确实存在一个“短暂的时间窗口”,可能让旧数据暂时回到缓存中。我们通过具体时间线模拟这个过程,再分析为什么企业仍选择这种方案,以及如何降低风险: + + +### 场景模拟:双删中的“旧数据回灌” + +假设我们要更新一条用户的“账户余额”数据,流程如下(时间顺序): +1️⃣ **第一次删缓存**(T0时刻):删除缓存中旧的“账户余额”(假设旧值是100元)。 +2️⃣ **读请求插入旧数据**(T1时刻,T0 < T1 < T2):此时数据库还未更新(仍是旧值100元),一个读请求到达→缓存缺失→从数据库读取旧值100元→重新写入缓存(缓存现在是100元)。 +3️⃣ **写数据库**(T2时刻):数据库更新为新值200元(完成持久化)。 +4️⃣ **第二次删缓存**(T3时刻,T3 = T2 + 延迟时间):等待一段时间后,再次删除缓存(此时缓存中的100元被删除)。 + + +### 为什么这种“短暂不一致”可以接受? + +虽然T1到T3期间,缓存中是旧值(100元),数据库是新值(200元),但这种不一致的**持续时间极短**(仅T3 - T1),且企业通过以下方式降低影响: + +#### 1. 延迟时间的“精准控制” + +第二次删除的“延迟时间”(如1-5秒)是关键。它需要覆盖“数据库写操作的耗时 + 可能的读请求重新填充缓存的耗时”。例如: + +- 假设数据库写操作耗时0.5秒,读请求从数据库读数据+写缓存耗时0.3秒,那么延迟时间设为1秒即可覆盖T2到T3的窗口,确保在第二次删除时,缓存中可能存在的旧数据被清除。 + +#### 2. 高并发下的“概率稀释” + +在实际场景中,“读请求恰好出现在T0-T2窗口”的概率较低。因为: + +- 写数据库操作通常是同步的(如事务提交),耗时极短(毫秒级); +- 即使有读请求进入,它们大多是“分散”的,不会集中在这个微小的时间窗口内。 + +#### 3. 最终一致性的“兜底” + +即使出现短暂不一致,第二次删除后,下一次读请求会从数据库读取最新值(200元)并重新填充缓存,最终保证缓存与数据库一致。 + + +### 企业如何进一步优化? + +如果业务对一致性要求极高(如金融交易),企业可能会结合以下策略: + +- **分布式锁**:在写操作期间,对关键数据加锁,阻止读请求在T0-T2窗口内重新填充缓存(但会牺牲部分性能); +- **异步补偿**:通过日志监控缓存与数据库的差异,发现不一致时主动触发缓存更新; +- **缓存版本号**:为缓存添加版本标识(如时间戳),读请求填充缓存时检查版本,旧版本数据直接丢弃。 + + +总结来说,双删策略的“旧数据回灌”是理论上存在的,但通过合理设置延迟时间、结合其他优化手段,企业可以将其影响控制在可接受范围内。它本质上是“性能与一致性”的权衡——用微小的短暂不一致,换取高并发下的整体高效。 + + +关于双删策略,你还想了解哪些细节?比如“如何计算具体的延迟时间”或“分布式锁如何与双删配合使用”? 😊 + +--- + +### 🧑‍💻 User + +这不和先更新数据库,再删除缓存一样吗? + +--- + +### 🤖 Assistant + +
+ 已深度思考
+ 好的,用户现在的问题是:“这不和先更新数据库,再删除缓存一样吗?”需要解释缓存双删和先写数据库再删缓存的区别,以及为什么两者不同。首先,我需要回顾之前的对话,用户已经了解了这两种策略,现在可能混淆了它们的差异。

首先,用户的疑问是双删和先写后删是否一样。需要明确两者的核心区别在于操作顺序和步骤数量。先写后删是两步:写库→删缓存;双删是三步:删缓存→写库→延迟删缓存。

接下来,需要解释为什么双删不是简单的先写后删的重复。要指出时间窗口的不同,以及双删如何针对先写后删的缺陷进行优化。比如,先写后删的问题是在写库和删缓存之间可能有读请求拿到旧缓存,而双删通过提前删缓存和延迟再删来缩小这个窗口。

然后,用具体的时间线对比两种策略,说明在同样的读请求干扰下,两种策略的表现差异。比如,先写后删中,读请求可能在写库之后、删缓存之前到达,导致缓存保留旧值;而双删中,第一次删缓存后,读请求可能在写库前重新填充旧值,但第二次删缓存会清除它。

还要强调双删的“延迟”步骤的作用,以及为什么这额外的步骤能提升一致性,尽管仍有短暂窗口,但比先写后删更优。

最后,总结两者的适用场景,让用户明白它们不是一样的,而是针对不同并发和一致性需求的解决方案。

需要保持解释清晰,用例子和时间线帮助理解,避免术语过于复杂。同时,按照系统提示的格式,标题要有emoji,解释部分分点,最后问用户想深入的问题。
+
+ + + +缓存双删 vs 先写后删:步骤差异与一致性提升的本质区别 🧩🔄 + +你的观察很敏锐!两者确实都涉及“写数据库”和“删缓存”,但核心差异在于**操作顺序、步骤数量**和**对“读请求干扰”的防御机制**。我们通过具体场景对比,就能清晰看到它们的不同。 + + +### 先写数据库,再删缓存:“被动防御”的基础策略 + +**流程**:写数据库(T1)→ 删缓存(T2)。 +**时间线示例**(假设要更新用户余额为200元): + +- T0:数据库旧值100元,缓存旧值100元(初始状态)。 +- T1:写数据库完成(新值200元)。 +- T2:删除缓存(此时缓存被清空)。 +- **风险窗口**:若在T1到T2之间(写库后、删缓存前)有读请求: + 读请求发现缓存未删(仍为100元)→ 直接返回旧值(缓存旧值,数据库新值),导致不一致。 + + +### 缓存双删:“主动+延迟”的增强策略 + +**流程**:删缓存(T0)→ 写数据库(T1)→ 延迟后再删缓存(T2)。 +**时间线示例**(同样更新用户余额为200元): + +- T0:第一次删缓存(缓存旧值100元被删除,缓存为空)。 +- T1:写数据库完成(新值200元)。 +- **风险窗口**:若在T0到T1之间(删缓存后、写库前)有读请求: + 读请求发现缓存为空→ 从数据库读旧值100元→ 重新写入缓存(缓存现在是100元)。 +- T2(延迟后):第二次删缓存(清除此时可能存在的旧值100元)。 + + +### 关键差异:风险窗口的“位置”与“处理方式” + +| 策略 | 风险窗口位置 | 风险窗口内的读请求行为 | 最终一致性如何保证? | +| -------- | ------------------------- | -------------------------------------- | ------------------------------------ | +| 先写后删 | 写库后、删缓存前(T1-T2) | 读请求拿到缓存旧值(数据库已更新) | 下次读请求时缓存被删,重新加载新值 | +| 缓存双删 | 删缓存后、写库前(T0-T1) | 读请求重新写入缓存旧值(数据库未更新) | 第二次删缓存清除旧值,下次读加载新值 | + + +### 为什么双删不是“先写后删的重复”? + +1. **风险窗口更小**:写数据库通常比删缓存耗时(如事务提交可能需要毫秒级),而“先删缓存”让风险窗口出现在“写库前”(此时数据库还是旧值),读请求即使回灌旧值,数据库本身未更新,后续第二次删缓存后,下次读请求会从已更新的数据库加载新值。 +2. **主动防御延迟**:双删通过“延迟第二次删除”覆盖了数据库写操作的耗时,确保即使有读请求在写库前回灌旧值,最终也会被清除。而先写后删没有这一步,风险窗口内的读请求可能长期保留旧缓存(直到下次自然失效)。 + + +### 总结:两者是“互补”而非“相同” + +- 先写后删:简单、高效,适用于低并发或一致性要求不高的场景(风险窗口短,概率低)。 +- 缓存双删:通过“两次删除+延迟”主动缩小风险窗口,适用于高并发、一致性要求更高的场景(如库存、交易)。 + + +你更想了解哪种策略在具体代码中的实现细节?比如先写后删如何用代码处理“删缓存失败”,或双删的“延迟时间”如何用定时任务实现? 😊 + +--- + +### 🧑‍💻 User + +奥我懂了,双删时候,风险窗口期间,数据库未更新,其实是不算不一致的对吧,就算请求B取数据库并且更新缓存,那个值也是那个时刻的正确数值。 +但是我有个问题,后面更新数据库后,这时候缓存如果被之前的请求更新为旧的数据,这时候不就是不一致了吗?这期间进来的请求拿到的不就是旧的值了吗? + +--- + +### 🤖 Assistant + +
+ 已深度思考
+ 好的,用户现在的问题是关于缓存双删策略中的不一致情况。用户之前已经理解了双删的基本流程,现在进一步疑惑:当数据库更新后,如果缓存被之前的读请求填充了旧数据,这时候是否会导致不一致,以及后续请求是否会拿到旧值。

首先,我需要确认用户的问题核心:在双删的时间窗口中,读请求可能在数据库更新前读取旧数据并填充缓存,而当数据库更新后,这个缓存中的旧数据是否会导致不一致,以及如何解决。

接下来,我需要解释在数据库更新后,缓存中的旧数据确实会导致短暂的不一致,但双删的第二次删除会解决这个问题。需要详细说明时间线,让用户明确每个阶段的数据状态。

然后,用户可能想知道为什么这种不一致是短暂的,以及第二次删除的作用。需要强调延迟时间的重要性,确保在第二次删除时,旧数据被清除,后续请求会从数据库获取新值。

另外,用户可能隐含担心这种短暂不一致是否会影响业务,需要说明实际场景中这种情况的概率和持续时间都很低,以及企业如何通过优化手段(如调整延迟时间、监控)来进一步降低风险。

最后,保持回答的结构:标题、解释、总结问题,确保符合之前的格式要求,用易懂的语言和例子帮助用户理解。
+
+ + + +缓存双删中的“短暂不一致”与二次删除的关键作用 🕰️🔧 + +你完全抓住了问题的核心!这种情况下确实会出现“数据库已更新,但缓存仍存旧值”的短暂不一致,但双删策略的“第二次删除”正是为了解决这个问题。我们通过时间线拆解和具体例子,来彻底理清这个过程。 + + +### 时间线模拟:数据库更新后,缓存为何会有旧值? + +假设要更新用户“账户余额”(旧值100元→新值200元),双删流程的时间线如下(关键节点标注): + +| 时间点 | 操作/事件 | 数据库状态 | 缓存状态 | +| ------ | ------------------------------ | ------------------- | ------------------------------------------------ | +| T0 | 第一次删缓存(主动删除旧缓存) | 仍为100元(未更新) | 缓存被清空(无数据) | +| T1 | 读请求A到达(在T0-T2之间) | 仍为100元(未更新) | 缓存空→读数据库取100元→写缓存(缓存现在是100元) | +| T2 | 写数据库完成(更新为200元) | 变为200元(已更新) | 缓存仍为100元(旧值) | +| T3 | 第二次删缓存(延迟后执行) | 200元(已更新) | 缓存中的100元被删除(缓存再次清空) | +| T4 | 读请求B到达(T3之后) | 200元(已更新) | 缓存空→读数据库取200元→写缓存(缓存现在是200元) | + + +### 为什么T2到T3期间会“不一致”? + +在T2(数据库更新完成)到T3(第二次删缓存)的时间段内: + +- 数据库:已存储最新值200元(正确); +- 缓存:被读请求A在T1时刻填充了旧值100元(错误)。 + +此时,若有新的读请求在T2-T3之间到达,会直接读取缓存中的旧值100元,导致“缓存旧、数据库新”的不一致。 + + +### 但这种不一致为何是“短暂可控”的? + +关键在**第二次删除(T3)**和**延迟时间的设计**: + +#### 1. 第二次删除强制清空旧缓存 + +无论缓存中是否有旧值,T3时刻的二次删除会直接清除缓存中的100元。此时缓存变为空,后续读请求(如T4的读请求B)只能从数据库读取最新的200元,并重新填充缓存为正确值。 + +#### 2. 延迟时间覆盖“数据库写操作耗时” + +T3 = T2 + 延迟时间(如1秒)。延迟时间需要足够长,确保: + +- 数据库写操作(T0到T2)已完全完成; +- 所有可能在T0-T2期间触发的读请求(如读请求A)都已完成缓存填充。 + +这样,第二次删除时,缓存中可能存在的旧值(100元)会被彻底清除,避免长期留存。 + + +### 极端情况:如果第二次删除失败了怎么办? + +假设T3时刻二次删除因网络问题失败,缓存中的旧值100元未被清除,此时会发生什么? + +- 后续读请求仍会读取到旧值,但这种情况属于“异常”,企业通常会通过以下方式兜底: + - **缓存过期时间**:即使没有二次删除,缓存的TTL(如5分钟)会自动失效,最终强制从数据库加载新值; + - **监控与补偿**:通过日志监控缓存与数据库的差异,发现不一致时手动或自动触发缓存更新。 + + +### 总结:双删的“不一致”是“阶段性的”,而非“永久性的” + +虽然T2-T3期间可能出现“缓存旧、数据库新”的短暂不一致,但: + +- 持续时间由延迟时间决定(通常仅几秒); +- 第二次删除强制清除旧缓存,后续请求会自动加载新值; +- 结合缓存过期时间,最终一致性得以保证。 + + +关于这种“短暂不一致”的业务影响,你还想了解哪些场景?比如“金融系统如何容忍这种短暂不一致”或“如何通过监控发现这类问题”? 😊 + +--- + +### 🧑‍💻 User + +这个第二次删缓存的延迟是什么?能讲一下吗 + +--- + +### 🤖 Assistant + +
+ 已深度思考
+ 好的,用户现在问的是缓存双删中第二次删除的延迟是什么,需要详细解释。首先,我需要明确延迟的作用、如何确定延迟时间,以及实际应用中的例子。用户可能已经理解了双删的基本流程,但对延迟的具体作用和设置方法不太清楚,需要用易懂的方式解释。

首先,要解释延迟的定义:在写数据库完成后,等待一段时间再执行第二次删缓存的操作。然后,说明为什么需要这个延迟——覆盖数据库写操作的耗时,确保所有可能在风险窗口内的读请求已经完成缓存填充,这样第二次删除才能有效清除旧值。

接下来,需要具体说明延迟时间的确定方法。比如,要考虑数据库写操作的最大耗时,加上网络延迟、应用处理时间等。举个例子,假设写数据库平均耗时200ms,最大可能到500ms,那么延迟可以设置为1秒(500ms*2),确保所有可能的读请求已经完成。

然后,可以用时间线的例子来演示延迟的作用,比如没有延迟的话,第二次删除可能在数据库还没写完时就执行,导致无效;而有了延迟,确保数据库已经更新完成,此时删除缓存才能真正清除可能存在的旧值。

还要提到实际应用中如何实现这个延迟,比如使用定时任务、消息队列的延迟投递,或者简单的线程睡眠(但要注意分布式系统中的问题)。同时,需要指出延迟时间不是固定的,需要根据业务场景调整,比如高并发系统可能需要更长的延迟,而低并发可以缩短。

最后,总结延迟的核心目的是确保第二次删除的有效性,避免缓存中残留旧值,从而减少不一致的时间窗口。同时,保持回答的结构,符合之前的格式要求:标题带emoji,解释部分分点或举例,最后提一个问题引导用户继续提问。
+
+ + + +缓存双删中“延迟时间”的作用与设计逻辑 ⏳🔍 + +你问到了双删策略的核心细节!这里的“延迟”不是随意等待,而是经过设计的“时间缓冲带”,它的存在直接决定了双删策略能否有效减少缓存与数据库的不一致。我们通过“为什么需要延迟”“如何确定延迟时长”“实际如何实现”三个维度来拆解。 + + +### 一、为什么需要“延迟时间”?——覆盖“风险窗口”的残留旧值 + +在双删流程中,第一次删缓存(T0)后,可能有读请求在“写数据库前”(T0-T1)从数据库读取旧值并重新写入缓存(比如读请求A在T0-T1期间将旧值100元写回缓存)。此时: + +- 数据库会在T1时刻更新为新值200元; +- 但缓存中已被读请求A写入了旧值100元(这是问题的根源)。 + +如果没有延迟,第二次删缓存(T2)会在写数据库(T1)完成后**立即执行**。但此时可能存在两种情况: + +1. 读请求A的缓存写入操作(旧值100元)还未完成(网络延迟或线程调度延迟),导致第二次删除时缓存中还没有旧值,删除无效; +2. 即使删除了旧值,若后续还有其他读请求在“写数据库完成后、第二次删除前”再次读取到旧值,仍会导致短暂不一致。 + +**延迟的作用**:让“第二次删除”等待足够长的时间,确保所有在“写数据库前”触发的读请求(如读请求A)已经完成缓存写入(旧值100元),此时再删除,就能彻底清除这些残留的旧缓存。 + + +### 二、如何确定“延迟时长”?——基于数据库写操作的耗时 + +延迟时间的核心依据是“数据库写操作的最大耗时”(包括事务提交、主从同步等)。具体计算逻辑如下: + +#### 公式:延迟时间 ≈ 数据库写操作的最大耗时 × 2(或1.5~3倍) + +- **数据库写操作耗时**:指从执行写命令(如`UPDATE user SET balance=200 WHERE id=1`)到数据库确认写入完成(如事务提交、主库同步到从库)的时间。例如,在MySQL中,写操作可能需要50ms~200ms(高并发时可能更长)。 +- **乘以倍数的原因**:覆盖网络延迟、应用服务器与数据库的通信耗时,以及可能的读请求处理延迟(如读请求A的线程可能因CPU调度延迟执行缓存写入)。 + +**举例**: +假设数据库写操作的最大耗时是300ms(包括主从同步),那么延迟时间可以设置为500ms~1秒。这样,即使读请求A在写数据库完成后(T1)才完成缓存写入(旧值100元),第二次删除(T2=T1+500ms)也能在缓存写入完成后执行,确保旧值被清除。 + + +### 三、实际如何实现“延迟删除”?——技术方案与注意事项 + +在代码中实现“延迟删除”有以下常见方式: + +#### 1. 线程睡眠(简单但局限) + +在写数据库完成后,让当前线程休眠指定时间(如500ms),然后执行第二次删除。 + +```java +// 伪代码示例 +public void updateUserBalance(Long userId, Integer newBalance) { + // 第一次删缓存 + redis.del("user:balance:" + userId); + + // 写数据库(假设耗时200ms) + userMapper.updateBalance(userId, newBalance); + + // 延迟500ms(覆盖写操作耗时+可能的读请求处理) + Thread.sleep(500); + + // 第二次删缓存 + redis.del("user:balance:" + userId); +} +``` + +**局限**:在分布式系统中,若多个实例同时更新同一数据,可能因线程休眠导致性能问题;且无法精准控制延迟时间(受JVM调度影响)。 + + +#### 2. 定时任务/延迟队列(更可靠) + +将第二次删除操作提交到延迟队列(如RocketMQ的延迟消息、Redis的ZSET+定时扫描),由队列在指定时间后触发删除。 + +```java +// 使用RocketMQ延迟消息示例 +Message message = new Message( + "cacheDeleteTopic", + "user:balance:" + userId, // 消息内容为缓存键 + ("deleteCache:" + userId).getBytes() +); +message.setDelayTimeLevel(3); // 延迟10秒(具体级别根据MQ配置) +producer.send(message); +``` + +**优势**:解耦业务逻辑,避免阻塞主线程;支持分布式场景,延迟时间更精准。 + + +#### 3. 异步线程池(折衷方案) + +通过线程池提交一个异步任务,任务中先休眠指定时间,再执行删除。 + +```java +// 伪代码示例 +ExecutorService executor = Executors.newSingleThreadExecutor(); +executor.submit(() -> { + try { + Thread.sleep(500); // 延迟500ms + redis.del("user:balance:" + userId); + } catch (InterruptedException e) { + Thread.currentThread().interrupt(); + } +}); +``` + + +### 四、极端情况:延迟时间不够会怎样? + +如果延迟时间过短(如小于数据库写操作耗时),可能出现: + +- 第二次删除时,数据库写操作尚未完成,此时缓存中可能被读请求写入更早的旧值(数据库还未更新),导致删除无效; +- 或者,读请求在“第二次删除后”才完成缓存写入(旧值),此时缓存中仍会残留旧值,需要等待缓存过期才能恢复一致。 + + +### 总结:延迟是双删策略的“时间保险栓” + +延迟时间的设计本质是**用时间换一致性**——通过等待足够长的时间,确保所有可能干扰缓存的读请求已完成旧值写入,再通过第二次删除彻底清除这些旧值。它是双删策略中“主动防御”的关键一环。 + + +关于延迟实现的具体技术细节(比如如何用Redis的ZSET实现延迟队列),或者不同业务场景下延迟时间的经验值(如电商库存vs用户信息),你更想深入了解哪部分? 😊 \ No newline at end of file diff --git a/src/content/posts/中间件/nacos/nacos安装.md b/src/content/posts/中间件/nacos/nacos安装.md new file mode 100644 index 0000000..70ade5c --- /dev/null +++ b/src/content/posts/中间件/nacos/nacos安装.md @@ -0,0 +1,23 @@ +--- +title: nacos安装 +published: 2025-07-19 +description: '' +image: '' +tags: [nacos, 中间件, 安装] +category: '中间件 > Nacos' +draft: false +lang: '' +--- +# nacos安装 + +``` +docker run --name nacos-standalone-derby \ + -e MODE=standalone \ + -e NACOS_AUTH_TOKEN=bWVvd3JhaW55eWRzNjY2Nm1lb3dyYWlueXlkczY2NjY= \ + -e NACOS_AUTH_IDENTITY_KEY=meowrain \ + -e NACOS_AUTH_IDENTITY_VALUE=meowrain \ + -p 8085:8080 \ + -p 8848:8848 \ + -p 9848:9848 \ + -d nacos/nacos-server:latest +``` diff --git a/src/content/posts/合集/DB.md b/src/content/posts/合集/DB.md new file mode 100644 index 0000000..49f3741 --- /dev/null +++ b/src/content/posts/合集/DB.md @@ -0,0 +1,16774 @@ +--- +title: 数据库笔记合集 +published: 2025-10-26 +description: '' +image: '' +tags: [DB] +category: '合集' +draft: false +lang: '' +--- + +# MySQL + +## 简介 + +### 数据库 + +数据库:DataBase,简称 DB,存储和管理数据的仓库 + +数据库的优势: + +- 可以持久化存储数据 +- 方便存储和管理数据 +- 使用了统一的方式操作数据库 SQL + +数据库、数据表、数据的关系介绍: + +- 数据库 + + - 用于存储和管理数据的仓库 + - 一个库中可以包含多个数据表 + +- 数据表 + + - 数据库最重要的组成部分之一 + - 由纵向的列和横向的行组成(类似 excel 表格) + - 可以指定列名、数据类型、约束等 + - 一个表中可以存储多条数据 + +- 数据:想要永久化存储的数据 + + + + +参考视频:https://www.bilibili.com/video/BV1zJ411M7TB + +参考专栏:https://time.geekbang.org/column/intro/139 + +参考书籍:https://book.douban.com/subject/35231266/ + + + +*** + + + +### MySQL + +MySQL 数据库是一个最流行的关系型数据库管理系统之一,关系型数据库是将数据保存在不同的数据表中,而且表与表之间可以有关联关系,提高了灵活性 + +缺点:数据存储在磁盘中,导致读写性能差,而且数据关系复杂,扩展性差 + +MySQL 所使用的 SQL 语句是用于访问数据库最常用的标准化语言 + +MySQL 配置: + +* MySQL 安装:https://www.jianshu.com/p/ba48f1e386f0 + +* MySQL 配置: + + * 修改 MySQL 默认字符集:安装 MySQL 之后第一件事就是修改字符集编码 + + ```mysql + vim /etc/mysql/my.cnf + + 添加如下内容: + [mysqld] + character-set-server=utf8 + collation-server=utf8_general_ci + + [client] + default-character-set=utf8 + ``` + + * 启动 MySQL 服务: + + ```shell + systemctl start/restart mysql + ``` + + * 登录 MySQL: + + ```shell + mysql -u root -p 敲回车,输入密码 + 初始密码查看:cat /var/log/mysqld.log + 在root@localhost: 后面的就是初始密码 + ``` + + * 查看默认字符集命令: + + ```mysql + SHOW VARIABLES LIKE 'char%'; + ``` + + * 修改MySQL登录密码: + + ```mysql + set global validate_password_policy=0; + set global validate_password_length=1; + + set password=password('密码'); + ``` + + * 授予远程连接权限(MySQL 内输入): + + ```mysql + -- 授权 + grant all privileges on *.* to 'root' @'%' identified by '密码'; + -- 刷新 + flush privileges; + ``` + +* 修改 MySQL 绑定 IP: + + ```shell + cd /etc/mysql/mysql.conf.d + sudo chmod 666 mysqld.cnf + vim mysqld.cnf + # bind-address = 127.0.0.1注释该行 + ``` + +* 关闭 Linux 防火墙 + + ```shell + systemctl stop firewalld.service + # 放行3306端口 + ``` + + + + + +*** + + + + + +## 体系架构 + +### 整体架构 + +体系结构详解: + +* 第一层:网络连接层 + * 一些客户端和链接服务,包含本地 Socket 通信和大多数基于客户端/服务端工具实现的 TCP/IP 通信,主要完成一些类似于连接处理、授权认证、及相关的安全方案 + * 在该层上引入了**连接池** Connection Pool 的概念,管理缓冲用户连接,线程处理等需要缓存的需求 + * 在该层上实现基于 SSL 的安全链接,服务器也会为安全接入的每个客户端验证它所具有的操作权限 + +- 第二层:核心服务层 + * 查询缓存、分析器、优化器、执行器等,涵盖 MySQL 的大多数核心服务功能,所有的内置函数(日期、数学、加密函数等) + * Management Serveices & Utilities:系统管理和控制工具,备份、安全、复制、集群等 + * SQL Interface:接受用户的 SQL 命令,并且返回用户需要查询的结果 + * Parser:SQL 语句分析器 + * Optimizer:查询优化器 + * Caches & Buffers:查询缓存,服务器会查询内部的缓存,如果缓存空间足够大,可以在大量读操作的环境中提升系统性能 + * 所有**跨存储引擎的功能**在这一层实现,如存储过程、触发器、视图等 + * 在该层服务器会解析查询并创建相应的内部解析树,并对其完成相应的优化如确定表的查询顺序,是否利用索引等, 最后生成相应的执行操作 + * MySQL 中服务器层不管理事务,**事务是由存储引擎实现的** +- 第三层:存储引擎层 + - Pluggable Storage Engines:存储引擎接口,MySQL 区别于其他数据库的重要特点就是其存储引擎的架构模式是插件式的(存储引擎是基于表的,而不是数据库) + - 存储引擎**真正的负责了 MySQL 中数据的存储和提取**,服务器通过 API 和存储引擎进行通信 + - 不同的存储引擎具有不同的功能,共用一个 Server 层,可以根据开发的需要,来选取合适的存储引擎 +- 第四层:系统文件层 + - 数据存储层,主要是将数据存储在文件系统之上,并完成与存储引擎的交互 + - File System:文件系统,保存配置文件、数据文件、日志文件、错误文件、二进制文件等 + +![](https://seazean.oss-cn-beijing.aliyuncs.com/img/DB/MySQL-体系结构.png) + + + +*** + + + +### 建立连接 + +#### 连接器 + +池化技术:对于访问数据库来说,建立连接的代价是比较昂贵的,因为每个连接对应一个用来交互的线程,频繁的创建关闭连接比较耗费资源,有必要建立数据库连接池,以提高访问的性能 + +连接建立 TCP 以后需要做**权限验证**,验证成功后可以进行执行 SQL。如果这时管理员账号对这个用户的权限做了修改,也不会影响已经存在连接的权限,只有再新建的连接才会使用新的权限设置 + +MySQL 服务器可以同时和多个客户端进行交互,所以要保证每个连接会话的隔离性(事务机制部分详解) + +整体的执行流程: + + + + + +*** + + + +#### 权限信息 + +grant 语句会同时修改数据表和内存,判断权限的时候使用的是内存数据 + +flush privileges 语句本身会用数据表(磁盘)的数据重建一份内存权限数据,所以在权限数据可能存在不一致的情况下使用,这种不一致往往是由于直接用 DML 语句操作系统权限表导致的,所以尽量不要使用这类语句 + +![](https://seazean.oss-cn-beijing.aliyuncs.com/img/DB/MySQL-权限范围.png) + + + + + + + +**** + + + +#### 连接状态 + +客户端如果长时间没有操作,连接器就会自动断开,时间是由参数 wait_timeout 控制的,默认值是 8 小时。如果在连接被断开之后,客户端**再次发送请求**的话,就会收到一个错误提醒:`Lost connection to MySQL server during query` + +数据库里面,长连接是指连接成功后,如果客户端持续有请求,则一直使用同一个连接;短连接则是指每次执行完很少的几次查询就断开连接,下次查询再重新建立一个 + +为了减少连接的创建,推荐使用长连接,但是**过多的长连接会造成 OOM**,解决方案: + +* 定期断开长连接,使用一段时间,或者程序里面判断执行过一个占用内存的大查询后,断开连接,之后要查询再重连 + + ```mysql + KILL CONNECTION id + ``` + +* MySQL 5.7 版本,可以在每次执行一个比较大的操作后,通过执行 mysql_reset_connection 来重新初始化连接资源,这个过程不需要重连和重新做权限验证,但是会将连接恢复到刚刚创建完时的状态 + +SHOW PROCESSLIST:查看当前 MySQL 在进行的线程,可以实时地查看 SQL 的执行情况,其中的 Command 列显示为 Sleep 的这一行,就表示现在系统里面有一个空闲连接 + +![](https://seazean.oss-cn-beijing.aliyuncs.com/img/DB/MySQL-SHOW_PROCESSLIST命令.png) + +| 参数 | 含义 | +| ------- | ------------------------------------------------------------ | +| ID | 用户登录 mysql 时系统分配的 connection_id,可以使用函数 connection_id() 查看 | +| User | 显示当前用户,如果不是 root,这个命令就只显示用户权限范围的 sql 语句 | +| Host | 显示这个语句是从哪个 ip 的哪个端口上发的,可以用来跟踪出现问题语句的用户 | +| db | 显示这个进程目前连接的是哪个数据库 | +| Command | 显示当前连接的执行的命令,一般取值为休眠 Sleep、查询 Query、连接 Connect 等 | +| Time | 显示这个状态持续的时间,单位是秒 | +| State | 显示使用当前连接的 sql 语句的状态,以查询为例,需要经过 copying to tmp table、sorting result、sending data等状态才可以完成 | +| Info | 显示执行的 sql 语句,是判断问题语句的一个重要依据 | + +**Sending data 状态**表示 MySQL 线程开始访问数据行并把结果返回给客户端,而不仅仅只是返回给客户端,是处于执行器过程中的任意阶段。由于在 Sending data 状态下,MySQL 线程需要做大量磁盘读取操作,所以是整个查询中耗时最长的状态 + + + + + +*** + + + +### 执行流程 + +#### 查询缓存 + +##### 工作流程 + +当执行完全相同的 SQL 语句的时候,服务器就会直接从缓存中读取结果,当数据被修改,之前的缓存会失效,修改比较频繁的表不适合做查询缓存 + +查询过程: + +1. 客户端发送一条查询给服务器 +2. 服务器先会检查查询缓存,如果命中了缓存,则立即返回存储在缓存中的结果(一般是 K-V 键值对),否则进入下一阶段 +3. 分析器进行 SQL 分析,再由优化器生成对应的执行计划 +4. 执行器根据优化器生成的执行计划,调用存储引擎的 API 来执行查询 +5. 将结果返回给客户端 + +大多数情况下不建议使用查询缓存,因为查询缓存往往弊大于利 + +* 查询缓存的**失效非常频繁**,只要有对一个表的更新,这个表上所有的查询缓存都会被清空。因此很可能费力地把结果存起来,还没使用就被一个更新全清空了,对于更新压力大的数据库来说,查询缓存的命中率会非常低 +* 除非业务就是有一张静态表,很长时间才会更新一次,比如一个系统配置表,那这张表上的查询才适合使用查询缓存 + + + +*** + + + +##### 缓存配置 + +1. 查看当前 MySQL 数据库是否支持查询缓存: + + ```mysql + SHOW VARIABLES LIKE 'have_query_cache'; -- YES + ``` + +2. 查看当前 MySQL 是否开启了查询缓存: + + ```mysql + SHOW VARIABLES LIKE 'query_cache_type'; -- OFF + ``` + + 参数说明: + + * OFF 或 0:查询缓存功能关闭 + + * ON 或 1:查询缓存功能打开,查询结果符合缓存条件即会缓存,否则不予缓存;可以显式指定 SQL_NO_CACHE 不予缓存 + + * DEMAND 或 2:查询缓存功能按需进行,显式指定 SQL_CACHE 的 SELECT 语句才缓存,其它不予缓存 + + ```mysql + SELECT SQL_CACHE id, name FROM customer; -- SQL_CACHE:查询结果可缓存 + SELECT SQL_NO_CACHE id, name FROM customer;-- SQL_NO_CACHE:不使用查询缓存 + ``` + +3. 查看查询缓存的占用大小: + + ```mysql + SHOW VARIABLES LIKE 'query_cache_size';-- 单位是字节 1048576 / 1024 = 1024 = 1KB + ``` + +4. 查看查询缓存的状态变量: + + ```mysql + SHOW STATUS LIKE 'Qcache%'; + ``` + + + + | 参数 | 含义 | + | ----------------------- | ------------------------------------------------------------ | + | Qcache_free_blocks | 查询缓存中的可用内存块数 | + | Qcache_free_memory | 查询缓存的可用内存量 | + | Qcache_hits | 查询缓存命中数 | + | Qcache_inserts | 添加到查询缓存的查询数 | + | Qcache_lowmen_prunes | 由于内存不足而从查询缓存中删除的查询数 | + | Qcache_not_cached | 非缓存查询的数量(由于 query_cache_type 设置而无法缓存或未缓存) | + | Qcache_queries_in_cache | 查询缓存中注册的查询数 | + | Qcache_total_blocks | 查询缓存中的块总数 | + +5. 配置 my.cnf: + + ```sh + sudo chmod 666 /etc/mysql/my.cnf + vim my.cnf + # mysqld中配置缓存 + query_cache_type=1 + ``` + + 重启服务既可生效,执行 SQL 语句进行验证 ,执行一条比较耗时的 SQL 语句,然后再多执行几次,查看后面几次的执行时间;获取通过查看查询缓存的缓存命中数,来判定是否走查询缓存 + + + +*** + + + +##### 缓存失效 + +查询缓存失效的情况: + +* SQL 语句不一致,要想命中查询缓存,查询的 SQL 语句必须一致,因为**缓存中 key 是查询的语句**,value 是查询结构 + + ```mysql + select count(*) from tb_item; + Select count(*) from tb_item; -- 不走缓存,首字母不一致 + ``` + +* 当查询语句中有一些不确定查询时,则不会缓存,比如:now()、current_date()、curdate()、curtime()、rand()、uuid()、user()、database() + + ```mysql + SELECT * FROM tb_item WHERE updatetime < NOW() LIMIT 1; + SELECT USER(); + SELECT DATABASE(); + ``` + +* 不使用任何表查询语句: + + ```mysql + SELECT 'A'; + ``` + +* 查询 mysql、information_schema、performance_schema 等系统表时,不走查询缓存: + + ```mysql + SELECT * FROM information_schema.engines; + ``` + +* 在**跨存储引擎**的存储过程、触发器或存储函数的主体内执行的查询,缓存失效 + +* 如果表更改,则使用该表的**所有高速缓存查询都将变为无效**并从高速缓存中删除,包括使用 MERGE 映射到已更改表的表的查询,比如:INSERT、UPDATE、DELETE、ALTER TABLE、DROP TABLE、DROP DATABASE + + + +*** + + + +#### 分析器 + +没有命中查询缓存,就开始了 SQL 的真正执行,分析器会对 SQL 语句做解析 + +```sql +SELECT * FROM t WHERE id = 1; +``` + +解析器:处理语法和解析查询,生成一课对应的解析树 + +* 先做**词法分析**,输入的是由多个字符串和空格组成的一条 SQL 语句,MySQL 需要识别出里面的字符串分别是什么代表什么。从输入的 select 这个关键字识别出来这是一个查询语句;把字符串 t 识别成 表名 t,把字符串 id 识别成列 id +* 然后做**语法分析**,根据词法分析的结果,语法分析器会根据语法规则,判断你输入的这个 SQL 语句是否满足 MySQL 语法。如果语句不对,就会收到 `You have an error in your SQL syntax` 的错误提醒 + +预处理器:进一步检查解析树的合法性,比如数据表和数据列是否存在、别名是否有歧义等 + + + +*** + + + +#### 优化器 + +##### 成本分析 + +优化器是在表里面有多个索引的时候,决定使用哪个索引;或者在一个语句有多表关联(join)的时候,决定各个表的连接顺序 + +* 根据搜索条件找出所有可能的使用的索引 +* 成本分析,执行成本由 I/O 成本和 CPU 成本组成,计算全表扫描和使用不同索引执行 SQL 的代价 +* 找到一个最优的执行方案,用最小的代价去执行语句 + +在数据库里面,扫描行数是影响执行代价的因素之一,扫描的行数越少意味着访问磁盘的次数越少,消耗的 CPU 资源越少,优化器还会结合是否使用临时表、是否排序等因素进行综合判断 + + + +*** + + + +##### 统计数据 + +MySQL 中保存着两种统计数据: + +* innodb_table_stats 存储了表的统计数据,每一条记录对应着一个表的统计数据 +* innodb_index_stats 存储了索引的统计数据,每一条记录对应着一个索引的一个统计项的数据 + +MySQL 在真正执行语句之前,并不能精确地知道满足条件的记录有多少条,只能根据统计信息来估算记录,统计信息就是索引的区分度,一个索引上不同的值的个数(比如性别只能是男女,就是 2 ),称之为基数(cardinality),**基数越大说明区分度越好** + +通过**采样统计**来获取基数,InnoDB 默认会选择 N 个数据页,统计这些页面上的不同值得到一个平均值,然后乘以这个索引的页面数,就得到了这个索引的基数 + +在 MySQL 中,有两种存储统计数据的方式,可以通过设置参数 `innodb_stats_persistent` 的值来选择: + +* ON:表示统计信息会持久化存储(默认),采样页数 N 默认为 20,可以通过 `innodb_stats_persistent_sample_pages` 指定,页数越多统计的数据越准确,但消耗的资源更大 +* OFF:表示统计信息只存储在内存,采样页数 N 默认为 8,也可以通过系统变量设置(不推荐,每次重新计算浪费资源) + +数据表是会持续更新的,两种统计信息的更新方式: + +* 设置 `innodb_stats_auto_recalc` 为 1,当发生变动的记录数量超过表大小的 10% 时,自动触发重新计算,不过是**异步进行** +* 调用 `ANALYZE TABLE t` 手动更新统计信息,只对信息做**重新统计**(不是重建表),没有修改数据,这个过程中加了 MDL 读锁并且是同步进行,所以会暂时阻塞系统 + +**EXPLAIN 执行计划在优化器阶段生成**,如果 explain 的结果预估的 rows 值跟实际情况差距比较大,可以执行 analyze 命令重新修正信息 + + + +*** + + + +##### 错选索引 + +采样统计本身是估算数据,或者 SQL 语句中的字段选择有问题时,可能导致 MySQL 没有选择正确的执行索引 + +解决方法: + +* 采用 force index 强行选择一个索引 + + ```sql + SELECT * FROM user FORCE INDEX(name) WHERE NAME='seazean'; + ``` + +* 可以考虑修改 SQL 语句,引导 MySQL 使用期望的索引 + +* 新建一个更合适的索引,来提供给优化器做选择,或删掉误用的索引 + + + +*** + + + +#### 执行器 + +开始执行的时候,要先判断一下当前连接对表有没有**执行查询的权限**,如果没有就会返回没有权限的错误,在工程实现上,如果命中查询缓存,会在查询缓存返回结果的时候,做权限验证。如果有权限,就打开表继续执行,执行器就会根据表的引擎定义,去使用这个引擎提供的接口 + + + +*** + + + +#### 引擎层 + +Server 层和存储引擎层的交互是**以记录为单位的**,存储引擎会将单条记录返回给 Server 层做进一步处理,并不是直接返回所有的记录 + +工作流程: + +* 首先根据二级索引选择扫描范围,获取第一条符合二级索引条件的记录,进行回表查询,将聚簇索引的记录返回 Server 层,由 Server 判断记录是否符合要求 +* 然后在二级索引上继续扫描下一个符合条件的记录 + + + +推荐阅读:https://mp.weixin.qq.com/s/YZ-LckObephrP1f15mzHpA + + + + + +*** + + + +### 终止流程 + +#### 终止语句 + +终止线程中正在执行的语句: + +```mysql +KILL QUERY thread_id +``` + +KILL 不是马上终止的意思,而是告诉执行线程这条语句已经不需要继续执行,可以开始执行停止的逻辑(类似于打断)。因为对表做增删改查操作,会在表上加 MDL 读锁,如果线程被 KILL 时就直接终止,那这个 MDL 读锁就没机会被释放了 + +命令 `KILL QUERYthread_id_A` 的执行流程: + +* 把 session A 的运行状态改成 THD::KILL_QUERY(将变量 killed 赋值为 THD::KILL_QUERY) +* 给 session A 的执行线程发一个信号,让 session A 来处理这个 THD::KILL_QUERY 状态 + +会话处于等待状态(锁阻塞),必须满足是一个可以被唤醒的等待,必须有机会去**判断线程的状态**,如果不满足就会造成 KILL 失败 + +典型场景:innodb_thread_concurrency 为 2,代表并发线程上限数设置为 2 + +* session A 执行事务,session B 执行事务,达到线程上限;此时 session C 执行事务会阻塞等待,session D 执行 kill query C 无效 +* C 的逻辑是每 10 毫秒判断是否可以进入 InnoDB 执行,如果不行就调用 nanosleep 函数进入 sleep 状态,没有去判断线程状态 + +补充:执行 Ctrl+C 的时候,是 MySQL 客户端另外启动一个连接,然后发送一个 KILL QUERY 命令 + + + +*** + + + +#### 终止连接 + +断开线程的连接: + +```mysql +KILL CONNECTION id +``` + +断开连接后执行 SHOW PROCESSLIST 命令,如果这条语句的 Command 列显示 Killed,代表线程的状态是 KILL_CONNECTION,说明这个线程有语句正在执行,当前状态是停止语句执行中,终止逻辑耗时较长 + +* 超大事务执行期间被 KILL,这时回滚操作需要对事务执行期间生成的所有新数据版本做回收操作,耗时很长 +* 大查询回滚,如果查询过程中生成了比较大的临时文件,删除临时文件可能需要等待 IO 资源,导致耗时较长 +* DDL 命令执行到最后阶段被 KILL,需要删除中间过程的临时文件,也可能受 IO 资源影响耗时较久 + +总结:KILL CONNECTION 本质上只是把客户端的 SQL 连接断开,后面的终止流程还是要走 KILL QUERY + +一个事务被 KILL 之后,持续处于回滚状态,不应该强行重启整个 MySQL 进程,应该等待事务自己执行完成,因为重启后依然继续做回滚操作的逻辑 + + + + + +*** + + + +### 常用工具 + +#### mysql + +mysql 不是指 mysql 服务,而是指 mysql 的客户端工具 + +```sh +mysql [options] [database] +``` + +* -u --user=name:指定用户名 +* -p --password[=name]:指定密码 +* -h --host=name:指定服务器IP或域名 +* -P --port=#:指定连接端口 +* -e --execute=name:执行SQL语句并退出,在控制台执行SQL语句,而不用连接到数据库执行 + +示例: + +```sh +mysql -h 127.0.0.1 -P 3306 -u root -p +mysql -uroot -p2143 db01 -e "select * from tb_book"; +``` + + + +*** + + + +#### admin + +mysqladmin 是一个执行管理操作的客户端程序,用来检查服务器的配置和当前状态、创建并删除数据库等 + +通过 `mysqladmin --help` 指令查看帮助文档 + +```sh +mysqladmin -uroot -p2143 create 'test01'; +``` + + + +*** + + + +#### binlog + +服务器生成的日志文件以二进制格式保存,如果需要检查这些文本,就要使用 mysqlbinlog 日志管理工具 + +```sh +mysqlbinlog [options] log-files1 log-files2 ... +``` + +* -d --database=name:指定数据库名称,只列出指定的数据库相关操作 + +* -o --offset=#:忽略掉日志中的前 n 行命令。 + +* -r --result-file=name:将输出的文本格式日志输出到指定文件。 + +* -s --short-form:显示简单格式,省略掉一些信息。 + +* --start-datatime=date1 --stop-datetime=date2:指定日期间隔内的所有日志 + +* --start-position=pos1 --stop-position=pos2:指定位置间隔内的所有日志 + + + +*** + + + +#### dump + +##### 命令介绍 + +mysqldump 客户端工具用来备份数据库或在不同数据库之间进行数据迁移,备份内容包含创建表,及插入表的 SQL 语句 + +```sh +mysqldump [options] db_name [tables] +mysqldump [options] --database/-B db1 [db2 db3...] +mysqldump [options] --all-databases/-A +``` + +连接选项: + +* -u --user=name:指定用户名 +* -p --password[=name]:指定密码 +* -h --host=name:指定服务器 IP 或域名 +* -P --port=#:指定连接端口 + +输出内容选项: + +* --add-drop-database:在每个数据库创建语句前加上 Drop database 语句 +* --add-drop-table:在每个表创建语句前加上 Drop table 语句 , 默认开启,不开启 (--skip-add-drop-table) +* -n --no-create-db:不包含数据库的创建语句 +* -t --no-create-info:不包含数据表的创建语句 +* -d --no-data:不包含数据 +* -T, --tab=name:自动生成两个文件:一个 .sql 文件,创建表结构的语句;一个 .txt 文件,数据文件,相当于 select into outfile + +示例: + +```sh +mysqldump -uroot -p2143 db01 tb_book --add-drop-database --add-drop-table > a +mysqldump -uroot -p2143 -T /tmp test city +``` + + + +*** + + + +##### 数据备份 + +命令行方式: + +* 备份命令:mysqldump -u root -p 数据库名称 > 文件保存路径 +* 恢复 + 1. 登录MySQL数据库:`mysql -u root p` + 2. 删除已经备份的数据库 + 3. 重新创建与备份数据库名称相同的数据库 + 4. 使用该数据库 + 5. 导入文件执行:`source 备份文件全路径` + +更多方式参考:https://time.geekbang.org/column/article/81925 + +图形化界面: + +* 备份 + + ![图形化界面备份](https://seazean.oss-cn-beijing.aliyuncs.com/img/DB/图形化界面备份.png) + +* 恢复 + + ![图形化界面恢复](https://seazean.oss-cn-beijing.aliyuncs.com/img/DB/图形化界面恢复.png) + + + + + +*** + + + +#### import + +mysqlimport 是客户端数据导入工具,用来导入mysqldump 加 -T 参数后导出的文本文件 + +```sh +mysqlimport [options] db_name textfile1 [textfile2...] +``` + +示例: + +```sh +mysqlimport -uroot -p2143 test /tmp/city.txt +``` + +导入 sql 文件,可以使用 MySQL 中的 source 指令 : + +```mysql +source 文件全路径 +``` + + + +*** + + + +#### show + +mysqlshow 客户端对象查找工具,用来很快地查找存在哪些数据库、数据库中的表、表中的列或者索引 + +```sh +mysqlshow [options] [db_name [table_name [col_name]]] +``` + +* --count:显示数据库及表的统计信息(数据库,表 均可以不指定) + +* -i:显示指定数据库或者指定表的状态信息 + +示例: + +```sh +#查询每个数据库的表的数量及表中记录的数量 +mysqlshow -uroot -p1234 --count +#查询test库中每个表中的字段书,及行数 +mysqlshow -uroot -p1234 test --count +#查询test库中book表的详细情况 +mysqlshow -uroot -p1234 test book --count +``` + + + + + + + +**** + + + + + +## 单表操作 + +### SQL + +- SQL + + - Structured Query Language:结构化查询语言 + - 定义了操作所有关系型数据库的规则,每种数据库操作的方式可能会存在不一样的地方,称为“方言” + +- SQL 通用语法 + + - SQL 语句可以单行或多行书写,以**分号结尾**。 + - 可使用空格和缩进来增强语句的可读性。 + - MySQL 数据库的 SQL 语句不区分大小写,**关键字建议使用大写**。 + - 数据库的注释: + - 单行注释:-- 注释内容 #注释内容(MySQL 特有) + - 多行注释:/* 注释内容 */ + +- SQL 分类 + + - DDL(Data Definition Language)数据定义语言 + + - 用来定义数据库对象:数据库,表,列等。关键字:create、drop,、alter 等 + + - DML(Data Manipulation Language)数据操作语言 + + - 用来对数据库中表的数据进行增删改。关键字:insert、delete、update 等 + + - DQL(Data Query Language)数据查询语言 + + - 用来查询数据库中表的记录(数据)。关键字:select、where 等 + + - DCL(Data Control Language)数据控制语言 + + - 用来定义数据库的访问权限和安全级别,及创建用户。关键字:grant, revoke等 + + ![](https://seazean.oss-cn-beijing.aliyuncs.com/img/DB/MySQL-SQL分类.png) + + + +*** + + + +### DDL + +#### 数据库 + +* R(Retrieve):查询 + + * 查询所有数据库: + + ```mysql + SHOW DATABASES; + ``` + + * 查询某个数据库的创建语句 + + ```sql + SHOW CREATE DATABASE 数据库名称; -- 标准语法 + + SHOW CREATE DATABASE mysql; -- 查看mysql数据库的创建格式 + ``` + + + +* C(Create):创建 + + * 创建数据库 + + ```mysql + CREATE DATABASE 数据库名称;-- 标准语法 + + CREATE DATABASE db1; -- 创建db1数据库 + ``` + + * 创建数据库(判断,如果不存在则创建) + + ```mysql + CREATE DATABASE IF NOT EXISTS 数据库名称; + ``` + + * 创建数据库,并指定字符集 + + ```mysql + CREATE DATABASE 数据库名称 CHARACTER SET 字符集名称; + ``` + + * 例如:创建db4数据库、如果不存在则创建,指定字符集为gbk + + ```mysql + -- 创建db4数据库、如果不存在则创建,指定字符集为gbk + CREATE DATABASE IF NOT EXISTS db4 CHARACTER SET gbk; + + -- 查看db4数据库的字符集 + SHOW CREATE DATABASE db4; + ``` + + + +* U(Update):修改 + + * 修改数据库的字符集 + + ```mysql + ALTER DATABASE 数据库名称 CHARACTER SET 字符集名称; + ``` + + * 常用字符集: + + ```mysql + --查询所有支持的字符集 + SHOW CHARSET; + --查看所有支持的校对规则 + SHOW COLLATION; + + -- 字符集: utf8,latinI,GBK,,GBK是utf8的子集 + -- 校对规则: ci 大小定不敏感,cs或bin大小写敏感 + ``` + + + +* D(Delete):删除 + + * 删除数据库: + + ```mysql + DROP DATABASE 数据库名称; + ``` + + * 删除数据库(判断,如果存在则删除): + + ```mysql + DROP DATABASE IF EXISTS 数据库名称; + ``` + + + +* 使用数据库: + + * 查询当前正在使用的数据库名称 + + ```mysql + SELECT DATABASE(); + ``` + + * 使用数据库 + + ```mysql + USE 数据库名称; -- 标准语法 + USE db4; -- 使用db4数据库 + ``` + + + + +*** + + + +#### 数据表 + +- R(Retrieve):查询 + + - 查询数据库中所有的数据表 + + ```mysql + USE mysql;-- 使用mysql数据库 + + SHOW TABLES;-- 查询库中所有的表 + ``` + + - 查询表结构 + + ```mysql + DESC 表名; + ``` + + - 查询表字符集 + + ```mysql + SHOW TABLE STATUS FROM 库名 LIKE '表名'; + ``` + + + +- C(Create):创建 + + - 创建数据表 + + ```mysql + CREATE TABLE 表名( + 列名1 数据类型1, + 列名2 数据类型2, + .... + 列名n 数据类型n + ); + -- 注意:最后一列,不需要加逗号 + ``` + + - 复制表 + + ```mysql + CREATE TABLE 表名 LIKE 被复制的表名; -- 标准语法 + + CREATE TABLE product2 LIKE product; -- 复制product表到product2表 + ``` + + - 数据类型 + + | 数据类型 | 说明 | + | --------- | ------------------------------------------------------------ | + | INT | 整数类型 | + | DOUBLE | 小数类型 | + | DATE | 日期,只包含年月日:yyyy-MM-dd | + | DATETIME | 日期,包含年月日时分秒:yyyy-MM-dd HH:mm:ss | + | TIMESTAMP | 时间戳类型,包含年月日时分秒:yyyy-MM-dd HH:mm:ss
如果不给这个字段赋值或赋值为 NULL,则默认使用当前的系统时间 | + | CHAR | 字符串,定长类型 | + | VARCHAR | 字符串,**变长类型**
name varchar(20) 代表姓名最大 20 个字符:zhangsan 8 个字符,张三 2 个字符 | + + `INT(n)`:n 代表位数 + + * 3:int(9)显示结果为 000000010 + * 3:int(3)显示结果为 010 + + `varchar(n)`:n 表示的是字符数 + + - 例如: + + ```mysql + -- 使用db3数据库 + USE db3; + + -- 创建一个product商品表 + CREATE TABLE product( + id INT, -- 商品编号 + NAME VARCHAR(30), -- 商品名称 + price DOUBLE, -- 商品价格 + stock INT, -- 商品库存 + insert_time DATE -- 上架时间 + ); + ``` + +​ + +- U(Update):修改 + + - 修改表名 + + ```mysql + ALTER TABLE 表名 RENAME TO 新的表名; + ``` + + - 修改表的字符集 + + ```mysql + ALTER TABLE 表名 CHARACTER SET 字符集名称; + ``` + + - 添加一列 + + ```mysql + ALTER TABLE 表名 ADD 列名 数据类型; + ``` + + - 修改列数据类型 + + ```mysql + ALTER TABLE 表名 MODIFY 列名 新数据类型; + ``` + + - 修改列名称和数据类型 + + ```mysql + ALTER TABLE 表名 CHANGE 列名 新列名 新数据类型; + ``` + + - 删除列 + + ```mysql + ALTER TABLE 表名 DROP 列名; + ``` + + + +- D(Delete):删除 + + - 删除数据表 + + ```mysql + DROP TABLE 表名; + ``` + + - 删除数据表(判断,如果存在则删除) + + ```mysql + DROP TABLE IF EXISTS 表名; + ``` + + + +*** + + + +### DML + +#### INSERT + +* 新增表数据 + + * 新增格式 1:给指定列添加数据 + + ```mysql + INSERT INTO 表名(列名1,列名2...) VALUES (值1,值2...); + ``` + + * 新增格式 2:默认给全部列添加数据 + + ```mysql + INSERT INTO 表名 VALUES (值1,值2,值3,...); + ``` + + * 新增格式 3:批量添加数据 + + ```mysql + -- 给指定列批量添加数据 + INSERT INTO 表名(列名1,列名2,...) VALUES (值1,值2,...),(值1,值2,...)...; + + -- 默认给所有列批量添加数据 + INSERT INTO 表名 VALUES (值1,值2,值3,...),(值1,值2,值3,...)...; + ``` + +* 字符串拼接 + + ```mysql + CONCAT(string1,string2,'',...) + ``` + + + +* 注意事项 + + - 列名和值的数量以及数据类型要对应 + - 除了数字类型,其他数据类型的数据都需要加引号(单引双引都可以,推荐单引) + + + +*** + + + +#### UPDATE + +* 修改表数据语法 + + * 标准语法 + + ```mysql + UPDATE 表名 SET 列名1 = 值1,列名2 = 值2,... [where 条件]; + ``` + + * 修改电视的价格为1800、库存为36 + + ```mysql + UPDATE product SET price=1800,stock=36 WHERE NAME='电视'; + SELECT * FROM product;-- 查看所有商品信息 + ``` + +* 注意事项 + + - 修改语句中必须加条件 + - 如果不加条件,则将所有数据都修改 + + + +*** + + + +#### DELETE + +* 删除表数据语法 + + ```mysql + DELETE FROM 表名 [WHERE 条件]; + ``` + +* 注意事项 + * 删除语句中必须加条件 + * 如果不加条件,则将所有数据删除 + + + +​ + +*** + + + +### DQL + +#### 查询语法 + +数据库查询遵循条件在前的原则 + +```mysql +SELECT DISTINCT + + +ORDER BY + +LIMIT +``` + + + +*** + + + +#### 查询全部 + +* 查询全部的表数据 + + ```mysql + -- 标准语法 + SELECT * FROM 表名; + + -- 查询product表所有数据(常用) + SELECT * FROM product; + ``` + +* 查询指定字段的表数据 + + ```mysql + SELECT 列名1,列名2,... FROM 表名; + ``` + +* **去除重复查询**:只有值全部重复的才可以去除,需要创建临时表辅助查询 + + ```mysql + SELECT DISTINCT 列名1,列名2,... FROM 表名; + ``` + +* 计算列的值(四则运算) + + ```mysql + SELECT 列名1 运算符(+ - * /) 列名2 FROM 表名; + + /*如果某一列值为null,可以进行替换 + ifnull(表达式1,表达式2) + 表达式1:想替换的列 + 表达式2:想替换的值*/ + ``` + + 例如: + + ```mysql + -- 查询商品名称和库存,库存数量在原有基础上加10 + SELECT NAME,stock+10 FROM product; + + -- 查询商品名称和库存,库存数量在原有基础上加10。进行null值判断 + SELECT NAME,IFNULL(stock,0)+10 FROM product; + ``` + +* **起别名** + + ```mysql + SELECT 列名1,列名2,... AS 别名 FROM 表名; + ``` + + 例如: + + ```mysql + -- 查询商品名称和库存,库存数量在原有基础上加10。进行null值判断,起别名为getSum,AS可以省略。 + SELECT NAME,IFNULL(stock,0)+10 AS getsum FROM product; + SELECT NAME,IFNULL(stock,0)+10 getsum FROM product; + ``` + + + + +*** + + + +#### 条件查询 + +* 条件查询语法 + + ```mysql + SELECT 列名 FROM 表名 WHERE 条件; + ``` + +* 条件分类 + + | 符号 | 功能 | + | ------------------- | ------------------------------------------------------------ | + | > | 大于 | + | < | 小于 | + | >= | 大于等于 | + | <= | 小于等于 | + | = | 等于 | + | <> 或 != | 不等于 | + | BETWEEN ... AND ... | 在某个范围之内(都包含) | + | IN(...) | 多选一 | + | LIKE | **模糊查询**:_单个任意字符、%任意个字符、[] 匹配集合内的字符
`LIKE '[^AB]%' `:不以 A 和 B 开头的任意文本 | + | IS NULL | 是NULL | + | IS NOT NULL | 不是NULL | + | AND 或 && | 并且 | + | OR 或 \|\| | 或者 | + | NOT 或 ! | 非,不是 | + | UNION | 对两个结果集进行**并集操作并进行去重,同时进行默认规则的排序** | + | UNION ALL | 对两个结果集进行并集操作不进行去重,不进行排序 | + +* 例如: + + ```mysql + -- 查询库存大于20的商品信息 + SELECT * FROM product WHERE stock > 20; + + -- 查询品牌为华为的商品信息 + SELECT * FROM product WHERE brand='华为'; + + -- 查询金额在4000 ~ 6000之间的商品信息 + SELECT * FROM product WHERE price >= 4000 AND price <= 6000; + SELECT * FROM product WHERE price BETWEEN 4000 AND 6000; + + -- 查询库存为14、30、23的商品信息 + SELECT * FROM product WHERE stock=14 OR stock=30 OR stock=23; + SELECT * FROM product WHERE stock IN(14,30,23); + + -- 查询库存为null的商品信息 + SELECT * FROM product WHERE stock IS NULL; + -- 查询库存不为null的商品信息 + SELECT * FROM product WHERE stock IS NOT NULL; + + -- 查询名称以'小米'为开头的商品信息 + SELECT * FROM product WHERE NAME LIKE '小米%'; + + -- 查询名称第二个字是'为'的商品信息 + SELECT * FROM product WHERE NAME LIKE '_为%'; + + -- 查询名称为四个字符的商品信息 4个下划线 + SELECT * FROM product WHERE NAME LIKE '____'; + + -- 查询名称中包含电脑的商品信息 + SELECT * FROM product WHERE NAME LIKE '%电脑%'; + ``` + + + + + +*** + + + +#### 函数查询 + +##### 聚合函数 + +聚合函数:将一列数据作为一个整体,进行纵向的计算 + +* 聚合函数语法 + + ```mysql + SELECT 函数名(列名) FROM 表名 [WHERE 条件] + ``` + +* 聚合函数分类 + + | 函数名 | 功能 | + | ----------- | ---------------------------------- | + | COUNT(列名) | 统计数量(一般选用不为 null 的列) | + | MAX(列名) | 最大值 | + | MIN(列名) | 最小值 | + | SUM(列名) | 求和 | + | AVG(列名) | 平均值(会忽略 null 行) | + +* 例如 + + ```mysql + -- 计算product表中总记录条数 7 + SELECT COUNT(*) FROM product; + + -- 获取最高价格 + SELECT MAX(price) FROM product; + -- 获取最高价格的商品名称 + SELECT NAME,price FROM product WHERE price = (SELECT MAX(price) FROM product); + + -- 获取最低库存 + SELECT MIN(stock) FROM product; + -- 获取最低库存的商品名称 + SELECT NAME,stock FROM product WHERE stock = (SELECT MIN(stock) FROM product); + + -- 获取总库存数量 + SELECT SUM(stock) FROM product; + -- 获取品牌为小米的平均商品价格 + SELECT AVG(price) FROM product WHERE brand='小米'; + ``` + + + +*** + + + +##### 文本函数 + +CONCAT():用于连接两个字段 + +```sql +SELECT CONCAT(TRIM(col1), '(', TRIM(col2), ')') AS concat_col FROM mytable +-- 许多数据库会使用空格把一个值填充为列宽,连接的结果出现一些不必要的空格,使用TRIM()可以去除首尾空格 +``` + +| 函数名称 | 作 用 | +| --------- | ------------------------------------------------------------ | +| LENGTH | 计算字符串长度函数,返回字符串的字节长度 | +| CONCAT | 合并字符串函数,返回结果为连接参数产生的字符串,参数可以使一个或多个 | +| INSERT | 替换字符串函数 | +| LOWER | 将字符串中的字母转换为小写 | +| UPPER | 将字符串中的字母转换为大写 | +| LEFT | 从左侧字截取符串,返回字符串左边的若干个字符 | +| RIGHT | 从右侧字截取符串,返回字符串右边的若干个字符 | +| TRIM | 删除字符串左右两侧的空格 | +| REPLACE | 字符串替换函数,返回替换后的新字符串 | +| SUBSTRING | 截取字符串,返回从指定位置开始的指定长度的字符换 | +| REVERSE | 字符串反转(逆序)函数,返回与原始字符串顺序相反的字符串 | + + + +*** + + + +##### 数字函数 + +| 函数名称 | 作 用 | +| --------------- | ---------------------------------------------------------- | +| ABS | 求绝对值 | +| SQRT | 求二次方根 | +| MOD | 求余数 | +| CEIL 和 CEILING | 两个函数功能相同,都是返回不小于参数的最小整数,即向上取整 | +| FLOOR | 向下取整,返回值转化为一个BIGINT | +| RAND | 生成一个0~1之间的随机数,传入整数参数是,用来产生重复序列 | +| ROUND | 对所传参数进行四舍五入 | +| SIGN | 返回参数的符号 | +| POW 和 POWER | 两个函数的功能相同,都是所传参数的次方的结果值 | +| SIN | 求正弦值 | +| ASIN | 求反正弦值,与函数 SIN 互为反函数 | +| COS | 求余弦值 | +| ACOS | 求反余弦值,与函数 COS 互为反函数 | +| TAN | 求正切值 | +| ATAN | 求反正切值,与函数 TAN 互为反函数 | +| COT | 求余切值 | + + + +*** + + + +##### 日期函数 + +| 函数名称 | 作 用 | +| ----------------------- | ------------------------------------------------------------ | +| CURDATE 和 CURRENT_DATE | 两个函数作用相同,返回当前系统的日期值 | +| CURTIME 和 CURRENT_TIME | 两个函数作用相同,返回当前系统的时间值 | +| NOW 和 SYSDATE | 两个函数作用相同,返回当前系统的日期和时间值 | +| MONTH | 获取指定日期中的月份 | +| MONTHNAME | 获取指定日期中的月份英文名称 | +| DAYNAME | 获取指定曰期对应的星期几的英文名称 | +| DAYOFWEEK | 获取指定日期对应的一周的索引位置值 | +| WEEK | 获取指定日期是一年中的第几周,返回值的范围是否为 0〜52 或 1〜53 | +| DAYOFYEAR | 获取指定曰期是一年中的第几天,返回值范围是1~366 | +| DAYOFMONTH | 获取指定日期是一个月中是第几天,返回值范围是1~31 | +| YEAR | 获取年份,返回值范围是 1970〜2069 | +| TIME_TO_SEC | 将时间参数转换为秒数 | +| SEC_TO_TIME | 将秒数转换为时间,与TIME_TO_SEC 互为反函数 | +| DATE_ADD 和 ADDDATE | 两个函数功能相同,都是向日期添加指定的时间间隔 | +| DATE_SUB 和 SUBDATE | 两个函数功能相同,都是向日期减去指定的时间间隔 | +| ADDTIME | 时间加法运算,在原始时间上添加指定的时间 | +| SUBTIME | 时间减法运算,在原始时间上减去指定的时间 | +| DATEDIFF | 获取两个日期之间间隔,返回参数 1 减去参数 2 的值 | +| DATE_FORMAT | 格式化指定的日期,根据参数返回指定格式的值 | +| WEEKDAY | 获取指定日期在一周内的对应的工作日索引 | + + + +**** + + + +#### 正则查询 + +正则表达式(Regular Expression)是指一个用来描述或者匹配一系列符合某个句法规则的字符串的单个字符串 + +```mysql +SELECT * FROM emp WHERE name REGEXP '^T'; -- 匹配以T开头的name值 +SELECT * FROM emp WHERE name REGEXP '2$'; -- 匹配以2结尾的name值 +SELECT * FROM emp WHERE name REGEXP '[uvw]';-- 匹配包含 uvw 的name值 +``` + +| 符号 | 含义 | +| ------ | ----------------------------- | +| ^ | 在字符串开始处进行匹配 | +| $ | 在字符串末尾处进行匹配 | +| . | 匹配任意单个字符, 包括换行符 | +| [...] | 匹配出括号内的任意字符 | +| [^...] | 匹配不出括号内的任意字符 | +| a* | 匹配零个或者多个a(包括空串) | +| a+ | 匹配一个或者多个a(不包括空串) | +| a? | 匹配零个或者一个a | +| a1\|a2 | 匹配a1或a2 | +| a(m) | 匹配m个a | +| a(m,) | 至少匹配m个a | +| a(m,n) | 匹配m个a 到 n个a | +| a(,n) | 匹配0到n个a | +| (...) | 将模式元素组成单一元素 | + + + + + +*** + + + +#### 排序查询 + +* 排序查询语法 + + ```mysql + SELECT 列名 FROM 表名 [WHERE 条件] ORDER BY 列名1 排序方式1,列名2 排序方式2; + ``` + +* 排序方式 + + ```mysql + ASC:升序 + DESC:降序 + ``` + + 注意:多个排序条件,当前边的条件值一样时,才会判断第二条件 + +* 例如 + + ```mysql + -- 按照库存升序排序 + SELECT * FROM product ORDER BY stock ASC; + + -- 查询名称中包含手机的商品信息。按照金额降序排序 + SELECT * FROM product WHERE NAME LIKE '%手机%' ORDER BY price DESC; + + -- 按照金额升序排序,如果金额相同,按照库存降序排列 + SELECT * FROM product ORDER BY price ASC,stock DESC; + ``` + + + +*** + + + +#### 分组查询 + +分组查询会进行去重 + +* 分组查询语法 + + ````mysql + SELECT 列名 FROM 表名 [WHERE 条件] GROUP BY 分组列名 [HAVING 分组后条件过滤] [ORDER BY 排序列名 排序方式]; + ```` + + WHERE 过滤行,HAVING 过滤分组,行过滤应当先于分组过滤 + + 分组规定: + + * GROUP BY 子句出现在 WHERE 子句之后,ORDER BY 子句之前 + * NULL 的行会单独分为一组 + * 大多数 SQL 实现不支持 GROUP BY 列具有可变长度的数据类型 + +* 例如 + + ```mysql + -- 按照品牌分组,获取每组商品的总金额 + SELECT brand,SUM(price) FROM product GROUP BY brand; + + -- 对金额大于4000元的商品,按照品牌分组,获取每组商品的总金额 + SELECT brand,SUM(price) FROM product WHERE price > 4000 GROUP BY brand; + + -- 对金额大于4000元的商品,按照品牌分组,获取每组商品的总金额,只显示总金额大于7000元的 + SELECT brand,SUM(price) AS getSum FROM product WHERE price > 4000 GROUP BY brand HAVING getSum > 7000; + + -- 对金额大于4000元的商品,按照品牌分组,获取每组商品的总金额,只显示总金额大于7000元的、并按照总金额的降序排列 + SELECT brand,SUM(price) AS getSum FROM product WHERE price > 4000 GROUP BY brand HAVING getSum > 7000 ORDER BY getSum DESC; + ``` + + + + +*** + + + +#### 分页查询 + +* 分页查询语法 + + ```mysql + SELECT 列名 FROM 表名 [WHERE 条件] GROUP BY 分组列名 [HAVING 分组后条件过滤] [ORDER BY 排序列名 排序方式] LIMIT 开始索引,查询条数; + ``` + +* 公式:开始索引 = (当前页码-1) * 每页显示的条数 + +* 例如 + + ```mysql + SELECT * FROM product LIMIT 0,2; -- 第一页 开始索引=(1-1) * 2 + SELECT * FROM product LIMIT 2,2; -- 第二页 开始索引=(2-1) * 2 + SELECT * FROM product LIMIT 4,2; -- 第三页 开始索引=(3-1) * 2 + SELECT * FROM product LIMIT 6,2; -- 第四页 开始索引=(4-1) * 2 + ``` + + ![](https://seazean.oss-cn-beijing.aliyuncs.com/img/DB/MySQL-DQL分页查询图解.png) + + + + + + +*** + + + + + +## 多表操作 + +### 约束分类 + +#### 约束介绍 + +约束:对表中的数据进行限定,保证数据的正确性、有效性、完整性 + +约束的分类: + +| 约束 | 说明 | +| ----------------------------- | -------------- | +| PRIMARY KEY | 主键约束 | +| PRIMARY KEY AUTO_INCREMENT | 主键、自动增长 | +| UNIQUE | 唯一约束 | +| NOT NULL | 非空约束 | +| FOREIGN KEY | 外键约束 | +| FOREIGN KEY ON UPDATE CASCADE | 外键级联更新 | +| FOREIGN KEY ON DELETE CASCADE | 外键级联删除 | + + + +*** + + + +#### 主键约束 + +* 主键约束特点: + + * 主键约束默认包含**非空和唯一**两个功能 + * 一张表只能有一个主键 + * 主键一般用于表中数据的唯一标识 + +* 建表时添加主键约束 + + ```mysql + CREATE TABLE 表名( + 列名 数据类型 PRIMARY KEY, + 列名 数据类型, + ... + ); + ``` + +* 删除主键约束 + + ```mysql + ALTER TABLE 表名 DROP PRIMARY KEY; + ``` + +* 建表后单独添加主键约束 + + ```mysql + ALTER TABLE 表名 MODIFY 列名 数据类型 PRIMARY KEY; + ``` + +* 例如 + + ```mysql + -- 创建student表 + CREATE TABLE student( + id INT PRIMARY KEY -- 给id添加主键约束 + ); + + -- 添加数据 + INSERT INTO student VALUES (1),(2); + -- 主键默认唯一,添加重复数据,会报错 + INSERT INTO student VALUES (2); + -- 主键默认非空,不能添加null的数据 + INSERT INTO student VALUES (NULL); + ``` + + + +*** + + + +#### 主键自增 + +主键自增约束可以为空,并自动增长。删除某条数据不影响自增的下一个数值,依然按照前一个值自增 + +* 建表时添加主键自增约束 + + ```mysql + CREATE TABLE 表名( + 列名 数据类型 PRIMARY KEY AUTO_INCREMENT, + 列名 数据类型, + ... + ); + ``` + +* 删除主键自增约束 + + ```mysql + ALTER TABLE 表名 MODIFY 列名 数据类型; + ``` + +* 建表后单独添加主键自增约束 + + ```mysql + ALTER TABLE 表名 MODIFY 列名 数据类型 AUTO_INCREMENT; + ``` + +* 例如 + + ```mysql + -- 创建student2表 + CREATE TABLE student2( + id INT PRIMARY KEY AUTO_INCREMENT -- 给id添加主键自增约束 + ); + + -- 添加数据 + INSERT INTO student2 VALUES (1),(2); + -- 添加null值,会自动增长 + INSERT INTO student2 VALUES (NULL),(NULL);-- 3,4 + ``` + + + +*** + + + +#### 唯一约束 + +唯一约束:约束不能有重复的数据 + +* 建表时添加唯一约束 + + ```mysql + CREATE TABLE 表名( + 列名 数据类型 UNIQUE, + 列名 数据类型, + ... + ); + ``` + +* 删除唯一约束 + + ```mysql + ALTER TABLE 表名 DROP INDEX 列名; + ``` + +* 建表后单独添加唯一约束 + + ```mysql + ALTER TABLE 表名 MODIFY 列名 数据类型 UNIQUE; + ``` + + + +*** + + + +#### 非空约束 + +* 建表时添加非空约束 + + ```mysql + CREATE TABLE 表名( + 列名 数据类型 NOT NULL, + 列名 数据类型, + ... + ); + ``` + +* 删除非空约束 + + ```mysql + ALTER TABLE 表名 MODIFY 列名 数据类型; + ``` + +* 建表后单独添加非空约束 + + ```mysql + ALTER TABLE 表名 MODIFY 列名 数据类型 NOT NULL; + ``` + + + +*** + + + +#### 外键约束 + + 外键约束:让表和表之间产生关系,从而保证数据的准确性 + +* 建表时添加外键约束 + + ```mysql + CREATE TABLE 表名( + 列名 数据类型 约束, + ... + CONSTRAINT 外键名 FOREIGN KEY (本表外键列名) REFERENCES 主表名(主表主键列名) + ); + ``` + +* 删除外键约束 + + ```mysql + ALTER TABLE 表名 DROP FOREIGN KEY 外键名; + ``` + +* 建表后单独添加外键约束 + + ```mysql + ALTER TABLE 表名 ADD CONSTRAINT 外键名 FOREIGN KEY (本表外键列名) REFERENCES 主表名(主表主键列名); + ``` + +* 例如 + + ```mysql + -- 创建user用户表 + CREATE TABLE USER( + id INT PRIMARY KEY AUTO_INCREMENT, -- id + name VARCHAR(20) NOT NULL -- 姓名 + ); + -- 添加用户数据 + INSERT INTO USER VALUES (NULL,'张三'),(NULL,'李四'),(NULL,'王五'); + + -- 创建orderlist订单表 + CREATE TABLE orderlist( + id INT PRIMARY KEY AUTO_INCREMENT, -- id + number VARCHAR(20) NOT NULL, -- 订单编号 + uid INT, -- 订单所属用户 + CONSTRAINT ou_fk1 FOREIGN KEY (uid) REFERENCES USER(id) -- 添加外键约束 + ); + -- 添加订单数据 + INSERT INTO orderlist VALUES (NULL,'hm001',1),(NULL,'hm002',1), + (NULL,'hm003',2),(NULL,'hm004',2), + (NULL,'hm005',3),(NULL,'hm006',3); + + -- 添加一个订单,但是没有所属用户。无法添加 + INSERT INTO orderlist VALUES (NULL,'hm007',8); + -- 删除王五这个用户,但是订单表中王五还有很多个订单呢。无法删除 + DELETE FROM USER WHERE NAME='王五'; + ``` + + + + +*** + + + +#### 外键级联 + +级联操作:当把主表中的数据进行删除或更新时,从表中有关联的数据的相应操作,包括 RESTRICT、CASCADE、SET NULL 和 NO ACTION + +* RESTRICT 和 NO ACTION相同, 是指限制在子表有关联记录的情况下, 父表不能更新 + +* CASCADE 表示父表在更新或者删除时,更新或者删除子表对应的记录 + +* SET NULL 则表示父表在更新或者删除的时候,子表的对应字段被SET NULL + +级联操作: + +* 添加级联更新 + + ```mysql + ALTER TABLE 表名 ADD CONSTRAINT 外键名 FOREIGN KEY (本表外键列名) REFERENCES 主表名(主表主键列名) ON UPDATE [CASCADE | RESTRICT | SET NULL]; + ``` + +* 添加级联删除 + + ```mysql + ALTER TABLE 表名 ADD CONSTRAINT 外键名 FOREIGN KEY (本表外键列名) REFERENCES 主表名(主表主键列名) ON DELETE CASCADE; + ``` + +* 同时添加级联更新和级联删除 + + ```mysql + ALTER TABLE 表名 ADD CONSTRAINT 外键名 FOREIGN KEY (本表外键列名) REFERENCES 主表名(主表主键列名) ON UPDATE CASCADE ON DELETE CASCADE; + ``` + + + + + +*** + + + + + +### 多表设计 + +#### 一对一 + +多表:有多张数据表,而表与表之间有一定的关联关系,通过外键约束实现,分为一对一、一对多、多对多三类 + +举例:人和身份证 + +实现原则:在任意一个表建立外键,去关联另外一个表的主键 + +```mysql +-- 创建person表 +CREATE TABLE person( + id INT PRIMARY KEY AUTO_INCREMENT, -- 主键id + NAME VARCHAR(20) -- 姓名 +); +-- 添加数据 +INSERT INTO person VALUES (NULL,'张三'),(NULL,'李四'); + +-- 创建card表 +CREATE TABLE card( + id INT PRIMARY KEY AUTO_INCREMENT, -- 主键id + number VARCHAR(20) UNIQUE NOT NULL, -- 身份证号 + pid INT UNIQUE, -- 外键列 + CONSTRAINT cp_fk1 FOREIGN KEY (pid) REFERENCES person(id) +); +-- 添加数据 +INSERT INTO card VALUES (NULL,'12345',1),(NULL,'56789',2); +``` + +![](https://seazean.oss-cn-beijing.aliyuncs.com/img/DB/多表设计一对一.png) + + + +*** + + + +#### 一对多 + +举例:用户和订单、商品分类和商品 + +实现原则:在多的一方,建立外键约束,来关联一的一方主键 + +```mysql +-- 创建user表 +CREATE TABLE USER( + id INT PRIMARY KEY AUTO_INCREMENT, -- 主键id + NAME VARCHAR(20) -- 姓名 +); +-- 添加数据 +INSERT INTO USER VALUES (NULL,'张三'),(NULL,'李四'); + +-- 创建orderlist表 +CREATE TABLE orderlist( + id INT PRIMARY KEY AUTO_INCREMENT, -- 主键id + number VARCHAR(20), -- 订单编号 + uid INT, -- 外键列 + CONSTRAINT ou_fk1 FOREIGN KEY (uid) REFERENCES USER(id) +); +-- 添加数据 +INSERT INTO orderlist VALUES (NULL,'hm001',1),(NULL,'hm002',1),(NULL,'hm003',2),(NULL,'hm004',2); +``` + +![多表设计一对多](https://seazean.oss-cn-beijing.aliyuncs.com/img/DB/多表设计一对多.png) + + + +*** + + + +#### 多对多 + +举例:学生和课程。一个学生可以选择多个课程,一个课程也可以被多个学生选择 + +实现原则:借助第三张表中间表,中间表至少包含两个列,这两个列作为中间表的外键,分别关联两张表的主键 + +```mysql +-- 创建student表 +CREATE TABLE student( + id INT PRIMARY KEY AUTO_INCREMENT, -- 主键id + NAME VARCHAR(20) -- 学生姓名 +); +-- 添加数据 +INSERT INTO student VALUES (NULL,'张三'),(NULL,'李四'); + +-- 创建course表 +CREATE TABLE course( + id INT PRIMARY KEY AUTO_INCREMENT, -- 主键id + NAME VARCHAR(10) -- 课程名称 +); +-- 添加数据 +INSERT INTO course VALUES (NULL,'语文'),(NULL,'数学'); + +-- 创建中间表 +CREATE TABLE stu_course( + id INT PRIMARY KEY AUTO_INCREMENT, -- 主键id + sid INT, -- 用于和student表中的id进行外键关联 + cid INT, -- 用于和course表中的id进行外键关联 + CONSTRAINT sc_fk1 FOREIGN KEY (sid) REFERENCES student(id), -- 添加外键约束 + CONSTRAINT sc_fk2 FOREIGN KEY (cid) REFERENCES course(id) -- 添加外键约束 +); +-- 添加数据 +INSERT INTO stu_course VALUES (NULL,1,1),(NULL,1,2),(NULL,2,1),(NULL,2,2); +``` + +![](https://seazean.oss-cn-beijing.aliyuncs.com/img/DB/多表设计多对多.png) + + + +*** + + + +### 连接查询 + +#### 内外连接 + +##### 内连接 + +连接查询的是两张表有交集的部分数据,两张表分为**驱动表和被驱动表**,如果结果集中的每条记录都是两个表相互匹配的组合,则称这样的结果集为笛卡尔积 + +内连接查询,若驱动表中的记录在被驱动表中找不到匹配的记录时,则该记录不会加到最后的结果集 + +* 显式内连接: + + ```mysql + SELECT 列名 FROM 表名1 [INNER] JOIN 表名2 ON 条件; + ``` + +* 隐式内连接:内连接中 WHERE 子句和 ON 子句是等价的 + + ```mysql + SELECT 列名 FROM 表名1,表名2 WHERE 条件; + ``` + +STRAIGHT_JOIN与 JOIN 类似,只不过左表始终在右表之前读取,只适用于内连接 + + + + +*** + + + +##### 外连接 + +外连接查询,若驱动表中的记录在被驱动表中找不到匹配的记录时,则该记录也会加到最后的结果集,只是对于被驱动表中**不匹配过滤条件**的记录,各个字段使用 NULL 填充 + +应用实例:查学生成绩,也想展示出缺考的人的成绩 + +* 左外连接:选择左侧的表为驱动表,查询左表的全部数据,和左右两张表有交集部分的数据 + + ```mysql + SELECT 列名 FROM 表名1 LEFT [OUTER] JOIN 表名2 ON 条件; + ``` + +* 右外连接:选择右侧的表为驱动表,查询右表的全部数据,和左右两张表有交集部分的数据 + + ```mysql + SELECT 列名 FROM 表名1 RIGHT [OUTER] JOIN 表名2 ON 条件; + ``` + +![](https://seazean.oss-cn-beijing.aliyuncs.com/img/DB/MySQL-JOIN查询图.png) + + + + +*** + + + + + +#### 关联查询 + +自关联查询:同一张表中有数据关联,可以多次查询这同一个表 + +* 数据准备 + + ```mysql + -- 创建员工表 + CREATE TABLE employee( + id INT PRIMARY KEY AUTO_INCREMENT, -- 员工编号 + NAME VARCHAR(20), -- 员工姓名 + mgr INT, -- 上级编号 + salary DOUBLE -- 员工工资 + ); + -- 添加数据 + INSERT INTO employee VALUES (1001,'孙悟空',1005,9000.00),..,(1009,'宋江',NULL,16000.00); + ``` + + ![](https://seazean.oss-cn-beijing.aliyuncs.com/img/DB/自关联查询数据准备.png) + +* 数据查询 + + ```mysql + -- 查询所有员工的姓名及其直接上级的姓名,没有上级的员工也需要查询 + /* + 分析 + 员工信息 employee表 + 条件:employee.mgr = employee.id + 查询左表的全部数据,和左右两张表有交集部分数据,左外连接 + */ + SELECT + e1.id, + e1.name, + e1.mgr, + e2.id, + e2.name + FROM + employee e1 + LEFT OUTER JOIN + employee e2 + ON + e1.mgr = e2.id; + ``` + +* 查询结果 + + ``` + id name mgr id name + 1001 孙悟空 1005 1005 唐僧 + 1002 猪八戒 1005 1005 唐僧 + 1003 沙和尚 1005 1005 唐僧 + 1004 小白龙 1005 1005 唐僧 + 1005 唐僧 NULL NULL NULL + 1006 武松 1009 1009 宋江 + 1007 李逵 1009 1009 宋江 + 1008 林冲 1009 1009 宋江 + 1009 宋江 NULL NULL NULL + ``` + + + +*** + + + +#### 连接原理 + +Index Nested-Loop Join 算法:查询驱动表得到**数据集**,然后根据数据集中的每一条记录的**关联字段再分别**到被驱动表中查找匹配(**走索引**),所以驱动表只需要访问一次,被驱动表要访问多次 + +MySQL 将查询驱动表后得到的记录成为驱动表的扇出,连接查询的成本:单次访问驱动表的成本 + 扇出值 * 单次访问被驱动表的成本,优化器会选择成本最小的表连接顺序(确定谁是驱动表,谁是被驱动表)生成执行计划,进行连接查询,优化方式: + +* 减少驱动表的扇出(让数据量小的表来做驱动表) +* 降低访问被驱动表的成本 + +说明:STRAIGHT_JOIN 是查一条驱动表,然后根据关联字段去查被驱动表,要访问多次驱动表,所以需要优化为 INL 算法 + +Block Nested-Loop Join 算法:一种**空间换时间**的优化方式,基于块的循环连接,执行连接查询前申请一块固定大小的内存作为连接缓冲区 Join Buffer,先把若干条驱动表中的扇出暂存在缓冲区,每一条被驱动表中的记录一次性的与 Buffer 中多条记录进行匹配(扫描全部数据,一条一条的匹配),因为是在内存中完成,所以速度快,并且降低了 I/O 成本 + +Join Buffer 可以通过参数 `join_buffer_size` 进行配置,默认大小是 256 KB + +在成本分析时,对于很多张表的连接查询,连接顺序有非常多,MySQL 如果挨着进行遍历计算成本,会消耗很多资源 + +* 提前结束某种连接顺序的成本评估:维护一个全局变量记录当前成本最小的连接方式,如果一种顺序只计算了一部分就已经超过了最小成本,可以提前结束计算 +* 系统变量 optimizer_search_depth:如果连接表的个数小于该变量,就继续穷举分析每一种连接数量,反之只对数量与 depth 值相同的表进行分析,该值越大成本分析的越精确 + +* 系统变量 optimizer_prune_level:控制启发式规则的启用,这些规则就是根据以往经验指定的,不满足规则的连接顺序不分析成本 + + + +*** + + + +#### 连接优化 + +##### BKA + +Batched Key Access 算法是对 NLJ 算法的优化,在读取被驱动表的记录时使用顺序 IO,Extra 信息中会有 Batched Key Access 信息 + +使用 BKA 的表的 JOIN 过程如下: + +* 连接驱动表将满足条件的记录放入 Join Buffer,并将两表连接的字段放入一个 DYNAMIC_ARRAY ranges 中 +* 在进行表的过接过程中,会将 ranges 相关的信息传入 Buffer 中,进行被驱动表主建的查找及排序操作 +* 调用步骤 2 中产生的有序主建,**顺序读取被驱动表的数据** +* 当缓冲区的数据被读完后,会重复进行步骤 2、3,直到记录被读取完 + +使用 BKA 优化需要设进行设置: + +```mysql +SET optimizer_switch='mrr=on,mrr_cost_based=off,batched_key_access=on'; +``` + +说明:前两个参数的作用是启用 MRR,因为 BKA 算法的优化要依赖于 MRR(系统优化 → 内存优化 → Read 详解) + + + +*** + + + +##### BNL + +###### 问题 + +BNL 即 Block Nested-Loop Join 算法,由于要访问多次被驱动表,会产生两个问题: + +* Join 语句多次扫描一个冷表,并且语句执行时间小于 1 秒,就会在再次扫描冷表时,把冷表的数据页移到 LRU 链表头部,导致热数据被淘汰,影响业务的正常运行 + + 这种情况冷表的数据量要小于整个 Buffer Pool 的 old 区域,能够完全放入 old 区,才会再次被读时加到 young,否则读取下一段时就已经把上一段淘汰 + +* Join 语句在循环读磁盘和淘汰内存页,进入 old 区域的数据页很可能在 1 秒之内就被淘汰,就会导致 MySQL 实例的 Buffer Pool 在这段时间内 young 区域的数据页没有被合理地淘汰 + +大表 Join 操作虽然对 IO 有影响,但是在语句执行结束后对 IO 的影响随之结束。但是对 Buffer Pool 的影响就是持续性的,需要依靠后续的查询请求慢慢恢复内存命中率 + + + +###### 优化 + +将 BNL 算法转成 BKA 算法,优化方向: + +* 在被驱动表上建索引,这样就可以根据索引进行顺序 IO +* 使用临时表,**在临时表上建立索引**,将被驱动表和临时表进行连接查询 + +驱动表 t1,被驱动表 t2,使用临时表的工作流程: + +* 把表 t1 中满足条件的数据放在临时表 tmp_t 中 +* 给临时表 tmp_t 的关联字段加上索引,使用 BKA 算法 +* 让表 t2 和 tmp_t 做 Join 操作(临时表是被驱动表) + +补充:MySQL 8.0 支持 hash join,join_buffer 维护的不再是一个无序数组,而是一个哈希表,查询效率更高,执行效率比临时表更高 + + + + + + +*** + + + +### 嵌套查询 + +#### 查询分类 + +查询语句中嵌套了查询语句,**将嵌套查询称为子查询**,FROM 子句后面的子查询的结果集称为派生表 + +根据结果分类: + +* 结果是单行单列:可以将查询的结果作为另一条语句的查询条件,使用运算符判断 + + ```mysql + SELECT 列名 FROM 表名 WHERE 列名=(SELECT 列名/聚合函数(列名) FROM 表名 [WHERE 条件]); + ``` + +* 结果是多行单列:可以作为条件,使用运算符 IN 或 NOT IN 进行判断 + + ```mysql + SELECT 列名 FROM 表名 WHERE 列名 [NOT] IN (SELECT 列名 FROM 表名 [WHERE 条件]); + ``` + +* 结果是多行多列:查询的结果可以作为一张虚拟表参与查询 + + ```mysql + SELECT 列名 FROM 表名 [别名],(SELECT 列名 FROM 表名 [WHERE 条件]) [别名] [WHERE 条件]; + + -- 查询订单表orderlist中id大于4的订单信息和所属用户USER信息 + SELECT + * + FROM + USER u, + (SELECT * FROM orderlist WHERE id>4) o + WHERE + u.id=o.uid; + ``` + +相关性分类: + +* 不相关子查询:子查询不依赖外层查询的值,可以单独运行出结果 +* 相关子查询:子查询的执行需要依赖外层查询的值 + + + +**** + + + +#### 查询优化 + +不相关子查询的结果集会被写入一个临时表,并且在写入时**去重**,该过程称为**物化**,存储结果集的临时表称为物化表 + +系统变量 tmp_table_size 或者 max_heap_table_size 为表的最值 + +* 小于系统变量时,内存中可以保存,会为建立**基于内存**的 MEMORY 存储引擎的临时表,并建立哈希索引 +* 大于任意一个系统变量时,物化表会使用**基于磁盘**的 InnoDB 存储引擎来保存结果集中的记录,索引类型为 B+ 树 + +物化后,嵌套查询就相当于外层查询的表和物化表进行内连接查询,然后经过优化器选择成本最小的表连接顺序执行查询 + +子查询物化会产生建立临时表的成本,但是将子查询转化为连接查询可以充分发挥优化器的作用,所以引入:半连接 + +* t1 和 t2 表进行半连接,对于 t1 表中的某条记录,只需要关心在 t2 表中是否存在,而不需要关心有多少条记录与之匹配,最终结果集只保留 t1 的记录 +* 半连接只是执行子查询的一种方式,MySQL 并没有提供面向用户的半连接语法 + + + +参考书籍:https://book.douban.com/subject/35231266/ + + + +*** + + + +#### 联合查询 + +UNION 是取这两个子查询结果的并集,并进行去重,同时进行默认规则的排序(union 是行加起来,join 是列加起来) + +UNION ALL 是对两个结果集进行并集操作不进行去重,不进行排序 + +```mysql +(select 1000 as f) union (select id from t1 order by id desc limit 2); #t1表中包含id 为 1-1000 的数据 +``` + +语句的执行流程: + +* 创建一个内存临时表,这个临时表只有一个整型字段 f,并且 f 是主键字段 +* 执行第一个子查询,得到 1000 这个值,并存入临时表中 +* 执行第二个子查询,拿到第一行 id=1000,试图插入临时表中,但由于 1000 这个值已经存在于临时表了,违反了唯一性约束,所以插入失败,然后继续执行 +* 取到第二行 id=999,插入临时表成功 +* 从临时表中按行取出数据,返回结果并删除临时表,结果中包含两行数据分别是 1000 和 999 + + + + + +**** + + + +### 查询练习 + +数据准备: + +```mysql +-- 创建db4数据库 +CREATE DATABASE db4; +-- 使用db4数据库 +USE db4; + +-- 创建user表 +CREATE TABLE USER( + id INT PRIMARY KEY AUTO_INCREMENT, -- 用户id + NAME VARCHAR(20), -- 用户姓名 + age INT -- 用户年龄 +); + +-- 订单表 +CREATE TABLE orderlist( + id INT PRIMARY KEY AUTO_INCREMENT, -- 订单id + number VARCHAR(30), -- 订单编号 + uid INT, -- 外键字段 + CONSTRAINT ou_fk1 FOREIGN KEY (uid) REFERENCES USER(id) +); + +-- 商品分类表 +CREATE TABLE category( + id INT PRIMARY KEY AUTO_INCREMENT, -- 商品分类id + NAME VARCHAR(10) -- 商品分类名称 +); + +-- 商品表 +CREATE TABLE product( + id INT PRIMARY KEY AUTO_INCREMENT, -- 商品id + NAME VARCHAR(30), -- 商品名称 + cid INT, -- 外键字段 + CONSTRAINT cp_fk1 FOREIGN KEY (cid) REFERENCES category(id) +); + +-- 中间表 +CREATE TABLE us_pro( + upid INT PRIMARY KEY AUTO_INCREMENT, -- 中间表id + uid INT, -- 外键字段。需要和用户表的主键产生关联 + pid INT, -- 外键字段。需要和商品表的主键产生关联 + CONSTRAINT up_fk1 FOREIGN KEY (uid) REFERENCES USER(id), + CONSTRAINT up_fk2 FOREIGN KEY (pid) REFERENCES product(id) +); +``` + +![多表练习架构设计](https://seazean.oss-cn-beijing.aliyuncs.com/img/DB/多表练习架构设计.png) + + + +**数据查询:** + +1. 查询用户的编号、姓名、年龄、订单编号 + + 数据:用户的编号、姓名、年龄在 user 表,订单编号在 orderlist 表 + + 条件:user.id = orderlist.uid + + ```mysql + SELECT + u.*, + o.number + FROM + USER u, + orderlist o + WHERE + u.id = o.uid; + ``` + +2. 查询所有的用户,显示用户的编号、姓名、年龄、订单编号。 + + ```mysql + SELECT + u.*, + o.number + FROM + USER u + LEFT OUTER JOIN + orderlist o + ON + u.id = o.uid; + ``` + +3. 查询用户年龄大于 23 岁的信息,显示用户的编号、姓名、年龄、订单编号 + + ```mysql + SELECT + u.*, + o.number + FROM + USER u, + orderlist o + WHERE + u.id = o.uid + AND + u.age > 23; + ``` + + ```mysql + SELECT + u.*, + o.number + FROM + (SELECT * FROM USER WHERE age > 23) u,-- 嵌套查询 + orderlist o + WHERE + u.id = o.uid; + ``` + +4. 查询张三和李四用户的信息,显示用户的编号、姓名、年龄、订单编号。 + + ````mysql + SELECT + u.*, + o.number + FROM + USER u, + orderlist o + WHERE + u.id=o.uid + AND + u.name IN ('张三','李四'); + ```` + +5. 查询所有的用户和该用户能查看的所有的商品,显示用户的编号、姓名、年龄、商品名称 + + 数据:用户的编号、姓名、年龄在 user 表,商品名称在 product 表,中间表 us_pro + + 条件:us_pro.uid = user.id AND us_pro.pid = product.id + + ```mysql + SELECT + u.id, + u.name, + u.age, + p.name + FROM + USER u, + product p, + us_pro up + WHERE + up.uid = u.id + AND + up.pid=p.id; + ``` + +6. 查询张三和李四这两个用户可以看到的商品,显示用户的编号、姓名、年龄、商品名称。 + + ```mysql + SELECT + u.id, + u.name, + u.age, + p.name + FROM + USER u, + product p, + us_pro up + WHERE + up.uid=u.id + AND + up.pid=p.id + AND + u.name IN ('张三','李四'); + ``` + + + + + + + +*** + + + + + +## 高级结构 + +### 视图 + +#### 基本介绍 + +视图概念:视图是一种虚拟存在的数据表,这个虚拟的表并不在数据库中实际存在 + +本质:将一条 SELECT 查询语句的结果封装到了一个虚拟表中,所以在创建视图的时候,工作重心要放在这条 SELECT 查询语句上 + +作用:将一些比较复杂的查询语句的结果,封装到一个虚拟表中,再有相同查询需求时,直接查询该虚拟表 + +优点: + +* 简单:使用视图的用户不需要关心表的结构、关联条件和筛选条件,因为虚拟表中已经是过滤好的结果集 +* 安全:使用视图的用户只能访问查询的结果集,对表的权限管理并不能限制到某个行某个列 + +* 数据独立,一旦视图的结构确定,可以屏蔽表结构变化对用户的影响,源表增加列对视图没有影响;源表修改列名,则可以通过修改视图来解决,不会造成对访问者的影响 + + + +*** + + + +#### 视图创建 + +* 创建视图 + + ```mysql + CREATE [OR REPLACE] + VIEW 视图名称 [(列名列表)] + AS 查询语句 + [WITH [CASCADED | LOCAL] CHECK OPTION]; + ``` + + `WITH [CASCADED | LOCAL] CHECK OPTION` 决定了是否允许更新数据使记录不再满足视图的条件: + + * LOCAL:只要满足本视图的条件就可以更新 + * CASCADED:必须满足所有针对该视图的所有视图的条件才可以更新, 默认值 + +* 例如 + + ```mysql + -- 数据准备 city + id NAME cid + 1 深圳 1 + 2 上海 1 + 3 纽约 2 + 4 莫斯科 3 + + -- 数据准备 country + id NAME + 1 中国 + 2 美国 + 3 俄罗斯 + + -- 创建city_country视图,保存城市和国家的信息(使用指定列名) + CREATE + VIEW + city_country (city_id,city_name,country_name) + AS + SELECT + c1.id, + c1.name, + c2.name + FROM + city c1, + country c2 + WHERE + c1.cid=c2.id; + ``` + + + +*** + + + +#### 视图查询 + +* 查询所有数据表,视图也会查询出来 + + ```mysql + SHOW TABLES; + SHOW TABLE STATUS [\G]; + ``` + +* 查询视图 + + ```mysql + SELECT * FROM 视图名称; + ``` + +* 查询某个视图创建 + + ```mysql + SHOW CREATE VIEW 视图名称; + ``` + + + +*** + + + +#### 视图修改 + +视图表数据修改,会**自动修改源表中的数据**,因为更新的是视图中的基表中的数据 + +* 修改视图表中的数据 + + ```mysql + UPDATE 视图名称 SET 列名 = 值 WHERE 条件; + ``` + +* 修改视图的结构 + + ```mysql + ALTER [ALGORITHM = {UNDEFINED | MERGE | TEMPTABLE}] + VIEW 视图名称 [(列名列表)] + AS 查询语句 + [WITH [CASCADED | LOCAL] CHECK OPTION] + + -- 将视图中的country_name修改为name + ALTER + VIEW + city_country (city_id,city_name,name) + AS + SELECT + c1.id, + c1.name, + c2.name + FROM + city c1, + country c2 + WHERE + c1.cid=c2.id; + ``` + + + +*** + + + +#### 视图删除 + +* 删除视图 + + ```mysql + DROP VIEW 视图名称; + ``` + +* 如果存在则删除 + + ```mysql + DROP VIEW IF EXISTS 视图名称; + ``` + + + + + + +*** + + + +### 存储过程 + +#### 基本介绍 + +存储过程和函数:存储过程和函数是事先经过编译并存储在数据库中的一段 SQL 语句的集合 + +存储过程和函数的好处: + +* 提高代码的复用性 +* 减少数据在数据库和应用服务器之间的传输,提高传输效率 +* 减少代码层面的业务处理 +* **一次编译永久有效** + +存储过程和函数的区别: + +* 存储函数必须有返回值 +* 存储过程可以没有返回值 + + + +*** + + + +#### 基本操作 + +DELIMITER: + +* DELIMITER 关键字用来声明 sql 语句的分隔符,告诉 MySQL 该段命令已经结束 + +* MySQL 语句默认的分隔符是分号,但是有时需要一条功能 sql 语句中包含分号,但是并不作为结束标识,这时使用 DELIMITER 来指定分隔符: + + ```mysql + DELIMITER 分隔符 + ``` + +存储过程的创建调用查看和删除: + +* 创建存储过程 + + ```mysql + -- 修改分隔符为$ + DELIMITER $ + + -- 标准语法 + CREATE PROCEDURE 存储过程名称(参数...) + BEGIN + sql语句; + END$ + + -- 修改分隔符为分号 + DELIMITER ; + ``` + +* 调用存储过程 + + ```mysql + CALL 存储过程名称(实际参数); + ``` + +* 查看存储过程 + + ```mysql + SELECT * FROM mysql.proc WHERE db='数据库名称'; + ``` + +* 删除存储过程 + + ```mysql + DROP PROCEDURE [IF EXISTS] 存储过程名称; + ``` + +练习: + +* 数据准备 + + ```mysql + id NAME age gender score + 1 张三 23 男 95 + 2 李四 24 男 98 + 3 王五 25 女 100 + 4 赵六 26 女 90 + ``` + +* 创建 stu_group() 存储过程,封装分组查询总成绩,并按照总成绩升序排序的功能 + + ```mysql + DELIMITER $ + + CREATE PROCEDURE stu_group() + BEGIN + SELECT gender,SUM(score) getSum FROM student GROUP BY gender ORDER BY getSum ASC; + END$ + + DELIMITER ; + + -- 调用存储过程 + CALL stu_group(); + -- 删除存储过程 + DROP PROCEDURE IF EXISTS stu_group; + ``` + + + +*** + + + +#### 存储语法 + +##### 变量使用 + +存储过程是可以进行编程的,意味着可以使用变量、表达式、条件控制语句等,来完成比较复杂的功能 + +* 定义变量:DECLARE 定义的是局部变量,只能用在 BEGIN END 范围之内 + + ```mysql + DECLARE 变量名 数据类型 [DEFAULT 默认值]; + ``` + +* 变量的赋值 + + ```mysql + SET 变量名 = 变量值; + SELECT 列名 INTO 变量名 FROM 表名 [WHERE 条件]; + ``` + +* 数据准备:表 student + + ```mysql + id NAME age gender score + 1 张三 23 男 95 + 2 李四 24 男 98 + 3 王五 25 女 100 + 4 赵六 26 女 90 + ``` + +* 定义两个 int 变量,用于存储男女同学的总分数 + + ```mysql + DELIMITER $ + CREATE PROCEDURE pro_test3() + BEGIN + -- 定义两个变量 + DECLARE men,women INT; + -- 查询男同学的总分数,为men赋值 + SELECT SUM(score) INTO men FROM student WHERE gender='男'; + -- 查询女同学的总分数,为women赋值 + SELECT SUM(score) INTO women FROM student WHERE gender='女'; + -- 使用变量 + SELECT men,women; + END$ + DELIMITER ; + -- 调用存储过程 + CALL pro_test3(); + ``` + + + +*** + + + +##### IF语句 + +* if 语句标准语法 + + ```mysql + IF 判断条件1 THEN 执行的sql语句1; + [ELSEIF 判断条件2 THEN 执行的sql语句2;] + ... + [ELSE 执行的sql语句n;] + END IF; + ``` + +* 数据准备:表 student + + ```mysql + id NAME age gender score + 1 张三 23 男 95 + 2 李四 24 男 98 + 3 王五 25 女 100 + 4 赵六 26 女 90 + ``` + +* 根据总成绩判断:全班 380 分及以上学习优秀、320 ~ 380 学习良好、320 以下学习一般 + + ```mysql + DELIMITER $ + CREATE PROCEDURE pro_test4() + BEGIN + DECLARE total INT; -- 定义总分数变量 + DECLARE description VARCHAR(10); -- 定义分数描述变量 + SELECT SUM(score) INTO total FROM student; -- 为总分数变量赋值 + -- 判断总分数 + IF total >= 380 THEN + SET description = '学习优秀'; + ELSEIF total >=320 AND total < 380 THEN + SET description = '学习良好'; + ELSE + SET description = '学习一般'; + END IF; + END$ + DELIMITER ; + -- 调用pro_test4存储过程 + CALL pro_test4(); + ``` + + + + +*** + + + +##### 参数传递 + +* 参数传递的语法 + + IN:代表输入参数,需要由调用者传递实际数据,默认的 + OUT:代表输出参数,该参数可以作为返回值 + INOUT:代表既可以作为输入参数,也可以作为输出参数 + + ```mysql + DELIMITER $ + + -- 标准语法 + CREATE PROCEDURE 存储过程名称([IN|OUT|INOUT] 参数名 数据类型) + BEGIN + 执行的sql语句; + END$ + + DELIMITER ; + ``` + +* 输入总成绩变量,代表学生总成绩,输出分数描述变量,代表学生总成绩的描述 + + ```mysql + DELIMITER $ + + CREATE PROCEDURE pro_test6(IN total INT, OUT description VARCHAR(10)) + BEGIN + -- 判断总分数 + IF total >= 380 THEN + SET description = '学习优秀'; + ELSEIF total >= 320 AND total < 380 THEN + SET description = '学习不错'; + ELSE + SET description = '学习一般'; + END IF; + END$ + + DELIMITER ; + -- 调用pro_test6存储过程 + CALL pro_test6(310,@description); + CALL pro_test6((SELECT SUM(score) FROM student), @description); + -- 查询总成绩描述 + SELECT @description; + ``` + +* 查看参数方法 + + * @变量名 : **用户会话变量**,代表整个会话过程他都是有作用的,类似于全局变量 + * @@变量名 : **系统变量** + + + +*** + + + +##### CASE + +* 标准语法 1 + + ```mysql + CASE 表达式 + WHEN 值1 THEN 执行sql语句1; + [WHEN 值2 THEN 执行sql语句2;] + ... + [ELSE 执行sql语句n;] + END CASE; + ``` + +* 标准语法 2 + + ```mysql + sCASE + WHEN 判断条件1 THEN 执行sql语句1; + [WHEN 判断条件2 THEN 执行sql语句2;] + ... + [ELSE 执行sql语句n;] + END CASE; + ``` + +* 演示 + + ```mysql + DELIMITER $ + CREATE PROCEDURE pro_test7(IN total INT) + BEGIN + -- 定义变量 + DECLARE description VARCHAR(10); + -- 使用case判断 + CASE + WHEN total >= 380 THEN + SET description = '学习优秀'; + WHEN total >= 320 AND total < 380 THEN + SET description = '学习不错'; + ELSE + SET description = '学习一般'; + END CASE; + + -- 查询分数描述信息 + SELECT description; + END$ + DELIMITER ; + -- 调用pro_test7存储过程 + CALL pro_test7(390); + CALL pro_test7((SELECT SUM(score) FROM student)); + ``` + + + +*** + + + +##### WHILE + +* while 循环语法 + + ```mysql + WHILE 条件判断语句 DO + 循环体语句; + 条件控制语句; + END WHILE; + ``` + +* 计算 1~100 之间的偶数和 + + ```mysql + DELIMITER $ + CREATE PROCEDURE pro_test6() + BEGIN + -- 定义求和变量 + DECLARE result INT DEFAULT 0; + -- 定义初始化变量 + DECLARE num INT DEFAULT 1; + -- while循环 + WHILE num <= 100 DO + IF num % 2 = 0 THEN + SET result = result + num; + END IF; + SET num = num + 1; + END WHILE; + -- 查询求和结果 + SELECT result; + END$ + DELIMITER ; + + -- 调用pro_test6存储过程 + CALL pro_test6(); + ``` + + + +*** + + + +##### REPEAT + +* repeat 循环标准语法 + + ```mysql + 初始化语句; + REPEAT + 循环体语句; + 条件控制语句; + UNTIL 条件判断语句 + END REPEAT; + ``` + +* 计算 1~10 之间的和 + + ```mysql + DELIMITER $ + CREATE PROCEDURE pro_test9() + BEGIN + -- 定义求和变量 + DECLARE result INT DEFAULT 0; + -- 定义初始化变量 + DECLARE num INT DEFAULT 1; + -- repeat循环 + REPEAT + -- 累加 + SET result = result + num; + -- 让num+1 + SET num = num + 1; + -- 停止循环 + UNTIL num > 10 + END REPEAT; + -- 查询求和结果 + SELECT result; + END$ + + DELIMITER ; + -- 调用pro_test9存储过程 + CALL pro_test9(); + ``` + + + + +*** + + + +##### LOOP + +LOOP 实现简单的循环,退出循环的条件需要使用其他的语句定义,通常可以使用 LEAVE 语句实现,如果不加退出循环的语句,那么就变成了死循环 + +* loop 循环标准语法 + + ```mysql + [循环名称:] LOOP + 条件判断语句 + [LEAVE 循环名称;] + 循环体语句; + 条件控制语句; + END LOOP 循环名称; + ``` + +* 计算 1~10 之间的和 + + ```mysql + DELIMITER $ + CREATE PROCEDURE pro_test10() + BEGIN + -- 定义求和变量 + DECLARE result INT DEFAULT 0; + -- 定义初始化变量 + DECLARE num INT DEFAULT 1; + -- loop循环 + l:LOOP + -- 条件成立,停止循环 + IF num > 10 THEN + LEAVE l; + END IF; + -- 累加 + SET result = result + num; + -- 让num+1 + SET num = num + 1; + END LOOP l; + -- 查询求和结果 + SELECT result; + END$ + DELIMITER ; + -- 调用pro_test10存储过程 + CALL pro_test10(); + ``` + + + +*** + + + +##### 游标 + +游标是用来存储查询结果集的数据类型,在存储过程和函数中可以使用光标对结果集进行循环的处理 +* 游标可以遍历返回的多行结果,每次拿到一整行数据 +* 简单来说游标就类似于集合的迭代器遍历 +* MySQL 中的游标只能用在存储过程和函数中 + +游标的语法 + +* 创建游标 + + ```mysql + DECLARE 游标名称 CURSOR FOR 查询sql语句; + ``` + +* 打开游标 + + ```mysql + OPEN 游标名称; + ``` + +* 使用游标获取数据 + + ```mysql + FETCH 游标名称 INTO 变量名1,变量名2,...; + ``` + +* 关闭游标 + + ```mysql + CLOSE 游标名称; + ``` + +* Mysql 通过一个 Error handler 声明来判断指针是否到尾部,并且必须和创建游标的 SQL 语句声明在一起: + + ```mysql + DECLARE EXIT HANDLER FOR NOT FOUND (do some action,一般是设置标志变量) + ``` + + + +游标的基本使用 + +* 数据准备:表 student + + ```mysql + id NAME age gender score + 1 张三 23 男 95 + 2 李四 24 男 98 + 3 王五 25 女 100 + 4 赵六 26 女 90 + ``` + +* 创建 stu_score 表 + + ```mysql + CREATE TABLE stu_score( + id INT PRIMARY KEY AUTO_INCREMENT, + score INT + ); + ``` + +* 将student表中所有的成绩保存到stu_score表中 + + ```mysql + DELIMITER $ + + CREATE PROCEDURE pro_test12() + BEGIN + -- 定义成绩变量 + DECLARE s_score INT; + -- 定义标记变量 + DECLARE flag INT DEFAULT 0; + + -- 创建游标,查询所有学生成绩数据 + DECLARE stu_result CURSOR FOR SELECT score FROM student; + -- 游标结束后,将标记变量改为1 这两个必须声明在一起 + DECLARE EXIT HANDLER FOR NOT FOUND SET flag = 1; + + -- 开启游标 + OPEN stu_result; + -- 循环使用游标 + REPEAT + -- 使用游标,遍历结果,拿到数据 + FETCH stu_result INTO s_score; + -- 将数据保存到stu_score表中 + INSERT INTO stu_score VALUES (NULL,s_score); + UNTIL flag=1 + END REPEAT; + -- 关闭游标 + CLOSE stu_result; + END$ + + DELIMITER ; + + -- 调用pro_test12存储过程 + CALL pro_test12(); + -- 查询stu_score表 + SELECT * FROM stu_score; + ``` + + + + + +*** + + + +#### 存储函数 + +存储函数和存储过程是非常相似的,存储函数可以做的事情,存储过程也可以做到 + +存储函数有返回值,存储过程没有返回值(参数的 out 其实也相当于是返回数据了) + +* 创建存储函数 + + ```mysql + DELIMITER $ + -- 标准语法 + CREATE FUNCTION 函数名称(参数 数据类型) + RETURNS 返回值类型 + BEGIN + 执行的sql语句; + RETURN 结果; + END$ + + DELIMITER ; + ``` + +* 调用存储函数,因为有返回值,所以使用 SELECT 调用 + + ```mysql + SELECT 函数名称(实际参数); + ``` + +* 删除存储函数 + + ```mysql + DROP FUNCTION 函数名称; + ``` + +* 定义存储函数,获取学生表中成绩大于95分的学生数量 + + ```mysql + DELIMITER $ + CREATE FUNCTION fun_test() + RETURN INT + BEGIN + -- 定义统计变量 + DECLARE result INT; + -- 查询成绩大于95分的学生数量,给统计变量赋值 + SELECT COUNT(score) INTO result FROM student WHERE score > 95; + -- 返回统计结果 + SELECT result; + END + DELIMITER ; + -- 调用fun_test存储函数 + SELECT fun_test(); + ``` + + + + + +*** + + + +### 触发器 + +#### 基本介绍 + +触发器是与表有关的数据库对象,在 insert/update/delete 之前或之后触发并执行触发器中定义的 SQL 语句 + +* 触发器的这种特性可以协助应用在数据库端确保数据的完整性 、日志记录 、数据校验等操作 + +- 使用别名 NEW 和 OLD 来引用触发器中发生变化的记录内容,这与其他的数据库是相似的 +- 现在触发器还只支持行级触发,不支持语句级触发 + +| 触发器类型 | OLD的含义 | NEW的含义 | +| --------------- | ------------------------------ | ------------------------------ | +| INSERT 型触发器 | 无 (因为插入前状态无数据) | NEW 表示将要或者已经新增的数据 | +| UPDATE 型触发器 | OLD 表示修改之前的数据 | NEW 表示将要或已经修改后的数据 | +| DELETE 型触发器 | OLD 表示将要或者已经删除的数据 | 无 (因为删除后状态无数据) | + + + +*** + + + +#### 基本操作 + +* 创建触发器 + + ```mysql + DELIMITER $ + + CREATE TRIGGER 触发器名称 + BEFORE|AFTER INSERT|UPDATE|DELETE + ON 表名 + [FOR EACH ROW] -- 行级触发器 + BEGIN + 触发器要执行的功能; + END$ + + DELIMITER ; + ``` + +* 查看触发器的状态、语法等信息 + + ```mysql + SHOW TRIGGERS; + ``` + +* 删除触发器,如果没有指定 schema_name,默认为当前数据库 + + ```mysql + DROP TRIGGER [schema_name.]trigger_name; + ``` + + + +*** + + + +#### 触发演示 + +通过触发器记录账户表的数据变更日志。包含:增加、修改、删除 + +* 数据准备 + + ```mysql + -- 创建db9数据库 + CREATE DATABASE db9; + -- 使用db9数据库 + USE db9; + ``` + + ```mysql + -- 创建账户表account + CREATE TABLE account( + id INT PRIMARY KEY AUTO_INCREMENT, -- 账户id + NAME VARCHAR(20), -- 姓名 + money DOUBLE -- 余额 + ); + -- 添加数据 + INSERT INTO account VALUES (NULL,'张三',1000),(NULL,'李四',2000); + ``` + + ```mysql + -- 创建日志表account_log + CREATE TABLE account_log( + id INT PRIMARY KEY AUTO_INCREMENT, -- 日志id + operation VARCHAR(20), -- 操作类型 (insert update delete) + operation_time DATETIME, -- 操作时间 + operation_id INT, -- 操作表的id + operation_params VARCHAR(200) -- 操作参数 + ); + ``` + +* 创建 INSERT 型触发器 + + ```mysql + DELIMITER $ + + CREATE TRIGGER account_insert + AFTER INSERT + ON account + FOR EACH ROW + BEGIN + INSERT INTO account_log VALUES (NULL,'INSERT',NOW(),new.id,CONCAT('插入后{id=',new.id,',name=',new.name,',money=',new.money,'}')); + END$ + + DELIMITER ; + ``` + + ```mysql + -- 向account表添加记录 + INSERT INTO account VALUES (NULL,'王五',3000); + + -- 查询日志表 + SELECT * FROM account_log; + /* + id operation operation_time operation_id operation_params + 1 INSERT 2021-01-26 19:51:11 3 插入后{id=3,name=王五money=2000} + */ + ``` + + + +* 创建 UPDATE 型触发器 + + ```mysql + DELIMITER $ + + CREATE TRIGGER account_update + AFTER UPDATE + ON account + FOR EACH ROW + BEGIN + INSERT INTO account_log VALUES (NULL,'UPDATE',NOW(),new.id,CONCAT('修改前{id=',old.id,',name=',old.name,',money=',old.money,'}','修改后{id=',new.id,',name=',new.name,',money=',new.money,'}')); + END$ + + DELIMITER ; + ``` + + ```mysql + -- 修改account表 + UPDATE account SET money=3500 WHERE id=3; + + -- 查询日志表 + SELECT * FROM account_log; + /* + id operation operation_time operation_id operation_params + 2 UPDATE 2021-01-26 19:58:54 2 更新前{id=2,name=李四money=1000} + 更新后{id=2,name=李四money=200} + */ + ``` + + + +* 创建 DELETE 型触发器 + + ```mysql + DELIMITER $ + + CREATE TRIGGER account_delete + AFTER DELETE + ON account + FOR EACH ROW + BEGIN + INSERT INTO account_log VALUES (NULL,'DELETE',NOW(),old.id,CONCAT('删除前{id=',old.id,',name=',old.name,',money=',old.money,'}')); + END$ + + DELIMITER ; + ``` + + ```mysql + -- 删除account表数据 + DELETE FROM account WHERE id=3; + + -- 查询日志表 + SELECT * FROM account_log; + /* + id operation operation_time operation_id operation_params + 3 DELETE 2021-01-26 20:02:48 3 删除前{id=3,name=王五money=2000} + */ + ``` + + + + + + + +*** + + + + + +## 存储引擎 + +### 基本介绍 + +对比其他数据库,MySQL 的架构可以在不同场景应用并发挥良好作用,主要体现在存储引擎,插件式的存储引擎架构将查询处理和其他的系统任务以及数据的存储提取分离,可以针对不同的存储需求可以选择最优的存储引擎 + +存储引擎的介绍: + +- MySQL 数据库使用不同的机制存取表文件 , 机制的差别在于不同的存储方式、索引技巧、锁定水平等不同的功能和能力,在 MySQL 中,将这些不同的技术及配套的功能称为存储引擎 +- Oracle、SqlServer 等数据库只有一种存储引擎,MySQL **提供了插件式的存储引擎架构**,所以 MySQL 存在多种存储引擎 , 就会让数据库采取了不同的处理数据的方式和扩展功能 +- 在关系型数据库中数据的存储是以表的形式存进行,所以存储引擎也称为表类型(存储和操作此表的类型) +- 通过选择不同的引擎,能够获取最佳的方案, 也能够获得额外的速度或者功能,提高程序的整体效果。 + +MySQL 支持的存储引擎: + +- MySQL 支持的引擎包括:InnoDB、MyISAM、MEMORY、Archive、Federate、CSV、BLACKHOLE 等 +- MySQL5.5 之前的默认存储引擎是 MyISAM,5.5 之后就改为了 InnoDB + + + +**** + + + +### 引擎对比 + +MyISAM 存储引擎: + +* 特点:不支持事务和外键,读取速度快,节约资源 +* 应用场景:**适用于读多写少的场景**,对事务的完整性要求不高,比如一些数仓、离线数据、支付宝的年度总结之类的场景,业务进行只读操作,查询起来会更快 +* 存储方式: + * 每个 MyISAM 在磁盘上存储成 3 个文件,其文件名都和表名相同,拓展名不同 + * 表的定义保存在 .frm 文件,表数据保存在 .MYD (MYData) 文件中,索引保存在 .MYI (MYIndex) 文件中 + +InnoDB 存储引擎:(MySQL5.5 版本后默认的存储引擎) + +- 特点:**支持事务**和外键操作,支持并发控制。对比 MyISAM 的存储引擎,InnoDB 写的处理效率差一些,并且会占用更多的磁盘空间以保留数据和索引 +- 应用场景:对事务的完整性有比较高的要求,在并发条件下要求数据的一致性,读写频繁的操作 +- 存储方式: + - 使用共享表空间存储, 这种方式创建的表的表结构保存在 .frm 文件中, 数据和索引保存在 innodb_data_home_dir 和 innodb_data_file_path 定义的表空间中,可以是多个文件 + - 使用多表空间存储,创建的表的表结构存在 .frm 文件中,每个表的数据和索引单独保存在 .ibd 中 + +MEMORY 存储引擎: + +- 特点:每个 MEMORY 表实际对应一个磁盘文件 ,该文件中只存储表的结构,表数据保存在内存中,且默认**使用 HASH 索引**,所以数据默认就是无序的,但是在需要快速定位记录可以提供更快的访问,**服务一旦关闭,表中的数据就会丢失**,存储不安全 +- 应用场景:**缓存型存储引擎**,通常用于更新不太频繁的小表,用以快速得到访问结果 +- 存储方式:表结构保存在 .frm 中 + +MERGE 存储引擎: + +* 特点: + + * 是一组 MyISAM 表的组合,这些 MyISAM 表必须结构完全相同,通过将不同的表分布在多个磁盘上 + * MERGE 表本身并没有存储数据,对 MERGE 类型的表可以进行查询、更新、删除操作,这些操作实际上是对内部的 MyISAM 表进行的 + +* 应用场景:将一系列等同的 MyISAM 表以逻辑方式组合在一起,并作为一个对象引用他们,适合做数据仓库 + +* 操作方式: + + * 插入操作是通过 INSERT_METHOD 子句定义插入的表,使用 FIRST 或 LAST 值使得插入操作被相应地作用在第一或者最后一个表上;不定义这个子句或者定义为 NO,表示不能对 MERGE 表执行插入操作 + * 对 MERGE 表进行 DROP 操作,但是这个操作只是删除 MERGE 表的定义,对内部的表是没有任何影响的 + + ```mysql + CREATE TABLE order_1( + )ENGINE = MyISAM DEFAULT CHARSET=utf8; + + CREATE TABLE order_2( + )ENGINE = MyISAM DEFAULT CHARSET=utf8; + + CREATE TABLE order_all( + -- 结构与MyISAM表相同 + )ENGINE = MERGE UNION = (order_1,order_2) INSERT_METHOD=LAST DEFAULT CHARSET=utf8; + ``` + + ![](https://seazean.oss-cn-beijing.aliyuncs.com/img/DB/MySQL-MERGE.png) + +| 特性 | MyISAM | InnoDB | MEMORY | +| ------------ | ------------------------------ | ------------- | -------------------- | +| 存储限制 | 有(平台对文件系统大小的限制) | 64TB | 有(平台的内存限制) | +| **事务安全** | **不支持** | **支持** | **不支持** | +| **锁机制** | **表锁** | **表锁/行锁** | **表锁** | +| B+Tree 索引 | 支持 | 支持 | 支持 | +| 哈希索引 | 不支持 | 不支持 | 支持 | +| 全文索引 | 支持 | 支持 | 不支持 | +| 集群索引 | 不支持 | 支持 | 不支持 | +| 数据索引 | 不支持 | 支持 | 支持 | +| 数据缓存 | 不支持 | 支持 | N/A | +| 索引缓存 | 支持 | 支持 | N/A | +| 数据可压缩 | 支持 | 不支持 | 不支持 | +| 空间使用 | 低 | 高 | N/A | +| 内存使用 | 低 | 高 | 中等 | +| 批量插入速度 | 高 | 低 | 高 | +| **外键** | **不支持** | **支持** | **不支持** | + +只读场景 MyISAM 比 InnoDB 更快: + +* 底层存储结构有差别,MyISAM 是非聚簇索引,叶子节点保存的是数据的具体地址,不用回表查询 +* InnoDB 每次查询需要维护 MVCC 版本状态,保证并发状态下的读写冲突问题 + + + +*** + + + +### 引擎操作 + +* 查询数据库支持的存储引擎 + + ```mysql + SHOW ENGINES; + SHOW VARIABLES LIKE '%storage_engine%'; -- 查看Mysql数据库默认的存储引擎 + ``` + +* 查询某个数据库中所有数据表的存储引擎 + + ```mysql + SHOW TABLE STATUS FROM 数据库名称; + ``` + +* 查询某个数据库中某个数据表的存储引擎 + + ```mysql + SHOW TABLE STATUS FROM 数据库名称 WHERE NAME = '数据表名称'; + ``` + +* 创建数据表,指定存储引擎 + + ```mysql + CREATE TABLE 表名( + 列名,数据类型, + ... + )ENGINE = 引擎名称; + ``` + +* 修改数据表的存储引擎 + + ```mysql + ALTER TABLE 表名 ENGINE = 引擎名称; + ``` + + + + + + + + +*** + + + + + +## 索引机制 + +### 索引介绍 + +#### 基本介绍 + +MySQL 官方对索引的定义为:索引(index)是帮助 MySQL 高效获取数据的一种数据结构,**本质是排好序的快速查找数据结构。**在表数据之外,数据库系统还维护着满足特定查找算法的数据结构,这些数据结构以某种方式指向数据, 这样就可以在这些数据结构上实现高级查找算法,这种数据结构就是索引 + +**索引是在存储引擎层实现的**,所以并没有统一的索引标准,即不同存储引擎的索引的工作方式并不一样 + +索引使用:一张数据表,用于保存数据;一个索引配置文件,用于保存索引;每个索引都指向了某一个数据 +![](https://seazean.oss-cn-beijing.aliyuncs.com/img/DB/MySQL-索引的介绍.png) + +左边是数据表,一共有两列七条记录,最左边的是数据记录的物理地址(注意逻辑上相邻的记录在磁盘上也并不是一定物理相邻的)。为了加快 Col2 的查找,可以维护一个右边所示的二叉查找树,每个节点分别包含索引键值和一个指向对应数据的物理地址的指针,这样就可以运用二叉查找快速获取到相应数据 + +索引的优点: + +* 类似于书籍的目录索引,提高数据检索的效率,降低数据库的 IO 成本 +* 通过索引列对数据进行排序,降低数据排序的成本,降低 CPU 的消耗 + +索引的缺点: + +* 一般来说索引本身也很大,不可能全部存储在内存中,因此索引往往以索引文件的形式**存储在磁盘**上 +* 虽然索引大大提高了查询效率,同时却也降低更新表的速度。对表进行 INSERT、UPDATE、DELETE 操作,MySQL 不仅要保存数据,还要保存一下索引文件每次更新添加了索引列的字段,还会调整因为更新所带来的键值变化后的索引信息,**但是更新数据也需要先从数据库中获取**,索引加快了获取速度,所以可以相互抵消一下。 +* 索引会影响到 WHERE 的查询条件和排序 ORDER BY 两大功能 + + + +*** + + + +#### 索引分类 + +索引一般的分类如下: + +- 功能分类 + - 主键索引:一种特殊的唯一索引,不允许有空值,一般在建表时同时创建主键索引 + - 单列索引:一个索引只包含单个列,一个表可以有多个单列索引(普通索引) + - 联合索引:顾名思义,就是将单列索引进行组合 + - 唯一索引:索引列的值必须唯一,**允许有空值**,如果是联合索引,则列值组合必须唯一 + * NULL 值可以出现多次,因为两个 NULL 比较的结果既不相等,也不不等,结果仍然是未知 + * 可以声明不允许存储 NULL 值的非空唯一索引 + - 外键索引:只有 InnoDB 引擎支持外键索引,用来保证数据的一致性、完整性和实现级联操作 + +- 结构分类 + - BTree 索引:MySQL 使用最频繁的一个索引数据结构,是 InnoDB 和 MyISAM 存储引擎默认的索引类型,底层基于 B+Tree + - Hash 索引:MySQL中 Memory 存储引擎默认支持的索引类型 + - R-tree 索引(空间索引):空间索引是 MyISAM 引擎的一个特殊索引类型,主要用于地理空间数据类型 + - Full-text 索引(全文索引):快速匹配全部文档的方式。MyISAM 支持, InnoDB 不支持 FULLTEXT 类型的索引,但是 InnoDB 可以使用 sphinx 插件支持全文索引,MEMORY 引擎不支持 + + | 索引 | InnoDB | MyISAM | Memory | + | --------- | ---------------- | ------ | ------ | + | BTREE | 支持 | 支持 | 支持 | + | HASH | 不支持 | 不支持 | 支持 | + | R-tree | 不支持 | 支持 | 不支持 | + | Full-text | 5.6 版本之后支持 | 支持 | 不支持 | + +联合索引图示:根据身高年龄建立的组合索引(height、age) + +![](https://seazean.oss-cn-beijing.aliyuncs.com/img/DB/MySQL-组合索引图.png) + + + + + +*** + + + +### 索引操作 + +索引在创建表的时候可以同时创建, 也可以随时增加新的索引 + +* 创建索引:如果一个表中有一列是主键,那么会**默认为其创建主键索引**(主键列不需要单独创建索引) + + ```mysql + CREATE [UNIQUE|FULLTEXT] INDEX 索引名称 [USING 索引类型] ON 表名(列名...); + -- 索引类型默认是 B+TREE + ``` + +* 查看索引 + + ```mysql + SHOW INDEX FROM 表名; + ``` + +* 添加索引 + + ```mysql + -- 单列索引 + ALTER TABLE 表名 ADD INDEX 索引名称(列名); + + -- 组合索引 + ALTER TABLE 表名 ADD INDEX 索引名称(列名1,列名2,...); + + -- 主键索引 + ALTER TABLE 表名 ADD PRIMARY KEY(主键列名); + + -- 外键索引(添加外键约束,就是外键索引) + ALTER TABLE 表名 ADD CONSTRAINT 外键名 FOREIGN KEY (本表外键列名) REFERENCES 主表名(主键列名); + + -- 唯一索引 + ALTER TABLE 表名 ADD UNIQUE 索引名称(列名); + + -- 全文索引(mysql只支持文本类型) + ALTER TABLE 表名 ADD FULLTEXT 索引名称(列名); + ``` + +* 删除索引 + + ```mysql + DROP INDEX 索引名称 ON 表名; + ``` + +* 案例练习 + + 数据准备:student + + ```mysql + id NAME age score + 1 张三 23 99 + 2 李四 24 95 + 3 王五 25 98 + 4 赵六 26 97 + ``` + + 索引操作: + + ```mysql + -- 为student表中姓名列创建一个普通索引 + CREATE INDEX idx_name ON student(NAME); + + -- 为student表中年龄列创建一个唯一索引 + CREATE UNIQUE INDEX idx_age ON student(age); + ``` + + + + + + +*** + + + +### 聚簇索引 + +#### 索引对比 + +聚簇索引是一种数据存储方式,并不是一种单独的索引类型 + +* 聚簇索引的叶子节点存放的是主键值和数据行,支持覆盖索引 + +* 非聚簇索引的叶子节点存放的是主键值或指向数据行的指针(由存储引擎决定) + +在 Innodb 下主键索引是聚簇索引,在 MyISAM 下主键索引是非聚簇索引 + + + +*** + + + +#### Innodb + +##### 聚簇索引 + +在 Innodb 存储引擎,B+ 树索引可以分为聚簇索引(也称聚集索引、clustered index)和辅助索引(也称非聚簇索引或二级索引、secondary index、non-clustered index) + +InnoDB 中,聚簇索引是按照每张表的主键构造一颗 B+ 树,叶子节点中存放的就是整张表的数据,将聚簇索引的叶子节点称为数据页 + +* 这个特性决定了**数据也是索引的一部分**,所以一张表只能有一个聚簇索引 +* 辅助索引的存在不影响聚簇索引中数据的组织,所以一张表可以有多个辅助索引 + +聚簇索引的优点: + +* 数据访问更快,聚簇索引将索引和数据保存在同一个 B+ 树中,因此从聚簇索引中获取数据比非聚簇索引更快 +* 聚簇索引对于主键的排序查找和范围查找速度非常快 + +聚簇索引的缺点: + +* 插入速度严重依赖于插入顺序,按照主键的顺序(递增)插入是最快的方式,否则将会出现页分裂,严重影响性能,所以对于 InnoDB 表,一般都会定义一个自增的 ID 列为主键 + +* 更新主键的代价很高,将会导致被更新的行移动,所以对于 InnoDB 表,一般定义主键为不可更新 + +* 二级索引访问需要两次索引查找,第一次找到主键值,第二次根据主键值找到行数据 + + + +*** + + + +##### 辅助索引 + +在聚簇索引之上创建的索引称之为辅助索引,非聚簇索引都是辅助索引,像复合索引、前缀索引、唯一索引等 + +辅助索引叶子节点存储的是主键值,而不是数据的物理地址,所以访问数据需要二次查找,推荐使用覆盖索引,可以减少回表查询 + +**检索过程**:辅助索引找到主键值,再通过聚簇索引(二分)找到数据页,最后通过数据页中的 Page Directory(二分)找到对应的数据分组,遍历组内所所有的数据找到数据行 + +补充:无索引走全表查询,查到数据页后和上述步骤一致 + + + +*** + + + +##### 索引实现 + +InnoDB 使用 B+Tree 作为索引结构,并且 InnoDB 一定有索引 + +主键索引: + +* 在 InnoDB 中,表数据文件本身就是按 B+Tree 组织的一个索引结构,这个索引的 key 是数据表的主键,叶子节点 data 域保存了完整的数据记录 + +* InnoDB 的表数据文件**通过主键聚集数据**,如果没有定义主键,会选择非空唯一索引代替,如果也没有这样的列,MySQL 会自动为 InnoDB 表生成一个**隐含字段 row_id** 作为主键,这个字段长度为 6 个字节,类型为长整形 + +辅助索引: + +* InnoDB 的所有辅助索引(二级索引)都引用主键作为 data 域 + +* InnoDB 表是基于聚簇索引建立的,因此 InnoDB 的索引能提供一种非常快速的主键查找性能。不过辅助索引也会包含主键列,所以不建议使用过长的字段作为主键,**过长的主索引会令辅助索引变得过大** + +![](https://seazean.oss-cn-beijing.aliyuncs.com/img/DB/MySQL-InnoDB聚簇和辅助索引结构.png) + + + +*** + + + +#### MyISAM + +##### 非聚簇 + +MyISAM 的主键索引使用的是非聚簇索引,索引文件和数据文件是分离的,**索引文件仅保存数据的地址** + +* 主键索引 B+ 树的节点存储了主键,辅助键索引 B+ 树存储了辅助键,表数据存储在独立的地方,这两颗 B+ 树的叶子节点都使用一个地址指向真正的表数据,对于表数据来说,这两个键没有任何差别 +* 由于索引树是独立的,通过辅助索引检索**无需回表查询**访问主键的索引树 + +![](https://seazean.oss-cn-beijing.aliyuncs.com/img/DB/MySQL-聚簇索引和辅助索引检锁数据图.jpg) + + + +*** + + + +##### 索引实现 + +MyISAM 的索引方式也叫做非聚集的,之所以这么称呼是为了与 InnoDB 的聚集索引区分 + +主键索引:MyISAM 引擎使用 B+Tree 作为索引结构,叶节点的 data 域存放的是数据记录的地址 + +辅助索引:MyISAM 中主索引和辅助索引(Secondary key)在结构上没有任何区别,只是主索引要求 key 是唯一的,而辅助索引的 key 可以重复 + +![](https://seazean.oss-cn-beijing.aliyuncs.com/img/DB/MySQL-MyISAM主键和辅助索引结构.png) + + + + + +参考文章:https://blog.csdn.net/lm1060891265/article/details/81482136 + + + +*** + + + +### 索引结构 + +#### 数据页 + +文件系统的最小单元是块(block),一个块的大小是 4K,系统从磁盘读取数据到内存时是以磁盘块为基本单位的,位于同一个磁盘块中的数据会被一次性读取出来,而不是需要什么取什么 + +InnoDB 存储引擎中有页(Page)的概念,页是 MySQL 磁盘管理的最小单位 + +* **InnoDB 存储引擎中默认每个页的大小为 16KB,索引中一个节点就是一个数据页**,所以会一次性读取 16KB 的数据到内存 +* InnoDB 引擎将若干个地址连接磁盘块,以此来达到页的大小 16KB +* 在查询数据时如果一个页中的每条数据都能有助于定位数据记录的位置,这将会减少磁盘 I/O 次数,提高查询效率 + +超过 16KB 的一条记录,主键索引页只会存储部分数据和指向**溢出页**的指针,剩余数据都会分散存储在溢出页中 + +数据页物理结构,从上到下: + +* File Header:上一页和下一页的指针、该页的类型(索引页、数据页、日志页等)、**校验和**、LSN(最近一次修改当前页面时的系统 lsn 值,事务持久性部分详解)等信息 +* Page Header:记录状态信息 +* Infimum + Supremum:当前页的最小记录和最大记录(头尾指针),Infimum 所在分组只有一条记录,Supremum 所在分组可以有 1 ~ 8 条记录,剩余的分组可以有 4 ~ 8 条记录 +* User Records:存储数据的记录 +* Free Space:尚未使用的存储空间 +* Page Directory:分组的目录,可以通过目录快速定位(二分法)数据的分组 +* File Trailer:检验和字段,在刷脏过程中,页首和页尾的校验和一致才能说明页面刷新成功,二者不同说明刷新期间发生了错误;LSN 字段,也是用来校验页面的完整性 + +数据页中包含数据行,数据的存储是基于数据行的,数据行有 next_record 属性指向下一个行数据,所以是可以遍历的,但是一组数据至多 8 个行,通过 Page Directory 先定位到组,然后遍历获取所需的数据行即可 + +数据行中有三个隐藏字段:trx_id、roll_pointer、row_id(在事务章节会详细介绍它们的作用) + + + +*** + + + +#### BTree + +BTree 的索引类型是基于 B+Tree 树型数据结构的,B+Tree 又是 BTree 数据结构的变种,用在数据库和操作系统中的文件系统,特点是能够保持数据稳定有序 + +BTree 又叫多路平衡搜索树,一颗 m 叉的 BTree 特性如下: + +- 树中每个节点最多包含 m 个孩子 +- 除根节点与叶子节点外,每个节点至少有 [ceil(m/2)] 个孩子 +- 若根节点不是叶子节点,则至少有两个孩子 +- 所有的叶子节点都在同一层 +- 每个非叶子节点由 n 个 key 与 n+1 个指针组成,其中 [ceil(m/2)-1] <= n <= m-1 + +5 叉,key 的数量 [ceil(m/2)-1] <= n <= m-1 为 2 <= n <=4 ,当 n>4 时中间节点分裂到父节点,两边节点分裂 + +插入 C N G A H E K Q M F W L T Z D P R X Y S 数据的工作流程: + +* 插入前 4 个字母 C N G A + + ![](https://seazean.oss-cn-beijing.aliyuncs.com/img/DB/MySQL-BTree工作流程1.png) + +* 插入 H,n>4,中间元素 G 字母向上分裂到新的节点 + + ![](https://seazean.oss-cn-beijing.aliyuncs.com/img/DB/MySQL-BTree工作流程2.png) + +* 插入 E、K、Q 不需要分裂 + + ![](https://seazean.oss-cn-beijing.aliyuncs.com/img/DB/MySQL-BTree工作流程3.png) + +* 插入 M,中间元素 M 字母向上分裂到父节点 G + + ![](https://seazean.oss-cn-beijing.aliyuncs.com/img/DB/MySQL-BTree工作流程4.png) + +* 插入 F,W,L,T 不需要分裂 + + ![](https://seazean.oss-cn-beijing.aliyuncs.com/img/DB/MySQL-BTree工作流程5.png) + +* 插入 Z,中间元素 T 向上分裂到父节点中 + + ![](https://seazean.oss-cn-beijing.aliyuncs.com/img/DB/MySQL-BTree工作流程6.png) + +* 插入 D,中间元素 D 向上分裂到父节点中,然后插入 P,R,X,Y 不需要分裂 + + ![](https://seazean.oss-cn-beijing.aliyuncs.com/img/DB/MySQL-BTree工作流程7.png) + +* 最后插入 S,NPQR 节点 n>5,中间节点 Q 向上分裂,但分裂后父节点 DGMT 的 n>5,中间节点 M 向上分裂 + + ![](https://seazean.oss-cn-beijing.aliyuncs.com/img/DB/MySQL-BTree工作流程8.png) + +BTree 树就已经构建完成了,BTree 树和二叉树相比, 查询数据的效率更高, 因为对于相同的数据量来说,**BTree 的层级结构比二叉树少**,所以搜索速度快 + +BTree 结构的数据可以让系统高效的找到数据所在的磁盘块,定义一条记录为一个二元组 [key, data] ,key 为记录的键值,对应表中的主键值,data 为一行记录中除主键外的数据。对于不同的记录,key 值互不相同,BTree 中的每个节点根据实际情况可以包含大量的关键字信息和分支 +![](https://seazean.oss-cn-beijing.aliyuncs.com/img/DB/索引的原理1.png) + +缺点:当进行范围查找时会出现回旋查找 + + + +*** + + + +#### B+Tree + +##### 数据结构 + +BTree 数据结构中每个节点中不仅包含数据的 key 值,还有 data 值。磁盘中每一页的存储空间是有限的,如果 data 数据较大时将会导致每个节点(即一个页)能存储的 key 的数量很小,当存储的数据量很大时同样会导致 B-Tree 的深度较大,增大查询时的磁盘 I/O 次数,进而影响查询效率,所以引入 B+Tree + +B+Tree 为 BTree 的变种,B+Tree 与 BTree 的区别为: + +* n 叉 B+Tree 最多含有 n 个 key(哈希值),而 BTree 最多含有 n-1 个 key + +- 所有**非叶子节点只存储键值 key** 信息,只进行数据索引,使每个非叶子节点所能保存的关键字大大增加 +- 所有**数据都存储在叶子节点**,所以每次数据查询的次数都一样 +- **叶子节点按照 key 大小顺序排列,左边结尾数据都会保存右边节点开始数据的指针,形成一个链表** +- 所有节点中的 key 在叶子节点中也存在(比如 5),**key 允许重复**,B 树不同节点不存在重复的 key + + + +B* 树:是 B+ 树的变体,在 B+ 树的非根和非叶子结点再增加指向兄弟的指针 + + + +*** + + + +##### 优化结构 + +MySQL 索引数据结构对经典的 B+Tree 进行了优化,在原 B+Tree 的基础上,增加一个指向相邻叶子节点的链表指针,就形成了带有顺序指针的 B+Tree,**提高区间访问的性能,防止回旋查找** + +区间访问的意思是访问索引为 5 - 15 的数据,可以直接根据相邻节点的指针遍历 + +B+ 树的**叶子节点是数据页**(page),一个页里面可以存多个数据行 + +![](https://seazean.oss-cn-beijing.aliyuncs.com/img/DB/索引的原理2.png) + +通常在 B+Tree 上有两个头指针,**一个指向根节点,另一个指向关键字最小的叶子节点**,而且所有叶子节点(即数据节点)之间是一种链式环结构。可以对 B+Tree 进行两种查找运算: + +- 有范围:对于主键的范围查找和分页查找 +- 有顺序:从根节点开始,进行随机查找,顺序查找 + +InnoDB 中每个数据页的大小默认是 16KB, + +* 索引行:一般表的主键类型为 INT(4 字节)或 BIGINT(8 字节),指针大小在 InnoDB 中设置为 6 字节节,也就是说一个页大概存储 16KB/(8B+6B)=1K 个键值(估值)。则一个深度为 3 的 B+Tree 索引可以维护 `10^3 * 10^3 * 10^3 = 10亿` 条记录 +* 数据行:一行数据的大小可能是 1k,一个数据页可以存储 16 行 + +实际情况中每个节点可能不能填充满,因此在数据库中,B+Tree 的高度一般都在 2-4 层。MySQL 的 InnoDB 存储引擎在设计时是**将根节点常驻内存的**,也就是说查找某一键值的行记录时最多只需要 1~3 次磁盘 I/O 操作 + +B+Tree 优点:提高查询速度,减少磁盘的 IO 次数,树形结构较小 + + + +*** + + + +##### 索引维护 + +B+ 树为了保持索引的有序性,在插入新值的时候需要做相应的维护 + +每个索引中每个块存储在磁盘页中,可能会出现以下两种情况: + +* 如果所在的数据页已经满了,这时候需要申请一个新的数据页,然后挪动部分数据过去,这个过程称为**页分裂**,原本放在一个页的数据现在分到两个页中,降低了空间利用率 +* 当相邻两个页由于删除了数据,利用率很低之后,会将数据页做**页合并**,合并的过程可以认为是分裂过程的逆过程 +* 这两个情况都是由 B+ 树的结构决定的 + +一般选用数据小的字段做索引,字段长度越小,普通索引的叶子节点就越小,普通索引占用的空间也就越小 + +自增主键的插入数据模式,可以让主键索引尽量地保持递增顺序插入,不涉及到挪动其他记录,**避免了页分裂**,页分裂的目的就是保证后一个数据页中的所有行主键值比前一个数据页中主键值大 + + + +参考文章:https://developer.aliyun.com/article/919861 + + + +*** + + + +### 设计原则 + +索引的设计可以遵循一些已有的原则,创建索引的时候请尽量考虑符合这些原则,便于提升索引的使用效率 + +创建索引时的原则: +- 对查询频次较高,且数据量比较大的表建立索引 +- 使用唯一索引,区分度越高,使用索引的效率越高 +- 索引字段的选择,最佳候选列应当从 where 子句的条件中提取,使用覆盖索引 +- 使用短索引,索引创建之后也是使用硬盘来存储的,因此提升索引访问的 I/O 效率,也可以提升总体的访问效率。假如构成索引的字段总长度比较短,那么在给定大小的存储块内可以存储更多的索引值,相应的可以有效的提升 MySQL 访问索引的 I/O 效率 +- 索引可以有效的提升查询数据的效率,但索引数量不是多多益善,索引越多,维护索引的代价越高。对于插入、更新、删除等 DML 操作比较频繁的表来说,索引过多,会引入相当高的维护代价,降低 DML 操作的效率,增加相应操作的时间消耗;另外索引过多的话,MySQL 也会犯选择困难病,虽然最终仍然会找到一个可用的索引,但提高了选择的代价 + +* MySQL 建立联合索引时会遵守**最左前缀匹配原则**,即最左优先,在检索数据时从联合索引的最左边开始匹配 + + N 个列组合而成的组合索引,相当于创建了 N 个索引,如果查询时 where 句中使用了组成该索引的**前**几个字段,那么这条查询 SQL 可以利用组合索引来提升查询效率 + + ```mysql + -- 对name、address、phone列建一个联合索引 + ALTER TABLE user ADD INDEX index_three(name,address,phone); + -- 查询语句执行时会依照最左前缀匹配原则,检索时分别会使用索引进行数据匹配。 + (name,address,phone) + (name,address) + (name,phone) -- 只有name字段走了索引 + (name) + + -- 索引的字段可以是任意顺序的,优化器会帮助我们调整顺序,下面的SQL语句可以命中索引 + SELECT * FROM user WHERE address = '北京' AND phone = '12345' AND name = '张三'; + ``` + + ```mysql + -- 如果联合索引中最左边的列不包含在条件查询中,SQL语句就不会命中索引,比如: + SELECT * FROM user WHERE address = '北京' AND phone = '12345'; + ``` + +哪些情况不要建立索引: + +* 记录太少的表 +* 经常增删改的表 +* 频繁更新的字段不适合创建索引 +* where 条件里用不到的字段不创建索引 + + + +*** + + + +### 索引优化 + +#### 覆盖索引 + +覆盖索引:包含所有满足查询需要的数据的索引(SELECT 后面的字段刚好是索引字段),可以利用该索引返回 SELECT 列表的字段,而不必根据索引去聚簇索引上读取数据文件 + +回表查询:要查找的字段不在非主键索引树上时,需要通过叶子节点的主键值去主键索引上获取对应的行数据 + +使用覆盖索引,防止回表查询: + +* 表 user 主键为 id,普通索引为 age,查询语句: + + ```mysql + SELECT * FROM user WHERE age = 30; + ``` + + 查询过程:先通过普通索引 age=30 定位到主键值 id=1,再通过聚集索引 id=1 定位到行记录数据,需要两次扫描 B+ 树 + +* 使用覆盖索引: + + ```mysql + DROP INDEX idx_age ON user; + CREATE INDEX idx_age_name ON user(age,name); + SELECT id,age FROM user WHERE age = 30; + ``` + + 在一棵索引树上就能获取查询所需的数据,无需回表速度更快 + +使用覆盖索引,要注意 SELECT 列表中只取出需要的列,不可用 SELECT *,所有字段一起做索引会导致索引文件过大,查询性能下降 + + + +*** + + + +#### 索引下推 + +索引条件下推优化(Index Condition Pushdown,ICP)是 MySQL5.6 添加,可以在索引遍历过程中,对索引中包含的字段先做判断,直接过滤掉不满足条件的记录,减少回表次数 + +索引下推充分利用了索引中的数据,在查询出整行数据之前过滤掉无效的数据,再去主键索引树上查找 + +* 不使用索引下推优化时存储引擎通过索引检索到数据,然后回表查询记录返回给 Server 层,**服务器判断数据是否符合条件** + + ![](https://seazean.oss-cn-beijing.aliyuncs.com/img/DB/MySQL-不使用索引下推.png) +* 使用索引下推优化时,如果**存在某些被索引的列的判断条件**时,由存储引擎在索引遍历的过程中判断数据是否符合传递的条件,将符合条件的数据进行回表,检索出来返回给服务器,由此减少 IO 次数 + + ![](https://seazean.oss-cn-beijing.aliyuncs.com/img/DB/MySQL-使用索引下推.png) + +**适用条件**: + +* 需要存储引擎将索引中的数据与条件进行判断(所以**条件列必须都在同一个索引中**),所以优化是基于存储引擎的,只有特定引擎可以使用,适用于 InnoDB 和 MyISAM +* 存储引擎没有调用跨存储引擎的能力,跨存储引擎的功能有存储过程、触发器、视图,所以调用这些功能的不可以进行索引下推优化 +* 对于 InnoDB 引擎只适用于二级索引,InnoDB 的聚簇索引会将整行数据读到缓冲区,不再需要去回表查询了 + +工作过程:用户表 user,(name, age) 是联合索引 + +```mysql +SELECT * FROM user WHERE name LIKE '张%' AND age = 10; -- 头部模糊匹配会造成索引失效 +``` + +* 优化前:在非主键索引树上找到满足第一个条件的行,然后通过叶子节点记录的主键值再回到主键索引树上查找到对应的行数据,再对比 AND 后的条件是否符合,符合返回数据,需要 4 次回表 + + ![](https://seazean.oss-cn-beijing.aliyuncs.com/img/DB/MySQL-索引下推优化1.png) + +* 优化后:检查索引中存储的列信息是否符合索引条件,然后交由存储引擎用剩余的判断条件判断此行数据是否符合要求,**不满足条件的不去读取表中的数据**,满足下推条件的就根据主键值进行回表查询,2 次回表 + ![](https://seazean.oss-cn-beijing.aliyuncs.com/img/DB/MySQL-索引下推优化2.png) + +当使用 EXPLAIN 进行分析时,如果使用了索引条件下推,Extra 会显示 Using index condition + + + +参考文章:https://blog.csdn.net/sinat_29774479/article/details/103470244 + +参考文章:https://time.geekbang.org/column/article/69636 + + + +*** + + + +#### 前缀索引 + +当要索引的列字符很多时,索引会变大变慢,可以只索引列开始的部分字符串,节约索引空间,提高索引效率 + +注意:使用前缀索引就系统就忽略覆盖索引对查询性能的优化了 + +优化原则:**降低重复的索引值** + +比如地区表: + +```mysql +area gdp code +chinaShanghai 100 aaa +chinaDalian 200 bbb +usaNewYork 300 ccc +chinaFuxin 400 ddd +chinaBeijing 500 eee +``` + +发现 area 字段很多都是以 china 开头的,那么如果以前 1-5 位字符做前缀索引就会出现大量索引值重复的情况,索引值重复性越低,查询效率也就越高,所以需要建立前 6 位字符的索引: + +```mysql +CREATE INDEX idx_area ON table_name(area(7)); +``` + +场景:存储身份证 + +* 直接创建完整索引,这样可能比较占用空间 +* 创建前缀索引,节省空间,但会增加查询扫描次数,并且不能使用覆盖索引 +* 倒序存储,再创建前缀索引,用于绕过字符串本身前缀的区分度不够的问题(前 6 位相同的很多) +* 创建 hash 字段索引,查询性能稳定,有额外的存储和计算消耗,跟第三种方式一样,都不支持范围扫描 + + + +**** + + + +#### 索引合并 + +使用多个索引来完成一次查询的执行方法叫做索引合并 index merge + +* Intersection 索引合并: + + ```sql + SELECT * FROM table_test WHERE key1 = 'a' AND key3 = 'b'; # key1 和 key3 列都是单列索引、二级索引 + ``` + + 从不同索引中扫描到的记录的 id 值取**交集**(相同 id),然后执行回表操作,要求从每个二级索引获取到的记录都是按照主键值排序 + +* Union 索引合并: + + ```sql + SELECT * FROM table_test WHERE key1 = 'a' OR key3 = 'b'; + ``` + + 从不同索引中扫描到的记录的 id 值取**并集**,然后执行回表操作,要求从每个二级索引获取到的记录都是按照主键值排序 + +* Sort-Union 索引合并 + + ```sql + SELECT * FROM table_test WHERE key1 < 'a' OR key3 > 'b'; + ``` + + 先将从不同索引中扫描到的记录的主键值进行排序,再按照 Union 索引合并的方式进行查询 + +索引合并算法的效率并不好,通过将其中的一个索引改成联合索引会优化效率 + + + + + +*** + + + + + +## 系统优化 + +### 表优化 + +#### 分区表 + +##### 基本介绍 + +分区表是将大表的数据按分区字段分成许多小的子集,建立一个以 ftime 年份为分区的表: + +```mysql +CREATE TABLE `t` ( + `ftime` datetime NOT NULL, + `c` int(11) DEFAULT NULL, + KEY (`ftime`) +) ENGINE=InnoDB DEFAULT CHARSET=latin1 +PARTITION BY RANGE (YEAR(ftime)) +(PARTITION p_2017 VALUES LESS THAN (2017) ENGINE = InnoDB, + PARTITION p_2018 VALUES LESS THAN (2018) ENGINE = InnoDB, + PARTITION p_2019 VALUES LESS THAN (2019) ENGINE = InnoDB, + PARTITION p_others VALUES LESS THAN MAXVALUE ENGINE = InnoDB); +INSERT INTO t VALUES('2017-4-1',1),('2018-4-1',1);-- 这两行记录分别落在 p_2018 和 p_2019 这两个分区上 +``` + +这个表包含了一个.frm 文件和 4 个.ibd 文件,每个分区对应一个.ibd 文件 + +* 对于引擎层来说,这是 4 个表,针对每个分区表的操作不会相互影响 +* 对于 Server 层来说,这是 1 个表 + + + +*** + + + +##### 分区策略 + +打开表行为:第一次访问一个分区表时,MySQL 需要**把所有的分区都访问一遍**,如果分区表的数量很多,超过了 open_files_limit 参数(默认值 1024),那么就会在访问这个表时打开所有的文件,导致打开表文件的个数超过了上限而报错 + +通用分区策略:MyISAM 分区表使用的分区策略,每次访问分区都由 Server 层控制,在文件管理、表管理的实现上很粗糙,因此有比较严重的性能问题 + +本地分区策略:从 MySQL 5.7.9 开始,InnoDB 引擎内部自己管理打开分区的行为,InnoDB 引擎打开文件超过 innodb_open_files 时就会**关掉一些之前打开的文件**,所以即使分区个数大于 open_files_limit,也不会报错 + +从 MySQL 8.0 版本开始,就不允许创建 MyISAM 分区表,只允许创建已经实现了本地分区策略的引擎,目前只有 InnoDB 和 NDB 这两个引擎支持了本地分区策略 + + + +*** + + + +##### Server 层 + +从 Server 层看一个分区表就只是一个表 + +* Session A: + + ```mysql + SELECT * FROM t WHERE ftime = '2018-4-1'; + ``` + +* Session B: + + ```mysql + ALTER TABLE t TRUNCATE PARTITION p_2017; -- blocked + ``` + +现象:Session B 只操作 p_2017 分区,但是由于 Session A 持有整个表 t 的 MDL 读锁,就导致 B 的 ALTER 语句获取 MDL 写锁阻塞 + +分区表的特点: + +* 第一次访问的时候需要访问所有分区 +* 在 Server 层认为这是同一张表,因此**所有分区共用同一个 MDL 锁** +* 在引擎层认为这是不同的表,因此 MDL 锁之后的执行过程,会根据分区表规则,只访问需要的分区 + + + +*** + + + +##### 应用场景 + +分区表的优点: + +* 对业务透明,相对于用户分表来说,使用分区表的业务代码更简洁 + +* 分区表可以很方便的清理历史数据。按照时间分区的分区表,就可以直接通过 `alter table t drop partition` 这个语法直接删除分区文件,从而删掉过期的历史数据,与使用 drop 语句删除数据相比,优势是速度快、对系统影响小 + +使用分区表,不建议创建太多的分区,注意事项: + +* 分区并不是越细越好,单表或者单分区的数据一千万行,只要没有特别大的索引,对于现在的硬件能力来说都已经是小表 +* 分区不要提前预留太多,在使用之前预先创建即可。比如是按月分区,每年年底时再把下一年度的 12 个新分区创建上即可,并且对于没有数据的历史分区,要及时的 drop 掉 + + + +参考文档:https://time.geekbang.org/column/article/82560 + + + +*** + + + +#### 临时表 + +##### 基本介绍 + +临时表分为内部临时表和用户临时表 + +* 内部临时表:系统执行 SQL 语句优化时产生的表,例如 Join 连接查询、去重查询等 + +* 用户临时表:用户主动创建的临时表 + + ```mysql + CREATE TEMPORARY TABLE temp_t like table_1; + ``` + +临时表可以是内存表,也可以是磁盘表(多表操作 → 嵌套查询章节提及) + +* 内存表指的是使用 Memory 引擎的表,建立哈希索引,建表语法是 `create table … engine=memory`,这种表的数据都保存在内存里,系统重启时会被清空,但是表结构还在 +* 磁盘表是使用 InnoDB 引擎或者 MyISAM 引擎的临时表,建立 B+ 树索引,写数据的时候是写到磁盘上的 + +临时表的特点: + +* 一个临时表只能被创建它的 session 访问,对其他线程不可见,所以不同 session 的临时表是**可以重名**的 +* 临时表可以与普通表同名,会话内有同名的临时表和普通表时,执行 show create 语句以及增删改查语句访问的都是临时表 +* show tables 命令不显示临时表 +* 数据库发生异常重启不需要担心数据删除问题,临时表会**自动回收** + + + +*** + + + +##### 重名原理 + +执行创建临时表的 SQL: + +```mysql +create temporary table temp_t(id int primary key)engine=innodb; +``` + +MySQL 给 InnoDB 表创建一个 frm 文件保存表结构定义,在 ibd 保存表数据。frm 文件放在临时文件目录下,文件名的后缀是 .frm,**前缀是** `#sql{进程 id}_{线程 id}_ 序列号`,使用 `select @@tmpdir` 命令,来显示实例的临时文件目录 + +MySQL 维护数据表,除了物理磁盘上的文件外,内存里也有一套机制区别不同的表,每个表都对应一个 table_def_key + +* 一个普通表的 table_def_key 的值是由 `库名 + 表名` 得到的,所以如果在同一个库下创建两个同名的普通表,创建第二个表的过程中就会发现 table_def_key 已经存在了 +* 对于临时表,table_def_key 在 `库名 + 表名` 基础上,又加入了 `server_id + thread_id`,所以不同线程之间,临时表可以重名 + +实现原理:每个线程都维护了自己的临时表链表,每次 session 内操作表时,先遍历链表,检查是否有这个名字的临时表,如果有就**优先操作临时表**,如果没有再操作普通表;在 session 结束时对链表里的每个临时表,执行 `DROP TEMPORARY TABLE + 表名` 操作 + +执行 rename table 语句无法修改临时表,因为会按照 `库名 / 表名.frm` 的规则去磁盘找文件,但是临时表文件名的规则是 `#sql{进程 id}_{线程 id}_ 序列号.frm`,因此会报找不到文件名的错误 + + + +**** + + + +##### 主备复制 + +创建临时表的语句会传到备库执行,因此备库的同步线程就会创建这个临时表。主库在线程退出时会自动删除临时表,但备库同步线程是持续在运行的并不会退出,所以这时就需要在主库上再写一个 DROP TEMPORARY TABLE 传给备库执行 + +binlog 日志写入规则: + +* binlog_format=row,跟临时表有关的语句就不会记录到 binlog +* binlog_format=statment/mixed,binlog 中才会记录临时表的操作,也就会记录 `DROP TEMPORARY TABLE` 这条命令 + +主库上不同的线程创建同名的临时表是不冲突的,但是备库只有一个执行线程,所以 MySQL 在记录 binlog 时会把主库执行这个语句的线程 id 写到 binlog 中,在备库的应用线程就可以获取执行每个语句的主库线程 id,并利用这个线程 id 来构造临时表的 table_def_key + +* session A 的临时表 t1,在备库的 table_def_key 就是:`库名 + t1 +“M 的 serverid" + "session A 的 thread_id”` +* session B 的临时表 t1,在备库的 table_def_key 就是 :`库名 + t1 +"M 的 serverid" + "session B 的 thread_id"` + +MySQL 在记录 binlog 的时不论是 create table 还是 alter table 语句都是原样记录,但是如果执行 drop table,系统记录 binlog 就会被服务端改写 + +```mysql +DROP TABLE `t_normal` /* generated by server */ +``` + + + +*** + + + +##### 跨库查询 + +分库分表系统的跨库查询使用临时表不用担心线程之间的重名冲突,分库分表就是要把一个逻辑上的大表分散到不同的数据库实例上 + +比如将一个大表 ht,按照字段 f,拆分成 1024 个分表,分布到 32 个数据库实例上,一般情况下都有一个中间层 proxy 解析 SQL 语句,通过分库规则通过分表规则(比如 N%1024)确定将这条语句路由到哪个分表做查询 + +```mysql +select v from ht where f=N; +``` + +如果这个表上还有另外一个索引 k,并且查询语句: + +```mysql +select v from ht where k >= M order by t_modified desc limit 100; +``` + +查询条件里面没有用到分区字段 f,只能**到所有的分区**中去查找满足条件的所有行,然后统一做 order by 操作,两种方式: + +* 在 proxy 层的进程代码中实现排序,拿到分库的数据以后,直接在内存中参与计算,但是对 proxy 端的压力比较大,很容易出现内存不够用和 CPU 瓶颈问题 +* 把各个分库拿到的数据,汇总到一个 MySQL 实例的一个表中,然后在这个汇总实例上做逻辑操作,执行流程: + * 在汇总库上创建一个临时表 temp_ht,表里包含三个字段 v、k、t_modified + * 在各个分库执行:`select v,k,t_modified from ht_x where k >= M order by t_modified desc limit 100` + * 把分库执行的结果插入到 temp_ht 表中 + * 在临时表上执行:`select v from temp_ht order by t_modified desc limit 100` + + + + + +*** + + + +### 优化步骤 + +#### 执行频率 + +MySQL 客户端连接成功后,查询服务器状态信息: + +```mysql +SHOW [SESSION|GLOBAL] STATUS LIKE ''; +-- SESSION: 显示当前会话连接的统计结果,默认参数 +-- GLOBAL: 显示自数据库上次启动至今的统计结果 +``` + +* 查看 SQL 执行频率: + + ```mysql + SHOW STATUS LIKE 'Com_____'; + ``` + + Com_xxx 表示每种语句执行的次数 + + ![](https://seazean.oss-cn-beijing.aliyuncs.com/img/DB/MySQL-SQL语句执行频率.png) + +* 查询 SQL 语句影响的行数: + + ```mysql + SHOW STATUS LIKE 'Innodb_rows_%'; + ``` + + ![](https://seazean.oss-cn-beijing.aliyuncs.com/img/DB/MySQL-SQL语句影响的行数.png) + +Com_xxxx:这些参数对于所有存储引擎的表操作都会进行累计 + +Innodb_xxxx:这几个参数只是针对 InnoDB 存储引擎的,累加的算法也略有不同 + +| 参数 | 含义 | +| :------------------- | ------------------------------------------------------------ | +| Com_select | 执行 SELECT 操作的次数,一次查询只累加 1 | +| Com_insert | 执行 INSERT 操作的次数,对于批量插入的 INSERT 操作,只累加一次 | +| Com_update | 执行 UPDATE 操作的次数 | +| Com_delete | 执行 DELETE 操作的次数 | +| Innodb_rows_read | 执行 SELECT 查询返回的行数 | +| Innodb_rows_inserted | 执行 INSERT 操作插入的行数 | +| Innodb_rows_updated | 执行 UPDATE 操作更新的行数 | +| Innodb_rows_deleted | 执行 DELETE 操作删除的行数 | +| Connections | 试图连接 MySQL 服务器的次数 | +| Uptime | 服务器工作时间 | +| Slow_queries | 慢查询的次数 | + + + +**** + + + +#### 定位低效 + +SQL 执行慢有两种情况: + +* 偶尔慢:DB 在刷新脏页(学完事务就懂了) + * redo log 写满了 + * 内存不够用,要从 LRU 链表中淘汰 + * MySQL 认为系统空闲的时候 + * MySQL 关闭时 +* 一直慢的原因:索引没有设计好、SQL 语句没写好、MySQL 选错了索引 + +通过以下两种方式定位执行效率较低的 SQL 语句 + +* 慢日志查询: 慢查询日志在查询结束以后才记录,执行效率出现问题时查询日志并不能定位问题 + + 配置文件修改:修改 .cnf 文件 `vim /etc/mysql/my.cnf`,重启 MySQL 服务器 + + ```sh + slow_query_log=ON + slow_query_log_file=/usr/local/mysql/var/localhost-slow.log + long_query_time=1 #记录超过long_query_time秒的SQL语句的日志 + log-queries-not-using-indexes = 1 + ``` + + 使用命令配置: + + ```mysql + mysql> SET slow_query_log=ON; + mysql> SET GLOBAL slow_query_log=ON; + ``` + + 查看是否配置成功: + + ```mysql + SHOW VARIABLES LIKE '%query%' + ``` + +* SHOW PROCESSLIST:**实时查看**当前 MySQL 在进行的连接线程,包括线程的状态、是否锁表、SQL 的执行情况,同时对一些锁表操作进行优化 + + ![](https://seazean.oss-cn-beijing.aliyuncs.com/img/DB/MySQL-SHOW_PROCESSLIST命令.png) + + + + + + +*** + + + +#### EXPLAIN + +##### 执行计划 + +通过 EXPLAIN 命令获取执行 SQL 语句的信息,包括在 SELECT 语句执行过程中如何连接和连接的顺序,执行计划在优化器优化完成后、执行器之前生成,然后执行器会调用存储引擎检索数据 + +查询 SQL 语句的执行计划: + +```mysql +EXPLAIN SELECT * FROM table_1 WHERE id = 1; +``` + +![](https://seazean.oss-cn-beijing.aliyuncs.com/img/DB/MySQL-explain查询SQL语句的执行计划.png) + +| 字段 | 含义 | +| ------------- | ------------------------------------------------------------ | +| id | SELECT 的序列号 | +| select_type | 表示 SELECT 的类型 | +| table | 访问数据库中表名称,有时可能是简称或者临时表名称() | +| type | 表示表的连接类型 | +| possible_keys | 表示查询时,可能使用的索引 | +| key | 表示实际使用的索引 | +| key_len | 索引字段的长度 | +| ref | 表示与索引列进行等值匹配的对象,常数、某个列、函数等,type 必须在(range, const] 之间,左闭右开 | +| rows | 扫描出的行数,表示 MySQL 根据表统计信息及索引选用情况,**估算**的找到所需的记录扫描的行数 | +| filtered | 条件过滤的行百分比,单表查询没意义,用于连接查询中对驱动表的扇出进行过滤,查询优化器预测所有扇出值满足剩余查询条件的百分比,相乘以后表示多表查询中还要对被驱动执行查询的次数 | +| extra | 执行情况的说明和描述 | + +MySQL **执行计划的局限**: + +* 只是计划,不是执行 SQL 语句,可以随着底层优化器输入的更改而更改 +* EXPLAIN 不会告诉显示关于触发器、存储过程的信息对查询的影响情况, 不考虑各种 Cache +* EXPLAIN 不能显示 MySQL 在执行查询时的动态,因为执行计划在执行**查询之前生成** +* EXPALIN 只能解释 SELECT 操作,其他操作要重写为 SELECT 后查看执行计划 +* EXPLAIN PLAN 显示的是在解释语句时数据库将如何运行 SQL 语句,由于执行环境和 EXPLAIN PLAN 环境的不同,此计划可能与 SQL 语句**实际的执行计划不同**,部分统计信息是估算的,并非精确值 + +SHOW WARINGS:在使用 EXPALIN 命令后执行该语句,可以查询与执行计划相关的拓展信息,展示出 Level、Code、Message 三个字段,当 Code 为 1003 时,Message 字段展示的信息类似于将查询语句重写后的信息,但是不是等价,不能执行复制过来运行 + +环境准备: + +![](https://seazean.oss-cn-beijing.aliyuncs.com/img/DB/MySQL-执行计划环境准备.png) + + + + + +*** + + + +##### id + +id 代表 SQL 执行的顺序的标识,每个 SELECT 关键字对应一个唯一 id,所以在同一个 SELECT 关键字中的表的 id 都是相同的。SELECT 后的 FROM 可以跟随多个表,每个表都会对应一条记录,这些记录的 id 都是相同的, + +* id 相同时,执行顺序由上至下。连接查询的执行计划,记录的 id 值都是相同的,出现在前面的表为驱动表,后面为被驱动表 + + ```mysql + EXPLAIN SELECT * FROM t_role r, t_user u, user_role ur WHERE r.id = ur.role_id AND u.id = ur.user_id ; + ``` + + ![](https://seazean.oss-cn-beijing.aliyuncs.com/img/DB/MySQL-explain之id相同.png) + +* id 不同时,id 值越大优先级越高,越先被执行 + + ```mysql + EXPLAIN SELECT * FROM t_role WHERE id = (SELECT role_id FROM user_role WHERE user_id = (SELECT id FROM t_user WHERE username = 'stu1')) + ``` + + ![](https://seazean.oss-cn-beijing.aliyuncs.com/img/DB/MySQL-explain之id不同.png) + +* id 有相同也有不同时,id 相同的可以认为是一组,从上往下顺序执行;在所有的组中,id 的值越大的组,优先级越高,越先执行 + + ```mysql + EXPLAIN SELECT * FROM t_role r , (SELECT * FROM user_role ur WHERE ur.`user_id` = '2') a WHERE r.id = a.role_id ; + ``` + + ![](https://seazean.oss-cn-beijing.aliyuncs.com/img/DB/MySQL-explain之id相同和不同.png) + +* id 为 NULL 时代表的是临时表 + + + +*** + + + +##### select + +表示查询中每个 select 子句的类型(简单 OR 复杂) + +| select_type | 含义 | +| ------------------ | ------------------------------------------------------------ | +| SIMPLE | 简单的 SELECT 查询,查询中不包含子查询或者 UNION | +| PRIMARY | 查询中若包含任何复杂的子查询,最外层(也就是最左侧)查询标记为该标识 | +| UNION | 对于 UNION 或者 UNION ALL 的复杂查询,除了最左侧的查询,其余的小查询都是 UNION | +| UNION RESULT | UNION 需要使用临时表进行去重,临时表的是 UNION RESULT | +| DEPENDENT UNION | 对于 UNION 或者 UNION ALL 的复杂查询,如果各个小查询都依赖外层查询,是相关子查询,除了最左侧的小查询为 DEPENDENT SUBQUERY,其余都是 DEPENDENT UNION | +| SUBQUERY | 子查询不是相关子查询,该子查询第一个 SELECT 代表的查询就是这种类型,会进行物化(该子查询只需要执行一次) | +| DEPENDENT SUBQUERY | 子查询是相关子查询,该子查询第一个 SELECT 代表的查询就是这种类型,不会物化(该子查询需要执行多次) | +| DERIVED | 在 FROM 列表中包含的子查询,被标记为 DERIVED(衍生),也就是生成物化派生表的这个子查询 | +| MATERIALIZED | 将子查询物化后与与外层进行连接查询,生成物化表的子查询 | + +子查询为 DERIVED:`SELECT * FROM (SELECT key1 FROM t1) AS derived_1 WHERE key1 > 10` + +子查询为 MATERIALIZED:`SELECT * FROM t1 WHERE key1 IN (SELECT key1 FROM t2)` + + + +**** + + + +##### type + +对表的访问方式,表示 MySQL 在表中找到所需行的方式,又称访问类型 + +| type | 含义 | +| --------------- | ------------------------------------------------------------ | +| ALL | 全表扫描,如果是 InnoDB 引擎是扫描聚簇索引 | +| index | 可以使用覆盖索引,但需要扫描全部索引 | +| range | 索引范围扫描,常见于 between、<、> 等的查询 | +| index_subquery | 子查询可以普通索引,则子查询的 type 为 index_subquery | +| unique_subquery | 子查询可以使用主键或唯一二级索引,则子查询的 type 为 index_subquery | +| index_merge | 索引合并 | +| ref_or_null | 非唯一性索引(普通二级索引)并且可以存储 NULL,进行等值匹配 | +| ref | 非唯一性索引与常量等值匹配 | +| eq_ref | 唯一性索引(主键或不存储 NULL 的唯一二级索引)进行等值匹配,如果二级索引是联合索引,那么所有联合的列都要进行等值匹配 | +| const | 通过主键或者唯一二级索引与常量进行等值匹配 | +| system | system 是 const 类型的特例,当查询的表只有一条记录的情况下,使用 system | +| NULL | MySQL 在优化过程中分解语句,执行时甚至不用访问表或索引 | + +从上到下,性能从差到好,一般来说需要保证查询至少达到 range 级别, 最好达到 ref + + + +*** + + + +##### key + +possible_keys: + +* 指出 MySQL 能使用哪个索引在表中找到记录,查询涉及到的字段上若存在索引,则该索引将被列出,但不一定被查询使用 +* 如果该列是 NULL,则没有相关的索引 + +key: + +* 显示 MySQL 在查询中实际使用的索引,若没有使用索引,显示为 NULL +* 查询中若使用了**覆盖索引**,则该索引可能出现在 key 列表,不出现在 possible_keys + +key_len: + +* 表示索引中使用的字节数,可通过该列计算查询中使用的索引的长度 +* key_len 显示的值为索引字段的最大可能长度,并非实际使用长度,即 key_len 是根据表定义计算而得,不是通过表内检索出的 +* 在不损失精确性的前提下,长度越短越好 + + + +*** + + + +##### Extra + +其他的额外的执行计划信息,在该列展示: + +* No tables used:查询语句中使用 FROM dual 或者没有 FROM 语句 +* Impossible WHERE:查询语句中的 WHERE 子句条件永远为 FALSE,会导致没有符合条件的行 +* Using index:该值表示相应的 SELECT 操作中使用了**覆盖索引**(Covering Index) +* Using index condition:第一种情况是搜索条件中虽然出现了索引列,但是部分条件无法形成扫描区间(**索引失效**),会根据可用索引的条件先搜索一遍再匹配无法使用索引的条件,回表查询数据;第二种是使用了**索引条件下推**优化 +* Using where:搜索的数据需要在 Server 层判断,无法使用索引下推 +* Using join buffer:连接查询被驱动表无法利用索引,需要连接缓冲区来存储中间结果 +* Using filesort:无法利用索引完成排序(优化方向),需要对数据使用外部排序算法,将取得的数据在内存或磁盘中进行排序 +* Using temporary:表示 MySQL 需要使用临时表来存储结果集,常见于**排序、去重(UNION)、分组**等场景 +* Select tables optimized away:说明仅通过使用索引,优化器可能仅从聚合函数结果中返回一行 +* No tables used:Query 语句中使用 from dual 或不含任何 from 子句 + + + +参考文章:https://www.cnblogs.com/ggjucheng/archive/2012/11/11/2765237.html + + + +**** + + + +#### PROFILES + +SHOW PROFILES 能够在做 SQL 优化时分析当前会话中语句执行的**资源消耗**情况 + +* 通过 have_profiling 参数,能够看到当前 MySQL 是否支持 profile: + ![](https://seazean.oss-cn-beijing.aliyuncs.com/img/DB/MySQL-have_profiling.png) + +* 默认 profiling 是关闭的,可以通过 set 语句在 Session 级别开启 profiling: + + ![](https://seazean.oss-cn-beijing.aliyuncs.com/img/DB/MySQL-profiling.png) + + ```mysql + SET profiling=1; #开启profiling 开关; + ``` + +* 执行 SHOW PROFILES 指令, 来查看 SQL 语句执行的耗时: + + ```mysql + SHOW PROFILES; + ``` + + ![](https://seazean.oss-cn-beijing.aliyuncs.com/img/DB/MySQL-查看SQL语句执行耗时.png) + +* 查看到该 SQL 执行过程中每个线程的状态和消耗的时间: + + ```mysql + SHOW PROFILE FOR QUERY query_id; + ``` + + ![](https://seazean.oss-cn-beijing.aliyuncs.com/img/DB/MySQL-SQL执行每个状态消耗的时间.png) + +* 在获取到最消耗时间的线程状态后,MySQL 支持选择 all、cpu、block io 、context switch、page faults 等类型查看 MySQL 在使用什么资源上耗费了过高的时间。例如,选择查看 CPU 的耗费时间: + + ![](https://seazean.oss-cn-beijing.aliyuncs.com/img/DB/MySQL-SQL执行每个状态消耗的CPU.png) + + * Status:SQL 语句执行的状态 + * Durationsql:执行过程中每一个步骤的耗时 + * CPU_user:当前用户占有的 CPU + * CPU_system:系统占有的 CPU + + + +*** + + + +#### TRACE + +MySQL 提供了对 SQL 的跟踪, 通过 trace 文件可以查看优化器**生成执行计划的过程** + +* 打开 trace 功能,设置格式为 JSON,并设置 trace 的最大使用内存,避免解析过程中因默认内存过小而不能够完整展示 + + ```mysql + SET optimizer_trace="enabled=on",end_markers_in_json=ON; -- 会话内有效 + SET optimizer_trace_max_mem_size=1000000; + ``` + +* 执行 SQL 语句: + + ```mysql + SELECT * FROM tb_item WHERE id < 4; + ``` + +* 检查 information_schema.optimizer_trace: + + ```mysql + SELECT * FROM information_schema.optimizer_trace \G; -- \G代表竖列展示 + ``` + + 执行信息主要有三个阶段:prepare 阶段、optimize 阶段(成本分析)、execute 阶段(执行) + + + + + +**** + + + +### 索引优化 + +#### 创建索引 + +索引是数据库优化最重要的手段之一,通过索引通常可以帮助用户解决大多数的 MySQL 的性能优化问题 + +```mysql +CREATE TABLE `tb_seller` ( + `sellerid` varchar (100), + `name` varchar (100), + `nickname` varchar (50), + `password` varchar (60), + `status` varchar (1), + `address` varchar (100), + `createtime` datetime, + PRIMARY KEY(`sellerid`) +)ENGINE=INNODB DEFAULT CHARSET=utf8mb4; +INSERT INTO `tb_seller` (`sellerid`, `name`, `nickname`, `password`, `status`, `address`, `createtime`) values('xiaomi','小米科技','小米官方旗舰店','e10adc3949ba59abbe56e057f20f883e','1','西安市','2088-01-01 12:00:00'); +CREATE INDEX idx_seller_name_sta_addr ON tb_seller(name, status, address); # 联合索引 +``` + +![](https://seazean.oss-cn-beijing.aliyuncs.com/img/DB/MySQL-优化SQL使用索引环境准备.png) + + + +**** + + + +#### 避免失效 + +##### 语句错误 + +* 全值匹配:对索引中所有列都指定具体值,这种情况索引生效,执行效率高 + + ```mysql + EXPLAIN SELECT * FROM tb_seller WHERE name='小米科技' AND status='1' AND address='西安市'; + ``` + + ![](https://seazean.oss-cn-beijing.aliyuncs.com/img/DB/MySQL-优化SQL使用索引1.png) + +* **最左前缀法则**:联合索引遵守最左前缀法则 + + 匹配最左前缀法则,走索引: + + ```mysql + EXPLAIN SELECT * FROM tb_seller WHERE name='小米科技'; + EXPLAIN SELECT * FROM tb_seller WHERE name='小米科技' AND status='1'; + ``` + + ![](https://seazean.oss-cn-beijing.aliyuncs.com/img/DB/MySQL-优化SQL使用索引2.png) + + 违法最左前缀法则 , 索引失效: + + ```mysql + EXPLAIN SELECT * FROM tb_seller WHERE status='1'; + EXPLAIN SELECT * FROM tb_seller WHERE status='1' AND address='西安市'; + ``` + + ![](https://seazean.oss-cn-beijing.aliyuncs.com/img/DB/MySQL-优化SQL使用索引3.png) + + 如果符合最左法则,但是出现跳跃某一列,只有最左列索引生效: + + ```mysql + EXPLAIN SELECT * FROM tb_seller WHERE name='小米科技' AND address='西安市'; + ``` + + ![](https://seazean.oss-cn-beijing.aliyuncs.com/img/DB/MySQL-优化SQL使用索引4.png) + + 虽然索引列失效,但是系统会**使用了索引下推进行了优化** + +* **范围查询**右边的列,不能使用索引: + + ```mysql + EXPLAIN SELECT * FROM tb_seller WHERE name='小米科技' AND status>'1' AND address='西安市'; + ``` + + 根据前面的两个字段 name , status 查询是走索引的, 但是最后一个条件 address 没有用到索引,使用了索引下推 + + ![](https://seazean.oss-cn-beijing.aliyuncs.com/img/DB/MySQL-优化SQL使用索引5.png) + +* 在索引列上**函数或者运算(+ - 数值)操作**, 索引将失效:会破坏索引值的有序性 + + ```mysql + EXPLAIN SELECT * FROM tb_seller WHERE SUBSTRING(name,3,2) = '科技'; + ``` + + ![](https://seazean.oss-cn-beijing.aliyuncs.com/img/DB/MySQL-优化SQL使用索引6.png) + +* **字符串不加单引号**,造成索引失效:隐式类型转换,当字符串和数字比较时会**把字符串转化为数字** + + 没有对字符串加单引号,查询优化器会调用 CAST 函数将 status 转换为 int 进行比较,造成索引失效 + + ```mysql + EXPLAIN SELECT * FROM tb_seller WHERE name='小米科技' AND status = 1; + ``` + + ![](https://seazean.oss-cn-beijing.aliyuncs.com/img/DB/MySQL-优化SQL使用索引7.png) + + 如果 status 是 int 类型,SQL 为 `SELECT * FROM tb_seller WHERE status = '1' ` 并不会造成索引失效,因为会将 `'1'` 转换为 `1`,并**不会对索引列产生操作** + +* 多表连接查询时,如果两张表的**字符集不同**,会造成索引失效,因为会进行类型转换 + + 解决方法:CONVERT 函数是加在输入参数上、修改表的字符集 + +* **用 OR 分割条件,索引失效**,导致全表查询: + + OR 前的条件中的列有索引而后面的列中没有索引或 OR 前后两个列是同一个复合索引,都造成索引失效 + + ```mysql + EXPLAIN SELECT * FROM tb_seller WHERE name='阿里巴巴' OR createtime = '2088-01-01 12:00:00'; + EXPLAIN SELECT * FROM tb_seller WHERE name='小米科技' OR status='1'; + ``` + + ![](https://seazean.oss-cn-beijing.aliyuncs.com/img/DB/MySQL-优化SQL使用索引10.png) + + **AND 分割的条件不影响**: + + ```mysql + EXPLAIN SELECT * FROM tb_seller WHERE name='阿里巴巴' AND createtime = '2088-01-01 12:00:00'; + ``` + + ![](https://seazean.oss-cn-beijing.aliyuncs.com/img/DB/MySQL-优化SQL使用索引11.png) + +* **以 % 开头的 LIKE 模糊查询**,索引失效: + + 如果是尾部模糊匹配,索引不会失效;如果是头部模糊匹配,索引失效 + + ```mysql + EXPLAIN SELECT * FROM tb_seller WHERE name like '%科技%'; + ``` + + ![](https://seazean.oss-cn-beijing.aliyuncs.com/img/DB/MySQL-优化SQL使用索引12.png) + + 解决方案:通过覆盖索引来解决 + + ```mysql + EXPLAIN SELECT sellerid,name,status FROM tb_seller WHERE name like '%科技%'; + ``` + + ![](https://seazean.oss-cn-beijing.aliyuncs.com/img/DB/MySQL-优化SQL使用索引13.png) + + 原因:在覆盖索引的这棵 B+ 数上只需要进行 like 的匹配,或者是基于覆盖索引查询再进行 WHERE 的判断就可以获得结果 + + + +*** + + + +##### 系统优化 + +系统优化为全表扫描: + +* 如果 MySQL 评估使用索引比全表更慢,则不使用索引,索引失效: + + ```mysql + CREATE INDEX idx_address ON tb_seller(address); + EXPLAIN SELECT * FROM tb_seller WHERE address='西安市'; + EXPLAIN SELECT * FROM tb_seller WHERE address='北京市'; + ``` + + 北京市的键值占 9/10(区分度低),所以优化为全表扫描,type = ALL + + ![](https://seazean.oss-cn-beijing.aliyuncs.com/img/DB/MySQL-优化SQL使用索引14.png) + +* IS NULL、IS NOT NULL **有时**索引失效: + + ```mysql + EXPLAIN SELECT * FROM tb_seller WHERE name IS NULL; + EXPLAIN SELECT * FROM tb_seller WHERE name IS NOT NULL; + ``` + + NOT NULL 失效的原因是 name 列全部不是 null,优化为全表扫描,当 NULL 过多时,IS NULL 失效 + + ![](https://seazean.oss-cn-beijing.aliyuncs.com/img/DB/MySQL-优化SQL使用索引15.png) + +* IN 肯定会走索引,但是当 IN 的取值范围较大时会导致索引失效,走全表扫描: + + ```mysql + EXPLAIN SELECT * FROM tb_seller WHERE sellerId IN ('alibaba','huawei');-- 都走索引 + EXPLAIN SELECT * FROM tb_seller WHERE sellerId NOT IN ('alibaba','huawei'); + ``` + +* [MySQL 实战 45 讲](https://time.geekbang.org/column/article/74687)该章节最后提出了一种慢查询场景,获取到数据以后 Server 层还会做判断 + + + +*** + + + +#### 底层原理 + +索引失效一般是针对联合索引,联合索引一般由几个字段组成,排序方式是先按照第一个字段进行排序,然后排序第二个,依此类推,图示(a, b)索引,**a 相等的情况下 b 是有序的** + + + +* 最左前缀法则:当不匹配前面的字段的时候,后面的字段都是无序的。这种无序不仅体现在叶子节点,也会**导致查询时扫描的非叶子节点也是无序的**,因为索引树相当于忽略的第一个字段,就无法使用二分查找 + +* 范围查询右边的列,不能使用索引,比如语句: `WHERE a > 1 AND b = 1 `,在 a 大于 1 的时候,b 是无序的,a > 1 是扫描时有序的,但是找到以后进行寻找 b 时,索引树就不是有序的了 + + + +* 以 % 开头的 LIKE 模糊查询,索引失效,比如语句:`WHERE a LIKE '%d'`,前面的不确定,导致不符合最左匹配,直接去索引中搜索以 d 结尾的节点,所以没有顺序 + ![](https://seazean.oss-cn-beijing.aliyuncs.com/img/DB/MySQL-索引失效底层原理3.png) + + + +参考文章:https://mp.weixin.qq.com/s/B_M09dzLe9w7cT46rdGIeQ + + + +*** + + + +#### 查看索引 + +```mysql +SHOW STATUS LIKE 'Handler_read%'; +SHOW GLOBAL STATUS LIKE 'Handler_read%'; +``` + +![](https://seazean.oss-cn-beijing.aliyuncs.com/img/DB/MySQL-优化SQL查看索引使用情况.png) + +* Handler_read_first:索引中第一条被读的次数,如果较高,表示服务器正执行大量全索引扫描(这个值越低越好) + +* Handler_read_key:如果索引正在工作,这个值代表一个行被索引值读的次数,值越低表示索引不经常使用(这个值越高越好) + +* Handler_read_next:按照键顺序读下一行的请求数,如果范围约束或执行索引扫描来查询索引列,值增加 + +* Handler_read_prev:按照键顺序读前一行的请求数,该读方法主要用于优化 ORDER BY ... DESC + +* Handler_read_rnd:根据固定位置读一行的请求数,如果执行大量查询并对结果进行排序则该值较高,可能是使用了大量需要 MySQL 扫描整个表的查询或连接,这个值较高意味着运行效率低,应该建立索引来解决 + +* Handler_read_rnd_next:在数据文件中读下一行的请求数,如果正进行大量的表扫描,该值较高,说明表索引不正确或写入的查询没有利用索引 + + + + + +*** + + + +### SQL 优化 + +#### 自增主键 + +##### 自增机制 + +自增主键可以让主键索引尽量地保持在数据页中递增顺序插入,不自增需要寻找其他页插入,导致随机 IO 和页分裂的情况 + +表的结构定义存放在后缀名为.frm 的文件中,但是并不会保存自增值,不同的引擎对于自增值的保存策略不同: + +* MyISAM 引擎的自增值保存在数据文件中 +* InnoDB 引擎的自增值保存在了内存里,每次打开表都会去找自增值的最大值 max(id),然后将 max(id)+1 作为当前的自增值;8.0 版本后,才有了自增值持久化的能力,将自增值的变更记录在了 redo log 中,重启的时候依靠 redo log 恢复重启之前的值 + +在插入一行数据的时候,自增值的行为如下: + +* 如果插入数据时 id 字段指定为 0、null 或未指定值,那么就把这个表当前的 AUTO_INCREMENT 值填到自增字段 +* 如果插入数据时 id 字段指定了具体的值,比如某次要插入的值是 X,当前的自增值是 Y + * 如果 X 优化为: + SELECT id,name,statu FROM tb_book; + ``` + +* 插入数据: + + ```mysql + INSERT INTO tb_test VALUES(1,'Tom'); + INSERT INTO tb_test VALUES(2,'Cat'); + INSERT INTO tb_test VALUES(3,'Jerry'); -- 连接三次数据库 + -- >优化为 + INSERT INTO tb_test VALUES(1,'Tom'),(2,'Cat'),(3,'Jerry'); -- 连接一次 + ``` + +* 在事务中进行数据插入: + + ```mysql + start transaction; + INSERT INTO tb_test VALUES(1,'Tom'); + INSERT INTO tb_test VALUES(2,'Cat'); + INSERT INTO tb_test VALUES(3,'Jerry'); + commit; -- 手动提交,分段提交 + ``` + +* 数据有序插入: + + ```mysql + INSERT INTO tb_test VALUES(1,'Tom'); + INSERT INTO tb_test VALUES(2,'Cat'); + INSERT INTO tb_test VALUES(3,'Jerry'); + ``` + +增加 cache 层:在应用中增加缓存层来达到减轻数据库负担的目的。可以部分数据从数据库中抽取出来放到应用端以文本方式存储,或者使用框架(Mybatis)提供的一级缓存 / 二级缓存,或者使用 Redis 数据库来缓存数据 + + + +*** + + + +#### 数据插入 + +当使用 load 命令导入数据的时候,适当的设置可以提高导入的效率: + +![](https://seazean.oss-cn-beijing.aliyuncs.com/img/DB/MySQL-优化SQL load data.png) + +```mysql +LOAD DATA LOCAL INFILE = '/home/seazean/sql1.log' INTO TABLE `tb_user_1` FIELD TERMINATED BY ',' LINES TERMINATED BY '\n'; -- 文件格式如上图 +``` + +对于 InnoDB 类型的表,有以下几种方式可以提高导入的效率: + +1. **主键顺序插入**:因为 InnoDB 类型的表是按照主键的顺序保存的,所以将导入的数据按照主键的顺序排列,可以有效的提高导入数据的效率,如果 InnoDB 表没有主键,那么系统会自动默认创建一个内部列作为主键 + + 主键是否连续对性能影响不大,只要是递增的就可以,比如雪花算法产生的 ID 不是连续的,但是是递增的,因为递增可以让主键索引尽量地保持顺序插入,**避免了页分裂**,因此索引更紧凑 + + * 插入 ID 顺序排列数据: + + ![](https://seazean.oss-cn-beijing.aliyuncs.com/img/DB/MySQL-优化SQL插入ID顺序排列数据.png) + + * 插入 ID 无序排列数据: + + ![](https://seazean.oss-cn-beijing.aliyuncs.com/img/DB/MySQL-优化SQL插入ID无序排列数据.png) + +2. **关闭唯一性校验**:在导入数据前执行 `SET UNIQUE_CHECKS=0`,关闭唯一性校验;导入结束后执行 `SET UNIQUE_CHECKS=1`,恢复唯一性校验,可以提高导入的效率。 + + ![](https://seazean.oss-cn-beijing.aliyuncs.com/img/DB/MySQL-优化SQL插入数据关闭唯一性校验.png) + +3. **手动提交事务**:如果应用使用自动提交的方式,建议在导入前执行`SET AUTOCOMMIT=0`,关闭自动提交;导入结束后再打开自动提交,可以提高导入的效率。 + + 事务需要控制大小,事务太大可能会影响执行的效率。MySQL 有 innodb_log_buffer_size 配置项,超过这个值的日志会写入磁盘数据,效率会下降,所以在事务大小达到配置项数据级前进行事务提交可以提高效率 + + ![](https://seazean.oss-cn-beijing.aliyuncs.com/img/DB/MySQL-优化SQL插入数据手动提交事务.png) + + + +**** + + + +#### 分组排序 + +##### ORDER + +数据准备: + +```mysql +CREATE TABLE `emp` ( + `id` INT(11) NOT NULL AUTO_INCREMENT, + `name` VARCHAR(100) NOT NULL, + `age` INT(3) NOT NULL, + `salary` INT(11) DEFAULT NULL, + PRIMARY KEY (`id`) +) ENGINE=INNODB DEFAULT CHARSET=utf8mb4; +INSERT INTO `emp` (`id`, `name`, `age`, `salary`) VALUES('1','Tom','25','2300');-- ... +CREATE INDEX idx_emp_age_salary ON emp(age, salary); +``` + +* 第一种是通过对返回数据进行排序,所有不通过索引直接返回结果的排序都叫 FileSort 排序,会在内存中重新排序 + + ```mysql + EXPLAIN SELECT * FROM emp ORDER BY age DESC; -- 年龄降序 + ``` + + ![](https://seazean.oss-cn-beijing.aliyuncs.com/img/DB/MySQL-优化SQL ORDER BY排序1.png) + +* 第二种通过有序索引顺序扫描直接返回**有序数据**,这种情况为 Using index,不需要额外排序,操作效率高 + + ```mysql + EXPLAIN SELECT id, age, salary FROM emp ORDER BY age DESC; + ``` + + ![](https://seazean.oss-cn-beijing.aliyuncs.com/img/DB/MySQL-优化SQL ORDER BY排序2.png) + +* 多字段排序: + + ```mysql + EXPLAIN SELECT id,age,salary FROM emp ORDER BY age DESC, salary DESC; + EXPLAIN SELECT id,age,salary FROM emp ORDER BY salary DESC, age DESC; + EXPLAIN SELECT id,age,salary FROM emp ORDER BY age DESC, salary ASC; + ``` + + ![](https://seazean.oss-cn-beijing.aliyuncs.com/img/DB/MySQL-优化SQL ORDER BY排序3.png) + + 尽量减少额外的排序,通过索引直接返回有序数据。**需要满足 Order by 使用相同的索引、Order By 的顺序和索引顺序相同、Order by 的字段都是升序或都是降序**,否则需要额外的操作,就会出现 FileSort + +* ORDER BY RAND() 命令用来进行随机排序,会使用了临时内存表,临时内存表排序的时使用 rowid 排序方法 + +优化方式:创建合适的索引能够减少 Filesort 的出现,但是某些情况下条件限制不能让 Filesort 消失,就要加快 Filesort 的排序操作 + +内存临时表,MySQL 有两种 Filesort 排序算法: + +* rowid 排序:首先根据条件取出排序字段和信息,然后在**排序区 sort buffer(Server 层)**中排序,如果 sort buffer 不够,则在临时表 temporary table 中存储排序结果。完成排序后再根据行指针**回表读取记录**,该操作可能会导致大量随机 I/O 操作 + + 说明:对于临时内存表,回表过程只是简单地根据数据行的位置,直接访问内存得到数据,不会导致多访问磁盘,优先选择该方式 + +* 全字段排序:一次性取出满足条件的所有数据,需要回表,然后在排序区 sort buffer 中排序后直接输出结果集。排序时内存开销较大,但是排序效率比两次扫描算法高 + +具体的选择方式: + +* MySQL 通过比较系统变量 max_length_for_sort_data 的大小和 Query 语句取出的字段的大小,来判定使用哪种排序算法。如果前者大,则说明 sort buffer 空间足够,使用第二种优化之后的算法,否则使用第一种。 + +* 可以适当提高 sort_buffer_size 和 max_length_for_sort_data 系统变量,来增大排序区的大小,提高排序的效率 + + ```mysql + SET @@max_length_for_sort_data = 10000; -- 设置全局变量 + SET max_length_for_sort_data = 10240; -- 设置会话变量 + SHOW VARIABLES LIKE 'max_length_for_sort_data'; -- 默认1024 + SHOW VARIABLES LIKE 'sort_buffer_size'; -- 默认262114 + ``` + +磁盘临时表:排序使用优先队列(堆)的方式 + + + +*** + + + +##### GROUP + +GROUP BY 也会进行排序操作,与 ORDER BY 相比,GROUP BY 主要只是多了排序之后的分组操作,所以在 GROUP BY 的实现过程中,与 ORDER BY 一样也可以利用到索引 + +* 分组查询: + + ```mysql + DROP INDEX idx_emp_age_salary ON emp; + EXPLAIN SELECT age,COUNT(*) FROM emp GROUP BY age; + ``` + + ![](https://seazean.oss-cn-beijing.aliyuncs.com/img/DB/MySQL-优化SQL GROUP BY排序1.png) + + Using temporary:表示 MySQL 需要使用临时表(不是 sort buffer)来存储结果集,常见于排序和分组查询 + +* 查询包含 GROUP BY 但是用户想要避免排序结果的消耗, 则可以执行 ORDER BY NULL 禁止排序: + + ```mysql + EXPLAIN SELECT age,COUNT(*) FROM emp GROUP BY age ORDER BY NULL; + ``` + + ![](https://seazean.oss-cn-beijing.aliyuncs.com/img/DB/MySQL-优化SQL GROUP BY排序2.png) + +* 创建索引:索引本身有序,不需要临时表,也不需要再额外排序 + + ```mysql + CREATE INDEX idx_emp_age_salary ON emp(age, salary); + ``` + + ![](https://seazean.oss-cn-beijing.aliyuncs.com/img/DB/MySQL-优化SQL GROUP BY排序3.png) + +* 数据量很大时,使用 SQL_BIG_RESULT 提示优化器直接使用直接用磁盘临时表 + + + +*** + + + +#### 联合查询 + +对于包含 OR 的查询子句,如果要利用索引,则 OR 之间的**每个条件列都必须用到索引,而且不能使用到条件之间的复合索引**,如果没有索引,则应该考虑增加索引 + +* 执行查询语句: + + ```mysql + EXPLAIN SELECT * FROM emp WHERE id = 1 OR age = 30; -- 两个索引,并且不是复合索引 + ``` + + ![](https://seazean.oss-cn-beijing.aliyuncs.com/img/DB/MySQL-优化SQL OR条件查询1.png) + + ```sh + Extra: Using sort_union(idx_emp_age_salary,PRIMARY); Using where + ``` + +* 使用 UNION 替换 OR,求并集: + + 注意:该优化只针对多个索引列有效,如果有列没有被索引,查询效率可能会因为没有选择 OR 而降低 + + ```mysql + EXPLAIN SELECT * FROM emp WHERE id = 1 UNION SELECT * FROM emp WHERE age = 30; + ``` + + ![](https://seazean.oss-cn-beijing.aliyuncs.com/img/DB/MySQL-优化SQL OR条件查询2.png) + +* UNION 要优于 OR 的原因: + + * UNION 语句的 type 值为 ref,OR 语句的 type 值为 range + * UNION 语句的 ref 值为 const,OR 语句的 ref 值为 null,const 表示是常量值引用,非常快 + + + +**** + + + +#### 嵌套查询 + +MySQL 4.1 版本之后,开始支持 SQL 的子查询 + +* 可以使用 SELECT 语句来创建一个单列的查询结果,然后把结果作为过滤条件用在另一个查询中 +* 使用子查询可以一次性的完成逻辑上需要多个步骤才能完成的 SQL 操作,同时也可以避免事务或者表锁死 +* 在有些情况下,**子查询是可以被更高效的连接(JOIN)替代** + +例如查找有角色的所有的用户信息: + +* 执行计划: + + ```mysql + EXPLAIN SELECT * FROM t_user WHERE id IN (SELECT user_id FROM user_role); + ``` + + ![](https://seazean.oss-cn-beijing.aliyuncs.com/img/DB/MySQL-优化SQL嵌套查询1.png) + +* 优化后: + + ```mysql + EXPLAIN SELECT * FROM t_user u , user_role ur WHERE u.id = ur.user_id; + ``` + + ![](https://seazean.oss-cn-beijing.aliyuncs.com/img/DB/MySQL-优化SQL嵌套查询2.png) + + 连接查询之所以效率更高 ,是因为**不需要在内存中创建临时表**来完成逻辑上需要两个步骤的查询工作 + + + + + +*** + + + +#### 分页查询 + +一般分页查询时,通过创建覆盖索引能够比较好地提高性能 + +一个常见的问题是 `LIMIT 200000,10`,此时需要 MySQL 扫描前 200010 记录,仅仅返回 200000 - 200010 之间的记录,其他记录丢弃,查询排序的代价非常大 + +* 分页查询: + + ```mysql + EXPLAIN SELECT * FROM tb_user_1 LIMIT 200000,10; + ``` + + ![](https://seazean.oss-cn-beijing.aliyuncs.com/img/DB/MySQL-优化SQL分页查询1.png) + +* 优化方式一:内连接查询,在索引列 id 上完成排序分页操作,最后根据主键关联回原表查询所需要的其他列内容 + + ```mysql + EXPLAIN SELECT * FROM tb_user_1 t,(SELECT id FROM tb_user_1 ORDER BY id LIMIT 200000,10) a WHERE t.id = a.id; + ``` + + ![](https://seazean.oss-cn-beijing.aliyuncs.com/img/DB/MySQL-优化SQL分页查询2.png) + +* 优化方式二:方案适用于主键自增的表,可以把 LIMIT 查询转换成某个位置的查询 + + ```mysql + EXPLAIN SELECT * FROM tb_user_1 WHERE id > 200000 LIMIT 10; -- 写法 1 + EXPLAIN SELECT * FROM tb_user_1 WHERE id BETWEEN 200000 and 200010; -- 写法 2 + ``` + + ![](https://seazean.oss-cn-beijing.aliyuncs.com/img/DB/MySQL-优化SQL分页查询3.png) + + + +**** + + + +#### 使用提示 + +SQL 提示,是优化数据库的一个重要手段,就是在 SQL 语句中加入一些提示来达到优化操作的目的 + +* USE INDEX:在查询语句中表名的后面添加 USE INDEX 来提供 MySQL 去参考的索引列表,可以让 MySQL 不再考虑其他可用的索引 + + ```mysql + CREATE INDEX idx_seller_name ON tb_seller(name); + EXPLAIN SELECT * FROM tb_seller USE INDEX(idx_seller_name) WHERE name='小米科技'; + ``` + + ![](https://seazean.oss-cn-beijing.aliyuncs.com/img/DB/MySQL-优化SQL使用提示1.png) + +* IGNORE INDEX:让 MySQL 忽略一个或者多个索引,则可以使用 IGNORE INDEX 作为提示 + + ```mysql + EXPLAIN SELECT * FROM tb_seller IGNORE INDEX(idx_seller_name) WHERE name = '小米科技'; + ``` + + ![](https://seazean.oss-cn-beijing.aliyuncs.com/img/DB/MySQL-优化SQL使用提示2.png) + +* FORCE INDEX:强制 MySQL 使用一个特定的索引 + + ```mysql + EXPLAIN SELECT * FROM tb_seller FORCE INDEX(idx_seller_name_sta_addr) WHERE NAME='小米科技'; + ``` + + ![](https://seazean.oss-cn-beijing.aliyuncs.com/img/DB/MySQL-优化SQL使用提示3.png) + + + + + +*** + + + +#### 统计计数 + +在不同的 MySQL 引擎中,count(*) 有不同的实现方式: + +* MyISAM 引擎把一个表的总行数存在了磁盘上,因此执行 count(*) 的时候会直接返回这个数,效率很高,但不支持事务 +* show table status 命令通过采样估算可以快速获取,但是不准确 +* InnoDB 表执行 count(*) 会遍历全表,虽然结果准确,但会导致性能问题 + +解决方案: + +* 计数保存在 Redis 中,但是更新 MySQL 和 Redis 的操作不是原子的,会存在数据一致性的问题 + +* 计数直接放到数据库里单独的一张计数表中,利用事务解决计数精确问题: + + + + 会话 B 的读操作在 T3 执行的,这时更新事务还没有提交,所以计数值加 1 这个操作对会话 B 还不可见,因此会话 B 查询的计数值和最近 100 条记录,返回的结果逻辑上就是一致的 + + 并发系统性能的角度考虑,应该先插入操作记录再更新计数表,因为更新计数表涉及到行锁的竞争,**先插入再更新能最大程度地减少事务之间的锁等待,提升并发度** + +count 函数的按照效率排序:`count(字段) < count(主键id) < count(1) ≈ count(*)`,所以建议尽量使用 count(*) + +* count(主键 id):InnoDB 引擎会遍历整张表,把每一行的 id 值都取出来返回给 Server 层,Server 判断 id 不为空就按行累加 +* count(1):InnoDB 引擎遍历整张表但不取值,Server 层对于返回的每一行,放一个数字 1 进去,判断不为空就按行累加 +* count(字段):如果这个字段是定义为 not null 的话,一行行地从记录里面读出这个字段,判断不能为 null,按行累加;如果这个字段定义允许为 null,那么执行的时候,判断到有可能是 null,还要把值取出来再判断一下,不是 null 才累加 +* count(*):不取值,按行累加 + + + +参考文章:https://time.geekbang.org/column/article/72775 + + + + + +*** + + + +### 缓冲优化 + +#### 优化原则 + +三个原则: + +* 将尽量多的内存分配给 MySQL 做缓存,但也要给操作系统和其他程序预留足够内存 +* MyISAM 存储引擎的数据文件读取依赖于操作系统自身的 IO 缓存,如果有 MyISAM 表,就要预留更多的内存给操作系统做 IO 缓存 +* 排序区、连接区等缓存是分配给每个数据库会话(Session)专用的,值的设置要根据最大连接数合理分配,如果设置太大,不但浪费资源,而且在并发数较高时会导致物理内存耗尽 + + + +*** + + + +#### 缓冲内存 + +Buffer Pool 本质上是 InnoDB 向操作系统申请的一段连续的内存空间。InnoDB 的数据是按数据页为单位来读写,每个数据页的大小默认是 16KB。数据是存放在磁盘中,每次读写数据都需要进行磁盘 IO 将数据读入内存进行操作,效率会很低,所以提供了 Buffer Pool 来暂存这些数据页,缓存中的这些页又叫缓冲页 + +工作原理: + +* 从数据库读取数据时,会首先从缓存中读取,如果缓存中没有,则从磁盘读取后放入 Buffer Pool +* 向数据库写入数据时,会写入缓存,缓存中修改的数据会**定期刷新**到磁盘,这一过程称为刷脏 + +Buffer Pool 中每个缓冲页都有对应的控制信息,包括表空间编号、页号、偏移量、链表信息等,控制信息存放在占用的内存称为控制块,控制块与缓冲页是一一对应的,但并不是物理上相连的,都在缓冲池中 + +MySQL 提供了缓冲页的快速查找方式:**哈希表**,使用表空间号和页号作为 Key,缓冲页控制块的地址作为 Value 创建一个哈希表,获取数据页时根据 Key 进行哈希寻址: + +* 如果不存在对应的缓存页,就从 free 链表中选一个空闲缓冲页,把磁盘中的对应页加载到该位置 +* 如果存在对应的缓存页,直接获取使用,提高查询数据的效率 + +当内存数据页跟磁盘数据页内容不一致时,称这个内存页为脏页;内存数据写入磁盘后,内存和磁盘上的数据页一致,称为干净页 + + + +*** + + + +#### 内存管理 + +##### Free 链表 + +MySQL 启动时完成对 Buffer Pool 的初始化,先向操作系统申请连续的内存空间,然后将内存划分为若干对控制块和缓冲页。为了区分空闲和已占用的数据页,将所有空闲缓冲页对应的**控制块作为一个节点**放入一个链表中,就是 Free 链表(**空闲链表**) + + + +基节点:是一块单独申请的内存空间(占 40 字节),并不在 Buffer Pool 的那一大片连续内存空间里 + +磁盘加载页的流程: + +* 从 Free 链表中取出一个空闲的缓冲页 +* 把缓冲页对应的控制块的信息填上(页所在的表空间、页号之类的信息) +* 把缓冲页对应的 Free 链表节点(控制块)从链表中移除,表示该缓冲页已经被使用 + + + +参考文章:https://blog.csdn.net/li1325169021/article/details/121124440 + + + +**** + + + +##### Flush 链表 + +Flush 链表是一个用来**存储脏页**的链表,对于已经修改过的缓冲脏页,第一次修改后加入到**链表头部**,以后每次修改都不会重新加入,只修改部分控制信息,出于性能考虑并不是直接更新到磁盘,而是在未来的某个时间进行刷脏 + + + +**后台有专门的线程每隔一段时间把脏页刷新到磁盘**: + +* 从 Flush 链表中刷新一部分页面到磁盘: + * **后台线程定时**从 Flush 链表刷脏,根据系统的繁忙程度来决定刷新速率,这种方式称为 BUF_FLUSH_LIST + * 线程刷脏的比较慢,导致用户线程加载一个新的数据页时发现没有空闲缓冲页,此时会尝试从 LRU 链表尾部寻找缓冲页直接释放,如果该页面是已经修改过的脏页就**同步刷新**到磁盘,速度较慢,这种方式称为 BUF_FLUSH_SINGLE_PAGE +* 从 LRU 链表的冷数据中刷新一部分页面到磁盘,即:BUF_FLUSH_LRU + * 后台线程会定时从 LRU 链表的尾部开始扫描一些页面,扫描的页面数量可以通过系统变量 `innodb_lru_scan_depth` 指定,如果在 LRU 链表中发现脏页,则把它们刷新到磁盘,这种方式称为 BUF_FLUSH_LRU + * 控制块里会存储该缓冲页是否被修改的信息,所以可以很容易的获取到某个缓冲页是否是脏页 + + + +参考文章:https://blog.csdn.net/li1325169021/article/details/121125765 + + + +*** + + + +##### LRU 链表 + +Buffer Pool 需要保证缓存的命中率,所以 MySQL 创建了一个 LRU 链表,当访问某个页时: + +* 如果该页不在 Buffer Pool 中,把该页从磁盘加载进来后会将该缓冲页对应的控制块作为节点放入 **LRU 链表的头部**,保证热点数据在链表头 +* 如果该页在 Buffer Pool 中,则直接把该页对应的控制块移动到 LRU 链表的头部,所以 LRU 链表尾部就是最近最少使用的缓冲页 + +MySQL 基于局部性原理提供了预读功能: + +* 线性预读:系统变量 `innodb_read_ahead_threshold`,如果顺序访问某个区(extent:16 KB 的页,连续 64 个形成一个区,一个区默认 1MB 大小)的页面数超过了该系统变量值,就会触发一次**异步读取**下一个区中全部的页面到 Buffer Pool 中 +* 随机预读:如果某个区 13 个连续的页面都被加载到 Buffer Pool,无论这些页面是否是顺序读取,都会触发一次**异步读取**本区所有的其他页面到 Buffer Pool 中 + +预读会造成加载太多用不到的数据页,造成那些使用频率很高的数据页被挤到 LRU 链表尾部,所以 InnoDB 将 LRU 链表分成两段,**冷热数据隔离**: + +* 一部分存储使用频率很高的数据页,这部分链表也叫热数据,young 区,靠近链表头部的区域 +* 一部分存储使用频率不高的冷数据,old 区,靠近链表尾部,默认占 37%,可以通过系统变量 `innodb_old_blocks_pct` 指定 + +当磁盘上的某数据页被初次加载到 Buffer Pool 中会被放入 old 区,淘汰时优先淘汰 old 区 + +* 当对 old 区的数据进行访问时,会在控制块记录下访问时间,等待后续的访问时间与第一次访问的时间是否在某个时间间隔内,通过系统变量 `innodb_old_blocks_time` 指定时间间隔,默认 1000ms,成立就**移动到 young 区的链表头部** +* `innodb_old_blocks_time` 为 0 时,每次访问一个页面都会放入 young 区的头部 + + + +*** + + + +#### 参数优化 + +InnoDB 用一块内存区做 IO 缓存池,该缓存池不仅用来缓存 InnoDB 的索引块,也用来缓存 InnoDB 的数据块,可以通过下面的指令查看 Buffer Pool 的状态信息: + +```mysql +SHOW ENGINE INNODB STATUS\G +``` + +`Buffer pool hit rate` 字段代表**内存命中率**,表示 Buffer Pool 对查询的加速效果 + +核心参数: + +* `innodb_buffer_pool_size`:该变量决定了 Innodb 存储引擎表数据和索引数据的最大缓存区大小,默认 128M + + ```mysql + SHOW VARIABLES LIKE 'innodb_buffer_pool_size'; + ``` + + 在保证操作系统及其他程序有足够内存可用的情况下,`innodb_buffer_pool_size` 的值越大,缓存命中率越高,建议设置成可用物理内存的 60%~80% + + ```sh + innodb_buffer_pool_size=512M + ``` + +* `innodb_log_buffer_size`:该值决定了 Innodb 日志缓冲区的大小,保存要写入磁盘上的日志文件数据 + + 对于可能产生大量更新记录的大事务,增加该值的大小,可以避免 Innodb 在事务提交前就执行不必要的日志写入磁盘操作,影响执行效率,通过配置文件修改: + + ```sh + innodb_log_buffer_size=10M + ``` + +在多线程下,访问 Buffer Pool 中的各种链表都需要加锁,所以将 Buffer Pool 拆成若干个小实例,**每个线程对应一个实例**,独立管理内存空间和各种链表(类似 ThreadLocal),多线程访问各自实例互不影响,提高了并发能力 + +MySQL 5.7.5 之前 `innodb_buffer_pool_size` 只支持在系统启动时修改,现在已经支持运行时修改 Buffer Pool 的大小,但是每次调整参数都会重新向操作系统申请一块连续的内存空间,**将旧的缓冲池的内容拷贝到新空间**非常耗时,所以 MySQL 开始以一个 chunk 为单位向操作系统申请内存,所以一个 Buffer Pool 实例可以由多个 chunk 组成 + +* 在系统启动时设置系统变量 `innodb_buffer_pool_instance` 可以指定 Buffer Pool 实例的个数,但是当 Buffer Pool 小于 1GB 时,设置多个实例时无效的 +* 指定系统变量 `innodb_buffer_pool_chunk_size` 来改变 chunk 的大小,只能在启动时修改,运行中不能修改,而且该变量并不包含缓冲页的控制块的内存大小 +* `innodb_buffer_pool_size` 必须是 `innodb_buffer_pool_chunk_size × innodb_buffer_pool_instance` 的倍数,默认值是 `128M × 16 = 2G`,Buffer Pool 必须是 2G 的整数倍,如果指定 5G,会自动调整成 6G +* 如果启动时 `chunk × instances` > `pool_size`,那么 chunk 的值会自动设置为 `pool_size ÷ instances` + + + +*** + + + +### 内存优化 + +#### Change + +InnoDB 管理的 Buffer Pool 中有一块内存叫 Change Buffer 用来对**增删改操作**提供缓存,可以通过参数来动态设置,设置为 50 时表示 Change Buffer 的大小最多占用 Buffer Pool 的 50% + +* 唯一索引的更新不能使用 Change Buffer,需要将数据页读入内存,判断没有冲突在写入 +* 普通索引可以使用 Change Buffer,**直接写入 Buffer 就结束**,不用校验唯一性 + +Change Buffer 并不是数据页,只是对操作的缓存,所以需要将 Change Buffer 中的操作应用到旧数据页,得到新的数据页(脏页)的过程称为 Merge + +* 触发时机:访问数据页时会触发 Merge、后台有定时线程进行 Merge、在数据库正常关闭(shutdown)的过程中也会触发 +* 工作流程:首先从磁盘读入数据页到内存(因为 Buffer Pool 中不一定存在对应的数据页),从 Change Buffer 中找到对应的操作应用到数据页,得到新的数据页即为脏页,然后写入 redo log,等待刷脏即可 + +说明:Change Buffer 中的记录,在事务提交时也会写入 redo log,所以是可以保证不丢失的 + +业务场景: + +* 对于**写多读少**的业务来说,页面在写完以后马上被访问到的概率比较小,此时 Change Buffer 的使用效果最好,常见的就是账单类、日志类的系统 + +* 一个业务的更新模式是写入后马上做查询,那么即使满足了条件,将更新先记录在 Change Buffer,但之后由于马上要访问这个数据页,会立即触发 Merge 过程,这样随机访问 IO 的次数不会减少,并且增加了 Change Buffer 的维护代价 + +补充:Change Buffer 的前身是 Insert Buffer,只能对 Insert 操作优化,后来增加了 Update/Delete 的支持,改为 Change Buffer + + + +*** + + + +#### Net + +Server 层针对优化**查询**的内存为 Net Buffer,内存的大小是由参数 `net_buffer_length`定义,默认 16k,实现流程: + +* 获取一行数据写入 Net Buffer,重复获取直到 Net Buffer 写满,调用网络接口发出去 +* 若发送成功就清空 Net Buffer,然后继续取下一行;若发送函数返回 EAGAIN 或 WSAEWOULDBLOCK,表示本地网络栈 `socket send buffer` 写满了,**进入等待**,直到网络栈重新可写再继续发送 + +MySQL 采用的是边读边发的逻辑,因此对于数据量很大的查询来说,不会在 Server 端保存完整的结果集,如果客户端读结果不及时,会堵住 MySQL 的查询过程,但是**不会把内存打爆导致 OOM** + + + +SHOW PROCESSLIST 获取线程信息后,处于 Sending to client 状态代表服务器端的网络栈写满,等待客户端接收数据 + +假设有一个业务的逻辑比较复杂,每读一行数据以后要处理很久的逻辑,就会导致客户端要过很久才会去取下一行数据,导致 MySQL 的阻塞,一直处于 Sending to client 的状态 + +解决方法:如果一个查询的返回结果很是很多,建议使用 mysql_store_result 这个接口,直接把查询结果保存到本地内存 + + + +参考文章:https://blog.csdn.net/qq_33589510/article/details/117673449 + + + +*** + + + +#### Read + +read_rnd_buffer 是 MySQL 的随机读缓冲区,当按任意顺序读取记录行时将分配一个随机读取缓冲区,进行排序查询时,MySQL 会首先扫描一遍该缓冲,以避免磁盘搜索,提高查询速度,大小是由 read_rnd_buffer_size 参数控制的 + +Multi-Range Read 优化,**将随机 IO 转化为顺序 IO** 以降低查询过程中 IO 开销,因为大多数的数据都是按照主键递增顺序插入得到,所以按照主键的递增顺序查询的话,对磁盘的读比较接近顺序读,能够提升读性能 + +二级索引为 a,聚簇索引为 id,优化回表流程: + +* 根据索引 a,定位到满足条件的记录,将 id 值放入 read_rnd_buffer 中 +* 将 read_rnd_buffer 中的 id 进行**递增排序** +* 排序后的 id 数组,依次回表到主键 id 索引中查记录,并作为结果返回 + +说明:如果步骤 1 中 read_rnd_buffer 放满了,就会先执行步骤 2 和 3,然后清空 read_rnd_buffer,之后继续找索引 a 的下个记录 + +使用 MRR 优化需要设进行设置: + +```mysql +SET optimizer_switch='mrr_cost_based=off' +``` + + + +*** + + + +#### Key + +MyISAM 存储引擎使用 key_buffer 缓存索引块,加速 MyISAM 索引的读写速度。对于 MyISAM 表的数据块没有特别的缓存机制,完全依赖于操作系统的 IO 缓存 + +* key_buffer_size:该变量决定 MyISAM 索引块缓存区的大小,直接影响到 MyISAM 表的存取效率 + + ```mysql + SHOW VARIABLES LIKE 'key_buffer_size'; -- 单位是字节 + ``` + + 在 MySQL 配置文件中设置该值,建议至少将1/4可用内存分配给 key_buffer_size: + + ```sh + vim /etc/mysql/my.cnf + key_buffer_size=1024M + ``` + +* read_buffer_size:如果需要经常顺序扫描 MyISAM 表,可以通过增大 read_buffer_size 的值来改善性能。但 read_buffer_size 是每个 Session 独占的,如果默认值设置太大,并发环境就会造成内存浪费 + +* read_rnd_buffer_size:对于需要做排序的 MyISAM 表的查询,如带有 ORDER BY 子句的语句,适当增加该的值,可以改善此类的 SQL 的性能,但是 read_rnd_buffer_size 是每个 Session 独占的,如果默认值设置太大,就会造成内存浪费 + + + + + +*** + + + + + +### 存储优化 + +#### 数据存储 + +系统表空间是用来放系统信息的,比如数据字典什么的,对应的磁盘文件是 ibdata,数据表空间是一个个的表数据文件,对应的磁盘文件就是表名.ibd + +表数据既可以存在共享表空间里,也可以是单独的文件,这个行为是由参数 innodb_file_per_table 控制的: + +* OFF:表示表的数据放在系统共享表空间,也就是跟数据字典放在一起 +* ON :表示每个 InnoDB 表数据存储在一个以 .ibd 为后缀的文件中(默认) + +一个表单独存储为一个文件更容易管理,在不需要这个表时通过 drop table 命令,系统就会直接删除这个文件;如果是放在共享表空间中,即使表删掉了,空间也是不会回收的 + + + + + +*** + + + +#### 数据删除 + +MySQL 的数据删除就是移除掉某个记录后,该位置就被标记为**可复用**,如果有符合范围条件的数据可以插入到这里。符合范围条件的意思是假设删除记录 R4,之后要再插入一个 ID 在 300 和 600 之间的记录时,就会复用这个位置 + + + +InnoDB 的数据是按页存储的如果删掉了一个数据页上的所有记录,整个数据页就可以被复用了,如果相邻的两个数据页利用率都很小,系统就会把这两个页上的数据合到其中一个页上,另外一个数据页就被标记为可复用 + +删除命令其实只是把记录的位置,或者**数据页标记为了可复用,但磁盘文件的大小是不会变的**,这些可以复用还没有被使用的空间,看起来就像是空洞,造成数据库的稀疏,因此需要进行紧凑处理 + + + +*** + + + +#### 重建数据 + +重建表就是按照主键 ID 递增的顺序,把数据一行一行地从旧表中读出来再插入到新表中,让数据更加紧凑。重建表时 MySQL 会自动完成转存数据、交换表名、删除旧表的操作,线上操作会阻塞大量的线程增删改查的操作 + +重建命令: + +```sql +ALTER TABLE A ENGINE=InnoDB +``` + +工作流程:新建临时表 tmp_table B(在 Server 层创建的),把表 A 中的数据导入到表 B 中,操作完成后用表 B 替换表 A,完成重建 + +重建表的步骤需要 DDL 不是 Online 的,因为在导入数据的过程有新的数据要写入到表 A 的话,就会造成数据丢失 + +MySQL 5.6 版本开始引入的 **Online DDL**,重建表的命令默认执行此步骤: + +* 建立一个临时文件 tmp_file(InnoDB 创建),扫描表 A 主键的所有数据页 +* 用数据页中表 A 的记录生成 B+ 树,存储到临时文件中 +* 生成临时文件的过程中,将所有对 A 的操作记录在一个日志文件(row log)中,对应的是图中 state2 的状态 +* 临时文件生成后,将日志文件中的操作应用到临时文件,得到一个逻辑数据上与表 A 相同的数据文件,对应的就是图中 state3 +* 用临时文件替换表 A 的数据文件 + + + +Online DDL 操作会先获取 MDL 写锁,再退化成 MDL 读锁。但 MDL 写锁持有时间比较短,所以可以称为 Online; 而 MDL 读锁,不阻止数据增删查改,但会阻止其它线程修改表结构(可以对比 `ANALYZE TABLE t` 命令) + +问题:重建表可以收缩表空间,但是执行指令后整体占用空间增大 + +原因:在重建表后 InnoDB 不会把整张表占满,每个页留了 1/16 给后续的更新使用。表在未整理之前页已经占用 15/16 以上,收缩之后需要保持数据占用空间在 15/16,所以文件占用空间更大才能保持 + +注意:临时文件也要占用空间,如果空间不足会重建失败 + + + +**** + + + +#### 原地置换 + +DDL 中的临时表 tmp_table 是在 Server 层创建的,Online DDL 中的临时文件 tmp_file 是 InnoDB 在内部创建出来的,整个 DDL 过程都在 InnoDB 内部完成,对于 Server 层来说,没有把数据挪动到临时表,是一个原地操作,这就是 inplace + +两者的关系: + +* DDL 过程如果是 Online 的,就一定是 inplace 的 +* inplace 的 DDL,有可能不是 Online 的,截止到 MySQL 8.0,全文索引(FULLTEXT)和空间索引(SPATIAL)属于这种情况 + + + + + +*** + + + +### 并发优化 + +MySQL Server 是多线程结构,包括后台线程和客户服务线程。多线程可以有效利用服务器资源,提高数据库的并发性能。在 MySQL 中,控制并发连接和线程的主要参数: + +* max_connections:控制允许连接到 MySQL 数据库的最大连接数,默认值是 151 + + 如果状态变量 connection_errors_max_connections 不为零,并且一直增长,则说明不断有连接请求因数据库连接数已达到允许最大值而失败,这时可以考虑增大 max_connections 的值 + + MySQL 最大可支持的连接数取决于很多因素,包括操作系统平台的线程库的质量、内存大小、每个连接的负荷、CPU的处理速度、期望的响应时间等。在 Linux 平台下,性能好的服务器,可以支持 500-1000 个连接,需要根据服务器性能进行评估设定 + +* innodb_thread_concurrency:并发线程数,代表系统内同时运行的线程数量(已经被移除) + +* back_log:控制 MySQL 监听 TCP 端口时的积压请求栈的大小 + + 如果 Mysql 的连接数达到 max_connections 时,新来的请求将会被存在堆栈中,以等待某一连接释放资源,该堆栈的数量即 back_log。如果等待连接的数量超过 back_log,将不被授予连接资源直接报错 + + 5.6.6 版本之前默认值为 50,之后的版本默认为 `50 + (max_connections/5)`,但最大不超过900,如果需要数据库在较短的时间内处理大量连接请求, 可以考虑适当增大 back_log 的值 + +* table_open_cache:控制所有 SQL 语句执行线程可打开表缓存的数量 + + 在执行 SQL 语句时,每个执行线程至少要打开1个表缓存,该参数的值应该根据设置的最大连接数以及每个连接执行关联查询中涉及的表的最大数量来设定:`max_connections * N` + +* thread_cache_size:可控制 MySQL 缓存客户服务线程的数量 + + 为了加快连接数据库的速度,MySQL 会缓存一定数量的客户服务线程以备重用,池化思想 + +* innodb_lock_wait_timeout:设置 InnoDB 事务等待行锁的时间,默认值是 50ms + + 对于需要快速反馈的业务系统,可以将行锁的等待时间调小,以避免事务被长时间挂起; 对于后台运行的批量处理程序来说,可以将行锁的等待时间调大,以避免发生大的回滚操作 + + + + + +*** + + + + + +## 事务机制 + +### 基本介绍 + +事务(Transaction)是访问和更新数据库的程序执行单元;事务中可能包含一个或多个 SQL 语句,这些语句要么都执行,要么都不执行,作为一个关系型数据库,MySQL 支持事务。 + +单元中的每条 SQL 语句都相互依赖,形成一个整体 + +* 如果某条 SQL 语句执行失败或者出现错误,那么整个单元就会回滚,撤回到事务最初的状态 + +* 如果单元中所有的 SQL 语句都执行成功,则事务就顺利执行 + +事务的四大特征:ACID + +- 原子性 (atomicity) +- 一致性 (consistency) +- 隔离性 (isolaction) +- 持久性 (durability) + +事务的几种状态: + +* 活动的(active):事务对应的数据库操作正在执行中 +* 部分提交的(partially committed):事务的最后一个操作执行完,但是内存还没刷新至磁盘 +* 失败的(failed):当事务处于活动状态或部分提交状态时,如果数据库遇到了错误或刷脏失败,或者用户主动停止当前的事务 +* 中止的(aborted):失败状态的事务回滚完成后的状态 +* 提交的(committed):当处于部分提交状态的事务刷脏成功,就处于提交状态 + + + + + +*** + + + +### 事务管理 + +#### 基本操作 + +事务管理的三个步骤 + +1. 开启事务:记录回滚点,并通知服务器,将要执行一组操作,要么同时成功、要么同时失败 + +2. 执行 SQL 语句:执行具体的一条或多条 SQL 语句 + +3. 结束事务(提交|回滚) + + - 提交:没出现问题,数据进行更新 + - 回滚:出现问题,数据恢复到开启事务时的状态 + + +事务操作: + +* 显式开启事务 + + ```mysql + START TRANSACTION [READ ONLY|READ WRITE|WITH CONSISTENT SNAPSHOT]; #可以跟一个或多个状态,最后的是一致性读 + BEGIN [WORK]; + ``` + + 说明:不填状态默认是读写事务 + +* 回滚事务,用来手动中止事务 + + ```mysql + ROLLBACK; + ``` + +* 提交事务,显示执行是手动提交,MySQL 默认为自动提交 + + ```mysql + COMMIT; + ``` + +* 保存点:在事务的执行过程中设置的还原点,调用 ROLLBACK 时可以指定回滚到哪个点 + + ```mysql + SAVEPOINT point_name; #设置保存点 + RELEASE point_name #删除保存点 + ROLLBACK [WORK] TO [SAVEPOINT] point_name #回滚至某个保存点,不填默认回滚到事务执行之前的状态 + ``` + +* 操作演示 + + ```mysql + -- 开启事务 + START TRANSACTION; + + -- 张三给李四转账500元 + -- 1.张三账户-500 + UPDATE account SET money=money-500 WHERE NAME='张三'; + -- 2.李四账户+500 + UPDATE account SET money=money+500 WHERE NAME='李四'; + + -- 回滚事务(出现问题) + ROLLBACK; + + -- 提交事务(没出现问题) + COMMIT; + ``` + + + +*** + + + +#### 提交方式 + +提交方式的相关语法: + +- 查看事务提交方式 + + ```mysql + SELECT @@AUTOCOMMIT; -- 会话,1 代表自动提交 0 代表手动提交 + SELECT @@GLOBAL.AUTOCOMMIT; -- 系统 + ``` + +- 修改事务提交方式 + + ```mysql + SET @@AUTOCOMMIT=数字; -- 系统 + SET AUTOCOMMIT=数字; -- 会话 + ``` + +- **系统变量的操作**: + + ```sql + SET [GLOBAL|SESSION] 变量名 = 值; -- 默认是会话 + SET @@[(GLOBAL|SESSION).]变量名 = 值; -- 默认是系统 + ``` + + ```sql + SHOW [GLOBAL|SESSION] VARIABLES [LIKE '变量%']; -- 默认查看会话内系统变量值 + ``` + +工作原理: + +* 自动提交:如果没有 START TRANSACTION 显式地开始一个事务,那么**每条 SQL 语句都会被当做一个事务执行提交操作**;显式开启事务后,会在本次事务结束(提交或回滚)前暂时关闭自动提交 +* 手动提交:不需要显式的开启事务,所有的 SQL 语句都在一个事务中,直到执行了提交或回滚,然后进入下一个事务 +* 隐式提交:存在一些特殊的命令,在事务中执行了这些命令会马上**强制执行 COMMIT 提交事务** + * **DDL 语句** (CREATE/DROP/ALTER)、LOCK TABLES 语句、LOAD DATA 导入数据语句、主从复制语句等 + * 当一个事务还没提交或回滚,显式的开启一个事务会隐式的提交上一个事务 + + + +**** + + + +#### 事务 ID + +事务在执行过程中对某个表执行了**增删改操作或者创建表**,就会为当前事务分配一个独一无二的事务 ID(对临时表并不会分配 ID),如果当前事务没有被分配 ID,默认是 0 + +说明:只读事务不能对普通的表进行增删改操作,但是可以对临时表增删改,读写事务可以对数据表执行增删改查操作 + +事务 ID 本质上就是一个数字,服务器在内存中维护一个全局变量: + +* 每当需要为某个事务分配 ID,就会把全局变量的值赋值给事务 ID,然后变量自增 1 +* 每当变量值为 256 的倍数时,就将该变量的值刷新到系统表空间的 Max Trx ID 属性中,该属性占 8 字节 +* 系统再次启动后,会读取表空间的 Max Trx ID 属性到内存,加上 256 后赋值给全局变量,因为关机时的事务 ID 可能并不是 256 的倍数,会比 Max Trx ID 大,所以需要加上 256 保持事务 ID 是一个**递增的数字** + +**聚簇索引**的行记录除了完整的数据,还会自动添加 trx_id、roll_pointer 隐藏列,如果表中没有主键并且没有非空唯一索引,也会添加一个 row_id 的隐藏列作为聚簇索引 + + + + +*** + + + +### 隔离级别 + +#### 四种级别 + +事务的隔离级别:多个客户端操作时,各个客户端的事务之间应该是隔离的,**不同的事务之间不该互相影响**,而如果多个事务操作同一批数据时,则需要设置不同的隔离级别,否则就会产生问题。 + +隔离级别分类: + +| 隔离级别 | 名称 | 会引发的问题 | 数据库默认隔离级别 | +| ---------------- | -------- | ---------------------- | ------------------- | +| Read Uncommitted | 读未提交 | 脏读、不可重复读、幻读 | | +| Read Committed | 读已提交 | 不可重复读、幻读 | Oracle / SQL Server | +| Repeatable Read | 可重复读 | 幻读 | MySQL | +| Serializable | 可串行化 | 无 | | + +一般来说,隔离级别越低,系统开销越低,可支持的并发越高,但隔离性也越差 + +* 脏写 (Dirty Write):当两个或多个事务选择同一行,最初的事务修改的值被后面事务修改的值覆盖,所有的隔离级别都可以避免脏写(又叫丢失更新),因为有行锁 + +* 脏读 (Dirty Reads):在一个事务处理过程中读取了另一个**未提交**的事务中修改过的数据 + +* 不可重复读 (Non-Repeatable Reads):在一个事务处理过程中读取了另一个事务中修改并**已提交**的数据 + + > 可重复读的意思是不管读几次,结果都一样,可以重复的读,可以理解为快照读,要读的数据集不会发生变化 + +* 幻读 (Phantom Reads):在事务中按某个条件先后两次查询数据库,后一次查询查到了前一次查询没有查到的行,**数据条目**发生了变化。比如查询某数据不存在,准备插入此记录,但执行插入时发现此记录已存在,无法插入 + +隔离级别操作语法: + +* 查询数据库隔离级别 + + ```mysql + SELECT @@TX_ISOLATION; -- 会话 + SELECT @@GLOBAL.TX_ISOLATION; -- 系统 + ``` + +* 修改数据库隔离级别 + + ```mysql + SET GLOBAL TRANSACTION ISOLATION LEVEL 级别字符串; + ``` + + + +*** + + + +#### 加锁分析 + +InnoDB 存储引擎支持事务,所以加锁分析是基于该存储引擎 + +* Read Uncommitted 级别,任何操作都不会加锁 + +* Read Committed 级别,增删改操作会加写锁(行锁),读操作不加锁 + + 在 Server 层过滤条件时发现不满足的记录会调用 unlock_row 方法释放该记录的行锁,保证最后只有满足条件的记录加锁,但是扫表过程中每条记录的**加锁操作不能省略**。所以对数据量很大的表做批量修改时,如果无法使用相应的索引(全表扫描),在 Server 过滤数据时就会特别慢,出现虽然没有修改某些行的数据,但是还是被锁住了的现象(锁表),这种情况同样适用于 RR + +* Repeatable Read 级别,增删改操作会加写锁,读操作不加锁。因为读写锁不兼容,**加了读锁后其他事务就无法修改数据**,影响了并发性能,为了保证隔离性和并发性,MySQL 通过 MVCC 解决了读写冲突。RR 级别下的锁有很多种,锁机制章节详解 + +* Serializable 级别,读加共享锁,写加排他锁,读写互斥,使用的悲观锁的理论,实现简单,数据更加安全,但是并发能力非常差 + + * 串行化:让所有事务按顺序单独执行,写操作会加写锁,读操作会加读锁 + * 可串行化:让所有操作相同数据的事务顺序执行,通过加锁实现 + + + +参考文章:https://tech.meituan.com/2014/08/20/innodb-lock.html + + + +*** + + + +### 原子特性 + +#### 实现方式 + +原子性是指事务是一个不可分割的工作单位,事务的操作如果成功就必须要完全应用到数据库,失败则不能对数据库有任何影响。比如事务中一个 SQL 语句执行失败,则已执行的语句也必须回滚,数据库退回到事务前的状态 + +InnoDB 存储引擎提供了两种事务日志:redo log(重做日志)和 undo log(回滚日志) + +* redo log 用于保证事务持久性 +* undo log 用于保证事务原子性和隔离性 + +undo log 属于**逻辑日志**,根据每行操作进行记录,记录了 SQL 执行相关的信息,用来回滚行记录到某个版本 + +当事务对数据库进行修改时,InnoDB 会先记录对应的 undo log,如果事务执行失败或调用了 rollback 导致事务回滚,InnoDB 会根据 undo log 的内容**做与之前相反的操作**: + +* 对于每个 insert,回滚时会执行 delete + +* 对于每个 delete,回滚时会执行 insert + +* 对于每个 update,回滚时会执行一个相反的 update,把数据修改回去 + + + +参考文章:https://www.cnblogs.com/kismetv/p/10331633.html + + + +*** + + + +#### DML 解析 + +##### INSERT + +乐观插入:当前数据页的剩余空间充足,直接将数据进行插入 + +悲观插入:当前数据页的剩余空间不足,需要进行页分裂,申请一个新的页面来插入数据,会造成更多的 redo log,undo log 影响不大 + +当向某个表插入一条记录,实际上需要向聚簇索引和所有二级索引都插入一条记录,但是 undo log **只针对聚簇索引记录**,在回滚时会根据聚簇索引去所有的二级索引进行回滚操作 + +roll_pointer 是一个指针,**指向记录对应的 undo log 日志**,一条记录就是一个数据行,行格式中的 roll_pointer 就指向 undo log + + + +*** + + + +##### DELETE + +插入到页面中的记录会根据 next_record 属性组成一个单向链表,这个链表称为正常链表,被删除的记录也会通过 next_record 组成一个垃圾链表,该链表中所占用的存储空间可以被重新利用,并不会直接清除数据 + +在页面 Page Header 中,PAGE_FREE 属性指向垃圾链表的头节点,删除的工作过程: + +* 将要删除的记录的 delete_flag 位置为 1,其他不做修改,这个过程叫 **delete mark** + +* 在事务提交前,delete_flag = 1 的记录一直都会处于中间状态 + +* 事务提交后,有专门的线程将 delete_flag = 1 的记录从正常链表移除并加入垃圾链表,这个过程叫 **purge** + + purge 线程在执行删除操作时会创建一个 ReadView,根据事务的可见性移除数据(隔离特性部分详解) + +当有新插入的记录时,首先判断 PAGE_FREE 指向的头节点是否足够容纳新纪录: + +* 如果可以容纳新纪录,就会直接重用已删除的记录的存储空间,然后让 PAGE_FREE 指向垃圾链表的下一个节点 +* 如果不能容纳新纪录,就直接向页面申请新的空间存储,并不会遍历垃圾链表 + +重用已删除的记录空间,可能会造成空间碎片,当数据页容纳不了一条记录时,会判断将碎片空间加起来是否可以容纳,判断为真就会重新组织页内的记录: + +* 开辟一个临时页面,将页内记录一次插入到临时页面,此时临时页面时没有碎片的 +* 把临时页面的内容复制到本页,这样就解放出了内存碎片,但是会耗费很大的性能资源 + + + +**** + + + +##### UPDATE + +执行 UPDATE 语句,对于更新主键和不更新主键有两种不同的处理方式 + +不更新主键的情况: + +* 就地更新(in-place update),如果更新后的列和更新前的列占用的存储空间一样大,就可以直接在原记录上修改 + +* 先删除旧纪录,再插入新纪录,这里的删除不是 delete mark,而是直接将记录加入垃圾链表,并且修改页面的相应的控制信息,执行删除的线程不是 purge,是执行更新的用户线程,插入新记录时可能造成页空间不足,从而导致页分裂 + + +更新主键的情况: + +* 将旧纪录进行 delete mark,在更新语句提交后由 purge 线程移入垃圾链表 +* 根据更新的各列的值创建一条新纪录,插入到聚簇索引中 + +在对一条记录修改前会**将记录的隐藏列 trx_id 和 roll_pointer 的旧值记录到当前 undo log 对应的属性中**,这样当前记录的 roll_pointer 指向当前 undo log 记录,当前 undo log 记录的 roll_pointer 指向旧的 undo log 记录,**形成一个版本链** + +UPDATE、DELETE 操作产生的 undo 日志会用于其他事务的 MVCC 操作,所以不能立即删除,INSERT 可以删除的原因是 MVCC 是对现有数据的快照 + + + +*** + + + +#### 回滚日志 + +undo log 是采用段的方式来记录,Rollback Segement 称为回滚段,本质上就是一个类型是 Rollback Segement Header 的页面 + +每个回滚段中有 1024 个 undo slot,每个 slot 存放 undo 链表页面的头节点页号,每个链表对应一个叫 undo log segment 的段 + +* 在以前老版本,只支持 1 个 Rollback Segement,只能记录 1024 个 undo log segment +* MySQL5.5 开始支持 128 个 Rollback Segement,支持 128*1024 个 undo 操作 + +工作流程: + +* 事务执行前需要到系统表空间第 5 号页面中分配一个回滚段(页),获取一个 Rollback Segement Header 页面的地址 +* 回滚段页面有 1024 个 undo slot,首先去回滚段的两个 cached 链表获取缓存的 slot,缓存中没有就在回滚段页面中找一个可用的 undo slot 分配给当前事务 +* 如果是缓存中获取的 slot,则该 slot 对应的 undo log segment 已经分配了,需要重新分配,然后从 undo log segment 中申请一个页面作为日志链表的头节点,并填入对应的 slot 中 +* 每个事务 undo 日志在记录的时候**占用两个 undo 页面的组成链表**,分别为 insert undo 链表和 update undo 链表,链表的头节点页面为 first undo page 会包含一些管理信息,其他页面为 normal undo page + + 说明:事务执行过程的临时表也需要两个 undo 链表,不和普通表共用,这些链表并不是事务开始就分配,而是按需分配 + + + + + +*** + + + +### 隔离特性 + +#### 实现方式 + +隔离性是指,事务内部的操作与其他事务是隔离的,多个并发事务之间要相互隔离,不能互相干扰 + +* 严格的隔离性,对应了事务隔离级别中的 serializable,实际应用中对性能考虑很少使用可串行化 + +* 与原子性、持久性侧重于研究事务本身不同,隔离性研究的是**不同事务**之间的相互影响 + +隔离性让并发情形下的事务之间互不干扰: + +- 一个事务的写操作对另一个事务的写操作(写写):锁机制保证隔离性 +- 一个事务的写操作对另一个事务的读操作(读写):MVCC 保证隔离性 + +锁机制:事务在修改数据之前,需要先获得相应的锁,获得锁之后,事务便可以修改数据;该事务操作期间,这部分数据是锁定的,其他事务如果需要修改数据,需要等待当前事务提交或回滚后释放锁(详解见锁机制) + + + +*** + + + +#### 并发控制 + +MVCC 全称 Multi-Version Concurrency Control,即多版本并发控制,用来**解决读写冲突的无锁并发控制**,可以在发生读写请求冲突时不用加锁解决,这个读是指的快照读(也叫一致性读或一致性无锁读),而不是当前读: + +* 快照读:实现基于 MVCC,因为是多版本并发,所以快照读读到的数据不一定是当前最新的数据,有可能是历史版本的数据 +* 当前读:又叫加锁读,读取数据库记录是当前**最新的版本**(产生幻读、不可重复读),可以对读取的数据进行加锁,防止其他事务修改数据,是悲观锁的一种操作,读写操作加共享锁或者排他锁和串行化事务的隔离级别都是当前读 + +数据库并发场景: + +* 读-读:不存在任何问题,也不需要并发控制 + +* 读-写:有线程安全问题,可能会造成事务隔离性问题,可能遇到脏读,幻读,不可重复读 + +* 写-写:有线程安全问题,可能会存在脏写(丢失更新)问题 + +MVCC 的优点: + +* 在并发读写数据库时,做到在读操作时不用阻塞写操作,写操作也不用阻塞读操作,提高了并发读写的性能 +* 可以解决脏读,不可重复读等事务隔离问题(加锁也能解决),但不能解决更新丢失问题(写锁会解决) + +提高读写和写写的并发性能: + +* MVCC + 悲观锁:MVCC 解决读写冲突,悲观锁解决写写冲突 +* MVCC + 乐观锁:MVCC 解决读写冲突,乐观锁解决写写冲突 + + + +参考文章:https://www.jianshu.com/p/8845ddca3b23 + + + +*** + + + +#### 实现原理 + +##### 隐藏字段 + +实现原理主要是隐藏字段,undo日志,Read View 来实现的 + +InnoDB 存储引擎,数据库中的**聚簇索引**每行数据,除了自定义的字段,还有数据库隐式定义的字段: + +* DB_TRX_ID:最近修改事务 ID,记录创建该数据或最后一次修改该数据的事务 ID +* DB_ROLL_PTR:回滚指针,**指向记录对应的 undo log 日志**,undo log 中又指向上一个旧版本的 undo log +* DB_ROW_ID:隐含的自增 ID(**隐藏主键**),如果数据表没有主键,InnoDB 会自动以 DB_ROW_ID 作为聚簇索引 + +![](https://seazean.oss-cn-beijing.aliyuncs.com/img/DB/MySQL-MVCC版本链隐藏字段.png) + + + + + +*** + + + +##### 版本链 + +undo log 是逻辑日志,记录的是每个事务对数据执行的操作,而不是记录的全部数据,要**根据 undo log 逆推出以往事务的数据** + +undo log 的作用: + +* 保证事务进行 rollback 时的原子性和一致性,当事务进行回滚的时候可以用 undo log 的数据进行恢复 +* 用于 MVCC 快照读,通过读取 undo log 的历史版本数据可以实现不同事务版本号都拥有自己独立的快照数据 + +undo log 主要分为两种: + +* insert undo log:事务在 insert 新记录时产生的 undo log,只在事务回滚时需要,并且在事务提交后可以被立即丢弃 + +* update undo log:事务在进行 update 或 delete 时产生的 undo log,在事务回滚时需要,在快照读时也需要。不能随意删除,只有在当前读或事务回滚不涉及该日志时,对应的日志才会被 purge 线程统一清除 + +每次对数据库记录进行改动,都会产生的新版本的 undo log,随着更新次数的增多,所有的版本都会被 roll_pointer 属性连接成一个链表,把这个链表称之为**版本链**,版本链的头节点就是当前的最新的 undo log,链尾就是最早的旧 undo log + +说明:因为 DELETE 删除记录,都是移动到垃圾链表中,不是真正的删除,所以才可以通过版本链访问原始数据 + + + +注意:undo 是逻辑日志,这里只是直观的展示出来 + +工作流程: + +* 有个事务插入 persion 表一条新记录,name 为 Jerry,age 为 24 +* 事务 1 修改该行数据时,数据库会先对该行加排他锁,然后先记录 undo log,然后修改该行 name 为 Tom,并且修改隐藏字段的事务 ID 为当前事务 1 的 ID(默认为 1 之后递增),回滚指针指向拷贝到 undo log 的副本记录,事务提交后,释放锁 +* 以此类推 + + + +*** + + + +##### 读视图 + +Read View 是事务进行读数据操作时产生的读视图,该事务执行快照读的那一刻会生成数据库系统当前的一个快照,记录并维护系统当前活跃事务的 ID,用来做可见性判断,根据视图判断当前事务能够看到哪个版本的数据 + +注意:这里的快照并不是把所有的数据拷贝一份副本,而是由 undo log 记录的逻辑日志,根据库中的数据进行计算出历史数据 + +工作流程:将版本链的头节点的事务 ID(最新数据事务 ID,大概率不是当前线程)DB_TRX_ID 取出来,与系统当前活跃事务的 ID 对比进行可见性分析,不可见就通过 DB_ROLL_PTR 回滚指针去取出 undo log 中的下一个 DB_TRX_ID 比较,直到找到最近的满足可见性的 DB_TRX_ID,该事务 ID 所在的旧记录就是当前事务能看见的最新的记录 + +Read View 几个属性: + +- m_ids:生成 Read View 时当前系统中活跃的事务 id 列表(未提交的事务集合,当前事务也在其中) +- min_trx_id:生成 Read View 时当前系统中活跃的最小的事务 id,也就是 m_ids 中的最小值(已提交的事务集合) +- max_trx_id:生成 Read View 时当前系统应该分配给下一个事务的 id 值,m_ids 中的最大值加 1(未开始事务) +- creator_trx_id:生成该 Read View 的事务的事务 id,就是判断该 id 的事务能读到什么数据 + +creator 创建一个 Read View,进行可见性算法分析:(解决了读未提交) + +* db_trx_id == creator_trx_id:表示这个数据就是当前事务自己生成的,自己生成的数据自己肯定能看见,所以此数据对 creator 是可见的 +* db_trx_id < min_trx_id:该版本对应的事务 ID 小于 Read view 中的最小活跃事务 ID,则这个事务在当前事务之前就已经被提交了,对 creator 可见(因为比已提交的最大事务 ID 小的并不一定已经提交,所以应该判断是否在活跃事务列表) + +* db_trx_id >= max_trx_id:该版本对应的事务 ID 大于 Read view 中当前系统的最大事务 ID,则说明该数据是在当前 Read view 创建之后才产生的,对 creator 不可见 +* min_trx_id<= db_trx_id < max_trx_id:判断 db_trx_id 是否在活跃事务列表 m_ids 中 + * 在列表中,说明该版本对应的事务正在运行,数据不能显示(**不能读到未提交的数据**) + * 不在列表中,说明该版本对应的事务已经被提交,数据可以显示(**可以读到已经提交的数据**) + + + +*** + + + +##### 工作流程 + +表 user 数据 + +```sh +id name age +1 张三 18 +``` + +Transaction 20: + +```mysql +START TRANSACTION; -- 开启事务 +UPDATE user SET name = '李四' WHERE id = 1; +UPDATE user SET name = '王五' WHERE id = 1; +``` + +Transaction 60: + +```mysql +START TRANSACTION; -- 开启事务 +-- 操作表的其他数据 +``` + +![](https://seazean.oss-cn-beijing.aliyuncs.com/img/DB/MySQL-MVCC工作流程1.png) + +ID 为 0 的事务创建 Read View: + +* m_ids:20、60 +* min_trx_id:20 +* max_trx_id:61 +* creator_trx_id:0 + +![](https://seazean.oss-cn-beijing.aliyuncs.com/img/DB/MySQL-MVCC工作流程2.png) + +只有红框部分才复合条件,所以只有张三对应的版本的数据可以被看到 + + + +参考视频:https://www.bilibili.com/video/BV1t5411u7Fg + + + +*** + + + +##### 二级索引 + +只有在聚簇索引中才有 trx_id 和 roll_pointer 的隐藏列,对于二级索引判断可见性的方式: + +* 二级索引页面的 Page Header 中有一个 PAGE_MAX_TRX_ID 属性,代表修改当前页面的最大的事务 ID,SELECT 语句访问某个二级索引时会判断 ReadView 的 min_trx_id 是否大于该属性,大于说明该页面的所有属性对 ReadView 可见 +* 如果属性判断不可见,就需要利用二级索引获取主键值进行**回表操作**,得到聚簇索引后按照聚簇索引的可见性判断的方法操作 + + + +*** + + + +#### RC RR + +Read View 用于支持 RC(Read Committed,读已提交)和 RR(Repeatable Read,可重复读)隔离级别的实现,所以 **SELECT 在 RC 和 RR 隔离级别使用 MVCC 读取记录** + +RR、RC 生成时机: + +- RC 隔离级别下,每次读取数据前都会生成最新的 Read View(当前读) +- RR 隔离级别下,在第一次数据读取时才会创建 Read View(快照读) + +RC、RR 级别下的 InnoDB 快照读区别 + +- RC 级别下,事务中每次快照读都会新生成一个 Read View,这就是在 RC 级别下的事务中可以看到别的事务提交的更新的原因 + +- RR 级别下,某个事务的对某条记录的**第一次快照读**会创建一个 Read View, 将当前系统活跃的其他事务记录起来,此后在调用快照读的时候,使用的是同一个 Read View,所以一个事务的查询结果每次都是相同的 + + RR 级别下,通过 `START TRANSACTION WITH CONSISTENT SNAPSHOT` 开启事务,会在执行该语句后立刻生成一个 Read View,不是在执行第一条 SELECT 语句时生成(所以说 `START TRANSACTION` 并不是事务的起点,执行第一条语句才算起点) + +解决幻读问题: + +- 快照读:通过 MVCC 来进行控制的,在可重复读隔离级别下,普通查询是快照读,是不会看到别的事务插入的数据的,但是**并不能完全避免幻读** + + 场景:RR 级别,T1 事务开启,创建 Read View,此时 T2 去 INSERT 新的一行然后提交,然后 T1 去 UPDATE 该行会发现更新成功,并且把这条新记录的 trx_id 变为当前的事务 id,所以对当前事务就是可见的。因为 **Read View 并不能阻止事务去更新数据,更新数据都是先读后写并且是当前读**,读取到的是最新版本的数据 + +- 当前读:通过 next-key 锁(行锁 + 间隙锁)来解决问题 + + + + + +*** + + + +### 持久特性 + +#### 实现方式 + +持久性是指一个事务一旦被提交了,那么对数据库中数据的改变就是永久性的,接下来的其他操作或故障不应该对其有任何影响。 + +Buffer Pool 的使用提高了读写数据的效率,但是如果 MySQL 宕机,此时 Buffer Pool 中修改的数据还没有刷新到磁盘,就会导致数据的丢失,事务的持久性无法保证,所以引入了 redo log 日志: + +* redo log **记录数据页的物理修改**,而不是某一行或某几行的修改,用来恢复提交后的数据页,只能**恢复到最后一次提交**的位置 +* redo log 采用的是 WAL(Write-ahead logging,**预写式日志**),所有修改要先写入日志,再更新到磁盘,保证了数据不会因 MySQL 宕机而丢失,从而满足了持久性要求 +* 简单的 redo log 是纯粹的物理日志,复杂的 redo log 会存在物理日志和逻辑日志 + +工作过程:MySQL 发生了宕机,InnoDB 会判断一个数据页在崩溃恢复时丢失了更新,就会将它读到内存,然后根据 redo log 内容更新内存,更新完成后,内存页变成脏页,然后进行刷脏 + +缓冲池的**刷脏策略**: + +* redo log 文件是固定大小的,如果写满了就要擦除以前的记录,在擦除之前需要把对应的更新持久化到磁盘中 +* Buffer Pool 内存不足,需要淘汰部分数据页(LRU 链表尾部),如果淘汰的是脏页,就要先将脏页写到磁盘(要避免大事务) +* 系统空闲时,后台线程会自动进行刷脏(Flush 链表部分已经详解) +* MySQL 正常关闭时,会把内存的脏页都刷新到磁盘上 + + + +**** + + + +#### 重做日志 + +##### 日志缓冲 + +服务器启动时会向操作系统申请一片连续内存空间作为 redo log buffer(重做日志缓冲区),可以通过 `innodb_log_buffer_size` 系统变量指定 redo log buffer 的大小,默认是 16MB + +log buffer 被划分为若干 redo log block(块,类似数据页的概念),每个默认大小 512 字节,每个 block 由 12 字节的 log block head、496 字节的 log block body、4 字节的 log block trailer 组成 + +* 当数据修改时,先修改 Change Buffer 中的数据,然后在 redo log buffer 记录这次操作,写入 log buffer 的过程是**顺序写入**的(先写入前面的 block,写满后继续写下一个) +* log buffer 中有一个指针 buf_free,来标识该位置之前都是填满的 block,该位置之后都是空闲区域 + +MySQL 规定对底层页面的一次原子访问称为一个 Mini-Transaction(MTR),比如在 B+ 树上插入一条数据就算一个 MTR + +* 一个事务包含若干个 MTR,一个 MTR 对应一组若干条 redo log,一组 redo log 是不可分割的,在进行数据恢复时也把一组 redo log 当作一个不可分割的整体处理 + +* 不是每生成一条 redo 日志就将其插入到 log buffer 中,而是一个 MTR 结束后**将一组 redo 日志写入** + +InnoDB 的 redo log 是**固定大小**的,redo 日志在磁盘中以文件组的形式存储,同一组中的每个文件大小一样格式一样 + +* `innodb_log_group_home_dir` 代表磁盘存储 redo log 的文件目录,默认是当前数据目录 +* `innodb_log_file_size` 代表文件大小,默认 48M,`innodb_log_files_in_group` 代表文件个数,默认 2 最大 100,所以日志的文件大小为 `innodb_log_file_size * innodb_log_files_in_group` + +redo 日志文件也是由若干个 512 字节的 block 组成,日志文件的前 2048 个字节(前 4 个 block)用来存储一些管理信息,以后的用来存储 log buffer 中的 block 镜像 + +注意:block 并不代表一组 redo log,一组日志可能占用不到一个 block 或者几个 block,依赖于 MTR 的大小 + + + +*** + + + +##### 日志刷盘 + +redo log 需要在事务提交时将日志写入磁盘,但是比 Buffer Pool 修改的数据写入磁盘的速度快,原因: + +* 刷脏是随机 IO,因为每次修改的数据位置随机;redo log 和 binlog 都是**顺序写**,磁盘的顺序 IO 比随机 IO 速度要快 +* 刷脏是以数据页(Page)为单位的,一个页上的一个小修改都要整页写入;redo log 中只包含真正需要写入的部分,好几页的数据修改可能只记录在一个 redo log 页中,减少无效 IO +* **组提交机制**,可以大幅度降低磁盘的 IO 消耗 + +InnoDB 引擎会在适当的时候,把内存中 redo log buffer 持久化(fsync)到磁盘,具体的**刷盘策略**: + +* 在事务提交时需要进行刷盘,通过修改参数 `innodb_flush_log_at_trx_commit` 设置: + * 0:表示当提交事务时,并不将缓冲区的 redo 日志写入磁盘,而是等待**后台线程每秒刷新一次** + * 1:在事务提交时将缓冲区的 redo 日志**同步写入**到磁盘,保证一定会写入成功(默认值) + * 2:在事务提交时将缓冲区的 redo 日志异步写入到磁盘,不能保证提交时肯定会写入,只是有这个动作。日志已经在操作系统的缓存,如果操作系统没有宕机而 MySQL 宕机,也是可以恢复数据的 +* 写入 redo log buffer 的日志超过了总容量的一半,就会将日志刷入到磁盘文件,这会影响执行效率,所以开发中应**避免大事务** +* 服务器关闭时 +* 并行的事务提交(组提交)时,会将将其他事务的 redo log 持久化到磁盘。假设事务 A 已经写入 redo log buffer 中,这时另外一个线程的事务 B 提交,如果 innodb_flush_log_at_trx_commit 设置的是 1,那么事务 B 要把 redo log buffer 里的日志全部持久化到磁盘,**因为多个事务共用一个 redo log buffer**,所以一次 fsync 可以刷盘多个事务的 redo log,提升了并发量 + +服务器启动后 redo 磁盘空间不变,所以 redo 磁盘中的日志文件是被**循环使用**的,采用循环写数据的方式,写完尾部重新写头部,所以要确保头部 log 对应的修改已经持久化到磁盘 + + + +*** + + + +##### 日志序号 + +lsn (log sequence number) 代表已经写入的 redo 日志量、flushed_to_disk_lsn 指刷新到磁盘中的 redo 日志量,两者都是**全局变量**,如果两者的值相同,说明 log buffer 中所有的 redo 日志都已经持久化到磁盘 + +工作过程:写入 log buffer 数据时,buf_free 会进行偏移,偏移量就会加到 lsn 上 + +MTR 的执行过程中修改过的页对应的控制块会加到 Buffer Pool 的 flush 链表中,链表中脏页是按照第一次修改的时间进行排序的(头插),控制块中有两个指针用来记录脏页被修改的时间: + +* oldest_modification:第一次修改 Buffer Pool 中某个缓冲页时,将修改该页的 MTR **开始时**对应的 lsn 值写入这个属性 +* newest_modification:每次修改页面,都将 MTR 结束时全局的 lsn 值写入这个属性,所以该值是该页面最后一次修改后的 lsn 值 + +全局变量 checkpoint_lsn 表示**当前系统可以被覆盖的 redo 日志总量**,当 redo 日志对应的脏页已经被刷新到磁盘后,该文件空间就可以被覆盖重用,此时执行一次 checkpoint 来更新 checkpoint_lsn 的值存入管理信息(刷脏和执行一次 checkpoint 并不是同一个线程),该值的增量就代表磁盘文件中当前位置向后可以被覆盖的文件的量,所以该值是一直增大的 + +**checkpoint**:从 flush 链表尾部中找出还未刷脏的页面,该页面是当前系统中最早被修改的脏页,该页面之前产生的脏页都已经刷脏,然后将该页 oldest_modification 值赋值给 checkpoint_lsn,因为 lsn 小于该值时产生的 redo 日志都可以被覆盖了 + +但是在系统忙碌时,后台线程的刷脏操作不能将脏页快速刷出,导致系统无法及时执行 checkpoint ,这时需要用户线程从 flush 链表中把最早修改的脏页刷新到磁盘中,然后执行 checkpoint + +```java +write pos ------- checkpoint_lsn // 两值之间的部分表示可以写入的日志量,当 pos 追赶上 lsn 时必须执行 checkpoint +``` + +使用命令可以查看当前 InnoDB 存储引擎各种 lsn 的值: + +```mysql +SHOW ENGINE INNODB STATUS\G +``` + + + +**** + + + +##### 崩溃恢复 + +恢复的起点:在从 redo 日志文件组的管理信息中获取最近发生 checkpoint 的信息,**从 checkpoint_lsn 对应的日志文件开始恢复** + +恢复的终点:扫描日志文件的 block,block 的头部记录着当前 block 使用了多少字节,填满的 block 总是 512 字节, 如果某个 block 不是 512 字节,说明该 block 就是需要恢复的最后一个 block + +恢复的过程:按照 redo log 依次执行恢复数据,优化方式 + +* 使用哈希表:根据 redo log 的 space id 和 page number 属性计算出哈希值,将对同一页面的修改放入同一个槽里,可以一次性完成对某页的恢复,**避免了随机 IO** +* 跳过已经刷新到磁盘中的页面:数据页的 File Header 中的 FILE_PAGE_LSN 属性(类似 newest_modification)表示最近一次修改页面时的 lsn 值,数据页被刷新到磁盘中,那么该页 lsn 属性肯定大于 checkpoint_lsn + + + +参考书籍:https://book.douban.com/subject/35231266/ + + + +*** + + + +#### 工作流程 + +##### 日志对比 + +MySQL 中还存在 binlog(二进制日志)也可以记录写操作并用于数据的恢复,**保证数据不丢失**,二者的区别是: + +* 作用不同:redo log 是用于 crash recovery (故障恢复),保证 MySQL 宕机也不会影响持久性;binlog 是用于 point-in-time recovery 的,保证服务器可以基于时间点恢复数据,此外 binlog 还用于主从复制 +* 层次不同:redo log 是 InnoDB 存储引擎实现的,而 binlog 是MySQL的 Server 层实现的,同时支持 InnoDB 和其他存储引擎 +* 内容不同:redo log 是物理日志,内容基于磁盘的 Page;binlog 的内容是二进制的,根据 binlog_format 参数的不同,可能基于SQL 语句、基于数据本身或者二者的混合(日志部分详解) +* 写入时机不同:binlog 在事务提交时一次写入;redo log 的写入时机相对多元 + +binlog 为什么不支持崩溃恢复? + +* binlog 记录的是语句,并不记录数据页级的数据(哪个页改了哪些地方),所以没有能力恢复数据页 +* binlog 是追加写,保存全量的日志,没有标志确定从哪个点开始的数据是已经刷盘了,而 redo log 只要在 checkpoint_lsn 后面的就是没有刷盘的 + + + +*** + + + +##### 更新记录 + +更新一条记录的过程:写之前一定先读 + +* 在 B+ 树中定位到该记录,如果该记录所在的页面不在 Buffer Pool 里,先将其加载进内存 + +* 首先更新该记录对应的聚簇索引,更新聚簇索引记录时: + * 更新记录前向 undo 页面写 undo 日志,由于这是更改页面,所以需要记录一下相应的 redo 日志 + + 注意:修改 undo 页面也是在**修改页面**,事务只要修改页面就需要先记录相应的 redo 日志 + + * 然后**记录对应的 redo 日志**(等待 MTR 提交后写入 redo log buffer),**最后进行真正的更新记录** + +* 更新其他的二级索引记录,不会再记录 undo log,只记录 redo log 到 buffer 中 + +* 在一条更新语句执行完成后(也就是将所有待更新记录都更新完了),就会开始记录该语句对应的 binlog 日志,此时记录的 binlog 并没有刷新到硬盘上,还在内存中,在事务提交时才会统一将该事务运行过程中的所有 binlog 日志刷新到硬盘 + +假设表中有字段 id 和 a,存在一条 `id = 1, a = 2` 的记录,此时执行更新语句: + +```sql +update table set a=2 where id=1; +``` + +InnoDB 会真正的去执行把值修改成 (1,2) 这个操作,先加行锁,在去更新,并不会提前判断相同就不修改了 + + + +参考文章:https://mp.weixin.qq.com/s/wcJ2KisSaMnfP4nH5NYaQA + + + +*** + + + +##### 两段提交 + +当客户端执行 COMMIT 语句或者在自动提交的情况下,MySQL 内部开启一个 XA 事务,分两阶段来完成 XA 事务的提交: + +```sql +update T set c=c+1 where ID=2; +``` + + + +流程说明:执行引擎将这行新数据读入到内存中(Buffer Pool)后,先将此次更新操作记录到 redo log buffer 里,然后更新记录。最后将 redo log 刷盘后事务处于 prepare 状态,执行器会生成这个操作的 binlog,并**把 binlog 写入磁盘**,完成提交 + +两阶段: + +* Prepare 阶段:存储引擎将该事务的 **redo 日志刷盘**,并且将本事务的状态设置为 PREPARE,代表执行完成随时可以提交事务 +* Commit 阶段:先将事务执行过程中产生的 binlog 刷新到硬盘,再执行存储引擎的提交工作,引擎把 redo log 改成提交状态 + +存储引擎层的 redo log 和 server 层的 binlog 可以认为是一个分布式事务, 都可以用于表示事务的提交状态,而**两阶段提交就是让这两个状态保持逻辑上的一致**,也有利于主从复制,更好的保持主从数据的一致性 + + + +*** + + + +##### 数据恢复 + +系统崩溃前没有提交的事务的 redo log 可能已经刷盘(定时线程或者 checkpoint),怎么处理崩溃恢复? + +工作流程:获取 undo 链表首节点页面的 undo segement header 中的 TRX_UNDO_STATE 属性,表示当前链表的事务属性,**事务状态是活跃(未提交)的就全部回滚**,如果是 PREPARE 状态,就需要根据 binlog 的状态进行判断: + +* 如果在时刻 A 发生了崩溃(crash),由于此时 binlog 还没完成,所以需要进行回滚 +* 如果在时刻 B 发生了崩溃,redo log 和 binlog 有一个共**同的数据字段叫 XID**,崩溃恢复的时候,会按顺序扫描 redo log: + * 如果 redo log 里面的事务是完整的,也就是已经有了 commit 标识,说明 binlog 也已经记录完整,直接从 redo log 恢复数据 + * 如果 redo log 里面的事务只有 prepare,就根据 XID 去 binlog 中判断对应的事务是否存在并完整,如果完整可以恢复数据 + + +判断一个事务的 binlog 是否完整的方法: + +* statement 格式的 binlog,最后会有 COMMIT +* row 格式的 binlog,最后会有一个 XID event +* MySQL 5.6.2 版本以后,引入了 binlog-checksum 参数用来验证 binlog 内容的正确性(可能日志中间出错) + + + +参考文章:https://time.geekbang.org/column/article/73161 + + + +*** + + + +#### 刷脏优化 + +系统在进行刷脏时会占用一部分系统资源,会影响系统的性能,**产生系统抖动** + +* 一个查询要淘汰的脏页个数太多,会导致查询的响应时间明显变长 +* 日志写满,更新全部堵住,写性能跌为 0,这种情况对敏感业务来说,是不能接受的 + +InnoDB 刷脏页的控制策略: + +* `innodb_io_capacity` 参数代表磁盘的读写能力,建议设置成磁盘的 IOPS(每秒的 IO 次数) +* 刷脏速度参考两个因素:脏页比例和 redo log 写盘速度 + * 参数 `innodb_max_dirty_pages_pct` 是脏页比例上限,默认值是 75%,InnoDB 会根据当前的脏页比例,算出一个范围在 0 到 100 之间的数字 + * InnoDB 每次写入的日志都有一个序号,当前写入的序号跟 checkpoint 对应的序号之间的差值,InnoDB 根据差值算出一个范围在 0 到 100 之间的数字 + * 两者较大的值记为 R,执行引擎按照 innodb_io_capacity 定义的能力乘以 R% 来控制刷脏页的速度 +* `innodb_flush_neighbors` 参数置为 1 代表控制刷脏时检查相邻的数据页,如果也是脏页就一起刷脏,并检查邻居的邻居,这个行为会一直蔓延直到不是脏页,在 MySQL 8.0 中该值的默认值是 0,不建议开启此功能 + + + + + +**** + + + +### 一致特性 + +一致性是指事务执行前后,数据库的完整性约束没有被破坏,事务执行的前后都是合法的数据状态。 + +数据库的完整性约束包括但不限于:实体完整性(如行的主键存在且唯一)、列完整性(如字段的类型、大小、长度要符合要求)、外键约束、用户自定义完整性(如转账前后,两个账户余额的和应该不变) + +实现一致性的措施: + +- 保证原子性、持久性和隔离性,如果这些特性无法保证,事务的一致性也无法保证 +- 数据库本身提供保障,例如不允许向整形列插入字符串值、字符串长度不能超过列的限制等 +- 应用层面进行保障,例如如果转账操作只扣除转账者的余额,而没有增加接收者的余额,无论数据库实现的多么完美,也无法保证状态的一致 + + + + + +**** + + + + + +## 锁机制 + +### 基本介绍 + +锁机制:数据库为了保证数据的一致性,在共享的资源被并发访问时变得安全有序所设计的一种规则 + +利用 MVCC 性质进行读取的操作叫**一致性读**,读取数据前加锁的操作叫**锁定读** + +锁的分类: + +- 按操作分类: + - 共享锁:也叫读锁。对同一份数据,多个事务读操作可以同时加锁而不互相影响 ,但不能修改数据 + - 排他锁:也叫写锁。当前的操作没有完成前,会阻断其他操作的读取和写入 +- 按粒度分类: + - 表级锁:会锁定整个表,开销小,加锁快;不会出现死锁;锁定力度大,发生锁冲突概率高,并发度最低,偏向 MyISAM + - 行级锁:会锁定当前操作行,开销大,加锁慢;会出现死锁;锁定力度小,发生锁冲突概率低,并发度高,偏向 InnoDB + - 页级锁:锁的力度、发生冲突的概率和加锁开销介于表锁和行锁之间,会出现死锁,并发性能一般 +- 按使用方式分类: + - 悲观锁:每次查询数据时都认为别人会修改,很悲观,所以查询时加锁 + - 乐观锁:每次查询数据时都认为别人不会修改,很乐观,但是更新时会判断一下在此期间别人有没有去更新这个数据 + +* 不同存储引擎支持的锁 + + | 存储引擎 | 表级锁 | 行级锁 | 页级锁 | + | -------- | -------- | -------- | ------ | + | MyISAM | 支持 | 不支持 | 不支持 | + | InnoDB | **支持** | **支持** | 不支持 | + | MEMORY | 支持 | 不支持 | 不支持 | + | BDB | 支持 | 不支持 | 支持 | + +从锁的角度来说:表级锁更适合于以查询为主,只有少量按索引条件更新数据的应用,如 Web 应用;而行级锁则更适合于有大量按索引条件并发更新少量不同数据,同时又有并查询的应用,如一些在线事务处理系统 + + + +*** + + + +### 内存结构 + +对一条记录加锁的本质就是**在内存中**创建一个锁结构与之关联,结构包括 + +* 事务信息:锁对应的事务信息,一个锁属于一个事务 +* 索引信息:对于行级锁,需要记录加锁的记录属于哪个索引 +* 表锁和行锁信息:表锁记录着锁定的表,行锁记录了 Space ID 所在表空间、Page Number 所在的页号、n_bits 使用了多少比特 +* type_mode:一个 32 比特的数,被分成 lock_mode、lock_type、rec_lock_type 三个部分 + * lock_mode:锁模式,记录是共享锁、排他锁、意向锁之类 + * lock_type:代表表级锁还是行级锁 + * rec_lock_type:代表行锁的具体类型和 is_waiting 属性,is_waiting = true 时表示当前事务尚未获取到锁,处于等待状态。事务获取锁后的锁结构是 is_waiting 为 false,释放锁时会检查是否与当前记录关联的锁结构,如果有就唤醒对应事务的线程 + +一个事务可能操作多条记录,为了节省内存,满足下面条件的锁使用同一个锁结构: + +* 在同一个事务中的加锁操作 +* 被加锁的记录在同一个页面中 +* 加锁的类型是一样的 +* 加锁的状态是一样的 + + + + + +**** + + + +### Server + +MySQL 里面表级别的锁有两种:一种是表锁,一种是元数据锁(meta data lock,MDL) + +MDL 叫元数据锁,主要用来保护 MySQL 内部对象的元数据,保证数据读写的正确性,**当对一个表做增删改查的时候,加 MDL 读锁;当要对表做结构变更操作 DDL 的时候,加 MDL 写锁**,两种锁不相互兼容,所以可以保证 DDL、DML、DQL 操作的安全 + +说明:DDL 操作执行前会隐式提交当前会话的事务,因为 DDL 一般会在若干个特殊事务中完成,开启特殊事务前需要提交到其他事务 + +MDL 锁的特性: + +* MDL 锁不需要显式使用,在访问一个表的时候会被自动加上,在事务开始时申请,整个事务提交后释放(执行完单条语句不释放) + +* MDL 锁是在 Server 中实现,不是 InnoDB 存储引擎层能直接实现的锁 + +* MDL 锁还能实现其他粒度级别的锁,比如全局锁、库级别的锁、表空间级别的锁 + +FLUSH TABLES WITH READ LOCK 简称(FTWRL),全局读锁,让整个库处于只读状态,DDL DML 都被阻塞,工作流程: + +1. 上全局读锁(lock_global_read_lock) +2. 清理表缓存(close_cached_tables) +3. 上全局 COMMIT 锁(make_global_read_lock_block_commit) + +该命令主要用于备份工具做**一致性备份**,由于 FTWRL 需要持有两把全局的 MDL 锁,并且还要关闭所有表对象,因此杀伤性很大 + + + +*** + + + +### MyISAM + +#### 表级锁 + +MyISAM 存储引擎只支持表锁,这也是 MySQL 开始几个版本中唯一支持的锁类型 + +MyISAM 引擎在执行查询语句之前,会**自动**给涉及到的所有表加读锁,在执行增删改之前,会**自动**给涉及的表加写锁,这个过程并不需要用户干预,所以用户一般不需要直接用 LOCK TABLE 命令给 MyISAM 表显式加锁 + +* 加锁命令:(对 InnoDB 存储引擎也适用) + + 读锁:所有连接只能读取数据,不能修改 + + 写锁:其他连接不能查询和修改数据 + + ```mysql + -- 读锁 + LOCK TABLE table_name READ; + + -- 写锁 + LOCK TABLE table_name WRITE; + ``` + +* 解锁命令: + + ```mysql + -- 将当前会话所有的表进行解锁 + UNLOCK TABLES; + ``` + +锁的兼容性: + +* 对 MyISAM 表的读操作,不会阻塞其他用户对同一表的读请求,但会阻塞对同一表的写请求 +* 对 MyISAM 表的写操作,则会阻塞其他用户对同一表的读和写操作 + +![](https://seazean.oss-cn-beijing.aliyuncs.com/img/DB/MySQL-MyISAM 锁的兼容性.png) + +锁调度:**MyISAM 的读写锁调度是写优先**,因为写锁后其他线程不能做任何操作,大量的更新会使查询很难得到锁,从而造成永远阻塞,所以 MyISAM 不适合做写为主的表的存储引擎 + + + +*** + + + +#### 锁操作 + +##### 读锁 + +两个客户端操作 Client 1和 Client 2,简化为 C1、C2 + +* 数据准备: + + ```mysql + CREATE TABLE `tb_book` ( + `id` INT(11) AUTO_INCREMENT, + `name` VARCHAR(50) DEFAULT NULL, + `publish_time` DATE DEFAULT NULL, + `status` CHAR(1) DEFAULT NULL, + PRIMARY KEY (`id`) + ) ENGINE=MYISAM DEFAULT CHARSET=utf8 ; + + INSERT INTO tb_book (id, NAME, publish_time, STATUS) VALUES(NULL,'java编程思想','2088-08-01','1'); + INSERT INTO tb_book (id, NAME, publish_time, STATUS) VALUES(NULL,'mysql编程思想','2088-08-08','0'); + ``` + +* C1、C2 加读锁,同时查询可以正常查询出数据 + + ```mysql + LOCK TABLE tb_book READ; -- C1、C2 + SELECT * FROM tb_book; -- C1、C2 + ``` + + ![](https://seazean.oss-cn-beijing.aliyuncs.com/img/DB/MySQL-MyISAM 读锁1.png) + +* C1 加读锁,C1、C2 查询未锁定的表,C1 报错,C2 正常查询 + + ```mysql + LOCK TABLE tb_book READ; -- C1 + SELECT * FROM tb_user; -- C1、C2 + ``` + + ![](https://seazean.oss-cn-beijing.aliyuncs.com/img/DB/MySQL-MyISAM 读锁2.png) + + C1、C2 执行插入操作,C1 报错,C2 等待获取 + + ```mysql + INSERT INTO tb_book VALUES(NULL,'Spring高级','2088-01-01','1'); -- C1、C2 + ``` + + ![](https://seazean.oss-cn-beijing.aliyuncs.com/img/DB/MySQL-MyISAM 读锁3.png) + + 当在 C1 中释放锁指令 UNLOCK TABLES,C2 中的 INSERT 语句立即执行 + + + +*** + + + +##### 写锁 + +两个客户端操作 Client 1和 Client 2,简化为 C1、C2 + +* C1 加写锁,C1、C2查询表,C1 正常查询,C2 需要等待 + + ```mysql + LOCK TABLE tb_book WRITE; -- C1 + SELECT * FROM tb_book; -- C1、C2 + ``` + + ![](https://seazean.oss-cn-beijing.aliyuncs.com/img/DB/MySQL-MyISAM 写锁1.png) + + 当在 C1 中释放锁指令 UNLOCK TABLES,C2 中的 SELECT 语句立即执行 + +* C1、C2 同时加写锁 + + ```mysql + LOCK TABLE tb_book WRITE; + ``` + + ![](https://seazean.oss-cn-beijing.aliyuncs.com/img/DB/MySQL-MyISAM 写锁2.png) + +* C1 加写锁,C1、C2查询未锁定的表,C1 报错,C2 正常查询 + + + +*** + + + +#### 锁状态 + +* 查看锁竞争: + + ```mysql + SHOW OPEN TABLES; + ``` + + ![](https://seazean.oss-cn-beijing.aliyuncs.com/img/DB/MySQL-锁争用情况查看1.png) + + In_user:表当前被查询使用的次数,如果该数为零,则表是打开的,但是当前没有被使用 + + Name_locked:表名称是否被锁定,名称锁定用于取消表或对表进行重命名等操作 + + ```mysql + LOCK TABLE tb_book READ; -- 执行命令 + ``` + + ![](https://seazean.oss-cn-beijing.aliyuncs.com/img/DB/MySQL-锁争用情况查看2.png) + +* 查看锁状态: + + ```mysql + SHOW STATUS LIKE 'Table_locks%'; + ``` + + ![](https://seazean.oss-cn-beijing.aliyuncs.com/img/DB/MySQL-MyISAM 锁状态.png) + + Table_locks_immediate:指的是能立即获得表级锁的次数,每立即获取锁,值加 1 + + Table_locks_waited:指的是不能立即获取表级锁而需要等待的次数,每等待一次,该值加 1,此值高说明存在着较为严重的表级锁争用情况 + + + +*** + + + +### InnoDB + +#### 行级锁 + +##### 记录锁 + +InnoDB 与 MyISAM 的最大不同有两点:一是支持事务;二是采用了行级锁,**InnoDB 同时支持表锁和行锁** + +行级锁,也称为记录锁(Record Lock),InnoDB 实现了以下两种类型的行锁: + +- 共享锁 (S):又称为读锁,简称 S 锁,多个事务对于同一数据可以共享一把锁,都能访问到数据,但是只能读不能修改 +- 排他锁 (X):又称为写锁,简称 X 锁,不能与其他锁并存,获取排他锁的事务是可以对数据读取和修改 + +RR 隔离界别下,对于 UPDATE、DELETE 和 INSERT 语句,InnoDB 会**自动给涉及数据集加排他锁**(行锁),在 commit 时自动释放;对于普通 SELECT 语句,不会加任何锁(只是针对 InnoDB 层来说的,因为在 Server 层会**加 MDL 读锁**),通过 MVCC 防止并发冲突 + +在事务中加的锁,并不是不需要了就释放,而是在事务中止或提交时自动释放,这个就是**两阶段锁协议**。所以一般将更新共享资源(并发高)的 SQL 放到事务的最后执行,可以让其他线程尽量的减少等待时间 + +锁的兼容性: + +- 共享锁和共享锁 兼容 +- 共享锁和排他锁 冲突 +- 排他锁和排他锁 冲突 +- 排他锁和共享锁 冲突 + +显式给数据集加共享锁或排他锁:**加锁读就是当前读,读取的是最新数据** + +```mysql +SELECT * FROM table_name WHERE ... LOCK IN SHARE MODE -- 共享锁 +SELECT * FROM table_name WHERE ... FOR UPDATE -- 排他锁 +``` + +注意:**锁默认会锁聚簇索引(锁就是加在索引上)**,但是当使用覆盖索引时,加共享锁只锁二级索引,不锁聚簇索引 + + + +*** + + + +##### 锁操作 + +两个客户端操作 Client 1和 Client 2,简化为 C1、C2 + +* 环境准备 + + ```mysql + CREATE TABLE test_innodb_lock( + id INT(11), + name VARCHAR(16), + sex VARCHAR(1) + )ENGINE = INNODB DEFAULT CHARSET=utf8; + + INSERT INTO test_innodb_lock VALUES(1,'100','1'); + -- .......... + + CREATE INDEX idx_test_innodb_lock_id ON test_innodb_lock(id); + CREATE INDEX idx_test_innodb_lock_name ON test_innodb_lock(name); + ``` + +* 关闭自动提交功能: + + ```mysql + SET AUTOCOMMIT=0; -- C1、C2 + ``` + + 正常查询数据: + + ```mysql + SELECT * FROM test_innodb_lock; -- C1、C2 + ``` + +* 查询 id 为 3 的数据,正常查询: + + ```mysql + SELECT * FROM test_innodb_lock WHERE id=3; -- C1、C2 + ``` + + ![](https://seazean.oss-cn-beijing.aliyuncs.com/img/DB/MySQL-InnoDB 锁操作1.png) + +* C1 更新 id 为 3 的数据,但不提交: + + ```mysql + UPDATE test_innodb_lock SET name='300' WHERE id=3; -- C1 + ``` + + ![](https://seazean.oss-cn-beijing.aliyuncs.com/img/DB/MySQL-InnoDB 锁操作2.png) + + C2 查询不到 C1 修改的数据,因为隔离界别为 REPEATABLE READ,C1 提交事务,C2 查询: + + ```mysql + COMMIT; -- C1 + ``` + + ![](https://seazean.oss-cn-beijing.aliyuncs.com/img/DB/MySQL-InnoDB 锁操作3.png) + + 提交后仍然查询不到 C1 修改的数据,因为隔离级别可以防止脏读、不可重复读,所以 C2 需要提交才可以查询到其他事务对数据的修改: + + ```mysql + COMMIT; -- C2 + SELECT * FROM test_innodb_lock WHERE id=3; -- C2 + ``` + + ![](https://seazean.oss-cn-beijing.aliyuncs.com/img/DB/MySQL-InnoDB 锁操作4.png) + +* C1 更新 id 为 3 的数据,但不提交,C2 也更新 id 为 3 的数据: + + ```mysql + UPDATE test_innodb_lock SET name='3' WHERE id=3; -- C1 + UPDATE test_innodb_lock SET name='30' WHERE id=3; -- C2 + ``` + + ![](https://seazean.oss-cn-beijing.aliyuncs.com/img/DB/MySQL-InnoDB 锁操作5.png) + + 当 C1 提交,C2 直接解除阻塞,直接更新 + +* 操作不同行的数据: + + ```mysql + UPDATE test_innodb_lock SET name='10' WHERE id=1; -- C1 + UPDATE test_innodb_lock SET name='30' WHERE id=3; -- C2 + ``` + + ![](https://seazean.oss-cn-beijing.aliyuncs.com/img/DB/MySQL-InnoDB 锁操作6.png) + + 由于 C1、C2 操作的不同行,获取不同的行锁,所以都可以正常获取行锁 + + + + + +**** + + + +#### 锁分类 + +##### 间隙锁 + +InnoDB 会对间隙(GAP)进行加锁,就是间隙锁 (RR 隔离级别下才有该锁)。间隙锁之间不存在冲突关系,**多个事务可以同时对一个间隙加锁**,但是间隙锁会阻止往这个间隙中插入一个记录的操作 + +InnoDB 加锁的基本单位是 next-key lock,该锁是行锁和 gap lock 的组合(X or S 锁),但是加锁过程是分为间隙锁和行锁两段执行 + +* 可以**保护当前记录和前面的间隙**,遵循左开右闭原则,单纯的间隙锁是左开右开 +* 假设有 10、11、13,那么可能的间隙锁包括:(负无穷,10]、(10,11]、(11,13]、(13,正无穷) + +几种索引的加锁情况: + +* 唯一索引加锁在值存在时是行锁,next-key lock 会退化为行锁,值不存在会变成间隙锁 +* 普通索引加锁会继续向右遍历到不满足条件的值为止,next-key lock 退化为间隙锁 +* 范围查询无论是否是唯一索引,都需要访问到不满足条件的第一个值为止 +* 对于联合索引且是唯一索引,如果 where 条件只包括联合索引的一部分,那么会加间隙锁 + +间隙锁优点:RR 级别下间隙锁可以**解决事务的一部分的幻读问题**,通过对间隙加锁,可以防止读取过程中数据条目发生变化。一部分的意思是不会对全部间隙加锁,只能加锁一部分的间隙 + +间隙锁危害: + +* 当锁定一个范围的键值后,即使某些不存在的键值也会被无辜的锁定,造成在锁定的时候无法插入锁定键值范围内的任何数据,在某些场景下这可能会对性能造成很大的危害,影响并发度 +* 事务 A B 同时锁住一个间隙后,A 往当前间隙插入数据时会被 B 的间隙锁阻塞,B 也执行插入间隙数据的操作时就会**产生死锁** + +现场演示: + +* 关闭自动提交功能: + + ```mysql + SET AUTOCOMMIT=0; -- C1、C2 + ``` + +* 查询数据表: + + ```mysql + SELECT * FROM test_innodb_lock; + ``` + + ![](https://seazean.oss-cn-beijing.aliyuncs.com/img/DB/MySQL-InnoDB 间隙锁1.png) + +* C1 根据 id 范围更新数据,C2 插入数据: + + ```mysql + UPDATE test_innodb_lock SET name='8888' WHERE id < 4; -- C1 + INSERT INTO test_innodb_lock VALUES(2,'200','2'); -- C2 + ``` + + ![](https://seazean.oss-cn-beijing.aliyuncs.com/img/DB/MySQL-InnoDB 间隙锁2.png) + + 出现间隙锁,C2 被阻塞,等待 C1 提交事务后才能更新 + + + +**** + + + +##### 意向锁 + +InnoDB 为了支持多粒度的加锁,允许行锁和表锁同时存在,支持在不同粒度上的加锁操作,InnoDB 增加了意向锁(Intention Lock) + +意向锁是将锁定的对象分为多个层次,意向锁意味着事务希望在更细粒度上进行加锁,意向锁分为两种: + +* 意向共享锁(IS):事务有意向对表加共享锁 +* 意向排他锁(IX):事务有意向对表加排他锁 + +**IX,IS 是表级锁**,不会和行级的 X,S 锁发生冲突,意向锁是在加表级锁之前添加,为了在加表级锁时可以快速判断表中是否有记录被上锁,比如向一个表添加表级 X 锁的时: + +- 没有意向锁,则需要遍历整个表判断是否有锁定的记录 +- 有了意向锁,首先判断是否存在意向锁,然后判断该意向锁与即将添加的表级锁是否兼容即可,因为意向锁的存在代表有表级锁的存在或者即将有表级锁的存在 + +兼容性如下所示: + +![](https://seazean.oss-cn-beijing.aliyuncs.com/img/DB/MySQL-意向锁兼容性.png) + +**插入意向锁** Insert Intention Lock 是在插入一行记录操作之前设置的一种间隙锁,是行级锁 + +插入意向锁释放了一种插入信号,即多个事务在相同的索引间隙插入时如果不是插入相同的间隙位置就不需要互相等待。假设某列有索引,只要两个事务插入位置不同,如事务 A 插入 3,事务 B 插入 4,那么就可以同时插入 + + + +*** + + + +##### 自增锁 + +系统会自动给 AUTO_INCREMENT 修饰的列进行递增赋值,实现方式: + +* AUTO_INC 锁:表级锁,执行插入语句时会自动添加,在该语句执行完成后释放,并不是事务结束 +* 轻量级锁:为插入语句生成 AUTO_INCREMENT 修饰的列时获取该锁,生成以后释放掉,不需要等到插入语句执行完后释放 + +系统变量 `innodb_autoinc_lock_mode` 控制采取哪种方式: + +* 0:全部采用 AUTO_INC 锁 +* 1:全部采用轻量级锁 +* 2:混合使用,在插入记录的数量确定时采用轻量级锁,不确定时采用 AUTO_INC 锁 + + + +**** + + + +##### 隐式锁 + +一般情况下 INSERT 语句是不需要在内存中生成锁结构的,会进行隐式的加锁,保护的是插入后的安全 + +注意:如果插入的间隙被其他事务加了间隙锁,此次插入会被阻塞,并在该间隙插入一个插入意向锁 + +* 聚簇索引:索引记录有 trx_id 隐藏列,表示最后改动该记录的事务 id,插入数据后事务 id 就是当前事务。其他事务想获取该记录的锁时会判断当前记录的事务 id 是否是活跃的,如果不是就可以正常加锁;如果是就创建一个 X 的锁结构,该锁的 is_waiting 是 false,为自己的事务创建一个锁结构,is_waiting 是 true(类似 Java 中的锁升级) +* 二级索引:获取数据页 Page Header 中的 PAGE_MAX_TRX_ID 属性,代表修改当前页面的最大的事务 ID,如果小于当前活跃的最小事务 id,就证明插入该数据的事务已经提交,否则就需要获取到主键值进行回表操作 + +隐式锁起到了延迟生成锁的效果,如果其他事务与隐式锁没有冲突,就可以避免锁结构的生成,节省了内存资源 + +INSERT 在两种情况下会生成锁结构: + +* 重复键:在插入主键或唯一二级索引时遇到重复的键值会报错,在报错前需要对对应的聚簇索引进行加锁 + * 隔离级别 <= Read Uncommitted,加 S 型 Record Lock + * 隔离级别 >= Repeatable Read,加 S 型 next_key 锁 + +* 外键检查:如果待插入的记录在父表中可以找到,会对父表的记录加 S 型 Record Lock。如果待插入的记录在父表中找不到 + * 隔离级别 <= Read Committed,不加锁 + * 隔离级别 >= Repeatable Read,加间隙锁 + + + + + +*** + + + +#### 锁优化 + +##### 优化锁 + +InnoDB 存储引擎实现了行级锁定,虽然在锁定机制的实现方面带来了性能损耗可能比表锁会更高,但是在整体并发处理能力方面要远远优于 MyISAM 的表锁,当系统并发量较高的时候,InnoDB 的整体性能远远好于 MyISAM + +但是使用不当可能会让 InnoDB 的整体性能表现不仅不能比 MyISAM 高,甚至可能会更差 + +优化建议: + +- 尽可能让所有数据检索都能通过索引来完成,避免无索引行锁升级为表锁 +- 合理设计索引,尽量缩小锁的范围 +- 尽可能减少索引条件及索引范围,避免间隙锁 +- 尽量控制事务大小,减少锁定资源量和时间长度 +- 尽可使用低级别事务隔离(需要业务层面满足需求) + + + +**** + + + +##### 锁升级 + +索引失效造成**行锁升级为表锁**,不通过索引检索数据,全局扫描的过程中 InnoDB 会将对表中的所有记录加锁,实际效果和**表锁**一样,实际开发过程应避免出现索引失效的状况 + +* 查看当前表的索引: + + ```mysql + SHOW INDEX FROM test_innodb_lock; + ``` + +* 关闭自动提交功能: + + ```mysql + SET AUTOCOMMIT=0; -- C1、C2 + ``` + +* 执行更新语句: + + ```mysql + UPDATE test_innodb_lock SET sex='2' WHERE name=10; -- C1 + UPDATE test_innodb_lock SET sex='2' WHERE id=3; -- C2 + ``` + + ![](https://seazean.oss-cn-beijing.aliyuncs.com/img/DB/MySQL-InnoDB 锁升级.png) + + 索引失效:执行更新时 name 字段为 varchar 类型,造成索引失效,最终行锁变为表锁 + + + +*** + + + +##### 死锁 + +不同事务由于互相持有对方需要的锁而导致事务都无法继续执行的情况称为死锁 + +死锁情况:线程 A 修改了 id = 1 的数据,请求修改 id = 2 的数据,线程 B 修改了 id = 2 的数据,请求修改 id = 1 的数据,产生死锁 + +解决策略: + +* 直接进入等待直到超时,超时时间可以通过参数 innodb_lock_wait_timeout 来设置,默认 50 秒,但是时间的设置不好控制,超时可能不是因为死锁,而是因为事务处理比较慢,所以一般不采取该方式 + +* 主动死锁检测,发现死锁后**主动回滚死锁链条中较小的一个事务**,让其他事务得以继续执行,将参数 `innodb_deadlock_detect` 设置为 on,表示开启该功能(事务较小的意思就是事务执行过程中插入、删除、更新的记录条数) + + 死锁检测并不是每个语句都要检测,只有在加锁访问的行上已经有锁时,当前事务被阻塞了才会检测,也是从当前事务开始进行检测 + +通过执行 `SHOW ENGINE INNODB STATUS` 可以查看最近发生的一次死循环,全局系统变量 `innodb_print_all_deadlocks` 设置为 on,就可以将每个死锁信息都记录在 MySQL 错误日志中 + +死锁一般是行级锁,当表锁发生死锁时,会在事务中访问其他表时**直接报错**,破坏了持有并等待的死锁条件 + + + +*** + + + +#### 锁状态 + +查看锁信息 + +```mysql +SHOW STATUS LIKE 'innodb_row_lock%'; +``` + + + +参数说明: + +* Innodb_row_lock_current_waits:当前正在等待锁定的数量 + +* Innodb_row_lock_time:从系统启动到现在锁定总时间长度 + +* Innodb_row_lock_time_avg:每次等待所花平均时长 + +* Innodb_row_lock_time_max:从系统启动到现在等待最长的一次所花的时间 + +* Innodb_row_lock_waits:系统启动后到现在总共等待的次数 + +当等待的次数很高,而且每次等待的时长也不短的时候,就需要分析系统中为什么会有如此多的等待,然后根据分析结果制定优化计划 + +查看锁状态: + +```mysql +SELECT * FROM information_schema.innodb_locks; #锁的概况 +SHOW ENGINE INNODB STATUS\G; #InnoDB整体状态,其中包括锁的情况 +``` + +![](https://seazean.oss-cn-beijing.aliyuncs.com/img/DB/MySQL-InnoDB查看锁状态.png) + +lock_id 是锁 id;lock_trx_id 为事务 id;lock_mode 为 X 代表排它锁(写锁);lock_type 为 RECORD 代表锁为行锁(记录锁) + + + + + +*** + + + +### 乐观锁 + +悲观锁:在整个数据处理过程中,将数据处于锁定状态,为了保证事务的隔离性,就需要一致性锁定读。读取数据时给加锁,其它事务无法修改这些数据,修改删除数据时也加锁,其它事务同样无法读取这些数据 + +悲观锁和乐观锁使用前提: + +- 对于读的操作远多于写的操作的时候,一个更新操作加锁会阻塞所有的读取操作,降低了吞吐量,最后需要释放锁,锁是需要一些开销的,这时候可以选择乐观锁 +- 如果是读写比例差距不是非常大或者系统没有响应不及时,吞吐量瓶颈的问题,那就不要去使用乐观锁,它增加了复杂度,也带来了业务额外的风险,这时候可以选择悲观锁 + +乐观锁的实现方式:就是 CAS,比较并交换 + +* 版本号 + + 1. 给数据表中添加一个 version 列,每次更新后都将这个列的值加 1 + + 2. 读取数据时,将版本号读取出来,在执行更新的时候,比较版本号 + + 3. 如果相同则执行更新,如果不相同,说明此条数据已经发生了变化 + + 4. 用户自行根据这个通知来决定怎么处理,比如重新开始一遍,或者放弃本次更新 + + ```mysql + -- 创建city表 + CREATE TABLE city( + id INT PRIMARY KEY AUTO_INCREMENT, -- 城市id + NAME VARCHAR(20), -- 城市名称 + VERSION INT -- 版本号 + ); + + -- 添加数据 + INSERT INTO city VALUES (NULL,'北京',1),(NULL,'上海',1),(NULL,'广州',1),(NULL,'深圳',1); + + -- 修改北京为北京市 + -- 1.查询北京的version + SELECT VERSION FROM city WHERE NAME='北京'; + -- 2.修改北京为北京市,版本号+1。并对比版本号 + UPDATE city SET NAME='北京市',VERSION=VERSION+1 WHERE NAME='北京' AND VERSION=1; + ``` + +* 时间戳 + + - 和版本号方式基本一样,给数据表中添加一个列,名称无所谓,数据类型需要是 **timestamp** + - 每次更新后都将最新时间插入到此列 + - 读取数据时,将时间读取出来,在执行更新的时候,比较时间 + - 如果相同则执行更新,如果不相同,说明此条数据已经发生了变化 + +乐观锁的异常情况:如果 version 被其他事务抢先更新,则在当前事务中更新失败,trx_id 没有变成当前事务的 ID,当前事务再次查询还是旧值,就会出现**值没变但是更新不了**的现象(anomaly) + +解决方案:每次 CAS 更新不管成功失败,就结束当前事务;如果失败则重新起一个事务进行查询更新 + + + + + +*** + + + + + +## 主从 + +### 基本介绍 + +主从复制是指将主数据库的 DDL 和 DML 操作通过二进制日志传到从库服务器中,然后在从库上对这些日志重新执行(也叫重做),从而使得从库和主库的数据保持同步 + +MySQL 支持一台主库同时向多台从库进行复制,从库同时也可以作为其他从服务器的主库,实现链状复制 + +MySQL 复制的优点主要包含以下三个方面: + +- 主库出现问题,可以快速切换到从库提供服务 + +- 可以在从库上执行查询操作,从主库中更新,实现读写分离 + +- 可以在从库中执行备份,以避免备份期间影响主库的服务(备份时会加全局读锁) + + + +*** + + + +### 主从复制 + +#### 主从结构 + +MySQL 的主从之间维持了一个**长连接**。主库内部有一个线程,专门用于服务从库的长连接,连接过程: + +* 从库执行 change master 命令,设置主库的 IP、端口、用户名、密码以及要从哪个位置开始请求 binlog,这个位置包含文件名和日志偏移量 +* 从库执行 start slave 命令,这时从库会启动两个线程,就是图中的 io_thread 和 sql_thread,其中 io_thread 负责与主库建立连接 +* 主库校验完用户名、密码后,开始按照从传过来的位置,从本地读取 binlog 发给从库,开始主从复制 + +主从复制原理图: + +![](https://seazean.oss-cn-beijing.aliyuncs.com/img/DB/MySQL-主从复制原理图.jpg) + +主从复制主要依赖的是 binlog,MySQL 默认是异步复制,需要三个线程: + +- binlog thread:在主库事务提交时,把数据变更记录在日志文件 binlog 中,并通知 slave 有数据更新 +- I/O thread:负责从主服务器上**拉取二进制日志**,并将 binlog 日志内容依次写到 relay log 中转日志的最末端,并将新的 binlog 文件名和 offset 记录到 master-info 文件中,以便下一次读取日志时从指定 binlog 日志文件及位置开始读取新的 binlog 日志内容 +- SQL thread:监测本地 relay log 中新增了日志内容,读取中继日志并重做其中的 SQL 语句,从库在 relay-log.info 中记录当前应用中继日志的文件名和位点以便下一次执行 + +同步与异步: + +* 异步复制有数据丢失风险,例如数据还未同步到从库,主库就给客户端响应,然后主库挂了,此时从库晋升为主库的话数据是缺失的 +* 同步复制,主库需要将 binlog 复制到所有从库,等所有从库响应了之后主库才进行其他逻辑,这样的话性能很差,一般不会选择 +* MySQL 5.7 之后出现了半同步复制,有参数可以选择成功同步几个从库就返回响应 + + + +**** + + + +#### 主主结构 + +主主结构就是两个数据库之间总是互为主从关系,这样在切换的时候就不用再修改主从关系 + +循环复制:在库 A 上更新了一条语句,然后把生成的 binlog 发给库 B,库 B 执行完这条更新语句后也会生成 binlog,会再发给 A + +解决方法: + +* 两个库的 server id 必须不同,如果相同则它们之间不能设定为主主关系 +* 一个库接到 binlog 并在重放的过程中,生成与原 binlog 的 server id 相同的新的 binlog +* 每个库在收到从主库发过来的日志后,先判断 server id,如果跟自己的相同,表示这个日志是自己生成的,就直接丢弃这个日志 + + + +*** + + + +### 主从延迟 + +#### 延迟原因 + +正常情况主库执行更新生成的所有 binlog,都可以传到从库并被正确地执行,从库就能达到跟主库一致的状态,这就是最终一致性 + +主从延迟是主从之间是存在一定时间的数据不一致,就是同一个事务在从库执行完成的时间和主库执行完成的时间的差值,即 T2-T1 + +- 主库 A 执行完成一个事务,写入 binlog,该时刻记为 T1 +- 日志传给从库 B,从库 B 执行完这个事务,该时刻记为 T2 + +通过在从库执行 `show slave status` 命令,返回结果会显示 seconds_behind_master 表示当前从库延迟了多少秒 + +- 每一个事务的 binlog 都有一个时间字段,用于记录主库上写入的时间 +- 从库取出当前正在执行的事务的时间字段,跟系统的时间进行相减,得到的就是 seconds_behind_master + +主从延迟的原因: + +* 从库的机器性能比主库的差,导致从库的复制能力弱 +* 从库的查询压力大,建立一主多从的结构 +* 大事务的执行,主库必须要等到事务完成之后才会写入 binlog,导致从节点出现应用 binlog 延迟 +* 主库的 DDL,从库与主库的 DDL 同步是串行进行,DDL 在主库执行时间很长,那么从库也会消耗同样的时间 +* 锁冲突问题也可能导致从节点的 SQL 线程执行慢 + +主从同步问题永远都是**一致性和性能的权衡**,需要根据实际的应用场景,可以采取下面的办法: + +* 优化 SQL,避免慢 SQL,减少批量操作 +* 降低多线程大事务并发的概率,优化业务逻辑 +* 业务中大多数情况查询操作要比更新操作更多,搭建**一主多从**结构,让这些从库来分担读的压力 + +* 尽量采用短的链路,主库和从库服务器的距离尽量要短,提升端口带宽,减少 binlog 传输的网络延时 +* 实时性要求高的业务读强制走主库,从库只做备份 + + + +*** + + + +#### 并行复制 + +##### MySQL5.6 + +高并发情况下,主库的会产生大量的 binlog,在从库中有两个线程 IO Thread 和 SQL Thread 单线程执行,会导致主库延迟变大。为了改善复制延迟问题,MySQL 5.6 版本增加了并行复制功能,以采用多线程机制来促进执行 + +coordinator 就是原来的 SQL Thread,并行复制中它不再直接更新数据,**只负责读取中转日志和分发事务**: + +* 线程分配完成并不是立即执行,为了防止造成更新覆盖,更新同一 DB 的两个事务必须被分发到同一个工作线程 +* 同一个事务不能被拆开,必须放到同一个工作线程 + +MySQL 5.6 版本的策略:每个线程对应一个 hash 表,用于保存当前这个线程的执行队列里的事务所涉及的表,hash 表的 key 是数据库名,value 是一个数字,表示队列中有多少个事务修改这个库,适用于主库上有多个 DB 的情况 + +每个事务在分发的时候,跟线程的**冲突**(事务操作的是同一个库)关系包括以下三种情况: + +* 如果跟所有线程都不冲突,coordinator 线程就会把这个事务分配给最空闲的线程 +* 如果只跟一个线程冲突,coordinator 线程就会把这个事务分配给这个存在冲突关系的线程 +* 如果跟多于一个线程冲突,coordinator 线程就进入等待状态,直到和这个事务存在冲突关系的线程只剩下 1 个 + +优缺点: + +* 构造 hash 值的时候很快,只需要库名,而且一个实例上 DB 数也不会很多,不会出现需要构造很多项的情况 +* 不要求 binlog 的格式,statement 格式的 binlog 也可以很容易拿到库名(日志章节详解了 binlog) +* 主库上的表都放在同一个 DB 里面,这个策略就没有效果了;或者不同 DB 的热点不同,比如一个是业务逻辑库,一个是系统配置库,那也起不到并行的效果,需要**把相同热度的表均匀分到这些不同的 DB 中**,才可以使用这个策略 + + + +*** + + + +##### MySQL5.7 + +MySQL 5.7 由参数 slave-parallel-type 来控制并行复制策略: + +* 配置为 DATABASE,表示使用 MySQL 5.6 版本的**按库(DB)并行策略** +* 配置为 LOGICAL_CLOCK,表示的**按提交状态并行**执行 + +按提交状态并行复制策略的思想是: + +* 所有处于 commit 状态的事务可以并行执行;同时处于 prepare 状态的事务,在从库执行时是可以并行的 +* 处于 prepare 状态的事务,与处于 commit 状态的事务之间,在从库执行时也是可以并行的 + +MySQL 5.7.22 版本里,MySQL 增加了一个新的并行复制策略,基于 WRITESET 的并行复制,新增了一个参数 binlog-transaction-dependency-tracking,用来控制是否启用这个新策略: + +* COMMIT_ORDER:表示根据同时进入 prepare 和 commit 来判断是否可以并行的策略 + +* WRITESET:表示的是对于每个事务涉及更新的每一行,计算出这一行的 hash 值,组成该事务的 writeset 集合,如果两个事务没有操作相同的行,也就是说它们的 writeset 没有交集,就可以并行(**按行并行**) + + 为了唯一标识,这个 hash 表的值是通过 `库名 + 表名 + 索引名 + 值`(表示的是某一行)计算出来的 + +* WRITESET_SESSION:是在 WRITESET 的基础上多了一个约束,即在主库上同一个线程先后执行的两个事务,在备库执行的时候,要保证相同的先后顺序 + + +MySQL 5.7.22 按行并发的优势: + +* writeset 是在主库生成后直接写入到 binlog 里面的,这样在备库执行的时候,不需要解析 binlog 内容,节省了计算量 +* 不需要把整个事务的 binlog 都扫一遍才能决定分发到哪个线程,更省内存 +* 从库的分发策略不依赖于 binlog 内容,所以 binlog 是 statement 格式也可以,更节约内存(因为 row 才记录更改的行) + +MySQL 5.7.22 的并行复制策略在通用性上是有保证的,但是对于表上没主键、唯一和外键约束的场景,WRITESET 策略也是没法并行的,也会暂时退化为单线程模型 + + + +参考文章:https://time.geekbang.org/column/article/77083 + + + +*** + + + +### 读写分离 + +#### 读写延迟 + +读写分离:可以降低主库的访问压力,提高系统的并发能力 + +* 主库不建查询的索引,从库建查询的索引。因为索引需要维护的,比如插入一条数据,不仅要在聚簇索引上面插入,对应的二级索引也得插入 +* 将读操作分到从库了之后,可以在主库把查询要用的索引删了,减少写操作对主库的影响 + +读写分离产生了读写延迟,造成数据的不一致性。假如客户端执行完一个更新事务后马上发起查询,如果查询选择的是从库的话,可能读到的还是以前的数据,叫过期读 + +解决方案: + +* 强制将写之后**立刻读的操作转移到主库**,比如刚注册的用户,直接登录从库查询可能查询不到,先走主库登录 +* **二次查询**,如果从库查不到数据,则再去主库查一遍,由 API 封装,比较简单,但导致主库压力大 +* 更新主库后,读从库之前先 sleep 一下,类似于执行一条 `select sleep(1)` 命令,大多数情况下主备延迟在 1 秒之内 + + + +*** + + + +#### 确保机制 + +##### 无延迟 + +确保主备无延迟的方法: + +* 每次从库执行查询请求前,先判断 seconds_behind_master 是否已经等于 0,如果不等于那就等到参数变为 0 执行查询请求 +* 对比位点,Master_Log_File 和 Read_Master_Log_Pos 表示的是读到的主库的最新位点,Relay_Master_Log_File 和 Exec_Master_Log_Pos 表示的是备库执行的最新位点,这两组值完全相同就说明接收到的日志已经同步完成 +* 对比 GTID 集合,Retrieved_Gtid_Set 是备库收到的所有日志的 GTID 集合,Executed_Gtid_Set 是备库所有已经执行完成的 GTID 集合,如果这两个集合相同也表示备库接收到的日志都已经同步完成 + + + +*** + + + +##### 半同步 + +半同步复制就是 semi-sync replication,适用于一主一备的场景,工作流程: + +* 事务提交的时候,主库把 binlog 发给从库 +* 从库收到 binlog 以后,发回给主库一个 ack,表示收到了 +* 主库收到这个 ack 以后,才能给客户端返回事务完成的确认 + +在一主多从场景中,主库只要等到一个从库的 ack,就开始给客户端返回确认,这时在从库上执行查询请求,有两种情况: + +* 如果查询是落在这个响应了 ack 的从库上,是能够确保读到最新数据 +* 如果查询落到其他从库上,它们可能还没有收到最新的日志,就会产生过期读的问题 + +在业务更新的高峰期,主库的位点或者 GTID 集合更新很快,导致从库来不及处理,那么两个位点等值判断就会一直不成立,很可能出现从库上迟迟无法响应查询请求的情况 + + + +**** + + + +##### 等位点 + +在**从库执行判断位点**的命令,参数 file 和 pos 指的是主库上的文件名和位置,timeout 可选,设置为正整数 N 表示最多等待 N 秒 + +```mysql +SELECT master_pos_wait(file, pos[, timeout]); +``` + +命令正常返回的结果是一个正整数 M,表示从命令开始执行,到应用完 file 和 pos 表示的 binlog 位置,执行了多少事务 + +* 如果执行期间,备库同步线程发生异常,则返回 NULL +* 如果等待超过 N 秒,就返回 -1 +* 如果刚开始执行的时候,就发现已经执行过这个位置了,则返回 0 + +工作流程:先执行 trx1,再执行一个查询请求的逻辑,要**保证能够查到正确的数据** + +* trx1 事务更新完成后,马上执行 `show master status` 得到当前主库执行到的 File 和 Position +* 选定一个从库执行判断位点语句,如果返回值是 >=0 的正整数,说明从库已经同步完事务,可以在这个从库执行查询语句 +* 如果出现其他情况,需要到主库执行查询语句 + +注意:如果所有的从库都延迟超过 timeout 秒,查询压力就都跑到主库上,所以需要进行权衡 + + + +*** + + + +##### 等GTID + +数据库开启了 GTID 模式,MySQL 提供了判断 GTID 的命令 + +```mysql +SELECT wait_for_executed_gtid_set(gtid_set [, timeout]) +``` + +* 等待直到这个库执行的事务中包含传入的 gtid_set,返回 0 +* 超时返回 1 + +工作流程:先执行 trx1,再执行一个查询请求的逻辑,要保证能够查到正确的数据 + +* trx1 事务更新完成后,从返回包直接获取这个事务的 GTID,记为 gtid +* 选定一个从库执行查询语句,如果返回值是 0,则在这个从库执行查询语句,否则到主库执行查询语句 + +对比等待位点方法,减少了一次 `show master status` 的方法,将参数 session_track_gtids 设置为 OWN_GTID,然后通过 API 接口 mysql_session_track_get_first 从返回包解析出 GTID 的值即可 + +总结:所有的等待无延迟的方法,都需要根据具体的业务场景去判断实施 + + + +参考文章:https://time.geekbang.org/column/article/77636 + + + +*** + + + +#### 负载均衡 + +负载均衡是应用中使用非常普遍的一种优化方法,机制就是利用某种均衡算法,将固定的负载量分布到不同的服务器上,以此来降低单台服务器的负载,达到优化的效果 + +* 分流查询:通过 MySQL 的主从复制,实现读写分离,使增删改操作走主节点,查询操作走从节点,从而可以降低单台服务器的读写压力 + + ![](https://seazean.oss-cn-beijing.aliyuncs.com/img/DB/MySQL-负载均衡主从复制.jpg) + +* 分布式数据库架构:适合大数据量、负载高的情况,具有良好的拓展性和高可用性。通过在多台服务器之间分布数据,可以实现在多台服务器之间的负载均衡,提高访问效率 + + + +**** + + + +### 主从搭建 + +#### master + +1. 在master 的配置文件(/etc/mysql/my.cnf)中,配置如下内容: + + ```sh + #mysql 服务ID,保证整个集群环境中唯一 + server-id=1 + + #mysql binlog 日志的存储路径和文件名 + log-bin=/var/lib/mysql/mysqlbin + + #错误日志,默认已经开启 + #log-err + + #mysql的安装目录 + #basedir + + #mysql的临时目录 + #tmpdir + + #mysql的数据存放目录 + #datadir + + #是否只读,1 代表只读, 0 代表读写 + read-only=0 + + #忽略的数据, 指不需要同步的数据库 + binlog-ignore-db=mysql + + #指定同步的数据库 + #binlog-do-db=db01 + ``` + +2. 执行完毕之后,需要重启 MySQL + +3. 创建同步数据的账户,并且进行授权操作: + + ```mysql + GRANT REPLICATION SLAVE ON *.* TO 'seazean'@'192.168.0.137' IDENTIFIED BY '123456'; + FLUSH PRIVILEGES; + ``` + +4. 查看 master 状态: + + ```mysql + SHOW MASTER STATUS; + ``` + + ![](https://seazean.oss-cn-beijing.aliyuncs.com/img/DB/MySQL-查看master状态.jpg) + + * File:从哪个日志文件开始推送日志文件 + * Position:从哪个位置开始推送日志 + * Binlog_Ignore_DB:指定不需要同步的数据库 + + + +*** + + + +#### slave + +1. 在 slave 端配置文件中,配置如下内容: + + ```sh + #mysql服务端ID,唯一 + server-id=2 + + #指定binlog日志 + log-bin=/var/lib/mysql/mysqlbin + ``` + +2. 执行完毕之后,需要重启 MySQL + +3. 指定当前从库对应的主库的IP地址、用户名、密码,从哪个日志文件开始的那个位置开始同步推送日志 + + ```mysql + CHANGE MASTER TO MASTER_HOST= '192.168.0.138', MASTER_USER='seazean', MASTER_PASSWORD='seazean', MASTER_LOG_FILE='mysqlbin.000001', MASTER_LOG_POS=413; + ``` + +4. 开启同步操作: + + ```mysql + START SLAVE; + SHOW SLAVE STATUS; + ``` + +5. 停止同步操作: + + ```mysql + STOP SLAVE; + ``` + + + +*** + + + +#### 验证 + +1. 在主库中创建数据库,创建表并插入数据: + + ```mysql + CREATE DATABASE db01; + USE db01; + CREATE TABLE user( + id INT(11) NOT NULL AUTO_INCREMENT, + name VARCHAR(50) NOT NULL, + sex VARCHAR(1), + PRIMARY KEY (id) + )ENGINE=INNODB DEFAULT CHARSET=utf8; + + INSERT INTO user(id,NAME,sex) VALUES(NULL,'Tom','1'); + INSERT INTO user(id,NAME,sex) VALUES(NULL,'Trigger','0'); + INSERT INTO user(id,NAME,sex) VALUES(NULL,'Dawn','1'); + ``` + +2. 在从库中查询数据,进行验证: + + 在从库中,可以查看到刚才创建的数据库: + + ![](https://seazean.oss-cn-beijing.aliyuncs.com/img/DB/MySQL-主从复制验证1.jpg) + + 在该数据库中,查询表中的数据: + + ![](https://seazean.oss-cn-beijing.aliyuncs.com/img/DB/MySQL-主从复制验证2.jpg) + + + +*** + + + +### 主从切换 + +#### 正常切换 + +正常切换步骤: + +* 在开始切换之前先对主库进行锁表 `flush tables with read lock`,然后等待所有语句执行完成,切换完成后可以释放锁 + +* 检查 slave 同步状态,在 slave 执行 `show processlist` + +* 停止 slave io 线程,执行命令 `STOP SLAVE IO_THREAD` + +* 提升 slave 为 master + + ```sql + Stop slave; + Reset master; + Reset slave all; + set global read_only=off; -- 设置为可更新状态 + ``` + +* 将原来 master 变为 slave(参考搭建流程中的 slave 方法) + +**可靠性优先策略**: + +* 判断备库 B 现在的 seconds_behind_master,如果小于某个值(比如 5 秒)继续下一步,否则持续重试这一步 +* 把主库 A 改成只读状态,即把 readonly 设置为 true +* 判断备库 B 的 seconds_behind_master 的值,直到这个值变成 0 为止(该步骤比较耗时,所以步骤 1 中要尽量等待该值变小) +* 把备库 B 改成可读写状态,也就是把 readonly 设置为 false +* 把业务请求切到备库 B + +可用性优先策略:先做最后两步,会造成主备数据不一致的问题 + + + +参考文章:https://time.geekbang.org/column/article/76795 + + + +*** + + + +#### 健康检测 + +主库发生故障后从库会上位,**其他从库指向新的主库**,所以需要一个健康检测的机制来判断主库是否宕机 + +* select 1 判断,但是高并发下检测不出线程的锁等待的阻塞问题 + +* 查表判断,在系统库(mysql 库)里创建一个表,比如命名为 health_check,里面只放一行数据,然后定期执行。但是当 binlog 所在磁盘的空间占用率达到 100%,所有的更新和事务提交语句都被阻塞,查询语句可以继续运行 + +* 更新判断,在健康检测表中放一个 timestamp 字段,用来表示最后一次执行检测的时间 + + ```mysql + UPDATE mysql.health_check SET t_modified=now(); + ``` + + 节点可用性的检测都应该包含主库和备库,为了让主备之间的更新不产生冲突,可以在 mysql.health_check 表上存入多行数据,并用主备的 server_id 做主键,保证主、备库各自的检测命令不会发生冲突 + + + +*** + + + + + +#### 基于位点 + +主库上位后,从库 B 执行 CHANGE MASTER TO 命令,指定 MASTER_LOG_FILE、MASTER_LOG_POS 表示从新主库 A 的哪个文件的哪个位点开始同步,这个位置就是**同步位点**,对应主库的文件名和日志偏移量 + +寻找位点需要找一个稍微往前的,然后再通过判断跳过那些在从库 B 上已经执行过的事务,获取位点方法: + +* 等待新主库 A 把中转日志(relay log)全部同步完成 +* 在 A 上执行 show master status 命令,得到当前 A 上最新的 File 和 Position +* 取原主库故障的时刻 T,用 mysqlbinlog 工具解析新主库 A 的 File,得到 T 时刻的位点 + +通常情况下该值并不准确,在切换的过程中会发生错误,所以要先主动跳过这些错误: + +* 切换过程中,可能会重复执行一个事务,所以需要主动跳过所有重复的事务 + + ```mysql + SET GLOBAL sql_slave_skip_counter=1; + START SLAVE; + ``` + +* 设置 slave_skip_errors 参数,直接设置跳过指定的错误,保证主从切换的正常进行 + + * 1062 错误是插入数据时唯一键冲突 + * 1032 错误是删除数据时找不到行 + + 该方法针对的是主备切换时,由于找不到精确的同步位点,只能采用这种方法来创建从库和新主库的主备关系。等到主备间的同步关系建立完成并稳定执行一段时间后,还需要把这个参数设置为空,以免真的出现了主从数据不一致也跳过了 + + + +**** + + + +#### 基于GTID + +##### GTID + +GTID 的全称是 Global Transaction Identifier,全局事务 ID,是一个事务**在提交时生成**的,是这个事务的唯一标识,组成: + +```mysql +GTID=source_id:transaction_id +``` + +* source_id:是一个实例第一次启动时自动生成的,是一个全局唯一的值 +* transaction_id:初始值是 1,每次提交事务的时候分配给这个事务,并加 1,是连续的(区分事务 ID,事务 ID 是在执行时生成) + +启动 MySQL 实例时,加上参数 `gtid_mode=on` 和 `enforce_gtid_consistency=on` 就可以启动 GTID 模式,每个事务都会和一个 GTID 一一对应,每个 MySQL 实例都维护了一个 GTID 集合,用来存储当前实例**执行过的所有事务** + +GTID 有两种生成方式,使用哪种方式取决于 session 变量 gtid_next: + +* `gtid_next=automatic`:使用默认值,把 source_id:transaction_id (递增)分配给这个事务,然后加入本实例的 GTID 集合 + + ```mysql + @@SESSION.GTID_NEXT = 'source_id:transaction_id'; + ``` + +* `gtid_next=GTID`:指定的 GTID 的值,如果该值已经存在于实例的 GTID 集合中,接下来执行的事务会直接被系统忽略;反之就将该值分配给接下来要执行的事务,系统不需要给这个事务生成新的 GTID,也不用加 1 + + 注意:一个 GTID 只能给一个事务使用,所以执行下一个事务,要把 gtid_next 设置成另外一个 GTID 或者 automatic + +业务场景: + +* 主库 X 和从库 Y 执行一条相同的指令后进行事务同步 + + ```mysql + INSERT INTO t VALUES(1,1); + ``` + +* 当 Y 同步 X 时,会出现主键冲突,导致实例 X 的同步线程停止,解决方法: + + ```mysql + SET gtid_next='(这里是主库 X 的 GTID 值)'; + BEGIN; + COMMIT; + SET gtid_next=automatic; + START SLAVE; + ``` + + 前三条语句通过**提交一个空事务**,把 X 的 GTID 加到实例 Y 的 GTID 集合中,实例 Y 就会直接跳过这个事务 + + + +**** + + + +##### 切换 + +在 GTID 模式下,CHANGE MASTER TO 不需要指定日志名和日志偏移量,指定 `master_auto_position=1` 代表使用 GTID 模式 + +新主库实例 A 的 GTID 集合记为 set_a,从库实例 B 的 GTID 集合记为 set_b,主备切换逻辑: + +* 实例 B 指定主库 A,基于主备协议建立连接,实例 B 并把 set_b 发给主库 A +* 实例 A 算出 set_a 与 set_b 的差集,就是所有存在于 set_a 但不存在于 set_b 的 GTID 的集合,判断 A 本地是否包含了这个**差集**需要的所有 binlog 事务 + * 如果不包含,表示 A 已经把实例 B 需要的 binlog 给删掉了,直接返回错误 + * 如果确认全部包含,A 从自己的 binlog 文件里面,找出第一个不在 set_b 的事务,发给 B +* 实例 A 之后就从这个事务开始,往后读文件,按顺序取 binlog 发给 B 去执行 + + + +参考文章:https://time.geekbang.org/column/article/77427 + + + +*** + + + + + +## 日志 + +### 日志分类 + +在任何一种数据库中,都会有各种各样的日志,记录着数据库工作的过程,可以帮助数据库管理员追踪数据库曾经发生过的各种事件 + +MySQL日志主要包括六种: + +1. 重做日志(redo log) +2. 回滚日志(undo log) +3. 归档日志(binlog)(二进制日志) +4. 错误日志(errorlog) +5. 慢查询日志(slow query log) +6. 一般查询日志(general log) +7. 中继日志(relay log) + + + +*** + + + +### 错误日志 + +错误日志是 MySQL 中最重要的日志之一,记录了当 mysqld 启动和停止时,以及服务器在运行过程中发生任何严重错误时的相关信息。当数据库出现任何故障导致无法正常使用时,可以首先查看此日志 + +该日志是默认开启的,默认位置是:`/var/log/mysql/error.log` + +查看指令: + +```mysql +SHOW VARIABLES LIKE 'log_error%'; +``` + +查看日志内容: + +```sh +tail -f /var/log/mysql/error.log +``` + + + +*** + + + +### 归档日志 + +#### 基本介绍 + +归档日志(BINLOG)也叫二进制日志,是因为采用二进制进行存储,记录了所有的 DDL(数据定义语言)语句和 DML(数据操作语言)语句,但**不包括数据查询语句,在事务提交前的最后阶段写入** + +作用:**灾难时的数据恢复和 MySQL 的主从复制** + +归档日志默认情况下是没有开启的,需要在 MySQL 配置文件中开启,并配置 MySQL 日志的格式: + +```sh +cd /etc/mysql +vim my.cnf + +# 配置开启binlog日志, 日志的文件前缀为 mysqlbin -----> 生成的文件名如: mysqlbin.000001 +log_bin=mysqlbin +# 配置二进制日志的格式 +binlog_format=STATEMENT +``` + +日志存放位置:配置时给定了文件名但是没有指定路径,日志默认写入MySQL 的数据目录 + +日志格式: + +* STATEMENT:该日志格式在日志文件中记录的都是 **SQL 语句**,每一条对数据进行修改的 SQL 都会记录在日志文件中,通过 mysqlbinlog 工具,可以查看到每条语句的文本。主从复制时,从库会将日志解析为原语句,并在从库重新执行一遍 + + 缺点:可能会导致主备不一致,因为记录的 SQL 在不同的环境中可能选择的索引不同,导致结果不同 +* ROW:该日志格式在日志文件中记录的是每一行的**数据变更**,而不是记录 SQL 语句。比如执行 SQL 语句 `update tb_book set status='1'`,如果是 STATEMENT,在日志中会记录一行 SQL 语句; 如果是 ROW,由于是对全表进行更新,就是每一行记录都会发生变更,ROW 格式的日志中会记录每一行的数据变更 + + 缺点:记录的数据比较多,占用很多的存储空间 + +* MIXED:这是 MySQL 默认的日志格式,混合了STATEMENT 和 ROW 两种格式,MIXED 格式能尽量利用两种模式的优点,而避开它们的缺点 + + + +*** + + + +#### 日志刷盘 + +事务执行过程中,先将日志写(write)到 binlog cache,事务提交时再把 binlog cache 写(fsync)到 binlog 文件中,一个事务的 binlog 是不能被拆开的,所以不论这个事务多大也要确保一次性写入 + +事务提交时执行器把 binlog cache 里的完整事务写入到 binlog 中,并清空 binlog cache + +write 和 fsync 的时机由参数 sync_binlog 控制的: + +* sync_binlog=0:表示每次提交事务都只 write,不 fsync +* sync_binlog=1:表示每次提交事务都会执行 fsync +* sync_binlog=N(N>1):表示每次提交事务都 write,但累积 N 个事务后才 fsync,但是如果主机发生异常重启,会丢失最近 N 个事务的 binlog 日志 + + + +*** + + + +#### 日志读取 + +日志文件存储位置:/var/lib/mysql + +由于日志以二进制方式存储,不能直接读取,需要用 mysqlbinlog 工具来查看,语法如下: + +```sh +mysqlbinlog log-file; +``` + +查看 STATEMENT 格式日志: + +* 执行插入语句: + + ```mysql + INSERT INTO tb_book VALUES(NULL,'Lucene','2088-05-01','0'); + ``` + +* `cd /var/lib/mysql`: + + ```sh + -rw-r----- 1 mysql mysql 177 5月 23 21:08 mysqlbin.000001 + -rw-r----- 1 mysql mysql 18 5月 23 21:04 mysqlbin.index + ``` + + mysqlbin.index:该文件是日志索引文件 , 记录日志的文件名; + + mysqlbing.000001:日志文件 + +* 查看日志内容: + + ```sh + mysqlbinlog mysqlbing.000001; + ``` + + ![](https://seazean.oss-cn-beijing.aliyuncs.com/img/DB/MySQL-日志读取1.png) + + 日志结尾有 COMMIT + +查看 ROW 格式日志: + +* 修改配置: + + ```sh + # 配置二进制日志的格式 + binlog_format=ROW + ``` + +* 插入数据: + + ```mysql + INSERT INTO tb_book VALUES(NULL,'SpringCloud实战','2088-05-05','0'); + ``` + +* 查看日志内容:日志格式 ROW,直接查看数据是乱码,可以在 mysqlbinlog 后面加上参数 -vv + + ```mysql + mysqlbinlog -vv mysqlbin.000002 + ``` + + ![](https://seazean.oss-cn-beijing.aliyuncs.com/img/DB/MySQL-日志读取2.png) + + + + + +*** + + + +#### 日志删除 + +对于比较繁忙的系统,生成日志量大,这些日志如果长时间不清除,将会占用大量的磁盘空间,需要删除日志 + +* Reset Master 指令删除全部 binlog 日志,删除之后,日志编号将从 xxxx.000001重新开始 + + ```mysql + Reset Master -- MySQL指令 + ``` + +* 执行指令 `PURGE MASTER LOGS TO 'mysqlbin.***`,该命令将删除 ` ***` 编号之前的所有日志 + +* 执行指令 `PURGE MASTER LOGS BEFORE 'yyyy-mm-dd hh:mm:ss'` ,该命令将删除日志为 `yyyy-mm-dd hh:mm:ss` 之前产生的日志 + +* 设置参数 `--expire_logs_days=#`,此参数的含义是设置日志的过期天数,过了指定的天数后日志将会被自动删除,这样做有利于减少管理日志的工作量,配置 my.cnf 文件: + + ```sh + log_bin=mysqlbin + binlog_format=ROW + --expire_logs_days=3 + ``` + + + + + +**** + + + +#### 数据恢复 + +误删库或者表时,需要根据 binlog 进行数据恢复 + +一般情况下数据库有定时的全量备份,假如每天 0 点定时备份,12 点误删了库,恢复流程: + +* 取最近一次全量备份,用备份恢复出一个临时库 +* 从日志文件中取出凌晨 0 点之后的日志 +* 把除了误删除数据的语句外日志,全部应用到临时库 + +跳过误删除语句日志的方法: + +* 如果原实例没有使用 GTID 模式,只能在应用到包含 12 点的 binlog 文件的时候,先用 –stop-position 参数执行到误操作之前的日志,然后再用 –start-position 从误操作之后的日志继续执行 +* 如果实例使用了 GTID 模式,假设误操作命令的 GTID 是 gtid1,那么只需要提交一个空事务先将这个 GTID 加到临时实例的 GTID 集合,之后按顺序执行 binlog 的时就会自动跳过误操作的语句 + + + +*** + + + +### 查询日志 + +查询日志中记录了客户端的所有操作语句,而二进制日志不包含查询数据的 SQL 语句 + +默认情况下,查询日志是未开启的。如果需要开启查询日志,配置 my.cnf: + +```sh +# 该选项用来开启查询日志,可选值0或者1,0代表关闭,1代表开启 +general_log=1 +# 设置日志的文件名,如果没有指定,默认的文件名为host_name.log,存放在/var/lib/mysql +general_log_file=mysql_query.log +``` + +配置完毕之后,在数据库执行以下操作: + +```mysql +SELECT * FROM tb_book; +SELECT * FROM tb_book WHERE id = 1; +UPDATE tb_book SET name = 'lucene入门指南' WHERE id = 5; +SELECT * FROM tb_book WHERE id < 8 +``` + +执行完毕之后, 再次来查询日志文件: + +![](https://seazean.oss-cn-beijing.aliyuncs.com/img/DB/MySQL-查询日志.png) + + + +*** + + + +### 慢日志 + +慢查询日志记录所有执行时间超过 long_query_time 并且扫描记录数不小于 min_examined_row_limit 的所有的 SQL 语句的日志long_query_time 默认为 10 秒,最小为 0, 精度到微秒 + +慢查询日志默认是关闭的,可以通过两个参数来控制慢查询日志,配置文件 `/etc/mysql/my.cnf`: + +```sh +# 该参数用来控制慢查询日志是否开启,可选值0或者1,0代表关闭,1代表开启 +slow_query_log=1 + +# 该参数用来指定慢查询日志的文件名,存放在 /var/lib/mysql +slow_query_log_file=slow_query.log + +# 该选项用来配置查询的时间限制,超过这个时间将认为值慢查询,将需要进行日志记录,默认10s +long_query_time=10 +``` + +日志读取: + +* 直接通过 cat 指令查询该日志文件: + + ```sh + cat slow_query.log + ``` + + ![](https://seazean.oss-cn-beijing.aliyuncs.com/img/DB/MySQL-慢日志读取1.png) + +* 如果慢查询日志内容很多,直接查看文件比较繁琐,可以借助 mysql 自带的 mysqldumpslow 工具对慢查询日志进行分类汇总: + + ```sh + mysqldumpslow slow_query.log + ``` + + ![](https://seazean.oss-cn-beijing.aliyuncs.com/img/DB/MySQL-慢日志读取2.png) + + + + + +*** + + + +## 范式 + +### 第一范式 + +建立科学的,**规范的数据表**就需要满足一些规则来优化数据的设计和存储,这些规则就称为范式 + +**1NF:**数据库表的每一列都是不可分割的原子数据项,不能是集合、数组等非原子数据项。即表中的某个列有多个值时,必须拆分为不同的列。简而言之,**第一范式每一列不可再拆分,称为原子性** + +基本表: + +![](https://seazean.oss-cn-beijing.aliyuncs.com/img/DB/普通表.png) + + +第一范式表: + +![](https://seazean.oss-cn-beijing.aliyuncs.com/img/DB/第一范式.png) + + + + + +**** + + + +### 第二范式 + +**2NF:**在满足第一范式的基础上,非主属性完全依赖于主码(主关键字、主键),消除非主属性对主码的部分函数依赖。简而言之,**表中的每一个字段 (所有列)都完全依赖于主键,记录的唯一性** + +作用:遵守第二范式减少数据冗余,通过主键区分相同数据。 + +1. 函数依赖:A → B,如果通过 A 属性(属性组)的值,可以确定唯一 B 属性的值,则称 B 依赖于 A + * 学号 → 姓名;(学号,课程名称) → 分数 +2. 完全函数依赖:A → B,如果A是一个属性组,则 B 属性值的确定需要依赖于 A 属性组的所有属性值 + * (学号,课程名称) → 分数 +3. 部分函数依赖:A → B,如果 A 是一个属性组,则 B 属性值的确定只需要依赖于 A 属性组的某些属性值 + * (学号,课程名称) → 姓名 +4. 传递函数依赖:A → B,B → C,如果通过A属性(属性组)的值,可以确定唯一 B 属性的值,在通过 B 属性(属性组)的值,可以确定唯一 C 属性的值,则称 C 传递函数依赖于 A + * 学号 → 系名,系名 → 系主任 +5. 码:如果在一张表中,一个属性或属性组,被其他所有属性所完全依赖,则称这个属性(属性组)为该表的码 + * 该表中的码:(学号,课程名称) + * 主属性:码属性组中的所有属性 + * 非主属性:除码属性组以外的属性 + +![](https://seazean.oss-cn-beijing.aliyuncs.com/img/DB/第二范式.png) + + + + + +**** + + + +### 第三范式 + +**3NF:**在满足第二范式的基础上,表中的任何属性不依赖于其它非主属性,消除传递依赖。简而言之,**非主键都直接依赖于主键,而不是通过其它的键来间接依赖于主键**。 + +作用:可以通过主键 id 区分相同数据,修改数据的时候只需要修改一张表(方便修改),反之需要修改多表。 + +![](https://seazean.oss-cn-beijing.aliyuncs.com/img/DB/第三范式.png) + + + + + + + +*** + + + +### 总结 + +![](https://seazean.oss-cn-beijing.aliyuncs.com/img/DB/三大范式.png) + + + + + + + + +*** + + + + + +# Redis + +## NoSQL + +### 概述 + +NoSQL(Not-Only SQL):泛指非关系型的数据库,作为关系型数据库的补充 + +MySQL 支持 ACID 特性,保证可靠性和持久性,读取性能不高,因此需要缓存的来减缓数据库的访问压力 + +作用:应对基于海量用户和海量数据前提下的数据处理问题 + +特征: + +* 可扩容,可伸缩,SQL 数据关系过于复杂,Nosql 不存关系,只存数据 +* 大数据量下高性能,数据不存取在磁盘 IO,存取在内存 +* 灵活的数据模型,设计了一些数据存储格式,能保证效率上的提高 +* 高可用,集群 + +常见的 NoSQL:Redis、memcache、HBase、MongoDB + + + +参考书籍:https://book.douban.com/subject/25900156/ + +参考视频:https://www.bilibili.com/video/BV1CJ411m7Gc + + + +*** + + + +### Redis + +Redis (REmote DIctionary Server) :用 C 语言开发的一个开源的高性能键值对(key-value)数据库 + +特征: + +* 数据间没有必然的关联关系,**不存关系,只存数据** +* 数据**存储在内存**,存取速度快,解决了磁盘 IO 速度慢的问题 +* 内部采用**单线程**机制进行工作 +* 高性能,官方测试数据,50 个并发执行 100000 个请求,读的速度是 110000 次/s,写的速度是 81000 次/s +* 多数据类型支持 + * 字符串类型:string(String) + * 列表类型:list(LinkedList) + * 散列类型:hash(HashMap) + * 集合类型:set(HashSet) + * 有序集合类型:zset/sorted_set(TreeSet) +* 支持持久化,可以进行数据灾难恢复 + + + +*** + + + +### 安装启动 + +安装: + +* Redis 5.0 被包含在默认的 Ubuntu 20.04 软件源中 + + ```sh + sudo apt update + sudo apt install redis-server + ``` + +* 检查 Redis 状态 + + ```sh + sudo systemctl status redis-server + ``` + +启动: + +* 启动服务器——参数启动 + + ```sh + redis-server [--port port] + #redis-server --port 6379 + ``` + +* 启动服务器——配置文件启动 + + ```sh + redis-server config_file_name + #redis-server /etc/redis/conf/redis-6397.conf + ``` + +* 启动客户端: + + ```sh + redis-cli [-h host] [-p port] + #redis-cli -h 192.168.2.185 -p 6397 + ``` + + 注意:服务器启动指定端口使用的是--port,客户端启动指定端口使用的是-p + + + +*** + + + +### 基本配置 + +#### 系统目录 + +1. 创建文件结构 + + 创建配置文件存储目录 + + ```sh + mkdir conf + ``` + + 创建服务器文件存储目录(包含日志、数据、临时配置文件等) + + ```sh + mkdir data + ``` + +2. 创建配置文件副本放入 conf 目录,Ubuntu 系统配置文件 redis.conf 在目录 `/etc/redis` 中 + + ```sh + cat redis.conf | grep -v "#" | grep -v "^$" -> /conf/redis-6379.conf + ``` + + 去除配置文件的注释和空格,输出到新的文件,命令方式采用 redis-port.conf + + + +*** + + + +#### 服务器 + +* 设置服务器以守护进程的方式运行,关闭后服务器控制台中将打印服务器运行信息(同日志内容相同): + + ```sh + daemonize yes|no + ``` + +* 绑定主机地址,绑定本地IP地址,否则SSH无法访问: + + ```sh + bind ip + ``` + +* 设置服务器端口: + + ```sh + port port + ``` + +* 设置服务器文件保存地址: + + ```sh + dir path + ``` + +* 设置数据库的数量: + + ```sh + databases 16 + ``` + +* 多服务器快捷配置: + + 导入并加载指定配置文件信息,用于快速创建 redis 公共配置较多的 redis 实例配置文件,便于维护 + + ```sh + include /path/conf_name.conf + ``` + + + +*** + + + +#### 客户端 + +* 服务器允许客户端连接最大数量,默认 0,表示无限制,当客户端连接到达上限后,Redis 会拒绝新的连接: + + ```sh + maxclients count + ``` + +* 客户端闲置等待最大时长,达到最大值后关闭对应连接,如需关闭该功能,设置为 0: + + ```sh + timeout seconds + ``` + + + +*** + + + +#### 日志配置 + +设置日志记录 + +* 设置服务器以指定日志记录级别 + + ```sh + loglevel debug|verbose|notice|warning + ``` + +* 日志记录文件名 + + ```sh + logfile filename + ``` + +注意:日志级别开发期设置为 verbose 即可,生产环境中配置为 notice,简化日志输出量,降低写日志 IO 的频度 + + + +**配置文件:** + +```sh +bind 192.168.2.185 +port 6379 +#timeout 0 +daemonize no +logfile /etc/redis/data/redis-6379.log +dir /etc/redis/data +dbfilename "dump-6379.rdb" +``` + + + +*** + + + +#### 基本指令 + +帮助信息: + +* 获取命令帮助文档 + + ```sh + help [command] + #help set + ``` + +* 获取组中所有命令信息名称 + + ```sh + help [@group-name] + #help @string + ``` + +退出服务 + +* 退出客户端: + + ```sh + quit + exit + ``` + +* 退出客户端服务器快捷键: + + ```sh + Ctrl+C + ``` + + + + + + + + +*** + + + + + +## 数据库 + +### 服务器 + +Redis 服务器将所有数据库保存在**服务器状态 redisServer 结构**的 db 数组中,数组的每一项都是 redisDb 结构,代表一个数据库,每个数据库之间相互独立,**共用 **Redis 内存,不区分大小。在初始化服务器时,根据 dbnum 属性决定创建数据库的数量,该属性由服务器配置的 database 选项决定,默认 16 + +```c +struct redisServer { + // 保存服务器所有的数据库 + redisDB *db; + + // 服务器数据库的数量 + int dbnum; +}; +``` + + + +**在服务器内部**,客户端状态 redisClient 结构的 db 属性记录了目标数据库,是一个指向 redisDb 结构的指针 + +```c +struct redisClient { + // 记录客户端正在使用的数据库,指向 redisServer.db 数组中的某一个 db + redisDB *db; +}; +``` + +每个 Redis 客户端都有目标数据库,执行数据库读写命令时目标数据库就会成为这些命令的操作对象,默认情况下 Redis 客户端的目标数据库为 0 号数据库,客户端可以执行 SELECT 命令切换目标数据库,原理是通过修改 redisClient.db 指针指向服务器中不同数据库 + +命令操作: + +```sh +select index #切换数据库,index从0-15取值 +move key db #数据移动到指定数据库,db是数据库编号 +ping #测试数据库是否连接正常,返回PONG +echo message #控制台输出信息 +``` + +Redis 没有可以返回客户端目标数据库的命令,但是 redis-cli 客户端旁边会提示当前所使用的目标数据库 + +```sh +redis> SELECT 1 +OK +redis[1]> +``` + + + + + +*** + + + +### 键空间 + +#### key space + +Redis 是一个键值对(key-value pair)数据库服务器,每个数据库都由一个 redisDb 结构表示,redisDb.dict **字典中保存了数据库的所有键值对**,将这个字典称为键空间(key space) + +```c +typedef struct redisDB { + // 数据库键空间,保存所有键值对 + dict *dict +} redisDB; +``` + +键空间和用户所见的数据库是直接对应的: + +* 键空间的键就是数据库的键,每个键都是一个字符串对象 +* 键空间的值就是数据库的值,每个值可以是任意一种 Redis 对象 + +![](https://seazean.oss-cn-beijing.aliyuncs.com/img/DB/Redis-数据库键空间.png) + +当使用 Redis 命令对数据库进行读写时,服务器不仅会对键空间执行指定的读写操作,还会**进行一些维护操作**: + +* 在读取一个键后(读操作和写操作都要对键进行读取),服务器会根据键是否存在来更新服务器的键空间命中 hit 次数或键空间不命中 miss 次数,这两个值可以在 `INFO stats` 命令的 keyspace_hits 属性和 keyspace_misses 属性中查看 +* 更新键的 LRU(最后使用)时间,该值可以用于计算键的闲置时间,使用 `OBJECT idletime key` 查看键 key 的闲置时间 +* 如果在读取一个键时发现该键已经过期,服务器会**先删除过期键**,再执行其他操作 +* 如果客户端使用 WATCH 命令监视了某个键,那么服务器在对被监视的键进行修改之后,会将这个键标记为脏(dirty),从而让事务注意到这个键已经被修改过 +* 服务器每次修改一个键之后,都会对 dirty 键计数器的值增1,该计数器会触发服务器的持久化以及复制操作 +* 如果服务器开启了数据库通知功能,那么在对键进行修改之后,服务器将按配置发送相应的数据库通知 + + + +*** + + + +#### 读写指令 + +常见键操作指令: + +* 增加指令 + + ```sh + set key value #添加一个字符串类型的键值对 + +* 删除指令 + + ```sh + del key #删除指定key + unlink key #非阻塞删除key,真正的删除会在后续异步操作 + ``` + +* 更新指令 + + ```sh + rename key newkey #改名 + renamenx key newkey #改名 + ``` + + 值得更新需要参看具体得 Redis 对象得操作方式,比如字符串对象执行 `SET key value` 就可以完成修改 + +* 查询指令 + + ```sh + exists key #获取key是否存在 + randomkey #随机返回一个键 + keys pattern #查询key + ``` + + KEYS 命令需要**遍历存储的键值对**,操作延时高,一般不被建议用于生产环境中 + + 查询模式规则:* 匹配任意数量的任意符号、? 配合一个任意符号、[] 匹配一个指定符号 + + ```sh + keys * #查询所有key + keys aa* #查询所有以aa开头 + keys *bb #查询所有以bb结尾 + keys ??cc #查询所有前面两个字符任意,后面以cc结尾 + keys user:? #查询所有以user:开头,最后一个字符任意 + keys u[st]er:1 #查询所有以u开头,以er:1结尾,中间包含一个字母,s或t + ``` + + +* 其他指令 + + ```sh + type key #获取key的类型 + dbsize #获取当前数据库的数据总量,即key的个数 + flushdb #清除当前数据库的所有数据(慎用) + flushall #清除所有数据(慎用) + ``` + + 在执行 FLUSHDB 这样的危险命令之前,最好先执行一个 SELECT 命令,保证当前所操作的数据库是目标数据库 + + + + + +*** + + + +#### 时效设置 + +客户端可以以秒或毫秒的精度为数据库中的某个键设置生存时间(TimeTo Live, TTL),在经过指定时间之后,服务器就会自动删除生存时间为 0 的键;也可以以 UNIX 时间戳的方式设置过期时间(expire time),当键的过期时间到达,服务器会自动删除这个键 + +```sh +expire key seconds #为指定key设置生存时间,单位为秒 +pexpire key milliseconds #为指定key设置生存时间,单位为毫秒 +expireat key timestamp #为指定key设置过期时间,单位为时间戳 +pexpireat key mil-timestamp #为指定key设置过期时间,单位为毫秒时间戳 +``` + +* 实际上 EXPIRE、EXPIRE、EXPIREAT 三个命令**底层都是转换为 PEXPIREAT 命令**来实现的 +* SETEX 命令可以在设置一个字符串键的同时为键设置过期时间,但是该命令是一个类型限定命令 + +redisDb 结构的 expires 字典保存了数据库中所有键的过期时间,字典称为过期字典: + +* 键是一个指针,指向键空间中的某个键对象(复用键空间的对象,不会产生内存浪费) +* 值是一个 long long 类型的整数,保存了键的过期时间,是一个毫秒精度的 UNIX 时间戳 + +```c +typedef struct redisDB { + // 过期字典,保存所有键的过期时间 + dict *expires +} redisDB; +``` + +客户端执行 PEXPIREAT 命令,服务器会在数据库的过期字典中关联给定的数据库键和过期时间: + +```python +def PEXPIREAT(key, expire_time_in_ms): + # 如果给定的键不存在于键空间,那么不能设置过期时间 + if key not in redisDb.dict: + return 0 + + # 在过期字典中关联键和过期时间 + redisDB.expires[key] = expire_time_in_ms + + # 过期时间设置成功 + return 1 +``` + + + +**** + + + +#### 时效状态 + +TTL 和 PTTL 命令通过计算键的过期时间和当前时间之间的差,返回这个键的剩余生存时间 + +* 返回正数代表该数据在内存中还能存活的时间 +* 返回 -1 代表永久性,返回 -2 代表键不存在 + +```sh +ttl key #获取key的剩余时间,每次获取会自动变化(减小),类似于倒计时 +pttl key #获取key的剩余时间,单位是毫秒,每次获取会自动变化(减小) +``` + +PERSIST 是 PEXPIREAT 命令的反操作,在过期字典中查找给定的键,并解除键和值(过期时间)在过期字典中的关联 + +```sh +persist key #切换key从时效性转换为永久性 +``` + +Redis 通过过期字典可以检查一个给定键是否过期: + +* 检查给定键是否存在于过期字典:如果存在,那么取得键的过期时间 +* 检查当前 UNIX 时间戳是否大于键的过期时间:如果是那么键已经过期,否则键未过期 + +补充:AOF、RDB 和复制功能对过期键的处理 + +* RDB : + * 生成 RDB 文件,程序会对数据库中的键进行检查,已过期的键不会被保存到新创建的 RDB 文件中 + * 载入 RDB 文件,如果服务器以主服务器模式运行,那么在载入时会对键进行检查,过期键会被忽略;如果服务器以从服务器模式运行,会载入所有键,包括过期键,但是主从服务器进行数据同步时就会删除这些键 +* AOF: + * 写入 AOF 文件,如果数据库中的某个键已经过期,但还没有被删除,那么 AOF 文件不会因为这个过期键而产生任何影响;当该过期键被删除,程序会向 AOF 文件追加一条 DEL 命令,显式的删除该键 + * AOF 重写,会对数据库中的键进行检查,忽略已经过期的键 +* 复制:当服务器运行在复制模式下时,从服务器的过期键删除动作由主服务器控制 + * 主服务器在删除一个过期键之后,会显式地向所有从服务器发送一个 DEL 命令,告知从服务器删除这个过期键 + * 从服务器在执行客户端发送的读命令时,即使碰到过期键也不会将过期键删除,会当作未过期键处理,只有在接到主服务器发来的 DEL 命令之后,才会删除过期键(数据不一致) + + + + + +**** + + + +### 过期删除 + +#### 删除策略 + +删除策略就是**针对已过期数据的处理策略**,已过期的数据不一定被立即删除,在不同的场景下使用不同的删除方式会有不同效果,在内存占用与 CPU 占用之间寻找一种平衡,顾此失彼都会造成整体 Redis 性能的下降,甚至引发服务器宕机或内存泄露 + +针对过期数据有三种删除策略: + +- 定时删除 +- 惰性删除(被动删除) +- 定期删除 + +Redis 采用惰性删除和定期删除策略的结合使用 + + + +*** + + + +#### 定时删除 + +在设置键的过期时间的同时,创建一个定时器(timer),让定时器在键的过期时间到达时,立即执行对键的删除操作 + +- 优点:节约内存,到时就删除,快速释放掉不必要的内存占用 +- 缺点:对 CPU 不友好,无论 CPU 此时负载多高均占用 CPU,会影响 Redis 服务器响应时间和指令吞吐量 +- 总结:用处理器性能换取存储空间(拿时间换空间) + +创建一个定时器需要用到 Redis 服务器中的时间事件,而时间事件的实现方式是无序链表,查找一个事件的时间复杂度为 O(N),并不能高效地处理大量时间事件,所以采用这种方式并不现实 + + + +*** + + + +#### 惰性删除 + +数据到达过期时间不做处理,等下次访问到该数据时执行 **expireIfNeeded()** 判断: + +* 如果输入键已经过期,那么 expireIfNeeded 函数将输入键从数据库中删除,接着访问就会返回空 +* 如果输入键未过期,那么 expireIfNeeded 函数不做动作 + +所有的 Redis 读写命令在执行前都会调用 expireIfNeeded 函数进行检查,该函数就像一个过滤器,在命令真正执行之前过滤掉过期键 + +惰性删除的特点: + +* 优点:节约 CPU 性能,删除的目标仅限于当前处理的键,不会在删除其他无关的过期键上花费任何 CPU 时间 +* 缺点:内存压力很大,出现长期占用内存的数据,如果过期键永远不被访问,这种情况相当于内存泄漏 +* 总结:用存储空间换取处理器性能(拿空间换时间) + + + +*** + + + +#### 定期删除 + +定期删除策略是每隔一段时间执行一次删除过期键操作,并通过限制删除操作执行的时长和频率来减少删除操作对 CPU 时间的影响 + +* 如果删除操作执行得太频繁,或者执行时间太长,就会退化成定时删除策略,将 CPU 时间过多地消耗在删除过期键上 +* 如果删除操作执行得太少,或者执行时间太短,定期删除策略又会和惰性删除策略一样,出现浪费内存的情况 + +定期删除是**周期性轮询 Redis 库中的时效性**数据,从过期字典中随机抽取一部分键检查,利用过期数据占比的方式控制删除频度 + +- Redis 启动服务器初始化时,读取配置 server.hz 的值,默认为 10,执行指令 info server 可以查看,每秒钟执行 server.hz 次 `serverCron() → activeExpireCycle()` + +- activeExpireCycle() 对某个数据库中的每个 expires 进行检测,工作模式: + + * 轮询每个数据库,从数据库中取出一定数量的随机键进行检查,并删除其中的过期键,如果过期 key 的比例超过了 25%,则继续重复此过程,直到过期 key 的比例下降到 25% 以下,或者这次任务的执行耗时超过了 25 毫秒 + + * 全局变量 current_db 用于记录 activeExpireCycle() 的检查进度(哪一个数据库),下一次调用时接着该进度处理 + * 随着函数的不断执行,服务器中的所有数据库都会被检查一遍,这时将 current_db 重置为 0,然后再次开始新一轮的检查 + +定期删除特点: + +- CPU 性能占用设置有峰值,检测频度可自定义设置 +- 内存压力不是很大,长期占用内存的**冷数据会被持续清理** +- 周期性抽查存储空间(随机抽查,重点抽查) + + + + + +*** + + + +### 数据淘汰 + +#### 逐出算法 + +数据淘汰策略:当新数据进入 Redis 时,在执行每一个命令前,会调用 **freeMemoryIfNeeded()** 检测内存是否充足。如果内存不满足新加入数据的最低存储要求,Redis 要临时删除一些数据为当前指令清理存储空间,清理数据的策略称为**逐出算法** + +逐出数据的过程不是 100% 能够清理出足够的可使用的内存空间,如果不成功则反复执行,当对所有数据尝试完毕,如不能达到内存清理的要求,**出现 Redis 内存打满异常**: + +```sh +(error) OOM command not allowed when used memory >'maxmemory' +``` + + + +**** + + + +#### 策略配置 + +Redis 如果不设置最大内存大小或者设置最大内存大小为 0,在 64 位操作系统下不限制内存大小,在 32 位操作系统默认为 3GB 内存,一般推荐设置 Redis 内存为最大物理内存的四分之三 + +内存配置方式: + +* 通过修改文件配置(永久生效):修改配置文件 maxmemory 字段,单位为字节 + +* 通过命令修改(重启失效): + + * `config set maxmemory 104857600`:设置 Redis 最大占用内存为 100MB + * `config get maxmemory`:获取 Redis 最大占用内存 + + * `info` :可以查看 Redis 内存使用情况,`used_memory_human` 字段表示实际已经占用的内存,`maxmemory` 表示最大占用内存 + +影响数据淘汰的相关配置如下,配置 conf 文件: + +* 每次选取待删除数据的个数,采用随机获取数据的方式作为待检测删除数据,防止全库扫描,导致严重的性能消耗,降低读写性能 + + ```sh + maxmemory-samples count + ``` + +* 达到最大内存后的,对被挑选出来的数据进行删除的策略 + + ```sh + maxmemory-policy policy + ``` + + 数据删除的策略 policy:3 类 8 种 + + 第一类:检测易失数据(可能会过期的数据集 server.db[i].expires): + + ```sh + volatile-lru # 对设置了过期时间的 key 选择最近最久未使用使用的数据淘汰 + volatile-lfu # 对设置了过期时间的 key 选择最近使用次数最少的数据淘汰 + volatile-ttl # 对设置了过期时间的 key 选择将要过期的数据淘汰 + volatile-random # 对设置了过期时间的 key 选择任意数据淘汰 + ``` + + 第二类:检测全库数据(所有数据集 server.db[i].dict ): + + ```sh + allkeys-lru # 对所有 key 选择最近最少使用的数据淘汰 + allkeLyRs-lfu # 对所有 key 选择最近使用次数最少的数据淘汰 + allkeys-random # 对所有 key 选择任意数据淘汰,相当于随机 + ``` + + 第三类:放弃数据驱逐 + + ```sh + no-enviction #禁止驱逐数据(redis4.0中默认策略),会引发OOM(Out Of Memory) + ``` + +数据淘汰策略配置依据:使用 INFO 命令输出监控信息,查询缓存 hit 和 miss 的次数,根据需求调优 Redis 配置 + + + + + +*** + + + +### 排序机制 + +#### 基本介绍 + +Redis 的 SORT 命令可以对列表键、集合键或者有序集合键的值进行排序,并不更改集合中的数据位置,只是查询 + +```sh +SORT key [ASC/DESC] #对key中数据排序,默认对数字排序,并不更改集合中的数据位置,只是查询 +SORT key ALPHA #对key中字母排序,按照字典序 +``` + + + + + +*** + + + +#### SORT + +`SORT ` 命令可以对一个包含数字值的键 key 进行排序 + +假设 `RPUSH numbers 3 1 2`,执行 `SORT numbers` 的详细步骤: + +* 创建一个和 key 列表长度相同的数组,数组每项都是 redisSortObject 结构 + + ```c + typedef struct redisSortObject { + // 被排序键的值 + robj *obj; + + // 权重 + union { + // 排序数字值时使用 + double score; + // 排序带有 BY 选项的字符串 + robj *cmpobj; + } u; + } + ``` + +* 遍历数组,将各个数组项的 obj 指针分别指向 numbers 列表的各个项 + +* 遍历数组,将 obj 指针所指向的列表项转换成一个 double 类型的浮点数,并将浮点数保存在对应数组项的 u.score 属性里 + +* 根据数组项 u.score 属性的值,对数组进行数字值排序,排序后的数组项按 u.score 属性的值**从小到大排列** + +* 遍历数组,将各个数组项的 obj 指针所指向的值作为排序结果返回给客户端,程序首先访问数组的索引 0,依次向后访问 + +![](https://seazean.oss-cn-beijing.aliyuncs.com/img/DB/Redis-sort排序.png) + +对于 `SORT key [ASC/DESC]` 函数: + +* 在执行升序排序时,排序算法使用的对比函数产生升序对比结果 +* 在执行降序排序时,排序算法使用的对比函数产生降序对比结果 + + + +**** + + + +#### BY + +SORT 命令默认使用被排序键中包含的元素作为排序的权重,元素本身决定了元素在排序之后所处的位置,通过使用 BY 选项,SORT 命令可以指定某些字符串键,或者某个哈希键所包含的某些域(field)来作为元素的权重,对一个键进行排序 + +```sh +SORT BY # 数值 +SORT BY ALPHA # 字符 +``` + +```sh +redis> SADD fruits "apple" "banana" "cherry" +(integer) 3 +redis> SORT fruits ALPHA +1) "apple" +2) "banana" +3) "cherry" +``` + +```sh +redis> MSET apple-price 8 banana-price 5.5 cherry-price 7 +OK +# 使用水果的价钱进行排序 +redis> SORT fruits BY *-price +1) "banana" +2) "cherry" +3) "apple" +``` + +实现原理:排序时的 u.score 属性就会被设置为对应的权重 + + + + + +*** + + + +#### LIMIT + +SORT 命令默认会将排序后的所有元素都返回给客户端,通过 LIMIT 选项可以让 SORT 命令只返回其中一部分已排序的元素 + +```sh +LIMIT +``` + +* offset 参数表示要跳过的已排序元素数量 +* count 参数表示跳过给定数量的元素后,要返回的已排序元素数量 + +```sh +# 对应 a b c d e f g +redis> SORT alphabet ALPHA LIMIT 2 3 +1) "c" +2) "d" +3) "e" +``` + +实现原理:在排序后的 redisSortObject 结构数组中,将指针移动到数组的索引 2 上,依次访问 array[2]、array[3]、array[4] 这 3 个数组项,并将数组项的 obj 指针所指向的元素返回给客户端 + + + + + +*** + + + +#### GET + +SORT 命令默认在对键进行排序后,返回被排序键本身所包含的元素,通过使用 GET 选项, 可以在对键进行排序后,根据被排序的元素以及 GET 选项所指定的模式,查找并返回某些键的值 + +```sh +SORT GET +``` + +```sh +redis> SADD students "tom" "jack" "sea" +#设置全名 +redis> SET tom-name "Tom Li" +OK +redis> SET jack-name "Jack Wang" +OK +redis> SET sea-name "Sea Zhang" +OK +``` + +```sh +redis> SORT students ALPHA GET *-name +1) "Jack Wang" +2) "Sea Zhang" +3) "Tom Li" +``` + +实现原理:对 students 进行排序后,对于 jack 元素和 *-name 模式,查找程序返回键 jack-name,然后获取 jack-name 键对应的值 + + + + + +*** + + + +#### STORE + +SORT 命令默认只向客户端返回排序结果,而不保存排序结果,通过使用 STORE 选项可以将排序结果保存在指定的键里面 + +```sh +SORT STORE +``` + +```sh +redis> SADD students "tom" "jack" "sea" +(integer) 3 +redis> SORT students ALPHA STORE sorted_students +(integer) 3 +``` + +实现原理:排序后,检查 sorted_students 键是否存在,如果存在就删除该键,设置 sorted_students 为空白的列表键,遍历排序数组将元素依次放入 + + + + + +*** + + + +#### 执行顺序 + +调用 SORT 命令,除了 GET 选项之外,改变其他选项的摆放顺序并不会影响命令执行选项的顺序 + +```sh +SORT ALPHA [ASC/DESC] BY LIMIT GET STORE +``` + +执行顺序: + +* 排序:命令会使用 ALPHA 、ASC 或 DESC、BY 这几个选项,对输入键进行排序,并得到一个排序结果集 +* 限制排序结果集的长度:使用 LIMIT 选项,对排序结果集的长度进行限制 +* 获取外部键:根据排序结果集中的元素以及 GET 选项指定的模式,查找并获取指定键的值,并用这些值来作为新的排序结果集 +* 保存排序结果集:使用 STORE 选项,将排序结果集保存到指定的键上面去 +* 向客户端返回排序结果集:最后一步命令遍历排序结果集,并依次向客户端返回排序结果集中的元素 + + + + + +*** + + + +### 通知机制 + +数据库通知是可以让客户端通过订阅给定的频道或者模式,来获知数据库中键的变化,以及数据库中命令的执行情况 + +* 关注某个键执行了什么命令的通知称为键空间通知(key-space notification) +* 关注某个命令被什么键执行的通知称为键事件通知(key-event notification) + +图示订阅 0 号数据库 message 键: + + + +服务器配置的 notify-keyspace-events 选项决定了服务器所发送通知的类型 + +* AKE 代表服务器发送所有类型的键空间通知和键事件通知 +* AK 代表服务器发送所有类型的键空间通知 +* AE 代表服务器发送所有类型的键事件通知 +* K$ 代表服务器只发送和字符串键有关的键空间通知 +* EL 代表服务器只发送和列表键有关的键事件通知 +* ..... + +发送数据库通知的功能是由 notifyKeyspaceEvent 函数实现的: + +* 如果给定的通知类型 type 不是服务器允许发送的通知类型,那么函数会直接返回 +* 如果给定的通知是服务器允许发送的通知 + * 检测服务器是否允许发送键空间通知,允许就会构建并发送事件通知 + * 检测服务器是否允许发送键事件通知,允许就会构建并发送事件通知 + + + + + +*** + + + + + +## 体系架构 + +### 事件驱动 + +#### 基本介绍 + +Redis 服务器是一个事件驱动程序,服务器需要处理两类事件 + +* 文件事件 (file event):服务器通过套接字与客户端(或其他 Redis 服务器)进行连接,而文件事件就是服务器对套接字操作的抽象。服务器与客户端的通信会产生相应的文件事件,服务器通过监听并处理这些事件完成一系列网络通信操作 +* 时间事件 (time event):Redis 服务器中的一些操作(比如 serverCron 函数)需要在指定时间执行,而时间事件就是服务器对这类定时操作的抽象 + + + + + +*** + + + +#### 文件事件 + +##### 基本组成 + +Redis 基于 Reactor 模式开发了网络事件处理器,这个处理器被称为文件事件处理器 (file event handler) + +* 使用 I/O 多路复用 (multiplexing) 程序来同时监听多个套接字,并根据套接字执行的任务来为套接字关联不同的事件处理器 + +* 当被监听的套接字准备好执行连接应答 (accept)、 读取 (read)、 写入 (write)、 关闭 (close) 等操作时,与操作相对应的文件事件就会产生,这时文件事件分派器会调用套接字关联好的事件处理器来处理事件 + +文件事件处理器**以单线程方式运行**,但通过使用 I/O 多路复用程序来监听多个套接字, 既实现了高性能的网络通信模型,又可以很好地与 Redis 服务器中其他同样以单线程方式运行的模块进行对接,保持了 Redis 内部单线程设计的简单性 + +文件事件处理器的组成结构: + + + +I/O 多路复用程序将所有产生事件的套接字处理请求放入一个**单线程的执行队列**中,通过队列有序、同步的向文件事件分派器传送套接字,上一个套接字产生的事件处理完后,才会继续向分派器传送下一个 + + + +Redis 单线程也能高效的原因: + +* 纯内存操作 +* 核心是基于非阻塞的 IO 多路复用机制,单线程可以高效处理多个请求 +* 底层使用 C 语言实现,C 语言实现的程序距离操作系统更近,执行速度相对会更快 +* 单线程同时也**避免了多线程的上下文频繁切换问题**,预防了多线程可能产生的竞争问题 + + + +**** + + + +##### 多路复用 + +Redis 的 I/O 多路复用程序的所有功能都是通过包装常见的 select 、epoll、 evport 和 kqueue 这些函数库来实现的,Redis 在 I/O 多路复用程序的实现源码中用 #include 宏定义了相应的规则,编译时自动选择系统中**性能最高的多路复用函数**来作为底层实现 + +I/O 多路复用程序监听多个套接字的 AE_READABLE 事件和 AE_WRITABLE 事件,这两类事件和套接字操作之间的对应关系如下: + +* 当套接字变得**可读**时(客户端对套接字执行 write 操作或者 close 操作),或者有新的**可应答**(acceptable)套接字出现时(客户端对服务器的监听套接字执行 connect 连接操作),套接字产生 AE_READABLE 事件 +* 当套接字变得可写时(客户端对套接字执行 read 操作,对于服务器来说就是可以写了),套接字产生 AE_WRITABLE 事件 + +I/O 多路复用程序允许服务器同时监听套接字的 AE_READABLE 和 AE_WRITABLE 事件, 如果一个套接字同时产生了这两种事件,那么文件事件分派器会优先处理 AE_READABLE 事件, 等 AE_READABLE 事件处理完之后才处理 AE_WRITABLE 事件 + + + +*** + + + +##### 处理器 + +Redis 为文件事件编写了多个处理器,这些事件处理器分别用于实现不同的网络通信需求: + +* 连接应答处理器,用于对连接服务器的各个客户端进行应答,Redis 服务器初始化时将该处理器与 AE_READABLE 事件关联 +* 命令请求处理器,用于接收客户端传来的命令请求,执行套接字的读入操作,与 AE_READABLE 事件关联 +* 命令回复处理器,用于向客户端返回命令的执行结果,执行套接字的写入操作,与 AE_WRITABLE 事件关联 +* 复制处理器,当主服务器和从服务器进行复制操作时,主从服务器都需要关联该处理器 + +Redis 客户端与服务器进行连接并发送命令的整个过程: + +* Redis 服务器正在运作监听套接字的 AE_READABLE 事件,关联连接应答处理器 +* 当 Redis 客户端向服务器发起连接,监听套接字将产生 AE_READABLE 事件,触发连接应答处理器执行,对客户端的连接请求进行应答,创建客户端套接字以及客户端状态,并将客户端套接字的 **AE_READABLE 事件与命令请求处理器**进行关联 +* 客户端向服务器发送命令请求,客户端套接字产生 AE_READABLE 事件,引发命令请求处理器执行,读取客户端的命令内容传给相关程序去执行 +* 执行命令会产生相应的命令回复,为了将这些命令回复传送回客户端,服务器会将客户端套接字的 **AE_WRITABLE 事件与命令回复处理器**进行关联 +* 当客户端尝试读取命令回复时,客户端套接字产生 AE_WRITABLE 事件,触发命令回复处理器执行,在命令回复全部写入套接字后,服务器就会解除客户端套接字的 AE_WRITABLE 事件与命令回复处理器之间的关联 + + + + + +*** + + + +#### 时间事件 + +Redis 的时间事件分为以下两类: + +* 定时事件:在指定的时间之后执行一次(Redis 中暂时未使用) +* 周期事件:每隔指定时间就执行一次 + +一个时间事件主要由以下三个属性组成: + +* id:服务器为时间事件创建的全局唯一 ID(标识号),从小到大顺序递增,新事件的 ID 比旧事件的 ID 号要大 +* when:毫秒精度的 UNIX 时间戳,记录了时间事件的到达(arrive)时间 +* timeProc:时间事件处理器,当时间事件到达时,服务器就会调用相应的处理器来处理事件 + +时间事件是定时事件还是周期性事件取决于时间事件处理器的返回值: + +* 定时事件:事件处理器返回 AE_NOMORE,该事件在到达一次后就会被删除 +* 周期事件:事件处理器返回非 AE_NOMORE 的整数值,服务器根据该值对事件的 when 属性更新,让该事件在一段时间后再次交付 + +服务器将所有时间事件都放在一个**无序链表**中,新的时间事件插入到链表的表头: + + + +无序链表指是链表不按 when 属性的大小排序,每当时间事件执行器运行时就必须遍历整个链表,查找所有已到达的时间事件,并调用相应的事件处理器处理 + +无序链表并不影响时间事件处理器的性能,因为正常模式下的 Redis 服务器**只使用 serverCron 一个时间事件**,在 benchmark 模式下服务器也只使用两个时间事件,所以无序链表不会影响服务器的性能,几乎可以按照一个指针处理 + + + +*** + + + +#### 事件调度 + +服务器中同时存在文件事件和时间事件两种事件类型,调度伪代码: + +```python +# 事件调度伪代码 +def aeProcessEvents(): + # 获取到达时间离当前时间最接近的时间事件 + time_event = aeSearchNearestTime() + + # 计算最接近的时间事件距离到达还有多少亳秒 + remaind_ms = time_event.when - unix_ts_now() + # 如果事件已到达,那么 remaind_ms 的值可能为负数,设置为 0 + if remaind_ms < 0: + remaind_ms = 0 + + # 根据 remaind_ms 的值,创建 timeval 结构 + timeval = create_timeval_with_ms(remaind_ms) + # 【阻塞并等待文件事件】产生,最大阻塞时间由传入的timeval结构决定,remaind_ms的值为0时调用后马上返回,不阻塞 + aeApiPoll(timeval) + + # 处理所有已产生的文件事件 + processFileEvents() + # 处理所有已到达的时间事件 + processTimeEvents() +``` + +事件的调度和执行规则: + +* aeApiPoll 函数的最大阻塞时间由到达时间最接近当前时间的时间事件决定,可以避免服务器对时间事件进行频繁的轮询(忙等待),也可以确保 aeApiPoll 函数不会阻塞过长时间 +* 对文件事件和时间事件的处理都是**同步、有序、原子地执行**,服务器不会中途中断事件处理,也不会对事件进行抢占,所以两种处理器都要尽可地减少程序的阻塞时间,并在有需要时**主动让出执行权**,从而降低事件饥饿的可能性 + * 命令回复处理器在写入字节数超过了某个预设常量,就会主动用 break 跳出写入循环,将余下的数据留到下次再写 + * 时间事件也会将非常耗时的持久化操作放到子线程或者子进程执行 +* 时间事件在文件事件之后执行,并且事件之间不会出现抢占,所以时间事件的实际处理时间通常会比设定的到达时间稍晚 + + + + + +**** + + + +#### 多线程 + +Redis6.0 引入多线程主要是为了提高网络 IO 读写性能,因为这是 Redis 的一个性能瓶颈(Redis 的瓶颈主要受限于内存和网络),多线程只是用来**处理网络数据的读写和协议解析**, 执行命令仍然是单线程顺序执行,因此不需要担心线程安全问题。 + +Redis6.0 的多线程默认是禁用的,只使用主线程。如需开启需要修改 redis 配置文件 `redis.conf` : + +```sh +io-threads-do-reads yesCopy to clipboardErrorCopied +``` + +开启多线程后,还需要设置线程数,否则是不生效的,同样需要修改 redis 配置文件 : + +```sh +io-threads 4 #官网建议4核的机器建议设置为2或3个线程,8核的建议设置为6个线程 +``` + + + + + +参考文章:https://mp.weixin.qq.com/s/dqmiR0ECf4lB6Y2OyK-dyA + + + + + +**** + + + +### 客户端 + +#### 基本介绍 + +Redis 服务器是典型的一对多程序,一个服务器可以与多个客户端建立网络连接,服务器对每个连接的客户端建立了相应的 redisClient 结构(客户端状态,**在服务器端的存储结构**),保存了客户端当前的状态信息,以及执行相关功能时需要用到的数据结构 + +Redis 服务器状态结构的 clients 属性是一个链表,这个链表保存了所有与服务器连接的客户端的状态结构: + +```c +struct redisServer { + // 一个链表,保存了所有客户端状态 + list *clients; + + //... +}; +``` + +![](https://seazean.oss-cn-beijing.aliyuncs.com/img/DB/Redis-服务器clients链表.png) + + + + + +*** + + + +#### 数据结构 + +##### redisClient + +客户端的数据结构: + +```c +typedef struct redisClient { + //... + + // 套接字 + int fd; + // 名字 + robj *name; + // 标志 + int flags; + + // 输入缓冲区 + sds querybuf; + // 输出缓冲区 buf 数组 + char buf[REDIS_REPLY_CHUNK_BYTES]; + // 记录了 buf 数组目前已使用的字节数量 + int bufpos; + // 可变大小的输出缓冲区,链表 + 字符串对象 + list *reply; + + // 命令数组 + rboj **argv; + // 命令数组的长度 + int argc; + // 命令的信息 + struct redisCommand *cmd; + + // 是否通过身份验证 + int authenticated; + + // 创建客户端的时间 + time_t ctime; + // 客户端与服务器最后一次进行交互的时间 + time_t lastinteraction; + // 输出缓冲区第一次到达软性限制 (soft limit) 的时间 + time_t obuf_soft_limit_reached_time; +} +``` + +客户端状态包括两类属性 + +* 一类是比较通用的属性,这些属性很少与特定功能相关,无论客户端执行的是什么工作,都要用到这些属性 +* 另一类是和特定功能相关的属性,比如操作数据库时用到的 db 属性和 dict id 属性,执行事务时用到的 mstate 属性,以及执行 WATCH 命令时用到的 watched_keys 属性等,代码中没有列出 + + + +*** + + + +##### 套接字 + +客户端状态的 fd 属性记录了客户端正在使用的套接字描述符,根据客户端类型的不同,fd 属性的值可以是 -1 或者大于 -1 的整数: + +* 伪客户端 (fake client) 的 fd 属性的值为 -1,命令请求来源于 AOF 文件或者 Lua 脚本,而不是网络,所以不需要套接字连接 +* 普通客户端的 fd 属性的值为大于 -1 的整数,因为合法的套接字描述符不能是 -1 + +执行 `CLIENT list` 命令可以列出目前所有连接到服务器的普通客户端,不包括伪客户端 + + + +*** + + + +##### 名字 + +在默认情况下,一个连接到服务器的客户端是没有名字的,使用 `CLIENT setname` 命令可以为客户端设置一个名字 + + + +*** + + + +##### 标志 + +客户端的标志属性 flags 记录了客户端的角色以及客户端目前所处的状态,每个标志使用一个常量表示 + +* flags 的值可以是单个标志:`flags = ` +* flags 的值可以是多个标志的二进制:`flags = | | ... ` + +一部分标志记录**客户端的角色**: + +* REDIS_MASTER 表示客户端是一个从服务器,REDIS_SLAVE 表示客户端是一个从服务器,在主从复制时使用 +* REDIS_PRE_PSYNC 表示客户端是一个版本低于 Redis2.8 的从服务器,主服务器不能使用 PSYNC 命令与该从服务器进行同步,这个标志只能在 REDIS_ SLAVE 标志处于打开状态时使用 +* REDIS_LUA_CLIENT 表示客户端是专门用于处理 Lua 脚本里面包含的 Redis 命令的伪客户端 + +一部分标志记录目前**客户端所处的状态**: + +* REDIS_UNIX_SOCKET 表示服务器使用 UNIX 套接字来连接客户端 +* REDIS_BLOCKED 表示客户端正在被 BRPOP、BLPOP 等命令阻塞 +* REDIS_UNBLOCKED 表示客户端已经从 REDIS_BLOCKED 所表示的阻塞状态脱离,在 REDIS_BLOCKED 标志打开的情况下使用 +* REDIS_MULTI 标志表示客户端正在执行事务 +* REDIS_DIRTY_CAS 表示事务使用 WATCH 命令监视的数据库键已经被修改 +* ..... + + + + + +**** + + + +##### 缓冲区 + +客户端状态的输入缓冲区用于保存客户端发送的命令请求,输入缓冲区的大小会根据输入内容动态地缩小或者扩大,但最大大小不能超过 1GB,否则服务器将关闭这个客户端,比如执行 `SET key value `,那么缓冲区 querybuf 的内容: + +```sh +*3\r\n$3\r\nSET\r\n$3\r\nkey\r\n$5\r\nvalue\r\n # +``` + +输出缓冲区是服务器用于保存执行客户端命令所得的命令回复,每个客户端都有两个输出缓冲区可用: + +* 一个是固定大小的缓冲区,保存长度比较小的回复,比如 OK、简短的字符串值、整数值、错误回复等 +* 一个是可变大小的缓冲区,保存那些长度比较大的回复, 比如一个非常长的字符串值或者一个包含了很多元素的集合等 + +buf 是一个大小为 REDIS_REPLY_CHUNK_BYTES (常量默认 16*1024 = 16KB) 字节的字节数组,bufpos 属性记录了 buf 数组目前已使用的字节数量,当 buf 数组的空间已经用完或者回复数据太大无法放进 buf 数组里,服务器就会开始使用可变大小的缓冲区 + +通过使用 reply 链表连接多个字符串对象,可以为客户端保存一个非常长的命令回复,而不必受到固定大小缓冲区 16KB 大小的限制 + +![](https://seazean.oss-cn-beijing.aliyuncs.com/img/DB/Redis-可变输出缓冲区.png) + + + + + +*** + + + +##### 命令 + +服务器对 querybuf 中的命令请求的内容进行分析,得出的命令参数以及参数的数量分别保存到客户端状态的 argv 和 argc 属性 + +* argv 属性是一个数组,数组中的每项都是字符串对象,其中 argv[0] 是要执行的命令,而之后的其他项则是命令的参数 +* argc 属性负责记录 argv 数组的长度 + + + +服务器将根据项 argv[0] 的值,在命令表中查找命令所对应的命令的 redisCommand,将客户端状态的 cmd 指向该结构 + +命令表是一个字典结构,键是 SDS 结构保存命令的名字;值是命令所对应的 redisCommand 结构,保存了命令的实现函数、命令标志、 命令应该给定的参数个数、命令的总执行次数和总消耗时长等统计信息 + + + + + +**** + + + +##### 验证 + +客户端状态的 authenticated 属性用于记录客户端是否通过了身份验证 + +* authenticated 值为 0,表示客户端未通过身份验证 +* authenticated 值为 1,表示客户端已通过身份验证 + +当客户端 authenticated = 0 时,除了 AUTH 命令之外, 客户端发送的所有其他命令都会被服务器拒绝执行 + +```sh +redis> PING +(error) NOAUTH Authentication required. +redis> AUTH 123321 +OK +redis> PING +PONG +``` + + + +*** + + + +##### 时间 + +ctime 属性记录了创建客户端的时间,这个时间可以用来计算客户端与服务器已经连接了多少秒,`CLIENT list` 命令的 age 域记录了这个秒数 + +lastinteraction 属性记录了客户端与服务器最后一次进行互动 (interaction) 的时间,互动可以是客户端向服务器发送命令请求,也可以是服务器向客户端发送命令回复。该属性可以用来计算客户端的空转 (idle) 时长, 就是距离客户端与服务器最后一次进行互动已经过去了多少秒,`CLIENT list` 命令的 idle 域记录了这个秒数 + +obuf_soft_limit_reached_time 属性记录了**输出缓冲区第一次到达软性限制** (soft limit) 的时间 + + + + + +*** + + + + + +#### 生命周期 + +##### 创建 + +服务器使用不同的方式来创建和关闭不同类型的客户端 + +如果客户端是通过网络连接与服务器进行连接的普通客户端,那么在客户端使用 connect 函数连接到服务器时,服务器就会调用连接应答处理器为客户端创建相应的客户端状态,并将这个新的客户端状态添加到服务器状态结构 clients 链表的末尾 + +![](https://seazean.oss-cn-beijing.aliyuncs.com/img/DB/Redis-服务器clients链表.png) + +服务器会在初始化时创建负责执行 Lua 脚本中包含的 Redis 命令的伪客户端,并将伪客户端关联在服务器状态的 lua_client 属性 + +```c +struct redisServer { + // 保存伪客户端 + redisClient *lua_client; + + //... +}; +``` + +lua_client 伪客户端在服务器运行的整个生命周期会一直存在,只有服务器被关闭时,这个客户端才会被关闭 + +载入 AOF 文件时, 服务器会创建用于执行 AOF 文件包含的 Redis 命令的伪客户端,并在载入完成之后,关闭这个伪客户端 + + + +**** + + + +##### 关闭 + +一个普通客户端可以因为多种原因而被关闭: + +* 客户端进程退出或者被杀死,那么客户端与服务器之间的网络连接将被关闭,从而造成客户端被关闭 +* 客户端向服务器发送了带有不符合协议格式的命令请求,那么这个客户端会**被服务器关闭** +* 客户端是 `CLIENT KILL` 命令的目标 +* 如果用户为服务器设置了 timeout 配置选项,那么当客户端的空转时间超过该值时将被关闭,特殊情况不会被关闭: + * 客户端是主服务器(REDIS_MASTER )或者从服务器(打开了 REDIS_SLAVE 标志) + * 正在被 BLPOP 等命令阻塞(REDIS_BLOCKED) + * 正在执行 SUBSCRIBE、PSUBSCRIBE 等订阅命令 +* 客户端发送的命令请求的大小超过了输入缓冲区的限制大小(默认为 1GB) +* 发送给客户端的命令回复的大小超过了输出缓冲区的限制大小 + +理论上来说,可变缓冲区可以保存任意长的命令回复,但是为了回复过大占用过多的服务器资源,服务器会时刻检查客户端的输出缓冲区的大小,并在缓冲区的大小超出范围时,执行相应的限制操作: + +* 硬性限制 (hard limit):输出缓冲区的大小超过了硬性限制所设置的大小,那么服务器会关闭客户端(serverCron 函数中执行),积存在输出缓冲区中的所有内容会被**直接释放**,不会返回给客户端 +* 软性限制 (soft limit):输出缓冲区的大小超过了软性限制所设置的大小,小于硬性限制的大小,服务器的操作: + * 用属性 obuf_soft_limit_reached_time 记录下客户端到达软性限制的起始时间,继续监视客户端 + * 如果输出缓冲区的大小一直超出软性限制,并且持续时间超过服务器设定的时长,那么服务器将关闭客户端 + * 如果在指定时间内不再超出软性限制,那么客户端就不会被关闭,并且 o_s_l_r_t 属性清零 + +使用 client-output-buffer-limit 选项可以为普通客户端、从服务器客户端、执行发布与订阅功能的客户端分别设置不同的软性限制和硬性限制,格式: + +```sh +client-output-buffer-limit + +client-output-buffer-limit normal 0 0 0 +client-output-buffer-limit slave 256mb 64mb 60 +client-output-buffer-limit pubsub 32mb 8mb 60 +``` + +* 第一行:将普通客户端的硬性限制和软性限制都设置为 0,表示不限制客户端的输出缓冲区大小 +* 第二行:将从服务器客户端的硬性限制设置为 256MB,软性限制设置为 64MB,软性限制的时长为 60 秒 +* 第三行:将执行发布与订阅功能的客户端的硬性限制设置为 32MB,软性限制设置为 8MB,软性限制的时长为 60 秒 + + + + + +**** + + + +### 服务器 + +#### 执行流程 + +Redis 服务器与多个客户端建立网络连接,处理客户端发送的命令请求,在数据库中保存客户端执行命令所产生的数据,并通过资源管理来维持服务器自身的运转,所以一个命令请求从发送到获得回复的过程中,客户端和服务器需要完成一系列操作 + + + +##### 命令请求 + +Redis 服务器的命令请求来自 Redis 客户端,当用户在客户端中键入一个命令请求时,客户端会将这个命令请求转换成协议格式,通过连接到服务器的套接字,将协议格式的命令请求发送给服务器 + +```sh +SET KEY VALUE -> # 命令 +*3\r\nS3\r\nSET\r\n$3\r\nKEY\r\n$5\r\nVALUE\r\n # 协议格式 +``` + +当客户端与服务器之间的连接套接字因为客户端的写入而变得可读,服务器调用**命令请求处理器**来执行以下操作: + +* 读取套接字中协议格式的命令请求,并将其保存到客户端状态的输入缓冲区里面 +* 对输入缓冲区中的命令请求进行分析,提取出命令请求中包含的命令参数以及命令参数的个数,然后分别将参数和参数个数保存到客户端状态的 argv 属性和 argc 属性里 +* 调用命令执行器,执行客户端指定的命令 + +最后客户端接收到协议格式的命令回复之后,会将这些回复转换成用户可读的格式打印给用户观看,至此整体流程结束 + + + +**** + + + +##### 命令执行 + +命令执行器开始对命令操作: + +* 查找命令:首先根据客户端状态的 argv[0] 参数,在**命令表 (command table)** 中查找参数所指定的命令,并将找到的命令保存到客户端状态的 cmd 属性里面,是一个 redisCommand 结构 + + 命令查找算法与字母的大小写无关,所以命令名字的大小写不影响命令表的查找结果 + +* 执行预备操作: + + * 检查客户端状态的 cmd 指针是否指向 NULL,根据 redisCommand 检查请求参数的数量是否正确 + * 检查客户端是否通过身份验证 + * 如果服务器打开了 maxmemory 功能,执行命令之前要先检查服务器的内存占用,在有需要时进行内存回收(**逐出算法**) + * 如果服务器上一次执行 BGSAVE 命令出错,并且服务器打开了 stop-writes-on-bgsave-error 功能,那么如果本次执行的是写命令,服务会拒绝执行,并返回错误 + * 如果客户端当前正在用 SUBSCRIBE 或 PSUBSCRIBE 命令订阅频道,那么服务器会拒绝除了 SUBSCRIBE、SUBSCRIBE、 UNSUBSCRIBE、PUNSUBSCRIBE 之外的其他命令 + * 如果服务器正在进行载入数据,只有 sflags 带有 1 标识(比如 INFO、SHUTDOWN、PUBLISH等)的命令才会被执行 + * 如果服务器执行 Lua 脚本而超时并进入阻塞状态,那么只会执行客户端发来的 SHUTDOWN nosave 和 SCRIPT KILL 命令 + * 如果客户端正在执行事务,那么服务器只会执行客户端发来的 EXEC、DISCARD、MULTI、WATCH 四个命令,其他命令都会被**放进事务队列**中 + * 如果服务器打开了监视器功能,那么会将要执行的命令和参数等信息发送给监视器 + +* 调用命令的实现函数:被调用的函数会执行指定的操作并产生相应的命令回复,回复会被保存在客户端状态的输出缓冲区里面(buf 和 reply 属性),然后实现函数还会**为客户端的套接字关联命令回复处理器**,这个处理器负责将命令回复返回给客户端 + +* 执行后续工作: + + * 如果服务器开启了慢查询日志功能,那么慢查询日志模块会检查是否需要为刚刚执行完的命令请求添加一条新的慢查询日志 + * 根据执行命令所耗费的时长,更新命令的 redisCommand 结构的 milliseconds 属性,并将命令 calls 计数器的值增一 + * 如果服务器开启了 AOF 持久化功能,那么 AOF 持久化模块会将执行的命令请求写入到 AOF 缓冲区里面 + * 如果有其他从服务器正在复制当前这个服务器,那么服务器会将执行的命令传播给所有从服务器 + +* 将命令回复发送给客户端:客户端**套接字变为可写状态**时,服务器就会执行命令回复处理器,将客户端输出缓冲区中的命令回复发送给客户端,发送完毕之后回复处理器会清空客户端状态的输出缓冲区,为处理下一个命令请求做好准备 + + + +**** + + + +##### Command + +每个 redisCommand 结构记录了一个Redis 命令的实现信息,主要属性 + +```c +struct redisCommand { + // 命令的名字,比如"set" + char *name; + + // 函数指针,指向命令的实现函数,比如setCommand + // redisCommandProc 类型的定义为 typedef void redisCommandProc(redisClient *c) + redisCommandProc *proc; + + // 命令参数的个数,用于检查命令请求的格式是否正确。如果这个值为负数-N, 那么表示参数的数量大于等于N。 + // 注意命令的名字本身也是一个参数,比如 SET msg "hello",命令的参数是"SET"、"msg"、"hello" 三个 + int arity; + + // 字符串形式的标识值,这个值记录了命令的属性,, + // 比如这个命令是写命令还是读命令,这个命令是否允许在载入数据时使用,是否允许在Lua脚本中使用等等 + char *sflags; + + // 对sflags标识进行分析得出的二进制标识,由程序自动生成。服务器对命令标识进行检查时使用的都是 flags 属性 + // 而不是sflags属性,因为对二进制标识的检查可以方便地通过& ^ ~ 等操作来完成 + int flags; + + // 服务器总共执行了多少次这个命令 + long long calls; + + // 服务器执行这个命令所耗费的总时长 + long long milliseconds; +}; +``` + + + + + +**** + + + +#### serverCron + +##### 基本介绍 + +Redis 服务器以周期性事件的方式来运行 serverCron 函数,服务器初始化时读取配置 server.hz 的值,默认为 10,代表每秒钟执行 10 次,即**每隔 100 毫秒执行一次**,执行指令 info server 可以查看 + +serverCron 函数负责定期对自身的资源和状态进行检查和调整,从而确保服务器可以长期、稳定地运行 + +* 更新服务器的各类统计信息,比如时间、内存占用、 数据库占用情况等 +* 清理数据库中的过期键值对 +* 关闭和清理连接失效的客户端 +* 进行 AOF 或 RDB 持久化操作 +* 如果服务器是主服务器,那么对从服务器进行定期同步 +* 如果处于集群模式,对集群进行定期同步和连接测试 + + + +**** + + + +##### 时间缓存 + +Redis 服务器中有很多功能需要获取系统的当前时间,而每次获取系统的当前时间都需要执行一次系统调用,为了减少系统调用的执行次数,服务器状态中的 unixtime 属性和 mstime 属性被用作当前时间的缓存 + +```c +struct redisServer { + // 保存了秒级精度的系统当前UNIX时间戳 + time_t unixtime; + // 保存了毫秒级精度的系统当前UNIX时间戳 + long long mstime; + +}; +``` + +serverCron 函数默认以每 100 毫秒一次的频率更新两个属性,所以属性记录的时间的精确度并不高 + +* 服务器只会在打印日志、更新服务器的 LRU 时钟、决定是否执行持久化任务、计算服务器上线时间(uptime)这类对时间精确度要求不高的功能上 +* 对于为键设置过期时间、添加慢查询日志这种需要高精确度时间的功能来说,服务器还是会再次执行系统调用,从而获得最准确的系统当前时间 + + + +*** + + + +##### LRU 时钟 + +服务器状态中的 lruclock 属性保存了服务器的 LRU 时钟 + +```c +struct redisServer { + // 默认每10秒更新一次的时钟缓存,用于计算键的空转(idle)时长。 + unsigned lruclock:22; +}; +``` + +每个 Redis 对象都会有一个 lru 属性, 这个 lru 属性保存了对象最后一次被命令访问的时间 + +```c +typedef struct redisObiect { + unsigned lru:22; +} robj; +``` + +当服务器要计算一个数据库键的空转时间(即数据库键对应的值对象的空转时间),程序会用服务器的 lruclock 属性记录的时间减去对象的 lru 属性记录的时间 + +serverCron 函数默认以每 100 毫秒一次的频率更新这个属性,所以得出的空转时间也是模糊的 + + + +*** + + + +##### 命令次数 + +serverCron 中的 trackOperationsPerSecond 函数以每 100 毫秒一次的频率执行,函数功能是以**抽样计算**的方式,估算并记录服务器在最近一秒钟处理的命令请求数量,这个值可以通过 INFO status 命令的 instantaneous_ops_per_sec 域查看: + +```sh +redis> INFO stats +# Stats +instantaneous_ops_per_sec:6 +``` + +根据上一次抽样时间 ops_sec_last_sample_time 和当前系统时间,以及上一次已执行的命令数 ops_sec_last_sample_ops 和服务器当前已经执行的命令数,计算出两次函数调用期间,服务器平均每毫秒处理了多少个命令请求,该值乘以 1000 得到每秒内的执行命令的估计值,放入 ops_sec_samples 环形数组里 + +```c +struct redisServer { + // 上一次进行抽样的时间 + long long ops_sec_last_sample_time; + // 上一次抽样时,服务器已执行命令的数量 + long long ops_sec_last_sample_ops; + // REDIS_OPS_SEC_SAMPLES 大小(默认值为16)的环形数组,数组的每一项记录一次的抽样结果 + long long ops_sec_samples[REDIS_OPS_SEC_SAMPLES]; + // ops_sec_samples数组的索引值,每次抽样后将值自增一,值为16时重置为0,让数组成为一个环形数组 + int ops_sec_idx; +}; +``` + + + + + +*** + + + +##### 内存峰值 + +服务器状态里的 stat_peak_memory 属性记录了服务器内存峰值大小,循环函数每次执行时都会查看服务器当前使用的内存数量,并与 stat_peak_memory 保存的数值进行比较,设置为较大的值 + +```c +struct redisServer { + // 已使用内存峰值 + size_t stat_peak_memory; +}; +``` + +INFO memory 命令的 used_memory_peak 和 used_memory_peak_human 两个域分别以两种格式记录了服务器的内存峰值: + +```sh +redis> INFO memory +# Memory +... +used_memory_peak:501824 +used_memory_peak_human:490.06K +``` + + + +*** + + + +##### SIGTERM + +服务器启动时,Redis 会为服务器进程的 SIGTERM 信号关联处理器 sigtermHandler 函数,该信号处理器负责在服务器接到 SIGTERM 信号时,打开服务器状态的 shutdown_asap 标识 + +```c +struct redisServer { + // 关闭服务器的标识:值为1时关闭服务器,值为0时不做操作 + int shutdown_asap; +}; +``` + +每次 serverCron 函数运行时,程序都会对服务器状态的 shutdown_asap 属性进行检查,并根据属性的值决定是否关闭服务器 + +服务器在接到 SIGTERM 信号之后,关闭服务器并打印相关日志的过程: + +```sh +[6794 | signal handler] (1384435690) Received SIGTERM, scheduling shutdown ... +[6794] 14 Nov 21:28:10.108 # User requested shutdown ... +[6794] 14 Nov 21:28:10.108 * Saving the final RDB snapshot before exiting. +[6794) 14 Nov 21:28:10.161 * DB saved on disk +[6794) 14 Nov 21:28:10.161 # Redisis now ready to exit, bye bye ... +``` + + + +*** + + + +##### 管理资源 + +serverCron 函数每次执行都会调用 clientsCron 和 databasesCron 函数,进行管理客户端资源和数据库资源 + +clientsCron 函数对一定数量的客户端进行以下两个检查: + +* 如果客户端与服务器之间的连接巳经超时(很长一段时间客户端和服务器都没有互动),那么程序释放这个客户端 +* 如果客户端在上一次执行命令请求之后,输入缓冲区的大小超过了一定的长度,那么程序会释放客户端当前的输入缓冲区,并重新创建一个默认大小的输入缓冲区,从而防止客户端的输入缓冲区耗费了过多的内存 + +databasesCron 函数会对服务器中的一部分数据库进行检查,删除其中的过期键,并在有需要时对字典进行收缩操作 + + + +*** + + + +##### 持久状态 + +服务器状态中记录执行 BGSAVE 命令和 BGREWRITEAOF 命令的子进程的 ID + +```c +struct redisServer { + // 记录执行BGSAVE命令的子进程的ID,如果服务器没有在执行BGSAVE,那么这个属性的值为-1 + pid_t rdb_child_pid; + // 记录执行BGREWRITEAOF命令的子进程的ID,如果服务器没有在执行那么这个属性的值为-1 + pid_t aof_child_pid +}; +``` + +serverCron 函数执行时,会检查两个属性的值,只要其中一个属性的值不为 -1,程序就会执行一次 wait3 函数,检查子进程是否有信号发来服务器进程: + +* 如果有信号到达,那么表示新的 RDB 文件已经生成或者 AOF 重写完毕,服务器需要进行相应命令的后续操作,比如用新的 RDB 文件替换现有的 RDB 文件,用重写后的 AOF 文件替换现有的 AOF 文件 +* 如果没有信号到达,那么表示持久化操作未完成,程序不做动作 + +如果两个属性的值都为 -1,表示服务器没有进行持久化操作 + +* 查看是否有 BGREWRITEAOF 被延迟,然后执行 AOF 后台重写 + +* 查看服务器的自动保存条件是否已经被满足,并且服务器没有在进行持久化,就开始一次新的 BGSAVE 操作 + + 因为条件 1 可能会引发一次 AOF,所以在这个检查中会再次确认服务器是否已经在执行持久化操作 + +* 检查服务器设置的 AOF 重写条件是否满足,条件满足并且服务器没有进行持久化,就进行一次 AOF 重写 + +如果服务器开启了 AOF 持久化功能,并且 AOF 缓冲区里还有待写入的数据, 那么 serverCron 函数会调用相应的程序,将 AOF 缓冲区中的内容写入到 AOF 文件里 + + + +*** + + + +##### 延迟执行 + +在服务器执行 BGSAVE 命令的期间,如果客户端发送 BGREWRITEAOF 命令,那么服务器会将 BGREWRITEAOF 命令的执行时间延迟到 BGSAVE 命令执行完毕之后,用服务器状态的 aof_rewrite_scheduled 属性标识延迟与否 + +```c +struct redisServer { + // 如果值为1,那么表示有 BGREWRITEAOF命令被延迟了 + int aof_rewrite_scheduled; +}; +``` + +serverCron 函数会检查 BGSAVE 或者 BGREWRITEAOF 命令是否正在执行,如果这两个命令都没在执行,并且 aof_rewrite_scheduled 属性的值为 1,那么服务器就会执行之前被推延的 BGREWRITEAOF 命令 + + + +**** + + + +##### 执行次数 + +服务器状态的 cronloops 属性记录了 serverCron 函数执行的次数 + +```c +struct redisServer { + // serverCron 函数每执行一次,这个属性的值就增 1 + int cronloops; +}; +``` + + + +**** + + + +##### 缓冲限制 + +服务器会关闭那些输入或者输出**缓冲区大小超出限制**的客户端 + + + + + +**** + + + +#### 初始化 + +##### 初始结构 + +一个 Redis 服务器从启动到能够接受客户端的命令请求,需要经过一系列的初始化和设置过程 + +第一步:创建一个 redisServer 类型的实例变量 server 作为服务器的状态,并为结构中的各个属性设置默认值,由 initServerConfig 函数进行初始化一般属性: + +* 设置服务器的运行 ID、默认运行频率、默认配置文件路径、默认端口号、默认 RDB 持久化条件和 AOF 持久化条件 +* 初始化服务器的 LRU 时钟,创建命令表 + +第二步:载入配置选项,用户可以通过给定配置参数或者指定配置文件,对 server 变量相关属性的默认值进行修改 + +第三步:初始化服务器数据结构(除了命令表之外),因为服务器**必须先载入用户指定的配置选项才能正确地对数据结构进行初始化**,所以载入配置完成后才进性数据结构的初始化,服务器将调用 initServer 函数: + +* server.clients 链表,记录了的客户端的状态结构;server.db 数组,包含了服务器的所有数据库 +* 用于保存频道订阅信息的 server.pubsub_channels 字典, 以及保存模式订阅信息的 server.pubsub_patterns 链表 +* 用于执行 Lua 脚本的 Lua 环境 server.lua +* 保存慢查询日志的 server.slowlog 属性 + +initServer 还进行了非常重要的设置操作: + +* 为服务器设置进程信号处理器 +* 创建共享对象,包含 OK、ERR、**整数 1 到 10000 的字符串对象**等 +* **打开服务器的监听端口** +* **为 serverCron 函数创建时间事件**, 等待服务器正式运行时执行 serverCron 函数 +* 如果 AOF 持久化功能已经打开,那么打开现有的 AOF 文件,如果 AOF 文件不存在,那么创建并打开一个新的 AOF 文件 ,为 AOF 写入做好准备 +* **初始化服务器的后台 I/O 模块**(BIO), 为将来的 I/O 操作做好准备 + +当 initServer 函数执行完毕之后, 服务器将用 ASCII 字符在日志中打印出 Redis 的图标, 以及 Redis 的版本号信息 + + + +*** + + + +##### 还原状态 + +在完成了对服务器状态的初始化之后,服务器需要载入RDB文件或者AOF 文件, 并根据文件记录的内容来还原服务器的数据库状态: + +* 如果服务器启用了 AOF 持久化功能,那么服务器使用 AOF 文件来还原数据库状态 +* 如果服务器没有启用 AOF 持久化功能,那么服务器使用 RDB 文件来还原数据库状态 + +当服务器完成数据库状态还原工作之后,服务器将在日志中打印出载入文件并还原数据库状态所耗费的时长 + +```sh +[7171] 22 Nov 22:43:49.084 * DB loaded from disk: 0.071 seconds +``` + + + +*** + + + +##### 驱动循环 + +在初始化的最后一步,服务器将打印出以下日志,并开始**执行服务器的事件循环**(loop) + +```c +[7171] 22 Nov 22:43:49.084 * The server is now ready to accept connections on pert 6379 +``` + +服务器现在开始可以接受客户端的连接请求,并处理客户端发来的命令请求了 + + + + + +***** + + + +### 慢日志 + +#### 基本介绍 + +Redis 的慢查询日志功能用于记录执行时间超过给定时长的命令请求,通过产生的日志来监视和优化查询速度 + +服务器配置有两个和慢查询日志相关的选项: + +* slowlog-log-slower-than 选项指定执行时间超过多少微秒的命令请求会被记录到日志上 +* slowlog-max-len 选项指定服务器最多保存多少条慢查询日志 + +服务器使用先进先出 FIFO 的方式保存多条慢查询日志,当服务器存储的慢查询日志数量等于 slowlog-max-len 选项的值时,在添加一条新的慢查询日志之前,会先将最旧的一条慢查询日志删除 + +配置选项可以通过 CONFIG SET option value 命令进行设置 + +常用命令: + +```sh +SLOWLOG GET [n] # 查看 n 条服务器保存的慢日志 +SLOWLOG LEN # 查看日志数量 +SLOWLOG RESET # 清除所有慢查询日志 +``` + + + +*** + + + +#### 日志保存 + +服务器状态中包含了慢查询日志功能有关的属性: + +```c +struct redisServer { + // 下一条慢查询日志的ID + long long slowlog_entry_id; + + // 保存了所有慢查询日志的链表 + list *slowlog; + + // 服务器配置选项的值 + long long slowlog-log-slower-than; + // 服务器配置选项的值 + unsigned long slowlog_max_len; +} +``` + +slowlog_entry_id 属性的初始值为 0,每当创建一条新的慢查询日志时,这个属性就会用作新日志的 id 值,之后该属性增一 + +slowlog 链表保存了服务器中的所有慢查询日志,链表中的每个节点是一个 slowlogEntry 结构, 代表一条慢查询日志: + +```c +typedef struct slowlogEntry { + // 唯一标识符 + long long id; + // 命令执行时的时间,格式为UNIX时间戳 + time_t time; + // 执行命令消耗的时间,以微秒为单位 + long long duration; + // 命令与命令参数 + robj **argv; + // 命令与命令参数的数量 + int argc; +} +``` + + + + + +*** + + + +#### 添加日志 + +在每次执行命令的前后,程序都会记录微秒格式的当前 UNIX 时间戳,两个时间之差就是执行命令所耗费的时长,函数会检查命令的执行时长是否超过 slowlog-log-slower-than 选项所设置: + +* 如果是的话,就为命令创建一个新的日志,并将新日志添加到 slowlog 链表的表头 +* 检查慢查询日志的长度是否超过 slowlog-max-len 选项所设置的长度,如果是将多出来的日志从 slowlog 链表中删除掉 + +* 将 redisServer. slowlog_entry_id 的值增 1 + + + + + +*** + + + + + +## 数据结构 + +### 字符串 + +#### SDS + +Redis 构建了简单动态字符串(SDS)的数据类型,作为 Redis 的默认字符串表示,包含字符串的键值对在底层都是由 SDS 实现 + +```c +struct sdshdr { + // 记录buf数组中已使用字节的数量,等于 SDS 所保存字符串的长度 + int len; + + // 记录buf数组中未使用字节的数量 + int free; + + // 【字节】数组,用于保存字符串(不是字符数组) + char buf[]; +}; +``` + +SDS 遵循 C 字符串**以空字符结尾**的惯例,保存空字符的 1 字节不计算在 len 属性,SDS 会自动为空字符分配额外的 1 字节空间和添加空字符到字符串末尾,所以空字符对于 SDS 的使用者来说是完全透明的 + +![](https://seazean.oss-cn-beijing.aliyuncs.com/img/DB/Redis-SDS底层结构.png) + + + +*** + + + +#### 对比 + +常数复杂度获取字符串长度: + +* C 字符串不记录自身的长度,获取时需要遍历整个字符串,遇到空字符串为止,时间复杂度为 O(N) +* SDS 获取字符串长度的时间复杂度为 O(1),设置和更新 SDS 长度由函数底层自动完成 + +杜绝缓冲区溢出: + +* C 字符串调用 strcat 函数拼接字符串时,如果字符串内存不够容纳目标字符串,就会造成缓冲区溢出(Buffer Overflow) + + s1 和 s2 是内存中相邻的字符串,执行 `strcat(s1, " Cluster")`(有空格): + + ![](https://seazean.oss-cn-beijing.aliyuncs.com/img/DB/Redis-内存溢出问题.png) + +* SDS 空间分配策略:当对 SDS 进行修改时,首先检查 SDS 的空间是否满足修改所需的要求, 如果不满足会自动将 SDS 的空间扩展至执行修改所需的大小,然后执行实际的修改操作, 避免了缓冲区溢出的问题 + +二进制安全: + +* C 字符串中的字符必须符合某种编码(比如 ASCII)方式,除了字符串末尾以外其他位置不能包含空字符,否则会被误认为是字符串的结尾,所以只能保存文本数据 +* SDS 的 API 都是二进制安全的,使用字节数组 buf 保存一系列的二进制数据,**使用 len 属性来判断数据的结尾**,所以可以保存图片、视频、压缩文件等二进制数据 + +兼容 C 字符串的函数:SDS 会在为 buf 数组分配空间时多分配一个字节来保存空字符,所以可以重用一部分 C 字符串函数库的函数 + + + +*** + + + +#### 内存 + +C 字符串**每次**增长或者缩短都会进行一次内存重分配,拼接操作通过重分配扩展底层数组空间,截断操作通过重分配释放不使用的内存空间,防止出现内存泄露 + +SDS 通过未使用空间解除了字符串长度和底层数组长度之间的关联,在 SDS 中 buf 数组的长度不一定就是字符数量加一, 数组里面可以包含未使用的字节,字节的数量由 free 属性记录 + +内存重分配涉及复杂的算法,需要执行**系统调用**,是一个比较耗时的操作,SDS 的两种优化策略: + +* 空间预分配:当 SDS 需要进行空间扩展时,程序不仅会为 SDS 分配修改所必需的空间, 还会为 SDS 分配额外的未使用空间 + + * 对 SDS 修改之后,SDS 的长度(len 属性)小于 1MB,程序分配和 len 属性同样大小的未使用空间,此时 len 和 free 相等 + + s 为 Redis,执行 `sdscat(s, " Cluster")` 后,len 变为 13 字节,所以也分配了 13 字节的 free 空间,总长度变为 27 字节(额外的一字节保存空字符,13 + 13 + 1 = 27) + + ![](https://seazean.oss-cn-beijing.aliyuncs.com/img/DB/Redis-SDS内存预分配.png) + + * 对 SDS 修改之后,SDS 的长度大于等于 1MB,程序会分配 1MB 的未使用空间 + + 在扩展 SDS 空间前,API 会先检查 free 空间是否足够,如果足够就无需执行内存重分配,所以通过预分配策略,SDS 将连续增长 N 次字符串所需内存的重分配次数从**必定 N 次降低为最多 N 次** + +* 惰性空间释放:当 SDS 缩短字符串时,程序并不立即使用内存重分配来回收缩短后多出来的字节,而是使用 free 属性将这些字节的数量记录起来,并等待将来复用 + + SDS 提供了相应的 API 来真正释放 SDS 的未使用空间,所以不用担心空间惰性释放策略造成的内存浪费问题 + + + + + +**** + + + +### 链表 + +链表提供了高效的节点重排能力,C 语言并没有内置这种数据结构,所以 Redis 构建了链表数据类型 + +链表节点: + +```c +typedef struct listNode { + // 前置节点 + struct listNode *prev; + + // 后置节点 + struct listNode *next; + + // 节点的值 + void *value +} listNode; +``` + +多个 listNode 通过 prev 和 next 指针组成**双端链表**: + +![](https://seazean.oss-cn-beijing.aliyuncs.com/img/DB/Redis-链表节点底层结构.png) + +list 链表结构:提供了表头指针 head 、表尾指针 tail 以及链表长度计数器 len + +```c +typedef struct list { + // 表头节点 + listNode *head; + // 表尾节点 + listNode *tail; + + // 链表所包含的节点数量 + unsigned long len; + + // 节点值复制函数,用于复制链表节点所保存的值 + void *(*dup) (void *ptr); + // 节点值释放函数,用于释放链表节点所保存的值 + void (*free) (void *ptr); + // 节点值对比函数,用于对比链表节点所保存的值和另一个输入值是否相等 + int (*match) (void *ptr, void *key); +} list; +``` + +![](https://seazean.oss-cn-beijing.aliyuncs.com/img/DB/Redis-链表底层结构.png) + +Redis 链表的特性: + +* 双端:链表节点带有 prev 和 next 指针,获取某个节点的前置节点和后置节点的时间复杂度都是 O(1) +* 无环:表头节点的 prev 指针和表尾节点的 next 指针都指向 NULL,对链表的访问以 NULL 为终点 +* 带表头指针和表尾指针: 通过 list 结构的 head 指针和 tail 指针,获取链表的表头节点和表尾节点的时间复杂度为 O(1) +* 带链表长度计数器:使用 len 属性来对 list 持有的链表节点进行计数,获取链表中节点数量的时间复杂度为 O(1) +* 多态:链表节点使用 void * 指针来保存节点值, 并且可以通过 dup、free 、match 三个属性为节点值设置类型特定函数,所以链表可以保存各种**不同类型的值** + + + + + +**** + + + +### 字典 + +#### 哈希表 + +Redis 字典使用的哈希表结构: + +```c +typedef struct dictht { + // 哈希表数组,数组中每个元素指向 dictEntry 结构 + dictEntry **table; + + // 哈希表大小,数组的长度 + unsigned long size; + + // 哈希表大小掩码,用于计算索引值,总是等于 【size-1】 + unsigned long sizemask; + + // 该哈希表已有节点的数量 + unsigned long used; +} dictht; +``` + +哈希表节点结构: + +```c +typedef struct dictEntry { + // 键 + void *key; + + // 值,可以是一个指针,或者整数 + union { + void *val; // 指针 + uint64_t u64; + int64_t s64; + } + + // 指向下个哈希表节点,形成链表,用来解决冲突问题 + struct dictEntry *next; +} dictEntry; +``` + +![](https://seazean.oss-cn-beijing.aliyuncs.com/img/DB/Redis-哈希表底层结构.png) + + + +*** + + + +#### 字典结构 + +字典,又称为符号表、关联数组、映射(Map),用于保存键值对的数据结构,字典中的每个键都是独一无二的。底层采用哈希表实现,一个哈希表包含多个哈希表节点,每个节点保存一个键值对 + +```c +typedef struct dict { + // 类型特定函数 + dictType *type; + + // 私有数据 + void *privdata; + + // 哈希表,数组中的每个项都是一个dictht哈希表, + // 一般情况下字典只使用 ht[0] 哈希表, ht[1] 哈希表只会在对 ht[0] 哈希表进行 rehash 时使用 + dictht ht[2]; + + // rehash 索引,当 rehash 不在进行时,值为 -1 + int rehashidx; +} dict; +``` + +type 属性和 privdata 属性是针对不同类型的键值对, 为创建多态字典而设置的: + +* type 属性是指向 dictType 结构的指针, 每个 dictType 结构保存了一簇用于操作特定类型键值对的函数, Redis 会为用途不同的字典设置不同的类型特定函数 +* privdata 属性保存了需要传给那些类型特定函数的可选参数 + +![](https://seazean.oss-cn-beijing.aliyuncs.com/img/DB/Redis-字典底层结构.png) + + + +**** + + + +#### 哈希冲突 + +Redis 使用 MurmurHash 算法来计算键的哈希值,这种算法的优点在于,即使输入的键是有规律的,算法仍能给出一个很好的随机分布性,并且算法的计算速度也非常快 + +将一个新的键值对添加到字典里,需要先根据键 key 计算出哈希值,然后进行取模运算(取余): + +```c +index = hash & dict->ht[x].sizemask +``` + +当有两个或以上数量的键被分配到了哈希表数组的同一个索引上时,就称这些键发生了哈希冲突(collision) + +Redis 的哈希表使用链地址法(separate chaining)来解决键哈希冲突, 每个哈希表节点都有一个 next 指针,多个节点通过 next 指针构成一个单向链表,被分配到同一个索引上的多个节点可以用这个单向链表连接起来,这就解决了键冲突的问题 + +dictEntry 节点组成的链表没有指向链表表尾的指针,为了速度考虑,程序总是将新节点添加到链表的表头位置(**头插法**),时间复杂度为 O(1) + +![](https://seazean.oss-cn-beijing.aliyuncs.com/img/DB/Redis-字典解决哈希冲突.png) + + + +**** + + + +#### 负载因子 + +负载因子的计算方式:哈希表中的**节点数量** / 哈希表的大小(**长度**) + +```c +load_factor = ht[0].used / ht[0].size +``` + +为了让哈希表的负载因子(load factor)维持在一个合理的范围之内,当哈希表保存的键值对数量太多或者太少时 ,程序会自动对哈希表的大小进行相应的扩展或者收缩 + +哈希表执行扩容的条件: + +* 服务器没有执行 BGSAVE 或者 BGREWRITEAOF 命令,哈希表的负载因子大于等于 1 + +* 服务器正在执行 BGSAVE 或者 BGREWRITEAOF 命令,哈希表的负载因子大于等于 5 + + 原因:执行该命令的过程中,Redis 需要创建当前服务器进程的子进程,而大多数操作系统都采用写时复制(copy-on­-write)技术来优化子进程的使用效率,通过提高执行扩展操作的负载因子,尽可能地避免在子进程存在期间进行哈希表扩展操作,可以避免不必要的内存写入操作,最大限度地节约内存 + +哈希表执行收缩的条件:负载因子小于 0.1(自动执行,servreCron 中检测) + + + +*** + + + +#### 重新散列 + +扩展和收缩哈希表的操作通过 rehash(重新散列)来完成,步骤如下: + +* 为字典的 ht[1] 哈希表分配空间,空间大小的分配情况: + * 如果执行的是扩展操作,ht[1] 的大小为第一个大于等于 $ht[0].used * 2$ 的 $2^n$ + * 如果执行的是收缩操作,ht[1] 的大小为第一个大于等于 $ht[0].used$ 的 $2^n$ +* 将保存在 ht[0] 中所有的键值对重新计算哈希值和索引值,迁移到 ht[1] 上 +* 当 ht[0] 包含的所有键值对都迁移到了 ht[1] 之后(ht[0] 变为空表),释放 ht[0],将 ht[1] 设置为 ht[0],并在 ht[1] 创建一个新的空白哈希表,为下一次 rehash 做准备 + +如果哈希表里保存的键值对数量很少,rehash 就可以在瞬间完成,但是如果哈希表里数据很多,那么要一次性将这些键值对全部 rehash 到 ht[1] 需要大量计算,可能会导致服务器在一段时间内停止服务 + +Redis 对 rehash 做了优化,使 rehash 的动作并不是一次性、集中式的完成,而是分多次,渐进式的完成,又叫**渐进式 rehash** + +* 为 ht[1] 分配空间,此时字典同时持有 ht[0] 和 ht[1] 两个哈希表 +* 在字典中维护了一个索引计数器变量 rehashidx,并将变量的值设为 0,表示 rehash 正式开始 +* 在 rehash 进行期间,每次对字典执行增删改查操作时,程序除了执行指定的操作以外,还会顺带将 ht[0] 哈希表在 rehashidx 索引上的所有键值对 rehash 到 ht[1],rehash 完成之后**将 rehashidx 属性的值增一** +* 随着字典操作的不断执行,最终在某个时间点 ht[0] 的所有键值对都被 rehash 至 ht[1],将 rehashidx 属性的值设为 -1 + +渐进式 rehash 采用**分而治之**的方式,将 rehash 键值对所需的计算工作均摊到对字典的每个添加、删除、查找和更新操作上,从而避免了集中式 rehash 带来的庞大计算量 + +渐进式 rehash 期间的哈希表操作: + +* 字典的查找、删除、更新操作会在两个哈希表上进行,比如查找一个键会先在 ht[0] 上查找,查找不到就去 ht[1] 继续查找 +* 字典的添加操作会直接在 ht[1] 上添加,不在 ht[0] 上进行任何添加 + + + + + +**** + + + +### 跳跃表 + +#### 底层结构 + +跳跃表(skiplist)是一种有序(**默认升序**)的数据结构,在链表的基础上**增加了多级索引以提升查找的效率**,索引是占内存的,所以是一个**空间换时间**的方案,跳表平均 O(logN)、最坏 O(N) 复杂度的节点查找,效率与平衡树相当但是实现更简单 + +原始链表中存储的有可能是很大的对象,而索引结点只需要存储关键值和几个指针,并不需要存储对象,因此当节点本身比较大或者元素数量比较多的时候,其优势可以被放大,而缺点(占内存)则可以忽略 + +Redis 只在两个地方应用了跳跃表,一个是实现有序集合键,另一个是在集群节点中用作内部数据结构 + +```c +typedef struct zskiplist { + // 表头节点和表尾节点,O(1) 的时间复杂度定位头尾节点 + struct skiplistNode *head, *tail; + + // 表的长度,也就是表内的节点数量 (表头节点不计算在内) + unsigned long length; + + // 表中层数最大的节点的层数 (表头节点的层高不计算在内) + int level +} zskiplist; +``` + +```c +typedef struct zskiplistNode { + // 层 + struct zskiplistLevel { + // 前进指针 + struct zskiplistNode *forward; + // 跨度 + unsigned int span; + } level[]; + + // 后退指针 + struct zskiplistNode *backward; + + // 分值 + double score; + + // 成员对象 + robj *obj; +} zskiplistNode; +``` + +![](https://seazean.oss-cn-beijing.aliyuncs.com/img/DB/Redis-跳表底层结构.png) + + + +*** + + + +#### 属性分析 + +层:level 数组包含多个元素,每个元素包含指向其他节点的指针。根据幕次定律(power law,越大的数出现的概率越小)**随机**生成一个介于 1 和 32 之间的值(Redis5 之后最大为 64)作为 level 数组的大小,这个大小就是层的高度,节点的第一层是 level[0] = L1 + +前进指针:forward 用于从表头到表尾方向**正序(升序)遍历节点**,遇到 NULL 停止遍历 + +跨度:span 用于记录两个节点之间的距离,用来计算排位(rank): + +* 两个节点之间的跨度越大相距的就越远,指向 NULL 的所有前进指针的跨度都为 0 + +* 在查找某个节点的过程中,**将沿途访问过的所有层的跨度累计起来,结果就是目标节点在跳跃表中的排位**,按照上图所示: + + 查找分值为 3.0 的节点,沿途经历的层:查找的过程只经过了一个层,并且层的跨度为 3,所以目标节点在跳跃表中的排位为 3 + + 查找分值为 2.0 的节点,沿途经历的层:经过了两个跨度为 1 的节点,因此可以计算出目标节点在跳跃表中的排位为 2 + +后退指针:backward 用于从表尾到表头方向**逆序(降序)遍历节点** + +分值:score 属性一个 double 类型的浮点数,跳跃表中的所有节点都按分值从小到大来排序 + +成员对象:obj 属性是一个指针,指向一个 SDS 字符串对象。同一个跳跃表中,各个节点保存的**成员对象必须是唯一的**,但是多个节点保存的分值可以是相同的,分值相同的节点将按照成员对象在字典序中的大小来进行排序(从小到大) + + + +个人笔记:JUC → 并发包 → ConcurrentSkipListMap 详解跳跃表 + + + +**** + + + +### 整数集合 + +#### 底层结构 + +整数集合(intset)是用于保存整数值的集合数据结构,是 Redis 集合键的底层实现之一 + +```c +typedef struct intset { + // 编码方式 + uint32_t encoding; + + // 集合包含的元素数量,也就是 contents 数组的长度 + uint32_t length; + + // 保存元素的数组 + int8_t contents[]; +} intset; +``` + +encoding 取值为三种:INTSET_ENC_INT16、INTSET_ENC_INT32、INTSET_ENC_INT64 + +整数集合的每个元素都是 contents 数组的一个数组项(item),在数组中按值的大小从小到大**有序排列**,并且数组中**不包含任何重复项**。虽然 contents 属性声明为 int8_t 类型,但实际上数组并不保存任何 int8_t 类型的值, 真正类型取决于 encoding 属性 + +![](https://seazean.oss-cn-beijing.aliyuncs.com/img/DB/Redis-整数集合底层结构.png) + +说明:底层存储结构是数组,所以为了保证有序性和不重复性,每次添加一个元素的时间复杂度是 O(N) + + + +**** + + + +#### 类型升级 + +整数集合添加的新元素的类型比集合现有所有元素的类型都要长时,需要先进行升级(upgrade),升级流程: + +* 根据新元素的类型长度以及集合元素的数量(包括新元素在内),扩展整数集合底层数组的空间大小 + +* 将底层数组现有的所有元素都转换成与新元素相同的类型,并将转换后的元素放入正确的位置,放置过程保证数组的有序性 + + 图示 32 * 4 = 128 位,首先将 3 放入索引 2(64 位 - 95 位),然后将 2 放置索引 1,将 1 放置在索引 0,从后向前依次放置在对应的区间,最后放置 65535 元素到索引 3(96 位- 127 位),修改 length 属性为 4 + +* 将新元素添加到底层数组里 + +![](https://seazean.oss-cn-beijing.aliyuncs.com/img/DB/Redis-整数集合升级.png) + +每次向整数集合添加新元素都可能会引起升级,而每次升级都需要对底层数组中的所有元素进行类型转换,所以向整数集合添加新元素的时间复杂度为 O(N) + +引发升级的新元素的长度总是比整数集合现有所有元素的长度都大,所以这个新元素的值要么就大于所有现有元素,要么就小于所有现有元素,升级之后新元素的摆放位置: + +* 在新元素小于所有现有元素的情况下,新元素会被放置在底层数组的最开头(索引 0) +* 在新元素大于所有现有元素的情况下,新元素会被放置在底层数组的最末尾(索引 length-1) + +整数集合升级策略的优点: + +* 提升整数集合的灵活性:C 语言是静态类型语言,为了避免类型错误通常不会将两种不同类型的值放在同一个数据结构里面,整数集合可以自动升级底层数组来适应新元素,所以可以随意的添加整数 + +* 节约内存:要让数组可以同时保存 int16、int32、int64 三种类型的值,可以直接使用 int64_t 类型的数组作为整数集合的底层实现,但是会造成内存浪费,整数集合可以确保升级操作只会在有需要的时候进行,尽量节省内存 + +整数集合**不支持降级操作**,一旦对数组进行了升级,编码就会一直保持升级后的状态 + + + + + +***** + + + +### 压缩列表 + +#### 底层结构 + +压缩列表(ziplist)是 Redis 为了节约内存而开发的,是列表键和哈希键的底层实现之一。是由一系列特殊编码的连续内存块组成的顺序型(sequential)数据结构,一个压缩列表可以包含任意多个节点(entry),每个节点可以保存一个字节数组或者一个整数值 + +![](https://seazean.oss-cn-beijing.aliyuncs.com/img/DB/Redis-压缩列表底层结构.png) + +* zlbytes:uint32_t 类型 4 字节,记录整个压缩列表占用的内存字节数,在对压缩列表进行内存重分配或者计算 zlend 的位置时使用 +* zltail:uint32_t 类型 4 字节,记录压缩列表表尾节点距离起始地址有多少字节,通过这个偏移量程序无须遍历整个压缩列表就可以确定表尾节点的地址 +* zllen:uint16_t 类型 2 字节,记录了压缩列表包含的节点数量,当该属性的值小于 UINT16_MAX (65535) 时,该值就是压缩列表中节点的数量;当这个值等于 UINT16_MAX 时节点的真实数量需要遍历整个压缩列表才能计算得出 +* entryX:列表节点,压缩列表中的各个节点,**节点的长度由节点保存的内容决定** +* zlend:uint8_t 类型 1 字节,是一个特殊值 0xFF (255),用于标记压缩列表的末端 + +![](https://seazean.oss-cn-beijing.aliyuncs.com/img/DB/Redis-压缩列表示例.png) + +列表 zlbytes 属性的值为 0x50 (十进制 80),表示压缩列表的总长为 80 字节,列表 zltail 属性的值为 0x3c (十进制 60),假设表的起始地址为 p,计算得出表尾节点 entry3 的地址 p + 60 + + + +**** + + + +#### 列表节点 + +列表节点 entry 的数据结构: + +![](https://seazean.oss-cn-beijing.aliyuncs.com/img/DB/Redis-压缩列表节点.png) + +previous_entry_length:以字节为单位记录了压缩列表中前一个节点的长度,程序可以通过指针运算,根据当前节点的起始地址来计算出前一个节点的起始地址,完成**从表尾向表头遍历**操作 + +* 如果前一节点的长度小于 254 字节,该属性的长度为 1 字节,前一节点的长度就保存在这一个字节里 +* 如果前一节点的长度大于等于 254 字节,该属性的长度为 5 字节,其中第一字节会被设置为 0xFE(十进制 254),之后的四个字节则用于保存前一节点的长度 + +encoding:记录了节点的 content 属性所保存的数据类型和长度 + +* **长度为 1 字节、2 字节或者 5 字节**,值的最高位为 00、01 或者 10 的是字节数组编码,数组的长度由编码除去最高两位之后的其他位记录,下划线 `_` 表示留空,而 `b`、`x` 等变量则代表实际的二进制数据 + + ![](https://seazean.oss-cn-beijing.aliyuncs.com/img/DB/Redis-压缩列表字节数组编码.png) + +* 长度为 1 字节,值的最高位为 11 的是整数编码,整数值的类型和长度由编码除去最高两位之后的其他位记录 + + ![](https://seazean.oss-cn-beijing.aliyuncs.com/img/DB/Redis-压缩列表整数编码.png) + +content:每个压缩列表节点可以保存一个字节数组或者一个整数值 + +* 字节数组可以是以下三种长度的其中一种: + + * 长度小于等于 $63 (2^6-1)$ 字节的字节数组 + + * 长度小于等于 $16383(2^{14}-1)$ 字节的字节数组 + + * 长度小于等于 $4294967295(2^{32}-1)$ 字节的字节数组 + +* 整数值则可以是以下六种长度的其中一种: + + * 4 位长,介于 0 至 12 之间的无符号整数 + + * 1 字节长的有符号整数 + + * 3 字节长的有符号整数 + + * int16_t 类型整数 + + * int32_t 类型整数 + + * int64_t 类型整数 + + + +*** + + + +#### 连锁更新 + +Redis 将在特殊情况下产生的连续多次空间扩展操作称之为连锁更新(cascade update) + +假设在一个压缩列表中,有多个连续的、长度介于 250 到 253 字节之间的节点 e1 至 eN。将一个长度大于等于 254 字节的新节点 new 设置为压缩列表的头节点,new 就成为 e1 的前置节点。e1 的 previous_entry_length 属性仅为 1 字节,无法保存新节点 new 的长度,所以要对压缩列表执行空间重分配操作,并将 e1 节点的 previous_entry_length 属性从 1 字节长扩展为 5 字节长。由于 e1 原本的长度介于 250 至 253 字节之间,所以扩展后 e1 的长度就变成了 254 至 257 字节之间,导致 e2 的 previous_entry_length 属性无法保存 e1 的长度,程序需要不断地对压缩列表执行空间重分配操作,直到 eN 为止 + +![](https://seazean.oss-cn-beijing.aliyuncs.com/img/DB/Redis-压缩列表连锁更新1.png) + + 删除节点也可能会引发连锁更新,big.length >= 254,small.length < 254,删除 small 节点 + +![](https://seazean.oss-cn-beijing.aliyuncs.com/img/DB/Redis-压缩列表连锁更新2.png) + +连锁更新在最坏情况下需要对压缩列表执行 N 次空间重分配,每次重分配的最坏复杂度为 O(N),所以连锁更新的最坏复杂度为 O(N^2) + +说明:尽管连锁更新的复杂度较高,但出现的记录是非常低的,即使出现只要被更新的节点数量不多,就不会对性能造成影响 + + + + + +**** + + + + + +## 数据类型 + +### redisObj + +#### 对象系统 + +Redis 使用对象来表示数据库中的键和值,当在 Redis 数据库中新创建一个键值对时至少会创建两个对象,一个对象用作键值对的键(**键对象**),另一个对象用作键值对的值(**值对象**) + +Redis 中对象由一个 redisObject 结构表示,该结构中和保存数据有关的三个属性分别是 type、 encoding、ptr: + +```c +typedef struct redisObiect { + // 类型 + unsigned type:4; + // 编码 + unsigned encoding:4; + // 指向底层数据结构的指针 + void *ptr; + + // .... +} robj; +``` + +Redis 并没有直接使用数据结构来实现键值对数据库,而是基于这些数据结构创建了一个对象系统,包含字符串对象、列表对象、哈希对象、集合对象和有序集合对象这五种类型的对象,而每种对象又通过不同的编码映射到不同的底层数据结构 + +Redis 是一个 Map 类型,其中所有的数据都是采用 key : value 的形式存储,**键对象都是字符串对象**,而值对象有五种基本类型和三种高级类型对象 + +![](https://seazean.oss-cn-beijing.aliyuncs.com/img/DB/Redis-对象编码.png) + +* 对一个数据库键执行 TYPE 命令,返回的结果为数据库键对应的值对象的类型,而不是键对象的类型 +* 对一个数据库键执行 OBJECT ENCODING 命令,查看数据库键对应的值对象的编码 + + + +**** + + + +#### 命令多态 + +Redis 中用于操作键的命令分为两种类型: + +* 一种命令可以对任何类型的键执行,比如说 DEL 、EXPIRE、RENAME、 TYPE 等(基于类型的多态) +* 只能对特定类型的键执行,比如 SET 只能对字符串键执行、HSET 对哈希键执行、SADD 对集合键执行,如果类型步匹配会报类型错误: `(error) WRONGTYPE Operation against a key holding the wrong kind of value` + +Redis 为了确保只有指定类型的键可以执行某些特定的命令,在执行类型特定的命令之前,先通过值对象 redisObject 结构 type 属性检查操作类型是否正确,然后再决定是否执行指定的命令 + +对于多态命令,比如列表对象有 ziplist 和 linkedlist 两种实现方式,通过 redisObject 结构 encoding 属性确定具体的编码类型,底层调用对应的 API 实现具体的操作(基于编码的多态) + + + +*** + + + +#### 内存回收 + +对象的整个生命周期可以划分为创建对象、 操作对象、 释放对象三个阶段 + +C 语言没有自动回收内存的功能,所以 Redis 在对象系统中构建了引用计数(reference counting)技术实现的内存回收机制,程序可以跟踪对象的引用计数信息,在适当的时候自动释放对象并进行内存回收 + +```c +typedef struct redisObiect { + // 引用计数 + int refcount; +} robj; +``` + +对象的引用计数信息会随着对象的使用状态而不断变化,创建时引用计数 refcount 初始化为 1,每次被一个新程序使用时引用计数加 1,当对象不再被一个程序使用时引用计数值会被减 1,当对象的引用计数值变为 0 时,对象所占用的内存会被释放 + + + +*** + + + +#### 对象共享 + +对象的引用计数属性带有对象共享的作用,共享对象机制更节约内存,数据库中保存的相同值对象越多,节约的内存就越多 + +让多个键共享一个对象的步骤: + +* 将数据库键的值指针指向一个现有的值对象 + +* 将被共享的值对象的引用计数增一 + + + +Redis 在初始化服务器时创建一万个(配置文件可以修改)字符串对象,包含了**从 0 到 9999 的所有整数值**,当服务器需要用到值为 0 到 9999 的字符串对象时,服务器就会使用这些共享对象,而不是新创建对象 + +比如创建一个值为 100 的键 A,并使用 OBJECT REFCOUNT 命令查看键 A 的值对象的引用计数,会发现值对象的引用计数为 2,引用这个值对象的两个程序分别是持有这个值对象的服务器程序,以及共享这个值对象的键 A + +共享对象在嵌套了字符串对象的对象(linkedlist 编码的列表、hashtable 编码的哈希、zset 编码的有序集合)中也能使用 + +Redis 不共享包含字符串对象的原因:验证共享对象和目标对象是否相同的复杂度越高,消耗的 CPU 时间也会越多 + +* 整数值的字符串对象, 验证操作的复杂度为 O(1) +* 字符串值的字符串对象, 验证操作的复杂度为 O(N) +* 如果共享对象是包含了多个值(或者对象的)对象,比如列表对象或者哈希对象,验证操作的复杂度为 O(N^2) + + + +**** + + + +#### 空转时长 + +redisObject 结构包含一个 lru 属性,该属性记录了对象最后一次被命令程序访问的时间 + +```c +typedef struct redisObiect { + unsigned lru:22; +} robj; +``` + +OBJECT IDLETIME 命令可以打印出给定键的空转时长,该值就是通过将当前时间减去键的值对象的 lru 时间计算得出的,这个命令在访问键的值对象时,不会修改值对象的 lru 属性 + +```sh +redis> OBJECT IDLETIME msg +(integer) 10 +# 等待一分钟 +redis> OBJECT IDLETIME msg +(integer) 70 +# 访问 msg +redis> GET msg +"hello world" +# 键处于活跃状态,空转时长为 0 +redis> OBJECT IDLETIME msg +(integer) 0 +``` + +空转时长的作用:如果服务器开启 maxmemory 选项,并且回收内存的算法为 volatile-lru 或者 allkeys-lru,那么当服务器占用的内存数超过了 maxmemory 所设置的上限值时,空转时长较高的那部分键会优先被服务器释放,从而回收内存(LRU 算法) + + + + + +*** + + + +### string + +#### 简介 + +存储的数据:单个数据,最简单的数据存储类型,也是最常用的数据存储类型,实质上是存一个字符串,string 类型是二进制安全的,可以包含任何数据,比如图片或者序列化的对象 + +存储数据的格式:一个存储空间保存一个数据,每一个空间中只能保存一个字符串信息 + +存储内容:通常使用字符串,如果字符串以整数的形式展示,可以作为数字操作使用 + + + +Redis 所有操作都是**原子性**的,采用**单线程**机制,命令是单个顺序执行,无需考虑并发带来影响,原子性就是有一个失败则都失败 + +字符串对象可以是 int、raw、embstr 三种实现方式 + + + +*** + + + +#### 操作 + +指令操作: + +* 数据操作: + + ```sh + set key value #添加/修改数据添加/修改数据 + del key #删除数据 + setnx key value #判定性添加数据,键值为空则设添加 + mset k1 v1 k2 v2... #添加/修改多个数据,m:Multiple + append key value #追加信息到原始信息后部(如果原始信息存在就追加,否则新建) + ``` + +* 查询操作 + + ```sh + get key #获取数据,如果不存在,返回空(nil) + mget key1 key2... #获取多个数据 + strlen key #获取数据字符个数(字符串长度) + ``` + +* 设置数值数据增加/减少指定范围的值 + + ```sh + incr key #key++ + incrby key increment #key+increment + incrbyfloat key increment #对小数操作 + decr key #key-- + decrby key increment #key-increment + ``` + +* 设置数据具有指定的生命周期 + + ```sh + setex key seconds value #设置key-value存活时间,seconds单位是秒 + psetex key milliseconds value #毫秒级 + ``` + +注意事项: + +1. 数据操作不成功的反馈与数据正常操作之间的差异 + + * 表示运行结果是否成功 + + * (integer) 0 → false ,失败 + + * (integer) 1 → true,成功 + + * 表示运行结果值 + + * (integer) 3 → 3 个 + + * (integer) 1 → 1 个 + +2. 数据未获取到时,对应的数据为(nil),等同于null + +3. **数据最大存储量**:512MB + +4. string 在 Redis 内部存储默认就是一个字符串,当遇到增减类操作 incr,decr 时**会转成数值型**进行计算 + +5. 按数值进行操作的数据,如果原始数据不能转成数值,或超越了Redis 数值上限范围,将报错 + 9223372036854775807(java 中 Long 型数据最大值,Long.MAX_VALUE) + +6. Redis 可用于控制数据库表主键 ID,为数据库表主键提供生成策略,保障数据库表的主键唯一性 + + +单数据和多数据的选择: + +* 单数据执行 3 条指令的过程:3 次发送 + 3 次处理 + 3 次返回 +* 多数据执行 1 条指令的过程:1 次发送 + 3 次处理 + 1 次返回(发送和返回的事件略高于单数据) + + + + + + + +*** + + + +#### 实现 + +字符串对象的编码可以是 int、raw、embstr 三种 + +* int:字符串对象保存的是**整数值**,并且整数值可以用 long 类型来表示,那么对象会将整数值保存在字符串对象结构的 ptr 属性面(将 void * 转换成 long),并将字符串对象的编码设置为 int(浮点数用另外两种方式) + + + +* raw:字符串对象保存的是一个字符串值,并且值的长度大于 39 字节,那么对象将使用简单动态字符串(SDS)来保存该值,并将对象的编码设置为 raw + + ![](https://seazean.oss-cn-beijing.aliyuncs.com/img/DB/Redis-字符串对象raw编码.png) + +* embstr:字符串对象保存的是一个字符串值,并且值的长度小于等于 39 字节,那么对象将使用 embstr 编码的方式来保存这个字符串值,并将对象的编码设置为 embstr + + ![](https://seazean.oss-cn-beijing.aliyuncs.com/img/DB/Redis-字符串对象embstr编码.png) + + 上图所示,embstr 与 raw 都使用了 redisObject 和 sdshdr 来表示字符串对象,但是 raw 需要调用两次内存分配函数分别创建两种结构,embstr 只需要一次内存分配来分配一块**连续的空间** + +embstr 是用于保存短字符串的一种编码方式,对比 raw 的优点: + +* 内存分配次数从两次降低为一次,同样释放内存的次数也从两次变为一次 +* embstr 编码的字符串对象的数据都保存在同一块连续内存,所以比 raw 编码能够更好地利用缓存优势(局部性原理) + +int 和 embstr 编码的字符串对象在条件满足的情况下,会被转换为 raw 编码的字符串对象: + +* int 编码的整数值,执行 APPEND 命令追加一个字符串值,先将整数值转为字符串然后追加,最后得到一个 raw 编码的对象 +* Redis 没有为 embstr 编码的字符串对象编写任何相应的修改程序,所以 embstr 对象实际上**是只读的**,执行修改命令会将对象的编码从 embstr 转换成 raw,操作完成后得到一个 raw 编码的对象 + +某些情况下,程序会将字符串对象里面的字符串值转换回浮点数值,执行某些操作后再将浮点数值转换回字符串值: + +```sh +redis> SET pi 3.14 +OK +redis> OBJECT ENCODING pi +"embstr" +redis> INCRBYFLOAT pi 2.0 # 转为浮点数执行增加的操作 +"5. 14" +redis> OBJECT ENCODING pi +"embstr" +``` + + + + + + + +**** + + + +#### 应用 + +主页高频访问信息显示控制,例如新浪微博大 V 主页显示粉丝数与微博数量 + +* 在 Redis 中为大 V 用户设定用户信息,以用户主键和属性值作为 key,后台设定定时刷新策略 + + ```sh + set user:id:3506728370:fans 12210947 + set user:id:3506728370:blogs 6164 + set user:id:3506728370:focuses 83 + ``` + +* 使用 JSON 格式保存数据 + + ```sh + user:id:3506728370 → {"fans":12210947,"blogs":6164,"focuses":83} + ``` + +* key的设置约定:表名 : 主键名 : 主键值 : 字段名 + + | 表名 | 主键名 | 主键值 | 字段名 | + | ----- | ------ | --------- | ------ | + | order | id | 29437595 | name | + | equip | id | 390472345 | type | + | news | id | 202004150 | title | + + + + + +*** + + + +### hash + +#### 简介 + +数据存储需求:对一系列存储的数据进行编组,方便管理,典型应用存储对象信息 + +数据存储结构:一个存储空间保存多个键值对数据 + +hash 类型:底层使用**哈希表**结构实现数据存储 + + + +Redis 中的 hash 类似于 Java 中的 `Map>`,左边是 key,右边是值,中间叫 field 字段,本质上 **hash 存了一个 key-value 的存储空间** + +hash 是指的一个数据类型,并不是一个数据 + +* 如果 field 数量较少,存储结构优化为**压缩列表结构**(有序) +* 如果 field 数量较多,存储结构使用 HashMap 结构(无序) + + + +*** + + + +#### 操作 + +指令操作: + +* 数据操作 + + ```sh + hset key field value #添加/修改数据 + hdel key field1 [field2] #删除数据,[]代表可选 + hsetnx key field value #设置field的值,如果该field存在则不做任何操作 + hmset key f1 v1 f2 v2... #添加/修改多个数据 + ``` + +* 查询操作 + + ```sh + hget key field #获取指定field对应数据 + hgetall key #获取指定key所有数据 + hmget key field1 field2... #获取多个数据 + hexists key field #获取哈希表中是否存在指定的字段 + hlen key #获取哈希表中字段的数量 + ``` + +* 获取哈希表中所有的字段名或字段值 + + ```sh + hkeys key #获取所有的field + hvals key #获取所有的value + ``` + +* 设置指定字段的数值数据增加指定范围的值 + + ```sh + hincrby key field increment #指定字段的数值数据增加指定的值,increment为负数则减少 + hincrbyfloat key field increment#操作小数 + ``` + + +注意事项 + +1. hash 类型中 value 只能存储字符串,不允许存储其他数据类型,不存在嵌套现象,如果数据未获取到,对应的值为(nil) +2. 每个 hash 可以存储 2^32 - 1 个键值对 +3. hash 类型和对象的数据存储形式相似,并且可以灵活添加删除对象属性。但 hash 设计初衷不是为了存储大量对象而设计的,不可滥用,不可将 hash 作为对象列表使用 +4. hgetall 操作可以获取全部属性,如果内部 field 过多,遍历整体数据效率就很会低,有可能成为数据访问瓶颈 + + + +*** + + + +#### 实现 + +哈希对象的内部编码有两种:ziplist(压缩列表)、hashtable(哈希表、字典) + +* 压缩列表实现哈希对象:同一键值对的节点总是挨在一起,保存键的节点在前,保存值的节点在后 + + ![](https://seazean.oss-cn-beijing.aliyuncs.com/img/DB/Redis-哈希对象ziplist.png) + +* 字典实现哈希对象:字典的每一个键都是一个字符串对象,每个值也是 + + + +当存储的数据量比较小的情况下,Redis 才使用压缩列表来实现字典类型,具体需要满足两个条件: + +- 当键值对数量小于 hash-max-ziplist-entries 配置(默认 512 个) +- 所有键和值的长度都小于 hash-max-ziplist-value 配置(默认 64 字节) + +以上两个条件的上限值是可以通过配置文件修改的,当两个条件的任意一个不能被满足时,对象的编码转换操作就会被执行 + +ziplist 使用更加紧凑的结构实现多个元素的连续存储,所以在节省内存方面比 hashtable 更加优秀,当 ziplist 无法满足哈希类型时,Redis 会使用 hashtable 作为哈希的内部实现,因为此时 ziplist 的读写效率会下降,而 hashtable 的读写时间复杂度为 O(1) + + + +*** + + + +#### 应用 + +```sh +user:id:3506728370 → {"name":"春晚","fans":12210862,"blogs":83} +``` + +对于以上数据,使用单条去存的话,存的条数会很多。但如果用 json 格式,存一条数据就够了。 + +假如现在粉丝数量发生了变化,要把整个值都改变,但是用单条存就不存在这个问题,只需要改其中一个就可以 + + + +可以实现购物车的功能,key 对应着每个用户,存储空间存储购物车的信息 + + + + + +*** + + + +### list + +#### 简介 + +数据存储需求:存储多个数据,并对数据进入存储空间的顺序进行区分 + +数据存储结构:一个存储空间保存多个数据,且通过数据可以体现进入顺序,允许重复元素 + +list 类型:保存多个数据,底层使用**双向链表**存储结构实现,类似于 LinkedList + + + +如果两端都能存取数据的话,这就是双端队列,如果只能从一端进一端出,这个模型叫栈 + + + +*** + + + +#### 操作 + +指令操作: + +* 数据操作 + + ```sh + lpush key value1 [value2]...#从左边添加/修改数据(表头) + rpush key value1 [value2]...#从右边添加/修改数据(表尾) + lpop key #从左边获取并移除第一个数据,类似于出栈/出队 + rpop key #从右边获取并移除第一个数据 + lrem key count value #删除指定数据,count=2删除2个,该value可能有多个(重复数据) + ``` + +* 查询操作 + + ```sh + lrange key start stop #从左边遍历数据并指定开始和结束索引,0是第一个索引,-1是终索引 + lindex key index #获取指定索引数据,没有则为nil,没有索引越界 + llen key #list中数据长度/个数 + ``` + +* 规定时间内获取并移除数据 + + ```sh + b #代表阻塞 + blpop key1 [key2] timeout #在指定时间内获取指定key(可以多个)的数据,超时则为(nil) + #可以从其他客户端写数据,当前客户端阻塞读取数据 + brpop key1 [key2] timeout #从右边操作 + ``` + +* 复制操作 + + ```sh + brpoplpush source destination timeout #从source获取数据放入destination,假如在指定时间内没有任何元素被弹出,则返回一个nil和等待时长。反之,返回一个含有两个元素的列表,第一个元素是被弹出元素的值,第二个元素是等待时长 + ``` + +注意事项 + +1. list 中保存的数据都是 string 类型的,数据总容量是有限的,最多 2^32 - 1 个元素(4294967295) +2. list 具有索引的概念,但操作数据时通常以队列的形式进行入队出队,或以栈的形式进行入栈出栈 +3. 获取全部数据操作结束索引设置为 -1 +4. list 可以对数据进行分页操作,通常第一页的信息来自于 list,第 2 页及更多的信息通过数据库的形式加载 + + + +**** + + + +#### 实现 + +在 Redis3.2 版本以前列表对象的内部编码有两种:ziplist(压缩列表)和 linkedlist(链表) + +* 压缩列表实现的列表对象:PUSH 1、three、5 三个元素 + + ![](https://seazean.oss-cn-beijing.aliyuncs.com/img/DB/Redis-列表对象ziplist.png) + +* 链表实现的列表对象:为了简化字符串对象的表示,使用了 StringObject 的结构,底层其实是 sdshdr 结构 + + ![](https://seazean.oss-cn-beijing.aliyuncs.com/img/DB/Redis-列表对象linkedlist.png) + +列表中存储的数据量比较小的时候,列表就会使用一块连续的内存存储,采用压缩列表的方式实现的条件: + +* 列表对象保存的所有字符串元素的长度都小于 64 字节 +* 列表对象保存的元素数量小于 512 个 + +以上两个条件的上限值是可以通过配置文件修改的,当两个条件的任意一个不能被满足时,对象的编码转换操作就会被执行 + +在 Redis3.2 版本 以后对列表数据结构进行了改造,使用 **quicklist(快速列表)**代替了 linkedlist,quicklist 实际上是 ziplist 和 linkedlist 的混合体,将 linkedlist 按段切分,每一段使用 ziplist 来紧凑存储,多个 ziplist 之间使用双向指针串接起来,既满足了快速的插入删除性能,又不会出现太大的空间冗余 + + + + + +*** + + + +#### 应用 + +企业运营过程中,系统将产生出大量的运营数据,如何保障多台服务器操作日志的统一顺序输出? + +* 依赖 list 的数据具有顺序的特征对信息进行管理,右进左查或者左近左查 +* 使用队列模型解决多路信息汇总合并的问题 +* 使用栈模型解决最新消息的问题 + +微信文章订阅公众号: + +* 比如订阅了两个公众号,它们发布了两篇文章,文章 ID 分别为 666 和 888,可以通过执行 `LPUSH key 666 888` 命令推送给我 + + + + + +*** + + + +### set + +#### 简介 + +数据存储需求:存储大量的数据,在查询方面提供更高的效率 + +数据存储结构:能够保存大量的数据,高效的内部存储机制,便于查询 + +set 类型:与 hash 存储结构哈希表完全相同,只是仅存储键不存储值(nil),所以添加,删除,查找的复杂度都是 O(1),并且**值是不允许重复且无序的** + + + + + +*** + + + +#### 操作 + +指令操作: + +* 数据操作 + + ```sh + sadd key member1 [member2] #添加数据 + srem key member1 [member2] #删除数据 + ``` + +* 查询操作 + + ```sh + smembers key #获取全部数据 + scard key #获取集合数据总量 + sismember key member #判断集合中是否包含指定数据 + ``` + +* 随机操作 + + ```sh + spop key [count] #随机获取集中的某个数据并将该数据移除集合 + srandmember key [count] #随机获取集合中指定(数量)的数据 + +* 集合的交、并、差 + + ```sh + sinter key1 [key2...] #两个集合的交集,不存在为(empty list or set) + sunion key1 [key2...] #两个集合的并集 + sdiff key1 [key2...] #两个集合的差集 + + sinterstore destination key1 [key2...] #两个集合的交集并存储到指定集合中 + sunionstore destination key1 [key2...] #两个集合的并集并存储到指定集合中 + sdiffstore destination key1 [key2...] #两个集合的差集并存储到指定集合中 + ``` + +* 复制 + + ```sh + smove source destination member #将指定数据从原始集合中移动到目标集合中 + ``` + + +注意事项 + +1. set 类型不允许数据重复,如果添加的数据在 set 中已经存在,将只保留一份 +2. set 虽然与 hash 的存储结构相同,但是无法启用 hash 中存储值的空间 + + + +*** + + + +#### 实现 + +集合对象的内部编码有两种:intset(整数集合)、hashtable(哈希表、字典) + +* 整数集合实现的集合对象: + + + +* 字典实现的集合对象:键值对的值为 NULL + + + +当集合对象可以同时满足以下两个条件时,对象使用 intset 编码: + +* 集合中的元素都是整数值 +* 集合中的元素数量小于 set-maxintset-entries配置(默认 512 个) + +以上两个条件的上限值是可以通过配置文件修改的 + + + +**** + + + +#### 应用 + +应用场景: + +1. 黑名单:资讯类信息类网站追求高访问量,但是由于其信息的价值,往往容易被不法分子利用,通过爬虫技术,快速获取信息,个别特种行业网站信息通过爬虫获取分析后,可以转换成商业机密。 + + 注意:爬虫不一定做摧毁性的工作,有些小型网站需要爬虫为其带来一些流量。 + +2. 白名单:对于安全性更高的应用访问,仅仅靠黑名单是不能解决安全问题的,此时需要设定可访问的用户群体, 依赖白名单做更为苛刻的访问验证 + +3. 随机操作可以实现抽奖功能 + +4. 集合的交并补可以实现微博共同关注的查看,可以根据共同关注或者共同喜欢推荐相关内容 + + + + + +*** + + + +### zset + +#### 简介 + +数据存储需求:数据排序有利于数据的有效展示,需要提供一种可以根据自身特征进行排序的方式 + +数据存储结构:新的存储模型,可以保存可排序的数据 + + + +**** + + + +#### 操作 + +指令操作: + +* 数据操作 + + ```sh + zadd key score1 member1 [score2 member2] #添加数据 + zrem key member [member ...] #删除数据 + zremrangebyrank key start stop #删除指定索引范围的数据 + zremrangebyscore key min max #删除指定分数区间内的数据 + zscore key member #获取指定值的分数 + zincrby key increment member #指定值的分数增加increment + ``` + +* 查询操作 + + ```sh + zrange key start stop [WITHSCORES] #获取指定范围的数据,升序,WITHSCORES 代表显示分数 + zrevrange key start stop [WITHSCORES] #获取指定范围的数据,降序 + + zrangebyscore key min max [WITHSCORES] [LIMIT offset count] #按条件获取数据,从小到大 + zrevrangebyscore key max min [WITHSCORES] [...] #从大到小 + + zcard key #获取集合数据的总量 + zcount key min max #获取指定分数区间内的数据总量 + zrank key member #获取数据对应的索引(排名)升序 + zrevrank key member #获取数据对应的索引(排名)降序 + ``` + + * min 与 max 用于限定搜索查询的条件 + * start 与 stop 用于限定查询范围,作用于索引,表示开始和结束索引 + * offset 与 count 用于限定查询范围,作用于查询结果,表示开始位置和数据总量 + +* 集合的交、并操作 + + ```sh + zinterstore destination numkeys key [key ...] #两个集合的交集并存储到指定集合中 + zunionstore destination numkeys key [key ...] #两个集合的并集并存储到指定集合中 + ``` + +注意事项: + +1. score 保存的数据存储空间是 64 位,如果是整数范围是 -9007199254740992~9007199254740992 +2. score 保存的数据也可以是一个双精度的 double 值,基于双精度浮点数的特征可能会丢失精度,慎重使用 +3. sorted_set 底层存储还是基于 set 结构的,因此数据不能重复,如果重复添加相同的数据,score 值将被反复覆盖,保留最后一次修改的结果 + + + +*** + + + +#### 实现 + +有序集合对象的内部编码有两种:ziplist(压缩列表)和 skiplist(跳跃表) + +* 压缩列表实现有序集合对象:ziplist 本身是有序、不可重复的,符合有序集合的特性 + + ![](https://seazean.oss-cn-beijing.aliyuncs.com/img/DB/Redis-有序集合对象ziplist.png) + +* 跳跃表实现有序集合对象:**底层是 zset 结构,zset 同时包含字典和跳跃表的结构**,图示字典和跳跃表中重复展示了各个元素的成员和分值,但实际上两者会**通过指针来共享相同元素的成员和分值**,不会产生空间浪费 + + ```c + typedef struct zset { + zskiplist *zsl; + dict *dict; + } zset; + ``` + + ![](https://seazean.oss-cn-beijing.aliyuncs.com/img/DB/Redis-有序集合对象zset.png) + +使用字典加跳跃表的优势: + +* 字典为有序集合创建了一个**从成员到分值的映射**,用 O(1) 复杂度查找给定成员的分值 +* **排序操作使用跳跃表完成**,节省每次重新排序带来的时间成本和空间成本 + +使用 ziplist 格式存储需要满足以下两个条件: + +- 有序集合保存的元素个数要小于 128 个; +- 有序集合保存的所有元素大小都小于 64 字节 + +当元素比较多时,此时 ziplist 的读写效率会下降,时间复杂度是 O(n),跳表的时间复杂度是 O(logn) + +为什么用跳表而不用平衡树? + +* 在做范围查找的时候,跳表操作简单(前进指针或后退指针),平衡树需要回旋查找 +* 跳表比平衡树实现简单,平衡树的插入和删除操作可能引发子树的旋转调整,而跳表的插入和删除只需要修改相邻节点的指针 + + + +*** + + + +#### 应用 + +* 排行榜 +* 对于基于时间线限定的任务处理,将处理时间记录为 score 值,利用排序功能区分处理的先后顺序 +* 当任务或者消息待处理,形成了任务队列或消息队列时,对于高优先级的任务要保障对其优先处理,采用 score 记录权重 + + + + + +*** + + + +### Bitmaps + +#### 基本操作 + +Bitmaps 是二进制位数组(bit array),底层使用 SDS 字符串表示,因为 SDS 是二进制安全的 + +![](https://seazean.oss-cn-beijing.aliyuncs.com/img/DB/Redis-位数组结构.png) + +buf 数组的每个字节用一行表示,buf[1] 是 `'\0'`,保存位数组的顺序和书写位数组的顺序是完全相反的,图示的位数组 0100 1101 + +数据结构的详解查看 Java → Algorithm → 位图 + + + + + +*** + + + +#### 命令实现 + +##### GETBIT + +GETBIT 命令获取位数组 bitarray 在 offset 偏移量上的二进制位的值 + +```sh +GETBIT +``` + +执行过程: + +* 计算 `byte = offset/8`(向下取整), byte 值记录数据保存在位数组中的索引 +* 计算 `bit = (offset mod 8) + 1`,bit 值记录数据在位数组中的第几个二进制位 +* 根据 byte 和 bit 值,在位数组 bitarray 中定位 offset 偏移量指定的二进制位,并返回这个位的值 + +GETBIT 命令执行的所有操作都可以在常数时间内完成,所以时间复杂度为 O(1) + + + +*** + + + +##### SETBIT + +SETBIT 将位数组 bitarray 在 offset 偏移量上的二进制位的值设置为 value,并向客户端返回二进制位的旧值 + +```sh +SETBIT +``` + +执行过程: + +* 计算 `len = offset/8 + 1`,len 值记录了保存该数据至少需要多少个字节 +* 检查 bitarray 键保存的位数组的长度是否小于 len,成立就会将 SDS 扩展为 len 字节(注意空间预分配机制),所有新扩展空间的二进制位的值置为 0 +* 计算 `byte = offset/8`(向下取整), byte 值记录数据保存在位数组中的索引 +* 计算 `bit = (offset mod 8) + 1`,bit 值记录数据在位数组中的第几个二进制位 +* 根据 byte 和 bit 值,在位数组 bitarray 中定位 offset 偏移量指定的二进制位,首先将指定位现存的值保存在 oldvalue 变量,然后将新值 value 设置为这个二进制位的值 +* 向客户端返回 oldvalue 变量的值 + + + +*** + + + +##### BITCOUNT + +BITCOUNT 命令用于统计给定位数组中,值为 1 的二进制位的数量 + +```sh +BITCOUNT [start end] +``` + +二进制位统计算法: + +* 遍历法:遍历位数组中的每个二进制位 +* 查表算法:读取每个字节(8 位)的数据,查表获取数值对应的二进制中有几个 1 +* variable-precision SWAR算法:计算汉明距离 +* Redis 实现: + * 如果二进制位的数量大于等于 128 位, 那么使用 variable-precision SWAR 算法来计算二进制位的汉明重量 + * 如果二进制位的数量小于 128 位,那么使用查表算法来计算二进制位的汉明重量 + + + +**** + + + +##### BITOP + +BITOP 命令对指定 key 按位进行交、并、非、异或操作,并将结果保存到指定的键中 + +```sh +BITOP OPTION destKey key1 [key2...] +``` + +OPTION 有 AND(与)、OR(或)、 XOR(异或)和 NOT(非)四个选项 + +AND、OR、XOR 三个命令可以接受多个位数组作为输入,需要遍历输入的每个位数组的每个字节来进行计算,所以命令的复杂度为 O(n^2);与此相反,NOT 命令只接受一个位数组输入,所以时间复杂度为 O(n) + + + +*** + + + +#### 应用场景 + +- **解决 Redis 缓存穿透**,判断给定数据是否存在, 防止缓存穿透 + + + +- 垃圾邮件过滤,对每一个发送邮件的地址进行判断是否在布隆的黑名单中,如果在就判断为垃圾邮件 + +- 爬虫去重,爬给定网址的时候对已经爬取过的 URL 去重 + +- 信息状态统计 + + + + + +*** + + + +### Hyper + +基数是数据集去重后元素个数,HyperLogLog 是用来做基数统计的,运用了 LogLog 的算法 + +```java +{1, 3, 5, 7, 5, 7, 8} 基数集: {1, 3, 5 ,7, 8} 基数:5 +{1, 1, 1, 1, 1, 7, 1} 基数集: {1,7} 基数:2 +``` + +相关指令: + +* 添加数据 + + ```sh + pfadd key element [element ...] + ``` + +* 统计数据 + + ```sh + pfcount key [key ...] + ``` + +* 合并数据 + + ```sh + pfmerge destkey sourcekey [sourcekey...] + ``` + +应用场景: + +* 用于进行基数统计,不是集合不保存数据,只记录数量而不是具体数据,比如网站的访问量 +* 核心是基数估算算法,最终数值存在一定误差 +* 误差范围:基数估计的结果是一个带有 0.81% 标准错误的近似值 +* 耗空间极小,每个 hyperloglog key 占用了12K的内存用于标记基数 +* pfadd 命令不是一次性分配12K内存使用,会随着基数的增加内存逐渐增大 +* Pfmerge 命令合并后占用的存储空间为12K,无论合并之前数据量多少 + + + +*** + + + +### GEO + +GeoHash 是一种地址编码方法,把二维的空间经纬度数据编码成一个字符串 + +* 添加坐标点 + + ```sh + geoadd key longitude latitude member [longitude latitude member ...] + georadius key longitude latitude radius m|km|ft|mi [withcoord] [withdist] [withhash] [count count] + ``` + +* 获取坐标点 + + ```sh + geopos key member [member ...] + georadiusbymember key member radius m|km|ft|mi [withcoord] [withdist] [withhash] [count count] + ``` + +* 计算距离 + + ```sh + geodist key member1 member2 [unit] #计算坐标点距离 + geohash key member [member ...] #计算经纬度 + ``` + +Redis 应用于地理位置计算 + + + + + +**** + + + + + +## 持久机制 + +### 概述 + +持久化:利用永久性存储介质将数据进行保存,在特定的时间将保存的数据进行恢复的工作机制称为持久化 + +作用:持久化用于防止数据的意外丢失,确保数据安全性,因为 Redis 是内存级,所以需要持久化到磁盘 + +计算机中的数据全部都是二进制,保存一组数据有两种方式 + + +RDB:将当前数据状态进行保存,快照形式,存储数据结果,存储格式简单 + +AOF:将数据的操作过程进行保存,日志形式,存储操作过程,存储格式复杂 + + + +*** + + + +### RDB + +#### 文件创建 + +RDB 持久化功能所生成的 RDB 文件是一个经过压缩的紧凑二进制文件,通过该文件可以还原生成 RDB 文件时的数据库状态,有两个 Redis 命令可以生成 RDB 文件,一个是 SAVE,另一个是 BGSAVE + + + +##### SAVE + +SAVE 指令:手动执行一次保存操作,该指令的执行会阻塞当前 Redis 服务器,客户端发送的所有命令请求都会被拒绝,直到当前 RDB 过程完成为止,有可能会造成长时间阻塞,线上环境不建议使用 + +工作原理:Redis 是个**单线程的工作模式**,会创建一个任务队列,所有的命令都会进到这个队列排队执行。当某个指令在执行的时候,队列后面的指令都要等待,所以这种执行方式会非常耗时 + +配置 redis.conf: + +```sh +dir path #设置存储.rdb文件的路径,通常设置成存储空间较大的目录中,目录名称data +dbfilename "x.rdb" #设置本地数据库文件名,默认值为dump.rdb,通常设置为dump-端口号.rdb +rdbcompression yes|no #设置存储至本地数据库时是否压缩数据,默认yes,设置为no节省CPU运行时间 +rdbchecksum yes|no #设置读写文件过程是否进行RDB格式校验,默认yes +``` + + + +*** + + + +##### BGSAVE + +BGSAVE:bg 是 background,代表后台执行,命令的完成需要两个进程,**进程之间不相互影响**,所以持久化期间 Redis 正常工作 + +工作原理: + + + +流程:客户端发出 BGSAVE 指令,Redis 服务器使用 fork 函数创建一个子进程,然后响应后台已经开始执行的信息给客户端。子进程会异步执行持久化的操作,持久化过程是先将数据写入到一个临时文件中,持久化操作结束再用这个临时文件**替换**上次持久化的文件 + +```python +# 创建子进程 +pid = fork() +if pid == 0: + # 子进程负责创建 RDB 文件 + rdbSave() + # 完成之后向父进程发送信号 + signal_parent() +elif pid > 0: + # 父进程继续处理命令请求,并通过轮询等待子进程的信号 + handle_request_and_wait_signal() +else: + # 处理出错恃况 + handle_fork_error() +``` + +配置 redis.conf + +```sh +stop-writes-on-bgsave-error yes|no #后台存储过程中如果出现错误,是否停止保存操作,默认yes +dbfilename filename +dir path +rdbcompression yes|no +rdbchecksum yes|no +``` + +注意:BGSAVE 命令是针对 SAVE 阻塞问题做的优化,Redis 内部所有涉及到 RDB 操作都采用 BGSAVE 的方式,SAVE 命令放弃使用 + +在 BGSAVE 命令执行期间,服务器处理 SAVE、BGSAVE、BGREWRITEAOF 三个命令的方式会和平时有所不同 + +* SAVE 命令会被服务器拒绝,服务器禁止 SAVE 和 BGSAVE 命令同时执行是为了避免父进程(服务器进程)和子进程同时执行两个 rdbSave 调用,产生竞争条件 +* BGSAVE 命令也会被服务器拒绝,也会产生竞争条件 +* BGREWRITEAOF 和 BGSAVE 两个命令不能同时执行 + * 如果 BGSAVE 命令正在执行,那么 BGREWRITEAOF 命令会被**延迟**到 BGSAVE 命令执行完毕之后执行 + * 如果 BGREWRITEAOF 命令正在执行,那么 BGSAVE 命令会被服务器拒绝 + + + +*** + + + +##### 特殊指令 + +RDB 特殊启动形式的指令(客户端输入) + +* 服务器运行过程中重启 + + ```sh + debug reload + ``` + +* 关闭服务器时指定保存数据 + + ```sh + shutdown save + ``` + + 默认情况下执行 shutdown 命令时,自动执行 bgsave(如果没有开启 AOF 持久化功能) + +* 全量复制:主从复制部分详解 + + + + + +*** + + + +#### 文件载入 + +RDB 文件的载入工作是在服务器启动时自动执行,期间 Redis 会一直处于阻塞状态,直到载入完成 + +Redis 并没有专门用于载入 RDB 文件的命令,只要服务器在启动时检测到 RDB 文件存在,就会自动载入 RDB 文件 + +```sh +[7379] 30 Aug 21:07:01.289 * DB loaded from disk: 0.018 seconds # 服务器在成功载入 RDB 文件之后打印 +``` + +AOF 文件的更新频率通常比 RDB 文件的更新频率高: + +* 如果服务器开启了 AOF 持久化功能,那么会优先使用 AOF 文件来还原数据库状态 +* 只有在 AOF 持久化功能处于关闭状态时,服务器才会使用 RDB 文件来还原数据库状态 + + + + + +**** + + + +#### 自动保存 + +##### 配置文件 + +Redis 支持通过配置服务器的 save 选项,让服务器每隔一段时间自动执行一次 BGSAVE 命令 + +配置 redis.conf: + +```sh +save second changes #设置自动持久化条件,满足限定时间范围内key的变化数量就进行持久化(bgsave) +``` + +* second:监控时间范围 +* changes:监控 key 的变化量 + +默认三个条件: + +```sh +save 900 1 # 900s内1个key发生变化就进行持久化 +save 300 10 +save 60 10000 +``` + +判定 key 变化的依据: + +* 对数据产生了影响,不包括查询 +* 不进行数据比对,比如 name 键存在,重新 set name seazean 也算一次变化 + +save 配置要根据实际业务情况进行设置,频度过高或过低都会出现性能问题,结果可能是灾难性的 + + + +*** + + + +##### 自动原理 + +服务器状态相关的属性: + +```c +struct redisServer { + // 记录了保存条件的数组 + struct saveparam *saveparams; + + // 修改计数器 + long long dirty; + + // 上一次执行保存的时间 + time_t lastsave; +}; +``` + +* Redis 服务器启动时,可以通过指定配置文件或者传入启动参数的方式设置 save 选项, 如果没有自定义就设置为三个默认值(上节提及),设置服务器状态 redisServe.saveparams 属性,该数组每一项为一个 saveparam 结构,代表 save 的选项设置 + + ```c + struct saveparam { + // 秒数 + time_t seconds + // 修改数 + int changes; + }; + ``` + +* dirty 计数器记录距离上一次成功执行 SAVE 或者 BGSAVE 命令之后,服务器中的所有数据库进行了多少次修改(包括写入、删除、更新等操作),当服务器成功执行一个修改指令,该命令修改了多少次数据库, dirty 的值就增加多少 + +* lastsave 属性是一个 UNIX 时间戳,记录了服务器上一次成功执行 SAVE 或者 BGSAVE 命令的时间 + +Redis 的服务器周期性操作函数 serverCron 默认每隔 100 毫秒就会执行一次,该函数用于对正在运行的服务器进行维护 + +serverCron 函数的其中一项工作是检查 save 选项所设置的保存条件是否满足,会遍历 saveparams 数组中的**所有保存条件**,只要有任意一个条件被满足服务器就会执行 BGSAVE 命令 + +![](https://seazean.oss-cn-beijing.aliyuncs.com/img/DB/Redis-BGSAVE执行原理.png) + + + + + +*** + + + +#### 文件结构 + +RDB 的存储结构:图示全大写单词标示常量,用全小写单词标示变量和数据 + +![](https://seazean.oss-cn-beijing.aliyuncs.com/img/DB/Redis-RDB文件结构.png) + +* REDIS:长度为 5 字节,保存着 `REDIS` 五个字符,是 RDB 文件的开头,在载入文件时可以快速检查所载入的文件是否 RDB 文件 +* db_version:长度为 4 字节,是一个用字符串表示的整数,记录 RDB 的版本号 +* database:包含着零个或任意多个数据库,以及各个数据库中的键值对数据 +* EOF:长度为 1 字节的常量,标志着 RDB 文件正文内容的结束,当读入遇到这个值时,代表所有数据库的键值对都已经载入完毕 +* check_sum:长度为 8 字节的无符号整数,保存着一个校验和,该值是通过 REDIS、db_version、databases、EOF 四个部分的内容进行计算得出。服务器在载入 RDB 文件时,会将载入数据所计算出的校验和与 check_sum 所记录的校验和进行对比,来检查 RDB 文件是否有出错或者损坏 + +Redis 本身带有 RDB 文件检查工具 redis-check-dump + + + + + +*** + + + +### AOF + +#### 基本概述 + +AOF(append only file)持久化:以独立日志的方式记录每次写命令(不记录读)来记录数据库状态,**增量保存**只许追加文件但不可以改写文件,**与 RDB 相比可以理解为由记录数据改为记录数据的变化** + +AOF 主要作用是解决了**数据持久化的实时性**,目前已经是 Redis 持久化的主流方式 + +AOF 写数据过程: + + + +Redis 只会将对数据库进行了修改的命令写入到 AOF 文件,并复制到各个从服务器,但是 PUBSUB 和 SCRIPT LOAD 命令例外: + +* PUBSUB 命令虽然没有修改数据库,但 PUBSUB 命令向频道的所有订阅者发送消息这一行为带有副作用,接收到消息的所有客户端的状态都会因为这个命令而改变,所以服务器需要使用 REDIS_FORCE_AOF 标志强制将这个命令写入 AOF 文件。这样在将来载入 AOF 文件时,服务器就可以再次执行相同的 PUBSUB 命令,并产生相同的副作用 +* SCRIPT LOAD 命令虽然没有修改数据库,但它修改了服务器状态,所以也是一个带有副作用的命令,需要使用 REDIS_FORCE_AOF + + + +*** + + + +#### 持久实现 + +AOF 持久化功能的实现可以分为命令追加(append)、文件写入、文件同步(sync)三个步骤 + + + +##### 命令追加 + +启动 AOF 的基本配置: + +```sh +appendonly yes|no #开启AOF持久化功能,默认no,即不开启状态 +appendfilename filename #AOF持久化文件名,默认appendonly.aof,建议设置appendonly-端口号.aof +dir #AOF持久化文件保存路径,与RDB持久化文件路径保持一致即可 +``` + +当 AOF 持久化功能处于打开状态时,服务器在执行完一个写命令之后,会以协议格式将被执行的写命令**追加**到服务器状态的 aof_buf 缓冲区的末尾 + +```c +struct redisServer { + // AOF 缓冲区 + sds aof_buf; +}; +``` + + + +*** + + + +##### 文件写入 + +服务器在处理文件事件时会执行**写命令,追加一些内容到 aof_buf 缓冲区**里,所以服务器每次结束一个事件循环之前,就会执行 flushAppendOnlyFile 函数,判断是否需要**将 aof_buf 缓冲区中的内容写入和保存到 AOF 文件**里 + +flushAppendOnlyFile 函数的行为由服务器配置的 appendfsync 选项的值来决定 + +```sh +appendfsync always|everysec|no #AOF写数据策略:默认为everysec +``` + +- always:每次写入操作都将 aof_buf 缓冲区中的所有内容**写入并同步**到 AOF 文件 + + 特点:安全性最高,数据零误差,但是性能较低,不建议使用 + + +- everysec:先将 aof_buf 缓冲区中的内容写入到操作系统缓存,判断上次同步 AOF 文件的时间距离现在超过一秒钟,再次进行同步 fsync,这个同步操作是由一个(子)线程专门负责执行的 + + 特点:在系统突然宕机的情况下丢失 1 秒内的数据,准确性较高,性能较高,建议使用,也是默认配置 + + +- no:将 aof_buf 缓冲区中的内容写入到操作系统缓存,但并不进行同步,何时同步由操作系统来决定 + + 特点:**整体不可控**,服务器宕机会丢失上次同步 AOF 后的所有写指令 + + + +**** + + + +##### 文件同步 + +在现代操作系统中,当用户调用 write 函数将数据写入文件时,操作系统通常会将写入数据暂时保存在一个内存缓冲区空间,等到缓冲区**写满或者到达特定时间周期**,才真正地将缓冲区中的数据写入到磁盘里面(刷脏) + +* 优点:提高文件的写入效率 +* 缺点:为写入数据带来了安全问题,如果计算机发生停机,那么保存在内存缓冲区里面的写入数据将会丢失 + +系统提供了 fsync 和 fdatasync 两个同步函数做**强制硬盘同步**,可以让操作系统立即将缓冲区中的数据写入到硬盘里面,函数会阻塞到写入硬盘完成后返回,保证了数据持久化 + +异常恢复:AOF 文件损坏,通过 redis-check-aof--fix appendonly.aof 进行恢复,重启 Redis,然后重新加载 + + + + + +*** + + + +#### 文件载入 + +AOF 文件里包含了重建数据库状态所需的所有写命令,所以服务器只要读入并重新执行一遍 AOF 文件里的命令,就还原服务器关闭之前的数据库状态,服务器在启动时,还原数据库状态打印的日志: + +```sh +[8321] 05 Sep 11:58:50.449 * DB loaded from append only file: 0.000 seconds +``` + +AOF 文件里面除了用于指定数据库的 SELECT 命令是服务器自动添加的,其他都是通过客户端发送的命令 + +```sh +* 2\r\n$6\r\nSELECT\r\n$1\r\n0\r\n # 服务器自动添加 +* 3\r\n$3\r\nSET\r\n$3\r\nmsg\r\n$5\r\nhello\r\n +* 5\r\n$4\r\nSADD\r\n$6\r\nfruits\r\n$5\r\napple\r\n$6\r\nbanana\r\n$6\r\ncherry\r\n +``` + +Redis 读取 AOF 文件并还原数据库状态的步骤: + +* 创建一个**不带网络连接的伪客户端**(fake client)执行命令,因为 Redis 的命令只能在客户端上下文中执行, 而载入 AOF 文件时所使用的命令来源于本地 AOF 文件而不是网络连接 +* 从 AOF 文件分析并读取一条写命令 +* 使用伪客户端执行被读出的写命令,然后重复上述步骤 + + + + + +**** + + + +#### 重写实现 + +##### 重写策略 + +AOF 重写:读取服务器当前的数据库状态,**生成新 AOF 文件来替换旧 AOF 文件**,不会对现有的 AOF 文件进行任何读取、分析或者写入操作,而是直接原子替换。新 AOF 文件不会包含任何浪费空间的冗余命令,所以体积通常会比旧 AOF 文件小得多 + +AOF 重写规则: + +- 进程内具有时效性的数据,并且数据已超时将不再写入文件 + + +- 对同一数据的多条写命令合并为一条命令,因为会读取当前的状态,所以直接将当前状态转换为一条命令即可。为防止数据量过大造成客户端缓冲区溢出,对 list、set、hash、zset 等集合类型,**单条指令**最多写入 64 个元素 + + 如 lpushlist1 a、lpush list1 b、lpush list1 c 可以转化为:lpush list1 a b c + +- 非写入类的无效指令将被忽略,只保留最终数据的写入命令,但是 select 指令虽然不更改数据,但是更改了数据的存储位置,此类命令同样需要记录 + +AOF 重写作用: + +- 降低磁盘占用量,提高磁盘利用率 +- 提高持久化效率,降低持久化写时间,提高 IO 性能 +- 降低数据恢复的用时,提高数据恢复效率 + + + +*** + + + +##### 重写原理 + +AOF 重写程序 aof_rewrite 函数可以创建一个新 AOF 文件, 但是该函数会进行大量的写入操作,调用这个函数的线程将被长时间阻塞,所以 Redis 将 AOF 重写程序放到 fork 的子进程里执行,不会阻塞父进程,重写命令: + +```sh +bgrewriteaof +``` + +* 子进程进行 AOF 重写期间,服务器进程(父进程)可以继续处理命令请求 + +* 子进程带有服务器进程的数据副本,使用子进程而不是线程,可以在避免使用锁的情况下, 保证数据安全性 + + ![](https://seazean.oss-cn-beijing.aliyuncs.com/img/DB/Redis-AOF手动重写原理.png) + +子进程在进行 AOF 重写期间,服务器进程还需要继续处理命令请求,而新命令可能会对现有的数据库状态进行修改,从而使得服务器当前的数据库状态和重写后的 AOF 文件所保存的数据库状态不一致,所以 Redis 设置了 AOF 重写缓冲区 + +工作流程: + +* Redis 服务器执行完一个写命令,会同时将该命令追加到 AOF 缓冲区和 AOF 重写缓冲区(从创建子进程后才开始写入) +* 当子进程完成 AOF 重写工作之后,会向父进程发送一个信号,父进程在接到该信号之后, 会调用一个信号处理函数,该函数执行时会**对服务器进程(父进程)造成阻塞**(影响很小,类似 JVM STW),主要工作: + * 将 AOF 重写缓冲区中的所有内容写入到新 AOF 文件中, 这时新 AOF 文件所保存的状态将和服务器当前的数据库状态一致 + * 对新的 AOF 文件进行改名,**原子地(atomic)覆盖**现有的 AOF 文件,完成新旧两个 AOF 文件的替换 + + + + + +*** + + + +##### 自动重写 + +触发时机:Redis 会记录上次重写时的 AOF 大小,默认配置是当 AOF 文件大小是上次重写后大小的一倍且文件大于 64M 时触发 + +```sh +auto-aof-rewrite-min-size size #设置重写的基准值,最小文件 64MB,达到这个值开始重写 +auto-aof-rewrite-percentage percent #触发AOF文件执行重写的增长率,当前AOF文件大小超过上一次重写的AOF文件大小的百分之多少才会重写,比如文件达到 100% 时开始重写就是两倍时触发 +``` + +自动重写触发比对参数( 运行指令 `info Persistence` 获取具体信息 ): + +```sh +aof_current_size #AOF文件当前尺寸大小(单位:字节) +aof_base_size #AOF文件上次启动和重写时的尺寸大小(单位:字节) +``` + +自动重写触发条件公式: + +- aof_current_size > auto-aof-rewrite-min-size +- (aof_current_size - aof_base_size) / aof_base_size >= auto-aof-rewrite-percentage + + + + + +**** + + + +### 对比 + +RDB 的特点 + +* RDB 优点: + - RDB 是一个紧凑压缩的二进制文件,存储效率较高,但存储数据量较大时,存储效率较低 + - RDB 内部存储的是 Redis 在某个时间点的数据快照,非常**适合用于数据备份,全量复制、灾难恢复** + - RDB 恢复数据的速度要比 AOF 快很多,因为是快照,直接恢复 +* RDB 缺点: + + - BGSAVE 指令每次运行要执行 fork 操作创建子进程,会牺牲一些性能 + - RDB 方式无论是执行指令还是利用配置,无法做到实时持久化,具有丢失数据的可能性,最后一次持久化后的数据可能丢失 + - Redis 的众多版本中未进行 RDB 文件格式的版本统一,可能出现各版本之间数据格式无法兼容 + +AOF 特点: + +* AOF 的优点:数据持久化有**较好的实时性**,通过 AOF 重写可以降低文件的体积 +* AOF 的缺点:文件较大时恢复较慢 + +AOF 和 RDB 同时开启,系统默认取 AOF 的数据(数据不会存在丢失) + +应用场景: + +- 对数据**非常敏感**,建议使用默认的 AOF 持久化方案,AOF 持久化策略使用 everysecond,每秒钟 fsync 一次,该策略 Redis 仍可以保持很好的处理性能 + + 注意:AOF 文件存储体积较大,恢复速度较慢,因为要执行每条指令 + +- 数据呈现**阶段有效性**,建议使用 RDB 持久化方案,可以做到阶段内无丢失,且恢复速度较快 + + 注意:利用 RDB 实现紧凑的数据持久化,存储数据量较大时,存储效率较低 + +综合对比: + +- RDB 与 AOF 的选择实际上是在做一种权衡,每种都有利有弊 +- 灾难恢复选用 RDB +- 如不能承受数分钟以内的数据丢失,对业务数据非常敏感,选用 AOF;如能承受数分钟以内的数据丢失,且追求大数据集的恢复速度,选用 RDB +- 双保险策略,同时开启 RDB 和 AOF,重启后 Redis 优先使用 AOF 来恢复数据,降低丢失数据的量 +- 不建议单独用 AOF,因为可能会出现 Bug,如果只是做纯内存缓存,可以都不用 + + + +*** + + + +### fork + +#### 介绍 + +fork() 函数创建一个子进程,子进程与父进程几乎是完全相同的进程,系统先给子进程分配资源,然后把父进程的所有数据都复制到子进程中,只有少数值与父进程的值不同,相当于克隆了一个进程 + +在完成对其调用之后,会产生 2 个进程,且每个进程都会**从 fork() 的返回处开始执行**,这两个进程将执行相同的程序段,但是拥有各自不同的堆段,栈段,数据段,每个子进程都可修改各自的数据段,堆段,和栈段 + +```c +#include +pid_t fork(void); +// 父进程返回子进程的pid,子进程返回0,错误返回负值,根据返回值的不同进行对应的逻辑处理 +``` + +fork 调用一次,却能够**返回两次**,可能有三种不同的返回值: + +* 在父进程中,fork 返回新创建子进程的进程 ID +* 在子进程中,fork 返回 0 +* 如果出现错误,fork 返回一个负值,错误原因: + * 当前的进程数已经达到了系统规定的上限,这时 errno 的值被设置为 EAGAIN + * 系统内存不足,这时 errno 的值被设置为 ENOMEM + +fpid 的值在父子进程中不同:进程形成了链表,父进程的 fpid 指向子进程的进程 id,因为子进程没有子进程,所以其 fpid 为0 + +创建新进程成功后,系统中出现两个基本完全相同的进程,这两个进程执行没有固定的先后顺序,哪个进程先执行要看系统的调度策略 + +每个进程都有一个独特(互不相同)的进程标识符 process ID,可以通过 getpid() 函数获得;还有一个记录父进程 pid 的变量,可以通过 getppid() 函数获得变量的值 + + + +*** + + + +#### 使用 + +基本使用: + +```c +#include +#include +int main () +{ + pid_t fpid; // fpid表示fork函数返回的值 + int count = 0; + fpid = fork(); + if (fpid < 0) + printf("error in fork!"); + else if (fpid == 0) { + printf("i am the child process, my process id is %d/n", getpid()); + count++; + } + else { + printf("i am the parent process, my process id is %d/n", getpid()); + count++; + } + printf("count: %d/n",count);// 1 + return 0; +} +/* 输出内容: + i am the child process, my process id is 5574 + count: 1 + i am the parent process, my process id is 5573 + count: 1 +*/ +``` + +进阶使用: + +```c +#include +#include +int main(void) +{ + int i = 0; + // ppid 指当前进程的父进程pid + // pid 指当前进程的pid, + // fpid 指fork返回给当前进程的值,在这可以表示子进程 + for(i = 0; i < 2; i++){ + pid_t fpid = fork(); + if(fpid == 0) + printf("%d child %4d %4d %4d/n",i, getppid(), getpid(), fpid); + else + printf("%d parent %4d %4d %4d/n",i, getppid(), getpid(),fpid); + } + return 0; +} +/*输出内容: + i 父id id 子id + 0 parent 2043 3224 3225 + 0 child 3224 3225 0 + 1 parent 2043 3224 3226 + 1 parent 3224 3225 3227 + 1 child 1 3227 0 + 1 child 1 3226 0 +*/ +``` + + + +在 p3224 和 p3225 执行完第二个循环后,main 函数退出,进程死亡。所以 p3226,p3227 就没有父进程了,成为孤儿进程,所以 p3226 和 p3227 的父进程就被置为 ID 为 1 的 init 进程(笔记 Tool → Linux → 进程管理详解) + +参考文章:https://blog.csdn.net/love_gaohz/article/details/41727415 + + + +*** + + + +#### 内存 + +fork() 调用之后父子进程的内存关系 + +早期 Linux 的 fork() 实现时,就是全部复制,这种方法效率太低,而且造成了很大的内存浪费,现在 Linux 实现采用了两种方法: + +* 父子进程的代码段是相同的,所以代码段是没必要复制的,只需内核将代码段标记为只读,父子进程就共享此代码段。fork() 之后在进程创建代码段时,子进程的进程级页表项都指向和父进程相同的物理页帧 + + + +* 对于父进程的数据段,堆段,栈段中的各页,由于父子进程相互独立,采用**写时复制 COW** 的技术,来提高内存以及内核的利用率 + + 在 fork 之后两个进程用的是相同的物理空间(内存区),子进程的代码段、数据段、堆栈都是指向父进程的物理空间,**两者的虚拟空间不同,但其对应的物理空间是同一个**,当父子进程中有更改相应段的行为发生时,再为子进程相应的段分配物理空间。如果两者的代码完全相同,代码段继续共享父进程的物理空间;而如果两者执行的代码不同,子进程的代码段也会分配单独的物理空间。 + + fork 之后内核会将子进程放在队列的前面,让子进程先执行,以免父进程执行导致写时复制,而后子进程再执行,因无意义的复制而造成效率的下降 + + + +补充知识: + +vfork(虚拟内存 fork virtual memory fork):调用 vfork() 父进程被挂起,子进程使用父进程的地址空间。不采用写时复制,如果子进程修改父地址空间的任何页面,这些修改过的页面对于恢复的父进程是可见的 + + + +参考文章:https://blog.csdn.net/Shreck66/article/details/47039937 + + + + + +**** + + + + + +## 事务机制 + +### 事务特征 + +Redis 事务就是将多个命令请求打包,然后**一次性、按顺序**地执行多个命令的机制,并且在事务执行期间,服务器不会中断事务去执行其他的命令请求,会将事务中的所有命令都执行完毕,然后才去处理其他客户端的命令请求,Redis 事务的特性: + +* Redis 事务**没有隔离级别**的概念,队列中的命令在事务没有提交之前都不会实际被执行 +* Redis 单条命令式保存原子性的,但是事务**不保证原子性**,事务中如果有一条命令执行失败,其后的命令仍然会被执行,没有回滚 + + + + + +*** + + + +### 工作流程 + +事务的执行流程分为三个阶段: + +* 事务开始:MULTI 命令的执行标志着事务的开始,通过在客户端状态的 flags 属性中打开 REDIS_MULTI 标识,将执行该命令的客户端从非事务状态切换至事务状态 + + ```sh + MULTI # 设定事务的开启位置,此指令执行后,后续的所有指令均加入到事务中 + ``` + +* 命令入队:事务队列以先进先出(FIFO)的方式保存入队的命令,每个 Redis 客户端都有事务状态,包含着事务队列: + + ```c + typedef struct redisClient { + // 事务状态 + multiState mstate; /* MULTI/EXEC state */ + } + + typedef struct multiState { + // 事务队列,FIFO顺序 + multiCmd *commands; + + // 已入队命令计数 + int count; + } + ``` + + * 如果命令为 EXEC、DISCARD、WATCH、MULTI 四个命中的一个,那么服务器立即执行这个命令 + * 其他命令服务器不执行,而是将命令放入一个事务队列里面,然后向客户端返回 QUEUED 回复 + +* 事务执行:EXEC 提交事务给服务器执行,服务器会遍历这个客户端的事务队列,执行队列中的命令并将执行结果返回 + + ```sh + EXEC # Commit 提交,执行事务,与multi成对出现,成对使用 + ``` + +事务取消的方法: + +* 取消事务: + + ```sh + DISCARD # 终止当前事务的定义,发生在multi之后,exec之前 + ``` + + 一般用于事务执行过程中输入了错误的指令,直接取消这次事务,类似于回滚 + + + + + +*** + + + +### WATCH + +#### 监视机制 + +WATCH 命令是一个乐观锁(optimistic locking),可以在 EXEC 命令执行之前,监视任意数量的数据库键,并在 EXEC 命令执行时,检查被监视的键是否至少有一个已经被修改过了,如果是服务器将拒绝执行事务,并向客户端返回代表事务执行失败的空回复 + +* 添加监控锁 + + ```sh + WATCH key1 [key2……] #可以监控一个或者多个key + ``` + +* 取消对所有 key 的监视 + + ```sh + UNWATCH + ``` + + + +*** + + + +#### 实现原理 + +每个 Redis 数据库都保存着一个 watched_keys 字典,键是某个被 WATCH 监视的数据库键,值则是一个链表,记录了所有监视相应数据库键的客户端: + +```c +typedef struct redisDb { + // 正在被 WATCH 命令监视的键 + dict *watched_keys; +} +``` + +所有对数据库进行修改的命令,在执行后都会调用 `multi.c/touchWatchKey` 函数对 watched_keys 字典进行检查,是否有客户端正在监视刚被命令修改过的数据库键,如果有的话函数会将监视被修改键的客户端的 REDIS_DIRTY_CAS 标识打开,表示该客户端的事务安全性已经被破坏 + +服务器接收到个客户端 EXEC 命令时,会根据这个客户端是否打开了 REDIS_DIRTY_CAS 标识,如果打开了说明客户端提交事务不安全,服务器会拒绝执行 + + + + + +**** + + + +### ACID + +#### 原子性 + +事务具有原子性(Atomicity)、一致性(Consistency)、隔离性(Isolation)、持久性(Durability) + +原子性指事务队列中的命令要么就全部都执行,要么一个都不执行,但是在命令执行出错时,不会保证原子性(下一节详解) + +Redis 不支持事务回滚机制(rollback),即使事务队列中的某个命令在执行期间出现了错误,整个事务也会继续执行下去,直到将事务队列中的所有命令都执行完毕为止 + +回滚需要程序员在代码中实现,应该尽可能避免: + +* 事务操作之前记录数据的状态 + + * 单数据:string + + * 多数据:hash、list、set、zset + + +* 设置指令恢复所有的被修改的项 + + * 单数据:直接 set(注意周边属性,例如时效) + + * 多数据:修改对应值或整体克隆复制 + + + +*** + + + +#### 一致性 + +事务具有一致性指的是,数据库在执行事务之前是一致的,那么在事务执行之后,无论事务是否执行成功,数据库也应该仍然是一致的 + +一致是数据符合数据库的定义和要求,没有包含非法或者无效的错误数据,Redis 通过错误检测和简单的设计来保证事务的一致性: + +* 入队错误:命令格式输入错误,出现语法错误造成,**整体事务中所有命令均不会执行**,包括那些语法正确的命令 + + + +* 执行错误:命令执行出现错误,例如对字符串进行 incr 操作,事务中正确的命令会被执行,运行错误的命令不会被执行 + + + +* 服务器停机: + + * 如果服务器运行在无持久化的内存模式下,那么重启之后的数据库将是空白的,因此数据库是一致的 + * 如果服务器运行在持久化模式下,重启之后将数据库还原到一致的状态 + + + + +*** + + + +#### 隔离性 + +Redis 是一个单线程的执行原理,所以对于隔离性,分以下两种情况: + +* 并发操作在 EXEC 命令前执行,隔离性的保证要使用 WATCH 机制来实现,否则隔离性无法保证 +* 并发操作在 EXEC 命令后执行,隔离性可以保证 + + + +*** + + + +#### 持久性 + +Redis 并没有为事务提供任何额外的持久化功能,事务的持久性由 Redis 所使用的持久化模式决定 + +配置选项 `no-appendfsync-on-rewrite` 可以配合 appendfsync 选项在 AOF 持久化模式使用: + +* 选项打开时在执行 BGSAVE 或者 BGREWRITEAOF 期间,服务器会暂时停止对 AOF 文件进行同步,从而尽可能地减少 I/O 阻塞 +* 选项打开时运行在 always 模式的 AOF 持久化,事务也不具有持久性,所以该选项默认关闭 + +在一个事务的最后加上 SAVE 命令总可以保证事务的耐久性 + + + + + +*** + + + +## Lua 脚本 + +### 环境创建 + +#### 基本介绍 + +Redis 对 Lua 脚本支持,通过在服务器中嵌入 Lua 环境,客户端可以使用 Lua 脚本直接在服务器端**原子地执行**多个命令 + +```sh +EVAL +
+ + + + +
+ + + ``` + + + + + +*** + + + +### 参数调优 + +#### CONNECT + +参数配置方式: + +* 客户端通过 .option() 方法配置参数,给 SocketChannel 配置参数 +* 服务器端: + * new ServerBootstrap().option(): 给 ServerSocketChannel 配置参数 + * new ServerBootstrap().childOption():给 SocketChannel 配置参数 + +CONNECT_TIMEOUT_MILLIS 参数: + +* 属于 SocketChannal 参数 +* 在客户端建立连接时,如果在指定毫秒内无法连接,会抛出 timeout 异常 + +* SO_TIMEOUT 主要用在阻塞 IO,阻塞 IO 中 accept,read 等都是无限等待的,如果不希望永远阻塞,可以调整超时时间 + +```java +public class ConnectionTimeoutTest { + public static void main(String[] args) { + NioEventLoopGroup group = new NioEventLoopGroup(); + try { + Bootstrap bootstrap = new Bootstrap() + .group(group) + .option(ChannelOption.CONNECT_TIMEOUT_MILLIS, 5000) + .channel(NioSocketChannel.class) + .handler(new LoggingHandler()); + ChannelFuture future = bootstrap.connect("127.0.0.1", 8080); + future.sync().channel().closeFuture().sync(); + } catch (Exception e) { + e.printStackTrace(); + log.debug("timeout"); + } finally { + group.shutdownGracefully(); + } + } +} +``` + + + +**** + + + +#### SO_BACKLOG + +属于 ServerSocketChannal 参数,通过 `option(ChannelOption.SO_BACKLOG, value)` 来设置大小 + +在 Linux 2.2 之前,backlog 大小包括了两个队列的大小,在 2.2 之后,分别用下面两个参数来控制 + +* sync queue:半连接队列,大小通过 `/proc/sys/net/ipv4/tcp_max_syn_backlog` 指定,在 `syncookies` 启用的情况下,逻辑上没有最大值限制 +* accept queue:全连接队列,大小通过 `/proc/sys/net/core/somaxconn` 指定,在使用 listen 函数时,内核会根据传入的 backlog 参数与系统参数,取二者的较小值。如果 accpet queue 队列满了,server 将**发送一个拒绝连接的错误信息**到 client + +![](https://seazean.oss-cn-beijing.aliyuncs.com/img/Frame/Netty-TCP三次握手.png) + + + +**** + + + +#### 其他参数 + +ALLOCATOR:属于 SocketChannal 参数,用来分配 ByteBuf, ctx.alloc() + +RCVBUF_ALLOCATOR:属于 SocketChannal 参数 + +* 控制 Netty 接收缓冲区大小 +* 负责入站数据的分配,决定入站缓冲区的大小(并可动态调整),统一采用 direct 直接内存,具体池化还是非池化由 allocator 决定 + + + + + + + +*** + + + + + + + +# RocketMQ + +## 基本介绍 + +### 消息队列 + +#### 应用场景 + +消息队列是一种先进先出的数据结构,常见的应用场景: + +* 应用解耦:系统的耦合性越高,容错性就越低 + + 实例:用户创建订单后,耦合调用库存系统、物流系统、支付系统,任何一个子系统出了故障都会造成下单异常,影响用户使用体验。使用消息队列解耦合,比如物流系统发生故障,需要几分钟恢复,将物流系统要处理的数据缓存到消息队列中,用户的下单操作正常完成。等待物流系统正常后处理存在消息队列中的订单消息即可,终端系统感知不到物流系统发生过几分钟故障 + + ![](https://seazean.oss-cn-beijing.aliyuncs.com/img/Frame/RocketMQ-解耦.png) + +* 流量削峰:应用系统如果遇到系统请求流量的瞬间猛增,有可能会将系统压垮,使用消息队列可以将大量请求缓存起来,分散到很长一段时间处理,这样可以提高系统的稳定性和用户体验 + + ![](https://seazean.oss-cn-beijing.aliyuncs.com/img/Frame/RocketMQ-流量削峰.png) + +* 数据分发:让数据在多个系统更加之间进行流通,数据的产生方不需要关心谁来使用数据,只需要将数据发送到消息队列,数据使用方直接在消息队列中直接获取数据 + + ![](https://seazean.oss-cn-beijing.aliyuncs.com/img/Frame/RocketMQ-数据分发.png) + + + + + +参考视频:https://www.bilibili.com/video/BV1L4411y7mn + + + +*** + + + +#### 技术选型 + +RocketMQ 对比 Kafka 的优点 + +* 支持 Pull和 Push 两种消息模式 + +- 支持延时消息、死信队列、消息重试、消息回溯、消息跟踪、事务消息等高级特性 +- 对消息可靠性做了改进,**保证消息不丢失并且至少消费一次**,与 Kafka 一样是先写 PageCache 再落盘,并且数据有多副本 +- RocketMQ 存储模型是所有的 Topic 都写到同一个 Commitlog 里,是一个 append only 操作,在海量 Topic 下也能将磁盘的性能发挥到极致,并且保持稳定的写入时延。Kafka 的吞吐非常高(零拷贝、操作系统页缓存、磁盘顺序写),但是在多 Topic 下时延不够稳定(顺序写入特性会被破坏从而引入大量的随机 I/O),不适合实时在线业务场景 +- 经过阿里巴巴多年双 11 验证过、可以支持亿级并发的开源消息队列 + +Kafka 比 RocketMQ 吞吐量高: + +* Kafka 将 Producer 端将多个小消息合并,采用异步批量发送的机制,当发送一条消息时,消息并没有发送到 Broker 而是缓存起来,直接向业务返回成功,当缓存的消息达到一定数量时再批量发送 + +* 减少了网络 I/O,提高了消息发送的性能,但是如果消息发送者宕机,会导致消息丢失,降低了可靠性 +* RocketMQ 缓存过多消息会导致频繁 GC,并且为了保证可靠性没有采用这种方式 + +Topic 的 partition 数量过多时,Kafka 的性能不如 RocketMQ: + +* 两者都使用文件存储,但是 Kafka 是一个分区一个文件,Topic 过多时分区的总量也会增加,过多的文件导致对消息刷盘时出现文件竞争磁盘,造成性能的下降。**一个分区只能被一个消费组中的一个消费线程进行消费**,因此可以同时消费的消费端也比较少 + +* RocketMQ 所有队列都存储在一个文件中,每个队列存储的消息量也比较小,因此多 Topic 的对 RocketMQ 的性能的影响较小 + + + +**** + + + +### 安装测试 + +安装需要 Java 环境,下载解压后进入安装目录,进行启动: + +* 启动 NameServer + + ```sh + # 1.启动 NameServer + nohup sh bin/mqnamesrv & + # 2.查看启动日志 + tail -f ~/logs/rocketmqlogs/namesrv.log + ``` + + RocketMQ 默认的虚拟机内存较大,需要编辑如下两个配置文件,修改 JVM 内存大小 + + ```shell + # 编辑runbroker.sh和runserver.sh修改默认JVM大小 + vi runbroker.sh + vi runserver.sh + ``` + + 参考配置:JAVA_OPT="${JAVA_OPT} -server -Xms256m -Xmx256m -Xmn128m -XX:MetaspaceSize=128m -XX:MaxMetaspaceSize=320m" + +* 启动 Broker + + ```sh + # 1.启动 Broker + nohup sh bin/mqbroker -n localhost:9876 autoCreateTopicEnable=true & + # 2.查看启动日志 + tail -f ~/logs/rocketmqlogs/broker.log + ``` + +* 发送消息: + + ```sh + # 1.设置环境变量 + export NAMESRV_ADDR=localhost:9876 + # 2.使用安装包的 Demo 发送消息 + sh bin/tools.sh org.apache.rocketmq.example.quickstart.Producer + ``` + +* 接受消息: + + ```sh + # 1.设置环境变量 + export NAMESRV_ADDR=localhost:9876 + # 2.接收消息 + sh bin/tools.sh org.apache.rocketmq.example.quickstart.Consumer + +* 关闭 RocketMQ: + + ```sh + # 1.关闭 NameServer + sh bin/mqshutdown namesrv + # 2.关闭 Broker + sh bin/mqshutdown broker + + + +*** + + + +### 相关概念 + +RocketMQ 主要由 Producer、Broker、Consumer 三部分组成,其中 Producer 负责生产消息,Consumer 负责消费消息,Broker 负责存储消息,NameServer 负责管理 Broker + +* 代理服务器(Broker Server):消息中转角色,负责**存储消息、转发消息**。在 RocketMQ 系统中负责接收从生产者发送来的消息并存储、同时为消费者的拉取请求作准备,也存储消息相关的元数据,包括消费者组、消费进度偏移和主题和队列消息等 +* 名字服务(Name Server):充当**路由消息**的提供者。生产者或消费者能够通过名字服务查找各主题相应的 Broker IP 列表 +* 消息生产者(Producer):负责**生产消息**,把业务应用系统里产生的消息发送到 Broker 服务器。RocketMQ 提供多种发送方式,同步发送、异步发送、顺序发送、单向发送,同步和异步方式均需要 Broker 返回确认信息,单向发送不需要;可以通过 MQ 的负载均衡模块选择相应的 Broker 集群队列进行消息投递,投递的过程支持快速失败并且低延迟 +* 消息消费者(Consumer):负责**消费消息**,一般是后台系统负责异步消费,一个消息消费者会从 Broker 服务器拉取消息、并将其提供给应用程序。从用户应用的角度而提供了两种消费形式: + * 拉取式消费(Pull Consumer):应用通主动调用 Consumer 的拉消息方法从 Broker 服务器拉消息,主动权由应用控制,一旦获取了批量消息,应用就会启动消费过程 + * 推动式消费(Push Consumer):该模式下 Broker 收到数据后会主动推送给消费端,实时性较高 +* 生产者组(Producer Group):同一类 Producer 的集合,发送同一类消息且发送逻辑一致。如果发送的是事务消息且原始生产者在发送之后崩溃,**则 Broker 服务器会联系同一生产者组的其他生产者实例以提交或回溯消费** +* 消费者组(Consumer Group):同一类 Consumer 的集合,消费者实例必须订阅完全相同的 Topic,消费同一类消息且消费逻辑一致。消费者组使得在消息消费方面更容易的实现负载均衡和容错。RocketMQ 支持两种消息模式: + * 集群消费(Clustering):相同 Consumer Group 的每个 Consumer 实例平均分摊消息 + * 广播消费(Broadcasting):相同 Consumer Group 的每个 Consumer 实例都接收全量的消息 + +每个 Broker 可以存储多个 Topic 的消息,每个 Topic 的消息也可以分片存储于不同的 Broker,Message Queue(消息队列)是用于存储消息的物理地址,每个 Topic 中的消息地址存储于多个 Message Queue 中 + +* 主题(Topic):表示一类消息的集合,每个主题包含若干条消息,每条消息只属于一个主题,是 RocketMQ 消息订阅的基本单位 + +* 消息(Message):消息系统所传输信息的物理载体,生产和消费数据的最小单位,每条消息必须属于一个主题。RocketMQ 中每个消息拥有唯一的 Message ID,且可以携带具有业务标识的 Key,系统提供了通过 Message ID 和 Key 查询消息的功能 + +* 标签(Tag):为消息设置的标志,用于同一主题下区分不同类型的消息。标签能够有效地保持代码的清晰度和连贯性,并优化 RocketMQ 提供的查询系统,消费者可以根据 Tag 实现对不同子主题的不同消费逻辑,实现更好的扩展性 + +* 普通顺序消息(Normal Ordered Message):消费者通过同一个消息队列(Topic 分区)收到的消息是有顺序的,不同消息队列收到的消息则可能是无顺序的 + +* 严格顺序消息(Strictly Ordered Message):消费者收到的所有消息均是有顺序的 + + + +官方文档:https://github.com/apache/rocketmq/tree/master/docs/cn(基础知识部分的笔记参考官方文档编写) + + + + + +**** + + + + + +## 消息操作 + +### 基本样例 + +#### 订阅发布 + +消息的发布是指某个生产者向某个 Topic 发送消息,消息的订阅是指某个消费者关注了某个 Topic 中带有某些 Tag 的消息,进而从该 Topic 消费数据 + +导入 MQ 客户端依赖: + +```xml + + org.apache.rocketmq + rocketmq-client + 4.4.0 + +``` + +消息发送者步骤分析: + +1. 创建消息生产者 Producer,并制定生产者组名 +2. 指定 Nameserver 地址 +3. 启动 Producer +4. 创建消息对象,指定主题 Topic、Tag 和消息体 +5. 发送消息 +6. 关闭生产者 Producer + +消息消费者步骤分析: + +1. 创建消费者 Consumer,制定消费者组名 +2. 指定 Nameserver 地址 +3. 订阅主题 Topic 和 Tag +4. 设置回调函数,处理消息 +5. 启动消费者 Consumer + + + +官方文档:https://github.com/apache/rocketmq/blob/master/docs/cn/RocketMQ_Example.md + + + +*** + + + +#### 发送消息 + +##### 同步发送 + +使用 RocketMQ 发送三种类型的消息:同步消息、异步消息和单向消息,其中前两种消息是可靠的,因为会有发送是否成功的应答 + +这种可靠性同步地发送方式使用的比较广泛,比如:重要的消息通知,短信通知 + +```java +public class SyncProducer { + public static void main(String[] args) throws Exception { + // 实例化消息生产者Producer + DefaultMQProducer producer = new DefaultMQProducer("please_rename_unique_group_name"); + // 设置NameServer的地址 + producer.setNamesrvAddr("localhost:9876"); + // 启动Producer实例 + producer.start(); + for (int i = 0; i < 100; i++) { + // 创建消息,并指定Topic,Tag和消息体 + Message msg = new Message( + "TopicTest" /* Topic */, + "TagA" /* Tag */, + ("Hello RocketMQ " + i).getBytes(RemotingHelper.DEFAULT_CHARSET) /* Message body */); + + // 发送消息到一个Broker + SendResult sendResult = producer.send(msg); + // 通过sendResult返回消息是否成功送达 + System.out.printf("%s%n", sendResult); + } + // 如果不再发送消息,关闭Producer实例。 + producer.shutdown(); + } +} +``` + + + +*** + + + +##### 异步发送 + +异步消息通常用在对响应时间敏感的业务场景,即发送端不能容忍长时间地等待 Broker 的响应 + +```java +public class AsyncProducer { + public static void main(String[] args) throws Exception { + // 实例化消息生产者Producer + DefaultMQProducer producer = new DefaultMQProducer("please_rename_unique_group_name"); + // 设置NameServer的地址 + producer.setNamesrvAddr("localhost:9876"); + // 启动Producer实例 + producer.start(); + producer.setRetryTimesWhenSendAsyncFailed(0); + + int messageCount = 100; + // 根据消息数量实例化倒计时计算器 + final CountDownLatch2 countDownLatch = new CountDownLatch2(messageCount); + for (int i = 0; i < messageCount; i++) { + final int index = i; + // 创建消息,并指定Topic,Tag和消息体 + Message msg = new Message("TopicTest", "TagA", "OrderID188", + "Hello world".getBytes(RemotingHelper.DEFAULT_CHARSET)); + + // SendCallback接收异步返回结果的回调 + producer.send(msg, new SendCallback() { + // 发送成功回调函数 + @Override + public void onSuccess(SendResult sendResult) { + countDownLatch.countDown(); + System.out.printf("%-10d OK %s %n", index, sendResult.getMsgId()); + } + + @Override + public void onException(Throwable e) { + countDownLatch.countDown(); + System.out.printf("%-10d Exception %s %n", index, e); + e.printStackTrace(); + } + }); + } + // 等待5s + countDownLatch.await(5, TimeUnit.SECONDS); + // 如果不再发送消息,关闭Producer实例。 + producer.shutdown(); + } +} +``` + + + +*** + + + +##### 单向发送 + +单向发送主要用在不特别关心发送结果的场景,例如日志发送 + +```java +public class OnewayProducer { + public static void main(String[] args) throws Exception{ + // 实例化消息生产者Producer + DefaultMQProducer producer = new DefaultMQProducer("please_rename_unique_group_name"); + // 设置NameServer的地址 + producer.setNamesrvAddr("localhost:9876"); + // 启动Producer实例 + producer.start(); + for (int i = 0; i < 100; i++) { + // 创建消息,并指定Topic,Tag和消息体 + Message msg = new Message("TopicTest","TagA", + ("Hello RocketMQ " + i).getBytes(RemotingHelper.DEFAULT_CHARSET)); + // 发送单向消息,没有任何返回结果 + producer.sendOneway(msg); + } + // 如果不再发送消息,关闭Producer实例。 + producer.shutdown(); + } +} +``` + + + +**** + + + +#### 消费消息 + +```java +public class Consumer { + public static void main(String[] args) throws InterruptedException, MQClientException { + // 实例化消费者 + DefaultMQPushConsumer consumer = new DefaultMQPushConsumer("please_rename_unique_group_name"); + // 设置NameServer的地址 + consumer.setNamesrvAddr("localhost:9876"); + + // 订阅一个或者多个Topic,以及Tag来过滤需要消费的消息 + consumer.subscribe("TopicTest", "*"); + // 注册消息监听器,回调实现类来处理从broker拉取回来的消息 + consumer.registerMessageListener(new MessageListenerConcurrently() { + // 接受消息内容 + @Override + public ConsumeConcurrentlyStatus consumeMessage(List msgs, ConsumeConcurrentlyContext context) { + System.out.printf("%s Receive New Messages: %s %n", Thread.currentThread().getName(), msgs); + // 标记该消息已经被成功消费 + return ConsumeConcurrentlyStatus.CONSUME_SUCCESS; + } + }); + // 启动消费者实例 + consumer.start(); + System.out.printf("Consumer Started.%n"); + } +} +``` + + + + + +**** + + + +### 顺序消息 + +#### 原理解析 + +消息有序指的是一类消息消费时,能按照发送的顺序来消费。例如:一个订单产生了三条消息分别是订单创建、订单付款、订单完成。消费时要按照这个顺序消费才能有意义,但是同时订单之间是可以并行消费的,RocketMQ 可以严格的保证消息有序。 + +顺序消息分为全局顺序消息与分区顺序消息, + +- 全局顺序:对于指定的一个 Topic,所有消息按照严格的先入先出(FIFO)的顺序进行发布和消费,适用于性能要求不高,所有的消息严格按照 FIFO 原则进行消息发布和消费的场景 +- 分区顺序:对于指定的一个 Topic,所有消息根据 Sharding key 进行分区,同一个分组内的消息按照严格的 FIFO 顺序进行发布和消费。Sharding key 是顺序消息中用来区分不同分区的关键字段,和普通消息的 Key 是完全不同的概念,适用于性能要求高的场景 + +在默认的情况下消息发送会采取 Round Robin 轮询方式把消息发送到不同的 queue(分区队列),而消费消息是从多个 queue 上拉取消息,这种情况发送和消费是不能保证顺序。但是如果控制发送的顺序消息只依次发送到同一个 queue 中,消费的时候只从这个 queue 上依次拉取,则就保证了顺序。当**发送和消费参与的 queue 只有一个**,则是全局有序;如果多个queue 参与,则为分区有序,即相对每个 queue,消息都是有序的 + + + +*** + + + +#### 代码实现 + +一个订单的顺序流程是:创建、付款、推送、完成,订单号相同的消息会被先后发送到同一个队列中,消费时同一个 OrderId 获取到的肯定是同一个队列 + +```java +public class Producer { + public static void main(String[] args) throws Exception { + DefaultMQProducer producer = new DefaultMQProducer("please_rename_unique_group_name"); + producer.setNamesrvAddr("127.0.0.1:9876"); + producer.start(); + // 标签集合 + String[] tags = new String[]{"TagA", "TagC", "TagD"}; + + // 订单列表 + List orderList = new Producer().buildOrders(); + + Date date = new Date(); + SimpleDateFormat sdf = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss"); + String dateStr = sdf.format(date); + for (int i = 0; i < 10; i++) { + // 加个时间前缀 + String body = dateStr + " Hello RocketMQ " + orderList.get(i); + Message msg = new Message("OrderTopic", tags[i % tags.length], "KEY" + i, body.getBytes()); + /** + * 参数一:消息对象 + * 参数二:消息队列的选择器 + * 参数三:选择队列的业务标识(订单 ID) + */ + SendResult sendResult = producer.send(msg, new MessageQueueSelector() { + @Override + /** + * mqs:队列集合 + * msg:消息对象 + * arg:业务标识的参数 + */ + public MessageQueue select(List mqs, Message msg, Object arg) { + Long id = (Long) arg; + long index = id % mqs.size(); // 根据订单id选择发送queue + return mqs.get((int) index); + } + }, orderList.get(i).getOrderId());//订单id + + System.out.println(String.format("SendResult status:%s, queueId:%d, body:%s", + sendResult.getSendStatus(), + sendResult.getMessageQueue().getQueueId(), + body)); + } + + producer.shutdown(); + } + + // 订单的步骤 + private static class OrderStep { + private long orderId; + private String desc; + // set + get + } + + // 生成模拟订单数据 + private List buildOrders() { + List orderList = new ArrayList(); + + OrderStep orderDemo = new OrderStep(); + orderDemo.setOrderId(15103111039L); + orderDemo.setDesc("创建"); + orderList.add(orderDemo); + + orderDemo = new OrderStep(); + orderDemo.setOrderId(15103111065L); + orderDemo.setDesc("创建"); + orderList.add(orderDemo); + + orderDemo = new OrderStep(); + orderDemo.setOrderId(15103111039L); + orderDemo.setDesc("付款"); + orderList.add(orderDemo); + + orderDemo = new OrderStep(); + orderDemo.setOrderId(15103117235L); + orderDemo.setDesc("创建"); + orderList.add(orderDemo); + + orderDemo = new OrderStep(); + orderDemo.setOrderId(15103111065L); + orderDemo.setDesc("付款"); + orderList.add(orderDemo); + + orderDemo = new OrderStep(); + orderDemo.setOrderId(15103117235L); + orderDemo.setDesc("付款"); + orderList.add(orderDemo); + + orderDemo = new OrderStep(); + orderDemo.setOrderId(15103111065L); + orderDemo.setDesc("完成"); + orderList.add(orderDemo); + + orderDemo = new OrderStep(); + orderDemo.setOrderId(15103111039L); + orderDemo.setDesc("推送"); + orderList.add(orderDemo); + + orderDemo = new OrderStep(); + orderDemo.setOrderId(15103117235L); + orderDemo.setDesc("完成"); + orderList.add(orderDemo); + + orderDemo = new OrderStep(); + orderDemo.setOrderId(15103111039L); + orderDemo.setDesc("完成"); + orderList.add(orderDemo); + + return orderList; + } +} +``` + +```java +// 顺序消息消费,带事务方式(应用可控制Offset什么时候提交) +public class ConsumerInOrder { + public static void main(String[] args) throws Exception { + DefaultMQPushConsumer consumer = new DefaultMQPushConsumer("please_rename_unique_group_name_3"); + consumer.setNamesrvAddr("127.0.0.1:9876"); + // 设置Consumer第一次启动是从队列头部开始消费还是队列尾部开始消费 + // 如果非第一次启动,那么按照上次消费的位置继续消费 + consumer.setConsumeFromWhere(ConsumeFromWhere.CONSUME_FROM_FIRST_OFFSET); + // 订阅三个tag + consumer.subscribe("OrderTopic", "TagA || TagC || TagD"); + consumer.registerMessageListener(new MessageListenerOrderly() { + Random random = new Random(); + @Override + public ConsumeOrderlyStatus consumeMessage(List msgs, ConsumeOrderlyContext context) { + context.setAutoCommit(true); + for (MessageExt msg : msgs) { + // 可以看到每个queue有唯一的consume线程来消费, 订单对每个queue(分区)有序 + System.out.println("consumeThread=" + Thread.currentThread().getName() + "queueId=" + msg.getQueueId() + ", content:" + new String(msg.getBody())); + } + return ConsumeOrderlyStatus.SUCCESS; + } + }); + consumer.start(); + System.out.println("Consumer Started."); + } +} +``` + + + + + +***** + + + +### 延时消息 + +#### 原理解析 + +定时消息(延迟队列)是指消息发送到 Broker 后,不会立即被消费,等待特定时间投递给真正的 Topic + +RocketMQ 并不支持任意时间的延时,需要设置几个固定的延时等级,从 1s 到 2h 分别对应着等级 1 到 18,消息消费失败会进入延时消息队列,消息发送时间与设置的延时等级和重试次数有关,详见代码 `SendMessageProcessor.java` + +```java +private String messageDelayLevel = "1s 5s 10s 30s 1m 2m 3m 4m 5m 6m 7m 8m 9m 10m 20m 30m 1h 2h"; +``` + +Broker 可以配置 messageDelayLevel,该属性是 Broker 的属性,不属于某个 Topic + +发消息时,可以设置延迟等级 `msg.setDelayLevel(level)`,level 有以下三种情况: + +- level == 0:消息为非延迟消息 +- 1<=level<=maxLevel:消息延迟特定时间,例如 level==1,延迟 1s +- level > maxLevel:则 level== maxLevel,例如 level==20,延迟 2h + +定时消息会暂存在名为 SCHEDULE_TOPIC_XXXX 的 Topic 中,并根据 delayTimeLevel 存入特定的 queue,队列的标识 `queueId = delayTimeLevel – 1`,即**一个 queue 只存相同延迟的消息**,保证具有相同发送延迟的消息能够顺序消费。Broker 会为每个延迟级别提交一个定时任务,调度地消费 SCHEDULE_TOPIC_XXXX,将消息写入真实的 Topic + +注意:定时消息在第一次写入和调度写入真实 Topic 时都会计数,因此发送数量、tps 都会变高 + + + +*** + + + +#### 代码实现 + +提交了一个订单就可以发送一个延时消息,1h 后去检查这个订单的状态,如果还是未付款就取消订单释放库存 + +```java +public class ScheduledMessageProducer { + public static void main(String[] args) throws Exception { + // 实例化一个生产者来产生延时消息 + DefaultMQProducer producer = new DefaultMQProducer("ExampleProducerGroup"); + producer.setNamesrvAddr("127.0.0.1:9876"); + // 启动生产者 + producer.start(); + int totalMessagesToSend = 100; + for (int i = 0; i < totalMessagesToSend; i++) { + Message message = new Message("DelayTopic", ("Hello scheduled message " + i).getBytes()); + // 设置延时等级3,这个消息将在10s之后发送(现在只支持固定的几个时间,详看delayTimeLevel) + message.setDelayTimeLevel(3); + // 发送消息 + producer.send(message); + } + // 关闭生产者 + producer.shutdown(); + } +} +``` + +```java +public class ScheduledMessageConsumer { + public static void main(String[] args) throws Exception { + // 实例化消费者 + DefaultMQPushConsumer consumer = new DefaultMQPushConsumer("ExampleConsumer"); + consumer.setNamesrvAddr("127.0.0.1:9876"); + // 订阅Topics + consumer.subscribe("DelayTopic", "*"); + // 注册消息监听者 + consumer.registerMessageListener(new MessageListenerConcurrently() { + @Override + public ConsumeConcurrentlyStatus consumeMessage(List messages, ConsumeConcurrentlyContext context) { + for (MessageExt message : messages) { + // 打印延迟的时间段 + System.out.println("Receive message[msgId=" + message.getMsgId() + "] " + (System.currentTimeMillis() - message.getBornTimestamp()) + "ms later");} + return ConsumeConcurrentlyStatus.CONSUME_SUCCESS; + } + }); + // 启动消费者 + consumer.start(); + } +} +``` + + + +**** + + + +### 批量消息 + +批量发送消息能显著提高传递小消息的性能,限制是这些批量消息应该有相同的 topic,相同的 waitStoreMsgOK,而且不能是延时消息,并且这一批消息的总大小不应超过 4MB + +```java +public class Producer { + + public static void main(String[] args) throws Exception { + DefaultMQProducer producer = new DefaultMQProducer("ExampleProducerGroup") + producer.setNamesrvAddr("127.0.0.1:9876"); + //启动producer + producer.start(); + + List msgs = new ArrayList(); + // 创建消息对象,指定主题Topic、Tag和消息体 + Message msg1 = new Message("BatchTopic", "Tag1", ("Hello World" + 1).getBytes()); + Message msg2 = new Message("BatchTopic", "Tag1", ("Hello World" + 2).getBytes()); + Message msg3 = new Message("BatchTopic", "Tag1", ("Hello World" + 3).getBytes()); + + msgs.add(msg1); + msgs.add(msg2); + msgs.add(msg3); + + // 发送消息 + SendResult result = producer.send(msgs); + System.out.println("发送结果:" + result); + // 关闭生产者producer + producer.shutdown(); + } +} +``` + +当发送大批量数据时,可能不确定消息是否超过了大小限制(4MB),所以需要将消息列表分割一下 + +```java +public class ListSplitter implements Iterator> { + private final int SIZE_LIMIT = 1024 * 1024 * 4; + private final List messages; + private int currIndex; + + public ListSplitter(List messages) { + this.messages = messages; + } + + @Override + public boolean hasNext() { + return currIndex < messages.size(); + } + + @Override + public List next() { + int startIndex = getStartIndex(); + int nextIndex = startIndex; + int totalSize = 0; + for (; nextIndex < messages.size(); nextIndex++) { + Message message = messages.get(nextIndex); + int tmpSize = calcMessageSize(message); + // 单个消息超过了最大的限制 + if (tmpSize + totalSize > SIZE_LIMIT) { + break; + } else { + totalSize += tmpSize; + } + } + List subList = messages.subList(startIndex, nextIndex); + currIndex = nextIndex; + return subList; + } + + private int getStartIndex() { + Message currMessage = messages.get(currIndex); + int tmpSize = calcMessageSize(currMessage); + while (tmpSize > SIZE_LIMIT) { + currIndex += 1; + Message message = messages.get(curIndex); + tmpSize = calcMessageSize(message); + } + return currIndex; + } + + private int calcMessageSize(Message message) { + int tmpSize = message.getTopic().length() + message.getBody().length; + Map properties = message.getProperties(); + for (Map.Entry entry : properties.entrySet()) { + tmpSize += entry.getKey().length() + entry.getValue().length(); + } + tmpSize = tmpSize + 20; // 增加⽇日志的开销20字节 + return tmpSize; + } + + public static void main(String[] args) { + //把大的消息分裂成若干个小的消息 + ListSplitter splitter = new ListSplitter(messages); + while (splitter.hasNext()) { + try { + List listItem = splitter.next(); + producer.send(listItem); + } catch (Exception e) { + e.printStackTrace(); + //处理error + } + } + } +} +``` + + + + + +*** + + + +### 过滤消息 + +#### 基本语法 + +RocketMQ 定义了一些基本语法来支持过滤特性,可以很容易地扩展: + +- 数值比较,比如:>,>=,<,<=,BETWEEN,= +- 字符比较,比如:=,<>,IN +- IS NULL 或者 IS NOT NULL +- 逻辑符号 AND,OR,NOT + +常量支持类型为: + +- 数值,比如 123,3.1415 +- 字符,比如 'abc',必须用单引号包裹起来 +- NULL,特殊的常量 +- 布尔值,TRUE 或 FALSE + +只有使用 push 模式的消费者才能用使用 SQL92 标准的 sql 语句,接口如下: + +```java +public void subscribe(final String topic, final MessageSelector messageSelector) +``` + +例如:消费者接收包含 TAGA 或 TAGB 或 TAGC 的消息 + +```java +DefaultMQPushConsumer consumer = new DefaultMQPushConsumer("CID_EXAMPLE"); +consumer.subscribe("TOPIC", "TAGA || TAGB || TAGC"); +``` + + + +*** + + + +#### 原理解析 + +RocketMQ 分布式消息队列的消息过滤方式是在 Consumer 端订阅消息时再做消息过滤的,所以是在 Broker 端实现的,优点是减少了对于 Consumer 无用消息的网络传输,缺点是增加了 Broker 的负担,而且实现相对复杂 + +RocketMQ 在 Producer 端写入消息和在 Consumer 端订阅消息采用**分离存储**的机制实现,Consumer 端订阅消息是需要通过 ConsumeQueue 这个消息消费的逻辑队列拿到一个索引,然后再从 CommitLog 里面读取真正的消息实体内容 + +ConsumeQueue 的存储结构如下,有 8 个字节存储的 Message Tag 的哈希值,基于 Tag 的消息过滤就是基于这个字段 + +![](https://seazean.oss-cn-beijing.aliyuncs.com/img/Frame/RocketMQ-消费队列结构.png) + +* Tag 过滤:Consumer 端订阅消息时指定 Topic 和 TAG,然后将订阅请求构建成一个 SubscriptionData,发送一个 Pull 消息的请求给 Broker 端。Broker 端用这些数据先构建一个 MessageFilter,然后传给文件存储层 Store。Store 从 ConsumeQueue 读取到一条记录后,会用它记录的消息 tag hash 值去做过滤。因为在服务端只是根据 hashcode 进行判断,无法精确对 tag 原始字符串进行过滤,所以消费端拉取到消息后,还需要对消息的原始 tag 字符串进行比对,如果不同,则丢弃该消息,不进行消息消费 + +* SQL92 过滤:工作流程和 Tag 过滤大致一样,只是在 Store 层的具体过滤方式不一样。真正的 SQL expression 的构建和执行由 rocketmq-filter 模块负责,每次过滤都去执行 SQL 表达式会影响效率,所以 RocketMQ 使用了 BloomFilter 来避免了每次都去执行 + + + + + +**** + + + +#### 代码实现 + +发送消息时,通过 putUserProperty 来设置消息的属性,SQL92 的表达式上下文为消息的属性 + +```java +public class Producer { + public static void main(String[] args) throws Exception { + DefaultMQProducer producer = new DefaultMQProducer("please_rename_unique_group_name"); + producer.setNamesrvAddr("127.0.0.1:9876"); + producer.start(); + for (int i = 0; i < 10; i++) { + Message msg = new Message("FilterTopic", "tag", + ("Hello RocketMQ " + i).getBytes(RemotingHelper.DEFAULT_CHARSET)); + // 设置一些属性 + msg.putUserProperty("i", String.valueOf(i)); + SendResult sendResult = producer.send(msg); + } + producer.shutdown(); + } +} +``` + +使用 SQL 筛选过滤消息: + +```java +public class Consumer { + public static void main(String[] args) throws Exception { + DefaultMQPushConsumer consumer = new DefaultMQPushConsumer("please_rename_unique_group_name"); + consumer.setNamesrvAddr("127.0.0.1:9876"); + // 过滤属性大于 5 的消息 + consumer.subscribe("FilterTopic", MessageSelector.bySql("i>5")); + + // 设置回调函数,处理消息 + consumer.registerMessageListener(new MessageListenerConcurrently() { + //接受消息内容 + @Override + public ConsumeConcurrentlyStatus consumeMessage(List msgs, ConsumeConcurrentlyContext context) { + for (MessageExt msg : msgs) { + System.out.println("consumeThread=" + Thread.currentThread().getName() + "," + new String(msg.getBody())); + } + return ConsumeConcurrentlyStatus.CONSUME_SUCCESS; + } + }); + // 启动消费者consumer + consumer.start(); + } +} +``` + + + + + +*** + + + +### 事务消息 + +#### 工作流程 + +RocketMQ 支持分布式事务消息,采用了 2PC 的思想来实现了提交事务消息,同时增加一个**补偿逻辑**来处理二阶段超时或者失败的消息,如下图所示: + +![](https://seazean.oss-cn-beijing.aliyuncs.com/img/Frame/RocketMQ-事务消息.png) + +事务消息的大致方案分为两个流程:正常事务消息的发送及提交、事务消息的补偿流程 + +1. 事务消息发送及提交: + + * 发送消息(Half 消息),服务器将消息的主题和队列改为半消息状态,并放入半消息队列 + + * 服务端响应消息写入结果(如果写入失败,此时 Half 消息对业务不可见) + * 根据发送结果执行本地事务 + * 根据本地事务状态执行 Commit 或者 Rollback + + +![](https://seazean.oss-cn-beijing.aliyuncs.com/img/Frame/RocketMQ-事务工作流程.png) + +2. 补偿机制:用于解决消息 Commit 或者 Rollback 发生超时或者失败的情况,比如出现网络问题 + + * Broker 服务端通过**对比 Half 消息和 Op 消息**,对未确定状态的消息推进 CheckPoint + * 没有 Commit/Rollback 的事务消息,服务端根据根据半消息的生产者组,到 ProducerManager 中获取生产者(同一个 Group 的 Producer)的会话通道,发起一次回查(**单向请求**) + * Producer 收到回查消息,检查事务消息状态表内对应的本地事务的状态 + * 根据本地事务状态,重新 Commit 或者 Rollback + + RocketMQ 并不会无休止的进行事务状态回查,最大回查 15 次,如果 15 次回查还是无法得知事务状态,则默认回滚该消息, + + 回查服务:`TransactionalMessageCheckService#run` + + + +**** + + + +#### 两阶段 + +##### 一阶段 + +事务消息相对普通消息最大的特点就是**一阶段发送的消息对用户是不可见的**,因为对于 Half 消息,会备份原消息的主题与消息消费队列,然后改变主题为 RMQ_SYS_TRANS_HALF_TOPIC,由于消费组未订阅该主题,故消费端无法消费 Half 类型的消息 + +RocketMQ 会开启一个**定时任务**,从 Topic 为 RMQ_SYS_TRANS_HALF_TOPIC 中拉取消息进行消费,根据生产者组获取一个服务提供者发送回查事务状态请求,根据事务状态来决定是提交或回滚消息 + +RocketMQ 的具体实现策略:如果写入的是事务消息,对消息的 Topic 和 Queue 等属性进行替换,同时将原来的 Topic 和 Queue 信息存储到**消息的属性**中,因为消息的主题被替换,所以消息不会转发到该原主题的消息消费队列,消费者无法感知消息的存在,不会消费 + + + +*** + + + +##### 二阶段 + +一阶段写入不可见的消息后,二阶段操作: + +* 如果执行 Commit 操作,则需要让消息对用户可见,构建出 Half 消息的索引。一阶段的 Half 消息写到一个特殊的 Topic,构建索引时需要读取出 Half 消息,然后通过一次普通消息的写入操作将 Topic 和 Queue 替换成真正的目标 Topic 和 Queue,生成一条对用户可见的消息。其实就是利用了一阶段存储的消息的内容,在二阶段时恢复出一条完整的普通消息,然后走一遍消息写入流程 + +* 如果是 Rollback 则需要撤销一阶段的消息,因为消息本就不可见,所以并**不需要真正撤销消息**(实际上 RocketMQ 也无法去删除一条消息,因为是顺序写文件的)。RocketMQ 为了区分这条消息没有确定状态的消息,采用 Op 消息标识已经确定状态的事务消息(Commit 或者 Rollback) + +**事务消息无论是 Commit 或者 Rollback 都会记录一个 Op 操作**,两者的区别是 Commit 相对于 Rollback 在写入 Op 消息前将原消息的主题和队列恢复。如果一条事务消息没有对应的 Op 消息,说明这个事务的状态还无法确定(可能是二阶段失败了) + +RocketMQ 将 Op 消息写入到全局一个特定的 Topic 中,通过源码中的方法 `TransactionalMessageUtil.buildOpTopic()`,这个主题是一个内部的 Topic(像 Half 消息的 Topic 一样),不会被用户消费。Op 消息的内容为对应的 Half 消息的存储的 Offset,这样**通过 Op 消息能索引到 Half 消息** + +![](https://seazean.oss-cn-beijing.aliyuncs.com/img/Frame/RocketMQ-OP消息.png) + + + +**** + + + +#### 基本使用 + +##### 使用方式 + +事务消息共有三种状态,提交状态、回滚状态、中间状态: + +- TransactionStatus.CommitTransaction:提交事务,允许消费者消费此消息。 +- TransactionStatus.RollbackTransaction:回滚事务,代表该消息将被删除,不允许被消费 +- TransactionStatus.Unknown:中间状态,代表需要检查消息队列来确定状态 + +使用限制: + +1. 事务消息不支持延时消息和批量消息 +2. Broker 配置文件中的参数 `transactionTimeout` 为特定时间,事务消息将在特定时间长度之后被检查。当发送事务消息时,还可以通过设置用户属性 `CHECK_IMMUNITY_TIME_IN_SECONDS` 来改变这个限制,该参数优先于 `transactionTimeout` 参数 +3. 为了避免单个消息被检查太多次而导致半队列消息累积,默认将单个消息的检查次数限制为 15 次,开发者可以通过 Broker 配置文件的 `transactionCheckMax` 参数来修改此限制。如果已经检查某条消息超过 N 次(N = `transactionCheckMax`), 则 Broker 将丢弃此消息,在默认情况下会打印错误日志。可以通过重写 `AbstractTransactionalMessageCheckListener` 类来修改这个行为 +4. 事务性消息可能不止一次被检查或消费 +5. 提交给用户的目标主题消息可能会失败,可以查看日志的记录。事务的高可用性通过 RocketMQ 本身的高可用性机制来保证,如果希望事务消息不丢失、并且事务完整性得到保证,可以使用同步的双重写入机制 +6. 事务消息的生产者 ID 不能与其他类型消息的生产者 ID 共享。与其他类型的消息不同,事务消息允许反向查询,MQ 服务器能通过消息的生产者 ID 查询到消费者 + + + +*** + + + +##### 代码实现 + +实现事务的监听接口,当发送半消息成功时: + +* `executeLocalTransaction` 方法来执行本地事务,返回三个事务状态之一 +* `checkLocalTransaction` 方法检查本地事务状态,响应消息队列的检查请求,返回三个事务状态之一 + +```java +public class TransactionListenerImpl implements TransactionListener { + private AtomicInteger transactionIndex = new AtomicInteger(0); + private ConcurrentHashMap localTrans = new ConcurrentHashMap<>(); + + @Override + public LocalTransactionState executeLocalTransaction(Message msg, Object arg) { + int value = transactionIndex.getAndIncrement(); + int status = value % 3; + // 将事务ID和状态存入 map 集合 + localTrans.put(msg.getTransactionId(), status); + return LocalTransactionState.UNKNOW; + } + + @Override + public LocalTransactionState checkLocalTransaction(MessageExt msg) { + // 从 map 集合读出当前事务对应的状态 + Integer status = localTrans.get(msg.getTransactionId()); + if (null != status) { + switch (status) { + case 0: + return LocalTransactionState.UNKNOW; + case 1: + return LocalTransactionState.COMMIT_MESSAGE; + case 2: + return LocalTransactionState.ROLLBACK_MESSAGE; + } + } + return LocalTransactionState.COMMIT_MESSAGE; + } +} +``` + +使用 **TransactionMQProducer** 类创建事务性生产者,并指定唯一的 `ProducerGroup`,就可以设置自定义线程池来处理这些检查请求,执行本地事务后,需要根据执行结果对消息队列进行回复 + +```java +public class Producer { + public static void main(String[] args) throws MQClientException, InterruptedException { + // 创建消息生产者 + TransactionMQProducer producer = new TransactionMQProducer("please_rename_unique_group_name"); + ExecutorService executorService = new ThreadPoolExecutor(2, 5, 100, TimeUnit.SECONDS); + producer.setExecutorService(executorService); + + // 创建事务监听器 + TransactionListener transactionListener = new TransactionListenerImpl(); + // 生产者的监听器 + producer.setTransactionListener(transactionListener); + // 启动生产者 + producer.start(); + String[] tags = new String[] {"TagA", "TagB", "TagC", "TagD", "TagE"}; + for (int i = 0; i < 10; i++) { + try { + Message msg = new Message("TransactionTopic", tags[i % tags.length], "KEY" + i, + ("Hello RocketMQ " + i).getBytes(RemotingHelper.DEFAULT_CHARSET)); + // 发送消息 + SendResult sendResult = producer.sendMessageInTransaction(msg, null); + System.out.printf("%s%n", sendResult); + Thread.sleep(10); + } catch (MQClientException | UnsupportedEncodingException e) { + e.printStackTrace(); + } + } + //Thread.sleep(1000000); + //producer.shutdown();暂时不关闭 + } +} +``` + +消费者代码和前面的实例相同的 + + + + + +**** + + + + + +## 系统特性 + +### 工作流程 + +#### 模块介绍 + +NameServer 是一个简单的 Topic 路由注册中心,支持 Broker 的动态注册与发现,生产者或消费者能够通过名字服务查找各主题相应的 Broker IP 列表 + +NameServer 主要包括两个功能: + +* Broker 管理,NameServer 接受 Broker 集群的注册信息,保存下来作为路由信息的基本数据,提供**心跳检测机制**检查 Broker 是否还存活,每 10 秒清除一次两小时没有活跃的 Broker +* 路由信息管理,每个 NameServer 将保存关于 Broker 集群的整个路由信息和用于客户端查询的队列信息,然后 Producer 和 Conumser 通过 NameServer 就可以知道整个 Broker 集群的路由信息,从而进行消息的投递和消费 + +NameServer 特点: + +* NameServer 通常是集群的方式部署,**各实例间相互不进行信息通讯** +* Broker 向每一台 NameServer(集群)注册自己的路由信息,所以每个 NameServer 实例上面**都保存一份完整的路由信息** +* 当某个 NameServer 因某种原因下线了,Broker 仍可以向其它 NameServer 同步其路由信息 + +BrokerServer 主要负责消息的存储、投递和查询以及服务高可用保证,在 RocketMQ 系统中接收从生产者发送来的消息并存储、同时为消费者的拉取请求作准备,也存储消息相关的元数据,包括消费者组、消费进度偏移和主题和队列消息等 + +Broker 包含了以下几个重要子模块: + +* Remoting Module:整个 Broker 的实体,负责处理来自 Clients 端的请求 + +* Client Manager:负责管理客户端(Producer/Consumer)和维护 Consumer 的 Topic 订阅信息 + +* Store Service:提供方便简单的 API 接口处理消息存储到物理硬盘和查询功能 + +* HA Service:高可用服务,提供 Master Broker 和 Slave Broker 之间的数据同步功能 + +* Index Service:根据特定的 Message key 对投递到 Broker 的消息进行索引服务,以提供消息的快速查询 + +![](https://seazean.oss-cn-beijing.aliyuncs.com/img/Frame/RocketMQ-Broker工作流程.png) + + + +*** + + + +#### 总体流程 + +RocketMQ 的工作流程: + +- 启动 NameServer 监听端口,等待 Broker、Producer、Consumer 连上来,相当于一个路由控制中心 +- Broker 启动,跟**所有的 NameServer 保持长连接**,每隔 30s 时间向 NameServer 上报 Topic 路由信息(心跳包)。心跳包中包含当前 Broker 信息(IP、端口等)以及存储所有 Topic 信息。注册成功后,NameServer 集群中就有 Topic 跟 Broker 的映射关系 +- 收发消息前,先创建 Topic,创建 Topic 时需要指定该 Topic 要存储在哪些 Broker 上,也可以在发送消息时自动创建 Topic +- Producer 启动时先跟 NameServer 集群中的**其中一台**建立长连接,并从 NameServer 中获取当前发送的 Topic 存在哪些 Broker 上,同时 Producer 会默认每隔 30s 向 NameServer **定时拉取**一次路由信息 +- Producer 发送消息时,根据消息的 Topic 从本地缓存的 TopicPublishInfoTable 获取路由信息,如果没有则会从 NameServer 上重新拉取并更新,轮询队列列表并选择一个队列 MessageQueue,然后与队列所在的 Broker 建立长连接,向 Broker 发消息 +- Consumer 跟 Producer 类似,跟其中一台 NameServer 建立长连接,**定时获取路由信息**,根据当前订阅 Topic 存在哪些 Broker 上,直接跟 Broker 建立连接通道,在完成客户端的负载均衡后,选择其中的某一个或者某几个 MessageQueue 来拉取消息并进行消费 + + + + + +*** + + + +#### 生产消费 + +At least Once:至少一次,指每个消息必须投递一次,Consumer 先 Pull 消息到本地,消费完成后才向服务器返回 ACK,如果没有消费一定不会 ACK 消息 + +回溯消费:指 Consumer 已经消费成功的消息,由于业务上需求需要重新消费,Broker 在向 Consumer 投递成功消息后,消息仍然需要保留。并且重新消费一般是按照时间维度,例如由于 Consumer 系统故障,恢复后需要重新消费 1 小时前的数据,RocketMQ 支持按照时间回溯消费,时间维度精确到毫秒 + +分布式队列因为有高可靠性的要求,所以数据要进行**持久化存储** + +1. 消息生产者发送消息 +2. MQ 收到消息,将消息进行持久化,在存储中新增一条记录 +3. 返回 ACK 给生产者 +4. MQ push 消息给对应的消费者,然后等待消费者返回 ACK +5. 如果消息消费者在指定时间内成功返回 ACK,那么 MQ 认为消息消费成功,在存储中删除消息;如果 MQ 在指定时间内没有收到 ACK,则认为消息消费失败,会尝试重新 push 消息,重复执行 4、5、6 步骤 +6. MQ 删除消息 + +![](https://seazean.oss-cn-beijing.aliyuncs.com/img/Frame/RocketMQ-消息存取.png) + + + + + +*** + + + + + +### 存储机制 + +#### 存储结构 + +RocketMQ 中 Broker 负责存储消息转发消息,所以以下的结构是存储在 Broker Server 上的,生产者和消费者与 Broker 进行消息的收发是通过主题对应的 Message Queue 完成,类似于通道 + +RocketMQ 消息的存储是由 ConsumeQueue 和 CommitLog 配合完成 的,CommitLog 是消息真正的**物理存储**文件,ConsumeQueue 是消息的逻辑队列,类似数据库的**索引节点**,存储的是指向物理存储的地址。**每个 Topic 下的每个 Message Queue 都有一个对应的 ConsumeQueue 文件** + +每条消息都会有对应的索引信息,Consumer 通过 ConsumeQueue 这个结构来读取消息实体内容 + +![](https://seazean.oss-cn-beijing.aliyuncs.com/img/Frame/RocketMQ-消息存储结构.png) + +* CommitLog:消息主体以及元数据的存储主体,存储 Producer 端写入的消息内容,消息内容不是定长的。消息主要是**顺序写入**日志文件,单个文件大小默认 1G,偏移量代表下一次写入的位置,当文件写满了就继续写入下一个文件 +* ConsumerQueue:消息消费队列,存储消息在 CommitLog 的索引。RocketMQ 消息消费时要遍历 CommitLog 文件,并根据主题 Topic 检索消息,这是非常低效的。引入 ConsumeQueue 作为消费消息的索引,**保存了指定 Topic 下的队列消息在 CommitLog 中的起始物理偏移量 offset**,消息大小 size 和消息 Tag 的 HashCode 值,每个 ConsumeQueue 文件大小约 5.72M +* IndexFile:为了消息查询提供了一种通过 Key 或时间区间来查询消息的方法,通过 IndexFile 来查找消息的方法**不影响发送与消费消息的主流程**。IndexFile 的底层存储为在文件系统中实现的 HashMap 结构,故 RocketMQ 的索引文件其底层实现为 **hash 索引** + +RocketMQ 采用的是混合型的存储结构,即为 Broker 单个实例下所有的队列共用一个日志数据文件(CommitLog)来存储,多个 Topic 的消息实体内容都存储于一个 CommitLog 中。混合型存储结构针对 Producer 和 Consumer 分别采用了**数据和索引部分相分离**的存储结构,Producer 发送消息至 Broker 端,然后 Broker 端使用同步或者异步的方式对消息刷盘持久化,保存至 CommitLog 中。只要消息被持久化至磁盘文件 CommitLog 中,Producer 发送的消息就不会丢失,Consumer 也就肯定有机会去消费这条消息 + +服务端支持长轮询模式,当消费者无法拉取到消息后,可以等下一次消息拉取,Broker 允许等待 30s 的时间,只要这段时间内有新消息到达,将直接返回给消费端。RocketMQ 的具体做法是,使用 Broker 端的后台服务线程 ReputMessageService 不停地分发请求并异步构建 ConsumeQueue(逻辑消费队列)和 IndexFile(索引文件)数据 + + + +**** + + + +#### 内存映射 + +操作系统分为用户态和内核态,文件操作、网络操作需要涉及这两种形态的切换,需要进行数据复制。一台服务器把本机磁盘文件的内容发送到客户端,分为两个步骤: + +* read:读取本地文件内容 + +* write:将读取的内容通过网络发送出去 + +![](https://seazean.oss-cn-beijing.aliyuncs.com/img/Frame/RocketMQ-文件与网络操作.png) + +补充:Prog → NET → I/O → 零拷贝部分的笔记详解相关内容 + +通过使用 mmap 的方式,可以省去向用户态的内存复制,RocketMQ 充分利用**零拷贝技术**,提高消息存盘和网络发送的速度 + +RocketMQ 通过 MappedByteBuffer 对文件进行读写操作,利用了 NIO 中的 FileChannel 模型将磁盘上的物理文件直接映射到用户态的内存地址中,将对文件的操作转化为直接对内存地址进行操作,从而极大地提高了文件的读写效率 + +MappedByteBuffer 内存映射的方式**限制**一次只能映射 1.5~2G 的文件至用户态的虚拟内存,所以 RocketMQ 默认设置单个 CommitLog 日志数据文件为 1G。RocketMQ 的文件存储使用定长结构来存储,方便一次将整个文件映射至内存 + + + +*** + + + +#### 页面缓存 + +页缓存(PageCache)是 OS 对文件的缓存,每一页的大小通常是 4K,用于加速对文件的读写。因为 OS 将一部分的内存用作 PageCache,所以程序对文件进行顺序读写的速度几乎接近于内存的读写速度 + +* 对于数据的写入,OS 会先写入至 Cache 内,随后**通过异步的方式由 pdflush 内核线程将 Cache 内的数据刷盘至物理磁盘上** +* 对于数据的读取,如果一次读取文件时出现未命中 PageCache 的情况,OS 从物理磁盘上访问读取文件的同时,会顺序对其他相邻块的数据文件进行**预读取**(局部性原理,最大 128K) + +在 RocketMQ 中,ConsumeQueue 逻辑消费队列存储的数据较少,并且是顺序读取,在 PageCache 机制的预读取作用下,Consume Queue 文件的读性能几乎接近读内存,即使在有消息堆积情况下也不会影响性能。但是 CommitLog 消息存储的日志数据文件读取内容时会产生较多的随机访问读取,严重影响性能。选择合适的系统 IO 调度算法和固态硬盘,比如设置调度算法为 Deadline,随机读的性能也会有所提升 + + + +*** + + + +#### 刷盘机制 + +两种持久化的方案: + +* 关系型数据库 DB:IO 读写性能比较差,如果 DB 出现故障,则 MQ 的消息就无法落盘存储导致线上故障,可靠性不高 +* 文件系统:消息刷盘至所部署虚拟机/物理机的文件系统来做持久化,分为异步刷盘和同步刷盘两种模式。消息刷盘为消息存储提供了一种高效率、高可靠性和高性能的数据持久化方式,除非部署 MQ 机器本身或是本地磁盘挂了,一般不会出现无法持久化的问题 + +RocketMQ 采用文件系统的方式,无论同步还是异步刷盘,都使用**顺序 IO**,因为磁盘的顺序读写要比随机读写快很多 + +* 同步刷盘:只有在消息真正持久化至磁盘后 RocketMQ 的 Broker 端才会真正返回给 Producer 端一个成功的 ACK 响应,保障 MQ 消息的可靠性,但是性能上会有较大影响,一般适用于金融业务应用该模式较多 + +* 异步刷盘:利用 OS 的 PageCache,只要消息写入内存 PageCache 即可将成功的 ACK 返回给 Producer 端,降低了读写延迟,提高了 MQ 的性能和吞吐量。消息刷盘采用**后台异步线程**提交的方式进行,当内存里的消息量积累到一定程度时,触发写磁盘动作 + +通过 Broker 配置文件里的 flushDiskType 参数设置采用什么方式,可以配置成 SYNC_FLUSH、ASYNC_FLUSH 中的一个 + +![](https://seazean.oss-cn-beijing.aliyuncs.com/img/Frame/RocketMQ-刷盘机制.png) + + + +官方文档:https://github.com/apache/rocketmq/blob/master/docs/cn/design.md + + + +**** + + + + + +### 集群设计 + +#### 集群模式 + +常用的以下几种模式: + +* 单 Master 模式:这种方式风险较大,一旦 Broker 重启或者宕机,会导致整个服务不可用 +* 多 Master 模式:一个集群无 Slave,全是 Master + + - 优点:配置简单,单个 Master 宕机或重启维护对应用无影响,在磁盘配置为 RAID10 时,即使机器宕机不可恢复情况下,由于 RAID10 磁盘非常可靠,消息也不会丢(异步刷盘丢失少量消息,同步刷盘一条不丢),性能最高 + + - 缺点:单台机器宕机期间,这台机器上未被消费的消息在机器恢复之前不可订阅,消息实时性会受到影响 +* 多 Master 多 Slave 模式(同步):每个 Master 配置一个 Slave,有多对 Master-Slave,HA 采用**同步双写**方式,即只有主备都写成功,才向应用返回成功 + + * 优点:数据与服务都无单点故障,Master 宕机情况下,消息无延迟,服务可用性与数据可用性都非常高 + * 缺点:性能比异步复制略低(大约低 10% 左右),发送单个消息的 RT 略高,目前不能实现主节点宕机,备机自动切换为主机 +* 多 Master 多 Slave 模式(异步):HA 采用异步复制的方式,会造成主备有短暂的消息延迟(毫秒级别) + + - 优点:即使磁盘损坏,消息丢失的非常少,且消息实时性不会受影响,同时 Master 宕机后,消费者仍然可以从 Slave 消费,而且此过程对应用透明,不需要人工干预,性能同多 Master 模式几乎一样 + - 缺点:Master 宕机,磁盘损坏情况下会丢失少量消息 + + + +*** + + + +#### 集群架构 + +RocketMQ 网络部署特点: + +- NameServer 是一个几乎**无状态节点**,节点之间相互独立,无任何信息同步 + +- Broker 部署相对复杂,Broker 分为 Master 与 Slave,Master 可以部署多个,一个 Master 可以对应多个 Slave,但是一个 Slave 只能对应一个 Master,Master 与 Slave 的对应关系通过指定相同 BrokerName、不同 BrokerId 来定义,BrokerId 为 0 是 Master,非 0 表示 Slave。**每个 Broker 与 NameServer 集群中的所有节点建立长连接**,定时注册 Topic 信息到所有 NameServer + + 说明:部署架构上也支持一 Master 多 Slave,但只有 BrokerId=1 的从服务器才会参与消息的读负载(读写分离) + +- Producer 与 NameServer 集群中的其中**一个节点(随机选择)建立长连接**,定期从 NameServer 获取 Topic 路由信息,并向提供 Topic 服务的 Master 建立长连接,且定时向 Master **发送心跳**。Producer 完全无状态,可集群部署 + +- Consumer 与 NameServer 集群中的其中一个节点(随机选择)建立长连接,定期从 NameServer 获取 Topic 路由信息,并向提供 Topic 服务的 Master、Slave 建立长连接,且定时向 Master、Slave 发送心跳 + + Consumer 既可以从 Master 订阅消息,也可以从 Slave 订阅消息,在向 Master 拉取消息时,Master 服务器会根据拉取偏移量与最大偏移量的距离(判断是否读老消息,产生读 I/O),以及从服务器是否可读等因素建议下一次是从 Master 还是 Slave 拉取 + +![](https://seazean.oss-cn-beijing.aliyuncs.com/img/Frame/RocketMQ-集群架构.png) + + + +官方文档:https://github.com/apache/rocketmq/blob/master/docs/cn/architecture.md + + + +**** + + + +#### 高可用性 + +NameServer 节点是无状态的,且各个节点直接的数据是一致的,部分 NameServer 不可用也可以保证 MQ 服务正常运行 + +BrokerServer 的高可用通过 Master 和 Slave 的配合: + +* Slave 只负责读,当 Master 不可用,对应的 Slave 仍能保证消息被正常消费 +* 配置多组 Master-Slave 组,其他的 Master-Slave 组也会保证消息的正常发送和消费 +* **目前不支持把 Slave 自动转成 Master**,需要手动停止 Slave 角色的 Broker,更改配置文件,用新的配置文件启动 Broker + + 所以需要配置多个 Master 保证可用性,否则一个 Master 挂了导致整体系统的写操作不可用 + +生产端的高可用:在创建 Topic 的时候,把 Topic 的**多个 Message Queue 创建在多个 Broker 组**上(相同 Broker 名称,不同 brokerId 的机器),当一个 Broker 组的 Master 不可用后,其他组的 Master 仍然可用,Producer 仍然可以发送消息 + +消费端的高可用:在 Consumer 的配置文件中,并不需要设置是从 Master Broker 读还是从 Slave 读,当 Master 不可用或者繁忙的时候,Consumer 会被自动切换到从 Slave 读。有了自动切换的机制,当一个 Master 机器出现故障后,Consumer 仍然可以从 Slave 读取消息,不影响 Consumer 程序,达到了消费端的高可用性 + +![](https://seazean.oss-cn-beijing.aliyuncs.com/img/Frame/RocketMQ-高可用.png) + + + +**** + + + +#### 主从复制 + +如果一个 Broker 组有 Master 和 Slave,消息需要从 Master 复制到 Slave 上,有同步和异步两种复制方式: + +* 同步复制方式:Master 和 Slave 均写成功后才反馈给客户端写成功状态(写 Page Cache)。在同步复制方式下,如果 Master 出故障, Slave 上有全部的备份数据,容易恢复,但是同步复制会增大数据写入延迟,降低系统吞吐量 + +* 异步复制方式:只要 Master 写成功,即可反馈给客户端写成功状态,系统拥有较低的延迟和较高的吞吐量,但是如果 Master 出了故障,有些数据因为没有被写入 Slave,有可能会丢失 + +同步复制和异步复制是通过 Broker 配置文件里的 brokerRole 参数进行设置的,可以设置成 ASYNC_MASTE、RSYNC_MASTER、SLAVE 三个值中的一个 + +一般把刷盘机制配置成 ASYNC_FLUSH,主从复制为 SYNC_MASTER,这样即使有一台机器出故障,仍然能保证数据不丢 + +RocketMQ 支持消息的高可靠,影响消息可靠性的几种情况: + +1. Broker 非正常关闭 +2. Broker 异常 Crash +3. OS Crash +4. 机器掉电,但是能立即恢复供电情况 +5. 机器无法开机(可能是 CPU、主板、内存等关键设备损坏) +6. 磁盘设备损坏 + +前四种情况都属于硬件资源可立即恢复情况,RocketMQ 在这四种情况下能保证消息不丢,或者丢失少量数据(依赖刷盘方式) + +后两种属于单点故障,且无法恢复,一旦发生,在此单点上的消息全部丢失。RocketMQ 在这两种情况下,通过主从异步复制,可保证 99% 的消息不丢,但是仍然会有极少量的消息可能丢失。通过**同步双写技术**可以完全避免单点,但是会影响性能,适合对消息可靠性要求极高的场合,RocketMQ 从 3.0 版本开始支持同步双写 + +一般而言,我们会建议采取同步双写 + 异步刷盘的方式,在消息的可靠性和性能间有一个较好的平衡 + + + +**** + + + +### 负载均衡 + +#### 生产端 + +RocketMQ 中的负载均衡可以分为 Producer 端发送消息时候的负载均衡和 Consumer 端订阅消息的负载均衡 + +Producer 端在发送消息时,会先根据 Topic 找到指定的 TopicPublishInfo,在获取了 TopicPublishInfo 路由信息后,RocketMQ 的客户端在默认方式调用 `selectOneMessageQueue()` 方法从 TopicPublishInfo 中的 messageQueueList 中选择一个队列 MessageQueue 进行发送消息 + +默认会**轮询所有的 Message Queue 发送**,以让消息平均落在不同的 queue 上,而由于 queue可以散落在不同的 Broker,所以消息就发送到不同的 Broker 下,图中箭头线条上的标号代表顺序,发布方会把第一条消息发送至 Queue 0,然后第二条消息发送至 Queue 1,以此类推: + +![](https://seazean.oss-cn-beijing.aliyuncs.com/img/Frame/RocketMQ-producer负载均衡.png) + +容错策略均在 MQFaultStrategy 这个类中定义,有一个 sendLatencyFaultEnable 开关变量: + +* 如果开启,会在**随机(只有初始化索引变量时才随机,正常都是递增)递增取模**的基础上,再过滤掉 not available 的 Broker +* 如果关闭,采用随机递增取模的方式选择一个队列(MessageQueue)来发送消息 + +LatencyFaultTolerance 机制是实现消息发送高可用的核心关键所在,对之前失败的,按一定的时间做退避。例如上次请求的 latency 超过 550Lms,就退避 3000Lms;超过 1000L,就退避 60000L + + + +*** + + + +#### 消费端 + +在 RocketMQ 中,Consumer 端的两种消费模式(Push/Pull)都是基于拉模式来获取消息的,而在 Push 模式只是对 Pull 模式的一种封装,其本质实现为消息拉取线程在从服务器拉取到一批消息,提交到消息消费线程池后,又继续向服务器再次尝试拉取消息,如果未拉取到消息,则延迟一下又继续拉取 + +在两种基于拉模式的消费方式(Push/Pull)中,均需要 Consumer 端在知道从 Broker 端的哪一个消息队列中去获取消息,所以在 Consumer 端来做负载均衡,即 Broker 端中多个 MessageQueue 分配给同一个 Consumer Group 中的哪些 Consumer 消费 + +* 广播模式下要求一条消息需要投递到一个消费组下面所有的消费者实例,所以不存在负载均衡,在实现上,Consumer 分配 queue 时,所有 Consumer 都分到所有的 queue。 + +* 在集群消费模式下,每条消息只需要投递到订阅这个 Topic 的 Consumer Group 下的一个实例即可,RocketMQ 采用主动拉取的方式拉取并消费消息,在拉取的时候需要明确指定拉取哪一条 Message Queue + +集群模式下,每当消费者实例的数量有变更,都会触发一次所有实例的负载均衡,这时候会按照 queue 的数量和实例的数量平均分配 queue 给每个实例。默认的分配算法是 AllocateMessageQueueAveragely: + +![](https://seazean.oss-cn-beijing.aliyuncs.com/img/Frame/RocketMQ-平均队列分配.png) + + 还有一种平均的算法是 AllocateMessageQueueAveragelyByCircle,以环状轮流均分 queue 的形式: + +![](https://seazean.oss-cn-beijing.aliyuncs.com/img/Frame/RocketMQ-平均队列轮流分配.png) + +集群模式下,**queue 都是只允许分配一个实例**,如果多个实例同时消费一个 queue 的消息,由于拉取哪些消息是 Consumer 主动控制的,会导致同一个消息在不同的实例下被消费多次 + +通过增加 Consumer 实例去分摊 queue 的消费,可以起到水平扩展的消费能力的作用。而当有实例下线时,会重新触发负载均衡,这时候原来分配到的 queue 将分配到其他实例上继续消费。但是如果 Consumer 实例的数量比 Message Queue 的总数量还多的话,多出来的 Consumer 实例将无法分到 queue,也就无法消费到消息,也就无法起到分摊负载的作用了,所以需要**控制让 queue 的总数量大于等于 Consumer 的数量** + + + +*** + + + +#### 原理解析 + +在 Consumer 启动后,会通过定时任务不断地向 RocketMQ 集群中的所有 Broker 实例发送心跳包。Broker 端在收到 Consumer 的心跳消息后,会将它维护在 ConsumerManager 的本地缓存变量 consumerTable,同时并将封装后的客户端网络通道信息保存在本地缓存变量 channelInfoTable 中,为 Consumer 端的负载均衡提供可以依据的元数据信息 + +Consumer 端实现负载均衡的核心类 **RebalanceImpl** + +在 Consumer 实例的启动流程中的会启动 MQClientInstance 实例,完成负载均衡服务线程 RebalanceService 的启动(**每隔 20s 执行一次**负载均衡),RebalanceService 线程的 run() 方法最终调用的是 RebalanceImpl 类的 rebalanceByTopic() 方法,该方法是实现 Consumer 端负载均衡的核心。rebalanceByTopic() 方法会根据广播模式还是集群模式做不同的逻辑处理。主要看集群模式: + +* 从 rebalanceImpl 实例的本地缓存变量 topicSubscribeInfoTable 中,获取该 Topic 主题下的消息消费队列集合 mqSet + +* 根据 Topic 和 consumerGroup 为参数调用 `mQClientFactory.findConsumerIdList()` 方法向 Broker 端发送获取该消费组下消费者 ID 列表的 RPC 通信请求(Broker 端基于前面 Consumer 端上报的心跳包数据而构建的 consumerTable 做出响应返回,业务请求码 `GET_CONSUMER_LIST_BY_GROUP`) + +* 先对 Topic 下的消息消费队列、消费者 ID 排序,然后用消息队列分配策略算法(默认是消息队列的平均分配算法),计算出待拉取的消息队列。平均分配算法类似于分页的算法,将所有 MessageQueue 排好序类似于记录,将所有消费端 Consumer 排好序类似页数,并求出每一页需要包含的平均 size 和每个页面记录的范围 range,最后遍历整个 range 而计算出当前 Consumer 端应该分配到的记录(这里即为 MessageQueue) + +* 调用 updateProcessQueueTableInRebalance() 方法,先将分配到的消息队列集合 mqSet 与 processQueueTable 做一个过滤比对 + + ![](https://seazean.oss-cn-beijing.aliyuncs.com/img/Frame/RocketMQ-负载均衡重新平衡算法.png) + +* processQueueTable 标注的红色部分,表示与分配到的消息队列集合 mqSet 互不包含,将这些队列设置 Dropped 属性为 true,然后查看这些队列是否可以移除出 processQueueTable 缓存变量。具体执行 removeUnnecessaryMessageQueue() 方法,即每隔 1s 查看是否可以获取当前消费处理队列的锁,拿到的话返回 true;如果等待 1s 后,仍然拿不到当前消费处理队列的锁则返回 false。如果返回 true,则从 processQueueTable 缓存变量中移除对应的 Entry + +* processQueueTable 的绿色部分,表示与分配到的消息队列集合 mqSet 的交集,判断该 ProcessQueue 是否已经过期了,在 Pull 模式的不用管,如果是 Push 模式的,设置 Dropped 属性为 true,并且调用 removeUnnecessaryMessageQueue() 方法,像上面一样尝试移除 Entry + +* 为过滤后的消息队列集合 mqSet 中每个 MessageQueue 创建 ProcessQueue 对象存入 RebalanceImpl 的 processQueueTable 队列中(其中调用 RebalanceImpl 实例的 `computePullFromWhere(MessageQueue mq)` 方法获取该 MessageQueue 对象的下一个进度消费值 offset,随后填充至接下来要创建的 pullRequest 对象属性中),并**创建拉取请求对象** pullRequest 添加到拉取列表 pullRequestList 中,最后执行 dispatchPullRequest() 方法,将 Pull 消息的请求对象 PullRequest 放入 PullMessageService 服务线程的**阻塞队列** pullRequestQueue 中,待该服务线程取出后向 Broker 端发起 Pull 消息的请求 + + 对比下 RebalancePushImpl 和 RebalancePullImpl 两个实现类的 dispatchPullRequest() 方法,RebalancePullImpl 类里面的该方法为空 + +消息消费队列在**同一消费组不同消费者之间的负载均衡**,其核心设计理念是在**一个消息消费队列在同一时间只允许被同一消费组内的一个消费者消费,一个消息消费者能同时消费多个消息队列** + + + + + +**** + + + +### 消息查询 + +#### 查询方式 + +RocketMQ 支持按照两种维度进行消息查询:按照 Message ID 查询消息、按照 Message Key 查询消息 + +* RocketMQ 中的 MessageID 的长度总共有 16 字节,其中包含了消息存储主机地址(IP 地址和端口),消息 Commit Log offset + + 实现方式:Client 端从 MessageID 中解析出 Broker 的地址(IP 地址和端口)和 Commit Log 的偏移地址,封装成一个 RPC 请求后通过 Remoting 通信层发送(业务请求码 VIEW_MESSAGE_BY_ID)。Broker 端走的是 QueryMessageProcessor,读取消息的过程用其中的 CommitLog 的 offset 和 size 去 CommitLog 中找到真正的记录并解析成一个完整的消息返回 + +* 按照 Message Key 查询消息,IndexFile 索引文件为提供了通过 Message Key 查询消息的服务 + + 实现方式:通过 Broker 端的 QueryMessageProcessor 业务处理器来查询,读取消息的过程用 **Topic 和 Key** 找到 IndexFile 索引文件中的一条记录,根据其中的 CommitLog Offset 从 CommitLog 文件中读取消息的实体内容 + + + +*** + + + +#### 索引机制 + +RocketMQ 的索引文件逻辑结构,类似 JDK 中 HashMap 的实现,具体结构如下: + +![](https://seazean.oss-cn-beijing.aliyuncs.com/img/Frame/RocketMQ-IndexFile索引文件.png) + +IndexFile 文件的存储在 `$HOME\store\index${fileName}`,文件名 fileName 是以创建时的时间戳命名,文件大小是固定的,等于 `40+500W*4+2000W*20= 420000040` 个字节大小。如果消息的 properties 中设置了 UNIQ_KEY 这个属性,就用 `topic + “#” + UNIQ_KEY` 作为 key 来做写入操作;如果消息设置了 KEYS 属性(多个 KEY 以空格分隔),也会用 `topic + “#” + KEY` 来做索引 + +整个 Index File 的结构如图,40 Byte 的 Header 用于保存一些总的统计信息,`4*500W` 的 Slot Table 并不保存真正的索引数据,而是保存每个槽位对应的单向链表的**头指针**,即一个 Index File 可以保存 2000W 个索引,`20*2000W` 是**真正的索引数据** + +索引数据包含了 Key Hash/CommitLog Offset/Timestamp/NextIndex offset 这四个字段,一共 20 Byte + +* NextIndex offset 即前面读出来的 slotValue,如果有 hash 冲突,就可以用这个字段将所有冲突的索引用链表的方式串起来 +* Timestamp 记录的是消息 storeTimestamp 之间的差,并不是一个绝对的时间 + + + +参考文档:https://github.com/apache/rocketmq/blob/master/docs/cn/design.md + + + + + +*** + + + +### 消息重试 + +#### 消息重投 + +生产者在发送消息时,同步消息和异步消息失败会重投,oneway 没有任何保证。消息重投保证消息尽可能发送成功、不丢失,但当出现消息量大、网络抖动时,可能会造成消息重复;生产者主动重发、Consumer 负载变化也会导致重复消息 + +如下方法可以设置消息重投策略: + +- retryTimesWhenSendFailed:同步发送失败重投次数,默认为 2,因此生产者会最多尝试发送 retryTimesWhenSendFailed + 1 次。不会选择上次失败的 Broker,尝试向其他 Broker 发送,**最大程度保证消息不丢**。超过重投次数抛出异常,由客户端保证消息不丢。当出现 RemotingException、MQClientException 和部分 MQBrokerException 时会重投 +- retryTimesWhenSendAsyncFailed:异步发送失败重试次数,异步重试不会选择其他 Broker,仅在同一个 Broker 上做重试,**不保证消息不丢** +- retryAnotherBrokerWhenNotStoreOK:消息刷盘(主或备)超时或 slave 不可用(返回状态非 SEND_OK),是否尝试发送到其他 Broker,默认 false,十分重要消息可以开启 + +注意点: + +* 如果同步模式发送失败,则选择到下一个 Broker,如果异步模式发送失败,则**只会在当前 Broker 进行重试** +* 发送消息超时时间默认 3000 毫秒,就不会再尝试重试 + + + +*** + + + +#### 消息重试 + +Consumer 消费消息失败后,提供了一种重试机制,令消息再消费一次。Consumer 消费消息失败可以认为有以下几种情况: + +- 由于消息本身的原因,例如反序列化失败,消息数据本身无法处理等。这种错误通常需要跳过这条消息,再消费其它消息,而这条失败的消息即使立刻重试消费,99% 也不成功,所以需要提供一种定时重试机制,即过 10 秒后再重试 +- 由于依赖的下游应用服务不可用,例如 DB 连接不可用,外系统网络不可达等。这种情况即使跳过当前失败的消息,消费其他消息同样也会报错,这种情况建议应用 sleep 30s,再消费下一条消息,这样可以减轻 Broker 重试消息的压力 + +RocketMQ 会为每个消费组都设置一个 Topic 名称为 `%RETRY%+consumerGroup` 的重试队列(这个 Topic 的重试队列是**针对消费组**,而不是针对每个 Topic 设置的),用于暂时保存因为各种异常而导致 Consumer 端无法消费的消息 + +* 顺序消息的重试,当消费者消费消息失败后,消息队列 RocketMQ 会自动不断进行消息重试(每次间隔时间为 1 秒),这时应用会出现消息消费被阻塞的情况。所以在使用顺序消息时,必须保证应用能够及时监控并处理消费失败的情况,避免阻塞现象的发生 + +* 无序消息(普通、定时、延时、事务消息)的重试,可以通过设置返回状态达到消息重试的结果。无序消息的重试只针对集群消费方式生效,广播方式不提供失败重试特性,即消费失败后,失败消息不再重试,继续消费新的消息 + +**无序消息情况下**,因为异常恢复需要一些时间,会为重试队列设置多个重试级别,每个重试级别都有对应的重新投递延时,重试次数越多投递延时就越大。RocketMQ 对于重试消息的处理是先保存至 Topic 名称为 `SCHEDULE_TOPIC_XXXX` 的延迟队列中,后台定时任务**按照对应的时间进行 Delay 后**重新保存至 `%RETRY%+consumerGroup` 的重试队列中 + +消息队列 RocketMQ 默认允许每条消息最多重试 16 次,每次重试的间隔时间如下表示: + +| 第几次重试 | 与上次重试的间隔时间 | 第几次重试 | 与上次重试的间隔时间 | +| :--------: | :------------------: | :--------: | :------------------: | +| 1 | 10 秒 | 9 | 7 分钟 | +| 2 | 30 秒 | 10 | 8 分钟 | +| 3 | 1 分钟 | 11 | 9 分钟 | +| 4 | 2 分钟 | 12 | 10 分钟 | +| 5 | 3 分钟 | 13 | 20 分钟 | +| 6 | 4 分钟 | 14 | 30 分钟 | +| 7 | 5 分钟 | 15 | 1 小时 | +| 8 | 6 分钟 | 16 | 2 小时 | + +如果消息重试 16 次后仍然失败,消息将**不再投递**,如果严格按照上述重试时间间隔计算,某条消息在一直消费失败的前提下,将会在接下来的 4 小时 46 分钟之内进行 16 次重试,超过这个时间范围消息将不再重试投递 + +时间间隔不支持自定义配置,最大重试次数可通过自定义参数 `MaxReconsumeTimes` 取值进行配置,若配置超过 16 次,则超过的间隔时间均为 2 小时 + +说明:一条消息无论重试多少次,**消息的 Message ID 是不会改变的** + + + +*** + + + +#### 重试操作 + +集群消费方式下,消息消费失败后期望消息重试,需要在消息监听器接口的实现中明确进行配置(三种方式任选一种): + +- 返回 Action.ReconsumeLater (推荐) +- 返回 null +- 抛出异常 + +```java +public class MessageListenerImpl implements MessageListener { + @Override + public Action consume(Message message, ConsumeContext context) { + // 处理消息 + doConsumeMessage(message); + //方式1:返回 Action.ReconsumeLater,消息将重试 + return Action.ReconsumeLater; + //方式2:返回 null,消息将重试 + return null; + //方式3:直接抛出异常, 消息将重试 + throw new RuntimeException("Consumer Message exceotion"); + } +} +``` + +集群消费方式下,消息失败后期望消息不重试,需要捕获消费逻辑中可能抛出的异常,最终返回 Action.CommitMessage,此后这条消息将不会再重试 + +```java +public class MessageListenerImpl implements MessageListener { + @Override + public Action consume(Message message, ConsumeContext context) { + try { + doConsumeMessage(message); + } catch (Throwable e) { + // 捕获消费逻辑中的所有异常,并返回 Action.CommitMessage; + return Action.CommitMessage; + } + //消息处理正常,直接返回 Action.CommitMessage; + return Action.CommitMessage; + } +} +``` + +自定义消息最大重试次数,RocketMQ 允许 Consumer 启动的时候设置最大重试次数,重试时间间隔将按照如下策略: + +- 最大重试次数小于等于 16 次,则重试时间间隔同上表描述 +- 最大重试次数大于 16 次,超过 16 次的重试时间间隔均为每次 2 小时 + +```java +Properties properties = new Properties(); +// 配置对应 Group ID 的最大消息重试次数为 20 次 +properties.put(PropertyKeyConst.MaxReconsumeTimes,"20"); +Consumer consumer = ONSFactory.createConsumer(properties); +``` + +注意: + +- 消息最大重试次数的设置对相同 Group ID 下的所有 Consumer 实例有效。例如只对相同 Group ID 下两个 Consumer 实例中的其中一个设置了 MaxReconsumeTimes,那么该配置对两个 Consumer 实例均生效 +- 配置采用覆盖的方式生效,即最后启动的 Consumer 实例会覆盖之前的启动实例的配置 + +消费者收到消息后,可按照如下方式获取消息的重试次数: + +```java +public class MessageListenerImpl implements MessageListener { + @Override + public Action consume(Message message, ConsumeContext context) { + // 获取消息的重试次数 + System.out.println(message.getReconsumeTimes()); + return Action.CommitMessage; + } +} +``` + + + + + +*** + + + +### 死信队列 + +正常情况下无法被消费的消息称为死信消息(Dead-Letter Message),存储死信消息的特殊队列称为死信队列(Dead-Letter Queue) + +当一条消息初次消费失败,消息队列 RocketMQ 会自动进行消息重试,达到最大重试次数后,若消费依然失败,则表明消费者在正常情况下无法正确地消费该消息,此时 RocketMQ 不会立刻将消息丢弃,而是将其发送到该消费者对应的死信队列中 + +死信消息具有以下特性: + +- 不会再被消费者正常消费 +- 有效期与正常消息相同,均为 3 天,3 天后会被自动删除,所以请在死信消息产生后的 3 天内及时处理 + +死信队列具有以下特性: + +- **一个死信队列对应一个 Group ID, 而不是对应单个消费者实例** +- 如果一个 Group ID 未产生死信消息,消息队列 RocketMQ 不会为其创建相应的死信队列 +- 一个死信队列包含了对应 Group ID 产生的所有死信消息,不论该消息属于哪个 Topic + +一条消息进入死信队列,需要排查可疑因素并解决问题后,可以在消息队列 RocketMQ 控制台重新发送该消息,让消费者重新消费一次 + + + + + +*** + + + +### 高可靠性 + +RocketMQ 消息丢失可能发生在以下三个阶段: + +- 生产阶段:消息在 Producer 发送端创建出来,经过网络传输发送到 Broker 存储端 + - 生产者得到一个成功的响应,就可以认为消息的存储和消息的消费都是可靠的 + - 消息重投机制 +- 存储阶段:消息在 Broker 端存储,如果是主备或者多副本,消息会在这个阶段被复制到其他的节点或者副本上 + - 单点:刷盘机制(同步或异步) + - 主从:消息同步机制(异步复制或同步双写,主从复制章节详解) + - 过期删除:操作 CommitLog、ConsumeQueue 文件是基于文件内存映射机制,并且在启动的时候会将所有的文件加载,为了避免内存与磁盘的浪费,让磁盘能够循环利用,防止磁盘不足导致消息无法写入等引入了文件过期删除机制。最终使得磁盘水位保持在一定水平,最终保证新写入消息的可靠存储 +- 消费阶段:Consumer 消费端从 Broker存储端拉取消息,经过网络传输发送到 Consumer 消费端上 + - 消息重试机制来最大限度的保证消息的消费 + - 消费失败的进行消息回退,重试次数过多的消息放入死信队列 + + + +推荐文章:https://cdn.modb.pro/db/394751 + + + +**** + + + +### 幂等消费 + +消息队列 RocketMQ 消费者在接收到消息以后,需要根据业务上的唯一 Key 对消息做幂等处理 + +At least Once 机制保证消息不丢失,但是可能会造成消息重复,RocketMQ 中无法避免消息重复(Exactly-Once),在互联网应用中,尤其在网络不稳定的情况下,几种情况: + +- 发送时消息重复:当一条消息已被成功发送到服务端并完成持久化,此时出现了网络闪断或客户端宕机,导致服务端对客户端应答失败。此时生产者意识到消息发送失败并尝试再次发送消息,消费者后续会收到两条内容相同并且 Message ID 也相同的消息 + +- 投递时消息重复:消息消费的场景下,消息已投递到消费者并完成业务处理,当客户端给服务端反馈应答的时候网络闪断。为了保证消息至少被消费一次,消息队列 RocketMQ 的服务端将在网络恢复后再次尝试投递之前已被处理过的消息,消费者后续会收到两条内容相同并且 Message ID 也相同的消息 + +- 负载均衡时消息重复:当消息队列 RocketMQ 的 Broker 或客户端重启、扩容或缩容时,会触发 Rebalance,此时消费者可能会收到重复消息 + + +处理方式: + +* 因为 Message ID 有可能出现冲突(重复)的情况,所以真正安全的幂等处理,不建议以 Message ID 作为处理依据,最好的方式是以业务唯一标识作为幂等处理的关键依据,而业务的唯一标识可以通过消息 Key 进行设置: + + ```java + Message message = new Message(); + message.setKey("ORDERID_100"); + SendResult sendResult = producer.send(message); + ``` + +* 订阅方收到消息时可以根据消息的 Key 进行幂等处理: + + ```java + consumer.subscribe("ons_test", "*", new MessageListener() { + public Action consume(Message message, ConsumeContext context) { + String key = message.getKey() + // 根据业务唯一标识的 key 做幂等处理 + } + }); + ``` + + + + + +*** + + + +### 流量控制 + +生产者流控,因为 Broker 处理能力达到瓶颈;消费者流控,因为消费能力达到瓶颈 + +生产者流控: + +- CommitLog 文件被锁时间超过 osPageCacheBusyTimeOutMills 时,参数默认为 1000ms,返回流控 +- 如果开启 transientStorePoolEnable == true,且 Broker 为异步刷盘的主机,且 transientStorePool 中资源不足,拒绝当前 send 请求,返回流控 +- Broker 每隔 10ms 检查 send 请求队列头部请求的等待时间,如果超过 waitTimeMillsInSendQueue,默认 200ms,拒绝当前 send 请求,返回流控。 +- Broker 通过拒绝 send 请求方式实现流量控制 + +注意:生产者流控,不会尝试消息重投 + +消费者流控: + +- 消费者本地缓存消息数超过 pullThresholdForQueue 时,默认 1000 +- 消费者本地缓存消息大小超过 pullThresholdSizeForQueue 时,默认 100MB +- 消费者本地缓存消息跨度超过 consumeConcurrentlyMaxSpan 时,默认 2000 + +消费者流控的结果是降低拉取频率 + + + + + +*** + + + + + +## 原理解析 + +### Namesrv + +#### 服务启动 + +##### 启动方法 + +NamesrvStartup 类中有 Namesrv 服务的启动方法: + +```java +public static void main(String[] args) { + // 如果启动时 使用 -c -p 设置参数了,这些参数存储在 args 中 + main0(args); +} + +public static NamesrvController main0(String[] args) { + try { + // 创建 namesrv 控制器,用来初始化 namesrv 启动 namesrv 关闭 namesrv + NamesrvController controller = createNamesrvController(args); + // 启动 controller + start(controller); + return controller; + } catch (Throwable e) { + // 出现异常,停止系统 + System.exit(-1); + } + return null; +} +``` + +NamesrvStartup#createNamesrvController:读取配置信息,初始化 Namesrv 控制器 + +* `ServerUtil.parseCmdLine("mqnamesrv", args, buildCommandlineOptions(options),..)`:解析启动时的参数信息 + +* `namesrvConfig = new NamesrvConfig()`:创建 Namesrv 配置对象 + + * `private String rocketmqHome`:获取 ROCKETMQ_HOME 值 + * `private boolean orderMessageEnable = false`:**顺序消息**功能是否开启 + +* `nettyServerConfig = new NettyServerConfig()`:Netty 的服务器配置对象 + +* `nettyServerConfig.setListenPort(9876)`:Namesrv 服务器的**监听端口设置为 9876** + +* `if (commandLine.hasOption('c'))`:读取命令行 -c 的参数值 + + `in = new BufferedInputStream(new FileInputStream(file))`:读取指定目录的配置文件 + + `properties.load(in)`:将配置文件信息加载到 properties 对象,相关属性会复写到 Namesrv 配置和 Netty 配置对象 + + `namesrvConfig.setConfigStorePath(file)`:将配置文件的路径保存到配置保存字段 + +* `if (null == namesrvConfig.getRocketmqHome())`:检查 ROCKETMQ_HOME 配置是否是空,是空就报错 + +* `lc = (LoggerContext) LoggerFactory.getILoggerFactory()`:创建日志对象 + +* `controller = new NamesrvController(namesrvConfig, nettyServerConfig)`:**创建 Namesrv 控制器** + +NamesrvStartup#start:启动 Namesrv 控制器 + +* `boolean initResult = controller.initialize()`:初始化方法 + +* ` Runtime.getRuntime().addShutdownHook(new ShutdownHookThread())`:JVM HOOK 平滑关闭的逻辑, 当 JVM 被关闭时,主动调用 controller.shutdown() 方法,让服务器平滑关机 +* `controller.start()`:启动服务器 + + + +源码解析参考视频:https://space.bilibili.com/457326371 + + + +**** + + + + + +##### 控制器类 + +NamesrvController 用来初始化和启动 Namesrv 服务器 + +* 成员变量: + + ```java + private final ScheduledExecutorService scheduledExecutorService; // 调度线程池,用来执行定时任务 + private final RouteInfoManager routeInfoManager; // 管理【路由信息】的对象 + private RemotingServer remotingServer; // 【网络层】封装对象 + private BrokerHousekeepingService brokerHousekeepingService; // 用于监听 channel 状态 + ``` + + `private ExecutorService remotingExecutor`:业务线程池,**netty 线程解析报文成 RemotingCommand 对象,然后将该对象交给业务线程池再继续处理** + +* 初始化: + + ```java + public boolean initialize() { + // 加载本地kv配置(我还不明白 kv 配置是啥) + this.kvConfigManager.load(); + // 创建网络服务器对象,【将 netty 的配置和监听器传入】 + // 监听器监听 channel 状态的改变,会向事件队列发起事件,最后交由 service 处理 + this.remotingServer = new NettyRemotingServer(this.nettyServerConfig, this.brokerHousekeepingService); + // 【创建业务线程池,默认线程数 8】 + this.remotingExecutor = Executors.newFixedThreadPool(nettyServerConfig.getServerWorkerThreads().); + + // 注册协议处理器(缺省协议处理器),【处理器是 DefaultRequestProcessor】,线程使用的是刚创建的业务的线程池 + this.registerProcessor(); + + // 定时任务1:每 10 秒钟检查 broker 存活状态,将 IDLE 状态的 broker 移除【扫描机制,心跳检测】 + this.scheduledExecutorService.scheduleAtFixedRate(new Runnable() { + @Override + public void run() { + // 扫描 brokerLiveTable 表,将两小时没有活动的 broker 关闭, + // 通过 next.getKey() 获取 broker 的地址,然后【关闭服务器与broker物理节点的 channel】 + NamesrvController.this.routeInfoManager.scanNotActiveBroker(); + } + }, 5, 10, TimeUnit.SECONDS); + + // 定时任务2:每 10 分钟打印一遍 kv 配置。 + this.scheduledExecutorService.scheduleAtFixedRate(new Runnable() { + @Override + public void run() { + NamesrvController.this.kvConfigManager.printAllPeriodically(); + } + }, 1, 10, TimeUnit.MINUTES); + + return true; + } + ``` + +* 启动方法: + + ```java + public void start() throws Exception { + // 服务器网络层启动。 + this.remotingServer.start(); + + if (this.fileWatchService != null) { + this.fileWatchService.start(); + } + } + ``` + + + + + +*** + + + +#### 网络通信 + +##### 通信原理 + +RocketMQ 的 RPC 通信采用 Netty 组件作为底层通信库,同样也遵循了 Reactor 多线程模型,NettyRemotingServer 类负责框架的通信服务,同时又在这之上做了一些扩展和优化 + +![](https://seazean.oss-cn-beijing.aliyuncs.com/img/Frame/RocketMQ-Reactor设计.png) + +RocketMQ 基于 NettyRemotingServer 的 Reactor 多线程模型: + +* 一个 Reactor 主线程(eventLoopGroupBoss)负责监听 TCP 网络连接请求,建立好连接创建 SocketChannel(RocketMQ 会自动根据 OS 的类型选择 NIO 和 Epoll,也可以通过参数配置),并注册到 Selector 上,然后监听真正的网络数据 + +* 拿到网络数据交给 Worker 线程池(eventLoopGroupSelector,默认设置为 3),在真正执行业务逻辑之前需要进行 SSL 验证、编解码、空闲检查、网络连接管理,这些工作交给 defaultEventExecutorGroup(默认设置为 8)去做 +* 处理业务操作放在业务线程池中执行,根据 RomotingCommand 的**业务请求码 code** 去 processorTable 这个本地缓存变量中找到对应的 processor,封装成 task 任务提交给对应的 processor 处理线程池来执行(sendMessageExecutor,以发送消息为例) +* 从入口到业务逻辑的几个步骤中线程池一直再增加,这跟每一步逻辑复杂性相关,越复杂,需要的并发通道越宽 + +| 线程数 | 线程名 | 线程具体说明 | +| ------ | ------------------------------ | ------------------------- | +| 1 | NettyBoss_%d | Reactor 主线程 | +| N | NettyServerEPOLLSelector_%d_%d | Reactor 线程池 | +| M1 | NettyServerCodecThread_%d | Worker 线程池 | +| M2 | RemotingExecutorThread_%d | 业务 processor 处理线程池 | + +RocketMQ 的异步通信流程: + +![](https://seazean.oss-cn-beijing.aliyuncs.com/img/Frame/RocketMQ-异步通信流程.png) + + + +==todo:后期对 Netty 有了更深的认知后会进行扩充,现在暂时 copy 官方文档== + +官方文档:https://github.com/apache/rocketmq/blob/master/docs/cn/design.md#2-%E9%80%9A%E4%BF%A1%E6%9C%BA%E5%88%B6 + + + +*** + + + +##### 成员属性 + +NettyRemotingServer 类成员变量: + +* 服务器相关属性: + + ```java + private final ServerBootstrap serverBootstrap; // netty 服务端启动对象 + private final EventLoopGroup eventLoopGroupSelector; // netty worker 组线程池,【默认 3 个线程】 + private final EventLoopGroup eventLoopGroupBoss; // netty boss 组线程池,【一般是 1 个线程】 + private final NettyServerConfig nettyServerConfig; // netty 服务端网络配置 + private int port = 0; // 服务器绑定的端口 + ``` + +* 公共线程池:注册处理器时如果未指定线程池,则业务处理使用公共线程池,线程数量默认是 4 + + ```java + private final ExecutorService publicExecutor; + ``` + +* 事件监听器:Nameserver 使用 BrokerHouseKeepingService,Broker 使用 ClientHouseKeepingService + + ```java + private final ChannelEventListener channelEventListener; + ``` + +* 事件处理线程池:默认是 8 + + ```java + private DefaultEventExecutorGroup defaultEventExecutorGroup; + ``` + +* 定时器:执行循环任务,并且将定时器线程设置为守护线程 + + ```java + private final Timer timer = new Timer("ServerHouseKeepingService", true); + ``` + +* 处理器:多个 Channel 共享的处理器 Handler,多个通道使用同一个对象 + +* Netty 配置对象: + + ```java + public class NettyServerConfig implements Cloneable { + // 服务端启动时监听的端口号 + private int listenPort = 8888; + // 【业务线程池】 线程数量 + private int serverWorkerThreads = 8; + // 根据该值创建 remotingServer 内部的一个 publicExecutor + private int serverCallbackExecutorThreads = 0; + // netty 【worker】线程数 + private int serverSelectorThreads = 3; + // 【单向访问】时的并发限制 + private int serverOnewaySemaphoreValue = 256; + // 【异步访问】时的并发限制 + private int serverAsyncSemaphoreValue = 64; + // channel 最大的空闲存活时间 默认是 2min + private int serverChannelMaxIdleTimeSeconds = 120; + // 发送缓冲区大小 65535 + private int serverSocketSndBufSize = NettySystemConfig.socketSndbufSize; + // 接收缓冲区大小 65535 + private int serverSocketRcvBufSize = NettySystemConfig.socketRcvbufSize; + // 是否启用 netty 内存池 默认开启 + private boolean serverPooledByteBufAllocatorEnable = true; + + // 默认 linux 会启用 【epoll】 + private boolean useEpollNativeSelector = false; + } + ``` + + +构造方法: + +* 无监听器构造: + + ```java + public NettyRemotingServer(final NettyServerConfig nettyServerConfig) { + this(nettyServerConfig, null); + } + ``` + +* 有参构造方法: + + ```java + public NettyRemotingServer(final NettyServerConfig nettyServerConfig, + final ChannelEventListener channelEventListener) { + // 服务器对客户端主动发起请求时并发限制。【单向请求和异步请求】的并发限制 + super(nettyServerConfig.getServerOnewaySemaphoreValue(), nettyServerConfig.getServerAsyncSemaphoreValue()); + // Netty 的启动器,负责组装 netty 组件 + this.serverBootstrap = new ServerBootstrap(); + // 成员变量的赋值 + this.nettyServerConfig = nettyServerConfig; + this.channelEventListener = channelEventListener; + + // 公共线程池的线程数量,默认给的0,这里最终修改为4. + int publicThreadNums = nettyServerConfig.getServerCallbackExecutorThreads(); + if (publicThreadNums <= 0) { + publicThreadNums = 4; + } + // 创建公共线程池,指定线程工厂,设置线程名称前缀:NettyServerPublicExecutor_[数字] + this.publicExecutor = Executors.newFixedThreadPool(publicThreadNums, new ThreadFactory(){.}); + + // 创建两个 netty 的线程组,一个是boss组,一个是worker组,【linux 系统默认启用 epoll】 + if (useEpoll()) {...} else {...} + // SSL 相关 + loadSslContext(); + } + ``` + + + + + +*** + + + +##### 启动方法 + +核心方法的解析: + +* start():启动方法,**创建 BootStrap,并添加 NettyServerHandler 处理器** + + ```java + public void start() { + // Channel Pipeline 内的 handler 使用的线程资源,【线程分配给 handler 处理事件】 + this.defaultEventExecutorGroup = new DefaultEventExecutorGroup(...); + + // 创建通用共享的处理器 handler,【非常重要的 NettyServerHandler】 + prepareSharableHandlers(); + + ServerBootstrap childHandler = + // 配置工作组 boss(数量1) 和 worker(数量3) 组 + this.serverBootstrap.group(this.eventLoopGroupBoss, this.eventLoopGroupSelector) + // 设置服务端 ServerSocketChannel 类型, Linux 用 epoll + .channel(useEpoll() ? EpollServerSocketChannel.class : NioServerSocketChannel.class) + // 设置服务端 channel 选项 + .option(ChannelOption.SO_BACKLOG, 1024) + // 客户端 channel 选项 + .childOption(ChannelOption.TCP_NODELAY, true) + // 设置服务器端口 + .localAddress(new InetSocketAddress(this.nettyServerConfig.getListenPort())) + // 向 channel pipeline 添加了很多 handler,【包括 NettyServerHandler】 + .childHandler(new ChannelInitializer() {}); + + // 客户端开启 内存池,使用的内存池是 PooledByteBufAllocator.DEFAULT + if (nettyServerConfig.isServerPooledByteBufAllocatorEnable()) { + childHandler.childOption(ChannelOption.ALLOCATOR, PooledByteBufAllocator.DEFAULT); + } + + try { + // 同步等待建立连接,并绑定端口。 + ChannelFuture sync = this.serverBootstrap.bind().sync(); + InetSocketAddress addr = (InetSocketAddress) sync.channel().localAddress(); + // 将服务器成功绑定的端口号赋值给字段 port。 + this.port = addr.getPort(); + } catch (InterruptedException e1) {} + + // housekeepingService 不为空,则创建【网络异常事件处理器】 + if (this.channelEventListener != null) { + // 线程一直轮询 nettyEvent 状态,根据 CONNECT,CLOSE,IDLE,EXCEPTION 四种事件类型 + // CONNECT 不做操作,其余都是回调 onChannelDestroy 【关闭服务器与 Broker 物理节点的 Channel】 + this.nettyEventExecutor.start(); + } + + // 提交定时任务,每一秒 执行一次。扫描 responseTable 表,将过期的数据移除 + this.timer.scheduleAtFixedRate(new TimerTask() { + @Override + public void run() { + NettyRemotingServer.this.scanResponseTable(); + } + }, 1000 * 3, 1000); + } + ``` + +* registerProcessor():注册业务处理器 + + ```java + public void registerProcessor(int requestCode, NettyRequestProcessor processor, ExecutorService executor) { + ExecutorService executorThis = executor; + if (null == executor) { + // 未指定线程池资源,将公共线程池赋值 + executorThis = this.publicExecutor; + } + // pair 对象,第一个参数代表的是处理器, 第二个参数是线程池,默认是公共的线程池 + Pair pair = new Pair(processor, executorThis); + + // key 是请求码,value 是 Pair 对象 + this.processorTable.put(requestCode, pair); + } + ``` + +* getProcessorPair():**根据请求码获取对应的处理器和线程池资源** + + ```java + public Pair getProcessorPair(int requestCode) { + return processorTable.get(requestCode); + } + ``` + + + +*** + + + +##### 请求方法 + +在 RocketMQ 消息队列中支持通信的方式主要有同步(sync)、异步(async)、单向(oneway)三种,其中单向通信模式相对简单,一般用在发送心跳包场景下,无需关注其 Response + +服务器主动向客户端发起请求时,使用三种方法 + +* invokeSync(): 同步调用,**服务器需要阻塞等待调用的返回结果** + * `int opaque = request.getOpaque()`:获取请求 ID(与请求码不同) + * `responseFuture = new ResponseFuture(...)`:**创建响应对象**,没有回调函数和 Once + * `this.responseTable.put(opaque, responseFuture)`:**加入到响应映射表中**,key 为请求 ID + * `SocketAddress addr = channel.remoteAddress()`:获取客户端的地址信息 + * `channel.writeAndFlush(request).addListener(...)`:将**业务 Command 信息**写入通道,业务线程将数据交给 Netty ,Netty 的 IO 线程接管写刷数据的操作,**监听器由 IO 线程在写刷后回调** + * `if (f.isSuccess())`:写入成功会将响应对象设置为成功状态直接 return,写入失败设置为失败状态 + * `responseTable.remove(opaque)`:将当前请求的 responseFuture **从映射表移除** + * `responseFuture.setCause(f.cause())`:设置错误的信息 + * `responseFuture.putResponse(null)`:响应 Command 设置为 null + * `responseCommand = responseFuture.waitResponse(timeoutMillis)`:当前线程设置超时时间挂起,**同步等待响应** + * `if (null == responseCommand)`:超时或者出现异常,直接报错 + * `return responseCommand`:返回响应 Command 信息 +* invokeAsync():异步调用,有回调对象,无返回值 + * `boolean acquired = this.semaphoreAsync.tryAcquire(timeoutMillis, TimeUnit.MILLISECONDS)`:获取信号量的许可证,信号量用来**限制异步请求**的数量 + * `if (acquired)`:许可证获取失败说明并发较高,会抛出异常 + * `once = new SemaphoreReleaseOnlyOnce(this.semaphoreAsync)`:Once 对象封装了释放信号量的操作 + * `costTime = System.currentTimeMillis() - beginStartTime`:计算一下耗费的时间,超时不再发起请求 + * `responseFuture = new ResponseFuture()`:**创建响应对象,包装了回调函数和 Once 对象** + * `this.responseTable.put(opaque, responseFuture)`:加入到响应映射表中,key 为请求 ID + * `channel.writeAndFlush(request).addListener(...)`:写刷数据 + * `if (f.isSuccess())`:写刷成功,设置 responseFuture 发生状态为 true + * `requestFail(opaque)`:写入失败,使用 publicExecutor **公共线程池异步执行回调对象的函数** + * `responseFuture.release()`:出现异常会释放信号量 + +* invokeOneway():单向调用,不关注响应结果 + * `request.markOnewayRPC()`:设置单向标记,对端检查标记可知该请是单向请求 + * `boolean acquired = this.semaphoreOneway.tryAcquire(timeoutMillis, TimeUnit.MILLISECONDS)`:获取信号量的许可证,信号量用来**限制单向请求**的数量 + + + + + +*** + + + +#### 处理器类 + +##### 协议设计 + +在 Client 和 Server 之间完成一次消息发送时,需要对发送的消息进行一个协议约定,所以自定义 RocketMQ 的消息协议。在 RocketMQ 中,为了高效地在网络中传输消息和对收到的消息读取,就需要对消息进行编解码,RemotingCommand 这个类在消息传输过程中对所有数据内容的封装,不但包含了所有的数据结构,还包含了编码解码操作 + +| Header字段 | 类型 | Request 说明 | Response 说明 | +| ---------- | ----------------------- | ------------------------------------------------------------ | ------------------------------------------- | +| code | int | 请求操作码,应答方根据不同的请求码进行不同的处理 | 应答响应码,0 表示成功,非 0 则表示各种错误 | +| language | LanguageCode | 请求方实现的语言 | 应答方实现的语言 | +| version | int | 请求方程序的版本 | 应答方程序的版本 | +| opaque | int | 相当于 requestId,在同一个连接上的不同请求标识码,与响应消息中的相对应 | 应答不做修改直接返回 | +| flag | int | 区分是普通 RPC 还是 onewayRPC 的标志 | 区分是普通 RPC 还是 onewayRPC的标志 | +| remark | String | 传输自定义文本信息 | 传输自定义文本信息 | +| extFields | HashMap | 请求自定义扩展信息 | 响应自定义扩展信息 | + +![](https://seazean.oss-cn-beijing.aliyuncs.com/img/Frame/RocketMQ-消息协议.png) + +传输内容主要可以分为以下四部分: + +* 消息长度:总长度,四个字节存储,占用一个 int 类型 + +* 序列化类型&消息头长度:同样占用一个 int 类型,第一个字节表示序列化类型,后面三个字节表示消息头长度 + +* 消息头数据:经过序列化后的消息头数据 + +* 消息主体数据:消息主体的二进制字节数据内容 + + + +官方文档:https://github.com/apache/rocketmq/blob/master/docs/cn/design.md + + + +**** + + + +##### 处理方法 + +NettyServerHandler 类用来处理 Channel 上的事件,在 NettyRemotingServer 启动时注册到 Netty 中,可以处理 RemotingCommand 相关的数据,针对某一种类型的**请求处理** + +```java +class NettyServerHandler extends SimpleChannelInboundHandler { + @Override + protected void channelRead0(ChannelHandlerContext ctx, RemotingCommand msg) throws Exception { + // 服务器处理接受到的请求信息 + processMessageReceived(ctx, msg); + } +} +public void processMessageReceived(ChannelHandlerContext ctx, RemotingCommand msg) throws Exception { + final RemotingCommand cmd = msg; + if (cmd != null) { + // 根据请求的类型进行处理 + switch (cmd.getType()) { + case REQUEST_COMMAND:// 客户端发起的请求,走这里 + processRequestCommand(ctx, cmd); + break; + case RESPONSE_COMMAND:// 客户端响应的数据,走这里【当前类本身是服务器类也是客户端类】 + processResponseCommand(ctx, cmd); + break; + default: + break; + } + } +} +``` + +NettyRemotingAbstract#processRequestCommand:**处理请求的数据** + +* `matched = this.processorTable.get(cmd.getCode())`:根据业务请求码获取 Pair 对象,包含**处理器和线程池资源** + +* `pair = null == matched ? this.defaultRequestProcessor : matched`:未找到处理器则使用缺省处理器 + +* `int opaque = cmd.getOpaque()`:获取请求 ID + +* `Runnable run = new Runnable()`:创建任务对象,任务在提交到线程池后开始执行 + + * `doBeforeRpcHooks()`:RPC HOOK 前置处理 + + * `callback = new RemotingResponseCallback()`:**封装响应客户端的逻辑** + + * `doAfterRpcHooks()`:RPC HOOK 后置处理 + * `if (!cmd.isOnewayRPC())`:条件成立说明不是单向请求,需要结果 + * `response.setOpaque(opaque)`:将请求 ID 设置到 response + * `response.markResponseType()`:**设置当前请求是响应** + * `ctx.writeAndFlush(response)`: **将响应数据交给 Netty IO 线程,完成数据写和刷** + + * `if (pair.getObject1() instanceof AsyncNettyRequestProcessor)`:Nameserver 默认使用 DefaultRequestProcessor 处理器,是一个 AsyncNettyRequestProcessor 子类 + + * `processor = (AsyncNettyRequestProcessor)pair.getObject1()`:获取处理器 + + * `processor.asyncProcessRequest(ctx, cmd, callback)`:异步调用,首先 processRequest,然后 callback 响应客户端 + + `DefaultRequestProcessor.processRequest`:**根据业务码处理请求,执行对应的操作** + + `ClientRemotingProcessor.processRequest`:处理事务回查消息,或者回执消息,需要消费者回执一条消息给生产者 + +* `requestTask = new RequestTask(run, ctx.channel(), cmd)`:将任务对象、通道、请求封装成 RequestTask 对象 + +* `pair.getObject2().submit(requestTask)`:**获取处理器对应的线程池,将 task 提交,从 IO 线程切换到业务线程** + +NettyRemotingAbstract#processResponseCommand:**处理响应的数据** + +* `int opaque = cmd.getOpaque()`:获取请求 ID +* `responseFuture = responseTable.get(opaque)`:**从响应映射表中获取对应的对象** +* `responseFuture.setResponseCommand(cmd)`:设置响应的 Command 对象 +* `responseTable.remove(opaque)`:从映射表中移除对象,代表处理完成 +* `if (responseFuture.getInvokeCallback() != null)`:包含回调对象,异步执行回调对象 +* `responseFuture.putResponse(cmd)`:不包含回调对象,**同步调用时,唤醒等待的业务线程** + +流程:客户端 invokeSync → 服务器的 processRequestCommand → 客户端的 processResponseCommand → 结束 + + + +*** + + + +#### 路由信息 + +##### 信息管理 + +RouteInfoManager 类负责管理路由信息,NamesrvController 的构造方法中创建该类的实例对象,管理服务端的路由数据 + +```java +public class RouteInfoManager { + // Broker 两个小时不活跃,视为离线,被定时任务删除 + private final static long BROKER_CHANNEL_EXPIRED_TIME = 1000 * 60 * 2; + // 读写锁,保证线程安全 + private final ReadWriteLock lock = new ReentrantReadWriteLock(); + // 主题队列数据,一个主题对应多个队列 + private final HashMap> topicQueueTable; + // Broker 数据列表 + private final HashMap brokerAddrTable; + // 集群 + private final HashMap> clusterAddrTable; + // Broker 存活信息 + private final HashMap brokerLiveTable; + // 服务过滤 + private final HashMap/* Filter Server */> filterServerTable; +} +``` + + + +*** + + + +##### 路由注册 + +DefaultRequestProcessor REGISTER_BROKER 方法解析: + +```java +public RemotingCommand registerBroker(ChannelHandlerContext ctx, RemotingCommand request) { + // 创建响应请求的对象,设置为响应类型,【先设置响应的状态码时系统错误码】 + // 反射创建 RegisterBrokerResponseHeader 对象设置到 response.customHeader 属性中 + final RemotingCommand response = RemotingCommand.createResponseCommand(RegisterBrokerResponseHeader.class); + + // 获取出反射创建的 RegisterBrokerResponseHeader 用户自定义header对象。 + final RegisterBrokerResponseHeader responseHeader = (RegisterBrokerResponseHeader) response.readCustomHeader(); + + // 反射创建 RegisterBrokerRequestHeader 对象,并且将 request.extFields 中的数据写入到该对象中 + final RegisterBrokerRequestHeader requestHeader = request.decodeCommandCustomHeader(RegisterBrokerRequestHeader.class); + + // CRC 校验,计算请求中的 CRC 值和请求头中包含的是否一致 + if (!checksum(ctx, request, requestHeader)) { + response.setCode(ResponseCode.SYSTEM_ERROR); + response.setRemark("crc32 not match"); + return response; + } + + TopicConfigSerializeWrapper topicConfigWrapper; + if (request.getBody() != null) { + // 【解析请求体 body】,解码出来的数据就是当前机器的主题信息 + topicConfigWrapper = TopicConfigSerializeWrapper.decode(request.getBody(), TopicConfigSerializeWrapper.class); + } else { + topicConfigWrapper = new TopicConfigSerializeWrapper(); + topicConfigWrapper.getDataVersion().setCounter(new AtomicLong(0)); + topicConfigWrapper.getDataVersion().setTimestamp(0); + } + + // 注册方法 + // 参数1 集群、参数2:节点ip地址、参数3:brokerName、参数4:brokerId 注意brokerId=0的节点为主节点 + // 参数5:ha节点ip地址、参数6当前节点主题信息、参数7:过滤服务器列表、参数8:当前服务器和客户端通信的channel + RegisterBrokerResult result = this.namesrvController.getRouteInfoManager().registerBroker(..); + + // 将结果信息 写到 responseHeader 中 + responseHeader.setHaServerAddr(result.getHaServerAddr()); + responseHeader.setMasterAddr(result.getMasterAddr()); + // 获取 kv配置,写入 response body 中,【kv 配置是顺序消息相关的】 + byte[] jsonValue = this.namesrvController.getKvConfigManager().getKVListByNamespace(NamesrvUtil.NAMESPACE_ORDER_TOPIC); + response.setBody(jsonValue); + + // code 设置为 SUCCESS + response.setCode(ResponseCode.SUCCESS); + response.setRemark(null); + // 返回 response ,【返回的 response 由 callback 对象处理】 + return response; +} +``` + +RouteInfoManager#registerBroker:注册 Broker 的信息 + +* `RegisterBrokerResult result = new RegisterBrokerResult()`:返回结果的封装对象 + +* `this.lock.writeLock().lockInterruptibly()`:加写锁后**同步执行** + +* `brokerNames = this.clusterAddrTable.get(clusterName)`:获取当前集群上的 Broker 名称列表,是空就新建列表 + +* `brokerNames.add(brokerName)`:将当前 Broker 名字加入到集群列表 + +* `brokerData = this.brokerAddrTable.get(brokerName)`:获取当前 Broker 的 brokerData,是空就新建放入映射表 + +* `brokerAddrsMap = brokerData.getBrokerAddrs()`:获取当前 Broker 的物理节点 map 表,进行遍历,如果物理节点角色发生变化(slave → master),先将旧数据从物理节点 map 中移除,然后重写放入,**保证节点的唯一性** + +* `if (null != topicConfigWrapper && MixAll.MASTER_ID == brokerId)`:Broker 上的 Topic 不为 null,并且当前物理节点是 Broker 上的 master 节点 + + `tcTable = topicConfigWrapper.getTopicConfigTable()`:获取当前 Broker 信息中的主题映射表 + + `if (tcTable != null)`:映射表不空就加入或者更新到 Namesrv 内 + +* ` prevBrokerLiveInfo = this.brokerLiveTable.put(brokerAddr)`:添加**当前节点的 BrokerLiveInfo** ,返回上一次心跳时当前 Broker 节点的存活对象数据。**NamesrvController 中的定时任务会扫描映射表 brokerLiveTable** + + ```java + BrokerLiveInfo prevBrokerLiveInfo = this.brokerLiveTable.put(brokerAddr, new BrokerLiveInfo( + System.currentTimeMillis(),topicConfigWrapper.getDataVersion(), channel,haServerAddr)); + ``` + +* `if (MixAll.MASTER_ID != brokerId)`:当前 Broker 不是 master 节点,**获取主节点的信息**设置到结果对象 + +* `this.lock.writeLock().unlock()`:释放写锁 + + + + + +**** + + + +### Broker + +#### MappedFile + +##### 成员属性 + +MappedFile 类是最基础的存储类,继承自 ReferenceResource 类,用来**保证线程安全** + +MappedFile 类成员变量: + +* 内存相关: + + ```java + public static final int OS_PAGE_SIZE = 1024 * 4;// 内存页大小:默认是 4k + private AtomicLong TOTAL_MAPPED_VIRTUAL_MEMORY; // 当前进程下所有的 mappedFile 占用的总虚拟内存大小 + private AtomicInteger TOTAL_MAPPED_FILES; // 当前进程下所有的 mappedFile 个数 + ``` + +* 数据位点: + + ```java + protected final AtomicInteger wrotePosition; // 当前 mappedFile 的数据写入点 + protected final AtomicInteger committedPosition;// 当前 mappedFile 的数据提交点 + private final AtomicInteger flushedPosition; // 数据落盘位点,在这之前的数据是持久化的安全数据 + // flushedPosition-wrotePosition 之间的数据属于脏页 + ``` + +* 文件相关:CL 是 CommitLog,CQ 是 ConsumeQueue + + ```java + private String fileName; // 文件名称,CL和CQ文件名是【第一条消息的物理偏移量】,索引文件是【年月日时分秒】 + private long fileFromOffset;// 文件名转long,代表该对象的【起始偏移量】 + private File file; // 文件对象 + ``` + + **MF 中以物理偏移量作为文件名,可以更好的寻址和进行判断** + +* 内存映射: + + ```java + protected FileChannel fileChannel; // 文件通道 + private MappedByteBuffer mappedByteBuffer; // 内存映射缓冲区,访问虚拟内存 + ``` + +ReferenceResource 类成员变量: + +* 引用数量:当 `refCount <= 0` 时,表示该资源可以释放了,没有任何其他程序依赖它了,用原子类保证线程安全 + + ```java + protected final AtomicLong refCount = new AtomicLong(1); // 初始值为 1 + ``` + +* 存活状态:表示资源的存活状态 + + ```java + protected volatile boolean available = true; + ``` + +* 是否清理:默认值 false,当执行完子类对象的 cleanup() 清理方法后,该值置为 true ,表示资源已经全部释放 + + ```java + protected volatile boolean cleanupOver = false; + ``` + +* 第一次关闭资源的时间:用来记录超时时间 + + ```java + private volatile long firstShutdownTimestamp = 0; + ``` + + + +*** + + + +##### 成员方法 + +MappedFile 类核心方法: + +* appendMessage():提供上层向内存映射中追加消息的方法,消息如何追加由 AppendMessageCallback 控制 + + ```java + // 参数一:消息 参数二:追加消息回调 + public AppendMessageResult appendMessage(MessageExtBrokerInner msg, AppendMessageCallback cb) + ``` + + ```java + // 将字节数组写入到文件通道 + public boolean appendMessage(final byte[] data) + ``` + +* flush():刷盘接口,参数 flushLeastPages 代表刷盘的最小页数 ,等于 0 时属于强制刷盘;> 0 时需要脏页(计算方法在数据位点)达到该值才进行物理刷盘;文件写满时强制刷盘 + + ```java + public int flush(final int flushLeastPages) + ``` + +* selectMappedBuffer():该方法以 pos 为开始位点 ,到有效数据为止,创建一个切片 ByteBuffer 作为数据副本,供业务访问数据 + + ```java + public SelectMappedBufferResult selectMappedBuffer(int pos) + ``` + +* destroy():销毁映射文件对象,并删除关联的系统文件,参数是强制关闭资源的时间 + + ```java + public boolean destroy(final long intervalForcibly) + ``` + +* cleanup():**释放堆外内存**,更新总虚拟内存和总内存映射文件数 + + ```java + public boolean cleanup(final long currentRef) + ``` + +* warmMappedFile():内存预热,当要新建的 MappedFile 对象大于 1g 时执行该方法。mappedByteBuffer 已经通过mmap映射,此时操作系统中只是记录了该文件和该 Buffer 的映射关系,而并没有映射到物理内存中,对该 MappedFile 的每个 Page Cache 进行写入一个字节分配内存,**将映射文件全部加载到内存** + + ```java + public void warmMappedFile(FlushDiskType type, int pages) + ``` + +* mlock():锁住指定的内存区域避免被操作系统调到 swap 空间,减少了缺页异常的产生 + + ```java + public void mlock() + ``` + + swap space 是磁盘上的一块区域,可以是一个分区或者一个文件或者是组合。当系统物理内存不足时,Linux 会将内存中不常访问的数据保存到 swap 区域上,这样系统就可以有更多的物理内存为各个进程服务,而当系统需要访问 swap 上存储的内容时,需要通过**缺页中断**将 swap 上的数据加载到内存中 + +ReferenceResource 类核心方法: + +* hold():增加引用记数 refCount,方法加锁 + + ```java + public synchronized boolean hold() + ``` + +* shutdown():关闭资源,参数代表强制关闭资源的时间间隔 + + ```java + // 系统当前时间 - firstShutdownTimestamp 时间 > intervalForcibly 进行【强制关闭】 + public void shutdown(final long intervalForcibly) + ``` + +* release():引用计数减 1,当 refCount 为 0 时,调用子类的 cleanup 方法 + + ```java + public void release() + ``` + + + + + + +*** + + + +#### MapQueue + +##### 成员属性 + +MappedFileQueue 用来管理 MappedFile 文件 + +成员变量: + +* 管理目录:CommitLog 是 `../store/commitlog`, ConsumeQueue 是 `../store/xxx_topic/0` + + ```java + private final String storePath; + ``` + +* 文件属性: + + ```java + private final int mappedFileSize; // 目录下每个文件大小,CL文件默认 1g,CQ文件 默认 600w字节 + private final CopyOnWriteArrayList mappedFiles; //目录下的每个 mappedFile 都加入该集合 + ``` + +* 数据位点: + + ```java + private long flushedWhere = 0; // 目录的刷盘位点,值为 mf.fileName + mf.wrotePosition + private long committedWhere = 0; // 目录的提交位点 + ``` + +* 消息存储: + + ```java + private volatile long storeTimestamp = 0; // 当前目录下最后一条 msg 的存储时间 + ``` + +* 创建服务:新建 MappedFile 实例,继承自 ServiceThread 是一个任务对象,run 方法用来创建实例 + + ```java + private final AllocateMappedFileService allocateMappedFileService; + ``` + + + +*** + + + +##### 成员方法 + +核心方法: + +* load():Broker 启动时,加载本地磁盘数据,该方法读取 storePath 目录下的文件,创建 MappedFile 对象放入集合内 + + ```java + public boolean load() + ``` + +* getLastMappedFile():获取当前正在顺序写入的 MappedFile 对象,如果最后一个 MappedFile 写满了,或者不存在 MappedFile 对象,则创建新的 MappedFile + + ```java + // 参数一:文件起始偏移量;参数二:当list为空时,是否新建 MappedFile + public MappedFile getLastMappedFile(final long startOffset, boolean needCreate) + ``` + +* flush():根据 flushedWhere 属性查找合适的 MappedFile,调用该 MappedFile 的落盘方法,并更新全局的 flushedWhere + + ```java + //参数:0 表示强制刷新, > 0 脏页数据必须达到 flushLeastPages 才刷新 + public boolean flush(final int flushLeastPages) + ``` + +* findMappedFileByOffset():根据偏移量查询对象 + + ```java + public MappedFile findMappedFileByOffset(final long offset, final boolean returnFirstOnNotFound) + ``` + +* deleteExpiredFileByTime():CL 删除过期文件,根据文件的保留时长决定是否删除 + + ```java + // 参数一:过期时间; 参数二:删除两个文件之间的时间间隔; 参数三:mf.destory传递的参数; 参数四:true 强制删除 + public int deleteExpiredFileByTime(final long expiredTime,final int deleteFilesInterval, final long intervalForcibly, final boolean cleanImmediately) + ``` + +* deleteExpiredFileByOffset():CQ 删除过期文件,遍历每个 MF 文件,获取当前文件最后一个数据单元的物理偏移量,小于 offset 说明当前 MF 文件内都是过期数据 + + ```java + // 参数一:consumeLog 目录下最小物理偏移量,就是第一条消息的 offset; + // 参数二:ConsumerQueue 文件内每个数据单元固定大小 + public int deleteExpiredFileByOffset(long offset, int unitSize) + ``` + + + + + + +*** + + + +#### CommitLog + +##### 成员属性 + +成员变量: + +* 魔数: + + ```java + public final static int MESSAGE_MAGIC_CODE = -626843481; // 消息的第一个字段是大小,第二个字段就是魔数 + protected final static int BLANK_MAGIC_CODE = -875286124; // 文件尾消息的魔法值 + ``` + +* MappedFileQueue:用于管理 `../store/commitlog` 目录下的文件 + + ```java + protected final MappedFileQueue mappedFileQueue; + ``` + +* 存储服务: + + ```java + protected final DefaultMessageStore defaultMessageStore; // 存储模块对象,上层服务 + private final FlushCommitLogService flushCommitLogService; // 刷盘服务,默认实现是异步刷盘 + ``` + +* 回调器:控制消息的哪些字段添加到 MappedFile + + ```java + private final AppendMessageCallback appendMessageCallback; + ``` + +* 队列偏移量字典表:key 是主题队列 id,value 是偏移量 + + ```java + protected HashMap topicQueueTable = new HashMap(1024); + ``` + +* 锁相关: + + ```java + private volatile long beginTimeInLock = 0; // 写数据时加锁的开始时间 + protected final PutMessageLock putMessageLock; // 写锁,两个实现类:自旋锁和重入锁 + ``` + + 因为发送消息是需要持久化的,在 Broker 端持久化时会获取该锁,**保证发送的消息的线程安全** + +构造方法: + +* 有参构造: + + ```java + public CommitLog(final DefaultMessageStore defaultMessageStore) { + // 创建 MappedFileQueue 对象 + // 参数1:../store/commitlog; 参数2:【1g】; 参数3:allocateMappedFileService + this.mappedFileQueue = new MappedFileQueue(...); + // 默认 异步刷盘,创建这个对象 + this.flushCommitLogService = new FlushRealTimeService(); + // 控制消息哪些字段追加到 mappedFile,【消息最大是 4M】 + this.appendMessageCallback = new DefaultAppendMessageCallback(...); + // 默认使用自旋锁 + this.putMessageLock = ...; + } + ``` + + + +*** + + + +##### 成员方法 + +CommitLog 类核心方法: + +* start():会启动刷盘服务 + + ```java + public void start() + ``` + +* shutdown():关闭刷盘服务 + + ```java + public void shutdown() + ``` + +* load():加载 CommitLog 目录下的文件 + + ```java + public boolean load() + ``` + +* getMessage():根据 offset 查询单条信息,返回的结果对象内部封装了一个 ByteBuffer,该 Buffer 表示 `[offset, offset + size]` 区间的 MappedFile 的数据 + + ```java + public SelectMappedBufferResult getMessage(final long offset, final int size) + ``` + +* deleteExpiredFile():删除过期文件,方法由 DefaultMessageStore 的定时任务调用 + + ```java + public int deleteExpiredFile() + ``` + +* asyncPutMessage():**存储消息** + + ```java + public CompletableFuture asyncPutMessage(final MessageExtBrokerInner msg) + ``` + + * `msg.setStoreTimestamp(System.currentTimeMillis())`:设置存储时间,后面获取到写锁后这个事件会重写 + * `msg.setBodyCRC(UtilAll.crc32(msg.getBody()))`:获取消息的 CRC 值 + * `topic、queueId`:获取主题和队列 ID + * `if (msg.getDelayTimeLevel() > 0) `:**获取消息的延迟级别,这里是延迟消息实现的关键** + * `topic = TopicValidator.RMQ_SYS_SCHEDULE_TOPIC`:**修改消息的主题为 `SCHEDULE_TOPIC_XXXX`** + * `queueId = ScheduleMessageService.delayLevel2QueueId()`:**队列 ID 为延迟级别 -1** + * `MessageAccessor.putProperty`:**将原来的消息主题和 ID 存入消息的属性 `REAL_TOPIC` 中** + * `mappedFile = this.mappedFileQueue.getLastMappedFile()`:获取当前顺序写的 MappedFile 对象 + * `putMessageLock.lock()`:**获取写锁** + * `msg.setStoreTimestamp(beginLockTimestamp)`:设置消息的存储时间为获取锁的时间 + * `if (null == mappedFile || mappedFile.isFull())`:文件写满了创建新的 MF 对象 + * `result = mappedFile.appendMessage(msg, this.appendMessageCallback)`:**消息追加**,核心逻辑在回调器类 + * `putMessageLock.unlock()`:释放写锁 + * `this.defaultMessageStore.unlockMappedFile(..)`:将 MappedByteBuffer 从 lock 切换为 unlock 状态 + * `putMessageResult = new PutMessageResult(PutMessageStatus.PUT_OK, result)`:结果封装 + * `flushResultFuture = submitFlushRequest(result, msg)`:**唤醒刷盘线程** + * `replicaResultFuture = submitReplicaRequest(result, msg)`:HA 消息同步 + +* recoverNormally():正常关机时的恢复方法,存储模块启动时**先恢复所有的 ConsumeQueue 数据,再恢复 CommitLog 数据** + + ```java + // 参数表示恢复阶段 ConsumeQueue 中已知的最大的消息 offset + public void recoverNormally(long maxPhyOffsetOfConsumeQueue) + ``` + + * `int index = mappedFiles.size() - 3`:从倒数第三个 file 开始向后恢复 + + * `dispatchRequest = this.checkMessageAndReturnSize()`:每次从切片内解析出一条 msg 封装成 DispatchRequest 对象 + + * `size = dispatchRequest.getMsgSize()`:获取消息的大小,检查 DispatchRequest 对象的状态 + + 情况 1:正常数据,则 `mappedFileOffset += size` + + 情况 2:文件尾数据,处理下一个文件,mappedFileOffset 置为 0,magic_code 表示文件尾 + + * `processOffset += mappedFileOffset`:计算出正确的数据存储位点,并设置 MappedFileQueue 的目录刷盘位点 + + * `this.mappedFileQueue.truncateDirtyFiles(processOffset)`:调整 MFQ 中文件的刷盘位点 + + * `if (maxPhyOffsetOfConsumeQueue >= processOffset)`:删除冗余数据,将超过全局位点的 CQ 下的文件删除,将包含全局位点的 CQ 下的文件重新定位 + +* recoverAbnormally():异常关机时的恢复方法 + + ```java + public void recoverAbnormally(long maxPhyOffsetOfConsumeQueue) + ``` + + * `int index = mappedFiles.size() - 1`:从尾部开始遍历 MFQ,验证 MF 的第一条消息,找到第一个验证通过的文件对象 + * `dispatchRequest = this.checkMessageAndReturnSize()`:每次解析出一条 msg 封装成 DispatchRequest 对象 + * `this.defaultMessageStore.doDispatch(dispatchRequest)`:**重建 ConsumerQueue 和 Index,避免上次异常停机导致 CQ 和 Index 与 CommitLog 不对齐** + * 剩余逻辑与正常关机的恢复方法相似 + + + +*** + + + +##### 服务线程 + +AppendMessageCallback 消息追加服务实现类为 DefaultAppendMessageCallback + +* doAppend(): + + ```java + public AppendMessageResult doAppend() + ``` + + * `long wroteOffset = fileFromOffset + byteBuffer.position()`:消息写入的位置,物理偏移量 phyOffset + * `String msgId`:**消息 ID,规则是客户端 IP + 消息偏移量 phyOffset** + * `byte[] topicData`:序列化消息,将消息的字段压入到 msgStoreItemMemory 这个 Buffer 中 + * `byteBuffer.put(this.msgStoreItemMemory.array(), 0, msgLen)`:将 msgStoreItemMemory 中的数据写入 MF 对象的内存映射的 Buffer 中,数据还没落盘 + * `AppendMessageResult result`:构造结果对象,包括存储位点、是否成功、队列偏移量等信息 + * `CommitLog.this.topicQueueTable.put(key, ++queueOffset)`:更新队列偏移量 + +FlushRealTimeService 刷盘 CL 数据,默认是异步刷盘类 FlushRealTimeService + +* run():运行方法 + + ```java + public void run() + ``` + + * `while (!this.isStopped())`:stopped为 true 才跳出循环 + + * `boolean flushCommitLogTimed`:控制线程的休眠方式,默认是 false,使用 `CountDownLatch.await()` 休眠,设置为 true 时使用 `Thread.sleep()` 休眠 + + * `int interval`:获取配置中的刷盘时间间隔 + + * `int flushPhysicQueueLeastPages`:**获取最小刷盘页数,默认是 4 页**,脏页达到指定页数才刷盘 + + * `int flushPhysicQueueThoroughInterval`:获取强制刷盘周期,默认是 10 秒,达到周期后强制刷盘,不考虑脏页 + + * `if (flushCommitLogTimed)`:**休眠逻辑**,避免 CPU 占用太长时间,导致无法执行其他更紧急的任务 + + * `CommitLog.this.mappedFileQueue.flush(flushPhysicQueueLeastPages)`:**刷盘** + + * `for (int i = 0; i < RETRY_TIMES_OVER && !result; i++)`:stopped 停止标记为 true 时,需要确保所有的数据都已经刷盘,所以此处尝试 10 次强制刷盘, + + `result = CommitLog.this.mappedFileQueue.flush(0)`:**强制刷盘** + +同步刷盘类 GroupCommitService + +* run():运行方法 + + ```java + public void run() + ``` + + * `while (!this.isStopped())`:stopped为 true 才跳出循环 + + `this.waitForRunning(10)`:线程休眠 10 毫秒,最后调用 `onWaitEnd()` 进行**请求的交换** `swapRequests()` + + `this.doCommit()`:做提交逻辑 + + * `if (!this.requestsRead.isEmpty()) `:读请求集合不为空 + + `for (GroupCommitRequest req : this.requestsRead)`:遍历所有的读请求,请求中的属性: + + * `private final long nextOffset`:本条消息存储之后,下一条消息开始的 offset + * `private CompletableFuture flushOKFuture`:Future 对象 + + `boolean flushOK = ...`:当前请求关注的数据是否全部落盘,**落盘成功唤醒消费者线程** + + `for (int i = 0; i < 2 && !flushOK; i++)`:尝试进行两次强制刷盘,保证刷盘成功 + + `CommitLog.this.mappedFileQueue.flush(0)`:强制刷盘 + + `req.wakeupCustomer(flushOK ? ...)`:设置 Future 结果,在 Future 阻塞的线程在这里会被唤醒 + + `this.requestsRead.clear()`:清理 reqeustsRead 列表,方便交换时成为 requestsWrite 使用 + + * `else`:读请求集合为空 + + `CommitLog.this.mappedFileQueue.flush(0)`:强制刷盘 + + * `this.swapRequests()`:交换读写请求 + + * `this.doCommit()`:交换后做一次提交 + + + +**** + + + +#### ConsQueue + +##### 成员属性 + +ConsumerQueue 是消息消费队列,存储消息在 CommitLog 的索引,便于快速定位消息 + +成员变量: + +* 数据单元:ConsumerQueueData 数据单元的固定大小是 20 字节,默认申请 20 字节的缓冲区 + + ```java + public static final int CQ_STORE_UNIT_SIZE = 20; + ``` + +* 文件管理: + + ```java + private final MappedFileQueue mappedFileQueue; // 文件管理器,管理 CQ 目录下的文件 + private final String storePath; // 目录,比如../store/consumequeue/xxx_topic/0 + private final int mappedFileSize; // 每一个 CQ 存储文件大小,默认 20 * 30w = 600w byte + ``` + +* 存储主模块:上层的对象 + + ```java + private final DefaultMessageStore defaultMessageStore; + ``` + +* 消息属性: + + ```java + private final String topic; // CQ 主题 + private final int queueId; // CQ 队列,每一个队列都有一个 ConsumeQueue 对象进行管理 + private final ByteBuffer byteBufferIndex; // 临时缓冲区,插新的 CQData 时使用 + private long maxPhysicOffset = -1; // 当前ConsumeQueue内存储的最大消息物理偏移量 + private volatile long minLogicOffset = 0; // 当前ConsumeQueue内存储的最小消息物理偏移量 + ``` + +构造方法: + +* 有参构造: + + ```java + public ConsumeQueue() { + // 申请了一个 20 字节大小的 临时缓冲区 + this.byteBufferIndex = ByteBuffer.allocate(CQ_STORE_UNIT_SIZE); + } + ``` + + + +*** + + + +##### 成员方法 + +ConsumeQueue 启动阶段方法: + +* load():第一步,加载 storePath 目录下的文件,初始化 MappedFileQueue +* recover():第二步,恢复 ConsumeQueue 数据 + * 从倒数第三个 MF 文件开始向后遍历,依次读取 MF 中 20 个字节的 CQData 数据,检查 offset 和 size 是否是有效数据 + * 找到无效的 CQData 的位点,该位点就是 CQ 的刷盘点和数据顺序写入点 + * 删除无效的 MF 文件,调整当前顺序写的 MF 文件的数据位点 + +其他方法: + +* truncateDirtyLogicFiles():CommitLog 恢复阶段调用,将 ConsumeQueue 有效数据文件与 CommitLog 对齐,将超出部分的数据文删除掉,并调整当前文件的数据位点。Broker 启动阶段先恢复 CQ 的数据,再恢复 CL 数据,但是**数据要以 CL 为基准** + + ```java + // 参数是最大消息物理偏移量 + public void truncateDirtyLogicFiles(long phyOffet) + ``` + +* flush():刷盘,调用 MFQ 的刷盘方法 + + ```java + public boolean flush(final int flushLeastPages) + ``` + +* deleteExpiredFile():删除过期文件,将小于 offset 的所有 MF 文件删除,offset 是 CommitLog 目录下最小的物理偏移量,小于该值的 CL 文件已经没有了,所以 CQ 也没有存在的必要 + + ```java + public int deleteExpiredFile(long offset) + ``` + +* putMessagePositionInfoWrapper():**向 CQ 中追加 CQData 数据**,由存储主模块 DefaultMessageStore 内部的异步线程调用,负责构建 ConsumeQueue 文件和 Index 文件的,该线程会持续关注 CommitLog 文件,当 CommitLog 文件内有新数据写入,就读出来封装成 DispatchRequest 对象,转发给 ConsumeQueue 或者 IndexService + + ```java + public void putMessagePositionInfoWrapper(DispatchRequest request) + ``` + +* getIndexBuffer():转换 startIndex 为 offset,获取包含该 offset 的 MappedFile 文件,读取 `[offset%maxSize, mfPos]` 范围的数据,包装成结果对象返回 + + ```java + public SelectMappedBufferResult getIndexBuffer(final long startIndex) + ``` + + + +**** + + + +#### IndexFile + +##### 成员属性 + +IndexFile 类成员属性 + +* 哈希: + + ```java + private static int hashSlotSize = 4; // 每个 hash 桶的大小是 4 字节,【用来存放索引的编号】 + private final int hashSlotNum; // hash 桶的个数,默认 500 万 + ``` + +* 索引: + + ```java + private static int indexSize = 20; // 每个 index 条目的大小是 20 字节 + private static int invalidIndex = 0; // 无效索引编号:0 特殊值 + private final int indexNum; // 默认值:2000w + private final IndexHeader indexHeader; // 索引头 + ``` + +* 映射: + + ```java + private final MappedFile mappedFile; // 【索引文件使用的 MF 文件】 + private final FileChannel fileChannel; // 文件通道 + private final MappedByteBuffer mappedByteBuffer;// 从 MF 中获取的内存映射缓冲区 + ``` + +构造方法: + +* 有参构造 + + ```java + // endPhyOffset 上个索引文件 最后一条消息的 物理偏移量 + // endTimestamp 上个索引文件 最后一条消息的 存储时间 + public IndexFile(final String fileName, final int hashSlotNum, final int indexNum, + final long endPhyOffset, final long endTimestamp) throws IOException { + // 文件大小 40 + 500w * 4 + 2000w * 20 + int fileTotalSize = + IndexHeader.INDEX_HEADER_SIZE + (hashSlotNum * hashSlotSize) + (indexNum * indexSize); + // 创建 mf 对象,会在disk上创建文件 + this.mappedFile = new MappedFile(fileName, fileTotalSize); + // 创建 索引头对象,传递 索引文件mf 的切片数据 + this.indexHeader = new IndexHeader(byteBuffer); + //... + } + ``` + + + +**** + + + +##### 成员方法 + +IndexFile 类方法 + +* load():加载 IndexHeader + + ```java + public void load() + ``` + +* flush():MappedByteBuffer 内的数据强制落盘 + + ```java + public void flush() + ``` + +* isWriteFull():检查当前的 IndexFile 已写索引数是否 >= indexNum,达到该值则当前 IndexFile 不能继续追加 IndexData 了 + + ```java + public boolean isWriteFull() + ``` + +* destroy():删除文件时使用的方法 + + ```java + public boolean destroy(final long intervalForcibly) + ``` + +* putKey():添加索引数据,解决哈希冲突使用**头插法** + + ```java + // 参数一:消息的 key,uniq_key 或者 keys="aaa bbb ccc" 会分别为 aaa bbb ccc 创建索引 + // 参数二:消息的物理偏移量; 参数三:消息存储时间 + public boolean putKey(final String key, final long phyOffset, final long storeTimestamp) + ``` + + * `int slotPos = keyHash % this.hashSlotNum`:对 key 计算哈希后,取模得到对应的哈希槽 slot 下标,然后计算出哈希槽的存储位置 absSlotPos + * `int slotValue = this.mappedByteBuffer.getInt(absSlotPos)`:获取槽中的值,如果是无效值说明没有哈希冲突 + * `timeDiff = timeDiff / 1000`:计算当前 msg 存储时间减去索引文件内第一条消息存储时间的差值,转化为秒进行存储 + * `int absIndexPos`:计算当前索引数据存储的位置,开始填充索引数据到对应的位置 + * `this.mappedByteBuffer.putInt(absIndexPos + 4 + 8 + 4, slotValue)`:**hash 桶的原值,头插法** + * `this.mappedByteBuffer.putInt(absSlotPos, this.indexHeader...)`:在 slot 放入当前索引的索引编号 + * `if (this.indexHeader.getIndexCount() <= 1)`:索引文件插入的第一条数据,需要设置起始偏移量和存储时间 + * `if (invalidIndex == slotValue)`:没有哈希冲突,说明占用了一个新的 hash slot + * `this.indexHeader`:设置索引头的相关属性 + +* selectPhyOffset():从索引文件查询消息的物理偏移量 + + ```java + // 参数一:查询结果全部放到该list内; 参数二:查询key; 参数三:结果最大数限制; 参数四五:时间范围 + public void selectPhyOffset(final List phyOffsets, final String key, final int maxNum,final long begin, final long end, boolean lock) + ``` + + * `if (this.mappedFile.hold())`: MF 的引用记数 +1,查询期间 MF 资源**不能被释放** + * `int slotValue = this.mappedByteBuffer.getInt(absSlotPos)`:获取槽中的值,可能是无效值或者索引编号,如果是无效值说明查询未命中 + * `int absIndexPos`:计算出索引编号对应索引数据的开始位点 + * `this.mappedByteBuffer`:读取索引数据 + * `long timeRead = this.indexHeader.getBeginTimestamp() + timeDiff`:计算出准确的存储时间 + * `boolean timeMatched = (timeRead >= begin) && (timeRead <= end)`:时间范围的匹配 + * `phyOffsets.add(phyOffsetRead)`:将命中的消息索引的消息偏移量加入到 list 集合中 + * `nextIndexToRead = prevIndexRead`:遍历前驱节点 + + + +**** + + + +#### IndexServ + +##### 成员属性 + +IndexService 类用来管理 IndexFile 文件 + +成员变量: + +* 存储主模块: + + ```java + private final DefaultMessageStore defaultMessageStore; + ``` + +* 索引文件存储目录:`../store/index` + + ```java + private final String storePath; + ``` + +* 索引对象集合:目录下的每个文件都有一个 IndexFile 对象 + + ```java + private final ArrayList indexFileList = new ArrayList(); + ``` + +* 索引文件: + + ```java + private final int hashSlotNum; // 每个索引文件包含的 哈希桶数量 :500w + private final int indexNum; // 每个索引文件包含的 索引条目数量 :2000w + ``` + + + +*** + + + +##### 成员方法 + +* load():加载 storePath 目录下的文件,为每个文件创建一个 IndexFile 实例对象,并加载 IndexHeader 信息 + + ```java + public boolean load(final boolean lastExitOK) + ``` + +* deleteExpiredFile():删除过期索引文件 + + ```java + // 参数 offset 表示 CommitLog 内最早的消息的 phyOffset + public void deleteExpiredFile(long offset) + ``` + + * `this.readWriteLock.readLock().lock()`:加锁判断 + * `long endPhyOffset = this.indexFileList.get(0).getEndPhyOffset()`:获取目录中第一个文件的结束偏移量 + * `if (endPhyOffset < offset)`:索引目录内存在过期的索引文件,并且当前的 IndexFile 都是过期的数据 + * `for (int i = 0; i < (files.length - 1); i++)`:遍历文件列表,删除过期的文件 + +* buildIndex():存储主模块 DefaultMessageStore 内部的异步线程调用,构建 Index 数据 + + ```java + public void buildIndex(DispatchRequest req) + ``` + + * `indexFile = retryGetAndCreateIndexFile()`:获取或者创建顺序写的索引文件对象 + + * `buildKey(topic, req.getUniqKey())`:**构建索引 key,`topic + # + uniqKey`** + + * `indexFile = putKey()`:插入索引文件 + + * `if (keys != null && keys.length() > 0)`:消息存在自定义索引 keys + + `for (int i = 0; i < keyset.length; i++)`:遍历每个索引,为每个 key 调用一次 putKey + +* getAndCreateLastIndexFile():获取当前顺序写的 IndexFile,没有就创建 + + ```java + public IndexFile getAndCreateLastIndexFile() + ``` + + + +*** + + + +#### HAService + +##### HAService + +###### Service + +HAService 类成员变量: + +* 主节点属性: + + ```java + // master 节点当前有多少个 slave 节点与其进行数据同步 + private final AtomicInteger connectionCount = new AtomicInteger(0); + // master 节点会给每个发起连接的 slave 节点的通道创建一个 HAConnection,【控制 master 端向 slave 端传输数据】 + private final List connectionList = new LinkedList<>(); + // master 向 slave 节点推送的最大的 offset,表示数据同步的进度 + private final AtomicLong push2SlaveMaxOffset = new AtomicLong(0) + ``` + +* 内部类属性: + + ```java + // 封装了绑定服务器指定端口,监听 slave 的连接的逻辑,没有使用 Netty,使用了原生态的 NIO 去做 + private final AcceptSocketService acceptSocketService; + // 控制生产者线程阻塞等待的逻辑 + private final GroupTransferService groupTransferService; + // slave 节点的客户端对象,【slave 端才会正常运行该实例】 + private final HAClient haClient; + ``` + +* 线程通信对象: + + ```java + private final WaitNotifyObject waitNotifyObject = new WaitNotifyObject() + ``` + +成员方法: + +* start():启动高可用服务 + + ```java + public void start() throws Exception { + // 监听从节点 + this.acceptSocketService.beginAccept(); + // 启动监听服务 + this.acceptSocketService.start(); + // 启动转移服务 + this.groupTransferService.start(); + // 启动从节点客户端实例 + this.haClient.start(); + } + ``` + + + +**** + + + +###### Accept + +AcceptSocketService 类用于**监听从节点的连接**,创建 HAConnection 连接对象 + +成员变量: + +* 端口信息:Master 绑定监听的端口信息 + + ```java + private final SocketAddress socketAddressListen; + ``` + +* 服务端通道: + + ```java + private ServerSocketChannel serverSocketChannel; + ``` + +* 多路复用器: + + ```java + private Selector selector; + ``` + +成员方法: + +* beginAccept():开始监听连接,**NIO** 标准模板 + + ```java + public void beginAccept() + ``` + +* run():服务启动 + + ```java + public void run() + ``` + + * `this.selector.select(1000)`:多路复用器阻塞获取就绪的通道,最多等待 1 秒钟 + * `Set selected = this.selector.selectedKeys()`:获取选择器中所有注册的通道中已经就绪好的事件 + * `for (SelectionKey k : selected)`:遍历所有就绪的事件 + * `if ((k.readyOps() & SelectionKey.OP_ACCEPT) != 0)`:说明 `OP_ACCEPT` 事件就绪 + * `SocketChannel sc = ((ServerSocketChannel) k.channel()).accept()`:**获取到客户端连接的通道** + * `HAConnection conn = new HAConnection(HAService.this, sc)`:**为每个连接 master 服务器的 slave 创建连接对象** + * `conn.start()`:**启动 HAConnection 对象**,内部启动两个服务为读数据服务、写数据服务 + * `HAService.this.addConnection(conn)`:加入到 HAConnection 集合内 + + + +**** + + + +###### Group + +GroupTransferService 用来控制数据同步 + +成员方法: + +* doWaitTransfer():等待主从数据同步 + + ```java + private void doWaitTransfer() + ``` + + * `if (!this.requestsRead.isEmpty())`:读请求不为空 + * `boolean transferOK = HAService.this.push2SlaveMaxOffset... >= req.getNextOffset()`:**主从同步是否完成** + * `req.wakeupCustomer(transferOK ? ...)`:唤醒消费者 + * `this.requestsRead.clear()`:清空读请求 + +* swapRequests():交换读写请求 + + ```java + private void swapRequests() + ``` + + + + + +**** + + + +##### HAClient + +###### 成员属性 + +HAClient 是 slave 端运行的代码,用于**和 master 服务器建立长连接**,上报本地同步进度,消费服务器发来的 msg 数据 + +成员变量: + +* 缓冲区: + + ```java + private static final int READ_MAX_BUFFER_SIZE = 1024 * 1024 * 4; // 默认大小:4 MB + private ByteBuffer byteBufferRead = ByteBuffer.allocate(READ_MAX_BUFFER_SIZE); + private ByteBuffer byteBufferBackup = ByteBuffer.allocate(READ_MAX_BUFFER_SIZE); + ``` + +* 主节点地址:格式为 `ip:port` + + ```java + private final AtomicReference masterAddress = new AtomicReference<>() + ``` + +* NIO 属性: + + ```java + private final ByteBuffer reportOffset; // 通信使用NIO,所以消息使用块传输,上报 slave offset 使用 + private SocketChannel socketChannel; // 客户端与 master 的会话通道 + private Selector selector; // 多路复用器 + ``` + +* 通信时间:上次会话通信时间,用于控制 socketChannel 是否关闭的 + + ````java + private long lastWriteTimestamp = System.currentTimeMillis(); + ```` + +* 进度信息: + + ```java + private long currentReportedOffset = 0; // slave 当前的进度信息 + private int dispatchPosition = 0; // 控制 byteBufferRead position 指针 + ``` + + + +*** + + + +###### 成员方法 + +* run():启动方法 + + ```java + public void run() + ``` + + * `if (this.connectMaster())`:连接主节点,连接失败会休眠 5 秒 + + * `String addr = this.masterAddress.get()`:获取 master 暴露的 HA 地址端口信息 + * `this.socketChannel = RemotingUtil.connect(socketAddress)`:建立连接 + * `this.socketChannel.register(this.selector, SelectionKey.OP_READ)`:注册到多路复用器,**关注读事件** + * `this.currentReportedOffset`: 初始化上报进度字段为 slave 的 maxPhyOffset + + * `if (this.isTimeToReportOffset())`:slave 每 5 秒会上报一次 slave 端的同步进度信息给 master + + `boolean result = this.reportSlaveMaxOffset()`:**上报同步信息**,上报失败关闭连接 + + * `this.selector.select(1000)`:多路复用器阻塞获取就绪的通道,最多等待 1 秒钟,**获取到就绪事件或者超时后结束** + + * `boolean ok = this.processReadEvent()`:处理读事件 + + * `if (!reportSlaveMaxOffsetPlus())`:检查是否重新上报同步进度 + +* reportSlaveMaxOffset():上报 slave 同步进度 + + ```java + private boolean reportSlaveMaxOffset(final long maxOffset) + ``` + + * 首先向缓冲区写入 slave 端最大偏移量,写完以后切换为指定置为初始状态 + + * `for (int i = 0; i < 3 && this.reportOffset.hasRemaining(); i++)`:尝试三次写数据 + + `this.socketChannel.write(this.reportOffset)`:**写数据** + + * `return !this.reportOffset.hasRemaining()`:写成功之后 pos = limit + +* processReadEvent():处理 master 发送给 slave 数据,返回 true 表示处理成功 false 表示 Socket 处于半关闭状态,需要上层重建 haClient + + ```java + private boolean processReadEvent() + ``` + + * `int readSizeZeroTimes = 0`:控制 while 循环的一个条件变量,当值为 3 时跳出循环 + + * `while (this.byteBufferRead.hasRemaining())`:byteBufferRead 有空间可以去 Socket 读缓冲区加载数据 + + * `int readSize = this.socketChannel.read(this.byteBufferRead)`:**从通道读数据** + + * `if (readSize > 0)`:加载成功,有新数据 + + `readSizeZeroTimes = 0`:置为 0 + + `boolean result = this.dispatchReadRequest()`:处理数据的核心逻辑 + + * `else if (readSize == 0) `:连续无新数据 3 次,跳出循环 + + * `else`:readSize = -1 就表示 Socket 处于半关闭状态,对端已经关闭了 + +* dispatchReadRequest():**处理数据的核心逻辑**,master 与 slave 传输的数据格式 `{[phyOffset][size][data...]}`,phyOffset 表示数据区间的开始偏移量,data 代表数据块,最大 32kb,可能包含多条消息的数据 + + ```java + private boolean dispatchReadRequest() + ``` + + * `final int msgHeaderSize = 8 + 4`:协议头大小 12 + + * `int readSocketPos = this.byteBufferRead.position()`:记录缓冲区处理数据前的 pos 位点,用于恢复指针 + + * `int diff = ...`:当前 byteBufferRead 还剩多少 byte 未处理,每处理一条帧数据都会更新 dispatchPosition + + * `if (diff >= msgHeaderSize)`:缓冲区还有完整的协议头 header 数据 + + * `if (diff >= (msgHeaderSize + bodySize))`:说明**缓冲区内是包含当前帧的全部数据的**,开始处理帧数据 + + `HAService...appendToCommitLog(masterPhyOffset, bodyData)`:**存储数据到 CommitLog**,并构建 Index 和 CQ + + `this.byteBufferRead.position(readSocketPos)`:恢复 byteBufferRead 的 pos 指针 + + `this.dispatchPosition += msgHeaderSize + bodySize`:加一帧数据长度,处理下一条数据使用 + + `if (!reportSlaveMaxOffsetPlus())`:上报 slave 同步信息 + + * `if (!this.byteBufferRead.hasRemaining())`:缓冲区写满了,重新分配缓冲区 + +* reallocateByteBuffer():重新分配缓冲区 + + ```java + private void reallocateByteBuffer() + ``` + + * `int remain = READ_MAX_BUFFER_SIZE - this.dispatchPosition`:表示缓冲区尚未处理过的字节数量 + + * `if (remain > 0)`:条件成立,说明缓冲区**最后一帧数据是半包数据**,但是不能丢失数据 + + `this.byteBufferBackup.put(this.byteBufferRead)`:**将半包数据拷贝到 backup 缓冲区** + + * `this.swapByteBuffer()`:交换 backup 成为 read + + * `this.byteBufferRead.position(remain)`:设置 pos 为 remain ,后续加载数据 pos 从remain 开始向后移动 + + * `this.dispatchPosition = 0`:当前缓冲区交换之后,相当于是一个全新的 byteBuffer,所以分配指针归零 + + + +*** + + + +##### HAConn + +###### Connection + +HAConnection 类成员变量: + +* 会话通道:master 和 slave 之间通信的 SocketChannel + + ```java + private final SocketChannel socketChannel; + ``` + +* 客户端地址: + + ```java + private final String clientAddr; + ``` + +* 服务类: + + ```java + private WriteSocketService writeSocketService; // 写数据服务 + private ReadSocketService readSocketService; // 读数据服务 + ``` + +* 请求位点:在 slave 上报本地的进度之后被赋值,该值大于 0 后同步逻辑才会运行,master 如果不知道 slave 节点当前消息的存储进度,就无法给 slave 推送数据 + + ```java + private volatile long slaveRequestOffset = -1; + ``` + +* 应答位点: 保存最新的 slave 上报的 offset 信息,slaveAckOffset 之前的数据都可以认为 slave 已经同步完成 + + ```java + private volatile long slaveAckOffset = -1; + ``` + +核心方法: + +* 构造方法: + + ```java + public HAConnection(final HAService haService, final SocketChannel socketChannel) { + // 初始化一些东西 + // 设置 socket 读写缓冲区为 64kb 大小 + this.socketChannel.socket().setReceiveBufferSize(1024 * 64); + this.socketChannel.socket().setSendBufferSize(1024 * 64); + // 创建读写服务 + this.writeSocketService = new WriteSocketService(this.socketChannel); + this.readSocketService = new ReadSocketService(this.socketChannel); + // 自增 + this.haService.getConnectionCount().incrementAndGet(); + } + ``` + +* 启动方法: + + ```java + public void start() { + this.readSocketService.start(); + this.writeSocketService.start(); + } + ``` + + + +*** + + + +###### ReadSocket + +ReadSocketService 类是一个任务对象,slave 向 master 传输的帧格式为 `[long][long][long]`,上报的是 slave 本地的同步进度,同步进度是一个 long 值 + +成员变量: + +* 读缓冲: + + ```java + private static final int READ_MAX_BUFFER_SIZE = 1024 * 1024; // 默认大小 1MB + private final ByteBuffer byteBufferRead = ByteBuffer.allocate(READ_MAX_BUFFER_SIZE); + ``` + +* NIO 属性: + + ```java + private final Selector selector; // 多路复用器 + private final SocketChannel socketChannel; // master 与 slave 之间的会话 SocketChannel + ``` + +* 处理位点:缓冲区处理位点 + + ```java + private int processPosition = 0; + ``` + +* 上次读操作的时间: + + ```java + private volatile long lastReadTimestamp = System.currentTimeMillis(); + ``` + +核心方法: + +* 构造方法: + + ```java + public ReadSocketService(final SocketChannel socketChannel) + ``` + + * `this.socketChannel.register(this.selector, SelectionKey.OP_READ)`:通道注册到多路复用器,关注读事件 + * `this.setDaemon(true)`:设置为守护线程 + +* 运行方法: + + ```java + public void run() + ``` + + * `this.selector.select(1000)`:多路复用器阻塞获取就绪的通道,最多等待 1 秒钟,获取到就绪事件或者超时后结束 + + * `boolean ok = this.processReadEvent()`:**读数据的核心方法**,返回 true 表示处理成功 false 表示 Socket 处于半关闭状态,需要上层重建 HAConnection 对象 + + * `int readSizeZeroTimes = 0`:控制 while 循环,当连续从 Socket 读取失败 3 次(未加载到数据)跳出循环 + + * `if (!this.byteBufferRead.hasRemaining())`:byteBufferRead 已经全部使用完,需要清理数据并更新位点 + + * `while (this.byteBufferRead.hasRemaining())`:byteBufferRead 有空间可以去 Socket 读缓冲区加载数据 + + * `int readSize = this.socketChannel.read(this.byteBufferRead)`:**从通道读数据** + + * `if (readSize > 0)`:加载成功,有新数据 + + `if ((byteBufferRead.position() - processPosition) >= 8)`:缓冲区的可读数据最少包含一个数据帧 + + * `int pos = ...`:**获取可读帧数据中最后一个完整的帧数据的位点,后面的数据丢弃** + * `long readOffset = ...byteBufferRead.getLong(pos - 8)`:读取最后一帧数据,slave 端当前的同步进度信息 + + * `this.processPosition = pos`:更新处理位点 + * `HAConnection.this.slaveAckOffset = readOffset`:更新应答位点 + * `if (HAConnection.this.slaveRequestOffset < 0)`:条件成立**给 slaveRequestOffset 赋值** + * `HAConnection...notifyTransferSome(slaveAckOffset)`:**唤醒阻塞的生产者线程** + + * `else if (readSize == 0) `:读取 3 次无新数据跳出循环 + + * `else`:readSize = -1 就表示 Socket 处于半关闭状态,对端已经关闭了 + + * `if (interval > 20)`:超过 20 秒未发生通信,直接结束循环 + + + +*** + + + +###### WriteSocket + +WriteSocketService 类是一个任务对象,master 向 slave 传输的数据帧格式为 `{[phyOffset][size][data...]}{[phyOffset][size][data...]}` + +* phyOffset:数据区间的开始偏移量,并不表示某一条具体的消息,表示的数据块开始的偏移量位置 +* size:同步的数据块的大小 +* data:数据块,最大 32kb,可能包含多条消息的数据 + +成员变量: + +* 协议头: + + ```java + private final int headerSize = 8 + 4; // 协议头大小:12 + private final ByteBuffer byteBufferHeader; // 帧头缓冲区 + ``` + +* NIO 属性: + + ```java + private final Selector selector; // 多路复用器 + private final SocketChannel socketChannel; // master 与 slave 之间的会话 SocketChannel + ``` + +* 处理位点:下一次传输同步数据的位置信息,master 给当前 slave 同步的位点 + + ```java + private long nextTransferFromWhere = -1; + ``` + +* 上次写操作: + + ```java + private boolean lastWriteOver = true; // 上一轮数据是否传输完毕 + private long lastWriteTimestamp = System.currentTimeMillis(); // 上次写操作的时间 + ``` + +核心方法: + +* 构造方法: + + ```java + public WriteSocketService(final SocketChannel socketChannel) + ``` + + * `this.socketChannel.register(this.selector, SelectionKey.OP_WRITE)`:通道注册到多路复用器,关注写事件 + * `this.setDaemon(true)`:设置为守护线程 + +* 运行方法: + + ```java + public void run() + ``` + + * `this.selector.select(1000)`:多路复用器阻塞获取就绪的通道,最多等待 1 秒钟,获取到就绪事件或者超时后结束 + + * `if (-1 == HAConnection.this.slaveRequestOffset)`:**等待 slave 同步完数据** + + * `if (-1 == this.nextTransferFromWhere)`:条件成立,需要初始化该变量 + + `if (0 == HAConnection.this.slaveRequestOffset)`:slave 是一个全新节点,从正在顺序写的 MF 开始同步数据 + + `long masterOffset = ...`:获取 master 最大的 offset,并计算归属的 mappedFile 文件的开始 offset + + `this.nextTransferFromWhere = masterOffset`:**赋值给下一次传输同步数据的位置信息** + + `this.nextTransferFromWhere = HAConnection.this.slaveRequestOffset`:大部分情况走这个赋值逻辑 + + * `if (this.lastWriteOver)`:上一次待发送数据全部发送完成 + + `if (interval > 5)`:**超过 5 秒未同步数据,发送一个 header 心跳数据包,维持长连接** + + * `else`:上一轮的待发送数据未全部发送,需要同步数据到 slave 节点 + + * `SelectMappedBufferResult selectResult`:**到 CommitLog 中查询 nextTransferFromWhere 开始位置的数据** + + * `if (size > 32k)`:一次最多同步 32k 数据 + + * `this.nextTransferFromWhere += size`:增加 size,下一轮传输跳过本帧数据 + + * `selectResult.getByteBuffer().limit(size)`:设置 byteBuffer 可访问数据区间为 [pos, size] + + * `this.selectMappedBufferResult = selectResult`:**待发送的数据** + + * `this.byteBufferHeader.put`:**构建帧头数据** + + * `this.lastWriteOver = this.transferData()`:处理数据,返回是否处理完成 + +* 同步方法:**同步数据到 slave 节点**,返回 true 表示本轮数据全部同步完成,false 表示本轮同步未完成(Header 和 Body 其中一个未同步完成都会返回 false) + + ```java + private boolean transferData() + ``` + + * `int writeSizeZeroTimes= 0`:控制 while 循环,当写失败连续 3 次时,跳出循环)跳出循环 + + * `while (this.byteBufferHeader.hasRemaining())`:**帧头数据缓冲区有待发送的数据** + + * `int writeSize = this.socketChannel.write(this.byteBufferHeader)`:向通道写帧头数据 + + * `if (null == this.selectMappedBufferResult)`:说明是心跳数据,返回心跳数据是否发送完成 + + * `if (!this.byteBufferHeader.hasRemaining())`:**Header写成功之后,才进行写 Body** + + * `while (this.selectMappedBufferResult.getByteBuffer().hasRemaining())`:**数据缓冲区有待发送的数据** + + * `int writeSize = this.socketChannel.write(this.selectMappedBufferResult...)`:向通道写帧头数据 + + * `if (writeSize > 0)`:写数据成功,但是不代表 SMBR 中的数据全部写完成 + + * `boolean result`:判断是否发送完成,返回该值 + + + + + +**** + + + +#### MesStore + +##### 生命周期 + +DefaultMessageStore 类核心是整个存储服务的调度类 + +* 构造方法: + + ```java + public DefaultMessageStore() + ``` + + * `this.allocateMappedFileService.start()`:启动**创建 MappedFile 文件服务** + * `this.indexService.start()`:启动索引服务 + +* load():先加载 CommitLog,再加载 ConsumeQueue,最后加载 IndexFile,加载完进入恢复阶段,先恢复 CQ,在恢复 CL + + ```java + public boolean load() + ``` + +* start():核心启动方法 + + ```java + public void start() + ``` + + * `lock = lockFile.getChannel().tryLock(0, 1, false)`:获取文件锁,获取失败说明当前目录已经启动过 Broker + + * `long maxPhysicalPosInLogicQueue = commitLog.getMinOffset()`:遍历全部的 CQ 对象,获取 CQ 中消息的最大偏移量 + + * `this.reputMessageService.start()`:设置分发服务的分发位点,启动**分发服务**,构建 ConsumerQueue 和 IndexFile + + * `if (dispatchBehindBytes() <= 0)`:线程等待分发服务将分发数据全部处理完毕 + + * `this.recoverTopicQueueTable()`:因为修改了 CQ 数据,所以再次构建队列偏移量字段表 + + * `this.haService.start()`:启动 **HA 服务** + + * `this.handleScheduleMessageService()`:启动**消息调度服务** + + * `this.flushConsumeQueueService.start()`:启动 CQ **消费队列刷盘服务** + + * `this.commitLog.start()`:启动 **CL 刷盘服务** + + * `this.storeStatsService.start()`:启动状态存储服务 + + * `this.createTempFile()`:创建 AbortFile,正常关机时 JVM HOOK 会删除该文件,**异常宕机时该文件不会删除**,开机数据恢复阶段根据是否存在该文件,执行不同的恢复策略 + + * `this.addScheduleTask()`:添加定时任务 + + * `DefaultMessageStore.this.cleanFilesPeriodically()`:**定时清理过期文件**,周期是 10 秒 + + * `this.cleanCommitLogService.run()`:启动清理过期的 CL 文件服务 + * `this.cleanConsumeQueueService.run()`:启动清理过期的 CQ 文件服务 + + * `DefaultMessageStore.this.checkSelf()`:每 10 分种进行健康检查 + + * `DefaultMessageStore.this.cleanCommitLogService.isSpaceFull()`:**磁盘预警定时任务**,每 10 秒一次 + + * `if (physicRatio > this.diskSpaceWarningLevelRatio)`:检查磁盘是否到达 waring 阈值,默认 90% + + `boolean diskok = ...runningFlags.getAndMakeDiskFull()`:设置磁盘写满标记 + + * `boolean diskok = ...this.runningFlags.getAndMakeDiskOK()`:设置磁盘可写标记 + + * `this.shutdown = false`:刚启动,设置为 false + +* shutdown():关闭各种服务和线程资源,设置存储模块状态为关闭状态 + + ```java + public void shutdown() + ``` + +* destroy():销毁 Broker 的工作目录 + + ```java + public void destroy() + ``` + + + + + +*** + + + +##### 服务线程 + +ServiceThread 类被很多服务继承,本身是一个 Runnable 任务对象,继承者通过重写 run 方法来实现服务的逻辑 + +* run():一般实现方式 + + ```java + public void run() { + while (!this.isStopped()) { + // 业务逻辑 + } + } + ``` + + 通过参数 stopped 控制服务的停止,使用 volatile 修饰保证可见性 + + ```java + protected volatile boolean stopped = false + ``` + +* shutdown():停止线程,首先设置 stopped 为 true,然后进行唤醒,默认不直接打断线程 + + ```java + public void shutdown() + ``` + +* waitForRunning():挂起线程,设置唤醒标记 hasNotified 为 false + + ```java + protected void waitForRunning(long interval) + ``` + +* wakeup():唤醒线程,设置 hasNotified 为 true + + ```java + public void wakeup() + ``` + + + +*** + + + +##### 构建服务 + +AllocateMappedFileService **创建 MappedFile 服务** + +* mmapOperation():核心服务 + + ```java + private boolean mmapOperation() + ``` + + * `req = this.requestQueue.take()`: **从 requestQueue 阻塞队列(优先级)中获取 AllocateRequest 任务** + * `if (...isTransientStorePoolEnable())`:条件成立使用直接内存写入数据, 从直接内存中 commit 到 FileChannel 中 + * `mappedFile = new MappedFile(req.getFilePath(), req.getFileSize())`:根据请求的路径和大小创建对象 + * `mappedFile.warmMappedFile()`:判断 mappedFile 大小,只有 CommitLog 才进行文件预热 + * `req.setMappedFile(mappedFile)`:将创建好的 MF 对象的赋值给请求对象的成员属性 + * `req.getCountDownLatch().countDown()`:**唤醒请求的阻塞线程** + +* putRequestAndReturnMappedFile():MappedFileQueue 中用来创建 MF 对象的方法 + + ```java + public MappedFile putRequestAndReturnMappedFile(String nextFilePath, String nextNextFilePath, int fileSize) + ``` + + * `AllocateRequest nextReq = new AllocateRequest(...)`:创建 nextFilePath 的 AllocateRequest 对象,放入请求列表和阻塞队列,然后创建 nextNextFilePath 的 AllocateRequest 对象,放入请求列表和阻塞队列 + * `AllocateRequest result = this.requestTable.get(nextFilePath)`:从请求列表获取 nextFilePath 的请求对象 + * `result.getCountDownLatch().await(...)`:**线程挂起**,直到超时或者 nextFilePath 对应的 MF 文件创建完成 + * `return result.getMappedFile()`:返回创建好的 MF 文件对象 + +ReputMessageService 消息分发服务,用于构**建 ConsumerQueue 和 IndexFile 文件** + +* run():**循环执行 doReput 方法**,**所以发送的消息存储进 CL 就可以产生对应的 CQ**,每执行一次线程休眠 1 毫秒 + + ```java + public void run() + ``` + +* doReput():实现分发的核心逻辑 + + ```java + private void doReput() + ``` + + * `for (boolean doNext = true; this.isCommitLogAvailable() && doNext; )`:循环遍历 + * `SelectMappedBufferResult result`: 从 CommitLog 拉取数据,数据范围 `[reputFromOffset, 包含该偏移量的 MF 的最大 Pos]`,封装成结果对象 + * `DispatchRequest dispatchRequest`:从结果对象读取出一条 DispatchRequest 数据 + * `DefaultMessageStore.this.doDispatch(dispatchRequest)`:将数据交给分发器进行分发,用于**构建 CQ 和索引文件** + * `this.reputFromOffset += size`:更新数据范围 + + + +*** + + + +##### 刷盘服务 + +FlushConsumeQueueService 刷盘 CQ 数据 + +* run():每隔 1 秒执行一次刷盘服务,跳出循环后还会执行一次强制刷盘 + + ```java + public void run() + ``` + +* doFlush():刷盘 + + ```java + private void doFlush(int retryTimes) + ``` + + * `int flushConsumeQueueLeastPages`:脏页阈值,默认是 2 + + * `if (retryTimes == RETRY_TIMES_OVER)`:**重试次数是 3** 时设置强制刷盘,设置脏页阈值为 0 + * `int flushConsumeQueueThoroughInterval`:两次刷新的**时间间隔超过 60 秒**会强制刷盘 + * `for (ConsumeQueue cq : maps.values())`:遍历所有的 CQ,进行刷盘 + * `DefaultMessageStore.this.getStoreCheckpoint().flush()`:强制刷盘时将 StoreCheckpoint 瞬时数据刷盘 + +FlushCommitLogService 刷盘 CL 数据,默认是异步刷盘 + +* run():运行方法 + + ```java + public void run() + ``` + + * `while (!this.isStopped())`:stopped为 true 才跳出循环 + + * `boolean flushCommitLogTimed`:控制线程的休眠方式,默认是 false,使用 `CountDownLatch.await()` 休眠,设置为 true 时使用 `Thread.sleep()` 休眠 + + * `int interval`:获取配置中的刷盘时间间隔 + + * `int flushPhysicQueueLeastPages`:获取最小刷盘页数,默认是 4 页,脏页达到指定页数才刷盘 + + * `int flushPhysicQueueThoroughInterval`:获取强制刷盘周期,默认是 10 秒,达到周期后强制刷盘,不考虑脏页 + + * `if (flushCommitLogTimed)`:休眠逻辑,避免 CPU 占用太长时间,导致无法执行其他更紧急的任务 + + * `CommitLog.this.mappedFileQueue.flush(flushPhysicQueueLeastPages)`:**刷盘** + + * `for (int i = 0; i < RETRY_TIMES_OVER && !result; i++)`:stopped 停止标记为 true 时,需要确保所有的数据都已经刷盘,所以此处尝试 10 次强制刷盘, + + `result = CommitLog.this.mappedFileQueue.flush(0)`:**强制刷盘** + + + +*** + + + +##### 清理服务 + +CleanCommitLogService 清理过期的 CL 数据,定时任务 10 秒调用一次,**先清理 CL,再清理 CQ**,因为 CQ 依赖于 CL 的数据 + +* run():运行方法 + + ```java + public void run() + ``` + +* deleteExpiredFiles():删除过期 CL 文件 + + ```java + private void deleteExpiredFiles() + ``` + + * `long fileReservedTime`:默认 72,代表文件的保留时间 + * `boolean timeup = this.isTimeToDelete()`:当前时间是否是凌晨 4 点 + * `boolean spacefull = this.isSpaceToDelete()`:CL 或者 CQ 的目录磁盘使用率达到阈值标准 85% + * `boolean manualDelete = this.manualDeleteFileSeveralTimes > 0`:手动删除文件 + * `fileReservedTime *= 60 * 60 * 1000`:默认保留 72 小时 + * `deleteCount = DefaultMessageStore.this.commitLog.deleteExpiredFile()`:**调用 MFQ 对象的删除方法** + +CleanConsumeQueueService 清理过期的 CQ 数据 + +* run():运行方法 + + ```java + public void run() + ``` + +* deleteExpiredFiles():删除过期 CQ 文件 + + ```java + private void deleteExpiredFiles() + ``` + + * `int deleteLogicsFilesInterval`:清理 CQ 的时间间隔,默认 100 毫秒 + * `long minOffset = DefaultMessageStore.this.commitLog.getMinOffset()`:获取 CL 文件中最小的物理偏移量 + * `if (minOffset > this.lastPhysicalMinOffset)`:CL 最小的偏移量大于 CQ 最小的,说明有过期数据 + * `this.lastPhysicalMinOffset = minOffset`:更新 CQ 的最小偏移量 + * `for (ConsumeQueue logic : maps.values())`:遍历所有的 CQ 文件 + * `logic.deleteExpiredFile(minOffset)`:**调用 MFQ 对象的删除方法** + * `DefaultMessageStore.this.indexService.deleteExpiredFile(minOffset)`:**删除过期的索引文件** + + + +*** + + + +##### 获取消息 + +DefaultMessageStore#getMessage 用于获取消息,在 PullMessageProcessor#processRequest 方法中被调用 (提示:建议学习消费者源码时再阅读) + +```java +// offset: 客户端拉消息使用位点; maxMsgNums: 32; messageFilter: 一般这里是 tagCode 过滤 +public GetMessageResult getMessage(final String group, final String topic, final int queueId, final long offset, final int maxMsgNums, final MessageFilter messageFilter) +``` + +* `if (this.shutdown)`:检查运行状态 + +* `GetMessageResult getResult`:创建查询结果对象 + +* `final long maxOffsetPy = this.commitLog.getMaxOffset()`:**获取 CommitLog 最大物理偏移量** + +* `ConsumeQueue consumeQueue = findConsumeQueue(topic, queueId)`:根据主题和队列 ID 获取 ConsumeQueue对象 + +* `minOffset, maxOffset`:获取当前 ConsumeQueue 的最小 offset 和 最大 offset,**判断是否满足本次 Pull 的 offset** + + `if (maxOffset == 0)`:说明队列内无数据,设置状态为 NO_MESSAGE_IN_QUEUE,外层进行长轮询 + + `else if (offset < minOffset)`:说明 offset 太小了,设置状态为 OFFSET_TOO_SMALL + + `else if (offset == maxOffset)`:消费进度持平,设置状态为 OFFSET_OVERFLOW_ONE,外层进行长轮询 + + `else if (offset > maxOffset)`:说明 offset 越界了,设置状态为 OFFSET_OVERFLOW_BADLY + +* `SelectMappedBufferResult bufferConsumeQueue`:查询 CQData **获取包含该 offset 的 MappedFile 文件**,如果该文件不是顺序写的文件,就读取 `[offset%maxSize, 文件尾]` 范围的数据,反之读取 `[offset%maxSize, 文件名+wrotePosition尾]` + + 先查 CQ 的原因:因为 CQ 时 CL 的索引,通过 CQ 查询 CL 更加快捷 + +* `if (bufferConsumeQueue != null)`:只有再 CQ 删除过期数据的逻辑执行时,条件才不成立,一般都是成立的 + +* `long nextPhyFileStartOffset = Long.MIN_VALUE`:下一个 commitLog 物理文件名,初始值为最小值 + +* `long maxPhyOffsetPulling = 0`:本次拉消息最后一条消息的物理偏移量 + +* `for ()`:**处理数据**,每次处理 20 字节处理字节数大于 16000 时跳出循环 + +* `offsetPy, sizePy, tagsCode`:读取 20 个字节后,获取消息物理偏移量、消息大小、消息 tagCode + +* `boolean isInDisk = checkInDiskByCommitOffset(...)`:**检查消息是热数据还是冷数据**,false 为热数据 + + * `long memory`:Broker 系统 40% 内存的字节数,写数据时内存不够会使用 LRU 算法淘汰数据,将淘汰数据持久化到磁盘 + * `return (maxOffsetPy - offsetPy) > memory`:返回 true 说明数据已经持久化到磁盘,为冷数据 + +* `if (this.isTheBatchFull())`:**控制是否跳出循环** + + * `if (0 == bufferTotal || 0 == messageTotal)`:本次 pull 消息未拉取到任何东西,需要外层 for 循环继续,返回 false + + * `if (maxMsgNums <= messageTotal)`:结果对象内消息数已经超过了最大消息数量,可以结束循环了 + + * `if (isInDisk)`:冷数据 + + `if ((bufferTotal + sizePy) > ...)`:冷数据一次 pull 请求最大允许获取 64kb 的消息 + + `if (messageTotal > ...)`:冷数据一次 pull 请求最大允许获取8 条消息 + + * `else`:热数据 + + `if ((bufferTotal + sizePy) > ...)`:热数据一次 pull 请求最大允许获取 256kb 的消息 + + `if (messageTotal > ...)`:冷数据一次 pull 请求最大允许获取 32 条消息 + +* `if (messageFilter != null)`:按照消息 tagCode 进行过滤 + +* `selectResult = this.commitLog.getMessage(offsetPy, sizePy)`:根据 CQ 消息物理偏移量和消息大小**到 commitLog 中查询这条 msg** + +* `if (null == selectResult)`:条件成立说明 commitLog 执行了删除过期文件的定时任务,因为是先清理的 CL,所以 CQ 还有该索引数据 + +* `nextPhyFileStartOffset = this.commitLog.rollNextFile(offsetPy)`:获取包含该 offsetPy 的下一个数据文件的文件名 + +* `getResult.addMessage(selectResult)`:**将本次循环查询出来的 msg 加入到 getResult 内** + +* `status = GetMessageStatus.FOUND`:查询状态设置为 FOUND + +* `nextPhyFileStartOffset = Long.MIN_VALUE`:设置为最小值,跳过期 CQData 数据的逻辑 + +* `nextBeginOffset = offset + (i / ConsumeQueue.CQ_STORE_UNIT_SIZE)`:计算客户端下一次 pull 时使用的位点信息 + +* `getResult.setSuggestPullingFromSlave(diff > memory)`:**选择主从节点的建议** + + * `diff > memory => true`:表示本轮查询最后一条消息为冷数据,Broker 建议客户端下一次 pull 时到 slave 节点 + * `diff > memory => false`:表示本轮查询最后一条消息为热数据,Broker 建议客户端下一次 pull 时到 master 节点 + +* `getResult.setStatus(status)`:设置结果状态 + +* `getResult.setNextBeginOffset(nextBeginOffset)`:设置客户端下一次 pull 时的 offset + +* `getResult.setMaxOffset(maxOffset)`:设置 queue 的最大 offset 和最小 offset + +* `return getResult`:返回结果对象 + + + +*** + + + +#### Broker + +BrokerStartup 启动方法 + +```java +public static void main(String[] args) { + start(createBrokerController(args)); +} +public static BrokerController start(BrokerController controller) { + controller.start(); // 启动 +} +``` + +BrokerStartup#createBrokerController:构造控制器,并初始化 + +* `final BrokerController controller()`:创建实例对象 +* `boolean initResult = controller.initialize()`:控制器初始化 + * `this.registerProcessor()`:**注册了处理器,包括发送消息、拉取消息、查询消息等核心处理器** + * `initialTransaction()`:初始化了事务服务,用于进行**事务回查** + +BrokerController#start:核心启动方法 + +* `this.messageStore.start()`:**启动存储服务** + +* `this.remotingServer.start()`:启动 Netty 通信服务 + +* `this.fileWatchService.start()`:启动文件监听服务 + +* `startProcessorByHa(messageStoreConfig.getBrokerRole())`:**启动事务回查** + +* `this.scheduledExecutorService.scheduleAtFixedRate()`:每隔 30s 向 NameServer 上报 Topic 路由信息,**心跳机制** + + `BrokerController.this.registerBrokerAll(true, false, brokerConfig.isForceRegister())` + + + +**** + + + +### Producer + +#### 生产者类 + +##### 生产者类 + +DefaultMQProducer 是生产者的默认实现类 + +成员变量: + +* 生产者实现类: + + ```java + protected final transient DefaultMQProducerImpl defaultMQProducerImpl + ``` + +* 生产者组:发送事务消息,Broker 端进行事务回查(补偿机制)时,选择当前生产者组的下一个生产者进行事务回查 + + ```java + private String producerGroup; + ``` + +* 默认主题:isAutoCreateTopicEnable 开启时,当发送消息指定的 Topic 在 Namesrv 未找到路由信息,使用该值创建 Topic 信息 + + ```java + private String createTopicKey = TopicValidator.AUTO_CREATE_TOPIC_KEY_TOPIC; + // 值为【TBW102】,Just for testing or demo program + ``` + +* 消息重投:系统特性消息重试部分详解了三个参数的作用 + + ```java + private int retryTimesWhenSendFailed = 2; // 同步发送失败后重试的发送次数,加上第一次发送,一共三次 + private int retryTimesWhenSendAsyncFailed = 2; // 异步 + private boolean retryAnotherBrokerWhenNotStoreOK = false; // 消息未存储成功,选择其他 Broker 重试 + ``` + +* 消息队列: + + ```java + private volatile int defaultTopicQueueNums = 4; // 默认 Broker 创建的队列数 + ``` + +* 消息属性: + + ```java + private int sendMsgTimeout = 3000; // 发送消息的超时限制 + private int compressMsgBodyOverHowmuch = 1024 * 4; // 压缩阈值,当 msg body 超过 4k 后使用压缩 + private int maxMessageSize = 1024 * 1024 * 4; // 消息体的最大限制,默认 4M + private TraceDispatcher traceDispatcher = null; // 消息轨迹 + +构造方法: + +* 构造方法: + + ```java + public DefaultMQProducer(final String namespace, final String producerGroup, RPCHook rpcHook) { + this.namespace = namespace; + this.producerGroup = producerGroup; + // 创建生产者实现对象 + defaultMQProducerImpl = new DefaultMQProducerImpl(this, rpcHook); + } + ``` + +成员方法: + +* start():启动方法 + + ```java + public void start() throws MQClientException { + // 重置生产者组名,如果传递了命名空间,则 【namespace%group】 + this.setProducerGroup(withNamespace(this.producerGroup)); + // 生产者实现对象启动 + this.defaultMQProducerImpl.start(); + if (null != traceDispatcher) { + // 消息轨迹的逻辑 + traceDispatcher.start(this.getNamesrvAddr(), this.getAccessChannel()); + } + } + ``` + +* send():**发送消息**: + + ```java + public SendResult send(Message msg){ + // 校验消息 + Validators.checkMessage(msg, this); + // 设置消息 Topic + msg.setTopic(withNamespace(msg.getTopic())); + return this.defaultMQProducerImpl.send(msg); + } + ``` + +* request():请求方法,**需要消费者回执消息** + + ```java + public Message request(final Message msg, final MessageQueue mq, final long timeout) { + msg.setTopic(withNamespace(msg.getTopic())); + return this.defaultMQProducerImpl.request(msg, mq, timeout); + } + ``` + + + + +*** + + + +##### 实现者类 + +DefaultMQProducerImpl 类是默认的生产者实现类 + +成员变量: + +* 实例对象: + + ```java + private final DefaultMQProducer defaultMQProducer; // 持有默认生产者对象,用来获取对象中的配置信息 + private MQClientInstance mQClientFactory; // 客户端实例对象,生产者启动后需要注册到该客户端对象内 + ``` + +* 主题发布信息映射表:key 是 Topic,value 是发布信息 + + ```java + private final ConcurrentMap topicPublishInfoTable = new ConcurrentHashMap(); + ``` + +* 异步发送消息:相关信息 + + ```java + private final BlockingQueue asyncSenderThreadPoolQueue;// 异步发送消息,异步线程池使用的队列 + private final ExecutorService defaultAsyncSenderExecutor; // 异步发送消息默认使用的线程池 + private ExecutorService asyncSenderExecutor; // 异步消息发送线程池,指定后就不使用默认线程池了 + ``` + +* 定时器:执行定时任务 + + ```java + private final Timer timer = new Timer("RequestHouseKeepingService", true); // 守护线程 + ``` + +* 状态信息:服务的状态,默认创建状态 + + ```java + private ServiceState serviceState = ServiceState.CREATE_JUST; + ``` + +* 压缩等级:ZIP 压缩算法的等级,默认是 5,越高压缩效果好,但是压缩的更慢 + + ```java + private int zipCompressLevel = Integer.parseInt(System.getProperty..., "5")); + ``` + +* 容错策略:选择队列的容错策略 + + ```java + private MQFaultStrategy mqFaultStrategy = new MQFaultStrategy(); + ``` + +* 钩子:用来进行前置或者后置处理 + + ```java + ArrayList sendMessageHookList; // 发送消息的钩子,留给用户扩展使用 + ArrayList checkForbiddenHookList; // 对比上面的钩子,可以抛异常,控制消息是否可以发送 + private final RPCHook rpcHook; // 传递给 NettyRemotingClient + ``` + +构造方法: + +* 默认构造: + + ```java + public DefaultMQProducerImpl(final DefaultMQProducer defaultMQProducer) { + // 默认 RPC HOOK 是空 + this(defaultMQProducer, null); + } + ``` + +* 有参构造: + + ```java + public DefaultMQProducerImpl(final DefaultMQProducer defaultMQProducer, RPCHook rpcHook) { + // 属性赋值 + this.defaultMQProducer = defaultMQProducer; + this.rpcHook = rpcHook; + + // 创建【异步消息线程池任务队列】,长度是 5w + this.asyncSenderThreadPoolQueue = new LinkedBlockingQueue(50000); + // 创建默认的异步消息任务线程池 + this.defaultAsyncSenderExecutor = new ThreadPoolExecutor( + // 核心线程数和最大线程数都是 系统可用的计算资源(8核16线程的系统就是 16)... + } + ``` + + + +**** + + + +##### 实现方法 + +* start():启动方法,参数默认是 true,代表正常的启动路径 + + ```java + public void start(final boolean startFactory) + ``` + + * `this.serviceState = ServiceState.START_FAILED`:先修改为启动失败,成功后再修改,这种思想很常见 + + * `this.checkConfig()`:判断生产者组名不能是空,也不能是 default_PRODUCER + + * `if (!getProducerGroup().equals(MixAll.CLIENT_INNER_PRODUCER_GROUP))`:条件成立说明当前生产者不是内部产生者,内部生产者是**处理消息回退**的这种情况使用的生产者 + + `this.defaultMQProducer.changeInstanceNameToPID()`:修改生产者实例名称为当前进程的 PID + + * ` this.mQClientFactory = ...`:获取当前进程的 MQ 客户端实例对象,从 factoryTable 中获取 key 为 客户端 ID,格式是`ip@pid`,**一个 JVM 进程只有一个 PID,也只有一个 MQClientInstance** + + * `boolean registerOK = mQClientFactory.registerProducer(...)`:将生产者注册到 RocketMQ 客户端实例内 + + * `this.topicPublishInfoTable.put(...)`:添加一个主题发布信息,key 是 **TBW102** ,value 是一个空对象 + + * `mQClientFactory.start()`:启动 RocketMQ 客户端实例对象 + + * `this.mQClientFactory.sendHeartbeatToAllBrokerWithLock()`:RocketMQ **客户端实例向已知的 Broker 节点发送一次心跳**(也是定时任务) + + * `this.timer.scheduleAtFixedRate()`: request 发送的消息需要消费着回执信息,启动定时任务每秒一次删除超时请求 + + * 生产者 msg 添加信息关联 ID 发送到 Broker + * 消费者从 Broker 拿到消息后会检查 msg 类型是一个需要回执的消息,处理完消息后会根据 msg 关联 ID 和客户端 ID 生成一条响应结果消息发送到 Broker,Broker 判断为回执消息,会根据客户端ID 找到 channel 推送给生产者 + * 生产者拿到回执消息后,读取出来关联 ID 找到对应的 RequestFuture,将阻塞线程唤醒 + +* sendDefaultImpl():发送消息 + + ```java + //参数1:消息;参数2:发送模式(同步异步单向);参数3:回调函数,异步发送时需要;参数4:发送超时时间, 默认 3 秒 + private SendResult sendDefaultImpl(msg, communicationMode, sendCallback,timeout) {} + ``` + + * `this.makeSureStateOK()`:校验生产者状态是运行中,否则抛出异常 + + * `topicPublishInfo = this.tryToFindTopicPublishInfo(msg.getTopic())`:**获取当前消息主题的发布信息** + + * `this.topicPublishInfoTable.get(topic)`:先尝试从本地主题发布信息映射表获取信息,获取不到继续执行 + + * `this.mQClientFactory.update...FromNameServer(topic)`:然后从 Namesrv 更新该 Topic 的路由数据 + + * `this.mQClientFactory.update...FromNameServer(...)`:**路由数据是空,获取默认 TBW102 的数据** + + `return topicPublishInfo`:返回 TBW102 主题的发布信息 + + * `String[] brokersSent = new String[timesTotal]`:下标索引代表第几次发送,值代表这次发送选择 Broker name + + * `for (; times < timesTotal; times++)`:循环发送,**发送成功或者发送尝试次数达到上限,结束循环** + + * `String lastBrokerName = null == mq ? null : mq.getBrokerName()`:获取上次发送失败的 BrokerName + + * `mqSelected = this.selectOneMessageQueue(topicPublishInfo, lastBrokerName)`:从发布信息中选择一个队列,生产者的**负载均衡策略**,参考系统特性章节 + + * `brokersSent[times] = mq.getBrokerName()`:将本次选择的 BrokerName 存入数组 + + * `msg.setTopic(this.defaultMQProducer.withNamespace(msg.getTopic()))`:**产生重投,重投消息需要加上标记** + + * `sendResult = this.sendKernelImpl`:核心发送方法 + + * `switch (communicationMode)`:异步或者单向消息直接返回 null,异步通过回调函数处理,同步发送进入逻辑判断 + + `if (sendResult.getSendStatus() != SendStatus.SEND_OK)`:**服务端 Broker 存储失败,需要重试其他 Broker** + + * `throw new MQClientException()`:未找到当前主题的路由数据,无法发送消息,抛出异常 + +* sendKernelImpl():**核心发送方法** + + ```java + //参数1:消息;参数2:选择的队列;参数3:发送模式(同步异步单向);参数4:回调函数,异步发送时需要;参数5:主题发布信息;参数6:剩余超时时间限制 + private SendResult sendKernelImpl(msg, mq, communicationMode, sendCallback, topicPublishInfo, timeout) + ``` + + * `brokerAddr = this.mQClientFactory(...)`:**获取指定 BrokerName 对应的 mater 节点的地址**,master 节点的 ID 为 0,集群模式下,**发送消息要发到主节点** + + * `brokerAddr = MixAll.brokerVIPChannel()`:Broker 启动时会绑定两个服务器端口,一个是普通端口,一个是 VIP 端口,服务器端根据不同端口创建不同的的 NioSocketChannel + + * `byte[] prevBody = msg.getBody()`:获取消息体 + + * `if (!(msg instanceof MessageBatch))`:非批量消息,需要重新设置消息 ID + + `MessageClientIDSetter.setUniqID(msg)`:**msg id 由两部分组成**,一部分是 ip 地址、进程号、Classloader 的 hashcode,另一部分是时间差(当前时间减去当月一号的时间)和计数器的值 + + * `if (this.tryToCompressMessage(msg))`:判断消息是否压缩,压缩需要设置压缩标记 + + * `hasCheckForbiddenHook、hasSendMessageHook`:执行钩子方法 + + * `requestHeader = new SendMessageRequestHeader()`:设置发送消息的消息头 + + * `if (requestHeader.getTopic().startsWith(MixAll.RETRY_GROUP_TOPIC_PREFIX))`:重投的发送消息 + + * `switch (communicationMode)`:异步发送一种处理方式,单向和同步同样的处理逻辑 + + `sendResult = this.mQClientFactory.getMQClientAPIImpl().sendMessage()`:**发送消息** + + * `request = RemotingCommand.createRequestCommand()`:创建一个 RequestCommand 对象 + * `request.setBody(msg.getBody())`:**将消息放入请求体** + * `switch (communicationMode)`:**根据不同的模式 invoke 不同的方法** + +* request():请求方法,消费者回执消息,这种消息是异步消息 + + * `requestResponseFuture = new RequestResponseFuture(correlationId, timeout, null)`:创建请求响应对象 + + * `getRequestFutureTable().put(correlationId, requestResponseFuture)`:放入RequestFutureTable 映射表中 + + * `this.sendDefaultImpl(msg, CommunicationMode.ASYNC, new SendCallback())`:**发送异步消息,有回调函数** + + * `return waitResponse(msg, timeout, requestResponseFuture, cost)`:用来挂起请求的方法 + + ```java + public Message waitResponseMessage(final long timeout) throws InterruptedException { + // 请求挂起 + this.countDownLatch.await(timeout, TimeUnit.MILLISECONDS); + return this.responseMsg; + } + + * 当消息被消费后,客户端处理响应时通过消息的关联 ID,从映射表中获取消息的 RequestResponseFuture,执行下面的方法唤醒挂起线程 + + ```java + public void putResponseMessage(final Message responseMsg) { + this.responseMsg = responseMsg; + this.countDownLatch.countDown(); + } + ``` + + + + +*** + + + +#### 路由信息 + +TopicPublishInfo 类用来存储路由信息 + +成员变量: + +* 顺序消息: + + ```java + private boolean orderTopic = false; + ``` + +* 消息队列: + + ```java + private List messageQueueList = new ArrayList<>(); // 主题全部的消息队列 + private volatile ThreadLocalIndex sendWhichQueue = new ThreadLocalIndex(); // 消息队列索引 + ``` + + ```java + // 【消息队列类】 + public class MessageQueue implements Comparable, Serializable { + private String topic; + private String brokerName; + private int queueId;// 队列 ID + } + ``` + +* 路由数据:主题对应的路由数据 + + ```java + private TopicRouteData topicRouteData; + ``` + + ```java + public class TopicRouteData extends RemotingSerializable { + private String orderTopicConf; + private List queueDatas; // 队列数据 + private List brokerDatas; // Broker 数据 + private HashMap/* Filter Server */> filterServerTable; + } + ``` + + ```java + public class QueueData implements Comparable { + private String brokerName; // 节点名称 + private int readQueueNums; // 读队列数 + private int writeQueueNums; // 写队列数 + private int perm; // 权限 + private int topicSynFlag; + } + ``` + + ```java + public class BrokerData implements Comparable { + private String cluster; // 集群名 + private String brokerName; // Broker节点名称 + private HashMap brokerAddrs; + } + ``` + +核心方法: + +* selectOneMessageQueue():**选择消息队列**使用 + + ```java + // 参数是上次失败时的 brokerName,可以为 null + public MessageQueue selectOneMessageQueue(final String lastBrokerName) { + if (lastBrokerName == null) { + return selectOneMessageQueue(); + } else { + // 遍历消息队列 + for (int i = 0; i < this.messageQueueList.size(); i++) { + // 【获取队列的索引,+1】 + int index = this.sendWhichQueue.getAndIncrement(); + // 获取队列的下标位置 + int pos = Math.abs(index) % this.messageQueueList.size(); + if (pos < 0) + pos = 0; + // 获取消息队列 + MessageQueue mq = this.messageQueueList.get(pos); + // 与上次选择的不同就可以返回 + if (!mq.getBrokerName().equals(lastBrokerName)) { + return mq; + } + } + return selectOneMessageQueue(); + } + } + ``` + + + + +*** + + + +#### 公共配置 + +公共的配置信息类 + +* ClientConfig 类 + + ```java + public class ClientConfig { + // Namesrv 地址配置 + private String namesrvAddr = NameServerAddressUtils.getNameServerAddresses(); + // 客户端的 IP 地址 + private String clientIP = RemotingUtil.getLocalAddress(); + // 客户端实例名称 + private String instanceName = System.getProperty("rocketmq.client.name", "DEFAULT"); + // 客户端回调线程池的数量,平台核心数,8核16线程的电脑返回16 + private int clientCallbackExecutorThreads = Runtime.getRuntime().availableProcessors(); + // 命名空间 + protected String namespace; + protected AccessChannel accessChannel = AccessChannel.LOCAL; + + // 获取路由信息的间隔时间 30s + private int pollNameServerInterval = 1000 * 30; + // 客户端与 broker 之间的心跳周期 30s + private int heartbeatBrokerInterval = 1000 * 30; + // 消费者持久化消费的周期 5s + private int persistConsumerOffsetInterval = 1000 * 5; + private long pullTimeDelayMillsWhenException = 1000; + private boolean unitMode = false; + private String unitName; + // vip 通道,broker 启动时绑定两个端口,其中一个是 vip 通道 + private boolean vipChannelEnabled = Boolean.parseBoolean(); + // 语言,默认是 Java + private LanguageCode language = LanguageCode.JAVA; + } + ``` + +* NettyClientConfig + + ```java + public class NettyClientConfig { + // 客户端工作线程数 + private int clientWorkerThreads = 4; + // 回调处理线程池 线程数:平台核心数 + private int clientCallbackExecutorThreads = Runtime.getRuntime().availableProcessors(); + // 单向请求并发数,默认 65535 + private int clientOnewaySemaphoreValue = NettySystemConfig.CLIENT_ONEWAY_SEMAPHORE_VALUE; + // 异步请求并发数,默认 65535 + private int clientAsyncSemaphoreValue = NettySystemConfig.CLIENT_ASYNC_SEMAPHORE_VALUE; + // 客户端连接服务器的超时时间限制 3秒 + private int connectTimeoutMillis = 3000; + // 客户端未激活周期,60s(指定时间内 ch 未激活,需要关闭) + private long channelNotActiveInterval = 1000 * 60; + // 客户端与服务器 ch 最大空闲时间 2分钟 + private int clientChannelMaxIdleTimeSeconds = 120; + + // 底层 Socket 写和收 缓冲区的大小 65535 64k + private int clientSocketSndBufSize = NettySystemConfig.socketSndbufSize; + private int clientSocketRcvBufSize = NettySystemConfig.socketRcvbufSize; + // 客户端 netty 是否启动内存池 + private boolean clientPooledByteBufAllocatorEnable = false; + // 客户端是否超时关闭 Socket 连接 + private boolean clientCloseSocketIfTimeout = false; + } + ``` + + + +*** + + + +#### 客户端类 + +##### 成员属性 + +MQClientInstance 是 RocketMQ 客户端实例,在一个 JVM 进程中只有一个客户端实例,**既服务于生产者,也服务于消费者** + +成员变量: + +* 配置信息: + + ```java + private final int instanceIndex; // 索引一般是 0,因为客户端实例一般都是一个进程只有一个 + private final String clientId; // 客户端 ID ip@pid + private final long bootTimestamp; // 客户端的启动时间 + private ServiceState serviceState; // 客户端状态 + ``` + +* 生产者消费者的映射表:key 是组名 + + ```java + private final ConcurrentMap producerTable + private final ConcurrentMap consumerTable + private final ConcurrentMap adminExtTable + ``` + +* 网络层配置: + + ```java + private final NettyClientConfig nettyClientConfig; + ``` + +* 核心功能的实现:负责将 MQ 业务层的数据转换为网络层的 RemotingCommand 对象,使用内部持有的 NettyRemotingClient 对象的 invoke 系列方法,完成网络 IO(同步、异步、单向) + + ```java + private final MQClientAPIImpl mQClientAPIImpl; + ``` + +* 本地路由数据:key 是主题名称,value 路由信息 + + ```java + private final ConcurrentMap topicRouteTable = new ConcurrentHashMap<>(); + ``` + +* 锁信息:两把锁,锁不同的数据 + + ```java + private final Lock lockNamesrv = new ReentrantLock(); + private final Lock lockHeartbeat = new ReentrantLock(); + ``` + +* 调度线程池:单线程,执行定时任务 + + ```java + private final ScheduledExecutorService scheduledExecutorService; + ``` + +* Broker 映射表:key 是 BrokerName + + ```java + // 物理节点映射表,value:Long 是 brokerID,【ID=0 的是主节点,其他是从节点】,String 是地址 ip:port + private final ConcurrentMap> brokerAddrTable; + // 物理节点版本映射表,String 是地址 ip:port,Integer 是版本 + ConcurrentMap> brokerVersionTable; + ``` + +* **客户端的协议处理器**:用于处理 IO 事件 + + ```java + private final ClientRemotingProcessor clientRemotingProcessor; + ``` + +* 消息服务: + + ```java + private final PullMessageService pullMessageService; // 拉消息服务 + private final RebalanceService rebalanceService; // 消费者负载均衡服务 + private final ConsumerStatsManager consumerStatsManager; // 消费者状态管理 + ``` + +* 内部生产者实例:处理消费端**消息回退**,用该生产者发送回退消息 + + ```java + private final DefaultMQProducer defaultMQProducer; + ``` + +* 心跳次数统计: + + ```java + private final AtomicLong sendHeartbeatTimesTotal = new AtomicLong(0) + ``` + +构造方法: + +* MQClientInstance 有参构造: + + ```java + public MQClientInstance(ClientConfig clientConfig, int instanceIndex, String clientId, RPCHook rpcHook) { + this.clientConfig = clientConfig; + this.instanceIndex = instanceIndex; + // Netty 相关的配置信息 + this.nettyClientConfig = new NettyClientConfig(); + // 平台核心数 + this.nettyClientConfig.setClientCallbackExecutorThreads(...); + this.nettyClientConfig.setUseTLS(clientConfig.isUseTLS()); + // 【创建客户端协议处理器】 + this.clientRemotingProcessor = new ClientRemotingProcessor(this); + // 创建 API 实现对象 + // 参数一:客户端网络配置 + // 参数二:客户端协议处理器,注册到客户端网络层 + // 参数三:rpcHook,注册到客户端网络层 + // 参数四:客户端配置 + this.mQClientAPIImpl = new MQClientAPIImpl(this.nettyClientConfig, this.clientRemotingProcessor, rpcHook, clientConfig); + + //... + // 内部生产者,指定内部生产者的组 + this.defaultMQProducer = new DefaultMQProducer(MixAll.CLIENT_INNER_PRODUCER_GROUP); + } + ``` + +* MQClientAPIImpl 有参构造: + + ```java + public MQClientAPIImpl(nettyClientConfig, clientRemotingProcessor, rpcHook, clientConfig) { + this.clientConfig = clientConfig; + topAddressing = new TopAddressing(MixAll.getWSAddr(), clientConfig.getUnitName()); + // 创建网络层对象,参数二为 null 说明客户端并不关心 channel event + this.remotingClient = new NettyRemotingClient(nettyClientConfig, null); + // 业务处理器 + this.clientRemotingProcessor = clientRemotingProcessor; + // 注册 RpcHook + this.remotingClient.registerRPCHook(rpcHook); + // ... + // 注册回退消息的请求码 + this.remotingClient.registerProcessor(RequestCode.PUSH_REPLY_MESSAGE_TO_CLIENT, this.clientRemotingProcessor, null); + } + ``` + + + +*** + + + +##### 成员方法 + +* start():启动方法 + + * `synchronized (this)`:加锁保证线程安全,保证只有一个实例对象启动 + * `this.mQClientAPIImpl.start()`:启动客户端网络层,底层调用 RemotingClient 类 + * `this.startScheduledTask()`:启动定时任务 + * `this.pullMessageService.start()`:启动拉取消息服务 + * `this.rebalanceService.start()`:启动负载均衡服务 + * `this.defaultMQProducer...start(false)`:启动内部生产者,参数为 false 代表不启动实例 + +* startScheduledTask():**启动定时任务**,调度线程池是单线程 + + * `if (null == this.clientConfig.getNamesrvAddr())`:Namesrv 地址是空,需要两分钟拉取一次 Namesrv 地址 + + * 定时任务 1:**从 Namesrv 更新客户端本地的路由数据**,周期 30 秒一次 + + ```java + // 获取生产者和消费者订阅的主题集合,遍历集合,对比从 namesrv 拉取最新的主题路由数据和本地数据,是否需要更新 + MQClientInstance.this.updateTopicRouteInfoFromNameServer(); + ``` + + * 定时任务 2:周期 30 秒一次,两个任务 + + * **清理下线的 Broker 节点**,遍历客户端的 Broker 物理节点映射表,将所有主题数据都不包含的 Broker 物理节点清理掉,如果被清理的 Broker 下所有的物理节点都没有了,就将该 Broker 的映射数据删除掉 + * **向在线的所有的 Broker 发送心跳数据**,同步发送的方式,返回值是 Broker 物理节点的版本号,更新版本映射表 + + ```java + MQClientInstance.this.cleanOfflineBroker(); + MQClientInstance.this.sendHeartbeatToAllBrokerWithLock(); + ``` + + ```java + // 心跳数据 + public class HeartbeatData extends RemotingSerializable { + // 客户端 ID ip@pid + private String clientID; + // 存储客户端所有生产者数据 + private Set producerDataSet = new HashSet(); + // 存储客户端所有消费者数据 + private Set consumerDataSet = new HashSet(); + } + ``` + + * 定时任务 3:消费者持久化消费数据,周期 5 秒一次 + + ```java + MQClientInstance.this.persistAllConsumerOffset(); + ``` + + * 定时任务 4:动态调整消费者线程池,周期 1 分钟一次 + + ```java + MQClientInstance.this.adjustThreadPool(); + ``` + +* updateTopicRouteInfoFromNameServer():**更新路由数据**,通过加锁保证当前实例只有一个线程去更新 + + * `if (isDefault && defaultMQProducer != null)`:需要默认数据 + + `topicRouteData = ...getDefaultTopicRouteInfoFromNameServer()`:从 Namesrv 获取默认的 TBW102 的路由数据 + + * `topicRouteData = ...getTopicRouteInfoFromNameServer(topic)`:需要**从 Namesrv 获取**路由数据(同步) + + * `old = this.topicRouteTable.get(topic)`:获取客户端实例本地的该主题的路由数据 + + * `boolean changed = topicRouteDataIsChange(old, topicRouteData)`:对比本地和最新下拉的数据是否一致 + + * `if (changed)`:不一致进入更新逻辑 + + `this.brokerAddrTable.put(...)`:更新客户端 broker 物理**节点映射表** + + `Update Pub info`:更新生产者信息 + + * `publishInfo = topicRouteData2TopicPublishInfo(topic, topicRouteData)`:将主题路由数据转化为发布数据,会**创建消息队列 MQ**,放入发布数据对象的集合中 + * `impl.updateTopicPublishInfo(topic, publishInfo)`:生产者将主题的发布数据保存到它本地,方便发送消息使用 + + `Update sub info`:更新消费者信息,创建 MQ 队列,更新订阅信息,用于负载均衡 + + `this.topicRouteTable.put(topic, cloneTopicRouteData)`:**将数据放入本地路由表** + + + +**** + + + +#### 网络通信 + +##### 成员属性 + +NettyRemotingClient 类负责客户端的网络通信 + +成员变量: + +* Netty 服务相关属性: + + ```java + private final NettyClientConfig nettyClientConfig; // 客户端的网络层配置 + private final Bootstrap bootstrap = new Bootstrap(); // 客户端网络层启动对象 + private final EventLoopGroup eventLoopGroupWorker; // 客户端网络层 Netty IO 线程组 + ``` + +* Channel 映射表: + + ```java + private final ConcurrentMap channelTables;// key 是服务器的地址,value 是通道对象 + private final Lock lockChannelTables = new ReentrantLock(); // 锁,控制并发安全 + ``` + +* 定时器:启动定时任务 + + ```java + private final Timer timer = new Timer("ClientHouseKeepingService", true) + ``` + +* 线程池: + + ```java + private ExecutorService publicExecutor; // 公共线程池 + private ExecutorService callbackExecutor; // 回调线程池,客户端发起异步请求,服务器的响应数据由回调线程池处理 + ``` + +* 事件监听器:客户端这里是 null + + ```java + private final ChannelEventListener channelEventListener; + ``` + +构造方法 + +* 无参构造: + + ```java + public NettyRemotingClient(final NettyClientConfig nettyClientConfig) { + this(nettyClientConfig, null); + } + ``` + +* 有参构造: + + ```java + public NettyRemotingClient(nettyClientConfig, channelEventListener) { + // 父类创建了2个信号量,1、控制单向请求的并发度,2、控制异步请求的并发度 + super(nettyClientConfig.getClientOnewaySemaphoreValue(), nettyClientConfig.getClientAsyncSemaphoreValue()); + this.nettyClientConfig = nettyClientConfig; + this.channelEventListener = channelEventListener; + + // 创建公共线程池 + int publicThreadNums = nettyClientConfig.getClientCallbackExecutorThreads(); + if (publicThreadNums <= 0) { + publicThreadNums = 4; + } + this.publicExecutor = Executors.newFixedThreadPool(publicThreadNums,); + + // 创建 Netty IO 线程,1个线程 + this.eventLoopGroupWorker = new NioEventLoopGroup(1, ); + + if (nettyClientConfig.isUseTLS()) { + sslContext = TlsHelper.buildSslContext(true); + } + } + ``` + + + +**** + + + +##### 成员方法 + +* start():启动方法 + + ```java + public void start() { + // channel pipeline 内的 handler 使用的线程资源,默认 4 个 + this.defaultEventExecutorGroup = new DefaultEventExecutorGroup(); + // 配置 netty 客户端启动类对象 + Bootstrap handler = this.bootstrap.group(this.eventLoopGroupWorker).channel(NioSocketChannel.class) + //... + .handler(new ChannelInitializer() { + @Override + public void initChannel(SocketChannel ch) throws Exception { + ChannelPipeline pipeline = ch.pipeline(); + // 加几个handler + pipeline.addLast( + // 服务端的数据,都会来到这个 + new NettyClientHandler()); + } + }); + // 注意 Bootstrap 只是配置好客户端的元数据了,【在这里并没有创建任何 channel 对象】 + // 定时任务 扫描 responseTable 中超时的 ResponseFuture,避免客户端线程长时间阻塞 + this.timer.scheduleAtFixedRate(() -> { + NettyRemotingClient.this.scanResponseTable(); + }, 1000 * 3, 1000); + // 这里是 null,不启动 + if (this.channelEventListener != null) { + this.nettyEventExecutor.start(); + } + } + ``` + +* 单向通信: + + ```java + public RemotingCommand invokeSync(String addr, final RemotingCommand request, long timeoutMillis) { + // 开始时间 + long beginStartTime = System.currentTimeMillis(); + // 获取或者创建客户端与服务端(addr)的通道 channel + final Channel channel = this.getAndCreateChannel(addr); + // 条件成立说明客户端与服务端 channel 通道正常,可以通信 + if (channel != null && channel.isActive()) { + try { + // 执行 rpcHook 拓展点 + doBeforeRpcHooks(addr, request); + // 计算耗时,如果当前耗时已经超过 timeoutMillis 限制,则直接抛出异常,不再进行系统通信 + long costTime = System.currentTimeMillis() - beginStartTime; + if (timeoutMillis < costTime) { + throw new RemotingTimeoutException("invokeSync call timeout"); + } + // 参数1:客户端-服务端通道channel + // 参数二:网络层传输对象,封装着请求数据 + // 参数三:剩余的超时限制 + RemotingCommand response = this.invokeSyncImpl(channel, request, ...); + // 后置处理 + doAfterRpcHooks(RemotingHelper.parseChannelRemoteAddr(channel), request, response); + // 返回响应数据 + return response; + } catch (RemotingSendRequestException e) {} + } else { + this.closeChannel(addr, channel); + throw new RemotingConnectException(addr); + } + } + ``` + + + + + +*** + + + +#### 延迟消息 + +##### 消息处理 + +BrokerStartup 初始化 BrokerController 调用 `registerProcessor()` 方法将 SendMessageProcessor 注册到 NettyRemotingServer 中,对应的请求 ID 为 `SEND_MESSAGE = 10`,NettyServerHandler 在处理请求时通过 CMD 会获取处理器执行 processRequest + +```java +// 参数一:处理通道的事件; 参数二:客户端 +public RemotingCommand processRequest(ChannelHandlerContext ctx, RemotingCommand request) { + RemotingCommand response = null; + response = asyncProcessRequest(ctx, request).get(); + return response; +} +``` + +SendMessageProcessor#asyncConsumerSendMsgBack:异步发送消费者的回调消息 + +* `final RemotingCommand response`:创建一个服务器响应对象 + +* `final ConsumerSendMsgBackRequestHeader requestHeader`:解析出客户端请求头信息,几个**核心字段**: + + * `private Long offset`:回退消息的 CommitLog offset + * `private Integer delayLevel`:延迟级别,一般是 0 + * `private String originMsgId, originTopic`:原始的消息 ID,主题 + * `private Integer maxReconsumeTimes`:最大重试次数,默认是 16 次 + +* `if ()`:鉴权,是否找到订阅组配置、Broker 是否支持写请求、订阅组是否支持消息重试 + +* `String newTopic = MixAll.getRetryTopic(...)`:**获取消费者组的重试主题**,规则是 `%RETRY%GroupName` + +* `int queueIdInt = Math.abs()`:**重试主题下的队列 ID 是 0** + +* `TopicConfig topicConfig`:获取重试主题的配置信息 + +* `MessageExt msgExt`:根据消息的物理 offset 到存储模块查询,内部先查询出这条消息的 size,然后再根据 offset 和 size 查询出整条 msg + +* `final String retryTopic`:获取消息的原始主题 + +* `if (null == retryTopic)`:条件成立说明**当前消息是第一次被回退**, 添加 `RETRY_TOPIC` 属性 + +* `msgExt.setWaitStoreMsgOK(false)`:异步刷盘 + +* `if (msgExt...() >= maxReconsumeTimes || delayLevel < 0)`:消息重试次数超过最大次数,不支持重试 + + `newTopic = MixAll.getDLQTopic()`:**获取消费者的死信队列**,规则是 `%DLQ%GroupName` + + `queueIdInt, topicConfig`:死信队列 ID 为 0,创建死信队列的配置 + +* `if (0 == delayLevel)`:说明延迟级别由 Broker 控制 + + `delayLevel = 3 + msgExt.getReconsumeTimes()`:**延迟级别默认从 3 级开始**,每重试一次,延迟级别 +1 + +* `msgExt.setDelayTimeLevel(delayLevel)`:**将延迟级别设置进消息属性**,存储时会检查该属性,该属性值 > 0 会**将消息的主题和队列修改为调度主题和调度队列 ID** + +* `MessageExtBrokerInner msgInner`:创建一条空消息,消息属性从 offset 查询出来的 msg 中拷贝 + +* `msgInner.setReconsumeTimes)`:重试次数设置为原 msg 的次数 +1 + +* `UtilAll.isBlank(originMsgId)`:判断消息是否是初次返回到服务器 + + * true:说明 msgExt 消息是第一次被返回到服务器,此时使用该 msg 的 id 作为 originMessageId + * false:说明原始消息已经被重试不止 1 次,此时使用 offset 查询出来的 msg 中的 originMessageId + +* `CompletableFuture putMessageResult = ..asyncPutMessage(msgInner)`:调用存储模块存储消息 + + `DefaultMessageStore#asyncPutMessage`: + + * `PutMessageResult result = this.commitLog.asyncPutMessage(msg)`:**将新消息存储到 CommitLog 中** + + + +*** + + + +##### 调度服务 + +DefaultMessageStore 中有成员属性 ScheduleMessageService,在 start 方法中会启动该调度服务 + +成员变量: + +* 延迟级别属性表: + + ```java + // 存储延迟级别对应的 延迟时间长度 (单位:毫秒) + private final ConcurrentMap delayLevelTable; + // 存储延迟级别 queue 的消费进度 offset,该 table 每 10 秒钟,会持久化一次,持久化到本地磁盘 + private final ConcurrentMap offsetTable; + ``` + +* 最大延迟级别: + + ```java + private int maxDelayLevel; + ``` + +* 模块启动状态: + + ```java + private final AtomicBoolean started = new AtomicBoolean(false); + ``` + +* 定时器:内部有线程资源,可执行调度任务 + + ```java + private Timer timer; + ``` + +成员方法: + +* load():加载调度消息,**初始化 delayLevelTable 和 offsetTable** + + ```java + public boolean load() + ``` + +* start():启动消息调度服务 + + ```java + public void start() + ``` + + * `if (started.compareAndSet(false, true))`:将启动状态设为 true + + * `this.timer`:创建定时器对象 + + * `for (... : this.delayLevelTable.entrySet())`:为**每个延迟级别创建一个延迟任务**提交到 timer ,周期执行,这样就可以**将延迟消息得到及时的消费** + + * `this.timer.scheduleAtFixedRate()`:提交周期型任务,延迟 10 秒执行,周期为 10 秒,持久化延迟队列消费进度任务 + + `ScheduleMessageService.this.persist()`:持久化消费进度 + + + +*** + + + +##### 调度任务 + +DeliverDelayedMessageTimerTask 是一个任务类 + +成员变量: + +* 延迟级别:延迟队列任务处理的延迟级别 + + ```java + private final int delayLevel; + ``` + +* 消费进度:延迟队列任务处理的延迟队列的消费进度 + + ```java + private final long offset; + ``` + +成员方法: + +* run():执行任务 + + ```java + public void run() { + if (isStarted()) { + this.executeOnTimeup(); + } + ``` + +* executeOnTimeup():执行任务 + + ```java + public void executeOnTimeup() + ``` + + * `ConsumeQueue cq`:获取出该延迟队列任务处理的**延迟队列 ConsumeQueue** + + * `SelectMappedBufferResult bufferCQ`:根据消费进度查询出 SMBR 对象 + + * `for (; i < bufferCQ.getSize(); i += ConsumeQueue.CQ_STORE_UNIT_SIZE)`:每次读取 20 各字节的数据 + + * `offsetPy, sizePy`:延迟消息的物理偏移量和消息大小 + + * `long tagsCode`:延迟消息的交付时间,在 ReputMessageService 转发时根据消息的 DELAY 属性是否 >0 ,会在 tagsCode 字段存储交付时间 + + * `long deliver... = this.correctDeliverTimestamp(..)`:**校准交付时间**,延迟时间过长会调整为当前时间立刻执行 + + * `long countdown = deliverTimestamp - now`:计算差值 + + * `if (countdown <= 0)`:**消息已经到达交付时间了** + + `MessageExt msgExt`:根据物理偏移量和消息大小获取这条消息 + + `MessageExtBrokerInner msgInner`:**构建一条新消息**,将原消息的属性拷贝过来 + + * `long tagsCodeValue`:不再是交付时间了 + * `MessageAccessor.clearProperty(msgInner, DELAY..)`:清理新消息的 DELAY 属性,避免存储时重定向到延迟队列 + * `msgInner.setTopic()`:**修改主题为原始的主题 `%RETRY%GroupName`** + * `String queueIdStr`:修改队列 ID 为原始的 ID + + `PutMessageResult putMessageResult`:**将新消息存储到 CommitLog**,消费者订阅的是目标主题,会再次消费该消息 + + * `else`:消息还未到达交付时间 + + `ScheduleMessageService.this.timer.schedule()`:创建该延迟级别的任务,延迟 countDown 毫秒之后再执行 + + `ScheduleMessageService.this.updateOffset()`:更新延迟级别队列的消费进度 + + * `PutMessageResult putMessageResult` + + * `bufferCQ == null`:说明通过消费进度没有获取到数据 + + `if (offset < cqMinOffset)`:如果消费进度比最小位点都小,说明是过期数据,重置为最小位点 + + * `ScheduleMessageService.this.timer.schedule()`:重新提交该延迟级别对应的延迟队列任务,延迟 100 毫秒后执行 + + + +**** + + + +#### 事务消息 + +##### 生产者类 + +TransactionMQProducer 类发送事务消息时使用 + +成员变量: + +* 事务回查线程池资源: + + ```java + private ExecutorService executorService; + +* 事务监听器: + + ```java + private TransactionListener transactionListener; + ``` + +核心方法: + +* start():启动方法 + + ```java + public void start() + ``` + + * `this.defaultMQProducerImpl.initTransactionEnv()`:初始化生产者实例和回查线程池资源 + * `super.start()`:启动生产者实例 + +* sendMessageInTransaction():发送事务消息 + + ```java + public TransactionSendResult sendMessageInTransaction(final Message msg, final Object arg) { + msg.setTopic(NamespaceUtil.wrapNamespace(this.getNamespace(), msg.getTopic())); + // 调用实现类的发送方法 + return this.defaultMQProducerImpl.sendMessageInTransaction(msg, null, arg); + } + ``` + + * `TransactionListener transactionListener = getCheckListener()`:获取监听器 + + * `if (null == localTransactionExecuter && null == transactionListener)`:两者都为 null 抛出异常 + + * `MessageAccessor.putProperty(msg, MessageConst.PROPERTY_TRANSACTION_PREPARED, "true")`:**设置事务标志** + + * `sendResult = this.send(msg)`:发送消息,同步发送 + + * `switch (sendResult.getSendStatus())`:**判断发送消息的结果状态** + + * `case SEND_OK`:消息发送成功 + + `msg.setTransactionId(transactionId)`:**设置事务 ID 为消息的 UNIQ_KEY 属性** + + `localTransactionState = ...executeLocalTransactionBranch(msg, arg)`:**执行本地事务** + + * `case SLAVE_NOT_AVAILABLE`:其他情况都需要回滚事务 + + `localTransactionState = LocalTransactionState.ROLLBACK_MESSAGE`:**事务状态设置为回滚** + + * `this.endTransaction(sendResult, ...)`:结束事务 + + * `EndTransactionRequestHeader requestHeader`:构建事务结束头对象 + * `this.mQClientFactory.getMQClientAPIImpl().endTransactionOneway()`:向 Broker 发起事务结束的单向请求 + + + +*** + + + +##### 接受消息 + +SendMessageProcessor 是服务端处理客户端发送来的消息的处理器,`processRequest()` 方法处理请求 + +核心方法: + +* `asyncProcessRequest()`:处理请求 + + ```java + public CompletableFuture asyncProcessRequest(ChannelHandlerContext ctx, + RemotingCommand request) { + final SendMessageContext mqtraceContext; + switch (request.getCode()) { + // 回调消息回退 + case RequestCode.CONSUMER_SEND_MSG_BACK: + return this.asyncConsumerSendMsgBack(ctx, request); + default: + // 解析出请求头对象 + SendMessageRequestHeader requestHeader = parseRequestHeader(request); + if (requestHeader == null) { + return CompletableFuture.completedFuture(null); + } + // 创建上下文对象 + mqtraceContext = buildMsgContext(ctx, requestHeader); + // 前置处理器 + this.executeSendMessageHookBefore(ctx, request, mqtraceContext); + // 判断是否是批量消息 + if (requestHeader.isBatch()) { + return this.asyncSendBatchMessage(ctx, request, mqtraceContext, requestHeader); + } else { + return this.asyncSendMessage(ctx, request, mqtraceContext, requestHeader); + } + } + } + ``` + +* asyncSendMessage():异步处理发送消息 + + ```java + private CompletableFuture asyncSendMessage(ChannelHandlerContext ctx, RemotingCommand request, SendMessageContext mqtraceContext, SendMessageRequestHeader requestHeader) + ``` + + * `RemotingCommand response`:创建响应对象 + + * `MessageExtBrokerInner msgInner = new MessageExtBrokerInner()`:创建 msgInner 对象,并赋值相关的属性,主题和队列 ID 都是请求头中的 + + * `String transFlag`:**获取事务属性** + + * `if (transFlag != null && Boolean.parseBoolean(transFlag))`:判断事务属性是否是 true,走事务消息的存储流程 + + * `putMessageResult = ...asyncPrepareMessage(msgInner)`:**事务消息处理流程** + + ```java + public CompletableFuture asyncPutHalfMessage(MessageExtBrokerInner messageInner) { + // 调用存储模块,将修改后的 msg 存储进 Broker(CommitLog) + return store.asyncPutMessage(parseHalfMessageInner(messageInner)); + } + ``` + + TransactionalMessageBridge#parseHalfMessageInner: + + * `MessageAccessor.putProperty(...)`:**将消息的原主题和队列 ID 放入消息的属性中** + * `msgInner.setSysFlag(...)`:消息设置为非事务状态 + * `msgInner.setTopic(TransactionalMessageUtil.buildHalfTopic())`:**消息主题设置为半消息主题** + * `msgInner.setQueueId(0)`:**队列 ID 设置为 0** + + * `else`:普通消息存储 + + + +*** + + + +##### 回查处理 + +ClientRemotingProcessor 是客户端用于处理请求,创建 MQClientAPIImpl 时将该处理器注册到 Netty 中,`processRequest()` 方法根据请求的命令码,进行不同的处理,事务回查的处理命令码为 `CHECK_TRANSACTION_STATE` + +Broker 端有定时任务发送回查请求 + +成员方法: + +* checkTransactionState():检查事务状态 + + ```java + public RemotingCommand checkTransactionState(ChannelHandlerContext ctx, RemotingCommand request) + ``` + + * `final CheckTransactionStateRequestHeader requestHeader`:解析出请求头对象 + * `final MessageExt messageExt`:从请求 body 中解析出服务器回查的事务消息 + * `String transactionId`:提取 UNIQ_KEY 字段属性值赋值给事务 ID + * `final String group`:提取生产者组名 + * `MQProducerInner producer = this...selectProducer(group)`:根据生产者组获取生产者对象 + * `String addr = RemotingHelper.parseChannelRemoteAddr()`:解析出要回查的 Broker 服务器的地址 + * `producer.checkTransactionState(addr, messageExt, requestHeader)`:生产者的事务回查 + * `Runnable request = new Runnable()`:**创建回查事务状态任务对象** + * 获取生产者的 TransactionCheckListener 和 TransactionListener,选择一个不为 null 的监听器进行事务状态回查 + * `this.processTransactionState()`:处理回查状态 + * `EndTransactionRequestHeader thisHeader`:构建 EndTransactionRequestHeader 对象 + * `DefaultMQProducerImpl...endTransactionOneway()`:向 Broker 发起结束事务单向请求,**二阶段提交** + * `this.checkExecutor.submit(request)`:提交到线程池运行 + + + +参考图:https://www.processon.com/view/link/61c8257e0e3e7474fb9dcbc0 + +参考视频:https://space.bilibili.com/457326371 + + + +*** + + + +##### 事务提交 + +EndTransactionProcessor 类是服务端用来处理客户端发来的提交或者回滚请求 + +* processRequest():处理请求 + + ```java + public RemotingCommand processRequest(ChannelHandlerContext ctx, RemotingCommand request) + ``` + + * `EndTransactionRequestHeader requestHeader`:从请求中解析出 EndTransactionRequestHeader + + * `if (MessageSysFlag.TRANSACTION_COMMIT_TYPE)`:**事务提交** + + `result = this.brokerController...commitMessage(requestHeader)`:根据 commitLogOffset 提取出 halfMsg 消息 + + `MessageExtBrokerInner msgInner`:根据 result 克隆出一条新消息 + + * `msgInner.setTopic(msgExt.getUserProperty(...))`:**设置回原主题** + + * `msgInner.setQueueId(Integer.parseInt(msgExt.getUserProperty(..)))`:**设置回原队列 ID** + * `MessageAccessor.clearProperty()`:清理上面的两个属性 + + `MessageAccessor.clearProperty(msgInner, ...)`:**清理事务属性** + + `RemotingCommand sendResult = sendFinalMessage(msgInner)`:调用存储模块存储至 Broker + + `this.brokerController...deletePrepareMessage(result.getPrepareMessage())`:**向删除(OP)队列添加消息**,消息体的数据是 halfMsg 的 queueOffset,**表示半消息队列指定的 offset 的消息已被删除** + + * `if (this...putOpMessage(msgExt, TransactionalMessageUtil.REMOVETAG))`:添加一条 OP 数据 + * `MessageQueue messageQueue`:新建一个消息队列,OP 队列 + * `return addRemoveTagInTransactionOp(messageExt, messageQueue)`:添加数据 + * `Message message`:创建 OP 消息 + * `writeOp(message, messageQueue)`:写入 OP 消息 + + * `else if (MessageSysFlag.TRANSACTION_ROLLBACK_TYPE)`:**事务回滚** + + `this.brokerController...deletePrepareMessage(result.getPrepareMessage())`:**也需要向 OP 队列添加消息** + + + +**** + + + +### Consumer + +#### 消费者类 + +##### 默认消费 + +DefaultMQPushConsumer 类是默认的消费者类 + +成员变量: + +* 消费者实现类: + + ```java + protected final transient DefaultMQPushConsumerImpl defaultMQPushConsumerImpl; + ``` + +* 消费属性: + + ```java + private String consumerGroup; // 消费者组 + private MessageModel messageModel = MessageModel.CLUSTERING; // 消费模式,默认集群模式 + ``` + +* 订阅信息:key 是主题,value 是过滤表达式,一般是 tag + + ```java + private Map subscription = new HashMap() + ``` + +* 消息监听器:**消息处理逻辑**,并发消费 MessageListenerConcurrently,顺序(分区)消费 MessageListenerOrderly + + ```java + private MessageListener messageListener; + ``` + +* 消费位点:当从 Broker 获取当前组内该 queue 的 offset 不存在时,consumeFromWhere 才有效,默认值代表从队列的最后 offset 开始消费,当队列内再有一条新的 msg 加入时,消费者才会去消费 + + ```java + private ConsumeFromWhere consumeFromWhere = ConsumeFromWhere.CONSUME_FROM_LAST_OFFSET; + ``` + +* 消费时间戳:当消费位点配置的是 CONSUME_FROM_TIMESTAMP 时,并且服务器 Group 内不存在该 queue 的 offset 时,会使用该时间戳进行消费 + + ```java + private String consumeTimestamp = UtilAll.timeMillisToHumanString3(System.currentTimeMillis() - (1000 * 60 * 30));// 消费者创建时间 - 30秒,转换成 格式: 年月日小时分钟秒,比如 20220203171201 + ``` + +* 队列分配策略:主题下的队列分配策略,RebalanceImpl 对象依赖该算法 + + ```java + private AllocateMessageQueueStrategy allocateMessageQueueStrategy; + ``` + +* 消费进度存储器: + + ```java + private OffsetStore offsetStore; + ``` + +核心方法: + +* start():启动消费者 + + ```java + public void start() + ``` + +* shutdown():关闭消费者 + + ```java + public void shutdown() + ``` + +* registerMessageListener():注册消息监听器 + + ```java + public void registerMessageListener(MessageListener messageListener) + ``` + +* subscribe():添加订阅信息,**将订阅信息放入负载均衡对象的 subscriptionInner 中** + + ```java + public void subscribe(String topic, String subExpression) + ``` + +* unsubscribe():删除订阅指定主题的信息 + + ```java + public void unsubscribe(String topic) + ``` + +* suspend():停止消费 + + ```java + public void suspend() + ``` + +* resume():恢复消费 + + ```java + public void resume() + ``` + + + +*** + + + +##### 默认实现 + +DefaultMQPushConsumerImpl 是默认消费者的实现类 + +成员变量: + +* 客户端实例:整个进程内只有一个客户端实例对象 + + ```java + private MQClientInstance mQClientFactory; + ``` + +* 消费者实例:门面对象 + + ```java + private final DefaultMQPushConsumer defaultMQPushConsumer; + ``` + +* **负载均衡**:分配订阅主题的队列给当前消费者,20 秒钟一个周期执行 Rebalance 算法(客户端实例触发) + + ```java + private final RebalanceImpl rebalanceImpl = new RebalancePushImpl(this); + ``` + +* 消费者信息: + + ```java + private final long consumerStartTimestamp; // 消费者启动时间 + private volatile ServiceState serviceState; // 消费者状态 + private volatile boolean pause = false; // 是否暂停 + private boolean consumeOrderly = false; // 是否顺序消费 + ``` + +* **拉取消息**:封装拉消息的 API,服务器 Broker 返回结果中包含下次 Pull 时推荐的 BrokerId,根据本次请求数据的冷热程度进行推荐 + + ```java + private PullAPIWrapper pullAPIWrapper; + ``` + +* **消息消费**服务:并发消费和顺序消费 + + ```java + private ConsumeMessageService consumeMessageService; + ``` + +* 流控: + + ```java + private long queueFlowControlTimes = 0; // 队列流控次数,默认每1000次流控,进行一次日志打印 + private long queueMaxSpanFlowControlTimes = 0; // 流控使用,控制打印日志 + ``` + +* HOOK:钩子方法 + + ```java + // 过滤消息 hook + private final ArrayList filterMessageHookList; + // 消息执行hook,在消息处理前和处理后分别执行 hook.before hook.after 系列方法 + private final ArrayList consumeMessageHookList; + ``` + +核心方法: + +* start():加锁保证线程安全 + + ```java + public synchronized void start() + ``` + + * `this.checkConfig()`:检查配置,包括组名、消费模式、订阅信息、消息监听器等 + * `this.copySubscription()`:拷贝订阅信息到 RebalanceImpl 对象 + * `this.rebalanceImpl.getSubscriptionInner().put(topic, subscriptionData)`:将订阅信息加入 rbl 的 map 中 + * `this.messageListenerInner = ...getMessageListener()`:将消息监听器保存到实例对象 + * `switch (this.defaultMQPushConsumer.getMessageModel())`:判断消费模式,广播模式下直接返回 + * `final String retryTopic`:创建当前**消费者组重试的主题名**,规则 `%RETRY%ConsumerGroup` + * `SubscriptionData subscriptionData = FilterAPI.buildSubscriptionData()`:创建重试主题的订阅数据对象 + * `this.rebalanceImpl.getSubscriptionInner().put(retryTopic, subscriptionData)`:将创建的重试主题加入到 rbl 对象的 map 中,**消息重试时会加入到该主题,消费者订阅这个主题之后,就有机会再次拿到该消息进行消费处理** + * `this.mQClientFactory = ...getOrCreateMQClientInstance()`:获取客户端实例对象 + * `this.rebalanceImpl.`:初始化负载均衡对象,设置**队列分配策略对象**到属性中 + * `this.pullAPIWrapper = new PullAPIWrapper()`:创建拉消息 API 对象,内部封装了查询推荐主机算法 + * `this.pullAPIWrapper.registerFilterMessageHook(filterMessageHookList)`:将过滤 Hook 列表注册到该对象内,消息拉取下来之后会执行该 Hook,**再进行一次自定义的消息过滤** + * `this.offsetStore = new RemoteBrokerOffsetStore()`:默认集群模式下创建消息进度存储器 + * `this.consumeMessageService = ...`:根据消息监听器的类型创建消费服务 + * `this.consumeMessageService.start()`:启动消费服务 + * `boolean registerOK = mQClientFactory.registerConsumer()`:**将消费者注册到客户端实例中**,客户端提供的服务: + * 心跳服务:把订阅数据同步到订阅主题的 Broker + * 拉消息服务:内部 PullMessageService 启动线程,基于 PullRequestQueue 工作,消费者负载均衡分配到队列后会向该队列提交 PullRequest + * 队列负载服务:每 20 秒调用一次 `consumer.doRebalance()` 接口 + * 消息进度持久化 + * 动态调整消费者、消费服务线程池 + * `mQClientFactory.start()`:启动客户端实例 + * ` this.updateTopic`:从 nameserver 获取主题路由数据,生成主题集合放入 rbl 对象的 table + * `this.mQClientFactory.checkClientInBroker()`:检查服务器是否支持消息过滤模式,一般使用 tag 过滤,服务器默认支持 + * `this.mQClientFactory.sendHeartbeatToAllBrokerWithLock()`:向所有已知的 Broker 节点,**发送心跳数据** + * `this.mQClientFactory.rebalanceImmediately()`:唤醒 rbl 线程,触发负载均衡执行 + + + +*** + + + +#### 负载均衡 + +##### 实现方式 + +MQClientInstance#start 中会启动负载均衡服务 RebalanceService: + +```java +public void run() { + // 检查停止标记 + while (!this.isStopped()) { + // 休眠 20 秒,防止其他线程饥饿,所以【每 20 秒负载均衡一次】 + this.waitForRunning(waitInterval); + // 调用客户端实例的负载均衡方法,底层【会遍历所有消费者,调用消费者的负载均衡】 + this.mqClientFactory.doRebalance(); + } +} +``` + +RebalanceImpl 类成员变量: + +* 分配给当前消费者的处理队列:处理消息队列集合,**ProcessQueue 是 MQ 队列在消费者端的快照** + + ```java + protected final ConcurrentMap processQueueTable; + ``` + +* 消费者订阅主题的队列信息: + + ```java + protected final ConcurrentMap> topicSubscribeInfoTable; + ``` + +* 订阅数据: + + ```java + protected final ConcurrentMap subscriptionInner; + ``` + +* 队列分配策略: + + ```java + protected AllocateMessageQueueStrategy allocateMessageQueueStrategy; + ``` + +成员方法: + +* doRebalance():负载均衡方法,以每个消费者实例为粒度进行负载均衡 + + ```java + public void doRebalance(final boolean isOrder) { + // 获取当前消费者的订阅数据 + Map subTable = this.getSubscriptionInner(); + if (subTable != null) { + // 遍历所有的订阅主题 + for (final Entry entry : subTable.entrySet()) { + // 获取订阅的主题 + final String topic = entry.getKey(); + // 按照主题进行负载均衡 + this.rebalanceByTopic(topic, isOrder); + } + } + // 将分配到当前消费者的队列进行过滤,不属于当前消费者订阅主题的直接移除 + this.truncateMessageQueueNotMyTopic(); + } + ``` + + 集群模式下: + + * `Set mqSet = this.topicSubscribeInfoTable.get(topic)`:订阅的主题下的全部队列信息 + + * `cidAll = this...findConsumerIdList(topic, consumerGroup)`:从服务器获取消费者组下的全部消费者 ID + + * `Collections.sort(mqAll)`:主题 MQ 队列和消费者 ID 都进行排序,**保证每个消费者的视图一致性** + + * `allocateResult = strategy.allocate()`: **调用队列分配策略**,给当前消费者进行分配 MessageQueue(下一节) + + * `boolean changed = this.updateProcessQueueTableInRebalance(...)`:**更新队列处理集合**,mqSet 是 rbl 算法分配到当前消费者的 MQ 集合 + + * `while (it.hasNext())`:遍历当前消费者的所有处理队列 + + * `if (mq.getTopic().equals(topic))`:该 MQ 是 本次 rbl 分配算法计算的主题 + + * `if (!mqSet.contains(mq))`:该 MQ 经过 rbl 计算之后,**被分配到其它 Consumer 节点** + + `pq.setDropped(true)`:将删除状态设置为 true + + `if (this.removeUnnecessaryMessageQueue(mq, pq))`:删除不需要的 MQ 队列 + + * `this...getOffsetStore().persist(mq)`:在 MQ 归属的 Broker 节点持久化消费进度 + + * `this...getOffsetStore().removeOffset(mq)`:删除该 MQ 在本地的消费进度 + + * `if (this.defaultMQPushConsumerImpl.isConsumeOrderly() &&)`:是否是**顺序消费**和集群模式 + + `if (pq.getLockConsume().tryLock(1000, ..))`: 获取锁成功,说明顺序消费任务已经停止消费工作 + + `return this.unlockDelay(mq, pq)`:**释放锁 Broker 端的队列锁,向服务器发起 oneway 的解锁请求** + + * `if (pq.hasTempMessage())`:队列中有消息,延迟 20 秒释放队列分布式锁,确保全局范围内只有一个消费任务 运行中 + * `else`:当前消费者本地该消费任务已经退出,直接释放锁 + + `else`:顺序消费任务正在消费一批消息,不可打断,增加尝试获取锁的次数 + + `it.remove()`:从 processQueueTable 移除该 MQ + + * `else if (pq.isPullExpired())`:说明当前 MQ 还是被当前 Consumer 消费,此时判断一下是否超过 2 分钟未到服务器 拉消息,如果条件成立进行上述相同的逻辑 + + * `for (MessageQueue mq : mqSet)`:开始处理当前主题**新分配到当前节点的队列** + + `if (isOrder && !this.lock(mq))`:**顺序消息为了保证有序性,需要获取队列锁** + + `ProcessQueue pq = new ProcessQueue()`:为每个新分配的消息队列创建快照队列 + + `long nextOffset = this.computePullFromWhere(mq)`:**从服务端获取新分配的 MQ 的消费进度** + + `ProcessQueue pre = this.processQueueTable.putIfAbsent(mq, pq)`:保存到处理队列集合 + + `PullRequest pullRequest = new PullRequest()`:**创建拉取请求对象** + + * `this.dispatchPullRequest(pullRequestList)`:放入 PullMessageService 的**本地阻塞队列**内,用于拉取消息工作 + +* lockAll():续约锁,对消费者的所有队列进行续约 + + ```java + public void lockAll() + ``` + + * `HashMap> brokerMqs`:将分配给当前消费者的全部 MQ 按照 BrokerName 分组 + + * `while (it.hasNext())`:遍历所有的分组 + + * `final Set mqs`:获取该 Broker 上分配给当前消费者的 queue 集合 + + * `FindBrokerResult findBrokerResult`:查询 Broker 主节点信息 + + * `LockBatchRequestBody requestBody`:创建请求对象,填充属性 + + * `Set lockOKMQSet`:**以组为单位向 Broker 发起批量续约锁的同步请求**,返回成功的队列集合 + + * `for (MessageQueue mq : lockOKMQSet)`:遍历续约锁成功的 MQ + + `processQueue.setLocked(true)`:**分布式锁状态设置为 true,表示允许顺序消费** + + `processQueue.setLastLockTimestamp(System.currentTimeMillis())`:设置上次获取锁的时间为当前时间 + + * `for (MessageQueue mq : mqs)`:遍历当前 Broker 上的所有队列集合 + + `if (!lockOKMQSet.contains(mq))`:条件成立说明续约锁失败 + + `processQueue.setLocked(false)`:**分布式锁状态设置为 false,表示不允许顺序消费** + + + + +*** + + + +##### 队列分配 + +AllocateMessageQueueStrategy 类是队列的分配策略 + +* 平均分配:AllocateMessageQueueAveragely 类 + + ```java + // 参数一:消费者组 参数二:当前消费者id + // 参数三:主题的全部队列,包括所有 broker 上该主题的 mq 参数四:全部消费者id集合 + public List allocate(String consumerGroup, String currentCID, List mqAll, List cidAll) { + // 获取当前消费者在全部消费者中的位置,【全部消费者是已经排序好的,排在前面的优先分配更多的队列】 + int index = cidAll.indexOf(currentCID); + // 平均分配完以后,还剩余的待分配的 mq 的数量 + int mod = mqAll.size() % cidAll.size(); + // 首先判断整体的 mq 的数量是否小于消费者的数量,小于消费者的数量就说明不够分的,先分一个 + int averageSize = mqAll.size() <= cidAll.size() ? 1 : + // 成立需要多分配一个队列,因为更靠前 + (mod > 0 && index < mod ? mqAll.size() / cidAll.size() + 1 : mqAll.size() / cidAll.size()); + // 获取起始的分配位置 + int startIndex = (mod > 0 && index < mod) ? index * averageSize : index * averageSize + mod; + // 防止索引越界 + int range = Math.min(averageSize, mqAll.size() - startIndex); + // 开始分配,【挨着分配,是直接就把当前的 消费者分配完成】 + for (int i = 0; i < range; i++) { + result.add(mqAll.get((startIndex + i) % mqAll.size())); + } + return result; + } + ``` + + 队列排序后:Q1 → Q2 → Q3,消费者排序后 C1 → C2 → C3 + + ![](https://seazean.oss-cn-beijing.aliyuncs.com/img/Frame/RocketMQ-平均队列分配.png) + +* 轮流分配:AllocateMessageQueueAveragelyByCircle + + ![](https://seazean.oss-cn-beijing.aliyuncs.com/img/Frame/RocketMQ-平均队列轮流分配.png) + +* 指定机房平均分配:AllocateMessageQueueByMachineRoom,前提是 Broker 的命名规则为 `机房名@BrokerName` + + + + + +*** + + + +#### 拉取服务 + +##### 实现方式 + +MQClientInstance#start 中会启动消息拉取服务:PullMessageService + +```java +public void run() { + // 检查停止标记,【循环拉取】 + while (!this.isStopped()) { + try { + // 从阻塞队列中获取拉消息请求 + PullRequest pullRequest = this.pullRequestQueue.take(); + // 拉取消息,获取请求对应的使用当前消费者组中的哪个消费者,调用消费者的 pullMessage 方法 + this.pullMessage(pullRequest); + } catch (Exception e) { + log.error("Pull Message Service Run Method exception", e); + } + } +} +``` + +DefaultMQPushConsumerImpl#pullMessage: + +* `ProcessQueue processQueue = pullRequest.getProcessQueue()`:获取请求对应的快照队列,并判断是否是删除状态 + +* `this.executePullRequestLater()`:如果当前消费者不是运行状态,则拉消息任务延迟 3 秒后执行,如果是暂停状态延迟 1 秒 + +* **流控的逻辑**: + + `long cachedMessageCount = processQueue.getMsgCount().get()`:获取消费者本地该 queue 快照内缓存的消息数量,如果大于 1000 条,进行流控,延迟 50 毫秒 + + `long cachedMessageSizeInMiB`: 消费者本地该 queue 快照内缓存的消息容量 size,超过 100m 消息未被消费进行流控 + + `if(processQueue.getMaxSpan() > 2000)`:消费者本地缓存消息第一条消息最后一条消息跨度超过 2000 进行流控 + +* `SubscriptionData subscriptionData`:本次拉消息请求订阅的主题数据,如果调用了 `unsubscribe(主题)` 将会获取为 null + +* `PullCallback pullCallback = new PullCallback()`:**拉消息处理回调对象** + + * `pullResult = ...processPullResult()`:预处理 PullResult 结果,将服务器端指定 MQ 的拉消息**下一次的推荐节点**保存到 pullFromWhichNodeTable 中,**并进行消息过滤** + + * `case FOUND`:正常拉取到消息 + + `pullRequest.setNextOffset(pullResult.getNextBeginOffset())`:更新 pullRequest 对象下一次拉取消息的位点 + + `if (pullResult.getMsgFoundList() == null...)`:消息过滤导致消息全部被过滤掉,需要立马发起下一次拉消息 + + `boolean .. = processQueue.putMessage()`:将服务器拉取的消息集合**加入到消费者本地**的 processQueue 内 + + `DefaultMQPushConsumerImpl...submitConsumeRequest()`:**提交消费任务,分为顺序消费和并发消费** + + `Defaul..executePullRequestImmediately(pullRequest)`:将更新过 nextOffset 字段的 PullRequest 对象,再次放到 pullMessageService 的阻塞队列中,**形成闭环** + + * `case NO_NEW_MSG ||NO_MATCHED_MSG`:**表示本次 pull 没有新的可消费的信息** + + `pullRequest.setNextOffset()`:更新更新 pullRequest 对象下一次拉取消息的位点 + + `Defaul..executePullRequestImmediately(pullRequest)`:再次拉取请求 + + * `case OFFSET_ILLEGAL`:**本次 pull 时使用的 offset 是无效的**,即 offset > maxOffset || offset < minOffset + + `pullRequest.setNextOffset()`:调整 pullRequest.nextOffset 为正确的 offset + + `pullRequest.getProcessQueue().setDropped(true)`:设置该 processQueue 为删除状态,如果有该 queue 的消费任务,消费任务会马上停止 + + `DefaultMQPushConsumerImpl.this.executeTaskLater()`:提交异步任务,10 秒后去执行 + + * `DefaultMQPushConsumerImpl...updateOffset()`:更新 offsetStore 该 MQ 的 offset 为正确值,内部直接替换 + + * `DefaultMQPushConsumerImpl...persist()`:持久化该 messageQueue 的 offset 到 Broker 端 + + * `DefaultMQPushConsumerImpl...removeProcessQueue()`: 删除该消费者该 messageQueue 对应的 processQueue + + * 这里没有再次提交 pullRequest 到 pullMessageService 的队列,那该队列不再拉消息了吗? + + 负载均衡 rbl 程序会重建该队列的 processQueue,重建完之后会为该队列创建新的 PullRequest 对象 + +* `int sysFlag = PullSysFlag.buildSysFlag()`:**构建标志对象**,sysFlag 高 4 位未使用,低 4 位使用,从左到右 0000 0011 + + * 第一位:表示是否提交消费者本地该队列的 offset,一般是 1 + * 第二位:表示是否允许服务器端进行长轮询,一般是 1 + * 第三位:表示是否提交消费者本地该主题的订阅数据,一般是 0 + * 第四位:表示是否为类过滤,一般是 0 + +* `this.pullAPIWrapper.pullKernelImpl()`:拉取消息的核心方法 + + + +*** + + + +##### 封装对象 + +PullAPIWrapper 类封装了拉取消息的 API + +成员变量: + +* 推荐拉消息使用的主机 ID: + + ```java + private ConcurrentMap pullFromWhichNodeTable + ``` + +成员方法: + +* pullKernelImpl():拉消息 + + * `FindBrokerResult findBrokerResult`:**本地查询指定 BrokerName 的地址信息**,推荐节点或者主节点 + + * `if (null == findBrokerResult)`:查询不到,就到 Namesrv 获取指定 topic 的路由数据 + + * `if (findBrokerResult.isSlave())`:成立说明 findBrokerResult 表示的主机为 slave 节点,**slave 不存储 offset 信息** + + `sysFlagInner = PullSysFlag.clearCommitOffsetFlag(sysFlagInner)`:将 sysFlag 标记位中 CommitOffset 的位置为 0 + + * `PullMessageRequestHeader requestHeader`:创建请求头对象,封装所有的参数 + + * `PullResult pullResult = this.mQClientFactory.getMQClientAPIImpl().pullMessage()`:调用客户端实例的方法,核心逻辑就是**将业务数据转化为 RemotingCommand 通过 NettyRemotingClient 的 IO 进行通信** + + * `RemotingCommand request`:创建网络层传输对象 RemotingCommand 对象,**请求 ID 为 `PULL_MESSAGE = 11`** + + * `return this.pullMessageSync(...)`:此处是**异步调用,处理结果放入 ResponseFuture 中**,参考服务端小节的处理器类 `NettyServerHandler#processMessageReceived` 方法 +* `RemotingCommand response = responseFuture.getResponseCommand()`:获取服务器端响应数据 response + + * `PullResult pullResult`:从 response 内提取出来拉消息结果对象,将响应头 PullMessageResponseHeader 对象中信息**填充到 PullResult 中**,列出两个重要的字段: + * `private Long suggestWhichBrokerId`:服务端建议客户端下次 Pull 时选择的 BrokerID + * `private Long nextBeginOffset`:客户端下次 Pull 时使用的 offset 信息 + +* `pullCallback.onSuccess(pullResult)`:将 PullResult 交给拉消息结果处理回调对象,调用 onSuccess 方法 + + + +*** + + + +#### 拉取处理 + +##### 处理器 + +BrokerStartup#createBrokerController 方法中创建了 BrokerController 并进行初始化,调用 `registerProcessor()` 方法将处理器 PullMessageProcessor 注册到 NettyRemotingServer 中,对应的请求 ID 为 `PULL_MESSAGE = 11`,NettyServerHandler 在处理请求时通过请求 ID 会获取处理器执行 processRequest 方法 + +```java +// 参数一:服务器与客户端 netty 通道; 参数二:客户端请求; 参数三:是否允许服务器端长轮询,默认 true +private RemotingCommand processRequest(final Channel channel, RemotingCommand request, boolean brokerAllowSuspend) +``` + +* `RemotingCommand response`:创建响应对象,设置为响应类型的请求,响应头是 PullMessageResponseHeader + +* `final PullMessageResponseHeader responseHeader`:获取响应对象的 header + +* `final PullMessageRequestHeader requestHeader`:解析出请求头 PullMessageRequestHeader + +* `response.setOpaque(request.getOpaque())`:设置 opaque 属性,客户端**根据该字段获取 ResponseFuture** 进行处理 + +* 进行一些鉴权的逻辑:是否允许长轮询、提交 offset、topicConfig 是否是空、队列 ID 是否合理 + +* `ConsumerGroupInfo consumerGroupInfo`:获取消费者组信息,包含全部的消费者和订阅数据 + +* `subscriptionData = consumerGroupInfo.findSubscriptionData()`:**获取指定主题的订阅数据** + +* `if (!ExpressionType.isTagType()`:表达式匹配 + +* `MessageFilter messageFilter`:创建消息过滤器,一般是通过 tagCode 进行过滤 + +* `DefaultMessageStore.getMessage()`:**查询消息的核心逻辑,在 Broker 端查询消息**(存储端笔记详解了该源码) + +* `response.setRemark()`:设置此次响应的状态 + +* `responseHeader.set..`:设置响应头对象的一些字段 + +* `switch (this.brokerController.getMessageStoreConfig().getBrokerRole())`:如果当前主机节点角色为 slave 并且**从节点读**并未开启的话,直接给客户端 一个状态 `PULL_RETRY_IMMEDIATELY`,并设置为下次从主节点读 + +* `if (this.brokerController.getBrokerConfig().isSlaveReadEnable())`:消费太慢,**下次从另一台机器拉取** + +* `switch (getMessageResult.getStatus())`:根据 getMessageResult 的状态设置 response 的 code + + ```java + public enum GetMessageStatus { + FOUND, // 查询成功 + NO_MATCHED_MESSAGE, // 未查询到到消息,服务端过滤 tagCode + MESSAGE_WAS_REMOVING, // 查询时赶上 CommitLog 清理过期文件,导致查询失败,立刻尝试 + OFFSET_FOUND_NULL, // 查询时赶上 ConsumerQueue 清理过期文件,导致查询失败,【进行长轮询】 + OFFSET_OVERFLOW_BADLY, // pullRequest.offset 越界 maxOffset + OFFSET_OVERFLOW_ONE, // pullRequest.offset == CQ.maxOffset,【进行长轮询】 + OFFSET_TOO_SMALL, // pullRequest.offset 越界 minOffset + NO_MATCHED_LOGIC_QUEUE, // 没有匹配到逻辑队列 + NO_MESSAGE_IN_QUEUE, // 空队列,创建队列也是因为查询导致,【进行长轮询】 + } + ``` + +* `switch (response.getCode())`:根据 response 状态做对应的业务处理 + + `case ResponseCode.SUCCESS`:查询成功 + + * `final byte[] r = this.readGetMessageResult()`:本次 pull 出来的全部消息导入 byte 数组 + * `response.setBody(r)`:将消息的 byte 数组保存到 response body 字段 + + `case ResponseCode.PULL_NOT_FOUND`:产生这种情况大部分原因是 `pullRequest.offset == queue.maxOffset`,说明已经没有需要获取的消息,此时如果直接返回给客户端,客户端会立刻重新请求,还是继续返回该状态,频繁拉取服务器导致服务器压力大,所以此处**需要长轮询** + + * `if (brokerAllowSuspend && hasSuspendFlag)`:brokerAllowSuspend = true,当长轮询结束再次执行 processRequest 时该参数为 false,所以**每次 Pull 请求至多在服务器端长轮询控制一次** + * `PullRequest pullRequest = new PullRequest()`:创建长轮询 PullRequest 对象 + * `this.brokerController...suspendPullRequest(topic, queueId, pullRequest)`:将长轮询请求对象交给长轮询服务 + * `String key = this.buildKey(topic, queueId)`:构建一个 `topic@queueId` 的 key + * `ManyPullRequest mpr = this.pullRequestTable.get(key)`:从拉请求表中获取对象 + * `mpr.addPullRequest(pullRequest)`:**将 PullRequest 对象放入到长轮询的请求集合中** + * `response = null`:响应设置为 null 内部的 callBack 就不会给客户端发送任何数据,**不进行通信**,否则就又开始重新请求 + +* `boolean storeOffsetEnable`:允许长轮询、sysFlag 表示提交消费者本地该队列的offset、当前 broker 节点角色为 master 节点三个条件成立,才**在 Broker 端存储消费者组内该主题的指定 queue 的消费进度** + +* `return response`:返回 response,不为 null 时外层 processRequestCommand 的 callback 会将数据写给客户端 + + + +*** + + + +##### 长轮询 + +PullRequestHoldService 类负责长轮询,BrokerController#start 方法中调用了 `this.pullRequestHoldService.start()` 启动该服务 + +核心方法: + +* run():核心运行方法 + + ```java + public void run() { + // 循环运行 + while (!this.isStopped()) { + if (this.brokerController.getBrokerConfig().isLongPollingEnable()) { + // 服务器开启长轮询开关:每次循环休眠5秒 + this.waitForRunning(5 * 1000); + } else { + // 服务器关闭长轮询开关:每次循环休眠1秒 + this.waitForRunning(...); + } + // 检查持有的请求 + this.checkHoldRequest(); + // ..... + } + } + ``` + +* checkHoldRequest():检查所有的请求 + + * `for (String key : this.pullRequestTable.keySet())`:**处理所有的 topic@queueId 的逻辑** + * `String[] kArray = key.split(TOPIC_QUEUEID_SEPARATOR)`:key 按照 @ 拆分,得到 topic 和 queueId + * `long offset = this...getMaxOffsetInQueue(topic, queueId)`: 到存储模块查询该 ConsumeQueue 的**最大 offset** + * `this.notifyMessageArriving(topic, queueId, offset)`:通知消息到达 + +* notifyMessageArriving():**通知消息到达**的逻辑,ReputMessageService 消息分发服务也会调用该方法 + + * `ManyPullRequest mpr = this.pullRequestTable.get(key)`:获取对应的的 manyPullRequest 对象 + * `List requestList`:获取该队列下的所有 PullRequest,并进行遍历 + * `List replayList`:当某个 pullRequest 不超时,并且对应的 `CQ.maxOffset <= pullRequest.offset`,就将该 PullRequest 再放入该列表 + * `long newestOffset`:该值为 CQ 的 maxOffset + * `if (newestOffset > request.getPullFromThisOffset())`:**请求对应的队列内可以 pull 消息了,结束长轮询** + * `boolean match`:进行过滤匹配 + * `this.brokerController...executeRequestWhenWakeup()`:将满足条件的 pullRequest 再次提交到线程池内执行 + * `final RemotingCommand response`:执行 processRequest 方法,并且**不会触发长轮询** + * `channel.writeAndFlush(response).addListene()`:**将结果数据发送给客户端** + * `if (System.currentTimeMillis() >= ...)`:判断该 pullRequest 是否超时,超时后的也是重新提交到线程池,并且不进行长轮询 + * `mpr.addPullRequest(replayList)`:将未满足条件的 PullRequest 对象再次添加到 ManyPullRequest 属性中 + + + +*** + + + +##### 结果类 + +GetMessageResult 类成员信息: + +```java +public class GetMessageResult { + // 查询消息时,最底层都是 mappedFile 支持的查询,查询时返回给外层一个 SelectMappedBufferResult, + // mappedFile 每查询一次都会 refCount++ ,通过SelectMappedBufferResult持有mappedFile,完成资源释放的句柄 + private final List messageMapedList = + new ArrayList(100); + + // 该List内存储消息,每一条消息都被转成 ByteBuffer 表示了 + private final List messageBufferList = new ArrayList(100); + // 查询结果状态 + private GetMessageStatus status; + // 客户端下次再向当前Queue拉消息时,使用的 offset + private long nextBeginOffset; + // 当前queue最小offset + private long minOffset; + // 当前queue最大offset + private long maxOffset; + // 消息总byte大小 + private int bufferTotalSize = 0; + // 服务器建议客户端下次到该 queue 拉消息时是否使用 【从节点】 + private boolean suggestPullingFromSlave = false; +} +``` + + + +*** + + + +#### 队列快照 + +##### 成员属性 + +ProcessQueue 类是消费队列的快照 + +成员变量: + +* 属性字段: + + ```java + private final AtomicLong msgCount = new AtomicLong(); // 队列中消息数量 + private final AtomicLong msgSize = new AtomicLong(); // 消息总大小 + private volatile long queueOffsetMax = 0L; // 快照中最大 offset + private volatile boolean dropped = false; // 快照是否移除 + private volatile long lastPullTimestamp = current; // 上一次拉消息的时间 + private volatile long lastConsumeTimestamp = current; // 上一次消费消息的时间 + private volatile long lastLockTimestamp = current; // 上一次获取锁的时间 + ``` + +* **消息容器**:key 是消息偏移量,val 是消息 + + ```java + private final TreeMap msgTreeMap = new TreeMap(); + ``` + +* **顺序消费临时容器**: + + ```java + private final TreeMap consumingMsgOrderlyTreeMap = new TreeMap(); + ``` + +* 锁: + + ```java + private final ReadWriteLock lockTreeMap; // 读写锁 + private final Lock lockConsume; // 重入锁,【顺序消费使用】 + ``` + +* 顺序消费状态: + + ```java + private volatile boolean locked = false; // 是否是锁定状态 + private volatile boolean consuming = false; // 是否是消费中 + ``` + + + +**** + + + +##### 成员方法 + +核心成员方法 + +* putMessage():将 Broker 拉取下来的 msgs 存储到快照队列内,返回为 true 表示提交顺序消费任务,false 表示不提交 + + ```java + public boolean putMessage(final List msgs) + ``` + + * `this.lockTreeMap.writeLock().lockInterruptibly()`:获取写锁 + + * `for (MessageExt msg : msgs)`:遍历 msgs 全部加入 msgTreeMap,key 是消息的 queueOffset + + * `if (!msgTreeMap.isEmpty() && !this.consuming)`:**消息容器中存在未处理的消息,并且不是消费中的状态** + + `dispatchToConsume = true`:代表需要提交顺序消费任务 + + `this.consuming = true`:设置为顺序消费执行中的状态 + + * `this.lockTreeMap.writeLock().unlock()`:释放写锁 + +* removeMessage():移除已经消费的消息,参数是已经消费的消息集合,并发消费使用 + + ```java + public long removeMessage(final List msgs) + ``` + + * `long result = -1`:结果初始化为 -1 + * `this.lockTreeMap.writeLock().lockInterruptibly()`:获取写锁 + * `this.lastConsumeTimestamp = now`:更新上一次消费消息的时间为现在 + * `if (!msgTreeMap.isEmpty())`:判断消息容器是否是空,**是空直接返回 -1** + * `result = this.queueOffsetMax + 1`:设置结果,**删除完后消息容器为空时返回** + * `for (MessageExt msg : msgs)`:将已经消费的消息全部从 msgTreeMap 移除 + * `if (!msgTreeMap.isEmpty())`:移除后容器内还有待消费的消息,**获取第一条消息 offset 返回** + * `this.lockTreeMap.writeLock().unlock()`:释放写锁 + +* takeMessages():获取一批消息,顺序消费使用 + + ```java + public List takeMessages(final int batchSize) + ``` + + * `this.lockTreeMap.writeLock().lockInterruptibly()`:获取写锁 + * `this.lastConsumeTimestamp = now`:更新上一次消费消息的时间为现在 + * `for (int i = 0; i < batchSize; i++)`:从头节点开始获取消息 + * `result.add(entry.getValue())`:将消息放入结果集合 + * `consumingMsgOrderlyTreeMap.put()`:将消息加入顺序消费容器中 + * `if (result.isEmpty())`:条件成立说明顺序消费容器本地快照内的消息全部处理完了,**当前顺序消费任务需要停止** + * `consuming = false`:消费状态置为 false + * `this.lockTreeMap.writeLock().unlock()`:释放写锁 + +* commit():处理完一批消息后调用,顺序消费使用 + + ```java + public long commit() + ``` + + * `this.lockTreeMap.writeLock().lockInterruptibly()`:获取写锁 + * `Long offset = this.consumingMsgOrderlyTreeMap.lastKey()`:获取顺序消费临时容器最后一条数据的 key + * `msgCount, msgSize`:更新顺序消费相关的字段 + * `this.consumingMsgOrderlyTreeMap.clear()`:清空顺序消费容器的数据 + * `return offset + 1`:**消费者下一条消费的位点** + * `this.lockTreeMap.writeLock().unlock()`:释放写锁 + +* cleanExpiredMsg():清除过期消息 + + ```java + public void cleanExpiredMsg(DefaultMQPushConsumer pushConsumer) + ``` + + * `if (pushConsumer.getDefaultMQPushConsumerImpl().isConsumeOrderly()) `:顺序消费不执行过期清理逻辑 + * `int loop = msgTreeMap.size() < 16 ? msgTreeMap.size() : 16`:最多循环 16 次 + * `if (!msgTreeMap.isEmpty() &&)`:如果容器中第一条消息的消费开始时间与当前系统时间差值 > 15min,则取出该消息 + * `else`:直接跳出循环,因为**快照队列内的消息是有顺序的**,第一条消息不过期,其他消息都不过期 + * `pushConsumer.sendMessageBack(msg, 3)`:**消息回退**到服务器,设置该消息的延迟级别为 3 + * `if (!msgTreeMap.isEmpty() && msg.getQueueOffset() == msgTreeMap.firstKey())`:条件成立说明消息回退期间,该目标消息并没有被消费任务成功消费 + * `removeMessage(Collections.singletonList(msg))`:从 treeMap 将该回退成功的 msg 删除 + + + +**** + + + +#### 并发消费 + +##### 成员属性 + +ConsumeMessageConcurrentlyService 负责并发消费服务 + +成员变量: + +* 消息监听器:封装处理消息的逻辑,该监听器由开发者实现,并注册到 defaultMQPushConsumer + + ```java + private final MessageListenerConcurrently messageListener; + ``` + +* 消费属性: + + ```java + private final BlockingQueue consumeRequestQueue; // 消费任务队列 + private final String consumerGroup; // 消费者组 + ``` + +* 线程池: + + ```java + private final ThreadPoolExecutor consumeExecutor; // 消费任务线程池,默认 20 + private final ScheduledExecutorService scheduledExecutorService;// 调度线程池,延迟提交消费任务 + private final ScheduledExecutorService cleanExpireMsgExecutors; // 清理过期消息任务线程池,15min 一次 + ``` + + + +*** + + + +##### 成员方法 + +ConsumeMessageConcurrentlyService 并发消费核心方法 + +* start():启动消费服务,DefaultMQPushConsumerImpl 启动时会调用该方法 + + ```java + public void start() { + // 提交“清理过期消息任务”任务,延迟15min之后执行,之后每15min执行一次 + this.cleanExpireMsgExecutors.scheduleAtFixedRate(() -> cleanExpireMsg()}, + 15, 15, TimeUnit.MINUTES); + } + ``` + +* cleanExpireMsg():清理过期消息任务 + + ```java + private void cleanExpireMsg() + ``` + + * `Iterator> it `:获取分配给当前消费者的队列 + * `while (it.hasNext())`:遍历所有的队列 + * `pq.cleanExpiredMsg(this.defaultMQPushConsumer)`:调用队列快照 ProcessQueue 清理过期消息的方法 + +* submitConsumeRequest():提交消费请求 + + ```java + // 参数一:从服务器 pull 下来的这批消息 + // 参数二:消息归属 mq 在消费者端的 processQueue,提交消费任务之前,msgs已经加入到该pq内了 + // 参数三:消息归属队列 + // 参数四:并发消息此参数无效 + public void submitConsumeRequest(List msgs, ProcessQueue processQueue, MessageQueue messageQueue, boolean dispatchToConsume) + ``` + + * `final int consumeBatchSize`:**一个消费任务可消费的消息数量**,默认为 1 + + * `if (msgs.size() <= consumeBatchSize)`:判断一个消费任务是否可以提交 + + `ConsumeRequest consumeRequest`:封装为消费请求 + + `this.consumeExecutor.submit(consumeRequest)`:提交消费任务,异步执行消息的处理 + + * `else`:说明消息较多,需要多个消费任务 + + `for (int total = 0; total < msgs.size(); )`:将消息拆分成多个消费任务 + +* processConsumeResult():处理消费结果 + + ```java + // 参数一:消费结果状态; 参数二:消费上下文; 参数三:当前消费任务 + public void processConsumeResult(status, context, consumeRequest) + ``` + + * `switch (status)`:根据消费结果状态进行处理 + + * `case CONSUME_SUCCESS`:消费成功 + + `if (ackIndex >= consumeRequest.getMsgs().size())`:消费成功的话,ackIndex 设置成 `消费消息数 - 1` 的值,比如有 5 条消息,这里就设置为 4 + + `ok, failed`:ok 设置为消息数量,failed 设置为 0 + + * `case RECONSUME_LATER`:消费失败 + + `ackIndex = -1`:设置为 -1 + + * `switch (this.defaultMQPushConsumer.getMessageModel())`:判断消费模式,默认是**集群模式** + + * `for (int i = ackIndex + 1; i < msgs.size(); i++)`:当消费失败时 ackIndex 为 -1,i 的起始值为 0,该消费任务内的**全部消息**都会尝试回退给服务器 + + * `MessageExt msg`:提取一条消息 + + * `boolean result = this.sendMessageBack(msg, context)`:**发送消息回退,同步发送** + + * `if (!result)`:回退失败的消息,将**消息的重试属性加 1**,并加入到回退失败的集合 + + * `if (!msgBackFailed.isEmpty())`:回退失败集合不为空 + + `consumeRequest.getMsgs().removeAll(msgBackFailed)`:将回退失败的消息从当前消费任务的 msgs 集合内移除 + + `this.submitConsumeRequestLater()`:**回退失败的消息会再次提交消费任务**,延迟 5 秒钟后再次尝试消费 + +* `long offset = ...removeMessage(msgs)`:从 pq 中删除已经消费成功的消息,返回 offset + +* `this...getOffsetStore().updateOffset()`:更新消费者本地该 mq 的**消费进度** + + + +*** + + + +##### 消费请求 + +ConsumeRequest 是 ConsumeMessageConcurrentlyService 的内部类,是一个 Runnable 任务对象 + +成员变量: + +* 分配到该消费任务的消息: + + ```java + private final List msgs; + ``` + +* 消息队列: + + ```java + private final ProcessQueue processQueue; // 消息处理队列 + private final MessageQueue messageQueue; // 消息队列 + ``` + +核心方法: + +* run():执行任务 + + ```java + public void run() + ``` + + * `if (this.processQueue.isDropped())`:条件成立说明该 queue 经过 rbl 算法分配到其他的 consumer + * `MessageListenerConcurrently listener`:获取消息监听器 + * `ConsumeConcurrentlyContext context`:创建消费上下文对象 + * `defaultMQPushConsumerImpl.resetRetryAndNamespace()`:重置重试标记 + * `final String groupTopic`:获取当前消费者组的重试主题 `%RETRY%GroupName` + * `for (MessageExt msg : msgs)`:遍历所有的消息 + * `String retryTopic = msg.getProperty(...)`:原主题,一般消息没有该属性,只有被重复消费的消息才有 + * `if (retryTopic != null && groupTopic.equals(...))`:条件成立说明该消息是被重复消费的消息 + * `msg.setTopic(retryTopic)`:将被**重复消费的消息主题修改回原主题** + * `if (ConsumeMessageConcurrentlyService...hasHook())`:前置处理 + * `boolean hasException = false`:消费过程中,是否向外抛出异常 + * `MessageAccessor.setConsumeStartTimeStamp()`:给每条消息设置消费开始时间 + * `status = listener.consumeMessage(Collections.unmodifiableList(msgs), context)`:**消费消息** + * `if (ConsumeMessageConcurrentlyService...hasHook())`:后置处理 + * `...processConsumeResult(status, context, this)`:**处理消费结果** + + + +**** + + + +#### 顺序消费 + +##### 成员属性 + +ConsumeMessageOrderlyService 负责顺序消费服务 + +成员变量: + +* 消息监听器:封装处理消息的逻辑,该监听器由开发者实现,并注册到 defaultMQPushConsumer + + ```java + private final MessageListenerOrderly messageListener; + ``` + +* 消费属性: + + ```java + private final BlockingQueue consumeRequestQueue; // 消费任务队列 + private final String consumerGroup; // 消费者组 + private volatile boolean stopped = false; // 消费停止状态 + ``` + +* 线程池: + + ```java + private final ThreadPoolExecutor consumeExecutor; // 消费任务线程池 + private final ScheduledExecutorService scheduledExecutorService;// 调度线程池,延迟提交消费任务 + ``` + +* 队列锁:消费者本地 MQ 锁,**确保本地对于需要顺序消费的 MQ 同一时间只有一个任务在执行** + + ```java + private final MessageQueueLock messageQueueLock = new MessageQueueLock(); + ``` + + ```java + public class MessageQueueLock { + private ConcurrentMap mqLockTable = new ConcurrentHashMap(); + // 获取本地队列锁对象 + public Object fetchLockObject(final MessageQueue mq) { + Object objLock = this.mqLockTable.get(mq); + if (null == objLock) { + objLock = new Object(); + Object prevLock = this.mqLockTable.putIfAbsent(mq, objLock); + if (prevLock != null) { + objLock = prevLock; + } + } + return objLock; + } + } + ``` + + 已经获取了 Broker 端该 Queue 的独占锁,为什么还要获取本地队列锁对象?(这里我也没太懂,先记录下来,本地多线程?) + + * Broker queue 占用锁的角度是 Client 占用,Client 从 Broker 的某个占用了锁的 queue 拉取下来消息以后,将消息存储到消费者本地的 ProcessQueue 中,快照对象的 consuming 属性置为 true,表示本地的队列正在消费处理中 + * ProcessQueue 调用 takeMessages 方法时会获取下一批待处理的消息,获取不到会修改 `consuming = false`,本消费任务马上停止。 + * 如果此时 Pull 再次拉取一批当前 ProcessQueue 的 msg,会再次向顺序消费服务提交消费任务,此时需要本地队列锁对象同步本地线程 + + + +*** + + + +##### 成员方法 + +* start():启动消费服务,DefaultMQPushConsumerImpl 启动时会调用该方法 + + ```java + public void start() + ``` + + * `this.scheduledExecutorService.scheduleAtFixedRate()`:提交锁续约任务,延迟 1 秒执行,周期为 20 秒钟 + * `ConsumeMessageOrderlyService.this.lockMQPeriodically()`:**锁续约任务** + * `this.defaultMQPushConsumerImpl.getRebalanceImpl().lockAll()`:对消费者的所有队列进行续约 + +* submitConsumeRequest():**提交消费任务请求** + + ```java + // 参数:true 表示创建消费任务并提交,false不创建消费任务,说明消费者本地已经有消费任务在执行了 + public void submitConsumeRequest(...., final boolean dispathToConsume) { + if (dispathToConsume) { + // 当前进程内不存在 顺序消费任务,创建新的消费任务,【提交到消费任务线程池】 + ConsumeRequest consumeRequest = new ConsumeRequest(processQueue, messageQueue); + this.consumeExecutor.submit(consumeRequest); + } + } + ``` + +* processConsumeResult():消费结果处理 + + ```java + // 参数1:msgs 本轮循环消费的消息集合 参数2:status 消费状态 + // 参数3:context 消费上下文 参数4:消费任务 + // 返回值:boolean 决定是否继续循环处理pq内的消息 + public boolean processConsumeResult(final List msgs, final ConsumeOrderlyStatus status, final ConsumeOrderlyContext context, final ConsumeRequest consumeRequest) + ``` + + * `if (context.isAutoCommit()) `:默认自动提交 + + * `switch (status)`:根据消费状态进行不同的处理 + + * `case SUCCESS`:消费成功 + + `commitOffset = ...commit()`:调用 pq 提交方法,会将本次循环处理的消息从顺序消费 map 删除,并且返回消息进度 + + * `case SUSPEND_CURRENT_QUEUE_A_MOMENT`:挂起当前队列 + + `consumeRequest.getProcessQueue().makeMessageToConsumeAgain(msgs)`:**回滚消息** + + * `for (MessageExt msg : msgs)`:遍历所有的消息 + * `this.consumingMsgOrderlyTreeMap.remove(msg.getQueueOffset())`:从顺序消费临时容器中移除 + * `this.msgTreeMap.put(msg.getQueueOffset(), msg)`:添加到消息容器 + + * `this.submitConsumeRequestLater()`:再次提交消费任务,1 秒后执行 + + * `continueConsume = false`:设置为 false,**外层会退出本次的消费任务** + + * `this.defaultMQPushConsumerImpl.getOffsetStore().updateOffset(...)`:更新本地消费进度 + + + +**** + + + +##### 消费请求 + +ConsumeRequest 是 ConsumeMessageOrderlyService 的内部类,是一个 Runnable 任务对象 + +核心方法: + +* run():执行任务 + + ```java + public void run() + ``` + + * `final Object objLock`:获取本地锁对象 + + * `synchronized (objLock)`:本地队列锁,确保每个 MQ 的消费任务只有一个在执行,**确保顺序消费** + + * `if(.. || (this.processQueue.isLocked() && !this.processQueue.isLockExpired())))`:当前队列持有分布式锁,并且锁未过期,持锁时间超过 30 秒算过期 + + * `final long beginTime`:消费开始时间 + + * `for (boolean continueConsume = true; continueConsume; )`:根据是否继续消费的标记判断是否继续 + + * `final int consumeBatchSize`:获取每次循环处理的消息数量,一般是 1 + + * `List msgs = this...takeMessages(consumeBatchSize)`:到**处理队列获取一批消息** + + * `if (!msgs.isEmpty())`:获取到了待消费的消息 + + `final ConsumeOrderlyContext context`:创建消费上下文对象 + + `this.processQueue.getLockConsume().lock()`:**获取 lockConsume 锁**,与 RBL 线程同步使用 + + `status = messageListener.consumeMessage(...)`:监听器处理消息 + + `this.processQueue.getLockConsume().unlock()`:**释放 lockConsume 锁** + + `if (null == status)`:处理消息状态返回 null,设置状态为挂起当前队列 + + `continueConsume = ...processConsumeResult()`:消费结果处理 + + * `else`:获取到的消息是空 + + `continueConsume = false`:结束任务循环 + + * `else`:当前队列未持有分布式锁,或者锁过期 + + `ConsumeMessageOrderlyService.this.tryLockLaterAndReconsume()`:重新提交任务,根据是否获取到队列锁,选择延迟 10 毫秒或者 300 毫秒 + + + +*** + + + +### 生产消费 + +生产流程: + +* 首先获取当前消息主题的发布信息,获取不到去 Namesrv 获取(默认有 TBW102),并将获取的到的路由数据转化为发布数据,**创建 MQ 队列在多个 Broker 组**(一组代表一主多从的 Broker 架构),客户端实例同样更新订阅数据,创建 MQ 队列,放入负载均衡服务 topicSubscribeInfoTable 中 +* 然后从发布数据中选择一个 MQ 队列发送消息 +* Broker 端通过 SendMessageProcessor 对发送的消息进行持久化处理,存储到 CommitLog。将重试次数过多的消息加入**死信队列**,将延迟消息的主题和队列修改为调度主题和调度队列 ID +* Broker 启动 ScheduleMessageService 服务会为每个延迟级别创建一个延迟任务,让延迟消息得到有效的处理,将到达交付时间的消息修改为原始主题的原始 ID 存入 CommitLog,消费者就可以进行消费了 + +消费流程: + +* 消息消费队列 ConsumerQueue 存储消息在 CommitLog 的索引,消费者通过该队列来读取消息实体内容,一个 MQ 就对应一个 CQ +* 首先通过负载均衡服务,将分配到当前消费者实例的 MQ 创建 PullRequest,并放入 PullMessageService 的本地阻塞队列内 +* PullMessageService 循环从阻塞队列获取请求对象,发起拉消息请求,并创建 PullCallback 回调对象,将正常拉取的消息**提交到消费任务线程池**,并设置请求的下一次拉取位点,重新放入阻塞队列,形成闭环 +* 消费任务服务对消费失败的消息进行回退,通过内部生产者实例发送回退消息,回退失败的消息会再次提交消费任务重新消费 +* Broker 端对拉取消息的请求进行处理(processRequestCommand),查询成功将消息放入响应体,通过 Netty 写回客户端,当 `pullRequest.offset == queue.maxOffset` 说明该队列已经没有需要获取的消息,将请求放入长轮询集合等待有新消息 +* PullRequestHoldService 负责长轮询,每 5 秒遍历一次长轮询集合,将满足条件的 PullRequest 再次提交到线程池内处理 + + + + + + + +*** + + + + + + + +# Zookeeper + +## 基本介绍 + +### 框架特征 + +Zookeeper 是 Apache Hadoop 项目子项目,为分布式框架提供协调服务,是一个树形目录服务 + +Zookeeper 是基于观察者模式设计的分布式服务管理框架,负责存储和管理共享数据,接受观察者的注册监控,一旦这些数据的状态发生变化,Zookeeper 会通知观察者 + +* Zookeeper 是一个领导者(Leader),多个跟随者(Follower)组成的集群 +* 集群中只要有半数以上节点存活就能正常服务,所以 Zookeeper 适合部署奇数台服务器 +* **全局数据一致**,每个 Server 保存一份相同的数据副本,Client 无论连接到哪个 Server,数据都是一致 +* 更新的请求顺序执行,来自同一个 Client 的请求按其发送顺序依次执行 +* **数据更新原子性**,一次数据更新要么成功,要么失败 +* 实时性,在一定的时间范围内,Client 能读到最新数据 +* 心跳检测,会定时向各个服务提供者发送一个请求(实际上建立的是一个 Socket 长连接) + +![](https://seazean.oss-cn-beijing.aliyuncs.com/img/Frame/Zookeeper-框架结构.png) + + + +参考视频:https://www.bilibili.com/video/BV1to4y1C7gw + + + + + +*** + + + +### 应用场景 + +Zookeeper 提供的主要功能包括:统一命名服务、统一配置管理、统一集群管理、服务器节点动态上下线、软负载均衡、分布式锁等 + +* 在分布式环境中,经常对应用/服务进行统一命名,便于识别,例如域名相对于 IP 地址更容易被接收 + + ```sh + /service/www.baidu.com # 节点路径 + 192.168.1.1 192.168.1.2 # 节点值 + ``` + + 如果在节点中记录每台服务器的访问数,让访问数最少的服务器去处理最新的客户端请求,可以实现负载均衡 + + ```sh + 192.168.1.1 10 # 次数 + 192.168.1.1 15 + ``` + +* 配置文件同步可以通过 Zookeeper 实现,将配置信息写入某个 ZNode,其他客户端监视该节点,当节点数据被修改,通知各个客户端服务器 + +* 集群环境中,需要实时掌握每个集群节点的状态,可以将这些信息放入 ZNode,通过监控通知的机制实现 + +* 实现客户端实时观察服务器上下线的变化,通过心跳检测实现 + + + + + +*** + + + + + +## 基本操作 + +### 安装搭建 + +安装步骤: + +* 安装 JDK + +* 拷贝 apache-zookeeper-3.5.7-bin.tar.gz 安装包到 Linux 系统下,并解压到指定目录 + +* conf 目录下的配置文件重命名: + + ``` + mv zoo_sample.cfg zoo.cfg + ``` + +* 修改配置文件: + + ```sh + vim zoo.cfg + # 修改内容 + dataDir=/home/seazean/SoftWare/zookeeper-3.5.7/zkData + ``` + +* 在对应目录创建 zkData 文件夹: + + ```sh + mkdir zkData + ``` + +Zookeeper 中的配置文件 zoo.cfg 中参数含义解读: + +* tickTime = 2000:通信心跳时间,**Zookeeper 服务器与客户端心跳**时间,单位毫秒 +* initLimit = 10:Leader 与 Follower 初始通信时限,初始连接时能容忍的最多心跳次数 +* syncLimit = 5:Leader 与 Follower 同步通信时限,LF 通信时间超过 `syncLimit * tickTime`,Leader 认为 Follwer 下线 +* dataDir:保存 Zookeeper 中的数据目录,默认是 tmp目录,容易被 Linux 系统定期删除,所以建议修改 +* clientPort = 2181:客户端连接端口,通常不做修改 + + + +*** + + + +### 操作命令 + +#### 服务端 + +Linux 命令: + +* 启动 ZooKeeper 服务:`./zkServer.sh start` + +* 查看 ZooKeeper 服务:`./zkServer.sh status` + +* 停止 ZooKeeper 服务:`./zkServer.sh stop` + +* 重启 ZooKeeper 服务:`./zkServer.sh restart ` + +* 查看进程是否启动:`jps` + + + + + +*** + + + +#### 客户端 + +Linux 命令: + +* 连接 ZooKeeper 服务端: + + ```sh + ./zkCli.sh # 直接启动 + ./zkCli.sh –server ip:port # 指定 host 启动 + ``` + +客户端命令: + +* 基础操作: + + ```sh + quit # 停止连接 + help # 查看命令帮助 + ``` + +* 创建命令:**`/` 代表根目录** + + ```sh + create /path value # 创建节点,value 可选 + create -e /path value # 创建临时节点 + create -s /path value # 创建顺序节点 + create -es /path value # 创建临时顺序节点,比如node10000012 删除12后也会继续从13开始,只会增加 + ``` + +* 查询命令: + + ```sh + ls /path # 显示指定目录下子节点 + ls –s /path # 查询节点详细信息 + ls –w /path # 监听子节点数量的变化 + stat /path # 查看节点状态 + get –s /path # 查询节点详细信息 + get –w /path # 监听节点数据的变化 + ``` + + ```sh + # 属性,分为当前节点的属性和子节点属性 + czxid: 节点被创建的事务ID, 是ZooKeeper中所有修改总的次序,每次修改都有唯一的 zxid,谁小谁先发生 + ctime: 被创建的时间戳 + mzxid: 最后一次被更新的事务ID + mtime: 最后修改的时间戳 + pzxid: 子节点列表最后一次被更新的事务ID + cversion: 子节点的变化号,修改次数 + dataversion: 节点的数据变化号,数据的变化次数 + aclversion: 节点的访问控制列表变化号 + ephemeralOwner: 用于临时节点,代表节点拥有者的 session id,如果为持久节点则为0 + dataLength: 节点存储的数据的长度 + numChildren: 当前节点的子节点数量 + ``` + +* 删除命令: + + ```sh + delete /path # 删除节点 + deleteall /path # 递归删除节点 + ``` + + + +*** + + + +### 数据结构 + +ZooKeeper 是一个树形目录服务,类似 Unix 的文件系统,每一个节点都被称为 ZNode,每个 ZNode 默认存储 1MB 的数据,节点上会保存数据和节点信息,每个 ZNode 都可以通过其路径唯一标识 + +节点可以分为四大类: + +* PERSISTENT:持久化节点 +* EPHEMERAL:临时节点,客户端和服务器端**断开连接**后,创建的节点删除 +* PERSISTENT_SEQUENTIAL:持久化顺序节点,创建 znode 时设置顺序标识,节点名称后会附加一个值,**顺序号是一个单调递增的计数器**,由父节点维护 +* EPHEMERAL_SEQUENTIAL:临时顺序节点 + +注意:在分布式系统中,顺序号可以被用于为所有的事件进行全局排序,这样客户端可以通过顺序号推断事件的顺序 + +![](https://seazean.oss-cn-beijing.aliyuncs.com/img/Frame/Zookeeper-节点树形结构.png) + + + +*** + + + +### 代码实现 + +添加 Maven 依赖: + +```xml + + org.apache.zookeeper + zookeeper + 3.5.7 + +``` + +实现代码: + +```java +public static void main(String[] args) { + // 参数一:连接地址 + // 参数二:会话超时时间 + // 参数三:监听器 + ZooKeeper zkClient = new ZooKeeper("192.168.3.128:2181", 20000, new Watcher() { + @Override + public void process(WatchedEvent event) { + System.out.println("监听处理函数"); + } + }); +} +``` + + + + + + + +*** + + + + + +## 集群介绍 + +### 相关概念 + +Zookeepe 集群三个角色: + +* Leader 领导者:处理客户端**事务请求**,负责集群内部各服务器的调度 + +* Follower 跟随者:处理客户端非事务请求,转发事务请求给 Leader 服务器,参与 Leader 选举投票 + +* Observer 观察者:观察集群的最新状态的变化,并将这些状态进行同步;处理非事务性请求,事务性请求会转发给 Leader 服务器进行处理;不会参与任何形式的投票。只提供非事务性的服务,通常用于在不影响集群事务处理能力的前提下,提升集群的非事务处理能力(提高集群读的能力,但是也降低了集群选主的复杂程度) + + +相关属性: + +* SID:服务器 ID,用来唯一标识一台集群中的机器,和 myid 一致 +* ZXID:事务 ID,用来标识一次服务器状态的变更,在某一时刻集群中每台机器的 ZXID 值不一定完全一致,这和 ZooKeeper 服务器对于客户端更新请求的处理逻辑有关 + +* Epoch:每个 Leader 任期的代号,同一轮选举投票过程中的该值是相同的,投完一次票就增加 + +选举机制:半数机制,超过半数的投票就通过 + +* 第一次启动选举规则:投票过半数时,服务器 ID 大的胜出 + +* 第二次启动选举规则: + * EPOCH 大的直接胜出 + * EPOCH 相同,事务 ID 大的胜出(事务 ID 越大,数据越新) + * 事务 ID 相同,服务器 ID 大的胜出 + + + + + +*** + + + +### 初次选举 + +选举过程: + +* 服务器 1 启动,发起一次选举,服务器 1 投自己一票,票数不超过半数,选举无法完成,服务器 1 状态保持为 LOOKING +* 服务器 2 启动,再发起一次选举,服务器 1 和 2 分别投自己一票并**交换选票信息**,此时服务器 1 会发现服务器 2 的 SID 比自己投票推举的(服务器 1)大,更改选票为推举服务器 2。投票结果为服务器 1 票数 0 票,服务器 2 票数 2 票,票数不超过半数,选举无法完成,服务器 1、2 状态保持 LOOKING +* 服务器 3 启动,发起一次选举,此时服务器 1 和 2 都会更改选票为服务器 3,投票结果为服务器 3 票数 3 票,此时服务器 3 的票数已经超过半数,服务器 3 当选 Leader,服务器 1、2 更改状态为 FOLLOWING,服务器 3 更改状态为 LEADING +* 服务器 4 启动,发起一次选举,此时服务器 1、2、3 已经不是 LOOKING 状态,不会更改选票信息,交换选票信息结果后服务器 3 为 3 票,服务器 4 为 1 票,此时服务器 4 更改选票信息为服务器 3,并更改状态为 FOLLOWING +* 服务器 5 启动,同 4 一样 + +![](https://seazean.oss-cn-beijing.aliyuncs.com/img/Frame/Zookeeper-初次选举机制.png) + + + +*** + + + +### 再次选举 + +ZooKeeper 集群中的一台服务器出现以下情况之一时,就会开始进入 Leader 选举: + +* 服务器初始化启动 +* 服务器运行期间无法和 Leader 保持连接 + +当一台服务器进入 Leader 选举流程时,当前集群可能会处于以下两种状态: + +* 集群中本来就已经存在一个 Leader,服务器试图去选举 Leader 时会被告知当前服务器的 Leader 信息,对于该服务器来说,只需要和 Leader 服务器建立连接,并进行状态同步即可 + +* 集群中确实不存在 Leader,假设服务器 3 和 5 出现故障,开始进行 Leader 选举,SID 为 1、2、4 的机器投票情况 + + ```sh + (EPOCH,ZXID,SID): (1, 8, 1), (1, 8, 2), (1, 7, 4) + ``` + + 根据选举规则,服务器 2 胜出 + + + +*** + + + +### 数据写入 + +写操作就是事务请求,写入请求直接发送给 Leader 节点:Leader 会先将数据写入自身,同时通知其他 Follower 写入,**当集群中有半数以上节点写入完成**,Leader 节点就会响应客户端数据写入完成 + + + +写入请求直接发送给 Follower 节点:Follower 没有写入权限,会将写请求转发给 Leader,Leader 将数据写入自身,通知其他 Follower 写入,当集群中有半数以上节点写入完成,Leader 会通知 Follower 写入完成,**由 Follower 响应客户端数据写入完成** + + + + + + + +**** + + + + + +## 底层协议 + +### Paxos + +Paxos 算法:基于消息传递且具有高度容错特性的一致性算法 + +优点:快速正确的在一个分布式系统中对某个数据值达成一致,并且保证不论发生任何异常,都不会破坏整个系统的一致性 + +缺陷:在网络复杂的情况下,可能很久无法收敛,甚至陷入活锁的情况 + + + +*** + + + +### ZAB + +#### 算法介绍 + +ZAB 协议借鉴了 Paxos 算法,是为 Zookeeper 设计的支持崩溃恢复的原子广播协议,基于该协议 Zookeeper 设计为只有一台客户端(Leader)负责处理外部的写事务请求,然后 Leader 将数据同步到其他 Follower 节点 + +Zab 协议包括两种基本的模式:消息广播、崩溃恢复 + + + +*** + + + +#### 消息广播 + +ZAB 协议针对事务请求的处理过程类似于一个**两阶段提交**过程:广播事务阶段、广播提交操作 + +* 客户端发起写操作请求,Leader 服务器将请求转化为事务 Proposal 提案,同时为 Proposal 分配一个全局的 ID,即 ZXID +* Leader 服务器为每个 Follower 分配一个单独的队列,将广播的 Proposal **依次放到队列**中去,根据 FIFO 策略进行消息发送 +* Follower 接收到 Proposal 后,将其以事务日志的方式写入本地磁盘中,写入成功后向 Leader 反馈一个 ACK 响应消息 +* Leader 接收到超过半数以上 Follower 的 ACK 响应消息后,即认为消息发送成功,可以发送 Commit 消息 +* Leader 向所有 Follower 广播 commit 消息,同时自身也会完成事务提交,Follower 接收到 Commit 后,将上一条事务提交 + + + +两阶段提交模型可能因为 Leader 宕机带来数据不一致: + +* Leader 发起一个事务 Proposal 后就宕机,Follower 都没有 Proposal +* Leader 收到半数 ACK 宕机,没来得及向 Follower 发送 Commit + + + +*** + + + +#### 崩溃恢复 + +Leader 服务器出现崩溃或者由于网络原因导致 Leader 服务器失去了与**过半 Follower的联系**,那么就会进入崩溃恢复模式,崩溃恢复主要包括两部分:Leader 选举和数据恢复 + +Zab 协议崩溃恢复要求满足以下两个要求: + +* 已经被 Leader 提交的提案 Proposal,必须最终被所有的 Follower 服务器正确提交 +* 丢弃已经被 Leader 提出的,但是没有被提交的 Proposal + +Zab 协议需要保证选举出来的 Leader 需要满足以下条件: + +* 新选举的 Leader 不能包含未提交的 Proposal,即新 Leader 必须都是已经提交了 Proposal 的 Follower 节点 +* 新选举的 Leader 节点含有**最大的 ZXID**,可以避免 Leader 服务器检查 Proposal 的提交和丢弃工作 + + + +数据恢复阶段: + +* 完成 Leader 选举后,在正式开始工作之前(接收事务请求提出新的 Proposal),Leader 服务器会首先确认事务日志中的所有 Proposal 是否已经被集群中过半的服务器 Commit +* Leader 服务器需要确保所有的 Follower 服务器能够接收到每一条事务的 Proposal,并且能将所有已经提交的事务 Proposal 应用到内存数据中,所以只有当 Follower 将所有尚未同步的事务 Proposal 都**从 Leader 服务器上同步**,并且应用到内存数据后,Leader 才会把该 Follower 加入到真正可用的 Follower 列表中 + + + +**** + + + +#### 异常处理 + +Zab 的事务编号 zxid 设计: + +* zxid 是一个 64 位的数字,低 32 位是一个简单的单增计数器,针对客户端每一个事务请求,Leader 在产生新的 Proposal 事务时,都会对该计数器加 1,而高 32 位则代表了 Leader 周期的 epoch 编号 +* epoch 为当前集群所处的代或者周期,每次 Leader 变更后都会在 epoch 的基础上加 1,Follower 只服从 epoch 最高的 Leader 命令,所以旧的 Leader 崩溃恢复之后,其他 Follower 就不会继续追随 +* 每次选举产生一个新的 Leader,就会从新 Leader 服务器上取出本地事务日志中最大编号 Proposal 的 zxid,从 zxid 中解析得到对应的 epoch 编号,然后再对其加 1 后作为新的 epoch 值,并将低 32 位数字归零,由 0 开始重新生成 zxid + +Zab 协议通过 epoch 编号来区分 Leader 变化周期,能够有效避免不同的 Leader 错误的使用了相同的 zxid 编号提出了不一样的 Proposal 的异常情况 + +Zab 数据同步过程:**数据同步阶段要以 Leader 服务器为准** + +* 一个包含了上个 Leader 周期中尚未提交过的事务 Proposal 的服务器启动时,这台机器加入集群中会以 Follower 角色连上 Leader +* Leader 会根据自己服务器上最后提交的 Proposal 和 Follower 服务器的 Proposal 进行比对,让 Follower 进行一个**回退或者前进操作**,到一个已经被集群中过半机器 Commit 的最新 Proposal(源码解析部分详解) + + + + + +*** + + + +### CAP + +CAP 理论指的是在一个分布式系统中,Consistency(一致性)、Availability(可用性)、Partition Tolerance(分区容错性)不能同时成立,ZooKeeper 保证的是 CP + +* ZooKeeper 不能保证每次服务请求的可用性,在极端环境下可能会丢弃一些请求,消费者程序需要重新请求才能获得结果 +* 进行 Leader 选举时**集群都是不可用** + +CAP 三个基本需求,因为 P 是必须的,因此分布式系统选择就在 CP 或者 AP 中: + +* 一致性:指数据在多个副本之间是否能够保持数据一致的特性,当一个系统在数据一致的状态下执行更新操作后,也能保证系统的数据仍然处于一致的状态 +* 可用性:指系统提供的服务必须一直处于可用的状态,即使集群中一部分节点故障,对于用户的每一个操作请求总是能够在有限的时间内返回结果 +* 分区容错性:分布式系统在遇到任何网络分区故障时,仍然能够保证对外提供服务,不会宕机,除非是整个网络环境都发生了故障 + + + + + + + +*** + + + + + +## 监听机制 + +### 实现原理 + +ZooKeeper 中引入了 Watcher 机制来实现了发布/订阅功能,客户端注册监听目录节点,在特定事件触发时,ZooKeeper 会通知所有关注该事件的客户端,保证 ZooKeeper 保存的任何的数据的任何改变都能快速的响应到监听应用程序 + +监听命令:**只能生效一次**,接收一次通知,再次监听需要重新注册 + +```sh +ls –w /path # 监听【子节点数量】的变化 +get –w /path # 监听【节点数据】的变化 +``` + +工作流程: + +* 在主线程中创建 Zookeeper 客户端,这时就会创建**两个线程**,一个负责网络连接通信(connet),一个负责监听(listener) +* 通过 connect 线程将注册的监听事件发送给 Zookeeper +* 在 Zookeeper 的注册监听器列表中将注册的**监听事件添加到列表**中 +* Zookeeper 监听到有数据或路径变化,将消息发送给 listener 线程 +* listener 线程内部调用 process() 方法 + +Curator 框架引入了 Cache 来实现对 ZooKeeper 服务端事件的监听,三种 Watcher: + +* NodeCache:只是监听某一个特定的节点 +* PathChildrenCache:监控一个 ZNode 的子节点 +* TreeCache:可以监控整个树上的所有节点,类似于 PathChildrenCache 和 NodeCache 的组合 + + + + + +*** + + + +### 监听案例 + +#### 整体架构 + +客户端实时监听服务器动态上下线 + + + + + +*** + + + +#### 代码实现 + +客户端:先启动客户端进行监听 + +```java +public class DistributeClient { + private String connectString = "192.168.3.128:2181"; + private int sessionTimeout = 20000; + private ZooKeeper zk; + + public static void main(String[] args) throws Exception { + DistributeClient client = new DistributeClient(); + + // 1 获取zk连接 + client.getConnect(); + + // 2 监听/servers下面子节点的增加和删除 + client.getServerList(); + + // 3 业务逻辑 + client.business(); + } + + private void business() throws InterruptedException { + Thread.sleep(Long.MAX_VALUE); + } + + private void getServerList() throws KeeperException, InterruptedException { + ArrayList servers = new ArrayList<>(); + // 获取所有子节点,true 代表触发监听操作 + List children = zk.getChildren("/servers", true); + + for (String child : children) { + // 获取子节点的数据 + byte[] data = zk.getData("/servers/" + child, false, null); + servers.add(new String(data)); + } + System.out.println(servers); + } + + private void getConnect() throws IOException { + zk = new ZooKeeper(connectString, sessionTimeout, new Watcher() { + @Override + public void process(WatchedEvent event) { + getServerList(); + } + }); + } +} +``` + +服务端:启动时需要 Program arguments + +```java +public class DistributeServer { + private String connectString = "192.168.3.128:2181"; + private int sessionTimeout = 20000; + private ZooKeeper zk; + + public static void main(String[] args) throws Exception { + DistributeServer server = new DistributeServer(); + + // 1 获取 zookeeper 连接 + server.getConnect(); + + // 2 注册服务器到 zk 集群,注意参数 + server.register(args[0]); + + // 3 启动业务逻辑 + server.business(); + } + + private void business() throws InterruptedException { + Thread.sleep(Long.MAX_VALUE); + } + + private void register(String hostname) throws KeeperException, InterruptedException { + // OPEN_ACL_UNSAFE: ACL 开放 + // EPHEMERAL_SEQUENTIAL: 临时顺序节点 + String create = zk.create("/servers/" + hostname, hostname.getBytes(), + ZooDefs.Ids.OPEN_ACL_UNSAFE, CreateMode.EPHEMERAL_SEQUENTIAL); + System.out.println(hostname + " is online"); + } + + private void getConnect() throws IOException { + zk = new ZooKeeper(connectString, sessionTimeout, new Watcher() { + @Override + public void process(WatchedEvent event) { + } + }); + } +} +``` + + + + + +*** + + + + + +## 分布式锁 + +### 实现原理 + +分布式锁可以实现在分布式系统中多个进程有序的访问该临界资源,多个进程之间不会相互干扰 + +核心思想:当客户端要获取锁,则创建节点,使用完锁,则删除该节点 + +1. 客户端获取锁时,在 /locks 节点下创建**临时顺序**节点 + * 使用临时节点是为了防止当服务器或客户端宕机以后节点无法删除(持久节点),导致锁无法释放 + * 使用顺序节点是为了系统自动编号排序,找最小的节点,防止客户端饥饿现象,保证公平 +2. 获取 /locks 目录的所有子节点,判断自己的**子节点序号是否最小**,成立则客户端获取到锁,使用完锁后将该节点删除 + +3. 反之客户端需要找到比自己小的节点,**对其注册事件监听器,监听删除事件** +4. 客户端的 Watcher 收到删除事件通知,就会重新判断当前节点是否是子节点中序号最小,如果是则获取到了锁, 如果不是则重复以上步骤继续获取到比自己小的一个节点并注册监听 + +![](https://seazean.oss-cn-beijing.aliyuncs.com/img/Frame/Zookeeper-分布式锁原理.png) + + + +*** + + + +### Curator + +Curator 实现分布式锁 API,在 Curator 中有五种锁方案: + +- InterProcessSemaphoreMutex:分布式排它锁(非可重入锁) + +- InterProcessMutex:分布式可重入排它锁 + +- InterProcessReadWriteLock:分布式读写锁 + +- InterProcessMultiLock:将多个锁作为单个实体管理的容器 + +- InterProcessSemaphoreV2:共享信号量 + +```java +public class CuratorLock { + + public static CuratorFramework getCuratorFramework() { + // 重试策略对象 + ExponentialBackoffRetry policy = new ExponentialBackoffRetry(3000, 3); + // 构建客户端 + CuratorFramework client = CuratorFrameworkFactory.builder() + .connectString("192.168.3.128:2181") + .connectionTimeoutMs(2000) // 连接超时时间 + .sessionTimeoutMs(20000) // 会话超时时间 单位ms + .retryPolicy(policy) // 重试策略 + .build(); + + // 启动客户端 + client.start(); + System.out.println("zookeeper 启动成功"); + return client; + } + + public static void main(String[] args) { + // 创建分布式锁1 + InterProcessMutex lock1 = new InterProcessMutex(getCuratorFramework(), "/locks"); + + // 创建分布式锁2 + InterProcessMutex lock2 = new InterProcessMutex(getCuratorFramework(), "/locks"); + + new Thread(new Runnable() { + @Override + public void run() { + lock1.acquire(); + System.out.println("线程1 获取到锁"); + + Thread.sleep(5 * 1000); + + lock1.release(); + System.out.println("线程1 释放锁"); + } + }).start(); + + new Thread(new Runnable() { + @Override + public void run() { + lock2.acquire(); + System.out.println("线程2 获取到锁"); + + Thread.sleep(5 * 1000); + + lock2.release(); + System.out.println("线程2 释放锁"); + + } + }).start(); + } +} +``` + +```xml + + org.apache.curator + curator-framework + 4.3.0 + + + org.apache.curator + curator-recipes + 4.3.0 + + + org.apache.curator + curator-client + 4.3.0 +``` + + + + + +*** + + + + + +## 源码解析 + +### 服务端 + +服务端程序的入口 QuorumPeerMain + +```java +public static void main(String[] args) { + QuorumPeerMain main = new QuorumPeerMain(); + main.initializeAndRun(args); +} +``` + +initializeAndRun 的工作: + +* 解析启动参数 + +* 提交周期任务,定时删除过期的快照 + +* 初始化通信模型,默认是 NIO 通信 + + ```java + // QuorumPeerMain#runFromConfig + public void runFromConfig(QuorumPeerConfig config) { + // 通信信组件初始化,默认是 NIO 通信 + ServerCnxnFactory cnxnFactory = ServerCnxnFactory.createFactory(); + // 初始化NIO 服务端socket,绑定2181 端口,可以接收客户端请求 + cnxnFactory.configure(config.getClientPortAddress(), config.getMaxClientCnxns(), false); + // 启动 zk + quorumPeer.start(); + } + ``` + +* 启动 zookeeper + + ```java + // QuorumPeer#start + public synchronized void start() { + if (!getView().containsKey(myid)) { + throw new RuntimeException("My id " + myid + " not in the peer list"); + } + // 冷启动数据恢复,将快照中数据恢复到 DataTree + loadDataBase(); + // 启动通信工厂实例对象 + startServerCnxnFactory(); + try { + adminServer.start(); + } catch (AdminServerException e) { + LOG.warn("Problem starting AdminServer", e); + System.out.println(e); + } + // 准备选举环境 + startLeaderElection(); + // 执行选举 + super.start(); + } + ``` + + + + + +*** + + + +### 选举机制 + +#### 环境准备 + +QuorumPeer#startLeaderElection 初始化选举环境: + +```java +synchronized public void startLeaderElection() { + try { + // Looking 状态,需要选举 + if (getPeerState() == ServerState.LOOKING) { + // 选票组件: myid (serverid), zxid, epoch + // 开始选票时,serverid 是自己,【先投自己】 + currentVote = new Vote(myid, getLastLoggedZxid(), getCurrentEpoch()); + } + } + if (electionType == 0) { + try { + udpSocket = new DatagramSocket(getQuorumAddress().getPort()); + // 响应投票结果线程 + responder = new ResponderThread(); + responder.start(); + } catch (SocketException e) { + throw new RuntimeException(e); + } + } + // 创建选举算法实例 + this.electionAlg = createElectionAlgorithm(electionType); +} +``` + +```java +// zk总的发送和接收队列准备好 +protected Election createElectionAlgorithm(int electionAlgorithm){ + // 负责选举过程中的所有网络通信,创建各种队列和集合 + QuorumCnxManager qcm = createCnxnManager(); + QuorumCnxManager.Listener listener = qcm.listener; + if(listener != null){ + // 启动监听线程, 调用 client = ss.accept()阻塞,等待处理请求 + listener.start(); + // 准备好发送和接收队列准备 + FastLeaderElection fle = new FastLeaderElection(this, qcm); + // 启动选举线程,【WorkerSender 和 WorkerReceiver】 + fle.start(); + le = fle; + } +} +``` + + + +*** + + + +#### 选举源码 + +当 Zookeeper 启动后,首先都是 Looking 状态,通过选举让其中一台服务器成为 Leader + +执行 `super.start()` 相当于执行 `QuorumPeer#run()` 方法 + +```java +public void run() { + case LOOKING: + // 进行选举,选举结束返回最终成为 Leader 胜选的那张选票 + setCurrentVote(makeLEStrategy().lookForLeader()); +} +``` + +FastLeaderElection 类: + +* lookForLeader:选举 + + ```java + public Vote lookForLeader() { + // 正常启动中其他服务器都会向我发送一个投票,保存每个服务器的最新合法有效的投票 + HashMap recvset = new HashMap(); + // 存储合法选举之外的投票结果 + HashMap outofelection = new HashMap(); + // 一次选举的最大等待时间,默认值是0.2s + int notTimeout = finalizeWait; + // 每发起一轮选举,logicalclock++,在没有合法的epoch 数据之前,都使用逻辑时钟代替 + synchronized(this){ + // 更新逻辑时钟,每进行一次选举,都需要更新逻辑时钟 + logicalclock.incrementAndGet(); + // 更新选票(serverid, zxid, epoch) + updateProposal(getInitId(), getInitLastLoggedZxid(), getPeerEpoch()); + } + // 广播选票,把自己的选票发给其他服务器 + sendNotifications(); + // 一轮一轮的选举直到选举成功 + while ((self.getPeerState() == ServerState.LOOKING) && (!stop)){ } + } + ``` + +* sendNotifications:广播选票 + + ```java + private void sendNotifications() { + // 遍历投票参与者,给每台服务器发送选票 + for (long sid : self.getCurrentAndNextConfigVoters()) { + // 创建发送选票 + ToSend notmsg = new ToSend(...); + // 把发送选票放入发送队列 + sendqueue.offer(notmsg); + } + } + ``` + +FastLeaderElection 中有 WorkerSender 线程: + +* `ToSend m = sendqueue.poll(3000, TimeUnit.MILLISECONDS)`:**阻塞获取要发送的选票** + +* `process(m)`:处理要发送的选票 + + `manager.toSend(m.sid, requestBuffer)`:发送选票 + + * `if (this.mySid == sid)`:如果**消息的接收者 sid 是自己**,直接进入自己的 RecvQueue(自己投自己) + + * `else`:如果接收者是其他服务器,创建对应的发送队列或者复用已经存在的发送队列,把消息放入该队列 + + * `connectOne(sid)`:建立连接 + + * `sock.connect(electionAddr, cnxTO)`:建立与 sid 服务器的连接 + + * `initiateConnection(sock, sid)`:初始化连接 + + `startConnection(sock, sid)`:创建并启动发送器线程和接收器线程 + + * `dout = new DataOutputStream(buf)`:**获取 Socket 输出流**,向服务器发送数据 + * `din = new DataInputStream(new BIS(sock.getInputStream())))`:通过输入流读取对方发送过来的选票 + * `if (sid > self.getId())`:接收者 sid 比我的大,没有资格给对方发送连接请求的,直接关闭自己的客户端 + * `SendWorker sw`:初始化发送器,并启动发送器线程,线程 run 方法 + * `while (running && !shutdown && sock != null)`:连接没有断开就一直运行 + * `ByteBuffer b = pollSendQueue()`:从发送队列 SendQueue 中获取发送消息 + * `lastMessageSent.put(sid, b)`:更新对于 sid 这台服务器的最近一条消息 + * `send(b)`:**执行发送** + * `RecvWorker rw`:初始化接收器,并启动接收器线程 + * `din.readFully(msgArray, 0, length)`:输入流接收消息 + * `addToRecvQueue(new Message(messagg, sid))`:将消息放入接收消息 recvQueue 队列 + +FastLeaderElection 中有 WorkerReceiver 线程 + +* `response = manager.pollRecvQueue()`:从 RecvQueue 中**阻塞获取出选举投票消息**(其他服务器发送过来的) + + + + + + + +*** + + + +#### 状态同步 + +选举结束后,每个节点都需要根据角色更新自己的状态,Leader 更新状态为 Leader,其他节点更新状态为 Follower,整体流程: + +* Follower 需要让 Leader 知道自己的状态 (sid, epoch, zxid) +* Leader 接收到信息,**根据信息构建新的 epoch**,要返回对应的信息给 Follower,Follower 更新自己的 epoch +* Leader 需要根据 Follower 的状态,确定何种方式的数据同步 DIFF、TRUNC、SNAP,就是要**以 Leader 服务器数据为准** + * DIFF:Leader 提交的 zxid 比 Follower 的 zxid 大,发送 Proposal 给 Follower 提交执行 + * TRUNC:Follower 的 zxid 比leader 的 zxid 大,Follower 要进行回滚 + * SNAP:Follower 没有任何数据,直接全量同步 +* 执行数据同步,当 Leader 接收到超过半数 Follower 的 Ack 之后,进入正常工作状态,集群启动完成 + + + +核心函数解析: + +* Leader 更新状态入口:`Leader.lead()` + * `zk.loadData()`:恢复数据到内存 + * `cnxAcceptor = new LearnerCnxAcceptor()`:启动通信组件 + * `s = ss.accept()`:等待其他 Follower 节点向 Leader 节点发送同步状态 + * `LearnerHandler fh `:接收到 Follower 的请求,就创建 LearnerHandler 对象 + * `fh.start()`:启动线程,通过 switch-case 语法判断接收的命令,执行相应的操作 +* Follower 更新状态入口:`Follower.followerLeader()` + * `QuorumServer leaderServer = findLeader()`:查找 Leader + * `connectToLeader(addr, hostname) `:与 Leader 建立连接 + * `long newEpochZxid = registerWithLeader(Leader.FOLLOWERINFO)`:向 Leader 注册 + + + + + +*** + + + +#### 主从工作 + +Leader:主服务的工作流程 + +![](https://seazean.oss-cn-beijing.aliyuncs.com/img/Frame/Zookeeper-Leader启动.png) + +Follower:从服务的工作流程,核心函数为 `Follower#followLeader()` + +* `readPacket(qp)`:读取信息 + +* `processPacket(qp)`:处理信息 + + ```java + protected void processPacket(QuorumPacket qp) throws Exception{ + switch (qp.getType()) { + case Leader.PING: + break; + case Leader.PROPOSAL: + break; + case Leader.COMMIT: + break; + case Leader.COMMITANDACTIVATE: + break; + case Leader.UPTODATE: + break; + case Leader.REVALIDATE: + break; + case Leader.SYNC: + break; + default: + break; + } + } + ``` + + + +*** + + + +### 客户端 + +![](https://seazean.oss-cn-beijing.aliyuncs.com/img/Frame/Zookeeper-客户端初始化.png) + + + + + + + + + + + diff --git a/src/content/posts/合集/Java.md b/src/content/posts/合集/Java.md new file mode 100644 index 0000000..74bca83 --- /dev/null +++ b/src/content/posts/合集/Java.md @@ -0,0 +1,17608 @@ +--- +title: Java笔记合集 +published: 2025-10-26 +description: '' +image: '' +tags: [Java] +category: '合集' +draft: false +lang: '' +--- + +# SE + +## 基础 + +### 数据 + +#### 变量类型 + +| | 成员变量 | 局部变量 | 静态变量 | +| :------: | :------------: | :------------------: | :-------------------------: | +| 定义位置 | 在类中,方法外 | 方法中或者方法的形参 | 在类中,方法外 | +| 初始化值 | 有默认初始化值 | 无,赋值后才能使用 | 有默认初始化值 | +| 调用方法 | 对象调用 | | 对象调用,类名调用 | +| 存储位置 | 堆中 | 栈中 | 方法区(JDK8 以后移到堆中) | +| 生命周期 | 与对象共存亡 | 与方法共存亡 | 与类共存亡 | +| 别名 | 实例变量 | | 类变量,静态成员变量 | + +静态变量只有一个,成员变量是类中的变量,局部变量是方法中的变量 + + + +初学时笔记内容参考视频:https://www.bilibili.com/video/BV1TE41177mP,随着学习的深入又增加很多知识 + + + + + +*** + + + +#### 数据类型 + +##### 基本类型 + +Java 语言提供了八种基本类型。六种数字类型(四个整数型,两个浮点型),一种字符类型,还有一种布尔型 + +**byte:** + +- byte 数据类型是 8 位、有符号的,以二进制补码表示的整数,**8 位一个字节**,首位是符号位 +- 最小值是 -128(-2^7)、最大值是 127(2^7-1) +- 默认值是 `0` +- byte 类型用在大型数组中节约空间,主要代替整数,byte 变量占用的空间只有 int 类型的四分之一 +- 例子:`byte a = 100,byte b = -50` + +**short:** + +- short 数据类型是 16 位、有符号的以二进制补码表示的整数 +- 最小值是 -32768(-2^15)、最大值是 32767(2^15 - 1) +- short 数据类型也可以像 byte 那样节省空间,一个 short 变量是 int 型变量所占空间的二分之一 +- 默认值是 `0` +- 例子:`short s = 1000,short r = -20000` + +**int:** + +- int 数据类型是 32 位 4 字节、有符号的以二进制补码表示的整数 +- 最小值是 -2,147,483,648(-2^31)、最大值是 2,147,483,647(2^31 - 1) +- 一般地整型变量默认为 int 类型 +- 默认值是 `0` +- 例子:`int a = 100000, int b = -200000` + +**long:** + +- long 数据类型是 64 位 8 字节、有符号的以二进制补码表示的整数 +- 最小值是 -9,223,372,036,854,775,808(-2^63)、最大值是 9,223,372,036,854,775,807(2^63 -1) +- 这种类型主要使用在需要比较大整数的系统上 +- 默认值是 ` 0L` +- 例子: `long a = 100000L,Long b = -200000L`,L 理论上不分大小写,但是若写成 I 容易与数字 1 混淆,不容易分辩 + +**float:** + +- float 数据类型是单精度、32 位、符合 IEEE 754 标准的浮点数 +- float 在储存大型浮点数组的时候可节省内存空间 +- 默认值是 `0.0f` +- 浮点数不能用来表示精确的值,如货币 +- 例子:`float f1 = 234.5F` + +**double:** + +- double 数据类型是双精度、64 位、符合 IEEE 754 标准的浮点数 +- 浮点数的默认类型为 double 类型 +- double 类型同样不能表示精确的值,如货币 +- 默认值是 `0.0d` +- 例子:`double d1 = 123.4` + +**boolean:** + +- boolean 数据类型表示一位的信息 +- 只有两个取值:true 和 false +- JVM 规范指出 boolean 当做 int 处理,boolean 数组当做 byte 数组处理,这样可以得出 boolean 类型单独使用占了 4 个字节,在数组中是 1 个字节 +- 默认值是 `false` +- 例子:`boolean one = true` + +**char:** + +- char 类型是一个单一的 16 位**两个字节**的 Unicode 字符 +- 最小值是 `\u0000`(即为 0) +- 最大值是 `\uffff`(即为 65535) +- char 数据类型可以**存储任何字符** +- 例子:`char c = 'A'`,`char c = '张'` + + + +**** + + + +##### 上下转型 + +* float 与 double: + + Java 不能隐式执行**向下转型**,因为这会使得精度降低,但是可以向上转型 + + ```java + //1.1字面量属于double类型,不能直接将1.1直接赋值给 float 变量,因为这是向下转型 + float f = 1.1;//报错 + //1.1f 字面量才是 float 类型 + float f = 1.1f; + ``` + + ```java + float f1 = 1.234f; + double d1 = f1; + + double d2 = 1.23; + float f2 = (float) d2;//向下转型需要强转 + ``` + + ```java + int i1 = 1245; + long l1 = i1; + + long l2 = 1234; + int i2 = (int) l2; + ``` + +* 隐式类型转换: + + 字面量 1 是 int 类型,比 short 类型精度要高,因此不能隐式地将 int 类型向下转型为 short 类型 + + 使用 += 或者 ++ 运算符会执行类型转换: + + ```java + short s1 = 1; + s1 += 1; //s1++; + //上面的语句相当于将 s1 + 1 的计算结果进行了向下转型 + s1 = (short) (s1 + 1); + ``` + + + + + +*** + + + +##### 引用类型 + +引用数据类型:类,接口,数组都是引用数据类型,又叫包装类 + +包装类的作用: + +* 包装类作为类首先拥有了 Object 类的方法 +* 包装类作为引用类型的变量可以**存储 null 值** + + +```java +基本数据类型 包装类(引用数据类型) +byte Byte +short Short +int Integer +long Long + +float Float +double Double +char Character +boolean Boolean +``` +Java 为包装类做了一些特殊功能,具体来看特殊功能主要有: + +* 可以把基本数据类型的值转换成字符串类型的值 + 1. 调用 toString() 方法 + 2. 调用 Integer.toString(基本数据类型的值) 得到字符串 + 3. 直接把基本数据类型 + 空字符串就得到了字符串(推荐使用) + +* 把字符串类型的数值转换成对应的基本数据类型的值(**重要**) + + 1. Xxx.parseXxx("字符串类型的数值") → `Integer.parseInt(numStr)` + 2. Xxx.valueOf("字符串类型的数值") → `Integer.valueOf(numStr)` (推荐使用) + + ```java + public class PackageClass02 { + public static void main(String[] args) { + // 1.把基本数据类型的值转成字符串 + Integer it = 100 ; + // a.调用toString()方法。 + String itStr = it.toString(); + System.out.println(itStr+1);//1001 + // b.调用Integer.toString(基本数据类型的值)得到字符串。 + String itStr1 = Integer.toString(it); + System.out.println(itStr1+1);//1001 + // c.直接把基本数据类型+空字符串就得到了字符串。 + String itStr2 = it + ""; + System.out.println(itStr2+1);// 1001 + + // 2.把字符串类型的数值转换成对应的基本数据类型的值 + String numStr = "23"; + int numInt = Integer.valueOf(numStr); + System.out.println(numInt+1);//24 + + String doubleStr = "99.9"; + double doubleDb = Double.valueOf(doubleStr); + System.out.println(doubleDb+0.1);//100.0 + } + } + ``` + + + + +*** + + + +##### 类型对比 + +* 有了基本数据类型,为什么还要引用数据类型? + + > 引用数据类型封装了数据和处理该数据的方法,比如 Integer.parseInt(String) 就是将 String 字符类型数据转换为 Integer 整型 + > + > Java 中大部分类和方法都是针对引用数据类型,包括泛型和集合 + +* 引用数据类型那么好,为什么还用基本数据类型? + + > 引用类型的对象要多储存对象头,对基本数据类型来说空间浪费率太高。逻辑上来讲,Java 只有包装类就够了,为了运行速度,需要用到基本数据类型;优先考虑运行效率的问题,所以二者同时存在是合乎情理的 + +* Java 集合不能存放基本数据类型,只存放对象的引用? + + > 不能放基本数据类型是因为不是 Object 的子类。泛型思想,如果不用泛型要写很多参数类型不同的但功能相同的函数(方法重载) + +* == + + > == 比较基本数据类型:比较的是具体的值 + > == 比较引用数据类型:比较的是对象地址值 + + + +*** + + + +#### 装箱拆箱 + +**自动装箱**:可以直接把基本数据类型的值或者变量赋值给包装类 + +**自动拆箱**:可以把包装类的变量直接赋值给基本数据类型 + +```java +public class PackegeClass { + public static void main(String[] args) { + int a = 12 ; + Integer a1 = 12 ; // 自动装箱 + Integer a2 = a ; // 自动装箱 + Integer a3 = null; // 引用数据类型的默认值可以为null + + Integer c = 100 ; + int c1 = c ; // 自动拆箱 + + Integer it = Integer.valueOf(12); // 手工装箱! + // Integer it1 = new Integer(12); // 手工装箱! + Integer it2 = 12; + + Integer it3 = 111 ; + int it33 = it3.intValue(); // 手工拆箱 + } +} +``` + +**自动装箱**反编译后底层调用 `Integer.valueOf()` 实现,源码: + +```java +public static Integer valueOf(int i) { + if (i >= IntegerCache.low && i <= IntegerCache.high) + // 【缓存池】,本质上是一个数组 + return IntegerCache.cache[i + (-IntegerCache.low)]; + return new Integer(i); +} +``` + +自动拆箱调用 `java.lang.Integer#intValue`,源码: + +```java +public int intValue() { + return value; +} +``` + + + +*** + + + +#### 缓存池 + +new Integer(123) 与 Integer.valueOf(123) 的区别在于: + +- new Integer(123):每次都会新建一个对象 + +- Integer.valueOf(123):会使用缓存池中的对象,多次调用取得同一个对象的引用 + + ```java + Integer x = new Integer(123); + Integer y = new Integer(123); + System.out.println(x == y); // false + Integer z = Integer.valueOf(123); + Integer k = Integer.valueOf(123); + System.out.println(z == k); // true + ``` + +valueOf() 方法的实现比较简单,就是先判断值是否在缓存池中,如果在的话就直接返回缓存池的内容。编译器会在自动装箱过程调用 valueOf() 方法,因此多个值相同且值在缓存池范围内的 Integer 实例使用自动装箱来创建,那么就会引用相同的对象 + +**基本类型对应的缓存池如下:** + +- Boolean values true and false +- all byte values +- Short values between -128 and 127 +- Long values between -128 and 127 +- Integer values between -128 and 127 +- Character in the range \u0000 to \u007F (0 and 127) + +在 jdk 1.8 所有的数值类缓冲池中,**Integer 的缓存池 IntegerCache 很特殊,这个缓冲池的下界是 -128,上界默认是 127**,但是上界是可调的,在启动 JVM 时通过 `AutoBoxCacheMax=` 来指定这个缓冲池的大小,该选项在 JVM 初始化的时候会设定一个名为 java.lang.Integer.IntegerCache 系统属性,然后 IntegerCache 初始化的时候就会读取该系统属性来决定上界 + +```java +Integer x = 100; // 自动装箱,底层调用 Integer.valueOf(1) +Integer y = 100; +System.out.println(x == y); // true + +Integer x = 1000; +Integer y = 1000; +System.out.println(x == y); // false,因为缓存池最大127 + +int x = 1000; +Integer y = 1000; +System.out.println(x == y); // true,因为 y 会调用 intValue 【自动拆箱】返回 int 原始值进行比较 +``` + + + +*** + + + +#### 输入数据 + +语法:`Scanner sc = new Scanner(System.in)` + +* next():遇到了空格,就不再录入数据了,结束标记:空格、tab 键 +* nextLine():可以将数据完整的接收过来,结束标记:回车换行符 + +一般使用 `sc.nextInt()` 或者 `sc.nextLine()` 接受整型和字符串,然后转成需要的数据类型 + +* Scanner:`BufferedReader br = new BufferedReader(new InputStreamReader(System.in))` +* print:`PrintStream.write()` + +> 使用引用数据类型的API + +```java +public static void main(String[] args) { + Scanner sc = new Scanner(System.in); + while (sc.hasNextLine()) { + String msg = sc.nextLine(); + } +} +``` + + + + + +**** + + + +### 数组 + +#### 初始化 + +数组就是存储数据长度固定的容器,存储多个数据的数据类型要一致,**数组也是一个对象** + +创建数组: + +* 数据类型[] 数组名:`int[] arr` (常用) +* 数据类型 数组名[]:`int arr[]` + +静态初始化: + +* 数据类型[] 数组名 = new 数据类型[]{元素1,元素2,...}:`int[] arr = new int[]{11,22,33}` +* 数据类型[] 数组名 = {元素1,元素2,...}:`int[] arr = {44,55,66}` + +动态初始化 + +* 数据类型[] 数组名 = new 数据类型[数组长度]:`int[] arr = new int[3]` + + + +#### 元素访问 + +* **索引**:每一个存储到数组的元素,都会自动的拥有一个编号,从 **0** 开始。这个自动编号称为数组索引(index),可以通过数组的索引访问到数组中的元素 + +* **访问格式**:数组名[索引],`arr[0]` +* **赋值:**`arr[0] = 10` + + + +*** + + + +#### 内存分配 + +内存是计算机中的重要器件,临时存储区域,作用是运行程序。编写的程序是存放在硬盘中,在硬盘中的程序是不会运行的,必须放进内存中才能运行,运行完毕后会清空内存,Java 虚拟机要运行程序,必须要对内存进行空间的分配和管理 + +| 区域名称 | 作用 | +| ---------- | ---------------------------------------------------------- | +| 寄存器 | 给 CPU 使用 | +| 本地方法栈 | JVM 在使用操作系统功能的时候使用 | +| 方法区 | 存储可以运行的 class 文件 | +| 堆内存 | 存储对象或者数组,new 来创建的,都存储在堆内存 | +| 方法栈 | 方法运行时使用的内存,比如 main 方法运行,进入方法栈中执行 | + +内存分配图:**Java 数组分配在堆内存** + +* 一个数组内存图 + + ![](https://seazean.oss-cn-beijing.aliyuncs.com/img/Java/数组内存分配-一个数组内存图.png) + +* 两个数组内存图 + + ![](https://seazean.oss-cn-beijing.aliyuncs.com/img/Java/数组内存分配-两个数组内存图.png) + +* 多个数组指向相同内存图 + + ![](https://seazean.oss-cn-beijing.aliyuncs.com/img/Java/数组内存分配-多个数组指向一个数组内存图.png) + +*** + + + +#### 数组异常 + +* 索引越界异常:ArrayIndexOutOfBoundsException + +* 空指针异常:NullPointerException + + ```java + public class ArrayDemo { + public static void main(String[] args) { + int[] arr = new int[3]; + //把null赋值给数组 + arr = null; + System.out.println(arr[0]); + } + } + ``` + + arr = null,表示变量 arr 将不再保存数组的内存地址,也就不允许再操作数组,因此运行的时候会抛出空指针异常。在开发中,空指针异常是不能出现的,一旦出现了,就必须要修改编写的代码 + + 解决方案:给数组一个真正的堆内存空间引用即可 + + + +*** + + + +#### 二维数组 + +二维数组也是一种容器,不同于一维数组,该容器存储的都是一维数组容器 + +初始化: + +* 动态初始化:数据类型[][] 变量名 = new 数据类型[m] [n],`int[][] arr = new int[3][3]` + + * m 表示这个二维数组,可以存放多少个一维数组,行 + * n 表示每一个一维数组,可以存放多少个元素,列 +* 静态初始化 + * 数据类型[][] 变量名 = new 数据类型 [][]{{元素1, 元素2...} , {元素1, 元素2...} + * 数据类型[][] 变量名 = {{元素1, 元素2...}, {元素1, 元素2...}...} + * `int[][] arr = {{11,22,33}, {44,55,66}}` + +遍历: + +```java +public class Test1 { + /* + 步骤: + 1. 遍历二维数组,取出里面每一个一维数组 + 2. 在遍历的过程中,对每一个一维数组继续完成遍历,获取内部存储的每一个元素 + */ + public static void main(String[] args) { + int[][] arr = {{11, 22, 33}, {33, 44, 55}}; + // 1. 遍历二维数组,取出里面每一个一维数组 + for (int i = 0; i < arr.length; i++) { + //System.out.println(arr[i]); + // 2. 在遍历的过程中,对每一个一维数组继续完成遍历,获取内部存储的每一个元素 + //int[] temp = arr[i]; + for (int j = 0; j < arr[i].length; j++) { + System.out.println(arr[i][j]); + } + } + } +} +``` + + + + + +**** + + + +### 运算 + +* i++ 与 ++i 的区别? + + i++ 表示先将 i 放在表达式中运算,然后再加 1,++i 表示先将 i 加 1,然后再放在表达式中运算 + +* || 和 |,&& 和& 的区别,逻辑运算符 + + **& 和| 称为布尔运算符,位运算符;&& 和 || 称为条件布尔运算符,也叫短路运算符** + + 如果 && 运算符的第一个操作数是 false,就不需要考虑第二个操作数的值了,因为无论第二个操作数的值是什么,其结果都是 false;同样,如果第一个操作数是 true,|| 运算符就返回 true,无需考虑第二个操作数的值;但 & 和 | 却不是这样,它们总是要计算两个操作数。为了提高性能,**尽可能使用 && 和 || 运算符** + +* 异或 ^:两位相异为 1,相同为 0,又叫不进位加法 + +* 同或:两位相同为 1,相异为 0 + +* switch:从 Java 7 开始,可以在 switch 条件判断语句中使用 String 对象 + + ```java + String s = "a"; + switch (s) { + case "a": + System.out.println("aaa"); + break; + case "b": + System.out.println("bbb"); + break; + default: + break; + } + ``` + + switch 不支持 long、float、double,switch 的设计初衷是对那些只有少数几个值的类型进行等值判断,如果值过于复杂,那么用 if 比较合适 + +* break:跳出一层循环 + +* 移位运算:计算机里一般用**补码表示数字**,正数、负数的表示区别就是最高位是 0 还是 1 + + * 正数的原码反码补码相同,最高位为 0 + + ```java + 100: 00000000 00000000 00000000 01100100 + ``` + + * 负数: + 原码:最高位为 1,其余位置和正数相同 + 反码:保证符号位不变,其余位置取反 + 补码:保证符号位不变,其余位置取反后加 1,即反码 +1 + + ```java + -100 原码: 10000000 00000000 00000000 01100100 //32位 + -100 反码: 11111111 11111111 11111111 10011011 + -100 补码: 11111111 11111111 11111111 10011100 + ``` + + 补码 → 原码:符号位不变,其余位置取反加 1 + + 运算符: + + * `>>` 运算符:将二进制位进行右移操作,相当于除 2 + * `<<` 运算符:将二进制位进行左移操作,相当于乘 2 + * `>>>` 运算符:无符号右移,忽略符号位,空位都以 0 补齐 + + 运算规则: + + * 正数的左移与右移,空位补 0 + + * 负数原码的左移与右移,空位补 0 + + 负数反码的左移与右移,空位补 1 + + 负数补码,左移低位补 0(会导致负数变为正数的问题,因为移动了符号位),右移高位补 1 + + * 无符号移位,空位补 0 + + + +**** + + + +### 参数 + +#### 形参实参 + +形参: + +* 形式参数,用于定义方法的时候使用的参数,只能是变量 +* 形参只有在方法被调用的时候,虚拟机才分配内存单元,方法调用结束之后便会释放所分配的内存单元 + +实参:调用方法时传递的数据可以是常量,也可以是变量 + + + +#### 可变参数 + +可变参数用在形参中可以接收多个数据,在方法内部**本质上就是一个数组** + +格式:数据类型... 参数名称 + +作用:传输参数非常灵活,可以不传输参数、传输一个参数、或者传输一个数组 + +可变参数的注意事项: + +* 一个形参列表中可变参数只能有一个 +* 可变参数必须放在形参列表的**最后面** + +```java +public static void main(String[] args) { + sum(); // 可以不传输参数。 + sum(10); // 可以传输一个参数。 + sum(10,20,30); // 可以传输多个参数。 + sum(new int[]{10,30,50,70,90}); // 可以传输一个数组。 +} + +public static void sum(int... nums){ + int sum = 0; + for(int i : a) { + sum += i; + } + return sum; +} +``` + + + +*** + + + +### 方法 + +#### 方法概述 + +方法(method)是将具有独立功能的代码块组织成为一个整体,使其具有特殊功能的代码集 + +注意:方法必须先创建才可以使用,该过程成为方法定义,方法创建后并不是直接可以运行的,需要手动使用后才执行,该过程成为方法调用 + +在方法内部定义的叫局部变量,局部变量不能加 static,包括 protected、private、public 这些也不能加 + +原因:局部变量是保存在栈中的,而静态变量保存于方法区(JDK8 在堆中),局部变量出了方法就被栈回收了,而静态变量不会,所以**在局部变量前不能加 static 关键字**,静态变量是定义在类中,又叫类变量 + + + +*** + + + +#### 定义调用 + +定义格式: + +```java +public static 返回值类型 方法名(参数) { + //方法体; + return 数据 ; +} +``` + +调用格式: + +```java +数据类型 变量名 = 方法名 (参数) ; +``` + +* 方法名:调用方法时候使用的标识 +* 参数:由数据类型和变量名组成,多个参数之间用逗号隔开 +* 方法体:完成功能的代码块 +* return:如果方法操作完毕,有数据返回,用于把数据返回给调用者 + +如果方法操作完毕 + +* void 类型的方法,直接调用即可,而且方法体中一般不写 return +* 非 void 类型的方法,推荐用变量接收调用 + +原理:每个方法在被调用执行的时候,都会进入栈内存,并且拥有自己独立的内存空间,方法内部代码调用完毕之后,会从栈内存中弹栈消失 + + + +*** + + + +#### 注意事项 + +* 方法不能嵌套定义 + + ```java + public class MethodDemo { + public static void main(String[] args) { + } + public static void methodOne() { + public static void methodTwo() { + // 这里会引发编译错误!!! + } + } + } + ``` + +* void 表示无返回值,可以省略 return,也可以单独的书写 return,后面不加数据 + + ```java + public static void methodTwo() { + //return 100; 编译错误,因为没有具体返回值类型 + return; + //System.out.println(100); return语句后面不能跟数据或代码 + } + ``` + + + +*** + + + +#### 方法重载 + +##### 重载介绍 + +方法重载指同一个类中定义的多个方法之间的关系,满足下列条件的多个方法相互构成重载: + +1. 多个方法在**同一个类**中 +2. 多个方法具有**相同的方法名** +3. 多个方法的**参数不相同**,类型不同或者数量不同 + +重载仅对应方法的定义,与方法的调用无关,调用方式参照标准格式 + +重载仅针对同一个类中方法的名称与参数进行识别,与返回值无关,**不能通过返回值来判定两个方法是否构成重载** + +原理:JVM → 运行机制 → 方法调用 → 多态原理 + +```java +public class MethodDemo { + public static void fn(int a) { + //方法体 + } + + public static int fn(int a) { /*错误原因:重载与返回值无关*/ + //方法体 + } + + public static void fn(int a, int b) {/*正确格式*/ + //方法体 + } +} +``` + + + +*** + + + +##### 方法选取 + +重载的方法在编译过程中即可完成识别,方法调用时 Java 编译器会根据所传入参数的声明类型(注意与实际类型区分)来选取重载方法。选取的过程共分为三个阶段: + +* 一阶段:在不考虑对基本类型自动装拆箱 (auto-boxing,auto-unboxing),以及可变长参数的情况下选取重载方法 +* 二阶段:如果第一阶段中没有找到适配的方法,那么在允许自动装拆箱,但不允许可变长参数的情况下选取重载方法 +* 三阶段:如果第二阶段中没有找到适配的方法,那么在允许自动装拆箱以及可变长参数的情况下选取重载方法 + +如果 Java 编译器在同一个阶段中找到了多个适配的方法,那么会选择一个最为贴切的,而决定贴切程度的一个关键就是形式参数类型的继承关系,**一般会选择形参为参数类型的子类的方法,因为子类时更具体的实现**: + +```java +public class MethodDemo { + void invoke(Object obj, Object... args) { ... } + void invoke(String s, Object obj, Object... args) { ... } + + invoke(null, 1); // 调用第二个invoke方法,选取的第二阶段 + invoke(null, 1, 2); // 调用第二个invoke方法,匹配第一个和第二个,但String是Object的子类 + + invoke(null, new Object[]{1}); // 只有手动绕开可变长参数的语法糖,才能调用第一个invoke方法 + // 可变参数底层是数组,JVM->运行机制->代码优化 +} +``` + +因此不提倡可变长参数方法的重载 + + + +*** + + + +##### 继承重载 + +除了同一个类中的方法,重载也可以作用于这个类所继承而来的方法。如果子类定义了与父类中**非私有方法**同名的方法,而且这两个方法的参数类型不同,那么在子类中,这两个方法同样构成了重载 + +* 如果这两个方法都是静态的,那么子类中的方法隐藏了父类中的方法 +* 如果这两个方法都不是静态的,且都不是私有的,那么子类的方法重写了父类中的方法,也就是**多态** + + + +*** + + + +#### 参数传递 + +Java 的参数是以**值传递**的形式传入方法中 + +值传递和引用传递的区别在于传递后会不会影响实参的值:**值传递会创建副本**,引用传递不会创建副本 + +* 基本数据类型:形式参数的改变,不影响实际参数 + + 每个方法在栈内存中,都会有独立的栈空间,方法运行结束后就会弹栈消失 + + ```java + public class ArgsDemo01 { + public static void main(String[] args) { + int number = 100; + System.out.println("调用change方法前:" + number);//100 + change(number); + System.out.println("调用change方法后:" + number);//100 + } + public static void change(int number) { + number = 200; + } + } + ``` + +* 引用类型:形式参数的改变,影响实际参数的值 + + **引用数据类型的传参,本质上是将对象的地址以值的方式传递到形参中**,内存中会造成两个引用指向同一个内存的效果,所以即使方法弹栈,堆内存中的数据也已经是改变后的结果 + + ```java + public class PassByValueExample { + public static void main(String[] args) { + Dog dog = new Dog("A"); + func(dog); + System.out.println(dog.getName()); // B + } + private static void func(Dog dog) { + dog.setName("B"); + } + } + class Dog { + String name;//..... + } + ``` + + + + + +*** + + + +### 枚举 + +枚举是 Java 中的一种特殊类型,为了做信息的标志和信息的分类 + +定义枚举的格式: + +```java +修饰符 enum 枚举名称{ + 第一行都是罗列枚举实例的名称。 +} +``` + +枚举的特点: + +* 枚举类是用 final 修饰的,枚举类不能被继承 +* 枚举类默认继承了 java.lang.Enum 枚举类 +* 枚举类的第一行都是常量,必须是罗列枚举类的实例名称 +* 枚举类相当于是多例设计模式 +* 每个枚举项都是一个实例,是一个静态成员变量 + +| 方法名 | 说明 | +| ------------------------------------------------- | ------------------------------------ | +| String name() | 获取枚举项的名称 | +| int ordinal() | 返回枚举项在枚举类中的索引值 | +| int compareTo(E o) | 比较两个枚举项,返回的是索引值的差值 | +| String toString() | 返回枚举常量的名称 | +| static T valueOf(Class type,String name) | 获取指定枚举类中的指定名称的枚举值 | +| values() | 获得所有的枚举项 | + +* 源码分析: + + ```java + enum Season { + SPRING , SUMMER , AUTUMN , WINTER; + } + // 枚举类的编译以后源代码: + public final class Season extends java.lang.Enum { + public static final Season SPRING = new Season(); + public static final Season SUMMER = new Season(); + public static final Season AUTUMN = new Season(); + public static final Season WINTER = new Season(); + + public static Season[] values(); + public static Season valueOf(java.lang.String); + } + ``` + +* API 使用 + + ```java + public class EnumDemo { + public static void main(String[] args){ + // 获取索引 + Season s = Season.SPRING; + System.out.println(s); //SPRING + System.out.println(s.ordinal()); // 0,该值代表索引,summer 就是 1 + s.s.doSomething(); + // 获取全部枚举 + Season[] ss = Season.values(); + for(int i = 0; i < ss.length; i++){ + System.out.println(ss[i]); + } + + int result = Season.SPRING.compareTo(Season.WINTER); + System.out.println(result);//-3 + } + } + enum Season { + SPRING , SUMMER , AUTUMN , WINTER; + + public void doSomething() { + System.out.println("hello "); + } + } + ``` + + + + + + +*** + + + +### Debug + +Debug 是供程序员使用的程序调试工具,它可以用于查看程序的执行流程,也可以用于追踪程序执行过程来调试程序。 + +加断点 → Debug 运行 → 单步运行 → 看 Debugger 窗口 → 看 Console 窗口 + +![](https://seazean.oss-cn-beijing.aliyuncs.com/img/Java/Debug按键说明.png) + +Debug条件断点 + + + + + + + +*** + + + + + +## 对象 + +### 概述 + +Java 是一种面向对象的高级编程语言 + +面向对象三大特征:**封装,继承,多态** + +两个概念:类和对象 + +* 类:相同事物共同特征的描述,类只是学术上的一个概念并非真实存在的,只能描述一类事物 +* 对象:是真实存在的实例, 实例 == 对象,**对象是类的实例化** +* 结论:有了类和对象就可以描述万千世界所有的事物,必须先有类才能有对象 + + + +*** + + + +### 类 + +#### 定义 + +定义格式 + +```java +修饰符 class 类名{ +} +``` + +1. 类名的首字母建议大写,满足驼峰模式,比如 StudentNameCode +2. 一个 Java 代码中可以定义多个类,按照规范一个 Java 文件一个类 +3. 一个 Java 代码文件中,只能有一个类是 public 修饰,**public 修饰的类名必须成为当前 Java 代码的文件名称** + +```java +类中的成分:有且仅有五大成分 +修饰符 class 类名{ + 1.成员变量(Field): 描述类或者对象的属性信息的。 + 2.成员方法(Method): 描述类或者对象的行为信息的。 + 3.构造器(Constructor): 初始化一个对象返回。 + 4.代码块 + 5.内部类 + } +类中有且仅有这五种成分,否则代码报错! +public class ClassDemo { + System.out.println(1);//报错 +} +``` + + + +*** + + + +#### 构造器 + +构造器格式: + +```java +修饰符 类名(形参列表){ + +} +``` + +作用:初始化类的一个对象返回 + +分类:无参数构造器,有参数构造器 + +注意:**一个类默认自带一个无参数构造器**,写了有参数构造器默认的无参数构造器就消失,还需要用无参数构造器就要重新写 + +构造器初始化对象的格式:类名 对象名称 = new 构造器 + +* 无参数构造器的作用:初始化一个类的对象(使用对象的默认值初始化)返回 +* 有参数构造器的作用:初始化一个类的对象(可以在初始化对象的时候为对象赋值)返回 + + + +------ + + + +### 包 + +包:分门别类的管理各种不同的技术,便于管理技术,扩展技术,阅读技术 + +定义包的格式:`package 包名`,必须放在类名的最上面 + +导包格式:`import 包名.类名` + +相同包下的类可以直接访问;不同包下的类必须导包才可以使用 + + + +*** + + + +### 封装 + +封装的哲学思维:合理隐藏,合理暴露 + +封装最初的目的:提高代码的安全性和复用性,组件化 + +封装的步骤: + +1. **成员变量应该私有,用 private 修饰,只能在本类中直接访问** +2. **提供成套的 getter 和 setter 方法暴露成员变量的取值和赋值** + +使用 private 修饰成员变量的原因:实现数据封装,不想让别人使用修改你的数据,比较安全 + + + +*** + + + +### this + +this 关键字的作用: + +* this 关键字代表了当前对象的引用 +* this 出现在方法中:**哪个对象调用这个方法 this 就代表谁** +* this 可以出现在构造器中:代表构造器正在初始化的那个对象 +* this 可以区分变量是访问的成员变量还是局部变量 + + + +------ + + + +### static + +#### 基本介绍 + +Java 是通过成员变量是否有 static 修饰来区分是类的还是属于对象的 + +按照有无 static 修饰,成员变量和方法可以分为: + +* 成员变量: + * 静态成员变量(类变量):static 修饰的成员变量,属于类本身,**与类一起加载一次,只有一个**,直接用类名访问即可 + * 实例成员变量:无 static 修饰的成员变量,属于类的每个对象的,**与类的对象一起加载**,对象有多少个,实例成员变量就加载多少个,必须用类的对象来访问 + +* 成员方法: + * 静态方法:有 static 修饰的成员方法称为静态方法也叫类方法,属于类本身的,直接用类名访问即可 + * 实例方法:无 static 修饰的成员方法称为实例方法,属于类的每个对象的,必须用类的对象来访问 + + + +**** + + + +#### static 用法 + +成员变量的访问语法: + +* 静态成员变量:只有一份可以被类和类的对象**共享访问** + * 类名.静态成员变量(同一个类中访问静态成员变量可以省略类名不写) + * 对象.静态成员变量(不推荐) + +* 实例成员变量: + * 对象.实例成员变量(先创建对象) + +成员方法的访问语法: + +* 静态方法:有 static 修饰,属于类 + + * 类名.静态方法(同一个类中访问静态成员可以省略类名不写) + * 对象.静态方法(不推荐,参考 JVM → 运行机制 → 方法调用) + +* 实例方法:无 static 修饰,属于对象 + + * 对象.实例方法 + + ```java + public class Student { + // 1.静态方法:有static修饰,属于类,直接用类名访问即可! + public static void inAddr(){ } + // 2.实例方法:无static修饰,属于对象,必须用对象访问! + public void eat(){} + + public static void main(String[] args) { + // a.类名.静态方法 + Student.inAddr(); + inAddr(); + // b.对象.实例方法 + // Student.eat(); // 报错了! + Student sea = new Student(); + sea.eat(); + } + } + ``` + + + +*** + + + +#### 两个问题 + +内存问题: + +* 栈内存存放 main 方法和地址 + +* 堆内存存放对象和变量 + +* 方法区存放 class 和静态变量(jdk8 以后移入堆) + +访问问题: + +* 实例方法是否可以直接访问实例成员变量?可以,因为它们都属于对象 +* 实例方法是否可以直接访问静态成员变量?可以,静态成员变量可以被共享访问 +* 实例方法是否可以直接访问实例方法? 可以,实例方法和实例方法都属于对象 +* 实例方法是否可以直接访问静态方法?可以,静态方法可以被共享访问 +* 静态方法是否可以直接访问实例变量? 不可以,实例变量**必须用对象访问**!! +* 静态方法是否可以直接访问静态变量? 可以,静态成员变量可以被共享访问。 +* 静态方法是否可以直接访问实例方法? 不可以,实例方法必须用对象访问!! +* 静态方法是否可以直接访问静态方法?可以,静态方法可以被共享访问!! + + + +------ + + + +### 继承 + +#### 基本介绍 + +继承是 Java 中一般到特殊的关系,是一种子类到父类的关系 + +* 被继承的类称为:父类/超类 +* 继承父类的类称为:子类 + +继承的作用: + +* **提高代码的复用**,相同代码可以定义在父类中 +* 子类继承父类,可以直接使用父类这些代码(相同代码重复利用) +* 子类得到父类的属性(成员变量)和行为(方法),还可以定义自己的功能,子类更强大 + +继承的特点: + +1. 子类的全部构造器默认先访问父类的无参数构造器,再执行自己的构造器 +2. **单继承**:一个类只能继承一个直接父类 +3. 多层继承:一个类可以间接继承多个父类(家谱) +4. 一个类可以有多个子类 +5. 一个类要么默认继承了 Object 类,要么间接继承了 Object 类,**Object 类是 Java 中的祖宗类** + +继承的格式: + +```java +子类 extends 父类{ + +} +``` + +子类不能继承父类的东西: + +* 子类不能继承父类的构造器,子类有自己的构造器 +* 子类是不能可以继承父类的私有成员的,可以反射暴力去访问继承自父类的私有成员 +* 子类是不能继承父类的静态成员,父类静态成员只有一份可以被子类共享访问,**共享并非继承** + +```java +public class ExtendsDemo { + public static void main(String[] args) { + Cat c = new Cat(); + // c.run(); + Cat.test(); + System.out.println(Cat.schoolName); + } +} + +class Cat extends Animal{ +} + +class Animal{ + public static String schoolName ="seazean"; + public static void test(){} + private void run(){} +} +``` + + + +*** + + + +#### 变量访问 + +继承后成员变量的访问特点:**就近原则**,子类有找子类,子类没有找父类,父类没有就报错 + +如果要申明访问父类的成员变量可以使用:super.父类成员变量,super指父类引用 + +```java +public class ExtendsDemo { + public static void wmain(String[] args) { + Wolf w = new Wolf();w + w.showName(); + } +} + +class Wolf extends Animal{ + private String name = "子类狼"; + public void showName(){ + String name = "局部名称"; + System.out.println(name); // 局部name + System.out.println(this.name); // 子类对象的name + System.out.println(super.name); // 父类的 + System.out.println(name1); // 父类的 + //System.out.println(name2); // 报错。子类父类都没有 + } +} + +class Animal{ + public String name = "父类动物名称"; + public String name1 = "父类"; +} +``` + + + +*** + + + +#### 方法访问 + +子类继承了父类就得到了父类的方法,**可以直接调用**,受权限修饰符的限制,也可以重写方法 + +方法重写:子类重写一个与父类申明一样的方法来**覆盖**父类的该方法 + +方法重写的校验注解:@Override + +* 方法加了这个注解,那就必须是成功重写父类的方法,否则报错 +* @Override 优势:可读性好,安全,优雅 + +**子类可以扩展父类的功能,但不能改变父类原有的功能**,重写有以下三个限制: + +- 子类方法的访问权限必须大于等于父类方法 +- 子类方法的返回类型必须是父类方法返回类型或为其子类型 +- 子类方法抛出的异常类型必须是父类抛出异常类型或为其子类型 + +继承中的隐藏问题: + +- 子类和父类方法都是静态的,那么子类中的方法会隐藏父类中的方法 +- 在子类中可以定义和父类成员变量同名的成员变量,此时子类的成员变量隐藏了父类的成员变量,在创建对象为对象分配内存的过程中,**隐藏变量依然会被分配内存** + +```java +public class ExtendsDemo { + public static void main(String[] args) { + Wolf w = new Wolf(); + w.run(); + } +} +class Wolf extends Animal{ + @Override + public void run(){}// +} +class Animal{ + public void run(){} +} +``` + + + +*** + + + +#### 常见问题 + +* 为什么子类构造器会先调用父类构造器? + + 1. 子类的构造器的第一行默认 super() 调用父类的无参数构造器,写不写都存在 + 2. 子类继承父类,子类就得到了父类的属性和行为。调用子类构造器初始化子类对象数据时,必须先调用父类构造器初始化继承自父类的属性和行为 + 3. 参考 JVM → 类加载 → 对象创建 + + ```java + class Animal { + public Animal() { + System.out.println("==父类Animal的无参数构造器=="); + } + } + + class Tiger extends Animal { + public Tiger() { + super(); // 默认存在的,根据参数去匹配调用父类的构造器。 + System.out.println("==子类Tiger的无参数构造器=="); + } + public Tiger(String name) { + //super(); 默认存在的,根据参数去匹配调用父类的构造器。 + System.out.println("==子类Tiger的有参数构造器=="); + } + } + ``` + +* **为什么 Java 是单继承的?** + + 答:反证法,假如 Java 可以多继承,请看如下代码: + + ```java + class A{ + public void test(){ + System.out.println("A"); + } + } + class B{ + public void test(){ + System.out.println("B"); + } + } + class C extends A , B { + public static void main(String[] args){ + C c = new C(); + c.test(); + // 出现了类的二义性!所以Java不能多继承!! + } + } + ``` + + + + + +------ + + + +### super + +继承后 super 调用父类构造器,父类构造器初始化继承自父类的数据。 + + +总结与拓展: + +* this 代表了当前对象的引用(继承中指代子类对象):this.子类成员变量、this.子类成员方法、**this(...)** 可以根据参数匹配访问本类其他构造器 +* super 代表了父类对象的引用(继承中指代了父类对象空间):super.父类成员变量、super.父类的成员方法、super(...) 可以根据参数匹配访问父类的构造器 + +注意: + +* this(...) 借用本类其他构造器,super(...) 调用父类的构造器 +* this(...) 或 super(...) 必须放在构造器的第一行,否则报错 +* this(...) 和 super(...) **不能同时出现**在构造器中,因为构造函数必须出现在第一行上,只能选择一个 + +```java +public class ThisDemo { + public static void main(String[] args) { + // 需求:希望如果不写学校默认就是”张三“! + Student s1 = new Student("天蓬元帅", 1000 ); + Student s2 = new Student("齐天大圣", 2000, "清华大学" ); + } +} +class Study extends Student { + public Study(String name, int age, String schoolName) { + super(name , age , schoolName) ; + // 根据参数匹配调用父类构造器 + } +} + +class Student{ + private String name ; + private int age ; + private String schoolName ; + + public Student() { + } + public Student(String name , int age){ + // 借用兄弟构造器的功能! + this(name , age , "张三"); + } + public Student(String name, int age, String schoolName) { + this.name = name; + this.age = age; + this.schoolName = schoolName; + } + // .......get + set +} +``` + + + +*** + + + +### final + +#### 基本介绍 + +final 用于修饰:类,方法,变量 + +* final 修饰类,类不能被继承了,类中的方法和变量可以使用 +* final 可以修饰方法,方法就不能被重写 +* final 修饰变量总规则:变量有且仅能被赋值一次 + +final 和 abstract 的关系是**互斥关系**,不能同时修饰类或者同时修饰方法 + + + +*** + + + +#### 修饰变量 + +##### 静态变量 + +final 修饰静态成员变量,变量变成了常量 + +常量:有 public static final 修饰,名称字母全部大写,多个单词用下划线连接 + +final 修饰静态成员变量可以在哪些地方赋值: + +1. 定义的时候赋值一次 + +2. 可以在静态代码块中赋值一次 + +```java +public class FinalDemo { + //常量:public static final修饰,名称字母全部大写,下划线连接。 + public static final String SCHOOL_NAME = "张三" ; + public static final String SCHOOL_NAME1; + + static{ + //SCHOOL_NAME = "java";//报错 + SCHOOL_NAME1 = "张三1"; + } +} +``` + + + +*** + + + +##### 实例变量 + +final 修饰变量的总规则:有且仅能被赋值一次 + +final 修饰实例成员变量可以在哪些地方赋值 1 次: + +1. 定义的时候赋值一次 +2. 可以在实例代码块中赋值一次 +3. 可以在每个构造器中赋值一次 + +```java +public class FinalDemo { + private final String name = "张三" ; + private final String name1; + private final String name2; + { + // 可以在实例代码块中赋值一次。 + name1 = "张三1"; + } + //构造器赋值一次 + public FinalDemo(){ + name2 = "张三2"; + } + public FinalDemo(String a){ + name2 = "张三2"; + } + + public static void main(String[] args) { + FinalDemo f1 = new FinalDemo(); + //f1.name = "张三1"; // 第二次赋值 报错! + } +} +``` + + + +*** + + + +### 抽象类 + +#### 基本介绍 + +> 父类知道子类要完成某个功能,但是每个子类实现情况不一样 + +抽象方法:没有方法体,只有方法签名,必须用 abstract 修饰的方法就是抽象方法 + +抽象类:拥有抽象方法的类必须定义成抽象类,必须用 abstract 修饰,**抽象类是为了被继承** + +一个类继承抽象类,**必须重写抽象类的全部抽象方法**,否则这个类必须定义成抽象类 + +```java +public class AbstractDemo { + public static void main(String[] args) { + Dog d = new Dog(); + d.run(); + } +} + +class Dog extends Animal{ + @Override + public void run() { + System.out.println("🐕跑"); + } +} + +abstract class Animal{ + public abstract void run(); +} +``` + + + +*** + + + +#### 常见问题 + +一、抽象类是否有构造器,是否可以创建对象? + +* 抽象类有构造器,但是抽象类不能创建对象,类的其他成分它都具备,构造器提供给子类继承后调用父类构造器使用 +* 抽象类中存在抽象方法,但不能执行,**抽象类中也可没有抽象方法** + +> 抽象在学术上本身意味着不能实例化 + +```java +public class AbstractDemo { + public static void main(String[] args) { + //Animal a = new Animal(); 抽象类不能创建对象! + //a.run(); // 抽象方法不能执行 + } +} +abstract class Animal{ + private String name; + public static String schoolName = "张三"; + public Animal(){ } + + public abstract void run(); + //普通方法 + public void go(){ } +} +``` + +二、static 与 abstract 能同时使用吗? + +答:不能,被 static 修饰的方法属于类,是类自己的东西,不是给子类来继承的,而抽象方法本身没有实现,就是用来给子类继承 + + + +*** + + + +#### 存在意义 + +**被继承**,抽象类就是为了被子类继承,否则抽象类将毫无意义(核心) + +抽象类体现的是"模板思想":**部分实现,部分抽象**,可以使用抽象类设计一个模板模式 + +```java +//作文模板 +public class ExtendsDemo { + public static void main(String[] args) { + Student xiaoMa = new Student(); + xiaoMa.write(); + } +} +class Student extends Template{ + @Override + public String writeText() {return "\t内容"} +} +// 1.写一个模板类:代表了作文模板。 +abstract class Template{ + private String title = "\t\t\t\t\t标题"; + private String start = "\t开头"; + private String last = "\t结尾"; + public void write(){ + System.out.println(title+"\n"+start); + System.out.println(writeText()); + System.out.println(last); + } + // 正文部分定义成抽象方法,交给子类重写!! + public abstract String writeText(); +} +``` + + + +*** + + + +### 接口 + +#### 基本介绍 + +接口是 Java 语言中一种引用类型,是方法的集合。 + +接口是更加彻底的抽象,接口中只有抽象方法和常量,没有其他成分 + +```java + 修饰符 interface 接口名称{ + // 抽象方法 + // 默认方法 + // 静态方法 + // 私有方法 +} +``` + +* 抽象方法:接口中的抽象方法默认会加上 public abstract 修饰,所以可以省略不写 + +* 静态方法:静态方法必须有方法体 + +* 常量:是 public static final 修饰的成员变量,仅能被赋值一次,值不能改变。常量的名称规范上要求全部大写,多个单词下划线连接,public static final 可以省略不写 + + ```java + public interface InterfaceDemo{ + //public static final String SCHOOL_NAME = "张三"; + String SCHOOL_NAME = "张三"; + + //public abstract void run(); + void run();//默认补充 + } + ``` + + + + +*** + + + + +#### 实现接口 + +**接口是用来被类实现的。** + +* 类与类是继承关系:一个类只能直接继承一个父类,单继承 +* 类与接口是实现关系:一个类可以实现多个接口,多实现,接口不能继承类 +* 接口与接口继承关系:**多继承** + +```java +修饰符 class 实现类名称 implements 接口1,接口2,接口3,....{ + +} +修饰符 interface 接口名 extend 接口1,接口2,接口3,....{ + +} +``` + +实现多个接口的使用注意事项: + +1. 当一个类实现多个接口时,多个接口中存在同名的静态方法并不会冲突,只能通过各自接口名访问静态方法 + +2. 当一个类实现多个接口时,多个接口中存在同名的默认方法,实现类必须重写这个方法 + +3. 当一个类既继承一个父类,又实现若干个接口时,父类中成员方法与接口中默认方法重名,子类**就近选择执行父类**的成员方法 + +4. 接口中,没有构造器,**不能创建对象**,接口是更彻底的抽象,连构造器都没有,自然不能创建对象 + + ```java + public class InterfaceDemo { + public static void main(String[] args) { + Student s = new Student(); + s.run(); + s.rule(); + } + } + class Student implements Food, Person{ + @Override + public void eat() {} + + @Override + public void run() {} + } + interface Food{ + void eat(); + } + interface Person{ + void run(); + } + //可以直接 interface Person extend Food, + //然后 class Student implements Person 效果一样 + ``` + + + +*** + + + +#### 新增功能 + +jdk1.8 以后新增的功能: + +* 默认方法(就是普通实例方法) + * 必须用 default 修饰,默认会 public 修饰 + * 必须用接口的实现类的对象来调用 + * 必须有默认实现 +* 静态方法 + * 默认会 public 修饰 + * 接口的静态方法必须用接口的类名本身来调用 + * 调用格式:ClassName.method() + * 必须有默认实现 +* 私有方法:JDK 1.9 才开始有的,只能在**本类中**被其他的默认方法或者私有方法访问 + +```java +public class InterfaceDemo { + public static void main(String[] args) { + // 1.默认方法调用:必须用接口的实现类对象调用。 + Man m = new Man(); + m.run(); + m.work(); + + // 2.接口的静态方法必须用接口的类名本身来调用。 + InterfaceJDK8.inAddr(); + } +} +class Man implements InterfaceJDK8 { + @Override + public void work() { + System.out.println("工作中。。。"); + } +} + +interface InterfaceJDK8 { + //抽象方法!! + void work(); + // a.默认方法(就是之前写的普通实例方法) + // 必须用接口的实现类的对象来调用。 + default void run() { + go(); + System.out.println("开始跑步🏃‍"); + } + + // b.静态方法 + // 注意:接口的静态方法必须用接口的类名本身来调用 + static void inAddr() { + System.out.println("我们在武汉"); + } + + // c.私有方法(就是私有的实例方法): JDK 1.9才开始有的。 + // 只能在本接口中被其他的默认方法或者私有方法访问。 + private void go() { + System.out.println("开始。。"); + } +} +``` + + + +*** + + + +#### 对比抽象类 + +| **参数** | **抽象类** | **接口** | +| ------------------ | ------------------------------------------------------------ | ------------------------------------------------------------ | +| 默认的方法实现 | 可以有默认的方法实现 | 接口完全是抽象的,jdk8 以后有默认的实现 | +| 实现 | 子类使用 **extends** 关键字来继承抽象类。如果子类不是抽象类的话,它需要提供抽象类中所有声明的方法的实现。 | 子类使用关键字 **implements** 来实现接口。它需要提供接口中所有声明的方法的实现 | +| 构造器 | 抽象类可以有构造器 | 接口不能有构造器 | +| 与正常Java类的区别 | 除了不能实例化抽象类之外,和普通 Java 类没有任何区别 | 接口是完全不同的类型 | +| 访问修饰符 | 抽象方法有 **public**、**protected** 和 **default** 这些修饰符 | 接口默认修饰符是 **public**,别的修饰符需要有方法体 | +| main方法 | 抽象方法可以有 main 方法并且我们可以运行它 | jdk8 以前接口没有 main 方法,不能运行;jdk8 以后接口可以有 default 和 static 方法,可以运行 main 方法 | +| 多继承 | 抽象方法可以继承一个类和实现多个接口 | 接口可以继承一个或多个其它接口,接口不可继承类 | +| 速度 | 比接口速度要快 | 接口是稍微有点慢的,因为它需要时间去寻找在类中实现的方法 | +| 添加新方法 | 如果往抽象类中添加新的方法,可以给它提供默认的实现,因此不需要改变现在的代码 | 如果往接口中添加方法,那么必须改变实现该接口的类 | + + + + + +------ + + + +### 多态 + +#### 基本介绍 + +多态的概念:同一个实体同时具有多种形式同一个类型的对象,执行同一个行为,在不同的状态下会表现出不同的行为特征 + +多态的格式: + +* 父类类型范围 > 子类类型范围 + +```java +父类类型 对象名称 = new 子类构造器; +接口 对象名称 = new 实现类构造器; +``` + +多态的执行: + +* 对于方法的调用:**编译看左边,运行看右边**(分派机制) +* 对于变量的调用:**编译看左边,运行看左边** + +多态的使用规则: + +* 必须存在继承或者实现关系 +* 必须存在父类类型的变量引用子类类型的对象 +* 存在方法重写 + +多态的优势: +* 在多态形式下,右边对象可以实现组件化切换,便于扩展和维护,也可以实现类与类之间的**解耦** +* 父类类型作为方法形式参数,传递子类对象给方法,可以传入一切子类对象进行方法的调用,更能体现出多态的**扩展性**与便利性 + +多态的劣势: +* 多态形式下,不能直接调用子类特有的功能,因为编译看左边,父类中没有子类独有的功能,所以代码在编译阶段就直接报错了 + +```java +public class PolymorphicDemo { + public static void main(String[] args) { + Animal c = new Cat(); + c.run(); + //c.eat();//报错 编译看左边 需要强转 + go(c); + go(new Dog); + } + //用 Dog或者Cat 都没办法让所有动物参与进来,只能用Anima + public static void go(Animal d){} + +} +class Dog extends Animal{} + +class Cat extends Animal{ + public void eat(); + @Override + public void run(){} +} + +class Animal{ + public void run(){} +} +``` + + + +*** + + + +#### 上下转型 + +>基本数据类型的转换: +> +>1. 小范围类型的变量或者值可以直接赋值给大范围类型的变量 +>2. 大范围类型的变量或者值必须强制类型转换给小范围类型的变量 + +引用数据类型的**自动**类型转换语法:子类类型的对象或者变量可以自动类型转换赋值给父类类型的变量 + +**父类引用指向子类对象** + +- **向上转型 (upcasting)**:通过子类对象(小范围)实例化父类对象(大范围),这种属于自动转换 +- **向下转型 (downcasting)**:通过父类对象(大范围)实例化子类对象(小范围),这种属于强制转换 + +```java +public class PolymorphicDemo { + public static void main(String[] args){ + Animal a = new Cat(); // 向上转型 + Cat c = (Cat)a; // 向下转型 + } +} +class Animal{} +class Cat extends Animal{} +``` + + + +*** + + + +#### instanceof + +instanceof:判断左边的对象是否是右边的类的实例,或者是其直接或间接子类,或者是其接口的实现类 + +* 引用类型强制类型转换:父类类型的变量或者对象强制类型转换成子类类型的变量,否则报错 +* 强制类型转换的格式:**类型 变量名称 = (类型)(对象或者变量)** +* 有继承/实现关系的两个类型就可以进行强制类型转换,编译阶段一定不报错,但是运行阶段可能出现类型转换异常 ClassCastException + +```java +public class Demo{ + public static void main(String[] args){ + Aniaml a = new Dog(); + //Dog d = (Dog)a; + //Cat c = (Cat)a; 编译不报错,运行报ClassCastException错误 + if(a instanceof Cat){ + Cat c = (Cat)a; + } else if(a instanceof Dog) { + Dog d = (Dog)a; + } + } +} +class Dog extends Animal{} +class Cat extends Animal{} +class Animal{} +``` + + + +*** + + + +### 内部类 + +#### 概述 + +内部类是类的五大成分之一:成员变量,方法,构造器,代码块,内部类 + +概念:定义在一个类里面的类就是内部类 + +作用:提供更好的封装性,体现出组件思想,**间接解决类无法多继承引起的一系列问题** + +分类:静态内部类、实例内部类(成员内部类)、局部内部类、**匿名内部类**(重点) + + + +*** + + + +#### 静态内部类 + +定义:有 static 修饰,属于外部类本身,会加载一次 + +静态内部类中的成分研究: + +* 类有的成分它都有,静态内部类属于外部类本身,只会加载一次 +* 特点与外部类是完全一样的,只是位置在别人里面 +* 可以定义静态成员 + +静态内部类的访问格式:外部类名称.内部类名称 + +静态内部类创建对象的格式:外部类名称.内部类名称 对象名称 = new 外部类名称.内部类构造器 + +静态内部类的访问拓展: + +* 静态内部类中是否可以直接访问外部类的静态成员? 可以,外部类的静态成员只有一份,可以被共享 +* 静态内部类中是否可以直接访问外部类的实例成员? 不可以,外部类的成员必须用外部类对象访问 + +```java +public class Demo{ + public static void main(String[] args){ + Outter.Inner in = new Outter.Inner(); + } +} + +static class Outter{ + public static int age; + private double salary; + public static class Inner{ + //拥有类的所有功能 构造器 方法 成员变量 + System.out.println(age); + //System.out.println(salary);报错 + } +} +``` + + + +*** + + + +#### 实例内部类 + +定义:无 static 修饰的内部类,属于外部类的每个对象,跟着外部类对象一起加载 + +实例内部类的成分特点:实例内部类中不能定义静态成员,其他都可以定义 + +实例内部类的访问格式:外部类名称.内部类名称 + +创建对象的格式:外部类名称.内部类名称 对象名称 = new 外部类构造器.new 内部构造器 + +* `Outter.Inner in = new Outter().new Inner()` + +**实例内部类可以访问外部类的全部成员** + +* 实例内部类中可以直接访问外部类的静态成员,外部类的静态成员可以被共享访问 +* 实例内部类中可以访问外部类的实例成员,实例内部类属于外部类对象,可以直接访问外部类对象的实例成员 + + + +*** + + + +#### 局部内部类 + +局部内部类:定义在方法中,在构造器中,代码块中,for 循环中定义的内部类 + +局部内部类中的成分特点:只能定义实例成员,不能定义静态成员 + +```java +public class InnerClass{ + public static void main(String[] args){ + String name; + class{} + } + public static void test(){ + class Animal{} + class Cat extends Animal{} + } +} +``` + + + +*** + + + +#### 匿名内部类 + +匿名内部类:没有名字的局部内部类 + +匿名内部类的格式: + +```java +new 类名|抽象类|接口(形参){ + //方法重写。 +} +``` + 匿名内部类的特点: + +* 匿名内部类不能定义静态成员 +* 匿名内部类一旦写出来,就会立即创建一个匿名内部类的对象返回 +* **匿名内部类的对象的类型相当于是当前 new 的那个的类型的子类类型** +* 匿名内部类引用局部变量必须是**常量**,底层创建为内部类的成员变量(原因:JVM → 运行机制 → 代码优化) + +```java +public class Anonymity { + public static void main(String[] args) { + Animal a = new Animal(){ + @Override + public void run() { + System.out.println("猫跑的贼溜~~"); + //System.out.println(n); + } + }; + a.run(); + a.go(); + } +} +abstract class Animal{ + public abstract void run(); + + public void go(){ + System.out.println("开始go~~~"); + } +} +``` + + + +*** + + + +### 权限符 + +权限修饰符:有四种**(private -> 缺省 -> protected - > public )** +可以修饰成员变量,修饰方法,修饰构造器,内部类,不同修饰符修饰的成员能够被访问的权限将受到限制 + +| 四种修饰符访问权限 | private | 缺省 | protected | public | +| ------------------ | :-----: | :--: | :-------: | :----: | +| 本类中 | √ | √ | √ | √ | +| 本包下的子类中 | X | √ | √ | √ | +| 本包下其他类中 | X | √ | √ | √ | +| 其他包下的子类中 | X | X | √ | √ | +| 其他包下的其他类中 | X | X | X | √ | + +protected 用于修饰成员,表示在继承体系中成员对于子类可见 + +* 基类的 protected 成员是包内可见的,并且对子类可见 +* 若子类与基类不在同一包中,那么子类实例可以访问其从基类继承而来的 protected 方法(重写),而不能访问基类实例的 protected 方法 + + + + + +*** + + + +### 代码块 + +#### 静态代码块 + +静态代码块的格式: + + ```java +static { +} + ``` + +* 静态代码块特点: + * 必须有 static 修饰,只能访问静态资源 + * 会与类一起优先加载,且自动触发执行一次 +* 静态代码块作用: + * 可以在执行类的方法等操作之前先在静态代码块中进行静态资源的初始化 + * **先执行静态代码块,在执行 main 函数里的操作** + +```java +public class CodeDemo { + public static String schoolName ; + public static ArrayList lists = new ArrayList<>(); + + // 静态代码块,属于类,与类一起加载一次! + static { + System.out.println("静态代码块被触发执行~~~~~~~"); + // 在静态代码块中进行静态资源的初始化操作 + schoolName = "张三"; + lists.add("3"); + lists.add("4"); + lists.add("5"); + } + public static void main(String[] args) { + System.out.println("main方法被执行"); + System.out.println(schoolName); + System.out.println(lists); + } +} +/*静态代码块被触发执行~~~~~~~ +main方法被执行 +张三 +[3, 4, 5] */ +``` + + + +*** + + + +#### 实例代码块 + +实例代码块的格式: + +```java +{ + +} +``` + +* 实例代码块的特点: + * 无 static 修饰,属于对象 + * 会与类的对象一起加载,每次创建类的对象的时候,实例代码块都会被加载且自动触发执行一次 + * 实例代码块的代码在底层实际上是提取到每个构造器中去执行的 + +* 实例代码块的作用:实例代码块可以在创建对象之前进行实例资源的初始化操作 + +```java +public class CodeDemo { + private String name; + private ArrayList lists = new ArrayList<>(); + { + name = "代码块"; + lists.add("java"); + System.out.println("实例代码块被触发执行一次~~~~~~~~"); + } + public CodeDemo02(){ }//构造方法 + public CodeDemo02(String name){} + + public static void main(String[] args) { + CodeDemo c = new CodeDemo();//实例代码块被触发执行一次 + System.out.println(c.name); + System.out.println(c.lists); + new CodeDemo02();//实例代码块被触发执行一次 + } +} +``` + + + + + +*** + + + + + + +## API + +### Object + +#### 基本介绍 + +Object 类是 Java 中的祖宗类,一个类或者默认继承 Object 类,或者间接继承 Object 类,Object 类的方法是一切子类都可以直接使用 + +Object 类常用方法: + +* `public String toString()`:默认是返回当前对象在堆内存中的地址信息:类的全限名@内存地址,例:Student@735b478; + * 直接输出对象名称,默认会调用 toString() 方法,所以省略 toString() 不写; + * 如果输出对象的内容,需要重写 toString() 方法,toString 方法存在的意义是为了被子类重写 +* `public boolean equals(Object o)`:默认是比较两个对象的引用是否相同 +* `protected Object clone()`:创建并返回此对象的副本 + +只要两个对象的内容一样,就认为是相等的: + +```java +public boolean equals(Object o) { + // 1.判断是否自己和自己比较,如果是同一个对象比较直接返回true + if (this == o) return true; + // 2.判断被比较者是否为null ,以及是否是学生类型。 + if (o == null || this.getClass() != o.getClass()) return false; + // 3.o一定是学生类型,强制转换成学生,开始比较内容! + Student student = (Student) o; + return age == student.age && + sex == student.sex && + Objects.equals(name, student.name); +} +``` + +**面试题**:== 和 equals 的区别 + +* == 比较的是变量(栈)内存中存放的对象的(堆)内存地址,用来判断两个对象的**地址**是否相同,即是否是指相同一个对象,比较的是真正意义上的指针操作 +* Object 类中的方法,**默认比较两个对象的引用**,重写 equals 方法比较的是两个对象的**内容**是否相等,所有的类都是继承自 java.lang.Object 类,所以适用于所有对象 + +hashCode 的作用: + +* hashCode 的存在主要是用于查找的快捷性,如 Hashtable,HashMap 等,可以在散列存储结构中确定对象的存储地址 +* 如果两个对象相同,就是适用于 equals(java.lang.Object) 方法,那么这两个对象的 hashCode 一定要相同 +* 哈希值相同的数据不一定内容相同,内容相同的数据哈希值一定相同 + + + +*** + + + +#### 深浅克隆 + +Object 的 clone() 是 protected 方法,一个类不显式去重写 clone(),就不能直接去调用该类实例的 clone() 方法 + +深浅拷贝(克隆)的概念: + +* 浅拷贝 (shallowCopy):**对基本数据类型进行值传递,对引用数据类型只是复制了引用**,被复制对象属性的所有的引用仍然指向原来的对象,简而言之就是增加了一个指针指向原来对象的内存地址 + + **Java 中的复制方法基本都是浅拷贝**:Object.clone()、System.arraycopy()、Arrays.copyOf() + +* 深拷贝 (deepCopy):对基本数据类型进行值传递,对引用数据类型是一个整个独立的对象拷贝,会拷贝所有的属性并指向的动态分配的内存,简而言之就是把所有属性复制到一个新的内存,增加一个指针指向新内存。所以使用深拷贝的情况下,释放内存的时候不会出现使用浅拷贝时释放同一块内存的错误 + +Cloneable 接口是一个标识性接口,即该接口不包含任何方法(包括 clone),但是如果一个类想合法的进行克隆,那么就必须实现这个接口,在使用 clone() 方法时,若该类未实现 Cloneable 接口,则抛出异常 + +* Clone & Copy:`Student s = new Student` + + `Student s1 = s`:只是 copy 了一下 reference,s 和 s1 指向内存中同一个 Object,对对象的修改会影响对方 + + `Student s2 = s.clone()`:会生成一个新的 Student 对象,并且和 s 具有相同的属性值和方法 + +* Shallow Clone & Deep Clone: + + 浅克隆:Object 中的 clone() 方法在对某个对象克隆时对其仅仅是简单地执行域对域的 copy + + * 对基本数据类型和包装类的克隆是没有问题的。String、Integer 等包装类型在内存中是**不可以被改变的对象**,所以在使用克隆时可以视为基本类型,只需浅克隆引用即可 + * 如果对一个引用类型进行克隆时只是克隆了它的引用,和原始对象共享对象成员变量 + + ![](https://seazean.oss-cn-beijing.aliyuncs.com/img/Java/Object浅克隆.jpg) + + 深克隆:在对整个对象浅克隆后,对其引用变量进行克隆,并将其更新到浅克隆对象中去 + + ```java + public class Student implements Cloneable{ + private String name; + private Integer age; + private Date date; + + @Override + protected Object clone() throws CloneNotSupportedException { + Student s = (Student) super.clone(); + s.date = (Date) date.clone(); + return s; + } + //..... + } + ``` + +SDP → 创建型 → 原型模式 + + + +*** + + + +### Objects + +Objects 类与 Object 是继承关系 + +Objects 的方法: + +* `public static boolean equals(Object a, Object b)`:比较两个对象是否相同 + + ```java + public static boolean equals(Object a, Object b) { + // 进行非空判断,从而可以避免空指针异常 + return a == b || a != null && a.equals(b); + } + ``` + +* `public static boolean isNull(Object obj)`:判断变量是否为 null ,为 null 返回 true + +* `public static String toString(对象)`:返回参数中对象的字符串表示形式 + +* `public static String toString(对象, 默认字符串)`:返回对象的字符串表示形式 + +```java +public class ObjectsDemo { + public static void main(String[] args) { + Student s1 = null; + Student s2 = new Student(); + System.out.println(Objects.equals(s1 , s2));//推荐使用 + // System.out.println(s1.equals(s2)); // 空指针异常 + + System.out.println(Objects.isNull(s1)); + System.out.println(s1 == null);//直接判断比较好 + } +} + +public class Student { +} +``` + + + +*** + + + +### String + +#### 基本介绍 + +String 被声明为 final,因此不可被继承 **(Integer 等包装类也不能被继承)** + +```java +public final class String implements java.io.Serializable, Comparable, CharSequence { + /** The value is used for character storage. */ + private final char value[]; + /** Cache the hash code for the string */ + private int hash; // Default to 0 +} +``` + +在 Java 9 之后,String 类的实现改用 byte 数组存储字符串,同时使用 `coder` 来标识使用了哪种编码 + +value 数组被声明为 final,这意味着 value 数组初始化之后就不能再引用其它数组,并且 String 内部没有改变 value 数组的方法,因此可以**保证 String 不可变,也保证线程安全** + +注意:不能改变的意思是**每次更改字符串都会产生新的对象**,并不是对原始对象进行改变 + +```java +String s = "abc"; +s = s + "cd"; //s = abccd 新对象 +``` + + + +**** + + + +#### 常用方法 + +常用 API: + +* `public boolean equals(String s)`:比较两个字符串内容是否相同、区分大小写 + +* `public boolean equalsIgnoreCase(String anotherString)`:比较字符串的内容,忽略大小写 +* `public int length()`:返回此字符串的长度 +* `public String trim()`:返回一个字符串,其值为此字符串,并删除任何前导和尾随空格 +* `public String[] split(String regex)`:将字符串按给定的正则表达式分割成字符串数组 +* `public char charAt(int index)`:取索引处的值 +* `public char[] toCharArray()`:将字符串拆分为字符数组后返回 +* `public boolean startsWith(String prefix)`:测试此字符串是否以指定的前缀开头 +* `public int indexOf(String str)`:返回指定子字符串第一次出现的字符串内的索引,没有返回 -1 +* `public int lastIndexOf(String str)`:返回字符串最后一次出现 str 的索引,没有返回 -1 +* `public String substring(int beginIndex)`:返回子字符串,以原字符串指定索引处到结尾 +* `public String substring(int i, int j)`:指定索引处扩展到 j - 1 的位置,字符串长度为 j - i +* `public String toLowerCase()`:将此 String 所有字符转换为小写,使用默认语言环境的规则 +* `public String toUpperCase()`:使用默认语言环境的规则将此 String 所有字符转换为大写 +* `public String replace(CharSequence target, CharSequence replacement)`:使用新值,将字符串中的旧值替换,得到新的字符串 + +```java +String s = 123-78; +s.replace("-","");//12378 +``` + + + +*** + + + +#### 构造方式 + +构造方法: + +* `public String()`:创建一个空白字符串对象,不含有任何内容 +* `public String(char[] chs)`:根据字符数组的内容,来创建字符串对象 +* `public String(String original)`:根据传入的字符串内容,来创建字符串对象 + +直接赋值:`String s = "abc"` 直接赋值的方式创建字符串对象,内容就是 abc + +- 通过构造方法创建:通过 new 创建的字符串对象,每一次 new 都会申请一个内存空间,虽然内容相同,但是地址值不同,**返回堆内存中对象的引用** +- 直接赋值方式创建:以 `" "` 方式给出的字符串,只要字符序列相同(顺序和大小写),无论在程序代码中出现几次,JVM 都只会**在 String Pool 中创建一个字符串对象**,并在字符串池中维护 + +`String str = new String("abc")` 创建字符串对象: + +* 创建一个对象:字符串池中已经存在 abc 对象,那么直接在创建一个对象放入堆中,返回堆内引用 +* 创建两个对象:字符串池中未找到 abc 对象,那么分别在堆中和字符串池中创建一个对象,字符串池中的比较都是采用 equals() + + +`new String("a") + new String("b")` 创建字符串对象: + +* 对象 1:new StringBuilder() + +* 对象 2:new String("a")、对象 3:常量池中的 a + +* 对象 4:new String("b")、对象 5:常量池中的 b + + +* StringBuilder 的 toString(): + + ```java + @Override + public String toString() { + return new String(value, 0, count); + } + ``` + + * 对象 6:new String("ab") + * StringBuilder 的 toString() 调用,**在字符串常量池中没有生成 ab**,new String("ab") 会创建两个对象因为传参数的时候使用字面量创建了对象 ab,当使用数组构造 String 对象时,没有加入常量池的操作 + + + +*** + + + +#### String Pool + +##### 基本介绍 + +字符串常量池(String Pool / StringTable / 串池)保存着所有字符串字面量(literal strings),这些字面量在编译时期就确定,常量池类似于 Java 系统级别提供的**缓存**,存放对象和引用 + +* StringTable,类似 HashTable 结构,通过 `-XX:StringTableSize` 设置大小,JDK 1.8 中默认 60013 +* 常量池中的字符串仅是符号,第一次使用时才变为对象,可以避免重复创建字符串对象 +* 字符串**变量**的拼接的原理是 StringBuilder#append,append 方法比字符串拼接效率高(JDK 1.8) +* 字符串**常量**拼接的原理是编译期优化,拼接结果放入常量池 +* 可以使用 String 的 intern() 方法在运行过程将字符串添加到 String Pool 中 + + + +*** + + + +##### intern() + +JDK 1.8:当一个字符串调用 intern() 方法时,如果 String Pool 中: +* 存在一个字符串和该字符串值相等,就会返回 String Pool 中字符串的引用(需要变量接收) +* 不存在,会把对象的**引用地址**复制一份放入串池,并返回串池中的引用地址,前提是堆内存有该对象,因为 Pool 在堆中,为了节省内存不再创建新对象 + +JDK 1.6:将这个字符串对象尝试放入串池,如果有就不放入,返回已有的串池中的对象的引用;如果没有会把此对象复制一份,放入串池,把串池中的对象返回 + +```java +public class Demo { + // 常量池中的信息都加载到运行时常量池,这时a b ab是常量池中的符号,还不是java字符串对象,是懒惰的 + // ldc #2 会把 a 符号变为 "a" 字符串对象 ldc:反编译后的指令 + // ldc #3 会把 b 符号变为 "b" 字符串对象 + // ldc #4 会把 ab 符号变为 "ab" 字符串对象 + public static void main(String[] args) { + String s1 = "a"; // 懒惰的 + String s2 = "b"; + String s3 = "ab"; // 串池 + + String s4 = s1 + s2; // 返回的是堆内地址 + // 原理:new StringBuilder().append("a").append("b").toString() new String("ab") + + String s5 = "a" + "b"; // javac 在编译期间的优化,结果已经在编译期确定为ab + + System.out.println(s3 == s4); // false + System.out.println(s3 == s5); // true + + String x2 = new String("c") + new String("d"); // new String("cd") + // 虽然 new,但是在字符串常量池没有 cd 对象,因为 toString() 方法 + x2.intern(); + String x1 = "cd"; + + System.out.println(x1 == x2); //true + } +} +``` + +- == 比较基本数据类型:比较的是具体的值 +- == 比较引用数据类型:比较的是对象地址值 + +结论: + +```java +String s1 = "ab"; // 仅放入串池 +String s2 = new String("a") + new String("b"); // 仅放入堆 +// 上面两条指令的结果和下面的 效果 相同 +String s = new String("ab"); +``` + + + +**** + + + +##### 常见问题 + +问题一: + +```java +public static void main(String[] args) { + String s = new String("a") + new String("b");//new String("ab") + //在上一行代码执行完以后,字符串常量池中并没有"ab" + + String s2 = s.intern(); + //jdk6:串池中创建一个字符串"ab" + //jdk8:串池中没有创建字符串"ab",而是创建一个引用指向 new String("ab"),将此引用返回 + + System.out.println(s2 == "ab");//jdk6:true jdk8:true + System.out.println(s == "ab");//jdk6:false jdk8:true +} +``` + +问题二: + +```java +public static void main(String[] args) { + String str1 = new StringBuilder("58").append("tongcheng").toString(); + System.out.println(str1 == str1.intern());//true,字符串池中不存在,把堆中的引用复制一份放入串池 + + String str2 = new StringBuilder("ja").append("va").toString(); + System.out.println(str2 == str2.intern());//false,字符串池中存在,直接返回已经存在的引用 +} +``` + +原因: + +* System 类当调用 Version 的静态方法,导致 Version 初始化: + + ```java + private static void initializeSystemClass() { + sun.misc.Version.init(); + } + ``` + +* Version 类初始化时需要对静态常量字段初始化,被 launcher_name 静态常量字段所引用的 `"java"` 字符串字面量就被放入的字符串常量池: + + ```java + package sun.misc; + + public class Version { + private static final String launcher_name = "java"; + private static final String java_version = "1.8.0_221"; + private static final String java_runtime_name = "Java(TM) SE Runtime Environment"; + private static final String java_profile_name = ""; + private static final String java_runtime_version = "1.8.0_221-b11"; + //... + } + ``` + + + +*** + + + +##### 内存位置 + +Java 7 之前,String Pool 被放在运行时常量池中,属于永久代;Java 7 以后,String Pool 被移到堆中,这是因为永久代的空间有限,在大量使用字符串的场景下会导致 OutOfMemoryError 错误 + +演示 StringTable 位置: + +* `-Xmx10m` 设置堆内存 10m + +* 在 JDK8 下设置: `-Xmx10m -XX:-UseGCOverheadLimit`(运行参数在 Run Configurations VM options) + +* 在 JDK6 下设置: `-XX:MaxPermSize=10m` + + ```java + public static void main(String[] args) throws InterruptedException { + List list = new ArrayList(); + int i = 0; + try { + for (int j = 0; j < 260000; j++) { + list.add(String.valueOf(j).intern()); + i++; + } + } catch (Throwable e) { + e.printStackTrace(); + } finally { + System.out.println(i); + } + } + ``` + +![](https://seazean.oss-cn-beijing.aliyuncs.com/img/Java/JVM-内存图对比.png) + + + +*** + + + +#### 优化常量池 + +两种方式: + +* 调整 -XX:StringTableSize=桶个数,数量越少,性能越差 + +* intern 将字符串对象放入常量池,通过复用字符串的引用,减少内存占用 + +```java +/** + * 演示 intern 减少内存占用 + * -XX:StringTableSize=200000 -XX:+PrintStringTableStatistics + * -Xsx500m -Xmx500m -XX:+PrintStringTableStatistics -XX:StringTableSize=200000 + */ +public class Demo1_25 { + public static void main(String[] args) throws IOException { + List address = new ArrayList<>(); + System.in.read(); + for (int i = 0; i < 10; i++) { + //很多数据 + try (BufferedReader reader = new BufferedReader(new InputStreamReader(new FileInputStream("linux.words"), "utf-8"))) { + String line = null; + long start = System.nanoTime(); + while (true) { + line = reader.readLine(); + if(line == null) { + break; + } + address.add(line.intern()); + } + System.out.println("cost:" +(System.nanoTime()-start)/1000000); + } + } + System.in.read(); + } +} +``` + + + +*** + + + +#### 不可变好处 + +* 可以缓存 hash 值,例如 String 用做 HashMap 的 key,不可变的特性可以使得 hash 值也不可变,只要进行一次计算 +* String Pool 的需要,如果一个 String 对象已经被创建过了,就会从 String Pool 中取得引用,只有 String 是不可变的,才可能使用 String Pool +* 安全性,String 经常作为参数,String 不可变性可以保证参数不可变。例如在作为网络连接参数的情况下如果 String 是可变的,那么在网络连接过程中,String 被改变,改变 String 的那一方以为现在连接的是其它主机,而实际情况却不一定是 +* String 不可变性天生具备线程安全,可以在多个线程中安全地使用 +* 防止子类继承,破坏 String 的 API 的使用 + + + + + +*** + + + +### StringBuilder + +String StringBuffer 和 StringBuilder 区别: + +* String : **不可变**的字符序列,线程安全 +* StringBuffer : **可变**的字符序列,线程安全,底层方法加 synchronized,效率低 +* StringBuilder : **可变**的字符序列,JDK5.0 新增;线程不安全,效率高 + +相同点:底层使用 char[] 存储 + +构造方法: + +* `public StringBuilder()`:创建一个空白可变字符串对象,不含有任何内容 +* `public StringBuilder(String str)`:根据字符串的内容,来创建可变字符串对象 + +常用API : + +* `public StringBuilder append(任意类型)`:添加数据,并返回对象本身 +* `public StringBuilder reverse()`:返回相反的字符序列 +* `public String toString()`:通过 toString() 就可以实现把 StringBuilder 转换为 String + +存储原理: + +```java +String str = "abc"; +char data[] = {'a', 'b', 'c'}; +StringBuffer sb1 = new StringBuffer();//new byte[16] +sb1.append('a'); //value[0] = 'a'; +``` + +append 源码:扩容为二倍 + +```java +public AbstractStringBuilder append(String str) { + if (str == null) return appendNull(); + int len = str.length(); + ensureCapacityInternal(count + len); + str.getChars(0, len, value, count); + count += len; + return this; +} +private void ensureCapacityInternal(int minimumCapacity) { + // 创建超过数组长度就新的char数组,把数据拷贝过去 + if (minimumCapacity - value.length > 0) { + //int newCapacity = (value.length << 1) + 2;每次扩容2倍+2 + value = Arrays.copyOf(value, newCapacity(minimumCapacity)); + } +} +public void getChars(int srcBegin, int srcEnd, char dst[], int dstBegin) { + // 将字符串中的字符复制到目标字符数组中 + // 字符串调用该方法,此时value是字符串的值,dst是目标字符数组 + System.arraycopy(value, srcBegin, dst, dstBegin, srcEnd - srcBegin); +} +``` + + + + + +**** + + + +### Arrays + +Array 的工具类 Arrays + +常用API: + +* `public static String toString(int[] a)`:返回指定数组的内容的字符串表示形式 +* `public static void sort(int[] a)`:按照数字顺序排列指定的数组 +* `public static int binarySearch(int[] a, int key)`:利用二分查找返回指定元素的索引 +* `public static List asList(T... a)`:返回由指定数组支持的列表 + +```java +public class MyArraysDemo { + public static void main(String[] args) { + //按照数字顺序排列指定的数组 + int [] arr = {3,2,4,6,7}; + Arrays.sort(arr); + System.out.println(Arrays.toString(arr)); + + int [] arr = {1,2,3,4,5,6,7,8,9,10}; + int index = Arrays.binarySearch(arr, 0); + System.out.println(index); + //1,数组必须有序 + //2.如果要查找的元素存在,那么返回的是这个元素实际的索引 + //3.如果要查找的元素不存在,那么返回的是 (-插入点-1) + //插入点:如果这个元素在数组中,他应该在哪个索引上. + } + } +``` + + + + + +*** + + + +### Random + +用于生成伪随机数。 + +使用步骤: +1. 导入包:`import java.util.Random` +2. 创建对象:`Random r = new Random()` +3. 随机整数:`int num = r.nextInt(10)` + * 解释:10 代表的是一个范围,如果括号写 10,产生的随机数就是 0 - 9,括号写 20 的随机数则是 0 - 19 + * 获取 0 - 10:`int num = r.nextInt(10 + 1)` + +4. 随机小数:`public double nextDouble()` 从范围 `0.0d` 至 `1.0d` (左闭右开),伪随机地生成并返回 + + + +*** + + + +### System + +System 代表当前系统 + +静态方法: + +* `public static void exit(int status)`:终止 JVM 虚拟机,**非 0 是异常终止** + +* `public static long currentTimeMillis()`:获取当前系统此刻时间毫秒值 + +* `static void arraycopy(Object var0, int var1, Object var2, int var3, int var4)`:数组拷贝 + * 参数一:原数组 + * 参数二:从原数组的哪个位置开始赋值 + * 参数三:目标数组 + * 参数四:从目标数组的哪个位置开始赋值 + * 参数五:赋值几个 + +```java +public class SystemDemo { + public static void main(String[] args) { + //System.exit(0); // 0代表正常终止!! + long startTime = System.currentTimeMillis();//定义sdf 按照格式输出 + for(int i = 0; i < 10000; i++){输出i} + long endTime = new Date().getTime(); + System.out.println( (endTime - startTime)/1000.0 +"s");//程序用时 + + int[] arr1 = new int[]{10 ,20 ,30 ,40 ,50 ,60 ,70}; + int[] arr2 = new int[6]; // [ 0 , 0 , 0 , 0 , 0 , 0] + // 变成arrs2 = [0 , 30 , 40 , 50 , 0 , 0 ] + System.arraycopy(arr1, 2, arr2, 1, 3); + } +} +``` + + + + + +*** + + + +### Date + +构造器: + +* `public Date()`:创建当前系统的此刻日期时间对象。 +* `public Date(long time)`:把时间毫秒值转换成日期对象 + +方法: + +* `public long getTime()`:返回自 1970 年 1 月 1 日 00:00:00 GMT 以来总的毫秒数。 + +时间记录的两种方式: + +1. Date 日期对象 +2. 时间毫秒值:从 `1970-01-01 00:00:00` 开始走到此刻的总的毫秒值,1s = 1000ms + +```java +public class DateDemo { + public static void main(String[] args) { + Date d = new Date(); + System.out.println(d);//Fri Oct 16 21:58:44 CST 2020 + long time = d.getTime() + 121*1000;//过121s是什么时间 + System.out.println(time);//1602856875485 + + Date d1 = new Date(time); + System.out.println(d1);//Fri Oct 16 22:01:15 CST 2020 + } +} +``` + +```java +public static void main(String[] args){ + Date d = new Date(); + long startTime = d.getTime(); + for(int i = 0; i < 10000; i++){输出i} + long endTime = new Date().getTime(); + System.out.println( (endTime - startTime) / 1000.0 +"s"); + //运行一万次输出需要多长时间 +} +``` + + + +*** + + + +### DateFormat + +DateFormat 作用: + +1. 可以把“日期对象”或者“时间毫秒值”格式化成我们喜欢的时间形式(格式化时间) +2. 可以把字符串的时间形式解析成日期对象(解析字符串时间) + +DateFormat 是一个抽象类,不能直接使用,使用它的子类:SimpleDateFormat + +SimpleDateFormat 简单日期格式化类: + +* `public SimpleDateFormat(String pattern)`:指定时间的格式创建简单日期对象 +* `public String format(Date date) `:把日期对象格式化成我们喜欢的时间形式,返回字符串 +* `public String format(Object time)`:把时间毫秒值格式化成设定的时间形式,返回字符串! +* `public Date parse(String date)`:把字符串的时间解析成日期对象 + +>yyyy年MM月dd日 HH:mm:ss EEE a" 周几 上午下午 + +```java +public static void main(String[] args){ + Date date = new Date(); + SimpleDateFormat sdf = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss); + String time = sdf.format(date); + System.out.println(time);//2020-10-18 19:58:34 + //过121s后是什么时间 + long time = date.getTime(); + time+=121; + System.out.println(sdf.formate(time)); + String d = "2020-10-18 20:20:20";//格式一致 + Date newDate = sdf.parse(d); + System.out.println(sdf.format(newDate)); //按照前面的方法输出 +} +``` + + + + +**** + + + +### Calendar + +Calendar 代表了系统此刻日期对应的日历对象,是一个抽象类,不能直接创建对象 + +Calendar 日历类创建日历对象:`Calendar rightNow = Calendar.getInstance()`(**饿汉单例模式**) + +Calendar 的方法: + +* `public static Calendar getInstance()`:返回一个日历类的对象 +* `public int get(int field)`:取日期中的某个字段信息 +* `public void set(int field,int value)`:修改日历的某个字段信息 +* `public void add(int field,int amount)`:为某个字段增加/减少指定的值 +* `public final Date getTime()`:拿到此刻日期对象 +* `public long getTimeInMillis()`:拿到此刻时间毫秒值 + +```java +public static void main(String[] args){ + Calendar rightNow = Calendar.getInsance(); + int year = rightNow.get(Calendar.YEAR);//获取年 + int month = rightNow.get(Calendar.MONTH) + 1;//月要+1 + int days = rightNow.get(Calendar.DAY_OF_YEAR); + rightNow.set(Calendar.YEAR , 2099);//修改某个字段 + rightNow.add(Calendar.HOUR , 15);//加15小时 -15就是减去15小时 + Date date = rightNow.getTime();//日历对象 + long time = rightNow.getTimeInMillis();//时间毫秒值 + //700天后是什么日子 + rightNow.add(Calendar.DAY_OF_YEAR , 701); + Date date d = rightNow.getTime(); + SimpleDateFormat sdf = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss"); + System.out.println(sdf.format(d));//输出700天后的日期 +} +``` + + + +*** + + + +### LocalDateTime + +JDK1.8 新增,线程安全 + ++ LocalDate 表示日期(年月日) ++ LocalTime 表示时间(时分秒) ++ LocalDateTime 表示时间+ 日期 (年月日时分秒) + +构造方法: + +* public static LocalDateTime now():获取当前系统时间 +* public static LocalDateTime of(年, 月 , 日, 时, 分, 秒):使用指定年月日和时分秒初始化一个对象 + +常用API: + +| 方法名 | 说明 | +| --------------------------------------------------------- | ------------------------------------------------------------ | +| public int getYear() | 获取年 | +| public int getMonthValue() | 获取月份(1-12) | +| public int getDayOfMonth() | 获取月份中的第几天(1-31) | +| public int getDayOfYear() | 获取一年中的第几天(1-366) | +| public DayOfWeek getDayOfWeek() | 获取星期 | +| public int getMinute() | 获取分钟 | +| public int getHour() | 获取小时 | +| public LocalDate toLocalDate() | 转换成为一个 LocalDate 对象(年月日) | +| public LocalTime toLocalTime() | 转换成为一个 LocalTime 对象(时分秒) | +| public String format(指定格式) | 把一个 LocalDateTime 格式化成为一个字符串 | +| public LocalDateTime parse(准备解析的字符串, 解析格式) | 把一个日期字符串解析成为一个 LocalDateTime 对象 | +| public static DateTimeFormatter ofPattern(String pattern) | 使用指定的日期模板获取一个日期格式化器 DateTimeFormatter 对象 | + +```java +public class JDK8DateDemo2 { + public static void main(String[] args) { + LocalDateTime now = LocalDateTime.now(); + System.out.println(now); + + LocalDateTime localDateTime = LocalDateTime.of(2020, 11, 11, 11, 11, 11); + System.out.println(localDateTime); + DateTimeFormatter pattern = DateTimeFormatter.ofPattern("yyyy年MM月dd日 HH:mm:ss"); + String s = localDateTime.format(pattern); + LocalDateTime parse = LocalDateTime.parse(s, pattern); + } +} +``` + +| 方法名 | 说明 | +| ------------------------------------------- | -------------- | +| public LocalDateTime plusYears (long years) | 添加或者减去年 | +| public LocalDateTime withYear(int year) | 直接修改年 | + + + +**时间间隔** Duration 类API: + +| 方法名 | 说明 | +| ------------------------------------------------ | -------------------- | +| public static Period between(开始时间,结束时间) | 计算两个“时间"的间隔 | +| public int getYears() | 获得这段时间的年数 | +| public int getMonths() | 获得此期间的总月数 | +| public int getDays() | 获得此期间的天数 | +| public long toTotalMonths() | 获取此期间的总月数 | +| public static Durationbetween(开始时间,结束时间) | 计算两个“时间"的间隔 | +| public long toSeconds() | 获得此时间间隔的秒 | +| public long toMillis() | 获得此时间间隔的毫秒 | +| public long toNanos() | 获得此时间间隔的纳秒 | + +```java +public class JDK8DateDemo9 { + public static void main(String[] args) { + LocalDate localDate1 = LocalDate.of(2020, 1, 1); + LocalDate localDate2 = LocalDate.of(2048, 12, 12); + Period period = Period.between(localDate1, localDate2); + System.out.println(period);//P28Y11M11D + Duration duration = Duration.between(localDateTime1, localDateTime2); + System.out.println(duration);//PT21H57M58S + } +} +``` + + + +*** + + + +### Math + +Math 用于做数学运算 + +Math 类中的方法全部是静态方法,直接用类名调用即可: + +| 方法 | 说明 | +| -------------------------------------------- | --------------------------------- | +| public static int abs(int a) | 获取参数a的绝对值 | +| public static double ceil(double a) | 向上取整 | +| public static double floor(double a) | 向下取整 | +| public static double pow(double a, double b) | 获取 a 的 b 次幂 | +| public static long round(double a) | 四舍五入取整 | +| public static int max(int a,int b) | 返回较大值 | +| public static int min(int a,int b) | 返回较小值 | +| public static double random() | 返回值为 double 的正值,[0.0,1.0) | + +```java +public class MathDemo { + public static void main(String[] args) { + // 1.取绝对值:返回正数。 + System.out.println(Math.abs(10)); + System.out.println(Math.abs(-10.3)); + // 2.向上取整: 5 + System.out.println(Math.ceil(4.00000001)); // 5.0 + System.out.println(Math.ceil(-4.00000001));//4.0 + // 3.向下取整:4 + System.out.println(Math.floor(4.99999999)); // 4.0 + System.out.println(Math.floor(-4.99999999)); // 5.0 + // 4.求指数次方 + System.out.println(Math.pow(2 , 3)); // 2^3 = 8.0 + // 5.四舍五入 10 + System.out.println(Math.round(4.49999)); // 4 + System.out.println(Math.round(4.500001)); // 5 + System.out.println(Math.round(5.5));//6 + } +} +``` + + + +**** + + + +### DecimalFormat + +使任何形式的数字解析和格式化 + +```java +public static void main(String[]args){ + double pi = 3.1415927; //圆周率 + //取一位整数 + System.out.println(new DecimalFormat("0").format(pi));   //3 + //取一位整数和两位小数 + System.out.println(new DecimalFormat("0.00").format(pi)); //3.14 + //取两位整数和三位小数,整数不足部分以0填补。 + System.out.println(new DecimalFormat("00.000").format(pi));// 03.142 + //取所有整数部分 + System.out.println(new DecimalFormat("#").format(pi));   //3 + //以百分比方式计数,并取两位小数 + System.out.println(new DecimalFormat("#.##%").format(pi)); //314.16% + + long c =299792458;  //光速 + //显示为科学计数法,并取五位小数 + System.out.println(new DecimalFormat("#.#####E0").format(c));//2.99792E8 + //显示为两位整数的科学计数法,并取四位小数 + System.out.println(new DecimalFormat("00.####E0").format(c));//29.9792E7 + //每三位以逗号进行分隔。 + System.out.println(new DecimalFormat(",###").format(c));//299,792,458 + //将格式嵌入文本 + System.out.println(new DecimalFormat("光速大小为每秒,###米。").format(c)); + +} +``` + + + + + +*** + + + +### BigDecimal + +Java 在 java.math 包中提供的 API 类,用来对超过16位有效位的数进行精确的运算 + +构造方法: + +* `public static BigDecimal valueOf(double val)`:包装浮点数成为大数据对象。 +* `public BigDecimal(double val)` +* `public BigDecimal(String val)` + +常用API: + +* `public BigDecimal add(BigDecimal value)`:加法运算 +* `public BigDecimal subtract(BigDecimal value)`:减法运算 +* `public BigDecimal multiply(BigDecimal value)`:乘法运算 +* `public BigDecimal divide(BigDecimal value)`:除法运算 +* `public double doubleValue()`:把 BigDecimal 转换成 double 类型 +* `public int intValue()`:转为 int 其他类型相同 +* `public BigDecimal divide (BigDecimal value,精确几位,舍入模式)`:除法 + +```java +public class BigDecimalDemo { + public static void main(String[] args) { + // 浮点型运算的时候直接+ - * / 可能会出现数据失真(精度问题)。 + System.out.println(0.1 + 0.2); + System.out.println(1.301 / 100); + + double a = 0.1 ; + double b = 0.2 ; + double c = a + b ; + System.out.println(c);//0.30000000000000004 + + // 1.把浮点数转换成大数据对象运算 + BigDecimal a1 = BigDecimal.valueOf(a); + BigDecimal b1 = BigDecimal.valueOf(b); + BigDecimal c1 = a1.add(b1);//a1.divide(b1);也可以 + System.out.println(c1); + + // BigDecimal只是解决精度问题的手段,double数据才是我们的目的!! + double d = c1.doubleValue(); + } +} +``` + +总结: + +1. BigDecimal 是用来进行精确计算的 +2. 创建 BigDecimal 的对象,构造方法使用参数类型为字符串的 +3. 四则运算中的除法,如果除不尽请使用 divide 的三个参数的方法 + +```java +BigDecimal divide = bd1.divide(参与运算的对象,小数点后精确到多少位,舍入模式); +//参数1:表示参与运算的BigDecimal 对象。 +//参数2:表示小数点后面精确到多少位 +//参数3:舍入模式 +// BigDecimal.ROUND_UP 进一法 +// BigDecimal.ROUND_FLOOR 去尾法 +// BigDecimal.ROUND_HALF_UP 四舍五入 +``` + + + +*** + + + +### Regex + +#### 概述 + +正则表达式的作用:是一些特殊字符组成的校验规则,可以校验信息的正确性,校验邮箱、电话号码、金额等。 + +比如检验 qq 号: + +```java +public static boolean checkQQRegex(String qq){ + return qq!=null && qq.matches("\\d{4,}");//即是数字 必须大于4位数 +}// 用\\d 是因为\用来告诉它是一个校验类,不是普通的字符 比如 \t \n +``` + +java.util.regex 包主要包括以下三个类: + +- Pattern 类: + + Pattern 对象是一个正则表达式的编译表示。Pattern 类没有公共构造方法,要创建一个 Pattern 对象,必须首先调用其公共静态编译方法,返回一个 Pattern 对象。该方法接受一个正则表达式作为它的第一个参数 + +- Matcher 类: + + Matcher 对象是对输入字符串进行解释和匹配操作的引擎。与Pattern 类一样,Matcher 也没有公共构造方法,需要调用 Pattern 对象的 matcher 方法来获得一个 Matcher 对象 + +- PatternSyntaxException: + + PatternSyntaxException 是一个非强制异常类,它表示一个正则表达式模式中的语法错误。 + + + +*** + + + +#### 字符匹配 + +##### 普通字符 + +字母、数字、汉字、下划线、以及没有特殊定义的标点符号,都是“普通字符”。表达式中的普通字符,在匹配一个字符串的时候,匹配与之相同的一个字符。其他统称**元字符** + + + +*** + + + +##### 特殊字符 + +\r\n 是 Windows 中的文本行结束标签,在 Unix/Linux 则是 \n + +| 元字符 | 说明 | +| ------ | ------------------------------------------------------------ | +| \ | 将下一个字符标记为一个特殊字符或原义字符,告诉它是一个校验类,不是普通字符 | +| \f | 换页符 | +| \n | 换行符 | +| \r | 回车符 | +| \t | 制表符 | +| \\ | 代表 \ 本身 | +| () | 使用 () 定义一个子表达式。子表达式的内容可以当成一个独立元素 | + + + +*** + + + +##### 标准字符 + +能够与多种字符匹配的表达式,注意区分大小写,大写是相反的意思,只能校验**单**个字符。 + +| 元字符 | 说明 | +| ------ | ------------------------------------------------------------ | +| . | 匹配任意一个字符(除了换行符),如果要匹配包括 \n 在内的所有字符,一般用 [\s\S] | +| \d | 数字字符,0~9 中的任意一个,等价于 [0-9] | +| \D | 非数字字符,等价于 [ ^0-9] | +| \w | 大小写字母或数字或下划线,等价于[a-zA-Z_0-9_] | +| \W | 对\w取非,等价于[ ^\w] | +| \s | 空格、制表符、换行符等空白字符的其中任意一个,等价于[\f\n\r\t\v] | +| \S | 对 \s 取非 | + +\x 匹配十六进制字符,\0 匹配八进制,例如 \xA 对应值为 10 的 ASCII 字符 ,即 \n + + + +*** + + + +##### 自定义符 + +自定义符号集合,[ ] 方括号匹配方式,能够匹配方括号中**任意一个**字符 + +| 元字符 | 说明 | +| ------------ | ----------------------------------------- | +| [ab5@] | 匹配 "a" 或 "b" 或 "5" 或 "@" | +| [^abc] | 匹配 "a","b","c" 之外的任意一个字符 | +| [f-k] | 匹配 "f"~"k" 之间的任意一个字母 | +| [^A-F0-3] | 匹配 "A","F","0"~"3" 之外的任意一个字符 | +| [a-d[m-p]] | 匹配 a 到 d 或者 m 到 p:[a-dm-p](并集) | +| [a-z&&[m-p]] | 匹配 a 到 z 并且 m 到 p:[a-dm-p](交集) | +| [^] | 取反 | + +* 正则表达式的特殊符号,被包含到中括号中,则失去特殊意义,除了 ^,- 之外,需要在前面加 \ + +* 标准字符集合,除小数点外,如果被包含于中括号,自定义字符集合将包含该集合。 + 比如:[\d. \ -+] 将匹配:数字、小数点、+、- + + + +*** + + + +##### 量词字符 + +修饰匹配次数的特殊符号。 + +* 匹配次数中的贪婪模式(匹配字符越多越好,默认 !),\* 和 + 都是贪婪型元字符。 +* 匹配次数中的非贪婪模式(匹配字符越少越好,修饰匹配次数的特殊符号后再加上一个 ? 号) + +| 元字符 | 说明 | +| ------ | --------------------------------- | +| X? | X 一次或一次也没,有相当于 {0,1} | +| X* | X 不出现或出现任意次,相当于 {0,} | +| X+ | X 至少一次,相当于 {1,} | +| X{n} | X 恰好 n 次 | +| {n,} | X 至少 n 次 | +| {n,m} | X 至少 n 次,但是不超过 m 次 | + + + +*** + + + +#### 位置匹配 + +##### 字符边界 + +本组标记匹配的不是字符而是位置,符合某种条件的位置 + +| 元字符 | 说明 | +| ------ | ------------------------------------------------------------ | +| ^ | 与字符串开始的地方匹配(在字符集合中用来求非,在字符集合外用作匹配字符串的开头) | +| $ | 与字符串结束的地方匹配 | +| \b | 匹配一个单词边界 | + + + +*** + + + +##### 捕获组 + +捕获组是把多个字符当一个单独单元进行处理的方法,它通过对括号内的字符分组来创建。 + +在表达式 `((A)(B(C)))`,有四个这样的组:((A)(B(C)))、(A)、(B(C))、(C)(按照括号从左到右依次为 group(1)...) + +* 调用 matcher 对象的 groupCount 方法返回一个 int 值,表示 matcher 对象当前有多个捕获组。 +* 特殊的组 group(0)、group(),代表整个表达式,该组不包括在 groupCount 的返回值中。 + +| 表达式 | 说明 | +| ------------------------- | ------------------------------------------------------------ | +| \| (分支结构) | 左右两边表达式之间 "或" 关系,匹配左边或者右边 | +| () (捕获组) | (1) 在被修饰匹配次数的时候,括号中的表达式可以作为整体被修饰
(2) 取匹配结果的时候,括号中的表达式匹配到的内容可以被单独得到
(3) 每一对括号分配一个编号,()的捕获根据左括号的顺序从 1 开始自动编号。捕获元素编号为零的第一个捕获是由整个正则表达式模式匹配的文本 | +| (?:Expression) 非捕获组 | 一些表达式中,不得不使用( ),但又不需要保存 () 中子表达式匹配的内容,这时可以用非捕获组来抵消使用( )带来的副作用。 | + + + +*** + + + +##### 反向引用 + +反向引用(\number),又叫回溯引用: + +* 每一对()会分配一个编号,使用 () 的捕获根据左括号的顺序从1开始自动编号 + +* 通过反向引用,可以对分组已捕获的字符串进行引用,继续匹配 + +* **把匹配到的字符重复一遍在进行匹配** + +* 应用 1: + + ```java + String regex = "((\d)3)\1[0-9](\w)\2{2}"; + ``` + + * 首先匹配 ((\d)3),其次 \1 匹配 ((\d)3) 已经匹配到的内容,\2 匹配 (\d), {2} 指的是 \2 的值出现两次 + * 实例:23238n22(匹配到 2 未来就继续匹配 2) + * 实例:43438n44 + +* 应用 2:爬虫 + + ```java + String regex = "<(h[1-6])>\w*?<\/\1>"; + ``` + + 匹配结果 + + ```java +

x

//匹配 +

x

//匹配 +

x

//不匹配 + ``` + + + +*** + + + +##### 零宽断言 + +预搜索(零宽断言)(环视) + +* 只进行子表达式的匹配,匹配内容不计入最终的匹配结果,是零宽度 + +* 判断当前位置的前后字符,是否符合指定的条件,但不匹配前后的字符,**是对位置的匹配** + +* 正则表达式匹配过程中,如果子表达式匹配到的是字符内容,而非位置,并被保存到最终的匹配结果中,那么就认为这个子表达式是占有字符的;如果子表达式匹配的仅仅是位置,或者匹配的内容并不保存到最终的匹配结果中,那么就认为这个子表达式是**零宽度**的。占有字符还是零宽度,是针对匹配的内容是否保存到最终的匹配结果中而言的 + + | 表达式 | 说明 | + | -------- | --------------------------------------- | + | (?=exp) | 断言自身出现的位置的后面能匹配表达式exp | + | (?<=exp) | 断言自身出现的位置的前面能匹配表达式exp | + | (?!exp) | 断言此位置的后面不能匹配表达式exp | + | (?(接口) + / \ + Set(接口) List(接口) + / \ / \ + HashSet(实现类) TreeSet<>(实现类) ArrayList(实现类) LinekdList<>(实现类) + / +LinkedHashSet<>(实现类) +``` + +**集合的特点:** + +* Set 系列集合:添加的元素是无序,不重复,无索引的 + * HashSet:添加的元素是无序,不重复,无索引的 + * LinkedHashSet:添加的元素是有序,不重复,无索引的 + * TreeSet:不重复,无索引,按照大小默认升序排序 +* List 系列集合:添加的元素是有序,可重复,有索引 + * ArrayList:添加的元素是有序,可重复,有索引 + * LinekdList:添加的元素是有序,可重复,有索引 + + + +*** + + + +#### API + +Collection 是集合的祖宗类,它的功能是全部集合都可以继承使用的,所以要学习它。 + +Collection 子类的构造器都有可以包装其他子类的构造方法,如: + +* `public ArrayList(Collection c)`:构造新集合,元素按照由集合的迭代器返回的顺序 + +* `public HashSet(Collection c)`:构造一个包含指定集合中的元素的新集合 + +Collection API 如下: + +* `public boolean add(E e)`:把给定的对象添加到当前集合中 。 +* `public void clear()`:清空集合中所有的元素。 +* `public boolean remove(E e)`:把给定的对象在当前集合中删除。 +* `public boolean contains(Object obj)`:判断当前集合中是否包含给定的对象。 +* `public boolean isEmpty()`:判断当前集合是否为空。 +* `public int size()`:返回集合中元素的个数。 +* `public Object[] toArray()`:把集合中的元素,存储到数组中 +* `public boolean addAll(Collection c)`:将指定集合中的所有元素添加到此集合 + +```java +public class CollectionDemo { + public static void main(String[] args) { + Collection sets = new HashSet<>(); + sets.add("MyBatis"); + System.out.println(sets.add("Java"));//true + System.out.println(sets.add("Java"));//false + sets.add("Spring"); + sets.add("MySQL"); + System.out.println(sets)//[]无序的; + System.out.println(sets.contains("java"));//true 存在 + Object[] arrs = sets.toArray(); + System.out.println("数组:"+ Arrays.toString(arrs)); + + Collection c1 = new ArrayList<>(); + c1.add("java"); + Collection c2 = new ArrayList<>(); + c2.add("ee"); + c1.addAll(c2);// c1:[java,ee] c2:[ee]; + } +} +``` + + + +*** + + + +#### 遍历 + +Collection 集合的遍历方式有三种: + +集合可以直接输出内容,因为底层重写了 toString() 方法 + +1. 迭代器 + + * `public Iterator iterator()`:获取集合对应的迭代器,用来遍历集合中的元素的 + * `E next()`:获取下一个元素值 + * `boolean hasNext()`:判断是否有下一个元素,有返回 true ,反之返回 false + * `default void remove()`:从底层集合中删除此迭代器返回的最后一个元素,这种方法只能在每次调用 next() 时调用一次 + +2. 增强 for 循环:可以遍历集合或者数组,遍历集合实际上是迭代器遍历的简化写法 + + ```java + for(被遍历集合或者数组中元素的类型 变量名称 : 被遍历集合或者数组){ + + } + ``` + + 缺点:遍历无法知道遍历到了哪个元素了,因为没有索引 + +3. JDK 1.8 开始之后的新技术 Lambda 表达式 + + ```java + public class CollectionDemo { + public static void main(String[] args) { + Collection lists = new ArrayList<>(); + lists.add("aa"); + lists.add("bb"); + lists.add("cc"); + System.out.println(lists); // lists = [aa, bb, cc] + //迭代器流程 + // 1.得到集合的迭代器对象。 + Iterator it = lists.iterator(); + // 2.使用while循环遍历。 + while(it.hasNext()){ + String ele = it.next(); + System.out.println(ele); + } + + //增强for + for (String ele : lists) { + System.out.println(ele); + } + //lambda表达式 + lists.forEach(s -> { + System.out.println(s); + }); + } + } + ``` + + + + + +*** + + + +#### List + +##### 概述 + +List 集合继承了 Collection 集合全部的功能。 + +List 系列集合有索引,所以多了很多按照索引操作元素的功能:for 循环遍历(4 种遍历) + +List 系列集合: + +* ArrayList:添加的元素是有序,可重复,有索引 + +* LinekdList:添加的元素是有序,可重复,有索引 + + + +*** + + + +##### ArrayList + +###### 介绍 + +ArrayList 添加的元素,是有序,可重复,有索引的 + +* `public boolean add(E e)`:将指定的元素追加到此集合的末尾 +* `public void add(int index, E element)`:将指定的元素,添加到该集合中的指定位置上 +* `public E get(int index)`:返回集合中指定位置的元素 +* `public E remove(int index)`:移除列表中指定位置的元素,返回的是被移除的元素 +* `public E set(int index, E element)`:用指定元素替换集合中指定位置的元素,返回更新前的元素值 +* `int indexOf(Object o)`:返回列表中指定元素第一次出现的索引,如果不包含此元素,则返回 -1 + +```java +public static void main(String[] args){ + List lists = new ArrayList<>();//多态 + lists.add("java1"); + lists.add("java1");//可以重复 + lists.add("java2"); + for(int i = 0 ; i < lists.size() ; i++ ) { + String ele = lists.get(i); + System.out.println(ele); + } +} +``` + + + +*** + + + +###### 源码 + +ArrayList 实现类集合底层**基于数组存储数据**的,查询快,增删慢,支持快速随机访问 + +```java +public class ArrayList extends AbstractList + implements List, RandomAccess, Cloneable, java.io.Serializable{} +``` + +- `RandomAccess` 是一个标志接口,表明实现这个这个接口的 List 集合是支持**快速随机访问**的。在 `ArrayList` 中,我们即可以通过元素的序号快速获取元素对象,这就是快速随机访问。 +- `ArrayList` 实现了 `Cloneable` 接口 ,即覆盖了函数 `clone()`,能被克隆 +- `ArrayList` 实现了 `Serializable ` 接口,这意味着 `ArrayList` 支持序列化,能通过序列化去传输 + +核心方法: + +* 构造函数:以无参数构造方法创建 ArrayList 时,实际上初始化赋值的是一个空数组。当真正对数组进行添加元素操作时,才真正分配容量(惰性初始化),即向数组中添加第一个元素时,**数组容量扩为 10** + +* 添加元素: + + ```java + // e 插入的元素 elementData底层数组 size 插入的位置 + public boolean add(E e) { + ensureCapacityInternal(size + 1); // Increments modCount!! + elementData[size++] = e; // 插入size位置,然后加一 + return true; + } + ``` + + 当 add 第 1 个元素到 ArrayList,size 是 0,进入 ensureCapacityInternal 方法, + + ```java + private void ensureCapacityInternal(int minCapacity) { + ensureExplicitCapacity(calculateCapacity(elementData, minCapacity)); + } + ``` + + ```java + private static int calculateCapacity(Object[] elementData, int minCapacity) { + // 判断elementData是不是空数组 + if (elementData == DEFAULTCAPACITY_EMPTY_ELEMENTDATA) { + // 返回默认值和最小需求容量最大的一个 + return Math.max(DEFAULT_CAPACITY, minCapacity); + } + return minCapacity; + } + ``` + + 如果需要的容量大于数组长度,进行扩容: + + ```java + // 判断是否需要扩容 + private void ensureExplicitCapacity(int minCapacity) { + modCount++; + // 索引越界 + if (minCapacity - elementData.length > 0) + // 调用grow方法进行扩容,调用此方法代表已经开始扩容了 + grow(minCapacity); + } + ``` + + 指定索引插入,**在旧数组上操作**: + + ```java + public void add(int index, E element) { + rangeCheckForAdd(index); + ensureCapacityInternal(size + 1); // Increments modCount!! + // 将指定索引后的数据后移 + System.arraycopy(elementData, index, elementData, index + 1, size - index); + elementData[index] = element; + size++; + } + ``` + +* 扩容:新容量的大小为 `oldCapacity + (oldCapacity >> 1)`,`oldCapacity >> 1` 需要取整,所以新容量大约是旧容量的 1.5 倍左右,即 oldCapacity+oldCapacity/2 + + 扩容操作需要调用 `Arrays.copyOf()`(底层 `System.arraycopy()`)把原数组整个复制到**新数组**中,这个操作代价很高,因此最好在创建 ArrayList 对象时就指定大概的容量大小,减少扩容操作的次数 + + ```java + private void grow(int minCapacity) { + int oldCapacity = elementData.length; + int newCapacity = oldCapacity + (oldCapacity >> 1); + //检查新容量是否大于最小需要容量,若小于最小需要容量,就把最小需要容量当作数组的新容量 + if (newCapacity - minCapacity < 0) + newCapacity = minCapacity;//不需要扩容计算 + //检查新容量是否大于最大数组容量 + if (newCapacity - MAX_ARRAY_SIZE > 0) + //如果minCapacity大于最大容量,则新容量则为`Integer.MAX_VALUE` + //否则,新容量大小则为 MAX_ARRAY_SIZE 即为 `Integer.MAX_VALUE - 8` + newCapacity = hugeCapacity(minCapacity); + elementData = Arrays.copyOf(elementData, newCapacity); + } + ``` + + MAX_ARRAY_SIZE:要分配的数组的最大大小,分配更大的**可能**会导致 + + * OutOfMemoryError:Requested array size exceeds VM limit(请求的数组大小超出 VM 限制) + * OutOfMemoryError: Java heap space(堆区内存不足,可以通过设置 JVM 参数 -Xmx 来调节) + +* 删除元素:需要调用 System.arraycopy() 将 index+1 后面的元素都复制到 index 位置上,在旧数组上操作,该操作的时间复杂度为 O(N),可以看到 ArrayList 删除元素的代价是非常高的 + + ```java + public E remove(int index) { + rangeCheck(index); + modCount++; + E oldValue = elementData(index); + + int numMoved = size - index - 1; + if (numMoved > 0) + System.arraycopy(elementData, index+1, elementData, index, numMoved); + elementData[--size] = null; // clear to let GC do its work + + return oldValue; + } + ``` + +* 序列化:ArrayList 基于数组并且具有动态扩容特性,因此保存元素的数组不一定都会被使用,就没必要全部进行序列化。保存元素的数组 elementData 使用 transient 修饰,该关键字声明数组默认不会被序列化 + + ```java + transient Object[] elementData; + ``` + +* ensureCapacity:增加此实例的容量,以确保它至少可以容纳最小容量参数指定的元素数,减少增量重新分配的次数 + + ```java + public void ensureCapacity(int minCapacity) { + if (minCapacity > elementData.length + && !(elementData == DEFAULTCAPACITY_EMPTY_ELEMENTDATA + && minCapacity <= DEFAULT_CAPACITY)) { + modCount++; + grow(minCapacity); + } + } + ``` + +* **Fail-Fast**:快速失败,modCount 用来记录 ArrayList **结构发生变化**的次数,结构发生变化是指添加或者删除至少一个元素的操作,或者是调整内部数组的大小,仅仅只是设置元素的值不算结构发生变化 + + 在进行序列化或者迭代等操作时,需要比较操作前后 modCount 是否改变,改变了抛出 ConcurrentModificationException 异常 + + ```java + public Iterator iterator() { + return new Itr(); + } + ``` + + ```java + private class Itr implements Iterator { + int cursor; // index of next element to return + int lastRet = -1; // index of last element returned; -1 if no such + int expectedModCount = modCount; + + Itr() {} + + public boolean hasNext() { + return cursor != size; + } + + // 获取下一个元素时首先判断结构是否发生变化 + public E next() { + checkForComodification(); + // ..... + } + // modCount 被其他线程改变抛出并发修改异常 + final void checkForComodification() { + if (modCount != expectedModCount) + throw new ConcurrentModificationException(); + } + // 【允许删除操作】 + public void remove() { + // ... + checkForComodification(); + // ... + // 删除后重置 expectedModCount + expectedModCount = modCount; + } + } + ``` + + + + + +*** + + + +##### Vector + +同步:Vector 的实现与 ArrayList 类似,但是方法上使用了 synchronized 进行同步 + +构造:默认长度为 10 的数组 + +扩容:Vector 的构造函数可以传入 capacityIncrement 参数,作用是在扩容时使容量 capacity 增长 capacityIncrement,如果这个参数的值小于等于 0(默认0),扩容时每次都令 capacity 为原来的两倍 + +对比 ArrayList + +1. Vector 是同步的,开销比 ArrayList 要大,访问速度更慢。最好使用 ArrayList 而不是 Vector,因为同步操作完全可以由程序来控制 + +2. Vector 每次扩容请求其大小的 2 倍(也可以通过构造函数设置增长的容量),而 ArrayList 是 1.5 倍 + +3. 底层都是 `Object[]` 数组存储 + + + +**** + + + +##### LinkedList + +###### 介绍 + +LinkedList 也是 List 的实现类:基于**双向链表**实现,使用 Node 存储链表节点信息,增删比较快,查询慢 + +LinkedList 除了拥有 List 集合的全部功能还多了很多操作首尾元素的特殊功能: + +* `public boolean add(E e)`:将指定元素添加到此列表的结尾 +* `public E poll()`:检索并删除此列表的头(第一个元素) +* `public void addFirst(E e)`:将指定元素插入此列表的开头 +* `public void addLast(E e)`:将指定元素添加到此列表的结尾 +* `public E pop()`:从此列表所表示的堆栈处弹出一个元素 +* `public void push(E e)`:将元素推入此列表所表示的堆栈 +* `public int indexOf(Object o)`:返回此列表中指定元素的第一次出现的索引,如果不包含返回 -1 +* `public int lastIndexOf(Object o)`:从尾遍历找 +* ` public boolean remove(Object o)`:一次只删除一个匹配的对象,如果删除了匹配对象返回 true +* `public E remove(int index)`:删除指定位置的元素 + +```java +public class ListDemo { + public static void main(String[] args) { + // 1.用LinkedList做一个队列:先进先出,后进后出。 + LinkedList queue = new LinkedList<>(); + // 入队 + queue.addLast("1号"); + queue.addLast("2号"); + queue.addLast("3号"); + System.out.println(queue); // [1号, 2号, 3号] + // 出队 + System.out.println(queue.removeFirst());//1号 + System.out.println(queue.removeFirst());//2号 + System.out.println(queue);//[3号] + + // 做一个栈 先进后出 + LinkedList stack = new LinkedList<>(); + // 压栈 + stack.push("第1颗子弹");//addFirst(e); + stack.push("第2颗子弹"); + stack.push("第3颗子弹"); + System.out.println(stack); // [ 第3颗子弹, 第2颗子弹, 第1颗子弹] + // 弹栈 + System.out.println(stack.pop());//removeFirst(); 第3颗子弹 + System.out.println(stack.pop()); + System.out.println(stack);// [第1颗子弹] + } +} +``` + + + +*** + + + +###### 源码 + +LinkedList 是一个实现了 List 接口的**双端链表**,支持高效的插入和删除操作,另外也实现了 Deque 接口,使得 LinkedList 类也具有队列的特性 + +![](https://seazean.oss-cn-beijing.aliyuncs.com/img/Java/LinkedList底层结构.png) + +核心方法: + +* 使 LinkedList 变成线程安全的,可以调用静态类 Collections 类中的 synchronizedList 方法: + + ```java + List list = Collections.synchronizedList(new LinkedList(...)); + ``` + +* 私有内部类 Node:这个类代表双端链表的节点 Node + + ```java + private static class Node { + E item; + Node next; + Node prev; + + Node(Node prev, E element, Node next) { + this.item = element; + this.next = next; + this.prev = prev; + } + } + ``` + +* 构造方法:只有无参构造和用已有的集合创建链表的构造方法 + +* 添加元素:默认加到尾部 + + ```java + public boolean add(E e) { + linkLast(e); + return true; + } + ``` + +* 获取元素:`get(int index)` 根据指定索引返回数据 + + * 获取头节点 (index=0):`getFirst()、element()、peek()、peekFirst()` 这四个获取头结点方法的区别在于对链表为空时的处理方式,是抛出异常还是返回NULL,其中 `getFirst() element()` 方法将会在链表为空时,抛出异常 + * 获取尾节点 (index=-1):getLast() 方法在链表为空时,抛出 NoSuchElementException,而 peekLast() 不会,只会返回 null + +* 删除元素: + + * remove()、removeFirst()、pop():删除头节点 + * removeLast()、pollLast():删除尾节点,removeLast()在链表为空时抛出NoSuchElementException,而pollLast()方法返回null + +对比 ArrayList + +1. 是否保证线程安全:ArrayList 和 LinkedList 都是不同步的,也就是不保证线程安全 +2. 底层数据结构: + * Arraylist 底层使用的是 `Object` 数组 + * LinkedList 底层使用的是双向链表数据结构(JDK1.6 之前为循环链表,JDK1.7 取消了循环) +3. 插入和删除是否受元素位置的影响: + * ArrayList 采用数组存储,所以插入和删除元素受元素位置的影响 + * LinkedList采 用链表存储,所以对于`add(E e)`方法的插入,删除元素不受元素位置的影响 +4. 是否支持快速随机访问: + * LinkedList 不支持高效的随机元素访问,ArrayList 支持 + * 快速随机访问就是通过元素的序号快速获取元素对象(对应于 `get(int index)` 方法) +5. 内存空间占用: + * ArrayList 的空间浪费主要体现在在 list 列表的结尾会预留一定的容量空间 + * LinkedList 的空间花费则体现在它的每一个元素都需要消耗比 ArrayList更多的空间(因为要存放直接后继和直接前驱以及数据) + + + +*** + + + +#### Set + +##### 概述 + +Set 系列集合: + +* HashSet:添加的元素是无序,不重复,无索引的 +* LinkedHashSet:添加的元素是有序,不重复,无索引的 +* TreeSet:不重复,无索引,按照大小默认升序排序 + +**注意**:没有索引,不能使用普通 for 循环遍历 + + + +*** + + + +##### HashSet + +哈希值: + +- 哈希值:JDK 根据对象的地址或者字符串或者数字计算出来的数值 + +- 获取哈希值:Object 类中的 public int hashCode() + +- 哈希值的特点 + + - 同一个对象多次调用 hashCode() 方法返回的哈希值是相同的 + - 默认情况下,不同对象的哈希值是不同的,而重写 hashCode() 方法,可以实现让不同对象的哈希值相同 + +**HashSet 底层就是基于 HashMap 实现,值是 PRESENT = new Object()** + +Set 集合添加的元素是无序,不重复的。 + +* 是如何去重复的? + + ```java + 1.对于有值特性的,Set集合可以直接判断进行去重复。 + 2.对于引用数据类型的类对象,Set集合是按照如下流程进行是否重复的判断。 + Set集合会让两两对象,先调用自己的hashCode()方法得到彼此的哈希值(所谓的内存地址) + 然后比较两个对象的哈希值是否相同,如果不相同则直接认为两个对象不重复。 + 如果哈希值相同,会继续让两个对象进行equals比较内容是否相同,如果相同认为真的重复了 + 如果不相同认为不重复。 + + Set集合会先让对象调用hashCode()方法获取两个对象的哈希值比较 + / \ + false true + / \ + 不重复 继续让两个对象进行equals比较 + / \ + false true + / \ + 不重复 重复了 + ``` + +* Set 系列集合元素无序的根本原因 + + Set 系列集合添加元素无序的根本原因是因为**底层采用了哈希表存储元素**。 + + * JDK 1.8 之前:哈希表 = 数组(初始容量16) + 链表 + (哈希算法) + * JDK 1.8 之后:哈希表 = 数组(初始容量16) + 链表 + 红黑树 + (哈希算法) + * 当链表长度超过阈值 8 且当前数组的长度 > 64时,将链表转换为红黑树,减少了查找时间 + * 当链表长度超过阈值 8 且当前数组的长度 < 64时,扩容 + + ![](https://seazean.oss-cn-beijing.aliyuncs.com/img/Java/HashSet底层结构哈希表.png) + + 每个元素的 hashcode() 的值进行响应的算法运算,计算出的值相同的存入一个数组块中,以链表的形式存储,如果链表长度超过8就采取红黑树存储,所以输出的元素是无序的。 + +* 如何设置只要对象内容一样,就希望集合认为重复:**重写 hashCode 和 equals 方法** + + + +**** + + + +##### Linked + +LinkedHashSet 为什么是有序的? + +LinkedHashSet 底层依然是使用哈希表存储元素的,但是每个元素都额外带一个链来维护添加顺序,不光增删查快,还有顺序,缺点是多了一个存储顺序的链会**占内存空间**,而且不允许重复,无索引 + + + +**** + + + +##### TreeSet + +TreeSet 集合自排序的方式: + +1. 有值特性的元素直接可以升序排序(浮点型,整型) +2. 字符串类型的元素会按照首字符的编号排序 +3. 对于自定义的引用数据类型,TreeSet 默认无法排序,执行的时候报错,因为不知道排序规则 + +自定义的引用数据类型,TreeSet 默认无法排序,需要定制排序的规则,方案有 2 种: + + * 直接为**对象的类**实现比较器规则接口 Comparable,重写比较方法: + + 方法:`public int compareTo(Employee o): this 是比较者, o 是被比较者` + + * 比较者大于被比较者,返回正数 + * 比较者小于被比较者,返回负数 + * 比较者等于被比较者,返回 0 + + * 直接为**集合**设置比较器 Comparator 对象,重写比较方法: + + 方法:`public int compare(Employee o1, Employee o2): o1 比较者, o2 被比较者` + + * 比较者大于被比较者,返回正数 + * 比较者小于被比较者,返回负数 + * 比较者等于被比较者,返回 0 + +注意:如果类和集合都带有比较规则,优先使用集合自带的比较规则 + +```java +public class TreeSetDemo{ + public static void main(String[] args){ + Set students = new TreeSet<>(); + Collections.add(students,s1,s2,s3); + System.out.println(students);//按照年龄比较 升序 + + Set s = new TreeSet<>(new Comparator(){ + @Override + public int compare(Student o1, Student o2) { + // o1比较者 o2被比较者 + return o2.getAge() - o1.getAge();//降序 + } + }); + } +} + +public class Student implements Comparable{ + private String name; + private int age; + // 重写了比较方法。 + // e1.compareTo(o) + // 比较者:this + // 被比较者:o + // 需求:按照年龄比较 升序,年龄相同按照姓名 + @Override + public int compareTo(Student o) { + int result = this.age - o.age; + return result == 0 ? this.getName().compareTo(o.getName):result; + } +} +``` + +比较器原理:底层是以第一个元素为基准,加一个新元素,就会和第一个元素比,如果大于,就继续和大于的元素进行比较,直到遇到比新元素大的元素为止,放在该位置的左边(红黑树) + + + + +*** + + + +#### Queue + +Queue:队列,先进先出的特性 + +PriorityQueue 是优先级队列,底层存储结构为 Object[],默认实现为小顶堆,每次出队最小的元素 + +构造方法: + +* `public PriorityQueue()`:构造默认长度为 11 的队列(数组) + +* `public PriorityQueue(Comparator comparator)`:利用比较器自定义堆排序的规则 + + ```java + Queue pq = new PriorityQueue<>((v1, v2) -> v2 - v1);//实现大顶堆 + +常用 API: + +* `public boolean offer(E e)`:将指定的元素插入到此优先级队列的**尾部** +* `public E poll() `:检索并删除此队列的**头元素**,如果此队列为空,则返回 null +* `public E peek()`:检索但不删除此队列的头,如果此队列为空,则返回 null +* `public boolean remove(Object o)`:从该队列中删除指定元素(如果存在),删除元素 e 使用 o.equals(e) 比较,如果队列包含多个这样的元素,删除第一个 + + + +**** + + + +#### Collections + +java.utils.Collections:集合**工具类**,Collections 并不属于集合,是用来操作集合的工具类 + +Collections 有几个常用的API: + +* `public static boolean addAll(Collection c, T... e)`:给集合对象批量添加元素 +* `public static void shuffle(List list)`:打乱集合顺序 +* `public static void sort(List list)`:将集合中元素按照默认规则排序 +* `public static void sort(List list,Comparator )`:集合中元素按照指定规则排序 +* `public static List synchronizedList(List list)`:返回由指定 list 支持的线程安全 list +* `public static Set singleton(T o)`:返回一个只包含指定对象的不可变组 + +```java +public class CollectionsDemo { + public static void main(String[] args) { + Collection names = new ArrayList<>(); + Collections.addAll(names,"张","王","李","赵"); + + List scores = new ArrayList<>(); + Collections.addAll(scores, 98.5, 66.5 , 59.5 , 66.5 , 99.5 ); + Collections.shuffle(scores); + Collections.sort(scores); // 默认升序排序! + System.out.println(scores); + + List students = new ArrayList<>(); + Collections.addAll(students,s1,s2,s3,s4); + Collections.sort(students,new Comparator(){ + + }) + } +} + +public class Student{ + private String name; + private int age; +} +``` + + + + + +*** + + + +### Map + +#### 概述 + +Collection 是单值集合体系,Map集合是一种双列集合,每个元素包含两个值。 + +Map集合的每个元素的格式:key=value(键值对元素),Map集合也被称为键值对集合 + +Map集合的完整格式:`{key1=value1, key2=value2, key3=value3, ...}` + +``` +Map集合的体系: + Map(接口,Map集合的祖宗类) + / \ + TreeMap HashMap(实现类,经典的,用的最多) + \ + LinkedHashMap(实现类) +``` + +Map 集合的特点: + +1. Map 集合的特点都是由键决定的 +2. Map 集合的键是无序,不重复的,无索引的(Set) +3. Map 集合的值无要求(List) +4. Map 集合的键值对都可以为 null +5. Map 集合后面重复的键对应元素会覆盖前面的元素 + +HashMap:元素按照键是无序,不重复,无索引,值不做要求 + +LinkedHashMap:元素按照键是有序,不重复,无索引,值不做要求 + + + +*** + + + +#### 常用API + +Map 集合的常用 API + +* `public V put(K key, V value)`:把指定的键与值添加到 Map 集合中,**重复的键会覆盖前面的值元素** +* `public V remove(Object key)`:把指定的键对应的键值对元素在集合中删除,返回被删除元素的值 +* `public V get(Object key)`:根据指定的键,在 Map 集合中获取对应的值 +* `public Set keySet()`:获取 Map 集合中所有的键,存储到 **Set 集合**中 +* `public Collection values()`:获取全部值的集合,存储到 **Collection 集合** +* `public Set> entrySet()`:获取Map集合中所有的键值对对象的集合 +* `public boolean containsKey(Object key)`:判断该集合中是否有此键 + +```java +public class MapDemo { + public static void main(String[] args) { + Map maps = new HashMap<>(); + maps.put(.....); + System.out.println(maps.isEmpty());//false + Integer value = maps.get("....");//返回键值对象 + Set keys = maps.keySet();//获取Map集合中所有的键, + //Map集合的键是无序不重复的,所以返回的是一个Set集合 + Collection values = maps.values(); + //Map集合的值是不做要求的,可能重复,所以值要用Collection集合接收! + } +} +``` + + + +*** + + + +#### 遍历方式 + +Map集合的遍历方式有:3种。 + +1. “键找值”的方式遍历:先获取 Map 集合全部的键,再根据遍历键找值。 +2. “键值对”的方式遍历:难度较大,采用增强 for 或者迭代器 +3. JDK 1.8 开始之后的新技术:foreach,采用 Lambda 表达式 + +集合可以直接输出内容,因为底层重写了 toString() 方法 + +```java +public static void main(String[] args){ + Map maps = new HashMap<>(); + //(1)键找值 + Set keys = maps.keySet(); + for(String key : keys) { + System.out.println(key + "=" + maps.get(key)); + } + //Iterator iterator = hm.keySet().iterator(); + + //(2)键值对 + //(2.1)普通方式 + Set> entries = maps.entrySet(); + for (Map.Entry entry : entries) { + System.out.println(entry.getKey() + "=>" + entry.getValue()); + } + //(2.2)迭代器方式 + Iterator> iterator = maps.entrySet().iterator(); + while (iterator.hasNext()) { + Map.Entry entry = iterator.next(); + System.out.println(entry.getKey() + "=" + entry.getValue()); + + } + //(3) Lamda + maps.forEach((k,v) -> { + System.out.println(k + "==>" + v); + }) +} +``` + + + +*** + + + +#### HashMap + +##### 基本介绍 + +HashMap 基于哈希表的 Map 接口实现,是以 key-value 存储形式存在,主要用来存放键值对 + +特点: + +* HashMap 的实现不是同步的,这意味着它不是线程安全的 +* key 是唯一不重复的,底层的哈希表结构,依赖 hashCode 方法和 equals 方法保证键的唯一 +* key、value 都可以为null,但是 key 位置只能是一个null +* HashMap 中的映射不是有序的,即存取是无序的 +* **key 要存储的是自定义对象,需要重写 hashCode 和 equals 方法,防止出现地址不同内容相同的 key** + +JDK7 对比 JDK8: + +* 7 = 数组 + 链表,8 = 数组 + 链表 + 红黑树 +* 7 中是头插法,多线程容易造成环,8 中是尾插法 +* 7 的扩容是全部数据重新定位,8 中是位置不变或者当前位置 + 旧 size 大小来实现 +* 7 是先判断是否要扩容再插入,8 中是先插入再看是否要扩容 + +底层数据结构: + +* 哈希表(Hash table,也叫散列表),根据关键码值而直接访问的数据结构。通过把关键码值映射到表中一个位置来访问记录,以加快查找的速度,这个映射函数叫做散列函数,存放记录的数组叫做散列表 + +* JDK1.8 之前 HashMap 由数组+链表组成 + + * 数组是 HashMap 的主体 + * 链表则是为了解决哈希冲突而存在的(**拉链法解决冲突**),拉链法就是头插法,两个对象调用的 hashCode 方法计算的哈希码值(键的哈希)一致导致计算的数组索引值相同 + +* JDK1.8 以后 HashMap 由**数组+链表 +红黑树**数据结构组成 + + * 解决哈希冲突时有了较大的变化 + * 当链表长度**超过(大于)阈值**(或者红黑树的边界值,默认为 8)并且当前数组的**长度大于等于 64 时**,此索引位置上的所有数据改为红黑树存储 + * 即使哈希函数取得再好,也很难达到元素百分百均匀分布。当 HashMap 中有大量的元素都存放到同一个桶中时,就相当于一个长的单链表,假如单链表有 n 个元素,遍历的**时间复杂度是 O(n)**,所以 JDK1.8 中引入了 红黑树(查找**时间复杂度为 O(logn)**)来优化这个问题,使得查找效率更高 + + ![](https://seazean.oss-cn-beijing.aliyuncs.com/img/Java/HashMap底层结构.png) + + + +参考视频:https://www.bilibili.com/video/BV1nJ411J7AA + + + +*** + + + +##### 继承关系 + +HashMap 继承关系如下图所示: + +![](https://seazean.oss-cn-beijing.aliyuncs.com/img/Java/HashMap继承关系.bmp) + +说明: + +* Cloneable 空接口,表示可以克隆, 创建并返回 HashMap 对象的一个副本。 +* Serializable 序列化接口,属于标记性接口,HashMap 对象可以被序列化和反序列化。 +* AbstractMap 父类提供了 Map 实现接口,以最大限度地减少实现此接口所需的工作 + + + +*** + + + +##### 成员属性 + +1. 序列化版本号 + + ```java + private static final long serialVersionUID = 362498820763181265L; + ``` + +2. 集合的初始化容量(**必须是二的 n 次幂** ) + + ```java + // 默认的初始容量是16 -- 1<<4相当于1*2的4次方---1*16 + static final int DEFAULT_INITIAL_CAPACITY = 1 << 4; + ``` + + HashMap 构造方法指定集合的初始化容量大小: + + ```java + HashMap(int initialCapacity)// 构造一个带指定初始容量和默认加载因子 (0.75) 的空 HashMap + ``` + + * 为什么必须是 2 的 n 次幂?用位运算替代取余计算,减少 rehash 的代价(移动的节点少) + + HashMap 中添加元素时,需要根据 key 的 hash 值确定在数组中的具体位置。为了减少碰撞,把数据分配均匀,每个链表长度大致相同,实现该方法就是取模 `hash%length`,计算机中直接求余效率不如位移运算, **`hash % length == hash & (length-1)` 的前提是 length 是 2 的 n 次幂** + + 散列平均分布:2 的 n 次方是 1 后面 n 个 0,2 的 n 次方 -1 是 n 个 1,可以**保证散列的均匀性**,减少碰撞 + + ```java + 例如长度为8时候,3&(8-1)=3 2&(8-1)=2 ,不同位置上,不碰撞; + 例如长度为9时候,3&(9-1)=0 2&(9-1)=0 ,都在0上,碰撞了; + ``` + + * 如果输入值不是 2 的幂会怎么样? + + 创建 HashMap 对象时,HashMap 通过位移运算和或运算得到的肯定是 2 的幂次数,并且是大于那个数的最近的数字,底层采用 tableSizeFor() 方法 + +3. 默认的负载因子,默认值是 0.75 + + ```java + static final float DEFAULT_LOAD_FACTOR = 0.75f; + ``` + +4. 集合最大容量 + + ```java + // 集合最大容量的上限是:2的30次幂 + static final int MAXIMUM_CAPACITY = 1 << 30;// 0100 0000 0000 0000 0000 0000 0000 0000 = 2 ^ 30 + ``` + +5. 当链表的值超过 8 则会转红黑树(JDK1.8 新增) + + ```java + // 当桶(bucket)上的结点数大于这个值时会转成红黑树 + static final int TREEIFY_THRESHOLD = 8; + ``` + + 为什么 Map 桶中节点个数大于 8 才转为红黑树? + + * 在 HashMap 中有一段注释说明:**空间和时间的权衡** + + ```java + TreeNodes占用空间大约是普通节点的两倍,所以我们只在箱子包含足够的节点时才使用树节点。当节点变少(由于删除或调整大小)时,就会被转换回普通的桶。在使用分布良好的用户hashcode时,很少使用树箱。理想情况下,在随机哈希码下,箱子中节点的频率服从"泊松分布",默认调整阈值为0.75,平均参数约为0.5,尽管由于调整粒度的差异很大。忽略方差,列表大小k的预期出现次数是(exp(-0.5)*pow(0.5, k)/factorial(k)) + 0: 0.60653066 + 1: 0.30326533 + 2: 0.07581633 + 3: 0.01263606 + 4: 0.00157952 + 5: 0.00015795 + 6: 0.00001316 + 7: 0.00000094 + 8: 0.00000006 + more: less than 1 in ten million + 一个bin中链表长度达到8个元素的概率为0.00000006,几乎是不可能事件,所以我们选择8这个数字 + ``` + + * 其他说法 + 红黑树的平均查找长度是 log(n),如果长度为 8,平均查找长度为 log(8)=3,链表的平均查找长度为 n/2,当长度为 8 时,平均查找长度为 8/2=4,这才有转换成树的必要;链表长度如果是小于等于 6,6/2=3,而 log(6)=2.6,虽然速度也很快的,但转化为树结构和生成树的时间并不短 + +6. 当链表的值小于 6 则会从红黑树转回链表 + + ```java + // 当桶(bucket)上的结点数小于这个值时树转链表 + static final int UNTREEIFY_THRESHOLD = 6; + ``` + +7. 当 Map 里面的数量**大于等于**这个阈值时,表中的桶才能进行树形化 ,否则桶内元素超过 8 时会扩容,而不是树形化。为了避免进行扩容、树形化选择的冲突,这个值不能小于 4 * TREEIFY_THRESHOLD (8) + + ```java + // 桶中结构转化为红黑树对应的数组长度最小的值 + static final int MIN_TREEIFY_CAPACITY = 64; + ``` + + 原因:数组比较小的情况下变为红黑树结构,反而会降低效率,红黑树需要进行左旋,右旋,变色这些操作来保持平衡 + +8. table 用来初始化(必须是二的 n 次幂) + + ```java + // 存储元素的数组 + transient Node[] table; + ``` + + 9. HashMap 中**存放元素的个数** + + ```java + // 存放元素的个数,HashMap中K-V的实时数量,不是table数组的长度 + transient int size; + ``` + +10. 记录 HashMap 的修改次数 + + ```java + // 每次扩容和更改map结构的计数器 + transient int modCount; + ``` + +11. 调整大小下一个容量的值计算方式为:容量 * 负载因子,容量是数组的长度 + + ```java + // 临界值,当实际大小(容量*负载因子)超过临界值时,会进行扩容 + int threshold; + ``` + +12. **哈希表的加载因子** + + ```java + final float loadFactor; + ``` + + * 加载因子的概述 + + loadFactor 加载因子,是用来衡量 HashMap 满的程度,表示 HashMap 的疏密程度,影响 hash 操作到同一个数组位置的概率,计算 HashMap 的实时加载因子的方法为 **size/capacity**,而不是占用桶的数量去除以 capacity,capacity 是桶的数量,也就是 table 的长度 length + + 当 HashMap 容纳的元素已经达到数组长度的 75% 时,表示 HashMap 拥挤需要扩容,而扩容这个过程涉及到 rehash、复制数据等操作,非常消耗性能,所以开发中尽量减少扩容的次数,通过创建 HashMap 集合对象时指定初始容量来避免 + + ```java + HashMap(int initialCapacity, float loadFactor)//构造指定初始容量和加载因子的空HashMap + ``` + + * 为什么加载因子设置为 0.75,初始化临界值是 12? + + loadFactor 太大导致查找元素效率低,存放的数据拥挤,太小导致数组的利用率低,存放的数据会很分散。loadFactor 的默认值为 **0.75f 是官方给出的一个比较好的临界值** + + * threshold 计算公式:capacity(数组长度默认16) * loadFactor(默认 0.75)。当 size >= threshold 的时候,那么就要考虑对数组的 resize(扩容),这就是衡量数组是否需要扩增的一个标准, 扩容后的 HashMap 容量是之前容量的**两倍** + + + +*** + + + +##### 构造方法 + +* 构造一个空的 HashMap ,**默认初始容量(16)和默认负载因子(0.75)** + + ```java + public HashMap() { + this.loadFactor = DEFAULT_LOAD_FACTOR; + // 将默认的加载因子0.75赋值给loadFactor,并没有创建数组 + } + ``` + +* 构造一个具有指定的初始容量和默认负载因子(0.75)HashMap + + ```java + // 指定“容量大小”的构造函数 + public HashMap(int initialCapacity) { + this(initialCapacity, DEFAULT_LOAD_FACTOR); + } + ``` + +* 构造一个具有指定的初始容量和负载因子的 HashMap + + ```java + public HashMap(int initialCapacity, float loadFactor) { + // 进行判断 + // 将指定的加载因子赋值给HashMap成员变量的负载因子loadFactor + this.loadFactor = loadFactor; + // 最后调用了tableSizeFor + this.threshold = tableSizeFor(initialCapacity); + } + ``` + + * 对于 `this.threshold = tableSizeFor(initialCapacity)` + + JDK8 以后的构造方法中,并没有对 table 这个成员变量进行初始化,table 的初始化被推迟到了 put 方法中,在 put 方法中会对 threshold 重新计算 + +* 包含另一个 `Map` 的构造函数 + + ```java + // 构造一个映射关系与指定 Map 相同的新 HashMap + public HashMap(Map m) { + // 负载因子loadFactor变为默认的负载因子0.75 + this.loadFactor = DEFAULT_LOAD_FACTOR; + putMapEntries(m, false); + } + ``` + + putMapEntries 源码分析: + + ```java + final void putMapEntries(Map m, boolean evict) { + //获取参数集合的长度 + int s = m.size(); + if (s > 0) { + //判断参数集合的长度是否大于0 + if (table == null) { // 判断table是否已经初始化 + // pre-size + // 未初始化,s为m的实际元素个数 + float ft = ((float)s / loadFactor) + 1.0F; + int t = ((ft < (float)MAXIMUM_CAPACITY) ? + (int)ft : MAXIMUM_CAPACITY); + // 计算得到的t大于阈值,则初始化阈值 + if (t > threshold) + threshold = tableSizeFor(t); + } + // 已初始化,并且m元素个数大于阈值,进行扩容处理 + else if (s > threshold) + resize(); + // 将m中的所有元素添加至HashMap中 + for (Map.Entry e : m.entrySet()) { + K key = e.getKey(); + V value = e.getValue(); + putVal(hash(key), key, value, false, evict); + } + } + } + ``` + + `float ft = ((float)s / loadFactor) + 1.0F` 这一行代码中为什么要加 1.0F ? + + s / loadFactor 的结果是小数,加 1.0F 相当于是对小数做一个向上取整以尽可能的保证更大容量,更大的容量能够减少 resize 的调用次数,这样可以减少数组的扩容 + + + +*** + + + +##### 成员方法 + +* hash():HashMap 是支持 Key 为空的;HashTable 是直接用 Key 来获取 HashCode,key 为空会抛异常 + + * &(按位与运算):相同的二进制数位上,都是 1 的时候,结果为 1,否则为零 + + * ^(按位异或运算):相同的二进制数位上,数字相同,结果为 0,不同为 1,**不进位加法** + + 0 1 相互做 & | ^ 运算,结果出现 0 和 1 的数量分别是 3:1、1:3、1:1,所以异或是最平均的 + + ```java + static final int hash(Object key) { + int h; + // 1)如果key等于null:可以看到当key等于null的时候也是有哈希值的,返回的是0 + // 2)如果key不等于null:首先计算出key的hashCode赋值给h,然后与h无符号右移16位后的二进制进行按位异或 + return (key == null) ? 0 : (h = key.hashCode()) ^ (h >>> 16); + } + ``` + + 计算 hash 的方法:将 hashCode 无符号右移 16 位,高 16bit 和低 16bit 做异或,扰动运算 + + 原因:当数组长度很小,假设是 16,那么 n-1 即为 1111 ,这样的值和 hashCode() 直接做按位与操作,实际上只使用了哈希值的后 4 位。如果当哈希值的高位变化很大,低位变化很小,就很容易造成哈希冲突了,所以这里**把高低位都利用起来,让高16 位也参与运算**,从而解决了这个问题 + + 哈希冲突的处理方式: + + * 开放定址法:线性探查法(ThreadLocalMap 使用),平方探查法(i + 1^2、i - 1^2、i + 2^2……)、双重散列(多个哈希函数) + * 链地址法:拉链法 + +* put():jdk1.8 前是头插法 (链地址法),多线程下扩容出现循环链表,jdk1.8 以后引入红黑树,插入方法变成尾插法 + + 第一次调用 put 方法时创建数组 Node[] table,因为散列表耗费内存,为了防止内存浪费,所以**延迟初始化** + + 存储数据步骤(存储过程): + + 1. 先通过 hash 值计算出 key 映射到哪个桶,哈希寻址 + 2. 如果桶上没有碰撞冲突,则直接插入 + 3. 如果出现碰撞冲突:如果该桶使用红黑树处理冲突,则调用红黑树的方法插入数据;否则采用传统的链式方法插入,如果链的长度达到临界值,则把链转变为红黑树 + 4. 如果数组位置相同,通过 equals 比较内容是否相同:相同则新的 value 覆盖旧 value,不相同则将新的键值对添加到哈希表中 + 5. 最后判断 size 是否大于阈值 threshold,则进行扩容 + + ```java + public V put(K key, V value) { + return putVal(hash(key), key, value, false, true); + } + ``` + + putVal() 方法中 key 在这里执行了一下 hash(),在 putVal 函数中使用到了上述 hash 函数计算的哈希值: + + ```java + final V putVal(int hash, K key, V value, boolean onlyIfAbsent, boolean evict) { + //。。。。。。。。。。。。。。 + if ((p = tab[i = (n - 1) & hash]) == null){//这里的n表示数组长度16 + //..... + } else { + if (e != null) { // existing mapping for key + V oldValue = e.value; + //onlyIfAbsent默认为false,所以可以覆盖已经存在的数据,如果为true说明不能覆盖 + if (!onlyIfAbsent || oldValue == null) + e.value = value; + afterNodeAccess(e); + // 如果这里允许覆盖,就直接返回了 + return oldValue; + } + } + // 如果是添加操作,modCount ++,如果不是替换,不会走这里的逻辑,modCount用来记录逻辑的变化 + ++modCount; + // 数量大于扩容阈值 + if (++size > threshold) + resize(); + afterNodeInsertion(evict); + return null; + } + ``` + + * `(n - 1) & hash`:计算下标位置 + + + + * 余数本质是不断做除法,把剩余的数减去,运算效率要比位运算低 + + + +* treeifyBin() + + 节点添加完成之后判断此时节点个数是否大于 TREEIFY_THRESHOLD 临界值 8,如果大于则将链表转换为红黑树,转换红黑树的方法 treeifyBin,整体代码如下: + + ```java + if (binCount >= TREEIFY_THRESHOLD - 1) // -1 for 1st + //转换为红黑树 tab表示数组名 hash表示哈希值 + treeifyBin(tab, hash); + ``` + + 1. 如果当前数组为空或者数组的长度小于进行树形化的阈 MIN_TREEIFY_CAPACITY = 64 就去扩容,而不是将节点变为红黑树 + 2. 如果是树形化遍历桶中的元素,创建相同个数的树形节点,复制内容,建立起联系,类似单向链表转换为双向链表 + 3. 让桶中的第一个元素即数组中的元素指向新建的红黑树的节点,以后这个桶里的元素就是红黑树而不是链表数据结构了 + +* tableSizeFor():创建 HashMap 指定容量时,HashMap 通过位移运算和或运算得到比指定初始化容量大的最小的 2 的 n 次幂 + + ```java + static final int tableSizeFor(int cap) {//int cap = 10 + 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; + } + ``` + + 分析算法: + + 1. `int n = cap - 1`:防止 cap 已经是 2 的幂。如果 cap 已经是 2 的幂, 不执行减 1 操作,则执行完后面的无符号右移操作之后,返回的 capacity 将是这个 cap 的 2 倍 + 2. n=0 (cap-1 之后),则经过后面的几次无符号右移依然是 0,返回的 capacity 是 1,最后有 n+1 + 3. |(按位或运算):相同的二进制数位上,都是 0 的时候,结果为 0,否则为 1 + 4. 核心思想:**把最高位是 1 的位以及右边的位全部置 1**,结果加 1 后就是大于指定容量的最小的 2 的 n 次幂 + + 例如初始化的值为 10: + + * 第一次右移 + + ```java + int n = cap - 1;//cap=10 n=9 + n |= n >>> 1; + 00000000 00000000 00000000 00001001 //9 + 00000000 00000000 00000000 00000100 //9右移之后变为4 + -------------------------------------------------- + 00000000 00000000 00000000 00001101 //按位或之后是13 + //使得n的二进制表示中与最高位的1紧邻的右边一位为1 + ``` + + * 第二次右移 + + ```java + n |= n >>> 2;//n通过第一次右移变为了:n=13 + 00000000 00000000 00000000 00001101 // 13 + 00000000 00000000 00000000 00000011 // 13右移之后变为3 + ------------------------------------------------- + 00000000 00000000 00000000 00001111 //按位或之后是15 + //无符号右移两位,会将最高位两个连续的1右移两位,然后再与原来的n做或操作,这样n的二进制表示的高位中会有4个连续的1 + ``` + + 注意:容量最大是 32bit 的正数,因此最后 `n |= n >>> 16`,最多是 32 个 1(但是这已经是负数了)。在执行 tableSizeFor 之前,对 initialCapacity 做了判断,如果大于 MAXIMUM_CAPACITY(2 ^ 30),则取 MAXIMUM_CAPACITY;如果小于 MAXIMUM_CAPACITY(2 ^ 30),会执行移位操作,所以移位操作之后,最大 30 个 1,加 1 之后得 2 ^ 30 + + * 得到的 capacity 被赋值给了 threshold + + ```java + this.threshold = tableSizeFor(initialCapacity);//initialCapacity=10 + ``` + + * JDK 11 + + ```java + static final int tableSizeFor(int cap) { + //无符号右移,高位补0 + //-1补码: 11111111 11111111 11111111 11111111 + int n = -1 >>> Integer.numberOfLeadingZeros(cap - 1); + return (n < 0) ? 1 : (n >= MAXIMUM_CAPACITY) ? MAXIMUM_CAPACITY : n + 1; + } + //返回最高位之前的0的位数 + public static int numberOfLeadingZeros(int i) { + if (i <= 0) + return i == 0 ? 32 : 0; + // 如果i>0,那么就表明在二进制表示中其至少有一位为1 + int n = 31; + // i的最高位1在高16位,把i右移16位,让最高位1进入低16位继续递进判断 + if (i >= 1 << 16) { n -= 16; i >>>= 16; } + if (i >= 1 << 8) { n -= 8; i >>>= 8; } + if (i >= 1 << 4) { n -= 4; i >>>= 4; } + if (i >= 1 << 2) { n -= 2; i >>>= 2; } + return n - (i >>> 1); + } + ``` + + + +* resize(): + + 当 HashMap 中的**元素个数**超过 `(数组长度)*loadFactor(负载因子)` 或者链表过长时(链表长度 > 8,数组长度 < 64),就会进行数组扩容,创建新的数组,伴随一次重新 hash 分配,并且遍历 hash 表中所有的元素非常耗时,所以要尽量避免 resize + + 扩容机制为扩容为原来容量的 2 倍: + + ```java + if (oldCap > 0) { + if (oldCap >= MAXIMUM_CAPACITY) { + // 以前的容量已经是最大容量了,这时调大 扩容阈值 threshold + threshold = Integer.MAX_VALUE; + return oldTab; + } + else if ((newCap = oldCap << 1) < MAXIMUM_CAPACITY && + oldCap >= DEFAULT_INITIAL_CAPACITY) + newThr = oldThr << 1; // double threshold + } + else if (oldThr > 0) // 初始化的threshold赋值给newCap + newCap = oldThr; + else { + newCap = DEFAULT_INITIAL_CAPACITY; + newThr = (int)(DEFAULT_LOAD_FACTOR * DEFAULT_INITIAL_CAPACITY); + } + ``` + + HashMap 在进行扩容后,节点**要么就在原来的位置,要么就被分配到"原位置+旧容量"的位置** + + 判断:e.hash 与 oldCap 对应的有效高位上的值是 1,即当前数组长度 n 二进制为 1 的位为 x 位,如果 key 的哈希值 x 位也为 1,则扩容后的索引为 now + n + + 注意:这里要求**数组长度 2 的幂** + + ![](https://seazean.oss-cn-beijing.aliyuncs.com/img/Java/HashMap-resize扩容.png) + + 普通节点:把所有节点分成高低位两个链表,转移到数组 + + ```java + // 遍历所有的节点 + do { + next = e.next; + // oldCap 旧数组大小,2 的 n 次幂 + if ((e.hash & oldCap) == 0) { + if (loTail == null) + loHead = e; //指向低位链表头节点 + else + loTail.next = e; + loTail = e; //指向低位链表尾节点 + } + else { + if (hiTail == null) + hiHead = e; + else + hiTail.next = e; + hiTail = e; + } + } while ((e = next) != null); + + if (loTail != null) { + loTail.next = null; // 低位链表的最后一个节点可能在原哈希表中指向其他节点,需要断开 + newTab[j] = loHead; + } + ``` + + 红黑树节点:扩容时 split 方法会将树**拆成高位和低位两个链表**,判断长度是否小于等于 6 + + ```java + //如果低位链表首节点不为null,说明有这个链表存在 + if (loHead != null) { + //如果链表下的元素小于等于6 + if (lc <= UNTREEIFY_THRESHOLD) + //那就从红黑树转链表了,低位链表,迁移到新数组中下标不变,还是等于原数组到下标 + tab[index] = loHead.untreeify(map); + else { + //低位链表,迁移到新数组中下标不变,把低位链表整个赋值到这个下标下 + tab[index] = loHead; + //如果高位首节点不为空,说明原来的红黑树已经被拆分成两个链表了 + if (hiHead != null) + //需要构建新的红黑树了 + loHead.treeify(tab); + } + } + ``` + +​ + +* remove():删除是首先先找到元素的位置,如果是链表就遍历链表找到元素之后删除。如果是用红黑树就遍历树然后找到之后做删除,树小于 6 的时候退化为链表 + + ```java + final Node removeNode(int hash, Object key, Object value, + boolean matchValue, boolean movable) { + Node[] tab; Node p; int n, index; + // 节点数组tab不为空、数组长度n大于0、根据hash定位到的节点对象p, + // 该节点为树的根节点或链表的首节点)不为空,从该节点p向下遍历,找到那个和key匹配的节点对象 + if ((tab = table) != null && (n = tab.length) > 0 && + (p = tab[index = (n - 1) & hash]) != null) { + Node node = null, e; K k; V v;//临时变量,储存要返回的节点信息 + //key和value都相等,直接返回该节点 + if (p.hash == hash && + ((k = p.key) == key || (key != null && key.equals(k)))) + node = p; + + else if ((e = p.next) != null) { + //如果是树节点,调用getTreeNode方法从树结构中查找满足条件的节点 + if (p instanceof TreeNode) + node = ((TreeNode)p).getTreeNode(hash, key); + //遍历链表 + else { + do { + //e节点的键是否和key相等,e节点就是要删除的节点,赋值给node变量 + if (e.hash == hash && + ((k = e.key) == key || + (key != null && key.equals(k)))) { + node = e; + //跳出循环 + break; + } + p = e;//把当前节点p指向e 继续遍历 + } while ((e = e.next) != null); + } + } + //如果node不为空,说明根据key匹配到了要删除的节点 + //如果不需要对比value值或者对比value值但是value值也相等,可以直接删除 + if (node != null && (!matchValue || (v = node.value) == value || + (value != null && value.equals(v)))) { + if (node instanceof TreeNode) + ((TreeNode)node).removeTreeNode(this, tab, movable); + else if (node == p)//node是首节点 + tab[index] = node.next; + else //node不是首节点 + p.next = node.next; + ++modCount; + --size; + //LinkedHashMap + afterNodeRemoval(node); + return node; + } + } + return null; + } + ``` + + + +* get() + + 1. 通过 hash 值获取该 key 映射到的桶 + + 2. 桶上的 key 就是要查找的 key,则直接找到并返回 + + 3. 桶上的 key 不是要找的 key,则查看后续的节点: + + * 如果后续节点是红黑树节点,通过调用红黑树的方法根据 key 获取 value + + * 如果后续节点是链表节点,则通过循环遍历链表根据 key 获取 value + + 4. 红黑树节点调用的是 getTreeNode 方法通过树形节点的 find 方法进行查 + + * 查找红黑树,之前添加时已经保证这个树是有序的,因此查找时就是折半查找,效率更高。 + * 这里和插入时一样,如果对比节点的哈希值相等并且通过 equals 判断值也相等,就会判断 key 相等,直接返回,不相等就从子树中递归查找 + + 5. 时间复杂度 O(1) + + * 若为树,则在树中通过 key.equals(k) 查找,**O(logn)** + * 若为链表,则在链表中通过 key.equals(k) 查找,**O(n)** + + + +**** + + + +##### 并发异常 + +HashMap 和 ArrayList 一样,内部采用 modCount 用来记录集合结构发生变化的次数,结构发生变化是指添加或者删除至少一个元素的所有操作,或者是调整内部数组的大小,仅仅只是设置元素的值不算结构发生变化 + +在进行序列化或者迭代等操作时,需要比较操作前后 modCount 是否改变,如果**其他线程此时修改了集合内部的结构**,就会直接抛出 ConcurrentModificationException 异常 + +```java +HashMap map = new HashMap(); +Iterator iterator = map.keySet().iterator(); +``` + +```java +final class KeySet extends AbstractSet { + // 底层获取的是 KeyIterator + public final Iterator iterator() { + return new KeyIterator(); + } +} +final class KeyIterator extends HashIterator implements Iterator { + // 回调 HashMap.HashIterator#nextNode + public final K next() { + return nextNode().key; + } +} +``` + +```java +abstract class HashIterator { + Node next; // next entry to return + Node current; // current entry + int expectedModCount; // for 【fast-fail】,快速失败 + int index; // current slot + + HashIterator() { + // 把当前 map 的数量赋值给 expectedModCount,迭代时判断 + expectedModCount = modCount; + Node[] t = table; + current = next = null; + index = 0; + if (t != null && size > 0) { // advance to first entry + do {} while (index < t.length && (next = t[index++]) == null); + } + } + + public final boolean hasNext() { + return next != null; + } + // iterator.next() 会调用这个函数 + final Node nextNode() { + Node[] t; + Node e = next; + // 这里会判断 集合的结构是否发生了变化,变化后 modCount 会改变,直接抛出并发异常 + if (modCount != expectedModCount) + throw new ConcurrentModificationException(); + if (e == null) + throw new NoSuchElementException(); + if ((next = (current = e).next) == null && (t = table) != null) { + do {} while (index < t.length && (next = t[index++]) == null); + } + return e; + } + // 迭代器允许删除集合的元素,【删除后会重置 expectedModCount = modCount】 + public final void remove() { + Node p = current; + if (p == null) + throw new IllegalStateException(); + if (modCount != expectedModCount) + throw new ConcurrentModificationException(); + current = null; + K key = p.key; + removeNode(hash(key), key, null, false, false); + // 同步expectedModCount + expectedModCount = modCount; + } +} +``` + + + + + +*** + + + +#### LinkedMap + +##### 原理分析 + +LinkedHashMap 是 HashMap 的子类 + +* 优点:添加的元素按照键有序不重复的,有序的原因是底层维护了一个双向链表 + +* 缺点:会占用一些内存空间 + +对比 Set: + +* HashSet 集合相当于是 HashMap 集合的键,不带值 +* LinkedHashSet 集合相当于是 LinkedHashMap 集合的键,不带值 +* 底层原理完全一样,都是基于哈希表按照键存储数据的,只是 Map 多了一个键的值 + +源码解析: + +* **内部维护了一个双向链表**,用来维护插入顺序或者 LRU 顺序 + + ```java + transient LinkedHashMap.Entry head; + transient LinkedHashMap.Entry tail; + ``` + +* accessOrder 决定了顺序,默认为 false 维护的是插入顺序(先进先出),true 为访问顺序(**LRU 顺序**) + + ```java + final boolean accessOrder; + ``` + +* 维护顺序的函数 + + ```java + void afterNodeAccess(Node p) {} + void afterNodeInsertion(boolean evict) {} + ``` + +* put() + + ```java + // 调用父类HashMap的put方法 + public V put(K key, V value) { + return putVal(hash(key), key, value, false, true); + } + final V putVal(int hash, K key, V value, boolean onlyIfAbsent, boolean evict) + → afterNodeInsertion(evict);// evict为true + ``` + + afterNodeInsertion方法,当 removeEldestEntry() 方法返回 true 时会移除最近最久未使用的节点,也就是链表首部节点 first + + ```java + void afterNodeInsertion(boolean evict) { + LinkedHashMap.Entry first; + // evict 只有在构建 Map 的时候才为 false,这里为 true + if (evict && (first = head) != null && removeEldestEntry(first)) { + K key = first.key; + removeNode(hash(key), key, null, false, true);//移除头节点 + } + } + ``` + + removeEldestEntry() 默认为 false,如果需要让它为 true,需要继承 LinkedHashMap 并且覆盖这个方法的实现,在实现 LRU 的缓存中特别有用,通过移除最近最久未使用的节点,从而保证缓存空间足够,并且缓存的数据都是热点数据 + + ```java + protected boolean removeEldestEntry(Map.Entry eldest) { + return false; + } + ``` + +* get() + + 当一个节点被访问时,如果 accessOrder 为 true,则会将该节点移到链表尾部。也就是说指定为 LRU 顺序之后,在每次访问一个节点时会将这个节点移到链表尾部,那么链表首部就是最近最久未使用的节点 + + ```java + public V get(Object key) { + Node e; + if ((e = getNode(hash(key), key)) == null) + return null; + if (accessOrder) + afterNodeAccess(e); + return e.value; + } + ``` + + ```java + void afterNodeAccess(Node e) { + LinkedHashMap.Entry last; + if (accessOrder && (last = tail) != e) { + // 向下转型 + LinkedHashMap.Entry p = + (LinkedHashMap.Entry)e, b = p.before, a = p.after; + p.after = null; + // 判断 p 是否是首节点 + if (b == null) + //是头节点 让p后继节点成为头节点 + head = a; + else + //不是头节点 让p的前驱节点的next指向p的后继节点,维护链表的连接 + b.after = a; + // 判断p是否是尾节点 + if (a != null) + // 不是尾节点 让p后继节点指向p的前驱节点 + a.before = b; + else + // 是尾节点 让last指向p的前驱节点 + last = b; + // 判断last是否是空 + if (last == null) + // last为空说明p是尾节点或者只有p一个节点 + head = p; + else { + // last和p相互连接 + p.before = last; + last.after = p; + } + tail = p; + ++modCount; + } + } + ``` + +* remove() + + ```java + //调用HashMap的remove方法 + final Node removeNode(int hash, Object key, Object value,boolean matchValue, boolean movable) + → afterNodeRemoval(node); + ``` + + 当 HashMap 删除一个键值对时调用,会把在 HashMap 中删除的那个键值对一并从链表中删除 + + ```java + void afterNodeRemoval(Node e) { + LinkedHashMap.Entry p = + (LinkedHashMap.Entry)e, b = p.before, a = p.after; + // 让p节点与前驱节点和后继节点断开链接 + p.before = p.after = null; + // 判断p是否是头节点 + if (b == null) + // p是头节点 让head指向p的后继节点 + head = a; + else + // p不是头节点 让p的前驱节点的next指向p的后继节点,维护链表的连接 + b.after = a; + // 判断p是否是尾节点,是就让tail指向p的前驱节点,不是就让p.after指向前驱节点,双向 + if (a == null) + tail = b; + else + a.before = b; + } + ``` + + + +*** + + + +##### LRU + +使用 LinkedHashMap 实现的一个 LRU 缓存: + +- 设定最大缓存空间 MAX_ENTRIES 为 3 +- 使用 LinkedHashMap 的构造函数将 accessOrder 设置为 true,开启 LRU 顺序 +- 覆盖 removeEldestEntry() 方法实现,在节点多于 MAX_ENTRIES 就会将最近最久未使用的数据移除 + +```java +public static void main(String[] args) { + LRUCache cache = new LRUCache<>(); + cache.put(1, "a"); + cache.put(2, "b"); + cache.put(3, "c"); + cache.get(1);//把1放入尾部 + cache.put(4, "d"); + System.out.println(cache.keySet());//[3, 1, 4]只能存3个,移除2 +} + +class LRUCache extends LinkedHashMap { + private static final int MAX_ENTRIES = 3; + + protected boolean removeEldestEntry(Map.Entry eldest) { + return size() > MAX_ENTRIES; + } + + LRUCache() { + super(MAX_ENTRIES, 0.75f, true); + } +} +``` + + + +*** + + + +#### TreeMap + +TreeMap 实现了 SotredMap 接口,是有序不可重复的键值对集合,基于红黑树(Red-Black tree)实现,每个 key-value 都作为一个红黑树的节点,如果构造 TreeMap 没有指定比较器,则根据 key 执行自然排序(默认升序),如果指定了比较器则按照比较器来进行排序 + +TreeMap 集合指定大小规则有 2 种方式: + +* 直接为对象的类实现比较器规则接口 Comparable,重写比较方法 +* 直接为集合设置比较器 Comparator 对象,重写比较方法 + +说明:TreeSet 集合的底层是基于 TreeMap,只是键的附属值为空对象而已 + +成员属性: + +* Entry 节点 + + ```java + static final class Entry implements Map.Entry { + K key; + V value; + Entry left; //左孩子节点 + Entry right; //右孩子节点 + Entry parent; //父节点 + boolean color = BLACK; //节点的颜色,在红黑树中只有两种颜色,红色和黑色 + } + ``` + +* compare() + + ```java + //如果comparator为null,采用comparable.compartTo进行比较,否则采用指定比较器比较大小 + final int compare(Object k1, Object k2) { + return comparator == null ? ((Comparable)k1).compareTo((K)k2) + : comparator.compare((K)k1, (K)k2); + } + ``` + + + +参考文章:https://blog.csdn.net/weixin_33991727/article/details/91518677 + + + +*** + + + +#### WeakMap + +WeakHashMap 是基于弱引用的,内部的 Entry 继承 WeakReference,被弱引用关联的对象在**下一次垃圾回收时会被回收**,并且构造方法传入引用队列,用来在清理对象完成以后清理引用 + +```java +private static class Entry extends WeakReference implements Map.Entry { + Entry(Object key, V value, ReferenceQueue queue, int hash, Entry next) { + super(key, queue); + this.value = value; + this.hash = hash; + this.next = next; + } +} +``` + +WeakHashMap 主要用来实现缓存,使用 WeakHashMap 来引用缓存对象,由 JVM 对这部分缓存进行回收 + +Tomcat 中的 ConcurrentCache 使用了 WeakHashMap 来实现缓存功能,ConcurrentCache 采取分代缓存: + +* 经常使用的对象放入 eden 中,eden 使用 ConcurrentHashMap 实现,不用担心会被回收(伊甸园) + +* 不常用的对象放入 longterm,longterm 使用 WeakHashMap 实现,这些老对象会被垃圾收集器回收 + +* 当调用 get() 方法时,会先从 eden 区获取,如果没有找到的话再到 longterm 获取,当从 longterm 获取到就把对象放入 eden 中,从而保证经常被访问的节点不容易被回收 + +* 当调用 put() 方法时,如果 eden 的大小超过了 size,那么就将 eden 中的所有对象都放入 longterm 中,利用虚拟机回收掉一部分不经常使用的对象 + + ```java + public final class ConcurrentCache { + private final int size; + private final Map eden; + private final Map longterm; + + public ConcurrentCache(int size) { + this.size = size; + this.eden = new ConcurrentHashMap<>(size); + this.longterm = new WeakHashMap<>(size); + } + + public V get(K k) { + V v = this.eden.get(k); + if (v == null) { + v = this.longterm.get(k); + if (v != null) + this.eden.put(k, v); + } + return v; + } + + public void put(K k, V v) { + if (this.eden.size() >= size) { + this.longterm.putAll(this.eden); + this.eden.clear(); + } + this.eden.put(k, v); + } + } + + + + + +*** + + + +### 泛型 + +#### 概述 + +泛型(Generic): + +* 泛型就是一个标签:<数据类型> +* 泛型可以在编译阶段约束只能操作某种数据类型。 + +注意: + +* JDK 1.7 开始之后,泛型后面的申明可以省略不写 +* **泛型和集合都只能支持引用数据类型,不支持基本数据类型** + +```java +ArrayList lists = new ArrayList<>(); +lists.add(99.9); +lists.add('a'); +lists.add("Java"); +ArrayList list = new ArrayList<>(); +lists1.add(10); +lists1.add(20); +``` + +优点:泛型在编译阶段约束了操作的数据类型,从而不会出现类型转换异常,体现的是 Java 的严谨性和规范性 + + + +**** + + + +#### 自定义 + +##### 泛型类 + +泛型类:使用了泛型定义的类就是泛型类 + +泛型类格式: + +```java +修饰符 class 类名<泛型变量>{ + +} +泛型变量建议使用 E , T , K , V +``` + +```java +public class GenericDemo { + public static void main(String[] args) { + MyArrayList list = new MyArrayList(); + MyArrayList list1 = new MyArrayList(); + list.add("自定义泛型类"); + } +} +class MyArrayList{ + public void add(E e){} + public void remove(E e){} +} +``` + + + +**** + + + +##### 泛型方法 + +泛型方法:定义了泛型的方法就是泛型方法 + +泛型方法的定义格式: + +```java +修饰符 <泛型变量> 返回值类型 方法名称(形参列表){ + +} +``` + +方法定义了是什么泛型变量,后面就只能用什么泛型变量。 + +泛型类的核心思想:把出现泛型变量的地方全部替换成传输的真实数据类型 + +```java +public class GenericDemo { + public static void main(String[] args) { + Integer[] num = {10 , 20 , 30 , 40 , 50}; + String s1 = arrToString(nums); + + String[] name = {"张三","李四","王五"}; + String s2 = arrToString(names); + } + + public static String arrToString(T[] arr){ + -------------- + } +} +``` + + + +自定义泛型接口 + +泛型接口:使用了泛型定义的接口就是泛型接口。 + +泛型接口的格式: + +```java +修饰符 interface 接口名称<泛型变量>{ + +} +``` + +```java +public class GenericDemo { + public static void main(String[] args) { + Data d = new StudentData(); + d.add(new Student()); + ................ + } +} + +public interface Data{ + void add(E e); + void delete(E e); + void update(E e); + E query(int index); +} +class Student{} +class StudentData implements Data{重写所有方法} +``` + + + +**** + + + +#### 通配符 + +通配符:? + +* ? 可以用在使用泛型的时候代表一切类型 +* E、T、K、V 是在定义泛型的时候使用代表一切类型 + +泛型的上下限: + +* ? extends Car:那么 ? 必须是 Car 或者其子类(泛型的上限) +* ? super Car:那么 ? 必须是 Car 或者其父类(泛型的下限,不是很常见) + +```java +//需求:开发一个极品飞车的游戏,所有的汽车都能一起参与比赛。 +public class GenericDemo { + public static void main(String[] args) { + ArrayList bmws = new ArrayList<>(); + ArrayList ads = new ArrayList<>(); + ArrayList dogs = new ArrayList<>(); + run(bmws); + //run(dogs); + } + //public static void run(ArrayList car){}//这样 dou对象也能进入 + public static void run(ArrayList car){} +} + +class Car{} +class BMW extends Car{} +class AD extends Car{} +class Dog{} +``` + + + + + +*** + + + + + +## 异常 + +### 基本介绍 + +异常:程序在编译或者执行的过程中可能出现的问题,Java 为常见的代码异常都设计一个类来代表 + +错误:Error ,程序员无法处理的错误,只能重启系统,比如内存奔溃,JVM 本身的奔溃 + +Java 中异常继承的根类是:Throwable + +``` +异常的体系: + Throwable(根类,不是异常类) + / \ + Error Exception(异常,需要研究和处理) + / \ + 编译时异常 RuntimeException(运行时异常) +``` + +Exception 异常的分类: + +* 编译时异常:继承自 Exception 的异常或者其子类,编译阶段就会报错 +* 运行时异常:继承自 RuntimeException 的异常或者其子类,编译阶段是不会出错的,在运行阶段出错 + + + +*** + + + +### 处理过程 + +异常的产生默认的处理过程解析:(自动处理的过程) + +1. 默认会在出现异常的代码那里自动的创建一个异常对象:ArithmeticException(算术异常) +2. 异常会从方法中出现的点这里抛出给调用者,调用者最终抛出给 JVM 虚拟机 +3. 虚拟机接收到异常对象后,先在控制台直接输出**异常栈**信息数据 +4. 直接从当前执行的异常点终止当前程序 +5. 后续代码没有机会执行了,因为程序已经死亡 + +```java +public class ExceptionDemo { + public static void main(String[] args) { + System.out.println("程序开始。。。。。。。。。。"); + chu( 10 ,0 ); + System.out.println("程序结束。。。。。。。。。。");//不执行 + } + public static void chu(int a , int b){ + int c = a / b ;// 出现了运行时异常,自动创建异常对象:ArithmeticException + System.out.println("结果是:"+c); + } +} +``` + + + +*** + + + +### 编译异常 + +#### 基本介绍 + +编译时异常:继承自 Exception 的异常或者其子类,没有继承 RuntimeException,编译时异常是编译阶段就会报错 + +编译时异常的作用是什么:在编译阶段就爆出一个错误,目的在于提醒,请检查并注意不要出 BUG + +```java +public static void main(String[] args) throws ParseException { + String date = "2015-01-12 10:23:21"; + SimpleDateFormat sdf = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss"); + Date d = sdf.parse(date); + System.out.println(d); +} +``` + + + +**** + + + +#### 处理机制 + +##### throws + +在出现编译时异常的地方层层把异常抛出去给调用者,调用者最终抛出给 JVM 虚拟机,JVM 虚拟机输出异常信息,直接终止掉程序,这种方式与默认方式是一样的 + +**Exception 是异常最高类型可以抛出一切异常** + +```java +public static void main(String[] args) throws Exception { + System.out.println("程序开始。。。。"); + String s = "2013-03-23 10:19:23"; + SimpleDateFormat sdf = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss"); + Date date = sdf.parse(s); + System.out.println("程序结束。。。。。"); +} +``` + + + +*** + + + +##### try/catch + +可以处理异常,并且出现异常后代码也不会死亡 + +* 捕获异常和处理异常的格式:**捕获处理** + + ```java + try{ + // 监视可能出现异常的代码! + }catch(异常类型1 变量){ + // 处理异常 + }catch(异常类型2 变量){ + // 处理异常 + }...finall{ + //资源释放 + } + ``` + +* 监视捕获处理异常写法:Exception 可以捕获处理一切异常类型 + + ```java + try{ + // 可能出现异常的代码! + }catch (Exception e){ + e.printStackTrace(); // **直接打印异常栈信息** + } + ``` + +**Throwable成员方法:** + +* `public String getMessage()`:返回此 throwable 的详细消息字符串 +* `public String toString()`:返回此可抛出的简短描述 +* `public void printStackTrace()`:把异常的错误信息输出在控制台 + +```java +public static void main(String[] args) { + System.out.println("程序开始。。。。"); + try { + String s = "2013-03-23 10:19:23"; + SimpleDateFormat sdf = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss"); + Date date = sdf.parse(s); + InputStream is = new FileInputStream("D:/meinv.png"); + } catch (Exception e) { + e.printStackTrace(); + } + System.out.println("程序结束。。。。。"); +} +``` + + + +*** + + + +##### 规范做法 + +在出现异常的地方把异常一层一层的抛出给最外层调用者,最外层调用者集中捕获处理 + +```java +public class ExceptionDemo{ + public static void main(String[] args){ + System.out.println("程序开始。。。。"); + try { + parseDate("2013-03-23 10:19:23"); + }catch (Exception e){ + e.printStackTrace(); + } + System.out.println("程序结束。。。。"); + } + public static void parseDate(String time) throws Exception{...} +} +``` + + + +*** + + + +### 运行异常 + +#### 基本介绍 + +继承自 RuntimeException 的异常或者其子类,编译阶段是不会出错的,是在运行时阶段可能出现的错误,运行时异常编译阶段可以处理也可以不处理,代码编译都能通过 + +**常见的运行时异常**: + +1. 数组索引越界异常:ArrayIndexOutOfBoundsException +2. 空指针异常:NullPointerException,直接输出没问题,调用空指针的变量的功能就会报错 +3. 类型转换异常:ClassCastException +4. 迭代器遍历没有此元素异常:NoSuchElementException +5. 算术异常(数学操作异常):ArithmeticException +6. 数字转换异常:NumberFormatException + + + +**** + + + +#### 处理机制 + +运行时异常在编译阶段是不会报错,在运行阶段才会出错,运行时出错了程序还是会停止,运行时异常也建议要处理,运行时异常是自动往外抛出的,不需要手工抛出 + +**运行时异常的处理规范**:直接在最外层捕获处理即可,底层会自动抛出 + +```java +public class ExceptionDemo{ + public static void main(String[] args){ + System.out.println("程序开始。。。。"); + try{ + chu(10 / 0);//ArithmeticException: / by zero + System.out.println("操作成功!");//没输出 + }catch (Exception e){ + e.printStackTrace(); + System.out.println("操作失败!");//输出了 + } + System.out.println("程序结束。。。。");//输出了 + } + + public static void chu(int a , int b) { System.out.println( a / b );} +} +``` + + + +*** + + + +### Finally + +用在捕获处理的异常格式中的,放在最后面 + +```java +try{ + // 可能出现异常的代码! +}catch(Exception e){ + e.printStackTrace(); +}finally{ + // 无论代码是出现异常还是正常执行,最终一定要执行这里的代码!! +} +try: 1次。 +catch:0-N次 (如果有finally那么catch可以没有!!) +finally: 0-1次 +``` + +**finally 的作用**:可以在代码执行完毕以后进行资源的释放操作 + +资源:资源都是实现了 Closeable 接口的,都自带 close() 关闭方法 + +注意:如果在 finally 中出现了 return,会吞掉异常 + +```java +public class FinallyDemo { + public static void main(String[] args) { + System.out.println(chu());//一定会输出 finally,优先级比return高 + } + + public static int chu(){ + try{ + int a = 10 / 2 ; + return a ; + }catch (Exception e){ + e.printStackTrace(); + return -1; + }finally { + System.out.println("=====finally被执行"); + //return 111; // 不建议在finally中写return,会覆盖前面所有的return值! + } + } + public static void test(){ + InputStream is = null; + try{ + is = new FileInputStream("D:/cang.png"); + }catch (Exception e){ + e.printStackTrace(); + }finally { + System.out.println("==finally被执行==="); + // 回收资源。用于在代码执行完毕以后进行资源的回收操作! + try { + if(is!=null)is.close(); + } catch (Exception e) { + e.printStackTrace(); + } + } + } +} +``` + + + +*** + + + +### 自定义 + +自定义异常: + +* 自定义编译时异常:定义一个异常类继承 Exception,重写构造器,在出现异常的地方用 throw new 自定义对象抛出 +* 自定义运行时异常:定义一个异常类继承 RuntimeException,重写构造器,在出现异常的地方用 throw new 自定义对象抛出 + +throws:用在方法上,用于抛出方法中的异常 + +throw: 用在出现异常的地方,创建异常对象且立即从此处抛出 + +```java +//需求:认为年龄小于0岁,大于200岁就是一个异常。 +public class ExceptionDemo { + public static void main(String[] args) { + try { + checkAge(101); + } catch (AgeIllegalException e) { + e.printStackTrace(); + } + } + + public static void checkAge(int age) throws ItheimaAgeIllegalException { + if(age < 0 || age > 200){//年龄在0-200之间 + throw new AgeIllegalException("/ age is illegal!"); + //throw new AgeIllegalRuntimeException("/ age is illegal!"); + }else{ + System.out.println("年龄是:" + age); + } + } +} + +public class AgeIllegalException extends Exception{ + Alt + Insert->Constructor +}//编译时异常 +public class AgeIllegalRuntimeException extends RuntimeException{ + public AgeIllegalRuntimeException() { + } + + public AgeIllegalRuntimeException(String message) { + super(message); + } +}//运行时异常 +``` + + + +*** + + + +### 处理规范 + +异常的语法注意: + +1. 运行时异常被抛出可以不处理,可以自动抛出;**编译时异常必须处理**;按照规范都应该处理 +2. **重写方法申明抛出的异常,子类方法抛出的异常类型必须是父类抛出异常类型或为其子类型** +3. 方法默认都可以自动抛出运行时异常, throws RuntimeException 可以省略不写 +4. 当多异常处理时,捕获处理,前面的异常类不能是后面异常类的父类 +5. 在 try/catch 后可以追加 finally 代码块,其中的代码一定会被执行,通常用于资源回收操作 + +异常的作用: + +1. 可以处理代码问题,防止程序出现异常后的死亡 + +2. 提高了程序的健壮性和安全性 + +```java +public class Demo{ + public static void main(String[] args){ + //请输入一个合法的年龄 + while(true){ + try{ + Scanner sc = new Scanner(System.in); + System.out.println("请您输入您的年年龄:"); + int age = sc.nextInt(); + System.out.println("年龄:"+age); + break; + }catch(Exception e){ + System.err.println("您的年龄是瞎输入的!"); + } + } + } +} +``` + + + + + +*** + + + + + +## λ + +### lambda + +#### 基本介绍 + +Lambda 表达式是 JDK1.8 开始之后的新技术,是一种代码的新语法,一种特殊写法 + +作用:为了简化匿名内部类的代码写法 + +Lambda 表达式的格式: + +```java +(匿名内部类被重写方法的形参列表) -> { + //被重写方法的方法体代码 +} +``` + +Lambda 表达式并不能简化所有匿名内部类的写法,只能简化**函数式接口的匿名内部类** + +简化条件:首先必须是接口,接口中只能有一个抽象方法 + +@FunctionalInterface 函数式接口注解:一旦某个接口加上了这个注解,这个接口只能有且仅有一个抽象方法 + + + +*** + + + +#### 简化方法 + +Lambda 表达式的省略写法(进一步在 Lambda 表达式的基础上继续简化) + +* 如果 Lambda 表达式的方法体代码只有一行代码,可以省略大括号不写,同时要省略分号;如果这行代码是 return 语句,必须省略 return 不写 +* 参数类型可以省略不写 +* 如果只有一个参数,参数类型可以省略,同时 `()` 也可以省略 + +```java +List names = new ArrayList<>(); +names.add("a"); +names.add("b"); +names.add("c"); + +names.forEach(new Consumer() { + @Override + public void accept(String s) { + System.out.println(s); + } +}); + +names.forEach((String s) -> { + System.out.println(s); +}); + +names.forEach((s) -> { + System.out.println(s); +}); + +names.forEach(s -> { + System.out.println(s); +}); + +names.forEach(s -> System.out.println(s) ); +``` + + + +*** + + + +#### 常用简化 + +Comparator + +```java +public class CollectionsDemo { + public static void main(String[] args) { + List lists = new ArrayList<>();//...s1 s2 s3 + Collections.addAll(lists , s1 , s2 , s3); + Collections.sort(lists, new Comparator() { + @Override + public int compare(Student s1, Student s2) { + return s1.getAge() - s2.getAge(); + } + }); + + // 简化写法 + Collections.sort(lists ,(Student t1, Student t2) -> { + return t1.getAge() - t2.getAge(); + }); + // 参数类型可以省略,最简单的 + Collections.sort(lists ,(t1,t2) -> t1.getAge()-t2.getAge()); + } +} +``` + + + + + +*** + + + +### 方法引用 + +#### 基本介绍 + +方法引用:方法引用是为了进一步简化 Lambda 表达式的写法 + +方法引用的格式:类型或者对象::引用的方法 + +关键语法是:`::` + +```java +lists.forEach( s -> System.out.println(s)); +// 方法引用! +lists.forEach(System.out::println); +``` + + + +*** + + + +#### 静态方法 + +引用格式:`类名::静态方法` + +简化步骤:定义一个静态方法,把需要简化的代码放到一个静态方法中去 + +静态方法引用的注意事项:被引用的方法的参数列表要和函数式接口中的抽象方法的参数列表一致,才能引用简化 + +```java +//定义集合加入几个Student元素 +// 使用静态方法进行简化! +Collections.sort(lists, (o1, o2) -> Student.compareByAge(o1 , o2)); +// 如果前后参数是一样的,而且方法是静态方法,既可以使用静态方法引用 +Collections.sort(lists, Student::compareByAge); + +public class Student { + private String name ; + private int age ; + + public static int compareByAge(Student o1 , Student o2){ + return o1.getAge() - o2.getAge(); + } +} +``` + + + +*** + + + +#### 实例方法 + +引用格式:`对象::实例方法` + +简化步骤:定义一个实例方法,把需要的代码放到实例方法中去 + +实例方法引用的注意事项:被引用的方法的参数列表要和函数式接口中的抽象方法的参数列表一致。 + +```java +public class MethodDemo { + public static void main(String[] args) { + List lists = new ArrayList<>(); + lists.add("java1"); + lists.add("java2"); + lists.add("java3"); + // 对象是 System.out = new PrintStream(); + // 实例方法:println() + // 前后参数正好都是一个 + lists.forEach(s -> System.out.println(s)); + lists.forEach(System.out::println); + } +} +``` + + + +*** + + + +#### 特定类型 + +特定类型:String,任何类型 + +引用格式:`特定类型::方法` + +注意事项:如果第一个参数列表中的形参中的第一个参数作为了后面的方法的调用者,并且其余参数作为后面方法的形参,那么就可以用特定类型方法引用了 + +```java +public class MethodDemo{ + public static void main(String[] args) { + String[] strs = new String[]{"James", "AA", "John", + "Patricia","Dlei" , "Robert","Boom", "Cao" ,"black" , + "Michael", "Linda","cao","after","sa"}; + + // public static void sort(T[] a, Comparator c) + // 需求:按照元素的首字符(忽略大小写)升序排序!!! + Arrays.sort(strs, new Comparator() { + @Override + public int compare(String s1, String s2) { + return s1.compareToIgnoreCase(s2);//按照元素的首字符(忽略大小写) + } + }); + + Arrays.sort(strs, ( s1, s2 ) -> s1.compareToIgnoreCase(s2)); + + // 特定类型的方法引用: + Arrays.sort(strs, String::compareToIgnoreCase); + System.out.println(Arrays.toString(strs)); + } +} +``` + + + +*** + + + +#### 构造器 + +格式:`类名::new` + +注意事项:前后参数一致的情况下,又在创建对象,就可以使用构造器引用 + +```java +public class ConstructorDemo { + public static void main(String[] args) { + List lists = new ArrayList<>(); + lists.add("java1"); + lists.add("java2"); + lists.add("java3"); + + // 集合默认只能转成Object类型的数组。 + Object[] objs = lists.toArray(); + + // 我们想指定转换成字符串类型的数组!最新的写法可以结合构造器引用实现 + String[] strs = lists.toArray(new IntFunction() { + @Override + public String[] apply(int value) { + return new String[value]; + } + }); + String[] strs1 = lists.toArray(s -> new String[s]); + String[] strs2 = lists.toArray(String[]::new); + + System.out.println("String类型的数组:"+ Arrays.toString(strs2)); + } +} +``` + + + + + +*** + + + + + +## I/O + +### Stream + +#### 概述 + +Stream 流其实就是一根传送带,元素在上面可以被 Stream 流操作 + +* 可以解决已有集合类库或者数组 API 的弊端 +* Stream 流简化集合和数组的操作 +* 链式编程 + +```java +list.stream().filter(new Predicate() { + @Override + public boolean test(String s) { + return s.startsWith("张"); + } + }); + +list.stream().filter(s -> s.startsWith("张")); +``` + + + +*** + + + +#### 获取流 + +集合获取 Stream 流用:`default Stream stream()` + +数组:Arrays.stream(数组) / Stream.of(数组); + +```java +// Collection集合获取Stream流。 +Collection c = new ArrayList<>(); +Stream listStream = c.stream(); + +// Map集合获取流 +// 先获取键的Stream流。 +Stream keysStream = map.keySet().stream(); +// 在获取值的Stream流 +Stream valuesStream = map.values().stream(); +// 获取键值对的Stream流(key=value: Map.Entry) +Stream> keyAndValues = map.entrySet().stream(); + +//数组获取流 +String[] arr = new String[]{"Java", "JavaEE" ,"Spring Boot"}; +Stream arrStream1 = Arrays.stream(arr); +Stream arrStream2 = Stream.of(arr); +``` + + + +**** + + + +#### 常用API + +| 方法名 | 说明 | +| --------------------------------------------------------- | -------------------------------------------------------- | +| void forEach(Consumer action) | 逐一处理(遍历) | +| long count | 返回流中的元素数 | +| Stream filter(Predicate predicate) | 用于对流中的数据进行过滤 | +| Stream limit(long maxSize) | 返回此流中的元素组成的流,截取前指定参数个数的数据 | +| Stream skip(long n) | 跳过指定参数个数的数据,返回由该流的剩余元素组成的流 | +| Stream map(Function mapper) | 加工方法,将当前流中的 T 类型数据转换为另一种 R 类型的流 | +| static Stream concat(Stream a, Stream b) | 合并 a 和 b 两个流为一个,调用 `Stream.concat(s1,s2)` | +| Stream distinct() | 返回由该流的不同元素组成的流 | + +```java +public class StreamDemo { + public static void main(String[] args) { + List list = new ArrayList<>(); + list.add("张无忌"); list.add("周芷若"); list.add("赵敏"); + list.add("张三"); list.add("张三丰"); list.add("张"); + //取以张开头并且名字是三位数的 + list.stream().filter(s -> s.startsWith("张") + .filter(s -> s.length == 3).forEach(System.out::println); + //统计数量 + long count = list.stream().filter(s -> s.startsWith("张") + .filter(s -> s.length == 3).count(); + //取前两个 + list.stream().filter(s -> s.length == 3).limit(2).forEach(...); + //跳过前两个 + list.stream().filter(s -> s.length == 3).skip(2).forEach(...); + + // 需求:把名称都加上“张三的:+xxx” + list.stream().map(s -> "张三的" + s).forEach(System.out::println); + // 需求:把名称都加工厂学生对象放上去!! + // list.stream().map(name -> new Student(name)); + list.stream.map(Student::new).forEach(System.out::println); + + //数组流 + Stream s1 = Stream.of(10,20,30,40,50); + //集合流 + Stream s2 = list.stream(); + //合并流 + Stream s3 = Stream.concat(s1,s2); + s3.forEach(System.out::println); + } +} +class Student{ + private String name; + //...... +} +``` + + + +*** + + + +#### 终结方法 + +终结方法:Stream 调用了终结方法,流的操作就全部终结,不能继续使用,如 foreach,count 方法等 + +非终结方法:每次调用完成以后返回一个新的流对象,可以继续使用,支持**链式编程** + +```java +// foreach终结方法 +list.stream().filter(s -> s.startsWith("张")) + .filter(s -> s.length() == 3).forEach(System.out::println); +``` + + + +*** + + + +#### 收集流 + +收集 Stream:把 Stream 流的数据转回到集合中去 + +* Stream 流:工具 +* 集合:目的 + +Stream 收集方法:`R collect(Collector collector)` 把结果收集到集合中 + +Collectors 方法: + +* `public static Collector toList()`:把元素收集到 List 集合中 +* `public static Collector toSet()`:把元素收集到 Set 集合中 +* `public static Collector toMap(Function keyMapper,Function valueMapper)`:把元素收集到 Map 集合中 +* `Object[] toArray()`:把元素收集数组中 +* `public static Collector groupingBy(Function classifier)`:分组 + +```java +public static void main(String[] args) { + List list = new ArrayList<>(); + Stream stream = list.stream().filter(s -> s.startsWith("张")); + //把stream流转换成Set集合。 + Set set = stream.collect(Collectors.toSet()); + + //把stream流转换成List集合。 + //重新定义,因为资源已经被关闭了 + Stream stream1 = list.stream().filter(s -> s.startsWith("张")); + List list = stream.collect(Collectors.toList()); + + //把stream流转换成数组。 + Stream stream2 = list.stream().filter(s -> s.startsWith("张")); + Object[] arr = stream2.toArray(); + // 可以借用构造器引用申明转换成的数组类型!!! + String[] arr1 = stream2.toArray(String[]::new); +} +``` + + + +*** + + + +### File + +#### 文件类 + +File 类:代表操作系统的文件对象,是用来操作操作系统的文件对象的,删除文件,获取文件信息,创建文件(文件夹),广义来说操作系统认为文件包含(文件和文件夹) + +File 类构造器: + +* `public File(String pathname)`:根据路径获取文件对象 +* `public File(String parent , String child)`:根据父路径和文件名称获取文件对象 + +File 类创建文件对象的格式: + +* `File f = new File("绝对路径/相对路径");` + * 绝对路径:从磁盘的的盘符一路走到目的位置的路径 + * 绝对路径依赖具体的环境,一旦脱离环境,代码可能出错 + * 一般是定位某个操作系统中的某个文件对象 + * **相对路径**:不带盘符的(重点) + * 默认是直接相对到工程目录下寻找文件的。 + * 相对路径只能用于寻找工程下的文件,可以跨平台 + +* `File f = new File("文件对象/文件夹对象")` 广义来说:文件是包含文件和文件夹的 + +```java +public class FileDemo{ + public static void main(String[] args) { + // 1.创建文件对象:使用绝对路径 + // 文件路径分隔符: + // -- a.使用正斜杠: / + // -- b.使用反斜杠: \\ + // -- c.使用分隔符API:File.separator + //File f1 = new File("D:"+File.separator+"it"+File.separator + //+"图片资源"+File.separator+"beautiful.jpg"); + File f1 = new File("D:\\seazean\\图片资源\\beautiful.jpg"); + System.out.println(f1.length()); // 获取文件的大小,字节大小 + + // 2.创建文件对象:使用相对路径 + File f2 = new File("Day09Demo/src/dlei.txt"); + System.out.println(f2.length()); + + // 3.创建文件对象:代表文件夹。 + File f3 = new File("D:\\it\\图片资源"); + System.out.println(f3.exists());// 判断路径是否存在!! + } +} +``` + + + +*** + + + +#### 常用API + +##### 常用方法 + +| 方法 | 说明 | +| ------------------------------ | -------------------------------------- | +| String getAbsolutePath() | 返回此 File 的绝对路径名字符串 | +| String getPath() | 获取创建文件对象的时候用的路径 | +| String getName() | 返回由此 File 表示的文件或目录的名称 | +| long length() | 返回由此 File 表示的文件的长度(大小) | +| long length(FileFilter filter) | 文件过滤器 | + +```java +public class FileDemo { + public static void main(String[] args) { + // 1.绝对路径创建一个文件对象 + File f1 = new File("E:/图片/test.jpg"); + // a.获取它的绝对路径。 + System.out.println(f1.getAbsolutePath()); + // b.获取文件定义的时候使用的路径。 + System.out.println(f1.getPath()); + // c.获取文件的名称:带后缀。 + System.out.println(f1.getName()); + // d.获取文件的大小:字节个数。 + System.out.println(f1.length()); + System.out.println("------------------------"); + + // 2.相对路径 + File f2 = new File("Demo/src/test.txt"); + // a.获取它的绝对路径。 + System.out.println(f2.getAbsolutePath()); + // b.获取文件定义的时候使用的路径。 + System.out.println(f2.getPath()); + // c.获取文件的名称:带后缀。 + System.out.println(f2.getName()); + // d.获取文件的大小:字节个数。 + System.out.println(f2.length()); + } +} + +``` + + + +*** + + + +##### 判断方法 + +方法列表: + +* `boolean exists()`:此 File 表示的文件或目录是否实际存在 +* `boolean isDirectory()`:此 File 表示的是否为目录 +* `boolean isFile()`:此 File 表示的是否为文件 + +```java +File f = new File("Demo/src/test.txt"); +// a.判断文件路径是否存在 +System.out.println(f.exists()); // true +// b.判断文件对象是否是文件,是文件返回true ,反之 +System.out.println(f.isFile()); // true +// c.判断文件对象是否是文件夹,是文件夹返回true ,反之 +System.out.println(f.isDirectory()); // false +``` + + + +**** + + + +##### 创建删除 + +方法列表: + +* `boolean createNewFile()`:当且仅当具有该名称的文件尚不存在时, 创建一个新的空文件 +* `boolean delete()`:删除由此 File 表示的文件或目录(只能删除空目录) +* `boolean mkdir()`:创建由此 File 表示的目录(只能创建一级目录) +* `boolean mkdirs()`:可以创建多级目录(建议使用) + +```java +public class FileDemo { + public static void main(String[] args) throws IOException { + File f = new File("Demo/src/test.txt"); + // a.创建新文件,创建成功返回true ,反之 + System.out.println(f.createNewFile()); + + // b.删除文件或者空文件夹 + System.out.println(f.delete()); + // 不能删除非空文件夹,只能删除空文件夹 + File f1 = new File("E:/it/aaaaa"); + System.out.println(f1.delete()); + + // c.创建一级目录 + File f2 = new File("E:/bbbb"); + System.out.println(f2.mkdir()); + + // d.创建多级目录 + File f3 = new File("D:/it/e/a/d/ds/fas/fas/fas/fas/fas/fas"); + System.out.println(f3.mkdirs()); + } +} +``` + + + +*** + + + +#### 遍历目录 + +- `public String[] list()`:获取当前目录下所有的一级文件名称到一个字符串数组中去返回 +- `public File[] listFiles()`:获取当前目录下所有的一级文件对象到一个**文件对象数组**中去返回(**重点**) +- `public long lastModified`:返回此抽象路径名表示的文件上次修改的时间 + +```java +public class FileDemo { + public static void main(String[] args) { + File dir = new File("D:\\seazean"); + // a.获取当前目录对象下的全部一级文件名称到一个字符串数组返回。 + String[] names = dir.list(); + for (String name : names) { + System.out.println(name); + } + // b.获取当前目录对象下的全部一级文件对象到一个File类型的数组返回。 + File[] files = dir.listFiles(); + for (File file : files) { + System.out.println(file.getAbsolutePath()); + } + + // c + File f1 = new File("D:\\图片资源\\beautiful.jpg"); + long time = f1.lastModified(); // 最后修改时间! + SimpleDateFormat sdf = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss"); + System.out.println(sdf.format(time)); + } +} +``` + + + +*** + + + +#### 文件搜索 + +递归实现文件搜索(非规律递归) + +* 定义一个方法用于做搜索 +* 进入方法中进行业务搜索分析 + +```java +/** + * 去某个目录下搜索某个文件 + * @param dir 搜索文件的目录。 + * @param fileName 搜索文件的名称。 + */ +public static void searchFiles(File dir , String fileName){ + // 1.判断是否存在该路径,是否是文件夹 + if(dir.exists() && dir.isDirectory()){ + // 2.提取当前目录下的全部一级文件对象 + File files = dir.listFiles();// 可能是null/也可能是空集合[] + // 3.判断是否存在一级文件对象,判断是否不为空目录 + if(files != null && files.length > 0){ + // 4.判断一级文件对象 + for(File file : files){ + // 5.判断file是文件还是文件夹 + if(file.isFile()){ + // 6.判断该文件是否为我要找的文件对象 + if(f.getName().contains(fileName)){//模糊查找 + sout(f.getAbsolutePath()); + try { + // 启动它(拓展) + Runtime r = Runtime.getRuntime(); + r.exec(f.getAbsolutePath()); + } catch (IOException e) { + e.printStackTrace(); + } + } + } else { + // 7.该文件是文件夹,文件夹要递归进入继续寻找 + searchFiles(file,fileName) + } + } + } + } +} +``` + + + +*** + + + +### Character + +字符集:为字符编制的一套编号规则 + +计算机的底层是不能直接存储字符的,只能存储二进制 010101 + +ASCII 编码:8 个开关一组就可以编码字符,1 个字节 2^8 = 256, 一个字节存储一个字符完全够用,英文和数字在底层存储都是采用 1 个字节存储的 + +``` +a 97 +b 98 + +A 65 +B 66 + +0 48 +1 49 +``` + +中国人:中国人有 9 万左右字符,2 个字节编码一个中文字符,1 个字节编码一个英文字符,这套编码叫:GBK 编码,兼容 ASCII 编码表 + +美国人:收集全球所有的字符,统一编号,这套编码叫 Unicode 编码(万国码),一个英文等于两个字节,一个中文(含繁体)等于两个字节,中文标点占两个字节,英文标点占两个字节 + +* UTF-8 是变种形式,也必须兼容 ASCII 编码表 +* UTF-8 一个中文一般占 3 个字节,中文标点占 3 个,英文字母和数字 1 个字节 + +编码前与编码后的编码集必须一致才不会乱码 + + + +*** + + + +### IOStream + +#### 概述 + +IO 输入输出流:输入/输出流 + +* Input:输入 +* Output:输出 + +引入:File 类只能操作文件对象本身,不能读写文件对象的内容,读写数据内容,应该使用 IO 流 + +IO 流是一个水流模型:IO 理解成水管,把数据理解成水流 + +IO 流的分类: + +* 按照流的方向分为:输入流,输出流。 + * 输出流:以内存为基准,把内存中的数据**写出到磁盘文件**或者网络介质中去的流称为输出流 + * 输入流:以内存为基准,把磁盘文件中的数据或者网络中的数据**读入到内存**中的流称为输入流 +* 按照流的内容分为:字节流,字符流 + * 字节流:流中的数据的最小单位是一个一个的字节,这个流就是字节流 + * 字符流:流中的数据的最小单位是一个一个的字符,这个流就是字符流(**针对于文本内容**) + +流大体分为四大类:字节输入流、字节输出流、字符输入流、字符输出流 + +```java +IO 流的体系: + 字节流 字符流 + 字节输入流 字节输出流 字符输入流 字符输出流 +InputStream OutputStream Reader Writer (抽象类) +FileInputStream FileOutputStream FileReader FileWriter(实现类) +BufferedInputStream BufferedOutputStream BufferedReader BufferedWriter(实现类缓冲流) + InputStreamReader OutputStreamWriter +ObjectInputStream ObjectOutputStream +``` + + + +**** + + + +#### 字节流 + +##### 字节输入 + +FileInputStream 文件字节输入流:以内存为基准,把磁盘文件中的数据按照字节的形式读入到内存中的流 + +构造方法: + +* `public FileInputStream(File path)`:创建一个字节输入流管道与源文件对象接通 +* `public FileInputStream(String pathName)`:创建一个字节输入流管道与文件路径对接,底层实质上创建 File 对象 + +方法: + +* `public int read()`:每次读取一个字节返回,读取完毕会返回 -1 +* `public int read(byte[] buffer)`:从字节输入流中读取字节到字节数组中去,返回读取的字节数量,没有字节可读返回 -1,**byte 中新读取的数据默认是覆盖原数据**,构造 String 需要设定长度 +* `public String(byte[] bytes,int offset,int length)`:构造新的 String +* `public long transferTo(OutputStream out) `:从输入流中读取所有字节,并按读取的顺序,将字节写入给定的输出流 + +```java +public class FileInputStreamDemo01 { + public static void main(String[] args) throws Exception { + // 1.创建文件对象定位dlei01.txt + File file = new File("Demo/src/dlei01.txt"); + // 2.创建一个字节输入流管道与源文件接通 + InputStream is = new FileInputStream(file); + // 3.读取一个字节的编号返回,读取完毕返回-1 + //int code1 = is.read(); // 读取一滴水,一个字节 + //System.out.println((char)code1); + + // 4.使用while读取字节数 + // 定义一个整数变量存储字节 + int ch = 0 ; + while((ch = is.read())!= -1){ + System.out.print((char) ch); + } + } +} +``` + +一个一个字节读取英文和数字没有问题,但是读取中文输出无法避免乱码,因为会截断中文的字节。一个一个字节的读取数据,性能也较差,所以**禁止使用上面的方案** + +采取下面的方案: + +```java +public static void main(String[] args) throws Exception { + //简化写法,底层实质上创建了File对象 + InputStream is = new FileInputStream("Demo/src/test.txt"); + byte[] buffer = new byte[3];//开发中使用byte[1024] + int len; + while((len = is.read(buffer)) !=-1){ + // 读取了多少就倒出多少! + String rs = new String(buffer, 0, len); + System.out.print(rs); + } +} +``` + +```java +File f = new File("Demo/src/test.txt"); +InputStream is = new FileInputStream(f); +// 读取全部的 +byte[] buffer = is.readAllBytes(); +String rs = new String(buffer); +System.out.println(rs); +``` + + + +**** + + + +##### 字节输出 + +FileOutputStream 文件字节输出流:以内存为基准,把内存中的数据,按照字节的形式写出到磁盘文件中去 + +构造方法: + +* `public FileOutputStream(File file)`:创建一个字节输出流管道通向目标文件对象 +* `public FileOutputStream(String file) `:创建一个字节输出流管道通向目标文件路径 +* `public FileOutputStream(File file, boolean append)` : 创建一个追加数据的字节输出流管道到目标文件对象 +* `public FileOutputStream(String file, boolean append)` : 创建一个追加数据的字节输出流管道通向目标文件路径 + +API: + +* `public void write(int a)`:写一个字节出去 +* `public void write(byte[] buffer)`:写一个字节数组出去 +* `public void write(byte[] buffer , int pos , int len)`:写一个字节数组的一部分出去,从 pos 位置,写出 len 长度 + +* FileOutputStream 字节输出流每次启动写数据的时候都会先清空之前的全部数据,重新写入: + * `OutputStream os = new FileOutputStream("Demo/out05")`:覆盖数据管道 + * `OutputStream os = new FileOutputStream("Demo/out05" , true)`:追加数据的管道 + +说明: + +* 字节输出流只能写字节出去,字节输出流默认是**覆盖**数据管道 +* 换行用:**os.write("\r\n".getBytes())** +* 关闭和刷新:刷新流可以继续使用,关闭包含刷新数据但是流就不能使用了 + +```java +OutputStream os = new FileOutputStream("Demo/out05"); +os.write(97);//a +os.write('b'); +os.write("\r\n".getBytes()); +os.write("我爱Java".getBytes()); +os.close(); +``` + + + +##### 文件复制 + +字节是计算机中一切文件的组成,所以字节流适合做一切文件的复制 + +```java +public class CopyDemo01 { + public static void main(String[] args) { + InputStream is = null ; + OutputStream os = null ; + try{ + //(1)创建一个字节输入流管道与源文件接通。 + is = new FileInputStream("D:\\seazean\\图片资源\\test.jpg"); + //(2)创建一个字节输出流与目标文件接通。 + os = new FileOutputStream("D:\\seazean\\test.jpg"); + //(3)创建一个字节数组作为桶 + byte buffer = new byte[1024]; + //(4)从字节输入流管道中读取数据,写出到字节输出流管道即可 + int len = 0; + while((len = is.read(buffer)) != -1){ + os.write(buffer,0,len); + } + System.out.println("复制完成!"); + }catch (Exception e){ + e.printStackTrace(); + } finally { + /**(5)关闭资源! */ + try{ + if(os!=null)os.close(); + if(is!=null)is.close(); + }catch (Exception e){ + e.printStackTrace(); + } + } + } +} +``` + + + +*** + + + +#### 字符流 + +##### 字符输入 + +FileReader:文件字符输入流,以内存为基准,把磁盘文件的数据以字符的形式读入到内存,读取文本文件内容到内存中去 + +构造器: + +* `public FileReader(File file)`:创建一个字符输入流与源文件对象接通。 +* `public FileReader(String filePath)`:创建一个字符输入流与源文件路径接通。 + +方法: + +* `public int read()`:读取一个字符的编号返回,读取完毕返回 -1 +* `public int read(char[] buffer)`:读取一个字符数组,读取多少个就返回多少个,读取完毕返回 -1 + +结论: + +* 字符流一个一个字符的读取文本内容输出,可以解决中文读取输出乱码的问题,适合操作文本文件,但是一个一个字符的读取文本内容性能较差 +* 字符流按照**字符数组循环读取数据**,可以解决中文读取输出乱码的问题,而且性能也较好 + +**字符流不能复制图片,视频等类型的文件**。字符流在读取完了字节数据后并没有直接往目的地写,而是先查编码表,查到对应的数据就将该数据写入目的地。如果查不到,则码表会将一些未知区域中的数据去 map 这些字节数据,然后写到目的地,这样的话就造成了源数据和目的数据的不一致。 + +```java +public class FileReaderDemo01{//字符 + public static void main(String[] args) throws Exception { + // 创建一个字符输入流管道与源文件路径接通 + Reader fr = new FileReader("Demo/src/test.txt"); + int ch; + while((ch = fr.read()) != -1){ + System.out.print((char)ch); + } + } +} +public class FileReaderDemo02 {//字符数组 + public static void main(String[] args) throws Exception { + Reader fr = new FileReader("Demo/src/test.txt"); + + char[] buffer = new char[1024]; + int len; + while((len = fr.read(buffer)) != -1) { + System.out.print(new String(buffer, 0 , len)); + } + } +} +``` + + + +*** + + + +##### 字符输出 + +FileWriter:文件字符输出流,以内存为基准,把内存中的数据按照字符的形式写出到磁盘文件中去 + +构造器: + +* `public FileWriter(File file)`:创建一个字符输出流管道通向目标文件对象(覆盖数据管道) +* `public FileWriter(String filePath)`:创建一个字符输出流管道通向目标文件路径 +* `public FileWriter(File file, boolean append)`:创建一个追加数据的字符输出流管道通向文件对象(追加数据管道) +* `public FileWriter(String filePath, boolean append)`:创建一个追加数据的字符输出流管道通向目标文件路径 + +方法: + +* `public void write(int c)`:写一个字符出去 +* `public void write(char[] buffer)`:写一个字符数组出去 +* `public void write(String c, int pos, int len)`:写字符串的一部分出去 +* `public void write(char[] buffer, int pos, int len)`:写字符数组的一部分出去 +* `fw.write("\r\n")`:换行 + +读写字符文件数据建议使用字符流 + +```java +Writer fw = new FileWriter("Demo/src/test.txt"); +fw.write(97); // 字符a +fw.write('b'); // 字符b +fw.write("Java是最优美的语言!"); +fw.write("\r\n"); +fw.close; +``` + + + +**** + + + +#### 缓冲流 + +##### 基本介绍 + +缓冲流可以提高字节流和字符流的读写数据的性能 + +缓冲流分为四类: + +* BufferedInputStream:字节缓冲输入流,可以提高字节输入流读数据的性能 +* BufferedOutStream:字节缓冲输出流,可以提高字节输出流写数据的性能 +* BufferedReader:字符缓冲输入流,可以提高字符输入流读数据的性能 +* BufferedWriter:字符缓冲输出流,可以提高字符输出流写数据的性能 + + + +*** + + + +##### 字节缓冲输入 + +字节缓冲输入流:BufferedInputStream + +作用:可以把低级的字节输入流包装成一个高级的缓冲字节输入流管道,提高字节输入流读数据的性能 + +构造器:`public BufferedInputStream(InputStream in)` + +原理:缓冲字节输入流管道自带了一个 8KB 的缓冲池,每次可以直接借用操作系统的功能最多提取 8KB 的数据到缓冲池中去,以后我们直接从缓冲池读取数据,所以性能较好 + +```java +public class BufferedInputStreamDemo01 { + public static void main(String[] args) throws Exception { + // 1.定义一个低级的字节输入流与源文件接通 + InputStream is = new FileInputStream("Demo/src/test.txt"); + // 2.把低级的字节输入流包装成一个高级的缓冲字节输入流。 + BufferInputStream bis = new BufferInputStream(is); + // 3.定义一个字节数组按照循环读取。 + byte[] buffer = new byte[1024]; + int len; + while((len = bis.read(buffer)) != -1){ + String rs = new String(buffer, 0 , len); + System.out.print(rs); + } + } +} +``` + + + +*** + + + +##### 字节缓冲输出 + +字节缓冲输出流:BufferedOutputStream + +作用:可以把低级的字节输出流包装成一个高级的缓冲字节输出流,从而提高写数据的性能 + +构造器:`public BufferedOutputStream(OutputStream os)` + +原理:缓冲字节输出流自带了 8KB 缓冲池,数据就直接写入到缓冲池中去,性能提高了 + +```java +public class BufferedOutputStreamDemo02 { + public static void main(String[] args) throws Exception { + // 1.写一个原始的字节输出流 + OutputStream os = new FileOutputStream("Demo/src/test.txt"); + // 2.把低级的字节输出流包装成一个高级的缓冲字节输出流 + BufferedOutputStream bos = new BufferedOutputStream(os); + // 3.写数据出去 + bos.write('a'); + bos.write(100); + bos.write("我爱中国".getBytes()); + bos.close(); + } +} + +``` + + + +##### 字节流性能 + +利用字节流的复制统计各种写法形式下缓冲流的性能执行情况 + +复制流: + +* 使用低级的字节流按照一个一个字节的形式复制文件 +* 使用低级的字节流按照一个一个字节数组的形式复制文件 +* 使用高级的缓冲字节流按照一个一个字节的形式复制文件 +* 使用高级的缓冲字节流按照一个一个字节数组的形式复制文件 + +高级的缓冲字节流按照一个一个字节数组的形式复制文件,性能最高,建议使用 + + + +**** + + + +##### 字符缓冲输入 + +字符缓冲输入流:BufferedReader + +作用:字符缓冲输入流把字符输入流包装成高级的缓冲字符输入流,可以提高字符输入流读数据的性能。 + +构造器:`public BufferedReader(Reader reader)` + +原理:缓冲字符输入流默认会有一个 8K 的字符缓冲池,可以提高读字符的性能 + +按照行读取数据的功能:`public String readLine()` 读取一行数据返回,读取完毕返回 null + +```java +public static void main(String[] args) throws Exception { + // 1.定义一个原始的字符输入流读取源文件 + Reader fr = new FileReader("Demo/src/test.txt"); + // 2.把低级的字符输入流管道包装成一个高级的缓冲字符输入流管道 + BufferedReader br = new BufferedReader(fr); + // 定义一个字符串变量存储每行数据 + String line; + while((line = br.readLine()) != null){ + System.out.println(line); + } + br.close(); + //淘汰数组循环读取 + //char[] buffer = new char[1024]; + //int len; + //while((len = br.read(buffer)) != -1){ + //System.out.println(new String(buffer , 0 , len)); +} +``` + + + +*** + + + +##### 字符缓冲输出 + +符缓冲输出流:BufferedWriter + +作用:把低级的字符输出流包装成一个高级的缓冲字符输出流,提高写字符数据的性能。 + +构造器:`public BufferedWriter(Writer writer)` + + 原理:高级的字符缓冲输出流多了一个 8K 的字符缓冲池,写数据性能极大提高了 + +字符缓冲输出流多了一个换行的特有功能:`public void newLine()` **新建一行** + +```java +public static void main(String[] args) throws Exception { + Writer fw = new FileWriter("Demo/src/test.txt",true);//追加 + BufferedWriter bw = new BufferedWriter(fw); + + bw.write("我爱学习Java"); + bw.newLine();//换行 + bw.close(); +} +``` + + + +*** + + + +##### 高效原因 + +字符型缓冲流高效的原因:(空间换时间) + +* BufferedReader:每次调用 read 方法,只有第一次从磁盘中读取了 8192(**8k**)个字符,存储到该类型对象的缓冲区数组中,将其中一个返回给调用者,再次调用 read 方法时,就不需要访问磁盘,直接从缓冲区中拿出一个数据即可,提升了效率 +* BufferedWriter:每次调用 write 方法,不会直接将字符刷新到文件中,而是存储到字符数组中,等字符数组写满了,才一次性刷新到文件中,减少了和磁盘交互的次数,提升了效率 + +字节型缓冲流高效的原因: + +* BufferedInputStream:在该类型中准备了一个数组,存储字节信息,当外界调用 read() 方法想获取一个字节的时候,该对象从文件中一次性读取了 8192 个字节到数组中,只返回了第一个字节给调用者。将来调用者再次调用 read 方法时,当前对象就不需要再次访问磁盘,只需要从数组中取出一个字节返回给调用者即可,由于读取的是数组,所以速度非常快。当 8192 个字节全都读取完成之后,再需要读取一个字节,就得让该对象到文件中读取下一个 8192 个字节 +* BufferedOutputStream:在该类型中准备了一个数组,存储字节信息,当外界调用 write 方法想写出一个字节的时候,该对象直接将这个字节存储到了自己的数组中,而不刷新到文件中。一直到该数组所有 8192 个位置全都占满,该对象才把这个数组中的所有数据一次性写出到目标文件中。如果最后一次循环没有将数组写满,最终在关闭流对象的时候,也会将该数组中的数据刷新到文件中。 + + + +注意:**字节流和字符流,都是装满时自动写出,或者没满时手动 flush 写出,或 close 时刷新写出** + + + +*** + + + +#### 转换流 + +##### 乱码问题 + +字符流读取: + +``` +代码编码 文件编码 中文情况。 +UTF-8 UTF-8 不乱码! +GBK GBK 不乱码! +UTF-8 GBK 乱码! +``` + +* 如果代码编码和读取的文件编码一致,字符流读取的时候不会乱码 +* 如果代码编码和读取的文件编码不一致,字符流读取的时候会乱码 + + + +*** + + + +##### 字符输入 + +字符输入转换流:InputStreamReader + +作用:解决字符流读取不同编码的乱码问题,把原始的**字节流**按照默认的编码或指定的编码**转换成字符输入流** + +构造器: + +* `public InputStreamReader(InputStream is)`:使用当前代码默认编码 UTF-8 转换成字符流 +* `public InputStreamReader(InputStream is, String charset)`:指定编码把字节流转换成字符流 + +```java +public class InputStreamReaderDemo{ + public static void main(String[] args) throws Exception { + // 1.提取GBK文件的原始字节流 + InputStream is = new FileInputStream("D:\\seazean\\Netty.txt"); + // 2.把原始字节输入流通过转换流,转换成 字符输入转换流InputStreamReader + InputStreamReader isr = new InputStreamReader(is, "GBK"); + // 3.包装成缓冲流 + BufferedReader br = new BufferedReader(isr); + //循环读取 + String line; + while((line = br.readLine()) != null){ + System.out.println(line); + } + } +} +``` + + + +*** + + + +##### 字符输出 + +字符输出转换流:OutputStreamWriter + +作用:可以指定编码**把字节输出流转换成字符输出流**,可以指定写出去的字符的编码 + +构造器: + +* `public OutputStreamWriter(OutputStream os)`:用默认编码 UTF-8 把字节输出流转换成字符输出流 +* `public OutputStreamWriter(OutputStream os, String charset)`:指定编码把字节输出流转换成 + +```Java +OutputStream os = new FileOutputStream("Demo/src/test.txt"); +OutputStreamWriter osw = new OutputStreamWriter(os,"GBK"); +osw.write("我在学习Java"); +osw.close(); +``` + + + +**** + + + +#### 序列化 + +##### 基本介绍 + +对象序列化:把 Java 对象转换成字节序列的过程,将对象写入到 IO 流中,对象 => 文件中 + +对象反序列化:把字节序列恢复为 Java 对象的过程,从 IO 流中恢复对象,文件中 => 对象 + +transient 关键字修饰的成员变量,将不参与序列化 + + + +*** + + + +##### 序列化 + +对象序列化流(对象字节输出流):ObjectOutputStream + +作用:把内存中的 Java 对象数据保存到文件中去 + +构造器:`public ObjectOutputStream(OutputStream out)` + +序列化方法:`public final void writeObject(Object obj)` + +注意:对象如果想参与序列化,对象必须实现序列化接口 **implements Serializable** ,否则序列化失败 + +```java +public class SerializeDemo01 { + public static void main(String[] args) throws Exception { + // 1.创建User用户对象 + User user = new User("seazean","980823","七十一"); + // 2.创建低级的字节输出流通向目标文件 + OutputStream os = new FileOutputStream("Demo/src/obj.dat"); + // 3.把低级的字节输出流包装成高级的对象字节输出流 ObjectOutputStream + ObjectOutputStream oos = new ObjectOutputStream(os); + // 4.通过对象字节输出流序列化对象: + oos.writeObject(user); + // 5.释放资源 + oos.close(); + System.out.println("序列化对象成功~~~~"); + } +} + +class User implements Serializable { + // 加入序列版本号 + private static final long serialVersionUID = 1L; + + private String loginName; + private transient String passWord; + private String userName; + // get+set +} +``` + +```java +// 序列化为二进制数据 +ByteArrayOutputStream bos = new ByteArrayOutputStream(); +ObjectOutputStream oos = new ObjectOutputStream(bos); +oos.writeObject(obj); // 将该对象序列化为二进制数据 +oos.flush(); +byte[] bytes = bos.toByteArray(); +``` + + + + + +**** + + + +##### 反序列 + +对象反序列化(对象字节输入流):ObjectInputStream + +作用:读取序列化的对象文件恢复到 Java 对象中 + +构造器:`public ObjectInputStream(InputStream is)` + +方法:`public final Object readObject()` + +序列化版本号:`private static final long serialVersionUID = 2L` + +注意:序列化使用的版本号和反序列化使用的版本号一致才可以正常反序列化,否则报错 + +```java +public class SerializeDemo02 { + public static void main(String[] args) throws Exception { + InputStream is = new FileInputStream("Demo/src/obj.dat"); + ObjectInputStream ois = new ObjectInputStream(is); + User user = (User)ois.readObject();//反序列化 + System.out.println(user); + System.out.println("反序列化完成!"); + } +} +class User implements Serializable { + // 加入序列版本号 + private static final long serialVersionUID = 1L; + //........ +} +``` + + + +**** + + + +#### 打印流 + +打印流 PrintStream / PrintWriter + +打印流的作用: + +* 可以方便,快速的写数据出去,可以实现打印什么类型,就是什么类型 +* PrintStream/PrintWriter 不光可以打印数据,还可以写字节数据和字符数据出去 +* **System.out.print() 底层基于打印流实现的** + +构造器: + +* `public PrintStream(OutputStream os)` +* `public PrintStream(String filepath)` + +System 类: + +* `public static void setOut(PrintStream out)`:让系统的输出流向打印流 + +```java +public class PrintStreamDemo01 { + public static void main(String[] args) throws Exception { + PrintStream ps = new PrintStream("Demo/src/test.txt"); + ps.println(任何类型的数据); + ps.print(不换行); + ps.write("我爱你".getBytes()); + ps.close(); + } +} +public class PrintStreamDemo02 { + public static void main(String[] args) throws Exception { + System.out.println("==seazean0=="); + PrintStream ps = new PrintStream("Demo/src/log.txt"); + System.setOut(ps); // 让系统的输出流向打印流 + //不输出在控制台,输出到文件里 + System.out.println("==seazean1=="); + System.out.println("==seazean2=="); + } +} +``` + + + +*** + + + +### Close + +try-with-resources: + +```java +try( + // 这里只能放置资源对象,用完会自动调用close()关闭 +){ + +}catch(Exception e){ + e.printStackTrace(); +} +``` + +资源类一定是实现了 Closeable 接口,实现这个接口的类就是资源 + +有 close() 方法,try-with-resources 会自动调用它的 close() 关闭资源 + +```java +try( + /** (1)创建一个字节输入流管道与源文件接通。 */ + InputStream is = new FileInputStream("D:\\seazean\\图片资源\\meinv.jpg"); + /** (2)创建一个字节输出流与目标文件接通。*/ + OutputStream os = new FileOutputStream("D:\\seazean\\meimei.jpg"); + /** (5)关闭资源!是自动进行的 */ +){ + byte[] buffer = new byte[1024]; + int len = 0; + while((len = is.read(buffer)) != -1){ + os.write(buffer, 0 , len); + } + System.out.println("复制完成!"); +}catch (Exception e){ + e.printStackTrace(); +} +``` + + + +*** + + + +### Properties + +Properties:属性集对象。就是一个 Map 集合,一个键值对集合 + +核心作用:Properties 代表的是一个属性文件,可以把键值对数据存入到一个属性文件 + +属性文件:后缀是 `.properties` 结尾的文件,里面的内容都是 key=value + +Properties 方法: + +| 方法名 | 说明 | +| -------------------------------------------- | --------------------------------------------- | +| Object setProperty(String key, String value) | 设置集合的键和值,底层调用 Hashtable 方法 put | +| String getProperty(String key) | 使用此属性列表中指定的键搜索属性 | +| Set stringPropertyNames() | 所有键的名称的集合 | +| synchronized void load(Reader r) | 从输入字符流读取属性列表(键和元素对) | +| synchronized void load(InputStream in) | 加载属性文件的数据到属性集对象中去 | +| void store(Writer w, String comments) | 将此属性列表(键和元素对)写入 Properties 表 | +| void store(OutputStream os, String comments) | 保存数据到属性文件中去 | + +````java +public class PropertiesDemo01 { + public static void main(String[] args) throws Exception { + // a.创建一个属性集对象:Properties的对象。 + Properties properties = new Properties();//{} + properties.setProperty("admin" , "123456"); + // b.把属性集对象的数据存入到属性文件中去(重点) + OutputStream os = new FileOutputStream("Demo/src/users.properties"); + properties.store(os,"i am very happy!!我保存了用户数据!"); + //参数一:被保存数据的输出管道 + //参数二:保存心得。就是对象保存的数据进行解释说明! + } +} +```` + +````java +public class PropertiesDemo02 { + public static void main(String[] args) throws Exception { + Properties properties = new Properties();//底层基于map集合 + properties.load(new FileInputStream("Demo/src/users.properties")); + System.out.println(properties); + System.out.println(properties.getProperty("admin")); + + Set set = properties.stringPropertyNames(); + for (String s : set) { + String value = properties.getProperty(s); + System.out.println(s + value); + } + } +} +```` + + + +*** + + + +### RandomIO + +RandomAccessFile 类:该类的实例支持读取和写入随机访问文件 + +构造器: + +* `RandomAccessFile(File file, String mode)`:创建随机访问文件流,从 File 参数指定的文件读取,可选择写入 +* `RandomAccessFile(String name, String mode)`:创建随机访问文件流,从指定名称文件读取,可选择写入文件 + +常用方法: + +* `public void seek(long pos)`:设置文件指针偏移,从该文件开头测量,发生下一次读取或写入(插入+覆盖) +* `public void write(byte[] b)`:从指定的字节数组写入 b.length 个字节到该文件 +* `public int read(byte[] b)`:从该文件读取最多 b.length 个字节的数据到字节数组 + +```java +public static void main(String[] args) throws Exception { + RandomAccessFile rf = new RandomAccessFile(new File(),"rw"); + rf.write("hello world".getBytes()); + rf.seek(5);//helloxxxxld + rf.write("xxxx".getBytes()); + rf.close(); +} +``` + + + +*** + + + +### Commons + +commons-io 是 apache 提供的一组有关 IO 操作的类库,可以提高 IO 功能开发的效率 + +commons-io 工具包提供了很多有关 IO 操作的类: + +| 包 | 功能描述 | +| ----------------------------------- | :---------------------------------------------- | +| org.apache.commons.io | 有关 Streams、Readers、Writers、Files 的工具类 | +| org.apache.commons.io.input | 输入流相关的实现类,包含 Reader 和 InputStream | +| org.apache.commons.io.output | 输出流相关的实现类,包含 Writer 和 OutputStream | +| org.apache.commons.io.serialization | 序列化相关的类 | + +IOUtils 和 FileUtils 可以方便的复制文件和文件夹 + +```java +public class CommonsIODemo01 { + public static void main(String[] args) throws Exception { + // 1.完成文件复制! + IOUtils.copy(new FileInputStream("Demo/src/books.xml"), + new FileOutputStream("Demo/new.xml")); + // 2.完成文件复制到某个文件夹下! + FileUtils.copyFileToDirectory(new File("Demo/src/books.xml"), + new File("D:/it")); + // 3.完成文件夹复制到某个文件夹下! + FileUtils.copyDirectoryToDirectory(new File("D:\\it\\图片服务器") , + new File("D:\\")); + + // Java从1.7开始提供了一些nio, 自己也有一行代码完成复制的技术。 + Files.copy(Paths.get("Demo/src/books.xml") + , new FileOutputStream("Demo/new11.txt")); + } +} +``` + + + + + +*** + + + + + +## 反射 + +### 测试框架 + +单元测试的经典框架:Junit,是 Java 语言编写的第三方单元测试框架 + +单元测试: +* 单元:在 Java 中,一个类就是一个单元 +* 单元测试:Junit 编写的一小段代码,用来对某个类中的某个方法进行功能测试或业务逻辑测试 + +Junit 单元测试框架的作用: + +* 用来对类中的方法功能进行有目的的测试,以保证程序的正确性和稳定性 +* 能够**独立的**测试某个方法或者所有方法的预期正确性 + +测试方法注意事项:**必须是 public 修饰的,没有返回值,没有参数,使用注解@Test修饰** + +Junit常用注解(Junit 4.xxxx 版本),@Test 测试方法: + +* @Before:用来修饰实例方法,该方法会在每一个测试方法执行之前执行一次 +* @After:用来修饰实例方法,该方法会在每一个测试方法执行之后执行一次 +* @BeforeClass:用来静态修饰方法,该方法会在所有测试方法之前**只**执行一次 +* @AfterClass:用来静态修饰方法,该方法会在所有测试方法之后**只**执行一次 + +Junit 常用注解(Junit5.xxxx 版本),@Test 测试方法: + +* @BeforeEach:用来修饰实例方法,该方法会在每一个测试方法执行之前执行一次 +* @AfterEach:用来修饰实例方法,该方法会在每一个测试方法执行之后执行一次 +* @BeforeAll:用来静态修饰方法,该方法会在所有测试方法之前只执行一次 +* @AfterAll:用来静态修饰方法,该方法会在所有测试方法之后只执行一次 + +作用: + +* 开始执行的方法:初始化资源 +* 执行完之后的方法:释放资源 + +```java +public class UserService { + public String login(String loginName , String passWord){ + if("admin".equals(loginName)&&"123456".equals(passWord)){ + return "success"; + } + return "用户名或者密码错误!"; + } + public void chu(int a , int b){ + System.out.println(a / b); + } +} +``` + +```java +//测试方法的要求:1.必须public修饰 2.没有返回值没有参数 3. 必须使注解@Test修饰 +public class UserServiceTest { + // @Before:用来修饰实例方法,该方法会在每一个测试方法执行之前执行一次。 + @Before + public void before(){ + System.out.println("===before==="); + } + // @After:用来修饰实例方法,该方法会在每一个测试方法执行之后执行一次。 + @After + public void after(){ + System.out.println("===after==="); + } + // @BeforeClass:用来静态修饰方法,该方法会在所有测试方法之前只执行一次。 + @BeforeClass + public static void beforeClass(){ + System.out.println("===beforeClass==="); + } + // @AfterClass:用来静态修饰方法,该方法会在所有测试方法之后只执行一次。 + @AfterClass + public static void afterClass(){ + System.out.println("===afterClass==="); + } + @Test + public void testLogin(){ + UserService userService = new UserService(); + String rs = userService.login("admin","123456"); + /**断言预期结果的正确性。 + * 参数一:测试失败的提示信息。 + * 参数二:期望值。 + * 参数三:实际值 + */ + Assert.assertEquals("登录业务功能方法有错误,请检查!","success",rs); + } + @Test + public void testChu(){ + UserService userService = new UserService(); + userService.chu(10 , 0); + } +} +``` + + + + + +**** + + + +### 介绍反射 + +反射是指对于任何一个类,在"运行的时候"都可以直接得到这个类全部成分 + +* 构造器对象:Constructor +* 成员变量对象:Field + +* 成员方法对象:Method + +核心思想:在运行时获取类编译后的字节码文件对象,然后解析类中的全部成分 + +反射提供了一个 Class 类型:HelloWorld.java → javac → HelloWorld.class + +* `Class c = HelloWorld.class` + +注意:反射是工作在**运行时**的技术,只有运行之后才会有 class 类对象 + +作用:可以在运行时得到一个类的全部成分然后操作,破坏封装性,也可以破坏泛型的约束性。 + +反射的优点: + +- 可扩展性:应用程序可以利用全限定名创建可扩展对象的实例,来使用来自外部的用户自定义类 +- 类浏览器和可视化开发环境:一个类浏览器需要可以枚举类的成员,可视化开发环境(如 IDE)可以从利用反射中可用的类型信息中受益,以帮助程序员编写正确的代码 +- 调试器和测试工具: 调试器需要能够检查一个类里的私有成员,测试工具可以利用反射来自动地调用类里定义的可被发现的 API 定义,以确保一组测试中有较高的代码覆盖率 + +反射的缺点: + +- **性能开销**:反射涉及了动态类型的解析,所以 JVM 无法对这些代码进行优化,反射操作的效率要比那些非射操作低得多,应该避免在经常被执行的代码或对性能要求很高的程序中使用反射 +- 安全限制:使用反射技术要求程序必须在一个没有安全限制的环境中运行,如果一个程序必须在有安全限制的环境中运行 +- 内部暴露:由于反射允许代码执行一些在正常情况下不被允许的操作(比如访问私有的属性和方法),所以使用反射可能会导致意料之外的副作用,这可能导致代码功能失调并破坏可移植性。反射代码破坏了抽象性,因此当平台发生改变的时候,代码的行为就有可能也随着变化 + + + +*** + + + +### 获取元素 + +#### 获取类 + +反射技术的第一步是先得到 Class 类对象,有三种方式获取: + +* 类名.class +* 类的对象.getClass() +* Class.forName("类的全限名"):`public static Class forName(String className) ` + +Class 类下的方法: + +| 方法 | 作用 | +| ---------------------- | ------------------------------------------------------------ | +| String getSimpleName() | 获得类名字符串:类名 | +| String getName() | 获得类全名:包名+类名 | +| T newInstance() | 创建 Class 对象关联类的对象,底层是调用无参数构造器,已经被淘汰 | + +```java +public class ReflectDemo{ + public static void main(String[] args) throws Exception { + // 反射的第一步永远是先得到类的Class文件对象: 字节码文件。 + // 1.类名.class + Class c1 = Student.class; + System.out.println(c1);//class _03反射_获取Class类对象.Student + + // 2.对象.getClass() + Student swk = new Student(); + Class c2 = swk.getClass(); + System.out.println(c2); + + // 3.Class.forName("类的全限名") + // 直接去加载该类的class文件。 + Class c3 = Class.forName("_03反射_获取Class类对象.Student"); + System.out.println(c3); + + System.out.println(c1.getSimpleName()); // 获取类名本身(简名)Student + System.out.println(c1.getName()); //获取类的全限名_03反射_获取Class类对象.Student + } +} +class Student{} +``` + + + +*** + + + +#### 获取构造 + +获取构造器的 API: + +* Constructor getConstructor(Class... parameterTypes):根据参数匹配获取某个构造器,只能拿 public 修饰的构造器 +* Constructor getDeclaredConstructor(Class... parameterTypes):根据参数匹配获取某个构造器,只要申明就可以定位,不关心权限修饰符 +* Constructor[] getConstructors():获取所有的构造器,只能拿 public 修饰的构造器 +* Constructor[] getDeclaredConstructors():获取所有构造器,只要申明就可以定位,不关心权限修饰符 + +Constructor 的常用 API: + +| 方法 | 作用 | +| --------------------------------- | --------------------------------------- | +| T newInstance(Object... initargs) | 创建对象,注入构造器需要的数据 | +| void setAccessible(true) | 修改访问权限,true 攻破权限(暴力反射) | +| String getName() | 以字符串形式返回此构造函数的名称 | +| int getParameterCount() | 返回参数数量 | +| Class[] getParameterTypes | 返回参数类型数组 | + +```java +public class TestStudent01 { + @Test + public void getDeclaredConstructors(){ + // a.反射第一步先得到Class类对象 + Class c = Student.class ; + // b.定位全部构造器,只要申明了就可以拿到 + Constructor[] cons = c.getDeclaredConstructors(); + // c.遍历这些构造器 + for (Constructor con : cons) { + System.out.println(con.getName()+"->"+con.getParameterCount()); + } + } + @Test + public void getDeclaredConstructor() throws Exception { + // a.反射第一步先得到Class类对象 + Class c = Student.class ; + // b.定位某个构造器,根据参数匹配,只要申明了就可以获取 + //Constructor con = c.getDeclaredConstructor(); // 可以拿到!定位无参数构造器! + Constructor con = c.getDeclaredConstructor(String.class, int.class); //有参数的!! + // c.构造器名称和参数 + System.out.println(con.getName()+"->"+con.getParameterCount()); + } +} +``` + +```java +public class Student { + private String name ; + private int age ; + private Student(){ + System.out.println("无参数构造器被执行~~~~"); + } + public Student(String name, int age) { + System.out.println("有参数构造器被执行~~~~"); + this.name = name; + this.age = age; + } +} +``` + +```java +//测试方法 +public class TestStudent02 { + // 1.调用无参数构造器得到一个类的对象返回。 + @Test + public void createObj01() throws Exception { + // a.反射第一步是先得到Class类对象 + Class c = Student.class ; + // b.定位无参数构造器对象 + Constructor constructor = c.getDeclaredConstructor(); + // c.暴力打开私有构造器的访问权限 + constructor.setAccessible(true); + // d.通过无参数构造器初始化对象返回 + Student swk = (Student) constructor.newInstance(); // 最终还是调用无参数构造器的! + System.out.println(swk);//Student{name='null', age=0} + } + + // 2.调用有参数构造器得到一个类的对象返回。 + @Test + public void createObj02() throws Exception { + // a.反射第一步是先得到Class类对象 + Class c = Student.class ; + // b.定位有参数构造器对象 + Constructor constructor = c.getDeclaredConstructor(String.class , int.class); + // c.通过无参数构造器初始化对象返回 + Student swk = (Student) constructor.newInstance("孙悟空",500); // 最终还是调用有参数构造器的! + System.out.println(swk);//Student{name='孙悟空', age=500} + } +} + + +``` + + + +*** + + + +#### 获取变量 + +获取 Field 成员变量 API: + +* Field getField(String name):根据成员变量名获得对应 Field 对象,只能获得 public 修饰 +* Field getDeclaredField(String name):根据成员变量名获得对应 Field 对象,所有申明的变量 +* Field[] getFields():获得所有的成员变量对应的 Field 对象,只能获得 public 的 +* Field[] getDeclaredFields():获得所有的成员变量对应的 Field 对象,只要申明了就可以得到 + +Field 的方法:给成员变量赋值和取值 + +| 方法 | 作用 | +| ---------------------------------- | ----------------------------------------------------------- | +| void set(Object obj, Object value) | 给对象注入某个成员变量数据,**obj 是对象**,value 是值 | +| Object get(Object obj) | 获取指定对象的成员变量的值,**obj 是对象**,没有对象为 null | +| void setAccessible(true) | 暴力反射,设置为可以直接访问私有类型的属性 | +| Class getType() | 获取属性的类型,返回 Class 对象 | +| String getName() | 获取属性的名称 | + +```Java +public class FieldDemo { + //获取全部成员变量 + @Test + public void getDeclaredFields(){ + // a.先获取class类对象 + Class c = Dog.class; + // b.获取全部申明的成员变量对象 + Field[] fields = c.getDeclaredFields(); + for (Field field : fields) { + System.out.println(field.getName()+"->"+field.getType()); + } + } + //获取某个成员变量 + @Test + public void getDeclaredField() throws Exception { + // a.先获取class类对象 + Class c = Dog.class; + // b.定位某个成员变量对象 :根据名称定位!! + Field ageF = c.getDeclaredField("age"); + System.out.println(ageF.getName()+"->"+ageF.getType()); + } +} +``` + +```java +public class Dog { + private String name; + private int age ; + private String color ; + public static String school; + public static final String SCHOOL_1 = "宠物学校"; + + public Dog() { + } + + public Dog(String name, int age, String color) { + this.name = name; + this.age = age; + this.color = color; + } +} +``` + +```java +//测试方法 +public class FieldDemo02 { + @Test + public void setField() throws Exception { + // a.反射的第一步获取Class类对象 + Class c = Dog.class ; + // b.定位name成员变量 + Field name = c.getDeclaredField("name"); + // c.为这个成员变量赋值! + Dog d = new Dog(); + name.setAccessible(true); + name.set(d,"泰迪"); + System.out.println(d);//Dog{name='泰迪', age=0, color='null'} + // d.获取成员变量的值 + String value = name.get(d)+""; + System.out.println(value);//泰迪 + } +} +``` + + + +*** + + + +#### 获取方法 + +获取 Method 方法 API: + +* Method getMethod(String name,Class...args):根据方法名和参数类型获得方法对象,public 修饰 +* Method getDeclaredMethod(String name,Class...args):根据方法名和参数类型获得方法对象,包括 private +* Method[] getMethods():获得类中的所有成员方法对象返回数组,只能获得 public 修饰且包含父类的 +* Method[] getDeclaredMethods():获得类中的所有成员方法对象,返回数组,只获得本类申明的方法 + +Method 常用 API: + +* public Object invoke(Object obj, Object... args):使用指定的参数调用由此方法对象,obj 对象名 + +```java +public class MethodDemo{ + //获得类中的所有成员方法对象 + @Test + public void getDeclaredMethods(){ + // a.先获取class类对象 + Class c = Dog.class ; + // b.获取全部申明的方法! + Method[] methods = c.getDeclaredMethods(); + // c.遍历这些方法 + for (Method method : methods) { + System.out.println(method.getName()+"->" + + method.getParameterCount()+"->" + method.getReturnType()); + } + } + @Test + public void getDeclardMethod() throws Exception { + Class c = Dog.class; + Method run = c.getDeclaredMethod("run"); + // c.触发方法执行! + Dog d = new Dog(); + Object o = run.invoke(d); + System.out.println(o);// 如果方法没有返回值,结果是null + + //参数一:方法名称 参数二:方法的参数个数和类型(可变参数!) + Method eat = c.getDeclaredMethod("eat",String.class); + eat.setAccessible(true); // 暴力反射! + + //参数一:被触发方法所在的对象 参数二:方法需要的入参值 + Object o1 = eat.invoke(d,"肉"); + System.out.println(o1);// 如果方法没有返回值,结果是null + } +} + +public class Dog { + private String name ; + public Dog(){ + } + public void run(){System.out.println("狗跑的贼快~~");} + private void eat(){System.out.println("狗吃骨头");} + private void eat(String name){System.out.println("狗吃"+name);} + public static void inAddr(){System.out.println("在吉山区有一只单身狗!");} +} +``` + + + +*** + + + +### 暴力攻击 + +泛型只能工作在编译阶段,运行阶段泛型就消失了,反射工作在运行时阶段 + +1. 反射可以破坏面向对象的封装性(暴力反射) +2. 同时可以破坏泛型的约束性 + +```java +public class ReflectDemo { + public static void main(String[] args) throws Exception { + List scores = new ArrayList<>(); + scores.add(99.3); + scores.add(199.3); + scores.add(89.5); + // 拓展:通过反射暴力的注入一个其他类型的数据进去。 + // a.先得到集合对象的Class文件对象 + Class c = scores.getClass(); + // b.从ArrayList的Class对象中定位add方法 + Method add = c.getDeclaredMethod("add", Object.class); + // c.触发scores集合对象中的add执行(运行阶段,泛型不能约束了) + add.invoke(scores, "字符串"); + System.out.println(scores); + } +} +``` + + + + + +*** + + + + + +## 注解 + +### 概念 + +注解:类的组成部分,可以给类携带一些额外的信息,提供一种安全的类似注释标记的机制,用来将任何信息或元数据(metadata)与程序元素(类、方法、成员变量等)进行关联 + +* 注解是给编译器或 JVM 看的,编译器或 JVM 可以根据注解来完成对应的功能 +* 注解类似修饰符,应用于包、类型、构造方法、方法、成员变量、参数及本地变量的声明语句中 +* **父类中的注解是不能被子类继承的** + +注解作用: + +* 标记 +* 框架技术多半都是在使用注解和反射,都是属于框架的底层基础技术 +* 在编译时进行格式检查,比如方法重写约束 @Override、函数式接口约束 @FunctionalInterface. + + + +*** + + + +### 注解格式 + +定义格式:自定义注解用 @interface 关键字,注解默认可以标记很多地方 + +```java +修饰符 @interface 注解名{ + // 注解属性 +} +``` + +使用注解的格式:@注解名 + +```java +@Book +@MyTest +public class MyBook { + //方法变量都可以注解 +} + +@interface Book{ +} +@interface MyTest{ +} +``` + + + +*** + + + +### 注解属性 + +#### 普通属性 + +注解可以有属性,**属性名必须带 ()**,在用注解的时候,属性必须赋值,除非属性有默认值 + +属性的格式: + +* 格式 1:数据类型 属性名() +* 格式 2:数据类型 属性名() default 默认值 + +属性适用的数据类型: + +* 八种数据数据类型(int,short,long,double,byte,char,boolean,float)和 String、Class +* 以上类型的数组形式都支持 + +```java +@MyBook(name="《精通Java基础》",authors = {"播仔","Dlei","播妞"} , price = 99.9 ) +public class AnnotationDemo01 { + @MyBook(name="《精通MySQL数据库入门到删库跑路》",authors = {"小白","小黑"} , + price = 19.9 , address = "北京") + public static void main(String[] args) { + } +} +// 自定义一个注解 +@interface MyBook{ + String name(); + String[] authors(); // 数组 + double price(); + String address() default "武汉"; +} + +``` + + + +*** + + + +#### 特殊属性 + +注解的特殊属性名称:value + +* 如果只有一个 value 属性的情况下,使用 value 属性的时候可以省略 value 名称不写 +* 如果有多个属性,且多个属性没有默认值,那么 value 是不能省略的 + +```java +//@Book("/deleteBook.action") +@Book(value = "/deleteBook.action" , age = 12) +public class AnnotationDemo01{ +} + +@interface Book{ + String value(); + int age() default 10; +} +``` + + + +*** + + + +### 元注解 + +元注解是 sun 公司提供的,用来注解自定义注解 + +元注解有四个: + +* @Target:约束自定义注解可以标记的范围,默认值为任何元素,表示该注解用于什么地方,可用值定义在 ElementType 类中: + + - `ElementType.CONSTRUCTOR`:用于描述构造器 + - `ElementType.FIELD`:成员变量、对象、属性(包括 enum 实例) + - `ElementType.LOCAL_VARIABLE`:用于描述局部变量 + - `ElementType.METHOD`:用于描述方法 + - `ElementType.PACKAGE`:用于描述包 + - `ElementType.PARAMETER`:用于描述参数 + - `ElementType.TYPE`:用于描述类、接口(包括注解类型)或 enum 声明 + +* @Retention:定义该注解的生命周期,申明注解的作用范围:编译时,运行时,可使用的值定义在 RetentionPolicy 枚举类中: + + - `RetentionPolicy.SOURCE`:在编译阶段丢弃,这些注解在编译结束之后就不再有任何意义,只作用在源码阶段,生成的字节码文件中不存在,`@Override`、`@SuppressWarnings` 都属于这类注解 + - `RetentionPolicy.CLASS`:在类加载时丢弃,在字节码文件的处理中有用,运行阶段不存在,默认值 + - `RetentionPolicy.RUNTIME` : 始终不会丢弃,运行期也保留该注解,因此可以使用反射机制读取该注解的信息,自定义的注解通常使用这种方式 + +* @Inherited:表示修饰的自定义注解可以被子类继承 + +* @Documented:表示是否将自定义的注解信息添加在 Java 文档中 + +```java +public class AnnotationDemo01{ + // @MyTest // 只能注解方法 + private String name; + + @MyTest + public static void main( String[] args) { + } +} +@Target(ElementType.METHOD) // 申明只能注解方法 +@Retention(RetentionPolicy.RUNTIME) // 申明注解从写代码一直到运行还在,永远存活!! +@interface MyTest{ +} +``` + + + +*** + + + +### 注解解析 + +开发中经常要知道一个类的成分上面到底有哪些注解,注解有哪些属性数据,这都需要进行注解的解析 + +注解解析相关的接口: + +* Annotation:注解类型,该类是所有注解的父类,注解都是一个 Annotation 的对象 +* AnnotatedElement:该接口定义了与注解解析相关的方法 +* Class、Method、Field、Constructor 类成分:实现 AnnotatedElement 接口,拥有解析注解的能力 + +Class 类 API : + +* `Annotation[] getDeclaredAnnotations()`:获得当前对象上使用的所有注解,返回注解数组 +* `T getDeclaredAnnotation(Class annotationClass)`:根据注解类型获得对应注解对象 +* `T getAnnotation(Class annotationClass)`:根据注解类型获得对应注解对象 +* `boolean isAnnotationPresent(Class class)`:判断对象是否使用了指定的注解 +* `boolean isAnnotation()`:此 Class 对象是否表示注释类型 + +注解原理:注解本质是**特殊接口**,继承了 `Annotation` ,其具体实现类是 Java 运行时生成的**动态代理类**,通过反射获取注解时,返回的是运行时生成的动态代理对象 `$Proxy1`,通过代理对象调用自定义注解(接口)的方法,回调 `AnnotationInvocationHandler` 的 `invoke` 方法,该方法会从 `memberValues` 这个 Map 中找出对应的值,而 `memberValues` 的来源是 Java 常量池 + +解析注解数据的原理:注解在哪个成分上,就先拿哪个成分对象,比如注解作用在类上,则要该类的 Class 对象,再来拿上面的注解 + +```java +public class AnnotationDemo{ + @Test + public void parseClass() { + // 1.定位Class类对象 + Class c = BookStore.class; + // 2.判断这个类上是否使用了某个注解 + if(c.isAnnotationPresent(Book.class)){ + // 3.获取这个注解对象 + Book b = (Book)c.getDeclarAnnotation(Book.class); + System.out.println(book.value()); + System.out.println(book.price()); + System.out.println(Arrays.toString(book.authors())); + } + } + @Test + public void parseMethod() throws Exception { + Class c = BookStore.class; + Method run = c.getDeclaredMethod("run"); + if(run.isAnnotationPresent(Book.class)){ + Book b = (Book)run.getDeclaredAnnotation(Book.class); + sout(上面的三个); + } + } +} + +@Book(value = "《Java基础到精通》", price = 99.5, authors = {"张三","李四"}) +class BookStore{ + @Book(value = "《Mybatis持久层框架》", price = 199.5, authors = {"王五","小六"}) + public void run(){ + } +} +@Target({ElementType.TYPE,ElementType.METHOD}) // 类和成员方法上使用 +@Retention(RetentionPolicy.RUNTIME) // 注解永久存活 +@interface Book{ + String value(); + double price() default 100; + String[] authors(); +} +``` + + + + + +**** + + + + + +## XML + +### 概述 + +XML介绍: + +- XML 指可扩展标记语言(EXtensible Markup Language) +- XML 是一种**标记语言**,很类似 HTML,HTML文件也是XML文档 +- XML 的设计宗旨是**传输数据**,而非显示数据 +- XML 标签没有被预定义,需要自行定义标签 +- XML 被设计为具有自我描述性,易于阅读 +- XML 是 W3C 的推荐标准 + +**XML 与 HTML 的区别**: + +* XML 不是 HTML 的替代,XML 和 HTML 为不同的目的而设计 +* XML 被设计为传输和存储数据,其焦点是数据的内容;XMl标签可自定义,便于阅读 +* HTML 被设计用来显示数据,其焦点是数据的外观;HTML标签被预设好,便于浏览器识别 +* HTML 旨在显示信息,而 XML 旨在传输信息 + + + +**** + + + +### 创建 + +person.xml + +```xml + + + 18 + 张三 + + +``` + + + +*** + + + +### 组成 + +XML 文件中常见的组成元素有:文档声明、元素、属性、注释、转义字符、字符区。文件后缀名为 xml + +* **文档声明** + ``,文档声明必须在第一行,以 `` 结束, + + * version:指定 XML 文档版本。必须属性,这里一般选择 1.0 + * enconding:指定当前文档的编码,可选属性,默认值是 utf-8 + * standalone:该属性不是必须的,描述 XML 文件是否依赖其他的 xml 文件,取值为 yes/no + +* **元素** + + * 格式 1:` ` + * 格式 2:`` + * 普通元素的结构由开始标签、元素体、结束标签组成 + * 标签由一对尖括号和合法标识符组成,标签必须成对出现。特殊的标签可以不成对,必须有结束标记 + +* 元素体:可以是元素,也可以是文本,例如:``张三`` + * 空元素:空元素只有标签,而没有结束标签,但**元素必须自己闭合**,例如:```` + * 元素命名:区分大小写、不能使用空格冒号、不建议用 XML、xml、Xml 等开头 + * 必须存在一个根标签,有且只能有一个 + +* **属性**:`` + + * 属性是元素的一部分,它必须出现在元素的开始标签中 + * 属性的定义格式:`属性名=“属性值”`,其中属性值必须使用单引或双引号括起来 + * 一个元素可以有 0~N 个属性,但一个元素中不能出现同名属性 + * 属性名不能使用空格 , 不要使用冒号等特殊字符,且必须以字母开头 + +* **注释**: + XML的注释与HTML相同,既以 `` 结束。 + +* **转义字符** + XML 中的转义字符与 HTML 一样。因为很多符号已经被文档结构所使用,所以在元素体或属性值中想使用这些符号就必须使用转义字符(也叫实体字符),例如:">"、"<"、"'"、"""、"&" + XML 中仅有字符 < 和 & 是非法的。省略号、引号和大于号是合法的,把它们替换为实体引用 + + | 字符 | 预定义的转义字符 | 说明 | + | :--: | :--------------: | :----: | + | < | ``<`` | 小于 | + | > | `` >`` | 大于 | + | " | `` "`` | 双引号 | + | ' | `` '`` | 单引号 | + | & | `` &`` | 和号 | + +* **字符区** + + ```xml + + ``` + + * CDATA 指的是不应由 XML 解析器进行解析的文本数据(Unparsed Character Data) +* CDATA 部分由 "" 结束; + * 大量的转义字符在xml文档中时,会使XML文档的可读性大幅度降低。这时使用CDATA段就会好一些 + + * 规则: + * CDATA 部分不能包含字符串 ]]>,也不允许嵌套的 CDATA 部分 + * 标记 CDATA 部分结尾的 ]]> 不能包含空格或折行 + + ```xml + + + + + + + + + 西门庆 + 32 + + + + select * from student where age < 18 && age > 10; + + + + 10; + ]]> + + + ``` + + + +**** + + + +### 约束 + +#### DTD + +DTD 是文档类型定义(Document Type Definition)。DTD 可以定义在 XML 文档中出现的元素、这些元素出现的次序、它们如何相互嵌套以及 XML 文档结构的其它详细信息。 + +DTD 规则: + +* 约束元素的嵌套层级 + + ```dtd + + ``` + +* 约束元素体里面的数据 + +* 语法 + + ```dtd + + ``` + +* 判断元素 + 简单元素:没有子元素。 + 复杂元素:有子元素的元素; + + * 标签类型 + + | 标签类型 | 代码写法 | 说明 | + | -------- | --------- | -------------------- | + | PCDATA | (#PCDATA) | 被解释的字符串数据 | + | EMPTY | EMPTY | 即空元素,例如\
| + | ANY | ANY | 即任意类型 | + + * 代码 + + ```dtd + + + + + ``` + + * 数量词 + + | 数量词符号 | 含义 | + | ---------- | ---------------------------- | + | 空 | 表示元素出现一次 | + | * | 表示元素可以出现0到多个 | + | + | 表示元素可以出现至少1个 | + | ? | 表示元素可以是0或1个 | + | , | 表示元素需要按照顺序显示 | + | \| | 表示元素需要选择其中的某一个 | + + + +* 属性声明 + + * 语法 + + ```dtd + + ``` + + * 属性类型 + + | 属性类型 | 含义 | + | ---------- | ------------------------------------------------------------ | + | CDATA | 代表属性是文本字符串, eg: | + | ID | 代码该属性值唯一,不能以数字开头, eg: | + | ENUMERATED | 代表属性值在指定范围内进行枚举 Eg: "社科类"是默认值,属性如果不设置默认值就是"社科类" | + + * 属性说明 + + | 属性说明 | 含义 | + | --------- | ----------------------------------------------------------- | + | #REQUIRED | 代表属性是必须有的 | + | #IMPLIED | 代表属性可有可无 | + | #FIXED | 代表属性为固定值,实现方式:book_info CDATA #FIXED "固定值" | + + * 代码 + + ```dtd + + id ID #REQUIRED + 编号 CDATA #IMPLIED + 出版社 (清华|北大) "清华" + type CDATA #FIXED "IT" + > + + ``` + + + +*** + + + +#### Schema + +XSD 定义: + +1. Schema 语言也可作为 XSD(XML Schema Definition) +2. Schema 约束文件本身也是一个 XML 文件,符合 XML 的语法,这个文件的后缀名 .xsd +3. 一个 XML 中可以引用多个 Schema 约束文件,多个 Schema 使用名称空间区分(名称空间类似于 Java 包名) +4. dtd 里面元素类型的取值比较单一常见的是 PCDATA 类型,但是在 Schema 里面可以支持很多个数据类型 +5. **Schema 文件约束 XML 文件的同时也被别的文件约束着** + +XSD 规则: + +1. 创建一个文件,这个文件的后缀名为 .xsd +2. 定义文档声明 +3. schema 文件的根标签为: +4. 在 中定义属性: + * xmlns=http://www.w3.org/2001/XMLSchema + * 代表当前文件时约束别人的,同时这个文件也对该 Schema 进行约束 +5. 在中定义属性 : + * targetNamespace = 唯一的 url 地址,指定当前这个 schema 文件的名称空间。 + * **名称空间**:当其他 xml 使用该 schema 文件,需要引入此空间 +6. 在中定义属性 : + * elementFormDefault="qualified“,表示当前 schema 文件是一个质量良好的文件。 +7. 通过 element 定义元素 +8. **判断当前元素是简单元素还是复杂元素** + +person.xsd + +```scheme + + + targetNamespace="http://www.seazean.cn/javase" + elementFormDefault="qualified" +> + + + + + + + + + + + + + + + + + + +``` + + + + + +*** + + + +### Dom4J + +#### 解析 + +XML 解析就是从 XML 中获取到数据,DOM 是解析思想 + +DOM(Document Object Model):文档对象模型,把文档的各个组成部分看做成对应的对象,把 XML 文件全部加载到内存,在内存中形成一个树形结构,再获取对应的值 + +Dom4J 实现: +* Dom4J 解析器构造方法:`SAXReader saxReader = new SAXReader()` + +* SAXReader 常用 API: + + * `public Document read(File file)`:Reads a Document from the given File + * `public Document read(InputStream in)`:Reads a Document from the given stream using SAX + +* Java Class 类 API: + + * `public InputStream getResourceAsStream(String path)`:加载文件成为一个字节输入流返回 + + + +**** + + + +#### 根元素 + +Document 方法:`Element getRootElement()` 获取根元素 + +```java +// 需求:解析books.xml文件成为一个Document文档树对象,得到根元素对象。 +public class Dom4JDemo { + public static void main(String[] args) throws Exception { + // 1.创建一个dom4j的解析器对象:代表整个dom4j框架。 + SAXReader saxReader = new SAXReader(); + // 2.第一种方式(简单):通过解析器对象去加载xml文件数据,成为一个Document文档树对象。 + //Document document = saxReader.read(new File("Day13Demo/src/books.xml")); + + // 3.第二种方式(代码多点)先把xml文件读成一个字节输入流 + // 这里的“/”是直接去src类路径下寻找文件。 + InputStream is = Dom4JDemo01.class.getResourceAsStream("/books.xml"); + Document document = saxReader.read(is); + System.out.println(document); + //org.dom4j.tree.DefaultDocument@27a5f880 [Document: name null] + // 4.从document文档树对象中提取根元素对象 + Element root = document.getRootElement(); + System.out.println(root.getName());//books + } +} +``` + +```xml + + + + JavaWeb开发教程 + 张三 + 100.00元 + + + 三国演义 + 罗贯中 + 100.00元 + + + + + +``` + + + +**** + + + +#### 子元素 + +Element 元素的 API: + +* String getName():取元素的名称。 +* List elements():获取当前元素下的全部子元素(一级) +* List elements(String name):获取当前元素下的指定名称的全部子元素(一级) +* Element element(String name):获取当前元素下的指定名称的某个子元素,默认取第一个(一级) + +```java +public class Dom4JDemo { + public static void main(String[] args) throws Exception { + SAXReader saxReader = new SAXReader(); + Document document = saxReader.read(new File("Day13Demo/src/books.xml")); + // 3.获取根元素对象 + Element root = document.getRootElement(); + System.out.println(root.getName()); + + // 4.获取根元素下的全部子元素 + List sonElements = root.elements(); + for (Element sonElement : sonElements) { + System.out.println(sonElement.getName()); + } + // 5.获取根源下的全部book子元素 + List sonElements1 = root.elements("book"); + for (Element sonElement : sonElements1) { + System.out.println(sonElement.getName()); + } + + // 6.获取根源下的指定的某个元素 + Element son = root.element("user"); + System.out.println(son.getName()); + // 默认会提取第一个名称一样的子元素对象返回! + Element son1 = root.element("book"); + System.out.println(son1.attributeValue("id")); + } +} + +``` + + + +*** + + + +#### 属性 + +Element 元素的 API: + +* List attributes():获取元素的全部属性对象 +* Attribute attribute(String name):根据名称获取某个元素的属性对象 +* String attributeValue(String var):直接获取某个元素的某个属性名称的值 + +Attribute 对象的 API: + +* String getName():获取属性名称 +* String getValue():获取属性值 + +```java +public class Dom4JDemo { + public static void main(String[] args) throws Exception { + SAXReader saxReader = new SAXReader(); + Document document = saxReader.read(new File("Day13Demo/src/books.xml")); + Element root = document.getRootElement(); + // 4.获取book子元素 + Element bookEle = root.element("book"); + + // 5.获取book元素的全部属性对象 + List attributes = bookEle.attributes(); + for (Attribute attribute : attributes) { + System.out.println(attribute.getName()+"->"+attribute.getValue()); + } + + // 6.获取Book元素的某个属性对象 + Attribute descAttr = bookEle.attribute("desc"); + System.out.println(descAttr.getName()+"->"+descAttr.getValue()); + + // 7.可以直接获取元素的属性值 + System.out.println(bookEle.attributeValue("id")); + System.out.println(bookEle.attributeValue("desc")); + } +} +``` + + + +*** + + + +#### 文本 + +Element: + +* String elementText(String name):可以直接获取当前元素的子元素的文本内容 +* String elementTextTrim(String name):去前后空格,直接获取当前元素的子元素的文本内容 +* String getText():直接获取当前元素的文本内容 +* String getTextTrim():去前后空格,直接获取当前元素的文本内容 + +```java +public class Dom4JDemo { + public static void main(String[] args) throws Exception { + SAXReader saxReader = new SAXReader(); + Document document = saxReader.read(new File("Day13Demo/src/books.xml")); + Element root = document.getRootElement(); + // 4.得到第一个子元素book + Element bookEle = root.element("book"); + + // 5.直接拿到当前book元素下的子元素文本值 + System.out.println(bookEle.elementText("name")); + System.out.println(bookEle.elementTextTrim("name")); // 去前后空格 + System.out.println(bookEle.elementText("author")); + System.out.println(bookEle.elementTextTrim("author")); // 去前后空格 + + // 6.先获取到子元素对象,再获取该文本值 + Element bookNameEle = bookEle.element("name"); + System.out.println(bookNameEle.getText()); + System.out.println(bookNameEle.getTextTrim());// 去前后空格 + } +} +``` + + + + + +**** + + + +### XPath + +Dom4J 可以用于解析整个 XML 的数据,但是如果要检索 XML 中的某些信息,建议使用 XPath + +XPath 常用API: + +* List selectNodes(String var1) : 检索出一批节点集合 +* Node selectSingleNode(String var1) : 检索出一个节点返回 + +XPath 提供的四种检索数据的写法: + +1. 绝对路径:/根元素/子元素/子元素 +2. 相对路径:./子元素/子元素 (.代表了当前元素) +3. 全文搜索: + * //元素:在全文找这个元素 + * //元素1/元素2:在全文找元素1下面的一级元素 2 + * //元素1//元素2:在全文找元素1下面的全部元素 2 +4. 属性查找: + * //@属性名称:在全文检索属性对象 + * //元素[@属性名称]:在全文检索包含该属性的元素对象 + * //元素[@属性名称=值]:在全文检索包含该属性的元素且属性值为该值的元素对象 + +```java +public class XPathDemo { + public static void main(String[] args) throws Exception { + SAXReader saxReader = new SAXReader(); + InputStream is = XPathDemo.class.getResourceAsStream("/Contact.xml"); + Document document = saxReader.read(is); + //1.使用绝对路径定位全部的name名称 + List nameNodes1 = document.selectNodes("/contactList/contact/name"); + for (Node nameNode : nameNodes) { + System.out.println(nameNode.getText()); + } + + //2.相对路径。从根元素开始检索,.代表很根元素 + List nameNodes2 = root.selectNodes("./contact/name"); + + //3.1 在全文中检索name节点 + List nameNodes3 = root.selectNodes("//name");//全部的 + //3.2 在全文中检索所有contact下的所有name节点 //包括sql,不外面的 + List nameNodes3 = root.selectNodes("//contact//name"); + //3.3 在全文中检索所有contact下的直接name节点 + List nameNodes3 = root.selectNodes("//contact/name");//不包括sql和外面 + + //4.1 检索全部属性对象 + List attributes1 = root.selectNodes("//@id");//包括sql4 + //4.2 在全文检索包含该属性的元素对象 + List attributes1 = root.selectNodes("//contact[@id]"); + //4.3 在全文检索包含该属性的元素且属性值为该值的元素对象 + Node nodeEle = document.selectSingleNode("//contact[@id=2]"); + Element ele = (Element)nodeEle; + System.out.println(ele.elementTextTrim("name"));//xi + } +} +``` + +```xml + + + + 小白 + + bai@seazean.cn + + + 小黑 + + hei@seazean.cn + + sql语句 + + + + 小虎 + + hu@seazean.cn + + +外面的名称 + +``` + + + + + +**** + + + + + +## SDP + +### 单例模式 + +#### 基本介绍 + +单例模式(Singleton Pattern)是 Java 中最简单的设计模式之一,提供了一种创建对象的最佳方式 + +单例设计模式分类两种: + +* 饿汉式:类加载就会导致该单实例对象被创建 + +* 懒汉式:类加载不会导致该单实例对象被创建,而是首次使用该对象时才会创建 + + + +*** + + + +#### 饿汉式 + +饿汉式在类加载的过程导致该单实例对象被创建,**虚拟机会保证类加载的线程安全**,但是如果只是为了加载该类不需要实例,则会造成内存的浪费 + +* 静态变量的方式: + + ```java + public final class Singleton { + // 私有构造方法 + private Singleton() {} + // 在成员位置创建该类的对象 + private static final Singleton instance = new Singleton(); + // 对外提供静态方法获取该对象 + public static Singleton getInstance() { + return instance; + } + + // 解决序列化问题 + protected Object readResolve() { + return INSTANCE; + } + } + ``` + + * 加 final 修饰,所以不会被子类继承,防止子类中不适当的行为覆盖父类的方法,破坏了单例 + + * 防止反序列化破坏单例的方式: + + * 对单例声明 transient,然后实现 readObject(ObjectInputStream in) 方法,复用原来的单例 + + 条件:访问权限为 private/protected、返回值必须是 Object、异常可以不抛 + + * 实现 readResolve() 方法,当 JVM 从内存中反序列化地组装一个新对象,就会自动调用 readResolve 方法返回原来单例 + + * 构造方法设置为私有,防止其他类无限创建对象,但是不能防止反射破坏 + + * 静态变量初始化在类加载时完成,**由 JVM 保证线程安全**,能保证单例对象创建时的安全 + + * 提供静态方法而不是直接将 INSTANCE 设置为 public,体现了更好的封装性、提供泛型支持、可以改进成懒汉单例设计 + +* 静态代码块的方式: + + ```java + public class Singleton { + // 私有构造方法 + private Singleton() {} + + // 在成员位置创建该类的对象 + private static Singleton instance; + static { + instance = new Singleton(); + } + + // 对外提供静态方法获取该对象 + public static Singleton getInstance() { + return instance; + } + } + ``` + +* 枚举方式:枚举类型是所用单例实现中**唯一一种不会被破坏**的单例实现模式 + + ```java + public enum Singleton { + INSTANCE; + public void doSomething() { + System.out.println("doSomething"); + } + } + public static void main(String[] args) { + Singleton.INSTANCE.doSomething(); + } + ``` + + * 问题1:枚举单例是如何限制实例个数的?每个枚举项都是一个实例,是一个静态成员变量 + * 问题2:枚举单例在创建时是否有并发问题?否 + * 问题3:枚举单例能否被反射破坏单例?否,反射创建对象时判断是枚举类型就直接抛出异常 + * 问题4:枚举单例能否被反序列化破坏单例?否 + * 问题5:枚举单例属于懒汉式还是饿汉式?**饿汉式** + * 问题6:枚举单例如果希望加入一些单例创建时的初始化逻辑该如何做?添加构造方法 + + 反编译结果: + + ```java + public final class Singleton extends java.lang.Enum { // Enum实现序列化接口 + public static final Singleton INSTANCE = new Singleton(); + } + ``` + + + + + +*** + + + +#### 懒汉式 + +* 线程不安全 + + ```java + public class Singleton { + // 私有构造方法 + private Singleton() {} + + // 在成员位置创建该类的对象 + private static Singleton instance; + + // 对外提供静态方法获取该对象 + public static Singleton getInstance() { + if(instance == null) { + // 多线程环境,会出现线程安全问题,可能多个线程同时进入这里 + instance = new Singleton(); + } + return instance; + } + } + ``` + +* 双端检锁机制 + + 在多线程的情况下,可能会出现空指针问题,出现问题的原因是 JVM 在实例化对象的时候会进行优化和指令重排序操作,所以需要使用 `volatile` 关键字 + + ```java + public class Singleton { + // 私有构造方法 + private Singleton() {} + private static volatile Singleton instance; + + // 对外提供静态方法获取该对象 + public static Singleton getInstance() { + // 第一次判断,如果instance不为null,不进入抢锁阶段,直接返回实例 + if(instance == null) { + synchronized (Singleton.class) { + // 抢到锁之后再次判断是否为null + if(instance == null) { + instance = new Singleton(); + } + } + } + return instance; + } + } + ``` + +* 静态内部类方式 + + ```java + public class Singleton { + // 私有构造方法 + private Singleton() {} + + private static class SingletonHolder { + private static final Singleton INSTANCE = new Singleton(); + } + + // 对外提供静态方法获取该对象 + public static Singleton getInstance() { + return SingletonHolder.INSTANCE; + } + } + ``` + + * 内部类属于懒汉式,类加载本身就是懒惰的,首次调用时加载,然后对单例进行初始化 + + 类加载的时候方法不会被调用,所以不会触发 getInstance 方法调用 invokestatic 指令对内部类进行加载;加载的时候字节码常量池会被加入类的运行时常量池,解析工作是将常量池中的符号引用解析成直接引用,但是解析过程不一定非得在类加载时完成,可以延迟到运行时进行,所以静态内部类实现单例会**延迟加载** + + * 没有线程安全问题,静态变量初始化在类加载时完成,由 JVM 保证线程安全 + + + +*** + + + +#### 破坏单例 + +##### 反序列化 + +将单例对象序列化再反序列化,对象从内存反序列化到程序中会重新创建一个对象,通过反序列化得到的对象是不同的对象,而且得到的对象不是通过构造器得到的,**反序列化得到的对象不执行构造器** + +* Singleton + + ```java + public class Singleton implements Serializable { //实现序列化接口 + // 私有构造方法 + private Singleton() {} + private static class SingletonHolder { + private static final Singleton INSTANCE = new Singleton(); + } + + // 对外提供静态方法获取该对象 + public static Singleton getInstance() { + return SingletonHolder.INSTANCE; + } + } + ``` + +* 序列化 + + ```java + public class Test { + public static void main(String[] args) throws Exception { + //往文件中写对象 + //writeObject2File(); + //从文件中读取对象 + Singleton s1 = readObjectFromFile(); + Singleton s2 = readObjectFromFile(); + //判断两个反序列化后的对象是否是同一个对象 + System.out.println(s1 == s2); + } + + private static Singleton readObjectFromFile() throws Exception { + //创建对象输入流对象 + ObjectInputStream ois = new ObjectInputStream(new FileInputStream("C://a.txt")); + //第一个读取Singleton对象 + Singleton instance = (Singleton) ois.readObject(); + return instance; + } + + public static void writeObject2File() throws Exception { + //获取Singleton类的对象 + Singleton instance = Singleton.getInstance(); + //创建对象输出流 + ObjectOutputStream oos = new ObjectOutputStream(new FileOutputStream("C://a.txt")); + //将instance对象写出到文件中 + oos.writeObject(instance); + } + } + ``` + +* 解决方法: + + 在 Singleton 类中添加 `readResolve()` 方法,在反序列化时被反射调用,如果定义了这个方法,就返回这个方法的值,如果没有定义,则返回新创建的对象 + + ```java + private Object readResolve() { + return SingletonHolder.INSTANCE; + } + ``` + + ObjectInputStream 类源码分析: + + ```java + public final Object readObject() throws IOException, ClassNotFoundException{ + //... + Object obj = readObject0(false);//重点查看readObject0方法 + } + + private Object readObject0(boolean unshared) throws IOException { + try { + switch (tc) { + case TC_OBJECT: + return checkResolve(readOrdinaryObject(unshared)); + } + } + } + private Object readOrdinaryObject(boolean unshared) throws IOException { + // isInstantiable 返回true,执行 desc.newInstance(),通过反射创建新的单例类 + obj = desc.isInstantiable() ? desc.newInstance() : null; + // 添加 readResolve 方法后 desc.hasReadResolveMethod() 方法执行结果为true + if (obj != null && handles.lookupException(passHandle) == null && desc.hasReadResolveMethod()) { + // 通过反射调用 Singleton 类中的 readResolve 方法,将返回值赋值给rep变量 + // 多次调用ObjectInputStream类中的readObject方法,本质调用定义的readResolve方法,返回的是同一个对象。 + Object rep = desc.invokeReadResolve(obj); + } + return obj; + } + ``` + + + +*** + + + +##### 反射破解 + +* 反射 + + ```java + public class Test { + public static void main(String[] args) throws Exception { + //获取Singleton类的字节码对象 + Class clazz = Singleton.class; + //获取Singleton类的私有无参构造方法对象 + Constructor constructor = clazz.getDeclaredConstructor(); + //取消访问检查 + constructor.setAccessible(true); + + //创建Singleton类的对象s1 + Singleton s1 = (Singleton) constructor.newInstance(); + //创建Singleton类的对象s2 + Singleton s2 = (Singleton) constructor.newInstance(); + + //判断通过反射创建的两个Singleton对象是否是同一个对象 + System.out.println(s1 == s2); //false + } + } + ``` + +* 反射方式破解单例的解决方法: + + ```java + public class Singleton { + private static volatile Singleton instance; + + // 私有构造方法 + private Singleton() { + // 反射破解单例模式需要添加的代码 + if(instance != null) { + throw new RuntimeException(); + } + } + + // 对外提供静态方法获取该对象 + public static Singleton getInstance() { + if(instance != null) { + return instance; + } + synchronized (Singleton.class) { + if(instance != null) { + return instance; + } + instance = new Singleton(); + return instance; + } + } + } + ``` + + + + + +*** + + + +#### Runtime + +Runtime 类就是使用的单例设计模式中的饿汉式 + +```java +public class Runtime { + private static Runtime currentRuntime = new Runtime(); + public static Runtime getRuntime() { + return currentRuntime; + } + private Runtime() {} + ... +} +``` + +使用 Runtime + +```java +public class RuntimeDemo { + public static void main(String[] args) throws IOException { + //获取Runtime类对象 + Runtime runtime = Runtime.getRuntime(); + + //返回 Java 虚拟机中的内存总量。 + System.out.println(runtime.totalMemory()); + //返回 Java 虚拟机试图使用的最大内存量。 + System.out.println(runtime.maxMemory()); + + //创建一个新的进程执行指定的字符串命令,返回进程对象 + Process process = runtime.exec("ipconfig"); + //获取命令执行后的结果,通过输入流获取 + InputStream inputStream = process.getInputStream(); + byte[] arr = new byte[1024 * 1024* 100]; + int b = inputStream.read(arr); + System.out.println(new String(arr,0,b,"gbk")); + } +} +``` + + + + + +**** + + + +### 代理模式 + +#### 静态代理 + +代理模式:由于某些原因需要给某对象提供一个代理以控制对该对象的访问,访问对象不适合或者不能直接引用为目标对象,代理对象作为访问对象和目标对象之间的中介 + +Java 中的代理按照代理类生成时机不同又分为静态代理和动态代理,静态代理代理类在编译期就生成,而动态代理代理类则是在 Java 运行时动态生成,动态代理又有 JDK 代理和 CGLib 代理两种 + +代理(Proxy)模式分为三种角色: + +* 抽象主题(Subject)类:通过接口或抽象类声明真实主题和代理对象实现的业务方法 +* 真实主题(Real Subject)类: 实现了抽象主题中的具体业务,是代理对象所代表的真实对象,是最终要引用的对象 +* 代理(Proxy)类:提供了与真实主题相同的接口,其内部含有对真实主题的引用,可以访问、控制或扩展真实主题的功能 + +买票案例,火车站是目标对象,代售点是代理对象 + +* 卖票接口: + + ```java + public interface SellTickets { + void sell(); + } + ``` + +* 火车站,具有卖票功能,需要实现SellTickets接口 + + ```java + public class TrainStation implements SellTickets { + public void sell() { + System.out.println("火车站卖票"); + } + } + ``` + +* 代售点: + + ```java + public class ProxyPoint implements SellTickets { + private TrainStation station = new TrainStation(); + + public void sell() { + System.out.println("代理点收取一些服务费用"); + station.sell(); + } + } + ``` + +* 测试类: + + ```java + public class Client { + public static void main(String[] args) { + ProxyPoint pp = new ProxyPoint(); + pp.sell(); + } + } + ``` + + 测试类直接访问的是 ProxyPoint 类对象,也就是 ProxyPoint 作为访问对象和目标对象的中介 + + + +**** + + + +#### JDK + +##### 使用方式 + +Java 中提供了一个动态代理类 Proxy,Proxy 并不是代理对象的类,而是提供了一个创建代理对象的静态方法 newProxyInstance() 来获取代理对象 + +`static Object newProxyInstance(ClassLoader loader,Class[] interfaces,InvocationHandler h) ` + +* 参数一:类加载器,负责加载代理类。传入类加载器,代理和被代理对象要用一个类加载器才是父子关系,不同类加载器加载相同的类在 JVM 中都不是同一个类对象 + +* 参数二:被代理业务对象的**全部实现的接口**,代理对象与真实对象实现相同接口,知道为哪些方法做代理 + +* 参数三:代理真正的执行方法,也就是代理的处理逻辑 + +代码实现: + +* 代理工厂:创建代理对象 + + ```java + public class ProxyFactory { + private TrainStation station = new TrainStation(); + //也可以在参数中提供 getProxyObject(TrainStation station) + public SellTickets getProxyObject() { + //使用 Proxy 获取代理对象 + SellTickets sellTickets = (SellTickets) Proxy.newProxyInstance( + station.getClass().getClassLoader(), + station.getClass().getInterfaces(), + new InvocationHandler() { + public Object invoke(Object proxy, Method method, Object[] args) { + System.out.println("代理点(JDK动态代理方式)"); + //执行真实对象 + Object result = method.invoke(station, args); + return result; + } + }); + return sellTickets; + } + } + ``` + +* 测试类: + + ```java + public class Client { + public static void main(String[] args) { + //获取代理对象 + ProxyFactory factory = new ProxyFactory(); + //必须时代理ji + SellTickets proxyObject = factory.getProxyObject(); + proxyObject.sell(); + } + } + ``` + + + +*** + + + +##### 实现原理 + +JDK 动态代理方式的优缺点: + +- 优点:可以为任意的接口实现类对象做代理,也可以为被代理对象的所有接口的所有方法做代理,动态代理可以在不改变方法源码的情况下,实现对方法功能的增强,提高了软件的可扩展性,Java 反射机制可以生成任意类型的动态代理类 +- 缺点:**只能针对接口或者接口的实现类对象做代理对象**,普通类是不能做代理对象的 +- 原因:**生成的代理类继承了 Proxy**,Java 是单继承的,所以 JDK 动态代理只能代理接口 + +ProxyFactory 不是代理模式中的代理类,而代理类是程序在运行过程中动态的在内存中生成的类,可以通过 Arthas 工具查看代理类结构: + +* 代理类($Proxy0)实现了 SellTickets 接口,真实类和代理类实现同样的接口 +* 代理类($Proxy0)将提供了的匿名内部类对象传递给了父类 +* 代理类($Proxy0)的修饰符是 public final + +```java +// 程序运行过程中动态生成的代理类 +public final class $Proxy0 extends Proxy implements SellTickets { + private static Method m3; + + public $Proxy0(InvocationHandler invocationHandler) { + super(invocationHandler);//InvocationHandler对象传递给父类 + } + + static { + m3 = Class.forName("proxy.dynamic.jdk.SellTickets").getMethod("sell", new Class[0]); + } + + public final void sell() { + // 调用InvocationHandler的invoke方法 + this.h.invoke(this, m3, null); + } +} + +// Java提供的动态代理相关类 +public class Proxy implements java.io.Serializable { + protected InvocationHandler h; + + protected Proxy(InvocationHandler h) { + this.h = h; + } +} +``` + +执行流程如下: + +1. 在测试类中通过代理对象调用 sell() 方法 +2. 根据多态的特性,执行的是代理类($Proxy0)中的 sell() 方法 +3. 代理类($Proxy0)中的 sell() 方法中又调用了 InvocationHandler 接口的子实现类对象的 invoke 方法 +4. invoke 方法通过反射执行了真实对象所属类(TrainStation)中的 sell() 方法 + + + +**** + + + +##### 源码解析 + +```java +public static Object newProxyInstance(ClassLoader loader, + Class[] interfaces, + InvocationHandler h){ + // InvocationHandler 为空则抛出异常 + Objects.requireNonNull(h); + + // 复制一份 interfaces + final Class[] intfs = interfaces.clone(); + final SecurityManager sm = System.getSecurityManager(); + if (sm != null) { + checkProxyAccess(Reflection.getCallerClass(), loader, intfs); + } + + // 从缓存中查找 class 类型的代理对象,会调用 ProxyClassFactory#apply 方法 + Class cl = getProxyClass0(loader, intfs); + //proxyClassCache = new WeakCache<>(new KeyFactory(), new ProxyClassFactory()) + + try { + if (sm != null) { + checkNewProxyPermission(Reflection.getCallerClass(), cl); + } + + // 获取代理类的构造方法,根据参数 InvocationHandler 匹配获取某个构造器 + final Constructor cons = cl.getConstructor(constructorParams); + final InvocationHandler ih = h; + // 构造方法不是 pubic 的需要启用权限,暴力p + if (!Modifier.isPublic(cl.getModifiers())) { + AccessController.doPrivileged(new PrivilegedAction() { + public Void run() { + // 设置可访问的权限 + cons.setAccessible(true); + return null; + } + }); + } + // cons 是构造方法,并且内部持有 InvocationHandler,在 InvocationHandler 中持有 target 目标对象 + return cons.newInstance(new Object[]{h}); + } catch (IllegalAccessException|InstantiationException e) {} +} +``` + +Proxy 的静态内部类: + +```java +private static final class ProxyClassFactory { + // 代理类型的名称前缀 + private static final String proxyClassNamePrefix = "$Proxy"; + + // 生成唯一数字使用,结合上面的代理类型名称前缀一起生成 + private static final AtomicLong nextUniqueNumber = new AtomicLong(); + + //参数一:Proxy.newInstance 时传递的 + //参数二:Proxy.newInstance 时传递的接口集合 + @Override + public Class apply(ClassLoader loader, Class[] interfaces) { + + Map, Boolean> interfaceSet = new IdentityHashMap<>(interfaces.length); + // 遍历接口集合 + for (Class intf : interfaces) { + Class interfaceClass = null; + try { + // 加载接口类到 JVM + interfaceClass = Class.forName(intf.getName(), false, loader); + } catch (ClassNotFoundException e) { + } + if (interfaceClass != intf) { + throw new IllegalArgumentException( + intf + " is not visible from class loader"); + } + // 如果 interfaceClass 不是接口 直接报错,保证集合内都是接口 + if (!interfaceClass.isInterface()) { + throw new IllegalArgumentException( + interfaceClass.getName() + " is not an interface"); + } + // 保证接口 interfaces 集合中没有重复的接口 + if (interfaceSet.put(interfaceClass, Boolean.TRUE) != null) { + throw new IllegalArgumentException( + "repeated interface: " + interfaceClass.getName()); + } + } + + // 生成的代理类的包名 + String proxyPkg = null; + // 【生成的代理类访问修饰符 public final】 + int accessFlags = Modifier.PUBLIC | Modifier.FINAL; + + // 检查接口集合内的接口,看看有没有某个接口的访问修饰符不是 public 的 如果不是 public 的接口, + // 生成的代理类 class 就必须和它在一个包下,否则访问出现问题 + for (Class intf : interfaces) { + // 获取访问修饰符 + int flags = intf.getModifiers(); + if (!Modifier.isPublic(flags)) { + accessFlags = Modifier.FINAL; + // 获取当前接口的全限定名 包名.类名 + String name = intf.getName(); + int n = name.lastIndexOf('.'); + // 获取包名 + String pkg = ((n == -1) ? "" : name.substring(0, n + 1)); + if (proxyPkg == null) { + proxyPkg = pkg; + } else if (!pkg.equals(proxyPkg)) { + throw new IllegalArgumentException( + "non-public interfaces from different packages"); + } + } + } + + if (proxyPkg == null) { + // if no non-public proxy interfaces, use com.sun.proxy package + proxyPkg = ReflectUtil.PROXY_PACKAGE + "."; + } + + // 获取唯一的编号 + long num = nextUniqueNumber.getAndIncrement(); + // 包名+ $proxy + 数字,比如 $proxy1 + String proxyName = proxyPkg + proxyClassNamePrefix + num; + + // 【生成二进制字节码,这个字节码写入到文件内】,就是编译好的 class 文件 + byte[] proxyClassFile = ProxyGenerator.generateProxyClass(proxyName, interfaces, accessFlags); + try { + // 【使用加载器加载二进制到 jvm】,并且返回 class + return defineClass0(loader, proxyName, proxyClassFile, 0, proxyClassFile.length); + } catch (ClassFormatError e) { } + } +} +``` + + + + + +*** + + + +#### CGLIB + +CGLIB 是一个功能强大,高性能的代码生成包,为没有实现接口的类提供代理,为 JDK 动态代理提供了补充($$Proxy) + +* CGLIB 是第三方提供的包,所以需要引入 jar 包的坐标: + + ```xml + + cglib + cglib + 2.2.2 + + ``` + +* 代理工厂类: + + ```java + public class ProxyFactory implements MethodInterceptor { + private TrainStation target = new TrainStation(); + + public TrainStation getProxyObject() { + //创建Enhancer对象,类似于JDK动态代理的Proxy类,下一步就是设置几个参数 + Enhancer enhancer = new Enhancer(); + //设置父类的字节码对象 + enhancer.setSuperclass(target.getClass()); + //设置回调函数 + enhancer.setCallback(new MethodInterceptor() { + @Override + public Object intercept(Object obj, Method method, Object[] args, MethodProxy proxy) throws Throwable { + System.out.println("代理点收取一些服务费用(CGLIB动态代理方式)"); + Object o = methodProxy.invokeSuper(obj, args); + return null;//因为返回值为void + } + }); + //创建代理对象 + TrainStation obj = (TrainStation) enhancer.create(); + return obj; + } + } + ``` + +CGLIB 的优缺点 + +* 优点: + * CGLIB 动态代理**不限定**是否具有接口,可以对任意操作进行增强 + * CGLIB 动态代理无需要原始被代理对象,动态创建出新的代理对象 + * **JDKProxy 仅对接口方法做增强,CGLIB 对所有方法做增强**,包括 Object 类中的方法,toString、hashCode 等 +* 缺点:CGLIB 不能对声明为 final 的类或者方法进行代理,因为 CGLIB 原理是**动态生成被代理类的子类,继承被代理类** + + + + + +**** + + + +#### 方式对比 + +三种方式对比: + +* 动态代理和静态代理: + + * 动态代理将接口中声明的所有方法都被转移到一个集中的方法中处理(InvocationHandler.invoke),在接口方法数量比较多的时候,可以进行灵活处理,不需要像静态代理那样每一个方法进行中转 + + * 静态代理是在编译时就已经将接口、代理类、被代理类的字节码文件确定下来 + * 动态代理是程序**在运行后通过反射创建字节码文件**交由 JVM 加载 + +* JDK 代理和 CGLIB 代理: + + JDK 动态代理采用 `ProxyGenerator.generateProxyClass()` 方法在运行时生成字节码;CGLIB 底层采用 ASM 字节码生成框架,使用字节码技术生成代理类。在 JDK1.6之前比使用 Java 反射效率要高,到 JDK1.8 的时候,JDK 代理效率高于 CGLIB 代理。所以如果有接口或者当前类就是接口使用 JDK 动态代理,如果没有接口使用 CGLIB 代理 + +代理模式的优缺点: + +* 优点: + * 代理模式在客户端与目标对象之间起到一个中介作用和保护目标对象的作用 + * **代理对象可以增强目标对象的功能,被用来间接访问底层对象,与原始对象具有相同的 hashCode** + * 代理模式能将客户端与目标对象分离,在一定程度上降低了系统的耦合度 + +* 缺点:增加了系统的复杂度 + +代理模式的使用场景: + +* 远程(Remote)代理:本地服务通过网络请求远程服务,需要实现网络通信,处理其中可能的异常。为了良好的代码设计和可维护性,将网络通信部分隐藏起来,只暴露给本地服务一个接口,通过该接口即可访问远程服务提供的功能 + +* 防火墙(Firewall)代理:当你将浏览器配置成使用代理功能时,防火墙就将你的浏览器的请求转给互联网,当互联网返回响应时,代理服务器再把它转给你的浏览器 + +* 保护(Protect or Access)代理:控制对一个对象的访问,如果需要,可以给不同的用户提供不同级别的使用权限 + + + + + + + +*** + + + + + +# JVM + +## JVM概述 + +### 基本介绍 + +JVM:全称 Java Virtual Machine,即 Java 虚拟机,一种规范,本身是一个虚拟计算机,直接和操作系统进行交互,与硬件不直接交互,而操作系统可以帮我们完成和硬件进行交互的工作 + +特点: + +* Java 虚拟机基于**二进制字节码**执行,由一套字节码指令集、一组寄存器、一个栈、一个垃圾回收堆、一个方法区等组成 +* JVM 屏蔽了与操作系统平台相关的信息,从而能够让 Java 程序只需要生成能够在 JVM 上运行的字节码文件,通过该机制实现的**跨平台性** + +Java 代码执行流程:`Java 程序 --(编译)--> 字节码文件 --(解释执行)--> 操作系统(Win,Linux)` + +JVM 结构: + + + +JVM、JRE、JDK 对比: + +* JDK(Java SE Development Kit):Java 标准开发包,提供了编译、运行 Java 程序所需的各种工具和资源 +* JRE( Java Runtime Environment):Java 运行环境,用于解释执行 Java 的字节码文件 + + + + + +参考书籍:https://book.douban.com/subject/34907497/ + +参考视频:https://www.bilibili.com/video/BV1PJ411n7xZ + +参考视频:https://www.bilibili.com/video/BV1yE411Z7AP + + + +*** + + + +### 架构模型 + +Java 编译器输入的指令流是一种基于栈的指令集架构。因为跨平台的设计,Java 的指令都是根据栈来设计的,不同平台 CPU 架构不同,所以不能设计为基于寄存器架构 + +* 基于栈式架构的特点: + * 设计和实现简单,适用于资源受限的系统 + * 使用零地址指令方式分配,执行过程依赖操作栈,指令集更小,编译器容易实现 + * 零地址指令:机器指令的一种,是指令系统中的一种不设地址字段的指令,只有操作码而没有地址码。这种指令有两种情况:一是无需操作数,另一种是操作数为默认的(隐含的),默认为操作数在寄存器(ACC)中,指令可直接访问寄存器 + * 一地址指令:一个操作码对应一个地址码,通过地址码寻找操作数 + * 不需要硬件的支持,可移植性更好,更好实现跨平台 +* 基于寄存器架构的特点: + * 需要硬件的支持,可移植性差 + * 性能更好,执行更高效,寄存器比内存快 + * 以一地址指令、二地址指令、三地址指令为主 + + + +*** + + + +### 生命周期 + +JVM 的生命周期分为三个阶段,分别为:启动、运行、死亡 + +- **启动**:当启动一个 Java 程序时,通过引导类加载器(bootstrap class loader)创建一个初始类(initial class),对于拥有 main 函数的类就是 JVM 实例运行的起点 +- **运行**: + + - main() 方法是一个程序的初始起点,任何线程均可由在此处启动 + - 在 JVM 内部有两种线程类型,分别为:用户线程和守护线程,**JVM 使用的是守护线程,main() 和其他线程使用的是用户线程**,守护线程会随着用户线程的结束而结束 + - 执行一个 Java 程序时,真真正正在执行的是一个 **Java 虚拟机的进程** + - JVM 有两种运行模式 Server 与 Client,两种模式的区别在于:Client 模式启动速度较快,Server 模式启动较慢;但是启动进入稳定期长期运行之后 Server 模式的程序运行速度比 Client 要快很多 + + Server 模式启动的 JVM 采用的是重量级的虚拟机,对程序采用了更多的优化;Client 模式启动的 JVM 采用的是轻量级的虚拟机 +- **死亡**: + + - 当程序中的用户线程都中止,JVM 才会退出 + - 程序正常执行结束、程序异常或错误而异常终止、操作系统错误导致终止 + - 线程调用 Runtime 类 halt 方法或 System 类 exit 方法,并且 Java 安全管理器允许这次 exit 或 halt 操作 + + + + + +*** + + + + + +## 内存结构 + +### 内存概述 + +内存结构是 JVM 中非常重要的一部分,是非常重要的系统资源,是硬盘和 CPU 的桥梁,承载着操作系统和应用程序的实时运行,又叫运行时数据区 + +JVM 内存结构规定了 Java 在运行过程中内存申请、分配、管理的策略,保证了 JVM 的高效稳定运行 + +* Java1.8 以前的内存结构图: + ![](https://seazean.oss-cn-beijing.aliyuncs.com/img/Java/JVM-Java7内存结构图.png) + +* Java1.8 之后的内存结果图: + + ![](https://seazean.oss-cn-beijing.aliyuncs.com/img/Java/JVM-Java8内存结构图.png) + +线程运行诊断: + +* 定位:jps 定位进程 ID +* jstack 进程 ID:用于打印出给定的 Java 进程 ID 或 core file 或远程调试服务的 Java 堆栈信息 + +常见 OOM 错误: + +* java.lang.StackOverflowError +* java.lang.OutOfMemoryError:java heap space +* java.lang.OutOfMemoryError:GC overhead limit exceeded +* java.lang.OutOfMemoryError:Direct buffer memory +* java.lang.OutOfMemoryError:unable to create new native thread +* java.lang.OutOfMemoryError:Metaspace + + + +*** + + + +### JVM内存 + +#### 虚拟机栈 + +##### Java 栈 + +Java 虚拟机栈:Java Virtual Machine Stacks,**每个线程**运行时所需要的内存 + +* 每个方法被执行时,都会在虚拟机栈中创建一个栈帧 stack frame(**一个方法一个栈帧**) + +* Java 虚拟机规范允许 **Java 栈的大小是动态的或者是固定不变的** + +* 虚拟机栈是**每个线程私有的**,每个线程只能有一个活动栈帧,对应方法调用到执行完成的整个过程 + +* 每个栈由多个栈帧(Frame)组成,对应着每次方法调用时所占用的内存,每个栈帧中存储着: + + * 局部变量表:存储方法里的 Java 基本数据类型以及对象的引用 + * 动态链接:也叫指向运行时常量池的方法引用 + * 方法返回地址:方法正常退出或者异常退出的定义 + * 操作数栈或表达式栈和其他一些附加信息 + + + +设置栈内存大小:`-Xss size` `-Xss 1024k` + +* 在 JDK 1.4 中默认为 256K,而在 JDK 1.5+ 默认为 1M + +虚拟机栈特点: + +* 栈内存**不需要进行GC**,方法开始执行的时候会进栈,方法调用后自动弹栈,相当于清空了数据 + +* 栈内存分配越大越大,可用的线程数越少(内存越大,每个线程拥有的内存越大) + +* 方法内的局部变量是否**线程安全**: + * 如果方法内局部变量没有逃离方法的作用访问,它是线程安全的(逃逸分析) + * 如果是局部变量引用了对象,并逃离方法的作用范围,需要考虑线程安全 + +异常: + +* 栈帧过多导致栈内存溢出 (超过了栈的容量),会抛出 OutOfMemoryError 异常 +* 当线程请求的栈深度超过最大值,会抛出 StackOverflowError 异常 + + + +*** + + + +##### 局部变量 + +局部变量表也被称之为局部变量数组或本地变量表,本质上定义为一个数字数组,主要用于存储方法参数和定义在方法体内的局部变量 + +* 表是建立在线程的栈上,是线程私有的数据,因此不存在数据安全问题 +* 表的容量大小是在编译期确定的,保存在方法的 Code 属性的 maximum local variables 数据项中 +* 表中的变量只在当前方法调用中有效,方法结束栈帧销毁,局部变量表也会随之销毁 +* 表中的变量也是重要的垃圾回收根节点,只要被表中数据直接或间接引用的对象都不会被回收 + +局部变量表最基本的存储单元是 **slot(变量槽)**: + +* 参数值的存放总是在局部变量数组的 index0 开始,到数组长度 -1 的索引结束,JVM 为每一个 slot 都分配一个访问索引,通过索引即可访问到槽中的数据 +* 存放编译期可知的各种基本数据类型(8种),引用类型(reference),returnAddress 类型的变量 +* 32 位以内的类型只占一个 slot(包括 returnAddress 类型),64 位的类型(long 和 double)占两个 slot +* 局部变量表中的槽位是可以**重复利用**的,如果一个局部变量过了其作用域,那么之后申明的新的局部变量就可能会复用过期局部变量的槽位,从而达到节省资源的目的 + + + +*** + + + +##### 操作数栈 + +栈:可以使用数组或者链表来实现 + +操作数栈:在方法执行过程中,根据字节码指令,往栈中写入数据或提取数据,即入栈(push)或出栈(pop) + +* 保存计算过程的中间结果,同时作为计算过程中变量临时的存储空间,是执行引擎的一个工作区 + +* Java 虚拟机的解释引擎是基于栈的执行引擎,其中的栈指的就是操作数栈 +* 如果被调用的方法带有返回值的话,其**返回值将会被压入当前栈帧的操作数栈中** + +栈顶缓存技术 ToS(Top-of-Stack Cashing):将栈顶元素全部缓存在 CPU 的寄存器中,以此降低对内存的读/写次数,提升执行的效率 + +基于栈式架构的虚拟机使用的零地址指令更加紧凑,完成一项操作需要使用很多入栈和出栈指令,所以需要更多的指令分派(instruction dispatch)次数和内存读/写次数,由于操作数是存储在内存中的,因此频繁地执行内存读/写操作必然会影响执行速度,所以需要栈顶缓存技术 + + + +*** + + + +##### 动态链接 + +动态链接是指向运行时常量池的方法引用,涉及到栈操作已经是类加载完成,这个阶段的解析是**动态绑定** + +* 为了支持当前方法的代码能够实现动态链接,每一个栈帧内部都包含一个指向运行时常量池或该栈帧所属方法的引用 + + ![](https://seazean.oss-cn-beijing.aliyuncs.com/img/Java/JVM-动态链接符号引用.png) + +* 在 Java 源文件被编译成的字节码文件中,所有的变量和方法引用都作为符号引用保存在 class 的常量池中 + + 常量池的作用:提供一些符号和常量,便于指令的识别 + + ![](https://seazean.oss-cn-beijing.aliyuncs.com/img/Java/JVM-动态链接运行时常量池.png) + + + +*** + + + +##### 返回地址 + +Return Address:存放调用该方法的 PC 寄存器的值 + +方法的结束有两种方式:正常执行完成、出现未处理的异常,在方法退出后都返回到该方法被调用的位置 + +* 正常:调用者的 PC 计数器的值作为返回地址,即调用该方法的指令的**下一条指令的地址** +* 异常:返回地址是要通过异常表来确定 + +正常完成出口:执行引擎遇到任意一个方法返回的字节码指令(return),会有返回值传递给上层的方法调用者 + +异常完成出口:方法执行的过程中遇到了异常(Exception),并且这个异常没有在方法内进行处理,本方法的异常表中没有搜素到匹配的异常处理器,导致方法退出 + +两者区别:通过异常完成出口退出的不会给上层调用者产生任何的返回值 + + + +##### 附加信息 + +栈帧中还允许携带与 Java 虚拟机实现相关的一些附加信息,例如对程序调试提供支持的信息 + + + +*** + + + +#### 本地方法栈 + +本地方法栈是为虚拟机执行本地方法时提供服务的 + +JNI:Java Native Interface,通过使用 Java 本地接口程序,可以确保代码在不同的平台上方便移植 + +* 不需要进行 GC,与虚拟机栈类似,也是线程私有的,有 StackOverFlowError 和 OutOfMemoryError 异常 +* 虚拟机栈执行的是 Java 方法,在 HotSpot JVM 中,直接将本地方法栈和虚拟机栈合二为一 +* 本地方法一般是由其他语言编写,并且被编译为基于本机硬件和操作系统的程序 +* 当某个线程调用一个本地方法时,就进入了不再受虚拟机限制的世界,和虚拟机拥有同样的权限 + + * 本地方法可以通过本地方法接口来**访问虚拟机内部的运行时数据区** + * 直接从本地内存的堆中分配任意数量的内存 + * 可以直接使用本地处理器中的寄存器 + + +原理:将本地的 C 函数(如 foo)编译到一个共享库(foo.so)中,当正在运行的 Java 程序调用 foo 时,Java 解释器利用 dlopen 接口动态链接和加载 foo.so 后再调用该函数 + +* dlopen 函数:Linux 系统加载和链接共享库 +* dlclose 函数:卸载共享库 + + + + + +图片来源:https://github.com/CyC2018/CS-Notes/blob/master/notes/Java%20%E8%99%9A%E6%8B%9F%E6%9C%BA.md + + + +*** + + + +#### 程序计数器 + +Program Counter Register 程序计数器(寄存器) + +作用:内部保存字节码的行号,用于记录正在执行的字节码指令地址(如果正在执行的是本地方法则为空) + +原理: + +* JVM 对于多线程是通过线程轮流切换并且分配线程执行时间,一个处理器只会处理执行一个线程 +* 切换线程需要从程序计数器中来回去到当前的线程上一次执行的行号 + +特点: + +* 是线程私有的 +* **不会存在内存溢出**,是 JVM 规范中唯一一个不出现 OOM 的区域,所以这个空间不会进行 GC + +Java 反编译指令:`javap -v Test.class` + +#20:代表去 Constant pool 查看该地址的指令 + +```java +0: getstatic #20 // PrintStream out = System.out; +3: astore_1 // -- +4: aload_1 // out.println(1); +5: iconst_1 // -- +6: invokevirtual #26 // -- +9: aload_1 // out.println(2); +10: iconst_2 // -- +11: invokevirtual #26 // -- +``` + + + +**** + + + +#### 堆 + +Heap 堆:是 JVM 内存中最大的一块,由所有线程共享,由垃圾回收器管理的主要区域,堆中对象大部分都需要考虑线程安全的问题 + +存放哪些资源: + +* 对象实例:类初始化生成的对象,**基本数据类型的数组也是对象实例**,new 创建对象都使用堆内存 +* 字符串常量池: + * 字符串常量池原本存放于方法区,JDK7 开始放置于堆中 + * 字符串常量池**存储的是 String 对象的直接引用或者对象**,是一张 string table +* 静态变量:静态变量是有 static 修饰的变量,JDK8 时从方法区迁移至堆中 +* 线程分配缓冲区 Thread Local Allocation Buffer:线程私有但不影响堆的共性,可以提升对象分配的效率 + +设置堆内存指令:`-Xmx Size` + +内存溢出:new 出对象,循环添加字符数据,当堆中没有内存空间可分配给实例,也无法再扩展时,就会抛出 OutOfMemoryError 异常 + +堆内存诊断工具:(控制台命令) + +1. jps:查看当前系统中有哪些 Java 进程 +2. jmap:查看堆内存占用情况 `jhsdb jmap --heap --pid 进程id` +3. jconsole:图形界面的,多功能的监测工具,可以连续监测 + +在 Java7 中堆内会存在**年轻代、老年代和方法区(永久代)**: + +* Young 区被划分为三部分,Eden 区和两个大小严格相同的 Survivor 区。Survivor 区某一时刻只有其中一个是被使用的,另外一个留做垃圾回收时复制对象。在 Eden 区变满的时候,GC 就会将存活的对象移到空闲的 Survivor 区间中,根据 JVM 的策略,在经过几次垃圾回收后,仍然存活于 Survivor 的对象将被移动到 Tenured 区间 +* Tenured 区主要保存生命周期长的对象,一般是一些老的对象,当一些对象在 Young 复制转移一定的次数以后,对象就会被转移到 Tenured 区 +* Perm 代主要保存 Class、ClassLoader、静态变量、常量、编译后的代码,在 Java7 中堆内方法区会受到 GC 的管理 + +分代原因:不同对象的生命周期不同,70%-99% 的对象都是临时对象,优化 GC 性能 + +```java +public static void main(String[] args) { + // 返回Java虚拟机中的堆内存总量 + long initialMemory = Runtime.getRuntime().totalMemory() / 1024 / 1024; + // 返回Java虚拟机使用的最大堆内存量 + long maxMemory = Runtime.getRuntime().maxMemory() / 1024 / 1024; + + System.out.println("-Xms : " + initialMemory + "M");//-Xms : 245M + System.out.println("-Xmx : " + maxMemory + "M");//-Xmx : 3641M +} +``` + + + +*** + + + +#### 方法区 + +方法区:是各个线程共享的内存区域,用于存储已被虚拟机加载的类信息、常量、即时编译器编译后的代码等数据,虽然 Java 虚拟机规范把方法区描述为堆的一个逻辑部分,但是也叫 Non-Heap(非堆) + +方法区是一个 JVM 规范,**永久代与元空间都是其一种实现方式** + +方法区的大小不必是固定的,可以动态扩展,加载的类太多,可能导致永久代内存溢出 (OutOfMemoryError) + +方法区的 GC:针对常量池的回收及对类型的卸载,比较难实现 + +为了**避免方法区出现 OOM**,在 JDK8 中将堆内的方法区(永久代)移动到了本地内存上,重新开辟了一块空间,叫做元空间,元空间存储类的元信息,**静态变量和字符串常量池等放入堆中** + +类元信息:在类编译期间放入方法区,存放了类的基本信息,包括类的方法、参数、接口以及常量池表 + +常量池表(Constant Pool Table)是 Class 文件的一部分,存储了**类在编译期间生成的字面量、符号引用**,JVM 为每个已加载的类维护一个常量池 + +- 字面量:基本数据类型、字符串类型常量、声明为 final 的常量值等 +- 符号引用:类、字段、方法、接口等的符号引用 + +运行时常量池是方法区的一部分 + +* 常量池(编译器生成的字面量和符号引用)中的数据会在类加载的加载阶段放入运行时常量池 +* 类在解析阶段将这些符号引用替换成直接引用 +* 除了在编译期生成的常量,还允许动态生成,例如 String 类的 intern() + + + +*** + + + +### 本地内存 + +#### 基本介绍 + +虚拟机内存:Java 虚拟机在执行的时候会把管理的内存分配成不同的区域,受虚拟机内存大小的参数控制,当大小超过参数设置的大小时就会报 OOM + +本地内存:又叫做**堆外内存**,线程共享的区域,本地内存这块区域是不会受到 JVM 的控制的,不会发生 GC;因此对于整个 Java 的执行效率是提升非常大,但是如果内存的占用超出物理内存的大小,同样也会报 OOM + +本地内存概述图: + + + + + +*** + + + +#### 元空间 + +PermGen 被元空间代替,永久代的**类信息、方法、常量池**等都移动到元空间区 + +元空间与永久代区别:元空间不在虚拟机中,使用的本地内存,默认情况下,元空间的大小仅受本地内存限制 + +方法区内存溢出: + +* JDK1.8 以前会导致永久代内存溢出:java.lang.OutOfMemoryError: PerGen space + + ```sh + -XX:MaxPermSize=8m #参数设置 + ``` + +* JDK1.8 以后会导致元空间内存溢出:java.lang.OutOfMemoryError: Metaspace + + ```sh + -XX:MaxMetaspaceSize=8m #参数设置 + ``` + +元空间内存溢出演示: + +```java +public class Demo1_8 extends ClassLoader { // 可以用来加载类的二进制字节码 + public static void main(String[] args) { + int j = 0; + try { + Demo1_8 test = new Demo1_8(); + for (int i = 0; i < 10000; i++, j++) { + // ClassWriter 作用是生成类的二进制字节码 + ClassWriter cw = new ClassWriter(0); + // 版本号, public, 类名, 包名, 父类, 接口 + cw.visit(Opcodes.V1_8, Opcodes.ACC_PUBLIC, "Class" + i, null, "java/lang/Object", null); + // 返回 byte[] + byte[] code = cw.toByteArray(); + // 执行了类的加载 + test.defineClass("Class" + i, code, 0, code.length); // Class 对象 + } + } finally { + System.out.println(j); + } + } +} +``` + + + +*** + + + +#### 直接内存 + +直接内存是 Java 堆外、直接向系统申请的内存区间,不是虚拟机运行时数据区的一部分,也不是《Java 虚拟机规范》中定义的内存区域 + + + +直接内存详解参考:NET → NIO → 直接内存 + + + +*** + + + +### 变量位置 + +变量的位置不取决于它是基本数据类型还是引用数据类型,取决于它的**声明位置** + +静态内部类和其他内部类: + +* **一个 class 文件只能对应一个 public 类型的类**,这个类可以有内部类,但不会生成新的 class 文件 + +* 静态内部类属于类本身,加载到方法区,其他内部类属于内部类的属性,加载到堆(待考证) + +类变量: + +* 类变量是用 static 修饰符修饰,定义在方法外的变量,随着 Java 进程产生和销毁 +* 在 Java8 之前把静态变量存放于方法区,在 Java8 时存放在堆中的静态变量区 + + +实例变量: + +* 实例(成员)变量是定义在类中,没有 static 修饰的变量,随着类的实例产生和销毁,是类实例的一部分 +* 在类初始化的时候,从运行时常量池取出直接引用或者值,**与初始化的对象一起放入堆中** + +局部变量: + +* 局部变量是定义在类的方法中的变量 +* 在所在方法被调用时**放入虚拟机栈的栈帧**中,方法执行结束后从虚拟机栈中弹出, + +类常量池、运行时常量池、字符串常量池有什么关系?有什么区别? + +* 类常量池与运行时常量池都存储在方法区,而字符串常量池在 Jdk7 时就已经从方法区迁移到了 Java 堆中 +* 在类编译过程中,会把类元信息放到方法区,类元信息的其中一部分便是类常量池,主要存放字面量和符号引用,而字面量的一部分便是文本字符 +* **在类加载时将字面量和符号引用解析为直接引用存储在运行时常量池** +* 对于文本字符,会在解析时查找字符串常量池,查出这个文本字符对应的字符串对象的直接引用,将直接引用存储在运行时常量池 + +什么是字面量?什么是符号引用? + +* 字面量:java 代码在编译过程中是无法构建引用的,字面量就是在编译时对于数据的一种表示 + + ```java + int a = 1; //这个1便是字面量 + String b = "iloveu"; //iloveu便是字面量 + ``` + +* 符号引用:在编译过程中并不知道每个类的地址,因为可能这个类还没有加载,如果在一个类中引用了另一个类,无法知道它的内存地址,只能用它的类名作为符号引用,在类加载完后用这个符号引用去获取内存地址 + + + + +*** + + + + + +## 内存管理 + +### 内存分配 + +#### 两种方式 + +不分配内存的对象无法进行其他操作,JVM 为对象分配内存的过程:首先计算对象占用空间大小,接着在堆中划分一块内存给新对象 + +* 如果内存规整,使用指针碰撞(Bump The Pointer)。所有用过的内存在一边,空闲的内存在另外一边,中间有一个指针作为分界点的指示器,分配内存就仅仅是把指针向空闲那边挪动一段与对象大小相等的距离 +* 如果内存不规整,虚拟机需要维护一个空闲列表(Free List)分配。已使用的内存和未使用的内存相互交错,虚拟机维护了一个列表,记录上哪些内存块是可用的,再分配的时候从列表中找到一块足够大的空间划分给对象实例,并更新列表上的内容 + + + +*** + + + +#### TLAB + +TLAB:Thread Local Allocation Buffer,为每个线程在堆内单独分配了一个缓冲区,多线程分配内存时,使用 TLAB 可以避免线程安全问题,同时还能够提升内存分配的吞吐量,这种内存分配方式叫做**快速分配策略** + +- 栈上分配使用的是栈来进行对象内存的分配 +- TLAB 分配使用的是 Eden 区域进行内存分配,属于堆内存 + +堆区是线程共享区域,任何线程都可以访问到堆区中的共享数据,由于对象实例的创建在 JVM 中非常频繁,因此在并发环境下为避免多个线程操作同一地址,需要使用加锁等机制,进而影响分配速度 + +问题:堆空间都是共享的么? 不一定,因为还有 TLAB,在堆中划分出一块区域,为每个线程所独占 + +![](https://seazean.oss-cn-beijing.aliyuncs.com/img/Java/JVM-TLAB内存分配策略.jpg) + +JVM 是将 TLAB 作为内存分配的首选,但不是所有的对象实例都能够在 TLAB 中成功分配内存,一旦对象在 TLAB 空间分配内存失败时,JVM 就会通过**使用加锁机制确保数据操作的原子性**,从而直接在堆中分配内存 + +栈上分配优先于 TLAB 分配进行,逃逸分析中若可进行栈上分配优化,会优先进行对象栈上直接分配内存 + +参数设置: + +* `-XX:UseTLAB`:设置是否开启 TLAB 空间 + +* `-XX:TLABWasteTargetPercent`:设置 TLAB 空间所占用 Eden 空间的百分比大小,默认情况下 TLAB 空间的内存非常小,仅占有整个 Eden 空间的1% +* `-XX:TLABRefillWasteFraction`:指当 TLAB 空间不足,请求分配的对象内存大小超过此阈值时不会进行 TLAB 分配,直接进行堆内存分配,否则还是会优先进行 TLAB 分配 + +![](https://seazean.oss-cn-beijing.aliyuncs.com/img/Java/JVM-TLAB内存分配过程.jpg) + + + +*** + + + +#### 逃逸分析 + +即时编译(Just-in-time Compilation,JIT)是一种通过在运行时将字节码翻译为机器码,从而改善性能的技术,在 HotSpot 实现中有多种选择:C1、C2 和 C1+C2,分别对应 Client、Server 和分层编译 + +* C1 编译速度快,优化方式比较保守;C2 编译速度慢,优化方式比较激进 +* C1+C2 在开始阶段采用 C1 编译,当代码运行到一定热度之后采用 C2 重新编译 + +逃逸分析并不是直接的优化手段,而是一个代码分析方式,通过动态分析对象的作用域,为优化手段如栈上分配、标量替换和同步消除等提供依据,发生逃逸行为的情况有两种:方法逃逸和线程逃逸 + +* 方法逃逸:当一个对象在方法中定义之后,被外部方法引用 + * 全局逃逸:一个对象的作用范围逃出了当前方法或者当前线程,比如对象是一个静态变量、全局变量赋值、已经发生逃逸的对象、作为当前方法的返回值 + * 参数逃逸:一个对象被作为方法参数传递或者被参数引用 +* 线程逃逸:如类变量或实例变量,可能被其它线程访问到 + +如果不存在逃逸行为,则可以对该对象进行如下优化:同步消除、标量替换和栈上分配 + +* 同步消除 + + 线程同步本身比较耗时,如果确定一个对象不会逃逸出线程,不被其它线程访问到,那对象的读写就不会存在竞争,则可以消除对该对象的**同步锁**,通过 `-XX:+EliminateLocks` 可以开启同步消除 ( - 号关闭) + +* 标量替换 + + * 标量替换:如果把一个对象拆散,将其成员变量恢复到基本类型来访问 + * 标量 (scalar) :不可分割的量,如基本数据类型和 reference 类型 + + 聚合量 (Aggregate):一个数据可以继续分解,对象一般是聚合量 + * 如果逃逸分析发现一个对象不会被外部访问,并且该对象可以被拆散,那么经过优化之后,并不直接生成该对象,而是将该对象成员变量分解若干个被这个方法使用的成员变量所代替 + * 参数设置: + + * `-XX:+EliminateAllocations`:开启标量替换 + * `-XX:+PrintEliminateAllocations`:查看标量替换情况 + +* 栈上分配 + + JIT 编译器在编译期间根据逃逸分析的结果,如果一个对象没有逃逸出方法的话,就可能被优化成栈上分配。分配完成后,继续在调用栈内执行,最后线程结束,栈空间被回收,局部变量对象也被回收,这样就无需 GC + + User 对象的作用域局限在方法 fn 中,可以使用标量替换的优化手段在栈上分配对象的成员变量,这样就不会生成 User 对象,大大减轻 GC 的压力 + + ```java + public class JVM { + public static void main(String[] args) throws Exception { + int sum = 0; + int count = 1000000; + //warm up + for (int i = 0; i < count ; i++) { + sum += fn(i); + } + System.out.println(sum); + System.in.read(); + } + private static int fn(int age) { + User user = new User(age); + int i = user.getAge(); + return i; + } + } + + class User { + private final int age; + + public User(int age) { + this.age = age; + } + + public int getAge() { + return age; + } + } + ``` + + + + +*** + + + +#### 分代思想 + +##### 分代介绍 + +Java8 时,堆被分为了两份:新生代和老年代(1:2),在 Java7 时,还存在一个永久代 + +- 新生代使用:复制算法 +- 老年代使用:标记 - 清除 或者 标记 - 整理 算法 + +**Minor GC 和 Full GC**: + +- Minor GC:回收新生代,新生代对象存活时间很短,所以 Minor GC 会频繁执行,执行的速度比较快 +- Full GC:回收老年代和新生代,老年代对象其存活时间长,所以 Full GC 很少执行,执行速度会比 Minor GC 慢很多 + + Eden 和 Survivor 大小比例默认为 8:1:1 + + + + + + + +*** + + + +##### 分代分配 + +工作机制: + +* **对象优先在 Eden 分配**:当创建一个对象的时候,对象会被分配在新生代的 Eden 区,当 Eden 区要满了时候,触发 YoungGC +* 当进行 YoungGC 后,此时在 Eden 区存活的对象被移动到 to 区,并且当前对象的年龄会加 1,清空 Eden 区 +* 当再一次触发 YoungGC 的时候,会把 Eden 区中存活下来的对象和 to 中的对象,移动到 from 区中,这些对象的年龄会加 1,清空 Eden 区和 to 区 +* To 区永远是空 Survivor 区,From 区是有数据的,每次 MinorGC 后两个区域互换 +* From 区和 To 区 也可以叫做 S0 区和 S1 区 + +晋升到老年代: + +* **长期存活的对象进入老年代**:为对象定义年龄计数器,对象在 Eden 出生并经过 Minor GC 依然存活,将移动到 Survivor 中,年龄就增加 1 岁,增加到一定年龄则移动到老年代中 + + `-XX:MaxTenuringThreshold`:定义年龄的阈值,对象头中用 4 个 bit 存储,所以最大值是 15,默认也是 15 + +* **大对象直接进入老年代**:需要连续内存空间的对象,最典型的大对象是很长的字符串以及数组;避免在 Eden 和 Survivor 之间的大量复制;经常出现大对象会提前触发 GC 以获取足够的连续空间分配给大对象 + + `-XX:PretenureSizeThreshold`:大于此值的对象直接在老年代分配 + +* **动态对象年龄判定**:如果在 Survivor 区中相同年龄的对象的所有大小之和超过 Survivor 空间的一半,年龄大于等于该年龄的对象就可以直接进入老年代 + +空间分配担保: + +* 在发生 Minor GC 之前,虚拟机先检查老年代最大可用的连续空间是否大于新生代所有对象总空间,如果条件成立的话,那么 Minor GC 可以确认是安全的 +* 如果不成立,虚拟机会查看 HandlePromotionFailure 的值是否允许担保失败,如果允许那么就会继续检查老年代最大可用的连续空间是否大于历次晋升到老年代对象的平均大小,如果大于将尝试着进行一次 Minor GC;如果小于或者 HandlePromotionFailure 的值不允许冒险,那么就要进行一次 Full GC + + + + + +*** + + + +### 回收策略 + +#### 触发条件 + +内存垃圾回收机制主要集中的区域就是线程共享区域:**堆和方法区** + +Minor GC 触发条件非常简单,当 Eden 空间满时,就将触发一次 Minor GC + +FullGC 同时回收新生代、老年代和方法区,只会存在一个 FullGC 的线程进行执行,其他的线程全部会被**挂起**,有以下触发条件: + +* 调用 System.gc(): + + * 在默认情况下,通过 System.gc() 或 Runtime.getRuntime().gc() 的调用,会显式触发 FullGC,同时对老年代和新生代进行回收,但是虚拟机不一定真正去执行,无法保证对垃圾收集器的调用 + * 不建议使用这种方式,应该让虚拟机管理内存。一般情况下,垃圾回收应该是自动进行的,无须手动触发;在一些特殊情况下,如正在编写一个性能基准,可以在运行之间调用 System.gc() + +* 老年代空间不足: + + * 为了避免引起的 Full GC,应当尽量不要创建过大的对象以及数组 + * 通过 -Xmn 参数调整新生代的大小,让对象尽量在新生代被回收掉不进入老年代,可以通过 `-XX:MaxTenuringThreshold` 调大对象进入老年代的年龄,让对象在新生代多存活一段时间 + +* 空间分配担保失败 + +* JDK 1.7 及以前的永久代(方法区)空间不足 + +* Concurrent Mode Failure:执行 CMS GC 的过程中同时有对象要放入老年代,而此时老年代空间不足(可能是 GC 过程中浮动垃圾过多导致暂时性的空间不足),便会报 Concurrent Mode Failure 错误,并触发 Full GC + + +手动 GC 测试,VM参数:`-XX:+PrintGcDetails` + +```java +public void localvarGC1() { + byte[] buffer = new byte[10 * 1024 * 1024];//10MB + System.gc(); //输出: 不会被回收, FullGC时被放入老年代 +} + +public void localvarGC2() { + byte[] buffer = new byte[10 * 1024 * 1024]; + buffer = null; + System.gc(); //输出: 正常被回收 +} + public void localvarGC3() { + { + byte[] buffer = new byte[10 * 1024 * 1024]; + } + System.gc(); //输出: 不会被回收, FullGC时被放入老年代 + } + +public void localvarGC4() { + { + byte[] buffer = new byte[10 * 1024 * 1024]; + } + int value = 10; + System.gc(); //输出: 正常被回收,slot复用,局部变量过了其作用域 buffer置空 +} +``` + + + +*** + + + +#### 安全区域 + +安全点 (Safepoint):程序执行时并非在所有地方都能停顿下来开始 GC,只有在安全点才能停下 + +- Safe Point 的选择很重要,如果太少可能导致 GC 等待的时间太长,如果太多可能导致运行时的性能问题 +- 大部分指令的执行时间都非常短,通常会根据是否具有让程序长时间执行的特征为标准,选择些执行时间较长的指令作为 Safe Point, 如方法调用、循环跳转和异常跳转等 + +在 GC 发生时,让所有线程都在最近的安全点停顿下来的方法: + +- 抢先式中断:没有虚拟机采用,首先中断所有线程,如果有线程不在安全点,就恢复线程让线程运行到安全点 +- 主动式中断:设置一个中断标志,各个线程运行到各个 Safe Point 时就轮询这个标志,如果中断标志为真,则将自己进行中断挂起 + +问题:Safepoint 保证程序执行时,在不太长的时间内就会遇到可进入 GC 的 Safepoint,但是当线程处于 Waiting 状态或 Blocked 状态,线程无法响应 JVM 的中断请求,运行到安全点去中断挂起,JVM 也不可能等待线程被唤醒,对于这种情况,需要安全区域来解决 + +安全区域 (Safe Region):指在一段代码片段中,**对象的引用关系不会发生变化**,在这个区域中的任何位置开始 GC 都是安全的 + +运行流程: + +- 当线程运行到 Safe Region 的代码时,首先标识已经进入了 Safe Region,如果这段时间内发生 GC,JVM 会忽略标识为 Safe Region 状态的线程 + +- 当线程即将离开 Safe Region 时,会检查 JVM 是否已经完成 GC,如果完成了则继续运行,否则线程必须等待 GC 完成,收到可以安全离开 SafeRegion 的信号 + + + +*** + + + +### 垃圾判断 + +#### 垃圾介绍 + +垃圾:**如果一个或多个对象没有任何的引用指向它了,那么这个对象现在就是垃圾** + +作用:释放没用的对象,清除内存里的记录碎片,碎片整理将所占用的堆内存移到堆的一端,以便 JVM 将整理出的内存分配给新的对象 + +垃圾收集主要是针对堆和方法区进行,程序计数器、虚拟机栈和本地方法栈这三个区域属于线程私有的,只存在于线程的生命周期内,线程结束之后就会消失,因此不需要对这三个区域进行垃圾回收 + +在堆里存放着几乎所有的 Java 对象实例,在 GC 执行垃圾回收之前,首先需要区分出内存中哪些是存活对象,哪些是已经死亡的对象。只有被标记为己经死亡的对象,GC 才会在执行垃圾回收时,释放掉其所占用的内存空间,因此这个过程可以称为垃圾标记阶段,判断对象存活一般有两种方式:**引用计数算法**和**可达性分析算法** + + + +*** + + + +#### 引用计数法 + +引用计数算法(Reference Counting):对每个对象保存一个整型的引用计数器属性,用于记录对象被引用的情况。对于一个对象 A,只要有任何一个对象引用了 A,则 A 的引用计数器就加 1;当引用失效时,引用计数器就减 1;当对象 A 的引用计数器的值为 0,即表示对象A不可能再被使用,可进行回收(Java 没有采用) + +优点: + +- 回收没有延迟性,无需等到内存不够的时候才开始回收,运行时根据对象计数器是否为 0,可以直接回收 +- 在垃圾回收过程中,应用无需挂起;如果申请内存时,内存不足,则立刻报 OOM 错误 +- 区域性,更新对象的计数器时,只是影响到该对象,不会扫描全部对象 + +缺点: + +- 每次对象被引用时,都需要去更新计数器,有一点时间开销 + +- 浪费 CPU 资源,即使内存够用,仍然在运行时进行计数器的统计。 + +- **无法解决循环引用问题,会引发内存泄露**(最大的缺点) + + ```java + public class Test { + public Object instance = null; + public static void main(String[] args) { + Test a = new Test();// a = 1 + Test b = new Test();// b = 1 + a.instance = b; // b = 2 + b.instance = a; // a = 2 + a = null; // a = 1 + b = null; // b = 1 + } + } + ``` + +![](https://seazean.oss-cn-beijing.aliyuncs.com/img/Java/JVM-循环引用.png) + + + +*** + + + +#### 可达性分析 + +##### GC Roots + +可达性分析算法:也可以称为根搜索算法、追踪性垃圾收集 + +GC Roots 对象: + +- 虚拟机栈中局部变量表中引用的对象:各个线程被调用的方法中使用到的参数、局部变量等 +- 本地方法栈中引用的对象 +- 堆中类静态属性引用的对象 +- 方法区中的常量引用的对象 +- 字符串常量池(string Table)里的引用 +- 同步锁 synchronized 持有的对象 + +**GC Roots 是一组活跃的引用,不是对象**,放在 GC Roots Set 集合 + + + +*** + + + +##### 工作原理 + +可达性分析算法以根对象集合(GCRoots)为起始点,从上至下的方式搜索被根对象集合所连接的目标对象 + +分析工作必须在一个保障**一致性的快照**中进行,否则结果的准确性无法保证,这也是导致 GC 进行时必须 Stop The World 的一个原因 + +基本原理: + +- 可达性分析算法后,内存中的存活对象都会被根对象集合直接或间接连接着,搜索走过的路径称为引用链 + +- 如果目标对象没有任何引用链相连,则是不可达的,就意味着该对象己经死亡,可以标记为垃圾对象 + +- 在可达性分析算法中,只有能够被根对象集合直接或者间接连接的对象才是存活对象 + + + + + +*** + + + +##### 三色标记 + +###### 标记算法 + +三色标记法把遍历对象图过程中遇到的对象,标记成以下三种颜色: + +- 白色:尚未访问过 +- 灰色:本对象已访问过,但是本对象引用到的其他对象尚未全部访问 +- 黑色:本对象已访问过,而且本对象引用到的其他对象也全部访问完成 + +当 Stop The World (STW) 时,对象间的引用是不会发生变化的,可以轻松完成标记,遍历访问过程为: + +1. 初始时,所有对象都在白色集合 +2. 将 GC Roots 直接引用到的对象挪到灰色集合 +3. 从灰色集合中获取对象: + * 将本对象引用到的其他对象全部挪到灰色集合中 + * 将本对象挪到黑色集合里面 +4. 重复步骤 3,直至灰色集合为空时结束 +5. 结束后,仍在白色集合的对象即为 GC Roots 不可达,可以进行回收 + + + + + +参考文章:https://www.jianshu.com/p/12544c0ad5c1 + + + +**** + + + +###### 并发标记 + +并发标记时,对象间的引用可能发生变化,多标和漏标的情况就有可能发生 + +**多标情况:**当 E 变为灰色或黑色时,其他线程断开的 D 对 E 的引用,导致这部分对象仍会被标记为存活,本轮 GC 不会回收这部分内存,这部分本应该回收但是没有回收到的内存,被称之为**浮动垃圾** + +* 针对并发标记开始后的**新对象**,通常的做法是直接全部当成黑色,也算浮动垃圾 +* 浮动垃圾并不会影响应用程序的正确性,只是需要等到下一轮垃圾回收中才被清除 + + + +**漏标情况:** + +* 条件一:灰色对象断开了对一个白色对象的引用(直接或间接),即灰色对象原成员变量的引用发生了变化 +* 条件二:其他线程中修改了黑色对象,插入了一条或多条对该白色对象的新引用 +* 结果:导致该白色对象当作垃圾被 GC,影响到了程序的正确性 + + + +代码角度解释漏标: + +```java +Object G = objE.fieldG; // 读 +objE.fieldG = null; // 写 +objD.fieldG = G; // 写 +``` + +为了解决问题,可以操作上面三步,**将对象 G 记录起来,然后作为灰色对象再进行遍历**,比如放到一个特定的集合,等初始的 GC Roots 遍历完(并发标记),再遍历该集合(重新标记) + +> 所以**重新标记需要 STW**,应用程序一直在运行,该集合可能会一直增加新的对象,导致永远都运行不完 + +解决方法:添加读写屏障,读屏障拦截第一步,写屏障拦截第二三步,在读写前后进行一些后置处理: + +* **写屏障 + 增量更新**:黑色对象新增引用,会将黑色对象变成灰色对象,最后对该节点重新扫描 + + 增量更新 (Incremental Update) 破坏了条件二,从而保证了不会漏标 + + 缺点:对黑色变灰的对象重新扫描所有引用,比较耗费时间 + +* **写屏障 (Store Barrier) + SATB**:当原来成员变量的引用发生变化之前,记录下原来的引用对象 + + 保留 GC 开始时的对象图,即原始快照 SATB,当 GC Roots 确定后,对象图就已经确定,那后续的标记也应该是按照这个时刻的对象图走,如果期间对白色对象有了新的引用会记录下来,并且将白色对象变灰(说明可达了,并且原始快照中本来就应该是灰色对象),最后重新扫描该对象的引用关系 + + SATB (Snapshot At The Beginning) 破坏了条件一,从而保证了不会漏标 + +* **读屏障 (Load Barrier)**:破坏条件二,黑色对象引用白色对象的前提是获取到该对象,此时读屏障发挥作用 + +以 Java HotSpot VM 为例,其并发标记时对漏标的处理方案如下: + +- CMS:写屏障 + 增量更新 +- G1:写屏障 + SATB +- ZGC:读屏障 + + + +*** + + + +#### finalization + +Java 语言提供了对象终止(finalization)机制来允许开发人员提供对象被销毁之前的自定义处理逻辑 + +垃圾回收此对象之前,会先调用这个对象的 finalize() 方法,finalize() 方法允许在子类中被重写,用于在对象被回收时进行后置处理,通常在这个方法中进行一些资源释放和清理,比如关闭文件、套接字和数据库连接等 + +生存 OR 死亡:如果从所有的根节点都无法访问到某个对象,说明对象己经不再使用,此对象需要被回收。但事实上这时候它们暂时处于缓刑阶段。**一个无法触及的对象有可能在某个条件下复活自己**,所以虚拟机中的对象可能的三种状态: + +- 可触及的:从根节点开始,可以到达这个对象 +- 可复活的:对象的所有引用都被释放,但是对象有可能在 finalize() 中复活 +- 不可触及的:对象的 finalize() 被调用并且没有复活,那么就会进入不可触及状态,不可触及的对象不可能被复活,因为 **finalize() 只会被调用一次**,等到这个对象再被标记为可回收时就必须回收 + +永远不要主动调用某个对象的 finalize() 方法,应该交给垃圾回收机制调用,原因: + +* finalize() 时可能会导致对象复活 +* finalize() 方法的执行时间是没有保障的,完全由 GC 线程决定,极端情况下,若不发生 GC,则 finalize() 方法将没有执行机会,因为优先级比较低,即使主动调用该方法,也不会因此就直接进行回收 +* 一个糟糕的 finalize() 会严重影响 GC 的性能 + + + +*** + + + +#### 引用分析 + +无论是通过引用计数算法判断对象的引用数量,还是通过可达性分析算法判断对象是否可达,判定对象是否可被回收都与引用有关,Java 提供了四种强度不同的引用类型 + +1. 强引用:被强引用关联的对象不会被回收,只有所有 GCRoots 都不通过强引用引用该对象,才能被垃圾回收 + + * 强引用可以直接访问目标对象 + * 虚拟机宁愿抛出 OOM 异常,也不会回收强引用所指向对象 + * 强引用可能导致**内存泄漏** + + ```java + Object obj = new Object();//使用 new 一个新对象的方式来创建强引用 + ``` + +2. 软引用(SoftReference):被软引用关联的对象只有在内存不够的情况下才会被回收 + + * **仅(可能有强引用,一个对象可以被多个引用)**有软引用引用该对象时,在垃圾回收后,内存仍不足时会再次出发垃圾回收,回收软引用对象 + * 配合**引用队列来释放软引用自身**,在构造软引用时,可以指定一个引用队列,当软引用对象被回收时,就会加入指定的引用队列,通过这个队列可以跟踪对象的回收情况 + * 软引用通常用来实现内存敏感的缓存,比如高速缓存就有用到软引用;如果还有空闲内存,就可以暂时保留缓存,当内存不足时清理掉,这样就保证了使用缓存的同时不会耗尽内存 + + ```java + Object obj = new Object(); + SoftReference sf = new SoftReference(obj); + obj = null; // 使对象只被软引用关联 + ``` + +3. 弱引用(WeakReference):被弱引用关联的对象一定会被回收,只能存活到下一次垃圾回收发生之前 + + * 仅有弱引用引用该对象时,在垃圾回收时,无论内存是否充足,都会回收弱引用对象 + * 配合引用队列来释放弱引用自身 + * WeakHashMap 用来存储图片信息,可以在内存不足的时候及时回收,避免了 OOM + + ```java + Object obj = new Object(); + WeakReference wf = new WeakReference(obj); + obj = null; + ``` + +4. 虚引用(PhantomReference):也称为幽灵引用或者幻影引用,是所有引用类型中最弱的一个 + + * 一个对象是否有虚引用的存在,不会对其生存时间造成影响,也无法通过虚引用得到一个对象 + * 为对象设置虚引用的唯一目的是在于跟踪垃圾回收过程,能在这个对象被回收时收到一个系统通知 + * 必须配合引用队列使用,主要配合 ByteBuffer 使用,被引用对象回收时会将虚引用入队,由 Reference Handler 线程调用虚引用相关方法释放直接内存 + + ```java + Object obj = new Object(); + PhantomReference pf = new PhantomReference(obj, null); + obj = null; + ``` + +5. 终结器引用(finalization) + + + +*** + + + +#### 无用属性 + +##### 无用类 + +方法区主要回收的是无用的类 + +判定一个类是否是无用的类,需要同时满足下面 3 个条件: + +- 该类所有的实例都已经被回收,也就是 Java 堆中不存在该类的任何实例 +- 加载该类的 `ClassLoader` 已经被回收 +- 该类对应的 `java.lang.Class` 对象没有在任何地方被引用,无法在任何地方通过反射访问该类的方法 + +虚拟机可以对满足上述 3 个条件的无用类进行回收,这里说的**仅仅是可以**,而并不是和对象一样不使用了就会必然被回收 + + + +*** + + + +##### 废弃常量 + +在常量池中存在字符串 "abc",如果当前没有任何 String 对象引用该常量,说明常量 "abc" 是废弃常量,如果这时发生内存回收的话**而且有必要的话**(内存不够用),"abc" 就会被系统清理出常量池 + + + +*** + + + +##### 静态变量 + +类加载时(第一次访问),这个类中所有静态成员就会被加载到静态变量区,该区域的成员一旦创建,直到程序退出才会被回收 + +如果是静态引用类型的变量,静态变量区只存储一份对象的引用地址,真正的对象在堆内,如果要回收该对象可以设置引用为 null + + + +参考文章:https://blog.csdn.net/zhengzhb/article/details/7331354 + + + +*** + + + +### 回收算法 + +#### 复制算法 + +复制算法的核心就是,**将原有的内存空间一分为二,每次只用其中的一块**,在垃圾回收时,将正在使用的对象复制到另一个内存空间中,然后将该内存空间清理,交换两个内存的角色,完成垃圾的回收 + +应用场景:如果内存中的垃圾对象较多,需要复制的对象就较少,这种情况下适合使用该方式并且效率比较高,反之则不适合 + +![](https://seazean.oss-cn-beijing.aliyuncs.com/img/Java/JVM-复制算法.png) + +算法优点: + +- 没有标记和清除过程,实现简单,运行速度快 +- 复制过去以后保证空间的连续性,不会出现碎片问题 + +算法缺点: + +- 主要不足是**只使用了内存的一半** +- 对于 G1 这种分拆成为大量 region 的 GC,复制而不是移动,意味着 GC 需要维护 region 之间对象引用关系,不管是内存占用或者时间开销都不小 + +现在的商业虚拟机都采用这种收集算法**回收新生代**,因为新生代 GC 频繁并且对象的存活率不高,但是并不是划分为大小相等的两块,而是一块较大的 Eden 空间和两块较小的 Survivor 空间 + + + +*** + + + +#### 标记清除 + +标记清除算法,是将垃圾回收分为两个阶段,分别是**标记和清除** + +- **标记**:Collector 从引用根节点开始遍历,标记所有被引用的对象,一般是在对象的 Header 中记录为可达对象,**标记的是引用的对象,不是垃圾** +- **清除**:Collector 对堆内存从头到尾进行线性的遍历,如果发现某个对象在其 Header 中没有标记为可达对象,则将其回收,把分块连接到**空闲列表**的单向链表,判断回收后的分块与前一个空闲分块是否连续,若连续会合并这两个分块,之后进行分配时只需要遍历这个空闲列表,就可以找到分块 + +- **分配阶段**:程序会搜索空闲链表寻找空间大于等于新对象大小 size 的块 block,如果找到的块等于 size,会直接返回这个分块;如果找到的块大于 size,会将块分割成大小为 size 与 block - size 的两部分,返回大小为 size 的分块,并把大小为 block - size 的块返回给空闲列表 + +算法缺点: + +- 标记和清除过程效率都不高 +- 会产生大量不连续的内存碎片,导致无法给大对象分配内存,需要维护一个空闲链表 + + + + + +*** + + + +#### 标记整理 + +标记整理(压缩)算法是在标记清除算法的基础之上,做了优化改进的算法 + +标记阶段和标记清除算法一样,也是从根节点开始,对对象的引用进行标记,在清理阶段,并不是简单的直接清理可回收对象,而是**将存活对象都向内存另一端移动**,然后清理边界以外的垃圾,从而**解决了碎片化**的问题 + +优点:不会产生内存碎片 + +缺点:需要移动大量对象,处理效率比较低 + + + +| | Mark-Sweep | Mark-Compact | Copying | +| -------- | ------------------ | ---------------- | --------------------------------------- | +| 速度 | 中等 | 最慢 | 最快 | +| 空间开销 | 少(但会堆积碎片) | 少(不堆积碎片) | 通常需要活对象的 2 倍大小(不堆积碎片) | +| 移动对象 | 否 | 是 | 是 | + + + + + +*** + + + +### 垃圾回收器 + +#### 概述 + +垃圾收集器分类: + +* 按线程数分(垃圾回收线程数),可以分为串行垃圾回收器和并行垃圾回收器 + * 除了 CMS 和 G1 之外,其它垃圾收集器都是以串行的方式执行 +* 按照工作模式分,可以分为并发式垃圾回收器和独占式垃圾回收器 + * 并发式垃圾回收器与应用程序线程交替工作,以尽可能减少应用程序的停顿时间 + * 独占式垃圾回收器(Stop the world)一旦运行,就停止应用程序中的所有用户线程,直到垃圾回收过程完全结束 +* 按碎片处理方式分,可分为压缩式垃圾回收器和非压缩式垃圾回收器 + * 压缩式垃圾回收器在回收完成后进行压缩整理,消除回收后的碎片,再分配对象空间使用指针碰撞 + * 非压缩式的垃圾回收器不进行这步操作,再分配对象空间使用空闲列表 +* 按工作的内存区间分,又可分为年轻代垃圾回收器和老年代垃圾回收器 + +GC 性能指标: + +- **吞吐量**:程序的运行时间占总运行时间的比例(总运行时间 = 程序的运行时间 + 内存回收的时间) +- 垃圾收集开销:吞吐量的补数,垃圾收集所用时间与总运行时间的比例 +- 暂停时间:执行垃圾收集时,程序的工作线程被暂停的时间 +- 收集频率:相对于应用程序的执行,收集操作发生的频率 +- 内存占用:Java 堆区所占的内存大小 +- 快速:一个对象从诞生到被回收所经历的时间 + +**垃圾收集器的组合关系**: + +![](https://seazean.oss-cn-beijing.aliyuncs.com/img/Java/JVM-垃圾回收器关系图.png) + +新生代收集器:Serial、ParNew、Parallel Scavenge + +老年代收集器:Serial old、Parallel old、CMS + +整堆收集器:G1 + +* 红色虚线在 JDK9 移除、绿色虚线在 JDK14 弃用该组合、青色虚线在 JDK14 删除 CMS 垃圾回收器 + +查看默认的垃圾收回收器: + +* `-XX:+PrintcommandLineFlags`:查看命令行相关参数(包含使用的垃圾收集器) + +* 使用命令行指令:jinfo -flag 相关垃圾回收器参数 进程 ID + + + +*** + + + +#### Serial + +Serial:串行垃圾收集器,作用于新生代,是指使用单线程进行垃圾回收,采用**复制算法**,新生代基本都是复制算法 + +**STW(Stop-The-World)**:垃圾回收时,只有一个线程在工作,并且 Java 应用中的所有线程都要暂停,等待垃圾回收的完成 + +**Serial old**:执行老年代垃圾回收的串行收集器,内存回收算法使用的是**标记-整理算法**,同样也采用了串行回收和 STW 机制 + +- Serial old 是 Client 模式下默认的老年代的垃圾回收器 +- Serial old 在 Server 模式下主要有两个用途: + - 在 JDK 1.5 以及之前版本(Parallel Old 诞生以前)中与 Parallel Scavenge 收集器搭配使用 + - 作为老年代 CMS 收集器的**后备垃圾回收方案**,在并发收集发生 Concurrent Mode Failure 时使用 + +开启参数:`-XX:+UseSerialGC` 等价于新生代用 Serial GC 且老年代用 Serial old GC + +![](https://seazean.oss-cn-beijing.aliyuncs.com/img/Java/JVM-Serial收集器.png) + +优点:简单而高效(与其他收集器的单线程比),对于限定单个 CPU 的环境来说,Serial 收集器由于没有线程交互的开销,可以获得最高的单线程收集效率 + +缺点:对于交互性较强的应用而言,这种垃圾收集器是不能够接受的,比如 JavaWeb 应用 + + + +**** + + + +#### ParNew + +Par 是 Parallel 并行的缩写,New 是只能处理的是新生代 + +并行垃圾收集器在串行垃圾收集器的基础之上做了改进,**采用复制算法**,将单线程改为了多线程进行垃圾回收,可以缩短垃圾回收的时间 + +对于其他的行为(收集算法、stop the world、对象分配规则、回收策略等)同 Serial 收集器一样,应用在年轻代,除 Serial 外,只有**ParNew GC 能与 CMS 收集器配合工作** + +相关参数: + +* `-XX:+UseParNewGC`:表示年轻代使用并行收集器,不影响老年代 + +* `-XX:ParallelGCThreads`:默认开启和 CPU 数量相同的线程数 + +![](https://seazean.oss-cn-beijing.aliyuncs.com/img/Java/JVM-ParNew收集器.png) + +ParNew 是很多 JVM 运行在 Server 模式下新生代的默认垃圾收集器 + +- 对于新生代,回收次数频繁,使用并行方式高效 +- 对于老年代,回收次数少,使用串行方式节省资源(CPU 并行需要切换线程,串行可以省去切换线程的资源) + + + +*** + + + +#### Parallel + +Parallel Scavenge 收集器是应用于新生代的并行垃圾回收器,**采用复制算法**、并行回收和 Stop the World 机制 + +Parallel Old 收集器:是一个应用于老年代的并行垃圾回收器,**采用标记-整理算法** + +对比其他回收器: + +* 其它收集器目标是尽可能缩短垃圾收集时用户线程的停顿时间 +* Parallel 目标是达到一个可控制的吞吐量,被称为**吞吐量优先**收集器 +* Parallel Scavenge 对比 ParNew 拥有**自适应调节策略**,可以通过一个开关参数打开 GC Ergonomics + +应用场景: + +* 停顿时间越短就越适合需要与用户交互的程序,良好的响应速度能提升用户体验 +* 高吞吐量可以高效率地利用 CPU 时间,尽快完成程序的运算任务,适合在后台运算而不需要太多交互 + +停顿时间和吞吐量的关系:新生代空间变小 → 缩短停顿时间 → 垃圾回收变得频繁 → 导致吞吐量下降 + +在注重吞吐量及 CPU 资源敏感的场合,都可以优先考虑 Parallel Scavenge + Parallel Old 收集器,在 Server 模式下的内存回收性能很好,**Java8 默认是此垃圾收集器组合** + +![](https://seazean.oss-cn-beijing.aliyuncs.com/img/Java/JVM-ParallelScavenge收集器.png) + +参数配置: + +* `-XX:+UseParallelGC`:手动指定年轻代使用 Paralle 并行收集器执行内存回收任务 +* `-XX:+UseParalleloldcc`:手动指定老年代使用并行回收收集器执行内存回收任务 + * 上面两个参数,默认开启一个,另一个也会被开启(互相激活),默认 JDK8 是开启的 +* `-XX:+UseAdaptivesizepplicy`:设置 Parallel Scavenge 收集器具有**自适应调节策略**,在这种模式下,年轻代的大小、Eden 和 Survivor 的比例、晋升老年代的对象年龄等参数会被自动调整,虚拟机会根据当前系统的运行情况收集性能监控信息,动态调整这些参数以提供最合适的停顿时间或者最大的吞吐量 +* `-XX:ParallelGcrhreads`:设置年轻代并行收集器的线程数,一般与 CPU 数量相等,以避免过多的线程数影响垃圾收集性能 + * 在默认情况下,当 CPU 数量小于 8 个,ParallelGcThreads 的值等于 CPU 数量 + * 当 CPU 数量大于 8 个,ParallelGCThreads 的值等于 3+[5*CPU Count]/8] +* `-XX:MaxGCPauseMillis`:设置垃圾收集器最大停顿时间(即 STW 的时间),单位是毫秒 + * 对于用户来讲,停顿时间越短体验越好;在服务器端,注重高并发,整体的吞吐量 + * 为了把停顿时间控制在 MaxGCPauseMillis 以内,收集器在工作时会调整 Java 堆大小或其他一些参数 +* `-XX:GCTimeRatio`:垃圾收集时间占总时间的比例 =1/(N+1),用于衡量吞吐量的大小 + * 取值范围(0,100)。默认值 99,也就是垃圾回收时间不超过 1 + * 与 `-xx:MaxGCPauseMillis` 参数有一定矛盾性,暂停时间越长,Radio 参数就容易超过设定的比例 + + + + + +**** + + + +#### CMS + +CMS 全称 Concurrent Mark Sweep,是一款**并发的、使用标记-清除**算法、针对老年代的垃圾回收器,其最大特点是**让垃圾收集线程与用户线程同时工作** + +CMS 收集器的关注点是尽可能缩短垃圾收集时用户线程的停顿时间,停顿时间越短(**低延迟**)越适合与用户交互的程序,良好的响应速度能提升用户体验 + +分为以下四个流程: + +- 初始标记:使用 STW 出现短暂停顿,仅标记一下 GC Roots 能直接关联到的对象,速度很快 +- 并发标记:进行 GC Roots 开始遍历整个对象图,在整个回收过程中耗时最长,不需要 STW,可以与用户线程并发运行 +- 重新标记:修正并发标记期间因用户程序继续运作而导致标记产生变动的那一部分对象,比初始标记时间长但远比并发标记时间短,需要 STW(不停顿就会一直变化,采用写屏障 + 增量更新来避免漏标情况) +- 并发清除:清除标记为可以回收对象,**不需要移动存活对象**,所以这个阶段可以与用户线程同时并发的 + +Mark Sweep 会造成内存碎片,不把算法换成 Mark Compact 的原因:Mark Compact 算法会整理内存,导致用户线程使用的**对象的地址改变**,影响用户线程继续执行 + +在整个过程中耗时最长的并发标记和并发清除过程中,收集器线程都可以与用户线程一起工作,不需要进行停顿 + +![](https://seazean.oss-cn-beijing.aliyuncs.com/img/Java/JVM-CMS收集器.png) + +优点:并发收集、低延迟 + +缺点: + +- 吞吐量降低:在并发阶段虽然不会导致用户停顿,但是会因为占用了一部分线程而导致应用程序变慢,CPU 利用率不够高 +- CMS 收集器**无法处理浮动垃圾**,可能出现 Concurrent Mode Failure 导致另一次 Full GC 的产生 + + 浮动垃圾是指并发清除阶段由于用户线程继续运行而产生的垃圾(产生了新对象),这部分垃圾只能到下一次 GC 时才能进行回收。由于浮动垃圾的存在,CMS 收集需要预留出一部分内存,不能等待老年代快满的时候再回收。如果预留的内存不够存放浮动垃圾,就会出现 Concurrent Mode Failure,这时虚拟机将临时启用 Serial Old 来替代 CMS,导致很长的停顿时间 +- 标记 - 清除算法导致的空间碎片,往往出现老年代空间无法找到足够大连续空间来分配当前对象,不得不提前触发一次 Full GC;为新对象分配内存空间时,将无法使用指针碰撞(Bump the Pointer)技术,而只能够选择空闲列表(Free List)执行内存分配 + +参数设置: + +* `-XX:+UseConcMarkSweepGC`:手动指定使用 CMS 收集器执行内存回收任务 + + 开启该参数后会自动将 `-XX:+UseParNewGC` 打开,即:ParNew + CMS + Serial old的组合 + +* `-XX:CMSInitiatingoccupanyFraction`:设置堆内存使用率的阈值,一旦达到该阈值,便开始进行回收 + + * JDK5 及以前版本的默认值为 68,即当老年代的空间使用率达到 68% 时,会执行一次CMS回收 + * JDK6 及以上版本默认值为 92% + +* `-XX:+UseCMSCompactAtFullCollection`:用于指定在执行完 Full GC 后对内存空间进行压缩整理,以此避免内存碎片的产生,由于内存压缩整理过程无法并发执行,所带来的问题就是停顿时间变得更长 + +* `-XX:CMSFullGCsBeforecompaction`:**设置在执行多少次 Full GC 后对内存空间进行压缩整理** + +* `-XX:ParallelCMSThreads`:设置 CMS 的线程数量 + + * CMS 默认启动的线程数是 (ParallelGCThreads+3)/4,ParallelGCThreads 是年轻代并行收集器的线程数 + * 收集线程占用的 CPU 资源多于25%,对用户程序影响可能较大;当 CPU 资源比较紧张时,受到 CMS 收集器线程的影响,应用程序的性能在垃圾回收阶段可能会非常糟糕 + + + +*** + + + +#### G1 + +##### G1 特点 + +G1(Garbage-First)是一款面向服务端应用的垃圾收集器,**应用于新生代和老年代**、采用标记-整理算法、软实时、低延迟、可设定目标(最大 STW 停顿时间)的垃圾回收器,用于代替 CMS,适用于较大的堆(>4 ~ 6G),在 JDK9 之后默认使用 G1 + +G1 对比其他处理器的优点: + +* 并发与并行: + * 并行性:G1 在回收期间,可以有多个 GC 线程同时工作,有效利用多核计算能力,此时用户线程 STW + * 并发性:G1 拥有与应用程序交替执行的能力,部分工作可以和应用程序同时执行,因此不会在整个回收阶段发生完全阻塞应用程序的情况 + * 其他的垃圾收集器使用内置的 JVM 线程执行 GC 的多线程操作,而 G1 GC 可以采用应用线程承担后台运行的 GC 工作,JVM 的 GC 线程处理速度慢时,系统会**调用应用程序线程加速垃圾回收**过程 + +* **分区算法**: + * 从分代上看,G1 属于分代型垃圾回收器,区分年轻代和老年代,年轻代依然有 Eden 区和 Survivor 区。从堆结构上看,**新生代和老年代不再物理隔离**,不用担心每个代内存是否足够,这种特性有利于程序长时间运行,分配大对象时不会因为无法找到连续内存空间而提前触发下一次 GC + * 将整个堆划分成约 2048 个大小相同的独立 Region 块,每个 Region 块大小根据堆空间的实际大小而定,整体被控制在 1MB 到 32 MB之间且为 2 的 N 次幂,所有 Region 大小相同,在 JVM 生命周期内不会被改变。G1 把堆划分成多个大小相等的独立区域,使得每个小空间可以单独进行垃圾回收 + * **新的区域 Humongous**:本身属于老年代区,当出现了一个巨型对象超出了分区容量的一半,该对象就会进入到该区域。如果一个 H 区装不下一个巨型对象,那么 G1 会寻找连续的 H 分区来存储,为了能找到连续的 H 区,有时候不得不启动 Full GC + * G1 不会对巨型对象进行拷贝,回收时被优先考虑,G1 会跟踪老年代所有 incoming 引用,这样老年代 incoming 引用为 0 的巨型对象就可以在新生代垃圾回收时处理掉 + + * Region 结构图: + + +![](https://seazean.oss-cn-beijing.aliyuncs.com/img/Java/JVM-G1-Region区域.png) + +- 空间整合: + + - CMS:标记-清除算法、内存碎片、若干次 GC 后进行一次碎片整理 + - G1:整体来看是**基于标记 - 整理算法实现**的收集器,从局部(Region 之间)上来看是基于复制算法实现的,两种算法都可以避免内存碎片 + +- **可预测的停顿时间模型(软实时 soft real-time)**:可以指定在 M 毫秒的时间片段内,消耗在 GC 上的时间不得超过 N 毫秒 + + - 由于分块的原因,G1 可以只选取部分区域进行内存回收,这样缩小了回收的范围,对于全局停顿情况也能得到较好的控制 + - G1 跟踪各个 Region 里面的垃圾堆积的价值大小(回收所获得的空间大小以及回收所需时间,通过过去回收的经验获得),在后台维护一个**优先列表**,每次根据允许的收集时间优先回收价值最大的 Region,保证了 G1 收集器在有限的时间内可以获取尽可能高的收集效率 + + * 相比于 CMS GC,G1 未必能做到 CMS 在最好情况下的延时停顿,但是最差情况要好很多 + +G1 垃圾收集器的缺点: + +* 相较于 CMS,G1 还不具备全方位、压倒性优势。比如在用户程序运行过程中,G1 无论是为了垃圾收集产生的内存占用还是程序运行时的额外执行负载都要比 CMS 要高 +* 从经验上来说,在小内存应用上 CMS 的表现大概率会优于 G1,而 G1 在大内存应用上则发挥其优势,平衡点在 6-8GB 之间 + +应用场景: + +* 面向服务端应用,针对具有大内存、多处理器的机器 +* 需要低 GC 延迟,并具有大堆的应用程序提供解决方案 + + + +*** + + + +##### 记忆集 + +记忆集 Remembered Set 在新生代中,每个 Region 都有一个 Remembered Set,用来被哪些其他 Region 里的对象引用(谁引用了我就记录谁) + + + +* 程序对 Reference 类型数据写操作时,产生一个 Write Barrier 暂时中断操作,检查该对象和 Reference 类型数据是否在不同的 Region(跨代引用),不同就将相关引用信息记录到 Reference 类型所属的 Region 的 Remembered Set 之中 +* 进行内存回收时,在 GC 根节点的枚举范围中加入 Remembered Set 即可保证不对全堆扫描也不会有遗漏 + +垃圾收集器在新生代中建立了记忆集这样的数据结构,可以理解为它是一个抽象类,具体实现记忆集的三种方式: + +* 字长精度 +* 对象精度 +* 卡精度(卡表) + +卡表(Card Table)在老年代中,是一种对记忆集的具体实现,主要定义了记忆集的记录精度、与堆内存的映射关系等,卡表中的每一个元素都对应着一块特定大小的内存块,这个内存块称之为卡页(card page),当存在跨代引用时,会将卡页标记为 dirty,JVM 对于卡页的维护也是通过写屏障的方式 + +收集集合 CSet 代表每次 GC 暂停时回收的一系列目标分区,在任意一次收集暂停中,CSet 所有分区都会被释放,内部存活的对象都会被转移到分配的空闲分区中。年轻代收集 CSet 只容纳年轻代分区,而混合收集会通过启发式算法,在老年代候选回收分区中,筛选出回收收益最高的分区添加到 CSet 中 + +* CSet of Young Collection +* CSet of Mix Collection + + + +*** + + + +##### 工作原理 + +G1 中提供了三种垃圾回收模式:YoungGC、Mixed GC 和 Full GC,在不同的条件下被触发 + +* 当堆内存使用达到一定值(默认 45%)时,开始老年代并发标记过程 +* 标记完成马上开始混合回收过程 + + + +顺时针:Young GC → Young GC + Concurrent Mark → Mixed GC 顺序,进行垃圾回收 + +* **Young GC**:发生在年轻代的 GC 算法,一般对象(除了巨型对象)都是在 eden region 中分配内存,当所有 eden region 被耗尽无法申请内存时,就会触发一次 Young GC,G1 停止应用程序的执行 STW,把活跃对象放入老年代,垃圾对象回收 + + **回收过程**: + + 1. 扫描根:根引用连同 RSet 记录的外部引用作为扫描存活对象的入口 + 2. 更新 RSet:处理 dirty card queue 更新 RS,此后 RSet 准确的反映对象的引用关系 + * dirty card queue:类似缓存,产生了引用先记录在这里,然后更新到 RSet + * 作用:产生引用直接更新 RSet 需要线程同步开销很大,使用队列性能好 + 3. 处理 RSet:识别被老年代对象指向的 Eden 中的对象,这些被指向的对象被认为是存活的对象,把需要回收的分区放入 Young CSet 中进行回收 + 4. 复制对象:Eden 区内存段中存活的对象会被复制到 survivor 区,survivor 区内存段中存活的对象如果年龄未达阈值,年龄会加1,达到阀值会被会被复制到 old 区中空的内存分段,如果 survivor 空间不够,Eden 空间的部分数据会直接晋升到老年代空间 + 5. 处理引用:处理 Soft,Weak,Phantom,JNI Weak 等引用,最终 Eden 空间的数据为空,GC 停止工作 + +* **Concurrent Mark **: + + * 初始标记:标记从根节点直接可达的对象,这个阶段是 STW 的,并且会触发一次年轻代 GC + * 并发标记 (Concurrent Marking):在整个堆中进行并发标记(应用程序并发执行),可能被 YoungGC 中断。会计算每个区域的对象活性,即区域中存活对象的比例,若区域中的所有对象都是垃圾,则这个区域会被立即回收(**实时回收**),给浮动垃圾准备出更多的空间,把需要收集的 Region 放入 CSet 当中 + * 最终标记:为了修正在并发标记期间因用户程序继续运作而导致标记产生变动的那一部分标记记录,虚拟机将这段时间对象变化记录在线程的 Remembered Set Logs 里面,最终标记阶段需要把 Remembered Set Logs 的数据合并到 Remembered Set 中,这阶段需要停顿线程,但是可并行执行(**防止漏标**) + * 筛选回收:并发清理阶段,首先对 CSet 中各个 Region 中的回收价值和成本进行排序,根据用户所期望的 GC 停顿时间来制定回收计划,也需要 STW + + ![](https://seazean.oss-cn-beijing.aliyuncs.com/img/Java/JVM-G1收集器.jpg) + +* **Mixed GC**:当很多对象晋升到老年代时,为了避免堆内存被耗尽,虚拟机会触发一个混合的垃圾收集器,即 Mixed GC,除了回收整个 young region,还会回收一部分的 old region,过程同 YGC + + 注意:**是一部分老年代,而不是全部老年代**,可以选择哪些老年代 region 收集,对垃圾回收的时间进行控制 + + 在 G1 中,Mixed GC 可以通过 `-XX:InitiatingHeapOccupancyPercent` 设置阈值 + +* **Full GC**:对象内存分配速度过快,Mixed GC 来不及回收,导致老年代被填满,就会触发一次 Full GC,G1 的 Full GC 算法就是单线程执行的垃圾回收,会导致异常长时间的暂停时间,需要进行不断的调优,尽可能的避免 Full GC + + 产生 Full GC 的原因: + + * 晋升时没有足够的空间存放晋升的对象 + * 并发处理过程完成之前空间耗尽,浮动垃圾 + + + +*** + + + +##### 相关参数 + +- `-XX:+UseG1GC`:手动指定使用 G1 垃圾收集器执行内存回收任务 +- `-XX:G1HeapRegionSize`:设置每个 Region 的大小。值是 2 的幂,范围是 1MB 到 32MB 之间,目标是根据最小的 Java 堆大小划分出约 2048 个区域,默认是堆内存的 1/2000 +- `-XX:MaxGCPauseMillis`:设置期望达到的最大 GC 停顿时间指标,JVM会尽力实现,但不保证达到,默认值是 200ms +- `-XX:+ParallelGcThread`:设置 STW 时 GC 线程数的值,最多设置为 8 +- `-XX:ConcGCThreads`:设置并发标记线程数,设置为并行垃圾回收线程数 ParallelGcThreads 的1/4左右 +- `-XX:InitiatingHeapoccupancyPercent`:设置触发并发 Mixed GC 周期的 Java 堆占用率阈值,超过此值,就触发 GC,默认值是 45 +- `-XX:+ClassUnloadingWithConcurrentMark`:并发标记类卸载,默认启用,所有对象都经过并发标记后,就可以知道哪些类不再被使用,当一个类加载器的所有类都不再使用,则卸载它所加载的所有类 +- `-XX:G1NewSizePercent`:新生代占用整个堆内存的最小百分比(默认5%) +- `-XX:G1MaxNewSizePercent`:新生代占用整个堆内存的最大百分比(默认60%) +- `-XX:G1ReservePercent=10`:保留内存区域,防止 to space(Survivor中的 to 区)溢出 + + + +*** + + + +##### 调优 + +G1 的设计原则就是简化 JVM 性能调优,只需要简单的三步即可完成调优: + +1. 开启 G1 垃圾收集器 +2. 设置堆的最大内存 +3. 设置最大的停顿时间(STW) + +不断调优暂停时间指标: + +* `XX:MaxGCPauseMillis=x` 可以设置启动应用程序暂停的时间,G1会根据这个参数选择 CSet 来满足响应时间的设置 +* 设置到 100ms 或者 200ms 都可以(不同情况下会不一样),但设置成50ms就不太合理 +* 暂停时间设置的太短,就会导致出现 G1 跟不上垃圾产生的速度,最终退化成 Full GC +* 对这个参数的调优是一个持续的过程,逐步调整到最佳状态 + +不要设置新生代和老年代的大小: + +- 避免使用 -Xmn 或 -XX:NewRatio 等相关选项显式设置年轻代大小,G1 收集器在运行的时候会调整新生代和老年代的大小,从而达到我们为收集器设置的暂停时间目标 +- 设置了新生代大小相当于放弃了 G1 的自动调优,我们只需要设置整个堆内存的大小,剩下的交给 G1 自己去分配各个代的大小 + + + +*** + + + +#### ZGC + +ZGC 收集器是一个可伸缩的、低延迟的垃圾收集器,基于 Region 内存布局的,不设分代,使用了读屏障、染色指针和内存多重映射等技术来实现**可并发的标记压缩算法** + +* 在 CMS 和 G1 中都用到了写屏障,而 ZGC 用到了读屏障 +* 染色指针:直接**将少量额外的信息存储在指针上的技术**,从 64 位的指针中拿高 4 位来标识对象此时的状态 + * 染色指针可以使某个 Region 的存活对象被移走之后,这个 Region 立即就能够被释放和重用 + * 可以直接从指针中看到引用对象的三色标记状态(Marked0、Marked1)、是否进入了重分配集、是否被移动过(Remapped)、是否只能通过 finalize() 方法才能被访问到(Finalizable) + * 可以大幅减少在垃圾收集过程中内存屏障的使用数量,写屏障的目的通常是为了记录对象引用的变动情况,如果将这些信息直接维护在指针中,显然就可以省去一些专门的记录操作 + * 可以作为一种可扩展的存储结构用来记录更多与对象标记、重定位过程相关的数据 +* 内存多重映射:多个虚拟地址指向同一个物理地址 + +可并发的标记压缩算法:染色指针标识对象是否被标记或移动,读屏障保证在每次应用程序或 GC 程序访问对象时先根据染色指针的标识判断是否被移动,如果被移动就根据转发表访问新的移动对象,**并更新引用**,不会像 G1 一样必须等待垃圾回收完成才能访问 + +ZGC 目标: + +- 停顿时间不会超过 10ms +- 停顿时间不会随着堆的增大而增大(不管多大的堆都能保持在 10ms 以下) +- 可支持几百 M,甚至几 T 的堆大小(最大支持4T) + +ZGC 的工作过程可以分为 4 个阶段: + +* 并发标记(Concurrent Mark): 遍历对象图做可达性分析的阶段,也要经过初始标记和最终标记,需要短暂停顿 +* 并发预备重分配(Concurrent Prepare for Relocate):根据特定的查询条件统计得出本次收集过程要清理哪些 Region,将这些 Region 组成重分配集(Relocation Set) +* 并发重分配(Concurrent Relocate): 重分配是 ZGC 执行过程中的核心阶段,这个过程要把重分配集中的存活对象复制到新的 Region 上,并为重分配集中的**每个 Region 维护一个转发表**(Forward Table),记录从旧地址到新地址的转向关系 +* 并发重映射(Concurrent Remap):修正整个堆中指向重分配集中旧对象的所有引用,ZGC 的并发映射并不是一个必须要立即完成的任务,ZGC 很巧妙地把并发重映射阶段要做的工作,合并到下一次垃圾收集循环中的并发标记阶段里去完成,因为都是要遍历所有对象,这样合并节省了一次遍历的开销 + +ZGC 几乎在所有地方并发执行的,除了初始标记的是 STW 的,但这部分的实际时间是非常少的,所以响应速度快,在尽可能对吞吐量影响不大的前提下,实现在任意堆内存大小下都可以把垃圾收集的停顿时间限制在十毫秒以内的低延迟 + +优点:高吞吐量、低延迟 + +缺点:浮动垃圾,当 ZGC 准备要对一个很大的堆做一次完整的并发收集,其全过程要持续十分钟以上,由于应用的对象分配速率很高,将创造大量的新对象产生浮动垃圾 + + + +参考文章:https://www.cnblogs.com/jimoer/p/13170249.html + + + +*** + + + +#### 总结 + +Serial GC、Parallel GC、Concurrent Mark Sweep GC 这三个 GC 不同: + +- 最小化地使用内存和并行开销,选 Serial GC +- 最大化应用程序的吞吐量,选 Parallel GC +- 最小化 GC 的中断或停顿时间,选 CMS GC + +![](https://seazean.oss-cn-beijing.aliyuncs.com/img/Java/JVM-垃圾回收器总结.png) + + + + + +*** + + + +### 内存泄漏 + +#### 泄露溢出 + +内存泄漏(Memory Leak):是指程序中已动态分配的堆内存由于某种原因程序未释放或无法释放,造成系统内存的浪费,导致程序运行速度减慢甚至系统崩溃等严重后果 + +可达性分析算法来判断对象是否是不再使用的对象,本质都是判断一个对象是否还被引用。由于代码的实现不同就会出现很多种内存泄漏问题,让 JVM 误以为此对象还在引用中,无法回收,造成内存泄漏 + +内存溢出(out of memory)指的是申请内存时,没有足够的内存可以使用 + +内存泄漏和内存溢出的关系:内存泄漏的越来越多,最终会导致内存溢出 + + + +*** + + + +#### 几种情况 + +##### 静态集合 + +静态集合类的生命周期与 JVM 程序一致,则容器中的对象在程序结束之前将不能被释放,从而造成内存泄漏。原因是**长生命周期的对象持有短生命周期对象的引用**,尽管短生命周期的对象不再使用,但是因为长生命周期对象持有它的引用而导致不能被回收 + +```java +public class MemoryLeak { + static List list = new ArrayList(); + public void oomTest(){ + Object obj = new Object();//局部变量 + list.add(obj); + } +} +``` + + + +*** + + + +##### 单例模式 + +单例模式和静态集合导致内存泄露的原因类似,因为单例的静态特性,它的生命周期和 JVM 的生命周期一样长,所以如果单例对象持有外部对象的引用,那么这个外部对象也不会被回收,那么就会造成内存泄漏 + + + +**** + + + +##### 内部类 + +内部类持有外部类的情况,如果一个外部类的实例对象调用方法返回了一个内部类的实例对象,即使那个外部类实例对象不再被使用,但由于内部类持有外部类的实例对象,这个外部类对象也不会被回收,造成内存泄漏 + + + +*** + + + +##### 连接相关 + +数据库连接、网络连接和 IO 连接等,当不再使用时,需要显式调用 close 方法来释放与连接,垃圾回收器才会回收对应的对象,否则将会造成大量的对象无法被回收,从而引起内存泄漏 + + + +**** + + + +##### 不合理域 + +变量不合理的作用域,一个变量的定义的作用范围大于其使用范围,很有可能会造成内存泄漏;如果没有及时地把对象设置为 null,也有可能导致内存泄漏的发生 + +```java +public class UsingRandom { + private String msg; + public void receiveMsg(){ + msg = readFromNet();// 从网络中接受数据保存到 msg 中 + saveDB(msg); // 把 msg 保存到数据库中 + } +} +``` + +通过 readFromNet 方法把接收消息保存在 msg 中,然后调用 saveDB 方法把内容保存到数据库中,此时 msg 已经可以被回收,但是 msg 的生命周期与对象的生命周期相同,造成 msg 不能回收,产生内存泄漏 + +解决: + +* msg 变量可以放在 receiveMsg 方法内部,当方法使用完,msg 的生命周期也就结束,就可以被回收了 +* 在使用完 msg 后,把 msg 设置为 null,这样垃圾回收器也会回收 msg 的内存空间。 + + + +**** + + + +##### 改变哈希 + +当一个对象被存储进 HashSet 集合中以后,就**不能修改这个对象中的那些参与计算哈希值的字段**,否则对象修改后的哈希值与最初存储进 HashSet 集合中时的哈希值不同,这种情况下使用该对象的当前引用作为的参数去 HashSet 集合中检索对象返回 false,导致无法从 HashSet 集合中单独删除当前对象,造成内存泄漏 + + + +*** + + + +##### 缓存泄露 + +内存泄漏的一个常见来源是缓存,一旦把对象引用放入到缓存中,就会很容易被遗忘 + +使用 WeakHashMap 代表缓存,当除了自身有对 key 的引用外没有其他引用,map 会自动丢弃此值 + + + + + +*** + + + +#### 案例分析 + +```java +public class Stack { + private Object[] elements; + private int size = 0; + private static final int DEFAULT_INITIAL_CAPACITY = 16; + + public Stack() { + elements = new Object[DEFAULT_INITIAL_CAPACITY]; + } + + public void push(Object e) { //入栈 + ensureCapacity(); + elements[size++] = e; + } + + public Object pop() { //出栈 + if (size == 0) + throw new EmptyStackException(); + return elements[--size]; + } + + private void ensureCapacity() { + if (elements.length == size) + elements = Arrays.copyOf(elements, 2 * size + 1); + } +} +``` + +程序并没有明显错误,但 pop 函数存在内存泄漏问题,因为 pop 函数只是把栈顶索引下移一位,并没有把上一个出栈索引处的引用置空,导致**栈数组一直强引用着已经出栈的对象** + +解决方法: + +```java +public Object pop() { + if (size == 0) + throw new EmptyStackException(); + Object result = elements[--size]; + elements[size] = null; + return result; +} +``` + + + + + +*** + + + + + +## 类加载 + +### 对象访存 + +#### 存储结构 + +一个 Java 对象内存中存储为三部分:对象头(Header)、实例数据(Instance Data)和对齐填充 (Padding) + +对象头: + +* 普通对象:分为两部分 + + * **Mark Word**:用于存储对象自身的运行时数据, 如哈希码(HashCode)、GC 分代年龄、锁状态标志、线程持有的锁、偏向线程ID、偏向时间戳等等 + + ```ruby + hash(25) + age(4) + lock(3) = 32bit #32位系统 + unused(25+1) + hash(31) + age(4) + lock(3) = 64bit #64位系统 + ``` + + * **Klass Word**:类型指针,**指向该对象的 Class 类对象的指针**,虚拟机通过这个指针来确定这个对象是哪个类的实例;在 64 位系统中,开启指针压缩(-XX:+UseCompressedOops)或者 JVM 堆的最大值小于 32G,这个指针也是 4byte,否则是 8byte(就是 **Java 中的一个引用的大小**) + + ```ruby + |-----------------------------------------------------| + | Object Header (64 bits) | + |---------------------------|-------------------------| + | Mark Word (32 bits) | Klass Word (32 bits) | + |---------------------------|-------------------------| + ``` + +* 数组对象:如果对象是一个数组,那在对象头中还有一块数据用于记录数组长度(12 字节) + + ```ruby + |-------------------------------------------------------------------------------| + | Object Header (96 bits) | + |-----------------------|-----------------------------|-------------------------| + | Mark Word(32bits) | Klass Word(32bits) | array length(32bits) | + |-----------------------|-----------------------------|-------------------------| + ``` + +实例数据:实例数据部分是对象真正存储的有效信息,也是在程序代码中所定义的各种类型的字段内容,无论是从父类继承下来的,还是在子类中定义的,都需要记录起来 + +对齐填充:Padding 起占位符的作用。64 位系统,由于 HotSpot VM 的自动内存管理系统要求**对象起始地址必须是 8 字节的整数倍**,就是对象的大小必须是 8 字节的整数倍,而对象头部分正好是 8 字节的倍数(1 倍或者 2 倍),因此当对象实例数据部分没有对齐时,就需要通过对齐填充来补全 + +32 位系统: + +* 一个 int 在 java 中占据 4byte,所以 Integer 的大小为: + + ```java + private final int value; + ``` + + ```ruby + # 需要补位4byte + 4(Mark Word) + 4(Klass Word) + 4(data) + 4(Padding) = 16byte + ``` + +* `int[] arr = new int[10]` + + ```ruby + # 由于需要8位对齐,所以最终大小为56byte + 4(Mark Word) + 4(Klass Word) + 4(length) + 4*10(10个int大小) + 4(Padding) = 56sbyte + ``` + + + +*** + + + +#### 实际大小 + +浅堆(Shallow Heap):**对象本身占用的内存,不包括内部引用对象的大小**,32 位系统中一个对象引用占 4 个字节,每个对象头占用 8 个字节,根据堆快照格式不同,对象的大小会同 8 字节进行对齐 + +JDK7 中的 String:2个 int 值共占 8 字节,value 对象引用占用 4 字节,对象头 8 字节,对齐后占 24 字节,为 String 对象的浅堆大小,与 value 实际取值无关,无论字符串长度如何,浅堆大小始终是 24 字节 + +```java +private final char value[]; +private int hash; +private int hash32; +``` + +保留集(Retained Set):对象 A 的保留集指当对象 A 被垃圾回收后,可以被释放的所有的对象集合(包括 A 本身),所以对象 A 的保留集就是只能通过对象 A 被直接或间接访问到的所有对象的集合,就是仅被对象 A 所持有的对象的集合 + +深堆(Retained Heap):指对象的保留集中所有的对象的浅堆大小之和,一个对象的深堆指只能通过该对象访问到的(直接或间接)所有对象的浅堆之和,即对象被回收后,可以释放的真实空间 + +对象的实际大小:一个对象所能触及的所有对象的浅堆大小之和,也就是通常意义上我们说的对象大小 + +下图显示了一个简单的对象引用关系图,对象 A 引用了 C 和 D,对象 B 引用了 C 和 E。那么对象 A 的浅堆大小只是 A 本身,**A 的实际大小为 A、C、D 三者之和**,A 的深堆大小为 A 与 D 之和,由于对象 C 还可以通过对象 B 访问到 C,因此 C 不在对象 A 的深堆范围内 + + + +内存分析工具 MAT 提供了一种叫支配树的对象图,体现了对象实例间的支配关系 + +基本性质: + +- 对象 A 的子树(所有被对象 A 支配的对象集合)表示对象 A 的保留集(retained set),即深堆 + +- 如果对象 A 支配对象 B,那么对象 A 的直接支配者也支配对象 B + +- 支配树的边与对象引用图的边不直接对应 + +左图表示对象引用图,右图表示左图所对应的支配树: + +![](https://seazean.oss-cn-beijing.aliyuncs.com/img/Java/JVM-支配树.png) + +比如:对象 F 与对象 D 相互引用,因为到对象 F 的所有路径必然经过对象 D,因此对象 D 是对象 F 的直接支配者 + + + +参考文章:https://www.yuque.com/u21195183/jvm/nkq31c + + + +*** + + + +#### 节约内存 + +* 尽量使用基本数据类型 + +* 满足容量前提下,尽量用小字段 + +* 尽量用数组,少用集合,数组中是可以使用基本类型的,但是集合中只能放包装类型,如果需要使用集合,推荐比较节约内存的集合工具:fastutil + + 一个 ArrayList 集合,如果里面放了 10 个数字,占用多少内存: + + ```java + private transient Object[] elementData; + private int size; + ``` + + Mark Word 占 4byte,Klass Word 占 4byte,一个 int 字段占 4byte,elementData 数组占 12byte,数组中 10 个 Integer 对象占 10×16,所以整个集合空间大小为 184byte(深堆) + +* 时间用 long/int 表示,不用 Date 或者 String + + + +*** + + + +#### 对象访问 + +JVM 是通过**栈帧中的对象引用**访问到其内部的对象实例: + +* 句柄访问:Java 堆中会划分出一块内存来作为句柄池,reference 中存储的就是对象的句柄地址,而句柄中包含了对象实例数据和类型数据各自的具体地址信息 + + 优点:reference 中存储的是稳定的句柄地址,在对象被移动(垃圾收集)时只会改变句柄中的实例数据指针,而 reference 本身不需要被修改 + + ![](https://seazean.oss-cn-beijing.aliyuncs.com/img/Java/JVM-对象访问-句柄访问.png) + +* 直接指针(HotSpot 采用):Java 堆对象的布局必须考虑如何放置访问类型数据的相关信息,reference 中直接存储的对象地址 + + 优点:速度更快,**节省了一次指针定位的时间开销** + + 缺点:对象被移动时(如进行 GC 后的内存重新排列),对象的 reference 也需要同步更新 + + ![](https://seazean.oss-cn-beijing.aliyuncs.com/img/Java/JVM-对象访问-直接指针.png) + + + +参考文章:https://www.cnblogs.com/afraidToForget/p/12584866.html + + + +*** + + + +### 对象创建 + +#### 生命周期 + +在 Java 中,对象的生命周期包括以下几个阶段: + +1. 创建阶段 (Created): +2. 应用阶段 (In Use):对象至少被一个强引用持有着 +3. 不可见阶段 (Invisible):程序的执行已经超出了该对象的作用域,不再持有该对象的任何强引用 +4. 不可达阶段 (Unreachable):该对象不再被任何强引用所持有,包括 GC Root 的强引用 +5. 收集阶段 (Collected):垃圾回收器对该对象的内存空间重新分配做好准备,该对象如果重写了 finalize() 方法,则会去执行该方法 +6. 终结阶段 (Finalized):等待垃圾回收器对该对象空间进行回收,当对象执行完 finalize() 方法后仍然处于不可达状态时进入该阶段 +7. 对象空间重分配阶段 (De-allocated):垃圾回收器对该对象的所占用的内存空间进行回收或者再分配 + + + +参考文章:https://blog.csdn.net/sodino/article/details/38387049 + + + +*** + + + +#### 创建时机 + +类在第一次实例化加载一次,后续实例化不再加载,引用第一次加载的类 + +Java 对象创建时机: + +1. 使用 new 关键字创建对象:由执行类实例创建表达式而引起的对象创建 + +2. 使用 Class 类的 newInstance 方法(反射机制) + +3. 使用 Constructor 类的 newInstance 方法(反射机制) + + ```java + public class Student { + private int id; + public Student(Integer id) { + this.id = id; + } + public static void main(String[] args) throws Exception { + Constructor c = Student.class.getConstructor(Integer.class); + Student stu = c.newInstance(123); + } + } + ``` + + 使用 newInstance 方法的这两种方式创建对象使用的就是 Java 的反射机制,事实上 Class 的 newInstance 方法内部调用的也是 Constructor 的 newInstance 方法 + +4. 使用 Clone 方法创建对象:用 clone 方法创建对象的过程中并不会调用任何构造函数,要想使用 clone 方法,我们就必须先实现 Cloneable 接口并实现其定义的 clone 方法 + +5. 使用(反)序列化机制创建对象:当反序列化一个对象时,JVM 会创建一个**单独的对象**,在此过程中,JVM 并不会调用任何构造函数,为了反序列化一个对象,需要让类实现 Serializable 接口 + +从 Java 虚拟机层面看,除了使用 new 关键字创建对象的方式外,其他方式全部都是通过转变为 invokevirtual 指令直接创建对象的 + + + +*** + + + +#### 创建过程 + +创建对象的过程: + +1. 判断对象对应的类是否加载、链接、初始化 + +2. 为对象分配内存:指针碰撞、空闲链表。当一个对象被创建时,虚拟机就会为其分配内存来存放对象的实例变量及其从父类继承过来的实例变量,即使从**隐藏变量**也会被分配空间(继承部分解释了为什么会隐藏) + +3. 处理并发安全问题: + + * 采用 CAS 配上自旋保证更新的原子性 + * 每个线程预先分配一块 TLAB + +4. 初始化分配的空间:虚拟机将分配到的内存空间都初始化为零值(不包括对象头),保证对象实例字段在不赋值时可以直接使用,程序能访问到这些字段的数据类型所对应的零值 + +5. 设置对象的对象头:将对象的所属类(类的元数据信息)、对象的 HashCode、对象的 GC 信息、锁信息等数据存储在对象头中 + +6. 执行 init 方法进行实例化:实例变量初始化、实例代码块初始化 、构造函数初始化 + + * 实例变量初始化与实例代码块初始化: + + 对实例变量直接赋值或者使用实例代码块赋值,**编译器会将其中的代码放到类的构造函数中去**,并且这些代码会被放在对超类构造函数的调用语句之后(Java 要求构造函数的第一条语句必须是超类构造函数的调用语句),构造函数本身的代码之前 + + * 构造函数初始化: + + **Java 要求在实例化类之前,必须先实例化其超类,以保证所创建实例的完整性**,在准备实例化一个类的对象前,首先准备实例化该类的父类,如果该类的父类还有父类,那么准备实例化该类的父类的父类,依次递归直到递归到 Object 类。然后从 Object 类依次对以下各类进行实例化,初始化父类中的变量和执行构造函数 + + + +*** + + + +#### 承上启下 + +1. 一个实例变量在对象初始化的过程中会被赋值几次?一个实例变量最多可以被初始化 4 次 + + JVM 在为一个对象分配完内存之后,会给每一个实例变量赋予默认值,这个实例变量被第一次赋值;在声明实例变量的同时对其进行了赋值操作,那么这个实例变量就被第二次赋值;在实例代码块中又对变量做了初始化操作,那么这个实例变量就被第三次赋值;;在构造函数中也对变量做了初始化操作,那么这个实例变量就被第四次赋值 + +2. 类的初始化过程与类的实例化过程的异同? + + 类的初始化是指类加载过程中的初始化阶段对类变量按照代码进行赋值的过程;类的实例化是指在类完全加载到内存中后创建对象的过程(类的实例化触发了类的初始化,先初始化才能实例化) + +3. 假如一个类还未加载到内存中,那么在创建一个该类的实例时,具体过程是怎样的?(**经典案例**) + + ```java + public class StaticTest { + public static void main(String[] args) { + staticFunction();//调用静态方法,触发初始化 + } + + static StaticTest st = new StaticTest(); + + static { //静态代码块 + System.out.println("1"); + } + + { // 实例代码块 + System.out.println("2"); + } + + StaticTest() { // 实例构造器 + System.out.println("3"); + System.out.println("a=" + a + ",b=" + b); + } + + public static void staticFunction() { // 静态方法 + System.out.println("4"); + } + + int a = 110; // 实例变量 + static int b = 112; // 静态变量 + }/* Output: + 2 + 3 + a=110,b=0 + 1 + 4 + *///:~ + ``` + + `static StaticTest st = new StaticTest();`: + + * 实例实例化不一定要在类初始化结束之后才开始 + + * 在同一个类加载器下,一个类型只会被初始化一次。所以一旦开始初始化一个类,无论是否完成后续都不会再重新触发该类型的初始化阶段了(只考虑在同一个类加载器下的情形)。因此在实例化上述程序中的 st 变量时,**实际上是把实例化嵌入到了静态初始化流程中,并且在上面的程序中,嵌入到了静态初始化的起始位置**,这就导致了实例初始化完全发生在静态初始化之前,这也是导致 a 为 110,b 为 0 的原因 + + 代码等价于: + + ```java + public class StaticTest { + (){ + a = 110; // 实例变量 + System.out.println("2"); // 实例代码块 + System.out.println("3"); // 实例构造器中代码的执行 + System.out.println("a=" + a + ",b=" + b); // 实例构造器中代码的执行 + 类变量st被初始化 + System.out.println("1"); //静态代码块 + 类变量b被初始化为112 + } + } + ``` + + + + + +*** + + + +### 加载过程 + +#### 生命周期 + +类是在运行期间**第一次使用时动态加载**的(不使用不加载),而不是一次性加载所有类,因为一次性加载会占用很多的内存,加载的类信息存放于一块成为方法区的内存空间 + +![](https://seazean.oss-cn-beijing.aliyuncs.com/img/Java/JVM-类的生命周期.png) + +包括 7 个阶段: + +* 加载(Loading) +* 链接:验证(Verification)、准备(Preparation)、解析(Resolution) +* 初始化(Initialization) +* 使用(Using) +* 卸载(Unloading) + + + +*** + + + +#### 加载阶段 + +加载是类加载的其中一个阶段,注意不要混淆 + +加载过程完成以下三件事: + +- 通过类的完全限定名称获取定义该类的二进制字节流(二进制字节码) +- 将该字节流表示的静态存储结构转换为方法区的运行时存储结构(Java 类模型) +- **将字节码文件加载至方法区后,在堆中生成一个代表该类的 Class 对象,作为该类在方法区中的各种数据的访问入口** + +其中二进制字节流可以从以下方式中获取: + +- 从 ZIP 包读取,成为 JAR、EAR、WAR 格式的基础 +- 从网络中获取,最典型的应用是 Applet +- 由其他文件生成,例如由 JSP 文件生成对应的 Class 类 +- 运行时计算生成,例如动态代理技术,在 java.lang.reflect.Proxy 使用 ProxyGenerator.generateProxyClass 生成字节码 + +方法区内部采用 C++ 的 instanceKlass 描述 Java 类的数据结构: + +* `_java_mirror` 即 Java 的类镜像,例如对 String 来说就是 String.class,作用是把 class 暴露给 Java 使用 +* `_super` 即父类、`_fields` 即成员变量、`_methods` 即方法、`_constants` 即常量池、`_class_loader` 即类加载器、`_vtable` **虚方法表**、`_itable` 接口方法表 + +加载过程: + +* 如果这个类还有父类没有加载,先加载父类 +* 加载和链接可能是交替运行的 +* Class 对象和 _java_mirror 相互持有对方的地址,堆中对象通过 instanceKlass 和元空间进行交互 + + + +创建数组类有些特殊,因为数组类本身并不是由类加载器负责创建,而是由 JVM 在运行时根据需要而直接创建的,但数组的元素类型仍然需要依靠类加载器去创建,创建数组类的过程: + +- 如果数组的元素类型是引用类型,那么遵循定义的加载过程递归加载和创建数组的元素类型 +- JVM 使用指定的元素类型和数组维度来创建新的数组类 +- **基本数据类型由启动类加载器加载** + + + +*** + + + +#### 链接阶段 + +##### 验证 + +确保 Class 文件的字节流中包含的信息是否符合 JVM 规范,保证被加载类的正确性,不会危害虚拟机自身的安全 + +主要包括**四种验证**: + +* 文件格式验证 + +* 语义检查,但凡在语义上不符合规范的,虚拟机不会给予验证通过 + + * 是否所有的类都有父类的存在(除了 Object 外,其他类都应该有父类) + + * 是否一些被定义为 final 的方法或者类被重写或继承了 + + * 非抽象类是否实现了所有抽象方法或者接口方法 + + * 是否存在不兼容的方法 + +* 字节码验证,试图通过对字节码流的分析,判断字节码是否可以被正确地执行 + + * 在字节码的执行过程中,是否会跳转到一条不存在的指令 + * 函数的调用是否传递了正确类型的参数 + * 变量的赋值是不是给了正确的数据类型 + * 栈映射帧(StackMapTable)在这个阶段用于检测在特定的字节码处,其局部变量表和操作数栈是否有着正确的数据类型 + +* 符号引用验证,Class 文件在其常量池会通过字符串记录将要使用的其他类或者方法 + + + +*** + + + +##### 准备 + +准备阶段为**静态变量(类变量)分配内存并设置初始值**,使用的是方法区的内存: + +说明:实例变量不会在这阶段分配内存,它会在对象实例化时随着对象一起被分配在堆中,类加载发生在所有实例化操作之前,并且类加载只进行一次,实例化可以进行多次 + +类变量初始化: + +* static 变量分配空间和赋值是两个步骤:**分配空间在准备阶段完成,赋值在初始化阶段完成** +* 如果 static 变量是 final 的基本类型以及字符串常量,那么编译阶段值(方法区)就确定了,准备阶段会显式初始化 +* 如果 static 变量是 final 的,但属于引用类型或者构造器方法的字符串,赋值在初始化阶段完成 + +实例: + +* 初始值一般为 0 值,例如下面的类变量 value 被初始化为 0 而不是 123: + + ```java + public static int value = 123; + ``` + +* 常量 value 被初始化为 123 而不是 0: + + ```java + public static final int value = 123; + ``` + +* Java 并不支持 boolean 类型,对于 boolean 类型,内部实现是 int,由于 int 的默认值是 0,故 boolean 的默认值就是 false + + + +*** + + + +##### 解析 + +将常量池中类、接口、字段、方法的**符号引用替换为直接引用**(内存地址)的过程: + +* 符号引用:一组符号来描述目标,可以是任何字面量,属于编译原理方面的概念,如:包括类和接口的全限名、字段的名称和描述符、方法的名称和**方法描述符**(因为类还没有加载完,很多方法是找不到的) +* 直接引用:直接指向目标的指针、相对偏移量或一个间接定位到目标的句柄,如果有了直接引用,那说明引用的目标必定已经存在于内存之中 + +例如:在 `com.demo.Solution` 类中引用了 `com.test.Quest`,把 `com.test.Quest` 作为符号引用存进类常量池,在类加载完后,**用这个符号引用去方法区找这个类的内存地址** + +解析动作主要针对类或接口、字段、类方法、接口方法、方法类型等 + +* 在类加载阶段解析的是非虚方法,静态绑定 +* 也可以在初始化阶段之后再开始解析,这是为了支持 Java 的**动态绑定** +* 通过解析操作,符号引用就可以转变为目标方法在类的虚方法表中的位置,从而使得方法被成功调用 + +```java +public class Load2 { + public static void main(String[] args) throws Exception{ + ClassLoader classloader = Load2.class.getClassLoader(); + // cloadClass 加载类方法不会导致类的解析和初始化,也不会加载D + Class c = classloader.loadClass("cn.jvm.t3.load.C"); + + // new C();会导致类的解析和初始化,从而解析初始化D + System.in.read(); + } +} +class C { + D d = new D(); +} +class D { +} +``` + + + +**** + + + +#### 初始化 + +##### 介绍 + +初始化阶段才真正开始执行类中定义的 Java 程序代码,在准备阶段,类变量已经赋过一次系统要求的初始值;在初始化阶段,通过程序制定的计划去初始化类变量和其它资源,执行 + +在编译生成 class 文件时,编译器会产生两个方法加于 class 文件中,一个是类的初始化方法 clinit,另一个是实例的初始化方法 init + +类构造器 () 与实例构造器 () 不同,它不需要程序员进行显式调用,在一个类的生命周期中,类构造器最多被虚拟机**调用一次**,而实例构造器则会被虚拟机调用多次,只要程序员创建对象 + +类在第一次实例化加载一次,把 class 读入内存,后续实例化不再加载,引用第一次加载的类 + + + +*** + + + +##### clinit + +():类构造器,由编译器自动收集类中**所有类变量的赋值动作和静态语句块**中的语句合并产生的 + +作用:是在类加载过程中的初始化阶段进行静态变量初始化和执行静态代码块 + +* 如果类中没有静态变量或静态代码块,那么 clinit 方法将不会被生成 +* clinit 方法只执行一次,在执行 clinit 方法时,必须先执行父类的clinit方法 +* static 变量的赋值操作和静态代码块的合并顺序由源文件中出现的顺序决定 +* static 不加 final 的变量都在初始化环节赋值 + +**线程安全**问题: + +* 虚拟机会保证一个类的 () 方法在多线程环境下被正确的加锁和同步,如果多个线程同时初始化一个类,只会有一个线程执行这个类的 () 方法,其它线程都阻塞等待,直到活动线程执行 () 方法完毕 +* 如果在一个类的 () 方法中有耗时的操作,就可能造成多个线程阻塞,在实际过程中此种阻塞很隐蔽 + +特别注意:静态语句块只能访问到定义在它之前的类变量,定义在它之后的类变量只能赋值,不能访问 + +```java +public class Test { + static { + //i = 0; // 给变量赋值可以正常编译通过 + System.out.print(i); // 这句编译器会提示“非法向前引用” + } + static int i = 1; +} +``` + +接口中不可以使用静态语句块,但仍然有类变量初始化的赋值操作,因此接口与类一样都会生成 () 方法,两者不同的是: + +* 在初始化一个接口时,并不会先初始化它的父接口,所以执行接口的 () 方法不需要先执行父接口的 () 方法 +* 在初始化一个类时,不会先初始化所实现的接口,所以接口的实现类在初始化时不会执行接口的 () 方法 +* 只有当父接口中定义的变量使用时,父接口才会初始化 + + + +**** + + + +##### 时机 + +类的初始化是懒惰的,只有在首次使用时才会被装载,JVM 不会无条件地装载 Class 类型,Java 虚拟机规定,一个类或接口在初次使用前,必须要进行初始化 + +**主动引用**:虚拟机规范中并没有强制约束何时进行加载,但是规范严格规定了有且只有下列情况必须对类进行初始化(加载、验证、准备都会发生): + +* 当创建一个类的实例时,使用 new 关键字,或者通过反射、克隆、反序列化(前文讲述的对象的创建时机) +* 当调用类的静态方法或访问静态字段时,遇到 getstatic、putstatic、invokestatic 这三条字节码指令,如果类没有进行过初始化,则必须先触发其初始化 + * getstatic:程序访问类的静态变量(不是静态常量,常量会被加载到运行时常量池) + * putstatic:程序给类的静态变量赋值 + * invokestatic :调用一个类的静态方法 +* 使用 java.lang.reflect 包的方法对类进行反射调用时,如果类没有进行初始化,则需要先触发其初始化 +* 当初始化一个类的时候,如果发现其父类还没有进行过初始化,则需要先触发其父类的初始化,但这条规则并**不适用于接口** +* 当虚拟机启动时,需要指定一个要执行的主类(包含 main() 方法的那个类),虚拟机会先初始化这个主类 +* MethodHandle 和 VarHandle 可以看作是轻量级的反射调用机制,而要想使用这两个调用, 就必须先使用 findStaticVarHandle 来初始化要调用的类 +* 补充:当一个接口中定义了 JDK8 新加入的默认方法(被 default 关键字修饰的接口方法)时,如果有这个接口的实现类发生了初始化,那该接口要在其之前被初始化 + +**被动引用**:所有引用类的方式都不会触发初始化,称为被动引用 + +* 通过子类引用父类的静态字段,不会导致子类初始化,只会触发父类的初始化 +* 通过数组定义来引用类,不会触发此类的初始化。该过程会对数组类进行初始化,数组类是一个由虚拟机自动生成的、直接继承自 Object 的子类,其中包含了数组的属性和方法 +* 常量(final 修饰)在编译阶段会存入调用类的常量池中,本质上没有直接引用到定义常量的类,因此不会触发定义常量的类的初始化 +* 调用 ClassLoader 类的 loadClass() 方法加载一个类,并不是对类的主动使用,不会导致类的初始化 + + + +*** + + + +##### init + +init 指的是实例构造器,主要作用是在类实例化过程中执行,执行内容包括成员变量初始化和代码块的执行 + +实例化即调用 ()V ,虚拟机会保证这个类的构造方法的线程安全,先为实例变量分配内存空间,再执行赋默认值,然后根据源码中的顺序执行赋初值或代码块,没有成员变量初始化和代码块则不会执行 + +类实例化过程:**父类的类构造器() -> 子类的类构造器() -> 父类的成员变量和实例代码块 -> 父类的构造函数 -> 子类的成员变量和实例代码块 -> 子类的构造函数** + +new 关键字会创建对象并复制 dup 一个对象引用,一个调用 方法,另一个用来赋值给接收者 + + + +*** + + + +#### 卸载阶段 + +时机:执行了 System.exit() 方法,程序正常执行结束,程序在执行过程中遇到了异常或错误而异常终止,由于操作系统出现错误而导致Java 虚拟机进程终止 + +卸载类即该类的 **Class 对象被 GC**,卸载类需要满足3个要求: + +1. 该类的所有的实例对象都已被 GC,也就是说堆不存在该类的实例对象 +2. 该类没有在其他任何地方被引用 +3. 该类的类加载器的实例已被 GC,一般是可替换类加载器的场景,如 OSGi、JSP 的重加载等,很难达成 + +在 JVM 生命周期类,由 JVM 自带的类加载器加载的类是不会被卸载的,自定义的类加载器加载的类是可能被卸载。因为 JVM 会始终引用启动、扩展、系统类加载器,这些类加载器始终引用它们所加载的类,这些类始终是可及的 + + + +**** + + + +### 类加载器 + +#### 类加载 + +类加载方式: + +* 隐式加载:不直接在代码中调用 ClassLoader 的方法加载类对象 + * 创建类对象、使用类的静态域、创建子类对象、使用子类的静态域 + * 在 JVM 启动时,通过三大类加载器加载 class +* 显式加载: + * ClassLoader.loadClass(className):只加载和连接,**不会进行初始化** + * Class.forName(String name, boolean initialize, ClassLoader loader):使用 loader 进行加载和连接,根据参数 initialize 决定是否初始化 + +类的唯一性: + +* 在 JVM 中表示两个 class 对象判断为同一个类存在的两个必要条件: + - 类的完整类名必须一致,包括包名 + - 加载这个类的 ClassLoader(指 ClassLoader 实例对象)必须相同 +* 这里的相等,包括类的 Class 对象的 equals() 方法、isAssignableFrom() 方法、isInstance() 方法的返回结果为 true,也包括使用 instanceof 关键字做对象所属关系判定结果为 true + +命名空间: + +- 每个类加载器都有自己的命名空间,命名空间由该加载器及所有的父加载器所加载的类组成 +- 在同一命名空间中,不会出现类的完整名字(包括类的包名)相同的两个类 + +基本特征: + +* **可见性**,子类加载器可以访问父加载器加载的类型,但是反过来是不允许的 +* **单一性**,由于父加载器的类型对于子加载器是可见的,所以父加载器中加载过的类型,不会在子加载器中重复加载 + + + +*** + + + +#### 加载器 + +类加载器是 Java 的核心组件,用于加载字节码到 JVM 内存,得到 Class 类的对象 + +从 Java 虚拟机规范来讲,只存在以下两种不同的类加载器: + +- 启动类加载器(Bootstrap ClassLoader):使用 C++ 实现,是虚拟机自身的一部分 +- 自定义类加载器(User-Defined ClassLoader):Java 虚拟机规范**将所有派生于抽象类 ClassLoader 的类加载器都划分为自定义类加载器**,使用 Java 语言实现,独立于虚拟机 + +从 Java 开发人员的角度看: + +* 启动类加载器(Bootstrap ClassLoader): + * 处于安全考虑,Bootstrap 启动类加载器只加载包名为 java、javax、sun 等开头的类 + * 类加载器负责加载在 `JAVA_HOME/jre/lib` 或 `sun.boot.class.path` 目录中的,或者被 -Xbootclasspath 参数所指定的路径中的类,并且是虚拟机识别的类库加载到虚拟机内存中 + * 仅按照文件名识别,如 rt.jar 名字不符合的类库即使放在 lib 目录中也不会被加载 + * 启动类加载器无法被 Java 程序直接引用,编写自定义类加载器时,如果要把加载请求委派给启动类加载器,直接使用 null 代替 +* 扩展类加载器(Extension ClassLoader): + * 由 ExtClassLoader (sun.misc.Launcher$ExtClassLoader) 实现,上级为 Bootstrap,显示为 null + * 将 `JAVA_HOME/jre/lib/ext` 或者被 `java.ext.dir` 系统变量所指定路径中的所有类库加载到内存中 + * 开发者可以使用扩展类加载器,创建的 JAR 放在此目录下,会由扩展类加载器自动加载 +* 应用程序类加载器(Application ClassLoader): + * 由 AppClassLoader(sun.misc.Launcher$AppClassLoader) 实现,上级为 Extension + * 负责加载环境变量 classpath 或系统属性 `java.class.path` 指定路径下的类库 + * 这个类加载器是 ClassLoader 中的 getSystemClassLoader() 方法的返回值,因此称为系统类加载器 + * 可以直接使用这个类加载器,如果应用程序中没有自定义类加载器,这个就是程序中默认的类加载器 +* 自定义类加载器:由开发人员自定义的类加载器,上级是 Application + +```java +public static void main(String[] args) { + //获取系统类加载器 + ClassLoader systemClassLoader = ClassLoader.getSystemClassLoader(); + System.out.println(systemClassLoader);//sun.misc.Launcher$AppClassLoader@18b4aac2 + + //获取其上层 扩展类加载器 + ClassLoader extClassLoader = systemClassLoader.getParent(); + System.out.println(extClassLoader);//sun.misc.Launcher$ExtClassLoader@610455d6 + + //获取其上层 获取不到引导类加载器 + ClassLoader bootStrapClassLoader = extClassLoader.getParent(); + System.out.println(bootStrapClassLoader);//null + + //对于用户自定义类来说:使用系统类加载器进行加载 + ClassLoader classLoader = ClassLoaderTest.class.getClassLoader(); + System.out.println(classLoader);//sun.misc.Launcher$AppClassLoader@18b4aac2 + + //String 类使用引导类加载器进行加载的 --> java核心类库都是使用启动类加载器加载的 + ClassLoader classLoader1 = String.class.getClassLoader(); + System.out.println(classLoader1);//null + +} +``` + +补充两个类加载器: + +* SecureClassLoader 扩展了 ClassLoader,新增了几个与使用相关的代码源和权限定义类验证(对 class 源码的访问权限)的方法,一般不会直接跟这个类打交道,更多是与它的子类 URLClassLoader 有所关联 +* ClassLoader 是一个抽象类,很多方法是空的没有实现,而 URLClassLoader 这个实现类为这些方法提供了具体的实现,并新增了 URLClassPath 类协助取得 Class 字节流等功能。在编写自定义类加载器时,如果没有太过于复杂的需求,可以直接继承 URLClassLoader 类,这样就可以避免去编写 findClass() 方法及其获取字节码流的方式,使自定义类加载器编写更加简洁 + + + +*** + + + +#### 常用API + +ClassLoader 类,是一个抽象类,其后所有的类加载器都继承自 ClassLoader(不包括启动类加载器) + +获取 ClassLoader 的途径: + +* 获取当前类的 ClassLoader:`clazz.getClassLoader()` +* 获取当前线程上下文的 ClassLoader:`Thread.currentThread.getContextClassLoader()` +* 获取系统的 ClassLoader:`ClassLoader.getSystemClassLoader()` +* 获取调用者的 ClassLoader:`DriverManager.getCallerClassLoader()` + +ClassLoader 类常用方法: + +* `getParent()`:返回该类加载器的超类加载器 +* `loadclass(String name)`:加载名为 name 的类,返回结果为 Class 类的实例,**该方法就是双亲委派模式** +* `findclass(String name)`:查找二进制名称为 name 的类,返回结果为 Class 类的实例,该方法会在检查完父类加载器之后被 loadClass() 方法调用 +* `findLoadedClass(String name)`:查找名称为 name 的已经被加载过的类,final 修饰无法重写 +* `defineClass(String name, byte[] b, int off, int len)`:将**字节流**解析成 JVM 能够识别的类对象 +* `resolveclass(Class c)`:链接指定的 Java 类,可以使类的 Class 对象创建完成的同时也被解析 +* `InputStream getResourceAsStream(String name)`:指定资源名称获取输入流 + + + +*** + + + +#### 加载模型 + +##### 加载机制 + +在 JVM 中,对于类加载模型提供了三种,分别为全盘加载、双亲委派、缓存机制 + +- **全盘加载:**当一个类加载器负责加载某个 Class 时,该 Class 所依赖和引用的其他 Class 也将由该类加载器负责载入,除非显示指定使用另外一个类加载器来载入 + +- **双亲委派:**某个特定的类加载器在接到加载类的请求时,首先将加载任务委托给父加载器,**依次递归**,如果父加载器可以完成类加载任务,就成功返回;只有当父加载器无法完成此加载任务时,才自己去加载 + +- **缓存机制:**会保证所有加载过的 Class 都会被缓存,当程序中需要使用某个 Class 时,类加载器先从缓存区中搜寻该 Class,只有当缓存区中不存在该 Class 对象时,系统才会读取该类对应的二进制数据,并将其转换成 Class 对象存入缓冲区(方法区)中 + - 这就是修改了 Class 后,必须重新启动 JVM,程序所做的修改才会生效的原因 + + + + + +*** + + + +##### 双亲委派 + +双亲委派模型(Parents Delegation Model):该模型要求除了顶层的启动类加载器外,其它类加载器都要有父类加载器,这里的父子关系一般通过组合关系(Composition)来实现,而不是继承关系(Inheritance) + +工作过程:一个类加载器首先将类加载请求转发到父类加载器,只有当父类加载器无法完成时才尝试自己加载 + +双亲委派机制的优点: + +* 可以避免某一个类被重复加载,当父类已经加载后则无需重复加载,保证全局唯一性 + +* Java 类随着它的类加载器一起具有一种带有优先级的层次关系,从而使得基础类得到统一 + +* 保护程序安全,防止类库的核心 API 被随意篡改 + + 例如:在工程中新建 java.lang 包,接着在该包下新建 String 类,并定义 main 函数 + + ```java + public class String { + public static void main(String[] args) { + System.out.println("demo info"); + } + } + ``` + + 此时执行 main 函数会出现异常,在类 java.lang.String 中找不到 main 方法。因为双亲委派的机制,java.lang.String 的在启动类加载器(Bootstrap)得到加载,启动类加载器优先级更高,在核心 jre 库中有其相同名字的类文件,但该类中并没有 main 方法 + +双亲委派机制的缺点:检查类是否加载的委托过程是单向的,这个方式虽然从结构上看比较清晰,使各个 ClassLoader 的职责非常明确,但**顶层的 ClassLoader 无法访问底层的 ClassLoader 所加载的类**(可见性) + + + + + +*** + + + +##### 源码分析 + +```java +protected Class loadClass(String name, boolean resolve) + throws ClassNotFoundException { + synchronized (getClassLoadingLock(name)) { + // 调用当前类加载器的 findLoadedClass(name),检查当前类加载器是否已加载过指定 name 的类 + Class c = findLoadedClass(name); + + // 当前类加载器如果没有加载过 + if (c == null) { + long t0 = System.nanoTime(); + try { + // 判断当前类加载器是否有父类加载器 + if (parent != null) { + // 如果当前类加载器有父类加载器,则调用父类加载器的 loadClass(name,false) +          // 父类加载器的 loadClass 方法,又会检查自己是否已经加载过 + c = parent.loadClass(name, false); + } else { + // 当前类加载器没有父类加载器,说明当前类加载器是 BootStrapClassLoader +           // 则调用 BootStrap ClassLoader 的方法加载类 + c = findBootstrapClassOrNull(name); + } + } catch (ClassNotFoundException e) { } + + if (c == null) { + // 如果调用父类的类加载器无法对类进行加载,则用自己的 findClass() 方法进行加载 + // 可以自定义 findClass() 方法 + long t1 = System.nanoTime(); + c = findClass(name); + + // this is the defining class loader; record the stats + sun.misc.PerfCounter.getParentDelegationTime().addTime(t1 - t0); + sun.misc.PerfCounter.getFindClassTime().addElapsedTimeFrom(t1); + sun.misc.PerfCounter.getFindClasses().increment(); + } + } + if (resolve) { + // 链接指定的 Java 类,可以使类的 Class 对象创建完成的同时也被解析 + resolveClass(c); + } + return c; + } +} +``` + + + +**** + + + +##### 破坏委派 + +双亲委派模型并不是一个具有强制性约束的模型,而是 Java 设计者推荐给开发者的类加载器实现方式 + +破坏双亲委派模型的方式: + +* 自定义 ClassLoader + + * 如果不想破坏双亲委派模型,只需要重写 findClass 方法 + * 如果想要去破坏双亲委派模型,需要去**重写 loadClass **方法 + +* 引入**线程上下文类加载器** + + Java 提供了很多服务提供者接口(Service Provider Interface,SPI),允许第三方为这些接口提供实现。常见的有 JDBC、JCE、JNDI 等。这些 SPI 接口由 Java 核心库来提供,而 SPI 的实现代码则是作为 Java 应用所依赖的 jar 包被包含进类路径 classpath 里,SPI 接口中的代码需要加载具体的实现类: + + * SPI 的接口是 Java 核心库的一部分,是由引导类加载器来加载的 + * SPI 的实现类是由系统类加载器加载,引导类加载器是无法找到 SPI 的实现类,因为双亲委派模型中 BootstrapClassloader 无法委派 AppClassLoader 来加载类 + + JDK 开发人员引入了线程上下文类加载器(Thread Context ClassLoader),这种类加载器可以通过 Thread 类的 setContextClassLoader 方法进行设置线程上下文类加载器,在执行线程中抛弃双亲委派加载模式,使程序可以逆向使用类加载器,使 Bootstrap 加载器拿到了 Application 加载器加载的类,破坏了双亲委派模型 + +* 实现程序的动态性,如代码热替换(Hot Swap)、模块热部署(Hot Deployment) + + IBM 公司主导的 JSR一291(OSGiR4.2)实现模块化热部署的关键是它自定义的类加载器机制的实现,每一个程序模块(OSGi 中称为 Bundle)都有一个自己的类加载器,当更换一个 Bundle 时,就把 Bundle 连同类加载器一起换掉以实现代码的热替换,在 OSGi 环境下,类加载器不再双亲委派模型推荐的树状结构,而是进一步发展为更加复杂的网状结构 + + 当收到类加载请求时,OSGi 将按照下面的顺序进行类搜索: + + 1. 将以 java.* 开头的类,委派给父类加载器加载 + 2. 否则,将委派列表名单内的类,委派给父类加载器加载 + 3. 否则,将 Import 列表中的类,委派给 Export 这个类的 Bundle 的类加载器加载 + 4. 否则,查找当前 Bundle 的 ClassPath,使用自己的类加载器加载 + 5. 否则,查找类是否在自己的 Fragment Bundle 中,如果在就委派给 Fragment Bundle 类加载器加载 + 6. 否则,查找 Dynamic Import 列表的 Bundle,委派给对应 Bundle 的类加载器加载 + 7. 否则,类查找失败 + + 热替换是指在程序的运行过程中,不停止服务,只通过替换程序文件来修改程序的行为,**热替换的关键需求在于服务不能中断**,修改必须立即表现正在运行的系统之中 + + + + + +*** + + + +#### 沙箱机制 + +沙箱机制(Sandbox):将 Java 代码限定在虚拟机特定的运行范围中,并且严格限制代码对本地系统资源访问,来保证对代码的有效隔离,防止对本地系统造成破坏 + +沙箱**限制系统资源访问**,包括 CPU、内存、文件系统、网络,不同级别的沙箱对资源访问的限制也不一样 + +* JDK1.0:Java 中将执行程序分成本地代码和远程代码两种,本地代码默认视为可信任的,而远程代码被看作是不受信的。对于授信的本地代码,可以访问一切本地资源,而对于非授信的远程代码不可以访问本地资源,其实依赖于沙箱机制。如此严格的安全机制也给程序的功能扩展带来障碍,比如当用户希望远程代码访问本地系统的文件时候,就无法实现 +* JDK1.1:针对安全机制做了改进,增加了安全策略。允许用户指定代码对本地资源的访问权限 +* JDK1.2:改进了安全机制,增加了代码签名,不论本地代码或是远程代码都会按照用户的安全策略设定,由类加载器加载到虚拟机中权限不同的运行空间,来实现差异化的代码执行权限控制 +* JDK1.6:当前最新的安全机制,引入了域(Domain)的概念。虚拟机会把所有代码加载到不同的系统域和应用域,不同的保护域对应不一样的权限。系统域部分专门负责与关键资源进行交互,而各个应用域部分则通过系统域的部分代理来对各种需要的资源进行访问 + + + + + +*** + + + +#### 自定义 + +对于自定义类加载器的实现,只需要继承 ClassLoader 类,覆写 findClass 方法即可 + +作用:隔离加载类、修改类加载的方式、拓展加载源、防止源码泄漏 + +```java +//自定义类加载器,读取指定的类路径classPath下的class文件 +public class MyClassLoader extends ClassLoader{ + private String classPath; + + public MyClassLoader(String classPath) { + this.classPath = classPath; + } + + public MyClassLoader(ClassLoader parent, String byteCodePath) { + super(parent); + this.classPath = classPath; + } + + @Override + protected Class findClass(String name) throws ClassNotFoundException { + BufferedInputStream bis = null; + ByteArrayOutputStream baos = null; + try { + // 获取字节码文件的完整路径 + String fileName = classPath + className + ".class"; + // 获取一个输入流 + bis = new BufferedInputStream(new FileInputStream(fileName)); + // 获取一个输出流 + baos = new ByteArrayOutputStream(); + // 具体读入数据并写出的过程 + int len; + byte[] data = new byte[1024]; + while ((len = bis.read(data)) != -1) { + baos.write(data, 0, len); + } + // 获取内存中的完整的字节数组的数据 + byte[] byteCodes = baos.toByteArray(); + // 调用 defineClass(),将字节数组的数据转换为 Class 的实例。 + Class clazz = defineClass(null, byteCodes, 0, byteCodes.length); + return clazz; + } catch (IOException e) { + e.printStackTrace(); + } finally { + try { + if (baos != null) + baos.close(); + } catch (IOException e) { + e.printStackTrace(); + } + try { + if (bis != null) + bis.close(); + } catch (IOException e) { + e.printStackTrace(); + } + } + return null; + } +} +``` + +```java +public static void main(String[] args) { + MyClassLoader loader = new MyClassLoader("D:\Workspace\Project\JVM_study\src\java1\"); + + try { + Class clazz = loader.loadClass("Demo1"); + System.out.println("加载此类的类的加载器为:" + clazz.getClassLoader().getClass().getName());//MyClassLoader + + System.out.println("加载当前类的类的加载器的父类加载器为:" + clazz.getClassLoader().getParent().getClass().getName());//sun.misc.Launcher$AppClassLoader + } catch (ClassNotFoundException e) { + e.printStackTrace(); + } +} +``` + + + +**** + + + +#### JDK9 + +为了保证兼容性,JDK9 没有改变三层类加载器架构和双亲委派模型,但为了模块化系统的顺利运行做了一些变动: + +* 扩展机制被移除,扩展类加载器由于**向后兼容性**的原因被保留,不过被重命名为平台类加载器(platform classloader),可以通过 ClassLoader 的新方法 getPlatformClassLoader() 来获取 + +* JDK9 基于模块化进行构建(原来的 rt.jar 和 tools.jar 被拆分成数个 JMOD 文件),其中 Java 类库就满足了可扩展的需求,那就无须再保留 `\lib\ext` 目录,此前使用这个目录或者 `java.ext.dirs` 系统变量来扩展 JDK 功能的机制就不需要再存在 + +* 启动类加载器、平台类加载器、应用程序类加载器全都继承于 `jdk.internal.loader.BuiltinClassLoader` + + + + + +*** + + + + + +## 运行机制 + +### 执行过程 + +Java 文件编译执行的过程: + +![](https://seazean.oss-cn-beijing.aliyuncs.com/img/Java/JVM-Java文件编译执行的过程.png) + +- 类加载器:用于装载字节码文件(.class文件) +- 运行时数据区:用于分配存储空间 +- 执行引擎:执行字节码文件或本地方法 +- 垃圾回收器:用于对 JVM 中的垃圾内容进行回收 + + + +**** + + + +### 字节码 + +#### 跨平台性 + +Java 语言:跨平台的语言(write once ,run anywhere) + +* 当 Java 源代码成功编译成字节码后,在不同的平台上面运行**无须再次编译** +* 让一个 Java 程序正确地运行在 JVM 中,Java 源码就必须要被编译为符合 JVM 规范的字节码 + +编译过程中的编译器: + +* 前端编译器: Sun 的全量式编译器 javac、 Eclipse 的增量式编译器 ECJ,**把源代码编译为字节码文件 .class** + + * IntelliJ IDEA 使用 javac 编译器 + * Eclipse 中,当开发人员编写完代码后保存时,ECJ 编译器就会把未编译部分的源码逐行进行编译,而非每次都全量编译,因此 ECJ 的编译效率会比 javac 更加迅速和高效 + * 前端编译器并不会直接涉及编译优化等方面的技术,具体优化细节移交给 HotSpot 的 JIT 编译器负责 + +* 后端运行期编译器:HotSpot VM 的 C1、C2 编译器,也就是 JIT 编译器,Graal 编译器 + + * JIT 编译器:执行引擎部分详解 + * Graal 编译器:JDK10 HotSpot 加入的一个全新的即时编译器,编译效果短短几年时间就追平了 C2 + +* 静态提前编译器:AOT (Ahead Of Time Compiler)编译器,直接把源代码编译成本地机器代码 + + * JDK 9 引入,是与即时编译相对立的一个概念,即时编译指的是在程序的运行过程中将字节码转换为机器码,AOT 是程序运行之前便将字节码转换为机器码 + + * 优点:JVM 加载已经预编译成二进制库,可以直接执行,不必等待即时编译器的预热,减少 Java 应用第一次运行慢的现象 + * 缺点: + * 破坏了 Java **一次编译,到处运行**,必须为每个不同硬件编译对应的发行包 + * 降低了 Java 链接过程的动态性,加载的代码在编译期就必须全部已知 + + + + + +*** + + + +#### 语言发展 + +机器码:各种用二进制编码方式表示的指令,与 CPU 紧密相关,所以不同种类的 CPU 对应的机器指令不同 + +指令:指令就是把机器码中特定的 0 和 1 序列,简化成对应的指令,例如 mov,inc 等,可读性稍好,但是不同的硬件平台的同一种指令(比如 mov),对应的机器码也可能不同 + +指令集:不同的硬件平台支持的指令是有区别的,每个平台所支持的指令,称之为对应平台的指令集 + +- x86 指令集,对应的是 x86 架构的平台 +- ARM 指令集,对应的是 ARM 架构的平台 + +汇编语言:用助记符代替机器指令的操作码,用地址符号或标号代替指令或操作数的地址 + +* 在不同的硬件平台,汇编语言对应着不同的机器语言指令集,通过汇编过程转换成机器指令 +* 计算机只认识指令码,汇编语言编写的程序也必须翻译成机器指令码,计算机才能识别和执行 + +高级语言:为了使计算机用户编程序更容易些,后来就出现了各种高级计算机语言 + +字节码:是一种中间状态(中间码)的二进制代码,比机器码更抽象,需要直译器转译后才能成为机器码 + +* 字节码为了实现特定软件运行和软件环境,与硬件环境无关 +* 通过编译器和虚拟机器实现,编译器将源码编译成字节码,虚拟机器将字节码转译为可以直接执行的指令 + + + + + +*** + + + + + +#### 类结构 + +##### 文件结构 + +字节码是一种二进制的类文件,是编译之后供虚拟机解释执行的二进制字节码文件,**一个 class 文件对应一个 public 类型的类或接口** + +字节码内容是 **JVM 的字节码指令**,不是机器码,C、C++ 经由编译器直接生成机器码,所以执行效率比 Java 高 + +JVM 官方文档:https://docs.oracle.com/javase/specs/jvms/se8/html/index.html + +根据 JVM 规范,类文件结构如下: + +```java +ClassFile { + u4 magic; + u2 minor_version; + u2 major_version; + u2 constant_pool_count; + cp_info constant_pool[constant_pool_count-1]; + u2 access_flags; + u2 this_class; + u2 super_class; + u2 interfaces_count; + u2 interfaces[interfaces_count]; + u2 fields_count; + field_info fields[fields_count]; + u2 methods_count; + method_info methods[methods_count]; + u2 attributes_count; + attribute_info attributes[attributes_count]; +} +``` + +| 类型 | 名称 | 说明 | 长度 | 数量 | +| -------------- | ------------------- | -------------------- | ------- | --------------------- | +| u4 | magic | 魔数,识别类文件格式 | 4个字节 | 1 | +| u2 | minor_version | 副版本号(小版本) | 2个字节 | 1 | +| u2 | major_version | 主版本号(大版本) | 2个字节 | 1 | +| u2 | constant_pool_count | 常量池计数器 | 2个字节 | 1 | +| cp_info | constant_pool | 常量池表 | n个字节 | constant_pool_count-1 | +| u2 | access_flags | 访问标识 | 2个字节 | 1 | +| u2 | this_class | 类索引 | 2个字节 | 1 | +| u2 | super_class | 父类索引 | 2个字节 | 1 | +| u2 | interfaces_count | 接口计数 | 2个字节 | 1 | +| u2 | interfaces | 接口索引集合 | 2个字节 | interfaces_count | +| u2 | fields_count | 字段计数器 | 2个字节 | 1 | +| field_info | fields | 字段表 | n个字节 | fields_count | +| u2 | methods_count | 方法计数器 | 2个字节 | 1 | +| method_info | methods | 方法表 | n个字节 | methods_count | +| u2 | attributes_count | 属性计数器 | 2个字节 | 1 | +| attribute_info | attributes | 属性表 | n个字节 | attributes_count | + +Class 文件格式采用一种类似于 C 语言结构体的方式进行数据存储,这种结构中只有两种数据类型:无符号数和表 + +* 无符号数属于基本的数据类型,以 u1、u2、u4、u8 来分别代表1个字节、2个字节、4个字节和8个字节的无符号数,无符号数可以用来描述数字、索引引用、数量值或者按照 UTF-8 编码构成字符串 +* 表是由多个无符号数或者其他表作为数据项构成的复合数据类型,表都以 `_info` 结尾,用于描述有层次关系的数据,整个 Class 文件本质上就是一张表,由于表没有固定长度,所以通常会在其前面加上个数说明 + +获取方式: + +* HelloWorld.java 执行 `javac -parameters -d . HellowWorld.java`指令 +* 写入文件指令 `javap -v xxx.class >xxx.txt` +* IDEA 插件 jclasslib + + + +*** + + + +##### 魔数版本 + +魔数:每个 Class 文件开头的 4 个字节的无符号整数称为魔数(Magic Number),是 Class 文件的标识符,代表这是一个能被虚拟机接受的有效合法的 Class 文件, + +* 魔数值固定为 0xCAFEBABE,不符合则会抛出错误 + +* 使用魔数而不是扩展名来进行识别主要是基于安全方面的考虑,因为文件扩展名可以随意地改动 + +版本:4 个 字节,5 6两个字节代表的是编译的副版本号 minor_version,而 7 8 两个字节是编译的主版本号 major_version + +* 不同版本的 Java 编译器编译的 Class 文件对应的版本是不一样的,高版本的 Java 虚拟机可以执行由低版本编译器生成的 Class 文件,反之 JVM 会抛出异常 `java.lang.UnsupportedClassVersionError` + +| 主版本(十进制) | 副版本(十进制) | 编译器版本 | +| ---------------- | ---------------- | ---------- | +| 45 | 3 | 1.1 | +| 46 | 0 | 1.2 | +| 47 | 0 | 1.3 | +| 48 | 0 | 1.4 | +| 49 | 0 | 1.5 | +| 50 | 0 | 1.6 | +| 51 | 0 | 1.7 | +| 52 | 0 | 1.8 | +| 53 | 0 | 1.9 | +| 54 | 0 | 1.10 | +| 55 | 0 | 1.11 | + +![](https://seazean.oss-cn-beijing.aliyuncs.com/img/Java/JVM-类结构.png) + + + +图片来源:https://www.bilibili.com/video/BV1PJ411n7xZ + + + +*** + + + +##### 常量池 + +常量池中常量的数量是不固定的,所以在常量池的入口需要放置一项 u2 类型的无符号数,代表常量池计数器(constant_pool_count),这个容量计数是从 1 而不是 0 开始,是为了满足后面某些指向常量池的索引值的数据在特定情况下需要表达不引用任何一个常量池项目,这种情况可用索引值 0 来表示 + +constant_pool 是一种表结构,以1 ~ constant_pool_count - 1为索引,表明有多少个常量池表项。表项中存放编译时期生成的各种字面量和符号引用,这部分内容将在类加载后进入方法区的运行时常量池 + +* 字面量(Literal) :基本数据类型、字符串类型常量、声明为 final 的常量值等 + +* 符号引用(Symbolic References):类和接口的全限定名、字段的名称和描述符、方法的名称和描述符 + + * 全限定名:com/test/Demo 这个就是类的全限定名,仅仅是把包名的 `.` 替换成 `/`,为了使连续的多个全限定名之间不产生混淆,在使用时最后一般会加入一个 `;` 表示全限定名结束 + + * 简单名称:指没有类型和参数修饰的方法或者字段名称,比如字段 x 的简单名称就是 x + + * 描述符:用来描述字段的数据类型、方法的参数列表(包括数量、类型以及顺序)和返回值 + + | 标志符 | 含义 | + | ------ | --------------------------------------------------------- | + | B | 基本数据类型 byte | + | C | 基本数据类型 char | + | D | 基本数据类型 double | + | F | 基本数据类型 float | + | I | 基本数据类型 int | + | J | 基本数据类型 long | + | S | 基本数据类型 short | + | Z | 基本数据类型 boolean | + | V | 代表 void 类型 | + | L | 对象类型,比如:`Ljava/lang/Object;`,不同方法间用`;`隔开 | + | [ | 数组类型,代表一维数组。比如:`double[][][] is [[[D` | + +常量类型和结构: + +| 类型 | 标志(或标识) | 描述 | +| -------------------------------- | ------------ | ---------------------- | +| CONSTANT_utf8_info | 1 | UTF-8编码的字符串 | +| CONSTANT_Integer_info | 3 | 整型字面量 | +| CONSTANT_Float_info | 4 | 浮点型字面量 | +| CONSTANT_Long_info | 5 | 长整型字面量 | +| CONSTANT_Double_info | 6 | 双精度浮点型字面量 | +| CONSTANT_Class_info | 7 | 类或接口的符号引用 | +| CONSTANT_String_info | 8 | 字符串类型字面量 | +| CONSTANT_Fieldref_info | 9 | 字段的符号引用 | +| CONSTANT_Methodref_info | 10 | 类中方法的符号引用 | +| CONSTANT_InterfaceMethodref_info | 11 | 接口中方法的符号引用 | +| CONSTANT_NameAndType_info | 12 | 字段或方法的符号引用 | +| CONSTANT_MethodHandle_info | 15 | 表示方法句柄 | +| CONSTANT_MethodType_info | 16 | 标志方法类型 | +| CONSTANT_InvokeDynamic_info | 18 | 表示一个动态方法调用点 | + +18 种常量没有出现 byte、short、char,boolean 的原因:编译之后都可以理解为 Integer + + + +**** + + + +##### 访问标识 + +访问标识(access_flag),又叫访问标志、访问标记,该标识用两个字节表示,用于识别一些类或者接口层次的访问信息,包括这个 Class 是类还是接口,是否定义为 public类型,是否定义为 abstract类型等 + +* 类的访问权限通常为 ACC_ 开头的常量 +* 每一种类型的表示都是通过设置访问标记的 32 位中的特定位来实现的,比如若是 public final 的类,则该标记为 `ACC_PUBLIC | ACC_FINAL` +* 使用 `ACC_SUPER` 可以让类更准确地定位到父类的方法,确定类或接口里面的 invokespecial 指令使用的是哪一种执行语义,现代编译器都会设置并且使用这个标记 + +| 标志名称 | 标志值 | 含义 | +| -------------- | ------ | ------------------------------------------------------------ | +| ACC_PUBLIC | 0x0001 | 标志为 public 类型 | +| ACC_FINAL | 0x0010 | 标志被声明为 final,只有类可以设置 | +| ACC_SUPER | 0x0020 | 标志允许使用 invokespecial 字节码指令的新语义,JDK1.0.2之后编译出来的类的这个标志默认为真,使用增强的方法调用父类方法 | +| ACC_INTERFACE | 0x0200 | 标志这是一个接口 | +| ACC_ABSTRACT | 0x0400 | 是否为 abstract 类型,对于接口或者抽象类来说,次标志值为真,其他类型为假 | +| ACC_SYNTHETIC | 0x1000 | 标志此类并非由用户代码产生(由编译器产生的类,没有源码对应) | +| ACC_ANNOTATION | 0x2000 | 标志这是一个注解 | +| ACC_ENUM | 0x4000 | 标志这是一个枚举 | + + + +*** + + + +##### 索引集合 + +类索引、父类索引、接口索引集合 + +* 类索引用于确定这个类的全限定名 + +* 父类索引用于确定这个类的父类的全限定名,Java 语言不允许多重继承,所以父类索引只有一个,除了Object 之外,所有的 Java 类都有父类,因此除了 java.lang.Object 外,所有 Java 类的父类索引都不为0 + +* 接口索引集合就用来描述这个类实现了哪些接口 + * interfaces_count 项的值表示当前类或接口的直接超接口数量 + * interfaces[] 接口索引集合,被实现的接口将按 implements 语句后的接口顺序从左到右排列在接口索引集合中 + +| 长度 | 含义 | +| ---- | ---------------------------- | +| u2 | this_class | +| u2 | super_class | +| u2 | interfaces_count | +| u2 | interfaces[interfaces_count] | + + + +*** + + + +##### 字段表 + +字段 fields 用于描述接口或类中声明的变量,包括类变量以及实例变量,但不包括方法内部、代码块内部声明的局部变量以及从父类或父接口继承。字段叫什么名字、被定义为什么数据类型,都是无法固定的,只能引用常量池中的常量来描述 + +fields_count(字段计数器),表示当前 class 文件 fields 表的成员个数,用两个字节来表示 + +fields[](字段表): + +* 表中的每个成员都是一个 fields_info 结构的数据项,用于表示当前类或接口中某个字段的完整描述 + +* 字段访问标识: + + | 标志名称 | 标志值 | 含义 | + | ------------- | ------ | -------------------------- | + | ACC_PUBLIC | 0x0001 | 字段是否为public | + | ACC_PRIVATE | 0x0002 | 字段是否为private | + | ACC_PROTECTED | 0x0004 | 字段是否为protected | + | ACC_STATIC | 0x0008 | 字段是否为static | + | ACC_FINAL | 0x0010 | 字段是否为final | + | ACC_VOLATILE | 0x0040 | 字段是否为volatile | + | ACC_TRANSTENT | 0x0080 | 字段是否为transient | + | ACC_SYNCHETIC | 0x1000 | 字段是否为由编译器自动产生 | + | ACC_ENUM | 0x4000 | 字段是否为enum | + +* 字段名索引:根据该值查询常量池中的指定索引项即可 + +* 描述符索引:用来描述字段的数据类型、方法的参数列表和返回值 + + | 字符 | 类型 | 含义 | + | ----------- | --------- | ----------------------- | + | B | byte | 有符号字节型树 | + | C | char | Unicode字符,UTF-16编码 | + | D | double | 双精度浮点数 | + | F | float | 单精度浮点数 | + | I | int | 整型数 | + | J | long | 长整数 | + | S | short | 有符号短整数 | + | Z | boolean | 布尔值true/false | + | V | void | 代表void类型 | + | L Classname | reference | 一个名为Classname的实例 | + | [ | reference | 一个一维数组 | + +* 属性表集合:属性个数存放在 attribute_count 中,属性具体内容存放在 attribute 数组中,一个字段还可能拥有一些属性,用于存储更多的额外信息,比如初始化值、一些注释信息等 + + ```java + ConstantValue_attribute{ + u2 attribute_name_index; + u4 attribute_length; + u2 constantvalue_index; + } + ``` + + 对于常量属性而言,attribute_length 值恒为2 + + + +*** + + + +##### 方法表 + +方法表是 methods 指向常量池索引集合,其中每一个 method_info 项都对应着一个类或者接口中的方法信息,完整描述了每个方法的签名 + +* 如果这个方法不是抽象的或者不是 native 的,字节码中就会体现出来 +* methods 表只描述当前类或接口中声明的方法,不包括从父类或父接口继承的方法 +* methods 表可能会出现由编译器自动添加的方法,比如初始化方法 和实例化方法 + +**重载(Overload)**一个方法,除了要与原方法具有相同的简单名称之外,还要求必须拥有一个与原方法不同的特征签名,特征签名就是一个方法中各个参数在常量池中的字段符号引用的集合,因为返回值不会包含在特征签名之中,因此 Java 语言里无法仅仅依靠返回值的不同来对一个已有方法进行重载。但在 Class 文件格式中,特征签名的范围更大一些,只要描述符不是完全一致的两个方法就可以共存 + +methods_count(方法计数器):表示 class 文件 methods 表的成员个数,使用两个字节来表示 + +methods[](方法表):每个表项都是一个 method_info 结构,表示当前类或接口中某个方法的完整描述 + +* 方法表结构如下: + + | 类型 | 名称 | 含义 | 数量 | + | -------------- | ---------------- | ---------- | ---------------- | + | u2 | access_flags | 访问标志 | 1 | + | u2 | name_index | 字段名索引 | 1 | + | u2 | descriptor_index | 描述符索引 | 1 | + | u2 | attrubutes_count | 属性计数器 | 1 | + | attribute_info | attributes | 属性集合 | attributes_count | + +* 方法表访问标志: + + | 标志名称 | 标志值 | 含义 | + | ------------- | ------ | -------------------------- | + | ACC_PUBLIC | 0x0001 | 字段是否为 public | + | ACC_PRIVATE | 0x0002 | 字段是否为 private | + | ACC_PROTECTED | 0x0004 | 字段是否为 protected | + | ACC_STATIC | 0x0008 | 字段是否为 static | + | ACC_FINAL | 0x0010 | 字段是否为 final | + | ACC_VOLATILE | 0x0040 | 字段是否为 volatile | + | ACC_TRANSTENT | 0x0080 | 字段是否为 transient | + | ACC_SYNCHETIC | 0x1000 | 字段是否为由编译器自动产生 | + | ACC_ENUM | 0x4000 | 字段是否为 enum | + + + +*** + + + +##### 属性表 + +属性表集合,指的是 Class 文件所携带的辅助信息,比如该 Class 文件的源文件的名称,以及任何带有 `RetentionPolicy.CLASS` 或者 `RetentionPolicy.RUNTIME` 的注解,这类信息通常被用于 Java 虚拟机的验证和运行,以及 Java 程序的调试。字段表、方法表都可以有自己的属性表,用于描述某些场景专有的信息 + +attributes_ count(属性计数器):表示当前文件属性表的成员个数 + +attributes[](属性表):属性表的每个项的值必须是 attribute_info 结构 + +* 属性的通用格式: + + ```java + ConstantValue_attribute{ + u2 attribute_name_index; //属性名索引 + u4 attribute_length; //属性长度 + u2 attribute_info; //属性表 + } + ``` + +* 属性类型: + + | 属性名称 | 使用位置 | 含义 | + | ------------------------------------- | ------------------ | ------------------------------------------------------------ | + | Code | 方法表 | Java 代码编译成的字节码指令 | + | ConstantValue | 字段表 | final 关键字定义的常量池 | + | Deprecated | 类、方法、字段表 | 被声明为 deprecated 的方法和字段 | + | Exceptions | 方法表 | 方法抛出的异常 | + | EnclosingMethod | 类文件 | 仅当一个类为局部类或者匿名类是才能拥有这个属性,这个属性用于标识这个类所在的外围方法 | + | InnerClass | 类文件 | 内部类列表 | + | LineNumberTable | Code 属性 | Java 源码的行号与字节码指令的对应关系 | + | LocalVariableTable | Code 属性 | 方法的局部变量描述 | + | StackMapTable | Code 属性 | JDK1.6 中新增的属性,供新的类型检查检验器检查和处理目标方法的局部变量和操作数有所需要的类是否匹配 | + | Signature | 类,方法表,字段表 | 用于支持泛型情况下的方法签名 | + | SourceFile | 类文件 | 记录源文件名称 | + | SourceDebugExtension | 类文件 | 用于存储额外的调试信息 | + | Syothetic | 类,方法表,字段表 | 标志方法或字段为编泽器自动生成的 | + | LocalVariableTypeTable | 类 | 使用特征签名代替描述符,是为了引入泛型语法之后能描述泛型参数化类型而添加 | + | RuntimeVisibleAnnotations | 类,方法表,字段表 | 为动态注解提供支持 | + | RuntimelnvisibleAnnotations | 类,方法表,字段表 | 用于指明哪些注解是运行时不可见的 | + | RuntimeVisibleParameterAnnotation | 方法表 | 作用与 RuntimeVisibleAnnotations 属性类似,只不过作用对象为方法 | + | RuntirmelnvisibleParameterAnniotation | 方法表 | 作用与 RuntimelnvisibleAnnotations 属性类似,作用对象哪个为方法参数 | + | AnnotationDefauit | 方法表 | 用于记录注解类元素的默认值 | + | BootstrapMethods | 类文件 | 用于保存 invokeddynanic 指令引用的引导方式限定符 | + + + + + +**** + + + +#### 编译指令 + +##### javac + +javac:编译命令,将 java 源文件编译成 class 字节码文件 + +`javac xx.java` 不会在生成对应的局部变量表等信息,使用 `javac -g xx.java` 可以生成所有相关信息 + + + +**** + + + +##### javap + +javap 反编译生成的字节码文件,根据 class 字节码文件,反解析出当前类对应的 code 区 (字节码指令)、局部变量表、异常表和代码行偏移量映射表、常量池等信息 + +用法:javap + +```sh +-help --help -? 输出此用法消息 +-version 版本信息 +-public 仅显示公共类和成员 +-protected 显示受保护的/公共类和成员 +-package 显示程序包/受保护的/公共类和成员 (默认) +-p -private 显示所有类和成员 + #常用的以下三个 +-v -verbose 输出附加信息 +-l 输出行号和本地变量表 +-c 对代码进行反汇编 #反编译 + +-s 输出内部类型签名 +-sysinfo 显示正在处理的类的系统信息 (路径, 大小, 日期, MD5 散列) +-constants 显示最终常量 +-classpath 指定查找用户类文件的位置 +-cp 指定查找用户类文件的位置 +-bootclasspath 覆盖引导类文件的位置 +``` + + + +*** + + + +#### 指令集 + +##### 执行指令 + +Java 字节码属于 JVM 基本执行指令。由一个字节长度的代表某种操作的操作码(opcode)以及零至多个代表此操作所需参数的操作数(operand)所构成,虚拟机中许多指令并不包含操作数,只有一个操作码(零地址指令) + +由于限制了 Java 虚拟机操作码的长度为一个字节(0~255),所以指令集的操作码总数不可能超过 256 条 + +在 JVM 的指令集中,大多数的指令都包含了其操作所对应的数据类型信息。例如 iload 指令用于从局部变量表中加载 int 型的数据到操作数栈中,而 fload 指令加载的则是 float 类型的数据 + +* i 代表对 int 类型的数据操作 +* l 代表 long +* s 代表 short +* b 代表 byte +* c 代表 char +* f 代表 float +* d 代表 double + +大部分的指令都没有支持 byte、char、short、boolean 类型,编译器会在编译期或运行期将 byte 和 short 类型的数据带符号扩展(Sign-Extend-)为相应的 int 类型数据,将 boolean 和 char 类型数据零位扩展(Zero-Extend)为相应的 int 类型数据 + +在做值相关操作时: + +- 一个指令,可以从局部变量表、常量池、堆中对象、方法调用、系统调用中等取得数据,这些数据(可能是值,也可能是对象的引用)被压入操作数栈 +- 一个指令,也可以从操作数栈中取出一到多个值(pop 多次),完成赋值、加减乘除、方法传参、系统调用等等操作 + + + +*** + + + +##### 加载存储 + +加载和存储指令用于将数据从栈帧的局部变量表和操作数栈之间来回传递 + +局部变量压栈指令:将给定的局部变量表中的数据压入操作数栈 + +* xload、xload_n,x 表示取值数据类型,为 i、l、f、d、a, n 为 0 到 3 +* 指令 xload_n 表示将第 n 个局部变量压入操作数栈,aload_n 表示将一个对象引用压栈 +* 指令 xload n 通过指定参数的形式,把局部变量压入操作数栈,局部变量数量超过 4 个时使用这个命令 + +常量入栈指令:将常数压入操作数栈,根据数据类型和入栈内容的不同,又分为 const、push、ldc 指令 + +* push:包括 bipush 和 sipush,区别在于接收数据类型的不同,bipush 接收 8 位整数作为参数,sipush 接收 16 位整数 +* ldc:如果以上指令不能满足需求,可以使用 ldc 指令,接收一个 8 位的参数,该参数指向常量池中的 int、 float 或者 String 的索引,将指定的内容压入堆栈。ldc_w 接收两个 8 位参数,能支持的索引范围更大,如果要压入的元素是 long 或 double 类型的,则使用 ldc2_w 指令 +* aconst_null 将 null 对象引用压入栈,iconst_m1 将 int 类型常量 -1 压入栈,iconst_0 将 int 类型常量 0 压入栈 + +出栈装入局部变量表指令:将操作数栈中栈顶元素弹出后,装入局部变量表的指定位置,用于给局部变量赋值 + +* xstore、xstore_n,x 表示取值类型为 i、l、f、d、a, n 为 0 到 3 +* xastore 表示存入数组,x 取值为 i、l、f、d、a、b、c、s + +扩充局部变量表的访问索引的指令:wide + + + +**** + + + +##### 算术指令 + +算术指令用于对两个操作数栈上的值进行某种特定运算,并把计算结果重新压入操作数栈 + +没有直接支持 byte、 short、 char 和 boolean 类型的算术指令,对于这些数据的运算,都使用 int 类型的指令来处理,数组类型也是转换成 int 数组 + +* 加法指令:iadd、ladd、fadd、dadd +* 减法指令:isub、lsub、fsub、dsub +* 乘法指令:imu、lmu、fmul、dmul +* 除法指令:idiv、ldiv、fdiv、ddiv +* 求余指令:irem、lrem、frem、drem(remainder 余数) +* 取反指令:ineg、lneg、fneg、dneg (negation 取反) +* 自增指令:iinc(直接**在局部变量 slot 上进行运算**,不用放入操作数栈) +* 位运算指令,又可分为: + - 位移指令:ishl、ishr、 iushr、lshl、lshr、 lushr + - 按位或指令:ior、lor + - 按位与指令:iand、land + - 按位异或指令:ixor、lxor + +* 比较指令:dcmpg、dcmpl、 fcmpg、fcmpl、lcmp + +运算模式: + +* 向最接近数舍入模式,JVM 在进行浮点数计算时,所有的运算结果都必须舍入到适当的精度,非精确结果必须舍入为可被表示的最接近的精确值,如果有两种可表示形式与该值一样接近,将优先选择最低有效位为零的 +* 向零舍入模式:将浮点数转换为整数时,该模式将在目标数值类型中选择一个最接近但是不大于原值的数字作为最精确的舍入结果 + +NaN 值:当一个操作产生溢出时,将会使用有符号的无穷大表示,如果某个操作结果没有明确的数学定义,将使用 NaN 值来表示 + +```java +double j = i / 0.0; +System.out.println(j);//无穷大,NaN: not a number +``` + +**分析 i++**:从字节码角度分析:a++ 和 ++a 的区别是先执行 iload 还是先执行 iinc + +```java + 4 iload_1 //存入操作数栈 + 5 iinc 1 by 1 //自增i++ + 8 istore_3 //把操作数栈没有自增的数据的存入局部变量表 + 9 iinc 2 by 1 //++i +12 iload_2 //加载到操作数栈 +13 istore 4 //存入局部变量表,这个存入没有 _ 符号,_只能到3 +``` + +```java +public class Demo { + public static void main(String[] args) { + int a = 10; + int b = a++ + ++a + a--; + System.out.println(a); //11 + System.out.println(b); //34 + } +} +``` + +判断结果: + +```java +public class Demo { + public static void main(String[] args) { + int i = 0; + int x = 0; + while (i < 10) { + x = x++; + i++; + } + System.out.println(x); // 结果是 0 + } +} +``` + + + +*** + + + +##### 类型转换 + +类型转换指令可以将两种不同的数值类型进行相互转换,除了 boolean 之外的七种类型 + +宽化类型转换: + +* JVM 支持以下数值的宽化类型转换(widening numeric conversion),小范围类型到大范围类型的安全转换 + * 从 int 类型到 long、float 或者 double 类型,对应的指令为 i2l、i2f、i2d + * 从 long 类型到 float、 double 类型,对应的指令为 l2f、l2d + * 从 float 类型到 double 类型,对应的指令为 f2d + +* 精度损失问题 + * 宽化类型转换是不会因为超过目标类型最大值而丢失信息 + * 从 int 转换到 float 或者 long 类型转换到 double 时,将可能发生精度丢失 + +* 从 byte、char 和 short 类型到 int 类型的宽化类型转换实际上是不存在的,JVM 把它们当作 int 处理 + +窄化类型转换: + +* Java 虚拟机直接支持以下窄化类型转换: + * 从 int 类型至 byte、 short 或者 char 类型,对应的指令有 i2b、i2c、i2s + * 从 long 类型到 int 类型,对应的指令有 l2i + * 从 float 类型到 int 或者 long 类型,对应的指令有:f2i、f2l + * 从 double 类型到 int、long 或 float 者类型,对应的指令有 d2i、d2、d2f + +* 精度损失问题: + * 窄化类型转换可能会导致转换结果具备不同的正负号、不同的数量级,转换过程可能会导致数值丢失精度 + * 将一个浮点值窄化转换为整数类型 T(T 限于 int 或 long 类型之一)时,将遵循以下转换规则: + - 如果浮点值是 NaN,那转换结果就是 int 或 long 类型的 0 + - 如果浮点值不是无穷大的话,浮点值使用 IEEE 754 的向零舍入模式取整,获得整数值 v,如果 v 在目标类型 T 的表示范围之内,那转换结果就是 v,否则将根据 v 的符号,转换为 T 所能表示的最大或者最小正数 + + + +*** + + + +##### 创建访问 + +创建指令: + +* 创建类实例指令:new,接收一个操作数指向常量池的索引,表示要创建的类型,执行完成后将对象的引用压入栈 + + ```java + 0: new #2 // class com/jvm/bytecode/Demo + 3: dup + 4: invokespecial #3 // Method "":()V + ``` + + **dup 是复制操作数栈栈顶的内容**,需要两份引用原因: + + - 一个要配合 invokespecial 调用该对象的构造方法 :()V (会消耗掉栈顶一个引用) + - 一个要配合 astore_1 赋值给局部变量 + +* 创建数组的指令:newarray、anewarray、multianewarray + + * newarray:创建基本类型数组 + * anewarray:创建引用类型数组 + * multianewarray:创建多维数组 + +字段访问指令:对象创建后可以通过对象访问指令获取对象实例或数组实例中的字段或者数组元素 + +* 访问类字段(static字段,或者称为类变量)的指令:getstatic、putstatic +* 访问类实例字段(非static字段,或者称为实例变量)的指令:getfield、 putfield + +类型检查指令:检查类实例或数组类型的指令 + +* checkcast:用于检查类型强制转换是否可以进行,如果可以进行 checkcast 指令不会改变操作数栈,否则它会抛出 ClassCastException 异常 + +* instanceof:判断给定对象是否是某一个类的实例,会将判断结果压入操作数栈 + + + + +**** + + + +##### 方法指令 + +方法调用指令:invokevirtual、 invokeinterface、invokespecial、invokestatic、invokedynamic + +**方法调用章节详解** + + + +*** + + + +##### 操作数栈 + +JVM 提供的操作数栈管理指令,可以用于直接操作操作数栈的指令 + +* pop、pop2:将一个或两个元素从栈顶弹出,并且直接废弃 +* dup、dup2,dup_x1、dup2_x1,dup_x2、dup2_x2:复制栈顶一个或两个数值并重新压入栈顶 + +* swap:将栈最顶端的两个 slot 数值位置交换,JVM 没有提供交换两个 64 位数据类型数值的指令 + +* nop:一个非常特殊的指令,字节码为 0x00,和汇编语言中的 nop 一样,表示什么都不做,一般可用于调试、占位等 + + + +*** + + + +##### 控制转移 + + +比较指令:比较栈顶两个元素的大小,并将比较结果入栈 + +* lcmp:比较两个 long 类型值 +* fcmpl:比较两个 float 类型值(当遇到NaN时,返回-1) +* fcmpg:比较两个 float 类型值(当遇到NaN时,返回1) +* dcmpl:比较两个 double 类型值(当遇到NaN时,返回-1) +* dcmpg:比较两个 double 类型值(当遇到NaN时,返回1) + +条件跳转指令: + +| 指令 | 说明 | +| --------- | -------------------------------------------------- | +| ifeq | equals,当栈顶int类型数值等于0时跳转 | +| ifne | not equals,当栈顶in类型数值不等于0时跳转 | +| iflt | lower than,当栈顶in类型数值小于0时跳转 | +| ifle | lower or equals,当栈顶in类型数值小于等于0时跳转 | +| ifgt | greater than,当栈顶int类型数组大于0时跳转 | +| ifge | greater or equals,当栈顶in类型数值大于等于0时跳转 | +| ifnull | 为 null 时跳转 | +| ifnonnull | 不为 null 时跳转 | + +比较条件跳转指令: + +| 指令 | 说明 | +| --------- | --------------------------------------------------------- | +| if_icmpeq | 比较栈顶两 int 类型数值大小(下同),当前者等于后者时跳转 | +| if_icmpne | 当前者不等于后者时跳转 | +| if_icmplt | 当前者小于后者时跳转 | +| if_icmple | 当前者小于等于后者时跳转 | +| if_icmpgt | 当前者大于后者时跳转 | +| if_icmpge | 当前者大于等于后者时跳转 | +| if_acmpeq | 当结果相等时跳转 | +| if_acmpne | 当结果不相等时跳转 | + +多条件分支跳转指令: + +* tableswitch:用于 switch 条件跳转,case 值连续 +* lookupswitch:用于 switch 条件跳转,case 值不连续 + +无条件跳转指令: + +* goto:用来进行跳转到指定行号的字节码 + +* goto_w:无条件跳转(宽索引) + + + + + +*** + + + +##### 异常处理 + +###### 处理机制 + +抛出异常指令:athrow 指令 + +JVM 处理异常(catch 语句)不是由字节码指令来实现的,而是**采用异常表来完成**的 + +* 代码: + + ```java + public static void main(String[] args) { + int i = 0; + try { + i = 10; + } catch (Exception e) { + i = 20; + } finally { + i = 30; + } + } + ``` + +* 字节码: + + * 多出一个 **Exception table** 的结构,**[from, to) 是前闭后开的检测范围**,一旦这个范围内的字节码执行出现异常,则通过 type 匹配异常类型,如果一致,进入 target 所指示行号 + * 11 行的字节码指令 astore_2 是将异常对象引用存入局部变量表的 slot 2 位置,因为异常出现时,只能进入 Exception table 中一个分支,所以局部变量表 slot 2 位置被共用 + + ```java + 0: iconst_0 + 1: istore_1 // 0 -> i ->赋值 + 2: bipush 10 // try 10 放入操作数栈顶 + 4: istore_1 // 10 -> i 将操作数栈顶数据弹出,存入局部变量表的 slot1 + 5: bipush 30 // 【finally】 + 7: istore_1 // 30 -> i + 8: goto 27 // return ----------------------------------- + 11: astore_2 // catch Exceptin -> e ---------------------- + 12: bipush 20 // + 14: istore_1 // 20 -> i + 15: bipush 30 // 【finally】 + 17: istore_1 // 30 -> i + 18: goto 27 // return ----------------------------------- + 21: astore_3 // catch any -> slot 3 ---------------------- + 22: bipush 30 // 【finally】 + 24: istore_1 // 30 -> i + 25: aload_3 // 将局部变量表的slot 3数据弹出,放入操作数栈栈顶 + 26: athrow // throw 抛出异常 + 27: return + Exception table: + // 任何阶段出现任务异常都会执行 finally + from to target type + 2 5 11 Class java/lang/Exception + 2 5 21 any // 剩余的异常类型,比如 Error + 11 15 21 any // 剩余的异常类型,比如 Error + LineNumberTable: ... + LocalVariableTable: + Start Length Slot Name Signature + 12 3 2 e Ljava/lang/Exception; + 0 28 0 args [Ljava/lang/String; + 2 26 1 i I + ``` + + + +*** + + + +###### finally + +finally 中的代码被**复制了 3 份**,分别放入 try 流程,catch 流程以及 catch 剩余的异常类型流程(上节案例) + +* 代码: + + ```java + public static int test() { + try { + return 10; + } finally { + return 20; + } + } + ``` + +* 字节码: + + ```java + 0: bipush 10 // 10 放入栈顶 + 2: istore_0 // 10 -> slot 0 【从栈顶移除了】 + 3: bipush 20 // 20 放入栈顶 + 5: ireturn // 返回栈顶 int(20) + 6: astore_1 // catch any 存入局部变量表的 slot1 + 7: bipush 20 // 20 放入栈顶 + 9: ireturn // 返回栈顶 int(20) + Exception table: + from to target type + 0 3 6 any + ``` + + + +*** + + + +###### return + +* 吞异常 + + ```java + public static int test() { + try { + return 10; + } finally { + return 20; + } + } + ``` + + ```java + 0: bipush 10 // 10 放入栈顶 + 2: istore_0 // 10 -> slot 0 【从栈顶移除了】 + 3: bipush 20 // 20 放入栈顶 + 5: ireturn // 返回栈顶 int(20) + 6: astore_1 // catch any 存入局部变量表的 slot1 + 7: bipush 20 // 20 放入栈顶 + 9: ireturn // 返回栈顶 int(20) + Exception table: + from to target type + 0 3 6 any + ``` + + * 由于 finally 中的 ireturn 被插入了所有可能的流程,因此返回结果以 finally 的为准 + * 字节码中没有 **athrow** ,表明如果在 finally 中出现了 return,会**吞掉异常** + +* 不吞异常 + + ```java + public class Demo { + public static void main(String[] args) { + int result = test(); + System.out.println(result);//10 + } + public static int test() { + int i = 10; + try { + return i;//返回10 + } finally { + i = 20; + } + } + } + ``` + + ```java + 0: bipush 10 // 10 放入栈顶 + 2: istore_0 // 10 赋值给i,放入slot 0 + 3: iload_0 // i(10)加载至操作数栈 + 4: istore_1 // 10 -> slot 1,【暂存至 slot 1,目的是为了固定返回值】 + 5: bipush 20 // 20 放入栈顶 + 7: istore_0 // 20 slot 0 + 8: iload_1 // slot 1(10) 载入 slot 1 暂存的值 + 9: ireturn // 返回栈顶的 int(10) + 10: astore_2 // catch any -> slot 2 存入局部变量表的 slot2 + 11: bipush 20 + 13: istore_0 + 14: aload_2 + 15: athrow // 不会吞掉异常 + Exception table: + from to target type + 3 5 10 any + ``` + + + +*** + + + +##### 同步控制 + +方法级的同步:是隐式的,无须通过字节码指令来控制,它实现在方法调用和返回操作之中,虚拟机可以从方法常量池的方法表结构中的 ACC_SYNCHRONIZED 访问标志得知一个方法是否声明为同步方法 + +方法内指定指令序列的同步:有 monitorenter 和 monitorexit 两条指令来支持 synchronized 关键字的语义 + +* montiorenter:进入并获取对象监视器,即为栈顶对象加锁 +* monitorexit:释放并退出对象监视器,即为栈顶对象解锁 + + + + + + + +*** + + + +#### 执行流程 + +原始 Java 代码: + +```java +public class Demo { + public static void main(String[] args) { + int a = 10; + int b = Short.MAX_VALUE + 1; + int c = a + b; + System.out.println(c); + } +} +``` + +javap -v Demo.class:省略 + +* 常量池载入运行时常量池 + +* 方法区字节码载入方法区 + +* main 线程开始运行,分配栈帧内存:(操作数栈stack=2,局部变量表locals=4) + +* **执行引擎**开始执行字节码 + + `bipush 10`:将一个 byte 压入操作数栈(其长度会补齐 4 个字节),类似的指令 + + * sipush 将一个 short 压入操作数栈(其长度会补齐 4 个字节) + * ldc 将一个 int 压入操作数栈 + * ldc2_w 将一个 long 压入操作数栈(分两次压入,因为 long 是 8 个字节) + * 这里小的数字都是和字节码指令存在一起,超过 short 范围的数字存入了常量池 + + `istore_1`:将操作数栈顶数据弹出,存入局部变量表的 slot 1 + + ![](https://seazean.oss-cn-beijing.aliyuncs.com/img/Java/JVM-字节码执行流程1.png) + + `ldc #3`:从常量池加载 #3 数据到操作数栈 + Short.MAX_VALUE 是 32767,所以 32768 = Short.MAX_VALUE + 1 实际是在编译期间计算完成 + + ![](https://seazean.oss-cn-beijing.aliyuncs.com/img/Java/JVM-字节码执行流程2.png) + + `istore_2`:将操作数栈顶数据弹出,存入局部变量表的 slot 2 + + `iload_1`:将局部变量表的 slot 1 数据弹出,放入操作数栈栈顶 + + `iload_2`:将局部变量表的 slot 2 数据弹出,放入操作数栈栈顶 + + `iadd`:执行相加操作 + + ![](https://seazean.oss-cn-beijing.aliyuncs.com/img/Java/JVM-字节码执行流程3.png) + + `istore_3`:将操作数栈顶数据弹出,存入局部变量表的 slot 3 + + `getstatic #4`:获取静态字段 + + ![](https://seazean.oss-cn-beijing.aliyuncs.com/img/Java/JVM-字节码执行流程4.png) + + `iload_3`: + + ![](https://seazean.oss-cn-beijing.aliyuncs.com/img/Java/JVM-字节码执行流程5.png) + + `invokevirtual #5`: + + * 找到常量池 #5 项 + * 定位到方法区 java/io/PrintStream.println:(I)V 方法 + * **生成新的栈帧**(分配 locals、stack等) + * 传递参数,执行新栈帧中的字节码 + * 执行完毕,弹出栈帧 + * 清除 main 操作数栈内容 + + ![](https://seazean.oss-cn-beijing.aliyuncs.com/img/Java/JVM-字节码执行流程6.png) + + return:完成 main 方法调用,弹出 main 栈帧,程序结束 + + + + + +*** + + + +### 执行引擎 + +#### 基本介绍 + +执行引擎:Java 虚拟机的核心组成部分之一,类加载主要任务是负责装载字节码到其内部,但字节码并不能够直接运行在操作系统之上,需要执行引擎将**字节码指令解释/编译为对应平台上的本地机器指令**,进行执行 + +虚拟机是一个相对于物理机的概念,这两种机器都有代码执行能力: + +* 物理机的执行引擎是直接建立在处理器、缓存、指令集和操作系统层面上 +* 虚拟机的执行引擎是由软件自行实现的,可以不受物理条件制约地定制指令集与执行引擎的结构体系 + +Java 是**半编译半解释型语言**,将解释执行与编译执行二者结合起来进行: + +* 解释器:根据预定义的规范对字节码采用逐行解释的方式执行,将每条字节码文件中的内容翻译为对应平台的本地机器指令执行 +* 即时编译器(JIT : Just In Time Compiler):虚拟机运行时将源代码直接编译成**和本地机器平台相关的机器码**后再执行,并存入 Code Cache,下次遇到相同的代码直接执行,效率高 + + + +*** + + + +#### 执行方式 + +HotSpot VM 采用**解释器与即时编译器并存的架构**,解释器和即时编译器能够相互协作,去选择最合适的方式来权衡编译本地代码和直接解释执行代码的时间 + +HostSpot JVM 的默认执行方式: + +* 当程序启动后,解释器可以马上发挥作用立即执行,省去编译器编译的时间(解释器存在的**必要性**) +* 随着程序运行时间的推移,即时编译器逐渐发挥作用,根据热点探测功能,将有价值的字节码编译为本地机器指令,以换取更高的程序执行效率 + +HotSpot VM 可以通过 VM 参数设置程序执行方式: + +- -Xint:完全采用解释器模式执行程序 +- -Xcomp:完全采用即时编译器模式执行程序。如果即时编译出现问题,解释器会介入执行 +- -Xmixed:采用解释器 + 即时编译器的混合模式共同执行程序 + +![](https://seazean.oss-cn-beijing.aliyuncs.com/img/Java/JVM-执行引擎工作流程.png) + + + +*** + + + +#### 热点探测 + +热点代码:被 JIT 编译器编译的字节码,根据代码被调用执行的频率而定,一个被多次调用的方法或者一个循环次数较多的循环体都可以被称之为热点代码 + +热点探测:JIT 编译器在运行时会针热点代码做出深度优化,将其直接编译为对应平台的本地机器指令进行缓存,以提升程序执行性能 + +JIT 编译在默认情况是异步进行的,当触发某方法或某代码块的优化时,先将其放入编译队列,然后由编译线程进行编译,编译之后的代码放在 CodeCache 中,通过 `-XX:-BackgroundCompilation` 参数可以关闭异步编译 + +* **CodeCache** 用于缓存编译后的机器码、动态生成的代码和本地方法代码 JNI +* 如果 CodeCache 区域被占满,编译器被停用,字节码将不会编译为机器码,应用程序继续运行,但运行性能会降低很多 + +HotSpot VM 采用的热点探测方式是基于计数器的热点探测,为每一个方法都建立 2 个不同类型的计数器:方法调用计数器(Invocation Counter)和回边计数器(BackEdge Counter) + +* 方法调用计数器:用于统计方法被调用的次数,默认阈值在 Client 模式 下是 1500 次,在 Server 模式下是 10000 次(需要进行激进的优化),超过这个阈值,就会触发 JIT 编译,阈值可以通过虚拟机参数 `-XX:CompileThreshold` 设置 + + 工作流程:当一个方法被调用时, 会先检查该方法是否存在被 JIT 编译过的版本,存在则使用编译后的本地代码来执行;如果不存在则将此方法的调用计数器值加 1,然后判断方法调用计数器与回边计数器值之和是否超过方法调用计数器的阈值,如果超过阈值会向即时编译器**提交一个该方法的代码编译请求** + +* 回边计数器:统计一个方法中循环体代码执行的次数,在字节码中控制流向后跳转的指令称为回边 + + 如果一个方法中的循环体需要执行多次,可以优化为为栈上替换,简称 OSR (On StackReplacement) 编译,**OSR 替换循环代码体的入口,C1、C2 替换的是方法调用的入口**,OSR 编译后会出现方法的整段代码被编译了,但是只有循环体部分才执行编译后的机器码,其他部分仍是解释执行 + + + +*** + + + +#### 分层编译 + +HotSpot VM 内嵌两个 JIT 编译器,分别为 Client Compiler 和 Server Compiler,简称 C1 编译器和 C2 编译器 + +C1 编译器会对字节码进行简单可靠的优化,耗时短,以达到更快的编译速度,C1 编译器的优化方法: + +* 方法内联:**将调用的函数代码编译到调用点处**,这样可以减少栈帧的生成,减少参数传递以及跳转过程 + + 方法内联能够消除方法调用的固定开销,任何方法除非被内联,否则调用都会有固定开销,来源于保存程序在该方法中的执行位置,以及新建、压入和弹出新方法所使用的栈帧。 + + ```java + private static int square(final int i) { + return i * i; + } + System.out.println(square(9)); + ``` + + square 是热点方法,会进行内联,把方法内代码拷贝粘贴到调用者的位置: + + ```java + System.out.println(9 * 9); + ``` + + 还能够进行常量折叠(constant folding)的优化: + + ```java + System.out.println(81); + ``` + +* 冗余消除:根据运行时状况进行代码折叠或削除 + +* 内联缓存:是一种加快动态绑定的优化技术(方法调用部分详解) + +C2 编译器进行耗时较长的优化以及激进优化,优化的代码执行效率更高,当激进优化的假设不成立时,再退回使用 C1 编译,这也是使用分层编译的原因 + +C2 的优化主要是在全局层面,逃逸分析是优化的基础:标量替换、栈上分配、同步消除 + +VM 参数设置: + +- -client:指定 Java 虚拟机运行在 Client 模式下,并使用 C1 编译器 +- -server:指定 Java 虚拟机运行在 Server 模式下,并使用 C2 编译器 +- `-server -XX:+TieredCompilation`:在 1.8 之前,分层编译默认是关闭的,可以添加该参数开启 + +分层编译策略 (Tiered Compilation):程序解释执行可以触发 C1 编译,将字节码编译成机器码,加上性能监控,C2 编译会根据性能监控信息进行激进优化,JVM 将执行状态分成了 5 个层次: + +* 0 层,解释执行(Interpreter) + +* 1 层,使用 C1 即时编译器编译执行(不带 profiling) + +* 2 层,使用 C1 即时编译器编译执行(带基本的 profiling) + +* 3 层,使用 C1 即时编译器编译执行(带完全的 profiling) + +* 4 层,使用 C2 即时编译器编译执行(C1 和 C2 协作运行) + + 说明:profiling 是指在运行过程中收集一些程序执行状态的数据,例如方法的调用次数,循环的回边次数等 + + + +参考文章:https://www.jianshu.com/p/20bd2e9b1f03 + + + +*** + + + +### 方法调用 + +#### 方法识别 + +Java 虚拟机识别方法的关键在于类名、方法名以及方法描述符(method descriptor) + +* **方法描述符是由方法的参数类型以及返回类型所构成**,Java 层面叫方法特征签名 +* 在同一个类中,如果同时出现多个名字相同且描述符也相同的方法,那么 Java 虚拟机会在类的验证阶段报错 + +JVM 根据名字和描述符来判断的,只要返回值不一样(方法描述符不一样),其它完全一样,在 JVM 中是允许的,但 Java 语言不允许 + +```java +// 返回值类型不同,编译阶段直接报错 +public static Integer invoke(Object... args) { + return 1; +} +public static int invoke(Object... args) { + return 2; +} +``` + + + +*** + + + +#### 调用机制 + +方法调用并不等于方法执行,方法调用阶段唯一的任务就是**确定被调用方法的版本**,不是方法的具体运行过程 + +在 JVM 中,将符号引用转换为直接引用有两种机制: + +- 静态链接:当一个字节码文件被装载进 JVM 内部时,如果被调用的目标方法在编译期可知,且运行期保持不变,将调用方法的符号引用转换为直接引用的过程称之为静态链接(类加载的解析阶段) +- 动态链接:被调用的方法在编译期无法被确定下来,只能在程序运行期将调用方法的符号引用转换为直接引用,由于这种引用转换过程具备动态性,因此被称为动态链接(初始化后的解析阶段) + +* 对应方法的绑定(分配)机制:静态绑定和动态绑定,编译器已经区分了重载的方法(静态绑定和动态绑定),因此可以认为虚拟机中不存在重载 + +非虚方法: + +- 非虚方法在编译期就确定了具体的调用版本,这个版本在运行时是不可变的 +- 静态方法、私有方法、final 方法、实例构造器、父类方法都是非虚方法 +- 所有普通成员方法、实例方法、被重写的方法都是虚方法 + +动态类型语言和静态类型语言: + +- 在于对类型的检查是在编译期还是在运行期,满足前者就是静态类型语言,反之则是动态类型语言 + +- 静态语言是判断变量自身的类型信息;动态类型语言是判断变量值的类型信息,变量没有类型信息 + +- **Java 是静态类型语言**(尽管 Lambda 表达式为其增加了动态特性),JS,Python 是动态类型语言 + + ```java + String s = "abc"; //Java + info = "abc"; //Python + ``` + + + +*** + + + +#### 调用指令 + +##### 五种指令 + +普通调用指令: + +- invokestatic:调用静态方法 +- invokespecial:调用私有方法、构造器,和父类的实例方法或构造器,以及所实现接口的默认方法 +- invokevirtual:调用所有虚方法(虚方法分派) +- invokeinterface:调用接口方法 + +动态调用指令: + +- invokedynamic:动态解析出需要调用的方法 + - Java7 为了实现动态类型语言支持而引入了该指令,但是并没有提供直接生成 invokedynamic 指令的方法,需要借助 ASM 这种底层字节码工具来产生 invokedynamic 指令 + - Java8 的 lambda 表达式的出现,invokedynamic 指令在 Java 中才有了直接生成方式 + +指令对比: + +- 普通调用指令固化在虚拟机内部,方法的调用执行不可干预,根据方法的符号引用链接到具体的目标方法 +- 动态调用指令支持用户确定方法 +- invokestatic 和 invokespecial 指令调用的方法称为非虚方法,虚拟机能够直接识别具体的目标方法 +- invokevirtual 和 invokeinterface 指令调用的方法称为虚方法,虚拟机需要在执行过程中根据调用者的动态类型来确定目标方法 + +指令说明: + +- 如果虚拟机能够确定目标方法有且仅有一个,比如说目标方法被标记为 final,那么可以不通过动态绑定,直接确定目标方法 +- 普通成员方法是由 invokevirtual 调用,属于**动态绑定**,即支持多态 + + + +*** + + + +##### 符号引用 + +在编译过程中,虚拟机并不知道目标方法的具体内存地址,Java 编译器会暂时用符号引用来表示该目标方法,这一符号引用包括目标方法所在的类或接口的名字,以及目标方法的方法名和方法描述符 + +符号引用存储在方法区常量池中,根据目标方法是否为接口方法,分为接口符号引用和非接口符号引用: + +```java +Constant pool: +... + #16 = InterfaceMethodref #27.#29 // 接口 +... + #22 = Methodref #1.#33 // 非接口 +... +``` + +对于非接口符号引用,假定该符号引用所指向的类为 C,则 Java 虚拟机会按照如下步骤进行查找: + +1. 在 C 中查找符合名字及描述符的方法 +2. 如果没有找到,在 C 的父类中继续搜索,直至 Object 类 +3. 如果没有找到,在 C 所直接实现或间接实现的接口中搜索,这一步搜索得到的目标方法必须是非私有、非静态的。如果有多个符合条件的目标方法,则任意返回其中一个 + +对于接口符号引用,假定该符号引用所指向的接口为 I,则 Java 虚拟机会按照如下步骤进行查找: + +1. 在 I 中查找符合名字及描述符的方法 +2. 如果没有找到,在 Object 类中的公有实例方法中搜索 +3. 如果没有找到,则在 I 的超接口中搜索,这一步的搜索结果的要求与非接口符号引用步骤 3 的要求一致 + + + +*** + + + +##### 执行流程 + +```java +public class Demo { + public Demo() { } + private void test1() { } + private final void test2() { } + + public void test3() { } + public static void test4() { } + + public static void main(String[] args) { + Demo3_9 d = new Demo3_9(); + d.test1(); + d.test2(); + d.test3(); + d.test4(); + Demo.test4(); + } +} +``` + +几种不同的方法调用对应的字节码指令: + +```java +0: new #2 // class cn/jvm/t3/bytecode/Demo +3: dup +4: invokespecial #3 // Method "":()V +7: astore_1 +8: aload_1 +9: invokespecial #4 // Method test1:()V +12: aload_1 +13: invokespecial #5 // Method test2:()V +16: aload_1 +17: invokevirtual #6 // Method test3:()V +20: aload_1 +21: pop +22: invokestatic #7 // Method test4:()V +25: invokestatic #7 // Method test4:()V +28: return +``` + +- invokespecial 调用该对象的构造方法 :()V +- invokevirtual 调用对象的成员方法 +- `d.test4()` 是通过**对象引用**调用一个静态方法,在调用 invokestatic 之前执行了 pop 指令,把对象引用从操作数栈弹掉 + - 不建议使用 `对象.静态方法()` 的方式调用静态方法,多了 aload 和 pop 指令 + - 成员方法与静态方法调用的区别是:执行方法前是否需要对象引用 + + + +*** + + + +#### 多态原理 + +##### 执行原理 + +Java 虚拟机中关于方法重写的判定基于方法描述符,如果子类定义了与父类中非私有、非静态方法同名的方法,只有当这两个方法的参数类型以及返回类型一致,Java 虚拟机才会判定为重写 + +理解多态: + +- 多态有编译时多态和运行时多态,即静态绑定和动态绑定 +- 前者是通过方法重载实现,后者是通过重写实现(子类覆盖父类方法,虚方法表) +- 虚方法:运行时动态绑定的方法,对比静态绑定的非虚方法调用来说,虚方法调用更加耗时 + +方法重写的本质: + +1. 找到操作数栈的第一个元素**所执行的对象的实际类型**,记作 C + +2. 如果在类型 C 中找到与描述符和名称都相符的方法,则进行访问**权限校验**(私有的),如果通过则返回这个方法的直接引用,查找过程结束;如果不通过,则返回 java.lang.IllegalAccessError 异常 + +3. 找不到,就会按照继承关系从下往上依次对 C 的各个父类进行第二步的搜索和验证过程 + +4. 如果始终没有找到合适的方法,则抛出 java.lang.AbstractMethodError 异常 + + + +*** + + + +##### 虚方法表 + +在虚拟机工作过程中会频繁使用到动态绑定,每次动态绑定的过程中都要重新在类的元数据中搜索合适目标,影响到执行效率。为了提高性能,JVM 采取了一种用**空间换取时间**的策略来实现动态绑定,在每个**类的方法区**建立一个虚方法表(virtual method table),实现使用索引表来代替查找,可以快速定位目标方法 + +* invokevirtual 所使用的虚方法表(virtual method table,vtable),执行流程 + 1. 先通过栈帧中的对象引用找到对象,分析对象头,找到对象的实际 Class + 2. Class 结构中有 vtable,查表得到方法的具体地址,执行方法的字节码 +* invokeinterface 所使用的接口方法表(interface method table,itable) + +虚方法表会在类加载的链接阶段被创建并开始初始化,类的变量初始值准备完成之后,JVM 会把该类的方法表也初始化完毕 + +虚方法表的执行过程: + +* 对于静态绑定的方法调用而言,实际引用是一个指向方法的指针 +* 对于动态绑定的方法调用而言,实际引用是方法表的索引值,也就是方法的间接地址。Java 虚拟机获取调用者的实际类型,并在该实际类型的虚方法表中,根据索引值获得目标方法内存偏移量(指针) + +为了优化对象调用方法的速度,方法区的类型信息会增加一个指针,该指针指向一个记录该类方法的方法表。每个类中都有一个虚方法表,本质上是一个数组,每个数组元素指向一个当前类及其祖先类中非私有的实例方法 + +方法表满足以下的特质: + +* 其一,子类方法表中包含父类方法表中的**所有方法**,并且在方法表中的索引值与父类方法表种的索引值相同 +* 其二,**非重写的方法指向父类的方法表项,与父类共享一个方法表项,重写的方法指向本身自己的实现**,这就是为什么多态情况下可以访问父类的方法。 + + + +Passenger 类的方法表包括两个方法,分别对应 0 号和 1 号。方法表调换了 toString 方法和 passThroughImmigration 方法的位置,是因为 toString 方法的索引值需要与 Object 类中同名方法的索引值一致,为了保持简洁,这里不考虑 Object 类中的其他方法。 + +虚方法表对性能的影响: + +* 使用了方法表的动态绑定与静态绑定相比,仅仅多出几个内存解引用操作:访问栈上的调用者、读取调用者的动态类型、读取该类型的方法表、读取方法表中某个索引值所对应的目标方法,但是相对于创建并初始化 Java 栈帧这操作的开销可以忽略不计 +* 上述优化的效果看上去不错,但实际上**仅存在于解释执行**中,或者即时编译代码的最坏情况。因为即时编译还拥有另外两种性能更好的优化手段:内联缓存(inlining cache)和方法内联(method inlining) + +```java +class Person { + public String toString() { + return "I'm a person."; + } + public void eat() {} + public void speak() {} +} + +class Boy extends Person { + public String toString() { + return "I'm a boy"; + } + public void speak() {} + public void fight() {} +} + +class Girl extends Person { + public String toString() { + return "I'm a girl"; + } + public void speak() {} + public void sing() {} +} +``` + +![](https://seazean.oss-cn-beijing.aliyuncs.com/img/Java/JVM-虚方法表指向.png) + + + +参考文档:https://www.cnblogs.com/kaleidoscope/p/9790766.html + + + +*** + + + +##### 内联缓存 + +内联缓存:是一种加快动态绑定的优化技术,能够缓存虚方法调用中**调用者的动态类型以及该类型所对应的目标方法**。在之后的执行过程中,如果碰到已缓存的类型,便会直接调用该类型所对应的目标方法;反之内联缓存则会退化至使用基于方法表的动态绑定 + +多态的三个术语: + +* 单态 (monomorphic):指的是仅有一种状态的情况 +* 多态 (polymorphic):指的是有限数量种状态的情况,二态(bimorphic)是多态的其中一种 +* 超多态 (megamorphic):指的是更多种状态的情况,通常用一个具体数值来区分多态和超多态,在这个数值之下,称之为多态,否则称之为超多态 + +对于内联缓存来说,有对应的单态内联缓存、多态内联缓存: + +* 单态内联缓存:只缓存了一种动态类型以及所对应的目标方法,实现简单,比较所缓存的动态类型,如果命中,则直接调用对应的目标方法。 +* 多态内联缓存:缓存了多个动态类型及其目标方法,需要逐个将所缓存的动态类型与当前动态类型进行比较,如果命中,则调用对应的目标方法 + +为了节省内存空间,**Java 虚拟机只采用单态内联缓存**,没有命中的处理方法: + +* 替换单态内联缓存中的纪录,类似于 CPU 中的数据缓存,对数据的局部性有要求,即在替换内联缓存之后的一段时间内,方法调用的调用者的动态类型应当保持一致,从而能够有效地利用内联缓存 +* 劣化为超多态状态,这也是 Java 虚拟机的具体实现方式,这种状态实际上放弃了优化的机会,将直接访问方法表来动态绑定目标方法,但是与替换内联缓存纪录相比节省了写缓存的额外开销 + +虽然内联缓存附带内联二字,但是并没有内联目标方法 + + + +参考文章:https://time.geekbang.org/column/intro/100010301 + + + +*** + + + +### 代码优化 + +#### 语法糖 + +语法糖:指 Java 编译器把 *.java 源码编译为 *.class 字节码的过程中,自动生成和转换的一些代码,主要是为了减轻程序员的负担 + + + +#### 构造器 + +```java +public class Candy1 { +} +``` + +```java +public class Candy1 { + // 这个无参构造是编译器帮助我们加上的 + public Candy1() { + super(); // 即调用父类 Object 的无参构造方法,即调用 java/lang/Object." + ":()V + } +} +``` + + + +*** + + + +#### 拆装箱 + +```java +Integer x = 1; +int y = x; +``` + +这段代码在 JDK 5 之前是无法编译通过的,必须改写为代码片段2: + +```java +Integer x = Integer.valueOf(1); +int y = x.intValue(); +``` + +JDK5 以后编译阶段自动转换成上述片段 + + + +*** + + + +#### 泛型擦除 + +泛型也是在 JDK 5 开始加入的特性,但 Java 在编译泛型代码后会执行**泛型擦除**的动作,即泛型信息在编译为字节码之后就丢失了,实际的类型都**当做了 Object 类型**来处理: + +```java +List list = new ArrayList<>(); +list.add(10); // 实际调用的是 List.add(Object e) +Integer x = list.get(0); // 实际调用的是 Object obj = List.get(int index); +``` + +编译器真正生成的字节码中,还要额外做一个类型转换的操作: + +```java +// 需要将 Object 转为 Integer +Integer x = (Integer)list.get(0); +``` + +如果前面的 x 变量类型修改为 int 基本类型那么最终生成的字节码是: + +```java +// 需要将 Object 转为 Integer, 并执行拆箱操作 +int x = ((Integer)list.get(0)).intValue(); +``` + + + +*** + + + +#### 可变参数 + +```java +public class Candy4 { + public static void foo(String... args) { + String[] array = args; // 直接赋值 + System.out.println(array); + } + public static void main(String[] args) { + foo("hello", "world"); + } +} +``` + +可变参数 `String... args` 其实是 `String[] args` , Java 编译器会在编译期间将上述代码变换为: + +```java +public static void main(String[] args) { + foo(new String[]{"hello", "world"}); +} +``` + +注意:如果调用了 `foo()` 则等价代码为 `foo(new String[]{})` ,创建了一个空的数组,而不会传递 null 进去 + + + +**** + + + +#### foreach + +数组的循环: + +```java +int[] array = {1, 2, 3, 4, 5}; // 数组赋初值的简化写法也是语法糖 +for (int e : array) { + System.out.println(e); +} +``` + +编译后为循环取数: + +```java +for(int i = 0; i < array.length; ++i) { + int e = array[i]; + System.out.println(e); +} +``` + +集合的循环: + +```java +List list = Arrays.asList(1,2,3,4,5); +for (Integer i : list) { + System.out.println(i); +} +``` + +编译后转换为对迭代器的调用: + +```java +List list = Arrays.asList(1, 2, 3, 4, 5); +Iterator iter = list.iterator(); +while(iter.hasNext()) { + Integer e = (Integer)iter.next(); + System.out.println(e); +} +``` + +注意:foreach 循环写法,能够配合数组以及所有实现了 Iterable 接口的集合类一起使用,其中 Iterable 用来获取集合的迭代器 + + + +*** + + + +#### switch + +##### 字符串 + +switch 可以作用于字符串和枚举类: + +```java +switch (str) { + case "hello": { + System.out.println("h"); + break; + } + case "world": { + System.out.println("w"); + break; + } +} +``` + +注意:**switch 配合 String 和枚举使用时,变量不能为 null** + +会被编译器转换为: + +```java +byte x = -1; +switch(str.hashCode()) { + case 99162322: // hello 的 hashCode + if (str.equals("hello")) { + x = 0; + } + break; + case 113318802: // world 的 hashCode + if (str.equals("world")) { + x = 1; + } +} +switch(x) { + case 0: + System.out.println("h"); + break; + case 1: + System.out.println("w"); + break; +} +``` + +总结: + +* 执行了两遍 switch,第一遍是根据字符串的 hashCode 和 equals 将字符串的转换为相应 byte 类型,第二遍才是利用 byte 执行进行比较 +* hashCode 是为了提高效率,减少可能的比较;而 equals 是为了防止 hashCode 冲突 + + + +*** + + + +##### 枚举 + +switch 枚举的例子,原始代码: + +```java +enum Sex { + MALE, FEMALE +} +public class Candy7 { + public static void foo(Sex sex) { + switch (sex) { + case MALE: + System.out.println("男"); + break; + case FEMALE: + System.out.println("女"); + break; + } + } +} +``` + +编译转换后的代码: + +```java +/** +* 定义一个合成类(仅 jvm 使用,对我们不可见) +* 用来映射枚举的 ordinal 与数组元素的关系 +* 枚举的 ordinal 表示枚举对象的序号,从 0 开始 +* 即 MALE 的 ordinal()=0,FEMALE 的 ordinal()=1 +*/ +static class $MAP { + // 数组大小即为枚举元素个数,里面存储 case 用来对比的数字 + static int[] map = new int[2]; + static { + map[Sex.MALE.ordinal()] = 1; + map[Sex.FEMALE.ordinal()] = 2; + } +} +public static void foo(Sex sex) { + int x = $MAP.map[sex.ordinal()]; + switch (x) { + case 1: + System.out.println("男"); + break; + case 2: + System.out.println("女"); + break; + } +} +``` + + + +*** + + + +#### 枚举类 + +JDK 7 新增了枚举类: + +```java +enum Sex { + MALE, FEMALE +} +``` + +编译转换后: + +```java +public final class Sex extends Enum { + public static final Sex MALE; + public static final Sex FEMALE; + private static final Sex[] $VALUES; + static { + MALE = new Sex("MALE", 0); + FEMALE = new Sex("FEMALE", 1); + $VALUES = new Sex[]{MALE, FEMALE}; + } + private Sex(String name, int ordinal) { + super(name, ordinal); + } + public static Sex[] values() { + return $VALUES.clone(); + } + public static Sex valueOf(String name) { + return Enum.valueOf(Sex.class, name); + } +} +``` + + + + + +*** + + + +#### try-w-r + +JDK 7 开始新增了对需要关闭的资源处理的特殊语法 `try-with-resources`,格式: + +```java +try(资源变量 = 创建资源对象){ +} catch( ) { +} +``` + +其中资源对象需要实现 **AutoCloseable** 接口,例如 InputStream、OutputStream、Connection、Statement、ResultSet 等接口都实现了 AutoCloseable ,使用 try-withresources可以不用写 finally 语句块,编译器会帮助生成关闭资源代码: + +```java +try(InputStream is = new FileInputStream("d:\\1.txt")) { + System.out.println(is); +} catch (IOException e) { + e.printStackTrace(); +} +``` + +转换成: + +`addSuppressed(Throwable e)`:添加被压制异常,是为了防止异常信息的丢失(**fianlly 中如果抛出了异常**) + +```java +try { + InputStream is = new FileInputStream("d:\\1.txt"); + Throwable t = null; + try { + System.out.println(is); + } catch (Throwable e1) { + // t 是我们代码出现的异常 + t = e1; + throw e1; + } finally { + // 判断了资源不为空 + if (is != null) { + // 如果我们代码有异常 + if (t != null) { + try { + is.close(); + } catch (Throwable e2) { + // 如果 close 出现异常,作为被压制异常添加 + t.addSuppressed(e2); + } + } else { + // 如果我们代码没有异常,close 出现的异常就是最后 catch 块中的 e + is.close(); + } + } + } +} catch (IOException e) { + e.printStackTrace(); +} +``` + + + +*** + + + +#### 方法重写 + +方法重写时对返回值分两种情况: + +* 父子类的返回值完全一致 +* 子类返回值可以是父类返回值的子类 + +```java +class A { + public Number m() { + return 1; + } +} +class B extends A { + @Override + // 子类m方法的返回值是Integer是父类m方法返回值Number的子类 + public Integer m() { + return 2; + } +} +``` + +对于子类,Java 编译器会做如下处理: + +```java +class B extends A { + public Integer m() { + return 2; + } + // 此方法才是真正重写了父类 public Number m() 方法 + public synthetic bridge Number m() { + // 调用 public Integer m() + return m(); + } +} +``` + +其中桥接方法比较特殊,仅对 Java 虚拟机可见,并且与原来的 public Integer m() 没有命名冲突 + + + +*** + + + +#### 匿名内部类 + +##### 无参优化 + +源代码: + +```java +public class Candy11 { + public static void main(String[] args) { + Runnable runnable = new Runnable() { + @Override + public void run() { + System.out.println("ok"); + } + }; + } +} +``` + +转化后代码: + +```java +// 额外生成的类 +final class Candy11$1 implements Runnable { + Candy11$1() { + } + public void run() { + System.out.println("ok"); + } +} +public class Candy11 { + public static void main(String[] args) { + Runnable runnable = new Candy11$1(); + } +} +``` + + + +*** + + + +##### 带参优化 + +引用局部变量的匿名内部类,源代码: + +```java +public class Candy11 { + public static void test(final int x) { + Runnable runnable = new Runnable() { + @Override + public void run() { + System.out.println("ok:" + x); + } + }; + } +} +``` + +转换后代码: + +```java +final class Candy11$1 implements Runnable { + int val$x; + Candy11$1(int x) { + this.val$x = x; + } + public void run() { + System.out.println("ok:" + this.val$x); + } +} +public class Candy11 { + public static void test(final int x) { + Runnable runnable = new Candy11$1(x); + } +} +``` + +局部变量在底层创建为内部类的成员变量,必须是 final 的原因: + +* 在 Java 中方法调用是值传递的,在匿名内部类中对变量的操作都是基于原变量的副本,不会影响到原变量的值,所以**原变量的值的改变也无法同步到副本中** + +* 外部变量为 final 是在编译期以强制手段确保用户不会在内部类中做修改原变量值的操作,也是**防止外部操作修改了变量而内部类无法随之变化**出现的影响 + + 在创建 `Candy11$1 ` 对象时,将 x 的值赋值给了 `Candy11$1` 对象的 val 属性,x 不应该再发生变化了,因为发生变化,this.val$x 属性没有机会再跟着变化 + + + +*** + + + +#### 反射优化 + +```java +public class Reflect1 { + public static void foo() { + System.out.println("foo..."); + } + public static void main(String[] args) throws Exception { + Method foo = Reflect1.class.getMethod("foo"); + for (int i = 0; i <= 16; i++) { + System.out.printf("%d\t", i); + foo.invoke(null); + } + System.in.read(); + } +} +``` + +foo.invoke 0 ~ 15 次调用的是 MethodAccessor 的实现类 `NativeMethodAccessorImpl.invoke0()`,本地方法执行速度慢;当调用到第 16 次时,会采用运行时生成的类 `sun.reflect.GeneratedMethodAccessor1` 代替 + +```java +public Object invoke(Object obj, Object[] args)throws Exception { + // inflationThreshold 膨胀阈值,默认 15 + if (++numInvocations > ReflectionFactory.inflationThreshold() + && !ReflectUtil.isVMAnonymousClass(method.getDeclaringClass())) { + MethodAccessorImpl acc = (MethodAccessorImpl) + new MethodAccessorGenerator(). + generateMethod(method.getDeclaringClass(), + method.getName(), + method.getParameterTypes(), + method.getReturnType(), + method.getExceptionTypes(), + method.getModifiers()); + parent.setDelegate(acc); + } + // 【调用本地方法实现】 + return invoke0(method, obj, args); +} +private static native Object invoke0(Method m, Object obj, Object[] args); +``` + +```java +public class GeneratedMethodAccessor1 extends MethodAccessorImpl { + // 如果有参数,那么抛非法参数异常 + block4 : { + if (arrobject == null || arrobject.length == 0) break block4; + throw new IllegalArgumentException(); + } + try { + // 【可以看到,已经是直接调用方法】 + Reflect1.foo(); + // 因为没有返回值 + return null; + } + //.... +} +``` + +通过查看 ReflectionFactory 源码可知: + +* sun.reflect.noInflation 可以用来禁用膨胀,直接生成 GeneratedMethodAccessor1,但首次生成比较耗时,如果仅反射调用一次,不划算 +* sun.reflect.inflationThreshold 可以修改膨胀阈值 + + + + + +*** + + + + + +## 系统优化 + +### 性能调优 + +#### 性能指标 + +性能指标主要是吞吐量、响应时间、QPS、TPS 等、并发用户数等,而这些性能指标又依赖于系统服务器的资源,如 CPU、内存、磁盘 IO、网络 IO 等,对于这些指标数据的收集,通常可以根据Java本身的工具或指令进行查询 + +几个重要的指标: + +1. 停顿时间(响应时间):提交请求和返回该请求的响应之间使用的时间,比如垃圾回收中 STW 的时间 +2. 吞吐量:对单位时间内完成的工作量(请求)的量度(可以对比 GC 的性能指标) +3. 并发数:同一时刻,对服务器有实际交互的请求数 +4. QPS:Queries Per Second,每秒处理的查询量 +5. TPS:Transactions Per Second,每秒产生的事务数 +6. 内存占用:Java 堆区所占的内存大小 + + + +*** + + + +#### 优化步骤 + +对于一个系统要部署上线时,则一定会对 JVM 进行调整,不经过任何调整直接上线,容易出现线上系统频繁 FullGC 造成系统卡顿、CPU 使用频率过高、系统无反应等问题 + +1. 性能监控:通过运行日志、堆栈信息、线程快照等信息监控是否出现 GC 频繁、OOM、内存泄漏、死锁、响应时间过长等情况 + +2. 性能分析: + + * 打印 GC 日志,通过 GCviewer 或者 http://gceasy.io 来分析异常信息 + + - 运用命令行工具、jstack、jmap、jinfo 等 + + - dump 出堆文件,使用内存分析工具分析文件 + + - 使用阿里 Arthas、jconsole、JVisualVM 来**实时查看 JVM 状态** + + - jstack 查看堆栈信息 + +3. 性能调优: + + * 适当增加内存,根据业务背景选择垃圾回收器 + + - 优化代码,控制内存使用 + + - 增加机器,分散节点压力 + + - 合理设置线程池线程数量 + + - 使用中间件提高程序效率,比如缓存、消息队列等 + + + +*** + + + +#### 参数调优 + +对于 JVM 调优,主要就是调整年轻代、老年代、元空间的内存空间大小及使用的垃圾回收器类型 + +* 设置堆的初始大小和最大大小,为了防止垃圾收集器在初始大小、最大大小之间收缩堆而产生额外的时间,通常把最大、初始大小设置为相同的值 + + ```sh + -Xms:设置堆的初始化大小 + -Xmx:设置堆的最大大小 + ``` + +* 设置年轻代中 Eden 区和两个 Survivor 区的大小比例。该值如果不设置,则默认比例为 8:1:1。Java 官方通过增大 Eden 区的大小,来减少 YGC 发生的次数,虽然次数减少了,但 Eden 区满的时候,由于占用的空间较大,导致释放缓慢,此时 STW 的时间较长,因此需要按照程序情况去调优 + + ```sh + -XX:SurvivorRatio + ``` + +* 年轻代和老年代默认比例为 1:2,可以通过调整二者空间大小比率来设置两者的大小。 + + ```sh + -XX:newSize 设置年轻代的初始大小 + -XX:MaxNewSize 设置年轻代的最大大小, 初始大小和最大大小两个值通常相同 + ``` + +* 线程堆栈的设置:**每个线程默认会开启 1M 的堆栈**,用于存放栈帧、调用参数、局部变量等,但一般 256K 就够用,通常减少每个线程的堆栈,可以产生更多的线程,但这实际上还受限于操作系统 + + ```sh + -Xss 对每个线程stack大小的调整,-Xss128k + ``` + +* 一般一天超过一次 FullGC 就是有问题,首先通过工具查看是否出现内存泄露,如果出现内存泄露则调整代码,没有的话则调整 JVM 参数 + +* 系统 CPU 持续飙高的话,首先先排查代码问题,如果代码没问题,则咨询运维或者云服务器供应商,通常服务器重启或者服务器迁移即可解决 + +* 如果数据查询性能很低下的话,如果系统并发量并没有多少,则应更加关注数据库的相关问题 + +* 如果服务器配置还不错,JDK8 开始尽量使用 G1 或者新生代和老年代组合使用并行垃圾回收器 + + + + + +**** + + + + + +### 命令行篇 + +#### jps + +jps(Java Process Statu):显示指定系统内所有的 HotSpot 虚拟机进程(查看虚拟机进程信息),可用于查询正在运行的虚拟机进程,进程的本地虚拟机 ID 与操作系统的进程 ID 是一致的,是唯一的 + +使用语法:`jps [options] [hostid]` + +options 参数: + +- -q:仅仅显示 LVMID(local virtual machine id),即本地虚拟机唯一 id,不显示主类的名称等 + +- -l:输出应用程序主类的全类名或如果进程执行的是 jar 包,则输出 jar 完整路径 + +- -m:输出虚拟机进程启动时传递给主类 main()的参数 + +- -v:列出虚拟机进程启动时的JVM参数,比如 -Xms20m -Xmx50m是启动程序指定的 jvm 参数 + +ostid 参数:RMI注册表中注册的主机名,如果想要远程监控主机上的 java 程序,需要安装 jstatd + + + +**** + + + +#### jstat + +jstat(JVM Statistics Monitoring Tool):用于监视 JVM 各种运行状态信息的命令行工具,可以显示本地或者远程虚拟机进程中的类装载、内存、垃圾收集、JIT编译等运行数据,在没有 GUI 的图形界面,只提供了纯文本控制台环境的服务器上,它是运行期定位虚拟机性能问题的首选工具,常用于检测垃圾回收问题以及内存泄漏问题 + +使用语法:`jstat -