<?xml version="1.0" encoding="UTF-8"?><rss version="2.0" xmlns:content="http://purl.org/rss/1.0/modules/content/"><channel><title>Amadeus</title><description>后端 · 工程实践 · 笔记</description><link>https://gilgameshzzz.github.io/</link><language>zh_CN</language><item><title>读两份 Agent 面试宝典：哪些能背，哪些会让你翻车</title><link>https://gilgameshzzz.github.io/posts/agent-interview-review/</link><guid isPermaLink="true">https://gilgameshzzz.github.io/posts/agent-interview-review/</guid><description>对阿里云社区两份 AI Agent 面试宝典的补充与勘误：哪些数字不能背、哪些结论被当成了普适真理、以及面试官真正会追问的那一层。附一份可直接复用的选型决策表。</description><pubDate>Tue, 15 Sep 2026 00:00:00 GMT</pubDate><content:encoded>&lt;p&gt;最近有人给我转了两份阿里云开发者社区的面试宝典，问我值不值得背：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;a href=&quot;https://developer.aliyun.com/article/1739618&quot;&gt;《65 题 AI Agent 全栈开发最新技术面试宝典》&lt;/a&gt;（2026-06）&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://developer.aliyun.com/article/1758787&quot;&gt;《2026 年 9 月 AI Agent 全栈开发技术选型专项面试宝典》&lt;/a&gt;（2026-08）&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;两份都值得读，覆盖面很全，结构也清楚。但如果你把里面的内容&lt;strong&gt;原样背进面试间，有几处会让你翻车&lt;/strong&gt;——不是因为写错了，而是因为它们把「某个场景下的结论」写成了「普适真理」，而面试官恰恰会追问那个边界。&lt;/p&gt;
&lt;p&gt;这篇不复述原文，只做三件事：&lt;strong&gt;补它没展开的、纠它容易误导的、给它没给的判断依据&lt;/strong&gt;。&lt;/p&gt;
&lt;p&gt;&amp;lt;!-- more --&amp;gt;&lt;/p&gt;
&lt;h2&gt;一、先说最危险的：那些不要背的数字&lt;/h2&gt;
&lt;p&gt;两份宝典里有大量形如「GPT-4o 工具调用准确率 92% vs Claude 88%」「成本降 70%，准确率只掉 2%」的对比。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;这类数字不要背。&lt;/strong&gt; 原因有三：&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;它们没有标注评测集。&lt;/strong&gt; 工具调用准确率在 BFCL、ToolBench、和你自己业务的私有集上，排名可以完全不同。脱离评测集谈准确率，等于脱离题目谈分数。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;模型版本在滚动。&lt;/strong&gt; 同一个模型名下的权重每隔几个月就会更新一次，半年前测出的数字今天大概率已经不成立。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;最要命的是,面试官可能真的测过。&lt;/strong&gt; 你说「GPT-4o 是 92%」，对方接一句「我们内部测是 84%，你这个数据哪来的」，这场面试基本就结束了。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;该怎么答&lt;/strong&gt;：给方法，不给数字。&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;「这个得看评测集。我们的做法是从生产日志里抽 200 条真实工具调用请求做成私有评测集，每次换模型跑一遍，看&lt;strong&gt;参数填充准确率&lt;/strong&gt;和&lt;strong&gt;该调不调的漏调率&lt;/strong&gt;这两个指标。公开榜单只用来筛初选名单，不作为决策依据。」&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;这个答案比任何数字都得分——它证明你真的干过。&lt;/p&gt;
&lt;h2&gt;二、「SSE 是标准答案」：对，但边界在哪&lt;/h2&gt;
&lt;p&gt;第二份宝典说「90% 的 AI 应用选 SSE」，这个判断我同意。但它给的理由不完整，而面试官会追问的恰恰是缺的那部分。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;SSE 真正的优势不是「无需握手」&lt;/strong&gt;，那点延迟差异可以忽略。真正的优势是：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;自动重连&lt;/strong&gt;：EventSource 原生带重连和 &lt;code&gt;Last-Event-ID&lt;/code&gt;，WebSocket 得自己写心跳和退避重连&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;能过大部分企业代理&lt;/strong&gt;：它就是普通 HTTP 响应，而 WebSocket 的 Upgrade 握手经常被老旧网关拦掉&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;无状态，好水平扩展&lt;/strong&gt;：不需要在网关层维护长连接的会话亲和性&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;strong&gt;但有三个场景 SSE 会卡住，必须知道&lt;/strong&gt;：&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;&lt;strong&gt;HTTP/1.1 下浏览器每域名 6 连接上限&lt;/strong&gt;。用户开 7 个标签页，第 7 个直接卡死连不上。HTTP/2 才解决。这是真实事故，不是理论问题。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;需要客户端高频上行&lt;/strong&gt;时（比如实时语音打断、协同光标），SSE 是单向的，你得再开一条 POST 通道，等于维护两套。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Nginx 默认开 &lt;code&gt;proxy_buffering&lt;/code&gt;&lt;/strong&gt;，会把流式响应攒着一起发——表现是「前端一直转圈，然后内容唰一下全出来」。必须显式关掉。&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;第 3 条几乎是每个做流式输出的人都踩过的坑，说出来立刻拉近距离。&lt;/p&gt;
&lt;h2&gt;三、「LangGraph 时代已来」：小心这题是个陷阱&lt;/h2&gt;
&lt;p&gt;两份宝典都在推 LangGraph，理由是状态机比链式更适合 Agent 循环。这个技术判断没问题。&lt;/p&gt;
&lt;p&gt;但如果你在面试里说「现在都用 LangGraph 了」，有经验的面试官很可能反问：&lt;strong&gt;「那你为什么不自己写？」&lt;/strong&gt;&lt;/p&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;Prompt 被藏起来了&lt;/strong&gt;：框架内置的 prompt 模板你很难控制，而 Agent 效果的大头恰恰在 prompt 上。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;升级不兼容&lt;/strong&gt;：这个生态的 breaking change 频率远高于传统后端框架。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;核心循环其实很短&lt;/strong&gt;：「调模型 → 解析 tool_call → 执行 → 拼回消息 → 再调」，手写不到 200 行，而且完全可控。&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;strong&gt;成熟的答法是分场景&lt;/strong&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;理由&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;快速验证想法 / 做 Demo&lt;/td&gt;
&lt;td&gt;LangGraph、CrewAI&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;/tr&gt;
&lt;tr&gt;
&lt;td&gt;重 RAG、轻 Agent&lt;/td&gt;
&lt;td&gt;LlamaIndex&lt;/td&gt;
&lt;td&gt;索引和检索抽象确实好用&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Java 技术栈&lt;/td&gt;
&lt;td&gt;Spring AI&lt;/td&gt;
&lt;td&gt;和现有 Spring 体系一致，团队上手快&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;p&gt;最后补一句会加分的：「框架的价值在生态而不是抽象——我会用它的 tool 定义、retriever 这些&lt;strong&gt;组件&lt;/strong&gt;，但主循环倾向自己掌握。」&lt;/p&gt;
&lt;h2&gt;四、被两份都轻描淡写的：评测&lt;/h2&gt;
&lt;p&gt;这是两份宝典&lt;strong&gt;共同的最大缺口&lt;/strong&gt;。65 题 + 33 题里，关于「怎么知道你的 Agent 变好了还是变坏了」几乎没有。&lt;/p&gt;
&lt;p&gt;而这恰恰是真实项目里最难的部分。改了个 prompt，怎么证明不是负优化？换了个模型，怎么证明没有悄悄退化？&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;能说清这一段，比会背 33 道选型题更能证明你干过活：&lt;/strong&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;私有评测集&lt;/strong&gt;：从生产日志抽 100–300 条真实请求，人工标注期望结果。这是一次性的苦活，但是所有后续判断的基准。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;分层看指标&lt;/strong&gt;，不要只看端到端成功率：
&lt;ul&gt;
&lt;li&gt;检索层：Recall@K、MRR&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;回归机制&lt;/strong&gt;：每次改 prompt 或换模型，跑一遍评测集，对比。&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;
&lt;h2&gt;五、安全这一节，补两个真会出事的&lt;/h2&gt;
&lt;p&gt;第一份宝典提到了 Prompt Injection 和权限隔离，方向对，但停在了「要做检测」。实际上&lt;strong&gt;检测本身是不可靠的&lt;/strong&gt;，真正有效的是架构层面的限制。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;间接注入才是主要威胁。&lt;/strong&gt; 大家防的是用户直接说「忽略之前的指令」，但真实攻击面在于：Agent 读取的&lt;strong&gt;外部内容&lt;/strong&gt;里藏着指令——网页、PDF、数据库里的某个字段、甚至另一个 Agent 的输出。用户完全无辜，攻击载荷从数据通道进来。&lt;/p&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;strong&gt;实际参数&lt;/strong&gt;而不是模型的自然语言描述&lt;/li&gt;
&lt;li&gt;数据库连接用&lt;strong&gt;只读账号&lt;/strong&gt;，别指望 prompt 里写「不要执行 DROP」能拦住&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; 为了排查问题，大家习惯把完整 prompt 打进日志。而 prompt 里可能有用户手机号、身份证、内部文档原文。这些日志通常还会进 ELK，被全公司检索。&lt;/p&gt;
&lt;p&gt;这是我见过最容易被忽略、又最容易出事的一条。做法是日志脱敏 + 完整上下文单独存到权限受控的位置、设短 TTL。&lt;/p&gt;
&lt;h2&gt;六、一个原文没给的：选型决策表&lt;/h2&gt;
&lt;p&gt;两份宝典列了大量选项的优劣，但没给&lt;strong&gt;判断顺序&lt;/strong&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;就选&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;数据量多大？&lt;/td&gt;
&lt;td&gt;&amp;lt; 10 万条&lt;/td&gt;
&lt;td&gt;PGVector，别上专门的向量库&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;/td&gt;
&lt;td&gt;&amp;gt; 千万条&lt;/td&gt;
&lt;td&gt;Milvus / Qdrant&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;要不要过滤查询？&lt;/td&gt;
&lt;td&gt;要（按租户、时间、权限）&lt;/td&gt;
&lt;td&gt;PGVector 或 Qdrant，混合查询是刚需&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;团队有 Java 栈吗？&lt;/td&gt;
&lt;td&gt;有&lt;/td&gt;
&lt;td&gt;Spring AI，别为了 Agent 引入 Python 微服务&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;要多轮工具调用吗？&lt;/td&gt;
&lt;td&gt;不要，就是问答&lt;/td&gt;
&lt;td&gt;别叫它 Agent，就是 RAG，简单得多&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;延迟要求？&lt;/td&gt;
&lt;td&gt;&amp;lt; 2s 首字&lt;/td&gt;
&lt;td&gt;砍掉 rerank 或换轻量 reranker&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;strong&gt;先建评测集，其他都别急&lt;/strong&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;p&gt;最后一行是认真的。没有评测集的情况下讨论「选 A 还是选 B」，本质上是在比谁的 PPT 好看。&lt;/p&gt;
&lt;h2&gt;小结：这两份宝典怎么用&lt;/h2&gt;
&lt;p&gt;&lt;strong&gt;可以直接背的&lt;/strong&gt;：Agentic Loop 流程、ReAct 范式、分层记忆、MCP/A2A 的定位、SSE vs WebSocket 的基本对比、FastAPI 异步原理。这些是共识，说错了才扣分。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;不要背的&lt;/strong&gt;：所有具体数字、所有「XX 是标准答案」式的结论。改成给方法和边界。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;要自己补的&lt;/strong&gt;：评测怎么做、间接注入怎么防、什么时候不该用框架、什么时候压根不该叫它 Agent。&lt;/p&gt;
&lt;p&gt;这三类里，第三类才是决定你能不能过终面的部分。前两类全网都有，面试官听了几十遍了。&lt;/p&gt;
&lt;hr /&gt;
&lt;p&gt;相关：&lt;a href=&quot;/posts/ai-dev-playbook/&quot;&gt;《AI 出码工程手记》&lt;/a&gt;讲日常怎么用 AI 写业务代码；&lt;a href=&quot;/posts/ai-interview-cheatsheet/&quot;&gt;《AI 开发岗面试速查》&lt;/a&gt;是更基础的 RAG / Agent / 微调速查。&lt;/p&gt;
</content:encoded></item><item><title>AI 开发岗面试速查：RAG、Agent 与微调</title><link>https://gilgameshzzz.github.io/posts/ai-interview-cheatsheet/</link><guid isPermaLink="true">https://gilgameshzzz.github.io/posts/ai-interview-cheatsheet/</guid><description>AI 开发岗高频面试题的速查答案：RAG 全链路与召回优化、Function Calling 原理、Agent 失控防护、微调时机、结构化输出与 Prompt 注入防御。</description><pubDate>Tue, 15 Sep 2026 00:00:00 GMT</pubDate><content:encoded>&lt;p&gt;一份面试前十分钟能扫完的速查。每题只给骨架和加分点——面试官想听的从来不是流程名，而是你在哪一环踩过坑。&lt;/p&gt;
&lt;p&gt;日常怎么用 AI 写业务代码，另见&lt;a href=&quot;/posts/ai-dev-playbook/&quot;&gt;《AI 出码工程手记》&lt;/a&gt;。&lt;/p&gt;
&lt;p&gt;&amp;lt;!-- more --&amp;gt;&lt;/p&gt;
&lt;h2&gt;RAG&lt;/h2&gt;
&lt;p&gt;&lt;strong&gt;Q：完整讲一下 RAG 链路&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;骨架：解析 → 切分 → Embedding → 入库 ｜ 查询改写 → 混合召回 → 重排 → 组装 → 生成 → 引用溯源。&lt;/p&gt;
&lt;p&gt;每步带一句实践细节（「PDF 表格解析我们换过三次方案」），比背完整流程加分得多。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Q：向量召回不准怎么办&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;① &lt;strong&gt;混合检索&lt;/strong&gt;：向量管语义，BM25 管精确匹配——型号、错误码、人名这类恰恰是向量最弱的地方；
② &lt;strong&gt;Query 改写 / HyDE&lt;/strong&gt;；
③ &lt;strong&gt;Rerank&lt;/strong&gt;：粗召回 top50 → cross-encoder 精排 top5，投入产出比最高的一环；
④ &lt;strong&gt;元数据过滤&lt;/strong&gt;：能用结构化条件先砍掉的，不要交给向量。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Q：Chunk 怎么切&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;重点是&lt;strong&gt;按语义边界切优于按字数切&lt;/strong&gt;：Markdown 按标题层级，代码按函数，合同按条款。字数只是兜底起点（300–800 token + 10~20% overlap），不是标准答案——长上下文成为常态后，偏大的 chunk 也完全可用。&lt;/p&gt;
&lt;p&gt;加分：chunk 前缀带上父级标题，否则切出来的片段脱离上下文后语义漂移。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Q：怎么评估 RAG 效果&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;拆成检索指标（Recall@K、MRR）和生成指标（忠实度、答案相关性）分别评——混在一起你永远不知道是没召回还是没答好。&lt;/p&gt;
&lt;p&gt;必须有评测集：真实问题抽 100~200 条，标注标准答案和应召回文档。线上看重问率、点踩率、人工接管率。&lt;/p&gt;
&lt;h2&gt;Agent 与工具&lt;/h2&gt;
&lt;p&gt;&lt;strong&gt;Q：Function Calling 的原理&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;模型&lt;strong&gt;不执行&lt;/strong&gt;任何函数。你把工具 JSON Schema 放进请求，模型输出结构化的调用意图，&lt;strong&gt;由你的代码执行&lt;/strong&gt;，结果回填后它继续推理。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Q：Agent 怎么防止死循环失控&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;最大迭代轮数 + 超时 + token 预算上限；工具白名单，写操作强制人工确认；检测重复动作（同工具同参数连续调用就中断）；每步结果校验，失败重试限次数。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Q：怎么保证输出的 JSON 一定能解析&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;分层兜底：① structured output / tool calling 做模型侧约束 → ② prompt 给完整示例 → ③ 代码侧剥 markdown 围栏、正则提最外层大括号 → ④ 解析失败把错误回喂重试，限次数。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Q：Prompt 注入怎么防&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;核心心智：&lt;strong&gt;把模型输出当作不可信输入处理&lt;/strong&gt;，和用户提交的表单同级。&lt;/p&gt;
&lt;p&gt;可信边界（系统指令与数据分离）+ 最小权限（危险操作人工确认）+ 输出侧过滤（防止生成内容直接进 SQL / shell / 前端渲染）。&lt;/p&gt;
&lt;h2&gt;模型选择&lt;/h2&gt;
&lt;p&gt;&lt;strong&gt;Q：什么时候该微调&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;排除法：先 prompt，再 RAG，最后才微调。&lt;/p&gt;
&lt;p&gt;缺知识用 RAG——微调塞知识性价比极低且难更新；缺格式 / 风格 / 领域语气才微调。没有持续标注能力就别开这个头。&lt;/p&gt;
&lt;h2&gt;开放题&lt;/h2&gt;
&lt;p&gt;&lt;strong&gt;Q：设计一个企业内部知识库问答&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;先反问澄清（不问直接扣分）：文档规模和格式？更新频率？用户是谁？准确率要求？权限隔离？延迟预算？&lt;/p&gt;
&lt;p&gt;再按 数据链路 → 检索链路 → 生成链路 → 评估迭代 → 非功能 展开。&lt;/p&gt;
&lt;p&gt;关键加分点：&lt;strong&gt;权限过滤必须在检索层做，不能靠 prompt&lt;/strong&gt;。靠 prompt 说「不要回答用户无权查看的内容」，等于没做。&lt;/p&gt;
</content:encoded></item><item><title>AI 出码工程手记：上下文、调试、并行与 Skill 的实践清单</title><link>https://gilgameshzzz.github.io/posts/ai-dev-playbook/</link><guid isPermaLink="true">https://gilgameshzzz.github.io/posts/ai-dev-playbook/</guid><description>在真实业务仓库里用 Claude Code / Cursor 做开发攒下的实践清单：怎么让 AI 写出能合进主干的代码、怎么让它查线上 bug、多 agent 并行会撞什么、封装 skill 和安全边界上会踩哪些坑。</description><pubDate>Tue, 15 Sep 2026 00:00:00 GMT</pubDate><content:encoded>&lt;p&gt;在真实业务仓库里用 Claude Code / Cursor / Codex 做开发，攒下来的一份实践清单：怎么让它写出能合进主干的代码、怎么查线上 bug、并行跑多个 agent 会撞什么、把重复流程封装成 skill 时会踩哪些坑。&lt;/p&gt;
&lt;p&gt;（面试题速查另开了一篇：&lt;a href=&quot;/posts/ai-interview-cheatsheet/&quot;&gt;《AI 开发岗面试速查》&lt;/a&gt;。）&lt;/p&gt;
&lt;p&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;三项是乘法关系——任何一项接近 0，换更强的模型也救不回来。绝大多数「AI 写的代码不能用」，根因在前两项。&lt;/p&gt;
&lt;h2&gt;一、上下文工程：让它「看见」你的工程&lt;/h2&gt;
&lt;p&gt;投入产出比最高的一件事，而且是一次性投入、长期复利。&lt;/p&gt;
&lt;h3&gt;写一份真正能用的 CLAUDE.md / AGENTS.md&lt;/h3&gt;
&lt;p&gt;不是写项目简介，是写&lt;strong&gt;新人 onboarding 文档&lt;/strong&gt;。判断标准：一个刚入职的同事只看这份文档，能不能提交一个不被打回的 PR。&lt;/p&gt;
&lt;p&gt;必须写的六块：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;技术栈与版本    Spring Boot 2.7 / JDK 11 / MyBatis-Plus，不要用 record、不要用 var
目录职责        xxx-api 只放 DTO；xxx-service 放业务；禁止 controller 直连 mapper
命名与分层      Service 接口 + Impl；DTO/VO/DO 三层不互穿，转换走 Converter
禁用写法        禁止 System.out；禁止裸 RuntimeException；禁止在循环里查库
改动前必读      新增接口先看 XxxController；新增表先看 db/migration 的 flyway 命名规范
验证命令        mvn -pl xxx-service -am compile
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;一条经验：与其写「要符合项目风格」，不如写「参照 &lt;code&gt;OrderServiceImpl&lt;/code&gt; 的分层和异常处理方式」。&lt;strong&gt;模型抄得比理解得好&lt;/strong&gt;——给正例的效果远超给规则。&lt;/p&gt;
&lt;p&gt;代价是这把刀双刃：&lt;strong&gt;你挑的正例如果本身有坏味道，它会连坏味道一起复制放大&lt;/strong&gt;。挑锚点文件时按「我希望新同事照着写的那个」来挑，而不是「最像的那个」。&lt;/p&gt;
&lt;h3&gt;控制信噪比，而不是塞满窗口&lt;/h3&gt;
&lt;p&gt;很多人以为上下文越多越好，实际是反的。无关文件会稀释注意力，还会让它「学」到你不想要的旧写法。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;典型翻车：&lt;/strong&gt;&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;「这是我们整个仓库，帮我加个导出功能」&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;它会在 200 个文件里挑一个最像的模仿，而那个文件很可能是三年前没人维护的老模块。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;改成：&lt;/strong&gt;&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;「参照 &lt;code&gt;@OrderExportController&lt;/code&gt; 的写法，在 &lt;code&gt;@ReportController&lt;/code&gt; 里加导出接口，Excel 工具用 &lt;code&gt;@ExcelUtil&lt;/code&gt;」&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;明确的 3 个锚点文件，比 200 个文件的召回准确率高一个量级。&lt;/p&gt;
&lt;h3&gt;陈旧上下文比没有上下文更危险&lt;/h3&gt;
&lt;p&gt;长会话里，模型记得的是文件&lt;strong&gt;几十轮之前的样子&lt;/strong&gt;。你手改过、或者它自己改过之后再讨论别的，很容易基于旧版本继续编辑，产出看似合理实则冲突的 diff。&lt;/p&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;code&gt;xxx&lt;/code&gt;」，而你刚改过&lt;/li&gt;
&lt;li&gt;它给的 diff 上下文行和文件当前内容有细微出入&lt;/li&gt;
&lt;li&gt;它「修复」了一个你上一轮已经修过的问题&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;看到任意一条，立刻让它重读文件，别争论。&lt;/p&gt;
&lt;p&gt;习惯：关键改动前让它重新读一遍当前文件；重构完立刻开新会话。&lt;/p&gt;
&lt;h2&gt;二、需求怎么下：写成可验收的&lt;/h2&gt;
&lt;p&gt;模棱两可的需求，模型不会来问你，它会自己编一个答案然后写 300 行。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;含糊的说法：&lt;/strong&gt;&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;帮我加个数据导出功能&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;结果：新造一个 ExcelHelper、自己定义一套错误码、全量 load 到内存、权限裸奔。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;可验收的说法：&lt;/strong&gt;&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;在 &lt;code&gt;ReportController&lt;/code&gt; 加 &lt;code&gt;/export&lt;/code&gt;：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;复用现有 &lt;code&gt;ExcelUtil&lt;/code&gt;，别新建工具类&lt;/li&gt;
&lt;li&gt;分页拉取，单页 2000，禁止全量 load&lt;/li&gt;
&lt;li&gt;权限走现有 &lt;code&gt;@PreAuthorize(&apos;report:export&apos;)&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;错误码用 &lt;code&gt;BizErrorCode&lt;/code&gt;，不要新增枚举&lt;/li&gt;
&lt;li&gt;改完执行 &lt;code&gt;mvn -pl report-service -am compile&lt;/code&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;/blockquote&gt;
&lt;p&gt;每一条都对应一个能当场判对错的检查点。&lt;/p&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;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;超过 3 个文件的改动，先让它输出&lt;strong&gt;实施计划&lt;/strong&gt;（Claude Code 的 plan mode，或直接说「先别写代码，先讲方案」）。你确认思路再放行。方案阶段推翻一次的成本是几秒，代码阶段是十分钟加一次心态崩溃。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;粒度陷阱&lt;/strong&gt;：一次让它改 20 个文件，&lt;strong&gt;你不会认真 review，它也知道你不会&lt;/strong&gt;。把改动切到「一个可 review 的 commit」——通常 3~8 个文件。粒度失控是 AI 代码埋雷的头号温床。&lt;/p&gt;
&lt;p&gt;例外是&lt;strong&gt;机械性批量改动&lt;/strong&gt;：全仓重命名、依赖升级、批量替换废弃 API。这类改动每个文件的变更模式完全一样，几十个文件反而是 AI 最擅长也最安全的场景——你 review 的是「模式对不对」，不是逐文件读。区分标准：&lt;strong&gt;这 20 个文件的改动是同一件事，还是 20 件事&lt;/strong&gt;。&lt;/p&gt;
&lt;h2&gt;三、调试既有 bug：和写新功能是两套方法&lt;/h2&gt;
&lt;p&gt;日常里「线上报错，帮我查」比「帮我写个新功能」高频得多，但方法论完全不同，也更容易翻车。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;最大的坑：信息不足时它不会说「我不知道」，它会从第一个可疑点开始猜，然后「修」三个不相干的地方。&lt;/strong&gt; 你收到一个五文件的 diff，其中零个碰到了真正的根因。&lt;/p&gt;
&lt;h3&gt;先给料，再提问&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;：什么请求、什么参数、什么数据状态下触发&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;边界&lt;/strong&gt;：什么情况下是正常的——「同样的接口 A 用户正常，B 用户报错」比任何描述都值钱&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;强制「先定位，再动手」&lt;/h3&gt;
&lt;p&gt;这是 plan mode 在 debug 场景的对应物，一句话就够：&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;/p&gt;
&lt;h3&gt;它爱盖症状，不爱修根因&lt;/h3&gt;
&lt;p&gt;最高频的三种「假修复」：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;加 &lt;code&gt;try-catch&lt;/code&gt; 把异常吞了，接口不报错了，数据还是错的&lt;/li&gt;
&lt;li&gt;加一堆 &lt;code&gt;if (x != null)&lt;/code&gt;，NPE 消失了，为什么是 null 没人知道&lt;/li&gt;
&lt;li&gt;改调用方绕开问题，而不是修被调用方&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;对策：验收时问一句「&lt;strong&gt;如果不加这个判空，为什么它会是 null？&lt;/strong&gt;」答不上来就是没找到根因。这和 Review 清单里的「异常吞掉」是同一个病。&lt;/p&gt;
&lt;h2&gt;四、多 agent 并行：能拆什么，不能拆什么&lt;/h2&gt;
&lt;p&gt;subagent 分工、git worktree 并行跑多个任务，是现在的常态形态，但不是所有活都能拆。&lt;/p&gt;
&lt;h3&gt;能丢给 subagent 的&lt;/h3&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;li&gt;独立维度的审查：一个查并发问题、一个查权限、一个查 SQL，各跑各的&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;不能丢的&lt;/h3&gt;
&lt;p&gt;&lt;strong&gt;需要你脑子里运行时上下文的改动。&lt;/strong&gt; subagent 拿不到你对这张表有多少行、这个接口 QPS 多高、这段逻辑三个月前为什么这么写的记忆——它只能看到代码。这类改动交出去，回来的是「语法正确、业务错误」。&lt;/p&gt;
&lt;h3&gt;subagent 的总结同样比代码乐观&lt;/h3&gt;
&lt;p&gt;这是第五章「别信总结」的放大版，而且更危险：主 agent 收到的&lt;strong&gt;只有总结&lt;/strong&gt;，它没看过 diff，会把那段乐观描述当事实继续往下做，误差就此被固化进后续所有决策。&lt;/p&gt;
&lt;p&gt;对策：subagent 的结论当&lt;strong&gt;线索&lt;/strong&gt;用，不当&lt;strong&gt;结论&lt;/strong&gt;用。要它带上可验证的定位（文件 + 行号），你或主 agent 自己去看一眼原文。&lt;/p&gt;
&lt;h3&gt;并行改同一个仓库，worktree 解决不了什么&lt;/h3&gt;
&lt;p&gt;worktree 给的是&lt;strong&gt;文件系统隔离&lt;/strong&gt;，不是&lt;strong&gt;语义隔离&lt;/strong&gt;。Spring Boot 仓库上并行跑两个任务，典型的撞车：&lt;/p&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;冲突点&lt;/th&gt;
&lt;th&gt;worktree 能解决吗&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;改同一个 &lt;code&gt;.java&lt;/code&gt; 文件&lt;/td&gt;
&lt;td&gt;能隔离，但合并时照样冲突&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;各自往 &lt;code&gt;pom.xml&lt;/code&gt; 加依赖&lt;/td&gt;
&lt;td&gt;不能，合并冲突几乎必然&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;各自新建 flyway 脚本&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;不能，而且最坑&lt;/strong&gt;——两个 &lt;code&gt;V1.14__xxx.sql&lt;/code&gt;，本地各自能跑，合进主干直接崩&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&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;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;p&gt;实际做法：并行任务&lt;strong&gt;按模块切&lt;/strong&gt;，不按功能切；flyway 版本号和端口在派活时就人工分配好，别指望它们自己协调。&lt;/p&gt;
&lt;h2&gt;五、反馈闭环：让它自己验证&lt;/h2&gt;
&lt;p&gt;这是「能用的 agent」和「高级补全」之间的分水岭。&lt;/p&gt;
&lt;p&gt;有编译、测试、lint 的项目，出码质量会&lt;strong&gt;断崖式提升&lt;/strong&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;它能自己发现什么&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;code&gt;mvn -pl x -am compile&lt;/code&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;code&gt;mvn test -Dtest=XxxTest&lt;/code&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;起服务 + curl 打一次接口&lt;/td&gt;
&lt;td&gt;配置、Bean 注入、SQL 实际是否跑得通&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;p&gt;老项目没有测试也别放弃——但在一个起不来 Spring 上下文、依赖三个外部服务、还要连库的老工程里，「让它补个测试」本身就是个大坑。按成本从低到高挑一个能跑通的就行：&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;&lt;strong&gt;只跑编译&lt;/strong&gt;。&lt;code&gt;mvn -pl xxx-service -am compile&lt;/code&gt; 几乎零成本，已经能拦掉一半低级错误。任何情况下先把这条给它。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;本地起服务 + curl&lt;/strong&gt;。服务能起来的话这是性价比最高的——连 dev 库、打一次真实接口，配置、Bean 注入、SQL 是否跑得通一次全验。把 curl 命令连同参数写进 CLAUDE.md，它每次都能直接用。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;切片测试，不起全 context&lt;/strong&gt;。&lt;code&gt;@SpringBootTest&lt;/code&gt; 起不来时，用 &lt;code&gt;@MybatisTest&lt;/code&gt; / &lt;code&gt;@WebMvcTest&lt;/code&gt; 只加载需要的那层；外部依赖 &lt;code&gt;@MockBean&lt;/code&gt; 掉。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;实在不行，一个 main 方法&lt;/strong&gt;。不优雅，但能跑就比不能跑强。&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;关键不是测试写得多正规，是&lt;strong&gt;让它有一条能自己反复跑的路径&lt;/strong&gt;。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;别信总结，看 diff&lt;/strong&gt;：它写的完成总结和 commit message，&lt;strong&gt;永远比代码本身乐观&lt;/strong&gt;。「已完成并通过测试」经常意味着「我改完了，测试我没跑」。&lt;/p&gt;
&lt;h2&gt;六、Token 优化：钱和效果是同一件事&lt;/h2&gt;
&lt;p&gt;省 token 不只是省钱——上下文越干净，出码质量越高。两个目标同向。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;先说最重要的：最贵的浪费不是 prompt 写长了，而是方向错了跑了二十轮。&lt;/strong&gt; 一次 5 分钟的方案确认，省下的 token 比所有微观优化加起来都多。&lt;strong&gt;省 token 的第一性原理是减少返工，不是压缩字数。&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;第二重要的是&lt;strong&gt;在任务边界主动开新会话&lt;/strong&gt;。注意这里的理由不是钱——有 prompt caching 在，续接会话的历史部分大多命中缓存，成本远低于「每轮重算全部历史」的直觉。真正的理由是&lt;strong&gt;上下文污染和注意力稀释&lt;/strong&gt;：上一个任务的文件、被推翻的方案、改了一半的思路全堆在那里，它的判断会越来越糊。&lt;/p&gt;
&lt;p&gt;至于自动 compact：长任务确实需要它兜底，但&lt;strong&gt;别把它当成不开新会话的理由&lt;/strong&gt;。压缩后它对项目的记忆会变粗，常见表现是突然又开始重复造轮子。&lt;/p&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;收益&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Prompt caching&lt;/td&gt;
&lt;td&gt;稳定内容（系统提示、规范文档）放&lt;strong&gt;最前面且逐字不变&lt;/strong&gt;，变化的放后面。前缀一改，缓存全废。自己调 API 时才能直接控制顺序；在 Claude Code / Cursor 这类工具里，对应的做法是&lt;strong&gt;别频繁改 CLAUDE.md、别中途切换工具集&lt;/strong&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;用 grep/glob 定位再读片段，而不是整文件塞进去&lt;/td&gt;
&lt;td&gt;高&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;子代理隔离&lt;/td&gt;
&lt;td&gt;大范围搜索丢给 subagent，主上下文只收结论（见第四章）&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;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;杂项&lt;/td&gt;
&lt;td&gt;约束输出长度（「只给 diff」）、命令加 &lt;code&gt;-q&lt;/code&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;p&gt;token 账单看不出问题——它只告诉你花了多少，不告诉你&lt;strong&gt;其中多少是白花的&lt;/strong&gt;。&lt;/p&gt;
&lt;p&gt;更值得看的一个数：&lt;strong&gt;返工轮数&lt;/strong&gt;。同一个任务，从下达到你接受，中间来回了几轮？三轮以内说明需求写清楚了；超过五轮，问题几乎一定在需求或上下文，不在模型。按这个数复盘，比盯账单有用得多。&lt;/p&gt;
&lt;h2&gt;七、封装成 Skill：会踩的坑&lt;/h2&gt;
&lt;p&gt;把重复流程（日报、发版检查、代码审查）写成 skill / 自定义命令，收益很大，但坑也集中。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;1. 触发描述写太泛&lt;/strong&gt;（最高频）
description 写成「处理文档相关任务」，结果它在不该触发时触发，或者该用的时候完全不认。
→ description 里写具体触发词和场景，包括「什么时候&lt;strong&gt;不&lt;/strong&gt;该用」。这段文本是唯一的路由依据。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;2. 写得太长&lt;/strong&gt;
把所有细节塞进一个 3000 行的 skill，反而稀释了关键指令，模型只执行开头和结尾。
→ 主文件只留决策路径和必须遵守的规则，细节拆到 reference 文件按需加载。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;3. 可移植性：环境与编码&lt;/strong&gt;
两个表现，一个根因——skill 里藏了「只在你这台机器成立」的假设。&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;隐式环境依赖&lt;/strong&gt;：能跑是因为你的 PATH 里有那个命令、venv 已激活、配置恰好在默认位置。换台机器全挂。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;跨平台编码&lt;/strong&gt;（Windows 尤其）：中文输出直接 &lt;code&gt;UnicodeEncodeError&lt;/code&gt;，写文件写成 GBK 后续读取乱码；&lt;code&gt;&amp;amp;&amp;amp;&lt;/code&gt; 在 PowerShell 5.1 里是语法错误。&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;→ 显式写清依赖和绝对路径，开头做一次前置检查、缺失就明确报错；统一指定 UTF-8；路径用 &lt;code&gt;os.path.join&lt;/code&gt;；命令链别假设 bash 语法。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;4. 默认值悄悄生效&lt;/strong&gt;
skill 里留了示例默认值（某个用户名、某个环境、某个目录），调用时没覆盖，&lt;strong&gt;它就真的用了&lt;/strong&gt;，还不报错——最后产出的是别人的数据。
→ 必填参数缺失就中断，不要给「看起来合理」的兜底默认值。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;5. 不幂等&lt;/strong&gt;
重跑一次就追加一遍、发两条消息、建重复记录。AI 执行本来就会重试。
→ 写操作先判断是否已存在；对外发送类动作加显式确认。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;6. 权限边界模糊&lt;/strong&gt;
skill 里放了能连生产、能改配置、能推送的能力，某天在一个无关任务里被顺手调用。
→ 读写分离成两个 skill；危险动作强制人工确认；生产凭据绝不进 skill 文件。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;7. 难调试、会漂移&lt;/strong&gt;
出问题时你不知道是 skill 写得不对、参数传错、还是模型没按指令走；而且&lt;strong&gt;模型版本一升级，原来稳定的 skill 行为就变了&lt;/strong&gt;。
→ skill 里要求打印关键中间步骤；准备 2~3 个固定输入的回归用例。&lt;/p&gt;
&lt;p&gt;回归用例不需要很正规，形式就是「&lt;strong&gt;固定输入 + 必须出现的关键字 + 不得出现的关键字&lt;/strong&gt;」。比如日报 skill：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;输入:  --date 2026-09-15 --user zhousongyao
必须有: 日期行、至少一条 commit、结尾的「明日计划」小节
不得有: 示例默认用户名、&quot;TODO&quot;、空的小节标题
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;换模型后跑一遍，两分钟，能挡住绝大多数行为漂移。&lt;/p&gt;
&lt;p&gt;什么该做成 skill？判断标准：&lt;strong&gt;同一个流程你已经手动解释过三次以上，且步骤稳定&lt;/strong&gt;。流程本身还在变的，先别封装——你会花更多时间维护 skill 而不是干活。&lt;/p&gt;
&lt;h2&gt;八、安全：AI 工具链自身的风险面&lt;/h2&gt;
&lt;p&gt;这块最容易被跳过，但踩一次就是事故。&lt;/p&gt;
&lt;h3&gt;敏感文件会被读进上下文&lt;/h3&gt;
&lt;p&gt;它读配置文件找 Bean 定义时，会顺带把 &lt;code&gt;application-dev.yml&lt;/code&gt; 里的数据库密码、AK/SK 一起读进上下文，然后出现在会话记录里、出现在它复述配置的回答里。&lt;/p&gt;
&lt;p&gt;对策：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;用 &lt;code&gt;.claudeignore&lt;/code&gt; / 工具的忽略配置排掉 &lt;code&gt;*-dev.yml&lt;/code&gt;、&lt;code&gt;*-local.yml&lt;/code&gt;、&lt;code&gt;.env&lt;/code&gt;、密钥目录&lt;/li&gt;
&lt;li&gt;本地配置根本不进仓库——这是治本的那一条&lt;/li&gt;
&lt;li&gt;已经进过 git 的凭据，&lt;strong&gt;删文件没用，要先轮换再清历史&lt;/strong&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;它很喜欢「顺手」硬编码密钥&lt;/h3&gt;
&lt;p&gt;写测试、写示例、写临时脚本时，它会把连接串、token 直接写进代码——因为这样「能跑」，而它的目标函数是能跑。&lt;/p&gt;
&lt;p&gt;对策：在 CLAUDE.md 里写死一条「&lt;strong&gt;任何凭据一律从环境变量或配置中心读，禁止出现在源码和测试里&lt;/strong&gt;」，并把它放进 Review 清单逐次扫。&lt;/p&gt;
&lt;h3&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;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;只读（grep、读文件、&lt;code&gt;git diff&lt;/code&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;放开，但靠 git 兜底&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;git push&lt;/code&gt;、改 CI 配置、发消息&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;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;p&gt;这和第七章第 6 坑是同一件事的两面：skill 是能力的封装，权限模式是能力的闸门。&lt;/p&gt;
&lt;h3&gt;间接注入&lt;/h3&gt;
&lt;p&gt;从第三方 MCP server、依赖的 README、网页抓来的内容里，可能带着「忽略之前的指令」这类文本。心智和防用户输入一样：&lt;strong&gt;模型读到的一切外部内容都是不可信输入&lt;/strong&gt;，别让它直接驱动危险动作。&lt;/p&gt;
&lt;h2&gt;九、Review 清单：AI 最容易写得「看起来对」的地方&lt;/h2&gt;
&lt;p&gt;它的语法和结构几乎不会错，所以错误全部集中在语义层。&lt;/p&gt;
&lt;h3&gt;先分诊，再逐条扫&lt;/h3&gt;
&lt;p&gt;十条清单逐行人工扫 8 个文件的 diff，没人扛得住，扫两次你就不扫了。先把能自动化的交出去：&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;&lt;strong&gt;lint / ArchUnit&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;分层违规（controller 直连 mapper）、禁用 API、命名规范、新建工具类&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;SQL 审核 / explain&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;全表扫描、缺索引、没有 limit&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;/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;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;p&gt;规则一次性写好就永久生效，而且对人写的代码同样有用。&lt;strong&gt;人眼只留最后那四类&lt;/strong&gt;——它们都依赖你脑子里的运行时上下文，工具查不了。&lt;/p&gt;
&lt;h3&gt;清单&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;事务边界&lt;/strong&gt;：&lt;code&gt;@Transactional&lt;/code&gt; 加在哪层、有没有被内部调用绕过、有没有把远程调用包进事务里&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;循环内查库 / 调服务&lt;/strong&gt;：它非常喜欢写 for 里面调 &lt;code&gt;selectById&lt;/code&gt;，功能正确但上线即事故&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;全量加载&lt;/strong&gt;：没有分页、没有 limit、导出直接 list 全表&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;：catch 之后只打日志继续往下走，错误被静默&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;空值与边界&lt;/strong&gt;：空列表、null、除零、时间区间左右闭开&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;重复造轮子&lt;/strong&gt;：项目里已有的工具类被无视，又写了一个几乎一样的&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;字段与命名幻觉&lt;/strong&gt;：用了不存在的表字段或方法名——编译能过的场景（Map 取值、SQL 字符串）最危险&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;顺手改了无关代码&lt;/strong&gt;：全量重写文件时改掉了不该动的行，diff 里一定逐行确认&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;这几类不是「模型不够强」，而是它&lt;strong&gt;缺少你脑子里的运行时上下文&lt;/strong&gt;——数据量多大、并发多高、这张表线上有多少行。把这些写进 CLAUDE.md，一半问题会在源头消失。&lt;/p&gt;
&lt;h2&gt;十、什么时候不该用 AI&lt;/h2&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;li&gt;&lt;strong&gt;需要和人反复确认语义的地方。&lt;/strong&gt; 瓶颈是沟通，不是打字，AI 加速不了。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;你不打算 review 的改动。&lt;/strong&gt; 这条最硬：&lt;strong&gt;不 review 就别让它写&lt;/strong&gt;。没被 review 的 AI 代码不是省时间，是把时间挪到了三个月后的线上排查。&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;判断口诀：&lt;strong&gt;它擅长「你知道要什么、只是懒得打字」，不擅长「你还不知道要什么」。&lt;/strong&gt;&lt;/p&gt;
&lt;h2&gt;一页纸&lt;/h2&gt;
&lt;ol&gt;
&lt;li&gt;&lt;strong&gt;写好 CLAUDE.md。&lt;/strong&gt; 你觉得「新同事上手困难」的地方，就是 AI 出码质量差的地方——问题通常在工程本身，不在模型。&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;查 bug 先定位再动手。&lt;/strong&gt; 给全堆栈和复现路径，明确说「先别改代码」。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;让它能自己跑验证。&lt;/strong&gt; 编译 / 测试闭环是质量的分水岭，不是锦上添花。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;省 token 的本质是减少返工&lt;/strong&gt;，不是压缩字数；开新会话是为了上下文干净，不是为了省钱。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;并行按模块切，不按功能切。&lt;/strong&gt; worktree 隔离不了 &lt;code&gt;pom.xml&lt;/code&gt; 和 flyway 版本号。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;敏感文件不进上下文，生产权限不放行。&lt;/strong&gt;&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Review 先分诊再人扫。&lt;/strong&gt; 能自动化的交给 lint / SQL 审核，人眼只留事务、并发、权限、业务语义。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;别信总结，看 diff。&lt;/strong&gt; subagent 的总结也一样。&lt;/li&gt;
&lt;/ol&gt;
</content:encoded></item><item><title>一次 GitHub Pages 部署的四连翻车复盘</title><link>https://gilgameshzzz.github.io/posts/deploy-postmortem/</link><guid isPermaLink="true">https://gilgameshzzz.github.io/posts/deploy-postmortem/</guid><description>这是一篇加密文章，请到站点输入密码后阅读。</description><pubDate>Tue, 15 Sep 2026 00:00:00 GMT</pubDate><content:encoded/></item><item><title>java lambda表达式</title><link>https://gilgameshzzz.github.io/posts/java%E7%9A%84lambda%E8%A1%A8%E8%BE%BE%E5%BC%8F/</link><guid isPermaLink="true">https://gilgameshzzz.github.io/posts/java%E7%9A%84lambda%E8%A1%A8%E8%BE%BE%E5%BC%8F/</guid><description>Lambda表达式：引入函数式编程的风格，不是所有的引用数据类型都可以使用lambda表达式 只有SAM接口（函数式接口）类型的形参，变量才可以赋值为Lambda表达式。 函数式接口：SAM(Single Abstrac…</description><pubDate>Sat, 30 May 2020 00:00:00 GMT</pubDate><content:encoded>&lt;p&gt;Lambda表达式：引入函数式编程的风格，不是所有的引用数据类型都可以使用lambda表达式&lt;br /&gt;
只有SAM接口（函数式接口）类型的形参，变量才可以赋值为Lambda表达式。&lt;br /&gt;
函数式接口：SAM(Single Abstract Method)表示该接口&lt;strong&gt;只有一个抽象方法&lt;/strong&gt;的接口。当然这个接口可以有默认方法和静态方法等成员。如Runnable、Callable、Comparable、Comparator、FileFilter&lt;/p&gt;
&lt;p&gt;JDK1.8建议，这样的接口加一个注解标记@FunctionalInterface&lt;/p&gt;
&lt;h3&gt;一、消费型接口 ， 特点：有参无返回值&lt;/h3&gt;
&lt;p&gt;1、最基本的代表： Consumer : void accept(T t)&lt;br /&gt;
2、其他变形 BiConsumer: void accept(T t, U u)&lt;br /&gt;
例如：集合java.util.Collection系列的集合在JDK1.8之后增加的方法&lt;br /&gt;
default void forEach(Consumer&amp;lt;? super T&amp;gt; action)&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;public class TestConsumer{
    @Test
    public void test(){
        ArrayList&amp;lt;String&amp;gt; list = new ArrayList&amp;lt;&amp;gt;();
        list.add(&quot;hello&quot;);
        list.add(&quot;hello world&quot;);
        list.add(&quot;hello java&quot;);
        list.add(&quot;hello lambda&quot;);
        // foreach循环
        for (String str:list){
            System.out.println(str);
        }
        // forEach 方法就是等价于foreach循环
        list.forEach(t -&amp;gt; System.out.println(t));
    }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;二、供给型接口 抽象方法：无参有返回值&lt;/h3&gt;
&lt;p&gt;1、最基本的代表&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;Supplier&amp;lt;T&amp;gt;： T get()
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;2、其他变型：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;BooleanSupplier  boolean  getAsBoolean()
DoubleSupplier   double   getAsDouble()
IntSupplier         int         getAsInt()
LongSupplier       long       getAsLong()
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;StreamAPI: Stream是一个数据流&lt;br /&gt;
javc.util.stream包Stream类型&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;static&amp;lt;T&amp;gt; Stream&amp;lt;T&amp;gt; generate(Supplier&amp;lt;T&amp;gt; s)
&lt;/code&gt;&lt;/pre&gt;
&lt;pre&gt;&lt;code&gt;class TestSupplier{
    @Test
    public void test(){
        // 产生generate, 产生随机数
        Stream&amp;lt;Double&amp;gt; stream = Stream.generate(() -&amp;gt; Math.random());
        stream.forEach(t-&amp;gt; System.out.println(t));
    }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;三、判断型（断定型）接口 , 返回值都为布尔值&lt;/h3&gt;
&lt;p&gt;抽象方法：boolean test(参数)&lt;br /&gt;
1、最基本的代表&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;Predicate&amp;lt;T&amp;gt; boolean test(T t)
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;2、其他的变形&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;BiPredicate&amp;lt;T,U&amp;gt;  boolean        test(T t,U u)
DoublePredicate  boolean         test(double value)
IntPredicate        boolean         test(int value)
LongPredicate      boolean         test(long value)
&lt;/code&gt;&lt;/pre&gt;
&lt;pre&gt;&lt;code&gt;例如： java.util.Collection&amp;lt;E&amp;gt;
default boolean removeIf(Predicate&amp;lt;? super E&amp;gt;filter)
&lt;/code&gt;&lt;/pre&gt;
&lt;pre&gt;&lt;code&gt;class TestPredicate{
    @Test
    public void test(){
        ArrayList&amp;lt;Integer&amp;gt; list = new ArrayList&amp;lt;&amp;gt;();
        for (int i = 1; i &amp;lt;= 10; i++) {
            list.add(i);
        }
        // 删除3的倍数元素
        //第一种
//        Iterator&amp;lt;Integer&amp;gt; iterator = list.iterator();
//        while (iterator.hasNext()){
//            Integer next = iterator.next();
//            if (next%3 ==0){
//                iterator.remove();
//            }
//        }
        // 第二种
        list.removeIf(t-&amp;gt; t%3 ==0);
        list.forEach(t -&amp;gt; System.out.println(t));
    }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;四、功能型接口 抽象方法：有参有返回值&lt;/h3&gt;
&lt;p&gt;1、最基本的代表：Function&amp;lt;T, R&amp;gt; R apply(T t)&lt;br /&gt;
Lambda表达式是给函数式接口的形参或变量赋值用的，为了给这个函数式接口的抽象方法传递代码用&lt;br /&gt;
语法格式：&lt;br /&gt;
（形参列表） -&amp;gt;{lambda体} 解释：&lt;br /&gt;
（形参列表）就是函数式接口的抽象方法的形参列表。&lt;br /&gt;
{lambda体}就是函数式接口的抽象方法的方法体。&lt;br /&gt;
说明：1、当（形参列表）是空参时，那么（）是不能省略的&lt;br /&gt;
2、当（形参列表）是非空参的，并且类型是确定的或者可以推断的，那么形参的数据类型可以省略&lt;br /&gt;
3、当（形参列表）是非空参的，并且只有一个形参，并且类型也省略了，那么此时()也可以省略，如果类型没有省略，那么()也不能省略。&lt;br /&gt;
4、如果{lambda体} 不止一个语句，那么{}不能省略，并且每一个语句都要；结束&lt;br /&gt;
5、如果{lambda体}只有一个语句，那么{}和；可以省略且同时省略&lt;br /&gt;
6、如果有返回值，用return返回,且只有一个return语句时，return、{}、；都同时省略&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;public void test(){
       ArrayList&amp;lt;String&amp;gt; list = new ArrayList&amp;lt;&amp;gt;();
       list.add(&quot;hello&quot;);
       list.add(&quot;hello world&quot;);
       list.add(&quot;hello java&quot;);
       list.add(&quot;hello lambda&quot;);
      //用匿名内部类给Consumer接口的形参赋值
       list.forEach(new Consumer&amp;lt;String&amp;gt;() {
           @Override
           public void accept(String s) {
               System.out.println(s);
           }
       });
       // forEach 方法就是等价于foreach循环
       //  list.forEach((t) -&amp;gt; {System.out.println(t)};); 省略后
       list.forEach(t -&amp;gt; System.out.println(t));
   }
&lt;/code&gt;&lt;/pre&gt;
</content:encoded></item><item><title>Java集合，泛型，spring注解、事务的一些问题</title><link>https://gilgameshzzz.github.io/posts/java%E9%9B%86%E5%90%88/</link><guid isPermaLink="true">https://gilgameshzzz.github.io/posts/java%E9%9B%86%E5%90%88/</guid><description>Java的集合类主要由两个接口派生而出：Collection和Map,Collection和Map是Java集合框架的根接口。代表了两种不同的数据结构：集合和映射表。</description><pubDate>Fri, 24 Apr 2020 00:00:00 GMT</pubDate><content:encoded>&lt;h2&gt;java 集合类之间的继承关系&lt;/h2&gt;
&lt;p&gt;Java的集合类主要由两个接口派生而出：Collection和Map,Collection和Map是Java集合框架的根接口。代表了两种不同的数据结构：集合和映射表。&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;/img/collection.png&quot; alt=&quot;collection&quot; /&gt;&lt;/p&gt;
&lt;p&gt;图中，ArrayList,HashSet,LinkedList,TreeSet是我们经常会有用到的已实现的集合类。&lt;/p&gt;
&lt;p&gt;Map实现类用于保存具有映射关系的数据。Map保存的每项数据都是key-value对，也就是由key和value两个值组成。Map里的key是不可重复的，key用户标识集合里的每项数据。&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;/img/map.png&quot; alt=&quot;map&quot; /&gt;&lt;/p&gt;
&lt;h2&gt;迭代器的作用&lt;/h2&gt;
&lt;p&gt;由于Java中数据容器众多，而对数据容器的操作在很多时候都具有极大的共性，于是Java采用了迭代器为各种容器提供公共的操作接口&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;解耦效果&lt;/strong&gt;：使用迭代器iterator可以使对容器的遍历操作完全与其底层相隔离，可以到达极好的解耦效果&lt;/p&gt;
&lt;p&gt;iterator方法返回一个实现了Iterator接口的对象，作用是依次访问集合中的元素，Iterator接口包含3个方法：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;boolean hasNext();  # 如果任有元素可以迭代，则返回true.
E next();  			# 返回迭代的下一个元素
void remove(); 		# 从迭代器指向的collection中移除迭代器返回的最后一个元素(可选操作)
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;通过多次调用next()方法可遍历集合中的所有元素，需要注意的是需要在调用next()之前调用hasNext()方法，并在hasNext()返回true的时候才可以调用next().&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;public static void main(String[] args)
{
        List&amp;lt;String&amp;gt; list=new ArrayList&amp;lt;&amp;gt;();
        list.add(&quot;C++&quot;);
        list.add(&quot;python&quot;);
        list.add(&quot;java&quot;);
        for(Iterator&amp;lt;String&amp;gt; it=list.iterator();it.hasNext();)
        {
            String item = it.next();
            if(&quot;python&quot;.equals(item)){
              // list.remove(item);  // 翻车写法：下次 it.next() 会抛 ConcurrentModificationException
              it.remove();           // 正确写法
            }
        }
    }
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;遍历集合时删除特定元素一定要用Iterator的remove,别用集合自带的remove。会报错：&lt;/p&gt;
&lt;p&gt;java.util.ConcurrentModificationException&lt;br /&gt;
在集合内部维护一个字段modCount用于记录集合被修改的次数，每当集合内部结构发生变化(add,remove，set)时，modCount+1。　　&lt;br /&gt;
在迭代器内部也维护一个字段expectedModCount，同样记录当前集合修改的次数，初始化为集合的modCount值。调用Iterator遍历时，每次next()都会检查modCount和expectedModCount是否相等，不等就抛出并发修改异常java.util.ConcurrentModificationException。&lt;strong&gt;最典型的场景恰恰是单线程&lt;/strong&gt;——自己在for循环里调了list.remove()，集合的modCount变了而迭代器的expectedModCount没变。多线程下别的线程改集合同样会触发。注意它是fail-fast机制，只保证尽快发现问题，不保证一定能检测到。&lt;img src=&quot;/img/ConcurrentModificationException.png&quot; alt=&quot;ConcurrentModificationException&quot; /&gt;&lt;/p&gt;
&lt;h2&gt;集合的排序&lt;/h2&gt;
&lt;p&gt;java集合的工具类Collections中提供了两种排序的方法,分别是:&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;Collections.sort(List list)&lt;/li&gt;
&lt;li&gt;Collections.sort(List list,Comparator c)&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;第一种称为自然排序,参与排序的对象需实现comparable接口,&lt;strong&gt;缺点会入侵代码&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;第二种叫定制排序,或自定义排序,需编写匿名内部类,先new一个Comparator接口的比较器对象,优点不用修改排序对象代码，例子：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;Collections.sort(ordered, new Comparator&amp;lt;Student&amp;gt;() {
      @Override
      public int compare(Student t1, Student t2) {
        return (int) (t1.getOrder() - t2.getOrder());
      }
    });
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;java8函数式排序&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;//根据学生身高排序
list.sort(Comparator.comparing(Student::getHeight).reversed())
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;泛型&lt;/h2&gt;
&lt;p&gt;泛型类&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;class Demo&amp;lt;T&amp;gt;{
    public T fun(T t){
        return t;
    }
    public &amp;lt;T&amp;gt; T fun2(T t){
        return t;
    }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;img src=&quot;/img/%E6%B3%9B%E5%9E%8B.png&quot; alt=&quot;&quot; /&gt;&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;泛型类的方法&lt;/strong&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;h3&gt;普通泛型类的方法&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;// 虽然方法中使用了泛型，但这并不是泛型方法
public T fun(T t){           // 可以接收任意类型的数据
    return t;                // 直接把参数返回
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;加static会编译错误&lt;/p&gt;
&lt;h3&gt;泛型方法&lt;/h3&gt;
&lt;p&gt;可以加static&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;// 这个&amp;lt;T&amp;gt;修饰的方法才是真的泛型方法
public static &amp;lt;T&amp;gt; T fun2(T t){          // 可以接收任意类型的数据
    return t;                           // 直接把参数返回
}
&lt;/code&gt;&lt;/pre&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;&lt;strong&gt;泛型方法可以加static,普通的泛型类的方法是不可以的&lt;/strong&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;/li&gt;
&lt;li&gt;泛型方法支持static&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;spring注解、事务传播机制&lt;/h2&gt;
&lt;h3&gt;&lt;strong&gt;7种事务的传播机制（可通过spring配置或注解来设置）&lt;/strong&gt;&lt;/h3&gt;
&lt;ol&gt;
&lt;li&gt;REQUIRED（默认）：支持使用当前事务，如果当前事务不存在，创建一个新事务。&lt;/li&gt;
&lt;li&gt;SUPPORTS：支持使用当前事务，如果当前事务不存在，则不使用事务。&lt;/li&gt;
&lt;li&gt;MANDATORY：中文翻译为强制，支持使用当前事务，如果当前事务不存在，则抛出Exception。&lt;/li&gt;
&lt;li&gt;REQUIRES_NEW：创建一个新事务，如果当前事务存在，把当前事务挂起。&lt;/li&gt;
&lt;li&gt;NOT_SUPPORTED：无事务执行，如果当前事务存在，把当前事务挂起。&lt;/li&gt;
&lt;li&gt;NEVER：无事务执行，如果当前有事务则抛出Exception。&lt;/li&gt;
&lt;li&gt;NESTED：嵌套事务，如果当前事务存在，那么在嵌套的事务中执行。如果当前事务不存在，则表现跟REQUIRED一样。&lt;/li&gt;
&lt;/ol&gt;
&lt;h3&gt;事务注解失效的例子&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;
public class WePageManagerServiceImpl implements WePageManagerService {

    @Override
    public void pagePublish(MysRequest request) {
        PublishHandler(request);
    }

    @Transactional(rollbackFor = Exception.class)
    protected ProcResult&amp;lt;String&amp;gt; PublishHandler(WePagePublishRequest request) {
        //多表dao操作......
    }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;解决方案：把事务代码下沉,用一个类去单独处理&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;public class WePageManagerServiceImpl implements WePageManagerService {

    @Autowired
    private MyTransaction myTransaction;

    @Override
    public void pagePublish(MysRequest request) {
       myTransaction.pagePublishHandler(request);
    }
}

@Service
@Transactional(rollbackFor = Throwable.class)
public class MyTransactionImpl implements MyTransaction{
    protected ProcResult&amp;lt;String&amp;gt; pagePublishHandler(WePagePublishRequest request) {
        //多表dao操作......
    }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;@Transactional&lt;strong&gt;失效原因分析：自身调用导致失败&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;在应用系统调用声明@Transactional 的目标方法时，Spring Framework 默认使用 AOP 代理，在代码运行时生成一个代理对象，再由这个代理对象来统一管理，当在Service实现类直接调用内部方法时，其本质是通过this对象来调用的方法，而不是代理对象，因为会出现事务失效的情况&lt;/p&gt;
&lt;p&gt;总结一句话，&lt;strong&gt;自身调用没有经过 Spring 的代理类&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;事务失效3种常见原因&lt;/strong&gt;&lt;/p&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;p&gt;&lt;a href=&quot;https://mp.weixin.qq.com/s?__biz=MzA4NzQ0Njc4Ng==&amp;amp;mid=2247485704&amp;amp;idx=1&amp;amp;sn=403faac580b5a4df8c0ad52d488fb59d&amp;amp;chksm=90380d65a74f847352f3792aeefd09f34cf28f082400917f9b1fdf26cfac435a54316eb0b328&amp;amp;scene=126&amp;amp;sessionid=1587719861&amp;amp;key=7a384acef7f5f4afc36c937135ad3926050d02aac9b3af4026ce6c49b54f270785f99728e6452babd861dad566b8da26113447843f7b3af55909d5433fc983cb16da5e89a312efa70eeb8f6929f428cf&amp;amp;ascene=1&amp;amp;uin=Mjg5MjMxNTQxMA%3D%3D&amp;amp;devicetype=Windows+10&amp;amp;version=62080079&amp;amp;lang=zh_CN&amp;amp;exportkey=A11GMdesik4lH9%2FEA1zySOc%3D&amp;amp;pass_ticket=cCRSLB2olzvtMXc6W%2FhQRmQOmaS%2BKEBVtkxlBUyxsIgb%2BL42ZloZw3WsR1GhVEfM&quot;&gt;参考内容-微信&lt;/a&gt;&lt;/p&gt;
</content:encoded></item><item><title>Java并发-AQS-ReentrantLock</title><link>https://gilgameshzzz.github.io/posts/java%E5%B9%B6%E5%8F%91aqs/</link><guid isPermaLink="true">https://gilgameshzzz.github.io/posts/java%E5%B9%B6%E5%8F%91aqs/</guid><description>在synchronized未优化之前，我们在编码中使用最多的同步工具类应该是ReentrantLock类，ReentrantLock拥有优化后synchronized关键字的性能，又提供了更多的灵活性。相比synchro…</description><pubDate>Wed, 22 Apr 2020 00:00:00 GMT</pubDate><content:encoded>&lt;h2&gt;前言&lt;/h2&gt;
&lt;p&gt;在&lt;code&gt;synchronized&lt;/code&gt;未优化之前，我们在编码中使用最多的同步工具类应该是&lt;code&gt;ReentrantLock&lt;/code&gt;类，&lt;code&gt;ReentrantLock&lt;/code&gt;拥有优化后&lt;code&gt;synchronized&lt;/code&gt;关键字的性能，又提供了更多的灵活性。相比&lt;code&gt;synchronized&lt;/code&gt;，他在功能上更加强大，具有等待可中断，公平锁以及绑定多个条件等&lt;code&gt;synchronized&lt;/code&gt;不具备的功能。&lt;/p&gt;
&lt;h2&gt;ReentrantLock 与AQS(AbstractQueuedSynchronizer)的关系&lt;/h2&gt;
&lt;p&gt;在使用ReentrantLock 类时，第一步实例化 &lt;strong&gt;new ReentrantLock()&lt;/strong&gt;,他的实例化源码&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;public ReentrantLock() {
    sync = new NonfairSync();
}

public ReentrantLock(boolean fair) {
    sync = fair ? new FairSync() : new NonfairSync();
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;通过名字可以看出是公平锁和非公平锁的实现，默认情况下ReentrantLock使用非公平锁，那么sync字段是怎么实现的？看sync的代码&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;private final Sync sync;

abstract static class Sync extends AbstractQueuedSynchronizer {......}

static final class NonfairSync extends Sync {......}

static final class FairSync extends Sync {......}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;到这里就发现了&lt;code&gt;AbstractQueuedSynchronizer&lt;/code&gt;类，公平锁和非公平锁其实都是在&lt;code&gt;AbstractQueuedSynchronizer&lt;/code&gt;的基础上实现的，也就是AQS。AQS提供了&lt;code&gt;ReentrantLock&lt;/code&gt;实现的基础。&lt;/p&gt;
&lt;h3&gt;AQS&lt;/h3&gt;
&lt;p&gt;AQS全称&lt;code&gt;AbstractQueuedSynchronizer&lt;/code&gt;，翻译为抽象队列同步器，他是构建JUC包下并发工具类的基础框架，提供了可中断锁，超时锁，独占锁，共享锁等等，它没有利用高级机器指令，也没有利用JDK编译时的特殊处理，仅仅是一个普通的类，就实现了并发的控制。&lt;/p&gt;
&lt;h4&gt;功能特点：&lt;/h4&gt;
&lt;p&gt;&lt;strong&gt;1、等待中断&lt;/strong&gt;。synchronized不可以被中断，指的是synchronized等待不可中断，一旦进入阻塞状态，就无法被中断。只能通过调用的方法抛出InterruptedException异常，那么它就可以被中断，不抛出InterruptedException的方法是不可中断的。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;2、锁超时&lt;/strong&gt;。AQS支持超时锁，可以指定一个时间，如果指定时间内没有获取锁，就直接退出获取锁。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;3、非阻塞&lt;/strong&gt;。尝试获取锁失败，并不进入阻塞状态，而是直接返回，那这个线程也有机会释放曾经持有的锁。&lt;/p&gt;
&lt;p&gt;AQS底层采用的是状态标志位（state变量）+ FIFO队列的方式来记录获取锁、释放锁、竞争锁等一系列操作。对于AQS而言,其中的&lt;strong&gt;state变量可以看做是锁&lt;/strong&gt;，队列采用的是先进先出的双向链表，&lt;strong&gt;state共享状态变量表示锁状态&lt;/strong&gt;，内部使用CAS对state进行原子操作修改来完成锁状态变更（锁的持有和释放）。&lt;br /&gt;
&lt;img src=&quot;/img/AQS.png&quot; alt=&quot;&quot; /&gt;&lt;/p&gt;
&lt;h3&gt;AQS核心&lt;/h3&gt;
&lt;h4&gt;状态state&lt;/h4&gt;
&lt;pre&gt;&lt;code&gt;private volatile int state;    
protected final int getState() {        
  return state;    
}    

protected final void setState(int newState) {        
  state = newState;    
}    

protected final boolean compareAndSetState(int expect, int update) {        
    return unsafe.compareAndSwapInt(this, stateOffset, expect, update);    
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;state状态这里还是比较简单的，使用volatile修饰，保证state变量的可见性， setState(int newState)方法只是用作给state进行初始化，而compareAndSetState(int expect, int update)用作了在运行期间对state变量的修改。&lt;/p&gt;
&lt;p&gt;为什么要单独多出来一个compareAndSetState方法对state变量进行修改呢？&lt;/p&gt;
&lt;p&gt;因为对共享变量的赋值，不是原子操作需要额外的锁同步，我们可能想到使用synchronized来保证原子性，但是&lt;strong&gt;synchronizedh会使线程阻塞&lt;/strong&gt;，导致线程上下文的切换，影响其性能。这里采用的是CAS无锁操作，但是CAS也是有不足的，它会进行自旋操作，这样也会对CPU的资源造成浪费。&lt;/p&gt;
&lt;h4&gt;同步队列FIFO&lt;/h4&gt;
&lt;p&gt;AQS会把没有争抢到锁的线程包装成Node节点，加入队列中：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;static final class Node {   
    //标记节点是共享模式    
    static final Node SHARED = new Node();    
 	//标记节点是独占的    
    static final Node EXCLUSIVE = null;   
    //代表此节点的线程取消了争抢资源    
    static final int CANCELLED =  1;     
    //表示当前node的后继节点对应的线程需要被唤醒    
    static final int SIGNAL    = -1;    
    //这两个状态和condition有关系，这里先不说condition    
    static final int CONDITION = -2;       
    static final int PROPAGATE = -3;   
    // 取值为上面的1、-1、-2、-3 或者 0    
    volatile int waitStatus;    
    volatile Node prev;    
    volatile Node next;   
    //等待线程   
 	volatile Thread thread;    
    Node nextWaiter;    
    final boolean isShared() {        
       return nextWaiter == SHARED;    
    }    
    final Node predecessor() throws NullPointerException {        
        Node p = prev;        
        if (p == null)           
            throw new NullPointerException();       
        else            
            return p;    
    }  
    
    Node() {       
    }    
    
    //线程入队。    
    Node(Thread thread, Node mode) {    
        // Used by addWaiter        
        this.nextWaiter = mode;        
        this.thread = thread;    
    }  
    //使用condition用到    
    Node(Thread thread, int waitStatus) { 
        // Used by Condition       
        this.waitStatus = waitStatus;       
        this.thread = thread;    
    }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;同步队列是AQS的核心，用来实现线程的阻塞和唤醒操作，waitStatus它表示了当前Node所代表的线程的等待锁的状态，在独占锁模式下，我们只需要关注CANCELLED、SIGNAL两种状态即可。nextWaiter属性，它在独占锁模式下永远为null，仅仅起到一个标记作用。&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;/img/queue.png&quot; alt=&quot;&quot; /&gt;&lt;/p&gt;
&lt;h4&gt;独占锁、共享锁&lt;/h4&gt;
&lt;p&gt;AQS定义两种资源共享方式&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Exclusive （独占模式）：只有一个线程能访问共享资源。如 ReentrantLock&lt;/li&gt;
&lt;li&gt;Share（共享模式）：多个线程可同时访问共享资源，如Semaphore/CountDownLatch）&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;在独占模式下和synchronized实现的效果是一样的，一次只能有一个线程访问。state 等于0 代表没有线程持有锁，大于0代表有线程持有当前锁。这个值可以大于1，是因为&lt;strong&gt;锁可以重入，每次重入都加上 1，也需要对应的多次释放&lt;/strong&gt;。即上锁多少次，就要解锁多少次。&lt;/p&gt;
&lt;p&gt;在共享模式下，state的值代表着有多少个许可，但是它在每个具体的工具类里的应用还是有一些差别的。&lt;/p&gt;
&lt;h3&gt;ReentrantLock独占锁&lt;/h3&gt;
&lt;p&gt;实现锁&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;//ReentrantLock的lock方法
public void lock() {
    sync.lock();
}

//调用了Sync中的lock抽象方法
abstract static class Sync extends AbstractQueuedSynchronizer {
    ......
    /**
        * Performs {@link Lock#lock}. The main reason for subclassing
        * is to allow fast path for nonfair version.
        */
    abstract void lock();
    ......
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;code&gt;Sync&lt;/code&gt;类的&lt;code&gt;lock()&lt;/code&gt;方法是一个抽象方法，&lt;code&gt;NonfairSync()&lt;/code&gt;和&lt;code&gt;FairSync()&lt;/code&gt;分别对&lt;code&gt;lock()&lt;/code&gt;方法进行了实现。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;//非公平锁的lock实现
static final class NonfairSync extends Sync {
    ......
    /**
        * Performs lock.  Try immediate barge, backing up to normal
        * acquire on failure.
        */
    final void lock() {
        if (compareAndSetState(0, 1)) //插队操作，首先尝试CAS获取锁，0为锁空闲
            setExclusiveOwnerThread(Thread.currentThread()); //获取锁成功后设置当前线程为占有锁线程
        else
            acquire(1);
    }
    ......
}

//公平锁的lock实现
static final class FairSync extends Sync {
    ......
    final void lock() {
        acquire(1);
    }
    ......
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;strong&gt;区别&lt;/strong&gt;：&lt;code&gt;NonfairSync()&lt;/code&gt;会先进行一个CAS操作，将一个state状态从0设置到1，这个也就是上面所说的非公平锁的“插队”操作，前面讲过CAS操作默认是原子性的，这样就保证了设置的线程安全性。&lt;/p&gt;
&lt;p&gt;看看跟state有关的源码：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;protected final boolean compareAndSetState(int expect, int update) {
    // See below for intrinsics setup to support this
    return unsafe.compareAndSwapInt(this, stateOffset, expect, update);
}

/**
    * The synchronization state.
    */
private volatile int state;

protected final int getState() {
    return state;
}

protected final void setState(int newState) {
    state = newState;
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;state变量是一个&lt;code&gt;volatile&lt;/code&gt;修饰的&lt;code&gt;int&lt;/code&gt;类型变量，这样就保证了这个变量在多线程环境下的可见性。从变量的注释“The synchronization state”可以看出state代表了一个同步状态。再回到上面的&lt;code&gt;lock()&lt;/code&gt;方法，在设置成功之后，调用了&lt;code&gt;setExclusiveOwnerThread&lt;/code&gt;方法将当前线程设置给了一个私有的变量，这个变量代表了当前获取锁的线程，放到了AQS的父类&lt;code&gt;AbstractOwnableSynchronizer&lt;/code&gt;类中实现。&lt;/p&gt;
&lt;h3&gt;总结&lt;/h3&gt;
&lt;p&gt;1、AQS中用state属性表示锁，在ReentranLock中当state = 1 获取锁，state = 0代表释放锁， state&amp;gt;1代表重入锁。exclusiveOwnerThread属性代表了占有锁的线程。&lt;/p&gt;
&lt;p&gt;2、addWaiter负责将当前等待锁的线程包装成Node,并成功地添加到队列的末尾，这一点是由它调用的enq方法保证的，enq方法同时还负责在队列为空时初始化队列。&lt;/p&gt;
&lt;p&gt;3、acquireQueued方法用于在Node成功入队后，继续尝试获取锁（取决于Node的前驱节点是不是head），或者将线程挂起&lt;/p&gt;
&lt;p&gt;4、shouldParkAfterFailedAcquire方法用于保证当前线程的前驱节点的waitStatus属性值为SIGNAL,从而保证了自己挂起后，前驱节点会负责在合适的时候唤醒自己。&lt;/p&gt;
&lt;p&gt;5、parkAndCheckInterrupt方法用于挂起当前线程，并检查中断状态。&lt;/p&gt;
&lt;p&gt;6、如果最终成功获取了锁，线程会从lock()方法返回，继续往下执行；否则，线程会阻塞等待。&lt;/p&gt;
&lt;p&gt;&lt;a href=&quot;https://juejin.cn/post/5b7235e951882560ed075893#heading-2&quot;&gt;参考链接1-掘金&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;&lt;a href=&quot;https://juejin.cn/post/5e9d3f01518825739b2d4866#heading-16&quot;&gt;参考链接2-掘金&lt;/a&gt;&lt;/p&gt;
</content:encoded></item><item><title>二叉树算法题</title><link>https://gilgameshzzz.github.io/posts/%E6%90%9C%E7%B4%A2%E4%BA%8C%E5%8F%89%E6%A0%91%E7%AE%97%E6%B3%95/</link><guid isPermaLink="true">https://gilgameshzzz.github.io/posts/%E6%90%9C%E7%B4%A2%E4%BA%8C%E5%8F%89%E6%A0%91%E7%AE%97%E6%B3%95/</guid><description>例如 数组posArr= 2,4,3,6,7,8,5,建出二叉树：!</description><pubDate>Mon, 20 Apr 2020 00:00:00 GMT</pubDate><content:encoded>&lt;p&gt;**二叉搜索树（BST）**又称二叉查找树或二叉排序树。若它的左子树不空，则左子树上所有结点的值均小于它的根结点的值； 若它的右子树不空，则右子树上所有结点的值均大于它的根结点的值； 它的左、右子树也分别为二叉搜索&lt;/p&gt;
&lt;h2&gt;问题：已知一个搜索二叉树，后序遍历的数组posArr，请根据posArr,重建出整棵数，返回新建树的头节点&lt;/h2&gt;
&lt;p&gt;例如 数组posArr= [2,4,3,6,7,8,5],建出二叉树：&lt;img src=&quot;/img/posArr%E4%BA%8C%E5%8F%89%E6%A0%91.png&quot; alt=&quot;&quot; /&gt;&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;public class PosArrayToBST {
    public static class Node{
        public int value;
        public Node left;
        public Node right;
        public Node(int v){
            value = v;
        }
    }

    public static Node startPosArrayToBST(int[] posArr){
       return process(posArr, 0, posArr.length-1);
    }
    // 使用posArr[L....R]这些数字，来建树，返回建好的树的头节点
    // 递归
    public static Node process(int[] posArr, int L, int R){
        if (L&amp;gt;R){
            return null;
        }
        // 头节点的数为R的数，即posArr最后的数为头节点
        Node head = new Node(posArr[R]);
        if (L == R){   // 说明该树只有一个节点
            return head;
        }
        int  M = L -1;
        int left = L;
        int right = R;
        while (left &amp;lt;= right){
            // 等同于 mid = (L+R)/2,之所以用位移，是因为位移更快，除法底层更复杂
            // int 范围 -21亿~21亿，L+R过大时有溢出风险， 所以用 L + （R - L）
            int mid = left + ((right - left) &amp;gt;&amp;gt; 1);
            if (posArr[mid] &amp;lt; posArr[R]){
                M = mid;
                left = mid +1;
            }else {
                right = mid - 1;
            }
        }
        head.left = process(posArr, L, M);
        head.right = process(posArr, M+1, R-1);
        return head;
    }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;M = L -1 原因：当二叉树既有左子树，又有右子树，M会改变，初始值无所谓，但当二叉树只有左边时（[1,2,3,4,5]），M最后的结果就为R-1，后续的递归&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;head.right = process(posArr, M+1, R-1);
// M = R -1 ;上面的结构就变为：
head.right = process(posArr, R, R-1); // 无效head.right = null;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;右边同理。等同于&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;if(M == -1){
    head.right = process(posArr, L, R-1);
}else if(M == R-1){
    head.left = process(posArr, L, R-1);
}else{
    head.left = process(posArr, L, M);
    head.right = process(posArr, M+1, R-1);
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;以上算法，最坏情况下时间复杂度 O(N * ㏒₂N)&lt;/p&gt;
&lt;h2&gt;问题二：给定长度为m的字符串aim,以及一个长度为n的字符串str。问能否在str中找到一个长度为m的连续子串，使得这个子串刚好由aim的m个字符组成，顺序无所谓，返回任意满足条件的一个子串的起始位置，未找到返回-1。&lt;/h2&gt;
&lt;pre&gt;&lt;code&gt;public class containExactly {
    // 时间复杂度 O（N^3 * logN） 不行
    public static int wayOne(String s, String a){
        if(s == null || a == null || s.length() &amp;lt; a.length()){
            return -1;
        }
        char[] aim = a.toCharArray();
        System.out.println(&quot;aim 的值为&quot; + aim);
        Arrays.sort(aim);
        // aim 排完序
        String aimSort = String.valueOf(aim);
        for(int L = 0; L&amp;lt; s.length(); L++){
            for(int R = L; R &amp;lt; s.length(); R++){
                // substring 获取的范围是 [ )
                char[] cur = s.substring(L, R+1).toCharArray();
                Arrays.sort(cur);
                System.out.println(cur);
                String curSort = String.valueOf(cur);
                if (curSort.equals(aimSort)){
                    return L;
                }
            }
        }
        return -1;
    }
    // 下面这个复杂度低
    public static int wayTwo(String s, String a){
        if(s == null || a == null || s.length() &amp;lt; a.length()){
            return -1;
        }
        char[] aim = a.toCharArray();
        int[] count = new int[256];
        int M = aim.length;
        char[] str = s.toCharArray();
        for (int i = 0; i &amp;lt; M; i++) {
            count[aim[i]] ++;
        }
        int inValidTimes = 0, R = 0;
        // 先让窗口拥有M个字符
        for (; R&amp;lt; M; R++){
            if(count[str[R]]-- &amp;lt;=0){
                inValidTimes++;
            }
        }
        System.out.println(inValidTimes);
        // R==M  [0...M-1]
        // 第一次形成长度为M 的窗口
        for(; R &amp;lt; str.length; R++){
            if (inValidTimes == 0){
                return R-M;
            }
            if (count[str[R]]-- &amp;lt;= 0){
                inValidTimes++;
            }
            if (count[str[R - M]]++ &amp;lt; 0){
                inValidTimes--;
            }
        }
        return inValidTimes == 0 ? R - M : -1;
    }
    public static void main(String[] args) {
        String string = &quot;abcdabcdsfcas&quot;;
        String a = &quot;abc&quot;;
        int result = wayTwo(string, a);
        System.out.println(result);
    }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;方法二解释：假设 aim = “acabb”, str = “caabcb”, M为aim的长度，值为5，count 记录aim每一个字符出现的次数，即第一个for循环。第二个for循环， str 前 M个字符在count列表出现的次数，刚好减掉aim在count列表里次数的值，则说明str前M个字符符合要求，如果不符合则进入第三个for循环，判断str中[1…M]的字符是否满足要求，否则再次进入第三个for循环，判断str中的[2….M+1]字符是否满足要求。&lt;/p&gt;
</content:encoded></item><item><title>计算机缓存的替换策略（算法）</title><link>https://gilgameshzzz.github.io/posts/%E7%BC%93%E5%AD%98%E6%9B%BF%E6%8D%A2%E7%AD%96%E7%95%A5/</link><guid isPermaLink="true">https://gilgameshzzz.github.io/posts/%E7%BC%93%E5%AD%98%E6%9B%BF%E6%8D%A2%E7%AD%96%E7%95%A5/</guid><description>def str(self): val = f&apos;{{{self.key} : {self.value}}}&apos; return val</description><pubDate>Fri, 17 Apr 2020 00:00:00 GMT</pubDate><content:encoded>&lt;p&gt;目前常见的替换策略有随机、FIFO、LFU、LRU 四种。下面三种（FIFO / LFU / LRU）都依赖双向链表做 O(1) 的节点移动，所以先把它实现出来。&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;随机算法（Random）本身没什么可实现的——命中淘汰时从缓存里随便挑一个踢掉即可，不需要维护任何顺序结构，实现最简单但命中率也最差，这里不单独展开。&lt;/p&gt;
&lt;/blockquote&gt;
&lt;h3&gt;基础：双向链表 python 实现&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;class Node:
    def __init__(self, key, value):
        self.key = key
        self.value = value
        self.prev = None
        self.next = None
    
    def __str__(self):
        val = f&apos;{{{self.key} : {self.value}}}&apos;
        return val
    
    def __repr__(self):
        val = f&apos;{{{self.key} : {self.value}}}&apos;
        return val


class DoubleLinkedList:
    def __init__(self, capacity=0xffff):
        self.capacity = capacity
        self.head = None
        self.tail = None
        self.size = 0
    
    # 从头部添加
    def add_head(self, node: Node):
        if not self.head:
            self.head = node
            self.tail = node
            self.head.next = None
            self.head.prev = None
        else:
            node.next = self.head
            self.head.prev = node
            self.head = node
            self.head.prev = None
        self.size += 1
        return node
    
    # 从尾部添加
    def add_tail(self, node: Node):
        if not self.tail:
            self.tail = node
            self.head = node
            self.tail.next = None
            self.tail.prev = None
        else:
            self.tail.next = node
            node.prev = self.tail
            self.tail = node
            self.tail.next = None
        self.size += 1
        return node
    
    # 从尾部删除
    def del_tail(self):
        if not self.tail:
            return
        node = self.tail
        if node.prev:
            self.tail = node.prev
            self.tail.next = None
        else:
            self.tail = self.head = None
        self.size -= 1
        return node
    
    # 从头部删除
    def del_head(self):
        if not self.head:
            return
        node = self.head
        if node.next:
            self.head = node.next
            self.head.prev = None
        else:
            self.tail = self.head = None
        self.size -= 1
        return node
    
    # 从任意节点删除
    def remove(self, node: Node):
        # 如果node= None ,默认删除尾部节点
        if not node:
            node = self.tail
        if node == self.tail:
            self.del_tail()
        elif node == self.head:
            self.del_head()
        else:
            node.prev.next = node.next
            node.next.prev = node.prev
            self.size -= 1
        return node
    
    def print(self):
        p = self.head
        line = &quot;&quot;
        while p:
            line += &quot;%s&quot; % p
            p = p.next
            if p:
                line += &quot;=&amp;gt;&quot;
        print(line)
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;先进先出算法（FIFO）&lt;/h3&gt;
&lt;p&gt;✦把缓存看作是一个先进先出的队列&lt;/p&gt;
&lt;p&gt;✦优先替换最先进入队列的字块&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;/img/FIFO.png&quot; alt=&quot;&quot; /&gt;&lt;/p&gt;
&lt;p&gt;python 实现代码&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;from DoubleLinkedList import DoubleLinkedList, Node


class FIFOCache():
    def __init__(self, capacity):
        self.capacity = capacity
        self.size = 0
        self.map = {}
        self.list = DoubleLinkedList(self.capacity)
    
    def get(self, key):
        if key not in self.map:
            return -1
        else:
            node: Node = self.map.get(key)
            return node.value
    
    def put(self, key, value):
        if self.capacity == 0:
            return &quot;容量为0&quot;
        if key in self.map:
            node: Node = self.map.get(key)
            self.list.remove(node)
            node.value = value
            self.list.add_tail(node)
        else:
            if self.size == self.capacity:
                node = self.list.del_tail()
                del self.map[node.key]
                self.size -= 1
            node = Node(key, value)
            self.list.add_tail(node)
            self.map[key] = node
            self.size += 1
    
    def print(self):
        return self.list.print()
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;最不经常使用算法（LFU）&lt;/h3&gt;
&lt;p&gt;✦优先淘汰最不经常使用的字块&lt;/p&gt;
&lt;p&gt;✦需要额外的空间记录字块的使用频率&lt;/p&gt;
&lt;p&gt;✦ 同频率节点按FIFO算法淘汰&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;/img/LFU.png&quot; alt=&quot;&quot; /&gt;&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;/img/LFU2.png&quot; alt=&quot;&quot; /&gt;&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;from DoubleLinkedList import DoubleLinkedList, Node


class LFUNode(Node):
    def __init__(self, key, value):
        self.freq = 0  # 频率
        super(LFUNode, self).__init__(key, value)


class LFUCache():
    def __init__(self, capacity):
        self.capacity = capacity
        self.map = {}
        # key :频率  value: 频率对应的双向链表
        self.freq_map = dict()
        self.size = 0
    
    # 更新节点频率的操作
    def update_freq(self, node: LFUNode):
        freq = node.freq
        # 删除
        node = self.freq_map[freq].remove(node)
        if self.freq_map[freq].size == 0:
            del self.freq_map[freq]
        # 更新
        freq += 1
        node.freq = freq
        if freq not in self.freq_map:
            self.freq_map[freq] = DoubleLinkedList()
        self.freq_map[freq].add_tail(node)
    
    def get(self, key):
        if key not in self.map:
            return -1
        node = self.map.get(key)
        self.update_freq(node)
        return node.value
    
    def put(self, key, value):
        if self.capacity == 0:
            return &quot;容量为0&quot;
        # 缓存命中
        if key in self.map:
            node = self.map.get(key)
            node.value = value
            self.update_freq(node)
        # 缓存没命中
        else:
            if self.capacity == self.size:
                min_freq = min(self.freq_map)
                node = self.freq_map[min_freq].del_tail()
                del self.map[node.key]
                self.size -= 1
            
            node = LFUNode(key, value)
            node.freq = 1
            self.map[key] = node
            if node.freq not in self.freq_map:
                self.freq_map[node.freq] = DoubleLinkedList()
            node = self.freq_map[node.freq].add_tail(node)
            self.size += 1
    
    def print(self):
        print(&quot;***************&quot;)
        for k, v in self.freq_map.items():
            print(f&quot;频率= {k}&quot;)
            self.freq_map[k].print()
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;最近最少使用算法（LRU）&lt;/h3&gt;
&lt;p&gt;✦优先淘汰一段时间内没有使用的字块&lt;/p&gt;
&lt;p&gt;✦有多种实现方法，一般使用双向链表&lt;/p&gt;
&lt;p&gt;✦把当前访问节点置于链表前面（保证链表头部节点是最近使用的）&lt;/p&gt;
&lt;p&gt;✦缓存淘汰时，把链表尾部的节点淘汰即可&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;/img/LRU%E5%88%97%E5%AD%90.png&quot; alt=&quot;&quot; /&gt;&lt;/p&gt;
&lt;p&gt;python 代码实现&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;from DoubleLinkedList import DoubleLinkedList, Node

class LRUCache():
    def __init__(self, capacity):
        self.capacity = capacity
        self.map = {}
        self.list = DoubleLinkedList(self.capacity)
    
    def get(self, key):
        if key in self.map:
            node: Node = self.map[key]
            self.list.remove(node)
            self.list.add_head(node)
            return node.value
        else:
            return -1
    
    def put(self, key, value):
        if key in self.map:
            node: Node = self.map.get(key)
            self.list.remove(node)
            node.value = value
            self.list.add_head(node)
        else:
            node = Node(key, value)
            # 缓存已经满了
            if self.list.capacity &amp;lt;= self.list.size:
                old_node = self.list.del_tail()
                self.map.pop(old_node.key)
        
            # 缓存没满
            self.list.add_head(node)
            self.map[key] = node
    
    def print(self):
        self.list.print()
&lt;/code&gt;&lt;/pre&gt;
&lt;h4&gt;LRU 问题：时间复杂度？ 多线程下怎么优化?&lt;/h4&gt;
</content:encoded></item><item><title>初识Netty</title><link>https://gilgameshzzz.github.io/posts/netty/</link><guid isPermaLink="true">https://gilgameshzzz.github.io/posts/netty/</guid><description>1、I/O 模型简单的理解：就是用什么样的通道进行数据的发送和接收，很大程度上决定了程序通信的性能</description><pubDate>Wed, 15 Apr 2020 00:00:00 GMT</pubDate><content:encoded>&lt;h3&gt;I/O 模型基本说明&lt;/h3&gt;
&lt;p&gt;1、I/O 模型简单的理解：就是用什么样的通道进行数据的发送和接收，很大程度上决定了程序通信的性能&lt;/p&gt;
&lt;p&gt;2、Java共支持3种网络编程模型/IO模式：BIO、NIO、AIO&lt;/p&gt;
&lt;h4&gt;BIO&lt;/h4&gt;
&lt;p&gt;3、Java BIO ： 同步并阻塞(&lt;strong&gt;传统阻塞型&lt;/strong&gt;)，服务器实现模式为一个连接一个线程，即客户端有连接请求时服务器端就需要启动一个线程进行处理，如果这个连接不做任何事情会造成不必要的线程开销&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;/img/BIO%E5%B7%A5%E4%BD%9C%E5%8E%9F%E7%90%86.png&quot; alt=&quot;&quot; /&gt;&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;BIO流程：&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;1)服务器端启动一个ServerSocket&lt;/p&gt;
&lt;p&gt;2)客户端启动Socket对服务器进行通信，默认情况下服务器端需要对每个客户 建立一个线程与之通讯&lt;/p&gt;
&lt;p&gt;3)客户端发出请求后, 先咨询服务器是否有线程响应，如果没有则会等待，或者被拒绝&lt;/p&gt;
&lt;p&gt;4)如果有响应，客户端线程会等待请求结束后，在继续执行&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;缺点分析：&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;1)每个请求都需要创建独立的线程，与对应的客户端进行数据 Read，业务处理，数据 Write 。&lt;/p&gt;
&lt;p&gt;2)当并发数较大时，需要&lt;strong&gt;创建大量线程来处理连接&lt;/strong&gt;，系统资源占用较大。&lt;/p&gt;
&lt;p&gt;3)连接建立后，如果当前线程暂时没有数据可读，则线程就阻塞在 Read 操作上，造成线程资源浪费&lt;/p&gt;
&lt;h4&gt;NIO&lt;/h4&gt;
&lt;p&gt;4、Java NIO ： &lt;strong&gt;同步非阻塞&lt;/strong&gt;，服务器实现模式为一个线程处理多个请求(连接)，即客户端发送的连接请求都会注册到多路复用器上，多路复用器轮询到连接有I/O请求就进行处理 【简单示意图】&lt;/p&gt;
&lt;p&gt;①NIO 有三大核心部分：Channel(通道)&lt;strong&gt;，&lt;strong&gt;Buffer(缓冲区)&lt;/strong&gt;,&lt;/strong&gt; Selector(选择器）&lt;/p&gt;
&lt;p&gt;②NIO是 面向 缓冲区 ，或者面向块 编程的。数据读取到一个它稍后处理的缓冲区，需要时可在缓冲区中前后移动，这就增加了处理过程中的灵活性，使用它可以提供&lt;strong&gt;非阻塞&lt;/strong&gt;式的高伸缩性网络&lt;/p&gt;
&lt;p&gt;③HTTP2.0使用了多路复用的技术，做到同一个连接并发处理多个请求，而且并发请求的数量比HTTP1.1大了好几个数量级&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;/img/NIO.png&quot; alt=&quot;NIO&quot; /&gt;&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;NIO存在的问题&lt;/strong&gt;&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;
&lt;p&gt;NIO 的类库和 API 繁杂，使用麻烦：需要熟练掌握 Selector、ServerSocketChannel、SocketChannel、ByteBuffer 等。&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;需要具备其他的额外技能：要熟悉 Java 多线程编程，因为 NIO 编程涉及到 Reactor 模式，你必须对多线程和网络编程非常熟悉，才能编写出高质量的 NIO 程序。&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;开发工作量和难度都非常大：例如客户端面临断连重连、网络闪断、半包读写、失败缓存、网络拥塞和异常流的处理等等。&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;JDK NIO 的 Bug：例如臭名昭著的 Epoll Bug，它会导致 Selector 空轮询，最终导致 CPU 100%。直到 JDK 1.7 版本该问题仍旧存在，没有被根本解决&lt;/p&gt;
&lt;/li&gt;
&lt;/ol&gt;
&lt;h4&gt;AIO&lt;/h4&gt;
&lt;p&gt;5、Java AIO(NIO.2) ： &lt;strong&gt;异步非阻塞&lt;/strong&gt;，AIO 引入异步通道的概念，采用了 Proactor 模式，简化了程序编写，有效的请求才启动线程，它的特点是先由操作系统完成后才通知服务端程序启动线程去处理，一般适用于连接数较多且连接时间较长的应用&lt;/p&gt;
&lt;p&gt;①、JDK 7 引入了 Asynchronous I/O，即 AIO。在进行 I/O 编程中，常用到两种模式：Reactor和 Proactor。Java 的 NIO 就是 Reactor，当有事件触发时，服务器端得到通知，进行相应的处理&lt;/p&gt;
&lt;p&gt;②、AIO 即 NIO2.0，叫做异步不阻塞的 IO。AIO 引入异步通道的概念，采用了 Proactor 模式，简化了程序编写，有效的请求才启动线程，它的特点是先由操作系统完成后才通知服务端程序启动线程去处理，一般适用于连接数较多且连接时间较长的应用&lt;/p&gt;
&lt;p&gt;③、目前 AIO 还没有广泛应用，Netty 也是基于NIO, 而不是AIO&lt;/p&gt;
&lt;h3&gt;线程模型&lt;/h3&gt;
&lt;h4&gt;传统阻塞I/O模型&lt;/h4&gt;
&lt;p&gt;&lt;img src=&quot;/img/%E4%BC%A0%E7%BB%9FIO.png&quot; alt=&quot;&quot; /&gt;&lt;/p&gt;
&lt;p&gt;黄色的框表示对象， 蓝色的框表示线程，白色的框表示方法(API)。&lt;/p&gt;
&lt;p&gt;**存在的问题：**1、当并发数很大，就会创建大量的线程，占用很大系统资源&lt;/p&gt;
&lt;p&gt;2、连接创建后，如果当前线程暂时没有数据可读，该线程会阻塞在read 操作，造成线程资源浪费&lt;/p&gt;
&lt;h4&gt;Reactor&lt;/h4&gt;
&lt;p&gt;主要针对传统2个缺点：基于 I/O 复用模型、基于线程池复用线程资源&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Reactor单线程&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;/img/Reactor%E5%8D%95%E7%BA%BF%E7%A8%8B.png&quot; alt=&quot;&quot; /&gt;&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;优缺点：&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;1)&lt;strong&gt;优点：&lt;/strong&gt; 模型简单，没有多线程、进程通信、竞争的问题，全部都在一个线程中完成&lt;/p&gt;
&lt;p&gt;2)**缺点：**性能问题，只有一个线程，无法完全发挥多核 CPU 的性能。Handler 在处理某个连接上的业务时，整个进程无法处理其他连接事件，很容易导致性能瓶颈&lt;/p&gt;
&lt;p&gt;3)**缺点：**可靠性问题，线程意外终止，或者进入死循环，会导致整个系统通信模块不可用，不能接收和处理外部消息，造成节点故障&lt;/p&gt;
&lt;p&gt;4)**使用场景：**客户端的数量有限，业务处理非常快速，比如 Redis在业务处理的时间复杂度 O(1) 的情况&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Reactor多线程&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;/img/Reactor%E5%A4%9A%E7%BA%BF%E7%A8%8B.png&quot; alt=&quot;&quot; /&gt;&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;优缺点&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;1)**优点：**可以充分的利用多核cpu 的处理能力&lt;/p&gt;
&lt;p&gt;2)**缺点：**多线程数据共享和访问比较复杂， reactor 处理所有的事件的监听和响应，在单线程运行， 在高并发场景容易出现性能瓶颈.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Reactor主从模型&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;主从Reactor多线程：多个acceptor的NIO线程池用于接受客户端的连接&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;/img/Reactor%E4%B8%BB%E4%BB%8E%E6%A8%A1%E5%9E%8B.png&quot; alt=&quot;&quot; /&gt;&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;优缺点&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;1)**优点：**父线程与子线程的数据交互简单职责明确，父线程只需要接收新连接，子线程完成后续的业务处理。&lt;/p&gt;
&lt;p&gt;2)**优点：**父线程与子线程的数据交互简单，Reactor 主线程只需要把新连接传给子线程，子线程无需返回数据。&lt;/p&gt;
&lt;p&gt;3)**缺点：**编程复杂度较高&lt;/p&gt;
&lt;p&gt;**结合实例：**这种模型在许多项目中广泛使用，包括 Nginx 主从 Reactor 多进程模型，Memcached 主从多线程，Netty 主从多线程模型的支持&lt;/p&gt;
&lt;h4&gt;种模式用生活案例来理解&lt;/h4&gt;
&lt;p&gt;1)单 Reactor 单线程，前台接待员和服务员是同一个人，全程为顾客服&lt;/p&gt;
&lt;p&gt;2)单 Reactor 多线程，1 个前台接待员，多个服务员，接待员只负责接待&lt;/p&gt;
&lt;p&gt;3)主从 Reactor 多线程，多个前台接待员，多个服务生&lt;/p&gt;
&lt;h3&gt;Netty介绍&lt;/h3&gt;
&lt;p&gt;Netty是一个异步的、基于事件驱动的网络应用框架，用以快速开发高性能、高可靠性的网络 IO 程序。&lt;/p&gt;
&lt;p&gt;Netty主要针对在TCP协议下，面向Clients端的高并发应用，或者Peer-to-Peer场景下的大量数据持续传输的应用。&lt;/p&gt;
&lt;p&gt;Netty本质是一个NIO框架，适用于服务器通讯相关的多种应用场景&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;优点：&lt;/strong&gt; 并发高 - NIO（非阻塞IO）、传输快-零拷贝&lt;/p&gt;
&lt;h3&gt;Netty 的零拷贝&lt;/h3&gt;
&lt;h4&gt;传统意义的拷贝&lt;/h4&gt;
&lt;p&gt;在发送数据的时候，传统的实现方式是：&lt;/p&gt;
&lt;p&gt;File.read(bytes)；Socket.send(bytes)&lt;br /&gt;
这种方式需要四次数据拷贝和四次上下文切换：&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;数据从磁盘读取到内核的read buffer&lt;/li&gt;
&lt;li&gt;数据从内核缓冲区拷贝到用户缓冲区&lt;/li&gt;
&lt;li&gt;数据从用户缓冲区拷贝到内核的socket buffer&lt;/li&gt;
&lt;li&gt;数据从内核的socket buffer拷贝到网卡接口（硬件）的缓冲区&lt;/li&gt;
&lt;/ol&gt;
&lt;h4&gt;零拷贝的概念&lt;/h4&gt;
&lt;p&gt;明显上面的第二步和第三步是没有必要的，通过java的FileChannel.transferTo方法，可以避免上面两次多余的拷贝（当然这需要底层操作系统支持）&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;调用transferTo,数据从文件由DMA引擎拷贝到内核read buffer&lt;/li&gt;
&lt;li&gt;接着DMA从内核read buffer将数据拷贝到网卡接口buffer&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;上面的两次操作都不需要CPU参与，所以就达到了零拷贝。&lt;/p&gt;
&lt;h4&gt;Netty中的零拷贝&lt;/h4&gt;
&lt;p&gt;主要体现在三个方面：&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;1、bytebuffer&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;Netty发送和接收消息主要使用bytebuffer，bytebuffer使用堆外内存（DirectMemory）直接进行Socket读写。&lt;/p&gt;
&lt;p&gt;原因：如果使用传统的堆内存进行Socket读写，JVM会将堆内存buffer拷贝一份到直接内存中然后再写入socket，多了一次缓冲区的内存拷贝。DirectMemory中可以直接通过DMA发送到网卡接口&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;2、Composite Buffers&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;传统的ByteBuffer，如果需要将两个ByteBuffer中的数据组合到一起，我们需要首先创建一个size=size1+size2大小的新的数组，然后将两个数组中的数据拷贝到新的数组中。但是使用Netty提供的组合ByteBuf，就可以避免这样的操作，因为CompositeByteBuf并没有真正将多个Buffer组合起来，而是保存了它们的引用，从而避免了数据的拷贝，实现了零拷贝。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;3、对于FileChannel.transferTo的使用&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;Netty中使用了FileChannel的transferTo方法，该方法依赖于操作系统实现零拷贝。&lt;/p&gt;
&lt;h3&gt;Netty工作原理示意图&lt;/h3&gt;
&lt;p&gt;&lt;img src=&quot;/img/Netty%E5%B7%A5%E4%BD%9C.png&quot; alt=&quot;&quot; /&gt;&lt;/p&gt;
&lt;p&gt;1、Netty抽象出两组线程池 BossGroup 专门负责接收客户端的连接, WorkerGroup 专门负责网络的读写&lt;/p&gt;
&lt;p&gt;2、BossGroup 和 WorkerGroup 类型都是 NioEventLoopGroup&lt;/p&gt;
&lt;p&gt;3、NioEventLoopGroup 相当于一个事件循环组, 这个组中含有多个事件循环 ，每一个事件循环是 NioEventLoop&lt;/p&gt;
&lt;p&gt;4、NioEventLoop 表示一个不断循环的执行处理任务的线程， 每个NioEventLoop 都有一个selector , 用于监听绑定在其上的socket的网络通讯&lt;/p&gt;
&lt;p&gt;5、NioEventLoopGroup 可以有多个线程, 即可以含有多个NioEventLoop&lt;/p&gt;
&lt;p&gt;6、每个Boss NioEventLoop 循环执行的步骤有3步&lt;/p&gt;
&lt;p&gt;1).轮询accept 事件&lt;/p&gt;
&lt;p&gt;2).处理accept 事件 , 与client建立连接 , 生成NioScocketChannel , 并将其注册到某个worker NIOEventLoop 上的 selector&lt;/p&gt;
&lt;p&gt;3).处理任务队列的任务 ， 即 runAllTasks&lt;/p&gt;
&lt;p&gt;7、每个 Worker NIOEventLoop 循环执行的步骤&lt;/p&gt;
&lt;p&gt;1).轮询read, write 事件&lt;/p&gt;
&lt;p&gt;2).处理i/o事件， 即read , write 事件，在对应NioScocketChannel 处理&lt;/p&gt;
&lt;p&gt;3).处理任务队列的任务 ， 即 runAllTasks&lt;/p&gt;
&lt;p&gt;8、 每个Worker NIOEventLoop 处理业务时，会使用pipeline(管道), pipeline 中包含了 channel , 即通过pipeline 可以获取到对应通道, 管道中维护了很多的 处理器&lt;/p&gt;
&lt;h4&gt;Netty执行流程&lt;/h4&gt;
&lt;p&gt;&lt;strong&gt;服务端：&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;/img/Netty%E6%9C%8D%E5%8A%A1%E7%AB%AF.png&quot; alt=&quot;&quot; /&gt;&lt;/p&gt;
&lt;p&gt;代码&lt;/p&gt;
&lt;pre&gt;&lt;code&gt; public static void main(String[] args) throws InterruptedException {
     // 创建BossGroup和WorkerGoup两个线程组
     // bossGoup只是处理连接请求，真正和客户端业务处理，会交给workerGoup完成
     // 两个都是无限循环
     // bossGroup、workerGoup含有的子线程(NioEventLoop)个数默认微实际CPU核数*2
     EventLoopGroup bossGroup = new NioEventLoopGroup();
     EventLoopGroup workerGoup = new NioEventLoopGroup();
     //创建服务器端的启动对象，配置参数
     ServerBootstrap bootstrap = new ServerBootstrap();

     //使用链式编程来进行设置
     try{
         bootstrap.group(bossGroup, workerGoup) //设置两个线程组
             .channel(NioServerSocketChannel.class) //使用NioSocketChannel作为服务器的通道实现
             .option(ChannelOption.SO_BACKLOG, 128) // 设置线程队列得到连接个数
             .childOption(ChannelOption.SO_KEEPALIVE, true) // 设置保持活动连接状态
             .childHandler(new ChannelInitializer&amp;lt;SocketChannel&amp;gt;() { //创建一个通道测试对象，匿名对象
                 // 给pipeline设置处理器
                 @Override
                 protected void initChannel(SocketChannel ch) throws Exception {
                     ch.pipeline().addLast(new NettyServerHandler());
                 }
             }); // 给workerGoup的EventLoop对应的管道设置处理器
         System.out.println(&quot;。。。。服务器已启动。。。。&quot;);
         // 绑定一个端口并且同步，生成了一个ChannelFuture对象
         ChannelFuture cf = bootstrap.bind(6668).sync(); // 启动服务器并绑定端口
         // 对关闭通道进行监听
         cf.channel().closeFuture().sync();
     }finally {
         bossGroup.shutdownGracefully();
         workerGoup.shutdownGracefully();
     }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;1、创建ServerBootStrap实例&lt;/p&gt;
&lt;p&gt;2、设置并绑定Reactor线程池：EventLoopGroup，EventLoop就是处理所有注册到本线程的Selector上面的Channel&lt;/p&gt;
&lt;p&gt;3、设置并绑定服务端的channel&lt;/p&gt;
&lt;p&gt;4、创建处理网络事件的ChannelPipeline和handler，网络时间以流的形式在其中流转，handler完成多数的功能定制：比如编解码 SSl安全认证&lt;/p&gt;
&lt;p&gt;5、绑定并启动监听端口&lt;/p&gt;
&lt;p&gt;6、当轮训到准备就绪的channel后，由Reactor线程：NioEventLoop执行pipline中的方法，最终调度并执行channelHandler&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;客户端：&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;/img/Netty%E5%AE%A2%E6%88%B7%E7%AB%AF.png&quot; alt=&quot;&quot; /&gt;&lt;/p&gt;
&lt;p&gt;代码&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;public static void main(String[] args) throws Exception {
    // 客户端需要一个事件循环组
    EventLoopGroup group = new NioEventLoopGroup();

    try {
        // 创建客户端启动对象
        // 注意客户段使用的不是ServerBootstrap 而是bootstrap
        Bootstrap bootstrap = new Bootstrap();
        //设置相关参数
        bootstrap.group(group) // 设置线程组
            .channel(NioSocketChannel.class)  // 设置客户端通道的实现类（反射）
            .handler(new ChannelInitializer&amp;lt;SocketChannel&amp;gt;() {
                @Override
                protected void initChannel(SocketChannel ch) throws Exception {
                    ch.pipeline().addLast(new NettyClientHandler()); // 加入自己的处理器
                }
            });
        System.out.println(&quot;客户端 ok//&quot;);
        // 启动客户端去连接服务器端，关于ChannelFuture要分析，涉及到netty异步模型
        ChannelFuture channelFuture = bootstrap.connect(&quot;127.0.0.1&quot;, 6668).sync();
        // 给关闭通道进行监听
        channelFuture.channel().closeFuture().sync();
    } finally {
        group.shutdownGracefully();
    }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;a href=&quot;https://juejin.cn/post/5bdaf8ea6fb9a0227b02275a&quot;&gt;参考链接 –掘金&lt;/a&gt;&lt;/p&gt;
</content:encoded></item><item><title>MyBatisPlus入门</title><link>https://gilgameshzzz.github.io/posts/mybatispluslearning/</link><guid isPermaLink="true">https://gilgameshzzz.github.io/posts/mybatispluslearning/</guid><description>1、导入对应的依赖 2、研究依赖如何配置 3、代码如何编写 4、提高扩展技术能力</description><pubDate>Tue, 14 Apr 2020 00:00:00 GMT</pubDate><content:encoded>&lt;h1&gt;快速入门&lt;/h1&gt;
&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;时效注记（2026 更新）&lt;/strong&gt;：本文基于 MyBatis-Plus 3.3.1（2020）。核心的 CRUD、条件构造器、分页、逻辑删除等用法至今有效，但&lt;strong&gt;代码生成器部分已完全重写&lt;/strong&gt;——文中的 &lt;code&gt;AutoGenerator&lt;/code&gt; + &lt;code&gt;GlobalConfig&lt;/code&gt; / &lt;code&gt;StrategyConfig&lt;/code&gt; 这套 setter API 在 &lt;strong&gt;3.5.1 之后已被移除&lt;/strong&gt;，现在改用 &lt;code&gt;FastAutoGenerator&lt;/code&gt; 链式调用。另有两处：&lt;code&gt;IdType.ID_WORKER&lt;/code&gt; 自 3.3.0 起弃用（改用 &lt;code&gt;IdType.ASSIGN_ID&lt;/code&gt;）；&lt;code&gt;setSwagger2()&lt;/code&gt; 已移除。具体见下方代码生成器一节的说明。&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;使用第三方组件：&lt;/p&gt;
&lt;p&gt;1、导入对应的依赖&lt;br /&gt;
2、研究依赖如何配置&lt;br /&gt;
3、代码如何编写&lt;br /&gt;
4、提高扩展技术能力&lt;/p&gt;
&lt;p&gt;步骤：&lt;/p&gt;
&lt;p&gt;1、创建数据库，2、创建数据表，3、编写项目，初始化项目，4、导入依赖,注意&lt;strong&gt;不要同时导入Mybatis和MyBatis-plus&lt;/strong&gt;&lt;br /&gt;
引入 spring-boot-starter、spring-boot-starter-test、mybatis-plus-boot-starter、lombok、h2 依赖：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;&amp;lt;!-- lombok --&amp;gt;
&amp;lt;dependency&amp;gt;
  &amp;lt;groupId&amp;gt;org.projectlombok&amp;lt;/groupId&amp;gt;
  &amp;lt;artifactId&amp;gt;lombok&amp;lt;/artifactId&amp;gt;
  &amp;lt;optional&amp;gt;true&amp;lt;/optional&amp;gt;
&amp;lt;/dependency&amp;gt;
&amp;lt;!-- 数据库驱动 --&amp;gt;
&amp;lt;dependency&amp;gt;
  &amp;lt;groupId&amp;gt;mysql&amp;lt;/groupId&amp;gt;
  &amp;lt;artifactId&amp;gt;mysql-connector-java&amp;lt;/artifactId&amp;gt;
&amp;lt;/dependency&amp;gt;
&amp;lt;!-- mybatisPlus 并不是官方的 --&amp;gt;
&amp;lt;dependency&amp;gt;
  &amp;lt;groupId&amp;gt;com.baomidou&amp;lt;/groupId&amp;gt;
  &amp;lt;artifactId&amp;gt;mybatis-plus-boot-starter&amp;lt;/artifactId&amp;gt;
  &amp;lt;version&amp;gt;3.3.1&amp;lt;/version&amp;gt;
&amp;lt;/dependency&amp;gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Mysql 5和8驱动不同5：com.mysql.jdbc.Driver;&lt;br /&gt;
8：com.mysql.cj.jdbc.Driver、需要增加时区的配置（驱动8兼容mysql 5版本）&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;# mysql 8
spring.datasource.username=root
spring.datasource.password=123456
spring.datasource.url=jdbc:mysql://localhost:3306/数据库?useSSL=false&amp;amp;useUnicode=true&amp;amp;characterEncoding=utf-8&amp;amp;serverTimezone=GMT%2B8
spring.datasource.driver-class-name=com.mysql.cj.jdbc.Driver
&lt;/code&gt;&lt;/pre&gt;
&lt;h4&gt;主键生成策略&lt;/h4&gt;
&lt;p&gt;分布式系统唯一id生成：&lt;a href=&quot;https://www.cnblogs.com/haoxinyue/p/5208136.html&quot;&gt;https://www.cnblogs.com/haoxinyue/p/5208136.html&lt;/a&gt;&lt;/p&gt;
&lt;h4&gt;雪花算法&lt;/h4&gt;
&lt;p&gt;snowflake是Twitter开源的分布式ID生成算法，结果是一个long型的ID。其核心思想是：使用41bit作为毫秒数，10bit作为机器的ID（5个bit是数据中心，5个bit的机器ID），12bit作为毫秒内的流水号（意味着每个节点在每毫秒可以产生 4096 个 ID），最后还有一个符号位，永远是0。&lt;/p&gt;
&lt;h2&gt;代码自动生成器&lt;/h2&gt;
&lt;p&gt;MyBatis-Plus 从 3.0.3 之后移除了代码生成器与模板引擎的默认依赖，需要手动添加相关依赖：&lt;br /&gt;
添加 代码生成器 依赖&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;&amp;lt;dependency&amp;gt;
  &amp;lt;groupId&amp;gt;com.baomidou&amp;lt;/groupId&amp;gt;
  &amp;lt;artifactId&amp;gt;mybatis-plus-generator&amp;lt;/artifactId&amp;gt;
  &amp;lt;version&amp;gt;3.3.1&amp;lt;/version&amp;gt;
&amp;lt;/dependency&amp;gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;添加 模板引擎 依赖，MyBatis-Plus 支持 Velocity（默认）、Freemarker、Beetl，用户可以选择自己熟悉的模板引擎，如果都不满足您的要求，可以采用自定义模板引擎。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;&amp;lt;dependency&amp;gt;
  &amp;lt;groupId&amp;gt;org.apache.velocity&amp;lt;/groupId&amp;gt;
  &amp;lt;artifactId&amp;gt;velocity-engine-core&amp;lt;/artifactId&amp;gt;
  &amp;lt;version&amp;gt;2.2&amp;lt;/version&amp;gt;
&amp;lt;/dependency&amp;gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;pre&gt;&lt;code&gt;package com.xkcoding.helloworld;
import com.baomidou.mybatisplus.annotation.DbType;
import com.baomidou.mybatisplus.annotation.FieldFill;
import com.baomidou.mybatisplus.annotation.IdType;
import com.baomidou.mybatisplus.generator.AutoGenerator;
import com.baomidou.mybatisplus.generator.config.DataSourceConfig;
import com.baomidou.mybatisplus.generator.config.GlobalConfig;
import com.baomidou.mybatisplus.generator.config.PackageConfig;
import com.baomidou.mybatisplus.generator.config.StrategyConfig;
import com.baomidou.mybatisplus.generator.config.po.TableFill;
import com.baomidou.mybatisplus.generator.config.rules.DateType;
import com.baomidou.mybatisplus.generator.config.rules.NamingStrategy;

import java.util.ArrayList;


// 代码自动生成器
public class AutoGeneratorCode {
    public static void main(String[] args) {
        AutoGenerator mpg = new AutoGenerator();
        // 配置策略
        //1、全局策略
        GlobalConfig gc = new GlobalConfig();
        String projectPath = System.getProperty(&quot;user.dir&quot;);
        gc.setOutputDir(projectPath+&quot;/src/main/java&quot;); // 设置生成地址
        gc.setAuthor(&quot;Amadeus&quot;);  // 设置作者
        gc.setOpen(false); //文件创建好是否打开文件管理器
        gc.setFileOverride(false); // 是否覆盖文件
        gc.setServiceName(&quot;%sService&quot;); //去Service的I 前缀
        gc.setIdType(IdType.ID_WORKER);
        gc.setDateType(DateType.ONLY_DATE);
        gc.setSwagger2(true);
        mpg.setGlobalConfig(gc);
        // 设置数据源
        DataSourceConfig dsc = new DataSourceConfig();
        dsc.setUrl(&quot;jdbc:mysql://localhost:3306/muxin-dev?useSSL=false&amp;amp;useUnicode=true&amp;amp;characterEncoding=utf-8&amp;amp;serverTimezone=GMT%2B8&quot;);
        dsc.setDriverName(&quot;com.mysql.cj.jdbc.Driver&quot;);
        dsc.setUsername(&quot;root&quot;);
        dsc.setPassword(&quot;密码&quot;);
        dsc.setDbType(DbType.MYSQL); // 数据库类型
        mpg.setDataSource(dsc);
        // 包的配置  生成哪些包
        PackageConfig pc = new PackageConfig();
        pc.setModuleName(&quot;blog&quot;); // 模块名
        pc.setParent(&quot;com.xkcoding.helloworld&quot;); // 生成在哪个包下
        pc.setEntity(&quot;entity&quot;); // 生成实体 models
        pc.setMapper(&quot;mapper&quot;);
        pc.setService(&quot;service&quot;);
        pc.setController(&quot;controller&quot;);
        mpg.setPackageInfo(pc);
        // 策略配置
        StrategyConfig strategy = new StrategyConfig();
        strategy.setInclude(&quot;user&quot;); // 设置要映射的表名，可以写多个
         strategy.setTablePrefix(new String[] { &quot;user_&quot; });// 此处可以修改为表的前缀
        strategy.setNaming(NamingStrategy.underline_to_camel);
        strategy.setColumnNaming(NamingStrategy.underline_to_camel);
        strategy.setEntityLombokModel(true); // 自动lombok
        strategy.setLogicDeleteFieldName(&quot;deleted&quot;); // 设置逻辑删除
        // 自动填充配置
        TableFill gmtCreate = new TableFill(&quot;gmt_create&quot;, FieldFill.INSERT);
        TableFill gmtModified = new TableFill(&quot;gmt_modified&quot;, FieldFill.INSERT_UPDATE);
        ArrayList&amp;lt;TableFill&amp;gt; tableFills = new ArrayList&amp;lt;&amp;gt;();
        tableFills.add(gmtCreate);
        tableFills.add(gmtModified);
        strategy.setTableFillList(tableFills);
        // 乐观锁
        strategy.setVersionFieldName(&quot;version&quot;);
        strategy.setRestControllerStyle(true);
        strategy.setControllerMappingHyphenStyle(true); // localhost:8080/hello_id_2
        mpg.setStrategy(strategy);
        mpg.execute(); // 执行
    }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;上面这套 API 在 MyBatis-Plus 3.5.1 之后已被移除&lt;/strong&gt;，新版改用 &lt;code&gt;FastAutoGenerator&lt;/code&gt; 链式配置，等价写法大致是：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;FastAutoGenerator.create(URL, USERNAME, PASSWORD)
    .globalConfig(builder -&amp;gt; builder
        .author(&quot;Amadeus&quot;)
        .outputDir(System.getProperty(&quot;user.dir&quot;) + &quot;/src/main/java&quot;)
        .disableOpenDir())
    .packageConfig(builder -&amp;gt; builder
        .parent(&quot;com.xkcoding.helloworld&quot;)
        .moduleName(&quot;blog&quot;))
    .strategyConfig(builder -&amp;gt; builder
        .addInclude(&quot;user&quot;)
        .addTablePrefix(&quot;user_&quot;)
        .entityBuilder().enableLombok().logicDeleteColumnName(&quot;deleted&quot;)
        .controllerBuilder().enableRestStyle().enableHyphenStyle())
    .templateEngine(new VelocityTemplateEngine())
    .execute();
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;另外文中 &lt;code&gt;gc.setIdType(IdType.ID_WORKER)&lt;/code&gt; 里的 &lt;code&gt;ID_WORKER&lt;/code&gt; 自 3.3.0 起弃用，应改为 &lt;code&gt;IdType.ASSIGN_ID&lt;/code&gt;（同样是雪花算法）；&lt;code&gt;gc.setSwagger2(true)&lt;/code&gt; 已移除，新版用 &lt;code&gt;.enableSwagger()&lt;/code&gt;。&lt;/p&gt;
&lt;p&gt;依赖坐标也变了：MySQL 8.0.31 起官方驱动从 &lt;code&gt;mysql:mysql-connector-java&lt;/code&gt; 迁到了 &lt;code&gt;com.mysql:mysql-connector-j&lt;/code&gt;，旧坐标已不再更新。&lt;/p&gt;
&lt;/blockquote&gt;
</content:encoded></item><item><title>Java常见面试题</title><link>https://gilgameshzzz.github.io/posts/javaquestions/</link><guid isPermaLink="true">https://gilgameshzzz.github.io/posts/javaquestions/</guid><description>Java 面试题整理：设计模式、并发、JVM、集合、Spring、数据库、中间件。2020 年整理，部分结论已加时效注记。</description><pubDate>Tue, 14 Apr 2020 00:00:00 GMT</pubDate><content:encoded>&lt;p&gt;================================&lt;/p&gt;
&lt;h4&gt;设计模式分为：创建型模式（单例、工厂、抽象工厂）、结构型模式（代理、组合、装饰）、行为型模式（策略、模板方法、迭代器）&lt;/h4&gt;
&lt;p&gt;================================&lt;br /&gt;
问：请解释一下对象的创建过程?(半初始化)&lt;/p&gt;
&lt;p&gt;答：1、申请内存，&lt;strong&gt;给对象里的成员变量设为默认值&lt;/strong&gt;(半初始化)，&lt;/p&gt;
&lt;p&gt;2、调用对象的构造方法，再给对象里的成员变量设初始值，&lt;/p&gt;
&lt;p&gt;3、将变量名与对象进行关联&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;/img/Object.png&quot; alt=&quot;image-20200331140607400&quot; /&gt;&lt;/p&gt;
&lt;p&gt;===============================&lt;/p&gt;
&lt;p&gt;DCL（Double Check Lock）单例&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;class Mgr{
    private static volatile Mgr instance;
    public static Mgr getInstance(){
        if(instance == null){
            synchronized(Mgr.class){
                if(instance == null){
                    instance = new Mgr();
                }
            }
        }
        return instance;
    }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;===============================&lt;/p&gt;
&lt;p&gt;网站是如何进行访问的：&lt;/p&gt;
&lt;p&gt;1、输入一个域名，回车；&lt;/p&gt;
&lt;p&gt;2、检查本机的hosts配置文件下有没有这个域名映射；有就返回对应的ip地址&lt;/p&gt;
&lt;p&gt;没有，就去DNS服务器找，找到返回，找不到就返回找不到&lt;/p&gt;
&lt;p&gt;===========Jvm=================&lt;/p&gt;
&lt;h3&gt;双亲委派机制&lt;/h3&gt;
&lt;p&gt;类加载器：作用加载Class文件&lt;/p&gt;
&lt;p&gt;1、虚拟机自带的加载器&lt;/p&gt;
&lt;p&gt;2、启动类（根）加载器&lt;/p&gt;
&lt;p&gt;3、扩展类加载器&lt;/p&gt;
&lt;p&gt;4、应用程序加载器&lt;/p&gt;
&lt;p&gt;双亲委派机制：主要是为了安全；&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;是什么：&lt;/strong&gt; 当某个类加载器需要加载某个.class文件时，它首先把这个任务委托给他的上级类加载器，递归这个操作，如果上级的类加载器没有加载，自己才会去加载这个类。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;作用：&lt;/strong&gt; 1、防止重复加载同一个.class。通过委托去向上面问一问，加载过了，就不用再加载一遍。保证数据安全。&lt;/p&gt;
&lt;p&gt;2、保证核心.class不能被篡改。通过委托方式，不会去篡改核心.clas，即使篡改也不会去加载，即使加载也不会是同一个.class对象了。不同的加载器加载同一个.class也不是同一个Class对象。这样保证了Class执行安全。&lt;/p&gt;
&lt;p&gt;App（系统类加载器） –&amp;gt;Ext（标准扩展类加载器） –&amp;gt;BOOT（启动类加载器）&lt;/p&gt;
&lt;p&gt;1、类加载器收到类加载的请求&lt;/p&gt;
&lt;p&gt;2、将这个请求向上委托给父类加载器去完成，一直向上委托，直到启动类加载器&lt;/p&gt;
&lt;p&gt;3、启动加载器检查是否能够加载当前这个类，能加载就结束，使用当前的加载器，否则，抛出异常，通知子加载器进行加载&lt;/p&gt;
&lt;p&gt;4、重复步骤3；&lt;br /&gt;
常见错误 class NOt Found~&lt;/p&gt;
&lt;h2&gt;native&lt;/h2&gt;
&lt;p&gt;凡是带了native关键字的，说明java的作用范围达不到了，会去调用底层C语言的库。会进入本地方法栈，调用本地方法本地接口 JNI&lt;/p&gt;
&lt;h2&gt;方法区&lt;/h2&gt;
&lt;p&gt;方法区是被所有线程共享的。主要存储&lt;strong&gt;静态变量，常量，类信息、运行时的常量池存在方法区，但是实例变量存在堆内存中，和方法区无关，&lt;/strong&gt; 也就是存储 static、final、Class、常量池&lt;/p&gt;
&lt;p&gt;===============================&lt;/p&gt;
&lt;h1&gt;问题一：请谈谈你对volatile的理解&lt;/h1&gt;
&lt;p&gt;一、volatile 是java虚拟机提供的轻量级的同步机制；&lt;strong&gt;有三大特性：&lt;/strong&gt; 1.保证可见性；2.不保证原子性；3.禁止指令重排（volatile实现禁止指令重排优化，从而避免多线程环境下程序出现&lt;strong&gt;乱序&lt;/strong&gt;执行的现象）&lt;/p&gt;
&lt;p&gt;二、JMM(java内存模型java Memory Model) &lt;strong&gt;特性：可见性，原子性，有序性&lt;/strong&gt;&lt;br /&gt;
&lt;em&gt;在变量前加上volatile，一个线程对这个变量进行修改，就及时通知其他线程，主物理内存的变量值已经被修改，其他线程的变量值就修改了&lt;/em&gt;&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;/img/java%E7%BA%BF%E7%A8%8B.png&quot; alt=&quot;JMM模型&quot; /&gt;&lt;/p&gt;
&lt;p&gt;本身是一种抽象的概念&lt;strong&gt;并不真实存在&lt;/strong&gt;，它描述的是一组规则或规范，通过这组规范定义了程序中各个变量（包括实例字段，静态字段和构成数组对象的元素）的访问方式。&lt;/p&gt;
&lt;h3&gt;JMM关于同步的规定：&lt;/h3&gt;
&lt;p&gt;1、线程解锁前，必须把共享变量的值刷新回主内存；&lt;/p&gt;
&lt;p&gt;2、线程加锁前，必须读取主内存的最新值到自己的工作内存；&lt;/p&gt;
&lt;p&gt;3、加锁解锁是同一把锁；&lt;/p&gt;
&lt;p&gt;由于JVM运行程序的实体是线程，而每个线程创建时JVM都会为其创建一个工作内存（或称栈空间），工作内存是每个线程的私有数据区域，而java内存模型中规定所有变量都存储在&lt;strong&gt;主内存&lt;/strong&gt;，主内存是共享内存区域，所有线程都可以访问，&lt;strong&gt;但线程对变量的操作（读取赋值等）必须在工作内存中进行，首先要将变量从主内存拷贝的自己的工作内存空间，然后对变量进行操作，操作完成后再将变量写回主内存&lt;/strong&gt;，不能直接操作主内存中的变量，各个线程中的工作内存中存储着主内存中的变量副本拷贝，因此不同的线程无法访问对方的工作内存，线程间的通信必须通过主内存来完成。&lt;/p&gt;
&lt;h3&gt;重排&lt;/h3&gt;
&lt;p&gt;计算机在执行程序时，为了提高性能，编译器和处理器的常常会对&lt;strong&gt;指令做重排&lt;/strong&gt;，分三种：1、单线程环境里面确保程序最终执行结果和代码顺序执行的结果一致；&lt;br /&gt;
2、处理器在进行重排序时必须要考虑指令之间的&lt;strong&gt;数据依赖性&lt;/strong&gt;；&lt;br /&gt;
3、多线程环境中线程&lt;em&gt;交替执行&lt;/em&gt;，由于编译器优化重排的存在，两个线程中使用的变量能否保证一致性是无法确定的，结果无法预测。&lt;/p&gt;
&lt;p&gt;源代码 –&amp;gt;编译器优化的重排 –&amp;gt;指令并行的重排 –&amp;gt; 内存系统的重排 –&amp;gt;最终执行的指令&lt;/p&gt;
&lt;h3&gt;如何禁止指令重排&lt;/h3&gt;
&lt;p&gt;内存屏障（Memory Barrier）又称内存栅栏，是一个CPU指令，它的作用有两个：&lt;br /&gt;
1、保证特定操作的执行顺序；&lt;br /&gt;
2、保证某些变量的内存可见性（利用该特性实现volatile的内存可见性）&lt;br /&gt;
&lt;strong&gt;通过插入内存屏障就禁止在内存屏障前后的指令执行重排序优化&lt;/strong&gt;，内存屏障另一个作用是强制刷出各种CPU的缓存数据，因此任何CPU上的线程都能读取到这些数据的最新版本（保证可见性）&lt;/p&gt;
&lt;h2&gt;在哪些方面用到volatile？&lt;/h2&gt;
&lt;h3&gt;单例模式DCL代码&lt;/h3&gt;
&lt;h3&gt;单例模式volatile分析&lt;/h3&gt;
&lt;p&gt;&lt;strong&gt;DCL(双端检锁)机制不一定线程安全，原始是有指令重排序的存在（某一个线程执行到第一次检测，读取到的instance不为null时，instance的引用对象可能没有&lt;em&gt;完成初始化&lt;/em&gt;。），加入volatile可以禁止指令重排&lt;/strong&gt;&lt;br /&gt;
指令重排只会保证串行语义的执行的一致性（单线程），但并不会关系多线程间的语义一致性。&lt;strong&gt;所以当一条线程访问instance不为null时，由于instance实例未必已初始化完成，也就造成了线程安全问题。&lt;/strong&gt;&lt;/p&gt;
&lt;h1&gt;问题二：CAS&lt;/h1&gt;
&lt;p&gt;CAS的全称为 Compare-And-Swap，它是一条CPU并发原语。它的功能是判断内存某个位置的值是否为预期值，如果是则更改为新的值，这个过程是原子的。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;总结&lt;/strong&gt;：CAS比较当前工作内存中的值和主内存中的值，如果相同则执行规定操作，否则继续比较直到主内存和工作内存中的值一致为止。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;CAS应用&lt;/strong&gt;：CAS有3个操作数，内存值V，旧的预期值A，要修改的更新值B。当且仅当预期值A和内存值V相同时，将内存值V修改为B，否则什么都不做。&lt;/p&gt;
&lt;h3&gt;CAS底层原理&lt;/h3&gt;
&lt;p&gt;CAS并发原语体现在Java中的sun.misc.Unsafe类中的各个方法。调用Unsafe类中的CAS方法，JVM会实现出CAS汇编指令，完全依赖于&lt;strong&gt;硬件&lt;/strong&gt;的功能。&lt;strong&gt;并且原语的执行必须是连续的，在执行过程中不允许被中断，也就是说CAS是一条CPU的原子指令，不会造成所谓的数据不一致问题。&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;1、Unsafe 是CAS的核心类，由于Java方法无法直接访问底层系统，需要通过本地（native）方法来访问，Unsafe 相当于一个后门，基于该类可以直接操作特定内存的数据。Unsafe类存在于sun.misc包中，其内部方法操作可以像C的指针一样直接操作内存，因为Java中CAS操作的执行依赖于Unsafe类的方法。&lt;br /&gt;
&lt;strong&gt;注意Unsafe类中的所有方法都是native修饰的，也就是说Unsafe类中的方法都直接调用操作系统底层资源执行相应任务&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;2、变量valueOffset，表示该变量值在内存中的偏移地址，因为Unsafe就是根据&lt;strong&gt;内存偏移地址获取数据&lt;/strong&gt;的。&lt;/p&gt;
&lt;p&gt;3、变量value用volatile修饰，保证了多线程之间的内存可见性&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;/img/1.png&quot; alt=&quot;&quot; /&gt;&lt;br /&gt;
&lt;img src=&quot;/img/2.png&quot; alt=&quot;&quot; /&gt;&lt;br /&gt;
&lt;img src=&quot;/img/3.png&quot; alt=&quot;&quot; /&gt;&lt;/p&gt;
&lt;h3&gt;CAS缺点：&lt;/h3&gt;
&lt;p&gt;1、循环时间长，开销大&lt;br /&gt;
2、只能保证一个共享变量的原子操作&lt;br /&gt;
3、引出ABA问题&lt;/p&gt;
&lt;hr /&gt;
&lt;p&gt;CAS —&amp;gt;Unsafe —&amp;gt;CAS底层思想 —&amp;gt;ABA —&amp;gt; 原子引用更新 —&amp;gt; 如何规避ABA问题&lt;/p&gt;
&lt;hr /&gt;
&lt;h3&gt;ABA问题&lt;/h3&gt;
&lt;p&gt;CAS会导致“ABA问题”&lt;br /&gt;
CAS算法实现一个重要前提需要提取内存中某时刻的数据并在当下时刻比较并替换，那么在这个时间差类会导致数据的变化。&lt;/p&gt;
&lt;p&gt;比如说：一个线程one从内存位置v取出A，这时候另一个线程two也从内存取出A，并且线程two进行了一些操作将值变为B，然后线程two又将V位置的数据变为A，这个时候线程one进行CAS操作发现内存中仍然是A，然后线程one操作成功。&lt;strong&gt;尽管线程one的CAS操作成功，但不代表这个过程就是没有问题的。&lt;/strong&gt;&lt;/p&gt;
&lt;h3&gt;解决ABA问题&lt;/h3&gt;
&lt;p&gt;–理解原子引用+新增一种机制，就是修改版本号（类似时间戳）–&lt;/p&gt;
&lt;h1&gt;问题三 ArrayList线程不安全，请编写一个不安全的case,并给出解决方案。(map，set也不安全)&lt;/h1&gt;
&lt;p&gt;&lt;em&gt;HashSet底层是HashMap, set在使用add方法时（实际使用map.put方法），之所以只用传一个参数，是因为传入的值被当作key,而value是一个默认的PRESENT的object。CopyOnWriteArraySet底层是CopyOnWriteArrayList&lt;/em&gt;&lt;/p&gt;
&lt;p&gt;解决map线程不安全，可以用ConcurrentHashMap;&lt;br /&gt;
之所以不安全，是因为add方法没有加锁&lt;br /&gt;
常见异常：java.util.ConcurrentModificationException&lt;/p&gt;
&lt;h3&gt;1、故障现象：&lt;/h3&gt;
&lt;p&gt;java.util.ConcurrentModificationException&lt;/p&gt;
&lt;h3&gt;2、导致原因:并发争抢修改；&lt;/h3&gt;
&lt;h3&gt;3、解决方案&lt;/h3&gt;
&lt;p&gt;① 使用vector,加了锁，并发性下降： new vector&amp;lt;&amp;gt;();&lt;/p&gt;
&lt;p&gt;② 使用Collections.synchronizedList(new ArrayList&amp;lt;&amp;gt;());&lt;/p&gt;
&lt;p&gt;③ 使用 new CopyOnWriteArrayList&amp;lt;&amp;gt;();&lt;/p&gt;
&lt;p&gt;CopyOnWrite容器即写时复制的容器，往一个容器添加元素时，先将当前容器进行copy,复制新的容器object[]newElements,然后新的容器里添加元素，添加完成后再将原容器的引用指向新的容器，这样的好处是可以对CopyOnWrite容器进行并发的读，而不需要加锁，因为当前容器不会添加任何元素，所以CopyOnWrite容器也是一种读写分离的思想，读和写不同的容器；&lt;/p&gt;
&lt;h1&gt;问题四 公平锁/非公平锁/可重入锁/递归锁/自旋锁谈谈你的理解，请手写一个自旋锁：&lt;/h1&gt;
&lt;h2&gt;公平锁&lt;/h2&gt;
&lt;p&gt;指多个线程按照申请锁的顺序来获取锁，类似排队打饭，先来后到。&lt;/p&gt;
&lt;h2&gt;非公平锁&lt;/h2&gt;
&lt;p&gt;指多个线程获取锁的顺序并不是按照申请锁的顺序，有可能后申请的线程比先申请的线程优先获取锁，在高并发的情况下，有可能会造成优先级反转或者饥饿现象（有一个线程一个锁也没有获取到）。&lt;/p&gt;
&lt;p&gt;并发包中ReentrantLock的创建可以指定构造函数的Boolean类型来得到公平锁或非公平锁，默认是非公平锁。&lt;/p&gt;
&lt;h3&gt;关于两者的区别&lt;/h3&gt;
&lt;p&gt;&lt;strong&gt;公平锁&lt;/strong&gt;：就是公平，在并发环境中，每个线程在获取锁时会先查看此锁维护并等待队列。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;非公平锁&lt;/strong&gt;：上来就直接尝试占有锁，如果尝试失败，就再采用类似公平锁那种方式。&lt;br /&gt;
非公平锁的优点在于吞吐量比公平锁大，对于Synchronized而言，也是一种非公平锁。&lt;/p&gt;
&lt;h2&gt;可重入锁（又名递归锁）&lt;/h2&gt;
&lt;p&gt;指同一线程外层函数获得锁之后，内层递归函数仍然能获取该锁的代码，在同一个线程在外层方法获取锁的时候，在进入内层方法会自动获取锁，也就是说&lt;strong&gt;线程可以进入任何一个它已经拥有的锁所同步着的代码块。&lt;/strong&gt;&lt;br /&gt;
ReentrantLock/Synchronized就是一个典型的可重入锁，可重入锁最大的作用是避免死锁。（加锁几次，解锁几次，程序不会报错，解锁少一次程序就会卡死）&lt;/p&gt;
&lt;h2&gt;自旋锁(Spinlock)&lt;/h2&gt;
&lt;p&gt;指尝试获取锁的线程不会立即阻塞，而是采用循环的方式去尝试获取锁，这样的好处是减少线程上下文切换的消耗，缺点是循环会消耗CPU.(CAS就是自旋锁)&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;//Unsafe.getAndAddInt
public final int getAndAddInt(Object var1, long var2, int var4){
  int var5
  do{
    var5 = this.getIntVolatile(var1, var2);
  }while(!this.compareAndSwapInt(var1,var2,var5,var5+var4));
  return var5
}
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;独占锁（写锁）/共享锁（读锁）/互斥锁&lt;/h2&gt;
&lt;p&gt;&lt;strong&gt;独占锁&lt;/strong&gt;：指该锁一次只能被一个线程所持有。对ReentrantLock和Synchronized而言都是独占锁。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;共享锁&lt;/strong&gt;：指该锁可被多个线程所持有。对ReentrantReadWriteLock其读锁是共享锁，其写锁是独占锁。读锁、共享锁可保证并发读是非常高效的，读写，写读，写写的过程是互斥的。&lt;/p&gt;
&lt;h1&gt;问题五 CountDownLatch/CyclicBarrier/Semaphore使用过吗？&lt;/h1&gt;
&lt;p&gt;&lt;strong&gt;CountDownLatch&lt;/strong&gt;:让一些线程阻塞直到另一些线程完成一系列操作后才被唤醒；&lt;br /&gt;
CountDownLatch主要有两个方法，当一个或多个线程调用await方法时，调用线程会被阻塞。其他线程调用countDown方法会将计数器减1（调用countDown方法的线程不会阻塞），当计数器值为0时，因调用await方法被阻塞的线程就会被唤醒，继续执行。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;CyclicBarrier&lt;/strong&gt;:字面意思是可循环（Cylic）使用的屏障（Barrier）。它要做的事情是，让一组线程到达一个屏障（也可以叫同步点）时被阻塞，直到最后一个线程到达屏障时，屏障才会开门，所有被屏障拦截的线程才会继续干活，线程进入屏障通过CyclicBarrier的await()方法。（与CountDownLatch相反）&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Semaphore&lt;/strong&gt;:信号量主要用于两个目的，一个是用于多个共享资源的互斥使用，另一个用于并发线程数的控制。&lt;/p&gt;
&lt;h1&gt;问题六：阻塞队列&lt;/h1&gt;
&lt;p&gt;&lt;strong&gt;ArrayBlockingQueue&lt;/strong&gt;:是一个基于数组结构的有界限阻塞队列，此队列按FIFO(先进先出)原则对元素进行排序。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;LinkedBlockingQueue&lt;/strong&gt;:是一个基于链表结构的阻塞队列，此队列按FIFO排序元素，吞吐量通常要高于ArrayBlockingQueue。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;SynchronousQueue&lt;/strong&gt;:一个不存储元素的阻塞队列。每个插入操作必须等到另一个线程调用移除操作，否则插入操作一直处于阻塞状态，吞吐量通常要高于LinkedBlockingQueue。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;阻塞队列&lt;/strong&gt;：&lt;br /&gt;
顾名思义，首先它是一个队列，一个阻塞队列在数据结构作用如下图：&lt;br /&gt;
&lt;img src=&quot;/img/BlockingQueue.png&quot; alt=&quot;&quot; /&gt;&lt;br /&gt;
当阻塞队列是空时，从队列获取元素的操作将会被阻塞(试图从空的阻塞队列中获取元素的线程将会被阻塞，直到其他的线程往空的队列插入新的元素)；&lt;/p&gt;
&lt;p&gt;当阻塞队列是满时，往队列添加元素的操作将会被阻塞(试图往已满的阻塞队列添加新元素的线程同样也会被阻塞，直到其他的线程从队列删除一个或多个元素或者清空队列，使队列变得空闲后新增)。&lt;/p&gt;
&lt;h2&gt;为什么需要BlockingQueue&lt;/h2&gt;
&lt;p&gt;好处是我们不需要关心什么时候需要阻塞线程，什么时候需要唤醒线程，因为这一切BlockingQueue都一手包办了。不需要兼顾效率和线程安全。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;种类分析&lt;/strong&gt;：&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;ArrayBlockingQueue&lt;/strong&gt;:有数组结构组成的有界阻塞队列；&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;LinkedBlockingQueue&lt;/strong&gt;：由链表结构组成的有界（但大小默认值为Integer.MAX_VALUE）阻塞队列；&lt;/p&gt;
&lt;p&gt;PriorityBlockingQueue:支持优先级排序的无界阻塞队列；&lt;/p&gt;
&lt;p&gt;DelayQueue：使用优先级队列实现的延迟无界阻塞队列；&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;SynchronousQueue&lt;/strong&gt;：不存储元素的阻塞队列，也即单个元素的队列；&lt;/p&gt;
&lt;p&gt;LinkedTransferQueue:由链表结构组成的无界阻塞队列；&lt;/p&gt;
&lt;p&gt;LinkedBlockingDeque:由链表结构组成的双向阻塞队列。&lt;/p&gt;
&lt;p&gt;BlockingQueue核心方法:&lt;br /&gt;
&lt;img src=&quot;/img/BlockingQueueMethod.png&quot; alt=&quot;&quot; /&gt;&lt;br /&gt;
&lt;img src=&quot;/img/4.png&quot; alt=&quot;&quot; /&gt;&lt;/p&gt;
&lt;h3&gt;SynchronousQueue&lt;/h3&gt;
&lt;p&gt;SynchronousQueue没有容量，与其他BlockingQueue不同，SynchronousQueue是一个不存储元素的BlockingQueue。每一个put操作必须要等待一个take操作，否则不能继续添加元素，反之亦然。&lt;/p&gt;
&lt;h3&gt;用在哪里&lt;/h3&gt;
&lt;h4&gt;生产者消费模式&lt;/h4&gt;
&lt;p&gt;多线程的判断用while判断，用if会出现虚假唤醒现象。&lt;/p&gt;
&lt;h1&gt;问题：Synchronized和lock有什么区别，用新的lock有什么好处？&lt;/h1&gt;
&lt;p&gt;&lt;strong&gt;1、原始构成：&lt;/strong&gt;&lt;br /&gt;
synchronized是关键字属于JVM层面，monitorenter(底层是通过monitor对象来完成，其实wait/notify等方法也依赖于monitor)&lt;/p&gt;
&lt;p&gt;Lock是具体类（Java.util.concurrent.locks.lock）是api层面的锁.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;2、使用方法：&lt;/strong&gt;&lt;br /&gt;
synchronized 不需要用户去手动释放锁，当synchronized代码执行完成后系统会自动让线程释放对锁的占用。&lt;br /&gt;
ReentrantLock则需要手动释放锁，若没有主动释放锁，就有可能导致出现死锁现象，需要lock()和unLock()方法配合try/finally语句块来完成。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;3、等待是否可中断：&lt;/strong&gt;&lt;br /&gt;
synchronized不可中断，除非抛出异常或者正常运行完成&lt;br /&gt;
ReentrantLock可中断：①设置超时方法trylock(long timeout,TimeUnit unit)。②lockInterruptibly()放代码块中，调用interrupt()方法可中断&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;4、加锁是否公平：&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;synchronized非公平锁&lt;/p&gt;
&lt;p&gt;ReentrantLock两者都可以，默认非公平锁，构造方法可以传入Boolean值，true为公平锁，false为非公平锁。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;5、锁绑定多个条件Condition&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;synchronized没有&lt;/p&gt;
&lt;p&gt;ReentrantLock用来实现分组唤醒需要唤醒的线程们，可以精确唤醒，而不是像synchronized要么随机唤醒一个线程，要么全部唤醒。&lt;/p&gt;
&lt;h1&gt;线程池&lt;/h1&gt;
&lt;p&gt;线程池做的工作主要是控制运行线程的数量，&lt;strong&gt;处理过程中将任务放入队列&lt;/strong&gt;，然后在线程创建后启动这些任务，&lt;strong&gt;如果线程数量超过了最大数量的线程，超出的线程就要排队等候&lt;/strong&gt;，等待其他线程执行完毕，再从队列中取出任务来执行。&lt;/p&gt;
&lt;p&gt;线程池主要特点或优势：&lt;strong&gt;线程复用，控制最大并发数量，管理线程&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;或者:&lt;/p&gt;
&lt;p&gt;①：降低资源消耗。通过重复利用已创建的线程降低线程创建和销毁造成的消耗。&lt;/p&gt;
&lt;p&gt;②：提高响应速度。当任务到达时，任务可以不需要等到线程创建，能立即执行。&lt;/p&gt;
&lt;p&gt;③：提高线程的可管理性，线程是稀缺资源，如果无限制的创建，不仅会消耗系统资源，还会降低系统的稳定性，使用线程池可以进行统一的分配，调优和监控。&lt;/p&gt;
&lt;p&gt;java中的线程池是通过Executor框架实现的，该框架中用到了Executor，Executors,ExecutorService,ThreadPoolExecutor这几个类。（底类是ThreadPoolExecutor）&lt;/p&gt;
&lt;h2&gt;java 使用多线程的方式:&lt;/h2&gt;
&lt;p&gt;①继承线程类，②使用Runable接口(没有返回值，不抛异常)，③使用Callable接口(有返回值，会抛异常)，④使用线程池&lt;/p&gt;
&lt;p&gt;1.Callable规定的方法是call()，而Runnable规定的方法是run().&lt;/p&gt;
&lt;p&gt;2.Callable的任务执行后可返回值，而Runnable的任务是不能返回值的。&lt;/p&gt;
&lt;p&gt;3.call() 方法可抛出异常，而run() 方法是不能抛出异常的。&lt;/p&gt;
&lt;p&gt;4.运行Callable任务可拿到一个Future对象， Future表示异步计算的结果。&lt;br /&gt;
它提供了检查计算是否完成的方法，以等待计算的完成，并检索计算的结果。&lt;/p&gt;
&lt;p&gt;5.通过Future对象可了解任务执行情况，可取消任务的执行，还可获取任务执行的结果。&lt;/p&gt;
&lt;p&gt;6.Callable是类似于Runnable的接口，实现Callable接口的类和实现Runnable的类都是可被其它线程执行的任务。&lt;/p&gt;
&lt;h3&gt;Executors重点方法：&lt;/h3&gt;
&lt;p&gt;Executors.newFixedThreadPool(),自己写开多少个线程,常用于执行长期的任务，性能好很多&lt;/p&gt;
&lt;p&gt;Executors.newSingleThreadExecutor只开启一个线程，常用于一个任务一个任务执行的场景&lt;/p&gt;
&lt;p&gt;Executors.newCachedThreadPool(),系统自己决定开多少线程。常用于执行很多短期异步的小程序或者负载较轻的服务。&lt;/p&gt;
&lt;h3&gt;线程池的七大参数：&lt;/h3&gt;
&lt;p&gt;corePoolSize:线程池中的常驻核心线程数；&lt;/p&gt;
&lt;p&gt;maximumPoolSize：线程池能同时执行的最大线程数，必须大于等于1；&lt;/p&gt;
&lt;p&gt;keepAliveTime:多余的空闲线程存活时间（只有当线程池中的线程数大于corePool Size时，才会起作用）；&lt;/p&gt;
&lt;p&gt;unit:keepAliveTime的单位&lt;/p&gt;
&lt;p&gt;workQueue:任务队列，被提交但尚未被执行的任务；&lt;/p&gt;
&lt;p&gt;threadFactory:表示生成线程池中工作线程的线程工厂，用于创建线程一般默认的即可；&lt;/p&gt;
&lt;p&gt;handler；拒绝策略，当队列满了且工作线程大于等于最大线程数时如何来拒绝请求执行的Runable请求策略&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;拒绝策略&lt;/strong&gt;：AbortPolicy(默认)：直接抛出RejectedExecutionException异常阻止系统正常运行；&lt;/p&gt;
&lt;p&gt;CallerRunsPolicy:”调用者运行”一种调节机制，该策略既不会抛弃任务，也不会抛出异常；而是将某些任务回退到调用者。（如果是main线程调用线程池，则线程池任务队列满了后，某些任务会由main线程处理。）&lt;/p&gt;
&lt;p&gt;DiscardOldestPolicy:抛出队列中等待最久的任务，然后把当前任务加入队列中尝试再次提交，&lt;/p&gt;
&lt;p&gt;DiscardPolicy:直接丢弃任务，不予任何处理也不抛出异常。(如果允许任务丢失，这是最好的策略)&lt;/p&gt;
&lt;h3&gt;问题：在生产中，你用Executors的哪个方法创建线程池&lt;/h3&gt;
&lt;p&gt;答：都不用。线程池通过ThreadPoolExecutor方式创建。&lt;br /&gt;
Executors返回的线程池对象的弊端如下：&lt;br /&gt;
FixedThreadPool和SingleThreadPool:允许的请求队列长度为Inter.MAX_VALUE,可能会堆积大量的请求，从而导致OOM。&lt;br /&gt;
CachedThreadPool和ScheduleThreadPool:&lt;br /&gt;
允许的创建线程数量为Inter.MAX_VALUE,可能会堆积大量的线程，从而导致OOM。&lt;/p&gt;
&lt;h3&gt;合理配置线程池的线程数&lt;/h3&gt;
&lt;p&gt;&lt;strong&gt;CPU密集型&lt;/strong&gt;：该任务需要大量的运算，没有阻塞，CPU一直运行，CPU密集型任务配置尽可能少的线程数量，CPU核数+1个线程的线程池。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;IO密集型&lt;/strong&gt;：由于不是一直在执行任务，则应配置尽可能多的线程，如CPU核数*2；如果大部分线程都阻塞，故需要多配置线程数：CPU核数/1-阻塞系数，阻塞系数在0.8~0.9之间。&lt;/p&gt;
&lt;h1&gt;死锁&lt;/h1&gt;
&lt;p&gt;指两个或两个以上的进程执行过程中，因争夺资源而造成的一种互相等待的现象。&lt;/p&gt;
&lt;h3&gt;查找死锁&lt;/h3&gt;
&lt;p&gt;终端输入jps -l，查看java进程的编号，再使用jstack 进程编号&lt;/p&gt;
&lt;h1&gt;CPU占用过高定位分析&lt;/h1&gt;
&lt;p&gt;步骤：先用top命令找到CPU占比最高的；ps -ef 或者jps进一步定位，得知是一个怎么样的后台程序；定位到具体线程或代码（ps -mp 进程 -o THREAD,tid,time;参数解释：-m 显示所有的线程， -p pid进程使用CPU的时间， -o 该参数后是用户自定义格式）；将需要的线程ID转换为16进制格式（英文小写格式），&lt;s&gt;然后printf”%x\n” 有问题的线程id&lt;/s&gt;;jstack 进程ID | grep tid(16进制线程ID小写英文)&lt;/p&gt;
&lt;h1&gt;问题 token和JWT存在什么区别&lt;/h1&gt;
&lt;p&gt;相同点： 都是访问资源的令牌，都可以记录用户的信息。&lt;/p&gt;
&lt;p&gt;不同点：&lt;strong&gt;token需要查库验证token是否有效。而JWT不用查库或者少查库，直接在服务端进行校验&lt;/strong&gt;，因为用户的信息及加密信息在第二部分payload和第三部分签证中已经生成，只要在服务段进行校验就行。&lt;br /&gt;
token验证流程：&lt;/p&gt;
&lt;p&gt;1.把用户的账号密码发到后端；&lt;br /&gt;
2.后端进行校验，校验成功生成token，把token发送到客户端；&lt;br /&gt;
3.客户段自己保存token，再次请求就要在http协议的请求头中带着token去访问服务端，和在服务端保存的token信息进行比对校验；&lt;/p&gt;
&lt;p&gt;JWT验证流程：&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;在头部信息中声明加密算法和常量， 然后把header使用json转化为字符串&lt;/li&gt;
&lt;li&gt;在载荷中声明用户信息，同时还有一些其他的内容；再次使用json 把载荷部分进行转化，转化为字符串&lt;/li&gt;
&lt;li&gt;使用在header中声明的加密算法和每个项目随机生成的secret来进行加密， 把第一步分字符串和第二部分的字符串进行加密， 生成新的字符串。词字符串是独一无二的。&lt;/li&gt;
&lt;li&gt;解密的时候，只要客户端带着JWT来发起请求，服务端就直接使用secret进行解密。&lt;/li&gt;
&lt;/ol&gt;
&lt;h1&gt;单点登录SSO(Single Sign On)&lt;/h1&gt;
&lt;p&gt;指在多系统应用群中登录一个系统，便可在其他所有系统中得到授权而无需再次登录。包括单点登录和单点注销。&lt;/p&gt;
&lt;p&gt;sso需要一个独立的认证中心，只有认证中心能接受用户的用户名密码等安全信息，其他系统不提供登录入口，只接受认证中心的间接授权。间接授权通过令牌实现，sso认证中心验证用户的用户名密码没问题，创建授权令牌，在接下来的跳转过程中，授权令牌作为参数发送给各个子系统，子系统拿到令牌，即得到了授权，可以借此创建局部会话，局部会话登录方式与单系统的登录方式相同。&lt;/p&gt;
&lt;h3&gt;验证过程&lt;/h3&gt;
&lt;p&gt;用户访问系统1的受保护资源，系统1发现用户未登录，跳转至sso认证中心，并将自己的地址作为参数;sso认证中心发现用户未登录，将用户引导至登录页面;用户输入用户名密码提交登录申请;sso认证中心校验用户信息，创建用户与sso认证中心之间的会话，称为全局会话，同时创建授权令牌。sso认证中心带着令牌跳转会最初的请求地址（系统1），系统1拿到令牌，去sso认证中心校验令牌是否有效，sso认证中心校验令牌，返回有效，注册系统1，系统1使用该令牌创建与用户的会话，称为局部会话，返回受保护资源。&lt;/p&gt;
&lt;p&gt;用户访问系统2的受保护资源，系统2发现用户未登录，跳转至sso认证中心，并将自己的地址作为参数，sso认证中心发现用户已登录，跳转回系统2的地址，并附上令牌，系统2拿到令牌，去sso认证中心校验令牌是否有效。sso认证中心校验令牌，返回有效，注册系统2，系统2使用该令牌创建与用户的局部会话，返回受保护资源。&lt;/p&gt;
&lt;p&gt;用户登录成功之后，会与sso认证中心及各个子系统建立会话，用户与sso认证中心建立的会话称为全局会话，用户与各个子系统建立的会话称为局部会话，局部会话建立之后，用户访问子系统受保护资源将不再通过sso认证中心，全局会话与局部会话有如下约束关系：&lt;/p&gt;
&lt;p&gt;局部会话存在，全局会话一定存在；&lt;/p&gt;
&lt;p&gt;全局会话存在，局部会话不一定存在；&lt;/p&gt;
&lt;p&gt;全局会话销毁，局部会话必须销毁。&lt;/p&gt;
&lt;h3&gt;微服务&lt;/h3&gt;
&lt;p&gt;通常而言，微服务架构是一种架构模式，或者说一种架构风格。它&lt;strong&gt;提倡将单一的应用程序划分成一组小的服务，彻底地去耦合&lt;/strong&gt;，每个服务运行在其独立的进程内，服务之间互相协调，互相配置.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;优点：&lt;/strong&gt;&lt;br /&gt;
每个服务足够内聚，足够小，易理解，松耦合，能使用不同的语言开发，易于和第三方集成。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;缺点：&lt;/strong&gt;&lt;br /&gt;
开发人员要处理分布式系统的复杂性，多服务运维难度，随着服务的增加，运维压力也在增大，系统部署依赖，服务通信成本。&lt;/p&gt;
&lt;h4&gt;微服务技术栈&lt;/h4&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;/td&gt;
&lt;td&gt;SpringBoot,Spring,SpringMVC&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;服务配置与管理&lt;/td&gt;
&lt;td&gt;NetFlix公司的Archaius,阿里的Diamond&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;服务注册与发现&lt;/td&gt;
&lt;td&gt;Eureka，Consul，Zookeeper&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;服务调用&lt;/td&gt;
&lt;td&gt;Rest，RPC，gRPC&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;服务熔断器&lt;/td&gt;
&lt;td&gt;Hystix,Envoy&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;负载均衡&lt;/td&gt;
&lt;td&gt;Ribbon，Nginx&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;服务接口地调用(客户端调用服务的简化工具)&lt;/td&gt;
&lt;td&gt;Feign&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;消息队列&lt;/td&gt;
&lt;td&gt;Kafka，RabbitMQ，ActiveMQ&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;服务配置中心管理&lt;/td&gt;
&lt;td&gt;SpringCloudConfig,Chef&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;服务路由（API网关）&lt;/td&gt;
&lt;td&gt;Zuul&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;服务监控&lt;/td&gt;
&lt;td&gt;Zabbix，Nagios，Metrics，Specatator&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;全链路追踪&lt;/td&gt;
&lt;td&gt;Zipkin，Brave，Dapper&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;服务部署&lt;/td&gt;
&lt;td&gt;Docker，OpenStack,Kubernetes&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;数据流操作开发包&lt;/td&gt;
&lt;td&gt;SpringCloud Stream(封装与Redis,Rabbit,Kafka等发送接受消息)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;事件消息总线&lt;/td&gt;
&lt;td&gt;SpringCloud Bus&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;p&gt;===============================&lt;/p&gt;
&lt;h1&gt;一. Java 基础模块&lt;/h1&gt;
&lt;h2&gt;类初始化过程&lt;/h2&gt;
&lt;p&gt;1、一个类要创建实例需要先加载并初始化该类&lt;/p&gt;
&lt;p&gt;⭐main方法所在的类需要先加载和初始化&lt;/p&gt;
&lt;p&gt;2、一个子类要初始化需要先初始化分类&lt;/p&gt;
&lt;p&gt;3、一个类初始化就是执行&amp;lt; clinit &amp;gt;()方法&lt;/p&gt;
&lt;p&gt;⭐&amp;lt; clinit &amp;gt;()方法由静态类变量显示赋值代码和静态代码块组成&lt;/p&gt;
&lt;p&gt;⭐类变量显示赋值代码和静态代码块从上到下顺序执行&lt;/p&gt;
&lt;p&gt;⭐ &amp;lt; clinit &amp;gt;()方法只执行一次&lt;/p&gt;
&lt;h2&gt;实例初始化过程&lt;/h2&gt;
&lt;p&gt;1、实例初始化就是执行&amp;lt; init &amp;gt;()方法&lt;br /&gt;
※ &amp;lt; init &amp;gt;()方法可能重载有多个，有几个构造器就有几个&amp;lt; init &amp;gt;()方法。&lt;br /&gt;
※&amp;lt; init &amp;gt;()方法由非静态实例变量显示赋值代码和非静态代码块、对应构造器代码组成。&lt;br /&gt;
※ 非静态实例变量显示赋值代码块和非静态代码块代码从上到下顺序执行，而对应构造器的代码最后执行。&lt;/p&gt;
&lt;h1&gt;哪些方法不可以被重写&lt;/h1&gt;
&lt;p&gt;final方法，静态方法，private等子类中不可见方法&lt;/p&gt;
&lt;h1&gt;局部变量与成员变量的区别：&lt;/h1&gt;
&lt;p&gt;1、作用域：&lt;/p&gt;
&lt;p&gt;局部变量：从声明处开始，到所属的 } 结束；&lt;/p&gt;
&lt;p&gt;实例变量：在当前类中“this.”(可省略)，在其他类中“对象名.”访问&lt;/p&gt;
&lt;p&gt;类变量：在当前类中“类名.”（可省略），在其他类中“类名.”或“对象名.”访问。&lt;/p&gt;
&lt;p&gt;2、生命周期：&lt;/p&gt;
&lt;p&gt;局部变量：每一个线程，每一次调用执行都是新的生命周期&lt;/p&gt;
&lt;p&gt;实例变量：随着对象的创建而初始化，随着对象的被回收而消亡，每个对象的实例变量是独立的&lt;/p&gt;
&lt;p&gt;类变量：随着类的初始化而初始化，随着类的卸载而消亡，该类的所有对象的类变量都是共享的。&lt;br /&gt;
如下：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;public class Exam{
    static int s;  // 成员变量，类变量
    int i;  // 成员变量，实例变量
    int j;  // 成员变量，实例变量
    {
        int i = 1; // 非静态代码块中的局部变量i
        i++;
        j++;
        s++;   
    }
    public void test(int j){ // 形参，局部变量j
        j++;
        i++;
        s++;
    }
    public static void main(String[] args){ // 形参，局部变量，args
      Exam obj1 = new Exam(); //局部变量，obj1
      Exam obj2 = new Exam(); //局部变量，obj2
      obj1.test(10);
      obj1.test(20);
      obj2.test(30);
      System.out.println(obj1.i +&quot;,&quot;+obj1.j+&quot;,&quot;+obj1.s);
      System.out.println(obj2.i +&quot;,&quot;+obj2.j+&quot;,&quot;+obj2.s);
    }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;代码结果为：2，1，5和1，1，5&lt;/p&gt;
&lt;h2&gt;1.JDK 和 JRE 有什么区别？&lt;/h2&gt;
&lt;p&gt;JDK：Java Development Kit 的简称，JDK是整个JAVA的核心，包括了Java运行环境JRE，一堆Java工具和Java基础的类库。通过JDK,开发人员将源码文件(java文件)编译成字节码文件(class文件)。&lt;br /&gt;
JRE：Java Runtime Environment 的简称，Java 运行环境，不含开发环境，即没有编译器和调试器。将class文件加载到内存准备运行。&lt;/p&gt;
&lt;h2&gt;2.== 和 equals 的区别是什么？&lt;/h2&gt;
&lt;p&gt;== 解读:&lt;br /&gt;
对于基本类型和引用类型 == 的作用效果是不同的，如下所示：&lt;/p&gt;
&lt;p&gt;基本类型：比较的是值是否相同；&lt;br /&gt;
引用类型：比较的是引用是否相同；&lt;br /&gt;
代码示例：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;String x = &quot;string&quot;;
String y = &quot;string&quot;;
String z = new String(&quot;string&quot;);
System.out.println(x==y); // true
System.out.println(x==z); // false
System.out.println(x.equals(y)); // true
System.out.println(x.equals(z)); // true
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;代码解读：因为 x 和 y 指向的是同一个引用，所以 == 也是 true，而 new String()方法则重写开辟了内存空间，所以 == 结果为 false，而 equals 比较的一直是值，所以结果都为 true。&lt;/p&gt;
&lt;p&gt;equals 解读:&lt;br /&gt;
equals 本质上就是 ==，只不过 String 和 Integer 等重写了 equals 方法，把它变成了值比较。看下面的代码就明白了。&lt;/p&gt;
&lt;p&gt;首先来看默认情况下 equals 比较一个有相同值的对象，代码如下：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;class Cat {
    public Cat(String name) {
        this.name = name;
    }
    private String name;
    public String getName() {
        return name;
    }
    public void setName(String name){
        this.name = name;
    }
}
Cat c1 = new Cat(&quot;精彩猿笔记&quot;);
Cat c2 = new Cat(&quot;精彩猿笔记&quot;);
System.out.println(c1.equals(c2)); // false
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;输出结果出乎我们的意料，竟然是 false？这是怎么回事，看了 equals 源码就知道了，源码如下：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;public boolean equals(Object obj) {
    return (this == obj);
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;原来 equals 本质上就是 ==。&lt;br /&gt;
那问题来了，两个相同值的 String 对象，为什么返回的是 true？代码如下：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;String s1 = new String(&quot;精彩猿笔记&quot;);
String s2 = new String(&quot;精彩猿笔记&quot;);
System.out.println(s1.equals(s2)); // true
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;同样的，当我们进入 String 的 equals 方法，找到了答案，代码如下：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;public boolean equals(Object anObject) {
       if (this == anObject) {
           return true;
       }
       if (anObject instanceof String) {
           String anotherString = (String) anObject;
           int n = value.length;
           if (n == anotherString.value.length) {
               char v1[] = value;
               char v2[] = anotherString.value;
               int i = 0;
               while (n-- != 0) {
                   if (v1[i] != v2[i])
                       return false;
                   i++;
               }
               return true;
           }
       }
       return false;
   }
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;原来是 String 重写了 Object 的 equals 方法，把引用比较改成了值比较。&lt;br /&gt;
总结 ：== 对于基本类型来说是值比较，对于引用类型来说是比较的是引用；而 equals 默认情况下是引用比较，只是很多类重新了 equals 方法，比如 String、Integer 等把它变成了值比较，所以一般情况下 equals 比较的是值是否相等。&lt;/p&gt;
&lt;h2&gt;3.两个对象的 hashCode() 相同，则 equals() 也一定为 true，对吗？&lt;/h2&gt;
&lt;p&gt;不对，(1)如果两个对象相同（equals方法返回true），那么它们的hashCode值一定要相同；(2)如果两个对象的hashCode相同，它们并不一定相同。&lt;br /&gt;
代码示例：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;String str1 = &quot;精彩&quot;;
String str2 = &quot;笔记&quot;;
System. out. println(String. format(&quot;str1：%d | str2：%d&quot;, str1. hashCode(),str2. hashCode()));
System. out. println(str1. equals(str2));
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;执行的结果：&lt;br /&gt;
str1：1179395 | str2：1179395&lt;br /&gt;
false&lt;/p&gt;
&lt;p&gt;代码解读：很显然“精彩”和“笔记”的 hashCode() 相同，然而 equals() 则为 false，因为在散列表中，hashCode() 相等即两个键值对的哈希值相等，然而哈希值相等，并不一定能得出键值对相等。&lt;/p&gt;
&lt;h2&gt;4.final 在 Java 中有什么作用？&lt;/h2&gt;
&lt;p&gt;final 修饰的类叫最终类，该类不能被继承。&lt;/p&gt;
&lt;p&gt;final 修饰的方法不能被重写。&lt;/p&gt;
&lt;p&gt;final 修饰的变量叫常量，常量必须初始化，初始化之后值就不能被修改。&lt;/p&gt;
&lt;p&gt;final修饰的对象只是引用地址不可变，内容还是能变的&lt;/p&gt;
&lt;h3&gt;string类为什么是final类型&lt;/h3&gt;
&lt;p&gt;为了实现字符串池(只有当字符是不可变的，字符串池才有可能实现)&lt;br /&gt;
为了线程安全(字符串自己便是线程安全的)&lt;br /&gt;
为了实现String可以创建HashCode不可变性&lt;/p&gt;
&lt;h2&gt;5.Java 中的 Math. round(-1. 5) 等于多少？&lt;/h2&gt;
&lt;p&gt;等于 -1。round()是四舍五入，注意负数5是舍的，例如：Math.round(1.5)值是2，Math.round(-1.5)值是-1。&lt;/p&gt;
&lt;h2&gt;6.String 属于基础的数据类型吗？&lt;/h2&gt;
&lt;p&gt;String 不属于基础类型，基础类型有 8 种：byte、boolean、char、short、int、float、long、double，除了基本类型（primitive type），剩下的都是引用类型（reference type），Java 5以后引入的枚举类型也算是一种比较特殊的引用类型。&lt;/p&gt;
&lt;h2&gt;7.Java 中操作字符串都有哪些类？它们之间有什么区别？&lt;/h2&gt;
&lt;p&gt;操作字符串的类有：String、StringBuffer、StringBuilder。&lt;br /&gt;
三者区别：&lt;/p&gt;
&lt;p&gt;StringBuffer和StringBuilder都继承自抽象类AbstractStringBuilder。&lt;/p&gt;
&lt;p&gt;String 声明的是不可变的对象，每次操作都会生成新的 String 对象，然后将指针指向新的 String 对象，而 StringBuffer、StringBuilder 存储数据的字符数组没有被final修饰，说明值可以改变，抽象类AbstractStringBuilder内部都提供了一个自动扩容机制，当发现长度不够的时候(初始默认长度是16)，会自动进行扩容工作，扩展为原数组长度的2倍加2，创建一个新的数组，并将数组的数据复制到新数组，所以对于拼接字符串效率要比String要高。&lt;/p&gt;
&lt;p&gt;线程安全性：StringBuffer由于很多方法都被 synchronized 修饰了所以线程安全，但是当多线程访问时，加锁和释放锁的过程很平凡，所以效率相比StringBuilder要低。StringBuilder相反执行效率高，但是线程不安全。所以单线程环境下推荐使用 StringBuilder，多线程环境下推荐使用 StringBuffer。&lt;/p&gt;
&lt;p&gt;执行速度:StringBuilder &amp;gt; StringBuffer &amp;gt; String。&lt;/p&gt;
&lt;h2&gt;8.String str=”i”与 String str=new String(“i”)一样吗？&lt;/h2&gt;
&lt;p&gt;不一样，因为内存的分配方式不一样。String str=“i”的方式，Java 虚拟机会将其分配到常量池中，如果常量池中有”i”，就返回”i”的地址，如果没有就创建”i”，然后返回”i”的地址；而 String str=new String(“i”) 则会被分到堆内存中新开辟一块空间。&lt;/p&gt;
&lt;h2&gt;9.如何将字符串反转？&lt;/h2&gt;
&lt;p&gt;使用 StringBuilder 或者 stringBuffer 的 reverse() 方法。&lt;br /&gt;
示例代码：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;// StringBuffer reverse
StringBuffer stringBuffer = new StringBuffer();
stringBuffer. append(&quot;abcdefg&quot;);
System. out. println(stringBuffer. reverse()); // gfedcba
// StringBuilder reverse
StringBuilder stringBuilder = new StringBuilder();
stringBuilder. append(&quot;abcdefg&quot;);
System. out. println(stringBuilder. reverse()); // gfedcba
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;10.String 类的常用方法都有那些？&lt;/h2&gt;
&lt;p&gt;indexOf()：返回指定字符的索引。&lt;/p&gt;
&lt;p&gt;charAt()：返回指定索引处的字符。&lt;/p&gt;
&lt;p&gt;replace()：字符串替换。&lt;/p&gt;
&lt;p&gt;trim()：去除字符串两端空白。&lt;/p&gt;
&lt;p&gt;split()：分割字符串，返回一个分割后的字符串数组。&lt;/p&gt;
&lt;p&gt;getBytes()：返回字符串的 byte 类型数组。&lt;/p&gt;
&lt;p&gt;length()：返回字符串长度。&lt;/p&gt;
&lt;p&gt;toLowerCase()：将字符串转成小写字母。&lt;/p&gt;
&lt;p&gt;toUpperCase()：将字符串转成大写字符。&lt;/p&gt;
&lt;p&gt;substring()：截取字符串。&lt;/p&gt;
&lt;p&gt;equals()：字符串比较。&lt;/p&gt;
&lt;h2&gt;11.抽象类必须要有抽象方法吗？&lt;/h2&gt;
&lt;p&gt;不需要，抽象类不一定非要有抽象方法；但是包含一个抽象方法的类一定是抽象类。&lt;br /&gt;
示例代码：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;abstract class Cat {
    public static void sayHi() {
        System. out. println(&quot;hi~&quot;);
      }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;上面代码，抽象类并没有抽象方法但完全可以正常运行。&lt;/p&gt;
&lt;h2&gt;12.普通类和抽象类有哪些区别？&lt;/h2&gt;
&lt;p&gt;普通类不能包含抽象方法，抽象类可以包含抽象方法。&lt;/p&gt;
&lt;p&gt;抽象类是不能被实例化的，就是不能用new调出构造方法创建对象，普通类可以直接实例化。&lt;/p&gt;
&lt;p&gt;如果一个类继承于抽象类，则该子类必须实现父类的抽象方法。如果子类没有实现父类的抽象方法，则必须将子类也定义为abstract类。&lt;/p&gt;
&lt;h2&gt;13.抽象类能使用 final 修饰吗？&lt;/h2&gt;
&lt;p&gt;不能，定义抽象类就是让其他类继承的，如果定义为 final 该类就不能被继承，这样彼此就会产生矛盾，所以 final 不能修饰抽象类，如下图所示，编辑器也会提示错误信息：&lt;/p&gt;
&lt;h2&gt;14.接口和抽象类有什么区别？&lt;/h2&gt;
&lt;p&gt;实现：抽象类的子类使用 extends 来继承；接口必须使用 implements 来实现接口。&lt;br /&gt;
构造函数：抽象类可以有构造函数；接口不能有。&lt;br /&gt;
实现数量：类可以实现很多个接口；但只能继承一个抽象类【java只支持单继承】。&lt;br /&gt;
访问修饰符：接口中的方法默认使用 public 修饰；抽象类中的抽象方法可以使用Public和Protected修饰，如果抽象方法修饰符为Private，则报错：The abstract method 方法名 in type Test can only set a visibility modifier, one of public or protected。&lt;/p&gt;
&lt;h2&gt;15.Java 中 IO 流分为几种？&lt;/h2&gt;
&lt;p&gt;按功能来分：输入流（input）、输出流（output）。&lt;br /&gt;
按类型来分：字节流和字符流。&lt;br /&gt;
字节流和字符流的区别是：字节流按 8 位传输以字节为单位输入输出数据，字符流按 16 位传输以字符为单位输入输出数据。&lt;/p&gt;
&lt;h2&gt;16.BIO、NIO、AIO 有什么区别？&lt;/h2&gt;
&lt;p&gt;BIO：Block IO 同步阻塞式 IO，就是我们平常使用的传统 IO，它的特点是模式简单使用方便，并发处理能力低。&lt;br /&gt;
NIO：New IO 同步非阻塞 IO，是传统 IO 的升级，客户端和服务器端通过 Channel（通道）通讯，实现了多路复用。&lt;br /&gt;
AIO：Asynchronous IO 是 NIO 的升级，也叫 NIO2，实现了异步非堵塞 IO ，异步 IO 的操作基于事件和回调机制。&lt;/p&gt;
&lt;h2&gt;17.Files的常用方法都有哪些？&lt;/h2&gt;
&lt;p&gt;Files. exists()：检测文件路径是否存在。&lt;br /&gt;
Files. createFile()：创建文件。&lt;br /&gt;
Files. createDirectory()：创建文件夹。&lt;br /&gt;
Files. delete()：删除一个文件或目录。&lt;br /&gt;
Files. copy()：复制文件。&lt;br /&gt;
Files. move()：移动文件。&lt;br /&gt;
Files. size()：返回文件的字节大小。&lt;br /&gt;
Files. readAllBytes() / readAllLines()：读取文件内容。&lt;br /&gt;
Files. write()：写入文件。&lt;/p&gt;
&lt;p&gt;====================================================================&lt;/p&gt;
&lt;h1&gt;二. Java 容器模块&lt;/h1&gt;
&lt;h2&gt;18.Java 容器都有哪些？&lt;/h2&gt;
&lt;p&gt;Java 容器分为 Collection 和 Map 两大类，其下又有很多子类，如下所示：&lt;br /&gt;
Collection、&lt;br /&gt;
List、&lt;br /&gt;
ArrayList、&lt;br /&gt;
LinkedList、&lt;br /&gt;
Vector、&lt;br /&gt;
Stack、&lt;br /&gt;
Set、&lt;br /&gt;
HashSet、&lt;br /&gt;
LinkedHashSet、&lt;br /&gt;
TreeSet、&lt;br /&gt;
Map、&lt;br /&gt;
HashMap、&lt;br /&gt;
LinkedHashMap、&lt;br /&gt;
TreeMap、&lt;br /&gt;
ConcurrentHashMap、&lt;br /&gt;
Hashtable、&lt;/p&gt;
&lt;h2&gt;19.Collection 和 Collections 有什么区别？&lt;/h2&gt;
&lt;p&gt;Collection 是一个集合接口，它提供了对集合对象进行基本操作的通用接口方法，所有集合都是它的子类，比如 List、Set 等。&lt;br /&gt;
Collections 是一个包装类，包含了很多静态方法，不能被实例化，就像一个工具类，比如提供的排序方法： Collections. sort(list)。&lt;/p&gt;
&lt;h2&gt;20.List、Set、Map 之间的区别是什么？&lt;/h2&gt;
&lt;p&gt;List、Set、Map 的区别主要体现在两个方面：元素是否有序、是否允许元素重复。&lt;br /&gt;
三者之间的区别，如下表：&lt;/p&gt;
&lt;h2&gt;21. HashMap 和 Hashtable 有什么区别？&lt;/h2&gt;
&lt;p&gt;HashMap是继承自AbstractMap类，而HashTable是继承自Dictionary类。不过它们都实现了同时实现了map、Cloneable（可复制）、Serializable（可序列化）这三个接口。&lt;br /&gt;
Hashtable比HashMap多提供了elments() 和contains() 两个方法。&lt;br /&gt;
HashMap的key-value支持key-value，null-null，key-null，null-value四种。而Hashtable只支持key-value一种（即key和value都不为null这种形式）。既然HashMap支持带有null的形式，那么在HashMap中不能由get()方法来判断HashMap中是否存在某个键， 而应该用containsKey()方法来判断，因为使用get的时候，当返回null时，你无法判断到底是不存在这个key，还是这个key就是null，还是key存在但value是null。&lt;br /&gt;
线程安全性不同：HashMap的方法都没有使用synchronized关键字修饰，都是非线程安全的，而Hashtable的方法几乎都是被synchronized关键字修饰的。但是，当我们需要HashMap是线程安全的时，怎么办呢？我们可以通过Collections.synchronizedMap(hashMap)来进行处理，亦或者我们使用线程安全的ConcurrentHashMap。ConcurrentHashMap虽然也是线程安全的，但是它的效率比Hashtable要高好多倍。因为ConcurrentHashMap使用了分段锁，并不对整个数据进行锁定。&lt;br /&gt;
初始容量大小和每次扩充容量大小的不同：Hashtable默认的初始大小为11，之后每次扩充，容量变为原来的2n+1。HashMap默认的初始化大小为16。之后每次扩充，容量变为原来的2倍。&lt;br /&gt;
计算hash值的方法不同：为了得到元素的位置，首先需要根据元素的 KEY计算出一个hash值，然后再用这个hash值来计算得到最终的位置。Hashtable直接使用对象的hashCode。hashCode是JDK根据对象的地址或者字符串或者数字算出来的int类型的数值。然后再使用除留余数发来获得最终的位置。&lt;/p&gt;
&lt;h2&gt;22.如何决定使用 HashMap 还是 TreeMap？&lt;/h2&gt;
&lt;p&gt;对于在 Map 中插入、删除、定位一个元素这类操作，HashMap 是最好的选择，因为相对而言 HashMap 的插入会更快，但如果你要对一个 key 集合进行有序的遍历，那 TreeMap 是更好的选择。&lt;/p&gt;
&lt;h2&gt;23.说一下 HashMap 的实现原理？&lt;/h2&gt;
&lt;p&gt;HashMap 基于 Hash 算法实现的，我们通过 put(key,value)存储，get(key)来获取。当传入 key 时，HashMap 会根据 key. hashCode() 计算出 hash 值，根据 hash 值将 value 保存在 bucket 里。当计算出的 hash 值相同时，我们称之为 hash 冲突，HashMap 的做法是用链表和红黑树存储相同 hash 值的 value。当 hash 冲突的个数比较少时，使用链表否则使用红黑树。&lt;/p&gt;
&lt;h2&gt;24.说一下 HashSet 的实现原理？&lt;/h2&gt;
&lt;p&gt;HashSet 是基于 HashMap 实现的，HashSet 底层使用 HashMap 来保存所有元素，因此 HashSet 的实现比较简单，相关 HashSet 的操作，基本上都是直接调用底层 HashMap 的相关方法来完成，HashSet 不允许重复的值。&lt;/p&gt;
&lt;h2&gt;25.ArrayList 和 LinkedList 的区别是什么？&lt;/h2&gt;
&lt;p&gt;数据结构实现：ArrayList 是动态数组的数据结构实现，而 LinkedList 是双向链表的数据结构实现。&lt;br /&gt;
随机访问效率：ArrayList 比 LinkedList 在随机访问的时候效率要高，因为 LinkedList 是线性的数据存储方式，所以需要移动指针从前往后依次查找。&lt;br /&gt;
增加和删除效率：在非首尾的增加和删除操作，LinkedList 要比 ArrayList 效率要高，因为 ArrayList 增删操作要影响数组内的其他数据的下标。&lt;br /&gt;
综合来说，在需要频繁读取集合中的元素时，更推荐使用 ArrayList，而在插入和删除操作较多时，更推荐使用 LinkedList。&lt;/p&gt;
&lt;h2&gt;26.如何实现数组和 List 之间的转换？&lt;/h2&gt;
&lt;p&gt;数组转 List：使用 Arrays. asList(array) 进行转换。&lt;br /&gt;
List 转数组：使用 List 自带的 toArray() 方法。&lt;br /&gt;
代码示例：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;// list to array
List&amp;lt;String&amp;gt; list = new ArrayList&amp;lt;String&amp;gt;();
list. add(&quot;☞精◈彩◈猿◈笔◈记☜&quot;);
list. add(&quot;的博客&quot;);
list. toArray();
// array to list
String[] array = new String[]{&quot;☞精◈彩◈猿◈笔◈记☜&quot;,&quot;的博客&quot;};
Arrays. asList(array);
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;27.ArrayList 和 Vector 的区别是什么？&lt;/h2&gt;
&lt;p&gt;线程安全：Vector 使用了 Synchronized 来实现线程同步，是线程安全的，而 ArrayList 是非线程安全的。&lt;br /&gt;
性能：ArrayList 在性能方面要优于 Vector。&lt;br /&gt;
扩容：ArrayList 和 Vector 都会根据实际的需要动态的调整容量，只不过在 Vector 扩容每次会增加 1 倍，而 ArrayList 只会增加 50%。&lt;/p&gt;
&lt;h2&gt;28.Array 和 ArrayList 有何区别？&lt;/h2&gt;
&lt;p&gt;Array 可以存储基本数据类型和对象，ArrayList 只能存储对象。&lt;br /&gt;
Array 是指定固定大小的，而 ArrayList 大小是自动扩展的。&lt;br /&gt;
Array 内置方法没有 ArrayList 多，比如 addAll、removeAll、iteration 等方法只有 ArrayList 有。&lt;/p&gt;
&lt;h2&gt;29.在 Queue 中 poll()和 remove()有什么区别？&lt;/h2&gt;
&lt;p&gt;相同点：都是返回第一个元素，并在队列中删除返回的对象。&lt;br /&gt;
不同点：如果没有元素 remove()会直接抛出NoSuchElementException 异常，而 poll()会返回 null。&lt;br /&gt;
代码示例：&lt;/p&gt;
&lt;p&gt;Queue queue = new LinkedList();&lt;br /&gt;
queue. offer(“string”); // add&lt;br /&gt;
System. out. println(queue. poll());&lt;br /&gt;
System. out. println(queue. remove());&lt;br /&gt;
System. out. println(queue. size());&lt;/p&gt;
&lt;h2&gt;30.哪些集合类是线程安全的？&lt;/h2&gt;
&lt;p&gt;Vector、Hashtable、Stack 都是线程安全的，而像 HashMap 则是非线程安全的，不过在 JDK 1.5 之后随着 Java. util. concurrent 并发包的出现，它们也有了自己对应的线程安全类，比如 HashMap 对应的线程安全类就是 ConcurrentHashMap。&lt;/p&gt;
&lt;h2&gt;31.迭代器 Iterator 是什么？&lt;/h2&gt;
&lt;p&gt;Iterator 接口提供遍历任何 Collection 的接口。我们可以从一个 Collection 中使用迭代器方法来获取迭代器实例。迭代器取代了 Java 集合框架中的 Enumeration，迭代器允许调用者在迭代过程中移除元素。&lt;/p&gt;
&lt;h2&gt;32.Iterator 怎么使用？有什么特点？&lt;/h2&gt;
&lt;p&gt;Iterator 使用代码如下：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;List&amp;lt;String&amp;gt; list = new ArrayList&amp;lt;&amp;gt;();
Iterator&amp;lt;String&amp;gt; it = list. iterator();
while(it. hasNext()){
String obj = it. next();
System. out. println(obj);
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Iterator 的特点是更加安全，因为它可以确保，在当前遍历的集合元素被更改的时候，就会抛出 ConcurrentModificationException 异常。&lt;/p&gt;
&lt;h2&gt;33.Iterator 和 ListIterator 有什么区别？&lt;/h2&gt;
&lt;p&gt;Iterator 可以遍历 Set 和 List 集合，而 ListIterator 只能遍历 List。&lt;br /&gt;
Iterator 只能单向遍历，而 ListIterator 可以双向遍历（向前/后遍历）。&lt;br /&gt;
ListIterator 从 Iterator 接口继承，然后添加了一些额外的功能，比如添加一个元素、替换一个元素、获取前面或后面元素的索引位置。&lt;/p&gt;
&lt;h2&gt;34.怎么确保一个集合不能被修改？&lt;/h2&gt;
&lt;p&gt;可以使用 Collections. unmodifiableCollection(Collection c) 方法来创建一个只读集合，这样改变集合的任何操作都会抛出 Java. lang. UnsupportedOperationException 异常。&lt;br /&gt;
示例代码如下：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;List&amp;lt;String&amp;gt; list = new ArrayList&amp;lt;&amp;gt;();
list. add(&quot;x&quot;);
Collection&amp;lt;String&amp;gt; clist = Collections. unmodifiableCollection(list);
clist. add(&quot;y&quot;); // 运行时此行报错
System. out. println(list. size());
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;====================================================================&lt;/p&gt;
&lt;h1&gt;三. Java 多线程模块&lt;/h1&gt;
&lt;h2&gt;35.并行和并发有什么区别？&lt;/h2&gt;
&lt;p&gt;并行：多个处理器或多核处理器同时处理多个任务。&lt;br /&gt;
并发：多个任务在同一个 CPU 核上，按细分的时间片轮流(交替)执行，从逻辑上来看那些任务是同时执行。&lt;br /&gt;
如下图：【并发 = 两个队列和一台咖啡机】 【并行 = 两个队列和两台咖啡机】&lt;/p&gt;
&lt;h2&gt;36.线程和进程的区别？&lt;/h2&gt;
&lt;p&gt;一个程序下至少有一个进程，一个进程下至少有一个线程，一个进程下也可以有多个线程来增加程序的执行速度。&lt;/p&gt;
&lt;h2&gt;37.守护线程是什么？&lt;/h2&gt;
&lt;p&gt;守护线程是运行在后台的一种特殊进程。它独立于控制终端并且周期性地执行某种任务或等待处理某些发生的事件。在 Java 中垃圾回收线程就是特殊的守护线程。&lt;/p&gt;
&lt;h2&gt;38.多线程有几种实现方式？&lt;/h2&gt;
&lt;p&gt;有4种，分别是：&lt;/p&gt;
&lt;p&gt;继承Thread类&lt;br /&gt;
实现Runnable接口&lt;br /&gt;
实现Callable接口通过FutureTask包装器来创建Thread线程&lt;br /&gt;
通过线程池创建线程，使用线程池接口ExecutorService结合Callable、Future实现有返回结果的多线程。&lt;br /&gt;
前面两种【无返回值】原因：通过重写run方法，run方法的返回值是void，所以没有办法返回结果。&lt;br /&gt;
后面两种【有返回值】原因：通过Callable接口，就要实现call方法，这个方法的返回值是Object，所以返回的结果可以放在Object对象中。&lt;/p&gt;
&lt;h2&gt;39.说一下 Runnable和 Callable有什么区别？&lt;/h2&gt;
&lt;p&gt;Runnable没有返回值，Callable可以拿到有返回值，Callable可以看作是 Runnable的补充。&lt;/p&gt;
&lt;h2&gt;40.线程有哪些状态？&lt;/h2&gt;
&lt;p&gt;线程的6种状态：&lt;/p&gt;
&lt;p&gt;初始(NEW)：新创建了一个线程对象，但还没有调用start()方法。&lt;br /&gt;
运行(RUNNABLE)：Java线程中将就绪（ready）和运行中（running）两种状态笼统的称为“运行”。线程对象创建后，其他线程(比如main线程）调用了该对象的start()方法。该状态的线程位于可运行线程池中，等待被线程调度选中，获取CPU的使用权，此时处于就绪状态（ready）。就绪状态的线程在获得CPU时间片后变为运行中状态（running）。&lt;br /&gt;
阻塞(BLOCKED)：表示线程阻塞于锁。&lt;br /&gt;
等待(WAITING)：进入该状态的线程需要等待其他线程做出一些特定动作（通知或中断）。&lt;br /&gt;
超时等待(TIMED_WAITING)：该状态不同于WAITING，它可以在指定的时间后自行返回。&lt;br /&gt;
终止(TERMINATED)：表示该线程已经执行完毕。&lt;/p&gt;
&lt;h2&gt;41.sleep() 和 wait() 有什么区别？&lt;/h2&gt;
&lt;p&gt;类的不同：sleep() 来自 Thread，wait() 来自 Object。&lt;br /&gt;
释放锁：sleep() 不释放锁；wait() 释放锁。&lt;br /&gt;
用法不同：sleep() 时间到会自动恢复；wait() 可以使用 notify()/notifyAll()直接唤醒。&lt;/p&gt;
&lt;h2&gt;42.notify()和 notifyAll()有什么区别？&lt;/h2&gt;
&lt;p&gt;notifyAll()会唤醒所有的线程，notify()之后唤醒一个线程。notifyAll() 调用后，会将全部线程由等待池移到锁池，然后参与锁的竞争，竞争成功则继续执行，如果不成功则留在锁池等待锁被释放后再次参与竞争。而 notify()只会唤醒一个线程，具体唤醒哪一个线程由虚拟机控制。&lt;/p&gt;
&lt;h2&gt;43.线程的 run() 和 start() 有什么区别？&lt;/h2&gt;
&lt;p&gt;start() 方法用于启动线程，run() 方法用于执行线程的运行时代码。run() 可以重复调用，而 start() 只能调用一次。&lt;/p&gt;
&lt;h2&gt;44.创建线程池有哪几种方式？&lt;/h2&gt;
&lt;p&gt;线程池创建有七种方式，最核心的是最后一种：&lt;/p&gt;
&lt;p&gt;newSingleThreadExecutor()：它的特点在于工作线程数目被限制为 1，操作一个无界的工作队列，所以它保证了所有任务的都是被顺序执行，最多会有一个任务处于活动状态，并且不允许使用者改动线程池实例，因此可以避免其改变线程数目；&lt;br /&gt;
newCachedThreadPool()：它是一种用来处理大量短时间工作任务的线程池，具有几个鲜明特点：它会试图缓存线程并重用，当无缓存线程可用时，就会创建新的工作线程；如果线程闲置的时间超过 60 秒，则被终止并移出缓存；长时间闲置时，这种线程池，不会消耗什么资源。其内部使用 SynchronousQueue 作为工作队列；&lt;br /&gt;
newFixedThreadPool(int nThreads)：重用指定数目（nThreads）的线程，其背后使用的是无界的工作队列，任何时候最多有 nThreads 个工作线程是活动的。这意味着，如果任务数量超过了活动队列数目，将在工作队列中等待空闲线程出现；如果有工作线程退出，将会有新的工作线程被创建，以补足指定的数目 nThreads；&lt;br /&gt;
newSingleThreadScheduledExecutor()：创建单线程池，返回 ScheduledExecutorService，可以进行定时或周期性的工作调度；&lt;br /&gt;
newScheduledThreadPool(int corePoolSize)：和newSingleThreadScheduledExecutor()类似，创建的是个 ScheduledExecutorService，可以进行定时或周期性的工作调度，区别在于单一工作线程还是多个工作线程；&lt;br /&gt;
newWorkStealingPool(int parallelism)：这是一个经常被人忽略的线程池，Java 8 才加入这个创建方法，其内部会构建ForkJoinPool，利用Work-Stealing算法，并行地处理任务，不保证处理顺序；&lt;br /&gt;
ThreadPoolExecutor()：是最原始的线程池创建，上面1-3创建方式都是对ThreadPoolExecutor的封装。&lt;/p&gt;
&lt;h2&gt;45.线程池都有哪些状态？&lt;/h2&gt;
&lt;p&gt;RUNNING：这是最正常的状态，接受新的任务，处理等待队列中的任务。&lt;br /&gt;
SHUTDOWN：不接受新的任务提交，但是会继续处理等待队列中的任务。&lt;br /&gt;
STOP：不接受新的任务提交，不再处理等待队列中的任务，中断正在执行任务的线程。&lt;br /&gt;
TIDYING：所有的任务都销毁了，workCount 为 0，线程池的状态在转换为 TIDYING 状态时，会执行钩子方法 terminated()。&lt;br /&gt;
TERMINATED：terminated()方法结束后，线程池的状态就会变成这个。&lt;/p&gt;
&lt;h2&gt;46.线程池中 submit() 和 execute() 方法有什么区别？&lt;/h2&gt;
&lt;p&gt;execute()：只能执行 Runnable 类型的任务。&lt;br /&gt;
submit()：可以执行 Runnable 和 Callable 类型的任务。&lt;br /&gt;
Callable 类型的任务可以获取执行的返回值，而 Runnable 执行无返回值。&lt;/p&gt;
&lt;h2&gt;47.在 Java 程序中怎么保证多线程的运行安全？&lt;/h2&gt;
&lt;p&gt;方法一：使用安全类，比如 Java. util. concurrent 下的类。&lt;br /&gt;
方法二：使用自动锁 synchronized。&lt;br /&gt;
方法三：使用手动锁 Lock。&lt;br /&gt;
手动锁 Java 示例代码如下：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;Lock lock = new ReentrantLock();
lock. lock();
try {
    System.out.println(&quot;获得锁&quot;);
} catch (Exception e) {
// TODO: handle exception
} finally {
    System.out.println(&quot;释放锁&quot;);
    lock.unlock();
}
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;48.多线程中 synchronized 锁升级的原理是什么？&lt;/h2&gt;
&lt;p&gt;synchronized 锁升级原理：在锁对象的对象头里面有一个 threadid 字段，在第一次访问的时候 threadid 为空，jvm 让其持有偏向锁，并将 threadid 设置为其线程 id，再次进入的时候会先判断 threadid 是否与其线程 id 一致，如果一致则可以直接使用此对象，如果不一致，则升级偏向锁为轻量级锁，通过自旋循环一定次数来获取锁，执行一定次数之后，如果还没有正常获取到要使用的对象，此时就会把锁从轻量级升级为重量级锁，此过程就构成了 synchronized 锁的升级。&lt;br /&gt;
锁的升级的目的：锁升级是为了减低了锁带来的性能消耗。在 Java 6 之后优化 synchronized 的实现方式，使用了偏向锁升级为轻量级锁再升级到重量级锁的方式，从而减低了锁带来的性能消耗。&lt;/p&gt;
&lt;h2&gt;49.什么是死锁？&lt;/h2&gt;
&lt;p&gt;当线程 A 持有独占锁a，并尝试去获取独占锁 b 的同时，线程 B 持有独占锁 b，并尝试获取独占锁 a 的情况下，就会发生 AB 两个线程由于互相持有对方需要的锁，而发生的阻塞现象，我们称为死锁。&lt;/p&gt;
&lt;h2&gt;50.怎么防止死锁？&lt;/h2&gt;
&lt;p&gt;尽量使用 tryLock(long timeout, TimeUnit unit)的方法(ReentrantLock、ReentrantReadWriteLock)，设置超时时间，超时可以退出防止死锁。&lt;br /&gt;
尽量使用 Java. util. concurrent 并发类代替自己手写锁。&lt;br /&gt;
尽量降低锁的使用粒度，尽量不要几个功能用同一把锁。&lt;br /&gt;
尽量减少同步的代码块。&lt;/p&gt;
&lt;h2&gt;51.ThreadLocal 是什么？有哪些使用场景？&lt;/h2&gt;
&lt;p&gt;ThreadLocal 为每个使用该变量的线程提供独立的变量副本，所以每一个线程都可以独立地改变自己的副本，而不会影响其它线程所对应的副本。&lt;br /&gt;
ThreadLocal 的经典使用场景是数据库连接和 session 管理等。&lt;/p&gt;
&lt;h2&gt;52.说一下 synchronized 底层实现原理？&lt;/h2&gt;
&lt;p&gt;synchronized 是由一对 monitorenter/monitorexit 指令实现的，monitor 对象是同步的基本实现单元。在 Java 6 之前，monitor 的实现完全是依靠操作系统内部的互斥锁，因为需要进行用户态到内核态的切换，所以同步操作是一个无差别的重量级操作，性能也很低。但在 Java 6 的时候，Java 虚拟机 对此进行了大刀阔斧地改进，提供了三种不同的 monitor 实现，也就是常说的三种不同的锁：偏向锁（Biased Locking）、轻量级锁和重量级锁，大大改进了其性能。&lt;/p&gt;
&lt;h2&gt;53.synchronized 和 volatile 的区别是什么？&lt;/h2&gt;
&lt;p&gt;volatile 是变量修饰符；synchronized 是修饰类、方法、代码段。&lt;br /&gt;
volatile 仅能实现变量的修改可见性，不能保证原子性；而 synchronized 则可以保证变量的修改可见性和原子性。&lt;br /&gt;
volatile 不会造成线程的阻塞；synchronized 可能会造成线程的阻塞。&lt;/p&gt;
&lt;h2&gt;54.synchronized 和 Lock 有什么区别？&lt;/h2&gt;
&lt;p&gt;synchronized 可以给类、方法、代码块加锁；而 lock 只能给代码块加锁。&lt;br /&gt;
synchronized 不需要手动获取锁和释放锁，使用简单，发生异常会自动释放锁，不会造成死锁；而 lock 需要自己加锁和释放锁，如果使用不当没有 unLock()去释放锁就会造成死锁。&lt;br /&gt;
通过 Lock 可以知道有没有成功获取锁，而 synchronized 却无法办到。&lt;/p&gt;
&lt;h2&gt;55.synchronized 和 ReentrantLock 区别是什么？&lt;/h2&gt;
&lt;p&gt;synchronized 早期的实现比较低效，对比 ReentrantLock，大多数场景性能都相差较大，但是在 Java 6 中对 synchronized 进行了非常多的改进。&lt;br /&gt;
主要区别如下：&lt;/p&gt;
&lt;p&gt;ReentrantLock 使用起来比较灵活，但是必须有释放锁的配合动作；&lt;br /&gt;
ReentrantLock 必须手动获取与释放锁，而 synchronized 不需要手动释放和开启锁；&lt;br /&gt;
ReentrantLock 只适用于代码块锁，而 synchronized 可用于修饰方法、代码块等。&lt;br /&gt;
ReentrantLock 标记的变量不会被编译器优化；synchronized 标记的变量可以被编译器优化。&lt;/p&gt;
&lt;h2&gt;56.说一下 atomic 的原理？&lt;/h2&gt;
&lt;p&gt;atomic 主要利用 CAS (Compare And Swap) 和 volatile 和 native 方法来保证原子操作，从而避免 synchronized 的高开销，执行效率大为提升。&lt;/p&gt;
&lt;p&gt;====================================================================&lt;/p&gt;
&lt;h1&gt;四. Java 反射模块&lt;/h1&gt;
&lt;h2&gt;57.什么是反射？&lt;/h2&gt;
&lt;p&gt;反射是在运行状态中，对于任意一个类，都能够知道这个类的所有属性和方法；对于任意一个对象，都能够调用它的任意一个方法和属性；这种动态获取的信息以及动态调用对象的方法的功能称为 Java 语言的反射机制。&lt;/p&gt;
&lt;h2&gt;58.什么是 Java 序列化？什么情况下需要序列化？&lt;/h2&gt;
&lt;p&gt;Java 序列化是为了保存各种对象在内存中的状态，并且可以把保存的对象状态再读出来。&lt;br /&gt;
以下情况需要使用 Java 序列化：&lt;/p&gt;
&lt;p&gt;想把的内存中的对象状态保存到一个文件中或者数据库中时候；&lt;br /&gt;
想用套接字在网络上传送对象的时候；&lt;br /&gt;
想通过RMI（远程方法调用）传输对象的时候。&lt;/p&gt;
&lt;h2&gt;59.动态代理是什么？有哪些应用？&lt;/h2&gt;
&lt;p&gt;动态代理是运行时动态生成代理类。&lt;br /&gt;
动态代理的应用有 spring aop、hibernate 数据查询、测试框架的后端 mock、rpc，Java注解对象获取等。&lt;/p&gt;
&lt;h2&gt;60.怎么实现动态代理？&lt;/h2&gt;
&lt;p&gt;JDK 原生动态代理和 cglib 动态代理。JDK 原生动态代理是基于接口实现的，而 cglib 是基于继承当前类的子类实现的。&lt;/p&gt;
&lt;p&gt;====================================================================&lt;/p&gt;
&lt;h1&gt;五. Java 对象拷贝模块&lt;/h1&gt;
&lt;h2&gt;61.为什么要使用克隆？&lt;/h2&gt;
&lt;p&gt;克隆的对象可能包含一些已经修改过的属性，而 new 出来的对象的属性都还是初始化时候的值，所以当需要一个新的对象来保存当前对象的“状态”就靠克隆方法了。&lt;/p&gt;
&lt;h2&gt;62.如何实现对象克隆？&lt;/h2&gt;
&lt;p&gt;实现 Cloneable 接口并重写 Object 类中的 clone() 方法。&lt;br /&gt;
实现 Serializable 接口，通过对象的序列化和反序列化实现克隆，可以实现真正的深度克隆。&lt;/p&gt;
&lt;h2&gt;63.深拷贝和浅拷贝区别是什么？&lt;/h2&gt;
&lt;p&gt;浅拷贝：当对象被复制时只复制它本身和其中包含的值类型的成员变量，而引用类型的成员对象并没有复制。&lt;br /&gt;
深拷贝：除了对象本身被复制外，对象所包含的所有成员变量也将复制。&lt;/p&gt;
&lt;p&gt;====================================================================&lt;/p&gt;
&lt;h1&gt;六. Java Web模块&lt;/h1&gt;
&lt;h2&gt;64.JSP 和 servlet 有什么区别？&lt;/h2&gt;
&lt;p&gt;JSP 是 servlet 技术的扩展，本质上就是 servlet 的简易方式。servlet 和 JSP 最主要的不同点在于，servlet 的应用逻辑是在 Java 文件中，并且完全从表示层中的 html 里分离开来，而 JSP 的情况是 Java 和 html 可以组合成一个扩展名为 JSP 的文件。JSP 侧重于视图，servlet 主要用于控制逻辑。&lt;/p&gt;
&lt;h2&gt;65.JSP 有哪些内置对象？作用分别是什么？&lt;/h2&gt;
&lt;p&gt;JSP 有 9 大内置对象：&lt;/p&gt;
&lt;p&gt;request：封装客户端的请求，其中包含来自 get 或 post 请求的参数；&lt;br /&gt;
response：封装服务器对客户端的响应；&lt;br /&gt;
pageContext：通过该对象可以获取其他对象；&lt;br /&gt;
session：封装用户会话的对象；&lt;br /&gt;
application：封装服务器运行环境的对象；&lt;br /&gt;
out：输出服务器响应的输出流对象；&lt;br /&gt;
config：Web 应用的配置对象；&lt;br /&gt;
page：JSP 页面本身（相当于 Java 程序中的 this）；&lt;br /&gt;
exception：封装页面抛出异常的对象。&lt;/p&gt;
&lt;h2&gt;66.说一下 JSP 的 4 种作用域？&lt;/h2&gt;
&lt;p&gt;page：代表与一个页面相关的对象和属性。&lt;br /&gt;
request：代表与客户端发出的一个请求相关的对象和属性。一个请求可能跨越多个页面，涉及多个 Web 组件；需要在页面显示的临时数据可以置于此作用域。&lt;br /&gt;
session：代表与某个用户与服务器建立的一次会话相关的对象和属性。跟某个用户相关的数据应该放在用户自己的 session 中。&lt;br /&gt;
application：代表与整个 Web 应用程序相关的对象和属性，它实质上是跨越整个 Web 应用程序，包括多个页面、请求和会话的一个全局作用域。&lt;/p&gt;
&lt;h2&gt;67.session 和 cookie 有什么区别？&lt;/h2&gt;
&lt;p&gt;session：是一种将会话状态保存在服务器端的技术。&lt;br /&gt;
Cookie ：是在 HTTP 协议下， Web 服务器保存在用户浏览器（客户端）上的小文本文件，它可以包含有关用户的信息。无论何时用户链接到服务器，Web 站点都可以访问 Cookie 信息 。&lt;/p&gt;
&lt;p&gt;存储位置不同：session 存储在服务器端；cookie 存储在浏览器端。&lt;br /&gt;
安全性不同：cookie 安全性一般，在浏览器存储，可以被伪造和修改。&lt;br /&gt;
容量和个数限制：cookie 有容量限制，每个站点下的 cookie 也有个数限制。&lt;br /&gt;
存储的多样性：session 可以存储在 Redis 中、数据库中、应用程序中；而 cookie 只能存储在浏览器中。&lt;/p&gt;
&lt;h2&gt;68.说一下 session 的工作原理？&lt;/h2&gt;
&lt;p&gt;session 的工作原理是客户端登录完成之后，服务器会创建对应的 session，session 创建完之后，会把 session 的 id 发送给客户端，客户端再存储到浏览器中。这样客户端每次访问服务器时，都会带着 sessionid，服务器拿到 sessionid 之后，在内存找到与之对应的 session 这样就可以正常工作了。&lt;/p&gt;
&lt;h2&gt;69.如果客户端禁止 cookie 能实现 session 还能用吗？&lt;/h2&gt;
&lt;p&gt;可以用，session 只是依赖 cookie 存储 sessionid，如果 cookie 被禁用了，可以使用 url 中添加 sessionid 的方式保证 session 能正常使用。&lt;/p&gt;
&lt;h2&gt;70.spring mvc 和 struts 的区别是什么？&lt;/h2&gt;
&lt;p&gt;拦截级别：struts2 是类级别的拦截；spring mvc 是方法级别的拦截。&lt;br /&gt;
数据独立性：spring mvc 的方法之间基本上独立的，独享 request 和 response 数据，请求数据通过参数获取，处理结果通过 ModelMap 交回给框架，方法之间不共享变量；而 struts2 虽然方法之间也是独立的，但其所有 action 变量是共享的，这不会影响程序运行，却给我们编码和读程序时带来了一定的麻烦。&lt;br /&gt;
拦截机制：struts2 有以自己的 interceptor 机制，spring mvc 用的是独立的 aop 方式，这样导致struts2 的配置文件量比 spring mvc 大。&lt;br /&gt;
对 ajax 的支持：spring mvc 集成了ajax，所有 ajax 使用很方便，只需要一个注解 @ResponseBody 就可以实现了；而 struts2 一般需要安装插件或者自己写代码才行。&lt;/p&gt;
&lt;h2&gt;71.如何避免 SQL 注入？&lt;/h2&gt;
&lt;p&gt;使用预处理 PreparedStatement。&lt;br /&gt;
使用正则表达式过滤掉字符中的特殊字符。&lt;/p&gt;
&lt;h2&gt;72.什么是 XSS 攻击，如何避免？&lt;/h2&gt;
&lt;p&gt;XSS 攻击：即跨站脚本攻击，它是 Web 程序中常见的漏洞。原理是攻击者往 Web 页面里插入恶意的脚本代码（css 代码、Javascript 代码等），当用户浏览该页面时，嵌入其中的脚本代码会被执行，从而达到恶意攻击用户的目的，如盗取用户 cookie、破坏页面结构、重定向到其他网站等。&lt;br /&gt;
预防 XSS 的核心是必须对输入的数据做过滤处理。&lt;/p&gt;
&lt;h2&gt;73.什么是 CSRF 攻击，如何避免？&lt;/h2&gt;
&lt;p&gt;CSRF：Cross-Site Request Forgery（中文：跨站请求伪造），可以理解为攻击者盗用了你的身份，以你的名义发送恶意请求，比如：以你名义发送邮件、发消息、购买商品，虚拟货币转账等。&lt;br /&gt;
防御手段：&lt;/p&gt;
&lt;p&gt;验证请求来源地址；&lt;br /&gt;
关键操作添加验证码；&lt;br /&gt;
在请求地址添加 token 并验证。&lt;/p&gt;
&lt;p&gt;====================================================================&lt;/p&gt;
&lt;h1&gt;七. Java 异常模块&lt;/h1&gt;
&lt;h2&gt;74.throw 和 throws 的区别？&lt;/h2&gt;
&lt;p&gt;throw：是真实抛出一个异常。&lt;br /&gt;
throws：是声明可能会抛出一个异常。&lt;/p&gt;
&lt;h2&gt;75.final、finally、finalize 有什么区别？&lt;/h2&gt;
&lt;p&gt;final：是修饰符，如果修饰类，此类不能被继承；如果修饰方法和变量，则表示此方法和此变量不能在被改变，只能使用。&lt;br /&gt;
finally：是 try{} catch{} finally{} 最后一部分，表示不论发生任何情况都会执行，finally 部分可以省略，但如果 finally 部分存在，则一定会执行 finally 里面的代码。&lt;br /&gt;
finalize： 是Object的protected方法，子类可以覆盖该方法以实现资源清理工作，GC在回收对象之前调用该方法。&lt;br /&gt;
76.try-catch-finally 中哪个部分可以省略？&lt;br /&gt;
try-catch-finally 其中 catch 和 finally 都可以被省略，但是不能同时省略，也就是说有 try 的时候，必须后面跟一个 catch 或者 finally。&lt;/p&gt;
&lt;h2&gt;77.try-catch-finally 中，如果 catch 中 return 了，finally 还会执行吗？&lt;/h2&gt;
&lt;p&gt;finally 一定会执行，即使是 catch 中 return 了，catch 中的 return 会等 finally 中的代码执行完之后，才会执行。&lt;/p&gt;
&lt;h2&gt;78.常见的异常类有哪些？&lt;/h2&gt;
&lt;p&gt;NullPointerException 空指针异常&lt;br /&gt;
ClassNotFoundException 指定类不存在&lt;br /&gt;
NumberFormatException 字符串转换为数字异常&lt;br /&gt;
IndexOutOfBoundsException 数组下标越界异常&lt;br /&gt;
ClassCastException 数据类型转换异常&lt;br /&gt;
FileNotFoundException 文件未找到异常&lt;br /&gt;
NoSuchMethodException 方法不存在异常&lt;br /&gt;
IOException IO 异常&lt;br /&gt;
SocketException Socket 异常&lt;/p&gt;
&lt;p&gt;====================================================================&lt;/p&gt;
&lt;h1&gt;八. 网络模块&lt;/h1&gt;
&lt;h2&gt;79.http 响应码 301 和 302 代表的是什么？有什么区别？&lt;/h2&gt;
&lt;p&gt;301：永久重定向；302：暂时重定向。&lt;br /&gt;
它们的区别是，301 对搜索引擎优化（SEO）更加有利；302 有被提示为网络拦截的风险。&lt;/p&gt;
&lt;h2&gt;80.forward 和 redirect 的区别？&lt;/h2&gt;
&lt;p&gt;forward 是转发 和 redirect 是重定向：&lt;br /&gt;
地址栏 url 显示：foward url 不会发生改变，redirect url 会发生改变；&lt;br /&gt;
数据共享：forward 可以共享 request 里的数据，redirect 不能共享；&lt;br /&gt;
效率：forward 比 redirect 效率高。&lt;/p&gt;
&lt;h2&gt;81.简述 tcp 和 udp的区别？&lt;/h2&gt;
&lt;p&gt;tcp 和 udp 是 OSI 模型中的运输层中的协议。tcp 提供可靠的通信传输，而 udp 则常被用于让广播和细节控制交给应用的通信传输。&lt;br /&gt;
两者的区别大致如下：&lt;/p&gt;
&lt;p&gt;tcp 面向连接，udp 面向非连接即发送数据前不需要建立链接；&lt;br /&gt;
tcp 提供可靠的服务（数据传输），udp 无法保证；&lt;br /&gt;
tcp 面向字节流，udp 面向报文；&lt;br /&gt;
tcp 数据传输慢，udp 数据传输快；&lt;/p&gt;
&lt;h2&gt;82.tcp 为什么要三次握手，两次不行吗？为什么？&lt;/h2&gt;
&lt;p&gt;我们假设A和B是通信的双方。我理解的握手实际上就是通信，发一次信息就是进行一次握手。&lt;/p&gt;
&lt;p&gt;第一次握手： A给B打电话说，你可以听到我说话吗？&lt;br /&gt;
第二次握手： B收到了A的信息，然后对A说： 我可以听得到你说话啊，你能听得到我说话吗？&lt;br /&gt;
第三次握手： A收到了B的信息，然后说可以的，我要给你发信息啦！&lt;br /&gt;
在三次握手之后，A和B都能确定这么一件事： 我说的话，你能听到； 你说的话，我也能听到。 这样，就可以开始正常通信了。&lt;br /&gt;
注意： HTTP是基于TCP协议的，所以每次都是客户端发送请求，服务器应答，但是TCP还可以给其他应用层提供服务，即可能A、B在建立链接之后，谁都可能先开始通信。&lt;/p&gt;
&lt;p&gt;如果采用两次握手，那么只要服务器发出确认数据包就会建立连接，但由于客户端此时并未响应服务器端的请求，那此时服务器端就会一直在等待客户端，这样服务器端就白白浪费了一定的资源。若采用三次握手，服务器端没有收到来自客户端的再此确认，则就会知道客户端并没有要求建立请求，就不会浪费服务器的资源。&lt;/p&gt;
&lt;h2&gt;83.说一下 tcp 粘包是怎么产生的？&lt;/h2&gt;
&lt;p&gt;tcp 粘包可能发生在发送端或者接收端，分别来看两端各种产生粘包的原因：&lt;/p&gt;
&lt;p&gt;发送端粘包：发送端需要等缓冲区满才发送出去，造成粘包；&lt;br /&gt;
接收方粘包：接收方不及时接收缓冲区的包，造成多个包接收。&lt;/p&gt;
&lt;h2&gt;84.OSI 的七层模型都有哪些？&lt;/h2&gt;
&lt;p&gt;物理层：利用传输介质为数据链路层提供物理连接，实现比特流的透明传输。&lt;br /&gt;
数据链路层：负责建立和管理节点间的链路。&lt;br /&gt;
网络层：通过路由选择算法，为报文或分组通过通信子网选择最适当的路径。&lt;br /&gt;
传输层：向用户提供可靠的端到端的差错和流量控制，保证报文的正确传输。&lt;br /&gt;
会话层：向两个实体的表示层提供建立和使用连接的方法。&lt;br /&gt;
表示层：处理用户信息的表示问题，如编码、数据格式转换和加密解密等。&lt;br /&gt;
应用层：直接向用户提供服务，完成用户希望在网络上完成的各种工作。&lt;/p&gt;
&lt;h2&gt;85.get 和 post 请求有哪些区别？&lt;/h2&gt;
&lt;p&gt;get 请求会被浏览器主动缓存，而 post 不会。&lt;br /&gt;
get 传递参数有大小限制，而 post 没有。&lt;br /&gt;
post 参数传输更安全，get 的参数会明文限制在 url 上，post 不会。&lt;/p&gt;
&lt;h2&gt;86.如何实现跨域？&lt;/h2&gt;
&lt;p&gt;实现跨域有以下几种方案：&lt;/p&gt;
&lt;p&gt;服务器端运行跨域 设置 CORS 等于 *；&lt;br /&gt;
在单个接口使用注解 @CrossOrigin 运行跨域；&lt;br /&gt;
使用 jsonp 跨域；&lt;/p&gt;
&lt;h2&gt;87.说一下 JSONP 实现原理？&lt;/h2&gt;
&lt;p&gt;jsonp：JSON with Padding，它是利用script标签的 src 连接可以访问不同源的特性，加载远程返回的“JS 函数”来执行的。&lt;/p&gt;
&lt;p&gt;====================================================================&lt;/p&gt;
&lt;h1&gt;九. 设计模式模块&lt;/h1&gt;
&lt;h2&gt;88.说一下你熟悉的设计模式？&lt;/h2&gt;
&lt;p&gt;单例模式：保证被创建一次，节省系统开销。&lt;br /&gt;
工厂模式（简单工厂、抽象工厂）：解耦代码。&lt;br /&gt;
观察者模式：定义了对象之间的一对多的依赖，这样一来，当一个对象改变时，它的所有的依赖者都会收到通知并自动更新。&lt;br /&gt;
外观模式：提供一个统一的接口，用来访问子系统中的一群接口，外观定义了一个高层的接口，让子系统更容易使用。&lt;br /&gt;
模版方法模式：定义了一个算法的骨架，而将一些步骤延迟到子类中，模版方法使得子类可以在不改变算法结构的情况下，重新定义算法的步骤。&lt;br /&gt;
状态模式：允许对象在内部状态改变时改变它的行为，对象看起来好像修改了它的类。&lt;/p&gt;
&lt;h2&gt;89.简单工厂和抽象工厂有什么区别？&lt;/h2&gt;
&lt;p&gt;简单工厂：用来生产同一等级结构中的任意产品，对于增加新的产品，无能为力。&lt;br /&gt;
工厂方法：用来生产同一等级结构中的固定产品，支持增加任意产品。&lt;br /&gt;
抽象工厂：用来生产不同产品族的全部产品，对于增加新的产品，无能为力；支持增加产品族。&lt;/p&gt;
&lt;p&gt;====================================================================&lt;/p&gt;
&lt;h1&gt;十. Spring/Spring MVC模块&lt;/h1&gt;
&lt;h2&gt;90.为什么要使用 spring？&lt;/h2&gt;
&lt;p&gt;spring 提供 ioc 技术，容器会帮你管理依赖的对象，从而不需要自己创建和管理依赖对象了，更轻松的实现了程序的解耦。&lt;br /&gt;
spring 提供了事务支持，使得事务操作变的更加方便。&lt;br /&gt;
spring 提供了面向切片编程，这样可以更方便的处理某一类的问题。&lt;br /&gt;
更方便的框架集成，spring 可以很方便的集成其他框架，比如 MyBatis、hibernate 等。&lt;/p&gt;
&lt;h2&gt;91.解释一下什么是 aop？&lt;/h2&gt;
&lt;p&gt;与OOP(面向对象)对比，AOP是处理一些横切行问题，这些横切性问题不会影响到主逻辑的实现，但是会散落到代码的各个部分，难以维护。AOP就是把这些问题和主业务逻辑分开，达到与主业务逻辑解耦的目的。&lt;/p&gt;
&lt;p&gt;传统OOP是自上而下的逻辑开发；&lt;/p&gt;
&lt;p&gt;aop 是面向切面编程，通过预编译方式和运行期动态代理实现程序功能的统一维护的一种技术。&lt;br /&gt;
简单来说就是统一处理某一“切面”（类）的问题的编程思想，比如统一处理日志、异常等。&lt;/p&gt;
&lt;h2&gt;92.解释一下什么是 ioc？&lt;/h2&gt;
&lt;p&gt;ioc：Inversionof Control（中文：控制反转）是 spring 的核心，对于 spring 框架来说，就是由 Spring IoC Container来负责控制对象的生命周期和对象间的关系。&lt;br /&gt;
简单来说，控制指的是当前对象对内部成员的控制权；控制反转指的是，这种控制权不由当前对象管理了，由其他（类,第三方容器）来管理。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;何为控制，控制的是什么？&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;答：是 bean 的创建、管理的权利，控制 bean 的整个生命周期。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;何为反转，反转了什么？&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;答：把这个权利交给了 Spring 容器，而不是自己去控制，就是反转。 由之前的自己主动创建对象，变成现在被动接收别人给我们的对象的过程，这就是反转。&lt;/p&gt;
&lt;h2&gt;93.spring 有哪些主要模块？&lt;/h2&gt;
&lt;p&gt;spring core：框架的最基础部分，提供 ioc 和依赖注入特性。&lt;br /&gt;
spring context：构建于 core 封装包基础上的 context 封装包，提供了一种框架式的对象访问方法。&lt;br /&gt;
spring dao：Data Access Object 提供了JDBC的抽象层。&lt;br /&gt;
spring aop：提供了面向切面的编程实现，让你可以自定义拦截器、切点等。&lt;br /&gt;
spring Web：提供了针对 Web 开发的集成特性，例如文件上传，利用 servlet listeners 进行 ioc 容器初始化和针对 Web 的 ApplicationContext。&lt;br /&gt;
spring Web mvc：spring 中的 mvc 封装包提供了 Web 应用的 Model-View-Controller（MVC）的实现。&lt;/p&gt;
&lt;h2&gt;94.spring 常用的依赖注入方式有哪些？&lt;/h2&gt;
&lt;p&gt;setter 属性注入&lt;br /&gt;
构造方法注入&lt;br /&gt;
注解方式注入&lt;/p&gt;
&lt;h2&gt;95.spring 中的 bean 是线程安全的吗？&lt;/h2&gt;
&lt;p&gt;spring 中的 bean 默认是单例模式，spring 框架并没有对单例 bean 进行多线程的封装处理。&lt;br /&gt;
实际上大部分时候 spring bean 无状态的（比如 dao 类），所有某种程度上来说 bean 也是安全的，但如果 bean 有状态的话（比如 view model 对象），那就要开发者自己去保证线程安全了，最简单的就是改变 bean 的作用域，把“singleton”变更为“prototype”，这样请求 bean 相当于 new Bean()了，所以就可以保证线程安全了。&lt;/p&gt;
&lt;p&gt;有状态就是有数据存储功能。&lt;br /&gt;
无状态就是不会保存数据。&lt;/p&gt;
&lt;h2&gt;阐述Spring框架中Bean的生命周期？&lt;/h2&gt;
&lt;p&gt;① Spring IoC容器找到关于Bean的定义并实例化该Bean。&lt;/p&gt;
&lt;p&gt;② Spring IoC容器对Bean进行依赖注入。&lt;/p&gt;
&lt;p&gt;③ 如果Bean实现了BeanNameAware接口，则将该Bean的id传给setBeanName方法。&lt;/p&gt;
&lt;p&gt;④ 如果Bean实现了BeanFactoryAware接口，则将BeanFactory对象传给setBeanFactory方法。&lt;/p&gt;
&lt;p&gt;⑤ 如果Bean实现了BeanPostProcessor接口，则调用其postProcessBeforeInitialization方法。&lt;/p&gt;
&lt;p&gt;⑥ 如果Bean实现了InitializingBean接口，则调用其afterPropertySet方法。&lt;/p&gt;
&lt;p&gt;⑦ 如果有和Bean关联的BeanPostProcessors对象，则这些对象的postProcessAfterInitialization方法被调用。&lt;/p&gt;
&lt;p&gt;⑧ 当销毁Bean实例时，如果Bean实现了DisposableBean接口，则调用其destroy方法。&lt;/p&gt;
&lt;h2&gt;96.spring 支持几种 bean 的作用域？&lt;/h2&gt;
&lt;p&gt;spring 支持 5 种作用域，如下：&lt;/p&gt;
&lt;p&gt;singleton：spring ioc 容器中只存在一个 bean 实例，bean 以单例模式存在，是系统默认值；&lt;/p&gt;
&lt;p&gt;prototype：每次从容器调用 bean 时都会创建一个新的示例，既每次 getBean()相当于执行 new Bean()操作；&lt;/p&gt;
&lt;p&gt;request：每次 http 请求都会创建一个 bean；&lt;/p&gt;
&lt;p&gt;session：同一个 http session 共享一个 bean 实例；&lt;/p&gt;
&lt;p&gt;global-session：用于 portlet 容器，因为每个 portlet 有单独的 session，globalsession 提供一个全局性的 http session。&lt;/p&gt;
&lt;p&gt;注意： 使用 prototype 作用域需要慎重的思考，因为频繁创建和销毁 bean 会带来很大的性能开销。&lt;/p&gt;
&lt;h2&gt;97.spring 自动装配 bean 有哪些方式？&lt;/h2&gt;
&lt;p&gt;no：默认值，表示没有自动装配，应使用显式 bean 引用进行装配。&lt;br /&gt;
byName：它根据 bean 的名称注入对象依赖项。&lt;br /&gt;
byType：它根据类型注入对象依赖项。&lt;br /&gt;
构造函数：通过构造函数来注入依赖项，需要设置大量的参数。&lt;br /&gt;
autodetect：容器首先通过构造函数使用 autowire 装配，如果不能，则通过 byType 自动装配。&lt;/p&gt;
&lt;h2&gt;98.spring 事务实现方式有哪些？&lt;/h2&gt;
&lt;p&gt;声明式事务：声明式事务也有两种实现方式，基于 xml 配置文件的方式和注解方式（在类上添加 @Transaction 注解）。&lt;br /&gt;
编码方式：提供编码的形式管理和维护事务。&lt;/p&gt;
&lt;h2&gt;&lt;strong&gt;7种事务的传播机制（可通过spring配置或注解来设置）&lt;/strong&gt;&lt;/h2&gt;
&lt;ol&gt;
&lt;li&gt;REQUIRED（默认）：支持使用当前事务，如果当前事务不存在，创建一个新事务。&lt;/li&gt;
&lt;li&gt;SUPPORTS：支持使用当前事务，如果当前事务不存在，则不使用事务。&lt;/li&gt;
&lt;li&gt;MANDATORY：中文翻译为强制，支持使用当前事务，如果当前事务不存在，则抛出Exception。&lt;/li&gt;
&lt;li&gt;REQUIRES_NEW：创建一个新事务，如果当前事务存在，把当前事务挂起。&lt;/li&gt;
&lt;li&gt;NOT_SUPPORTED：无事务执行，如果当前事务存在，把当前事务挂起。&lt;/li&gt;
&lt;li&gt;NEVER：无事务执行，如果当前有事务则抛出Exception。&lt;/li&gt;
&lt;li&gt;NESTED：嵌套事务，如果当前事务存在，那么在嵌套的事务中执行。如果当前事务不存在，则表现跟REQUIRED一样。&lt;/li&gt;
&lt;/ol&gt;
&lt;h2&gt;99.说一下 spring 的事务隔离？&lt;/h2&gt;
&lt;p&gt;spring 有五大隔离级别，默认值为 ISOLATION_DEFAULT（使用数据库的设置），其他四个隔离级别和数据库的隔离级别一致：&lt;/p&gt;
&lt;p&gt;ISOLATION_DEFAULT：用底层数据库的设置隔离级别，数据库设置的是什么我就用什么；&lt;br /&gt;
ISOLATION_READ_UNCOMMITTED：&lt;strong&gt;未提交读&lt;/strong&gt;，最低隔离级别、事务未提交前，就可被其他事务读取（会出现幻读、脏读、不可重复读）；&lt;br /&gt;
ISOLATION_READ_COMMITTED：&lt;strong&gt;提交读&lt;/strong&gt;，一个事务提交后才能被其他事务读取到（会造成幻读、不可重复读），SQL server 的默认级别；&lt;br /&gt;
ISOLATION_REPEATABLE_READ：&lt;strong&gt;可重复读&lt;/strong&gt;，保证多次读取同一个数据时，其值都和事务开始时候的内容是一致，禁止读取到别的事务未提交的数据（会造成幻读），MySQL 的默认级别；&lt;br /&gt;
ISOLATION_SERIALIZABLE：&lt;strong&gt;序列化&lt;/strong&gt;，代价最高最可靠的隔离级别，该隔离级别能防止脏读、不可重复读、幻读。&lt;br /&gt;
&lt;strong&gt;脏读&lt;/strong&gt; ：表示一个事务能够读取另一个事务中还未提交的数据。比如，某个事务尝试插入记录 A，此时该事务还未提交，然后另一个事务尝试读取到了记录 A。&lt;br /&gt;
&lt;strong&gt;不可重复读&lt;/strong&gt; ：是指在一个事务内，多次读同一数据。&lt;br /&gt;
&lt;strong&gt;幻读&lt;/strong&gt; ：指同一个事务内多次查询返回的结果不一样。比如同一个事务 A 第一次查询时候有 n 条记录，但是第二次同等条件下查询却有 n+1 条记录，这就好像产生了幻觉。发生幻读的原因也是另外一个事务新增或者删除或者修改了第一个事务结果集里面的数据，同一个记录的数据内容被修改了，所有数据行的记录就变多或者变少了。&lt;/p&gt;
&lt;h2&gt;100.说一下 spring mvc 运行流程？&lt;/h2&gt;
&lt;p&gt;spring mvc 先将请求发送给 DispatcherServlet。&lt;br /&gt;
DispatcherServlet 查询一个或多个 HandlerMapping，找到处理请求的 Controller。&lt;br /&gt;
DispatcherServlet 再把请求提交到对应的 Controller。&lt;br /&gt;
Controller 进行业务逻辑处理后，会返回一个ModelAndView。&lt;br /&gt;
Dispathcher 查询一个或多个 ViewResolver 视图解析器，找到 ModelAndView 对象指定的视图对象。&lt;br /&gt;
视图对象负责渲染返回给客户端。&lt;/p&gt;
&lt;h2&gt;101.spring mvc 有哪些组件？&lt;/h2&gt;
&lt;p&gt;前置控制器 DispatcherServlet。&lt;br /&gt;
映射控制器 HandlerMapping。&lt;br /&gt;
处理器 Controller。&lt;br /&gt;
模型和视图 ModelAndView。&lt;br /&gt;
视图解析器 ViewResolver。&lt;/p&gt;
&lt;h2&gt;102.@RequestMapping 的作用是什么？&lt;/h2&gt;
&lt;p&gt;将 http 请求映射到相应的类/方法上。&lt;/p&gt;
&lt;h2&gt;103.@Autowired 的作用是什么？&lt;/h2&gt;
&lt;p&gt;@Autowired 它可以对类成员变量、方法及构造函数进行标注，完成自动装配的工作，通过@Autowired 的使用来消除 set/get 方法。&lt;/p&gt;
&lt;p&gt;====================================================================&lt;/p&gt;
&lt;h1&gt;十一. Spring Boot/Spring Cloud模块&lt;/h1&gt;
&lt;h2&gt;104.什么是 spring boot？&lt;/h2&gt;
&lt;p&gt;spring boot 是为 spring 服务的，是用来简化新 spring 应用的初始搭建以及开发过程的。&lt;/p&gt;
&lt;h2&gt;105.为什么要用 spring boot？&lt;/h2&gt;
&lt;p&gt;配置简单&lt;br /&gt;
独立运行&lt;br /&gt;
自动装配&lt;br /&gt;
无代码生成和 xml 配置&lt;br /&gt;
提供应用监控&lt;br /&gt;
易上手&lt;br /&gt;
提升开发效率&lt;/p&gt;
&lt;h2&gt;106.spring boot 核心配置文件是什么？&lt;/h2&gt;
&lt;p&gt;spring boot 核心的两个配置文件：&lt;/p&gt;
&lt;p&gt;bootstrap (. yml 或者 . properties)：boostrap 由父 ApplicationContext 加载的，比 applicaton 优先加载，且 boostrap 里面的属性不能被覆盖；&lt;br /&gt;
application (. yml 或者 . properties)：用于 spring boot 项目的自动化配置。&lt;/p&gt;
&lt;h2&gt;107.spring boot 配置文件有哪几种类型？它们有什么区别？&lt;/h2&gt;
&lt;p&gt;配置文件有 . properties 格式和 . yml 格式，它们主要的区别是书法风格不同。&lt;br /&gt;
properties 配置如下：&lt;/p&gt;
&lt;p&gt;spring. RabbitMQ. port=5672&lt;br /&gt;
yml 配置如下：&lt;/p&gt;
&lt;p&gt;spring:&lt;br /&gt;
RabbitMQ:&lt;br /&gt;
port: 5672&lt;/p&gt;
&lt;p&gt;yml 格式不支持 @PropertySource 注解导入。&lt;/p&gt;
&lt;h2&gt;108.spring boot 有哪些方式可以实现热部署？&lt;/h2&gt;
&lt;p&gt;使用 devtools 启动热部署，添加 devtools 库，在配置文件中把 spring. devtools. restart. enabled 设置为 true；&lt;br /&gt;
使用 Intellij Idea 编辑器，勾上自动编译或手动重新编译。&lt;/p&gt;
&lt;h2&gt;109.jpa 和 hibernate 有什么区别？&lt;/h2&gt;
&lt;p&gt;jpa 全称 Java Persistence API，是 Java 持久化接口规范，hibernate 属于 jpa 的具体实现。&lt;/p&gt;
&lt;h2&gt;110.什么是 spring cloud？&lt;/h2&gt;
&lt;p&gt;spring cloud 是一系列框架的有序集合。它利用 spring boot 的开发便利性巧妙地简化了分布式系统基础设施的开发，如服务发现注册、配置中心、消息总线、负载均衡、断路器、数据监控等，都可以用 spring boot 的开发风格做到一键启动和部署。&lt;/p&gt;
&lt;h2&gt;111.spring cloud 断路器的作用是什么？&lt;/h2&gt;
&lt;p&gt;在分布式架构中，断路器模式的作用也是类似的，当某个服务单元发生故障（类似用电器发生短路）之后，通过断路器的故障监控（类似熔断保险丝），向调用方返回一个错误响应，而不是长时间的等待。这样就不会使得线程因调用故障服务被长时间占用不释放，避免了故障在分布式系统中的蔓延。&lt;/p&gt;
&lt;h2&gt;112.spring cloud 的核心组件有哪些？&lt;/h2&gt;
&lt;p&gt;Eureka：服务注册于发现。&lt;br /&gt;
Feign：基于动态代理机制，根据注解和选择的机器，拼接请求 url 地址，发起请求。&lt;br /&gt;
Ribbon：实现负载均衡，从一个服务的多台机器中选择一台。&lt;br /&gt;
Hystrix：提供线程池，不同的服务走不同的线程池，实现了不同服务调用的隔离，避免了服务雪崩的问题。&lt;br /&gt;
Zuul：网关管理，由 Zuul 网关转发请求给对应的服务。&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;时效注记（2026 更新）&lt;/strong&gt;：上面这套 Netflix 全家桶已整体进入维护/停更状态，Spring Cloud 2020 版起陆续移除。现在的对应关系是：Eureka → Nacos / Consul；Ribbon → Spring Cloud LoadBalancer；Hystrix → Resilience4j / Sentinel；Zuul → Spring Cloud Gateway。只有 &lt;strong&gt;Feign&lt;/strong&gt;（现为 OpenFeign）还在主线使用。国内大量团队直接用 Spring Cloud Alibaba（Nacos + Sentinel + Seata）。&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;====================================================================&lt;/p&gt;
&lt;h1&gt;十二. Hibernate模块&lt;/h1&gt;
&lt;h2&gt;113.为什么要使用 hibernate？&lt;/h2&gt;
&lt;p&gt;hibernate 是对 jdbc 的封装，大大简化了数据访问层的繁琐的重复性代码。&lt;br /&gt;
hibernate 是一个优秀的 ORM 实现，很多程度上简化了 DAO 层的编码功能。&lt;br /&gt;
可以很方便的进行数据库的移植工作。&lt;br /&gt;
提供了缓存机制，是程序执行更改的高效。&lt;/p&gt;
&lt;h2&gt;114.什么是 ORM 框架？&lt;/h2&gt;
&lt;p&gt;ORM（Object Relation Mapping）对象关系映射，是把数据库中的关系数据映射成为程序中的对象。&lt;br /&gt;
使用 ORM 的优点：提高了开发效率降低了开发成本、开发更简单更对象化、可移植更强。&lt;/p&gt;
&lt;h2&gt;115.hibernate 中如何在控制台查看打印的 SQL 语句？&lt;/h2&gt;
&lt;p&gt;在 Config 里面把 hibernate. show_SQL 设置为 true 就可以。但不建议开启，开启之后会降低程序的运行效率。&lt;/p&gt;
&lt;h2&gt;116.hibernate 有几种查询方式？&lt;/h2&gt;
&lt;p&gt;三种：hql、原生 SQL、条件查询 Criteria。&lt;/p&gt;
&lt;h2&gt;117.hibernate 实体类可以被定义为 final 吗？&lt;/h2&gt;
&lt;p&gt;实体类可以定义为 final 类，但这样的话就不能使用 hibernate 代理模式下的延迟关联提供性能了，所以不建议定义实体类为 final。&lt;/p&gt;
&lt;h2&gt;118.在 hibernate 中使用 Integer 和 int 做映射有什么区别？&lt;/h2&gt;
&lt;p&gt;Integer 类型为对象，它的值允许为 null，而 int 属于基础数据类型，值不能为 null。&lt;/p&gt;
&lt;h2&gt;119.hibernate 是如何工作的？&lt;/h2&gt;
&lt;p&gt;读取并解析配置文件。&lt;br /&gt;
读取并解析映射文件，创建 SessionFactory。&lt;br /&gt;
打开 Session。&lt;br /&gt;
创建事务。&lt;br /&gt;
进行持久化操作。&lt;br /&gt;
提交事务。&lt;br /&gt;
关闭 Session。&lt;br /&gt;
关闭 SessionFactory。&lt;/p&gt;
&lt;h2&gt;120.get()和 load()的区别？&lt;/h2&gt;
&lt;p&gt;数据查询时，没有 OID 指定的对象，get() 返回 null；load() 返回一个代理对象。&lt;br /&gt;
load()支持延迟加载；get() 不支持延迟加载。&lt;/p&gt;
&lt;h2&gt;121.说一下 hibernate 的缓存机制？&lt;/h2&gt;
&lt;p&gt;hibernate 常用的缓存有一级缓存和二级缓存：&lt;br /&gt;
一级缓存：也叫 Session 缓存，只在 Session 作用范围内有效，不需要用户干涉，由 hibernate 自身维护，可以通过：evict(object)清除 object 的缓存；clear()清除一级缓存中的所有缓存；flush()刷出缓存；&lt;br /&gt;
二级缓存：应用级别的缓存，在所有 Session 中都有效，支持配置第三方的缓存，如：EhCache。&lt;/p&gt;
&lt;h2&gt;122.hibernate 对象有哪些状态？&lt;/h2&gt;
&lt;p&gt;临时/瞬时状态：直接 new 出来的对象，该对象还没被持久化（没保存在数据库中），不受 Session 管理。&lt;br /&gt;
持久化状态：当调用 Session 的 save/saveOrupdate/get/load/list 等方法的时候，对象就是持久化状态。&lt;br /&gt;
游离状态：Session 关闭之后对象就是游离状态。&lt;/p&gt;
&lt;h2&gt;123.在 hibernate 中 getCurrentSession 和 openSession 的区别是什么？&lt;/h2&gt;
&lt;p&gt;getCurrentSession 会绑定当前线程，而 openSession 则不会。&lt;br /&gt;
getCurrentSession 事务是 Spring 控制的，并且不需要手动关闭，而 openSession 需要我们自己手动开启和提交事务。&lt;/p&gt;
&lt;h2&gt;124.hibernate 实体类必须要有无参构造函数吗？为什么？&lt;/h2&gt;
&lt;p&gt;hibernate 中每个实体类必须提供一个无参构造函数，因为 hibernate 框架要使用 reflection api，通过调用 ClassnewInstance() 来创建实体类的实例，如果没有无参的构造函数就会抛出异常。&lt;/p&gt;
&lt;p&gt;====================================================================&lt;/p&gt;
&lt;h1&gt;十三. MyBatis模块&lt;/h1&gt;
&lt;h2&gt;125.MyBatis 中 #{}和 &lt;code&gt;${}&lt;/code&gt;的区别是什么？&lt;/h2&gt;
&lt;p&gt;#{}是预编译处理，&lt;code&gt;${}&lt;/code&gt;是字符替换。 在使用 #{}时，MyBatis 会将 SQL 中的 #{}替换成“?”，配合 PreparedStatement 的 set 方法赋值，这样可以有效的防止 SQL 注入，保证程序的运行安全。&lt;/p&gt;
&lt;h2&gt;126.MyBatis 有几种分页方式？&lt;/h2&gt;
&lt;p&gt;分页方式：逻辑分页和物理分页。&lt;br /&gt;
逻辑分页： 使用 MyBatis 自带的 RowBounds 进行分页，它是一次性查询很多数据，然后在数据中再进行检索。&lt;br /&gt;
物理分页： 自己手写 SQL 分页或使用分页插件 PageHelper，去数据库查询指定条数的分页数据的形式。&lt;/p&gt;
&lt;h2&gt;127.RowBounds 是一次性查询全部结果吗？为什么？&lt;/h2&gt;
&lt;p&gt;RowBounds 表面是在“所有”数据中检索数据，其实并非是一次性查询出所有数据，因为 MyBatis 是对 jdbc 的封装，在 jdbc 驱动中有一个 Fetch Size 的配置，它规定了每次最多从数据库查询多少条数据，假如你要查询更多数据，它会在你执行 next()的时候，去查询更多的数据。就好比你去自动取款机取 10000 元，但取款机每次最多能取 2500 元，所以你要取 4 次才能把钱取完。只是对于 jdbc 来说，当你调用 next()的时候会自动帮你完成查询工作。这样做的好处可以有效的防止内存溢出。&lt;br /&gt;
Fetch Size 官方相关文档：&lt;a href=&quot;http://t&quot;&gt;http://t&lt;/a&gt;. cn/EfSE2g3&lt;/p&gt;
&lt;h2&gt;128.MyBatis 逻辑分页和物理分页的区别是什么？&lt;/h2&gt;
&lt;p&gt;逻辑分页是一次性查询很多数据，然后再在结果中检索分页的数据。这样做弊端是需要消耗大量的内存、有内存溢出的风险、对数据库压力较大。&lt;br /&gt;
物理分页是从数据库查询指定条数的数据，弥补了一次性全部查出的所有数据的种种缺点，比如需要大量的内存，对数据库查询压力较大等问题。&lt;/p&gt;
&lt;h2&gt;129.MyBatis 是否支持延迟加载？延迟加载的原理是什么？&lt;/h2&gt;
&lt;p&gt;MyBatis 支持延迟加载，设置 lazyLoadingEnabled=true 即可。&lt;br /&gt;
延迟加载的原理的是调用的时候触发加载，而不是在初始化的时候就加载信息。比如调用 a. getB(). getName()，这个时候发现 a. getB() 的值为 null，此时会单独触发事先保存好的关联 B 对象的 SQL，先查询出来 B，然后再调用 a. setB(b)，而这时候再调用 a. getB(). getName() 就有值了，这就是延迟加载的基本原理。&lt;/p&gt;
&lt;h2&gt;130.说一下 MyBatis 的一级缓存和二级缓存？&lt;/h2&gt;
&lt;p&gt;一级缓存：基于 PerpetualCache 的 HashMap 本地缓存，它的声明周期是和 SQLSession 一致的，有多个 SQLSession 或者分布式的环境中数据库操作，可能会出现脏数据。当 Session flush 或 close 之后，该 Session 中的所有 Cache 就将清空，默认一级缓存是开启的。&lt;br /&gt;
二级缓存：也是基于 PerpetualCache 的 HashMap 本地缓存，不同在于其存储作用域为 Mapper 级别的，如果多个SQLSession之间需要共享缓存，则需要使用到二级缓存，并且二级缓存可自定义存储源，如 Ehcache。默认不打开二级缓存，要开启二级缓存，使用二级缓存属性类需要实现 Serializable 序列化接口(可用来保存对象的状态)。&lt;br /&gt;
开启二级缓存数据查询流程：二级缓存 -&amp;gt; 一级缓存 -&amp;gt; 数据库。&lt;br /&gt;
缓存更新机制：当某一个作用域(一级缓存 Session/二级缓存 Mapper)进行了C/U/D 操作后，默认该作用域下所有 select 中的缓存将被 clear。&lt;/p&gt;
&lt;h2&gt;131.MyBatis 和 hibernate 的区别有哪些？&lt;/h2&gt;
&lt;p&gt;灵活性：MyBatis 更加灵活，自己可以写 SQL 语句，使用起来比较方便。&lt;br /&gt;
可移植性：MyBatis 有很多自己写的 SQL，因为每个数据库的 SQL 可以不相同，所以可移植性比较差。&lt;br /&gt;
学习和使用门槛：MyBatis 入门比较简单，使用门槛也更低。&lt;br /&gt;
二级缓存：hibernate 拥有更好的二级缓存，它的二级缓存可以自行更换为第三方的二级缓存。&lt;/p&gt;
&lt;h2&gt;132.MyBatis 有哪些执行器（Executor）？&lt;/h2&gt;
&lt;p&gt;MyBatis 有三种基本的Executor执行器：&lt;/p&gt;
&lt;p&gt;SimpleExecutor：每执行一次 update 或 select 就开启一个 Statement 对象，用完立刻关闭 Statement 对象；&lt;br /&gt;
ReuseExecutor：执行 update 或 select，以 SQL 作为 key 查找 Statement 对象，存在就使用，不存在就创建，用完后不关闭 Statement 对象，而是放置于 Map 内供下一次使用。简言之，就是重复使用 Statement 对象；&lt;br /&gt;
BatchExecutor：执行 update（没有 select，jdbc 批处理不支持 select），将所有 SQL 都添加到批处理中（addBatch()），等待统一执行（executeBatch()），它缓存了多个 Statement 对象，每个 Statement 对象都是 addBatch()完毕后，等待逐一执行 executeBatch()批处理，与 jdbc 批处理相同。&lt;/p&gt;
&lt;h2&gt;133.MyBatis 分页插件的实现原理是什么？&lt;/h2&gt;
&lt;p&gt;分页插件的基本原理是使用 MyBatis 提供的插件接口，实现自定义插件，在插件的拦截方法内拦截待执行的 SQL，然后重写 SQL，根据 dialect 方言，添加对应的物理分页语句和物理分页参数。&lt;/p&gt;
&lt;h2&gt;134.MyBatis 如何编写一个自定义插件？&lt;/h2&gt;
&lt;p&gt;自定义插件实现原理：&lt;/p&gt;
&lt;p&gt;MyBatis 自定义插件针对 MyBatis 四大对象（Executor、StatementHandler、ParameterHandler、ResultSetHandler）进行拦截：&lt;br /&gt;
Executor：拦截内部执行器，它负责调用 StatementHandler 操作数据库，并把结果集通过 ResultSetHandler 进行自动映射，另外它还处理了二级缓存的操作；&lt;br /&gt;
StatementHandler：拦截 SQL 语法构建的处理，它是 MyBatis 直接和数据库执行 SQL 脚本的对象，另外它也实现了 MyBatis 的一级缓存；&lt;br /&gt;
ParameterHandler：拦截参数的处理；&lt;br /&gt;
ResultSetHandler：拦截结果集的处理。&lt;br /&gt;
自定义插件实现关键：&lt;br /&gt;
MyBatis 插件要实现 Interceptor 接口，接口包含的方法，如下：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;public interface Interceptor {
    Object intercept(Invocation invocation) throws Throwable;
    Object plugin(Object target);
    void setProperties(Properties properties);
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;setProperties 方法是在 MyBatis 进行配置插件的时候可以配置自定义相关属性，即：接口实现对象的参数配置；&lt;br /&gt;
plugin 方法是插件用于封装目标对象的，通过该方法我们可以返回目标对象本身，也可以返回一个它的代理，可以决定是否要进行拦截进而决定要返回一个什么样的目标对象，官方提供了示例：return Plugin. wrap(target, this)；&lt;br /&gt;
intercept 方法就是要进行拦截的时候要执行的方法。&lt;br /&gt;
自定义插件实现示例：&lt;br /&gt;
官方插件实现：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;@Intercepts({@Signature(type = Executor. class, method = &quot;query&quot;,
args = {MappedStatement. class, Object. class, RowBounds. class, ResultHandler. class})})
public class TestInterceptor implements Interceptor {
    public Object intercept(Invocation invocation) throws Throwable {
        Object target = invocation. getTarget(); //被代理对象
        Method method = invocation. getMethod(); //代理方法
        Object[] args = invocation. getArgs(); //方法参数
        // do something . . . . . . 方法拦截前执行代码块
        Object result = invocation. proceed();
        // do something . . . . . . . 方法拦截后执行代码块
        return result;
}
public Object plugin(Object target) {
    return Plugin. wrap(target, this);
    }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;====================================================================&lt;/p&gt;
&lt;h1&gt;十四. RabbitMQ模块&lt;/h1&gt;
&lt;h2&gt;135.RabbitMQ 的使用场景有哪些？&lt;/h2&gt;
&lt;p&gt;抢购活动，削峰填谷，防止系统崩塌。&lt;br /&gt;
延迟信息处理，比如 10 分钟之后给下单未付款的用户发送邮件提醒。&lt;br /&gt;
解耦系统，对于新增的功能可以单独写模块扩展，比如用户确认评价之后，新增了给用户返积分的功能，这个时候不用在业务代码里添加新增积分的功能，只需要把新增积分的接口订阅确认评价的消息队列即可，后面再添加任何功能只需要订阅对应的消息队列即可。&lt;/p&gt;
&lt;h2&gt;136.RabbitMQ 有哪些重要的角色？&lt;/h2&gt;
&lt;p&gt;RabbitMQ 中重要的角色有：生产者、消费者和代理：&lt;/p&gt;
&lt;p&gt;生产者：消息的创建者，负责创建和推送数据到消息服务器；&lt;br /&gt;
消费者：消息的接收方，用于处理数据和确认消息；&lt;br /&gt;
代理：就是 RabbitMQ 本身，用于扮演“快递”的角色，本身不生产消息，只是扮演“快递”的角色。&lt;/p&gt;
&lt;h2&gt;137.RabbitMQ 有哪些重要的组件？&lt;/h2&gt;
&lt;p&gt;ConnectionFactory（连接管理器）：应用程序与Rabbit之间建立连接的管理器，程序代码中使用。&lt;/p&gt;
&lt;p&gt;Channel（信道）：消息推送使用的通道。&lt;br /&gt;
Exchange（交换器）：用于接受、分配消息。&lt;br /&gt;
Queue（队列）：用于存储生产者的消息。&lt;br /&gt;
RoutingKey（路由键）：用于把生成者的数据分配到交换器上。&lt;br /&gt;
BindingKey（绑定键）：用于把交换器的消息绑定到队列上。&lt;br /&gt;
138.RabbitMQ 中 vhost 的作用是什么？&lt;br /&gt;
vhost：每个 RabbitMQ 都能创建很多 vhost，我们称之为虚拟主机，每个虚拟主机其实都是 mini 版的RabbitMQ，它拥有自己的队列，交换器和绑定，拥有自己的权限机制。&lt;/p&gt;
&lt;h2&gt;139.RabbitMQ 的消息是怎么发送的？&lt;/h2&gt;
&lt;p&gt;首先客户端必须连接到 RabbitMQ 服务器才能发布和消费消息，&lt;br /&gt;
客户端和 rabbit server 之间会创建一个 tcp 连接，一旦 tcp 打开并通过了认证&lt;br /&gt;
（认证就是你发送给 rabbit 服务器的用户名和密码），&lt;br /&gt;
你的客户端和 RabbitMQ 就创建了一条 amqp 信道（channel），&lt;br /&gt;
信道是创建在“真实” tcp 上的虚拟连接，amqp 命令都是通过信道发送出去的，&lt;br /&gt;
每个信道都会有一个唯一的 id，不论是发布消息，订阅队列都是通过这个信道完成的。&lt;/p&gt;
&lt;h2&gt;140.RabbitMQ 怎么保证消息的稳定性？&lt;/h2&gt;
&lt;p&gt;提供了事务的功能。&lt;br /&gt;
通过将 channel 设置为 confirm（确认）模式。&lt;/p&gt;
&lt;h2&gt;141.RabbitMQ 怎么避免消息丢失？&lt;/h2&gt;
&lt;p&gt;把消息持久化磁盘，保证服务器重启消息不丢失。&lt;br /&gt;
每个集群中至少有一个物理磁盘，保证消息落入磁盘。&lt;/p&gt;
&lt;h2&gt;142.要保证消息持久化成功的条件有哪些？&lt;/h2&gt;
&lt;p&gt;声明队列必须设置持久化 durable 设置为 true.&lt;br /&gt;
消息推送投递模式必须设置持久化，deliveryMode 设置为 2（持久）。&lt;br /&gt;
消息已经到达持久化交换器。&lt;br /&gt;
消息已经到达持久化队列。&lt;br /&gt;
以上四个条件都满足才能保证消息持久化成功。&lt;/p&gt;
&lt;h2&gt;143.RabbitMQ 持久化有什么缺点？&lt;/h2&gt;
&lt;p&gt;持久化的缺地就是降低了服务器的吞吐量，因为使用的是磁盘而非内存存储，&lt;br /&gt;
从而降低了吞吐量。可尽量使用 ssd 硬盘来缓解吞吐量的问题。&lt;/p&gt;
&lt;h2&gt;144.RabbitMQ 有几种广播类型？&lt;/h2&gt;
&lt;p&gt;direct（默认方式）：最基础最简单的模式，发送方把消息发送给订阅方，&lt;br /&gt;
如果有多个订阅者，默认采取轮询的方式进行消息发送。&lt;br /&gt;
headers：与 direct 类似，只是性能很差，此类型几乎用不到。&lt;br /&gt;
fanout：分发模式，把消费分发给所有订阅者。&lt;br /&gt;
topic：匹配订阅模式，使用正则匹配到消息队列，能匹配到的都能接收到。&lt;/p&gt;
&lt;h2&gt;145.RabbitMQ 怎么实现延迟消息队列？&lt;/h2&gt;
&lt;p&gt;延迟队列的实现有两种方式：&lt;/p&gt;
&lt;p&gt;通过消息过期后进入死信交换器，再由交换器转发到延迟消费队列，实现延迟功能；&lt;br /&gt;
使用 RabbitMQ-delayed-message-exchange 插件实现延迟功能。&lt;/p&gt;
&lt;h2&gt;146.RabbitMQ 集群有什么用？&lt;/h2&gt;
&lt;p&gt;集群主要有以下两个用途：&lt;/p&gt;
&lt;p&gt;高可用：某个服务器出现问题，整个 RabbitMQ 还可以继续使用；&lt;br /&gt;
高容量：集群可以承载更多的消息量。&lt;/p&gt;
&lt;h2&gt;147.RabbitMQ 节点的类型有哪些？&lt;/h2&gt;
&lt;p&gt;磁盘节点：消息会存储到磁盘。&lt;br /&gt;
内存节点：消息都存储在内存中，重启服务器消息丢失，性能高于磁盘类型。&lt;/p&gt;
&lt;h2&gt;148.RabbitMQ 集群搭建需要注意哪些问题？&lt;/h2&gt;
&lt;p&gt;各节点之间使用“–link”连接，此属性不能忽略。&lt;br /&gt;
各节点使用的 erlang cookie 值必须相同，此值相当于“秘钥”的功能，用于各节点的认证。&lt;br /&gt;
整个集群中必须包含一个磁盘节点。&lt;/p&gt;
&lt;h2&gt;149.RabbitMQ 每个节点是其他节点的完整拷贝吗？为什么？&lt;/h2&gt;
&lt;p&gt;不是，原因有以下两个：&lt;/p&gt;
&lt;p&gt;存储空间的考虑：如果每个节点都拥有所有队列的完全拷贝，&lt;br /&gt;
这样新增节点不但没有新增存储空间，反而增加了更多的冗余数据；&lt;br /&gt;
性能的考虑：如果每条消息都需要完整拷贝到每一个集群节点，&lt;br /&gt;
那新增节点并没有提升处理消息的能力，最多是保持和单节点相同的性能甚至是更糟。&lt;/p&gt;
&lt;h2&gt;150.RabbitMQ 集群中唯一一个磁盘节点崩溃了会发生什么情况？&lt;/h2&gt;
&lt;p&gt;如果唯一磁盘的磁盘节点崩溃了，不能进行以下操作：&lt;br /&gt;
不能创建队列、&lt;br /&gt;
不能创建交换器、&lt;br /&gt;
不能创建绑定、&lt;br /&gt;
不能添加用户、&lt;br /&gt;
不能更改权限、&lt;br /&gt;
不能添加和删除集群节点、&lt;br /&gt;
唯一磁盘节点崩溃了，集群是可以保持运行的，但你不能更改任何东西。&lt;/p&gt;
&lt;h2&gt;151.RabbitMQ 对集群节点停止顺序有要求吗？&lt;/h2&gt;
&lt;p&gt;RabbitMQ 对集群的停止的顺序是有要求的，应该先关闭内存节点，&lt;br /&gt;
最后再关闭磁盘节点。如果顺序恰好相反的话，可能会造成消息的丢失。&lt;/p&gt;
&lt;p&gt;====================================================================&lt;/p&gt;
&lt;h1&gt;十五. Kafka&lt;/h1&gt;
&lt;h2&gt;152.kafka 可以脱离 zookeeper 单独使用吗？为什么？&lt;/h2&gt;
&lt;p&gt;kafka 不能脱离 zookeeper 单独使用，因为 kafka 使用 zookeeper&lt;br /&gt;
管理和协调 kafka 的节点服务器。&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;时效注记（2026 更新）&lt;/strong&gt;：此结论已过时。Kafka 2.8（2021）引入 KRaft 模式，用内置的 Raft 协议替代 ZooKeeper 管理元数据；3.3 起 KRaft 生产可用；&lt;strong&gt;4.0 已彻底移除 ZooKeeper&lt;/strong&gt;。现在的正确答案是：新版本 Kafka 不但可以脱离 ZooKeeper，而且已经不再支持它。面试时答出 KRaft 会明显加分。&lt;/p&gt;
&lt;/blockquote&gt;
&lt;h2&gt;153.kafka 有几种数据保留的策略？&lt;/h2&gt;
&lt;p&gt;kafka 有两种数据保存策略：按照过期时间保留和按照存储的消息大小保留。&lt;/p&gt;
&lt;h2&gt;154.kafka 同时设置了 7 天和 10G 清除数据，到第五天的时候消息达到了 10G，这个时候 kafka 将如何处理？&lt;/h2&gt;
&lt;p&gt;这个时候 kafka 会执行数据清除工作，时间和大小不论那个满足条件，都会清空数据。&lt;/p&gt;
&lt;h2&gt;155.什么情况会导致 kafka 运行变慢？&lt;/h2&gt;
&lt;p&gt;cpu性能瓶颈、&lt;br /&gt;
磁盘读写瓶颈、&lt;br /&gt;
网络瓶颈。&lt;/p&gt;
&lt;h2&gt;156.使用 kafka 集群需要注意什么？&lt;/h2&gt;
&lt;p&gt;集群的数量不是越多越好，最好不要超过 7 个，因为节点越多，消息复制需要的时间就越长，&lt;br /&gt;
整个群组的吞吐量就越低。&lt;br /&gt;
集群数量最好是单数，因为超过一半故障集群就不能用了，设置为单数容错率更高。&lt;/p&gt;
&lt;p&gt;====================================================================&lt;/p&gt;
&lt;h1&gt;十六. Zookeeper模块&lt;/h1&gt;
&lt;h2&gt;157.zookeeper 是什么？&lt;/h2&gt;
&lt;p&gt;zookeeper 是一个分布式的，开放源码的分布式应用程序协调服务，是 google chubby 的开源实现，是 hadoop 和 hbase 的重要组件。它是一个为分布式应用提供一致性服务的软件，提供的功能包括：配置维护、域名服务、分布式同步、组服务等。&lt;/p&gt;
&lt;h2&gt;158.zookeeper 都有哪些功能？&lt;/h2&gt;
&lt;p&gt;集群管理：监控节点存活状态、运行请求等。&lt;br /&gt;
主节点选举：主节点挂掉了之后可以从备用的节点开始新一轮选主，主节点选举说的就是这个选举的过程，使用 zookeeper 可以协助完成这个过程。&lt;/p&gt;
&lt;p&gt;分布式锁：zookeeper 提供两种锁：独占锁、共享锁。独占锁即一次只能有一个线程使用资源，共享锁是读锁共享，读写互斥，即可以有多线线程同时读同一个资源，如果要使用写锁也只能有一个线程使用。zookeeper可以对分布式锁进行控制。&lt;br /&gt;
命名服务：在分布式系统中，通过使用命名服务，客户端应用能够根据指定名字来获取资源或服务的地址，提供者等信息。&lt;/p&gt;
&lt;h2&gt;159.zookeeper 有几种部署模式？&lt;/h2&gt;
&lt;p&gt;zookeeper 有三种部署模式：&lt;/p&gt;
&lt;p&gt;单机部署：一台集群上运行；&lt;/p&gt;
&lt;p&gt;集群部署：多台集群运行；&lt;/p&gt;
&lt;p&gt;伪集群部署：一台集群启动多个 zookeeper 实例运行。&lt;/p&gt;
&lt;h2&gt;160.zookeeper 怎么保证主从节点的状态同步？&lt;/h2&gt;
&lt;p&gt;zookeeper 的核心是原子广播，这个机制保证了各个 server 之间的同步。&lt;br /&gt;
实现这个机制的协议叫做 zab 协议。 zab 协议有两种模式，&lt;br /&gt;
分别是恢复模式（选主）和广播模式（同步）。当服务启动或者在领导者崩溃后，&lt;br /&gt;
zab 就进入了恢复模式，当领导者被选举出来，且大多数 server 完成了和 leader 的&lt;br /&gt;
状态同步以后，恢复模式就结束了。状态同步保证了 leader 和 server 具有相同的系统状态。&lt;/p&gt;
&lt;h2&gt;161.集群中为什么要有主节点？&lt;/h2&gt;
&lt;p&gt;在分布式环境中，有些业务逻辑只需要集群中的某一台机器进行执行，&lt;br /&gt;
其他的机器可以共享这个结果，这样可以大大减少重复计算，提高性能，所以就需要主节点。&lt;/p&gt;
&lt;h2&gt;162.集群中有 3 台服务器，其中一个节点宕机，这个时候 zookeeper 还可以使用吗？&lt;/h2&gt;
&lt;p&gt;可以继续使用，单数服务器只要没超过一半的服务器宕机就可以继续使用。&lt;/p&gt;
&lt;h2&gt;163.说一下 zookeeper 的通知机制？&lt;/h2&gt;
&lt;p&gt;客户端端会对某个 znode 建立一个 watcher 事件，当该 znode 发生变化时，&lt;br /&gt;
这些客户端会收到 zookeeper 的通知，然后客户端可以根据 znode 变化来做出业务上的改变。&lt;/p&gt;
&lt;p&gt;====================================================================&lt;/p&gt;
&lt;h1&gt;十七. MySQL模块&lt;/h1&gt;
&lt;h2&gt;164.数据库的三范式是什么？&lt;/h2&gt;
&lt;p&gt;第一范式（1NF）：强调的是列的原子性，即数据库表的每一列都是不可分割的原子数据项。&lt;br /&gt;
第二范式（2NF）：要求实体的属性完全依赖于主关键字。所谓完全依赖是指不能存在仅依赖主关键字一部分的属性。（在1NF基础上消除非主属性对主键的部分函数依赖）&lt;br /&gt;
第三范式（3NF）：任何非主属性不依赖于其它非主属性。（在2NF基础上消除传递依赖）&lt;/p&gt;
&lt;h2&gt;165.一张自增表里面总共有 7 条数据，删除了最后 2 条数据，重启 MySQL 数据库，又插入了一条数据，此时 id 是几？&lt;/h2&gt;
&lt;p&gt;表类型如果是 MyISAM ，那 id 就是 8。&lt;br /&gt;
表类型如果是 InnoDB，那 id 就是 6。&lt;br /&gt;
InnoDB 表只会把自增主键的最大 id 记录在内存中，所以重启之后会导致最大 id 丢失。&lt;/p&gt;
&lt;h2&gt;166.如何获取当前数据库版本？&lt;/h2&gt;
&lt;p&gt;使用 select version() 获取当前 MySQL 数据库版本。&lt;/p&gt;
&lt;h2&gt;167.说一下 ACID 是什么？&lt;/h2&gt;
&lt;p&gt;Atomicity（原子性）：一个事务（transaction）中的所有操作，或者全部完成，或者全部不完成，不会结束在中间某个环节。事务在执行过程中发生错误，会被恢复（Rollback）到事务开始前的状态，就像这个事务从来没有执行过一样。即，事务不可分割、不可约简。&lt;/p&gt;
&lt;p&gt;Consistency（一致性）：在事务开始之前和事务结束以后，数据库的完整性没有被破坏。这表示写入的资料必须完全符合所有的预设约束、触发器、级联回滚等。&lt;/p&gt;
&lt;p&gt;Isolation（隔离性）：数据库允许多个并发事务同时对其数据进行读写和修改的能力，隔离性可以防止多个事务并发执行时由于交叉执行而导致数据的不一致。事务隔离分为不同级别，包括读未提交（Read uncommitted）、读提交（read committed）、可重复读（repeatable read）和串行化（Serializable）。&lt;/p&gt;
&lt;p&gt;Durability（持久性）：事务处理结束后，对数据的修改就是永久的，即便系统故障也不会丢失。&lt;/p&gt;
&lt;h2&gt;168.char 和 varchar 的区别是什么？&lt;/h2&gt;
&lt;p&gt;char(n) ：固定长度类型，比如订阅 char(10)，当你输入”abc”三个字符的时候，它们占的空间还是 10 个字节，其他 7 个是空字节。&lt;br /&gt;
优点：效率高；缺点：占用空间；适用场景：存储密码的 md5 值，固定长度的，使用 char 非常合适。&lt;/p&gt;
&lt;p&gt;varchar(n) ：可变长度，存储的值是每个值占用的字节再加上一个用来记录其长度的字节的长度。&lt;br /&gt;
所以，从空间上考虑 varcahr 比较合适；从效率上考虑 char 比较合适，二者使用需要权衡。&lt;/p&gt;
&lt;h2&gt;169.float 和 double 的区别是什么？&lt;/h2&gt;
&lt;p&gt;float 最多可以存储 8 位的十进制数，并在内存中占 4 字节。&lt;br /&gt;
double 最可可以存储 16 位的十进制数，并在内存中占 8 字节。&lt;/p&gt;
&lt;h2&gt;170.MySQL 的内连接、左连接、右连接有什么区别？&lt;/h2&gt;
&lt;p&gt;内连接关键字：inner join；左连接：left join；右连接：right join。&lt;br /&gt;
内连接是把匹配的关联数据显示出来；左连接是左边的表全部显示出来，右边的表显示出符合条件的数据；右连接正好相反。&lt;/p&gt;
&lt;h2&gt;171.MySQL 索引是怎么实现的？&lt;/h2&gt;
&lt;p&gt;索引是满足某种特定查找算法的数据结构，而这些数据结构会以某种方式指向数据，从而实现高效查找数据。&lt;br /&gt;
具体来说 MySQL 中的索引，不同的数据引擎实现有所不同，但目前主流的数据库引擎的索引都是 B+ 树实现的，B+ 树的搜索效率，可以到达二分法的性能，找到数据区域之后就找到了完整的数据结构了，所有索引的性能也是更好的。&lt;/p&gt;
&lt;h2&gt;172.怎么验证 MySQL 的索引是否满足需求？&lt;/h2&gt;
&lt;p&gt;使用 explain 查看 SQL 是如何执行查询语句的，从而分析你的索引是否满足需求。&lt;br /&gt;
explain 语法：explain select * from table where type=1。&lt;/p&gt;
&lt;h2&gt;173.说一下数据库的事务隔离？&lt;/h2&gt;
&lt;p&gt;MySQL 的事务隔离是在 MySQL. ini 配置文件里添加的，在文件的最后添加：&lt;/p&gt;
&lt;p&gt;transaction-isolation = REPEATABLE-READ&lt;br /&gt;
1&lt;br /&gt;
可用的配置值：READ-UNCOMMITTED、READ-COMMITTED、REPEATABLE-READ、SERIALIZABLE。&lt;/p&gt;
&lt;p&gt;READ-UNCOMMITTED：未提交读，最低隔离级别、事务未提交前，就可被其他事务读取（会出现幻读、脏读、不可重复读）。&lt;br /&gt;
READ-COMMITTED：提交读，一个事务提交后才能被其他事务读取到（会造成幻读、不可重复读）。&lt;br /&gt;
REPEATABLE-READ：可重复读，默认级别，保证多次读取同一个数据时，其值都和事务开始时候的内容是一致，禁止读取到别的事务未提交的数据（会造成幻读）。&lt;br /&gt;
SERIALIZABLE：序列化，代价最高最可靠的隔离级别，该隔离级别能防止脏读、不可重复读、幻读。&lt;br /&gt;
脏读 ：表示一个事务能够读取另一个事务中还未提交的数据。比如，某个事务尝试插入记录 A，此时该事务还未提交，然后另一个事务尝试读取到了记录 A。&lt;br /&gt;
不可重复读 ：是指在一个事务内，多次读同一数据。&lt;br /&gt;
幻读 ：指同一个事务内多次查询返回的结果集不一样。比如同一个事务 A 第一次查询时候有 n 条记录，但是第二次同等条件下查询却有 n+1 条记录，这就好像产生了幻觉。发生幻读的原因也是另外一个事务新增或者删除或者修改了第一个事务结果集里面的数据，同一个记录的数据内容被修改了，所有数据行的记录就变多或者变少了。&lt;/p&gt;
&lt;h2&gt;174.说一下 MySQL 常用的引擎？&lt;/h2&gt;
&lt;p&gt;InnoDB引擎：InnoDB 引擎提供了对数据库 acid 事务的支持，并且还提供了行级锁和外键的约束，它的设计的目标就是处理大数据容量的数据库系统。MySQL 运行的时候，InnoDB 会在内存中建立缓冲池，用于缓冲数据和索引。但是该引擎是不支持全文搜索，同时启动也比较的慢，它是不会保存表的行数的，所以当进行 select count(*) from table 指令的时候，需要进行扫描全表。由于锁的粒度小，写操作是不会锁定全表的,所以在并发度较高的场景下使用会提升效率的。&lt;/p&gt;
&lt;p&gt;MyIASM引擎：MySQL 的默认引擎，但不提供事务的支持，也不支持行级锁和外键。因此当执行插入和更新语句时，即执行写操作的时候需要锁定这个表，所以会导致效率会降低。不过和 InnoDB 不同的是，MyIASM 引擎是保存了表的行数，于是当进行 select count(*) from table 语句时，可以直接的读取已经保存的值而不需要进行扫描全表。所以，如果表的读操作远远多于写操作时，并且不需要事务的支持的，可以将 MyIASM 作为数据库引擎的首选。&lt;/p&gt;
&lt;h2&gt;175.说一下 MySQL 的行锁和表锁？&lt;/h2&gt;
&lt;p&gt;MyISAM 只支持表锁，InnoDB 支持表锁和行锁，默认为行锁。&lt;/p&gt;
&lt;p&gt;表级锁：开销小，加锁快，不会出现死锁。锁定粒度大，发生锁冲突的概率最高，并发量最低。&lt;/p&gt;
&lt;p&gt;行级锁：开销大，加锁慢，会出现死锁。锁力度小，发生锁冲突的概率小，并发度最高。&lt;/p&gt;
&lt;h2&gt;176.说一下乐观锁和悲观锁？&lt;/h2&gt;
&lt;p&gt;&lt;strong&gt;乐观锁&lt;/strong&gt;：每次去拿数据的时候都认为别人不会修改，所以不会上锁，但是在提交更新的时候会判断一下在此期间别人有没有去更新这个数据。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;悲观锁&lt;/strong&gt;：每次去拿数据的时候都认为别人会修改，所以每次在拿数据的时候都会上锁，这样别人想拿这个数据就会阻止，直到这个锁被释放。&lt;/p&gt;
&lt;p&gt;数据库的乐观锁需要自己实现，在表里面添加一个 version 字段，每次修改成功值加 1，这样每次修改的时候先对比一下，自己拥有的 version 和数据库现在的 version 是否一致，如果不一致就不修改，这样就实现了乐观锁。&lt;/p&gt;
&lt;h2&gt;177.MySQL 问题排查都有哪些手段？&lt;/h2&gt;
&lt;p&gt;使用 show processlist 命令查看当前所有连接信息。&lt;br /&gt;
使用 explain 命令查询 SQL 语句执行计划。&lt;br /&gt;
开启慢查询日志，查看慢查询的 SQL。&lt;/p&gt;
&lt;h2&gt;178.如何做 MySQL 的性能优化？&lt;/h2&gt;
&lt;p&gt;为搜索字段创建索引。&lt;br /&gt;
避免使用 select *，列出需要查询的字段。&lt;br /&gt;
垂直分割分表。&lt;/p&gt;
&lt;h1&gt;选择正确的存储引擎。&lt;/h1&gt;
&lt;h1&gt;十八. Redis模块&lt;/h1&gt;
&lt;h2&gt;179.Redis 是什么？都有哪些使用场景？&lt;/h2&gt;
&lt;p&gt;Redis 是一个使用 C 语言开发的高速缓存数据库。&lt;br /&gt;
redis是一个key-value存储系统。和Memcached类似，它支持存储的value类型相对更多，包括string(字符串)、list(链表)、set(集合)、zset(sorted set –有序集合)和hash（哈希类型）。这些数据类型都支持push/pop、add/remove及取交集并集和差集及更丰富的操作，而且这些操作都是原子性的。在此基础上，redis支持各种不同方式的排序。与memcached一样，为了保证效率，数据都是缓存在内存中。区别的是redis会周期性的把更新的数据写入磁盘或者把修改操作写入追加的记录文件，并且在此基础上实现了master-slave主从同步【主从同步：数据可以从主服务器向任意数量的从服务器上同步】。&lt;br /&gt;
Redis 是一个高性能的key-value数据库。 redis的出现，很大程度补偿了memcached这类key/value存储的不足，在部 分场合可以对关系数据库起到很好的补充作用。它提供了Java，C/C++，C#，PHP，JavaScript，Perl，Object-C，Python，Ruby，Erlang等客户端，使用很方便。&lt;br /&gt;
Redis 使用场景：&lt;/p&gt;
&lt;p&gt;记录帖子点赞数、点击数、评论数；&lt;br /&gt;
缓存近期热帖；&lt;br /&gt;
缓存文章详情信息；&lt;br /&gt;
记录用户会话信息。&lt;/p&gt;
&lt;h2&gt;180.Redis 有哪些功能？&lt;/h2&gt;
&lt;p&gt;数据缓存功能、&lt;br /&gt;
分布式锁的功能、&lt;br /&gt;
支持数据持久化、&lt;br /&gt;
支持事务、&lt;br /&gt;
支持消息队列、&lt;/p&gt;
&lt;h2&gt;181.Redis 和 memcache 有什么区别？&lt;/h2&gt;
&lt;p&gt;存储方式不同：memcache 把数据全部存在内存之中，断电后会挂掉，数据不能超过内存大小；Redis 有部份存在硬盘上，这样能保证数据的持久性。&lt;br /&gt;
数据支持类型：memcache 对数据类型支持相对简单；Redis 有复杂的数据类型。&lt;br /&gt;
使用底层模型不同：它们之间底层实现方式，以及与客户端之间通信的应用协议不一样，Redis 自己构建了 vm 机制，因为一般的系统调用系统函数的话，会浪费一定的时间去移动和请求。&lt;br /&gt;
value 值大小不同：Redis 最大可以达到 1gb；memcache 只有 1mb。&lt;/p&gt;
&lt;h2&gt;182.Redis 为什么是单线程的？&lt;/h2&gt;
&lt;p&gt;因为 cpu 不是 Redis 的瓶颈，Redis 的瓶颈最有可能是机器内存或者网络带宽。既然单线程容易实现，而且 cpu 又不会成为瓶颈，那就顺理成章地采用单线程的方案了。&lt;br /&gt;
关于 Redis 的性能，官方网站也有，普通笔记本轻松处理每秒几十万的请求。而且单线程并不代表就慢， nginx 和 node.js 也都是高性能单线程的代表。&lt;/p&gt;
&lt;h2&gt;183.什么是缓存穿透？怎么解决？&lt;/h2&gt;
&lt;p&gt;&lt;strong&gt;缓存穿透&lt;/strong&gt;：指查询一个一定不存在的数据，由于缓存是不命中时需要从数据库查询，查不到数据则不写入缓存，这将导致这个不存在的数据每次请求都要到数据库去查询，造成缓存穿透。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;解决方案&lt;/strong&gt;：最简单粗暴的方法如果一个查询返回的数据为空（不管是数据不存在，还是系统故障），我们就把这个空结果进行缓存，但它的过期时间会很短，最长不超过五分钟。&lt;/p&gt;
&lt;h2&gt;184.Redis 支持的数据类型有哪些？&lt;/h2&gt;
&lt;p&gt;Redis 支持的数据类型：string（字符串）、list（列表）、hash（字典）、set（集合）、zset（有序集合）；&lt;/p&gt;
&lt;p&gt;另外3种特殊数据类型Geospatial(地理空间)、Bitmap、hyperLogLog&lt;/p&gt;
&lt;h2&gt;185.Redis 支持的 Java 客户端都有哪些？&lt;/h2&gt;
&lt;p&gt;支持的 Java 客户端有 Redisson、jedis、lettuce 等。&lt;/p&gt;
&lt;h2&gt;186.jedis 和 Redisson 有哪些区别？&lt;/h2&gt;
&lt;p&gt;jedis：提供了比较全面的 Redis 命令的支持。&lt;/p&gt;
&lt;p&gt;Redisson：实现了分布式和可扩展的 Java 数据结构，与 jedis 相比 Redisson 的功能相对简单，不支持排序、事务、管道、分区等 Redis 特性。&lt;/p&gt;
&lt;h2&gt;187.怎么保证缓存和数据库数据的一致性？&lt;/h2&gt;
&lt;p&gt;合理设置缓存的过期时间。&lt;br /&gt;
新增、更改、删除数据库操作时同步更新 Redis，可以使用事物机制来保证数据的一致性。&lt;/p&gt;
&lt;h2&gt;188.Redis 持久化有几种方式？&lt;/h2&gt;
&lt;p&gt;Redis 的持久化有两种方式，或者说有两种策略：&lt;/p&gt;
&lt;p&gt;RDB（Redis Database）：指定的时间间隔能对你的数据进行快照存储。&lt;/p&gt;
&lt;p&gt;AOF（Append Only File）：每一个收到的写命令都通过write函数追加到文件中。&lt;/p&gt;
&lt;h2&gt;189.Redis 怎么实现分布式锁？&lt;/h2&gt;
&lt;p&gt;Redis 分布式锁其实就是在系统里面占一个“坑”，其他程序也要占“坑”的时候，占用成功了就可以继续执行，失败了就只能放弃或稍后重试。&lt;br /&gt;
占坑一般使用 setnx(set if not exists)指令，只允许被一个程序占有，使用完调用 del 释放锁。&lt;/p&gt;
&lt;h2&gt;190.Redis 分布式锁有什么缺陷？&lt;/h2&gt;
&lt;p&gt;Redis 分布式锁不能解决超时的问题，分布式锁有一个超时时间，程序的执行如果超出了锁的超时时间就会出现问题。&lt;/p&gt;
&lt;h2&gt;191.Redis 如何做内存优化？&lt;/h2&gt;
&lt;p&gt;尽量使用 Redis 的散列表，把相关的信息放到散列表里面存储，而不是把每个字段单独存储，这样可以有效的减少内存使用。比如将 Web 系统的用户对象，应该放到散列表里面再整体存储到 Redis，而不是把用户的姓名、年龄、密码、邮箱等字段分别设置 key 进行存储。&lt;/p&gt;
&lt;h2&gt;192.Redis 淘汰策略有哪些？&lt;/h2&gt;
&lt;p&gt;volatile-lru：从已设置过期时间的数据集（server. db[i]. expires）中挑选最近最少使用的数据淘汰。&lt;br /&gt;
volatile-ttl：从已设置过期时间的数据集（server. db[i]. expires）中挑选将要过期的数据淘汰。&lt;br /&gt;
volatile-random：从已设置过期时间的数据集（server. db[i]. expires）中任意选择数据淘汰。&lt;br /&gt;
allkeys-lru：从数据集（server. db[i]. dict）中挑选最近最少使用的数据淘汰。&lt;br /&gt;
allkeys-random：从数据集（server. db[i]. dict）中任意选择数据淘汰。&lt;br /&gt;
no-enviction（驱逐）：禁止驱逐数据。&lt;/p&gt;
&lt;h2&gt;193.Redis 常见的性能问题有哪些？该如何解决？&lt;/h2&gt;
&lt;p&gt;主服务器写内存快照，会阻塞主线程的工作，当快照比较大时对性能影响是非常大的，会间断性暂停服务，所以主服务器最好不要写内存快照。&lt;br /&gt;
Redis 主从复制的性能问题，为了主从复制的速度和连接的稳定性，主从库最好在同一个局域网内。&lt;/p&gt;
&lt;p&gt;====================================================================&lt;/p&gt;
&lt;h1&gt;十九. JVM模块&lt;/h1&gt;
&lt;h2&gt;194.说一下 JVM 的主要组成部分？及其作用？&lt;/h2&gt;
&lt;p&gt;类加载器（ClassLoader）&lt;br /&gt;
运行时数据区（Runtime Data Area）&lt;br /&gt;
执行引擎（Execution Engine）&lt;br /&gt;
本地库接口（Native Interface）&lt;br /&gt;
组件的作用： 首先通过类加载器（ClassLoader）会把 Java 代码转换成字节码，运行时数据区（Runtime Data Area）再把字节码加载到内存中，而字节码文件只是 JVM 的一套指令集规范，并不能直接交个底层操作系统去执行，因此需要特定的命令解析器执行引擎（Execution Engine），将字节码翻译成底层系统指令，再交由 CPU 去执行，而这个过程中需要调用其他语言的本地库接口（Native Interface）来实现整个程序的功能。&lt;/p&gt;
&lt;h2&gt;195.说一下 JVM 运行时数据区？&lt;/h2&gt;
&lt;p&gt;不同虚拟机的运行时数据区可能略微有所不同，但都会遵从 Java 虚拟机规范， Java 虚拟机规范规定的区域分为以下 5 个部分：&lt;/p&gt;
&lt;p&gt;程序计数器（Program Counter Register）：当前线程所执行的字节码的行号指示器，字节码解析器的工作是通过改变这个计数器的值，来选取下一条需要执行的字节码指令，分支、循环、跳转、异常处理、线程恢复等基础功能，都需要依赖这个计数器来完成；&lt;br /&gt;
Java 虚拟机栈（Java Virtual Machine Stacks）：用于存储局部变量表、操作数栈、动态链接、方法出口等信息；&lt;br /&gt;
本地方法栈（Native Method Stack）：与虚拟机栈的作用是一样的，只不过虚拟机栈是服务 Java 方法的，而本地方法栈是为虚拟机调用 Native 方法服务的；&lt;br /&gt;
Java 堆（Java Heap）：Java 虚拟机中内存最大的一块，是被所有线程共享的，几乎所有的对象实例都在这里分配内存；&lt;br /&gt;
方法区（Methed Area）：用于存储已被虚拟机加载的类信息、常量、静态变量、即时编译后的代码等数据。&lt;/p&gt;
&lt;h2&gt;196.说一下堆栈的区别？&lt;/h2&gt;
&lt;p&gt;功能方面：堆是用来存放对象的，栈是用来执行程序的。&lt;br /&gt;
共享性：堆是线程共享的，栈是线程私有的。&lt;br /&gt;
空间大小：堆大小远远大于栈。&lt;/p&gt;
&lt;h2&gt;197.队列和栈是什么？有什么区别？&lt;/h2&gt;
&lt;p&gt;队列和栈都是被用来预存储数据的。&lt;br /&gt;
队列允许先进先出检索元素，但也有例外的情况，Deque 接口允许从两端检索元素。&lt;br /&gt;
栈和队列很相似，但它运行对元素进行后进先出进行检索。&lt;/p&gt;
&lt;h2&gt;198.什么是双亲委派模型？&lt;/h2&gt;
&lt;p&gt;在介绍双亲委派模型之前先说下类加载器。对于任意一个类，都需要由加载它的类加载器和这个类本身统一确立在 JVM 中的唯一性，每一个类加载器，都有一个独立的类名称空间。类加载器就是根据指定全限定名称将 class 文件加载到 JVM 内存，然后再转化为 class 对象。&lt;br /&gt;
类加载器分类：&lt;/p&gt;
&lt;p&gt;启动类加载器（Bootstrap ClassLoader），是虚拟机自身的一部分，用来加载Java_HOME/lib/目录中的，或者被 -Xbootclasspath 参数所指定的路径中并且被虚拟机识别的类库；&lt;br /&gt;
其他类加载器：&lt;br /&gt;
扩展类加载器（Extension ClassLoader）：负责加载\lib\ext目录或Java. ext. dirs系统变量指定的路径中的所有类库；&lt;br /&gt;
应用程序类加载器（Application ClassLoader）。负责加载用户类路径（classpath）上的指定类库，我们可以直接使用这个类加载器。一般情况，如果我们没有自定义类加载器默认就是用这个加载器。&lt;br /&gt;
双亲委派模型：如果一个类加载器收到了类加载的请求，它首先不会自己去加载这个类，而是把这个请求委派给父类加载器去完成，每一层的类加载器都是如此，这样所有的加载请求都会被传送到顶层的启动类加载器中，只有当父加载无法完成加载请求（它的搜索范围中没找到所需的类）时，子加载器才会尝试去加载类。&lt;/p&gt;
&lt;h2&gt;199.说一下类装载的执行过程？&lt;/h2&gt;
&lt;p&gt;类装载分为以下 5 个步骤：&lt;/p&gt;
&lt;p&gt;加载：根据查找路径找到相应的 class 文件然后导入；&lt;br /&gt;
检查：检查加载的 class 文件的正确性；&lt;br /&gt;
准备：给类中的静态变量分配内存空间；&lt;br /&gt;
解析：虚拟机将常量池中的符号引用替换成直接引用的过程。符号引用就理解为一个标示，而在直接引用直接指向内存中的地址；&lt;br /&gt;
初始化：对静态变量和静态代码块执行初始化工作。&lt;/p&gt;
&lt;h2&gt;200.怎么判断对象是否可以被回收？&lt;/h2&gt;
&lt;p&gt;一般有两种方法来判断：&lt;/p&gt;
&lt;p&gt;引用计数器：为每个对象创建一个引用计数，有对象引用时计数器 +1，&lt;br /&gt;
引用被释放时计数 -1，当计数器为 0 时就可以被回收。&lt;br /&gt;
它有一个缺点不能解决循环引用的问题；&lt;br /&gt;
可达性分析：从 GC Roots 开始向下搜索，&lt;br /&gt;
搜索所走过的路径称为引用链。当一个对象到 GC Roots 没有任何引用链相连时，&lt;br /&gt;
则证明此对象是可以被回收的。&lt;/p&gt;
&lt;h2&gt;201.Java 中都有哪些引用类型？&lt;/h2&gt;
&lt;p&gt;强引用：发生 gc 的时候不会被回收。&lt;br /&gt;
软引用：有用但不是必须的对象，在发生内存溢出之前会被回收。&lt;br /&gt;
弱引用：有用但不是必须的对象，在下一次GC时会被回收。&lt;br /&gt;
虚引用（幽灵引用/幻影引用）：无法通过虚引用获得对象，用 PhantomReference 现虚引用，虚引用的用途是在 gc 时返回一个通知。&lt;/p&gt;
&lt;h2&gt;202.说一下 JVM 有哪些垃圾回收算法？&lt;/h2&gt;
&lt;p&gt;标记-清除算法：标记无用对象，然后进行清除回收。缺点：效率不高，无法清除垃圾碎片。&lt;br /&gt;
标记-整理算法：标记无用对象，让所有存活的对象都向一端移动，&lt;br /&gt;
然后直接清除掉端边界以外的内存。&lt;br /&gt;
复制算法：按照容量划分二个大小相等的内存区域，&lt;br /&gt;
当一块用完的时候将活着的对象复制到另一块上，然后再把已使用的内存空间一次清理掉。&lt;br /&gt;
缺点：内存使用率不高，只有原来的一半。&lt;br /&gt;
分代算法：根据对象存活周期的不同将内存划分为几块，一般是新生代和老年代，&lt;br /&gt;
新生代基本采用复制算法，老年代采用标记整理算法。&lt;/p&gt;
&lt;h2&gt;203.说一下 JVM 有哪些垃圾回收器？&lt;/h2&gt;
&lt;p&gt;Serial：最早的单线程串行垃圾回收器。&lt;br /&gt;
Serial Old：Serial 垃圾回收器的老年版本，同样也是单线程的，可以作为 CMS 垃圾回收器的备选预案。&lt;br /&gt;
ParNew：是 Serial 的多线程版本。&lt;br /&gt;
Parallel 和 ParNew 收集器类似是多线程的，但 Parallel 是吞吐量优先的收集器，可以牺牲等待时间换取系统的吞吐量。&lt;br /&gt;
Parallel Old 是 Parallel 老生代版本，Parallel 使用的是复制的内存回收算法，Parallel Old 使用的是标记-整理的内存回收算法。&lt;br /&gt;
CMS：一种以获得最短停顿时间为目标的收集器，非常适用 B/S 系统。&lt;br /&gt;
G1：一种兼顾吞吐量和停顿时间的 GC 实现，是 JDK 9 以后的默认 GC 选项。&lt;/p&gt;
&lt;h2&gt;204.详细介绍一下 CMS 垃圾回收器？&lt;/h2&gt;
&lt;p&gt;CMS 是英文 Concurrent Mark-Sweep 的简称，是以牺牲吞吐量为代价来获得最短回收停顿时间的垃圾回收器。对于要求服务器响应速度的应用上，这种垃圾回收器非常适合。在启动 JVM 的参数加上“-XX:+UseConcMarkSweepGC”来指定使用 CMS 垃圾回收器。&lt;br /&gt;
CMS 使用的是标记-清除的算法实现的，所以在 gc 的时候回产生大量的内存碎片，当剩余内存不能满足程序运行要求时，系统将会出现 Concurrent Mode Failure，临时 CMS 会采用 Serial Old 回收器进行垃圾清除，此时的性能将会被降低。&lt;/p&gt;
&lt;h2&gt;205.新生代垃圾回收器和老生代垃圾回收器都有哪些？有什么区别？&lt;/h2&gt;
&lt;p&gt;新生代回收器：Serial、ParNew、Parallel Scavenge&lt;br /&gt;
老年代回收器：Serial Old、Parallel Old、CMS&lt;br /&gt;
整堆回收器：G1&lt;br /&gt;
新生代垃圾回收器一般采用的是复制算法，复制算法的优点是效率高，缺点是内存利用率低；老年代回收器一般采用的是标记-整理的算法进行垃圾回收。&lt;/p&gt;
&lt;h2&gt;206.简述分代垃圾回收器是怎么工作的？&lt;/h2&gt;
&lt;p&gt;分代回收器有两个分区：老生代和新生代，新生代默认的空间占比总空间的 1/3，老生代的默认占比是 2/3。&lt;br /&gt;
新生代使用的是复制算法，新生代里有 3 个分区：Eden、To Survivor、From Survivor，它们的默认占比是 8:1:1，它的执行流程如下：&lt;/p&gt;
&lt;p&gt;把 Eden + From Survivor 存活的对象放入 To Survivor 区；&lt;br /&gt;
清空 Eden 和 From Survivor 分区；&lt;br /&gt;
From Survivor 和 To Survivor 分区交换，From Survivor 变 To Survivor，To Survivor 变 From Survivor。&lt;br /&gt;
每次在 From Survivor 到 To Survivor 移动时都存活的对象，年龄就 +1，当年龄到达 15（默认配置是 15）时，升级为老生代。大对象也会直接进入老生代。&lt;br /&gt;
老生代当空间占用到达某个值之后就会触发全局垃圾收回，一般使用标记整理的执行算法。以上这些循环往复就构成了整个分代垃圾回收的整体执行流程。&lt;/p&gt;
&lt;h2&gt;207.说一下 JVM 调优的工具？&lt;/h2&gt;
&lt;p&gt;JDK 自带了很多监控工具，都位于 JDK 的 bin 目录下，其中最常用的是 jconsole 和 jvisualvm 这两款视图监控工具。&lt;/p&gt;
&lt;p&gt;jconsole：用于对 JVM 中的内存、线程和类等进行监控；&lt;br /&gt;
jvisualvm：JDK 自带的全能分析工具，可以分析：内存快照、线程快照、程序死锁、监控内存的变化、gc 变化等。&lt;/p&gt;
&lt;h2&gt;208.常用的 JVM 调优的参数都有哪些？&lt;/h2&gt;
&lt;p&gt;-Xms2g：初始化推大小为 2g；&lt;br /&gt;
-Xmx2g：堆最大内存为 2g；&lt;br /&gt;
-XX:NewRatio=4：设置年轻的和老年代的内存比例为 1:4；&lt;br /&gt;
-XX:SurvivorRatio=8：设置新生代 Eden 和 Survivor 比例为 8:2；&lt;br /&gt;
–XX:+UseParNewGC：指定使用 ParNew + Serial Old 垃圾回收器组合；&lt;br /&gt;
-XX:+UseParallelOldGC：指定使用 ParNew + ParNew Old 垃圾回收器组合；&lt;br /&gt;
-XX:+UseConcMarkSweepGC：指定使用 CMS + Serial Old 垃圾回收器组合；&lt;br /&gt;
-XX:+PrintGC：开启打印 gc 信息；&lt;br /&gt;
-XX:+PrintGCDetails：打印 gc 详细信息。&lt;/p&gt;
</content:encoded></item><item><title>理解Go位运算符</title><link>https://gilgameshzzz.github.io/posts/%E7%90%86%E8%A7%A3go%E4%BD%8D%E8%BF%90%E7%AE%97%E7%AC%A6/</link><guid isPermaLink="true">https://gilgameshzzz.github.io/posts/%E7%90%86%E8%A7%A3go%E4%BD%8D%E8%BF%90%E7%AE%97%E7%AC%A6/</guid><description>假定A = 14, B = 11,转换为二进制：A = 1110， B = 1011。</description><pubDate>Mon, 08 Jul 2019 00:00:00 GMT</pubDate><content:encoded>&lt;p&gt;假定 A = 14, B = 11，转换为二进制：A = 1110，B = 1011。&lt;/p&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;运算&lt;/th&gt;
&lt;th&gt;位3&lt;/th&gt;
&lt;th&gt;位2&lt;/th&gt;
&lt;th&gt;位1&lt;/th&gt;
&lt;th&gt;位0&lt;/th&gt;
&lt;th&gt;十进制&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;1&lt;/td&gt;
&lt;td&gt;1&lt;/td&gt;
&lt;td&gt;1&lt;/td&gt;
&lt;td&gt;0&lt;/td&gt;
&lt;td&gt;14&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;B&lt;/td&gt;
&lt;td&gt;1&lt;/td&gt;
&lt;td&gt;0&lt;/td&gt;
&lt;td&gt;1&lt;/td&gt;
&lt;td&gt;1&lt;/td&gt;
&lt;td&gt;11&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;A&amp;amp;B&lt;/td&gt;
&lt;td&gt;1&lt;/td&gt;
&lt;td&gt;0&lt;/td&gt;
&lt;td&gt;1&lt;/td&gt;
&lt;td&gt;0&lt;/td&gt;
&lt;td&gt;10&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;A|B&lt;/td&gt;
&lt;td&gt;1&lt;/td&gt;
&lt;td&gt;1&lt;/td&gt;
&lt;td&gt;1&lt;/td&gt;
&lt;td&gt;1&lt;/td&gt;
&lt;td&gt;15&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;A^B&lt;/td&gt;
&lt;td&gt;0&lt;/td&gt;
&lt;td&gt;1&lt;/td&gt;
&lt;td&gt;0&lt;/td&gt;
&lt;td&gt;1&lt;/td&gt;
&lt;td&gt;5&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;p&gt;&amp;amp; 是求与，两个数都为 1 才为 1，其余情况为 0；&lt;/p&gt;
&lt;p&gt;| 是求或，两个数都为 0 才为 0，其余情况为 1；&lt;/p&gt;
&lt;p&gt;^ 是求异或，两个数相同时为 0，不同时为 1；&lt;/p&gt;
&lt;p&gt;&lt;code&gt;&amp;lt;&amp;lt;&lt;/code&gt; 左移运算，将数字的二进制左移 n 位，相当于乘以 2 的 n 次方，由 &lt;code&gt;&amp;lt;&amp;lt;&lt;/code&gt; 右边的数指定移动的位数，高位丢弃，低位补 0：&lt;/p&gt;
&lt;p&gt;N * 2 等同于 N &amp;lt;&amp;lt; 1&lt;/p&gt;
&lt;p&gt;&lt;code&gt;&amp;gt;&amp;gt;&lt;/code&gt; 右移运算，将数字的二进制右移 n 位，相当于除以 2 的 n 次方，由 &lt;code&gt;&amp;gt;&amp;gt;&lt;/code&gt; 右边的数指定移动的位数：&lt;/p&gt;
&lt;p&gt;N / 2 等同于 N &amp;gt;&amp;gt; 1&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;两个容易踩的点：Go 的 &lt;code&gt;&amp;gt;&amp;gt;&lt;/code&gt; 对&lt;strong&gt;有符号数&lt;/strong&gt;是算术右移（高位补符号位），负数右移不等于向零取整的除法，比如 &lt;code&gt;-7 &amp;gt;&amp;gt; 1 == -4&lt;/code&gt; 而 &lt;code&gt;-7 / 2 == -3&lt;/code&gt;；对无符号数才是逻辑右移。另外 Go 里 &lt;code&gt;^&lt;/code&gt; 放在单个操作数前面是&lt;strong&gt;按位取反&lt;/strong&gt;（其他语言多用 &lt;code&gt;~&lt;/code&gt;），&lt;code&gt;&amp;amp;^&lt;/code&gt; 则是 Go 特有的按位清除（AND NOT）。&lt;/p&gt;
&lt;/blockquote&gt;
</content:encoded></item><item><title>MongoDB删除重复数据和唯一索引</title><link>https://gilgameshzzz.github.io/posts/mongodb%E5%88%A0%E9%99%A4%E9%87%8D%E5%A4%8D%E6%95%B0%E6%8D%AE%E5%92%8C%E5%94%AF%E4%B8%80%E7%B4%A2%E5%BC%95/</link><guid isPermaLink="true">https://gilgameshzzz.github.io/posts/mongodb%E5%88%A0%E9%99%A4%E9%87%8D%E5%A4%8D%E6%95%B0%E6%8D%AE%E5%92%8C%E5%94%AF%E4%B8%80%E7%B4%A2%E5%BC%95/</guid><description>1.根据pid分组并统计数量，group只会返回参与分组的字段，使用addToSet在返回结果数组中增加\id字段</description><pubDate>Mon, 03 Jun 2019 00:00:00 GMT</pubDate><content:encoded>&lt;p&gt;MongoDB 版本 V4.0.9&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;时效注记（2026 更新）&lt;/strong&gt;：本文写于 MongoDB 4.0 时期，其中 &lt;code&gt;ensureIndex&lt;/code&gt; 和 &lt;code&gt;remove&lt;/code&gt; 两个 API 后来都被移除了，下面的代码已同步更新为现行写法，并在各节标注了差异。&lt;/p&gt;
&lt;/blockquote&gt;
&lt;h2&gt;删除MongoDB删除重复数据&lt;/h2&gt;
&lt;pre&gt;&lt;code&gt;db.CompanyId.aggregate([{$group:{ _id:{&apos;pid&apos;:&apos;$pid&apos;},count:{$sum: 1},dups: {$addToSet: &apos;$_id&apos;}}}, {$match: {count: {$gt: 1}}}]).forEach(function(doc){
doc.dups.shift();db.CompanyId.deleteMany({_id: {$in: doc.dups}});})
&lt;/code&gt;&lt;/pre&gt;
&lt;blockquote&gt;
&lt;p&gt;原文用的是 &lt;code&gt;db.CompanyId.remove()&lt;/code&gt;。&lt;code&gt;remove()&lt;/code&gt; 自 MongoDB 3.2 起被 &lt;code&gt;deleteOne()&lt;/code&gt; / &lt;code&gt;deleteMany()&lt;/code&gt; 取代，新版驱动和 mongosh 中已移除，这里按批量删除的语义改成了 &lt;code&gt;deleteMany&lt;/code&gt;。&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;1.根据pid分组并统计数量，&lt;code&gt;$group&lt;/code&gt;只会返回参与分组的字段，使用&lt;code&gt;$addToSet&lt;/code&gt;在返回结果数组中增加_id字段&lt;/p&gt;
&lt;p&gt;2.使用&lt;code&gt;$match&lt;/code&gt;匹配数量大于1的数据&lt;/p&gt;
&lt;p&gt;3.doc.dups.shift();表示从数组第一个值开始删除；作用是踢除重复数据其中一个_id，让后面的删除语句不会删除所有数据&lt;/p&gt;
&lt;p&gt;4.使用forEach循环根据_id删除数据，&lt;code&gt;$addToSet&lt;/code&gt; 操作符只有在值没有存在于数组中时才会向数组中添加一个值。如果值已经存在于数组中，&lt;code&gt;$addToSet&lt;/code&gt;返回，不会修改数组。&lt;/p&gt;
&lt;p&gt;注意：forEach和&lt;code&gt;$addToSet&lt;/code&gt;的驼峰写法不能全部写成小写，因为mongodb严格区分大小写、mongodb严格区分大小写、mongodb严格区分大小写，重要的事情说三遍！&lt;/p&gt;
&lt;p&gt;聚合框架它是数据聚合的一个新框架，其概念类似于数据处理的管道。 每个文档通过一个由多个节点组成的管道，每个节点有自己特殊的功能（分组、过滤等），文档经过管道处理后，最后输出相应的结果。&lt;/p&gt;
&lt;p&gt;管道基本的功能有两个：&lt;br /&gt;
一是对文档进行“过滤”，也就是筛选出符合条件的文档;&lt;br /&gt;
二是对文档进行“变换”，也就是改变文档的输出形式。&lt;/p&gt;
&lt;h2&gt;MongoDB唯一索引&lt;/h2&gt;
&lt;p&gt;在 createIndex 命令中指定 &lt;code&gt;unique: true&lt;/code&gt; 即可创建唯一索引&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;db.CompanyId.createIndex({字段1: 1, 字段2: 1}, {unique: true});
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;字段1：1，中的1表示升序，-1表示降序&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;原文用的是 &lt;code&gt;ensureIndex&lt;/code&gt;。该方法自 MongoDB 3.0 起就是 &lt;code&gt;createIndex&lt;/code&gt; 的别名，&lt;strong&gt;5.0 已彻底移除&lt;/strong&gt;，在新版本上执行会直接报错。另外要注意：如果集合里已有重复数据，创建唯一索引会失败——所以本文前半部分的去重要先做。&lt;/p&gt;
&lt;/blockquote&gt;
&lt;h2&gt;MongoDB 查询两个字段的值相同的数据&lt;/h2&gt;
&lt;p&gt;因为MongoDB 不是关系型数据库，不可以直接使用”…Where 字段A=字段B”的方式来查找字段相同的条目，但可以使用“&lt;code&gt;$where&lt;/code&gt;”&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;db.foo.find({&quot;$where&quot;:function(){
 for(var current in this){
   for(var other in this){
     if(current != other &amp;amp;&amp;amp; this[current] == this[other]){
       return true;
     }
   }
 }
 return false;
}})
&lt;/code&gt;&lt;/pre&gt;
&lt;blockquote&gt;
&lt;p&gt;&lt;code&gt;$where&lt;/code&gt; 现在已&lt;strong&gt;不推荐使用&lt;/strong&gt;：它需要为每个文档启动 JavaScript 解释器，无法利用索引，全集合扫描，性能很差；而且执行任意 JS 有注入风险。现在优先用 &lt;code&gt;$expr&lt;/code&gt; 配合聚合表达式，例如比较两个已知字段是否相等：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;db.foo.find({$expr: {$eq: [&quot;$字段A&quot;, &quot;$字段B&quot;]}})
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;本文这个「任意两个字段相等」的需求因为要遍历未知字段名，&lt;code&gt;$expr&lt;/code&gt; 表达不了，更适合放到应用层或用聚合管道的 &lt;code&gt;$objectToArray&lt;/code&gt; 处理。&lt;/p&gt;
&lt;/blockquote&gt;
</content:encoded></item><item><title>Redis Sentinel哨兵</title><link>https://gilgameshzzz.github.io/posts/redis_sentinel/</link><guid isPermaLink="true">https://gilgameshzzz.github.io/posts/redis_sentinel/</guid><description>该方案由一个或多个Sentinel实例组成的Sentinel系统可以监视任意多个主服务器，以及这些主服务器属下的所有从服务器，并在被监视的主服务器进行下线状态时，自动将下线主服务器属下的某个从服务器升级为新的主服务器，然…</description><pubDate>Thu, 09 May 2019 00:00:00 GMT</pubDate><content:encoded>&lt;h2&gt;Redis-Sentinel是Redis官方推荐的高可用性(HA)解决方案&lt;/h2&gt;
&lt;p&gt;该方案由一个或多个Sentinel实例组成的Sentinel系统可以监视任意多个主服务器，以及这些主服务器属下的所有从服务器，并在被监视的主服务器进行下线状态时，自动将下线主服务器属下的某个从服务器升级为新的主服务器，然后由新的主服务器代替已下线的主服务器继续处理命令请求。Redis提供的sentinel（哨兵）机制，通过sentinel模式启动redis后，自动监控master/slave的运行状态，基本原理是：心跳机制+投票裁决，主要功能：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;监控&lt;/strong&gt;，Sentinel不断地监控redis是否按照预期良好地运行;&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;提醒&lt;/strong&gt;，如果发现某个redis节点运行出现状况，Sentinel 可以通过 API 向管理员或者其他应用程序发送通知。或能够通知另外一个进程(例如它的客户端)。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;自动故障迁移&lt;/strong&gt;，当一个master节点不可用时，能够选举出master的多个slave(如果有超过一个slave的话)中的一个来作为新的master,其它的slave节点会将它所追随的master的地址改为被提升为master的slave的新地址。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;自动更新配置信息&lt;/strong&gt;：哨兵提供了认证和服务发现，客户端连接到哨兵去获取当前redis主服务器地址，如果发生故障转移，哨兵将会汇报新的服务器地址。每次进行主从切换时，sentinel配置文件自动更新。&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;运行Sentinel&lt;/h2&gt;
&lt;p&gt;第一种：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;redis-sentinel /path/to/sentinel.conf
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;第二种：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;redis-server /path/to/sentinel.conf --sentinel
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;无论使用哪种都需要指定一个配置文件，sentinel默认26379端口。&lt;/p&gt;
&lt;h2&gt;Sentinel配置&lt;/h2&gt;
&lt;pre&gt;&lt;code&gt;[root@DB ~]# vim /sentinel.conf 
port 26379
daemonize yes #程序后台执行
logfile &quot;/var/log/sentinel.log&quot;
dir &quot;/tmp&quot;
#第一次设置哨兵时此ip一定要设置为redis集群中的主ip
sentinel monitor mymaster 127.0.0.1 6379 2 
sentinel down-after-milliseconds mymaster 5000
sentinel failover-timeout mymaster 6000

sentinel parallel-syncs mymaster 1
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;strong&gt;sentinel monitor mymaster 127.0.0.1 6379 2&lt;/strong&gt;&lt;br /&gt;
  这一行代表sentinel监控的master的名字叫做mymaster,地址为127.0.0.1:6379，行尾最后的一个2代表,当集群中有2个sentinel认为master死了时，才能真正认为该master已经不可用了。（sentinel集群中各个sentinel也有互相通信，通过gossip协议）。&lt;/p&gt;
&lt;p&gt;除了第一行配置，剩下的配置都有一个统一的格式:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;sentinel &amp;lt;option_name&amp;gt; &amp;lt;master_name&amp;gt; &amp;lt;option_value&amp;gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;ul&gt;
&lt;li&gt;down-after-milliseconds&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;表示：如果master在“一定时间范围”内不回应PONG 或者是回复了一个错误消息，那么这个sentinel会主观地(单方面地)认为这个master已经不可用了(subjectively down, 也简称为SDOWN)。而这个down-after-milliseconds就是用来指定这个“一定时间范围”的，单位是毫秒。&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;parallel-syncs&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;在发生failover主备切换时，这个选项指定了最多可以有多少个slave同时对新的master进行同步，这个数字越小，完成failover所需的时间就越长，但是如果这个数字越大，就意味着越多的slave因为replication而不可用。可以通过将这个值设为 1 来保证每次只有一个slave处于不能处理命令请求的状态。&lt;/p&gt;
&lt;p&gt;​ &lt;a href=&quot;https://segmentfault.com/a/1190000002685515&quot;&gt;Redis Sentinel机制与用法(二)&lt;/a&gt;&lt;/p&gt;
&lt;h2&gt;python访问Sentinel集群&lt;/h2&gt;
&lt;pre&gt;&lt;code&gt;from redis.sentinel import Sentinel #加载redis模块
# 连接哨兵服务器
sentinel = Sentinel([(&apos;192.168.221.160&apos;, 26379),
       (&apos;192.168.221.161&apos;, 26379)], socket_timeout=0.1)
       
sentinel.discover_master(&apos;mymaster&apos;) #获取主redis服务器地址

sentinel.discover_slaves(&apos;mymaster&apos;)#获取从redis服务区地址

master = sentinel.master_for(&apos;mymaster&apos;, socket_timeout=0.1)
master.set(&apos;foo&apos;,&apos;bar&apos;) #获取主redis服务器并进行写入

slave = sentinel.slave_for(&apos;mymaster&apos;, socket_timeout=0.1)
slave.get(&apos;foo&apos;)#获取从redis服务器进行获取
&lt;/code&gt;&lt;/pre&gt;
</content:encoded></item><item><title>requests 带 cookie 请求百度接口拿不到数据</title><link>https://gilgameshzzz.github.io/posts/request_cookie/</link><guid isPermaLink="true">https://gilgameshzzz.github.io/posts/request_cookie/</guid><description>Postman 里只填 BDUSS 就能拿到数据，Python requests 照抄却是空的——差在 Postman 自动补上的 BDPPN。</description><pubDate>Fri, 19 Apr 2019 00:00:00 GMT</pubDate><content:encoded>&lt;h2&gt;问题&lt;/h2&gt;
&lt;p&gt;调用百度某个接口时，Postman 里只需要在 Cookie 里填 &lt;code&gt;BDUSS&lt;/code&gt; 就能拿到数据；但用 Python &lt;code&gt;requests&lt;/code&gt; 同样只传 &lt;code&gt;BDUSS&lt;/code&gt;，返回的却是空内容。&lt;/p&gt;
&lt;p&gt;把浏览器上的 cookie 整个复制过去就正常了，一时看不出是哪一项在起作用。&lt;/p&gt;
&lt;h2&gt;原因&lt;/h2&gt;
&lt;p&gt;逐项排除后发现，Postman 除了我自己填的 &lt;code&gt;BDUSS&lt;/code&gt;，还额外带了一个 &lt;code&gt;BDPPN&lt;/code&gt;。Python 这边补上这两个就能取到数据：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;import requests

cookies = {
    &quot;BDUSS&quot;: &quot;...&quot;,
    &quot;BDPPN&quot;: &quot;...&quot;,
}
resp = requests.get(url, cookies=cookies)
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;code&gt;BDUSS&lt;/code&gt; 和 &lt;code&gt;BDPPN&lt;/code&gt; 都是百度自己定义的 cookie 名。&lt;/p&gt;
&lt;h2&gt;Postman 是怎么知道要带 BDPPN 的&lt;/h2&gt;
&lt;p&gt;把 Postman 里的 cookie 全部删掉重试：&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;第一次访问拿不到内容，但响应里带了 &lt;code&gt;Set-Cookie&lt;/code&gt;，Postman 把它存进了自己的 cookie jar；&lt;/li&gt;
&lt;li&gt;第二次访问时 Postman 自动带上了这个 &lt;code&gt;BDPPN&lt;/code&gt;，于是有数据了。&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;也就是说 Postman 默认维护会话状态，而裸写的 &lt;code&gt;requests.get()&lt;/code&gt; 不会。&lt;/p&gt;
&lt;h2&gt;更好的写法&lt;/h2&gt;
&lt;p&gt;用 &lt;code&gt;requests.Session()&lt;/code&gt;，它同样会自动处理 &lt;code&gt;Set-Cookie&lt;/code&gt;，行为就和 Postman 一致了，不用手工抄 cookie：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;import requests

with requests.Session() as s:
    s.cookies.set(&quot;BDUSS&quot;, &quot;...&quot;)
    s.get(url)        # 第一次：服务端下发 BDPPN，session 自动存下
    resp = s.get(url) # 第二次：自动带上，拿到数据
&lt;/code&gt;&lt;/pre&gt;
</content:encoded></item><item><title>python定时运行代码</title><link>https://gilgameshzzz.github.io/posts/python%E6%AF%8F%E6%97%A5%E5%AE%9A%E6%97%B6/</link><guid isPermaLink="true">https://gilgameshzzz.github.io/posts/python%E6%AF%8F%E6%97%A5%E5%AE%9A%E6%97%B6/</guid><description>这种方式最简单，只要在循环里加入需要执行的代码，然后sleep后在执行。</description><pubDate>Fri, 01 Mar 2019 00:00:00 GMT</pubDate><content:encoded>&lt;h1&gt;循环 sleep&lt;/h1&gt;
&lt;p&gt;这种方式最简单，只要在循环里加入需要执行的代码，然后sleep后在执行。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;import time

# 每n秒执行一次
def timer(n):
    while True:
        function(*args,**kargs)
        time.sleep(n)
# 5s
timer(5)
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;这个方法的缺点：只能执行固定间隔时间的任务，如果有定时任务就无法完成。并且sleep是一个阻塞函数，也就是说sleep这一段时间，啥都不能做。&lt;/p&gt;
&lt;hr /&gt;
&lt;h1&gt;threading模块中的Timer&lt;/h1&gt;
&lt;p&gt;threading模块中的Timer是一个非阻塞函数，比sleep稍好一点。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;from threading import Timer

def Timertion(second):
    function(*args, **kargs)
    t = Timer(second, printTime, (*args, **kargs))
    t.start()
# 5s
Timertion(5)
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Timer 函数第一个参数是时间间隔（单位是秒），第二个参数是要调用的函数名，第三个参数是调用函数的参数(tuple)&lt;/p&gt;
&lt;hr /&gt;
&lt;h1&gt;sched模块&lt;/h1&gt;
&lt;p&gt;sched 模块是 Python 内置的模块，它是一个调度（延时处理机制），每次想要定时执行某任务都必须写入一个调度。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;import sched
import time

# 初始化sched模块的 scheduler 类
# 第一个参数是一个可以返回时间戳的函数，第二个参数可以在定时未到达之前阻塞。
schedule = sched.scheduler(time.time, time.sleep)
# 被周期性调度触发的函数
def Timertion(second):
    function(*args, **kargs)
    schedule.enter(second, 0, function, (*args, **kargs))
# 默认参数60s
def main(second=60):
    # enter四个参数分别为：间隔时间、优先级（用于同时间到达的两个事件同时执行时定序）、被调用触发的函数，
    # 第四个：给该触发函数的参数（tuple形式）
    schedule.enter(0, 0, function, (*args, **kargs))
    schedule.run()
    
# 10s 输出一次
main(10)
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;使用注意：&lt;/h2&gt;
&lt;p&gt;（1）传调用函数的参数时，一定要以 tuple 给，如果只有一个参数就(xx,)&lt;br /&gt;
  （2）sched 模块不是循环的，一次调度被执行后就 Over 了，如果想再执行，请再次 enter&lt;/p&gt;
&lt;hr /&gt;
&lt;h1&gt;APScheduler定时框架&lt;/h1&gt;
&lt;p&gt;终于找到了可以每天定时运行指定代码的方式了&lt;br /&gt;
  &lt;a href=&quot;https://apscheduler.readthedocs.io/en/latest/userguide.html&quot;&gt;APScheduler&lt;/a&gt;是一个 Python 定时任务框架，使用起来十分方便。提供了基于日期、固定时间间隔以及 crontab 类型的任务，并且可以持久化任务、并以 daemon 方式运行应用。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;from apscheduler.schedulers.blocking import BlockingScheduler

def every_job():
    function(*args, **kargs)
    
# BlockingScheduler
scheduler = BlockingScheduler()
scheduler.add_job(every_job, &apos;cron&apos;, day_of_week=&apos;1-6&apos;, hour=16, minute=40)
scheduler.start()
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;代码中的 BlockingScheduler 是什么呢？&lt;/p&gt;
&lt;p&gt;BlockingScheduler 是 APScheduler 中的调度器，APScheduler 中有两种常用的调度器，BlockingScheduler 和 BackgroundScheduler，当调度器是应用中唯一要运行的任务时，使用 BlockingSchedule，如果希望调度器在后台执行，使用 BackgroundScheduler。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;1. BlockingScheduler: use when the scheduler is the only thing running in your process
2. BackgroundScheduler: use when you’re not using any of the frameworks below, and want the scheduler to run in the background inside your application
3. AsyncIOScheduler: use if your application uses the asyncio module
4. GeventScheduler: use if your application uses gevent
5. TornadoScheduler: use if you’re building a Tornado application
6. TwistedScheduler: use if you’re building a Twisted application
7. QtScheduler: use if you’re building a Qt application
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;APScheduler四个组件&lt;/h2&gt;
&lt;p&gt;APScheduler 四个组件分别为：触发器(trigger)，作业存储(job store)，执行器(executor)，调度器(scheduler)。&lt;/p&gt;
&lt;h3&gt;触发器(trigger)&lt;/h3&gt;
&lt;p&gt;包含调度逻辑，每一个作业有它自己的触发器，用于决定接下来哪一个作业会运行。除了他们自己初始配置意外，触发器完全是无状态的。&lt;br /&gt;
APScheduler 有三种内建的 trigger:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;date: 特定的时间点触发&lt;/li&gt;
&lt;li&gt;interval: 固定时间间隔触发&lt;/li&gt;
&lt;li&gt;cron: 在特定时间周期性地触发&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;作业存储(job store)&lt;/h3&gt;
&lt;p&gt;存储被调度的作业，默认的作业存储是简单地把作业保存在内存中，其他的作业存储是将作业保存在数据库中。一个作业的数据讲在保存在持久化作业存储时被序列化，并在加载时被反序列化。调度器不能分享同一个作业存储。&lt;br /&gt;
APScheduler 默认使用 MemoryJobStore，可以修改使用 DB 存储方案&lt;/p&gt;
&lt;h3&gt;执行器(executor)&lt;/h3&gt;
&lt;p&gt;处理作业的运行，他们通常通过在作业中提交制定的可调用对象到一个线程或者进城池来进行。当作业完成时，执行器将会通知调度器。&lt;br /&gt;
最常用的 executor 有两种：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;ProcessPoolExecutor&lt;/li&gt;
&lt;li&gt;ThreadPoolExecutor&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;调度器(scheduler)&lt;/h3&gt;
&lt;p&gt;通常在应用中只有一个调度器，应用的开发者通常不会直接处理作业存储、调度器和触发器，相反，调度器提供了处理这些的合适的接口。配置作业存储和执行器可以在调度器中完成，例如添加、修改和移除作业。&lt;/p&gt;
&lt;h2&gt;配置调度器&lt;/h2&gt;
&lt;p&gt;APScheduler提供了许多不同的方式来配置调度器，灵活性强&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;from apscheduler.schedulers.blocking import BlockingScheduler

def job():
   function(*args, **kargs)
   
# 定义BlockingScheduler
sched = BlockingScheduler()
sched.add_job(job, &apos;interval&apos;, seconds=5)
sched.start()
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;上述代码创建了一个 BlockingScheduler，并使用默认内存存储和默认执行器。(默认选项分别是 MemoryJobStore 和 ThreadPoolExecutor，其中线程池的最大线程数为10)。配置完成后使用 start() 方法来启动。&lt;/p&gt;
&lt;p&gt;如果想要显式设置 job store(使用mongo存储)和 executor 可以这样写：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;from pymongo import MongoClient
from apscheduler.schedulers.blocking import BlockingScheduler
from apscheduler.jobstores.memory import MemoryJobStore
from apscheduler.jobstores.mongodb import MongoDBJobStore
from apscheduler.executors.pool import ThreadPoolExecutor, ProcessPoolExecutor
# MongoDB 参数
host = &apos;127.0.0.1&apos;
port = 27017
client = MongoClient(host, port)
# 输出时间
def job():
    function(*args, **kargs)
# 存储方式
jobstores = {
    &apos;mongo&apos;: MongoDBJobStore(collection=&apos;job&apos;, database=&apos;test&apos;, client=client),
    &apos;default&apos;: MemoryJobStore()
}
executors = {
    &apos;default&apos;: ThreadPoolExecutor(10),
    &apos;processpool&apos;: ProcessPoolExecutor(3)
}
job_defaults = {
    &apos;coalesce&apos;: False,
    &apos;max_instances&apos;: 3
}
scheduler = BlockingScheduler(jobstores=jobstores, executors=executors, job_defaults=job_defaults)
scheduler.add_job(job, &apos;interval&apos;, seconds=5, jobstore=&apos;mongo&apos;)
scheduler.start()
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;在 MongoDB 中可以看到 job 的状态&lt;/p&gt;
&lt;h2&gt;对 job 的操作&lt;/h2&gt;
&lt;h3&gt;添加 job&lt;/h3&gt;
&lt;ol&gt;
&lt;li&gt;
&lt;p&gt;add_job()&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;scheduled_job()&lt;br /&gt;
第二种方法只适用于应用运行期间不会改变的 job，而第一种方法返回一个&lt;a href=&quot;https://apscheduler.readthedocs.io/en/latest/modules/job.html#apscheduler.job.Job&quot;&gt;apscheduler.job.Job&lt;/a&gt; 的实例，可以用来改变或者移除 job。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;from apscheduler.schedulers.blocking import BlockingScheduler
sched = BlockingScheduler()
# 装饰器
@sched.scheduled_job(&apos;interval&apos;, id=&apos;my_job_id&apos;, seconds=5)
def job_function():
    print(&quot;Hello World&quot;)
# 开始
sched.start()
&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;/ol&gt;
&lt;h3&gt;删除 job&lt;/h3&gt;
&lt;ol&gt;
&lt;li&gt;
&lt;p&gt;remove_job()&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;job.remove()&lt;br /&gt;
remove_job 使用 jobID 移除&lt;br /&gt;
job.remove() 使用 add_job() 返回的实例&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;job = scheduler.add_job(myfunc, &apos;interval&apos;, minutes=2)
job.remove()
# id
scheduler.add_job(myfunc, &apos;interval&apos;, minutes=2, id=&apos;my_job_id&apos;)
scheduler.remove_job(&apos;my_job_id&apos;)
&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;/ol&gt;
&lt;h3&gt;暂停和恢复 job&lt;/h3&gt;
&lt;p&gt;暂停:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;apscheduler.job.Job.pause()
apscheduler.schedulers.base.BaseScheduler.pause_job()
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;恢复:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;apscheduler.job.Job.resume()
apscheduler.schedulers.base.BaseScheduler.resume_job()
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;希望你还记得 apscheduler.job.Job 是 add_job() 返回的实例&lt;/p&gt;
&lt;h3&gt;获取 job 列表&lt;/h3&gt;
&lt;p&gt;获得可调度 job 列表，可以使用&lt;a href=&quot;https://apscheduler.readthedocs.io/en/latest/modules/schedulers/base.html#apscheduler.schedulers.base.BaseScheduler.get_jobs&quot;&gt;get_jobs()&lt;/a&gt; 来完成，它会返回所有的 job 实例.也可以使用&lt;a href=&quot;https://apscheduler.readthedocs.io/en/latest/modules/schedulers/base.html#apscheduler.schedulers.base.BaseScheduler.print_jobs&quot;&gt;print_jobs()&lt;/a&gt; 来输出所有格式化的 job 列表。&lt;/p&gt;
&lt;h3&gt;修改 job&lt;/h3&gt;
&lt;p&gt;除了 jobID 之外 job 的所有属性都可以修改，使用 apscheduler.job.Job.modify() 或者 modify_job() 修改一个 job 的属性&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;job.modify(max_instances=6, name=&apos;Alternate name&apos;)
modify_job(&apos;my_job_id&apos;, trigger=&apos;cron&apos;, minute=&apos;*/5&apos;)
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;关闭 job&lt;/h3&gt;
&lt;p&gt;默认情况下调度器会等待所有的 job 完成后，关闭所有的调度器和作业存储。将 wait 选项设置为 False 可以立即关闭。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;scheduler.shutdown()
scheduler.shutdown(wait=False)
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;scheduler 事件&lt;/h3&gt;
&lt;p&gt;scheduler 可以添加事件监听器，并在特殊的时间触发。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;def my_listener(event):
    if event.exception:
        print(&apos;The job crashed :(&apos;)
    else:
        print(&apos;The job worked :)&apos;)
# 添加监听器
scheduler.add_listener(my_listener, EVENT_JOB_EXECUTED | EVENT_JOB_ERROR)
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;trigger 规则&lt;/h2&gt;
&lt;p&gt;&lt;a href=&quot;https://apscheduler.readthedocs.io/en/latest/modules/triggers/date.html&quot;&gt;date&lt;/a&gt;&lt;br /&gt;
  最基本的一种调度，作业只会执行一次。它的参数如下：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;run_date (datetime|str) – the date/time to run the job at&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;timezone (datetime.tzinfo|str) – time zone for run_date if it doesn’t have one already&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;from datetime import date
from apscheduler.schedulers.blocking import BlockingScheduler
sched = BlockingScheduler()
def my_job(text):
    print(text)
# The job will be executed on November 6th, 2009
sched.add_job(my_job, &apos;date&apos;, run_date=date(2009, 11, 6), args=[&apos;text&apos;])
sched.add_job(my_job, &apos;date&apos;, run_date=datetime(2009, 11, 6, 16, 30, 5), args=[&apos;text&apos;])
sched.add_job(my_job, &apos;date&apos;, run_date=&apos;2009-11-06 16:30:05&apos;, args=[&apos;text&apos;])
# The &apos;date&apos; trigger and datetime.now() as run_date are implicit
sched.add_job(my_job, args=[&apos;text&apos;])
sched.start()
&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;a href=&quot;https://apscheduler.readthedocs.io/en/latest/modules/triggers/cron.html&quot;&gt;cron&lt;/a&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;year (int|str) – 4-digit year&lt;/li&gt;
&lt;li&gt;month (int|str) – month (1-12)&lt;/li&gt;
&lt;li&gt;day (int|str) – day of the (1-31)&lt;/li&gt;
&lt;li&gt;week (int|str) – ISO week (1-53)&lt;/li&gt;
&lt;li&gt;day_of_week (int|str) – number or name of weekday (0-6 or mon,tue,wed,thu,fri,sat,sun)&lt;/li&gt;
&lt;li&gt;hour (int|str) – hour (0-23)&lt;/li&gt;
&lt;li&gt;minute (int|str) – minute (0-59)&lt;/li&gt;
&lt;li&gt;second (int|str) – second (0-59)&lt;/li&gt;
&lt;li&gt;start_date (datetime|str) – earliest possible date/time to trigger on (inclusive)&lt;/li&gt;
&lt;li&gt;end_date (datetime|str) – latest possible date/time to trigger on (inclusive)&lt;/li&gt;
&lt;li&gt;timezone (datetime.tzinfo|str) – time zone to use for the date/time calculations (defaults to scheduler timezone)&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;表达式:&lt;br /&gt;
&lt;img src=&quot;/img/python_timer_expression.png&quot; alt=&quot;python_timer_expression&quot; /&gt;&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;from apscheduler.schedulers.blocking import BlockingScheduler

def job_function():
    print(&quot;Hello World&quot;)
# BlockingScheduler
sched = BlockingScheduler()
# Schedules job_function to be run on the third Friday
# of June, July, August, November and December at 00:00, 01:00, 02:00 and 03:00
sched.add_job(job_function, &apos;cron&apos;, month=&apos;6-8,11-12&apos;, day=&apos;3rd fri&apos;, hour=&apos;0-3&apos;)
# Runs from Monday to Friday at 5:30 (am) until 2014-05-30 00:00:00
sched.add_job(job_function, &apos;cron&apos;, day_of_week=&apos;mon-fri&apos;, hour=5, minute=30, end_date=&apos;2014-05-30&apos;)
sched.start()
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;a href=&quot;https://apscheduler.readthedocs.io/en/latest/modules/triggers/interval.html&quot;&gt;interval&lt;/a&gt;&lt;br /&gt;
参数：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;weeks (int) – number of weeks to wait&lt;/li&gt;
&lt;li&gt;days (int) – number of days to wait&lt;/li&gt;
&lt;li&gt;hours (int) – number of hours to wait&lt;/li&gt;
&lt;li&gt;minutes (int) – number of minutes to wait&lt;/li&gt;
&lt;li&gt;seconds (int) – number of seconds to wait&lt;/li&gt;
&lt;li&gt;start_date (datetime|str) – starting point for the interval calculation&lt;/li&gt;
&lt;li&gt;end_date (datetime|str) – latest possible date/time to trigger on&lt;/li&gt;
&lt;li&gt;timezone (datetime.tzinfo|str) – time zone to use for the date/time calculations&lt;/li&gt;
&lt;/ul&gt;
&lt;pre&gt;&lt;code&gt;from datetime import datetime
from apscheduler.schedulers.blocking import BlockingScheduler

def job_function():
    print(&quot;Hello World&quot;)
# BlockingScheduler
sched = BlockingScheduler()
# Schedule job_function to be called every two hours
sched.add_job(job_function, &apos;interval&apos;, hours=2)
# The same as before, but starts on 2010-10-10 at 9:30 and stops on 2014-06-15 at 11:00
sched.add_job(job_function, &apos;interval&apos;, hours=2, start_date=&apos;2010-10-10 09:30:00&apos;, end_date=&apos;2014-06-15 11:00:00&apos;)
sched.start()
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;a href=&quot;https://lz5z.com/Python%E5%AE%9A%E6%97%B6%E4%BB%BB%E5%8A%A1%E7%9A%84%E5%AE%9E%E7%8E%B0%E6%96%B9%E5%BC%8F/&quot;&gt;参考文章&lt;/a&gt;&lt;/p&gt;
</content:encoded></item><item><title>布隆过滤器 BloomFilter</title><link>https://gilgameshzzz.github.io/posts/bloomfilter/</link><guid isPermaLink="true">https://gilgameshzzz.github.io/posts/bloomfilter/</guid><description>用 k 个独立散列函数把元素映射到 m 位的位数组上，实现高效的&apos;可能存在 / 一定不存在&apos;判断，附 Python + Redis 实现。</description><pubDate>Thu, 28 Feb 2019 00:00:00 GMT</pubDate><content:encoded>&lt;h2&gt;Bloom Filter的算法&lt;/h2&gt;
&lt;p&gt;​       在Bloom Filter 中使用位数组来辅助实现检测判断。在初始状态下，我们声明一个包含m位的位数组，它的所有位都是0，如图14-7所示。
​					&lt;img src=&quot;/img/14-7.png&quot; alt=&quot;&quot; /&gt;&lt;/p&gt;
&lt;p&gt;​        现在我们有了一个待检测集合，其表示为S={x1, x2, …, xn} 。接下来需要做的就是检测一个x是否已经存在于集合S中。在Bloom Filter 算法中，首先使用k个相互独立、随机的散列函数来将集合S中的每个元素x1, x2, …, xn，映射到长度为m的位数组上，散列函数得到的结果记作位置索引，然后将位数组该索引对应的位置置为 1。例如，我们取k为3，表示有三个散列函数，x1 经过三个散列函数映射得到的结果分别为 1、4、8，x2 经过三个散列函数映射得到的结果分别为 4、6、10，那么位数组的 1、4、6、8、10 这五位就会被置为 1，如图14-8所示。
&lt;img src=&quot;/img/14-8.png&quot; alt=&quot;&quot; /&gt;&lt;/p&gt;
&lt;p&gt;​        如果有一个新的元素x，我们要判断x是否属于S集合，我们仍然用k个散列函数对x求映射结果。如果所有结果对应的位数组位置均为 1，那么 x 可能属于 S 这个集合，如果有一个不为1，则x不属于S集合。
​       例如，新元素x 经过三个散列函数映射的结果为4、6、8，对应的位置均为 1，则 x 可能属于 S 集合。如果结果为4、6、7，而7对应的位置为0，则x不属于S集合。
​        注意，这里m、n、k 满足的关系是 m &amp;gt; nk，也就是说位数组的长度m要比集合元素n和散列函数k的乘积还要大。
​        这样的判定方法很高效，但是也是有代价的，它可能把不属于这个集合的元素误认为属于这个集合。
​       当k值确定时，随着m/n的增大，误判概率逐渐变小。当m/n的值确定时，当k越靠近最优K值，误判概率越小。（计算公式不展示）&lt;/p&gt;
&lt;h1&gt;代码&lt;/h1&gt;
&lt;pre&gt;&lt;code&gt;class HashMap(object):
    # 实现基本的散列算法，将一个值经过散列运算后映射到一个m位数组的某一位上
    def __init__(self, m, seed):
        self.m = m
        self.seed = seed
    
    def hash(self, value):
        &quot;&quot;&quot;
        哈希算法
        :param value: value
        :return: Hash value
        &quot;&quot;&quot;
        ret = 0
        for i in range(len(value)):
            ret += self.seed * ret + ord(value[i])
        # print((self.m - 1) &amp;amp; ret)
        return (self.m - 1) &amp;amp; ret
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;​       value 是要被处理的内容。这里遍历了value的每一位，并利用ord( )方法取到每一位的ASCII码值，然后混淆seed进行迭代求和运算，最终得到一个数值。这个数值的结果就由value和seed唯一确定。再将这个数值和m进行按位与运算，即可获取到m位数组的映射结果，这样就实现了一个由字符串和seed来确定的散列函数。当m固定时，只要seed值相同，散列函数就是相同的，相同value必然会映射到相同的位置。所以如果想要构造几个不同的散列函数，只需要改变其seed就好了。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;#  seed取值范围
BLOOMFILTER_HASH_NUMBER = 6
#  需要的位数m
BLOOMFILTER_BIT = 5


class BloomFilter(object):
    &quot;&quot;&quot;
    Bloom Filter 里面需要用到k个散列函数，这里要对这几个散列
    函数指定相同的m值和不同的seed值
    &quot;&quot;&quot;
    
    def __init__(self, server):
        self.block_num = 1
        self.key = &apos;BloomFilter&apos;
        self.m = 1 &amp;lt;&amp;lt; BLOOMFILTER_BIT
        self.seeds = range(BLOOMFILTER_HASH_NUMBER)
        self.server = server
        #  散列函数列表
        self.maps = [HashMap(self.m, seed) for seed in self.seeds]
    
    def exists(self, value):
        # 判断元素是否重复
        if not value:
            return False
        exist = 1
        for map in self.maps:
            offset = map.hash(value)
            exist = exist &amp;amp; self.server.getbit(self.key, offset)
        return exist
    
    def insert(self, value):
        for f in self.maps:
            offset = f.hash(value)
            print(offset)
            self.server.setbit(self.key, offset, 1)
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;​       传入散列函数的个数，用它来生成几个不同的seed。用不同的seed来定义不同的散列原函数，这样我们就可以构造一个散列函数列表。遍历seed，构造带有不同seed值的HashMap对象，然后将HashMap对象保存成变量maps供后续使用。另外，server就是Redis连接对象，key就是这个m位数组的名称。&lt;/p&gt;
&lt;h2&gt;两个容易搞混的点&lt;/h2&gt;
&lt;p&gt;&lt;strong&gt;误判是单向的。&lt;/strong&gt; 布隆过滤器说「不存在」一定准确，说「存在」则可能是误判（false positive）。所以它适合做「快速排除」——比如缓存穿透防护、爬虫 URL 去重，判断为「可能存在」时再去查真正的数据源确认。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;标准布隆过滤器不能删除。&lt;/strong&gt; 因为一个位可能被多个元素共用，直接置 0 会把别的元素也误删。需要删除就用 Counting Bloom Filter（把每位换成计数器）或 Cuckoo Filter。&lt;/p&gt;
</content:encoded></item><item><title>Docker</title><link>https://gilgameshzzz.github.io/posts/docker/</link><guid isPermaLink="true">https://gilgameshzzz.github.io/posts/docker/</guid><description>Docker 入门笔记：安装、镜像与容器操作、用容器跑 MySQL / Nginx / Redis 主从。原文命令已按现行版本更正。</description><pubDate>Thu, 28 Feb 2019 00:00:00 GMT</pubDate><content:encoded>&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;时效注记（2026 更新）&lt;/strong&gt;：本文是 2019 年的入门笔记，原文里的安装命令（&lt;code&gt;docker-io&lt;/code&gt; 包）、&lt;code&gt;--link&lt;/code&gt; 参数、Redis 的 &lt;code&gt;slaveof&lt;/code&gt; 写法都已过时或本身有拼写错误，照抄跑不通。下面的命令已更正为现行可执行版本，并在各处标注了变化。&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;Docker 基于 Linux 的 cgroup / namespace 实现，是&lt;strong&gt;轻量级&lt;/strong&gt;的隔离方案——和虚拟机的区别在于它不虚拟硬件、不跑独立内核，直接共享宿主机内核。&lt;/p&gt;
&lt;p&gt;虚拟机（VMware / VirtualBox）屏蔽的是软硬件环境差异，属于重量级方案，占用系统资源多。&lt;/p&gt;
&lt;p&gt;适合用容器跑的常见中间件：Nginx / MySQL / Redis / RabbitMQ / ElasticSearch。&lt;/p&gt;
&lt;h2&gt;安装 Docker&lt;/h2&gt;
&lt;pre&gt;&lt;code&gt;# 原文写的是 yum -y intsall docker-io
# 一是 intsall 拼写错误，二是 docker-io 这个包早已废弃
# 现在用官方脚本或 docker-ce 包
curl -fsSL https://get.docker.com | sh

# 或者 CentOS / RHEL 手动装
sudo dnf install -y docker-ce docker-ce-cli containerd.io
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;启动服务并设为开机自启：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;sudo systemctl start docker
sudo systemctl enable docker
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;镜像与容器基本操作&lt;/h2&gt;
&lt;pre&gt;&lt;code&gt;# 查看本地镜像
docker images

# 下载镜像（原文写成大写 Docker pull，命令区分大小写，跑不了）
docker pull mysql:5.7          # 格式为 镜像名:版本号

# 查看已启动的容器
docker ps

# 查看所有容器（含已停止的）
docker ps -a

# 启动 / 停止容器
docker start 容器名
docker stop 容器名

# 查看端口占用（原文 grep 后面漏了参数）
netstat -nap | grep 3306
# 更推荐用 docker 自己的命令
docker port 容器名
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;创建容器&lt;/h2&gt;
&lt;p&gt;Nginx：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;docker run -d -p 80:80 --name nginx nginx
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;MySQL：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;docker run -d -p 3306:3306 --name mysql57 \
  -e MYSQL_ROOT_PASSWORD=你的密码 \
  mysql:5.7
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;code&gt;-d&lt;/code&gt; 后台运行，&lt;code&gt;-p 宿主机端口:容器端口&lt;/code&gt; 做端口映射，&lt;code&gt;--name&lt;/code&gt; 指定容器名，&lt;code&gt;-e&lt;/code&gt; 传环境变量。&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;密码别像原文那样直接写在命令里——它会进 shell 历史和 &lt;code&gt;docker inspect&lt;/code&gt; 的输出。生产环境用 &lt;code&gt;--env-file&lt;/code&gt; 或 secret。&lt;/p&gt;
&lt;/blockquote&gt;
&lt;h2&gt;Redis 主从&lt;/h2&gt;
&lt;p&gt;Redis 提供两种持久化方式：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;RDB&lt;/strong&gt; —— 把内存中的数据快照存成二进制 dump 文件&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;AOF&lt;/strong&gt; —— 用一个文件记录写命令，默认每秒 fsync 一次&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;先起主节点。注意 Redis 镜像的入口就是 &lt;code&gt;redis-server&lt;/code&gt;，配置参数直接跟在镜像名后面：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;docker network create redis-net

docker run -d --name redis-master --network redis-net \
  -p 6379:6379 redis \
  redis-server --appendonly yes --requirepass 你的密码
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;再起从节点：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;docker run -d --name redis-slave-1 --network redis-net redis \
  redis-server --replicaof redis-master 6379 \
               --masterauth 你的密码 \
               --requirepass 你的密码
&lt;/code&gt;&lt;/pre&gt;
&lt;blockquote&gt;
&lt;p&gt;原文这条命令有四个问题：&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;&lt;code&gt;--link&lt;/code&gt; 已是 legacy 特性，官方明确不推荐，应改用自定义 network（上面的 &lt;code&gt;redis-net&lt;/code&gt;）——同一网络内容器可直接用容器名互相访问&lt;/li&gt;
&lt;li&gt;&lt;code&gt;-- slaveof&lt;/code&gt; 中间多了个空格，参数解析不了&lt;/li&gt;
&lt;li&gt;&lt;code&gt;redis-mater&lt;/code&gt; 拼错了，应为 &lt;code&gt;redis-master&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;Redis 5.0 起 &lt;code&gt;slaveof&lt;/code&gt; 更名为 &lt;code&gt;replicaof&lt;/code&gt;（旧名仍兼容但已不推荐）&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;另外从节点自己也要设 &lt;code&gt;--requirepass&lt;/code&gt;，否则从节点是无密码裸奔的。&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;验证主从是否建立：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;docker exec -it redis-master redis-cli -a 你的密码 info replication
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;输出里 &lt;code&gt;connected_slaves&lt;/code&gt; 应该等于从节点数量。&lt;/p&gt;
</content:encoded></item><item><title>装饰器、函数、一条语句实现阶乘</title><link>https://gilgameshzzz.github.io/posts/day02-%E8%A3%85%E9%A5%B0%E5%99%A8-%E5%87%BD%E6%95%B0-%E4%B8%80%E6%9D%A1%E8%AF%AD%E5%8F%A5%E5%AE%9E%E7%8E%B0%E9%98%B6%E4%B9%98/</link><guid isPermaLink="true">https://gilgameshzzz.github.io/posts/day02-%E8%A3%85%E9%A5%B0%E5%99%A8-%E5%87%BD%E6%95%B0-%E4%B8%80%E6%9D%A1%E8%AF%AD%E5%8F%A5%E5%AE%9E%E7%8E%B0%E9%98%B6%E4%B9%98/</guid><description>filter --&gt;map --&gt; reduce</description><pubDate>Thu, 28 Feb 2019 00:00:00 GMT</pubDate><content:encoded>&lt;p&gt;filter --&amp;gt;map --&amp;gt;  reduce&lt;/p&gt;
&lt;p&gt;过滤       映射       归约&lt;/p&gt;
&lt;p&gt;过滤      映射&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;a =[12, 95, 88, 45]
sum([x**x for x in a if x % 2]) 


fn = lambda x,y :x**y
def foo():
	pass
fn =foo
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;在python中函数式一等对象（一定公民）&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;函数可以赋值给变量&lt;/li&gt;
&lt;li&gt;函数可以作为函数的参数   ---&amp;gt;fillter&lt;/li&gt;
&lt;li&gt;函数可以作为函数的返回值 ---&amp;gt;装饰器&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;函数参数:&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;
&lt;p&gt;位置参数&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;可变参数 -tuple&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;关键字参数 - dict&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;命名关键字参数&lt;/p&gt;
&lt;p&gt;​&lt;/p&gt;
&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;自定义装饰器 - 装饰函数 / 装饰类  --设计模式（代理模式）&lt;/p&gt;
&lt;p&gt;代理模式&lt;/p&gt;
&lt;p&gt;用代理对象（装饰器函数）去执行被代理对象（被装饰的函数）的行为&lt;/p&gt;
&lt;p&gt;面向切面编程 —AOP&lt;/p&gt;
&lt;p&gt;在程序中跟正常业务逻辑没有必然联系而且会重复使用的功能通常被称为横切关注功能，这种横切关注功能不应该写在业务逻辑代码是上，而应该使用装饰器或中间件来完成&lt;/p&gt;
&lt;p&gt;指令式编程（汇编语言） /过程式语言（c语言）&lt;/p&gt;
&lt;p&gt;Python既支持面向对象编程，也支持函数式编程&lt;/p&gt;
&lt;p&gt;一条语句求阶乘&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;import functools

fn = lambda n: functools.reduce(int.__mul__, range(1, n + 1))
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;heapq内置模块  提供基于堆的优先排序算法&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;import heapq

list1 = [1, 4, 5, 63, 24]
heapq.nsmallest(3, list1)   # 最小的 3 个，注意 n 在前
heapq.nlargest(3, list1)    # 最大的 3 个
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;组合（从 n 个里取 k 个，不计顺序）&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;import itertools

for val in itertools.combinations(&apos;abcde&apos;, 3):
    print(val)
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;全排列用 &lt;code&gt;permutations&lt;/code&gt;——它计顺序，&lt;code&gt;(&apos;a&apos;,&apos;b&apos;)&lt;/code&gt; 和 &lt;code&gt;(&apos;b&apos;,&apos;a&apos;)&lt;/code&gt; 算两个：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;for val in itertools.permutations(&apos;abc&apos;):
    print(val)
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;笛卡尔积&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;for val in itertools.product(&apos;ABCD&apos;, &apos;123&apos;):
    print(val)
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;装饰器&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;from functools import wraps

def record(func):
    @wraps(func)
    def wrapper(*args, **kwargs):
        ret_value = func(*args, **kwargs)
        return ret_value

    return wrapper
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;带参数的装饰器&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;import time
from functools import wraps 
from random import randint

def record_time(output):

    def func(function):

        # 装饰器函数，计算函数执行时间
        @wraps(function)
        def timeCount(*args, **kwargs):
            timestart = time.time()
            result = function(*args, **kwargs)
            timesend= time.time()
            output(function.__name__, timesend - timestart) 
            return result
        return timeCount

    return func

def log_to_file(fn, duration):
    with open(&apos;result.txt&apos;, &apos;a&apos;)as fs:
        fs.write(&apos;%s: %.3f秒\n&apos; %(fn,duration))


@record_time(log_to_file)
def fool():
    time.sleep(randint(1,3))


def main():
    for _ in range(5):
        # 此处调用foo并不是真正执行分两类而是执行了wrpper
        fool()
    # 取消装饰器 
    foo2 = fool.__wrapped__
    for _ in range(3):
        # 此处调用被装饰之前写的函数
        foo2()



if __name__ == &apos;__main__&apos;:
    main()
&lt;/code&gt;&lt;/pre&gt;
</content:encoded></item><item><title>python的技巧和方法</title><link>https://gilgameshzzz.github.io/posts/python%E6%8A%80%E5%B7%A7/</link><guid isPermaLink="true">https://gilgameshzzz.github.io/posts/python%E6%8A%80%E5%B7%A7/</guid><description>l例如：获取当前文件路径。 os版： python print(os.path.dirname(file)) print(os.getcwd()) pathlib版： python print(pathlib.Path.…</description><pubDate>Thu, 28 Feb 2019 00:00:00 GMT</pubDate><content:encoded>&lt;h1&gt;1. 路径操作&lt;/h1&gt;
&lt;p&gt;比起os模块的path方法，python3标准库的pathlib模块的Path处理起路径更加的容易。&lt;/p&gt;
&lt;p&gt;l例如：获取当前文件路径。
os版：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;print(os.path.dirname(__file__))
print(os.getcwd())
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;pathlib版：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;print(pathlib.Path.cwd())
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;看着好像没啥区别，然后看下面这个。&lt;/p&gt;
&lt;h2&gt;获取上两级文件目录&lt;/h2&gt;
&lt;p&gt;os版：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;print(os.path.dirname(os.path.dirname(os.getcwd())))
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;pathlib版:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;print(pathlib.Path.cwd().parent.parent)
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;拼接路径&lt;/h2&gt;
&lt;p&gt;os版:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;print(os.path.join(os.path.dirname(os.path.dirname(os.getcwd())),&quot;yamls&quot;,&quot;a.yaml&quot;))
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;pathlib版:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;parts=[&quot;yamls&quot;,&quot;a.yaml&quot;]
print(pathlib.Path.cwd().parent.parent.joinpath(*parts))
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;运行时拼接路径&lt;/h2&gt;
&lt;p&gt;os版:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;os.path.join(os.path.dirname(os.path.dirname(os.path.dirname(__file__))), &apos;yamls&apos;,f&apos;{site_name}.yaml&apos;)
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;pathlib版:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;parts=[&quot;yamls&quot;,&quot;a.yaml&quot;]
print(pathlib.Path(__file__).resolve().parent.parent.joinpath(*parts))
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;另外pathlib生成的是个对象，在open文件操作中可以直接运行的但是如果当作字符串操作会出现错误，此时需要对其进行转换，使用os.fspath()即可，不过一般很少有操作路径字符串的习惯。
综合起来，还是pathlib拼接路径方便。&lt;/p&gt;
&lt;h1&gt;2. 保存标准格式的yaml文件&lt;/h1&gt;
&lt;p&gt;编程免不了要写配置文件，怎么写配置也是一门学问。
YAML 是专门用来写配置文件的语言，非常简洁和强大，远比 JSON 格式方便。
YAML在python语言中有PyYAML安装包。
前提安装第三方库&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;pip install pyyaml
pip install ruamel.yaml
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;关于yaml的读取知识网上一堆了我就不说了，这里主要说写入。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;from ruamel import yaml
data={&quot;age&quot;:23,&quot;sex&quot;:&quot;男&quot;,&quot;name&quot;:&quot;牛皮&quot;}
with open(conf_file, &quot;w&quot;, encoding=&apos;utf-8&apos;) as fs:
    yaml.dump(data, fs, Dumper=yaml.RoundTripDumper, allow_unicode=True)
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;yaml写文件和json一样也是使用dump。&lt;/p&gt;
&lt;h1&gt;3. 同时迭代两个列表&lt;/h1&gt;
&lt;p&gt;以前的时候我是这么解决的&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;a = [&quot;a&quot;, &quot;b&quot;, &quot;c&quot;, &quot;d&quot;]
b = [1, 2, 3]  # 空的补充None
for index, a_item in enumerate(a):
    b_item = None
    if len(b) - 1 &amp;lt;= index:
        pass
    else:
        b_item = b[index]
    print({a_item:b_item})
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;现在通过itertools标准库的zip升级版zip_longest解决，可以通过fillvalue参数补充缺失值。当然如果比较的元素个数相同可以直接用zip。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;from itertools import zip_longest

a = [&quot;a&quot;, &quot;b&quot;, &quot;c&quot;, &quot;d&quot;,&quot;e&quot;]
b = [1, 2, 3]  # 空的补充None
for a_item, b_item in zip_longest(a,b,fillvalue=0):
    print({a_item:b_item})
&lt;/code&gt;&lt;/pre&gt;
&lt;h1&gt;4. 三元表达式还能这么用？&lt;/h1&gt;
&lt;p&gt;一般的我们这样写&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;a=&quot;hello&quot; if 2&amp;gt;1 else &quot;bye&quot;
print(a)
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;我们知道python中false实际式0，true是1，所以对于上面的式子我们就可以这么写了。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;a=[&quot;hello&quot;,&quot;bye&quot;][2&amp;lt;1]
print(a)
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;因为2&amp;lt;1是false也就是0，所以输出了第一个元素hello。&lt;/p&gt;
&lt;h1&gt;5.简单的类使用namedtuple代替&lt;/h1&gt;
&lt;p&gt;先来一个简单的例子&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;import collections
# Person=collections.namedtuple(&apos;Person&apos;,&apos;name age&apos;)
# 如果使用python中的关键字会出现错误,此时使用rename字段。
# 按照元素在元组中的下标赋值。class就是_2,def是_3
Person = collections.namedtuple(&apos;Person&apos;, [&apos;name&apos;, &apos;age&apos;, &apos;class&apos;, &apos;def&apos;, &apos;name&apos;, &apos;name&apos;], rename=True)
p = Person(name=&apos;lisa&apos;, age=&apos;12&apos;, _2=&quot;class2&quot;, _3=&quot;def&quot;, _4=&quot;name2&quot;, _5=&quot;name3&quot;)
print(p)
# 如果出现相同的字段第二次出现的时候也是用其下标，参考上面的例子。
# _fields查看字段名,可以发现内置模块和重复的字段标记为_加下标的形式
print(p._fields)
# 使用_asdict将namedtuple转为OrderedDict。
od = p._asdict()
print(od)
# 然后可以转为字典
print(dict(od))
# _replace()方法构建一个新实例，因为namedtuple是不可变类型所以这个方法可以返回一个新的对象。
new_p = p._replace(name=&quot;samJ&quot;)
print(new_p)
print(new_p is p)  # 可以看到不是同一个对象。
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;一个实用的例子pyppeteer的例子感受下&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;import asyncio
import pyppeteer
from collections import namedtuple

Response = namedtuple(&quot;rs&quot;, &quot;title url html cookies headers history status&quot;)


async def get_html(url, timeout=30):
    # 默认30s
    browser = await pyppeteer.launch(headless=True, args=[&apos;--no-sandbox&apos;])
    page = await  browser.newPage()
    res = await page.goto(url, options={&apos;timeout&apos;: int(timeout * 1000)})
    data = await page.content()
    title = await page.title()
    resp_cookies = await page.cookies()
    resp_headers = res.headers
    resp_history = None
    resp_status = res.status
    response = Response(title=title, url=url,
                        html=data,
                        cookies=resp_cookies,
                        headers=resp_headers,
                        history=resp_history,
                        status=resp_status)
    return response


if __name__ == &apos;__main__&apos;:
    url_list = [&quot;http://www.10086.cn/index/tj/index_220_220.html&quot;, &quot;http://www.10010.com/net5/011/&quot;,
                &quot;http://python.jobbole.com/87541/&quot;]
    task = (get_html(url) for url in url_list)

    loop = asyncio.get_event_loop()
    results = loop.run_until_complete(asyncio.gather(*task))
    for res in results:
        print(res.title)
&lt;/code&gt;&lt;/pre&gt;
&lt;h1&gt;6 使用枚举让数字变得更易懂。&lt;/h1&gt;
&lt;pre&gt;&lt;code&gt;import enum


# 枚举
@enum.unique
class Sex(enum.Enum):
    man = 12
    woman = 13

    # 因为加了唯一值的装饰器所以下面添加属性会报错
    # boy=12


print(Sex.man.name)
print(Sex.woman.value)

# 遍历
for item in Sex:
    print(item.name)
    print(item.value)
print(&quot;-&quot; * 40)
# 其他使用方式
words = enum.Enum(
    value=&apos;item&apos;,
    names=(&apos;a b c d e f&apos;),
)
# 输出元素c，必须是上面names里含有的值
print(words.c)
print(words.f)
# 因为names不含有w所以报错
try:
    print(words.w)
except AttributeError as e:
    print(e.args)
print(&quot;-&quot; * 40)
for word in words:
    print(word.name, word.value)  # 默认赋值为、从1开始自增。
print(&quot;-&quot; * 40)
# 如果自定义元素的值啧改为一下元组的形式
words2 = enum.Enum(
    value=&apos;item2&apos;,
    names=[(&apos;a&apos;, 23), (&apos;b&apos;, 56), (&quot;c&quot;, 12), (&quot;d&quot;, 333)]
)
for word2 in words2:
    print(word2.name, word2.value)
&lt;/code&gt;&lt;/pre&gt;
&lt;h1&gt;7 链式合并字典chainmap的使用&lt;/h1&gt;
&lt;pre&gt;&lt;code&gt;from collections import ChainMap

# ChainMap

d1 = {&apos;a&apos;: 1, &apos;b&apos;: 2}
d2 = {&apos;a2&apos;: 3, &apos;b2&apos;: 4}
d3 = {&apos;a3&apos;: 5, &apos;b3&apos;: 6}
d4 = {&apos;a4&apos;: 7, &apos;b4&apos;: 8}
c = ChainMap(d1, d2, d3, d4)  # 多个字典合并为一个
for k, v in c.items():
    print(k, v)
print(c.maps)  # 要搜索的索引列表

c.maps = list(reversed(c.maps))  # 逆转映射列表
print(c)

# 因为c和d1-d4对应的索引位置实际是一个所以，修改c的时候会影响到d1到d4其中饿的一个值，同理修改
# d1-d4的时候也会影响到c。
# 所以使用new_child创建一个新的映射。再修改就影响不到底层的数据了。
c2 = c.new_child()
c2[&quot;a4&quot;] = 100
print(c)
print(c2)
# 输出发现c的值没有发生变化，只要c2变化。
d5 = {&quot;a5&quot;: 34, &quot;b5&quot;: 78}
c2 = c2.new_child(d5)  # 可以在原来的映射基础上添加新的映射
print(c2)
&lt;/code&gt;&lt;/pre&gt;
&lt;h1&gt;8 在不打乱列表顺序的基础上插入元素&lt;/h1&gt;
&lt;pre&gt;&lt;code&gt;import bisect

&quot;&quot;&quot;
bisect 模块，用于维护有序列表。
bisect 模块实现了一个算法用于插入元素到有序列表。
在一些情况下，这比反复排序列表或构造一个大的列表再排序的效率更高。
Bisect 是二分法的意思，这里使用二分法来排序，它会将一个元素插入到一个有序列表的合适位置，
这使得不需要每次调用 sort 的方式维护有序列表。
&quot;&quot;&quot;
values = [14, 85, 77, 26, 50, 45, 66, 79, 10, 3, 84, 77, 1]
print(&quot;New Pos Content&quot;)
print(&quot;--- --- -------&quot;)
l = []
for i in values:
    postion = bisect.bisect(l, i)  # 返回插入的位置
    bisect.insort(l, i)  # 等于insort_right
    print(&apos;{:3}{:3}&apos;.format(i, postion), l)

&quot;&quot;&quot;
Bisect模块提供的函数有：

bisect.bisect_left(a,x, lo=0, hi=len(a)) :
查找在有序列表 a 中插入 x 的index。lo 和 hi 用于指定列表的区间，默认是使用整个列表。如果 x 已经存在，在其左边插入。返回值为 index。

bisect.bisect_right(a,x, lo=0, hi=len(a))
bisect.bisect(a, x,lo=0, hi=len(a)) ：
这2个函数和 bisect_left 类似，但如果 x 已经存在，在其右边插入。

bisect.insort_left(a,x, lo=0, hi=len(a)) ：
在有序列表 a 中插入 x。和 a.insert(bisect.bisect_left(a,x, lo, hi), x) 的效果相同。

bisect.insort_right(a,x, lo=0, hi=len(a))
bisect.insort(a, x,lo=0, hi=len(a)) :
和 insort_left 类似，但如果 x 已经存在，在其右边插入。

Bisect 模块提供的函数可以分两类： bisect* 只用于查找 index， 不进行实际的插入；
而 insort* 则用于实际插入。该模块比较典型的应用是计算分数等级：
&quot;&quot;&quot;
&lt;/code&gt;&lt;/pre&gt;
&lt;h1&gt;9 关于字典的逻辑运算你了解多少&lt;/h1&gt;
&lt;pre&gt;&lt;code&gt;# 使用&amp;amp;操作符查看字典的相同之处
#字典键支持常见的集合操作，并集交集差集。
a = {&apos;x&apos;: 1, &apos;y&apos;: 2, &apos;z&apos;: 3}
b = {&apos;w&apos;: 2, &apos;z&apos;: 4, &apos;x&apos;: 3, &apos;z&apos;: 3}

# 获取相同的键
c = a.keys() &amp;amp; b.keys()
print(c)
# 获取相同的键值对
d = a.items() &amp;amp; b.items()
print(d)
# 创建一个新的字典并删除某些键

e = {k: a[k] for k in a.keys() - {&apos;z&apos;, &apos;x&apos;}}
print(e)
&lt;/code&gt;&lt;/pre&gt;
&lt;h1&gt;10 给切片起个名字&lt;/h1&gt;
&lt;pre&gt;&lt;code&gt;a=&quot;safr3.14&quot;
print(a[-4:])
#上面可以改为
pie = slice(len(a)-4, len(a))
print(a[pie])   # 和 a[-4:] 等价，但切片有了名字，可复用、可读
&lt;/code&gt;&lt;/pre&gt;
&lt;h1&gt;11 获取出现频率高的元素&lt;/h1&gt;
&lt;pre&gt;&lt;code&gt;from collections import Counter

text = &quot;abcdfegtehto;grgtgjri&quot;  # 可迭代对象
lis = [&quot;a&quot;, &quot;c&quot;, &quot;d&quot;, &quot;t&quot;, &quot;b&quot;]
dic = {&quot;a&quot;: 1, &quot;b&quot;: 4, &quot;c&quot;: 2, &quot;d&quot;: 9}  # 字典也可以
c = Counter()  # 可以定义空容器然后update
c.update(text)
c2 = Counter()
c2.update(dic)

c3 = Counter(lis)  # 也可以直接传入对象
print(c)
print(c2)
print(c3)

# 使用c.most_common(n)获取前n出现频率最高的元素,列表元组类型
print(c.most_common(4))
&lt;/code&gt;&lt;/pre&gt;
</content:encoded></item><item><title>redis订阅python代码</title><link>https://gilgameshzzz.github.io/posts/redis%E8%AE%A2%E9%98%85python%E4%BB%A3%E7%A0%81/</link><guid isPermaLink="true">https://gilgameshzzz.github.io/posts/redis%E8%AE%A2%E9%98%85python%E4%BB%A3%E7%A0%81/</guid><description>def listen(self): for item in self.crawlseeds.listen(): if item&apos;type&apos; != &apos;message&apos;: continue meta = str(item&apos;d…</description><pubDate>Thu, 28 Feb 2019 00:00:00 GMT</pubDate><content:encoded>&lt;pre&gt;&lt;code&gt;class Listener(object):
    &quot;&quot;&quot;
    监听器，用于监听Redis订阅的data
    &quot;&quot;&quot;
    def __init__(self):
        # self.r = redis.Redis(host=&apos;139.198.4.56&apos;, port=&apos;6379&apos;)
        # redis_client为redis连接设置
        self.redis_client = redis_client
        self.crawl_seeds = self.redis_client.pubsub()
        self.crawl_seeds.subscribe(&apos;content&apos;)
        logging.info(&quot;连接到Redis...&quot;)
    
    def listen(self):
        for item in self.crawl_seeds.listen():
            if item[&apos;type&apos;] != &apos;message&apos;:
                continue
            meta = str(item[&apos;data&apos;], encoding=&apos;utf-8&apos;)
            logging.info(&apos;收:%s &apos; % json.loads(meta).get(&apos;content&apos;, &apos;&apos;).strip())
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;用日志打印接受结果&lt;/p&gt;
</content:encoded></item><item><title>哈希算法-加密-编码</title><link>https://gilgameshzzz.github.io/posts/%E5%93%88%E5%B8%8C%E7%AE%97%E6%B3%95-%E5%8A%A0%E5%AF%86-%E7%BC%96%E7%A0%81/</link><guid isPermaLink="true">https://gilgameshzzz.github.io/posts/%E5%93%88%E5%B8%8C%E7%AE%97%E6%B3%95-%E5%8A%A0%E5%AF%86-%E7%BC%96%E7%A0%81/</guid><description>哈希算法 - 哈希摘要 - 数字签名 - 防篡改/保护敏感信息 哈希算法是一个单向运算的函数（单向哈希函数） 通过哈希算法可以将对象计算出哈希摘要但是哈希摘要无法还原成原来的对</description><pubDate>Thu, 28 Feb 2019 00:00:00 GMT</pubDate><content:encoded>&lt;h1&gt;哈希算法 - 加密 - 编码&lt;/h1&gt;
&lt;p&gt;哈希算法 - 哈希摘要 - 数字签名 - 防篡改/保护敏感信息
哈希算法是一个单向运算的函数（单向哈希函数）
通过哈希算法可以将对象计算出哈希摘要但是哈希摘要无法还原成原来的对&lt;/p&gt;
&lt;p&gt;Martin flower -代码有很多味道重复是最坏的一种
重构(refactor)&lt;/p&gt;
&lt;p&gt;策略模式
加密解密 - 明文 ---- 加密 -----&amp;gt; 密文 ---- 解密 -----&amp;gt; 明文
对称加密 - 加密和解密使用同一个密钥 - AES
非对称加密 - 加密和解密使用不同的密钥（公钥和私钥）- RSA
pip install pycryptodome&lt;/p&gt;
&lt;p&gt;编码和解码 - 将内存中的二进制数据处理成其他的格式 - BASE64
BASE64 - 用64个字符(a-zA-Z0-9/+)表二进制数据示所有的&lt;/p&gt;
&lt;p&gt;编码黑洞&lt;/p&gt;
&lt;p&gt;中文/日文 --&amp;gt;iso -8859 -1 Latin --&amp;gt; ??&lt;/p&gt;
&lt;p&gt;乱码 --&amp;gt;编码和解码的时候没用使用相同的字符集&lt;/p&gt;
&lt;p&gt;utf-8 -Unicode的一种变长实现方案&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;
import hashlib


class StreamHasher(object):
    &quot;&quot;&quot;docstring for streamHasher&quot;&quot;&quot;
    def __init__(self, algorithm=&apos;md5&apos;, size=4096):
        self.size = size
        alg = algorithm.lower()
        if alg ==&apos;md5&apos;:
            self.hasher = hashlib.md5()
        elif alg == &apos;sha1&apos;:
            self.hasher = hashlib.sha1()
        elif alg == &apos;sha256&apos;:
            self.hasher = hashlib.sha256()
        elif alg == &apos;sha512&apos;:
            self.hasher = hashlib.sha512()
        else:
            raise ValueError(&apos;不支持指定的哈希算法&apos;)

    
    # __call__ -魔术方法，调用对象的时候调用该方法
    def __call__(self,stream):
        return self.to_digest(stream)


    def to_digest(self, stream):
        &apos;&apos;&apos;生成摘要&apos;&apos;&apos;
        # b&apos;&apos; 是终止符，读文本文件时终止符为 &apos;&apos;
        hasher = self.hasher
        for buf in iter(lambda:stream.read(self.size), b&apos;&apos;):
            hasher.update(buf)
        # 16进制的摘要
        return hasher.hexdigest()



def to_sha256_digest(stream, block_size = 4096):
    &apos;&apos;&apos;生成 SHA256 摘要&apos;&apos;&apos;
    # b&apos;&apos; 是终止符，读文本文件时终止符为 &apos;&apos;
    hasher = hashlib.sha256()
    for buf in iter(lambda:stream.read(block_size), b&apos;&apos;):
        hasher.update(buf)
    # 16进制的摘要
    return hasher.hexdigest()


def main():
    hasher = hashlib.md5()
    with open(&apos;aaa.png&apos;, &apos;rb&apos;) as f:     
        sh = StreamHasher()
        print(sh.to_digest(f))
                
    
if __name__ == &apos;__main__&apos;:
    main()

&lt;/code&gt;&lt;/pre&gt;
</content:encoded></item><item><title>多并发编程-多线程</title><link>https://gilgameshzzz.github.io/posts/%E5%A4%9A%E5%B9%B6%E5%8F%91%E7%BC%96%E7%A8%8B-%E5%A4%9A%E7%BA%BF%E7%A8%8B/</link><guid isPermaLink="true">https://gilgameshzzz.github.io/posts/%E5%A4%9A%E5%B9%B6%E5%8F%91%E7%BC%96%E7%A8%8B-%E5%A4%9A%E7%BA%BF%E7%A8%8B/</guid><description>如果主线程结束了守护线程也不再保留即使守护线程还在执行（没有结束）</description><pubDate>Thu, 28 Feb 2019 00:00:00 GMT</pubDate><content:encoded>&lt;h1&gt;多并发编程-多线程&lt;/h1&gt;
&lt;h4&gt;守护线程/守护进程 - daemon&lt;/h4&gt;
&lt;p&gt;如果主线程结束了守护线程也不再保留即使守护线程还在执行（没有结束）&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;import threading
import time


def show_message(content):
    while True:
        print(content, end=&apos;&apos;)


def main():
    threading.Thread(
        target=show_message, args=(&apos;Ping&apos;, ), daemon=True
    ).start()
    threading.Thread(
        target=show_message, args=(&apos;Pong&apos;, ), daemon=True
    ).start()
    sleep(5)

&lt;/code&gt;&lt;/pre&gt;
&lt;blockquote&gt;
&lt;p&gt;上面这段直接跑会报 &lt;code&gt;NameError: name &apos;sleep&apos; is not defined&lt;/code&gt;——&lt;code&gt;sleep&lt;/code&gt; 来自 &lt;code&gt;time&lt;/code&gt; 模块，要么 &lt;code&gt;import time&lt;/code&gt; 后写 &lt;code&gt;time.sleep(5)&lt;/code&gt;，要么 &lt;code&gt;from time import sleep&lt;/code&gt;。&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;&lt;strong&gt;Lock&lt;/strong&gt; - 多个线程竞争临界资源（资源只有1个） - 获得锁才能操作资源&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Condition&lt;/strong&gt; —  基于Lock对象可以创建它 - wait() / notify_all()&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;实现线程调度&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;Semaphore - 多个线程竞争资源（资源有多个，但线程数量大于资源数量）&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;多个线程通信比较简单因为可以共享内存&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;多个进程通信相对比较困难，可以使用multiprocessing.Queue，通过多个进程共享一个队列来实现进程间的通信&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;生产者消费者模型 / 哲学家进餐模型 - 多线程编程模型&lt;/p&gt;
&lt;h4&gt;三种方式实现多线程&lt;/h4&gt;
&lt;p&gt;多线程程序如果没有竞争资源的场景那么通常会比较简单&lt;/p&gt;
&lt;p&gt;临界资源 - 被多个线程竞争的资源&lt;/p&gt;
&lt;p&gt;当多个线程竞争临界资源的时候如果缺乏必要的保护措施就会导致数据错乱&lt;/p&gt;
&lt;h5&gt;1.  定义线程类&lt;/h5&gt;
&lt;pre&gt;&lt;code&gt;import threading
import time


class Account(object):
    &quot;&quot;&quot;银行账户&quot;&quot;&quot;

    def __init__(self):
        self.balance = 0.0
        self.lock = threading.Lock()

    def deposit(self, money):
        # 通过锁保护临界资源
        # 可以写try-finally也可以使用上下文语法
        # self.lock.acquire()
        # try:
        #     pass
        # finally:
        #     self.lock.release()
        with self.lock:
            new_balance = self.balance + money
            time.sleep(0.001)
            self.balance = new_balance


class AddMoneyThread(threading.Thread):

    def __init__(self, account, money):
        self.account = account
        self.money = money
        # 自定义线程的初始化方法中必须调用父类的初始化方法
        super().__init__()

    # run方法是线程启动之后要执行的回调方法（钩子函数）
    # 所以启动线程不能够直接调用run方法而是通过start方法启动线程
    # 什么时候需要使用回调式编程？
    # 你知道要做什么但不知道什么时候会做这件事情
    def run(self):
        # 线程启动之后要执行的操作
        self.account.deposit(self.money)


def main():
    a1 = Account()
    threads = []
    for _ in range(100):
        t = AddMoneyThread(a1, 1)
        threads.append(t)
        t.start()
    for t in threads:
        # 等待线程执行完
        t.join()

    print(a1.balance)


if __name__ == &apos;__main__&apos;:
    main()
&lt;/code&gt;&lt;/pre&gt;
&lt;h5&gt;2. 直接通过threading.Thread&lt;/h5&gt;
&lt;pre&gt;&lt;code&gt;def add_money(account, money):
    account.deposit(money)


threads = []
account = Account()
for _ in range(100):
    t = threading.Thread(target=add_money, args=(account, 1))
    threads.append(t)
    t.start()
for t in threads:
    t.join()
print(account.balance)
&lt;/code&gt;&lt;/pre&gt;
&lt;h5&gt;3.调用线程池中的线程执行特定任务&lt;/h5&gt;
&lt;pre&gt;&lt;code&gt;from concurrent.futures import ThreadPoolExecutor


# 创建线程池
pool = ThreadPoolExecutor(max_workers=10)
futures = []
# 调用线程池中的线程来执行特定的任务
for _ in range(100):
    future = pool.submit(add_money, account, 1)
    futures.append(future)
pool.shutdown()
for future in futures:
    future.result()
print(account.balance)

# 也可以用上下文方法
futures = []
with ThreadPoolExecutor(max_workers=10) as pool:
    for _ in range(100):
        futures.append(pool.submit(add_money, account, 1))
    for future in futures:
        # 获取函数add_money的返回值
        future.result()
&lt;/code&gt;&lt;/pre&gt;
</content:encoded></item><item><title>深拷贝和浅拷贝</title><link>https://gilgameshzzz.github.io/posts/%E6%B7%B1%E6%8B%B7%E8%B4%9D%E5%92%8C%E6%B5%85%E6%8B%B7%E8%B4%9D/</link><guid isPermaLink="true">https://gilgameshzzz.github.io/posts/%E6%B7%B1%E6%8B%B7%E8%B4%9D%E5%92%8C%E6%B5%85%E6%8B%B7%E8%B4%9D/</guid><description>if name == &apos;main&apos;: print(a, id(a)) print(b, id(b)) print(c, id(c))</description><pubDate>Thu, 28 Feb 2019 00:00:00 GMT</pubDate><content:encoded>&lt;h2&gt;可变和不可变&lt;/h2&gt;
&lt;p&gt;可变类型（mutable）：列表，字典；value值改变，id值不变；
不可变类型（immutable）：数字，字符串，元组；value值改变，id值也随之改变。&lt;/p&gt;
&lt;h2&gt;赋值，拷贝和深拷贝&lt;/h2&gt;
&lt;p&gt;&lt;strong&gt;赋值&lt;/strong&gt;，只是创建一个变量，该变量指向原来内存地址，如下例：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;n4 = n3 = n2 = n1 = &quot;123/&apos;Wu&apos;&quot;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;img src=&quot;/img/fuzhi.png&quot; alt=&quot;&amp;quot;赋值&amp;quot;&quot; /&gt;&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;浅拷贝&lt;/strong&gt;，在内存中只额外创建第一层数据，如下图&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;import copy
n1 = {&quot;k1&quot;: &quot;wu&quot;, &quot;k2&quot;: 123, &quot;k3&quot;: [&quot;alex&quot;, 456]}
n3 = copy.copy(n1)
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;img src=&quot;/img/copy.png&quot; alt=&quot;浅拷贝&quot; /&gt;&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;深拷贝&lt;/strong&gt;，在内存中将所有的数据重新创建一份（递归拷贝，排除最后一层，即：python内部对字符串和数字的优化），如下图：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;import copy
n1 = {&quot;k1&quot;: &quot;wu&quot;, &quot;k2&quot;: 123, &quot;k3&quot;: [&quot;alex&quot;, 456]}
n4 = copy.deepcopy(n1)
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;img src=&quot;/img/deepcopy.png&quot; alt=&quot;深拷贝&quot; /&gt;&lt;/p&gt;
&lt;h2&gt;字符串 数字 元祖拷贝&lt;/h2&gt;
&lt;pre&gt;&lt;code&gt;# 字符串
a = &apos;copytest&apos;
b = copy.copy(a)
c = copy.deepcopy(a)

if __name__ == &apos;__main__&apos;:
    print(a, id(a))
    print(b, id(b))
    print(c, id(c))
    
#============输出结果===============#
copytest 49797384
copytest 49797384
copytest 49797384
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;整型数字，浮点型数字和元祖拷贝结果与字符串拷贝结果相同&lt;/p&gt;
&lt;h2&gt;列表拷贝&lt;/h2&gt;
&lt;pre&gt;&lt;code&gt;a = [1, 2, 5, [1, 3, 5]]
b = copy.copy(a)
c = copy.deepcopy(a)
b[-1].pop()

if __name__ == &apos;__main__&apos;:
    print(a, id(a))
    print(b, id(b))
    print(c, id(c))
#============输出结果===============#
[1, 2, 5, [1, 3]] 50059904
[1, 2, 5, [1, 3]] 50007496
[1, 2, 5, [1, 3, 5]] 50059584
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;img src=&quot;/img/process.png&quot; alt=&quot;&quot; /&gt;&lt;/p&gt;
&lt;h2&gt;单例模式下的类的拷贝&lt;/h2&gt;
&lt;pre&gt;&lt;code&gt;def singleton(cls):
    # 单例装饰器
    instances = {}

    @wraps(cls)
    def get_instances(*args, **kwargs):
        if cls not in instances:
            instances[cls] = cls(*args, **kwargs)
        return instances[cls]

    return get_instances


@singleton
class Animal():
    def __init__(self, name):
        self.name = name

    def aloud(self):
        print(self.name + &apos;123568&apos;)


a = Animal(&apos;cat&apos;)
b = copy.copy(a)
c = copy.deepcopy(a)
c.name = &apos;dog&apos;
d = Animal(&apos;adas&apos;)

if __name__ == &apos;__main__&apos;:
    print(a, id(a))
    a.aloud()
    print(b, id(b))
    b.aloud()
    print(c, id(c))
    c.aloud()
    print(d, id(d))
    d.aloud()

#============输出结果===============#
&amp;lt;__main__.Animal object at 0x02F67DD0&amp;gt; 49708496
cat123568
&amp;lt;__main__.Animal object at 0x02FACD90&amp;gt; 49991056
cat123568
&amp;lt;__main__.Animal object at 0x02FACAD0&amp;gt; 49990352
dog123568
&amp;lt;__main__.Animal object at 0x02F67DD0&amp;gt; 49708496
cat123568
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;不可变对象类型，没有被拷贝的说法，即便是用深拷贝，查看id的话也是一样的，如果对其重新赋值，也只是新创建一个对象，替换掉旧的而已。
一句话就是，不可变类型，不管是深拷贝还是浅拷贝，地址值和拷贝后的值都是一样的。&lt;/p&gt;
&lt;h2&gt;总结&lt;/h2&gt;
&lt;p&gt;1，深浅拷贝都是对源对象的复制，占用不同的内存空间。
2，不可变类型的对象，对于深浅拷贝毫无影响，最终的地址值和值都是相等的。
3，可变类型：
&lt;code&gt;=&lt;/code&gt; 赋值（严格说不是拷贝）：值相等，地址相等——它只是给同一个对象多起了一个名字，两边会互相影响
copy浅拷贝：值相等，地址不相等
deepcopy深拷贝：值相等，地址不相等&lt;/p&gt;
</content:encoded></item><item><title>Web 安全常见攻击面与 Celery 异步任务</title><link>https://gilgameshzzz.github.io/posts/%E7%AC%94%E8%AE%B0/</link><guid isPermaLink="true">https://gilgameshzzz.github.io/posts/%E7%AC%94%E8%AE%B0/</guid><description>令牌生成、CSRF、XSS、SQL 注入、点击劫持的处理要点，以及用消息队列 / Celery 把可推迟的任务异步化。</description><pubDate>Thu, 28 Feb 2019 00:00:00 GMT</pubDate><content:encoded>&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;时效注记（2026 更新）&lt;/strong&gt;：本文是 2019 年的学习笔记。安全部分的结论至今有效，只有一处依赖变了——文中提到的 &lt;code&gt;djangorestframework-jwt&lt;/code&gt; 已停止维护多年，现在应使用 &lt;code&gt;djangorestframework-simplejwt&lt;/code&gt;。&lt;/p&gt;
&lt;/blockquote&gt;
&lt;h2&gt;生成用户令牌（token）&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;哈希码 —— MD5 / SHA1（已不适合做安全用途，仅做标识可以）&lt;/li&gt;
&lt;li&gt;UUID —— 全局唯一标识符&lt;/li&gt;
&lt;li&gt;JWT —— JSON Web Token，Django 生态下用 &lt;code&gt;djangorestframework-simplejwt&lt;/code&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;两个常见用途：&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;防表单重复提交：每一次请求刷新令牌&lt;/li&gt;
&lt;li&gt;防跨站请求伪造（CSRF / XSRF）&lt;/li&gt;
&lt;/ol&gt;
&lt;h2&gt;XSS —— 跨站脚本攻击&lt;/h2&gt;
&lt;p&gt;用户在评论里输入的是 JS 代码，页面渲染时被执行。处理方式是「消毒」：&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;
&lt;p&gt;清理用户输入，过滤 JS 代码和特殊字符：&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;&lt;code&gt;&amp;lt;&lt;/code&gt; &lt;code&gt;&amp;gt;&lt;/code&gt; 尖括号&lt;/td&gt;
&lt;td&gt;&lt;code&gt;;&lt;/code&gt; 分号&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;&quot;&lt;/code&gt; 引号&lt;/td&gt;
&lt;td&gt;&lt;code&gt;()&lt;/code&gt; 括号&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;&apos;&lt;/code&gt; 单引号&lt;/td&gt;
&lt;td&gt;&lt;code&gt;&amp;amp;&lt;/code&gt; and 符号&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;%&lt;/code&gt; 百分号&lt;/td&gt;
&lt;td&gt;&lt;code&gt;+&lt;/code&gt; 加号&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;输出时做 HTML 编码，而不是存储时才处理。以 ASP.NET 的 AntiXSS 类库为例：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;// 编码后只会原样显示，不会弹框
this.lblName.Text = Encoder.HtmlEncode(&quot;&amp;lt;script&amp;gt;alert(&apos;OK&apos;);&amp;lt;/script&amp;gt;&quot;);
&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;响应头加上 &lt;code&gt;X-Content-Type-Options: nosniff&lt;/code&gt;，避免浏览器「自作聪明」地嗅探并按可执行类型解析内容。&lt;/p&gt;
&lt;/li&gt;
&lt;/ol&gt;
&lt;h2&gt;SQL 注入&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;不用字符串拼接 SQL，一律用参数化查询 / ORM&lt;/li&gt;
&lt;li&gt;应用连接数据库不要用管理员权限账号&lt;/li&gt;
&lt;li&gt;避免动态拼接表名、列名；必须动态时用白名单&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;点击劫持（clickjacking）&lt;/h2&gt;
&lt;p&gt;攻击者用 &lt;code&gt;iframe&lt;/code&gt; 嵌套你的网页并把透明度设为 0，诱导用户在看不见的页面上点击。&lt;/p&gt;
&lt;p&gt;对策是响应头 &lt;code&gt;X-Frame-Options: DENY&lt;/code&gt;（Django 里配置 &lt;code&gt;X_FRAME_OPTIONS = &apos;DENY&apos;&lt;/code&gt;），现代写法是 CSP 的 &lt;code&gt;frame-ancestors &apos;none&apos;&lt;/code&gt;。&lt;/p&gt;
&lt;h2&gt;密码存储&lt;/h2&gt;
&lt;p&gt;密码原文 → 加盐 → 生成哈希摘要。注意要用 bcrypt / scrypt / Argon2 这类慢哈希，别用 MD5、SHA1。&lt;/p&gt;
&lt;h2&gt;Web 应用优化的两大定律&lt;/h2&gt;
&lt;ol&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;消息队列让任务异步化&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;同步 —— 阻塞&lt;/li&gt;
&lt;li&gt;异步 —— 非阻塞&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;Celery&lt;/h3&gt;
&lt;p&gt;Celery 既可以充当消息的生产者，也可以充当消费者。使用时需要为其配置队列服务：RabbitMQ（推荐）或 Redis。支持定时任务 + 异步任务。&lt;/p&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;/ul&gt;
&lt;p&gt;受理用户请求的程序作为生产者把任务丢进队列，稍后由消费者取出处理。&lt;/p&gt;
&lt;p&gt;参考：&lt;a href=&quot;https://www.cnblogs.com/derek1184405959/p/8722212.html&quot;&gt;https://www.cnblogs.com/derek1184405959/p/8722212.html&lt;/a&gt;&lt;/p&gt;
</content:encoded></item><item><title>数据库优化</title><link>https://gilgameshzzz.github.io/posts/%E6%95%B0%E6%8D%AE%E5%BA%93%E4%BC%98%E5%8C%96/</link><guid isPermaLink="true">https://gilgameshzzz.github.io/posts/%E6%95%B0%E6%8D%AE%E5%BA%93%E4%BC%98%E5%8C%96/</guid><description>也许会有人要问：增加索引有如此多的优点，为什么不对表中的每一个列创建一个索引呢？因为，增加索引也有许多不利的方面。 第一，创建索引和维护索引要耗费时间，这种时间随着数据量的增加而增加。 第二，索引需要占物理空间，除了数据…</description><pubDate>Thu, 28 Feb 2019 00:00:00 GMT</pubDate><content:encoded>&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;时效注记（2026 更新）&lt;/strong&gt;：本文是 2019 年整理的面试向笔记。分库分表、索引权衡这些结构性结论依然成立，但「SQL 语句优化」一节里有几条是流传很广的经验之谈，在现代 MySQL（5.7 / 8.0）上已经不准确，下面逐条做了订正。缓存选型一节提到的 Memcached 现在基本已被 Redis 取代，新项目不必再考虑。&lt;/p&gt;
&lt;/blockquote&gt;
&lt;h2&gt;1. SQL 语句优化&lt;/h2&gt;
&lt;p&gt;1）应尽量避免在 &lt;code&gt;where&lt;/code&gt; 子句中使用 &lt;code&gt;!=&lt;/code&gt; 或 &lt;code&gt;&amp;lt;&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;2）应尽量避免在 &lt;code&gt;where&lt;/code&gt; 子句中对字段做 &lt;code&gt;null&lt;/code&gt; 判断：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;select id from t where num is null;
-- 建议改成给 num 设默认值 0，保证无 NULL：
select id from t where num = 0;
&lt;/code&gt;&lt;/pre&gt;
&lt;blockquote&gt;
&lt;p&gt;订正：&lt;strong&gt;这条已经过时&lt;/strong&gt;。MySQL 从很早开始 &lt;code&gt;IS NULL&lt;/code&gt; 就能走索引（B+ 树中 NULL 值是被索引的），&lt;code&gt;EXPLAIN&lt;/code&gt; 里会显示 &lt;code&gt;ref&lt;/code&gt; 或 &lt;code&gt;range&lt;/code&gt;。用 0 冒充「没有值」反而丢失了语义，还会让 &lt;code&gt;COUNT&lt;/code&gt;、&lt;code&gt;AVG&lt;/code&gt; 之类的聚合结果出错。该用 NULL 就用 NULL。&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;3）很多时候用 &lt;code&gt;exists&lt;/code&gt; 代替 &lt;code&gt;in&lt;/code&gt; 是一个好的选择。&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;订正：&lt;strong&gt;MySQL 5.6 起已无此必要&lt;/strong&gt;。旧版对 &lt;code&gt;IN (子查询)&lt;/code&gt; 会退化成低效的相关子查询，5.6 引入了半连接（semi-join）与物化优化后，&lt;code&gt;IN&lt;/code&gt; 和 &lt;code&gt;EXISTS&lt;/code&gt; 通常被改写成同一个执行计划。直接写更易读的那个即可，以 &lt;code&gt;EXPLAIN&lt;/code&gt; 为准。&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;4）用 &lt;code&gt;WHERE&lt;/code&gt; 子句替换 &lt;code&gt;HAVING&lt;/code&gt; 子句，因为 &lt;code&gt;HAVING&lt;/code&gt; 只在检索出所有记录之后才对结果集过滤。&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;这条仍然成立：能在分组前过滤的条件放 &lt;code&gt;WHERE&lt;/code&gt;，&lt;code&gt;HAVING&lt;/code&gt; 只留给真正针对聚合结果的条件（如 &lt;code&gt;HAVING count(*) &amp;gt; 1&lt;/code&gt;）。&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;&lt;strong&gt;真正通用的原则只有一条&lt;/strong&gt;：不要让索引列参与表达式或函数运算，否则无法使用索引。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;-- 用不上 create_time 上的索引
select * from t where date(create_time) = &apos;2026-01-01&apos;;
-- 改写成范围查询就能用上
select * from t where create_time &amp;gt;= &apos;2026-01-01&apos; and create_time &amp;lt; &apos;2026-01-02&apos;;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;隐式类型转换是同样的陷阱：&lt;code&gt;varchar&lt;/code&gt; 列写成 &lt;code&gt;where phone = 13800138000&lt;/code&gt;（不加引号）会触发转换，索引同样失效。&lt;/p&gt;
&lt;h2&gt;2.索引优化&lt;/h2&gt;
&lt;p&gt;数据库索引，是数据库管理系统中一个排序的数据结构，以协助快速查询、更新数据库表中数据。索引的实现通常使用B树及其变种B+树。
为表设置索引要付出代价的：一是增加了数据库的存储空间，二是在插入和修改数据时要花费较多的时间(因为索引也要随之变动)。
创建索引可以大大提高系统的性能。
第一，通过创建唯一性索引，可以保证数据库表中每一行数据的唯一性。
第二，可以大大加快数据的检索速度，这也是创建索引的最主要的原因。
第三，可以加速表和表之间的连接，特别是在实现数据的参考完整性方面特别有意义。
第四，在使用分组和排序子句进行数据检索时，同样可以显著减少查询中分组和排序的时间。
第五，通过使用索引，可以在查询的过程中，使用优化隐藏器，提高系统的性能。&lt;/p&gt;
&lt;p&gt;也许会有人要问：增加索引有如此多的优点，为什么不对表中的每一个列创建一个索引呢？因为，增加索引也有许多不利的方面。
第一，创建索引和维护索引要耗费时间，这种时间随着数据量的增加而增加。
第二，索引需要占物理空间，除了数据表占数据空间之外，每一个索引还要占一定的物理空间，如果要建立聚簇索引，那么需要的空间就会更大。
第三，当对表中的数据进行增加、删除和修改的时候，索引也要动态的维护，这样就降低了数据的维护速度。&lt;/p&gt;
&lt;h2&gt;3.数据库结构优化&lt;/h2&gt;
&lt;p&gt;1）范式优化： 比如消除冗余（节省空间。。）
2）反范式优化：比如适当加冗余等（减少join）
3）拆分表： 分区将数据在物理上分隔开，不同分区的数据可以制定保存在处于不同磁盘上的数据文件里。这样，当对这个表进行查询时，只需要在表分区中进行扫描，而不必进行全表扫描，明显缩短了查询时间，另外处于不同磁盘的分区也将对这个表的数据传输分散在不同的磁盘I/O，一个精心设置的分区可以将数据传输对磁盘I/O竞争均匀地分散开。对数据量大的时时表可采取此方法。可按月自动建表分区。
4）拆分，又分垂直拆分和水平拆分：
案例： 简单购物系统暂设涉及如下表：
1.产品表（数据量10w，稳定） 2.订单表（数据量200w，且有增长趋势） 3.用户表 （数据量100w，且有增长趋势） 以mysql为例讲述下水平拆分和垂直拆分，mysql能容忍的数量级在百万静态数据可以到千万&lt;/p&gt;
&lt;p&gt;​    垂直拆分：
解决问题：表与表之间的io竞争，不解决问题：单表中数据量增长出现的压力
方案: 把产品表和用户表放到一个server上 订单表单独放到一个server上&lt;/p&gt;
&lt;p&gt;​  水平拆分：
解决问题：单表中数据量增长出现的压力， 不解决问题：表与表之间的io争夺
方案: 用户表通过性别拆分为男用户表和女用户表 订单表通过已完成和完成中拆分为已完成订单和未完成订单 产品表 未完成订单放一个server上 已完成订单表盒男用户表放一个server上 女用户表放一个server上(女的爱购物 哈哈)&lt;/p&gt;
&lt;h2&gt;4.服务器硬件优化&lt;/h2&gt;
&lt;p&gt;在 SQL 和索引都调不动之后才轮到硬件。按收益排序大致是：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;内存&lt;/strong&gt; —— 让 InnoDB buffer pool 能装下热点数据集，是单项收益最大的投入（&lt;code&gt;innodb_buffer_pool_size&lt;/code&gt; 一般设为物理内存的 50%~70%）&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;磁盘&lt;/strong&gt; —— 换 NVMe SSD，随机 IOPS 比机械盘高两个数量级&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;CPU / 网络&lt;/strong&gt; —— 通常不是 OLTP 数据库的瓶颈，除非有大量复杂计算或全表扫描&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;再往上就是读写分离、分库分表和集群，属于架构问题而非硬件问题。&lt;/p&gt;
&lt;h1&gt;面试回答数据库优化问题从以下几个层面入手&lt;/h1&gt;
&lt;p&gt;（1）、根据服务层面：配置mysql性能优化参数；
（2）、从系统层面增强mysql的性能：优化数据表结构、字段类型、字段索引、分表，分库、读写分离等等。
（3）、从数据库层面增强性能：优化SQL语句，合理使用字段索引。
（4）、从代码层面增强性能：使用缓存和NoSQL数据库方式存储，如 MongoDB / Redis 来缓解高并发下数据库查询的压力。（原文还列了 Memcached，现在基本已被 Redis 取代——Redis 支持更多数据结构、有持久化和集群方案，新项目不必再考虑 Memcached。）
（5）、减少数据库操作次数，尽量使用数据库访问驱动的批处理方法。
（6）、不常使用的数据迁移备份，避免每次都在海量数据中去检索。
（7）、提升数据库服务器硬件配置，或者搭建数据库集群。
（8）、编程手段防止SQL注入：使用JDBC PreparedStatement按位插入或查询；正则表达式过滤（非法字符串过滤）；&lt;/p&gt;
</content:encoded></item><item><title>类和类之间的关系</title><link>https://gilgameshzzz.github.io/posts/%E7%B1%BB%E5%92%8C%E7%B1%BB%E4%B9%8B%E9%97%B4%E7%9A%84%E5%85%B3%E7%B3%BB/</link><guid isPermaLink="true">https://gilgameshzzz.github.io/posts/%E7%B1%BB%E5%92%8C%E7%B1%BB%E4%B9%8B%E9%97%B4%E7%9A%84%E5%85%B3%E7%B3%BB/</guid><description>Python解释器搜索一个变量会按照LEGB的顺序进行搜索 LEGB - Local -&gt;Embedded -&gt;Global -&gt; BUilt-in 局部 嵌套 全局 内置 在函数中定义的变量正常情况下属于局部作用域 但…</description><pubDate>Thu, 28 Feb 2019 00:00:00 GMT</pubDate><content:encoded>&lt;h1&gt;类和类之间的关系&lt;/h1&gt;
&lt;p&gt;-is-a 关系：（继承）
-has-a 关系（关联/聚合/合成）
-use-a 关系（依赖）&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;Python解释器搜索一个变量会按照LEGB的顺序进行搜索
 LEGB - Local -&amp;gt;Embedded -&amp;gt;Global -&amp;gt; BUilt-in
       局部     嵌套       全局      内置
 在函数中定义的变量正常情况下属于局部作用域
 但是可以通过nolocal或global关键字将其放到嵌套或全局作用域
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;UML -Unified Modeling Language - 统一建模语言
标准化的图形符号 - 便于沟通交流&lt;/p&gt;
&lt;p&gt;重要的三种图：用例图 / 类图 / 时序图&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;
from enum import Enum,unique
import random

# 定义枚举（定义符号常量的最佳选择，符号常量总是优于字面常量）
# @unique 限定唯一性
@unique
class Suite(Enum):
    &quot;&quot;&quot;docstring for suite&quot;&quot;&quot;

    SPADE = 0
    HEART = 1
    CLUB = 2
    DIAMOND = 3


class Card(object):
    &quot;&quot;&quot;docstring for ClassName&quot;&quot;&quot;
    def __init__(self, suite, face):
        self.suite =suite
        self.face = face

    def show(self):
        &apos;显示牌面&apos;
        suites = [&apos;黑桃&apos;, &apos;红心&apos;, &apos;梅花&apos;, &apos;方块&apos;]
        face = [&apos;&apos;, &apos;A&apos;,&apos;2&apos;,&apos;3&apos;, &apos;4&apos;, &apos;5&apos;, &apos;6&apos;, &apos;7&apos;, &apos;8&apos;, &apos;9&apos;, &apos;10&apos;, &apos;J&apos;, &apos;Q&apos;,&apos;k&apos;]
        return f&apos;{suites[self.suite.value]} {face[self.face]}&apos;
        
    def __str__(self):
        return self.show()

    def __repr__(self):
        return self.show()  

class Poker(object):
    &quot;&quot;&quot;扑克&quot;&quot;&quot;

    def __init__(self):
        self.index = 0
        self.cards = [Card(suite,face) 
                      for suite in Suite 
                      for face in range(1,14)]

    def shuffle(self):
        &apos;&apos;&apos;洗牌（随机乱序）&apos;&apos;&apos;
        random.shuffle(self.cards)
        self.index = 0
    
    def deal(self):
        &apos;&apos;&apos;发牌&apos;&apos;&apos;
        card = self.cards[self.index]
        self.index += 1
        return card

    @property
    def has_more(self):
        return self.index &amp;lt; len(self.cards)

class Player(object):

    def __init__(self,name):
        self.name = name
        self.card = []

    def get_one(self,card):
        &apos;&apos;&apos;摸一张牌&apos;&apos;&apos;
        self.card.append(card)

    def sort(self, comp=lambda card: card.face):
        self.card.sort(key= comp)

def main():
    poker = Poker()
    print(poker.cards)    
    poker.shuffle()
   
    # print(poker.deal())
    players= [Player(&apos;东邪&apos;),Player(&apos;西毒&apos;), Player(&apos;南帝&apos;), Player(&apos;北丐&apos;)]
    while poker.has_more:
        for player in players:
            player.get_one(poker.deal())
            
    for player in players:
        player.sort()
        print(player.name, end=&apos;&apos;)
        print(player.card)


if __name__ == &apos;__main__&apos;:
    main()

&lt;/code&gt;&lt;/pre&gt;
</content:encoded></item><item><title>面向对象-OOP</title><link>https://gilgameshzzz.github.io/posts/%E9%9D%A2%E5%90%91%E5%AF%B9%E8%B1%A1-oop-object-oriendted-programming-%E9%9D%A2%E5%90%91%E5%AF%B9%E8%B1%A1%E7%9A%84%E5%9B%9B%E5%A4%A7%E6%94%AF%E6%9F%B1/</link><guid isPermaLink="true">https://gilgameshzzz.github.io/posts/%E9%9D%A2%E5%90%91%E5%AF%B9%E8%B1%A1-oop-object-oriendted-programming-%E9%9D%A2%E5%90%91%E5%AF%B9%E8%B1%A1%E7%9A%84%E5%9B%9B%E5%A4%A7%E6%94%AF%E6%9F%B1/</guid><description>面向对象 -OOP(Object Oriendted Programming) 面向对象的四大支柱：</description><pubDate>Thu, 28 Feb 2019 00:00:00 GMT</pubDate><content:encoded>&lt;p&gt;面向对象 -OOP(Object Oriendted Programming)
面向对象的四大支柱：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;抽象：类是抽象的，对象是具体的，定义类的过程就是一个抽象过程，  需要最&lt;/p&gt;
&lt;p&gt;数据抽象(发现静态特征（属性）)和行为抽象（发现动态特征（方法））&lt;/p&gt;
&lt;/li&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;li&gt;
&lt;p&gt;多态：子类在继承父类的过程中可以重写（override）父类已有的方法
不同的子类可以给出不同放任实现版本，那么在调用该方法时会表现出多态&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;面向对象七原则：
1.单一职责原则 -SRP- 一个类只该做的事情
2.开闭原则 - 软件实体应该对扩展开放，对修改关闭
3.依赖倒转原则
4.里氏替换原则 - 任何可以用子类对象替换父类对象
5.接口隔离原则
6.合成聚合复用原则 - 优先使用强关联关系而不是继承关系复用代码
7.最少知识原则（迪米特法则） - 不要给没有必然联系的对象发消息&lt;/p&gt;
&lt;p&gt;GoF设计模式 -23种场景：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;

from abc import ABCMeta,abstractmethod

# 元类 - 描述类
# 通过 metaclass=ABCmeta可以将一个类声明为抽象类
# 通过abstractment装饰器可以将方法装饰成抽象方法
# 抽象类不能实例化
class  Employee(object, metaclass=ABCMeta):
    &quot;&quot;&quot;员工  &quot;&quot;&quot;
    def __init__(self, name):
        self.name = name

    @abstractmethod
    def __salary__(self):
        pass

class Manager(Employee):
    &quot;&quot;&quot;部门经理&quot;&quot;&quot;
    
    # 重写父类的抽象方法（如果没有重写抽象方法，也不能实例化）
    # 不同的子类都会重写这个抽象方法所以这个，所以这个方法是现了多态
    def get_salary(self):
        return 15000

class Programmer(Employee):
    &quot;&quot;&quot;程序员&quot;&quot;&quot;
    def __init__(self, name):
        super ().__init__(name)
        self.working_hour = 0

    def get_salary(self):
        return 200 * self.working_hour

class Saleman(Employee):
    &quot;&quot;&quot;docstring for Saleman&quot;&quot;&quot;
    def __init__(self, name):
        super().__init__(name)
        self.sale = 0

    def get_salary(self):
        return 0.05 * self.sale + 1800
        

def main():
    &quot;&quot;&quot;主函数&quot;&quot;&quot;
    emps = [Manager(&apos;曹操&apos;), Programmer(&apos;司马懿&apos;),
    Saleman(&apos;郭嘉&apos;)
    ]
    for emp in emps:
        # 通过isinstance函数可以进行类型识别
        if isinstance(emp,Programmer):
            hour = int(input(f&apos;{emp.name}本月工作时间：&apos;)) 
            emp.working_hour =hour
        elif isinstance(emp,Saleman):
            sale = float(input(f&apos;{emp.name}本月销售额：&apos;))
            emp.sale = sale
        print(f&apos;{emp.name}本月工资：{emp.get_salary()}元&apos;)

if __name__ == &apos;__main__&apos;:
    main()
&lt;/code&gt;&lt;/pre&gt;
</content:encoded></item></channel></rss>