<?xml version="1.0" encoding="UTF-8"?><rss version="2.0" xmlns:content="http://purl.org/rss/1.0/modules/content/"><channel><title>veyliss | Blog</title><description>记录技术学习、工程实践与长期思考</description><link>https://blog.veyliss.top/</link><language>zh-CN</language><item><title>最近一段时间太累了......</title><link>https://blog.veyliss.top/blog/april_20_notes_2026/</link><guid isPermaLink="true">https://blog.veyliss.top/blog/april_20_notes_2026/</guid><pubDate>Mon, 20 Jul 2026 00:00:00 GMT</pubDate><content:encoded>&lt;div&gt;&lt;h2 id=&quot;背景&quot;&gt;背景&lt;/h2&gt;&lt;/div&gt;
&lt;p&gt;这是一篇记录3月份到四月中旬下旬的经历感受。&lt;/p&gt;
&lt;p&gt;这段时间的经历和感受主要是公益站和注册机。这些日子大家都在薅奥特曼的羊毛，
我还寻思着这么多都公益站的账号都是哪里来的。于是在机缘巧合下寻找到了一个能用的注册机。
从部署new api 到 CPA 再到注册机，既是开始也是噩梦降临。&lt;/p&gt;
&lt;div&gt;&lt;h2 id=&quot;薅羊毛的人&quot;&gt;薅羊毛的人&lt;/h2&gt;&lt;/div&gt;
&lt;p&gt;这段时间总是会有第三方中转站的商家把一些过时的漏洞爆出来掀桌子，导致很多人都来薅羊毛，薅的人多了渠道就会不稳定，
这样一来，没有掌握渠道的商家就会被迫去找新的渠道，或者自己去找漏洞。于是就会有不同的渠道，例如 代充、注册机等。&lt;/p&gt;
&lt;div&gt;&lt;h2 id=&quot;薅的羊毛用做公益站&quot;&gt;薅的羊毛用做公益站&lt;/h2&gt;&lt;/div&gt;
&lt;p&gt;这段时间在L站，很多大佬都在薅羊毛，薅来的羊毛都用在用一张，让更多人认识到了AI模型。特别是御三家，这三个模型也是公认的最强模型。&lt;/p&gt;
&lt;div&gt;&lt;h2 id=&quot;一直需要修复的注册机&quot;&gt;一直需要修复的注册机&lt;/h2&gt;&lt;/div&gt;
&lt;p&gt;注册机可以说是利用bug来批量注册账号，用来薅free账号的额度，最开始我也就只注册一些账号，几十个差不多就够用了。但是那些滥用的人一次性注册
几万甚至几十万。这时候我就已经知道了，这个渠道最后会被封掉。大家会变成奥特曼的免费质检机器和测试员。最开始的渠道可以存活很久，几周，一个月
我最早的账号存活了一个多月，最短存活的账号只有几个小时。注册机也是，最开始的验证没有多少，后续注册账号越来越严格，风控也越敏感。最后还出现了连坐
机制和特征识别。只会封的月来越多。一开始一周或者好几天才会更新一次，到后面，一天更新一次，注册机一直都在改的路上…&lt;/p&gt;
&lt;div&gt;&lt;h2 id=&quot;公益站的尽头是付费&quot;&gt;公益站的尽头是付费&lt;/h2&gt;&lt;/div&gt;
&lt;p&gt;公益站不能说他们的做法是对的，也不能说是错的。在某种意义上还是说给奥特曼做宣传了。大家也都知道，免费的也就是图一乐，它并不稳定，速度也不快 。
安全性也难以保证。所以，公益站的尽头是付费&lt;/p&gt;
&lt;div&gt;&lt;h2 id=&quot;中转站需要提高安全意识&quot;&gt;中转站需要提高安全意识&lt;/h2&gt;&lt;/div&gt;
&lt;p&gt;在任何中转站或者模型的chat页面中，需要时刻保持数据脱敏。在很多中转站中，都存在着数据泄漏的风险，后台是可以直接保存上下文日志的。
甚至在这些中转站中出现了非常恶心且没有职业道德的行为，直接把用户的上下文数据拿去卖钱。&lt;/p&gt;
&lt;div&gt;&lt;h2 id=&quot;结语&quot;&gt;结语&lt;/h2&gt;&lt;/div&gt;
&lt;p&gt;这段时间虽然很累，但是也有收获。压力很大，但是也动力了我去学习更多的知识。
也希望大家分享在使用这些中转站或者模型的时候，能够提高安全意识，保护好自己的数据.&lt;/p&gt;</content:encoded><category>博客</category><category>随笔</category></item><item><title>全程 Vibe Coding 做了一个股票分析系统，我的一些技巧</title><link>https://blog.veyliss.top/blog/vibe_coding_stock_analysis_system_2026_07/</link><guid isPermaLink="true">https://blog.veyliss.top/blog/vibe_coding_stock_analysis_system_2026_07/</guid><pubDate>Wed, 01 Jul 2026 00:00:00 GMT</pubDate><content:encoded>&lt;p&gt;最近全程 Vibe Coding 了一个股票分析系统。&lt;/p&gt;
&lt;p&gt;第一版大概用了 15 个小时。&lt;/p&gt;
&lt;p&gt;比较有意思的是，这 15 个小时里，大部分实现都不是我一行一行写出来的，而是 Codex 按照我给的计划自己往前推进。&lt;/p&gt;
&lt;p&gt;我做的更多是给方向、补充约束、看结果、提调整意见。&lt;/p&gt;
&lt;p&gt;这次体验让我对 Vibe Coding 又有了一些新的感受。&lt;/p&gt;
&lt;p&gt;以前说 AI 写代码快，更多是在说“它能帮我补代码、修 bug、写页面”。&lt;/p&gt;
&lt;p&gt;但这一次，我更明显地感觉到：如果前置计划足够清楚，Codex 真的可以像一个执行力很强的工程协作者，把一个项目从 0 推到第一版可用。&lt;/p&gt;
&lt;p&gt;当然，这里说的是股票分析系统的工程实现。&lt;/p&gt;
&lt;p&gt;它不是投资建议系统，也不代表生成出来的分析结果就可以直接用于决策。&lt;/p&gt;
&lt;p&gt;我关心的重点，是这次 Vibe Coding 的工程过程。&lt;/p&gt;
&lt;div&gt;&lt;h2 id=&quot;不要只给一句需求&quot;&gt;不要只给一句需求&lt;/h2&gt;&lt;/div&gt;
&lt;p&gt;很多人用 AI 做项目，第一句话可能是：&lt;/p&gt;
&lt;p&gt;帮我做一个股票分析系统。&lt;/p&gt;
&lt;p&gt;这句话当然可以开始，但它太空了。&lt;/p&gt;
&lt;p&gt;AI 会做，但它只能根据自己的理解去猜。&lt;/p&gt;
&lt;p&gt;它会猜页面长什么样，猜后端接口怎么设计，猜数据怎么组织，猜你想要哪些分析指标，猜要不要登录，猜要不要缓存，猜要不要图表。&lt;/p&gt;
&lt;p&gt;猜得多了，项目就容易偏。&lt;/p&gt;
&lt;p&gt;这次我没有让 Codex 猜。&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;ul&gt;
&lt;li&gt;这个系统先解决什么问题&lt;/li&gt;
&lt;li&gt;第一版做到什么程度&lt;/li&gt;
&lt;li&gt;哪些功能必须有&lt;/li&gt;
&lt;li&gt;哪些功能可以后面再补&lt;/li&gt;
&lt;li&gt;前端有哪些页面&lt;/li&gt;
&lt;li&gt;后端有哪些接口&lt;/li&gt;
&lt;li&gt;数据怎么流转&lt;/li&gt;
&lt;li&gt;什么情况下算跑通&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;这一步非常关键。&lt;/p&gt;
&lt;p&gt;因为 AI 的执行力很强，但它需要一个明确的方向。&lt;/p&gt;
&lt;p&gt;方向越模糊，它越容易“看起来很努力”，但做出来不是你想要的东西。&lt;/p&gt;
&lt;div&gt;&lt;h2 id=&quot;把-ai-当执行团队而不是许愿机&quot;&gt;把 AI 当执行团队，而不是许愿机&lt;/h2&gt;&lt;/div&gt;
&lt;p&gt;这次我最大的体会是：Vibe Coding 不是许愿。&lt;/p&gt;
&lt;p&gt;不是我说一句“我要股票分析系统”，然后等它变出一个完整产品。&lt;/p&gt;
&lt;p&gt;更好的心态是，把 Codex 当成一个执行团队。&lt;/p&gt;
&lt;p&gt;我负责产品判断和架构方案。&lt;/p&gt;
&lt;p&gt;它负责大量实现、联调、修复和补齐细节。&lt;/p&gt;
&lt;p&gt;这两件事不能混在一起。&lt;/p&gt;
&lt;p&gt;如果我把方向也完全交给 AI，它可能会做得很快，但最后会越来越像“它理解中的产品”。&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;你也不会每一行代码都盯着他写。&lt;/p&gt;
&lt;p&gt;你会说清楚目标、约束、优先级和验收标准，然后让他推进。&lt;/p&gt;
&lt;p&gt;Codex 也是这样。&lt;/p&gt;
&lt;div&gt;&lt;h2 id=&quot;计划要写成可执行任务&quot;&gt;计划要写成可执行任务&lt;/h2&gt;&lt;/div&gt;
&lt;p&gt;我现在越来越觉得，给 AI 的计划不能只是“想法列表”。&lt;/p&gt;
&lt;p&gt;想法列表对人有用，但对 AI 来说还不够。&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;ol&gt;
&lt;li&gt;先搭建项目结构，确认前后端能启动。&lt;/li&gt;
&lt;li&gt;再实现股票基础数据模型和接口。&lt;/li&gt;
&lt;li&gt;再做股票列表页面。&lt;/li&gt;
&lt;li&gt;再做股票详情页。&lt;/li&gt;
&lt;li&gt;再接入分析指标。&lt;/li&gt;
&lt;li&gt;再补图表展示。&lt;/li&gt;
&lt;li&gt;最后跑构建，修复类型和样式问题。&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;这样 Codex 就知道先后顺序。&lt;/p&gt;
&lt;p&gt;它不会一上来就纠结一个复杂图表，也不会在后端模型没定的时候先写一堆页面。&lt;/p&gt;
&lt;p&gt;任务顺序越清楚，它越能自主推进。&lt;/p&gt;
&lt;p&gt;这次第一版能比较顺地跑下来，很大一部分原因就是计划里已经把路径铺好了。&lt;/p&gt;
&lt;p&gt;AI 不是不知道怎么做。&lt;/p&gt;
&lt;p&gt;它最怕的是不知道先做什么，做到哪里停，什么叫完成。&lt;/p&gt;
&lt;div&gt;&lt;h2 id=&quot;先让系统跑起来再追求漂亮&quot;&gt;先让系统跑起来，再追求漂亮&lt;/h2&gt;&lt;/div&gt;
&lt;p&gt;股票分析系统很容易一开始就想做复杂。&lt;/p&gt;
&lt;p&gt;各种指标、K 线、财务数据、新闻情绪、行业对比、AI 解读、风险提示。&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;再有详情页。&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;闭环跑通以后，再优化体验、样式、指标和数据质量。&lt;/p&gt;
&lt;p&gt;这对 Vibe Coding 尤其重要。&lt;/p&gt;
&lt;p&gt;因为 AI 很擅长在已有结构上继续补东西。&lt;/p&gt;
&lt;p&gt;但如果一开始结构就混乱，它补得越快，后面整理成本越高。&lt;/p&gt;
&lt;p&gt;所以我更愿意先让它完成一个清晰的小闭环，再逐步加功能。&lt;/p&gt;
&lt;div&gt;&lt;h2 id=&quot;让-codex-自己跑命令和修问题&quot;&gt;让 Codex 自己跑命令和修问题&lt;/h2&gt;&lt;/div&gt;
&lt;p&gt;这次我没有只让 Codex 写代码。&lt;/p&gt;
&lt;p&gt;我让它自己读项目、改文件、跑构建、看报错、继续修。&lt;/p&gt;
&lt;p&gt;这是 Vibe Coding 和普通聊天式写代码很不一样的地方。&lt;/p&gt;
&lt;p&gt;以前用 AI，很多时候是它给一段代码，我复制过去，然后自己跑。&lt;/p&gt;
&lt;p&gt;报错了，再贴错误回来。&lt;/p&gt;
&lt;p&gt;这个过程其实很割裂。&lt;/p&gt;
&lt;p&gt;而 Codex 可以在项目里直接工作，它能看到文件结构，也能运行命令。&lt;/p&gt;
&lt;p&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;li&gt;再根据错误修复&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;p&gt;比如它做得太复杂，我让它收回来。&lt;/p&gt;
&lt;p&gt;比如页面气质不对，我重新描述目标。&lt;/p&gt;
&lt;p&gt;其他重复的调试工作，就交给它自己处理。&lt;/p&gt;
&lt;p&gt;这会省很多精力。&lt;/p&gt;
&lt;div&gt;&lt;h2 id=&quot;验收标准要具体&quot;&gt;验收标准要具体&lt;/h2&gt;&lt;/div&gt;
&lt;p&gt;Vibe Coding 里，人最重要的工作之一是验收。&lt;/p&gt;
&lt;p&gt;不是 AI 写完就结束了。&lt;/p&gt;
&lt;p&gt;尤其是股票分析系统这种项目，表面看起来能跑，不代表真的对。&lt;/p&gt;
&lt;p&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;li&gt;空数据和异常状态有没有处理&lt;/li&gt;
&lt;li&gt;构建是否通过&lt;/li&gt;
&lt;li&gt;图表是否遮挡&lt;/li&gt;
&lt;li&gt;分析文案是否过度自信&lt;/li&gt;
&lt;li&gt;有没有把展示工具写成投资建议&lt;/li&gt;
&lt;/ul&gt;
&lt;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;
&lt;p&gt;AI 可以帮你实现功能，但什么东西该不该这样表达，还是要人判断。&lt;/p&gt;
&lt;div&gt;&lt;h2 id=&quot;提示词不如上下文重要&quot;&gt;提示词不如上下文重要&lt;/h2&gt;&lt;/div&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;
&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;
&lt;p&gt;所以我现在更重视前置文档。&lt;/p&gt;
&lt;p&gt;把项目目标、模块边界、接口约定、数据模型、优先级写清楚。&lt;/p&gt;
&lt;p&gt;这些内容既是给人看的，也是给 AI 看的。&lt;/p&gt;
&lt;p&gt;文档越清楚，AI 越像一个熟悉项目的协作者。&lt;/p&gt;
&lt;p&gt;文档越模糊，AI 越像一个手很快但刚进组的人。&lt;/p&gt;
&lt;div&gt;&lt;h2 id=&quot;人的角色变了&quot;&gt;人的角色变了&lt;/h2&gt;&lt;/div&gt;
&lt;p&gt;这次 15 个小时做出第一版，我并不觉得自己“没参与”。&lt;/p&gt;
&lt;p&gt;只是参与方式变了。&lt;/p&gt;
&lt;p&gt;以前我更多是在写代码细节里消耗时间。&lt;/p&gt;
&lt;p&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;li&gt;判断取舍&lt;/li&gt;
&lt;li&gt;验收结果&lt;/li&gt;
&lt;li&gt;修正方向&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;这其实更像产品负责人加架构师的角色。&lt;/p&gt;
&lt;p&gt;AI 把大量执行工作接走了，但人的判断没有消失。&lt;/p&gt;
&lt;p&gt;甚至更重要了。&lt;/p&gt;
&lt;p&gt;因为执行速度越快，方向错了的代价也越大。&lt;/p&gt;
&lt;p&gt;以前方向错了，可能写两小时才发现。&lt;/p&gt;
&lt;p&gt;现在方向错了，AI 可能很快帮你实现一大片。&lt;/p&gt;
&lt;p&gt;所以 Vibe Coding 不是让人不用思考。&lt;/p&gt;
&lt;p&gt;它是把人的思考从“怎么写这一段代码”，往上推到了“这个系统应该怎么长出来”。&lt;/p&gt;
&lt;div&gt;&lt;h2 id=&quot;我这次最大的收获&quot;&gt;我这次最大的收获&lt;/h2&gt;&lt;/div&gt;
&lt;p&gt;我觉得最重要的一句话是：计划越清楚，AI 的自主执行能力越强。&lt;/p&gt;
&lt;p&gt;Codex 这次能在 15 个小时左右把第一版跑通，不只是因为它代码能力强。&lt;/p&gt;
&lt;p&gt;更重要的是，我给了一个足够具体的计划。&lt;/p&gt;
&lt;p&gt;它知道要做什么，知道先做什么，知道做到什么程度算完成，也知道哪些地方不要越界。&lt;/p&gt;
&lt;p&gt;Vibe Coding 的技巧，不是写一句神奇提示词。&lt;/p&gt;
&lt;p&gt;而是把你的想法整理成 AI 可以执行的工程上下文。&lt;/p&gt;
&lt;p&gt;你给它一个模糊愿望，它会给你一个模糊结果。&lt;/p&gt;
&lt;p&gt;你给它一个清晰方案，它就能变成一个很强的执行者。&lt;/p&gt;
&lt;p&gt;这也是我现在越来越喜欢 Vibe Coding 的原因。&lt;/p&gt;
&lt;p&gt;它不是替我做决定。&lt;/p&gt;
&lt;p&gt;它是在我把决定说清楚以后，能够将任务推进到验收版本。&lt;/p&gt;</content:encoded><category>博客</category><category>AI</category><category>随笔</category><category>工具</category></item><item><title>聊聊我对 MCP 的一点理解</title><link>https://blog.veyliss.top/blog/mcp_understanding_2026_06/</link><guid isPermaLink="true">https://blog.veyliss.top/blog/mcp_understanding_2026_06/</guid><pubDate>Thu, 18 Jun 2026 00:00:00 GMT</pubDate><content:encoded>&lt;div&gt;&lt;h2 id=&quot;什么是mcp&quot;&gt;什么是MCP&lt;/h2&gt;&lt;/div&gt;
&lt;p&gt;全称模型上下文协议（Model Context Protocol）。这是由 Anthropic 推出的一项开放标准，目标是为大型语言模型和 AI 助手提供一个统一、标准化的接口，使 AI 能够轻松操作外部工具并完成更复杂的任务。&lt;/p&gt;
&lt;p&gt;通过使用 MCP，Claude 或 ChatGPT 等 AI 应用程序可以连接到数据源（如本地文件、数据库）、工具（如搜索引擎、计算器）和工作流（如专门的提示词），从而使它们能够获取关键信息并执行任务。
可以将 MCP 想象成 AI 应用程序的 USB-C 接口。正如 USB-C 为连接电子设备提供了一种标准化方式一样，MCP 也为连接 AI 应用程序和外部系统提供了一种标准化方式。&lt;/p&gt;
&lt;div&gt;&lt;h2 id=&quot;讲个例子&quot;&gt;讲个例子&lt;/h2&gt;&lt;/div&gt;
&lt;p&gt;就拿我开发的 &lt;a href=&quot;https://github.com/veyliss/ai-localbase&quot;&gt;AI LocalBase&lt;/a&gt; 来说吧。AI LocalBase 是一个本地化的知识库和数据管理工具，它可以帮助用户在本地存储和管理各种数据，并通过 AI 助手进行智能查询和操作。
虽然是本地化，但它也可以支持 OpenAPI 和 MCP 协议。通过 MCP 协议，AI LocalBase 可以和 Claude、Cursor、Codex 这类 AI 应用连接起来，使用户能够通过自然语言与本地知识库进行交互。&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://blog.veyliss.top/diagrams/mcp-protocol-architecture.svg&quot; alt=&quot;MCP 协议连接层架构示意图&quot;&gt;&lt;/p&gt;
&lt;div&gt;&lt;h2 id=&quot;mcp&quot;&gt;MCP&lt;/h2&gt;&lt;/div&gt;
&lt;p&gt;放到 AI LocalBase 这个项目里看，MCP 其实没有那么玄。&lt;/p&gt;
&lt;p&gt;本质上，它还是启动一个后端服务，然后暴露一个符合 MCP 协议的接口，给其他 AI 应用看。&lt;/p&gt;
&lt;p&gt;只不过这个接口不是普通业务接口。&lt;/p&gt;
&lt;p&gt;普通 HTTP API 更多是给前端页面、脚本或者其他服务调用的。调用方需要提前知道接口路径、参数格式和业务含义。&lt;/p&gt;
&lt;p&gt;MCP 接口面对的是 AI 应用。它要让对方能够先发现能力，再决定怎么调用。&lt;/p&gt;
&lt;p&gt;比如 AI LocalBase 后端启动后，可以暴露一个 MCP 入口。Claude、Cursor、Codex 这类工具作为 MCP Client 连进来之后，先问后端：&lt;/p&gt;
&lt;p&gt;你有哪些工具？&lt;/p&gt;
&lt;p&gt;你有哪些资源？&lt;/p&gt;
&lt;p&gt;你支持哪些操作？&lt;/p&gt;
&lt;p&gt;后端再把自己的能力按 MCP 的方式描述出去。&lt;/p&gt;
&lt;p&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;li&gt;可以生成带来源的回答&lt;/li&gt;
&lt;li&gt;可以查看索引状态&lt;/li&gt;
&lt;li&gt;在有权限时，也可以上传文档或重建索引&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;这样一来，AI 应用访问的就不是 AI LocalBase 的网页，而是 AI LocalBase 暴露出来的一组能力。&lt;/p&gt;
&lt;p&gt;架构上大概就是上面这张图表达的链路：AI 应用通过 MCP Client 连到 AI LocalBase 后端暴露的 MCP 入口，再由后端去访问知识库、文档、向量索引和检索服务。&lt;/p&gt;
&lt;p&gt;这个时候，AI LocalBase 就不只是一个可以打开的本地知识库工具了。&lt;/p&gt;
&lt;p&gt;它还变成了其他 AI 应用可以调用的知识库后端。&lt;/p&gt;
&lt;p&gt;这也是我理解 MCP 的一个关键点：它不是替代业务系统，而是让业务系统把自己的能力用一种标准方式暴露出去。&lt;/p&gt;
&lt;p&gt;真正有价值的地方，不是“我又多写了一个接口”，而是这个接口背后有一套能力发现、工具描述、权限控制和调用返回的协议约定。&lt;/p&gt;
&lt;p&gt;所以 MCP Server 不是把所有内部接口原样丢给 AI。&lt;/p&gt;
&lt;p&gt;更好的做法是站在 AI 应用的视角重新整理能力。&lt;/p&gt;
&lt;p&gt;比如对外暴露 &lt;code dir=&quot;auto&quot;&gt;search_knowledge_base&lt;/code&gt;，而不是暴露一堆底层查询接口。&lt;/p&gt;
&lt;p&gt;比如返回检索结果时，不只返回文本，还要返回来源、置信度、文档 ID、chunk 信息，方便 AI 继续组织答案。&lt;/p&gt;
&lt;p&gt;比如写入、删除、重建索引这种操作，就不能和普通查询放在同一个权限层级里。&lt;/p&gt;
&lt;p&gt;说白了，MCP 的工程价值在于：让后端服务多了一层面向 AI 应用的能力出口。&lt;/p&gt;
&lt;div&gt;&lt;h2 id=&quot;代码大概长什么样&quot;&gt;代码大概长什么样&lt;/h2&gt;&lt;/div&gt;
&lt;p&gt;如果用代码表达，它其实可以很朴素。&lt;/p&gt;
&lt;p&gt;下面这些不是完整实现，更像是把核心链路拆开看：后端启动时挂一个 &lt;code dir=&quot;auto&quot;&gt;/mcp&lt;/code&gt; 入口，然后在这个入口里处理 MCP 的初始化、工具发现和工具调用。&lt;/p&gt;
&lt;p&gt;比如在 Go + Gin 后端里，可以大概这样挂路由：&lt;/p&gt;
&lt;div&gt;&lt;figure&gt;&lt;figcaption&gt;&lt;/figcaption&gt;&lt;pre&gt;&lt;code&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;func&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;RegisterRoutes&lt;/span&gt;&lt;span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;r&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;/span&gt;&lt;span&gt;*&lt;/span&gt;&lt;span&gt;&lt;span&gt;gin.Engine, &lt;/span&gt;&lt;span&gt;cfg&lt;/span&gt;&lt;span&gt; Config, &lt;/span&gt;&lt;span&gt;deps&lt;/span&gt;&lt;span&gt; Dependencies) {&lt;/span&gt;&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;  &lt;/span&gt;&lt;span&gt;api&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;:=&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;r&lt;/span&gt;&lt;span&gt;.&lt;/span&gt;&lt;span&gt;Group&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;&quot;&lt;/span&gt;&lt;span&gt;/api&lt;/span&gt;&lt;span&gt;&quot;&lt;/span&gt;&lt;span&gt;)&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;
&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;  &lt;/span&gt;&lt;span&gt;if&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;cfg&lt;/span&gt;&lt;span&gt;.&lt;/span&gt;&lt;span&gt;MCP&lt;/span&gt;&lt;span&gt;.&lt;/span&gt;&lt;span&gt;Enabled&lt;/span&gt;&lt;span&gt; {&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;    &lt;/span&gt;&lt;span&gt;mcp&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;:=&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;NewMCPHandler&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;deps&lt;/span&gt;&lt;span&gt;.&lt;/span&gt;&lt;span&gt;KnowledgeBaseService&lt;/span&gt;&lt;span&gt;, &lt;/span&gt;&lt;span&gt;deps&lt;/span&gt;&lt;span&gt;.&lt;/span&gt;&lt;span&gt;SearchService&lt;/span&gt;&lt;span&gt;)&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;
&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;    &lt;/span&gt;&lt;span&gt;api&lt;/span&gt;&lt;span&gt;.&lt;/span&gt;&lt;span&gt;POST&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;      &lt;/span&gt;&lt;span&gt;&quot;&lt;/span&gt;&lt;span&gt;/mcp&lt;/span&gt;&lt;span&gt;&quot;&lt;/span&gt;&lt;span&gt;,&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;      &lt;/span&gt;&lt;span&gt;RequireAPIKey&lt;/span&gt;&lt;span&gt;(),&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;      &lt;/span&gt;&lt;span&gt;RequireScope&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;&quot;&lt;/span&gt;&lt;span&gt;mcp:read&lt;/span&gt;&lt;span&gt;&quot;&lt;/span&gt;&lt;span&gt;),&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;      &lt;/span&gt;&lt;span&gt;mcp&lt;/span&gt;&lt;span&gt;.&lt;/span&gt;&lt;span&gt;Handle&lt;/span&gt;&lt;span&gt;,&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;&lt;span&gt;    &lt;/span&gt;&lt;/span&gt;&lt;span&gt;)&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;&lt;span&gt;  &lt;/span&gt;&lt;/span&gt;&lt;span&gt;}&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;}&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/figure&gt;&lt;/div&gt;
&lt;p&gt;这里的重点不是 &lt;code dir=&quot;auto&quot;&gt;/api/mcp&lt;/code&gt; 这个路径本身。&lt;/p&gt;
&lt;p&gt;重点是：AI LocalBase 后端启动以后，多暴露了一个 MCP 协议入口。其他 AI 应用不是直接访问页面，而是通过这个入口发现和调用后端能力。&lt;/p&gt;
&lt;p&gt;MCP 请求底层可以理解成一类 JSON-RPC 消息。后端收到请求以后，根据 &lt;code dir=&quot;auto&quot;&gt;method&lt;/code&gt; 分发到不同处理逻辑。&lt;/p&gt;
&lt;div&gt;&lt;figure&gt;&lt;figcaption&gt;&lt;/figcaption&gt;&lt;pre&gt;&lt;code&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;type&lt;/span&gt;&lt;span&gt; MCPRequest &lt;/span&gt;&lt;span&gt;struct&lt;/span&gt;&lt;span&gt; {&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;  &lt;/span&gt;&lt;span&gt;JSONRPC&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;string&lt;/span&gt;&lt;span&gt;          &lt;/span&gt;&lt;span&gt;`&lt;/span&gt;&lt;span&gt;json:&quot;jsonrpc&quot;&lt;/span&gt;&lt;span&gt;`&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;  &lt;/span&gt;&lt;span&gt;ID&lt;/span&gt;&lt;span&gt;      any             &lt;/span&gt;&lt;span&gt;`&lt;/span&gt;&lt;span&gt;json:&quot;id,omitempty&quot;&lt;/span&gt;&lt;span&gt;`&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;  &lt;/span&gt;&lt;span&gt;Method&lt;/span&gt;&lt;span&gt;  &lt;/span&gt;&lt;span&gt;string&lt;/span&gt;&lt;span&gt;          &lt;/span&gt;&lt;span&gt;`&lt;/span&gt;&lt;span&gt;json:&quot;method&quot;&lt;/span&gt;&lt;span&gt;`&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;  &lt;/span&gt;&lt;span&gt;Params&lt;/span&gt;&lt;span&gt;  json.RawMessage &lt;/span&gt;&lt;span&gt;`&lt;/span&gt;&lt;span&gt;json:&quot;params,omitempty&quot;&lt;/span&gt;&lt;span&gt;`&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;}&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;
&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;func&lt;/span&gt;&lt;span&gt;&lt;span&gt; (&lt;/span&gt;&lt;span&gt;h &lt;/span&gt;&lt;/span&gt;&lt;span&gt;*&lt;/span&gt;&lt;span&gt;MCPHandler) &lt;/span&gt;&lt;span&gt;Handle&lt;/span&gt;&lt;span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;c&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;/span&gt;&lt;span&gt;*&lt;/span&gt;&lt;span&gt;gin.Context) {&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;  &lt;/span&gt;&lt;span&gt;var&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;req&lt;/span&gt;&lt;span&gt; MCPRequest&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;  &lt;/span&gt;&lt;span&gt;if&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;err&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;:=&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;c&lt;/span&gt;&lt;span&gt;.&lt;/span&gt;&lt;span&gt;ShouldBindJSON&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;&amp;#x26;&lt;/span&gt;&lt;span&gt;req&lt;/span&gt;&lt;span&gt;); &lt;/span&gt;&lt;span&gt;err&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;!=&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;nil&lt;/span&gt;&lt;span&gt; {&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;    &lt;/span&gt;&lt;span&gt;c&lt;/span&gt;&lt;span&gt;.&lt;/span&gt;&lt;span&gt;JSON&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;http&lt;/span&gt;&lt;span&gt;.&lt;/span&gt;&lt;span&gt;StatusBadRequest&lt;/span&gt;&lt;span&gt;, &lt;/span&gt;&lt;span&gt;rpcError&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;req&lt;/span&gt;&lt;span&gt;.&lt;/span&gt;&lt;span&gt;ID&lt;/span&gt;&lt;span&gt;, &lt;/span&gt;&lt;span&gt;-&lt;/span&gt;&lt;span&gt;32700&lt;/span&gt;&lt;span&gt;, &lt;/span&gt;&lt;span&gt;&quot;&lt;/span&gt;&lt;span&gt;invalid json&lt;/span&gt;&lt;span&gt;&quot;&lt;/span&gt;&lt;span&gt;))&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;    &lt;/span&gt;&lt;span&gt;return&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;&lt;span&gt;  &lt;/span&gt;&lt;/span&gt;&lt;span&gt;}&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;
&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;  &lt;/span&gt;&lt;span&gt;switch&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;req&lt;/span&gt;&lt;span&gt;.&lt;/span&gt;&lt;span&gt;Method&lt;/span&gt;&lt;span&gt; {&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;  &lt;/span&gt;&lt;span&gt;case&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;&quot;&lt;/span&gt;&lt;span&gt;initialize&lt;/span&gt;&lt;span&gt;&quot;&lt;/span&gt;&lt;span&gt;:&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;    &lt;/span&gt;&lt;span&gt;c&lt;/span&gt;&lt;span&gt;.&lt;/span&gt;&lt;span&gt;JSON&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;http&lt;/span&gt;&lt;span&gt;.&lt;/span&gt;&lt;span&gt;StatusOK&lt;/span&gt;&lt;span&gt;, &lt;/span&gt;&lt;span&gt;h&lt;/span&gt;&lt;span&gt;.&lt;/span&gt;&lt;span&gt;initialize&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;req&lt;/span&gt;&lt;span&gt;.&lt;/span&gt;&lt;span&gt;ID&lt;/span&gt;&lt;span&gt;))&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;  &lt;/span&gt;&lt;span&gt;case&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;&quot;&lt;/span&gt;&lt;span&gt;tools/list&lt;/span&gt;&lt;span&gt;&quot;&lt;/span&gt;&lt;span&gt;:&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;    &lt;/span&gt;&lt;span&gt;c&lt;/span&gt;&lt;span&gt;.&lt;/span&gt;&lt;span&gt;JSON&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;http&lt;/span&gt;&lt;span&gt;.&lt;/span&gt;&lt;span&gt;StatusOK&lt;/span&gt;&lt;span&gt;, &lt;/span&gt;&lt;span&gt;h&lt;/span&gt;&lt;span&gt;.&lt;/span&gt;&lt;span&gt;listTools&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;req&lt;/span&gt;&lt;span&gt;.&lt;/span&gt;&lt;span&gt;ID&lt;/span&gt;&lt;span&gt;))&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;  &lt;/span&gt;&lt;span&gt;case&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;&quot;&lt;/span&gt;&lt;span&gt;tools/call&lt;/span&gt;&lt;span&gt;&quot;&lt;/span&gt;&lt;span&gt;:&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;    &lt;/span&gt;&lt;span&gt;c&lt;/span&gt;&lt;span&gt;.&lt;/span&gt;&lt;span&gt;JSON&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;http&lt;/span&gt;&lt;span&gt;.&lt;/span&gt;&lt;span&gt;StatusOK&lt;/span&gt;&lt;span&gt;, &lt;/span&gt;&lt;span&gt;h&lt;/span&gt;&lt;span&gt;.&lt;/span&gt;&lt;span&gt;callTool&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;c&lt;/span&gt;&lt;span&gt;.&lt;/span&gt;&lt;span&gt;Request&lt;/span&gt;&lt;span&gt;.&lt;/span&gt;&lt;span&gt;Context&lt;/span&gt;&lt;span&gt;(), &lt;/span&gt;&lt;span&gt;req&lt;/span&gt;&lt;span&gt;.&lt;/span&gt;&lt;span&gt;ID&lt;/span&gt;&lt;span&gt;, &lt;/span&gt;&lt;span&gt;req&lt;/span&gt;&lt;span&gt;.&lt;/span&gt;&lt;span&gt;Params&lt;/span&gt;&lt;span&gt;))&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;  &lt;/span&gt;&lt;span&gt;default&lt;/span&gt;&lt;span&gt;:&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;    &lt;/span&gt;&lt;span&gt;c&lt;/span&gt;&lt;span&gt;.&lt;/span&gt;&lt;span&gt;JSON&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;http&lt;/span&gt;&lt;span&gt;.&lt;/span&gt;&lt;span&gt;StatusOK&lt;/span&gt;&lt;span&gt;, &lt;/span&gt;&lt;span&gt;rpcError&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;req&lt;/span&gt;&lt;span&gt;.&lt;/span&gt;&lt;span&gt;ID&lt;/span&gt;&lt;span&gt;, &lt;/span&gt;&lt;span&gt;-&lt;/span&gt;&lt;span&gt;32601&lt;/span&gt;&lt;span&gt;, &lt;/span&gt;&lt;span&gt;&quot;&lt;/span&gt;&lt;span&gt;method not found&lt;/span&gt;&lt;span&gt;&quot;&lt;/span&gt;&lt;span&gt;))&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;&lt;span&gt;  &lt;/span&gt;&lt;/span&gt;&lt;span&gt;}&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;}&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/figure&gt;&lt;/div&gt;
&lt;p&gt;&lt;code dir=&quot;auto&quot;&gt;tools/list&lt;/code&gt; 可以理解成 AI 应用进来以后先问一句：你这里有什么能力？&lt;/p&gt;
&lt;p&gt;后端返回的不是普通菜单，而是一组工具描述。&lt;/p&gt;
&lt;div&gt;&lt;figure&gt;&lt;figcaption&gt;&lt;/figcaption&gt;&lt;pre&gt;&lt;code&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;func&lt;/span&gt;&lt;span&gt;&lt;span&gt; (&lt;/span&gt;&lt;span&gt;h &lt;/span&gt;&lt;/span&gt;&lt;span&gt;*&lt;/span&gt;&lt;span&gt;MCPHandler) &lt;/span&gt;&lt;span&gt;listTools&lt;/span&gt;&lt;span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;id&lt;/span&gt;&lt;span&gt; any) MCPResponse {&lt;/span&gt;&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;  &lt;/span&gt;&lt;span&gt;return&lt;/span&gt;&lt;span&gt; MCPResponse{&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;    &lt;/span&gt;&lt;span&gt;JSONRPC&lt;/span&gt;&lt;span&gt;: &lt;/span&gt;&lt;span&gt;&quot;&lt;/span&gt;&lt;span&gt;2.0&lt;/span&gt;&lt;span&gt;&quot;&lt;/span&gt;&lt;span&gt;,&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;    &lt;/span&gt;&lt;span&gt;ID&lt;/span&gt;&lt;span&gt;:      &lt;/span&gt;&lt;span&gt;id&lt;/span&gt;&lt;span&gt;,&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;    &lt;/span&gt;&lt;span&gt;Result&lt;/span&gt;&lt;span&gt;: &lt;/span&gt;&lt;span&gt;map&lt;/span&gt;&lt;span&gt;[&lt;/span&gt;&lt;span&gt;string&lt;/span&gt;&lt;span&gt;]any{&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;      &lt;/span&gt;&lt;span&gt;&quot;&lt;/span&gt;&lt;span&gt;tools&lt;/span&gt;&lt;span&gt;&quot;&lt;/span&gt;&lt;span&gt;: []&lt;/span&gt;&lt;span&gt;map&lt;/span&gt;&lt;span&gt;[&lt;/span&gt;&lt;span&gt;string&lt;/span&gt;&lt;span&gt;]any{&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;&lt;span&gt;        &lt;/span&gt;&lt;/span&gt;&lt;span&gt;{&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;          &lt;/span&gt;&lt;span&gt;&quot;&lt;/span&gt;&lt;span&gt;name&lt;/span&gt;&lt;span&gt;&quot;&lt;/span&gt;&lt;span&gt;:        &lt;/span&gt;&lt;span&gt;&quot;&lt;/span&gt;&lt;span&gt;list_knowledge_bases&lt;/span&gt;&lt;span&gt;&quot;&lt;/span&gt;&lt;span&gt;,&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;          &lt;/span&gt;&lt;span&gt;&quot;&lt;/span&gt;&lt;span&gt;description&lt;/span&gt;&lt;span&gt;&quot;&lt;/span&gt;&lt;span&gt;: &lt;/span&gt;&lt;span&gt;&quot;&lt;/span&gt;&lt;span&gt;列出当前用户可以访问的知识库&lt;/span&gt;&lt;span&gt;&quot;&lt;/span&gt;&lt;span&gt;,&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;          &lt;/span&gt;&lt;span&gt;&quot;&lt;/span&gt;&lt;span&gt;inputSchema&lt;/span&gt;&lt;span&gt;&quot;&lt;/span&gt;&lt;span&gt;: &lt;/span&gt;&lt;span&gt;map&lt;/span&gt;&lt;span&gt;[&lt;/span&gt;&lt;span&gt;string&lt;/span&gt;&lt;span&gt;]any{&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;            &lt;/span&gt;&lt;span&gt;&quot;&lt;/span&gt;&lt;span&gt;type&lt;/span&gt;&lt;span&gt;&quot;&lt;/span&gt;&lt;span&gt;:       &lt;/span&gt;&lt;span&gt;&quot;&lt;/span&gt;&lt;span&gt;object&lt;/span&gt;&lt;span&gt;&quot;&lt;/span&gt;&lt;span&gt;,&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;            &lt;/span&gt;&lt;span&gt;&quot;&lt;/span&gt;&lt;span&gt;properties&lt;/span&gt;&lt;span&gt;&quot;&lt;/span&gt;&lt;span&gt;: &lt;/span&gt;&lt;span&gt;map&lt;/span&gt;&lt;span&gt;[&lt;/span&gt;&lt;span&gt;string&lt;/span&gt;&lt;span&gt;]any{},&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;&lt;span&gt;          &lt;/span&gt;&lt;/span&gt;&lt;span&gt;},&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;&lt;span&gt;        &lt;/span&gt;&lt;/span&gt;&lt;span&gt;},&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;&lt;span&gt;        &lt;/span&gt;&lt;/span&gt;&lt;span&gt;{&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;          &lt;/span&gt;&lt;span&gt;&quot;&lt;/span&gt;&lt;span&gt;name&lt;/span&gt;&lt;span&gt;&quot;&lt;/span&gt;&lt;span&gt;:        &lt;/span&gt;&lt;span&gt;&quot;&lt;/span&gt;&lt;span&gt;search_knowledge_base&lt;/span&gt;&lt;span&gt;&quot;&lt;/span&gt;&lt;span&gt;,&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;          &lt;/span&gt;&lt;span&gt;&quot;&lt;/span&gt;&lt;span&gt;description&lt;/span&gt;&lt;span&gt;&quot;&lt;/span&gt;&lt;span&gt;: &lt;/span&gt;&lt;span&gt;&quot;&lt;/span&gt;&lt;span&gt;在指定知识库中检索相关文档片段&lt;/span&gt;&lt;span&gt;&quot;&lt;/span&gt;&lt;span&gt;,&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;          &lt;/span&gt;&lt;span&gt;&quot;&lt;/span&gt;&lt;span&gt;inputSchema&lt;/span&gt;&lt;span&gt;&quot;&lt;/span&gt;&lt;span&gt;: &lt;/span&gt;&lt;span&gt;map&lt;/span&gt;&lt;span&gt;[&lt;/span&gt;&lt;span&gt;string&lt;/span&gt;&lt;span&gt;]any{&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;            &lt;/span&gt;&lt;span&gt;&quot;&lt;/span&gt;&lt;span&gt;type&lt;/span&gt;&lt;span&gt;&quot;&lt;/span&gt;&lt;span&gt;: &lt;/span&gt;&lt;span&gt;&quot;&lt;/span&gt;&lt;span&gt;object&lt;/span&gt;&lt;span&gt;&quot;&lt;/span&gt;&lt;span&gt;,&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;            &lt;/span&gt;&lt;span&gt;&quot;&lt;/span&gt;&lt;span&gt;properties&lt;/span&gt;&lt;span&gt;&quot;&lt;/span&gt;&lt;span&gt;: &lt;/span&gt;&lt;span&gt;map&lt;/span&gt;&lt;span&gt;[&lt;/span&gt;&lt;span&gt;string&lt;/span&gt;&lt;span&gt;]any{&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;              &lt;/span&gt;&lt;span&gt;&quot;&lt;/span&gt;&lt;span&gt;knowledgeBaseId&lt;/span&gt;&lt;span&gt;&quot;&lt;/span&gt;&lt;span&gt;: &lt;/span&gt;&lt;span&gt;map&lt;/span&gt;&lt;span&gt;[&lt;/span&gt;&lt;span&gt;string&lt;/span&gt;&lt;span&gt;]&lt;/span&gt;&lt;span&gt;string&lt;/span&gt;&lt;span&gt;{&lt;/span&gt;&lt;span&gt;&quot;&lt;/span&gt;&lt;span&gt;type&lt;/span&gt;&lt;span&gt;&quot;&lt;/span&gt;&lt;span&gt;: &lt;/span&gt;&lt;span&gt;&quot;&lt;/span&gt;&lt;span&gt;string&lt;/span&gt;&lt;span&gt;&quot;&lt;/span&gt;&lt;span&gt;},&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;              &lt;/span&gt;&lt;span&gt;&quot;&lt;/span&gt;&lt;span&gt;query&lt;/span&gt;&lt;span&gt;&quot;&lt;/span&gt;&lt;span&gt;:           &lt;/span&gt;&lt;span&gt;map&lt;/span&gt;&lt;span&gt;[&lt;/span&gt;&lt;span&gt;string&lt;/span&gt;&lt;span&gt;]&lt;/span&gt;&lt;span&gt;string&lt;/span&gt;&lt;span&gt;{&lt;/span&gt;&lt;span&gt;&quot;&lt;/span&gt;&lt;span&gt;type&lt;/span&gt;&lt;span&gt;&quot;&lt;/span&gt;&lt;span&gt;: &lt;/span&gt;&lt;span&gt;&quot;&lt;/span&gt;&lt;span&gt;string&lt;/span&gt;&lt;span&gt;&quot;&lt;/span&gt;&lt;span&gt;},&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;              &lt;/span&gt;&lt;span&gt;&quot;&lt;/span&gt;&lt;span&gt;topK&lt;/span&gt;&lt;span&gt;&quot;&lt;/span&gt;&lt;span&gt;:            &lt;/span&gt;&lt;span&gt;map&lt;/span&gt;&lt;span&gt;[&lt;/span&gt;&lt;span&gt;string&lt;/span&gt;&lt;span&gt;]any{&lt;/span&gt;&lt;span&gt;&quot;&lt;/span&gt;&lt;span&gt;type&lt;/span&gt;&lt;span&gt;&quot;&lt;/span&gt;&lt;span&gt;: &lt;/span&gt;&lt;span&gt;&quot;&lt;/span&gt;&lt;span&gt;integer&lt;/span&gt;&lt;span&gt;&quot;&lt;/span&gt;&lt;span&gt;, &lt;/span&gt;&lt;span&gt;&quot;&lt;/span&gt;&lt;span&gt;default&lt;/span&gt;&lt;span&gt;&quot;&lt;/span&gt;&lt;span&gt;: &lt;/span&gt;&lt;span&gt;5&lt;/span&gt;&lt;span&gt;},&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;&lt;span&gt;            &lt;/span&gt;&lt;/span&gt;&lt;span&gt;},&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;            &lt;/span&gt;&lt;span&gt;&quot;&lt;/span&gt;&lt;span&gt;required&lt;/span&gt;&lt;span&gt;&quot;&lt;/span&gt;&lt;span&gt;: []&lt;/span&gt;&lt;span&gt;string&lt;/span&gt;&lt;span&gt;{&lt;/span&gt;&lt;span&gt;&quot;&lt;/span&gt;&lt;span&gt;knowledgeBaseId&lt;/span&gt;&lt;span&gt;&quot;&lt;/span&gt;&lt;span&gt;, &lt;/span&gt;&lt;span&gt;&quot;&lt;/span&gt;&lt;span&gt;query&lt;/span&gt;&lt;span&gt;&quot;&lt;/span&gt;&lt;span&gt;},&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;&lt;span&gt;          &lt;/span&gt;&lt;/span&gt;&lt;span&gt;},&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;&lt;span&gt;        &lt;/span&gt;&lt;/span&gt;&lt;span&gt;},&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;&lt;span&gt;      &lt;/span&gt;&lt;/span&gt;&lt;span&gt;},&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;&lt;span&gt;    &lt;/span&gt;&lt;/span&gt;&lt;span&gt;},&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;&lt;span&gt;  &lt;/span&gt;&lt;/span&gt;&lt;span&gt;}&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;}&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/figure&gt;&lt;/div&gt;
&lt;p&gt;当 AI 应用决定调用 &lt;code dir=&quot;auto&quot;&gt;search_knowledge_base&lt;/code&gt; 时，后端再把这个工具调用转成内部业务服务调用。&lt;/p&gt;
&lt;div&gt;&lt;figure&gt;&lt;figcaption&gt;&lt;/figcaption&gt;&lt;pre&gt;&lt;code&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;func&lt;/span&gt;&lt;span&gt;&lt;span&gt; (&lt;/span&gt;&lt;span&gt;h &lt;/span&gt;&lt;/span&gt;&lt;span&gt;*&lt;/span&gt;&lt;span&gt;MCPHandler) &lt;/span&gt;&lt;span&gt;callSearchKnowledgeBase&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;&lt;span&gt;  &lt;/span&gt;&lt;/span&gt;&lt;span&gt;ctx&lt;/span&gt;&lt;span&gt; context.Context,&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;&lt;span&gt;  &lt;/span&gt;&lt;/span&gt;&lt;span&gt;args&lt;/span&gt;&lt;span&gt; SearchKnowledgeBaseArgs,&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;) (MCPToolResult, &lt;/span&gt;&lt;span&gt;error&lt;/span&gt;&lt;span&gt;) {&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;  &lt;/span&gt;&lt;span&gt;hits&lt;/span&gt;&lt;span&gt;, &lt;/span&gt;&lt;span&gt;err&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;:=&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;h&lt;/span&gt;&lt;span&gt;.&lt;/span&gt;&lt;span&gt;search&lt;/span&gt;&lt;span&gt;.&lt;/span&gt;&lt;span&gt;Search&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;ctx&lt;/span&gt;&lt;span&gt;, SearchRequest{&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;    &lt;/span&gt;&lt;span&gt;KnowledgeBaseID&lt;/span&gt;&lt;span&gt;: &lt;/span&gt;&lt;span&gt;args&lt;/span&gt;&lt;span&gt;.&lt;/span&gt;&lt;span&gt;KnowledgeBaseID&lt;/span&gt;&lt;span&gt;,&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;    &lt;/span&gt;&lt;span&gt;Query&lt;/span&gt;&lt;span&gt;:           &lt;/span&gt;&lt;span&gt;args&lt;/span&gt;&lt;span&gt;.&lt;/span&gt;&lt;span&gt;Query&lt;/span&gt;&lt;span&gt;,&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;    &lt;/span&gt;&lt;span&gt;TopK&lt;/span&gt;&lt;span&gt;:            &lt;/span&gt;&lt;span&gt;args&lt;/span&gt;&lt;span&gt;.&lt;/span&gt;&lt;span&gt;TopK&lt;/span&gt;&lt;span&gt;,&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;&lt;span&gt;  &lt;/span&gt;&lt;/span&gt;&lt;span&gt;})&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;  &lt;/span&gt;&lt;span&gt;if&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;err&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;!=&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;nil&lt;/span&gt;&lt;span&gt; {&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;    &lt;/span&gt;&lt;span&gt;return&lt;/span&gt;&lt;span&gt; MCPToolResult{}, &lt;/span&gt;&lt;span&gt;err&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;&lt;span&gt;  &lt;/span&gt;&lt;/span&gt;&lt;span&gt;}&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;
&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;  &lt;/span&gt;&lt;span&gt;return&lt;/span&gt;&lt;span&gt; MCPToolResult{&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;    &lt;/span&gt;&lt;span&gt;Content&lt;/span&gt;&lt;span&gt;: []MCPContent{&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;&lt;span&gt;      &lt;/span&gt;&lt;/span&gt;&lt;span&gt;{&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;        &lt;/span&gt;&lt;span&gt;Type&lt;/span&gt;&lt;span&gt;: &lt;/span&gt;&lt;span&gt;&quot;&lt;/span&gt;&lt;span&gt;text&lt;/span&gt;&lt;span&gt;&quot;&lt;/span&gt;&lt;span&gt;,&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;        &lt;/span&gt;&lt;span&gt;Text&lt;/span&gt;&lt;span&gt;: &lt;/span&gt;&lt;span&gt;formatSearchSummary&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;args&lt;/span&gt;&lt;span&gt;.&lt;/span&gt;&lt;span&gt;Query&lt;/span&gt;&lt;span&gt;, &lt;/span&gt;&lt;span&gt;hits&lt;/span&gt;&lt;span&gt;),&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;&lt;span&gt;      &lt;/span&gt;&lt;/span&gt;&lt;span&gt;},&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;&lt;span&gt;    &lt;/span&gt;&lt;/span&gt;&lt;span&gt;},&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;    &lt;/span&gt;&lt;span&gt;Structured&lt;/span&gt;&lt;span&gt;: &lt;/span&gt;&lt;span&gt;map&lt;/span&gt;&lt;span&gt;[&lt;/span&gt;&lt;span&gt;string&lt;/span&gt;&lt;span&gt;]any{&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;      &lt;/span&gt;&lt;span&gt;&quot;&lt;/span&gt;&lt;span&gt;query&lt;/span&gt;&lt;span&gt;&quot;&lt;/span&gt;&lt;span&gt;: &lt;/span&gt;&lt;span&gt;args&lt;/span&gt;&lt;span&gt;.&lt;/span&gt;&lt;span&gt;Query&lt;/span&gt;&lt;span&gt;,&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;      &lt;/span&gt;&lt;span&gt;&quot;&lt;/span&gt;&lt;span&gt;hits&lt;/span&gt;&lt;span&gt;&quot;&lt;/span&gt;&lt;span&gt;:  &lt;/span&gt;&lt;span&gt;hits&lt;/span&gt;&lt;span&gt;,&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;&lt;span&gt;    &lt;/span&gt;&lt;/span&gt;&lt;span&gt;},&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;&lt;span&gt;  &lt;/span&gt;&lt;/span&gt;&lt;span&gt;}, &lt;/span&gt;&lt;span&gt;nil&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;}&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/figure&gt;&lt;/div&gt;
&lt;p&gt;这里其实就能看出 MCP Server 的位置了。&lt;/p&gt;
&lt;p&gt;它不是向量数据库。&lt;/p&gt;
&lt;p&gt;它也不是 RAG 本身。&lt;/p&gt;
&lt;p&gt;它只是把 AI LocalBase 已经有的检索能力，包装成 AI 应用能发现、能理解、能调用的工具。&lt;/p&gt;
&lt;p&gt;客户端配置也可以很简单。比如一个支持 HTTP MCP 的 AI 应用，可能只需要知道 MCP 服务地址和访问凭证：&lt;/p&gt;
&lt;div&gt;&lt;figure&gt;&lt;figcaption&gt;&lt;/figcaption&gt;&lt;pre&gt;&lt;code&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;{&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;  &lt;/span&gt;&lt;span&gt;&quot;mcpServers&quot;&lt;/span&gt;&lt;span&gt;: {&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;    &lt;/span&gt;&lt;span&gt;&quot;ai-localbase&quot;&lt;/span&gt;&lt;span&gt;: {&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;      &lt;/span&gt;&lt;span&gt;&quot;type&quot;&lt;/span&gt;&lt;span&gt;: &lt;/span&gt;&lt;span&gt;&quot;&lt;/span&gt;&lt;span&gt;http&lt;/span&gt;&lt;span&gt;&quot;&lt;/span&gt;&lt;span&gt;,&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;      &lt;/span&gt;&lt;span&gt;&quot;url&quot;&lt;/span&gt;&lt;span&gt;: &lt;/span&gt;&lt;span&gt;&quot;&lt;/span&gt;&lt;span&gt;http://localhost:8080/api/mcp&lt;/span&gt;&lt;span&gt;&quot;&lt;/span&gt;&lt;span&gt;,&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;      &lt;/span&gt;&lt;span&gt;&quot;headers&quot;&lt;/span&gt;&lt;span&gt;: {&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;        &lt;/span&gt;&lt;span&gt;&quot;Authorization&quot;&lt;/span&gt;&lt;span&gt;: &lt;/span&gt;&lt;span&gt;&quot;&lt;/span&gt;&lt;span&gt;Bearer lb_xxx&lt;/span&gt;&lt;span&gt;&quot;&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;&lt;span&gt;      &lt;/span&gt;&lt;/span&gt;&lt;span&gt;}&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;&lt;span&gt;    &lt;/span&gt;&lt;/span&gt;&lt;span&gt;}&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;&lt;span&gt;  &lt;/span&gt;&lt;/span&gt;&lt;span&gt;}&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;}&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/figure&gt;&lt;/div&gt;
&lt;p&gt;这样配置之后，AI 应用就可以把 AI LocalBase 当成一个外部能力源。&lt;/p&gt;
&lt;p&gt;用户问问题时，AI 应用可以先通过 MCP 调用 &lt;code dir=&quot;auto&quot;&gt;search_knowledge_base&lt;/code&gt;，拿到相关文档片段，再基于这些片段组织回答。&lt;/p&gt;
&lt;p&gt;所以从代码角度看，MCP 并不是多神秘的东西。&lt;/p&gt;
&lt;p&gt;它就是一层协议适配。&lt;/p&gt;
&lt;p&gt;只不过这层适配面向的是 AI 应用，所以它不只要能调用，还要能描述能力、限制权限、返回结构化结果，并且让后续的 Agent 工作流继续往下走。&lt;/p&gt;
&lt;div&gt;&lt;h2 id=&quot;参考资料&quot;&gt;参考资料&lt;/h2&gt;&lt;/div&gt;
&lt;ul&gt;
&lt;li&gt;&lt;a href=&quot;https://modelcontextprotocol.io/docs/learn/architecture&quot;&gt;MCP 官方架构文档&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://modelcontextprotocol.io/specification/2025-06-18/basic/transports&quot;&gt;MCP 官方传输协议说明&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://github.com/veyliss/ai-localbase&quot;&gt;AI LocalBase&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://github.com/veyliss/ai-localbase/blob/main/docs/mcp.md&quot;&gt;AI LocalBase MCP 接入说明&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;</content:encoded><category>博客</category><category>AI</category><category>MCP</category><category>架构</category></item><item><title>关于逆向项目的一些想法</title><link>https://blog.veyliss.top/blog/reverse_project_thoughts_2026_06/</link><guid isPermaLink="true">https://blog.veyliss.top/blog/reverse_project_thoughts_2026_06/</guid><pubDate>Fri, 12 Jun 2026 00:00:00 GMT</pubDate><content:encoded>&lt;p&gt;继上一次爬虫架构设计的博客记录，回顾写一写当时做逆向项目的一些经历和感受。
肯定没有当时那样的心情了，但还是想把一些想法记录下来，算是对这个项目的一个总结和反思。&lt;/p&gt;
&lt;div&gt;&lt;h2 id=&quot;为什么想写这篇&quot;&gt;为什么想写这篇&lt;/h2&gt;&lt;/div&gt;
&lt;p&gt;最近看到不少分享逆向的文章，感觉挺有意思，也想记录一下自己在做这类项目时的一些想法和体会。
这个项目还是比较典型的逆向项目，涉及到代码分析、参数破解、请求模拟、数据处理等多个环节。
也是AI协助下的一种风口和未来趋势，虽然AI不能完全替代人工判断，但确实能加速很多分析和调试的过程。&lt;/p&gt;
&lt;div&gt;&lt;h2 id=&quot;背景&quot;&gt;背景&lt;/h2&gt;&lt;/div&gt;
&lt;p&gt;某游戏大厂的虚拟物品交易平台。
需要从这个平台上获取一些数据，但官方没有提供公开的API接口。
所以只能通过逆向分析前端代码和网络请求，来找到合适的接口和参数，再模拟请求获取数据。
接口返回的数据分成两部分：一部分是基本的列表数据，另一部分是需要额外请求才能拿到的详细数据。
js的代码是经过混淆的，参数生成也比较复杂，涉及到一些加密和动态生成的逻辑。&lt;/p&gt;
&lt;p&gt;标注：这篇博客不粘贴代码，只使用文字的方式来描述分析和思考的过程，避免泄露任何敏感信息（🐶狗头保命）。&lt;/p&gt;
&lt;div&gt;&lt;h2 id=&quot;先聊聊开始前的准备和事项&quot;&gt;先聊聊开始前的准备和事项&lt;/h2&gt;&lt;/div&gt;
&lt;p&gt;使用的浏览器 Network，看 XHR 、 Fetch、resource，进行断点调试，分析。
再分析请求参数、Headers、Cookie、响应结构。尝试请求接口，看看能不能拿到数据。
分析 JavaScript 代码，找到请求相关的函数，看看参数是怎么生成的，是否有加密、混淆、动态生成等。&lt;/p&gt;
&lt;p&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;/p&gt;
&lt;div&gt;&lt;h2 id=&quot;开始&quot;&gt;开始&lt;/h2&gt;&lt;/div&gt;
&lt;p&gt;总计耗时三天，第一天主要是分析请求，参数，熟悉请求流程和代码，第二天主要是分析代码和找到解密方法，第三天主要是把请求流程跑通。&lt;/p&gt;
&lt;p&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;/p&gt;
&lt;div&gt;&lt;h2 id=&quot;逆向不只是找接口&quot;&gt;逆向不只是找接口&lt;/h2&gt;&lt;/div&gt;
&lt;p&gt;很多时候，逆向项目的第一步确实是抓包。&lt;/p&gt;
&lt;p&gt;但这只是开始。&lt;/p&gt;
&lt;p&gt;真正麻烦的地方往往在后面：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;参数是不是动态生成的&lt;/li&gt;
&lt;li&gt;Token 是否有时效&lt;/li&gt;
&lt;li&gt;Cookie 是否会失效&lt;/li&gt;
&lt;li&gt;请求频率是否有限制&lt;/li&gt;
&lt;li&gt;数据结构是否会变化&lt;/li&gt;
&lt;li&gt;失败任务能不能恢复&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;先跑通一次流程，后续的流程会很快&lt;/p&gt;
&lt;div&gt;&lt;h2 id=&quot;项目里最考验人的地方&quot;&gt;项目里最考验人的地方&lt;/h2&gt;&lt;/div&gt;
&lt;p&gt;我觉得逆向项目里最考验人的，不是某一次成功请求。&lt;/p&gt;
&lt;p&gt;而是你能不能把它从“脚本”做成一个能运行的系统。&lt;/p&gt;
&lt;p&gt;比如：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;失败要能重试&lt;/li&gt;
&lt;li&gt;账号要能管理&lt;/li&gt;
&lt;li&gt;Cookie 要能维护&lt;/li&gt;
&lt;li&gt;请求要控制频率&lt;/li&gt;
&lt;li&gt;数据要能落库&lt;/li&gt;
&lt;li&gt;异常要能被发现&lt;/li&gt;
&lt;li&gt;任务要能继续跑&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;这些东西看起来不如破解一个参数酷，但它们才决定项目能不能真正用起来。&lt;/p&gt;
&lt;div&gt;&lt;h2 id=&quot;gpt-能帮忙但不能替你判断&quot;&gt;GPT 能帮忙，但不能替你判断&lt;/h2&gt;&lt;/div&gt;
&lt;p&gt;现在做这类项目，AI 工具确实能帮很多忙。&lt;/p&gt;
&lt;p&gt;它可以帮你分析代码片段，解释混淆逻辑，整理调试思路，甚至帮你把 JavaScript 逻辑改写成 Python。
甚至可以将加密的数据给AI看一下，确定加密的类型，或者帮你分析加密算法的逻辑。&lt;/p&gt;
&lt;p&gt;但它也有局限。&lt;/p&gt;
&lt;p&gt;它不知道真实请求环境，不知道站点当时的状态，也不知道你的 Cookie、代理、网络和账号到底发生了什么。&lt;/p&gt;
&lt;p&gt;所以最后还是要自己判断。&lt;/p&gt;
&lt;p&gt;AI 可以加速排查，但不能代替现场判断。同理，AI 也会犯错。&lt;/p&gt;
&lt;div&gt;&lt;h2 id=&quot;写在末尾&quot;&gt;写在末尾&lt;/h2&gt;&lt;/div&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;</content:encoded><category>博客</category><category>爬虫</category><category>逆向</category><category>随笔</category></item><item><title>Vibe Coding 一段时间后，我的一些新感受</title><link>https://blog.veyliss.top/blog/vibe_coding_recent_thoughts_2026_06/</link><guid isPermaLink="true">https://blog.veyliss.top/blog/vibe_coding_recent_thoughts_2026_06/</guid><pubDate>Wed, 10 Jun 2026 00:00:00 GMT</pubDate><content:encoded>&lt;p&gt;Vibe Coding 又用了有一段时间。&lt;/p&gt;
&lt;p&gt;一开始接触它的时候，更多是新鲜感。看到 AI 能读项目、改文件、跑命令、修问题，会觉得这东西多少有点离谱。&lt;/p&gt;
&lt;p&gt;但用久一点之后，新鲜感会慢慢退下去，剩下的就是一些更真实的体感。&lt;/p&gt;
&lt;p&gt;它确实提高了效率。&lt;/p&gt;
&lt;p&gt;但它也不是魔法。&lt;/p&gt;
&lt;div&gt;&lt;h2 id=&quot;它让开发变快了&quot;&gt;它让开发变快了&lt;/h2&gt;&lt;/div&gt;
&lt;p&gt;以前做一个功能，很多时间会花在重复劳动上。&lt;/p&gt;
&lt;p&gt;比如找文件、改样式、补类型、处理小 bug、跑构建、看报错。&lt;/p&gt;
&lt;p&gt;这些事情不难，但很碎。&lt;/p&gt;
&lt;p&gt;碎事情多了，人就容易烦。&lt;/p&gt;
&lt;p&gt;Vibe Coding 最大的变化，是它把这些碎事情接过去了一部分。很多时候，我只需要描述清楚目标，它就可以帮我把第一版做出来。&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;因为很多想法不是想出来的，是做出来之后才知道哪里不对。&lt;/p&gt;
&lt;div&gt;&lt;h2 id=&quot;但快不等于稳&quot;&gt;但快不等于稳&lt;/h2&gt;&lt;/div&gt;
&lt;p&gt;AI 写代码很快。&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;比如一个样式，它可能会顺手加背景、卡片、动效；但我可能只需要一个很轻的交互反馈。&lt;/p&gt;
&lt;p&gt;所以用 Vibe Coding 越久，我越觉得验收很重要。&lt;/p&gt;
&lt;p&gt;不是它写完就结束了。&lt;/p&gt;
&lt;p&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;li&gt;有没有破坏已有习惯&lt;/li&gt;
&lt;li&gt;有没有只是“看起来很努力”&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;有时候 AI 的代码没错，但气质不对。&lt;/p&gt;
&lt;p&gt;这就需要人来判断。&lt;/p&gt;
&lt;div&gt;&lt;h2 id=&quot;上下文比提示词更重要&quot;&gt;上下文比提示词更重要&lt;/h2&gt;&lt;/div&gt;
&lt;p&gt;以前我会觉得提示词很重要。&lt;/p&gt;
&lt;p&gt;现在我觉得，上下文更重要。&lt;/p&gt;
&lt;p&gt;一个项目里已经有自己的结构、命名、样式、内容风格和取舍习惯。如果 AI 不理解这些，它就很容易写出一个“能跑但不像这个项目”的东西。&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;这和人接手项目其实是一样的。一个靠谱的人不会上来就重构，他会先看看这个项目原来是怎么长出来的。&lt;/p&gt;
&lt;p&gt;AI 也一样。&lt;/p&gt;
&lt;p&gt;给它足够的上下文，它会更像协作者。&lt;/p&gt;
&lt;p&gt;不给上下文，它就更像一个手很快的陌生人。&lt;/p&gt;
&lt;div&gt;&lt;h2 id=&quot;人的判断没有变少&quot;&gt;人的判断没有变少&lt;/h2&gt;&lt;/div&gt;
&lt;p&gt;Vibe Coding 用得越多，我越不觉得人会变得不重要。&lt;/p&gt;
&lt;p&gt;相反，人要负责的东西变得更靠前了。&lt;/p&gt;
&lt;p&gt;以前我主要关心“怎么实现”。&lt;/p&gt;
&lt;p&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;li&gt;会不会影响长期维护&lt;/li&gt;
&lt;li&gt;这个功能是不是符合产品气质&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;AI 可以帮我往前推，但方向还是要自己定。&lt;/p&gt;
&lt;p&gt;如果方向不清楚，AI 只会更快地把混乱实现出来。&lt;/p&gt;
&lt;p&gt;这也是我最近很明显的感受：Vibe Coding 不是让人不用思考，而是把思考的位置往上抬。&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;div&gt;&lt;h2 id=&quot;它也会制造整理需求&quot;&gt;它也会制造整理需求&lt;/h2&gt;&lt;/div&gt;
&lt;p&gt;还有一个很现实的问题：AI 参与越多，项目越需要规则。&lt;/p&gt;
&lt;p&gt;比如文件怎么命名，文章放哪里，数据抽到哪里，样式怎么复用，哪些信息不能写进仓库。&lt;/p&gt;
&lt;p&gt;如果这些规则不清楚，AI 每次都会根据当下情况做一个“看起来可以”的选择。&lt;/p&gt;
&lt;p&gt;短期没问题。&lt;/p&gt;
&lt;p&gt;长期就会慢慢乱。&lt;/p&gt;
&lt;p&gt;所以我最近反而更重视项目规范。&lt;/p&gt;
&lt;p&gt;比如博客按年份月份归档，URL 用 slug 固定；友链和技术栈抽到 data；部署信息用 &lt;code dir=&quot;auto&quot;&gt;.env&lt;/code&gt;；新增文章用脚本生成。&lt;/p&gt;
&lt;p&gt;这些事情看起来不酷。&lt;/p&gt;
&lt;p&gt;但它们能让 AI 更好地工作，也能让未来的自己少骂几句过去的自己。&lt;/p&gt;
&lt;div&gt;&lt;h2 id=&quot;最后&quot;&gt;最后&lt;/h2&gt;&lt;/div&gt;
&lt;p&gt;现在再看 Vibe Coding，我已经不太把它当成一个炫技词了。&lt;/p&gt;
&lt;p&gt;它更像一种新的工作方式。&lt;/p&gt;
&lt;p&gt;不是我把所有事情交给 AI。&lt;/p&gt;
&lt;p&gt;而是我负责目标、边界、判断和验收，AI 负责承担大量具体执行。&lt;/p&gt;
&lt;p&gt;这个分工如果配合得好，效率确实会高很多。&lt;/p&gt;
&lt;p&gt;但如果人自己没想清楚，它也会很快把问题放大。&lt;/p&gt;
&lt;p&gt;所以我现在对 Vibe Coding 的感受很简单：&lt;/p&gt;
&lt;p&gt;它不是让开发变得不用动脑。&lt;/p&gt;
&lt;p&gt;它是让开发者必须更清楚地知道，自己到底想要什么。&lt;/p&gt;
&lt;p&gt;决策会变得越来越重要。&lt;/p&gt;</content:encoded><category>博客</category><category>AI</category><category>随笔</category><category>工具</category></item><item><title>第一次迁移生产服务器</title><link>https://blog.veyliss.top/blog/server_migration_nginx_docker_ssl_2026/</link><guid isPermaLink="true">https://blog.veyliss.top/blog/server_migration_nginx_docker_ssl_2026/</guid><pubDate>Sun, 07 Jun 2026 00:00:00 GMT</pubDate><content:encoded>&lt;p&gt;好消息，服务器迁移完成了。&lt;/p&gt;
&lt;p&gt;坏消息，迁移服务器花了近四小时。这要是生产停机迁移，这不事不得凌晨干。&lt;/p&gt;
&lt;p&gt;但我没有，这是一次停机的迁移…&lt;/p&gt;
&lt;p&gt;这还是我第一次遇到服务商要跑路。有一说一，机器很稳，也会提前30天发公告，如此的一家服务商就这样没了，挺可惜的，我倒是希望你活下去。&lt;/p&gt;
&lt;p&gt;以前总觉得服务器迁移这种事离我挺远，直到通知的时候，才发现：哦，倒霉事落到我身上了。&lt;/p&gt;
&lt;div&gt;&lt;h2 id=&quot;搬了什么&quot;&gt;搬了什么&lt;/h2&gt;&lt;/div&gt;
&lt;p&gt;这次主要迁移的是一些 Web 服务、数据卷。&lt;/p&gt;
&lt;p&gt;大部分都跑在 Docker 里，包括：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;cpa&lt;/li&gt;
&lt;li&gt;new-api&lt;/li&gt;
&lt;li&gt;博客&lt;/li&gt;
&lt;li&gt;PostgreSQL&lt;/li&gt;
&lt;li&gt;Redis&lt;/li&gt;
&lt;li&gt;Nginx&lt;/li&gt;
&lt;li&gt;SSL 证书&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;需要备份data volume，重新部署 Docker Compose，重新配置 Nginx，重新申请 SSL 证书。&lt;/p&gt;
&lt;p&gt;每一层看起来都不复杂，但它们连在一起，就开始有点会折磨人了。&lt;/p&gt;
&lt;div&gt;&lt;h2 id=&quot;没经验所以慢&quot;&gt;没经验，所以慢&lt;/h2&gt;&lt;/div&gt;
&lt;p&gt;这次耗时比较长，主要还是因为没什么服务器迁移经验。&lt;/p&gt;
&lt;p&gt;平时部署一个服务还好，看文档，用docker-compose。&lt;/p&gt;
&lt;p&gt;但迁移不一样。&lt;/p&gt;
&lt;p&gt;迁移是你要先把旧环境拆开，再在新环境里重新拼起来。&lt;/p&gt;
&lt;p&gt;比如 Docker volume 看起来恢复了，但数据库不一定真的恢复对了。&lt;/p&gt;
&lt;p&gt;Nginx 配置看起来写了，但请求不一定真的进了你想要的 server block。&lt;/p&gt;
&lt;p&gt;SSL 证书看起来签了，但域名不一定都覆盖到了。&lt;/p&gt;
&lt;p&gt;这几个“不一定”，加起来就是一个下午。&lt;/p&gt;
&lt;div&gt;&lt;h2 id=&quot;nginx-是这次的重点嘉宾&quot;&gt;Nginx 是这次的重点嘉宾&lt;/h2&gt;&lt;/div&gt;
&lt;p&gt;整个过程中最让我头大的还是 Nginx。&lt;/p&gt;
&lt;p&gt;主域名访问时，一直出现 nginx welcome page。&lt;/p&gt;
&lt;p&gt;二级域名访问却没问题。&lt;/p&gt;
&lt;p&gt;这个页面真的很神奇。&lt;/p&gt;
&lt;p&gt;就是这个熟悉的家伙：&lt;/p&gt;
&lt;div&gt;&lt;figure&gt;&lt;figcaption&gt;&lt;/figcaption&gt;&lt;pre&gt;&lt;code&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;/etc/nginx/sites-enabled/default&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/figure&gt;&lt;/div&gt;
&lt;p&gt;删掉默认站点，再 reload Nginx，主域名才终于正常。&lt;/p&gt;
&lt;p&gt;这件事给我的感觉就是：有时候不是你没配置，而是有默认配置。&lt;/p&gt;
&lt;div&gt;&lt;h2 id=&quot;gpt-也会帮倒忙&quot;&gt;GPT 也会帮倒忙&lt;/h2&gt;&lt;/div&gt;
&lt;p&gt;这次迁移过程中，我也一直在让 GPT 帮我看问题。&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;但实际情况是：你的服务器可能有 &lt;code dir=&quot;auto&quot;&gt;conf.d&lt;/code&gt;，也可能有 &lt;code dir=&quot;auto&quot;&gt;sites-enabled&lt;/code&gt;，还可能有 certbot 自动生成的配置。&lt;/p&gt;
&lt;p&gt;它说得很有道理。&lt;/p&gt;
&lt;p&gt;但是服务器情况不一定是gpt说的那样。&lt;/p&gt;
&lt;p&gt;所以最后还是要自己去看：&lt;/p&gt;
&lt;div&gt;&lt;figure&gt;&lt;figcaption&gt;&lt;span&gt;&lt;/span&gt;&lt;/figcaption&gt;&lt;pre&gt;&lt;code&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;nginx&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;-T&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/figure&gt;&lt;/div&gt;
&lt;p&gt;真实配置永远比想象中的配置重要。&lt;/p&gt;
&lt;div&gt;&lt;h2 id=&quot;最后还是跑起来了&quot;&gt;最后还是跑起来了&lt;/h2&gt;&lt;/div&gt;
&lt;p&gt;折腾了几个小时后，服务终于都恢复了。&lt;/p&gt;
&lt;p&gt;博客可以访问，new-api 正常，cpa的数据没丢，统计服务也能起来，主域名和二级域名都能用，SSL 证书也处理好了。&lt;/p&gt;
&lt;p&gt;那一刻没有什么特别激动的感觉。&lt;/p&gt;
&lt;p&gt;更多是松了一口气。&lt;/p&gt;
&lt;div&gt;&lt;h2 id=&quot;这次的收获&quot;&gt;这次的收获&lt;/h2&gt;&lt;/div&gt;
&lt;p&gt;这次最大的感受是：&lt;/p&gt;
&lt;p&gt;时间的流逝，非常快。折腾一会，几个小时没了。&lt;/p&gt;
&lt;p&gt;需要树立工程意识，迁移服务的环节流程需要熟悉，才能避免踩坑。要有自己的判断，不能轻易全部的交给 GPT。&lt;/p&gt;
&lt;p&gt;服务器迁移不是单纯的搬数据，更是一个重新搭建环境的过程。
每个单独的docker容器挂在的数据卷。
在备份项目和数据卷的时候注意有没有嵌套的卷。
运维也不容易…&lt;/p&gt;</content:encoded><category>博客</category><category>运维</category><category>Docker</category><category>Nginx</category></item><item><title>AI 编辑器工具的一点观察</title><link>https://blog.veyliss.top/blog/ai_editor_tools_platform_observation_01/</link><guid isPermaLink="true">https://blog.veyliss.top/blog/ai_editor_tools_platform_observation_01/</guid><pubDate>Mon, 01 Jun 2026 00:00:00 GMT</pubDate><content:encoded>&lt;p&gt;最近一直在想一类工具。&lt;/p&gt;
&lt;p&gt;类似 Cursor 这样的 AI 编辑器工具平台。&lt;/p&gt;
&lt;p&gt;从去年开始，市面上出现了很多新的编辑器产品。它们大多不是从零开始做一个全新的 IDE，而是选择基于开源的 VS Code 平台继续往上搭。&lt;/p&gt;
&lt;p&gt;这个选择其实很现实。&lt;/p&gt;
&lt;p&gt;VS Code 已经有成熟的编辑体验、插件生态、终端、调试、文件管理和开发者习惯。重新做一套编辑器，成本太高，也很难让开发者迁移。&lt;/p&gt;
&lt;p&gt;所以大家更愿意站在 VS Code 的基础上，把真正的差异放到 AI 能力上。&lt;/p&gt;
&lt;div&gt;&lt;h2 id=&quot;vs-code-成了底座&quot;&gt;VS Code 成了底座&lt;/h2&gt;&lt;/div&gt;
&lt;p&gt;我用过一些类似工具，比如腾讯的 CodeBuddy、阿里的 Qoder，还有字节的 Trae。&lt;/p&gt;
&lt;p&gt;它们给人的第一感觉都很熟悉。&lt;/p&gt;
&lt;p&gt;因为底层体验都绕不开 VS Code。&lt;/p&gt;
&lt;p&gt;文件树、编辑区、终端、快捷键、插件习惯，这些东西不用重新教育用户。开发者打开之后，不会有太强的陌生感。&lt;/p&gt;
&lt;p&gt;所以现在这类工具的竞争，已经不太是“谁更像一个编辑器”。&lt;/p&gt;
&lt;p&gt;大家的地基差不多，真正的差异开始出现在 AI 层：&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;li&gt;谁能在执行过程中少犯错。&lt;/li&gt;
&lt;li&gt;谁能把结果验证到位。&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;这些问题，开始比传统编辑器功能更重要。&lt;/p&gt;
&lt;div&gt;&lt;h2 id=&quot;差异点变成-agent&quot;&gt;差异点变成 Agent&lt;/h2&gt;&lt;/div&gt;
&lt;p&gt;每个平台都会有自己想强调的 Agent 能力。&lt;/p&gt;
&lt;p&gt;比如阿里的 Qoder，有“专家执行团”这种偏多角色协作的设计。&lt;/p&gt;
&lt;p&gt;Trae 里也有 Solo 这种更偏独立执行任务的模式。&lt;/p&gt;
&lt;p&gt;这些名字背后，其实都在回答一个问题：&lt;/p&gt;
&lt;div&gt;&lt;figure&gt;&lt;figcaption&gt;&lt;/figcaption&gt;&lt;pre&gt;&lt;code&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;AI 到底应该以什么方式参与开发？&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/figure&gt;&lt;/div&gt;
&lt;p&gt;是作为一个回答问题的助手？&lt;/p&gt;
&lt;p&gt;还是作为一个能拆任务、改代码、跑命令、检查结果的执行者？&lt;/p&gt;
&lt;p&gt;我感觉现在的趋势很明显：大家都不满足于“问答式 AI 编程”了。&lt;/p&gt;
&lt;p&gt;单纯聊天已经不够。&lt;/p&gt;
&lt;p&gt;开发者真正想要的是：&lt;/p&gt;
&lt;div&gt;&lt;figure&gt;&lt;figcaption&gt;&lt;/figcaption&gt;&lt;pre&gt;&lt;code&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;我给你一个目标，你能不能理解项目，然后一步步把事情做完？&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/figure&gt;&lt;/div&gt;
&lt;p&gt;这就是 Agent 开始变重要的原因。&lt;/p&gt;
&lt;div&gt;&lt;h2 id=&quot;从补全到执行&quot;&gt;从补全到执行&lt;/h2&gt;&lt;/div&gt;
&lt;p&gt;最早 AI 编程给人的印象，更多是代码补全。&lt;/p&gt;
&lt;p&gt;写一半，它补一半。&lt;/p&gt;
&lt;p&gt;后来变成 Chat。&lt;/p&gt;
&lt;p&gt;你问它一个问题，它回答你一段解释，或者给一段代码。&lt;/p&gt;
&lt;p&gt;现在越来越多工具在往执行系统走。&lt;/p&gt;
&lt;p&gt;也就是：&lt;/p&gt;
&lt;div&gt;&lt;figure&gt;&lt;figcaption&gt;&lt;/figcaption&gt;&lt;pre&gt;&lt;code&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;读项目 -&gt; 理解需求 -&gt; 修改文件 -&gt; 运行检查 -&gt; 根据结果继续调整&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/figure&gt;&lt;/div&gt;
&lt;p&gt;这个变化很大。&lt;/p&gt;
&lt;p&gt;因为它改变的不只是“写代码速度”，而是开发流程本身。&lt;/p&gt;
&lt;p&gt;以前 AI 像是一个在旁边回答问题的人。&lt;/p&gt;
&lt;p&gt;现在它开始像一个能参与工作的协作者。&lt;/p&gt;
&lt;p&gt;当然，这并不意味着可以完全放手。&lt;/p&gt;
&lt;p&gt;Agent 越强，人越需要判断方向。&lt;/p&gt;
&lt;p&gt;它可以帮你做很多具体事情，但你仍然要知道目标是什么、边界在哪里、什么东西不能乱改、结果是否真的符合需求。&lt;/p&gt;
&lt;div&gt;&lt;h2 id=&quot;移动端成为新入口&quot;&gt;移动端成为新入口&lt;/h2&gt;&lt;/div&gt;
&lt;p&gt;最近还有一个新的趋势：很多平台开始推出移动端。&lt;/p&gt;
&lt;p&gt;这件事挺有意思。&lt;/p&gt;
&lt;p&gt;以前开发工具基本都绑定在电脑上。哪怕是 AI 编辑器，本质上也还是一个桌面工作台：打开项目、读文件、改代码、跑命令。&lt;/p&gt;
&lt;p&gt;但移动端出来之后，入口变了。&lt;/p&gt;
&lt;p&gt;它更像是把 Agent 从编辑器里拆出来，放到一个随时可以对话的地方。&lt;/p&gt;
&lt;p&gt;有些未完成的工作，或者临时想到的事情，不一定非要坐到电脑前才能处理。&lt;/p&gt;
&lt;p&gt;比如：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;路上想到一个需求，让 Agent 先整理方案。&lt;/li&gt;
&lt;li&gt;临时发现一个 bug，让 Agent 先定位可能原因。&lt;/li&gt;
&lt;li&gt;让它继续某个没做完的任务。&lt;/li&gt;
&lt;li&gt;让它总结项目状态。&lt;/li&gt;
&lt;li&gt;让它根据上下文生成一个待办清单。&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;这时候手机不是用来完整开发的。&lt;/p&gt;
&lt;p&gt;它更像一个轻量入口。&lt;/p&gt;
&lt;p&gt;真正的执行可能还是在云端、远程环境或者本地项目里完成，但人和 Agent 的交互，不再必须发生在电脑前。&lt;/p&gt;
&lt;p&gt;这会让 AI 编程工具从“编辑器产品”继续往“工作流产品”走。&lt;/p&gt;
&lt;p&gt;桌面端负责深度开发，移动端负责随时调度。&lt;/p&gt;
&lt;div&gt;&lt;h2 id=&quot;平台竞争的核心&quot;&gt;平台竞争的核心&lt;/h2&gt;&lt;/div&gt;
&lt;p&gt;我觉得这类工具后面的竞争，可能不只是模型本身。&lt;/p&gt;
&lt;p&gt;模型当然重要。&lt;/p&gt;
&lt;p&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;li&gt;终端和测试集成。&lt;/li&gt;
&lt;li&gt;Agent 任务规划。&lt;/li&gt;
&lt;li&gt;错误恢复能力。&lt;/li&gt;
&lt;li&gt;UI 和交互设计。&lt;/li&gt;
&lt;li&gt;对开发者工作流的理解。&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;如果只是接入一个模型，其实很容易被复制。&lt;/p&gt;
&lt;p&gt;但如果一个工具能把模型、编辑器、终端、文件系统、Git、测试和任务流整合得很顺，那它就会变成真正的平台。&lt;/p&gt;
&lt;p&gt;这也是为什么大家都在做自己的 Agent。&lt;/p&gt;
&lt;p&gt;模型是发动机，但工具平台决定这辆车怎么开。&lt;/p&gt;
&lt;div&gt;&lt;h2 id=&quot;我自己的判断&quot;&gt;我自己的判断&lt;/h2&gt;&lt;/div&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;
&lt;p&gt;它们正在把开发者从“每一行都自己写”推向“定义目标、审查过程、把控结果”。&lt;/p&gt;
&lt;p&gt;这和我之前接触 Vibe Coding 的感受也能连起来。&lt;/p&gt;
&lt;p&gt;AI 工具越强，人的角色越往上移。&lt;/p&gt;
&lt;p&gt;不是不写代码了，而是不再只盯着代码本身。&lt;/p&gt;
&lt;p&gt;如果移动端这个方向走通，开发者和工具的关系会再变一次。&lt;/p&gt;
&lt;p&gt;以前是：&lt;/p&gt;
&lt;div&gt;&lt;figure&gt;&lt;figcaption&gt;&lt;/figcaption&gt;&lt;pre&gt;&lt;code&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;我打开编辑器，然后开始工作。&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/figure&gt;&lt;/div&gt;
&lt;p&gt;以后可能会变成：&lt;/p&gt;
&lt;div&gt;&lt;figure&gt;&lt;figcaption&gt;&lt;/figcaption&gt;&lt;pre&gt;&lt;code&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;我想到一个任务，先丢给 Agent，让它继续推进。&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/figure&gt;&lt;/div&gt;
&lt;p&gt;这也是为什么移动端值得关注。&lt;/p&gt;
&lt;p&gt;它不一定是为了在手机上写代码，而是为了让 AI 参与工作的入口变得更随身。&lt;/p&gt;
&lt;div&gt;&lt;h2 id=&quot;下一个-ai-编辑器工具平台会是什么样子&quot;&gt;下一个 AI 编辑器工具平台会是什么样子？&lt;/h2&gt;&lt;/div&gt;
&lt;p&gt;从对话到主动编辑，现在把 Agent 装进口袋，下一个阶段呢？&lt;/p&gt;
&lt;p&gt;我觉得它可能不只是更聪明的聊天窗口，也不只是更强的代码补全。&lt;/p&gt;
&lt;div&gt;&lt;figure&gt;&lt;figcaption&gt;&lt;/figcaption&gt;&lt;pre&gt;&lt;code&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;AI 编辑器正在从“写代码的工具”，变成“管理开发过程的入口”。&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/figure&gt;&lt;/div&gt;
&lt;p&gt;如果说过去的编辑器是人操作代码的地方，那么下一代 AI 编辑器，可能会变成 Agent 和人一起推进项目的地方。人不再只是盯着每一行代码，而是更多地定义目标、判断方向、确认结果。&lt;/p&gt;</content:encoded><category>AI</category><category>工具</category><category>随笔</category></item><item><title>六一这天，云服务商给我开了个玩笑</title><link>https://blog.veyliss.top/blog/cloud_provider_migration_notice_2026_06/</link><guid isPermaLink="true">https://blog.veyliss.top/blog/cloud_provider_migration_notice_2026_06/</guid><pubDate>Mon, 01 Jun 2026 00:00:00 GMT</pubDate><content:encoded>&lt;p&gt;六一这天，云服务商给我开了个天大的玩笑。&lt;/p&gt;
&lt;p&gt;AI爆发，硬件暴涨，云服务商的成本也在暴涨，撑不住的已经倒下。这股风也吹到了我这里。&lt;/p&gt;
&lt;p&gt;怎么说呢。&lt;/p&gt;
&lt;p&gt;儿童节，成年人收到的是服务器搬家通知。&lt;/p&gt;
&lt;p&gt;服务商还是比较良心，提前30天发布了公告。&lt;/p&gt;
&lt;div&gt;&lt;h2 id=&quot;博客搬家&quot;&gt;博客搬家&lt;/h2&gt;&lt;/div&gt;
&lt;p&gt;接下来这段时间，我会把博客迁移到新的服务器上。&lt;/p&gt;
&lt;p&gt;迁移后域名仍然保持不变：&lt;/p&gt;
&lt;div&gt;&lt;figure&gt;&lt;figcaption&gt;&lt;/figcaption&gt;&lt;pre&gt;&lt;code&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;blog.veyliss.top&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/figure&gt;&lt;/div&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;div&gt;&lt;h2 id=&quot;最后&quot;&gt;最后&lt;/h2&gt;&lt;/div&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;</content:encoded><category>博客</category><category>随笔</category></item><item><title>七年后和初中同学小聚</title><link>https://blog.veyliss.top/blog/reunion_after_seven_years_ai_and_life_01/</link><guid isPermaLink="true">https://blog.veyliss.top/blog/reunion_after_seven_years_ai_and_life_01/</guid><pubDate>Sun, 31 May 2026 00:00:00 GMT</pubDate><content:encoded>&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;
&lt;div&gt;&lt;h2 id=&quot;聊到-ai&quot;&gt;聊到 AI&lt;/h2&gt;&lt;/div&gt;
&lt;p&gt;他是动漫专业的。&lt;/p&gt;
&lt;p&gt;我们聊着聊着，很自然地就聊到了 AI。&lt;/p&gt;
&lt;p&gt;聊到 AI 对行业的冲击时，能感觉到那种复杂的情绪。&lt;/p&gt;
&lt;p&gt;一方面，AI 确实很强。图片生成、视频生成、分镜、角色设计、动作参考，这些东西正在变得越来越快。很多过去需要大量时间打磨的环节，现在似乎被压缩了。&lt;/p&gt;
&lt;p&gt;另一方面，这种强也会让人不安。&lt;/p&gt;
&lt;p&gt;不是简单地说“AI 会不会取代谁”，而是整个行业的工作方式正在被改变。学了很多年的技能，突然要面对一个速度极快的新工具，这种感觉并不轻松。&lt;/p&gt;
&lt;p&gt;聊这些的时候，我也会想到自己的行业。&lt;/p&gt;
&lt;p&gt;IT 这边其实也一样。&lt;/p&gt;
&lt;p&gt;代码生成、Agent、自动化测试、需求分析、文档总结，AI 也在一点点进入开发流程。它不是停在旁边给建议，而是开始坐到工作台上，参与具体的产出。&lt;/p&gt;
&lt;p&gt;所以我们虽然专业不同，但聊到 AI 的时候，感受是相通的。&lt;/p&gt;
&lt;p&gt;大家都在面对同一个问题：&lt;/p&gt;
&lt;div&gt;&lt;figure&gt;&lt;figcaption&gt;&lt;/figcaption&gt;&lt;pre&gt;&lt;code&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;当 AI 开始参与创作和工作，我们到底应该怎么重新理解自己的能力？&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/figure&gt;&lt;/div&gt;
&lt;div&gt;&lt;h2 id=&quot;聊了很多&quot;&gt;聊了很多&lt;/h2&gt;&lt;/div&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;
&lt;p&gt;老同学知道我很早之前的样子。&lt;/p&gt;
&lt;p&gt;不是现在这个写代码、做项目、折腾博客、学习 AI 的我，而是更早一点、更青涩一点的我。&lt;/p&gt;
&lt;p&gt;所以重逢时，会有一种时间被拉开的感觉。&lt;/p&gt;
&lt;p&gt;我能从对方身上看到过去，也能从彼此现在的状态里看到这些年各自的变化。&lt;/p&gt;
&lt;div&gt;&lt;h2 id=&quot;生活不是支线&quot;&gt;生活不是支线&lt;/h2&gt;&lt;/div&gt;
&lt;p&gt;回来的时候，心里有一点感慨。&lt;/p&gt;
&lt;p&gt;这段时间我好像一直在写技术、折腾项目、看 AI、做博客。&lt;/p&gt;
&lt;p&gt;这些东西当然重要。&lt;/p&gt;
&lt;p&gt;但那天和同学坐下来聊天，突然又觉得，生活本身也很重要。&lt;/p&gt;
&lt;p&gt;很多时候，我们会把生活当成技术之外的“空白时间”。写完代码，修完 bug，部署完服务，学习完一个概念，剩下的才叫生活。&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;div&gt;&lt;h2 id=&quot;小结&quot;&gt;小结&lt;/h2&gt;&lt;/div&gt;
&lt;p&gt;这次小聚没有什么特别宏大的主题。&lt;/p&gt;
&lt;p&gt;就是七年后见了一位初中同学，聊了很多，也感受到 AI 对不同行业都在产生影响。&lt;/p&gt;
&lt;p&gt;动漫也好，IT 也好，大家都在重新适应这个变化很快的时代。&lt;/p&gt;
&lt;p&gt;最后还是想感叹一句：&lt;/p&gt;
&lt;div&gt;&lt;figure&gt;&lt;figcaption&gt;&lt;/figcaption&gt;&lt;pre&gt;&lt;code&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;IT 除了代码，还有生活。&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/figure&gt;&lt;/div&gt;
&lt;p&gt;技术会一直往前走，项目也会继续做，AI 也会越来越强。&lt;/p&gt;
&lt;p&gt;但人不能只活在代码里。&lt;/p&gt;
&lt;p&gt;偶尔见见老朋友，聊聊过去和以后，也挺好。&lt;/p&gt;</content:encoded><category>生活</category><category>随笔</category></item><item><title>做 AI 本地优先知识库三个月后，我发现 RAG 没那么简单</title><link>https://blog.veyliss.top/blog/local_first_ai_knowledge_base_three_months_01/</link><guid isPermaLink="true">https://blog.veyliss.top/blog/local_first_ai_knowledge_base_three_months_01/</guid><pubDate>Fri, 29 May 2026 00:00:00 GMT</pubDate><content:encoded>&lt;p&gt;上周基本都在更新我的 AI 本地优先知识库。&lt;/p&gt;
&lt;p&gt;说是更新，其实更像是把一个放了一个多月的项目重新捡起来，拍掉灰，再试着往前推几步。&lt;/p&gt;
&lt;p&gt;从发布到现在，差不多快三个月了。&lt;/p&gt;
&lt;p&gt;项目地址是：&lt;a href=&quot;https://github.com/veyliss/ai-localbase&quot;&gt;veyliss/ai-localbase&lt;/a&gt;。&lt;/p&gt;
&lt;p&gt;Star 到了 300+。这个数字说大不大，说小也不小。对一个个人项目来说，能被三百多人点一下星，已经说明它至少击中过一小部分人的需求。&lt;/p&gt;
&lt;p&gt;但我也很清楚，中间有一个多月没怎么更新，所以 Star 涨得慢下来也很正常。&lt;/p&gt;
&lt;p&gt;开源项目有时候像一条很细的火线。你持续更新，它就偶尔亮一下；你停下来，它就慢慢变暗。不是项目突然没价值了，而是互联网上每天都有太多新的东西流过去。&lt;/p&gt;
&lt;div&gt;&lt;h2 id=&quot;还是-demo&quot;&gt;还是 Demo&lt;/h2&gt;&lt;/div&gt;
&lt;p&gt;重新看这个项目的时候，第一感觉是：&lt;/p&gt;
&lt;div&gt;&lt;figure&gt;&lt;figcaption&gt;&lt;/figcaption&gt;&lt;pre&gt;&lt;code&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;它还是一个 Demo。&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/figure&gt;&lt;/div&gt;
&lt;p&gt;不是不能用，也不是只有一个空壳。&lt;/p&gt;
&lt;p&gt;它已经能跑起来，方向也比较明确：本地优先、知识库、AI 问答、RAG、个人数据掌控。&lt;/p&gt;
&lt;p&gt;但离“一个成熟产品”还有距离。&lt;/p&gt;
&lt;p&gt;Bug 还不少，体验也不算很好。有些流程能走通，但不够顺。有些功能看起来有了，但真的连续使用时，边角问题会一个个冒出来。&lt;/p&gt;
&lt;p&gt;这也是个人项目很真实的一面。&lt;/p&gt;
&lt;p&gt;发出来的时候，更多是在表达一个想法：&lt;/p&gt;
&lt;div&gt;&lt;figure&gt;&lt;figcaption&gt;&lt;/figcaption&gt;&lt;pre&gt;&lt;code&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;我想做一个本地优先的 AI 知识库。&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/figure&gt;&lt;/div&gt;
&lt;p&gt;但真正要让别人舒服地用起来，靠一个想法远远不够。&lt;/p&gt;
&lt;div&gt;&lt;h2 id=&quot;补了一些体验&quot;&gt;补了一些体验&lt;/h2&gt;&lt;/div&gt;
&lt;p&gt;这次回来更新，主要做了两件事：优化 UI，优化检索。&lt;/p&gt;
&lt;p&gt;UI 的问题比较直观。一个工具能不能让人继续用下去，很多时候不是取决于它有没有某个大功能，而是打开之后顺不顺、清不清楚、有没有那种“我愿意继续点下去”的感觉。&lt;/p&gt;
&lt;p&gt;检索就更核心了。一个知识库项目，如果检索不准，后面 AI 回答再漂亮也没有意义。&lt;/p&gt;
&lt;p&gt;RAG 的链路里，LLM 往往是最显眼的部分，但真正影响回答质量的，很多时候是前面的检索。&lt;/p&gt;
&lt;p&gt;这也是我这几天最明显的感受：&lt;/p&gt;
&lt;div&gt;&lt;figure&gt;&lt;figcaption&gt;&lt;/figcaption&gt;&lt;pre&gt;&lt;code&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;RAG 不是接一个模型，再塞一点文档，就自然变聪明。&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/figure&gt;&lt;/div&gt;
&lt;div&gt;&lt;h2 id=&quot;rag-比想象中难&quot;&gt;RAG 比想象中难&lt;/h2&gt;&lt;/div&gt;
&lt;p&gt;真正做起来才发现，RAG 的难点不只在“调用大模型”。&lt;/p&gt;
&lt;p&gt;模型只是最后一步。&lt;/p&gt;
&lt;p&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;li&gt;怎么检索。&lt;/li&gt;
&lt;li&gt;怎么重排序。&lt;/li&gt;
&lt;li&gt;怎么拼上下文。&lt;/li&gt;
&lt;li&gt;怎么让答案有依据。&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;以前看 RAG 的概念图，会觉得它很清晰：&lt;/p&gt;
&lt;div&gt;&lt;figure&gt;&lt;figcaption&gt;&lt;/figcaption&gt;&lt;pre&gt;&lt;code&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;用户问题 -&gt; 检索资料 -&gt; 交给模型 -&gt; 生成答案&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/figure&gt;&lt;/div&gt;
&lt;p&gt;但真正落到项目里，它就不是一条那么干净的线了。&lt;/p&gt;
&lt;p&gt;文档内容可能很乱，切分可能切断语义，向量检索可能召回不准，模型可能忽略上下文，回答可能没有引用依据。这些问题不会在 Demo 截图里出现，但会在真实使用里一点点暴露。&lt;/p&gt;
&lt;p&gt;也正因为这样，我越来越觉得 RAG 是一个工程问题，不只是一个 AI 概念。&lt;/p&gt;
&lt;div&gt;&lt;h2 id=&quot;本地优先和现实&quot;&gt;本地优先和现实&lt;/h2&gt;&lt;/div&gt;
&lt;p&gt;这个项目一开始很强调本地优先。&lt;/p&gt;
&lt;p&gt;我很喜欢这个方向。&lt;/p&gt;
&lt;p&gt;知识库本来就带有很强的个人属性。笔记、文档、想法、项目资料，这些东西放在本地，用户自己掌控，会让人更安心。&lt;/p&gt;
&lt;p&gt;所以最开始我很自然地选择了 Ollama。&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;本地优先的理念是好的，但如果体验慢到让人不想继续用，那它就会从“安全感”变成“阻力”。&lt;/p&gt;
&lt;p&gt;这件事让我重新思考一个问题：&lt;/p&gt;
&lt;div&gt;&lt;figure&gt;&lt;figcaption&gt;&lt;/figcaption&gt;&lt;pre&gt;&lt;code&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;本地优先是不是等于只能使用本地模型？&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/figure&gt;&lt;/div&gt;
&lt;p&gt;后来我觉得，不一定。本地优先更重要的是数据和控制权。用户的数据应该尽量保存在本地，知识库应该由用户自己掌控，项目不应该强绑定某个平台。&lt;/p&gt;
&lt;div&gt;&lt;h2 id=&quot;加入-api-接入&quot;&gt;加入 API 接入&lt;/h2&gt;&lt;/div&gt;
&lt;p&gt;后面我加入了 API 接入能力。&lt;/p&gt;
&lt;p&gt;这样项目不再只依赖 Ollama，也可以接入市面上很多模型。&lt;/p&gt;
&lt;p&gt;这不是放弃本地优先，更像是给项目留出两个出口：&lt;/p&gt;
&lt;div&gt;&lt;figure&gt;&lt;figcaption&gt;&lt;/figcaption&gt;&lt;pre&gt;&lt;code&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;想要完全本地，可以走 Ollama。&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;想要更好体验，可以走 API。&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/figure&gt;&lt;/div&gt;
&lt;p&gt;我现在更愿意把它理解成：&lt;/p&gt;
&lt;div&gt;&lt;figure&gt;&lt;figcaption&gt;&lt;/figcaption&gt;&lt;pre&gt;&lt;code&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;数据本地优先，模型选择开放。&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/figure&gt;&lt;/div&gt;
&lt;p&gt;这个取舍比一开始更现实，也更接近一个工具真的要被使用时的样子。&lt;/p&gt;
&lt;div&gt;&lt;h2 id=&quot;知识储备不太够&quot;&gt;知识储备不太够&lt;/h2&gt;&lt;/div&gt;
&lt;p&gt;这几天还有一个很强烈的感受：自己的知识储备还不太够。&lt;/p&gt;
&lt;p&gt;以前以为做一个 AI 知识库，重点是把产品形态搭起来。现在发现，产品形态只是外壳。里面真正难的是检索、向量化、上下文组织和回答质量。&lt;/p&gt;
&lt;p&gt;向量化这块尤其明显。Embedding 模型怎么选？中文效果怎么样？切片多大合适？什么时候只用向量检索，什么时候要结合关键词检索？要不要重排序？&lt;/p&gt;
&lt;p&gt;这些问题都不是一句“接入向量数据库”就能解决的。它们需要实验，也需要积累。&lt;/p&gt;
&lt;p&gt;我现在还在补这些东西。&lt;/p&gt;
&lt;div&gt;&lt;h2 id=&quot;三个月后的状态&quot;&gt;三个月后的状态&lt;/h2&gt;&lt;/div&gt;
&lt;p&gt;所以，如果给这个项目现在的状态做一个描述，我会这样写：&lt;/p&gt;
&lt;div&gt;&lt;figure&gt;&lt;figcaption&gt;&lt;/figcaption&gt;&lt;pre&gt;&lt;code&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;方向是对的，但还不够好用。&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/figure&gt;&lt;/div&gt;
&lt;p&gt;它已经证明了一些事情：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;本地优先知识库是有需求的。&lt;/li&gt;
&lt;li&gt;AI 问答和个人知识库结合是有吸引力的。&lt;/li&gt;
&lt;li&gt;但体验、速度、检索质量和稳定性都还需要继续打磨。&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;Star 300+ 当然让我开心。&lt;/p&gt;
&lt;p&gt;但我也知道，Star 不是产品成熟的证明。&lt;/p&gt;
&lt;p&gt;真正的证明是：有人能把它装起来，导入自己的资料，连续用几天，还愿意继续打开。&lt;/p&gt;
&lt;p&gt;这比 Star 更难。&lt;/p&gt;
&lt;div&gt;&lt;h2 id=&quot;继续往前&quot;&gt;继续往前&lt;/h2&gt;&lt;/div&gt;
&lt;p&gt;写到这里，感觉这不是一篇项目总结，更像是给自己留的一张便签。&lt;/p&gt;
&lt;p&gt;提醒自己不要被概念骗得太轻松。&lt;/p&gt;
&lt;p&gt;RAG 很热，本地优先也很好听，AI 知识库也很有想象空间。&lt;/p&gt;
&lt;p&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;li&gt;一个模型是否太慢。&lt;/li&gt;
&lt;li&gt;一个新用户能不能顺利跑起来。&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;这些东西不酷，但它们决定项目能不能从 Demo 往前走。&lt;/p&gt;
&lt;p&gt;项目现在还不成熟。&lt;/p&gt;
&lt;p&gt;但至少，它还在往前。&lt;/p&gt;
&lt;p&gt;这几天重新更新它的时候，我又找回了一点刚开始做它时的感觉：不是很确定，但挺想继续看看它能长成什么样。&lt;/p&gt;</content:encoded><category>AI</category><category>RAG</category><category>开源</category><category>随笔</category></item><item><title>软考高级架构师考试后的一个感受</title><link>https://blog.veyliss.top/blog/software_architect_exam_ai_reflection_2026/</link><guid isPermaLink="true">https://blog.veyliss.top/blog/software_architect_exam_ai_reflection_2026/</guid><pubDate>Sun, 24 May 2026 00:00:00 GMT</pubDate><content:encoded>&lt;p&gt;昨天，也就是 2026 年 5 月 23 日，去参加了软考高级系统架构设计师考试。&lt;/p&gt;
&lt;p&gt;考完之后最明显的感受是：这类考试真的在越来越快地贴近新的技术趋势。以前提到架构，更多想到的是分层、缓存、消息队列、高并发、数据库、微服务这些传统工程问题；但这次做题时，AI、模型、多模态这些内容已经很自然地出现在题目里了。&lt;/p&gt;
&lt;p&gt;这种感觉还挺直接的。&lt;/p&gt;
&lt;p&gt;不是那种“AI 作为热点，被强行塞进试卷”的感觉，而是它开始变成架构师应该了解的背景知识之一。&lt;/p&gt;
&lt;div&gt;&lt;h2 id=&quot;选择题整体还可以&quot;&gt;选择题：整体还可以&lt;/h2&gt;&lt;/div&gt;
&lt;p&gt;上午选择题做下来，感觉整体还可以。&lt;/p&gt;
&lt;p&gt;时间上比较充裕，没有那种一路卡住、最后疯狂赶题的感觉。很多题还是围绕软件工程、架构设计、数据库、网络、安全、项目管理这些基础内容展开，只要平时有积累，大多数题都能比较顺地往下做。&lt;/p&gt;
&lt;p&gt;比较有意思的是，题型很快就来到了 AI 和模型相关内容。&lt;/p&gt;
&lt;p&gt;印象里有一道和 Transformer 相关的题，也有一道涉及多模态的题。看到这些题的时候，会明显感觉到考试范围已经不只是传统软件架构知识了。&lt;/p&gt;
&lt;p&gt;这其实也合理。&lt;/p&gt;
&lt;p&gt;现在很多系统已经不只是“业务系统 + 数据库 + 缓存 + 接口”这么简单。越来越多项目会接入大模型能力，可能涉及文本生成、向量检索、多模态理解、智能测试、智能客服、知识库问答等场景。&lt;/p&gt;
&lt;p&gt;架构师如果完全不了解这些内容，后面做系统设计时确实会越来越吃力。&lt;/p&gt;
&lt;div&gt;&lt;h2 id=&quot;案例题对我来说还是有难度&quot;&gt;案例题：对我来说还是有难度&lt;/h2&gt;&lt;/div&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;
&lt;ul&gt;
&lt;li&gt;能不能看懂业务场景。&lt;/li&gt;
&lt;li&gt;能不能识别系统中的关键矛盾。&lt;/li&gt;
&lt;li&gt;能不能把架构方案和问题对应起来。&lt;/li&gt;
&lt;li&gt;能不能用比较规范的语言写出答案。&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;这和实际做架构也很像。真实工作里，知道某个技术名词没有太大意义，关键还是能不能把它放到合适的系统问题里。&lt;/p&gt;
&lt;div&gt;&lt;h2 id=&quot;ai-含量越来越高&quot;&gt;AI 含量越来越高&lt;/h2&gt;&lt;/div&gt;
&lt;p&gt;这次考试让我比较在意的一点，是 AI 相关内容的出现频率。&lt;/p&gt;
&lt;p&gt;选择题里出现了 Transformer、多模态，论文题里也直接出现了“向量数据库”和“多模态大模型在移动智能测试框架中的应用”。&lt;/p&gt;
&lt;p&gt;这说明考试已经不只是把 AI 当成一个新名词，而是在尝试把它放进架构设计语境里。&lt;/p&gt;
&lt;p&gt;比如向量数据库不是单独存在的知识点，它背后对应的是：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;文本向量化。&lt;/li&gt;
&lt;li&gt;相似度检索。&lt;/li&gt;
&lt;li&gt;RAG 检索增强生成。&lt;/li&gt;
&lt;li&gt;知识库问答。&lt;/li&gt;
&lt;li&gt;语义搜索。&lt;/li&gt;
&lt;li&gt;大模型应用的数据底座。&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;多模态大模型也不是简单知道“能处理图片和文本”就够了。它进入移动智能测试框架时，可能会涉及：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;UI 截图理解。&lt;/li&gt;
&lt;li&gt;测试步骤生成。&lt;/li&gt;
&lt;li&gt;异常页面识别。&lt;/li&gt;
&lt;li&gt;测试用例自动补全。&lt;/li&gt;
&lt;li&gt;文本、图像、操作行为的联合分析。&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;这些东西已经开始和软件工程、测试框架、系统架构结合起来了。&lt;/p&gt;
&lt;div&gt;&lt;h2 id=&quot;论文题四个方向都挺有代表性&quot;&gt;论文题：四个方向都挺有代表性&lt;/h2&gt;&lt;/div&gt;
&lt;p&gt;这次论文题是四选一，题目大概是：&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;六边形架构设计。&lt;/li&gt;
&lt;li&gt;向量数据库。&lt;/li&gt;
&lt;li&gt;论高并发系统设计。&lt;/li&gt;
&lt;li&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;向量数据库和多模态大模型则明显代表新趋势，考察的是架构师能不能把 AI 相关能力纳入系统设计。&lt;/p&gt;
&lt;p&gt;如果从稳妥角度看，高并发系统设计可能是很多人比较熟悉的方向。它素材多、案例多，也比较容易结合实际项目经验展开。&lt;/p&gt;
&lt;p&gt;但从趋势角度看，向量数据库和多模态大模型这两个题很值得重视。&lt;/p&gt;
&lt;p&gt;它们释放了一个信号：以后软考高级架构师可能会越来越多地考察 AI 时代下的软件架构能力。&lt;/p&gt;
&lt;div&gt;&lt;h2 id=&quot;考完之后的想法&quot;&gt;考完之后的想法&lt;/h2&gt;&lt;/div&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;ul&gt;
&lt;li&gt;Transformer 的基本概念。&lt;/li&gt;
&lt;li&gt;向量数据库和语义检索。&lt;/li&gt;
&lt;li&gt;RAG 应用架构。&lt;/li&gt;
&lt;li&gt;多模态模型的输入输出方式。&lt;/li&gt;
&lt;li&gt;AI 能力如何接入现有业务系统。&lt;/li&gt;
&lt;li&gt;模型服务的成本、延迟、稳定性和安全边界。&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;这些内容不一定都要学到算法研究层面，但作为架构师，至少要知道它们能做什么、不能做什么、适合放在系统里的哪个位置、会带来哪些工程风险。&lt;/p&gt;
&lt;div&gt;&lt;h2 id=&quot;小结&quot;&gt;小结&lt;/h2&gt;&lt;/div&gt;
&lt;p&gt;这次软考高级架构师考试给我的一个提醒是：&lt;/p&gt;
&lt;div&gt;&lt;figure&gt;&lt;figcaption&gt;&lt;/figcaption&gt;&lt;pre&gt;&lt;code&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;架构师的知识边界正在被 AI 拉宽。&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/figure&gt;&lt;/div&gt;
&lt;p&gt;选择题里出现 Transformer 和多模态，论文题里出现向量数据库和多模态大模型，这些都说明 AI 已经逐渐进入软件架构的主干知识里。&lt;/p&gt;
&lt;p&gt;对我来说，选择题感觉还可以，案例题仍然需要继续练。更重要的是，后面复习和学习时，不能只看传统架构内容，也要把 AI 工程化、大模型应用架构、向量检索和多模态场景补起来。&lt;/p&gt;
&lt;p&gt;考试只是一个节点。&lt;/p&gt;
&lt;p&gt;真正值得记录的，是它让我看到技术趋势已经走到试卷上了。&lt;/p&gt;</content:encoded><category>软考</category><category>架构</category><category>AI</category><category>随笔</category></item><item><title>大规模无状态爬虫系统设计</title><link>https://blog.veyliss.top/blog/stateless_crawler_system_review_01/</link><guid isPermaLink="true">https://blog.veyliss.top/blog/stateless_crawler_system_review_01/</guid><pubDate>Fri, 22 May 2026 00:00:00 GMT</pubDate><content:encoded>&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;div&gt;&lt;h2 id=&quot;为什么没有继续用-scrapy&quot;&gt;为什么没有继续用 Scrapy&lt;/h2&gt;&lt;/div&gt;
&lt;p&gt;在做这个项目之前，Scrapy 是很自然会被想到的方案。&lt;/p&gt;
&lt;p&gt;它有成熟的爬虫生命周期、调度器、下载器、中间件、管道、去重、状态管理等能力。对于中小规模、结构清晰、业务变化不频繁的采集任务来说，Scrapy 确实是一套完整方案。&lt;/p&gt;
&lt;p&gt;但这套系统面对的问题不太一样。&lt;/p&gt;
&lt;p&gt;我们更关注的是大规模任务下的采集吞吐、任务调度、账号分配、异常处理和快速扩容。Scrapy 自带的体系虽然完整，但学习成本较高，入手较慢，架构也相对复杂。尤其当系统需要把任务状态、账号状态、异常流转、代理分配、补偿处理这些能力统一放到一个调度中心管理时，爬虫本身再保留太多状态，反而会让边界变得不清楚。&lt;/p&gt;
&lt;p&gt;所以最后的方向是：不沿用 Scrapy 的架构模式，而是结合现有高并发框架，设计一套更轻、更快、更容易水平扩展的无状态爬虫系统。&lt;/p&gt;
&lt;p&gt;爬虫核心只负责一件事：拿到任务后尽快完成数据抓取，包括必要的增量更新，然后把结果交给后续链路。&lt;/p&gt;
&lt;div&gt;&lt;h2 id=&quot;系统整体结构&quot;&gt;系统整体结构&lt;/h2&gt;&lt;/div&gt;
&lt;p&gt;按当时的架构草图抽象后，整体链路大概是这样：&lt;/p&gt;
&lt;figure&gt;&lt;img src=&quot;https://blog.veyliss.top/diagrams/stateless-crawler-architecture.svg&quot; alt=&quot;大规模无状态爬虫系统架构图&quot; loading=&quot;lazy&quot;&gt;&lt;figcaption&gt;任务、账号、代理由调度中心统一下发；爬虫节点保持无状态，采集结果进入 Kafka、Flink、ES 数据链路。&lt;/figcaption&gt;&lt;/figure&gt;
&lt;p&gt;在这个体系里，Java 服务承担调度中心的角色。它负责任务协调、账号分配、账号状态管理、异常状态流转、代理下发等能力。&lt;/p&gt;
&lt;p&gt;爬虫端则被刻意设计得很薄。&lt;/p&gt;
&lt;p&gt;爬虫启动后向调度中心领取任务。调度中心在下发任务时，会同时给出这次采集所需的账号和代理。爬虫拿到这些一次性上下文后开始采集，采集完成后把数据写入 Kafka，并向调度中心汇报任务结果和心跳状态。&lt;/p&gt;
&lt;p&gt;这里的“一次性”不是指账号用一次就丢弃，而是指一次采集任务内绑定一次任务、账号和代理。任务结束后，账号会根据结果重新回到有效账号池，或者进入异常账号池，等待专门的登录模块重新处理。&lt;/p&gt;
&lt;div&gt;&lt;h2 id=&quot;为什么要让爬虫无状态&quot;&gt;为什么要让爬虫无状态&lt;/h2&gt;&lt;/div&gt;
&lt;p&gt;我觉得这套系统里最关键的设计，就是把爬虫做成无状态。&lt;/p&gt;
&lt;p&gt;传统爬虫经常会在自己内部维护很多信息：当前任务跑到哪一步、账号是否可用、代理是否失效、失败后要不要重试、异常应该怎么处理、下次从哪里继续等。&lt;/p&gt;
&lt;p&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;li&gt;错误处理混在采集逻辑里，爬虫代码越来越难维护。&lt;/li&gt;
&lt;/ul&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;div&gt;&lt;h2 id=&quot;单次任务为什么能做到很快&quot;&gt;单次任务为什么能做到很快&lt;/h2&gt;&lt;/div&gt;
&lt;p&gt;这套系统里，爬虫单次采集任务大约 15 秒左右就可以完成。&lt;/p&gt;
&lt;p&gt;它能快起来，原因不只是“并发写得高”，更重要的是系统边界清楚。&lt;/p&gt;
&lt;p&gt;调度中心已经提前准备好了任务、账号和代理，爬虫不需要在执行过程中再做大量决策。拿到任务后，爬虫可以直接进入采集流程。它只处理当前任务所需的请求、解析、增量判断和结果投递。&lt;/p&gt;
&lt;p&gt;采集结果进入 Kafka 后，后面的清洗、聚合、存储交给 Flink 和 ES 链路。爬虫不在本地做过多处理，也不会把数据链路和采集链路耦合在一起。&lt;/p&gt;
&lt;p&gt;从工程上看，这其实是在减少爬虫节点的职责。&lt;/p&gt;
&lt;p&gt;节点职责越少，单次任务越短，失败成本也越低。即使某个任务失败，也可以快速上报并进入调度中心的异常处理流程，而不是让爬虫自己在本地反复纠缠。&lt;/p&gt;
&lt;div&gt;&lt;h2 id=&quot;为什么用-docker-部署爬虫&quot;&gt;为什么用 Docker 部署爬虫&lt;/h2&gt;&lt;/div&gt;
&lt;p&gt;爬虫选择 Docker 部署，是因为这个系统天然需要横向扩容。&lt;/p&gt;
&lt;p&gt;如果爬虫直接跑在固定机器上，扩容会比较麻烦。新机器环境要配置，依赖要安装，版本要对齐，启动方式也容易不一致。Docker 把运行环境打包后，爬虫就可以在任意一台机器上快速启动。&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;任务高峰期增加爬虫节点，任务低谷期减少节点。爬虫节点本身不保存长期状态，所以扩容和缩容都比较自然。&lt;/p&gt;
&lt;p&gt;这也是无状态设计和容器化部署非常契合的地方：一个节点随时可以来，也随时可以走，系统的长期状态不依赖它。&lt;/p&gt;
&lt;div&gt;&lt;h2 id=&quot;为什么架构图里没有代理模块&quot;&gt;为什么架构图里没有代理模块&lt;/h2&gt;&lt;/div&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;
&lt;p&gt;这让异常处理有了更清晰的入口。&lt;/p&gt;
&lt;div&gt;&lt;h2 id=&quot;账号池和异常流转&quot;&gt;账号池和异常流转&lt;/h2&gt;&lt;/div&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;
&lt;ul&gt;
&lt;li&gt;爬虫负责发现和上报异常。&lt;/li&gt;
&lt;li&gt;调度中心负责状态流转和资源分配。&lt;/li&gt;
&lt;li&gt;账号登录模块负责账号恢复。&lt;/li&gt;
&lt;li&gt;有效账号池只保留可用于任务分配的账号。&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;当系统规模变大时，这种职责拆分会比“爬虫自己判断一切”更稳。&lt;/p&gt;
&lt;div&gt;&lt;h2 id=&quot;数据链路kafkaflink-和-es&quot;&gt;数据链路：Kafka、Flink 和 ES&lt;/h2&gt;&lt;/div&gt;
&lt;p&gt;爬虫采集到的数据不会直接写入最终存储，而是先进入 Kafka。&lt;/p&gt;
&lt;p&gt;Kafka 在这里承担缓冲和解耦作用。爬虫只需要稳定地把采集结果投递出去，不需要关心后续清洗、转换和索引写入的具体细节。&lt;/p&gt;
&lt;p&gt;Flink 负责消费 Kafka 中的数据，做实时清洗、转换、去重或补充处理。处理后的数据再写入 ES，供后续检索和查询使用。&lt;/p&gt;
&lt;p&gt;这条链路的好处是采集和处理分离。&lt;/p&gt;
&lt;p&gt;爬虫节点只追求采集效率，数据处理链路则可以按自己的节奏扩展。如果后续清洗逻辑变复杂，也不会直接拖慢爬虫侧的执行。&lt;/p&gt;
&lt;div&gt;&lt;h2 id=&quot;设计总结&quot;&gt;设计总结&lt;/h2&gt;&lt;/div&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;
&lt;p&gt;我开始意识到，架构设计不是把所有能力都堆进一个模块里，而是决定每个模块应该知道什么、不应该知道什么。一个模块越清楚自己不负责什么，边界往往越稳定。&lt;/p&gt;
&lt;p&gt;回头看，这套无状态爬虫体系最让我印象深刻的地方，就是它把复杂性从爬虫端拿走了。&lt;/p&gt;
&lt;p&gt;爬虫只领取任务、执行采集、上报结果；调度中心统一管理任务、账号、代理和异常；数据进入 Kafka、Flink、ES 组成的后续链路。每一层都有自己的职责，每一层也都可以独立扩展。&lt;/p&gt;
&lt;p&gt;这比单纯写一个“能跑的爬虫”要更接近真正的工程系统。&lt;/p&gt;</content:encoded><category>博客</category><category>爬虫</category><category>架构</category></item><item><title>redis核心原理与实战应用</title><link>https://blog.veyliss.top/blog/redis/</link><guid isPermaLink="true">https://blog.veyliss.top/blog/redis/</guid><pubDate>Wed, 20 May 2026 00:00:00 GMT</pubDate><content:encoded>&lt;p&gt;Redis 常被用作缓存、分布式锁、排行榜、计数器和会话存储。它的核心优势不是“能存数据”这么简单，而是用内存访问、丰富数据结构、持久化和高可用机制，支撑高并发场景下的读写性能与系统稳定性。&lt;/p&gt;
&lt;p&gt;这篇文章按实际使用路径整理 Redis：先看基础能力，再看高可用与缓存问题，最后整理一致性、性能优化和典型场景。&lt;/p&gt;
&lt;div&gt;&lt;h2 id=&quot;一redis-基础概念&quot;&gt;一、Redis 基础概念&lt;/h2&gt;&lt;/div&gt;
&lt;div&gt;&lt;h3 id=&quot;数据类型支持&quot;&gt;数据类型支持&lt;/h3&gt;&lt;/div&gt;
&lt;p&gt;Redis 支持多种常用数据结构：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;String&lt;/strong&gt;：适合缓存简单值、计数器、分布式锁标记。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Hash&lt;/strong&gt;：适合存储对象字段，例如用户信息。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;List&lt;/strong&gt;：适合队列、消息列表、时间线等场景。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Set&lt;/strong&gt;：适合去重、集合交并差。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Sorted Set（ZSet）&lt;/strong&gt;：适合排行榜、权重排序、延迟队列。&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;这些结构让 Redis 不只是一个 Key-Value 缓存，而是可以承接一部分高频读写业务逻辑。&lt;/p&gt;
&lt;div&gt;&lt;h3 id=&quot;持久化机制&quot;&gt;持久化机制&lt;/h3&gt;&lt;/div&gt;
&lt;p&gt;Redis 主要有两种持久化方式：RDB 和 AOF。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;RDB（快照）&lt;/strong&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;定期生成内存快照，例如通过 &lt;code dir=&quot;auto&quot;&gt;bgsave&lt;/code&gt; 命令。&lt;/li&gt;
&lt;li&gt;优点：文件体积较小，数据恢复速度快。&lt;/li&gt;
&lt;li&gt;缺点：如果 Redis 异常退出，可能丢失最后一次快照之后的数据。&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;strong&gt;AOF（追加日志）&lt;/strong&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;记录每一次写操作命令，例如 &lt;code dir=&quot;auto&quot;&gt;SET key value&lt;/code&gt;。&lt;/li&gt;
&lt;li&gt;优点：数据安全性更高，可以配置更高频率的落盘。&lt;/li&gt;
&lt;li&gt;缺点：文件体积更大，恢复速度通常慢于 RDB。&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;实际生产中常见策略是 &lt;strong&gt;RDB + AOF 组合使用&lt;/strong&gt;：RDB 用于快速恢复，AOF 用于降低数据丢失风险。&lt;/p&gt;
&lt;div&gt;&lt;h2 id=&quot;二主从同步与高可用&quot;&gt;二、主从同步与高可用&lt;/h2&gt;&lt;/div&gt;
&lt;div&gt;&lt;h3 id=&quot;主从同步方式&quot;&gt;主从同步方式&lt;/h3&gt;&lt;/div&gt;
&lt;p&gt;Redis 主从同步分为全量同步和增量同步。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;全量同步&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;通常发生在从节点初次连接主节点，或断线时间过长无法增量追赶时：&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;主节点生成 RDB 文件。&lt;/li&gt;
&lt;li&gt;主节点把 RDB 文件发送给从节点。&lt;/li&gt;
&lt;li&gt;从节点清空旧数据并加载 RDB。&lt;/li&gt;
&lt;li&gt;主节点继续把同步期间缓冲区中的写操作发送给从节点。&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;&lt;strong&gt;增量同步&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;当从节点短暂断线后重新连接，可以通过 &lt;code dir=&quot;auto&quot;&gt;psync&lt;/code&gt; 根据复制偏移量（offset）同步缺失的写操作，避免重新做一次全量同步。&lt;/p&gt;
&lt;div&gt;&lt;h3 id=&quot;高可用实现&quot;&gt;高可用实现&lt;/h3&gt;&lt;/div&gt;
&lt;p&gt;Redis 常见高可用方案主要有两类：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;哨兵模式（Sentinel）&lt;/strong&gt;：监控主从节点状态，在主节点故障时自动完成故障转移。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;集群模式（Cluster）&lt;/strong&gt;：通过分片存储数据，提升容量上限，并支持水平扩展。&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;哨兵更偏向主从高可用，集群更偏向容量扩展和分片治理。&lt;/p&gt;
&lt;div&gt;&lt;h2 id=&quot;三缓存问题与解决方案&quot;&gt;三、缓存问题与解决方案&lt;/h2&gt;&lt;/div&gt;
&lt;div&gt;&lt;h3 id=&quot;缓存击穿&quot;&gt;缓存击穿&lt;/h3&gt;&lt;/div&gt;
&lt;p&gt;缓存击穿指的是一个热点 Key 在高并发访问时突然过期，大量请求同时打到数据库。&lt;/p&gt;
&lt;p&gt;常见解决方案：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;热点 Key 不设置过期时间&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;div&gt;&lt;h3 id=&quot;缓存穿透&quot;&gt;缓存穿透&lt;/h3&gt;&lt;/div&gt;
&lt;p&gt;缓存穿透指的是大量请求访问不存在的数据，缓存无法命中，请求直接落到数据库。&lt;/p&gt;
&lt;p&gt;常见解决方案：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;布隆过滤器&lt;/strong&gt;：在请求进入缓存和数据库之前过滤明显不存在的 Key。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;缓存空对象&lt;/strong&gt;：对不存在的数据短时间缓存空值，避免重复打到数据库。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;参数校验&lt;/strong&gt;：拦截非法 ID、异常参数和明显无效请求。&lt;/li&gt;
&lt;/ul&gt;
&lt;div&gt;&lt;h3 id=&quot;缓存雪崩&quot;&gt;缓存雪崩&lt;/h3&gt;&lt;/div&gt;
&lt;p&gt;缓存雪崩指的是大量 Key 在同一时间过期，导致请求集中打到数据库。&lt;/p&gt;
&lt;p&gt;常见解决方案：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;随机过期时间&lt;/strong&gt;，避免大量 Key 同时失效。&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;div&gt;&lt;h2 id=&quot;四数据一致性与-redis-mysql-同步&quot;&gt;四、数据一致性与 Redis-MySQL 同步&lt;/h2&gt;&lt;/div&gt;
&lt;div&gt;&lt;h3 id=&quot;双写一致性方案&quot;&gt;双写一致性方案&lt;/h3&gt;&lt;/div&gt;
&lt;p&gt;业务中常见的数据源组合是 MySQL 存储主数据，Redis 作为缓存。这里的核心问题是：数据库更新后，缓存如何保持一致。&lt;/p&gt;
&lt;p&gt;常见方案：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;先写 MySQL，再更新 Redis&lt;/strong&gt;：逻辑直接，但并发下可能出现旧值覆盖新值。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;先写 MySQL，再删除 Redis&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;：通过消息队列或 Canal 监听 MySQL Binlog，把数据变更异步同步到 Redis。&lt;/li&gt;
&lt;/ul&gt;
&lt;div&gt;&lt;h3 id=&quot;业务场景适配&quot;&gt;业务场景适配&lt;/h3&gt;&lt;/div&gt;
&lt;ul&gt;
&lt;li&gt;对实时一致性要求很高的场景，需要更严格的事务或强一致方案。&lt;/li&gt;
&lt;li&gt;对短暂延迟可接受的场景，通常使用缓存删除、消息队列、Binlog 同步等最终一致性方案。&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;缓存一致性没有绝对通用答案，重点是结合业务对“延迟、正确性、复杂度”的要求做取舍。&lt;/p&gt;
&lt;div&gt;&lt;h2 id=&quot;五redis-性能优化&quot;&gt;五、Redis 性能优化&lt;/h2&gt;&lt;/div&gt;
&lt;div&gt;&lt;h3 id=&quot;慢查询排查&quot;&gt;慢查询排查&lt;/h3&gt;&lt;/div&gt;
&lt;p&gt;Redis 提供 &lt;code dir=&quot;auto&quot;&gt;slowlog&lt;/code&gt; 用于排查慢查询。常见优化方向：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;避免 &lt;code dir=&quot;auto&quot;&gt;KEYS *&lt;/code&gt;、&lt;code dir=&quot;auto&quot;&gt;SORT&lt;/code&gt;、大范围 &lt;code dir=&quot;auto&quot;&gt;SUNION&lt;/code&gt; 等高复杂度命令。&lt;/li&gt;
&lt;li&gt;对批量操作使用 Pipeline，减少网络往返。&lt;/li&gt;
&lt;li&gt;控制单次命令处理的数据规模，避免阻塞主线程。&lt;/li&gt;
&lt;/ul&gt;
&lt;div&gt;&lt;h3 id=&quot;内存淘汰策略&quot;&gt;内存淘汰策略&lt;/h3&gt;&lt;/div&gt;
&lt;p&gt;当内存达到 &lt;code dir=&quot;auto&quot;&gt;maxmemory&lt;/code&gt; 限制时，Redis 会根据配置的淘汰策略处理 Key。&lt;/p&gt;
&lt;p&gt;常见策略：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;noeviction&lt;/strong&gt;：内存不足时拒绝写入。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;volatile-lru&lt;/strong&gt;：只在设置了过期时间的 Key 中淘汰最近最少使用的 Key。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;volatile-lfu&lt;/strong&gt;：只在设置了过期时间的 Key 中淘汰最近最不常用的 Key。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;allkeys-lru&lt;/strong&gt;：在所有 Key 中淘汰最近最少使用的 Key。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;allkeys-lfu&lt;/strong&gt;：在所有 Key 中淘汰最近最不常用的 Key。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;allkeys-random&lt;/strong&gt;：在所有 Key 中随机淘汰。&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;缓存场景通常更常见 &lt;code dir=&quot;auto&quot;&gt;allkeys-lru&lt;/code&gt; 或 &lt;code dir=&quot;auto&quot;&gt;allkeys-lfu&lt;/code&gt;，但具体选择要看访问分布和业务容忍度。&lt;/p&gt;
&lt;div&gt;&lt;h3 id=&quot;bigkey-与-hotkey&quot;&gt;BigKey 与 HotKey&lt;/h3&gt;&lt;/div&gt;
&lt;p&gt;&lt;strong&gt;BigKey&lt;/strong&gt; 指单个 Key 占用过大内存或包含过多元素，会导致网络传输、删除、迁移和持久化变慢。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;HotKey&lt;/strong&gt; 指少数 Key 被高频访问，容易造成单点压力。&lt;/p&gt;
&lt;p&gt;优化方向：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;拆分过大的 Hash、List、Set、ZSet。&lt;/li&gt;
&lt;li&gt;避免一次性读取或删除大 Key。&lt;/li&gt;
&lt;li&gt;对热点数据做本地缓存、多副本缓存或请求合并。&lt;/li&gt;
&lt;/ul&gt;
&lt;div&gt;&lt;h2 id=&quot;六线程模型与内存管理&quot;&gt;六、线程模型与内存管理&lt;/h2&gt;&lt;/div&gt;
&lt;div&gt;&lt;h3 id=&quot;单线程模型&quot;&gt;单线程模型&lt;/h3&gt;&lt;/div&gt;
&lt;p&gt;Redis 的命令执行主要是单线程模型，这让它避免了大量锁竞争，命令执行顺序也更容易理解。&lt;/p&gt;
&lt;p&gt;Redis 6.0 之后引入多线程处理网络 I/O，但命令执行本身仍然保持单线程语义。因此，慢命令、大 Key 操作、复杂聚合仍然可能阻塞 Redis。&lt;/p&gt;
&lt;div&gt;&lt;h3 id=&quot;过期键删除策略&quot;&gt;过期键删除策略&lt;/h3&gt;&lt;/div&gt;
&lt;p&gt;Redis 对过期 Key 主要使用两种删除策略：&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;惰性删除&lt;/strong&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;获取 Key 时检查是否过期，过期则删除。&lt;/li&gt;
&lt;li&gt;优点：不主动消耗额外资源。&lt;/li&gt;
&lt;li&gt;缺点：如果过期 Key 长时间不被访问，可能继续占用内存。&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;strong&gt;定期删除&lt;/strong&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Redis 定期随机抽样部分 Key，删除其中已经过期的 Key。&lt;/li&gt;
&lt;li&gt;优点：可以主动清理过期数据。&lt;/li&gt;
&lt;li&gt;缺点：不保证所有过期 Key 都会立刻被删除。&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;Redis 默认使用 &lt;strong&gt;惰性删除 + 定期删除&lt;/strong&gt; 的组合策略。&lt;/p&gt;
&lt;div&gt;&lt;h2 id=&quot;七redis-变慢的排查方向&quot;&gt;七、Redis 变慢的排查方向&lt;/h2&gt;&lt;/div&gt;
&lt;p&gt;Redis 变慢时，可以从以下方向排查：&lt;/p&gt;
&lt;div&gt;&lt;h3 id=&quot;硬件资源&quot;&gt;硬件资源&lt;/h3&gt;&lt;/div&gt;
&lt;ul&gt;
&lt;li&gt;检查内存是否耗尽，是否发生 Swap，例如使用 &lt;code dir=&quot;auto&quot;&gt;free -m&lt;/code&gt;。&lt;/li&gt;
&lt;li&gt;检查磁盘 I/O，尤其是 AOF 写入和重写期间。&lt;/li&gt;
&lt;li&gt;使用 SSD 替代传统磁盘，降低持久化带来的 I/O 压力。&lt;/li&gt;
&lt;/ul&gt;
&lt;div&gt;&lt;h3 id=&quot;命令复杂度&quot;&gt;命令复杂度&lt;/h3&gt;&lt;/div&gt;
&lt;ul&gt;
&lt;li&gt;排查是否使用了 &lt;code dir=&quot;auto&quot;&gt;KEYS&lt;/code&gt;、&lt;code dir=&quot;auto&quot;&gt;SORT&lt;/code&gt;、&lt;code dir=&quot;auto&quot;&gt;SUNION&lt;/code&gt; 等高复杂度命令。&lt;/li&gt;
&lt;li&gt;检查是否存在大批量读取、大批量删除或大集合遍历。&lt;/li&gt;
&lt;li&gt;对批量请求使用 Pipeline，减少网络延迟。&lt;/li&gt;
&lt;/ul&gt;
&lt;div&gt;&lt;h3 id=&quot;aof-重写阻塞&quot;&gt;AOF 重写阻塞&lt;/h3&gt;&lt;/div&gt;
&lt;p&gt;AOF 重写期间可能与 &lt;code dir=&quot;auto&quot;&gt;fsync&lt;/code&gt; 竞争磁盘资源。可以根据场景配置：&lt;/p&gt;
&lt;div&gt;&lt;figure&gt;&lt;figcaption&gt;&lt;/figcaption&gt;&lt;pre&gt;&lt;code&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;no-appendfsync-on-rewrite yes&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/figure&gt;&lt;/div&gt;
&lt;p&gt;这个配置可以降低 AOF 重写期间的写入阻塞风险，但也会增加极端情况下的数据丢失窗口，需要结合业务容忍度评估。&lt;/p&gt;
&lt;div&gt;&lt;h3 id=&quot;缓存雪崩或击穿&quot;&gt;缓存雪崩或击穿&lt;/h3&gt;&lt;/div&gt;
&lt;p&gt;如果数据库和 Redis 同时出现压力波动，需要排查是否存在：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;大量 Key 同时过期。&lt;/li&gt;
&lt;li&gt;热点 Key 突然失效。&lt;/li&gt;
&lt;li&gt;缓存重建逻辑没有互斥保护。&lt;/li&gt;
&lt;/ul&gt;
&lt;div&gt;&lt;h2 id=&quot;八redis-vs-memcached&quot;&gt;八、Redis vs Memcached&lt;/h2&gt;&lt;/div&gt;








































&lt;table&gt;&lt;thead&gt;&lt;tr&gt;&lt;th&gt;特性&lt;/th&gt;&lt;th&gt;Redis&lt;/th&gt;&lt;th&gt;Memcached&lt;/th&gt;&lt;/tr&gt;&lt;/thead&gt;&lt;tbody&gt;&lt;tr&gt;&lt;td&gt;数据类型&lt;/td&gt;&lt;td&gt;String、Hash、List、Set、ZSet 等&lt;/td&gt;&lt;td&gt;主要支持简单 Key-Value&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;持久化&lt;/td&gt;&lt;td&gt;支持 RDB、AOF&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;支持 LRU 等缓存淘汰机制&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;原子操作&lt;/td&gt;&lt;td&gt;支持较丰富的原子操作&lt;/td&gt;&lt;td&gt;支持有限原子操作&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;线程模型&lt;/td&gt;&lt;td&gt;命令执行以单线程为主，Redis 6.0 后支持 I/O 多线程&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;简单缓存、高吞吐 Key-Value&lt;/td&gt;&lt;/tr&gt;&lt;/tbody&gt;&lt;/table&gt;
&lt;p&gt;简单说：如果只是做非常简单的缓存，Memcached 也可以胜任；如果需要更丰富的数据结构、持久化、高可用和分布式能力，Redis 更适合。&lt;/p&gt;
&lt;div&gt;&lt;h2 id=&quot;九典型场景应用&quot;&gt;九、典型场景应用&lt;/h2&gt;&lt;/div&gt;
&lt;p&gt;Redis 常见业务场景包括：&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;：存储用户 Session 或登录态。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;排行榜&lt;/strong&gt;：通过 ZSet 实现 Top N 榜单。&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;code dir=&quot;auto&quot;&gt;SET key value NX PX&lt;/code&gt; 等命令实现基础锁能力。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;延迟队列&lt;/strong&gt;：通过 ZSet 分数存储执行时间，实现定时任务调度。&lt;/li&gt;
&lt;/ul&gt;
&lt;div&gt;&lt;h2 id=&quot;总结&quot;&gt;总结&lt;/h2&gt;&lt;/div&gt;
&lt;p&gt;Redis 的核心价值在于 &lt;strong&gt;高性能访问、灵活数据结构、持久化能力和分布式支持&lt;/strong&gt;。&lt;/p&gt;
&lt;p&gt;实际使用 Redis 时，需要重点关注四件事：&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;数据结构是否选对。&lt;/li&gt;
&lt;li&gt;缓存异常场景是否有兜底方案。&lt;/li&gt;
&lt;li&gt;Redis 与数据库之间是否能接受最终一致性。&lt;/li&gt;
&lt;li&gt;是否避免了慢命令、BigKey、HotKey 和集中过期。&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;理解这些核心机制后，Redis 就不只是一个缓存组件，而是高并发系统中非常重要的性能与稳定性基础设施。&lt;/p&gt;</content:encoded><category>redis</category><category>数据库</category><category>sql</category></item><item><title>接触 Vibe Coding 八个多月后的感受</title><link>https://blog.veyliss.top/blog/vibe_coding_eight_months/</link><guid isPermaLink="true">https://blog.veyliss.top/blog/vibe_coding_eight_months/</guid><pubDate>Wed, 20 May 2026 00:00:00 GMT</pubDate><content:encoded>&lt;p&gt;接触 Vibe Coding 已经八个多月了。&lt;/p&gt;
&lt;p&gt;回头看，这段时间给我带来的变化非常大。它不只是让我多认识了一些 AI 工具，也不只是让我写代码的速度变快了。更重要的是，它改变了我看待开发这件事的方式。&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;这就是 Vibe Coding 对我最大的影响。&lt;/p&gt;
&lt;div&gt;&lt;h2 id=&quot;从开发辅助开始&quot;&gt;从开发辅助开始&lt;/h2&gt;&lt;/div&gt;
&lt;p&gt;在更早的时候，我使用 AI 的方式其实很简单。&lt;/p&gt;
&lt;p&gt;去年的上半年，AI 对我来说更多还是一个开发辅助工具。它主要停留在 Web 式的 Chat 形态里，我会问它一些问题，让它帮我解释概念、检索资料、分析报错、生成一些代码片段。&lt;/p&gt;
&lt;p&gt;那个阶段的 AI 很像一个随时在线的助手。&lt;/p&gt;
&lt;p&gt;它能帮我查东西，也能帮我补充思路，但大多数时候，真正的开发过程还是由我自己主导。我要自己拆任务、自己打开项目、自己修改文件、自己调试和验证。&lt;/p&gt;
&lt;p&gt;AI 参与了过程，但没有真正进入开发工作流的中心。&lt;/p&gt;
&lt;p&gt;那时候我对它的理解也很朴素：它可以提高效率，可以减少搜索成本，可以帮我更快理解一些不熟悉的知识。&lt;/p&gt;
&lt;p&gt;但我还没有意识到，它会在后面改变整个开发方式。&lt;/p&gt;
&lt;div&gt;&lt;h2 id=&quot;agent-概念开始爆发&quot;&gt;Agent 概念开始爆发&lt;/h2&gt;&lt;/div&gt;
&lt;p&gt;后来，Agent 的概念越来越火。&lt;/p&gt;
&lt;p&gt;我开始看到各种 CLI 工具出现，也开始频繁听到 &lt;code dir=&quot;auto&quot;&gt;token&lt;/code&gt;、上下文、模型网关、提示词、工具调用、代码代理这些词。AI 不再只是一个聊天窗口，它开始进入终端、进入编辑器、进入项目目录，甚至可以直接阅读代码、修改文件、运行命令、检查结果。&lt;/p&gt;
&lt;p&gt;这和以前完全不一样。&lt;/p&gt;
&lt;p&gt;以前是我把问题复制给 AI，然后把答案再搬回项目里。现在则更像是 AI 直接坐进了项目现场，和我一起看代码、改代码、验证代码。&lt;/p&gt;
&lt;p&gt;这时我也开始加入 Vibe Coding 的行列。&lt;/p&gt;
&lt;p&gt;刚开始的时候，我其实并不知道这些工具应该怎么用。面对各种模型、API、CLI、代理配置，我会有点茫然。它们看起来都很强，但真正落到自己的项目里，还是需要一段适应过程。&lt;/p&gt;
&lt;p&gt;我需要理解它们的边界。&lt;/p&gt;
&lt;p&gt;哪些事情可以交给它？哪些事情必须自己判断？什么时候应该让它改代码？什么时候只是让它分析？上下文应该怎么给？任务应该怎么拆？&lt;/p&gt;
&lt;p&gt;这些并不是看一篇教程就能立刻掌握的。&lt;/p&gt;
&lt;div&gt;&lt;h2 id=&quot;工具链慢慢成形&quot;&gt;工具链慢慢成形&lt;/h2&gt;&lt;/div&gt;
&lt;p&gt;后来我逐渐知道了 New API 这类整合型网关，也开始理解它们在 AI 工作流中的意义。&lt;/p&gt;
&lt;p&gt;模型越来越多，不同模型有不同能力、价格和使用限制。如果每一个工具都单独配置，就会很分散。整合型网关的意义在于，它能把不同模型入口统一起来，让工具调用变得更稳定，也更容易管理。&lt;/p&gt;
&lt;p&gt;再后来，我接触到了 Claude Code 这类工具。&lt;/p&gt;
&lt;p&gt;它让我真正感受到“AI 参与编码”这件事和普通问答的区别。&lt;/p&gt;
&lt;p&gt;普通问答更像是你问一句，它答一句。CLI 编码工具则更像是你把它放进项目里，它可以沿着任务往前走：阅读文件、理解结构、修改代码、运行检查、再根据结果继续调整。&lt;/p&gt;
&lt;p&gt;这时候，AI 就不只是回答问题，而是在参与完成工作。&lt;/p&gt;
&lt;p&gt;当然，这并不意味着我可以完全放手。&lt;/p&gt;
&lt;p&gt;相反，我越来越感觉到，使用这类工具时，人要承担更高层次的判断。你要知道目标是什么，知道验收标准是什么，知道哪里不能乱动，知道生成的代码是否符合项目长期维护的方向。&lt;/p&gt;
&lt;p&gt;AI 可以很快，但方向仍然要由人来定。&lt;/p&gt;
&lt;div&gt;&lt;h2 id=&quot;从关注代码到关注产品&quot;&gt;从关注代码到关注产品&lt;/h2&gt;&lt;/div&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;
&lt;p&gt;因为当 AI 可以承担大量具体编码工作后，我的注意力就被释放出来了。我不再需要把所有精力都压在每一行代码上，而是可以站得稍微高一点，看整个产品的结构和体验。&lt;/p&gt;
&lt;p&gt;这并不是说技术不重要。&lt;/p&gt;
&lt;p&gt;技术仍然重要，而且越到后面越重要。只是技术不再是唯一中心。它更像是实现产品目标的手段，而不是最终目的。&lt;/p&gt;
&lt;p&gt;以前我可能会因为某个技术点很有意思就想做点东西。现在我会先问：这个东西解决了什么问题？它对用户、对内容、对长期维护有什么价值？&lt;/p&gt;
&lt;div&gt;&lt;h2 id=&quot;业务流程变得更重要&quot;&gt;业务流程变得更重要&lt;/h2&gt;&lt;/div&gt;
&lt;p&gt;Vibe Coding 也让我更关注业务流程。&lt;/p&gt;
&lt;p&gt;一个功能不是孤立存在的。它前面有入口，后面有结果，中间有状态变化和用户决策。只把某个页面写出来，并不代表功能真的完成。&lt;/p&gt;
&lt;p&gt;比如一个博客站，不只是能展示文章就够了。&lt;/p&gt;
&lt;p&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;li&gt;列表页如何让读者快速判断是否要点进去。&lt;/li&gt;
&lt;li&gt;文章页如何让阅读体验稳定。&lt;/li&gt;
&lt;li&gt;部署后如何只专注维护内容。&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;这些都不是单纯的代码问题，而是产品流程问题。&lt;/p&gt;
&lt;p&gt;当 AI 能帮我更快完成具体实现后，我反而会花更多时间思考这些流程是否合理。&lt;/p&gt;
&lt;p&gt;我会更在意一个功能放在系统里是不是自然，一个页面是不是为后续内容增长留好了空间，一个交互是不是符合读者直觉。&lt;/p&gt;
&lt;p&gt;这其实是更接近产品视角的思考。&lt;/p&gt;
&lt;div&gt;&lt;h2 id=&quot;ai-让我更聚焦结果&quot;&gt;AI 让我更聚焦结果&lt;/h2&gt;&lt;/div&gt;
&lt;p&gt;这八个多月里，我最大的感受是：AI 把很多“执行层面的阻力”变小了。&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;以前更像是先想很久，再动手实现。现在更像是先做出一个版本，然后不断观察、调整、迭代。&lt;/p&gt;
&lt;p&gt;AI 给我的不是简单的偷懒，而是更短的反馈周期。&lt;/p&gt;
&lt;p&gt;当反馈周期变短，人就更容易围绕结果做判断，而不是长时间停留在假设里。&lt;/p&gt;
&lt;div&gt;&lt;h2 id=&quot;但人依然是关键&quot;&gt;但人依然是关键&lt;/h2&gt;&lt;/div&gt;
&lt;p&gt;使用 Vibe Coding 越久，我越觉得人并没有变得不重要。&lt;/p&gt;
&lt;p&gt;相反，人变得更重要。&lt;/p&gt;
&lt;p&gt;因为 AI 可以生成代码，但它不一定知道什么是适合你的。它可以给出方案，但它不知道你真正想要的产品气质。它可以完成任务，但它不会天然理解你的长期规划。&lt;/p&gt;
&lt;p&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;li&gt;控制范围。&lt;/li&gt;
&lt;li&gt;验收结果。&lt;/li&gt;
&lt;li&gt;维护产品方向。&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;如果没有这些判断，AI 很容易把事情做得很快，但不一定做得正确。&lt;/p&gt;
&lt;p&gt;这也是我慢慢学到的一点：Vibe Coding 不是随便让 AI 写代码，而是学会用清晰的目标和上下文引导它，把人的判断和 AI 的执行力结合起来。&lt;/p&gt;
&lt;div&gt;&lt;h2 id=&quot;对我的改变&quot;&gt;对我的改变&lt;/h2&gt;&lt;/div&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;
&lt;p&gt;第三，我对学习 AI 相关知识更有动力。&lt;/p&gt;
&lt;p&gt;从模型到上下文，从 CLI 工具到网关，从提示词到任务拆解，这些内容不再只是概念，而是会真实影响我每天的开发方式。&lt;/p&gt;
&lt;p&gt;第四，我开始更相信个人项目的可能性。&lt;/p&gt;
&lt;p&gt;以前一个人做完整产品会觉得很重。现在虽然仍然不轻松，但至少很多原本消耗人的细节可以被 AI 分担。一个人的上限，正在被工具重新拉高。&lt;/p&gt;
&lt;div&gt;&lt;h2 id=&quot;写在最后&quot;&gt;写在最后&lt;/h2&gt;&lt;/div&gt;
&lt;p&gt;接触 Vibe Coding 的这八个多月，对我来说像是一次开发方式的迁移。&lt;/p&gt;
&lt;p&gt;我从把 AI 当作问答工具，慢慢转向把它当作开发协作者。也从更关注技术实现，逐渐转向关注产品本身、业务流程和最终结果。&lt;/p&gt;
&lt;p&gt;这种变化不是一夜之间发生的。&lt;/p&gt;
&lt;p&gt;它是在一次次尝试工具、配置模型、修改项目、验证结果的过程中慢慢形成的。&lt;/p&gt;
&lt;p&gt;现在的我依然还在学习 AI，也还在摸索更适合自己的工作流。但有一点已经很明确：未来的开发不会再回到过去那种完全依赖手工推进的状态。&lt;/p&gt;
&lt;p&gt;AI 会继续进入开发流程，而我需要做的，是学会站在更高的位置使用它。&lt;/p&gt;
&lt;p&gt;把注意力从代码细节里适当抽出来，更多地放到产品、体验、流程和价值上。&lt;/p&gt;
&lt;p&gt;这可能就是 Vibe Coding 最吸引我的地方。&lt;/p&gt;
&lt;p&gt;它不是让我不再关心技术，而是让我终于有更多精力去关心技术背后真正要完成的事情。&lt;/p&gt;</content:encoded><category>博客</category><category>随笔</category><category>AI</category></item><item><title>梦开始的地方</title><link>https://blog.veyliss.top/blog/dream/</link><guid isPermaLink="true">https://blog.veyliss.top/blog/dream/</guid><pubDate>Mon, 18 May 2026 00:00:00 GMT</pubDate><content:encoded>&lt;p&gt;这是我创建项目的第一篇博客文章。&lt;/p&gt;
&lt;p&gt;在这个博客里，我将分享我的技术学习历程、项目经验以及一些随笔思考。希望通过这个平台，能够记录下我的成长轨迹，也能与志同道合的朋友们交流和分享。&lt;/p&gt;
&lt;p&gt;我的名字叫做 veyliss，这是我“梦开始的地方”。我希望在这里能够记录下我的梦想和努力的过程，也希望能够激励自己不断前行。无论是技术上的突破，还是生活中的点滴，我都希望能够在这里留下足迹。&lt;/p&gt;
&lt;p&gt;在许多年前我就曾想着自己搭建真正属于自己的博客网站，记录自己的学习和成长。如今这个想法终于实现了，我感到非常兴奋和满足。这个博客不仅是一个记录工具，更是一个激励自己不断前进的动力源泉。&lt;/p&gt;
&lt;p&gt;我相信，通过这个博客，我能够更好地总结和反思自己的学习过程，也能够与更多的人分享我的经验和见解。无论是技术上的问题，还是生活中的思考，我都希望能够在这里找到共鸣和支持。&lt;/p&gt;
&lt;p&gt;最后，感谢每一个来到这个博客的朋友们，希望你们能够在这里找到有价值的内容，也希望我们能够在这里一起成长和进步。让我们一起在这个梦开始的地方，书写属于我们的故事吧！&lt;/p&gt;</content:encoded><category>博客</category><category>随笔</category></item><item><title>回顾 2025</title><link>https://blog.veyliss.top/blog/year_review_2025/</link><guid isPermaLink="true">https://blog.veyliss.top/blog/year_review_2025/</guid><pubDate>Wed, 31 Dec 2025 00:00:00 GMT</pubDate><content:encoded>&lt;p&gt;回望 2025 年，时间像被按下了快进键。&lt;/p&gt;
&lt;p&gt;这一年走得很快，也走得并不轻松。很多事情在开始时都带着热情，真正走到最后却发现，能完整收尾的并不算多。想做的项目、想沉淀的知识、想持续推进的计划，有些还停留在半成品阶段，有些甚至只是短暂地闪过念头。&lt;/p&gt;
&lt;p&gt;这并不是一个让人完全满意的年份。&lt;/p&gt;
&lt;p&gt;但它也不是毫无意义的一年。&lt;/p&gt;
&lt;div&gt;&lt;h2 id=&quot;工作带来的真实成长&quot;&gt;工作带来的真实成长&lt;/h2&gt;&lt;/div&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;
&lt;p&gt;不过遗憾也很明显。&lt;/p&gt;
&lt;p&gt;个人能力确实在提升，但还没有达到自己期待中的飞跃。和行业里真正优秀的人相比，差距依然存在，而且并不小。意识到这一点的时候，会有一点失落，但也会更清楚自己接下来应该往哪里用力。&lt;/p&gt;
&lt;div&gt;&lt;h2 id=&quot;疲惫也是这一年的关键词&quot;&gt;疲惫也是这一年的关键词&lt;/h2&gt;&lt;/div&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;
&lt;p&gt;它只是提醒我：人的精力是有限的，自我提升也不能只靠一时热情。真正能走得远的，应该是更稳定、更可持续的节奏。&lt;/p&gt;
&lt;div&gt;&lt;h2 id=&quot;知识体系仍然需要建立&quot;&gt;知识体系仍然需要建立&lt;/h2&gt;&lt;/div&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;
&lt;p&gt;文字不是为了证明什么，而是为了给自己留下路径。现在的记录，也许会成为未来某个阶段重新出发时的坐标。&lt;/p&gt;
&lt;div&gt;&lt;h2 id=&quot;遇见更多不同的人&quot;&gt;遇见更多不同的人&lt;/h2&gt;&lt;/div&gt;
&lt;p&gt;2025 年，我结识了很多圈内朋友。&lt;/p&gt;
&lt;p&gt;他们来自不同领域，有交易所、Web3、外卖行业、传统行业、SaaS 领域，也有来自大厂的朋友。还有一些自由职业者，他们的工作方式和生活状态让我产生过很多羡慕与向往。&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;div&gt;&lt;h2 id=&quot;ai-时代正在加速到来&quot;&gt;AI 时代正在加速到来&lt;/h2&gt;&lt;/div&gt;
&lt;p&gt;2025 年也是 AI 快速爆发的一年。&lt;/p&gt;
&lt;p&gt;越来越多行业开始投入 AI 相关研发，尝试用 AI 提升效率、优化流程、创造新的业务价值。无论是技术研发、内容生产，还是产品设计、数据分析，AI 都在逐渐进入日常工作。&lt;/p&gt;
&lt;p&gt;这不是一个遥远的趋势，而是正在发生的变化。&lt;/p&gt;
&lt;p&gt;我能明显感觉到，未来互联网生态会和 AI 更紧密地连接在一起。很多岗位的工作方式会被改变，很多工具会被重做，很多原本依赖人工经验的流程，也会被新的方式重新组织。&lt;/p&gt;
&lt;p&gt;这对个人来说既是压力，也是机会。&lt;/p&gt;
&lt;p&gt;压力在于，原本掌握的技能可能很快变得不够用。机会在于，如果能更早理解这些变化，并把 AI 当成能力放大器，就有可能在新的阶段找到更好的位置。&lt;/p&gt;
&lt;div&gt;&lt;h2 id=&quot;写在最后&quot;&gt;写在最后&lt;/h2&gt;&lt;/div&gt;
&lt;p&gt;2025 年并不是一个完成度很高的年份。&lt;/p&gt;
&lt;p&gt;它有遗憾，有拖延，有没有收尾的项目，也有很多没有真正落实的计划。但它同样留下了工作经验、人际连接、行业观察和对自我节奏的重新认识。&lt;/p&gt;
&lt;p&gt;我不想把这一年写得过于漂亮。&lt;/p&gt;
&lt;p&gt;因为真实的成长并不总是热血的。很多时候，它只是一次次发现自己的不足，然后在疲惫里慢慢调整方向。&lt;/p&gt;
&lt;p&gt;如果要给 2025 年一个总结，我想它更像是一个提醒：&lt;/p&gt;
&lt;p&gt;不要只依赖热情，也不要害怕缓慢。&lt;/p&gt;
&lt;p&gt;真正重要的，是在每一次停顿之后，还能重新开始。&lt;/p&gt;</content:encoded><category>博客</category><category>随笔</category></item><item><title>Java 校招面试题复盘清单</title><link>https://blog.veyliss.top/blog/java_campus_interview_review_01/</link><guid isPermaLink="true">https://blog.veyliss.top/blog/java_campus_interview_review_01/</guid><pubDate>Wed, 29 Oct 2025 00:00:00 GMT</pubDate><content:encoded>&lt;p&gt;这是一份 Java 校招面试题复盘清单。&lt;/p&gt;
&lt;p&gt;它不适合当作“背诵稿”逐字记忆，更适合当作复习地图：先知道面试会问哪些方向，再把每个问题整理成可以讲清楚的核心答案。&lt;/p&gt;
&lt;p&gt;Java 校招面试通常会围绕三部分展开：&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;约 40%&lt;/td&gt;&lt;td&gt;Java、Spring、MySQL、Redis、JVM、并发&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;项目深挖&lt;/td&gt;&lt;td&gt;约 40%&lt;/td&gt;&lt;td&gt;项目背景、技术选型、难点、优化、问题排查&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;学习能力&lt;/td&gt;&lt;td&gt;约 20%&lt;/td&gt;&lt;td&gt;最近在学什么、为什么做 Java、如何解决问题&lt;/td&gt;&lt;/tr&gt;&lt;/tbody&gt;&lt;/table&gt;
&lt;p&gt;如果项目里有 Elasticsearch，那么 ES 往往会成为面试官重点追问的方向。&lt;/p&gt;
&lt;div&gt;&lt;h2 id=&quot;java-基础&quot;&gt;Java 基础&lt;/h2&gt;&lt;/div&gt;
&lt;div&gt;&lt;h3 id=&quot;hashmap-的底层原理是什么&quot;&gt;HashMap 的底层原理是什么&lt;/h3&gt;&lt;/div&gt;
&lt;p&gt;&lt;code dir=&quot;auto&quot;&gt;HashMap&lt;/code&gt; 底层主要是数组、链表和红黑树。&lt;/p&gt;
&lt;p&gt;当放入一个 key-value 时，会先根据 key 的 &lt;code dir=&quot;auto&quot;&gt;hashCode()&lt;/code&gt; 计算 hash，再定位到数组下标。如果该位置没有元素，就直接放入；如果已经有元素，就会形成链表或红黑树。&lt;/p&gt;
&lt;p&gt;在 Java 8 之后，当链表长度达到一定阈值，并且数组容量足够大时，链表会转换为红黑树，用来提高查询效率。&lt;/p&gt;
&lt;p&gt;可以这样回答：&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;HashMap 通过 hash 定位数组下标，数组中每个位置叫 bucket。发生 hash 冲突时，Java 8 以前主要用链表，Java 8 之后链表过长会转为红黑树。扩容时会重新计算元素位置，所以 HashMap 的性能和初始容量、负载因子、hash 分布都有关系。&lt;/p&gt;
&lt;/blockquote&gt;
&lt;div&gt;&lt;h3 id=&quot;hashmap-为什么线程不安全&quot;&gt;HashMap 为什么线程不安全&lt;/h3&gt;&lt;/div&gt;
&lt;p&gt;&lt;code dir=&quot;auto&quot;&gt;HashMap&lt;/code&gt; 没有做同步控制，多线程同时读写时可能出现数据覆盖、状态不一致、扩容异常等问题。&lt;/p&gt;
&lt;p&gt;常见风险：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;多个线程同时 &lt;code dir=&quot;auto&quot;&gt;put&lt;/code&gt;，可能覆盖彼此写入。&lt;/li&gt;
&lt;li&gt;扩容时结构变化，其他线程同时访问可能拿到异常结果。&lt;/li&gt;
&lt;li&gt;统计数量 &lt;code dir=&quot;auto&quot;&gt;size&lt;/code&gt; 可能不准确。&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;所以多线程场景通常使用 &lt;code dir=&quot;auto&quot;&gt;ConcurrentHashMap&lt;/code&gt;。&lt;/p&gt;
&lt;div&gt;&lt;h3 id=&quot;concurrenthashmap-如何实现线程安全&quot;&gt;ConcurrentHashMap 如何实现线程安全&lt;/h3&gt;&lt;/div&gt;
&lt;p&gt;Java 8 中，&lt;code dir=&quot;auto&quot;&gt;ConcurrentHashMap&lt;/code&gt; 主要通过 CAS 和 &lt;code dir=&quot;auto&quot;&gt;synchronized&lt;/code&gt; 保证线程安全。&lt;/p&gt;
&lt;p&gt;它不是给整张表加一把大锁，而是尽量缩小锁粒度。&lt;/p&gt;
&lt;p&gt;常见回答：&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;Java 8 的 ConcurrentHashMap 底层也是数组、链表和红黑树。插入时，如果桶为空，会通过 CAS 放入节点；如果桶不为空，会对桶头节点加 synchronized，只锁当前桶。这样既保证线程安全，又比 Hashtable 整表加锁性能更好。&lt;/p&gt;
&lt;/blockquote&gt;
&lt;div&gt;&lt;h3 id=&quot;arraylist-和-linkedlist-的区别&quot;&gt;ArrayList 和 LinkedList 的区别&lt;/h3&gt;&lt;/div&gt;
&lt;p&gt;&lt;code dir=&quot;auto&quot;&gt;ArrayList&lt;/code&gt; 底层是动态数组，&lt;code dir=&quot;auto&quot;&gt;LinkedList&lt;/code&gt; 底层是双向链表。&lt;/p&gt;



































&lt;table&gt;&lt;thead&gt;&lt;tr&gt;&lt;th&gt;对比项&lt;/th&gt;&lt;th&gt;ArrayList&lt;/th&gt;&lt;th&gt;LinkedList&lt;/th&gt;&lt;/tr&gt;&lt;/thead&gt;&lt;tbody&gt;&lt;tr&gt;&lt;td&gt;底层结构&lt;/td&gt;&lt;td&gt;动态数组&lt;/td&gt;&lt;td&gt;双向链表&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;随机访问&lt;/td&gt;&lt;td&gt;快，按下标访问 O(1)&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;内存占用&lt;/td&gt;&lt;td&gt;相对较少&lt;/td&gt;&lt;td&gt;每个节点要存前后指针&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;常用场景&lt;/td&gt;&lt;td&gt;查询多&lt;/td&gt;&lt;td&gt;插入删除多，但实际也要看位置&lt;/td&gt;&lt;/tr&gt;&lt;/tbody&gt;&lt;/table&gt;
&lt;p&gt;面试里要注意：不要简单说 &lt;code dir=&quot;auto&quot;&gt;LinkedList&lt;/code&gt; 插入删除一定快。因为如果要先按索引找到位置，遍历本身也有成本。&lt;/p&gt;
&lt;div&gt;&lt;h3 id=&quot;stringstringbuilderstringbuffer-的区别&quot;&gt;String、StringBuilder、StringBuffer 的区别&lt;/h3&gt;&lt;/div&gt;
&lt;p&gt;&lt;code dir=&quot;auto&quot;&gt;String&lt;/code&gt; 是不可变对象，每次修改都会产生新字符串。&lt;/p&gt;
&lt;p&gt;&lt;code dir=&quot;auto&quot;&gt;StringBuilder&lt;/code&gt; 是可变字符序列，线程不安全，但性能较好。&lt;/p&gt;
&lt;p&gt;&lt;code dir=&quot;auto&quot;&gt;StringBuffer&lt;/code&gt; 也是可变字符序列，方法加了同步，线程安全，但性能通常低于 &lt;code dir=&quot;auto&quot;&gt;StringBuilder&lt;/code&gt;。&lt;/p&gt;
&lt;p&gt;常见选择：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;少量字符串拼接：直接用 &lt;code dir=&quot;auto&quot;&gt;String&lt;/code&gt;。&lt;/li&gt;
&lt;li&gt;单线程大量拼接：用 &lt;code dir=&quot;auto&quot;&gt;StringBuilder&lt;/code&gt;。&lt;/li&gt;
&lt;li&gt;多线程共享拼接对象：用 &lt;code dir=&quot;auto&quot;&gt;StringBuffer&lt;/code&gt;，但实际业务中较少这样用。&lt;/li&gt;
&lt;/ul&gt;
&lt;div&gt;&lt;h3 id=&quot;equals-和--的区别&quot;&gt;equals 和 == 的区别&lt;/h3&gt;&lt;/div&gt;
&lt;p&gt;&lt;code dir=&quot;auto&quot;&gt;==&lt;/code&gt; 比较的是两边是否相等。&lt;/p&gt;
&lt;p&gt;对于基本类型，比较的是值。&lt;/p&gt;
&lt;p&gt;对于引用类型，比较的是对象地址。&lt;/p&gt;
&lt;p&gt;&lt;code dir=&quot;auto&quot;&gt;equals()&lt;/code&gt; 是对象方法，默认实现也是比较地址，但很多类会重写它，比如 &lt;code dir=&quot;auto&quot;&gt;String&lt;/code&gt; 会比较字符串内容。&lt;/p&gt;
&lt;div&gt;&lt;figure&gt;&lt;figcaption&gt;&lt;span&gt;equals-demo.java&lt;/span&gt;&lt;/figcaption&gt;&lt;pre&gt;&lt;code&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;String&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;a&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;=&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;new&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;String&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;&quot;&lt;/span&gt;&lt;span&gt;hello&lt;/span&gt;&lt;span&gt;&quot;&lt;/span&gt;&lt;span&gt;)&lt;/span&gt;&lt;span&gt;;&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;String&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;b&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;=&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;new&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;String&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;&quot;&lt;/span&gt;&lt;span&gt;hello&lt;/span&gt;&lt;span&gt;&quot;&lt;/span&gt;&lt;span&gt;)&lt;/span&gt;&lt;span&gt;;&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;
&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;System&lt;/span&gt;&lt;span&gt;.&lt;/span&gt;&lt;span&gt;out&lt;/span&gt;&lt;span&gt;.&lt;/span&gt;&lt;span&gt;println&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;a &lt;/span&gt;&lt;span&gt;==&lt;/span&gt;&lt;span&gt; b&lt;/span&gt;&lt;span&gt;)&lt;/span&gt;&lt;span&gt;; &lt;/span&gt;&lt;span&gt;// false&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;System&lt;/span&gt;&lt;span&gt;.&lt;/span&gt;&lt;span&gt;out&lt;/span&gt;&lt;span&gt;.&lt;/span&gt;&lt;span&gt;println&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;a&lt;/span&gt;&lt;span&gt;.&lt;/span&gt;&lt;span&gt;equals&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;b&lt;/span&gt;&lt;span&gt;))&lt;/span&gt;&lt;span&gt;; &lt;/span&gt;&lt;span&gt;// true&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/figure&gt;&lt;/div&gt;
&lt;div&gt;&lt;h3 id=&quot;java-中的异常体系是什么&quot;&gt;Java 中的异常体系是什么&lt;/h3&gt;&lt;/div&gt;
&lt;p&gt;Java 异常体系的顶层是 &lt;code dir=&quot;auto&quot;&gt;Throwable&lt;/code&gt;。&lt;/p&gt;
&lt;p&gt;它下面主要分为：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code dir=&quot;auto&quot;&gt;Error&lt;/code&gt;：严重错误，程序通常不主动处理，比如 &lt;code dir=&quot;auto&quot;&gt;OutOfMemoryError&lt;/code&gt;。&lt;/li&gt;
&lt;li&gt;&lt;code dir=&quot;auto&quot;&gt;Exception&lt;/code&gt;：程序可以捕获和处理的异常。&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;code dir=&quot;auto&quot;&gt;Exception&lt;/code&gt; 又分为：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;checked exception：编译期异常，必须处理或声明抛出。&lt;/li&gt;
&lt;li&gt;unchecked exception：运行时异常，继承自 &lt;code dir=&quot;auto&quot;&gt;RuntimeException&lt;/code&gt;，例如空指针、数组越界。&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;面试里可以补一句：业务开发中不要滥用异常控制正常流程，异常更适合表示非预期情况。&lt;/p&gt;
&lt;div&gt;&lt;h3 id=&quot;什么是反射&quot;&gt;什么是反射&lt;/h3&gt;&lt;/div&gt;
&lt;p&gt;反射是 Java 在运行时获取类信息、创建对象、调用方法、访问字段的能力。&lt;/p&gt;
&lt;p&gt;常见应用场景：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Spring 创建 Bean、依赖注入。&lt;/li&gt;
&lt;li&gt;MyBatis 映射对象字段。&lt;/li&gt;
&lt;li&gt;注解解析。&lt;/li&gt;
&lt;li&gt;测试框架调用测试方法。&lt;/li&gt;
&lt;li&gt;动态代理。&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;反射灵活，但也有缺点：性能相对普通调用更低，可读性和安全性更差。&lt;/p&gt;
&lt;div&gt;&lt;h2 id=&quot;java-并发&quot;&gt;Java 并发&lt;/h2&gt;&lt;/div&gt;
&lt;div&gt;&lt;h3 id=&quot;什么是线程什么是进程&quot;&gt;什么是线程，什么是进程&lt;/h3&gt;&lt;/div&gt;
&lt;p&gt;进程是操作系统资源分配的基本单位。一个应用程序运行起来通常就是一个进程。&lt;/p&gt;
&lt;p&gt;线程是 CPU 调度的基本单位，一个进程中可以包含多个线程。&lt;/p&gt;
&lt;p&gt;可以这样回答：&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;进程拥有独立的内存空间，线程共享同一进程的内存资源。线程切换成本通常低于进程，但共享数据也会带来线程安全问题。&lt;/p&gt;
&lt;/blockquote&gt;
&lt;div&gt;&lt;h3 id=&quot;synchronized-的原理是什么&quot;&gt;synchronized 的原理是什么&lt;/h3&gt;&lt;/div&gt;
&lt;p&gt;&lt;code dir=&quot;auto&quot;&gt;synchronized&lt;/code&gt; 可以修饰方法或代码块，用来保证同一时间只有一个线程进入临界区。&lt;/p&gt;
&lt;p&gt;它依赖对象监视器锁，也就是 monitor。&lt;/p&gt;
&lt;p&gt;进入同步代码块时，线程尝试获取对象锁；执行完或异常退出时释放锁。&lt;/p&gt;
&lt;p&gt;可以补充：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;修饰普通方法，锁的是当前对象 &lt;code dir=&quot;auto&quot;&gt;this&lt;/code&gt;。&lt;/li&gt;
&lt;li&gt;修饰静态方法，锁的是当前类的 &lt;code dir=&quot;auto&quot;&gt;Class&lt;/code&gt; 对象。&lt;/li&gt;
&lt;li&gt;修饰代码块，可以指定锁对象。&lt;/li&gt;
&lt;/ul&gt;
&lt;div&gt;&lt;h3 id=&quot;volatile-的作用是什么&quot;&gt;volatile 的作用是什么&lt;/h3&gt;&lt;/div&gt;
&lt;p&gt;&lt;code dir=&quot;auto&quot;&gt;volatile&lt;/code&gt; 主要有两个作用：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;保证变量对多线程的可见性。&lt;/li&gt;
&lt;li&gt;禁止指令重排序。&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;它不能保证复合操作的原子性。&lt;/p&gt;
&lt;p&gt;比如 &lt;code dir=&quot;auto&quot;&gt;count++&lt;/code&gt; 包含读取、加一、写回，不是一个原子操作，所以只加 &lt;code dir=&quot;auto&quot;&gt;volatile&lt;/code&gt; 仍然不安全。&lt;/p&gt;
&lt;p&gt;常见场景：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;状态标志位。&lt;/li&gt;
&lt;li&gt;单例模式双重检查锁中的实例变量。&lt;/li&gt;
&lt;/ul&gt;
&lt;div&gt;&lt;h3 id=&quot;什么是线程池&quot;&gt;什么是线程池&lt;/h3&gt;&lt;/div&gt;
&lt;p&gt;线程池是提前创建并管理一组线程，任务来了以后交给线程池执行。&lt;/p&gt;
&lt;p&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;li&gt;统一管理任务队列、拒绝策略和线程生命周期。&lt;/li&gt;
&lt;/ul&gt;
&lt;div&gt;&lt;h3 id=&quot;threadpoolexecutor-核心参数有哪些&quot;&gt;ThreadPoolExecutor 核心参数有哪些&lt;/h3&gt;&lt;/div&gt;
&lt;p&gt;&lt;code dir=&quot;auto&quot;&gt;ThreadPoolExecutor&lt;/code&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;&lt;code dir=&quot;auto&quot;&gt;corePoolSize&lt;/code&gt;&lt;/td&gt;&lt;td&gt;核心线程数&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;&lt;code dir=&quot;auto&quot;&gt;maximumPoolSize&lt;/code&gt;&lt;/td&gt;&lt;td&gt;最大线程数&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;&lt;code dir=&quot;auto&quot;&gt;keepAliveTime&lt;/code&gt;&lt;/td&gt;&lt;td&gt;非核心线程空闲存活时间&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;&lt;code dir=&quot;auto&quot;&gt;unit&lt;/code&gt;&lt;/td&gt;&lt;td&gt;时间单位&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;&lt;code dir=&quot;auto&quot;&gt;workQueue&lt;/code&gt;&lt;/td&gt;&lt;td&gt;任务队列&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;&lt;code dir=&quot;auto&quot;&gt;threadFactory&lt;/code&gt;&lt;/td&gt;&lt;td&gt;线程创建工厂&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;&lt;code dir=&quot;auto&quot;&gt;handler&lt;/code&gt;&lt;/td&gt;&lt;td&gt;拒绝策略&lt;/td&gt;&lt;/tr&gt;&lt;/tbody&gt;&lt;/table&gt;
&lt;p&gt;执行流程可以概括为：&lt;/p&gt;
&lt;div&gt;&lt;figure&gt;&lt;figcaption&gt;&lt;/figcaption&gt;&lt;pre&gt;&lt;code&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;核心线程未满 -&gt; 创建核心线程&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;核心线程已满 -&gt; 放入任务队列&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;队列满了 -&gt; 创建非核心线程&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;线程数达到最大且队列也满 -&gt; 执行拒绝策略&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/figure&gt;&lt;/div&gt;
&lt;div&gt;&lt;h3 id=&quot;什么是死锁如何避免&quot;&gt;什么是死锁，如何避免&lt;/h3&gt;&lt;/div&gt;
&lt;p&gt;死锁是多个线程互相持有对方需要的资源，导致都无法继续执行。&lt;/p&gt;
&lt;p&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;li&gt;循环等待。&lt;/li&gt;
&lt;/ul&gt;
&lt;p&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;li&gt;避免嵌套锁。&lt;/li&gt;
&lt;li&gt;使用并发工具类代替手写锁。&lt;/li&gt;
&lt;/ul&gt;
&lt;div&gt;&lt;h2 id=&quot;jvm&quot;&gt;JVM&lt;/h2&gt;&lt;/div&gt;
&lt;div&gt;&lt;h3 id=&quot;jvm-的内存结构是什么&quot;&gt;JVM 的内存结构是什么&lt;/h3&gt;&lt;/div&gt;
&lt;p&gt;JVM 运行时数据区主要包括：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;程序计数器。&lt;/li&gt;
&lt;li&gt;Java 虚拟机栈。&lt;/li&gt;
&lt;li&gt;本地方法栈。&lt;/li&gt;
&lt;li&gt;堆。&lt;/li&gt;
&lt;li&gt;方法区。&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;线程私有：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;程序计数器。&lt;/li&gt;
&lt;li&gt;Java 虚拟机栈。&lt;/li&gt;
&lt;li&gt;本地方法栈。&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;线程共享：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;堆。&lt;/li&gt;
&lt;li&gt;方法区。&lt;/li&gt;
&lt;/ul&gt;
&lt;div&gt;&lt;h3 id=&quot;堆和栈的区别&quot;&gt;堆和栈的区别&lt;/h3&gt;&lt;/div&gt;
&lt;p&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;线程关系&lt;/td&gt;&lt;td&gt;线程私有&lt;/td&gt;&lt;td&gt;线程共享&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;存储内容&lt;/td&gt;&lt;td&gt;方法调用、局部变量&lt;/td&gt;&lt;td&gt;对象实例&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;生命周期&lt;/td&gt;&lt;td&gt;随方法调用入栈出栈&lt;/td&gt;&lt;td&gt;由 GC 管理&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;常见错误&lt;/td&gt;&lt;td&gt;&lt;code dir=&quot;auto&quot;&gt;StackOverflowError&lt;/code&gt;&lt;/td&gt;&lt;td&gt;&lt;code dir=&quot;auto&quot;&gt;OutOfMemoryError&lt;/code&gt;&lt;/td&gt;&lt;/tr&gt;&lt;/tbody&gt;&lt;/table&gt;
&lt;div&gt;&lt;h3 id=&quot;什么是垃圾回收&quot;&gt;什么是垃圾回收&lt;/h3&gt;&lt;/div&gt;
&lt;p&gt;垃圾回收是 JVM 自动回收不再使用对象的机制。&lt;/p&gt;
&lt;p&gt;判断对象是否可回收，主流方法是可达性分析：从 GC Roots 出发，能到达的对象是存活对象，不能到达的对象可以被回收。&lt;/p&gt;
&lt;p&gt;常见 GC Roots：&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;li&gt;本地方法栈中的引用。&lt;/li&gt;
&lt;/ul&gt;
&lt;div&gt;&lt;h3 id=&quot;常见-gc-算法有哪些&quot;&gt;常见 GC 算法有哪些&lt;/h3&gt;&lt;/div&gt;
&lt;p&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;li&gt;分代收集：按对象生命周期分区，不同区域用不同算法。&lt;/li&gt;
&lt;/ul&gt;
&lt;div&gt;&lt;h3 id=&quot;什么情况下会发生-oom&quot;&gt;什么情况下会发生 OOM&lt;/h3&gt;&lt;/div&gt;
&lt;p&gt;OOM 是内存不足导致的错误。&lt;/p&gt;
&lt;p&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;li&gt;线程过多导致栈空间不足。&lt;/li&gt;
&lt;li&gt;元空间加载类过多。&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;排查时通常会看日志、堆 dump、GC 情况和对象引用链。&lt;/p&gt;
&lt;div&gt;&lt;h3 id=&quot;jvm-调优一般关注哪些参数&quot;&gt;JVM 调优一般关注哪些参数&lt;/h3&gt;&lt;/div&gt;
&lt;p&gt;常见关注点：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;堆大小：&lt;code dir=&quot;auto&quot;&gt;-Xms&lt;/code&gt;、&lt;code dir=&quot;auto&quot;&gt;-Xmx&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;新生代大小：&lt;code dir=&quot;auto&quot;&gt;-Xmn&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;元空间大小：&lt;code dir=&quot;auto&quot;&gt;-XX:MetaspaceSize&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;GC 收集器选择。&lt;/li&gt;
&lt;li&gt;GC 日志。&lt;/li&gt;
&lt;li&gt;停顿时间和吞吐量。&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;调优不是盲目改参数，而是先看现象：内存是否够、GC 是否频繁、停顿是否过长、对象是否异常增长。&lt;/p&gt;
&lt;div&gt;&lt;h2 id=&quot;spring&quot;&gt;Spring&lt;/h2&gt;&lt;/div&gt;
&lt;div&gt;&lt;h3 id=&quot;什么是-ioc&quot;&gt;什么是 IoC&lt;/h3&gt;&lt;/div&gt;
&lt;p&gt;IoC 是控制反转。&lt;/p&gt;
&lt;p&gt;原本对象由程序自己创建和管理，现在交给 Spring 容器创建和管理。&lt;/p&gt;
&lt;p&gt;DI 依赖注入是 IoC 的一种实现方式。比如一个 Service 依赖 Mapper，不需要自己 &lt;code dir=&quot;auto&quot;&gt;new&lt;/code&gt;，而是由 Spring 注入。&lt;/p&gt;
&lt;div&gt;&lt;h3 id=&quot;什么是-aop&quot;&gt;什么是 AOP&lt;/h3&gt;&lt;/div&gt;
&lt;p&gt;AOP 是面向切面编程，用来把通用逻辑从业务代码中抽离出来。&lt;/p&gt;
&lt;p&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;li&gt;监控统计。&lt;/li&gt;
&lt;li&gt;接口耗时。&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;核心思想是：不修改业务方法本身，在方法执行前后织入增强逻辑。&lt;/p&gt;
&lt;div&gt;&lt;h3 id=&quot;bean-的生命周期是什么&quot;&gt;Bean 的生命周期是什么&lt;/h3&gt;&lt;/div&gt;
&lt;p&gt;简化流程：&lt;/p&gt;
&lt;div&gt;&lt;figure&gt;&lt;figcaption&gt;&lt;/figcaption&gt;&lt;pre&gt;&lt;code&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;实例化 -&gt; 属性注入 -&gt; 初始化前后处理 -&gt; 初始化方法 -&gt; 使用 -&gt; 销毁&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/figure&gt;&lt;/div&gt;
&lt;p&gt;常见扩展点：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;构造方法。&lt;/li&gt;
&lt;li&gt;属性填充。&lt;/li&gt;
&lt;li&gt;&lt;code dir=&quot;auto&quot;&gt;BeanPostProcessor&lt;/code&gt;。&lt;/li&gt;
&lt;li&gt;&lt;code dir=&quot;auto&quot;&gt;InitializingBean&lt;/code&gt; 或 &lt;code dir=&quot;auto&quot;&gt;init-method&lt;/code&gt;。&lt;/li&gt;
&lt;li&gt;&lt;code dir=&quot;auto&quot;&gt;DisposableBean&lt;/code&gt; 或 &lt;code dir=&quot;auto&quot;&gt;destroy-method&lt;/code&gt;。&lt;/li&gt;
&lt;/ul&gt;
&lt;div&gt;&lt;h3 id=&quot;为什么使用-spring-boot&quot;&gt;为什么使用 Spring Boot&lt;/h3&gt;&lt;/div&gt;
&lt;p&gt;Spring Boot 主要解决传统 Spring 项目配置繁琐的问题。&lt;/p&gt;
&lt;p&gt;优势：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;自动配置。&lt;/li&gt;
&lt;li&gt;内置 Web 容器。&lt;/li&gt;
&lt;li&gt;starter 依赖简化。&lt;/li&gt;
&lt;li&gt;快速创建项目。&lt;/li&gt;
&lt;li&gt;方便监控和部署。&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;一句话回答：&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;Spring Boot 让 Spring 项目更容易启动和维护，它通过自动配置和 starter 机制减少大量 XML 或手动配置。&lt;/p&gt;
&lt;/blockquote&gt;
&lt;div&gt;&lt;h3 id=&quot;spring-boot-自动配置原理是什么&quot;&gt;Spring Boot 自动配置原理是什么&lt;/h3&gt;&lt;/div&gt;
&lt;p&gt;自动配置的核心是根据类路径、配置文件和条件注解，自动创建合适的 Bean。&lt;/p&gt;
&lt;p&gt;常见关键词：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;starter。&lt;/li&gt;
&lt;li&gt;auto configuration。&lt;/li&gt;
&lt;li&gt;&lt;code dir=&quot;auto&quot;&gt;@EnableAutoConfiguration&lt;/code&gt;。&lt;/li&gt;
&lt;li&gt;条件注解，比如 &lt;code dir=&quot;auto&quot;&gt;@ConditionalOnClass&lt;/code&gt;、&lt;code dir=&quot;auto&quot;&gt;@ConditionalOnMissingBean&lt;/code&gt;。&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;面试中可以这样说：&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;Spring Boot 会根据引入的依赖和当前环境判断是否满足条件，如果满足，就把对应配置类里的 Bean 注册到容器中。&lt;/p&gt;
&lt;/blockquote&gt;
&lt;div&gt;&lt;h3 id=&quot;什么是-restful-api&quot;&gt;什么是 RESTful API&lt;/h3&gt;&lt;/div&gt;
&lt;p&gt;RESTful API 是一种接口设计风格。&lt;/p&gt;
&lt;p&gt;核心思想：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;使用 URL 表示资源。&lt;/li&gt;
&lt;li&gt;使用 HTTP 方法表示操作。&lt;/li&gt;
&lt;li&gt;使用状态码表示结果。&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;示例：&lt;/p&gt;
&lt;div&gt;&lt;figure&gt;&lt;figcaption&gt;&lt;/figcaption&gt;&lt;pre&gt;&lt;code&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;GET /users/1      查询用户&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;POST /users       创建用户&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;PUT /users/1      更新用户&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;DELETE /users/1   删除用户&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/figure&gt;&lt;/div&gt;
&lt;div&gt;&lt;h2 id=&quot;mysql&quot;&gt;MySQL&lt;/h2&gt;&lt;/div&gt;
&lt;div&gt;&lt;h3 id=&quot;mysql-索引是什么&quot;&gt;MySQL 索引是什么&lt;/h3&gt;&lt;/div&gt;
&lt;p&gt;索引是帮助 MySQL 快速查找数据的数据结构。&lt;/p&gt;
&lt;p&gt;可以理解为书的目录。没有索引时，数据库可能要全表扫描；有索引时，可以更快定位数据。&lt;/p&gt;
&lt;p&gt;索引能提高查询效率，但会增加写入成本和存储空间。&lt;/p&gt;
&lt;div&gt;&lt;h3 id=&quot;mysql-为什么使用-btree&quot;&gt;MySQL 为什么使用 B+Tree&lt;/h3&gt;&lt;/div&gt;
&lt;p&gt;B+Tree 适合磁盘存储和范围查询。&lt;/p&gt;
&lt;p&gt;原因：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;树高度低，减少磁盘 IO。&lt;/li&gt;
&lt;li&gt;非叶子节点只存索引，能放更多 key。&lt;/li&gt;
&lt;li&gt;叶子节点之间有链表，范围查询效率高。&lt;/li&gt;
&lt;li&gt;查询性能稳定。&lt;/li&gt;
&lt;/ul&gt;
&lt;div&gt;&lt;h3 id=&quot;什么是覆盖索引&quot;&gt;什么是覆盖索引&lt;/h3&gt;&lt;/div&gt;
&lt;p&gt;覆盖索引指查询需要的字段都能从索引中获得，不需要回表查询。&lt;/p&gt;
&lt;p&gt;比如有联合索引 &lt;code dir=&quot;auto&quot;&gt;(name, age)&lt;/code&gt;：&lt;/p&gt;
&lt;div&gt;&lt;figure&gt;&lt;figcaption&gt;&lt;/figcaption&gt;&lt;pre&gt;&lt;code&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;SELECT&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;name&lt;/span&gt;&lt;span&gt;, age &lt;/span&gt;&lt;span&gt;FROM&lt;/span&gt;&lt;span&gt; user &lt;/span&gt;&lt;span&gt;WHERE&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;name&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;=&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;&apos;&lt;/span&gt;&lt;span&gt;xiaoxi&lt;/span&gt;&lt;span&gt;&apos;&lt;/span&gt;&lt;span&gt;;&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/figure&gt;&lt;/div&gt;
&lt;p&gt;如果查询字段都在索引里，就可能走覆盖索引。&lt;/p&gt;
&lt;div&gt;&lt;h3 id=&quot;什么情况下索引会失效&quot;&gt;什么情况下索引会失效&lt;/h3&gt;&lt;/div&gt;
&lt;p&gt;常见情况：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;对索引列使用函数。&lt;/li&gt;
&lt;li&gt;对索引列做计算。&lt;/li&gt;
&lt;li&gt;使用左模糊匹配，例如 &lt;code dir=&quot;auto&quot;&gt;LIKE &apos;%abc&apos;&lt;/code&gt;。&lt;/li&gt;
&lt;li&gt;联合索引不符合最左前缀原则。&lt;/li&gt;
&lt;li&gt;隐式类型转换。&lt;/li&gt;
&lt;li&gt;&lt;code dir=&quot;auto&quot;&gt;OR&lt;/code&gt; 条件使用不当。&lt;/li&gt;
&lt;/ul&gt;
&lt;div&gt;&lt;h3 id=&quot;什么是事务&quot;&gt;什么是事务&lt;/h3&gt;&lt;/div&gt;
&lt;p&gt;事务是一组数据库操作的集合，要么全部成功，要么全部失败。&lt;/p&gt;
&lt;p&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;/p&gt;
&lt;div&gt;&lt;h3 id=&quot;mysql-事务的-acid&quot;&gt;MySQL 事务的 ACID&lt;/h3&gt;&lt;/div&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;Atomicity 原子性&lt;/td&gt;&lt;td&gt;事务要么全部成功，要么全部失败&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;Consistency 一致性&lt;/td&gt;&lt;td&gt;事务前后数据满足约束&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;Isolation 隔离性&lt;/td&gt;&lt;td&gt;并发事务之间互相隔离&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;Durability 持久性&lt;/td&gt;&lt;td&gt;事务提交后数据持久保存&lt;/td&gt;&lt;/tr&gt;&lt;/tbody&gt;&lt;/table&gt;
&lt;div&gt;&lt;h3 id=&quot;什么是-mvcc&quot;&gt;什么是 MVCC&lt;/h3&gt;&lt;/div&gt;
&lt;p&gt;MVCC 是多版本并发控制。&lt;/p&gt;
&lt;p&gt;它通过保存数据的多个版本，让读写尽量不互相阻塞。&lt;/p&gt;
&lt;p&gt;在 InnoDB 中，MVCC 主要依赖：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;隐藏字段。&lt;/li&gt;
&lt;li&gt;undo log。&lt;/li&gt;
&lt;li&gt;ReadView。&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;常见作用是支持可重复读和快照读。&lt;/p&gt;
&lt;div&gt;&lt;h2 id=&quot;redis&quot;&gt;Redis&lt;/h2&gt;&lt;/div&gt;
&lt;div&gt;&lt;h3 id=&quot;redis-为什么这么快&quot;&gt;Redis 为什么这么快&lt;/h3&gt;&lt;/div&gt;
&lt;p&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;li&gt;IO 多路复用。&lt;/li&gt;
&lt;li&gt;C 语言实现，执行效率高。&lt;/li&gt;
&lt;/ul&gt;
&lt;div&gt;&lt;h3 id=&quot;redis-有哪些数据结构&quot;&gt;Redis 有哪些数据结构&lt;/h3&gt;&lt;/div&gt;
&lt;p&gt;常见类型：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;String。&lt;/li&gt;
&lt;li&gt;Hash。&lt;/li&gt;
&lt;li&gt;List。&lt;/li&gt;
&lt;li&gt;Set。&lt;/li&gt;
&lt;li&gt;Sorted Set。&lt;/li&gt;
&lt;li&gt;Bitmap。&lt;/li&gt;
&lt;li&gt;HyperLogLog。&lt;/li&gt;
&lt;li&gt;Geo。&lt;/li&gt;
&lt;li&gt;Stream。&lt;/li&gt;
&lt;/ul&gt;
&lt;div&gt;&lt;h3 id=&quot;什么是缓存穿透&quot;&gt;什么是缓存穿透&lt;/h3&gt;&lt;/div&gt;
&lt;p&gt;缓存穿透是请求的数据在缓存和数据库中都不存在，导致请求持续打到数据库。&lt;/p&gt;
&lt;p&gt;解决方式：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;缓存空对象。&lt;/li&gt;
&lt;li&gt;布隆过滤器。&lt;/li&gt;
&lt;/ul&gt;
&lt;div&gt;&lt;h3 id=&quot;什么是缓存击穿&quot;&gt;什么是缓存击穿&lt;/h3&gt;&lt;/div&gt;
&lt;p&gt;缓存击穿是热点 Key 过期瞬间，大量并发请求同时打到数据库。&lt;/p&gt;
&lt;p&gt;解决方式：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;互斥锁。&lt;/li&gt;
&lt;li&gt;逻辑过期。&lt;/li&gt;
&lt;/ul&gt;
&lt;div&gt;&lt;h3 id=&quot;什么是缓存雪崩&quot;&gt;什么是缓存雪崩&lt;/h3&gt;&lt;/div&gt;
&lt;p&gt;缓存雪崩是大量 Key 同时过期，或者 Redis 整体不可用，导致大量请求涌向数据库。&lt;/p&gt;
&lt;p&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;li&gt;Redis 高可用。&lt;/li&gt;
&lt;/ul&gt;
&lt;div&gt;&lt;h2 id=&quot;elasticsearch&quot;&gt;Elasticsearch&lt;/h2&gt;&lt;/div&gt;
&lt;div&gt;&lt;h3 id=&quot;什么是-elasticsearch&quot;&gt;什么是 Elasticsearch&lt;/h3&gt;&lt;/div&gt;
&lt;p&gt;Elasticsearch 是一个分布式搜索和分析引擎，常用于全文搜索、日志检索、商品搜索等场景。&lt;/p&gt;
&lt;p&gt;它底层基于 Lucene，对外提供 REST API，支持分布式、倒排索引和复杂查询。&lt;/p&gt;
&lt;div&gt;&lt;h3 id=&quot;什么是倒排索引&quot;&gt;什么是倒排索引&lt;/h3&gt;&lt;/div&gt;
&lt;p&gt;倒排索引是从词到文档的映射。&lt;/p&gt;
&lt;p&gt;普通索引更像：&lt;/p&gt;
&lt;div&gt;&lt;figure&gt;&lt;figcaption&gt;&lt;/figcaption&gt;&lt;pre&gt;&lt;code&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;文档 -&gt; 包含哪些词&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/figure&gt;&lt;/div&gt;
&lt;p&gt;倒排索引更像：&lt;/p&gt;
&lt;div&gt;&lt;figure&gt;&lt;figcaption&gt;&lt;/figcaption&gt;&lt;pre&gt;&lt;code&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;词 -&gt; 出现在哪些文档中&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/figure&gt;&lt;/div&gt;
&lt;p&gt;这就是 ES 做全文搜索快的重要原因。&lt;/p&gt;
&lt;div&gt;&lt;h3 id=&quot;es-中-indextypedocument-是什么&quot;&gt;ES 中 index、type、document 是什么&lt;/h3&gt;&lt;/div&gt;
&lt;p&gt;&lt;code dir=&quot;auto&quot;&gt;index&lt;/code&gt; 类似一类数据的集合。&lt;/p&gt;
&lt;p&gt;&lt;code dir=&quot;auto&quot;&gt;document&lt;/code&gt; 是一条 JSON 数据。&lt;/p&gt;
&lt;p&gt;&lt;code dir=&quot;auto&quot;&gt;type&lt;/code&gt; 在早期版本中用于区分类型，但新版本已经逐步移除，不建议在新项目中依赖 type。&lt;/p&gt;
&lt;p&gt;可以类比：&lt;/p&gt;





















&lt;table&gt;&lt;thead&gt;&lt;tr&gt;&lt;th&gt;ES&lt;/th&gt;&lt;th&gt;关系型数据库&lt;/th&gt;&lt;/tr&gt;&lt;/thead&gt;&lt;tbody&gt;&lt;tr&gt;&lt;td&gt;index&lt;/td&gt;&lt;td&gt;table 或 database 的某种集合概念&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;document&lt;/td&gt;&lt;td&gt;row&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;field&lt;/td&gt;&lt;td&gt;column&lt;/td&gt;&lt;/tr&gt;&lt;/tbody&gt;&lt;/table&gt;
&lt;div&gt;&lt;h3 id=&quot;什么是-dsl-查询&quot;&gt;什么是 DSL 查询&lt;/h3&gt;&lt;/div&gt;
&lt;p&gt;DSL 是 Elasticsearch 的查询语言，使用 JSON 描述查询条件。&lt;/p&gt;
&lt;p&gt;示例：&lt;/p&gt;
&lt;div&gt;&lt;figure&gt;&lt;figcaption&gt;&lt;span&gt;es-dsl.json&lt;/span&gt;&lt;/figcaption&gt;&lt;pre&gt;&lt;code&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;{&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;  &lt;/span&gt;&lt;span&gt;&quot;query&quot;&lt;/span&gt;&lt;span&gt;: {&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;    &lt;/span&gt;&lt;span&gt;&quot;match&quot;&lt;/span&gt;&lt;span&gt;: {&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;      &lt;/span&gt;&lt;span&gt;&quot;title&quot;&lt;/span&gt;&lt;span&gt;: &lt;/span&gt;&lt;span&gt;&quot;&lt;/span&gt;&lt;span&gt;redis&lt;/span&gt;&lt;span&gt;&quot;&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;&lt;span&gt;    &lt;/span&gt;&lt;/span&gt;&lt;span&gt;}&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;&lt;span&gt;  &lt;/span&gt;&lt;/span&gt;&lt;span&gt;}&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;}&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/figure&gt;&lt;/div&gt;
&lt;div&gt;&lt;h3 id=&quot;bool-query-有哪些条件&quot;&gt;bool query 有哪些条件&lt;/h3&gt;&lt;/div&gt;
&lt;p&gt;常见条件：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code dir=&quot;auto&quot;&gt;must&lt;/code&gt;：必须匹配，影响评分。&lt;/li&gt;
&lt;li&gt;&lt;code dir=&quot;auto&quot;&gt;should&lt;/code&gt;：可选匹配，可能影响评分。&lt;/li&gt;
&lt;li&gt;&lt;code dir=&quot;auto&quot;&gt;filter&lt;/code&gt;：必须匹配，但不参与评分，适合过滤条件。&lt;/li&gt;
&lt;li&gt;&lt;code dir=&quot;auto&quot;&gt;must_not&lt;/code&gt;：必须不匹配。&lt;/li&gt;
&lt;/ul&gt;
&lt;div&gt;&lt;h3 id=&quot;为什么-elasticsearch-搜索比-mysql-快&quot;&gt;为什么 Elasticsearch 搜索比 MySQL 快&lt;/h3&gt;&lt;/div&gt;
&lt;p&gt;ES 在全文搜索场景下更快，主要因为它使用倒排索引。&lt;/p&gt;
&lt;p&gt;MySQL 更擅长结构化数据查询和事务处理，全文检索不是它最核心的场景。&lt;/p&gt;
&lt;p&gt;可以这样回答：&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;ES 会先对文本分词，再建立词到文档的倒排索引。查询关键词时，可以快速定位包含该词的文档。MySQL B+Tree 索引更适合精确匹配和范围查询，对复杂全文搜索、相关性评分和分词检索不如 ES 合适。&lt;/p&gt;
&lt;/blockquote&gt;
&lt;div&gt;&lt;h2 id=&quot;系统与运维基础&quot;&gt;系统与运维基础&lt;/h2&gt;&lt;/div&gt;
&lt;div&gt;&lt;h3 id=&quot;什么是-docker&quot;&gt;什么是 Docker&lt;/h3&gt;&lt;/div&gt;
&lt;p&gt;Docker 是容器化技术，可以把应用和依赖打包成镜像，再以容器方式运行。&lt;/p&gt;
&lt;p&gt;它解决了“本地能跑，服务器不能跑”的环境一致性问题。&lt;/p&gt;
&lt;div&gt;&lt;h3 id=&quot;docker-和虚拟机有什么区别&quot;&gt;Docker 和虚拟机有什么区别&lt;/h3&gt;&lt;/div&gt;



































&lt;table&gt;&lt;thead&gt;&lt;tr&gt;&lt;th&gt;对比项&lt;/th&gt;&lt;th&gt;Docker&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;/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;资源占用&lt;/td&gt;&lt;td&gt;较少&lt;/td&gt;&lt;td&gt;较多&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;系统内核&lt;/td&gt;&lt;td&gt;共享宿主机内核&lt;/td&gt;&lt;td&gt;每个虚拟机有完整操作系统&lt;/td&gt;&lt;/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;/tbody&gt;&lt;/table&gt;
&lt;div&gt;&lt;h3 id=&quot;linux-常用命令有哪些&quot;&gt;Linux 常用命令有哪些&lt;/h3&gt;&lt;/div&gt;
&lt;p&gt;常见命令：&lt;/p&gt;
&lt;div&gt;&lt;figure&gt;&lt;figcaption&gt;&lt;span&gt;linux-commands.sh&lt;/span&gt;&lt;/figcaption&gt;&lt;pre&gt;&lt;code&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;ls&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;cd&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;pwd&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;mkdir&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;rm&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;cp&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;mv&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;cat&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;tail&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;-f&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;app.log&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;grep&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;&quot;&lt;/span&gt;&lt;span&gt;error&lt;/span&gt;&lt;span&gt;&quot;&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;app.log&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;ps&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;-ef&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;top&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;df&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;-h&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;free&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;-m&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;chmod&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;chown&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/figure&gt;&lt;/div&gt;
&lt;p&gt;面试时如果结合项目部署经历回答，会比单纯背命令更好。&lt;/p&gt;
&lt;div&gt;&lt;h3 id=&quot;什么是-rest-api&quot;&gt;什么是 REST API&lt;/h3&gt;&lt;/div&gt;
&lt;p&gt;REST API 是基于 REST 风格设计的接口。&lt;/p&gt;
&lt;p&gt;它通常使用 HTTP 协议，通过 URL 表示资源，通过 HTTP 方法表示操作。&lt;/p&gt;
&lt;p&gt;这个问题和 Spring 里的 RESTful API 本质相同。&lt;/p&gt;
&lt;div&gt;&lt;h3 id=&quot;什么是微服务架构&quot;&gt;什么是微服务架构&lt;/h3&gt;&lt;/div&gt;
&lt;p&gt;微服务是把一个大系统拆成多个小服务，每个服务负责一个相对独立的业务能力。&lt;/p&gt;
&lt;p&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;li&gt;团队边界更清晰。&lt;/li&gt;
&lt;/ul&gt;
&lt;p&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;div&gt;&lt;h3 id=&quot;什么是-api-网关&quot;&gt;什么是 API 网关&lt;/h3&gt;&lt;/div&gt;
&lt;p&gt;API 网关是系统入口，负责把外部请求转发到内部服务。&lt;/p&gt;
&lt;p&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;li&gt;熔断。&lt;/li&gt;
&lt;li&gt;日志。&lt;/li&gt;
&lt;li&gt;跨域处理。&lt;/li&gt;
&lt;/ul&gt;
&lt;div&gt;&lt;h2 id=&quot;项目深挖elasticsearch-项目&quot;&gt;项目深挖：Elasticsearch 项目&lt;/h2&gt;&lt;/div&gt;
&lt;p&gt;如果你的项目重点是 Elasticsearch，面试官很可能会围绕项目继续追问。&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;/tr&gt;&lt;/thead&gt;&lt;tbody&gt;&lt;tr&gt;&lt;td&gt;为什么项目要用 ES&lt;/td&gt;&lt;td&gt;MySQL 搜索能力不足、全文检索、分词、排序、性能&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;数据怎么同步到 ES&lt;/td&gt;&lt;td&gt;同步写、异步消息、定时补偿&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;ES 和 MySQL 数据不一致怎么办&lt;/td&gt;&lt;td&gt;重试、补偿任务、最终一致性&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;索引怎么设计&lt;/td&gt;&lt;td&gt;index、mapping、分词器、字段类型&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;filter、分页限制、字段选择、索引设计&lt;/td&gt;&lt;/tr&gt;&lt;/tbody&gt;&lt;/table&gt;
&lt;p&gt;面试时不要只说“我用了 ES”。更好的说法是：&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;我在项目中用 Elasticsearch 解决全文检索问题。MySQL 更适合事务和结构化查询，但对分词搜索、相关性排序支持有限。所以把需要搜索的数据同步到 ES，通过倒排索引提升搜索效率。项目里还需要考虑 MySQL 和 ES 的数据一致性，比如通过消息队列异步同步，并配合定时任务补偿。&lt;/p&gt;
&lt;/blockquote&gt;
&lt;div&gt;&lt;h2 id=&quot;学习能力问题&quot;&gt;学习能力问题&lt;/h2&gt;&lt;/div&gt;
&lt;p&gt;学习能力问题通常不会太难，但很考验真实感。&lt;/p&gt;
&lt;p&gt;常见问题：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;最近在学什么技术？&lt;/li&gt;
&lt;li&gt;为什么选择 Java？&lt;/li&gt;
&lt;li&gt;遇到不会的问题怎么解决？&lt;/li&gt;
&lt;li&gt;看过哪些技术文档？&lt;/li&gt;
&lt;li&gt;项目里最有收获的地方是什么？&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;回答建议：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;不要只说“我在学 Java”。&lt;/li&gt;
&lt;li&gt;要说具体学了什么、为什么学、怎么实践。&lt;/li&gt;
&lt;li&gt;最好能和项目或面试岗位关联起来。&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;例如：&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;最近我在补 Redis 和 Elasticsearch。Redis 主要关注缓存穿透、击穿、雪崩以及数据结构的使用场景；Elasticsearch 主要学习倒排索引、DSL 查询和数据同步。因为我的项目里有搜索场景，所以我想把搜索链路和缓存链路都理解得更完整。&lt;/p&gt;
&lt;/blockquote&gt;
&lt;div&gt;&lt;h2 id=&quot;复习优先级&quot;&gt;复习优先级&lt;/h2&gt;&lt;/div&gt;
&lt;p&gt;如果时间有限，可以按这个顺序准备：&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;Java 基础：集合、字符串、异常、反射。&lt;/li&gt;
&lt;li&gt;MySQL：索引、事务、MVCC、索引失效。&lt;/li&gt;
&lt;li&gt;Redis：数据结构、缓存问题、常用命令。&lt;/li&gt;
&lt;li&gt;Spring Boot：IoC、AOP、自动配置、REST API。&lt;/li&gt;
&lt;li&gt;JVM：内存结构、GC、OOM。&lt;/li&gt;
&lt;li&gt;并发：线程池、锁、volatile、死锁。&lt;/li&gt;
&lt;li&gt;Elasticsearch 项目：倒排索引、DSL、数据同步、搜索优化。&lt;/li&gt;
&lt;li&gt;Docker 和 Linux：能讲清楚部署和常用命令即可。&lt;/li&gt;
&lt;/ol&gt;
&lt;div&gt;&lt;h2 id=&quot;小结&quot;&gt;小结&lt;/h2&gt;&lt;/div&gt;
&lt;p&gt;Java 校招面试不是只背八股。&lt;/p&gt;
&lt;p&gt;基础题要能答清楚概念，项目题要能讲清楚自己做了什么、为什么这样做、遇到了什么问题、怎么解决。&lt;/p&gt;
&lt;p&gt;这份清单的重点不是一次性背完，而是把每个问题都整理成三层：&lt;/p&gt;
&lt;div&gt;&lt;figure&gt;&lt;figcaption&gt;&lt;/figcaption&gt;&lt;pre&gt;&lt;code&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;一句话结论&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;核心原理&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;项目或使用场景&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/figure&gt;&lt;/div&gt;
&lt;p&gt;这样回答时会更稳，也更像真的理解过。&lt;/p&gt;</content:encoded><category>Java</category><category>后端</category><category>面试</category></item><item><title>爬虫与 JS 逆向面试题复盘</title><link>https://blog.veyliss.top/blog/crawler_reverse_interview_01/</link><guid isPermaLink="true">https://blog.veyliss.top/blog/crawler_reverse_interview_01/</guid><pubDate>Tue, 16 Sep 2025 00:00:00 GMT</pubDate><content:encoded>&lt;p&gt;这是一组爬虫和 JS 逆向相关的面试题。&lt;/p&gt;
&lt;p&gt;这类面试不会只问“会不会用 &lt;code dir=&quot;auto&quot;&gt;requests&lt;/code&gt;”，更常见的是从一个具体业务场景开始追问：怎么登录、怎么抓动态接口、怎么处理反爬、百万级数据怎么调度、数据怎么进入后续处理链路。&lt;/p&gt;
&lt;p&gt;这篇文章按面试题复盘的方式整理，重点是把回答讲得更清楚、更工程化。&lt;/p&gt;
&lt;p&gt;同时要先明确一点：爬虫和逆向要遵守法律、站点协议和数据合规要求。面试中可以讲技术思路，但不应该表达绕过风控、攻击站点或采集敏感数据的意图。更稳的说法是：&lt;strong&gt;在授权范围内做数据采集和接口分析。&lt;/strong&gt;&lt;/p&gt;
&lt;div&gt;&lt;h2 id=&quot;自动登录怎么选&quot;&gt;自动登录怎么选&lt;/h2&gt;&lt;/div&gt;
&lt;p&gt;题目：&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;登录有两种方式，一种是账号密码登录，并且需要输入动态 token；另一种是二维码登录。如果要自动登录，你会选择哪种方式，为什么？说说实现方法。&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;我的回答倾向是：优先选择账号密码加 token 的方式。&lt;/p&gt;
&lt;p&gt;原因：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;账号密码登录更适合程序化请求。&lt;/li&gt;
&lt;li&gt;登录流程相对稳定，便于抓包分析。&lt;/li&gt;
&lt;li&gt;登录成功后可以拿到 token、cookie 或 session。&lt;/li&gt;
&lt;li&gt;二维码登录通常依赖人工扫码，不适合长期自动化任务。&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;可以这样回答：&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;我会优先选择账号密码加 token 的登录方式。因为它更容易通过请求和响应模拟，流程上可以先请求登录页或初始化接口，拿到登录所需的 token、cookie，再携带账号密码和动态 token 请求登录接口。登录成功后保存 cookie 或 access token，后续请求统一带上认证信息。二维码登录更适合人工确认，自动化成本更高，而且很多二维码登录会绑定设备、时效和扫码确认，不适合爬虫任务长期稳定运行。&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;一个简化流程：&lt;/p&gt;
&lt;div&gt;&lt;figure&gt;&lt;figcaption&gt;&lt;/figcaption&gt;&lt;pre&gt;&lt;code&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;请求登录页或初始化接口&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;&lt;span&gt;   &lt;/span&gt;&lt;/span&gt;&lt;span&gt;|&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;获取 token / csrf / cookie&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;&lt;span&gt;   &lt;/span&gt;&lt;/span&gt;&lt;span&gt;|&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;提交账号、密码、动态 token&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;&lt;span&gt;   &lt;/span&gt;&lt;/span&gt;&lt;span&gt;|&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;登录成功&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;&lt;span&gt;   &lt;/span&gt;&lt;/span&gt;&lt;span&gt;|&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;保存 cookie 或 access token&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;&lt;span&gt;   &lt;/span&gt;&lt;/span&gt;&lt;span&gt;|&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;后续请求携带认证信息&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/figure&gt;&lt;/div&gt;
&lt;p&gt;需要注意：如果动态 token 是验证码、短信码、二次验证，不能假设可以无成本自动化。面试里可以强调“在授权账号和合规场景下处理登录态”。&lt;/p&gt;
&lt;div&gt;&lt;h2 id=&quot;前端动态渲染的网站怎么爬&quot;&gt;前端动态渲染的网站怎么爬&lt;/h2&gt;&lt;/div&gt;
&lt;p&gt;题目：&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;这种前端返回数据的网站，如何爬取数据？&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;现在很多网站是前端框架渲染，HTML 源码里没有完整数据。此时不要急着解析页面，而是先看网络请求。&lt;/p&gt;
&lt;p&gt;常规步骤：&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;打开 Chrome DevTools。&lt;/li&gt;
&lt;li&gt;进入 Network 面板。&lt;/li&gt;
&lt;li&gt;过滤 XHR / Fetch 请求。&lt;/li&gt;
&lt;li&gt;找到真正返回 JSON 数据的接口。&lt;/li&gt;
&lt;li&gt;分析 URL、请求方法、参数、Headers、Cookie。&lt;/li&gt;
&lt;li&gt;用 Python 模拟请求。&lt;/li&gt;
&lt;/ol&gt;
&lt;div&gt;&lt;figure&gt;&lt;figcaption&gt;&lt;span&gt;fetch-api-data.py&lt;/span&gt;&lt;/figcaption&gt;&lt;pre&gt;&lt;code&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;import&lt;/span&gt;&lt;span&gt; requests&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;
&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;url &lt;/span&gt;&lt;span&gt;=&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;&quot;&lt;/span&gt;&lt;span&gt;https://example.com/api/list&lt;/span&gt;&lt;span&gt;&quot;&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;
&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;headers &lt;/span&gt;&lt;span&gt;=&lt;/span&gt;&lt;span&gt; {&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;    &lt;/span&gt;&lt;span&gt;&quot;&lt;/span&gt;&lt;span&gt;User-Agent&lt;/span&gt;&lt;span&gt;&quot;&lt;/span&gt;&lt;span&gt;: &lt;/span&gt;&lt;span&gt;&quot;&lt;/span&gt;&lt;span&gt;Mozilla/5.0&lt;/span&gt;&lt;span&gt;&quot;&lt;/span&gt;&lt;span&gt;,&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;    &lt;/span&gt;&lt;span&gt;&quot;&lt;/span&gt;&lt;span&gt;Referer&lt;/span&gt;&lt;span&gt;&quot;&lt;/span&gt;&lt;span&gt;: &lt;/span&gt;&lt;span&gt;&quot;&lt;/span&gt;&lt;span&gt;https://example.com/list&lt;/span&gt;&lt;span&gt;&quot;&lt;/span&gt;&lt;span&gt;,&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;}&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;
&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;params &lt;/span&gt;&lt;span&gt;=&lt;/span&gt;&lt;span&gt; {&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;    &lt;/span&gt;&lt;span&gt;&quot;&lt;/span&gt;&lt;span&gt;page&lt;/span&gt;&lt;span&gt;&quot;&lt;/span&gt;&lt;span&gt;: &lt;/span&gt;&lt;span&gt;1&lt;/span&gt;&lt;span&gt;,&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;    &lt;/span&gt;&lt;span&gt;&quot;&lt;/span&gt;&lt;span&gt;size&lt;/span&gt;&lt;span&gt;&quot;&lt;/span&gt;&lt;span&gt;: &lt;/span&gt;&lt;span&gt;20&lt;/span&gt;&lt;span&gt;,&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;}&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;
&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;response &lt;/span&gt;&lt;span&gt;=&lt;/span&gt;&lt;span&gt; requests.&lt;/span&gt;&lt;span&gt;get&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;url&lt;/span&gt;&lt;span&gt;,&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;headers&lt;/span&gt;&lt;span&gt;=&lt;/span&gt;&lt;span&gt;headers&lt;/span&gt;&lt;span&gt;,&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;params&lt;/span&gt;&lt;span&gt;=&lt;/span&gt;&lt;span&gt;params&lt;/span&gt;&lt;span&gt;,&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;timeout&lt;/span&gt;&lt;span&gt;=&lt;/span&gt;&lt;span&gt;10&lt;/span&gt;&lt;span&gt;)&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;data &lt;/span&gt;&lt;span&gt;=&lt;/span&gt;&lt;span&gt; response.&lt;/span&gt;&lt;span&gt;json&lt;/span&gt;&lt;span&gt;()&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;
&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;print&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;data&lt;/span&gt;&lt;span&gt;)&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/figure&gt;&lt;/div&gt;
&lt;p&gt;如果接口参数是动态生成的，就继续分析 JS。&lt;/p&gt;
&lt;p&gt;如果页面确实没有接口，或者数据必须通过浏览器运行后才出现，可以考虑 Selenium 或 Playwright。但大规模采集时，优先分析接口，因为浏览器自动化成本更高。&lt;/p&gt;
&lt;div&gt;&lt;h2 id=&quot;百万级数据怎么爬&quot;&gt;百万级数据怎么爬&lt;/h2&gt;&lt;/div&gt;
&lt;p&gt;题目：&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;面对百万甚至千万数据量的爬取，你的爬取策略是怎么样的？爬取到的数据如何存储？&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;这题考察的是系统设计，而不是单机脚本。&lt;/p&gt;
&lt;p&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;li&gt;数据存储和后续处理。&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;一个比较完整的链路是：&lt;/p&gt;
&lt;div&gt;&lt;figure&gt;&lt;figcaption&gt;&lt;/figcaption&gt;&lt;pre&gt;&lt;code&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;任务调度 -&gt; 爬虫采集 -&gt; Kafka -&gt; Flink 清洗 -&gt; 数据存储&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/figure&gt;&lt;/div&gt;
&lt;p&gt;如果只是普通项目，可以存 MySQL 或 CSV；如果是百万、千万规模，就要考虑分批写入、去重、失败重试、数据清洗和存储扩展。&lt;/p&gt;
&lt;p&gt;可以这样回答：&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;面对百万级数据，我不会用一个单机脚本顺序爬。一般会先把任务拆成分页任务、关键词任务或 ID 区间任务，放到任务队列里，由多个爬虫节点并发消费。采集时会限制请求频率，设置超时重试和代理池，避免单点 IP 或账号压力过大。采集到的数据先进入 Kafka，后续由 Flink 做实时清洗，再写入 MySQL、ES 或数据仓库。对于失败任务会记录状态，后续补偿重试。&lt;/p&gt;
&lt;/blockquote&gt;
&lt;div&gt;&lt;h2 id=&quot;平时用什么工具分析网站接口&quot;&gt;平时用什么工具分析网站接口&lt;/h2&gt;&lt;/div&gt;
&lt;p&gt;常用 Chrome DevTools 的 Network 面板。&lt;/p&gt;
&lt;p&gt;主要看：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;XHR / Fetch 请求。&lt;/li&gt;
&lt;li&gt;请求 URL。&lt;/li&gt;
&lt;li&gt;请求方法。&lt;/li&gt;
&lt;li&gt;Query 参数和 Request Payload。&lt;/li&gt;
&lt;li&gt;Headers。&lt;/li&gt;
&lt;li&gt;Cookie。&lt;/li&gt;
&lt;li&gt;Response。&lt;/li&gt;
&lt;li&gt;Initiator 调用来源。&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;如果参数是动态生成的，会继续去 Sources 面板断点调试，或在 JS 文件中搜索参数名。&lt;/p&gt;
&lt;div&gt;&lt;h2 id=&quot;如何判断网站是否有反爬&quot;&gt;如何判断网站是否有反爬&lt;/h2&gt;&lt;/div&gt;
&lt;p&gt;可以从几个现象判断：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;请求参数里存在动态加密参数。&lt;/li&gt;
&lt;li&gt;接口依赖 token、cookie、签名或时间戳。&lt;/li&gt;
&lt;li&gt;请求频率过高会被封 IP。&lt;/li&gt;
&lt;li&gt;返回内容出现验证码、空数据或风控页面。&lt;/li&gt;
&lt;li&gt;同一个接口在浏览器能访问，程序请求失败。&lt;/li&gt;
&lt;li&gt;Headers 缺失时返回异常。&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;可以这样回答：&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;我会先比较浏览器正常请求和程序模拟请求的差异。如果同样的 URL 在浏览器里返回正常，但程序里返回空数据、验证码、403 或风控响应，就说明可能存在反爬。再继续分析是否有动态参数、token 校验、cookie 校验、频率限制或行为检测。&lt;/p&gt;
&lt;/blockquote&gt;
&lt;div&gt;&lt;h2 id=&quot;xpath-和-css-selector-的区别&quot;&gt;XPath 和 CSS Selector 的区别&lt;/h2&gt;&lt;/div&gt;
&lt;p&gt;XPath 和 CSS Selector 都能定位 HTML 节点。&lt;/p&gt;






























&lt;table&gt;&lt;thead&gt;&lt;tr&gt;&lt;th&gt;对比项&lt;/th&gt;&lt;th&gt;XPath&lt;/th&gt;&lt;th&gt;CSS Selector&lt;/th&gt;&lt;/tr&gt;&lt;/thead&gt;&lt;tbody&gt;&lt;tr&gt;&lt;td&gt;语法&lt;/td&gt;&lt;td&gt;类似路径表达式&lt;/td&gt;&lt;td&gt;类似 CSS 选择器&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;能力&lt;/td&gt;&lt;td&gt;更强，支持轴、文本、复杂路径&lt;/td&gt;&lt;td&gt;简洁，适合常见选择&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;可读性&lt;/td&gt;&lt;td&gt;复杂表达式可读性一般&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;/tbody&gt;&lt;/table&gt;
&lt;p&gt;面试可以说：&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;简单页面我会用 CSS Selector，因为语法简洁；复杂定位，比如按文本、层级、相邻节点查找时，我更倾向 XPath。&lt;/p&gt;
&lt;/blockquote&gt;
&lt;div&gt;&lt;h2 id=&quot;做过-js-逆向吗&quot;&gt;做过 JS 逆向吗&lt;/h2&gt;&lt;/div&gt;
&lt;p&gt;可以按流程回答：&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;使用 Network 抓包找到目标接口。&lt;/li&gt;
&lt;li&gt;确认哪个参数是动态生成的。&lt;/li&gt;
&lt;li&gt;全局搜索参数名。&lt;/li&gt;
&lt;li&gt;在 Sources 面板下断点。&lt;/li&gt;
&lt;li&gt;观察 Call Stack 调用链。&lt;/li&gt;
&lt;li&gt;找到最终生成参数的函数。&lt;/li&gt;
&lt;li&gt;用 Python 或 Node 复现算法。&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;更完整的回答：&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;我在项目中遇到过接口参数由 JS 加密生成的情况。处理时先通过 Network 找到接口和异常参数，然后在 JS 文件中搜索参数名。如果搜索不到，就从请求发起位置或 XHR 断点入手，在 Sources 里下断点，结合 Call Stack 分析调用链，找到参数生成函数。确定算法后，再用 Python 或 Node 复现，最后和浏览器生成结果对比，确保请求参数一致。&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;这类回答要强调“分析和复现授权接口参数”，不要说成攻击或绕过安全系统。&lt;/p&gt;
&lt;div&gt;&lt;h2 id=&quot;js-混淆怎么分析&quot;&gt;JS 混淆怎么分析&lt;/h2&gt;&lt;/div&gt;
&lt;p&gt;JS 混淆后，变量名和函数名可能没有意义，所以不要期待完全看懂所有代码。&lt;/p&gt;
&lt;p&gt;常见思路：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;不全量还原，只找关键链路。&lt;/li&gt;
&lt;li&gt;通过 XHR/fetch 断点定位请求发起位置。&lt;/li&gt;
&lt;li&gt;使用 Call Stack 看调用链。&lt;/li&gt;
&lt;li&gt;打印关键变量。&lt;/li&gt;
&lt;li&gt;对关键函数做输入输出对比。&lt;/li&gt;
&lt;li&gt;必要时把关键函数拎出来运行。&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;可以这样回答：&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;遇到混淆 JS 时，我不会从头读完整文件，而是围绕目标接口定位关键参数。通过断点、调用栈、关键变量打印和函数输入输出分析，逐步缩小范围，最终定位生成参数的函数。&lt;/p&gt;
&lt;/blockquote&gt;
&lt;div&gt;&lt;h2 id=&quot;为什么不用-selenium&quot;&gt;为什么不用 Selenium&lt;/h2&gt;&lt;/div&gt;
&lt;p&gt;Selenium 是浏览器自动化工具，适合复杂页面、需要真实浏览器环境的场景。&lt;/p&gt;
&lt;p&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;li&gt;速度慢。&lt;/li&gt;
&lt;li&gt;大规模采集不划算。&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;所以一般优先分析接口直接请求。只有接口很难复现、页面强依赖浏览器环境、或需要真实交互时，才考虑 Selenium 或 Playwright。&lt;/p&gt;
&lt;p&gt;面试回答：&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;Selenium 可以用，但我不会作为首选。因为大规模采集更关注吞吐和稳定性，直接请求接口效率更高。Selenium 更适合登录、复杂交互或无法绕开浏览器渲染的页面。&lt;/p&gt;
&lt;/blockquote&gt;
&lt;div&gt;&lt;h2 id=&quot;如何保证爬虫长期稳定运行&quot;&gt;如何保证爬虫长期稳定运行&lt;/h2&gt;&lt;/div&gt;
&lt;p&gt;长期稳定运行靠的不是一个脚本，而是容错和监控。&lt;/p&gt;
&lt;p&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;li&gt;异常捕获。&lt;/li&gt;
&lt;li&gt;失败任务记录。&lt;/li&gt;
&lt;li&gt;账号状态检测。&lt;/li&gt;
&lt;li&gt;IP 或代理状态检测。&lt;/li&gt;
&lt;li&gt;任务监控。&lt;/li&gt;
&lt;li&gt;健康检查。&lt;/li&gt;
&lt;li&gt;失败告警。&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;可以这样回答：&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;我会为爬虫设计超时重试、异常捕获、失败任务记录和任务监控机制。如果请求失败，会根据错误类型决定重试、切换账号、切换代理或标记任务失败。系统层面会有健康监测和失败上报，保证爬虫可以长期稳定运行。&lt;/p&gt;
&lt;/blockquote&gt;
&lt;div&gt;&lt;h2 id=&quot;爬虫规模是多少&quot;&gt;爬虫规模是多少&lt;/h2&gt;&lt;/div&gt;
&lt;p&gt;如果项目里支持 &lt;code dir=&quot;auto&quot;&gt;500万+ / 日&lt;/code&gt; 的采集规模，可以这样回答：&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;系统支持 500 万以上日采集量。采集任务不是由单个脚本完成，而是通过任务调度系统统一拆分和分发，多节点并发执行。采集结果进入 Kafka，再由 Flink 进行实时清洗和处理。&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;面试时不要只报数字，最好补上支撑数字的架构。&lt;/p&gt;
&lt;div&gt;&lt;h2 id=&quot;系统架构是什么&quot;&gt;系统架构是什么&lt;/h2&gt;&lt;/div&gt;
&lt;p&gt;整体链路可以这样描述：&lt;/p&gt;
&lt;div&gt;&lt;figure&gt;&lt;figcaption&gt;&lt;/figcaption&gt;&lt;pre&gt;&lt;code&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;调度系统&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;&lt;span&gt;   &lt;/span&gt;&lt;/span&gt;&lt;span&gt;|&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;&lt;span&gt;   &lt;/span&gt;&lt;/span&gt;&lt;span&gt;v&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;爬虫节点&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;&lt;span&gt;   &lt;/span&gt;&lt;/span&gt;&lt;span&gt;|&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;&lt;span&gt;   &lt;/span&gt;&lt;/span&gt;&lt;span&gt;v&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;Kafka&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;&lt;span&gt;   &lt;/span&gt;&lt;/span&gt;&lt;span&gt;|&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;&lt;span&gt;   &lt;/span&gt;&lt;/span&gt;&lt;span&gt;v&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;Flink&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;&lt;span&gt;   &lt;/span&gt;&lt;/span&gt;&lt;span&gt;|&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;&lt;span&gt;   &lt;/span&gt;&lt;/span&gt;&lt;span&gt;v&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;MySQL / Elasticsearch / 数据仓库&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/figure&gt;&lt;/div&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;调度系统&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;tr&gt;&lt;td&gt;Kafka&lt;/td&gt;&lt;td&gt;解耦采集和处理，缓冲流量&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;Flink&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;这类回答会比“我用 Scrapy 分布式”更有工程感。&lt;/p&gt;
&lt;div&gt;&lt;h2 id=&quot;调度系统怎么工作&quot;&gt;调度系统怎么工作&lt;/h2&gt;&lt;/div&gt;
&lt;p&gt;调度系统主要负责任务生成和账号协调。&lt;/p&gt;
&lt;p&gt;你笔记中的规模是：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;1400+ 爬虫任务。&lt;/li&gt;
&lt;li&gt;400+ 账号 Cookie。&lt;/li&gt;
&lt;li&gt;任务信息存储在 Redis。&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;可以这样回答：&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;调度系统会把采集目标拆成具体任务，任务状态存储在 Redis 中。爬虫节点从 Redis 获取任务，执行后回写任务状态。账号 Cookie 也由调度系统统一管理，分配任务时会根据账号状态选择可用账号，避免单个账号压力过大。&lt;/p&gt;
&lt;/blockquote&gt;
&lt;div&gt;&lt;h2 id=&quot;为什么任务放-redis&quot;&gt;为什么任务放 Redis&lt;/h2&gt;&lt;/div&gt;
&lt;p&gt;Redis 适合做任务队列和状态缓存。&lt;/p&gt;
&lt;p&gt;原因：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;读写性能高。&lt;/li&gt;
&lt;li&gt;支持 List、Set、Hash、Sorted Set 等结构。&lt;/li&gt;
&lt;li&gt;适合存任务状态、账号状态和临时调度数据。&lt;/li&gt;
&lt;li&gt;操作简单，延迟低。&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;可以补一句：&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;如果任务需要更强的可靠性、确认机制和重试语义，也可以引入消息队列；Redis 更适合轻量级任务调度和状态管理。&lt;/p&gt;
&lt;/blockquote&gt;
&lt;div&gt;&lt;h2 id=&quot;如何处理账号失效&quot;&gt;如何处理账号失效&lt;/h2&gt;&lt;/div&gt;
&lt;p&gt;账号失效的表现：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;登录失败。&lt;/li&gt;
&lt;li&gt;Cookie 失效。&lt;/li&gt;
&lt;li&gt;返回 401、403。&lt;/li&gt;
&lt;li&gt;返回验证码或风控页面。&lt;/li&gt;
&lt;li&gt;请求结果为空或异常。&lt;/li&gt;
&lt;/ul&gt;
&lt;p&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;li&gt;切换可用账号。&lt;/li&gt;
&lt;li&gt;触发重新登录或人工处理。&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;可以这样回答：&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;系统会根据响应状态和页面内容判断账号是否异常。一旦发现 Cookie 失效或登录状态异常，就标记账号状态，避免继续分配任务，同时把未完成任务重新放回队列，交给其他可用账号处理。&lt;/p&gt;
&lt;/blockquote&gt;
&lt;div&gt;&lt;h2 id=&quot;python-爬虫常用库&quot;&gt;Python 爬虫常用库&lt;/h2&gt;&lt;/div&gt;
&lt;p&gt;常用库：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code dir=&quot;auto&quot;&gt;requests&lt;/code&gt;：发送 HTTP 请求。&lt;/li&gt;
&lt;li&gt;&lt;code dir=&quot;auto&quot;&gt;httpx&lt;/code&gt;：支持同步和异步请求。&lt;/li&gt;
&lt;li&gt;&lt;code dir=&quot;auto&quot;&gt;scrapy&lt;/code&gt;：爬虫框架。&lt;/li&gt;
&lt;li&gt;&lt;code dir=&quot;auto&quot;&gt;lxml&lt;/code&gt;：解析 HTML，支持 XPath。&lt;/li&gt;
&lt;li&gt;&lt;code dir=&quot;auto&quot;&gt;beautifulsoup4&lt;/code&gt;：HTML 解析。&lt;/li&gt;
&lt;li&gt;&lt;code dir=&quot;auto&quot;&gt;selenium&lt;/code&gt;：浏览器自动化。&lt;/li&gt;
&lt;li&gt;&lt;code dir=&quot;auto&quot;&gt;playwright&lt;/code&gt;：现代浏览器自动化。&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;项目里如果主要使用 &lt;code dir=&quot;auto&quot;&gt;requests + XPath&lt;/code&gt;，可以这样说：&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;普通接口采集我主要使用 requests，请求接口后用 XPath 或 JSON 解析数据。如果是复杂任务调度和大规模采集，会考虑 Scrapy 或自研调度系统。&lt;/p&gt;
&lt;/blockquote&gt;
&lt;div&gt;&lt;h2 id=&quot;如何处理请求超时&quot;&gt;如何处理请求超时&lt;/h2&gt;&lt;/div&gt;
&lt;p&gt;基本做法：&lt;/p&gt;
&lt;div&gt;&lt;figure&gt;&lt;figcaption&gt;&lt;span&gt;request-timeout.py&lt;/span&gt;&lt;/figcaption&gt;&lt;pre&gt;&lt;code&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;import&lt;/span&gt;&lt;span&gt; requests&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;
&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;try&lt;/span&gt;&lt;span&gt;:&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;&lt;span&gt;    &lt;/span&gt;&lt;/span&gt;&lt;span&gt;response &lt;/span&gt;&lt;span&gt;=&lt;/span&gt;&lt;span&gt; requests.&lt;/span&gt;&lt;span&gt;get&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;        &lt;/span&gt;&lt;span&gt;&quot;&lt;/span&gt;&lt;span&gt;https://example.com/api&lt;/span&gt;&lt;span&gt;&quot;&lt;/span&gt;&lt;span&gt;,&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;        &lt;/span&gt;&lt;span&gt;timeout&lt;/span&gt;&lt;span&gt;=&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;3&lt;/span&gt;&lt;span&gt;, &lt;/span&gt;&lt;span&gt;10&lt;/span&gt;&lt;span&gt;)&lt;/span&gt;&lt;span&gt;,&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;    &lt;/span&gt;&lt;span&gt;)&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;&lt;span&gt;    &lt;/span&gt;&lt;/span&gt;&lt;span&gt;response.&lt;/span&gt;&lt;span&gt;raise_for_status&lt;/span&gt;&lt;span&gt;()&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;except&lt;/span&gt;&lt;span&gt; requests.Timeout:&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;    &lt;/span&gt;&lt;span&gt;# 记录超时并重试&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;    &lt;/span&gt;&lt;span&gt;pass&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;except&lt;/span&gt;&lt;span&gt; requests.RequestException:&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;    &lt;/span&gt;&lt;span&gt;# 记录其他请求异常&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;    &lt;/span&gt;&lt;span&gt;pass&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/figure&gt;&lt;/div&gt;
&lt;p&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;li&gt;切换代理或账号。&lt;/li&gt;
&lt;/ul&gt;
&lt;div&gt;&lt;h2 id=&quot;如何控制爬虫速度&quot;&gt;如何控制爬虫速度&lt;/h2&gt;&lt;/div&gt;
&lt;p&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;li&gt;对单域名限速。&lt;/li&gt;
&lt;li&gt;对单账号限速。&lt;/li&gt;
&lt;li&gt;对异常响应动态降速。&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;面试里可以说：&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;控制速度不只是 sleep，而是结合并发数、任务队列、账号维度和站点响应来动态调整，避免触发反爬，也保护目标站点和自身系统。&lt;/p&gt;
&lt;/blockquote&gt;
&lt;div&gt;&lt;h2 id=&quot;为什么使用-docker&quot;&gt;为什么使用 Docker&lt;/h2&gt;&lt;/div&gt;
&lt;p&gt;Docker 的价值：&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;li&gt;便于隔离依赖。&lt;/li&gt;
&lt;li&gt;适合配合 CI/CD。&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;爬虫系统里尤其适合把爬虫节点容器化。需要扩容时，可以快速启动多个容器实例。&lt;/p&gt;
&lt;div&gt;&lt;h2 id=&quot;kafka-在系统中作用&quot;&gt;Kafka 在系统中作用&lt;/h2&gt;&lt;/div&gt;
&lt;p&gt;Kafka 主要承担数据通道和缓冲层。&lt;/p&gt;
&lt;p&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;li&gt;方便后续多个消费者处理数据。&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;可以这样回答：&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;爬虫采集速度和后续清洗入库速度不一定一致，所以中间用 Kafka 解耦。爬虫只负责把原始数据写入 Kafka，Flink 再从 Kafka 消费并清洗处理。&lt;/p&gt;
&lt;/blockquote&gt;
&lt;div&gt;&lt;h2 id=&quot;redis-在项目中用来做什么&quot;&gt;Redis 在项目中用来做什么&lt;/h2&gt;&lt;/div&gt;
&lt;p&gt;Redis 在项目中可以承担：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;任务队列。&lt;/li&gt;
&lt;li&gt;任务状态缓存。&lt;/li&gt;
&lt;li&gt;账号 Cookie 管理。&lt;/li&gt;
&lt;li&gt;去重集合。&lt;/li&gt;
&lt;li&gt;临时失败记录。&lt;/li&gt;
&lt;li&gt;限速计数。&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;面试回答：&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;Redis 主要用于调度层，保存任务队列、任务状态和账号 Cookie。因为它读写快，并且数据结构丰富，适合管理这种高频变化的临时状态。&lt;/p&gt;
&lt;/blockquote&gt;
&lt;div&gt;&lt;h2 id=&quot;项目中最难的部分是什么&quot;&gt;项目中最难的部分是什么&lt;/h2&gt;&lt;/div&gt;
&lt;p&gt;可以回答 JS 加密参数逆向。&lt;/p&gt;
&lt;p&gt;更完整的说法：&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;最难的是 JS 加密参数逆向。因为网站 JS 做了混淆，不能直接通过阅读代码看懂逻辑。我通过 Network 定位接口和动态参数，再用 Sources 下断点，结合调用栈分析参数生成流程，最后把关键算法用 Python 或 Node 复现出来。这个过程比较考验调试能力和耐心。&lt;/p&gt;
&lt;/blockquote&gt;
&lt;div&gt;&lt;h2 id=&quot;如果网站增加新的反爬怎么办&quot;&gt;如果网站增加新的反爬怎么办&lt;/h2&gt;&lt;/div&gt;
&lt;p&gt;处理步骤：&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;先复现问题，确认是哪些请求失败。&lt;/li&gt;
&lt;li&gt;对比正常浏览器请求和爬虫请求差异。&lt;/li&gt;
&lt;li&gt;判断新增机制：token、cookie、签名、频率、验证码、行为检测。&lt;/li&gt;
&lt;li&gt;如果是参数变化，重新调试 JS。&lt;/li&gt;
&lt;li&gt;如果是频率问题，调整限速和调度策略。&lt;/li&gt;
&lt;li&gt;如果涉及强验证或合规风险，停止采集或走授权接口。&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;可以这样回答：&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;我会先分析新增反爬属于哪一类，再决定策略。如果是参数签名变化，就重新定位 JS 生成逻辑；如果是频率限制，就降低并发、调整账号和代理策略；如果是登录态或 Cookie 变化，就更新账号状态检测和重新登录流程。对于验证码或强风控场景，需要评估合规性，不能盲目绕过。&lt;/p&gt;
&lt;/blockquote&gt;
&lt;div&gt;&lt;h2 id=&quot;面试回答模板&quot;&gt;面试回答模板&lt;/h2&gt;&lt;/div&gt;
&lt;p&gt;如果面试官让你整体介绍这个爬虫项目，可以这样组织：&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;这个项目主要做大规模数据采集。整体链路是爬虫采集、Kafka 缓冲、Flink 清洗、最终写入存储。爬虫侧通过 Chrome DevTools 分析接口，优先直接请求接口而不是 Selenium。调度系统负责管理 1400 多个任务和 400 多个账号 Cookie，任务状态存储在 Redis。系统支持超时重试、失败任务记录、账号失效检测和健康监控。项目中比较难的是 JS 加密参数逆向，我通过断点调试、调用栈分析和算法复现解决过接口动态参数问题。&lt;/p&gt;
&lt;/blockquote&gt;
&lt;div&gt;&lt;h2 id=&quot;小结&quot;&gt;小结&lt;/h2&gt;&lt;/div&gt;
&lt;p&gt;爬虫和逆向面试题，重点不是只会某个库，而是能把采集链路讲完整：&lt;/p&gt;
&lt;div&gt;&lt;figure&gt;&lt;figcaption&gt;&lt;/figcaption&gt;&lt;pre&gt;&lt;code&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;接口分析 -&gt; 登录态处理 -&gt; 参数逆向 -&gt; 任务调度 -&gt; 并发控制 -&gt; 数据通道 -&gt; 清洗入库 -&gt; 监控补偿&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/figure&gt;&lt;/div&gt;
&lt;p&gt;如果能把这条链路讲清楚，再结合自己实际做过的规模、账号调度、Kafka/Flink、Redis 和 Docker，回答就会更像真实项目经验，而不是零散知识点。&lt;/p&gt;</content:encoded><category>Python</category><category>爬虫</category><category>面试</category></item><item><title>一道 gRPC 简易计算器面试题复盘</title><link>https://blog.veyliss.top/blog/grpc_calculator_interview_01/</link><guid isPermaLink="true">https://blog.veyliss.top/blog/grpc_calculator_interview_01/</guid><pubDate>Fri, 11 Apr 2025 00:00:00 GMT</pubDate><content:encoded>&lt;p&gt;这是我曾经做过的一道面试题中的一个小项目：用 &lt;strong&gt;Go + gRPC + gRPC-Web + Next.js&lt;/strong&gt; 做一个简易计算器。&lt;/p&gt;
&lt;p&gt;题目本身不复杂，只有加、减、乘、除四种运算。但它真正考察的不是“会不会写计算器”，而是能不能把前后端通信、接口契约、错误处理和跨端调用讲清楚。&lt;/p&gt;
&lt;p&gt;项目地址：&lt;a href=&quot;https://github.com/veyliss/GolangNextGrpcSimpleCalculator&quot;&gt;GolangNextGrpcSimpleCalculator&lt;/a&gt;&lt;/p&gt;
&lt;div&gt;&lt;h2 id=&quot;题目要做什么&quot;&gt;题目要做什么&lt;/h2&gt;&lt;/div&gt;
&lt;p&gt;这道题可以理解为：&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;前端输入两个数字和一个运算符，通过 gRPC-Web 调用 Go 后端，后端完成计算并返回结果。&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;最小功能闭环包括：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;前端页面可以输入两个操作数。&lt;/li&gt;
&lt;li&gt;前端可以选择 &lt;code dir=&quot;auto&quot;&gt;+&lt;/code&gt;、&lt;code dir=&quot;auto&quot;&gt;-&lt;/code&gt;、&lt;code dir=&quot;auto&quot;&gt;*&lt;/code&gt;、&lt;code dir=&quot;auto&quot;&gt;/&lt;/code&gt; 四种运算符。&lt;/li&gt;
&lt;li&gt;前端通过 gRPC-Web 调用后端。&lt;/li&gt;
&lt;li&gt;后端使用 Go 实现 gRPC 服务。&lt;/li&gt;
&lt;li&gt;后端根据请求参数返回计算结果。&lt;/li&gt;
&lt;li&gt;除数为 &lt;code dir=&quot;auto&quot;&gt;0&lt;/code&gt; 或未知运算符时，需要返回错误。&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;如果只是做一个普通 HTTP 接口，这题可能很快就结束了。但这里要求使用 gRPC，就会多出一层“接口契约”的设计：前后端都要围绕 &lt;code dir=&quot;auto&quot;&gt;.proto&lt;/code&gt; 文件生成代码。&lt;/p&gt;
&lt;div&gt;&lt;h2 id=&quot;项目结构&quot;&gt;项目结构&lt;/h2&gt;&lt;/div&gt;
&lt;p&gt;这个项目大致可以拆成两部分：&lt;/p&gt;
&lt;starlight-file-tree data-pagefind-ignore=&quot;true&quot;&gt;&lt;ul&gt;&lt;li&gt;&lt;details open&gt;&lt;summary&gt;&lt;span&gt;&lt;span&gt;&lt;span&gt;&lt;svg width=&quot;16&quot; height=&quot;16&quot; aria-hidden=&quot;true&quot; viewBox=&quot;0 0 24 24&quot;&gt;&lt;path d=&quot;M22.073 4.900L22.073 4.900L12.148 4.900L12.148 3.950Q12.148 3.125 11.585 2.563Q11.023 2 10.198 2L10.198 2L0.048 2L0.048 22L23.948 22L23.948 6.850Q23.998 6.025 23.448 5.462Q22.898 4.900 22.073 4.900Z&quot;&gt;&lt;/path&gt;&lt;/svg&gt;&lt;/span&gt;calculator-backend/
&lt;/span&gt;&lt;/span&gt;&lt;/summary&gt;&lt;ul&gt;&lt;li&gt;&lt;details open&gt;&lt;summary&gt;&lt;span&gt;&lt;span&gt;&lt;span&gt;&lt;svg width=&quot;16&quot; height=&quot;16&quot; aria-hidden=&quot;true&quot; viewBox=&quot;0 0 24 24&quot;&gt;&lt;path d=&quot;M22.073 4.900L22.073 4.900L12.148 4.900L12.148 3.950Q12.148 3.125 11.585 2.563Q11.023 2 10.198 2L10.198 2L0.048 2L0.048 22L23.948 22L23.948 6.850Q23.998 6.025 23.448 5.462Q22.898 4.900 22.073 4.900Z&quot;&gt;&lt;/path&gt;&lt;/svg&gt;&lt;/span&gt;calculator/
&lt;/span&gt;&lt;/span&gt;&lt;/summary&gt;&lt;ul&gt;&lt;li&gt;&lt;span&gt;&lt;span&gt;&lt;span&gt;&lt;svg width=&quot;16&quot; height=&quot;16&quot; aria-hidden=&quot;true&quot; viewBox=&quot;0 0 24 24&quot;&gt;&lt;path d=&quot;M1.082 16.876L1.082 14.014L22.918 14.014L22.918 16.876L1.082 16.876ZM1.082 9.986L1.082 7.071L13.272 7.071L13.272 9.986L1.082 9.986ZM1.082 3.096L1.082 0.181L22.918 0.181L22.918 3.096L1.082 3.096ZM1.082 23.819L1.082 20.904L17.300 20.904L17.300 23.819L1.082 23.819Z&quot;&gt;&lt;/path&gt;&lt;/svg&gt;&lt;/span&gt;calculator.proto&lt;/span&gt;&lt;/span&gt;&lt;/li&gt;&lt;/ul&gt;&lt;/details&gt;&lt;/li&gt;&lt;li&gt;&lt;span&gt;&lt;span&gt;&lt;span&gt;&lt;svg width=&quot;16&quot; height=&quot;16&quot; aria-hidden=&quot;true&quot; viewBox=&quot;0 0 24 24&quot;&gt;&lt;path d=&quot;M6.119 10.273L1.933 10.247Q1.855 10.247 1.881 10.195L1.881 10.195L2.141 9.883Q2.167 9.831 2.271 9.831L2.271 9.831L6.379 9.831Q6.457 9.831 6.431 9.883L6.431 9.883L6.223 10.195Q6.171 10.273 6.119 10.273L6.119 10.273ZM5.651 11.313L0.191 11.313Q0.113 11.313 0.139 11.261L0.139 11.261L0.399 10.949Q0.425 10.897 0.529 10.897L0.529 10.897L5.781 10.897Q5.859 10.897 5.833 10.949L5.833 10.949L5.755 11.235Q5.729 11.313 5.651 11.313L5.651 11.313ZM5.495 12.379L2.973 12.379Q2.895 12.379 2.947 12.301L2.947 12.301L3.103 12.015Q3.155 11.963 3.233 11.963L3.233 11.963L5.521 11.963Q5.599 11.963 5.599 12.041L5.599 12.041L5.573 12.301Q5.573 12.379 5.495 12.379L5.495 12.379ZM14.985 10.039L14.985 10.039Q14.569 10.143 13.893 10.325L13.893 10.325L13.009 10.559Q12.905 10.585 12.853 10.572Q12.801 10.559 12.697 10.429L12.697 10.429Q12.411 10.117 12.151 9.987L12.151 9.987Q11.111 9.493 10.071 10.169L10.071 10.169Q8.875 10.949 8.875 12.379L8.875 12.379Q8.901 13.055 9.330 13.575Q9.759 14.095 10.435 14.173L10.435 14.173Q11.605 14.329 12.411 13.419L12.411 13.419L12.723 13.003L10.487 13.003Q10.305 13.003 10.253 12.899Q10.201 12.795 10.279 12.639L10.279 12.639Q10.591 11.911 10.851 11.391L10.851 11.391Q10.955 11.209 11.137 11.209L11.137 11.209L15.349 11.209L15.349 11.521Q15.323 11.937 15.297 12.145L15.297 12.145Q15.089 13.419 14.335 14.407L14.335 14.407Q13.061 16.097 11.033 16.357L11.033 16.357Q9.265 16.591 7.939 15.603L7.939 15.603Q6.665 14.641 6.457 13.029L6.457 13.029Q6.249 11.209 7.445 9.649L7.445 9.649Q8.693 8.011 10.695 7.647L10.695 7.647Q12.437 7.335 13.737 8.219L13.737 8.219Q14.647 8.817 15.089 9.857L15.089 9.857Q15.193 9.987 14.985 10.039ZM19.119 16.409L18.807 16.435Q17.117 16.409 15.973 15.421L15.973 15.421Q14.933 14.511 14.725 13.185L14.725 13.185Q14.439 11.313 15.661 9.701L15.661 9.701Q16.285 8.869 17.065 8.414Q17.845 7.959 18.911 7.777L18.911 7.777Q20.835 7.439 22.226 8.375Q23.617 9.311 23.825 10.923L23.825 10.923Q24.111 13.211 22.499 14.849L22.499 14.849Q21.381 15.993 19.717 16.331L19.717 16.331Q19.509 16.357 19.119 16.409L19.119 16.409ZM21.537 11.755L21.537 11.755Q21.537 11.729 21.537 11.651L21.537 11.651Q21.537 11.469 21.511 11.391L21.511 11.391Q21.355 10.533 20.666 10.091Q19.977 9.649 19.145 9.857L19.145 9.857Q17.507 10.221 17.143 11.859L17.143 11.859Q16.987 12.535 17.286 13.146Q17.585 13.757 18.209 14.043L18.209 14.043Q19.145 14.459 20.081 13.965L20.081 13.965Q21.459 13.263 21.537 11.755Z&quot;&gt;&lt;/path&gt;&lt;/svg&gt;&lt;/span&gt;main.go&lt;/span&gt;&lt;/span&gt;&lt;/li&gt;&lt;li&gt;&lt;span&gt;&lt;span&gt;&lt;span&gt;&lt;svg width=&quot;16&quot; height=&quot;16&quot; aria-hidden=&quot;true&quot; viewBox=&quot;0 0 24 24&quot;&gt;&lt;path d=&quot;M6.119 10.273L1.933 10.247Q1.855 10.247 1.881 10.195L1.881 10.195L2.141 9.883Q2.167 9.831 2.271 9.831L2.271 9.831L6.379 9.831Q6.457 9.831 6.431 9.883L6.431 9.883L6.223 10.195Q6.171 10.273 6.119 10.273L6.119 10.273ZM5.651 11.313L0.191 11.313Q0.113 11.313 0.139 11.261L0.139 11.261L0.399 10.949Q0.425 10.897 0.529 10.897L0.529 10.897L5.781 10.897Q5.859 10.897 5.833 10.949L5.833 10.949L5.755 11.235Q5.729 11.313 5.651 11.313L5.651 11.313ZM5.495 12.379L2.973 12.379Q2.895 12.379 2.947 12.301L2.947 12.301L3.103 12.015Q3.155 11.963 3.233 11.963L3.233 11.963L5.521 11.963Q5.599 11.963 5.599 12.041L5.599 12.041L5.573 12.301Q5.573 12.379 5.495 12.379L5.495 12.379ZM14.985 10.039L14.985 10.039Q14.569 10.143 13.893 10.325L13.893 10.325L13.009 10.559Q12.905 10.585 12.853 10.572Q12.801 10.559 12.697 10.429L12.697 10.429Q12.411 10.117 12.151 9.987L12.151 9.987Q11.111 9.493 10.071 10.169L10.071 10.169Q8.875 10.949 8.875 12.379L8.875 12.379Q8.901 13.055 9.330 13.575Q9.759 14.095 10.435 14.173L10.435 14.173Q11.605 14.329 12.411 13.419L12.411 13.419L12.723 13.003L10.487 13.003Q10.305 13.003 10.253 12.899Q10.201 12.795 10.279 12.639L10.279 12.639Q10.591 11.911 10.851 11.391L10.851 11.391Q10.955 11.209 11.137 11.209L11.137 11.209L15.349 11.209L15.349 11.521Q15.323 11.937 15.297 12.145L15.297 12.145Q15.089 13.419 14.335 14.407L14.335 14.407Q13.061 16.097 11.033 16.357L11.033 16.357Q9.265 16.591 7.939 15.603L7.939 15.603Q6.665 14.641 6.457 13.029L6.457 13.029Q6.249 11.209 7.445 9.649L7.445 9.649Q8.693 8.011 10.695 7.647L10.695 7.647Q12.437 7.335 13.737 8.219L13.737 8.219Q14.647 8.817 15.089 9.857L15.089 9.857Q15.193 9.987 14.985 10.039ZM19.119 16.409L18.807 16.435Q17.117 16.409 15.973 15.421L15.973 15.421Q14.933 14.511 14.725 13.185L14.725 13.185Q14.439 11.313 15.661 9.701L15.661 9.701Q16.285 8.869 17.065 8.414Q17.845 7.959 18.911 7.777L18.911 7.777Q20.835 7.439 22.226 8.375Q23.617 9.311 23.825 10.923L23.825 10.923Q24.111 13.211 22.499 14.849L22.499 14.849Q21.381 15.993 19.717 16.331L19.717 16.331Q19.509 16.357 19.119 16.409L19.119 16.409ZM21.537 11.755L21.537 11.755Q21.537 11.729 21.537 11.651L21.537 11.651Q21.537 11.469 21.511 11.391L21.511 11.391Q21.355 10.533 20.666 10.091Q19.977 9.649 19.145 9.857L19.145 9.857Q17.507 10.221 17.143 11.859L17.143 11.859Q16.987 12.535 17.286 13.146Q17.585 13.757 18.209 14.043L18.209 14.043Q19.145 14.459 20.081 13.965L20.081 13.965Q21.459 13.263 21.537 11.755Z&quot;&gt;&lt;/path&gt;&lt;/svg&gt;&lt;/span&gt;calculator_test.go&lt;/span&gt;&lt;/span&gt;&lt;/li&gt;&lt;li&gt;&lt;span&gt;&lt;span&gt;&lt;span&gt;&lt;svg width=&quot;16&quot; height=&quot;16&quot; aria-hidden=&quot;true&quot; viewBox=&quot;0 0 24 24&quot;&gt;&lt;path d=&quot;M1.082 16.876L1.082 14.014L22.918 14.014L22.918 16.876L1.082 16.876ZM1.082 9.986L1.082 7.071L13.272 7.071L13.272 9.986L1.082 9.986ZM1.082 3.096L1.082 0.181L22.918 0.181L22.918 3.096L1.082 3.096ZM1.082 23.819L1.082 20.904L17.300 20.904L17.300 23.819L1.082 23.819Z&quot;&gt;&lt;/path&gt;&lt;/svg&gt;&lt;/span&gt;go.mod&lt;/span&gt;&lt;/span&gt;&lt;/li&gt;&lt;/ul&gt;&lt;/details&gt;&lt;/li&gt;&lt;li&gt;&lt;details open&gt;&lt;summary&gt;&lt;span&gt;&lt;span&gt;&lt;span&gt;&lt;svg width=&quot;16&quot; height=&quot;16&quot; aria-hidden=&quot;true&quot; viewBox=&quot;0 0 24 24&quot;&gt;&lt;path d=&quot;M22.073 4.900L22.073 4.900L12.148 4.900L12.148 3.950Q12.148 3.125 11.585 2.563Q11.023 2 10.198 2L10.198 2L0.048 2L0.048 22L23.948 22L23.948 6.850Q23.998 6.025 23.448 5.462Q22.898 4.900 22.073 4.900Z&quot;&gt;&lt;/path&gt;&lt;/svg&gt;&lt;/span&gt;calculator-frontend/
&lt;/span&gt;&lt;/span&gt;&lt;/summary&gt;&lt;ul&gt;&lt;li&gt;&lt;details open&gt;&lt;summary&gt;&lt;span&gt;&lt;span&gt;&lt;span&gt;&lt;svg width=&quot;16&quot; height=&quot;16&quot; aria-hidden=&quot;true&quot; viewBox=&quot;0 0 24 24&quot;&gt;&lt;path d=&quot;M22.073 4.900L22.073 4.900L12.148 4.900L12.148 3.950Q12.148 3.125 11.585 2.563Q11.023 2 10.198 2L10.198 2L0.048 2L0.048 22L23.948 22L23.948 6.850Q23.998 6.025 23.448 5.462Q22.898 4.900 22.073 4.900Z&quot;&gt;&lt;/path&gt;&lt;/svg&gt;&lt;/span&gt;app/
&lt;/span&gt;&lt;/span&gt;&lt;/summary&gt;&lt;ul&gt;&lt;li&gt;&lt;details open&gt;&lt;summary&gt;&lt;span&gt;&lt;span&gt;&lt;span&gt;&lt;svg width=&quot;16&quot; height=&quot;16&quot; aria-hidden=&quot;true&quot; viewBox=&quot;0 0 24 24&quot;&gt;&lt;path d=&quot;M22.073 4.900L22.073 4.900L12.148 4.900L12.148 3.950Q12.148 3.125 11.585 2.563Q11.023 2 10.198 2L10.198 2L0.048 2L0.048 22L23.948 22L23.948 6.850Q23.998 6.025 23.448 5.462Q22.898 4.900 22.073 4.900Z&quot;&gt;&lt;/path&gt;&lt;/svg&gt;&lt;/span&gt;generated/
&lt;/span&gt;&lt;/span&gt;&lt;/summary&gt;&lt;ul&gt;&lt;li&gt;&lt;details&gt;&lt;summary&gt;&lt;span&gt;&lt;span&gt;&lt;span&gt;&lt;svg width=&quot;16&quot; height=&quot;16&quot; aria-hidden=&quot;true&quot; viewBox=&quot;0 0 24 24&quot;&gt;&lt;path d=&quot;M22.073 4.900L22.073 4.900L12.148 4.900L12.148 3.950Q12.148 3.125 11.585 2.563Q11.023 2 10.198 2L10.198 2L0.048 2L0.048 22L23.948 22L23.948 6.850Q23.998 6.025 23.448 5.462Q22.898 4.900 22.073 4.900Z&quot;&gt;&lt;/path&gt;&lt;/svg&gt;&lt;/span&gt;calculator/&lt;/span&gt;&lt;/span&gt;&lt;/summary&gt;&lt;ul&gt;&lt;li&gt;&lt;span&gt;&lt;span&gt;…&lt;/span&gt;&lt;/span&gt;&lt;/li&gt;&lt;/ul&gt;&lt;/details&gt;&lt;/li&gt;&lt;/ul&gt;&lt;/details&gt;&lt;/li&gt;&lt;li&gt;&lt;span&gt;&lt;span&gt;&lt;span&gt;&lt;svg width=&quot;16&quot; height=&quot;16&quot; aria-hidden=&quot;true&quot; viewBox=&quot;0 0 24 24&quot;&gt;&lt;path d=&quot;M18.955 15.993L18.993 15.993Q18.993 16.107 18.993 16.297L18.993 16.297Q19.221 18.615 19.221 19.774Q19.221 20.933 18.651 21.693Q18.081 22.453 17.207 22.529L17.207 22.529Q16.333 22.719 15.307 22.301L15.307 22.301Q14.547 21.921 13.179 21.009L13.179 21.009Q12.305 20.439 11.849 20.173L11.849 20.173Q10.671 21.123 9.949 21.579L9.949 21.579Q9.341 21.997 8.771 22.187L8.771 22.187Q7.707 22.643 6.795 22.472Q5.883 22.301 5.332 21.579Q4.781 20.857 4.762 19.679Q4.743 18.501 4.971 15.879L4.971 15.879Q3.527 15.423 2.805 14.929L2.805 14.929Q1.855 14.473 0.905 13.523L0.905 13.523Q0.145 12.725 0.183 11.794Q0.221 10.863 1.057 10.065L1.057 10.065Q1.665 9.343 2.691 8.849L2.691 8.849Q3.299 8.507 4.591 8.051L4.591 8.051Q4.667 8.051 4.838 7.994Q5.009 7.937 5.085 7.937L5.085 7.937L4.743 5.315Q4.705 4.479 4.971 3.073L4.971 3.073Q5.237 2.085 6.054 1.667Q6.871 1.249 7.935 1.515L7.935 1.515Q8.885 1.743 9.835 2.351L9.835 2.351Q10.519 2.769 11.393 3.529L11.393 3.529Q11.545 3.605 11.735 3.871L11.735 3.871L11.849 4.023Q13.749 2.579 14.813 1.971L14.813 1.971Q16.029 1.173 17.207 1.515L17.207 1.515Q18.081 1.705 18.594 2.446Q19.107 3.187 19.221 4.365L19.221 4.365L19.221 6.265Q19.221 6.645 19.031 7.405L19.031 7.405Q18.917 7.899 18.841 8.165L18.841 8.165Q19.905 8.507 20.874 8.982Q21.843 9.457 22.261 9.761L22.261 9.761Q22.983 10.255 23.401 10.882Q23.819 11.509 23.819 12.193Q23.819 12.877 23.401 13.504Q22.983 14.131 22.261 14.625L22.261 14.625Q21.805 14.929 20.855 15.423L20.855 15.423Q20.171 15.537 18.955 15.993L18.955 15.993ZM10.861 15.841L12.077 15.879Q12.267 15.879 12.685 15.841L12.685 15.841Q13.255 15.765 13.521 15.765L13.521 15.765Q14.015 15.765 14.357 15.271L14.357 15.271Q15.535 13.371 15.991 12.307L15.991 12.307Q16.105 12.155 16.105 11.927Q16.105 11.699 15.991 11.623L15.991 11.623Q15.421 10.521 14.205 8.659L14.205 8.659Q14.053 8.279 13.635 8.279L13.635 8.279Q11.621 8.279 10.557 8.165L10.557 8.165Q9.607 8.165 9.113 9.001L9.113 9.001Q8.657 9.723 8.391 10.179L8.391 10.179L7.859 11.167Q7.517 11.661 7.460 11.870Q7.403 12.079 7.460 12.288Q7.517 12.497 7.859 13.029L7.859 13.029L8.391 13.979L8.847 14.739Q9.227 15.347 9.379 15.499L9.379 15.499Q9.607 15.765 9.911 15.803L9.911 15.803Q10.139 15.841 10.861 15.841L10.861 15.841ZM5.199 14.929L5.199 14.929Q5.921 13.029 6.263 12.193L6.263 12.193L6.263 11.737L5.199 9.001Q3.071 9.647 1.893 10.521L1.893 10.521Q1.057 11.167 1.057 11.908Q1.057 12.649 1.893 13.257L1.893 13.257Q2.501 13.903 3.451 14.321L3.451 14.321Q4.021 14.587 5.199 14.929ZM18.613 8.887L18.613 8.887Q18.157 9.951 17.435 11.851L17.435 11.851L17.397 11.965Q17.359 12.079 17.435 12.079L17.435 12.079Q18.157 13.979 18.613 15.157L18.613 15.157Q19.069 14.929 20.019 14.435L20.019 14.435Q21.273 13.865 21.805 13.523L21.805 13.523Q22.793 12.877 22.793 12.079Q22.793 11.281 21.843 10.673L21.843 10.673Q21.197 10.103 19.753 9.419L19.753 9.419Q18.955 9.077 18.613 8.887ZM5.883 7.481L5.921 7.671Q7.821 7.481 8.885 7.215L8.885 7.215Q8.961 7.215 9.037 7.139L9.037 7.139L9.113 7.101Q9.797 6.151 11.013 4.707L11.013 4.707Q10.063 3.909 9.569 3.529L9.569 3.529Q8.733 2.883 7.935 2.579L7.935 2.579Q6.947 2.237 6.339 2.598Q5.731 2.959 5.541 4.023L5.541 4.023Q5.465 4.745 5.579 5.695L5.579 5.695Q5.655 6.265 5.883 7.481L5.883 7.481ZM17.777 7.709L17.777 7.671Q17.777 7.633 17.834 7.462Q17.891 7.291 17.891 7.215L17.891 7.215Q18.157 6.037 18.157 5.391L18.157 5.391Q18.233 4.327 17.891 3.529L17.891 3.529Q17.739 2.921 17.321 2.655Q16.903 2.389 16.371 2.465L16.371 2.465Q15.421 2.617 14.509 3.225L14.509 3.225Q13.939 3.567 12.951 4.479L12.951 4.479L12.685 4.707Q13.293 5.543 14.471 6.987L14.471 6.987L14.813 7.329L17.777 7.709ZM5.921 16.221L5.921 16.221Q5.883 16.449 5.807 16.867L5.807 16.867Q5.579 17.931 5.541 18.425L5.541 18.425Q5.465 19.299 5.693 20.059L5.693 20.059Q5.845 20.933 6.434 21.275Q7.023 21.617 7.935 21.351L7.935 21.351Q8.695 21.123 9.493 20.591L9.493 20.591Q9.949 20.249 10.823 19.489L10.823 19.489L11.127 19.223Q10.443 18.273 9.227 16.829L9.227 16.829Q8.999 16.601 8.885 16.601L8.885 16.601Q7.517 16.601 5.921 16.221ZM12.571 19.223L12.571 19.223Q13.065 19.869 14.015 20.553L14.015 20.553Q14.889 21.199 15.649 21.465L15.649 21.465Q17.549 22.035 17.891 20.173L17.891 20.173Q18.043 19.337 17.967 18.349L17.967 18.349Q17.929 17.741 17.701 16.525L17.701 16.525L17.663 16.373L16.903 16.449Q15.307 16.563 14.585 16.829L14.585 16.829Q14.243 17.019 13.939 17.361L13.939 17.361Q13.749 17.589 13.445 18.045L13.445 18.045Q13.179 18.425 13.046 18.634Q12.913 18.843 12.571 19.223ZM10.443 7.101L13.293 7.101Q13.027 6.797 12.552 6.265Q12.077 5.733 11.849 5.429L11.849 5.429L11.659 5.695Q10.937 6.607 10.443 7.101L10.443 7.101ZM12.837 17.361L13.293 16.829L10.557 16.829Q10.785 17.133 11.260 17.665Q11.735 18.197 11.963 18.501L11.963 18.501Q12.229 18.083 12.837 17.361L12.837 17.361ZM8.391 15.651L8.391 15.651Q8.239 15.385 7.935 14.853L7.935 14.853Q7.289 13.789 6.985 13.143L6.985 13.143L6.263 15.157Q6.909 15.423 8.391 15.651ZM16.713 13.143L16.713 13.143L15.307 15.651L17.435 15.271Q17.283 14.511 16.713 13.143ZM6.985 10.787L6.985 10.787L8.391 8.279L6.263 8.621L6.415 9.115Q6.681 10.217 6.985 10.787ZM15.307 8.279L15.307 8.279Q15.535 8.773 16.010 9.476Q16.485 10.179 16.713 10.673L16.713 10.673Q17.207 9.229 17.435 8.659L17.435 8.659Q16.523 8.621 15.307 8.279Z&quot;&gt;&lt;/path&gt;&lt;/svg&gt;&lt;/span&gt;page.tsx&lt;/span&gt;&lt;/span&gt;&lt;/li&gt;&lt;/ul&gt;&lt;/details&gt;&lt;/li&gt;&lt;li&gt;&lt;details open&gt;&lt;summary&gt;&lt;span&gt;&lt;span&gt;&lt;span&gt;&lt;svg width=&quot;16&quot; height=&quot;16&quot; aria-hidden=&quot;true&quot; viewBox=&quot;0 0 24 24&quot;&gt;&lt;path d=&quot;M22.073 4.900L22.073 4.900L12.148 4.900L12.148 3.950Q12.148 3.125 11.585 2.563Q11.023 2 10.198 2L10.198 2L0.048 2L0.048 22L23.948 22L23.948 6.850Q23.998 6.025 23.448 5.462Q22.898 4.900 22.073 4.900Z&quot;&gt;&lt;/path&gt;&lt;/svg&gt;&lt;/span&gt;calculator/
&lt;/span&gt;&lt;/span&gt;&lt;/summary&gt;&lt;ul&gt;&lt;li&gt;&lt;span&gt;&lt;span&gt;&lt;span&gt;&lt;svg width=&quot;16&quot; height=&quot;16&quot; aria-hidden=&quot;true&quot; viewBox=&quot;0 0 24 24&quot;&gt;&lt;path d=&quot;M1.082 16.876L1.082 14.014L22.918 14.014L22.918 16.876L1.082 16.876ZM1.082 9.986L1.082 7.071L13.272 7.071L13.272 9.986L1.082 9.986ZM1.082 3.096L1.082 0.181L22.918 0.181L22.918 3.096L1.082 3.096ZM1.082 23.819L1.082 20.904L17.300 20.904L17.300 23.819L1.082 23.819Z&quot;&gt;&lt;/path&gt;&lt;/svg&gt;&lt;/span&gt;calculator.proto&lt;/span&gt;&lt;/span&gt;&lt;/li&gt;&lt;/ul&gt;&lt;/details&gt;&lt;/li&gt;&lt;li&gt;&lt;span&gt;&lt;span&gt;&lt;span&gt;&lt;svg width=&quot;16&quot; height=&quot;16&quot; aria-hidden=&quot;true&quot; viewBox=&quot;0 0 24 24&quot;&gt;&lt;path d=&quot;M0.734 13.269L0.562 10.732Q1.938 10.732 2.497 10.087L2.497 10.087Q2.884 9.614 2.884 8.711L2.884 8.711Q2.884 8.324 2.798 7.571Q2.712 6.819 2.712 6.410Q2.712 6.002 2.669 5.185L2.669 5.185Q2.583 4.454 2.583 4.153L2.583 4.153Q2.583 2.089 3.787 1.099Q4.991 0.111 7.184 0.111L7.184 0.111L8.259 0.111L8.259 2.648L7.700 2.648Q6.754 2.648 6.345 3.185Q5.937 3.723 5.937 4.798L5.937 4.798Q5.937 5.056 6.023 5.572L6.023 5.572Q6.109 6.217 6.109 6.561L6.109 6.561Q6.109 6.819 6.152 7.378L6.152 7.378Q6.238 8.152 6.238 8.582L6.238 8.582Q6.238 10.216 5.550 11.033L5.550 11.033Q4.948 11.764 3.658 12.065L3.658 12.065Q4.948 12.409 5.550 13.097L5.550 13.097Q6.238 13.957 6.238 15.548L6.238 15.548Q6.238 16.021 6.152 16.795L6.152 16.795Q6.066 17.354 6.088 17.612Q6.109 17.870 6.023 18.515L6.023 18.515Q5.937 18.988 5.937 19.203L5.937 19.203Q5.937 20.278 6.345 20.815Q6.754 21.353 7.700 21.353L7.700 21.353L8.259 21.353L8.259 23.890L7.184 23.890Q2.712 23.890 2.712 19.848L2.712 19.848Q2.712 18.386 2.862 17.590Q3.013 16.795 3.013 15.290L3.013 15.290Q3.013 13.269 0.734 13.269L0.734 13.269ZM23.438 10.732L23.438 13.011Q21.159 13.011 21.159 15.032L21.159 15.032Q21.159 15.419 21.224 16.171Q21.288 16.924 21.288 17.311L21.288 17.311Q21.417 18.128 21.417 19.590L21.417 19.590Q21.417 23.632 16.859 23.632L16.859 23.632L15.784 23.632L15.784 21.353L16.300 21.353Q17.246 21.353 17.654 20.815Q18.063 20.278 18.063 19.203Q18.063 18.128 17.934 17.569L17.934 17.569Q17.934 17.225 17.848 16.558Q17.762 15.892 17.762 15.548L17.762 15.548Q17.762 13.957 18.450 13.097L18.450 13.097Q19.052 12.409 20.342 12.065L20.342 12.065Q19.052 11.764 18.450 11.033L18.450 11.033Q17.762 10.216 17.762 8.582L17.762 8.582Q17.762 8.152 17.848 7.378L17.848 7.378Q17.934 6.819 17.934 6.561L17.934 6.561Q18.063 5.873 18.063 4.841Q18.063 3.809 17.633 3.293Q17.203 2.777 16.300 2.648L16.300 2.648L15.784 2.648L15.784 0.111L16.859 0.111Q19.009 0.111 20.213 1.099Q21.417 2.089 21.417 4.153L21.417 4.153Q21.417 4.540 21.352 5.292Q21.288 6.045 21.288 6.432L21.288 6.432Q21.159 7.249 21.116 8.711L21.116 8.711Q21.159 9.614 21.503 10.087L21.503 10.087Q22.062 10.732 23.438 10.732L23.438 10.732Z&quot;&gt;&lt;/path&gt;&lt;/svg&gt;&lt;/span&gt;package.json&lt;/span&gt;&lt;/span&gt;&lt;/li&gt;&lt;/ul&gt;&lt;/details&gt;&lt;/li&gt;&lt;/ul&gt;&lt;/starlight-file-tree&gt;
&lt;p&gt;后端负责定义和实现计算服务，前端负责生成 gRPC-Web 客户端代码并发起调用。&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;/tr&gt;&lt;/thead&gt;&lt;tbody&gt;&lt;tr&gt;&lt;td&gt;&lt;code dir=&quot;auto&quot;&gt;calculator.proto&lt;/code&gt;&lt;/td&gt;&lt;td&gt;定义服务、请求结构、响应结构&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;&lt;code dir=&quot;auto&quot;&gt;main.go&lt;/code&gt;&lt;/td&gt;&lt;td&gt;实现 Go gRPC 服务，并包装成 gRPC-Web HTTP 服务&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;&lt;code dir=&quot;auto&quot;&gt;page.tsx&lt;/code&gt;&lt;/td&gt;&lt;td&gt;在 Next.js 页面中创建请求并调用后端&lt;/td&gt;&lt;/tr&gt;&lt;/tbody&gt;&lt;/table&gt;
&lt;div&gt;&lt;h2 id=&quot;先定义接口契约&quot;&gt;先定义接口契约&lt;/h2&gt;&lt;/div&gt;
&lt;p&gt;gRPC 的入口通常不是先写控制器，而是先写 &lt;code dir=&quot;auto&quot;&gt;.proto&lt;/code&gt;。&lt;/p&gt;
&lt;div&gt;&lt;figure&gt;&lt;figcaption&gt;&lt;span&gt;calculator.proto&lt;/span&gt;&lt;/figcaption&gt;&lt;pre&gt;&lt;code&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;syntax&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;=&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;&quot;proto3&quot;&lt;/span&gt;&lt;span&gt;;&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;
&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;package&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;calculator&lt;/span&gt;&lt;span&gt;;&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;
&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;option&lt;/span&gt;&lt;span&gt; go_package &lt;/span&gt;&lt;span&gt;=&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;&quot;github.com/2760439882/calculator-backend/calculator;calculator&quot;&lt;/span&gt;&lt;span&gt;;&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;
&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;service&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;Calculator&lt;/span&gt;&lt;span&gt; {&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;  &lt;/span&gt;&lt;span&gt;rpc&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;Calculate&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;CalculationRequest&lt;/span&gt;&lt;span&gt;) &lt;/span&gt;&lt;span&gt;returns&lt;/span&gt;&lt;span&gt; (&lt;/span&gt;&lt;span&gt;CalculationResponse&lt;/span&gt;&lt;span&gt;);&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;}&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;
&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;message&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;CalculationRequest&lt;/span&gt;&lt;span&gt; {&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;  &lt;/span&gt;&lt;span&gt;double&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;operand1&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;=&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;1&lt;/span&gt;&lt;span&gt;;&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;  &lt;/span&gt;&lt;span&gt;double&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;operand2&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;=&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;2&lt;/span&gt;&lt;span&gt;;&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;  &lt;/span&gt;&lt;span&gt;string&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;operator&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;=&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;3&lt;/span&gt;&lt;span&gt;; &lt;/span&gt;&lt;span&gt;// &quot;+&quot;, &quot;-&quot;, &quot;*&quot;, &quot;/&quot;&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;}&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;
&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;message&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;CalculationResponse&lt;/span&gt;&lt;span&gt; {&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;  &lt;/span&gt;&lt;span&gt;double&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;result&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;=&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;1&lt;/span&gt;&lt;span&gt;;&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;}&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/figure&gt;&lt;/div&gt;
&lt;p&gt;这份协议里有三个关键信息：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code dir=&quot;auto&quot;&gt;Calculator&lt;/code&gt; 是服务名。&lt;/li&gt;
&lt;li&gt;&lt;code dir=&quot;auto&quot;&gt;Calculate&lt;/code&gt; 是远程调用方法。&lt;/li&gt;
&lt;li&gt;&lt;code dir=&quot;auto&quot;&gt;CalculationRequest&lt;/code&gt; 和 &lt;code dir=&quot;auto&quot;&gt;CalculationResponse&lt;/code&gt; 分别是请求和响应结构。&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;这就是 gRPC 和普通 REST 接口很不一样的地方。REST 接口经常先约定 URL 和 JSON 字段，而 gRPC 会先约定服务方法和强类型消息结构。&lt;/p&gt;
&lt;div&gt;&lt;h2 id=&quot;后端实现计算服务&quot;&gt;后端实现计算服务&lt;/h2&gt;&lt;/div&gt;
&lt;p&gt;后端使用 Go 实现 &lt;code dir=&quot;auto&quot;&gt;Calculator&lt;/code&gt; 服务。&lt;/p&gt;
&lt;div&gt;&lt;figure&gt;&lt;figcaption&gt;&lt;span&gt;main.go&lt;/span&gt;&lt;/figcaption&gt;&lt;pre&gt;&lt;code&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;type&lt;/span&gt;&lt;span&gt; server &lt;/span&gt;&lt;span&gt;struct&lt;/span&gt;&lt;span&gt; {&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;&lt;span&gt;    &lt;/span&gt;&lt;/span&gt;&lt;span&gt;pb.UnimplementedCalculatorServer&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;}&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;
&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;func&lt;/span&gt;&lt;span&gt;&lt;span&gt; (&lt;/span&gt;&lt;span&gt;s &lt;/span&gt;&lt;/span&gt;&lt;span&gt;*&lt;/span&gt;&lt;span&gt;server) &lt;/span&gt;&lt;span&gt;Calculate&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;&lt;span&gt;    &lt;/span&gt;&lt;/span&gt;&lt;span&gt;ctx&lt;/span&gt;&lt;span&gt; context.Context,&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;&lt;span&gt;    &lt;/span&gt;&lt;/span&gt;&lt;span&gt;req&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;*&lt;/span&gt;&lt;span&gt;pb.CalculationRequest,&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;) (&lt;/span&gt;&lt;span&gt;*&lt;/span&gt;&lt;span&gt;pb.CalculationResponse, &lt;/span&gt;&lt;span&gt;error&lt;/span&gt;&lt;span&gt;) {&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;    &lt;/span&gt;&lt;span&gt;var&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;result&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;float64&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;
&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;    &lt;/span&gt;&lt;span&gt;switch&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;req&lt;/span&gt;&lt;span&gt;.&lt;/span&gt;&lt;span&gt;Operator&lt;/span&gt;&lt;span&gt; {&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;    &lt;/span&gt;&lt;span&gt;case&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;&quot;&lt;/span&gt;&lt;span&gt;+&lt;/span&gt;&lt;span&gt;&quot;&lt;/span&gt;&lt;span&gt;:&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;        &lt;/span&gt;&lt;span&gt;result&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;=&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;req&lt;/span&gt;&lt;span&gt;.&lt;/span&gt;&lt;span&gt;Operand1&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;+&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;req&lt;/span&gt;&lt;span&gt;.&lt;/span&gt;&lt;span&gt;Operand2&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;    &lt;/span&gt;&lt;span&gt;case&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;&quot;&lt;/span&gt;&lt;span&gt;-&lt;/span&gt;&lt;span&gt;&quot;&lt;/span&gt;&lt;span&gt;:&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;        &lt;/span&gt;&lt;span&gt;result&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;=&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;req&lt;/span&gt;&lt;span&gt;.&lt;/span&gt;&lt;span&gt;Operand1&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;-&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;req&lt;/span&gt;&lt;span&gt;.&lt;/span&gt;&lt;span&gt;Operand2&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;    &lt;/span&gt;&lt;span&gt;case&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;&quot;&lt;/span&gt;&lt;span&gt;*&lt;/span&gt;&lt;span&gt;&quot;&lt;/span&gt;&lt;span&gt;:&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;        &lt;/span&gt;&lt;span&gt;result&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;=&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;req&lt;/span&gt;&lt;span&gt;.&lt;/span&gt;&lt;span&gt;Operand1&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;*&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;req&lt;/span&gt;&lt;span&gt;.&lt;/span&gt;&lt;span&gt;Operand2&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;    &lt;/span&gt;&lt;span&gt;case&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;&quot;&lt;/span&gt;&lt;span&gt;/&lt;/span&gt;&lt;span&gt;&quot;&lt;/span&gt;&lt;span&gt;:&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;        &lt;/span&gt;&lt;span&gt;if&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;req&lt;/span&gt;&lt;span&gt;.&lt;/span&gt;&lt;span&gt;Operand2&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;==&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;0&lt;/span&gt;&lt;span&gt; {&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;            &lt;/span&gt;&lt;span&gt;return&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;nil&lt;/span&gt;&lt;span&gt;, &lt;/span&gt;&lt;span&gt;fmt&lt;/span&gt;&lt;span&gt;.&lt;/span&gt;&lt;span&gt;Errorf&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;&quot;&lt;/span&gt;&lt;span&gt;division by zero&lt;/span&gt;&lt;span&gt;&quot;&lt;/span&gt;&lt;span&gt;)&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;&lt;span&gt;        &lt;/span&gt;&lt;/span&gt;&lt;span&gt;}&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;        &lt;/span&gt;&lt;span&gt;result&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;=&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;req&lt;/span&gt;&lt;span&gt;.&lt;/span&gt;&lt;span&gt;Operand1&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;/&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;req&lt;/span&gt;&lt;span&gt;.&lt;/span&gt;&lt;span&gt;Operand2&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;    &lt;/span&gt;&lt;span&gt;default&lt;/span&gt;&lt;span&gt;:&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;        &lt;/span&gt;&lt;span&gt;return&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;nil&lt;/span&gt;&lt;span&gt;, &lt;/span&gt;&lt;span&gt;fmt&lt;/span&gt;&lt;span&gt;.&lt;/span&gt;&lt;span&gt;Errorf&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;&quot;&lt;/span&gt;&lt;span&gt;unknown operator&lt;/span&gt;&lt;span&gt;&quot;&lt;/span&gt;&lt;span&gt;)&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;&lt;span&gt;    &lt;/span&gt;&lt;/span&gt;&lt;span&gt;}&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;
&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;    &lt;/span&gt;&lt;span&gt;return&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;&amp;#x26;&lt;/span&gt;&lt;span&gt;pb.CalculationResponse{&lt;/span&gt;&lt;span&gt;Result&lt;/span&gt;&lt;span&gt;: &lt;/span&gt;&lt;span&gt;result&lt;/span&gt;&lt;span&gt;}, &lt;/span&gt;&lt;span&gt;nil&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;}&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/figure&gt;&lt;/div&gt;
&lt;p&gt;这段实现很适合面试时讲，因为它虽然简单，但覆盖了服务端接口实现的几个基本点：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;方法签名来自 &lt;code dir=&quot;auto&quot;&gt;.proto&lt;/code&gt; 生成代码。&lt;/li&gt;
&lt;li&gt;请求参数来自 &lt;code dir=&quot;auto&quot;&gt;CalculationRequest&lt;/code&gt;。&lt;/li&gt;
&lt;li&gt;返回值必须符合 &lt;code dir=&quot;auto&quot;&gt;CalculationResponse&lt;/code&gt;。&lt;/li&gt;
&lt;li&gt;错误可以通过 &lt;code dir=&quot;auto&quot;&gt;error&lt;/code&gt; 返回给调用方。&lt;/li&gt;
&lt;li&gt;除法需要额外处理除数为 &lt;code dir=&quot;auto&quot;&gt;0&lt;/code&gt; 的情况。&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;真正的业务逻辑只有一个 &lt;code dir=&quot;auto&quot;&gt;switch&lt;/code&gt;，但重点是：这个 &lt;code dir=&quot;auto&quot;&gt;switch&lt;/code&gt; 被放在了 gRPC 服务方法里，前端不会直接知道后端怎么计算，只知道自己要调用 &lt;code dir=&quot;auto&quot;&gt;Calculate&lt;/code&gt;。&lt;/p&gt;
&lt;div&gt;&lt;h2 id=&quot;为什么要用-grpc-web&quot;&gt;为什么要用 gRPC-Web&lt;/h2&gt;&lt;/div&gt;
&lt;p&gt;浏览器不能像 Go、Java、Node 服务端那样直接发起原生 gRPC 请求。原生 gRPC 基于 HTTP/2，而浏览器端直接使用 gRPC 会受到限制。&lt;/p&gt;
&lt;p&gt;所以前端调用 gRPC 服务时，通常需要一层 gRPC-Web。&lt;/p&gt;
&lt;p&gt;这个项目里，Go 后端把原始 gRPC 服务包装成了 gRPC-Web 服务：&lt;/p&gt;
&lt;div&gt;&lt;figure&gt;&lt;figcaption&gt;&lt;span&gt;main.go&lt;/span&gt;&lt;/figcaption&gt;&lt;pre&gt;&lt;code&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;grpcServer&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;:=&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;grpc&lt;/span&gt;&lt;span&gt;.&lt;/span&gt;&lt;span&gt;NewServer&lt;/span&gt;&lt;span&gt;()&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;pb&lt;/span&gt;&lt;span&gt;.&lt;/span&gt;&lt;span&gt;RegisterCalculatorServer&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;grpcServer&lt;/span&gt;&lt;span&gt;, &lt;/span&gt;&lt;span&gt;&amp;#x26;&lt;/span&gt;&lt;span&gt;server{})&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;
&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;wrappedGrpc&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;:=&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;grpcweb&lt;/span&gt;&lt;span&gt;.&lt;/span&gt;&lt;span&gt;WrapServer&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;grpcServer&lt;/span&gt;&lt;span&gt;)&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;
&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;httpServer&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;:=&lt;/span&gt;&lt;span&gt; http.Server{&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;    &lt;/span&gt;&lt;span&gt;Addr&lt;/span&gt;&lt;span&gt;: &lt;/span&gt;&lt;span&gt;&quot;&lt;/span&gt;&lt;span&gt;:8080&lt;/span&gt;&lt;span&gt;&quot;&lt;/span&gt;&lt;span&gt;,&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;    &lt;/span&gt;&lt;span&gt;Handler&lt;/span&gt;&lt;span&gt;: &lt;/span&gt;&lt;span&gt;cors&lt;/span&gt;&lt;span&gt;.&lt;/span&gt;&lt;span&gt;New&lt;/span&gt;&lt;span&gt;(cors.Options{&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;        &lt;/span&gt;&lt;span&gt;AllowedOrigins&lt;/span&gt;&lt;span&gt;: []&lt;/span&gt;&lt;span&gt;string&lt;/span&gt;&lt;span&gt;{&lt;/span&gt;&lt;span&gt;&quot;&lt;/span&gt;&lt;span&gt;http://localhost:3000&lt;/span&gt;&lt;span&gt;&quot;&lt;/span&gt;&lt;span&gt;},&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;        &lt;/span&gt;&lt;span&gt;AllowedMethods&lt;/span&gt;&lt;span&gt;: []&lt;/span&gt;&lt;span&gt;string&lt;/span&gt;&lt;span&gt;{&lt;/span&gt;&lt;span&gt;&quot;&lt;/span&gt;&lt;span&gt;GET&lt;/span&gt;&lt;span&gt;&quot;&lt;/span&gt;&lt;span&gt;, &lt;/span&gt;&lt;span&gt;&quot;&lt;/span&gt;&lt;span&gt;POST&lt;/span&gt;&lt;span&gt;&quot;&lt;/span&gt;&lt;span&gt;, &lt;/span&gt;&lt;span&gt;&quot;&lt;/span&gt;&lt;span&gt;OPTIONS&lt;/span&gt;&lt;span&gt;&quot;&lt;/span&gt;&lt;span&gt;},&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;        &lt;/span&gt;&lt;span&gt;AllowedHeaders&lt;/span&gt;&lt;span&gt;: []&lt;/span&gt;&lt;span&gt;string&lt;/span&gt;&lt;span&gt;{&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;            &lt;/span&gt;&lt;span&gt;&quot;&lt;/span&gt;&lt;span&gt;Content-Type&lt;/span&gt;&lt;span&gt;&quot;&lt;/span&gt;&lt;span&gt;,&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;            &lt;/span&gt;&lt;span&gt;&quot;&lt;/span&gt;&lt;span&gt;X-Grpc-Web&lt;/span&gt;&lt;span&gt;&quot;&lt;/span&gt;&lt;span&gt;,&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;            &lt;/span&gt;&lt;span&gt;&quot;&lt;/span&gt;&lt;span&gt;X-User-Agent&lt;/span&gt;&lt;span&gt;&quot;&lt;/span&gt;&lt;span&gt;,&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;            &lt;/span&gt;&lt;span&gt;&quot;&lt;/span&gt;&lt;span&gt;grpc-timeout&lt;/span&gt;&lt;span&gt;&quot;&lt;/span&gt;&lt;span&gt;,&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;&lt;span&gt;        &lt;/span&gt;&lt;/span&gt;&lt;span&gt;},&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;        &lt;/span&gt;&lt;span&gt;AllowCredentials&lt;/span&gt;&lt;span&gt;: &lt;/span&gt;&lt;span&gt;true&lt;/span&gt;&lt;span&gt;,&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;&lt;span&gt;    &lt;/span&gt;&lt;/span&gt;&lt;span&gt;}).&lt;/span&gt;&lt;span&gt;Handler&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;http&lt;/span&gt;&lt;span&gt;.&lt;/span&gt;&lt;span&gt;HandlerFunc&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;func&lt;/span&gt;&lt;span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;w&lt;/span&gt;&lt;span&gt; http.ResponseWriter, &lt;/span&gt;&lt;span&gt;r&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;/span&gt;&lt;span&gt;*&lt;/span&gt;&lt;span&gt;http.Request) {&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;        &lt;/span&gt;&lt;span&gt;if&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;wrappedGrpc&lt;/span&gt;&lt;span&gt;.&lt;/span&gt;&lt;span&gt;IsGrpcWebRequest&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;r&lt;/span&gt;&lt;span&gt;) &lt;/span&gt;&lt;span&gt;||&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;            &lt;/span&gt;&lt;span&gt;wrappedGrpc&lt;/span&gt;&lt;span&gt;.&lt;/span&gt;&lt;span&gt;IsAcceptableGrpcCorsRequest&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;r&lt;/span&gt;&lt;span&gt;) &lt;/span&gt;&lt;span&gt;||&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;            &lt;/span&gt;&lt;span&gt;wrappedGrpc&lt;/span&gt;&lt;span&gt;.&lt;/span&gt;&lt;span&gt;IsGrpcWebSocketRequest&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;r&lt;/span&gt;&lt;span&gt;) {&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;            &lt;/span&gt;&lt;span&gt;wrappedGrpc&lt;/span&gt;&lt;span&gt;.&lt;/span&gt;&lt;span&gt;ServeHTTP&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;w&lt;/span&gt;&lt;span&gt;, &lt;/span&gt;&lt;span&gt;r&lt;/span&gt;&lt;span&gt;)&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;            &lt;/span&gt;&lt;span&gt;return&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;&lt;span&gt;        &lt;/span&gt;&lt;/span&gt;&lt;span&gt;}&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;
&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;        &lt;/span&gt;&lt;span&gt;http&lt;/span&gt;&lt;span&gt;.&lt;/span&gt;&lt;span&gt;NotFound&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;w&lt;/span&gt;&lt;span&gt;, &lt;/span&gt;&lt;span&gt;r&lt;/span&gt;&lt;span&gt;)&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;&lt;span&gt;    &lt;/span&gt;&lt;/span&gt;&lt;span&gt;})),&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;}&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/figure&gt;&lt;/div&gt;
&lt;p&gt;这里有两个面试时值得说清楚的点。&lt;/p&gt;
&lt;p&gt;第一，后端并不是直接写一个普通 HTTP JSON 接口，而是先创建 &lt;code dir=&quot;auto&quot;&gt;grpc.NewServer()&lt;/code&gt;，再通过 &lt;code dir=&quot;auto&quot;&gt;grpcweb.WrapServer()&lt;/code&gt; 包装。&lt;/p&gt;
&lt;p&gt;第二，因为前端运行在 &lt;code dir=&quot;auto&quot;&gt;http://localhost:3000&lt;/code&gt;，后端运行在 &lt;code dir=&quot;auto&quot;&gt;http://localhost:8080&lt;/code&gt;，所以需要配置 CORS。否则浏览器会先把请求挡掉，根本到不了 gRPC-Web 服务。&lt;/p&gt;
&lt;div&gt;&lt;h2 id=&quot;前端如何调用&quot;&gt;前端如何调用&lt;/h2&gt;&lt;/div&gt;
&lt;p&gt;前端是 Next.js 页面。因为要在浏览器里响应用户输入和发起请求，所以页面文件使用了 &lt;code dir=&quot;auto&quot;&gt;&apos;use client&apos;&lt;/code&gt;。&lt;/p&gt;
&lt;div&gt;&lt;figure&gt;&lt;figcaption&gt;&lt;span&gt;page.tsx&lt;/span&gt;&lt;/figcaption&gt;&lt;pre&gt;&lt;code&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;&apos;&lt;/span&gt;&lt;span&gt;use client&lt;/span&gt;&lt;span&gt;&apos;&lt;/span&gt;&lt;span&gt;;&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;
&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;import&lt;/span&gt;&lt;span&gt; { useState } &lt;/span&gt;&lt;span&gt;from&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;&apos;&lt;/span&gt;&lt;span&gt;react&lt;/span&gt;&lt;span&gt;&apos;&lt;/span&gt;&lt;span&gt;;&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;import&lt;/span&gt;&lt;span&gt; { CalculatorClient } &lt;/span&gt;&lt;span&gt;from&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;&apos;&lt;/span&gt;&lt;span&gt;./generated/calculator/calculator_grpc_web_pb&lt;/span&gt;&lt;span&gt;&apos;&lt;/span&gt;&lt;span&gt;;&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;import&lt;/span&gt;&lt;span&gt; { CalculateRequest } &lt;/span&gt;&lt;span&gt;from&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;&apos;&lt;/span&gt;&lt;span&gt;./generated/calculator/calculator_pb&lt;/span&gt;&lt;span&gt;&apos;&lt;/span&gt;&lt;span&gt;;&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;
&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;const &lt;/span&gt;&lt;span&gt;client&lt;/span&gt;&lt;span&gt; = &lt;/span&gt;&lt;span&gt;new&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;CalculatorClient&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;&apos;&lt;/span&gt;&lt;span&gt;http://localhost:8080&lt;/span&gt;&lt;span&gt;&apos;&lt;/span&gt;&lt;span&gt;, &lt;/span&gt;&lt;span&gt;null&lt;/span&gt;&lt;span&gt;, &lt;/span&gt;&lt;span&gt;null&lt;/span&gt;&lt;span&gt;);&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;
&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;export&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;default&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;function&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;Home&lt;/span&gt;&lt;span&gt;()&lt;/span&gt;&lt;span&gt; {&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;  &lt;/span&gt;&lt;span&gt;const [&lt;/span&gt;&lt;span&gt;operand1&lt;/span&gt;&lt;span&gt;,&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;setOperand1&lt;/span&gt;&lt;span&gt;] = &lt;/span&gt;&lt;span&gt;useState&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;&apos;&apos;&lt;/span&gt;&lt;span&gt;);&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;  &lt;/span&gt;&lt;span&gt;const [&lt;/span&gt;&lt;span&gt;operand2&lt;/span&gt;&lt;span&gt;,&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;setOperand2&lt;/span&gt;&lt;span&gt;] = &lt;/span&gt;&lt;span&gt;useState&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;&apos;&apos;&lt;/span&gt;&lt;span&gt;);&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;  &lt;/span&gt;&lt;span&gt;const [&lt;/span&gt;&lt;span&gt;operator&lt;/span&gt;&lt;span&gt;,&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;setOperator&lt;/span&gt;&lt;span&gt;] = &lt;/span&gt;&lt;span&gt;useState&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;&apos;&lt;/span&gt;&lt;span&gt;+&lt;/span&gt;&lt;span&gt;&apos;&lt;/span&gt;&lt;span&gt;);&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;  &lt;/span&gt;&lt;span&gt;const [&lt;/span&gt;&lt;span&gt;result&lt;/span&gt;&lt;span&gt;,&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;setResult&lt;/span&gt;&lt;span&gt;] = &lt;/span&gt;&lt;span&gt;useState&lt;/span&gt;&lt;span&gt;&amp;#x3C;&lt;/span&gt;&lt;span&gt;number&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;|&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;null&lt;/span&gt;&lt;span&gt;&gt;&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;null&lt;/span&gt;&lt;span&gt;);&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;  &lt;/span&gt;&lt;span&gt;const [&lt;/span&gt;&lt;span&gt;error&lt;/span&gt;&lt;span&gt;,&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;setError&lt;/span&gt;&lt;span&gt;] = &lt;/span&gt;&lt;span&gt;useState&lt;/span&gt;&lt;span&gt;&amp;#x3C;&lt;/span&gt;&lt;span&gt;string&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;|&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;null&lt;/span&gt;&lt;span&gt;&gt;&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;null&lt;/span&gt;&lt;span&gt;);&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;  &lt;/span&gt;&lt;span&gt;const [&lt;/span&gt;&lt;span&gt;loading&lt;/span&gt;&lt;span&gt;,&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;setLoading&lt;/span&gt;&lt;span&gt;] = &lt;/span&gt;&lt;span&gt;useState&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;false&lt;/span&gt;&lt;span&gt;);&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;
&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;  &lt;/span&gt;&lt;span&gt;const &lt;/span&gt;&lt;span&gt;handleCalculate&lt;/span&gt;&lt;span&gt; = &lt;/span&gt;&lt;span&gt;()&lt;/span&gt;&lt;span&gt; =&gt; {&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;    &lt;/span&gt;&lt;span&gt;setLoading&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;true&lt;/span&gt;&lt;span&gt;)&lt;/span&gt;&lt;span&gt;;&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;    &lt;/span&gt;&lt;span&gt;setResult&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;null&lt;/span&gt;&lt;span&gt;)&lt;/span&gt;&lt;span&gt;;&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;    &lt;/span&gt;&lt;span&gt;setError&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;null&lt;/span&gt;&lt;span&gt;)&lt;/span&gt;&lt;span&gt;;&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;
&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;&lt;span&gt;    &lt;/span&gt;&lt;/span&gt;&lt;span&gt;const &lt;/span&gt;&lt;span&gt;req&lt;/span&gt;&lt;span&gt; = &lt;/span&gt;&lt;span&gt;new&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;CalculateRequest&lt;/span&gt;&lt;span&gt;()&lt;/span&gt;&lt;span&gt;;&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;    &lt;/span&gt;&lt;span&gt;req&lt;/span&gt;&lt;span&gt;.&lt;/span&gt;&lt;span&gt;setOperand1&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;parseFloat&lt;/span&gt;&lt;span&gt;(operand1))&lt;/span&gt;&lt;span&gt;;&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;    &lt;/span&gt;&lt;span&gt;req&lt;/span&gt;&lt;span&gt;.&lt;/span&gt;&lt;span&gt;setOperand2&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;parseFloat&lt;/span&gt;&lt;span&gt;(operand2))&lt;/span&gt;&lt;span&gt;;&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;    &lt;/span&gt;&lt;span&gt;req&lt;/span&gt;&lt;span&gt;.&lt;/span&gt;&lt;span&gt;setOperator&lt;/span&gt;&lt;span&gt;(operator)&lt;/span&gt;&lt;span&gt;;&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;
&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;    &lt;/span&gt;&lt;span&gt;client&lt;/span&gt;&lt;span&gt;.&lt;/span&gt;&lt;span&gt;calculate&lt;/span&gt;&lt;span&gt;(req&lt;/span&gt;&lt;span&gt;,&lt;/span&gt;&lt;span&gt; {}&lt;/span&gt;&lt;span&gt;,&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;err&lt;/span&gt;&lt;span&gt;, &lt;/span&gt;&lt;span&gt;response&lt;/span&gt;&lt;span&gt;)&lt;/span&gt;&lt;span&gt; =&gt; {&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;      &lt;/span&gt;&lt;span&gt;setLoading&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;false&lt;/span&gt;&lt;span&gt;)&lt;/span&gt;&lt;span&gt;;&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;
&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;&lt;span&gt;      &lt;/span&gt;&lt;/span&gt;&lt;span&gt;if &lt;/span&gt;&lt;span&gt;(err)&lt;/span&gt;&lt;span&gt; {&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;        &lt;/span&gt;&lt;span&gt;setError&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;&apos;&lt;/span&gt;&lt;span&gt;请求失败: &lt;/span&gt;&lt;span&gt;&apos;&lt;/span&gt;&lt;span&gt; + &lt;/span&gt;&lt;span&gt;err&lt;/span&gt;&lt;span&gt;.&lt;/span&gt;&lt;span&gt;message&lt;/span&gt;&lt;span&gt;)&lt;/span&gt;&lt;span&gt;;&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;&lt;span&gt;        &lt;/span&gt;&lt;/span&gt;&lt;span&gt;return;&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;&lt;span&gt;      &lt;/span&gt;&lt;/span&gt;&lt;span&gt;}&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;
&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;      &lt;/span&gt;&lt;span&gt;setResult&lt;/span&gt;&lt;span&gt;(response&lt;/span&gt;&lt;span&gt;.&lt;/span&gt;&lt;span&gt;getResult&lt;/span&gt;&lt;span&gt;())&lt;/span&gt;&lt;span&gt;;&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;&lt;span&gt;    &lt;/span&gt;&lt;/span&gt;&lt;span&gt;}&lt;/span&gt;&lt;span&gt;)&lt;/span&gt;&lt;span&gt;;&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;&lt;span&gt;  &lt;/span&gt;&lt;/span&gt;&lt;span&gt;}&lt;/span&gt;&lt;span&gt;;&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;}&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/figure&gt;&lt;/div&gt;
&lt;p&gt;这里的关键不是 &lt;code dir=&quot;auto&quot;&gt;useState&lt;/code&gt;，而是这三步：&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;使用生成的 &lt;code dir=&quot;auto&quot;&gt;CalculatorClient&lt;/code&gt; 创建客户端。&lt;/li&gt;
&lt;li&gt;使用生成的 &lt;code dir=&quot;auto&quot;&gt;CalculateRequest&lt;/code&gt; 创建请求对象。&lt;/li&gt;
&lt;li&gt;调用 &lt;code dir=&quot;auto&quot;&gt;client.calculate()&lt;/code&gt;，在回调里处理结果或错误。&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;这说明前端并没有手写请求路径、请求体字段和响应解析逻辑。它依赖 &lt;code dir=&quot;auto&quot;&gt;.proto&lt;/code&gt; 生成的代码来保证调用结构一致。&lt;/p&gt;
&lt;div&gt;&lt;h2 id=&quot;这道题真正考什么&quot;&gt;这道题真正考什么&lt;/h2&gt;&lt;/div&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;gRPC 基础&lt;/td&gt;&lt;td&gt;是否知道 &lt;code dir=&quot;auto&quot;&gt;.proto&lt;/code&gt;、service、message、生成代码&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;前后端通信&lt;/td&gt;&lt;td&gt;是否知道浏览器不能直接使用原生 gRPC，需要 gRPC-Web&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;Go 服务端&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 dir=&quot;auto&quot;&gt;0&lt;/code&gt;、未知运算符、请求失败&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;跨域问题&lt;/td&gt;&lt;td&gt;是否知道前后端端口不同会触发 CORS&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;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;我用 &lt;code dir=&quot;auto&quot;&gt;.proto&lt;/code&gt; 定义了计算服务和消息结构，再用 Go 实现 gRPC 服务。因为前端运行在浏览器里，不能直接调用原生 gRPC，所以后端用 gRPC-Web 包装了一层 HTTP 服务，并配置 CORS。前端通过生成的 gRPC-Web Client 创建请求对象，调用后端的 &lt;code dir=&quot;auto&quot;&gt;Calculate&lt;/code&gt; 方法，最后处理成功结果和错误信息。&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;这段回答会比“我写了加减乘除”更能说明你理解了题目。&lt;/p&gt;
&lt;div&gt;&lt;h2 id=&quot;可以继续优化的地方&quot;&gt;可以继续优化的地方&lt;/h2&gt;&lt;/div&gt;
&lt;p&gt;这个项目作为面试题已经能跑通主流程，但如果继续完善，可以从下面几个方向补强。&lt;/p&gt;
&lt;p&gt;第一，输入校验可以提前放在前端。&lt;/p&gt;
&lt;p&gt;现在前端直接对输入值 &lt;code dir=&quot;auto&quot;&gt;parseFloat()&lt;/code&gt;。如果用户没有输入数字，可能得到 &lt;code dir=&quot;auto&quot;&gt;NaN&lt;/code&gt;。可以在发请求前判断两个操作数是否合法。&lt;/p&gt;
&lt;p&gt;第二，后端错误可以使用 gRPC status。&lt;/p&gt;
&lt;p&gt;当前代码使用 &lt;code dir=&quot;auto&quot;&gt;fmt.Errorf()&lt;/code&gt; 返回错误。实际项目里可以使用 &lt;code dir=&quot;auto&quot;&gt;status.Error()&lt;/code&gt; 搭配 &lt;code dir=&quot;auto&quot;&gt;codes.InvalidArgument&lt;/code&gt;，这样调用方可以更准确地区分错误类型。&lt;/p&gt;
&lt;div&gt;&lt;figure&gt;&lt;figcaption&gt;&lt;/figcaption&gt;&lt;pre&gt;&lt;code&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;return&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;nil&lt;/span&gt;&lt;span&gt;, &lt;/span&gt;&lt;span&gt;status&lt;/span&gt;&lt;span&gt;.&lt;/span&gt;&lt;span&gt;Error&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;codes&lt;/span&gt;&lt;span&gt;.&lt;/span&gt;&lt;span&gt;InvalidArgument&lt;/span&gt;&lt;span&gt;, &lt;/span&gt;&lt;span&gt;&quot;&lt;/span&gt;&lt;span&gt;division by zero&lt;/span&gt;&lt;span&gt;&quot;&lt;/span&gt;&lt;span&gt;)&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/figure&gt;&lt;/div&gt;
&lt;p&gt;第三，运算符可以使用枚举。&lt;/p&gt;
&lt;p&gt;当前 &lt;code dir=&quot;auto&quot;&gt;operator&lt;/code&gt; 是字符串，优点是直观，缺点是容易传入非法值。如果要更严谨，可以在 &lt;code dir=&quot;auto&quot;&gt;.proto&lt;/code&gt; 中定义 enum。&lt;/p&gt;
&lt;div&gt;&lt;figure&gt;&lt;figcaption&gt;&lt;/figcaption&gt;&lt;pre&gt;&lt;code&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;enum&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;Operator&lt;/span&gt;&lt;span&gt; {&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;  &lt;/span&gt;&lt;span&gt;OPERATOR_UNSPECIFIED&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;=&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;0&lt;/span&gt;&lt;span&gt;;&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;  &lt;/span&gt;&lt;span&gt;ADD&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;=&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;1&lt;/span&gt;&lt;span&gt;;&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;  &lt;/span&gt;&lt;span&gt;SUBTRACT&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;=&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;2&lt;/span&gt;&lt;span&gt;;&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;  &lt;/span&gt;&lt;span&gt;MULTIPLY&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;=&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;3&lt;/span&gt;&lt;span&gt;;&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;  &lt;/span&gt;&lt;span&gt;DIVIDE&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;=&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;4&lt;/span&gt;&lt;span&gt;;&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;}&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/figure&gt;&lt;/div&gt;
&lt;p&gt;第四，配置可以抽出来。&lt;/p&gt;
&lt;p&gt;前端里的 &lt;code dir=&quot;auto&quot;&gt;http://localhost:8080&lt;/code&gt; 和后端 CORS 里的 &lt;code dir=&quot;auto&quot;&gt;http://localhost:3000&lt;/code&gt; 都是开发环境地址。后续如果部署，最好放进环境变量里。&lt;/p&gt;
&lt;div&gt;&lt;h2 id=&quot;面试时怎么讲&quot;&gt;面试时怎么讲&lt;/h2&gt;&lt;/div&gt;
&lt;p&gt;这道题可以按四步讲：&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;先说题目目标：前端输入表达式，后端通过 gRPC 完成计算。&lt;/li&gt;
&lt;li&gt;再说接口契约：用 &lt;code dir=&quot;auto&quot;&gt;.proto&lt;/code&gt; 定义 &lt;code dir=&quot;auto&quot;&gt;Calculate&lt;/code&gt; 方法、请求和响应。&lt;/li&gt;
&lt;li&gt;接着说后端实现：Go 实现服务，处理四种运算和异常情况。&lt;/li&gt;
&lt;li&gt;最后说浏览器调用：使用 gRPC-Web 生成客户端，前端发起请求并处理结果。&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;如果面试官继续追问，可以展开这几个点：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;gRPC 和 REST 的区别是什么？&lt;/li&gt;
&lt;li&gt;为什么浏览器需要 gRPC-Web？&lt;/li&gt;
&lt;li&gt;&lt;code dir=&quot;auto&quot;&gt;.proto&lt;/code&gt; 文件改了以后要做什么？&lt;/li&gt;
&lt;li&gt;如果除数为 &lt;code dir=&quot;auto&quot;&gt;0&lt;/code&gt;，应该怎么返回错误？&lt;/li&gt;
&lt;li&gt;如果以后要支持更多运算，应该怎么扩展？&lt;/li&gt;
&lt;/ul&gt;
&lt;div&gt;&lt;h2 id=&quot;小结&quot;&gt;小结&lt;/h2&gt;&lt;/div&gt;
&lt;p&gt;这个项目的价值不在于计算器本身，而在于它用一个很小的功能，把 gRPC 项目里最关键的链路串起来了：&lt;/p&gt;
&lt;div&gt;&lt;figure&gt;&lt;figcaption&gt;&lt;/figcaption&gt;&lt;pre&gt;&lt;code&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;proto 定义 -&gt; 生成代码 -&gt; Go 实现服务 -&gt; gRPC-Web 包装 -&gt; 前端调用 -&gt; 展示结果&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/figure&gt;&lt;/div&gt;
&lt;p&gt;对于面试题来说，这种项目很合适。它足够小，能在有限时间内完成；同时又能覆盖接口设计、后端实现、前端调用、跨域和错误处理这些真实工程里会遇到的问题。&lt;/p&gt;</content:encoded><category>Go</category><category>gRPC</category><category>后端</category><category>面试</category></item><item><title>Java 集合底层：List、Set、HashMap 与红黑树</title><link>https://blog.veyliss.top/blog/java_collection_hash_tree_interview_01/</link><guid isPermaLink="true">https://blog.veyliss.top/blog/java_collection_hash_tree_interview_01/</guid><pubDate>Tue, 11 Jun 2024 00:00:00 GMT</pubDate><content:encoded>&lt;p&gt;Java 集合面试题经常会从一个很小的问题开始：&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;List 和 Set 有什么区别？&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;如果继续追问，很快就会进入 &lt;code dir=&quot;auto&quot;&gt;HashMap&lt;/code&gt;、&lt;code dir=&quot;auto&quot;&gt;HashSet&lt;/code&gt;、&lt;code dir=&quot;auto&quot;&gt;TreeSet&lt;/code&gt;、红黑树、哈希冲突、扩容、&lt;code dir=&quot;auto&quot;&gt;equals()&lt;/code&gt; 和 &lt;code dir=&quot;auto&quot;&gt;hashCode()&lt;/code&gt; 这些内容。&lt;/p&gt;
&lt;p&gt;这篇文章把几份旧笔记整理成一条完整的复习线：先看集合接口，再看哈希表，最后看树结构。&lt;/p&gt;
&lt;div&gt;&lt;h2 id=&quot;一list-和-set-的区别&quot;&gt;一、List 和 Set 的区别&lt;/h2&gt;&lt;/div&gt;
&lt;p&gt;&lt;code dir=&quot;auto&quot;&gt;List&lt;/code&gt; 和 &lt;code dir=&quot;auto&quot;&gt;Set&lt;/code&gt; 都继承自 &lt;code dir=&quot;auto&quot;&gt;Collection&lt;/code&gt; 接口，都属于 Java 集合体系中存放单个元素的容器。&lt;/p&gt;
&lt;p&gt;它们的核心区别可以从三个角度理解。&lt;/p&gt;
&lt;div&gt;&lt;h3 id=&quot;是否允许重复&quot;&gt;是否允许重复&lt;/h3&gt;&lt;/div&gt;
&lt;p&gt;&lt;code dir=&quot;auto&quot;&gt;List&lt;/code&gt; 允许重复元素：&lt;/p&gt;
&lt;div&gt;&lt;figure&gt;&lt;figcaption&gt;&lt;/figcaption&gt;&lt;pre&gt;&lt;code&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;List&lt;/span&gt;&lt;span&gt;&amp;#x3C;&lt;/span&gt;&lt;span&gt;String&lt;/span&gt;&lt;span&gt;&gt; &lt;/span&gt;&lt;span&gt;list&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;=&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;new&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;ArrayList&lt;/span&gt;&lt;span&gt;&amp;#x3C;&gt;();&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;list&lt;/span&gt;&lt;span&gt;.&lt;/span&gt;&lt;span&gt;add&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;&quot;&lt;/span&gt;&lt;span&gt;Java&lt;/span&gt;&lt;span&gt;&quot;&lt;/span&gt;&lt;span&gt;)&lt;/span&gt;&lt;span&gt;;&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;list&lt;/span&gt;&lt;span&gt;.&lt;/span&gt;&lt;span&gt;add&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;&quot;&lt;/span&gt;&lt;span&gt;Java&lt;/span&gt;&lt;span&gt;&quot;&lt;/span&gt;&lt;span&gt;)&lt;/span&gt;&lt;span&gt;;&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;
&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;System&lt;/span&gt;&lt;span&gt;.&lt;/span&gt;&lt;span&gt;out&lt;/span&gt;&lt;span&gt;.&lt;/span&gt;&lt;span&gt;println&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;list&lt;/span&gt;&lt;span&gt;.&lt;/span&gt;&lt;span&gt;size&lt;/span&gt;&lt;span&gt;())&lt;/span&gt;&lt;span&gt;; &lt;/span&gt;&lt;span&gt;// 2&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/figure&gt;&lt;/div&gt;
&lt;p&gt;&lt;code dir=&quot;auto&quot;&gt;Set&lt;/code&gt; 不允许重复元素：&lt;/p&gt;
&lt;div&gt;&lt;figure&gt;&lt;figcaption&gt;&lt;/figcaption&gt;&lt;pre&gt;&lt;code&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;Set&lt;/span&gt;&lt;span&gt;&amp;#x3C;&lt;/span&gt;&lt;span&gt;String&lt;/span&gt;&lt;span&gt;&gt; &lt;/span&gt;&lt;span&gt;set&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;=&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;new&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;HashSet&lt;/span&gt;&lt;span&gt;&amp;#x3C;&gt;();&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;set&lt;/span&gt;&lt;span&gt;.&lt;/span&gt;&lt;span&gt;add&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;&quot;&lt;/span&gt;&lt;span&gt;Java&lt;/span&gt;&lt;span&gt;&quot;&lt;/span&gt;&lt;span&gt;)&lt;/span&gt;&lt;span&gt;;&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;set&lt;/span&gt;&lt;span&gt;.&lt;/span&gt;&lt;span&gt;add&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;&quot;&lt;/span&gt;&lt;span&gt;Java&lt;/span&gt;&lt;span&gt;&quot;&lt;/span&gt;&lt;span&gt;)&lt;/span&gt;&lt;span&gt;;&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;
&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;System&lt;/span&gt;&lt;span&gt;.&lt;/span&gt;&lt;span&gt;out&lt;/span&gt;&lt;span&gt;.&lt;/span&gt;&lt;span&gt;println&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;set&lt;/span&gt;&lt;span&gt;.&lt;/span&gt;&lt;span&gt;size&lt;/span&gt;&lt;span&gt;())&lt;/span&gt;&lt;span&gt;; &lt;/span&gt;&lt;span&gt;// 1&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/figure&gt;&lt;/div&gt;
&lt;p&gt;需要注意的是，&lt;code dir=&quot;auto&quot;&gt;Set&lt;/code&gt; 判断重复通常依赖 &lt;code dir=&quot;auto&quot;&gt;equals()&lt;/code&gt; 和 &lt;code dir=&quot;auto&quot;&gt;hashCode()&lt;/code&gt;。如果自定义对象放入 &lt;code dir=&quot;auto&quot;&gt;HashSet&lt;/code&gt;，却没有正确重写这两个方法，就可能出现“看起来相同的对象却没有去重”的问题。&lt;/p&gt;
&lt;div&gt;&lt;h3 id=&quot;是否保持插入顺序&quot;&gt;是否保持插入顺序&lt;/h3&gt;&lt;/div&gt;
&lt;p&gt;&lt;code dir=&quot;auto&quot;&gt;List&lt;/code&gt; 按插入顺序保存元素，可以通过下标访问：&lt;/p&gt;
&lt;div&gt;&lt;figure&gt;&lt;figcaption&gt;&lt;/figcaption&gt;&lt;pre&gt;&lt;code&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;list&lt;/span&gt;&lt;span&gt;.&lt;/span&gt;&lt;span&gt;get&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;0&lt;/span&gt;&lt;span&gt;)&lt;/span&gt;&lt;span&gt;;&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/figure&gt;&lt;/div&gt;
&lt;p&gt;&lt;code dir=&quot;auto&quot;&gt;Set&lt;/code&gt; 不一定保持插入顺序，也不能通过下标访问元素。&lt;/p&gt;
&lt;p&gt;不同 &lt;code dir=&quot;auto&quot;&gt;Set&lt;/code&gt; 实现的顺序语义也不同：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code dir=&quot;auto&quot;&gt;HashSet&lt;/code&gt;：不保证插入顺序，也不保证排序。&lt;/li&gt;
&lt;li&gt;&lt;code dir=&quot;auto&quot;&gt;LinkedHashSet&lt;/code&gt;：按插入顺序迭代。&lt;/li&gt;
&lt;li&gt;&lt;code dir=&quot;auto&quot;&gt;TreeSet&lt;/code&gt;：按自然顺序或比较器规则排序。&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;所以不能简单说“Set 会升序排序”。准确说法是：&lt;strong&gt;TreeSet 会排序，HashSet 不保证顺序。&lt;/strong&gt;&lt;/p&gt;
&lt;div&gt;&lt;h3 id=&quot;查询插入和删除特点&quot;&gt;查询、插入和删除特点&lt;/h3&gt;&lt;/div&gt;
&lt;p&gt;&lt;code dir=&quot;auto&quot;&gt;List&lt;/code&gt; 更像动态数组或链表，具体性能取决于实现类。&lt;/p&gt;
&lt;p&gt;&lt;code dir=&quot;auto&quot;&gt;ArrayList&lt;/code&gt; 的特点：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;按下标访问快，时间复杂度通常是 &lt;code dir=&quot;auto&quot;&gt;O(1)&lt;/code&gt;。&lt;/li&gt;
&lt;li&gt;中间插入和删除可能需要移动元素，时间复杂度通常是 &lt;code dir=&quot;auto&quot;&gt;O(n)&lt;/code&gt;。&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;code dir=&quot;auto&quot;&gt;LinkedList&lt;/code&gt; 的特点：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;插入和删除节点时，只需要调整节点引用。&lt;/li&gt;
&lt;li&gt;但查找指定位置的元素需要遍历链表。&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;code dir=&quot;auto&quot;&gt;Set&lt;/code&gt; 更强调“唯一性”。例如 &lt;code dir=&quot;auto&quot;&gt;HashSet&lt;/code&gt; 底层依赖哈希表，添加、删除、查找在理想情况下可以接近 &lt;code dir=&quot;auto&quot;&gt;O(1)&lt;/code&gt;。&lt;/p&gt;
&lt;div&gt;&lt;h2 id=&quot;二collection-常见方法&quot;&gt;二、Collection 常见方法&lt;/h2&gt;&lt;/div&gt;
&lt;p&gt;&lt;code dir=&quot;auto&quot;&gt;Collection&lt;/code&gt; 是 &lt;code dir=&quot;auto&quot;&gt;List&lt;/code&gt;、&lt;code dir=&quot;auto&quot;&gt;Set&lt;/code&gt; 等集合接口的上层接口，常见方法包括：&lt;/p&gt;
&lt;div&gt;&lt;figure&gt;&lt;figcaption&gt;&lt;/figcaption&gt;&lt;pre&gt;&lt;code&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;boolean&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;add&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;E&lt;/span&gt;&lt;span&gt; e&lt;/span&gt;&lt;span&gt;)&lt;/span&gt;&lt;span&gt;;&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;boolean&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;remove&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;Object&lt;/span&gt;&lt;span&gt; o&lt;/span&gt;&lt;span&gt;)&lt;/span&gt;&lt;span&gt;;&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;boolean&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;contains&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;Object&lt;/span&gt;&lt;span&gt; o&lt;/span&gt;&lt;span&gt;)&lt;/span&gt;&lt;span&gt;;&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;boolean&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;isEmpty&lt;/span&gt;&lt;span&gt;()&lt;/span&gt;&lt;span&gt;;&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;int&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;size&lt;/span&gt;&lt;span&gt;()&lt;/span&gt;&lt;span&gt;;&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;void&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;clear&lt;/span&gt;&lt;span&gt;()&lt;/span&gt;&lt;span&gt;;&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;Iterator&amp;#x3C;&lt;/span&gt;&lt;span&gt;E&lt;/span&gt;&lt;span&gt;&gt;&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;iterator&lt;/span&gt;&lt;span&gt;()&lt;/span&gt;&lt;span&gt;;&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;Object&lt;/span&gt;&lt;span&gt;[] &lt;/span&gt;&lt;span&gt;toArray&lt;/span&gt;&lt;span&gt;()&lt;/span&gt;&lt;span&gt;;&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/figure&gt;&lt;/div&gt;
&lt;p&gt;有一个细节很容易忽略：&lt;code dir=&quot;auto&quot;&gt;Collection&lt;/code&gt; 本身没有 &lt;code dir=&quot;auto&quot;&gt;get(index)&lt;/code&gt; 方法。&lt;/p&gt;
&lt;p&gt;因为不是所有集合都有下标语义。例如 &lt;code dir=&quot;auto&quot;&gt;List&lt;/code&gt; 可以按下标取值，但 &lt;code dir=&quot;auto&quot;&gt;Set&lt;/code&gt; 没有稳定的下标概念，只能通过迭代器遍历：&lt;/p&gt;
&lt;div&gt;&lt;figure&gt;&lt;figcaption&gt;&lt;/figcaption&gt;&lt;pre&gt;&lt;code&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;for&lt;/span&gt;&lt;span&gt; (&lt;/span&gt;&lt;span&gt;String&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;item&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;:&lt;/span&gt;&lt;span&gt; set) {&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;    &lt;/span&gt;&lt;span&gt;System&lt;/span&gt;&lt;span&gt;.&lt;/span&gt;&lt;span&gt;out&lt;/span&gt;&lt;span&gt;.&lt;/span&gt;&lt;span&gt;println&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;item&lt;/span&gt;&lt;span&gt;)&lt;/span&gt;&lt;span&gt;;&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;}&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/figure&gt;&lt;/div&gt;
&lt;div&gt;&lt;h2 id=&quot;三哈希表是什么&quot;&gt;三、哈希表是什么&lt;/h2&gt;&lt;/div&gt;
&lt;p&gt;哈希表也叫散列表，是一种非常重要的数据结构。很多缓存、字典、索引、去重结构的核心思想，都是在内存中维护一张哈希表。&lt;/p&gt;
&lt;p&gt;哈希表的核心公式可以理解为：&lt;/p&gt;
&lt;div&gt;&lt;figure&gt;&lt;figcaption&gt;&lt;/figcaption&gt;&lt;pre&gt;&lt;code&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;存储位置 = f(关键字)&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/figure&gt;&lt;/div&gt;
&lt;p&gt;这里的 &lt;code dir=&quot;auto&quot;&gt;f&lt;/code&gt; 就是哈希函数。它会根据 key 计算出一个哈希值，再根据数组长度换算成数组下标。&lt;/p&gt;
&lt;p&gt;理想情况下，哈希表的查询过程是：&lt;/p&gt;
&lt;div&gt;&lt;figure&gt;&lt;figcaption&gt;&lt;/figcaption&gt;&lt;pre&gt;&lt;code&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;key -&gt; hash -&gt; 数组下标 -&gt; 找到元素&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/figure&gt;&lt;/div&gt;
&lt;p&gt;如果没有哈希冲突，查找、插入、删除都可以接近 &lt;code dir=&quot;auto&quot;&gt;O(1)&lt;/code&gt;。&lt;/p&gt;
&lt;p&gt;和其他结构对比：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;数组：按下标访问快，按值查找通常需要遍历。&lt;/li&gt;
&lt;li&gt;链表：插入删除节点方便，但查找需要遍历。&lt;/li&gt;
&lt;li&gt;平衡二叉搜索树：查找、插入、删除通常是 &lt;code dir=&quot;auto&quot;&gt;O(log n)&lt;/code&gt;。&lt;/li&gt;
&lt;li&gt;哈希表：理想情况下查找、插入、删除接近 &lt;code dir=&quot;auto&quot;&gt;O(1)&lt;/code&gt;。&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;不过哈希表的性能高度依赖哈希函数、数组容量、负载因子和冲突处理方式。&lt;/p&gt;
&lt;div&gt;&lt;h2 id=&quot;四哈希冲突怎么解决&quot;&gt;四、哈希冲突怎么解决&lt;/h2&gt;&lt;/div&gt;
&lt;p&gt;哈希冲突指的是：不同 key 经过哈希计算后，落到了同一个数组位置。&lt;/p&gt;
&lt;p&gt;例如：&lt;/p&gt;
&lt;div&gt;&lt;figure&gt;&lt;figcaption&gt;&lt;/figcaption&gt;&lt;pre&gt;&lt;code&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;keyA -&gt; index 3&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;keyB -&gt; index 3&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/figure&gt;&lt;/div&gt;
&lt;p&gt;这时就需要处理冲突。&lt;/p&gt;
&lt;p&gt;常见方式有三类：&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;开放地址法：当前位置冲突后，继续寻找下一个可用位置。&lt;/li&gt;
&lt;li&gt;再哈希法：使用另一个哈希函数重新计算位置。&lt;/li&gt;
&lt;li&gt;链地址法：数组每个位置挂一个链表或树，把冲突元素放到同一个桶里。&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;Java &lt;code dir=&quot;auto&quot;&gt;HashMap&lt;/code&gt; 使用的就是链地址法的思路。更准确地说，在 Java 8 之后，&lt;code dir=&quot;auto&quot;&gt;HashMap&lt;/code&gt; 的桶结构可能是：&lt;/p&gt;
&lt;div&gt;&lt;figure&gt;&lt;figcaption&gt;&lt;/figcaption&gt;&lt;pre&gt;&lt;code&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;数组 + 链表&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;数组 + 红黑树&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/figure&gt;&lt;/div&gt;
&lt;p&gt;当同一个桶里的元素过多时，链表会在满足条件后树化，变成红黑树，以降低极端冲突下的查询成本。&lt;/p&gt;
&lt;div&gt;&lt;h2 id=&quot;五hashmap-的底层结构&quot;&gt;五、HashMap 的底层结构&lt;/h2&gt;&lt;/div&gt;
&lt;p&gt;&lt;code dir=&quot;auto&quot;&gt;HashMap&lt;/code&gt; 存储的是 key-value 键值对。&lt;/p&gt;
&lt;p&gt;可以把它简化理解成：&lt;/p&gt;
&lt;div&gt;&lt;figure&gt;&lt;figcaption&gt;&lt;/figcaption&gt;&lt;pre&gt;&lt;code&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;HashMap&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;&lt;span&gt;  &lt;/span&gt;&lt;/span&gt;&lt;span&gt;table 数组&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;&lt;span&gt;    &lt;/span&gt;&lt;/span&gt;&lt;span&gt;bucket 0 -&gt; null&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;&lt;span&gt;    &lt;/span&gt;&lt;/span&gt;&lt;span&gt;bucket 1 -&gt; Node -&gt; Node&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;&lt;span&gt;    &lt;/span&gt;&lt;/span&gt;&lt;span&gt;bucket 2 -&gt; Node&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;&lt;span&gt;    &lt;/span&gt;&lt;/span&gt;&lt;span&gt;bucket 3 -&gt; TreeNode 红黑树&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/figure&gt;&lt;/div&gt;
&lt;p&gt;每个节点大致保存：&lt;/p&gt;
&lt;div&gt;&lt;figure&gt;&lt;figcaption&gt;&lt;/figcaption&gt;&lt;pre&gt;&lt;code&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;hash&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;key&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;value&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;next&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/figure&gt;&lt;/div&gt;
&lt;p&gt;put 一个元素时，大致过程是：&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;计算 key 的 hash。&lt;/li&gt;
&lt;li&gt;根据 hash 和数组长度计算桶下标。&lt;/li&gt;
&lt;li&gt;如果桶为空，直接放入。&lt;/li&gt;
&lt;li&gt;如果桶不为空，判断 key 是否已经存在。&lt;/li&gt;
&lt;li&gt;如果 key 已存在，更新 value。&lt;/li&gt;
&lt;li&gt;如果 key 不存在，挂到链表或红黑树中。&lt;/li&gt;
&lt;li&gt;如果元素数量超过阈值，触发扩容。&lt;/li&gt;
&lt;/ol&gt;
&lt;div&gt;&lt;h2 id=&quot;六hashmap-的初始容量和负载因子&quot;&gt;六、HashMap 的初始容量和负载因子&lt;/h2&gt;&lt;/div&gt;
&lt;p&gt;&lt;code dir=&quot;auto&quot;&gt;HashMap&lt;/code&gt; 有两个重要参数：&lt;/p&gt;
&lt;div&gt;&lt;figure&gt;&lt;figcaption&gt;&lt;/figcaption&gt;&lt;pre&gt;&lt;code&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;initialCapacity 初始容量&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;loadFactor 负载因子&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/figure&gt;&lt;/div&gt;
&lt;p&gt;常见默认值是：&lt;/p&gt;
&lt;div&gt;&lt;figure&gt;&lt;figcaption&gt;&lt;/figcaption&gt;&lt;pre&gt;&lt;code&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;initialCapacity = 16&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;loadFactor = 0.75&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/figure&gt;&lt;/div&gt;
&lt;p&gt;阈值计算方式可以理解为：&lt;/p&gt;
&lt;div&gt;&lt;figure&gt;&lt;figcaption&gt;&lt;/figcaption&gt;&lt;pre&gt;&lt;code&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;threshold = capacity * loadFactor&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/figure&gt;&lt;/div&gt;
&lt;p&gt;默认情况下：&lt;/p&gt;
&lt;div&gt;&lt;figure&gt;&lt;figcaption&gt;&lt;/figcaption&gt;&lt;pre&gt;&lt;code&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;16 * 0.75 = 12&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/figure&gt;&lt;/div&gt;
&lt;p&gt;当元素数量超过阈值后，&lt;code dir=&quot;auto&quot;&gt;HashMap&lt;/code&gt; 会扩容。扩容通常会带来重新分布元素的成本，所以如果能预估数据量，创建 &lt;code dir=&quot;auto&quot;&gt;HashMap&lt;/code&gt; 时可以指定合适的初始容量。&lt;/p&gt;
&lt;div&gt;&lt;h2 id=&quot;七hashmap-的扩容机制&quot;&gt;七、HashMap 的扩容机制&lt;/h2&gt;&lt;/div&gt;
&lt;p&gt;&lt;code dir=&quot;auto&quot;&gt;HashMap&lt;/code&gt; 的扩容通常发生在元素数量超过阈值之后：&lt;/p&gt;
&lt;div&gt;&lt;figure&gt;&lt;figcaption&gt;&lt;/figcaption&gt;&lt;pre&gt;&lt;code&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;size &gt; threshold&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/figure&gt;&lt;/div&gt;
&lt;p&gt;而阈值由容量和负载因子决定：&lt;/p&gt;
&lt;div&gt;&lt;figure&gt;&lt;figcaption&gt;&lt;/figcaption&gt;&lt;pre&gt;&lt;code&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;threshold = capacity * loadFactor&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/figure&gt;&lt;/div&gt;
&lt;p&gt;默认情况下：&lt;/p&gt;
&lt;div&gt;&lt;figure&gt;&lt;figcaption&gt;&lt;/figcaption&gt;&lt;pre&gt;&lt;code&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;capacity = 16&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;loadFactor = 0.75&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;threshold = 12&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/figure&gt;&lt;/div&gt;
&lt;p&gt;也就是说，当第 13 个元素放入时，&lt;code dir=&quot;auto&quot;&gt;HashMap&lt;/code&gt; 就可能触发扩容。&lt;/p&gt;
&lt;p&gt;扩容的核心动作可以概括成三步：&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;创建一个更大的新数组。&lt;/li&gt;
&lt;li&gt;把旧数组中的节点迁移到新数组。&lt;/li&gt;
&lt;li&gt;重新计算扩容后的阈值。&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;通常情况下，新容量会变成旧容量的 2 倍：&lt;/p&gt;
&lt;div&gt;&lt;figure&gt;&lt;figcaption&gt;&lt;/figcaption&gt;&lt;pre&gt;&lt;code&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;oldCapacity = 16&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;newCapacity = 32&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/figure&gt;&lt;/div&gt;
&lt;p&gt;新阈值也会随之变化：&lt;/p&gt;
&lt;div&gt;&lt;figure&gt;&lt;figcaption&gt;&lt;/figcaption&gt;&lt;pre&gt;&lt;code&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;oldThreshold = 16 * 0.75 = 12&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;newThreshold = 32 * 0.75 = 24&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/figure&gt;&lt;/div&gt;
&lt;div&gt;&lt;h3 id=&quot;扩容时元素怎么迁移&quot;&gt;扩容时元素怎么迁移&lt;/h3&gt;&lt;/div&gt;
&lt;p&gt;在 Java 8 之后，&lt;code dir=&quot;auto&quot;&gt;HashMap&lt;/code&gt; 扩容迁移时有一个很重要的规律：&lt;/p&gt;
&lt;div&gt;&lt;figure&gt;&lt;figcaption&gt;&lt;/figcaption&gt;&lt;pre&gt;&lt;code&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;元素的新位置要么是原位置，要么是原位置 + oldCapacity&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/figure&gt;&lt;/div&gt;
&lt;p&gt;例如旧数组长度是 &lt;code dir=&quot;auto&quot;&gt;16&lt;/code&gt;，某个元素原来在下标 &lt;code dir=&quot;auto&quot;&gt;5&lt;/code&gt;：&lt;/p&gt;
&lt;div&gt;&lt;figure&gt;&lt;figcaption&gt;&lt;/figcaption&gt;&lt;pre&gt;&lt;code&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;旧位置：5&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;新位置：5 或 5 + 16 = 21&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/figure&gt;&lt;/div&gt;
&lt;p&gt;为什么会这样？因为数组长度翻倍后，参与下标计算的二进制位多了一位。只需要看 hash 在这一位上是 &lt;code dir=&quot;auto&quot;&gt;0&lt;/code&gt; 还是 &lt;code dir=&quot;auto&quot;&gt;1&lt;/code&gt;：&lt;/p&gt;
&lt;div&gt;&lt;figure&gt;&lt;figcaption&gt;&lt;/figcaption&gt;&lt;pre&gt;&lt;code&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;hash &amp;#x26; oldCapacity == 0  -&gt; 留在原位置&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;hash &amp;#x26; oldCapacity != 0  -&gt; 移到原位置 + oldCapacity&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/figure&gt;&lt;/div&gt;
&lt;p&gt;可以用一个简化例子理解：&lt;/p&gt;
&lt;div&gt;&lt;figure&gt;&lt;figcaption&gt;&lt;/figcaption&gt;&lt;pre&gt;&lt;code&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;旧容量 16：下标范围 0 ~ 15&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;新容量 32：下标范围 0 ~ 31&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;
&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;原 bucket 5 拆成：&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;&lt;span&gt;  &lt;/span&gt;&lt;/span&gt;&lt;span&gt;bucket 5&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;&lt;span&gt;  &lt;/span&gt;&lt;/span&gt;&lt;span&gt;bucket 21&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/figure&gt;&lt;/div&gt;
&lt;p&gt;这也是 &lt;code dir=&quot;auto&quot;&gt;HashMap&lt;/code&gt; 容量使用 2 的次幂的一个好处：扩容后不需要完全重新计算每个元素的位置，可以通过位运算快速拆分旧桶。&lt;/p&gt;
&lt;div&gt;&lt;h3 id=&quot;扩容和链表树化的关系&quot;&gt;扩容和链表树化的关系&lt;/h3&gt;&lt;/div&gt;
&lt;p&gt;Java 8 之后，&lt;code dir=&quot;auto&quot;&gt;HashMap&lt;/code&gt; 桶内链表过长时可能会树化为红黑树，但并不是链表一长就立刻树化。&lt;/p&gt;
&lt;p&gt;常见条件可以记成：&lt;/p&gt;
&lt;div&gt;&lt;figure&gt;&lt;figcaption&gt;&lt;/figcaption&gt;&lt;pre&gt;&lt;code&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;桶内链表长度 &gt;= 8&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;数组容量 &gt;= 64&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/figure&gt;&lt;/div&gt;
&lt;p&gt;如果桶内链表已经很长，但数组容量还比较小，&lt;code dir=&quot;auto&quot;&gt;HashMap&lt;/code&gt; 通常会优先扩容，而不是马上树化。&lt;/p&gt;
&lt;p&gt;原因也很直观：容量太小时，冲突可能只是数组太小导致的。先扩容可以让元素重新分布，很多冲突自然会减少。&lt;/p&gt;
&lt;p&gt;面试里可以这样总结：&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;&lt;code dir=&quot;auto&quot;&gt;HashMap&lt;/code&gt; 默认容量是 16，负载因子是 0.75，超过阈值后通常扩容为原来的 2 倍。Java 8 扩容迁移时，元素的新位置要么保持原下标，要么移动到原下标加旧容量。桶内链表过长时可能树化，但容量较小时会优先扩容。&lt;/p&gt;
&lt;/blockquote&gt;
&lt;div&gt;&lt;h2 id=&quot;八为什么-hashmap-容量常用-2-的次幂&quot;&gt;八、为什么 HashMap 容量常用 2 的次幂&lt;/h2&gt;&lt;/div&gt;
&lt;p&gt;&lt;code dir=&quot;auto&quot;&gt;HashMap&lt;/code&gt; 的数组长度通常会调整为 2 的次幂。&lt;/p&gt;
&lt;p&gt;这样做的一个重要原因是可以用位运算快速计算下标：&lt;/p&gt;
&lt;div&gt;&lt;figure&gt;&lt;figcaption&gt;&lt;/figcaption&gt;&lt;pre&gt;&lt;code&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;index &lt;/span&gt;&lt;span&gt;=&lt;/span&gt;&lt;span&gt; (n &lt;/span&gt;&lt;span&gt;-&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;1&lt;/span&gt;&lt;span&gt;) &lt;/span&gt;&lt;span&gt;&amp;#x26;&lt;/span&gt;&lt;span&gt; hash;&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/figure&gt;&lt;/div&gt;
&lt;p&gt;当 &lt;code dir=&quot;auto&quot;&gt;n&lt;/code&gt; 是 2 的次幂时，&lt;code dir=&quot;auto&quot;&gt;n - 1&lt;/code&gt; 的二进制低位全是 &lt;code dir=&quot;auto&quot;&gt;1&lt;/code&gt;，可以更好地利用 hash 的低位信息。&lt;/p&gt;
&lt;p&gt;扩容时也有一个好处：元素的新位置通常只有两种可能：&lt;/p&gt;
&lt;div&gt;&lt;figure&gt;&lt;figcaption&gt;&lt;/figcaption&gt;&lt;pre&gt;&lt;code&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;原位置&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;原位置 + oldCapacity&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/figure&gt;&lt;/div&gt;
&lt;p&gt;这可以减少扩容后重新计算和移动元素的成本。&lt;/p&gt;
&lt;div&gt;&lt;h2 id=&quot;九equals-和-hashcode-为什么要一起重写&quot;&gt;九、equals 和 hashCode 为什么要一起重写&lt;/h2&gt;&lt;/div&gt;
&lt;p&gt;在 &lt;code dir=&quot;auto&quot;&gt;HashMap&lt;/code&gt;、&lt;code dir=&quot;auto&quot;&gt;HashSet&lt;/code&gt; 这类哈希结构里，&lt;code dir=&quot;auto&quot;&gt;hashCode()&lt;/code&gt; 决定元素大致落在哪个桶，&lt;code dir=&quot;auto&quot;&gt;equals()&lt;/code&gt; 决定桶内元素是否真的相等。&lt;/p&gt;
&lt;p&gt;如果两个对象通过 &lt;code dir=&quot;auto&quot;&gt;equals()&lt;/code&gt; 判断相等，它们的 &lt;code dir=&quot;auto&quot;&gt;hashCode()&lt;/code&gt; 必须相等。&lt;/p&gt;
&lt;p&gt;错误示例：&lt;/p&gt;
&lt;div&gt;&lt;figure&gt;&lt;figcaption&gt;&lt;/figcaption&gt;&lt;pre&gt;&lt;code&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;class&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;User&lt;/span&gt;&lt;span&gt; {&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;    &lt;/span&gt;&lt;span&gt;private&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;Long&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;id&lt;/span&gt;&lt;span&gt;;&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;
&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;&lt;span&gt;    &lt;/span&gt;&lt;/span&gt;&lt;span&gt;@&lt;/span&gt;&lt;span&gt;Override&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;    &lt;/span&gt;&lt;span&gt;public&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;boolean&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;equals&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;Object&lt;/span&gt;&lt;span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;obj&lt;/span&gt;&lt;/span&gt;&lt;span&gt;)&lt;/span&gt;&lt;span&gt; {&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;        &lt;/span&gt;&lt;span&gt;if&lt;/span&gt;&lt;span&gt; (&lt;/span&gt;&lt;span&gt;this&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;==&lt;/span&gt;&lt;span&gt; obj) &lt;/span&gt;&lt;span&gt;return&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;true&lt;/span&gt;&lt;span&gt;;&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;        &lt;/span&gt;&lt;span&gt;if&lt;/span&gt;&lt;span&gt; (&lt;/span&gt;&lt;span&gt;!&lt;/span&gt;&lt;span&gt;(obj &lt;/span&gt;&lt;span&gt;instanceof&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;User&lt;/span&gt;&lt;span&gt; other)) &lt;/span&gt;&lt;span&gt;return&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;false&lt;/span&gt;&lt;span&gt;;&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;        &lt;/span&gt;&lt;span&gt;return&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;Objects&lt;/span&gt;&lt;span&gt;.&lt;/span&gt;&lt;span&gt;equals&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;this&lt;/span&gt;&lt;span&gt;.&lt;/span&gt;&lt;span&gt;id&lt;/span&gt;&lt;span&gt;, &lt;/span&gt;&lt;span&gt;other&lt;/span&gt;&lt;span&gt;.&lt;/span&gt;&lt;span&gt;&lt;span&gt;id&lt;/span&gt;&lt;span&gt;)&lt;/span&gt;&lt;/span&gt;&lt;span&gt;;&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;&lt;span&gt;    &lt;/span&gt;&lt;/span&gt;&lt;span&gt;}&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;}&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/figure&gt;&lt;/div&gt;
&lt;p&gt;这个类只重写了 &lt;code dir=&quot;auto&quot;&gt;equals()&lt;/code&gt;，没有重写 &lt;code dir=&quot;auto&quot;&gt;hashCode()&lt;/code&gt;。放进 &lt;code dir=&quot;auto&quot;&gt;HashSet&lt;/code&gt; 时，相同 &lt;code dir=&quot;auto&quot;&gt;id&lt;/code&gt; 的对象可能因为 hash 不同，被放到不同桶里，导致去重失败。&lt;/p&gt;
&lt;p&gt;正确做法：&lt;/p&gt;
&lt;div&gt;&lt;figure&gt;&lt;figcaption&gt;&lt;/figcaption&gt;&lt;pre&gt;&lt;code&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;class&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;User&lt;/span&gt;&lt;span&gt; {&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;    &lt;/span&gt;&lt;span&gt;private&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;Long&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;id&lt;/span&gt;&lt;span&gt;;&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;
&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;&lt;span&gt;    &lt;/span&gt;&lt;/span&gt;&lt;span&gt;@&lt;/span&gt;&lt;span&gt;Override&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;    &lt;/span&gt;&lt;span&gt;public&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;boolean&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;equals&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;Object&lt;/span&gt;&lt;span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;obj&lt;/span&gt;&lt;/span&gt;&lt;span&gt;)&lt;/span&gt;&lt;span&gt; {&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;        &lt;/span&gt;&lt;span&gt;if&lt;/span&gt;&lt;span&gt; (&lt;/span&gt;&lt;span&gt;this&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;==&lt;/span&gt;&lt;span&gt; obj) &lt;/span&gt;&lt;span&gt;return&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;true&lt;/span&gt;&lt;span&gt;;&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;        &lt;/span&gt;&lt;span&gt;if&lt;/span&gt;&lt;span&gt; (&lt;/span&gt;&lt;span&gt;!&lt;/span&gt;&lt;span&gt;(obj &lt;/span&gt;&lt;span&gt;instanceof&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;User&lt;/span&gt;&lt;span&gt; other)) &lt;/span&gt;&lt;span&gt;return&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;false&lt;/span&gt;&lt;span&gt;;&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;        &lt;/span&gt;&lt;span&gt;return&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;Objects&lt;/span&gt;&lt;span&gt;.&lt;/span&gt;&lt;span&gt;equals&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;this&lt;/span&gt;&lt;span&gt;.&lt;/span&gt;&lt;span&gt;id&lt;/span&gt;&lt;span&gt;, &lt;/span&gt;&lt;span&gt;other&lt;/span&gt;&lt;span&gt;.&lt;/span&gt;&lt;span&gt;&lt;span&gt;id&lt;/span&gt;&lt;span&gt;)&lt;/span&gt;&lt;/span&gt;&lt;span&gt;;&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;&lt;span&gt;    &lt;/span&gt;&lt;/span&gt;&lt;span&gt;}&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;
&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;&lt;span&gt;    &lt;/span&gt;&lt;/span&gt;&lt;span&gt;@&lt;/span&gt;&lt;span&gt;Override&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;    &lt;/span&gt;&lt;span&gt;public&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;int&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;hashCode&lt;/span&gt;&lt;span&gt;()&lt;/span&gt;&lt;span&gt; {&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;        &lt;/span&gt;&lt;span&gt;return&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;Objects&lt;/span&gt;&lt;span&gt;.&lt;/span&gt;&lt;span&gt;hash&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;id&lt;/span&gt;&lt;span&gt;)&lt;/span&gt;&lt;span&gt;;&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;&lt;span&gt;    &lt;/span&gt;&lt;/span&gt;&lt;span&gt;}&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;}&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/figure&gt;&lt;/div&gt;
&lt;p&gt;面试里可以总结成一句话：&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;只要重写 &lt;code dir=&quot;auto&quot;&gt;equals()&lt;/code&gt;，通常就必须重写 &lt;code dir=&quot;auto&quot;&gt;hashCode()&lt;/code&gt;，否则哈希集合和哈希映射可能出现行为异常。&lt;/p&gt;
&lt;/blockquote&gt;
&lt;div&gt;&lt;h2 id=&quot;十hashset-的底层原理&quot;&gt;十、HashSet 的底层原理&lt;/h2&gt;&lt;/div&gt;
&lt;p&gt;&lt;code dir=&quot;auto&quot;&gt;HashSet&lt;/code&gt; 的底层主要依赖 &lt;code dir=&quot;auto&quot;&gt;HashMap&lt;/code&gt;。&lt;/p&gt;
&lt;p&gt;可以粗略理解成：&lt;/p&gt;
&lt;div&gt;&lt;figure&gt;&lt;figcaption&gt;&lt;/figcaption&gt;&lt;pre&gt;&lt;code&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;HashSet&amp;#x3C;&lt;/span&gt;&lt;span&gt;E&lt;/span&gt;&lt;span&gt;&gt;&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;&lt;span&gt;  &lt;/span&gt;&lt;/span&gt;&lt;span&gt;内部维护 &lt;/span&gt;&lt;span&gt;HashMap&amp;#x3C;&lt;/span&gt;&lt;span&gt;E, Object&lt;/span&gt;&lt;span&gt;&gt;&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/figure&gt;&lt;/div&gt;
&lt;p&gt;&lt;code dir=&quot;auto&quot;&gt;HashSet&lt;/code&gt; 添加元素时，本质上是把元素作为 &lt;code dir=&quot;auto&quot;&gt;HashMap&lt;/code&gt; 的 key 存进去：&lt;/p&gt;
&lt;div&gt;&lt;figure&gt;&lt;figcaption&gt;&lt;/figcaption&gt;&lt;pre&gt;&lt;code&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;map&lt;/span&gt;&lt;span&gt;.&lt;/span&gt;&lt;span&gt;put&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;element, PRESENT&lt;/span&gt;&lt;span&gt;)&lt;/span&gt;&lt;span&gt;;&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/figure&gt;&lt;/div&gt;
&lt;p&gt;因为 &lt;code dir=&quot;auto&quot;&gt;HashMap&lt;/code&gt; 的 key 不能重复，所以 &lt;code dir=&quot;auto&quot;&gt;HashSet&lt;/code&gt; 就天然具备了去重能力。&lt;/p&gt;
&lt;p&gt;因此，&lt;code dir=&quot;auto&quot;&gt;HashSet&lt;/code&gt; 的几个特点可以这样理解：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;不允许重复元素。&lt;/li&gt;
&lt;li&gt;不保证插入顺序。&lt;/li&gt;
&lt;li&gt;底层依赖 &lt;code dir=&quot;auto&quot;&gt;hashCode()&lt;/code&gt; 和 &lt;code dir=&quot;auto&quot;&gt;equals()&lt;/code&gt; 判断重复。&lt;/li&gt;
&lt;li&gt;查询、插入、删除在理想情况下接近 &lt;code dir=&quot;auto&quot;&gt;O(1)&lt;/code&gt;。&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;如果需要保持插入顺序，可以使用 &lt;code dir=&quot;auto&quot;&gt;LinkedHashSet&lt;/code&gt;。如果需要排序，可以使用 &lt;code dir=&quot;auto&quot;&gt;TreeSet&lt;/code&gt;。&lt;/p&gt;
&lt;div&gt;&lt;h2 id=&quot;十一二叉树二叉搜索树和平衡二叉树&quot;&gt;十一、二叉树、二叉搜索树和平衡二叉树&lt;/h2&gt;&lt;/div&gt;
&lt;p&gt;普通二叉树只限制每个节点最多有两个子节点，本身不要求元素大小关系。&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://blog.veyliss.top/diagrams/binary-tree-rotation-guide.svg&quot; alt=&quot;二叉搜索树、退化树和四种平衡旋转示意图&quot;&gt;&lt;/p&gt;
&lt;p&gt;二叉搜索树则有明确规则：&lt;/p&gt;
&lt;div&gt;&lt;figure&gt;&lt;figcaption&gt;&lt;/figcaption&gt;&lt;pre&gt;&lt;code&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;左子树节点 &amp;#x3C; 当前节点 &amp;#x3C; 右子树节点&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/figure&gt;&lt;/div&gt;
&lt;p&gt;例如下面这棵树就是一棵二叉搜索树：&lt;/p&gt;
&lt;div&gt;&lt;figure&gt;&lt;figcaption&gt;&lt;/figcaption&gt;&lt;pre&gt;&lt;code&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;&lt;span&gt;          &lt;/span&gt;&lt;/span&gt;&lt;span&gt;8&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;&lt;span&gt;        &lt;/span&gt;&lt;/span&gt;&lt;span&gt;/   \&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;&lt;span&gt;       &lt;/span&gt;&lt;/span&gt;&lt;span&gt;4     12&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;&lt;span&gt;      &lt;/span&gt;&lt;/span&gt;&lt;span&gt;/ \    / \&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;&lt;span&gt;     &lt;/span&gt;&lt;/span&gt;&lt;span&gt;2   6  10  14&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/figure&gt;&lt;/div&gt;
&lt;p&gt;每个节点都满足：&lt;/p&gt;
&lt;div&gt;&lt;figure&gt;&lt;figcaption&gt;&lt;/figcaption&gt;&lt;pre&gt;&lt;code&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;左边比自己小，右边比自己大&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/figure&gt;&lt;/div&gt;
&lt;p&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;/p&gt;
&lt;div&gt;&lt;figure&gt;&lt;figcaption&gt;&lt;/figcaption&gt;&lt;pre&gt;&lt;code&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;左 -&gt; 根 -&gt; 右&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/figure&gt;&lt;/div&gt;
&lt;p&gt;以上面这棵树为例，中序遍历结果是：&lt;/p&gt;
&lt;div&gt;&lt;figure&gt;&lt;figcaption&gt;&lt;/figcaption&gt;&lt;pre&gt;&lt;code&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;2 -&gt; 4 -&gt; 6 -&gt; 8 -&gt; 10 -&gt; 12 -&gt; 14&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/figure&gt;&lt;/div&gt;
&lt;p&gt;但普通二叉搜索树有一个问题：如果插入数据本身接近有序，就可能退化成链表。&lt;/p&gt;
&lt;p&gt;例如连续插入：&lt;/p&gt;
&lt;div&gt;&lt;figure&gt;&lt;figcaption&gt;&lt;/figcaption&gt;&lt;pre&gt;&lt;code&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;1, 2, 3, 4, 5&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/figure&gt;&lt;/div&gt;
&lt;p&gt;树可能长成这样：&lt;/p&gt;
&lt;div&gt;&lt;figure&gt;&lt;figcaption&gt;&lt;/figcaption&gt;&lt;pre&gt;&lt;code&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;1&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;&lt;span&gt; &lt;/span&gt;&lt;/span&gt;&lt;span&gt;\&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;&lt;span&gt;  &lt;/span&gt;&lt;/span&gt;&lt;span&gt;2&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;&lt;span&gt;   &lt;/span&gt;&lt;/span&gt;&lt;span&gt;\&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;&lt;span&gt;    &lt;/span&gt;&lt;/span&gt;&lt;span&gt;3&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;&lt;span&gt;     &lt;/span&gt;&lt;/span&gt;&lt;span&gt;\&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;&lt;span&gt;      &lt;/span&gt;&lt;/span&gt;&lt;span&gt;4&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;&lt;span&gt;       &lt;/span&gt;&lt;/span&gt;&lt;span&gt;\&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;&lt;span&gt;        &lt;/span&gt;&lt;/span&gt;&lt;span&gt;5&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/figure&gt;&lt;/div&gt;
&lt;p&gt;这时查找复杂度会从 &lt;code dir=&quot;auto&quot;&gt;O(log n)&lt;/code&gt; 退化到 &lt;code dir=&quot;auto&quot;&gt;O(n)&lt;/code&gt;。&lt;/p&gt;
&lt;p&gt;平衡二叉树就是为了解决这个问题。它要求任意节点左右子树高度差不能过大，常见规则是高度差不超过 1。插入或删除节点后，如果树失衡，就通过旋转恢复平衡。&lt;/p&gt;
&lt;p&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;li&gt;右左：先局部右旋，再整体左旋。&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;左左失衡，做一次右旋：&lt;/p&gt;
&lt;div&gt;&lt;figure&gt;&lt;figcaption&gt;&lt;/figcaption&gt;&lt;pre&gt;&lt;code&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;旋转前：&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;
&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;&lt;span&gt;        &lt;/span&gt;&lt;/span&gt;&lt;span&gt;30&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;&lt;span&gt;       &lt;/span&gt;&lt;/span&gt;&lt;span&gt;/&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;&lt;span&gt;     &lt;/span&gt;&lt;/span&gt;&lt;span&gt;20&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;&lt;span&gt;    &lt;/span&gt;&lt;/span&gt;&lt;span&gt;/&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;&lt;span&gt;  &lt;/span&gt;&lt;/span&gt;&lt;span&gt;10&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;
&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;右旋后：&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;
&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;&lt;span&gt;      &lt;/span&gt;&lt;/span&gt;&lt;span&gt;20&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;&lt;span&gt;     &lt;/span&gt;&lt;/span&gt;&lt;span&gt;/  \&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;&lt;span&gt;   &lt;/span&gt;&lt;/span&gt;&lt;span&gt;10    30&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/figure&gt;&lt;/div&gt;
&lt;p&gt;右右失衡，做一次左旋：&lt;/p&gt;
&lt;div&gt;&lt;figure&gt;&lt;figcaption&gt;&lt;/figcaption&gt;&lt;pre&gt;&lt;code&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;旋转前：&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;
&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;&lt;span&gt;  &lt;/span&gt;&lt;/span&gt;&lt;span&gt;10&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;&lt;span&gt;    &lt;/span&gt;&lt;/span&gt;&lt;span&gt;\&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;&lt;span&gt;     &lt;/span&gt;&lt;/span&gt;&lt;span&gt;20&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;&lt;span&gt;       &lt;/span&gt;&lt;/span&gt;&lt;span&gt;\&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;&lt;span&gt;        &lt;/span&gt;&lt;/span&gt;&lt;span&gt;30&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;
&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;左旋后：&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;
&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;&lt;span&gt;      &lt;/span&gt;&lt;/span&gt;&lt;span&gt;20&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;&lt;span&gt;     &lt;/span&gt;&lt;/span&gt;&lt;span&gt;/  \&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;&lt;span&gt;   &lt;/span&gt;&lt;/span&gt;&lt;span&gt;10    30&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/figure&gt;&lt;/div&gt;
&lt;p&gt;左右失衡，先对左子树左旋，再对整体右旋：&lt;/p&gt;
&lt;div&gt;&lt;figure&gt;&lt;figcaption&gt;&lt;/figcaption&gt;&lt;pre&gt;&lt;code&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;旋转前：&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;
&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;&lt;span&gt;        &lt;/span&gt;&lt;/span&gt;&lt;span&gt;30&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;&lt;span&gt;       &lt;/span&gt;&lt;/span&gt;&lt;span&gt;/&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;&lt;span&gt;     &lt;/span&gt;&lt;/span&gt;&lt;span&gt;10&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;&lt;span&gt;       &lt;/span&gt;&lt;/span&gt;&lt;span&gt;\&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;&lt;span&gt;        &lt;/span&gt;&lt;/span&gt;&lt;span&gt;20&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;
&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;先局部左旋：&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;
&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;&lt;span&gt;        &lt;/span&gt;&lt;/span&gt;&lt;span&gt;30&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;&lt;span&gt;       &lt;/span&gt;&lt;/span&gt;&lt;span&gt;/&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;&lt;span&gt;     &lt;/span&gt;&lt;/span&gt;&lt;span&gt;20&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;&lt;span&gt;    &lt;/span&gt;&lt;/span&gt;&lt;span&gt;/&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;&lt;span&gt;  &lt;/span&gt;&lt;/span&gt;&lt;span&gt;10&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;
&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;再整体右旋：&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;
&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;&lt;span&gt;      &lt;/span&gt;&lt;/span&gt;&lt;span&gt;20&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;&lt;span&gt;     &lt;/span&gt;&lt;/span&gt;&lt;span&gt;/  \&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;&lt;span&gt;   &lt;/span&gt;&lt;/span&gt;&lt;span&gt;10    30&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/figure&gt;&lt;/div&gt;
&lt;p&gt;右左失衡，先对右子树右旋，再对整体左旋：&lt;/p&gt;
&lt;div&gt;&lt;figure&gt;&lt;figcaption&gt;&lt;/figcaption&gt;&lt;pre&gt;&lt;code&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;旋转前：&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;
&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;&lt;span&gt;  &lt;/span&gt;&lt;/span&gt;&lt;span&gt;10&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;&lt;span&gt;    &lt;/span&gt;&lt;/span&gt;&lt;span&gt;\&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;&lt;span&gt;     &lt;/span&gt;&lt;/span&gt;&lt;span&gt;30&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;&lt;span&gt;    &lt;/span&gt;&lt;/span&gt;&lt;span&gt;/&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;&lt;span&gt;  &lt;/span&gt;&lt;/span&gt;&lt;span&gt;20&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;
&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;先局部右旋：&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;
&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;&lt;span&gt;  &lt;/span&gt;&lt;/span&gt;&lt;span&gt;10&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;&lt;span&gt;    &lt;/span&gt;&lt;/span&gt;&lt;span&gt;\&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;&lt;span&gt;     &lt;/span&gt;&lt;/span&gt;&lt;span&gt;20&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;&lt;span&gt;       &lt;/span&gt;&lt;/span&gt;&lt;span&gt;\&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;&lt;span&gt;        &lt;/span&gt;&lt;/span&gt;&lt;span&gt;30&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;
&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;再整体左旋：&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;
&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;&lt;span&gt;      &lt;/span&gt;&lt;/span&gt;&lt;span&gt;20&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;&lt;span&gt;     &lt;/span&gt;&lt;/span&gt;&lt;span&gt;/  \&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;&lt;span&gt;   &lt;/span&gt;&lt;/span&gt;&lt;span&gt;10    30&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/figure&gt;&lt;/div&gt;
&lt;p&gt;这几张图可以帮助记忆：&lt;strong&gt;哪边重，就先看它是不是同方向；同方向一次旋转，折线方向两次旋转。&lt;/strong&gt;&lt;/p&gt;
&lt;div&gt;&lt;h2 id=&quot;十二红黑树和-treeset&quot;&gt;十二、红黑树和 TreeSet&lt;/h2&gt;&lt;/div&gt;
&lt;p&gt;红黑树是一种自平衡二叉搜索树。&lt;/p&gt;
&lt;p&gt;它不像严格平衡二叉树那样要求左右高度差非常精确，而是通过节点颜色和一组规则，让树保持“大致平衡”。&lt;/p&gt;
&lt;p&gt;可以简单理解为：&lt;/p&gt;
&lt;div&gt;&lt;figure&gt;&lt;figcaption&gt;&lt;/figcaption&gt;&lt;pre&gt;&lt;code&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;红黑树 = 带颜色规则的二叉搜索树&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/figure&gt;&lt;/div&gt;
&lt;p&gt;它的目标是避免树退化成链表，使查找、插入、删除都能保持在 &lt;code dir=&quot;auto&quot;&gt;O(log n)&lt;/code&gt; 级别。&lt;/p&gt;
&lt;p&gt;Java 中：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code dir=&quot;auto&quot;&gt;TreeMap&lt;/code&gt; 底层是红黑树。&lt;/li&gt;
&lt;li&gt;&lt;code dir=&quot;auto&quot;&gt;TreeSet&lt;/code&gt; 底层通常基于 &lt;code dir=&quot;auto&quot;&gt;TreeMap&lt;/code&gt;。&lt;/li&gt;
&lt;li&gt;&lt;code dir=&quot;auto&quot;&gt;HashMap&lt;/code&gt; 在桶内链表过长时，也可能把链表树化成红黑树。&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;code dir=&quot;auto&quot;&gt;TreeSet&lt;/code&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;li&gt;线程不安全。&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;示例：&lt;/p&gt;
&lt;div&gt;&lt;figure&gt;&lt;figcaption&gt;&lt;/figcaption&gt;&lt;pre&gt;&lt;code&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;Set&lt;/span&gt;&lt;span&gt;&amp;#x3C;&lt;/span&gt;&lt;span&gt;Integer&lt;/span&gt;&lt;span&gt;&gt; &lt;/span&gt;&lt;span&gt;set&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;=&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;new&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;TreeSet&lt;/span&gt;&lt;span&gt;&amp;#x3C;&gt;();&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;set&lt;/span&gt;&lt;span&gt;.&lt;/span&gt;&lt;span&gt;add&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;3&lt;/span&gt;&lt;span&gt;)&lt;/span&gt;&lt;span&gt;;&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;set&lt;/span&gt;&lt;span&gt;.&lt;/span&gt;&lt;span&gt;add&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;1&lt;/span&gt;&lt;span&gt;)&lt;/span&gt;&lt;span&gt;;&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;set&lt;/span&gt;&lt;span&gt;.&lt;/span&gt;&lt;span&gt;add&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;2&lt;/span&gt;&lt;span&gt;)&lt;/span&gt;&lt;span&gt;;&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;
&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;System&lt;/span&gt;&lt;span&gt;.&lt;/span&gt;&lt;span&gt;out&lt;/span&gt;&lt;span&gt;.&lt;/span&gt;&lt;span&gt;println&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;set&lt;/span&gt;&lt;span&gt;)&lt;/span&gt;&lt;span&gt;; &lt;/span&gt;&lt;span&gt;// [1, 2, 3]&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/figure&gt;&lt;/div&gt;
&lt;p&gt;如果放入自定义对象，需要让对象实现 &lt;code dir=&quot;auto&quot;&gt;Comparable&lt;/code&gt;，或者创建 &lt;code dir=&quot;auto&quot;&gt;TreeSet&lt;/code&gt; 时传入 &lt;code dir=&quot;auto&quot;&gt;Comparator&lt;/code&gt;。&lt;/p&gt;
&lt;div&gt;&lt;h2 id=&quot;十三面试回答思路&quot;&gt;十三、面试回答思路&lt;/h2&gt;&lt;/div&gt;
&lt;p&gt;如果面试官问 &lt;code dir=&quot;auto&quot;&gt;HashMap&lt;/code&gt; 原理，可以这样答：&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;&lt;code dir=&quot;auto&quot;&gt;HashMap&lt;/code&gt; 底层是数组加链表或红黑树。put 时先计算 key 的 hash，再通过数组长度计算桶下标。如果没有冲突直接放入；如果有冲突，就在桶内链表或红黑树中比较 key。默认初始容量是 16，负载因子是 0.75，超过阈值会扩容。Java 8 之后，当桶内链表过长并且数组容量满足条件时，链表会树化成红黑树，以优化极端冲突下的查询性能。&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;如果问 &lt;code dir=&quot;auto&quot;&gt;HashSet&lt;/code&gt; 原理，可以这样答：&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;&lt;code dir=&quot;auto&quot;&gt;HashSet&lt;/code&gt; 底层依赖 &lt;code dir=&quot;auto&quot;&gt;HashMap&lt;/code&gt;，元素作为 &lt;code dir=&quot;auto&quot;&gt;HashMap&lt;/code&gt; 的 key 保存，value 使用一个固定占位对象。因此 &lt;code dir=&quot;auto&quot;&gt;HashSet&lt;/code&gt; 不允许重复元素，去重依赖 &lt;code dir=&quot;auto&quot;&gt;hashCode()&lt;/code&gt; 和 &lt;code dir=&quot;auto&quot;&gt;equals()&lt;/code&gt;。&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;如果问 &lt;code dir=&quot;auto&quot;&gt;List&lt;/code&gt; 和 &lt;code dir=&quot;auto&quot;&gt;Set&lt;/code&gt; 区别，可以这样答：&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;&lt;code dir=&quot;auto&quot;&gt;List&lt;/code&gt; 有序、可重复、可按下标访问；&lt;code dir=&quot;auto&quot;&gt;Set&lt;/code&gt; 不允许重复，通常不能按下标访问。&lt;code dir=&quot;auto&quot;&gt;HashSet&lt;/code&gt; 不保证顺序，&lt;code dir=&quot;auto&quot;&gt;LinkedHashSet&lt;/code&gt; 保持插入顺序，&lt;code dir=&quot;auto&quot;&gt;TreeSet&lt;/code&gt; 按比较规则排序。&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;如果问 &lt;code dir=&quot;auto&quot;&gt;TreeSet&lt;/code&gt; 原理，可以这样答：&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;&lt;code dir=&quot;auto&quot;&gt;TreeSet&lt;/code&gt; 底层基于红黑树，元素不能重复，并按自然顺序或比较器排序。它不保留插入顺序，查找、插入、删除通常是 &lt;code dir=&quot;auto&quot;&gt;O(log n)&lt;/code&gt;。&lt;/p&gt;
&lt;/blockquote&gt;
&lt;div&gt;&lt;h2 id=&quot;十四小结&quot;&gt;十四、小结&lt;/h2&gt;&lt;/div&gt;
&lt;p&gt;这几个知识点可以串成一张图：&lt;/p&gt;
&lt;div&gt;&lt;figure&gt;&lt;figcaption&gt;&lt;/figcaption&gt;&lt;pre&gt;&lt;code&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;Collection&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;&lt;span&gt;  &lt;/span&gt;&lt;/span&gt;&lt;span&gt;├── List&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;&lt;span&gt;  &lt;/span&gt;&lt;/span&gt;&lt;span&gt;│   ├── ArrayList&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;&lt;span&gt;  &lt;/span&gt;&lt;/span&gt;&lt;span&gt;│   └── LinkedList&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;&lt;span&gt;  &lt;/span&gt;&lt;/span&gt;&lt;span&gt;│&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;&lt;span&gt;  &lt;/span&gt;&lt;/span&gt;&lt;span&gt;└── Set&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;&lt;span&gt;      &lt;/span&gt;&lt;/span&gt;&lt;span&gt;├── HashSet       -&gt; HashMap -&gt; 哈希表&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;&lt;span&gt;      &lt;/span&gt;&lt;/span&gt;&lt;span&gt;├── LinkedHashSet -&gt; HashMap + 链表顺序&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;&lt;span&gt;      &lt;/span&gt;&lt;/span&gt;&lt;span&gt;└── TreeSet       -&gt; TreeMap -&gt; 红黑树&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;
&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;Map&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;&lt;span&gt;  &lt;/span&gt;&lt;/span&gt;&lt;span&gt;├── HashMap -&gt; 数组 + 链表 + 红黑树&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;&lt;span&gt;  &lt;/span&gt;&lt;/span&gt;&lt;span&gt;└── TreeMap -&gt; 红黑树&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/figure&gt;&lt;/div&gt;
&lt;p&gt;复习时不要只背结论，而要抓住底层结构：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code dir=&quot;auto&quot;&gt;List&lt;/code&gt; 关注顺序和下标。&lt;/li&gt;
&lt;li&gt;&lt;code dir=&quot;auto&quot;&gt;Set&lt;/code&gt; 关注唯一性。&lt;/li&gt;
&lt;li&gt;&lt;code dir=&quot;auto&quot;&gt;HashMap&lt;/code&gt; 关注哈希、冲突、扩容和树化。&lt;/li&gt;
&lt;li&gt;&lt;code dir=&quot;auto&quot;&gt;HashSet&lt;/code&gt; 关注 &lt;code dir=&quot;auto&quot;&gt;HashMap&lt;/code&gt; 的 key 去重。&lt;/li&gt;
&lt;li&gt;&lt;code dir=&quot;auto&quot;&gt;TreeSet&lt;/code&gt; 和 &lt;code dir=&quot;auto&quot;&gt;TreeMap&lt;/code&gt; 关注红黑树排序。&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;这样从使用层一路讲到底层结构，面试回答会更完整。&lt;/p&gt;</content:encoded><category>Java</category><category>集合</category><category>数据结构</category><category>面试</category></item></channel></rss>