<?xml version="1.0" encoding="UTF-8"?><rss version="2.0" xmlns:content="http://purl.org/rss/1.0/modules/content/"><channel><title>Yu’s Blog</title><description>🎉</description><link>https://yanghc.dev/</link><language>zh_CN</language><item><title>Claude Code Workflows运行机制与大规模Agent编排模式解析</title><link>https://yanghc.dev/posts/abfa528c/</link><guid isPermaLink="true">https://yanghc.dev/posts/abfa528c/</guid><pubDate>Tue, 02 Jun 2026 00:43:00 GMT</pubDate><content:encoded>&lt;p&gt;在 LLM 驱动的自动化工具链中，传统的单窗口或多 Agent 交互模式往往受限于上下文窗口膨胀和状态管理混乱。近日，Claude Code 推出了一项关键更新——Workflows。该机制通过将状态管理与逻辑控制从大模型上下文移至本地脚本，为解决复杂、大规模的 Agent 编排任务提供了一种更具确定性的方案 。&lt;/p&gt;
&lt;h2&gt;传统多Agent编排难以规模化&lt;/h2&gt;
&lt;p&gt;在传统的 Claude 交互或早期的 Sub-agent（子智能体）模式中，用户通常通过一个主会话窗口进行操作 。当需要执行复杂任务时，主窗口作为核心编排器，负责调度不同的子 Agent、运行工具或调用模型能力 。
这种设计在面对大规模或长流程任务时，暴露出以下几项核心缺陷：&lt;/p&gt;
&lt;p&gt;随着对话的持续、工具的频繁调用（如 MCP、Shell 脚本执行）以及长推理过程的加入，主窗口的上下文会迅速被垃圾信息填满 。即使模型拥有高达 1M 的上下文容量，这种饱含中间过程的“冗余上下文”依然会导致模型注意力分散、响应速度变慢，并引发高额的 Token 消耗 。&lt;/p&gt;
&lt;p&gt;当主会话窗口需要同时或先后管理多个子 Agent 时，它必须在自身的上下文中持有所有中间状态 。模型既要追踪哪个子 Agent 正在运行，又要管理它们返回的数据，同时还要决定下一步的执行逻辑 。这种高负载的非确定性管理，极易导致主编排器在规模化场景下丢失上下文或产生逻辑混乱 。&lt;/p&gt;
&lt;h2&gt;改用确定性的静态脚本&lt;/h2&gt;
&lt;p&gt;Workflows 的核心改进在于&lt;strong&gt;解耦“编排控制层”与“模型上下文层”&lt;/strong&gt;，将原本由 LLM 承担的管理职责移交给本地的JS脚本（如 &lt;code&gt;workflow.js&lt;/code&gt;）。&lt;/p&gt;
&lt;p&gt;在 Workflows 架构下，主会话窗口不再直接感知复杂的中间执行过程 。当工作流启动时，系统会为具体任务派生出独立的 Sub-agent 。每个 Sub-agent 拥有完全隔离且干净的上下文窗口 。它们在各自的独立环境里执行数万 Token 级别的工具调用和推理，最终只将压缩后的核心结果投喂回主会话，从而避免了主窗口的上下文膨胀 。&lt;/p&gt;
&lt;p&gt;通过引入 &lt;code&gt;workflow.js&lt;/code&gt; 脚本，工作流的状态不再依赖不稳定的 LLM 记忆，而是记录在确定性的编程语言变量中 。流程控制（如条件分支、Fan-out 并行、Pipeline 串行、重试循环）全部交由代码层面的逻辑来硬性约束 。&lt;/p&gt;
&lt;p&gt;在运行时，Workflow 以独立进程的方式启动，并在脚本与 Sub-agent 之间维护一个名为 &lt;strong&gt;Journal（日志状态流）&lt;/strong&gt; 的中间层 。该机制能够实时记录当前工作流的执行进度与每一步的状态变更 。这意味着，当工作流由于网络、预算触发或人工干预中断时，用户可以安全地暂停并稍后恢复任务，而无需从头重新跑一遍流程、重复消耗 Token 。&lt;/p&gt;
&lt;h2&gt;技术实现与落地细节&lt;/h2&gt;
&lt;h3&gt;如何调用&lt;/h3&gt;
&lt;p&gt;终端输入框输入指令 effort ，选择ultracode模式
&lt;img src=&quot;assets/PixPin_2026-06-02_00-52-38.jpg&quot; alt=&quot;&quot; /&gt;&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;assets/PixPin_2026-06-02_01-26-29.jpg&quot; alt=&quot;&quot; /&gt;&lt;/p&gt;
&lt;h3&gt;脚本分析&lt;/h3&gt;
&lt;p&gt;可以通过自然语言命令（需要在对话中包含 workflow关键字）让 Claude 自动生成对应的控制脚本，或者直接手动编写与修改该配置文件 。&lt;/p&gt;
&lt;p&gt;一个典型的多阶段、多模型混合调度的自动化工作流配置结构示意：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;// workflow.js示例结构
export default {
  name: &quot;brand_identity_forge&quot;,
  [cite_start]description: &quot;演示 Fan-out 并行、Pipeline 串行及多模型混合调度的品牌设计工作流&quot;, [cite: 91]
  
  // 显式划分不同的阶段 (Phases)
  phases: [
    {
      name: &quot;generate&quot;,
      [cite_start]description: &quot;并行头脑风暴生成候选方案&quot;, [cite: 103]
      [cite_start]model: &quot;claude-3-haiku&quot;, // 阶段一：使用低成本、高速度的模型 [cite: 120]
      [cite_start]count: 6, // 启动 6 个并发 Agent [cite: 103]
      [cite_start]prompt: &quot;根据输入主题，快速头脑风暴 3 个富有创意的品牌名称与口号。&quot; [cite: 103]
    },
    {
      name: &quot;critique&quot;,
      [cite_start]description: &quot;对候选方案进行多维度打分审查&quot;, [cite: 104]
      [cite_start]model: &quot;claude-3-5-sonnet&quot;, // 阶段二：使用推理能力较强的标准模型 [cite: 121]
      [cite_start]prompt: &quot;分析上一阶段生成的候选方案，从市场可行性、新颖度等维度进行严格打分，选出最优解。&quot; [cite: 71, 104]
    },
    {
      name: &quot;synthesize&quot;,
      [cite_start]description: &quot;最终方案终审与文档输出&quot;, [cite: 104]
      [cite_start]model: &quot;claude-3-opus&quot;, // 阶段三：使用高理解力的旗舰模型进行统筹 [cite: 121]
      [cite_start]prompt: &quot;针对胜出的方案，完善并输出最终的品牌白皮书与商业规划陈述。&quot; [cite: 104, 105]
    }
  ]
};

&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;通过这种配置，开发者可以在流程初期全量指派给低成本的Haiku 模型，在核心逻辑审计或代码实现部分调用 Sonnet，最后由 Opus 进行全盘校对与文本终审 。这种混合架构在保证全流程质量的同时，能有效控制开销 。&lt;/p&gt;
&lt;h2&gt;大规模编排模式：Deep Research 深度研究工作流&lt;/h2&gt;
&lt;p&gt;Workflows 最具代表性的开箱即用落地场景是 Claude 内置的 &lt;strong&gt;Deep Research（深度研究）能力&lt;/strong&gt; 。该功能通过五阶段的复杂 Agent 网络，展现了大规模编排的典型设计模式 ：&lt;/p&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;&lt;strong&gt;阶段 (Phase)&lt;/strong&gt;&lt;/th&gt;
&lt;th&gt;&lt;strong&gt;执行逻辑与 Agent 规模 TXT&lt;/strong&gt;&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;1. Scope (定义边界)&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;拆解核心诉求。例如将单技术/业务课题拆解为 5 个不同的纵向搜索视角（Angles），由 1 个基准 Agent 运行 。&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;2. Fetch (并发检索)&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;启动 5 个并行的 Web 搜索任务（对应 5 个视角），进行全网数据去重（De-duplicate），并提取前 15 个高质量源的确定性断言或结论 。&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;3. Verify (对抗性验证)&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;消耗资源最大的核心核心阶段。&lt;/strong&gt; 对提取出的每一个核心断言，引入对抗性机制：为每个声明指派 3 个独立的对立验证 Agent 进行联合交叉质询，必须满足“2/3 多数票”通过才能确认为真 。&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;4. Synthesize (合流综合)&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;清洗、去伪存真后，整合所有被证实的论点，开始进行全局的信息合流与文本输出 。&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;p&gt;在实际跑一个关于“特定生物学/医学参数研究”的 Deep Research 任务时，由于 Verify 阶段的对抗性设计，Agent 数量会随着断言数量呈线性几何倍数爆发 。例如在 15 分钟内，系统可能会动态派生出超过 &lt;strong&gt;105 个 Agent&lt;/strong&gt; 进行并发交互，吞吐的 Token 总数可轻易冲破 &lt;strong&gt;300 万次（3.1M Tokens）&lt;/strong&gt; 。&lt;/p&gt;
&lt;h2&gt;总结&lt;/h2&gt;
&lt;p&gt;Workflows 的出现并非为了替代日常的简单对话或轻量级的 Skills 机制 。在商业和开发场景中：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;如果任务是日常、线性的重复动作（如简单的 PR 审查、标准格式转换），&lt;strong&gt;优先使用常规 Skills 机制&lt;/strong&gt;，以确保最低的 Token 开销 。&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;只有在“任务量极大需要分身并发、准确度要求极高需要对抗辩论、耗时极长需要存档续传”的复杂场景下，才值得用 Workflows 静态脚本编排的工作流&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
</content:encoded></item><item><title>记一次Azure Front Door CDN部署经过</title><link>https://yanghc.dev/posts/c2317431/</link><guid isPermaLink="true">https://yanghc.dev/posts/c2317431/</guid><pubDate>Mon, 19 Jan 2026 23:01:38 GMT</pubDate><content:encoded>&lt;p&gt;我有一个运行在本地或云服务器上的 Web 应用（&lt;code&gt;http://127.0.0.1:8088&lt;/code&gt;），希望通过以下方式对外提供服务：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;域名：&lt;code&gt;a.b.com&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;使用 &lt;strong&gt;Azure Front Door&lt;/strong&gt; 作为全球 CDN 加速节点&lt;/li&gt;
&lt;li&gt;启用 HTTPS（免费自动签发证书）&lt;/li&gt;
&lt;li&gt;源站使用 Nginx 反向代理&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;但在实际操作中：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;自定义域名访问报 &lt;strong&gt;证书错误&lt;/strong&gt;&lt;/li&gt;
&lt;li&gt;浏览器提示 &lt;strong&gt;HSTS 安全拦截&lt;/strong&gt;&lt;/li&gt;
&lt;li&gt;不确定 Nginx 如何配置回源&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;配置步骤&lt;/h2&gt;
&lt;p&gt;&lt;img src=&quot;assets/cdn.webp&quot; alt=&quot;&quot; /&gt;&lt;/p&gt;
&lt;h3&gt;步骤 1：在 Cloudflare 配置 DNS&lt;/h3&gt;
&lt;p&gt;确保 DNS 记录正确：&lt;/p&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Name&lt;/th&gt;
&lt;th&gt;Type&lt;/th&gt;
&lt;th&gt;Target&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;a.b.com&lt;/td&gt;
&lt;td&gt;CNAME&lt;/td&gt;
&lt;td&gt;&lt;code&gt;e-d6gafsdfc5aa.z03.azurefd.net&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;相当于是用户在访问原本服务器的时候，其实是通过我的域名解析到了CDN的域名，然后CDN查看缓存，如果存在缓存的话就直接返回，不经过服务器，如果不存在缓存。那就回源到服务器。然后再通过CDN返回给用户。&lt;/td&gt;
&lt;td&gt;&lt;/td&gt;
&lt;td&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;h3&gt;步骤 2：在 Azure Front Door 添加自定义域名&lt;/h3&gt;
&lt;ol&gt;
&lt;li&gt;进入 Azure Portal → 你的 Front Door 资源&lt;/li&gt;
&lt;li&gt;点击 &lt;strong&gt;“域”&lt;/strong&gt;&lt;/li&gt;
&lt;li&gt;点击 &lt;strong&gt;“+ 添加域”&lt;/strong&gt;&lt;/li&gt;
&lt;li&gt;填写：
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;主机名&lt;/strong&gt;：&lt;code&gt;a.b.com&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;启用 HTTPS&lt;/strong&gt;&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;证书类型&lt;/strong&gt;：&lt;strong&gt;Managed certificate&lt;/strong&gt;（免费自动签发）&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;点击 &lt;strong&gt;添加&lt;/strong&gt;&lt;/li&gt;
&lt;/ol&gt;
&lt;h3&gt;步骤 3：完成域名所有权验证&lt;/h3&gt;
&lt;p&gt;Azure 会要求你添加一条 &lt;strong&gt;TXT 记录&lt;/strong&gt; 到 DNS：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;Name&lt;/strong&gt;: &lt;code&gt;_acme-challenge.ecode&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Value&lt;/strong&gt;: &lt;code&gt;xxxxxxxxxxxxxxxxxxxxxx&lt;/code&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;在 Cloudflare 中添加该记录（类型 TXT），保存后等待 Azure 自动检测。&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;验证工具：
dns解析生效检测 https://dnschecker.org/
证书验证 https://www.ssllabs.com/ssltest/analyze.html?d=&lt;/p&gt;
&lt;/blockquote&gt;
&lt;h3&gt;步骤 4：检查路由规则是否关联域名&lt;/h3&gt;
&lt;ol&gt;
&lt;li&gt;进入 &lt;strong&gt;“路由规则”&lt;/strong&gt;&lt;/li&gt;
&lt;li&gt;编辑你的规则&lt;/li&gt;
&lt;li&gt;在 &lt;strong&gt;“前端主机”&lt;/strong&gt; 中，&lt;strong&gt;必须勾选 &lt;code&gt;a.b.com&lt;/code&gt;&lt;/strong&gt;
&lt;ul&gt;
&lt;li&gt;如果只勾了默认域名（&lt;code&gt;xxx.azurefd.net&lt;/code&gt;），自定义域名将被忽略！&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ol&gt;
&lt;h2&gt;Nginx 源站配置（仅用于回源）&lt;/h2&gt;
&lt;p&gt;因为 Azure Front Door 已处理 HTTPS，&lt;strong&gt;Nginx 只需监听 HTTP（80 端口）&lt;/strong&gt;，无需证书。&lt;/p&gt;
&lt;h3&gt;推荐配置：&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;# /etc/nginx/sites-available/origin.b.com
server {
    listen 80;
    server_name origin.b.com;

    # 安全：只允许 Azure Front Door IP 访问
    include /etc/nginx/azure-fd-ips.conf;

    location / {
        proxy_pass http://127.0.0.1:8088/;
        proxy_set_header Host $host;
        proxy_set_header X-Real-IP $remote_addr;
        proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
        proxy_set_header X-Forwarded-Proto $scheme;
    }

    # 禁止直接 IP 访问（防探测）
    server_tokens off;
}

# 默认 server：拦截 IP 直连
server {
    listen 80 default_server;
    return 444;  # 直接关闭连接
}
&lt;/code&gt;&lt;/pre&gt;
&lt;hr /&gt;
&lt;h2&gt;如何获取 Azure Front Door 的出站 IP 范围？&lt;/h2&gt;
&lt;p&gt;微软官方每周更新 IP 列表：&lt;/p&gt;
&lt;p&gt;&lt;a href=&quot;https://www.microsoft.com/en-us/download/details.aspx?id=56519&quot;&gt;https://www.microsoft.com/en-us/download/details.aspx?id=56519&lt;/a&gt;&lt;/p&gt;
&lt;h3&gt;获取步骤：&lt;/h3&gt;
&lt;ol&gt;
&lt;li&gt;下载 &lt;code&gt;ServiceTags_Public_*.json&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;搜索 &lt;code&gt;&quot;name&quot;: &quot;AzureFrontDoor&quot;&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;提取 &lt;code&gt;addressPrefixes&lt;/code&gt; 中的所有 CIDR&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;示例（截至 2024 年）：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;# /etc/nginx/azure-fd-ips.conf
allow 147.243.0.0/16;
allow 52.142.0.0/15;
allow 2a01:4f8::/32;
allow 2a01:4180::/32;
deny all;
&lt;/code&gt;&lt;/pre&gt;
&lt;blockquote&gt;
&lt;p&gt;建议每季度更新一次&lt;/p&gt;
&lt;/blockquote&gt;
&lt;h2&gt;如何防止源站 IP 泄露？&lt;/h2&gt;
&lt;p&gt;即使使用 CDN，仍需防范以下泄露途径：&lt;/p&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;风险点&lt;/th&gt;
&lt;th&gt;防护措施&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;历史 DNS 记录&lt;/td&gt;
&lt;td&gt;避免早期直接解析到 IP&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;SSL 证书绑定主域名&lt;/td&gt;
&lt;td&gt;源站只用 &lt;code&gt;origin.xxx.com&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;子域名未走 CDN&lt;/td&gt;
&lt;td&gt;所有子域名接入 CDN 或删除&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;直接 IP 访问&lt;/td&gt;
&lt;td&gt;Nginx 返回 &lt;code&gt;444&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;响应头泄露&lt;/td&gt;
&lt;td&gt;删除 &lt;code&gt;Server&lt;/code&gt;、&lt;code&gt;X-Powered-By&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;p&gt;自查工具：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;a href=&quot;https://securitytrails.com/&quot;&gt;https://securitytrails.com/&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://dnsdumpster.com/&quot;&gt;https://dnsdumpster.com/&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;误区：为什么“能访问默认域名，但不能访问自定义域名”？&lt;/h2&gt;
&lt;p&gt;发现：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;# 能正常访问
curl https://ecode-d6gaewa5aa.z03.azurefd.net

# 但访问自定义域名失败
curl https://a.b.com  # 报证书错误
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;根本原因：&lt;/h3&gt;
&lt;p&gt;Azure Front Door &lt;strong&gt;不会自动识别你的自定义域名&lt;/strong&gt;。即使 DNS 已 CNAME 到 &lt;code&gt;*.azurefd.net&lt;/code&gt;，如果你&lt;strong&gt;没有在 Azure Portal 中显式添加该域名作为“前端主机”&lt;/strong&gt;，Azure 就会返回一个默认的共享证书（如 &lt;code&gt;*.azurefd.net&lt;/code&gt; 或 &lt;code&gt;*.azureedge.net&lt;/code&gt;）。&lt;/p&gt;
&lt;p&gt;浏览器看到证书是 &lt;code&gt;*.azurefd.net&lt;/code&gt;，但你访问的是 &lt;code&gt;a.b.com&lt;/code&gt; → &lt;strong&gt;证书域名不匹配&lt;/strong&gt; → 拦截连接。&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;结论：&lt;strong&gt;DNS 告诉流量“去哪”，Azure 配置告诉服务器“认谁”。两者缺一不可。&lt;/strong&gt;&lt;/p&gt;
&lt;/blockquote&gt;
&lt;h2&gt;常用命令&lt;/h2&gt;
&lt;pre&gt;&lt;code&gt;# 测试回源
curl -H &quot;Host: origin.b.com&quot; http://127.0.0.1

# 检查证书（绕过本地代理）
curl -UseBasicParsing -NoProxy https://a.b.com

# 查看 DNS 解析
nslookup a.b.com

# 清除 HSTS（Chrome/Edge）
chrome://net-internals/#hsts
&lt;/code&gt;&lt;/pre&gt;
</content:encoded></item><item><title>浅谈古巴 SNET 街头网络</title><link>https://yanghc.dev/posts/3e62b75c/</link><guid isPermaLink="true">https://yanghc.dev/posts/3e62b75c/</guid><pubDate>Fri, 20 Mar 2026 01:03:00 GMT</pubDate><content:encoded>&lt;p&gt;在互联网被视为像水和电一样理所当然的今天，古巴哈瓦那的居民却用双手在城市的上空编织了一张看不见的网。这就是 &lt;strong&gt;SNET (Street Network)&lt;/strong&gt; —— 一个完全由民间自发建立、不依赖国家电信基础设施、甚至一度处于法律灰色地带的&lt;strong&gt;城域无线网状网络 (City-scale Wireless Mesh Network)&lt;/strong&gt;。&lt;/p&gt;
&lt;p&gt;它不仅仅是一个技术项目，更是一场关于连接、生存与社区自治的社会实验。本文将深入 SNET 的肌理，从物理架构到协议逻辑，全方位解析这个“离线互联网”是如何运作的。&lt;/p&gt;
&lt;h2&gt;一、背景：为什么需要 SNET？&lt;/h2&gt;
&lt;p&gt;要理解 SNET，必须先理解古巴的网络环境。&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;高昂的资费&lt;/strong&gt;：在 SNET 兴起之初（约 2011 年），古巴的国际互联网接入极其昂贵，普通人的月收入可能只够买几个小时的流量。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;内容的匮乏&lt;/strong&gt;：国际带宽极窄，导致访问国外网站几乎不可能。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;“每周数据包”的局限&lt;/strong&gt;：当时流行的 &lt;em&gt;El Paquete Semanal&lt;/em&gt; 通过硬盘拷贝分发电影和软件，但这是一种&lt;strong&gt;单向、延迟高&lt;/strong&gt;的分发方式，无法实现人与人之间的实时互动（如聊天、联机游戏）。&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;于是，极客们想到了一个办法：&lt;strong&gt;既然连不上世界，那就先连接彼此。&lt;/strong&gt; 他们利用廉价的民用 Wi-Fi 设备，将一个个孤立的家庭连接成一个巨大的局域网。&lt;/p&gt;
&lt;h2&gt;二、深度解析：分层网状拓扑结构&lt;/h2&gt;
&lt;p&gt;SNET 并非杂乱无章的乱连，经过十余年的演化，它形成了一套严谨的&lt;strong&gt;三层分层架构&lt;/strong&gt;。这种设计是为了解决无线信号传输距离有限和城市建筑遮挡的问题。&lt;/p&gt;
&lt;h3&gt;1. 骨干层 (The Backbone)：城市的天际线高速公路&lt;/h3&gt;
&lt;p&gt;这是 SNET 的大动脉，负责跨街区、跨城区的长距离数据传输。&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;节点位置&lt;/strong&gt;：通常位于城市最高的建筑楼顶、水塔或专门搭建的高杆上。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;硬件配置&lt;/strong&gt;：使用企业级无线网桥（如 Ubiquiti Rocket, Mimosa）配合&lt;strong&gt;高增益定向天线&lt;/strong&gt;（如 24dBi 以上的栅格天线或抛物面天线）。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;频段策略&lt;/strong&gt;：主要使用 &lt;strong&gt;5.8 GHz&lt;/strong&gt; 频段。
&lt;ul&gt;
&lt;li&gt;&lt;em&gt;原因&lt;/em&gt;：5.8GHz 频段干扰少，可用信道多（相比 2.4GHz 只有 3 个非重叠信道，5.8GHz 有十几个），且支持更高的调制率，能提供更大的带宽。&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;连接方式&lt;/strong&gt;：&lt;strong&gt;点对点 (PtP)&lt;/strong&gt; 或 &lt;strong&gt;点对多点 (PtMP)&lt;/strong&gt;。两个骨干节点之间必须保持严格的&lt;strong&gt;视距 (Line of Sight, LOS)&lt;/strong&gt;，中间不能有任何建筑物或树木遮挡。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;作用&lt;/strong&gt;：将哈瓦那的不同区域（如 Centro Habana, Marianao, Playa）连接成一个整体。&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;2. 汇聚/中继层 (Distribution Layer)：社区的枢纽&lt;/h3&gt;
&lt;p&gt;这一层起到承上启下的作用，将骨干网的信号“下沉”到具体的街道和小区。&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;节点位置&lt;/strong&gt;：中等高度的楼房阳台或屋顶。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;硬件配置&lt;/strong&gt;：中端无线路由器，外接扇区天线（覆盖 90°-120°范围）或中等增益的全向天线。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;功能&lt;/strong&gt;：一个汇聚节点可能同时向上连接骨干节点，向下连接数十个家庭用户。它负责数据的聚合与分发。&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;3. 接入层 (Access Layer)：最后一百米&lt;/h3&gt;
&lt;p&gt;这是普通用户接触到的部分。&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;节点位置&lt;/strong&gt;：居民家中。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;硬件配置&lt;/strong&gt;：普通的家用无线路由器（如 TP-Link WR740N 等经典型号），往往刷入了 &lt;strong&gt;OpenWrt&lt;/strong&gt; 或 &lt;strong&gt;DD-WRT&lt;/strong&gt; 等第三方固件以解锁高级功能。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;连接&lt;/strong&gt;：通过全向天线接收来自汇聚层的信号，并通过网线或 Wi-Fi 连接用户的电脑、游戏机。&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;img src=&quot;assets/%E5%8F%A4%E5%B7%B4%E7%BD%91%E7%BB%9C%E6%9E%B6%E6%9E%84.gif&quot; alt=&quot;&quot; /&gt;&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;graph TD
    subgraph &quot;骨干层 (5.8GHz, 定向天线, 长距离)&quot;
    NodeA[&amp;lt;b&amp;gt;骨干节点 A&amp;lt;/b&amp;gt;&amp;lt;br/&amp;gt;市中心高楼]
    NodeB[&amp;lt;b&amp;gt;骨干节点 B&amp;lt;/b&amp;gt;&amp;lt;br/&amp;gt;Marianao 区高楼]
    NodeC[&amp;lt;b&amp;gt;骨干节点 C&amp;lt;/b&amp;gt;&amp;lt;br/&amp;gt;Playa 区高楼]
    NodeA &amp;lt;==|5km 高速链路 |==&amp;gt; NodeB
    NodeB &amp;lt;==|4km 高速链路 |==&amp;gt; NodeC
    end

    subgraph &quot;汇聚/中继层 (混合频段, 扇区/全向)&quot;
    Relay1[&amp;lt;b&amp;gt;汇聚节点 1&amp;lt;/b&amp;gt;&amp;lt;br/&amp;gt;中层建筑]
    Relay2[&amp;lt;b&amp;gt;汇聚节点 2&amp;lt;/b&amp;gt;&amp;lt;br/&amp;gt;中层建筑]
    NodeA --&amp;gt;|下行链路 | Relay1
    NodeB --&amp;gt;|下行链路 | Relay2
    end

    subgraph &quot;接入层 (2.4GHz, 家庭路由)&quot;
    User1((用户 1))
    User2((用户 2))
    User3((用户 3))
    User4((用户 4))
    User5((用户 5))
    
    Relay1 -.-&amp;gt;|Wi-Fi 覆盖 | User1
    Relay1 -.-&amp;gt;|Wi-Fi 覆盖 | User2
    Relay1 -.-&amp;gt;|Wi-Fi 覆盖 | User3
    
    Relay2 -.-&amp;gt;|Wi-Fi 覆盖 | User4
    Relay2 -.-&amp;gt;|Wi-Fi 覆盖 | User5
    end

    style NodeA fill:#f96,stroke:#333,stroke-width:2px
    style NodeB fill:#f96,stroke:#333,stroke-width:2px
    style NodeC fill:#f96,stroke:#333,stroke-width:2px
    style Relay1 fill:#6cf,stroke:#333,stroke-width:2px
    style Relay2 fill:#6cf,stroke:#333,stroke-width:2px
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;三、核心技术深潜：为什么这么设计？&lt;/h2&gt;
&lt;h3&gt;1. 寻址之谜：为何坚持手动静态 IP？&lt;/h3&gt;
&lt;p&gt;在现代网络中，DHCP（动态主机配置协议）是标配，但在 SNET 中，&lt;strong&gt;静态 IP (Static IP)&lt;/strong&gt; 是铁律。这背后有深刻的技术考量：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;路由协议的依赖性&lt;/strong&gt;：SNET 广泛使用 &lt;strong&gt;OLSR (Optimized Link State Routing)&lt;/strong&gt; 协议。OLSR 是一种“先应式”路由协议，每个节点都维护一张全网拓扑图。如果大量节点的 IP 频繁变动（DHCP 租约到期重获新 IP），会导致全网路由表剧烈震荡（Flapping），消耗宝贵的无线带宽用于广播更新，甚至导致网络瘫痪。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;去中心化的困境&lt;/strong&gt;：部署可靠的分布式 DHCP 服务器集群在低带宽、高延迟的无线网状网中极其困难。一旦主服务器宕机，大片区域将无法入网。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;可管理性与故障排查&lt;/strong&gt;：
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;规划&lt;/strong&gt;：社区管理员会按照地理区域划分 IP 段。例如，&lt;code&gt;10.20.5.x&lt;/code&gt; 代表某条街道，&lt;code&gt;10.20.6.x&lt;/code&gt; 代表隔壁街道。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;定位&lt;/strong&gt;：当网络出现环路或攻击时，管理员看到日志中的源 IP &lt;code&gt;10.20.5.12&lt;/code&gt;，立刻就能知道是“那条街的第 12 号节点”出了问题，直接上门解决。如果是动态 IP，这几乎是不可能的任务。&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;2. 频谱战争：信号干扰与“隐藏节点”&lt;/h3&gt;
&lt;p&gt;SNET 面临的最大物理挑战是&lt;strong&gt;无线电干扰&lt;/strong&gt;。&lt;/p&gt;
&lt;h4&gt;问题根源：2.4GHz 的拥堵&lt;/h4&gt;
&lt;p&gt;绝大多数家庭路由器工作在 &lt;strong&gt;2.4 GHz ISM 频段&lt;/strong&gt;。这个频段非常拥挤：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;信道稀缺&lt;/strong&gt;：在 2.4GHz 频段，真正互不干扰的信道只有 &lt;strong&gt;3 个&lt;/strong&gt;（信道 1, 6, 11）。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;高密度冲突&lt;/strong&gt;：在哈瓦那密集的居住区，视野范围内可能有几十个 Wi-Fi 信号。如果大家都自动选择信道，必然大量重叠，导致信噪比（SNR）极低，丢包率飙升。&lt;/li&gt;
&lt;/ul&gt;
&lt;h4&gt;物理难题：隐藏节点 (Hidden Node Problem)&lt;/h4&gt;
&lt;p&gt;这是无线网络特有的噩梦。&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;场景&lt;/strong&gt;：节点 A 和节点 C 都想发送数据给中间的节点 B。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;问题&lt;/strong&gt;：由于距离或障碍物，A 听不到 C 的信号，C 也听不到 A 的信号。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;后果&lt;/strong&gt;：A 觉得信道空闲，开始发送；C 也觉得空闲，也开始发送。两股信号在 B 处发生碰撞，数据全部损坏。B 会要求重传，导致有效吞吐量急剧下降。&lt;/li&gt;
&lt;/ul&gt;
&lt;pre&gt;&lt;code&gt;sequenceDiagram
    participant A as 节点 A (发送者)
    participant B as 节点 B (接收者/冲突点)
    participant C as 节点 C (发送者)
    
    Note over A,C: A 和 C 互相不可见 (隐藏节点)
    
    A-&amp;gt;&amp;gt;B: 检测信道... 空闲!
    C-&amp;gt;&amp;gt;B: 检测信道... 空闲!
    
    A-&amp;gt;&amp;gt;B: 发送数据包 (数据流 1)
    C-&amp;gt;&amp;gt;B: 发送数据包 (数据流 2)
    
    Note over B: 💥 信号碰撞! 数据损坏!
    
    B--&amp;gt;&amp;gt;A: NACK (请求重传)
    B--&amp;gt;&amp;gt;C: NACK (请求重传)
    
    Note over A,C: 带宽浪费在重传上，网速变慢
&lt;/code&gt;&lt;/pre&gt;
&lt;h4&gt;解决方案：人工频谱规划&lt;/h4&gt;
&lt;p&gt;SNET 没有昂贵的自动化射频管理系统，他们靠的是&lt;strong&gt;人&lt;/strong&gt;。&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;区域协调员&lt;/strong&gt;：每个片区都有技术志愿者。他们会带着笔记本电脑和频谱分析软件（如 inSSIDer, Wireshark）爬上屋顶。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;手动调优&lt;/strong&gt;：
&lt;ul&gt;
&lt;li&gt;强制规定相邻节点使用不同的非重叠信道（1, 6, 11 轮换）。&lt;/li&gt;
&lt;li&gt;调整发射功率（Tx Power）：功率不是越大越好，过大的功率会增加噪声底，干扰远处节点。协调员会精确计算每个节点所需的最佳功率。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;骨干升级&lt;/strong&gt;：强制骨干链路迁移至 &lt;strong&gt;5.8 GHz&lt;/strong&gt; 频段，利用其丰富的信道资源避开 2.4 GHz 的“战场”。&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;3. 路由协议：让数据包找到路&lt;/h3&gt;
&lt;p&gt;SNET 的灵魂在于其路由协议，最常用的是 &lt;strong&gt;OLSR (Optimized Link State Routing)&lt;/strong&gt;。&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;工作原理&lt;/strong&gt;：
&lt;ol&gt;
&lt;li&gt;&lt;strong&gt;Hello 消息&lt;/strong&gt;：每个节点定期广播“Hello”包，告诉邻居“我还活着”。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;MPR 选举 (Multi-Point Relays)&lt;/strong&gt;：为了减少广播风暴，节点会选举出一部分“关键中继节点”。只有这些 MPR 节点负责转发拓扑控制 (TC) 消息。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;拓扑构建&lt;/strong&gt;：通过收集 TC 消息，每个节点都能绘制出整个网络的地图。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;路径计算&lt;/strong&gt;：使用 Dijkstra 算法计算到达任意目标 IP 的最短路径（基于链路质量度量，而不仅仅是跳数）。&lt;/li&gt;
&lt;/ol&gt;
&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;优势&lt;/strong&gt;：即使某个节点突然断电，OLSR 能在秒级时间内重新计算路径，绕过故障点，保证网络不中断。&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;四、生态与应用：离线互联网的繁荣&lt;/h2&gt;
&lt;p&gt;在这个没有外网的局域网里，古巴人构建了完整的数字生态：&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;&lt;strong&gt;本地 Web 服务&lt;/strong&gt;：成千上万个运行在个人电脑上的 Apache/Nginx 服务器。内容包括：
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;新闻与博客&lt;/strong&gt;：转载国际新闻（通过离线方式导入）、本地社区公告。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;视频索引站&lt;/strong&gt;：类似 IMDb 的本地数据库，链接到局域网内的 FTP 电影资源。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;论坛与聊天室&lt;/strong&gt;：基于 PHP/MySQL 的本地论坛，是居民交流的主要场所。&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;文件共享&lt;/strong&gt;：
&lt;ul&gt;
&lt;li&gt;巨大的 FTP 服务器集群存储着数 PB 的电影、剧集、软件和电子书。&lt;/li&gt;
&lt;li&gt;内容更新流程：最新的美剧通过“每周数据包”硬盘拷贝进入种子节点 -&amp;gt; 上传至核心服务器 -&amp;gt; 全网高速下载（局域网速度可达 10-50 Mbps，远超当时的国际宽带）。&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;在线游戏&lt;/strong&gt;：
&lt;ul&gt;
&lt;li&gt;CS 1.6, Warcraft III, FIFA, Counter-Strike: Source 等游戏的局域网服务器遍布全网。这是 SNET 早期爆发的主要驱动力。&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;即时通讯&lt;/strong&gt;：
&lt;ul&gt;
&lt;li&gt;开发了专门的局域网聊天客户端（如 &lt;em&gt;SNET Messenger&lt;/em&gt;），支持文字、文件传输，甚至语音，完全不需要互联网。&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ol&gt;
&lt;h2&gt;五、治理与挑战：不仅仅是代码&lt;/h2&gt;
&lt;p&gt;SNET 的成功离不开其独特的&lt;strong&gt;社会架构&lt;/strong&gt;。&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;去中心化自治&lt;/strong&gt;：没有中央控制室。网络由数百个独立的节点所有者维护。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;“宪法”与规范&lt;/strong&gt;：社区自发制定了行为准则：
&lt;ul&gt;
&lt;li&gt;禁止商业盈利（允许分摊电费，但禁止大规模售卖带宽）。&lt;/li&gt;
&lt;li&gt;严禁非法内容（儿童色情、恶意软件）。&lt;/li&gt;
&lt;li&gt;骨干节点承诺高在线率（99%）。&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;与政府的博弈&lt;/strong&gt;：
&lt;ul&gt;
&lt;li&gt;2019 年，古巴政府出台法令，试图将此类民间网络收归国有或取缔，要求所有基础设施必须经过国家电信公司 (ETECSA) 批准。&lt;/li&gt;
&lt;li&gt;这导致大量骨干节点被拆除，SNET 被迫转型、隐蔽化，或部分整合进官方的“国家局域网”项目。但其精神内核——&lt;strong&gt;社区互助与技术赋权&lt;/strong&gt;——依然存活。&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;六、结语&lt;/h2&gt;
&lt;p&gt;古巴 SNET 是人类技术史上一个独特的案例。它证明了：&lt;strong&gt;连接的本质不在于光纤或卫星，而在于人与人之间的协作意愿。&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;在资源极度匮乏、外部环境封闭的条件下，古巴人民利用最廉价的硬件、开源的软件和卓越的社区组织能力，硬生生在城市上空搭建起了一座数字巴别塔。虽然它的形态可能随着政策而变化，但 SNET 所展现的极客精神和韧性，将永远激励着那些相信“网络属于每个人”的人们。&lt;/p&gt;
&lt;p&gt;对于网络工程师而言，SNET 是一本活生生的教科书，讲述了如何在不完美的物理世界中，通过协议优化和人工智慧，构建出鲁棒的通信系统。&lt;/p&gt;
</content:encoded></item><item><title>ThreadLocal详解</title><link>https://yanghc.dev/posts/443887f3/</link><guid isPermaLink="true">https://yanghc.dev/posts/443887f3/</guid><pubDate>Thu, 09 Oct 2025 22:13:55 GMT</pubDate><content:encoded>&lt;h1&gt;ThreadLocal结构&lt;/h1&gt;
&lt;p&gt;&lt;code&gt;ThreadLocal&lt;/code&gt; 的设计在 Java 中是为了实现线程隔离，让每个线程拥有自己的私有变量副本，避免多个线程访问共享数据时的竞争和同步问题。它背后的核心思想是 &lt;strong&gt;线程本地存储&lt;/strong&gt;，即每个线程都可以独立存储自己的数据，线程之间不会互相干扰。&lt;/p&gt;
&lt;h3&gt;1. &lt;strong&gt;核心：&lt;/strong&gt;&lt;/h3&gt;
&lt;p&gt;&lt;code&gt;ThreadLocal&lt;/code&gt; 的作用是为每个线程提供一个独立的变量副本。不同的线程通过 &lt;code&gt;ThreadLocal.get()&lt;/code&gt; 和 &lt;code&gt;ThreadLocal.set()&lt;/code&gt; 方法访问和设置属于自己的变量副本，而不会互相干扰。&lt;/p&gt;
&lt;h3&gt;2. &lt;strong&gt;基本设计：&lt;/strong&gt;&lt;/h3&gt;
&lt;p&gt;需要在每个线程的本地都存一份值，说白了就是每个线程需要有个变量，来存储这些需要本地化资源的值，并且值有可能有多个。在线程对象内部搞个map，把 ThreadLocal 对象自身作为key，把它的值作为 map 的值。这样每个线程可以利用同一个对象作为key，去各自的map中找到对应的值。&lt;/p&gt;
&lt;p&gt;比如现在有 3 个 ThreadLocal 对象，2 个线程。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;ThreadLocal&amp;lt;String&amp;gt; threadLocal1 =  new ThreadLocal&amp;lt;&amp;gt;();
ThreadLocal&amp;lt;Integer&amp;gt; threadLocal2 =  new ThreadLocal&amp;lt;&amp;gt;();
ThreadLocal&amp;lt;Integer&amp;gt; threadLocal3 =  new ThreadLocal&amp;lt;&amp;gt;();
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;ThreadLocal和线程的关系如下：
&lt;img src=&quot;assets/ThreadLocal%E8%AF%A6%E8%A7%A3/IMG-ThreadLocal%E8%AF%A6%E8%A7%A3-20260307011321189.jpg&quot; alt=&quot;&quot; /&gt;&lt;/p&gt;
&lt;p&gt;源码：
在Thread类中，定义了变量&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;/* 与此线程相关的ThreadLocal值。此映射由ThreadLocal类维护。 */
ThreadLocal.ThreadLocalMap threadLocals = null;
&lt;/code&gt;&lt;/pre&gt;
&lt;h4&gt;为什么会出现内存泄漏？&lt;/h4&gt;
&lt;p&gt;如果说我们正常去new thread用完之后正常销毁,那thread类中的thread local map这个属性也就会被GC回收了,不会出现内存泄漏问题,但是项目中都是直接使用线程池,spring boot项目处理请求的时候,也是从线程池中拿出来一个线程,然后去处理请求的线程池中的线程也不会销毁,会一直复用，会一直存在一条引用链&lt;/p&gt;
&lt;h4&gt;为什么key要设置为弱引用&lt;/h4&gt;
&lt;p&gt;避免内存泄漏问题。假设你有一个 &lt;code&gt;ThreadLocal&lt;/code&gt; 对象，它与线程之间建立了强引用关系。如果 &lt;code&gt;key&lt;/code&gt; 是强引用，当线程结束后，它和 &lt;code&gt;key&lt;/code&gt; 会继续保持引用关系，导致 &lt;code&gt;ThreadLocal&lt;/code&gt; 以及 &lt;code&gt;ThreadLocal&lt;/code&gt; 存储的值（&lt;code&gt;value&lt;/code&gt;）不能被 GC 回收。
&lt;img src=&quot;assets/ThreadLocal%E8%AF%A6%E8%A7%A3/IMG-ThreadLocal%E8%AF%A6%E8%A7%A3-20260307011321416.jpg&quot; alt=&quot;&quot; /&gt;
弱引用的引入，确保 &lt;code&gt;key&lt;/code&gt; 对象在没有其他强引用指向它时会被 GC 清理掉，这样就避免了内存泄漏。&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;assets/ThreadLocal%E8%AF%A6%E8%A7%A3/IMG-ThreadLocal%E8%AF%A6%E8%A7%A3-20260307011321622.jpg&quot; alt=&quot;&quot; /&gt;
&lt;code&gt;ThreadLocal&lt;/code&gt; 中的 &lt;strong&gt;&lt;code&gt;key&lt;/code&gt;&lt;/strong&gt; 使用了 &lt;strong&gt;弱引用&lt;/strong&gt;，但这并不意味着 &lt;strong&gt;在垃圾回收（GC）之后，&lt;code&gt;key&lt;/code&gt; 一定会被回收&lt;/strong&gt;。
在 &lt;code&gt;ThreadLocal&lt;/code&gt; 中，&lt;code&gt;key&lt;/code&gt; 是 &lt;code&gt;ThreadLocal&lt;/code&gt; 对象，它是弱引用的。这是为了确保当&lt;code&gt;ThreadLocal&lt;/code&gt; 对象没有强引用时，可以及时被回收，避免因长时间存在于 &lt;code&gt;ThreadLocalMap&lt;/code&gt; 中而造成内存泄漏。只有把threadlocal（key)对象设置为null,没有其它的对象引用，只剩下entry对key的弱引用，GC才能自动回收。&lt;/p&gt;
&lt;p&gt;key虽然为null了，但是&lt;code&gt;ThreadLocalMap&lt;/code&gt; 中的 &lt;code&gt;value&lt;/code&gt;（存储线程本地变量的数据）仍然存在，没有被清除。因为value仍然被Entity引用。为了避免这种情况，JDK 的 &lt;code&gt;ThreadLocalMap&lt;/code&gt; 设计中，每当线程调用 &lt;code&gt;ThreadLocal&lt;/code&gt; 的 &lt;code&gt;get()&lt;/code&gt;、&lt;code&gt;set()&lt;/code&gt; 或 &lt;code&gt;remove()&lt;/code&gt;（为什么我们要手动remove，因为这种自动清理是惰性的，不完全的） 方法时，它都会遍历 &lt;code&gt;ThreadLocalMap&lt;/code&gt; 中的所有条目，&lt;strong&gt;清理掉 &lt;code&gt;key&lt;/code&gt; 为 &lt;code&gt;null&lt;/code&gt; 的无效条目&lt;/strong&gt;。&lt;/p&gt;
&lt;h4&gt;&lt;strong&gt;为什么 &lt;code&gt;value&lt;/code&gt; 不是弱引用？&lt;/strong&gt;&lt;/h4&gt;
&lt;p&gt;&lt;code&gt;value&lt;/code&gt; 需要保持强引用，因为在你调用 &lt;code&gt;ThreadLocal&lt;/code&gt; 的 &lt;code&gt;get&lt;/code&gt; 方法时，通常是希望能获取到当前线程中对应的 &lt;code&gt;value&lt;/code&gt;。如果 &lt;code&gt;value&lt;/code&gt; 是弱引用，一旦没有强引用指向它，它可能会被 GC 回收，导致我们无法获得正确的 &lt;code&gt;value&lt;/code&gt;。因此，&lt;code&gt;value&lt;/code&gt; 需要是强引用，以保证线程在有效期内能继续访问它。&lt;/p&gt;
&lt;h4&gt;&lt;strong&gt;为什么会有内存泄漏，怎么避免？&lt;/strong&gt;&lt;/h4&gt;
&lt;p&gt;内存泄漏的主要原因是当我们使用 &lt;code&gt;ThreadLocal&lt;/code&gt; 时，线程池中的线程会一直存活，线程本身会复用，而 &lt;code&gt;ThreadLocal&lt;/code&gt; 中的 &lt;code&gt;key&lt;/code&gt; 可能没有及时清理，导致内存无法释放。为了避免内存泄漏，开发者需要手动调用 &lt;code&gt;ThreadLocal.remove()&lt;/code&gt; 方法，确保当不再需要 &lt;code&gt;ThreadLocal&lt;/code&gt; 存储的 &lt;code&gt;value&lt;/code&gt; 时，它可以被清除，避免形成不必要的强引用链&lt;/p&gt;
&lt;h3&gt;从源码层面分析 Java 的 ThreadLocal 原理  （下方转载自面试鸭）&lt;/h3&gt;
&lt;p&gt;上面我们说到 Thread 对象里面会有个 ThreadLocalMap，用来保存本地变量。&lt;/p&gt;
&lt;p&gt;我们来看下 jdk 的 Thread 实现：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;public class Thread implements Runnable {  
// 这就是我们说的那个 map 。  
ThreadLocal.ThreadLocalMap threadLocals = null;  
}  
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;可以看到，确实有个 map ，不过这个 map 是 ThreadLocal 的静态内部类，记住这个变量的名字 threadLocals，下面会有用的哈。&lt;/p&gt;
&lt;p&gt;看到这里，想必有很多小伙伴会产生一个疑问。&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;竟然这个 map 是放在 Thread 里面使用，那为什么要定义成 ThreadLocal 的静态内部类呢？&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;首先内部类这个东西是编译层面的概念，就像语法糖一样，经过编译器之后其实内部类会提升为外部顶级类，和平日里外部定义的类没有区别，也就是说在 JVM 中是没有内部类这个概念的。&lt;/p&gt;
&lt;p&gt;一般情况下非静态内部类用在内部类，跟其他类无任何关联，专属于这个外部类使用，并且也便于调用外部类的成员变量和方法，比较方便。&lt;/p&gt;
&lt;p&gt;而静态外部类其实就等于一个顶级类，可以独立于外部类使用，所以更多的只是表明类结构和命名空间。&lt;/p&gt;
&lt;p&gt;所以说这样定义的用意就是说明 ThreadLocalMap 是和 ThreadLocal 强相关的，专用于保存线程本地变量。&lt;/p&gt;
&lt;p&gt;现在我们来看一下 ThreadLocalMap 的定义：&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;assets/ThreadLocal%E8%AF%A6%E8%A7%A3/IMG-ThreadLocal%E8%AF%A6%E8%A7%A3-20260307011321887.png&quot; alt=&quot;&quot; /&gt;&lt;/p&gt;
&lt;p&gt;重点我已经标出来了，首先可以看到这个 ThreadLocalMap 里面有个 Entry 数组，熟悉 HashMap 的小伙伴可能有点感觉了。&lt;/p&gt;
&lt;p&gt;这个 Entry 继承了 WeakReference 即弱引用。这里需要注意，不是说 Entry 自己是弱引用，看到我标注的 Entry 构造函数的 super(k) 没，这个 key 才是弱引用。&lt;/p&gt;
&lt;p&gt;所以 ThreadLocalMap 里有个 Entry 的数组，这个 Entry 的 key 就是 ThreadLocal 对象，value 就是我们需要保存的值。&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;那是如何通过 key 在数组中找到 Entry 然后得到 value 的呢 ？&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;来看下 ThreadLocal 的get 方法，这里就可以得知为什么不同的线程对同一个 ThreadLocal 对象调用 get 方法竟然能得到不同的值了。&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;assets/ThreadLocal%E8%AF%A6%E8%A7%A3/IMG-ThreadLocal%E8%AF%A6%E8%A7%A3-20260307011321953.png&quot; alt=&quot;&quot; /&gt;&lt;/p&gt;
&lt;p&gt;这个中文注释想必很清晰了吧！&lt;code&gt;ThreadLocal#get &lt;/code&gt;方法首先获取当前线程，然后得到当前线程的 ThreadLocalMap 变量即 threadLocals，然后将自己作为 key 从 ThreadLocalMap 中找到 Entry ，最终返回 Entry 里面的 value 值。&lt;/p&gt;
&lt;p&gt;这里我们再看一下 key 是如何从 ThreadLocalMap 中找到 Entry 的，即&lt;code&gt;map.getEntry(this)&lt;/code&gt;是如何实现的，其实很简单。&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;assets/ThreadLocal%E8%AF%A6%E8%A7%A3/IMG-ThreadLocal%E8%AF%A6%E8%A7%A3-20260307011322120.png&quot; alt=&quot;&quot; /&gt;&lt;/p&gt;
&lt;p&gt;可以看到 ThreadLocalMap 虽然和 HashMap 一样，都是基于数组实现的，但是它们对于 Hash 冲突的解决方法不一样，HashMap 是通过链表(红黑树)法来解决冲突，而 ThreadLocalMap 是通过开放寻址法来解决冲突。&lt;/p&gt;
&lt;p&gt;听起来好像很高级，其实道理很简单，我们来看一张图就很清晰了。&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;assets/ThreadLocal%E8%AF%A6%E8%A7%A3/IMG-ThreadLocal%E8%AF%A6%E8%A7%A3-20260307011322210.png&quot; alt=&quot;&quot; /&gt;&lt;/p&gt;
&lt;p&gt;所以说，如果通过 key 的哈希值得到的下标无法直接命中，则会将下标 +1，即继续往后遍历数组查找 Entry ，直到找到或者返回 null。&lt;/p&gt;
&lt;p&gt;可以看到，这种 hash 冲突的解决效率其实不高，但是一般 ThreadLocal 也不会太多，所以用这种简单的办法解决即可。&lt;/p&gt;
&lt;p&gt;至于代码中的 &lt;code&gt;expungeStaleEntry&lt;/code&gt; 我们等下再分析，先来看下 &lt;code&gt;ThreadLocalMap#set&lt;/code&gt; 方法，看看写入的怎样实现的，来看看 hash 冲突的解决方法是否和上面说的一致。&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;assets/ThreadLocal%E8%AF%A6%E8%A7%A3/IMG-ThreadLocal%E8%AF%A6%E8%A7%A3-20260307011322275.png&quot; alt=&quot;&quot; /&gt;&lt;/p&gt;
&lt;p&gt;可以看到 set 的逻辑也很清晰，先通过 key 的 hash 值计算出一个数组下标，然后看看这个下标是否被占用了，如果被占了看看是否就是要找的 Entry ，如果是则进行更新，如果不是则下标++，即往后遍历数组，查找下一个位置，找到空位就 new 个 Entry 然后把坑给占用了。&lt;/p&gt;
&lt;p&gt;当然，这种数组操作一般免不了阈值的判断，如果超过阈值则需要进行扩容。&lt;/p&gt;
&lt;p&gt;上面的清理操作和 key 为空的情况，下面再做分析，这里先略过。&lt;/p&gt;
&lt;p&gt;至此，我们已经分析了 ThreadLocalMap 的&lt;strong&gt;核心操作 get 和 set&lt;/strong&gt; ，想必你对 ThreadLocalMap 的原理已经从源码层面清晰了！&lt;/p&gt;
&lt;p&gt;可能有些小伙伴对 key 的哈希值的来源有点疑惑，所以我再来补充一下 &lt;code&gt;key.threadLocalHashCode&lt;/code&gt;的分析。&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;assets/ThreadLocal%E8%AF%A6%E8%A7%A3/IMG-ThreadLocal%E8%AF%A6%E8%A7%A3-20260307011322341.png&quot; alt=&quot;&quot; /&gt;&lt;/p&gt;
&lt;p&gt;可以看到 &lt;code&gt;key.threadLocalHashCode&lt;/code&gt; 其实就是调用 &lt;code&gt;nextHashCode&lt;/code&gt; 进行一个原子类的累加。&lt;/p&gt;
&lt;p&gt;注意看上面都是静态变量和静态方法，所以在 ThreadLocal 对象之间是共享的，然后通过固定累加一个奇怪的数字&lt;code&gt;0x61c88647&lt;/code&gt;来分配 hash 值。&lt;/p&gt;
&lt;p&gt;这个数字当然不是乱写的，是实验证明的一个值，即通过 0x61c88647 累加生成的值与 2 的幂取模的结果，可以较为均匀地分布在 2 的幂长度的数组中，这样可以减少 hash 冲突。&lt;/p&gt;
&lt;p&gt;参考：https://www.mianshiya.com/question/1780933295064772609
【面试官内心os：坏了，这小子是真懂ThreadLocal】 https://www.bilibili.com/video/BV13y7hzvEKa/?share_source=copy_web&amp;amp;vd_source=2742e5dda987a0c5a7869bfc88246163&lt;/p&gt;
</content:encoded></item><item><title>Spring Bean生命周期</title><link>https://yanghc.dev/posts/9f2c44e1/</link><guid isPermaLink="true">https://yanghc.dev/posts/9f2c44e1/</guid><pubDate>Mon, 15 Sep 2025 13:41:43 GMT</pubDate><content:encoded>&lt;h1&gt;Spring Bean 的生命周期&lt;/h1&gt;
&lt;p&gt;其实就是 &lt;strong&gt;一个 Bean 从创建、初始化、使用、销毁&lt;/strong&gt; 的完整过程。Spring 容器（&lt;code&gt;ApplicationContext&lt;/code&gt;）在管理 Bean 的过程中，会按照固定的流程来操作。下面我给你按顺序梳理一下：&lt;/p&gt;
&lt;hr /&gt;
&lt;h3&gt;1. &lt;strong&gt;实例化 Instantiation&lt;/strong&gt;&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;Spring 容器启动时，根据配置（&lt;code&gt;@Component&lt;/code&gt;、&lt;code&gt;@Bean&lt;/code&gt;、XML等）&lt;strong&gt;通过反射调用无参构造方法&lt;/strong&gt;创建 Bean 实例。&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;如果需要参数注入，则走 &lt;strong&gt;构造器注入&lt;/strong&gt; 或 &lt;strong&gt;工厂方法&lt;/strong&gt;。&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;hr /&gt;
&lt;h3&gt;2. &lt;strong&gt;属性赋值 Populate Properties&lt;/strong&gt;&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;Spring 会通过反射或 &lt;code&gt;setXxx()&lt;/code&gt; 方法，把配置文件中的属性值、依赖的 Bean 注入进去。&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;这一步也就是 &lt;strong&gt;依赖注入（DI）&lt;/strong&gt;。&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;hr /&gt;
&lt;h3&gt;3. &lt;strong&gt;Aware 接口回调&lt;/strong&gt;&lt;/h3&gt;
&lt;p&gt;如果 Bean 实现了某些 &lt;code&gt;Aware&lt;/code&gt; 接口，Spring 会把容器信息注入进去：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;&lt;code&gt;BeanNameAware&lt;/code&gt;：注入当前 Bean 的名字。&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;code&gt;BeanFactoryAware&lt;/code&gt;：注入当前 BeanFactory。&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;code&gt;ApplicationContextAware&lt;/code&gt;：注入 ApplicationContext。&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;hr /&gt;
&lt;h3&gt;4. &lt;strong&gt;BeanPostProcessor 前置处理&lt;/strong&gt;&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;如果容器中有 &lt;code&gt;BeanPostProcessor&lt;/code&gt;，会调用它的&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;postProcessBeforeInitialization(bean, beanName)
&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;在这里你可以对 Bean 做一些“加工处理”。&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;hr /&gt;
&lt;h3&gt;5. &lt;strong&gt;初始化 Initialization&lt;/strong&gt;&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;如果 Bean 实现了 &lt;code&gt;InitializingBean&lt;/code&gt; 接口，会执行其 &lt;code&gt;afterPropertiesSet()&lt;/code&gt; 方法。&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;如果 Bean 配置了 &lt;code&gt;init-method&lt;/code&gt;，Spring 会调用这个方法。&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;👉 这一步就是 Bean 的 &lt;strong&gt;初始化阶段&lt;/strong&gt;。&lt;/p&gt;
&lt;hr /&gt;
&lt;h3&gt;6. &lt;strong&gt;BeanPostProcessor 后置处理&lt;/strong&gt;&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;初始化完成后，再次经过 &lt;code&gt;BeanPostProcessor&lt;/code&gt; 的&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;postProcessAfterInitialization(bean, beanName)
&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;常见用法：&lt;strong&gt;AOP 动态代理&lt;/strong&gt; 就是在这里织入的。&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;hr /&gt;
&lt;h3&gt;7. &lt;strong&gt;使用阶段 Bean in Use&lt;/strong&gt;&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;此时 Bean 就交给应用程序使用。&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;在整个 Spring 容器运行期间，Bean 会根据作用域（&lt;code&gt;singleton&lt;/code&gt;、&lt;code&gt;prototype&lt;/code&gt; 等）被使用。&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;hr /&gt;
&lt;h3&gt;8. &lt;strong&gt;销毁 Destruction&lt;/strong&gt;&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;当容器关闭时（&lt;code&gt;context.close()&lt;/code&gt;）：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;如果 Bean 实现了 &lt;code&gt;DisposableBean&lt;/code&gt;，会调用其 &lt;code&gt;destroy()&lt;/code&gt; 方法。&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;如果配置了 &lt;code&gt;destroy-method&lt;/code&gt;，Spring 会调用该方法。&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;👉 &lt;strong&gt;注意&lt;/strong&gt;：&lt;code&gt;prototype&lt;/code&gt; 作用域的 Bean 不会被 Spring 管理销毁，需要你自己手动清理。&lt;/p&gt;
&lt;hr /&gt;
&lt;h2&gt;总结（完整流程图）&lt;/h2&gt;
&lt;p&gt;&lt;strong&gt;实例化 → 属性注入 → Aware 回调 → BeanPostProcessor 前置 → 初始化 → BeanPostProcessor 后置 → 使用 → 销毁&lt;/strong&gt;&lt;/p&gt;
&lt;h2&gt;&lt;strong&gt;Aware 回调 → BeanPostProcessor 前置 → 初始化 → BeanPostProcessor 后置&lt;/strong&gt;&lt;/h2&gt;
&lt;p&gt;这四步其实就是 &lt;strong&gt;Spring 在 Bean 创建完成之后，到真正可以投入使用之前&lt;/strong&gt; 的一个关键环节。&lt;br /&gt;
我给你拆开解释一下每一步是“干什么的”：&lt;/p&gt;
&lt;hr /&gt;
&lt;h3&gt;1. &lt;strong&gt;Aware 回调&lt;/strong&gt;&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;目的&lt;/strong&gt;：让 Bean 拿到 Spring 容器里的“系统级资源”。&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;怎么用&lt;/strong&gt;：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;如果一个类实现了 &lt;code&gt;BeanNameAware&lt;/code&gt;，Spring 会把当前 Bean 在容器中的名字传进去。&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;如果实现了 &lt;code&gt;ApplicationContextAware&lt;/code&gt;，就能直接拿到整个 &lt;code&gt;ApplicationContext&lt;/code&gt;，相当于可以在 Bean 内部操作 Spring 容器。&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;作用&lt;/strong&gt;：提供一个“入口”，让 Bean 感知到自己在容器中的一些信息。&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;hr /&gt;
&lt;h3&gt;2. &lt;strong&gt;BeanPostProcessor 前置处理&lt;/strong&gt;&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;方法&lt;/strong&gt;：&lt;code&gt;postProcessBeforeInitialization(Object bean, String beanName)&lt;/code&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;什么时候调用&lt;/strong&gt;：在执行 Bean 的初始化方法之前。&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;作用&lt;/strong&gt;：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;可以对 Bean 进行“增强”或者额外处理。&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;比如：给某些字段赋默认值、打印日志，甚至替换 Bean。&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;典型场景&lt;/strong&gt;：Spring 内置的注解处理（如 &lt;code&gt;@Autowired&lt;/code&gt;、&lt;code&gt;@Resource&lt;/code&gt;）就依赖于这个阶段。&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;hr /&gt;
&lt;h3&gt;3. &lt;strong&gt;初始化 (Initialization)&lt;/strong&gt;&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;方法&lt;/strong&gt;：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;如果实现了 &lt;code&gt;InitializingBean&lt;/code&gt; 接口 → 执行 &lt;code&gt;afterPropertiesSet()&lt;/code&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;如果配置了 &lt;code&gt;init-method&lt;/code&gt; → 调用指定方法&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;作用&lt;/strong&gt;：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;一般用来做一些 &lt;strong&gt;资源准备工作&lt;/strong&gt;，比如建立数据库连接、开启定时任务、加载缓存数据等。&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;比喻&lt;/strong&gt;：这一步就是“启动前的自检和准备”。&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;hr /&gt;
&lt;h3&gt;4. &lt;strong&gt;BeanPostProcessor 后置处理&lt;/strong&gt;&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;方法&lt;/strong&gt;：&lt;code&gt;postProcessAfterInitialization(Object bean, String beanName)&lt;/code&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;什么时候调用&lt;/strong&gt;：在 Bean 完成初始化之后。&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;作用&lt;/strong&gt;：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;可以对 Bean 再次加工，最常见的是 &lt;strong&gt;AOP（切面代理）&lt;/strong&gt;。&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;Spring 在这里会给需要增强的 Bean 包一层代理对象（比如事务增强、日志增强）。&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;结果&lt;/strong&gt;：最后交给你使用的 Bean，可能是一个代理类而不是原始对象。&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;hr /&gt;
&lt;p&gt;✅ &lt;strong&gt;整体理解&lt;/strong&gt;&lt;br /&gt;
这四步就像是：&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;Aware 回调&lt;/strong&gt; → 给你一张“地图”，告诉你自己在哪，周围有什么。&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;前置处理&lt;/strong&gt; → 在初始化前帮你“化妆、调整”。&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;初始化&lt;/strong&gt; → 正式“开机”，做启动必备的准备。&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;后置处理&lt;/strong&gt; → 加装“外挂/插件”，比如 AOP 增强。&lt;/p&gt;
&lt;/li&gt;
&lt;/ol&gt;
&lt;h1&gt;Spring 作用域&lt;/h1&gt;
&lt;h2&gt;常见的 Spring Bean 作用域&lt;/h2&gt;
&lt;ol&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;singleton（单例，默认值）&lt;/strong&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;整个 Spring 容器里只存在该 Bean 的 &lt;strong&gt;一个实例&lt;/strong&gt;。&lt;/li&gt;
&lt;li&gt;每次 &lt;code&gt;getBean()&lt;/code&gt; 返回的都是同一个对象。&lt;/li&gt;
&lt;li&gt;生命周期：
&lt;ul&gt;
&lt;li&gt;容器启动时创建（默认行为）。&lt;/li&gt;
&lt;li&gt;容器关闭时销毁。&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;prototype（原型）&lt;/strong&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;每次 &lt;code&gt;getBean()&lt;/code&gt; 或 &lt;code&gt;@Autowired&lt;/code&gt; 注入，都会返回一个 &lt;strong&gt;新的 Bean 实例&lt;/strong&gt;&lt;/li&gt;
&lt;li&gt;生命周期：
&lt;ul&gt;
&lt;li&gt;创建：每次获取时才创建。&lt;/li&gt;
&lt;li&gt;销毁：Spring &lt;strong&gt;不负责&lt;/strong&gt;管理销毁（需要你自己管理）。&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;request&lt;/strong&gt;（仅 Web 环境）&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;每次 HTTP 请求都会创建一个新的 Bean。&lt;/li&gt;
&lt;li&gt;同一个请求内是同一个实例，不同请求是不同实例。&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;session&lt;/strong&gt;（仅 Web 环境）&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;每个 HTTP Session 创建一个新的 Bean。&lt;/li&gt;
&lt;li&gt;同一个 Session 内共享，不同 Session 不同。&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;application&lt;/strong&gt;（仅 Web 环境）&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;整个 &lt;code&gt;ServletContext&lt;/code&gt; 生命周期内只有一个 Bean 实例。&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;websocket&lt;/strong&gt;（仅 WebSocket 环境）&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;每个 WebSocket 会话对应一个 Bean 实例。&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ol&gt;
&lt;hr /&gt;
&lt;h2&gt;使用方式&lt;/h2&gt;
&lt;p&gt;在 Bean 上加上 &lt;code&gt;@Scope&lt;/code&gt; 注解即可：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;import org.springframework.context.annotation.Scope;
import org.springframework.stereotype.Component;

@Component
@Scope(&quot;prototype&quot;)  // 原型作用域
public class PrototypeBean {
    public PrototypeBean() {
        System.out.println(&quot;PrototypeBean 实例化: &quot; + this);
    }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;1. &lt;strong&gt;singleton（单例，默认）&lt;/strong&gt;&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;特点&lt;/strong&gt;：整个容器中只有一个 Bean 实例。&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;适用场景&lt;/strong&gt;：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;Bean &lt;strong&gt;无状态&lt;/strong&gt;或者共享状态，不依赖特定用户或请求。&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;Bean 初始化开销大，但可重复使用。&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;示例&lt;/strong&gt;：&lt;/p&gt;
&lt;p&gt;&lt;code&gt;@Component public class ConfigService {     // 存储系统配置     private Map&amp;lt;String, String&amp;gt; configs = new HashMap&amp;lt;&amp;gt;();     public String getConfig(String key) { return configs.get(key); }     public void setConfig(String key, String value) { configs.put(key, value); } }&lt;/code&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;说明&lt;/strong&gt;：配置类通常整个应用都共享，所以用单例。&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;hr /&gt;
&lt;h2&gt;2. &lt;strong&gt;prototype（原型）&lt;/strong&gt;&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;特点&lt;/strong&gt;：每次获取都会生成新的实例，Spring 不管理销毁。&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;适用场景&lt;/strong&gt;：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;Bean &lt;strong&gt;有状态&lt;/strong&gt;，每个用户或操作需要独立实例。&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;Bean 生命周期短或者属于临时对象。&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;示例&lt;/strong&gt;：&lt;/p&gt;
&lt;p&gt;&lt;code&gt;@Component @Scope(&quot;prototype&quot;) public class ShoppingCart {     private List&amp;lt;String&amp;gt; items = new ArrayList&amp;lt;&amp;gt;();     public void addItem(String item) { items.add(item); }     public List&amp;lt;String&amp;gt; getItems() { return items; } }&lt;/code&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;说明&lt;/strong&gt;：每个用户都需要一个独立购物车，不能共享。&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;hr /&gt;
&lt;h2&gt;3. &lt;strong&gt;request（请求）&lt;/strong&gt; — Web 环境&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;特点&lt;/strong&gt;：每个 HTTP 请求会创建一个新的 Bean 实例。&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;适用场景&lt;/strong&gt;：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;用户请求级别的数据&lt;/strong&gt;，例如请求参数封装、临时计算结果。&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;示例&lt;/strong&gt;：&lt;/p&gt;
&lt;p&gt;&lt;code&gt;@Component @Scope(&quot;request&quot;) public class RequestLogger {     private long requestStartTime = System.currentTimeMillis();     public long getStartTime() { return requestStartTime; } }&lt;/code&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;说明&lt;/strong&gt;：每次请求都有独立日志，不会互相覆盖。&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;hr /&gt;
&lt;h2&gt;4. &lt;strong&gt;session（会话）&lt;/strong&gt; — Web 环境&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;特点&lt;/strong&gt;：每个 HTTP Session 会话创建一个 Bean。&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;适用场景&lt;/strong&gt;：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;用户登录后保存&lt;strong&gt;会话级状态&lt;/strong&gt;。&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;示例&lt;/strong&gt;：&lt;/p&gt;
&lt;p&gt;&lt;code&gt;@Component @Scope(&quot;session&quot;) public class UserSession {     private String username;     public void setUsername(String username) { this.username = username; }     public String getUsername() { return username; } }&lt;/code&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;说明&lt;/strong&gt;：每个用户登录后，保存自己的 session 数据。&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;hr /&gt;
&lt;h2&gt;总结：&lt;/h2&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;作用域&lt;/th&gt;
&lt;th&gt;是否单例&lt;/th&gt;
&lt;th&gt;典型使用场景&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;singleton&lt;/td&gt;
&lt;td&gt;✅&lt;/td&gt;
&lt;td&gt;配置类、无状态服务、共享资源&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;prototype&lt;/td&gt;
&lt;td&gt;❌&lt;/td&gt;
&lt;td&gt;用户对象、临时任务、临时 Bean&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;request&lt;/td&gt;
&lt;td&gt;❌&lt;/td&gt;
&lt;td&gt;HTTP 请求级临时对象、请求参数处理&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;session&lt;/td&gt;
&lt;td&gt;❌&lt;/td&gt;
&lt;td&gt;用户会话相关的数据、购物车、登录状态&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;hr /&gt;
&lt;p&gt;💡 &lt;strong&gt;经验法则&lt;/strong&gt;：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;默认使用 singleton&lt;/strong&gt;，简单、高效、共享。&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;有状态或临时 Bean&lt;/strong&gt; 使用 prototype。&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;Web 环境下的用户请求或会话数据&lt;/strong&gt; 使用 request/session。&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
</content:encoded></item><item><title>Springboot使用AOP结合Redis+Lua脚本分布式限流</title><link>https://yanghc.dev/posts/2e06b5c8/</link><guid isPermaLink="true">https://yanghc.dev/posts/2e06b5c8/</guid><pubDate>Sat, 16 Aug 2025 00:00:00 GMT</pubDate><content:encoded>&lt;h3&gt;1.定义一个限流注解，方便AOP调用&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;@Target(ElementType.METHOD)  
@Retention(RetentionPolicy.RUNTIME)  
@Documented  
public @interface RateLimiter {  
    long DEFAULT_REQUEST = 10;  
  
    /**  
     * max 最大请求数  
     */  
    @AliasFor(&quot;max&quot;) long value() default DEFAULT_REQUEST;  
  
    /**  
     * max 最大请求数  
     */  
    @AliasFor(&quot;value&quot;) long max() default DEFAULT_REQUEST;  
    //上方写两次是为了等值使用，添加了 AliasFor 必须通过 AnnotationUtils 获取，才会生效  
  
    /**  
     * 限流key  
     */    String key() default &quot;&quot;;  
  
    /**  
     * 超时时长，默认1分钟  
     */  
    long timeout() default 1;  
  
    /**  
     * 超时时间单位，默认 分钟  
     */  
    TimeUnit timeUnit() default TimeUnit.MINUTES;  
}
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;2.切面类&lt;/h3&gt;
&lt;p&gt;定义切点表达式，创建pointcut方法，通过方法形参point传入，获取到方法后，通过AnnotationUtils获取方法所携带的注解及其参数，获取最大访问次数max、超时时间timeout，以及时间格式timeUnit，把参数交给限流方法，计算出窗口时间范围expired到now，交给Lua脚本执行&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;@Slf4j  
@Aspect  
@Component  
@RequiredArgsConstructor()  
public class RateLimiterAspect {  
    private final static String SEPARATOR = &quot;:&quot;;  
    private final static String REDIS_LIMIT_KEY_PREFIX = &quot;limit:&quot;;  
    private final StringRedisTemplate stringRedisTemplate;  
    private final RedisScript&amp;lt;Long&amp;gt; limitRedisScript;  
  
    @Pointcut(&quot;@annotation(com.xkcoding.ratelimit.redis.annotation.RateLimiter)&quot;)  
    public void rateLimit() {  
  
    }  
  
    @Around(&quot;rateLimit()&quot;)  
    public Object pointcut(ProceedingJoinPoint point) throws Throwable {  
        MethodSignature signature = (MethodSignature) point.getSignature();  
        Method method = signature.getMethod();  
        // 通过 AnnotationUtils.findAnnotation 获取 RateLimiter 注解  
        RateLimiter rateLimiter = AnnotationUtils.findAnnotation(method, RateLimiter.class);  
        if (rateLimiter != null) {  
            String key = rateLimiter.key();  
            // 默认用类名+方法名做限流的 key 前缀  
            if (StrUtil.isBlank(key)) {  
                key = method.getDeclaringClass().getName() + StrUtil.DOT + method.getName();  
            }  
            // 最终限流的 key 为 前缀 + IP地址  
            // TODO: 此时需要考虑局域网多用户访问的情况，因此 key 后续需要加上方法参数更加合理 ，其实就是：当前限流 key 只包含了“自定义 key 或类名.方法名”和“IP 地址”，但在局域网环境下，多个用户可能共用同一个外网 IP，导致限流不够精确。为了解决这个问题，建议在 key 中加入方法参数（如用户 ID、请求参数等），这样可以更细粒度地区分不同用户或请求，避免误伤。
            key = key + SEPARATOR + IpUtil.getIpAddr();  
  
            long max = rateLimiter.max();  
            long timeout = rateLimiter.timeout();  
            TimeUnit timeUnit = rateLimiter.timeUnit();  
            boolean limited = shouldLimited(key, max, timeout, timeUnit);  
            if (limited) {  
                throw new RuntimeException(&quot;手速太快了，慢点儿吧~&quot;);  
            }  
        }  
  
        return point.proceed();  
    }  
  
    private boolean shouldLimited(String key, long max, long timeout, TimeUnit timeUnit) {  
        // 最终的 key 格式为：  
        // limit:自定义key:IP  
        // limit:类名.方法名:IP  
        key = REDIS_LIMIT_KEY_PREFIX + key;  
        // 统一使用单位毫秒  
        long ttl = timeUnit.toMillis(timeout);  
        // 当前时间毫秒数  
        long now = Instant.now().toEpochMilli();  
        long expired = now - ttl;  
        // 注意这里必须转为 String,否则会报错 java.lang.Long cannot be cast to java.lang.String  
	    Long executeTimes = stringRedisTemplate.execute(limitRedisScript, Collections.singletonList(key), now + &quot;&quot;, ttl + &quot;&quot;, expired + &quot;&quot;, max + &quot;&quot;);  
        if (executeTimes != null) {  
            if (executeTimes == 0) {  
                log.error(&quot;【{}】在单位时间 {} 毫秒内已达到访问上限，当前接口上限 {}&quot;, key, ttl, max);  
                return true;  
            } else {  
                log.info(&quot;【{}】在单位时间 {} 毫秒内访问 {} 次&quot;, key, ttl, executeTimes);  
                return false;  
            }  
        }  
        return false;  
    }  
}
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;3.Lua脚本&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;-- 下标从 1 开始  
local key = KEYS[1]  
local now = tonumber(ARGV[1])  
local ttl = tonumber(ARGV[2])  
local expired = tonumber(ARGV[3])  
-- 最大访问量  
local max = tonumber(ARGV[4])  
  
-- 清除过期的数据  
-- 移除指定分数区间内的所有元素，expired 即已经过期的 score-- 根据当前时间毫秒数 - 超时毫秒数，得到过期时间 
expiredredis.call(&apos;zremrangebyscore&apos;, key, 0, expired)  
  
-- 获取 zset 中的当前元素个数  
local current = tonumber(redis.call(&apos;zcard&apos;, key))  
local next = current + 1  
  
if next &amp;gt; max then  
  -- 达到限流大小 返回 0  return 0;  
else  
  -- 往 zset 中添加一个值、得分均为当前时间戳的元素，[value,score]  
  redis.call(&quot;zadd&quot;, key, now, now)  
  -- 每次访问均重新设置 zset 的过期时间，单位毫秒  
  redis.call(&quot;pexpire&quot;, key, ttl)  
  return next  
end
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;获取参数，使用滑动窗口动态计算，首先删除不在窗口范围内的数据，然后使用计数器计算窗口内数据+当前数据总数与最大访问数据做比较，如果大于max，说明当前时间段数据超限了，返回0，不再继续访问该数据
否则就把当前数据插进去，并重新设置key过期时间（延长timeout时间），这是为了防止多个用户在访问该接口一次后再也不访问了，key永久存在，就算超时了数据也会存在，因为用户没有第二次访问数据，也就没有&lt;code&gt;移除指定分数区间内的所有元素&lt;/code&gt;，是为了&quot;僵尸Key&quot;问题，而不是&quot;过期数据&quot;问题。它确保即使用户再也不访问，相关的限流Key也能在timeout后自动清理，避免Redis中累积大量无用的空Key。。顺带说一句，就算key为空他也是占内存的，Redis需要去维护，&lt;/p&gt;
&lt;h3&gt;4.其它&lt;/h3&gt;
&lt;h4&gt;为什么Redis数据结构要使用zset实现限流？&lt;/h4&gt;
&lt;p&gt;使用 ZSET 的核心原因是要实现&lt;strong&gt;滑动时间窗口限流&lt;/strong&gt;。&lt;/p&gt;
&lt;h5&gt;核心需求&lt;/h5&gt;
&lt;p&gt;限流需要判断：&lt;strong&gt;在过去N秒内，是否已经有超过M次请求&lt;/strong&gt;&lt;/p&gt;
&lt;h5&gt;ZSET 的优势&lt;/h5&gt;
&lt;h6&gt;1. &lt;strong&gt;存储时间戳&lt;/strong&gt;&lt;/h6&gt;
&lt;pre&gt;&lt;code&gt;-- ZSET 同时存储时间戳作为 value 和 score
zadd key 1697788800000 1697788800000  -- [时间戳, 时间戳]
&lt;/code&gt;&lt;/pre&gt;
&lt;h6&gt;2. &lt;strong&gt;高效清理过期数据&lt;/strong&gt;&lt;/h6&gt;
&lt;pre&gt;&lt;code&gt;-- 一条命令删除所有过期请求记录
redis.call(&apos;zremrangebyscore&apos;, key, 0, expired)
&lt;/code&gt;&lt;/pre&gt;
&lt;h6&gt;3. &lt;strong&gt;快速统计当前请求数&lt;/strong&gt;&lt;/h6&gt;
&lt;pre&gt;&lt;code&gt;-- 直接获取有效请求数量
local current = redis.call(&apos;zcard&apos;, key)
&lt;/code&gt;&lt;/pre&gt;
&lt;h5&gt;对比其他方案&lt;/h5&gt;
&lt;p&gt;&lt;strong&gt;STRING 计数器：&lt;/strong&gt; 只能固定时间窗口，无法滑动
&lt;strong&gt;LIST：&lt;/strong&gt; 清理过期数据效率低，需要逐个检查
&lt;strong&gt;HASH：&lt;/strong&gt; 需要手动维护时间字段，复杂度高&lt;/p&gt;
&lt;h5&gt;实现效果&lt;/h5&gt;
&lt;pre&gt;&lt;code&gt;窗口=60秒，限制=5次

时间轴: 10:00:00  10:00:30  10:01:00  10:01:10
请求:     ①②③      ④⑤       检查      ⑥

在 10:01:10 检查时：
├─ 删除 10:00:10 之前的请求 (①②③被删除)
├─ 保留 ④⑤ (在窗口内)  
├─ 当前计数=2，允许⑥通过
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;strong&gt;ZSET 让滑动窗口限流变得简单高效&lt;/strong&gt;，这是选择它的根本原因。&lt;/p&gt;
&lt;h4&gt;为什么要用lua脚本，而不是java代码实现&lt;/h4&gt;
&lt;p&gt;使用 Lua 脚本而不是 Java 代码实现限流逻辑，主要有以下几个关键原因：&lt;/p&gt;
&lt;h5&gt;1. &lt;strong&gt;保证操作的原子性&lt;/strong&gt;&lt;/h5&gt;
&lt;p&gt;说明的是，如果ip级别的限流，他们的key是各不相同的，所以用java代码实现也能保证原子性&lt;/p&gt;
&lt;h6&gt;Java 代码的问题（非原子性）：&lt;/h6&gt;
&lt;pre&gt;&lt;code&gt;// ❌ 这些操作不是原子的，存在竞态条件
stringRedisTemplate.opsForZSet().removeRangeByScore(key, 0, expired);  // 操作1
Long current = stringRedisTemplate.opsForZSet().zCard(key);             // 操作2
if (current &amp;lt; max) {
    stringRedisTemplate.opsForZSet().add(key, now, now);                // 操作3
    stringRedisTemplate.expire(key, ttl, TimeUnit.MILLISECONDS);        // 操作4
}
&lt;/code&gt;&lt;/pre&gt;
&lt;h6&gt;并发问题示例：&lt;/h6&gt;
&lt;pre&gt;&lt;code&gt;时间线：用户A 和 用户B 同时访问（当前计数=4，限制=5）

用户A：removeRangeByScore → current=4 → 判断通过 → [被中断]
用户B：removeRangeByScore → current=4 → 判断通过 → zadd → 计数=5
用户A：[继续] → zadd → 计数=6 ❌ 超出限制！
&lt;/code&gt;&lt;/pre&gt;
&lt;h6&gt;Lua 脚本的优势（原子性）：&lt;/h6&gt;
&lt;pre&gt;&lt;code&gt;-- ✅ 整个脚本作为一个原子操作执行
redis.call(&apos;zremrangebyscore&apos;, key, 0, expired)
local current = tonumber(redis.call(&apos;zcard&apos;, key))
-- 中间不会被其他请求打断
redis.call(&quot;zadd&quot;, key, now, now)
&lt;/code&gt;&lt;/pre&gt;
&lt;h5&gt;2. &lt;strong&gt;减少网络往返次数&lt;/strong&gt;&lt;/h5&gt;
&lt;h6&gt;Java 代码：&lt;/h6&gt;
&lt;pre&gt;&lt;code&gt;// ❌ 需要 3-4 次网络往返
stringRedisTemplate.opsForZSet().removeRangeByScore(key, 0, expired);  // 网络往返1
Long current = stringRedisTemplate.opsForZSet().zCard(key);             // 网络往返2
stringRedisTemplate.opsForZSet().add(key, now, now);                    // 网络往返3
stringRedisTemplate.expire(key, ttl, TimeUnit.MILLISECONDS);           // 网络往返4
&lt;/code&gt;&lt;/pre&gt;
&lt;h6&gt;Lua 脚本：&lt;/h6&gt;
&lt;pre&gt;&lt;code&gt;-- ✅ 只需要 1 次网络往返
-- 所有操作在 Redis 服务器端执行
&lt;/code&gt;&lt;/pre&gt;
&lt;h5&gt;3. &lt;strong&gt;性能优势&lt;/strong&gt;&lt;/h5&gt;
&lt;h6&gt;高并发场景下的差异：&lt;/h6&gt;
&lt;pre&gt;&lt;code&gt;1000个并发请求的情况：

Java 实现：
├─ 网络往返：1000 × 4 = 4000次
├─ 竞态条件：可能导致限流失效
└─ 响应时间：较慢

Lua 脚本：
├─ 网络往返：1000 × 1 = 1000次
├─ 原子操作：限流严格有效
└─ 响应时间：更快
&lt;/code&gt;&lt;/pre&gt;
&lt;h5&gt;4. &lt;strong&gt;避免分布式锁&lt;/strong&gt;&lt;/h5&gt;
&lt;h6&gt;如果用 Java 实现，需要加锁：&lt;/h6&gt;
&lt;pre&gt;&lt;code&gt;// ❌ 复杂且性能差
String lockKey = &quot;lock:&quot; + key;
try {
    // 获取分布式锁
    if (redisLock.tryLock(lockKey, 100, TimeUnit.MILLISECONDS)) {
        // 执行限流逻辑
        // ...
    }
} finally {
    redisLock.unlock(lockKey);
}
&lt;/code&gt;&lt;/pre&gt;
&lt;h6&gt;Lua 脚本天然避免了这个问题：&lt;/h6&gt;
&lt;pre&gt;&lt;code&gt;-- ✅ 不需要显式加锁，Redis 保证脚本原子执行
&lt;/code&gt;&lt;/pre&gt;
&lt;h5&gt;5. &lt;strong&gt;事务一致性&lt;/strong&gt;&lt;/h5&gt;
&lt;p&gt;Lua 脚本确保限流判断和计数更新在同一个事务中：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;-- 判断和操作是一体的，不会出现：
-- &quot;判断时未超限，但执行时已超限&quot; 的情况
if next &amp;gt; max then
  return 0;
else
  redis.call(&quot;zadd&quot;, key, now, now)
  return next
end
&lt;/code&gt;&lt;/pre&gt;
&lt;h5&gt;总结&lt;/h5&gt;
&lt;p&gt;&lt;strong&gt;Lua 脚本的核心优势：&lt;/strong&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;✅ &lt;strong&gt;原子性&lt;/strong&gt;：避免竞态条件&lt;/li&gt;
&lt;li&gt;✅ &lt;strong&gt;高性能&lt;/strong&gt;：减少网络往返&lt;/li&gt;
&lt;li&gt;✅ &lt;strong&gt;简洁&lt;/strong&gt;：无需分布式锁&lt;/li&gt;
&lt;li&gt;✅ &lt;strong&gt;准确性&lt;/strong&gt;：严格的限流控制&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;优化：惩罚机制和风控策略&lt;/h3&gt;
&lt;p&gt;你的观点很对！当前的限流实现确实比较基础，只是简单地拒绝请求。在实际生产环境中，需要更完善的&lt;strong&gt;惩罚机制&lt;/strong&gt;和&lt;strong&gt;风控策略&lt;/strong&gt;。&lt;/p&gt;
&lt;h4&gt;当前实现的局限性&lt;/h4&gt;
&lt;pre&gt;&lt;code&gt;// 当前逻辑：超出限制就直接抛异常
if (limited) {
    throw new RuntimeException(&quot;手速太快了，慢点儿吧~&quot;);
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;问题：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;❌ &lt;strong&gt;无惩罚机制&lt;/strong&gt;：用户可以一直尝试&lt;/li&gt;
&lt;li&gt;❌ &lt;strong&gt;无封禁逻辑&lt;/strong&gt;：恶意用户成本很低&lt;/li&gt;
&lt;li&gt;❌ &lt;strong&gt;无分级处理&lt;/strong&gt;：所有超限都是同等处理&lt;/li&gt;
&lt;/ul&gt;
&lt;h4&gt;改进方案：分级惩罚机制&lt;/h4&gt;
&lt;h5&gt;1. &lt;strong&gt;增强的注解定义&lt;/strong&gt;&lt;/h5&gt;
&lt;pre&gt;&lt;code&gt;@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
public @interface RateLimiter {
    // 原有参数
    long max() default 5;
    long timeout() default 1;
    TimeUnit timeUnit() default TimeUnit.MINUTES;
    
    // 新增惩罚机制参数
    boolean enablePunishment() default false;        // 是否启用惩罚
    long warningThreshold() default 10;              // 警告阈值
    long banThreshold() default 20;                  // 封禁阈值
    long banDuration() default 30;                   // 封禁时长
    TimeUnit banTimeUnit() default TimeUnit.MINUTES; // 封禁时间单位
}
&lt;/code&gt;&lt;/pre&gt;
&lt;h5&gt;2. &lt;strong&gt;分级处理逻辑&lt;/strong&gt;&lt;/h5&gt;
&lt;pre&gt;&lt;code&gt;@Service
public class EnhancedRateLimiterService {
    
    private static final String VIOLATION_KEY_PREFIX = &quot;violation:&quot;;
    private static final String BAN_KEY_PREFIX = &quot;ban:&quot;;
    
    public RateLimitResult checkRateLimit(String key, RateLimiter config) {
        // 1. 检查是否被封禁
        if (isBanned(key)) {
            return RateLimitResult.banned(&quot;用户已被封禁&quot;);
        }
        
        // 2. 执行正常限流检查
        boolean normalLimited = executeNormalRateLimit(key, config);
        
        if (!normalLimited) {
            return RateLimitResult.allowed();
        }
        
        // 3. 记录违规次数
        long violationCount = recordViolation(key);
        
        // 4. 根据违规次数分级处理
        if (config.enablePunishment()) {
            return handleViolation(key, violationCount, config);
        }
        
        return RateLimitResult.rejected(&quot;请求过于频繁&quot;);
    }
    
    private RateLimitResult handleViolation(String key, long count, RateLimiter config) {
        if (count &amp;gt;= config.banThreshold()) {
            // 封禁用户
            banUser(key, config.banDuration(), config.banTimeUnit());
            return RateLimitResult.banned(&quot;违规次数过多，已被封禁 &quot; + 
                config.banDuration() + &quot; &quot; + config.banTimeUnit().name().toLowerCase());
                
        } else if (count &amp;gt;= config.warningThreshold()) {
            // 警告
            return RateLimitResult.warning(&quot;警告：继续违规将被封禁，当前违规次数：&quot; + count);
        }
        
        return RateLimitResult.rejected(&quot;请求过于频繁，违规次数：&quot; + count);
    }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;h5&gt;3. &lt;strong&gt;Lua 脚本增强&lt;/strong&gt;&lt;/h5&gt;
&lt;pre&gt;&lt;code&gt;-- enhanced_rate_limit.lua
local key = KEYS[1]
local violation_key = KEYS[2]
local ban_key = KEYS[3]

local now = tonumber(ARGV[1])
local ttl = tonumber(ARGV[2])
local expired = tonumber(ARGV[3])
local max = tonumber(ARGV[4])
local ban_duration = tonumber(ARGV[5])
local ban_threshold = tonumber(ARGV[6])

-- 检查是否被封禁
local ban_expire = redis.call(&apos;GET&apos;, ban_key)
if ban_expire and tonumber(ban_expire) &amp;gt; now then
    return {-1, tonumber(ban_expire) - now} -- 返回剩余封禁时间
end

-- 正常限流逻辑
redis.call(&apos;zremrangebyscore&apos;, key, 0, expired)
local current = tonumber(redis.call(&apos;zcard&apos;, key))

if current &amp;lt; max then
    redis.call(&quot;zadd&quot;, key, now, now)
    redis.call(&quot;pexpire&quot;, key, ttl)
    return {current + 1, 0}
else
    -- 记录违规
    local violation_count = redis.call(&apos;INCR&apos;, violation_key)
    redis.call(&apos;EXPIRE&apos;, violation_key, 3600) -- 违规记录1小时过期
    
    -- 检查是否需要封禁
    if violation_count &amp;gt;= ban_threshold then
        redis.call(&apos;SET&apos;, ban_key, now + ban_duration)
        redis.call(&apos;EXPIRE&apos;, ban_key, ban_duration / 1000)
        return {-2, ban_duration} -- 触发封禁
    end
    
    return {0, violation_count} -- 返回违规次数
end
&lt;/code&gt;&lt;/pre&gt;
&lt;h5&gt;4. &lt;strong&gt;控制器中的使用&lt;/strong&gt;&lt;/h5&gt;
&lt;pre&gt;&lt;code&gt;@RestController
public class EnhancedTestController {
    
    @RateLimiter(
        max = 5, 
        timeout = 1, 
        timeUnit = TimeUnit.MINUTES,
        enablePunishment = true,
        warningThreshold = 3,
        banThreshold = 5,
        banDuration = 30,
        banTimeUnit = TimeUnit.MINUTES
    )
    @GetMapping(&quot;/test1&quot;)
    public ResponseEntity&amp;lt;Dict&amp;gt; test1() {
        return ResponseEntity.ok(
            Dict.create()
                .set(&quot;msg&quot;, &quot;hello,world!&quot;)
                .set(&quot;description&quot;, &quot;正常访问&quot;)
        );
    }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;h5&gt;5. &lt;strong&gt;结果类型定义&lt;/strong&gt;&lt;/h5&gt;
&lt;pre&gt;&lt;code&gt;@Data
@AllArgsConstructor
public class RateLimitResult {
    private boolean allowed;
    private String message;
    private long remainingTime; // 剩余封禁时间（秒）
    private int violationCount; // 违规次数
    
    public static RateLimitResult allowed() {
        return new RateLimitResult(true, &quot;允许访问&quot;, 0, 0);
    }
    
    public static RateLimitResult banned(String message) {
        return new RateLimitResult(false, message, 0, 0);
    }
    
    public static RateLimitResult warning(String message) {
        return new RateLimitResult(true, message, 0, 0);
    }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;h4&gt;效果展示&lt;/h4&gt;
&lt;pre&gt;&lt;code&gt;用户访问轨迹：

第1-5次：正常返回数据
第6次：  &quot;请求过于频繁，违规次数：1&quot;
第7次：  &quot;请求过于频繁，违规次数：2&quot;  
第8次：  &quot;警告：继续违规将被封禁，当前违规次数：3&quot;
第9次：  &quot;警告：继续违规将被封禁，当前违规次数：4&quot;
第10次： &quot;违规次数过多，已被封禁 30 minutes&quot;

接下来30分钟内的所有请求：
&quot;用户已被封禁，剩余时间：XX分钟&quot;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;这样的机制能够：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;✅ &lt;strong&gt;递进式惩罚&lt;/strong&gt;：从警告到封禁&lt;/li&gt;
&lt;li&gt;✅ &lt;strong&gt;震慑效果&lt;/strong&gt;：增加恶意访问成本&lt;/li&gt;
&lt;li&gt;✅ &lt;strong&gt;保护系统&lt;/strong&gt;：防止资源被滥用&lt;/li&gt;
&lt;li&gt;✅ &lt;strong&gt;用户体验&lt;/strong&gt;：给正常用户提供清晰的反馈&lt;/li&gt;
&lt;/ul&gt;
</content:encoded></item><item><title>手搓布隆过滤器</title><link>https://yanghc.dev/posts/9b4a2633/</link><guid isPermaLink="true">https://yanghc.dev/posts/9b4a2633/</guid><pubDate>Wed, 16 Jul 2025 22:10:15 GMT</pubDate><content:encoded>&lt;p&gt;布隆过滤器的核心思想是：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;使用一个大的 &lt;strong&gt;位数组（BitSet）&lt;/strong&gt;&lt;/li&gt;
&lt;li&gt;使用 &lt;strong&gt;多个哈希函数（HashFunction）&lt;/strong&gt;&lt;/li&gt;
&lt;li&gt;插入元素时，对元素进行多次哈希并将对应的位数组位置置为true（遍历哈希函数，便于使用不同的种子来把一个要哈希的对象变为多个int值放入bitset中）&lt;/li&gt;
&lt;li&gt;查询元素时，只要任意一个对应位为0，就确定该元素一定不存在；如果所有位都为1，可能存在（&lt;strong&gt;存在误判&lt;/strong&gt;）&lt;/li&gt;
&lt;/ul&gt;
&lt;pre&gt;&lt;code&gt;import java.util.BitSet;  
  
public class MyBloomFilter {  
    private static final int DEFAULT_SIZE = Integer.MAX_VALUE;  
    private static final int MIN_SIZE = 1000;  
    private static int SIZE = DEFAULT_SIZE;  
    //hash函数种子因子  
    private static final int[] HASH_SEEDS = new int[]{3, 5, 7, 11, 13, 17, 19, 23, 29, 31};  
  
    private BitSet bitSet = null;  
  
    //HASH函数  
    private HashFunction[] hashFunctions = new HashFunction[HASH_SEEDS.length];  
  
    public MyBloomFilter() {  
        init();  
    }  
  
    public MyBloomFilter(int size) {  
        if (size &amp;gt;= MIN_SIZE) {  
            SIZE = size;  
        }  
        init();  
    }  
  
    private void init() {  
        bitSet = new BitSet(SIZE);  
        for (int i = 0; i &amp;lt; HASH_SEEDS.length; i++) {  
            hashFunctions[i] = new HashFunction(SIZE, HASH_SEEDS[i]);  
        }  
    }  
  
    public void add(Object value) {  
        for (HashFunction f : hashFunctions) {  
            bitSet.set(f.hash(value), true);  
        }  
    }  
  
    public boolean contains(Object value){  
        boolean result = true;  
        for (HashFunction f : hashFunctions) {  
            result = result &amp;amp;&amp;amp; bitSet.get(f.hash(value));  
  
            if (!result){  
                return result;  
            }  
        }  
        return result;  
    }  
  
    public static class HashFunction {  
        private int size;  
        private int seed;  
  
        public HashFunction(int size, int seed) {  
            this.size = size;  
            this.seed = seed;  
        }  
  
        public int hash(Object value){  
            if (value == null){  
                return 0;  
            }  
            int hash1 = value.hashCode();  
  
            int hash2 = hash1 &amp;gt;&amp;gt;&amp;gt; 16;  
  
            int combine = hash1 ^ hash2;  
  
            return Math.abs(combine * seed) % size;  
  
        }  
    }  
  
    public static void main(String[] args) {  
        Integer num1 = 31312;  
        Integer num2 = 312312;  
  
        MyBloomFilter bloomFilter = new MyBloomFilter();  
  
        System.out.println(bloomFilter.contains(num1) + &quot; &quot; + bloomFilter.contains(num2));  
  
        bloomFilter.add(num1);  
        bloomFilter.add(num2);  
  
        System.out.println(bloomFilter.contains(num1) + &quot; &quot; + bloomFilter.contains(num2));  
  
        System.out.println(bloomFilter.contains(312432));  
    }  
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;详解关键&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;int hash1 = value.hashCode();  
  
int hash2 = hash1 &amp;gt;&amp;gt;&amp;gt; 16;  
  
int combine = hash1 ^ hash2;  
  
return Math.abs(combine * seed) % size;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;这段代码是布隆过滤器中哈希函数的核心，用于将任意对象转化为在 &lt;code&gt;[0, size)&lt;/code&gt; 范围内的整数索引。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;int hash1 = value.hashCode();
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;✅ 获取原始哈希值&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;使用 Java 的 &lt;code&gt;hashCode()&lt;/code&gt; 方法得到对象的哈希值。&lt;/li&gt;
&lt;li&gt;这是 Java 中所有对象都具备的，通常能反映其数据特征。&lt;/li&gt;
&lt;/ul&gt;
&lt;pre&gt;&lt;code&gt;int hash2 = hash1 &amp;gt;&amp;gt;&amp;gt; 16;
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;✅ 提取高位信息，做混淆处理&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;将 &lt;code&gt;hash1&lt;/code&gt; 无符号右移 16 位，得到高位部分。&lt;/li&gt;
&lt;li&gt;这么做是为了将高位信息与低位结合，增强哈希的分布性（也叫“扰动函数”）。&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;类似思路也出现在 HashMap 的源码中：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;// HashMap.java 中对 hashCode 的扰动函数
static final int hash(Object key) {
    int h;
    return (key == null) ? 0 : (h = key.hashCode()) ^ (h &amp;gt;&amp;gt;&amp;gt; 16);
}
&lt;/code&gt;&lt;/pre&gt;
&lt;pre&gt;&lt;code&gt;int combine = hash1 ^ hash2;
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;✅ 异或操作，打乱模式&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;将原始哈希值与高位进行异或，增强随机性，避免哈希冲突集中。&lt;/li&gt;
&lt;li&gt;异或操作本质上是一种 &lt;strong&gt;位级扰动（bit-level mixing）&lt;/strong&gt;。&lt;/li&gt;
&lt;/ul&gt;
&lt;pre&gt;&lt;code&gt;return Math.abs(combine * seed) % size;
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;✅ 与种子结合，压缩到位图范围&lt;/h3&gt;
&lt;ol&gt;
&lt;li&gt;&lt;strong&gt;乘上 seed&lt;/strong&gt;：
&lt;ul&gt;
&lt;li&gt;使用不同的种子值，可以生成多个不同的哈希函数。&lt;/li&gt;
&lt;li&gt;这是布隆过滤器中实现多个独立哈希函数的一种方式。&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;取绝对值&lt;/strong&gt;：
&lt;ul&gt;
&lt;li&gt;保证哈希结果是非负的。&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;模 size&lt;/strong&gt;：
&lt;ul&gt;
&lt;li&gt;将哈希结果限定到 &lt;code&gt;BitSet&lt;/code&gt; 的合法索引范围 &lt;code&gt;[0, size)&lt;/code&gt;。&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ol&gt;
&lt;h2&gt;🔎 整体目的是：&lt;/h2&gt;
&lt;ol&gt;
&lt;li&gt;&lt;strong&gt;增强分布性和抗冲突性&lt;/strong&gt;（扰动 &lt;code&gt;hash1&lt;/code&gt;）&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;通过种子值构造出多个不同的哈希函数&lt;/strong&gt;&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;最终将结果落入 BitSet 有效范围内&lt;/strong&gt;&lt;/li&gt;
&lt;/ol&gt;
&lt;h2&gt;✅ 总结&lt;/h2&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;步骤&lt;/th&gt;
&lt;th&gt;目的&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;hashCode()&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;获取对象哈希值&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;&amp;gt;&amp;gt;&amp;gt; 16&lt;/code&gt; + &lt;code&gt;^&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;扰动，提高分布性&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;* seed&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;构造多个哈希函数&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;Math.abs(...) % size&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;映射到 BitSet 的有效索引范围&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;p&gt;这是一种 &lt;strong&gt;轻量级但分布性还可以的哈希函数构造方式&lt;/strong&gt;，适合用于布隆过滤器中快速构建多个 hash 函数。如果想进一步优化精度，可以考虑使用 &lt;code&gt;MurmurHash&lt;/code&gt;, &lt;code&gt;FNV&lt;/code&gt;, &lt;code&gt;Guava.hashing()&lt;/code&gt; 等更高质量的哈希算法。&lt;/p&gt;
</content:encoded></item><item><title>HTTPS加密原理讲解</title><link>https://yanghc.dev/posts/501cc362/</link><guid isPermaLink="true">https://yanghc.dev/posts/501cc362/</guid><pubDate>Thu, 09 Oct 2025 22:13:55 GMT</pubDate><content:encoded>&lt;p&gt;&lt;img src=&quot;assets/IMG-HTTPS%E5%8A%A0%E5%AF%86%E5%8E%9F%E7%90%86%E8%AE%B2%E8%A7%A3-20250621215556131.png&quot; alt=&quot;&quot; /&gt;&lt;/p&gt;
&lt;h3&gt;1. &lt;strong&gt;SSL/TLS 握手过程概述&lt;/strong&gt;&lt;/h3&gt;
&lt;p&gt;在 &lt;strong&gt;SSL/TLS 握手&lt;/strong&gt; 中，客户端和服务器进行一系列的交互，以确保加密通信的安全性。握手过程分为多个步骤，其中包括非对称加密（公钥加密）和对称加密（会话密钥）。这个过程并不会在 TCP 三次握手中完成，而是在 TCP 连接建立后执行，紧接着进行。&lt;/p&gt;
&lt;h3&gt;2. &lt;strong&gt;客户端和服务器的通信过程&lt;/strong&gt;&lt;/h3&gt;
&lt;p&gt;根据图片的步骤，整个 &lt;strong&gt;SSL/TLS 握手&lt;/strong&gt; 过程可以分为以下几个主要阶段：&lt;/p&gt;
&lt;h4&gt;&lt;strong&gt;步骤 1：Client Hello（客户端问候）&lt;/strong&gt;&lt;/h4&gt;
&lt;ul&gt;
&lt;li&gt;客户端向服务器发送 &lt;code&gt;Client Hello&lt;/code&gt; 消息，其中包括：
&lt;ul&gt;
&lt;li&gt;支持的 TLS 版本（如 TLS 1.2 或 TLS 1.3）。&lt;/li&gt;
&lt;li&gt;支持的加密套件（如 RSA、ECDSA）。&lt;/li&gt;
&lt;li&gt;随机数（&lt;code&gt;client_random&lt;/code&gt;），用于后续生成会话密钥。&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;h4&gt;&lt;strong&gt;步骤 2：Server Hello（服务器问候）&lt;/strong&gt;&lt;/h4&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;服务器响应客户端的 &lt;code&gt;Client Hello&lt;/code&gt;，并发送 &lt;code&gt;Server Hello&lt;/code&gt; 消息，其中包括：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;服务器选择的加密套件。&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;服务器选择的 TLS 版本。&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;随机数（&lt;code&gt;server_random&lt;/code&gt;），用于后续生成会话密钥。&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;h4&gt;&lt;strong&gt;步骤 3：服务器证书（Server Certificate）&lt;/strong&gt;&lt;/h4&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;服务器发送 &lt;strong&gt;SSL/TLS 证书&lt;/strong&gt;，该证书由可信的 &lt;strong&gt;证书颁发机构（CA）&lt;/strong&gt; 签发，证书包含服务器的公钥和服务器的身份信息（如域名）。&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;客户端验证证书的有效性，确保证书是由受信任的 CA 签发，且证书有效（未过期）。&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;h4&gt;&lt;strong&gt;步骤 4：服务器密钥交换（Server Key Exchange）&lt;/strong&gt;&lt;/h4&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;如果使用 &lt;strong&gt;非对称加密&lt;/strong&gt;，服务器会发送公钥（如果使用 &lt;strong&gt;RSA&lt;/strong&gt;，则客户端直接使用公钥加密会话密钥）。&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;在某些加密套件（如 &lt;strong&gt;ECDHE&lt;/strong&gt;）中，服务器会发送额外的密钥交换信息，以便客户端生成共享密钥。&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;h4&gt;&lt;strong&gt;步骤 5：服务器完成问候（Server Hello Done）&lt;/strong&gt;&lt;/h4&gt;
&lt;ul&gt;
&lt;li&gt;服务器发送 &lt;code&gt;Server Hello Done&lt;/code&gt; 消息，表示服务器已完成初始化过程，等待客户端的进一步响应。&lt;/li&gt;
&lt;/ul&gt;
&lt;h4&gt;&lt;strong&gt;步骤 6：客户端密钥交换（Client Key Exchange）&lt;/strong&gt;&lt;/h4&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;客户端生成一个 &lt;strong&gt;Pre-master Secret&lt;/strong&gt;，并使用服务器的公钥加密该密钥。然后，客户端将加密的 Pre-master Secret 发送给服务器。&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;如果使用 &lt;strong&gt;RSA&lt;/strong&gt; 加密方式，客户端直接使用服务器的公钥加密 Pre-master Secret。&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;如果使用 &lt;strong&gt;DH&lt;/strong&gt; 或 &lt;strong&gt;ECDHE&lt;/strong&gt; 等协议，客户端会计算出一个共享的密钥。&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;h4&gt;&lt;strong&gt;步骤 7：更改密码规范（Change Cipher Spec）&lt;/strong&gt;&lt;/h4&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;客户端和服务器在握手过程中交换一个 &lt;strong&gt;Change Cipher Spec&lt;/strong&gt; 消息，表明接下来的通信将使用经过握手协商的加密和认证算法。&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;这一步标志着非对称加密阶段结束，双方将开始使用 &lt;strong&gt;对称加密&lt;/strong&gt;（会话密钥）进行数据传输。&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;h4&gt;&lt;strong&gt;步骤 8：加密握手消息（Encrypted Handshake Message）&lt;/strong&gt;&lt;/h4&gt;
&lt;ul&gt;
&lt;li&gt;客户端和服务器交换加密的握手消息，确保这一步的所有信息在传输过程中没有被篡改。&lt;/li&gt;
&lt;/ul&gt;
&lt;h4&gt;&lt;strong&gt;步骤 9：会话完成，开始安全通信&lt;/strong&gt;&lt;/h4&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;一旦双方完成了上述步骤，安全的对称加密会话密钥就会建立起来。&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;之后的通信将通过对称加密进行保护，从而提高性能并减少计算开销。&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;3. &lt;strong&gt;非对称加密与对称加密的结合&lt;/strong&gt;&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;在 &lt;strong&gt;SSL/TLS 握手&lt;/strong&gt; 中，使用了 &lt;strong&gt;非对称加密&lt;/strong&gt; 和 &lt;strong&gt;对称加密&lt;/strong&gt; 的结合：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;非对称加密&lt;/strong&gt;：在握手过程中使用公钥加密和私钥解密，用来交换 &lt;strong&gt;Pre-master Secret&lt;/strong&gt;，并确保初始的安全通道。&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;对称加密&lt;/strong&gt;：一旦会话密钥（对称密钥）生成，之后的所有通信都会使用对称加密进行处理，因为对称加密的效率更高。&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;4. &lt;strong&gt;证书与 CA 证书链的关系&lt;/strong&gt;&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;服务器证书&lt;/strong&gt; 是由 &lt;strong&gt;CA（证书颁发机构）&lt;/strong&gt; 颁发的，CA 是受信任的第三方，负责验证服务器的身份。&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;客户端通过 &lt;strong&gt;证书链&lt;/strong&gt; 来验证服务器证书的有效性：&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;根证书（Root Certificate）&lt;/strong&gt;：浏览器或操作系统已经预装了根证书，这些证书被广泛信任。&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;中间证书（Intermediate Certificate）&lt;/strong&gt;：根证书签发中间证书，中间证书进一步签发服务器证书。客户端通过根证书和中间证书建立信任。&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;验证过程&lt;/strong&gt;：浏览器会验证整个证书链，从根证书到中间证书，再到服务器证书，确保证书链的完整性和有效性。&lt;/p&gt;
&lt;/li&gt;
&lt;/ol&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;5. &lt;strong&gt;SSL/TLS 握手与 TCP 三次握手的关系&lt;/strong&gt;&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;SSL/TLS 握手&lt;/strong&gt; 与 &lt;strong&gt;TCP 三次握手&lt;/strong&gt; 是两个不同的过程：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;TCP 三次握手&lt;/strong&gt; 主要用于建立一个可靠的连接。&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;SSL/TLS 握手&lt;/strong&gt; 则是在 TCP 连接建立后，用于协商加密算法、交换证书、生成会话密钥等安全措施。&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;SSL/TLS 握手&lt;/strong&gt; 依赖于 &lt;strong&gt;TCP 连接&lt;/strong&gt;，但是它不是 TCP 三次握手的一部分，而是一个在建立连接后的后续过程。&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;6. &lt;strong&gt;总结&lt;/strong&gt;&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;SSL/TLS 握手过程&lt;/strong&gt; 使用 &lt;strong&gt;非对称加密&lt;/strong&gt; 交换 &lt;strong&gt;Pre-master Secret&lt;/strong&gt;，并在会话建立后使用 &lt;strong&gt;对称加密&lt;/strong&gt; 进行高效的加密通信。&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;证书的可信性依赖于 &lt;strong&gt;证书链&lt;/strong&gt;，并通过 &lt;strong&gt;CA（证书颁发机构）&lt;/strong&gt; 验证。根证书是浏览器信任的基础，确保服务器身份的真实性。&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;握手过程中包括了多个重要步骤，从客户端和服务器交换消息、生成共享密钥到加密通信的开始。&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;这些步骤保证了 HTTPS 连接的 &lt;strong&gt;机密性&lt;/strong&gt;、&lt;strong&gt;完整性&lt;/strong&gt; 和 &lt;strong&gt;身份验证&lt;/strong&gt;，确保客户端与服务器之间的安全通信。&lt;/p&gt;
</content:encoded></item><item><title>Windows11 如何正确启动远程桌面</title><link>https://yanghc.dev/posts/a800e976/</link><guid isPermaLink="true">https://yanghc.dev/posts/a800e976/</guid><pubDate>Sat, 05 Jul 2025 20:15:45 GMT</pubDate><content:encoded>&lt;blockquote&gt;
&lt;p&gt;[!warning] 提示&lt;br /&gt;
远程桌面貌似只支持专业版，不支持家庭版，请自行在&lt;code&gt;设置&lt;/code&gt;&amp;gt;&lt;code&gt;系统&lt;/code&gt;&amp;gt;&lt;code&gt;系统信息&lt;/code&gt;检查&lt;/p&gt;
&lt;/blockquote&gt;
&lt;h2&gt;1.检查远程设置是否开启&lt;/h2&gt;
&lt;p&gt;打开&lt;code&gt;设置&lt;/code&gt;&amp;gt;&lt;code&gt;系统&lt;/code&gt;&amp;gt;&lt;code&gt;远程桌面&lt;/code&gt;，把远程桌面开关打开&lt;br /&gt;
&lt;img src=&quot;assets/289010b936bc4720906fa84bfe0c5d70.png&quot; alt=&quot;在这里插入图片描述&quot; /&gt;&lt;/p&gt;
&lt;p&gt;打开&lt;code&gt;设置&lt;/code&gt;&amp;gt;&lt;code&gt;系统&lt;/code&gt;&amp;gt;&lt;code&gt;系统信息&lt;/code&gt;&amp;gt;&lt;code&gt;高级系统设置&lt;/code&gt;，点击远程，把&lt;code&gt;允许远程连接到计算机&lt;/code&gt;选中，对于&lt;code&gt;仅允许运行使用网络级别身份验证的远程桌面的计算机连接(建议)(N)&lt;/code&gt;，可勾可不勾&lt;br /&gt;
&lt;img src=&quot;assets/b627169f240c4150a622fabadeb850ea.png&quot; alt=&quot;在这里插入图片描述&quot; /&gt;&lt;/p&gt;
&lt;h2&gt;2.检查防火墙、服务、组策略&lt;/h2&gt;
&lt;p&gt;&lt;strong&gt;防火墙：&lt;/strong&gt; 打开&lt;code&gt;控制面板&lt;/code&gt;&amp;gt;&lt;code&gt;系统和安全&lt;/code&gt;&amp;gt;&lt;code&gt;Windows Defender 防火墙&lt;/code&gt;，选择&lt;code&gt;允许应用通过WindowsDefender防火墙进行通信&lt;/code&gt;&lt;br /&gt;
往下滑，找到远程桌面，然后勾选专用，或者可以都勾选上（按理说应该只有一个，我这里不知道为什么有两个就都勾选了）&lt;br /&gt;
&lt;img src=&quot;assets/225dcd8aca5e40eba63ba053392b60da.png&quot; alt=&quot;在这里插入图片描述&quot; /&gt;&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;服务：&lt;/strong&gt; 任务栏搜索框搜索&lt;code&gt;服务&lt;/code&gt;打开应用，找到，没启动的话启动一下&lt;br /&gt;
&lt;img src=&quot;assets/a8b08b7f2c76434c92f91b3d1b81b0ec.png&quot; alt=&quot;在这里插入图片描述&quot; /&gt;&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;组策略：&lt;/strong&gt; 任务栏搜索框搜索&lt;code&gt;编辑组策略&lt;/code&gt;，选择计算机配置&amp;gt;Windows设置&amp;gt;安全设置&amp;gt;本地策略&amp;gt;安全选项，找到&lt;code&gt;网络访问：本地帐户的共享和安全模型&lt;/code&gt;，把它改为经典；找到&lt;code&gt;帐户：使用空密码的本地帐户只允许进行控制台登录&lt;/code&gt;，把它禁用&lt;br /&gt;
&lt;img src=&quot;assets/fd47680462e7452c9e9b5917e09cbba7.png&quot; alt=&quot;在这里插入图片描述&quot; /&gt;&lt;/p&gt;
&lt;h2&gt;3.正确登录方式&lt;/h2&gt;
&lt;p&gt;把以上设置后，应该能正常连接，现在的问题是账户连接问题&lt;/p&gt;
&lt;h3&gt;3.1通过微软账户登录&lt;/h3&gt;
&lt;p&gt;针对电脑是使用用微软账户登录，而不是本地账户登录的电脑（不知道可&lt;code&gt;设置&lt;/code&gt;&amp;gt;&lt;code&gt;账户信息&lt;/code&gt;查看）&lt;br /&gt;
&lt;img src=&quot;assets/1904a6a9d61b45ebbc70accba32b3539.png&quot; alt=&quot;在这里插入图片描述&quot; /&gt;&lt;/p&gt;
&lt;p&gt;微软账号登录正常应该是输入邮箱作为账号，密码就微软账户密码即可，发现无法登录成功，是因为近段时间没有使用微软账户密码进行登录，如果你没有发现电脑锁定后有通过微软账户密码进行登录的选项，请把&lt;code&gt;设置&lt;/code&gt;&amp;gt;&lt;code&gt;账户&lt;/code&gt;&amp;gt;&lt;code&gt;登录选项&lt;/code&gt;，把&lt;code&gt;为了提高安全性，在此设备上仅允许使用WindowsHello登录Microsoft 帐户（推荐）&lt;/code&gt;即可&lt;br /&gt;
&lt;img src=&quot;assets/dc02ddc296e9456d9a9f90ec7caca2b5.png&quot; alt=&quot;在这里插入图片描述&quot; /&gt;&lt;br /&gt;
或者可以输入下方命令刷新密码，不过这个我未做尝试， &lt;code&gt;xxxx@qq.com&lt;/code&gt;是你的用户名&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;runas /u:MicrosoftAccount\xxxx@qq.com winver
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;此时我们锁定电脑，应该会有通过微软密码登录的选项，输入密码重新登录一下&lt;br /&gt;
&lt;img src=&quot;assets/80f3e8866720444ebdf9affce970ff52.png&quot; alt=&quot;在这里插入图片描述&quot; /&gt;&lt;/p&gt;
&lt;p&gt;此时尝试进行远程连接，应该是没问题了&lt;/p&gt;
&lt;h3&gt;3.2通过本地账户登录&lt;/h3&gt;
&lt;p&gt;由于我使用的是微软账户登录，所以没试过，应该可以直接使用本地用户名和密码登录（注意不是pin码，记不住可以在&lt;code&gt;设置&amp;gt;登录选项&amp;gt;密码&lt;/code&gt;更改），不行的话接着看下面方法。&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;注意：因为使用新创建的用户，可能部分数据或应用无法找到，因为不是原来的用户，应用的话安装时选择了只安装到当前用户，如要确保数据完整，最好还是&lt;strong&gt;当前微软账户或本地账户登录&lt;/strong&gt;&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;此方法，对于使用微软账户登录的电脑同样适用！！&lt;br /&gt;
任务栏搜索框搜索&lt;code&gt;计算机管理&lt;/code&gt;，打开，选择系统&lt;code&gt;工具&amp;gt;本地用户和组&amp;gt;用户&lt;/code&gt;&lt;br /&gt;
&lt;img src=&quot;assets/c3144232244c4b3d950bb1f5130614eb.png&quot; alt=&quot;在这里插入图片描述&quot; /&gt;&lt;/p&gt;
&lt;p&gt;用户名密码自己定义，记得把&lt;code&gt;用户下次登录时须更改密码(M)&lt;/code&gt;取消勾选，把&lt;code&gt;密码永不过期打开&lt;/code&gt;&lt;br /&gt;
&lt;img src=&quot;assets/dc9f5ff1e287404aa0705eb0cb7499d4.png&quot; alt=&quot;在这里插入图片描述&quot; /&gt;&lt;/p&gt;
&lt;p&gt;回到&lt;code&gt;设置&lt;/code&gt;&amp;gt;&lt;code&gt;系统&lt;/code&gt;&amp;gt;&lt;code&gt;远程桌面&lt;/code&gt;，选择远程桌面用户，点击添加，输入刚刚创建好的用户名，点击检查名称，选择我们需要的用户进行添加，然后确定（也可以在第一步，第二张图片点击&lt;code&gt;选择用户&lt;/code&gt;进入，是一样的）&lt;br /&gt;
&lt;img src=&quot;assets/5e68f55707fa4d42bc65399663fece12.png&quot; alt=&quot;在这里插入图片描述&quot; /&gt;&lt;/p&gt;
&lt;p&gt;此时，远程桌面用户应该已经有了我们的用户&lt;br /&gt;
&lt;img src=&quot;assets/6615c2979a7d4b72994b223686e0acf0.png&quot; alt=&quot;在这里插入图片描述&quot; /&gt;&lt;/p&gt;
&lt;p&gt;此时应该可以正常进行远程连接了&lt;/p&gt;
&lt;h2&gt;4.其它&lt;/h2&gt;
&lt;p&gt;电脑连接远程的话，打开桌面远程连接即可；手机或平板可选择使用Windows app来链接，苹果应用商店和Google Play都有&lt;/p&gt;
&lt;p&gt;首发来自我的 CSDN https://blog.csdn.net/weixin_45564332/article/details/149141381?spm=1001.2014.3001.5501&lt;/p&gt;
</content:encoded></item><item><title>Java线程安全与匿名内部类</title><link>https://yanghc.dev/posts/b0518014/</link><guid isPermaLink="true">https://yanghc.dev/posts/b0518014/</guid><pubDate>Thu, 13 Nov 2025 22:16:35 GMT</pubDate><content:encoded>&lt;h2&gt;✅ 一、局部变量 vs 成员变量&lt;/h2&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;特性&lt;/th&gt;
&lt;th&gt;局部变量（方法内）&lt;/th&gt;
&lt;th&gt;成员变量（类字段）&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;存储位置&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;栈（Stack）&lt;/td&gt;
&lt;td&gt;堆（Heap，随对象存在）&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;默认值&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;❌ 没有，默认必须显式初始化&lt;/td&gt;
&lt;td&gt;✅ 有（&lt;code&gt;0&lt;/code&gt;, &lt;code&gt;false&lt;/code&gt;, &lt;code&gt;null&lt;/code&gt; 等）&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;生命周期&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;方法调用期间&lt;/td&gt;
&lt;td&gt;对象存在期间&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;线程安全性&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;若不逃逸 → ✅ 安全  &amp;lt;br&amp;gt;若引用共享对象 → ❌ 可能不安全&lt;/td&gt;
&lt;td&gt;默认 ❌ 不安全（单例 Bean 中多线程共享）&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;blockquote&gt;
&lt;p&gt;📌 &lt;strong&gt;关键原则&lt;/strong&gt;：局部变量本身是线程私有的，但&lt;strong&gt;它引用的对象可能被共享&lt;/strong&gt;。&lt;/p&gt;
&lt;/blockquote&gt;
&lt;h2&gt;✅ 二、线程安全判断核心准则&lt;/h2&gt;
&lt;blockquote&gt;
&lt;p&gt;🔐 &lt;strong&gt;一个方法是否线程安全，取决于是否存在「多个线程共享可变状态」&lt;/strong&gt;。&lt;/p&gt;
&lt;/blockquote&gt;
&lt;ul&gt;
&lt;li&gt;✅ &lt;strong&gt;安全&lt;/strong&gt;：只操作局部创建
的对象，且对象&lt;strong&gt;不逃逸&lt;/strong&gt;到方法外（如不赋给成员变量、不返回、不传给共享组件）。&lt;/li&gt;
&lt;li&gt;❌ &lt;strong&gt;不安全&lt;/strong&gt;：操作了共享的可变状态（如实例变量、静态变量），且该状态&lt;strong&gt;非线程安全&lt;/strong&gt;（如 &lt;code&gt;ArrayList&lt;/code&gt;, &lt;code&gt;HashMap&lt;/code&gt;）。&lt;/li&gt;
&lt;/ul&gt;
&lt;blockquote&gt;
&lt;p&gt;💡 &lt;strong&gt;“对象逃逸”是关键&lt;/strong&gt;：即使变量是局部的，只要它指向的对象被多个线程访问，就可能不安全。&lt;/p&gt;
&lt;/blockquote&gt;
&lt;h2&gt;✅ 三、Lambda / 匿名内部类对局部变量的限制&lt;/h2&gt;
&lt;h3&gt;规则：&lt;/h3&gt;
&lt;blockquote&gt;
&lt;p&gt;被 lambda 或匿名内部类捕获的&lt;strong&gt;局部变量必须是 effectively final&lt;/strong&gt;（实质上不可变）。&lt;/p&gt;
&lt;/blockquote&gt;
&lt;ul&gt;
&lt;li&gt;✅ 允许：
&lt;ul&gt;
&lt;li&gt;读取基本类型变量（如 &lt;code&gt;int x = 5; System.out.println(x);&lt;/code&gt;）&lt;/li&gt;
&lt;li&gt;调用引用类型对象的方法（如 &lt;code&gt;log.append(...)&lt;/code&gt;）&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;❌ 禁止：
&lt;ul&gt;
&lt;li&gt;修改基本类型变量（&lt;code&gt;x++&lt;/code&gt;）&lt;/li&gt;
&lt;li&gt;重新赋值引用变量（&lt;code&gt;log = new StringBuilder();&lt;/code&gt;）&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;为什么？&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;编译器会将局部变量的&lt;strong&gt;值（基本类型）或引用（对象地址）拷贝&lt;/strong&gt;到内部类中；&lt;/li&gt;
&lt;li&gt;如果原变量后续被修改，会导致&lt;strong&gt;拷贝值与原始值不一致&lt;/strong&gt;，引发逻辑错误；&lt;/li&gt;
&lt;li&gt;所以 Java 强制要求：&lt;strong&gt;被捕获的局部变量不能变&lt;/strong&gt;。&lt;/li&gt;
&lt;/ul&gt;
&lt;blockquote&gt;
&lt;p&gt;⚠️ 注意：这个限制&lt;strong&gt;与是否用 lambda 无关&lt;/strong&gt;，匿名内部类同样受此约束。&lt;/p&gt;
&lt;/blockquote&gt;
&lt;h2&gt;✅ 四、对象 vs 变量：关键区分&lt;/h2&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;操作&lt;/th&gt;
&lt;th&gt;修改的是？&lt;/th&gt;
&lt;th&gt;是否违反 effectively final？&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;count++&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;局部变量本身的值&lt;/strong&gt;（栈上）&lt;/td&gt;
&lt;td&gt;✅ 违反&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;log.append(&quot;...&quot;)&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;堆上对象的内容&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;❌ 不违反（&lt;code&gt;log&lt;/code&gt; 引用未变）&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;log = new StringBuilder()&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;局部引用变量的值&lt;/strong&gt;（栈上地址）&lt;/td&gt;
&lt;td&gt;✅ 违反&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;blockquote&gt;
&lt;p&gt;🧠 &lt;strong&gt;记住&lt;/strong&gt;：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;变量 ≠ 对象&lt;/strong&gt;&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;引用变量的值 = 地址&lt;/strong&gt;&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;对象的内容 = 堆上的状态&lt;/strong&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;/blockquote&gt;
&lt;h2&gt;✅ 五、如何实现“可变但安全”的异步操作？&lt;/h2&gt;
&lt;p&gt;当需要在 lambda/异步任务中修改状态时：&lt;/p&gt;
&lt;h3&gt;推荐方案：&lt;/h3&gt;
&lt;ol&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;使用线程安全的可变容器&lt;/strong&gt;：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;AtomicInteger count = new AtomicInteger(0);
AtomicReference&amp;lt;StringBuilder&amp;gt; logRef = new AtomicReference&amp;lt;&amp;gt;(new StringBuilder());
&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;封装到自定义堆对象中&lt;/strong&gt;（确保不被多线程共享）：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;class Context { int count; StringBuilder log; }
Context ctx = new Context();
CompletableFuture.runAsync(() -&amp;gt; ctx.count++);
&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;/ol&gt;
&lt;blockquote&gt;
&lt;p&gt;✅ 只要每个调用创建独立对象，且仅被一个异步任务使用 → &lt;strong&gt;线程安全&lt;/strong&gt;。&lt;/p&gt;
&lt;/blockquote&gt;
&lt;h2&gt;✅ 六、Spring Bean 与线程安全&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;Spring 默认 Bean 是 &lt;strong&gt;单例（Singleton）&lt;/strong&gt;；&lt;/li&gt;
&lt;li&gt;所有请求共享同一个实例；&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;因此：&lt;/strong&gt;
&lt;ul&gt;
&lt;li&gt;❌ 避免在 Bean 中使用&lt;strong&gt;可变的实例变量&lt;/strong&gt;存储请求级数据；&lt;/li&gt;
&lt;li&gt;✅ 尽量让方法&lt;strong&gt;无状态（stateless）&lt;/strong&gt;，只用局部变量；&lt;/li&gt;
&lt;li&gt;✅ 如需状态，使用 &lt;code&gt;ThreadLocal&lt;/code&gt;、方法参数传递、或每次新建对象。&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
</content:encoded></item><item><title>Java 中 final 关键字的核心知识点总结</title><link>https://yanghc.dev/posts/41961988/</link><guid isPermaLink="true">https://yanghc.dev/posts/41961988/</guid><pubDate>Thu, 13 Nov 2025 22:15:54 GMT</pubDate><content:encoded>&lt;p&gt;&lt;code&gt;final&lt;/code&gt; 是 Java 中用于表达“不可变性”（Immutability）的关键字，可修饰变量、方法和类。其核心作用是限制变更，提升代码安全性与可读性。&lt;/p&gt;
&lt;h2&gt;1. &lt;code&gt;final&lt;/code&gt; 修饰变量&lt;/h2&gt;
&lt;h3&gt;局部变量&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;必须在声明时或使用前显式初始化；&lt;/li&gt;
&lt;li&gt;一旦赋值，不能重新赋值；&lt;/li&gt;
&lt;li&gt;若被匿名内部类或 Lambda 表达式捕获，必须是 &lt;strong&gt;effectively final&lt;/strong&gt;（实质上不可变），显式 &lt;code&gt;final&lt;/code&gt; 非必需但可增强语义。&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;成员变量（字段）&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;只能在声明处或构造器中赋值一次；&lt;/li&gt;
&lt;li&gt;对象构造完成后，引用不可更改；&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;注意&lt;/strong&gt;：&lt;code&gt;final&lt;/code&gt; 仅锁定引用，不阻止修改对象内部状态（如 &lt;code&gt;final List&lt;/code&gt; 仍可 &lt;code&gt;add()&lt;/code&gt;）。&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;引用类型 vs 基本类型&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;final int x = 10;&lt;/code&gt; → &lt;code&gt;x&lt;/code&gt; 的值不可变；&lt;/li&gt;
&lt;li&gt;&lt;code&gt;final StringBuilder sb = new StringBuilder();&lt;/code&gt; → &lt;code&gt;sb&lt;/code&gt; 引用不可变，但 &lt;code&gt;sb.append()&lt;/code&gt; 允许（对象内容可变）。&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;2. &lt;code&gt;final&lt;/code&gt; 修饰方法&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;禁止子类重写（override）；&lt;/li&gt;
&lt;li&gt;可用于保护关键逻辑不被覆盖；&lt;/li&gt;
&lt;li&gt;不影响继承和调用。&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;3. &lt;code&gt;final&lt;/code&gt; 修饰类&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;禁止被继承；&lt;/li&gt;
&lt;li&gt;常用于设计不可变类或安全敏感类（如 &lt;code&gt;String&lt;/code&gt;、&lt;code&gt;Integer&lt;/code&gt;）；&lt;/li&gt;
&lt;li&gt;提升安全性与 JVM 优化潜力。&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;4. &lt;code&gt;final&lt;/code&gt; 与线程安全&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;&lt;code&gt;final&lt;/code&gt; 本身不等于线程安全&lt;/strong&gt;；&lt;/li&gt;
&lt;li&gt;它保证：
&lt;ul&gt;
&lt;li&gt;引用不可变；&lt;/li&gt;
&lt;li&gt;在 Java 内存模型（JMM）下，正确构造的对象其 &lt;code&gt;final&lt;/code&gt; 字段对所有线程&lt;strong&gt;立即可见&lt;/strong&gt;；&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;线程安全性最终取决于所引用对象是否线程安全：
&lt;ul&gt;
&lt;li&gt;✅ &lt;code&gt;final String s = &quot;x&quot;&lt;/code&gt; → 线程安全（&lt;code&gt;String&lt;/code&gt; 不可变）；&lt;/li&gt;
&lt;li&gt;❌ &lt;code&gt;final List&amp;lt;String&amp;gt; list = new ArrayList&amp;lt;&amp;gt;()&lt;/code&gt; + 多线程修改 → &lt;strong&gt;非线程安全&lt;/strong&gt;。&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;5. 最佳实践&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;优先将成员变量声明为 &lt;code&gt;final&lt;/code&gt;，尤其是依赖注入字段；&lt;/li&gt;
&lt;li&gt;构造不可变对象时，所有字段应为 &lt;code&gt;final&lt;/code&gt;；&lt;/li&gt;
&lt;li&gt;结合不可变集合（如 &lt;code&gt;List.of()&lt;/code&gt;、&lt;code&gt;Collections.unmodifiableList()&lt;/code&gt;）实现真正不可变状态；&lt;/li&gt;
&lt;li&gt;避免误认为 &lt;code&gt;final&lt;/code&gt; 能自动保证并发安全。&lt;/li&gt;
&lt;/ul&gt;
&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;核心原则&lt;/strong&gt;：&lt;code&gt;final&lt;/code&gt; 锁定的是“标识符的绑定”，而非“对象的内容”。理解这一点，是正确使用 &lt;code&gt;final&lt;/code&gt; 的关键。&lt;/p&gt;
&lt;/blockquote&gt;
</content:encoded></item><item><title>面试题01</title><link>https://yanghc.dev/posts/dabb34ff/</link><guid isPermaLink="true">https://yanghc.dev/posts/dabb34ff/</guid><pubDate>Mon, 10 Nov 2025 16:33:03 GMT</pubDate><content:encoded>&lt;h2&gt;01cookie、session、token的区别，以及使用场景    （为什么要用cookie、session、token，什么时候用）&lt;/h2&gt;
&lt;h3&gt;一、基本概念&lt;/h3&gt;
&lt;h4&gt;1. Cookie&lt;/h4&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;定义&lt;/strong&gt;：由服务器发送给客户端（浏览器）的一小段数据，浏览器会自动保存并在后续请求中自动携带。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;存储位置&lt;/strong&gt;：客户端（浏览器）。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;生命周期&lt;/strong&gt;：可设置过期时间（持久 Cookie）或仅在当前会话有效（会话 Cookie）。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;安全性&lt;/strong&gt;：可通过 &lt;code&gt;HttpOnly&lt;/code&gt;、&lt;code&gt;Secure&lt;/code&gt;、&lt;code&gt;SameSite&lt;/code&gt; 等属性增强安全。&lt;/li&gt;
&lt;/ul&gt;
&lt;h4&gt;2. Session&lt;/h4&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;定义&lt;/strong&gt;：服务器端保存的用户会话数据，通常通过一个唯一 ID（Session ID）与客户端关联。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;存储位置&lt;/strong&gt;：服务器（如内存、Redis、数据库等）。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;关联方式&lt;/strong&gt;：Session ID 一般通过 Cookie 传递（也可通过 URL 参数）。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;生命周期&lt;/strong&gt;：由服务器控制，可设置超时时间。&lt;/li&gt;
&lt;/ul&gt;
&lt;h4&gt;3. Token（通常指 JWT）&lt;/h4&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;定义&lt;/strong&gt;：一种自包含的令牌（如 JSON Web Token），包含用户信息和签名，无需服务器存储状态。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;存储位置&lt;/strong&gt;：客户端（可存在 Cookie、localStorage、sessionStorage 等）。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;结构&lt;/strong&gt;：Header + Payload + Signature，Payload 可包含用户 ID、角色、过期时间等。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;无状态&lt;/strong&gt;：服务器无需保存 Token，只需验证签名即可。&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;二、核心区别对比&lt;/h3&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;特性&lt;/th&gt;
&lt;th&gt;Cookie&lt;/th&gt;
&lt;th&gt;Session&lt;/th&gt;
&lt;th&gt;Token (JWT)&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;存储位置&lt;/td&gt;
&lt;td&gt;客户端&lt;/td&gt;
&lt;td&gt;服务器&lt;/td&gt;
&lt;td&gt;客户端&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;是否需要服务器存储&lt;/td&gt;
&lt;td&gt;否&lt;/td&gt;
&lt;td&gt;是&lt;/td&gt;
&lt;td&gt;否（无状态）&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;自动携带&lt;/td&gt;
&lt;td&gt;是（同域下自动随请求发送）&lt;/td&gt;
&lt;td&gt;依赖 Cookie 或 URL&lt;/td&gt;
&lt;td&gt;否（需手动添加到请求头）&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;安全性&lt;/td&gt;
&lt;td&gt;中（易受 CSRF 攻击）&lt;/td&gt;
&lt;td&gt;高（数据在服务端）&lt;/td&gt;
&lt;td&gt;高（但需防 XSS）&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;跨域支持&lt;/td&gt;
&lt;td&gt;困难（受同源策略限制）&amp;lt;br&amp;gt;&lt;/td&gt;
&lt;td&gt;困难&lt;/td&gt;
&lt;td&gt;容易（适合前后端分离/跨域）&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;扩展性&lt;/td&gt;
&lt;td&gt;差（依赖服务器状态）&lt;/td&gt;
&lt;td&gt;差（需共享 Session）&lt;/td&gt;
&lt;td&gt;好（天然支持分布式）&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;生命周期管理&lt;/td&gt;
&lt;td&gt;可设过期时间&lt;/td&gt;
&lt;td&gt;服务端控制&lt;/td&gt;
&lt;td&gt;Token 内置过期时间&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;/td&gt;
&lt;td&gt;&lt;/td&gt;
&lt;td&gt;&lt;/td&gt;
&lt;td&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;blockquote&gt;
&lt;p&gt;同源：
协议、域名、端口完全相同，即非跨域&lt;/p&gt;
&lt;/blockquote&gt;
&lt;h3&gt;三、为什么需要它们？&lt;/h3&gt;
&lt;p&gt;Web 协议 HTTP 是&lt;strong&gt;无状态&lt;/strong&gt;的，每次请求都是独立的。为了实现“记住用户登录状态”、“个性化内容”等功能，就需要某种机制来&lt;strong&gt;维持状态或识别用户身份&lt;/strong&gt;。&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;Cookie&lt;/strong&gt;：解决客户端如何携带标识的问题。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Session&lt;/strong&gt;：解决服务器如何记住用户状态的问题。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Token&lt;/strong&gt;：解决在无状态架构（如 RESTful API、微服务）中如何安全认证的问题。&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;四、使用场景建议&lt;/h3&gt;
&lt;h4&gt;使用 Cookie + Session 的场景：&lt;/h4&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;传统 Web 应用&lt;/strong&gt;（如 PHP、Java Web、Django 等服务端渲染应用）&lt;/li&gt;
&lt;li&gt;用户登录后跳转页面，保持登录状态&lt;/li&gt;
&lt;li&gt;对安全性要求高，且不涉及跨域&lt;/li&gt;
&lt;li&gt;例如：电商后台、企业管理系统&lt;/li&gt;
&lt;/ul&gt;
&lt;blockquote&gt;
&lt;p&gt;优点：简单、成熟、自动管理；&lt;br /&gt;
缺点：难以扩展到分布式系统，跨域麻烦。&lt;/p&gt;
&lt;/blockquote&gt;
&lt;h4&gt;使用 Token（如 JWT）的场景：&lt;/h4&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;前后端分离架构&lt;/strong&gt;（React/Vue + RESTful API）&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;移动端 App / 小程序&lt;/strong&gt; 与后端通信&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;微服务架构&lt;/strong&gt;，需要无状态认证&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;跨域或第三方集成&lt;/strong&gt;（如 OAuth2）
&lt;code&gt;jti&lt;/code&gt;（JWT ID）是 JWT（JSON Web Token）中的一个&lt;strong&gt;可选标准声明字段&lt;/strong&gt;，用于&lt;strong&gt;唯一标识一个具体的 Token 实例&lt;/strong&gt;。它的核心作用是：&lt;strong&gt;让每个签发的Token 都有一个“身份证号”&lt;/strong&gt;，便于后续追踪、吊销或审计。可以把jti储存在Redis中，作为黑名单&lt;/li&gt;
&lt;/ul&gt;
&lt;blockquote&gt;
&lt;p&gt;优点：无状态、易扩展、跨平台；&lt;br /&gt;
缺点：Token 一旦签发无法主动失效（除非引入黑名单），Payload 不宜过大。&lt;/p&gt;
&lt;/blockquote&gt;
&lt;h4&gt;⚠️ 注意事项&lt;/h4&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;CSRF（跨站请求伪造）&lt;/strong&gt;：Cookie + Session 易受攻击，需用 &lt;code&gt;SameSite=Strict/Lax&lt;/code&gt; 或 CSRF Token 防护。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;XSS（跨站脚本）&lt;/strong&gt;：若 Token 存在 localStorage，可能被 JS 窃取；建议存入 &lt;code&gt;HttpOnly Cookie&lt;/code&gt; 并配合 &lt;code&gt;SameSite&lt;/code&gt;。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;敏感信息&lt;/strong&gt;：不要在 Cookie 或 Token Payload 中存放密码等敏感数据。&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;02.Redis能干点啥&lt;/h2&gt;
&lt;h3&gt;Redis 的核心作用（按使用场景分类）&lt;/h3&gt;
&lt;h4&gt;1. &lt;strong&gt;缓存（Cache）—— 最常见用途&lt;/strong&gt;&lt;/h4&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;目的&lt;/strong&gt;：减轻数据库压力，提升系统响应速度。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;原理&lt;/strong&gt;：将热点数据（如用户信息、商品详情、文章内容）存入 Redis，后续请求直接读缓存，避免查数据库。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;典型场景&lt;/strong&gt;：
&lt;ul&gt;
&lt;li&gt;Web 页面缓存&lt;/li&gt;
&lt;li&gt;API 接口结果缓存&lt;/li&gt;
&lt;li&gt;数据库查询结果缓存（如 MyBatis + Redis）&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;优势&lt;/strong&gt;：读写性能极高（10万+ QPS），延迟微秒级。&lt;/li&gt;
&lt;/ul&gt;
&lt;blockquote&gt;
&lt;p&gt;💡 示例：用户访问商品页 → 先查 Redis → 有则返回，无则查 DB 并写入 Redis。&lt;/p&gt;
&lt;/blockquote&gt;
&lt;h4&gt;2. &lt;strong&gt;分布式会话存储（Session Store）&lt;/strong&gt;&lt;/h4&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;问题&lt;/strong&gt;：传统 Session 存在单机内存中，无法用于负载均衡下的多台服务器。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;解决方案&lt;/strong&gt;：将 Session 数据（如用户登录状态）统一存到 Redis。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;效果&lt;/strong&gt;：任意后端节点都能读取用户会话，实现&lt;strong&gt;无状态服务 + 水平扩展&lt;/strong&gt;。&lt;/li&gt;
&lt;/ul&gt;
&lt;blockquote&gt;
&lt;p&gt;✅ 常用于：Java（Spring Session + Redis）、Node.js、PHP 等 Web 应用。&lt;/p&gt;
&lt;/blockquote&gt;
&lt;h4&gt;3. &lt;strong&gt;分布式锁（Distributed Lock）&lt;/strong&gt;&lt;/h4&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;目的&lt;/strong&gt;：在分布式系统中，保证多个进程/服务对共享资源的互斥访问。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;实现方式&lt;/strong&gt;：利用 &lt;code&gt;SET key value NX EX&lt;/code&gt; 命令（原子操作）。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;典型场景&lt;/strong&gt;：
&lt;ul&gt;
&lt;li&gt;秒杀库存扣减&lt;/li&gt;
&lt;li&gt;防止重复提交订单&lt;/li&gt;
&lt;li&gt;定时任务只在一个节点执行&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;blockquote&gt;
&lt;p&gt;⚠️ 注意：需处理锁过期、续期、误删等问题（可结合 Redlock 或 Lua 脚本）。&lt;/p&gt;
&lt;/blockquote&gt;
&lt;h4&gt;4. &lt;strong&gt;消息队列（Message Queue）—— 轻量级&lt;/strong&gt;&lt;/h4&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;数据结构&lt;/strong&gt;：使用 &lt;code&gt;List&lt;/code&gt;（LPUSH + BRPOP）或 &lt;code&gt;Stream&lt;/code&gt;（Redis 5.0+）。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;适用场景&lt;/strong&gt;：
&lt;ul&gt;
&lt;li&gt;异步任务处理（如发邮件、日志收集）&lt;/li&gt;
&lt;li&gt;解耦生产者和消费者&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;优点&lt;/strong&gt;：简单、快速、无需引入 Kafka/RabbitMQ&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;缺点&lt;/strong&gt;：功能不如专业 MQ（无 ACK、持久化弱、无路由）&lt;/li&gt;
&lt;/ul&gt;
&lt;blockquote&gt;
&lt;p&gt;✅ 适合中小型项目或临时任务队列。&lt;/p&gt;
&lt;/blockquote&gt;
&lt;h4&gt;5. &lt;strong&gt;计数器 &amp;amp; 限流（Rate Limiting）&lt;/strong&gt;&lt;/h4&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;数据结构&lt;/strong&gt;：&lt;code&gt;String&lt;/code&gt;（INCR）或 &lt;code&gt;ZSet&lt;/code&gt;（滑动窗口）&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;典型应用&lt;/strong&gt;：
&lt;ul&gt;
&lt;li&gt;接口调用次数统计&lt;/li&gt;
&lt;li&gt;用户登录失败次数限制&lt;/li&gt;
&lt;li&gt;短信发送频率控制&lt;/li&gt;
&lt;li&gt;分布式 ID 生成（INCR）&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;blockquote&gt;
&lt;p&gt;💡 示例：&lt;code&gt;INCR user:123:login_attempts&lt;/code&gt; + &lt;code&gt;EXPIRE&lt;/code&gt; 实现 5 分钟内最多试错 5 次。&lt;/p&gt;
&lt;/blockquote&gt;
&lt;h4&gt;6. &lt;strong&gt;排行榜 &amp;amp; 实时统计（Leaderboard）&lt;/strong&gt;&lt;/h4&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;数据结构&lt;/strong&gt;：&lt;code&gt;Sorted Set（ZSet）&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;原理&lt;/strong&gt;：成员（member）+ 分数（score），自动排序。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;场景&lt;/strong&gt;：
&lt;ul&gt;
&lt;li&gt;游戏积分榜&lt;/li&gt;
&lt;li&gt;热门文章 Top 10&lt;/li&gt;
&lt;li&gt;实时销售额排名&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;blockquote&gt;
&lt;p&gt;✅ 支持高效插入、更新、范围查询（如 &lt;code&gt;ZREVRANGE&lt;/code&gt;）。&lt;/p&gt;
&lt;/blockquote&gt;
&lt;h4&gt;7. &lt;strong&gt;发布/订阅（Pub/Sub）—— 实时通信&lt;/strong&gt;&lt;/h4&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;模式&lt;/strong&gt;：消息广播机制&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;命令&lt;/strong&gt;：&lt;code&gt;PUBLISH channel msg&lt;/code&gt; / &lt;code&gt;SUBSCRIBE channel&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;应用场景&lt;/strong&gt;：
&lt;ul&gt;
&lt;li&gt;聊天室消息推送&lt;/li&gt;
&lt;li&gt;系统通知广播&lt;/li&gt;
&lt;li&gt;微服务间简单事件通知&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;blockquote&gt;
&lt;p&gt;⚠️ 缺点：消息不持久（订阅者离线会丢消息），不适合可靠消息场景。&lt;/p&gt;
&lt;/blockquote&gt;
&lt;h4&gt;8. &lt;strong&gt;位图（Bitmap）与布隆过滤器（Bloom Filter）&lt;/strong&gt;&lt;/h4&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;Bitmap&lt;/strong&gt;：用 bit 位存储状态，节省空间&lt;br /&gt;
→ 如：用户签到（&lt;code&gt;SETBIT user:123:sign 20251110 1&lt;/code&gt;）&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Bloom Filter&lt;/strong&gt;（需 RedisBloom 模块）：&lt;br /&gt;
→ 高效判断“某元素可能存在”（如防止缓存穿透）&lt;/li&gt;
&lt;/ul&gt;
&lt;h4&gt;9. &lt;strong&gt;地理位置（GEO）&lt;/strong&gt;&lt;/h4&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;命令&lt;/strong&gt;：&lt;code&gt;GEOADD&lt;/code&gt;、&lt;code&gt;GEORADIUS&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;场景&lt;/strong&gt;：附近的人、门店搜索&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;底层&lt;/strong&gt;：基于 Sorted Set + GeoHash&lt;/li&gt;
&lt;/ul&gt;
&lt;h4&gt;10. &lt;strong&gt;作为 JWT 黑名单/白名单存储（认证控制）&lt;/strong&gt;&lt;/h4&gt;
&lt;ul&gt;
&lt;li&gt;存储被吊销的 Token ID（&lt;code&gt;jti&lt;/code&gt;）&lt;/li&gt;
&lt;li&gt;控制用户登出、权限变更等实时生效&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;03.hashmap，hashtable，concurrenthashmap的区别&lt;/h2&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;特性&lt;/th&gt;
&lt;th&gt;&lt;code&gt;HashMap&lt;/code&gt;&lt;/th&gt;
&lt;th&gt;&lt;code&gt;Hashtable&lt;/code&gt;&lt;/th&gt;
&lt;th&gt;&lt;code&gt;ConcurrentHashMap&lt;/code&gt;&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;线程安全&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;❌ 不安全&lt;/td&gt;
&lt;td&gt;✅ 安全（全表锁）&lt;/td&gt;
&lt;td&gt;✅ 安全（分段锁 / CAS + synchronized）&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;null 键/值&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;✅ 允许一个 &lt;code&gt;null&lt;/code&gt; key，多个 &lt;code&gt;null&lt;/code&gt; value&lt;/td&gt;
&lt;td&gt;❌ 不允许 &lt;code&gt;null&lt;/code&gt; key 或 value&lt;/td&gt;
&lt;td&gt;❌ 不允许 &lt;code&gt;null&lt;/code&gt; key 或 value&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;底层结构&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;数组 + 链表 + 红黑树（JDK 8+）&lt;/td&gt;
&lt;td&gt;数组 + 链表&lt;/td&gt;
&lt;td&gt;数组 + 链表 + 红黑树（JDK 8+）&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;锁粒度&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;无锁&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;方法级 synchronized（整表锁）&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;分段锁（JDK 7）→ CAS + synchronized（JDK 8+，锁单个桶）&lt;/strong&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;性能（并发）&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;最高（无锁）&lt;/td&gt;
&lt;td&gt;最低（高并发下严重阻塞）&lt;/td&gt;
&lt;td&gt;高（并发读不加锁，写只锁部分）&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;迭代器&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;fail-fast&lt;/td&gt;
&lt;td&gt;fail-fast&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;弱一致性（不会抛 ConcurrentModificationException）&lt;/strong&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;继承关系&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;AbstractMap&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;Dictionary&lt;/code&gt;（已废弃）&lt;/td&gt;
&lt;td&gt;&lt;code&gt;AbstractMap&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;h2&gt;04rabbitmq消息有序性&lt;/h2&gt;
&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;保证同一业务实体（如订单ID、用户ID）的消息处理顺序，不同实体之间可并行处理。&lt;/strong&gt;&lt;/p&gt;
&lt;/blockquote&gt;
&lt;h3&gt;🔑 核心思想：&lt;strong&gt;分区（Partitioning） + 单线程消费&lt;/strong&gt;&lt;/h3&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;原则&lt;/th&gt;
&lt;th&gt;说明&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;1. 同一业务 ID 的消息必须进入同一个队列&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;通过 &lt;code&gt;hash(业务ID) % N&lt;/code&gt; 路由到固定队列&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;2. 每个队列只由一个消费者（单线程）处理&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;避免并发打乱顺序&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;3. RabbitMQ 队列本身是 FIFO&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;入队顺序 = 出队顺序，天然保序&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;h3&gt;🛠️ 实现步骤&lt;/h3&gt;
&lt;ol&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;预创建多个队列&lt;/strong&gt;（如 &lt;code&gt;order.queue.0&lt;/code&gt; ~ &lt;code&gt;order.queue.3&lt;/code&gt;）&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;生产者按业务 ID 路由&lt;/strong&gt;：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;int index = Math.abs(orderId.hashCode()) % queueCount;
channel.basicPublish(&quot;&quot;, &quot;order.queue.&quot; + index, ..., message);
&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;每个队列绑定一个单线程消费者&lt;/strong&gt;（禁止多线程并发消费同一队列）&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;消费者关闭 prefetch，手动 ACK&lt;/strong&gt;：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;channel.basicQos(1); // 每次只取1条
// 处理成功后再 basicAck
&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;/ol&gt;
&lt;h3&gt;⚠️ 关键前提（缺一不可）&lt;/h3&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;环节&lt;/th&gt;
&lt;th&gt;要求&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;生产者&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;同一业务 ID 的消息必须&lt;strong&gt;按序发送&lt;/strong&gt;（避免并发发同一流消息）&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Broker&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;队列 FIFO 特性保证入队顺序&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;消费者&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;单线程处理&lt;/strong&gt; + &lt;strong&gt;不 requeue 到队首&lt;/strong&gt;（失败应进死信队列）&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;h2&gt;05.什么时候用rabbitmq&lt;/h2&gt;
&lt;h3&gt;1. &lt;strong&gt;应用解耦（Decoupling）&lt;/strong&gt;&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;问题&lt;/strong&gt;：模块 A 直接调用模块 B，B 挂了 → A 也失败。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;RabbitMQ 方案&lt;/strong&gt;：A 发消息到队列，B 异步消费。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;例子&lt;/strong&gt;：
&lt;ul&gt;
&lt;li&gt;用户注册后，发邮件、发短信、写日志 → 各自独立消费&lt;/li&gt;
&lt;li&gt;订单创建后，通知库存、物流、积分系统&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;blockquote&gt;
&lt;p&gt;🎯 &lt;strong&gt;价值&lt;/strong&gt;：系统间松耦合，故障隔离。&lt;/p&gt;
&lt;/blockquote&gt;
&lt;h3&gt;2. &lt;strong&gt;异步处理（Asynchronous Processing）&lt;/strong&gt;&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;问题&lt;/strong&gt;：用户点击“下单”，要等 2 秒（因为同步调支付、发短信…），体验差。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;RabbitMQ 方案&lt;/strong&gt;：下单成功后发消息，后台慢慢处理。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;效果&lt;/strong&gt;：接口响应从 2s → 50ms。&lt;/li&gt;
&lt;/ul&gt;
&lt;blockquote&gt;
&lt;p&gt;🎯 &lt;strong&gt;价值&lt;/strong&gt;：提升用户体验，降低请求链路延迟。&lt;/p&gt;
&lt;/blockquote&gt;
&lt;h3&gt;3. &lt;strong&gt;流量削峰（Traffic Shaping / Peak Shaving）&lt;/strong&gt;&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;问题&lt;/strong&gt;：秒杀活动瞬间 10 万请求，数据库扛不住。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;RabbitMQ 方案&lt;/strong&gt;：请求先入队列，消费者按能力匀速处理（比如每秒 1000 单）。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;注意&lt;/strong&gt;：需配合限流 + 排队提示（“正在排队中…”）&lt;/li&gt;
&lt;/ul&gt;
&lt;blockquote&gt;
&lt;p&gt;🎯 &lt;strong&gt;价值&lt;/strong&gt;：保护后端系统不被冲垮。&lt;/p&gt;
&lt;/blockquote&gt;
&lt;h3&gt;4. &lt;strong&gt;最终一致性（Eventual Consistency）&lt;/strong&gt;&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;问题&lt;/strong&gt;：分布式事务太重（如 Seata），但又需要数据一致。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;RabbitMQ 方案&lt;/strong&gt;：通过“事件驱动”实现补偿。
&lt;ul&gt;
&lt;li&gt;例：扣款成功 → 发 &lt;code&gt;PaymentSuccessEvent&lt;/code&gt; → 库存服务消费并减库存&lt;/li&gt;
&lt;li&gt;失败则重试 or 进死信队列人工干预&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;blockquote&gt;
&lt;p&gt;🎯 &lt;strong&gt;价值&lt;/strong&gt;：轻量级实现跨服务数据同步。&lt;/p&gt;
&lt;/blockquote&gt;
&lt;h3&gt;5. &lt;strong&gt;广播通知（Pub/Sub）&lt;/strong&gt;&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;问题&lt;/strong&gt;：一个事件要通知多个系统（如“用户注销”要清空缓存、踢掉 WebSocket、记录审计日志）。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;RabbitMQ 方案&lt;/strong&gt;：用 &lt;strong&gt;Exchange + 多个 Queue 绑定&lt;/strong&gt;，实现一对多广播。&lt;/li&gt;
&lt;/ul&gt;
&lt;blockquote&gt;
&lt;p&gt;🎯 &lt;strong&gt;价值&lt;/strong&gt;：避免硬编码通知列表。&lt;/p&gt;
&lt;/blockquote&gt;
&lt;h3&gt;❌ 二、不适合使用 RbbitMQ 的场景&lt;/h3&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;场景&lt;/th&gt;
&lt;th&gt;原因&lt;/th&gt;
&lt;th&gt;更佳选择&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;需要强顺序（全局有序）&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;RabbitMQ 不天然支持&lt;/td&gt;
&lt;td&gt;Kafka / RocketMQ&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;需要消息回溯（重复消费历史消息）&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;队列消费后即删除&lt;/td&gt;
&lt;td&gt;Kafka（持久化日志）&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;超大消息（&amp;gt; 10MB）&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;内存压力大，性能差&lt;/td&gt;
&lt;td&gt;文件存储 + 消息传 URL&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;低延迟要求（&amp;lt; 10ms）&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;网络 + Broker 开销&lt;/td&gt;
&lt;td&gt;直接 RPC / gRPC&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;简单定时任务&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;杀鸡用牛刀&lt;/td&gt;
&lt;td&gt;Quartz / XXL-JOB&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;高吞吐日志收集（每秒百万条）&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;性能不如专用系统&lt;/td&gt;
&lt;td&gt;Kafka / Fluentd&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;h2&gt;06.秒杀实现&lt;/h2&gt;
&lt;h3&gt;🎯 核心目标&lt;/h3&gt;
&lt;ol&gt;
&lt;li&gt;&lt;strong&gt;高性能&lt;/strong&gt;：扛住瞬时高并发（万级 QPS）&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;一致性&lt;/strong&gt;：不能超卖（卖出去的 ≤ 实际库存）&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;可靠性&lt;/strong&gt;：即使系统崩溃，数据最终一致&lt;/li&gt;
&lt;/ol&gt;
&lt;h3&gt;阶段 1️⃣：库存预热 —— 从数据库加载到 Redis&lt;/h3&gt;
&lt;h4&gt;⏰ 时机&lt;/h4&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;秒杀开始前&lt;/strong&gt;（比如提前 5 分钟、1 小时，或商品上架时）&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;不能在秒杀请求到来时才查 DB&lt;/strong&gt;（会压垮数据库）&lt;/li&gt;
&lt;/ul&gt;
&lt;h4&gt;💡 做什么&lt;/h4&gt;
&lt;p&gt;将商品的&lt;strong&gt;可售库存&lt;/strong&gt;写入 Redis，作为秒杀的“令牌池”。&lt;/p&gt;
&lt;h4&gt;✅ 示例&lt;/h4&gt;
&lt;pre&gt;&lt;code&gt;-- 数据库商品表
SELECT id, stock FROM products WHERE id = 1001;
-- 结果：stock = 100
&lt;/code&gt;&lt;/pre&gt;
&lt;pre&gt;&lt;code&gt;# 写入 Redis（Key 设计很重要！）
SET stock:1001 100   # 初始库存 100
EXPIRE stock:1001 3600  # 可选：设置过期时间
&lt;/code&gt;&lt;/pre&gt;
&lt;blockquote&gt;
&lt;p&gt;📌 &lt;strong&gt;关键点&lt;/strong&gt;：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Redis 中的库存 = &lt;strong&gt;本次秒杀可售数量&lt;/strong&gt;（可能是总库存，也可能是单独配置的秒杀库存）&lt;/li&gt;
&lt;li&gt;通常用 &lt;strong&gt;String 类型&lt;/strong&gt;（方便 &lt;code&gt;DECR&lt;/code&gt; 原子操作）&lt;/li&gt;
&lt;/ul&gt;
&lt;/blockquote&gt;
&lt;hr /&gt;
&lt;h3&gt;阶段 2️⃣：秒杀扣减 —— 在 Redis 中原子操作&lt;/h3&gt;
&lt;h4&gt;🚀 用户点击“抢购”时&lt;/h4&gt;
&lt;p&gt;后端只做一件事：&lt;strong&gt;原子扣减 Redis 库存&lt;/strong&gt;&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;// Java + Lettuce / Jedis 示例
String key = &quot;stock:1001&quot;;
Long result = redis.decr(key); // 原子减 1

if (result &amp;gt;= 0) {
    // ✅ 抢到了！返回“下单成功”
    return &quot;success&quot;;
} else {
    // ❌ 库存不足（可能被别人抢光了）
    redis.incr(key); // 可选：回滚（但通常不回滚，因为 result&amp;lt;0 说明已超卖）
    return &quot;sold out&quot;;
}
&lt;/code&gt;&lt;/pre&gt;
&lt;blockquote&gt;
&lt;p&gt;⚠️ &lt;strong&gt;为什么安全？&lt;/strong&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;DECR&lt;/code&gt; 是 Redis 单线程原子操作，天然防并发超卖&lt;/li&gt;
&lt;li&gt;即使 10 万人同时请求，Redis 也能保证最多 100 人成功&lt;/li&gt;
&lt;/ul&gt;
&lt;/blockquote&gt;
&lt;blockquote&gt;
&lt;p&gt;📌 &lt;strong&gt;注意&lt;/strong&gt;：&lt;br /&gt;
有些系统会用 &lt;code&gt;GET + SET&lt;/code&gt; 或 Lua 脚本做更复杂的校验（如每人限购），但核心仍是&lt;strong&gt;原子性&lt;/strong&gt;。&lt;/p&gt;
&lt;/blockquote&gt;
&lt;hr /&gt;
&lt;h3&gt;阶段 3️⃣：异步落库 —— 订单创建 &amp;amp; 数据库库存扣减&lt;/h3&gt;
&lt;h4&gt;📦 关键思想：&lt;strong&gt;“先抢资格，后建订单”&lt;/strong&gt;&lt;/h4&gt;
&lt;p&gt;当 Redis 扣减成功后：&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;立即返回用户：“抢购成功，请支付”&lt;/strong&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;同时发送一条消息到 RabbitMQ/Kafka&lt;/strong&gt;，内容如：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;{
  &quot;userId&quot;: 123,
  &quot;productId&quot;: 1001,
  &quot;orderId&quot;: &quot;O20251110123456&quot;
}
&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;/ol&gt;
&lt;h4&gt;🔄 后台消费者处理消息（异步）&lt;/h4&gt;
&lt;pre&gt;&lt;code&gt;// 消费者收到消息
void handleOrderMessage(OrderMsg msg) {
    try {
        // 1. 创建订单（写订单表）
        orderDao.insert(new Order(msg.orderId, msg.userId, msg.productId));

        // 2. 【可选】扣减数据库库存（如果 Redis 库存 ≠ DB 库存）
        //    注意：这里要防重复消费！
        if (!isProcessed(msg.orderId)) {
            productDao.decreaseStock(msg.productId, 1);
            markAsProcessed(msg.orderId);
        }

        // 3. 发通知、积分、日志等...
    } catch (Exception e) {
        // 失败则重试 or 进死信队列
        requeueToDLQ(msg);
    }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;❓ 你可能会问：&lt;strong&gt;数据库库存还要扣吗？&lt;/strong&gt;&lt;/h3&gt;
&lt;p&gt;这取决于你的库存模型：&lt;/p&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;方案&lt;/th&gt;
&lt;th&gt;说明&lt;/th&gt;
&lt;th&gt;是否需要扣 DB 库存&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;✅ 纯 Redis 库存&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;秒杀库存独立于 DB，DB 只存“总库存”&lt;/td&gt;
&lt;td&gt;❌ 不需要（但需对账）&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;🔄 Redis + DB 双写&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Redis 是缓存，DB 是权威&lt;/td&gt;
&lt;td&gt;✅ 需要（最终一致）&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;🧩 分离库存&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;DB 存“总库存”，Redis 存“秒杀专用库存”&lt;/td&gt;
&lt;td&gt;❌ 秒杀库存扣完即可，DB 总库存定期同步&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;blockquote&gt;
&lt;p&gt;💡 &lt;strong&gt;主流做法&lt;/strong&gt;：&lt;br /&gt;
&lt;strong&gt;秒杀用独立库存（Redis）&lt;/strong&gt;，和普通销售库存分开。&lt;br /&gt;
秒杀结束后，再把“已售数量”同步到 DB 做汇总。&lt;/p&gt;
&lt;/blockquote&gt;
&lt;h3&gt;🔐 如何保证“不超卖 + 不少卖”？&lt;/h3&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;风险&lt;/th&gt;
&lt;th&gt;解决方案&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Redis 扣了，但订单创建失败&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;消息队列重试 + 幂等处理（订单 ID 唯一）&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;重复消费导致多扣库存&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;消费者做幂等（如用订单 ID 去重）&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Redis 崩溃丢失库存&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;持久化（AOF）+ 高可用（哨兵/集群）&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;DB 和 Redis 不一致&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;定时对账任务（补偿）&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;blockquote&gt;
&lt;p&gt;🛡️ &lt;strong&gt;终极保障&lt;/strong&gt;：&lt;br /&gt;
即使中间环节出错，&lt;strong&gt;对账系统&lt;/strong&gt;会在事后修复数据（比如发现 Redis 卖了 100，DB 只扣了 98 → 补扣 2）。&lt;/p&gt;
&lt;/blockquote&gt;
&lt;h3&gt;🌰 完整流程示例（以 100 件商品为例）&lt;/h3&gt;
&lt;ol&gt;
&lt;li&gt;&lt;strong&gt;预热&lt;/strong&gt;：&lt;code&gt;SET stock:1001 100&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;秒杀开始&lt;/strong&gt;：
&lt;ul&gt;
&lt;li&gt;用户 A 请求 → &lt;code&gt;DECR&lt;/code&gt; → 返回 99 ≥ 0 → 成功&lt;/li&gt;
&lt;li&gt;用户 B 请求 → &lt;code&gt;DECR&lt;/code&gt; → 返回 98 ≥ 0 → 成功&lt;/li&gt;
&lt;li&gt;...&lt;/li&gt;
&lt;li&gt;第 100 个用户 → &lt;code&gt;DECR&lt;/code&gt; → 返回 0 ≥ 0 → 成功&lt;/li&gt;
&lt;li&gt;第 101 个用户 → &lt;code&gt;DECR&lt;/code&gt; → 返回 -1 &amp;lt; 0 → 失败&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;异步&lt;/strong&gt;：100 条消息进入 MQ，订单服务慢慢创建 100 个订单&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;结束&lt;/strong&gt;：DB 中该商品“秒杀已售”字段更新为 100&lt;/li&gt;
&lt;/ol&gt;
&lt;h3&gt;✅ 总结：秒杀库存流转三原则&lt;/h3&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;阶段&lt;/th&gt;
&lt;th&gt;原则&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;预热&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;提前加载，绝不现场查 DB&lt;/strong&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;扣减&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;Redis 原子操作，保证不超卖&lt;/strong&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;落库&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;异步处理，通过 MQ 解耦 + 幂等保证最终一致&lt;/strong&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;h2&gt;07.spring的事务传递性&lt;/h2&gt;
&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;方法 A 调用方法 B，A 有事务，B 也有 &lt;code&gt;@Transactional&lt;/code&gt;，&lt;br /&gt;
那 B 是加入 A 的事务？还是开启新事务？还是不支持事务？&lt;/strong&gt;&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;答案由 &lt;strong&gt;&lt;code&gt;@Transactional(propagation = ...)&lt;/code&gt;&lt;/strong&gt; 中的 &lt;strong&gt;传播行为（Propagation）&lt;/strong&gt; 决定。&lt;/p&gt;
&lt;h3&gt;✅ Spring 提供的 7 种事务传播行为&lt;/h3&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;传播行为&lt;/th&gt;
&lt;th&gt;说明&lt;/th&gt;
&lt;th&gt;类比理解&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;&lt;code&gt;REQUIRED&lt;/code&gt;&lt;/strong&gt;（默认）&lt;/td&gt;
&lt;td&gt;如果当前存在事务，则加入；否则新建一个事务&lt;/td&gt;
&lt;td&gt;“有群就进，没群就建”&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;&lt;code&gt;SUPPORTS&lt;/code&gt;&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;如果当前存在事务，则加入；否则以非事务方式执行&lt;/td&gt;
&lt;td&gt;“有群就聊，没群就算了”&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;&lt;code&gt;MANDATORY&lt;/code&gt;&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;必须在已有事务中执行，否则抛异常&lt;/td&gt;
&lt;td&gt;“必须在群里说话，否则禁言！”&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;&lt;code&gt;REQUIRES_NEW&lt;/code&gt;&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;无论当前是否有事务，都&lt;strong&gt;挂起当前事务（如果有），创建新事务&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;“我要单独开个小会，原会议暂停”&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;&lt;code&gt;NOT_SUPPORTED&lt;/code&gt;&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;以非事务方式执行，&lt;strong&gt;如果当前有事务，则挂起它&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;“我现在不想开会，请暂停群聊”&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;&lt;code&gt;NEVER&lt;/code&gt;&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;以非事务方式执行，&lt;strong&gt;如果当前有事务，则抛异常&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;“禁止在群里说话！”&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;&lt;code&gt;NESTED&lt;/code&gt;&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;如果当前存在事务，则在&lt;strong&gt;嵌套事务内执行&lt;/strong&gt;；否则行为同 &lt;code&gt;REQUIRED&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;“在大会议里开个子议题，可单独回滚”&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;h2&gt;Java 方法参数传递机制&lt;/h2&gt;
&lt;p&gt;Java 中所有的参数传递都是 &lt;strong&gt;值传递（Pass-by-Value）&lt;/strong&gt;，无论是基本类型还是引用类型。&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;对于 &lt;strong&gt;基本类型&lt;/strong&gt;（如 &lt;code&gt;int&lt;/code&gt;、&lt;code&gt;boolean&lt;/code&gt;），传递的是实际值的副本。方法内部对参数的修改不会影响原始变量。&lt;/li&gt;
&lt;li&gt;对于 &lt;strong&gt;引用类型&lt;/strong&gt;（如对象、数组），传递的是 &lt;strong&gt;对象引用的副本&lt;/strong&gt;（即堆中对象地址的拷贝）。这个副本和原引用指向同一个对象（储存在堆中），因此通过它修改对象的属性，会影响原对象；但如果在方法内让引用指向一个新对象（比如 &lt;code&gt;obj = new Xxx()&lt;/code&gt;），则不会影响原来的引用。&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;本质上，Java 没有‘按引用传递’。之所以对象属性能被修改，是因为多个引用共享同一个堆对象，而不是因为传参机制不同。”&lt;/p&gt;
&lt;p&gt;在JVM中，&lt;strong&gt;Java 方法调用时，实参的“值”被复制到被调用方法的局部变量表中。&lt;/strong&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;基本类型的“值” = 数据本身（如 &lt;code&gt;int=5&lt;/code&gt;）&lt;/li&gt;
&lt;li&gt;引用类型的“值” = 对象在堆中的地址（即 &lt;strong&gt;Oop 指针&lt;/strong&gt;，例如 &lt;code&gt;0x000000076ab23450&lt;/code&gt;）&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;strong&gt;JVM 不会传递变量本身，也不会传递对象本身，只传递“值”的副本。&lt;/strong&gt;&lt;/p&gt;
</content:encoded></item><item><title>MySQL 大表拆分</title><link>https://yanghc.dev/posts/fa141799/</link><guid isPermaLink="true">https://yanghc.dev/posts/fa141799/</guid><pubDate>Fri, 05 Sep 2025 22:12:30 GMT</pubDate><content:encoded>&lt;p&gt;当一张表数据量达到 &lt;strong&gt;2亿条&lt;/strong&gt; 以上时，即使有主从库，也可能遇到以下问题：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;查询变慢，索引树过大，点查延迟明显。&lt;/li&gt;
&lt;li&gt;批量写入、归档效率低。&lt;/li&gt;
&lt;li&gt;表结构变更（DDL）风险高。&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;这时，就需要考虑 &lt;strong&gt;水平拆分（分库/分表）&lt;/strong&gt;。本文结合一个“点查为主”的业务场景，介绍如何安全、平滑地完成大表拆分。&lt;/p&gt;
&lt;h2&gt;1. 拆分思路&lt;/h2&gt;
&lt;h3&gt;1.1 为什么拆分&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;减少单表数据量，提升查询和维护效率。&lt;/li&gt;
&lt;li&gt;提高系统扩展性，支持未来数据继续增长。&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;1.2 拆分方式&lt;/h3&gt;
&lt;ol&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;水平分表&lt;/strong&gt;：在同一个库里创建多张表，把数据均匀切开。&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;优点：运维简单，迁移快。&lt;/li&gt;
&lt;li&gt;缺点：数据库单点压力仍然存在。&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;水平分库 + 分表&lt;/strong&gt;：把数据拆到多个库里，每个库再分多张表。&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;优点：扩展性最强，单库压力小。&lt;/li&gt;
&lt;li&gt;缺点：改造成本高，涉及分布式事务和路由。&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;如果只是单库，先做水平分表即可。&lt;/p&gt;
&lt;h2&gt;2. 拆分方案设计&lt;/h2&gt;
&lt;p&gt;假设有一张订单表 &lt;code&gt;order&lt;/code&gt;，2亿条数据，查询条件以 &lt;strong&gt;订单号点查&lt;/strong&gt; 为主：&lt;/p&gt;
&lt;h3&gt;2.1 分片键选择&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;订单ID（order_id）&lt;/strong&gt;：自增或雪花算法生成，能均匀分布。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;取模方式&lt;/strong&gt;：例如 &lt;code&gt;order_id % 16&lt;/code&gt; → 把数据拆到 16 张表。&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;2.2 新表设计&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;order_00
order_01
...
order_15
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;与原表结构完全一致。&lt;/p&gt;
&lt;h3&gt;2.3 路由规则&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;写入：按 &lt;code&gt;order_id % 16&lt;/code&gt; 选择目标表。&lt;/li&gt;
&lt;li&gt;查询：由代码或中间件（如 ShardingSphere、MyCat）根据 ID 定位表。&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;3. 数据迁移流程&lt;/h2&gt;
&lt;p&gt;为了保证 &lt;strong&gt;生产安全&lt;/strong&gt;，迁移必须采用 &lt;strong&gt;灰度切流 + 双写 + 校验&lt;/strong&gt; 的方式：&lt;/p&gt;
&lt;h3&gt;步骤 1：准备环境&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;创建分表 &lt;code&gt;order_00&lt;/code&gt; … &lt;code&gt;order_15&lt;/code&gt;。&lt;/li&gt;
&lt;li&gt;配置好代码里的路由逻辑（支持新老表双写）。&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;步骤 2：灰度切流&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;先让一部分新写入（比如 1% 用户）走新表，其他仍然写老表。&lt;/li&gt;
&lt;li&gt;验证新表能正常查询、写入。&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;步骤 3：历史数据迁移&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;使用迁移脚本（推荐 &lt;strong&gt;批量+限速&lt;/strong&gt; 的方式）：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;INSERT INTO order_03 (col1, col2, ...)
SELECT col1, col2, ...
FROM order
WHERE order_id % 16 = 3
LIMIT 10000;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;配合任务调度器分批跑，避免锁表。&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;步骤 4：双写校验&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;迁移期间，写操作同时写入老表和新表。&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;定期做 &lt;strong&gt;数据校验&lt;/strong&gt;：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;行数比对：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;SELECT COUNT(*) FROM order WHERE order_id % 16 = 3;
SELECT COUNT(*) FROM order_03;
&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;校验哈希：对比 MD5/SUM 聚合。&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;步骤 5：全量切换&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;确认迁移完成、校验通过后，关闭老表写入。&lt;/li&gt;
&lt;li&gt;所有请求统一走新表。&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;步骤 6：回收老表&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;观察一段时间后，老表只保留归档数据，或直接下线。&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;4. 风险与安全保障&lt;/h2&gt;
&lt;ol&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;不影响生产&lt;/strong&gt;：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;迁移分批次+限流，避免一次性锁全表。&lt;/li&gt;
&lt;li&gt;业务代码支持双写，保证数据一致。&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;回滚预案&lt;/strong&gt;：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;灰度期间若新表有问题，立即回切老表。&lt;/li&gt;
&lt;li&gt;保证老表在迁移前始终可用。&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;监控报警&lt;/strong&gt;：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;校验迁移速率、落库延迟。&lt;/li&gt;
&lt;li&gt;重点监控主从延迟和慢查询。&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ol&gt;
&lt;h2&gt;5. 总结&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;拆分本质&lt;/strong&gt;：减少单表压力，提升扩展性。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;点查业务&lt;/strong&gt;：适合用主键取模分表。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;迁移原则&lt;/strong&gt;：灰度切流 → 双写迁移 → 数据校验 → 全量切换。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;生产保障&lt;/strong&gt;：限速迁移、校验数据、保留回滚预案。&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;一句话记忆：
&lt;strong&gt;“小步快跑，双写保命，先灰度再全量”&lt;/strong&gt;。&lt;/p&gt;
</content:encoded></item><item><title>数据库事务：undo log 与 redo log 区别</title><link>https://yanghc.dev/posts/42c9cf2b/</link><guid isPermaLink="true">https://yanghc.dev/posts/42c9cf2b/</guid><pubDate>Thu, 04 Sep 2025 23:56:25 GMT</pubDate><content:encoded>&lt;h1&gt;缓冲池与数据页&lt;/h1&gt;
&lt;p&gt;&lt;strong&gt;缓冲池(buffer pool)&lt;/strong&gt;:主内存中的一个区域,里面可以缓存磁盘上经常操作的真实数据,在执行增删改查操作时,先操作 缓冲池中的数据(若缓冲池没有数据,则从磁盘加载并缓存),以一定频率刷新到磁盘,从而减少磁盘IO,加快处理速度&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;数据页(page)&lt;/strong&gt;:是lnnoDB存储引擎磁盘管理的最小单元,每个页的大小默认为16KB。页中存储的是行数据&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;assets/PixPin_2025-09-04_23-56-36.jpg&quot; alt=&quot;&quot; /&gt;&lt;/p&gt;
&lt;h1&gt;redo log（重做日志）&lt;/h1&gt;
&lt;p&gt;重做日志，记录的是事务提交时数据页的物理修改，是用来&lt;strong&gt;实现事务的持久性&lt;/strong&gt;。
该日志文件由两部分组成：重做日志缓冲（redo log buffer）以及重做日志文件（redo log file），前者是在内存中，后者在磁盘中。当事务提交之后会把所有修改信息都存到该日志文件中，用于在刷新脏页到磁盘，发生错误时，进行数据恢复使用。&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;&lt;strong&gt;前置概念铺垫&lt;/strong&gt;：先引入MySQL中的“缓冲池（Buffer Pool）”与“数据页”概念，说明MySQL会先将数据加载到缓冲池中的数据页进行操作，而非直接修改磁盘数据，为redo log的作用场景做铺垫。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;核心作用&lt;/strong&gt;：保证事务的&lt;strong&gt;持久性（ACID中的D）&lt;/strong&gt;，即事务提交后，即使发生数据库崩溃，重启后也能通过redo log恢复已提交的修改。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;记录内容&lt;/strong&gt;：记录事务提交时数据页的&lt;strong&gt;物理修改&lt;/strong&gt;（如“在某个数据页的某偏移量位置，将值从A修改为B”），不记录数据修改前的状态，只关注“要做什么修改”。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;存储结构&lt;/strong&gt;：由“内存中的redo log buffer（重做日志缓冲）”和“磁盘上的redo log file（重做日志文件）”两部分组成，避免频繁直接写磁盘导致性能损耗。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;写入机制&lt;/strong&gt;：采用“先写日志（Write-Ahead Logging，WAL）”机制——事务提交时，先将redo log写入磁盘的redo log file，再将缓冲池中的数据页异步刷到磁盘；同时采用“循环写”方式，当日志文件写满后，会覆盖旧的未被需要的日志（需配合 checkpoint 机制判断哪些日志可覆盖）。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;性能优势&lt;/strong&gt;：通过“先写缓冲、批量刷盘”和“循环写”，减少磁盘随机IO，大幅提升数据同步到磁盘的性能，保证数据库高并发场景下的效率。&lt;/li&gt;
&lt;/ol&gt;
&lt;h1&gt;undo log（回滚日志）&lt;/h1&gt;
&lt;p&gt;回滚日志，用于记录数据被修改前的信息，作用包含两个：提供回滚和 MVCC(多版本并发控制）。undo log和redo log记录物理日志不一样，它是逻辑日志。&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;可以认为当delete一条记录时，undolog中会记录一条对应的insert记录，反之亦然，&lt;/li&gt;
&lt;li&gt;当update一条记录时，它记录一条对应相反的update记录。当执行rollback时，就可以从undolog中的逻辑记录读取到相应的内容并进行回滚。&lt;/li&gt;
&lt;/ul&gt;
&lt;ol&gt;
&lt;li&gt;&lt;strong&gt;核心作用&lt;/strong&gt;：保证事务的&lt;strong&gt;原子性（ACID中的A）&lt;/strong&gt; 和&lt;strong&gt;一致性（ACID中的C）&lt;/strong&gt;：
&lt;ul&gt;
&lt;li&gt;原子性：若事务执行过程中出错或用户触发回滚（ROLLBACK），可通过undo log恢复数据到事务开始前的状态，确保事务“要么全做，要么全不做”。&lt;/li&gt;
&lt;li&gt;一致性：配合MVCC（多版本并发控制）机制，为读操作提供数据的历史版本，避免脏读、不可重复读等问题，保证事务执行过程中数据的一致性视图。&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;记录内容&lt;/strong&gt;：记录数据修改前的&lt;strong&gt;逻辑信息&lt;/strong&gt;（如“执行了INSERT操作，回滚时需执行DELETE；执行了UPDATE操作，回滚时需将值改回修改前的状态”），本质是“撤销操作的指令”，而非物理数据页修改。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;使用场景&lt;/strong&gt;：
&lt;ul&gt;
&lt;li&gt;事务回滚：当事务需要取消执行时，数据库根据undo log反向执行操作，恢复数据。&lt;/li&gt;
&lt;li&gt;MVCC快照读：查询时若数据已被其他事务修改，可通过undo log获取历史版本数据，实现“读不加锁、写不阻塞读”。&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ol&gt;
&lt;h3&gt;（三）undo log 与 redo log 核心区别对比&lt;/h3&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;对比维度&lt;/th&gt;
&lt;th&gt;undo log（回滚日志）&lt;/th&gt;
&lt;th&gt;redo log（重做日志）&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;核心作用&lt;/td&gt;
&lt;td&gt;保证事务原子性、一致性；支持MVCC&lt;/td&gt;
&lt;td&gt;保证事务持久性&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;记录内容&lt;/td&gt;
&lt;td&gt;数据修改前的&lt;strong&gt;逻辑信息&lt;/strong&gt;（撤销指令）&lt;/td&gt;
&lt;td&gt;数据页的&lt;strong&gt;物理修改&lt;/strong&gt;（执行的修改）&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;数据恢复方向&lt;/td&gt;
&lt;td&gt;反向恢复（回滚到事务开始前状态）&lt;/td&gt;
&lt;td&gt;正向恢复（崩溃后重现已提交的修改）&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;与事务的关联&lt;/td&gt;
&lt;td&gt;事务执行过程中实时生成，事务提交后可能被保留（供MVCC使用）&lt;/td&gt;
&lt;td&gt;事务提交时写入，后续可被循环覆盖&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;对性能的影响&lt;/td&gt;
&lt;td&gt;主要影响事务回滚和读性能（支撑MVCC）&lt;/td&gt;
&lt;td&gt;优化写性能（减少磁盘随机IO）&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
</content:encoded></item><item><title>docker开启TLS 双向认证远程访问</title><link>https://yanghc.dev/posts/21e1bce7/</link><guid isPermaLink="true">https://yanghc.dev/posts/21e1bce7/</guid><pubDate>Sat, 30 Aug 2025 00:00:00 GMT</pubDate><content:encoded>&lt;h2&gt;&lt;strong&gt;整体思路&lt;/strong&gt;&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;远程服务器上运行 Docker API，通过 &lt;code&gt;2376&lt;/code&gt; 端口对外暴露，并开启 TLS 双向认证。&lt;/li&gt;
&lt;li&gt;服务端证书绑定 &lt;strong&gt;远程服务器 IP 或域名&lt;/strong&gt;。&lt;/li&gt;
&lt;li&gt;客户端证书仅用于认证，不绑定 IP，所以客户端 IP 可变。&lt;/li&gt;
&lt;li&gt;推荐使用 &lt;strong&gt;域名&lt;/strong&gt;（固定或动态 DNS），防止 IP 变动导致证书不匹配。&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;1.在远程服务器生成证书&lt;/h2&gt;
&lt;p&gt;生成：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;CA 证书&lt;/strong&gt;：用于签发服务端和客户端证书&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;服务端证书&lt;/strong&gt;：给 Docker Daemon&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;客户端证书&lt;/strong&gt;：给你的本地电脑&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;&lt;strong&gt;1.1 创建证书目录&lt;/strong&gt;&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;mkdir -p /etc/docker/certs
cd /etc/docker/certs
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;&lt;strong&gt;1.2 生成 CA&lt;/strong&gt;&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;# 生成 CA 私钥（输入密码）
openssl genrsa -aes256 -out ca-key.pem 4096

# 生成 CA 根证书
openssl req -new -x509 -days 365 -key ca-key.pem -sha256 -out ca.pem
&lt;/code&gt;&lt;/pre&gt;
&lt;blockquote&gt;
&lt;p&gt;&lt;code&gt;Common Name&lt;/code&gt; 可以填 &lt;code&gt;My Docker CA&lt;/code&gt;。&lt;/p&gt;
&lt;/blockquote&gt;
&lt;h3&gt;&lt;strong&gt;1.3 生成服务端证书&lt;/strong&gt;&lt;/h3&gt;
&lt;p&gt;关键点：SAN 必须包含 &lt;strong&gt;服务器的公网 IP 或域名&lt;/strong&gt;（推荐域名）。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;# 服务端私钥
openssl genrsa -out server-key.pem 4096

# 服务端 CSR
openssl req -subj &quot;/CN=docker-server&quot; -new -key server-key.pem -out server.csr

# 扩展文件（SAN 配置）
echo subjectAltName = IP:203.0.XXX.10,DNS:docker.myserver.com &amp;gt; extfile.cnf

#如果只有ip，上方配置可以写成
echo subjectAltName = IP:1XX.XX.XX0.XX,IP:10.0.0.5 &amp;gt; extfile.cnf

# 签发服务端证书
openssl x509 -req -days 365 -sha256 \
  -in server.csr \
  -CA ca.pem -CAkey ca-key.pem -CAcreateserial \
  -out server-cert.pem \
  -extfile extfile.cnf
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;&lt;strong&gt;1.4 生成客户端证书&lt;/strong&gt;&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;# 客户端私钥
openssl genrsa -out key.pem 4096

# 客户端 CSR
openssl req -subj &apos;/CN=client&apos; -new -key key.pem -out client.csr

# 客户端扩展文件（声明 clientAuth）
echo extendedKeyUsage = clientAuth &amp;gt; extfile-client.cnf

# 签发客户端证书
openssl x509 -req -days 365 -sha256 \
  -in client.csr \
  -CA ca.pem -CAkey ca-key.pem -CAcreateserial \
  -out cert.pem \
  -extfile extfile-client.cnf
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;&lt;strong&gt;最终文件分布&lt;/strong&gt;&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;服务端：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;/etc/docker/certs/ca.pem&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;/etc/docker/certs/server-cert.pem&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;/etc/docker/certs/server-key.pem&lt;/code&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;客户端：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;ca.pem&lt;/code&gt;、&lt;code&gt;cert.pem&lt;/code&gt;、&lt;code&gt;key.pem&lt;/code&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;2.配置 Docker Daemon&lt;/h2&gt;
&lt;p&gt;编辑 &lt;strong&gt;&lt;code&gt;/usr/lib/systemd/system/docker.service&lt;/code&gt;&lt;/strong&gt;：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;ExecStart=/usr/bin/dockerd \
  -H unix:///var/run/docker.sock \
  -H tcp://0.0.0.0:5489 \
  --tlsverify \
  --tlscacert=/etc/docker/certs/ca.pem \
  --tlscert=/etc/docker/certs/server-cert.pem \
  --tlskey=/etc/docker/certs/server-key.pem \
  --containerd=/run/containerd/containerd.sock
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;重启 Docker：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;systemctl daemon-reload
systemctl restart docker
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;确认 Docker 在 &lt;code&gt;5489&lt;/code&gt; 端口监听：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;netstat -tulnp | grep 5489
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;3.客户端配置（本地电脑)&lt;/h2&gt;
&lt;p&gt;把这三个文件复制到本地，例如 &lt;code&gt;~/.docker/certs/&lt;/code&gt;：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;ca.pem&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;cert.pem&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;key.pem&lt;/code&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;设置环境变量：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;DOCKER_HOST=tcp://192.168.1.100:5489
DOCKER_TLS_VERIFY=1
DOCKER_CERT_PATH=D:\SoftDate\Develop\Docker\certs
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;测试连接：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;docker info
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;或者显式指定：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;docker --tlsverify \
  --tlscacert=~/.docker/certs/ca.pem \
  --tlscert=~/.docker/certs/cert.pem \
  --tlskey=~/.docker/certs/key.pem \
  -H=tcp://docker.myserver.com:5489 info
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;&lt;strong&gt;注意事项&lt;/strong&gt;&lt;/h2&gt;
&lt;p&gt;&lt;strong&gt;防火墙要放行 2376 端口&lt;/strong&gt;&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;```bash
ufw allow 5489/tcp
```
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;strong&gt;限制来源 IP&lt;/strong&gt;（安全）&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;```bash
ufw allow from &amp;lt;你的本地 IP&amp;gt;/32 to any port 2376 proto tcp
```
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;strong&gt;客户端 IP 可变没关系&lt;/strong&gt;，因为客户端证书不绑定 IP。&lt;/p&gt;
&lt;h2&gt;TLS 原理&lt;/h2&gt;
&lt;p&gt;Docker 远程 API 默认是 &lt;strong&gt;HTTP&lt;/strong&gt;，如果直接暴露 TCP 端口，没有认证和加密，任何能访问这个端口的人都可以 &lt;strong&gt;完全控制 Docker&lt;/strong&gt;，相当于 root 权限。&lt;/p&gt;
&lt;p&gt;TLS（Transport Layer Security）在 Docker 中的作用：&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;加密通信&lt;/strong&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;客户端和服务端之间的数据通过 TLS 加密，防止中间人监听。&lt;/li&gt;
&lt;li&gt;即使在公共网络，也不会泄露镜像名、命令或敏感信息。&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;身份验证（认证）&lt;/strong&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;服务端验证客户端证书&lt;/strong&gt;：通过 &lt;code&gt;--tlsverify&lt;/code&gt;，服务端只允许受信任的客户端连接。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;客户端验证服务端证书&lt;/strong&gt;：客户端验证服务器证书，防止连接到伪造的 Docker 服务。&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;完整性保护&lt;/strong&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;TLS 可以保证数据在传输过程中未被篡改。&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ol&gt;
&lt;h2&gt;Docker TLS 的实现方式&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;服务端&lt;/strong&gt;：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;dockerd --tlsverify --tlscacert=ca.pem --tlscert=server-cert.pem --tlskey=server-key.pem&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;开启 TLS，同时指定 CA 证书和服务端证书/私钥。&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;客户端&lt;/strong&gt;：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;使用 CA 签发的客户端证书：&lt;code&gt;cert.pem&lt;/code&gt; + &lt;code&gt;key.pem&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;客户端通过 TLS 连接到服务端时，服务端会验证客户端证书，拒绝未授权访问。&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;安全性分析&lt;/h2&gt;
&lt;h3&gt;优点&lt;/h3&gt;
&lt;ol&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;通信加密&lt;/strong&gt;：防止数据被截获&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;身份验证&lt;/strong&gt;：保证只有持有客户端证书的人可以访问 Docker API&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;防中间人攻击&lt;/strong&gt;：客户端验证服务端证书&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;操作安全&lt;/strong&gt;：不像直接暴露 HTTP 那样任何人都可以控制 Docker&lt;/p&gt;
&lt;/li&gt;
&lt;/ol&gt;
&lt;h3&gt;限制/风险&lt;/h3&gt;
&lt;ol&gt;
&lt;li&gt;&lt;strong&gt;证书泄露&lt;/strong&gt;
&lt;ul&gt;
&lt;li&gt;如果 CA 或客户端证书/私钥泄露，攻击者可以直接访问 Docker&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;端口暴露&lt;/strong&gt;
&lt;ul&gt;
&lt;li&gt;TCP 端口暴露在公网也可能被扫描，建议配合防火墙或 VPN 使用&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;配置错误&lt;/strong&gt;
&lt;ul&gt;
&lt;li&gt;TLS 证书路径错误或使用服务端证书当客户端证书，会导致连接失败或安全缺失&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;&lt;strong&gt;最佳实践&lt;/strong&gt;：
1. 使用独立 CA 签发客户端证书
2. 不在公网暴露 Docker TCP 端口，建议 VPN/内网访问
3. 定期更新证书、不要共享私钥&lt;/p&gt;
</content:encoded></item><item><title>docker自建镜像加速</title><link>https://yanghc.dev/posts/eb1fde38/</link><guid isPermaLink="true">https://yanghc.dev/posts/eb1fde38/</guid><pubDate>Sat, 12 Jul 2025 00:00:00 GMT</pubDate><content:encoded>&lt;p&gt;&lt;a href=&quot;https://github.com/dqzboy/Docker-Proxy?tab=readme-ov-file&quot;&gt;dqzboy/Docker-Proxy: 🔥 🔥 🔥 自建Docker镜像加速服务，基于官方Docker Registry 一键部署Docker、K8s、Quay、Ghcr、Mcr、Nvcr等镜像加速\管理服务。支持免服务器部署到Render\Koyeb&lt;/a&gt;&lt;/p&gt;
</content:encoded></item><item><title>HashMap底层讲解</title><link>https://yanghc.dev/posts/c260459f/</link><guid isPermaLink="true">https://yanghc.dev/posts/c260459f/</guid><pubDate>Sat, 28 Jun 2025 22:12:04 GMT</pubDate><content:encoded>&lt;p&gt;首先HashMap是初始容量为 16，负载因子为 0.75，则扩容阈值为 &lt;code&gt;16 × 0.75 = 12&lt;/code&gt;。当存入第 13 个元素时，&lt;code&gt;HashMap&lt;/code&gt; 就会触发扩容。
当触发扩容时，&lt;code&gt;HashMap&lt;/code&gt; 的容量会扩大为当前容量的&lt;strong&gt;两倍&lt;/strong&gt;。例如，容量从 16 增加到 32，从 32 增加到 64 等。数组长度一定是 2 的幂&lt;/p&gt;
&lt;h2&gt;问题：为什么数组长度一定是 2 的幂？&lt;/h2&gt;
&lt;p&gt;这是为了&lt;strong&gt;用位运算代替取模运算&lt;/strong&gt;，提升性能。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;hash % length == hash &amp;amp; (length - 1)   // 前提：length 是 2 的幂
&lt;/code&gt;&lt;/pre&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;%&lt;/code&gt; 是耗性能的除法操作&lt;/li&gt;
&lt;li&gt;&lt;code&gt;&amp;amp;&lt;/code&gt; 是非常快的位运算&lt;/li&gt;
&lt;li&gt;所以为了高性能，HashMap 规定数组长度只能是 2 的幂，这样 &lt;code&gt;&amp;amp;&lt;/code&gt; 操作就相当于 &lt;code&gt;%&lt;/code&gt;
因为只用了 &lt;code&gt;hash&lt;/code&gt; 的&lt;strong&gt;低位&lt;/strong&gt;，如果很多 key 的低位部分相同（比如都以 0 结尾、或者 hashCode 差异集中在高位），就会被分配到同一个桶中，发生严重冲突！
所以才引入了“&lt;strong&gt;扰动函数&lt;/strong&gt;”：&lt;/li&gt;
&lt;/ul&gt;
&lt;pre&gt;&lt;code&gt;hash = h ^ (h &amp;gt;&amp;gt;&amp;gt; 16)
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;h是key.hashCode()得到的，在经过上面的运算之后，把高位的信息混进了地位，减少了分布不均匀的情况&lt;/p&gt;
&lt;h4&gt;右移高位 + 异或：让高位参与进低位的计算&lt;/h4&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;h &amp;gt;&amp;gt;&amp;gt; 16&lt;/code&gt;：把哈希值的高 16 位移到低位。&lt;/li&gt;
&lt;li&gt;&lt;code&gt;h ^ (h &amp;gt;&amp;gt;&amp;gt; 16)&lt;/code&gt;：将原始值和高位部分做 &lt;strong&gt;按位异或&lt;/strong&gt;，等于&lt;strong&gt;把高位的信息折叠混进低位&lt;/strong&gt;。
这样一来，即便多个 key 的 &lt;code&gt;hashCode()&lt;/code&gt; 低位相同，但如果高位不同，最终 &lt;code&gt;hash()&lt;/code&gt; 的值也会不同，从而分散到不同的桶中。&lt;/li&gt;
&lt;/ul&gt;
&lt;h5&gt;✅ 举个例子：&lt;/h5&gt;
&lt;p&gt;假设两个 key 的 &lt;code&gt;hashCode()&lt;/code&gt; 是：&lt;/p&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;key&lt;/th&gt;
&lt;th&gt;hashCode&lt;/th&gt;
&lt;th&gt;高16位&lt;/th&gt;
&lt;th&gt;低16位&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;A&lt;/td&gt;
&lt;td&gt;0x12340001&lt;/td&gt;
&lt;td&gt;0x1234&lt;/td&gt;
&lt;td&gt;0x0001&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;B&lt;/td&gt;
&lt;td&gt;0x56780001&lt;/td&gt;
&lt;td&gt;0x5678&lt;/td&gt;
&lt;td&gt;0x0001&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;p&gt;原始 &lt;code&gt;hashCode&lt;/code&gt; 的低 16 位完全一样：0x0001，最终用来计算桶位置时结果会一样（冲突！）。
现在看扰动函数：&lt;/p&gt;
&lt;p&gt;&lt;code&gt;h ^ (h &amp;gt;&amp;gt;&amp;gt; 16)&lt;/code&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;A: &lt;code&gt;0x12340001 ^ 0x1234 = 0x12341235&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;B: &lt;code&gt;0x56780001 ^ 0x5678 = 0x56785679&lt;/code&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;扰动后，它们的低位已经不一样了 → &lt;strong&gt;可以分配到不同的桶，冲突减少了！&lt;/strong&gt;&lt;/p&gt;
&lt;h2&gt;问题：HashMap 是&lt;strong&gt;怎么根据哈希值的低位来分配桶的&lt;/strong&gt;？&lt;/h2&gt;
&lt;h3&gt;✅ 第一步：桶的结构&lt;/h3&gt;
&lt;p&gt;HashMap 的底层结构是一个&lt;strong&gt;数组 + 链表（或红黑树）&lt;/strong&gt;：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;Node&amp;lt;K,V&amp;gt;[] table; // 本质是一个数组，每个元素就是一个“桶”
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;数组长度是 2 的幂，比如 16、32、64……（默认初始容量为 16）。&lt;/p&gt;
&lt;h3&gt;✅ 第二步：桶的下标是怎么计算的？&lt;/h3&gt;
&lt;p&gt;桶的下标，也就是哈希值对应的桶的位置，是通过下面这个公式计算的：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;int index = (table.length - 1) &amp;amp; hash;
&lt;/code&gt;&lt;/pre&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;hash&lt;/code&gt;：是经过扰动函数处理后的哈希值。&lt;/li&gt;
&lt;li&gt;&lt;code&gt;table.length - 1&lt;/code&gt;：是一个掩码（mask），比如：
&lt;ul&gt;
&lt;li&gt;如果数组长度是 16，则掩码为 &lt;code&gt;0b0000_1111&lt;/code&gt;（即 &lt;code&gt;15&lt;/code&gt;）。&lt;/li&gt;
&lt;li&gt;如果数组长度是 32，则掩码为 &lt;code&gt;0b0001_1111&lt;/code&gt;（即 &lt;code&gt;31&lt;/code&gt;）。&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;这个 &lt;code&gt;&amp;amp;&lt;/code&gt; 操作就相当于：&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;只保留 hash 的低几位，用作数组下标&lt;/strong&gt;&lt;/p&gt;
&lt;/blockquote&gt;
&lt;h3&gt;🧠 举个例子：&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;hash = 0b1010_0110_1010_1111_1101_0100_1100_1010   // 假设扰动后的哈希值
table.length = 16  // =&amp;gt; table.length - 1 = 15 = 0b0000_0000_0000_0000_0000_0000_0000_1111

index = hash &amp;amp; 0b1111 = 0b1010 = 10
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;所以这个 key 最终被映射到数组的第 10 个桶中。&lt;/p&gt;
&lt;h3&gt;✅ 为什么只保留低位就能定位桶？&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;因为桶的数量（table.length）是 2 的幂，比如 2⁴ = 16。&lt;/li&gt;
&lt;li&gt;所以桶的下标只需要 4 位（二进制）来表示。&lt;/li&gt;
&lt;li&gt;使用 &lt;code&gt;(table.length - 1) &amp;amp; hash&lt;/code&gt; 就能直接取出&lt;strong&gt;哈希值的低 4 位&lt;/strong&gt;作为下标。&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;这是一个比 &lt;code&gt;%&lt;/code&gt; 更快的方式。&lt;/p&gt;
&lt;h3&gt;📌 总结图示：&lt;/h3&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;操作阶段&lt;/th&gt;
&lt;th&gt;示例&lt;/th&gt;
&lt;th&gt;说明&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;key.hashCode()&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;123456&lt;/td&gt;
&lt;td&gt;原始哈希值&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;h ^ (h &amp;gt;&amp;gt;&amp;gt; 16)&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;扰动后的 hash 值&lt;/td&gt;
&lt;td&gt;高低位混合，增强分布&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;(table.length - 1) &amp;amp; hash&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;取低几位&lt;/td&gt;
&lt;td&gt;映射到桶下标&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;table[index]&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;找到桶位置&lt;/td&gt;
&lt;td&gt;将节点插入对应桶中&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;h4&gt;💡 常见误区&lt;/h4&gt;
&lt;p&gt;很多人以为“HashMap 根据 hash 值来分配桶”，&lt;strong&gt;其实更准确地说：它是根据 hash 值的低位来定位桶的&lt;/strong&gt;。&lt;br /&gt;
这就是为什么哈希值低位分布不均时，冲突会非常严重 —— 所以才要用扰动函数混合高位。&lt;/p&gt;
&lt;h2&gt;元素搬迁&lt;/h2&gt;
&lt;h3&gt;rehashing 细节&lt;/h3&gt;
&lt;p&gt;按照我们的思维，每一个元素应该是重新 hash 一个一个搬迁过去。&lt;/p&gt;
&lt;p&gt;在 1.7 的时候就是这样实现的，然而 1.8 在这里做了优化，关键点就在于数组的长度是 2 的次方，且扩容为 2 倍。&lt;/p&gt;
&lt;p&gt;因为数组的长度是 2 的 n 次方，所以假设以前的数组长度（16）二进制表示是 010000，那么新数组的长度（32）二进制表示是 100000，这个应该很好理解吧？&lt;/p&gt;
&lt;p&gt;它们之间的差别就在于高位多了一个 1，而我们通过 key 的 hash 值定位其在数组位置所采用的方法是 &lt;code&gt;(数组长度-1) &amp;amp; hash&lt;/code&gt;。我们还是拿 16 和 32 长度来举例：&lt;/p&gt;
&lt;p&gt;&lt;code&gt;16-1=15，二进制为 001111&lt;/code&gt;&lt;/p&gt;
&lt;p&gt;&lt;code&gt;32-1=31，二进制为 011111&lt;/code&gt;&lt;/p&gt;
&lt;p&gt;所以重点就在 key 的 hash 值的从右往左数第五位是否是 1，如果是 1 说明需要搬迁到新位置，且新位置的下标就是原下标+16（原数组大小），如果是 0 说明吃不到新数组长度的高位，那就还是在原位置，不需要迁移。&lt;/p&gt;
&lt;p&gt;所以，我们刚好拿老数组的长度（010000）来判断高位是否是 1，这里只有两种情况，要么是 1 要么是 0 。&lt;/p&gt;
&lt;p&gt;扩容后，HashMap 多了一位用于分桶，所以只要那一位是 &lt;code&gt;1&lt;/code&gt;，就说明这个 key 应该去“新增加的桶”。&lt;/p&gt;
&lt;h2&gt;为什么 HashMap 中桶位置计算要用 &lt;code&gt;(length - 1) &amp;amp; hash&lt;/code&gt;，而不是直接用 &lt;code&gt;length &amp;amp; hash&lt;/code&gt;？&lt;/h2&gt;
&lt;h3&gt;✅ 一句话解释：&lt;/h3&gt;
&lt;blockquote&gt;
&lt;p&gt;因为只有当 &lt;strong&gt;数组长度是 2 的幂&lt;/strong&gt; 时，&lt;code&gt;length - 1&lt;/code&gt; 才能构造出一个全是 &lt;code&gt;1&lt;/code&gt; 的掩码，从而提取出 &lt;strong&gt;哈希值的低位&lt;/strong&gt;。&lt;/p&gt;
&lt;/blockquote&gt;
&lt;hr /&gt;
&lt;h3&gt;🔍 举个例子直观理解：&lt;/h3&gt;
&lt;h3&gt;假设数组长度是 16：&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;length       =  16    = 0b0001_0000
length - 1   =  15    = 0b0000_1111
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;如果写：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;index = hash &amp;amp; length
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;就等于：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;index = hash &amp;amp; 0b0001_0000 // 只有第 5 位能参与计算
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;➡️ 只看了一位！几乎所有 hash 都会落到几个固定的位置，极其不均匀，导致严重哈希冲突！&lt;/p&gt;
&lt;p&gt;而如果你写：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;index = hash &amp;amp; (length - 1)
       = hash &amp;amp; 0b0000_1111  // 看的是低 4 位
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;➡️ 这样 hash 的低位能平均分布在 &lt;code&gt;0 ~ 15&lt;/code&gt; 共 16 个桶中，非常均匀！&lt;/p&gt;
&lt;h2&gt;📌 为什么这样做只有在 2 的幂下才成立？&lt;/h2&gt;
&lt;p&gt;我们来看几个例子：&lt;/p&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;数组长度&lt;/th&gt;
&lt;th&gt;二进制&lt;/th&gt;
&lt;th&gt;length - 1 的掩码&lt;/th&gt;
&lt;th&gt;作用&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;2&lt;/td&gt;
&lt;td&gt;0b10&lt;/td&gt;
&lt;td&gt;0b01&lt;/td&gt;
&lt;td&gt;保留 1 位&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;4&lt;/td&gt;
&lt;td&gt;0b100&lt;/td&gt;
&lt;td&gt;0b011&lt;/td&gt;
&lt;td&gt;保留 2 位&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;8&lt;/td&gt;
&lt;td&gt;0b1000&lt;/td&gt;
&lt;td&gt;0b0111&lt;/td&gt;
&lt;td&gt;保留 3 位&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;16&lt;/td&gt;
&lt;td&gt;0b10000&lt;/td&gt;
&lt;td&gt;0b01111&lt;/td&gt;
&lt;td&gt;保留 4 位&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;p&gt;可以看出：&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;&lt;code&gt;length - 1&lt;/code&gt; 恰好是一个&lt;strong&gt;连续的低位全 1 掩码&lt;/strong&gt;，可以保留 hash 的低位，实现高效分桶！&lt;/p&gt;
&lt;/blockquote&gt;
&lt;h3&gt;🔁 反例：如果不是 2 的幂会怎样？&lt;/h3&gt;
&lt;p&gt;比如数组长度是 10（不是 2 的幂）：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;&lt;code&gt;length - 1 = 9 = 0b1001&lt;/code&gt;，掩码不连续！&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;code&gt;hash &amp;amp; 0b1001&lt;/code&gt; 结果会跳跃分布，桶分布非常不均匀！&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;所以 HashMap &lt;strong&gt;强制数组长度只能是 2 的幂&lt;/strong&gt;，就是为了让 &lt;code&gt;(length - 1)&lt;/code&gt; 变成理想的掩码。&lt;/p&gt;
</content:encoded></item><item><title>docker部署mysql并修改其占用内存大小</title><link>https://yanghc.dev/posts/80661e20/</link><guid isPermaLink="true">https://yanghc.dev/posts/80661e20/</guid><pubDate>Wed, 05 Mar 2025 00:00:00 GMT</pubDate><content:encoded>&lt;h4&gt;一.安装mysql&lt;/h4&gt;
&lt;p&gt;1.下载好镜像&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;docker pull mysql:8.0.18
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;2.创建&lt;code&gt;MySQL&lt;/code&gt;容器&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;docker run -id --name mysql -p 3306:3306 -e MYSQL_ROOT_PASSWORD=123456 mysql:8.0.18
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;3.查看安装情况&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;[root@ ~]# docker ps -lCONTAINER ID   IMAGE          COMMAND                  CREATED          STATUS         PORTS                               NAMESa28d702be74a   mysql:8.0.18   &quot;docker-entrypoint.s…&quot;   20 minutes ago   Up 6 minutes   0.0.0.0:3306-&amp;gt;3306/tcp, 33060/tcp   mysql
&lt;/code&gt;&lt;/pre&gt;
&lt;h4&gt;二.修改mysql占用内存大小&lt;/h4&gt;
&lt;p&gt;因为自己部署服务内存比较小，而mysql在docker初始化就占500M，所以可优化其占用内存大小&lt;/p&gt;
&lt;p&gt;1.查看运行内存&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;[root@ ~]# docker statsCONTAINER ID   NAME        CPU %     MEM USAGE / LIMIT     MEM %     NET I/O           BLOCK I/O
PIDSa28d702be74a   mysql       0.34%     400.49MiB / 1.694GiB  24.33%     0B / 0B           19.3MB / 14.1MB   38c8adbf02c7a5   kafka       0.30%     435.7MiB / 1.694GiB   25.11%    1.77MB / 2.93MB   130MB / 86kB      6592187cc1f68e   zookeeper   0.07%     89.78MiB / 1.694GiB   5.18%     2.93MB / 1.77MB   99.5MB / 127kB    19
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;2.&lt;a href=&quot;https://so.csdn.net/so/search?q=%E8%BF%9B%E5%85%A5mysql&amp;amp;spm=1001.2101.3001.7020&quot;&gt;进入mysql&lt;/a&gt;容器终端&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;docker exec -it mysql bash
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;3.切换进入/etc/mysql/conf.d 目录&lt;/p&gt;
&lt;pre&gt;&lt;code&gt; cd /etc/mysql/conf.d
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;4.我们需要进入容器当中进行修改容器里面的配置文件，可能有的服务器是没有安装vim的，所以我们没有的需要安装的&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;apt-get updateapt-get install vim
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;5.docker进入mysql容器内，进入/etc/mysql/conf.d 目录执行 vim docker.cnf&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;vim docker.cnf
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;6.在对应文件后面添加下面的参数&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;performance_schema_max_table_instances=400  table_definition_cache=400    performance_schema=off    table_open_cache=64    innodb_buffer_pool_chunk_size=64M    innodb_buffer_pool_size=64M    
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;各参数对应的意义为&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;[mysqld]
performance_schema_max_table_instances=400  
table_definition_cache=400    #缓存
performance_schema=off    #用于监控MySQL server在一个较低级别的运行过程中的资源消耗、资源东西table_open_cache=64    #打开表的缓存
innodb_buffer_pool_chunk_size=64M    #InnoDB缓冲池大小调整操作的块大小
innodb_buffer_pool_size=64M    #InnoDB 存储引擎的表数据和索引数据的最大内存缓冲区大小
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;7.退出&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;exit
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;8.&lt;a href=&quot;https://so.csdn.net/so/search?q=%E9%87%8D%E5%90%AFmysql&amp;amp;spm=1001.2101.3001.7020&quot;&gt;重启mysql&lt;/a&gt;容器&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;docker stop mysql
docker start mysql
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;9.观察修改后的内存情况&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;docker stats
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;img src=&quot;https://i-blog.csdnimg.cn/blog_migrate/aba1541644829f372287ed8ae8bca59c.png&quot; alt=&quot;&quot; /&gt;&lt;/p&gt;
&lt;p&gt;本文转自 &lt;a href=&quot;https://blog.csdn.net/qq_39449880/article/details/125887603&quot;&gt;https://blog.csdn.net/qq_39449880/article/details/125887603&lt;/a&gt;，如有侵权，请联系删除。&lt;/p&gt;
</content:encoded></item><item><title>Bitwarden使用备份</title><link>https://yanghc.dev/posts/29d75657/</link><guid isPermaLink="true">https://yanghc.dev/posts/29d75657/</guid><pubDate>Sun, 02 Mar 2025 00:53:21 GMT</pubDate><content:encoded>&lt;pre&gt;&lt;code&gt;url: https://github.com/dani-garcia/vaultwarden/discussions/4664
title: &quot;Missing Icons · dani-garcia/vaultwarden · Discussion #4664&quot;
description: &quot;Deployment environment Your environment (Generated via diagnostics page) Vaultwarden version: v1.30.5 Web-vault version: v2024.1.2b OS/Arch: linux/x86_64 Running within a container: true (Base: Deb...&quot;
host: github.com
favicon: https://github.githubassets.com/favicons/favicon.svg
image: https://opengraph.githubassets.com/621640ceaff12b91c056267da259ab5dafcfd0a8930c1b0bccf78a827cac35a6/dani-garcia/vaultwarden/discussions/4664
&lt;/code&gt;&lt;/pre&gt;
&lt;pre&gt;&lt;code&gt;url: https://host.ppgg.in/deploying-and-using-of-vaultwarden/configuration#about-configuration
title: &quot;配置 | Bitwarden 部署和使用&quot;
host: host.ppgg.in
favicon: https://1098296495-files.gitbook.io/~/files/v0/b/gitbook-legacy-files/o/spaces%2F-MGNKm8ywtCheKTpUjpY%2Favatar-1603889553012.png?generation=1603889553284125&amp;amp;alt=media
image: https://host.ppgg.in/~gitbook/ogimage/-MJKdUsLbd4wL7RVzAOa
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;a href=&quot;https://tglbbs.com/thread-199987-1-1.html&quot;&gt;VaultWarden 的网站图标功能很不好用 - VPS专版 - TGL论坛 - 中文顶尖的英文站长社区&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;&quot;icon_service&quot;: &quot;https://api.faviconkit.com/{}/32&quot;,&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;url: https://help.ppgg.in/password-manager/developer-tools/ssh-agent
title: &quot;SSH 代理 | Bitwarden 帮助中心中文版&quot;
host: help.ppgg.in
favicon: https://463484399-files.gitbook.io/~/files/v0/b/gitbook-legacy-files/o/spaces%2F-M2XqgFI6fAcTD0lL3MZ%2Favatar-1584637381575.png?generation=1584637382149328&amp;amp;alt=media
image: https://help.ppgg.in/~gitbook/ogimage/5LxzQXM3wbKDRshTsqkF
&lt;/code&gt;&lt;/pre&gt;
&lt;pre&gt;&lt;code&gt;url: https://rs.ppgg.in/
title: &quot;关于 | Vaultwarden Wiki 中文版&quot;
host: rs.ppgg.in
favicon: https://551209699-files.gitbook.io/~/files/v0/b/gitbook-legacy-files/o/spaces%2F-M2Yi-lp0fd1uaijcnF_%2Favatar-1584637417236.png?generation=1584637417496173&amp;amp;alt=media
image: https://rs.ppgg.in/~gitbook/ogimage/-M2Yi65qaGLbd7hLATDg
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;vaultwarden更新&lt;/h2&gt;
&lt;pre&gt;&lt;code&gt;cd /root/myapp/Vaultwarden

# 拉取最新版本的镜像
docker pull vaultwarden/server:latest

# 停止并移除旧版本容器
docker stop vaultwarden
docker rm vaultwarden

# 使用已挂载的数据启动容器
docker run -d --name vaultwarden -v ./vw_data/:/data/ -p 7659:80 vaultwarden/server:latest
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;其它&lt;/h2&gt;
&lt;p&gt;环境变量修改后&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;docker compose down &amp;amp;&amp;amp; docker compose up -d
&lt;/code&gt;&lt;/pre&gt;
&lt;pre&gt;&lt;code&gt;&quot;push_enabled&quot;: true,
  &quot;push_identity_uri&quot;: &quot;https://identity.bitwarden.com&quot;,
  &quot;push_installation_id&quot;: &quot;xxxxx&quot;,
  &quot;push_installation_key&quot;: &quot;xxxxxx&quot;,
  &quot;push_relay_uri&quot;: &quot;https://push.bitwarden.com&quot;, 
  &quot;experimental_client_feature_flags&quot;: &quot;fido2-vault-credentials,ssh-key-vault-item,ssh-agent&quot;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;我的环境变量&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;## 时区
TZ=Asia/Shanghai

## 管理界面的令牌，最好是 Argon2 PHC 字符串。
## Vaultwarden 提供了一个内置生成器，可以通过调用 `vaultwarden hash` 来生成。
## 详情请参阅：https://github.com/dani-garcia/vaultwarden/wiki/Enabling-admin-page#secure-the-admin_token
## 如果未设置，管理面板将被禁用。
## 新的 Argon2 PHC 字符串
## 注意，在某些环境中，如 docker-compose 中，您需要将所有美元符号 `$` 转义为两个美元符号 `$$`。
## 并且在必要时使用单引号 (&apos;) 而不是双引号 (&quot;) 包含字符串。
ADMIN_TOKEN=&apos;XXXX&apos;

## 日志
LOG_FILE=/data/vaultwarden.log

## 日志级别
## 更改日志输出的详细程度。
## 有效值为 &quot;trace&quot;, &quot;debug&quot;, &quot;info&quot;, &quot;warn&quot;, &quot;error&quot; 和 &quot;off&quot;。
## 将其设置为 &quot;trace&quot; 或 &quot;debug&quot; 也会显示挂载路由和静态文件、WebSocket 和 alive 请求的日志。
## 对于特定模块，可以附加一个逗号分隔的 `path::to::module=log_level`
## 例如，要只为图标查看 info 日志，使用：LOG_LEVEL=&quot;info,vaultwarden::api::icons=debug&quot;
LOG_LEVEL=warn

## 控制当 SMTP 服务未配置且允许密码提示时，是否应在网页上直接显示密码提示。
## 不建议在公共可访问实例上使用，因为这会提供对潜在敏感数据的未经授权访问。
SHOW_PASSWORD_HINT=false

## 启用推送通知（需要从 https://bitwarden.com/host 获取密钥和 ID）
## 关于移动客户端推送通知的详情：
## - https://github.com/dani-garcia/vaultwarden/wiki/Enabling-Mobile-Client-push-notification
PUSH_ENABLED=true
PUSH_INSTALLATION_ID=XXXXXX
PUSH_INSTALLATION_KEY=XXXXXXXX
# 警告：除非完全理解其含义，否则不要修改以下设置！
# 默认推送中继和身份验证 URI
PUSH_RELAY_URI=https://push.bitwarden.com
PUSH_IDENTITY_URI=https://identity.bitwarden.com

## 客户端设置
## 启用客户端的实验性功能标志。
## 这是一个逗号分隔的功能标志列表，例如 &quot;flag1,flag2,flag3&quot;。
##
## 可用的标志如下：
## - &quot;autofill-overlay&quot;: 在表单字段中添加一个覆盖菜单，以便快速访问凭据。
## - &quot;autofill-v2&quot;: 使用新的自动填充实现。
## - &quot;browser-fileless-import&quot;: 直接从其他提供商导入凭据而无需文件。
## - &quot;extension-refresh&quot;: 临时启用新扩展设计直到正式发布（应与 beta Chrome 扩展一起使用）。
## - &quot;fido2-vault-credentials&quot;: 启用 FIDO2 安全密钥作为第二因素。
## - &quot;inline-menu-positioning-improvements&quot;: 启用浏览器扩展中的内联菜单密码生成器和身份建议。
## - &quot;ssh-key-vault-item&quot;: 启用 SSH 密钥保险库项的创建和使用。（需要客户端 &amp;gt;=2024.12.0）
## - &quot;ssh-agent&quot;: 在桌面上启用 SSH 代理支持。（需要桌面 &amp;gt;=2024.12.0）
EXPERIMENTAL_CLIENT_FEATURE_FLAGS=ssh-key-vault-item,ssh-agent,fido2-vault-credentials,browser-fileless-import,inline-menu-positioning-improvements,autofill-overlay
&lt;/code&gt;&lt;/pre&gt;
</content:encoded></item><item><title>Vaultwarden Fail2Ban配置</title><link>https://yanghc.dev/posts/1777f4d4/</link><guid isPermaLink="true">https://yanghc.dev/posts/1777f4d4/</guid><pubDate>Sun, 02 Mar 2025 00:52:05 GMT</pubDate><content:encoded>&lt;p&gt;&lt;a href=&quot;https://rs.ppgg.in/configuration/security/hardening-guide&quot;&gt;1.强化指南 |  Wiki 中文版&lt;/a&gt;&lt;/p&gt;
&lt;h1&gt;官方原版&lt;/h1&gt;
&lt;p&gt;对应的&lt;a href=&quot;https://github.com/dani-garcia/vaultwarden/wiki/Fail2Ban-Setup&quot;&gt;官方页面地址&lt;/a&gt;
&lt;a href=&quot;https://rs.ppgg.in/configuration/security/fail2ban-setup#setup-for-web-vault&quot;&gt;2.Fail2ban 设置 | Vaultwarden Wiki 中文版&lt;/a&gt;
设置 Fail2ban 可以阻止攻击者暴力破解您的密码库登录。如果您的实例是公开的，这一点尤其重要。&lt;/p&gt;
&lt;h2&gt;&lt;a href=&quot;#pre-requisite&quot;&gt;&lt;/a&gt;预先说明&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;文件名位于每个代码块的顶部。&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;从 1.5.0 版开始，Vaultwarden 支持记录到文件。请设置&lt;a href=&quot;/configuration/logging&quot;&gt;日志记录&lt;/a&gt;。&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;尝试使用错误的账户信息登录到网页版密码库，并检查日志文件中如下格式的记录项：&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;复制&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;[YYYY-MM-DD hh:mm:ss][vaultwarden::api::identity][ERROR] Username or password is incorrect. Try again. IP: XXX.XXX.XXX.XXX. Username: email@domain.com.
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;&lt;a href=&quot;#installation&quot;&gt;&lt;/a&gt;安装&lt;/h2&gt;
&lt;h3&gt;&lt;a href=&quot;#debian-ubuntu-raspian-pi-os&quot;&gt;&lt;/a&gt;Debian / Ubuntu / Raspian Pi OS&lt;/h3&gt;
&lt;p&gt;复制&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;sudo apt-get install fail2ban -y
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;&lt;a href=&quot;#fedora-centos&quot;&gt;&lt;/a&gt;Fedora / Centos&lt;/h3&gt;
&lt;p&gt;需要 EPEL 库 (CentOS 7)&lt;/p&gt;
&lt;p&gt;复制&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;sudo yum install epel-release
sudo yum install fail2ban -y
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;&lt;a href=&quot;#synology-dsm&quot;&gt;&lt;/a&gt;群晖 DSM&lt;/h3&gt;
&lt;p&gt;使用 Synology 的话，由于各种原因需要做更多的工作。使用 Docker Compose 的完整的解决方案发布在&lt;a href=&quot;https://github.com/sosandroid/docker-fail2ban-synology&quot;&gt;这里&lt;/a&gt;。主要的问题是：&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;
&lt;p&gt;嵌入式 IP 禁令系统不适用于 Docker 容器&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;嵌入式 iptables 不支持 &lt;code&gt;REJECT&lt;/code&gt; 块类型&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;Docker GUI 不允许某些高级设置&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;修改系统配置不符合升级要求&lt;/p&gt;
&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;因此，我们将在 Docker 容器中使用 Fail2ban。&lt;a href=&quot;https://github.com/crazy-max/docker-fail2ban&quot;&gt;Crazy-max/docker-fail2ban&lt;/a&gt; 提供了一个很好的解决方案，并且 Synology 的 Docker GUI 将被忽略。通过 SSH 的命令行，执行下列步骤（根据您的 Synology 配置调整 &lt;code&gt;volumeX&lt;/code&gt;）：&lt;/p&gt;
&lt;p&gt;1、获取 root 权限&lt;/p&gt;
&lt;p&gt;复制&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;sudo -i
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;2、创建持久性文件夹&lt;/p&gt;
&lt;p&gt;复制&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;mkdir -p /volumeX/docker/fail2ban/action.d/
mkdir -p /volumeX/docker/fail2ban/jail.d/
mkdir -p /volumeX/docker/fail2ban/filter.d/
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;3、将 blocktype 的 &lt;code&gt;REJECT&lt;/code&gt; 替换为 &lt;code&gt;DROP&lt;/code&gt; 块类型&lt;/p&gt;
&lt;p&gt;复制&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;# /volumeX/docker/fail2ban/action.d/iptables.local

[Init]
blocktype = DROP
[Init?family=inet6]
blocktype = DROP
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;4、创建 docker-compose 文件&lt;/p&gt;
&lt;p&gt;复制&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;# /volumeX/docker/fail2ban/docker-compose.yml

version: &apos;3&apos;
services:
	fail2ban:
		container_name: fail2ban
		restart: always
		image: crazymax/fail2ban:latest
		environment: 
		- TZ=Europe/Paris
		- F2B_DB_PURGE_AGE=30d
		- F2B_LOG_TARGET=/data/fail2ban.log
		- F2B_LOG_LEVEL=INFO
		- F2B_IPTABLES_CHAIN=INPUT

		volumes:
		- /volumeX/docker/fail2ban:/data
		- /volumeX/docker/vw-data:/vaultwarden:ro

		network_mode: &quot;host&quot;

		privileged: true
		cap_add:
			- NET_ADMIN
			- NET_RAW
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;5、使用命令行启动容器&lt;/p&gt;
&lt;p&gt;复制&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;cd /volumeX/docker/fail2ban
docker-compose up -d
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;您现在应该看到该容器在 Synolog 的 Docker GUI 中运行了。在配置筛选器和 jail 后，您必须重新加载。&lt;/p&gt;
&lt;h2&gt;&lt;a href=&quot;#setup-for-web-vault&quot;&gt;&lt;/a&gt;为网页密码库设置&lt;/h2&gt;
&lt;p&gt;按照惯例，&lt;code&gt;path_f2b&lt;/code&gt; 代表 Fail2ban 工作所需的路径。这取决于您的系统，例如在 Synology 上是 &lt;code&gt;/volumeX/docker/fail2ban/&lt;/code&gt;，但在其他系统上是 &lt;code&gt;/etc/fail2ban/&lt;/code&gt;。&lt;/p&gt;
&lt;h3&gt;&lt;a href=&quot;#filter&quot;&gt;&lt;/a&gt;Filter&lt;/h3&gt;
&lt;p&gt;使用如下内容创建文件：&lt;/p&gt;
&lt;p&gt;复制&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;# path_f2b/filter.d/vaultwarden.local

[INCLUDES]
before = common.conf

[Definition]
failregex = ^.*?Username or password is incorrect\. Try again\. IP: &amp;lt;ADDR&amp;gt;\. Username:.*$
ignoreregex =
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;strong&gt;提示&lt;/strong&gt;：如果在 &lt;code&gt;fail2ban.log&lt;/code&gt; 中出现以下错误消息 (CentOS 7, Fail2Ban v0.9.7) &lt;code&gt;fail2ban.filter [5291]: ERROR No &apos;host&apos; group in &apos;^.*Username or password is incorrect\. Try again\. IP: &amp;lt;ADDR&amp;gt;\. Username:.*$&apos;&lt;/code&gt; 请将 &lt;code&gt;vaultwarden.local&lt;/code&gt; 中的 &lt;code&gt;&amp;lt;ADDR&amp;gt;&lt;/code&gt; 改为 &lt;code&gt;&amp;lt;HOST&amp;gt;&lt;/code&gt;。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;提示&lt;/strong&gt;：对于 Cloudflare 用户，请确保在&lt;strong&gt;管理面板&lt;/strong&gt; -&amp;gt; &lt;strong&gt;高级设置&lt;/strong&gt; -&amp;gt; &lt;strong&gt;客户端 IP 标头&lt;/strong&gt;中将客户端 IP 标头设置为 &lt;code&gt;CF-Connecting-IP&lt;/code&gt;，否则客户端的真实 IP 将不会被识别和阻止。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;提示&lt;/strong&gt;：如果您在 &lt;code&gt;vaultwarden.log&lt;/code&gt; 中看到 127.0.0.1 是登录失败的 IP 地址，那么您可能正在使用反向代理，而 Fail2ban 无法正常工作：&lt;/p&gt;
&lt;p&gt;复制&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;[YYYY-MM-DD hh:mm:ss][vaultwarden::api::identity][ERROR] Username or password is incorrect. Try again. IP: 127.0.0.1. Username: email@example.com.
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;要解决这个问题，需要通过 X-Real-IP 头将真实的远程地址转发给 Vaultwarden。如何操作呢？根据你使用的代理服务器不同而不同。例如，在 Caddy 2.x 中，当您定义反向代理时，同时定义 &lt;code&gt;header_up X-Real-IP {remote_host}&lt;/code&gt;。更多信息请参阅&lt;a href=&quot;/deployment/proxy-examples&quot;&gt;代理示例&lt;/a&gt;。&lt;/p&gt;
&lt;h3&gt;&lt;a href=&quot;#jail&quot;&gt;&lt;/a&gt;Jail&lt;/h3&gt;
&lt;blockquote&gt;
&lt;p&gt;[&lt;strong&gt;译者注&lt;/strong&gt;]：&lt;a href=&quot;https://docs.freebsd.org/zh-cn/books/arch-handbook/jail/&quot;&gt;什么是 Jail&lt;/a&gt;&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;使用如下内容创建文件：&lt;/p&gt;
&lt;p&gt;复制&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;# path_f2b/jail.d/vaultwarden.local

[vaultwarden]
enabled = true
port = 80,443,8081
filter = vaultwarden
banaction = %(banaction_allports)s
logpath = /path/to/vaultwarden.log
maxretry = 3
bantime = 14400
findtime = 14400
&lt;/code&gt;&lt;/pre&gt;
&lt;h4&gt;&lt;a href=&quot;#note-for-docker-users&quot;&gt;&lt;/a&gt;Docker 用户注意事项&lt;/h4&gt;
&lt;p&gt;Docker 使用 FORWARD 链而不是默认的 INPUT 链。如果接收请求的机器将他们直接映射到 Docker 容器，那么无论容器里有什么（反向代理、Vaultwarden 等），链都需要适当地设置。默认的 &lt;code&gt;action&lt;/code&gt; 被设置为&lt;code&gt;action_&lt;/code&gt;（它使用 &lt;code&gt;banaction&lt;/code&gt;，其别名我们设置为 &lt;code&gt;banaction_allports&lt;/code&gt;），&lt;code&gt;action_&lt;/code&gt; 已经考虑了链的问题，因此，只需设置 &lt;code&gt;chain&lt;/code&gt; 即可。参阅&lt;a href=&quot;https://forum.openwrt.org/t/resolved-fail2ban-and-iptables-ip-bans-not-blocked/90057&quot;&gt;这个类似的问题&lt;/a&gt;。&lt;/p&gt;
&lt;p&gt;复制&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;chain = FORWARD
&lt;/code&gt;&lt;/pre&gt;
&lt;h4&gt;&lt;a href=&quot;#note-for-synology-dsm-docker-users&quot;&gt;&lt;/a&gt;Synology DSM Docker 用户注意事项&lt;/h4&gt;
&lt;p&gt;请将 &lt;code&gt;chain&lt;/code&gt; 设置为 &lt;code&gt;DOCKER-USER&lt;/code&gt;&lt;/p&gt;
&lt;p&gt;复制&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;chain = DOCKER-USER
&lt;/code&gt;&lt;/pre&gt;
&lt;h4&gt;&lt;a href=&quot;#note-for-docker-users-with-fail2ban-v1.1.1.dev1-and-possibly-newer&quot;&gt;&lt;/a&gt;使用 Fail2Ban v1.1.1.dev1（以及可能更高版本）的 Docker 用户注意事项&lt;/h4&gt;
&lt;p&gt;在 Fail2Ban v1.1.1.dev1 中，Debian 的默认 &lt;code&gt;banactions&lt;/code&gt; 从 iptables 变成了 nftables（参阅&lt;a href=&quot;https://github.com/fail2ban/fail2ban/commit/d0d07285234871bad3dc0c359d0ec03365b6dddc&quot;&gt;此处&lt;/a&gt;）。另一方面，Docker（至少是 25.0.3 版）仍在使用 iptables。因此，&lt;code&gt;banaction = %(banaction_allports)s&lt;/code&gt; 无法阻止对 Docker 容器的请求。在这种情况下，使用：&lt;/p&gt;
&lt;p&gt;复制&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;banaction = iptables
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;如果您使用 systemd 来管理 Vaultwarden，您可以为 Fail2ban 使用 systemd-journal：&lt;/p&gt;
&lt;p&gt;复制&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;backend = systemd
filter = vaultwarden[journalmatch=&apos;_SYSTEMD_UNIT=your_vaultwarden.service&apos;]
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;使用它们来代替 &lt;code&gt;logpath = &lt;/code&gt;和 &lt;code&gt;filter = &lt;/code&gt;变量。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;后端注意事项&lt;/strong&gt;：如果您使用 &lt;code&gt;sudo apt install&lt;/code&gt; 等方式安装 fail2ban，&lt;code&gt;/etc/fail2ban/jail.conf&lt;/code&gt; 可能会使用 systemd 作为默认的后端。此默认配置项将导致无法监控 logpath 日志。&lt;/p&gt;
&lt;p&gt;将 &lt;code&gt;backend = pyinotify&lt;/code&gt; 或 &lt;code&gt;backend = inotify&lt;/code&gt; 添加到 &lt;code&gt;vaultwarden.local&lt;/code&gt; 配置中：&lt;/p&gt;
&lt;p&gt;复制&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;# path_f2b/jail.d/vaultwarden.local

[vaultwarden]
enabled = true
backend = pyinotify
port = 80,443,8081
filter = vaultwarden
banaction = %(banaction_allports)s
logpath = /path/to/vaultwarden.log
maxretry = 3
bantime = 14400
findtime = 14400
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;重启 fail2ban 以使更改生效：&lt;/p&gt;
&lt;p&gt;复制&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;sudo systemctl restart fail2ban
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;**Cloudflare 用户注意事项：**如果您使用 Cloudflare 代理，您需要将 Cloudflare 添加到您的操作列表中，如&lt;a href=&quot;https://niksec.com/using-fail2ban-with-cloudflare/&quot;&gt;这个指南&lt;/a&gt;中所示。&lt;/p&gt;
&lt;p&gt;重新加载 Fail2ban 使更改生效：&lt;/p&gt;
&lt;p&gt;复制&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;sudo systemctl reload fail2ban
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;请根据您自己的需要自由修改这些选项。&lt;/p&gt;
&lt;h2&gt;&lt;a href=&quot;#setup-for-admin-page&quot;&gt;&lt;/a&gt;为管理页面设置&lt;/h2&gt;
&lt;p&gt;如果您通过设置 &lt;code&gt;ADMIN_TOKEN&lt;/code&gt; 环境变量启用了管理控制台，则可以使用 Fail2ban 来阻止攻击者暴力破解您的管理令牌。该过程与网页密码库相同。&lt;/p&gt;
&lt;h3&gt;&lt;a href=&quot;#filter-1&quot;&gt;&lt;/a&gt;Filter&lt;/h3&gt;
&lt;p&gt;使用如下内容创建文件：&lt;/p&gt;
&lt;p&gt;复制&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;# path_f2b/filter.d/vaultwarden-admin.local

[INCLUDES]
before = common.conf

[Definition]
failregex = ^.*Invalid admin token\. IP: &amp;lt;ADDR&amp;gt;.*$
ignoreregex =
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;strong&gt;提示&lt;/strong&gt;：如果在 &lt;code&gt;fail2ban.log&lt;/code&gt; 中出现以下错误消息：&lt;code&gt;ERROR NOK: (&quot;No &apos;host&apos; group in &apos;^.*Invalid admin token\\. IP: &amp;lt;ADDR&amp;gt;.*$&apos;&quot;)&lt;/code&gt;，请将 &lt;code&gt;vaultwarden-admin.local&lt;/code&gt; 中的 &lt;code&gt;&amp;lt;ADDR&amp;gt;&lt;/code&gt; 改为 &lt;code&gt;&amp;lt;HOST&amp;gt;&lt;/code&gt;&lt;/p&gt;
&lt;h3&gt;&lt;a href=&quot;#jail-1&quot;&gt;&lt;/a&gt;Jail&lt;/h3&gt;
&lt;p&gt;使用如下内容创建文件：&lt;/p&gt;
&lt;p&gt;复制&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;# path_f2b/jail.d/vaultwarden-admin.local

[vaultwarden-admin]
enabled = true
port = 80,443
filter = vaultwarden-admin
banaction = %(banaction_allports)s
logpath = /path/to/vaultwarden.log
maxretry = 3
bantime = 14400
findtime = 14400
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;strong&gt;注意&lt;/strong&gt;：Docker 使用 FORWARD 链而不是默认的 INPUT 链。因此，当使用 Docker 时，请使用下面的 &lt;code&gt;action&lt;/code&gt; 行替换掉 &lt;code&gt;banaction&lt;/code&gt; 行：&lt;/p&gt;
&lt;p&gt;复制&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;action = iptables-allports[name=vaultwarden, chain=FORWARD]
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;如果您使用 systemd 来管理 Vaultwarden，您同样可以在这里为 Fail2ban 使用 systemd-journal：&lt;/p&gt;
&lt;p&gt;复制&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;backend = systemd
filter = vaultwarden-admin[journalmatch=&apos;_SYSTEMD_UNIT=your_vaultwarden.service&apos;]
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;使用它们来代替 &lt;code&gt;logpath = &lt;/code&gt;和 &lt;code&gt;filter = &lt;/code&gt;变量。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;后端注意事项&lt;/strong&gt;：如果您使用 &lt;code&gt;sudo apt install&lt;/code&gt; 等方式安装 fail2ban，&lt;code&gt;/etc/fai2ban/jail.conf&lt;/code&gt; 可能会使用 systemd 作为默认的后端。此默认配置项将导致无法监控 logpath 日志。&lt;/p&gt;
&lt;p&gt;将 &lt;code&gt;backend = pyinotify&lt;/code&gt; 或 &lt;code&gt;backend = inotify&lt;/code&gt; 添加到 &lt;code&gt;vaultwarden.local&lt;/code&gt; 配置中：&lt;/p&gt;
&lt;p&gt;复制&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;# path_f2b/jail.d/vaultwarden.local

[vaultwarden]
enabled = true
backend = pyinotify
port = 80,443,8081
filter = vaultwarden
banaction = %(banaction_allports)s
logpath = /path/to/vaultwarden.log
maxretry = 3
bantime = 14400
findtime = 14400
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;重启 fail2ban 以使更改生效：&lt;/p&gt;
&lt;p&gt;复制&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;sudo systemctl restart fail2ban
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;**Cloudflare 用户请注意事项：**如果您使用 Cloudflare 代理，您需要将 Cloudflare 添加到您的操作列表中，如&lt;a href=&quot;https://niksec.com/using-fail2ban-with-cloudflare/&quot;&gt;本指南&lt;/a&gt;中所示。&lt;/p&gt;
&lt;p&gt;重新加载 Fail2ban 使更改生效：&lt;/p&gt;
&lt;p&gt;复制&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;sudo systemctl reload fail2ban
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;&lt;a href=&quot;#setup-for-totp&quot;&gt;&lt;/a&gt;为 TOTP 代码设置&lt;/h2&gt;
&lt;p&gt;按照惯例，&lt;code&gt;path_f2b&lt;/code&gt; 表示 Fail2ban 运行所需的路径。这取决于您的系统。例如，在 Synology 上，我们讨论的是 &lt;code&gt;/volumeX/docker/fail2ban/&lt;/code&gt;，而在其他一些系统上，我们讨论的是 &lt;code&gt;/etc/fail2ban/&lt;/code&gt;。&lt;/p&gt;
&lt;h3&gt;&lt;a href=&quot;#filter-2&quot;&gt;&lt;/a&gt;Filter&lt;/h3&gt;
&lt;p&gt;使用如下内容创建文件：&lt;/p&gt;
&lt;p&gt;复制&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;# path_f2b/filter.d/vaultwarden-totp.local
# Fail2Ban filter for Vaultwarden TOTP

[INCLUDES]
before = common.conf

[Definition]
failregex = ^.*\[ERROR\] Invalid TOTP code! Server time: (.*) UTC IP: &amp;lt;ADDR&amp;gt;$
ignoreregex =
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;日志示例：&lt;/p&gt;
&lt;p&gt;复制&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;[YYYY-MM-DD hh:mm:ss][vaultwarden::api::core::two_factor::authenticator][ERROR] Invalid TOTP code! Server time: YYYY-MM-DD hh:mm:ss UTC IP: 1.2.3.4
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;&lt;a href=&quot;#jail-2&quot;&gt;&lt;/a&gt;Jail&lt;/h3&gt;
&lt;p&gt;使用如下内容创建文件：&lt;/p&gt;
&lt;p&gt;复制&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;# path_f2b/jail.d/vaultwarden-totp.local

[vaultwarden-totp]
enabled = true
port = 80,443
filter = vaultwarden-totp
banaction = iptables-multiport[name=vaultwarden-totp, port=&quot;80,443&quot;, protocol=tcp]
logpath = /path/to/vaultwarden.log
maxretry = 3
bantime = 14400
findtime = 14400
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;重启 fail2ban 以使更改生效：&lt;/p&gt;
&lt;p&gt;复制&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;sudo systemctl restart fail2ban
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;请根据您自己的需要自由修改这些选项。&lt;/p&gt;
&lt;h2&gt;&lt;a href=&quot;#testing-fail-2-ban&quot;&gt;&lt;/a&gt;测试 Fail2ban&lt;/h2&gt;
&lt;p&gt;现在，尝试使用任何电子邮件地址登录 Vaultwarden（不必是有效电子邮件，只需是电子邮件格式即可）。如果它可以正常工作，您的 IP 将被阻止。运行以下命令来取消阻止的 IP：&lt;/p&gt;
&lt;p&gt;复制&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;# 使用 Docker
sudo docker exec -t fail2ban fail2ban-client set vaultwarden unbanip XX.XX.XX.XX
# 未使用 Docker
sudo fail2ban-client set vaultwarden unbanip XX.XX.XX.XX
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;如果 Fail2ban 无法正常运行，请检查 Vaultwarden 日志文件的路径是否正确。对于 Docker：如果指定的日志文件未生成和/或更新，请确保将 &lt;code&gt;EXTENDED_LOGGING&lt;/code&gt; 变量设置为 &lt;code&gt;true&lt;/code&gt;（默认值），并且确保日志文件的路径是 Docker 内部的路径（当您使用 &lt;code&gt;/vw-data/:/data/&lt;/code&gt; 时，日志文件应位于容器外部的 &lt;code&gt;/data/...&lt;/code&gt; 中）。&lt;/p&gt;
&lt;p&gt;还要确认 Docker 容器的时区与主机的时区是否一致。通过将日志文件中显示的时间与主机操作系统的时间进行比较来进行检查。如果它们不一致，则有多种解决方法。一种是使用 &lt;code&gt;-e &quot;TZ = &amp;lt;timezone&amp;gt;&quot;&lt;/code&gt; 选项启动 Docker 。可用的时区（比如 &lt;code&gt;-e TZ = &quot;Australia/Melbourne&quot;&lt;/code&gt;）列表在&lt;a href=&quot;https://en.wikipedia.org/wiki/List_of_tz_database_time_zones&quot;&gt;这里&lt;/a&gt;查看。&lt;/p&gt;
&lt;p&gt;如果您使用的是 podman 而不是 Docker，则无法通过 &lt;code&gt;-e &quot;TZ = &amp;lt;timezone&amp;gt;&quot;&lt;/code&gt; 来设置时区。可以按照以下指南解决此问题（当使用 alpine 镜像时）：&lt;a href=&quot;https://wiki.alpinelinux.org/wiki/Setting_the_timezone&quot;&gt;https://wiki.alpinelinux.org/wiki/Setting_the_timezone&lt;/a&gt;。&lt;/p&gt;
&lt;h2&gt;&lt;a href=&quot;#selinux-problems&quot;&gt;&lt;/a&gt;SELinux 中的问题&lt;/h2&gt;
&lt;p&gt;当使用 SELinux 时，SELinux 可能会阻止 Fail2ban 读取日志。如果是这样，请运行此命令： &lt;code&gt;sudo tail /var/log/audit/audit.log&lt;/code&gt;。您应该会看到如下类似内容（当然，实际的审核 ID (pid) 会因您的情况而不一样）：&lt;/p&gt;
&lt;p&gt;复制&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;type=AVC msg=audit(1571777936.719:2193): avc:  denied  { search } for  pid=5853 comm=&quot;fail2ban-server&quot; name=&quot;containers&quot; dev=&quot;dm-0&quot; ino=1144588 scontext=system_u:system_r:fail2ban_t:s0 tcontext=unconfined_u:object_r:container_var_lib_t:s0 tclass=dir permissive=0
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;您可以使用 &lt;code&gt;grep &apos;type=AVC msg=audit(1571777936.719:2193)&apos; /var/log/audit/audit.log | audit2why&lt;/code&gt; 来找出真正的原因。&lt;code&gt;audit2allow -a&lt;/code&gt; 将为您提供有关如何创建模块并允许 Fail2ban 访问日志的具体说明。&lt;/p&gt;
&lt;p&gt;按照这些步骤操作后就结束了！Fail2ban 现在应该可以正常工作了。&lt;/p&gt;
&lt;h1&gt;第三方ai配置&lt;/h1&gt;
&lt;h3&gt;&lt;strong&gt;1. 确认 Vaultwarden 日志路径&lt;/strong&gt;&lt;/h3&gt;
&lt;p&gt;Vaultwarden 默认会将日志记录到以下位置：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;日志文件路径&lt;/strong&gt;：&lt;code&gt;/path/to/vaultwarden/logs/identity.log&lt;/code&gt;
&lt;ul&gt;
&lt;li&gt;如果你使用 Docker 部署，日志可能在容器内或挂载到宿主机的某个目录中。&lt;/li&gt;
&lt;li&gt;例如：&lt;code&gt;/var/log/vaultwarden/identity.log&lt;/code&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;确保你知道日志文件的具体位置，并确认日志中包含失败登录尝试的信息，通常类似于以下格式：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;[2025-04-02 18:00:00][WARN] Failed login attempt. IP: 192.168.1.100
&lt;/code&gt;&lt;/pre&gt;
&lt;hr /&gt;
&lt;h3&gt;&lt;strong&gt;2. 创建 Fail2Ban 过滤器&lt;/strong&gt;&lt;/h3&gt;
&lt;p&gt;Fail2Ban 使用过滤器来解析日志文件并提取攻击者的 IP 地址。我们需要为 Vaultwarden 创建一个自定义过滤器。&lt;/p&gt;
&lt;h4&gt;(1) 创建过滤器文件&lt;/h4&gt;
&lt;p&gt;创建一个新的过滤器文件，例如 &lt;code&gt;/etc/fail2ban/filter.d/vaultwarden.conf&lt;/code&gt;：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;sudo nano /etc/fail2ban/filter.d/vaultwarden.conf
&lt;/code&gt;&lt;/pre&gt;
&lt;h4&gt;(2) 编辑过滤器内容&lt;/h4&gt;
&lt;p&gt;在文件中添加以下内容：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;[INCLUDES]
before = common.conf

[Definition]
_daemon = vaultwarden
failregex = ^\[\d{4}-\d{2}-\d{2} \d{2}:\d{2}:\d{2}\]\[WARN\] Failed login attempt\. IP: &amp;lt;ADDR&amp;gt;$
ignoreregex =
&lt;/code&gt;&lt;/pre&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;&lt;code&gt;_daemon&lt;/code&gt;&lt;/strong&gt;：指定服务名称，这里是 &lt;code&gt;vaultwarden&lt;/code&gt;。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;&lt;code&gt;failregex&lt;/code&gt;&lt;/strong&gt;：
&lt;ul&gt;
&lt;li&gt;匹配日志中的失败登录尝试。&lt;/li&gt;
&lt;li&gt;&lt;code&gt;&amp;lt;ADDR&amp;gt;&lt;/code&gt; 是 Fail2Ban 的占位符，用于提取 IP 地址。&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;&lt;code&gt;ignoreregex&lt;/code&gt;&lt;/strong&gt;：忽略某些特定的日志行（这里为空，表示不忽略任何内容）。&lt;/li&gt;
&lt;/ul&gt;
&lt;h4&gt;(3) 测试过滤器&lt;/h4&gt;
&lt;p&gt;使用 &lt;code&gt;fail2ban-regex&lt;/code&gt; 工具测试过滤器是否能正确匹配日志中的失败登录尝试：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;sudo fail2ban-regex /path/to/vaultwarden/logs/identity.log /etc/fail2ban/filter.d/vaultwarden.conf
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;如果测试成功，你会看到类似以下输出：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;Lines: 10 lines, 0 ignored, 5 matched, 5 missed
&lt;/code&gt;&lt;/pre&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;&lt;code&gt;matched&lt;/code&gt;&lt;/strong&gt; 表示成功匹配的日志行数。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;&lt;code&gt;missed&lt;/code&gt;&lt;/strong&gt; 表示未匹配的日志行数。&lt;/li&gt;
&lt;/ul&gt;
&lt;hr /&gt;
&lt;h3&gt;&lt;strong&gt;3. 配置 Fail2Ban Jail&lt;/strong&gt;&lt;/h3&gt;
&lt;p&gt;Jail 是 Fail2Ban 的规则集，用于定义如何处理匹配到的事件。&lt;/p&gt;
&lt;h4&gt;(1) 编辑 Jail 配置文件&lt;/h4&gt;
&lt;p&gt;编辑 Fail2Ban 的主配置文件 &lt;code&gt;/etc/fail2ban/jail.local&lt;/code&gt;：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;sudo nano /etc/fail2ban/jail.local
&lt;/code&gt;&lt;/pre&gt;
&lt;h4&gt;(2) 添加 Vaultwarden Jail 配置&lt;/h4&gt;
&lt;p&gt;在文件末尾添加以下内容：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;[vaultwarden]
enabled  = true
filter   = vaultwarden
logpath  = /path/to/vaultwarden/logs/identity.log
maxretry = 5
bantime  = 3600
findtime = 600
action   = iptables[name=Vaultwarden, port=http, protocol=tcp]
&lt;/code&gt;&lt;/pre&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;&lt;code&gt;enabled&lt;/code&gt;&lt;/strong&gt;：启用该 Jail。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;&lt;code&gt;filter&lt;/code&gt;&lt;/strong&gt;：指定使用的过滤器（即我们刚刚创建的 &lt;code&gt;vaultwarden&lt;/code&gt;）。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;&lt;code&gt;logpath&lt;/code&gt;&lt;/strong&gt;：指定 Vaultwarden 的日志文件路径。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;&lt;code&gt;maxretry&lt;/code&gt;&lt;/strong&gt;：允许的最大失败尝试次数（例如 5 次）。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;&lt;code&gt;bantime&lt;/code&gt;&lt;/strong&gt;：封禁时间（单位为秒，例如 3600 秒 = 1 小时）。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;&lt;code&gt;findtime&lt;/code&gt;&lt;/strong&gt;：检测时间窗口（单位为秒，例如 600 秒 = 10 分钟）。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;&lt;code&gt;action&lt;/code&gt;&lt;/strong&gt;：指定封禁动作，这里使用 &lt;code&gt;iptables&lt;/code&gt; 来阻止 HTTP 请求。&lt;/li&gt;
&lt;/ul&gt;
&lt;h4&gt;(3) 重新加载 Fail2Ban&lt;/h4&gt;
&lt;p&gt;保存配置后，重新加载 Fail2Ban 以应用更改：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;sudo systemctl reload fail2ban
&lt;/code&gt;&lt;/pre&gt;
&lt;hr /&gt;
&lt;h3&gt;&lt;strong&gt;4. 验证配置&lt;/strong&gt;&lt;/h3&gt;
&lt;h4&gt;(1) 检查 Jail 状态&lt;/h4&gt;
&lt;p&gt;运行以下命令查看 Vaultwarden Jail 是否已启用：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;sudo fail2ban-client status vaultwarden
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;输出示例：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;Status for the jail: vaultwarden
|- Filter
|  |- Currently failed: 0
|  |- Total failed:     5
|  `- File list:        /path/to/vaultwarden/logs/identity.log
`- Actions
   |- Currently banned: 1
   |- Total banned:     1
   `- Banned IP list:   192.168.1.100
&lt;/code&gt;&lt;/pre&gt;
&lt;h4&gt;(2) 模拟攻击&lt;/h4&gt;
&lt;p&gt;你可以通过多次尝试错误的登录信息来模拟攻击，然后检查 Fail2Ban 是否正确封禁了你的 IP。&lt;/p&gt;
&lt;hr /&gt;
&lt;h3&gt;&lt;strong&gt;5. 可选优化&lt;/strong&gt;&lt;/h3&gt;
&lt;h4&gt;(1) 使用 Docker 部署&lt;/h4&gt;
&lt;p&gt;如果你使用 Docker 部署 Vaultwarden，可以通过挂载日志文件的方式将日志暴露给宿主机。例如：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;volumes:
  - ./vaultwarden-logs:/data/logs
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;然后在 Fail2Ban 中指定挂载的日志路径。&lt;/p&gt;
&lt;h4&gt;(2) 增强安全性&lt;/h4&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;限制 SSH 访问&lt;/strong&gt;：仅允许基于密钥的身份验证。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;启用双因素认证（2FA）&lt;/strong&gt;：确保所有用户都启用了 2FA。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;定期更新 Vaultwarden&lt;/strong&gt;：保持最新版本以修复已知漏洞。&lt;/li&gt;
&lt;/ul&gt;
</content:encoded></item><item><title>Vaultwarden环境变量</title><link>https://yanghc.dev/posts/74786dc6/</link><guid isPermaLink="true">https://yanghc.dev/posts/74786dc6/</guid><pubDate>Sat, 22 Feb 2025 00:00:00 GMT</pubDate><content:encoded>&lt;pre&gt;&lt;code&gt;# shellcheck disable=SC2034,SC2148
## Vaultwarden 配置文件
## 取消注释以下任何行以更改默认设置
##
## 请注意，如果在管理员界面中更改了这些设置，则大多数设置将被覆盖。这些覆盖会存储在 DATA_FOLDER/config.json 文件中。
##
## 默认情况下，Vaultwarden 希望此文件名为“.env”，并位于当前工作目录中。如果不是这种情况，可以在启动 Vaultwarden 之前设置环境变量 ENV_FILE 指向此文件的位置。

####################
### 数据文件夹 ###
####################

## 主数据文件夹
# DATA_FOLDER=data

## 单独的文件夹，这些选项会覆盖 %DATA_FOLDER%
# RSA_KEY_FILENAME=data/rsa_key
# ICON_CACHE_FOLDER=data/icon_cache
# ATTACHMENTS_FOLDER=data/attachments
# SENDS_FOLDER=data/sends
# TMP_FOLDER=data/tmp

## 模板数据文件夹，默认使用嵌入式模板
## 查看源代码以了解格式
# TEMPLATES_FOLDER=data/templates
## 对每个请求自动重新加载模板，较慢，仅用于开发环境
# RELOAD_TEMPLATES=false

## Web 密码库设置
# WEB_VAULT_FOLDER=web-vault/
# WEB_VAULT_ENABLED=true

#########################
### 数据库设置 ###
#########################

## 数据库 URL
## 使用 SQLite 时，这是 DB 文件的路径，默认为 %DATA_FOLDER%/db.sqlite3
# DATABASE_URL=data/db.sqlite3
## 使用 MySQL 时，请指定适当的连接 URI。
## 详情：https://docs.diesel.rs/2.1.x/diesel/mysql/struct.MysqlConnection.html
# DATABASE_URL=mysql://用户名:密码@主机[:端口]/数据库名
## 使用 PostgreSQL 时，请指定适当的连接 URI（推荐）或关键字/值连接字符串。
## 详情：
## - https://docs.diesel.rs/2.1.x/diesel/pg/struct.PgConnection.html
## - https://www.postgresql.org/docs/current/libpq-connect.html#LIBPQ-CONNSTRING
# DATABASE_URL=postgresql://用户名:密码@主机[:端口]/数据库名

## 启用 WAL（预写日志）
## 设置为 false 以避免在启动时启用 WAL。
## 注意，如果数据库已启用 WAL，您还需要在数据库中禁用 WAL，此设置仅阻止 Vaultwarden 在启动时自动启用它。
## 请先阅读项目 wiki 页面中的相关设置，因为修改此值可能会导致性能下降或使服务无法启动。
# ENABLE_DB_WAL=true

## 数据库连接重试次数
## 启动期间尝试连接数据库的次数，每次重试间隔 1 秒，设置为 0 表示无限次重试
# DB_CONNECTION_RETRIES=15

## 数据库超时时间
## 获取数据库连接的超时时间
# DATABASE_TIMEOUT=30

## 数据库最大连接数
## 定义用于连接数据库的连接池大小。
# DATABASE_MAX_CONNS=10

## 数据库连接初始化
## 允许在创建新数据库连接时运行 SQL 语句。
## 这主要用于连接范围的指令。
## 如果为空，则使用数据库特定的默认值：
## - SQLite: &quot;PRAGMA busy_timeout = 5000; PRAGMA synchronous = NORMAL;&quot;
## - MySQL: &quot;&quot;
## - PostgreSQL: &quot;&quot;
# DATABASE_CONN_INIT=&quot;&quot;

#################
### WebSocket ###
#################

## 启用 WebSocket 通知
# ENABLE_WEBSOCKET=true

##########################
### 推送通知 ###
##########################

## 启用推送通知（需要从 https://bitwarden.com/host 获取密钥和 ID）
## 关于移动客户端推送通知的详情：
## - https://github.com/dani-garcia/vaultwarden/wiki/Enabling-Mobile-Client-push-notification
# PUSH_ENABLED=false
# PUSH_INSTALLATION_ID=请替换此处
# PUSH_INSTALLATION_KEY=请替换此处

# 警告：除非完全理解其含义，否则不要修改以下设置！
# 默认推送中继和身份验证 URI
# PUSH_RELAY_URI=https://push.bitwarden.com
# PUSH_IDENTITY_URI=https://identity.bitwarden.com
# 欧盟数据区域设置
# 如果选择了“欧盟”作为您的数据区域，请使用以下 URI：
# PUSH_RELAY_URI=https://api.bitwarden.eu
# PUSH_IDENTITY_URI=https://identity.bitwarden.eu

#####################
### 计划任务 ###
#####################

## 计划任务设置
##
## 任务计划使用类似 cron 的语法（由 https://crates.io/crates/cron 解析），并且始终基于 UTC 时间（无论您的本地时区设置如何）。
##
## 该格式与 crontab 略有不同，因为 crontab 不包含秒。
## 您可以在这里测试格式：https://crontab.guru，但请删除第一个数字！
## 秒   分   小时   月份中的某天    月   星期中的某天
## &quot;0   30   9,12,15     1,15       May-Aug  Mon,Wed,Fri&quot;
## &quot;0   30     *          *            *          *     &quot;
## &quot;0   30     1          *            *          *     &quot;
##
## 工作调度线程检查需要运行的任务的时间间隔（毫秒）。设置为 0 可全局禁用计划任务。
# JOB_POLL_INTERVAL_MS=30000
##
## 检查已过删除日期的发送任务的计划。默认每小时一次（整点后 5 分钟）。留空可禁用此任务。
# SEND_PURGE_SCHEDULE=&quot;0 5 * * * *&quot;
##
## 检查应永久删除的垃圾箱项目的计划。默认每天一次（午夜后 5 分钟）。留空可禁用此任务。
# TRASH_PURGE_SCHEDULE=&quot;0 5 0 * * *&quot;
##
## 检查未完成的双因素登录的计划。默认每分钟一次。留空可禁用此任务。
# INCOMPLETE_2FA_SCHEDULE=&quot;30 * * * * *&quot;
##
## 发送紧急访问授权人到期提醒的计划。默认每小时一次（整点后 3 分钟）。留空可禁用此任务。
# EMERGENCY_NOTIFICATION_REMINDER_SCHEDULE=&quot;0 3 * * * *&quot;
##
## 处理已达到等待时间的紧急访问请求的计划。默认每小时一次（整点后 7 分钟）。留空可禁用此任务。
# EMERGENCY_REQUEST_TIMEOUT_SCHEDULE=&quot;0 7 * * * *&quot;
##
## 清理事件表中旧事件的计划。默认每天执行一次。如果不设置 EVENTS_DAYS_RETAIN，此任务不会启动。
# EVENT_CLEANUP_SCHEDULE=&quot;0 10 0 * * *&quot;
## 数据库中保留事件的天数。
## 如果未设置（默认），事件将无限期保留，并禁用计划任务！
# EVENTS_DAYS_RETAIN=
##
## 清理数据库中旧认证请求的计划。默认每分钟一次。留空可禁用此任务。
# AUTH_REQUEST_PURGE_SCHEDULE=&quot;30 * * * * *&quot;
##
## 清理数据库中过期的 Duo 上下文的计划。如果 Duo MFA 被禁用或设置为使用旧版 iframe 提示，则不执行操作。默认每分钟一次。留空可禁用此任务。
# DUO_CONTEXT_PURGE_SCHEDULE=&quot;30 * * * * *&quot;

########################
### 通用设置 ###
########################

## 域名设置
## 域名必须与您访问服务器的地址匹配。
## 推荐配置此值，否则某些功能可能无法工作，如附件下载、电子邮件链接和U2F。
## 要使 U2F 正常工作，服务器必须使用 HTTPS，您可以使用 Let&apos;s Encrypt 获取免费证书。
## 使用 HTTPS 的推荐方式是将 Vaultwarden 放在反向代理后面。
## 详情：
## - https://github.com/dani-garcia/vaultwarden/wiki/Enabling-HTTPS
## - https://github.com/dani-garcia/vaultwarden/wiki/Proxy-examples
## 开发环境
# DOMAIN=http://localhost
## 公共服务器
# DOMAIN=https://vw.domain.tld
## 公共服务器（带端口号的URL）
# DOMAIN=https://vw.domain.tld:8443
## 公共服务器（带路径的URL）
# DOMAIN=https://domain.tld/vw

## 控制是否允许用户创建 Bitwarden Sends。
## 此设置对所有用户全局适用。
## 若要按组织控制此设置，请使用“禁用发送”组织策略。
# SENDS_ALLOWED=true

## HIBP API 密钥
## HaveIBeenPwned API 密钥，可在此请求：https://haveibeenpwned.com/API/Key
# HIBP_API_KEY=

## 每个组织的附件存储限制（KB）
## 每个组织允许的最大附件存储量（以 KB 为单位）。
## 当达到此限制时，组织成员将不允许上传更多属于该组织的密码附件。
# ORG_ATTACHMENT_LIMIT=
## 每个用户的附件存储限制（KB）
## 每个用户允许的最大附件存储量（以 KB 为单位）。
## 当达到此限制时，用户将不允许上传更多附件。
# USER_ATTACHMENT_LIMIT=
## 每个用户的发送存储限制（KB）
## 每个用户允许的最大发送存储量（以 KB 为单位）。
## 当达到此限制时，用户将不允许上传更多发送内容。
# USER_SEND_LIMIT=

## 在自动删除已放入回收站的项目前等待的天数。
## 如果未设置（默认），则不会自动删除回收站中的项目。
## 此设置全局适用，因此请确保通知所有用户有关此设置的任何更改。
# TRASH_AUTO_DELETE_DAYS=

## 等待多少分钟后认为两次验证登录不完整，并触发电子邮件通知。不完整的两次验证登录是指提供了正确的主密码但未完成所需的二次验证步骤，这可能表示主密码被泄露。设置为 0 可禁用此检查。
## 此设置对所有用户全局适用。
# INCOMPLETE_2FA_TIME_LIMIT=3

## 禁用图标下载
## 设置为 true 以禁用内部图标服务中的图标下载。
## 这仍然会从 $ICON_CACHE_FOLDER 提供现有图标，而不生成任何外部网络请求。$ICON_CACHE_TTL 也必须设置为 0；否则，现有的图标最终会被删除，但不会再被下载。
# DISABLE_ICON_DOWNLOAD=false

## 控制是否允许新用户注册
# SIGNUPS_ALLOWED=true

## 控制新用户注册时是否需要验证其电子邮件地址。
## 注意，如果将此选项设置为 true，则在验证电子邮件地址之前禁止登录！
## 欢迎邮件中会包含一个验证链接，尝试登录时会定期触发重新发送验证邮件。
# SIGNUPS_VERIFY=false

## 如果 SIGNUPS_VERIFY 设置为 true，这限制了自上次发送电子邮件验证链接后多长时间会再次发送验证邮件（秒）
# SIGNUPS_VERIFY_RESEND_TIME=3600

## 如果 SIGNUPS_VERIFY 设置为 true，这限制了尝试登录时可以重新发送验证邮件的最大次数。
# SIGNUPS_VERIFY_RESEND_LIMIT=6

## 控制来自逗号分隔域名列表的新用户是否可以注册，即使 SIGNUPS_ALLOWED 设置为 false
# SIGNUPS_DOMAINS_WHITELIST=example.com,example.net,example.org

## 控制是否为组织启用事件日志记录。
## 此设置适用于组织。
## 默认情况下禁用。还需检查 EVENT_CLEANUP_SCHEDULE 和 EVENTS_DAYS_RETAIN 设置。
# ORG_EVENTS_ENABLED=false

## 控制哪些用户可以创建新的组织。
## 空或 &apos;all&apos; 表示所有用户都可以创建组织（这是默认值）：
# ORG_CREATION_USERS=
## &apos;none&apos; 表示没有用户可以创建组织：
# ORG_CREATION_USERS=none
## 逗号分隔列表表示只有这些用户可以创建组织：
# ORG_CREATION_USERS=admin1@example.com,admin2@example.com

## 即使注册被禁用，邀请组织管理员邀请用户
# INVITATIONS_ALLOWED=true
## 在不属于特定组织的邀请电子邮件中显示的名称
# INVITATION_ORG_NAME=Vaultwarden

## 组织邀请令牌、紧急访问邀请令牌、电子邮件验证令牌和删除请求令牌将在多少小时后过期（必须至少为 1 小时）
# INVITATION_EXPIRATION_HOURS=120

## 控制用户是否可以启用对其帐户的紧急访问。
## 此设置对所有用户全局适用。
# EMERGENCY_ACCESS_ALLOWED=true

## 控制用户是否可以更改其电子邮件。
## 此设置对所有用户全局适用。
# EMAIL_CHANGE_ALLOWED=true

## 密码哈希迭代次数用于服务器端密码哈希。
## 新用户的默认值。如果更改，会在用户下次登录时更新现有用户的设置。
# PASSWORD_ITERATIONS=600000

## 控制用户是否可以设置或显示密码提示。此设置对所有用户全局适用。
# PASSWORD_HINTS_ALLOWED=true

## 控制当 SMTP 服务未配置且允许密码提示时，是否应在网页上直接显示密码提示。
## 不建议在公共可访问实例上使用，因为这会提供对潜在敏感数据的未经授权访问。
# SHOW_PASSWORD_HINT=false

#########################
### 高级设置 ###
#########################

## 客户端 IP 头，用于识别客户端的 IP，默认为 &quot;X-Real-IP&quot;
## 设置为字符串 &quot;none&quot;（不带引号），以禁用任何头并仅使用远程 IP
# IP_HEADER=X-Real-IP

## 图标服务
## 预定义的图标服务有：internal, bitwarden, duckduckgo, google。
## 要指定自定义图标服务，请设置一个包含一个 `{}` 的 URL 模板，该模板会被替换为域名。例如：`https://icon.example.com/domain/{}`。
##
## `internal` 指的是 Vaultwarden 内置的图标获取实现。
## 如果设置了外部服务，对 Vaultwarden 的图标请求将返回 HTTP 重定向到外部服务上的相应图标。外部服务可能在您的 Vaultwarden 实例没有外部网络连接时有用，或者如果您担心有人可能会探测您的实例以尝试检测某些站点的图标是否已缓存。
# ICON_SERVICE=internal

## 图标重定向代码
## 用于重定向到外部图标服务的 HTTP 状态码。
## 支持的代码有 301（永久）、302（临时）、307（临时）和 308（永久）。临时重定向在测试不同的图标服务时很有用，但一旦确定了服务，建议使用永久重定向以提高缓存能力。旧版代码目前在 Bitwarden 客户端中支持得更好。
# ICON_REDIRECT_CODE=302

## 成功获取的图标的缓存生存时间（秒），0 表示“永远”
## 默认值：2592000（30天）
# ICON_CACHE_TTL=2592000
## 未找到的图标的缓存生存时间（秒），0 表示“永远”
## 默认值：259200（3天）
# ICON_CACHE_NEGTTL=259200

## 图标下载超时
## 配置下载 favicon 时的超时值。
## 默认是 10 秒，但在较慢的网络连接上可能过低。
# ICON_DOWNLOAD_TIMEOUT=10

## 通过正则表达式阻止 HTTP 域名/IP
## 任何匹配此正则表达式的域名或 IP 都不会被内部 HTTP 客户端获取。
## 对于隐藏本地网络中的其他服务器很有用。更多信息请参阅 WIKI。
## 注意：始终将此正则表达式用单引号括起来！
# HTTP_REQUEST_BLOCK_REGEX=&apos;^(192\.168\.0\.[0-9]+|192\.168\.1\.[0-9]+)$&apos;

## 启用此选项将导致内部 HTTP 客户端拒绝连接到任何非全局 IP 地址。
## 用于保护内部环境：参见 https://en.wikipedia.org/wiki/Reserved_IP_addresses 了解其会阻止的 IP 列表。
# HTTP_REQUEST_BLOCK_NON_GLOBAL_IPS=true

## 客户端设置
## 启用客户端的实验性功能标志。
## 这是一个逗号分隔的功能标志列表，例如 &quot;flag1,flag2,flag3&quot;。
##
## 可用的标志如下：
## - &quot;autofill-overlay&quot;: 在表单字段中添加一个覆盖菜单，以便快速访问凭据。
## - &quot;autofill-v2&quot;: 使用新的自动填充实现。
## - &quot;browser-fileless-import&quot;: 直接从其他提供商导入凭据而无需文件。
## - &quot;extension-refresh&quot;: 临时启用新扩展设计直到正式发布（应与 beta Chrome 扩展一起使用）。
## - &quot;fido2-vault-credentials&quot;: 启用 FIDO2 安全密钥作为第二因素。
## - &quot;inline-menu-positioning-improvements&quot;: 启用浏览器扩展中的内联菜单密码生成器和身份建议。
## - &quot;ssh-key-vault-item&quot;: 启用 SSH 密钥保险库项的创建和使用。（需要客户端 &amp;gt;=2024.12.0）
## - &quot;ssh-agent&quot;: 在桌面上启用 SSH 代理支持。（需要桌面 &amp;gt;=2024.12.0）
# EXPERIMENTAL_CLIENT_FEATURE_FLAGS=fido2-vault-credentials

## 要求新设备电子邮件。当用户登录时需要发送一封电子邮件。
## 如果发送电子邮件失败，则登录尝试将失败！
# REQUIRE_DEVICE_EMAIL=false

## 启用扩展日志记录，这将在日志中显示时间戳和目标。
# EXTENDED_LOGGING=true

## 扩展日志记录的时间戳格式。
## 格式说明符：https://docs.rs/chrono/latest/chrono/format/strftime
# LOG_TIMESTAMP_FORMAT=&quot;%Y-%m-%d %H:%M:%S.%3f&quot;

## 日志记录到 Syslog
## 这需要扩展日志记录
# USE_SYSLOG=false

## 记录到文件
# LOG_FILE=/path/to/log

## 日志级别
## 更改日志输出的详细程度。
## 有效值为 &quot;trace&quot;, &quot;debug&quot;, &quot;info&quot;, &quot;warn&quot;, &quot;error&quot; 和 &quot;off&quot;。
## 将其设置为 &quot;trace&quot; 或 &quot;debug&quot; 也会显示挂载路由和静态文件、WebSocket 和 alive 请求的日志。
## 对于特定模块，可以附加一个逗号分隔的 `path::to::module=log_level`
## 例如，要只为图标查看 info 日志，使用：LOG_LEVEL=&quot;info,vaultwarden::api::icons=debug&quot;
# LOG_LEVEL=info

## 管理界面的令牌，最好是 Argon2 PHC 字符串。
## Vaultwarden 提供了一个内置生成器，可以通过调用 `vaultwarden hash` 来生成。
## 详情请参阅：https://github.com/dani-garcia/vaultwarden/wiki/Enabling-admin-page#secure-the-admin_token
## 如果未设置，管理面板将被禁用。
## 新的 Argon2 PHC 字符串
## 注意，在某些环境中，如 docker-compose 中，您需要将所有美元符号 `$` 转义为两个美元符号 `$$`。
## 并且在必要时使用单引号 (&apos;) 而不是双引号 (&quot;) 包含字符串。
# ADMIN_TOKEN=&apos;$argon2id$v=19$m=65540,t=3,p=4$MmeKRnGK5RW5mJS7h3TOL89GrpLPXJPAtTK8FTqj9HM$DqsstvoSAETl9YhnsXbf43WeaUwJC6JhViIvuPoig78&apos;
## 旧的明文字符串（会产生警告，建议使用 Argon2）
# ADMIN_TOKEN=Vy2VyYTTsKPv8W5aEOWUbB/Bt3DEKePbHmI4m9VcemUMS2rEviDowNAFqYi1xjmp

## 启用此选项以绕过管理面板的安全性。此选项仅适用于前面带有单独认证层的情况。
# DISABLE_ADMIN_TOKEN=false

## 在相同的 IP 地址下，两次管理登录请求之间的平均秒数，超过这个时间限制才会触发限速。
# ADMIN_RATELIMIT_SECONDS=300
## 允许的最大突发请求数，同时保持由 `ADMIN_RATELIMIT_SECONDS` 指定的平均值。
# ADMIN_RATELIMIT_MAX_BURST=3

## 设置管理员会话的有效期为此值（分钟）。
# ADMIN_SESSION_LIFETIME=20

## 允许的 iframe 祖先（了解风险！）
## https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Content-Security-Policy/frame-ancestors
## 允许其他域嵌入 web vault 到 iframe 中，便于嵌入安全的内部网。
## 此值将被添加到 &apos;Content-Security-Policy&apos; 头部的 &apos;frame-ancestors&apos; 值中。
## 多个值必须用空格分隔。
# ALLOWED_IFRAME_ANCESTORS=

## 允许的 connect-src（了解风险！）
## https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Content-Security-Policy/connect-src
## 允许其他域加载使用脚本接口（如转发电子邮件别名功能）的 URL。
## 此值将被添加到 &apos;Content-Security-Policy&apos; 头部的 &apos;connect-src&apos; 值中。
## 多个值必须用空格分隔，并且只允许 HTTPS 值。
## 示例：&quot;https://my-addy-io.domain.tld https://my-simplelogin.domain.tld&quot;
# ALLOWED_CONNECT_SRC=&quot;&quot;

## 在相同的 IP 地址下，两次登录请求之间的平均秒数，超过这个时间限制才会触发限速。
# LOGIN_RATELIMIT_SECONDS=60
## 允许的最大突发请求数，同时保持由 `LOGIN_RATELIMIT_SECONDS` 指定的平均值。
## 注意：这适用于登录和两步验证，因此建议至少允许突发大小为 2。
# LOGIN_RATELIMIT_MAX_BURST=10

## BETA 功能：组
## 控制组织是否启用组支持。
## 此设置适用于组织。
## 默认情况下禁用，因为这是一个 Beta 功能，包含已知问题！
## 了解你在做什么！
# ORG_GROUPS_ENABLED=false

## 增加安全笔记大小限制（了解风险！）
## 将安全笔记大小限制设置为 100_000 而不是默认的 10_000。
## 警告：这可能导致客户端出现问题。此外，在 Bitwarden 服务器上导出将不起作用！
## 了解你在做什么！
# INCREASE_NOTE_SIZE_LIMIT=false

## 强制单组织策略与重置密码策略
## 强制在启用重置密码策略之前启用单组织策略。
## Bitwarden 默认强制执行此策略。在 Vaultwarden 中，我们鼓励使用多个组织，因为组不可用。
## 将此设置为 true 将在启用重置密码策略之前强制启用单组织策略。
# ENFORCE_SINGLE_ORG_WITH_RESET_PW_POLICY=false

########################
### MFA/2FA 设置 ###
########################

## Yubico (Yubikey) 设置
## 设置您的 Yubikey OTP 的 Client ID 和 Secret Key。
## 您可以在此生成：https://upgrade.yubico.com/getapikey/
## 您也可以选择指定一个自定义的 OTP 服务器。
# YUBICO_CLIENT_ID=11111
# YUBICO_SECRET_KEY=AAAAAAAAAAAAAAAAAAAAAAAA
# YUBICO_SERVER=http://yourdomain.com/wsapi/2.0/verify

## Duo 设置
## 您需要配置 DUO_IKEY, DUO_SKEY 和 DUO_HOST 选项以启用全局 Duo 支持。
## 否则用户需要自己配置这些设置。
## 创建账户并保护应用程序，请参阅此链接（仅第一步，其余步骤忽略）：
## https://help.bitwarden.com/article/setup-two-step-login-duo/#create-a-duo-security-account
## 然后根据最后一步获得的值设置以下选项：
# DUO_IKEY=&amp;lt;Client ID&amp;gt;
# DUO_SKEY=&amp;lt;Client Secret&amp;gt;
# DUO_HOST=&amp;lt;API Hostname&amp;gt;
## 之后，您可以继续按照上述链接中的指南操作，
## 忽略那些您已经预先配置的字段。
##
## 如果您想尝试使用 Duo 的 &apos;Traditional Prompt&apos;（已弃用，基于 iframe），将 DUO_USE_IFRAME 设置为 &apos;true&apos;。
## Duo 不再支持此功能，但某些集成仍然有效。
## 如果不确定，请忽略此设置。
# DUO_USE_IFRAME=false

## 邮件 2FA 设置
## 邮件令牌大小
## 邮件 2FA 令牌中的数字位数（最小：6，最大：255）。
## 注意：无论此设置如何，Bitwarden 客户端都硬编码为提及 6 位代码！
# EMAIL_TOKEN_SIZE=6
##
## 令牌过期时间
## 令牌有效的最长时间（秒）。用户打开邮件客户端并复制令牌的时间。
# EMAIL_EXPIRATION_TIME=600
##
## 在重置邮件令牌并需要发送新邮件之前的最大尝试次数。
# EMAIL_ATTEMPTS_LIMIT=3
##
## 无论任何组织策略，设置邮件 2FA
# EMAIL_2FA_ENFORCE_ON_VERIFIED_INVITE=false
## 在需要时自动设置邮件 2FA 作为备用提供程序
# EMAIL_2FA_AUTO_FALLBACK=false

## 其他 MFA/2FA 设置
## 禁用记住 2FA
## 启用此选项将强制用户每次登录时使用第二因素。
## 注意：复选框仍然会显示，但会被忽略。
# DISABLE_2FA_REMEMBER=false
##
## 身份验证器设置
## 禁用身份验证器时间偏移代码的有效性。
## 前 30 秒和后 30 秒的身份验证代码将无效。
##
## 根据 RFC6238（https://tools.ietf.org/html/rfc6238），
## 我们默认允许前一步和下一步的有效 TOTP 代码。
## 但是，这可能会让攻击者更容易成功，因为有 3 个有效的代码。
## 您可以禁用此功能，以便只允许当前的 TOTP 代码。
## 请注意，如果服务器时间偏移，有效的代码可能会被标记为无效。
## 无论如何，一旦代码被使用，它就不能再次使用，并且之前的所有代码也将无效。
# AUTHENTICATOR_DISABLE_TIME_DRIFT=false

###########################
### SMTP 邮件设置 ###
###########################

## 邮件特定设置，设置 SMTP_FROM 并设置 SMTP_HOST 或 USE_SENDMAIL 以启用邮件服务。
## 为了确保电子邮件链接指向正确的主机，请设置 DOMAIN 变量。
## 注意：如果指定了 SMTP_USERNAME，则必须指定 SMTP_PASSWORD。
# SMTP_HOST=smtp.domain.tld
# SMTP_FROM=vaultwarden@domain.tld
# SMTP_FROM_NAME=Vaultwarden
# SMTP_USERNAME=username
# SMTP_PASSWORD=password
# SMTP_TIMEOUT=15

## 选择用于 SMTP 的安全连接类型。默认是 &quot;starttls&quot;。
## 可用选项包括：
## - &quot;starttls&quot;: 默认端口是 587。
## - &quot;force_tls&quot;: 默认端口是 465。
## - &quot;off&quot;: 默认端口是 25。
## 端口 587（提交）和 25（smtp）是标准无加密和通过 STARTTLS 加密的连接。端口 465（提交）用于加密提交（隐式 TLS）。
# SMTP_SECURITY=starttls
# SMTP_PORT=587

# 是否通过 `sendmail` 命令发送邮件
# USE_SENDMAIL=false
# 使用哪个 sendmail 命令。如果没有指定，则使用 $PATH 中找到的命令。
# SENDMAIL_COMMAND=&quot;/path/to/sendmail&quot;

## 默认情况下 SSL 是 &quot;Plain&quot; 和 &quot;Login&quot;，非 SSL 连接不使用任何机制。
## 可能的值：[&quot;Plain&quot;, &quot;Login&quot;, &quot;Xoauth2&quot;]。
## 多个选项需要用逗号 &apos;,&apos; 分隔。
# SMTP_AUTH_MECHANISM=

## 发送 SMTP HELO 时使用的服务器名称
## 默认情况下，该值应该是机器的主机名，
## 但如果触发了一些反垃圾邮件过滤器，则可能需要更改此值。
# HELO_NAME=

## 将图像嵌入为邮件附件
# SMTP_EMBED_IMAGES=true

## SMTP 调试
## 当设置为 true 时，这将输出非常详细的 SMTP 消息。
## 警告：这可能包含敏感信息如密码和用户名！仅在故障排除时启用！
# SMTP_DEBUG=false

## 接受无效证书
## 危险：此选项引入了中间人攻击的重大漏洞！
## 仅在无法使用有效证书时作为最后手段使用。
## 如果证书有效但主机名不匹配，请使用 SMTP_ACCEPT_INVALID_HOSTNAMES。
# SMTP_ACCEPT_INVALID_CERTS=false

## 接受无效主机名
## 危险：此选项引入了中间人攻击的重大漏洞！
## 仅在无法使用有效证书时作为最后手段使用。
# SMTP_ACCEPT_INVALID_HOSTNAMES=false

#######################
### Rocket 设置 ###
#######################

## Rocket 特定设置
## 详情请参见：https://rocket.rs/v0.5/guide/configuration/
# ROCKET_ADDRESS=0.0.0.0
## 默认端口是 8000，除非在 Docker 容器中运行，在这种情况下，默认端口是 80。
# ROCKET_PORT=8000
# ROCKET_TLS={certs=&quot;/path/to/certs.pem&quot;,key=&quot;/path/to/key.pem&quot;}

# vim: syntax=ini

&lt;/code&gt;&lt;/pre&gt;
</content:encoded></item><item><title>Vaultwarden备份数据要求</title><link>https://yanghc.dev/posts/5839238a/</link><guid isPermaLink="true">https://yanghc.dev/posts/5839238a/</guid><pubDate>Fri, 21 Feb 2025 00:00:00 GMT</pubDate><content:encoded>&lt;h2&gt;翻译而来
概述&lt;/h2&gt;
&lt;p&gt;vaultwarden 数据应该定期备份，优先使用自动化过程（例如：cron 作业）。理想情况下，至少应将备份存储在远程位置（例如：云存储或不同计算机）。避免依赖文件系统或虚拟机快照作为备份方法，因为这些是更复杂的操作，更多的东西可能会出错，且在这种情况下恢复可能会很困难或不可能。 在备份中添加额外的加密层通常是一个好主意（尤其是当备份还包括配置数据，如&lt;a href=&quot;https://github.com/dani-garcia/vaultwarden/wiki/Enabling-admin-page&quot;&gt;admin token&lt;/a&gt;时），但如果您对主密码（以及其他用户的密码，如果有）感到自信，可以跳过这个步骤。&lt;/p&gt;
&lt;h2&gt;备份数据&lt;/h2&gt;
&lt;p&gt;默认情况下，vaultwarden 将所有数据存储在一个名为 &lt;code&gt;data&lt;/code&gt; (与 &lt;code&gt;vaultwarden&lt;/code&gt; 可执行文件位于同一目录) 的目录下。可以通过设置 &lt;a href=&quot;https://github.com/dani-garcia/vaultwarden/wiki/Changing-persistent-data-location&quot;&gt;DATA_FOLDER&lt;/a&gt;环境变量来更改此位置。 如果您使用 SQLite（这是最常见的设置），则 SQL 数据库就是数据文件夹中的一个文件。如果您使用 MySQL 或 PostgreSQL，则必须单独导出该数据——这超出了本文的范围，但进行网络搜索会找到许多其他教程，涵盖了这个主题。&lt;/p&gt;
&lt;p&gt;当使用 SQLite 后端时，vaultwarden &lt;code&gt;data&lt;/code&gt; 目录结构如下：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;数据
├── attachments          # 每个附件作为单独的文件存储在此目录下。
│   └── &amp;lt;uuid&amp;gt;           # （如果没有创建过附件，attachments 目录将不存在。）
│       └── &amp;lt;随机_id&amp;gt;
├── config.json          # 存储管理员页面配置；仅在管理员页面曾经启用过时存在。
├── db.sqlite3           # 主SQLite数据库文件。
├── db.sqlite3-shm       # SQLite共享内存文件（并不总是存在）。
├── db.sqlite3-wal       # SQLite预写日志文件（并不总是存在）。
├── icon_cache           # 站点图标（收藏夹图标）在此目录下缓存。
│   ├── &amp;lt;域名&amp;gt;.png
│   ├── example.com.png
│   ├── example.net.png
│   └── example.org.png
├── rsa_key.der          # `rsa_key.*` 文件用于签署身份验证令牌。
├── rsa_key.pem
├── rsa_key.pub.der
└── sends                # 每个Send附件作为单独的文件存储在此目录下。
    └── &amp;lt;uuid&amp;gt;           # （如果没有创建过Send附件，sends 目录将不存在。）
        └── &amp;lt;随机_id&amp;gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;当使用 MySQL 或 PostgreSQL 后端时，目录结构与 SQLite 一致，除了没有 SQLite 文件。您仍然希望备份 &lt;code&gt;data&lt;/code&gt; 目录中的文件，以及 MySQL 或 PostgreSQL 表的备份。&lt;/p&gt;
&lt;p&gt;每个文件集的详细信息将在下一节讨论。&lt;/p&gt;
&lt;h3&gt;SQLite 数据库文件&lt;/h3&gt;
&lt;p&gt;&lt;em&gt;&lt;strong&gt;备份需要。&lt;/strong&gt;&lt;/em&gt;&lt;/p&gt;
&lt;p&gt;SQLite 数据库文件（ &lt;code&gt;db.sqlite3&lt;/code&gt; ）存储了几乎所有重要的 vaultwarden 数据/状态（数据库条目、用户/组织/设备元数据等），主要的例外是附件，它们以单独的文件形式存储在文件系统中。&lt;/p&gt;
&lt;p&gt;您应该在 SQLite CLI（ &lt;code&gt;sqlite3&lt;/code&gt; ）中使用 &lt;code&gt;.backup&lt;/code&gt; 命令来备份数据库文件。这条命令使用在线备份 API，这是 SQLite 文档中推荐的备份可能正在使用中的数据库文件的最佳方法。如果您可以确保数据库在备份时不会被使用，那么您也可以使用其他方法，如 &lt;code&gt;.dump&lt;/code&gt; 命令，或者简单地复制所有 SQLite 数据库文件（包括 &lt;code&gt;-wal&lt;/code&gt; 文件，如果存在）。&lt;/p&gt;
&lt;p&gt;一个基本的备份命令看起来像这样，假设你的数据文件夹是 &lt;code&gt;data&lt;/code&gt; （默认值）：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;sqlite3 data/db.sqlite3 &quot;.backup &apos;/path/to/backups/db-$(date &apos;+%Y%m%d-%H%M&apos;).sqlite3&apos;&quot;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;你也可以使用 VACUUM INTO，它会压缩空余空间，但需要稍微多一些处理时间：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;sqlite3 data/db.sqlite3 &quot;VACUUM INTO &apos;/path/to/backups/db-$(date &apos;+%Y%m%d-%H%M&apos;).sqlite3&apos;&quot;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;假设这个命令在 2021 年 1 月 1 日 12:34pm (本地时间) 运行，这会将您的 SQLite 数据库文件备份到 &lt;code&gt;/path/to/backups/db-20210101-1234.sqlite3&lt;/code&gt; 。&lt;/p&gt;
&lt;p&gt;可以通过 cron 作业定期运行此命令（最好每天至少一次）。如果您正在通过 Docker 运行，请注意 Docker 镜像不包括 &lt;code&gt;sqlite3&lt;/code&gt; 二进制文件或 &lt;code&gt;cron&lt;/code&gt; 守护进程，因此您通常会在 Docker 主机上安装这些并在容器外运行 cron 作业。如果您真的想在容器内运行备份，例如某些原因，您可以在容器启动时安装必要的包，或者创建一个自定义的 Docker 镜像，使用您偏好的 &lt;code&gt;vaultwarden/server:&amp;lt;tag&amp;gt;&lt;/code&gt; 镜像作为父镜像。&lt;/p&gt;
&lt;p&gt;如果你想将备份数据复制到云存储中，rclone 是一个有用的工具，可以与各种云存储系统进行接口。 restic 或 rustic 是其他好的选择，尤其是如果你有较大的附件并且想避免将它们作为每次备份的一部分重新复制。&lt;/p&gt;
&lt;h3&gt;&lt;code&gt;attachments&lt;/code&gt; 目录&lt;/h3&gt;
&lt;p&gt;&lt;em&gt;&lt;strong&gt;备份需要。&lt;/strong&gt;&lt;/em&gt;&lt;/p&gt;
&lt;p&gt;文件附件是唯一一个不存储在数据库表中的重要数据类别，主要是因为它们可以任意大，而 SQL 数据库通常不设计来高效处理大块数据。这一目录不会存在如果从未创建过文件附件。&lt;/p&gt;
&lt;h3&gt;&lt;code&gt;sends&lt;/code&gt; 目录&lt;/h3&gt;
&lt;p&gt;&lt;em&gt;&lt;strong&gt;备份可选。&lt;/strong&gt;&lt;/em&gt;&lt;/p&gt;
&lt;p&gt;像普通文件附件一样，发送文件附件不存储在数据库表中。 (发送文本笔记是存储在数据库中的，然而。)&lt;/p&gt;
&lt;p&gt;与普通附件不同，Send 附件是临时的。因此，如果您想最小化备份大小，您可能不想备份这个目录。另一方面，如果您更关心恢复时维持 Send 的正常功能，那么您应该备份这个目录。&lt;/p&gt;
&lt;p&gt;如果从来没有创建过发送附件，则这个目录不会存在。&lt;/p&gt;
&lt;h3&gt;&lt;code&gt;config.json&lt;/code&gt; 目录&lt;/h3&gt;
&lt;p&gt;&lt;em&gt;&lt;strong&gt;建议进行备份。&lt;/strong&gt;&lt;/em&gt;&lt;/p&gt;
&lt;p&gt;如果您使用管理员页面配置您的 Vaultwarden 实例，并且没有通过其他方式备份您的配置，那么您可能希望备份这个文件，以免再次尝试找到您的偏好配置。&lt;/p&gt;
&lt;p&gt;请注意，这个文件包含一些明文数据，这些数据可能被认为是敏感的（管理员令牌、SMTP 凭证等），因此如果你担心有人可能会访问它（例如，当它上传到云存储时），请确保加密这些数据。&lt;/p&gt;
&lt;h3&gt;&lt;code&gt;rsa_key*&lt;/code&gt; 文件&lt;/h3&gt;
&lt;p&gt;&lt;em&gt;&lt;strong&gt;建议进行备份。&lt;/strong&gt;&lt;/em&gt;&lt;/p&gt;
&lt;p&gt;这些文件用于签署当前登录用户的 JWTs（认证令牌）。删除它们将简单地将每个用户登出，迫使他们再次登录，并且还将使任何通过邮件发送的开放邀请令牌失效。&lt;/p&gt;
&lt;p&gt;该 &lt;code&gt;rsa_key.pem&lt;/code&gt; （私钥）文件可能被认为是相当敏感的。在原则上，它可以用来伪造服务器的安全库登录会话，但在实践中，这将需要额外的 UUID（例如，从数据库副本中获取的）知识。使用伪造的会话获得的任何数据仍将使用个人和/或组织密钥加密，因此要获得这些密钥所需的相关主密码的暴力破解仍然需要。然而，管理员面板登录会话可以轻松伪造（这只会在管理员面板启用时起作用）。这不会提供对安全库数据的访问，但它会允许一些管理操作，如删除用户或移除 2FA。&lt;/p&gt;
&lt;p&gt;如果您担心有人可能会访问它（例如，上传到云存储时），那么建议加密私钥。&lt;/p&gt;
&lt;h3&gt;&lt;code&gt;icon_cache&lt;/code&gt; 目录&lt;/h3&gt;
&lt;p&gt;&lt;em&gt;&lt;strong&gt;备份可选。&lt;/strong&gt;&lt;/em&gt;&lt;/p&gt;
&lt;p&gt;图标缓存存储网站图标，以便它们不需要从登录网站反复获取。它可能不值得备份，除非你真的想避免重新获取大量图标。&lt;/p&gt;
&lt;h2&gt;恢复备份数据&lt;/h2&gt;
&lt;p&gt;确保 Vaultwarden 停止后，简单地将 &lt;code&gt;data&lt;/code&gt; 目录中的每个文件或目录替换为其备份版本。&lt;/p&gt;
&lt;p&gt;当恢复使用 &lt;code&gt;.backup&lt;/code&gt; 或 &lt;code&gt;VACUUM INTO&lt;/code&gt; 创建的备份时，请先删除任何现有的 &lt;code&gt;db.sqlite3-wal&lt;/code&gt; 文件，因为 SQLite 尝试使用过时/不匹配的 WAL 文件恢复 &lt;code&gt;db.sqlite3&lt;/code&gt; 时可能会导致数据库损坏。然而，如果您使用 &lt;code&gt;db.sqlite3&lt;/code&gt; 和其匹配的 &lt;code&gt;db.sqlite3-wal&lt;/code&gt; 文件的直接拷贝备份了数据库，那么您必须以配对的形式恢复两个文件。您不需要备份或恢复 &lt;code&gt;db.sqlite3-shm&lt;/code&gt; 文件。&lt;/p&gt;
&lt;p&gt;定期运行从备份恢复的过程，仅仅是为了验证你的备份是否正常工作。当进行此操作时，请确保将你的原始数据复制一份或移动一份，以防你的备份实际上没有正常工作。&lt;/p&gt;
&lt;h2&gt;示例&lt;/h2&gt;
&lt;p&gt;这个部分包含第三方备份示例的索引。您应该仔细查看一个示例并了解它在做什么，然后再使用它。&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;a href=&quot;https://github.com/ttionya/vaultwarden-backup&quot;&gt;https://github.com/ttionya/vaultwarden-backup&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://github.com/shivpatel/bitwarden_rs-local-backup&quot;&gt;https://github.com/shivpatel/bitwarden_rs-local-backup&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://github.com/shivpatel/bitwarden_rs_dropbox_backup&quot;&gt;https://github.com/shivpatel/bitwarden_rs_dropbox_backup&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://gitlab.com/1O/vaultwarden-backup&quot;&gt;https://gitlab.com/1O/vaultwarden-backup&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://github.com/jjlin/vaultwarden-backup&quot;&gt;https://github.com/jjlin/vaultwarden-backup&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://github.com/jmqm/vaultwarden_backup&quot;&gt;https://github.com/jmqm/vaultwarden_backup&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;阿里云docker镜像加速&lt;/h2&gt;
&lt;p&gt;阿里云镜像加速有问题，请换其它加速服务&lt;/p&gt;
&lt;p&gt;&lt;a href=&quot;https://hostloc.com/thread-1356392-1-1.html&quot;&gt;【求助】Vaultwarden数据恢复问题-美国VPS综合讨论-全球主机交流论坛 - Powered by Discuz!&lt;/a&gt;&lt;/p&gt;
</content:encoded></item><item><title>Vaultwarden备份脚本</title><link>https://yanghc.dev/posts/4d6390f3/</link><guid isPermaLink="true">https://yanghc.dev/posts/4d6390f3/</guid><pubDate>Thu, 20 Feb 2025 00:00:00 GMT</pubDate><content:encoded>&lt;h1&gt;打包备份版本&lt;/h1&gt;
&lt;pre&gt;&lt;code&gt;#!/bin/bash

# 设置备份存储目录（请根据需要修改路径）
BACKUP_DIR=&quot;./rclone_backup&quot;

# 当前日期
DATE=$(date +%F-%H%M%S)

# Vaultwarden 数据目录
VW_DATA_DIR=&quot;./vw_data&quot;

# 备份文件名
BACKUP_FILE=&quot;$BACKUP_DIR/vw-backup-$DATE.tar.gz&quot;

#定义远程数组
REMOTE_DIRS=(&quot;jianguoyun:/Vaultwarden备份/&quot; &quot;onedrive:/Vaultwarden备份/&quot;)

# 日志文件
LOG_FILE=&quot;./rclone-days.log&quot;

# 创建备份，并排除 vaultwarden.log 文件和 icon_cache，sends 目录
tar --exclude=&apos;vaultwarden.log&apos; --exclude=&apos;icon_cache&apos; --exclude=&apos;sends&apos; -czvf $BACKUP_FILE $VW_DATA_DIR

# 保留最近 7 天的备份，删除旧备份
find $BACKUP_DIR -type f -name &quot;vw-backup-*.tar.gz&quot; -mtime +7 -exec rm {} \;

for remote_dir in &quot;${REMOTE_DIRS[@]}&quot;
do
	# 备份文件
	rclone -v copy &quot;$BACKUP_FILE&quot; &quot;$remote_dir&quot; -P --log-file=$LOG_FILE
done

echo &quot;Vaultwarden 数据备份已完成: $BACKUP_FILE&quot;

&lt;/code&gt;&lt;/pre&gt;
&lt;h1&gt;每日备份（不打包）&lt;/h1&gt;
&lt;pre&gt;&lt;code&gt;#!/bin/bash

#定义数据库匹配
SOURCE_P_SQL=&quot;db.sqlite3*&quot;
#定义rsa_key* 匹配
SOURCE_P_KEY=&quot;rsa_key*&quot;
#定义配置文件名
SOURCE_CONFIG=&quot;config.json&quot;
#定义数据目录
SOURCE_DATA_DIR=&quot;./vw_data/&quot;
#定义日志存放目录
LOGS_DIR=&quot;./rclone_logs&quot;
#定义数据目录内需要备份的子目录数组
SOURCE_BACKUP_DIRS=(&quot;attachments&quot; &quot;sends&quot;)
#定义远程数组
REMOTE_DIRS=(&quot;jianguoyun:/Vaultwarden备份t/&quot; &quot;onedrive:/Vaultwarden备份t/&quot;)


for remote_dir in &quot;${REMOTE_DIRS[@]}&quot;
do
    # 日志文件
	LOG_FILE=&quot;./rclone-days.log&quot;

	# 使用 --include 备份 db.sqlite3* 文件
	rclone -v copy &quot;$SOURCE_DATA_DIR&quot; &quot;$remote_dir&quot; --include &quot;$SOURCE_P_SQL&quot; -P --log-file=$LOG_FILE

	# 使用 --include 备份 rsa_key* 文件
	rclone -v copy &quot;$SOURCE_DATA_DIR&quot; &quot;$remote_dir&quot; --include &quot;$SOURCE_P_KEY&quot; -P --log-file=$LOG_FILE

	# 备份 config.json 文件
	rclone -v copy &quot;$SOURCE_DATA_DIR$SOURCE_CONFIG&quot; &quot;$remote_dir&quot; -P --log-file=$LOG_FILE

	# 备份目录文件
	for dir in &quot;${SOURCE_BACKUP_DIRS[@]}&quot;
	do
		rclone -v sync &quot;$SOURCE_DATA_DIR$dir&quot; &quot;$remote_dir$dir&quot; -P --log-file=$LOG_FILE
	done
done

&lt;/code&gt;&lt;/pre&gt;
</content:encoded></item><item><title>hexo评论系统</title><link>https://yanghc.dev/posts/342fb4b/</link><guid isPermaLink="true">https://yanghc.dev/posts/342fb4b/</guid><pubDate>Sat, 01 Feb 2025 00:00:00 GMT</pubDate><content:encoded>&lt;pre&gt;&lt;code&gt;&amp;lt;script src=&quot;https://giscus.app/client.js&quot;
        data-repo=&quot;xuanyue1024/xxxxx&quot;
        data-repo-id=&quot;xxxxxx&quot;
        data-category=&quot;Announcements&quot;
        data-category-id=&quot;xxxxx&quot;
        data-mapping=&quot;url&quot;
        data-strict=&quot;0&quot;
        data-reactions-enabled=&quot;1&quot;
        data-emit-metadata=&quot;0&quot;
        data-input-position=&quot;bottom&quot;
        data-theme=&quot;preferred_color_scheme&quot;
        data-lang=&quot;zh-CN&quot;

        crossorigin=&quot;anonymous&quot;
        async&amp;gt;
&amp;lt;/script&amp;gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;a href=&quot;https://luhuadong.com/hexo/hexo-comments-giscus&quot;&gt;Hexo 评论系统（Giscus） | 阿基米东 Freedom Blog&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;&lt;a href=&quot;https://blog.yuhang.run/hexo/2025-02-24%2020:05:37/&quot;&gt;Hexo - 为volantis主题启用评论系统 | 逆流而上&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;&lt;a href=&quot;https://blog.bxzdyg.cn/p/%E4%BD%BF%E7%94%A8Hexo%E5%92%8CStellar%E6%90%AD%E5%BB%BA%E4%B8%AA%E4%BA%BA%E5%8D%9A%E5%AE%A2%E7%BD%91%E7%AB%99/#%E6%96%87%E7%AB%A0%E8%B7%AF%E7%94%B1%EF%BC%88%E9%80%89%EF%BC%89&quot;&gt;使用Hexo和Stellar搭建个人博客网站【超详细贴心保姆级教程💖】 - BoBoBlog&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;&lt;a href=&quot;https://felicxfoster.github.io/4199909915.html&quot;&gt;Hexo Stellar 主题装修笔记 - YOLOのBLOG&lt;/a&gt;&lt;/p&gt;
</content:encoded></item><item><title>hexo搭建参考</title><link>https://yanghc.dev/posts/a068d060/</link><guid isPermaLink="true">https://yanghc.dev/posts/a068d060/</guid><pubDate>Fri, 31 Jan 2025 00:00:00 GMT</pubDate><content:encoded>&lt;p&gt;【【零成本】Hexo个人博客搭建教程 | 无需服务器-哔哩哔哩】 https://b23.tv/GVuJ7EB&lt;/p&gt;
&lt;p&gt;&lt;a href=&quot;https://blog.fiveth.cc/p/bb32.html&quot;&gt;404 Page not found&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;&lt;a href=&quot;https://docs.anheyu.com/page/fcircle.html&quot;&gt;朋友圈页面配置 | 安知鱼主题官方文档&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;&lt;a href=&quot;https://zhuanlan.zhihu.com/p/554333805&quot;&gt;Obsidian+Git完美维护Hexo博客 - 知乎&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;&lt;a href=&quot;https://hexo.io/zh-cn/docs/github-pages&quot;&gt;在 GitHub Pages 上部署 Hexo | Hexo&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;&lt;a href=&quot;https://blog.skk.moe/post/deploy-blog-to-cf-workers-site/&quot;&gt;# 将 Hexo 部署到 Cloudflare Workers Site 上的趟坑记录&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;使用hexo generate生成静态文件后，可以观察到图片已经拷贝到了对应的目录下。但是现在浏览的时候还不能够看到图片，因为我们插入的时候用的是包含了一个与Markdown文件同名文件夹的相对路径，而生成的静态文件夹下没有那个同名文件夹所以造成了访问404。&lt;/p&gt;
&lt;p&gt;我们可以使用hexo-simple-image插件来解决这个问题&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;npm install hexo-simple-image --save
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;或者
使用github:7ym0n/hexo-asset-image&lt;/p&gt;
&lt;h2&gt;主题推荐&lt;/h2&gt;
&lt;p&gt;&lt;a href=&quot;https://hexo.fluid-dev.com/docs/&quot;&gt;Hexo Fluid 用户手册&lt;/a&gt;
&lt;a href=&quot;https://rpniu.github.io/2025/03/26/hexo-theme-fluid%E7%9A%84%E4%BD%BF%E7%94%A8/?&quot;&gt;hexo-theme-fluid的gittalk使用 - rPniu&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;&lt;a href=&quot;https://felicxfoster.github.io/4199909915.html#%E6%B7%BB%E5%8A%A0%E6%8D%90%E8%B5%A0%E5%9B%BE%E6%A0%87&quot;&gt;Hexo Stellar 主题装修笔记 - YOLOのBLOG&lt;/a&gt;
&lt;a href=&quot;https://xaoxuu.com/wiki/stellar/&quot;&gt;Stellar：开始您全新的博客之旅 - XAOXUU&lt;/a&gt;
&lt;a href=&quot;https://blog.bxzdyg.cn/p/stellar-dark-mode/&quot;&gt;stellar主题深色模式设置 - BoBoBlog&lt;/a&gt;
&lt;a href=&quot;https://xaoxuu.com/wiki/stellar/widgets/&quot;&gt;Stellar：侧边栏组件的配置与使用（9个） - XAOXUU&lt;/a&gt;
&lt;a href=&quot;https://www.bianliangrensheng.cn/blog/github-actions-auto-vercel&quot;&gt;使用 GitHub Actions 自动部署 Vercel 教程 | 变量人生&lt;/a&gt;
&lt;a href=&quot;https://www.mysticstars.cn/archives/speed-vercel&quot;&gt;Vercel 加速，快，不止更快 | Mystic Stars&lt;/a&gt;&lt;/p&gt;
</content:encoded></item><item><title>我的ob插件</title><link>https://yanghc.dev/posts/b222956a/</link><guid isPermaLink="true">https://yanghc.dev/posts/b222956a/</guid><pubDate>Thu, 30 Jan 2025 00:00:00 GMT</pubDate><content:encoded>&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;序号&lt;/th&gt;
&lt;th&gt;插件名称&lt;/th&gt;
&lt;th&gt;介绍&lt;/th&gt;
&lt;th&gt;其它&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;1&lt;/td&gt;
&lt;td&gt;Trash Explorer&lt;/td&gt;
&lt;td&gt;查看回收站插件&lt;/td&gt;
&lt;td&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;2&lt;/td&gt;
&lt;td&gt;Style Settings&lt;/td&gt;
&lt;td&gt;对Obsidian样式微调&lt;/td&gt;
&lt;td&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;3&lt;/td&gt;
&lt;td&gt;Remotely Save&lt;/td&gt;
&lt;td&gt;保存笔记到云端&lt;/td&gt;
&lt;td&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;4&lt;/td&gt;
&lt;td&gt;Mousewheel Image zoom&lt;/td&gt;
&lt;td&gt;鼠标调整图像大小&lt;/td&gt;
&lt;td&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;5&lt;/td&gt;
&lt;td&gt;Media Extended&lt;/td&gt;
&lt;td&gt;B站或其它视频笔记&lt;/td&gt;
&lt;td&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;6&lt;/td&gt;
&lt;td&gt;Image Magician&lt;/td&gt;
&lt;td&gt;主要用来支持img标签&lt;/td&gt;
&lt;td&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;7&lt;/td&gt;
&lt;td&gt;Iconize&lt;/td&gt;
&lt;td&gt;给文件或文件夹设置表情&lt;/td&gt;
&lt;td&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;8&lt;/td&gt;
&lt;td&gt;Git&lt;/td&gt;
&lt;td&gt;版本管理&lt;/td&gt;
&lt;td&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;9&lt;/td&gt;
&lt;td&gt;Fast Image Cleaner&lt;/td&gt;
&lt;td&gt;图片右键删除，包括源文件&lt;/td&gt;
&lt;td&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;10&lt;/td&gt;
&lt;td&gt;Enhancing Mindmap&lt;/td&gt;
&lt;td&gt;制作编辑思维导图&lt;/td&gt;
&lt;td&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;11&lt;/td&gt;
&lt;td&gt;Editing Toolbar&lt;/td&gt;
&lt;td&gt;MD编辑工具条&lt;/td&gt;
&lt;td&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;12&lt;/td&gt;
&lt;td&gt;Contribution Graph&lt;/td&gt;
&lt;td&gt;贡献图展示&lt;/td&gt;
&lt;td&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;13&lt;/td&gt;
&lt;td&gt;Commander&lt;/td&gt;
&lt;td&gt;添加自定义功能按钮&lt;/td&gt;
&lt;td&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;14&lt;/td&gt;
&lt;td&gt;Calendar&lt;/td&gt;
&lt;td&gt;简单使用的阅历和任务管理方式&lt;/td&gt;
&lt;td&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;15&lt;/td&gt;
&lt;td&gt;BRAT&lt;/td&gt;
&lt;td&gt;下载GitHub插件&lt;/td&gt;
&lt;td&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;16&lt;/td&gt;
&lt;td&gt;Auto Card Link&lt;/td&gt;
&lt;td&gt;卡片链接生成&lt;/td&gt;
&lt;td&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;17&lt;/td&gt;
&lt;td&gt;增强编辑&lt;/td&gt;
&lt;td&gt;提升编辑体验的插件&lt;/td&gt;
&lt;td&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;18&lt;/td&gt;
&lt;td&gt;Dataview&lt;/td&gt;
&lt;td&gt;高性能的索引笔记文件，并创建复杂的查询视图，如表格、列表、任务、日历视图&lt;/td&gt;
&lt;td&gt;&lt;a href=&quot;https://pkmer.cn/Pkmer-Docs/10-obsidian/obsidian%E7%A4%BE%E5%8C%BA%E6%8F%92%E4%BB%B6/dataview/dataview/&quot;&gt;Dataview&lt;/a&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;19&lt;/td&gt;
&lt;td&gt;Homepage&lt;/td&gt;
&lt;td&gt;设置主页插件&lt;/td&gt;
&lt;td&gt;暂停&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;20&lt;/td&gt;
&lt;td&gt;Quiet Outline&lt;/td&gt;
&lt;td&gt;大纲增强&lt;/td&gt;
&lt;td&gt;暂停&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;21&lt;/td&gt;
&lt;td&gt;Copilot&lt;/td&gt;
&lt;td&gt;ai插件&lt;/td&gt;
&lt;td&gt;暂停&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;22&lt;/td&gt;
&lt;td&gt;Share to NotionNext&lt;/td&gt;
&lt;td&gt;分享到notion&lt;/td&gt;
&lt;td&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;23&lt;/td&gt;
&lt;td&gt;Image toolkit&lt;/td&gt;
&lt;td&gt;查看图片&lt;/td&gt;
&lt;td&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;24&lt;/td&gt;
&lt;td&gt;Text Generator&lt;/td&gt;
&lt;td&gt;AI生成插件&lt;/td&gt;
&lt;td&gt;&lt;a href=&quot;https://sspai.com/post/86682&quot;&gt;使用Text Generator插件打造你的Obsidian AI - 少数派&lt;/a&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;25&lt;/td&gt;
&lt;td&gt;mermaid-tools&lt;/td&gt;
&lt;td&gt;mermaid示例图表&lt;/td&gt;
&lt;td&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
</content:encoded></item><item><title>搭建一个属于自己的docker镜像加速器</title><link>https://yanghc.dev/posts/4a585e53/</link><guid isPermaLink="true">https://yanghc.dev/posts/4a585e53/</guid><pubDate>Thu, 30 Jan 2025 00:00:00 GMT</pubDate><content:encoded>&lt;p&gt;转载至：
&lt;a href=&quot;https://www.cnblogs.com/pkyit/p/18259923&quot;&gt;搭建一个属于自己的docker镜像加速器 - 彭狗头 - 博客园&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;近期国内的docker镜像加速器已经失效，导致docker镜像拉不下来。&lt;br /&gt;
如图所示，阿里云镜像加速器已经失效了：&lt;br /&gt;
&lt;img src=&quot;https://img2024.cnblogs.com/blog/3470163/202406/3470163-20240623015043761-1311381345.png&quot; alt=&quot;&quot; /&gt;&lt;/p&gt;
&lt;p&gt;（有人可能会问我，为什么不直接自建harbor仓库把镜像包都放在harbor上，其实这也是种方法，但是本人很不喜欢harbor私服仓库的镜像名字一长串的写法，如 192.168.33.234:5000/harbor/neo4j:5.20.0-ubi9这种拉取镜像的方法看着就心烦。还是简简单单的neo4j:5.20.0-ubi9这种写法方便）。下面我们利用nginx的反向代理实现自建docker镜像加速服务器实现这种效果。先看我自己做的效果：&lt;br /&gt;
&lt;img src=&quot;https://img2024.cnblogs.com/blog/3470163/202406/3470163-20240623015204107-1709315836.png&quot; alt=&quot;&quot; /&gt;&lt;/p&gt;
&lt;p&gt;怎么样？成功拉取，和之前的一样的效果。上图的这个地址就是我自建的镜像加速器。&lt;/p&gt;
&lt;h2&gt;一、准备工作&lt;/h2&gt;
&lt;p&gt;首先，你必须有一台带公网IPv4的海外云服务器。我这里推荐你使用香港的云服务器（不是中国大陆的服务器就OK），尽可能离我们近，这样延时低响应快，新加坡韩国的也行吧。我自己使用的是一台阿里云香港轻量级应用服务器20-30块钱一个月，如果你觉得太贵可以和别人合买，大家一起用就是了。&lt;a href=&quot;https://www.aliyun.com/product/swas?scm=20140722.S_product@@%E4%BA%91%E4%BA%A7%E5%93%81@@65233._.ID_product@@%E4%BA%91%E4%BA%A7%E5%93%81@@65233-RL_%E8%BD%BB%E9%87%8F%E5%BA%94%E7%94%A8%E6%9C%8D%E5%8A%A1%E5%99%A8-LOC_llm-OR_ser-V_3-RE_new2-P0_0&amp;amp;source=5176.11533457&amp;amp;userCode=apvtdcf3&quot;&gt;点击这里直接购买阿里云服务器&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://img2024.cnblogs.com/blog/3470163/202406/3470163-20240623015252199-1100143271.png&quot; alt=&quot;&quot; /&gt;&lt;/p&gt;
&lt;p&gt;30M的带宽足够下载docker镜像了。&lt;br /&gt;
然后去买一个域名，就买那种最便宜的域名，大概是几块钱就能买一年的那种。&lt;a href=&quot;https://www.namesilo.com/&quot;&gt;点击这里购买域名&lt;/a&gt;&lt;br /&gt;
也可以去cloudflare购买域名。（反正怎么便宜怎么来）&lt;br /&gt;
&lt;img src=&quot;https://img2024.cnblogs.com/blog/3470163/202406/3470163-20240623015305099-1458965804.png&quot; alt=&quot;&quot; /&gt;&lt;/p&gt;
&lt;h2&gt;二、服务器的准备工作&lt;/h2&gt;
&lt;p&gt;购买的阿里云海外轻量级服务器推荐安装centos系统&lt;br /&gt;
连接服务器，在阿里云安全面板里放行安全组端口，&lt;br /&gt;
&lt;img src=&quot;https://img2024.cnblogs.com/blog/3470163/202406/3470163-20240623015316493-453897966.png&quot; alt=&quot;&quot; /&gt;&lt;/p&gt;
&lt;p&gt;连接服务器，centos系统运行如下命令安装宝塔面板：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;yum install -y wget &amp;amp;&amp;amp; wget -O install.sh https://download.bt.cn/install/install_6.0.sh &amp;amp;&amp;amp; sh install.sh ed8484bec
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;如果你是安装的Ubuntu系统则执行如下命令安装宝塔面板：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;wget -O install.sh https://download.bt.cn/install/install-ubuntu_6.0.sh &amp;amp;&amp;amp; sudo bash install.sh ed8484bec
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;安装完成显示如下：&lt;br /&gt;
&lt;img src=&quot;https://cdn.nlark.com/yuque/0/2024/png/39106204/1718897011242-d9435242-3ba3-4d65-bfe3-532dba235a64.png&quot; alt=&quot;image.png&quot; /&gt;&lt;br /&gt;
&lt;strong&gt;记得放开上图中的端口，80，22，443，8888，8080，8443等端口，最好是直接关闭防火墙。&lt;/strong&gt;&lt;br /&gt;
浏览器中访问外网面板地址：&lt;br /&gt;
第一进入会推荐安装一些常用软件，我们这里就只安装nginx就好了：&lt;br /&gt;
&lt;img src=&quot;https://img2024.cnblogs.com/blog/3470163/202406/3470163-20240623015400015-849741358.png&quot; alt=&quot;&quot; /&gt;&lt;/p&gt;
&lt;p&gt;在等待安装过程中我们先去把公网IPv4和域名绑定好。&lt;/p&gt;
&lt;h2&gt;三、绑定域名&lt;/h2&gt;
&lt;p&gt;在你买的域名下面找到DNS解析选项，添加一条A记录，名称自定义就行，如图我这里填写abc,&lt;br /&gt;
IP地址填你云服务器的公网ipv4地址，然后点击保存。我的是如图所示：&lt;br /&gt;
&lt;img src=&quot;https://img2024.cnblogs.com/blog/3470163/202406/3470163-20240623015418749-549455468.png&quot; alt=&quot;&quot; /&gt;&lt;/p&gt;
&lt;p&gt;那么你服务器对应的域名就是你填写的名称加上你买的域名，例如我上图中红色框里的域名。其他的域名厂商买的域名也是类似的操作。例如namesilo家的域名操作页面如下所示&lt;br /&gt;
&lt;img src=&quot;https://img2024.cnblogs.com/blog/3470163/202406/3470163-20240623015430212-476570970.png&quot; alt=&quot;&quot; /&gt;&lt;/p&gt;
&lt;p&gt;保存后我们看看能不能通过ping这个域名得到我们服务器的公网ipv4地址：&lt;br /&gt;
&lt;a href=&quot;https://www.itdog.cn/ping/&quot;&gt;https://www.itdog.cn/ping/&lt;/a&gt;&lt;br /&gt;
&lt;img src=&quot;https://cdn.nlark.com/yuque/0/2024/png/39106204/1718898892557-13e74548-0613-4e22-af8a-fe0d659220f3.png&quot; alt=&quot;image.png&quot; /&gt;&lt;br /&gt;
&lt;img src=&quot;https://img2024.cnblogs.com/blog/3470163/202406/3470163-20240623015456156-1284432105.png&quot; alt=&quot;&quot; /&gt;&lt;/p&gt;
&lt;p&gt;等全国各地乃至全世界得到的响应都是绿色通过状态那么我们就可以进行下一步操作了。（这个过程可能需要10几分钟，慢一点可能需要几个小时，我是等了一个多小时才全部变绿色的）&lt;/p&gt;
&lt;h2&gt;四、使用服务器和域名搭建docker镜像加速器&lt;/h2&gt;
&lt;p&gt;回到我们的宝塔面板：&lt;br /&gt;
点击左侧网站按钮，点击添加站点，填入域名，点击确定保存。如下图所示：&lt;br /&gt;
&lt;img src=&quot;https://cdn.nlark.com/yuque/0/2024/png/39106204/1718899258614-4d8cbd3c-d5e0-46ab-b77b-8316b454d59c.png&quot; alt=&quot;image.png&quot; /&gt;&lt;br /&gt;
点击域名站点右侧的未部署按钮，选择Let&apos;s Encrypt 选中域名并申请证书。稍等片刻，证书申请成功，如果失败可以多试几次。&lt;br /&gt;
&lt;img src=&quot;https://img2024.cnblogs.com/blog/3470163/202406/3470163-20240623015604048-1024076392.png&quot; alt=&quot;&quot; /&gt;&lt;/p&gt;
&lt;p&gt;证书申请成功如图所示。&lt;br /&gt;
&lt;img src=&quot;https://cdn.nlark.com/yuque/0/2024/png/39106204/1718899758642-712ddab8-79bc-4fd2-a810-da3d8a1d7cf7.png&quot; alt=&quot;image.png&quot; /&gt;&lt;br /&gt;
然后我们设置nginx反向代理，&lt;strong&gt;点击配置文件，在原来的配置文件最后面的 } 之前插入如下配置：&lt;/strong&gt;&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;  location / {
                     proxy_pass https://registry-1.docker.io;  
                     proxy_set_header Host registry-1.docker.io;
                     proxy_set_header X-Real-IP $remote_addr;
                     proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
                     proxy_set_header X-Forwarded-Proto $scheme;               
                     proxy_buffering off;
                     proxy_set_header Authorization $http_authorization;
                     proxy_pass_header  Authorization;
                     proxy_intercept_errors on;
                     recursive_error_pages on;
                     error_page 301 302 307 = @handle_redirect;

             }
                          location @handle_redirect {
                     resolver 1.1.1.1;
                     set $saved_redirect_location &apos;$upstream_http_location&apos;;
                     proxy_pass $saved_redirect_location;
             }
    
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;如图所示：&lt;br /&gt;
&lt;img src=&quot;https://img2024.cnblogs.com/blog/3470163/202406/3470163-20240623015626545-912582124.png&quot; alt=&quot;&quot; /&gt;&lt;/p&gt;
&lt;p&gt;这一步其实就是设置nginx的默认配置文件，确认以上无误，就可以点击保存，至此完成了nginx反代docker hub的配置。&lt;/p&gt;
&lt;h2&gt;五、测试并验证&lt;/h2&gt;
&lt;p&gt;如果你在浏览器里访问你的域名，会是404页面，这是正常的：&lt;br /&gt;
如果访问不通，看看是不是80和443端口没开放。&lt;br /&gt;
&lt;img src=&quot;https://img2024.cnblogs.com/blog/3470163/202406/3470163-20240623015639869-1110979694.png&quot; alt=&quot;&quot; /&gt;&lt;br /&gt;
我们在docker里验证一下是否能拉取镜像了，修改docker的配置文件&lt;br /&gt;
vim /etc/docker/daemon.json&lt;br /&gt;
把镜像改为你的域名：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;{
  &quot;registry-mirrors&quot;: [&quot;https://你的域名地址&quot;],
  &quot;exec-opts&quot;: [&quot;native.cgroupdriver=systemd&quot;],
  &quot;log-driver&quot;: &quot;json-file&quot;,
  &quot;log-opts&quot;: {
    &quot;max-size&quot;: &quot;128m&quot;
  },
  &quot;storage-driver&quot;: &quot;overlay2&quot;
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;保存，然后重启docker验证是否有效：&lt;br /&gt;
systemctl daemon-reload&lt;br /&gt;
systemctl restart docker&lt;br /&gt;
尝试拉取一个dockerhub上的redis镜像，速度非常快。&lt;br /&gt;
&lt;img src=&quot;https://img2024.cnblogs.com/blog/3470163/202406/3470163-20240623015659064-504155040.png&quot; alt=&quot;&quot; /&gt;&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://cdn.nlark.com/yuque/0/2024/png/39106204/1718900594865-1ef295d3-6819-4d4b-88bf-8e854763eeac.png&quot; alt=&quot;image.png&quot; /&gt;&lt;br /&gt;
&lt;img src=&quot;https://img2024.cnblogs.com/blog/3470163/202406/3470163-20240623015710678-996596683.png&quot; alt=&quot;&quot; /&gt;&lt;/p&gt;
&lt;p&gt;1.反向代理取决于海外服务器的速度，所以如果服务器速度太慢，建议换个服务器，一般来说香港的服务器速度快点。&lt;br /&gt;
2.反向代理也需要消耗服务器的流量，但是一般情况下个人使用不会消耗太多，如果服务器成本太高，可以多个人合租也行。&lt;br /&gt;
3.不要随意把访问地址告诉他人，不然服务器被攻击或者被刷流量，损失的的是自己，并且建议做好防护措施。&lt;br /&gt;
&lt;strong&gt;最后欢迎大家在评论区留言讨论。&lt;/strong&gt;&lt;br /&gt;
我的技术分享博客地址：&lt;a href=&quot;https://blog.csdn.net/pky86676022&quot;&gt;CSDN＠ｐｋｙｉｔ&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;本文转自 &lt;a href=&quot;https://www.cnblogs.com/pkyit/p/18259923&quot;&gt;https://www.cnblogs.com/pkyit/p/18259923&lt;/a&gt;，如有侵权，请联系删除。&lt;/p&gt;
</content:encoded></item><item><title>如何开启Docker的远程访问</title><link>https://yanghc.dev/posts/71cc795/</link><guid isPermaLink="true">https://yanghc.dev/posts/71cc795/</guid><pubDate>Mon, 02 Dec 2024 00:00:00 GMT</pubDate><content:encoded>&lt;blockquote&gt;
&lt;p&gt;本文为转载文章，主要介绍如何开启docker的远程访问并通过idea进行远程操作。&lt;/p&gt;
&lt;p&gt;原文地址：&lt;a href=&quot;https://www.cnblogs.com/niceyoo/p/13270224.html&quot;&gt;Docker开启远程安全访问 - niceyoo - 博客园&lt;/a&gt;&lt;/p&gt;
&lt;/blockquote&gt;
&lt;h3&gt;1、编辑docker.server文件&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;vi /usr/lib/systemd/system/docker.service
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;找到 &lt;strong&gt;[Service]&lt;/strong&gt; 节点，修改 ExecStart 属性，增加 &lt;code&gt;-H tcp://0.0.0.0:2375&lt;/code&gt;&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;ExecStart=/usr/bin/dockerd -H fd:// --containerd=/run/containerd/containerd.sock -H tcp://0.0.0.0:2375
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;img src=&quot;https://i-blog.csdnimg.cn/blog_migrate/3e0e1cea1b854fe9582c51af25394d61.png&quot; alt=&quot;&quot; /&gt;&lt;/p&gt;
&lt;p&gt;这样相当于对外开放的是 &lt;strong&gt;2375&lt;/strong&gt; 端口，当然也可以根据自己情况修改成其他的。&lt;/p&gt;
&lt;h3&gt;2、重新加载Docker配置生效&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;systemctl daemon-reload 
systemctl restart docker 
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;通过浏览器访问 &lt;strong&gt;2375&lt;/strong&gt; 测试一下，格式为：http://ip:2375/version&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://i-blog.csdnimg.cn/blog_migrate/03d5e1a7dcad5edc4cd25a2a618e5427.png&quot; alt=&quot;&quot; /&gt;&lt;/p&gt;
&lt;p&gt;如果无法访问的话，可以尝试一下开放防火墙2375端口，具体命令如下：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;firewall-cmd --zone=public --add-port=2375/tcp --permanent
firewall-cmd --reload
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;如果还是不能访问，如果使用的机器是云服务器，比如阿里云、腾讯云等等，需要到服务器安全组规则中看看是否开放2375端口，如未配置，增加该端口配置即可。&lt;/p&gt;
&lt;p&gt;这样我们就可以直接在Idea中的Docker插件中直接连接测试了：&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://i-blog.csdnimg.cn/blog_migrate/b2641cd33f849029972b303bdadf7b32.png&quot; alt=&quot;&quot; /&gt;&lt;/p&gt;
&lt;h3&gt;3、配置Docker安全访问&lt;/h3&gt;
&lt;p&gt;如上两步切勿用于生产环境！在开发环境用用就行了，如果直接把Docker这样对外暴露是非常危险的，就跟你Redis对外开放6379还不设置密码一样。&lt;/p&gt;
&lt;p&gt;基本网上好多文章都是如上两步，裸奔的步骤... 你品，你细品，不给你挂马给谁挂。&lt;/p&gt;
&lt;p&gt;其实官方文档已经提供基于CA证书的加密方法了，&lt;a href=&quot;https://docs.docker.com/engine/security/https/#create-a-ca-server-and-client-keys-with-openss&quot;&gt;详情点击此处链接&lt;/a&gt;&lt;/p&gt;
&lt;h4&gt;3.1、创建CA私钥和CA公钥&lt;/h4&gt;
&lt;p&gt;首先创建一个ca文件夹用来存放私钥跟公钥&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;mkdir -p /usr/local/ca
cd /usr/local/ca
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;然后在&lt;strong&gt;Docker守护程序的主机上&lt;/strong&gt;，生成CA私钥和公钥：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;openssl genrsa -aes256 -out ca-key.pem 4096
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;执行完如上指令后，会要求我们输入密码（yhc123321)才能进行下一步。&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://i-blog.csdnimg.cn/blog_migrate/7f4f1481118b5cac8bea7a138b96f901.png&quot; alt=&quot;&quot; /&gt;&lt;/p&gt;
&lt;h4&gt;3.2、补全CA证书信息&lt;/h4&gt;
&lt;p&gt;执行如下指令：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;openssl req -new -x509 -days 365 -key ca-key.pem -sha256 -out ca.pem
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;然后依次输入：访问密码、国家、省、市、组织名称、单位名称、随便一个名字、邮箱等。为了省事，组织、单位之类的。&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://i-blog.csdnimg.cn/blog_migrate/69b846641f0284124aed2e7a04029ba7.png&quot; alt=&quot;&quot; /&gt;&lt;/p&gt;
&lt;p&gt;至此，CA证书就创建完成了，有了CA之后，就可以创建服务器密钥和证书签名请求(CSR)了，确保“通用名称”与你连接Docker时使用的主机名相匹配。&lt;/p&gt;
&lt;h4&gt;3.3、生成server-key.pem&lt;/h4&gt;
&lt;pre&gt;&lt;code&gt;openssl genrsa -out server-key.pem 4096
&lt;/code&gt;&lt;/pre&gt;
&lt;h4&gt;3.4、用CA签署公钥&lt;/h4&gt;
&lt;p&gt;由于可以通过IP地址和DNS名称建立TLS连接，因此在创建证书时需要指定IP地址。例如，允许使用&lt;code&gt;10.211.55.4&lt;/code&gt;进行连接：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;openssl req -subj &quot;/CN=10.211.55.4&quot; -sha256 -new -key server-key.pem -out server.csr
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;如果你是用的网址(比如:www.sscai.club)则替换一下即可：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;openssl req -subj &quot;/CN=www.sscai.club&quot; -sha256 -new -key server-key.pem -out server.csr
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;注意：这里指的ip或者是域名，都是指的将来用于对外的地址。&lt;/p&gt;
&lt;h4&gt;3.5、匹配白名单&lt;/h4&gt;
&lt;p&gt;配置白名单的意义在于，允许哪些ip可以远程连接docker。&lt;/p&gt;
&lt;p&gt;配置0.0.0.0，允许所有的ip可以链接（但只允许永久证书的才可以连接成功）&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;echo subjectAltName = DNS:www.yumeng.com,IP:0.0.0.0 &amp;gt;&amp;gt; extfile.cnf
&lt;/code&gt;&lt;/pre&gt;
&lt;h4&gt;3.6、执行命令&lt;/h4&gt;
&lt;p&gt;将Docker守护程序密钥的扩展使用属性设置为仅用于服务器身份验证：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;echo extendedKeyUsage = serverAuth &amp;gt;&amp;gt; extfile.cnf
&lt;/code&gt;&lt;/pre&gt;
&lt;h4&gt;3.7、生成签名整数&lt;/h4&gt;
&lt;pre&gt;&lt;code&gt;openssl x509 -req -days 365 -sha256 -in server.csr -CA ca.pem -CAkey ca-key.pem \  -CAcreateserial -out server-cert.pem -extfile extfile.cnf
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;执行后需要输入上方设置的密码。&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://i-blog.csdnimg.cn/blog_migrate/c281edf6f92b536486e9ae6f660c376e.png&quot; alt=&quot;&quot; /&gt;&lt;/p&gt;
&lt;h4&gt;3.8、生成客户端的key.pem&lt;/h4&gt;
&lt;pre&gt;&lt;code&gt;openssl genrsa -out key.pem 4096 
openssl req -subj &apos;/CN=client&apos; -new -key key.pem -out client.csr
&lt;/code&gt;&lt;/pre&gt;
&lt;h4&gt;3.9、要使秘钥适合客户端身份验证&lt;/h4&gt;
&lt;p&gt;创建扩展配置文件：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;echo extendedKeyUsage = clientAuth &amp;gt;&amp;gt; extfile.cnf
echo extendedKeyUsage = clientAuth &amp;gt; extfile-client.cnf
&lt;/code&gt;&lt;/pre&gt;
&lt;h4&gt;3.10、生成签名整数&lt;/h4&gt;
&lt;pre&gt;&lt;code&gt;openssl x509 -req -days 365 -sha256 -in client.csr -CA ca.pem -CAkey ca-key.pem \  -CAcreateserial -out cert.pem -extfile extfile-client.cnf
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;生成cert.pem，需要再次输入之前设置的密码。&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://i-blog.csdnimg.cn/blog_migrate/59cec7df518cefdf8365b3be96dab913.png&quot; alt=&quot;&quot; /&gt;&lt;/p&gt;
&lt;h4&gt;3.11、删除不需要的文件，两个整数签名请求&lt;/h4&gt;
&lt;p&gt;生成后&lt;code&gt;cert.pem&lt;/code&gt;，&lt;code&gt;server-cert.pem&lt;/code&gt;您可以安全地删除两个证书签名请求和扩展配置文件：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;rm -v client.csr server.csr extfile.cnf extfile-client.cnf
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;img src=&quot;https://i-blog.csdnimg.cn/blog_migrate/0dbe0fe484ebe939138dff1e58a07088.png&quot; alt=&quot;&quot; /&gt;&lt;/p&gt;
&lt;h4&gt;3.12、可修改权限&lt;/h4&gt;
&lt;p&gt;为了保护您的密钥免于意外损坏，请删除其写入权限。要使它们仅供您阅读，请按以下方式更改文件模式：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;chmod -v 0400 ca-key.pem key.pem server-key.pem
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;证书可以使对外可读的，删除写入权限以防止意外损坏：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;chmod -v 0444 ca.pem server-cert.pem cert.pem
&lt;/code&gt;&lt;/pre&gt;
&lt;h4&gt;3. 13、归集服务器证书&lt;/h4&gt;
&lt;pre&gt;&lt;code&gt;cp server-*.pem /etc/docker/
cp ca.pem /etc/docker/
&lt;/code&gt;&lt;/pre&gt;
&lt;h4&gt;3.14、修改Docker配置&lt;/h4&gt;
&lt;p&gt;使Docker守护程序仅接收来自提供CA信任的证书的客户端的链接&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;vim /lib/systemd/system/docker.service
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;将 &lt;code&gt;ExecStart&lt;/code&gt; 属性值进行替换：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;ExecStart=/usr/bin/dockerd --tlsverify --tlscacert=/usr/local/ca/ca.pem --tlscert=/usr/local/ca/server-cert.pem --tlskey=/usr/local/ca/server-key.pem -H tcp://0.0.0.0:2375 -H unix:///var/run/docker.sock
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;img src=&quot;https://i-blog.csdnimg.cn/blog_migrate/38111ec4eaaa9de8f75cd50f0da3e6de.png&quot; alt=&quot;&quot; /&gt;&lt;/p&gt;
&lt;h4&gt;3.15、重新加载daemon并重启docker&lt;/h4&gt;
&lt;pre&gt;&lt;code&gt;systemctl daemon-reload 
systemctl restart docker
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;我们去IDEA中的docker模块验证一下，先看一下之前的连接：&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://i-blog.csdnimg.cn/blog_migrate/f79a992b7086d61357dcfbf26d5ee219.png&quot; alt=&quot;&quot; /&gt;&lt;/p&gt;
&lt;p&gt;显然是无法连接了，此时我们需要去拿到docker宿主机创建的证书，使用证书才可以进行连接：&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://i-blog.csdnimg.cn/blog_migrate/27d2b65bac979fe76ddf2f48aaa5b98c.png&quot; alt=&quot;&quot; /&gt;&lt;/p&gt;
&lt;p&gt;拉取这四个证书文件至本地文件夹，这个文件夹将用于在idea指定，需要说的是，TCP 里的链接需要改成 Https 格式，具体内容如下图所示：&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://i-blog.csdnimg.cn/blog_migrate/dcbc182c57fdb99585e0f9ae82e3d951.png&quot; alt=&quot;&quot; /&gt;&lt;/p&gt;
&lt;hr /&gt;
&lt;p&gt;记录分享程序进阶之路中的点点滴滴。&lt;/p&gt;
&lt;p&gt;本文转自 &lt;a href=&quot;https://blog.csdn.net/sg_knight/article/details/126319965&quot;&gt;https://blog.csdn.net/sg_knight/article/details/126319965&lt;/a&gt;，如有侵权，请联系删除。&lt;/p&gt;
</content:encoded></item><item><title>docker常用命令</title><link>https://yanghc.dev/posts/749ad7d8/</link><guid isPermaLink="true">https://yanghc.dev/posts/749ad7d8/</guid><pubDate>Sun, 26 May 2024 00:00:00 GMT</pubDate><content:encoded>&lt;p&gt;Docker 是一个开源的平台，旨在使应用程序的开发、部署和运行更轻松。Docker 使用容器这一轻量级的虚拟化技术，使应用程序及其所有依赖项都可以打包到一个标准化的单元中。&lt;/p&gt;
&lt;h2&gt;Docker常用命令&lt;/h2&gt;
&lt;h3&gt;Docker 容器相关命令&lt;/h3&gt;
&lt;ol&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;运行容器&lt;/strong&gt;&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;docker run -it --name &amp;lt;容器名&amp;gt; &amp;lt;镜像名&amp;gt;:&amp;lt;标签&amp;gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;例如：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;docker run -it --name mynginx nginx:latest
&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;列出正在运行的容器&lt;/strong&gt;&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;docker ps
&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;列出所有容器（包括已停止的容器）&lt;/strong&gt;&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;docker ps -a
&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;停止容器&lt;/strong&gt;&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;docker stop &amp;lt;容器ID或容器名&amp;gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;启动容器&lt;/strong&gt;&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;docker start &amp;lt;容器ID或容器名&amp;gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;重启容器&lt;/strong&gt;&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;docker restart &amp;lt;容器ID或容器名&amp;gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;删除容器&lt;/strong&gt;&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;docker rm &amp;lt;容器ID或容器名&amp;gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;查看容器日志&lt;/strong&gt;&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;docker logs &amp;lt;容器ID或容器名&amp;gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;进入正在运行的容器&lt;/strong&gt;&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;docker exec -it &amp;lt;容器ID或容器名&amp;gt; /bin/bash
&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;/ol&gt;
&lt;h3&gt;Docker 镜像相关命令&lt;/h3&gt;
&lt;ol&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;搜索镜像&lt;/strong&gt;&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;docker search &amp;lt;镜像名&amp;gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;例如：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;docker search nginx
&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;拉取镜像&lt;/strong&gt;&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;docker pull &amp;lt;镜像名&amp;gt;:&amp;lt;标签&amp;gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;例如：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;docker pull nginx:latest
&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;列出本地镜像&lt;/strong&gt;&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;docker images
&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;删除镜像&lt;/strong&gt;&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;docker rmi &amp;lt;镜像ID或镜像名&amp;gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;构建镜像&lt;/strong&gt;&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;docker build -t &amp;lt;镜像名&amp;gt;:&amp;lt;标签&amp;gt; &amp;lt;Dockerfile路径&amp;gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;例如：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;docker build -t myapp:1.0 .
&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;/ol&gt;
&lt;h3&gt;Docker 网络相关命令&lt;/h3&gt;
&lt;ol&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;列出网络&lt;/strong&gt;&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;docker network ls
&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;创建网络&lt;/strong&gt;&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;docker network create &amp;lt;网络名&amp;gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;删除网络&lt;/strong&gt;&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;docker network rm &amp;lt;网络名&amp;gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;/ol&gt;
&lt;h3&gt;Docker 卷相关命令&lt;/h3&gt;
&lt;ol&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;列出卷&lt;/strong&gt;&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;docker volume ls
&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;创建卷&lt;/strong&gt;&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;docker volume create &amp;lt;卷名&amp;gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;删除卷&lt;/strong&gt;&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;docker volume rm &amp;lt;卷名&amp;gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;/ol&gt;
&lt;h2&gt;开机自启&lt;/h2&gt;
&lt;p&gt;要让 Docker 容器在系统启动时自动运行，你可以使用 Docker 提供的 &lt;code&gt;--restart&lt;/code&gt; 选项来配置容器的重启策略。以下是几种常见的重启策略：&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;&lt;strong&gt;no&lt;/strong&gt;: 容器不会自动重启。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;always&lt;/strong&gt;: 无论容器退出状态如何，总是自动重启。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;on-failure&lt;/strong&gt;: 只有在容器以非零退出代码退出时才自动重启。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;unless-stopped&lt;/strong&gt;: 容器将始终重新启动，除非它被手动停止。&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;要设置一个容器在系统启动时自动运行，可以使用 &lt;code&gt;--restart&lt;/code&gt; 选项创建或更新容器。例如：&lt;/p&gt;
&lt;h3&gt;创建新的自动重启容器&lt;/h3&gt;
&lt;p&gt;如果你还没有创建容器，可以在运行 &lt;code&gt;docker run&lt;/code&gt; 命令时添加 &lt;code&gt;--restart&lt;/code&gt; 选项。例如，使用 &lt;code&gt;gotify/server&lt;/code&gt; 镜像：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;docker run -d --name gotify-server --restart always gotify/server
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;更新现有的容器以自动重启&lt;/h3&gt;
&lt;p&gt;如果你已经有一个容器，并希望更新它的重启策略，可以使用 &lt;code&gt;docker update&lt;/code&gt; 命令：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;docker update --restart always gotify-server
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;验证容器的重启策略&lt;/h3&gt;
&lt;p&gt;你可以使用 &lt;code&gt;docker inspect&lt;/code&gt; 命令来验证容器的重启策略：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;docker inspect gotify-server --format=&apos;{{.HostConfig.RestartPolicy}}&apos;
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;示例：启动多个容器并设置自动重启&lt;/h3&gt;
&lt;p&gt;假设你有多个容器需要设置自动重启策略，你可以按如下方式操作：&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;Bitwarden Setup&lt;/strong&gt;&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;docker run -d --name bitwarden-setup --restart always bitwarden/setup:2024.1.2
&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;Qinglong&lt;/strong&gt;&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;docker run -d --name qinglong --restart always whyour/qinglong:latest
&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;Certbot&lt;/strong&gt;&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;docker run -d --name certbot --restart always certbot/certbot:latest
&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;Watchtower&lt;/strong&gt;&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;docker run -d --name watchtower --restart always containrrr/watchtower:latest
&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;/ol&gt;
&lt;h3&gt;管理自动启动的服务&lt;/h3&gt;
&lt;p&gt;除了使用 Docker 自身的重启策略，你还可以使用系统服务管理工具（如 &lt;code&gt;systemd&lt;/code&gt;）来确保 Docker 服务在系统启动时自动启动，并配置自定义的服务单元文件来管理 Docker 容器的启动。以下是一个简单的 &lt;code&gt;systemd&lt;/code&gt; 服务单元文件的示例：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;[Unit]
Description=Gotify Server Container
Requires=docker.service
After=docker.service

[Service]
Restart=always
ExecStart=/usr/bin/docker start -a gotify-server
ExecStop=/usr/bin/docker stop -t 2 gotify-server

[Install]
WantedBy=default.target
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;将上述内容保存为 &lt;code&gt;/etc/systemd/system/gotify-server.service&lt;/code&gt;，然后启用和启动服务：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;sudo systemctl enable gotify-server
sudo systemctl start gotify-server
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;这样配置后，系统启动时将自动启动 &lt;code&gt;gotify-server&lt;/code&gt; 容器。&lt;/p&gt;
</content:encoded></item><item><title>服务器防火墙相关命令</title><link>https://yanghc.dev/posts/9c75190f/</link><guid isPermaLink="true">https://yanghc.dev/posts/9c75190f/</guid><pubDate>Sat, 25 May 2024 00:00:00 GMT</pubDate><content:encoded>&lt;h1&gt;firewalld防火墙相关命令&lt;/h1&gt;
&lt;p&gt;&lt;code&gt;firewalld&lt;/code&gt; 是一个动态管理防火墙的Linux工具，允许用户在不重新启动防火墙的情况下管理规则。它使用了 &lt;code&gt;zones&lt;/code&gt;（区域）的概念，允许根据网络连接类型定义不同的防火墙设置。&lt;/p&gt;
&lt;h3&gt;基本命令&lt;/h3&gt;
&lt;ol&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;启动 &lt;code&gt;firewalld&lt;/code&gt; 服务&lt;/strong&gt;&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;sudo systemctl start firewalld
&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;停止 &lt;code&gt;firewalld&lt;/code&gt; 服务&lt;/strong&gt;&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;sudo systemctl stop firewalld
&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;重启 &lt;code&gt;firewalld&lt;/code&gt; 服务&lt;/strong&gt;&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;sudo systemctl restart firewalld
&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;启用 &lt;code&gt;firewalld&lt;/code&gt; 服务开机自启&lt;/strong&gt;&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;sudo systemctl enable firewalld
&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;禁用 &lt;code&gt;firewalld&lt;/code&gt; 服务开机自启&lt;/strong&gt;&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;sudo systemctl disable firewalld
&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;查看 &lt;code&gt;firewalld&lt;/code&gt; 服务状态&lt;/strong&gt;&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;sudo systemctl status firewalld
&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;/ol&gt;
&lt;h3&gt;配置命令&lt;/h3&gt;
&lt;ol&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;查看所有区域&lt;/strong&gt;&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;sudo firewall-cmd --get-zones
&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;查看活动区域&lt;/strong&gt;&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;sudo firewall-cmd --get-active-zones
&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;查看特定区域的设置&lt;/strong&gt;&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;sudo firewall-cmd --zone=public --list-all
&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;添加服务到区域（立即生效，但不会持久保存）&lt;/strong&gt;&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;sudo firewall-cmd --zone=public --add-service=http
&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;添加服务到区域（持久保存）&lt;/strong&gt;&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;sudo firewall-cmd --zone=public --add-service=http --permanent
&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;删除服务&lt;/strong&gt;&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;sudo firewall-cmd --zone=public --remove-service=http --permanent
&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;重新加载防火墙配置&lt;/strong&gt;&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;sudo firewall-cmd --reload
&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;添加端口（立即生效，但不会持久保存）&lt;/strong&gt;&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;sudo firewall-cmd --zone=public --add-port=8080/tcp
&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;添加端口（持久保存）&lt;/strong&gt;&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;sudo firewall-cmd --zone=public --add-port=8080/tcp --permanent
&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;删除端口&lt;/strong&gt;&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;sudo firewall-cmd --zone=public --remove-port=8080/tcp --permanent
&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;/ol&gt;
&lt;h3&gt;其他命令&lt;/h3&gt;
&lt;ol&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;查看所有已添加的服务&lt;/strong&gt;&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;sudo firewall-cmd --list-services
&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;查看所有已添加的端口&lt;/strong&gt;&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;sudo firewall-cmd --list-ports
&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;禁用特定区域&lt;/strong&gt;&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;sudo firewall-cmd --zone=public --set-target=DROP
&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;允许某个IP地址访问&lt;/strong&gt;&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;sudo firewall-cmd --zone=public --add-source=192.168.1.100
&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;永久允许某个IP地址访问&lt;/strong&gt;&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;sudo firewall-cmd --zone=public --add-source=192.168.1.100 --permanent
&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;/ol&gt;
&lt;h1&gt;ufw防火墙相关命令&lt;/h1&gt;
&lt;p&gt;&lt;code&gt;ufw&lt;/code&gt;（Uncomplicated Firewall）是一个简化管理防火墙规则的工具，特别适用于&lt;code&gt;iptables&lt;/code&gt;。&lt;/p&gt;
&lt;h3&gt;基本命令&lt;/h3&gt;
&lt;ol&gt;
&lt;li&gt;&lt;strong&gt;查看 &lt;code&gt;ufw&lt;/code&gt;设置端口&lt;/strong&gt;&lt;/li&gt;
&lt;/ol&gt;
&lt;pre&gt;&lt;code&gt;sudo ufw status numbered
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;重载配置&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;sudo ufw reload
&lt;/code&gt;&lt;/pre&gt;
&lt;ol&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;启用 &lt;code&gt;ufw&lt;/code&gt; 防火墙&lt;/strong&gt;&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;sudo ufw enable
&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;禁用 &lt;code&gt;ufw&lt;/code&gt; 防火墙&lt;/strong&gt;&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;sudo ufw disable
&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;查看 &lt;code&gt;ufw&lt;/code&gt; 状态&lt;/strong&gt;&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;sudo ufw status
&lt;/code&gt;&lt;/pre&gt;
&lt;ul&gt;
&lt;li&gt;如果需要详细信息：&lt;/li&gt;
&lt;/ul&gt;
&lt;pre&gt;&lt;code&gt;sudo ufw status verbose
&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;重置 &lt;code&gt;ufw&lt;/code&gt; 配置&lt;/strong&gt;&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;sudo ufw reset
&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;/ol&gt;
&lt;h3&gt;配置规则&lt;/h3&gt;
&lt;ol&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;允许服务（例如 HTTP）&lt;/strong&gt;&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;sudo ufw allow http
&lt;/code&gt;&lt;/pre&gt;
&lt;ul&gt;
&lt;li&gt;或者使用端口号：&lt;/li&gt;
&lt;/ul&gt;
&lt;pre&gt;&lt;code&gt;sudo ufw allow 80/tcp
&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;允许服务（例如 HTTPS）&lt;/strong&gt;&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;sudo ufw allow https
&lt;/code&gt;&lt;/pre&gt;
&lt;ul&gt;
&lt;li&gt;或者使用端口号：&lt;/li&gt;
&lt;/ul&gt;
&lt;pre&gt;&lt;code&gt;sudo ufw allow 443/tcp
&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;禁止服务&lt;/strong&gt;&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;sudo ufw deny http
&lt;/code&gt;&lt;/pre&gt;
&lt;ul&gt;
&lt;li&gt;或者使用端口号：&lt;/li&gt;
&lt;/ul&gt;
&lt;pre&gt;&lt;code&gt;sudo ufw deny 80/tcp
&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;删除规则&lt;/strong&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;允许规则：&lt;/li&gt;
&lt;/ul&gt;
&lt;pre&gt;&lt;code&gt;sudo ufw delete allow http
&lt;/code&gt;&lt;/pre&gt;
&lt;ul&gt;
&lt;li&gt;禁止规则：&lt;/li&gt;
&lt;/ul&gt;
&lt;pre&gt;&lt;code&gt;sudo ufw delete deny http
&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;指定IP地址的允许规则&lt;/strong&gt;&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;sudo ufw allow from 192.168.1.100
&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;指定IP地址和端口的允许规则&lt;/strong&gt;&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;sudo ufw allow from 192.168.1.100 to any port 22
&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;允许特定网段的访问&lt;/strong&gt;&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;sudo ufw allow from 192.168.1.0/24 to any port 22
&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;/ol&gt;
&lt;h3&gt;复杂规则&lt;/h3&gt;
&lt;ol&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;限制某端口的访问（防止暴力破解）&lt;/strong&gt;&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;sudo ufw limit ssh
&lt;/code&gt;&lt;/pre&gt;
&lt;ul&gt;
&lt;li&gt;或者使用端口号：&lt;/li&gt;
&lt;/ul&gt;
&lt;pre&gt;&lt;code&gt;sudo ufw limit 22/tcp
&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;拒绝特定IP地址的访问&lt;/strong&gt;&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;sudo ufw deny from 192.168.1.100
&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;允许出站流量&lt;/strong&gt;&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;sudo ufw default allow outgoing
&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;禁止出站流量&lt;/strong&gt;&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;sudo ufw default deny outgoing
&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;允许入站流量&lt;/strong&gt;&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;sudo ufw default allow incoming
&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;禁止入站流量&lt;/strong&gt;&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;sudo ufw default deny incoming
&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;/ol&gt;
&lt;h3&gt;日志和监控&lt;/h3&gt;
&lt;ol&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;启用日志记录&lt;/strong&gt;&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;sudo ufw logging on
&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;禁用日志记录&lt;/strong&gt;&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;sudo ufw logging off
&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;设置日志记录级别&lt;/strong&gt;&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;sudo ufw logging low
&lt;/code&gt;&lt;/pre&gt;
&lt;ul&gt;
&lt;li&gt;可用级别：&lt;code&gt;off&lt;/code&gt;, &lt;code&gt;low&lt;/code&gt;, &lt;code&gt;medium&lt;/code&gt;, &lt;code&gt;high&lt;/code&gt;, &lt;code&gt;full&lt;/code&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ol&gt;
&lt;h3&gt;其他命令&lt;/h3&gt;
&lt;ol&gt;
&lt;li&gt;&lt;strong&gt;检查 &lt;code&gt;ufw&lt;/code&gt; 版本&lt;/strong&gt;&lt;pre&gt;&lt;code&gt;ufw version
&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;/ol&gt;
</content:encoded></item><item><title>Stcp 和 Xtcp区别</title><link>https://yanghc.dev/posts/66c2ef13/</link><guid isPermaLink="true">https://yanghc.dev/posts/66c2ef13/</guid><pubDate>Sat, 25 May 2024 00:00:00 GMT</pubDate><content:encoded>&lt;p&gt;FRP 的 &lt;strong&gt;Stcp&lt;/strong&gt; 和 &lt;strong&gt;Xtcp&lt;/strong&gt; 是两种不同的点对点通信模式，它们的核心目标都是避免将服务直接暴露在公网中，但在实现方式和使用场景上有一些关键区别。以下是它们的详细对比：&lt;/p&gt;
&lt;hr /&gt;
&lt;h3&gt;1. &lt;strong&gt;Stcp（Secure TCP）&lt;/strong&gt;&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;工作原理&lt;/strong&gt;：
&lt;ul&gt;
&lt;li&gt;Stcp 是一种基于 TCP 的加密隧道模式，数据通过 FRP 服务端中转，但不会直接暴露在公网中。&lt;/li&gt;
&lt;li&gt;服务提供者和访问者都需要配置相同的共享密钥（&lt;code&gt;sk&lt;/code&gt;），只有密钥匹配的客户端才能建立连接。&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;优点&lt;/strong&gt;：
&lt;ul&gt;
&lt;li&gt;数据通过 FRP 服务端中转，适合网络环境不支持 P2P 穿透的场景。&lt;/li&gt;
&lt;li&gt;配置简单，适合小范围的点对点通信。&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;缺点&lt;/strong&gt;：
&lt;ul&gt;
&lt;li&gt;数据需要经过 FRP 服务端中转，可能会增加延迟。&lt;/li&gt;
&lt;li&gt;不适合大规模公开访问的场景。&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;适用场景&lt;/strong&gt;：
&lt;ul&gt;
&lt;li&gt;需要安全访问内网服务，且网络环境不支持 P2P 穿透的场景。&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;hr /&gt;
&lt;h3&gt;2. &lt;strong&gt;Xtcp（UDP-based P2P TCP）&lt;/strong&gt;&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;工作原理&lt;/strong&gt;：
&lt;ul&gt;
&lt;li&gt;Xtcp 是一种基于 UDP 的 P2P 穿透模式，尝试在服务提供者和访问者之间建立直接的点对点连接。&lt;/li&gt;
&lt;li&gt;如果 P2P 穿透成功，数据将直接在两台机器之间传输，不再经过 FRP 服务端中转。&lt;/li&gt;
&lt;li&gt;如果 P2P 穿透失败，FRP 会自动回退到服务端中转模式。&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;优点&lt;/strong&gt;：
&lt;ul&gt;
&lt;li&gt;如果 P2P 穿透成功，数据传输延迟更低，带宽利用率更高。&lt;/li&gt;
&lt;li&gt;适合需要高效点对点通信的场景。&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;缺点&lt;/strong&gt;：
&lt;ul&gt;
&lt;li&gt;P2P 穿透依赖于网络环境（如 NAT 类型），在某些网络环境下可能无法成功。&lt;/li&gt;
&lt;li&gt;配置相对复杂，需要确保 UDP 端口可用。&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;适用场景&lt;/strong&gt;：
&lt;ul&gt;
&lt;li&gt;需要高效点对点通信，且网络环境支持 P2P 穿透的场景。&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;hr /&gt;
&lt;h3&gt;3. &lt;strong&gt;Stcp 和 Xtcp 的对比&lt;/strong&gt;&lt;/h3&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;特性&lt;/th&gt;
&lt;th&gt;Stcp&lt;/th&gt;
&lt;th&gt;Xtcp&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;通信方式&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;通过 FRP 服务端中转&lt;/td&gt;
&lt;td&gt;尝试 P2P 穿透，失败时回退到服务端中转&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;协议&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;基于 TCP&lt;/td&gt;
&lt;td&gt;基于 UDP&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;延迟&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;较高（数据经过服务端中转）&lt;/td&gt;
&lt;td&gt;较低（如果 P2P 穿透成功）&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;带宽利用率&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;较低&lt;/td&gt;
&lt;td&gt;较高&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;配置复杂度&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;简单&lt;/td&gt;
&lt;td&gt;较复杂&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;适用网络环境&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;任何网络环境&lt;/td&gt;
&lt;td&gt;需要支持 P2P 穿透的网络环境&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;安全性&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;高（数据加密，密钥验证）&lt;/td&gt;
&lt;td&gt;高（数据加密，密钥验证）&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;适用场景&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;小范围点对点通信&lt;/td&gt;
&lt;td&gt;高效点对点通信&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;hr /&gt;
&lt;h3&gt;4. &lt;strong&gt;如何选择 Stcp 和 Xtcp&lt;/strong&gt;&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;选择 Stcp 的情况&lt;/strong&gt;：
&lt;ul&gt;
&lt;li&gt;网络环境不支持 P2P 穿透（如对称型 NAT）。&lt;/li&gt;
&lt;li&gt;对延迟和带宽要求不高。&lt;/li&gt;
&lt;li&gt;配置简单，适合快速搭建点对点通信。&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;选择 Xtcp 的情况&lt;/strong&gt;：
&lt;ul&gt;
&lt;li&gt;网络环境支持 P2P 穿透（如全锥型 NAT）。&lt;/li&gt;
&lt;li&gt;对延迟和带宽要求较高。&lt;/li&gt;
&lt;li&gt;需要高效的点对点通信。&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;hr /&gt;
</content:encoded></item><item><title>frp内网穿透</title><link>https://yanghc.dev/posts/c067e7e3/</link><guid isPermaLink="true">https://yanghc.dev/posts/c067e7e3/</guid><pubDate>Sat, 25 May 2024 00:00:00 GMT</pubDate><content:encoded>&lt;p&gt;官方文档&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;url: https://gofrp.org/
title: &quot;/zh-cn/&quot;
host: gofrp.org
&lt;/code&gt;&lt;/pre&gt;
&lt;pre&gt;&lt;code&gt;url: https://blog.csdn.net/lihuayong/article/details/128575019
title: &quot;FRP 内网穿透搭建(无域名)_frp搭建-CSDN博客&quot;
description: &quot;文章浏览阅读3k次，点赞5次，收藏18次。frp是一种快速反向代理，可帮助您将NAT或防火墙后面的本地服务器暴露到Internet。到目前为止，它支持TCP和UDP，以及HTTP和HTTPS协议，其中请求可以通过域名转发到内部服务。_frp搭建&quot;
host: blog.csdn.net
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;编译&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;url: https://www.wyr.me/post/737##toc1-3
title: &quot;blog&quot;
description: &quot;Yige Blog 2.0&quot;
host: www.wyr.me
favicon: https://www.wyr.me/favicon.ico
&lt;/code&gt;&lt;/pre&gt;
&lt;pre&gt;&lt;code&gt;url: https://www.cnblogs.com/TianyuSu/p/11961994.html
title: &quot;frp 配置多个 web 项目，无需购买域名 (访问内网可视化界面，jupyter noterbook, visdom, tensorboard) - 佰大于 - 博客园&quot;
description: &quot;通过一台外网服务器访问多台内网服务器。应用场景，调试内网web项目，深度学习展示loss，feature等图形&quot;
host: www.cnblogs.com
favicon: https://assets.cnblogs.com/favicon_v3_2.ico
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;a href=&quot;https://www.ersansi.top/index.php/archives/1194/&quot;&gt;Fetching Data#t6ge&lt;/a&gt;&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;# 此配置文件仅供参考，请勿直接使用此配置运行程序，可能会有各种问题。
# 您的代理名称将更改为 {user}.{proxy}
user = &quot;your_name&quot;
# 对于IPv6的字面地址或主机名，必须用方括号括起来，如 &quot;[::1]:80&quot;, &quot;[ipv6-host]:http&quot; 或 &quot;[ipv6-host%zone]:80&quot;
# 对于单一的 serverAddr 字段，不需要方括号，例如 serverAddr = &quot;::&quot;。
serverAddr = &quot;0.0.0.0&quot;
serverPort = 7000
# STUN服务器，用于帮助穿透NAT。
# natHoleStunServer = &quot;stun.easyvoip.com:3478&quot;
# 决定在首次登录失败时是否退出程序，否则将连续重新登录frps
# 默认为 true
loginFailExit = true
# 日志输出位置，可设置为控制台或实际的日志文件路径，例如 ./frpc.log
log.to = &quot;./frpc.log&quot;
# 日志级别，可设置为 trace, debug, info, warn, error
log.level = &quot;info&quot;
# 日志保留天数
log.maxDays = 3
# 当 log.to 设置为控制台时，是否禁用日志颜色，默认值为 false
log.disablePrintColor = false
# 认证方法
auth.method = &quot;token&quot;
# auth.additionalScopes 指定附加的作用域以包含认证信息。
# 可选值为 HeartBeats, NewWorkConns。
# auth.additionalScopes = [&quot;HeartBeats&quot;, &quot;NewWorkConns&quot;]
# 认证的 token
auth.token = &quot;12345678&quot;
# oidc.clientID 指定用于获取OIDC认证token的客户端ID。
# auth.oidc.clientID = &quot;&quot;
# oidc.clientSecret 指定用于获取OIDC认证token的客户端密钥。
# auth.oidc.clientSecret = &quot;&quot;
# oidc.audience 指定OIDC认证的受众。
# auth.oidc.audience = &quot;&quot;
# oidc.scope 指定在认证方法为 OIDC 时的token权限。默认值为 &quot;&quot;。
# auth.oidc.scope = &quot;&quot;
# oidc.tokenEndpointURL 指定实现OIDC Token端点的URL，用于获取OIDC token。
# auth.oidc.tokenEndpointURL = &quot;&quot;
# oidc.additionalEndpointParams 指定发送给OIDC Token端点的附加参数。
# 例如，如果您想指定“audience”参数，可以设置如下。
# frp将添加 &quot;audience=&amp;lt;value&amp;gt;&quot; &quot;var1=&amp;lt;value&amp;gt;&quot; 到附加参数中。
# auth.oidc.additionalEndpointParams.audience = &quot;https://dev.auth.com/api/v2/&quot;
# auth.oidc.additionalEndpointParams.var1 = &quot;foobar&quot;
# 设置管理控制台地址，用于通过HTTP API控制frpc的行为，例如重新加载
webServer.addr = &quot;127.0.0.1&quot;
webServer.port = 7400
webServer.user = &quot;admin&quot;
webServer.password = &quot;admin&quot;
# 管理控制台的静态资源目录。默认情况下，这些资源与frpc捆绑在一起。
# webServer.assetsDir = &quot;./static&quot;
# 在管理监听器中启用golang pprof处理程序。
webServer.pprofEnable = false
# 指定与服务器连接的最大拨号时间，默认为10秒。
# transport.dialServerTimeout = 10
# dialServerKeepalive 指定frpc与frps之间活跃网络连接的保活探测间隔。如果为负值，则禁用保活探测。
# transport.dialServerKeepalive = 7200
# 提前建立的连接数，默认值为0
transport.poolCount = 5
# 如果使用TCP流多路复用，默认值为 true，必须与frps一致
# transport.tcpMux = true
# 指定TCP多路复用的保活间隔，仅在tcpMux启用时有效。
# transport.tcpMuxKeepaliveInterval = 30
# 连接服务器时使用的通信协议
# 目前支持 tcp, kcp, quic, websocket 和 wss，默认是 tcp
transport.protocol = &quot;tcp&quot;
# 设置客户端绑定的IP地址用于连接服务器，默认为空。
# 仅在 protocol = tcp 或 websocket 时，该值才会被使用。
transport.connectServerLocalIP = &quot;0.0.0.0&quot;
# 如果您想通过HTTP代理、SOCKS5代理或NTLM代理连接frps，可以在这里或在全局环境变量中设置proxyURL
# 仅在 protocol 为 tcp 时有效
# transport.proxyURL = &quot;http://user:passwd@192.168.1.128:8080&quot;
# transport.proxyURL = &quot;socks5://user:passwd@192.168.1.128:1080&quot;
# transport.proxyURL = &quot;ntlm://user:passwd@192.168.1.128:2080&quot;
# QUIC协议选项
# transport.quic.keepalivePeriod = 10
# transport.quic.maxIdleTimeout = 30
# transport.quic.maxIncomingStreams = 100000
# 如果 tls.enable 为 true，frpc 将通过 TLS 连接 frps。
# 自v0.50.0起，默认值已更改为true，TLS默认启用。
transport.tls.enable = true
# transport.tls.certFile = &quot;client.crt&quot;
# transport.tls.keyFile = &quot;client.key&quot;
# transport.tls.trustedCaFile = &quot;ca.crt&quot;
# transport.tls.serverName = &quot;example.com&quot;
# 如果 disableCustomTLSFirstByte 设置为 false，启用 TLS 时，frpc 将使用第一个自定义字节与 frps 建立连接。
# 自v0.50.0起，默认值已更改为true，第一个自定义字节默认禁用。
# transport.tls.disableCustomTLSFirstByte = true
# 心跳配置，不建议修改默认值。
# heartbeatInterval 的默认值为10，heartbeatTimeout 为90。设置负值可禁用心跳。
# transport.heartbeatInterval = 30
# transport.heartbeatTimeout = 90
# 指定 DNS 服务器，frpc 将使用该服务器而非默认的
# dnsServer = &quot;8.8.8.8&quot;
# 要启动的代理名称，默认为空，表示启动所有代理。
# start = [&quot;ssh&quot;, &quot;dns&quot;]
# 指定UDP包大小，单位为字节。如果未设置，默认值为1500。
# 此参数在客户端和服务器之间应保持一致，影响UDP和SUDP代理。
udpPacketSize = 1500
# 客户端的附加元数据。
metadatas.var1 = &quot;abc&quot;
metadatas.var2 = &quot;123&quot;
# 包含其他代理配置文件。
# includes = [&quot;./confd/*.ini&quot;]
[[proxies]]
# &apos;ssh&apos; 是唯一的代理名称
# 如果全局 user 不为空，它将被更改为 {user}.{proxy}，如 &apos;your_name.ssh&apos;
name = &quot;ssh&quot;
type = &quot;tcp&quot;
localIP = &quot;127.0.0.1&quot;
localPort = 22
# 限制此代理的带宽，单位为KB或MB
transport.bandwidthLimit = &quot;1MB&quot;
# 带宽限制的位置，可以是 &apos;client&apos; 或 &apos;server&apos;，默认为 &apos;client&apos;
transport.bandwidthLimitMode = &quot;client&quot;
# 如果为 true，则对该代理的流量进行加密，默认值为 false
transport.useEncryption = false
# 如果为 true，则对流量进行压缩
transport.useCompression = false
# frps监听的远程端口
remotePort = 6001
# frps将对同一组中的代理连接进行负载均衡
loadBalancer.group = &quot;test_group&quot;
# 组应该有相同的组密钥
loadBalancer.groupKey = &quot;123456&quot;
# 启用后端服务的健康检查，目前支持 &apos;tcp&apos; 和 &apos;http&apos;
# frpc 将连接本地服务的端口以检测其健康状态
healthCheck.type = &quot;tcp&quot;
# 健康检查连接超时时间
healthCheck.timeoutSeconds = 3
# 如果连续失败3次，该代理将从frps中删除
healthCheck.maxFailed = 3
# 每10秒进行一次健康检查
healthCheck.intervalSeconds = 10
# 为每个代理添加附加的元数据信息。这些信息将传递给服务器端插件使用。
metadatas.var1 = &quot;abc&quot;
metadatas.var2 = &quot;123&quot;
# 通过注释为代理添加一些额外的信息。这些注释将在frps仪表板上显示。
[proxies.annotations]
key1 = &quot;value1&quot;
&quot;prefix/key2&quot; = &quot;value2&quot;
[[proxies]]
name = &quot;ssh_random&quot;
type = &quot;tcp&quot;
localIP = &quot;192.168.31.100&quot;
localPort = 22
# 如果 remotePort 为 0，frps 将为您分配一个随机端口
remotePort = 0
[[proxies]]
name = &quot;dns&quot;
type = &quot;udp&quot;
localIP = &quot;114.114.114.114&quot;
localPort = 53
remotePort = 6002
# 将您的域名解析到 [serverAddr]，这样您就可以使用 http://web01.yourdomain.com 浏览 web01，使用 http://web02.yourdomain.com 浏览 web02
[[proxies]]
name = &quot;web01&quot;
type = &quot;http&quot;
localIP = &quot;127.0.0.1&quot;
localPort = 80
# http 用户名和密码是 http 协议的安全认证
# 如果未设置，您可以在没有认证的情况下访问这个 customDomains
httpUser = &quot;admin&quot;
httpPassword = &quot;admin&quot;
# 如果 frps 的域名为 frps.com，那么您可以通过 URL http://web01.frps.com 访问 [web01] 代理
subdomain = &quot;web01&quot;
customDomains = [&quot;web01.yourdomain.com&quot;]
# locations 仅适用于 http 类型
locations = [&quot;/&quot;, &quot;/pic&quot;]
# 如果 http basic 认证用户为 abc，则将请求路由到此服务
# routeByHTTPUser = abc
hostHeaderRewrite = &quot;example.com&quot;
requestHeaders.set.x-from-where = &quot;frp&quot;
responseHeaders.set.foo = &quot;bar&quot;
healthCheck.type = &quot;http&quot;
# frpc 将发送 GET http 请求 &apos;/status&apos; 到本地 http 服务
# 当本地 http 服务返回 2xx http 响应代码时，表示服务正常
healthCheck.path = &quot;/status&quot;
healthCheck.intervalSeconds = 10
healthCheck.maxFailed = 3
healthCheck.timeoutSeconds = 3
# 设置健康检查请求头
healthCheck.httpHeaders=[
{ name = &quot;x-from-where&quot;, value = &quot;frp&quot; }
]
[[proxies]]
name = &quot;web02&quot;
type = &quot;https&quot;
localIP = &quot;127.0.0.1&quot;
localPort = 8000
subdomain = &quot;web02&quot;
customDomains = [&quot;web02.yourdomain.com&quot;]
# 如果不为空，frpc 将使用代理协议将连接信息传递到本地服务
# v1 或 v2 或 为空
transport.proxyProtocolVersion = &quot;v2&quot;
[[proxies]]
name = &quot;tcpmuxhttpconnect&quot;
type = &quot;tcpmux&quot;
multiplexer = &quot;httpconnect&quot;
localIP = &quot;127.0.0.1&quot;
localPort = 10701
customDomains = [&quot;tunnel1&quot;]
# routeByHTTPUser = &quot;user1&quot;
[[proxies]]
name = &quot;plugin_unix_domain_socket&quot;
type = &quot;tcp&quot;
remotePort = 6003
# 如果定义了插件，localIP 和 localPort 无效
# 插件将处理从 frps 接收到的连接
[proxies.plugin]
type = &quot;unix_domain_socket&quot;
unixPath = &quot;/var/run/docker.sock&quot;
[[proxies]]
name = &quot;plugin_http_proxy&quot;
type = &quot;tcp&quot;
remotePort = 6004
[proxies.plugin]
type = &quot;http_proxy&quot;
httpUser = &quot;abc&quot;
httpPassword = &quot;abc&quot;
[[proxies]]
name = &quot;plugin_socks5&quot;
type = &quot;tcp&quot;
remotePort = 6005
[proxies.plugin]
type = &quot;socks5&quot;
username = &quot;abc&quot;
password = &quot;abc&quot;
[[proxies]]
name = &quot;plugin_static_file&quot;
type = &quot;tcp&quot;
remotePort = 6006
[proxies.plugin]
type = &quot;static_file&quot;
localPath = &quot;/var/www/blog&quot;
stripPrefix = &quot;static&quot;
httpUser = &quot;abc&quot;
httpPassword = &quot;abc&quot;
[[proxies]]
name = &quot;plugin_https2http&quot;
type = &quot;https&quot;
customDomains = [&quot;test.yourdomain.com&quot;]
[proxies.plugin]
type = &quot;https2http&quot;
localAddr = &quot;127.0.0.1:80&quot;
crtPath = &quot;./server.crt&quot;
keyPath = &quot;./server.key&quot;
hostHeaderRewrite = &quot;127.0.0.1&quot;
requestHeaders.set.x-from-where = &quot;frp&quot;
[[proxies]]
name = &quot;plugin_https2https&quot;
type = &quot;https&quot;
customDomains = [&quot;test.yourdomain.com&quot;]
[proxies.plugin]
type = &quot;https2https&quot;
localAddr = &quot;127.0.0.1:443&quot;
crtPath = &quot;./server.crt&quot;
keyPath = &quot;./server.key&quot;
hostHeaderRewrite = &quot;127.0.0.1&quot;
requestHeaders.set.x-from-where = &quot;frp&quot;
[[proxies]]
name = &quot;plugin_http2https&quot;
type = &quot;http&quot;
customDomains = [&quot;test.yourdomain.com&quot;]
[proxies.plugin]
type = &quot;http2https&quot;
localAddr = &quot;127.0.0.1:443&quot;
hostHeaderRewrite = &quot;127.0.0.1&quot;
requestHeaders.set.x-from-where = &quot;frp&quot;
[[proxies]]
name = &quot;plugin_http2http&quot;
type = &quot;tcp&quot;
remotePort = 6007
[proxies.plugin]
type = &quot;http2http&quot;
localAddr = &quot;127.0.0.1:80&quot;
hostHeaderRewrite = &quot;127.0.0.1&quot;
requestHeaders.set.x-from-where = &quot;frp&quot;
[[proxies]]
name = &quot;plugin_tls2raw&quot;
type = &quot;https&quot;
remotePort = 6008
[proxies.plugin]
type = &quot;tls2raw&quot;
localAddr = &quot;127.0.0.1:80&quot;
crtPath = &quot;./server.crt&quot;
keyPath = &quot;./server.key&quot;
[[proxies]]
name = &quot;secret_tcp&quot;
# 如果类型为 secret tcp，remotePort 无效
# 想要连接本地端口的用户应部署另一个带有 stcp 代理且角色为 visitor 的 frpc
type = &quot;stcp&quot;
# secretKey 用于认证访问者
secretKey = &quot;abcdefg&quot;
localIP = &quot;127.0.0.1&quot;
localPort = 22
# 如果不为空，仅允许指定用户的访问者连接。
# 否则，来自同一用户的访问者可以连接。&apos;*&apos; 表示允许所有用户。
allowUsers = [&quot;*&quot;]
[[proxies]]
name = &quot;p2p_tcp&quot;
type = &quot;xtcp&quot;
secretKey = &quot;abcdefg&quot;
localIP = &quot;127.0.0.1&quot;
localPort = 22
# 如果不为空，仅允许指定用户的访问者连接。
# 否则，来自同一用户的访问者可以连接。&apos;*&apos; 表示允许所有用户。
allowUsers = [&quot;user1&quot;, &quot;user2&quot;]
# frpc 角色为 visitor -&amp;gt; frps -&amp;gt; frpc 角色为 server
[[visitors]]
name = &quot;secret_tcp_visitor&quot;
type = &quot;stcp&quot;
# 您要访问的服务器名称
serverName = &quot;secret_tcp&quot;
secretKey = &quot;abcdefg&quot;
# 连接此地址以访问 stcp 服务器
bindAddr = &quot;127.0.0.1&quot;
# bindPort 可以小于0，表示不绑定端口，仅接收从其他访问者重定向的连接。（SUDP 目前不支持此功能）
bindPort = 9000
[[visitors]]
name = &quot;p2p_tcp_visitor&quot;
type = &quot;xtcp&quot;
# 如果未设置服务器用户，则默认为当前用户
serverUser = &quot;user1&quot;
serverName = &quot;p2p_tcp&quot;
secretKey = &quot;abcdefg&quot;
bindAddr = &quot;127.0.0.1&quot;
# bindPort 可以小于0，表示不绑定端口，仅接收从其他访问者重定向的连接。（SUDP 目前不支持此功能）
bindPort = 9001
# 当需要自动隧道持久性时，将其设置为 true
keepTunnelOpen = false
# 当 keepTunnelOpen 设置为 true 时，每小时打洞的尝试次数
maxRetriesAnHour = 8
minRetryInterval = 90
# fallbackTo = &quot;stcp_visitor&quot;
# fallbackTimeoutMs = 500
 

frps.toml完整配置模板中文注释（0.60.0版本）
# 该配置文件仅供参考，请勿直接使用运行程序，可能会存在各种问题。
# 对于 IPv6 地址或主机名，必须使用方括号括起来，例如 &quot;[::1]:80&quot;、&quot;[ipv6-host]:http&quot; 或 &quot;[ipv6-host%zone]:80&quot;
# 对于单个 &quot;bindAddr&quot; 字段，不需要使用方括号，例如 bindAddr = &quot;::&quot;。
bindAddr = &quot;0.0.0.0&quot;
bindPort = 7000
# UDP 端口，用于 KCP 协议。可以与 &apos;bindPort&apos; 相同。
# 如果未设置，则 frps 中禁用 KCP。
kcpBindPort = 7000
# UDP 端口，用于 QUIC 协议。
# 如果未设置，则 frps 中禁用 QUIC。
# quicBindPort = 7002
# 指定代理监听的地址，默认值与 bindAddr 相同。
# proxyBindAddr = &quot;127.0.0.1&quot;
# QUIC 协议配置选项
# transport.quic.keepalivePeriod = 10
# transport.quic.maxIdleTimeout = 30
# transport.quic.maxIncomingStreams = 100000
# 心跳配置，不建议修改默认值。
# 默认的 heartbeatTimeout 为 90。设置为负值可禁用它。
# transport.heartbeatTimeout = 90
# 每个代理中保留的连接池数量不超过 maxPoolCount。
transport.maxPoolCount = 5
# 是否使用 TCP 流多路复用，默认为 true。
# transport.tcpMux = true
# 指定 TCP 多路复用的保持连接时间间隔。
# 仅在 tcpMux 为 true 时有效。
# transport.tcpMuxKeepaliveInterval = 30
# tcpKeepalive 指定 frpc 和 frps 之间活动网络连接的保持存活探测间隔。
# 如果为负数，则禁用保持存活探测。
# transport.tcpKeepalive = 7200
# transport.tls.force 指定是否仅接受 TLS 加密连接。默认值为 false。
transport.tls.force = false
# transport.tls.certFile = &quot;server.crt&quot;
# transport.tls.keyFile = &quot;server.key&quot;
# transport.tls.trustedCaFile = &quot;ca.crt&quot;
# 如果希望支持虚拟主机，必须设置监听的 HTTP 端口（可选）。
# 注意：HTTP 端口和 HTTPS 端口可以与 bindPort 相同。
vhostHTTPPort = 80
vhostHTTPSPort = 443
# 虚拟主机 HTTP 服务器的响应头超时时间（秒），默认为 60 秒。
# vhostHTTPTimeout = 60
# tcpmuxHTTPConnectPort 指定服务器监听 TCP HTTP CONNECT 请求的端口。
# 如果值为 0，服务器不会在单一端口上多路复用 TCP 请求。否则，它会监听这个值所指定的端口。
# 默认为 0。
# tcpmuxHTTPConnectPort = 1337
# 如果 tcpmuxPassthrough 为 true，frps 不会对流量进行任何更新。
# tcpmuxPassthrough = false
# 配置 Web 服务器以启用 frps 的仪表板。
# 仅在设置了 webServer.port 时，仪表板可用。
webServer.addr = &quot;127.0.0.1&quot;
webServer.port = 7500
webServer.user = &quot;admin&quot;
webServer.password = &quot;admin&quot;
# webServer.tls.certFile = &quot;server.crt&quot;
# webServer.tls.keyFile = &quot;server.key&quot;
# 仪表板静态资源目录（仅用于调试模式）
# webServer.assetsDir = &quot;./static&quot;
# 在仪表板监听器中启用 golang pprof 处理程序。
# 必须首先设置仪表板端口。
webServer.pprofEnable = false
# enablePrometheus 将在 /metrics API 上通过 webServer 导出 prometheus 指标。
enablePrometheus = true
# 日志输出位置，可以是控制台或文件路径，如 &quot;./frps.log&quot;
log.to = &quot;./frps.log&quot;
# 日志级别：trace, debug, info, warn, error
log.level = &quot;info&quot;
log.maxDays = 3
# 当 log.to 为控制台时，禁用日志颜色输出，默认值为 false
log.disablePrintColor = false
# DetailedErrorsToClient 定义是否向 frpc 发送具体错误（包含调试信息）。默认值为 true。
detailedErrorsToClient = true
# auth.method 指定用于验证 frpc 和 frps 的认证方法。
# 如果指定为 &quot;token&quot;，则会在登录消息中读取 token。
# 如果指定为 &quot;oidc&quot;，则会使用 OIDC 设置发出 OIDC 令牌。默认值为 &quot;token&quot;。
auth.method = &quot;token&quot;
# auth.additionalScopes 指定包含认证信息的额外范围。
# 可选值为 HeartBeats, NewWorkConns。
# auth.additionalScopes = [&quot;HeartBeats&quot;, &quot;NewWorkConns&quot;]
# 认证 token
auth.token = &quot;12345678&quot;
# oidc 发行者指定用于验证 OIDC 令牌的发行者。
auth.oidc.issuer = &quot;&quot;
# oidc 受众指定在验证 OIDC 令牌时应包含的受众。
auth.oidc.audience = &quot;&quot;
# oidc skipExpiryCheck 指定是否跳过检查 OIDC 令牌是否过期。
auth.oidc.skipExpiryCheck = false
# oidc skipIssuerCheck 指定是否跳过检查 OIDC 令牌的发行者声明是否与 oidcIssuer 指定的发行者匹配。
auth.oidc.skipIssuerCheck = false
# userConnTimeout 指定工作连接的最大等待时间。
# userConnTimeout = 10
# 仅允许 frpc 绑定指定的端口。默认情况下没有限制。
allowPorts = [
{ start = 2000, end = 3000 },
{ single = 3001 },
{ single = 3003 },
{ start = 4000, end = 50000 }
]
# 每个客户端可使用的最大端口数，默认为 0 表示无限制。
maxPortsPerClient = 0
# 如果 subDomainHost 不为空，则在 frpc 配置文件中使用 http 或 https 类型时，可以设置子域名。
# 当子域名为 test 时，用于路由的主机是 test.frps.com。
subDomainHost = &quot;frps.com&quot;
# HTTP 请求的自定义 404 页面
# custom404Page = &quot;/path/to/404.html&quot;
# 指定 UDP 包大小，单位为字节。如果未设置，默认值为 1500。
# 该参数应在客户端和服务器之间保持一致。
# 它影响 UDP 和 SUDP 代理。
udpPacketSize = 1500
# NAT 穿透策略数据的保留时间。
natholeAnalysisDataReserveHours = 168
# SSH 隧道网关
# 如果要启用此功能，bindPort 参数是必需的，其他为可选。
# 默认情况下，此功能是禁用的。当 bindPort 大于 0 时将启用它。
# sshTunnelGateway.bindPort = 2200
# sshTunnelGateway.privateKeyFile = &quot;/home/frp-user/.ssh/id_rsa&quot;
# sshTunnelGateway.autoGenPrivateKeyPath = &quot;&quot;
# sshTunnelGateway.authorizedKeysFile = &quot;/home/frp-user/.ssh/authorized_keys&quot;
[[httpPlugins]]
name = &quot;user-manager&quot;
addr = &quot;127.0.0.1:9000&quot;
path = &quot;/handler&quot;
ops = [&quot;Login&quot;]
[[httpPlugins]]
name = &quot;port-manager&quot;
addr = &quot;127.0.0.1:9001&quot;
path = &quot;/handler&quot;
ops = [&quot;NewProxy&quot;]
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;服务端frp配置&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;[common]
bind_port = 7500
vhost_http_port = 7501
#subdomain_host = 8.xxx.xx.xx
authentication_method = token
authenticate_new_work_conns = true
token = 123321
 
# 远访问监控面板接口
dashboard_port = 7502
 
# 登录用户名和密码
dashboard_user = xuanyue
dashboard_pwd = xxxxx
 
#dashboard_tls_mode = false
 
#enable_prometheus = true 
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;客户端配置&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;[common]
server_addr = xxxxx
server_port = 7500
authentication_method = token
authenticate_new_work_conns = true
token = 123xxxx

[web]
type = http
local_ip = 192.168.100.1
local_port = 80
custom_domains = xxxxx
#路由·
locations = /
#动态配置请求头，
host_header_rewrite = 192.168.100.1
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;关于动态配置请求头，正常情况下是不需要使用这个参数的。&lt;/p&gt;
&lt;p&gt;举个例子，比如你想通过&lt;code&gt; test.yourdomain.com&lt;/code&gt; 访问 &lt;code&gt;www.baidu.com&lt;/code&gt;。 虽然这个请求被转发到了百度的服务器，但是你发送的 http 请求的 header 中的 host 参数是 test.yourdomain.com，百度的后端服务可能会进行检测，如果 host 不是 &lt;code&gt;www.baidu.com&lt;/code&gt; 就拒绝，返回相应错误。&lt;/p&gt;
&lt;p&gt;这个时候，如果你设置&lt;code&gt; host_header_rewrite = www.baidu.com&lt;/code&gt;，那么 frp 会将这个请求的 host 动态修改为&lt;code&gt; www.baidu.com&lt;/code&gt;，百度的服务器看到的就是正常的请求，才能正常访问。&lt;/p&gt;
&lt;p&gt;如果你后端的 nginx 启用了虚拟主机这样的功能，需要根据 host 做路由，那么你才需要配置这个参数。&lt;/p&gt;
&lt;h2&gt;ubuntu系统配置systemctl&lt;/h2&gt;
&lt;p&gt;&lt;a href=&quot;systemctl%E7%B3%BB%E7%BB%9F%E5%92%8C%E6%9C%8D%E5%8A%A1%E7%AE%A1%E7%90%86%E5%99%A8.md&quot;&gt;systemctl系统和服务管理器&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;服务端配置 systemctl 来控制frps，自启动
vim /etc/systemd/system/frps.service&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;[Unit]
# 服务名称，可自定义
Description = frps内网穿透服务
After = network.target syslog.target
Wants = network.target

[Service]
Type = simple
# 启动frps的命令，需修改为您的frps的安装路径
ExecStart = /root/App/frp/frps -c /root/App/frp/frps.toml

[Install]
WantedBy = multi-user.target
&lt;/code&gt;&lt;/pre&gt;
&lt;pre&gt;&lt;code&gt;# 启动frps
systemctl start frps
# 开机自动启动frps
systemctl enable frps
# 重启frps
systemctl restart frps
# 停止frps
systemctl stop frps
# 查看frps状态
systemctl status frps
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;URL 路由&lt;/h2&gt;
&lt;p&gt;frp 支持根据请求的 URL 路径路由转发到不同的后端服务。&lt;/p&gt;
&lt;p&gt;通过配置文件中的 locations 字段指定一个或多个 proxy 能够匹配的 URL 前缀(目前仅支持最大前缀匹配，之后会考虑正则匹配)。例如指定 &lt;code&gt;locations = &quot;/news&quot;&lt;/code&gt;，则所有 URL 以 &lt;code&gt;/news&lt;/code&gt; 开头的请求都会被转发到这个服务。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;# frpc.toml
[[proxies]]
name = &quot;web01&quot;
type = &quot;http&quot;
localPort = 80
customDomains = [&quot;web.yourdomain.com&quot;]
locations = [&quot;/&quot;]

[[proxies]]
name = &quot;web02&quot;
type = &quot;http&quot;
localPort = 81
customDomains = [&quot;web.yourdomain.com&quot;]
locations = [&quot;/news&quot;, &quot;/about&quot;]
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;按照上述的示例配置后，&lt;code&gt;web.yourdomain.com&lt;/code&gt; 这个域名下所有以 &lt;code&gt;/news&lt;/code&gt; 以及 &lt;code&gt;/about&lt;/code&gt; 作为前缀的 URL 请求都会被转发到 web02，其余的请求会被转发到 web01。&lt;/p&gt;
&lt;p&gt;路由器配置举例&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;[common]
server_addr = 190.xxxx.xxxx.xxx
server_port = 7500
authentication_method = token
authenticate_new_work_conns = true
token = xxx

#log_file = /dev/null
#log_level = info
#log_max_days = 3

[家里路由]
type = http
local_ip = 192.168.123.1
local_port = 80
custom_domains = 190.xxxx.xxxx.xxx
host_header_rewrite = 192.168.123.1

[家里wifi]
type = http
local_ip = 192.168.0.1
local_port = 80
custom_domains = a.b.c

[路由ssh]
type = tcp
local_ip = 192.168.123.1         
local_port = 22
remote_port = 7503

&lt;/code&gt;&lt;/pre&gt;
</content:encoded></item><item><title>498回文日期解题思路及代码</title><link>https://yanghc.dev/posts/37932e1c/</link><guid isPermaLink="true">https://yanghc.dev/posts/37932e1c/</guid><pubDate>Wed, 08 Nov 2023 16:43:37 GMT</pubDate><content:encoded>&lt;h4&gt;题目&lt;/h4&gt;
&lt;p&gt;2020 年春节期间，有一个特殊的日期引起了大家的注意：2020 年 2 月 2 日。因为如果将这个日期按 “yyyymmdd” 的格式写成一个 8 位数是 20200202，恰好是一个&lt;a href=&quot;https://so.csdn.net/so/search?q=%E5%9B%9E%E6%96%87%E6%95%B0&amp;amp;spm=1001.2101.3001.7020&quot;&gt;回文数&lt;/a&gt;。我们称这样的日期是回文日期。&lt;/p&gt;
&lt;p&gt;有人表示 20200202 是 “千年一遇” 的特殊日子。对此小明很不认同，因为不到 2 年之后就是下一个&lt;a href=&quot;https://so.csdn.net/so/search?q=%E5%9B%9E%E6%96%87%E6%97%A5%E6%9C%9F&amp;amp;spm=1001.2101.3001.7020&quot;&gt;回文日期&lt;/a&gt;：20211202 即 2021 年 12 月 2 日。&lt;/p&gt;
&lt;p&gt;也有人表示 20200202 并不仅仅是一个回文日期，还是一个 ABABBABA 型的回文日期。对此小明也不认同，因为大约 100 年后就能遇到下一个 ABABBABA 型的回文日期：21211212 即 2121 年 12 月 12 日。算不上 “千年一遇”，顶多算 “千年两遇”。&lt;/p&gt;
&lt;p&gt;给定一个 8 位数的日期，请你计算该日期之后下一个回文日期和下一个 ABABBABA 型的回文日期各是哪一天。&lt;/p&gt;
&lt;h4&gt;解题&lt;/h4&gt;
&lt;h5&gt;思路&lt;/h5&gt;
&lt;p&gt;利用LocalDate完成，不断让时间加1，同时用将其转成字符串后放入StringBuilder中并反转，比较反转前后的值是否一致
题目要求判断两个时间，定义while循环，当且仅当两个判断都完成后结束循环，输出结果&lt;/p&gt;
&lt;h5&gt;代码&lt;/h5&gt;
&lt;pre&gt;&lt;code&gt;import java.time.LocalDate;
import java.time.format.DateTimeFormatter;
import java.util.Scanner;
public class Main {
    public static void main(String[] args) {
        Scanner sc = new Scanner(System.in);
        //输入一个时间，20200202
        String time = sc.next();
        //定义时间格式
        DateTimeFormatter dtf1=DateTimeFormatter.ofPattern(&quot;yyyyMMdd&quot;);
        //把字符串时间放入LocalDate中
        LocalDate l = LocalDate.parse(time,dtf1);
        //定义转化为字符串的时间对象和初始化反转后的字符串
        String lStr,sbStr = null;
        //定义两个判断，fa1完成普通回文后修改为true，fa2完成ABABBABA形式后修改为true，当且仅当两个都为true时结束循环
        boolean fa1 = false,fa2 = false;
        while (true){
            //天数+1
            l = l.plusDays(1);
            //将时间转化为指定格式的字符串
            lStr = l.format(dtf1);
            //普通回文
            if (!fa1) {
                //将字符串形时间放入StringBuilder中反转并转化为字符串
                sbStr = new StringBuilder(lStr).reverse().toString();
                if (lStr.equals(sbStr)) fa1 = true;
            }
            //ABABBABA形式
            if (!fa2) {
                //截取年份前两个和后两个，判断是否相同（ABAB)
                String a1 = lStr.substring(0, 2);
                String a2 = lStr.substring(2, 4);
                if (a1.equals(a2)) {
                    //截取年份和月日两部分，并判断年份反转后和月日是否一致（ABABBABA）
                    String a12 = lStr.substring(0, 4);
                    String a34 = lStr.substring(4, 8);
                    if (new StringBuilder(a12).reverse().toString().equals(a34)) fa2 = true;
                }
            }
            //两个判断都完成结束循环
            if (fa1&amp;amp;&amp;amp;fa2) break;
        }
        System.out.println(sbStr + &quot;\n&quot; + l.format(dtf1));
    }
}
&lt;/code&gt;&lt;/pre&gt;
</content:encoded></item><item><title>Java 简单代码验证三门问题</title><link>https://yanghc.dev/posts/c2b72720/</link><guid isPermaLink="true">https://yanghc.dev/posts/c2b72720/</guid><pubDate>Sun, 16 Apr 2023 00:36:32 GMT</pubDate><content:encoded>&lt;p&gt;三门问题（Monty Hall problem）是一个有关于博弈论的趣味数学问题。问题名字来自美国的电视游戏节目Let&apos;s Make a Deal的主持人蒙提·霍尔（Monty Hall）。
参赛者会看见三扇关闭了的门，其中一扇的后面有一辆汽车，选中后面有车的那扇门就可以赢得该汽车，另外两扇门后面则各藏有一只山羊。当参赛者选定了其中一扇门，但未去开启它的时候，节目主持人会开启剩下两扇门的其中一扇，露出其中一只山羊。此时，参赛者是否应该换另一扇门才能增加获得汽车的概率？这个问题在数学上的解答是：改变选择会使获奖概率增加。&lt;/p&gt;
&lt;p&gt;玛丽莲·沃斯·莎凡特（Marilyn vos Savant）是美国《读者文摘》杂志专栏作家，她在1990年6月9日发表了一篇题为“Ask Marilyn”的专栏文章，回答了这个问题。她指出，如果参赛者改变选择，他将有2/3的概率获胜；如果坚持原来的选择，则只有1/3的概率获胜。
这个问题引起了很多争议，许多人认为玛丽莲的答案是错误的。据报道，当时有将近1000名博士和数学家写信给玛丽莲质疑这个答案。&lt;/p&gt;
&lt;p&gt;这段代码是通过模拟三门问题的游戏过程,来验证玛丽莲·沃斯·莎凡特提出的换门后获胜概率增加的结论。代码的主要逻辑如下:1. 输入测试次数,用于多次模拟实验,获得较为准确的概率估计。2. 生成随机的胜利门号(1-3)和玩家初始选择的门号(1-3)。3. 如果初始选择的门号与胜利门号不匹配,则换门必胜。此时,增加换门获胜的次数。否则,增加不换门失败的次数。4. 重复步骤2-3,进行多次模拟实验,获得换门获胜的概率。5. 同样进行多次模拟实验,当玩家选择的初始门号匹配胜利门号时,不换门获胜。否则,不换门失败。获得不换门获胜的概率。6. 输出换门与不换门获胜的概率,可以看出换门的概率是2/3,不换门的概率是1/3,验证了玛丽莲的结论。所以,这段代码通过对三门问题的多次模拟实验,比较换门与不换门的最终获胜次数与概率,证明了换门可以显著增加胜算,概率提高到2/3。这是一个很好的代码示例,不仅展示了如何通过模拟实验来验证一个概率问题,也体现了如何清晰地组织代码逻辑,添加注释等,具有一定的学习价值。对概率、统计以及模拟实验感兴趣的读者可以参考这段代码,理解三门问题这一有趣的概率论命题。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;package MontyHallProblem;
import java.util.Random;
import java.util.Scanner;
 
public class MontyHallProblem {
    public static void main(String[] args) {
        Scanner sc = new Scanner(System.in);
        System.out.println(&quot;请输入测试次数：&quot;);
        //输入测试次数
        double testTimes = sc.nextInt();
        System.out.println(&quot;开始计算\n&quot;);
        Random r = new Random();
        //换门输赢测试
        //初始化赢与输的次数
        int Win_ChangeDoor = 0, Lost_InitialDoor = 0;
        //for循环，实现多次的数据测试
        for (int i = 0; i &amp;lt; testTimes; i++) {
            //随机生成赢的门号与猜测的门号(1-3)
            int winNumber = r.nextInt(3) + 1;
            int selectedNumbers = r.nextInt(3) + 1;
            //当赢的值与猜测的值不一样时，换门必定赢，因为换门时排除了一个错误答案,自己猜的又是错的
            if (winNumber != selectedNumbers) {
                //增加赢的次数
                Win_ChangeDoor++;
            } else Lost_InitialDoor++;//增加输的次数
        }
        System.out.println(&quot;换门赢概率\t &quot; + Double.parseDouble(String.format(&quot;%.2f&quot;, (Win_ChangeDoor / testTimes) * 100)) + &quot;%\t 赢了&quot; + Win_ChangeDoor + &quot;次   输了&quot; + Lost_InitialDoor + &quot;次\n&quot;);
 
 
        //不换门输赢测试
        //初始化赢与输的次数
        Win_ChangeDoor = 0;
        Lost_InitialDoor = 0;
        //for循环，实现多次的数据测试
        for (int i2 = 0; i2 &amp;lt; testTimes; i2++) {
            //随机生成赢的门号与猜测的门号(1-3)
            int winNumber = r.nextInt(3) + 1;
            int selectedNumbers = r.nextInt(3) + 1;
            //不换门时很简单，猜对就是赢，猜错就是输
            if (winNumber == selectedNumbers) {
                //增加赢的次数
                Win_ChangeDoor++;
            } else Lost_InitialDoor++;//增加输的次数
        }
        //输出不换门与换门的最终结果
        System.out.println(&quot;不换门赢概率\t &quot; + Double.parseDouble(String.format(&quot;%.2f&quot;, (Win_ChangeDoor / testTimes) * 100)) + &quot;%\t 赢了&quot; + Win_ChangeDoor + &quot;次   输了&quot; + Lost_InitialDoor + &quot;次&quot;);
    }
}
&lt;/code&gt;&lt;/pre&gt;
</content:encoded></item></channel></rss>